@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.
Files changed (43) hide show
  1. package/README.md +5 -4
  2. package/dist/algorithms.js +518 -525
  3. package/dist/algorithms.js.map +1 -1
  4. package/dist/algorithms.standalone.js +519 -526
  5. package/dist/algorithms.standalone.js.map +1 -1
  6. package/dist/src/algorithms/centrality/eigenvector.d.ts +13 -2
  7. package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
  8. package/dist/src/algorithms/centrality/eigenvector.js +96 -73
  9. package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
  10. package/dist/src/algorithms/community/girvan-newman.js +35 -15
  11. package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
  12. package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
  13. package/dist/src/algorithms/community/leiden.js +214 -222
  14. package/dist/src/algorithms/community/leiden.js.map +1 -1
  15. package/dist/src/algorithms/community/louvain-optimized.d.ts +28 -72
  16. package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -1
  17. package/dist/src/algorithms/community/louvain-optimized.js +249 -262
  18. package/dist/src/algorithms/community/louvain-optimized.js.map +1 -1
  19. package/dist/src/algorithms/community/modularity-utils.d.ts +15 -7
  20. package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -1
  21. package/dist/src/algorithms/community/modularity-utils.js +38 -38
  22. package/dist/src/algorithms/community/modularity-utils.js.map +1 -1
  23. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
  24. package/dist/src/algorithms/shortest-path/bellman-ford.js +35 -8
  25. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  26. package/dist/src/errors.d.ts +21 -0
  27. package/dist/src/errors.d.ts.map +1 -0
  28. package/dist/src/errors.js +23 -0
  29. package/dist/src/errors.js.map +1 -0
  30. package/dist/src/index.d.ts +1 -0
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/index.js +2 -0
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +8 -8
  36. package/src/algorithms/centrality/eigenvector.ts +103 -84
  37. package/src/algorithms/community/girvan-newman.ts +38 -17
  38. package/src/algorithms/community/leiden.ts +270 -275
  39. package/src/algorithms/community/louvain-optimized.ts +303 -313
  40. package/src/algorithms/community/modularity-utils.ts +42 -45
  41. package/src/algorithms/shortest-path/bellman-ford.ts +46 -8
  42. package/src/errors.ts +26 -0
  43. package/src/index.ts +3 -0
