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