graphwise 1.1.1 → 1.3.0

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 (147) hide show
  1. package/README.md +149 -2
  2. package/dist/expansion/frontier-balanced.d.ts +12 -0
  3. package/dist/expansion/frontier-balanced.d.ts.map +1 -0
  4. package/dist/expansion/frontier-balanced.unit.test.d.ts +2 -0
  5. package/dist/expansion/frontier-balanced.unit.test.d.ts.map +1 -0
  6. package/dist/expansion/index.d.ts +12 -13
  7. package/dist/expansion/index.d.ts.map +1 -1
  8. package/dist/expansion/random-priority.d.ts +20 -0
  9. package/dist/expansion/random-priority.d.ts.map +1 -0
  10. package/dist/expansion/random-priority.unit.test.d.ts +2 -0
  11. package/dist/expansion/random-priority.unit.test.d.ts.map +1 -0
  12. package/dist/expansion/standard-bfs.d.ts +12 -0
  13. package/dist/expansion/standard-bfs.d.ts.map +1 -0
  14. package/dist/expansion/standard-bfs.unit.test.d.ts +2 -0
  15. package/dist/expansion/standard-bfs.unit.test.d.ts.map +1 -0
  16. package/dist/extraction/index.d.ts +6 -6
  17. package/dist/extraction/index.d.ts.map +1 -1
  18. package/dist/extraction/motif.d.ts.map +1 -1
  19. package/dist/gpu/context.d.ts.map +1 -1
  20. package/dist/gpu/csr.d.ts.map +1 -1
  21. package/dist/gpu/index.cjs +410 -5
  22. package/dist/gpu/index.cjs.map +1 -0
  23. package/dist/gpu/index.d.ts +4 -5
  24. package/dist/gpu/index.d.ts.map +1 -1
  25. package/dist/gpu/index.js +400 -2
  26. package/dist/gpu/index.js.map +1 -0
  27. package/dist/graph/index.cjs +222 -2
  28. package/dist/graph/index.cjs.map +1 -0
  29. package/dist/graph/index.d.ts +3 -3
  30. package/dist/graph/index.d.ts.map +1 -1
  31. package/dist/graph/index.js +221 -1
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index/index.cjs +966 -126
  34. package/dist/index/index.cjs.map +1 -1
  35. package/dist/index/index.js +939 -126
  36. package/dist/index/index.js.map +1 -1
  37. package/dist/{kmeans-B0HEOU6k.cjs → kmeans-87ExSUNZ.js} +27 -13
  38. package/dist/{kmeans-DgbsOznU.js.map → kmeans-87ExSUNZ.js.map} +1 -1
  39. package/dist/{kmeans-DgbsOznU.js → kmeans-BIgSyGKu.cjs} +44 -2
  40. package/dist/{kmeans-B0HEOU6k.cjs.map → kmeans-BIgSyGKu.cjs.map} +1 -1
  41. package/dist/ranking/baselines/betweenness.d.ts +13 -0
  42. package/dist/ranking/baselines/betweenness.d.ts.map +1 -0
  43. package/dist/ranking/baselines/betweenness.unit.test.d.ts +2 -0
  44. package/dist/ranking/baselines/betweenness.unit.test.d.ts.map +1 -0
  45. package/dist/ranking/baselines/communicability.d.ts +13 -0
  46. package/dist/ranking/baselines/communicability.d.ts.map +1 -0
  47. package/dist/ranking/baselines/communicability.unit.test.d.ts +2 -0
  48. package/dist/ranking/baselines/communicability.unit.test.d.ts.map +1 -0
  49. package/dist/ranking/baselines/degree-sum.d.ts +13 -0
  50. package/dist/ranking/baselines/degree-sum.d.ts.map +1 -0
  51. package/dist/ranking/baselines/degree-sum.unit.test.d.ts +2 -0
  52. package/dist/ranking/baselines/degree-sum.unit.test.d.ts.map +1 -0
  53. package/dist/ranking/baselines/index.d.ts +20 -0
  54. package/dist/ranking/baselines/index.d.ts.map +1 -0
  55. package/dist/ranking/baselines/jaccard-arithmetic.d.ts +13 -0
  56. package/dist/ranking/baselines/jaccard-arithmetic.d.ts.map +1 -0
  57. package/dist/ranking/baselines/jaccard-arithmetic.unit.test.d.ts +2 -0
  58. package/dist/ranking/baselines/jaccard-arithmetic.unit.test.d.ts.map +1 -0
  59. package/dist/ranking/baselines/katz.d.ts +13 -0
  60. package/dist/ranking/baselines/katz.d.ts.map +1 -0
  61. package/dist/ranking/baselines/katz.unit.test.d.ts +2 -0
  62. package/dist/ranking/baselines/katz.unit.test.d.ts.map +1 -0
  63. package/dist/ranking/baselines/pagerank.d.ts +13 -0
  64. package/dist/ranking/baselines/pagerank.d.ts.map +1 -0
  65. package/dist/ranking/baselines/pagerank.unit.test.d.ts +2 -0
  66. package/dist/ranking/baselines/pagerank.unit.test.d.ts.map +1 -0
  67. package/dist/ranking/baselines/random-ranking.d.ts +21 -0
  68. package/dist/ranking/baselines/random-ranking.d.ts.map +1 -0
  69. package/dist/ranking/baselines/random-ranking.unit.test.d.ts +2 -0
  70. package/dist/ranking/baselines/random-ranking.unit.test.d.ts.map +1 -0
  71. package/dist/ranking/baselines/resistance-distance.d.ts +13 -0
  72. package/dist/ranking/baselines/resistance-distance.d.ts.map +1 -0
  73. package/dist/ranking/baselines/resistance-distance.unit.test.d.ts +2 -0
  74. package/dist/ranking/baselines/resistance-distance.unit.test.d.ts.map +1 -0
  75. package/dist/ranking/baselines/widest-path.d.ts +13 -0
  76. package/dist/ranking/baselines/widest-path.d.ts.map +1 -0
  77. package/dist/ranking/baselines/widest-path.unit.test.d.ts +2 -0
  78. package/dist/ranking/baselines/widest-path.unit.test.d.ts.map +1 -0
  79. package/dist/ranking/index.d.ts +3 -6
  80. package/dist/ranking/index.d.ts.map +1 -1
  81. package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
  82. package/dist/ranking/mi/adaptive.d.ts +1 -1
  83. package/dist/ranking/mi/adaptive.d.ts.map +1 -1
  84. package/dist/ranking/mi/etch.d.ts.map +1 -1
  85. package/dist/ranking/mi/index.d.ts +9 -9
  86. package/dist/ranking/mi/index.d.ts.map +1 -1
  87. package/dist/ranking/mi/jaccard.d.ts.map +1 -1
  88. package/dist/ranking/mi/notch.d.ts.map +1 -1
  89. package/dist/ranking/mi/scale.d.ts.map +1 -1
  90. package/dist/ranking/mi/skew.d.ts.map +1 -1
  91. package/dist/ranking/mi/span.d.ts.map +1 -1
  92. package/dist/schemas/index.d.ts +2 -2
  93. package/dist/schemas/index.d.ts.map +1 -1
  94. package/dist/seeds/index.cjs +398 -3
  95. package/dist/seeds/index.cjs.map +1 -0
  96. package/dist/seeds/index.d.ts +2 -4
  97. package/dist/seeds/index.d.ts.map +1 -1
  98. package/dist/seeds/index.js +396 -1
  99. package/dist/seeds/index.js.map +1 -0
  100. package/dist/seeds/stratified.d.ts.map +1 -1
  101. package/dist/structures/index.cjs +133 -2
  102. package/dist/structures/index.cjs.map +1 -0
  103. package/dist/structures/index.d.ts +1 -2
  104. package/dist/structures/index.d.ts.map +1 -1
  105. package/dist/structures/index.js +132 -1
  106. package/dist/structures/index.js.map +1 -0
  107. package/dist/traversal/index.cjs +152 -5
  108. package/dist/traversal/index.cjs.map +1 -0
  109. package/dist/traversal/index.d.ts +2 -2
  110. package/dist/traversal/index.d.ts.map +1 -1
  111. package/dist/traversal/index.js +148 -1
  112. package/dist/traversal/index.js.map +1 -0
  113. package/dist/utils/index.cjs +254 -9
  114. package/dist/utils/index.cjs.map +1 -0
  115. package/dist/utils/index.d.ts +4 -3
  116. package/dist/utils/index.d.ts.map +1 -1
  117. package/dist/utils/index.js +242 -3
  118. package/dist/utils/index.js.map +1 -0
  119. package/dist/utils/neighbours.d.ts +54 -0
  120. package/dist/utils/neighbours.d.ts.map +1 -0
  121. package/dist/utils/neighbours.unit.test.d.ts +5 -0
  122. package/dist/utils/neighbours.unit.test.d.ts.map +1 -0
  123. package/package.json +1 -1
  124. package/dist/gpu-BJRVYBjx.cjs +0 -338
  125. package/dist/gpu-BJRVYBjx.cjs.map +0 -1
  126. package/dist/gpu-BveuXugy.js +0 -315
  127. package/dist/gpu-BveuXugy.js.map +0 -1
  128. package/dist/graph-DLWiziLB.js +0 -222
  129. package/dist/graph-DLWiziLB.js.map +0 -1
  130. package/dist/graph-az06J1YV.cjs +0 -227
  131. package/dist/graph-az06J1YV.cjs.map +0 -1
  132. package/dist/seeds-B6J9oJfU.cjs +0 -404
  133. package/dist/seeds-B6J9oJfU.cjs.map +0 -1
  134. package/dist/seeds-UNZxqm_U.js +0 -393
  135. package/dist/seeds-UNZxqm_U.js.map +0 -1
  136. package/dist/structures-BPfhfqNP.js +0 -133
  137. package/dist/structures-BPfhfqNP.js.map +0 -1
  138. package/dist/structures-CJ_S_7fs.cjs +0 -138
  139. package/dist/structures-CJ_S_7fs.cjs.map +0 -1
  140. package/dist/traversal-CQCjUwUJ.js +0 -149
  141. package/dist/traversal-CQCjUwUJ.js.map +0 -1
  142. package/dist/traversal-QeHaNUWn.cjs +0 -172
  143. package/dist/traversal-QeHaNUWn.cjs.map +0 -1
  144. package/dist/utils-Q_akvlMn.js +0 -164
  145. package/dist/utils-Q_akvlMn.js.map +0 -1
  146. package/dist/utils-spZa1ZvS.cjs +0 -205
  147. package/dist/utils-spZa1ZvS.cjs.map +0 -1
