graphwise 1.8.0 → 1.9.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 (125) hide show
  1. package/dist/adjacency-map-BtKzcuJq.js +229 -0
  2. package/dist/adjacency-map-BtKzcuJq.js.map +1 -0
  3. package/dist/adjacency-map-JqBnMNkF.cjs +234 -0
  4. package/dist/adjacency-map-JqBnMNkF.cjs.map +1 -0
  5. package/dist/async/index.cjs +15 -242
  6. package/dist/async/index.js +2 -229
  7. package/dist/expansion/index.cjs +43 -0
  8. package/dist/expansion/index.js +2 -0
  9. package/dist/expansion-ClDhlMK8.js +1704 -0
  10. package/dist/expansion-ClDhlMK8.js.map +1 -0
  11. package/dist/expansion-DaTroIyv.cjs +1949 -0
  12. package/dist/expansion-DaTroIyv.cjs.map +1 -0
  13. package/dist/extraction/index.cjs +630 -0
  14. package/dist/extraction/index.cjs.map +1 -0
  15. package/dist/extraction/index.js +621 -0
  16. package/dist/extraction/index.js.map +1 -0
  17. package/dist/gpu/csr.d.ts +29 -30
  18. package/dist/gpu/csr.d.ts.map +1 -1
  19. package/dist/gpu/dispatch.d.ts +31 -0
  20. package/dist/gpu/dispatch.d.ts.map +1 -0
  21. package/dist/gpu/dispatch.unit.test.d.ts +5 -0
  22. package/dist/gpu/dispatch.unit.test.d.ts.map +1 -0
  23. package/dist/gpu/index.cjs +15 -410
  24. package/dist/gpu/index.d.ts +3 -1
  25. package/dist/gpu/index.d.ts.map +1 -1
  26. package/dist/gpu/index.js +2 -400
  27. package/dist/gpu/kernels/bfs/kernel.d.ts +59 -0
  28. package/dist/gpu/kernels/bfs/kernel.d.ts.map +1 -0
  29. package/dist/gpu/kernels/bfs/logic.d.ts +47 -0
  30. package/dist/gpu/kernels/bfs/logic.d.ts.map +1 -0
  31. package/dist/gpu/kernels/bfs/logic.unit.test.d.ts +2 -0
  32. package/dist/gpu/kernels/bfs/logic.unit.test.d.ts.map +1 -0
  33. package/dist/gpu/kernels/degree-histogram/kernel.d.ts +32 -0
  34. package/dist/gpu/kernels/degree-histogram/kernel.d.ts.map +1 -0
  35. package/dist/gpu/kernels/degree-histogram/logic.d.ts +45 -0
  36. package/dist/gpu/kernels/degree-histogram/logic.d.ts.map +1 -0
  37. package/dist/gpu/kernels/degree-histogram/logic.unit.test.d.ts +2 -0
  38. package/dist/gpu/kernels/degree-histogram/logic.unit.test.d.ts.map +1 -0
  39. package/dist/gpu/kernels/jaccard/kernel.d.ts +40 -0
  40. package/dist/gpu/kernels/jaccard/kernel.d.ts.map +1 -0
  41. package/dist/gpu/kernels/jaccard/logic.d.ts +43 -0
  42. package/dist/gpu/kernels/jaccard/logic.d.ts.map +1 -0
  43. package/dist/gpu/kernels/jaccard/logic.unit.test.d.ts +2 -0
  44. package/dist/gpu/kernels/jaccard/logic.unit.test.d.ts.map +1 -0
  45. package/dist/gpu/kernels/pagerank/kernel.d.ts +44 -0
  46. package/dist/gpu/kernels/pagerank/kernel.d.ts.map +1 -0
  47. package/dist/gpu/kernels/pagerank/logic.d.ts +50 -0
  48. package/dist/gpu/kernels/pagerank/logic.d.ts.map +1 -0
  49. package/dist/gpu/kernels/pagerank/logic.unit.test.d.ts +2 -0
  50. package/dist/gpu/kernels/pagerank/logic.unit.test.d.ts.map +1 -0
  51. package/dist/gpu/kernels/spmv/kernel.d.ts +43 -0
  52. package/dist/gpu/kernels/spmv/kernel.d.ts.map +1 -0
  53. package/dist/gpu/kernels/spmv/logic.d.ts +31 -0
  54. package/dist/gpu/kernels/spmv/logic.d.ts.map +1 -0
  55. package/dist/gpu/kernels/spmv/logic.unit.test.d.ts +2 -0
  56. package/dist/gpu/kernels/spmv/logic.unit.test.d.ts.map +1 -0
  57. package/dist/gpu/operations.d.ts +76 -0
  58. package/dist/gpu/operations.d.ts.map +1 -0
  59. package/dist/gpu/operations.unit.test.d.ts +5 -0
  60. package/dist/gpu/operations.unit.test.d.ts.map +1 -0
  61. package/dist/gpu/root.d.ts +53 -0
  62. package/dist/gpu/root.d.ts.map +1 -0
  63. package/dist/gpu/root.unit.test.d.ts +2 -0
  64. package/dist/gpu/root.unit.test.d.ts.map +1 -0
  65. package/dist/gpu/types.d.ts +3 -8
  66. package/dist/gpu/types.d.ts.map +1 -1
  67. package/dist/gpu-CHiCN0wa.js +16945 -0
  68. package/dist/gpu-CHiCN0wa.js.map +1 -0
  69. package/dist/gpu-Y6owRVMi.cjs +17028 -0
  70. package/dist/gpu-Y6owRVMi.cjs.map +1 -0
  71. package/dist/graph/index.cjs +2 -229
  72. package/dist/graph/index.js +1 -228
  73. package/dist/index/index.cjs +141 -4040
  74. package/dist/index/index.js +15 -3917
  75. package/dist/jaccard-3rCdilwm.js +39 -0
  76. package/dist/jaccard-3rCdilwm.js.map +1 -0
  77. package/dist/jaccard-Bys9_dGW.cjs +50 -0
  78. package/dist/jaccard-Bys9_dGW.cjs.map +1 -0
  79. package/dist/{kmeans-BIgSyGKu.cjs → kmeans-B8x9D1kt.cjs} +1 -1
  80. package/dist/{kmeans-BIgSyGKu.cjs.map → kmeans-B8x9D1kt.cjs.map} +1 -1
  81. package/dist/{kmeans-87ExSUNZ.js → kmeans-DKkL9rAN.js} +1 -1
  82. package/dist/{kmeans-87ExSUNZ.js.map → kmeans-DKkL9rAN.js.map} +1 -1
  83. package/dist/ops-djAsQQSh.cjs +277 -0
  84. package/dist/ops-djAsQQSh.cjs.map +1 -0
  85. package/dist/ops-upIi6JIi.js +212 -0
  86. package/dist/ops-upIi6JIi.js.map +1 -0
  87. package/dist/priority-queue-BIiD1L0k.cjs +148 -0
  88. package/dist/priority-queue-BIiD1L0k.cjs.map +1 -0
  89. package/dist/priority-queue-CFDd5cBg.js +143 -0
  90. package/dist/priority-queue-CFDd5cBg.js.map +1 -0
  91. package/dist/ranking/index.cjs +43 -0
  92. package/dist/ranking/index.js +4 -0
  93. package/dist/ranking/mi/index.cjs +581 -0
  94. package/dist/ranking/mi/index.cjs.map +1 -0
  95. package/dist/ranking/mi/index.js +555 -0
  96. package/dist/ranking/mi/index.js.map +1 -0
  97. package/dist/ranking-3ez5m67U.js +1016 -0
  98. package/dist/ranking-3ez5m67U.js.map +1 -0
  99. package/dist/ranking-DVvajgUZ.cjs +1093 -0
  100. package/dist/ranking-DVvajgUZ.cjs.map +1 -0
  101. package/dist/seeds/index.cjs +1 -1
  102. package/dist/seeds/index.js +1 -1
  103. package/dist/structures/index.cjs +2 -143
  104. package/dist/structures/index.js +1 -142
  105. package/dist/utils/index.cjs +1 -1
  106. package/dist/utils/index.js +1 -1
  107. package/dist/utils-BodeE2Mo.js +22 -0
  108. package/dist/utils-BodeE2Mo.js.map +1 -0
  109. package/dist/utils-CDtCcsyF.cjs +33 -0
  110. package/dist/utils-CDtCcsyF.cjs.map +1 -0
  111. package/package.json +3 -1
  112. package/dist/async/index.cjs.map +0 -1
  113. package/dist/async/index.js.map +0 -1
  114. package/dist/gpu/context.d.ts +0 -118
  115. package/dist/gpu/context.d.ts.map +0 -1
  116. package/dist/gpu/context.unit.test.d.ts +0 -2
  117. package/dist/gpu/context.unit.test.d.ts.map +0 -1
  118. package/dist/gpu/index.cjs.map +0 -1
  119. package/dist/gpu/index.js.map +0 -1
  120. package/dist/graph/index.cjs.map +0 -1
  121. package/dist/graph/index.js.map +0 -1
  122. package/dist/index/index.cjs.map +0 -1
  123. package/dist/index/index.js.map +0 -1
  124. package/dist/structures/index.cjs.map +0 -1
  125. package/dist/structures/index.js.map +0 -1
@@ -1,4058 +1,159 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_graph = require("../graph/index.cjs");
2
+ const require_gpu = require("../gpu-Y6owRVMi.cjs");
3
+ const require_adjacency_map = require("../adjacency-map-JqBnMNkF.cjs");
4
+ require("../graph/index.cjs");
3
5
  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");
6
+ const require_priority_queue = require("../priority-queue-BIiD1L0k.cjs");
7
+ require("../structures/index.cjs");
8
+ const require_utils = require("../utils-CDtCcsyF.cjs");
9
+ const require_ops = require("../ops-djAsQQSh.cjs");
10
+ const require_expansion = require("../expansion-DaTroIyv.cjs");
11
+ const require_kmeans = require("../kmeans-B8x9D1kt.cjs");
12
+ const require_utils$1 = require("../utils/index.cjs");
13
+ const require_jaccard = require("../jaccard-Bys9_dGW.cjs");
14
+ const require_ranking = require("../ranking-DVvajgUZ.cjs");
15
+ const require_ranking_mi = require("../ranking/mi/index.cjs");
7
16
  const require_seeds = require("../seeds/index.cjs");
