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.
- package/README.md +81 -30
- package/dist/adjacency-map-B6wPtmaq.cjs +234 -0
- package/dist/adjacency-map-B6wPtmaq.cjs.map +1 -0
- package/dist/adjacency-map-D-Ul7V1r.js +229 -0
- package/dist/adjacency-map-D-Ul7V1r.js.map +1 -0
- package/dist/async/index.cjs +16 -0
- package/dist/async/index.js +3 -0
- package/dist/expansion/dfs-priority.d.ts +11 -0
- package/dist/expansion/dfs-priority.d.ts.map +1 -1
- package/dist/expansion/dome.d.ts +20 -0
- package/dist/expansion/dome.d.ts.map +1 -1
- package/dist/expansion/edge.d.ts +18 -0
- package/dist/expansion/edge.d.ts.map +1 -1
- package/dist/expansion/flux.d.ts +16 -0
- package/dist/expansion/flux.d.ts.map +1 -1
- package/dist/expansion/frontier-balanced.d.ts +11 -0
- package/dist/expansion/frontier-balanced.d.ts.map +1 -1
- package/dist/expansion/fuse.d.ts +16 -0
- package/dist/expansion/fuse.d.ts.map +1 -1
- package/dist/expansion/hae.d.ts +16 -0
- package/dist/expansion/hae.d.ts.map +1 -1
- package/dist/expansion/index.cjs +43 -0
- package/dist/expansion/index.js +2 -0
- package/dist/expansion/lace.d.ts +16 -0
- package/dist/expansion/lace.d.ts.map +1 -1
- package/dist/expansion/maze.d.ts +17 -0
- package/dist/expansion/maze.d.ts.map +1 -1
- package/dist/expansion/pipe.d.ts +16 -0
- package/dist/expansion/pipe.d.ts.map +1 -1
- package/dist/expansion/random-priority.d.ts +18 -0
- package/dist/expansion/random-priority.d.ts.map +1 -1
- package/dist/expansion/reach.d.ts +17 -0
- package/dist/expansion/reach.d.ts.map +1 -1
- package/dist/expansion/sage.d.ts +15 -0
- package/dist/expansion/sage.d.ts.map +1 -1
- package/dist/expansion/sift.d.ts +16 -0
- package/dist/expansion/sift.d.ts.map +1 -1
- package/dist/expansion/standard-bfs.d.ts +11 -0
- package/dist/expansion/standard-bfs.d.ts.map +1 -1
- package/dist/expansion/tide.d.ts +16 -0
- package/dist/expansion/tide.d.ts.map +1 -1
- package/dist/expansion/warp.d.ts +16 -0
- package/dist/expansion/warp.d.ts.map +1 -1
- package/dist/expansion-FkmEYlrQ.cjs +1949 -0
- package/dist/expansion-FkmEYlrQ.cjs.map +1 -0
- package/dist/expansion-sldRognt.js +1704 -0
- package/dist/expansion-sldRognt.js.map +1 -0
- package/dist/extraction/index.cjs +630 -0
- package/dist/extraction/index.cjs.map +1 -0
- package/dist/extraction/index.js +621 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/graph/index.cjs +2 -229
- package/dist/graph/index.js +1 -228
- package/dist/index/index.cjs +131 -3406
- package/dist/index/index.js +14 -3334
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/jaccard-Bmd1IEFO.cjs +50 -0
- package/dist/jaccard-Bmd1IEFO.cjs.map +1 -0
- package/dist/jaccard-Yddrtt5D.js +39 -0
- package/dist/jaccard-Yddrtt5D.js.map +1 -0
- package/dist/{kmeans-BIgSyGKu.cjs → kmeans-D3yX5QFs.cjs} +1 -1
- package/dist/{kmeans-BIgSyGKu.cjs.map → kmeans-D3yX5QFs.cjs.map} +1 -1
- package/dist/{kmeans-87ExSUNZ.js → kmeans-DVCe61Me.js} +1 -1
- package/dist/{kmeans-87ExSUNZ.js.map → kmeans-DVCe61Me.js.map} +1 -1
- package/dist/ops-4nmI-pwk.cjs +277 -0
- package/dist/ops-4nmI-pwk.cjs.map +1 -0
- package/dist/ops-Zsu4ecEG.js +212 -0
- package/dist/ops-Zsu4ecEG.js.map +1 -0
- package/dist/priority-queue-ChVLoG6T.cjs +148 -0
- package/dist/priority-queue-ChVLoG6T.cjs.map +1 -0
- package/dist/priority-queue-DqCuFTR8.js +143 -0
- package/dist/priority-queue-DqCuFTR8.js.map +1 -0
- package/dist/ranking/index.cjs +43 -0
- package/dist/ranking/index.js +4 -0
- package/dist/ranking/mi/adamic-adar.d.ts +8 -0
- package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
- package/dist/ranking/mi/adaptive.d.ts +8 -0
- package/dist/ranking/mi/adaptive.d.ts.map +1 -1
- package/dist/ranking/mi/cosine.d.ts +7 -0
- package/dist/ranking/mi/cosine.d.ts.map +1 -1
- package/dist/ranking/mi/etch.d.ts +8 -0
- package/dist/ranking/mi/etch.d.ts.map +1 -1
- package/dist/ranking/mi/hub-promoted.d.ts +7 -0
- package/dist/ranking/mi/hub-promoted.d.ts.map +1 -1
- package/dist/ranking/mi/index.cjs +581 -0
- package/dist/ranking/mi/index.cjs.map +1 -0
- package/dist/ranking/mi/index.js +555 -0
- package/dist/ranking/mi/index.js.map +1 -0
- package/dist/ranking/mi/jaccard.d.ts +7 -0
- package/dist/ranking/mi/jaccard.d.ts.map +1 -1
- package/dist/ranking/mi/notch.d.ts +8 -0
- package/dist/ranking/mi/notch.d.ts.map +1 -1
- package/dist/ranking/mi/overlap-coefficient.d.ts +7 -0
- package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -1
- package/dist/ranking/mi/resource-allocation.d.ts +8 -0
- package/dist/ranking/mi/resource-allocation.d.ts.map +1 -1
- package/dist/ranking/mi/scale.d.ts +7 -0
- package/dist/ranking/mi/scale.d.ts.map +1 -1
- package/dist/ranking/mi/skew.d.ts +7 -0
- package/dist/ranking/mi/skew.d.ts.map +1 -1
- package/dist/ranking/mi/sorensen.d.ts +7 -0
- package/dist/ranking/mi/sorensen.d.ts.map +1 -1
- package/dist/ranking/mi/span.d.ts +8 -0
- package/dist/ranking/mi/span.d.ts.map +1 -1
- package/dist/ranking/mi/types.d.ts +12 -0
- package/dist/ranking/mi/types.d.ts.map +1 -1
- package/dist/ranking/parse.d.ts +24 -1
- package/dist/ranking/parse.d.ts.map +1 -1
- package/dist/ranking-mUm9rV-C.js +1016 -0
- package/dist/ranking-mUm9rV-C.js.map +1 -0
- package/dist/ranking-riRrEVAR.cjs +1093 -0
- package/dist/ranking-riRrEVAR.cjs.map +1 -0
- package/dist/seeds/index.cjs +1 -1
- package/dist/seeds/index.js +1 -1
- package/dist/structures/index.cjs +2 -143
- package/dist/structures/index.js +1 -142
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils-CcIrKAEb.js +22 -0
- package/dist/utils-CcIrKAEb.js.map +1 -0
- package/dist/utils-CpyzmzIF.cjs +33 -0
- package/dist/utils-CpyzmzIF.cjs.map +1 -0
- package/package.json +6 -1
- package/dist/graph/index.cjs.map +0 -1
- package/dist/graph/index.js.map +0 -1
- package/dist/index/index.cjs.map +0 -1
- package/dist/index/index.js.map +0 -1
- package/dist/structures/index.cjs.map +0 -1
- package/dist/structures/index.js.map +0 -1
|
@@ -0,0 +1,1949 @@
|
|
|
1
|
+
const require_priority_queue = require("./priority-queue-ChVLoG6T.cjs");
|
|
2
|
+
const require_ops = require("./ops-4nmI-pwk.cjs");
|
|
3
|
+
const require_utils = require("./utils/index.cjs");
|
|
4
|
+
const require_jaccard = require("./jaccard-Bmd1IEFO.cjs");
|
|
5
|
+
//#region src/expansion/base-helpers.ts
|
|
6
|
+
/**
|
|
7
|
+
* Check whether expansion should continue given current progress.
|
|
8
|
+
*
|
|
9
|
+
* Returns shouldContinue=false as soon as any configured limit is reached,
|
|
10
|
+
* along with the appropriate termination reason.
|
|
11
|
+
*
|
|
12
|
+
* @param iterations - Number of iterations completed so far
|
|
13
|
+
* @param nodesVisited - Number of distinct nodes visited so far
|
|
14
|
+
* @param pathsFound - Number of paths discovered so far
|
|
15
|
+
* @param limits - Configured expansion limits (0 = unlimited)
|
|
16
|
+
* @returns Whether to continue and the termination reason if stopping
|
|
17
|
+
*/
|
|
18
|
+
function continueExpansion(iterations, nodesVisited, pathsFound, limits) {
|
|
19
|
+
if (limits.maxIterations > 0 && iterations >= limits.maxIterations) return {
|
|
20
|
+
shouldContinue: false,
|
|
21
|
+
termination: "limit"
|
|
22
|
+
};
|
|
23
|
+
if (limits.maxNodes > 0 && nodesVisited >= limits.maxNodes) return {
|
|
24
|
+
shouldContinue: false,
|
|
25
|
+
termination: "limit"
|
|
26
|
+
};
|
|
27
|
+
if (limits.maxPaths > 0 && pathsFound >= limits.maxPaths) return {
|
|
28
|
+
shouldContinue: false,
|
|
29
|
+
termination: "limit"
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
shouldContinue: true,
|
|
33
|
+
termination: "exhausted"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reconstruct path from collision point.
|
|
38
|
+
*
|
|
39
|
+
* Traces backwards through the predecessor maps of both frontiers from the
|
|
40
|
+
* collision node, then concatenates the two halves to form the full path.
|
|
41
|
+
*
|
|
42
|
+
* @param collisionNode - The node where the two frontiers met
|
|
43
|
+
* @param frontierA - Index of the first frontier
|
|
44
|
+
* @param frontierB - Index of the second frontier
|
|
45
|
+
* @param predecessors - Predecessor maps, one per frontier
|
|
46
|
+
* @param seeds - Seed nodes, one per frontier
|
|
47
|
+
* @returns The reconstructed path, or null if seeds are missing
|
|
48
|
+
*/
|
|
49
|
+
function reconstructPath$1(collisionNode, frontierA, frontierB, predecessors, seeds) {
|
|
50
|
+
const pathA = [collisionNode];
|
|
51
|
+
const predA = predecessors[frontierA];
|
|
52
|
+
if (predA !== void 0) {
|
|
53
|
+
let node = collisionNode;
|
|
54
|
+
let next = predA.get(node);
|
|
55
|
+
while (next !== null && next !== void 0) {
|
|
56
|
+
node = next;
|
|
57
|
+
pathA.unshift(node);
|
|
58
|
+
next = predA.get(node);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const pathB = [];
|
|
62
|
+
const predB = predecessors[frontierB];
|
|
63
|
+
if (predB !== void 0) {
|
|
64
|
+
let node = collisionNode;
|
|
65
|
+
let next = predB.get(node);
|
|
66
|
+
while (next !== null && next !== void 0) {
|
|
67
|
+
node = next;
|
|
68
|
+
pathB.push(node);
|
|
69
|
+
next = predB.get(node);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const fullPath = [...pathA, ...pathB];
|
|
73
|
+
const seedA = seeds[frontierA];
|
|
74
|
+
const seedB = seeds[frontierB];
|
|
75
|
+
if (seedA === void 0 || seedB === void 0) return null;
|
|
76
|
+
return {
|
|
77
|
+
fromSeed: seedA,
|
|
78
|
+
toSeed: seedB,
|
|
79
|
+
nodes: fullPath
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create an empty expansion result for early termination (e.g. no seeds given).
|
|
84
|
+
*
|
|
85
|
+
* @param algorithm - Name of the algorithm producing this result
|
|
86
|
+
* @param startTime - performance.now() timestamp taken before the algorithm began
|
|
87
|
+
* @returns An ExpansionResult with zero paths and zero stats
|
|
88
|
+
*/
|
|
89
|
+
function emptyResult$2(algorithm, startTime) {
|
|
90
|
+
return {
|
|
91
|
+
paths: [],
|
|
92
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
93
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
94
|
+
visitedPerFrontier: [],
|
|
95
|
+
stats: {
|
|
96
|
+
iterations: 0,
|
|
97
|
+
nodesVisited: 0,
|
|
98
|
+
edgesTraversed: 0,
|
|
99
|
+
pathsFound: 0,
|
|
100
|
+
durationMs: performance.now() - startTime,
|
|
101
|
+
algorithm,
|
|
102
|
+
termination: "exhausted"
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/expansion/base-core.ts
|
|
108
|
+
/**
|
|
109
|
+
* Default priority function — degree-ordered (DOME).
|
|
110
|
+
*
|
|
111
|
+
* Lower degree = higher priority, so sparse nodes are explored before hubs.
|
|
112
|
+
*/
|
|
113
|
+
function degreePriority(_nodeId, context) {
|
|
114
|
+
return context.degree;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Generator core of the BASE expansion algorithm.
|
|
118
|
+
*
|
|
119
|
+
* Yields GraphOp objects to request graph data, allowing the caller to
|
|
120
|
+
* provide a sync or async runner. The optional `graphRef` parameter is
|
|
121
|
+
* required when the priority function accesses `context.graph` — it is
|
|
122
|
+
* populated in sync mode by `base()`. In async mode (Phase 4+), a proxy
|
|
123
|
+
* graph may be supplied instead.
|
|
124
|
+
*
|
|
125
|
+
* @param graphMeta - Immutable graph metadata (directed, nodeCount, edgeCount)
|
|
126
|
+
* @param seeds - Seed nodes for expansion
|
|
127
|
+
* @param config - Expansion configuration (priority, limits, debug)
|
|
128
|
+
* @param graphRef - Optional real graph reference for context.graph in priority functions
|
|
129
|
+
* @returns An ExpansionResult with all discovered paths and statistics
|
|
130
|
+
*/
|
|
131
|
+
function* baseCore(graphMeta, seeds, config, graphRef) {
|
|
132
|
+
const startTime = performance.now();
|
|
133
|
+
const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
|
|
134
|
+
if (seeds.length === 0) return emptyResult$2("base", startTime);
|
|
135
|
+
const numFrontiers = seeds.length;
|
|
136
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
137
|
+
const combinedVisited = /* @__PURE__ */ new Map();
|
|
138
|
+
const visitedByFrontier = [];
|
|
139
|
+
const predecessors = [];
|
|
140
|
+
const queues = [];
|
|
141
|
+
for (let i = 0; i < numFrontiers; i++) {
|
|
142
|
+
visitedByFrontier.push(/* @__PURE__ */ new Map());
|
|
143
|
+
predecessors.push(/* @__PURE__ */ new Map());
|
|
144
|
+
queues.push(new require_priority_queue.PriorityQueue());
|
|
145
|
+
const seed = seeds[i];
|
|
146
|
+
if (seed === void 0) continue;
|
|
147
|
+
const seedNode = seed.id;
|
|
148
|
+
predecessors[i]?.set(seedNode, null);
|
|
149
|
+
combinedVisited.set(seedNode, i);
|
|
150
|
+
allVisited.add(seedNode);
|
|
151
|
+
const seedDegree = yield* require_ops.opDegree(seedNode);
|
|
152
|
+
const seedPriority = priority(seedNode, buildPriorityContext(seedNode, i, combinedVisited, allVisited, [], 0, seedDegree, graphRef));
|
|
153
|
+
queues[i]?.push({
|
|
154
|
+
nodeId: seedNode,
|
|
155
|
+
frontierIndex: i,
|
|
156
|
+
predecessor: null
|
|
157
|
+
}, seedPriority);
|
|
158
|
+
}
|
|
159
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
160
|
+
const discoveredPaths = [];
|
|
161
|
+
let iterations = 0;
|
|
162
|
+
let edgesTraversed = 0;
|
|
163
|
+
let termination = "exhausted";
|
|
164
|
+
const limits = {
|
|
165
|
+
maxIterations,
|
|
166
|
+
maxNodes,
|
|
167
|
+
maxPaths
|
|
168
|
+
};
|
|
169
|
+
for (;;) {
|
|
170
|
+
const check = continueExpansion(iterations, allVisited.size, discoveredPaths.length, limits);
|
|
171
|
+
if (!check.shouldContinue) {
|
|
172
|
+
termination = check.termination;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
let lowestPriority = Number.POSITIVE_INFINITY;
|
|
176
|
+
let activeFrontier = -1;
|
|
177
|
+
for (let i = 0; i < numFrontiers; i++) {
|
|
178
|
+
const queue = queues[i];
|
|
179
|
+
if (queue !== void 0 && !queue.isEmpty()) {
|
|
180
|
+
const peek = queue.peek();
|
|
181
|
+
if (peek !== void 0 && peek.priority < lowestPriority) {
|
|
182
|
+
lowestPriority = peek.priority;
|
|
183
|
+
activeFrontier = i;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (activeFrontier < 0) {
|
|
188
|
+
termination = "exhausted";
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const queue = queues[activeFrontier];
|
|
192
|
+
if (queue === void 0) break;
|
|
193
|
+
const entry = queue.pop();
|
|
194
|
+
if (entry === void 0) break;
|
|
195
|
+
const { nodeId, predecessor } = entry.item;
|
|
196
|
+
const frontierVisited = visitedByFrontier[activeFrontier];
|
|
197
|
+
if (frontierVisited === void 0 || frontierVisited.has(nodeId)) continue;
|
|
198
|
+
frontierVisited.set(nodeId, activeFrontier);
|
|
199
|
+
combinedVisited.set(nodeId, activeFrontier);
|
|
200
|
+
if (predecessor !== null) {
|
|
201
|
+
const predMap = predecessors[activeFrontier];
|
|
202
|
+
if (predMap !== void 0) predMap.set(nodeId, predecessor);
|
|
203
|
+
}
|
|
204
|
+
allVisited.add(nodeId);
|
|
205
|
+
if (debug) console.log(`[BASE] Iteration ${String(iterations)}: Frontier ${String(activeFrontier)} visiting ${nodeId}`);
|
|
206
|
+
for (let otherFrontier = 0; otherFrontier < numFrontiers; otherFrontier++) {
|
|
207
|
+
if (otherFrontier === activeFrontier) continue;
|
|
208
|
+
const otherVisited = visitedByFrontier[otherFrontier];
|
|
209
|
+
if (otherVisited === void 0) continue;
|
|
210
|
+
if (otherVisited.has(nodeId)) {
|
|
211
|
+
const path = reconstructPath$1(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
|
|
212
|
+
if (path !== null) {
|
|
213
|
+
discoveredPaths.push(path);
|
|
214
|
+
if (debug) console.log(`[BASE] Path found: ${path.nodes.join(" -> ")}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const neighbours = yield* require_ops.opNeighbours(nodeId);
|
|
219
|
+
for (const neighbour of neighbours) {
|
|
220
|
+
edgesTraversed++;
|
|
221
|
+
const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
|
|
222
|
+
let targets = sampledEdgeMap.get(s);
|
|
223
|
+
if (targets === void 0) {
|
|
224
|
+
targets = /* @__PURE__ */ new Set();
|
|
225
|
+
sampledEdgeMap.set(s, targets);
|
|
226
|
+
}
|
|
227
|
+
targets.add(t);
|
|
228
|
+
const fv = visitedByFrontier[activeFrontier];
|
|
229
|
+
if (fv === void 0 || fv.has(neighbour)) continue;
|
|
230
|
+
const neighbourDegree = yield* require_ops.opDegree(neighbour);
|
|
231
|
+
const neighbourPriority = priority(neighbour, buildPriorityContext(neighbour, activeFrontier, combinedVisited, allVisited, discoveredPaths, iterations + 1, neighbourDegree, graphRef));
|
|
232
|
+
queue.push({
|
|
233
|
+
nodeId: neighbour,
|
|
234
|
+
frontierIndex: activeFrontier,
|
|
235
|
+
predecessor: nodeId
|
|
236
|
+
}, neighbourPriority);
|
|
237
|
+
}
|
|
238
|
+
iterations++;
|
|
239
|
+
}
|
|
240
|
+
const endTime = performance.now();
|
|
241
|
+
const visitedPerFrontier = visitedByFrontier.map((m) => new Set(m.keys()));
|
|
242
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
243
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
244
|
+
return {
|
|
245
|
+
paths: discoveredPaths,
|
|
246
|
+
sampledNodes: allVisited,
|
|
247
|
+
sampledEdges: edgeTuples,
|
|
248
|
+
visitedPerFrontier,
|
|
249
|
+
stats: {
|
|
250
|
+
iterations,
|
|
251
|
+
nodesVisited: allVisited.size,
|
|
252
|
+
edgesTraversed,
|
|
253
|
+
pathsFound: discoveredPaths.length,
|
|
254
|
+
durationMs: endTime - startTime,
|
|
255
|
+
algorithm: "base",
|
|
256
|
+
termination
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Create a sentinel ReadableGraph that throws if any member is accessed.
|
|
262
|
+
*
|
|
263
|
+
* Used in async mode when no graphRef is provided. Gives a clear error
|
|
264
|
+
* message rather than silently returning incorrect results if a priority
|
|
265
|
+
* function attempts to access `context.graph` before Phase 4b introduces
|
|
266
|
+
* a real async proxy.
|
|
267
|
+
*/
|
|
268
|
+
function makeNoGraphSentinel() {
|
|
269
|
+
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.";
|
|
270
|
+
const fail = () => {
|
|
271
|
+
throw new Error(msg);
|
|
272
|
+
};
|
|
273
|
+
return {
|
|
274
|
+
get directed() {
|
|
275
|
+
return fail();
|
|
276
|
+
},
|
|
277
|
+
get nodeCount() {
|
|
278
|
+
return fail();
|
|
279
|
+
},
|
|
280
|
+
get edgeCount() {
|
|
281
|
+
return fail();
|
|
282
|
+
},
|
|
283
|
+
hasNode: fail,
|
|
284
|
+
getNode: fail,
|
|
285
|
+
nodeIds: fail,
|
|
286
|
+
neighbours: fail,
|
|
287
|
+
degree: fail,
|
|
288
|
+
getEdge: fail,
|
|
289
|
+
edges: fail
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Build a PriorityContext for a node using a pre-fetched degree.
|
|
294
|
+
*
|
|
295
|
+
* When `graphRef` is provided (sync mode), it is used as `context.graph` so
|
|
296
|
+
* priority functions can access the graph directly. When it is absent (async
|
|
297
|
+
* mode), a Proxy is used in its place that throws a clear error if any
|
|
298
|
+
* property is accessed — this prevents silent failures until Phase 4b
|
|
299
|
+
* introduces a real async proxy graph.
|
|
300
|
+
*/
|
|
301
|
+
function buildPriorityContext(_nodeId, frontierIndex, combinedVisited, allVisited, discoveredPaths, iteration, degree, graphRef) {
|
|
302
|
+
return {
|
|
303
|
+
graph: graphRef ?? makeNoGraphSentinel(),
|
|
304
|
+
degree,
|
|
305
|
+
frontierIndex,
|
|
306
|
+
visitedByFrontier: combinedVisited,
|
|
307
|
+
allVisited,
|
|
308
|
+
discoveredPaths,
|
|
309
|
+
iteration
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/expansion/base.ts
|
|
314
|
+
/**
|
|
315
|
+
* Run BASE expansion synchronously.
|
|
316
|
+
*
|
|
317
|
+
* Delegates to baseCore + runSync. Behaviour is identical to the previous
|
|
318
|
+
* direct implementation — all existing callers are unaffected.
|
|
319
|
+
*
|
|
320
|
+
* @param graph - Source graph
|
|
321
|
+
* @param seeds - Seed nodes for expansion
|
|
322
|
+
* @param config - Expansion configuration
|
|
323
|
+
* @returns Expansion result with discovered paths
|
|
324
|
+
*/
|
|
325
|
+
function base(graph, seeds, config) {
|
|
326
|
+
return require_ops.runSync(baseCore({
|
|
327
|
+
directed: graph.directed,
|
|
328
|
+
nodeCount: graph.nodeCount,
|
|
329
|
+
edgeCount: graph.edgeCount
|
|
330
|
+
}, seeds, config, graph), graph);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Run BASE expansion asynchronously.
|
|
334
|
+
*
|
|
335
|
+
* Delegates to baseCore + runAsync. Supports:
|
|
336
|
+
* - Cancellation via AbortSignal (config.signal)
|
|
337
|
+
* - Progress callbacks (config.onProgress)
|
|
338
|
+
* - Custom cooperative yield strategies (config.yieldStrategy)
|
|
339
|
+
*
|
|
340
|
+
* Note: priority functions that access `context.graph` are not supported in
|
|
341
|
+
* async mode without a graph proxy (Phase 4b). The default degree-based
|
|
342
|
+
* priority (DOME) does not access context.graph and works correctly.
|
|
343
|
+
*
|
|
344
|
+
* @param graph - Async source graph
|
|
345
|
+
* @param seeds - Seed nodes for expansion
|
|
346
|
+
* @param config - Expansion and async runner configuration
|
|
347
|
+
* @returns Promise resolving to the expansion result
|
|
348
|
+
*/
|
|
349
|
+
async function baseAsync(graph, seeds, config) {
|
|
350
|
+
const [nodeCount, edgeCount] = await Promise.all([graph.nodeCount, graph.edgeCount]);
|
|
351
|
+
const gen = baseCore({
|
|
352
|
+
directed: graph.directed,
|
|
353
|
+
nodeCount,
|
|
354
|
+
edgeCount
|
|
355
|
+
}, seeds, config);
|
|
356
|
+
const runnerOptions = {};
|
|
357
|
+
if (config?.signal !== void 0) runnerOptions.signal = config.signal;
|
|
358
|
+
if (config?.onProgress !== void 0) runnerOptions.onProgress = config.onProgress;
|
|
359
|
+
if (config?.yieldStrategy !== void 0) runnerOptions.yieldStrategy = config.yieldStrategy;
|
|
360
|
+
return require_ops.runAsync(gen, graph, runnerOptions);
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/expansion/dome.ts
|
|
364
|
+
/**
|
|
365
|
+
* DOME priority: lower degree is expanded first.
|
|
366
|
+
*/
|
|
367
|
+
function domePriority(_nodeId, context) {
|
|
368
|
+
return context.degree;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* DOME high-degree priority: negate degree to prioritise high-degree nodes.
|
|
372
|
+
*/
|
|
373
|
+
function domeHighDegreePriority(_nodeId, context) {
|
|
374
|
+
return -context.degree;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Run DOME expansion (degree-ordered).
|
|
378
|
+
*
|
|
379
|
+
* @param graph - Source graph
|
|
380
|
+
* @param seeds - Seed nodes for expansion
|
|
381
|
+
* @param config - Expansion configuration
|
|
382
|
+
* @returns Expansion result with discovered paths
|
|
383
|
+
*/
|
|
384
|
+
function dome(graph, seeds, config) {
|
|
385
|
+
return base(graph, seeds, {
|
|
386
|
+
...config,
|
|
387
|
+
priority: domePriority
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Run DOME expansion asynchronously (degree-ordered).
|
|
392
|
+
*
|
|
393
|
+
* @param graph - Async source graph
|
|
394
|
+
* @param seeds - Seed nodes for expansion
|
|
395
|
+
* @param config - Expansion and async runner configuration
|
|
396
|
+
* @returns Promise resolving to the expansion result
|
|
397
|
+
*/
|
|
398
|
+
async function domeAsync(graph, seeds, config) {
|
|
399
|
+
return baseAsync(graph, seeds, {
|
|
400
|
+
...config,
|
|
401
|
+
priority: domePriority
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* DOME with reverse priority (high degree first).
|
|
406
|
+
*/
|
|
407
|
+
function domeHighDegree(graph, seeds, config) {
|
|
408
|
+
return base(graph, seeds, {
|
|
409
|
+
...config,
|
|
410
|
+
priority: domeHighDegreePriority
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Run DOME high-degree expansion asynchronously (high degree first).
|
|
415
|
+
*
|
|
416
|
+
* @param graph - Async source graph
|
|
417
|
+
* @param seeds - Seed nodes for expansion
|
|
418
|
+
* @param config - Expansion and async runner configuration
|
|
419
|
+
* @returns Promise resolving to the expansion result
|
|
420
|
+
*/
|
|
421
|
+
async function domeHighDegreeAsync(graph, seeds, config) {
|
|
422
|
+
return baseAsync(graph, seeds, {
|
|
423
|
+
...config,
|
|
424
|
+
priority: domeHighDegreePriority
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/expansion/hae.ts
|
|
429
|
+
var EPSILON = 1e-10;
|
|
430
|
+
/**
|
|
431
|
+
* Default type mapper - uses node.type property.
|
|
432
|
+
*/
|
|
433
|
+
function defaultTypeMapper$1(node) {
|
|
434
|
+
return node.type ?? "default";
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Create a priority function using the given type mapper.
|
|
438
|
+
*/
|
|
439
|
+
function createHAEPriority(typeMapper) {
|
|
440
|
+
return function haePriority(nodeId, context) {
|
|
441
|
+
const graph = context.graph;
|
|
442
|
+
const neighbours = graph.neighbours(nodeId);
|
|
443
|
+
const neighbourTypes = [];
|
|
444
|
+
for (const neighbour of neighbours) {
|
|
445
|
+
const node = graph.getNode(neighbour);
|
|
446
|
+
if (node !== void 0) neighbourTypes.push(typeMapper(node));
|
|
447
|
+
}
|
|
448
|
+
return 1 / (require_utils.localTypeEntropy(neighbourTypes) + EPSILON) * Math.log(context.degree + 1);
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Run HAE expansion (Heterogeneity-Aware Expansion).
|
|
453
|
+
*
|
|
454
|
+
* Discovers paths by prioritising nodes with diverse neighbour types,
|
|
455
|
+
* using a custom type mapper for flexible type extraction.
|
|
456
|
+
*
|
|
457
|
+
* @param graph - Source graph
|
|
458
|
+
* @param seeds - Seed nodes for expansion
|
|
459
|
+
* @param config - HAE configuration with optional typeMapper
|
|
460
|
+
* @returns Expansion result with discovered paths
|
|
461
|
+
*/
|
|
462
|
+
function hae(graph, seeds, config) {
|
|
463
|
+
const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
|
|
464
|
+
return base(graph, seeds, {
|
|
465
|
+
...config,
|
|
466
|
+
priority: createHAEPriority(typeMapper)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Run HAE expansion asynchronously.
|
|
471
|
+
*
|
|
472
|
+
* Note: the HAE priority function accesses `context.graph` to retrieve
|
|
473
|
+
* neighbour types. Full async equivalence requires PriorityContext
|
|
474
|
+
* refactoring (Phase 4b deferred). This export establishes the async API
|
|
475
|
+
* surface; use with a `wrapAsync`-wrapped sync graph for testing.
|
|
476
|
+
*
|
|
477
|
+
* @param graph - Async source graph
|
|
478
|
+
* @param seeds - Seed nodes for expansion
|
|
479
|
+
* @param config - HAE configuration combined with async runner options
|
|
480
|
+
* @returns Promise resolving to the expansion result
|
|
481
|
+
*/
|
|
482
|
+
async function haeAsync(graph, seeds, config) {
|
|
483
|
+
const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
|
|
484
|
+
return baseAsync(graph, seeds, {
|
|
485
|
+
...config,
|
|
486
|
+
priority: createHAEPriority(typeMapper)
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/expansion/edge.ts
|
|
491
|
+
/** Default type mapper: reads `node.type`, falling back to "default". */
|
|
492
|
+
var defaultTypeMapper = (n) => typeof n.type === "string" ? n.type : "default";
|
|
493
|
+
/**
|
|
494
|
+
* Run EDGE expansion (Entropy-Driven Graph Expansion).
|
|
495
|
+
*
|
|
496
|
+
* Discovers paths by prioritising nodes with diverse neighbour types,
|
|
497
|
+
* deferring nodes with homogeneous neighbourhoods.
|
|
498
|
+
*
|
|
499
|
+
* @param graph - Source graph
|
|
500
|
+
* @param seeds - Seed nodes for expansion
|
|
501
|
+
* @param config - Expansion configuration
|
|
502
|
+
* @returns Expansion result with discovered paths
|
|
503
|
+
*/
|
|
504
|
+
function edge(graph, seeds, config) {
|
|
505
|
+
return hae(graph, seeds, {
|
|
506
|
+
...config,
|
|
507
|
+
typeMapper: defaultTypeMapper
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Run EDGE expansion asynchronously.
|
|
512
|
+
*
|
|
513
|
+
* Delegates to `haeAsync` with the default `node.type` mapper.
|
|
514
|
+
*
|
|
515
|
+
* Note: the HAE priority function accesses `context.graph` to retrieve
|
|
516
|
+
* neighbour types. Full async equivalence requires PriorityContext
|
|
517
|
+
* refactoring (Phase 4b deferred). This export establishes the async API
|
|
518
|
+
* surface; use with a `wrapAsync`-wrapped sync graph for testing.
|
|
519
|
+
*
|
|
520
|
+
* @param graph - Async source graph
|
|
521
|
+
* @param seeds - Seed nodes for expansion
|
|
522
|
+
* @param config - Expansion and async runner configuration
|
|
523
|
+
* @returns Promise resolving to the expansion result
|
|
524
|
+
*/
|
|
525
|
+
async function edgeAsync(graph, seeds, config) {
|
|
526
|
+
return haeAsync(graph, seeds, {
|
|
527
|
+
...config,
|
|
528
|
+
typeMapper: defaultTypeMapper
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/expansion/pipe.ts
|
|
533
|
+
/**
|
|
534
|
+
* Priority function using path potential.
|
|
535
|
+
* Lower values = higher priority (expanded first).
|
|
536
|
+
*
|
|
537
|
+
* Path potential measures how many of a node's neighbours have been
|
|
538
|
+
* visited by OTHER frontiers (not the current frontier).
|
|
539
|
+
*/
|
|
540
|
+
function pipePriority(nodeId, context) {
|
|
541
|
+
const neighbours = context.graph.neighbours(nodeId);
|
|
542
|
+
let pathPotential = 0;
|
|
543
|
+
for (const neighbour of neighbours) {
|
|
544
|
+
const visitedBy = context.visitedByFrontier.get(neighbour);
|
|
545
|
+
if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
|
|
546
|
+
}
|
|
547
|
+
return context.degree / (1 + pathPotential);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Run PIPE expansion (Path-Potential Informed Priority Expansion).
|
|
551
|
+
*
|
|
552
|
+
* Discovers paths by prioritising nodes that bridge multiple frontiers,
|
|
553
|
+
* identifying connecting points between seed regions.
|
|
554
|
+
*
|
|
555
|
+
* @param graph - Source graph
|
|
556
|
+
* @param seeds - Seed nodes for expansion
|
|
557
|
+
* @param config - Expansion configuration
|
|
558
|
+
* @returns Expansion result with discovered paths
|
|
559
|
+
*/
|
|
560
|
+
function pipe(graph, seeds, config) {
|
|
561
|
+
return base(graph, seeds, {
|
|
562
|
+
...config,
|
|
563
|
+
priority: pipePriority
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Run PIPE expansion asynchronously.
|
|
568
|
+
*
|
|
569
|
+
* Note: the PIPE priority function accesses `context.graph` to retrieve
|
|
570
|
+
* neighbour lists. Full async equivalence requires PriorityContext
|
|
571
|
+
* refactoring (Phase 4b deferred). This export establishes the async API
|
|
572
|
+
* surface; use with a `wrapAsync`-wrapped sync graph for testing.
|
|
573
|
+
*
|
|
574
|
+
* @param graph - Async source graph
|
|
575
|
+
* @param seeds - Seed nodes for expansion
|
|
576
|
+
* @param config - Expansion and async runner configuration
|
|
577
|
+
* @returns Promise resolving to the expansion result
|
|
578
|
+
*/
|
|
579
|
+
async function pipeAsync(graph, seeds, config) {
|
|
580
|
+
return baseAsync(graph, seeds, {
|
|
581
|
+
...config,
|
|
582
|
+
priority: pipePriority
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/expansion/priority-helpers.ts
|
|
587
|
+
/**
|
|
588
|
+
* Compute the average mutual information between a node and all visited
|
|
589
|
+
* nodes in the same frontier.
|
|
590
|
+
*
|
|
591
|
+
* Returns a value in [0, 1] — higher means the node is more similar
|
|
592
|
+
* (on average) to already-visited same-frontier nodes.
|
|
593
|
+
*
|
|
594
|
+
* @param graph - Source graph
|
|
595
|
+
* @param nodeId - Node being prioritised
|
|
596
|
+
* @param context - Current priority context
|
|
597
|
+
* @param mi - MI function to use for pairwise scoring
|
|
598
|
+
* @returns Average MI score, or 0 if no same-frontier visited nodes exist
|
|
599
|
+
*/
|
|
600
|
+
function avgFrontierMI(graph, nodeId, context, mi) {
|
|
601
|
+
const { frontierIndex, visitedByFrontier } = context;
|
|
602
|
+
let total = 0;
|
|
603
|
+
let count = 0;
|
|
604
|
+
for (const [visitedId, idx] of visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
|
|
605
|
+
total += mi(graph, visitedId, nodeId);
|
|
606
|
+
count++;
|
|
607
|
+
}
|
|
608
|
+
return count > 0 ? total / count : 0;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Count the number of a node's neighbours that have been visited by
|
|
612
|
+
* frontiers other than the node's own frontier.
|
|
613
|
+
*
|
|
614
|
+
* A higher count indicates this node is likely to bridge two frontiers,
|
|
615
|
+
* making it a strong candidate for path completion.
|
|
616
|
+
*
|
|
617
|
+
* @param graph - Source graph
|
|
618
|
+
* @param nodeId - Node being evaluated
|
|
619
|
+
* @param context - Current priority context
|
|
620
|
+
* @returns Number of neighbours visited by other frontiers
|
|
621
|
+
*/
|
|
622
|
+
function countCrossFrontierNeighbours(graph, nodeId, context) {
|
|
623
|
+
const { frontierIndex, visitedByFrontier } = context;
|
|
624
|
+
const nodeNeighbours = new Set(graph.neighbours(nodeId));
|
|
625
|
+
let count = 0;
|
|
626
|
+
for (const [visitedId, idx] of visitedByFrontier) if (idx !== frontierIndex && nodeNeighbours.has(visitedId)) count++;
|
|
627
|
+
return count;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Incrementally update salience counts for paths discovered since the
|
|
631
|
+
* last update.
|
|
632
|
+
*
|
|
633
|
+
* Iterates only over paths from `fromIndex` onwards, avoiding redundant
|
|
634
|
+
* re-processing of already-counted paths.
|
|
635
|
+
*
|
|
636
|
+
* @param salienceCounts - Mutable map of node ID to salience count (mutated in place)
|
|
637
|
+
* @param paths - Full list of discovered paths
|
|
638
|
+
* @param fromIndex - Index to start counting from (exclusive of earlier paths)
|
|
639
|
+
* @returns The new `fromIndex` value (i.e. `paths.length` after update)
|
|
640
|
+
*/
|
|
641
|
+
function updateSalienceCounts(salienceCounts, paths, fromIndex) {
|
|
642
|
+
for (let i = fromIndex; i < paths.length; i++) {
|
|
643
|
+
const path = paths[i];
|
|
644
|
+
if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
|
|
645
|
+
}
|
|
646
|
+
return paths.length;
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/expansion/sage.ts
|
|
650
|
+
/**
|
|
651
|
+
* Run SAGE expansion algorithm.
|
|
652
|
+
*
|
|
653
|
+
* Salience-aware multi-frontier expansion with two phases:
|
|
654
|
+
* - Phase 1: Degree-based priority (early exploration)
|
|
655
|
+
* - Phase 2: Salience feedback (path-aware frontier steering)
|
|
656
|
+
*
|
|
657
|
+
* @param graph - Source graph
|
|
658
|
+
* @param seeds - Seed nodes for expansion
|
|
659
|
+
* @param config - Expansion configuration
|
|
660
|
+
* @returns Expansion result with discovered paths
|
|
661
|
+
*/
|
|
662
|
+
function sage(graph, seeds, config) {
|
|
663
|
+
const salienceCounts = /* @__PURE__ */ new Map();
|
|
664
|
+
let inPhase2 = false;
|
|
665
|
+
let lastPathCount = 0;
|
|
666
|
+
/**
|
|
667
|
+
* SAGE priority function with phase transition logic.
|
|
668
|
+
*/
|
|
669
|
+
function sagePriority(nodeId, context) {
|
|
670
|
+
const pathCount = context.discoveredPaths.length;
|
|
671
|
+
if (pathCount > 0 && !inPhase2) inPhase2 = true;
|
|
672
|
+
if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
673
|
+
if (!inPhase2) return Math.log(context.degree + 1);
|
|
674
|
+
return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
|
|
675
|
+
}
|
|
676
|
+
return base(graph, seeds, {
|
|
677
|
+
...config,
|
|
678
|
+
priority: sagePriority
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Run SAGE expansion asynchronously.
|
|
683
|
+
*
|
|
684
|
+
* Creates fresh closure state (salienceCounts, phase tracking) for this
|
|
685
|
+
* invocation. The SAGE priority function does not access `context.graph`
|
|
686
|
+
* directly, so it is safe to use in async mode via `baseAsync`.
|
|
687
|
+
*
|
|
688
|
+
* @param graph - Async source graph
|
|
689
|
+
* @param seeds - Seed nodes for expansion
|
|
690
|
+
* @param config - Expansion and async runner configuration
|
|
691
|
+
* @returns Promise resolving to the expansion result
|
|
692
|
+
*/
|
|
693
|
+
async function sageAsync(graph, seeds, config) {
|
|
694
|
+
const salienceCounts = /* @__PURE__ */ new Map();
|
|
695
|
+
let inPhase2 = false;
|
|
696
|
+
let lastPathCount = 0;
|
|
697
|
+
function sagePriority(nodeId, context) {
|
|
698
|
+
const pathCount = context.discoveredPaths.length;
|
|
699
|
+
if (pathCount > 0 && !inPhase2) inPhase2 = true;
|
|
700
|
+
if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
701
|
+
if (!inPhase2) return Math.log(context.degree + 1);
|
|
702
|
+
return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
|
|
703
|
+
}
|
|
704
|
+
return baseAsync(graph, seeds, {
|
|
705
|
+
...config,
|
|
706
|
+
priority: sagePriority
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/expansion/reach.ts
|
|
711
|
+
/**
|
|
712
|
+
* Run REACH expansion algorithm.
|
|
713
|
+
*
|
|
714
|
+
* Mutual information-aware multi-frontier expansion with two phases:
|
|
715
|
+
* - Phase 1: Degree-based priority (early exploration)
|
|
716
|
+
* - Phase 2: Structural similarity feedback (MI-guided frontier steering)
|
|
717
|
+
*
|
|
718
|
+
* @param graph - Source graph
|
|
719
|
+
* @param seeds - Seed nodes for expansion
|
|
720
|
+
* @param config - Expansion configuration
|
|
721
|
+
* @returns Expansion result with discovered paths
|
|
722
|
+
*/
|
|
723
|
+
function reach(graph, seeds, config) {
|
|
724
|
+
let inPhase2 = false;
|
|
725
|
+
const jaccardCache = /* @__PURE__ */ new Map();
|
|
726
|
+
/**
|
|
727
|
+
* Compute Jaccard similarity with caching.
|
|
728
|
+
*
|
|
729
|
+
* Exploits symmetry of Jaccard (J(A,B) = J(B,A)) to reduce
|
|
730
|
+
* duplicate computations when the same pair appears in multiple
|
|
731
|
+
* discovered paths. Key format ensures consistent ordering.
|
|
732
|
+
*/
|
|
733
|
+
function cachedJaccard(source, target) {
|
|
734
|
+
const key = source < target ? `${source}::${target}` : `${target}::${source}`;
|
|
735
|
+
let score = jaccardCache.get(key);
|
|
736
|
+
if (score === void 0) {
|
|
737
|
+
score = require_jaccard.jaccard(graph, source, target);
|
|
738
|
+
jaccardCache.set(key, score);
|
|
739
|
+
}
|
|
740
|
+
return score;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* REACH priority function with MI estimation.
|
|
744
|
+
*/
|
|
745
|
+
function reachPriority(nodeId, context) {
|
|
746
|
+
if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
|
|
747
|
+
if (!inPhase2) return Math.log(context.degree + 1);
|
|
748
|
+
let totalMI = 0;
|
|
749
|
+
let endpointCount = 0;
|
|
750
|
+
for (const path of context.discoveredPaths) {
|
|
751
|
+
const fromNodeId = path.fromSeed.id;
|
|
752
|
+
const toNodeId = path.toSeed.id;
|
|
753
|
+
totalMI += cachedJaccard(nodeId, fromNodeId);
|
|
754
|
+
totalMI += cachedJaccard(nodeId, toNodeId);
|
|
755
|
+
endpointCount += 2;
|
|
756
|
+
}
|
|
757
|
+
const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
|
|
758
|
+
return Math.log(context.degree + 1) * (1 - miHat);
|
|
759
|
+
}
|
|
760
|
+
return base(graph, seeds, {
|
|
761
|
+
...config,
|
|
762
|
+
priority: reachPriority
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Run REACH expansion asynchronously.
|
|
767
|
+
*
|
|
768
|
+
* Creates fresh closure state (phase tracking, Jaccard cache) for this
|
|
769
|
+
* invocation. The REACH priority function uses `jaccard(context.graph, ...)`
|
|
770
|
+
* in Phase 2; in async mode `context.graph` is the sentinel and will throw.
|
|
771
|
+
* Full async equivalence requires PriorityContext refactoring (Phase 4b
|
|
772
|
+
* deferred). This export establishes the async API surface.
|
|
773
|
+
*
|
|
774
|
+
* @param graph - Async source graph
|
|
775
|
+
* @param seeds - Seed nodes for expansion
|
|
776
|
+
* @param config - Expansion and async runner configuration
|
|
777
|
+
* @returns Promise resolving to the expansion result
|
|
778
|
+
*/
|
|
779
|
+
async function reachAsync(graph, seeds, config) {
|
|
780
|
+
let inPhase2 = false;
|
|
781
|
+
function reachPriority(nodeId, context) {
|
|
782
|
+
if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
|
|
783
|
+
if (!inPhase2) return Math.log(context.degree + 1);
|
|
784
|
+
let totalMI = 0;
|
|
785
|
+
let endpointCount = 0;
|
|
786
|
+
for (const path of context.discoveredPaths) {
|
|
787
|
+
totalMI += require_jaccard.jaccard(context.graph, nodeId, path.fromSeed.id);
|
|
788
|
+
totalMI += require_jaccard.jaccard(context.graph, nodeId, path.toSeed.id);
|
|
789
|
+
endpointCount += 2;
|
|
790
|
+
}
|
|
791
|
+
const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
|
|
792
|
+
return Math.log(context.degree + 1) * (1 - miHat);
|
|
793
|
+
}
|
|
794
|
+
return baseAsync(graph, seeds, {
|
|
795
|
+
...config,
|
|
796
|
+
priority: reachPriority
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region src/expansion/maze.ts
|
|
801
|
+
/** Default threshold for switching to phase 2 (after M paths) */
|
|
802
|
+
var DEFAULT_PHASE2_THRESHOLD = 1;
|
|
803
|
+
/** Salience weighting factor */
|
|
804
|
+
var SALIENCE_WEIGHT = 1e3;
|
|
805
|
+
/**
|
|
806
|
+
* Run MAZE expansion algorithm.
|
|
807
|
+
*
|
|
808
|
+
* Multi-phase expansion combining path potential and salience with
|
|
809
|
+
* adaptive frontier steering.
|
|
810
|
+
*
|
|
811
|
+
* @param graph - Source graph
|
|
812
|
+
* @param seeds - Seed nodes for expansion
|
|
813
|
+
* @param config - Expansion configuration
|
|
814
|
+
* @returns Expansion result with discovered paths
|
|
815
|
+
*/
|
|
816
|
+
function maze(graph, seeds, config) {
|
|
817
|
+
const salienceCounts = /* @__PURE__ */ new Map();
|
|
818
|
+
let inPhase2 = false;
|
|
819
|
+
let lastPathCount = 0;
|
|
820
|
+
/**
|
|
821
|
+
* MAZE priority function with path potential and salience feedback.
|
|
822
|
+
*/
|
|
823
|
+
function mazePriority(nodeId, context) {
|
|
824
|
+
const pathCount = context.discoveredPaths.length;
|
|
825
|
+
if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
|
|
826
|
+
inPhase2 = true;
|
|
827
|
+
updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
|
|
828
|
+
}
|
|
829
|
+
if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
830
|
+
const nodeNeighbours = graph.neighbours(nodeId);
|
|
831
|
+
let pathPotential = 0;
|
|
832
|
+
for (const neighbour of nodeNeighbours) {
|
|
833
|
+
const visitedBy = context.visitedByFrontier.get(neighbour);
|
|
834
|
+
if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
|
|
835
|
+
}
|
|
836
|
+
if (!inPhase2) return context.degree / (1 + pathPotential);
|
|
837
|
+
const salience = salienceCounts.get(nodeId) ?? 0;
|
|
838
|
+
return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
|
|
839
|
+
}
|
|
840
|
+
return base(graph, seeds, {
|
|
841
|
+
...config,
|
|
842
|
+
priority: mazePriority
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Run MAZE expansion asynchronously.
|
|
847
|
+
*
|
|
848
|
+
* Creates fresh closure state (salienceCounts, phase tracking) for this
|
|
849
|
+
* invocation. The MAZE priority function accesses `context.graph` to
|
|
850
|
+
* retrieve neighbour lists for path potential computation. Full async
|
|
851
|
+
* equivalence requires PriorityContext refactoring (Phase 4b deferred).
|
|
852
|
+
* This export establishes the async API surface.
|
|
853
|
+
*
|
|
854
|
+
* @param graph - Async source graph
|
|
855
|
+
* @param seeds - Seed nodes for expansion
|
|
856
|
+
* @param config - Expansion and async runner configuration
|
|
857
|
+
* @returns Promise resolving to the expansion result
|
|
858
|
+
*/
|
|
859
|
+
async function mazeAsync(graph, seeds, config) {
|
|
860
|
+
const salienceCounts = /* @__PURE__ */ new Map();
|
|
861
|
+
let inPhase2 = false;
|
|
862
|
+
let lastPathCount = 0;
|
|
863
|
+
function mazePriority(nodeId, context) {
|
|
864
|
+
const pathCount = context.discoveredPaths.length;
|
|
865
|
+
if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
|
|
866
|
+
inPhase2 = true;
|
|
867
|
+
updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
|
|
868
|
+
}
|
|
869
|
+
if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
870
|
+
const nodeNeighbours = context.graph.neighbours(nodeId);
|
|
871
|
+
let pathPotential = 0;
|
|
872
|
+
for (const neighbour of nodeNeighbours) {
|
|
873
|
+
const visitedBy = context.visitedByFrontier.get(neighbour);
|
|
874
|
+
if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
|
|
875
|
+
}
|
|
876
|
+
if (!inPhase2) return context.degree / (1 + pathPotential);
|
|
877
|
+
const salience = salienceCounts.get(nodeId) ?? 0;
|
|
878
|
+
return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
|
|
879
|
+
}
|
|
880
|
+
return baseAsync(graph, seeds, {
|
|
881
|
+
...config,
|
|
882
|
+
priority: mazePriority
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region src/expansion/tide.ts
|
|
887
|
+
/**
|
|
888
|
+
* TIDE priority function.
|
|
889
|
+
*
|
|
890
|
+
* Priority = degree(source) + degree(target)
|
|
891
|
+
* Lower values = higher priority (explored first)
|
|
892
|
+
*/
|
|
893
|
+
function tidePriority(nodeId, context) {
|
|
894
|
+
const graph = context.graph;
|
|
895
|
+
let totalDegree = context.degree;
|
|
896
|
+
for (const neighbour of graph.neighbours(nodeId)) totalDegree += graph.degree(neighbour);
|
|
897
|
+
return totalDegree;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Run TIDE expansion algorithm.
|
|
901
|
+
*
|
|
902
|
+
* Expands from seeds prioritising low-degree edges first.
|
|
903
|
+
* Useful for avoiding hubs and exploring sparse regions.
|
|
904
|
+
*
|
|
905
|
+
* @param graph - Source graph
|
|
906
|
+
* @param seeds - Seed nodes for expansion
|
|
907
|
+
* @param config - Expansion configuration
|
|
908
|
+
* @returns Expansion result with discovered paths
|
|
909
|
+
*/
|
|
910
|
+
function tide(graph, seeds, config) {
|
|
911
|
+
return base(graph, seeds, {
|
|
912
|
+
...config,
|
|
913
|
+
priority: tidePriority
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Run TIDE expansion asynchronously.
|
|
918
|
+
*
|
|
919
|
+
* Note: the TIDE priority function accesses `context.graph` to retrieve
|
|
920
|
+
* neighbour lists and per-neighbour degrees. Full async equivalence
|
|
921
|
+
* requires PriorityContext refactoring (Phase 4b deferred). This export
|
|
922
|
+
* establishes the async API surface.
|
|
923
|
+
*
|
|
924
|
+
* @param graph - Async source graph
|
|
925
|
+
* @param seeds - Seed nodes for expansion
|
|
926
|
+
* @param config - Expansion and async runner configuration
|
|
927
|
+
* @returns Promise resolving to the expansion result
|
|
928
|
+
*/
|
|
929
|
+
async function tideAsync(graph, seeds, config) {
|
|
930
|
+
return baseAsync(graph, seeds, {
|
|
931
|
+
...config,
|
|
932
|
+
priority: tidePriority
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/expansion/lace.ts
|
|
937
|
+
/**
|
|
938
|
+
* LACE priority function.
|
|
939
|
+
*
|
|
940
|
+
* Priority = 1 - avgMI(node, same-frontier visited nodes)
|
|
941
|
+
* Higher average MI = lower priority value = explored first
|
|
942
|
+
*/
|
|
943
|
+
function lacePriority(nodeId, context, mi) {
|
|
944
|
+
return 1 - avgFrontierMI(context.graph, nodeId, context, mi);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Run LACE expansion algorithm.
|
|
948
|
+
*
|
|
949
|
+
* Expands from seeds prioritising high-MI edges.
|
|
950
|
+
* Useful for finding paths with strong semantic associations.
|
|
951
|
+
*
|
|
952
|
+
* @param graph - Source graph
|
|
953
|
+
* @param seeds - Seed nodes for expansion
|
|
954
|
+
* @param config - Expansion configuration with MI function
|
|
955
|
+
* @returns Expansion result with discovered paths
|
|
956
|
+
*/
|
|
957
|
+
function lace(graph, seeds, config) {
|
|
958
|
+
const { mi = require_jaccard.jaccard, ...restConfig } = config ?? {};
|
|
959
|
+
const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
|
|
960
|
+
return base(graph, seeds, {
|
|
961
|
+
...restConfig,
|
|
962
|
+
priority
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Run LACE expansion asynchronously.
|
|
967
|
+
*
|
|
968
|
+
* Note: the LACE priority function accesses `context.graph` via
|
|
969
|
+
* `avgFrontierMI`. Full async equivalence requires PriorityContext
|
|
970
|
+
* refactoring (Phase 4b deferred). This export establishes the async
|
|
971
|
+
* API surface.
|
|
972
|
+
*
|
|
973
|
+
* @param graph - Async source graph
|
|
974
|
+
* @param seeds - Seed nodes for expansion
|
|
975
|
+
* @param config - LACE configuration combined with async runner options
|
|
976
|
+
* @returns Promise resolving to the expansion result
|
|
977
|
+
*/
|
|
978
|
+
async function laceAsync(graph, seeds, config) {
|
|
979
|
+
const { mi = require_jaccard.jaccard, ...restConfig } = config ?? {};
|
|
980
|
+
const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
|
|
981
|
+
return baseAsync(graph, seeds, {
|
|
982
|
+
...restConfig,
|
|
983
|
+
priority
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
//#endregion
|
|
987
|
+
//#region src/expansion/warp.ts
|
|
988
|
+
/**
|
|
989
|
+
* WARP priority function.
|
|
990
|
+
*
|
|
991
|
+
* Priority = 1 / (1 + bridge_score)
|
|
992
|
+
* Bridge score = cross-frontier neighbour count plus bonus for nodes
|
|
993
|
+
* already on discovered paths.
|
|
994
|
+
* Higher bridge score = more likely to complete paths = explored first.
|
|
995
|
+
*/
|
|
996
|
+
function warpPriority(nodeId, context) {
|
|
997
|
+
let bridgeScore = countCrossFrontierNeighbours(context.graph, nodeId, context);
|
|
998
|
+
for (const path of context.discoveredPaths) if (path.nodes.includes(nodeId)) bridgeScore += 2;
|
|
999
|
+
return 1 / (1 + bridgeScore);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Run WARP expansion algorithm.
|
|
1003
|
+
*
|
|
1004
|
+
* Expands from seeds prioritising bridge nodes.
|
|
1005
|
+
* Useful for finding paths through structurally important nodes.
|
|
1006
|
+
*
|
|
1007
|
+
* @param graph - Source graph
|
|
1008
|
+
* @param seeds - Seed nodes for expansion
|
|
1009
|
+
* @param config - Expansion configuration
|
|
1010
|
+
* @returns Expansion result with discovered paths
|
|
1011
|
+
*/
|
|
1012
|
+
function warp(graph, seeds, config) {
|
|
1013
|
+
return base(graph, seeds, {
|
|
1014
|
+
...config,
|
|
1015
|
+
priority: warpPriority
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Run WARP expansion asynchronously.
|
|
1020
|
+
*
|
|
1021
|
+
* Note: the WARP priority function accesses `context.graph` via
|
|
1022
|
+
* `countCrossFrontierNeighbours`. Full async equivalence requires
|
|
1023
|
+
* PriorityContext refactoring (Phase 4b deferred). This export
|
|
1024
|
+
* establishes the async API surface.
|
|
1025
|
+
*
|
|
1026
|
+
* @param graph - Async source graph
|
|
1027
|
+
* @param seeds - Seed nodes for expansion
|
|
1028
|
+
* @param config - Expansion and async runner configuration
|
|
1029
|
+
* @returns Promise resolving to the expansion result
|
|
1030
|
+
*/
|
|
1031
|
+
async function warpAsync(graph, seeds, config) {
|
|
1032
|
+
return baseAsync(graph, seeds, {
|
|
1033
|
+
...config,
|
|
1034
|
+
priority: warpPriority
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/expansion/fuse.ts
|
|
1039
|
+
/**
|
|
1040
|
+
* FUSE priority function.
|
|
1041
|
+
*
|
|
1042
|
+
* Combines degree with average frontier MI as a salience proxy:
|
|
1043
|
+
* Priority = (1 - w) * degree + w * (1 - avgMI)
|
|
1044
|
+
* Lower values = higher priority; high salience lowers priority
|
|
1045
|
+
*/
|
|
1046
|
+
function fusePriority(nodeId, context, mi, salienceWeight) {
|
|
1047
|
+
const avgSalience = avgFrontierMI(context.graph, nodeId, context, mi);
|
|
1048
|
+
return (1 - salienceWeight) * context.degree + salienceWeight * (1 - avgSalience);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Run FUSE expansion algorithm.
|
|
1052
|
+
*
|
|
1053
|
+
* Combines structural exploration with semantic salience.
|
|
1054
|
+
* Useful for finding paths that are both short and semantically meaningful.
|
|
1055
|
+
*
|
|
1056
|
+
* @param graph - Source graph
|
|
1057
|
+
* @param seeds - Seed nodes for expansion
|
|
1058
|
+
* @param config - Expansion configuration with MI function
|
|
1059
|
+
* @returns Expansion result with discovered paths
|
|
1060
|
+
*/
|
|
1061
|
+
function fuse(graph, seeds, config) {
|
|
1062
|
+
const { mi = require_jaccard.jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
|
|
1063
|
+
const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
|
|
1064
|
+
return base(graph, seeds, {
|
|
1065
|
+
...restConfig,
|
|
1066
|
+
priority
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Run FUSE expansion asynchronously.
|
|
1071
|
+
*
|
|
1072
|
+
* Note: the FUSE priority function accesses `context.graph` via
|
|
1073
|
+
* `avgFrontierMI`. Full async equivalence requires PriorityContext
|
|
1074
|
+
* refactoring (Phase 4b deferred). This export establishes the async
|
|
1075
|
+
* API surface.
|
|
1076
|
+
*
|
|
1077
|
+
* @param graph - Async source graph
|
|
1078
|
+
* @param seeds - Seed nodes for expansion
|
|
1079
|
+
* @param config - FUSE configuration combined with async runner options
|
|
1080
|
+
* @returns Promise resolving to the expansion result
|
|
1081
|
+
*/
|
|
1082
|
+
async function fuseAsync(graph, seeds, config) {
|
|
1083
|
+
const { mi = require_jaccard.jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
|
|
1084
|
+
const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
|
|
1085
|
+
return baseAsync(graph, seeds, {
|
|
1086
|
+
...restConfig,
|
|
1087
|
+
priority
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
//#endregion
|
|
1091
|
+
//#region src/expansion/sift.ts
|
|
1092
|
+
/**
|
|
1093
|
+
* REACH (SIFT) priority function.
|
|
1094
|
+
*
|
|
1095
|
+
* Prioritises nodes with average frontier MI above the threshold;
|
|
1096
|
+
* falls back to degree-based ordering for those below it.
|
|
1097
|
+
*/
|
|
1098
|
+
function siftPriority(nodeId, context, mi, miThreshold) {
|
|
1099
|
+
const avgMi = avgFrontierMI(context.graph, nodeId, context, mi);
|
|
1100
|
+
if (avgMi >= miThreshold) return 1 - avgMi;
|
|
1101
|
+
else return context.degree + 100;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Run SIFT expansion algorithm.
|
|
1105
|
+
*
|
|
1106
|
+
* Two-phase adaptive expansion that learns MI thresholds
|
|
1107
|
+
* from initial sampling, then uses them for guided expansion.
|
|
1108
|
+
*
|
|
1109
|
+
* @param graph - Source graph
|
|
1110
|
+
* @param seeds - Seed nodes for expansion
|
|
1111
|
+
* @param config - Expansion configuration
|
|
1112
|
+
* @returns Expansion result with discovered paths
|
|
1113
|
+
*/
|
|
1114
|
+
function sift(graph, seeds, config) {
|
|
1115
|
+
const { mi = require_jaccard.jaccard, miThreshold = .25, ...restConfig } = config ?? {};
|
|
1116
|
+
const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
|
|
1117
|
+
return base(graph, seeds, {
|
|
1118
|
+
...restConfig,
|
|
1119
|
+
priority
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Run SIFT expansion asynchronously.
|
|
1124
|
+
*
|
|
1125
|
+
* Note: the SIFT priority function accesses `context.graph` via
|
|
1126
|
+
* `avgFrontierMI`. Full async equivalence requires PriorityContext
|
|
1127
|
+
* refactoring (Phase 4b deferred). This export establishes the async
|
|
1128
|
+
* API surface.
|
|
1129
|
+
*
|
|
1130
|
+
* @param graph - Async source graph
|
|
1131
|
+
* @param seeds - Seed nodes for expansion
|
|
1132
|
+
* @param config - SIFT (REACHConfig) configuration combined with async runner options
|
|
1133
|
+
* @returns Promise resolving to the expansion result
|
|
1134
|
+
*/
|
|
1135
|
+
async function siftAsync(graph, seeds, config) {
|
|
1136
|
+
const { mi = require_jaccard.jaccard, miThreshold = .25, ...restConfig } = config ?? {};
|
|
1137
|
+
const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
|
|
1138
|
+
return baseAsync(graph, seeds, {
|
|
1139
|
+
...restConfig,
|
|
1140
|
+
priority
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/expansion/flux.ts
|
|
1145
|
+
/**
|
|
1146
|
+
* Compute local density around a node.
|
|
1147
|
+
*/
|
|
1148
|
+
function localDensity(graph, nodeId) {
|
|
1149
|
+
const neighbours = Array.from(graph.neighbours(nodeId));
|
|
1150
|
+
const degree = neighbours.length;
|
|
1151
|
+
if (degree < 2) return 0;
|
|
1152
|
+
let edges = 0;
|
|
1153
|
+
for (let i = 0; i < neighbours.length; i++) for (let j = i + 1; j < neighbours.length; j++) {
|
|
1154
|
+
const ni = neighbours[i];
|
|
1155
|
+
const nj = neighbours[j];
|
|
1156
|
+
if (ni !== void 0 && nj !== void 0 && graph.getEdge(ni, nj) !== void 0) edges++;
|
|
1157
|
+
}
|
|
1158
|
+
const maxEdges = degree * (degree - 1) / 2;
|
|
1159
|
+
return edges / maxEdges;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* MAZE adaptive priority function.
|
|
1163
|
+
*
|
|
1164
|
+
* Switches strategies based on local conditions:
|
|
1165
|
+
* - High density + low bridge: EDGE mode
|
|
1166
|
+
* - Low density + low bridge: DOME mode
|
|
1167
|
+
* - High bridge score: PIPE mode
|
|
1168
|
+
*/
|
|
1169
|
+
function fluxPriority(nodeId, context, densityThreshold, bridgeThreshold) {
|
|
1170
|
+
const graph = context.graph;
|
|
1171
|
+
const degree = context.degree;
|
|
1172
|
+
const density = localDensity(graph, nodeId);
|
|
1173
|
+
const bridge = countCrossFrontierNeighbours(graph, nodeId, context);
|
|
1174
|
+
const numFrontiers = new Set(context.visitedByFrontier.values()).size;
|
|
1175
|
+
if ((numFrontiers > 0 ? bridge / numFrontiers : 0) >= bridgeThreshold) return 1 / (1 + bridge);
|
|
1176
|
+
else if (density >= densityThreshold) return 1 / (degree + 1);
|
|
1177
|
+
else return degree;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Run FLUX expansion algorithm.
|
|
1181
|
+
*
|
|
1182
|
+
* Adaptively switches between expansion strategies based on
|
|
1183
|
+
* local graph structure. Useful for heterogeneous graphs
|
|
1184
|
+
* with varying density.
|
|
1185
|
+
*
|
|
1186
|
+
* @param graph - Source graph
|
|
1187
|
+
* @param seeds - Seed nodes for expansion
|
|
1188
|
+
* @param config - Expansion configuration
|
|
1189
|
+
* @returns Expansion result with discovered paths
|
|
1190
|
+
*/
|
|
1191
|
+
function flux(graph, seeds, config) {
|
|
1192
|
+
const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
|
|
1193
|
+
const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
|
|
1194
|
+
return base(graph, seeds, {
|
|
1195
|
+
...restConfig,
|
|
1196
|
+
priority
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Run FLUX expansion asynchronously.
|
|
1201
|
+
*
|
|
1202
|
+
* Note: the FLUX priority function accesses `context.graph` to compute
|
|
1203
|
+
* local density and cross-frontier bridge scores. Full async equivalence
|
|
1204
|
+
* requires PriorityContext refactoring (Phase 4b deferred). This export
|
|
1205
|
+
* establishes the async API surface.
|
|
1206
|
+
*
|
|
1207
|
+
* @param graph - Async source graph
|
|
1208
|
+
* @param seeds - Seed nodes for expansion
|
|
1209
|
+
* @param config - FLUX (MAZEConfig) configuration combined with async runner options
|
|
1210
|
+
* @returns Promise resolving to the expansion result
|
|
1211
|
+
*/
|
|
1212
|
+
async function fluxAsync(graph, seeds, config) {
|
|
1213
|
+
const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
|
|
1214
|
+
const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
|
|
1215
|
+
return baseAsync(graph, seeds, {
|
|
1216
|
+
...restConfig,
|
|
1217
|
+
priority
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/expansion/standard-bfs.ts
|
|
1222
|
+
/**
|
|
1223
|
+
* BFS priority: discovery iteration order (FIFO).
|
|
1224
|
+
*/
|
|
1225
|
+
function bfsPriority(_nodeId, context) {
|
|
1226
|
+
return context.iteration;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Run standard BFS expansion (FIFO discovery order).
|
|
1230
|
+
*
|
|
1231
|
+
* @param graph - Source graph
|
|
1232
|
+
* @param seeds - Seed nodes for expansion
|
|
1233
|
+
* @param config - Expansion configuration
|
|
1234
|
+
* @returns Expansion result with discovered paths
|
|
1235
|
+
*/
|
|
1236
|
+
function standardBfs(graph, seeds, config) {
|
|
1237
|
+
return base(graph, seeds, {
|
|
1238
|
+
...config,
|
|
1239
|
+
priority: bfsPriority
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Run standard BFS expansion asynchronously (FIFO discovery order).
|
|
1244
|
+
*
|
|
1245
|
+
* @param graph - Async source graph
|
|
1246
|
+
* @param seeds - Seed nodes for expansion
|
|
1247
|
+
* @param config - Expansion and async runner configuration
|
|
1248
|
+
* @returns Promise resolving to the expansion result
|
|
1249
|
+
*/
|
|
1250
|
+
async function standardBfsAsync(graph, seeds, config) {
|
|
1251
|
+
return baseAsync(graph, seeds, {
|
|
1252
|
+
...config,
|
|
1253
|
+
priority: bfsPriority
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/expansion/frontier-balanced.ts
|
|
1258
|
+
/**
|
|
1259
|
+
* Frontier-balanced priority: frontier index dominates, then discovery iteration.
|
|
1260
|
+
* Scales frontier index by 1e9 to ensure round-robin ordering across frontiers.
|
|
1261
|
+
*/
|
|
1262
|
+
function balancedPriority(_nodeId, context) {
|
|
1263
|
+
return context.frontierIndex * 1e9 + context.iteration;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Run frontier-balanced expansion (round-robin across frontiers).
|
|
1267
|
+
*
|
|
1268
|
+
* @param graph - Source graph
|
|
1269
|
+
* @param seeds - Seed nodes for expansion
|
|
1270
|
+
* @param config - Expansion configuration
|
|
1271
|
+
* @returns Expansion result with discovered paths
|
|
1272
|
+
*/
|
|
1273
|
+
function frontierBalanced(graph, seeds, config) {
|
|
1274
|
+
return base(graph, seeds, {
|
|
1275
|
+
...config,
|
|
1276
|
+
priority: balancedPriority
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Run frontier-balanced expansion asynchronously (round-robin across frontiers).
|
|
1281
|
+
*
|
|
1282
|
+
* @param graph - Async source graph
|
|
1283
|
+
* @param seeds - Seed nodes for expansion
|
|
1284
|
+
* @param config - Expansion and async runner configuration
|
|
1285
|
+
* @returns Promise resolving to the expansion result
|
|
1286
|
+
*/
|
|
1287
|
+
async function frontierBalancedAsync(graph, seeds, config) {
|
|
1288
|
+
return baseAsync(graph, seeds, {
|
|
1289
|
+
...config,
|
|
1290
|
+
priority: balancedPriority
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region src/expansion/random-priority.ts
|
|
1295
|
+
/**
|
|
1296
|
+
* Deterministic seeded random number generator.
|
|
1297
|
+
* Uses FNV-1a-like hash for input → [0, 1] output.
|
|
1298
|
+
*
|
|
1299
|
+
* @param input - String to hash
|
|
1300
|
+
* @param seed - Random seed for reproducibility
|
|
1301
|
+
* @returns Deterministic random value in [0, 1]
|
|
1302
|
+
*/
|
|
1303
|
+
function seededRandom(input, seed = 0) {
|
|
1304
|
+
let h = seed;
|
|
1305
|
+
for (let i = 0; i < input.length; i++) {
|
|
1306
|
+
h = Math.imul(h ^ input.charCodeAt(i), 2654435769);
|
|
1307
|
+
h ^= h >>> 16;
|
|
1308
|
+
}
|
|
1309
|
+
return (h >>> 0) / 4294967295;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Build a seeded random priority function for a given seed value.
|
|
1313
|
+
*/
|
|
1314
|
+
function makeRandomPriorityFn(seed) {
|
|
1315
|
+
return (nodeId) => seededRandom(nodeId, seed);
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Run random-priority expansion (null hypothesis baseline).
|
|
1319
|
+
*
|
|
1320
|
+
* @param graph - Source graph
|
|
1321
|
+
* @param seeds - Seed nodes for expansion
|
|
1322
|
+
* @param config - Expansion configuration
|
|
1323
|
+
* @returns Expansion result with discovered paths
|
|
1324
|
+
*/
|
|
1325
|
+
function randomPriority(graph, seeds, config) {
|
|
1326
|
+
const { seed = 0 } = config ?? {};
|
|
1327
|
+
return base(graph, seeds, {
|
|
1328
|
+
...config,
|
|
1329
|
+
priority: makeRandomPriorityFn(seed)
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Run random-priority expansion asynchronously (null hypothesis baseline).
|
|
1334
|
+
*
|
|
1335
|
+
* @param graph - Async source graph
|
|
1336
|
+
* @param seeds - Seed nodes for expansion
|
|
1337
|
+
* @param config - Expansion and async runner configuration
|
|
1338
|
+
* @returns Promise resolving to the expansion result
|
|
1339
|
+
*/
|
|
1340
|
+
async function randomPriorityAsync(graph, seeds, config) {
|
|
1341
|
+
const { seed = 0 } = config ?? {};
|
|
1342
|
+
return baseAsync(graph, seeds, {
|
|
1343
|
+
...config,
|
|
1344
|
+
priority: makeRandomPriorityFn(seed)
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
//#endregion
|
|
1348
|
+
//#region src/expansion/dfs-priority.ts
|
|
1349
|
+
/**
|
|
1350
|
+
* DFS priority function: negative iteration produces LIFO ordering.
|
|
1351
|
+
*
|
|
1352
|
+
* Lower priority values are expanded first, so negating the iteration
|
|
1353
|
+
* counter ensures the most recently enqueued node is always next.
|
|
1354
|
+
*/
|
|
1355
|
+
function dfsPriorityFn(_nodeId, context) {
|
|
1356
|
+
return -context.iteration;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Run DFS-priority expansion (LIFO discovery order).
|
|
1360
|
+
*
|
|
1361
|
+
* Uses the BASE framework with a negative-iteration priority function,
|
|
1362
|
+
* which causes the most recently discovered node to be expanded first —
|
|
1363
|
+
* equivalent to depth-first search behaviour.
|
|
1364
|
+
*
|
|
1365
|
+
* @param graph - Source graph
|
|
1366
|
+
* @param seeds - Seed nodes for expansion
|
|
1367
|
+
* @param config - Expansion configuration
|
|
1368
|
+
* @returns Expansion result with discovered paths
|
|
1369
|
+
*/
|
|
1370
|
+
function dfsPriority(graph, seeds, config) {
|
|
1371
|
+
return base(graph, seeds, {
|
|
1372
|
+
...config,
|
|
1373
|
+
priority: dfsPriorityFn
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Run DFS-priority expansion asynchronously (LIFO discovery order).
|
|
1378
|
+
*
|
|
1379
|
+
* @param graph - Async source graph
|
|
1380
|
+
* @param seeds - Seed nodes for expansion
|
|
1381
|
+
* @param config - Expansion and async runner configuration
|
|
1382
|
+
* @returns Promise resolving to the expansion result
|
|
1383
|
+
*/
|
|
1384
|
+
async function dfsPriorityAsync(graph, seeds, config) {
|
|
1385
|
+
return baseAsync(graph, seeds, {
|
|
1386
|
+
...config,
|
|
1387
|
+
priority: dfsPriorityFn
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
//#endregion
|
|
1391
|
+
//#region src/expansion/k-hop.ts
|
|
1392
|
+
/**
|
|
1393
|
+
* Run k-hop expansion (fixed-depth BFS).
|
|
1394
|
+
*
|
|
1395
|
+
* Explores all nodes reachable within exactly k hops of any seed using
|
|
1396
|
+
* breadth-first search. Paths between seeds are detected when a node
|
|
1397
|
+
* is reached by frontiers from two different seeds.
|
|
1398
|
+
*
|
|
1399
|
+
* @param graph - Source graph
|
|
1400
|
+
* @param seeds - Seed nodes for expansion
|
|
1401
|
+
* @param config - K-hop configuration (k defaults to 2)
|
|
1402
|
+
* @returns Expansion result with discovered paths
|
|
1403
|
+
*/
|
|
1404
|
+
function kHop(graph, seeds, config) {
|
|
1405
|
+
const startTime = performance.now();
|
|
1406
|
+
const { k = 2 } = config ?? {};
|
|
1407
|
+
if (seeds.length === 0) return emptyResult$1(startTime);
|
|
1408
|
+
const visitedByFrontier = seeds.map(() => /* @__PURE__ */ new Map());
|
|
1409
|
+
const firstVisitedBy = /* @__PURE__ */ new Map();
|
|
1410
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
1411
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
1412
|
+
const discoveredPaths = [];
|
|
1413
|
+
let iterations = 0;
|
|
1414
|
+
let edgesTraversed = 0;
|
|
1415
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
1416
|
+
const seed = seeds[i];
|
|
1417
|
+
if (seed === void 0) continue;
|
|
1418
|
+
if (!graph.hasNode(seed.id)) continue;
|
|
1419
|
+
visitedByFrontier[i]?.set(seed.id, null);
|
|
1420
|
+
allVisited.add(seed.id);
|
|
1421
|
+
if (!firstVisitedBy.has(seed.id)) firstVisitedBy.set(seed.id, i);
|
|
1422
|
+
else {
|
|
1423
|
+
const otherIdx = firstVisitedBy.get(seed.id) ?? -1;
|
|
1424
|
+
if (otherIdx < 0) continue;
|
|
1425
|
+
const fromSeed = seeds[otherIdx];
|
|
1426
|
+
const toSeed = seeds[i];
|
|
1427
|
+
if (fromSeed !== void 0 && toSeed !== void 0) discoveredPaths.push({
|
|
1428
|
+
fromSeed,
|
|
1429
|
+
toSeed,
|
|
1430
|
+
nodes: [seed.id]
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
let currentLevel = seeds.map((s, i) => {
|
|
1435
|
+
const frontier = visitedByFrontier[i];
|
|
1436
|
+
if (frontier === void 0) return [];
|
|
1437
|
+
return frontier.has(s.id) ? [s.id] : [];
|
|
1438
|
+
});
|
|
1439
|
+
for (let hop = 0; hop < k; hop++) {
|
|
1440
|
+
const nextLevel = seeds.map(() => []);
|
|
1441
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
1442
|
+
const level = currentLevel[i];
|
|
1443
|
+
if (level === void 0) continue;
|
|
1444
|
+
const frontierVisited = visitedByFrontier[i];
|
|
1445
|
+
if (frontierVisited === void 0) continue;
|
|
1446
|
+
for (const nodeId of level) {
|
|
1447
|
+
iterations++;
|
|
1448
|
+
for (const neighbour of graph.neighbours(nodeId)) {
|
|
1449
|
+
edgesTraversed++;
|
|
1450
|
+
const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
|
|
1451
|
+
let targets = sampledEdgeMap.get(s);
|
|
1452
|
+
if (targets === void 0) {
|
|
1453
|
+
targets = /* @__PURE__ */ new Set();
|
|
1454
|
+
sampledEdgeMap.set(s, targets);
|
|
1455
|
+
}
|
|
1456
|
+
targets.add(t);
|
|
1457
|
+
if (frontierVisited.has(neighbour)) continue;
|
|
1458
|
+
frontierVisited.set(neighbour, nodeId);
|
|
1459
|
+
allVisited.add(neighbour);
|
|
1460
|
+
nextLevel[i]?.push(neighbour);
|
|
1461
|
+
const previousFrontier = firstVisitedBy.get(neighbour);
|
|
1462
|
+
if (previousFrontier !== void 0 && previousFrontier !== i) {
|
|
1463
|
+
const fromSeed = seeds[previousFrontier];
|
|
1464
|
+
const toSeed = seeds[i];
|
|
1465
|
+
if (fromSeed !== void 0 && toSeed !== void 0) {
|
|
1466
|
+
const path = reconstructPath(neighbour, previousFrontier, i, visitedByFrontier, seeds);
|
|
1467
|
+
if (path !== null) {
|
|
1468
|
+
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);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (!firstVisitedBy.has(neighbour)) firstVisitedBy.set(neighbour, i);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
currentLevel = nextLevel;
|
|
1477
|
+
if (currentLevel.every((level) => level.length === 0)) break;
|
|
1478
|
+
}
|
|
1479
|
+
const endTime = performance.now();
|
|
1480
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
1481
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
1482
|
+
return {
|
|
1483
|
+
paths: discoveredPaths,
|
|
1484
|
+
sampledNodes: allVisited,
|
|
1485
|
+
sampledEdges: edgeTuples,
|
|
1486
|
+
visitedPerFrontier: visitedByFrontier.map((m) => new Set(m.keys())),
|
|
1487
|
+
stats: {
|
|
1488
|
+
iterations,
|
|
1489
|
+
nodesVisited: allVisited.size,
|
|
1490
|
+
edgesTraversed,
|
|
1491
|
+
pathsFound: discoveredPaths.length,
|
|
1492
|
+
durationMs: endTime - startTime,
|
|
1493
|
+
algorithm: "k-hop",
|
|
1494
|
+
termination: "exhausted"
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Reconstruct the path between two colliding frontiers.
|
|
1500
|
+
*/
|
|
1501
|
+
function reconstructPath(collisionNode, frontierA, frontierB, visitedByFrontier, seeds) {
|
|
1502
|
+
const seedA = seeds[frontierA];
|
|
1503
|
+
const seedB = seeds[frontierB];
|
|
1504
|
+
if (seedA === void 0 || seedB === void 0) return null;
|
|
1505
|
+
const pathA = [collisionNode];
|
|
1506
|
+
const predA = visitedByFrontier[frontierA];
|
|
1507
|
+
if (predA !== void 0) {
|
|
1508
|
+
let node = collisionNode;
|
|
1509
|
+
let pred = predA.get(node);
|
|
1510
|
+
while (pred !== null && pred !== void 0) {
|
|
1511
|
+
pathA.unshift(pred);
|
|
1512
|
+
node = pred;
|
|
1513
|
+
pred = predA.get(node);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const pathB = [];
|
|
1517
|
+
const predB = visitedByFrontier[frontierB];
|
|
1518
|
+
if (predB !== void 0) {
|
|
1519
|
+
let node = collisionNode;
|
|
1520
|
+
let pred = predB.get(node);
|
|
1521
|
+
while (pred !== null && pred !== void 0) {
|
|
1522
|
+
pathB.push(pred);
|
|
1523
|
+
node = pred;
|
|
1524
|
+
pred = predB.get(node);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
fromSeed: seedA,
|
|
1529
|
+
toSeed: seedB,
|
|
1530
|
+
nodes: [...pathA, ...pathB]
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Create an empty result for early termination (no seeds).
|
|
1535
|
+
*/
|
|
1536
|
+
function emptyResult$1(startTime) {
|
|
1537
|
+
return {
|
|
1538
|
+
paths: [],
|
|
1539
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
1540
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
1541
|
+
visitedPerFrontier: [],
|
|
1542
|
+
stats: {
|
|
1543
|
+
iterations: 0,
|
|
1544
|
+
nodesVisited: 0,
|
|
1545
|
+
edgesTraversed: 0,
|
|
1546
|
+
pathsFound: 0,
|
|
1547
|
+
durationMs: performance.now() - startTime,
|
|
1548
|
+
algorithm: "k-hop",
|
|
1549
|
+
termination: "exhausted"
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
//#endregion
|
|
1554
|
+
//#region src/expansion/random-walk.ts
|
|
1555
|
+
/**
|
|
1556
|
+
* Mulberry32 seeded PRNG — fast, compact, and high-quality for simulation.
|
|
1557
|
+
*
|
|
1558
|
+
* Returns a closure that yields the next pseudo-random value in [0, 1)
|
|
1559
|
+
* on each call.
|
|
1560
|
+
*
|
|
1561
|
+
* @param seed - 32-bit integer seed
|
|
1562
|
+
*/
|
|
1563
|
+
function mulberry32(seed) {
|
|
1564
|
+
let s = seed;
|
|
1565
|
+
return () => {
|
|
1566
|
+
s += 1831565813;
|
|
1567
|
+
let t = s;
|
|
1568
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1569
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1570
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Run random-walk-with-restart expansion.
|
|
1575
|
+
*
|
|
1576
|
+
* For each seed, performs `walks` independent random walks of up to
|
|
1577
|
+
* `walkLength` steps. At each step the walk either restarts (with
|
|
1578
|
+
* probability `restartProbability`) or moves to a uniformly sampled
|
|
1579
|
+
* neighbour. All visited nodes and traversed edges are collected.
|
|
1580
|
+
*
|
|
1581
|
+
* Inter-seed paths are detected when a walk reaches a node that was
|
|
1582
|
+
* previously reached by a walk originating from a different seed.
|
|
1583
|
+
* The recorded path contains only the two seed endpoints rather than
|
|
1584
|
+
* the full walk trajectory, consistent with the ExpansionPath contract.
|
|
1585
|
+
*
|
|
1586
|
+
* @param graph - Source graph
|
|
1587
|
+
* @param seeds - Seed nodes for expansion
|
|
1588
|
+
* @param config - Random walk configuration
|
|
1589
|
+
* @returns Expansion result with discovered paths
|
|
1590
|
+
*/
|
|
1591
|
+
function randomWalk(graph, seeds, config) {
|
|
1592
|
+
const startTime = performance.now();
|
|
1593
|
+
const { restartProbability = .15, walks = 10, walkLength = 20, seed = 0 } = config ?? {};
|
|
1594
|
+
if (seeds.length === 0) return emptyResult(startTime);
|
|
1595
|
+
const rand = mulberry32(seed);
|
|
1596
|
+
const firstVisitedBySeed = /* @__PURE__ */ new Map();
|
|
1597
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
1598
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
1599
|
+
const discoveredPaths = [];
|
|
1600
|
+
let iterations = 0;
|
|
1601
|
+
let edgesTraversed = 0;
|
|
1602
|
+
const visitedPerFrontier = seeds.map(() => /* @__PURE__ */ new Set());
|
|
1603
|
+
for (let seedIdx = 0; seedIdx < seeds.length; seedIdx++) {
|
|
1604
|
+
const seed_ = seeds[seedIdx];
|
|
1605
|
+
if (seed_ === void 0) continue;
|
|
1606
|
+
const seedId = seed_.id;
|
|
1607
|
+
if (!graph.hasNode(seedId)) continue;
|
|
1608
|
+
if (!firstVisitedBySeed.has(seedId)) firstVisitedBySeed.set(seedId, seedIdx);
|
|
1609
|
+
allVisited.add(seedId);
|
|
1610
|
+
visitedPerFrontier[seedIdx]?.add(seedId);
|
|
1611
|
+
for (let w = 0; w < walks; w++) {
|
|
1612
|
+
let current = seedId;
|
|
1613
|
+
for (let step = 0; step < walkLength; step++) {
|
|
1614
|
+
iterations++;
|
|
1615
|
+
if (rand() < restartProbability) {
|
|
1616
|
+
current = seedId;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
const neighbourList = [];
|
|
1620
|
+
for (const nb of graph.neighbours(current)) neighbourList.push(nb);
|
|
1621
|
+
if (neighbourList.length === 0) {
|
|
1622
|
+
current = seedId;
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const next = neighbourList[Math.floor(rand() * neighbourList.length)];
|
|
1626
|
+
if (next === void 0) {
|
|
1627
|
+
current = seedId;
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
edgesTraversed++;
|
|
1631
|
+
const [s, t] = current < next ? [current, next] : [next, current];
|
|
1632
|
+
let targets = sampledEdgeMap.get(s);
|
|
1633
|
+
if (targets === void 0) {
|
|
1634
|
+
targets = /* @__PURE__ */ new Set();
|
|
1635
|
+
sampledEdgeMap.set(s, targets);
|
|
1636
|
+
}
|
|
1637
|
+
targets.add(t);
|
|
1638
|
+
const previousSeedIdx = firstVisitedBySeed.get(next);
|
|
1639
|
+
if (previousSeedIdx !== void 0 && previousSeedIdx !== seedIdx) {
|
|
1640
|
+
const fromSeed = seeds[previousSeedIdx];
|
|
1641
|
+
const toSeed = seeds[seedIdx];
|
|
1642
|
+
if (fromSeed !== void 0 && toSeed !== void 0) {
|
|
1643
|
+
const path = {
|
|
1644
|
+
fromSeed,
|
|
1645
|
+
toSeed,
|
|
1646
|
+
nodes: [
|
|
1647
|
+
fromSeed.id,
|
|
1648
|
+
next,
|
|
1649
|
+
toSeed.id
|
|
1650
|
+
].filter((n, i, arr) => arr.indexOf(n) === i)
|
|
1651
|
+
};
|
|
1652
|
+
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);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (!firstVisitedBySeed.has(next)) firstVisitedBySeed.set(next, seedIdx);
|
|
1656
|
+
allVisited.add(next);
|
|
1657
|
+
visitedPerFrontier[seedIdx]?.add(next);
|
|
1658
|
+
current = next;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
const endTime = performance.now();
|
|
1663
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
1664
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
1665
|
+
return {
|
|
1666
|
+
paths: discoveredPaths,
|
|
1667
|
+
sampledNodes: allVisited,
|
|
1668
|
+
sampledEdges: edgeTuples,
|
|
1669
|
+
visitedPerFrontier,
|
|
1670
|
+
stats: {
|
|
1671
|
+
iterations,
|
|
1672
|
+
nodesVisited: allVisited.size,
|
|
1673
|
+
edgesTraversed,
|
|
1674
|
+
pathsFound: discoveredPaths.length,
|
|
1675
|
+
durationMs: endTime - startTime,
|
|
1676
|
+
algorithm: "random-walk",
|
|
1677
|
+
termination: "exhausted"
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Create an empty result for early termination (no seeds).
|
|
1683
|
+
*/
|
|
1684
|
+
function emptyResult(startTime) {
|
|
1685
|
+
return {
|
|
1686
|
+
paths: [],
|
|
1687
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
1688
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
1689
|
+
visitedPerFrontier: [],
|
|
1690
|
+
stats: {
|
|
1691
|
+
iterations: 0,
|
|
1692
|
+
nodesVisited: 0,
|
|
1693
|
+
edgesTraversed: 0,
|
|
1694
|
+
pathsFound: 0,
|
|
1695
|
+
durationMs: performance.now() - startTime,
|
|
1696
|
+
algorithm: "random-walk",
|
|
1697
|
+
termination: "exhausted"
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
//#endregion
|
|
1702
|
+
Object.defineProperty(exports, "base", {
|
|
1703
|
+
enumerable: true,
|
|
1704
|
+
get: function() {
|
|
1705
|
+
return base;
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
Object.defineProperty(exports, "baseAsync", {
|
|
1709
|
+
enumerable: true,
|
|
1710
|
+
get: function() {
|
|
1711
|
+
return baseAsync;
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
Object.defineProperty(exports, "dfsPriority", {
|
|
1715
|
+
enumerable: true,
|
|
1716
|
+
get: function() {
|
|
1717
|
+
return dfsPriority;
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
Object.defineProperty(exports, "dfsPriorityAsync", {
|
|
1721
|
+
enumerable: true,
|
|
1722
|
+
get: function() {
|
|
1723
|
+
return dfsPriorityAsync;
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
Object.defineProperty(exports, "dfsPriorityFn", {
|
|
1727
|
+
enumerable: true,
|
|
1728
|
+
get: function() {
|
|
1729
|
+
return dfsPriorityFn;
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
Object.defineProperty(exports, "dome", {
|
|
1733
|
+
enumerable: true,
|
|
1734
|
+
get: function() {
|
|
1735
|
+
return dome;
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
Object.defineProperty(exports, "domeAsync", {
|
|
1739
|
+
enumerable: true,
|
|
1740
|
+
get: function() {
|
|
1741
|
+
return domeAsync;
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
Object.defineProperty(exports, "domeHighDegree", {
|
|
1745
|
+
enumerable: true,
|
|
1746
|
+
get: function() {
|
|
1747
|
+
return domeHighDegree;
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
Object.defineProperty(exports, "domeHighDegreeAsync", {
|
|
1751
|
+
enumerable: true,
|
|
1752
|
+
get: function() {
|
|
1753
|
+
return domeHighDegreeAsync;
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
Object.defineProperty(exports, "edge", {
|
|
1757
|
+
enumerable: true,
|
|
1758
|
+
get: function() {
|
|
1759
|
+
return edge;
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
Object.defineProperty(exports, "edgeAsync", {
|
|
1763
|
+
enumerable: true,
|
|
1764
|
+
get: function() {
|
|
1765
|
+
return edgeAsync;
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
Object.defineProperty(exports, "flux", {
|
|
1769
|
+
enumerable: true,
|
|
1770
|
+
get: function() {
|
|
1771
|
+
return flux;
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
Object.defineProperty(exports, "fluxAsync", {
|
|
1775
|
+
enumerable: true,
|
|
1776
|
+
get: function() {
|
|
1777
|
+
return fluxAsync;
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
Object.defineProperty(exports, "frontierBalanced", {
|
|
1781
|
+
enumerable: true,
|
|
1782
|
+
get: function() {
|
|
1783
|
+
return frontierBalanced;
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
Object.defineProperty(exports, "frontierBalancedAsync", {
|
|
1787
|
+
enumerable: true,
|
|
1788
|
+
get: function() {
|
|
1789
|
+
return frontierBalancedAsync;
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
Object.defineProperty(exports, "fuse", {
|
|
1793
|
+
enumerable: true,
|
|
1794
|
+
get: function() {
|
|
1795
|
+
return fuse;
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
Object.defineProperty(exports, "fuseAsync", {
|
|
1799
|
+
enumerable: true,
|
|
1800
|
+
get: function() {
|
|
1801
|
+
return fuseAsync;
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
Object.defineProperty(exports, "hae", {
|
|
1805
|
+
enumerable: true,
|
|
1806
|
+
get: function() {
|
|
1807
|
+
return hae;
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
Object.defineProperty(exports, "haeAsync", {
|
|
1811
|
+
enumerable: true,
|
|
1812
|
+
get: function() {
|
|
1813
|
+
return haeAsync;
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
Object.defineProperty(exports, "kHop", {
|
|
1817
|
+
enumerable: true,
|
|
1818
|
+
get: function() {
|
|
1819
|
+
return kHop;
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
Object.defineProperty(exports, "lace", {
|
|
1823
|
+
enumerable: true,
|
|
1824
|
+
get: function() {
|
|
1825
|
+
return lace;
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
Object.defineProperty(exports, "laceAsync", {
|
|
1829
|
+
enumerable: true,
|
|
1830
|
+
get: function() {
|
|
1831
|
+
return laceAsync;
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
Object.defineProperty(exports, "maze", {
|
|
1835
|
+
enumerable: true,
|
|
1836
|
+
get: function() {
|
|
1837
|
+
return maze;
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
Object.defineProperty(exports, "mazeAsync", {
|
|
1841
|
+
enumerable: true,
|
|
1842
|
+
get: function() {
|
|
1843
|
+
return mazeAsync;
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
Object.defineProperty(exports, "pipe", {
|
|
1847
|
+
enumerable: true,
|
|
1848
|
+
get: function() {
|
|
1849
|
+
return pipe;
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
Object.defineProperty(exports, "pipeAsync", {
|
|
1853
|
+
enumerable: true,
|
|
1854
|
+
get: function() {
|
|
1855
|
+
return pipeAsync;
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
Object.defineProperty(exports, "randomPriority", {
|
|
1859
|
+
enumerable: true,
|
|
1860
|
+
get: function() {
|
|
1861
|
+
return randomPriority;
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
Object.defineProperty(exports, "randomPriorityAsync", {
|
|
1865
|
+
enumerable: true,
|
|
1866
|
+
get: function() {
|
|
1867
|
+
return randomPriorityAsync;
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
Object.defineProperty(exports, "randomWalk", {
|
|
1871
|
+
enumerable: true,
|
|
1872
|
+
get: function() {
|
|
1873
|
+
return randomWalk;
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
Object.defineProperty(exports, "reach", {
|
|
1877
|
+
enumerable: true,
|
|
1878
|
+
get: function() {
|
|
1879
|
+
return reach;
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
Object.defineProperty(exports, "reachAsync", {
|
|
1883
|
+
enumerable: true,
|
|
1884
|
+
get: function() {
|
|
1885
|
+
return reachAsync;
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
Object.defineProperty(exports, "sage", {
|
|
1889
|
+
enumerable: true,
|
|
1890
|
+
get: function() {
|
|
1891
|
+
return sage;
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
Object.defineProperty(exports, "sageAsync", {
|
|
1895
|
+
enumerable: true,
|
|
1896
|
+
get: function() {
|
|
1897
|
+
return sageAsync;
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
Object.defineProperty(exports, "sift", {
|
|
1901
|
+
enumerable: true,
|
|
1902
|
+
get: function() {
|
|
1903
|
+
return sift;
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
Object.defineProperty(exports, "siftAsync", {
|
|
1907
|
+
enumerable: true,
|
|
1908
|
+
get: function() {
|
|
1909
|
+
return siftAsync;
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
Object.defineProperty(exports, "standardBfs", {
|
|
1913
|
+
enumerable: true,
|
|
1914
|
+
get: function() {
|
|
1915
|
+
return standardBfs;
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
Object.defineProperty(exports, "standardBfsAsync", {
|
|
1919
|
+
enumerable: true,
|
|
1920
|
+
get: function() {
|
|
1921
|
+
return standardBfsAsync;
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
Object.defineProperty(exports, "tide", {
|
|
1925
|
+
enumerable: true,
|
|
1926
|
+
get: function() {
|
|
1927
|
+
return tide;
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
Object.defineProperty(exports, "tideAsync", {
|
|
1931
|
+
enumerable: true,
|
|
1932
|
+
get: function() {
|
|
1933
|
+
return tideAsync;
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
Object.defineProperty(exports, "warp", {
|
|
1937
|
+
enumerable: true,
|
|
1938
|
+
get: function() {
|
|
1939
|
+
return warp;
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
Object.defineProperty(exports, "warpAsync", {
|
|
1943
|
+
enumerable: true,
|
|
1944
|
+
get: function() {
|
|
1945
|
+
return warpAsync;
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
//# sourceMappingURL=expansion-FkmEYlrQ.cjs.map
|