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