graphwise 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -30
- package/dist/adjacency-map-B6wPtmaq.cjs +234 -0
- package/dist/adjacency-map-B6wPtmaq.cjs.map +1 -0
- package/dist/adjacency-map-D-Ul7V1r.js +229 -0
- package/dist/adjacency-map-D-Ul7V1r.js.map +1 -0
- package/dist/async/index.cjs +16 -0
- package/dist/async/index.js +3 -0
- package/dist/expansion/dfs-priority.d.ts +11 -0
- package/dist/expansion/dfs-priority.d.ts.map +1 -1
- package/dist/expansion/dome.d.ts +20 -0
- package/dist/expansion/dome.d.ts.map +1 -1
- package/dist/expansion/edge.d.ts +18 -0
- package/dist/expansion/edge.d.ts.map +1 -1
- package/dist/expansion/flux.d.ts +16 -0
- package/dist/expansion/flux.d.ts.map +1 -1
- package/dist/expansion/frontier-balanced.d.ts +11 -0
- package/dist/expansion/frontier-balanced.d.ts.map +1 -1
- package/dist/expansion/fuse.d.ts +16 -0
- package/dist/expansion/fuse.d.ts.map +1 -1
- package/dist/expansion/hae.d.ts +16 -0
- package/dist/expansion/hae.d.ts.map +1 -1
- package/dist/expansion/index.cjs +43 -0
- package/dist/expansion/index.js +2 -0
- package/dist/expansion/lace.d.ts +16 -0
- package/dist/expansion/lace.d.ts.map +1 -1
- package/dist/expansion/maze.d.ts +17 -0
- package/dist/expansion/maze.d.ts.map +1 -1
- package/dist/expansion/pipe.d.ts +16 -0
- package/dist/expansion/pipe.d.ts.map +1 -1
- package/dist/expansion/random-priority.d.ts +18 -0
- package/dist/expansion/random-priority.d.ts.map +1 -1
- package/dist/expansion/reach.d.ts +17 -0
- package/dist/expansion/reach.d.ts.map +1 -1
- package/dist/expansion/sage.d.ts +15 -0
- package/dist/expansion/sage.d.ts.map +1 -1
- package/dist/expansion/sift.d.ts +16 -0
- package/dist/expansion/sift.d.ts.map +1 -1
- package/dist/expansion/standard-bfs.d.ts +11 -0
- package/dist/expansion/standard-bfs.d.ts.map +1 -1
- package/dist/expansion/tide.d.ts +16 -0
- package/dist/expansion/tide.d.ts.map +1 -1
- package/dist/expansion/warp.d.ts +16 -0
- package/dist/expansion/warp.d.ts.map +1 -1
- package/dist/expansion-FkmEYlrQ.cjs +1949 -0
- package/dist/expansion-FkmEYlrQ.cjs.map +1 -0
- package/dist/expansion-sldRognt.js +1704 -0
- package/dist/expansion-sldRognt.js.map +1 -0
- package/dist/extraction/index.cjs +630 -0
- package/dist/extraction/index.cjs.map +1 -0
- package/dist/extraction/index.js +621 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/graph/index.cjs +2 -229
- package/dist/graph/index.js +1 -228
- package/dist/index/index.cjs +131 -3406
- package/dist/index/index.js +14 -3334
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/jaccard-Bmd1IEFO.cjs +50 -0
- package/dist/jaccard-Bmd1IEFO.cjs.map +1 -0
- package/dist/jaccard-Yddrtt5D.js +39 -0
- package/dist/jaccard-Yddrtt5D.js.map +1 -0
- package/dist/{kmeans-BIgSyGKu.cjs → kmeans-D3yX5QFs.cjs} +1 -1
- package/dist/{kmeans-BIgSyGKu.cjs.map → kmeans-D3yX5QFs.cjs.map} +1 -1
- package/dist/{kmeans-87ExSUNZ.js → kmeans-DVCe61Me.js} +1 -1
- package/dist/{kmeans-87ExSUNZ.js.map → kmeans-DVCe61Me.js.map} +1 -1
- package/dist/ops-4nmI-pwk.cjs +277 -0
- package/dist/ops-4nmI-pwk.cjs.map +1 -0
- package/dist/ops-Zsu4ecEG.js +212 -0
- package/dist/ops-Zsu4ecEG.js.map +1 -0
- package/dist/priority-queue-ChVLoG6T.cjs +148 -0
- package/dist/priority-queue-ChVLoG6T.cjs.map +1 -0
- package/dist/priority-queue-DqCuFTR8.js +143 -0
- package/dist/priority-queue-DqCuFTR8.js.map +1 -0
- package/dist/ranking/index.cjs +43 -0
- package/dist/ranking/index.js +4 -0
- package/dist/ranking/mi/adamic-adar.d.ts +8 -0
- package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
- package/dist/ranking/mi/adaptive.d.ts +8 -0
- package/dist/ranking/mi/adaptive.d.ts.map +1 -1
- package/dist/ranking/mi/cosine.d.ts +7 -0
- package/dist/ranking/mi/cosine.d.ts.map +1 -1
- package/dist/ranking/mi/etch.d.ts +8 -0
- package/dist/ranking/mi/etch.d.ts.map +1 -1
- package/dist/ranking/mi/hub-promoted.d.ts +7 -0
- package/dist/ranking/mi/hub-promoted.d.ts.map +1 -1
- package/dist/ranking/mi/index.cjs +581 -0
- package/dist/ranking/mi/index.cjs.map +1 -0
- package/dist/ranking/mi/index.js +555 -0
- package/dist/ranking/mi/index.js.map +1 -0
- package/dist/ranking/mi/jaccard.d.ts +7 -0
- package/dist/ranking/mi/jaccard.d.ts.map +1 -1
- package/dist/ranking/mi/notch.d.ts +8 -0
- package/dist/ranking/mi/notch.d.ts.map +1 -1
- package/dist/ranking/mi/overlap-coefficient.d.ts +7 -0
- package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -1
- package/dist/ranking/mi/resource-allocation.d.ts +8 -0
- package/dist/ranking/mi/resource-allocation.d.ts.map +1 -1
- package/dist/ranking/mi/scale.d.ts +7 -0
- package/dist/ranking/mi/scale.d.ts.map +1 -1
- package/dist/ranking/mi/skew.d.ts +7 -0
- package/dist/ranking/mi/skew.d.ts.map +1 -1
- package/dist/ranking/mi/sorensen.d.ts +7 -0
- package/dist/ranking/mi/sorensen.d.ts.map +1 -1
- package/dist/ranking/mi/span.d.ts +8 -0
- package/dist/ranking/mi/span.d.ts.map +1 -1
- package/dist/ranking/mi/types.d.ts +12 -0
- package/dist/ranking/mi/types.d.ts.map +1 -1
- package/dist/ranking/parse.d.ts +24 -1
- package/dist/ranking/parse.d.ts.map +1 -1
- package/dist/ranking-mUm9rV-C.js +1016 -0
- package/dist/ranking-mUm9rV-C.js.map +1 -0
- package/dist/ranking-riRrEVAR.cjs +1093 -0
- package/dist/ranking-riRrEVAR.cjs.map +1 -0
- package/dist/seeds/index.cjs +1 -1
- package/dist/seeds/index.js +1 -1
- package/dist/structures/index.cjs +2 -143
- package/dist/structures/index.js +1 -142
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils-CcIrKAEb.js +22 -0
- package/dist/utils-CcIrKAEb.js.map +1 -0
- package/dist/utils-CpyzmzIF.cjs +33 -0
- package/dist/utils-CpyzmzIF.cjs.map +1 -0
- package/package.json +6 -1
- package/dist/graph/index.cjs.map +0 -1
- package/dist/graph/index.js.map +0 -1
- package/dist/index/index.cjs.map +0 -1
- package/dist/index/index.js.map +0 -1
- package/dist/structures/index.cjs.map +0 -1
- package/dist/structures/index.js.map +0 -1
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
const require_jaccard = require("./jaccard-Bmd1IEFO.cjs");
|
|
2
|
+
//#region src/ranking/parse.ts
|
|
3
|
+
/**
|
|
4
|
+
* Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
|
|
5
|
+
*
|
|
6
|
+
* Computes geometric mean of edge MI scores for each path,
|
|
7
|
+
* then sorts by salience (highest first).
|
|
8
|
+
*
|
|
9
|
+
* @param graph - Source graph
|
|
10
|
+
* @param paths - Paths to rank
|
|
11
|
+
* @param config - Configuration options
|
|
12
|
+
* @returns Ranked paths with statistics
|
|
13
|
+
*/
|
|
14
|
+
function parse(graph, paths, config) {
|
|
15
|
+
const startTime = performance.now();
|
|
16
|
+
const { mi = require_jaccard.jaccard, epsilon = 1e-10 } = config ?? {};
|
|
17
|
+
const rankedPaths = [];
|
|
18
|
+
for (const path of paths) {
|
|
19
|
+
const salience = computePathSalience(graph, path, mi, epsilon);
|
|
20
|
+
rankedPaths.push({
|
|
21
|
+
...path,
|
|
22
|
+
salience
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
rankedPaths.sort((a, b) => b.salience - a.salience);
|
|
26
|
+
const endTime = performance.now();
|
|
27
|
+
const saliences = rankedPaths.map((p) => p.salience);
|
|
28
|
+
const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
|
|
29
|
+
const sortedSaliences = [...saliences].sort((a, b) => a - b);
|
|
30
|
+
const mid = Math.floor(sortedSaliences.length / 2);
|
|
31
|
+
const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
|
|
32
|
+
const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
|
|
33
|
+
const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
|
|
34
|
+
return {
|
|
35
|
+
paths: rankedPaths,
|
|
36
|
+
stats: {
|
|
37
|
+
pathsRanked: rankedPaths.length,
|
|
38
|
+
meanSalience,
|
|
39
|
+
medianSalience,
|
|
40
|
+
maxSalience,
|
|
41
|
+
minSalience,
|
|
42
|
+
durationMs: endTime - startTime
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Rank paths using async PARSE (Path-Aware Ranking via Salience Estimation).
|
|
48
|
+
*
|
|
49
|
+
* Async variant suitable for use with remote or lazy graph data sources.
|
|
50
|
+
* Computes geometric mean of edge MI scores for each path using Promise.all
|
|
51
|
+
* for parallelism, then sorts by salience (highest first).
|
|
52
|
+
*
|
|
53
|
+
* @param graph - Async source graph
|
|
54
|
+
* @param paths - Paths to rank
|
|
55
|
+
* @param config - Configuration options
|
|
56
|
+
* @returns Ranked paths with statistics
|
|
57
|
+
*/
|
|
58
|
+
async function parseAsync(graph, paths, config) {
|
|
59
|
+
const startTime = performance.now();
|
|
60
|
+
const { mi = require_jaccard.jaccardAsync, epsilon = 1e-10 } = config ?? {};
|
|
61
|
+
const rankedPaths = [];
|
|
62
|
+
for (const path of paths) {
|
|
63
|
+
const salience = await computePathSalienceAsync(graph, path, mi, epsilon);
|
|
64
|
+
rankedPaths.push({
|
|
65
|
+
...path,
|
|
66
|
+
salience
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
rankedPaths.sort((a, b) => b.salience - a.salience);
|
|
70
|
+
const endTime = performance.now();
|
|
71
|
+
const saliences = rankedPaths.map((p) => p.salience);
|
|
72
|
+
const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
|
|
73
|
+
const sortedSaliences = [...saliences].sort((a, b) => a - b);
|
|
74
|
+
const mid = Math.floor(sortedSaliences.length / 2);
|
|
75
|
+
const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
|
|
76
|
+
const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
|
|
77
|
+
const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
|
|
78
|
+
return {
|
|
79
|
+
paths: rankedPaths,
|
|
80
|
+
stats: {
|
|
81
|
+
pathsRanked: rankedPaths.length,
|
|
82
|
+
meanSalience,
|
|
83
|
+
medianSalience,
|
|
84
|
+
maxSalience,
|
|
85
|
+
minSalience,
|
|
86
|
+
durationMs: endTime - startTime
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compute salience for a single path asynchronously.
|
|
92
|
+
*
|
|
93
|
+
* Uses geometric mean of edge MI scores for length-unbiased ranking.
|
|
94
|
+
* Edge MI values are computed in parallel via Promise.all.
|
|
95
|
+
*/
|
|
96
|
+
async function computePathSalienceAsync(graph, path, mi, epsilon) {
|
|
97
|
+
const nodes = path.nodes;
|
|
98
|
+
if (nodes.length < 2) return epsilon;
|
|
99
|
+
const edgeMIs = await Promise.all(nodes.slice(0, -1).map((source, i) => {
|
|
100
|
+
const target = nodes[i + 1];
|
|
101
|
+
if (target !== void 0) return mi(graph, source, target);
|
|
102
|
+
return Promise.resolve(epsilon);
|
|
103
|
+
}));
|
|
104
|
+
let productMi = 1;
|
|
105
|
+
let edgeCount = 0;
|
|
106
|
+
for (const edgeMi of edgeMIs) {
|
|
107
|
+
productMi *= Math.max(epsilon, edgeMi);
|
|
108
|
+
edgeCount++;
|
|
109
|
+
}
|
|
110
|
+
if (edgeCount === 0) return epsilon;
|
|
111
|
+
const salience = Math.pow(productMi, 1 / edgeCount);
|
|
112
|
+
return Math.max(epsilon, Math.min(1, salience));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Compute salience for a single path.
|
|
116
|
+
*
|
|
117
|
+
* Uses geometric mean of edge MI scores for length-unbiased ranking.
|
|
118
|
+
*/
|
|
119
|
+
function computePathSalience(graph, path, mi, epsilon) {
|
|
120
|
+
const nodes = path.nodes;
|
|
121
|
+
if (nodes.length < 2) return epsilon;
|
|
122
|
+
let productMi = 1;
|
|
123
|
+
let edgeCount = 0;
|
|
124
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
125
|
+
const source = nodes[i];
|
|
126
|
+
const target = nodes[i + 1];
|
|
127
|
+
if (source !== void 0 && target !== void 0) {
|
|
128
|
+
const edgeMi = mi(graph, source, target);
|
|
129
|
+
productMi *= Math.max(epsilon, edgeMi);
|
|
130
|
+
edgeCount++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (edgeCount === 0) return epsilon;
|
|
134
|
+
const salience = Math.pow(productMi, 1 / edgeCount);
|
|
135
|
+
return Math.max(epsilon, Math.min(1, salience));
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/ranking/baselines/utils.ts
|
|
139
|
+
/**
|
|
140
|
+
* Normalise a set of scored paths and return them sorted highest-first.
|
|
141
|
+
*
|
|
142
|
+
* All scores are normalised relative to the maximum observed score.
|
|
143
|
+
* When `includeScores` is false, raw (un-normalised) scores are preserved.
|
|
144
|
+
* Handles degenerate cases: empty input and all-zero scores.
|
|
145
|
+
*
|
|
146
|
+
* @param paths - Original paths in input order
|
|
147
|
+
* @param scored - Paths paired with their computed scores
|
|
148
|
+
* @param method - Method name to embed in the result
|
|
149
|
+
* @param includeScores - When true, normalise scores to [0, 1]; when false, keep raw scores
|
|
150
|
+
* @returns BaselineResult with ranked paths
|
|
151
|
+
*/
|
|
152
|
+
function normaliseAndRank(paths, scored, method, includeScores) {
|
|
153
|
+
if (scored.length === 0) return {
|
|
154
|
+
paths: [],
|
|
155
|
+
method
|
|
156
|
+
};
|
|
157
|
+
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
158
|
+
if (maxScore === 0) return {
|
|
159
|
+
paths: paths.map((path) => ({
|
|
160
|
+
...path,
|
|
161
|
+
score: 0
|
|
162
|
+
})),
|
|
163
|
+
method
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
paths: scored.map(({ path, score }) => ({
|
|
167
|
+
...path,
|
|
168
|
+
score: includeScores ? score / maxScore : score
|
|
169
|
+
})).sort((a, b) => b.score - a.score),
|
|
170
|
+
method
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/ranking/baselines/shortest.ts
|
|
175
|
+
/**
|
|
176
|
+
* Rank paths by length (shortest first).
|
|
177
|
+
*
|
|
178
|
+
* Score = 1 / path_length, normalised to [0, 1].
|
|
179
|
+
*
|
|
180
|
+
* @param _graph - Source graph (unused for length ranking)
|
|
181
|
+
* @param paths - Paths to rank
|
|
182
|
+
* @param config - Configuration options
|
|
183
|
+
* @returns Ranked paths (shortest first)
|
|
184
|
+
*/
|
|
185
|
+
function shortest(_graph, paths, config) {
|
|
186
|
+
const { includeScores = true } = config ?? {};
|
|
187
|
+
if (paths.length === 0) return {
|
|
188
|
+
paths: [],
|
|
189
|
+
method: "shortest"
|
|
190
|
+
};
|
|
191
|
+
return normaliseAndRank(paths, paths.map((path) => ({
|
|
192
|
+
path,
|
|
193
|
+
score: 1 / path.nodes.length
|
|
194
|
+
})), "shortest", includeScores);
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/ranking/baselines/degree-sum.ts
|
|
198
|
+
/**
|
|
199
|
+
* Rank paths by sum of node degrees.
|
|
200
|
+
*
|
|
201
|
+
* @param graph - Source graph
|
|
202
|
+
* @param paths - Paths to rank
|
|
203
|
+
* @param config - Configuration options
|
|
204
|
+
* @returns Ranked paths (highest degree-sum first)
|
|
205
|
+
*/
|
|
206
|
+
function degreeSum(graph, paths, config) {
|
|
207
|
+
const { includeScores = true } = config ?? {};
|
|
208
|
+
if (paths.length === 0) return {
|
|
209
|
+
paths: [],
|
|
210
|
+
method: "degree-sum"
|
|
211
|
+
};
|
|
212
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
213
|
+
let degreeSum = 0;
|
|
214
|
+
for (const nodeId of path.nodes) degreeSum += graph.degree(nodeId);
|
|
215
|
+
return {
|
|
216
|
+
path,
|
|
217
|
+
score: degreeSum
|
|
218
|
+
};
|
|
219
|
+
}), "degree-sum", includeScores);
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/ranking/baselines/widest-path.ts
|
|
223
|
+
/**
|
|
224
|
+
* Rank paths by widest bottleneck (minimum edge similarity).
|
|
225
|
+
*
|
|
226
|
+
* @param graph - Source graph
|
|
227
|
+
* @param paths - Paths to rank
|
|
228
|
+
* @param config - Configuration options
|
|
229
|
+
* @returns Ranked paths (highest bottleneck first)
|
|
230
|
+
*/
|
|
231
|
+
function widestPath(graph, paths, config) {
|
|
232
|
+
const { includeScores = true } = config ?? {};
|
|
233
|
+
if (paths.length === 0) return {
|
|
234
|
+
paths: [],
|
|
235
|
+
method: "widest-path"
|
|
236
|
+
};
|
|
237
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
238
|
+
if (path.nodes.length < 2) return {
|
|
239
|
+
path,
|
|
240
|
+
score: 1
|
|
241
|
+
};
|
|
242
|
+
let minSimilarity = Number.POSITIVE_INFINITY;
|
|
243
|
+
for (let i = 0; i < path.nodes.length - 1; i++) {
|
|
244
|
+
const source = path.nodes[i];
|
|
245
|
+
const target = path.nodes[i + 1];
|
|
246
|
+
if (source === void 0 || target === void 0) continue;
|
|
247
|
+
const edgeSimilarity = require_jaccard.jaccard(graph, source, target);
|
|
248
|
+
minSimilarity = Math.min(minSimilarity, edgeSimilarity);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
path,
|
|
252
|
+
score: minSimilarity === Number.POSITIVE_INFINITY ? 1 : minSimilarity
|
|
253
|
+
};
|
|
254
|
+
}), "widest-path", includeScores);
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/ranking/baselines/jaccard-arithmetic.ts
|
|
258
|
+
/**
|
|
259
|
+
* Rank paths by arithmetic mean of edge Jaccard similarities.
|
|
260
|
+
*
|
|
261
|
+
* @param graph - Source graph
|
|
262
|
+
* @param paths - Paths to rank
|
|
263
|
+
* @param config - Configuration options
|
|
264
|
+
* @returns Ranked paths (highest arithmetic mean first)
|
|
265
|
+
*/
|
|
266
|
+
function jaccardArithmetic(graph, paths, config) {
|
|
267
|
+
const { includeScores = true } = config ?? {};
|
|
268
|
+
if (paths.length === 0) return {
|
|
269
|
+
paths: [],
|
|
270
|
+
method: "jaccard-arithmetic"
|
|
271
|
+
};
|
|
272
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
273
|
+
if (path.nodes.length < 2) return {
|
|
274
|
+
path,
|
|
275
|
+
score: 1
|
|
276
|
+
};
|
|
277
|
+
let similaritySum = 0;
|
|
278
|
+
let edgeCount = 0;
|
|
279
|
+
for (let i = 0; i < path.nodes.length - 1; i++) {
|
|
280
|
+
const source = path.nodes[i];
|
|
281
|
+
const target = path.nodes[i + 1];
|
|
282
|
+
if (source === void 0 || target === void 0) continue;
|
|
283
|
+
const edgeSimilarity = require_jaccard.jaccard(graph, source, target);
|
|
284
|
+
similaritySum += edgeSimilarity;
|
|
285
|
+
edgeCount++;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
path,
|
|
289
|
+
score: edgeCount > 0 ? similaritySum / edgeCount : 1
|
|
290
|
+
};
|
|
291
|
+
}), "jaccard-arithmetic", includeScores);
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/ranking/baselines/pagerank.ts
|
|
295
|
+
/**
|
|
296
|
+
* Compute PageRank centrality for all nodes using power iteration.
|
|
297
|
+
*
|
|
298
|
+
* @param graph - Source graph
|
|
299
|
+
* @param damping - Damping factor (default 0.85)
|
|
300
|
+
* @param tolerance - Convergence tolerance (default 1e-6)
|
|
301
|
+
* @param maxIterations - Maximum iterations (default 100)
|
|
302
|
+
* @returns Map of node ID to PageRank value
|
|
303
|
+
*/
|
|
304
|
+
function computePageRank(graph, damping = .85, tolerance = 1e-6, maxIterations = 100) {
|
|
305
|
+
const nodes = Array.from(graph.nodeIds());
|
|
306
|
+
const n = nodes.length;
|
|
307
|
+
if (n === 0) return /* @__PURE__ */ new Map();
|
|
308
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
309
|
+
const newRanks = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const nodeId of nodes) {
|
|
311
|
+
ranks.set(nodeId, 1 / n);
|
|
312
|
+
newRanks.set(nodeId, 0);
|
|
313
|
+
}
|
|
314
|
+
let isCurrentRanks = true;
|
|
315
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
316
|
+
let maxChange = 0;
|
|
317
|
+
const currMap = isCurrentRanks ? ranks : newRanks;
|
|
318
|
+
const nextMap = isCurrentRanks ? newRanks : ranks;
|
|
319
|
+
for (const nodeId of nodes) {
|
|
320
|
+
let incomingSum = 0;
|
|
321
|
+
for (const incomingId of graph.neighbours(nodeId, "in")) {
|
|
322
|
+
const incomingRank = currMap.get(incomingId) ?? 0;
|
|
323
|
+
const outDegree = graph.degree(incomingId);
|
|
324
|
+
if (outDegree > 0) incomingSum += incomingRank / outDegree;
|
|
325
|
+
}
|
|
326
|
+
const newRank = (1 - damping) / n + damping * incomingSum;
|
|
327
|
+
nextMap.set(nodeId, newRank);
|
|
328
|
+
const oldRank = currMap.get(nodeId) ?? 0;
|
|
329
|
+
maxChange = Math.max(maxChange, Math.abs(newRank - oldRank));
|
|
330
|
+
}
|
|
331
|
+
if (maxChange < tolerance) break;
|
|
332
|
+
isCurrentRanks = !isCurrentRanks;
|
|
333
|
+
currMap.clear();
|
|
334
|
+
}
|
|
335
|
+
return isCurrentRanks ? ranks : newRanks;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Rank paths by sum of PageRank scores.
|
|
339
|
+
*
|
|
340
|
+
* @param graph - Source graph
|
|
341
|
+
* @param paths - Paths to rank
|
|
342
|
+
* @param config - Configuration options
|
|
343
|
+
* @returns Ranked paths (highest PageRank sum first)
|
|
344
|
+
*/
|
|
345
|
+
function pagerank(graph, paths, config) {
|
|
346
|
+
const { includeScores = true } = config ?? {};
|
|
347
|
+
if (paths.length === 0) return {
|
|
348
|
+
paths: [],
|
|
349
|
+
method: "pagerank"
|
|
350
|
+
};
|
|
351
|
+
const ranks = computePageRank(graph);
|
|
352
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
353
|
+
let prSum = 0;
|
|
354
|
+
for (const nodeId of path.nodes) prSum += ranks.get(nodeId) ?? 0;
|
|
355
|
+
return {
|
|
356
|
+
path,
|
|
357
|
+
score: prSum
|
|
358
|
+
};
|
|
359
|
+
}), "pagerank", includeScores);
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/ranking/baselines/betweenness.ts
|
|
363
|
+
/**
|
|
364
|
+
* Compute betweenness centrality for all nodes using Brandes algorithm.
|
|
365
|
+
*
|
|
366
|
+
* @param graph - Source graph
|
|
367
|
+
* @returns Map of node ID to betweenness value
|
|
368
|
+
*/
|
|
369
|
+
function computeBetweenness(graph) {
|
|
370
|
+
const nodes = Array.from(graph.nodeIds());
|
|
371
|
+
const betweenness = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const nodeId of nodes) betweenness.set(nodeId, 0);
|
|
373
|
+
for (const source of nodes) {
|
|
374
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
375
|
+
const distance = /* @__PURE__ */ new Map();
|
|
376
|
+
const sigma = /* @__PURE__ */ new Map();
|
|
377
|
+
const queue = [];
|
|
378
|
+
for (const nodeId of nodes) {
|
|
379
|
+
predecessors.set(nodeId, []);
|
|
380
|
+
distance.set(nodeId, -1);
|
|
381
|
+
sigma.set(nodeId, 0);
|
|
382
|
+
}
|
|
383
|
+
distance.set(source, 0);
|
|
384
|
+
sigma.set(source, 1);
|
|
385
|
+
queue.push(source);
|
|
386
|
+
for (const v of queue) {
|
|
387
|
+
const vDist = distance.get(v) ?? -1;
|
|
388
|
+
const neighbours = graph.neighbours(v);
|
|
389
|
+
for (const w of neighbours) {
|
|
390
|
+
const wDist = distance.get(w) ?? -1;
|
|
391
|
+
if (wDist < 0) {
|
|
392
|
+
distance.set(w, vDist + 1);
|
|
393
|
+
queue.push(w);
|
|
394
|
+
}
|
|
395
|
+
if (wDist === vDist + 1) {
|
|
396
|
+
const wSigma = sigma.get(w) ?? 0;
|
|
397
|
+
const vSigma = sigma.get(v) ?? 0;
|
|
398
|
+
sigma.set(w, wSigma + vSigma);
|
|
399
|
+
const wPred = predecessors.get(w) ?? [];
|
|
400
|
+
wPred.push(v);
|
|
401
|
+
predecessors.set(w, wPred);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const delta = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const nodeId of nodes) delta.set(nodeId, 0);
|
|
407
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
408
|
+
const aD = distance.get(a) ?? -1;
|
|
409
|
+
return (distance.get(b) ?? -1) - aD;
|
|
410
|
+
});
|
|
411
|
+
for (const w of sorted) {
|
|
412
|
+
if (w === source) continue;
|
|
413
|
+
const wDelta = delta.get(w) ?? 0;
|
|
414
|
+
const wSigma = sigma.get(w) ?? 0;
|
|
415
|
+
const wPred = predecessors.get(w) ?? [];
|
|
416
|
+
for (const v of wPred) {
|
|
417
|
+
const vSigma = sigma.get(v) ?? 0;
|
|
418
|
+
const vDelta = delta.get(v) ?? 0;
|
|
419
|
+
if (wSigma > 0) delta.set(v, vDelta + vSigma / wSigma * (1 + wDelta));
|
|
420
|
+
}
|
|
421
|
+
if (w !== source) {
|
|
422
|
+
const current = betweenness.get(w) ?? 0;
|
|
423
|
+
betweenness.set(w, current + wDelta);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return betweenness;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Rank paths by sum of betweenness scores.
|
|
431
|
+
*
|
|
432
|
+
* @param graph - Source graph
|
|
433
|
+
* @param paths - Paths to rank
|
|
434
|
+
* @param config - Configuration options
|
|
435
|
+
* @returns Ranked paths (highest betweenness sum first)
|
|
436
|
+
*/
|
|
437
|
+
function betweenness(graph, paths, config) {
|
|
438
|
+
const { includeScores = true } = config ?? {};
|
|
439
|
+
if (paths.length === 0) return {
|
|
440
|
+
paths: [],
|
|
441
|
+
method: "betweenness"
|
|
442
|
+
};
|
|
443
|
+
const bcMap = computeBetweenness(graph);
|
|
444
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
445
|
+
let bcSum = 0;
|
|
446
|
+
for (const nodeId of path.nodes) bcSum += bcMap.get(nodeId) ?? 0;
|
|
447
|
+
return {
|
|
448
|
+
path,
|
|
449
|
+
score: bcSum
|
|
450
|
+
};
|
|
451
|
+
}), "betweenness", includeScores);
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/ranking/baselines/katz.ts
|
|
455
|
+
/**
|
|
456
|
+
* Compute truncated Katz centrality between two nodes.
|
|
457
|
+
*
|
|
458
|
+
* Uses iterative matrix-vector products to avoid full matrix powers.
|
|
459
|
+
* score(s,t) = sum_{k=1}^{K} beta^k * walks_k(s,t)
|
|
460
|
+
*
|
|
461
|
+
* @param graph - Source graph
|
|
462
|
+
* @param source - Source node ID
|
|
463
|
+
* @param target - Target node ID
|
|
464
|
+
* @param k - Truncation depth (default 5)
|
|
465
|
+
* @param beta - Attenuation factor (default 0.005)
|
|
466
|
+
* @returns Katz score
|
|
467
|
+
*/
|
|
468
|
+
function computeKatz(graph, source, target, k = 5, beta = .005) {
|
|
469
|
+
const nodes = Array.from(graph.nodeIds());
|
|
470
|
+
const nodeToIdx = /* @__PURE__ */ new Map();
|
|
471
|
+
nodes.forEach((nodeId, idx) => {
|
|
472
|
+
nodeToIdx.set(nodeId, idx);
|
|
473
|
+
});
|
|
474
|
+
const n = nodes.length;
|
|
475
|
+
if (n === 0) return 0;
|
|
476
|
+
const sourceIdx = nodeToIdx.get(source);
|
|
477
|
+
const targetIdx = nodeToIdx.get(target);
|
|
478
|
+
if (sourceIdx === void 0 || targetIdx === void 0) return 0;
|
|
479
|
+
let walks = new Float64Array(n);
|
|
480
|
+
walks[targetIdx] = 1;
|
|
481
|
+
let katzScore = 0;
|
|
482
|
+
for (let depth = 1; depth <= k; depth++) {
|
|
483
|
+
const walksNext = new Float64Array(n);
|
|
484
|
+
for (const sourceNode of nodes) {
|
|
485
|
+
const srcIdx = nodeToIdx.get(sourceNode);
|
|
486
|
+
if (srcIdx === void 0) continue;
|
|
487
|
+
const neighbours = graph.neighbours(sourceNode);
|
|
488
|
+
for (const neighbourId of neighbours) {
|
|
489
|
+
const nIdx = nodeToIdx.get(neighbourId);
|
|
490
|
+
if (nIdx === void 0) continue;
|
|
491
|
+
walksNext[srcIdx] = (walksNext[srcIdx] ?? 0) + (walks[nIdx] ?? 0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const walkCount = walksNext[sourceIdx] ?? 0;
|
|
495
|
+
katzScore += Math.pow(beta, depth) * walkCount;
|
|
496
|
+
walks = walksNext;
|
|
497
|
+
}
|
|
498
|
+
return katzScore;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Rank paths by Katz centrality between endpoints.
|
|
502
|
+
*
|
|
503
|
+
* @param graph - Source graph
|
|
504
|
+
* @param paths - Paths to rank
|
|
505
|
+
* @param config - Configuration options
|
|
506
|
+
* @returns Ranked paths (highest Katz score first)
|
|
507
|
+
*/
|
|
508
|
+
function katz(graph, paths, config) {
|
|
509
|
+
const { includeScores = true } = config ?? {};
|
|
510
|
+
if (paths.length === 0) return {
|
|
511
|
+
paths: [],
|
|
512
|
+
method: "katz"
|
|
513
|
+
};
|
|
514
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
515
|
+
const source = path.nodes[0];
|
|
516
|
+
const target = path.nodes[path.nodes.length - 1];
|
|
517
|
+
if (source === void 0 || target === void 0) return {
|
|
518
|
+
path,
|
|
519
|
+
score: 0
|
|
520
|
+
};
|
|
521
|
+
return {
|
|
522
|
+
path,
|
|
523
|
+
score: computeKatz(graph, source, target)
|
|
524
|
+
};
|
|
525
|
+
}), "katz", includeScores);
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/ranking/baselines/communicability.ts
|
|
529
|
+
/**
|
|
530
|
+
* Compute truncated communicability between two nodes.
|
|
531
|
+
*
|
|
532
|
+
* Uses Taylor series expansion: (e^A)_{s,t} ≈ sum_{k=0}^{K} A^k_{s,t} / k!
|
|
533
|
+
*
|
|
534
|
+
* @param graph - Source graph
|
|
535
|
+
* @param source - Source node ID
|
|
536
|
+
* @param target - Target node ID
|
|
537
|
+
* @param k - Truncation depth (default 15)
|
|
538
|
+
* @returns Communicability score
|
|
539
|
+
*/
|
|
540
|
+
function computeCommunicability(graph, source, target, k = 15) {
|
|
541
|
+
const nodes = Array.from(graph.nodeIds());
|
|
542
|
+
const nodeToIdx = /* @__PURE__ */ new Map();
|
|
543
|
+
nodes.forEach((nodeId, idx) => {
|
|
544
|
+
nodeToIdx.set(nodeId, idx);
|
|
545
|
+
});
|
|
546
|
+
const n = nodes.length;
|
|
547
|
+
if (n === 0) return 0;
|
|
548
|
+
const sourceIdx = nodeToIdx.get(source);
|
|
549
|
+
const targetIdx = nodeToIdx.get(target);
|
|
550
|
+
if (sourceIdx === void 0 || targetIdx === void 0) return 0;
|
|
551
|
+
let walks = new Float64Array(n);
|
|
552
|
+
walks[targetIdx] = 1;
|
|
553
|
+
let commScore = walks[sourceIdx] ?? 0;
|
|
554
|
+
let factorial = 1;
|
|
555
|
+
for (let depth = 1; depth <= k; depth++) {
|
|
556
|
+
const walksNext = new Float64Array(n);
|
|
557
|
+
for (const fromNode of nodes) {
|
|
558
|
+
const fromIdx = nodeToIdx.get(fromNode);
|
|
559
|
+
if (fromIdx === void 0) continue;
|
|
560
|
+
const neighbours = graph.neighbours(fromNode);
|
|
561
|
+
for (const toNodeId of neighbours) {
|
|
562
|
+
const toIdx = nodeToIdx.get(toNodeId);
|
|
563
|
+
if (toIdx === void 0) continue;
|
|
564
|
+
walksNext[fromIdx] = (walksNext[fromIdx] ?? 0) + (walks[toIdx] ?? 0);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
factorial *= depth;
|
|
568
|
+
commScore += (walksNext[sourceIdx] ?? 0) / factorial;
|
|
569
|
+
walks = walksNext;
|
|
570
|
+
}
|
|
571
|
+
return commScore;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Rank paths by communicability between endpoints.
|
|
575
|
+
*
|
|
576
|
+
* @param graph - Source graph
|
|
577
|
+
* @param paths - Paths to rank
|
|
578
|
+
* @param config - Configuration options
|
|
579
|
+
* @returns Ranked paths (highest communicability first)
|
|
580
|
+
*/
|
|
581
|
+
function communicability(graph, paths, config) {
|
|
582
|
+
const { includeScores = true } = config ?? {};
|
|
583
|
+
if (paths.length === 0) return {
|
|
584
|
+
paths: [],
|
|
585
|
+
method: "communicability"
|
|
586
|
+
};
|
|
587
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
588
|
+
const source = path.nodes[0];
|
|
589
|
+
const target = path.nodes[path.nodes.length - 1];
|
|
590
|
+
if (source === void 0 || target === void 0) return {
|
|
591
|
+
path,
|
|
592
|
+
score: 0
|
|
593
|
+
};
|
|
594
|
+
return {
|
|
595
|
+
path,
|
|
596
|
+
score: computeCommunicability(graph, source, target)
|
|
597
|
+
};
|
|
598
|
+
}), "communicability", includeScores);
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/ranking/baselines/resistance-distance.ts
|
|
602
|
+
/**
|
|
603
|
+
* Compute effective resistance between two nodes via Laplacian pseudoinverse.
|
|
604
|
+
*
|
|
605
|
+
* Resistance = L^+_{s,s} + L^+_{t,t} - 2*L^+_{s,t}
|
|
606
|
+
* where L^+ is the pseudoinverse of the Laplacian matrix.
|
|
607
|
+
*
|
|
608
|
+
* @param graph - Source graph
|
|
609
|
+
* @param source - Source node ID
|
|
610
|
+
* @param target - Target node ID
|
|
611
|
+
* @returns Effective resistance
|
|
612
|
+
*/
|
|
613
|
+
function computeResistance(graph, source, target) {
|
|
614
|
+
const nodes = Array.from(graph.nodeIds());
|
|
615
|
+
const nodeToIdx = /* @__PURE__ */ new Map();
|
|
616
|
+
nodes.forEach((nodeId, idx) => {
|
|
617
|
+
nodeToIdx.set(nodeId, idx);
|
|
618
|
+
});
|
|
619
|
+
const n = nodes.length;
|
|
620
|
+
if (n === 0 || n > 5e3) throw new Error(`Cannot compute resistance distance: graph too large (${String(n)} nodes). Maximum 5000.`);
|
|
621
|
+
const sourceIdx = nodeToIdx.get(source);
|
|
622
|
+
const targetIdx = nodeToIdx.get(target);
|
|
623
|
+
if (sourceIdx === void 0 || targetIdx === void 0) return 0;
|
|
624
|
+
const L = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
|
|
625
|
+
for (let i = 0; i < n; i++) {
|
|
626
|
+
const nodeId = nodes[i];
|
|
627
|
+
if (nodeId === void 0) continue;
|
|
628
|
+
const degree = graph.degree(nodeId);
|
|
629
|
+
const row = L[i];
|
|
630
|
+
if (row !== void 0) row[i] = degree;
|
|
631
|
+
const neighbours = graph.neighbours(nodeId);
|
|
632
|
+
for (const neighbourId of neighbours) {
|
|
633
|
+
const j = nodeToIdx.get(neighbourId);
|
|
634
|
+
if (j !== void 0 && row !== void 0) row[j] = -1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const Lpinv = pinv(L);
|
|
638
|
+
const resistance = (Lpinv[sourceIdx]?.[sourceIdx] ?? 0) + (Lpinv[targetIdx]?.[targetIdx] ?? 0) - 2 * (Lpinv[sourceIdx]?.[targetIdx] ?? 0);
|
|
639
|
+
return Math.max(resistance, 1e-10);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Compute Moore-Penrose pseudoinverse of a matrix.
|
|
643
|
+
* Simplified implementation for small dense matrices.
|
|
644
|
+
*
|
|
645
|
+
* @param A - Square matrix
|
|
646
|
+
* @returns Pseudoinverse A^+
|
|
647
|
+
*/
|
|
648
|
+
function pinv(A) {
|
|
649
|
+
const n = A.length;
|
|
650
|
+
if (n === 0) return [];
|
|
651
|
+
const M = A.map((row) => [...row]);
|
|
652
|
+
const epsilon = 1e-10;
|
|
653
|
+
for (let i = 0; i < n; i++) {
|
|
654
|
+
const row = M[i];
|
|
655
|
+
if (row !== void 0) row[i] = (row[i] ?? 0) + epsilon;
|
|
656
|
+
}
|
|
657
|
+
return gaussianInverse(M);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Compute matrix inverse using Gaussian elimination with partial pivoting.
|
|
661
|
+
*
|
|
662
|
+
* @param A - Matrix to invert
|
|
663
|
+
* @returns Inverted matrix
|
|
664
|
+
*/
|
|
665
|
+
function gaussianInverse(A) {
|
|
666
|
+
const n = A.length;
|
|
667
|
+
const aug = A.map((row, i) => {
|
|
668
|
+
const identity = Array.from({ length: n }, (_, j) => i === j ? 1 : 0);
|
|
669
|
+
return [...row, ...identity];
|
|
670
|
+
});
|
|
671
|
+
for (let col = 0; col < n; col++) {
|
|
672
|
+
let maxRow = col;
|
|
673
|
+
for (let row = col + 1; row < n; row++) {
|
|
674
|
+
const currentRow = aug[row];
|
|
675
|
+
const maxRowRef = aug[maxRow];
|
|
676
|
+
if (currentRow !== void 0 && maxRowRef !== void 0 && Math.abs(currentRow[col] ?? 0) > Math.abs(maxRowRef[col] ?? 0)) maxRow = row;
|
|
677
|
+
}
|
|
678
|
+
const currentCol = aug[col];
|
|
679
|
+
const maxRowAug = aug[maxRow];
|
|
680
|
+
if (currentCol !== void 0 && maxRowAug !== void 0) {
|
|
681
|
+
aug[col] = maxRowAug;
|
|
682
|
+
aug[maxRow] = currentCol;
|
|
683
|
+
}
|
|
684
|
+
const pivotRow = aug[col];
|
|
685
|
+
const pivot = pivotRow?.[col];
|
|
686
|
+
if (pivot === void 0 || Math.abs(pivot) < 1e-12) continue;
|
|
687
|
+
if (pivotRow !== void 0) for (let j = col; j < 2 * n; j++) pivotRow[j] = (pivotRow[j] ?? 0) / pivot;
|
|
688
|
+
for (let row = 0; row < n; row++) {
|
|
689
|
+
if (row === col) continue;
|
|
690
|
+
const eliminationRow = aug[row];
|
|
691
|
+
const factor = eliminationRow?.[col] ?? 0;
|
|
692
|
+
if (eliminationRow !== void 0 && pivotRow !== void 0) for (let j = col; j < 2 * n; j++) eliminationRow[j] = (eliminationRow[j] ?? 0) - factor * (pivotRow[j] ?? 0);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const Ainv = [];
|
|
696
|
+
for (let i = 0; i < n; i++) Ainv[i] = (aug[i]?.slice(n) ?? []).map((v) => v);
|
|
697
|
+
return Ainv;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Rank paths by reciprocal of resistance distance between endpoints.
|
|
701
|
+
*
|
|
702
|
+
* @param graph - Source graph
|
|
703
|
+
* @param paths - Paths to rank
|
|
704
|
+
* @param config - Configuration options
|
|
705
|
+
* @returns Ranked paths (highest conductance first)
|
|
706
|
+
*/
|
|
707
|
+
function resistanceDistance(graph, paths, config) {
|
|
708
|
+
const { includeScores = true } = config ?? {};
|
|
709
|
+
if (paths.length === 0) return {
|
|
710
|
+
paths: [],
|
|
711
|
+
method: "resistance-distance"
|
|
712
|
+
};
|
|
713
|
+
const nodeCount = Array.from(graph.nodeIds()).length;
|
|
714
|
+
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.`);
|
|
715
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
716
|
+
const source = path.nodes[0];
|
|
717
|
+
const target = path.nodes[path.nodes.length - 1];
|
|
718
|
+
if (source === void 0 || target === void 0) return {
|
|
719
|
+
path,
|
|
720
|
+
score: 0
|
|
721
|
+
};
|
|
722
|
+
return {
|
|
723
|
+
path,
|
|
724
|
+
score: 1 / computeResistance(graph, source, target)
|
|
725
|
+
};
|
|
726
|
+
}), "resistance-distance", includeScores);
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/ranking/baselines/random-ranking.ts
|
|
730
|
+
/**
|
|
731
|
+
* Deterministic seeded random number generator.
|
|
732
|
+
* Uses FNV-1a-like hash for input → [0, 1] output.
|
|
733
|
+
*
|
|
734
|
+
* @param input - String to hash
|
|
735
|
+
* @param seed - Random seed for reproducibility
|
|
736
|
+
* @returns Deterministic random value in [0, 1]
|
|
737
|
+
*/
|
|
738
|
+
function seededRandom(input, seed = 0) {
|
|
739
|
+
let h = seed;
|
|
740
|
+
for (let i = 0; i < input.length; i++) {
|
|
741
|
+
h = Math.imul(h ^ input.charCodeAt(i), 2654435769);
|
|
742
|
+
h ^= h >>> 16;
|
|
743
|
+
}
|
|
744
|
+
return (h >>> 0) / 4294967295;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Rank paths randomly (null hypothesis baseline).
|
|
748
|
+
*
|
|
749
|
+
* @param _graph - Source graph (unused)
|
|
750
|
+
* @param paths - Paths to rank
|
|
751
|
+
* @param config - Configuration options
|
|
752
|
+
* @returns Ranked paths (randomly ordered)
|
|
753
|
+
*/
|
|
754
|
+
function randomRanking(_graph, paths, config) {
|
|
755
|
+
const { includeScores = true, seed = 0 } = config ?? {};
|
|
756
|
+
if (paths.length === 0) return {
|
|
757
|
+
paths: [],
|
|
758
|
+
method: "random"
|
|
759
|
+
};
|
|
760
|
+
return normaliseAndRank(paths, paths.map((path) => {
|
|
761
|
+
return {
|
|
762
|
+
path,
|
|
763
|
+
score: seededRandom(path.nodes.join(","), seed)
|
|
764
|
+
};
|
|
765
|
+
}), "random", includeScores);
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/ranking/baselines/hitting-time.ts
|
|
769
|
+
/**
|
|
770
|
+
* Seeded deterministic random number generator (LCG).
|
|
771
|
+
* Suitable for reproducible random walk simulation.
|
|
772
|
+
*/
|
|
773
|
+
var SeededRNG = class {
|
|
774
|
+
state;
|
|
775
|
+
constructor(seed) {
|
|
776
|
+
this.state = seed;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Generate next pseudorandom value in [0, 1).
|
|
780
|
+
*/
|
|
781
|
+
next() {
|
|
782
|
+
this.state = this.state * 1103515245 + 12345 & 2147483647;
|
|
783
|
+
return this.state / 2147483647;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
/**
|
|
787
|
+
* Compute hitting time via Monte Carlo random walk simulation.
|
|
788
|
+
*
|
|
789
|
+
* @param graph - Source graph
|
|
790
|
+
* @param source - Source node ID
|
|
791
|
+
* @param target - Target node ID
|
|
792
|
+
* @param walks - Number of walks to simulate
|
|
793
|
+
* @param maxSteps - Maximum steps per walk
|
|
794
|
+
* @param rng - Seeded RNG instance
|
|
795
|
+
* @returns Average hitting time across walks
|
|
796
|
+
*/
|
|
797
|
+
function computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng) {
|
|
798
|
+
if (source === target) return 0;
|
|
799
|
+
let totalSteps = 0;
|
|
800
|
+
let successfulWalks = 0;
|
|
801
|
+
for (let w = 0; w < walks; w++) {
|
|
802
|
+
let current = source;
|
|
803
|
+
let steps = 0;
|
|
804
|
+
while (current !== target && steps < maxSteps) {
|
|
805
|
+
const neighbours = Array.from(graph.neighbours(current));
|
|
806
|
+
if (neighbours.length === 0) break;
|
|
807
|
+
const nextNode = neighbours[Math.floor(rng.next() * neighbours.length)];
|
|
808
|
+
if (nextNode === void 0) break;
|
|
809
|
+
current = nextNode;
|
|
810
|
+
steps++;
|
|
811
|
+
}
|
|
812
|
+
if (current === target) {
|
|
813
|
+
totalSteps += steps;
|
|
814
|
+
successfulWalks++;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (successfulWalks > 0) return totalSteps / successfulWalks;
|
|
818
|
+
return maxSteps;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Compute hitting time via exact fundamental matrix method.
|
|
822
|
+
*
|
|
823
|
+
* For small graphs, computes exact expected hitting times using
|
|
824
|
+
* the fundamental matrix of the random walk.
|
|
825
|
+
*
|
|
826
|
+
* @param graph - Source graph
|
|
827
|
+
* @param source - Source node ID
|
|
828
|
+
* @param target - Target node ID
|
|
829
|
+
* @returns Exact hitting time (or approximation if convergence fails)
|
|
830
|
+
*/
|
|
831
|
+
function computeHittingTimeExact(graph, source, target) {
|
|
832
|
+
if (source === target) return 0;
|
|
833
|
+
const nodes = Array.from(graph.nodeIds());
|
|
834
|
+
const nodeToIdx = /* @__PURE__ */ new Map();
|
|
835
|
+
nodes.forEach((nodeId, idx) => {
|
|
836
|
+
nodeToIdx.set(nodeId, idx);
|
|
837
|
+
});
|
|
838
|
+
const n = nodes.length;
|
|
839
|
+
const sourceIdx = nodeToIdx.get(source);
|
|
840
|
+
const targetIdx = nodeToIdx.get(target);
|
|
841
|
+
if (sourceIdx === void 0 || targetIdx === void 0) return 0;
|
|
842
|
+
const P = [];
|
|
843
|
+
for (let i = 0; i < n; i++) {
|
|
844
|
+
const row = [];
|
|
845
|
+
for (let j = 0; j < n; j++) row[j] = 0;
|
|
846
|
+
P[i] = row;
|
|
847
|
+
}
|
|
848
|
+
for (const nodeId of nodes) {
|
|
849
|
+
const idx = nodeToIdx.get(nodeId);
|
|
850
|
+
if (idx === void 0) continue;
|
|
851
|
+
const pRow = P[idx];
|
|
852
|
+
if (pRow === void 0) continue;
|
|
853
|
+
if (idx === targetIdx) pRow[idx] = 1;
|
|
854
|
+
else {
|
|
855
|
+
const neighbours = Array.from(graph.neighbours(nodeId));
|
|
856
|
+
const degree = neighbours.length;
|
|
857
|
+
if (degree > 0) for (const neighbourId of neighbours) {
|
|
858
|
+
const nIdx = nodeToIdx.get(neighbourId);
|
|
859
|
+
if (nIdx !== void 0) pRow[nIdx] = 1 / degree;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const transientIndices = [];
|
|
864
|
+
for (let i = 0; i < n; i++) if (i !== targetIdx) transientIndices.push(i);
|
|
865
|
+
const m = transientIndices.length;
|
|
866
|
+
const Q = [];
|
|
867
|
+
for (let i = 0; i < m; i++) {
|
|
868
|
+
const row = [];
|
|
869
|
+
for (let j = 0; j < m; j++) row[j] = 0;
|
|
870
|
+
Q[i] = row;
|
|
871
|
+
}
|
|
872
|
+
for (let i = 0; i < m; i++) {
|
|
873
|
+
const qRow = Q[i];
|
|
874
|
+
if (qRow === void 0) continue;
|
|
875
|
+
const origI = transientIndices[i];
|
|
876
|
+
if (origI === void 0) continue;
|
|
877
|
+
const pRow = P[origI];
|
|
878
|
+
if (pRow === void 0) continue;
|
|
879
|
+
for (let j = 0; j < m; j++) {
|
|
880
|
+
const origJ = transientIndices[j];
|
|
881
|
+
if (origJ === void 0) continue;
|
|
882
|
+
qRow[j] = pRow[origJ] ?? 0;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const IMQ = [];
|
|
886
|
+
for (let i = 0; i < m; i++) {
|
|
887
|
+
const row = [];
|
|
888
|
+
for (let j = 0; j < m; j++) row[j] = i === j ? 1 : 0;
|
|
889
|
+
IMQ[i] = row;
|
|
890
|
+
}
|
|
891
|
+
for (let i = 0; i < m; i++) {
|
|
892
|
+
const imqRow = IMQ[i];
|
|
893
|
+
if (imqRow === void 0) continue;
|
|
894
|
+
const qRow = Q[i];
|
|
895
|
+
for (let j = 0; j < m; j++) {
|
|
896
|
+
const qVal = qRow?.[j] ?? 0;
|
|
897
|
+
imqRow[j] = (i === j ? 1 : 0) - qVal;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const N = invertMatrix(IMQ);
|
|
901
|
+
if (N === null) return 1;
|
|
902
|
+
const sourceTransientIdx = transientIndices.indexOf(sourceIdx);
|
|
903
|
+
if (sourceTransientIdx < 0) return 0;
|
|
904
|
+
let hittingTime = 0;
|
|
905
|
+
const row = N[sourceTransientIdx];
|
|
906
|
+
if (row !== void 0) for (const val of row) hittingTime += val;
|
|
907
|
+
return hittingTime;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Invert a square matrix using Gaussian elimination with partial pivoting.
|
|
911
|
+
*
|
|
912
|
+
* @param matrix - Input matrix (n × n)
|
|
913
|
+
* @returns Inverted matrix, or null if singular
|
|
914
|
+
*/
|
|
915
|
+
function invertMatrix(matrix) {
|
|
916
|
+
const n = matrix.length;
|
|
917
|
+
const aug = [];
|
|
918
|
+
for (let i = 0; i < n; i++) {
|
|
919
|
+
const row = [];
|
|
920
|
+
const matRow = matrix[i];
|
|
921
|
+
for (let j = 0; j < n; j++) row[j] = matRow?.[j] ?? 0;
|
|
922
|
+
for (let j = 0; j < n; j++) row[n + j] = i === j ? 1 : 0;
|
|
923
|
+
aug[i] = row;
|
|
924
|
+
}
|
|
925
|
+
for (let col = 0; col < n; col++) {
|
|
926
|
+
let pivotRow = col;
|
|
927
|
+
const pivotCol = aug[pivotRow];
|
|
928
|
+
if (pivotCol === void 0) return null;
|
|
929
|
+
for (let row = col + 1; row < n; row++) {
|
|
930
|
+
const currRowVal = aug[row]?.[col] ?? 0;
|
|
931
|
+
const pivotRowVal = pivotCol[col] ?? 0;
|
|
932
|
+
if (Math.abs(currRowVal) > Math.abs(pivotRowVal)) pivotRow = row;
|
|
933
|
+
}
|
|
934
|
+
const augPivot = aug[pivotRow];
|
|
935
|
+
if (augPivot === void 0 || Math.abs(augPivot[col] ?? 0) < 1e-10) return null;
|
|
936
|
+
[aug[col], aug[pivotRow]] = [aug[pivotRow] ?? [], aug[col] ?? []];
|
|
937
|
+
const scaledPivotRow = aug[col];
|
|
938
|
+
if (scaledPivotRow === void 0) return null;
|
|
939
|
+
const pivot = scaledPivotRow[col] ?? 1;
|
|
940
|
+
for (let j = 0; j < 2 * n; j++) scaledPivotRow[j] = (scaledPivotRow[j] ?? 0) / pivot;
|
|
941
|
+
for (let row = col + 1; row < n; row++) {
|
|
942
|
+
const currRow = aug[row];
|
|
943
|
+
if (currRow === void 0) continue;
|
|
944
|
+
const factor = currRow[col] ?? 0;
|
|
945
|
+
for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (scaledPivotRow[j] ?? 0);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (let col = n - 1; col > 0; col--) {
|
|
949
|
+
const colRow = aug[col];
|
|
950
|
+
if (colRow === void 0) return null;
|
|
951
|
+
for (let row = col - 1; row >= 0; row--) {
|
|
952
|
+
const currRow = aug[row];
|
|
953
|
+
if (currRow === void 0) continue;
|
|
954
|
+
const factor = currRow[col] ?? 0;
|
|
955
|
+
for (let j = 0; j < 2 * n; j++) currRow[j] = (currRow[j] ?? 0) - factor * (colRow[j] ?? 0);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const inv = [];
|
|
959
|
+
for (let i = 0; i < n; i++) {
|
|
960
|
+
const row = [];
|
|
961
|
+
for (let j = 0; j < n; j++) row[j] = 0;
|
|
962
|
+
inv[i] = row;
|
|
963
|
+
}
|
|
964
|
+
for (let i = 0; i < n; i++) {
|
|
965
|
+
const invRow = inv[i];
|
|
966
|
+
if (invRow === void 0) continue;
|
|
967
|
+
const augRow = aug[i];
|
|
968
|
+
if (augRow === void 0) continue;
|
|
969
|
+
for (let j = 0; j < n; j++) invRow[j] = augRow[n + j] ?? 0;
|
|
970
|
+
}
|
|
971
|
+
return inv;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Rank paths by inverse hitting time between endpoints.
|
|
975
|
+
*
|
|
976
|
+
* @param graph - Source graph
|
|
977
|
+
* @param paths - Paths to rank
|
|
978
|
+
* @param config - Configuration options
|
|
979
|
+
* @returns Ranked paths (highest inverse hitting time first)
|
|
980
|
+
*/
|
|
981
|
+
function hittingTime(graph, paths, config) {
|
|
982
|
+
const { includeScores = true, mode = "auto", walks = 1e3, maxSteps = 1e4, seed = 42 } = config ?? {};
|
|
983
|
+
if (paths.length === 0) return {
|
|
984
|
+
paths: [],
|
|
985
|
+
method: "hitting-time"
|
|
986
|
+
};
|
|
987
|
+
const nodeCount = Array.from(graph.nodeIds()).length;
|
|
988
|
+
const actualMode = mode === "auto" ? nodeCount < 100 ? "exact" : "approximate" : mode;
|
|
989
|
+
const rng = new SeededRNG(seed);
|
|
990
|
+
const scored = paths.map((path) => {
|
|
991
|
+
const source = path.nodes[0];
|
|
992
|
+
const target = path.nodes[path.nodes.length - 1];
|
|
993
|
+
if (source === void 0 || target === void 0) return {
|
|
994
|
+
path,
|
|
995
|
+
score: 0
|
|
996
|
+
};
|
|
997
|
+
const ht = actualMode === "exact" ? computeHittingTimeExact(graph, source, target) : computeHittingTimeApproximate(graph, source, target, walks, maxSteps, rng);
|
|
998
|
+
return {
|
|
999
|
+
path,
|
|
1000
|
+
score: ht > 0 ? 1 / ht : 0
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
const maxScore = Math.max(...scored.map((s) => s.score));
|
|
1004
|
+
if (!Number.isFinite(maxScore)) return {
|
|
1005
|
+
paths: paths.map((path) => ({
|
|
1006
|
+
...path,
|
|
1007
|
+
score: 0
|
|
1008
|
+
})),
|
|
1009
|
+
method: "hitting-time"
|
|
1010
|
+
};
|
|
1011
|
+
return normaliseAndRank(paths, scored, "hitting-time", includeScores);
|
|
1012
|
+
}
|
|
1013
|
+
//#endregion
|
|
1014
|
+
Object.defineProperty(exports, "betweenness", {
|
|
1015
|
+
enumerable: true,
|
|
1016
|
+
get: function() {
|
|
1017
|
+
return betweenness;
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
Object.defineProperty(exports, "communicability", {
|
|
1021
|
+
enumerable: true,
|
|
1022
|
+
get: function() {
|
|
1023
|
+
return communicability;
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
Object.defineProperty(exports, "degreeSum", {
|
|
1027
|
+
enumerable: true,
|
|
1028
|
+
get: function() {
|
|
1029
|
+
return degreeSum;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
Object.defineProperty(exports, "hittingTime", {
|
|
1033
|
+
enumerable: true,
|
|
1034
|
+
get: function() {
|
|
1035
|
+
return hittingTime;
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
Object.defineProperty(exports, "jaccardArithmetic", {
|
|
1039
|
+
enumerable: true,
|
|
1040
|
+
get: function() {
|
|
1041
|
+
return jaccardArithmetic;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
Object.defineProperty(exports, "katz", {
|
|
1045
|
+
enumerable: true,
|
|
1046
|
+
get: function() {
|
|
1047
|
+
return katz;
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
Object.defineProperty(exports, "pagerank", {
|
|
1051
|
+
enumerable: true,
|
|
1052
|
+
get: function() {
|
|
1053
|
+
return pagerank;
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
Object.defineProperty(exports, "parse", {
|
|
1057
|
+
enumerable: true,
|
|
1058
|
+
get: function() {
|
|
1059
|
+
return parse;
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
Object.defineProperty(exports, "parseAsync", {
|
|
1063
|
+
enumerable: true,
|
|
1064
|
+
get: function() {
|
|
1065
|
+
return parseAsync;
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
Object.defineProperty(exports, "randomRanking", {
|
|
1069
|
+
enumerable: true,
|
|
1070
|
+
get: function() {
|
|
1071
|
+
return randomRanking;
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
Object.defineProperty(exports, "resistanceDistance", {
|
|
1075
|
+
enumerable: true,
|
|
1076
|
+
get: function() {
|
|
1077
|
+
return resistanceDistance;
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
Object.defineProperty(exports, "shortest", {
|
|
1081
|
+
enumerable: true,
|
|
1082
|
+
get: function() {
|
|
1083
|
+
return shortest;
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
Object.defineProperty(exports, "widestPath", {
|
|
1087
|
+
enumerable: true,
|
|
1088
|
+
get: function() {
|
|
1089
|
+
return widestPath;
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
//# sourceMappingURL=ranking-riRrEVAR.cjs.map
|