graphwise 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/__test__/fixtures/graphs/index.d.ts +1 -0
  2. package/dist/__test__/fixtures/graphs/index.d.ts.map +1 -1
  3. package/dist/__test__/fixtures/graphs/linear-chain.d.ts +18 -0
  4. package/dist/__test__/fixtures/graphs/linear-chain.d.ts.map +1 -0
  5. package/dist/__test__/fixtures/helpers.d.ts +20 -1
  6. package/dist/__test__/fixtures/helpers.d.ts.map +1 -1
  7. package/dist/expansion/comparison.integration.test.d.ts +14 -0
  8. package/dist/expansion/comparison.integration.test.d.ts.map +1 -0
  9. package/dist/expansion/edge.d.ts.map +1 -1
  10. package/dist/expansion/flux.d.ts.map +1 -1
  11. package/dist/expansion/fuse.d.ts.map +1 -1
  12. package/dist/expansion/lace.d.ts.map +1 -1
  13. package/dist/expansion/maze.d.ts.map +1 -1
  14. package/dist/expansion/priority-helpers.d.ts +43 -0
  15. package/dist/expansion/priority-helpers.d.ts.map +1 -0
  16. package/dist/expansion/sage.d.ts.map +1 -1
  17. package/dist/expansion/sift.d.ts.map +1 -1
  18. package/dist/expansion/warp.d.ts.map +1 -1
  19. package/dist/index/index.cjs +186 -307
  20. package/dist/index/index.cjs.map +1 -1
  21. package/dist/index/index.js +187 -309
  22. package/dist/index/index.js.map +1 -1
  23. package/dist/ranking/baselines/betweenness.d.ts.map +1 -1
  24. package/dist/ranking/baselines/communicability.d.ts.map +1 -1
  25. package/dist/ranking/baselines/degree-sum.d.ts.map +1 -1
  26. package/dist/ranking/baselines/hitting-time.d.ts.map +1 -1
  27. package/dist/ranking/baselines/jaccard-arithmetic.d.ts.map +1 -1
  28. package/dist/ranking/baselines/katz.d.ts.map +1 -1
  29. package/dist/ranking/baselines/pagerank.d.ts.map +1 -1
  30. package/dist/ranking/baselines/random-ranking.d.ts.map +1 -1
  31. package/dist/ranking/baselines/resistance-distance.d.ts.map +1 -1
  32. package/dist/ranking/baselines/shortest.d.ts.map +1 -1
  33. package/dist/ranking/baselines/utils.d.ts +20 -0
  34. package/dist/ranking/baselines/utils.d.ts.map +1 -0
  35. package/dist/ranking/baselines/widest-path.d.ts.map +1 -1
  36. package/dist/ranking/mi/adaptive.d.ts.map +1 -1
  37. package/dist/ranking/mi/comparison.integration.test.d.ts +18 -0
  38. package/dist/ranking/mi/comparison.integration.test.d.ts.map +1 -0
  39. package/dist/ranking/mi/etch.d.ts.map +1 -1
  40. package/dist/ranking/mi/jaccard.d.ts.map +1 -1
  41. package/dist/ranking/mi/notch.d.ts.map +1 -1
  42. package/dist/ranking/mi/scale.d.ts.map +1 -1
  43. package/dist/ranking/mi/skew.d.ts.map +1 -1
  44. package/dist/ranking/mi/span.d.ts.map +1 -1
  45. package/dist/utils/index.cjs +22 -0
  46. package/dist/utils/index.cjs.map +1 -1
  47. package/dist/utils/index.js +22 -1
  48. package/dist/utils/index.js.map +1 -1
  49. package/dist/utils/neighbours.d.ts +23 -0
  50. package/dist/utils/neighbours.d.ts.map +1 -1
  51. package/package.json +1 -1
