graphwise 1.4.3 → 1.5.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/dist/__test__/fixtures/graphs/index.d.ts +1 -0
- package/dist/__test__/fixtures/graphs/index.d.ts.map +1 -1
- package/dist/__test__/fixtures/graphs/linear-chain.d.ts +18 -0
- package/dist/__test__/fixtures/graphs/linear-chain.d.ts.map +1 -0
- package/dist/__test__/fixtures/helpers.d.ts +20 -1
- package/dist/__test__/fixtures/helpers.d.ts.map +1 -1
- package/dist/__test__/fixtures/index.d.ts +1 -0
- package/dist/__test__/fixtures/index.d.ts.map +1 -1
- package/dist/__test__/fixtures/metrics.d.ts +86 -0
- package/dist/__test__/fixtures/metrics.d.ts.map +1 -0
- package/dist/__test__/fixtures/metrics.unit.test.d.ts +7 -0
- package/dist/__test__/fixtures/metrics.unit.test.d.ts.map +1 -0
- package/dist/expansion/dfs-priority.d.ts +23 -0
- package/dist/expansion/dfs-priority.d.ts.map +1 -0
- package/dist/expansion/dfs-priority.unit.test.d.ts +2 -0
- package/dist/expansion/dfs-priority.unit.test.d.ts.map +1 -0
- package/dist/expansion/edge.d.ts.map +1 -1
- package/dist/expansion/flux.d.ts.map +1 -1
- package/dist/expansion/fuse.d.ts.map +1 -1
- package/dist/expansion/index.d.ts +3 -0
- package/dist/expansion/index.d.ts.map +1 -1
- package/dist/expansion/k-hop.d.ts +26 -0
- package/dist/expansion/k-hop.d.ts.map +1 -0
- package/dist/expansion/k-hop.unit.test.d.ts +2 -0
- package/dist/expansion/k-hop.unit.test.d.ts.map +1 -0
- package/dist/expansion/lace.d.ts.map +1 -1
- package/dist/expansion/maze.d.ts.map +1 -1
- package/dist/expansion/priority-helpers.d.ts +43 -0
- package/dist/expansion/priority-helpers.d.ts.map +1 -0
- package/dist/expansion/random-walk.d.ts +35 -0
- package/dist/expansion/random-walk.d.ts.map +1 -0
- package/dist/expansion/random-walk.unit.test.d.ts +2 -0
- package/dist/expansion/random-walk.unit.test.d.ts.map +1 -0
- package/dist/expansion/sage.d.ts.map +1 -1
- package/dist/expansion/sift.d.ts.map +1 -1
- package/dist/expansion/warp.d.ts.map +1 -1
- package/dist/index/index.cjs +887 -297
- package/dist/index/index.cjs.map +1 -1
- package/dist/index/index.js +878 -299
- package/dist/index/index.js.map +1 -1
- package/dist/ranking/baselines/betweenness.d.ts.map +1 -1
- package/dist/ranking/baselines/communicability.d.ts.map +1 -1
- package/dist/ranking/baselines/degree-sum.d.ts.map +1 -1
- package/dist/ranking/baselines/hitting-time.d.ts +27 -0
- package/dist/ranking/baselines/hitting-time.d.ts.map +1 -0
- package/dist/ranking/baselines/hitting-time.unit.test.d.ts +2 -0
- package/dist/ranking/baselines/hitting-time.unit.test.d.ts.map +1 -0
- package/dist/ranking/baselines/index.d.ts +1 -0
- package/dist/ranking/baselines/index.d.ts.map +1 -1
- package/dist/ranking/baselines/jaccard-arithmetic.d.ts.map +1 -1
- package/dist/ranking/baselines/katz.d.ts.map +1 -1
- package/dist/ranking/baselines/pagerank.d.ts.map +1 -1
- package/dist/ranking/baselines/random-ranking.d.ts.map +1 -1
- package/dist/ranking/baselines/resistance-distance.d.ts.map +1 -1
- package/dist/ranking/baselines/shortest.d.ts.map +1 -1
- package/dist/ranking/baselines/utils.d.ts +20 -0
- package/dist/ranking/baselines/utils.d.ts.map +1 -0
- package/dist/ranking/baselines/widest-path.d.ts.map +1 -1
- package/dist/ranking/mi/adaptive.d.ts.map +1 -1
- package/dist/ranking/mi/cosine.d.ts +13 -0
- package/dist/ranking/mi/cosine.d.ts.map +1 -0
- package/dist/ranking/mi/cosine.unit.test.d.ts +2 -0
- package/dist/ranking/mi/cosine.unit.test.d.ts.map +1 -0
- package/dist/ranking/mi/etch.d.ts.map +1 -1
- package/dist/ranking/mi/hub-promoted.d.ts +13 -0
- package/dist/ranking/mi/hub-promoted.d.ts.map +1 -0
- package/dist/ranking/mi/hub-promoted.unit.test.d.ts +2 -0
- package/dist/ranking/mi/hub-promoted.unit.test.d.ts.map +1 -0
- package/dist/ranking/mi/index.d.ts +5 -0
- package/dist/ranking/mi/index.d.ts.map +1 -1
- package/dist/ranking/mi/jaccard.d.ts.map +1 -1
- package/dist/ranking/mi/notch.d.ts.map +1 -1
- package/dist/ranking/mi/overlap-coefficient.d.ts +13 -0
- package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -0
- package/dist/ranking/mi/overlap-coefficient.unit.test.d.ts +2 -0
- package/dist/ranking/mi/overlap-coefficient.unit.test.d.ts.map +1 -0
- package/dist/ranking/mi/resource-allocation.d.ts +13 -0
- package/dist/ranking/mi/resource-allocation.d.ts.map +1 -0
- package/dist/ranking/mi/resource-allocation.unit.test.d.ts +2 -0
- package/dist/ranking/mi/resource-allocation.unit.test.d.ts.map +1 -0
- package/dist/ranking/mi/scale.d.ts.map +1 -1
- package/dist/ranking/mi/skew.d.ts.map +1 -1
- package/dist/ranking/mi/sorensen.d.ts +13 -0
- package/dist/ranking/mi/sorensen.d.ts.map +1 -0
- package/dist/ranking/mi/sorensen.unit.test.d.ts +2 -0
- package/dist/ranking/mi/sorensen.unit.test.d.ts.map +1 -0
- package/dist/ranking/mi/span.d.ts.map +1 -1
- package/dist/ranking/mi/types.d.ts +1 -1
- package/dist/ranking/mi/types.d.ts.map +1 -1
- package/dist/schemas/graph.d.ts +1 -1
- package/dist/utils/index.cjs +22 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +22 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/neighbours.d.ts +23 -0
- package/dist/utils/neighbours.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index/index.cjs
CHANGED
|
@@ -24,7 +24,7 @@ function degreePriority(_nodeId, context) {
|
|
|
24
24
|
function base(graph, seeds, config) {
|
|
25
25
|
const startTime = performance.now();
|
|
26
26
|
const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
|
|
27
|
-
if (seeds.length === 0) return emptyResult("base", startTime);
|
|
27
|
+
if (seeds.length === 0) return emptyResult$2("base", startTime);
|
|
28
28
|
const numFrontiers = seeds.length;
|
|
29
29
|
const allVisited = /* @__PURE__ */ new Set();
|
|
30
30
|
const combinedVisited = /* @__PURE__ */ new Map();
|
|
@@ -105,7 +105,7 @@ function base(graph, seeds, config) {
|
|
|
105
105
|
const otherVisited = visitedByFrontier[otherFrontier];
|
|
106
106
|
if (otherVisited === void 0) continue;
|
|
107
107
|
if (otherVisited.has(nodeId)) {
|
|
108
|
-
const path = reconstructPath(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
|
|
108
|
+
const path = reconstructPath$1(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
|
|
109
109
|
if (path !== null) {
|
|
110
110
|
discoveredPaths.push(path);
|
|
111
111
|
if (debug) console.log(`[BASE] Path found: ${path.nodes.join(" -> ")}`);
|
|
@@ -170,7 +170,7 @@ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, al
|
|
|
170
170
|
/**
|
|
171
171
|
* Reconstruct path from collision point.
|
|
172
172
|
*/
|
|
173
|
-
function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seeds) {
|
|
173
|
+
function reconstructPath$1(collisionNode, frontierA, frontierB, predecessors, seeds) {
|
|
174
174
|
const pathA = [collisionNode];
|
|
175
175
|
const predA = predecessors[frontierA];
|
|
176
176
|
if (predA !== void 0) {
|
|
@@ -206,7 +206,7 @@ function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seed
|
|
|
206
206
|
/**
|
|
207
207
|
* Create an empty result for early termination.
|
|
208
208
|
*/
|
|
209
|
-
function emptyResult(algorithm, startTime) {
|
|
209
|
+
function emptyResult$2(algorithm, startTime) {
|
|
210
210
|
return {
|
|
211
211
|
paths: [],
|
|
212
212
|
sampledNodes: /* @__PURE__ */ new Set(),
|
|
@@ -255,46 +255,12 @@ function domeHighDegree(graph, seeds, config) {
|
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
//#endregion
|
|
258
|
-
//#region src/expansion/edge.ts
|
|
259
|
-
var EPSILON$1 = 1e-10;
|
|
260
|
-
/**
|
|
261
|
-
* Priority function using local type entropy.
|
|
262
|
-
* Lower values = higher priority (expanded first).
|
|
263
|
-
*/
|
|
264
|
-
function edgePriority(nodeId, context) {
|
|
265
|
-
const graph = context.graph;
|
|
266
|
-
const neighbours = graph.neighbours(nodeId);
|
|
267
|
-
const neighbourTypes = [];
|
|
268
|
-
for (const neighbour of neighbours) {
|
|
269
|
-
const node = graph.getNode(neighbour);
|
|
270
|
-
neighbourTypes.push(node?.type ?? "default");
|
|
271
|
-
}
|
|
272
|
-
return 1 / (require_utils.localTypeEntropy(neighbourTypes) + EPSILON$1) * Math.log(context.degree + 1);
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Run EDGE expansion (Entropy-Driven Graph Expansion).
|
|
276
|
-
*
|
|
277
|
-
* Discovers paths by prioritising nodes with diverse neighbour types,
|
|
278
|
-
* deferring nodes with homogeneous neighbourhoods.
|
|
279
|
-
*
|
|
280
|
-
* @param graph - Source graph
|
|
281
|
-
* @param seeds - Seed nodes for expansion
|
|
282
|
-
* @param config - Expansion configuration
|
|
283
|
-
* @returns Expansion result with discovered paths
|
|
284
|
-
*/
|
|
285
|
-
function edge(graph, seeds, config) {
|
|
286
|
-
return base(graph, seeds, {
|
|
287
|
-
...config,
|
|
288
|
-
priority: edgePriority
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
//#endregion
|
|
292
258
|
//#region src/expansion/hae.ts
|
|
293
259
|
var EPSILON = 1e-10;
|
|
294
260
|
/**
|
|
295
261
|
* Default type mapper - uses node.type property.
|
|
296
262
|
*/
|
|
297
|
-
function defaultTypeMapper(node) {
|
|
263
|
+
function defaultTypeMapper$1(node) {
|
|
298
264
|
return node.type ?? "default";
|
|
299
265
|
}
|
|
300
266
|
/**
|
|
@@ -324,13 +290,34 @@ function createHAEPriority(typeMapper) {
|
|
|
324
290
|
* @returns Expansion result with discovered paths
|
|
325
291
|
*/
|
|
326
292
|
function hae(graph, seeds, config) {
|
|
327
|
-
const typeMapper = config?.typeMapper ?? defaultTypeMapper;
|
|
293
|
+
const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
|
|
328
294
|
return base(graph, seeds, {
|
|
329
295
|
...config,
|
|
330
296
|
priority: createHAEPriority(typeMapper)
|
|
331
297
|
});
|
|
332
298
|
}
|
|
333
299
|
//#endregion
|
|
300
|
+
//#region src/expansion/edge.ts
|
|
301
|
+
/** Default type mapper: reads `node.type`, falling back to "default". */
|
|
302
|
+
var defaultTypeMapper = (n) => typeof n.type === "string" ? n.type : "default";
|
|
303
|
+
/**
|
|
304
|
+
* Run EDGE expansion (Entropy-Driven Graph Expansion).
|
|
305
|
+
*
|
|
306
|
+
* Discovers paths by prioritising nodes with diverse neighbour types,
|
|
307
|
+
* deferring nodes with homogeneous neighbourhoods.
|
|
308
|
+
*
|
|
309
|
+
* @param graph - Source graph
|
|
310
|
+
* @param seeds - Seed nodes for expansion
|
|
311
|
+
* @param config - Expansion configuration
|
|
312
|
+
* @returns Expansion result with discovered paths
|
|
313
|
+
*/
|
|
314
|
+
function edge(graph, seeds, config) {
|
|
315
|
+
return hae(graph, seeds, {
|
|
316
|
+
...config,
|
|
317
|
+
typeMapper: defaultTypeMapper
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
334
321
|
//#region src/expansion/pipe.ts
|
|
335
322
|
/**
|
|
336
323
|
* Priority function using path potential.
|
|
@@ -366,6 +353,69 @@ function pipe(graph, seeds, config) {
|
|
|
366
353
|
});
|
|
367
354
|
}
|
|
368
355
|
//#endregion
|
|
356
|
+
//#region src/expansion/priority-helpers.ts
|
|
357
|
+
/**
|
|
358
|
+
* Compute the average mutual information between a node and all visited
|
|
359
|
+
* nodes in the same frontier.
|
|
360
|
+
*
|
|
361
|
+
* Returns a value in [0, 1] — higher means the node is more similar
|
|
362
|
+
* (on average) to already-visited same-frontier nodes.
|
|
363
|
+
*
|
|
364
|
+
* @param graph - Source graph
|
|
365
|
+
* @param nodeId - Node being prioritised
|
|
366
|
+
* @param context - Current priority context
|
|
367
|
+
* @param mi - MI function to use for pairwise scoring
|
|
368
|
+
* @returns Average MI score, or 0 if no same-frontier visited nodes exist
|
|
369
|
+
*/
|
|
370
|
+
function avgFrontierMI(graph, nodeId, context, mi) {
|
|
371
|
+
const { frontierIndex, visitedByFrontier } = context;
|
|
372
|
+
let total = 0;
|
|
373
|
+
let count = 0;
|
|
374
|
+
for (const [visitedId, idx] of visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
|
|
375
|
+
total += mi(graph, visitedId, nodeId);
|
|
376
|
+
count++;
|
|
377
|
+
}
|
|
378
|
+
return count > 0 ? total / count : 0;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Count the number of a node's neighbours that have been visited by
|
|
382
|
+
* frontiers other than the node's own frontier.
|
|
383
|
+
*
|
|
384
|
+
* A higher count indicates this node is likely to bridge two frontiers,
|
|
385
|
+
* making it a strong candidate for path completion.
|
|
386
|
+
*
|
|
387
|
+
* @param graph - Source graph
|
|
388
|
+
* @param nodeId - Node being evaluated
|
|
389
|
+
* @param context - Current priority context
|
|
390
|
+
* @returns Number of neighbours visited by other frontiers
|
|
391
|
+
*/
|
|
392
|
+
function countCrossFrontierNeighbours(graph, nodeId, context) {
|
|
393
|
+
const { frontierIndex, visitedByFrontier } = context;
|
|
394
|
+
const nodeNeighbours = new Set(graph.neighbours(nodeId));
|
|
395
|
+
let count = 0;
|
|
396
|
+
for (const [visitedId, idx] of visitedByFrontier) if (idx !== frontierIndex && nodeNeighbours.has(visitedId)) count++;
|
|
397
|
+
return count;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Incrementally update salience counts for paths discovered since the
|
|
401
|
+
* last update.
|
|
402
|
+
*
|
|
403
|
+
* Iterates only over paths from `fromIndex` onwards, avoiding redundant
|
|
404
|
+
* re-processing of already-counted paths.
|
|
405
|
+
*
|
|
406
|
+
* @param salienceCounts - Mutable map of node ID to salience count (mutated in place)
|
|
407
|
+
* @param paths - Full list of discovered paths
|
|
408
|
+
* @param fromIndex - Index to start counting from (exclusive of earlier paths)
|
|
409
|
+
* @returns The new `fromIndex` value (i.e. `paths.length` after update)
|
|
410
|
+
*/
|
|
411
|
+
function updateSalienceCounts(salienceCounts, paths, fromIndex) {
|
|
412
|
+
for (let i = fromIndex; i < paths.length; i++) {
|
|
413
|
+
const path = paths[i];
|
|
414
|
+
if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
|
|
415
|
+
}
|
|
416
|
+
return paths.length;
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
369
419
|
//#region src/expansion/sage.ts
|
|
370
420
|
/**
|
|
371
421
|
* Run SAGE expansion algorithm.
|
|
@@ -389,13 +439,7 @@ function sage(graph, seeds, config) {
|
|
|
389
439
|
function sagePriority(nodeId, context) {
|
|
390
440
|
const pathCount = context.discoveredPaths.length;
|
|
391
441
|
if (pathCount > 0 && !inPhase2) inPhase2 = true;
|
|
392
|
-
if (pathCount > lastPathCount)
|
|
393
|
-
for (let i = lastPathCount; i < pathCount; i++) {
|
|
394
|
-
const path = context.discoveredPaths[i];
|
|
395
|
-
if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
|
|
396
|
-
}
|
|
397
|
-
lastPathCount = pathCount;
|
|
398
|
-
}
|
|
442
|
+
if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
399
443
|
if (!inPhase2) return Math.log(context.degree + 1);
|
|
400
444
|
return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
|
|
401
445
|
}
|
|
@@ -417,10 +461,9 @@ function sage(graph, seeds, config) {
|
|
|
417
461
|
*/
|
|
418
462
|
function jaccard(graph, source, target, config) {
|
|
419
463
|
const { epsilon = 1e-10 } = config ?? {};
|
|
420
|
-
const {
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
return Math.max(epsilon, score);
|
|
464
|
+
const { jaccard: jaccardScore, sourceNeighbours, targetNeighbours } = require_utils.computeJaccard(graph, source, target);
|
|
465
|
+
if (sourceNeighbours.size === 0 && targetNeighbours.size === 0) return 0;
|
|
466
|
+
return Math.max(epsilon, jaccardScore);
|
|
424
467
|
}
|
|
425
468
|
//#endregion
|
|
426
469
|
//#region src/expansion/reach.ts
|
|
@@ -506,15 +549,9 @@ function maze(graph, seeds, config) {
|
|
|
506
549
|
const pathCount = context.discoveredPaths.length;
|
|
507
550
|
if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
|
|
508
551
|
inPhase2 = true;
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
if (inPhase2 && pathCount > lastPathCount) {
|
|
512
|
-
for (let i = lastPathCount; i < pathCount; i++) {
|
|
513
|
-
const path = context.discoveredPaths[i];
|
|
514
|
-
if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
|
|
515
|
-
}
|
|
516
|
-
lastPathCount = pathCount;
|
|
552
|
+
updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
|
|
517
553
|
}
|
|
554
|
+
if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
|
|
518
555
|
const nodeNeighbours = graph.neighbours(nodeId);
|
|
519
556
|
let pathPotential = 0;
|
|
520
557
|
for (const neighbour of nodeNeighbours) {
|
|
@@ -566,22 +603,11 @@ function tide(graph, seeds, config) {
|
|
|
566
603
|
/**
|
|
567
604
|
* LACE priority function.
|
|
568
605
|
*
|
|
569
|
-
* Priority = 1 -
|
|
570
|
-
* Higher MI = lower priority value = explored first
|
|
606
|
+
* Priority = 1 - avgMI(node, same-frontier visited nodes)
|
|
607
|
+
* Higher average MI = lower priority value = explored first
|
|
571
608
|
*/
|
|
572
609
|
function lacePriority(nodeId, context, mi) {
|
|
573
|
-
|
|
574
|
-
const frontierIndex = context.frontierIndex;
|
|
575
|
-
let maxMi = 0;
|
|
576
|
-
let totalMi = 0;
|
|
577
|
-
let count = 0;
|
|
578
|
-
for (const [visitedId, idx] of context.visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
|
|
579
|
-
const edgeMi = mi(graph, visitedId, nodeId);
|
|
580
|
-
totalMi += edgeMi;
|
|
581
|
-
count++;
|
|
582
|
-
if (edgeMi > maxMi) maxMi = edgeMi;
|
|
583
|
-
}
|
|
584
|
-
return 1 - (count > 0 ? totalMi / count : 0);
|
|
610
|
+
return 1 - avgFrontierMI(context.graph, nodeId, context, mi);
|
|
585
611
|
}
|
|
586
612
|
/**
|
|
587
613
|
* Run LACE expansion algorithm.
|
|
@@ -605,18 +631,15 @@ function lace(graph, seeds, config) {
|
|
|
605
631
|
//#endregion
|
|
606
632
|
//#region src/expansion/warp.ts
|
|
607
633
|
/**
|
|
608
|
-
*
|
|
634
|
+
* WARP priority function.
|
|
609
635
|
*
|
|
610
636
|
* Priority = 1 / (1 + bridge_score)
|
|
611
|
-
* Bridge score =
|
|
612
|
-
*
|
|
637
|
+
* Bridge score = cross-frontier neighbour count plus bonus for nodes
|
|
638
|
+
* already on discovered paths.
|
|
639
|
+
* Higher bridge score = more likely to complete paths = explored first.
|
|
613
640
|
*/
|
|
614
641
|
function warpPriority(nodeId, context) {
|
|
615
|
-
|
|
616
|
-
const currentFrontier = context.frontierIndex;
|
|
617
|
-
const nodeNeighbours = new Set(graph.neighbours(nodeId));
|
|
618
|
-
let bridgeScore = 0;
|
|
619
|
-
for (const [visitedId, frontierIdx] of context.visitedByFrontier) if (frontierIdx !== currentFrontier && nodeNeighbours.has(visitedId)) bridgeScore++;
|
|
642
|
+
let bridgeScore = countCrossFrontierNeighbours(context.graph, nodeId, context);
|
|
620
643
|
for (const path of context.discoveredPaths) if (path.nodes.includes(nodeId)) bridgeScore += 2;
|
|
621
644
|
return 1 / (1 + bridgeScore);
|
|
622
645
|
}
|
|
@@ -640,24 +663,15 @@ function warp(graph, seeds, config) {
|
|
|
640
663
|
//#endregion
|
|
641
664
|
//#region src/expansion/fuse.ts
|
|
642
665
|
/**
|
|
643
|
-
*
|
|
666
|
+
* FUSE priority function.
|
|
644
667
|
*
|
|
645
|
-
* Combines degree with salience:
|
|
646
|
-
* Priority = (1 - w) * degree + w * (1 -
|
|
647
|
-
* Lower values = higher priority
|
|
668
|
+
* Combines degree with average frontier MI as a salience proxy:
|
|
669
|
+
* Priority = (1 - w) * degree + w * (1 - avgMI)
|
|
670
|
+
* Lower values = higher priority; high salience lowers priority
|
|
648
671
|
*/
|
|
649
672
|
function fusePriority(nodeId, context, mi, salienceWeight) {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
const frontierIndex = context.frontierIndex;
|
|
653
|
-
let totalSalience = 0;
|
|
654
|
-
let count = 0;
|
|
655
|
-
for (const [visitedId, idx] of context.visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
|
|
656
|
-
totalSalience += mi(graph, visitedId, nodeId);
|
|
657
|
-
count++;
|
|
658
|
-
}
|
|
659
|
-
const avgSalience = count > 0 ? totalSalience / count : 0;
|
|
660
|
-
return (1 - salienceWeight) * degree + salienceWeight * (1 - avgSalience);
|
|
673
|
+
const avgSalience = avgFrontierMI(context.graph, nodeId, context, mi);
|
|
674
|
+
return (1 - salienceWeight) * context.degree + salienceWeight * (1 - avgSalience);
|
|
661
675
|
}
|
|
662
676
|
/**
|
|
663
677
|
* Run FUSE expansion algorithm.
|
|
@@ -681,20 +695,13 @@ function fuse(graph, seeds, config) {
|
|
|
681
695
|
//#endregion
|
|
682
696
|
//#region src/expansion/sift.ts
|
|
683
697
|
/**
|
|
684
|
-
* REACH priority function
|
|
698
|
+
* REACH (SIFT) priority function.
|
|
685
699
|
*
|
|
686
|
-
*
|
|
700
|
+
* Prioritises nodes with average frontier MI above the threshold;
|
|
701
|
+
* falls back to degree-based ordering for those below it.
|
|
687
702
|
*/
|
|
688
703
|
function siftPriority(nodeId, context, mi, miThreshold) {
|
|
689
|
-
const
|
|
690
|
-
const frontierIndex = context.frontierIndex;
|
|
691
|
-
let totalMi = 0;
|
|
692
|
-
let count = 0;
|
|
693
|
-
for (const [visitedId, idx] of context.visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
|
|
694
|
-
totalMi += mi(graph, visitedId, nodeId);
|
|
695
|
-
count++;
|
|
696
|
-
}
|
|
697
|
-
const avgMi = count > 0 ? totalMi / count : 0;
|
|
704
|
+
const avgMi = avgFrontierMI(context.graph, nodeId, context, mi);
|
|
698
705
|
if (avgMi >= miThreshold) return 1 - avgMi;
|
|
699
706
|
else return context.degree + 100;
|
|
700
707
|
}
|
|
@@ -736,16 +743,6 @@ function localDensity(graph, nodeId) {
|
|
|
736
743
|
return edges / maxEdges;
|
|
737
744
|
}
|
|
738
745
|
/**
|
|
739
|
-
* Compute bridge score (how many other frontiers visit neighbours).
|
|
740
|
-
*/
|
|
741
|
-
function bridgeScore(nodeId, context) {
|
|
742
|
-
const currentFrontier = context.frontierIndex;
|
|
743
|
-
const nodeNeighbours = new Set(context.graph.neighbours(nodeId));
|
|
744
|
-
let score = 0;
|
|
745
|
-
for (const [visitedId, idx] of context.visitedByFrontier) if (idx !== currentFrontier && nodeNeighbours.has(visitedId)) score++;
|
|
746
|
-
return score;
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
746
|
* MAZE adaptive priority function.
|
|
750
747
|
*
|
|
751
748
|
* Switches strategies based on local conditions:
|
|
@@ -757,10 +754,10 @@ function fluxPriority(nodeId, context, densityThreshold, bridgeThreshold) {
|
|
|
757
754
|
const graph = context.graph;
|
|
758
755
|
const degree = context.degree;
|
|
759
756
|
const density = localDensity(graph, nodeId);
|
|
760
|
-
const bridge =
|
|
757
|
+
const bridge = countCrossFrontierNeighbours(graph, nodeId, context);
|
|
761
758
|
const numFrontiers = new Set(context.visitedByFrontier.values()).size;
|
|
762
759
|
if ((numFrontiers > 0 ? bridge / numFrontiers : 0) >= bridgeThreshold) return 1 / (1 + bridge);
|
|
763
|
-
else if (density >= densityThreshold) return
|
|
760
|
+
else if (density >= densityThreshold) return 1 / (degree + 1);
|
|
764
761
|
else return degree;
|
|
765
762
|
}
|
|
766
763
|
/**
|
|
@@ -858,6 +855,346 @@ function randomPriority(graph, seeds, config) {
|
|
|
858
855
|
});
|
|
859
856
|
}
|
|
860
857
|
//#endregion
|
|
858
|
+
//#region src/expansion/dfs-priority.ts
|
|
859
|
+
/**
|
|
860
|
+
* DFS priority function: negative iteration produces LIFO ordering.
|
|
861
|
+
*
|
|
862
|
+
* Lower priority values are expanded first, so negating the iteration
|
|
863
|
+
* counter ensures the most recently enqueued node is always next.
|
|
864
|
+
*/
|
|
865
|
+
function dfsPriorityFn(_nodeId, context) {
|
|
866
|
+
return -context.iteration;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Run DFS-priority expansion (LIFO discovery order).
|
|
870
|
+
*
|
|
871
|
+
* Uses the BASE framework with a negative-iteration priority function,
|
|
872
|
+
* which causes the most recently discovered node to be expanded first —
|
|
873
|
+
* equivalent to depth-first search behaviour.
|
|
874
|
+
*
|
|
875
|
+
* @param graph - Source graph
|
|
876
|
+
* @param seeds - Seed nodes for expansion
|
|
877
|
+
* @param config - Expansion configuration
|
|
878
|
+
* @returns Expansion result with discovered paths
|
|
879
|
+
*/
|
|
880
|
+
function dfsPriority(graph, seeds, config) {
|
|
881
|
+
return base(graph, seeds, {
|
|
882
|
+
...config,
|
|
883
|
+
priority: dfsPriorityFn
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/expansion/k-hop.ts
|
|
888
|
+
/**
|
|
889
|
+
* Run k-hop expansion (fixed-depth BFS).
|
|
890
|
+
*
|
|
891
|
+
* Explores all nodes reachable within exactly k hops of any seed using
|
|
892
|
+
* breadth-first search. Paths between seeds are detected when a node
|
|
893
|
+
* is reached by frontiers from two different seeds.
|
|
894
|
+
*
|
|
895
|
+
* @param graph - Source graph
|
|
896
|
+
* @param seeds - Seed nodes for expansion
|
|
897
|
+
* @param config - K-hop configuration (k defaults to 2)
|
|
898
|
+
* @returns Expansion result with discovered paths
|
|
899
|
+
*/
|
|
900
|
+
function kHop(graph, seeds, config) {
|
|
901
|
+
const startTime = performance.now();
|
|
902
|
+
const { k = 2 } = config ?? {};
|
|
903
|
+
if (seeds.length === 0) return emptyResult$1(startTime);
|
|
904
|
+
const visitedByFrontier = seeds.map(() => /* @__PURE__ */ new Map());
|
|
905
|
+
const firstVisitedBy = /* @__PURE__ */ new Map();
|
|
906
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
907
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
908
|
+
const discoveredPaths = [];
|
|
909
|
+
let iterations = 0;
|
|
910
|
+
let edgesTraversed = 0;
|
|
911
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
912
|
+
const seed = seeds[i];
|
|
913
|
+
if (seed === void 0) continue;
|
|
914
|
+
if (!graph.hasNode(seed.id)) continue;
|
|
915
|
+
visitedByFrontier[i]?.set(seed.id, null);
|
|
916
|
+
allVisited.add(seed.id);
|
|
917
|
+
if (!firstVisitedBy.has(seed.id)) firstVisitedBy.set(seed.id, i);
|
|
918
|
+
else {
|
|
919
|
+
const otherIdx = firstVisitedBy.get(seed.id) ?? -1;
|
|
920
|
+
if (otherIdx < 0) continue;
|
|
921
|
+
const fromSeed = seeds[otherIdx];
|
|
922
|
+
const toSeed = seeds[i];
|
|
923
|
+
if (fromSeed !== void 0 && toSeed !== void 0) discoveredPaths.push({
|
|
924
|
+
fromSeed,
|
|
925
|
+
toSeed,
|
|
926
|
+
nodes: [seed.id]
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
let currentLevel = seeds.map((s, i) => {
|
|
931
|
+
const frontier = visitedByFrontier[i];
|
|
932
|
+
if (frontier === void 0) return [];
|
|
933
|
+
return frontier.has(s.id) ? [s.id] : [];
|
|
934
|
+
});
|
|
935
|
+
for (let hop = 0; hop < k; hop++) {
|
|
936
|
+
const nextLevel = seeds.map(() => []);
|
|
937
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
938
|
+
const level = currentLevel[i];
|
|
939
|
+
if (level === void 0) continue;
|
|
940
|
+
const frontierVisited = visitedByFrontier[i];
|
|
941
|
+
if (frontierVisited === void 0) continue;
|
|
942
|
+
for (const nodeId of level) {
|
|
943
|
+
iterations++;
|
|
944
|
+
for (const neighbour of graph.neighbours(nodeId)) {
|
|
945
|
+
edgesTraversed++;
|
|
946
|
+
const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
|
|
947
|
+
let targets = sampledEdgeMap.get(s);
|
|
948
|
+
if (targets === void 0) {
|
|
949
|
+
targets = /* @__PURE__ */ new Set();
|
|
950
|
+
sampledEdgeMap.set(s, targets);
|
|
951
|
+
}
|
|
952
|
+
targets.add(t);
|
|
953
|
+
if (frontierVisited.has(neighbour)) continue;
|
|
954
|
+
frontierVisited.set(neighbour, nodeId);
|
|
955
|
+
allVisited.add(neighbour);
|
|
956
|
+
nextLevel[i]?.push(neighbour);
|
|
957
|
+
const previousFrontier = firstVisitedBy.get(neighbour);
|
|
958
|
+
if (previousFrontier !== void 0 && previousFrontier !== i) {
|
|
959
|
+
const fromSeed = seeds[previousFrontier];
|
|
960
|
+
const toSeed = seeds[i];
|
|
961
|
+
if (fromSeed !== void 0 && toSeed !== void 0) {
|
|
962
|
+
const path = reconstructPath(neighbour, previousFrontier, i, visitedByFrontier, seeds);
|
|
963
|
+
if (path !== null) {
|
|
964
|
+
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);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (!firstVisitedBy.has(neighbour)) firstVisitedBy.set(neighbour, i);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
currentLevel = nextLevel;
|
|
973
|
+
if (currentLevel.every((level) => level.length === 0)) break;
|
|
974
|
+
}
|
|
975
|
+
const endTime = performance.now();
|
|
976
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
977
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
978
|
+
return {
|
|
979
|
+
paths: discoveredPaths,
|
|
980
|
+
sampledNodes: allVisited,
|
|
981
|
+
sampledEdges: edgeTuples,
|
|
982
|
+
visitedPerFrontier: visitedByFrontier.map((m) => new Set(m.keys())),
|
|
983
|
+
stats: {
|
|
984
|
+
iterations,
|
|
985
|
+
nodesVisited: allVisited.size,
|
|
986
|
+
edgesTraversed,
|
|
987
|
+
pathsFound: discoveredPaths.length,
|
|
988
|
+
durationMs: endTime - startTime,
|
|
989
|
+
algorithm: "k-hop",
|
|
990
|
+
termination: "exhausted"
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Reconstruct the path between two colliding frontiers.
|
|
996
|
+
*/
|
|
997
|
+
function reconstructPath(collisionNode, frontierA, frontierB, visitedByFrontier, seeds) {
|
|
998
|
+
const seedA = seeds[frontierA];
|
|
999
|
+
const seedB = seeds[frontierB];
|
|
1000
|
+
if (seedA === void 0 || seedB === void 0) return null;
|
|
1001
|
+
const pathA = [collisionNode];
|
|
1002
|
+
const predA = visitedByFrontier[frontierA];
|
|
1003
|
+
if (predA !== void 0) {
|
|
1004
|
+
let node = collisionNode;
|
|
1005
|
+
let pred = predA.get(node);
|
|
1006
|
+
while (pred !== null && pred !== void 0) {
|
|
1007
|
+
pathA.unshift(pred);
|
|
1008
|
+
node = pred;
|
|
1009
|
+
pred = predA.get(node);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const pathB = [];
|
|
1013
|
+
const predB = visitedByFrontier[frontierB];
|
|
1014
|
+
if (predB !== void 0) {
|
|
1015
|
+
let node = collisionNode;
|
|
1016
|
+
let pred = predB.get(node);
|
|
1017
|
+
while (pred !== null && pred !== void 0) {
|
|
1018
|
+
pathB.push(pred);
|
|
1019
|
+
node = pred;
|
|
1020
|
+
pred = predB.get(node);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
fromSeed: seedA,
|
|
1025
|
+
toSeed: seedB,
|
|
1026
|
+
nodes: [...pathA, ...pathB]
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Create an empty result for early termination (no seeds).
|
|
1031
|
+
*/
|
|
1032
|
+
function emptyResult$1(startTime) {
|
|
1033
|
+
return {
|
|
1034
|
+
paths: [],
|
|
1035
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
1036
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
1037
|
+
visitedPerFrontier: [],
|
|
1038
|
+
stats: {
|
|
1039
|
+
iterations: 0,
|
|
1040
|
+
nodesVisited: 0,
|
|
1041
|
+
edgesTraversed: 0,
|
|
1042
|
+
pathsFound: 0,
|
|
1043
|
+
durationMs: performance.now() - startTime,
|
|
1044
|
+
algorithm: "k-hop",
|
|
1045
|
+
termination: "exhausted"
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/expansion/random-walk.ts
|
|
1051
|
+
/**
|
|
1052
|
+
* Mulberry32 seeded PRNG — fast, compact, and high-quality for simulation.
|
|
1053
|
+
*
|
|
1054
|
+
* Returns a closure that yields the next pseudo-random value in [0, 1)
|
|
1055
|
+
* on each call.
|
|
1056
|
+
*
|
|
1057
|
+
* @param seed - 32-bit integer seed
|
|
1058
|
+
*/
|
|
1059
|
+
function mulberry32(seed) {
|
|
1060
|
+
let s = seed;
|
|
1061
|
+
return () => {
|
|
1062
|
+
s += 1831565813;
|
|
1063
|
+
let t = s;
|
|
1064
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1065
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1066
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Run random-walk-with-restart expansion.
|
|
1071
|
+
*
|
|
1072
|
+
* For each seed, performs `walks` independent random walks of up to
|
|
1073
|
+
* `walkLength` steps. At each step the walk either restarts (with
|
|
1074
|
+
* probability `restartProbability`) or moves to a uniformly sampled
|
|
1075
|
+
* neighbour. All visited nodes and traversed edges are collected.
|
|
1076
|
+
*
|
|
1077
|
+
* Inter-seed paths are detected when a walk reaches a node that was
|
|
1078
|
+
* previously reached by a walk originating from a different seed.
|
|
1079
|
+
* The recorded path contains only the two seed endpoints rather than
|
|
1080
|
+
* the full walk trajectory, consistent with the ExpansionPath contract.
|
|
1081
|
+
*
|
|
1082
|
+
* @param graph - Source graph
|
|
1083
|
+
* @param seeds - Seed nodes for expansion
|
|
1084
|
+
* @param config - Random walk configuration
|
|
1085
|
+
* @returns Expansion result with discovered paths
|
|
1086
|
+
*/
|
|
1087
|
+
function randomWalk(graph, seeds, config) {
|
|
1088
|
+
const startTime = performance.now();
|
|
1089
|
+
const { restartProbability = .15, walks = 10, walkLength = 20, seed = 0 } = config ?? {};
|
|
1090
|
+
if (seeds.length === 0) return emptyResult(startTime);
|
|
1091
|
+
const rand = mulberry32(seed);
|
|
1092
|
+
const firstVisitedBySeed = /* @__PURE__ */ new Map();
|
|
1093
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
1094
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
1095
|
+
const discoveredPaths = [];
|
|
1096
|
+
let iterations = 0;
|
|
1097
|
+
let edgesTraversed = 0;
|
|
1098
|
+
const visitedPerFrontier = seeds.map(() => /* @__PURE__ */ new Set());
|
|
1099
|
+
for (let seedIdx = 0; seedIdx < seeds.length; seedIdx++) {
|
|
1100
|
+
const seed_ = seeds[seedIdx];
|
|
1101
|
+
if (seed_ === void 0) continue;
|
|
1102
|
+
const seedId = seed_.id;
|
|
1103
|
+
if (!graph.hasNode(seedId)) continue;
|
|
1104
|
+
if (!firstVisitedBySeed.has(seedId)) firstVisitedBySeed.set(seedId, seedIdx);
|
|
1105
|
+
allVisited.add(seedId);
|
|
1106
|
+
visitedPerFrontier[seedIdx]?.add(seedId);
|
|
1107
|
+
for (let w = 0; w < walks; w++) {
|
|
1108
|
+
let current = seedId;
|
|
1109
|
+
for (let step = 0; step < walkLength; step++) {
|
|
1110
|
+
iterations++;
|
|
1111
|
+
if (rand() < restartProbability) {
|
|
1112
|
+
current = seedId;
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const neighbourList = [];
|
|
1116
|
+
for (const nb of graph.neighbours(current)) neighbourList.push(nb);
|
|
1117
|
+
if (neighbourList.length === 0) {
|
|
1118
|
+
current = seedId;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const next = neighbourList[Math.floor(rand() * neighbourList.length)];
|
|
1122
|
+
if (next === void 0) {
|
|
1123
|
+
current = seedId;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
edgesTraversed++;
|
|
1127
|
+
const [s, t] = current < next ? [current, next] : [next, current];
|
|
1128
|
+
let targets = sampledEdgeMap.get(s);
|
|
1129
|
+
if (targets === void 0) {
|
|
1130
|
+
targets = /* @__PURE__ */ new Set();
|
|
1131
|
+
sampledEdgeMap.set(s, targets);
|
|
1132
|
+
}
|
|
1133
|
+
targets.add(t);
|
|
1134
|
+
const previousSeedIdx = firstVisitedBySeed.get(next);
|
|
1135
|
+
if (previousSeedIdx !== void 0 && previousSeedIdx !== seedIdx) {
|
|
1136
|
+
const fromSeed = seeds[previousSeedIdx];
|
|
1137
|
+
const toSeed = seeds[seedIdx];
|
|
1138
|
+
if (fromSeed !== void 0 && toSeed !== void 0) {
|
|
1139
|
+
const path = {
|
|
1140
|
+
fromSeed,
|
|
1141
|
+
toSeed,
|
|
1142
|
+
nodes: [
|
|
1143
|
+
fromSeed.id,
|
|
1144
|
+
next,
|
|
1145
|
+
toSeed.id
|
|
1146
|
+
].filter((n, i, arr) => arr.indexOf(n) === i)
|
|
1147
|
+
};
|
|
1148
|
+
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);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (!firstVisitedBySeed.has(next)) firstVisitedBySeed.set(next, seedIdx);
|
|
1152
|
+
allVisited.add(next);
|
|
1153
|
+
visitedPerFrontier[seedIdx]?.add(next);
|
|
1154
|
+
current = next;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const endTime = performance.now();
|
|
1159
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
1160
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
1161
|
+
return {
|
|
1162
|
+
paths: discoveredPaths,
|
|
1163
|
+
sampledNodes: allVisited,
|
|
1164
|
+
sampledEdges: edgeTuples,
|
|
1165
|
+
visitedPerFrontier,
|
|
1166
|
+
stats: {
|
|
1167
|
+
iterations,
|
|
1168
|
+
nodesVisited: allVisited.size,
|
|
1169
|
+
edgesTraversed,
|
|
1170
|
+
pathsFound: discoveredPaths.length,
|
|
1171
|
+
durationMs: endTime - startTime,
|
|
1172
|
+
algorithm: "random-walk",
|
|
1173
|
+
termination: "exhausted"
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Create an empty result for early termination (no seeds).
|
|
1179
|
+
*/
|
|
1180
|
+
function emptyResult(startTime) {
|
|
1181
|
+
return {
|
|
1182
|
+
paths: [],
|
|
1183
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
1184
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
1185
|
+
visitedPerFrontier: [],
|
|
1186
|
+
stats: {
|
|
1187
|
+
iterations: 0,
|
|
1188
|
+
nodesVisited: 0,
|
|
1189
|
+
edgesTraversed: 0,
|
|
1190
|
+
pathsFound: 0,
|
|
1191
|
+
durationMs: performance.now() - startTime,
|
|
1192
|
+
algorithm: "random-walk",
|
|
1193
|
+
termination: "exhausted"
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
//#endregion
|
|
861
1198
|
//#region src/ranking/parse.ts
|
|
862
1199
|
/**
|
|
863
1200
|
* Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
|
|
@@ -951,20 +1288,128 @@ function adamicAdar(graph, source, target, config) {
|
|
|
951
1288
|
return Math.max(epsilon, score);
|
|
952
1289
|
}
|
|
953
1290
|
//#endregion
|
|
1291
|
+
//#region src/ranking/mi/cosine.ts
|
|
1292
|
+
/**
|
|
1293
|
+
* Compute cosine similarity between neighbourhoods of two nodes.
|
|
1294
|
+
*
|
|
1295
|
+
* @param graph - Source graph
|
|
1296
|
+
* @param source - Source node ID
|
|
1297
|
+
* @param target - Target node ID
|
|
1298
|
+
* @param config - Optional configuration
|
|
1299
|
+
* @returns Cosine similarity in [0, 1]
|
|
1300
|
+
*/
|
|
1301
|
+
function cosine(graph, source, target, config) {
|
|
1302
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1303
|
+
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1304
|
+
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1305
|
+
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
1306
|
+
const denominator = Math.sqrt(sourceNeighbours.size) * Math.sqrt(targetNeighbours.size);
|
|
1307
|
+
if (denominator === 0) return 0;
|
|
1308
|
+
const score = intersection / denominator;
|
|
1309
|
+
return Math.max(epsilon, score);
|
|
1310
|
+
}
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#region src/ranking/mi/sorensen.ts
|
|
1313
|
+
/**
|
|
1314
|
+
* Compute Sorensen-Dice similarity between neighbourhoods of two nodes.
|
|
1315
|
+
*
|
|
1316
|
+
* @param graph - Source graph
|
|
1317
|
+
* @param source - Source node ID
|
|
1318
|
+
* @param target - Target node ID
|
|
1319
|
+
* @param config - Optional configuration
|
|
1320
|
+
* @returns Sorensen-Dice coefficient in [0, 1]
|
|
1321
|
+
*/
|
|
1322
|
+
function sorensen(graph, source, target, config) {
|
|
1323
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1324
|
+
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1325
|
+
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1326
|
+
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
1327
|
+
const denominator = sourceNeighbours.size + targetNeighbours.size;
|
|
1328
|
+
if (denominator === 0) return 0;
|
|
1329
|
+
const score = 2 * intersection / denominator;
|
|
1330
|
+
return Math.max(epsilon, score);
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/ranking/mi/resource-allocation.ts
|
|
1334
|
+
/**
|
|
1335
|
+
* Compute Resource Allocation index between neighbourhoods of two nodes.
|
|
1336
|
+
*
|
|
1337
|
+
* @param graph - Source graph
|
|
1338
|
+
* @param source - Source node ID
|
|
1339
|
+
* @param target - Target node ID
|
|
1340
|
+
* @param config - Optional configuration
|
|
1341
|
+
* @returns Resource Allocation index (normalised to [0, 1] if configured)
|
|
1342
|
+
*/
|
|
1343
|
+
function resourceAllocation(graph, source, target, config) {
|
|
1344
|
+
const { epsilon = 1e-10, normalise = true } = config ?? {};
|
|
1345
|
+
const commonNeighbours = require_utils.neighbourIntersection(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
|
|
1346
|
+
let score = 0;
|
|
1347
|
+
for (const neighbour of commonNeighbours) {
|
|
1348
|
+
const degree = graph.degree(neighbour);
|
|
1349
|
+
if (degree > 0) score += 1 / degree;
|
|
1350
|
+
}
|
|
1351
|
+
if (normalise && commonNeighbours.size > 0) {
|
|
1352
|
+
const maxScore = commonNeighbours.size;
|
|
1353
|
+
score = score / maxScore;
|
|
1354
|
+
}
|
|
1355
|
+
return Math.max(epsilon, score);
|
|
1356
|
+
}
|
|
1357
|
+
//#endregion
|
|
1358
|
+
//#region src/ranking/mi/overlap-coefficient.ts
|
|
1359
|
+
/**
|
|
1360
|
+
* Compute Overlap Coefficient between neighbourhoods of two nodes.
|
|
1361
|
+
*
|
|
1362
|
+
* @param graph - Source graph
|
|
1363
|
+
* @param source - Source node ID
|
|
1364
|
+
* @param target - Target node ID
|
|
1365
|
+
* @param config - Optional configuration
|
|
1366
|
+
* @returns Overlap Coefficient in [0, 1]
|
|
1367
|
+
*/
|
|
1368
|
+
function overlapCoefficient(graph, source, target, config) {
|
|
1369
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1370
|
+
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1371
|
+
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1372
|
+
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
1373
|
+
const denominator = Math.min(sourceNeighbours.size, targetNeighbours.size);
|
|
1374
|
+
if (denominator === 0) return 0;
|
|
1375
|
+
const score = intersection / denominator;
|
|
1376
|
+
return Math.max(epsilon, score);
|
|
1377
|
+
}
|
|
1378
|
+
//#endregion
|
|
1379
|
+
//#region src/ranking/mi/hub-promoted.ts
|
|
1380
|
+
/**
|
|
1381
|
+
* Compute Hub Promoted index between neighbourhoods of two nodes.
|
|
1382
|
+
*
|
|
1383
|
+
* @param graph - Source graph
|
|
1384
|
+
* @param source - Source node ID
|
|
1385
|
+
* @param target - Target node ID
|
|
1386
|
+
* @param config - Optional configuration
|
|
1387
|
+
* @returns Hub Promoted index in [0, 1]
|
|
1388
|
+
*/
|
|
1389
|
+
function hubPromoted(graph, source, target, config) {
|
|
1390
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1391
|
+
const { intersection } = require_utils.neighbourOverlap(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
|
|
1392
|
+
const sourceDegree = graph.degree(source);
|
|
1393
|
+
const targetDegree = graph.degree(target);
|
|
1394
|
+
const denominator = Math.min(sourceDegree, targetDegree);
|
|
1395
|
+
if (denominator === 0) return 0;
|
|
1396
|
+
const score = intersection / denominator;
|
|
1397
|
+
return Math.max(epsilon, score);
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
954
1400
|
//#region src/ranking/mi/scale.ts
|
|
955
1401
|
/**
|
|
956
1402
|
* Compute SCALE MI between two nodes.
|
|
957
1403
|
*/
|
|
958
1404
|
function scale(graph, source, target, config) {
|
|
959
1405
|
const { epsilon = 1e-10 } = config ?? {};
|
|
960
|
-
const {
|
|
961
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
1406
|
+
const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
|
|
962
1407
|
const n = graph.nodeCount;
|
|
963
1408
|
const m = graph.edgeCount;
|
|
964
1409
|
const possibleEdges = n * (n - 1);
|
|
965
1410
|
const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
|
|
966
1411
|
if (density === 0) return epsilon;
|
|
967
|
-
const score =
|
|
1412
|
+
const score = jaccardScore / density;
|
|
968
1413
|
return Math.max(epsilon, score);
|
|
969
1414
|
}
|
|
970
1415
|
//#endregion
|
|
@@ -974,14 +1419,13 @@ function scale(graph, source, target, config) {
|
|
|
974
1419
|
*/
|
|
975
1420
|
function skew(graph, source, target, config) {
|
|
976
1421
|
const { epsilon = 1e-10 } = config ?? {};
|
|
977
|
-
const {
|
|
978
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
1422
|
+
const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
|
|
979
1423
|
const N = graph.nodeCount;
|
|
980
1424
|
const sourceDegree = graph.degree(source);
|
|
981
1425
|
const targetDegree = graph.degree(target);
|
|
982
1426
|
const sourceIdf = Math.log(N / (sourceDegree + 1));
|
|
983
1427
|
const targetIdf = Math.log(N / (targetDegree + 1));
|
|
984
|
-
const score =
|
|
1428
|
+
const score = jaccardScore * sourceIdf * targetIdf;
|
|
985
1429
|
return Math.max(epsilon, score);
|
|
986
1430
|
}
|
|
987
1431
|
//#endregion
|
|
@@ -991,11 +1435,10 @@ function skew(graph, source, target, config) {
|
|
|
991
1435
|
*/
|
|
992
1436
|
function span(graph, source, target, config) {
|
|
993
1437
|
const { epsilon = 1e-10 } = config ?? {};
|
|
994
|
-
const {
|
|
995
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
1438
|
+
const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
|
|
996
1439
|
const sourceCc = require_utils.localClusteringCoefficient(graph, source);
|
|
997
1440
|
const targetCc = require_utils.localClusteringCoefficient(graph, target);
|
|
998
|
-
const score =
|
|
1441
|
+
const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
|
|
999
1442
|
return Math.max(epsilon, score);
|
|
1000
1443
|
}
|
|
1001
1444
|
//#endregion
|
|
@@ -1005,13 +1448,12 @@ function span(graph, source, target, config) {
|
|
|
1005
1448
|
*/
|
|
1006
1449
|
function etch(graph, source, target, config) {
|
|
1007
1450
|
const { epsilon = 1e-10 } = config ?? {};
|
|
1008
|
-
const {
|
|
1009
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
1451
|
+
const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
|
|
1010
1452
|
const edge = graph.getEdge(source, target);
|
|
1011
|
-
if (edge?.type === void 0) return Math.max(epsilon,
|
|
1453
|
+
if (edge?.type === void 0) return Math.max(epsilon, jaccardScore);
|
|
1012
1454
|
const edgeTypeCount = require_utils.countEdgesOfType(graph, edge.type);
|
|
1013
|
-
if (edgeTypeCount === 0) return Math.max(epsilon,
|
|
1014
|
-
const score =
|
|
1455
|
+
if (edgeTypeCount === 0) return Math.max(epsilon, jaccardScore);
|
|
1456
|
+
const score = jaccardScore * Math.log(graph.edgeCount / edgeTypeCount);
|
|
1015
1457
|
return Math.max(epsilon, score);
|
|
1016
1458
|
}
|
|
1017
1459
|
//#endregion
|
|
@@ -1021,17 +1463,16 @@ function etch(graph, source, target, config) {
|
|
|
1021
1463
|
*/
|
|
1022
1464
|
function notch(graph, source, target, config) {
|
|
1023
1465
|
const { epsilon = 1e-10 } = config ?? {};
|
|
1024
|
-
const {
|
|
1025
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
1466
|
+
const { jaccard: jaccardScore } = require_utils.computeJaccard(graph, source, target);
|
|
1026
1467
|
const sourceNode = graph.getNode(source);
|
|
1027
1468
|
const targetNode = graph.getNode(target);
|
|
1028
|
-
if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon,
|
|
1469
|
+
if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccardScore);
|
|
1029
1470
|
const sourceTypeCount = require_utils.countNodesOfType(graph, sourceNode.type);
|
|
1030
1471
|
const targetTypeCount = require_utils.countNodesOfType(graph, targetNode.type);
|
|
1031
|
-
if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon,
|
|
1472
|
+
if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccardScore);
|
|
1032
1473
|
const sourceRarity = Math.log(graph.nodeCount / sourceTypeCount);
|
|
1033
1474
|
const targetRarity = Math.log(graph.nodeCount / targetTypeCount);
|
|
1034
|
-
const score =
|
|
1475
|
+
const score = jaccardScore * sourceRarity * targetRarity;
|
|
1035
1476
|
return Math.max(epsilon, score);
|
|
1036
1477
|
}
|
|
1037
1478
|
//#endregion
|
|
@@ -1050,13 +1491,12 @@ function notch(graph, source, target, config) {
|
|
|
1050
1491
|
*/
|
|
1051
1492
|
function adaptive(graph, source, target, config) {
|
|
1052
1493
|
const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
|
|
1053
|
-
const
|
|
1494
|
+
const { jaccard: jaccardScore, sourceNeighbours, targetNeighbours } = require_utils.computeJaccard(graph, source, target);
|
|
1495
|
+
const structural = sourceNeighbours.size === 0 && targetNeighbours.size === 0 ? 0 : Math.max(epsilon, jaccardScore);
|
|
1054
1496
|
const degreeComponent = adamicAdar(graph, source, target, {
|
|
1055
1497
|
epsilon,
|
|
1056
1498
|
normalise: true
|
|
1057
1499
|
});
|
|
1058
|
-
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1059
|
-
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1060
1500
|
let overlap;
|
|
1061
1501
|
if (sourceNeighbours.size > 0 && targetNeighbours.size > 0) {
|
|
1062
1502
|
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
@@ -1068,6 +1508,42 @@ function adaptive(graph, source, target, config) {
|
|
|
1068
1508
|
return Math.max(epsilon, Math.min(1, score));
|
|
1069
1509
|
}
|
|
1070
1510
|
//#endregion
|
|
1511
|
+
//#region src/ranking/baselines/utils.ts
|
|
1512
|
+
/**
|
|
1513
|
+
* Normalise a set of scored paths and return them sorted highest-first.
|
|
1514
|
+
*
|
|
1515
|
+
* All scores are normalised relative to the maximum observed score.
|
|
1516
|
+
* When `includeScores` is false, raw (un-normalised) scores are preserved.
|
|
1517
|
+
* Handles degenerate cases: empty input and all-zero scores.
|
|
1518
|
+
*
|
|
1519
|
+
* @param paths - Original paths in input order
|
|
1520
|
+
* @param scored - Paths paired with their computed scores
|
|
1521
|
+
* @param method - Method name to embed in the result
|
|
1522
|
+
* @param includeScores - When true, normalise scores to [0, 1]; when false, keep raw scores
|
|
1523
|
+
* @returns BaselineResult with ranked paths
|
|
1524
|
+
*/
|
|
1525
|
+
function normaliseAndRank(paths, scored, method, includeScores) {
|
|
1526
|
+
if (scored.length === 0) return {
|
|
1527
|
+
paths: [],
|
|
1528
|
+
method
|
|
1529
|
+
};
|
|
1530
|
+
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1531
|
+
if (maxScore === 0) return {
|
|
1532
|
+
paths: paths.map((path) => ({
|
|
1533
|
+
...path,
|
|
1534
|
+
score: 0
|
|
1535
|
+
})),
|
|
1536
|
+
method
|
|
1537
|
+
};
|
|
1538
|
+
return {
|
|
1539
|
+
paths: scored.map(({ path, score }) => ({
|
|
1540
|
+
...path,
|
|
1541
|
+
score: includeScores ? score / maxScore : score
|
|
1542
|
+
})).sort((a, b) => b.score - a.score),
|
|
1543
|
+
method
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
//#endregion
|
|
1071
1547
|
//#region src/ranking/baselines/shortest.ts
|
|
1072
1548
|
/**
|
|
1073
1549
|
* Rank paths by length (shortest first).
|
|
@@ -1085,18 +1561,10 @@ function shortest(_graph, paths, config) {
|
|
|
1085
1561
|
paths: [],
|
|
1086
1562
|
method: "shortest"
|
|
1087
1563
|
};
|
|
1088
|
-
|
|
1564
|
+
return normaliseAndRank(paths, paths.map((path) => ({
|
|
1089
1565
|
path,
|
|
1090
1566
|
score: 1 / path.nodes.length
|
|
1091
|
-
}));
|
|
1092
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1093
|
-
return {
|
|
1094
|
-
paths: scored.map(({ path, score }) => ({
|
|
1095
|
-
...path,
|
|
1096
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1097
|
-
})).sort((a, b) => b.score - a.score),
|
|
1098
|
-
method: "shortest"
|
|
1099
|
-
};
|
|
1567
|
+
})), "shortest", includeScores);
|
|
1100
1568
|
}
|
|
1101
1569
|
//#endregion
|
|
1102
1570
|
//#region src/ranking/baselines/degree-sum.ts
|
|
@@ -1114,29 +1582,14 @@ function degreeSum(graph, paths, config) {
|
|
|
1114
1582
|
paths: [],
|
|
1115
1583
|
method: "degree-sum"
|
|
1116
1584
|
};
|
|
1117
|
-
|
|
1585
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1118
1586
|
let degreeSum = 0;
|
|
1119
1587
|
for (const nodeId of path.nodes) degreeSum += graph.degree(nodeId);
|
|
1120
1588
|
return {
|
|
1121
1589
|
path,
|
|
1122
1590
|
score: degreeSum
|
|
1123
1591
|
};
|
|
1124
|
-
});
|
|
1125
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1126
|
-
if (maxScore === 0) return {
|
|
1127
|
-
paths: paths.map((path) => ({
|
|
1128
|
-
...path,
|
|
1129
|
-
score: 0
|
|
1130
|
-
})),
|
|
1131
|
-
method: "degree-sum"
|
|
1132
|
-
};
|
|
1133
|
-
return {
|
|
1134
|
-
paths: scored.map(({ path, score }) => ({
|
|
1135
|
-
...path,
|
|
1136
|
-
score: includeScores ? score / maxScore : score
|
|
1137
|
-
})).sort((a, b) => b.score - a.score),
|
|
1138
|
-
method: "degree-sum"
|
|
1139
|
-
};
|
|
1592
|
+
}), "degree-sum", includeScores);
|
|
1140
1593
|
}
|
|
1141
1594
|
//#endregion
|
|
1142
1595
|
//#region src/ranking/baselines/widest-path.ts
|
|
@@ -1154,7 +1607,7 @@ function widestPath(graph, paths, config) {
|
|
|
1154
1607
|
paths: [],
|
|
1155
1608
|
method: "widest-path"
|
|
1156
1609
|
};
|
|
1157
|
-
|
|
1610
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1158
1611
|
if (path.nodes.length < 2) return {
|
|
1159
1612
|
path,
|
|
1160
1613
|
score: 1
|
|
@@ -1171,22 +1624,7 @@ function widestPath(graph, paths, config) {
|
|
|
1171
1624
|
path,
|
|
1172
1625
|
score: minSimilarity === Number.POSITIVE_INFINITY ? 1 : minSimilarity
|
|
1173
1626
|
};
|
|
1174
|
-
});
|
|
1175
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1176
|
-
if (maxScore === 0) return {
|
|
1177
|
-
paths: paths.map((path) => ({
|
|
1178
|
-
...path,
|
|
1179
|
-
score: 0
|
|
1180
|
-
})),
|
|
1181
|
-
method: "widest-path"
|
|
1182
|
-
};
|
|
1183
|
-
return {
|
|
1184
|
-
paths: scored.map(({ path, score }) => ({
|
|
1185
|
-
...path,
|
|
1186
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1187
|
-
})).sort((a, b) => b.score - a.score),
|
|
1188
|
-
method: "widest-path"
|
|
1189
|
-
};
|
|
1627
|
+
}), "widest-path", includeScores);
|
|
1190
1628
|
}
|
|
1191
1629
|
//#endregion
|
|
1192
1630
|
//#region src/ranking/baselines/jaccard-arithmetic.ts
|
|
@@ -1204,7 +1642,7 @@ function jaccardArithmetic(graph, paths, config) {
|
|
|
1204
1642
|
paths: [],
|
|
1205
1643
|
method: "jaccard-arithmetic"
|
|
1206
1644
|
};
|
|
1207
|
-
|
|
1645
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1208
1646
|
if (path.nodes.length < 2) return {
|
|
1209
1647
|
path,
|
|
1210
1648
|
score: 1
|
|
@@ -1223,22 +1661,7 @@ function jaccardArithmetic(graph, paths, config) {
|
|
|
1223
1661
|
path,
|
|
1224
1662
|
score: edgeCount > 0 ? similaritySum / edgeCount : 1
|
|
1225
1663
|
};
|
|
1226
|
-
});
|
|
1227
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1228
|
-
if (maxScore === 0) return {
|
|
1229
|
-
paths: paths.map((path) => ({
|
|
1230
|
-
...path,
|
|
1231
|
-
score: 0
|
|
1232
|
-
})),
|
|
1233
|
-
method: "jaccard-arithmetic"
|
|
1234
|
-
};
|
|
1235
|
-
return {
|
|
1236
|
-
paths: scored.map(({ path, score }) => ({
|
|
1237
|
-
...path,
|
|
1238
|
-
score: includeScores ? score / maxScore : score
|
|
1239
|
-
})).sort((a, b) => b.score - a.score),
|
|
1240
|
-
method: "jaccard-arithmetic"
|
|
1241
|
-
};
|
|
1664
|
+
}), "jaccard-arithmetic", includeScores);
|
|
1242
1665
|
}
|
|
1243
1666
|
//#endregion
|
|
1244
1667
|
//#region src/ranking/baselines/pagerank.ts
|
|
@@ -1299,29 +1722,14 @@ function pagerank(graph, paths, config) {
|
|
|
1299
1722
|
method: "pagerank"
|
|
1300
1723
|
};
|
|
1301
1724
|
const ranks = computePageRank(graph);
|
|
1302
|
-
|
|
1725
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1303
1726
|
let prSum = 0;
|
|
1304
1727
|
for (const nodeId of path.nodes) prSum += ranks.get(nodeId) ?? 0;
|
|
1305
1728
|
return {
|
|
1306
1729
|
path,
|
|
1307
1730
|
score: prSum
|
|
1308
1731
|
};
|
|
1309
|
-
});
|
|
1310
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1311
|
-
if (maxScore === 0) return {
|
|
1312
|
-
paths: paths.map((path) => ({
|
|
1313
|
-
...path,
|
|
1314
|
-
score: 0
|
|
1315
|
-
})),
|
|
1316
|
-
method: "pagerank"
|
|
1317
|
-
};
|
|
1318
|
-
return {
|
|
1319
|
-
paths: scored.map(({ path, score }) => ({
|
|
1320
|
-
...path,
|
|
1321
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1322
|
-
})).sort((a, b) => b.score - a.score),
|
|
1323
|
-
method: "pagerank"
|
|
1324
|
-
};
|
|
1732
|
+
}), "pagerank", includeScores);
|
|
1325
1733
|
}
|
|
1326
1734
|
//#endregion
|
|
1327
1735
|
//#region src/ranking/baselines/betweenness.ts
|
|
@@ -1406,29 +1814,14 @@ function betweenness(graph, paths, config) {
|
|
|
1406
1814
|
method: "betweenness"
|
|
1407
1815
|
};
|
|
1408
1816
|
const bcMap = computeBetweenness(graph);
|
|
1409
|
-
|
|
1817
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1410
1818
|
let bcSum = 0;
|
|
1411
1819
|
for (const nodeId of path.nodes) bcSum += bcMap.get(nodeId) ?? 0;
|
|
1412
1820
|
return {
|
|
1413
1821
|
path,
|
|
1414
1822
|
score: bcSum
|
|
1415
1823
|
};
|
|
1416
|
-
});
|
|
1417
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1418
|
-
if (maxScore === 0) return {
|
|
1419
|
-
paths: paths.map((path) => ({
|
|
1420
|
-
...path,
|
|
1421
|
-
score: 0
|
|
1422
|
-
})),
|
|
1423
|
-
method: "betweenness"
|
|
1424
|
-
};
|
|
1425
|
-
return {
|
|
1426
|
-
paths: scored.map(({ path, score }) => ({
|
|
1427
|
-
...path,
|
|
1428
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1429
|
-
})).sort((a, b) => b.score - a.score),
|
|
1430
|
-
method: "betweenness"
|
|
1431
|
-
};
|
|
1824
|
+
}), "betweenness", includeScores);
|
|
1432
1825
|
}
|
|
1433
1826
|
//#endregion
|
|
1434
1827
|
//#region src/ranking/baselines/katz.ts
|
|
@@ -1491,7 +1884,7 @@ function katz(graph, paths, config) {
|
|
|
1491
1884
|
paths: [],
|
|
1492
1885
|
method: "katz"
|
|
1493
1886
|
};
|
|
1494
|
-
|
|
1887
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1495
1888
|
const source = path.nodes[0];
|
|
1496
1889
|
const target = path.nodes[path.nodes.length - 1];
|
|
1497
1890
|
if (source === void 0 || target === void 0) return {
|
|
@@ -1502,22 +1895,7 @@ function katz(graph, paths, config) {
|
|
|
1502
1895
|
path,
|
|
1503
1896
|
score: computeKatz(graph, source, target)
|
|
1504
1897
|
};
|
|
1505
|
-
});
|
|
1506
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1507
|
-
if (maxScore === 0) return {
|
|
1508
|
-
paths: paths.map((path) => ({
|
|
1509
|
-
...path,
|
|
1510
|
-
score: 0
|
|
1511
|
-
})),
|
|
1512
|
-
method: "katz"
|
|
1513
|
-
};
|
|
1514
|
-
return {
|
|
1515
|
-
paths: scored.map(({ path, score }) => ({
|
|
1516
|
-
...path,
|
|
1517
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1518
|
-
})).sort((a, b) => b.score - a.score),
|
|
1519
|
-
method: "katz"
|
|
1520
|
-
};
|
|
1898
|
+
}), "katz", includeScores);
|
|
1521
1899
|
}
|
|
1522
1900
|
//#endregion
|
|
1523
1901
|
//#region src/ranking/baselines/communicability.ts
|
|
@@ -1579,7 +1957,7 @@ function communicability(graph, paths, config) {
|
|
|
1579
1957
|
paths: [],
|
|
1580
1958
|
method: "communicability"
|
|
1581
1959
|
};
|
|
1582
|
-
|
|
1960
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1583
1961
|
const source = path.nodes[0];
|
|
1584
1962
|
const target = path.nodes[path.nodes.length - 1];
|
|
1585
1963
|
if (source === void 0 || target === void 0) return {
|
|
@@ -1590,22 +1968,7 @@ function communicability(graph, paths, config) {
|
|
|
1590
1968
|
path,
|
|
1591
1969
|
score: computeCommunicability(graph, source, target)
|
|
1592
1970
|
};
|
|
1593
|
-
});
|
|
1594
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1595
|
-
if (maxScore === 0) return {
|
|
1596
|
-
paths: paths.map((path) => ({
|
|
1597
|
-
...path,
|
|
1598
|
-
score: 0
|
|
1599
|
-
})),
|
|
1600
|
-
method: "communicability"
|
|
1601
|
-
};
|
|
1602
|
-
return {
|
|
1603
|
-
paths: scored.map(({ path, score }) => ({
|
|
1604
|
-
...path,
|
|
1605
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1606
|
-
})).sort((a, b) => b.score - a.score),
|
|
1607
|
-
method: "communicability"
|
|
1608
|
-
};
|
|
1971
|
+
}), "communicability", includeScores);
|
|
1609
1972
|
}
|
|
1610
1973
|
//#endregion
|
|
1611
1974
|
//#region src/ranking/baselines/resistance-distance.ts
|
|
@@ -1722,7 +2085,7 @@ function resistanceDistance(graph, paths, config) {
|
|
|
1722
2085
|
};
|
|
1723
2086
|
const nodeCount = Array.from(graph.nodeIds()).length;
|
|
1724
2087
|
if (nodeCount > 5e3) throw new Error(`Cannot rank paths: graph too large (${String(nodeCount)} nodes). Resistance distance requires O(n^3) computation; maximum 5000 nodes.`);
|
|
1725
|
-
|
|
2088
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1726
2089
|
const source = path.nodes[0];
|
|
1727
2090
|
const target = path.nodes[path.nodes.length - 1];
|
|
1728
2091
|
if (source === void 0 || target === void 0) return {
|
|
@@ -1733,22 +2096,7 @@ function resistanceDistance(graph, paths, config) {
|
|
|
1733
2096
|
path,
|
|
1734
2097
|
score: 1 / computeResistance(graph, source, target)
|
|
1735
2098
|
};
|
|
1736
|
-
});
|
|
1737
|
-
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1738
|
-
if (maxScore === 0) return {
|
|
1739
|
-
paths: paths.map((path) => ({
|
|
1740
|
-
...path,
|
|
1741
|
-
score: 0
|
|
1742
|
-
})),
|
|
1743
|
-
method: "resistance-distance"
|
|
1744
|
-
};
|
|
1745
|
-
return {
|
|
1746
|
-
paths: scored.map(({ path, score }) => ({
|
|
1747
|
-
...path,
|
|
1748
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1749
|
-
})).sort((a, b) => b.score - a.score),
|
|
1750
|
-
method: "resistance-distance"
|
|
1751
|
-
};
|
|
2099
|
+
}), "resistance-distance", includeScores);
|
|
1752
2100
|
}
|
|
1753
2101
|
//#endregion
|
|
1754
2102
|
//#region src/ranking/baselines/random-ranking.ts
|
|
@@ -1782,27 +2130,258 @@ function randomRanking(_graph, paths, config) {
|
|
|
1782
2130
|
paths: [],
|
|
1783
2131
|
method: "random"
|
|
1784
2132
|
};
|
|
1785
|
-
|
|
2133
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
1786
2134
|
return {
|
|
1787
2135
|
path,
|
|
1788
2136
|
score: seededRandom(path.nodes.join(","), seed)
|
|
1789
2137
|
};
|
|
2138
|
+
}), "random", includeScores);
|
|
2139
|
+
}
|
|
2140
|
+
//#endregion
|
|
2141
|
+
//#region src/ranking/baselines/hitting-time.ts
|
|
2142
|
+
/**
|
|
2143
|
+
* Seeded deterministic random number generator (LCG).
|
|
2144
|
+
* Suitable for reproducible random walk simulation.
|
|
2145
|
+
*/
|
|
2146
|
+
var SeededRNG = class {
|
|
2147
|
+
state;
|
|
2148
|
+
constructor(seed) {
|
|
2149
|
+
this.state = seed;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Generate next pseudorandom value in [0, 1).
|
|
2153
|
+
*/
|
|
2154
|
+
next() {
|
|
2155
|
+
this.state = this.state * 1103515245 + 12345 & 2147483647;
|
|
2156
|
+
return this.state / 2147483647;
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
/**
|
|
2160
|
+
* Compute hitting time via Monte Carlo random walk simulation.
|
|
2161
|
+
*
|
|
2162
|
+
* @param graph - Source graph
|
|
2163
|
+
* @param source - Source node ID
|
|
2164
|
+
* @param target - Target node ID
|
|
2165
|
+
* @param walks - Number of walks to simulate
|
|
2166
|
+
* @param maxSteps - Maximum steps per walk
|
|
2167
|
+
* @param rng - Seeded RNG instance
|
|
2168
|
+
* @returns Average hitting time across walks
|
|
2169
|
+
*/
|
|
2170
|
+
function computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng) {
|
|
2171
|
+
if (source === target) return 0;
|
|
2172
|
+
let totalSteps = 0;
|
|
2173
|
+
let successfulWalks = 0;
|
|
2174
|
+
for (let w = 0; w < walks; w++) {
|
|
2175
|
+
let current = source;
|
|
2176
|
+
let steps = 0;
|
|
2177
|
+
while (current !== target && steps < maxSteps) {
|
|
2178
|
+
const neighbours = Array.from(graph.neighbours(current));
|
|
2179
|
+
if (neighbours.length === 0) break;
|
|
2180
|
+
const nextNode = neighbours[Math.floor(rng.next() * neighbours.length)];
|
|
2181
|
+
if (nextNode === void 0) break;
|
|
2182
|
+
current = nextNode;
|
|
2183
|
+
steps++;
|
|
2184
|
+
}
|
|
2185
|
+
if (current === target) {
|
|
2186
|
+
totalSteps += steps;
|
|
2187
|
+
successfulWalks++;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
if (successfulWalks > 0) return totalSteps / successfulWalks;
|
|
2191
|
+
return maxSteps;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Compute hitting time via exact fundamental matrix method.
|
|
2195
|
+
*
|
|
2196
|
+
* For small graphs, computes exact expected hitting times using
|
|
2197
|
+
* the fundamental matrix of the random walk.
|
|
2198
|
+
*
|
|
2199
|
+
* @param graph - Source graph
|
|
2200
|
+
* @param source - Source node ID
|
|
2201
|
+
* @param target - Target node ID
|
|
2202
|
+
* @returns Exact hitting time (or approximation if convergence fails)
|
|
2203
|
+
*/
|
|
2204
|
+
function computeHittingTimeExact(graph, source, target) {
|
|
2205
|
+
if (source === target) return 0;
|
|
2206
|
+
const nodes = Array.from(graph.nodeIds());
|
|
2207
|
+
const nodeToIdx = /* @__PURE__ */ new Map();
|
|
2208
|
+
nodes.forEach((nodeId, idx) => {
|
|
2209
|
+
nodeToIdx.set(nodeId, idx);
|
|
2210
|
+
});
|
|
2211
|
+
const n = nodes.length;
|
|
2212
|
+
const sourceIdx = nodeToIdx.get(source);
|
|
2213
|
+
const targetIdx = nodeToIdx.get(target);
|
|
2214
|
+
if (sourceIdx === void 0 || targetIdx === void 0) return 0;
|
|
2215
|
+
const P = [];
|
|
2216
|
+
for (let i = 0; i < n; i++) {
|
|
2217
|
+
const row = [];
|
|
2218
|
+
for (let j = 0; j < n; j++) row[j] = 0;
|
|
2219
|
+
P[i] = row;
|
|
2220
|
+
}
|
|
2221
|
+
for (const nodeId of nodes) {
|
|
2222
|
+
const idx = nodeToIdx.get(nodeId);
|
|
2223
|
+
if (idx === void 0) continue;
|
|
2224
|
+
const pRow = P[idx];
|
|
2225
|
+
if (pRow === void 0) continue;
|
|
2226
|
+
if (idx === targetIdx) pRow[idx] = 1;
|
|
2227
|
+
else {
|
|
2228
|
+
const neighbours = Array.from(graph.neighbours(nodeId));
|
|
2229
|
+
const degree = neighbours.length;
|
|
2230
|
+
if (degree > 0) for (const neighbourId of neighbours) {
|
|
2231
|
+
const nIdx = nodeToIdx.get(neighbourId);
|
|
2232
|
+
if (nIdx !== void 0) pRow[nIdx] = 1 / degree;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
const transientIndices = [];
|
|
2237
|
+
for (let i = 0; i < n; i++) if (i !== targetIdx) transientIndices.push(i);
|
|
2238
|
+
const m = transientIndices.length;
|
|
2239
|
+
const Q = [];
|
|
2240
|
+
for (let i = 0; i < m; i++) {
|
|
2241
|
+
const row = [];
|
|
2242
|
+
for (let j = 0; j < m; j++) row[j] = 0;
|
|
2243
|
+
Q[i] = row;
|
|
2244
|
+
}
|
|
2245
|
+
for (let i = 0; i < m; i++) {
|
|
2246
|
+
const qRow = Q[i];
|
|
2247
|
+
if (qRow === void 0) continue;
|
|
2248
|
+
const origI = transientIndices[i];
|
|
2249
|
+
if (origI === void 0) continue;
|
|
2250
|
+
const pRow = P[origI];
|
|
2251
|
+
if (pRow === void 0) continue;
|
|
2252
|
+
for (let j = 0; j < m; j++) {
|
|
2253
|
+
const origJ = transientIndices[j];
|
|
2254
|
+
if (origJ === void 0) continue;
|
|
2255
|
+
qRow[j] = pRow[origJ] ?? 0;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const IMQ = [];
|
|
2259
|
+
for (let i = 0; i < m; i++) {
|
|
2260
|
+
const row = [];
|
|
2261
|
+
for (let j = 0; j < m; j++) row[j] = i === j ? 1 : 0;
|
|
2262
|
+
IMQ[i] = row;
|
|
2263
|
+
}
|
|
2264
|
+
for (let i = 0; i < m; i++) {
|
|
2265
|
+
const imqRow = IMQ[i];
|
|
2266
|
+
if (imqRow === void 0) continue;
|
|
2267
|
+
const qRow = Q[i];
|
|
2268
|
+
for (let j = 0; j < m; j++) {
|
|
2269
|
+
const qVal = qRow?.[j] ?? 0;
|
|
2270
|
+
imqRow[j] = (i === j ? 1 : 0) - qVal;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
const N = invertMatrix(IMQ);
|
|
2274
|
+
if (N === null) return 1;
|
|
2275
|
+
const sourceTransientIdx = transientIndices.indexOf(sourceIdx);
|
|
2276
|
+
if (sourceTransientIdx < 0) return 0;
|
|
2277
|
+
let hittingTime = 0;
|
|
2278
|
+
const row = N[sourceTransientIdx];
|
|
2279
|
+
if (row !== void 0) for (const val of row) hittingTime += val;
|
|
2280
|
+
return hittingTime;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Invert a square matrix using Gaussian elimination with partial pivoting.
|
|
2284
|
+
*
|
|
2285
|
+
* @param matrix - Input matrix (n × n)
|
|
2286
|
+
* @returns Inverted matrix, or null if singular
|
|
2287
|
+
*/
|
|
2288
|
+
function invertMatrix(matrix) {
|
|
2289
|
+
const n = matrix.length;
|
|
2290
|
+
const aug = [];
|
|
2291
|
+
for (let i = 0; i < n; i++) {
|
|
2292
|
+
const row = [];
|
|
2293
|
+
const matRow = matrix[i];
|
|
2294
|
+
for (let j = 0; j < n; j++) row[j] = matRow?.[j] ?? 0;
|
|
2295
|
+
for (let j = 0; j < n; j++) row[n + j] = i === j ? 1 : 0;
|
|
2296
|
+
aug[i] = row;
|
|
2297
|
+
}
|
|
2298
|
+
for (let col = 0; col < n; col++) {
|
|
2299
|
+
let pivotRow = col;
|
|
2300
|
+
const pivotCol = aug[pivotRow];
|
|
2301
|
+
if (pivotCol === void 0) return null;
|
|
2302
|
+
for (let row = col + 1; row < n; row++) {
|
|
2303
|
+
const currRowVal = aug[row]?.[col] ?? 0;
|
|
2304
|
+
const pivotRowVal = pivotCol[col] ?? 0;
|
|
2305
|
+
if (Math.abs(currRowVal) > Math.abs(pivotRowVal)) pivotRow = row;
|
|
2306
|
+
}
|
|
2307
|
+
const augPivot = aug[pivotRow];
|
|
2308
|
+
if (augPivot === void 0 || Math.abs(augPivot[col] ?? 0) < 1e-10) return null;
|
|
2309
|
+
[aug[col], aug[pivotRow]] = [aug[pivotRow] ?? [], aug[col] ?? []];
|
|
2310
|
+
const scaledPivotRow = aug[col];
|
|
2311
|
+
if (scaledPivotRow === void 0) return null;
|
|
2312
|
+
const pivot = scaledPivotRow[col] ?? 1;
|
|
2313
|
+
for (let j = 0; j < 2 * n; j++) scaledPivotRow[j] = (scaledPivotRow[j] ?? 0) / pivot;
|
|
2314
|
+
for (let row = col + 1; row < n; row++) {
|
|
2315
|
+
const currRow = aug[row];
|
|
2316
|
+
if (currRow === void 0) continue;
|
|
2317
|
+
const factor = currRow[col] ?? 0;
|
|
2318
|
+
for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (scaledPivotRow[j] ?? 0);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
for (let col = n - 1; col > 0; col--) {
|
|
2322
|
+
const colRow = aug[col];
|
|
2323
|
+
if (colRow === void 0) return null;
|
|
2324
|
+
for (let row = col - 1; row >= 0; row--) {
|
|
2325
|
+
const currRow = aug[row];
|
|
2326
|
+
if (currRow === void 0) continue;
|
|
2327
|
+
const factor = currRow[col] ?? 0;
|
|
2328
|
+
for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (colRow[j] ?? 0);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
const inv = [];
|
|
2332
|
+
for (let i = 0; i < n; i++) {
|
|
2333
|
+
const row = [];
|
|
2334
|
+
for (let j = 0; j < n; j++) row[j] = 0;
|
|
2335
|
+
inv[i] = row;
|
|
2336
|
+
}
|
|
2337
|
+
for (let i = 0; i < n; i++) {
|
|
2338
|
+
const invRow = inv[i];
|
|
2339
|
+
if (invRow === void 0) continue;
|
|
2340
|
+
const augRow = aug[i];
|
|
2341
|
+
if (augRow === void 0) continue;
|
|
2342
|
+
for (let j = 0; j < n; j++) invRow[j] = augRow[n + j] ?? 0;
|
|
2343
|
+
}
|
|
2344
|
+
return inv;
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Rank paths by inverse hitting time between endpoints.
|
|
2348
|
+
*
|
|
2349
|
+
* @param graph - Source graph
|
|
2350
|
+
* @param paths - Paths to rank
|
|
2351
|
+
* @param config - Configuration options
|
|
2352
|
+
* @returns Ranked paths (highest inverse hitting time first)
|
|
2353
|
+
*/
|
|
2354
|
+
function hittingTime(graph, paths, config) {
|
|
2355
|
+
const { includeScores = true, mode = "auto", walks = 1e3, maxSteps = 1e4, seed = 42 } = config ?? {};
|
|
2356
|
+
if (paths.length === 0) return {
|
|
2357
|
+
paths: [],
|
|
2358
|
+
method: "hitting-time"
|
|
2359
|
+
};
|
|
2360
|
+
const nodeCount = Array.from(graph.nodeIds()).length;
|
|
2361
|
+
const actualMode = mode === "auto" ? nodeCount < 100 ? "exact" : "approximate" : mode;
|
|
2362
|
+
const rng = new SeededRNG(seed);
|
|
2363
|
+
const scored = paths.map((path) => {
|
|
2364
|
+
const source = path.nodes[0];
|
|
2365
|
+
const target = path.nodes[path.nodes.length - 1];
|
|
2366
|
+
if (source === void 0 || target === void 0) return {
|
|
2367
|
+
path,
|
|
2368
|
+
score: 0
|
|
2369
|
+
};
|
|
2370
|
+
const ht = actualMode === "exact" ? computeHittingTimeExact(graph, source, target) : computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng);
|
|
2371
|
+
return {
|
|
2372
|
+
path,
|
|
2373
|
+
score: ht > 0 ? 1 / ht : 0
|
|
2374
|
+
};
|
|
1790
2375
|
});
|
|
1791
2376
|
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1792
|
-
if (maxScore
|
|
2377
|
+
if (!Number.isFinite(maxScore)) return {
|
|
1793
2378
|
paths: paths.map((path) => ({
|
|
1794
2379
|
...path,
|
|
1795
2380
|
score: 0
|
|
1796
2381
|
})),
|
|
1797
|
-
method: "
|
|
1798
|
-
};
|
|
1799
|
-
return {
|
|
1800
|
-
paths: scored.map(({ path, score }) => ({
|
|
1801
|
-
...path,
|
|
1802
|
-
score: includeScores ? score / maxScore : score / maxScore
|
|
1803
|
-
})).sort((a, b) => b.score - a.score),
|
|
1804
|
-
method: "random"
|
|
2382
|
+
method: "hitting-time"
|
|
1805
2383
|
};
|
|
2384
|
+
return normaliseAndRank(paths, scored, "hitting-time", includeScores);
|
|
1806
2385
|
}
|
|
1807
2386
|
//#endregion
|
|
1808
2387
|
//#region src/extraction/ego-network.ts
|
|
@@ -2437,7 +3016,9 @@ exports.betweenness = betweenness;
|
|
|
2437
3016
|
exports.bfs = require_traversal.bfs;
|
|
2438
3017
|
exports.bfsWithPath = require_traversal.bfsWithPath;
|
|
2439
3018
|
exports.communicability = communicability;
|
|
3019
|
+
exports.computeJaccard = require_utils.computeJaccard;
|
|
2440
3020
|
exports.computeTrussNumbers = computeTrussNumbers;
|
|
3021
|
+
exports.cosine = cosine;
|
|
2441
3022
|
exports.countEdgesOfType = require_utils.countEdgesOfType;
|
|
2442
3023
|
exports.countNodesOfType = require_utils.countNodesOfType;
|
|
2443
3024
|
exports.createGPUContext = require_gpu.createGPUContext;
|
|
@@ -2446,6 +3027,8 @@ exports.csrToGPUBuffers = require_gpu.csrToGPUBuffers;
|
|
|
2446
3027
|
exports.degreeSum = degreeSum;
|
|
2447
3028
|
exports.detectWebGPU = require_gpu.detectWebGPU;
|
|
2448
3029
|
exports.dfs = require_traversal.dfs;
|
|
3030
|
+
exports.dfsPriority = dfsPriority;
|
|
3031
|
+
exports.dfsPriorityFn = dfsPriorityFn;
|
|
2449
3032
|
exports.dfsWithPath = require_traversal.dfsWithPath;
|
|
2450
3033
|
exports.dome = dome;
|
|
2451
3034
|
exports.domeHighDegree = domeHighDegree;
|
|
@@ -2467,9 +3050,12 @@ exports.getMotifName = getMotifName;
|
|
|
2467
3050
|
exports.graphToCSR = require_gpu.graphToCSR;
|
|
2468
3051
|
exports.grasp = require_seeds.grasp;
|
|
2469
3052
|
exports.hae = hae;
|
|
3053
|
+
exports.hittingTime = hittingTime;
|
|
3054
|
+
exports.hubPromoted = hubPromoted;
|
|
2470
3055
|
exports.isWebGPUAvailable = require_gpu.isWebGPUAvailable;
|
|
2471
3056
|
exports.jaccard = jaccard;
|
|
2472
3057
|
exports.jaccardArithmetic = jaccardArithmetic;
|
|
3058
|
+
exports.kHop = kHop;
|
|
2473
3059
|
exports.katz = katz;
|
|
2474
3060
|
exports.lace = lace;
|
|
2475
3061
|
exports.localClusteringCoefficient = require_utils.localClusteringCoefficient;
|
|
@@ -2483,20 +3069,24 @@ exports.normaliseFeatures = require_kmeans.normaliseFeatures;
|
|
|
2483
3069
|
exports.zScoreNormalise = require_kmeans.normaliseFeatures;
|
|
2484
3070
|
exports.normalisedEntropy = require_utils.normalisedEntropy;
|
|
2485
3071
|
exports.notch = notch;
|
|
3072
|
+
exports.overlapCoefficient = overlapCoefficient;
|
|
2486
3073
|
exports.pagerank = pagerank;
|
|
2487
3074
|
exports.parse = parse;
|
|
2488
3075
|
exports.pipe = pipe;
|
|
2489
3076
|
exports.randomPriority = randomPriority;
|
|
2490
3077
|
exports.randomRanking = randomRanking;
|
|
3078
|
+
exports.randomWalk = randomWalk;
|
|
2491
3079
|
exports.reach = reach;
|
|
2492
3080
|
exports.readBufferToCPU = require_gpu.readBufferToCPU;
|
|
2493
3081
|
exports.resistanceDistance = resistanceDistance;
|
|
3082
|
+
exports.resourceAllocation = resourceAllocation;
|
|
2494
3083
|
exports.sage = sage;
|
|
2495
3084
|
exports.scale = scale;
|
|
2496
3085
|
exports.shannonEntropy = require_utils.shannonEntropy;
|
|
2497
3086
|
exports.shortest = shortest;
|
|
2498
3087
|
exports.sift = sift;
|
|
2499
3088
|
exports.skew = skew;
|
|
3089
|
+
exports.sorensen = sorensen;
|
|
2500
3090
|
exports.span = span;
|
|
2501
3091
|
exports.standardBfs = standardBfs;
|
|
2502
3092
|
exports.stratified = require_seeds.stratified;
|