graphwise 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -30
- package/dist/async/index.cjs +243 -0
- package/dist/async/index.cjs.map +1 -0
- package/dist/async/index.js +230 -0
- package/dist/async/index.js.map +1 -0
- package/dist/expansion/base-core.d.ts +24 -0
- package/dist/expansion/base-core.d.ts.map +1 -0
- package/dist/expansion/base-core.unit.test.d.ts +10 -0
- package/dist/expansion/base-core.unit.test.d.ts.map +1 -0
- package/dist/expansion/base-helpers.d.ts +57 -0
- package/dist/expansion/base-helpers.d.ts.map +1 -0
- package/dist/expansion/base.d.ts +32 -1
- package/dist/expansion/base.d.ts.map +1 -1
- 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/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/index/index.cjs +1060 -99
- package/dist/index/index.cjs.map +1 -1
- package/dist/index/index.js +1015 -100
- package/dist/index/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- 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/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/package.json +6 -1
package/dist/index/index.js
CHANGED
|
@@ -5,22 +5,134 @@ import { n as miniBatchKMeans, r as normaliseFeatures, t as _computeMean } from
|
|
|
5
5
|
import { approximateClusteringCoefficient, batchClusteringCoefficients, computeJaccard, countEdgesOfType, countNodesOfType, entropyFromCounts, localClusteringCoefficient, localTypeEntropy, neighbourIntersection, neighbourOverlap, neighbourSet, normalisedEntropy, shannonEntropy } from "../utils/index.js";
|
|
6
6
|
import { grasp, stratified } from "../seeds/index.js";
|
|
7
7
|
import { GPUContext, GPUNotAvailableError, assertWebGPUAvailable, createGPUContext, createResultBuffer, csrToGPUBuffers, detectWebGPU, getGPUContext, graphToCSR, isWebGPUAvailable, readBufferToCPU } from "../gpu/index.js";
|
|
8
|
-
|
|
8
|
+
import { collectAsyncIterable, defaultYieldStrategy, opDegree, opGetEdge, opGetNode, opHasNode, opNeighbours, opProgress, opYield, resolveAsyncOp, resolveSyncOp, runAsync, runSync } from "../async/index.js";
|
|
9
|
+
//#region src/expansion/base-helpers.ts
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Check whether expansion should continue given current progress.
|
|
12
|
+
*
|
|
13
|
+
* Returns shouldContinue=false as soon as any configured limit is reached,
|
|
14
|
+
* along with the appropriate termination reason.
|
|
15
|
+
*
|
|
16
|
+
* @param iterations - Number of iterations completed so far
|
|
17
|
+
* @param nodesVisited - Number of distinct nodes visited so far
|
|
18
|
+
* @param pathsFound - Number of paths discovered so far
|
|
19
|
+
* @param limits - Configured expansion limits (0 = unlimited)
|
|
20
|
+
* @returns Whether to continue and the termination reason if stopping
|
|
21
|
+
*/
|
|
22
|
+
function continueExpansion(iterations, nodesVisited, pathsFound, limits) {
|
|
23
|
+
if (limits.maxIterations > 0 && iterations >= limits.maxIterations) return {
|
|
24
|
+
shouldContinue: false,
|
|
25
|
+
termination: "limit"
|
|
26
|
+
};
|
|
27
|
+
if (limits.maxNodes > 0 && nodesVisited >= limits.maxNodes) return {
|
|
28
|
+
shouldContinue: false,
|
|
29
|
+
termination: "limit"
|
|
30
|
+
};
|
|
31
|
+
if (limits.maxPaths > 0 && pathsFound >= limits.maxPaths) return {
|
|
32
|
+
shouldContinue: false,
|
|
33
|
+
termination: "limit"
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
shouldContinue: true,
|
|
37
|
+
termination: "exhausted"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reconstruct path from collision point.
|
|
42
|
+
*
|
|
43
|
+
* Traces backwards through the predecessor maps of both frontiers from the
|
|
44
|
+
* collision node, then concatenates the two halves to form the full path.
|
|
45
|
+
*
|
|
46
|
+
* @param collisionNode - The node where the two frontiers met
|
|
47
|
+
* @param frontierA - Index of the first frontier
|
|
48
|
+
* @param frontierB - Index of the second frontier
|
|
49
|
+
* @param predecessors - Predecessor maps, one per frontier
|
|
50
|
+
* @param seeds - Seed nodes, one per frontier
|
|
51
|
+
* @returns The reconstructed path, or null if seeds are missing
|
|
52
|
+
*/
|
|
53
|
+
function reconstructPath$1(collisionNode, frontierA, frontierB, predecessors, seeds) {
|
|
54
|
+
const pathA = [collisionNode];
|
|
55
|
+
const predA = predecessors[frontierA];
|
|
56
|
+
if (predA !== void 0) {
|
|
57
|
+
let node = collisionNode;
|
|
58
|
+
let next = predA.get(node);
|
|
59
|
+
while (next !== null && next !== void 0) {
|
|
60
|
+
node = next;
|
|
61
|
+
pathA.unshift(node);
|
|
62
|
+
next = predA.get(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const pathB = [];
|
|
66
|
+
const predB = predecessors[frontierB];
|
|
67
|
+
if (predB !== void 0) {
|
|
68
|
+
let node = collisionNode;
|
|
69
|
+
let next = predB.get(node);
|
|
70
|
+
while (next !== null && next !== void 0) {
|
|
71
|
+
node = next;
|
|
72
|
+
pathB.push(node);
|
|
73
|
+
next = predB.get(node);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const fullPath = [...pathA, ...pathB];
|
|
77
|
+
const seedA = seeds[frontierA];
|
|
78
|
+
const seedB = seeds[frontierB];
|
|
79
|
+
if (seedA === void 0 || seedB === void 0) return null;
|
|
80
|
+
return {
|
|
81
|
+
fromSeed: seedA,
|
|
82
|
+
toSeed: seedB,
|
|
83
|
+
nodes: fullPath
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create an empty expansion result for early termination (e.g. no seeds given).
|
|
88
|
+
*
|
|
89
|
+
* @param algorithm - Name of the algorithm producing this result
|
|
90
|
+
* @param startTime - performance.now() timestamp taken before the algorithm began
|
|
91
|
+
* @returns An ExpansionResult with zero paths and zero stats
|
|
92
|
+
*/
|
|
93
|
+
function emptyResult$2(algorithm, startTime) {
|
|
94
|
+
return {
|
|
95
|
+
paths: [],
|
|
96
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
97
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
98
|
+
visitedPerFrontier: [],
|
|
99
|
+
stats: {
|
|
100
|
+
iterations: 0,
|
|
101
|
+
nodesVisited: 0,
|
|
102
|
+
edgesTraversed: 0,
|
|
103
|
+
pathsFound: 0,
|
|
104
|
+
durationMs: performance.now() - startTime,
|
|
105
|
+
algorithm,
|
|
106
|
+
termination: "exhausted"
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/expansion/base-core.ts
|
|
112
|
+
/**
|
|
113
|
+
* Default priority function — degree-ordered (DOME).
|
|
114
|
+
*
|
|
115
|
+
* Lower degree = higher priority, so sparse nodes are explored before hubs.
|
|
11
116
|
*/
|
|
12
117
|
function degreePriority(_nodeId, context) {
|
|
13
118
|
return context.degree;
|
|
14
119
|
}
|
|
15
120
|
/**
|
|
16
|
-
*
|
|
121
|
+
* Generator core of the BASE expansion algorithm.
|
|
17
122
|
*
|
|
18
|
-
*
|
|
123
|
+
* Yields GraphOp objects to request graph data, allowing the caller to
|
|
124
|
+
* provide a sync or async runner. The optional `graphRef` parameter is
|
|
125
|
+
* required when the priority function accesses `context.graph` — it is
|
|
126
|
+
* populated in sync mode by `base()`. In async mode (Phase 4+), a proxy
|
|
127
|
+
* graph may be supplied instead.
|
|
128
|
+
*
|
|
129
|
+
* @param graphMeta - Immutable graph metadata (directed, nodeCount, edgeCount)
|
|
19
130
|
* @param seeds - Seed nodes for expansion
|
|
20
|
-
* @param config - Expansion configuration
|
|
21
|
-
* @
|
|
131
|
+
* @param config - Expansion configuration (priority, limits, debug)
|
|
132
|
+
* @param graphRef - Optional real graph reference for context.graph in priority functions
|
|
133
|
+
* @returns An ExpansionResult with all discovered paths and statistics
|
|
22
134
|
*/
|
|
23
|
-
function
|
|
135
|
+
function* baseCore(graphMeta, seeds, config, graphRef) {
|
|
24
136
|
const startTime = performance.now();
|
|
25
137
|
const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
|
|
26
138
|
if (seeds.length === 0) return emptyResult$2("base", startTime);
|
|
@@ -40,7 +152,8 @@ function base(graph, seeds, config) {
|
|
|
40
152
|
predecessors[i]?.set(seedNode, null);
|
|
41
153
|
combinedVisited.set(seedNode, i);
|
|
42
154
|
allVisited.add(seedNode);
|
|
43
|
-
const
|
|
155
|
+
const seedDegree = yield* opDegree(seedNode);
|
|
156
|
+
const seedPriority = priority(seedNode, buildPriorityContext(seedNode, i, combinedVisited, allVisited, [], 0, seedDegree, graphRef));
|
|
44
157
|
queues[i]?.push({
|
|
45
158
|
nodeId: seedNode,
|
|
46
159
|
frontierIndex: i,
|
|
@@ -52,22 +165,17 @@ function base(graph, seeds, config) {
|
|
|
52
165
|
let iterations = 0;
|
|
53
166
|
let edgesTraversed = 0;
|
|
54
167
|
let termination = "exhausted";
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (maxNodes > 0 && allVisited.size >= maxNodes) {
|
|
61
|
-
termination = "limit";
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
if (maxPaths > 0 && discoveredPaths.length >= maxPaths) {
|
|
65
|
-
termination = "limit";
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
return true;
|
|
168
|
+
const limits = {
|
|
169
|
+
maxIterations,
|
|
170
|
+
maxNodes,
|
|
171
|
+
maxPaths
|
|
69
172
|
};
|
|
70
|
-
|
|
173
|
+
for (;;) {
|
|
174
|
+
const check = continueExpansion(iterations, allVisited.size, discoveredPaths.length, limits);
|
|
175
|
+
if (!check.shouldContinue) {
|
|
176
|
+
termination = check.termination;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
71
179
|
let lowestPriority = Number.POSITIVE_INFINITY;
|
|
72
180
|
let activeFrontier = -1;
|
|
73
181
|
for (let i = 0; i < numFrontiers; i++) {
|
|
@@ -111,7 +219,7 @@ function base(graph, seeds, config) {
|
|
|
111
219
|
}
|
|
112
220
|
}
|
|
113
221
|
}
|
|
114
|
-
const neighbours =
|
|
222
|
+
const neighbours = yield* opNeighbours(nodeId);
|
|
115
223
|
for (const neighbour of neighbours) {
|
|
116
224
|
edgesTraversed++;
|
|
117
225
|
const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
|
|
@@ -121,9 +229,10 @@ function base(graph, seeds, config) {
|
|
|
121
229
|
sampledEdgeMap.set(s, targets);
|
|
122
230
|
}
|
|
123
231
|
targets.add(t);
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
const
|
|
232
|
+
const fv = visitedByFrontier[activeFrontier];
|
|
233
|
+
if (fv === void 0 || fv.has(neighbour)) continue;
|
|
234
|
+
const neighbourDegree = yield* opDegree(neighbour);
|
|
235
|
+
const neighbourPriority = priority(neighbour, buildPriorityContext(neighbour, activeFrontier, combinedVisited, allVisited, discoveredPaths, iterations + 1, neighbourDegree, graphRef));
|
|
127
236
|
queue.push({
|
|
128
237
|
nodeId: neighbour,
|
|
129
238
|
frontierIndex: activeFrontier,
|
|
@@ -153,12 +262,50 @@ function base(graph, seeds, config) {
|
|
|
153
262
|
};
|
|
154
263
|
}
|
|
155
264
|
/**
|
|
156
|
-
* Create
|
|
265
|
+
* Create a sentinel ReadableGraph that throws if any member is accessed.
|
|
266
|
+
*
|
|
267
|
+
* Used in async mode when no graphRef is provided. Gives a clear error
|
|
268
|
+
* message rather than silently returning incorrect results if a priority
|
|
269
|
+
* function attempts to access `context.graph` before Phase 4b introduces
|
|
270
|
+
* a real async proxy.
|
|
157
271
|
*/
|
|
158
|
-
function
|
|
272
|
+
function makeNoGraphSentinel() {
|
|
273
|
+
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.";
|
|
274
|
+
const fail = () => {
|
|
275
|
+
throw new Error(msg);
|
|
276
|
+
};
|
|
159
277
|
return {
|
|
160
|
-
|
|
161
|
-
|
|
278
|
+
get directed() {
|
|
279
|
+
return fail();
|
|
280
|
+
},
|
|
281
|
+
get nodeCount() {
|
|
282
|
+
return fail();
|
|
283
|
+
},
|
|
284
|
+
get edgeCount() {
|
|
285
|
+
return fail();
|
|
286
|
+
},
|
|
287
|
+
hasNode: fail,
|
|
288
|
+
getNode: fail,
|
|
289
|
+
nodeIds: fail,
|
|
290
|
+
neighbours: fail,
|
|
291
|
+
degree: fail,
|
|
292
|
+
getEdge: fail,
|
|
293
|
+
edges: fail
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Build a PriorityContext for a node using a pre-fetched degree.
|
|
298
|
+
*
|
|
299
|
+
* When `graphRef` is provided (sync mode), it is used as `context.graph` so
|
|
300
|
+
* priority functions can access the graph directly. When it is absent (async
|
|
301
|
+
* mode), a Proxy is used in its place that throws a clear error if any
|
|
302
|
+
* property is accessed — this prevents silent failures until Phase 4b
|
|
303
|
+
* introduces a real async proxy graph.
|
|
304
|
+
*/
|
|
305
|
+
function buildPriorityContext(_nodeId, frontierIndex, combinedVisited, allVisited, discoveredPaths, iteration, degree, graphRef) {
|
|
306
|
+
return {
|
|
307
|
+
graph: graphRef ?? makeNoGraphSentinel(),
|
|
308
|
+
degree,
|
|
162
309
|
frontierIndex,
|
|
163
310
|
visitedByFrontier: combinedVisited,
|
|
164
311
|
allVisited,
|
|
@@ -166,65 +313,71 @@ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, al
|
|
|
166
313
|
iteration
|
|
167
314
|
};
|
|
168
315
|
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/expansion/base.ts
|
|
169
318
|
/**
|
|
170
|
-
*
|
|
319
|
+
* Run BASE expansion synchronously.
|
|
320
|
+
*
|
|
321
|
+
* Delegates to baseCore + runSync. Behaviour is identical to the previous
|
|
322
|
+
* direct implementation — all existing callers are unaffected.
|
|
323
|
+
*
|
|
324
|
+
* @param graph - Source graph
|
|
325
|
+
* @param seeds - Seed nodes for expansion
|
|
326
|
+
* @param config - Expansion configuration
|
|
327
|
+
* @returns Expansion result with discovered paths
|
|
171
328
|
*/
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
while (next !== null && next !== void 0) {
|
|
179
|
-
node = next;
|
|
180
|
-
pathA.unshift(node);
|
|
181
|
-
next = predA.get(node);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const pathB = [];
|
|
185
|
-
const predB = predecessors[frontierB];
|
|
186
|
-
if (predB !== void 0) {
|
|
187
|
-
let node = collisionNode;
|
|
188
|
-
let next = predB.get(node);
|
|
189
|
-
while (next !== null && next !== void 0) {
|
|
190
|
-
node = next;
|
|
191
|
-
pathB.push(node);
|
|
192
|
-
next = predB.get(node);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const fullPath = [...pathA, ...pathB];
|
|
196
|
-
const seedA = seeds[frontierA];
|
|
197
|
-
const seedB = seeds[frontierB];
|
|
198
|
-
if (seedA === void 0 || seedB === void 0) return null;
|
|
199
|
-
return {
|
|
200
|
-
fromSeed: seedA,
|
|
201
|
-
toSeed: seedB,
|
|
202
|
-
nodes: fullPath
|
|
203
|
-
};
|
|
329
|
+
function base(graph, seeds, config) {
|
|
330
|
+
return runSync(baseCore({
|
|
331
|
+
directed: graph.directed,
|
|
332
|
+
nodeCount: graph.nodeCount,
|
|
333
|
+
edgeCount: graph.edgeCount
|
|
334
|
+
}, seeds, config, graph), graph);
|
|
204
335
|
}
|
|
205
336
|
/**
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
337
|
+
* Run BASE expansion asynchronously.
|
|
338
|
+
*
|
|
339
|
+
* Delegates to baseCore + runAsync. Supports:
|
|
340
|
+
* - Cancellation via AbortSignal (config.signal)
|
|
341
|
+
* - Progress callbacks (config.onProgress)
|
|
342
|
+
* - Custom cooperative yield strategies (config.yieldStrategy)
|
|
343
|
+
*
|
|
344
|
+
* Note: priority functions that access `context.graph` are not supported in
|
|
345
|
+
* async mode without a graph proxy (Phase 4b). The default degree-based
|
|
346
|
+
* priority (DOME) does not access context.graph and works correctly.
|
|
347
|
+
*
|
|
348
|
+
* @param graph - Async source graph
|
|
349
|
+
* @param seeds - Seed nodes for expansion
|
|
350
|
+
* @param config - Expansion and async runner configuration
|
|
351
|
+
* @returns Promise resolving to the expansion result
|
|
352
|
+
*/
|
|
353
|
+
async function baseAsync(graph, seeds, config) {
|
|
354
|
+
const [nodeCount, edgeCount] = await Promise.all([graph.nodeCount, graph.edgeCount]);
|
|
355
|
+
const gen = baseCore({
|
|
356
|
+
directed: graph.directed,
|
|
357
|
+
nodeCount,
|
|
358
|
+
edgeCount
|
|
359
|
+
}, seeds, config);
|
|
360
|
+
const runnerOptions = {};
|
|
361
|
+
if (config?.signal !== void 0) runnerOptions.signal = config.signal;
|
|
362
|
+
if (config?.onProgress !== void 0) runnerOptions.onProgress = config.onProgress;
|
|
363
|
+
if (config?.yieldStrategy !== void 0) runnerOptions.yieldStrategy = config.yieldStrategy;
|
|
364
|
+
return runAsync(gen, graph, runnerOptions);
|
|
224
365
|
}
|
|
225
366
|
//#endregion
|
|
226
367
|
//#region src/expansion/dome.ts
|
|
227
368
|
/**
|
|
369
|
+
* DOME priority: lower degree is expanded first.
|
|
370
|
+
*/
|
|
371
|
+
function domePriority(_nodeId, context) {
|
|
372
|
+
return context.degree;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* DOME high-degree priority: negate degree to prioritise high-degree nodes.
|
|
376
|
+
*/
|
|
377
|
+
function domeHighDegreePriority(_nodeId, context) {
|
|
378
|
+
return -context.degree;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
228
381
|
* Run DOME expansion (degree-ordered).
|
|
229
382
|
*
|
|
230
383
|
* @param graph - Source graph
|
|
@@ -233,24 +386,46 @@ function emptyResult$2(algorithm, startTime) {
|
|
|
233
386
|
* @returns Expansion result with discovered paths
|
|
234
387
|
*/
|
|
235
388
|
function dome(graph, seeds, config) {
|
|
236
|
-
const domePriority = (nodeId, context) => {
|
|
237
|
-
return context.degree;
|
|
238
|
-
};
|
|
239
389
|
return base(graph, seeds, {
|
|
240
390
|
...config,
|
|
241
391
|
priority: domePriority
|
|
242
392
|
});
|
|
243
393
|
}
|
|
244
394
|
/**
|
|
395
|
+
* Run DOME expansion asynchronously (degree-ordered).
|
|
396
|
+
*
|
|
397
|
+
* @param graph - Async source graph
|
|
398
|
+
* @param seeds - Seed nodes for expansion
|
|
399
|
+
* @param config - Expansion and async runner configuration
|
|
400
|
+
* @returns Promise resolving to the expansion result
|
|
401
|
+
*/
|
|
402
|
+
async function domeAsync(graph, seeds, config) {
|
|
403
|
+
return baseAsync(graph, seeds, {
|
|
404
|
+
...config,
|
|
405
|
+
priority: domePriority
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
245
409
|
* DOME with reverse priority (high degree first).
|
|
246
410
|
*/
|
|
247
411
|
function domeHighDegree(graph, seeds, config) {
|
|
248
|
-
const domePriority = (nodeId, context) => {
|
|
249
|
-
return -context.degree;
|
|
250
|
-
};
|
|
251
412
|
return base(graph, seeds, {
|
|
252
413
|
...config,
|
|
253
|
-
priority:
|
|
414
|
+
priority: domeHighDegreePriority
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Run DOME high-degree expansion asynchronously (high degree first).
|
|
419
|
+
*
|
|
420
|
+
* @param graph - Async source graph
|
|
421
|
+
* @param seeds - Seed nodes for expansion
|
|
422
|
+
* @param config - Expansion and async runner configuration
|
|
423
|
+
* @returns Promise resolving to the expansion result
|
|
424
|
+
*/
|
|
425
|
+
async function domeHighDegreeAsync(graph, seeds, config) {
|
|
426
|
+
return baseAsync(graph, seeds, {
|
|
427
|
+
...config,
|
|
428
|
+
priority: domeHighDegreePriority
|
|
254
429
|
});
|
|
255
430
|
}
|
|
256
431
|
//#endregion
|
|
@@ -295,6 +470,26 @@ function hae(graph, seeds, config) {
|
|
|
295
470
|
priority: createHAEPriority(typeMapper)
|
|
296
471
|
});
|
|
297
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Run HAE expansion asynchronously.
|
|
475
|
+
*
|
|
476
|
+
* Note: the HAE priority function accesses `context.graph` to retrieve
|
|
477
|
+
* neighbour types. Full async equivalence requires PriorityContext
|
|
478
|
+
* refactoring (Phase 4b deferred). This export establishes the async API
|
|
479
|
+
* surface; use with a `wrapAsync`-wrapped sync graph for testing.
|
|
480
|
+
*
|
|
481
|
+
* @param graph - Async source graph
|
|
482
|
+
* @param seeds - Seed nodes for expansion
|
|
483
|
+
* @param config - HAE configuration combined with async runner options
|
|
484
|
+
* @returns Promise resolving to the expansion result
|
|
485
|
+
*/
|
|
486
|
+
async function haeAsync(graph, seeds, config) {
|
|
487
|
+
const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
|
|
488
|
+
return baseAsync(graph, seeds, {
|
|
489
|
+
...config,
|
|
490
|
+
priority: createHAEPriority(typeMapper)
|
|
491
|
+
});
|
|
492
|
+
}
|
|
298
493
|
//#endregion
|
|
299
494
|
//#region src/expansion/edge.ts
|
|
300
495
|
/** Default type mapper: reads `node.type`, falling back to "default". */
|
|
@@ -316,6 +511,27 @@ function edge(graph, seeds, config) {
|
|
|
316
511
|
typeMapper: defaultTypeMapper
|
|
317
512
|
});
|
|
318
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Run EDGE expansion asynchronously.
|
|
516
|
+
*
|
|
517
|
+
* Delegates to `haeAsync` with the default `node.type` mapper.
|
|
518
|
+
*
|
|
519
|
+
* Note: the HAE priority function accesses `context.graph` to retrieve
|
|
520
|
+
* neighbour types. Full async equivalence requires PriorityContext
|
|
521
|
+
* refactoring (Phase 4b deferred). This export establishes the async API
|
|
522
|
+
* surface; use with a `wrapAsync`-wrapped sync graph for testing.
|
|
523
|
+
*
|
|
524
|
+
* @param graph - Async source graph
|
|
525
|
+
* @param seeds - Seed nodes for expansion
|
|
526
|
+
* @param config - Expansion and async runner configuration
|
|
527
|
+
* @returns Promise resolving to the expansion result
|
|
528
|
+
*/
|
|
529
|
+
async function edgeAsync(graph, seeds, config) {
|
|
530
|
+
return haeAsync(graph, seeds, {
|
|
531
|
+
...config,
|
|
532
|
+
typeMapper: defaultTypeMapper
|
|
533
|
+
});
|
|
534
|
+
}
|
|
319
535
|
//#endregion
|
|
320
536
|
//#region src/expansion/pipe.ts
|
|
321
537
|
/**
|
|
@@ -351,6 +567,25 @@ function pipe(graph, seeds, config) {
|
|
|
351
567
|
priority: pipePriority
|
|
352
568
|
});
|
|
353
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Run PIPE expansion asynchronously.
|
|
572
|
+
*
|
|
573
|
+
* Note: the PIPE priority function accesses `context.graph` to retrieve
|
|
574
|
+
* neighbour lists. Full async equivalence requires PriorityContext
|
|
575
|
+
* refactoring (Phase 4b deferred). This export establishes the async API
|
|
576
|
+
* surface; use with a `wrapAsync`-wrapped sync graph for testing.
|
|
577
|
+
*
|
|
578
|
+
* @param graph - Async source graph
|
|
579
|
+
* @param seeds - Seed nodes for expansion
|
|
580
|
+
* @param config - Expansion and async runner configuration
|
|
581
|
+
* @returns Promise resolving to the expansion result
|
|
582
|
+
*/
|
|
583
|
+
async function pipeAsync(graph, seeds, config) {
|
|
584
|
+
return baseAsync(graph, seeds, {
|
|
585
|
+
...config,
|
|
586
|
+
priority: pipePriority
|
|
587
|
+
});
|
|
588
|
+
}
|
|
354
589
|
//#endregion
|
|
355
590
|
//#region src/expansion/priority-helpers.ts
|
|
356
591
|
/**
|
|
@@ -447,6 +682,34 @@ function sage(graph, seeds, config) {
|
|
|
447
682
|
priority: sagePriority
|
|
448
683
|
});
|
|
449
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Run SAGE expansion asynchronously.
|
|
687
|
+
*
|
|
688
|
+
* Creates fresh closure state (salienceCounts, phase tracking) for this
|
|
689
|
+
* invocation. The SAGE priority function does not access `context.graph`
|
|
690
|
+
* directly, so it is safe to use in async mode via `baseAsync`.
|
|
691
|
+
*
|
|
692
|
+
* @param graph - Async source graph
|
|
693
|
+
* @param seeds - Seed nodes for expansion
|
|
694
|
+
* @param config - Expansion and async runner configuration
|
|
695
|
+
* @returns Promise resolving to the expansion result
|
|
696
|
+
*/
|
|
697
|
+
async function sageAsync(graph, seeds, config) {
|
|
698
|
+
const salienceCounts = /* @__PURE__ */ new Map();
|
|
699
|
+
let inPhase2 = false;
|
|
700
|
+
let lastPathCount = 0;
|
|
701
|
+
function sagePriority(nodeId, context) {
|
|
702
|
+
const pathCount = context.discoveredPaths.length;
|
|
703
|
+
if (pathCount > 0 && !inPhase2) inPhase2 = true;
|
|
704
|
+
if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
705
|
+
if (!inPhase2) return Math.log(context.degree + 1);
|
|
706
|
+
return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
|
|
707
|
+
}
|
|
708
|
+
return baseAsync(graph, seeds, {
|
|
709
|
+
...config,
|
|
710
|
+
priority: sagePriority
|
|
711
|
+
});
|
|
712
|
+
}
|
|
450
713
|
//#endregion
|
|
451
714
|
//#region src/ranking/mi/jaccard.ts
|
|
452
715
|
/**
|
|
@@ -464,6 +727,23 @@ function jaccard(graph, source, target, config) {
|
|
|
464
727
|
if (sourceNeighbours.size === 0 && targetNeighbours.size === 0) return 0;
|
|
465
728
|
return Math.max(epsilon, jaccardScore);
|
|
466
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* Async variant of Jaccard similarity for use with async graph data sources.
|
|
732
|
+
*
|
|
733
|
+
* Fetches both neighbourhoods concurrently, then applies the same formula.
|
|
734
|
+
*/
|
|
735
|
+
async function jaccardAsync(graph, source, target, config) {
|
|
736
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
737
|
+
const [sourceNeighboursArr, targetNeighboursArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
738
|
+
const srcSet = new Set(sourceNeighboursArr.filter((n) => n !== target));
|
|
739
|
+
const tgtSet = new Set(targetNeighboursArr.filter((n) => n !== source));
|
|
740
|
+
if (srcSet.size === 0 && tgtSet.size === 0) return 0;
|
|
741
|
+
let intersection = 0;
|
|
742
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
743
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
744
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
745
|
+
return Math.max(epsilon, jaccardScore);
|
|
746
|
+
}
|
|
467
747
|
//#endregion
|
|
468
748
|
//#region src/expansion/reach.ts
|
|
469
749
|
/**
|
|
@@ -520,6 +800,40 @@ function reach(graph, seeds, config) {
|
|
|
520
800
|
priority: reachPriority
|
|
521
801
|
});
|
|
522
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* Run REACH expansion asynchronously.
|
|
805
|
+
*
|
|
806
|
+
* Creates fresh closure state (phase tracking, Jaccard cache) for this
|
|
807
|
+
* invocation. The REACH priority function uses `jaccard(context.graph, ...)`
|
|
808
|
+
* in Phase 2; in async mode `context.graph` is the sentinel and will throw.
|
|
809
|
+
* Full async equivalence requires PriorityContext refactoring (Phase 4b
|
|
810
|
+
* deferred). This export establishes the async API surface.
|
|
811
|
+
*
|
|
812
|
+
* @param graph - Async source graph
|
|
813
|
+
* @param seeds - Seed nodes for expansion
|
|
814
|
+
* @param config - Expansion and async runner configuration
|
|
815
|
+
* @returns Promise resolving to the expansion result
|
|
816
|
+
*/
|
|
817
|
+
async function reachAsync(graph, seeds, config) {
|
|
818
|
+
let inPhase2 = false;
|
|
819
|
+
function reachPriority(nodeId, context) {
|
|
820
|
+
if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
|
|
821
|
+
if (!inPhase2) return Math.log(context.degree + 1);
|
|
822
|
+
let totalMI = 0;
|
|
823
|
+
let endpointCount = 0;
|
|
824
|
+
for (const path of context.discoveredPaths) {
|
|
825
|
+
totalMI += jaccard(context.graph, nodeId, path.fromSeed.id);
|
|
826
|
+
totalMI += jaccard(context.graph, nodeId, path.toSeed.id);
|
|
827
|
+
endpointCount += 2;
|
|
828
|
+
}
|
|
829
|
+
const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
|
|
830
|
+
return Math.log(context.degree + 1) * (1 - miHat);
|
|
831
|
+
}
|
|
832
|
+
return baseAsync(graph, seeds, {
|
|
833
|
+
...config,
|
|
834
|
+
priority: reachPriority
|
|
835
|
+
});
|
|
836
|
+
}
|
|
523
837
|
//#endregion
|
|
524
838
|
//#region src/expansion/maze.ts
|
|
525
839
|
/** Default threshold for switching to phase 2 (after M paths) */
|
|
@@ -566,6 +880,46 @@ function maze(graph, seeds, config) {
|
|
|
566
880
|
priority: mazePriority
|
|
567
881
|
});
|
|
568
882
|
}
|
|
883
|
+
/**
|
|
884
|
+
* Run MAZE expansion asynchronously.
|
|
885
|
+
*
|
|
886
|
+
* Creates fresh closure state (salienceCounts, phase tracking) for this
|
|
887
|
+
* invocation. The MAZE priority function accesses `context.graph` to
|
|
888
|
+
* retrieve neighbour lists for path potential computation. Full async
|
|
889
|
+
* equivalence requires PriorityContext refactoring (Phase 4b deferred).
|
|
890
|
+
* This export establishes the async API surface.
|
|
891
|
+
*
|
|
892
|
+
* @param graph - Async source graph
|
|
893
|
+
* @param seeds - Seed nodes for expansion
|
|
894
|
+
* @param config - Expansion and async runner configuration
|
|
895
|
+
* @returns Promise resolving to the expansion result
|
|
896
|
+
*/
|
|
897
|
+
async function mazeAsync(graph, seeds, config) {
|
|
898
|
+
const salienceCounts = /* @__PURE__ */ new Map();
|
|
899
|
+
let inPhase2 = false;
|
|
900
|
+
let lastPathCount = 0;
|
|
901
|
+
function mazePriority(nodeId, context) {
|
|
902
|
+
const pathCount = context.discoveredPaths.length;
|
|
903
|
+
if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
|
|
904
|
+
inPhase2 = true;
|
|
905
|
+
updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
|
|
906
|
+
}
|
|
907
|
+
if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
908
|
+
const nodeNeighbours = context.graph.neighbours(nodeId);
|
|
909
|
+
let pathPotential = 0;
|
|
910
|
+
for (const neighbour of nodeNeighbours) {
|
|
911
|
+
const visitedBy = context.visitedByFrontier.get(neighbour);
|
|
912
|
+
if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
|
|
913
|
+
}
|
|
914
|
+
if (!inPhase2) return context.degree / (1 + pathPotential);
|
|
915
|
+
const salience = salienceCounts.get(nodeId) ?? 0;
|
|
916
|
+
return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
|
|
917
|
+
}
|
|
918
|
+
return baseAsync(graph, seeds, {
|
|
919
|
+
...config,
|
|
920
|
+
priority: mazePriority
|
|
921
|
+
});
|
|
922
|
+
}
|
|
569
923
|
//#endregion
|
|
570
924
|
//#region src/expansion/tide.ts
|
|
571
925
|
/**
|
|
@@ -597,6 +951,25 @@ function tide(graph, seeds, config) {
|
|
|
597
951
|
priority: tidePriority
|
|
598
952
|
});
|
|
599
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Run TIDE expansion asynchronously.
|
|
956
|
+
*
|
|
957
|
+
* Note: the TIDE priority function accesses `context.graph` to retrieve
|
|
958
|
+
* neighbour lists and per-neighbour degrees. Full async equivalence
|
|
959
|
+
* requires PriorityContext refactoring (Phase 4b deferred). This export
|
|
960
|
+
* establishes the async API surface.
|
|
961
|
+
*
|
|
962
|
+
* @param graph - Async source graph
|
|
963
|
+
* @param seeds - Seed nodes for expansion
|
|
964
|
+
* @param config - Expansion and async runner configuration
|
|
965
|
+
* @returns Promise resolving to the expansion result
|
|
966
|
+
*/
|
|
967
|
+
async function tideAsync(graph, seeds, config) {
|
|
968
|
+
return baseAsync(graph, seeds, {
|
|
969
|
+
...config,
|
|
970
|
+
priority: tidePriority
|
|
971
|
+
});
|
|
972
|
+
}
|
|
600
973
|
//#endregion
|
|
601
974
|
//#region src/expansion/lace.ts
|
|
602
975
|
/**
|
|
@@ -627,6 +1000,27 @@ function lace(graph, seeds, config) {
|
|
|
627
1000
|
priority
|
|
628
1001
|
});
|
|
629
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Run LACE expansion asynchronously.
|
|
1005
|
+
*
|
|
1006
|
+
* Note: the LACE priority function accesses `context.graph` via
|
|
1007
|
+
* `avgFrontierMI`. Full async equivalence requires PriorityContext
|
|
1008
|
+
* refactoring (Phase 4b deferred). This export establishes the async
|
|
1009
|
+
* API surface.
|
|
1010
|
+
*
|
|
1011
|
+
* @param graph - Async source graph
|
|
1012
|
+
* @param seeds - Seed nodes for expansion
|
|
1013
|
+
* @param config - LACE configuration combined with async runner options
|
|
1014
|
+
* @returns Promise resolving to the expansion result
|
|
1015
|
+
*/
|
|
1016
|
+
async function laceAsync(graph, seeds, config) {
|
|
1017
|
+
const { mi = jaccard, ...restConfig } = config ?? {};
|
|
1018
|
+
const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
|
|
1019
|
+
return baseAsync(graph, seeds, {
|
|
1020
|
+
...restConfig,
|
|
1021
|
+
priority
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
630
1024
|
//#endregion
|
|
631
1025
|
//#region src/expansion/warp.ts
|
|
632
1026
|
/**
|
|
@@ -659,6 +1053,25 @@ function warp(graph, seeds, config) {
|
|
|
659
1053
|
priority: warpPriority
|
|
660
1054
|
});
|
|
661
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Run WARP expansion asynchronously.
|
|
1058
|
+
*
|
|
1059
|
+
* Note: the WARP priority function accesses `context.graph` via
|
|
1060
|
+
* `countCrossFrontierNeighbours`. Full async equivalence requires
|
|
1061
|
+
* PriorityContext refactoring (Phase 4b deferred). This export
|
|
1062
|
+
* establishes the async API surface.
|
|
1063
|
+
*
|
|
1064
|
+
* @param graph - Async source graph
|
|
1065
|
+
* @param seeds - Seed nodes for expansion
|
|
1066
|
+
* @param config - Expansion and async runner configuration
|
|
1067
|
+
* @returns Promise resolving to the expansion result
|
|
1068
|
+
*/
|
|
1069
|
+
async function warpAsync(graph, seeds, config) {
|
|
1070
|
+
return baseAsync(graph, seeds, {
|
|
1071
|
+
...config,
|
|
1072
|
+
priority: warpPriority
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
662
1075
|
//#endregion
|
|
663
1076
|
//#region src/expansion/fuse.ts
|
|
664
1077
|
/**
|
|
@@ -691,6 +1104,27 @@ function fuse(graph, seeds, config) {
|
|
|
691
1104
|
priority
|
|
692
1105
|
});
|
|
693
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Run FUSE expansion asynchronously.
|
|
1109
|
+
*
|
|
1110
|
+
* Note: the FUSE priority function accesses `context.graph` via
|
|
1111
|
+
* `avgFrontierMI`. Full async equivalence requires PriorityContext
|
|
1112
|
+
* refactoring (Phase 4b deferred). This export establishes the async
|
|
1113
|
+
* API surface.
|
|
1114
|
+
*
|
|
1115
|
+
* @param graph - Async source graph
|
|
1116
|
+
* @param seeds - Seed nodes for expansion
|
|
1117
|
+
* @param config - FUSE configuration combined with async runner options
|
|
1118
|
+
* @returns Promise resolving to the expansion result
|
|
1119
|
+
*/
|
|
1120
|
+
async function fuseAsync(graph, seeds, config) {
|
|
1121
|
+
const { mi = jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
|
|
1122
|
+
const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
|
|
1123
|
+
return baseAsync(graph, seeds, {
|
|
1124
|
+
...restConfig,
|
|
1125
|
+
priority
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
694
1128
|
//#endregion
|
|
695
1129
|
//#region src/expansion/sift.ts
|
|
696
1130
|
/**
|
|
@@ -723,6 +1157,27 @@ function sift(graph, seeds, config) {
|
|
|
723
1157
|
priority
|
|
724
1158
|
});
|
|
725
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Run SIFT expansion asynchronously.
|
|
1162
|
+
*
|
|
1163
|
+
* Note: the SIFT priority function accesses `context.graph` via
|
|
1164
|
+
* `avgFrontierMI`. Full async equivalence requires PriorityContext
|
|
1165
|
+
* refactoring (Phase 4b deferred). This export establishes the async
|
|
1166
|
+
* API surface.
|
|
1167
|
+
*
|
|
1168
|
+
* @param graph - Async source graph
|
|
1169
|
+
* @param seeds - Seed nodes for expansion
|
|
1170
|
+
* @param config - SIFT (REACHConfig) configuration combined with async runner options
|
|
1171
|
+
* @returns Promise resolving to the expansion result
|
|
1172
|
+
*/
|
|
1173
|
+
async function siftAsync(graph, seeds, config) {
|
|
1174
|
+
const { mi = jaccard, miThreshold = .25, ...restConfig } = config ?? {};
|
|
1175
|
+
const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
|
|
1176
|
+
return baseAsync(graph, seeds, {
|
|
1177
|
+
...restConfig,
|
|
1178
|
+
priority
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
726
1181
|
//#endregion
|
|
727
1182
|
//#region src/expansion/flux.ts
|
|
728
1183
|
/**
|
|
@@ -779,9 +1234,36 @@ function flux(graph, seeds, config) {
|
|
|
779
1234
|
priority
|
|
780
1235
|
});
|
|
781
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Run FLUX expansion asynchronously.
|
|
1239
|
+
*
|
|
1240
|
+
* Note: the FLUX priority function accesses `context.graph` to compute
|
|
1241
|
+
* local density and cross-frontier bridge scores. Full async equivalence
|
|
1242
|
+
* requires PriorityContext refactoring (Phase 4b deferred). This export
|
|
1243
|
+
* establishes the async API surface.
|
|
1244
|
+
*
|
|
1245
|
+
* @param graph - Async source graph
|
|
1246
|
+
* @param seeds - Seed nodes for expansion
|
|
1247
|
+
* @param config - FLUX (MAZEConfig) configuration combined with async runner options
|
|
1248
|
+
* @returns Promise resolving to the expansion result
|
|
1249
|
+
*/
|
|
1250
|
+
async function fluxAsync(graph, seeds, config) {
|
|
1251
|
+
const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
|
|
1252
|
+
const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
|
|
1253
|
+
return baseAsync(graph, seeds, {
|
|
1254
|
+
...restConfig,
|
|
1255
|
+
priority
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
782
1258
|
//#endregion
|
|
783
1259
|
//#region src/expansion/standard-bfs.ts
|
|
784
1260
|
/**
|
|
1261
|
+
* BFS priority: discovery iteration order (FIFO).
|
|
1262
|
+
*/
|
|
1263
|
+
function bfsPriority(_nodeId, context) {
|
|
1264
|
+
return context.iteration;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
785
1267
|
* Run standard BFS expansion (FIFO discovery order).
|
|
786
1268
|
*
|
|
787
1269
|
* @param graph - Source graph
|
|
@@ -790,17 +1272,35 @@ function flux(graph, seeds, config) {
|
|
|
790
1272
|
* @returns Expansion result with discovered paths
|
|
791
1273
|
*/
|
|
792
1274
|
function standardBfs(graph, seeds, config) {
|
|
793
|
-
const bfsPriority = (_nodeId, context) => {
|
|
794
|
-
return context.iteration;
|
|
795
|
-
};
|
|
796
1275
|
return base(graph, seeds, {
|
|
797
1276
|
...config,
|
|
798
1277
|
priority: bfsPriority
|
|
799
1278
|
});
|
|
800
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Run standard BFS expansion asynchronously (FIFO discovery order).
|
|
1282
|
+
*
|
|
1283
|
+
* @param graph - Async source graph
|
|
1284
|
+
* @param seeds - Seed nodes for expansion
|
|
1285
|
+
* @param config - Expansion and async runner configuration
|
|
1286
|
+
* @returns Promise resolving to the expansion result
|
|
1287
|
+
*/
|
|
1288
|
+
async function standardBfsAsync(graph, seeds, config) {
|
|
1289
|
+
return baseAsync(graph, seeds, {
|
|
1290
|
+
...config,
|
|
1291
|
+
priority: bfsPriority
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
801
1294
|
//#endregion
|
|
802
1295
|
//#region src/expansion/frontier-balanced.ts
|
|
803
1296
|
/**
|
|
1297
|
+
* Frontier-balanced priority: frontier index dominates, then discovery iteration.
|
|
1298
|
+
* Scales frontier index by 1e9 to ensure round-robin ordering across frontiers.
|
|
1299
|
+
*/
|
|
1300
|
+
function balancedPriority(_nodeId, context) {
|
|
1301
|
+
return context.frontierIndex * 1e9 + context.iteration;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
804
1304
|
* Run frontier-balanced expansion (round-robin across frontiers).
|
|
805
1305
|
*
|
|
806
1306
|
* @param graph - Source graph
|
|
@@ -809,14 +1309,25 @@ function standardBfs(graph, seeds, config) {
|
|
|
809
1309
|
* @returns Expansion result with discovered paths
|
|
810
1310
|
*/
|
|
811
1311
|
function frontierBalanced(graph, seeds, config) {
|
|
812
|
-
const balancedPriority = (_nodeId, context) => {
|
|
813
|
-
return context.frontierIndex * 1e9 + context.iteration;
|
|
814
|
-
};
|
|
815
1312
|
return base(graph, seeds, {
|
|
816
1313
|
...config,
|
|
817
1314
|
priority: balancedPriority
|
|
818
1315
|
});
|
|
819
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Run frontier-balanced expansion asynchronously (round-robin across frontiers).
|
|
1319
|
+
*
|
|
1320
|
+
* @param graph - Async source graph
|
|
1321
|
+
* @param seeds - Seed nodes for expansion
|
|
1322
|
+
* @param config - Expansion and async runner configuration
|
|
1323
|
+
* @returns Promise resolving to the expansion result
|
|
1324
|
+
*/
|
|
1325
|
+
async function frontierBalancedAsync(graph, seeds, config) {
|
|
1326
|
+
return baseAsync(graph, seeds, {
|
|
1327
|
+
...config,
|
|
1328
|
+
priority: balancedPriority
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
820
1331
|
//#endregion
|
|
821
1332
|
//#region src/expansion/random-priority.ts
|
|
822
1333
|
/**
|
|
@@ -836,6 +1347,12 @@ function seededRandom$1(input, seed = 0) {
|
|
|
836
1347
|
return (h >>> 0) / 4294967295;
|
|
837
1348
|
}
|
|
838
1349
|
/**
|
|
1350
|
+
* Build a seeded random priority function for a given seed value.
|
|
1351
|
+
*/
|
|
1352
|
+
function makeRandomPriorityFn(seed) {
|
|
1353
|
+
return (nodeId) => seededRandom$1(nodeId, seed);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
839
1356
|
* Run random-priority expansion (null hypothesis baseline).
|
|
840
1357
|
*
|
|
841
1358
|
* @param graph - Source graph
|
|
@@ -845,12 +1362,24 @@ function seededRandom$1(input, seed = 0) {
|
|
|
845
1362
|
*/
|
|
846
1363
|
function randomPriority(graph, seeds, config) {
|
|
847
1364
|
const { seed = 0 } = config ?? {};
|
|
848
|
-
const randomPriorityFn = (nodeId, context) => {
|
|
849
|
-
return seededRandom$1(nodeId, seed);
|
|
850
|
-
};
|
|
851
1365
|
return base(graph, seeds, {
|
|
852
1366
|
...config,
|
|
853
|
-
priority:
|
|
1367
|
+
priority: makeRandomPriorityFn(seed)
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Run random-priority expansion asynchronously (null hypothesis baseline).
|
|
1372
|
+
*
|
|
1373
|
+
* @param graph - Async source graph
|
|
1374
|
+
* @param seeds - Seed nodes for expansion
|
|
1375
|
+
* @param config - Expansion and async runner configuration
|
|
1376
|
+
* @returns Promise resolving to the expansion result
|
|
1377
|
+
*/
|
|
1378
|
+
async function randomPriorityAsync(graph, seeds, config) {
|
|
1379
|
+
const { seed = 0 } = config ?? {};
|
|
1380
|
+
return baseAsync(graph, seeds, {
|
|
1381
|
+
...config,
|
|
1382
|
+
priority: makeRandomPriorityFn(seed)
|
|
854
1383
|
});
|
|
855
1384
|
}
|
|
856
1385
|
//#endregion
|
|
@@ -882,6 +1411,20 @@ function dfsPriority(graph, seeds, config) {
|
|
|
882
1411
|
priority: dfsPriorityFn
|
|
883
1412
|
});
|
|
884
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Run DFS-priority expansion asynchronously (LIFO discovery order).
|
|
1416
|
+
*
|
|
1417
|
+
* @param graph - Async source graph
|
|
1418
|
+
* @param seeds - Seed nodes for expansion
|
|
1419
|
+
* @param config - Expansion and async runner configuration
|
|
1420
|
+
* @returns Promise resolving to the expansion result
|
|
1421
|
+
*/
|
|
1422
|
+
async function dfsPriorityAsync(graph, seeds, config) {
|
|
1423
|
+
return baseAsync(graph, seeds, {
|
|
1424
|
+
...config,
|
|
1425
|
+
priority: dfsPriorityFn
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
885
1428
|
//#endregion
|
|
886
1429
|
//#region src/expansion/k-hop.ts
|
|
887
1430
|
/**
|
|
@@ -1239,6 +1782,74 @@ function parse(graph, paths, config) {
|
|
|
1239
1782
|
};
|
|
1240
1783
|
}
|
|
1241
1784
|
/**
|
|
1785
|
+
* Rank paths using async PARSE (Path-Aware Ranking via Salience Estimation).
|
|
1786
|
+
*
|
|
1787
|
+
* Async variant suitable for use with remote or lazy graph data sources.
|
|
1788
|
+
* Computes geometric mean of edge MI scores for each path using Promise.all
|
|
1789
|
+
* for parallelism, then sorts by salience (highest first).
|
|
1790
|
+
*
|
|
1791
|
+
* @param graph - Async source graph
|
|
1792
|
+
* @param paths - Paths to rank
|
|
1793
|
+
* @param config - Configuration options
|
|
1794
|
+
* @returns Ranked paths with statistics
|
|
1795
|
+
*/
|
|
1796
|
+
async function parseAsync(graph, paths, config) {
|
|
1797
|
+
const startTime = performance.now();
|
|
1798
|
+
const { mi = jaccardAsync, epsilon = 1e-10 } = config ?? {};
|
|
1799
|
+
const rankedPaths = [];
|
|
1800
|
+
for (const path of paths) {
|
|
1801
|
+
const salience = await computePathSalienceAsync(graph, path, mi, epsilon);
|
|
1802
|
+
rankedPaths.push({
|
|
1803
|
+
...path,
|
|
1804
|
+
salience
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
rankedPaths.sort((a, b) => b.salience - a.salience);
|
|
1808
|
+
const endTime = performance.now();
|
|
1809
|
+
const saliences = rankedPaths.map((p) => p.salience);
|
|
1810
|
+
const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
|
|
1811
|
+
const sortedSaliences = [...saliences].sort((a, b) => a - b);
|
|
1812
|
+
const mid = Math.floor(sortedSaliences.length / 2);
|
|
1813
|
+
const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
|
|
1814
|
+
const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
|
|
1815
|
+
const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
|
|
1816
|
+
return {
|
|
1817
|
+
paths: rankedPaths,
|
|
1818
|
+
stats: {
|
|
1819
|
+
pathsRanked: rankedPaths.length,
|
|
1820
|
+
meanSalience,
|
|
1821
|
+
medianSalience,
|
|
1822
|
+
maxSalience,
|
|
1823
|
+
minSalience,
|
|
1824
|
+
durationMs: endTime - startTime
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Compute salience for a single path asynchronously.
|
|
1830
|
+
*
|
|
1831
|
+
* Uses geometric mean of edge MI scores for length-unbiased ranking.
|
|
1832
|
+
* Edge MI values are computed in parallel via Promise.all.
|
|
1833
|
+
*/
|
|
1834
|
+
async function computePathSalienceAsync(graph, path, mi, epsilon) {
|
|
1835
|
+
const nodes = path.nodes;
|
|
1836
|
+
if (nodes.length < 2) return epsilon;
|
|
1837
|
+
const edgeMIs = await Promise.all(nodes.slice(0, -1).map((source, i) => {
|
|
1838
|
+
const target = nodes[i + 1];
|
|
1839
|
+
if (target !== void 0) return mi(graph, source, target);
|
|
1840
|
+
return Promise.resolve(epsilon);
|
|
1841
|
+
}));
|
|
1842
|
+
let productMi = 1;
|
|
1843
|
+
let edgeCount = 0;
|
|
1844
|
+
for (const edgeMi of edgeMIs) {
|
|
1845
|
+
productMi *= Math.max(epsilon, edgeMi);
|
|
1846
|
+
edgeCount++;
|
|
1847
|
+
}
|
|
1848
|
+
if (edgeCount === 0) return epsilon;
|
|
1849
|
+
const salience = Math.pow(productMi, 1 / edgeCount);
|
|
1850
|
+
return Math.max(epsilon, Math.min(1, salience));
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1242
1853
|
* Compute salience for a single path.
|
|
1243
1854
|
*
|
|
1244
1855
|
* Uses geometric mean of edge MI scores for length-unbiased ranking.
|
|
@@ -1286,6 +1897,29 @@ function adamicAdar(graph, source, target, config) {
|
|
|
1286
1897
|
}
|
|
1287
1898
|
return Math.max(epsilon, score);
|
|
1288
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Async variant of Adamic-Adar index for use with async graph data sources.
|
|
1902
|
+
*
|
|
1903
|
+
* Fetches both neighbourhoods concurrently, then fetches degree for each common
|
|
1904
|
+
* neighbour to compute the inverse-log-degree weighted sum.
|
|
1905
|
+
*/
|
|
1906
|
+
async function adamicAdarAsync(graph, source, target, config) {
|
|
1907
|
+
const { epsilon = 1e-10, normalise = true } = config ?? {};
|
|
1908
|
+
const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
1909
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
1910
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
1911
|
+
const commonNeighbours = [];
|
|
1912
|
+
for (const n of srcSet) if (tgtSet.has(n)) commonNeighbours.push(n);
|
|
1913
|
+
if (commonNeighbours.length === 0) return epsilon;
|
|
1914
|
+
const degrees = await Promise.all(commonNeighbours.map((n) => graph.degree(n)));
|
|
1915
|
+
let score = 0;
|
|
1916
|
+
for (const degree of degrees) score += 1 / Math.log(degree + 1);
|
|
1917
|
+
if (normalise) {
|
|
1918
|
+
const maxScore = commonNeighbours.length / Math.log(2);
|
|
1919
|
+
score = score / maxScore;
|
|
1920
|
+
}
|
|
1921
|
+
return Math.max(epsilon, score);
|
|
1922
|
+
}
|
|
1289
1923
|
//#endregion
|
|
1290
1924
|
//#region src/ranking/mi/cosine.ts
|
|
1291
1925
|
/**
|
|
@@ -1307,6 +1941,23 @@ function cosine(graph, source, target, config) {
|
|
|
1307
1941
|
const score = intersection / denominator;
|
|
1308
1942
|
return Math.max(epsilon, score);
|
|
1309
1943
|
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Async variant of cosine similarity for use with async graph data sources.
|
|
1946
|
+
*
|
|
1947
|
+
* Fetches both neighbourhoods concurrently, then applies the same formula.
|
|
1948
|
+
*/
|
|
1949
|
+
async function cosineAsync(graph, source, target, config) {
|
|
1950
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1951
|
+
const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
1952
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
1953
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
1954
|
+
let intersection = 0;
|
|
1955
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
1956
|
+
const denominator = Math.sqrt(srcSet.size) * Math.sqrt(tgtSet.size);
|
|
1957
|
+
if (denominator === 0) return 0;
|
|
1958
|
+
const score = intersection / denominator;
|
|
1959
|
+
return Math.max(epsilon, score);
|
|
1960
|
+
}
|
|
1310
1961
|
//#endregion
|
|
1311
1962
|
//#region src/ranking/mi/sorensen.ts
|
|
1312
1963
|
/**
|
|
@@ -1328,6 +1979,23 @@ function sorensen(graph, source, target, config) {
|
|
|
1328
1979
|
const score = 2 * intersection / denominator;
|
|
1329
1980
|
return Math.max(epsilon, score);
|
|
1330
1981
|
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Async variant of Sorensen-Dice coefficient for use with async graph data sources.
|
|
1984
|
+
*
|
|
1985
|
+
* Fetches both neighbourhoods concurrently, then applies the same formula.
|
|
1986
|
+
*/
|
|
1987
|
+
async function sorensenAsync(graph, source, target, config) {
|
|
1988
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1989
|
+
const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
1990
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
1991
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
1992
|
+
let intersection = 0;
|
|
1993
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
1994
|
+
const denominator = srcSet.size + tgtSet.size;
|
|
1995
|
+
if (denominator === 0) return 0;
|
|
1996
|
+
const score = 2 * intersection / denominator;
|
|
1997
|
+
return Math.max(epsilon, score);
|
|
1998
|
+
}
|
|
1331
1999
|
//#endregion
|
|
1332
2000
|
//#region src/ranking/mi/resource-allocation.ts
|
|
1333
2001
|
/**
|
|
@@ -1353,6 +2021,29 @@ function resourceAllocation(graph, source, target, config) {
|
|
|
1353
2021
|
}
|
|
1354
2022
|
return Math.max(epsilon, score);
|
|
1355
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Async variant of Resource Allocation index for use with async graph data sources.
|
|
2026
|
+
*
|
|
2027
|
+
* Fetches both neighbourhoods concurrently, then fetches degree for each common
|
|
2028
|
+
* neighbour to compute the inverse-degree weighted sum.
|
|
2029
|
+
*/
|
|
2030
|
+
async function resourceAllocationAsync(graph, source, target, config) {
|
|
2031
|
+
const { epsilon = 1e-10, normalise = true } = config ?? {};
|
|
2032
|
+
const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
2033
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2034
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2035
|
+
const commonNeighbours = [];
|
|
2036
|
+
for (const n of srcSet) if (tgtSet.has(n)) commonNeighbours.push(n);
|
|
2037
|
+
if (commonNeighbours.length === 0) return epsilon;
|
|
2038
|
+
const degrees = await Promise.all(commonNeighbours.map((n) => graph.degree(n)));
|
|
2039
|
+
let score = 0;
|
|
2040
|
+
for (const degree of degrees) if (degree > 0) score += 1 / degree;
|
|
2041
|
+
if (normalise) {
|
|
2042
|
+
const maxScore = commonNeighbours.length;
|
|
2043
|
+
score = score / maxScore;
|
|
2044
|
+
}
|
|
2045
|
+
return Math.max(epsilon, score);
|
|
2046
|
+
}
|
|
1356
2047
|
//#endregion
|
|
1357
2048
|
//#region src/ranking/mi/overlap-coefficient.ts
|
|
1358
2049
|
/**
|
|
@@ -1374,6 +2065,23 @@ function overlapCoefficient(graph, source, target, config) {
|
|
|
1374
2065
|
const score = intersection / denominator;
|
|
1375
2066
|
return Math.max(epsilon, score);
|
|
1376
2067
|
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Async variant of Overlap Coefficient for use with async graph data sources.
|
|
2070
|
+
*
|
|
2071
|
+
* Fetches both neighbourhoods concurrently, then applies the same formula.
|
|
2072
|
+
*/
|
|
2073
|
+
async function overlapCoefficientAsync(graph, source, target, config) {
|
|
2074
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2075
|
+
const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
2076
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2077
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2078
|
+
let intersection = 0;
|
|
2079
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2080
|
+
const denominator = Math.min(srcSet.size, tgtSet.size);
|
|
2081
|
+
if (denominator === 0) return 0;
|
|
2082
|
+
const score = intersection / denominator;
|
|
2083
|
+
return Math.max(epsilon, score);
|
|
2084
|
+
}
|
|
1377
2085
|
//#endregion
|
|
1378
2086
|
//#region src/ranking/mi/hub-promoted.ts
|
|
1379
2087
|
/**
|
|
@@ -1395,6 +2103,28 @@ function hubPromoted(graph, source, target, config) {
|
|
|
1395
2103
|
const score = intersection / denominator;
|
|
1396
2104
|
return Math.max(epsilon, score);
|
|
1397
2105
|
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Async variant of Hub Promoted index for use with async graph data sources.
|
|
2108
|
+
*
|
|
2109
|
+
* Fetches both neighbourhoods and degrees concurrently, then applies the same formula.
|
|
2110
|
+
*/
|
|
2111
|
+
async function hubPromotedAsync(graph, source, target, config) {
|
|
2112
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2113
|
+
const [sourceArr, targetArr, sourceDegree, targetDegree] = await Promise.all([
|
|
2114
|
+
collectAsyncIterable(graph.neighbours(source)),
|
|
2115
|
+
collectAsyncIterable(graph.neighbours(target)),
|
|
2116
|
+
graph.degree(source),
|
|
2117
|
+
graph.degree(target)
|
|
2118
|
+
]);
|
|
2119
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2120
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2121
|
+
let intersection = 0;
|
|
2122
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2123
|
+
const denominator = Math.min(sourceDegree, targetDegree);
|
|
2124
|
+
if (denominator === 0) return 0;
|
|
2125
|
+
const score = intersection / denominator;
|
|
2126
|
+
return Math.max(epsilon, score);
|
|
2127
|
+
}
|
|
1398
2128
|
//#endregion
|
|
1399
2129
|
//#region src/ranking/mi/scale.ts
|
|
1400
2130
|
/**
|
|
@@ -1411,6 +2141,31 @@ function scale(graph, source, target, config) {
|
|
|
1411
2141
|
const score = jaccardScore / density;
|
|
1412
2142
|
return Math.max(epsilon, score);
|
|
1413
2143
|
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Async variant of SCALE MI for use with async graph data sources.
|
|
2146
|
+
*
|
|
2147
|
+
* Fetches both neighbourhoods, node count, and edge count concurrently.
|
|
2148
|
+
*/
|
|
2149
|
+
async function scaleAsync(graph, source, target, config) {
|
|
2150
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2151
|
+
const [sourceArr, targetArr, n, m] = await Promise.all([
|
|
2152
|
+
collectAsyncIterable(graph.neighbours(source)),
|
|
2153
|
+
collectAsyncIterable(graph.neighbours(target)),
|
|
2154
|
+
graph.nodeCount,
|
|
2155
|
+
graph.edgeCount
|
|
2156
|
+
]);
|
|
2157
|
+
const srcSet = new Set(sourceArr.filter((node) => node !== target));
|
|
2158
|
+
const tgtSet = new Set(targetArr.filter((node) => node !== source));
|
|
2159
|
+
let intersection = 0;
|
|
2160
|
+
for (const node of srcSet) if (tgtSet.has(node)) intersection++;
|
|
2161
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
2162
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
2163
|
+
const possibleEdges = n * (n - 1);
|
|
2164
|
+
const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
|
|
2165
|
+
if (density === 0) return epsilon;
|
|
2166
|
+
const score = jaccardScore / density;
|
|
2167
|
+
return Math.max(epsilon, score);
|
|
2168
|
+
}
|
|
1414
2169
|
//#endregion
|
|
1415
2170
|
//#region src/ranking/mi/skew.ts
|
|
1416
2171
|
/**
|
|
@@ -1427,6 +2182,31 @@ function skew(graph, source, target, config) {
|
|
|
1427
2182
|
const score = jaccardScore * sourceIdf * targetIdf;
|
|
1428
2183
|
return Math.max(epsilon, score);
|
|
1429
2184
|
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Async variant of SKEW MI for use with async graph data sources.
|
|
2187
|
+
*
|
|
2188
|
+
* Fetches both neighbourhoods, degrees, and node count concurrently.
|
|
2189
|
+
*/
|
|
2190
|
+
async function skewAsync(graph, source, target, config) {
|
|
2191
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2192
|
+
const [sourceArr, targetArr, N, sourceDegree, targetDegree] = await Promise.all([
|
|
2193
|
+
collectAsyncIterable(graph.neighbours(source)),
|
|
2194
|
+
collectAsyncIterable(graph.neighbours(target)),
|
|
2195
|
+
graph.nodeCount,
|
|
2196
|
+
graph.degree(source),
|
|
2197
|
+
graph.degree(target)
|
|
2198
|
+
]);
|
|
2199
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2200
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2201
|
+
let intersection = 0;
|
|
2202
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2203
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
2204
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
2205
|
+
const sourceIdf = Math.log(N / (sourceDegree + 1));
|
|
2206
|
+
const targetIdf = Math.log(N / (targetDegree + 1));
|
|
2207
|
+
const score = jaccardScore * sourceIdf * targetIdf;
|
|
2208
|
+
return Math.max(epsilon, score);
|
|
2209
|
+
}
|
|
1430
2210
|
//#endregion
|
|
1431
2211
|
//#region src/ranking/mi/span.ts
|
|
1432
2212
|
/**
|
|
@@ -1440,6 +2220,40 @@ function span(graph, source, target, config) {
|
|
|
1440
2220
|
const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
|
|
1441
2221
|
return Math.max(epsilon, score);
|
|
1442
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Async variant of SPAN MI for use with async graph data sources.
|
|
2225
|
+
*
|
|
2226
|
+
* Fetches both neighbourhoods concurrently, then computes the clustering
|
|
2227
|
+
* coefficient for each endpoint from the collected neighbour arrays.
|
|
2228
|
+
*/
|
|
2229
|
+
async function spanAsync(graph, source, target, config) {
|
|
2230
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2231
|
+
const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
|
|
2232
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2233
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2234
|
+
let intersection = 0;
|
|
2235
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2236
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
2237
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
2238
|
+
const computeClusteringCoefficient = async (nodeId, neighbourArr) => {
|
|
2239
|
+
const degree = neighbourArr.length;
|
|
2240
|
+
if (degree < 2) return 0;
|
|
2241
|
+
const pairs = [];
|
|
2242
|
+
for (let i = 0; i < neighbourArr.length; i++) for (let j = i + 1; j < neighbourArr.length; j++) {
|
|
2243
|
+
const u = neighbourArr[i];
|
|
2244
|
+
const v = neighbourArr[j];
|
|
2245
|
+
if (u !== void 0 && v !== void 0) pairs.push([u, v]);
|
|
2246
|
+
}
|
|
2247
|
+
const edgeResults = await Promise.all(pairs.flatMap(([u, v]) => [graph.getEdge(u, v), graph.getEdge(v, u)]));
|
|
2248
|
+
let triangleCount = 0;
|
|
2249
|
+
for (let i = 0; i < pairs.length; i++) if (edgeResults[2 * i] !== void 0 || edgeResults[2 * i + 1] !== void 0) triangleCount++;
|
|
2250
|
+
const possibleTriangles = degree * (degree - 1) / 2;
|
|
2251
|
+
return triangleCount / possibleTriangles;
|
|
2252
|
+
};
|
|
2253
|
+
const [sourceCc, targetCc] = await Promise.all([computeClusteringCoefficient(source, sourceArr), computeClusteringCoefficient(target, targetArr)]);
|
|
2254
|
+
const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
|
|
2255
|
+
return Math.max(epsilon, score);
|
|
2256
|
+
}
|
|
1443
2257
|
//#endregion
|
|
1444
2258
|
//#region src/ranking/mi/etch.ts
|
|
1445
2259
|
/**
|
|
@@ -1455,6 +2269,37 @@ function etch(graph, source, target, config) {
|
|
|
1455
2269
|
const score = jaccardScore * Math.log(graph.edgeCount / edgeTypeCount);
|
|
1456
2270
|
return Math.max(epsilon, score);
|
|
1457
2271
|
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Async variant of ETCH MI for use with async graph data sources.
|
|
2274
|
+
*
|
|
2275
|
+
* Fetches both neighbourhoods and edge data concurrently, then counts
|
|
2276
|
+
* edges of the same type by iterating the async edge stream.
|
|
2277
|
+
*/
|
|
2278
|
+
async function etchAsync(graph, source, target, config) {
|
|
2279
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2280
|
+
const [sourceArr, targetArr, edge] = await Promise.all([
|
|
2281
|
+
collectAsyncIterable(graph.neighbours(source)),
|
|
2282
|
+
collectAsyncIterable(graph.neighbours(target)),
|
|
2283
|
+
graph.getEdge(source, target)
|
|
2284
|
+
]);
|
|
2285
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2286
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2287
|
+
let intersection = 0;
|
|
2288
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2289
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
2290
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
2291
|
+
if (edge?.type === void 0) return Math.max(epsilon, jaccardScore);
|
|
2292
|
+
const edgeType = edge.type;
|
|
2293
|
+
let edgeTypeCount = 0;
|
|
2294
|
+
let totalEdges = 0;
|
|
2295
|
+
for await (const e of graph.edges()) {
|
|
2296
|
+
totalEdges++;
|
|
2297
|
+
if (e.type === edgeType) edgeTypeCount++;
|
|
2298
|
+
}
|
|
2299
|
+
if (edgeTypeCount === 0) return Math.max(epsilon, jaccardScore);
|
|
2300
|
+
const score = jaccardScore * Math.log(totalEdges / edgeTypeCount);
|
|
2301
|
+
return Math.max(epsilon, score);
|
|
2302
|
+
}
|
|
1458
2303
|
//#endregion
|
|
1459
2304
|
//#region src/ranking/mi/notch.ts
|
|
1460
2305
|
/**
|
|
@@ -1474,6 +2319,44 @@ function notch(graph, source, target, config) {
|
|
|
1474
2319
|
const score = jaccardScore * sourceRarity * targetRarity;
|
|
1475
2320
|
return Math.max(epsilon, score);
|
|
1476
2321
|
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Async variant of NOTCH MI for use with async graph data sources.
|
|
2324
|
+
*
|
|
2325
|
+
* Fetches both neighbourhoods and node data concurrently, then counts
|
|
2326
|
+
* nodes of each type by iterating the async node stream.
|
|
2327
|
+
*/
|
|
2328
|
+
async function notchAsync(graph, source, target, config) {
|
|
2329
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
2330
|
+
const [sourceArr, targetArr, sourceNode, targetNode] = await Promise.all([
|
|
2331
|
+
collectAsyncIterable(graph.neighbours(source)),
|
|
2332
|
+
collectAsyncIterable(graph.neighbours(target)),
|
|
2333
|
+
graph.getNode(source),
|
|
2334
|
+
graph.getNode(target)
|
|
2335
|
+
]);
|
|
2336
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2337
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2338
|
+
let intersection = 0;
|
|
2339
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2340
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
2341
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
2342
|
+
if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccardScore);
|
|
2343
|
+
const sourceType = sourceNode.type;
|
|
2344
|
+
const targetType = targetNode.type;
|
|
2345
|
+
let totalNodes = 0;
|
|
2346
|
+
let sourceTypeCount = 0;
|
|
2347
|
+
let targetTypeCount = 0;
|
|
2348
|
+
for await (const nodeId of graph.nodeIds()) {
|
|
2349
|
+
totalNodes++;
|
|
2350
|
+
const node = await graph.getNode(nodeId);
|
|
2351
|
+
if (node?.type === sourceType) sourceTypeCount++;
|
|
2352
|
+
if (node?.type === targetType) targetTypeCount++;
|
|
2353
|
+
}
|
|
2354
|
+
if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccardScore);
|
|
2355
|
+
const sourceRarity = Math.log(totalNodes / sourceTypeCount);
|
|
2356
|
+
const targetRarity = Math.log(totalNodes / targetTypeCount);
|
|
2357
|
+
const score = jaccardScore * sourceRarity * targetRarity;
|
|
2358
|
+
return Math.max(epsilon, score);
|
|
2359
|
+
}
|
|
1477
2360
|
//#endregion
|
|
1478
2361
|
//#region src/ranking/mi/adaptive.ts
|
|
1479
2362
|
/**
|
|
@@ -1506,6 +2389,38 @@ function adaptive(graph, source, target, config) {
|
|
|
1506
2389
|
const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
|
|
1507
2390
|
return Math.max(epsilon, Math.min(1, score));
|
|
1508
2391
|
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Async variant of Adaptive MI for use with async graph data sources.
|
|
2394
|
+
*
|
|
2395
|
+
* Fetches both neighbourhoods concurrently, then delegates degree-weighted
|
|
2396
|
+
* component to the async Adamic-Adar variant.
|
|
2397
|
+
*/
|
|
2398
|
+
async function adaptiveAsync(graph, source, target, config) {
|
|
2399
|
+
const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
|
|
2400
|
+
const [sourceArr, targetArr, degreeComponent] = await Promise.all([
|
|
2401
|
+
collectAsyncIterable(graph.neighbours(source)),
|
|
2402
|
+
collectAsyncIterable(graph.neighbours(target)),
|
|
2403
|
+
adamicAdarAsync(graph, source, target, {
|
|
2404
|
+
epsilon,
|
|
2405
|
+
normalise: true
|
|
2406
|
+
})
|
|
2407
|
+
]);
|
|
2408
|
+
const srcSet = new Set(sourceArr.filter((n) => n !== target));
|
|
2409
|
+
const tgtSet = new Set(targetArr.filter((n) => n !== source));
|
|
2410
|
+
let intersection = 0;
|
|
2411
|
+
for (const n of srcSet) if (tgtSet.has(n)) intersection++;
|
|
2412
|
+
const union = srcSet.size + tgtSet.size - intersection;
|
|
2413
|
+
const jaccardScore = union > 0 ? intersection / union : 0;
|
|
2414
|
+
const structural = srcSet.size === 0 && tgtSet.size === 0 ? 0 : Math.max(epsilon, jaccardScore);
|
|
2415
|
+
let overlap;
|
|
2416
|
+
if (srcSet.size > 0 && tgtSet.size > 0) {
|
|
2417
|
+
const minDegree = Math.min(srcSet.size, tgtSet.size);
|
|
2418
|
+
overlap = minDegree > 0 ? intersection / minDegree : epsilon;
|
|
2419
|
+
} else overlap = epsilon;
|
|
2420
|
+
const totalWeight = structuralWeight + degreeWeight + overlapWeight;
|
|
2421
|
+
const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
|
|
2422
|
+
return Math.max(epsilon, Math.min(1, score));
|
|
2423
|
+
}
|
|
1509
2424
|
//#endregion
|
|
1510
2425
|
//#region src/ranking/baselines/utils.ts
|
|
1511
2426
|
/**
|
|
@@ -3000,6 +3915,6 @@ function filterSubgraph(graph, options) {
|
|
|
3000
3915
|
return result;
|
|
3001
3916
|
}
|
|
3002
3917
|
//#endregion
|
|
3003
|
-
export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adaptive, approximateClusteringCoefficient, assertWebGPUAvailable, base, 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 };
|
|
3918
|
+
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 };
|
|
3004
3919
|
|
|
3005
3920
|
//# sourceMappingURL=index.js.map
|