@@ -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
@@ -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 { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
420
- if (union === 0) return 0;
421
- const score = intersection / union;
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
- for (const path of context.discoveredPaths) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
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 - MI(source, neighbour)
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
- const graph = context.graph;
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
- * PIPE priority function.
633
+ * WARP priority function.
608
634
  *
609
635
  * Priority = 1 / (1 + bridge_score)
610
- * Bridge score = neighbourhood overlap with other frontiers
611
- * Higher bridge score = more likely to be on paths = explored first
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
- const graph = context.graph;
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
- * SAGE priority function.
665
+ * FUSE priority function.
643
666
  *
644
- * Combines degree with salience:
645
- * Priority = (1 - w) * degree + w * (1 - avg_salience)
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 graph = context.graph;
650
- const degree = context.degree;
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 (phase 2).
697
+ * REACH (SIFT) priority function.
684
698
  *
685
- * Uses learned MI threshold to prioritise high-MI edges.
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 graph = context.graph;
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,7 +753,7 @@ 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 = bridgeScore(nodeId, context);
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
759
  else if (density >= densityThreshold) return 1 / (degree + 1);
@@ -1405,14 +1402,13 @@ function hubPromoted(graph, source, target, config) {
1405
1402
  */
1406
1403
  function scale(graph, source, target, config) {
1407
1404
  const { epsilon = 1e-10 } = config ?? {};
1408
- const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
1409
- const jaccard = union > 0 ? intersection / union : 0;
1405
+ const { jaccard: jaccardScore } = computeJaccard(graph, source, target);
1410
1406
  const n = graph.nodeCount;
1411
1407
  const m = graph.edgeCount;
1412
1408
  const possibleEdges = n * (n - 1);
1413
1409
  const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
1414
1410
  if (density === 0) return epsilon;
1415
- const score = jaccard / density;
1411
+ const score = jaccardScore / density;
1416
1412
  return Math.max(epsilon, score);
1417
1413
  }
1418
1414
  //#endregion
@@ -1422,14 +1418,13 @@ function scale(graph, source, target, config) {
1422
1418
  */
1423
1419
  function skew(graph, source, target, config) {
1424
1420
  const { epsilon = 1e-10 } = config ?? {};
1425
- const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
1426
- const jaccard = union > 0 ? intersection / union : 0;
1421
+ const { jaccard: jaccardScore } = computeJaccard(graph, source, target);
1427
1422
  const N = graph.nodeCount;
1428
1423
  const sourceDegree = graph.degree(source);
1429
1424
  const targetDegree = graph.degree(target);
1430
1425
  const sourceIdf = Math.log(N / (sourceDegree + 1));
1431
1426
  const targetIdf = Math.log(N / (targetDegree + 1));
1432
- const score = jaccard * sourceIdf * targetIdf;
1427
+ const score = jaccardScore * sourceIdf * targetIdf;
1433
1428
  return Math.max(epsilon, score);
1434
1429
  }
1435
1430
  //#endregion
@@ -1439,11 +1434,10 @@ function skew(graph, source, target, config) {
1439
1434
  */
1440
1435
  function span(graph, source, target, config) {
1441
1436
  const { epsilon = 1e-10 } = config ?? {};
1442
- const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
1443
- const jaccard = union > 0 ? intersection / union : 0;
1437
+ const { jaccard: jaccardScore } = computeJaccard(graph, source, target);
1444
1438
  const sourceCc = localClusteringCoefficient(graph, source);
1445
1439
  const targetCc = localClusteringCoefficient(graph, target);
1446
- const score = jaccard * (1 - Math.max(sourceCc, targetCc));
1440
+ const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
1447
1441
  return Math.max(epsilon, score);
1448
1442
  }
1449
1443
  //#endregion
@@ -1453,13 +1447,12 @@ function span(graph, source, target, config) {
1453
1447
  */
1454
1448
  function etch(graph, source, target, config) {
1455
1449
  const { epsilon = 1e-10 } = config ?? {};
1456
- const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
1457
- const jaccard = union > 0 ? intersection / union : 0;
1450
+ const { jaccard: jaccardScore } = computeJaccard(graph, source, target);
1458
1451
  const edge = graph.getEdge(source, target);
1459
- if (edge?.type === void 0) return Math.max(epsilon, jaccard);
1452
+ if (edge?.type === void 0) return Math.max(epsilon, jaccardScore);
1460
1453
  const edgeTypeCount = countEdgesOfType(graph, edge.type);
1461
- if (edgeTypeCount === 0) return Math.max(epsilon, jaccard);
1462
- const score = jaccard * Math.log(graph.edgeCount / edgeTypeCount);
1454
+ if (edgeTypeCount === 0) return Math.max(epsilon, jaccardScore);
1455
+ const score = jaccardScore * Math.log(graph.edgeCount / edgeTypeCount);
1463
1456
  return Math.max(epsilon, score);
1464
1457
  }
1465
1458
  //#endregion
@@ -1469,17 +1462,16 @@ function etch(graph, source, target, config) {
1469
1462
  */
1470
1463
  function notch(graph, source, target, config) {
1471
1464
  const { epsilon = 1e-10 } = config ?? {};
1472
- const { intersection, union } = neighbourOverlap(neighbourSet(graph, source, target), neighbourSet(graph, target, source));
1473
- const jaccard = union > 0 ? intersection / union : 0;
1465
+ const { jaccard: jaccardScore } = computeJaccard(graph, source, target);
1474
1466
  const sourceNode = graph.getNode(source);
1475
1467
  const targetNode = graph.getNode(target);
1476
- if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccard);
1468
+ if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccardScore);
1477
1469
  const sourceTypeCount = countNodesOfType(graph, sourceNode.type);
1478
1470
  const targetTypeCount = countNodesOfType(graph, targetNode.type);
1479
- if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccard);
1471
+ if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccardScore);
1480
1472
  const sourceRarity = Math.log(graph.nodeCount / sourceTypeCount);
1481
1473
  const targetRarity = Math.log(graph.nodeCount / targetTypeCount);
1482
- const score = jaccard * sourceRarity * targetRarity;
1474
+ const score = jaccardScore * sourceRarity * targetRarity;
1483
1475
  return Math.max(epsilon, score);
1484
1476
  }
1485
1477
  //#endregion
@@ -1498,13 +1490,12 @@ function notch(graph, source, target, config) {
1498
1490
  */
1499
1491
  function adaptive(graph, source, target, config) {
1500
1492
  const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
1501
- const structural = jaccard(graph, source, target, { epsilon });
1493
+ const { jaccard: jaccardScore, sourceNeighbours, targetNeighbours } = computeJaccard(graph, source, target);
1494
+ const structural = sourceNeighbours.size === 0 && targetNeighbours.size === 0 ? 0 : Math.max(epsilon, jaccardScore);
1502
1495
  const degreeComponent = adamicAdar(graph, source, target, {
1503
1496
  epsilon,
1504
1497
  normalise: true
1505
1498
  });
1506
- const sourceNeighbours = neighbourSet(graph, source, target);
1507
- const targetNeighbours = neighbourSet(graph, target, source);
1508
1499
  let overlap;
1509
1500
  if (sourceNeighbours.size > 0 && targetNeighbours.size > 0) {
1510
1501
  const { intersection } = neighbourOverlap(sourceNeighbours, targetNeighbours);
@@ -1516,6 +1507,42 @@ function adaptive(graph, source, target, config) {
1516
1507
  return Math.max(epsilon, Math.min(1, score));
1517
1508
  }
1518
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
1519
1546
  //#region src/ranking/baselines/shortest.ts
1520
1547
  /**
1521
1548
  * Rank paths by length (shortest first).
@@ -1533,18 +1560,10 @@ function shortest(_graph, paths, config) {
1533
1560
  paths: [],
1534
1561
  method: "shortest"
1535
1562
  };
1536
- const scored = paths.map((path) => ({
1563
+ return normaliseAndRank(paths, paths.map((path) => ({
1537
1564
  path,
1538
1565
  score: 1 / path.nodes.length
1539
- }));
1540
- const maxScore = Math.max(...scored.map((s) => s.score));
1541
- return {
1542
- paths: scored.map(({ path, score }) => ({
1543
- ...path,
1544
- score: includeScores ? score / maxScore : score / maxScore
1545
- })).sort((a, b) => b.score - a.score),
1546
- method: "shortest"
1547
- };
1566
+ })), "shortest", includeScores);
1548
1567
  }
1549
1568
  //#endregion
1550
1569
  //#region src/ranking/baselines/degree-sum.ts
@@ -1562,29 +1581,14 @@ function degreeSum(graph, paths, config) {
1562
1581
  paths: [],
1563
1582
  method: "degree-sum"
1564
1583
  };
1565
- const scored = paths.map((path) => {
1584
+ return normaliseAndRank(paths, paths.map((path) => {
1566
1585
  let degreeSum = 0;
1567
1586
  for (const nodeId of path.nodes) degreeSum += graph.degree(nodeId);
1568
1587
  return {
1569
1588
  path,
1570
1589
  score: degreeSum
1571
1590
  };
1572
- });
1573
- const maxScore = Math.max(...scored.map((s) => s.score));
1574
- if (maxScore === 0) return {
1575
- paths: paths.map((path) => ({
1576
- ...path,
1577
- score: 0
1578
- })),
1579
- method: "degree-sum"
1580
- };
1581
- return {
1582
- paths: scored.map(({ path, score }) => ({
1583
- ...path,
1584
- score: includeScores ? score / maxScore : score
1585
- })).sort((a, b) => b.score - a.score),
1586
- method: "degree-sum"
1587
- };
1591
+ }), "degree-sum", includeScores);
1588
1592
  }
1589
1593
  //#endregion
1590
1594
  //#region src/ranking/baselines/widest-path.ts
@@ -1602,7 +1606,7 @@ function widestPath(graph, paths, config) {
1602
1606
  paths: [],
1603
1607
  method: "widest-path"
1604
1608
  };
1605
- const scored = paths.map((path) => {
1609
+ return normaliseAndRank(paths, paths.map((path) => {
1606
1610
  if (path.nodes.length < 2) return {
1607
1611
  path,
1608
1612
  score: 1
@@ -1619,22 +1623,7 @@ function widestPath(graph, paths, config) {
1619
1623
  path,
1620
1624
  score: minSimilarity === Number.POSITIVE_INFINITY ? 1 : minSimilarity
1621
1625
  };
1622
- });
1623
- const maxScore = Math.max(...scored.map((s) => s.score));
1624
- if (maxScore === 0) return {
1625
- paths: paths.map((path) => ({
1626
- ...path,
1627
- score: 0
1628
- })),
1629
- method: "widest-path"
1630
- };
1631
- return {
1632
- paths: scored.map(({ path, score }) => ({
1633
- ...path,
1634
- score: includeScores ? score / maxScore : score / maxScore
1635
- })).sort((a, b) => b.score - a.score),
1636
- method: "widest-path"
1637
- };
1626
+ }), "widest-path", includeScores);
1638
1627
  }
1639
1628
  //#endregion
1640
1629
  //#region src/ranking/baselines/jaccard-arithmetic.ts
@@ -1652,7 +1641,7 @@ function jaccardArithmetic(graph, paths, config) {
1652
1641
  paths: [],
1653
1642
  method: "jaccard-arithmetic"
1654
1643
  };
1655
- const scored = paths.map((path) => {
1644
+ return normaliseAndRank(paths, paths.map((path) => {
1656
1645
  if (path.nodes.length < 2) return {
1657
1646
  path,
1658
1647
  score: 1
@@ -1671,22 +1660,7 @@ function jaccardArithmetic(graph, paths, config) {
1671
1660
  path,
1672
1661
  score: edgeCount > 0 ? similaritySum / edgeCount : 1
1673
1662
  };
1674
- });
1675
- const maxScore = Math.max(...scored.map((s) => s.score));
1676
- if (maxScore === 0) return {
1677
- paths: paths.map((path) => ({
1678
- ...path,
1679
- score: 0
1680
- })),
1681
- method: "jaccard-arithmetic"
1682
- };
1683
- return {
1684
- paths: scored.map(({ path, score }) => ({
1685
- ...path,
1686
- score: includeScores ? score / maxScore : score
1687
- })).sort((a, b) => b.score - a.score),
1688
- method: "jaccard-arithmetic"
1689
- };
1663
+ }), "jaccard-arithmetic", includeScores);
1690
1664
  }
1691
1665
  //#endregion
1692
1666
  //#region src/ranking/baselines/pagerank.ts
@@ -1747,29 +1721,14 @@ function pagerank(graph, paths, config) {
1747
1721
  method: "pagerank"
1748
1722
  };
1749
1723
  const ranks = computePageRank(graph);
1750
- const scored = paths.map((path) => {
1724
+ return normaliseAndRank(paths, paths.map((path) => {
1751
1725
  let prSum = 0;
1752
1726
  for (const nodeId of path.nodes) prSum += ranks.get(nodeId) ?? 0;
1753
1727
  return {
1754
1728
  path,
1755
1729
  score: prSum
1756
1730
  };
1757
- });
1758
- const maxScore = Math.max(...scored.map((s) => s.score));
1759
- if (maxScore === 0) return {
1760
- paths: paths.map((path) => ({
1761
- ...path,
1762
- score: 0
1763
- })),
1764
- method: "pagerank"
1765
- };
1766
- return {
1767
- paths: scored.map(({ path, score }) => ({
1768
- ...path,
1769
- score: includeScores ? score / maxScore : score / maxScore
1770
- })).sort((a, b) => b.score - a.score),
1771
- method: "pagerank"
1772
- };
1731
+ }), "pagerank", includeScores);
1773
1732
  }
1774
1733
  //#endregion
1775
1734
  //#region src/ranking/baselines/betweenness.ts
@@ -1854,29 +1813,14 @@ function betweenness(graph, paths, config) {
1854
1813
  method: "betweenness"
1855
1814
  };
1856
1815
  const bcMap = computeBetweenness(graph);
1857
- const scored = paths.map((path) => {
1816
+ return normaliseAndRank(paths, paths.map((path) => {
1858
1817
  let bcSum = 0;
1859
1818
  for (const nodeId of path.nodes) bcSum += bcMap.get(nodeId) ?? 0;
1860
1819
  return {
1861
1820
  path,
1862
1821
  score: bcSum
1863
1822
  };
1864
- });
1865
- const maxScore = Math.max(...scored.map((s) => s.score));
1866
- if (maxScore === 0) return {
1867
- paths: paths.map((path) => ({
1868
- ...path,
1869
- score: 0
1870
- })),
1871
- method: "betweenness"
1872
- };
1873
- return {
1874
- paths: scored.map(({ path, score }) => ({
1875
- ...path,
1876
- score: includeScores ? score / maxScore : score / maxScore
1877
- })).sort((a, b) => b.score - a.score),
1878
- method: "betweenness"
1879
- };
1823
+ }), "betweenness", includeScores);
1880
1824
  }
1881
1825
  //#endregion
1882
1826
  //#region src/ranking/baselines/katz.ts
@@ -1939,7 +1883,7 @@ function katz(graph, paths, config) {
1939
1883
  paths: [],
1940
1884
  method: "katz"
1941
1885
  };
1942
- const scored = paths.map((path) => {
1886
+ return normaliseAndRank(paths, paths.map((path) => {
1943
1887
  const source = path.nodes[0];
1944
1888
  const target = path.nodes[path.nodes.length - 1];
1945
1889
  if (source === void 0 || target === void 0) return {
@@ -1950,22 +1894,7 @@ function katz(graph, paths, config) {
1950
1894
  path,
1951
1895
  score: computeKatz(graph, source, target)
1952
1896
  };
1953
- });
1954
- const maxScore = Math.max(...scored.map((s) => s.score));
1955
- if (maxScore === 0) return {
1956
- paths: paths.map((path) => ({
1957
- ...path,
1958
- score: 0
1959
- })),
1960
- method: "katz"
1961
- };
1962
- return {
1963
- paths: scored.map(({ path, score }) => ({
1964
- ...path,
1965
- score: includeScores ? score / maxScore : score / maxScore
1966
- })).sort((a, b) => b.score - a.score),
1967
- method: "katz"
1968
- };
1897
+ }), "katz", includeScores);
1969
1898
  }
1970
1899
  //#endregion
1971
1900
  //#region src/ranking/baselines/communicability.ts
@@ -2027,7 +1956,7 @@ function communicability(graph, paths, config) {
2027
1956
  paths: [],
2028
1957
  method: "communicability"
2029
1958
  };
2030
- const scored = paths.map((path) => {
1959
+ return normaliseAndRank(paths, paths.map((path) => {
2031
1960
  const source = path.nodes[0];
2032
1961
  const target = path.nodes[path.nodes.length - 1];
2033
1962
  if (source === void 0 || target === void 0) return {
@@ -2038,22 +1967,7 @@ function communicability(graph, paths, config) {
2038
1967
  path,
2039
1968
  score: computeCommunicability(graph, source, target)
2040
1969
  };
2041
- });
2042
- const maxScore = Math.max(...scored.map((s) => s.score));
2043
- if (maxScore === 0) return {
2044
- paths: paths.map((path) => ({
2045
- ...path,
2046
- score: 0
2047
- })),
2048
- method: "communicability"
2049
- };
2050
- return {
2051
- paths: scored.map(({ path, score }) => ({
2052
- ...path,
2053
- score: includeScores ? score / maxScore : score / maxScore
2054
- })).sort((a, b) => b.score - a.score),
2055
- method: "communicability"
2056
- };
1970
+ }), "communicability", includeScores);
2057
1971
  }
2058
1972
  //#endregion
2059
1973
  //#region src/ranking/baselines/resistance-distance.ts
@@ -2170,7 +2084,7 @@ function resistanceDistance(graph, paths, config) {
2170
2084
  };
2171
2085
  const nodeCount = Array.from(graph.nodeIds()).length;
2172
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.`);
2173
- const scored = paths.map((path) => {
2087
+ return normaliseAndRank(paths, paths.map((path) => {
2174
2088
  const source = path.nodes[0];
2175
2089
  const target = path.nodes[path.nodes.length - 1];
2176
2090
  if (source === void 0 || target === void 0) return {
@@ -2181,22 +2095,7 @@ function resistanceDistance(graph, paths, config) {
2181
2095
  path,
2182
2096
  score: 1 / computeResistance(graph, source, target)
2183
2097
  };
2184
- });
2185
- const maxScore = Math.max(...scored.map((s) => s.score));
2186
- if (maxScore === 0) return {
2187
- paths: paths.map((path) => ({
2188
- ...path,
2189
- score: 0
2190
- })),
2191
- method: "resistance-distance"
2192
- };
2193
- return {
2194
- paths: scored.map(({ path, score }) => ({
2195
- ...path,
2196
- score: includeScores ? score / maxScore : score / maxScore
2197
- })).sort((a, b) => b.score - a.score),
2198
- method: "resistance-distance"
2199
- };
2098
+ }), "resistance-distance", includeScores);
2200
2099
  }
2201
2100
  //#endregion
2202
2101
  //#region src/ranking/baselines/random-ranking.ts
@@ -2230,27 +2129,12 @@ function randomRanking(_graph, paths, config) {
2230
2129
  paths: [],
2231
2130
  method: "random"
2232
2131
  };
2233
- const scored = paths.map((path) => {
2132
+ return normaliseAndRank(paths, paths.map((path) => {
2234
2133
  return {
2235
2134
  path,
2236
2135
  score: seededRandom(path.nodes.join(","), seed)
2237
2136
  };
2238
- });
2239
- const maxScore = Math.max(...scored.map((s) => s.score));
2240
- if (maxScore === 0) return {
2241
- paths: paths.map((path) => ({
2242
- ...path,
2243
- score: 0
2244
- })),
2245
- method: "random"
2246
- };
2247
- return {
2248
- paths: scored.map(({ path, score }) => ({
2249
- ...path,
2250
- score: includeScores ? score / maxScore : score / maxScore
2251
- })).sort((a, b) => b.score - a.score),
2252
- method: "random"
2253
- };
2137
+ }), "random", includeScores);
2254
2138
  }
2255
2139
  //#endregion
2256
2140
  //#region src/ranking/baselines/hitting-time.ts
@@ -2489,20 +2373,14 @@ function hittingTime(graph, paths, config) {
2489
2373
  };
2490
2374
  });
2491
2375
  const maxScore = Math.max(...scored.map((s) => s.score));
2492
- if (maxScore === 0 || !Number.isFinite(maxScore)) return {
2376
+ if (!Number.isFinite(maxScore)) return {
2493
2377
  paths: paths.map((path) => ({
2494
2378
  ...path,
2495
2379
  score: 0
2496
2380
  })),
2497
2381
  method: "hitting-time"
2498
2382
  };
2499
- return {
2500
- paths: scored.map(({ path, score }) => ({
2501
- ...path,
2502
- score: includeScores ? score / maxScore : score / maxScore
2503
- })).sort((a, b) => b.score - a.score),
2504
- method: "hitting-time"
2505
- };
2383
+ return normaliseAndRank(paths, scored, "hitting-time", includeScores);
2506
2384
  }
2507
2385
  //#endregion
2508
2386
  //#region src/extraction/ego-network.ts
@@ -3122,6 +3000,6 @@ function filterSubgraph(graph, options) {
3122
3000
  return result;
3123
3001
  }
3124
3002
  //#endregion
3125
- export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adaptive, approximateClusteringCoefficient, assertWebGPUAvailable, base, batchClusteringCoefficients, betweenness, bfs, bfsWithPath, communicability, 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 };
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 };
3126
3004
 
3127
3005
  //# sourceMappingURL=index.js.map