@@ -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 edge of Array.from(graph.edges())) {
3666
- const u = edge.source;
3667
- const v = edge.target;
3668
- const weight = edge.weight ?? 1;
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 edge of graph.edges()) {
3690
- const u = edge.source;
3691
- const v = edge.target;
3692
- const weight = edge.weight ?? 1;
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
- const nodes = Array.from(graph.nodes());
5230
- const nodeIds = nodes.map((node) => node.id);
5231
- if (nodeIds.length === 0) {
5261
+ if (n === 0) {
5232
5262
  return centrality;
5233
5263
  }
5234
- let currentVector = /* @__PURE__ */ new Map();
5235
- let previousVector = /* @__PURE__ */ new Map();
5236
- if (startVector) {
5237
- for (const nodeId of nodeIds) {
5238
- const key = nodeId.toString();
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
- for (let iteration = 0; iteration < maxIterations; iteration++) {
5248
- previousVector = new Map(currentVector);
5249
- currentVector = /* @__PURE__ */ new Map();
5250
- for (const nodeId of nodeIds) {
5251
- let sum = 0;
5252
- const neighbors = Array.from(graph.neighbors(nodeId));
5253
- for (const neighbor of neighbors) {
5254
- const neighborKey = neighbor.toString();
5255
- const prevValue = previousVector.get(neighborKey);
5256
- sum += prevValue ?? 0;
5257
- }
5258
- currentVector.set(nodeId.toString(), sum);
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
- return centrality;
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
- if (maxDiff < tolerance) {
5281
- break;
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
- for (const [nodeId, value] of Array.from(currentVector)) {
5285
- centrality[nodeId] = value;
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 Object.values(centrality)) {
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
- if (range > 0) {
5296
- for (const nodeId of Object.keys(centrality)) {
5297
- const centralityValue = centrality[nodeId];
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
- let modularity = 0;
6563
- const degrees = /* @__PURE__ */ new Map();
6603
+ const degreeSum = /* @__PURE__ */ new Map();
6564
6604
  for (const node of graph.nodes()) {
6565
- degrees.set(node.id, getNodeDegree$1(graph, node.id));
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
- const edgeWeight = edge.weight ?? 1;
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 / (2 * totalEdgeWeight);
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
- let totalWeight = 0;
6936
- const degrees = /* @__PURE__ */ new Map();
6937
- for (const [node, neighbors] of currentGraph) {
6938
- let degree = 0;
6939
- for (const weight of neighbors.values()) {
6940
- degree += weight;
6941
- totalWeight += weight;
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
- let improved = false;
6956
- const nodeOrder = [...nodes];
6957
- shuffle(nodeOrder, random);
6958
- for (const node of nodeOrder) {
6959
- const currentCommunity = communities.get(node);
6960
- if (currentCommunity === void 0) {
6961
- continue;
6962
- }
6963
- const neighborCommunities = getNeighborCommunities$1(node, currentGraph, communities);
6964
- let bestCommunity = currentCommunity;
6965
- let bestGain = 0;
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
- createAggregateNetwork(currentGraph, communities);
6991
- const subsetPartition = refinePartition(currentGraph, communities);
6992
- for (const [node, newCommunity] of subsetPartition) {
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 = new Map(communities);
6999
- improved = true;
7000
- }
7001
- if (!improved) {
7002
- break;
7009
+ bestCommunities = candidate;
7003
7010
  }
7004
- const aggregated = aggregateCommunities(currentGraph, communities);
7005
- if (aggregated.graph.size === currentGraph.size) {
7006
- break;
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
- const { graph: aggregatedGraph } = aggregated;
7009
- currentGraph = aggregatedGraph;
7010
- communities.clear();
7011
- let communityId = 0;
7012
- for (const node of currentGraph.keys()) {
7013
- communities.set(node, communityId++);
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
- finalCommunities.set(node, community);
7019
- }
7020
- const communityRenumber = /* @__PURE__ */ new Map();
7021
- let newId = 0;
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 aggregateCommunities(graph, communities) {
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 = communityNodes.get(community);
7235
- if (superNode !== void 0) {
7236
- mapping.set(node, superNode);
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 sourceCommunity = communities.get(node);
7241
- if (sourceCommunity === void 0) {
7242
- continue;
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 targetCommunity = communities.get(neighbor);
7250
- if (targetCommunity === void 0) {
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
- if (sourceSuper !== targetSuper) {
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 = 1,
7298
- maxIterations = 100,
7299
- tolerance = 1e-6,
7300
- pruneLeaves = true,
7301
- importanceOrdering = true,
7302
- pruningThreshold = 0.01,
7303
- thresholdCycling = true
7304
- } = options;
7305
- this.initialize();
7306
- let modularity = this.calculateModularity(resolution);
7307
- let iteration = 0;
7308
- let improved = true;
7309
- while (iteration < maxIterations && improved) {
7310
- const orderedNodes = importanceOrdering ? this.getNodesInImportanceOrder() : Array.from(this.graph.nodes()).map((n) => n.id);
7311
- const threshold = thresholdCycling ? this.getAdaptiveThreshold(iteration, pruningThreshold) : 0;
7312
- improved = this.performLocalMoving(orderedNodes, {
7313
- pruneLeaves,
7314
- threshold,
7315
- resolution
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
- const group = communityGroups.get(community);
7332
- if (group) {
7333
- group.push(nodeId);
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: Array.from(communityGroups.values()),
7338
- modularity,
7339
- iterations: iteration
7339
+ communities: groups.filter((group) => group.length > 0),
7340
+ modularity: levelModularity(level, settings.resolution),
7341
+ iterations
7340
7342
  };
7341
7343
  }
7342
7344
  /**
7343
- * Initialize data structures
7345
+ * Get pruning statistics
7346
+ * @returns Statistics about nodes pruned during optimization
7344
7347
  */
7345
- initialize() {
7346
- let communityId = 0;
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
- * Get nodes ordered by importance (degree * log(weight))
7377
- * @returns Array of node IDs sorted by descending importance
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
- getNodesInImportanceOrder() {
7380
- const nodeImportance = /* @__PURE__ */ new Map();
7381
- for (const [nodeId, degree] of this.nodeDegrees) {
7382
- const weight = this.nodeWeights.get(nodeId) ?? 0;
7383
- const importance = degree * Math.log(1 + weight);
7384
- nodeImportance.set(nodeId, importance);
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 Array.from(nodeImportance.entries()).sort((a, b) => b[1] - a[1]).map(([nodeId]) => nodeId);
7384
+ return { level: buildCsr(n, counts, edges, self, degree), ids };
7387
7385
  }
7388
7386
  /**
7389
- * Perform local moving phase with optimizations
7390
- * @param nodes - Array of node IDs to process
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
- * @param options.pruneLeaves - Whether to skip leaf nodes
7393
- * @param options.threshold - Minimum gain threshold for moves
7394
- * @param options.resolution - Resolution parameter for modularity
7395
- * @returns True if any improvement was made, false otherwise
7396
- */
7397
- performLocalMoving(nodes, options) {
7398
- const { pruneLeaves, threshold, resolution } = options;
7399
- let improvement = false;
7400
- let hasChanged = true;
7401
- while (hasChanged) {
7402
- hasChanged = false;
7403
- for (const nodeId of nodes) {
7404
- if (pruneLeaves && this.isLeafNode(nodeId)) {
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
- const currentCommunity = this.communities.get(nodeId) ?? 0;
7409
- const neighborCommunities = this.getNeighborCommunities(nodeId);
7410
- if (neighborCommunities.size === 0) {
7422
+ if (end === start) {
7411
7423
  continue;
7412
7424
  }
7413
- let bestCommunity = currentCommunity;
7414
- let bestGain = 0;
7415
- this.removeNodeFromCommunity(nodeId, currentCommunity);
7416
- for (const community of neighborCommunities) {
7417
- const gain = this.calculateModularityGain(nodeId, community, resolution);
7418
- if (gain > bestGain + threshold) {
7419
- bestGain = gain;
7420
- bestCommunity = community;
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
- const currentGain = this.calculateModularityGain(nodeId, currentCommunity, resolution);
7424
- if (currentGain > bestGain + threshold) {
7425
- bestGain = currentGain;
7426
- bestCommunity = currentCommunity;
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
- return improvement;
7436
- }
7437
- /**
7438
- * Check if node is a leaf (degree 1)
7439
- * @param nodeId - The node ID to check
7440
- * @returns True if the node has degree 1, false otherwise
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
- if (!this.graph.isDirected) {
7472
- for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
7473
- if (this.communities.get(neighbor) === targetCommunity && !this.graph.hasEdge(nodeId, neighbor)) {
7474
- const edge = this.graph.getEdge(neighbor, nodeId);
7475
- weightToTarget += edge?.weight ?? 1;
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
- const targetWeight = this.communityWeights.get(targetCommunity) ?? 0;
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
- * Add node to community
7495
- * @param nodeId - The node ID to add
7496
- * @param community - The community ID to add to
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
- addNodeToCommunity(nodeId, community) {
7499
- const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
7500
- this.communityWeights.set(community, (this.communityWeights.get(community) ?? 0) + nodeWeight);
7501
- this.communities.set(nodeId, community);
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 (!this.graph.isDirected) {
7517
- for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
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
- return communities;
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
- let modularity = 0;
7536
- const communityInternalWeights = /* @__PURE__ */ new Map();
7537
- for (const node of this.graph.nodes()) {
7538
- const nodeId = node.id;
7539
- const community = this.communities.get(nodeId) ?? 0;
7540
- for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
7541
- if (this.communities.get(neighbor) === community) {
7542
- const edge = this.graph.getEdge(nodeId, neighbor);
7543
- const weight = edge?.weight ?? 1;
7544
- communityInternalWeights.set(community, (communityInternalWeights.get(community) ?? 0) + weight);
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 [community, internalWeight] of communityInternalWeights) {
7549
- const communityWeight = this.communityWeights.get(community) ?? 0;
7550
- const aIn = this.graph.isDirected ? internalWeight : internalWeight / 2;
7551
- const aTotal = communityWeight;
7552
- const actualFraction = aIn / this.totalWeight;
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
- return modularity;
7549
+ touched.length = 0;
7557
7550
  }
7558
- /**
7559
- * Get pruning statistics
7560
- * @returns Statistics about nodes pruned during optimization
7561
- */
7562
- getPruningStats() {
7563
- return { ...this.pruningStats };
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
- let modularity = 0;
7601
- const countedEdges = /* @__PURE__ */ new Set();
7602
- for (const nodeI of graph.nodes()) {
7603
- for (const nodeJ of graph.nodes()) {
7604
- if (!graph.isDirected) {
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
- return modularity / (2 * totalEdgeWeight);
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,