graphwise 1.4.2 → 1.5.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 (57) hide show
  1. package/README.md +38 -0
  2. package/dist/__test__/fixtures/index.d.ts +1 -0
  3. package/dist/__test__/fixtures/index.d.ts.map +1 -1
  4. package/dist/__test__/fixtures/metrics.d.ts +86 -0
  5. package/dist/__test__/fixtures/metrics.d.ts.map +1 -0
  6. package/dist/__test__/fixtures/metrics.unit.test.d.ts +7 -0
  7. package/dist/__test__/fixtures/metrics.unit.test.d.ts.map +1 -0
  8. package/dist/expansion/dfs-priority.d.ts +23 -0
  9. package/dist/expansion/dfs-priority.d.ts.map +1 -0
  10. package/dist/expansion/dfs-priority.unit.test.d.ts +2 -0
  11. package/dist/expansion/dfs-priority.unit.test.d.ts.map +1 -0
  12. package/dist/expansion/index.d.ts +3 -0
  13. package/dist/expansion/index.d.ts.map +1 -1
  14. package/dist/expansion/k-hop.d.ts +26 -0
  15. package/dist/expansion/k-hop.d.ts.map +1 -0
  16. package/dist/expansion/k-hop.unit.test.d.ts +2 -0
  17. package/dist/expansion/k-hop.unit.test.d.ts.map +1 -0
  18. package/dist/expansion/random-walk.d.ts +35 -0
  19. package/dist/expansion/random-walk.d.ts.map +1 -0
  20. package/dist/expansion/random-walk.unit.test.d.ts +2 -0
  21. package/dist/expansion/random-walk.unit.test.d.ts.map +1 -0
  22. package/dist/index/index.cjs +716 -5
  23. package/dist/index/index.cjs.map +1 -1
  24. package/dist/index/index.js +707 -6
  25. package/dist/index/index.js.map +1 -1
  26. package/dist/ranking/baselines/hitting-time.d.ts +27 -0
  27. package/dist/ranking/baselines/hitting-time.d.ts.map +1 -0
  28. package/dist/ranking/baselines/hitting-time.unit.test.d.ts +2 -0
  29. package/dist/ranking/baselines/hitting-time.unit.test.d.ts.map +1 -0
  30. package/dist/ranking/baselines/index.d.ts +1 -0
  31. package/dist/ranking/baselines/index.d.ts.map +1 -1
  32. package/dist/ranking/mi/cosine.d.ts +13 -0
  33. package/dist/ranking/mi/cosine.d.ts.map +1 -0
  34. package/dist/ranking/mi/cosine.unit.test.d.ts +2 -0
  35. package/dist/ranking/mi/cosine.unit.test.d.ts.map +1 -0
  36. package/dist/ranking/mi/hub-promoted.d.ts +13 -0
  37. package/dist/ranking/mi/hub-promoted.d.ts.map +1 -0
  38. package/dist/ranking/mi/hub-promoted.unit.test.d.ts +2 -0
  39. package/dist/ranking/mi/hub-promoted.unit.test.d.ts.map +1 -0
  40. package/dist/ranking/mi/index.d.ts +5 -0
  41. package/dist/ranking/mi/index.d.ts.map +1 -1
  42. package/dist/ranking/mi/overlap-coefficient.d.ts +13 -0
  43. package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -0
  44. package/dist/ranking/mi/overlap-coefficient.unit.test.d.ts +2 -0
  45. package/dist/ranking/mi/overlap-coefficient.unit.test.d.ts.map +1 -0
  46. package/dist/ranking/mi/resource-allocation.d.ts +13 -0
  47. package/dist/ranking/mi/resource-allocation.d.ts.map +1 -0
  48. package/dist/ranking/mi/resource-allocation.unit.test.d.ts +2 -0
  49. package/dist/ranking/mi/resource-allocation.unit.test.d.ts.map +1 -0
  50. package/dist/ranking/mi/sorensen.d.ts +13 -0
  51. package/dist/ranking/mi/sorensen.d.ts.map +1 -0
  52. package/dist/ranking/mi/sorensen.unit.test.d.ts +2 -0
  53. package/dist/ranking/mi/sorensen.unit.test.d.ts.map +1 -0
  54. package/dist/ranking/mi/types.d.ts +1 -1
  55. package/dist/ranking/mi/types.d.ts.map +1 -1
  56. package/dist/schemas/graph.d.ts +1 -1
  57. package/package.json +1 -1
