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