@@ -1,10 +1,10 @@
1
- import { t as AdjacencyMapGraph } from "../graph-DLWiziLB.js";
2
- import { i as bfsWithPath, n as dfsWithPath, r as bfs, t as dfs } from "../traversal-CQCjUwUJ.js";
3
- import { t as PriorityQueue } from "../structures-BPfhfqNP.js";
4
- import { n as normaliseFeatures, t as miniBatchKMeans } from "../kmeans-DgbsOznU.js";
5
- import { n as grasp, t as stratified } from "../seeds-UNZxqm_U.js";
6
- import { a as approximateClusteringCoefficient, i as shannonEntropy, n as localTypeEntropy, o as batchClusteringCoefficients, r as normalisedEntropy, s as localClusteringCoefficient, t as entropyFromCounts } from "../utils-Q_akvlMn.js";
7
- import { i as detectWebGPU, n as graphToCSR, r as GPUContext, t as csrToGPUBuffers } from "../gpu-BveuXugy.js";
1
+ import { AdjacencyMapGraph } from "../graph/index.js";
2
+ import { bfs, bfsWithPath, dfs, dfsWithPath } from "../traversal/index.js";
3
+ import { PriorityQueue } from "../structures/index.js";
4
+ import { n as miniBatchKMeans, r as normaliseFeatures, t as _computeMean } from "../kmeans-87ExSUNZ.js";
5
+ import { approximateClusteringCoefficient, batchClusteringCoefficients, countEdgesOfType, countNodesOfType, entropyFromCounts, localClusteringCoefficient, localTypeEntropy, neighbourIntersection, neighbourOverlap, neighbourSet, normalisedEntropy, shannonEntropy } from "../utils/index.js";
6
+ import { grasp, stratified } from "../seeds/index.js";
7
+ import { GPUContext, GPUNotAvailableError, assertWebGPUAvailable, createGPUContext, createResultBuffer, csrToGPUBuffers, detectWebGPU, getGPUContext, graphToCSR, isWebGPUAvailable, readBufferToCPU } from "../gpu/index.js";
8
8
  //#region src/expansion/base.ts
9
9
  /**
10
10
  * Default priority function - degree-ordered (DOME).
@@ -242,6 +242,18 @@ function dome(graph, seeds, config) {
242
242
  priority: domePriority
243
243
  });
244
244
  }
245
+ /**
246
+ * DOME with reverse priority (high degree first).
247
+ */
248
+ function domeHighDegree(graph, seeds, config) {
249
+ const domePriority = (nodeId, context) => {
250
+ return -graph.degree(nodeId);
251
+ };
252
+ return base(graph, seeds, {
253
+ ...config,
254
+ priority: domePriority
255
+ });
256
+ }
245
257
  //#endregion
246
258
  //#region src/expansion/edge.ts
247
259
  /**
@@ -286,15 +298,9 @@ function edge(graph, seeds, config) {
286
298
  */
287
299
  function jaccard(graph, source, target, config) {
288
300
  const { epsilon = 1e-10 } = config ?? {};
289
- const sourceNeighbours = new Set(graph.neighbours(source));
290
- const targetNeighbours = new Set(graph.neighbours(target));
291
- sourceNeighbours.delete(target);
292
- targetNeighbours.delete(source);
293
- let intersectionSize = 0;
294
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
295
- const unionSize = sourceNeighbours.size + targetNeighbours.size - intersectionSize;
296
- if (unionSize === 0) return 0;
297
- const score = intersectionSize / unionSize;
301
+ const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
302
+ if (union === 0) return 0;
303
+ const score = intersection / union;
298
304
  return Math.max(epsilon, score);
299
305
  }
300
306
  //#endregion
@@ -520,6 +526,80 @@ function maze(graph, seeds, config) {
520
526
  });
521
527
  }
522
528
  //#endregion
529
+ //#region src/expansion/standard-bfs.ts
530
+ /**
531
+ * Run standard BFS expansion (FIFO discovery order).
532
+ *
533
+ * @param graph - Source graph
534
+ * @param seeds - Seed nodes for expansion
535
+ * @param config - Expansion configuration
536
+ * @returns Expansion result with discovered paths
537
+ */
538
+ function standardBfs(graph, seeds, config) {
539
+ const bfsPriority = (_nodeId, context) => {
540
+ return context.iteration;
541
+ };
542
+ return base(graph, seeds, {
543
+ ...config,
544
+ priority: bfsPriority
545
+ });
546
+ }
547
+ //#endregion
548
+ //#region src/expansion/frontier-balanced.ts
549
+ /**
550
+ * Run frontier-balanced expansion (round-robin across frontiers).
551
+ *
552
+ * @param graph - Source graph
553
+ * @param seeds - Seed nodes for expansion
554
+ * @param config - Expansion configuration
555
+ * @returns Expansion result with discovered paths
556
+ */
557
+ function frontierBalanced(graph, seeds, config) {
558
+ const balancedPriority = (_nodeId, context) => {
559
+ return context.frontierIndex * 1e9 + context.iteration;
560
+ };
561
+ return base(graph, seeds, {
562
+ ...config,
563
+ priority: balancedPriority
564
+ });
565
+ }
566
+ //#endregion
567
+ //#region src/expansion/random-priority.ts
568
+ /**
569
+ * Deterministic seeded random number generator.
570
+ * Uses FNV-1a-like hash for input → [0, 1] output.
571
+ *
572
+ * @param input - String to hash
573
+ * @param seed - Random seed for reproducibility
574
+ * @returns Deterministic random value in [0, 1]
575
+ */
576
+ function seededRandom$1(input, seed = 0) {
577
+ let h = seed;
578
+ for (let i = 0; i < input.length; i++) {
579
+ h = Math.imul(h ^ input.charCodeAt(i), 2654435769);
580
+ h ^= h >>> 16;
581
+ }
582
+ return (h >>> 0) / 4294967295;
583
+ }
584
+ /**
585
+ * Run random-priority expansion (null hypothesis baseline).
586
+ *
587
+ * @param graph - Source graph
588
+ * @param seeds - Seed nodes for expansion
589
+ * @param config - Expansion configuration
590
+ * @returns Expansion result with discovered paths
591
+ */
592
+ function randomPriority(graph, seeds, config) {
593
+ const { seed = 0 } = config ?? {};
594
+ const randomPriorityFn = (nodeId, context) => {
595
+ return seededRandom$1(nodeId, seed);
596
+ };
597
+ return base(graph, seeds, {
598
+ ...config,
599
+ priority: randomPriorityFn
600
+ });
601
+ }
602
+ //#endregion
523
603
  //#region src/ranking/parse.ts
