graphwise 1.10.0 → 1.11.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/dist/index/index.cjs +4 -0
- package/dist/index/index.js +2 -2
- package/dist/seeds/crest.d.ts +48 -0
- package/dist/seeds/crest.d.ts.map +1 -0
- package/dist/seeds/crest.unit.test.d.ts +2 -0
- package/dist/seeds/crest.unit.test.d.ts.map +1 -0
- package/dist/seeds/crisp.d.ts +57 -0
- package/dist/seeds/crisp.d.ts.map +1 -0
- package/dist/seeds/crisp.unit.test.d.ts +2 -0
- package/dist/seeds/crisp.unit.test.d.ts.map +1 -0
- package/dist/seeds/index.cjs +714 -4
- package/dist/seeds/index.cjs.map +1 -1
- package/dist/seeds/index.d.ts +4 -0
- package/dist/seeds/index.d.ts.map +1 -1
- package/dist/seeds/index.js +711 -5
- package/dist/seeds/index.js.map +1 -1
- package/dist/seeds/spine.d.ts +50 -0
- package/dist/seeds/spine.d.ts.map +1 -0
- package/dist/seeds/spine.unit.test.d.ts +2 -0
- package/dist/seeds/spine.unit.test.d.ts.map +1 -0
- package/dist/seeds/stride.d.ts +55 -0
- package/dist/seeds/stride.d.ts.map +1 -0
- package/dist/seeds/stride.unit.test.d.ts +2 -0
- package/dist/seeds/stride.unit.test.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/seeds/index.cjs
CHANGED
|
@@ -1,8 +1,343 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_kmeans = require("../kmeans-CZ7tJFYw.cjs");
|
|
3
|
+
//#region src/seeds/crisp.ts
|
|
4
|
+
/** Default configuration values */
|
|
5
|
+
var DEFAULTS$5 = {
|
|
6
|
+
nPairs: 100,
|
|
7
|
+
rngSeed: 42,
|
|
8
|
+
minDistance: 2,
|
|
9
|
+
maxDistance: 4,
|
|
10
|
+
minCommonNeighbours: 2,
|
|
11
|
+
diversityThreshold: .5,
|
|
12
|
+
sampleSize: 5e3
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Simple seeded pseudo-random number generator using mulberry32.
|
|
16
|
+
*/
|
|
17
|
+
function createRNG$5(seed) {
|
|
18
|
+
let state = seed >>> 0;
|
|
19
|
+
return () => {
|
|
20
|
+
state = state + 1831565813 >>> 0;
|
|
21
|
+
let t = Math.imul(state ^ state >>> 15, state | 1);
|
|
22
|
+
t = (t ^ t >>> 7) * (t | 1640531527);
|
|
23
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute shortest-path distance between two nodes via BFS.
|
|
28
|
+
* Returns -1 if no path exists.
|
|
29
|
+
*/
|
|
30
|
+
function bfsDistance(graph, source, target) {
|
|
31
|
+
if (source === target) return 0;
|
|
32
|
+
const visited = new Set([source]);
|
|
33
|
+
const queue = [{
|
|
34
|
+
node: source,
|
|
35
|
+
dist: 0
|
|
36
|
+
}];
|
|
37
|
+
while (queue.length > 0) {
|
|
38
|
+
const item = queue.shift();
|
|
39
|
+
if (!item) break;
|
|
40
|
+
const { node, dist } = item;
|
|
41
|
+
for (const neighbour of graph.neighbours(node)) {
|
|
42
|
+
if (neighbour === target) return dist + 1;
|
|
43
|
+
if (!visited.has(neighbour)) {
|
|
44
|
+
visited.add(neighbour);
|
|
45
|
+
queue.push({
|
|
46
|
+
node: neighbour,
|
|
47
|
+
dist: dist + 1
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return -1;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the 1-hop neighbour set of a node.
|
|
56
|
+
*/
|
|
57
|
+
function neighbourSet(graph, node) {
|
|
58
|
+
return new Set(graph.neighbours(node));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Count common neighbours between two nodes.
|
|
62
|
+
*/
|
|
63
|
+
function commonNeighbours(graph, a, b) {
|
|
64
|
+
const na = neighbourSet(graph, a);
|
|
65
|
+
const nb = neighbourSet(graph, b);
|
|
66
|
+
let count = 0;
|
|
67
|
+
for (const n of na) if (nb.has(n)) count++;
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compute Jaccard similarity between two sets.
|
|
72
|
+
*/
|
|
73
|
+
function jaccard$3(a, b) {
|
|
74
|
+
let intersection = 0;
|
|
75
|
+
for (const x of a) if (b.has(x)) intersection++;
|
|
76
|
+
const union = new Set([...a, ...b]).size;
|
|
77
|
+
return union === 0 ? 0 : intersection / union;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Compute distance score peaking at distance 3.
|
|
81
|
+
* Score ranges from 0 to 1, with maximum at dist=3.
|
|
82
|
+
*/
|
|
83
|
+
function distanceScore(dist) {
|
|
84
|
+
return 1 - Math.abs(dist - 3) / 3;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* CRISP — Connectivity-Rich Informed Seed Pairing.
|
|
88
|
+
*
|
|
89
|
+
* Samples random node pairs and scores them by common neighbour count
|
|
90
|
+
* and BFS distance (preferring 2–4 hops, peaking at 3). Greedy
|
|
91
|
+
* selection with Jaccard diversity ensures selected pairs are
|
|
92
|
+
* spread across different structural regions.
|
|
93
|
+
*
|
|
94
|
+
* @param graph - The graph to sample seeds from
|
|
95
|
+
* @param options - Configuration options
|
|
96
|
+
* @returns Selected seed pairs with connectivity metadata
|
|
97
|
+
*/
|
|
98
|
+
function crisp(graph, options = {}) {
|
|
99
|
+
const config = {
|
|
100
|
+
...DEFAULTS$5,
|
|
101
|
+
...options
|
|
102
|
+
};
|
|
103
|
+
const rng = createRNG$5(config.rngSeed);
|
|
104
|
+
const allNodes = [...graph.nodeIds()];
|
|
105
|
+
if (allNodes.length < 2) return { pairs: [] };
|
|
106
|
+
if (allNodes.length < 4) {
|
|
107
|
+
const a = allNodes[0];
|
|
108
|
+
const b = allNodes[1];
|
|
109
|
+
if (a !== void 0 && b !== void 0) return { pairs: [{
|
|
110
|
+
source: { id: a },
|
|
111
|
+
target: { id: b },
|
|
112
|
+
distance: 1,
|
|
113
|
+
commonNeighbours: 0,
|
|
114
|
+
score: 0
|
|
115
|
+
}] };
|
|
116
|
+
return { pairs: [] };
|
|
117
|
+
}
|
|
118
|
+
const candidates = [];
|
|
119
|
+
const sampledPairs = /* @__PURE__ */ new Set();
|
|
120
|
+
const maxAttempts = config.sampleSize * 10;
|
|
121
|
+
let attempts = 0;
|
|
122
|
+
while (sampledPairs.size < config.sampleSize && attempts < maxAttempts) {
|
|
123
|
+
attempts++;
|
|
124
|
+
const a = allNodes[Math.floor(rng() * allNodes.length)];
|
|
125
|
+
const b = allNodes[Math.floor(rng() * allNodes.length)];
|
|
126
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
127
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
128
|
+
if (sampledPairs.has(pairKey)) continue;
|
|
129
|
+
sampledPairs.add(pairKey);
|
|
130
|
+
const dist = bfsDistance(graph, a, b);
|
|
131
|
+
if (dist < config.minDistance || dist > config.maxDistance) continue;
|
|
132
|
+
const cn = commonNeighbours(graph, a, b);
|
|
133
|
+
if (cn < config.minCommonNeighbours) continue;
|
|
134
|
+
const score = cn * (1 + distanceScore(dist));
|
|
135
|
+
candidates.push({
|
|
136
|
+
score,
|
|
137
|
+
distance: dist,
|
|
138
|
+
commonNeighbours: cn,
|
|
139
|
+
a,
|
|
140
|
+
b
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (candidates.length < config.nPairs) for (const pairKey of sampledPairs) {
|
|
144
|
+
const parts = pairKey.split("|");
|
|
145
|
+
if (parts.length !== 2) continue;
|
|
146
|
+
const a = parts[0];
|
|
147
|
+
const b = parts[1];
|
|
148
|
+
if (a === void 0 || b === void 0) continue;
|
|
149
|
+
const dist = bfsDistance(graph, a, b);
|
|
150
|
+
if (dist < 1 || dist > 6) continue;
|
|
151
|
+
const cn = commonNeighbours(graph, a, b);
|
|
152
|
+
const score = cn + .1;
|
|
153
|
+
candidates.push({
|
|
154
|
+
score,
|
|
155
|
+
distance: dist,
|
|
156
|
+
commonNeighbours: cn,
|
|
157
|
+
a,
|
|
158
|
+
b
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
candidates.sort((x, y) => y.score - x.score);
|
|
162
|
+
const selected = [];
|
|
163
|
+
const selectedPairKeys = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const { score, distance, commonNeighbours, a, b } of candidates) {
|
|
165
|
+
if (selected.length >= config.nPairs) break;
|
|
166
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
167
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
168
|
+
const aNbrs = neighbourSet(graph, a);
|
|
169
|
+
const bNbrs = neighbourSet(graph, b);
|
|
170
|
+
let isDiverse = true;
|
|
171
|
+
for (const prev of selected) {
|
|
172
|
+
const paNbrs = neighbourSet(graph, prev.source.id);
|
|
173
|
+
const pbNbrs = neighbourSet(graph, prev.target.id);
|
|
174
|
+
if (jaccard$3(aNbrs, paNbrs) >= config.diversityThreshold && jaccard$3(bNbrs, pbNbrs) >= config.diversityThreshold) {
|
|
175
|
+
isDiverse = false;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!isDiverse) continue;
|
|
180
|
+
selectedPairKeys.add(pairKey);
|
|
181
|
+
selected.push({
|
|
182
|
+
source: { id: a },
|
|
183
|
+
target: { id: b },
|
|
184
|
+
distance,
|
|
185
|
+
commonNeighbours,
|
|
186
|
+
score
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
let fillAttempts = 0;
|
|
190
|
+
while (selected.length < config.nPairs && allNodes.length >= 2 && fillAttempts < config.nPairs * 20) {
|
|
191
|
+
fillAttempts++;
|
|
192
|
+
const i1 = Math.floor(rng() * allNodes.length);
|
|
193
|
+
const i2 = Math.floor(rng() * allNodes.length);
|
|
194
|
+
const a = allNodes[i1];
|
|
195
|
+
const b = allNodes[i2];
|
|
196
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
197
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
198
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
199
|
+
selectedPairKeys.add(pairKey);
|
|
200
|
+
selected.push({
|
|
201
|
+
source: { id: a },
|
|
202
|
+
target: { id: b },
|
|
203
|
+
distance: 0,
|
|
204
|
+
commonNeighbours: 0,
|
|
205
|
+
score: 0
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { pairs: selected.slice(0, config.nPairs) };
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/seeds/crest.ts
|
|
212
|
+
/** Default configuration values */
|
|
213
|
+
var DEFAULTS$4 = {
|
|
214
|
+
nPairs: 100,
|
|
215
|
+
rngSeed: 42,
|
|
216
|
+
diversityThreshold: .5,
|
|
217
|
+
sampleSize: 5e3
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Simple seeded pseudo-random number generator using mulberry32.
|
|
221
|
+
*/
|
|
222
|
+
function createRNG$4(seed) {
|
|
223
|
+
let state = seed >>> 0;
|
|
224
|
+
return () => {
|
|
225
|
+
state = state + 1831565813 >>> 0;
|
|
226
|
+
let t = Math.imul(state ^ state >>> 15, state | 1);
|
|
227
|
+
t = (t ^ t >>> 7) * (t | 1640531527);
|
|
228
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Compute community-bridge score for a pair of nodes.
|
|
233
|
+
*
|
|
234
|
+
* High score = pair connects different communities.
|
|
235
|
+
* Ratio = exclusive_neighbours / shared_neighbours.
|
|
236
|
+
*
|
|
237
|
+
* Nodes with many shared neighbours are in the same dense community.
|
|
238
|
+
* Nodes with few shared neighbours relative to exclusive ones span communities.
|
|
239
|
+
*/
|
|
240
|
+
function bridgeScore(aNbrs, bNbrs, bId, aId) {
|
|
241
|
+
const shared = [...aNbrs].filter((x) => bNbrs.has(x)).length;
|
|
242
|
+
const exclusive = [...aNbrs].filter((x) => !bNbrs.has(x) && x !== bId).length + [...bNbrs].filter((x) => !aNbrs.has(x) && x !== aId).length;
|
|
243
|
+
if (shared === 0) return 1 / Math.max(exclusive, 1);
|
|
244
|
+
return exclusive / shared;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Compute Jaccard similarity between two sets.
|
|
248
|
+
*/
|
|
249
|
+
function jaccard$2(a, b) {
|
|
250
|
+
const intersection = [...a].filter((x) => b.has(x)).length;
|
|
251
|
+
const union = new Set([...a, ...b]).size;
|
|
252
|
+
return union === 0 ? 0 : intersection / union;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* CREST — Community-Revealing Edge Sampling Technique.
|
|
256
|
+
*
|
|
257
|
+
* Samples random node pairs and scores them by the ratio of exclusive
|
|
258
|
+
* to shared neighbours. Pairs with high ratios connect different
|
|
259
|
+
* communities (few shared neighbours, many exclusive). Greedy
|
|
260
|
+
* selection with Jaccard diversity ensures selected pairs are
|
|
261
|
+
* spread across different structural regions.
|
|
262
|
+
*
|
|
263
|
+
* @param graph - The graph to sample seeds from
|
|
264
|
+
* @param options - Configuration options
|
|
265
|
+
* @returns Selected seed pairs with bridge score metadata
|
|
266
|
+
*/
|
|
267
|
+
function crest(graph, options = {}) {
|
|
268
|
+
const config = {
|
|
269
|
+
...DEFAULTS$4,
|
|
270
|
+
...options
|
|
271
|
+
};
|
|
272
|
+
const rng = createRNG$4(config.rngSeed);
|
|
273
|
+
const allNodes = [...graph.nodeIds()];
|
|
274
|
+
if (allNodes.length < 2) return { pairs: [] };
|
|
275
|
+
const candidates = [];
|
|
276
|
+
const sampledPairs = /* @__PURE__ */ new Set();
|
|
277
|
+
const maxAttempts = config.sampleSize * 10;
|
|
278
|
+
let attempts = 0;
|
|
279
|
+
while (sampledPairs.size < config.sampleSize && attempts < maxAttempts) {
|
|
280
|
+
attempts++;
|
|
281
|
+
const a = allNodes[Math.floor(rng() * allNodes.length)];
|
|
282
|
+
const b = allNodes[Math.floor(rng() * allNodes.length)];
|
|
283
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
284
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
285
|
+
if (sampledPairs.has(pairKey)) continue;
|
|
286
|
+
sampledPairs.add(pairKey);
|
|
287
|
+
const score = bridgeScore(new Set(graph.neighbours(a)), new Set(graph.neighbours(b)), b, a);
|
|
288
|
+
candidates.push({
|
|
289
|
+
score,
|
|
290
|
+
a,
|
|
291
|
+
b
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
candidates.sort((x, y) => y.score - x.score);
|
|
295
|
+
const selected = [];
|
|
296
|
+
const selectedPairKeys = /* @__PURE__ */ new Set();
|
|
297
|
+
for (const { score, a, b } of candidates) {
|
|
298
|
+
if (selected.length >= config.nPairs) break;
|
|
299
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
300
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
301
|
+
const aNbrs = new Set(graph.neighbours(a));
|
|
302
|
+
const bNbrs = new Set(graph.neighbours(b));
|
|
303
|
+
let isDiverse = true;
|
|
304
|
+
for (const prev of selected) if (jaccard$2(aNbrs, new Set(graph.neighbours(prev.source.id))) >= config.diversityThreshold) {
|
|
305
|
+
if (jaccard$2(bNbrs, new Set(graph.neighbours(prev.target.id))) >= config.diversityThreshold) {
|
|
306
|
+
isDiverse = false;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!isDiverse) continue;
|
|
311
|
+
selectedPairKeys.add(pairKey);
|
|
312
|
+
selected.push({
|
|
313
|
+
source: { id: a },
|
|
314
|
+
target: { id: b },
|
|
315
|
+
bridgeScore: score
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
let fillAttempts = 0;
|
|
319
|
+
while (selected.length < config.nPairs && allNodes.length >= 2 && fillAttempts < config.nPairs * 20) {
|
|
320
|
+
fillAttempts++;
|
|
321
|
+
const i1 = Math.floor(rng() * allNodes.length);
|
|
322
|
+
const i2 = Math.floor(rng() * allNodes.length);
|
|
323
|
+
const a = allNodes[i1];
|
|
324
|
+
const b = allNodes[i2];
|
|
325
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
326
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
327
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
328
|
+
selectedPairKeys.add(pairKey);
|
|
329
|
+
selected.push({
|
|
330
|
+
source: { id: a },
|
|
331
|
+
target: { id: b },
|
|
332
|
+
bridgeScore: 0
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return { pairs: selected.slice(0, config.nPairs) };
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
3
338
|
//#region src/seeds/grasp.ts
|
|
4
339
|
/** Default configuration values */
|
|
5
|
-
var DEFAULTS$
|
|
340
|
+
var DEFAULTS$3 = {
|
|
6
341
|
nClusters: 100,
|
|
7
342
|
pairsPerCluster: 10,
|
|
8
343
|
withinClusterRatio: .5,
|
|
@@ -13,7 +348,7 @@ var DEFAULTS$1 = {
|
|
|
13
348
|
/**
|
|
14
349
|
* Simple seeded pseudo-random number generator using mulberry32.
|
|
15
350
|
*/
|
|
16
|
-
function createRNG$
|
|
351
|
+
function createRNG$3(seed) {
|
|
17
352
|
let state = seed >>> 0;
|
|
18
353
|
return () => {
|
|
19
354
|
state = state + 1831565813 >>> 0;
|
|
@@ -261,10 +596,10 @@ function computeFeatureDistance(a, b) {
|
|
|
261
596
|
*/
|
|
262
597
|
function grasp(graph, options = {}) {
|
|
263
598
|
const config = {
|
|
264
|
-
...DEFAULTS$
|
|
599
|
+
...DEFAULTS$3,
|
|
265
600
|
...options
|
|
266
601
|
};
|
|
267
|
-
const rng = createRNG$
|
|
602
|
+
const rng = createRNG$3(config.rngSeed);
|
|
268
603
|
const { nodeIds, neighbourMap } = reservoirSample(graph, config.sampleSize, rng);
|
|
269
604
|
let features = computeFeatures(graph, nodeIds, neighbourMap, approximatePageRank(nodeIds, neighbourMap, config.pagerankIterations));
|
|
270
605
|
if (features.length > 0) features = require_kmeans.normaliseFeatures(features);
|
|
@@ -283,6 +618,377 @@ function grasp(graph, options = {}) {
|
|
|
283
618
|
};
|
|
284
619
|
}
|
|
285
620
|
//#endregion
|
|
621
|
+
//#region src/seeds/spine.ts
|
|
622
|
+
/** Default configuration values */
|
|
623
|
+
var DEFAULTS$2 = {
|
|
624
|
+
nPairs: 100,
|
|
625
|
+
rngSeed: 42,
|
|
626
|
+
diversityThreshold: .5
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Simple seeded pseudo-random number generator using mulberry32.
|
|
630
|
+
*/
|
|
631
|
+
function createRNG$2(seed) {
|
|
632
|
+
let state = seed >>> 0;
|
|
633
|
+
return () => {
|
|
634
|
+
state = state + 1831565813 >>> 0;
|
|
635
|
+
let t = Math.imul(state ^ state >>> 15, state | 1);
|
|
636
|
+
t = (t ^ t >>> 7) * (t | 1640531527);
|
|
637
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Compute skewness of the 2-hop degree distribution for a node.
|
|
642
|
+
*
|
|
643
|
+
* For each neighbour u of node, collect the degree of u.
|
|
644
|
+
* Skewness = E[(X - μ)³] / σ³ where X is the degree distribution.
|
|
645
|
+
*
|
|
646
|
+
* High skewness = neighbour degrees are heavily concentrated at one end.
|
|
647
|
+
* Low skewness = neighbour degrees are relatively uniform.
|
|
648
|
+
*
|
|
649
|
+
* Returns 0.0 for nodes with fewer than 3 neighbours.
|
|
650
|
+
*/
|
|
651
|
+
function degreeSkewness(graph, node) {
|
|
652
|
+
const neighbours = [...graph.neighbours(node)];
|
|
653
|
+
if (neighbours.length < 3) return 0;
|
|
654
|
+
const degrees = neighbours.map((n) => graph.degree(n, "both"));
|
|
655
|
+
const n = degrees.length;
|
|
656
|
+
const mean = degrees.reduce((s, d) => s + d, 0) / n;
|
|
657
|
+
const variance = degrees.reduce((s, d) => s + (d - mean) ** 2, 0) / n;
|
|
658
|
+
if (variance < 1e-10) return 0;
|
|
659
|
+
const std = Math.sqrt(variance);
|
|
660
|
+
return degrees.reduce((s, d) => s + (d - mean) ** 3, 0) / n / std ** 3;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Compute Jaccard similarity between two sets.
|
|
664
|
+
*/
|
|
665
|
+
function jaccard$1(a, b) {
|
|
666
|
+
const intersection = [...a].filter((x) => b.has(x)).length;
|
|
667
|
+
const union = new Set([...a, ...b]).size;
|
|
668
|
+
return union === 0 ? 0 : intersection / union;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* SPINE — Structural Position-Informed Node Extraction.
|
|
672
|
+
*
|
|
673
|
+
* Computes 2-hop degree distribution skewness for each node. Nodes with
|
|
674
|
+
* high positive skewness have structurally diverse neighbours (hub-periphery
|
|
675
|
+
* mix), while nodes with low skewness have uniform neighbours. Pairs
|
|
676
|
+
* connecting high-skewness and low-skewness nodes explore structurally
|
|
677
|
+
* varied terrain.
|
|
678
|
+
*
|
|
679
|
+
* @param graph - The graph to sample seeds from
|
|
680
|
+
* @param options - Configuration options
|
|
681
|
+
* @returns Selected seed pairs with skewness metadata
|
|
682
|
+
*/
|
|
683
|
+
function spine(graph, options = {}) {
|
|
684
|
+
const config = {
|
|
685
|
+
...DEFAULTS$2,
|
|
686
|
+
...options
|
|
687
|
+
};
|
|
688
|
+
const rng = createRNG$2(config.rngSeed);
|
|
689
|
+
const allNodes = [...graph.nodeIds()];
|
|
690
|
+
if (allNodes.length < 2) return {
|
|
691
|
+
pairs: [],
|
|
692
|
+
skewness: /* @__PURE__ */ new Map()
|
|
693
|
+
};
|
|
694
|
+
const skewnessMap = /* @__PURE__ */ new Map();
|
|
695
|
+
for (const node of allNodes) skewnessMap.set(node, degreeSkewness(graph, node));
|
|
696
|
+
const sortedNodes = [...allNodes].sort((a, b) => (skewnessMap.get(a) ?? 0) - (skewnessMap.get(b) ?? 0));
|
|
697
|
+
const n = sortedNodes.length;
|
|
698
|
+
const lowSkew = sortedNodes.slice(0, Math.floor(n / 3));
|
|
699
|
+
const midSkew = sortedNodes.slice(Math.floor(n / 3), Math.floor(2 * n / 3));
|
|
700
|
+
const highSkew = sortedNodes.slice(Math.floor(2 * n / 3));
|
|
701
|
+
const candidates = [];
|
|
702
|
+
const sampleSize = Math.max(config.nPairs * 20, 200);
|
|
703
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
704
|
+
if (!highSkew.length || !lowSkew.length) break;
|
|
705
|
+
const a = highSkew[Math.floor(rng() * highSkew.length)];
|
|
706
|
+
const b = lowSkew[Math.floor(rng() * lowSkew.length)];
|
|
707
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
708
|
+
const skA = skewnessMap.get(a) ?? 0;
|
|
709
|
+
const skB = skewnessMap.get(b) ?? 0;
|
|
710
|
+
candidates.push({
|
|
711
|
+
score: Math.abs(skA - skB),
|
|
712
|
+
a,
|
|
713
|
+
b
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
const halfSample = Math.floor(sampleSize / 2);
|
|
717
|
+
for (let i = 0; i < halfSample; i++) {
|
|
718
|
+
if (midSkew.length && highSkew.length) {
|
|
719
|
+
const a = highSkew[Math.floor(rng() * highSkew.length)];
|
|
720
|
+
const b = midSkew[Math.floor(rng() * midSkew.length)];
|
|
721
|
+
if (a !== void 0 && b !== void 0 && a !== b) {
|
|
722
|
+
const skA = skewnessMap.get(a) ?? 0;
|
|
723
|
+
const skB = skewnessMap.get(b) ?? 0;
|
|
724
|
+
candidates.push({
|
|
725
|
+
score: Math.abs(skA - skB) * .8,
|
|
726
|
+
a,
|
|
727
|
+
b
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (midSkew.length && lowSkew.length) {
|
|
732
|
+
const a = midSkew[Math.floor(rng() * midSkew.length)];
|
|
733
|
+
const b = lowSkew[Math.floor(rng() * lowSkew.length)];
|
|
734
|
+
if (a !== void 0 && b !== void 0 && a !== b) {
|
|
735
|
+
const skA = skewnessMap.get(a) ?? 0;
|
|
736
|
+
const skB = skewnessMap.get(b) ?? 0;
|
|
737
|
+
candidates.push({
|
|
738
|
+
score: Math.abs(skA - skB) * .8,
|
|
739
|
+
a,
|
|
740
|
+
b
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const quarterSample = Math.floor(sampleSize / 4);
|
|
746
|
+
for (let i = 0; i < quarterSample; i++) if (highSkew.length >= 2) {
|
|
747
|
+
const i1 = Math.floor(rng() * highSkew.length);
|
|
748
|
+
let i2 = Math.floor(rng() * highSkew.length);
|
|
749
|
+
while (i1 === i2) i2 = Math.floor(rng() * highSkew.length);
|
|
750
|
+
const a = highSkew[i1];
|
|
751
|
+
const b = highSkew[i2];
|
|
752
|
+
if (a !== void 0 && b !== void 0) {
|
|
753
|
+
const skA = skewnessMap.get(a) ?? 0;
|
|
754
|
+
const skB = skewnessMap.get(b) ?? 0;
|
|
755
|
+
candidates.push({
|
|
756
|
+
score: Math.abs(skA - skB) * .5,
|
|
757
|
+
a,
|
|
758
|
+
b
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
candidates.sort((x, y) => y.score - x.score);
|
|
763
|
+
const selected = [];
|
|
764
|
+
const selectedPairKeys = /* @__PURE__ */ new Set();
|
|
765
|
+
for (const { a, b } of candidates) {
|
|
766
|
+
if (selected.length >= config.nPairs) break;
|
|
767
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
768
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
769
|
+
const aNbrs = new Set(graph.neighbours(a));
|
|
770
|
+
const bNbrs = new Set(graph.neighbours(b));
|
|
771
|
+
let isDiverse = true;
|
|
772
|
+
for (const prev of selected) if (jaccard$1(aNbrs, new Set(graph.neighbours(prev.source.id))) >= config.diversityThreshold) {
|
|
773
|
+
if (jaccard$1(bNbrs, new Set(graph.neighbours(prev.target.id))) >= config.diversityThreshold) {
|
|
774
|
+
isDiverse = false;
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (!isDiverse) continue;
|
|
779
|
+
selectedPairKeys.add(pairKey);
|
|
780
|
+
selected.push({
|
|
781
|
+
source: { id: a },
|
|
782
|
+
target: { id: b },
|
|
783
|
+
sourceSkewness: skewnessMap.get(a) ?? 0,
|
|
784
|
+
targetSkewness: skewnessMap.get(b) ?? 0
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
let fillAttempts = 0;
|
|
788
|
+
while (selected.length < config.nPairs && allNodes.length >= 2 && fillAttempts < config.nPairs * 20) {
|
|
789
|
+
fillAttempts++;
|
|
790
|
+
const i1 = Math.floor(rng() * allNodes.length);
|
|
791
|
+
const i2 = Math.floor(rng() * allNodes.length);
|
|
792
|
+
const a = allNodes[i1];
|
|
793
|
+
const b = allNodes[i2];
|
|
794
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
795
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
796
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
797
|
+
selectedPairKeys.add(pairKey);
|
|
798
|
+
selected.push({
|
|
799
|
+
source: { id: a },
|
|
800
|
+
target: { id: b },
|
|
801
|
+
sourceSkewness: skewnessMap.get(a) ?? 0,
|
|
802
|
+
targetSkewness: skewnessMap.get(b) ?? 0
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
pairs: selected.slice(0, config.nPairs),
|
|
807
|
+
skewness: skewnessMap
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
//#endregion
|
|
811
|
+
//#region src/seeds/stride.ts
|
|
812
|
+
/** Default configuration values */
|
|
813
|
+
var DEFAULTS$1 = {
|
|
814
|
+
nPairs: 100,
|
|
815
|
+
rngSeed: 42,
|
|
816
|
+
diversityThreshold: .5
|
|
817
|
+
};
|
|
818
|
+
/**
|
|
819
|
+
* Simple seeded pseudo-random number generator using mulberry32.
|
|
820
|
+
*/
|
|
821
|
+
function createRNG$1(seed) {
|
|
822
|
+
let state = seed >>> 0;
|
|
823
|
+
return () => {
|
|
824
|
+
state = state + 1831565813 >>> 0;
|
|
825
|
+
let t = Math.imul(state ^ state >>> 15, state | 1);
|
|
826
|
+
t = (t ^ t >>> 7) * (t | 1640531527);
|
|
827
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Count closed triads (3-cycles) involving a node.
|
|
832
|
+
*
|
|
833
|
+
* A triad is a triple (node, u, v) where u and v are both neighbours
|
|
834
|
+
* of node AND u-v is an edge.
|
|
835
|
+
*/
|
|
836
|
+
function countTriads(graph, node) {
|
|
837
|
+
const neighbours = [...graph.neighbours(node)];
|
|
838
|
+
if (neighbours.length < 2) return 0;
|
|
839
|
+
const neighbourSet = new Set(neighbours);
|
|
840
|
+
let count = 0;
|
|
841
|
+
for (let i = 0; i < neighbours.length; i++) for (let j = i + 1; j < neighbours.length; j++) {
|
|
842
|
+
const u = neighbours[i];
|
|
843
|
+
const v = neighbours[j];
|
|
844
|
+
if (u !== void 0 && v !== void 0) {
|
|
845
|
+
if (new Set(graph.neighbours(u)).has(v) && neighbourSet.has(v)) count++;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return count;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Classify a node by triad count into core/bridge/periphery.
|
|
852
|
+
*/
|
|
853
|
+
function triadCategory(triadCount, p33, p66) {
|
|
854
|
+
if (triadCount <= p33) return "periphery";
|
|
855
|
+
if (triadCount <= p66) return "bridge";
|
|
856
|
+
return "core";
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Compute Jaccard similarity between two sets.
|
|
860
|
+
*/
|
|
861
|
+
function jaccard(a, b) {
|
|
862
|
+
const intersection = [...a].filter((x) => b.has(x)).length;
|
|
863
|
+
const union = new Set([...a, ...b]).size;
|
|
864
|
+
return union === 0 ? 0 : intersection / union;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* STRIDE — Shortest-TRIangle Diversity seed selection.
|
|
868
|
+
*
|
|
869
|
+
* Nodes are categorised by local triad count into core, bridge, and
|
|
870
|
+
* periphery. Pairs spanning different categories capture structural
|
|
871
|
+
* diversity: core-periphery pairs traverse community interiors to
|
|
872
|
+
* boundaries, bridge-bridge pairs span community gaps.
|
|
873
|
+
*
|
|
874
|
+
* @param graph - The graph to sample seeds from
|
|
875
|
+
* @param options - Configuration options
|
|
876
|
+
* @returns Selected seed pairs with triad metadata
|
|
877
|
+
*/
|
|
878
|
+
function stride(graph, options = {}) {
|
|
879
|
+
const config = {
|
|
880
|
+
...DEFAULTS$1,
|
|
881
|
+
...options
|
|
882
|
+
};
|
|
883
|
+
const rng = createRNG$1(config.rngSeed);
|
|
884
|
+
const allNodes = [...graph.nodeIds()];
|
|
885
|
+
if (allNodes.length < 2) return {
|
|
886
|
+
pairs: [],
|
|
887
|
+
triadCounts: /* @__PURE__ */ new Map(),
|
|
888
|
+
categories: /* @__PURE__ */ new Map()
|
|
889
|
+
};
|
|
890
|
+
const triadCounts = /* @__PURE__ */ new Map();
|
|
891
|
+
for (const node of allNodes) triadCounts.set(node, countTriads(graph, node));
|
|
892
|
+
const countsSorted = [...triadCounts.values()].sort((a, b) => a - b);
|
|
893
|
+
const n = countsSorted.length;
|
|
894
|
+
const p33 = countsSorted[Math.floor(n / 3)] ?? 0;
|
|
895
|
+
const p66 = countsSorted[Math.floor(2 * n / 3)] ?? 0;
|
|
896
|
+
const categories = /* @__PURE__ */ new Map();
|
|
897
|
+
const categoryGroups = /* @__PURE__ */ new Map();
|
|
898
|
+
for (const [node, tc] of triadCounts) {
|
|
899
|
+
const cat = triadCategory(tc, p33, p66);
|
|
900
|
+
categories.set(node, cat);
|
|
901
|
+
let group = categoryGroups.get(cat);
|
|
902
|
+
if (group === void 0) {
|
|
903
|
+
group = [];
|
|
904
|
+
categoryGroups.set(cat, group);
|
|
905
|
+
}
|
|
906
|
+
group.push(node);
|
|
907
|
+
}
|
|
908
|
+
const catNames = [
|
|
909
|
+
"core",
|
|
910
|
+
"bridge",
|
|
911
|
+
"periphery"
|
|
912
|
+
];
|
|
913
|
+
const candidates = [];
|
|
914
|
+
for (const catA of catNames) for (const catB of catNames) {
|
|
915
|
+
const groupA = categoryGroups.get(catA);
|
|
916
|
+
const groupB = categoryGroups.get(catB);
|
|
917
|
+
if (groupA === void 0 || groupB === void 0) continue;
|
|
918
|
+
const aLen = groupA.length;
|
|
919
|
+
const bLen = groupB.length;
|
|
920
|
+
if (aLen === 0 || bLen === 0) continue;
|
|
921
|
+
const crossBonus = catA === catB ? 0 : 1;
|
|
922
|
+
const sampleSize = Math.max(config.nPairs * 3, 30);
|
|
923
|
+
for (let s = 0; s < sampleSize; s++) {
|
|
924
|
+
const a = groupA[Math.floor(rng() * groupA.length)];
|
|
925
|
+
const b = catA === catB ? groupA[Math.floor(rng() * groupA.length)] : groupB[Math.floor(rng() * groupB.length)];
|
|
926
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
927
|
+
const tcA = triadCounts.get(a) ?? 0;
|
|
928
|
+
const tcB = triadCounts.get(b) ?? 0;
|
|
929
|
+
const score = Math.abs(tcA - tcB) + crossBonus * Math.max(tcA, tcB, 1);
|
|
930
|
+
candidates.push({
|
|
931
|
+
score,
|
|
932
|
+
a,
|
|
933
|
+
b
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
candidates.sort((x, y) => y.score - x.score);
|
|
938
|
+
const selected = [];
|
|
939
|
+
const selectedPairKeys = /* @__PURE__ */ new Set();
|
|
940
|
+
for (const candidate of candidates) {
|
|
941
|
+
const { a, b } = candidate;
|
|
942
|
+
if (selected.length >= config.nPairs) break;
|
|
943
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
944
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
945
|
+
const aNbrs = new Set(graph.neighbours(a));
|
|
946
|
+
const bNbrs = new Set(graph.neighbours(b));
|
|
947
|
+
let isDiverse = true;
|
|
948
|
+
for (const prev of selected) if (jaccard(aNbrs, new Set(graph.neighbours(prev.source.id))) >= config.diversityThreshold) {
|
|
949
|
+
if (jaccard(bNbrs, new Set(graph.neighbours(prev.target.id))) >= config.diversityThreshold) {
|
|
950
|
+
isDiverse = false;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (!isDiverse) continue;
|
|
955
|
+
selectedPairKeys.add(pairKey);
|
|
956
|
+
selected.push({
|
|
957
|
+
source: { id: a },
|
|
958
|
+
target: { id: b },
|
|
959
|
+
sourceTriads: triadCounts.get(a) ?? 0,
|
|
960
|
+
targetTriads: triadCounts.get(b) ?? 0,
|
|
961
|
+
sourceCategory: categories.get(a) ?? "periphery",
|
|
962
|
+
targetCategory: categories.get(b) ?? "periphery"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
let fillAttempts = 0;
|
|
966
|
+
while (selected.length < config.nPairs && allNodes.length >= 2 && fillAttempts < config.nPairs * 20) {
|
|
967
|
+
fillAttempts++;
|
|
968
|
+
const i1 = Math.floor(rng() * allNodes.length);
|
|
969
|
+
const i2 = Math.floor(rng() * allNodes.length);
|
|
970
|
+
const a = allNodes[i1];
|
|
971
|
+
const b = allNodes[i2];
|
|
972
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
973
|
+
const pairKey = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
974
|
+
if (selectedPairKeys.has(pairKey)) continue;
|
|
975
|
+
selectedPairKeys.add(pairKey);
|
|
976
|
+
selected.push({
|
|
977
|
+
source: { id: a },
|
|
978
|
+
target: { id: b },
|
|
979
|
+
sourceTriads: triadCounts.get(a) ?? 0,
|
|
980
|
+
targetTriads: triadCounts.get(b) ?? 0,
|
|
981
|
+
sourceCategory: categories.get(a) ?? "periphery",
|
|
982
|
+
targetCategory: categories.get(b) ?? "periphery"
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
pairs: selected.slice(0, config.nPairs),
|
|
987
|
+
triadCounts,
|
|
988
|
+
categories
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
//#endregion
|
|
286
992
|
//#region src/seeds/stratified.ts
|
|
287
993
|
/** Default values */
|
|
288
994
|
var DEFAULTS = {
|
|
@@ -393,7 +1099,11 @@ function stratified(graph, options) {
|
|
|
393
1099
|
};
|
|
394
1100
|
}
|
|
395
1101
|
//#endregion
|
|
1102
|
+
exports.crest = crest;
|
|
1103
|
+
exports.crisp = crisp;
|
|
396
1104
|
exports.grasp = grasp;
|
|
1105
|
+
exports.spine = spine;
|
|
397
1106
|
exports.stratified = stratified;
|
|
1107
|
+
exports.stride = stride;
|
|
398
1108
|
|
|
399
1109
|
//# sourceMappingURL=index.cjs.map
|