@@ -24,7 +24,7 @@ function degreePriority(_nodeId, context) {
24
24
  function base(graph, seeds, config) {
25
25
  const startTime = performance.now();
26
26
  const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
27
- if (seeds.length === 0) return emptyResult("base", startTime);
27
+ if (seeds.length === 0) return emptyResult$2("base", startTime);
28
28
  const numFrontiers = seeds.length;
29
29
  const allVisited = /* @__PURE__ */ new Set();
30
30
  const combinedVisited = /* @__PURE__ */ new Map();
@@ -105,7 +105,7 @@ function base(graph, seeds, config) {
105
105
  const otherVisited = visitedByFrontier[otherFrontier];
106
106
  if (otherVisited === void 0) continue;
107
107
  if (otherVisited.has(nodeId)) {
108
- const path = reconstructPath(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
108
+ const path = reconstructPath$1(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
109
109
  if (path !== null) {
110
110
  discoveredPaths.push(path);
111
111
  if (debug) console.log(`[BASE] Path found: ${path.nodes.join(" -> ")}`);
@@ -170,7 +170,7 @@ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, al
170
170
  /**
171
171
  * Reconstruct path from collision point.
172
172
  */
173
- function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seeds) {
173
+ function reconstructPath$1(collisionNode, frontierA, frontierB, predecessors, seeds) {
174
174
  const pathA = [collisionNode];
175
175
  const predA = predecessors[frontierA];
176
176
  if (predA !== void 0) {
@@ -206,7 +206,7 @@ function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seed
206
206
  /**
207
207
  * Create an empty result for early termination.
208
208
  */
209
- function emptyResult(algorithm, startTime) {
209
+ function emptyResult$2(algorithm, startTime) {
210
210
  return {
211
211
  paths: [],
212
212
  sampledNodes: /* @__PURE__ */ new Set(),
@@ -760,7 +760,7 @@ function fluxPriority(nodeId, context, densityThreshold, bridgeThreshold) {
760
760
  const bridge = bridgeScore(nodeId, context);
761
761
  const numFrontiers = new Set(context.visitedByFrontier.values()).size;
762
762
  if ((numFrontiers > 0 ? bridge / numFrontiers : 0) >= bridgeThreshold) return 1 / (1 + bridge);
763
- else if (density >= densityThreshold) return -degree;
763
+ else if (density >= densityThreshold) return 1 / (degree + 1);
764
764
  else return degree;
765
765
  }
766
766
  /**
@@ -858,6 +858,346 @@ function randomPriority(graph, seeds, config) {
858
858
  });
859
859
  }
860
860
  //#endregion
861
+ //#region src/expansion/dfs-priority.ts
862
+ /**
863
+ * DFS priority function: negative iteration produces LIFO ordering.
864
+ *
865
+ * Lower priority values are expanded first, so negating the iteration
866
+ * counter ensures the most recently enqueued node is always next.
867
+ */
868
+ function dfsPriorityFn(_nodeId, context) {
869
+ return -context.iteration;
870
+ }
871
+ /**
872
+ * Run DFS-priority expansion (LIFO discovery order).
873
+ *
874
+ * Uses the BASE framework with a negative-iteration priority function,
875
+ * which causes the most recently discovered node to be expanded first —
876
+ * equivalent to depth-first search behaviour.
877
+ *
878
+ * @param graph - Source graph
879
+ * @param seeds - Seed nodes for expansion
880
+ * @param config - Expansion configuration
881
+ * @returns Expansion result with discovered paths
882
+ */
883
+ function dfsPriority(graph, seeds, config) {
884
+ return base(graph, seeds, {
885
+ ...config,
886
+ priority: dfsPriorityFn
887
+ });
888
+ }
889
+ //#endregion
890
+ //#region src/expansion/k-hop.ts
891
+ /**
892
+ * Run k-hop expansion (fixed-depth BFS).
893
+ *
894
+ * Explores all nodes reachable within exactly k hops of any seed using
895
+ * breadth-first search. Paths between seeds are detected when a node
896
+ * is reached by frontiers from two different seeds.
897
+ *
898
+ * @param graph - Source graph
899
+ * @param seeds - Seed nodes for expansion
900
+ * @param config - K-hop configuration (k defaults to 2)
901
+ * @returns Expansion result with discovered paths
902
+ */
903
+ function kHop(graph, seeds, config) {
904
+ const startTime = performance.now();
905
+ const { k = 2 } = config ?? {};
906
+ if (seeds.length === 0) return emptyResult$1(startTime);
907
+ const visitedByFrontier = seeds.map(() => /* @__PURE__ */ new Map());
908
+ const firstVisitedBy = /* @__PURE__ */ new Map();
909
+ const allVisited = /* @__PURE__ */ new Set();
910
+ const sampledEdgeMap = /* @__PURE__ */ new Map();
911
+ const discoveredPaths = [];
912
+ let iterations = 0;
913
+ let edgesTraversed = 0;
914
+ for (let i = 0; i < seeds.length; i++) {
915
+ const seed = seeds[i];
916
+ if (seed === void 0) continue;
917
+ if (!graph.hasNode(seed.id)) continue;
918
+ visitedByFrontier[i]?.set(seed.id, null);
919
+ allVisited.add(seed.id);
920
+ if (!firstVisitedBy.has(seed.id)) firstVisitedBy.set(seed.id, i);
921
+ else {
922
+ const otherIdx = firstVisitedBy.get(seed.id) ?? -1;
923
+ if (otherIdx < 0) continue;
924
+ const fromSeed = seeds[otherIdx];
925
+ const toSeed = seeds[i];
926
+ if (fromSeed !== void 0 && toSeed !== void 0) discoveredPaths.push({
927
+ fromSeed,
928
+ toSeed,
929
+ nodes: [seed.id]
930
+ });
931
+ }
932
+ }
933
+ let currentLevel = seeds.map((s, i) => {
934
+ const frontier = visitedByFrontier[i];
935
+ if (frontier === void 0) return [];
936
+ return frontier.has(s.id) ? [s.id] : [];
937
+ });
938
+ for (let hop = 0; hop < k; hop++) {
939
+ const nextLevel = seeds.map(() => []);
940
+ for (let i = 0; i < seeds.length; i++) {
941
+ const level = currentLevel[i];
942
+ if (level === void 0) continue;
943
+ const frontierVisited = visitedByFrontier[i];
944
+ if (frontierVisited === void 0) continue;
945
+ for (const nodeId of level) {
946
+ iterations++;
947
+ for (const neighbour of graph.neighbours(nodeId)) {
948
+ edgesTraversed++;
949
+ const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
950
+ let targets = sampledEdgeMap.get(s);
951
+ if (targets === void 0) {
952
+ targets = /* @__PURE__ */ new Set();
953
+ sampledEdgeMap.set(s, targets);
954
+ }
955
+ targets.add(t);
956
+ if (frontierVisited.has(neighbour)) continue;
957
+ frontierVisited.set(neighbour, nodeId);
958
+ allVisited.add(neighbour);
959
+ nextLevel[i]?.push(neighbour);
960
+ const previousFrontier = firstVisitedBy.get(neighbour);
961
+ if (previousFrontier !== void 0 && previousFrontier !== i) {
962
+ const fromSeed = seeds[previousFrontier];
963
+ const toSeed = seeds[i];
964
+ if (fromSeed !== void 0 && toSeed !== void 0) {
965
+ const path = reconstructPath(neighbour, previousFrontier, i, visitedByFrontier, seeds);
966
+ if (path !== null) {
967
+ if (!discoveredPaths.some((p) => p.fromSeed.id === fromSeed.id && p.toSeed.id === toSeed.id || p.fromSeed.id === toSeed.id && p.toSeed.id === fromSeed.id)) discoveredPaths.push(path);
968
+ }
969
+ }
970
+ }
971
+ if (!firstVisitedBy.has(neighbour)) firstVisitedBy.set(neighbour, i);
972
+ }
973
+ }
974
+ }
975
+ currentLevel = nextLevel;
976
+ if (currentLevel.every((level) => level.length === 0)) break;
977
+ }
978
+ const endTime = performance.now();
979
+ const edgeTuples = /* @__PURE__ */ new Set();
980
+ for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
981
+ return {
982
+ paths: discoveredPaths,
983
+ sampledNodes: allVisited,
984
+ sampledEdges: edgeTuples,
985
+ visitedPerFrontier: visitedByFrontier.map((m) => new Set(m.keys())),
986
+ stats: {
987
+ iterations,
988
+ nodesVisited: allVisited.size,
989
+ edgesTraversed,
990
+ pathsFound: discoveredPaths.length,
991
+ durationMs: endTime - startTime,
992
+ algorithm: "k-hop",
993
+ termination: "exhausted"
994
+ }
995
+ };
996
+ }
997
+ /**
998
+ * Reconstruct the path between two colliding frontiers.
999
+ */
1000
+ function reconstructPath(collisionNode, frontierA, frontierB, visitedByFrontier, seeds) {
1001
+ const seedA = seeds[frontierA];
1002
+ const seedB = seeds[frontierB];
1003
+ if (seedA === void 0 || seedB === void 0) return null;
1004
+ const pathA = [collisionNode];
1005
+ const predA = visitedByFrontier[frontierA];
1006
+ if (predA !== void 0) {
1007
+ let node = collisionNode;
1008
+ let pred = predA.get(node);
1009
+ while (pred !== null && pred !== void 0) {
1010
+ pathA.unshift(pred);
1011
+ node = pred;
1012
+ pred = predA.get(node);
1013
+ }
1014
+ }
1015
+ const pathB = [];
1016
+ const predB = visitedByFrontier[frontierB];
1017
+ if (predB !== void 0) {
1018
+ let node = collisionNode;
1019
+ let pred = predB.get(node);
1020
+ while (pred !== null && pred !== void 0) {
1021
+ pathB.push(pred);
1022
+ node = pred;
1023
+ pred = predB.get(node);
1024
+ }
1025
+ }
1026
+ return {
1027
+ fromSeed: seedA,
1028
+ toSeed: seedB,
1029
+ nodes: [...pathA, ...pathB]
1030
+ };
1031
+ }
1032
+ /**
1033
+ * Create an empty result for early termination (no seeds).
1034
+ */
1035
+ function emptyResult$1(startTime) {
1036
+ return {
1037
+ paths: [],
1038
+ sampledNodes: /* @__PURE__ */ new Set(),
1039
+ sampledEdges: /* @__PURE__ */ new Set(),
1040
+ visitedPerFrontier: [],
1041
+ stats: {
1042
+ iterations: 0,
1043
+ nodesVisited: 0,
1044
+ edgesTraversed: 0,
1045
+ pathsFound: 0,
1046
+ durationMs: performance.now() - startTime,
1047
+ algorithm: "k-hop",
1048
+ termination: "exhausted"
1049
+ }
1050
+ };
1051
+ }
1052
+ //#endregion
1053
+ //#region src/expansion/random-walk.ts
1054
+ /**
1055
+ * Mulberry32 seeded PRNG — fast, compact, and high-quality for simulation.
1056
+ *
1057
+ * Returns a closure that yields the next pseudo-random value in [0, 1)
1058
+ * on each call.
1059
+ *
1060
+ * @param seed - 32-bit integer seed
1061
+ */
1062
+ function mulberry32(seed) {
1063
+ let s = seed;
1064
+ return () => {
1065
+ s += 1831565813;
1066
+ let t = s;
1067
+ t = Math.imul(t ^ t >>> 15, t | 1);
1068
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1069
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1070
+ };
1071
+ }
1072
+ /**
1073
+ * Run random-walk-with-restart expansion.
1074
+ *
1075
+ * For each seed, performs `walks` independent random walks of up to
1076
+ * `walkLength` steps. At each step the walk either restarts (with
1077
+ * probability `restartProbability`) or moves to a uniformly sampled
1078
+ * neighbour. All visited nodes and traversed edges are collected.
1079
+ *
1080
+ * Inter-seed paths are detected when a walk reaches a node that was
1081
+ * previously reached by a walk originating from a different seed.
1082
+ * The recorded path contains only the two seed endpoints rather than
1083
+ * the full walk trajectory, consistent with the ExpansionPath contract.
1084
+ *
1085
+ * @param graph - Source graph
1086
+ * @param seeds - Seed nodes for expansion
1087
+ * @param config - Random walk configuration
1088
+ * @returns Expansion result with discovered paths
1089
+ */
1090
+ function randomWalk(graph, seeds, config) {
1091
+ const startTime = performance.now();
1092
+ const { restartProbability = .15, walks = 10, walkLength = 20, seed = 0 } = config ?? {};
1093
+ if (seeds.length === 0) return emptyResult(startTime);
1094
+ const rand = mulberry32(seed);
1095
+ const firstVisitedBySeed = /* @__PURE__ */ new Map();
1096
+ const allVisited = /* @__PURE__ */ new Set();
1097
+ const sampledEdgeMap = /* @__PURE__ */ new Map();
1098
+ const discoveredPaths = [];
1099
+ let iterations = 0;
1100
+ let edgesTraversed = 0;
1101
+ const visitedPerFrontier = seeds.map(() => /* @__PURE__ */ new Set());
1102
+ for (let seedIdx = 0; seedIdx < seeds.length; seedIdx++) {
1103
+ const seed_ = seeds[seedIdx];
1104
+ if (seed_ === void 0) continue;
1105
+ const seedId = seed_.id;
1106
+ if (!graph.hasNode(seedId)) continue;
1107
+ if (!firstVisitedBySeed.has(seedId)) firstVisitedBySeed.set(seedId, seedIdx);
1108
+ allVisited.add(seedId);
1109
+ visitedPerFrontier[seedIdx]?.add(seedId);
1110
+ for (let w = 0; w < walks; w++) {
1111
+ let current = seedId;
1112
+ for (let step = 0; step < walkLength; step++) {
1113
+ iterations++;
1114
+ if (rand() < restartProbability) {
1115
+ current = seedId;
1116
+ continue;
1117
+ }
1118
+ const neighbourList = [];
1119
+ for (const nb of graph.neighbours(current)) neighbourList.push(nb);
1120
+ if (neighbourList.length === 0) {
1121
+ current = seedId;
1122
+ continue;
1123
+ }
1124
+ const next = neighbourList[Math.floor(rand() * neighbourList.length)];
1125
+ if (next === void 0) {
1126
+ current = seedId;
1127
+ continue;
1128
+ }
1129
+ edgesTraversed++;
1130
+ const [s, t] = current < next ? [current, next] : [next, current];
1131
+ let targets = sampledEdgeMap.get(s);
1132
+ if (targets === void 0) {
1133
+ targets = /* @__PURE__ */ new Set();
1134
+ sampledEdgeMap.set(s, targets);
1135
+ }
1136
+ targets.add(t);
1137
+ const previousSeedIdx = firstVisitedBySeed.get(next);
1138
+ if (previousSeedIdx !== void 0 && previousSeedIdx !== seedIdx) {
1139
+ const fromSeed = seeds[previousSeedIdx];
1140
+ const toSeed = seeds[seedIdx];
1141
+ if (fromSeed !== void 0 && toSeed !== void 0) {
1142
+ const path = {
1143
+ fromSeed,
1144
+ toSeed,
1145
+ nodes: [
1146
+ fromSeed.id,
1147
+ next,
1148
+ toSeed.id
1149
+ ].filter((n, i, arr) => arr.indexOf(n) === i)
1150
+ };
1151
+ if (!discoveredPaths.some((p) => p.fromSeed.id === fromSeed.id && p.toSeed.id === toSeed.id || p.fromSeed.id === toSeed.id && p.toSeed.id === fromSeed.id)) discoveredPaths.push(path);
1152
+ }
1153
+ }
1154
+ if (!firstVisitedBySeed.has(next)) firstVisitedBySeed.set(next, seedIdx);
1155
+ allVisited.add(next);
1156
+ visitedPerFrontier[seedIdx]?.add(next);
1157
+ current = next;
1158
+ }
1159
+ }
1160
+ }
1161
+ const endTime = performance.now();
1162
+ const edgeTuples = /* @__PURE__ */ new Set();
1163
+ for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
1164
+ return {
1165
+ paths: discoveredPaths,
1166
+ sampledNodes: allVisited,
1167
+ sampledEdges: edgeTuples,
1168
+ visitedPerFrontier,
1169
+ stats: {
1170
+ iterations,
1171
+ nodesVisited: allVisited.size,
1172
+ edgesTraversed,
1173
+ pathsFound: discoveredPaths.length,
1174
+ durationMs: endTime - startTime,
1175
+ algorithm: "random-walk",
1176
+ termination: "exhausted"
1177
+ }
1178
+ };
1179
+ }
1180
+ /**
1181
+ * Create an empty result for early termination (no seeds).
1182
+ */
1183
+ function emptyResult(startTime) {
1184
+ return {
1185
+ paths: [],
1186
+ sampledNodes: /* @__PURE__ */ new Set(),
1187
+ sampledEdges: /* @__PURE__ */ new Set(),
1188
+ visitedPerFrontier: [],
1189
+ stats: {
1190
+ iterations: 0,
1191
+ nodesVisited: 0,
1192
+ edgesTraversed: 0,
1193
+ pathsFound: 0,
1194
+ durationMs: performance.now() - startTime,
1195
+ algorithm: "random-walk",
1196
+ termination: "exhausted"
1197
+ }
1198
+ };
1199
+ }
1200
+ //#endregion
861
1201
  //#region src/ranking/parse.ts
862
1202
  /**
863
1203
  * Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
@@ -951,6 +1291,115 @@ function adamicAdar(graph, source, target, config) {
951
1291
  return Math.max(epsilon, score);
952
1292
  }
953
1293
  //#endregion
1294
+ //#region src/ranking/mi/cosine.ts
1295
+ /**
1296
+ * Compute cosine similarity between neighbourhoods of two nodes.
1297
+ *
1298
+ * @param graph - Source graph
1299
+ * @param source - Source node ID
1300
+ * @param target - Target node ID
1301
+ * @param config - Optional configuration
1302
+ * @returns Cosine similarity in [0, 1]
1303
+ */
1304
+ function cosine(graph, source, target, config) {
1305
+ const { epsilon = 1e-10 } = config ?? {};
1306
+ const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
1307
+ const targetNeighbours = require_utils.neighbourSet(graph, target, source);
1308
+ const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
1309
+ const denominator = Math.sqrt(sourceNeighbours.size) * Math.sqrt(targetNeighbours.size);
1310
+ if (denominator === 0) return 0;
1311
+ const score = intersection / denominator;
1312
+ return Math.max(epsilon, score);
1313
+ }
1314
+ //#endregion
1315
+ //#region src/ranking/mi/sorensen.ts
1316
+ /**
1317
+ * Compute Sorensen-Dice similarity between neighbourhoods of two nodes.
1318
+ *
1319
+ * @param graph - Source graph
1320
+ * @param source - Source node ID
1321
+ * @param target - Target node ID
1322
+ * @param config - Optional configuration
1323
+ * @returns Sorensen-Dice coefficient in [0, 1]
1324
+ */
1325
+ function sorensen(graph, source, target, config) {
1326
+ const { epsilon = 1e-10 } = config ?? {};
1327
+ const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
1328
+ const targetNeighbours = require_utils.neighbourSet(graph, target, source);
1329
+ const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
1330
+ const denominator = sourceNeighbours.size + targetNeighbours.size;
1331
+ if (denominator === 0) return 0;
1332
+ const score = 2 * intersection / denominator;
1333
+ return Math.max(epsilon, score);
1334
+ }
1335
+ //#endregion
1336
+ //#region src/ranking/mi/resource-allocation.ts
1337
+ /**
1338
+ * Compute Resource Allocation index between neighbourhoods of two nodes.
1339
+ *
1340
+ * @param graph - Source graph
1341
+ * @param source - Source node ID
1342
+ * @param target - Target node ID
1343
+ * @param config - Optional configuration
1344
+ * @returns Resource Allocation index (normalised to [0, 1] if configured)
1345
+ */
1346
+ function resourceAllocation(graph, source, target, config) {
1347
+ const { epsilon = 1e-10, normalise = true } = config ?? {};
1348
+ const commonNeighbours = require_utils.neighbourIntersection(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
1349
+ let score = 0;
1350
+ for (const neighbour of commonNeighbours) {
1351
+ const degree = graph.degree(neighbour);
1352
+ if (degree > 0) score += 1 / degree;
1353
+ }
1354
+ if (normalise && commonNeighbours.size > 0) {
1355
+ const maxScore = commonNeighbours.size;
1356
+ score = score / maxScore;
1357
+ }
1358
+ return Math.max(epsilon, score);
1359
+ }
1360
+ //#endregion
1361
+ //#region src/ranking/mi/overlap-coefficient.ts
1362
+ /**
1363
+ * Compute Overlap Coefficient between neighbourhoods of two nodes.
1364
+ *
1365
+ * @param graph - Source graph
1366
+ * @param source - Source node ID
1367
+ * @param target - Target node ID
1368
+ * @param config - Optional configuration
1369
+ * @returns Overlap Coefficient in [0, 1]
1370
+ */
1371
+ function overlapCoefficient(graph, source, target, config) {
1372
+ const { epsilon = 1e-10 } = config ?? {};
1373
+ const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
1374
+ const targetNeighbours = require_utils.neighbourSet(graph, target, source);
1375
+ const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
1376
+ const denominator = Math.min(sourceNeighbours.size, targetNeighbours.size);
1377
+ if (denominator === 0) return 0;
1378
+ const score = intersection / denominator;
1379
+ return Math.max(epsilon, score);
1380
+ }
1381
+ //#endregion
1382
+ //#region src/ranking/mi/hub-promoted.ts
1383
+ /**
1384
+ * Compute Hub Promoted index between neighbourhoods of two nodes.
1385
+ *
1386
+ * @param graph - Source graph
1387
+ * @param source - Source node ID
1388
+ * @param target - Target node ID
1389
+ * @param config - Optional configuration
1390
+ * @returns Hub Promoted index in [0, 1]
1391
+ */
1392
+ function hubPromoted(graph, source, target, config) {
1393
+ const { epsilon = 1e-10 } = config ?? {};
1394
+ const { intersection } = require_utils.neighbourOverlap(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
1395
+ const sourceDegree = graph.degree(source);
1396
+ const targetDegree = graph.degree(target);
1397
+ const denominator = Math.min(sourceDegree, targetDegree);
1398
+ if (denominator === 0) return 0;
1399
+ const score = intersection / denominator;
1400
+ return Math.max(epsilon, score);
1401
+ }
1402
+ //#endregion
954
1403
  //#region src/ranking/mi/scale.ts
955
1404
  /**
956
1405
  * Compute SCALE MI between two nodes.
@@ -1805,6 +2254,258 @@ function randomRanking(_graph, paths, config) {
1805
2254
  };
1806
2255
  }
1807
2256
  //#endregion
2257
+ //#region src/ranking/baselines/hitting-time.ts
2258
+ /**
2259
+ * Seeded deterministic random number generator (LCG).
2260
+ * Suitable for reproducible random walk simulation.
2261
+ */
2262
+ var SeededRNG = class {
2263
+ state;
2264
+ constructor(seed) {
2265
+ this.state = seed;
2266
+ }
2267
+ /**
2268
+ * Generate next pseudorandom value in [0, 1).
2269
+ */
2270
+ next() {
2271
+ this.state = this.state * 1103515245 + 12345 & 2147483647;
2272
+ return this.state / 2147483647;
2273
+ }
2274
+ };
2275
+ /**
2276
+ * Compute hitting time via Monte Carlo random walk simulation.
2277
+ *
2278
+ * @param graph - Source graph
2279
+ * @param source - Source node ID
2280
+ * @param target - Target node ID
2281
+ * @param walks - Number of walks to simulate
2282
+ * @param maxSteps - Maximum steps per walk
2283
+ * @param rng - Seeded RNG instance
2284
+ * @returns Average hitting time across walks
2285
+ */
2286
+ function computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng) {
2287
+ if (source === target) return 0;
2288
+ let totalSteps = 0;
2289
+ let successfulWalks = 0;
2290
+ for (let w = 0; w < walks; w++) {
2291
+ let current = source;
2292
+ let steps = 0;
2293
+ while (current !== target && steps < maxSteps) {
2294
+ const neighbours = Array.from(graph.neighbours(current));
2295
+ if (neighbours.length === 0) break;
2296
+ const nextNode = neighbours[Math.floor(rng.next() * neighbours.length)];
2297
+ if (nextNode === void 0) break;
2298
+ current = nextNode;
2299
+ steps++;
2300
+ }
2301
+ if (current === target) {
2302
+ totalSteps += steps;
2303
+ successfulWalks++;
2304
+ }
2305
+ }
2306
+ if (successfulWalks > 0) return totalSteps / successfulWalks;
2307
+ return maxSteps;
2308
+ }
2309
+ /**
2310
+ * Compute hitting time via exact fundamental matrix method.
2311
+ *
2312
+ * For small graphs, computes exact expected hitting times using
2313
+ * the fundamental matrix of the random walk.
2314
+ *
2315
+ * @param graph - Source graph
2316
+ * @param source - Source node ID
2317
+ * @param target - Target node ID
2318
+ * @returns Exact hitting time (or approximation if convergence fails)
2319
+ */
2320
+ function computeHittingTimeExact(graph, source, target) {
2321
+ if (source === target) return 0;
2322
+ const nodes = Array.from(graph.nodeIds());
2323
+ const nodeToIdx = /* @__PURE__ */ new Map();
2324
+ nodes.forEach((nodeId, idx) => {
2325
+ nodeToIdx.set(nodeId, idx);
2326
+ });
2327
+ const n = nodes.length;
2328
+ const sourceIdx = nodeToIdx.get(source);
2329
+ const targetIdx = nodeToIdx.get(target);
2330
+ if (sourceIdx === void 0 || targetIdx === void 0) return 0;
2331
+ const P = [];
2332
+ for (let i = 0; i < n; i++) {
2333
+ const row = [];
2334
+ for (let j = 0; j < n; j++) row[j] = 0;
2335
+ P[i] = row;
2336
+ }
2337
+ for (const nodeId of nodes) {
2338
+ const idx = nodeToIdx.get(nodeId);
2339
+ if (idx === void 0) continue;
2340
+ const pRow = P[idx];
2341
+ if (pRow === void 0) continue;
2342
+ if (idx === targetIdx) pRow[idx] = 1;
2343
+ else {
2344
+ const neighbours = Array.from(graph.neighbours(nodeId));
2345
+ const degree = neighbours.length;
2346
+ if (degree > 0) for (const neighbourId of neighbours) {
2347
+ const nIdx = nodeToIdx.get(neighbourId);
2348
+ if (nIdx !== void 0) pRow[nIdx] = 1 / degree;
2349
+ }
2350
+ }
2351
+ }
2352
+ const transientIndices = [];
2353
+ for (let i = 0; i < n; i++) if (i !== targetIdx) transientIndices.push(i);
2354
+ const m = transientIndices.length;
2355
+ const Q = [];
2356
+ for (let i = 0; i < m; i++) {
2357
+ const row = [];
2358
+ for (let j = 0; j < m; j++) row[j] = 0;
2359
+ Q[i] = row;
2360
+ }
2361
+ for (let i = 0; i < m; i++) {
2362
+ const qRow = Q[i];
2363
+ if (qRow === void 0) continue;
2364
+ const origI = transientIndices[i];
2365
+ if (origI === void 0) continue;
2366
+ const pRow = P[origI];
2367
+ if (pRow === void 0) continue;
2368
+ for (let j = 0; j < m; j++) {
2369
+ const origJ = transientIndices[j];
2370
+ if (origJ === void 0) continue;
2371
+ qRow[j] = pRow[origJ] ?? 0;
2372
+ }
2373
+ }
2374
+ const IMQ = [];
2375
+ for (let i = 0; i < m; i++) {
2376
+ const row = [];
2377
+ for (let j = 0; j < m; j++) row[j] = i === j ? 1 : 0;
2378
+ IMQ[i] = row;
2379
+ }
2380
+ for (let i = 0; i < m; i++) {
2381
+ const imqRow = IMQ[i];
2382
+ if (imqRow === void 0) continue;
2383
+ const qRow = Q[i];
2384
+ for (let j = 0; j < m; j++) {
2385
+ const qVal = qRow?.[j] ?? 0;
2386
+ imqRow[j] = (i === j ? 1 : 0) - qVal;
2387
+ }
2388
+ }
2389
+ const N = invertMatrix(IMQ);
2390
+ if (N === null) return 1;
2391
+ const sourceTransientIdx = transientIndices.indexOf(sourceIdx);
2392
+ if (sourceTransientIdx < 0) return 0;
2393
+ let hittingTime = 0;
2394
+ const row = N[sourceTransientIdx];
2395
+ if (row !== void 0) for (const val of row) hittingTime += val;
2396
+ return hittingTime;
2397
+ }
2398
+ /**
2399
+ * Invert a square matrix using Gaussian elimination with partial pivoting.
2400
+ *
2401
+ * @param matrix - Input matrix (n × n)
2402
+ * @returns Inverted matrix, or null if singular
2403
+ */
2404
+ function invertMatrix(matrix) {
2405
+ const n = matrix.length;
2406
+ const aug = [];
2407
+ for (let i = 0; i < n; i++) {
2408
+ const row = [];
2409
+ const matRow = matrix[i];
2410
+ for (let j = 0; j < n; j++) row[j] = matRow?.[j] ?? 0;
2411
+ for (let j = 0; j < n; j++) row[n + j] = i === j ? 1 : 0;
2412
+ aug[i] = row;
2413
+ }
2414
+ for (let col = 0; col < n; col++) {
2415
+ let pivotRow = col;
2416
+ const pivotCol = aug[pivotRow];
2417
+ if (pivotCol === void 0) return null;
2418
+ for (let row = col + 1; row < n; row++) {
2419
+ const currRowVal = aug[row]?.[col] ?? 0;
2420
+ const pivotRowVal = pivotCol[col] ?? 0;
2421
+ if (Math.abs(currRowVal) > Math.abs(pivotRowVal)) pivotRow = row;
2422
+ }
2423
+ const augPivot = aug[pivotRow];
2424
+ if (augPivot === void 0 || Math.abs(augPivot[col] ?? 0) < 1e-10) return null;
2425
+ [aug[col], aug[pivotRow]] = [aug[pivotRow] ?? [], aug[col] ?? []];
2426
+ const scaledPivotRow = aug[col];
2427
+ if (scaledPivotRow === void 0) return null;
2428
+ const pivot = scaledPivotRow[col] ?? 1;
2429
+ for (let j = 0; j < 2 * n; j++) scaledPivotRow[j] = (scaledPivotRow[j] ?? 0) / pivot;
2430
+ for (let row = col + 1; row < n; row++) {
2431
+ const currRow = aug[row];
2432
+ if (currRow === void 0) continue;
2433
+ const factor = currRow[col] ?? 0;
2434
+ for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (scaledPivotRow[j] ?? 0);
2435
+ }
2436
+ }
2437
+ for (let col = n - 1; col > 0; col--) {
2438
+ const colRow = aug[col];
2439
+ if (colRow === void 0) return null;
2440
+ for (let row = col - 1; row >= 0; row--) {
2441
+ const currRow = aug[row];
2442
+ if (currRow === void 0) continue;
2443
+ const factor = currRow[col] ?? 0;
2444
+ for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (colRow[j] ?? 0);
2445
+ }
2446
+ }
2447
+ const inv = [];
2448
+ for (let i = 0; i < n; i++) {
2449
+ const row = [];
2450
+ for (let j = 0; j < n; j++) row[j] = 0;
2451
+ inv[i] = row;
2452
+ }
2453
+ for (let i = 0; i < n; i++) {
2454
+ const invRow = inv[i];
2455
+ if (invRow === void 0) continue;
2456
+ const augRow = aug[i];
2457
+ if (augRow === void 0) continue;
2458
+ for (let j = 0; j < n; j++) invRow[j] = augRow[n + j] ?? 0;
2459
+ }
2460
+ return inv;
2461
+ }
2462
+ /**
2463
+ * Rank paths by inverse hitting time between endpoints.
2464
+ *
2465
+ * @param graph - Source graph
2466
+ * @param paths - Paths to rank
2467
+ * @param config - Configuration options
2468
+ * @returns Ranked paths (highest inverse hitting time first)
2469
+ */
2470
+ function hittingTime(graph, paths, config) {
2471
+ const { includeScores = true, mode = "auto", walks = 1e3, maxSteps = 1e4, seed = 42 } = config ?? {};
2472
+ if (paths.length === 0) return {
2473
+ paths: [],
2474
+ method: "hitting-time"
2475
+ };
2476
+ const nodeCount = Array.from(graph.nodeIds()).length;
2477
+ const actualMode = mode === "auto" ? nodeCount < 100 ? "exact" : "approximate" : mode;
2478
+ const rng = new SeededRNG(seed);
2479
+ const scored = paths.map((path) => {
2480
+ const source = path.nodes[0];
2481
+ const target = path.nodes[path.nodes.length - 1];
2482
+ if (source === void 0 || target === void 0) return {
2483
+ path,
2484
+ score: 0
2485
+ };
2486
+ const ht = actualMode === "exact" ? computeHittingTimeExact(graph, source, target) : computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng);
2487
+ return {
2488
+ path,
2489
+ score: ht > 0 ? 1 / ht : 0
2490
+ };
2491
+ });
2492
+ const maxScore = Math.max(...scored.map((s) => s.score));
2493
+ if (maxScore === 0 || !Number.isFinite(maxScore)) return {
2494
+ paths: paths.map((path) => ({
2495
+ ...path,
2496
+ score: 0
2497
+ })),
2498
+ method: "hitting-time"
2499
+ };
2500
+ return {
2501
+ paths: scored.map(({ path, score }) => ({
2502
+ ...path,
2503
+ score: includeScores ? score / maxScore : score / maxScore
2504
+ })).sort((a, b) => b.score - a.score),
2505
+ method: "hitting-time"
2506
+ };
2507
+ }
2508
+ //#endregion
1808
2509
  //#region src/extraction/ego-network.ts
1809
2510
  /**
1810
2511
  * Extract the ego-network (k-hop neighbourhood) of a centre node.
@@ -2438,6 +3139,7 @@ exports.bfs = require_traversal.bfs;
2438
3139
  exports.bfsWithPath = require_traversal.bfsWithPath;
2439
3140
  exports.communicability = communicability;
2440
3141
  exports.computeTrussNumbers = computeTrussNumbers;
3142
+ exports.cosine = cosine;
2441
3143
  exports.countEdgesOfType = require_utils.countEdgesOfType;
2442
3144
  exports.countNodesOfType = require_utils.countNodesOfType;
2443
3145
  exports.createGPUContext = require_gpu.createGPUContext;
@@ -2446,6 +3148,8 @@ exports.csrToGPUBuffers = require_gpu.csrToGPUBuffers;
2446
3148
  exports.degreeSum = degreeSum;
2447
3149
  exports.detectWebGPU = require_gpu.detectWebGPU;
2448
3150
  exports.dfs = require_traversal.dfs;
3151
+ exports.dfsPriority = dfsPriority;
3152
+ exports.dfsPriorityFn = dfsPriorityFn;
2449
3153
  exports.dfsWithPath = require_traversal.dfsWithPath;
2450
3154
  exports.dome = dome;
2451
3155
  exports.domeHighDegree = domeHighDegree;
@@ -2467,9 +3171,12 @@ exports.getMotifName = getMotifName;
2467
3171
  exports.graphToCSR = require_gpu.graphToCSR;
2468
3172
  exports.grasp = require_seeds.grasp;
2469
3173
  exports.hae = hae;
3174
+ exports.hittingTime = hittingTime;
3175
+ exports.hubPromoted = hubPromoted;
2470
3176
  exports.isWebGPUAvailable = require_gpu.isWebGPUAvailable;
2471
3177
  exports.jaccard = jaccard;
2472
3178
  exports.jaccardArithmetic = jaccardArithmetic;
3179
+ exports.kHop = kHop;
2473
3180
  exports.katz = katz;
2474
3181
  exports.lace = lace;
2475
3182
  exports.localClusteringCoefficient = require_utils.localClusteringCoefficient;
@@ -2483,20 +3190,24 @@ exports.normaliseFeatures = require_kmeans.normaliseFeatures;
2483
3190
  exports.zScoreNormalise = require_kmeans.normaliseFeatures;
2484
3191
  exports.normalisedEntropy = require_utils.normalisedEntropy;
2485
3192
  exports.notch = notch;
3193
+ exports.overlapCoefficient = overlapCoefficient;
2486
3194
  exports.pagerank = pagerank;
2487
3195
  exports.parse = parse;
2488
3196
  exports.pipe = pipe;
2489
3197
  exports.randomPriority = randomPriority;
2490
3198
  exports.randomRanking = randomRanking;
3199
+ exports.randomWalk = randomWalk;
2491
3200
  exports.reach = reach;
2492
3201
  exports.readBufferToCPU = require_gpu.readBufferToCPU;
2493
3202
  exports.resistanceDistance = resistanceDistance;
3203
+ exports.resourceAllocation = resourceAllocation;
2494
3204
  exports.sage = sage;
2495
3205
  exports.scale = scale;
2496
3206
  exports.shannonEntropy = require_utils.shannonEntropy;
2497
3207
  exports.shortest = shortest;
2498
3208
  exports.sift = sift;
2499
3209
  exports.skew = skew;
3210
+ exports.sorensen = sorensen;
2500
3211
  exports.span = span;
2501
3212
  exports.standardBfs = standardBfs;
2502
3213
  exports.stratified = require_seeds.stratified;