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