graphwise 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- 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/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/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/index/index.cjs +716 -5
- package/dist/index/index.cjs.map +1 -1
- package/dist/index/index.js +707 -6
- package/dist/index/index.js.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/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/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/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/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/types.d.ts +1 -1
- package/dist/ranking/mi/types.d.ts.map +1 -1
- package/dist/schemas/graph.d.ts +1 -1
- package/package.json +1 -1
package/dist/index/index.cjs
CHANGED
|
@@ -24,7 +24,7 @@ function degreePriority(_nodeId, context) {
|
|
|
24
24
|
function base(graph, seeds, config) {
|
|
25
25
|
const startTime = performance.now();
|
|
26
26
|
const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
|
|
27
|
-
if (seeds.length === 0) return emptyResult("base", startTime);
|
|
27
|
+
if (seeds.length === 0) return emptyResult$2("base", startTime);
|
|
28
28
|
const numFrontiers = seeds.length;
|
|
29
29
|
const allVisited = /* @__PURE__ */ new Set();
|
|
30
30
|
const combinedVisited = /* @__PURE__ */ new Map();
|
|
@@ -105,7 +105,7 @@ function base(graph, seeds, config) {
|
|
|
105
105
|
const otherVisited = visitedByFrontier[otherFrontier];
|
|
106
106
|
if (otherVisited === void 0) continue;
|
|
107
107
|
if (otherVisited.has(nodeId)) {
|
|
108
|
-
const path = reconstructPath(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
|
|
108
|
+
const path = reconstructPath$1(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
|
|
109
109
|
if (path !== null) {
|
|
110
110
|
discoveredPaths.push(path);
|
|
111
111
|
if (debug) console.log(`[BASE] Path found: ${path.nodes.join(" -> ")}`);
|
|
@@ -170,7 +170,7 @@ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, al
|
|
|
170
170
|
/**
|
|
171
171
|
* Reconstruct path from collision point.
|
|
172
172
|
*/
|
|
173
|
-
function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seeds) {
|
|
173
|
+
function reconstructPath$1(collisionNode, frontierA, frontierB, predecessors, seeds) {
|
|
174
174
|
const pathA = [collisionNode];
|
|
175
175
|
const predA = predecessors[frontierA];
|
|
176
176
|
if (predA !== void 0) {
|
|
@@ -206,7 +206,7 @@ function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seed
|
|
|
206
206
|
/**
|
|
207
207
|
* Create an empty result for early termination.
|
|
208
208
|
*/
|
|
209
|
-
function emptyResult(algorithm, startTime) {
|
|
209
|
+
function emptyResult$2(algorithm, startTime) {
|
|
210
210
|
return {
|
|
211
211
|
paths: [],
|
|
212
212
|
sampledNodes: /* @__PURE__ */ new Set(),
|
|
@@ -760,7 +760,7 @@ function fluxPriority(nodeId, context, densityThreshold, bridgeThreshold) {
|
|
|
760
760
|
const bridge = bridgeScore(nodeId, context);
|
|
761
761
|
const numFrontiers = new Set(context.visitedByFrontier.values()).size;
|
|
762
762
|
if ((numFrontiers > 0 ? bridge / numFrontiers : 0) >= bridgeThreshold) return 1 / (1 + bridge);
|
|
763
|
-
else if (density >= densityThreshold) return
|
|
763
|
+
else if (density >= densityThreshold) return 1 / (degree + 1);
|
|
764
764
|
else return degree;
|
|
765
765
|
}
|
|
766
766
|
/**
|
|
@@ -858,6 +858,346 @@ function randomPriority(graph, seeds, config) {
|
|
|
858
858
|
});
|
|
859
859
|
}
|
|
860
860
|
//#endregion
|
|
861
|
+
//#region src/expansion/dfs-priority.ts
|
|
862
|
+
/**
|
|
863
|
+
* DFS priority function: negative iteration produces LIFO ordering.
|
|
864
|
+
*
|
|
865
|
+
* Lower priority values are expanded first, so negating the iteration
|
|
866
|
+
* counter ensures the most recently enqueued node is always next.
|
|
867
|
+
*/
|
|
868
|
+
function dfsPriorityFn(_nodeId, context) {
|
|
869
|
+
return -context.iteration;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Run DFS-priority expansion (LIFO discovery order).
|
|
873
|
+
*
|
|
874
|
+
* Uses the BASE framework with a negative-iteration priority function,
|
|
875
|
+
* which causes the most recently discovered node to be expanded first —
|
|
876
|
+
* equivalent to depth-first search behaviour.
|
|
877
|
+
*
|
|
878
|
+
* @param graph - Source graph
|
|
879
|
+
* @param seeds - Seed nodes for expansion
|
|
880
|
+
* @param config - Expansion configuration
|
|
881
|
+
* @returns Expansion result with discovered paths
|
|
882
|
+
*/
|
|
883
|
+
function dfsPriority(graph, seeds, config) {
|
|
884
|
+
return base(graph, seeds, {
|
|
885
|
+
...config,
|
|
886
|
+
priority: dfsPriorityFn
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/expansion/k-hop.ts
|
|
891
|
+
/**
|
|
892
|
+
* Run k-hop expansion (fixed-depth BFS).
|
|
893
|
+
*
|
|
894
|
+
* Explores all nodes reachable within exactly k hops of any seed using
|
|
895
|
+
* breadth-first search. Paths between seeds are detected when a node
|
|
896
|
+
* is reached by frontiers from two different seeds.
|
|
897
|
+
*
|
|
898
|
+
* @param graph - Source graph
|
|
899
|
+
* @param seeds - Seed nodes for expansion
|
|
900
|
+
* @param config - K-hop configuration (k defaults to 2)
|
|
901
|
+
* @returns Expansion result with discovered paths
|
|
902
|
+
*/
|
|
903
|
+
function kHop(graph, seeds, config) {
|
|
904
|
+
const startTime = performance.now();
|
|
905
|
+
const { k = 2 } = config ?? {};
|
|
906
|
+
if (seeds.length === 0) return emptyResult$1(startTime);
|
|
907
|
+
const visitedByFrontier = seeds.map(() => /* @__PURE__ */ new Map());
|
|
908
|
+
const firstVisitedBy = /* @__PURE__ */ new Map();
|
|
909
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
910
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
911
|
+
const discoveredPaths = [];
|
|
912
|
+
let iterations = 0;
|
|
913
|
+
let edgesTraversed = 0;
|
|
914
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
915
|
+
const seed = seeds[i];
|
|
916
|
+
if (seed === void 0) continue;
|
|
917
|
+
if (!graph.hasNode(seed.id)) continue;
|
|
918
|
+
visitedByFrontier[i]?.set(seed.id, null);
|
|
919
|
+
allVisited.add(seed.id);
|
|
920
|
+
if (!firstVisitedBy.has(seed.id)) firstVisitedBy.set(seed.id, i);
|
|
921
|
+
else {
|
|
922
|
+
const otherIdx = firstVisitedBy.get(seed.id) ?? -1;
|
|
923
|
+
if (otherIdx < 0) continue;
|
|
924
|
+
const fromSeed = seeds[otherIdx];
|
|
925
|
+
const toSeed = seeds[i];
|
|
926
|
+
if (fromSeed !== void 0 && toSeed !== void 0) discoveredPaths.push({
|
|
927
|
+
fromSeed,
|
|
928
|
+
toSeed,
|
|
929
|
+
nodes: [seed.id]
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
let currentLevel = seeds.map((s, i) => {
|
|
934
|
+
const frontier = visitedByFrontier[i];
|
|
935
|
+
if (frontier === void 0) return [];
|
|
936
|
+
return frontier.has(s.id) ? [s.id] : [];
|
|
937
|
+
});
|
|
938
|
+
for (let hop = 0; hop < k; hop++) {
|
|
939
|
+
const nextLevel = seeds.map(() => []);
|
|
940
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
941
|
+
const level = currentLevel[i];
|
|
942
|
+
if (level === void 0) continue;
|
|
943
|
+
const frontierVisited = visitedByFrontier[i];
|
|
944
|
+
if (frontierVisited === void 0) continue;
|
|
945
|
+
for (const nodeId of level) {
|
|
946
|
+
iterations++;
|
|
947
|
+
for (const neighbour of graph.neighbours(nodeId)) {
|
|
948
|
+
edgesTraversed++;
|
|
949
|
+
const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
|
|
950
|
+
let targets = sampledEdgeMap.get(s);
|
|
951
|
+
if (targets === void 0) {
|
|
952
|
+
targets = /* @__PURE__ */ new Set();
|
|
953
|
+
sampledEdgeMap.set(s, targets);
|
|
954
|
+
}
|
|
955
|
+
targets.add(t);
|
|
956
|
+
if (frontierVisited.has(neighbour)) continue;
|
|
957
|
+
frontierVisited.set(neighbour, nodeId);
|
|
958
|
+
allVisited.add(neighbour);
|
|
959
|
+
nextLevel[i]?.push(neighbour);
|
|
960
|
+
const previousFrontier = firstVisitedBy.get(neighbour);
|
|
961
|
+
if (previousFrontier !== void 0 && previousFrontier !== i) {
|
|
962
|
+
const fromSeed = seeds[previousFrontier];
|
|
963
|
+
const toSeed = seeds[i];
|
|
964
|
+
if (fromSeed !== void 0 && toSeed !== void 0) {
|
|
965
|
+
const path = reconstructPath(neighbour, previousFrontier, i, visitedByFrontier, seeds);
|
|
966
|
+
if (path !== null) {
|
|
967
|
+
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);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (!firstVisitedBy.has(neighbour)) firstVisitedBy.set(neighbour, i);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
currentLevel = nextLevel;
|
|
976
|
+
if (currentLevel.every((level) => level.length === 0)) break;
|
|
977
|
+
}
|
|
978
|
+
const endTime = performance.now();
|
|
979
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
980
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
981
|
+
return {
|
|
982
|
+
paths: discoveredPaths,
|
|
983
|
+
sampledNodes: allVisited,
|
|
984
|
+
sampledEdges: edgeTuples,
|
|
985
|
+
visitedPerFrontier: visitedByFrontier.map((m) => new Set(m.keys())),
|
|
986
|
+
stats: {
|
|
987
|
+
iterations,
|
|
988
|
+
nodesVisited: allVisited.size,
|
|
989
|
+
edgesTraversed,
|
|
990
|
+
pathsFound: discoveredPaths.length,
|
|
991
|
+
durationMs: endTime - startTime,
|
|
992
|
+
algorithm: "k-hop",
|
|
993
|
+
termination: "exhausted"
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Reconstruct the path between two colliding frontiers.
|
|
999
|
+
*/
|
|
1000
|
+
function reconstructPath(collisionNode, frontierA, frontierB, visitedByFrontier, seeds) {
|
|
1001
|
+
const seedA = seeds[frontierA];
|
|
1002
|
+
const seedB = seeds[frontierB];
|
|
1003
|
+
if (seedA === void 0 || seedB === void 0) return null;
|
|
1004
|
+
const pathA = [collisionNode];
|
|
1005
|
+
const predA = visitedByFrontier[frontierA];
|
|
1006
|
+
if (predA !== void 0) {
|
|
1007
|
+
let node = collisionNode;
|
|
1008
|
+
let pred = predA.get(node);
|
|
1009
|
+
while (pred !== null && pred !== void 0) {
|
|
1010
|
+
pathA.unshift(pred);
|
|
1011
|
+
node = pred;
|
|
1012
|
+
pred = predA.get(node);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const pathB = [];
|
|
1016
|
+
const predB = visitedByFrontier[frontierB];
|
|
1017
|
+
if (predB !== void 0) {
|
|
1018
|
+
let node = collisionNode;
|
|
1019
|
+
let pred = predB.get(node);
|
|
1020
|
+
while (pred !== null && pred !== void 0) {
|
|
1021
|
+
pathB.push(pred);
|
|
1022
|
+
node = pred;
|
|
1023
|
+
pred = predB.get(node);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
fromSeed: seedA,
|
|
1028
|
+
toSeed: seedB,
|
|
1029
|
+
nodes: [...pathA, ...pathB]
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Create an empty result for early termination (no seeds).
|
|
1034
|
+
*/
|
|
1035
|
+
function emptyResult$1(startTime) {
|
|
1036
|
+
return {
|
|
1037
|
+
paths: [],
|
|
1038
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
1039
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
1040
|
+
visitedPerFrontier: [],
|
|
1041
|
+
stats: {
|
|
1042
|
+
iterations: 0,
|
|
1043
|
+
nodesVisited: 0,
|
|
1044
|
+
edgesTraversed: 0,
|
|
1045
|
+
pathsFound: 0,
|
|
1046
|
+
durationMs: performance.now() - startTime,
|
|
1047
|
+
algorithm: "k-hop",
|
|
1048
|
+
termination: "exhausted"
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/expansion/random-walk.ts
|
|
1054
|
+
/**
|
|
1055
|
+
* Mulberry32 seeded PRNG — fast, compact, and high-quality for simulation.
|
|
1056
|
+
*
|
|
1057
|
+
* Returns a closure that yields the next pseudo-random value in [0, 1)
|
|
1058
|
+
* on each call.
|
|
1059
|
+
*
|
|
1060
|
+
* @param seed - 32-bit integer seed
|
|
1061
|
+
*/
|
|
1062
|
+
function mulberry32(seed) {
|
|
1063
|
+
let s = seed;
|
|
1064
|
+
return () => {
|
|
1065
|
+
s += 1831565813;
|
|
1066
|
+
let t = s;
|
|
1067
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1068
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1069
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Run random-walk-with-restart expansion.
|
|
1074
|
+
*
|
|
1075
|
+
* For each seed, performs `walks` independent random walks of up to
|
|
1076
|
+
* `walkLength` steps. At each step the walk either restarts (with
|
|
1077
|
+
* probability `restartProbability`) or moves to a uniformly sampled
|
|
1078
|
+
* neighbour. All visited nodes and traversed edges are collected.
|
|
1079
|
+
*
|
|
1080
|
+
* Inter-seed paths are detected when a walk reaches a node that was
|
|
1081
|
+
* previously reached by a walk originating from a different seed.
|
|
1082
|
+
* The recorded path contains only the two seed endpoints rather than
|
|
1083
|
+
* the full walk trajectory, consistent with the ExpansionPath contract.
|
|
1084
|
+
*
|
|
1085
|
+
* @param graph - Source graph
|
|
1086
|
+
* @param seeds - Seed nodes for expansion
|
|
1087
|
+
* @param config - Random walk configuration
|
|
1088
|
+
* @returns Expansion result with discovered paths
|
|
1089
|
+
*/
|
|
1090
|
+
function randomWalk(graph, seeds, config) {
|
|
1091
|
+
const startTime = performance.now();
|
|
1092
|
+
const { restartProbability = .15, walks = 10, walkLength = 20, seed = 0 } = config ?? {};
|
|
1093
|
+
if (seeds.length === 0) return emptyResult(startTime);
|
|
1094
|
+
const rand = mulberry32(seed);
|
|
1095
|
+
const firstVisitedBySeed = /* @__PURE__ */ new Map();
|
|
1096
|
+
const allVisited = /* @__PURE__ */ new Set();
|
|
1097
|
+
const sampledEdgeMap = /* @__PURE__ */ new Map();
|
|
1098
|
+
const discoveredPaths = [];
|
|
1099
|
+
let iterations = 0;
|
|
1100
|
+
let edgesTraversed = 0;
|
|
1101
|
+
const visitedPerFrontier = seeds.map(() => /* @__PURE__ */ new Set());
|
|
1102
|
+
for (let seedIdx = 0; seedIdx < seeds.length; seedIdx++) {
|
|
1103
|
+
const seed_ = seeds[seedIdx];
|
|
1104
|
+
if (seed_ === void 0) continue;
|
|
1105
|
+
const seedId = seed_.id;
|
|
1106
|
+
if (!graph.hasNode(seedId)) continue;
|
|
1107
|
+
if (!firstVisitedBySeed.has(seedId)) firstVisitedBySeed.set(seedId, seedIdx);
|
|
1108
|
+
allVisited.add(seedId);
|
|
1109
|
+
visitedPerFrontier[seedIdx]?.add(seedId);
|
|
1110
|
+
for (let w = 0; w < walks; w++) {
|
|
1111
|
+
let current = seedId;
|
|
1112
|
+
for (let step = 0; step < walkLength; step++) {
|
|
1113
|
+
iterations++;
|
|
1114
|
+
if (rand() < restartProbability) {
|
|
1115
|
+
current = seedId;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const neighbourList = [];
|
|
1119
|
+
for (const nb of graph.neighbours(current)) neighbourList.push(nb);
|
|
1120
|
+
if (neighbourList.length === 0) {
|
|
1121
|
+
current = seedId;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const next = neighbourList[Math.floor(rand() * neighbourList.length)];
|
|
1125
|
+
if (next === void 0) {
|
|
1126
|
+
current = seedId;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
edgesTraversed++;
|
|
1130
|
+
const [s, t] = current < next ? [current, next] : [next, current];
|
|
1131
|
+
let targets = sampledEdgeMap.get(s);
|
|
1132
|
+
if (targets === void 0) {
|
|
1133
|
+
targets = /* @__PURE__ */ new Set();
|
|
1134
|
+
sampledEdgeMap.set(s, targets);
|
|
1135
|
+
}
|
|
1136
|
+
targets.add(t);
|
|
1137
|
+
const previousSeedIdx = firstVisitedBySeed.get(next);
|
|
1138
|
+
if (previousSeedIdx !== void 0 && previousSeedIdx !== seedIdx) {
|
|
1139
|
+
const fromSeed = seeds[previousSeedIdx];
|
|
1140
|
+
const toSeed = seeds[seedIdx];
|
|
1141
|
+
if (fromSeed !== void 0 && toSeed !== void 0) {
|
|
1142
|
+
const path = {
|
|
1143
|
+
fromSeed,
|
|
1144
|
+
toSeed,
|
|
1145
|
+
nodes: [
|
|
1146
|
+
fromSeed.id,
|
|
1147
|
+
next,
|
|
1148
|
+
toSeed.id
|
|
1149
|
+
].filter((n, i, arr) => arr.indexOf(n) === i)
|
|
1150
|
+
};
|
|
1151
|
+
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);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!firstVisitedBySeed.has(next)) firstVisitedBySeed.set(next, seedIdx);
|
|
1155
|
+
allVisited.add(next);
|
|
1156
|
+
visitedPerFrontier[seedIdx]?.add(next);
|
|
1157
|
+
current = next;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const endTime = performance.now();
|
|
1162
|
+
const edgeTuples = /* @__PURE__ */ new Set();
|
|
1163
|
+
for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
|
|
1164
|
+
return {
|
|
1165
|
+
paths: discoveredPaths,
|
|
1166
|
+
sampledNodes: allVisited,
|
|
1167
|
+
sampledEdges: edgeTuples,
|
|
1168
|
+
visitedPerFrontier,
|
|
1169
|
+
stats: {
|
|
1170
|
+
iterations,
|
|
1171
|
+
nodesVisited: allVisited.size,
|
|
1172
|
+
edgesTraversed,
|
|
1173
|
+
pathsFound: discoveredPaths.length,
|
|
1174
|
+
durationMs: endTime - startTime,
|
|
1175
|
+
algorithm: "random-walk",
|
|
1176
|
+
termination: "exhausted"
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Create an empty result for early termination (no seeds).
|
|
1182
|
+
*/
|
|
1183
|
+
function emptyResult(startTime) {
|
|
1184
|
+
return {
|
|
1185
|
+
paths: [],
|
|
1186
|
+
sampledNodes: /* @__PURE__ */ new Set(),
|
|
1187
|
+
sampledEdges: /* @__PURE__ */ new Set(),
|
|
1188
|
+
visitedPerFrontier: [],
|
|
1189
|
+
stats: {
|
|
1190
|
+
iterations: 0,
|
|
1191
|
+
nodesVisited: 0,
|
|
1192
|
+
edgesTraversed: 0,
|
|
1193
|
+
pathsFound: 0,
|
|
1194
|
+
durationMs: performance.now() - startTime,
|
|
1195
|
+
algorithm: "random-walk",
|
|
1196
|
+
termination: "exhausted"
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
//#endregion
|
|
861
1201
|
//#region src/ranking/parse.ts
|
|
862
1202
|
/**
|
|
863
1203
|
* Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
|
|
@@ -951,6 +1291,115 @@ function adamicAdar(graph, source, target, config) {
|
|
|
951
1291
|
return Math.max(epsilon, score);
|
|
952
1292
|
}
|
|
953
1293
|
//#endregion
|
|
1294
|
+
//#region src/ranking/mi/cosine.ts
|
|
1295
|
+
/**
|
|
1296
|
+
* Compute cosine similarity between neighbourhoods of two nodes.
|
|
1297
|
+
*
|
|
1298
|
+
* @param graph - Source graph
|
|
1299
|
+
* @param source - Source node ID
|
|
1300
|
+
* @param target - Target node ID
|
|
1301
|
+
* @param config - Optional configuration
|
|
1302
|
+
* @returns Cosine similarity in [0, 1]
|
|
1303
|
+
*/
|
|
1304
|
+
function cosine(graph, source, target, config) {
|
|
1305
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1306
|
+
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1307
|
+
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1308
|
+
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
1309
|
+
const denominator = Math.sqrt(sourceNeighbours.size) * Math.sqrt(targetNeighbours.size);
|
|
1310
|
+
if (denominator === 0) return 0;
|
|
1311
|
+
const score = intersection / denominator;
|
|
1312
|
+
return Math.max(epsilon, score);
|
|
1313
|
+
}
|
|
1314
|
+
//#endregion
|
|
1315
|
+
//#region src/ranking/mi/sorensen.ts
|
|
1316
|
+
/**
|
|
1317
|
+
* Compute Sorensen-Dice similarity between neighbourhoods of two nodes.
|
|
1318
|
+
*
|
|
1319
|
+
* @param graph - Source graph
|
|
1320
|
+
* @param source - Source node ID
|
|
1321
|
+
* @param target - Target node ID
|
|
1322
|
+
* @param config - Optional configuration
|
|
1323
|
+
* @returns Sorensen-Dice coefficient in [0, 1]
|
|
1324
|
+
*/
|
|
1325
|
+
function sorensen(graph, source, target, config) {
|
|
1326
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1327
|
+
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1328
|
+
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1329
|
+
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
1330
|
+
const denominator = sourceNeighbours.size + targetNeighbours.size;
|
|
1331
|
+
if (denominator === 0) return 0;
|
|
1332
|
+
const score = 2 * intersection / denominator;
|
|
1333
|
+
return Math.max(epsilon, score);
|
|
1334
|
+
}
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/ranking/mi/resource-allocation.ts
|
|
1337
|
+
/**
|
|
1338
|
+
* Compute Resource Allocation index between neighbourhoods of two nodes.
|
|
1339
|
+
*
|
|
1340
|
+
* @param graph - Source graph
|
|
1341
|
+
* @param source - Source node ID
|
|
1342
|
+
* @param target - Target node ID
|
|
1343
|
+
* @param config - Optional configuration
|
|
1344
|
+
* @returns Resource Allocation index (normalised to [0, 1] if configured)
|
|
1345
|
+
*/
|
|
1346
|
+
function resourceAllocation(graph, source, target, config) {
|
|
1347
|
+
const { epsilon = 1e-10, normalise = true } = config ?? {};
|
|
1348
|
+
const commonNeighbours = require_utils.neighbourIntersection(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
|
|
1349
|
+
let score = 0;
|
|
1350
|
+
for (const neighbour of commonNeighbours) {
|
|
1351
|
+
const degree = graph.degree(neighbour);
|
|
1352
|
+
if (degree > 0) score += 1 / degree;
|
|
1353
|
+
}
|
|
1354
|
+
if (normalise && commonNeighbours.size > 0) {
|
|
1355
|
+
const maxScore = commonNeighbours.size;
|
|
1356
|
+
score = score / maxScore;
|
|
1357
|
+
}
|
|
1358
|
+
return Math.max(epsilon, score);
|
|
1359
|
+
}
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/ranking/mi/overlap-coefficient.ts
|
|
1362
|
+
/**
|
|
1363
|
+
* Compute Overlap Coefficient between neighbourhoods of two nodes.
|
|
1364
|
+
*
|
|
1365
|
+
* @param graph - Source graph
|
|
1366
|
+
* @param source - Source node ID
|
|
1367
|
+
* @param target - Target node ID
|
|
1368
|
+
* @param config - Optional configuration
|
|
1369
|
+
* @returns Overlap Coefficient in [0, 1]
|
|
1370
|
+
*/
|
|
1371
|
+
function overlapCoefficient(graph, source, target, config) {
|
|
1372
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1373
|
+
const sourceNeighbours = require_utils.neighbourSet(graph, source, target);
|
|
1374
|
+
const targetNeighbours = require_utils.neighbourSet(graph, target, source);
|
|
1375
|
+
const { intersection } = require_utils.neighbourOverlap(sourceNeighbours, targetNeighbours);
|
|
1376
|
+
const denominator = Math.min(sourceNeighbours.size, targetNeighbours.size);
|
|
1377
|
+
if (denominator === 0) return 0;
|
|
1378
|
+
const score = intersection / denominator;
|
|
1379
|
+
return Math.max(epsilon, score);
|
|
1380
|
+
}
|
|
1381
|
+
//#endregion
|
|
1382
|
+
//#region src/ranking/mi/hub-promoted.ts
|
|
1383
|
+
/**
|
|
1384
|
+
* Compute Hub Promoted index between neighbourhoods of two nodes.
|
|
1385
|
+
*
|
|
1386
|
+
* @param graph - Source graph
|
|
1387
|
+
* @param source - Source node ID
|
|
1388
|
+
* @param target - Target node ID
|
|
1389
|
+
* @param config - Optional configuration
|
|
1390
|
+
* @returns Hub Promoted index in [0, 1]
|
|
1391
|
+
*/
|
|
1392
|
+
function hubPromoted(graph, source, target, config) {
|
|
1393
|
+
const { epsilon = 1e-10 } = config ?? {};
|
|
1394
|
+
const { intersection } = require_utils.neighbourOverlap(require_utils.neighbourSet(graph, source, target), require_utils.neighbourSet(graph, target, source));
|
|
1395
|
+
const sourceDegree = graph.degree(source);
|
|
1396
|
+
const targetDegree = graph.degree(target);
|
|
1397
|
+
const denominator = Math.min(sourceDegree, targetDegree);
|
|
1398
|
+
if (denominator === 0) return 0;
|
|
1399
|
+
const score = intersection / denominator;
|
|
1400
|
+
return Math.max(epsilon, score);
|
|
1401
|
+
}
|
|
1402
|
+
//#endregion
|
|
954
1403
|
//#region src/ranking/mi/scale.ts
|
|
955
1404
|
/**
|
|
956
1405
|
* Compute SCALE MI between two nodes.
|
|
@@ -1805,6 +2254,258 @@ function randomRanking(_graph, paths, config) {
|
|
|
1805
2254
|
};
|
|
1806
2255
|
}
|
|
1807
2256
|
//#endregion
|
|
2257
|
+
//#region src/ranking/baselines/hitting-time.ts
|
|
2258
|
+
/**
|
|
2259
|
+
* Seeded deterministic random number generator (LCG).
|
|
2260
|
+
* Suitable for reproducible random walk simulation.
|
|
2261
|
+
*/
|
|
2262
|
+
var SeededRNG = class {
|
|
2263
|
+
state;
|
|
2264
|
+
constructor(seed) {
|
|
2265
|
+
this.state = seed;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Generate next pseudorandom value in [0, 1).
|
|
2269
|
+
*/
|
|
2270
|
+
next() {
|
|
2271
|
+
this.state = this.state * 1103515245 + 12345 & 2147483647;
|
|
2272
|
+
return this.state / 2147483647;
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
/**
|
|
2276
|
+
* Compute hitting time via Monte Carlo random walk simulation.
|
|
2277
|
+
*
|
|
2278
|
+
* @param graph - Source graph
|
|
2279
|
+
* @param source - Source node ID
|
|
2280
|
+
* @param target - Target node ID
|
|
2281
|
+
* @param walks - Number of walks to simulate
|
|
2282
|
+
* @param maxSteps - Maximum steps per walk
|
|
2283
|
+
* @param rng - Seeded RNG instance
|
|
2284
|
+
* @returns Average hitting time across walks
|
|
2285
|
+
*/
|
|
2286
|
+
function computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng) {
|
|
2287
|
+
if (source === target) return 0;
|
|
2288
|
+
let totalSteps = 0;
|
|
2289
|
+
let successfulWalks = 0;
|
|
2290
|
+
for (let w = 0; w < walks; w++) {
|
|
2291
|
+
let current = source;
|
|
2292
|
+
let steps = 0;
|
|
2293
|
+
while (current !== target && steps < maxSteps) {
|
|
2294
|
+
const neighbours = Array.from(graph.neighbours(current));
|
|
2295
|
+
if (neighbours.length === 0) break;
|
|
2296
|
+
const nextNode = neighbours[Math.floor(rng.next() * neighbours.length)];
|
|
2297
|
+
if (nextNode === void 0) break;
|
|
2298
|
+
current = nextNode;
|
|
2299
|
+
steps++;
|
|
2300
|
+
}
|
|
2301
|
+
if (current === target) {
|
|
2302
|
+
totalSteps += steps;
|
|
2303
|
+
successfulWalks++;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (successfulWalks > 0) return totalSteps / successfulWalks;
|
|
2307
|
+
return maxSteps;
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Compute hitting time via exact fundamental matrix method.
|
|
2311
|
+
*
|
|
2312
|
+
* For small graphs, computes exact expected hitting times using
|
|
2313
|
+
* the fundamental matrix of the random walk.
|
|
2314
|
+
*
|
|
2315
|
+
* @param graph - Source graph
|
|
2316
|
+
* @param source - Source node ID
|
|
2317
|
+
* @param target - Target node ID
|
|
2318
|
+
* @returns Exact hitting time (or approximation if convergence fails)
|
|
2319
|
+
*/
|
|
2320
|
+
function computeHittingTimeExact(graph, source, target) {
|
|
2321
|
+
if (source === target) return 0;
|
|
2322
|
+
const nodes = Array.from(graph.nodeIds());
|
|
2323
|
+
const nodeToIdx = /* @__PURE__ */ new Map();
|
|
2324
|
+
nodes.forEach((nodeId, idx) => {
|
|
2325
|
+
nodeToIdx.set(nodeId, idx);
|
|
2326
|
+
});
|
|
2327
|
+
const n = nodes.length;
|
|
2328
|
+
const sourceIdx = nodeToIdx.get(source);
|
|
2329
|
+
const targetIdx = nodeToIdx.get(target);
|
|
2330
|
+
if (sourceIdx === void 0 || targetIdx === void 0) return 0;
|
|
2331
|
+
const P = [];
|
|
2332
|
+
for (let i = 0; i < n; i++) {
|
|
2333
|
+
const row = [];
|
|
2334
|
+
for (let j = 0; j < n; j++) row[j] = 0;
|
|
2335
|
+
P[i] = row;
|
|
2336
|
+
}
|
|
2337
|
+
for (const nodeId of nodes) {
|
|
2338
|
+
const idx = nodeToIdx.get(nodeId);
|
|
2339
|
+
if (idx === void 0) continue;
|
|
2340
|
+
const pRow = P[idx];
|
|
2341
|
+
if (pRow === void 0) continue;
|
|
2342
|
+
if (idx === targetIdx) pRow[idx] = 1;
|
|
2343
|
+
else {
|
|
2344
|
+
const neighbours = Array.from(graph.neighbours(nodeId));
|
|
2345
|
+
const degree = neighbours.length;
|
|
2346
|
+
if (degree > 0) for (const neighbourId of neighbours) {
|
|
2347
|
+
const nIdx = nodeToIdx.get(neighbourId);
|
|
2348
|
+
if (nIdx !== void 0) pRow[nIdx] = 1 / degree;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const transientIndices = [];
|
|
2353
|
+
for (let i = 0; i < n; i++) if (i !== targetIdx) transientIndices.push(i);
|
|
2354
|
+
const m = transientIndices.length;
|
|
2355
|
+
const Q = [];
|
|
2356
|
+
for (let i = 0; i < m; i++) {
|
|
2357
|
+
const row = [];
|
|
2358
|
+
for (let j = 0; j < m; j++) row[j] = 0;
|
|
2359
|
+
Q[i] = row;
|
|
2360
|
+
}
|
|
2361
|
+
for (let i = 0; i < m; i++) {
|
|
2362
|
+
const qRow = Q[i];
|
|
2363
|
+
if (qRow === void 0) continue;
|
|
2364
|
+
const origI = transientIndices[i];
|
|
2365
|
+
if (origI === void 0) continue;
|
|
2366
|
+
const pRow = P[origI];
|
|
2367
|
+
if (pRow === void 0) continue;
|
|
2368
|
+
for (let j = 0; j < m; j++) {
|
|
2369
|
+
const origJ = transientIndices[j];
|
|
2370
|
+
if (origJ === void 0) continue;
|
|
2371
|
+
qRow[j] = pRow[origJ] ?? 0;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const IMQ = [];
|
|
2375
|
+
for (let i = 0; i < m; i++) {
|
|
2376
|
+
const row = [];
|
|
2377
|
+
for (let j = 0; j < m; j++) row[j] = i === j ? 1 : 0;
|
|
2378
|
+
IMQ[i] = row;
|
|
2379
|
+
}
|
|
2380
|
+
for (let i = 0; i < m; i++) {
|
|
2381
|
+
const imqRow = IMQ[i];
|
|
2382
|
+
if (imqRow === void 0) continue;
|
|
2383
|
+
const qRow = Q[i];
|
|
2384
|
+
for (let j = 0; j < m; j++) {
|
|
2385
|
+
const qVal = qRow?.[j] ?? 0;
|
|
2386
|
+
imqRow[j] = (i === j ? 1 : 0) - qVal;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const N = invertMatrix(IMQ);
|
|
2390
|
+
if (N === null) return 1;
|
|
2391
|
+
const sourceTransientIdx = transientIndices.indexOf(sourceIdx);
|
|
2392
|
+
if (sourceTransientIdx < 0) return 0;
|
|
2393
|
+
let hittingTime = 0;
|
|
2394
|
+
const row = N[sourceTransientIdx];
|
|
2395
|
+
if (row !== void 0) for (const val of row) hittingTime += val;
|
|
2396
|
+
return hittingTime;
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Invert a square matrix using Gaussian elimination with partial pivoting.
|
|
2400
|
+
*
|
|
2401
|
+
* @param matrix - Input matrix (n × n)
|
|
2402
|
+
* @returns Inverted matrix, or null if singular
|
|
2403
|
+
*/
|
|
2404
|
+
function invertMatrix(matrix) {
|
|
2405
|
+
const n = matrix.length;
|
|
2406
|
+
const aug = [];
|
|
2407
|
+
for (let i = 0; i < n; i++) {
|
|
2408
|
+
const row = [];
|
|
2409
|
+
const matRow = matrix[i];
|
|
2410
|
+
for (let j = 0; j < n; j++) row[j] = matRow?.[j] ?? 0;
|
|
2411
|
+
for (let j = 0; j < n; j++) row[n + j] = i === j ? 1 : 0;
|
|
2412
|
+
aug[i] = row;
|
|
2413
|
+
}
|
|
2414
|
+
for (let col = 0; col < n; col++) {
|
|
2415
|
+
let pivotRow = col;
|
|
2416
|
+
const pivotCol = aug[pivotRow];
|
|
2417
|
+
if (pivotCol === void 0) return null;
|
|
2418
|
+
for (let row = col + 1; row < n; row++) {
|
|
2419
|
+
const currRowVal = aug[row]?.[col] ?? 0;
|
|
2420
|
+
const pivotRowVal = pivotCol[col] ?? 0;
|
|
2421
|
+
if (Math.abs(currRowVal) > Math.abs(pivotRowVal)) pivotRow = row;
|
|
2422
|
+
}
|
|
2423
|
+
const augPivot = aug[pivotRow];
|
|
2424
|
+
if (augPivot === void 0 || Math.abs(augPivot[col] ?? 0) < 1e-10) return null;
|
|
2425
|
+
[aug[col], aug[pivotRow]] = [aug[pivotRow] ?? [], aug[col] ?? []];
|
|
2426
|
+
const scaledPivotRow = aug[col];
|
|
2427
|
+
if (scaledPivotRow === void 0) return null;
|
|
2428
|
+
const pivot = scaledPivotRow[col] ?? 1;
|
|
2429
|
+
for (let j = 0; j < 2 * n; j++) scaledPivotRow[j] = (scaledPivotRow[j] ?? 0) / pivot;
|
|
2430
|
+
for (let row = col + 1; row < n; row++) {
|
|
2431
|
+
const currRow = aug[row];
|
|
2432
|
+
if (currRow === void 0) continue;
|
|
2433
|
+
const factor = currRow[col] ?? 0;
|
|
2434
|
+
for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (scaledPivotRow[j] ?? 0);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
for (let col = n - 1; col > 0; col--) {
|
|
2438
|
+
const colRow = aug[col];
|
|
2439
|
+
if (colRow === void 0) return null;
|
|
2440
|
+
for (let row = col - 1; row >= 0; row--) {
|
|
2441
|
+
const currRow = aug[row];
|
|
2442
|
+
if (currRow === void 0) continue;
|
|
2443
|
+
const factor = currRow[col] ?? 0;
|
|
2444
|
+
for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (colRow[j] ?? 0);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
const inv = [];
|
|
2448
|
+
for (let i = 0; i < n; i++) {
|
|
2449
|
+
const row = [];
|
|
2450
|
+
for (let j = 0; j < n; j++) row[j] = 0;
|
|
2451
|
+
inv[i] = row;
|
|
2452
|
+
}
|
|
2453
|
+
for (let i = 0; i < n; i++) {
|
|
2454
|
+
const invRow = inv[i];
|
|
2455
|
+
if (invRow === void 0) continue;
|
|
2456
|
+
const augRow = aug[i];
|
|
2457
|
+
if (augRow === void 0) continue;
|
|
2458
|
+
for (let j = 0; j < n; j++) invRow[j] = augRow[n + j] ?? 0;
|
|
2459
|
+
}
|
|
2460
|
+
return inv;
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Rank paths by inverse hitting time between endpoints.
|
|
2464
|
+
*
|
|
2465
|
+
* @param graph - Source graph
|
|
2466
|
+
* @param paths - Paths to rank
|
|
2467
|
+
* @param config - Configuration options
|
|
2468
|
+
* @returns Ranked paths (highest inverse hitting time first)
|
|
2469
|
+
*/
|
|
2470
|
+
function hittingTime(graph, paths, config) {
|
|
2471
|
+
const { includeScores = true, mode = "auto", walks = 1e3, maxSteps = 1e4, seed = 42 } = config ?? {};
|
|
2472
|
+
if (paths.length === 0) return {
|
|
2473
|
+
paths: [],
|
|
2474
|
+
method: "hitting-time"
|
|
2475
|
+
};
|
|
2476
|
+
const nodeCount = Array.from(graph.nodeIds()).length;
|
|
2477
|
+
const actualMode = mode === "auto" ? nodeCount < 100 ? "exact" : "approximate" : mode;
|
|
2478
|
+
const rng = new SeededRNG(seed);
|
|
2479
|
+
const scored = paths.map((path) => {
|
|
2480
|
+
const source = path.nodes[0];
|
|
2481
|
+
const target = path.nodes[path.nodes.length - 1];
|
|
2482
|
+
if (source === void 0 || target === void 0) return {
|
|
2483
|
+
path,
|
|
2484
|
+
score: 0
|
|
2485
|
+
};
|
|
2486
|
+
const ht = actualMode === "exact" ? computeHittingTimeExact(graph, source, target) : computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng);
|
|
2487
|
+
return {
|
|
2488
|
+
path,
|
|
2489
|
+
score: ht > 0 ? 1 / ht : 0
|
|
2490
|
+
};
|
|
2491
|
+
});
|
|
2492
|
+
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
2493
|
+
if (maxScore === 0 || !Number.isFinite(maxScore)) return {
|
|
2494
|
+
paths: paths.map((path) => ({
|
|
2495
|
+
...path,
|
|
2496
|
+
score: 0
|
|
2497
|
+
})),
|
|
2498
|
+
method: "hitting-time"
|
|
2499
|
+
};
|
|
2500
|
+
return {
|
|
2501
|
+
paths: scored.map(({ path, score }) => ({
|
|
2502
|
+
...path,
|
|
2503
|
+
score: includeScores ? score / maxScore : score / maxScore
|
|
2504
|
+
})).sort((a, b) => b.score - a.score),
|
|
2505
|
+
method: "hitting-time"
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
//#endregion
|
|
1808
2509
|
//#region src/extraction/ego-network.ts
|
|
1809
2510
|
/**
|
|
1810
2511
|
* Extract the ego-network (k-hop neighbourhood) of a centre node.
|
|
@@ -2438,6 +3139,7 @@ exports.bfs = require_traversal.bfs;
|
|
|
2438
3139
|
exports.bfsWithPath = require_traversal.bfsWithPath;
|
|
2439
3140
|
exports.communicability = communicability;
|
|
2440
3141
|
exports.computeTrussNumbers = computeTrussNumbers;
|
|
3142
|
+
exports.cosine = cosine;
|
|
2441
3143
|
exports.countEdgesOfType = require_utils.countEdgesOfType;
|
|
2442
3144
|
exports.countNodesOfType = require_utils.countNodesOfType;
|
|
2443
3145
|
exports.createGPUContext = require_gpu.createGPUContext;
|
|
@@ -2446,6 +3148,8 @@ exports.csrToGPUBuffers = require_gpu.csrToGPUBuffers;
|
|
|
2446
3148
|
exports.degreeSum = degreeSum;
|
|
2447
3149
|
exports.detectWebGPU = require_gpu.detectWebGPU;
|
|
2448
3150
|
exports.dfs = require_traversal.dfs;
|
|
3151
|
+
exports.dfsPriority = dfsPriority;
|
|
3152
|
+
exports.dfsPriorityFn = dfsPriorityFn;
|
|
2449
3153
|
exports.dfsWithPath = require_traversal.dfsWithPath;
|
|
2450
3154
|
exports.dome = dome;
|
|
2451
3155
|
exports.domeHighDegree = domeHighDegree;
|
|
@@ -2467,9 +3171,12 @@ exports.getMotifName = getMotifName;
|
|
|
2467
3171
|
exports.graphToCSR = require_gpu.graphToCSR;
|
|
2468
3172
|
exports.grasp = require_seeds.grasp;
|
|
2469
3173
|
exports.hae = hae;
|
|
3174
|
+
exports.hittingTime = hittingTime;
|
|
3175
|
+
exports.hubPromoted = hubPromoted;
|
|
2470
3176
|
exports.isWebGPUAvailable = require_gpu.isWebGPUAvailable;
|
|
2471
3177
|
exports.jaccard = jaccard;
|
|
2472
3178
|
exports.jaccardArithmetic = jaccardArithmetic;
|
|
3179
|
+
exports.kHop = kHop;
|
|
2473
3180
|
exports.katz = katz;
|
|
2474
3181
|
exports.lace = lace;
|
|
2475
3182
|
exports.localClusteringCoefficient = require_utils.localClusteringCoefficient;
|
|
@@ -2483,20 +3190,24 @@ exports.normaliseFeatures = require_kmeans.normaliseFeatures;
|
|
|
2483
3190
|
exports.zScoreNormalise = require_kmeans.normaliseFeatures;
|
|
2484
3191
|
exports.normalisedEntropy = require_utils.normalisedEntropy;
|
|
2485
3192
|
exports.notch = notch;
|
|
3193
|
+
exports.overlapCoefficient = overlapCoefficient;
|
|
2486
3194
|
exports.pagerank = pagerank;
|
|
2487
3195
|
exports.parse = parse;
|
|
2488
3196
|
exports.pipe = pipe;
|
|
2489
3197
|
exports.randomPriority = randomPriority;
|
|
2490
3198
|
exports.randomRanking = randomRanking;
|
|
3199
|
+
exports.randomWalk = randomWalk;
|
|
2491
3200
|
exports.reach = reach;
|
|
2492
3201
|
exports.readBufferToCPU = require_gpu.readBufferToCPU;
|
|
2493
3202
|
exports.resistanceDistance = resistanceDistance;
|
|
3203
|
+
exports.resourceAllocation = resourceAllocation;
|
|
2494
3204
|
exports.sage = sage;
|
|
2495
3205
|
exports.scale = scale;
|
|
2496
3206
|
exports.shannonEntropy = require_utils.shannonEntropy;
|
|
2497
3207
|
exports.shortest = shortest;
|
|
2498
3208
|
exports.sift = sift;
|
|
2499
3209
|
exports.skew = skew;
|
|
3210
|
+
exports.sorensen = sorensen;
|
|
2500
3211
|
exports.span = span;
|
|
2501
3212
|
exports.standardBfs = standardBfs;
|
|
2502
3213
|
exports.stratified = require_seeds.stratified;
|