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