@graphty/algorithms 1.8.1 → 2.0.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 +5 -4
- package/dist/algorithms.js +518 -525
- package/dist/algorithms.js.map +1 -1
- package/dist/algorithms.standalone.js +519 -526
- package/dist/algorithms.standalone.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +13 -2
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.js +96 -73
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +35 -15
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
- package/dist/src/algorithms/community/leiden.js +214 -222
- package/dist/src/algorithms/community/leiden.js.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.d.ts +28 -72
- package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.js +249 -262
- package/dist/src/algorithms/community/louvain-optimized.js.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.d.ts +15 -7
- package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.js +38 -38
- package/dist/src/algorithms/community/modularity-utils.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +35 -8
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/errors.d.ts +21 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/algorithms/centrality/eigenvector.ts +103 -84
- package/src/algorithms/community/girvan-newman.ts +38 -17
- package/src/algorithms/community/leiden.ts +270 -275
- package/src/algorithms/community/louvain-optimized.ts +303 -313
- package/src/algorithms/community/modularity-utils.ts +42 -45
- package/src/algorithms/shortest-path/bellman-ford.ts +46 -8
- package/src/errors.ts +26 -0
- package/src/index.ts +3 -0
package/dist/algorithms.js
CHANGED
|
@@ -335,6 +335,23 @@ class Graph {
|
|
|
335
335
|
return count;
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
+
class ConvergenceError extends Error {
|
|
339
|
+
/**
|
|
340
|
+
* Builds the error and its message.
|
|
341
|
+
* @param algorithm - The function that gave up, e.g. `"eigenvectorCentrality"`
|
|
342
|
+
* @param iterations - The passes it ran, which is its `maxIterations`
|
|
343
|
+
* @param tolerance - The tolerance it did not meet
|
|
344
|
+
*/
|
|
345
|
+
constructor(algorithm, iterations, tolerance) {
|
|
346
|
+
super(
|
|
347
|
+
`${algorithm} did not converge in ${String(iterations)} iterations (tolerance ${String(tolerance)}); raise maxIterations or tolerance`
|
|
348
|
+
);
|
|
349
|
+
this.algorithm = algorithm;
|
|
350
|
+
this.iterations = iterations;
|
|
351
|
+
this.tolerance = tolerance;
|
|
352
|
+
this.name = "ConvergenceError";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
338
355
|
var lib = {};
|
|
339
356
|
var SparseTypedFastBitSet = {};
|
|
340
357
|
var utils = {};
|
|
@@ -3649,6 +3666,17 @@ function createTransposeGraph(graph) {
|
|
|
3649
3666
|
}
|
|
3650
3667
|
return transpose;
|
|
3651
3668
|
}
|
|
3669
|
+
function relaxableArcs(graph) {
|
|
3670
|
+
const arcs = [];
|
|
3671
|
+
for (const edge of graph.edges()) {
|
|
3672
|
+
const weight = edge.weight ?? 1;
|
|
3673
|
+
arcs.push({ from: edge.source, to: edge.target, weight });
|
|
3674
|
+
if (!graph.isDirected) {
|
|
3675
|
+
arcs.push({ from: edge.target, to: edge.source, weight });
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
return arcs;
|
|
3679
|
+
}
|
|
3652
3680
|
function bellmanFord(graph, source, options = {}) {
|
|
3653
3681
|
if (!graph.hasNode(source)) {
|
|
3654
3682
|
throw new Error(`Source node ${String(source)} not found in graph`);
|
|
@@ -3656,16 +3684,17 @@ function bellmanFord(graph, source, options = {}) {
|
|
|
3656
3684
|
const distances = /* @__PURE__ */ new Map();
|
|
3657
3685
|
const predecessors = /* @__PURE__ */ new Map();
|
|
3658
3686
|
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
3687
|
+
const arcs = relaxableArcs(graph);
|
|
3659
3688
|
for (const nodeId of nodes) {
|
|
3660
3689
|
distances.set(nodeId, nodeId === source ? 0 : Infinity);
|
|
3661
3690
|
predecessors.set(nodeId, null);
|
|
3662
3691
|
}
|
|
3663
3692
|
for (let i = 0; i < nodes.length - 1; i++) {
|
|
3664
3693
|
let updated = false;
|
|
3665
|
-
for (const
|
|
3666
|
-
const u =
|
|
3667
|
-
const v =
|
|
3668
|
-
const weight =
|
|
3694
|
+
for (const arc of arcs) {
|
|
3695
|
+
const u = arc.from;
|
|
3696
|
+
const v = arc.to;
|
|
3697
|
+
const { weight } = arc;
|
|
3669
3698
|
const distanceU = distances.get(u);
|
|
3670
3699
|
const distanceV = distances.get(v);
|
|
3671
3700
|
if (distanceU !== void 0 && distanceV !== void 0 && distanceU !== Infinity) {
|
|
@@ -3686,10 +3715,10 @@ function bellmanFord(graph, source, options = {}) {
|
|
|
3686
3715
|
}
|
|
3687
3716
|
const negativeCycleNodes = [];
|
|
3688
3717
|
let hasNegativeCycle2 = false;
|
|
3689
|
-
for (const
|
|
3690
|
-
const u =
|
|
3691
|
-
const v =
|
|
3692
|
-
const weight =
|
|
3718
|
+
for (const arc of arcs) {
|
|
3719
|
+
const u = arc.from;
|
|
3720
|
+
const v = arc.to;
|
|
3721
|
+
const { weight } = arc;
|
|
3693
3722
|
const distanceU = distances.get(u);
|
|
3694
3723
|
const distanceV = distances.get(v);
|
|
3695
3724
|
if (distanceU !== void 0 && distanceV !== void 0 && distanceU !== Infinity) {
|
|
@@ -5225,88 +5254,100 @@ class PriorityDeltaPageRank {
|
|
|
5225
5254
|
}
|
|
5226
5255
|
function eigenvectorCentrality(graph, options = {}) {
|
|
5227
5256
|
const { maxIterations = 100, tolerance = 1e-6, normalized = true, startVector } = options;
|
|
5257
|
+
const nodeIds = Array.from(graph.nodes(), (node) => node.id);
|
|
5258
|
+
const keys = nodeIds.map((id) => id.toString());
|
|
5259
|
+
const n = keys.length;
|
|
5228
5260
|
const centrality = {};
|
|
5229
|
-
|
|
5230
|
-
const nodeIds = nodes.map((node) => node.id);
|
|
5231
|
-
if (nodeIds.length === 0) {
|
|
5261
|
+
if (n === 0) {
|
|
5232
5262
|
return centrality;
|
|
5233
5263
|
}
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
if (
|
|
5237
|
-
for (const
|
|
5238
|
-
|
|
5239
|
-
currentVector.set(key, startVector.get(key) ?? 1 / Math.sqrt(nodeIds.length));
|
|
5240
|
-
}
|
|
5241
|
-
} else {
|
|
5242
|
-
const initialValue = 1 / Math.sqrt(nodeIds.length);
|
|
5243
|
-
for (const nodeId of nodeIds) {
|
|
5244
|
-
currentVector.set(nodeId.toString(), initialValue);
|
|
5264
|
+
const index2 = new Map(keys.map((key, i) => [key, i]));
|
|
5265
|
+
const adjacency = nodeIds.map((id) => Array.from(graph.neighbors(id), (m) => index2.get(m.toString()) ?? 0));
|
|
5266
|
+
if (isNilpotent(adjacency)) {
|
|
5267
|
+
for (const key of keys) {
|
|
5268
|
+
centrality[key] = 0;
|
|
5245
5269
|
}
|
|
5270
|
+
return centrality;
|
|
5246
5271
|
}
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
}
|
|
5260
|
-
let norm = 0;
|
|
5261
|
-
for (const value of Array.from(currentVector.values())) {
|
|
5262
|
-
norm += value * value;
|
|
5263
|
-
}
|
|
5264
|
-
norm = Math.sqrt(norm);
|
|
5265
|
-
if (norm === 0) {
|
|
5266
|
-
for (const nodeId of nodeIds) {
|
|
5267
|
-
centrality[nodeId.toString()] = 0;
|
|
5272
|
+
let x = new Float64Array(n);
|
|
5273
|
+
for (let i = 0; i < n; i++) {
|
|
5274
|
+
x[i] = adjacency[i]?.length ? startVector?.get(keys[i] ?? "") ?? 1 : 0;
|
|
5275
|
+
}
|
|
5276
|
+
scaleToUnitLength(x);
|
|
5277
|
+
let next = new Float64Array(n);
|
|
5278
|
+
let converged = false;
|
|
5279
|
+
for (let iteration = 0; iteration < maxIterations && !converged; iteration++) {
|
|
5280
|
+
for (let i = 0; i < n; i++) {
|
|
5281
|
+
let sum = x[i] ?? 0;
|
|
5282
|
+
for (const j of adjacency[i] ?? []) {
|
|
5283
|
+
sum += x[j] ?? 0;
|
|
5268
5284
|
}
|
|
5269
|
-
|
|
5270
|
-
}
|
|
5271
|
-
for (const [nodeId, value] of Array.from(currentVector)) {
|
|
5272
|
-
currentVector.set(nodeId, value / norm);
|
|
5273
|
-
}
|
|
5274
|
-
let maxDiff = 0;
|
|
5275
|
-
for (const [nodeId, value] of Array.from(currentVector)) {
|
|
5276
|
-
const prevValue = previousVector.get(nodeId) ?? 0;
|
|
5277
|
-
const diff = Math.abs(value - prevValue);
|
|
5278
|
-
maxDiff = Math.max(maxDiff, diff);
|
|
5285
|
+
next[i] = sum;
|
|
5279
5286
|
}
|
|
5280
|
-
|
|
5281
|
-
|
|
5287
|
+
scaleToUnitLength(next);
|
|
5288
|
+
let change = 0;
|
|
5289
|
+
for (let i = 0; i < n; i++) {
|
|
5290
|
+
change += Math.abs((next[i] ?? 0) - (x[i] ?? 0));
|
|
5282
5291
|
}
|
|
5292
|
+
[x, next] = [next, x];
|
|
5293
|
+
converged = change < n * tolerance;
|
|
5283
5294
|
}
|
|
5284
|
-
|
|
5285
|
-
|
|
5295
|
+
if (!converged) {
|
|
5296
|
+
throw new ConvergenceError("eigenvectorCentrality", maxIterations, tolerance);
|
|
5297
|
+
}
|
|
5298
|
+
for (let i = 0; i < n; i++) {
|
|
5299
|
+
centrality[keys[i] ?? ""] = x[i] ?? 0;
|
|
5286
5300
|
}
|
|
5287
5301
|
if (normalized) {
|
|
5288
5302
|
let maxValue = 0;
|
|
5289
5303
|
let minValue = Number.POSITIVE_INFINITY;
|
|
5290
|
-
for (const value of
|
|
5304
|
+
for (const value of x) {
|
|
5291
5305
|
maxValue = Math.max(maxValue, value);
|
|
5292
5306
|
minValue = Math.min(minValue, value);
|
|
5293
5307
|
}
|
|
5294
5308
|
const range = maxValue - minValue;
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
if (centralityValue !== void 0) {
|
|
5299
|
-
centrality[nodeId] = (centralityValue - minValue) / range;
|
|
5300
|
-
}
|
|
5301
|
-
}
|
|
5302
|
-
} else {
|
|
5303
|
-
for (const nodeId of Object.keys(centrality)) {
|
|
5304
|
-
centrality[nodeId] = maxValue > 0 ? 1 : 0;
|
|
5305
|
-
}
|
|
5309
|
+
const flat = maxValue > 0 ? 1 : 0;
|
|
5310
|
+
for (const key of keys) {
|
|
5311
|
+
centrality[key] = range > 0 ? ((centrality[key] ?? 0) - minValue) / range : flat;
|
|
5306
5312
|
}
|
|
5307
5313
|
}
|
|
5308
5314
|
return centrality;
|
|
5309
5315
|
}
|
|
5316
|
+
function isNilpotent(adjacency) {
|
|
5317
|
+
const inDegree = new Int32Array(adjacency.length);
|
|
5318
|
+
for (const targets of adjacency) {
|
|
5319
|
+
for (const j of targets) {
|
|
5320
|
+
inDegree[j] = (inDegree[j] ?? 0) + 1;
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
const queue = [];
|
|
5324
|
+
inDegree.forEach((degree, i) => {
|
|
5325
|
+
if (degree === 0) {
|
|
5326
|
+
queue.push(i);
|
|
5327
|
+
}
|
|
5328
|
+
});
|
|
5329
|
+
for (let head = 0; head < queue.length; head++) {
|
|
5330
|
+
for (const j of adjacency[queue[head] ?? 0] ?? []) {
|
|
5331
|
+
inDegree[j] = (inDegree[j] ?? 0) - 1;
|
|
5332
|
+
if (inDegree[j] === 0) {
|
|
5333
|
+
queue.push(j);
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
return queue.length === adjacency.length;
|
|
5338
|
+
}
|
|
5339
|
+
function scaleToUnitLength(v) {
|
|
5340
|
+
let norm = 0;
|
|
5341
|
+
for (const value of v) {
|
|
5342
|
+
norm += value * value;
|
|
5343
|
+
}
|
|
5344
|
+
norm = Math.sqrt(norm);
|
|
5345
|
+
if (norm > 0) {
|
|
5346
|
+
for (let i = 0; i < v.length; i++) {
|
|
5347
|
+
v[i] = (v[i] ?? 0) / norm;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5310
5351
|
function nodeEigenvectorCentrality(graph, nodeId, options = {}) {
|
|
5311
5352
|
if (!graph.hasNode(nodeId)) {
|
|
5312
5353
|
throw new Error(`Node ${String(nodeId)} not found in graph`);
|
|
@@ -6559,25 +6600,29 @@ function calculateModularity$3(graph, communityMap) {
|
|
|
6559
6600
|
if (totalEdgeWeight === 0) {
|
|
6560
6601
|
return 0;
|
|
6561
6602
|
}
|
|
6562
|
-
|
|
6563
|
-
const degrees = /* @__PURE__ */ new Map();
|
|
6603
|
+
const degreeSum = /* @__PURE__ */ new Map();
|
|
6564
6604
|
for (const node of graph.nodes()) {
|
|
6565
|
-
|
|
6605
|
+
const community = communityMap.get(node.id);
|
|
6606
|
+
if (community === void 0) {
|
|
6607
|
+
continue;
|
|
6608
|
+
}
|
|
6609
|
+
degreeSum.set(community, (degreeSum.get(community) ?? 0) + getNodeDegree$1(graph, node.id));
|
|
6566
6610
|
}
|
|
6611
|
+
const internalWeight = /* @__PURE__ */ new Map();
|
|
6567
6612
|
for (const edge of graph.edges()) {
|
|
6568
6613
|
const communityI = communityMap.get(edge.source);
|
|
6569
6614
|
const communityJ = communityMap.get(edge.target);
|
|
6570
|
-
if (communityI === communityJ) {
|
|
6571
|
-
|
|
6572
|
-
const degreeI = degrees.get(edge.source);
|
|
6573
|
-
const degreeJ = degrees.get(edge.target);
|
|
6574
|
-
if (degreeI === void 0 || degreeJ === void 0) {
|
|
6575
|
-
continue;
|
|
6576
|
-
}
|
|
6577
|
-
modularity += edgeWeight - degreeI * degreeJ / (2 * totalEdgeWeight);
|
|
6615
|
+
if (communityI === void 0 || communityI !== communityJ) {
|
|
6616
|
+
continue;
|
|
6578
6617
|
}
|
|
6618
|
+
internalWeight.set(communityI, (internalWeight.get(communityI) ?? 0) + (edge.weight ?? 1));
|
|
6619
|
+
}
|
|
6620
|
+
let modularity = 0;
|
|
6621
|
+
for (const [community, degrees] of degreeSum) {
|
|
6622
|
+
const share = degrees / (2 * totalEdgeWeight);
|
|
6623
|
+
modularity += (internalWeight.get(community) ?? 0) / totalEdgeWeight - share * share;
|
|
6579
6624
|
}
|
|
6580
|
-
return modularity
|
|
6625
|
+
return modularity;
|
|
6581
6626
|
}
|
|
6582
6627
|
function getTotalEdgeWeight$1(graph) {
|
|
6583
6628
|
let totalWeight = 0;
|
|
@@ -6921,6 +6966,7 @@ function labelPropagationSemiSupervised(graph, seedLabels, options = {}) {
|
|
|
6921
6966
|
const graphMap = graphToMap(graph);
|
|
6922
6967
|
return labelPropagationSemiSupervisedImpl(graphMap, seedLabels, options);
|
|
6923
6968
|
}
|
|
6969
|
+
const LOCAL_MOVE_PASSES = 50;
|
|
6924
6970
|
function leidenImpl(inputGraph, options = {}) {
|
|
6925
6971
|
const { resolution = 1, randomSeed = 42, maxIterations = 100, threshold = 1e-7 } = options;
|
|
6926
6972
|
if (inputGraph.size === 0) {
|
|
@@ -6930,103 +6976,70 @@ function leidenImpl(inputGraph, options = {}) {
|
|
|
6930
6976
|
iterations: 0
|
|
6931
6977
|
};
|
|
6932
6978
|
}
|
|
6933
|
-
let currentGraph = inputGraph;
|
|
6934
6979
|
const random = SeededRandom.createGenerator(randomSeed);
|
|
6935
|
-
|
|
6936
|
-
const
|
|
6937
|
-
for (const
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
degrees.set(node, degree);
|
|
6944
|
-
}
|
|
6945
|
-
totalWeight /= 2;
|
|
6946
|
-
const communities = /* @__PURE__ */ new Map();
|
|
6947
|
-
const nodes = Array.from(currentGraph.keys());
|
|
6948
|
-
nodes.forEach((node, i) => communities.set(node, i));
|
|
6949
|
-
let modularity = calculateModularity$2(currentGraph, communities, degrees, totalWeight, resolution);
|
|
6950
|
-
let bestModularity = modularity;
|
|
6951
|
-
let bestCommunities = new Map(communities);
|
|
6980
|
+
const base = weighLevel(inputGraph);
|
|
6981
|
+
const placement = /* @__PURE__ */ new Map();
|
|
6982
|
+
for (const node of inputGraph.keys()) {
|
|
6983
|
+
placement.set(node, node);
|
|
6984
|
+
}
|
|
6985
|
+
let bestCommunities = singletonCommunities(inputGraph);
|
|
6986
|
+
let bestModularity = calculateModularity$2(inputGraph, bestCommunities, base.degrees, base.totalWeight, resolution);
|
|
6987
|
+
let levelGraph = inputGraph;
|
|
6952
6988
|
let iterations = 0;
|
|
6989
|
+
let polishing = false;
|
|
6953
6990
|
while (iterations < maxIterations) {
|
|
6954
6991
|
iterations++;
|
|
6955
|
-
|
|
6956
|
-
const
|
|
6957
|
-
|
|
6958
|
-
for (const node of
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
const
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
for (const [community] of neighborCommunities) {
|
|
6967
|
-
if (community === currentCommunity) {
|
|
6968
|
-
continue;
|
|
6969
|
-
}
|
|
6970
|
-
const gain = calculateModularityGain(
|
|
6971
|
-
node,
|
|
6972
|
-
community,
|
|
6973
|
-
currentGraph,
|
|
6974
|
-
communities,
|
|
6975
|
-
degrees,
|
|
6976
|
-
totalWeight,
|
|
6977
|
-
resolution
|
|
6978
|
-
);
|
|
6979
|
-
if (gain > bestGain) {
|
|
6980
|
-
bestGain = gain;
|
|
6981
|
-
bestCommunity = community;
|
|
6982
|
-
}
|
|
6983
|
-
}
|
|
6984
|
-
if (bestCommunity !== currentCommunity) {
|
|
6985
|
-
communities.set(node, bestCommunity);
|
|
6986
|
-
modularity += bestGain;
|
|
6987
|
-
improved = true;
|
|
6992
|
+
const weights = weighLevel(levelGraph);
|
|
6993
|
+
const communities = polishing ? new Map(bestCommunities) : singletonCommunities(levelGraph);
|
|
6994
|
+
const moved = moveNodesLocally(levelGraph, weights, communities, random, resolution, LOCAL_MOVE_PASSES);
|
|
6995
|
+
for (const [node, piece] of refinePartition(levelGraph, communities)) {
|
|
6996
|
+
communities.set(node, piece);
|
|
6997
|
+
}
|
|
6998
|
+
const candidate = /* @__PURE__ */ new Map();
|
|
6999
|
+
for (const [original, levelNode] of placement) {
|
|
7000
|
+
const community = communities.get(levelNode);
|
|
7001
|
+
if (community !== void 0) {
|
|
7002
|
+
candidate.set(original, community);
|
|
6988
7003
|
}
|
|
6989
7004
|
}
|
|
6990
|
-
|
|
6991
|
-
const
|
|
6992
|
-
|
|
6993
|
-
communities.set(node, newCommunity);
|
|
6994
|
-
}
|
|
6995
|
-
modularity = calculateModularity$2(currentGraph, communities, degrees, totalWeight, resolution);
|
|
6996
|
-
if (modularity > bestModularity + threshold) {
|
|
7005
|
+
const modularity = calculateModularity$2(inputGraph, candidate, base.degrees, base.totalWeight, resolution);
|
|
7006
|
+
const improved = modularity > bestModularity + threshold;
|
|
7007
|
+
if (modularity > bestModularity) {
|
|
6997
7008
|
bestModularity = modularity;
|
|
6998
|
-
bestCommunities =
|
|
6999
|
-
improved = true;
|
|
7000
|
-
}
|
|
7001
|
-
if (!improved) {
|
|
7002
|
-
break;
|
|
7009
|
+
bestCommunities = candidate;
|
|
7003
7010
|
}
|
|
7004
|
-
const
|
|
7005
|
-
|
|
7006
|
-
|
|
7011
|
+
const distinct = new Set(communities.values()).size;
|
|
7012
|
+
const settled = polishing ? !improved : !improved || !moved || distinct === levelGraph.size;
|
|
7013
|
+
if (settled) {
|
|
7014
|
+
if (polishing) {
|
|
7015
|
+
break;
|
|
7016
|
+
}
|
|
7017
|
+
polishing = true;
|
|
7018
|
+
levelGraph = inputGraph;
|
|
7019
|
+
for (const node of inputGraph.keys()) {
|
|
7020
|
+
placement.set(node, node);
|
|
7021
|
+
}
|
|
7022
|
+
continue;
|
|
7007
7023
|
}
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7024
|
+
polishing = false;
|
|
7025
|
+
const aggregated = aggregateLevel(levelGraph, communities);
|
|
7026
|
+
for (const [original, levelNode] of placement) {
|
|
7027
|
+
const superNode = aggregated.mapping.get(levelNode);
|
|
7028
|
+
if (superNode !== void 0) {
|
|
7029
|
+
placement.set(original, superNode);
|
|
7030
|
+
}
|
|
7014
7031
|
}
|
|
7032
|
+
levelGraph = aggregated.graph;
|
|
7015
7033
|
}
|
|
7034
|
+
const renumbered = /* @__PURE__ */ new Map();
|
|
7016
7035
|
const finalCommunities = /* @__PURE__ */ new Map();
|
|
7017
7036
|
for (const [node, community] of bestCommunities) {
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
for (const community of new Set(finalCommunities.values())) {
|
|
7023
|
-
communityRenumber.set(community, newId++);
|
|
7024
|
-
}
|
|
7025
|
-
for (const [node, community] of finalCommunities) {
|
|
7026
|
-
const newCommunityId = communityRenumber.get(community);
|
|
7027
|
-
if (newCommunityId !== void 0) {
|
|
7028
|
-
finalCommunities.set(node, newCommunityId);
|
|
7037
|
+
let id = renumbered.get(community);
|
|
7038
|
+
if (id === void 0) {
|
|
7039
|
+
id = renumbered.size;
|
|
7040
|
+
renumbered.set(community, id);
|
|
7029
7041
|
}
|
|
7042
|
+
finalCommunities.set(node, id);
|
|
7030
7043
|
}
|
|
7031
7044
|
return {
|
|
7032
7045
|
communities: finalCommunities,
|
|
@@ -7078,75 +7091,6 @@ function getNeighborCommunities$1(node, graph, communities) {
|
|
|
7078
7091
|
}
|
|
7079
7092
|
return neighborCommunities;
|
|
7080
7093
|
}
|
|
7081
|
-
function calculateModularityGain(node, targetCommunity, graph, communities, degrees, totalWeight, resolution) {
|
|
7082
|
-
const currentCommunity = communities.get(node);
|
|
7083
|
-
const nodeDegree = degrees.get(node);
|
|
7084
|
-
if (currentCommunity === void 0 || nodeDegree === void 0) {
|
|
7085
|
-
return 0;
|
|
7086
|
-
}
|
|
7087
|
-
let weightToTarget = 0;
|
|
7088
|
-
let weightToCurrent = 0;
|
|
7089
|
-
const neighbors = graph.get(node);
|
|
7090
|
-
if (neighbors) {
|
|
7091
|
-
for (const [neighbor, weight] of neighbors) {
|
|
7092
|
-
const neighborCommunity = communities.get(neighbor);
|
|
7093
|
-
if (neighborCommunity === void 0) {
|
|
7094
|
-
continue;
|
|
7095
|
-
}
|
|
7096
|
-
if (neighborCommunity === targetCommunity) {
|
|
7097
|
-
weightToTarget += weight;
|
|
7098
|
-
} else if (neighborCommunity === currentCommunity && neighbor !== node) {
|
|
7099
|
-
weightToCurrent += weight;
|
|
7100
|
-
}
|
|
7101
|
-
}
|
|
7102
|
-
}
|
|
7103
|
-
let targetDegree = 0;
|
|
7104
|
-
let currentDegree = 0;
|
|
7105
|
-
for (const [n, c] of communities) {
|
|
7106
|
-
if (c === targetCommunity && n !== node) {
|
|
7107
|
-
const deg = degrees.get(n);
|
|
7108
|
-
if (deg !== void 0) {
|
|
7109
|
-
targetDegree += deg;
|
|
7110
|
-
}
|
|
7111
|
-
} else if (c === currentCommunity && n !== node) {
|
|
7112
|
-
const deg = degrees.get(n);
|
|
7113
|
-
if (deg !== void 0) {
|
|
7114
|
-
currentDegree += deg;
|
|
7115
|
-
}
|
|
7116
|
-
}
|
|
7117
|
-
}
|
|
7118
|
-
const m2 = 2 * totalWeight;
|
|
7119
|
-
const gain = (weightToTarget - weightToCurrent) / totalWeight - resolution * nodeDegree * (targetDegree - currentDegree) / (m2 * m2);
|
|
7120
|
-
return gain;
|
|
7121
|
-
}
|
|
7122
|
-
function createAggregateNetwork(graph, communities) {
|
|
7123
|
-
const aggregateGraph = /* @__PURE__ */ new Map();
|
|
7124
|
-
const nodeMapping = /* @__PURE__ */ new Map();
|
|
7125
|
-
for (const [node, community] of communities) {
|
|
7126
|
-
nodeMapping.set(node, community);
|
|
7127
|
-
if (!aggregateGraph.has(community)) {
|
|
7128
|
-
aggregateGraph.set(community, /* @__PURE__ */ new Map());
|
|
7129
|
-
}
|
|
7130
|
-
}
|
|
7131
|
-
for (const [node, neighbors] of graph) {
|
|
7132
|
-
const sourceCommunity = communities.get(node);
|
|
7133
|
-
if (sourceCommunity === void 0) {
|
|
7134
|
-
continue;
|
|
7135
|
-
}
|
|
7136
|
-
for (const [neighbor, weight] of neighbors) {
|
|
7137
|
-
const targetCommunity = communities.get(neighbor);
|
|
7138
|
-
if (targetCommunity === void 0) {
|
|
7139
|
-
continue;
|
|
7140
|
-
}
|
|
7141
|
-
const sourceNeighbors = aggregateGraph.get(sourceCommunity);
|
|
7142
|
-
if (sourceNeighbors) {
|
|
7143
|
-
const current = sourceNeighbors.get(targetCommunity) ?? 0;
|
|
7144
|
-
sourceNeighbors.set(targetCommunity, current + weight);
|
|
7145
|
-
}
|
|
7146
|
-
}
|
|
7147
|
-
}
|
|
7148
|
-
return { aggregateGraph, nodeMapping };
|
|
7149
|
-
}
|
|
7150
7094
|
function refinePartition(originalGraph, communities) {
|
|
7151
7095
|
const refined = /* @__PURE__ */ new Map();
|
|
7152
7096
|
const communityNodes = /* @__PURE__ */ new Map();
|
|
@@ -7221,46 +7165,118 @@ function findConnectedComponents(graph) {
|
|
|
7221
7165
|
}
|
|
7222
7166
|
return components;
|
|
7223
7167
|
}
|
|
7224
|
-
function
|
|
7168
|
+
function weighLevel(graph) {
|
|
7169
|
+
const degrees = /* @__PURE__ */ new Map();
|
|
7170
|
+
let summed = 0;
|
|
7171
|
+
for (const [node, neighbors] of graph) {
|
|
7172
|
+
let degree = 0;
|
|
7173
|
+
for (const weight of neighbors.values()) {
|
|
7174
|
+
degree += weight;
|
|
7175
|
+
}
|
|
7176
|
+
degrees.set(node, degree);
|
|
7177
|
+
summed += degree;
|
|
7178
|
+
}
|
|
7179
|
+
return { degrees, totalWeight: summed / 2 };
|
|
7180
|
+
}
|
|
7181
|
+
function singletonCommunities(graph) {
|
|
7182
|
+
const communities = /* @__PURE__ */ new Map();
|
|
7183
|
+
let next = 0;
|
|
7184
|
+
for (const node of graph.keys()) {
|
|
7185
|
+
communities.set(node, next++);
|
|
7186
|
+
}
|
|
7187
|
+
return communities;
|
|
7188
|
+
}
|
|
7189
|
+
function communityDegrees(communities, degrees) {
|
|
7190
|
+
const totals = /* @__PURE__ */ new Map();
|
|
7191
|
+
for (const [node, community] of communities) {
|
|
7192
|
+
totals.set(community, (totals.get(community) ?? 0) + (degrees.get(node) ?? 0));
|
|
7193
|
+
}
|
|
7194
|
+
return totals;
|
|
7195
|
+
}
|
|
7196
|
+
function moveNodesLocally(graph, weights, communities, random, resolution, maxPasses) {
|
|
7197
|
+
const { degrees, totalWeight } = weights;
|
|
7198
|
+
if (totalWeight === 0) {
|
|
7199
|
+
return false;
|
|
7200
|
+
}
|
|
7201
|
+
const totals = communityDegrees(communities, degrees);
|
|
7202
|
+
const nodes = [...graph.keys()];
|
|
7203
|
+
let movedEver = false;
|
|
7204
|
+
let nextEmpty = 0;
|
|
7205
|
+
for (const community of communities.values()) {
|
|
7206
|
+
nextEmpty = Math.max(nextEmpty, community + 1);
|
|
7207
|
+
}
|
|
7208
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
7209
|
+
const order = [...nodes];
|
|
7210
|
+
shuffle(order, random);
|
|
7211
|
+
let movedThisPass = false;
|
|
7212
|
+
for (const node of order) {
|
|
7213
|
+
const current = communities.get(node);
|
|
7214
|
+
const degree = degrees.get(node);
|
|
7215
|
+
if (current === void 0 || degree === void 0) {
|
|
7216
|
+
continue;
|
|
7217
|
+
}
|
|
7218
|
+
const neighborCommunities = getNeighborCommunities$1(node, graph, communities);
|
|
7219
|
+
const selfLoop = graph.get(node)?.get(node) ?? 0;
|
|
7220
|
+
const weightToCurrent = (neighborCommunities.get(current) ?? 0) - selfLoop;
|
|
7221
|
+
const degreeOfCurrent = (totals.get(current) ?? 0) - degree;
|
|
7222
|
+
let bestCommunity = current;
|
|
7223
|
+
let bestGain = 0;
|
|
7224
|
+
if (degreeOfCurrent > 0) {
|
|
7225
|
+
const alone = -weightToCurrent / totalWeight + resolution * degree * degreeOfCurrent / (2 * totalWeight * totalWeight);
|
|
7226
|
+
if (alone > bestGain) {
|
|
7227
|
+
bestGain = alone;
|
|
7228
|
+
bestCommunity = nextEmpty;
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
for (const [community, weightToTarget] of neighborCommunities) {
|
|
7232
|
+
if (community === current) {
|
|
7233
|
+
continue;
|
|
7234
|
+
}
|
|
7235
|
+
const gain = (weightToTarget - weightToCurrent) / totalWeight - resolution * degree * ((totals.get(community) ?? 0) - degreeOfCurrent) / (2 * totalWeight * totalWeight);
|
|
7236
|
+
if (gain > bestGain) {
|
|
7237
|
+
bestGain = gain;
|
|
7238
|
+
bestCommunity = community;
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7241
|
+
if (bestCommunity !== current) {
|
|
7242
|
+
if (bestCommunity === nextEmpty) {
|
|
7243
|
+
nextEmpty++;
|
|
7244
|
+
}
|
|
7245
|
+
communities.set(node, bestCommunity);
|
|
7246
|
+
totals.set(current, (totals.get(current) ?? 0) - degree);
|
|
7247
|
+
totals.set(bestCommunity, (totals.get(bestCommunity) ?? 0) + degree);
|
|
7248
|
+
movedThisPass = true;
|
|
7249
|
+
movedEver = true;
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
if (!movedThisPass) {
|
|
7253
|
+
break;
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
return movedEver;
|
|
7257
|
+
}
|
|
7258
|
+
function aggregateLevel(graph, communities) {
|
|
7225
7259
|
const aggregated = /* @__PURE__ */ new Map();
|
|
7226
7260
|
const mapping = /* @__PURE__ */ new Map();
|
|
7227
|
-
const communityNodes = /* @__PURE__ */ new Map();
|
|
7228
|
-
for (const community of new Set(communities.values())) {
|
|
7229
|
-
const superNode = `super_${String(community)}`;
|
|
7230
|
-
communityNodes.set(community, superNode);
|
|
7231
|
-
aggregated.set(superNode, /* @__PURE__ */ new Map());
|
|
7232
|
-
}
|
|
7233
7261
|
for (const [node, community] of communities) {
|
|
7234
|
-
const superNode =
|
|
7235
|
-
|
|
7236
|
-
|
|
7262
|
+
const superNode = `c${String(community)}`;
|
|
7263
|
+
mapping.set(node, superNode);
|
|
7264
|
+
if (!aggregated.has(superNode)) {
|
|
7265
|
+
aggregated.set(superNode, /* @__PURE__ */ new Map());
|
|
7237
7266
|
}
|
|
7238
7267
|
}
|
|
7239
7268
|
for (const [node, neighbors] of graph) {
|
|
7240
|
-
const
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
}
|
|
7244
|
-
const sourceSuper = communityNodes.get(sourceCommunity);
|
|
7245
|
-
if (sourceSuper === void 0) {
|
|
7269
|
+
const source = mapping.get(node);
|
|
7270
|
+
const row = source === void 0 ? void 0 : aggregated.get(source);
|
|
7271
|
+
if (row === void 0) {
|
|
7246
7272
|
continue;
|
|
7247
7273
|
}
|
|
7248
7274
|
for (const [neighbor, weight] of neighbors) {
|
|
7249
|
-
const
|
|
7250
|
-
if (
|
|
7251
|
-
continue;
|
|
7252
|
-
}
|
|
7253
|
-
const targetSuper = communityNodes.get(targetCommunity);
|
|
7254
|
-
if (targetSuper === void 0) {
|
|
7275
|
+
const target = mapping.get(neighbor);
|
|
7276
|
+
if (target === void 0) {
|
|
7255
7277
|
continue;
|
|
7256
7278
|
}
|
|
7257
|
-
|
|
7258
|
-
const sourceNeighbors = aggregated.get(sourceSuper);
|
|
7259
|
-
if (sourceNeighbors) {
|
|
7260
|
-
const current = sourceNeighbors.get(targetSuper) ?? 0;
|
|
7261
|
-
sourceNeighbors.set(targetSuper, current + weight);
|
|
7262
|
-
}
|
|
7263
|
-
}
|
|
7279
|
+
row.set(target, (row.get(target) ?? 0) + weight);
|
|
7264
7280
|
}
|
|
7265
7281
|
}
|
|
7266
7282
|
return { graph: aggregated, mapping };
|
|
@@ -7276,11 +7292,6 @@ class OptimizedLouvain {
|
|
|
7276
7292
|
*/
|
|
7277
7293
|
constructor(graph) {
|
|
7278
7294
|
this.graph = graph;
|
|
7279
|
-
this.communities = /* @__PURE__ */ new Map();
|
|
7280
|
-
this.communityWeights = /* @__PURE__ */ new Map();
|
|
7281
|
-
this.nodeWeights = /* @__PURE__ */ new Map();
|
|
7282
|
-
this.nodeDegrees = /* @__PURE__ */ new Map();
|
|
7283
|
-
this.totalWeight = 0;
|
|
7284
7295
|
this.pruningStats = {
|
|
7285
7296
|
leafNodesPruned: 0,
|
|
7286
7297
|
lowDegreeNodesPruned: 0,
|
|
@@ -7290,278 +7301,266 @@ class OptimizedLouvain {
|
|
|
7290
7301
|
/**
|
|
7291
7302
|
* Run optimized Louvain algorithm
|
|
7292
7303
|
* @param options - Algorithm configuration options
|
|
7293
|
-
* @returns Community detection result with communities, modularity, and iterations
|
|
7304
|
+
* @returns Community detection result with communities, modularity, and iterations (levels)
|
|
7294
7305
|
*/
|
|
7295
7306
|
detectCommunities(options = {}) {
|
|
7296
|
-
const {
|
|
7297
|
-
resolution
|
|
7298
|
-
maxIterations
|
|
7299
|
-
tolerance
|
|
7300
|
-
pruneLeaves
|
|
7301
|
-
importanceOrdering
|
|
7302
|
-
pruningThreshold
|
|
7303
|
-
thresholdCycling
|
|
7304
|
-
}
|
|
7305
|
-
this.
|
|
7306
|
-
|
|
7307
|
-
let
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
});
|
|
7317
|
-
if (improved) {
|
|
7318
|
-
const newModularity = this.calculateModularity(resolution);
|
|
7319
|
-
if (Math.abs(newModularity - modularity) < tolerance) {
|
|
7320
|
-
break;
|
|
7321
|
-
}
|
|
7322
|
-
modularity = newModularity;
|
|
7323
|
-
iteration++;
|
|
7324
|
-
}
|
|
7325
|
-
}
|
|
7326
|
-
const communityGroups = /* @__PURE__ */ new Map();
|
|
7327
|
-
for (const [nodeId, community] of this.communities) {
|
|
7328
|
-
if (!communityGroups.has(community)) {
|
|
7329
|
-
communityGroups.set(community, []);
|
|
7307
|
+
const settings = {
|
|
7308
|
+
resolution: options.resolution ?? 1,
|
|
7309
|
+
maxIterations: options.maxIterations ?? 100,
|
|
7310
|
+
tolerance: options.tolerance ?? 1e-6,
|
|
7311
|
+
pruneLeaves: options.pruneLeaves ?? true,
|
|
7312
|
+
importanceOrdering: options.importanceOrdering ?? true,
|
|
7313
|
+
pruningThreshold: options.pruningThreshold ?? 0.01,
|
|
7314
|
+
thresholdCycling: options.thresholdCycling ?? true
|
|
7315
|
+
};
|
|
7316
|
+
const { level: base, ids } = this.buildLevel();
|
|
7317
|
+
const membership = new Int32Array(base.n);
|
|
7318
|
+
for (let i = 0; i < base.n; i++) {
|
|
7319
|
+
membership[i] = i;
|
|
7320
|
+
}
|
|
7321
|
+
let level = base;
|
|
7322
|
+
let iterations = 0;
|
|
7323
|
+
while (level.n > 1) {
|
|
7324
|
+
const { community, count, moved } = this.localMoving(level, settings);
|
|
7325
|
+
if (!moved) {
|
|
7326
|
+
break;
|
|
7330
7327
|
}
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7328
|
+
iterations++;
|
|
7329
|
+
for (let i = 0; i < membership.length; i++) {
|
|
7330
|
+
membership[i] = community[membership[i]];
|
|
7334
7331
|
}
|
|
7332
|
+
level = aggregate(level, community, count);
|
|
7333
|
+
}
|
|
7334
|
+
const groups = Array.from({ length: level.n }, () => []);
|
|
7335
|
+
for (let i = 0; i < membership.length; i++) {
|
|
7336
|
+
groups[membership[i]].push(ids[i]);
|
|
7335
7337
|
}
|
|
7336
7338
|
return {
|
|
7337
|
-
communities:
|
|
7338
|
-
modularity,
|
|
7339
|
-
iterations
|
|
7339
|
+
communities: groups.filter((group) => group.length > 0),
|
|
7340
|
+
modularity: levelModularity(level, settings.resolution),
|
|
7341
|
+
iterations
|
|
7340
7342
|
};
|
|
7341
7343
|
}
|
|
7342
7344
|
/**
|
|
7343
|
-
*
|
|
7345
|
+
* Get pruning statistics
|
|
7346
|
+
* @returns Statistics about nodes pruned during optimization
|
|
7344
7347
|
*/
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
for (const node of this.graph.nodes()) {
|
|
7348
|
-
this.communities.set(node.id, communityId);
|
|
7349
|
-
let nodeWeight = 0;
|
|
7350
|
-
let degree = 0;
|
|
7351
|
-
for (const neighbor of Array.from(this.graph.neighbors(node.id))) {
|
|
7352
|
-
const edge = this.graph.getEdge(node.id, neighbor);
|
|
7353
|
-
const weight = edge?.weight ?? 1;
|
|
7354
|
-
nodeWeight += weight;
|
|
7355
|
-
degree++;
|
|
7356
|
-
}
|
|
7357
|
-
if (!this.graph.isDirected) {
|
|
7358
|
-
for (const neighbor of Array.from(this.graph.inNeighbors(node.id))) {
|
|
7359
|
-
if (!this.graph.hasEdge(node.id, neighbor)) {
|
|
7360
|
-
const edge = this.graph.getEdge(neighbor, node.id);
|
|
7361
|
-
const weight = edge?.weight ?? 1;
|
|
7362
|
-
nodeWeight += weight;
|
|
7363
|
-
degree++;
|
|
7364
|
-
}
|
|
7365
|
-
}
|
|
7366
|
-
}
|
|
7367
|
-
this.nodeWeights.set(node.id, nodeWeight);
|
|
7368
|
-
this.nodeDegrees.set(node.id, degree);
|
|
7369
|
-
this.communityWeights.set(communityId, nodeWeight);
|
|
7370
|
-
this.totalWeight += nodeWeight;
|
|
7371
|
-
communityId++;
|
|
7372
|
-
}
|
|
7373
|
-
this.totalWeight = this.totalWeight / 2;
|
|
7348
|
+
getPruningStats() {
|
|
7349
|
+
return { ...this.pruningStats };
|
|
7374
7350
|
}
|
|
7375
7351
|
/**
|
|
7376
|
-
*
|
|
7377
|
-
* @returns
|
|
7352
|
+
* Index the graph's nodes 0..n-1 and build the level-0 CSR adjacency
|
|
7353
|
+
* @returns The level-0 graph and the node id at each index
|
|
7378
7354
|
*/
|
|
7379
|
-
|
|
7380
|
-
const
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7355
|
+
buildLevel() {
|
|
7356
|
+
const ids = [];
|
|
7357
|
+
const index2 = /* @__PURE__ */ new Map();
|
|
7358
|
+
for (const node of this.graph.nodes()) {
|
|
7359
|
+
index2.set(node.id, ids.length);
|
|
7360
|
+
ids.push(node.id);
|
|
7361
|
+
}
|
|
7362
|
+
const n = ids.length;
|
|
7363
|
+
const edges = [];
|
|
7364
|
+
const counts = new Int32Array(n + 1);
|
|
7365
|
+
const self = new Float64Array(n);
|
|
7366
|
+
const degree = new Float64Array(n);
|
|
7367
|
+
for (const edge of this.graph.edges()) {
|
|
7368
|
+
const s = index2.get(edge.source);
|
|
7369
|
+
const t = index2.get(edge.target);
|
|
7370
|
+
if (s === void 0 || t === void 0) {
|
|
7371
|
+
continue;
|
|
7372
|
+
}
|
|
7373
|
+
const w = edge.weight ?? 1;
|
|
7374
|
+
degree[s] += w;
|
|
7375
|
+
degree[t] += w;
|
|
7376
|
+
if (s === t) {
|
|
7377
|
+
self[s] += w;
|
|
7378
|
+
continue;
|
|
7379
|
+
}
|
|
7380
|
+
edges.push([s, t, w]);
|
|
7381
|
+
counts[s + 1]++;
|
|
7382
|
+
counts[t + 1]++;
|
|
7385
7383
|
}
|
|
7386
|
-
return
|
|
7384
|
+
return { level: buildCsr(n, counts, edges, self, degree), ids };
|
|
7387
7385
|
}
|
|
7388
7386
|
/**
|
|
7389
|
-
*
|
|
7390
|
-
*
|
|
7387
|
+
* Move each node to the neighbouring community with the best modularity gain, until no move
|
|
7388
|
+
* improves modularity
|
|
7389
|
+
* @param level - The graph at this level
|
|
7391
7390
|
* @param options - Local moving options
|
|
7392
|
-
* @
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
const
|
|
7399
|
-
|
|
7400
|
-
let
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7391
|
+
* @returns Each node's community (numbered 0..count-1), the count, and whether any node moved
|
|
7392
|
+
*/
|
|
7393
|
+
localMoving(level, options) {
|
|
7394
|
+
const { n, offsets, targets, weights, degree } = level;
|
|
7395
|
+
const { resolution, maxIterations, tolerance, pruneLeaves } = options;
|
|
7396
|
+
const twoM = degree.reduce((sum, d) => sum + d, 0);
|
|
7397
|
+
const community = new Int32Array(n);
|
|
7398
|
+
const total = new Float64Array(n);
|
|
7399
|
+
for (let i = 0; i < n; i++) {
|
|
7400
|
+
community[i] = i;
|
|
7401
|
+
total[i] = degree[i];
|
|
7402
|
+
}
|
|
7403
|
+
if (twoM === 0) {
|
|
7404
|
+
return { community, count: n, moved: false };
|
|
7405
|
+
}
|
|
7406
|
+
const order = this.nodeOrder(level, options.importanceOrdering);
|
|
7407
|
+
const toCommunity = new Float64Array(n);
|
|
7408
|
+
const touched = new Int32Array(n);
|
|
7409
|
+
let moved = false;
|
|
7410
|
+
let useThreshold = options.thresholdCycling;
|
|
7411
|
+
for (let sweep = 0; sweep < maxIterations; sweep++) {
|
|
7412
|
+
const threshold = useThreshold ? options.pruningThreshold * Math.pow(0.5, sweep / 10) : 0;
|
|
7413
|
+
let sweepMoved = false;
|
|
7414
|
+
let sweepGain = 0;
|
|
7415
|
+
for (const i of order) {
|
|
7416
|
+
const start = offsets[i];
|
|
7417
|
+
const end = offsets[i + 1];
|
|
7418
|
+
if (pruneLeaves && end - start === 1 && community[targets[start]] === community[i]) {
|
|
7405
7419
|
this.pruningStats.leafNodesPruned++;
|
|
7406
7420
|
continue;
|
|
7407
7421
|
}
|
|
7408
|
-
|
|
7409
|
-
const neighborCommunities = this.getNeighborCommunities(nodeId);
|
|
7410
|
-
if (neighborCommunities.size === 0) {
|
|
7422
|
+
if (end === start) {
|
|
7411
7423
|
continue;
|
|
7412
7424
|
}
|
|
7413
|
-
|
|
7414
|
-
let
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7425
|
+
const current = community[i];
|
|
7426
|
+
let touchedCount = 0;
|
|
7427
|
+
for (let e = start; e < end; e++) {
|
|
7428
|
+
const c = community[targets[e]];
|
|
7429
|
+
if (toCommunity[c] === 0) {
|
|
7430
|
+
touched[touchedCount++] = c;
|
|
7431
|
+
}
|
|
7432
|
+
toCommunity[c] += weights[e];
|
|
7433
|
+
}
|
|
7434
|
+
const k = degree[i];
|
|
7435
|
+
total[current] -= k;
|
|
7436
|
+
const stayGain = toCommunity[current] - resolution * total[current] * k / twoM;
|
|
7437
|
+
let best = current;
|
|
7438
|
+
let bestGain = stayGain;
|
|
7439
|
+
const minImprovement = threshold * k;
|
|
7440
|
+
for (let t = 0; t < touchedCount; t++) {
|
|
7441
|
+
const c = touched[t];
|
|
7442
|
+
if (c !== current) {
|
|
7443
|
+
const gain = toCommunity[c] - resolution * total[c] * k / twoM;
|
|
7444
|
+
if (gain > bestGain + minImprovement) {
|
|
7445
|
+
bestGain = gain;
|
|
7446
|
+
best = c;
|
|
7447
|
+
}
|
|
7421
7448
|
}
|
|
7449
|
+
toCommunity[c] = 0;
|
|
7422
7450
|
}
|
|
7423
|
-
|
|
7424
|
-
if (
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
this.addNodeToCommunity(nodeId, bestCommunity);
|
|
7429
|
-
if (bestCommunity !== currentCommunity) {
|
|
7430
|
-
hasChanged = true;
|
|
7431
|
-
improvement = true;
|
|
7451
|
+
total[best] += k;
|
|
7452
|
+
if (best !== current) {
|
|
7453
|
+
community[i] = best;
|
|
7454
|
+
sweepMoved = true;
|
|
7455
|
+
sweepGain += (bestGain - stayGain) / (twoM / 2);
|
|
7432
7456
|
}
|
|
7433
7457
|
}
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
*/
|
|
7442
|
-
isLeafNode(nodeId) {
|
|
7443
|
-
const degree = this.nodeDegrees.get(nodeId) ?? 0;
|
|
7444
|
-
return degree === 1;
|
|
7445
|
-
}
|
|
7446
|
-
/**
|
|
7447
|
-
* Get adaptive threshold that decreases with iterations
|
|
7448
|
-
* @param iteration - Current iteration number
|
|
7449
|
-
* @param baseThreshold - Base threshold value to scale
|
|
7450
|
-
* @returns Adaptive threshold value that decays over iterations
|
|
7451
|
-
*/
|
|
7452
|
-
getAdaptiveThreshold(iteration, baseThreshold) {
|
|
7453
|
-
return baseThreshold * Math.pow(0.5, iteration / 10);
|
|
7454
|
-
}
|
|
7455
|
-
/**
|
|
7456
|
-
* Calculate modularity gain from moving a node to a community
|
|
7457
|
-
* @param nodeId - The node ID to move
|
|
7458
|
-
* @param targetCommunity - The target community ID
|
|
7459
|
-
* @param resolution - Resolution parameter for modularity calculation
|
|
7460
|
-
* @returns The modularity gain from moving the node
|
|
7461
|
-
*/
|
|
7462
|
-
calculateModularityGain(nodeId, targetCommunity, resolution) {
|
|
7463
|
-
const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
|
|
7464
|
-
let weightToTarget = 0;
|
|
7465
|
-
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
7466
|
-
if (this.communities.get(neighbor) === targetCommunity) {
|
|
7467
|
-
const edge = this.graph.getEdge(nodeId, neighbor);
|
|
7468
|
-
weightToTarget += edge?.weight ?? 1;
|
|
7458
|
+
moved ||= sweepMoved;
|
|
7459
|
+
if (!sweepMoved || sweepGain < tolerance) {
|
|
7460
|
+
if (useThreshold) {
|
|
7461
|
+
useThreshold = false;
|
|
7462
|
+
continue;
|
|
7463
|
+
}
|
|
7464
|
+
break;
|
|
7469
7465
|
}
|
|
7470
7466
|
}
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7467
|
+
const renumber = new Int32Array(n).fill(-1);
|
|
7468
|
+
let count = 0;
|
|
7469
|
+
for (let i = 0; i < n; i++) {
|
|
7470
|
+
const c = community[i];
|
|
7471
|
+
if (renumber[c] === -1) {
|
|
7472
|
+
renumber[c] = count++;
|
|
7477
7473
|
}
|
|
7474
|
+
community[i] = renumber[c];
|
|
7478
7475
|
}
|
|
7479
|
-
|
|
7480
|
-
const gain = (weightToTarget - resolution * nodeWeight * targetWeight / (2 * this.totalWeight)) / this.totalWeight;
|
|
7481
|
-
return gain;
|
|
7482
|
-
}
|
|
7483
|
-
/**
|
|
7484
|
-
* Remove node from community (for gain calculation)
|
|
7485
|
-
* @param nodeId - The node ID to remove
|
|
7486
|
-
* @param community - The community ID to remove from
|
|
7487
|
-
*/
|
|
7488
|
-
removeNodeFromCommunity(nodeId, community) {
|
|
7489
|
-
const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
|
|
7490
|
-
this.communityWeights.set(community, (this.communityWeights.get(community) ?? 0) - nodeWeight);
|
|
7491
|
-
this.communities.delete(nodeId);
|
|
7476
|
+
return { community, count, moved };
|
|
7492
7477
|
}
|
|
7493
7478
|
/**
|
|
7494
|
-
*
|
|
7495
|
-
* @param
|
|
7496
|
-
* @param
|
|
7479
|
+
* Order nodes for local moving, by importance (degree * log(1 + weighted degree)) if asked
|
|
7480
|
+
* @param level - The graph at this level
|
|
7481
|
+
* @param importanceOrdering - Whether to sort by importance
|
|
7482
|
+
* @returns Node indices in processing order
|
|
7497
7483
|
*/
|
|
7498
|
-
|
|
7499
|
-
const
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
}
|
|
7503
|
-
/**
|
|
7504
|
-
* Get neighboring communities of a node
|
|
7505
|
-
* @param nodeId - The node ID to find neighbor communities for
|
|
7506
|
-
* @returns Set of community IDs that neighbors belong to
|
|
7507
|
-
*/
|
|
7508
|
-
getNeighborCommunities(nodeId) {
|
|
7509
|
-
const communities = /* @__PURE__ */ new Set();
|
|
7510
|
-
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
7511
|
-
const community = this.communities.get(neighbor);
|
|
7512
|
-
if (community !== void 0) {
|
|
7513
|
-
communities.add(community);
|
|
7514
|
-
}
|
|
7484
|
+
nodeOrder(level, importanceOrdering) {
|
|
7485
|
+
const order = new Int32Array(level.n);
|
|
7486
|
+
for (let i = 0; i < level.n; i++) {
|
|
7487
|
+
order[i] = i;
|
|
7515
7488
|
}
|
|
7516
|
-
if (!
|
|
7517
|
-
|
|
7518
|
-
const community = this.communities.get(neighbor);
|
|
7519
|
-
if (community !== void 0) {
|
|
7520
|
-
communities.add(community);
|
|
7521
|
-
}
|
|
7522
|
-
}
|
|
7489
|
+
if (!importanceOrdering) {
|
|
7490
|
+
return order;
|
|
7523
7491
|
}
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
* Calculate total modularity
|
|
7528
|
-
* @param resolution - Resolution parameter for modularity calculation
|
|
7529
|
-
* @returns The modularity score of the current partition
|
|
7530
|
-
*/
|
|
7531
|
-
calculateModularity(resolution) {
|
|
7532
|
-
if (this.totalWeight === 0) {
|
|
7533
|
-
return 0;
|
|
7492
|
+
const importance = new Float64Array(level.n);
|
|
7493
|
+
for (let i = 0; i < level.n; i++) {
|
|
7494
|
+
importance[i] = (level.offsets[i + 1] - level.offsets[i]) * Math.log(1 + level.degree[i]);
|
|
7534
7495
|
}
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7496
|
+
return order.sort((a, b) => importance[b] - importance[a]);
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
function buildCsr(n, counts, edges, self, degree) {
|
|
7500
|
+
const offsets = counts;
|
|
7501
|
+
for (let i = 0; i < n; i++) {
|
|
7502
|
+
offsets[i + 1] += offsets[i];
|
|
7503
|
+
}
|
|
7504
|
+
const targets = new Int32Array(offsets[n]);
|
|
7505
|
+
const weights = new Float64Array(offsets[n]);
|
|
7506
|
+
const fill = offsets.slice(0, n);
|
|
7507
|
+
for (const [s, t, w] of edges) {
|
|
7508
|
+
targets[fill[s]] = t;
|
|
7509
|
+
weights[fill[s]++] = w;
|
|
7510
|
+
targets[fill[t]] = s;
|
|
7511
|
+
weights[fill[t]++] = w;
|
|
7512
|
+
}
|
|
7513
|
+
return { n, offsets, targets, weights, self, degree };
|
|
7514
|
+
}
|
|
7515
|
+
function aggregate(level, community, count) {
|
|
7516
|
+
const self = new Float64Array(count);
|
|
7517
|
+
const degree = new Float64Array(count);
|
|
7518
|
+
const members = Array.from({ length: count }, () => []);
|
|
7519
|
+
for (let i = 0; i < level.n; i++) {
|
|
7520
|
+
const c = community[i];
|
|
7521
|
+
members[c].push(i);
|
|
7522
|
+
self[c] += level.self[i];
|
|
7523
|
+
degree[c] += level.degree[i];
|
|
7524
|
+
}
|
|
7525
|
+
const edges = [];
|
|
7526
|
+
const counts = new Int32Array(count + 1);
|
|
7527
|
+
const toCommunity = new Float64Array(count);
|
|
7528
|
+
const touched = [];
|
|
7529
|
+
for (let c = 0; c < count; c++) {
|
|
7530
|
+
for (const i of members[c]) {
|
|
7531
|
+
for (let e = level.offsets[i]; e < level.offsets[i + 1]; e++) {
|
|
7532
|
+
const d = community[level.targets[e]];
|
|
7533
|
+
if (d === c) {
|
|
7534
|
+
self[c] += level.weights[e] / 2;
|
|
7535
|
+
} else if (d > c) {
|
|
7536
|
+
if (toCommunity[d] === 0) {
|
|
7537
|
+
touched.push(d);
|
|
7538
|
+
}
|
|
7539
|
+
toCommunity[d] += level.weights[e];
|
|
7545
7540
|
}
|
|
7546
7541
|
}
|
|
7547
7542
|
}
|
|
7548
|
-
for (const
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
const expectedFraction = resolution * Math.pow(aTotal / (2 * this.totalWeight), 2);
|
|
7554
|
-
modularity += actualFraction - expectedFraction;
|
|
7543
|
+
for (const d of touched) {
|
|
7544
|
+
edges.push([c, d, toCommunity[d]]);
|
|
7545
|
+
counts[c + 1]++;
|
|
7546
|
+
counts[d + 1]++;
|
|
7547
|
+
toCommunity[d] = 0;
|
|
7555
7548
|
}
|
|
7556
|
-
|
|
7549
|
+
touched.length = 0;
|
|
7557
7550
|
}
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
return
|
|
7551
|
+
return buildCsr(count, counts, edges, self, degree);
|
|
7552
|
+
}
|
|
7553
|
+
function levelModularity(level, resolution) {
|
|
7554
|
+
const twoM = level.degree.reduce((sum, d) => sum + d, 0);
|
|
7555
|
+
if (twoM === 0) {
|
|
7556
|
+
return 0;
|
|
7564
7557
|
}
|
|
7558
|
+
let modularity = 0;
|
|
7559
|
+
for (let c = 0; c < level.n; c++) {
|
|
7560
|
+
const share = level.degree[c] / twoM;
|
|
7561
|
+
modularity += 2 * level.self[c] / twoM - resolution * share * share;
|
|
7562
|
+
}
|
|
7563
|
+
return modularity;
|
|
7565
7564
|
}
|
|
7566
7565
|
function louvainOptimized(graph, options = {}) {
|
|
7567
7566
|
const optimizer = new OptimizedLouvain(graph);
|
|
@@ -7597,36 +7596,29 @@ function calculateModularity$1(graph, communities, resolution = 1) {
|
|
|
7597
7596
|
if (totalEdgeWeight === 0) {
|
|
7598
7597
|
return 0;
|
|
7599
7598
|
}
|
|
7600
|
-
|
|
7601
|
-
const
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
const nodeIStr = String(nodeI.id);
|
|
7606
|
-
const nodeJStr = String(nodeJ.id);
|
|
7607
|
-
const edgeKey = nodeIStr <= nodeJStr ? `${nodeIStr}-${nodeJStr}` : `${nodeJStr}-${nodeIStr}`;
|
|
7608
|
-
if (countedEdges.has(edgeKey)) {
|
|
7609
|
-
continue;
|
|
7610
|
-
}
|
|
7611
|
-
countedEdges.add(edgeKey);
|
|
7612
|
-
}
|
|
7613
|
-
if (communities.get(nodeI.id) === communities.get(nodeJ.id)) {
|
|
7614
|
-
const edge = graph.getEdge(nodeI.id, nodeJ.id);
|
|
7615
|
-
const reverseEdge = !graph.isDirected ? graph.getEdge(nodeJ.id, nodeI.id) : null;
|
|
7616
|
-
let edgeWeight = 0;
|
|
7617
|
-
if (edge) {
|
|
7618
|
-
edgeWeight += edge.weight ?? 1;
|
|
7619
|
-
}
|
|
7620
|
-
if (reverseEdge && nodeI.id !== nodeJ.id) {
|
|
7621
|
-
edgeWeight += reverseEdge.weight ?? 1;
|
|
7622
|
-
}
|
|
7623
|
-
const degreeI = getNodeDegree(graph, nodeI.id);
|
|
7624
|
-
const degreeJ = getNodeDegree(graph, nodeJ.id);
|
|
7625
|
-
modularity += edgeWeight - resolution * degreeI * degreeJ / (2 * totalEdgeWeight);
|
|
7626
|
-
}
|
|
7599
|
+
const degreeSum = /* @__PURE__ */ new Map();
|
|
7600
|
+
for (const node of graph.nodes()) {
|
|
7601
|
+
const community = communities.get(node.id);
|
|
7602
|
+
if (community === void 0) {
|
|
7603
|
+
continue;
|
|
7627
7604
|
}
|
|
7605
|
+
degreeSum.set(community, (degreeSum.get(community) ?? 0) + getNodeDegree(graph, node.id));
|
|
7628
7606
|
}
|
|
7629
|
-
|
|
7607
|
+
const internalWeight = /* @__PURE__ */ new Map();
|
|
7608
|
+
for (const edge of graph.edges()) {
|
|
7609
|
+
const communityI = communities.get(edge.source);
|
|
7610
|
+
const communityJ = communities.get(edge.target);
|
|
7611
|
+
if (communityI === void 0 || communityI !== communityJ) {
|
|
7612
|
+
continue;
|
|
7613
|
+
}
|
|
7614
|
+
internalWeight.set(communityI, (internalWeight.get(communityI) ?? 0) + (edge.weight ?? 1));
|
|
7615
|
+
}
|
|
7616
|
+
let modularity = 0;
|
|
7617
|
+
for (const [community, degrees] of degreeSum) {
|
|
7618
|
+
const share = degrees / (2 * totalEdgeWeight);
|
|
7619
|
+
modularity += (internalWeight.get(community) ?? 0) / totalEdgeWeight - resolution * share * share;
|
|
7620
|
+
}
|
|
7621
|
+
return modularity;
|
|
7630
7622
|
}
|
|
7631
7623
|
function louvain(graph, options = {}) {
|
|
7632
7624
|
const resolution = options.resolution ?? 1;
|
|
@@ -11814,6 +11806,7 @@ function accelerated(acc) {
|
|
|
11814
11806
|
export {
|
|
11815
11807
|
CSRGraph,
|
|
11816
11808
|
CompactDistanceArray,
|
|
11809
|
+
ConvergenceError,
|
|
11817
11810
|
DeltaPageRank,
|
|
11818
11811
|
DirectionOptimizedBFS,
|
|
11819
11812
|
Graph,
|