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