graphwise 1.7.0 → 1.8.1

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