8
- const require_gpu = require("../gpu/index.cjs");
9
- const require_async = require("../async/index.cjs");
10
- //#region src/expansion/base-helpers.ts
11
- /**
12
- * Check whether expansion should continue given current progress.
13
- *
14
- * Returns shouldContinue=false as soon as any configured limit is reached,
15
- * along with the appropriate termination reason.
16
- *
17
- * @param iterations - Number of iterations completed so far
18
- * @param nodesVisited - Number of distinct nodes visited so far
19
- * @param pathsFound - Number of paths discovered so far
20
- * @param limits - Configured expansion limits (0 = unlimited)
21
- * @returns Whether to continue and the termination reason if stopping
22
- */
23
- function continueExpansion(iterations, nodesVisited, pathsFound, limits) {
24
- if (limits.maxIterations > 0 && iterations >= limits.maxIterations) return {
25
- shouldContinue: false,
26
- termination: "limit"
27
- };
28
- if (limits.maxNodes > 0 && nodesVisited >= limits.maxNodes) return {
29
- shouldContinue: false,
30
- termination: "limit"
31
- };
32
- if (limits.maxPaths > 0 && pathsFound >= limits.maxPaths) return {
33
- shouldContinue: false,
34
- termination: "limit"
35
- };
36
- return {
37
- shouldContinue: true,
38
- termination: "exhausted"
39
- };
40
- }
41
- /**
42
- * Reconstruct path from collision point.
43
- *
44
- * Traces backwards through the predecessor maps of both frontiers from the
45
- * collision node, then concatenates the two halves to form the full path.
46
- *
47
- * @param collisionNode - The node where the two frontiers met
48
- * @param frontierA - Index of the first frontier
49
- * @param frontierB - Index of the second frontier
50
- * @param predecessors - Predecessor maps, one per frontier
51
- * @param seeds - Seed nodes, one per frontier
52
- * @returns The reconstructed path, or null if seeds are missing
53
- */
54
- function reconstructPath$1(collisionNode, frontierA, frontierB, predecessors, seeds) {
55
- const pathA = [collisionNode];
56
- const predA = predecessors[frontierA];
57
- if (predA !== void 0) {
58
- let node = collisionNode;
59
- let next = predA.get(node);
60
- while (next !== null && next !== void 0) {
61
- node = next;
62
- pathA.unshift(node);
63
- next = predA.get(node);
64
- }
65
- }
66
- const pathB = [];
67
- const predB = predecessors[frontierB];
68
- if (predB !== void 0) {
69
- let node = collisionNode;
70
- let next = predB.get(node);
71
- while (next !== null && next !== void 0) {
72
- node = next;
73
- pathB.push(node);
74
- next = predB.get(node);
75
- }
76
- }
77
- const fullPath = [...pathA, ...pathB];
78
- const seedA = seeds[frontierA];
79
- const seedB = seeds[frontierB];
80
- if (seedA === void 0 || seedB === void 0) return null;
81
- return {
82
- fromSeed: seedA,
83
- toSeed: seedB,
84
- nodes: fullPath
85
- };
86
- }
87
- /**
88
- * Create an empty expansion result for early termination (e.g. no seeds given).
89
- *
90
- * @param algorithm - Name of the algorithm producing this result
91
- * @param startTime - performance.now() timestamp taken before the algorithm began
92
- * @returns An ExpansionResult with zero paths and zero stats
93
- */
94
- function emptyResult$2(algorithm, startTime) {
95
- return {
96
- paths: [],
97
- sampledNodes: /* @__PURE__ */ new Set(),
98
- sampledEdges: /* @__PURE__ */ new Set(),
99
- visitedPerFrontier: [],
100
- stats: {
101
- iterations: 0,
102
- nodesVisited: 0,
103
- edgesTraversed: 0,
104
- pathsFound: 0,
105
- durationMs: performance.now() - startTime,
106
- algorithm,
107
- termination: "exhausted"
108
- }
109
- };
110
- }
111
- //#endregion
112
- //#region src/expansion/base-core.ts
113
- /**
114
- * Default priority function — degree-ordered (DOME).
115
- *
116
- * Lower degree = higher priority, so sparse nodes are explored before hubs.
117
- */
118
- function degreePriority(_nodeId, context) {
119
- return context.degree;
120
- }
121
- /**
122
- * Generator core of the BASE expansion algorithm.
123
- *
124
- * Yields GraphOp objects to request graph data, allowing the caller to
125
- * provide a sync or async runner. The optional `graphRef` parameter is
126
- * required when the priority function accesses `context.graph` — it is
127
- * populated in sync mode by `base()`. In async mode (Phase 4+), a proxy
128
- * graph may be supplied instead.
129
- *
130
- * @param graphMeta - Immutable graph metadata (directed, nodeCount, edgeCount)
131
- * @param seeds - Seed nodes for expansion
132
- * @param config - Expansion configuration (priority, limits, debug)
133
- * @param graphRef - Optional real graph reference for context.graph in priority functions
134
- * @returns An ExpansionResult with all discovered paths and statistics
135
- */
136
- function* baseCore(graphMeta, seeds, config, graphRef) {
137
- const startTime = performance.now();
138
- const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
139
- if (seeds.length === 0) return emptyResult$2("base", startTime);
140
- const numFrontiers = seeds.length;
141
- const allVisited = /* @__PURE__ */ new Set();
142
- const combinedVisited = /* @__PURE__ */ new Map();
143
- const visitedByFrontier = [];
144
- const predecessors = [];
145
- const queues = [];
146
- for (let i = 0; i < numFrontiers; i++) {
147
- visitedByFrontier.push(/* @__PURE__ */ new Map());
148
- predecessors.push(/* @__PURE__ */ new Map());
149
- queues.push(new require_structures.PriorityQueue());
150
- const seed = seeds[i];
151
- if (seed === void 0) continue;
152
- const seedNode = seed.id;
153
- predecessors[i]?.set(seedNode, null);
154
- combinedVisited.set(seedNode, i);
155
- allVisited.add(seedNode);
156
- const seedDegree = yield* require_async.opDegree(seedNode);
157
- const seedPriority = priority(seedNode, buildPriorityContext(seedNode, i, combinedVisited, allVisited, [], 0, seedDegree, graphRef));
158
- queues[i]?.push({
159
- nodeId: seedNode,
160
- frontierIndex: i,
161
- predecessor: null
162
- }, seedPriority);
163
- }
164
- const sampledEdgeMap = /* @__PURE__ */ new Map();
165
- const discoveredPaths = [];
166
- let iterations = 0;
167
- let edgesTraversed = 0;
168
- let termination = "exhausted";
169
- const limits = {
170
- maxIterations,
171
- maxNodes,
172
- maxPaths
173
- };
174
- for (;;) {
175
- const check = continueExpansion(iterations, allVisited.size, discoveredPaths.length, limits);
176
- if (!check.shouldContinue) {
177
- termination = check.termination;
178
- break;
179
- }
180
- let lowestPriority = Number.POSITIVE_INFINITY;
181
- let activeFrontier = -1;
182
- for (let i = 0; i < numFrontiers; i++) {
183
- const queue = queues[i];
184
- if (queue !== void 0 && !queue.isEmpty()) {
185
- const peek = queue.peek();
186
- if (peek !== void 0 && peek.priority < lowestPriority) {
187
- lowestPriority = peek.priority;
188
- activeFrontier = i;
189
- }
190
- }
191
- }
192
- if (activeFrontier < 0) {
193
- termination = "exhausted";
194
- break;
195
- }
196
- const queue = queues[activeFrontier];
197
- if (queue === void 0) break;
198
- const entry = queue.pop();
199
- if (entry === void 0) break;
200
- const { nodeId, predecessor } = entry.item;
201
- const frontierVisited = visitedByFrontier[activeFrontier];
202
- if (frontierVisited === void 0 || frontierVisited.has(nodeId)) continue;
203
- frontierVisited.set(nodeId, activeFrontier);
204
- combinedVisited.set(nodeId, activeFrontier);
205
- if (predecessor !== null) {
206
- const predMap = predecessors[activeFrontier];
207
- if (predMap !== void 0) predMap.set(nodeId, predecessor);
208
- }
209
- allVisited.add(nodeId);
210
- if (debug) console.log(`[BASE] Iteration ${String(iterations)}: Frontier ${String(activeFrontier)} visiting ${nodeId}`);
211
- for (let otherFrontier = 0; otherFrontier < numFrontiers; otherFrontier++) {
212
- if (otherFrontier === activeFrontier) continue;
213
- const otherVisited = visitedByFrontier[otherFrontier];
214
- if (otherVisited === void 0) continue;
215
- if (otherVisited.has(nodeId)) {
216
- const path = reconstructPath$1(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
217
- if (path !== null) {
218
- discoveredPaths.push(path);
219
- if (debug) console.log(`[BASE] Path found: ${path.nodes.join(" -> ")}`);
220
- }
221
- }
222
- }
223
- const neighbours = yield* require_async.opNeighbours(nodeId);
224
- for (const neighbour of neighbours) {
225
- edgesTraversed++;
226
- const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
227
- let targets = sampledEdgeMap.get(s);
228
- if (targets === void 0) {
229
- targets = /* @__PURE__ */ new Set();
230
- sampledEdgeMap.set(s, targets);
231
- }
232
- targets.add(t);
233
- const fv = visitedByFrontier[activeFrontier];
234
- if (fv === void 0 || fv.has(neighbour)) continue;
235
- const neighbourDegree = yield* require_async.opDegree(neighbour);
236
- const neighbourPriority = priority(neighbour, buildPriorityContext(neighbour, activeFrontier, combinedVisited, allVisited, discoveredPaths, iterations + 1, neighbourDegree, graphRef));
237
- queue.push({
238
- nodeId: neighbour,
239
- frontierIndex: activeFrontier,
240
- predecessor: nodeId
241
- }, neighbourPriority);
242
- }
243
- iterations++;
244
- }
245
- const endTime = performance.now();
246
- const visitedPerFrontier = visitedByFrontier.map((m) => new Set(m.keys()));
247
- const edgeTuples = /* @__PURE__ */ new Set();
248
- for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
249
- return {
250
- paths: discoveredPaths,
251
- sampledNodes: allVisited,
252
- sampledEdges: edgeTuples,
253
- visitedPerFrontier,
254
- stats: {
255
- iterations,
256
- nodesVisited: allVisited.size,
257
- edgesTraversed,
258
- pathsFound: discoveredPaths.length,
259
- durationMs: endTime - startTime,
260
- algorithm: "base",
261
- termination
262
- }
263
- };
264
- }
265
- /**
266
- * Create a sentinel ReadableGraph that throws if any member is accessed.
267
- *
268
- * Used in async mode when no graphRef is provided. Gives a clear error
269
- * message rather than silently returning incorrect results if a priority
270
- * function attempts to access `context.graph` before Phase 4b introduces
271
- * a real async proxy.
272
- */
273
- function makeNoGraphSentinel() {
274
- const msg = "Priority function accessed context.graph in async mode without a graph proxy. Pass a graphRef or use a priority function that does not access context.graph.";
275
- const fail = () => {
276
- throw new Error(msg);
277
- };
278
- return {
279
- get directed() {
280
- return fail();
281
- },
282
- get nodeCount() {
283
- return fail();
284
- },
285
- get edgeCount() {
286
- return fail();
287
- },
288
- hasNode: fail,
289
- getNode: fail,
290
- nodeIds: fail,
291
- neighbours: fail,
292
- degree: fail,
293
- getEdge: fail,
294
- edges: fail
295
- };
296
- }
297
- /**
298
- * Build a PriorityContext for a node using a pre-fetched degree.
299
- *
300
- * When `graphRef` is provided (sync mode), it is used as `context.graph` so
301
- * priority functions can access the graph directly. When it is absent (async
302
- * mode), a Proxy is used in its place that throws a clear error if any
303
- * property is accessed — this prevents silent failures until Phase 4b
304
- * introduces a real async proxy graph.
305
- */
306
- function buildPriorityContext(_nodeId, frontierIndex, combinedVisited, allVisited, discoveredPaths, iteration, degree, graphRef) {
307
- return {
308
- graph: graphRef ?? makeNoGraphSentinel(),
309
- degree,
310
- frontierIndex,
311
- visitedByFrontier: combinedVisited,
312
- allVisited,
313
- discoveredPaths,
314
- iteration
315
- };
316
- }
317
- //#endregion
318
- //#region src/expansion/base.ts
319
- /**
320
- * Run BASE expansion synchronously.
321
- *
322
- * Delegates to baseCore + runSync. Behaviour is identical to the previous
323
- * direct implementation — all existing callers are unaffected.
324
- *
325
- * @param graph - Source graph
326
- * @param seeds - Seed nodes for expansion
327
- * @param config - Expansion configuration
328
- * @returns Expansion result with discovered paths
329
- */
330
- function base(graph, seeds, config) {
331
- return require_async.runSync(baseCore({
332
- directed: graph.directed,
333
- nodeCount: graph.nodeCount,
334
- edgeCount: graph.edgeCount
335
- }, seeds, config, graph), graph);
336
- }
337
- /**
338
- * Run BASE expansion asynchronously.
339
- *
340
- * Delegates to baseCore + runAsync. Supports:
341
- * - Cancellation via AbortSignal (config.signal)
342
- * - Progress callbacks (config.onProgress)
343
- * - Custom cooperative yield strategies (config.yieldStrategy)
344
- *
345
- * Note: priority functions that access `context.graph` are not supported in
346
- * async mode without a graph proxy (Phase 4b). The default degree-based
347
- * priority (DOME) does not access context.graph and works correctly.
348
- *
349
- * @param graph - Async source graph
350
- * @param seeds - Seed nodes for expansion
351
- * @param config - Expansion and async runner configuration
352
- * @returns Promise resolving to the expansion result
353
- */
354
- async function baseAsync(graph, seeds, config) {
355
- const [nodeCount, edgeCount] = await Promise.all([graph.nodeCount, graph.edgeCount]);
356
- const gen = baseCore({
357
- directed: graph.directed,
358
- nodeCount,
359
- edgeCount
360
- }, seeds, config);
361
- const runnerOptions = {};
362
- if (config?.signal !== void 0) runnerOptions.signal = config.signal;
363
- if (config?.onProgress !== void 0) runnerOptions.onProgress = config.onProgress;
364
- if (config?.yieldStrategy !== void 0) runnerOptions.yieldStrategy = config.yieldStrategy;
365
- return require_async.runAsync(gen, graph, runnerOptions);
366
- }
367
- //#endregion
368
- //#region src/expansion/dome.ts
369
- /**
370
- * DOME priority: lower degree is expanded first.
371
- */
372
- function domePriority(_nodeId, context) {
373
- return context.degree;
374
- }
375
- /**
376
- * DOME high-degree priority: negate degree to prioritise high-degree nodes.
377
- */
378
- function domeHighDegreePriority(_nodeId, context) {
379
- return -context.degree;
380
- }
381
- /**
382
- * Run DOME expansion (degree-ordered).
383
- *
384
- * @param graph - Source graph
385
- * @param seeds - Seed nodes for expansion
386
- * @param config - Expansion configuration
387
- * @returns Expansion result with discovered paths
388
- */
389
- function dome(graph, seeds, config) {
390
- return base(graph, seeds, {
391
- ...config,
392
- priority: domePriority
393
- });
394
- }
395
- /**
396
- * Run DOME expansion asynchronously (degree-ordered).
397
- *
398
- * @param graph - Async source graph
399
- * @param seeds - Seed nodes for expansion
400
- * @param config - Expansion and async runner configuration
401
- * @returns Promise resolving to the expansion result
402
- */
403
- async function domeAsync(graph, seeds, config) {
404
- return baseAsync(graph, seeds, {
405
- ...config,
406
- priority: domePriority
407
- });
408
- }
409
- /**
410
- * DOME with reverse priority (high degree first).
411
- */
412
- function domeHighDegree(graph, seeds, config) {
413
- return base(graph, seeds, {
414
- ...config,
415
- priority: domeHighDegreePriority
416
- });
417
- }
418
- /**
419
- * Run DOME high-degree expansion asynchronously (high degree first).
420
- *
421
- * @param graph - Async source graph
422
- * @param seeds - Seed nodes for expansion
423
- * @param config - Expansion and async runner configuration
424
- * @returns Promise resolving to the expansion result
425
- */
426
- async function domeHighDegreeAsync(graph, seeds, config) {
427
- return baseAsync(graph, seeds, {
428
- ...config,
429
- priority: domeHighDegreePriority
430
- });
431
- }
432
- //#endregion
433
- //#region src/expansion/hae.ts
434
- var EPSILON = 1e-10;
435
- /**
436
- * Default type mapper - uses node.type property.
437
- */
438
- function defaultTypeMapper$1(node) {
439
- return node.type ?? "default";
440
- }
441
- /**
442
- * Create a priority function using the given type mapper.
443
- */
444
- function createHAEPriority(typeMapper) {
445
- return function haePriority(nodeId, context) {
446
- const graph = context.graph;
447
- const neighbours = graph.neighbours(nodeId);
448
- const neighbourTypes = [];
449
- for (const neighbour of neighbours) {
450
- const node = graph.getNode(neighbour);
451
- if (node !== void 0) neighbourTypes.push(typeMapper(node));
452
- }
453
- return 1 / (require_utils.localTypeEntropy(neighbourTypes) + EPSILON) * Math.log(context.degree + 1);
454
- };
455
- }
456
- /**
457
- * Run HAE expansion (Heterogeneity-Aware Expansion).
458
- *
459
- * Discovers paths by prioritising nodes with diverse neighbour types,
460
- * using a custom type mapper for flexible type extraction.
461
- *
462
- * @param graph - Source graph
463
- * @param seeds - Seed nodes for expansion
464
- * @param config - HAE configuration with optional typeMapper
465
- * @returns Expansion result with discovered paths
466
- */
467
- function hae(graph, seeds, config) {
468
- const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
469
- return base(graph, seeds, {
470
- ...config,
471
- priority: createHAEPriority(typeMapper)
472
- });
473
- }
474
- /**
475
- * Run HAE expansion asynchronously.
476
- *
477
- * Note: the HAE priority function accesses `context.graph` to retrieve
478
- * neighbour types. Full async equivalence requires PriorityContext
479
- * refactoring (Phase 4b deferred). This export establishes the async API
480
- * surface; use with a `wrapAsync`-wrapped sync graph for testing.
481
- *
482
- * @param graph - Async source graph
483
- * @param seeds - Seed nodes for expansion
484
- * @param config - HAE configuration combined with async runner options
485
- * @returns Promise resolving to the expansion result
486
- */
487
- async function haeAsync(graph, seeds, config) {
488
- const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
489
- return baseAsync(graph, seeds, {
490
- ...config,
491
- priority: createHAEPriority(typeMapper)
492
- });
493
- }
494
- //#endregion
495
- //#region src/expansion/edge.ts
496
- /** Default type mapper: reads `node.type`, falling back to "default". */
497
- var defaultTypeMapper = (n) => typeof n.type === "string" ? n.type : "default";
498
- /**
499
- * Run EDGE expansion (Entropy-Driven Graph Expansion).
500
- *
501
- * Discovers paths by prioritising nodes with diverse neighbour types,
502
- * deferring nodes with homogeneous neighbourhoods.
503
- *
504
- * @param graph - Source graph
505
- * @param seeds - Seed nodes for expansion
506
- * @param config - Expansion configuration
507
- * @returns Expansion result with discovered paths
508
- */
509
- function edge(graph, seeds, config) {
510
- return hae(graph, seeds, {
511
- ...config,
512
- typeMapper: defaultTypeMapper
513
- });
514
- }
515
- /**
516
- * Run EDGE expansion asynchronously.
517
- *
518
- * Delegates to `haeAsync` with the default `node.type` mapper.
519
- *
520
- * Note: the HAE priority function accesses `context.graph` to retrieve
521
- * neighbour types. Full async equivalence requires PriorityContext
522
- * refactoring (Phase 4b deferred). This export establishes the async API
523
- * surface; use with a `wrapAsync`-wrapped sync graph for testing.
524
- *
525
- * @param graph - Async source graph
526
- * @param seeds - Seed nodes for expansion
527
- * @param config - Expansion and async runner configuration
528
- * @returns Promise resolving to the expansion result
529
- */
530
- async function edgeAsync(graph, seeds, config) {
531
- return haeAsync(graph, seeds, {
532
- ...config,
533
- typeMapper: defaultTypeMapper
534
- });
535
- }
536
- //#endregion
537
- //#region src/expansion/pipe.ts
538
- /**
539
- * Priority function using path potential.
540
- * Lower values = higher priority (expanded first).
541
- *
542
- * Path potential measures how many of a node's neighbours have been
543
- * visited by OTHER frontiers (not the current frontier).
544
- */
545
- function pipePriority(nodeId, context) {
546
- const neighbours = context.graph.neighbours(nodeId);
547
- let pathPotential = 0;
548
- for (const neighbour of neighbours) {
549
- const visitedBy = context.visitedByFrontier.get(neighbour);
550
- if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
551
- }
552
- return context.degree / (1 + pathPotential);
553
- }
554
- /**
555
- * Run PIPE expansion (Path-Potential Informed Priority Expansion).
556
- *
557
- * Discovers paths by prioritising nodes that bridge multiple frontiers,
558
- * identifying connecting points between seed regions.
559
- *
560
- * @param graph - Source graph
561
- * @param seeds - Seed nodes for expansion
562
- * @param config - Expansion configuration
563
- * @returns Expansion result with discovered paths
564
- */
565
- function pipe(graph, seeds, config) {
566
- return base(graph, seeds, {
567
- ...config,
568
- priority: pipePriority
569
- });
570
- }
571
- /**
572
- * Run PIPE expansion asynchronously.
573
- *
574
- * Note: the PIPE priority function accesses `context.graph` to retrieve
575
- * neighbour lists. Full async equivalence requires PriorityContext
576
- * refactoring (Phase 4b deferred). This export establishes the async API
577
- * surface; use with a `wrapAsync`-wrapped sync graph for testing.
578
- *
579
- * @param graph - Async source graph
580
- * @param seeds - Seed nodes for expansion
581
- * @param config - Expansion and async runner configuration
582
- * @returns Promise resolving to the expansion result
583
- */
584
- async function pipeAsync(graph, seeds, config) {
585
- return baseAsync(graph, seeds, {
586
- ...config,
587
- priority: pipePriority
588
- });
589
- }
590
- //#endregion
591
- //#region src/expansion/priority-helpers.ts
592
- /**
593
- * Compute the average mutual information between a node and all visited
594
- * nodes in the same frontier.
595
- *
596
- * Returns a value in [0, 1] — higher means the node is more similar
597
- * (on average) to already-visited same-frontier nodes.
598
- *
599
- * @param graph - Source graph
600
- * @param nodeId - Node being prioritised
601
- * @param context - Current priority context
602
- * @param mi - MI function to use for pairwise scoring
603
- * @returns Average MI score, or 0 if no same-frontier visited nodes exist
604
- */
605
- function avgFrontierMI(graph, nodeId, context, mi) {
606
- const { frontierIndex, visitedByFrontier } = context;
607
- let total = 0;
608
- let count = 0;
609
- for (const [visitedId, idx] of visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
610
- total += mi(graph, visitedId, nodeId);
611
- count++;
612
- }
613
- return count > 0 ? total / count : 0;
614
- }
615
- /**
616
- * Count the number of a node's neighbours that have been visited by
617
- * frontiers other than the node's own frontier.
618
- *
619
- * A higher count indicates this node is likely to bridge two frontiers,
620
- * making it a strong candidate for path completion.
621
- *
622
- * @param graph - Source graph
623
- * @param nodeId - Node being evaluated
624
- * @param context - Current priority context
625
- * @returns Number of neighbours visited by other frontiers
626
- */
627
- function countCrossFrontierNeighbours(graph, nodeId, context) {
628
- const { frontierIndex, visitedByFrontier } = context;
629
- const nodeNeighbours = new Set(graph.neighbours(nodeId));
630
- let count = 0;
631
- for (const [visitedId, idx] of visitedByFrontier) if (idx !== frontierIndex && nodeNeighbours.has(visitedId)) count++;
632
- return count;
633
- }
634
- /**
635
- * Incrementally update salience counts for paths discovered since the
636
- * last update.
637
- *
638
- * Iterates only over paths from `fromIndex` onwards, avoiding redundant
639
- * re-processing of already-counted paths.
640
- *
641
- * @param salienceCounts - Mutable map of node ID to salience count (mutated in place)
642
- * @param paths - Full list of discovered paths
643
- * @param fromIndex - Index to start counting from (exclusive of earlier paths)
644
- * @returns The new `fromIndex` value (i.e. `paths.length` after update)
645
- */
646
- function updateSalienceCounts(salienceCounts, paths, fromIndex) {
647
- for (let i = fromIndex; i < paths.length; i++) {
648
- const path = paths[i];
649
- if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
650
- }
651
- return paths.length;
652
- }
653
- //#endregion
654
- //#region src/expansion/sage.ts
655
- /**
656
- * Run SAGE expansion algorithm.
657
- *
658
- * Salience-aware multi-frontier expansion with two phases:
659
- * - Phase 1: Degree-based priority (early exploration)
660
- * - Phase 2: Salience feedback (path-aware frontier steering)
661
- *
662
- * @param graph - Source graph
663
- * @param seeds - Seed nodes for expansion
664
- * @param config - Expansion configuration
665
- * @returns Expansion result with discovered paths
666
- */
667
- function sage(graph, seeds, config) {
668
- const salienceCounts = /* @__PURE__ */ new Map();
669
- let inPhase2 = false;
670
- let lastPathCount = 0;
671
- /**
672
- * SAGE priority function with phase transition logic.
673
- */
674
- function sagePriority(nodeId, context) {
675
- const pathCount = context.discoveredPaths.length;
676
- if (pathCount > 0 && !inPhase2) inPhase2 = true;
677
- if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
678
- if (!inPhase2) return Math.log(context.degree + 1);
679
- return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
680
- }
681
- return base(graph, seeds, {
682
- ...config,
683
- priority: sagePriority
684
- });
685
- }
686
- /**
687
- * Run SAGE expansion asynchronously.
688
- *
689
- * Creates fresh closure state (salienceCounts, phase tracking) for this
690
- * invocation. The SAGE priority function does not access `context.graph`
691
- * directly, so it is safe to use in async mode via `baseAsync`.
692
- *
693
- * @param graph - Async source graph
694
- * @param seeds - Seed nodes for expansion
695
- * @param config - Expansion and async runner configuration
696
- * @returns Promise resolving to the expansion result
697
- */
698
- async function sageAsync(graph, seeds, config) {
699
- const salienceCounts = /* @__PURE__ */ new Map();
700
- let inPhase2 = false;
701
- let lastPathCount = 0;
702
- function sagePriority(nodeId, context) {
703
- const pathCount = context.discoveredPaths.length;
704
- if (pathCount > 0 && !inPhase2) inPhase2 = true;
705
- if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
706
- if (!inPhase2) return Math.log(context.degree + 1);
707
- return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
708
- }
709
- return baseAsync(graph, seeds, {
710
- ...config,
711
- priority: sagePriority
712
- });
713
- }
714
- //#endregion
715
- //#region src/ranking/mi/jaccard.ts
716
- /**
717
- * Compute Jaccard similarity between neighbourhoods of two nodes.
718
- *
719
- * @param graph - Source graph
720
- * @param source - Source node ID
721
- * @param target - Target node ID
722
- * @param config - Optional configuration
723
- * @returns Jaccard coefficient in [0, 1]
724
- */
725
- function jaccard(graph, source, target, config) {
726
- const { epsilon = 1e-10 } = config ?? {};
727
- const { jaccard: jaccardScore, sourceNeighbours, targetNeighbours } = require_utils.computeJaccard(graph, source, target);
728
- if (sourceNeighbours.size === 0 && targetNeighbours.size === 0) return 0;
729
- return Math.max(epsilon, jaccardScore);
730
- }
731
- /**
732
- * Async variant of Jaccard similarity for use with async graph data sources.
733
- *
734
- * Fetches both neighbourhoods concurrently, then applies the same formula.
735
- */
736
- async function jaccardAsync(graph, source, target, config) {
737
- const { epsilon = 1e-10 } = config ?? {};
738
- const [sourceNeighboursArr, targetNeighboursArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
739
- const srcSet = new Set(sourceNeighboursArr.filter((n) => n !== target));
740
- const tgtSet = new Set(targetNeighboursArr.filter((n) => n !== source));
741
- if (srcSet.size === 0 && tgtSet.size === 0) return 0;
742
- let intersection = 0;
743
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
744
- const union = srcSet.size + tgtSet.size - intersection;
745
- const jaccardScore = union > 0 ? intersection / union : 0;
746
- return Math.max(epsilon, jaccardScore);
747
- }
748
- //#endregion
749
- //#region src/expansion/reach.ts
750
- /**
751
- * Run REACH expansion algorithm.
752
- *
753
- * Mutual information-aware multi-frontier expansion with two phases:
754
- * - Phase 1: Degree-based priority (early exploration)
755
- * - Phase 2: Structural similarity feedback (MI-guided frontier steering)
756
- *
757
- * @param graph - Source graph
758
- * @param seeds - Seed nodes for expansion
759
- * @param config - Expansion configuration
760
- * @returns Expansion result with discovered paths
761
- */
762
- function reach(graph, seeds, config) {
763
- let inPhase2 = false;
764
- const jaccardCache = /* @__PURE__ */ new Map();
765
- /**
766
- * Compute Jaccard similarity with caching.
767
- *
768
- * Exploits symmetry of Jaccard (J(A,B) = J(B,A)) to reduce
769
- * duplicate computations when the same pair appears in multiple
770
- * discovered paths. Key format ensures consistent ordering.
771
- */
772
- function cachedJaccard(source, target) {
773
- const key = source < target ? `${source}::${target}` : `${target}::${source}`;
774
- let score = jaccardCache.get(key);
775
- if (score === void 0) {
776
- score = jaccard(graph, source, target);
777
- jaccardCache.set(key, score);
778
- }
779
- return score;
780
- }
781
- /**
782
- * REACH priority function with MI estimation.
783
- */
784
- function reachPriority(nodeId, context) {
785
- if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
786
- if (!inPhase2) return Math.log(context.degree + 1);
787
- let totalMI = 0;
788
- let endpointCount = 0;
789
- for (const path of context.discoveredPaths) {
790
- const fromNodeId = path.fromSeed.id;
791
- const toNodeId = path.toSeed.id;
792
- totalMI += cachedJaccard(nodeId, fromNodeId);
793
- totalMI += cachedJaccard(nodeId, toNodeId);
794
- endpointCount += 2;
795
- }
796
- const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
797
- return Math.log(context.degree + 1) * (1 - miHat);
798
- }
799
- return base(graph, seeds, {
800
- ...config,
801
- priority: reachPriority
802
- });
803
- }
804
- /**
805
- * Run REACH expansion asynchronously.
806
- *
807
- * Creates fresh closure state (phase tracking, Jaccard cache) for this
808
- * invocation. The REACH priority function uses `jaccard(context.graph, ...)`
809
- * in Phase 2; in async mode `context.graph` is the sentinel and will throw.
810
- * Full async equivalence requires PriorityContext refactoring (Phase 4b
811
- * deferred). This export establishes the async API surface.
812
- *
813
- * @param graph - Async source graph
814
- * @param seeds - Seed nodes for expansion
815
- * @param config - Expansion and async runner configuration
816
- * @returns Promise resolving to the expansion result
817
- */
818
- async function reachAsync(graph, seeds, config) {
819
- let inPhase2 = false;
820
- function reachPriority(nodeId, context) {
821
- if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
822
- if (!inPhase2) return Math.log(context.degree + 1);
823
- let totalMI = 0;
824
- let endpointCount = 0;
825
- for (const path of context.discoveredPaths) {
826
- totalMI += jaccard(context.graph, nodeId, path.fromSeed.id);
827
- totalMI += jaccard(context.graph, nodeId, path.toSeed.id);
828
- endpointCount += 2;
829
- }
830
- const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
831
- return Math.log(context.degree + 1) * (1 - miHat);
832
- }
833
- return baseAsync(graph, seeds, {
834
- ...config,
835
- priority: reachPriority
836
- });
837
- }
838
- //#endregion
839
- //#region src/expansion/maze.ts
840
- /** Default threshold for switching to phase 2 (after M paths) */
841
- var DEFAULT_PHASE2_THRESHOLD = 1;
842
- /** Salience weighting factor */
843
- var SALIENCE_WEIGHT = 1e3;
844
- /**
845
- * Run MAZE expansion algorithm.
846
- *
847
- * Multi-phase expansion combining path potential and salience with
848
- * adaptive frontier steering.
849
- *
850
- * @param graph - Source graph
851
- * @param seeds - Seed nodes for expansion
852
- * @param config - Expansion configuration
853
- * @returns Expansion result with discovered paths
854
- */
855
- function maze(graph, seeds, config) {
856
- const salienceCounts = /* @__PURE__ */ new Map();
857
- let inPhase2 = false;
858
- let lastPathCount = 0;
859
- /**
860
- * MAZE priority function with path potential and salience feedback.
861
- */
862
- function mazePriority(nodeId, context) {
863
- const pathCount = context.discoveredPaths.length;
864
- if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
865
- inPhase2 = true;
866
- updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
867
- }
868
- if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
869
- const nodeNeighbours = graph.neighbours(nodeId);
870
- let pathPotential = 0;
871
- for (const neighbour of nodeNeighbours) {
872
- const visitedBy = context.visitedByFrontier.get(neighbour);
873
- if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
874
- }
875
- if (!inPhase2) return context.degree / (1 + pathPotential);
876
- const salience = salienceCounts.get(nodeId) ?? 0;
877
- return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
878
- }
879
- return base(graph, seeds, {
880
- ...config,
881
- priority: mazePriority
882
- });
883
- }
884
- /**
885
- * Run MAZE expansion asynchronously.
886
- *
887
- * Creates fresh closure state (salienceCounts, phase tracking) for this
888
- * invocation. The MAZE priority function accesses `context.graph` to
889
- * retrieve neighbour lists for path potential computation. Full async
890
- * equivalence requires PriorityContext refactoring (Phase 4b deferred).
891
- * This export establishes the async API surface.
892
- *
893
- * @param graph - Async source graph
894
- * @param seeds - Seed nodes for expansion
895
- * @param config - Expansion and async runner configuration
896
- * @returns Promise resolving to the expansion result
897
- */
898
- async function mazeAsync(graph, seeds, config) {
899
- const salienceCounts = /* @__PURE__ */ new Map();
900
- let inPhase2 = false;
901
- let lastPathCount = 0;
902
- function mazePriority(nodeId, context) {
903
- const pathCount = context.discoveredPaths.length;
904
- if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
905
- inPhase2 = true;
906
- updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
907
- }
908
- if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
909
- const nodeNeighbours = context.graph.neighbours(nodeId);
910
- let pathPotential = 0;
911
- for (const neighbour of nodeNeighbours) {
912
- const visitedBy = context.visitedByFrontier.get(neighbour);
913
- if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
914
- }
915
- if (!inPhase2) return context.degree / (1 + pathPotential);
916
- const salience = salienceCounts.get(nodeId) ?? 0;
917
- return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
918
- }
919
- return baseAsync(graph, seeds, {
920
- ...config,
921
- priority: mazePriority
922
- });
923
- }
924
- //#endregion
925
- //#region src/expansion/tide.ts
926
- /**
927
- * TIDE priority function.
928
- *
929
- * Priority = degree(source) + degree(target)
930
- * Lower values = higher priority (explored first)
931
- */
932
- function tidePriority(nodeId, context) {
933
- const graph = context.graph;
934
- let totalDegree = context.degree;
935
- for (const neighbour of graph.neighbours(nodeId)) totalDegree += graph.degree(neighbour);
936
- return totalDegree;
937
- }
938
- /**
939
- * Run TIDE expansion algorithm.
940
- *
941
- * Expands from seeds prioritising low-degree edges first.
942
- * Useful for avoiding hubs and exploring sparse regions.
943
- *
944
- * @param graph - Source graph
945
- * @param seeds - Seed nodes for expansion
946
- * @param config - Expansion configuration
947
- * @returns Expansion result with discovered paths
948
- */
949
- function tide(graph, seeds, config) {
950
- return base(graph, seeds, {
951
- ...config,
952
- priority: tidePriority
953
- });
954
- }
955
- /**
956
- * Run TIDE expansion asynchronously.
957
- *
958
- * Note: the TIDE priority function accesses `context.graph` to retrieve
959
- * neighbour lists and per-neighbour degrees. Full async equivalence
960
- * requires PriorityContext refactoring (Phase 4b deferred). This export
961
- * establishes the async API surface.
962
- *
963
- * @param graph - Async source graph
964
- * @param seeds - Seed nodes for expansion
965
- * @param config - Expansion and async runner configuration
966
- * @returns Promise resolving to the expansion result
967
- */
968
- async function tideAsync(graph, seeds, config) {
969
- return baseAsync(graph, seeds, {
970
- ...config,
971
- priority: tidePriority
972
- });
973
- }
974
- //#endregion
975
- //#region src/expansion/lace.ts
976
- /**
977
- * LACE priority function.
978
- *
979
- * Priority = 1 - avgMI(node, same-frontier visited nodes)
980
- * Higher average MI = lower priority value = explored first
981
- */
982
- function lacePriority(nodeId, context, mi) {
983
- return 1 - avgFrontierMI(context.graph, nodeId, context, mi);
984
- }
985
- /**
986
- * Run LACE expansion algorithm.
987
- *
988
- * Expands from seeds prioritising high-MI edges.
989
- * Useful for finding paths with strong semantic associations.
990
- *
991
- * @param graph - Source graph
992
- * @param seeds - Seed nodes for expansion
993
- * @param config - Expansion configuration with MI function
994
- * @returns Expansion result with discovered paths
995
- */
996
- function lace(graph, seeds, config) {
997
- const { mi = jaccard, ...restConfig } = config ?? {};
998
- const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
999
- return base(graph, seeds, {
1000
- ...restConfig,
1001
- priority
1002
- });
1003
- }
1004
- /**
1005
- * Run LACE expansion asynchronously.
1006
- *
1007
- * Note: the LACE priority function accesses `context.graph` via
1008
- * `avgFrontierMI`. Full async equivalence requires PriorityContext
1009
- * refactoring (Phase 4b deferred). This export establishes the async
1010
- * API surface.
1011
- *
1012
- * @param graph - Async source graph
1013
- * @param seeds - Seed nodes for expansion
1014
- * @param config - LACE configuration combined with async runner options
1015
- * @returns Promise resolving to the expansion result
1016
- */
1017
- async function laceAsync(graph, seeds, config) {
1018
- const { mi = jaccard, ...restConfig } = config ?? {};
1019
- const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
1020
- return baseAsync(graph, seeds, {
1021
- ...restConfig,
1022
- priority
1023
- });
1024
- }
1025
- //#endregion
1026
- //#region src/expansion/warp.ts
1027
- /**
1028
- * WARP priority function.
1029
- *
1030
- * Priority = 1 / (1 + bridge_score)
1031
- * Bridge score = cross-frontier neighbour count plus bonus for nodes
1032
- * already on discovered paths.
1033
- * Higher bridge score = more likely to complete paths = explored first.
1034
- */
1035
- function warpPriority(nodeId, context) {
1036
- let bridgeScore = countCrossFrontierNeighbours(context.graph, nodeId, context);
1037
- for (const path of context.discoveredPaths) if (path.nodes.includes(nodeId)) bridgeScore += 2;
1038
- return 1 / (1 + bridgeScore);
1039
- }
1040
- /**
1041
- * Run WARP expansion algorithm.
1042
- *
1043
- * Expands from seeds prioritising bridge nodes.
1044
- * Useful for finding paths through structurally important nodes.
1045
- *
1046
- * @param graph - Source graph
1047
- * @param seeds - Seed nodes for expansion
1048
- * @param config - Expansion configuration
1049
- * @returns Expansion result with discovered paths
1050
- */
1051
- function warp(graph, seeds, config) {
1052
- return base(graph, seeds, {
1053
- ...config,
1054
- priority: warpPriority
1055
- });
1056
- }
1057
- /**
1058
- * Run WARP expansion asynchronously.
1059
- *
1060
- * Note: the WARP priority function accesses `context.graph` via
1061
- * `countCrossFrontierNeighbours`. Full async equivalence requires
1062
- * PriorityContext refactoring (Phase 4b deferred). This export
1063
- * establishes the async API surface.
1064
- *
1065
- * @param graph - Async source graph
1066
- * @param seeds - Seed nodes for expansion
1067
- * @param config - Expansion and async runner configuration
1068
- * @returns Promise resolving to the expansion result
1069
- */
1070
- async function warpAsync(graph, seeds, config) {
1071
- return baseAsync(graph, seeds, {
1072
- ...config,
1073
- priority: warpPriority
1074
- });
1075
- }
1076
- //#endregion
1077
- //#region src/expansion/fuse.ts
1078
- /**
1079
- * FUSE priority function.
1080
- *
1081
- * Combines degree with average frontier MI as a salience proxy:
1082
- * Priority = (1 - w) * degree + w * (1 - avgMI)
1083
- * Lower values = higher priority; high salience lowers priority
1084
- */
1085
- function fusePriority(nodeId, context, mi, salienceWeight) {
1086
- const avgSalience = avgFrontierMI(context.graph, nodeId, context, mi);
1087
- return (1 - salienceWeight) * context.degree + salienceWeight * (1 - avgSalience);
1088
- }
1089
- /**
1090
- * Run FUSE expansion algorithm.
1091
- *
1092
- * Combines structural exploration with semantic salience.
1093
- * Useful for finding paths that are both short and semantically meaningful.
1094
- *
1095
- * @param graph - Source graph
1096
- * @param seeds - Seed nodes for expansion
1097
- * @param config - Expansion configuration with MI function
1098
- * @returns Expansion result with discovered paths
1099
- */
1100
- function fuse(graph, seeds, config) {
1101
- const { mi = jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
1102
- const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
1103
- return base(graph, seeds, {
1104
- ...restConfig,
1105
- priority
1106
- });
1107
- }
1108
- /**
1109
- * Run FUSE expansion asynchronously.
1110
- *
1111
- * Note: the FUSE priority function accesses `context.graph` via
1112
- * `avgFrontierMI`. Full async equivalence requires PriorityContext
1113
- * refactoring (Phase 4b deferred). This export establishes the async
1114
- * API surface.
1115
- *
1116
- * @param graph - Async source graph
1117
- * @param seeds - Seed nodes for expansion
1118
- * @param config - FUSE configuration combined with async runner options
1119
- * @returns Promise resolving to the expansion result
1120
- */
1121
- async function fuseAsync(graph, seeds, config) {
1122
- const { mi = jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
1123
- const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
1124
- return baseAsync(graph, seeds, {
1125
- ...restConfig,
1126
- priority
1127
- });
1128
- }
1129
- //#endregion
1130
- //#region src/expansion/sift.ts
1131
- /**
1132
- * REACH (SIFT) priority function.
1133
- *
1134
- * Prioritises nodes with average frontier MI above the threshold;
1135
- * falls back to degree-based ordering for those below it.
1136
- */
1137
- function siftPriority(nodeId, context, mi, miThreshold) {
1138
- const avgMi = avgFrontierMI(context.graph, nodeId, context, mi);
1139
- if (avgMi >= miThreshold) return 1 - avgMi;
1140
- else return context.degree + 100;
1141
- }
1142
- /**
1143
- * Run SIFT expansion algorithm.
1144
- *
1145
- * Two-phase adaptive expansion that learns MI thresholds
1146
- * from initial sampling, then uses them for guided expansion.
1147
- *
1148
- * @param graph - Source graph
1149
- * @param seeds - Seed nodes for expansion
1150
- * @param config - Expansion configuration
1151
- * @returns Expansion result with discovered paths
1152
- */
1153
- function sift(graph, seeds, config) {
1154
- const { mi = jaccard, miThreshold = .25, ...restConfig } = config ?? {};
1155
- const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
1156
- return base(graph, seeds, {
1157
- ...restConfig,
1158
- priority
1159
- });
1160
- }
1161
- /**
1162
- * Run SIFT expansion asynchronously.
1163
- *
1164
- * Note: the SIFT priority function accesses `context.graph` via
1165
- * `avgFrontierMI`. Full async equivalence requires PriorityContext
1166
- * refactoring (Phase 4b deferred). This export establishes the async
1167
- * API surface.
1168
- *
1169
- * @param graph - Async source graph
1170
- * @param seeds - Seed nodes for expansion
1171
- * @param config - SIFT (REACHConfig) configuration combined with async runner options
1172
- * @returns Promise resolving to the expansion result
1173
- */
1174
- async function siftAsync(graph, seeds, config) {
1175
- const { mi = jaccard, miThreshold = .25, ...restConfig } = config ?? {};
1176
- const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
1177
- return baseAsync(graph, seeds, {
1178
- ...restConfig,
1179
- priority
1180
- });
1181
- }
1182
- //#endregion
1183
- //#region src/expansion/flux.ts
1184
- /**
1185
- * Compute local density around a node.
1186
- */
1187
- function localDensity(graph, nodeId) {
1188
- const neighbours = Array.from(graph.neighbours(nodeId));
1189
- const degree = neighbours.length;
1190
- if (degree < 2) return 0;
1191
- let edges = 0;
1192
- for (let i = 0; i < neighbours.length; i++) for (let j = i + 1; j < neighbours.length; j++) {
1193
- const ni = neighbours[i];
1194
- const nj = neighbours[j];
1195
- if (ni !== void 0 && nj !== void 0 && graph.getEdge(ni, nj) !== void 0) edges++;
1196
- }
1197
- const maxEdges = degree * (degree - 1) / 2;
1198
- return edges / maxEdges;
1199
- }
1200
- /**
1201
- * MAZE adaptive priority function.
1202
- *
1203
- * Switches strategies based on local conditions:
1204
- * - High density + low bridge: EDGE mode
1205
- * - Low density + low bridge: DOME mode
1206
- * - High bridge score: PIPE mode
1207
- */
1208
- function fluxPriority(nodeId, context, densityThreshold, bridgeThreshold) {
1209
- const graph = context.graph;
1210
- const degree = context.degree;
1211
- const density = localDensity(graph, nodeId);
1212
- const bridge = countCrossFrontierNeighbours(graph, nodeId, context);
1213
- const numFrontiers = new Set(context.visitedByFrontier.values()).size;
1214
- if ((numFrontiers > 0 ? bridge / numFrontiers : 0) >= bridgeThreshold) return 1 / (1 + bridge);
1215
- else if (density >= densityThreshold) return 1 / (degree + 1);
1216
- else return degree;
1217
- }
1218
- /**
1219
- * Run FLUX expansion algorithm.
1220
- *
1221
- * Adaptively switches between expansion strategies based on
1222
- * local graph structure. Useful for heterogeneous graphs
1223
- * with varying density.
1224
- *
1225
- * @param graph - Source graph
1226
- * @param seeds - Seed nodes for expansion
1227
- * @param config - Expansion configuration
1228
- * @returns Expansion result with discovered paths
1229
- */
1230
- function flux(graph, seeds, config) {
1231
- const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
1232
- const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
1233
- return base(graph, seeds, {
1234
- ...restConfig,
1235
- priority
1236
- });
1237
- }
1238
- /**
1239
- * Run FLUX expansion asynchronously.
1240
- *
1241
- * Note: the FLUX priority function accesses `context.graph` to compute
1242
- * local density and cross-frontier bridge scores. Full async equivalence
1243
- * requires PriorityContext refactoring (Phase 4b deferred). This export
1244
- * establishes the async API surface.
1245
- *
1246
- * @param graph - Async source graph
1247
- * @param seeds - Seed nodes for expansion
1248
- * @param config - FLUX (MAZEConfig) configuration combined with async runner options
1249
- * @returns Promise resolving to the expansion result
1250
- */
1251
- async function fluxAsync(graph, seeds, config) {
1252
- const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
1253
- const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
1254
- return baseAsync(graph, seeds, {
1255
- ...restConfig,
1256
- priority
1257
- });
1258
- }
1259
- //#endregion
1260
- //#region src/expansion/standard-bfs.ts
1261
- /**
1262
- * BFS priority: discovery iteration order (FIFO).
1263
- */
1264
- function bfsPriority(_nodeId, context) {
1265
- return context.iteration;
1266
- }
1267
- /**
1268
- * Run standard BFS expansion (FIFO discovery order).
1269
- *
1270
- * @param graph - Source graph
1271
- * @param seeds - Seed nodes for expansion
1272
- * @param config - Expansion configuration
1273
- * @returns Expansion result with discovered paths
1274
- */
1275
- function standardBfs(graph, seeds, config) {
1276
- return base(graph, seeds, {
1277
- ...config,
1278
- priority: bfsPriority
1279
- });
1280
- }
1281
- /**
1282
- * Run standard BFS expansion asynchronously (FIFO discovery order).
1283
- *
1284
- * @param graph - Async source graph
1285
- * @param seeds - Seed nodes for expansion
1286
- * @param config - Expansion and async runner configuration
1287
- * @returns Promise resolving to the expansion result
1288
- */
1289
- async function standardBfsAsync(graph, seeds, config) {
1290
- return baseAsync(graph, seeds, {
1291
- ...config,
1292
- priority: bfsPriority
1293
- });
1294
- }
1295
- //#endregion
1296
- //#region src/expansion/frontier-balanced.ts
1297
- /**
1298
- * Frontier-balanced priority: frontier index dominates, then discovery iteration.
1299
- * Scales frontier index by 1e9 to ensure round-robin ordering across frontiers.
1300
- */
1301
- function balancedPriority(_nodeId, context) {
1302
- return context.frontierIndex * 1e9 + context.iteration;
1303
- }
1304
- /**
1305
- * Run frontier-balanced expansion (round-robin across frontiers).
1306
- *
1307
- * @param graph - Source graph
1308
- * @param seeds - Seed nodes for expansion
1309
- * @param config - Expansion configuration
1310
- * @returns Expansion result with discovered paths
1311
- */
1312
- function frontierBalanced(graph, seeds, config) {
1313
- return base(graph, seeds, {
1314
- ...config,
1315
- priority: balancedPriority
1316
- });
1317
- }
1318
- /**
1319
- * Run frontier-balanced expansion asynchronously (round-robin across frontiers).
1320
- *
1321
- * @param graph - Async source graph
1322
- * @param seeds - Seed nodes for expansion
1323
- * @param config - Expansion and async runner configuration
1324
- * @returns Promise resolving to the expansion result
1325
- */
1326
- async function frontierBalancedAsync(graph, seeds, config) {
1327
- return baseAsync(graph, seeds, {
1328
- ...config,
1329
- priority: balancedPriority
1330
- });
1331
- }
1332
- //#endregion
1333
- //#region src/expansion/random-priority.ts
1334
- /**
1335
- * Deterministic seeded random number generator.
1336
- * Uses FNV-1a-like hash for input → [0, 1] output.
1337
- *
1338
- * @param input - String to hash
1339
- * @param seed - Random seed for reproducibility
1340
- * @returns Deterministic random value in [0, 1]
1341
- */
1342
- function seededRandom$1(input, seed = 0) {
1343
- let h = seed;
1344
- for (let i = 0; i < input.length; i++) {
1345
- h = Math.imul(h ^ input.charCodeAt(i), 2654435769);
1346
- h ^= h >>> 16;
1347
- }
1348
- return (h >>> 0) / 4294967295;
1349
- }
1350
- /**
1351
- * Build a seeded random priority function for a given seed value.
1352
- */
1353
- function makeRandomPriorityFn(seed) {
1354
- return (nodeId) => seededRandom$1(nodeId, seed);
1355
- }
1356
- /**
1357
- * Run random-priority expansion (null hypothesis baseline).
1358
- *
1359
- * @param graph - Source graph
1360
- * @param seeds - Seed nodes for expansion
1361
- * @param config - Expansion configuration
1362
- * @returns Expansion result with discovered paths
1363
- */
1364
- function randomPriority(graph, seeds, config) {
1365
- const { seed = 0 } = config ?? {};
1366
- return base(graph, seeds, {
1367
- ...config,
1368
- priority: makeRandomPriorityFn(seed)
1369
- });
1370
- }
1371
- /**
1372
- * Run random-priority expansion asynchronously (null hypothesis baseline).
1373
- *
1374
- * @param graph - Async source graph
1375
- * @param seeds - Seed nodes for expansion
1376
- * @param config - Expansion and async runner configuration
1377
- * @returns Promise resolving to the expansion result
1378
- */
1379
- async function randomPriorityAsync(graph, seeds, config) {
1380
- const { seed = 0 } = config ?? {};
1381
- return baseAsync(graph, seeds, {
1382
- ...config,
1383
- priority: makeRandomPriorityFn(seed)
1384
- });
1385
- }
1386
- //#endregion
1387
- //#region src/expansion/dfs-priority.ts
1388
- /**
1389
- * DFS priority function: negative iteration produces LIFO ordering.
1390
- *
1391
- * Lower priority values are expanded first, so negating the iteration
1392
- * counter ensures the most recently enqueued node is always next.
1393
- */
1394
- function dfsPriorityFn(_nodeId, context) {
1395
- return -context.iteration;
1396
- }
1397
- /**
1398
- * Run DFS-priority expansion (LIFO discovery order).
1399
- *
1400
- * Uses the BASE framework with a negative-iteration priority function,
1401
- * which causes the most recently discovered node to be expanded first —
1402
- * equivalent to depth-first search behaviour.
1403
- *
1404
- * @param graph - Source graph
1405
- * @param seeds - Seed nodes for expansion
1406
- * @param config - Expansion configuration
1407
- * @returns Expansion result with discovered paths
1408
- */
1409
- function dfsPriority(graph, seeds, config) {
1410
- return base(graph, seeds, {
1411
- ...config,
1412
- priority: dfsPriorityFn
1413
- });
1414
- }
1415
- /**
1416
- * Run DFS-priority expansion asynchronously (LIFO discovery order).
1417
- *
1418
- * @param graph - Async source graph
1419
- * @param seeds - Seed nodes for expansion
1420
- * @param config - Expansion and async runner configuration
1421
- * @returns Promise resolving to the expansion result
1422
- */
1423
- async function dfsPriorityAsync(graph, seeds, config) {
1424
- return baseAsync(graph, seeds, {
1425
- ...config,
1426
- priority: dfsPriorityFn
1427
- });
1428
- }
1429
- //#endregion
1430
- //#region src/expansion/k-hop.ts
1431
- /**
1432
- * Run k-hop expansion (fixed-depth BFS).
1433
- *
1434
- * Explores all nodes reachable within exactly k hops of any seed using
1435
- * breadth-first search. Paths between seeds are detected when a node
1436
- * is reached by frontiers from two different seeds.
1437
- *
1438
- * @param graph - Source graph
1439
- * @param seeds - Seed nodes for expansion
1440
- * @param config - K-hop configuration (k defaults to 2)
1441
- * @returns Expansion result with discovered paths
1442
- */
1443
- function kHop(graph, seeds, config) {
1444
- const startTime = performance.now();
1445
- const { k = 2 } = config ?? {};
1446
- if (seeds.length === 0) return emptyResult$1(startTime);
1447
- const visitedByFrontier = seeds.map(() => /* @__PURE__ */ new Map());
1448
- const firstVisitedBy = /* @__PURE__ */ new Map();
1449
- const allVisited = /* @__PURE__ */ new Set();
1450
- const sampledEdgeMap = /* @__PURE__ */ new Map();
1451
- const discoveredPaths = [];
1452
- let iterations = 0;
1453
- let edgesTraversed = 0;
1454
- for (let i = 0; i < seeds.length; i++) {
1455
- const seed = seeds[i];
1456
- if (seed === void 0) continue;
1457
- if (!graph.hasNode(seed.id)) continue;
1458
- visitedByFrontier[i]?.set(seed.id, null);
1459
- allVisited.add(seed.id);
1460
- if (!firstVisitedBy.has(seed.id)) firstVisitedBy.set(seed.id, i);
1461
- else {
1462
- const otherIdx = firstVisitedBy.get(seed.id) ?? -1;
1463
- if (otherIdx < 0) continue;
1464
- const fromSeed = seeds[otherIdx];
1465
- const toSeed = seeds[i];
1466
- if (fromSeed !== void 0 && toSeed !== void 0) discoveredPaths.push({
1467
- fromSeed,
1468
- toSeed,
1469
- nodes: [seed.id]
1470
- });
1471
- }
1472
- }
1473
- let currentLevel = seeds.map((s, i) => {
1474
- const frontier = visitedByFrontier[i];
1475
- if (frontier === void 0) return [];
1476
- return frontier.has(s.id) ? [s.id] : [];
1477
- });
1478
- for (let hop = 0; hop < k; hop++) {
1479
- const nextLevel = seeds.map(() => []);
1480
- for (let i = 0; i < seeds.length; i++) {
1481
- const level = currentLevel[i];
1482
- if (level === void 0) continue;
1483
- const frontierVisited = visitedByFrontier[i];
1484
- if (frontierVisited === void 0) continue;
1485
- for (const nodeId of level) {
1486
- iterations++;
1487
- for (const neighbour of graph.neighbours(nodeId)) {
1488
- edgesTraversed++;
1489
- const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
1490
- let targets = sampledEdgeMap.get(s);
1491
- if (targets === void 0) {
1492
- targets = /* @__PURE__ */ new Set();
1493
- sampledEdgeMap.set(s, targets);
1494
- }
1495
- targets.add(t);
1496
- if (frontierVisited.has(neighbour)) continue;
1497
- frontierVisited.set(neighbour, nodeId);
1498
- allVisited.add(neighbour);
1499
- nextLevel[i]?.push(neighbour);
1500
- const previousFrontier = firstVisitedBy.get(neighbour);
1501
- if (previousFrontier !== void 0 && previousFrontier !== i) {
1502
- const fromSeed = seeds[previousFrontier];
1503
- const toSeed = seeds[i];
1504
- if (fromSeed !== void 0 && toSeed !== void 0) {
1505
- const path = reconstructPath(neighbour, previousFrontier, i, visitedByFrontier, seeds);
1506
- if (path !== null) {
1507
- 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);
1508
- }
1509
- }
1510
- }
1511
- if (!firstVisitedBy.has(neighbour)) firstVisitedBy.set(neighbour, i);
1512
- }
1513
- }
1514
- }
1515
- currentLevel = nextLevel;
1516
- if (currentLevel.every((level) => level.length === 0)) break;
1517
- }
1518
- const endTime = performance.now();
1519
- const edgeTuples = /* @__PURE__ */ new Set();
1520
- for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
1521
- return {
1522
- paths: discoveredPaths,
1523
- sampledNodes: allVisited,
1524
- sampledEdges: edgeTuples,
1525
- visitedPerFrontier: visitedByFrontier.map((m) => new Set(m.keys())),
1526
- stats: {
1527
- iterations,
1528
- nodesVisited: allVisited.size,
1529
- edgesTraversed,
1530
- pathsFound: discoveredPaths.length,
1531
- durationMs: endTime - startTime,
1532
- algorithm: "k-hop",
1533
- termination: "exhausted"
1534
- }
1535
- };
1536
- }
1537
- /**
1538
- * Reconstruct the path between two colliding frontiers.
1539
- */
1540
- function reconstructPath(collisionNode, frontierA, frontierB, visitedByFrontier, seeds) {
1541
- const seedA = seeds[frontierA];
1542
- const seedB = seeds[frontierB];
1543
- if (seedA === void 0 || seedB === void 0) return null;
1544
- const pathA = [collisionNode];
1545
- const predA = visitedByFrontier[frontierA];
1546
- if (predA !== void 0) {
1547
- let node = collisionNode;
1548
- let pred = predA.get(node);
1549
- while (pred !== null && pred !== void 0) {
1550
- pathA.unshift(pred);
1551
- node = pred;
1552
- pred = predA.get(node);
1553
- }
1554
- }
1555
- const pathB = [];
1556
- const predB = visitedByFrontier[frontierB];
1557
- if (predB !== void 0) {
1558
- let node = collisionNode;
1559
- let pred = predB.get(node);
1560
- while (pred !== null && pred !== void 0) {
1561
- pathB.push(pred);
1562
- node = pred;
1563
- pred = predB.get(node);
1564
- }
1565
- }
1566
- return {
1567
- fromSeed: seedA,
1568
- toSeed: seedB,
1569
- nodes: [...pathA, ...pathB]
1570
- };
1571
- }
1572
- /**
1573
- * Create an empty result for early termination (no seeds).
1574
- */
1575
- function emptyResult$1(startTime) {
1576
- return {
1577
- paths: [],
1578
- sampledNodes: /* @__PURE__ */ new Set(),
1579
- sampledEdges: /* @__PURE__ */ new Set(),
1580
- visitedPerFrontier: [],
1581
- stats: {
1582
- iterations: 0,
1583
- nodesVisited: 0,
1584
- edgesTraversed: 0,
1585
- pathsFound: 0,
1586
- durationMs: performance.now() - startTime,
1587
- algorithm: "k-hop",
1588
- termination: "exhausted"
1589
- }
1590
- };
1591
- }
1592
- //#endregion
1593
- //#region src/expansion/random-walk.ts
1594
- /**
1595
- * Mulberry32 seeded PRNG — fast, compact, and high-quality for simulation.
1596
- *
1597
- * Returns a closure that yields the next pseudo-random value in [0, 1)
1598
- * on each call.
1599
- *
1600
- * @param seed - 32-bit integer seed
1601
- */
1602
- function mulberry32(seed) {
1603
- let s = seed;
1604
- return () => {
1605
- s += 1831565813;
1606
- let t = s;
1607
- t = Math.imul(t ^ t >>> 15, t | 1);
1608
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1609
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1610
- };
1611
- }
1612
- /**
1613
- * Run random-walk-with-restart expansion.
1614
- *
1615
- * For each seed, performs `walks` independent random walks of up to
1616
- * `walkLength` steps. At each step the walk either restarts (with
1617
- * probability `restartProbability`) or moves to a uniformly sampled
1618
- * neighbour. All visited nodes and traversed edges are collected.
1619
- *
1620
- * Inter-seed paths are detected when a walk reaches a node that was
1621
- * previously reached by a walk originating from a different seed.
1622
- * The recorded path contains only the two seed endpoints rather than
1623
- * the full walk trajectory, consistent with the ExpansionPath contract.
1624
- *
1625
- * @param graph - Source graph
1626
- * @param seeds - Seed nodes for expansion
1627
- * @param config - Random walk configuration
1628
- * @returns Expansion result with discovered paths
1629
- */
1630
- function randomWalk(graph, seeds, config) {
1631
- const startTime = performance.now();
1632
- const { restartProbability = .15, walks = 10, walkLength = 20, seed = 0 } = config ?? {};
1633
- if (seeds.length === 0) return emptyResult(startTime);
1634
- const rand = mulberry32(seed);
1635
- const firstVisitedBySeed = /* @__PURE__ */ new Map();
1636
- const allVisited = /* @__PURE__ */ new Set();
1637
- const sampledEdgeMap = /* @__PURE__ */ new Map();
1638
- const discoveredPaths = [];
1639
- let iterations = 0;
1640
- let edgesTraversed = 0;
1641
- const visitedPerFrontier = seeds.map(() => /* @__PURE__ */ new Set());
1642
- for (let seedIdx = 0; seedIdx < seeds.length; seedIdx++) {
1643
- const seed_ = seeds[seedIdx];
1644
- if (seed_ === void 0) continue;
1645
- const seedId = seed_.id;
1646
- if (!graph.hasNode(seedId)) continue;
1647
- if (!firstVisitedBySeed.has(seedId)) firstVisitedBySeed.set(seedId, seedIdx);
1648
- allVisited.add(seedId);
1649
- visitedPerFrontier[seedIdx]?.add(seedId);
1650
- for (let w = 0; w < walks; w++) {
1651
- let current = seedId;
1652
- for (let step = 0; step < walkLength; step++) {
1653
- iterations++;
1654
- if (rand() < restartProbability) {
1655
- current = seedId;
1656
- continue;
1657
- }
1658
- const neighbourList = [];
1659
- for (const nb of graph.neighbours(current)) neighbourList.push(nb);
1660
- if (neighbourList.length === 0) {
1661
- current = seedId;
1662
- continue;
1663
- }
1664
- const next = neighbourList[Math.floor(rand() * neighbourList.length)];
1665
- if (next === void 0) {
1666
- current = seedId;
1667
- continue;
1668
- }
1669
- edgesTraversed++;
1670
- const [s, t] = current < next ? [current, next] : [next, current];
1671
- let targets = sampledEdgeMap.get(s);
1672
- if (targets === void 0) {
1673
- targets = /* @__PURE__ */ new Set();
1674
- sampledEdgeMap.set(s, targets);
1675
- }
1676
- targets.add(t);
1677
- const previousSeedIdx = firstVisitedBySeed.get(next);
1678
- if (previousSeedIdx !== void 0 && previousSeedIdx !== seedIdx) {
1679
- const fromSeed = seeds[previousSeedIdx];
1680
- const toSeed = seeds[seedIdx];
1681
- if (fromSeed !== void 0 && toSeed !== void 0) {
1682
- const path = {
1683
- fromSeed,
1684
- toSeed,
1685
- nodes: [
1686
- fromSeed.id,
1687
- next,
1688
- toSeed.id
1689
- ].filter((n, i, arr) => arr.indexOf(n) === i)
1690
- };
1691
- 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);
1692
- }
1693
- }
1694
- if (!firstVisitedBySeed.has(next)) firstVisitedBySeed.set(next, seedIdx);
1695
- allVisited.add(next);
1696
- visitedPerFrontier[seedIdx]?.add(next);
1697
- current = next;
1698
- }
1699
- }
1700
- }
1701
- const endTime = performance.now();
1702
- const edgeTuples = /* @__PURE__ */ new Set();
1703
- for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
1704
- return {
1705
- paths: discoveredPaths,
1706
- sampledNodes: allVisited,
1707
- sampledEdges: edgeTuples,
1708
- visitedPerFrontier,
1709
- stats: {
1710
- iterations,
1711
- nodesVisited: allVisited.size,
1712
- edgesTraversed,
1713
- pathsFound: discoveredPaths.length,
1714
- durationMs: endTime - startTime,
1715
- algorithm: "random-walk",
1716
- termination: "exhausted"
1717
- }
1718
- };
1719
- }
1720
- /**
1721
- * Create an empty result for early termination (no seeds).
1722
- */
1723
- function emptyResult(startTime) {
1724
- return {
1725
- paths: [],
1726
- sampledNodes: /* @__PURE__ */ new Set(),
1727
- sampledEdges: /* @__PURE__ */ new Set(),
1728
- visitedPerFrontier: [],
1729
- stats: {
1730
- iterations: 0,
1731
- nodesVisited: 0,
1732
- edgesTraversed: 0,
1733
- pathsFound: 0,
1734
- durationMs: performance.now() - startTime,
1735
- algorithm: "random-walk",
1736
- termination: "exhausted"
1737
- }
1738
- };
1739
- }
1740
- //#endregion
1741
- //#region src/ranking/parse.ts
1742
- /**
1743
- * Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
1744
- *
1745
- * Computes geometric mean of edge MI scores for each path,
1746
- * then sorts by salience (highest first).
1747
- *
1748
- * @param graph - Source graph
1749
- * @param paths - Paths to rank
1750
- * @param config - Configuration options
1751
- * @returns Ranked paths with statistics
1752
- */
1753
- function parse(graph, paths, config) {
1754
- const startTime = performance.now();
1755
- const { mi = jaccard, epsilon = 1e-10 } = config ?? {};
1756
- const rankedPaths = [];
1757
- for (const path of paths) {
1758
- const salience = computePathSalience(graph, path, mi, epsilon);
1759
- rankedPaths.push({
1760
- ...path,
1761
- salience
1762
- });
1763
- }
1764
- rankedPaths.sort((a, b) => b.salience - a.salience);
1765
- const endTime = performance.now();
1766
- const saliences = rankedPaths.map((p) => p.salience);
1767
- const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
1768
- const sortedSaliences = [...saliences].sort((a, b) => a - b);
1769
- const mid = Math.floor(sortedSaliences.length / 2);
1770
- const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
1771
- const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
1772
- const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
1773
- return {
1774
- paths: rankedPaths,
1775
- stats: {
1776
- pathsRanked: rankedPaths.length,
1777
- meanSalience,
1778
- medianSalience,
1779
- maxSalience,
1780
- minSalience,
1781
- durationMs: endTime - startTime
1782
- }
1783
- };
1784
- }
1785
- /**
1786
- * Rank paths using async PARSE (Path-Aware Ranking via Salience Estimation).
1787
- *
1788
- * Async variant suitable for use with remote or lazy graph data sources.
1789
- * Computes geometric mean of edge MI scores for each path using Promise.all
1790
- * for parallelism, then sorts by salience (highest first).
1791
- *
1792
- * @param graph - Async source graph
1793
- * @param paths - Paths to rank
1794
- * @param config - Configuration options
1795
- * @returns Ranked paths with statistics
1796
- */
1797
- async function parseAsync(graph, paths, config) {
1798
- const startTime = performance.now();
1799
- const { mi = jaccardAsync, epsilon = 1e-10 } = config ?? {};
1800
- const rankedPaths = [];
1801
- for (const path of paths) {
1802
- const salience = await computePathSalienceAsync(graph, path, mi, epsilon);
1803
- rankedPaths.push({
1804
- ...path,
1805
- salience
1806
- });
1807
- }
1808
- rankedPaths.sort((a, b) => b.salience - a.salience);
1809
- const endTime = performance.now();
1810
- const saliences = rankedPaths.map((p) => p.salience);
1811
- const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
1812
- const sortedSaliences = [...saliences].sort((a, b) => a - b);
1813
- const mid = Math.floor(sortedSaliences.length / 2);
1814
- const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
1815
- const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
1816
- const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
1817
- return {
1818
- paths: rankedPaths,
1819
- stats: {
1820
- pathsRanked: rankedPaths.length,
1821
- meanSalience,
1822
- medianSalience,
1823
- maxSalience,
1824
- minSalience,
1825
- durationMs: endTime - startTime
1826
- }
1827
- };
1828
- }
1829
- /**
1830
- * Compute salience for a single path asynchronously.
1831
- *
1832
- * Uses geometric mean of edge MI scores for length-unbiased ranking.
1833
- * Edge MI values are computed in parallel via Promise.all.
1834
- */
1835
- async function computePathSalienceAsync(graph, path, mi, epsilon) {
1836
- const nodes = path.nodes;
1837
- if (nodes.length < 2) return epsilon;
1838
- const edgeMIs = await Promise.all(nodes.slice(0, -1).map((source, i) => {
1839
- const target = nodes[i + 1];
1840
- if (target !== void 0) return mi(graph, source, target);
1841
- return Promise.resolve(epsilon);
1842
- }));
1843
- let productMi = 1;
1844
- let edgeCount = 0;
1845
- for (const edgeMi of edgeMIs) {
1846
- productMi *= Math.max(epsilon, edgeMi);
1847
- edgeCount++;
1848
- }
1849
- if (edgeCount === 0) return epsilon;
1850
- const salience = Math.pow(productMi, 1 / edgeCount);
1851
- return Math.max(epsilon, Math.min(1, salience));
1852
- }
1853
- /**
1854
- * Compute salience for a single path.
1855
- *
1856
- * Uses geometric mean of edge MI scores for length-unbiased ranking.
1857
- */
1858
- function computePathSalience(graph, path, mi, epsilon) {
1859
- const nodes = path.nodes;
1860
- if (nodes.length < 2) return epsilon;
1861
- let productMi = 1;
1862
- let edgeCount = 0;
1863
- for (let i = 0; i < nodes.length - 1; i++) {
1864
- const source = nodes[i];
1865
- const target = nodes[i + 1];
1866
- if (source !== void 0 && target !== void 0) {
1867
- const edgeMi = mi(graph, source, target);
1868
- productMi *= Math.max(epsilon, edgeMi);
1869
- edgeCount++;
1870
- }
1871
- }
1872
- if (edgeCount === 0) return epsilon;
1873
- const salience = Math.pow(productMi, 1 / edgeCount);
1874
- return Math.max(epsilon, Math.min(1, salience));
1875
- }
1876
- //#endregion
1877
- //#region src/ranking/mi/adamic-adar.ts
1878
- /**
1879
- * Compute Adamic-Adar index between neighbourhoods of two nodes.
1880
- *
1881
- * @param graph - Source graph
1882
- * @param source - Source node ID
1883
- * @param target - Target node ID
1884
- * @param config - Optional configuration
1885
- * @returns Adamic-Adar index (normalised to [0, 1] if configured)
1886
- */
1887
- function adamicAdar(graph, source, target, config) {
1888
- const { epsilon = 1e-10, normalise = true } = config ?? {};
1889
- const commonNeighbours = require_utils.neighbourIntersection(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
1890
- let score = 0;
1891
- for (const neighbour of commonNeighbours) {
1892
- const degree = graph.degree(neighbour);
1893
- score += 1 / Math.log(degree + 1);
1894
- }
1895
- if (normalise && commonNeighbours.size > 0) {
1896
- const maxScore = commonNeighbours.size / Math.log(2);
1897
- score = score / maxScore;
1898
- }
1899
- return Math.max(epsilon, score);
1900
- }
1901
- /**
1902
- * Async variant of Adamic-Adar index for use with async graph data sources.
1903
- *
1904
- * Fetches both neighbourhoods concurrently, then fetches degree for each common
1905
- * neighbour to compute the inverse-log-degree weighted sum.
1906
- */
1907
- async function adamicAdarAsync(graph, source, target, config) {
1908
- const { epsilon = 1e-10, normalise = true } = config ?? {};
1909
- const [sourceArr, targetArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
1910
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
1911
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
1912
- const commonNeighbours = [];
1913
- for (const n of srcSet) if (tgtSet.has(n)) commonNeighbours.push(n);
1914
- if (commonNeighbours.length === 0) return epsilon;
1915
- const degrees = await Promise.all(commonNeighbours.map((n) => graph.degree(n)));
1916
- let score = 0;
1917
- for (const degree of degrees) score += 1 / Math.log(degree + 1);
1918
- if (normalise) {
1919
- const maxScore = commonNeighbours.length / Math.log(2);
1920
- score = score / maxScore;
1921
- }
1922
- return Math.max(epsilon, score);
1923
- }
1924
- //#endregion
1925
- //#region src/ranking/mi/cosine.ts
1926
- /**
1927
- * Compute cosine similarity between neighbourhoods of two nodes.
1928
- *
1929
- * @param graph - Source graph
1930
- * @param source - Source node ID
1931
- * @param target - Target node ID
1932
- * @param config - Optional configuration
1933
- * @returns Cosine similarity in [0, 1]
1934
- */
1935
- function cosine(graph, source, target, config) {
1936
- const { epsilon = 1e-10 } = config ?? {};
1937
- const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
1938
- const targetNeighbours = require_utils.neighbourSet(graph, target, source);
1939
- const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
1940
- const denominator = Math.sqrt(sourceNeighbours.size) * Math.sqrt(targetNeighbours.size);
1941
- if (denominator === 0) return 0;
1942
- const score = intersection / denominator;
1943
- return Math.max(epsilon, score);
1944
- }
1945
- /**
1946
- * Async variant of cosine similarity for use with async graph data sources.
1947
- *
1948
- * Fetches both neighbourhoods concurrently, then applies the same formula.
1949
- */
1950
- async function cosineAsync(graph, source, target, config) {
1951
- const { epsilon = 1e-10 } = config ?? {};
1952
- const [sourceArr, targetArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
1953
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
1954
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
1955
- let intersection = 0;
1956
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
1957
- const denominator = Math.sqrt(srcSet.size) * Math.sqrt(tgtSet.size);
1958
- if (denominator === 0) return 0;
1959
- const score = intersection / denominator;
1960
- return Math.max(epsilon, score);
1961
- }
1962
- //#endregion
1963
- //#region src/ranking/mi/sorensen.ts
1964
- /**
1965
- * Compute Sorensen-Dice similarity between neighbourhoods of two nodes.
1966
- *
1967
- * @param graph - Source graph
1968
- * @param source - Source node ID
1969
- * @param target - Target node ID
1970
- * @param config - Optional configuration
1971
- * @returns Sorensen-Dice coefficient in [0, 1]
1972
- */
1973
- function sorensen(graph, source, target, config) {
1974
- const { epsilon = 1e-10 } = config ?? {};
1975
- const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
1976
- const targetNeighbours = require_utils.neighbourSet(graph, target, source);
1977
- const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
1978
- const denominator = sourceNeighbours.size + targetNeighbours.size;
1979
- if (denominator === 0) return 0;
1980
- const score = 2 * intersection / denominator;
1981
- return Math.max(epsilon, score);
1982
- }
1983
- /**
1984
- * Async variant of Sorensen-Dice coefficient for use with async graph data sources.
1985
- *
1986
- * Fetches both neighbourhoods concurrently, then applies the same formula.
1987
- */
1988
- async function sorensenAsync(graph, source, target, config) {
1989
- const { epsilon = 1e-10 } = config ?? {};
1990
- const [sourceArr, targetArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
1991
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
1992
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
1993
- let intersection = 0;
1994
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
1995
- const denominator = srcSet.size + tgtSet.size;
1996
- if (denominator === 0) return 0;
1997
- const score = 2 * intersection / denominator;
1998
- return Math.max(epsilon, score);
1999
- }
2000
- //#endregion
2001
- //#region src/ranking/mi/resource-allocation.ts
2002
- /**
2003
- * Compute Resource Allocation index between neighbourhoods of two nodes.
2004
- *
2005
- * @param graph - Source graph
2006
- * @param source - Source node ID
2007
- * @param target - Target node ID
2008
- * @param config - Optional configuration
2009
- * @returns Resource Allocation index (normalised to [0, 1] if configured)
2010
- */
2011
- function resourceAllocation(graph, source, target, config) {
2012
- const { epsilon = 1e-10, normalise = true } = config ?? {};
2013
- const commonNeighbours = require_utils.neighbourIntersection(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
2014
- let score = 0;
2015
- for (const neighbour of commonNeighbours) {
2016
- const degree = graph.degree(neighbour);
2017
- if (degree > 0) score += 1 / degree;
2018
- }
2019
- if (normalise && commonNeighbours.size > 0) {
2020
- const maxScore = commonNeighbours.size;
2021
- score = score / maxScore;
2022
- }
2023
- return Math.max(epsilon, score);
2024
- }
2025
- /**
2026
- * Async variant of Resource Allocation index for use with async graph data sources.
2027
- *
2028
- * Fetches both neighbourhoods concurrently, then fetches degree for each common
2029
- * neighbour to compute the inverse-degree weighted sum.
2030
- */
2031
- async function resourceAllocationAsync(graph, source, target, config) {
2032
- const { epsilon = 1e-10, normalise = true } = config ?? {};
2033
- const [sourceArr, targetArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
2034
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2035
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2036
- const commonNeighbours = [];
2037
- for (const n of srcSet) if (tgtSet.has(n)) commonNeighbours.push(n);
2038
- if (commonNeighbours.length === 0) return epsilon;
2039
- const degrees = await Promise.all(commonNeighbours.map((n) => graph.degree(n)));
2040
- let score = 0;
2041
- for (const degree of degrees) if (degree > 0) score += 1 / degree;
2042
- if (normalise) {
2043
- const maxScore = commonNeighbours.length;
2044
- score = score / maxScore;
2045
- }
2046
- return Math.max(epsilon, score);
2047
- }
2048
- //#endregion
2049
- //#region src/ranking/mi/overlap-coefficient.ts
2050
- /**
2051
- * Compute Overlap Coefficient between neighbourhoods of two nodes.
2052
- *
2053
- * @param graph - Source graph
2054
- * @param source - Source node ID
2055
- * @param target - Target node ID
2056
- * @param config - Optional configuration
2057
- * @returns Overlap Coefficient in [0, 1]
2058
- */
2059
- function overlapCoefficient(graph, source, target, config) {
2060
- const { epsilon = 1e-10 } = config ?? {};
2061
- const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
2062
- const targetNeighbours = require_utils.neighbourSet(graph, target, source);
2063
- const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
2064
- const denominator = Math.min(sourceNeighbours.size, targetNeighbours.size);
2065
- if (denominator === 0) return 0;
2066
- const score = intersection / denominator;
2067
- return Math.max(epsilon, score);
2068
- }
2069
- /**
2070
- * Async variant of Overlap Coefficient for use with async graph data sources.
2071
- *
2072
- * Fetches both neighbourhoods concurrently, then applies the same formula.
2073
- */
2074
- async function overlapCoefficientAsync(graph, source, target, config) {
2075
- const { epsilon = 1e-10 } = config ?? {};
2076
- const [sourceArr, targetArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
2077
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2078
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2079
- let intersection = 0;
2080
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2081
- const denominator = Math.min(srcSet.size, tgtSet.size);
2082
- if (denominator === 0) return 0;
2083
- const score = intersection / denominator;
2084
- return Math.max(epsilon, score);
2085
- }
2086
- //#endregion
2087
- //#region src/ranking/mi/hub-promoted.ts
2088
- /**
2089
- * Compute Hub Promoted index between neighbourhoods of two nodes.
2090
- *
2091
- * @param graph - Source graph
2092
- * @param source - Source node ID
2093
- * @param target - Target node ID
2094
- * @param config - Optional configuration
2095
- * @returns Hub Promoted index in [0, 1]
2096
- */
2097
- function hubPromoted(graph, source, target, config) {
2098
- const { epsilon = 1e-10 } = config ?? {};
2099
- const { intersection } = require_utils.neighbourOverlap(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
2100
- const sourceDegree = graph.degree(source);
2101
- const targetDegree = graph.degree(target);
2102
- const denominator = Math.min(sourceDegree, targetDegree);
2103
- if (denominator === 0) return 0;
2104
- const score = intersection / denominator;
2105
- return Math.max(epsilon, score);
2106
- }
2107
- /**
2108
- * Async variant of Hub Promoted index for use with async graph data sources.
2109
- *
2110
- * Fetches both neighbourhoods and degrees concurrently, then applies the same formula.
2111
- */
2112
- async function hubPromotedAsync(graph, source, target, config) {
2113
- const { epsilon = 1e-10 } = config ?? {};
2114
- const [sourceArr, targetArr, sourceDegree, targetDegree] = await Promise.all([
2115
- require_async.collectAsyncIterable(graph.neighbours(source)),
2116
- require_async.collectAsyncIterable(graph.neighbours(target)),
2117
- graph.degree(source),
2118
- graph.degree(target)
2119
- ]);
2120
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2121
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2122
- let intersection = 0;
2123
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2124
- const denominator = Math.min(sourceDegree, targetDegree);
2125
- if (denominator === 0) return 0;
2126
- const score = intersection / denominator;
2127
- return Math.max(epsilon, score);
2128
- }
2129
- //#endregion
2130
- //#region src/ranking/mi/scale.ts
2131
- /**
2132
- * Compute SCALE MI between two nodes.
2133
- */
2134
- function scale(graph, source, target, config) {
2135
- const { epsilon = 1e-10 } = config ?? {};
2136
- const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
2137
- const n = graph.nodeCount;
2138
- const m = graph.edgeCount;
2139
- const possibleEdges = n * (n - 1);
2140
- const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
2141
- if (density === 0) return epsilon;
2142
- const score = jaccardScore / density;
2143
- return Math.max(epsilon, score);
2144
- }
2145
- /**
2146
- * Async variant of SCALE MI for use with async graph data sources.
2147
- *
2148
- * Fetches both neighbourhoods, node count, and edge count concurrently.
2149
- */
2150
- async function scaleAsync(graph, source, target, config) {
2151
- const { epsilon = 1e-10 } = config ?? {};
2152
- const [sourceArr, targetArr, n, m] = await Promise.all([
2153
- require_async.collectAsyncIterable(graph.neighbours(source)),
2154
- require_async.collectAsyncIterable(graph.neighbours(target)),
2155
- graph.nodeCount,
2156
- graph.edgeCount
2157
- ]);
2158
- const srcSet = new Set(sourceArr.filter((node) => node !== target));
2159
- const tgtSet = new Set(targetArr.filter((node) => node !== source));
2160
- let intersection = 0;
2161
- for (const node of srcSet) if (tgtSet.has(node)) intersection++;
2162
- const union = srcSet.size + tgtSet.size - intersection;
2163
- const jaccardScore = union > 0 ? intersection / union : 0;
2164
- const possibleEdges = n * (n - 1);
2165
- const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
2166
- if (density === 0) return epsilon;
2167
- const score = jaccardScore / density;
2168
- return Math.max(epsilon, score);
2169
- }
2170
- //#endregion
2171
- //#region src/ranking/mi/skew.ts
2172
- /**
2173
- * Compute SKEW MI between two nodes.
2174
- */
2175
- function skew(graph, source, target, config) {
2176
- const { epsilon = 1e-10 } = config ?? {};
2177
- const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
2178
- const N = graph.nodeCount;
2179
- const sourceDegree = graph.degree(source);
2180
- const targetDegree = graph.degree(target);
2181
- const sourceIdf = Math.log(N / (sourceDegree + 1));
2182
- const targetIdf = Math.log(N / (targetDegree + 1));
2183
- const score = jaccardScore * sourceIdf * targetIdf;
2184
- return Math.max(epsilon, score);
2185
- }
2186
- /**
2187
- * Async variant of SKEW MI for use with async graph data sources.
2188
- *
2189
- * Fetches both neighbourhoods, degrees, and node count concurrently.
2190
- */
2191
- async function skewAsync(graph, source, target, config) {
2192
- const { epsilon = 1e-10 } = config ?? {};
2193
- const [sourceArr, targetArr, N, sourceDegree, targetDegree] = await Promise.all([
2194
- require_async.collectAsyncIterable(graph.neighbours(source)),
2195
- require_async.collectAsyncIterable(graph.neighbours(target)),
2196
- graph.nodeCount,
2197
- graph.degree(source),
2198
- graph.degree(target)
2199
- ]);
2200
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2201
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2202
- let intersection = 0;
2203
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2204
- const union = srcSet.size + tgtSet.size - intersection;
2205
- const jaccardScore = union > 0 ? intersection / union : 0;
2206
- const sourceIdf = Math.log(N / (sourceDegree + 1));
2207
- const targetIdf = Math.log(N / (targetDegree + 1));
2208
- const score = jaccardScore * sourceIdf * targetIdf;
2209
- return Math.max(epsilon, score);
2210
- }
2211
- //#endregion
2212
- //#region src/ranking/mi/span.ts
2213
- /**
2214
- * Compute SPAN MI between two nodes.
2215
- */
2216
- function span(graph, source, target, config) {
2217
- const { epsilon = 1e-10 } = config ?? {};
2218
- const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
2219
- const sourceCc = require_utils.localClusteringCoefficient(graph, source);
2220
- const targetCc = require_utils.localClusteringCoefficient(graph, target);
2221
- const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
2222
- return Math.max(epsilon, score);
2223
- }
2224
- /**
2225
- * Async variant of SPAN MI for use with async graph data sources.
2226
- *
2227
- * Fetches both neighbourhoods concurrently, then computes the clustering
2228
- * coefficient for each endpoint from the collected neighbour arrays.
2229
- */
2230
- async function spanAsync(graph, source, target, config) {
2231
- const { epsilon = 1e-10 } = config ?? {};
2232
- const [sourceArr, targetArr] = await Promise.all([require_async.collectAsyncIterable(graph.neighbours(source)), require_async.collectAsyncIterable(graph.neighbours(target))]);
2233
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2234
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2235
- let intersection = 0;
2236
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2237
- const union = srcSet.size + tgtSet.size - intersection;
2238
- const jaccardScore = union > 0 ? intersection / union : 0;
2239
- const computeClusteringCoefficient = async (nodeId, neighbourArr) => {
2240
- const degree = neighbourArr.length;
2241
- if (degree < 2) return 0;
2242
- const pairs = [];
2243
- for (let i = 0; i < neighbourArr.length; i++) for (let j = i + 1; j < neighbourArr.length; j++) {
2244
- const u = neighbourArr[i];
2245
- const v = neighbourArr[j];
2246
- if (u !== void 0 && v !== void 0) pairs.push([u, v]);
2247
- }
2248
- const edgeResults = await Promise.all(pairs.flatMap(([u, v]) => [graph.getEdge(u, v), graph.getEdge(v, u)]));
2249
- let triangleCount = 0;
2250
- for (let i = 0; i < pairs.length; i++) if (edgeResults[2 * i] !== void 0 || edgeResults[2 * i + 1] !== void 0) triangleCount++;
2251
- const possibleTriangles = degree * (degree - 1) / 2;
2252
- return triangleCount / possibleTriangles;
2253
- };
2254
- const [sourceCc, targetCc] = await Promise.all([computeClusteringCoefficient(source, sourceArr), computeClusteringCoefficient(target, targetArr)]);
2255
- const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
2256
- return Math.max(epsilon, score);
2257
- }
2258
- //#endregion
2259
- //#region src/ranking/mi/etch.ts
2260
- /**
2261
- * Compute ETCH MI between two nodes.
2262
- */
2263
- function etch(graph, source, target, config) {
2264
- const { epsilon = 1e-10 } = config ?? {};
2265
- const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
2266
- const edge = graph.getEdge(source, target);
2267
- if (edge?.type === void 0) return Math.max(epsilon, jaccardScore);
2268
- const edgeTypeCount = require_utils.countEdgesOfType(graph, edge.type);
2269
- if (edgeTypeCount === 0) return Math.max(epsilon, jaccardScore);
2270
- const score = jaccardScore * Math.log(graph.edgeCount / edgeTypeCount);
2271
- return Math.max(epsilon, score);
2272
- }
2273
- /**
2274
- * Async variant of ETCH MI for use with async graph data sources.
2275
- *
2276
- * Fetches both neighbourhoods and edge data concurrently, then counts
2277
- * edges of the same type by iterating the async edge stream.
2278
- */
2279
- async function etchAsync(graph, source, target, config) {
2280
- const { epsilon = 1e-10 } = config ?? {};
2281
- const [sourceArr, targetArr, edge] = await Promise.all([
2282
- require_async.collectAsyncIterable(graph.neighbours(source)),
2283
- require_async.collectAsyncIterable(graph.neighbours(target)),
2284
- graph.getEdge(source, target)
2285
- ]);
2286
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2287
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2288
- let intersection = 0;
2289
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2290
- const union = srcSet.size + tgtSet.size - intersection;
2291
- const jaccardScore = union > 0 ? intersection / union : 0;
2292
- if (edge?.type === void 0) return Math.max(epsilon, jaccardScore);
2293
- const edgeType = edge.type;
2294
- let edgeTypeCount = 0;
2295
- let totalEdges = 0;
2296
- for await (const e of graph.edges()) {
2297
- totalEdges++;
2298
- if (e.type === edgeType) edgeTypeCount++;
2299
- }
2300
- if (edgeTypeCount === 0) return Math.max(epsilon, jaccardScore);
2301
- const score = jaccardScore * Math.log(totalEdges / edgeTypeCount);
2302
- return Math.max(epsilon, score);
2303
- }
2304
- //#endregion
2305
- //#region src/ranking/mi/notch.ts
2306
- /**
2307
- * Compute NOTCH MI between two nodes.
2308
- */
2309
- function notch(graph, source, target, config) {
2310
- const { epsilon = 1e-10 } = config ?? {};
2311
- const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
2312
- const sourceNode = graph.getNode(source);
2313
- const targetNode = graph.getNode(target);
2314
- if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccardScore);
2315
- const sourceTypeCount = require_utils.countNodesOfType(graph, sourceNode.type);
2316
- const targetTypeCount = require_utils.countNodesOfType(graph, targetNode.type);
2317
- if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccardScore);
2318
- const sourceRarity = Math.log(graph.nodeCount / sourceTypeCount);
2319
- const targetRarity = Math.log(graph.nodeCount / targetTypeCount);
2320
- const score = jaccardScore * sourceRarity * targetRarity;
2321
- return Math.max(epsilon, score);
2322
- }
2323
- /**
2324
- * Async variant of NOTCH MI for use with async graph data sources.
2325
- *
2326
- * Fetches both neighbourhoods and node data concurrently, then counts
2327
- * nodes of each type by iterating the async node stream.
2328
- */
2329
- async function notchAsync(graph, source, target, config) {
2330
- const { epsilon = 1e-10 } = config ?? {};
2331
- const [sourceArr, targetArr, sourceNode, targetNode] = await Promise.all([
2332
- require_async.collectAsyncIterable(graph.neighbours(source)),
2333
- require_async.collectAsyncIterable(graph.neighbours(target)),
2334
- graph.getNode(source),
2335
- graph.getNode(target)
2336
- ]);
2337
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2338
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2339
- let intersection = 0;
2340
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2341
- const union = srcSet.size + tgtSet.size - intersection;
2342
- const jaccardScore = union > 0 ? intersection / union : 0;
2343
- if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccardScore);
2344
- const sourceType = sourceNode.type;
2345
- const targetType = targetNode.type;
2346
- let totalNodes = 0;
2347
- let sourceTypeCount = 0;
2348
- let targetTypeCount = 0;
2349
- for await (const nodeId of graph.nodeIds()) {
2350
- totalNodes++;
2351
- const node = await graph.getNode(nodeId);
2352
- if (node?.type === sourceType) sourceTypeCount++;
2353
- if (node?.type === targetType) targetTypeCount++;
2354
- }
2355
- if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccardScore);
2356
- const sourceRarity = Math.log(totalNodes / sourceTypeCount);
2357
- const targetRarity = Math.log(totalNodes / targetTypeCount);
2358
- const score = jaccardScore * sourceRarity * targetRarity;
2359
- return Math.max(epsilon, score);
2360
- }
2361
- //#endregion
2362
- //#region src/ranking/mi/adaptive.ts
2363
- /**
2364
- * Compute unified adaptive MI between two connected nodes.
2365
- *
2366
- * Combines structural, degree, and overlap signals with
2367
- * configurable weighting.
2368
- *
2369
- * @param graph - Source graph
2370
- * @param source - Source node ID
2371
- * @param target - Target node ID
2372
- * @param config - Optional configuration with component weights
2373
- * @returns Adaptive MI score in [0, 1]
2374
- */
2375
- function adaptive(graph, source, target, config) {
2376
- const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
2377
- const { jaccard: jaccardScore, sourceNeighbours, targetNeighbours } = require_utils.computeJaccard(graph, source, target);
2378
- const structural = sourceNeighbours.size === 0 && targetNeighbours.size === 0 ? 0 : Math.max(epsilon, jaccardScore);
2379
- const degreeComponent = adamicAdar(graph, source, target, {
2380
- epsilon,
2381
- normalise: true
2382
- });
2383
- let overlap;
2384
- if (sourceNeighbours.size > 0 && targetNeighbours.size > 0) {
2385
- const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
2386
- const minDegree = Math.min(sourceNeighbours.size, targetNeighbours.size);
2387
- overlap = minDegree > 0 ? intersection / minDegree : epsilon;
2388
- } else overlap = epsilon;
2389
- const totalWeight = structuralWeight + degreeWeight + overlapWeight;
2390
- const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
2391
- return Math.max(epsilon, Math.min(1, score));
2392
- }
2393
- /**
2394
- * Async variant of Adaptive MI for use with async graph data sources.
2395
- *
2396
- * Fetches both neighbourhoods concurrently, then delegates degree-weighted
2397
- * component to the async Adamic-Adar variant.
2398
- */
2399
- async function adaptiveAsync(graph, source, target, config) {
2400
- const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
2401
- const [sourceArr, targetArr, degreeComponent] = await Promise.all([
2402
- require_async.collectAsyncIterable(graph.neighbours(source)),
2403
- require_async.collectAsyncIterable(graph.neighbours(target)),
2404
- adamicAdarAsync(graph, source, target, {
2405
- epsilon,
2406
- normalise: true
2407
- })
2408
- ]);
2409
- const srcSet = new Set(sourceArr.filter((n) => n !== target));
2410
- const tgtSet = new Set(targetArr.filter((n) => n !== source));
2411
- let intersection = 0;
2412
- for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2413
- const union = srcSet.size + tgtSet.size - intersection;
2414
- const jaccardScore = union > 0 ? intersection / union : 0;
2415
- const structural = srcSet.size === 0 && tgtSet.size === 0 ? 0 : Math.max(epsilon, jaccardScore);
2416
- let overlap;
2417
- if (srcSet.size > 0 && tgtSet.size > 0) {
2418
- const minDegree = Math.min(srcSet.size, tgtSet.size);
2419
- overlap = minDegree > 0 ? intersection / minDegree : epsilon;
2420
- } else overlap = epsilon;
2421
- const totalWeight = structuralWeight + degreeWeight + overlapWeight;
2422
- const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
2423
- return Math.max(epsilon, Math.min(1, score));
2424
- }
2425
- //#endregion
2426
- //#region src/ranking/baselines/utils.ts
2427
- /**
2428
- * Normalise a set of scored paths and return them sorted highest-first.
2429
- *
2430
- * All scores are normalised relative to the maximum observed score.
2431
- * When `includeScores` is false, raw (un-normalised) scores are preserved.
2432
- * Handles degenerate cases: empty input and all-zero scores.
2433
- *
2434
- * @param paths - Original paths in input order
2435
- * @param scored - Paths paired with their computed scores
2436
- * @param method - Method name to embed in the result
2437
- * @param includeScores - When true, normalise scores to [0, 1]; when false, keep raw scores
2438
- * @returns BaselineResult with ranked paths
2439
- */
2440
- function normaliseAndRank(paths, scored, method, includeScores) {
2441
- if (scored.length === 0) return {
2442
- paths: [],
2443
- method
2444
- };
2445
- const maxScore = Math.max(...scored.map((s) => s.score));
2446
- if (maxScore === 0) return {
2447
- paths: paths.map((path) => ({
2448
- ...path,
2449
- score: 0
2450
- })),
2451
- method
2452
- };
2453
- return {
2454
- paths: scored.map(({ path, score }) => ({
2455
- ...path,
2456
- score: includeScores ? score / maxScore : score
2457
- })).sort((a, b) => b.score - a.score),
2458
- method
2459
- };
2460
- }
2461
- //#endregion
2462
- //#region src/ranking/baselines/shortest.ts
2463
- /**
2464
- * Rank paths by length (shortest first).
2465
- *
2466
- * Score = 1 / path_length, normalised to [0, 1].
2467
- *
2468
- * @param _graph - Source graph (unused for length ranking)
2469
- * @param paths - Paths to rank
2470
- * @param config - Configuration options
2471
- * @returns Ranked paths (shortest first)
2472
- */
2473
- function shortest(_graph, paths, config) {
2474
- const { includeScores = true } = config ?? {};
2475
- if (paths.length === 0) return {
2476
- paths: [],
2477
- method: "shortest"
2478
- };
2479
- return normaliseAndRank(paths, paths.map((path) => ({
2480
- path,
2481
- score: 1 / path.nodes.length
2482
- })), "shortest", includeScores);
2483
- }
2484
- //#endregion
2485
- //#region src/ranking/baselines/degree-sum.ts
2486
- /**
2487
- * Rank paths by sum of node degrees.
2488
- *
2489
- * @param graph - Source graph
2490
- * @param paths - Paths to rank
2491
- * @param config - Configuration options
2492
- * @returns Ranked paths (highest degree-sum first)
2493
- */
2494
- function degreeSum(graph, paths, config) {
2495
- const { includeScores = true } = config ?? {};
2496
- if (paths.length === 0) return {
2497
- paths: [],
2498
- method: "degree-sum"
2499
- };
2500
- return normaliseAndRank(paths, paths.map((path) => {
2501
- let degreeSum = 0;
2502
- for (const nodeId of path.nodes) degreeSum += graph.degree(nodeId);
2503
- return {
2504
- path,
2505
- score: degreeSum
2506
- };
2507
- }), "degree-sum", includeScores);
2508
- }
2509
- //#endregion
2510
- //#region src/ranking/baselines/widest-path.ts
2511
- /**
2512
- * Rank paths by widest bottleneck (minimum edge similarity).
2513
- *
2514
- * @param graph - Source graph
2515
- * @param paths - Paths to rank
2516
- * @param config - Configuration options
2517
- * @returns Ranked paths (highest bottleneck first)
2518
- */
2519
- function widestPath(graph, paths, config) {
2520
- const { includeScores = true } = config ?? {};
2521
- if (paths.length === 0) return {
2522
- paths: [],
2523
- method: "widest-path"
2524
- };
2525
- return normaliseAndRank(paths, paths.map((path) => {
2526
- if (path.nodes.length < 2) return {
2527
- path,
2528
- score: 1
2529
- };
2530
- let minSimilarity = Number.POSITIVE_INFINITY;
2531
- for (let i = 0; i < path.nodes.length - 1; i++) {
2532
- const source = path.nodes[i];
2533
- const target = path.nodes[i + 1];
2534
- if (source === void 0 || target === void 0) continue;
2535
- const edgeSimilarity = jaccard(graph, source, target);
2536
- minSimilarity = Math.min(minSimilarity, edgeSimilarity);
2537
- }
2538
- return {
2539
- path,
2540
- score: minSimilarity === Number.POSITIVE_INFINITY ? 1 : minSimilarity
2541
- };
2542
- }), "widest-path", includeScores);
2543
- }
2544
- //#endregion
2545
- //#region src/ranking/baselines/jaccard-arithmetic.ts
2546
- /**
2547
- * Rank paths by arithmetic mean of edge Jaccard similarities.
2548
- *
2549
- * @param graph - Source graph
2550
- * @param paths - Paths to rank
2551
- * @param config - Configuration options
2552
- * @returns Ranked paths (highest arithmetic mean first)
2553
- */
2554
- function jaccardArithmetic(graph, paths, config) {
2555
- const { includeScores = true } = config ?? {};
2556
- if (paths.length === 0) return {
2557
- paths: [],
2558
- method: "jaccard-arithmetic"
2559
- };
2560
- return normaliseAndRank(paths, paths.map((path) => {
2561
- if (path.nodes.length < 2) return {
2562
- path,
2563
- score: 1
2564
- };
2565
- let similaritySum = 0;
2566
- let edgeCount = 0;
2567
- for (let i = 0; i < path.nodes.length - 1; i++) {
2568
- const source = path.nodes[i];
2569
- const target = path.nodes[i + 1];
2570
- if (source === void 0 || target === void 0) continue;
2571
- const edgeSimilarity = jaccard(graph, source, target);
2572
- similaritySum += edgeSimilarity;
2573
- edgeCount++;
2574
- }
2575
- return {
2576
- path,
2577
- score: edgeCount > 0 ? similaritySum / edgeCount : 1
2578
- };
2579
- }), "jaccard-arithmetic", includeScores);
2580
- }
2581
- //#endregion
2582
- //#region src/ranking/baselines/pagerank.ts
2583
- /**
2584
- * Compute PageRank centrality for all nodes using power iteration.
2585
- *
2586
- * @param graph - Source graph
2587
- * @param damping - Damping factor (default 0.85)
2588
- * @param tolerance - Convergence tolerance (default 1e-6)
2589
- * @param maxIterations - Maximum iterations (default 100)
2590
- * @returns Map of node ID to PageRank value
2591
- */
2592
- function computePageRank(graph, damping = .85, tolerance = 1e-6, maxIterations = 100) {
2593
- const nodes = Array.from(graph.nodeIds());
2594
- const n = nodes.length;
2595
- if (n === 0) return /* @__PURE__ */ new Map();
2596
- const ranks = /* @__PURE__ */ new Map();
2597
- const newRanks = /* @__PURE__ */ new Map();
2598
- for (const nodeId of nodes) {
2599
- ranks.set(nodeId, 1 / n);
2600
- newRanks.set(nodeId, 0);
2601
- }
2602
- let isCurrentRanks = true;
2603
- for (let iteration = 0; iteration < maxIterations; iteration++) {
2604
- let maxChange = 0;
2605
- const currMap = isCurrentRanks ? ranks : newRanks;
2606
- const nextMap = isCurrentRanks ? newRanks : ranks;
2607
- for (const nodeId of nodes) {
2608
- let incomingSum = 0;
2609
- for (const incomingId of graph.neighbours(nodeId, "in")) {
2610
- const incomingRank = currMap.get(incomingId) ?? 0;
2611
- const outDegree = graph.degree(incomingId);
2612
- if (outDegree > 0) incomingSum += incomingRank / outDegree;
2613
- }
2614
- const newRank = (1 - damping) / n + damping * incomingSum;
2615
- nextMap.set(nodeId, newRank);
2616
- const oldRank = currMap.get(nodeId) ?? 0;
2617
- maxChange = Math.max(maxChange, Math.abs(newRank - oldRank));
2618
- }
2619
- if (maxChange < tolerance) break;
2620
- isCurrentRanks = !isCurrentRanks;
2621
- currMap.clear();
2622
- }
2623
- return isCurrentRanks ? ranks : newRanks;
2624
- }
2625
- /**
2626
- * Rank paths by sum of PageRank scores.
2627
- *
2628
- * @param graph - Source graph
2629
- * @param paths - Paths to rank
2630
- * @param config - Configuration options
2631
- * @returns Ranked paths (highest PageRank sum first)
2632
- */
2633
- function pagerank(graph, paths, config) {
2634
- const { includeScores = true } = config ?? {};
2635
- if (paths.length === 0) return {
2636
- paths: [],
2637
- method: "pagerank"
2638
- };
2639
- const ranks = computePageRank(graph);
2640
- return normaliseAndRank(paths, paths.map((path) => {
2641
- let prSum = 0;
2642
- for (const nodeId of path.nodes) prSum += ranks.get(nodeId) ?? 0;
2643
- return {
2644
- path,
2645
- score: prSum
2646
- };
2647
- }), "pagerank", includeScores);
2648
- }
2649
- //#endregion
2650
- //#region src/ranking/baselines/betweenness.ts
2651
- /**
2652
- * Compute betweenness centrality for all nodes using Brandes algorithm.
2653
- *
2654
- * @param graph - Source graph
2655
- * @returns Map of node ID to betweenness value
2656
- */
2657
- function computeBetweenness(graph) {
2658
- const nodes = Array.from(graph.nodeIds());
2659
- const betweenness = /* @__PURE__ */ new Map();
2660
- for (const nodeId of nodes) betweenness.set(nodeId, 0);
2661
- for (const source of nodes) {
2662
- const predecessors = /* @__PURE__ */ new Map();
2663
- const distance = /* @__PURE__ */ new Map();
2664
- const sigma = /* @__PURE__ */ new Map();
2665
- const queue = [];
2666
- for (const nodeId of nodes) {
2667
- predecessors.set(nodeId, []);
2668
- distance.set(nodeId, -1);
2669
- sigma.set(nodeId, 0);
2670
- }
2671
- distance.set(source, 0);
2672
- sigma.set(source, 1);
2673
- queue.push(source);
2674
- for (const v of queue) {
2675
- const vDist = distance.get(v) ?? -1;
2676
- const neighbours = graph.neighbours(v);
2677
- for (const w of neighbours) {
2678
- const wDist = distance.get(w) ?? -1;
2679
- if (wDist < 0) {
2680
- distance.set(w, vDist + 1);
2681
- queue.push(w);
2682
- }
2683
- if (wDist === vDist + 1) {
2684
- const wSigma = sigma.get(w) ?? 0;
2685
- const vSigma = sigma.get(v) ?? 0;
2686
- sigma.set(w, wSigma + vSigma);
2687
- const wPred = predecessors.get(w) ?? [];
2688
- wPred.push(v);
2689
- predecessors.set(w, wPred);
2690
- }
2691
- }
2692
- }
2693
- const delta = /* @__PURE__ */ new Map();
2694
- for (const nodeId of nodes) delta.set(nodeId, 0);
2695
- const sorted = [...nodes].sort((a, b) => {
2696
- const aD = distance.get(a) ?? -1;
2697
- return (distance.get(b) ?? -1) - aD;
2698
- });
2699
- for (const w of sorted) {
2700
- if (w === source) continue;
2701
- const wDelta = delta.get(w) ?? 0;
2702
- const wSigma = sigma.get(w) ?? 0;
2703
- const wPred = predecessors.get(w) ?? [];
2704
- for (const v of wPred) {
2705
- const vSigma = sigma.get(v) ?? 0;
2706
- const vDelta = delta.get(v) ?? 0;
2707
- if (wSigma > 0) delta.set(v, vDelta + vSigma / wSigma * (1 + wDelta));
2708
- }
2709
- if (w !== source) {
2710
- const current = betweenness.get(w) ?? 0;
2711
- betweenness.set(w, current + wDelta);
2712
- }
2713
- }
2714
- }
2715
- return betweenness;
2716
- }
2717
- /**
2718
- * Rank paths by sum of betweenness scores.
2719
- *
2720
- * @param graph - Source graph
2721
- * @param paths - Paths to rank
2722
- * @param config - Configuration options
2723
- * @returns Ranked paths (highest betweenness sum first)
2724
- */
2725
- function betweenness(graph, paths, config) {
2726
- const { includeScores = true } = config ?? {};
2727
- if (paths.length === 0) return {
2728
- paths: [],
2729
- method: "betweenness"
2730
- };
2731
- const bcMap = computeBetweenness(graph);
2732
- return normaliseAndRank(paths, paths.map((path) => {
2733
- let bcSum = 0;
2734
- for (const nodeId of path.nodes) bcSum += bcMap.get(nodeId) ?? 0;
2735
- return {
2736
- path,
2737
- score: bcSum
2738
- };
2739
- }), "betweenness", includeScores);
2740
- }
2741
- //#endregion
2742
- //#region src/ranking/baselines/katz.ts
2743
- /**
2744
- * Compute truncated Katz centrality between two nodes.
2745
- *
2746
- * Uses iterative matrix-vector products to avoid full matrix powers.
2747
- * score(s,t) = sum_{k=1}^{K} beta^k * walks_k(s,t)
2748
- *
2749
- * @param graph - Source graph
2750
- * @param source - Source node ID
2751
- * @param target - Target node ID
2752
- * @param k - Truncation depth (default 5)
2753
- * @param beta - Attenuation factor (default 0.005)
2754
- * @returns Katz score
2755
- */
2756
- function computeKatz(graph, source, target, k = 5, beta = .005) {
2757
- const nodes = Array.from(graph.nodeIds());
2758
- const nodeToIdx = /* @__PURE__ */ new Map();
2759
- nodes.forEach((nodeId, idx) => {
2760
- nodeToIdx.set(nodeId, idx);
2761
- });
2762
- const n = nodes.length;
2763
- if (n === 0) return 0;
2764
- const sourceIdx = nodeToIdx.get(source);
2765
- const targetIdx = nodeToIdx.get(target);
2766
- if (sourceIdx === void 0 || targetIdx === void 0) return 0;
2767
- let walks = new Float64Array(n);
2768
- walks[targetIdx] = 1;
2769
- let katzScore = 0;
2770
- for (let depth = 1; depth <= k; depth++) {
2771
- const walksNext = new Float64Array(n);
2772
- for (const sourceNode of nodes) {
2773
- const srcIdx = nodeToIdx.get(sourceNode);
2774
- if (srcIdx === void 0) continue;
2775
- const neighbours = graph.neighbours(sourceNode);
2776
- for (const neighbourId of neighbours) {
2777
- const nIdx = nodeToIdx.get(neighbourId);
2778
- if (nIdx === void 0) continue;
2779
- walksNext[srcIdx] = (walksNext[srcIdx] ?? 0) + (walks[nIdx] ?? 0);
2780
- }
2781
- }
2782
- const walkCount = walksNext[sourceIdx] ?? 0;
2783
- katzScore += Math.pow(beta, depth) * walkCount;
2784
- walks = walksNext;
2785
- }
2786
- return katzScore;
2787
- }
2788
- /**
2789
- * Rank paths by Katz centrality between endpoints.
2790
- *
2791
- * @param graph - Source graph
2792
- * @param paths - Paths to rank
2793
- * @param config - Configuration options
2794
- * @returns Ranked paths (highest Katz score first)
2795
- */
2796
- function katz(graph, paths, config) {
2797
- const { includeScores = true } = config ?? {};
2798
- if (paths.length === 0) return {
2799
- paths: [],
2800
- method: "katz"
2801
- };
2802
- return normaliseAndRank(paths, paths.map((path) => {
2803
- const source = path.nodes[0];
2804
- const target = path.nodes[path.nodes.length - 1];
2805
- if (source === void 0 || target === void 0) return {
2806
- path,
2807
- score: 0
2808
- };
2809
- return {
2810
- path,
2811
- score: computeKatz(graph, source, target)
2812
- };
2813
- }), "katz", includeScores);
2814
- }
2815
- //#endregion
2816
- //#region src/ranking/baselines/communicability.ts
2817
- /**
2818
- * Compute truncated communicability between two nodes.
2819
- *
2820
- * Uses Taylor series expansion: (e^A)_{s,t} ≈ sum_{k=0}^{K} A^k_{s,t} / k!
2821
- *
2822
- * @param graph - Source graph
2823
- * @param source - Source node ID
2824
- * @param target - Target node ID
2825
- * @param k - Truncation depth (default 15)
2826
- * @returns Communicability score
2827
- */
2828
- function computeCommunicability(graph, source, target, k = 15) {
2829
- const nodes = Array.from(graph.nodeIds());
2830
- const nodeToIdx = /* @__PURE__ */ new Map();
2831
- nodes.forEach((nodeId, idx) => {
2832
- nodeToIdx.set(nodeId, idx);
2833
- });
2834
- const n = nodes.length;
2835
- if (n === 0) return 0;
2836
- const sourceIdx = nodeToIdx.get(source);
2837
- const targetIdx = nodeToIdx.get(target);
2838
- if (sourceIdx === void 0 || targetIdx === void 0) return 0;
2839
- let walks = new Float64Array(n);
2840
- walks[targetIdx] = 1;
2841
- let commScore = walks[sourceIdx] ?? 0;
2842
- let factorial = 1;
2843
- for (let depth = 1; depth <= k; depth++) {
2844
- const walksNext = new Float64Array(n);
2845
- for (const fromNode of nodes) {
2846
- const fromIdx = nodeToIdx.get(fromNode);
2847
- if (fromIdx === void 0) continue;
2848
- const neighbours = graph.neighbours(fromNode);
2849
- for (const toNodeId of neighbours) {
2850
- const toIdx = nodeToIdx.get(toNodeId);
2851
- if (toIdx === void 0) continue;
2852
- walksNext[fromIdx] = (walksNext[fromIdx] ?? 0) + (walks[toIdx] ?? 0);
2853
- }
2854
- }
2855
- factorial *= depth;
2856
- commScore += (walksNext[sourceIdx] ?? 0) / factorial;
2857
- walks = walksNext;
2858
- }
2859
- return commScore;
2860
- }
2861
- /**
2862
- * Rank paths by communicability between endpoints.
2863
- *
2864
- * @param graph - Source graph
2865
- * @param paths - Paths to rank
2866
- * @param config - Configuration options
2867
- * @returns Ranked paths (highest communicability first)
2868
- */
2869
- function communicability(graph, paths, config) {
2870
- const { includeScores = true } = config ?? {};
2871
- if (paths.length === 0) return {
2872
- paths: [],
2873
- method: "communicability"
2874
- };
2875
- return normaliseAndRank(paths, paths.map((path) => {
2876
- const source = path.nodes[0];
2877
- const target = path.nodes[path.nodes.length - 1];
2878
- if (source === void 0 || target === void 0) return {
2879
- path,
2880
- score: 0
2881
- };
2882
- return {
2883
- path,
2884
- score: computeCommunicability(graph, source, target)
2885
- };
2886
- }), "communicability", includeScores);
2887
- }
2888
- //#endregion
2889
- //#region src/ranking/baselines/resistance-distance.ts
2890
- /**
2891
- * Compute effective resistance between two nodes via Laplacian pseudoinverse.
2892
- *
2893
- * Resistance = L^+_{s,s} + L^+_{t,t} - 2*L^+_{s,t}
2894
- * where L^+ is the pseudoinverse of the Laplacian matrix.
2895
- *
2896
- * @param graph - Source graph
2897
- * @param source - Source node ID
2898
- * @param target - Target node ID
2899
- * @returns Effective resistance
2900
- */
2901
- function computeResistance(graph, source, target) {
2902
- const nodes = Array.from(graph.nodeIds());
2903
- const nodeToIdx = /* @__PURE__ */ new Map();
2904
- nodes.forEach((nodeId, idx) => {
2905
- nodeToIdx.set(nodeId, idx);
2906
- });
2907
- const n = nodes.length;
2908
- if (n === 0 || n > 5e3) throw new Error(`Cannot compute resistance distance: graph too large (${String(n)} nodes). Maximum 5000.`);
2909
- const sourceIdx = nodeToIdx.get(source);
2910
- const targetIdx = nodeToIdx.get(target);
2911
- if (sourceIdx === void 0 || targetIdx === void 0) return 0;
2912
- const L = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
2913
- for (let i = 0; i < n; i++) {
2914
- const nodeId = nodes[i];
2915
- if (nodeId === void 0) continue;
2916
- const degree = graph.degree(nodeId);
2917
- const row = L[i];
2918
- if (row !== void 0) row[i] = degree;
2919
- const neighbours = graph.neighbours(nodeId);
2920
- for (const neighbourId of neighbours) {
2921
- const j = nodeToIdx.get(neighbourId);
2922
- if (j !== void 0 && row !== void 0) row[j] = -1;
2923
- }
2924
- }
2925
- const Lpinv = pinv(L);
2926
- const resistance = (Lpinv[sourceIdx]?.[sourceIdx] ?? 0) + (Lpinv[targetIdx]?.[targetIdx] ?? 0) - 2 * (Lpinv[sourceIdx]?.[targetIdx] ?? 0);
2927
- return Math.max(resistance, 1e-10);
2928
- }
2929
- /**
2930
- * Compute Moore-Penrose pseudoinverse of a matrix.
2931
- * Simplified implementation for small dense matrices.
2932
- *
2933
- * @param A - Square matrix
2934
- * @returns Pseudoinverse A^+
2935
- */
2936
- function pinv(A) {
2937
- const n = A.length;
2938
- if (n === 0) return [];
2939
- const M = A.map((row) => [...row]);
2940
- const epsilon = 1e-10;
2941
- for (let i = 0; i < n; i++) {
2942
- const row = M[i];
2943
- if (row !== void 0) row[i] = (row[i] ?? 0) + epsilon;
2944
- }
2945
- return gaussianInverse(M);
2946
- }
2947
- /**
2948
- * Compute matrix inverse using Gaussian elimination with partial pivoting.
2949
- *
2950
- * @param A - Matrix to invert
2951
- * @returns Inverted matrix
2952
- */
2953
- function gaussianInverse(A) {
2954
- const n = A.length;
2955
- const aug = A.map((row, i) => {
2956
- const identity = Array.from({ length: n }, (_, j) => i === j ? 1 : 0);
2957
- return [...row, ...identity];
2958
- });
2959
- for (let col = 0; col < n; col++) {
2960
- let maxRow = col;
2961
- for (let row = col + 1; row < n; row++) {
2962
- const currentRow = aug[row];
2963
- const maxRowRef = aug[maxRow];
2964
- if (currentRow !== void 0 && maxRowRef !== void 0 && Math.abs(currentRow[col] ?? 0) > Math.abs(maxRowRef[col] ?? 0)) maxRow = row;
2965
- }
2966
- const currentCol = aug[col];
2967
- const maxRowAug = aug[maxRow];
2968
- if (currentCol !== void 0 && maxRowAug !== void 0) {
2969
- aug[col] = maxRowAug;
2970
- aug[maxRow] = currentCol;
2971
- }
2972
- const pivotRow = aug[col];
2973
- const pivot = pivotRow?.[col];
2974
- if (pivot === void 0 || Math.abs(pivot) < 1e-12) continue;
2975
- if (pivotRow !== void 0) for (let j = col; j < 2 * n; j++) pivotRow[j] = (pivotRow[j] ?? 0) / pivot;
2976
- for (let row = 0; row < n; row++) {
2977
- if (row === col) continue;
2978
- const eliminationRow = aug[row];
2979
- const factor = eliminationRow?.[col] ?? 0;
2980
- if (eliminationRow !== void 0 && pivotRow !== void 0) for (let j = col; j < 2 * n; j++) eliminationRow[j] = (eliminationRow[j] ?? 0) - factor * (pivotRow[j] ?? 0);
2981
- }
2982
- }
2983
- const Ainv = [];
2984
- for (let i = 0; i < n; i++) Ainv[i] = (aug[i]?.slice(n) ?? []).map((v) => v);
2985
- return Ainv;
2986
- }
2987
- /**
2988
- * Rank paths by reciprocal of resistance distance between endpoints.
2989
- *
2990
- * @param graph - Source graph
2991
- * @param paths - Paths to rank
2992
- * @param config - Configuration options
2993
- * @returns Ranked paths (highest conductance first)
2994
- */
2995
- function resistanceDistance(graph, paths, config) {
2996
- const { includeScores = true } = config ?? {};
2997
- if (paths.length === 0) return {
2998
- paths: [],
2999
- method: "resistance-distance"
3000
- };
3001
- const nodeCount = Array.from(graph.nodeIds()).length;
3002
- 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.`);
3003
- return normaliseAndRank(paths, paths.map((path) => {
3004
- const source = path.nodes[0];
3005
- const target = path.nodes[path.nodes.length - 1];
3006
- if (source === void 0 || target === void 0) return {
3007
- path,
3008
- score: 0
3009
- };
3010
- return {
3011
- path,
3012
- score: 1 / computeResistance(graph, source, target)
3013
- };
3014
- }), "resistance-distance", includeScores);
3015
- }
3016
- //#endregion
3017
- //#region src/ranking/baselines/random-ranking.ts
3018
- /**
3019
- * Deterministic seeded random number generator.
3020
- * Uses FNV-1a-like hash for input → [0, 1] output.
3021
- *
3022
- * @param input - String to hash
3023
- * @param seed - Random seed for reproducibility
3024
- * @returns Deterministic random value in [0, 1]
3025
- */
3026
- function seededRandom(input, seed = 0) {
3027
- let h = seed;
3028
- for (let i = 0; i < input.length; i++) {
3029
- h = Math.imul(h ^ input.charCodeAt(i), 2654435769);
3030
- h ^= h >>> 16;
3031
- }
3032
- return (h >>> 0) / 4294967295;
3033
- }
3034
- /**
3035
- * Rank paths randomly (null hypothesis baseline).
3036
- *
3037
- * @param _graph - Source graph (unused)
3038
- * @param paths - Paths to rank
3039
- * @param config - Configuration options
3040
- * @returns Ranked paths (randomly ordered)
3041
- */
3042
- function randomRanking(_graph, paths, config) {
3043
- const { includeScores = true, seed = 0 } = config ?? {};
3044
- if (paths.length === 0) return {
3045
- paths: [],
3046
- method: "random"
3047
- };
3048
- return normaliseAndRank(paths, paths.map((path) => {
3049
- return {
3050
- path,
3051
- score: seededRandom(path.nodes.join(","), seed)
3052
- };
3053
- }), "random", includeScores);
3054
- }
3055
- //#endregion
3056
- //#region src/ranking/baselines/hitting-time.ts
3057
- /**
3058
- * Seeded deterministic random number generator (LCG).
3059
- * Suitable for reproducible random walk simulation.
3060
- */
3061
- var SeededRNG = class {
3062
- state;
3063
- constructor(seed) {
3064
- this.state = seed;
3065
- }
3066
- /**
3067
- * Generate next pseudorandom value in [0, 1).
3068
- */
3069
- next() {
3070
- this.state = this.state * 1103515245 + 12345 & 2147483647;
3071
- return this.state / 2147483647;
3072
- }
3073
- };
3074
- /**
3075
- * Compute hitting time via Monte Carlo random walk simulation.
3076
- *
3077
- * @param graph - Source graph
3078
- * @param source - Source node ID
3079
- * @param target - Target node ID
3080
- * @param walks - Number of walks to simulate
3081
- * @param maxSteps - Maximum steps per walk
3082
- * @param rng - Seeded RNG instance
3083
- * @returns Average hitting time across walks
3084
- */
3085
- function computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng) {
3086
- if (source === target) return 0;
3087
- let totalSteps = 0;
3088
- let successfulWalks = 0;
3089
- for (let w = 0; w < walks; w++) {
3090
- let current = source;
3091
- let steps = 0;
3092
- while (current !== target && steps < maxSteps) {
3093
- const neighbours = Array.from(graph.neighbours(current));
3094
- if (neighbours.length === 0) break;
3095
- const nextNode = neighbours[Math.floor(rng.next() * neighbours.length)];
3096
- if (nextNode === void 0) break;
3097
- current = nextNode;
3098
- steps++;
3099
- }
3100
- if (current === target) {
3101
- totalSteps += steps;
3102
- successfulWalks++;
3103
- }
3104
- }
3105
- if (successfulWalks > 0) return totalSteps / successfulWalks;
3106
- return maxSteps;
3107
- }
3108
- /**
3109
- * Compute hitting time via exact fundamental matrix method.
3110
- *
3111
- * For small graphs, computes exact expected hitting times using
3112
- * the fundamental matrix of the random walk.
3113
- *
3114
- * @param graph - Source graph
3115
- * @param source - Source node ID
3116
- * @param target - Target node ID
3117
- * @returns Exact hitting time (or approximation if convergence fails)
3118
- */
3119
- function computeHittingTimeExact(graph, source, target) {
3120
- if (source === target) return 0;
3121
- const nodes = Array.from(graph.nodeIds());
3122
- const nodeToIdx = /* @__PURE__ */ new Map();
3123
- nodes.forEach((nodeId, idx) => {
3124
- nodeToIdx.set(nodeId, idx);
3125
- });
3126
- const n = nodes.length;
3127
- const sourceIdx = nodeToIdx.get(source);
3128
- const targetIdx = nodeToIdx.get(target);
3129
- if (sourceIdx === void 0 || targetIdx === void 0) return 0;
3130
- const P = [];
3131
- for (let i = 0; i < n; i++) {
3132
- const row = [];
3133
- for (let j = 0; j < n; j++) row[j] = 0;
3134
- P[i] = row;
3135
- }
3136
- for (const nodeId of nodes) {
3137
- const idx = nodeToIdx.get(nodeId);
3138
- if (idx === void 0) continue;
3139
- const pRow = P[idx];
3140
- if (pRow === void 0) continue;
3141
- if (idx === targetIdx) pRow[idx] = 1;
3142
- else {
3143
- const neighbours = Array.from(graph.neighbours(nodeId));
3144
- const degree = neighbours.length;
3145
- if (degree > 0) for (const neighbourId of neighbours) {
3146
- const nIdx = nodeToIdx.get(neighbourId);
3147
- if (nIdx !== void 0) pRow[nIdx] = 1 / degree;
3148
- }
3149
- }
3150
- }
3151
- const transientIndices = [];
3152
- for (let i = 0; i < n; i++) if (i !== targetIdx) transientIndices.push(i);
3153
- const m = transientIndices.length;
3154
- const Q = [];
3155
- for (let i = 0; i < m; i++) {
3156
- const row = [];
3157
- for (let j = 0; j < m; j++) row[j] = 0;
3158
- Q[i] = row;
3159
- }
3160
- for (let i = 0; i < m; i++) {
3161
- const qRow = Q[i];
3162
- if (qRow === void 0) continue;
3163
- const origI = transientIndices[i];
3164
- if (origI === void 0) continue;
3165
- const pRow = P[origI];
3166
- if (pRow === void 0) continue;
3167
- for (let j = 0; j < m; j++) {
3168
- const origJ = transientIndices[j];
3169
- if (origJ === void 0) continue;
3170
- qRow[j] = pRow[origJ] ?? 0;
3171
- }
3172
- }
3173
- const IMQ = [];
3174
- for (let i = 0; i < m; i++) {
3175
- const row = [];
3176
- for (let j = 0; j < m; j++) row[j] = i === j ? 1 : 0;
3177
- IMQ[i] = row;
3178
- }
3179
- for (let i = 0; i < m; i++) {
3180
- const imqRow = IMQ[i];
3181
- if (imqRow === void 0) continue;
3182
- const qRow = Q[i];
3183
- for (let j = 0; j < m; j++) {
3184
- const qVal = qRow?.[j] ?? 0;
3185
- imqRow[j] = (i === j ? 1 : 0) - qVal;
3186
- }
3187
- }
3188
- const N = invertMatrix(IMQ);
3189
- if (N === null) return 1;
3190
- const sourceTransientIdx = transientIndices.indexOf(sourceIdx);
3191
- if (sourceTransientIdx < 0) return 0;
3192
- let hittingTime = 0;
3193
- const row = N[sourceTransientIdx];
3194
- if (row !== void 0) for (const val of row) hittingTime += val;
3195
- return hittingTime;
3196
- }
3197
- /**
3198
- * Invert a square matrix using Gaussian elimination with partial pivoting.
3199
- *
3200
- * @param matrix - Input matrix (n × n)
3201
- * @returns Inverted matrix, or null if singular
3202
- */
3203
- function invertMatrix(matrix) {
3204
- const n = matrix.length;
3205
- const aug = [];
3206
- for (let i = 0; i < n; i++) {
3207
- const row = [];
3208
- const matRow = matrix[i];
3209
- for (let j = 0; j < n; j++) row[j] = matRow?.[j] ?? 0;
3210
- for (let j = 0; j < n; j++) row[n + j] = i === j ? 1 : 0;
3211
- aug[i] = row;
3212
- }
3213
- for (let col = 0; col < n; col++) {
3214
- let pivotRow = col;
3215
- const pivotCol = aug[pivotRow];
3216
- if (pivotCol === void 0) return null;
3217
- for (let row = col + 1; row < n; row++) {
3218
- const currRowVal = aug[row]?.[col] ?? 0;
3219
- const pivotRowVal = pivotCol[col] ?? 0;
3220
- if (Math.abs(currRowVal) > Math.abs(pivotRowVal)) pivotRow = row;
3221
- }
3222
- const augPivot = aug[pivotRow];
3223
- if (augPivot === void 0 || Math.abs(augPivot[col] ?? 0) < 1e-10) return null;
3224
- [aug[col], aug[pivotRow]] = [aug[pivotRow] ?? [], aug[col] ?? []];
3225
- const scaledPivotRow = aug[col];
3226
- if (scaledPivotRow === void 0) return null;
3227
- const pivot = scaledPivotRow[col] ?? 1;
3228
- for (let j = 0; j < 2 * n; j++) scaledPivotRow[j] = (scaledPivotRow[j] ?? 0) / pivot;
3229
- for (let row = col + 1; row < n; row++) {
3230
- const currRow = aug[row];
3231
- if (currRow === void 0) continue;
3232
- const factor = currRow[col] ?? 0;
3233
- for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (scaledPivotRow[j] ?? 0);
3234
- }
3235
- }
3236
- for (let col = n - 1; col > 0; col--) {
3237
- const colRow = aug[col];
3238
- if (colRow === void 0) return null;
3239
- for (let row = col - 1; row >= 0; row--) {
3240
- const currRow = aug[row];
3241
- if (currRow === void 0) continue;
3242
- const factor = currRow[col] ?? 0;
3243
- for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (colRow[j] ?? 0);
3244
- }
3245
- }
3246
- const inv = [];
3247
- for (let i = 0; i < n; i++) {
3248
- const row = [];
3249
- for (let j = 0; j < n; j++) row[j] = 0;
3250
- inv[i] = row;
3251
- }
3252
- for (let i = 0; i < n; i++) {
3253
- const invRow = inv[i];
3254
- if (invRow === void 0) continue;
3255
- const augRow = aug[i];
3256
- if (augRow === void 0) continue;
3257
- for (let j = 0; j < n; j++) invRow[j] = augRow[n + j] ?? 0;
3258
- }
3259
- return inv;
3260
- }
3261
- /**
3262
- * Rank paths by inverse hitting time between endpoints.
3263
- *
3264
- * @param graph - Source graph
3265
- * @param paths - Paths to rank
3266
- * @param config - Configuration options
3267
- * @returns Ranked paths (highest inverse hitting time first)
3268
- */
3269
- function hittingTime(graph, paths, config) {
3270
- const { includeScores = true, mode = "auto", walks = 1e3, maxSteps = 1e4, seed = 42 } = config ?? {};
3271
- if (paths.length === 0) return {
3272
- paths: [],
3273
- method: "hitting-time"
3274
- };
3275
- const nodeCount = Array.from(graph.nodeIds()).length;
3276
- const actualMode = mode === "auto" ? nodeCount < 100 ? "exact" : "approximate" : mode;
3277
- const rng = new SeededRNG(seed);
3278
- const scored = paths.map((path) => {
3279
- const source = path.nodes[0];
3280
- const target = path.nodes[path.nodes.length - 1];
3281
- if (source === void 0 || target === void 0) return {
3282
- path,
3283
- score: 0
3284
- };
3285
- const ht = actualMode === "exact" ? computeHittingTimeExact(graph, source, target) : computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng);
3286
- return {
3287
- path,
3288
- score: ht > 0 ? 1 / ht : 0
3289
- };
3290
- });
3291
- const maxScore = Math.max(...scored.map((s) => s.score));
3292
- if (!Number.isFinite(maxScore)) return {
3293
- paths: paths.map((path) => ({
3294
- ...path,
3295
- score: 0
3296
- })),
3297
- method: "hitting-time"
3298
- };
3299
- return normaliseAndRank(paths, scored, "hitting-time", includeScores);
3300
- }
3301
- //#endregion
3302
- //#region src/extraction/ego-network.ts
3303
- /**
3304
- * Extract the ego-network (k-hop neighbourhood) of a centre node.
3305
- *
3306
- * The ego-network includes all nodes reachable within k hops from the
3307
- * centre node, plus all edges between those nodes (induced subgraph).
3308
- *
3309
- * For directed graphs, the search follows outgoing edges by default.
3310
- * To include incoming edges, use direction 'both' in the underlying traversal.
3311
- *
3312
- * @param graph - The source graph
3313
- * @param centre - The centre node ID
3314
- * @param options - Extraction options
3315
- * @returns An induced subgraph of the k-hop neighbourhood
3316
- * @throws Error if the centre node does not exist in the graph
3317
- *
3318
- * @example
3319
- * ```typescript
3320
- * // 2-hop neighbourhood
3321
- * const ego = extractEgoNetwork(graph, 'A', { hops: 2 });
3322
- * ```
3323
- */
3324
- function extractEgoNetwork(graph, centre, options) {
3325
- const hops = options?.hops ?? 1;
3326
- if (!graph.hasNode(centre)) throw new Error(`Centre node '${centre}' does not exist in the graph`);
3327
- if (hops < 0) throw new Error(`Hops must be non-negative, got ${String(hops)}`);
3328
- const nodesInEgoNetwork = new Set([centre]);
3329
- if (hops > 0) {
3330
- const visited = new Set([centre]);
3331
- const queue = [[centre, 0]];
3332
- while (queue.length > 0) {
3333
- const entry = queue.shift();
3334
- if (entry === void 0) break;
3335
- const [current, distance] = entry;
3336
- if (distance < hops) {
3337
- for (const neighbour of graph.neighbours(current)) if (!visited.has(neighbour)) {
3338
- visited.add(neighbour);
3339
- nodesInEgoNetwork.add(neighbour);
3340
- queue.push([neighbour, distance + 1]);
3341
- }
3342
- }
3343
- }
3344
- }
3345
- const result = graph.directed ? require_graph.AdjacencyMapGraph.directed() : require_graph.AdjacencyMapGraph.undirected();
3346
- for (const nodeId of nodesInEgoNetwork) {
3347
- const nodeData = graph.getNode(nodeId);
3348
- if (nodeData !== void 0) result.addNode(nodeData);
3349
- }
3350
- for (const edge of graph.edges()) if (nodesInEgoNetwork.has(edge.source) && nodesInEgoNetwork.has(edge.target)) result.addEdge(edge);
3351
- return result;
3352
- }
3353
- //#endregion
3354
- //#region src/extraction/k-core.ts
3355
- /**
3356
- * Extract the k-core of a graph.
3357
- *
3358
- * The k-core is the maximal connected subgraph where every node has
3359
- * degree at least k. This is computed using a peeling algorithm that
3360
- * iteratively removes nodes with degree less than k.
3361
- *
3362
- * For undirected graphs, degree counts all adjacent nodes.
3363
- * For directed graphs, degree counts both in- and out-neighbours.
3364
- *
3365
- * @param graph - The source graph
3366
- * @param k - The minimum degree threshold
3367
- * @returns A new graph containing the k-core (may be empty)
3368
- *
3369
- * @example
3370
- * ```typescript
3371
- * // Extract the 3-core (nodes with at least 3 neighbours)
3372
- * const core3 = extractKCore(graph, 3);
3373
- * ```
3374
- */
3375
- function extractKCore(graph, k) {
3376
- if (k < 0) throw new Error(`k must be non-negative, got ${String(k)}`);
3377
- const remaining = /* @__PURE__ */ new Set();
3378
- const degrees = /* @__PURE__ */ new Map();
3379
- for (const nodeId of graph.nodeIds()) {
3380
- remaining.add(nodeId);
3381
- const deg = graph.directed ? graph.degree(nodeId, "both") : graph.degree(nodeId);
3382
- degrees.set(nodeId, deg);
3383
- }
3384
- const toRemove = [];
3385
- for (const [nodeId, deg] of degrees) if (deg < k) toRemove.push(nodeId);
3386
- while (toRemove.length > 0) {
3387
- const nodeId = toRemove.shift();
3388
- if (nodeId === void 0) break;
3389
- if (!remaining.has(nodeId)) continue;
3390
- remaining.delete(nodeId);
3391
- const neighbours = graph.directed ? graph.neighbours(nodeId, "both") : graph.neighbours(nodeId);
3392
- for (const neighbour of neighbours) if (remaining.has(neighbour)) {
3393
- const newDeg = (degrees.get(neighbour) ?? 0) - 1;
3394
- degrees.set(neighbour, newDeg);
3395
- if (newDeg < k && newDeg === k - 1) toRemove.push(neighbour);
3396
- }
3397
- }
3398
- const result = graph.directed ? require_graph.AdjacencyMapGraph.directed() : require_graph.AdjacencyMapGraph.undirected();
3399
- for (const nodeId of remaining) {
3400
- const nodeData = graph.getNode(nodeId);
3401
- if (nodeData !== void 0) result.addNode(nodeData);
3402
- }
3403
- for (const edge of graph.edges()) if (remaining.has(edge.source) && remaining.has(edge.target)) result.addEdge(edge);
3404
- return result;
3405
- }
3406
- //#endregion
3407
- //#region src/extraction/truss.ts
3408
- /**
3409
- * Count triangles involving a given edge.
3410
- *
3411
- * For an edge (u, v), count common neighbours of u and v.
3412
- * Each common neighbour w forms a triangle u-v-w.
3413
- *
3414
- * @param graph - The graph
3415
- * @param u - First endpoint
3416
- * @param v - Second endpoint
3417
- * @returns Number of triangles containing the edge (u, v)
3418
- */
3419
- function countEdgeTriangles(graph, u, v) {
3420
- const uNeighbours = new Set(graph.neighbours(u));
3421
- let count = 0;
3422
- for (const w of graph.neighbours(v)) if (w !== u && uNeighbours.has(w)) count++;
3423
- return count;
3424
- }
3425
- /**
3426
- * Extract the k-truss of a graph.
3427
- *
3428
- * The k-truss is the maximal subgraph where every edge participates in
3429
- * at least k-2 triangles. This is computed by iteratively removing edges
3430
- * with fewer than k-2 triangles, then removing isolated nodes.
3431
- *
3432
- * Note: K-truss is typically defined for undirected graphs. For directed
3433
- * graphs, this treats the graph as undirected for triangle counting.
3434
- *
3435
- * @param graph - The source graph
3436
- * @param k - The minimum triangle count threshold (edge must be in >= k-2 triangles)
3437
- * @returns A new graph containing the k-truss (may be empty)
3438
- *
3439
- * @example
3440
- * ```typescript
3441
- * // Extract the 3-truss (edges in at least 1 triangle)
3442
- * const truss3 = extractKTruss(graph, 3);
3443
- * ```
3444
- */
3445
- function extractKTruss(graph, k) {
3446
- if (k < 2) throw new Error(`k must be at least 2, got ${String(k)}`);
3447
- const minTriangles = k - 2;
3448
- const adjacency = /* @__PURE__ */ new Map();
3449
- const edgeData = /* @__PURE__ */ new Map();
3450
- const remainingEdges = /* @__PURE__ */ new Set();
3451
- for (const nodeId of graph.nodeIds()) adjacency.set(nodeId, /* @__PURE__ */ new Set());
3452
- for (const edge of graph.edges()) {
3453
- const { source, target } = edge;
3454
- adjacency.get(source)?.add(target);
3455
- adjacency.get(target)?.add(source);
3456
- const key = source < target ? `${source}::${target}` : `${target}::${source}`;
3457
- edgeData.set(key, edge);
3458
- remainingEdges.add(key);
3459
- }
3460
- const triangleCounts = /* @__PURE__ */ new Map();
3461
- const edgesToRemove = [];
3462
- for (const key of remainingEdges) {
3463
- const edge = edgeData.get(key);
3464
- if (edge !== void 0) {
3465
- const count = countEdgeTriangles(graph, edge.source, edge.target);
3466
- triangleCounts.set(key, count);
3467
- if (count < minTriangles) edgesToRemove.push(key);
3468
- }
3469
- }
3470
- while (edgesToRemove.length > 0) {
3471
- const edgeKey = edgesToRemove.shift();
3472
- if (edgeKey === void 0) break;
3473
- if (!remainingEdges.has(edgeKey)) continue;
3474
- remainingEdges.delete(edgeKey);
3475
- const edge = edgeData.get(edgeKey);
3476
- if (edge === void 0) continue;
3477
- const { source, target } = edge;
3478
- adjacency.get(source)?.delete(target);
3479
- adjacency.get(target)?.delete(source);
3480
- const sourceNeighbours = adjacency.get(source);
3481
- if (sourceNeighbours !== void 0) {
3482
- for (const w of adjacency.get(target) ?? []) if (sourceNeighbours.has(w)) {
3483
- const keySw = source < w ? `${source}::${w}` : `${w}::${source}`;
3484
- const keyTw = target < w ? `${target}::${w}` : `${w}::${target}`;
3485
- for (const keyToUpdate of [keySw, keyTw]) if (remainingEdges.has(keyToUpdate)) {
3486
- const newCount = (triangleCounts.get(keyToUpdate) ?? 0) - 1;
3487
- triangleCounts.set(keyToUpdate, newCount);
3488
- if (newCount < minTriangles && newCount === minTriangles - 1) edgesToRemove.push(keyToUpdate);
3489
- }
3490
- }
3491
- }
3492
- }
3493
- const nodesWithEdges = /* @__PURE__ */ new Set();
3494
- for (const key of remainingEdges) {
3495
- const edge = edgeData.get(key);
3496
- if (edge !== void 0) {
3497
- nodesWithEdges.add(edge.source);
3498
- nodesWithEdges.add(edge.target);
3499
- }
3500
- }
3501
- const result = graph.directed ? require_graph.AdjacencyMapGraph.directed() : require_graph.AdjacencyMapGraph.undirected();
3502
- for (const nodeId of nodesWithEdges) {
3503
- const nodeData = graph.getNode(nodeId);
3504
- if (nodeData !== void 0) result.addNode(nodeData);
3505
- }
3506
- for (const key of remainingEdges) {
3507
- const edge = edgeData.get(key);
3508
- if (edge !== void 0 && result.hasNode(edge.source) && result.hasNode(edge.target)) result.addEdge(edge);
3509
- }
3510
- return result;
3511
- }
3512
- /**
3513
- * Compute the truss number for each edge.
3514
- *
3515
- * The truss number of an edge is the largest k such that the edge
3516
- * belongs to the k-truss.
3517
- *
3518
- * @param graph - The source graph
3519
- * @returns Map from edge key (canonical "u::v") to truss number
3520
- *
3521
- * @example
3522
- * ```typescript
3523
- * const trussNumbers = computeTrussNumbers(graph);
3524
- * const edgeKey = 'A::B'; // where A < B lexicographically
3525
- * console.log(`Edge A-B is in the ${trussNumbers.get(edgeKey)}-truss`);
3526
- * ```
3527
- */
3528
- function computeTrussNumbers(graph) {
3529
- const adjacency = /* @__PURE__ */ new Map();
3530
- const edgeData = /* @__PURE__ */ new Map();
3531
- const remainingEdges = /* @__PURE__ */ new Set();
3532
- for (const nodeId of graph.nodeIds()) adjacency.set(nodeId, /* @__PURE__ */ new Set());
3533
- for (const edge of graph.edges()) {
3534
- const { source, target } = edge;
3535
- adjacency.get(source)?.add(target);
3536
- adjacency.get(target)?.add(source);
3537
- const key = source < target ? `${source}::${target}` : `${target}::${source}`;
3538
- edgeData.set(key, edge);
3539
- remainingEdges.add(key);
3540
- }
3541
- const triangleCounts = /* @__PURE__ */ new Map();
3542
- for (const key of remainingEdges) {
3543
- const edge = edgeData.get(key);
3544
- if (edge !== void 0) triangleCounts.set(key, countEdgeTriangles(graph, edge.source, edge.target));
3545
- }
3546
- const trussNumbers = /* @__PURE__ */ new Map();
3547
- const edgesByTriangleCount = /* @__PURE__ */ new Map();
3548
- for (const [key, count] of triangleCounts) {
3549
- if (!edgesByTriangleCount.has(count)) edgesByTriangleCount.set(count, /* @__PURE__ */ new Set());
3550
- edgesByTriangleCount.get(count)?.add(key);
3551
- }
3552
- const sortedCounts = [...edgesByTriangleCount.keys()].sort((a, b) => a - b);
3553
- for (const currentCount of sortedCounts) {
3554
- const bucket = edgesByTriangleCount.get(currentCount);
3555
- if (bucket === void 0) continue;
3556
- while (bucket.size > 0) {
3557
- const edgeKey = bucket.values().next().value;
3558
- if (edgeKey === void 0) break;
3559
- bucket.delete(edgeKey);
3560
- if (!remainingEdges.has(edgeKey)) continue;
3561
- const trussNumber = currentCount + 2;
3562
- trussNumbers.set(edgeKey, trussNumber);
3563
- remainingEdges.delete(edgeKey);
3564
- const edge = edgeData.get(edgeKey);
3565
- if (edge === void 0) continue;
3566
- const { source, target } = edge;
3567
- adjacency.get(source)?.delete(target);
3568
- adjacency.get(target)?.delete(source);
3569
- const sourceNeighbours = adjacency.get(source);
3570
- if (sourceNeighbours !== void 0) {
3571
- for (const w of adjacency.get(target) ?? []) if (sourceNeighbours.has(w)) {
3572
- const keySw = source < w ? `${source}::${w}` : `${w}::${source}`;
3573
- const keyTw = target < w ? `${target}::${w}` : `${w}::${target}`;
3574
- for (const keyToUpdate of [keySw, keyTw]) if (remainingEdges.has(keyToUpdate)) {
3575
- const oldCount = triangleCounts.get(keyToUpdate) ?? 0;
3576
- const newCount = oldCount - 1;
3577
- triangleCounts.set(keyToUpdate, newCount);
3578
- edgesByTriangleCount.get(oldCount)?.delete(keyToUpdate);
3579
- if (!edgesByTriangleCount.has(newCount)) edgesByTriangleCount.set(newCount, /* @__PURE__ */ new Set());
3580
- edgesByTriangleCount.get(newCount)?.add(keyToUpdate);
3581
- }
3582
- }
3583
- }
3584
- }
3585
- }
3586
- return trussNumbers;
3587
- }
3588
- //#endregion
3589
- //#region src/extraction/motif.ts
3590
- /**
3591
- * Canonicalise an edge pattern for hashing.
3592
- *
3593
- * Returns a canonical string representation of a small graph pattern.
3594
- */
3595
- function canonicalisePattern(nodeCount, edges) {
3596
- const permutations = getPermutations(nodeCount);
3597
- let minPattern = null;
3598
- for (const perm of permutations) {
3599
- const transformedEdges = edges.map(([u, v]) => {
3600
- const pu = perm[u] ?? -1;
3601
- const pv = perm[v] ?? -1;
3602
- if (pu < 0 || pv < 0) return;
3603
- return pu < pv ? `${String(pu)}-${String(pv)}` : `${String(pv)}-${String(pu)}`;
3604
- }).filter((edge) => edge !== void 0).sort().join(",");
3605
- if (minPattern === null || transformedEdges < minPattern) minPattern = transformedEdges;
3606
- }
3607
- return minPattern ?? "";
3608
- }
3609
- /**
3610
- * Generate all permutations of [0, n-1].
3611
- */
3612
- function getPermutations(n) {
3613
- if (n === 0) return [[]];
3614
- if (n === 1) return [[0]];
3615
- const result = [];
3616
- const arr = Array.from({ length: n }, (_, i) => i);
3617
- function permute(start) {
3618
- if (start === n - 1) {
3619
- result.push([...arr]);
3620
- return;
3621
- }
3622
- for (let i = start; i < n; i++) {
3623
- const startVal = arr[start];
3624
- const iVal = arr[i];
3625
- if (startVal === void 0 || iVal === void 0) continue;
3626
- arr[start] = iVal;
3627
- arr[i] = startVal;
3628
- permute(start + 1);
3629
- arr[start] = startVal;
3630
- arr[i] = iVal;
3631
- }
3632
- }
3633
- permute(0);
3634
- return result;
3635
- }
3636
- /**
3637
- * Enumerate all 3-node motifs in the graph.
3638
- *
3639
- * A 3-node motif (triad) can be one of 4 isomorphism classes for undirected graphs:
3640
- * - Empty: no edges
3641
- * - 1-edge: single edge
3642
- * - 2-star: two edges sharing a node (path of length 2)
3643
- * - Triangle: three edges (complete graph K3)
3644
- *
3645
- * For directed graphs, there are 16 isomorphism classes.
3646
- *
3647
- * @param graph - The source graph
3648
- * @param includeInstances - Whether to include node instances in the result
3649
- * @returns Motif census with counts and optionally instances
3650
- */
3651
- function enumerate3NodeMotifs(graph, includeInstances) {
3652
- const counts = /* @__PURE__ */ new Map();
3653
- const instances = includeInstances ? /* @__PURE__ */ new Map() : void 0;
3654
- const nodeList = [...graph.nodeIds()];
3655
- const n = nodeList.length;
3656
- for (let i = 0; i < n; i++) {
3657
- const ni = nodeList[i];
3658
- if (ni === void 0) continue;
3659
- for (let j = i + 1; j < n; j++) {
3660
- const nj = nodeList[j];
3661
- if (nj === void 0) continue;
3662
- for (let k = j + 1; k < n; k++) {
3663
- const nk = nodeList[k];
3664
- if (nk === void 0) continue;
3665
- const nodes = [
3666
- ni,
3667
- nj,
3668
- nk
3669
- ];
3670
- const edges = [];
3671
- for (const [u, v] of [
3672
- [0, 1],
3673
- [0, 2],
3674
- [1, 2]
3675
- ]) {
3676
- const nu = nodes[u];
3677
- const nv = nodes[v];
3678
- if (nu === void 0 || nv === void 0) continue;
3679
- if (graph.getEdge(nu, nv) !== void 0) edges.push([u, v]);
3680
- else if (!graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([u, v]);
3681
- else if (graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([v, u]);
3682
- }
3683
- const pattern = canonicalisePattern(3, edges);
3684
- const count = counts.get(pattern) ?? 0;
3685
- counts.set(pattern, count + 1);
3686
- if (includeInstances && instances !== void 0) {
3687
- if (!instances.has(pattern)) instances.set(pattern, []);
3688
- const patternInstances = instances.get(pattern);
3689
- if (patternInstances !== void 0) patternInstances.push([
3690
- ni,
3691
- nj,
3692
- nk
3693
- ]);
3694
- }
3695
- }
3696
- }
3697
- }
3698
- if (instances !== void 0) return {
3699
- counts,
3700
- instances
3701
- };
3702
- return { counts };
3703
- }
3704
- /**
3705
- * Enumerate all 4-node motifs in the graph.
3706
- *
3707
- * A 4-node motif can be one of 11 isomorphism classes for undirected graphs
3708
- * (ranging from empty to complete K4), or many more for directed graphs.
3709
- *
3710
- * @param graph - The source graph
3711
- * @param includeInstances - Whether to include node instances in the result
3712
- * @returns Motif census with counts and optionally instances
3713
- */
3714
- function enumerate4NodeMotifs(graph, includeInstances) {
3715
- const counts = /* @__PURE__ */ new Map();
3716
- const instances = includeInstances ? /* @__PURE__ */ new Map() : void 0;
3717
- const nodeList = [...graph.nodeIds()];
3718
- const n = nodeList.length;
3719
- for (let i = 0; i < n; i++) {
3720
- const ni = nodeList[i];
3721
- if (ni === void 0) continue;
3722
- for (let j = i + 1; j < n; j++) {
3723
- const nj = nodeList[j];
3724
- if (nj === void 0) continue;
3725
- for (let k = j + 1; k < n; k++) {
3726
- const nk = nodeList[k];
3727
- if (nk === void 0) continue;
3728
- for (let l = k + 1; l < n; l++) {
3729
- const nl = nodeList[l];
3730
- if (nl === void 0) continue;
3731
- const nodes = [
3732
- ni,
3733
- nj,
3734
- nk,
3735
- nl
3736
- ];
3737
- const edges = [];
3738
- for (const [u, v] of [
3739
- [0, 1],
3740
- [0, 2],
3741
- [0, 3],
3742
- [1, 2],
3743
- [1, 3],
3744
- [2, 3]
3745
- ]) {
3746
- const nu = nodes[u];
3747
- const nv = nodes[v];
3748
- if (nu === void 0 || nv === void 0) continue;
3749
- if (graph.getEdge(nu, nv) !== void 0) edges.push([u, v]);
3750
- else if (!graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([u, v]);
3751
- else if (graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([v, u]);
3752
- }
3753
- const pattern = canonicalisePattern(4, edges);
3754
- const count = counts.get(pattern) ?? 0;
3755
- counts.set(pattern, count + 1);
3756
- if (includeInstances && instances !== void 0) {
3757
- if (!instances.has(pattern)) instances.set(pattern, []);
3758
- const patternInstances = instances.get(pattern);
3759
- if (patternInstances !== void 0) patternInstances.push([
3760
- ni,
3761
- nj,
3762
- nk,
3763
- nl
3764
- ]);
3765
- }
3766
- }
3767
- }
3768
- }
3769
- }
3770
- if (instances !== void 0) return {
3771
- counts,
3772
- instances
3773
- };
3774
- return { counts };
3775
- }
3776
- /**
3777
- * Human-readable names for common 3-node motifs.
3778
- */
3779
- var MOTIF_3_NAMES = new Map([
3780
- ["", "empty"],
3781
- ["0-1", "1-edge"],
3782
- ["0-1,0-2", "2-star"],
3783
- ["0-1,1-2", "path-3"],
3784
- ["0-1,0-2,1-2", "triangle"]
3785
- ]);
3786
- /**
3787
- * Human-readable names for common 4-node motifs.
3788
- */
3789
- var MOTIF_4_NAMES = new Map([
3790
- ["", "empty"],
3791
- ["0-1", "1-edge"],
3792
- ["0-1,0-2", "2-star"],
3793
- ["0-1,0-2,0-3", "3-star"],
3794
- ["0-1,0-2,1-2", "triangle"],
3795
- ["0-1,0-2,1-2,2-3", "paw"],
3796
- ["0-1,0-2,2-3", "path-4"],
3797
- ["0-1,0-2,1-3,2-3", "4-cycle"],
3798
- ["0-1,0-2,1-2,0-3,1-3", "diamond"],
3799
- ["0-1,0-2,0-3,1-2,1-3,2-3", "K4"]
3800
- ]);
3801
- /**
3802
- * Enumerate motifs of a given size in the graph.
3803
- *
3804
- * This function counts all occurrences of each distinct motif type
3805
- * (isomorphism class) in the graph. For graphs with many nodes,
3806
- * 4-motif enumeration can be expensive (O(n^4) worst case).
3807
- *
3808
- * @param graph - The source graph
3809
- * @param size - Motif size (3 or 4 nodes)
3810
- * @returns Motif census with counts per motif type
3811
- *
3812
- * @example
3813
- * ```typescript
3814
- * // Count all triangles and other 3-node patterns
3815
- * const census3 = enumerateMotifs(graph, 3);
3816
- * console.log(`Triangles: ${census3.counts.get('0-1,0-2,1-2')}`);
3817
- *
3818
- * // Count 4-node patterns
3819
- * const census4 = enumerateMotifs(graph, 4);
3820
- * ```
3821
- */
3822
- function enumerateMotifs(graph, size) {
3823
- return size === 3 ? enumerate3NodeMotifs(graph, false) : enumerate4NodeMotifs(graph, false);
3824
- }
3825
- /**
3826
- * Enumerate motifs with optional instance tracking.
3827
- *
3828
- * @param graph - The source graph
3829
- * @param size - Motif size (3 or 4 nodes)
3830
- * @param includeInstances - Whether to include node instances
3831
- * @returns Motif census with counts and optionally instances
3832
- */
3833
- function enumerateMotifsWithInstances(graph, size, includeInstances) {
3834
- return size === 3 ? enumerate3NodeMotifs(graph, includeInstances) : enumerate4NodeMotifs(graph, includeInstances);
3835
- }
3836
- /**
3837
- * Get a human-readable name for a motif pattern.
3838
- *
3839
- * @param pattern - The canonical pattern string
3840
- * @param size - Motif size (3 or 4 nodes)
3841
- * @returns A human-readable name, or the pattern itself if unknown
3842
- */
3843
- function getMotifName(pattern, size) {
3844
- return (size === 3 ? MOTIF_3_NAMES : MOTIF_4_NAMES).get(pattern) ?? pattern;
3845
- }
3846
- //#endregion
3847
- //#region src/extraction/induced-subgraph.ts
3848
- /**
3849
- * Extract the induced subgraph containing exactly the specified nodes.
3850
- *
3851
- * The induced subgraph includes all nodes from the input set that exist
3852
- * in the original graph, plus all edges where both endpoints are in the set.
3853
- *
3854
- * @param graph - The source graph
3855
- * @param nodes - Set of node IDs to include in the subgraph
3856
- * @returns A new graph containing the induced subgraph
3857
- *
3858
- * @example
3859
- * ```typescript
3860
- * const subgraph = extractInducedSubgraph(graph, new Set(['A', 'B', 'C']));
3861
- * ```
3862
- */
3863
- function extractInducedSubgraph(graph, nodes) {
3864
- const result = graph.directed ? require_graph.AdjacencyMapGraph.directed() : require_graph.AdjacencyMapGraph.undirected();
3865
- for (const nodeId of nodes) {
3866
- const nodeData = graph.getNode(nodeId);
3867
- if (nodeData !== void 0) result.addNode(nodeData);
3868
- }
3869
- for (const edge of graph.edges()) if (result.hasNode(edge.source) && result.hasNode(edge.target)) result.addEdge(edge);
3870
- return result;
3871
- }
3872
- //#endregion
3873
- //#region src/extraction/node-filter.ts
3874
- /**
3875
- * Extract a filtered subgraph based on node and edge predicates.
3876
- *
3877
- * Nodes are first filtered by the node predicate (if provided).
3878
- * Edges are then filtered by the edge predicate (if provided), and only
3879
- * retained if both endpoints pass the node predicate.
3880
- *
3881
- * @param graph - The source graph
3882
- * @param options - Filter options specifying node/edge predicates
3883
- * @returns A new graph containing only nodes and edges that pass the predicates
3884
- *
3885
- * @example
3886
- * ```typescript
3887
- * // Extract subgraph of high-weight nodes
3888
- * const filtered = filterSubgraph(graph, {
3889
- * nodePredicate: (node) => (node.weight ?? 0) > 0.5,
3890
- * removeIsolated: true
3891
- * });
3892
- * ```
3893
- */
3894
- function filterSubgraph(graph, options) {
3895
- const { nodePredicate, edgePredicate, removeIsolated = false } = options ?? {};
3896
- const result = graph.directed ? require_graph.AdjacencyMapGraph.directed() : require_graph.AdjacencyMapGraph.undirected();
3897
- const includedNodes = /* @__PURE__ */ new Set();
3898
- for (const nodeId of graph.nodeIds()) {
3899
- const nodeData = graph.getNode(nodeId);
3900
- if (nodeData !== void 0) {
3901
- if (nodePredicate === void 0 || nodePredicate(nodeData)) {
3902
- result.addNode(nodeData);
3903
- includedNodes.add(nodeId);
3904
- }
3905
- }
3906
- }
3907
- for (const edge of graph.edges()) {
3908
- if (!includedNodes.has(edge.source) || !includedNodes.has(edge.target)) continue;
3909
- if (edgePredicate === void 0 || edgePredicate(edge)) result.addEdge(edge);
3910
- }
3911
- if (removeIsolated) {
3912
- const isolatedNodes = [];
3913
- for (const nodeId of result.nodeIds()) if (result.degree(nodeId) === 0) isolatedNodes.push(nodeId);
3914
- for (const nodeId of isolatedNodes) result.removeNode(nodeId);
3915
- }
3916
- return result;
3917
- }
3918
- //#endregion
3919
- exports.AdjacencyMapGraph = require_graph.AdjacencyMapGraph;
3920
- exports.GPUContext = require_gpu.GPUContext;
17
+ const require_extraction = require("../extraction/index.cjs");
18
+ require("../async/index.cjs");
19
+ exports.AdjacencyMapGraph = require_adjacency_map.AdjacencyMapGraph;
3921
20
  exports.GPUNotAvailableError = require_gpu.GPUNotAvailableError;
3922
- exports.PriorityQueue = require_structures.PriorityQueue;
21
+ exports.PriorityQueue = require_priority_queue.PriorityQueue;
3923
22
  exports._computeMean = require_kmeans._computeMean;
3924
- exports.adamicAdar = adamicAdar;
3925
- exports.adamicAdarAsync = adamicAdarAsync;
3926
- exports.adaptive = adaptive;
3927
- exports.adaptiveAsync = adaptiveAsync;
3928
- exports.approximateClusteringCoefficient = require_utils.approximateClusteringCoefficient;
23
+ exports.adamicAdar = require_ranking_mi.adamicAdar;
24
+ exports.adamicAdarAsync = require_ranking_mi.adamicAdarAsync;
25
+ exports.adaptive = require_ranking_mi.adaptive;
26
+ exports.adaptiveAsync = require_ranking_mi.adaptiveAsync;
27
+ exports.approximateClusteringCoefficient = require_utils$1.approximateClusteringCoefficient;
3929
28
  exports.assertWebGPUAvailable = require_gpu.assertWebGPUAvailable;
3930
- exports.base = base;
3931
- exports.baseAsync = baseAsync;
3932
- exports.batchClusteringCoefficients = require_utils.batchClusteringCoefficients;
3933
- exports.betweenness = betweenness;
29
+ exports.base = require_expansion.base;
30
+ exports.baseAsync = require_expansion.baseAsync;
31
+ exports.batchClusteringCoefficients = require_utils$1.batchClusteringCoefficients;
32
+ exports.betweenness = require_ranking.betweenness;
3934
33
  exports.bfs = require_traversal.bfs;
3935
34
  exports.bfsWithPath = require_traversal.bfsWithPath;
3936
- exports.collectAsyncIterable = require_async.collectAsyncIterable;
3937
- exports.communicability = communicability;
3938
- exports.computeJaccard = require_utils.computeJaccard;
3939
- exports.computeTrussNumbers = computeTrussNumbers;
3940
- exports.cosine = cosine;
3941
- exports.cosineAsync = cosineAsync;
3942
- exports.countEdgesOfType = require_utils.countEdgesOfType;
3943
- exports.countNodesOfType = require_utils.countNodesOfType;
3944
- exports.createGPUContext = require_gpu.createGPUContext;
3945
- exports.createResultBuffer = require_gpu.createResultBuffer;
3946
- exports.csrToGPUBuffers = require_gpu.csrToGPUBuffers;
3947
- exports.defaultYieldStrategy = require_async.defaultYieldStrategy;
3948
- exports.degreeSum = degreeSum;
35
+ exports.collectAsyncIterable = require_utils.collectAsyncIterable;
36
+ exports.communicability = require_ranking.communicability;
37
+ exports.computeJaccard = require_utils$1.computeJaccard;
38
+ exports.computeTrussNumbers = require_extraction.computeTrussNumbers;
39
+ exports.cosine = require_ranking_mi.cosine;
40
+ exports.cosineAsync = require_ranking_mi.cosineAsync;
41
+ exports.countEdgesOfType = require_utils$1.countEdgesOfType;
42
+ exports.countNodesOfType = require_utils$1.countNodesOfType;
43
+ exports.csrToTypedBuffers = require_gpu.csrToTypedBuffers;
44
+ exports.defaultYieldStrategy = require_utils.defaultYieldStrategy;
45
+ exports.degreeSum = require_ranking.degreeSum;
3949
46
  exports.detectWebGPU = require_gpu.detectWebGPU;
3950
47
  exports.dfs = require_traversal.dfs;
3951
- exports.dfsPriority = dfsPriority;
3952
- exports.dfsPriorityAsync = dfsPriorityAsync;
3953
- exports.dfsPriorityFn = dfsPriorityFn;
48
+ exports.dfsPriority = require_expansion.dfsPriority;
49
+ exports.dfsPriorityAsync = require_expansion.dfsPriorityAsync;
50
+ exports.dfsPriorityFn = require_expansion.dfsPriorityFn;
3954
51
  exports.dfsWithPath = require_traversal.dfsWithPath;
3955
- exports.dome = dome;
3956
- exports.domeAsync = domeAsync;
3957
- exports.domeHighDegree = domeHighDegree;
3958
- exports.domeHighDegreeAsync = domeHighDegreeAsync;
3959
- exports.edge = edge;
3960
- exports.edgeAsync = edgeAsync;
3961
- exports.entropyFromCounts = require_utils.entropyFromCounts;
3962
- exports.enumerateMotifs = enumerateMotifs;
3963
- exports.enumerateMotifsWithInstances = enumerateMotifsWithInstances;
3964
- exports.etch = etch;
3965
- exports.etchAsync = etchAsync;
3966
- exports.extractEgoNetwork = extractEgoNetwork;
3967
- exports.extractInducedSubgraph = extractInducedSubgraph;
3968
- exports.extractKCore = extractKCore;
3969
- exports.extractKTruss = extractKTruss;
3970
- exports.filterSubgraph = filterSubgraph;
3971
- exports.flux = flux;
3972
- exports.fluxAsync = fluxAsync;
3973
- exports.frontierBalanced = frontierBalanced;
3974
- exports.frontierBalancedAsync = frontierBalancedAsync;
3975
- exports.fuse = fuse;
3976
- exports.fuseAsync = fuseAsync;
3977
- exports.getGPUContext = require_gpu.getGPUContext;
3978
- exports.getMotifName = getMotifName;
52
+ exports.dome = require_expansion.dome;
53
+ exports.domeAsync = require_expansion.domeAsync;
54
+ exports.domeHighDegree = require_expansion.domeHighDegree;
55
+ exports.domeHighDegreeAsync = require_expansion.domeHighDegreeAsync;
56
+ exports.edge = require_expansion.edge;
57
+ exports.edgeAsync = require_expansion.edgeAsync;
58
+ exports.entropyFromCounts = require_utils$1.entropyFromCounts;
59
+ exports.enumerateMotifs = require_extraction.enumerateMotifs;
60
+ exports.enumerateMotifsWithInstances = require_extraction.enumerateMotifsWithInstances;
61
+ exports.etch = require_ranking_mi.etch;
62
+ exports.etchAsync = require_ranking_mi.etchAsync;
63
+ exports.extractEgoNetwork = require_extraction.extractEgoNetwork;
64
+ exports.extractInducedSubgraph = require_extraction.extractInducedSubgraph;
65
+ exports.extractKCore = require_extraction.extractKCore;
66
+ exports.extractKTruss = require_extraction.extractKTruss;
67
+ exports.filterSubgraph = require_extraction.filterSubgraph;
68
+ exports.flux = require_expansion.flux;
69
+ exports.fluxAsync = require_expansion.fluxAsync;
70
+ exports.frontierBalanced = require_expansion.frontierBalanced;
71
+ exports.frontierBalancedAsync = require_expansion.frontierBalancedAsync;
72
+ exports.fuse = require_expansion.fuse;
73
+ exports.fuseAsync = require_expansion.fuseAsync;
74
+ exports.getMotifName = require_extraction.getMotifName;
75
+ exports.gpuBfsLevels = require_gpu.gpuBfsLevels;
76
+ exports.gpuDegreeHistogram = require_gpu.gpuDegreeHistogram;
77
+ exports.gpuJaccardBatch = require_gpu.gpuJaccardBatch;
78
+ exports.gpuPageRank = require_gpu.gpuPageRank;
79
+ exports.gpuSpmv = require_gpu.gpuSpmv;
3979
80
  exports.graphToCSR = require_gpu.graphToCSR;
3980
81
  exports.grasp = require_seeds.grasp;
3981
- exports.hae = hae;
3982
- exports.haeAsync = haeAsync;
3983
- exports.hittingTime = hittingTime;
3984
- exports.hubPromoted = hubPromoted;
3985
- exports.hubPromotedAsync = hubPromotedAsync;
82
+ exports.hae = require_expansion.hae;
83
+ exports.haeAsync = require_expansion.haeAsync;
84
+ exports.hittingTime = require_ranking.hittingTime;
85
+ exports.hubPromoted = require_ranking_mi.hubPromoted;
86
+ exports.hubPromotedAsync = require_ranking_mi.hubPromotedAsync;
87
+ exports.initGPU = require_gpu.initGPU;
88
+ exports.initGPUFromDevice = require_gpu.initGPUFromDevice;
3986
89
  exports.isWebGPUAvailable = require_gpu.isWebGPUAvailable;
3987
- exports.jaccard = jaccard;
3988
- exports.jaccardArithmetic = jaccardArithmetic;
3989
- exports.jaccardAsync = jaccardAsync;
3990
- exports.kHop = kHop;
3991
- exports.katz = katz;
3992
- exports.lace = lace;
3993
- exports.laceAsync = laceAsync;
3994
- exports.localClusteringCoefficient = require_utils.localClusteringCoefficient;
3995
- exports.localTypeEntropy = require_utils.localTypeEntropy;
3996
- exports.maze = maze;
3997
- exports.mazeAsync = mazeAsync;
90
+ exports.jaccard = require_jaccard.jaccard;
91
+ exports.jaccardArithmetic = require_ranking.jaccardArithmetic;
92
+ exports.jaccardAsync = require_jaccard.jaccardAsync;
93
+ exports.kHop = require_expansion.kHop;
94
+ exports.katz = require_ranking.katz;
95
+ exports.lace = require_expansion.lace;
96
+ exports.laceAsync = require_expansion.laceAsync;
97
+ exports.localClusteringCoefficient = require_utils$1.localClusteringCoefficient;
98
+ exports.localTypeEntropy = require_utils$1.localTypeEntropy;
99
+ exports.maze = require_expansion.maze;
100
+ exports.mazeAsync = require_expansion.mazeAsync;
3998
101
  exports.miniBatchKMeans = require_kmeans.miniBatchKMeans;
3999
- exports.neighbourIntersection = require_utils.neighbourIntersection;
4000
- exports.neighbourOverlap = require_utils.neighbourOverlap;
4001
- exports.neighbourSet = require_utils.neighbourSet;
102
+ exports.neighbourIntersection = require_utils$1.neighbourIntersection;
103
+ exports.neighbourOverlap = require_utils$1.neighbourOverlap;
104
+ exports.neighbourSet = require_utils$1.neighbourSet;
4002
105
  exports.normaliseFeatures = require_kmeans.normaliseFeatures;
4003
106
  exports.zScoreNormalise = require_kmeans.normaliseFeatures;
4004
- exports.normalisedEntropy = require_utils.normalisedEntropy;
4005
- exports.notch = notch;
4006
- exports.notchAsync = notchAsync;
4007
- exports.opDegree = require_async.opDegree;
4008
- exports.opGetEdge = require_async.opGetEdge;
4009
- exports.opGetNode = require_async.opGetNode;
4010
- exports.opHasNode = require_async.opHasNode;
4011
- exports.opNeighbours = require_async.opNeighbours;
4012
- exports.opProgress = require_async.opProgress;
4013
- exports.opYield = require_async.opYield;
4014
- exports.overlapCoefficient = overlapCoefficient;
4015
- exports.overlapCoefficientAsync = overlapCoefficientAsync;
4016
- exports.pagerank = pagerank;
4017
- exports.parse = parse;
4018
- exports.parseAsync = parseAsync;
4019
- exports.pipe = pipe;
4020
- exports.pipeAsync = pipeAsync;
4021
- exports.randomPriority = randomPriority;
4022
- exports.randomPriorityAsync = randomPriorityAsync;
4023
- exports.randomRanking = randomRanking;
4024
- exports.randomWalk = randomWalk;
4025
- exports.reach = reach;
4026
- exports.reachAsync = reachAsync;
4027
- exports.readBufferToCPU = require_gpu.readBufferToCPU;
4028
- exports.resistanceDistance = resistanceDistance;
4029
- exports.resolveAsyncOp = require_async.resolveAsyncOp;
4030
- exports.resolveSyncOp = require_async.resolveSyncOp;
4031
- exports.resourceAllocation = resourceAllocation;
4032
- exports.resourceAllocationAsync = resourceAllocationAsync;
4033
- exports.runAsync = require_async.runAsync;
4034
- exports.runSync = require_async.runSync;
4035
- exports.sage = sage;
4036
- exports.sageAsync = sageAsync;
4037
- exports.scale = scale;
4038
- exports.scaleAsync = scaleAsync;
4039
- exports.shannonEntropy = require_utils.shannonEntropy;
4040
- exports.shortest = shortest;
4041
- exports.sift = sift;
4042
- exports.siftAsync = siftAsync;
4043
- exports.skew = skew;
4044
- exports.skewAsync = skewAsync;
4045
- exports.sorensen = sorensen;
4046
- exports.sorensenAsync = sorensenAsync;
4047
- exports.span = span;
4048
- exports.spanAsync = spanAsync;
4049
- exports.standardBfs = standardBfs;
4050
- exports.standardBfsAsync = standardBfsAsync;
107
+ exports.normalisedEntropy = require_utils$1.normalisedEntropy;
108
+ exports.notch = require_ranking_mi.notch;
109
+ exports.notchAsync = require_ranking_mi.notchAsync;
110
+ exports.opDegree = require_ops.opDegree;
111
+ exports.opGetEdge = require_ops.opGetEdge;
112
+ exports.opGetNode = require_ops.opGetNode;
113
+ exports.opHasNode = require_ops.opHasNode;
114
+ exports.opNeighbours = require_ops.opNeighbours;
115
+ exports.opProgress = require_ops.opProgress;
116
+ exports.opYield = require_ops.opYield;
117
+ exports.overlapCoefficient = require_ranking_mi.overlapCoefficient;
118
+ exports.overlapCoefficientAsync = require_ranking_mi.overlapCoefficientAsync;
119
+ exports.pagerank = require_ranking.pagerank;
120
+ exports.parse = require_ranking.parse;
121
+ exports.parseAsync = require_ranking.parseAsync;
122
+ exports.pipe = require_expansion.pipe;
123
+ exports.pipeAsync = require_expansion.pipeAsync;
124
+ exports.randomPriority = require_expansion.randomPriority;
125
+ exports.randomPriorityAsync = require_expansion.randomPriorityAsync;
126
+ exports.randomRanking = require_ranking.randomRanking;
127
+ exports.randomWalk = require_expansion.randomWalk;
128
+ exports.reach = require_expansion.reach;
129
+ exports.reachAsync = require_expansion.reachAsync;
130
+ exports.resistanceDistance = require_ranking.resistanceDistance;
131
+ exports.resolveAsyncOp = require_ops.resolveAsyncOp;
132
+ exports.resolveSyncOp = require_ops.resolveSyncOp;
133
+ exports.resourceAllocation = require_ranking_mi.resourceAllocation;
134
+ exports.resourceAllocationAsync = require_ranking_mi.resourceAllocationAsync;
135
+ exports.runAsync = require_ops.runAsync;
136
+ exports.runSync = require_ops.runSync;
137
+ exports.sage = require_expansion.sage;
138
+ exports.sageAsync = require_expansion.sageAsync;
139
+ exports.scale = require_ranking_mi.scale;
140
+ exports.scaleAsync = require_ranking_mi.scaleAsync;
141
+ exports.shannonEntropy = require_utils$1.shannonEntropy;
142
+ exports.shortest = require_ranking.shortest;
143
+ exports.sift = require_expansion.sift;
144
+ exports.siftAsync = require_expansion.siftAsync;
145
+ exports.skew = require_ranking_mi.skew;
146
+ exports.skewAsync = require_ranking_mi.skewAsync;
147
+ exports.sorensen = require_ranking_mi.sorensen;
148
+ exports.sorensenAsync = require_ranking_mi.sorensenAsync;
149
+ exports.span = require_ranking_mi.span;
150
+ exports.spanAsync = require_ranking_mi.spanAsync;
151
+ exports.standardBfs = require_expansion.standardBfs;
152
+ exports.standardBfsAsync = require_expansion.standardBfsAsync;
4051
153
  exports.stratified = require_seeds.stratified;
4052
- exports.tide = tide;
4053
- exports.tideAsync = tideAsync;
4054
- exports.warp = warp;
4055
- exports.warpAsync = warpAsync;
4056
- exports.widestPath = widestPath;
4057
-
4058
- //# sourceMappingURL=index.cjs.map
154
+ exports.tide = require_expansion.tide;
155
+ exports.tideAsync = require_expansion.tideAsync;
156
+ exports.warp = require_expansion.warp;
157
+ exports.warpAsync = require_expansion.warpAsync;
158
+ exports.widestPath = require_ranking.widestPath;
159
+ exports.withBackend = require_gpu.withBackend;