524
604
  /**
525
605
  * Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
@@ -600,19 +680,14 @@ function computePathSalience(graph, path, mi, epsilon) {
600
680
  */
601
681
  function adamicAdar(graph, source, target, config) {
602
682
  const { epsilon = 1e-10, normalise = true } = config ?? {};
603
- const sourceNeighbours = new Set(graph.neighbours(source));
604
- const targetNeighbours = new Set(graph.neighbours(target));
605
- sourceNeighbours.delete(target);
606
- targetNeighbours.delete(source);
683
+ const commonNeighbours = neighbourIntersection(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
607
684
  let score = 0;
608
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) {
685
+ for (const neighbour of commonNeighbours) {
609
686
  const degree = graph.degree(neighbour);
610
- if (degree > 1) score += 1 / Math.log(degree);
687
+ score += 1 / Math.log(degree + 1);
611
688
  }
612
- if (normalise) {
613
- const commonCount = sourceNeighbours.size < targetNeighbours.size ? sourceNeighbours.size : targetNeighbours.size;
614
- if (commonCount === 0) return 0;
615
- const maxScore = commonCount / Math.log(2);
689
+ if (normalise && commonNeighbours.size > 0) {
690
+ const maxScore = commonNeighbours.size / Math.log(2);
616
691
  score = score / maxScore;
617
692
  }
618
693
  return Math.max(epsilon, score);
@@ -624,22 +699,15 @@ function adamicAdar(graph, source, target, config) {
624
699
  */
625
700
  function scale(graph, source, target, config) {
626
701
  const { epsilon = 1e-10 } = config ?? {};
627
- const sourceNeighbours = new Set(graph.neighbours(source));
628
- const targetNeighbours = new Set(graph.neighbours(target));
629
- sourceNeighbours.delete(target);
630
- targetNeighbours.delete(source);
631
- const sourceDegree = sourceNeighbours.size;
632
- const targetDegree = targetNeighbours.size;
633
- let intersectionSize = 0;
634
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
635
- const unionSize = sourceDegree + targetDegree - intersectionSize;
636
- const jaccard = unionSize > 0 ? intersectionSize / unionSize : 0;
637
- const minDegree = Math.min(sourceDegree, targetDegree);
638
- const maxDegree = Math.max(sourceDegree, targetDegree);
639
- const degreeRatio = maxDegree > 0 ? minDegree / maxDegree : 0;
640
- if (jaccard + degreeRatio === 0) return epsilon;
641
- const score = 2 * jaccard * degreeRatio / (jaccard + degreeRatio);
642
- return Math.max(epsilon, Math.min(1, score));
702
+ const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
703
+ const jaccard = union > 0 ? intersection / union : 0;
704
+ const n = graph.nodeCount;
705
+ const m = graph.edgeCount;
706
+ const densityNormaliser = graph.directed ? n * (n - 1) : 2 * n * (n - 1);
707
+ const density = densityNormaliser > 0 ? m / densityNormaliser : 0;
708
+ if (density === 0) return epsilon;
709
+ const score = jaccard / density;
710
+ return Math.max(epsilon, score);
643
711
  }
644
712
  //#endregion
645
713
  //#region src/ranking/mi/skew.ts
@@ -648,23 +716,15 @@ function scale(graph, source, target, config) {
648
716
  */
649
717
  function skew(graph, source, target, config) {
650
718
  const { epsilon = 1e-10 } = config ?? {};
651
- const sourceNeighbours = new Set(graph.neighbours(source));
652
- const targetNeighbours = new Set(graph.neighbours(target));
653
- sourceNeighbours.delete(target);
654
- targetNeighbours.delete(source);
655
- let weightedIntersection = 0;
656
- let commonCount = 0;
657
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) {
658
- commonCount++;
659
- const degree = graph.degree(neighbour);
660
- if (degree > 1) weightedIntersection += 1 / Math.log(degree);
661
- }
662
- if (commonCount === 0) return epsilon;
663
- const sourceDegree = sourceNeighbours.size;
664
- const targetDegree = targetNeighbours.size;
665
- const maxScore = Math.min(sourceDegree, targetDegree) / Math.log(2);
666
- const score = weightedIntersection / maxScore;
667
- return Math.max(epsilon, Math.min(1, score));
719
+ const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
720
+ const jaccard = union > 0 ? intersection / union : 0;
721
+ const N = graph.nodeCount;
722
+ const sourceDegree = graph.degree(source);
723
+ const targetDegree = graph.degree(target);
724
+ const sourceIdf = Math.log(N / (sourceDegree + 1));
725
+ const targetIdf = Math.log(N / (targetDegree + 1));
726
+ const score = jaccard * sourceIdf * targetIdf;
727
+ return Math.max(epsilon, score);
668
728
  }
669
729
  //#endregion
670
730
  //#region src/ranking/mi/span.ts
@@ -673,21 +733,12 @@ function skew(graph, source, target, config) {
673
733
  */
674
734
  function span(graph, source, target, config) {
675
735
  const { epsilon = 1e-10 } = config ?? {};
676
- const sourceNeighbours = new Set(graph.neighbours(source));
677
- const targetNeighbours = new Set(graph.neighbours(target));
678
- sourceNeighbours.delete(target);
679
- targetNeighbours.delete(source);
680
- const sourceDegree = sourceNeighbours.size;
681
- const targetDegree = targetNeighbours.size;
682
- let intersectionSize = 0;
683
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
684
- const unionSize = sourceDegree + targetDegree - intersectionSize;
685
- const jaccard = unionSize > 0 ? intersectionSize / unionSize : 0;
686
- const maxDegree = Math.max(sourceDegree, targetDegree);
687
- const degreeDiff = Math.abs(sourceDegree - targetDegree);
688
- const degreeSimilarity = maxDegree > 0 ? 1 - degreeDiff / maxDegree : 1;
689
- const score = Math.sqrt(jaccard * degreeSimilarity);
690
- return Math.max(epsilon, Math.min(1, score));
736
+ const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
737
+ const jaccard = union > 0 ? intersection / union : 0;
738
+ const sourceCc = localClusteringCoefficient(graph, source);
739
+ const targetCc = localClusteringCoefficient(graph, target);
740
+ const score = jaccard * (1 - Math.max(sourceCc, targetCc));
741
+ return Math.max(epsilon, score);
691
742
  }
692
743
  //#endregion
693
744
  //#region src/ranking/mi/etch.ts
@@ -696,30 +747,14 @@ function span(graph, source, target, config) {
696
747
  */
697
748
  function etch(graph, source, target, config) {
698
749
  const { epsilon = 1e-10 } = config ?? {};
699
- const sourceNeighbours = new Set(graph.neighbours(source));
700
- const targetNeighbours = new Set(graph.neighbours(target));
701
- sourceNeighbours.delete(target);
702
- targetNeighbours.delete(source);
703
- const commonNeighbours = [];
704
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) commonNeighbours.push(neighbour);
705
- if (commonNeighbours.length < 2) return epsilon;
706
- let jointEdges = 0;
707
- for (let i = 0; i < commonNeighbours.length; i++) for (let j = i + 1; j < commonNeighbours.length; j++) {
708
- const ni = commonNeighbours[i];
709
- const nj = commonNeighbours[j];
710
- if (ni !== void 0 && nj !== void 0 && graph.getEdge(ni, nj) !== void 0) jointEdges++;
711
- }
712
- const maxJointEdges = commonNeighbours.length * (commonNeighbours.length - 1) / 2;
713
- const jointDensity = maxJointEdges > 0 ? jointEdges / maxJointEdges : 0;
714
- let commonEdges = 0;
715
- for (const cn of commonNeighbours) {
716
- if (graph.getEdge(source, cn) !== void 0) commonEdges++;
717
- if (graph.getEdge(target, cn) !== void 0) commonEdges++;
718
- }
719
- const maxCommonEdges = commonNeighbours.length * 2;
720
- const commonDensity = maxCommonEdges > 0 ? commonEdges / maxCommonEdges : 0;
721
- const score = jointDensity * .7 + commonDensity * .3;
722
- return Math.max(epsilon, Math.min(1, score));
750
+ const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
751
+ const jaccard = union > 0 ? intersection / union : 0;
752
+ const edge = graph.getEdge(source, target);
753
+ if (edge?.type === void 0) return Math.max(epsilon, jaccard);
754
+ const edgeTypeCount = countEdgesOfType(graph, edge.type);
755
+ if (edgeTypeCount === 0) return Math.max(epsilon, jaccard);
756
+ const score = jaccard * Math.log(graph.edgeCount / edgeTypeCount);
757
+ return Math.max(epsilon, score);
723
758
  }
724
759
  //#endregion
725
760
  //#region src/ranking/mi/notch.ts
@@ -728,20 +763,18 @@ function etch(graph, source, target, config) {
728
763
  */
729
764
  function notch(graph, source, target, config) {
730
765
  const { epsilon = 1e-10 } = config ?? {};
731
- const sourceNeighbours = new Set(graph.neighbours(source));
732
- const targetNeighbours = new Set(graph.neighbours(target));
733
- sourceNeighbours.delete(target);
734
- targetNeighbours.delete(source);
735
- const sourceDegree = sourceNeighbours.size;
736
- const targetDegree = targetNeighbours.size;
737
- let intersectionSize = 0;
738
- for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
739
- const minDegree = Math.min(sourceDegree, targetDegree);
740
- const overlap = minDegree > 0 ? intersectionSize / minDegree : 0;
741
- const maxDegree = Math.max(sourceDegree, targetDegree);
742
- const correlation = maxDegree > 0 ? 1 - Math.abs(sourceDegree - targetDegree) / maxDegree : 1;
743
- const score = overlap * .6 + correlation * .4;
744
- return Math.max(epsilon, Math.min(1, score));
766
+ const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
767
+ const jaccard = union > 0 ? intersection / union : 0;
768
+ const sourceNode = graph.getNode(source);
769
+ const targetNode = graph.getNode(target);
770
+ if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccard);
771
+ const sourceTypeCount = countNodesOfType(graph, sourceNode.type);
772
+ const targetTypeCount = countNodesOfType(graph, targetNode.type);
773
+ if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccard);
774
+ const sourceRarity = Math.log(graph.nodeCount / sourceTypeCount);
775
+ const targetRarity = Math.log(graph.nodeCount / targetTypeCount);
776
+ const score = jaccard * sourceRarity * targetRarity;
777
+ return Math.max(epsilon, score);
745
778
  }
746
779
  //#endregion
747
780
  //#region src/ranking/mi/adaptive.ts
@@ -749,7 +782,7 @@ function notch(graph, source, target, config) {
749
782
  * Compute unified adaptive MI between two connected nodes.
750
783
  *
751
784
  * Combines structural, degree, and overlap signals with
752
- * adaptive weighting based on graph density.
785
+ * configurable weighting.
753
786
  *
754
787
  * @param graph - Source graph
755
788
  * @param source - Source node ID
@@ -764,17 +797,13 @@ function adaptive(graph, source, target, config) {
764
797
  epsilon,
765
798
  normalise: true
766
799
  });
767
- const sourceNeighbours = new Set(graph.neighbours(source));
768
- const targetNeighbours = new Set(graph.neighbours(target));
769
- sourceNeighbours.delete(target);
770
- targetNeighbours.delete(source);
771
- const sourceDegree = sourceNeighbours.size;
772
- const targetDegree = targetNeighbours.size;
800
+ const sourceNeighbours = neighbourSet(graph, source, target);
801
+ const targetNeighbours = neighbourSet(graph, target, source);
773
802
  let overlap;
774
- if (sourceDegree > 0 && targetDegree > 0) {
775
- let commonCount = 0;
776
- for (const n of sourceNeighbours) if (targetNeighbours.has(n)) commonCount++;
777
- overlap = commonCount / Math.min(sourceDegree, targetDegree);
803
+ if (sourceNeighbours.size > 0 && targetNeighbours.size > 0) {
804
+ const { intersection } = neighbourOverlap(sourceNeighbours, targetNeighbours);
805
+ const minDegree = Math.min(sourceNeighbours.size, targetNeighbours.size);
806
+ overlap = minDegree > 0 ? intersection / minDegree : epsilon;
778
807
  } else overlap = epsilon;
779
808
  const totalWeight = structuralWeight + degreeWeight + overlapWeight;
780
809
  const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
@@ -812,6 +841,712 @@ function shortest(_graph, paths, config) {
812
841
  };
813
842
  }
814
843
  //#endregion
844
+ //#region src/ranking/baselines/degree-sum.ts
845
+ /**
846
+ * Rank paths by sum of node degrees.
847
+ *
848
+ * @param graph - Source graph
849
+ * @param paths - Paths to rank
850
+ * @param config - Configuration options
851
+ * @returns Ranked paths (highest degree-sum first)
852
+ */
853
+ function degreeSum(graph, paths, config) {
854
+ const { includeScores = true } = config ?? {};
855
+ if (paths.length === 0) return {
856
+ paths: [],
857
+ method: "degree-sum"
858
+ };
859
+ const scored = paths.map((path) => {
860
+ let degreeSum = 0;
861
+ for (const nodeId of path.nodes) degreeSum += graph.degree(nodeId);
862
+ return {
863
+ path,
864
+ score: degreeSum
865
+ };
866
+ });
867
+ const maxScore = Math.max(...scored.map((s) => s.score));
868
+ if (maxScore === 0) return {
869
+ paths: paths.map((path) => ({
870
+ ...path,
871
+ score: 0
872
+ })),
873
+ method: "degree-sum"
874
+ };
875
+ return {
876
+ paths: scored.map(({ path, score }) => ({
877
+ ...path,
878
+ score: includeScores ? score / maxScore : score / maxScore
879
+ })).sort((a, b) => b.score - a.score),
880
+ method: "degree-sum"
881
+ };
882
+ }
883
+ //#endregion
884
+ //#region src/ranking/baselines/widest-path.ts
885
+ /**
886
+ * Rank paths by widest bottleneck (minimum edge similarity).
887
+ *
888
+ * @param graph - Source graph
889
+ * @param paths - Paths to rank
890
+ * @param config - Configuration options
891
+ * @returns Ranked paths (highest bottleneck first)
892
+ */
893
+ function widestPath(graph, paths, config) {
894
+ const { includeScores = true } = config ?? {};
895
+ if (paths.length === 0) return {
896
+ paths: [],
897
+ method: "widest-path"
898
+ };
899
+ const scored = paths.map((path) => {
900
+ if (path.nodes.length < 2) return {
901
+ path,
902
+ score: 1
903
+ };
904
+ let minSimilarity = Number.POSITIVE_INFINITY;
905
+ for (let i = 0; i < path.nodes.length - 1; i++) {
906
+ const source = path.nodes[i];
907
+ const target = path.nodes[i + 1];
908
+ if (source === void 0 || target === void 0) continue;
909
+ const edgeSimilarity = jaccard(graph, source, target);
910
+ minSimilarity = Math.min(minSimilarity, edgeSimilarity);
911
+ }
912
+ return {
913
+ path,
914
+ score: minSimilarity === Number.POSITIVE_INFINITY ? 1 : minSimilarity
915
+ };
916
+ });
917
+ const maxScore = Math.max(...scored.map((s) => s.score));
918
+ if (maxScore === 0) return {
919
+ paths: paths.map((path) => ({
920
+ ...path,
921
+ score: 0
922
+ })),
923
+ method: "widest-path"
924
+ };
925
+ return {
926
+ paths: scored.map(({ path, score }) => ({
927
+ ...path,
928
+ score: includeScores ? score / maxScore : score / maxScore
929
+ })).sort((a, b) => b.score - a.score),
930
+ method: "widest-path"
931
+ };
932
+ }
933
+ //#endregion
934
+ //#region src/ranking/baselines/jaccard-arithmetic.ts
935
+ /**
936
+ * Rank paths by arithmetic mean of edge Jaccard similarities.
937
+ *
938
+ * @param graph - Source graph
939
+ * @param paths - Paths to rank
940
+ * @param config - Configuration options
941
+ * @returns Ranked paths (highest arithmetic mean first)
942
+ */
943
+ function jaccardArithmetic(graph, paths, config) {
944
+ const { includeScores = true } = config ?? {};
945
+ if (paths.length === 0) return {
946
+ paths: [],
947
+ method: "jaccard-arithmetic"
948
+ };
949
+ const scored = paths.map((path) => {
950
+ if (path.nodes.length < 2) return {
951
+ path,
952
+ score: 1
953
+ };
954
+ let similaritySum = 0;
955
+ let edgeCount = 0;
956
+ for (let i = 0; i < path.nodes.length - 1; i++) {
957
+ const source = path.nodes[i];
958
+ const target = path.nodes[i + 1];
959
+ if (source === void 0 || target === void 0) continue;
960
+ const edgeSimilarity = jaccard(graph, source, target);
961
+ similaritySum += edgeSimilarity;
962
+ edgeCount++;
963
+ }
964
+ return {
965
+ path,
966
+ score: edgeCount > 0 ? similaritySum / edgeCount : 1
967
+ };
968
+ });
969
+ const maxScore = Math.max(...scored.map((s) => s.score));
970
+ if (maxScore === 0) return {
971
+ paths: paths.map((path) => ({
972
+ ...path,
973
+ score: 0
974
+ })),
975
+ method: "jaccard-arithmetic"
976
+ };
977
+ return {
978
+ paths: scored.map(({ path, score }) => ({
979
+ ...path,
980
+ score: includeScores ? score / maxScore : score / maxScore
981
+ })).sort((a, b) => b.score - a.score),
982
+ method: "jaccard-arithmetic"
983
+ };
984
+ }
985
+ //#endregion
986
+ //#region src/ranking/baselines/pagerank.ts
987
+ /**
988
+ * Compute PageRank centrality for all nodes using power iteration.
989
+ *
990
+ * @param graph - Source graph
991
+ * @param damping - Damping factor (default 0.85)
992
+ * @param tolerance - Convergence tolerance (default 1e-6)
993
+ * @param maxIterations - Maximum iterations (default 100)
994
+ * @returns Map of node ID to PageRank value
995
+ */
996
+ function computePageRank(graph, damping = .85, tolerance = 1e-6, maxIterations = 100) {
997
+ const nodes = Array.from(graph.nodeIds());
998
+ const n = nodes.length;
999
+ if (n === 0) return /* @__PURE__ */ new Map();
1000
+ const ranks = /* @__PURE__ */ new Map();
1001
+ const newRanks = /* @__PURE__ */ new Map();
1002
+ for (const nodeId of nodes) {
1003
+ ranks.set(nodeId, 1 / n);
1004
+ newRanks.set(nodeId, 0);
1005
+ }
1006
+ let isCurrentRanks = true;
1007
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
1008
+ let maxChange = 0;
1009
+ const currMap = isCurrentRanks ? ranks : newRanks;
1010
+ const nextMap = isCurrentRanks ? newRanks : ranks;
1011
+ for (const nodeId of nodes) {
1012
+ let incomingSum = 0;
1013
+ for (const incomingId of graph.neighbours(nodeId, "in")) {
1014
+ const incomingRank = currMap.get(incomingId) ?? 0;
1015
+ const outDegree = graph.degree(incomingId);
1016
+ if (outDegree > 0) incomingSum += incomingRank / outDegree;
1017
+ }
1018
+ const newRank = (1 - damping) / n + damping * incomingSum;
1019
+ nextMap.set(nodeId, newRank);
1020
+ const oldRank = currMap.get(nodeId) ?? 0;
1021
+ maxChange = Math.max(maxChange, Math.abs(newRank - oldRank));
1022
+ }
1023
+ if (maxChange < tolerance) break;
1024
+ isCurrentRanks = !isCurrentRanks;
1025
+ currMap.clear();
1026
+ }
1027
+ return isCurrentRanks ? ranks : newRanks;
1028
+ }
1029
+ /**
1030
+ * Rank paths by sum of PageRank scores.
1031
+ *
1032
+ * @param graph - Source graph
1033
+ * @param paths - Paths to rank
1034
+ * @param config - Configuration options
1035
+ * @returns Ranked paths (highest PageRank sum first)
1036
+ */
1037
+ function pagerank(graph, paths, config) {
1038
+ const { includeScores = true } = config ?? {};
1039
+ if (paths.length === 0) return {
1040
+ paths: [],
1041
+ method: "pagerank"
1042
+ };
1043
+ const ranks = computePageRank(graph);
1044
+ const scored = paths.map((path) => {
1045
+ let prSum = 0;
1046
+ for (const nodeId of path.nodes) prSum += ranks.get(nodeId) ?? 0;
1047
+ return {
1048
+ path,
1049
+ score: prSum
1050
+ };
1051
+ });
1052
+ const maxScore = Math.max(...scored.map((s) => s.score));
1053
+ if (maxScore === 0) return {
1054
+ paths: paths.map((path) => ({
1055
+ ...path,
1056
+ score: 0
1057
+ })),
1058
+ method: "pagerank"
1059
+ };
1060
+ return {
1061
+ paths: scored.map(({ path, score }) => ({
1062
+ ...path,
1063
+ score: includeScores ? score / maxScore : score / maxScore
1064
+ })).sort((a, b) => b.score - a.score),
1065
+ method: "pagerank"
1066
+ };
1067
+ }
1068
+ //#endregion
1069
+ //#region src/ranking/baselines/betweenness.ts
1070
+ /**
1071
+ * Compute betweenness centrality for all nodes using Brandes algorithm.
1072
+ *
1073
+ * @param graph - Source graph
1074
+ * @returns Map of node ID to betweenness value
1075
+ */
1076
+ function computeBetweenness(graph) {
1077
+ const nodes = Array.from(graph.nodeIds());
1078
+ const betweenness = /* @__PURE__ */ new Map();
1079
+ for (const nodeId of nodes) betweenness.set(nodeId, 0);
1080
+ for (const source of nodes) {
1081
+ const predecessors = /* @__PURE__ */ new Map();
1082
+ const distance = /* @__PURE__ */ new Map();
1083
+ const sigma = /* @__PURE__ */ new Map();
1084
+ const queue = [];
1085
+ for (const nodeId of nodes) {
1086
+ predecessors.set(nodeId, []);
1087
+ distance.set(nodeId, -1);
1088
+ sigma.set(nodeId, 0);
1089
+ }
1090
+ distance.set(source, 0);
1091
+ sigma.set(source, 1);
1092
+ queue.push(source);
1093
+ for (const v of queue) {
1094
+ const vDist = distance.get(v) ?? -1;
1095
+ const neighbours = graph.neighbours(v);
1096
+ for (const w of neighbours) {
1097
+ const wDist = distance.get(w) ?? -1;
1098
+ if (wDist < 0) {
1099
+ distance.set(w, vDist + 1);
1100
+ queue.push(w);
1101
+ }
1102
+ if (wDist === vDist + 1) {
1103
+ const wSigma = sigma.get(w) ?? 0;
1104
+ const vSigma = sigma.get(v) ?? 0;
1105
+ sigma.set(w, wSigma + vSigma);
1106
+ const wPred = predecessors.get(w) ?? [];
1107
+ wPred.push(v);
1108
+ predecessors.set(w, wPred);
1109
+ }
1110
+ }
1111
+ }
1112
+ const delta = /* @__PURE__ */ new Map();
1113
+ for (const nodeId of nodes) delta.set(nodeId, 0);
1114
+ const sorted = [...nodes].sort((a, b) => {
1115
+ const aD = distance.get(a) ?? -1;
1116
+ return (distance.get(b) ?? -1) - aD;
1117
+ });
1118
+ for (const w of sorted) {
1119
+ if (w === source) continue;
1120
+ const wDelta = delta.get(w) ?? 0;
1121
+ const wSigma = sigma.get(w) ?? 0;
1122
+ const wPred = predecessors.get(w) ?? [];
1123
+ for (const v of wPred) {
1124
+ const vSigma = sigma.get(v) ?? 0;
1125
+ const vDelta = delta.get(v) ?? 0;
1126
+ if (wSigma > 0) delta.set(v, vDelta + vSigma / wSigma * (1 + wDelta));
1127
+ }
1128
+ if (w !== source) {
1129
+ const current = betweenness.get(w) ?? 0;
1130
+ betweenness.set(w, current + wDelta);
1131
+ }
1132
+ }
1133
+ }
1134
+ return betweenness;
1135
+ }
1136
+ /**
1137
+ * Rank paths by sum of betweenness scores.
1138
+ *
1139
+ * @param graph - Source graph
1140
+ * @param paths - Paths to rank
1141
+ * @param config - Configuration options
1142
+ * @returns Ranked paths (highest betweenness sum first)
1143
+ */
1144
+ function betweenness(graph, paths, config) {
1145
+ const { includeScores = true } = config ?? {};
1146
+ if (paths.length === 0) return {
1147
+ paths: [],
1148
+ method: "betweenness"
1149
+ };
1150
+ const bcMap = computeBetweenness(graph);
1151
+ const scored = paths.map((path) => {
1152
+ let bcSum = 0;
1153
+ for (const nodeId of path.nodes) bcSum += bcMap.get(nodeId) ?? 0;
1154
+ return {
1155
+ path,
1156
+ score: bcSum
1157
+ };
1158
+ });
1159
+ const maxScore = Math.max(...scored.map((s) => s.score));
1160
+ if (maxScore === 0) return {
1161
+ paths: paths.map((path) => ({
1162
+ ...path,
1163
+ score: 0
1164
+ })),
1165
+ method: "betweenness"
1166
+ };
1167
+ return {
1168
+ paths: scored.map(({ path, score }) => ({
1169
+ ...path,
1170
+ score: includeScores ? score / maxScore : score / maxScore
1171
+ })).sort((a, b) => b.score - a.score),
1172
+ method: "betweenness"
1173
+ };
1174
+ }
1175
+ //#endregion
1176
+ //#region src/ranking/baselines/katz.ts
1177
+ /**
1178
+ * Compute truncated Katz centrality between two nodes.
1179
+ *
1180
+ * Uses iterative matrix-vector products to avoid full matrix powers.
1181
+ * score(s,t) = sum_{k=1}^{K} beta^k * walks_k(s,t)
1182
+ *
1183
+ * @param graph - Source graph
1184
+ * @param source - Source node ID
1185
+ * @param target - Target node ID
1186
+ * @param k - Truncation depth (default 5)
1187
+ * @param beta - Attenuation factor (default 0.005)
1188
+ * @returns Katz score
1189
+ */
1190
+ function computeKatz(graph, source, target, k = 5, beta = .005) {
1191
+ const nodes = Array.from(graph.nodeIds());
1192
+ const nodeToIdx = /* @__PURE__ */ new Map();
1193
+ nodes.forEach((nodeId, idx) => {
1194
+ nodeToIdx.set(nodeId, idx);
1195
+ });
1196
+ const n = nodes.length;
1197
+ if (n === 0) return 0;
1198
+ const sourceIdx = nodeToIdx.get(source);
1199
+ const targetIdx = nodeToIdx.get(target);
1200
+ if (sourceIdx === void 0 || targetIdx === void 0) return 0;
1201
+ let walks = new Float64Array(n);
1202
+ walks[targetIdx] = 1;
1203
+ let katzScore = 0;
1204
+ for (let depth = 1; depth <= k; depth++) {
1205
+ const walksNext = new Float64Array(n);
1206
+ for (const sourceNode of nodes) {
1207
+ const srcIdx = nodeToIdx.get(sourceNode);
1208
+ if (srcIdx === void 0) continue;
1209
+ const neighbours = graph.neighbours(sourceNode);
1210
+ for (const neighbourId of neighbours) {
1211
+ const nIdx = nodeToIdx.get(neighbourId);
1212
+ if (nIdx === void 0) continue;
1213
+ walksNext[srcIdx] = (walksNext[srcIdx] ?? 0) + (walks[nIdx] ?? 0);
1214
+ }
1215
+ }
1216
+ const walkCount = walksNext[sourceIdx] ?? 0;
1217
+ katzScore += Math.pow(beta, depth) * walkCount;
1218
+ walks = walksNext;
1219
+ }
1220
+ return katzScore;
1221
+ }
1222
+ /**
1223
+ * Rank paths by Katz centrality between endpoints.
1224
+ *
1225
+ * @param graph - Source graph
1226
+ * @param paths - Paths to rank
1227
+ * @param config - Configuration options
1228
+ * @returns Ranked paths (highest Katz score first)
1229
+ */
1230
+ function katz(graph, paths, config) {
1231
+ const { includeScores = true } = config ?? {};
1232
+ if (paths.length === 0) return {
1233
+ paths: [],
1234
+ method: "katz"
1235
+ };
1236
+ const scored = paths.map((path) => {
1237
+ const source = path.nodes[0];
1238
+ const target = path.nodes[path.nodes.length - 1];
1239
+ if (source === void 0 || target === void 0) return {
1240
+ path,
1241
+ score: 0
1242
+ };
1243
+ return {
1244
+ path,
1245
+ score: computeKatz(graph, source, target)
1246
+ };
1247
+ });
1248
+ const maxScore = Math.max(...scored.map((s) => s.score));
1249
+ if (maxScore === 0) return {
1250
+ paths: paths.map((path) => ({
1251
+ ...path,
1252
+ score: 0
1253
+ })),
1254
+ method: "katz"
1255
+ };
1256
+ return {
1257
+ paths: scored.map(({ path, score }) => ({
1258
+ ...path,
1259
+ score: includeScores ? score / maxScore : score / maxScore
1260
+ })).sort((a, b) => b.score - a.score),
1261
+ method: "katz"
1262
+ };
1263
+ }
1264
+ //#endregion
1265
+ //#region src/ranking/baselines/communicability.ts
1266
+ /**
1267
+ * Compute truncated communicability between two nodes.
1268
+ *
1269
+ * Uses Taylor series expansion: (e^A)_{s,t} ≈ sum_{k=0}^{K} A^k_{s,t} / k!
1270
+ *
1271
+ * @param graph - Source graph
1272
+ * @param source - Source node ID
1273
+ * @param target - Target node ID
1274
+ * @param k - Truncation depth (default 15)
1275
+ * @returns Communicability score
1276
+ */
1277
+ function computeCommunicability(graph, source, target, k = 15) {
1278
+ const nodes = Array.from(graph.nodeIds());
1279
+ const nodeToIdx = /* @__PURE__ */ new Map();
1280
+ nodes.forEach((nodeId, idx) => {
1281
+ nodeToIdx.set(nodeId, idx);
1282
+ });
1283
+ const n = nodes.length;
1284
+ if (n === 0) return 0;
1285
+ const sourceIdx = nodeToIdx.get(source);
1286
+ const targetIdx = nodeToIdx.get(target);
1287
+ if (sourceIdx === void 0 || targetIdx === void 0) return 0;
1288
+ let walks = new Float64Array(n);
1289
+ walks[targetIdx] = 1;
1290
+ let commScore = walks[sourceIdx] ?? 0;
1291
+ let factorial = 1;
1292
+ for (let depth = 1; depth <= k; depth++) {
1293
+ const walksNext = new Float64Array(n);
1294
+ for (const fromNode of nodes) {
1295
+ const fromIdx = nodeToIdx.get(fromNode);
1296
+ if (fromIdx === void 0) continue;
1297
+ const neighbours = graph.neighbours(fromNode);
1298
+ for (const toNodeId of neighbours) {
1299
+ const toIdx = nodeToIdx.get(toNodeId);
1300
+ if (toIdx === void 0) continue;
1301
+ walksNext[fromIdx] = (walksNext[fromIdx] ?? 0) + (walks[toIdx] ?? 0);
1302
+ }
1303
+ }
1304
+ factorial *= depth;
1305
+ commScore += (walksNext[sourceIdx] ?? 0) / factorial;
1306
+ walks = walksNext;
1307
+ }
1308
+ return commScore;
1309
+ }
1310
+ /**
1311
+ * Rank paths by communicability between endpoints.
1312
+ *
1313
+ * @param graph - Source graph
1314
+ * @param paths - Paths to rank
1315
+ * @param config - Configuration options
1316
+ * @returns Ranked paths (highest communicability first)
1317
+ */
1318
+ function communicability(graph, paths, config) {
1319
+ const { includeScores = true } = config ?? {};
1320
+ if (paths.length === 0) return {
1321
+ paths: [],
1322
+ method: "communicability"
1323
+ };
1324
+ const scored = paths.map((path) => {
1325
+ const source = path.nodes[0];
1326
+ const target = path.nodes[path.nodes.length - 1];
1327
+ if (source === void 0 || target === void 0) return {
1328
+ path,
1329
+ score: 0
1330
+ };
1331
+ return {
1332
+ path,
1333
+ score: computeCommunicability(graph, source, target)
1334
+ };
1335
+ });
1336
+ const maxScore = Math.max(...scored.map((s) => s.score));
1337
+ if (maxScore === 0) return {
1338
+ paths: paths.map((path) => ({
1339
+ ...path,
1340
+ score: 0
1341
+ })),
1342
+ method: "communicability"
1343
+ };
1344
+ return {
1345
+ paths: scored.map(({ path, score }) => ({
1346
+ ...path,
1347
+ score: includeScores ? score / maxScore : score / maxScore
1348
+ })).sort((a, b) => b.score - a.score),
1349
+ method: "communicability"
1350
+ };
1351
+ }
1352
+ //#endregion
1353
+ //#region src/ranking/baselines/resistance-distance.ts
1354
+ /**
1355
+ * Compute effective resistance between two nodes via Laplacian pseudoinverse.
1356
+ *
1357
+ * Resistance = L^+_{s,s} + L^+_{t,t} - 2*L^+_{s,t}
1358
+ * where L^+ is the pseudoinverse of the Laplacian matrix.
1359
+ *
1360
+ * @param graph - Source graph
1361
+ * @param source - Source node ID
1362
+ * @param target - Target node ID
1363
+ * @returns Effective resistance
1364
+ */
1365
+ function computeResistance(graph, source, target) {
1366
+ const nodes = Array.from(graph.nodeIds());
1367
+ const nodeToIdx = /* @__PURE__ */ new Map();
1368
+ nodes.forEach((nodeId, idx) => {
1369
+ nodeToIdx.set(nodeId, idx);
1370
+ });
1371
+ const n = nodes.length;
1372
+ if (n === 0 || n > 5e3) throw new Error(`Cannot compute resistance distance: graph too large (${String(n)} nodes). Maximum 5000.`);
1373
+ const sourceIdx = nodeToIdx.get(source);
1374
+ const targetIdx = nodeToIdx.get(target);
1375
+ if (sourceIdx === void 0 || targetIdx === void 0) return 0;
1376
+ const L = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
1377
+ for (let i = 0; i < n; i++) {
1378
+ const nodeId = nodes[i];
1379
+ if (nodeId === void 0) continue;
1380
+ const degree = graph.degree(nodeId);
1381
+ const row = L[i];
1382
+ if (row !== void 0) row[i] = degree;
1383
+ const neighbours = graph.neighbours(nodeId);
1384
+ for (const neighbourId of neighbours) {
1385
+ const j = nodeToIdx.get(neighbourId);
1386
+ if (j !== void 0 && row !== void 0) row[j] = -1;
1387
+ }
1388
+ }
1389
+ const Lpinv = pinv(L);
1390
+ const resistance = (Lpinv[sourceIdx]?.[sourceIdx] ?? 0) + (Lpinv[targetIdx]?.[targetIdx] ?? 0) - 2 * (Lpinv[sourceIdx]?.[targetIdx] ?? 0);
1391
+ return Math.max(resistance, 1e-10);
1392
+ }
1393
+ /**
1394
+ * Compute Moore-Penrose pseudoinverse of a matrix.
1395
+ * Simplified implementation for small dense matrices.
1396
+ *
1397
+ * @param A - Square matrix
1398
+ * @returns Pseudoinverse A^+
1399
+ */
1400
+ function pinv(A) {
1401
+ const n = A.length;
1402
+ if (n === 0) return [];
1403
+ const M = A.map((row) => [...row]);
1404
+ const epsilon = 1e-10;
1405
+ for (let i = 0; i < n; i++) {
1406
+ const row = M[i];
1407
+ if (row !== void 0) row[i] = (row[i] ?? 0) + epsilon;
1408
+ }
1409
+ return gaussianInverse(M);
1410
+ }
1411
+ /**
1412
+ * Compute matrix inverse using Gaussian elimination with partial pivoting.
1413
+ *
1414
+ * @param A - Matrix to invert
1415
+ * @returns Inverted matrix
1416
+ */
1417
+ function gaussianInverse(A) {
1418
+ const n = A.length;
1419
+ const aug = A.map((row, i) => {
1420
+ const identity = Array.from({ length: n }, (_, j) => i === j ? 1 : 0);
1421
+ return [...row, ...identity];
1422
+ });
1423
+ for (let col = 0; col < n; col++) {
1424
+ let maxRow = col;
1425
+ for (let row = col + 1; row < n; row++) {
1426
+ const currentRow = aug[row];
1427
+ const maxRowRef = aug[maxRow];
1428
+ if (currentRow !== void 0 && maxRowRef !== void 0 && Math.abs(currentRow[col] ?? 0) > Math.abs(maxRowRef[col] ?? 0)) maxRow = row;
1429
+ }
1430
+ const currentCol = aug[col];
1431
+ const maxRowAug = aug[maxRow];
1432
+ if (currentCol !== void 0 && maxRowAug !== void 0) {
1433
+ aug[col] = maxRowAug;
1434
+ aug[maxRow] = currentCol;
1435
+ }
1436
+ const pivotRow = aug[col];
1437
+ const pivot = pivotRow?.[col];
1438
+ if (pivot === void 0 || Math.abs(pivot) < 1e-12) continue;
1439
+ if (pivotRow !== void 0) for (let j = col; j < 2 * n; j++) pivotRow[j] = (pivotRow[j] ?? 0) / pivot;
1440
+ for (let row = 0; row < n; row++) {
1441
+ if (row === col) continue;
1442
+ const eliminationRow = aug[row];
1443
+ const factor = eliminationRow?.[col] ?? 0;
1444
+ if (eliminationRow !== void 0 && pivotRow !== void 0) for (let j = col; j < 2 * n; j++) eliminationRow[j] = (eliminationRow[j] ?? 0) - factor * (pivotRow[j] ?? 0);
1445
+ }
1446
+ }
1447
+ const Ainv = [];
1448
+ for (let i = 0; i < n; i++) Ainv[i] = (aug[i]?.slice(n) ?? []).map((v) => v);
1449
+ return Ainv;
1450
+ }
1451
+ /**
1452
+ * Rank paths by reciprocal of resistance distance between endpoints.
1453
+ *
1454
+ * @param graph - Source graph
1455
+ * @param paths - Paths to rank
1456
+ * @param config - Configuration options
1457
+ * @returns Ranked paths (highest conductance first)
1458
+ */
1459
+ function resistanceDistance(graph, paths, config) {
1460
+ const { includeScores = true } = config ?? {};
1461
+ if (paths.length === 0) return {
1462
+ paths: [],
1463
+ method: "resistance-distance"
1464
+ };
1465
+ const nodeCount = Array.from(graph.nodeIds()).length;
1466
+ if (nodeCount > 5e3) throw new Error(`Cannot rank paths: graph too large (${String(nodeCount)} nodes). Resistance distance requires O(n^3) computation; maximum 5000 nodes.`);
1467
+ const scored = paths.map((path) => {
1468
+ const source = path.nodes[0];
1469
+ const target = path.nodes[path.nodes.length - 1];
1470
+ if (source === void 0 || target === void 0) return {
1471
+ path,
1472
+ score: 0
1473
+ };
1474
+ return {
1475
+ path,
1476
+ score: 1 / computeResistance(graph, source, target)
1477
+ };
1478
+ });
1479
+ const maxScore = Math.max(...scored.map((s) => s.score));
1480
+ if (maxScore === 0) return {
1481
+ paths: paths.map((path) => ({
1482
+ ...path,
1483
+ score: 0
1484
+ })),
1485
+ method: "resistance-distance"
1486
+ };
1487
+ return {
1488
+ paths: scored.map(({ path, score }) => ({
1489
+ ...path,
1490
+ score: includeScores ? score / maxScore : score / maxScore
1491
+ })).sort((a, b) => b.score - a.score),
1492
+ method: "resistance-distance"
1493
+ };
1494
+ }
1495
+ //#endregion
1496
+ //#region src/ranking/baselines/random-ranking.ts
1497
+ /**
1498
+ * Deterministic seeded random number generator.
1499
+ * Uses FNV-1a-like hash for input → [0, 1] output.
1500
+ *
1501
+ * @param input - String to hash
1502
+ * @param seed - Random seed for reproducibility
1503
+ * @returns Deterministic random value in [0, 1]
1504
+ */
1505
+ function seededRandom(input, seed = 0) {
1506
+ let h = seed;
1507
+ for (let i = 0; i < input.length; i++) {
1508
+ h = Math.imul(h ^ input.charCodeAt(i), 2654435769);
1509
+ h ^= h >>> 16;
1510
+ }
1511
+ return (h >>> 0) / 4294967295;
1512
+ }
1513
+ /**
1514
+ * Rank paths randomly (null hypothesis baseline).
1515
+ *
1516
+ * @param _graph - Source graph (unused)
1517
+ * @param paths - Paths to rank
1518
+ * @param config - Configuration options
1519
+ * @returns Ranked paths (randomly ordered)
1520
+ */
1521
+ function randomRanking(_graph, paths, config) {
1522
+ const { includeScores = true, seed = 0 } = config ?? {};
1523
+ if (paths.length === 0) return {
1524
+ paths: [],
1525
+ method: "random"
1526
+ };
1527
+ const scored = paths.map((path) => {
1528
+ return {
1529
+ path,
1530
+ score: seededRandom(path.nodes.join(","), seed)
1531
+ };
1532
+ });
1533
+ const maxScore = Math.max(...scored.map((s) => s.score));
1534
+ if (maxScore === 0) return {
1535
+ paths: paths.map((path) => ({
1536
+ ...path,
1537
+ score: 0
1538
+ })),
1539
+ method: "random"
1540
+ };
1541
+ return {
1542
+ paths: scored.map(({ path, score }) => ({
1543
+ ...path,
1544
+ score: includeScores ? score / maxScore : score / maxScore
1545
+ })).sort((a, b) => b.score - a.score),
1546
+ method: "random"
1547
+ };
1548
+ }
1549
+ //#endregion
815
1550
  //#region src/extraction/ego-network.ts
816
1551
  /**
817
1552
  * Extract the ego-network (k-hop neighbourhood) of a centre node.
@@ -1022,6 +1757,82 @@ function extractKTruss(graph, k) {
1022
1757
  }
1023
1758
  return result;
1024
1759
  }
1760
+ /**
1761
+ * Compute the truss number for each edge.
1762
+ *
1763
+ * The truss number of an edge is the largest k such that the edge
1764
+ * belongs to the k-truss.
1765
+ *
1766
+ * @param graph - The source graph
1767
+ * @returns Map from edge key (canonical "u::v") to truss number
1768
+ *
1769
+ * @example
1770
+ * ```typescript
1771
+ * const trussNumbers = computeTrussNumbers(graph);
1772
+ * const edgeKey = 'A::B'; // where A < B lexicographically
1773
+ * console.log(`Edge A-B is in the ${trussNumbers.get(edgeKey)}-truss`);
1774
+ * ```
1775
+ */
1776
+ function computeTrussNumbers(graph) {
1777
+ const adjacency = /* @__PURE__ */ new Map();
1778
+ const edgeData = /* @__PURE__ */ new Map();
1779
+ const remainingEdges = /* @__PURE__ */ new Set();
1780
+ for (const nodeId of graph.nodeIds()) adjacency.set(nodeId, /* @__PURE__ */ new Set());
1781
+ for (const edge of graph.edges()) {
1782
+ const { source, target } = edge;
1783
+ adjacency.get(source)?.add(target);
1784
+ adjacency.get(target)?.add(source);
1785
+ const key = source < target ? `${source}::${target}` : `${target}::${source}`;
1786
+ edgeData.set(key, edge);
1787
+ remainingEdges.add(key);
1788
+ }
1789
+ const triangleCounts = /* @__PURE__ */ new Map();
1790
+ for (const key of remainingEdges) {
1791
+ const edge = edgeData.get(key);
1792
+ if (edge !== void 0) triangleCounts.set(key, countEdgeTriangles(graph, edge.source, edge.target));
1793
+ }
1794
+ const trussNumbers = /* @__PURE__ */ new Map();
1795
+ const edgesByTriangleCount = /* @__PURE__ */ new Map();
1796
+ for (const [key, count] of triangleCounts) {
1797
+ if (!edgesByTriangleCount.has(count)) edgesByTriangleCount.set(count, /* @__PURE__ */ new Set());
1798
+ edgesByTriangleCount.get(count)?.add(key);
1799
+ }
1800
+ const sortedCounts = [...edgesByTriangleCount.keys()].sort((a, b) => a - b);
1801
+ for (const currentCount of sortedCounts) {
1802
+ const bucket = edgesByTriangleCount.get(currentCount);
1803
+ if (bucket === void 0) continue;
1804
+ while (bucket.size > 0) {
1805
+ const edgeKey = bucket.values().next().value;
1806
+ if (edgeKey === void 0) break;
1807
+ bucket.delete(edgeKey);
1808
+ if (!remainingEdges.has(edgeKey)) continue;
1809
+ const trussNumber = currentCount + 2;
1810
+ trussNumbers.set(edgeKey, trussNumber);
1811
+ remainingEdges.delete(edgeKey);
1812
+ const edge = edgeData.get(edgeKey);
1813
+ if (edge === void 0) continue;
1814
+ const { source, target } = edge;
1815
+ adjacency.get(source)?.delete(target);
1816
+ adjacency.get(target)?.delete(source);
1817
+ const sourceNeighbours = adjacency.get(source);
1818
+ if (sourceNeighbours !== void 0) {
1819
+ for (const w of adjacency.get(target) ?? []) if (sourceNeighbours.has(w)) {
1820
+ const keySw = source < w ? `${source}::${w}` : `${w}::${source}`;
1821
+ const keyTw = target < w ? `${target}::${w}` : `${w}::${target}`;
1822
+ for (const keyToUpdate of [keySw, keyTw]) if (remainingEdges.has(keyToUpdate)) {
1823
+ const oldCount = triangleCounts.get(keyToUpdate) ?? 0;
1824
+ const newCount = oldCount - 1;
1825
+ triangleCounts.set(keyToUpdate, newCount);
1826
+ edgesByTriangleCount.get(oldCount)?.delete(keyToUpdate);
1827
+ if (!edgesByTriangleCount.has(newCount)) edgesByTriangleCount.set(newCount, /* @__PURE__ */ new Set());
1828
+ edgesByTriangleCount.get(newCount)?.add(keyToUpdate);
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ }
1834
+ return trussNumbers;
1835
+ }
1025
1836
  //#endregion
1026
1837
  //#region src/extraction/motif.ts
1027
1838
  /**
@@ -1132,10 +1943,11 @@ function enumerate3NodeMotifs(graph, includeInstances) {
1132
1943
  }
1133
1944
  }
1134
1945
  }
1135
- return {
1946
+ if (instances !== void 0) return {
1136
1947
  counts,
1137
1948
  instances
1138
1949
  };
1950
+ return { counts };
1139
1951
  }
1140
1952
  /**
1141
1953
  * Enumerate all 4-node motifs in the graph.
@@ -1203,10 +2015,11 @@ function enumerate4NodeMotifs(graph, includeInstances) {
1203
2015
  }
1204
2016
  }
1205
2017
  }
1206
- return {
2018
+ if (instances !== void 0) return {
1207
2019
  counts,
1208
2020
  instances
1209
2021
  };
2022
+ return { counts };
1210
2023
  }
1211
2024
  /**
1212
2025
  * Human-readable names for common 3-node motifs.
@@ -1351,6 +2164,6 @@ function filterSubgraph(graph, options) {
1351
2164
  return result;
1352
2165
  }
1353
2166
  //#endregion
1354
- export { AdjacencyMapGraph, GPUContext, PriorityQueue, adamicAdar, adaptive, approximateClusteringCoefficient, base, batchClusteringCoefficients, bfs, bfsWithPath, csrToGPUBuffers, detectWebGPU, dfs, dfsWithPath, dome, edge, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, getMotifName, graphToCSR, grasp, hae, jaccard, localClusteringCoefficient, localTypeEntropy, maze, miniBatchKMeans, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, parse, pipe, reach, sage, scale, shannonEntropy, shortest, skew, span, stratified };
2167
+ export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adaptive, approximateClusteringCoefficient, assertWebGPUAvailable, base, batchClusteringCoefficients, betweenness, bfs, bfsWithPath, communicability, computeTrussNumbers, countEdgesOfType, countNodesOfType, createGPUContext, createResultBuffer, csrToGPUBuffers, degreeSum, detectWebGPU, dfs, dfsWithPath, dome, domeHighDegree, edge, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, frontierBalanced, getGPUContext, getMotifName, graphToCSR, grasp, hae, isWebGPUAvailable, jaccard, jaccardArithmetic, katz, localClusteringCoefficient, localTypeEntropy, maze, miniBatchKMeans, neighbourIntersection, neighbourOverlap, neighbourSet, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, pagerank, parse, pipe, randomPriority, randomRanking, reach, readBufferToCPU, resistanceDistance, sage, scale, shannonEntropy, shortest, skew, span, standardBfs, stratified, widestPath };
1355
2168
 
1356
2169
  //# sourceMappingURL=index.js.map