@graphty/algorithms 1.8.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/package.json +11 -8
  35. package/src/algorithms/centrality/eigenvector.ts +103 -84
  36. package/src/algorithms/community/girvan-newman.ts +38 -17
  37. package/src/algorithms/community/leiden.ts +270 -275
  38. package/src/algorithms/community/louvain-optimized.ts +303 -313
  39. package/src/algorithms/community/modularity-utils.ts +42 -45
  40. package/src/algorithms/shortest-path/bellman-ford.ts +46 -8
  41. package/src/errors.ts +26 -0
  42. package/src/index.ts +3 -0
  43. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -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 edge of Array.from(graph.edges())) {
3665
- const u = edge.source;
3666
- const v = edge.target;
3667
- const weight = edge.weight ?? 1;
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 edge of graph.edges()) {
3689
- const u = edge.source;
3690
- const v = edge.target;
3691
- const weight = edge.weight ?? 1;
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
- const nodes = Array.from(graph.nodes());
5229
- const nodeIds = nodes.map((node) => node.id);
5230
- if (nodeIds.length === 0) {
5260
+ if (n === 0) {
5231
5261
  return centrality;
5232
5262
  }
5233
- let currentVector = /* @__PURE__ */ new Map();
5234
- let previousVector = /* @__PURE__ */ new Map();
5235
- if (startVector) {
5236
- for (const nodeId of nodeIds) {
5237
- const key = nodeId.toString();
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
- for (let iteration = 0; iteration < maxIterations; iteration++) {
5247
- previousVector = new Map(currentVector);
5248
- currentVector = /* @__PURE__ */ new Map();
5249
- for (const nodeId of nodeIds) {
5250
- let sum = 0;
5251
- const neighbors = Array.from(graph.neighbors(nodeId));
5252
- for (const neighbor of neighbors) {
5253
- const neighborKey = neighbor.toString();
5254
- const prevValue = previousVector.get(neighborKey);
5255
- sum += prevValue ?? 0;
5256
- }
5257
- currentVector.set(nodeId.toString(), sum);
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
- return centrality;
5269
- }
5270
- for (const [nodeId, value] of Array.from(currentVector)) {
5271
- currentVector.set(nodeId, value / norm);
5284
+ next[i] = sum;
5272
5285
  }
5273
- let maxDiff = 0;
5274
- for (const [nodeId, value] of Array.from(currentVector)) {
5275
- const prevValue = previousVector.get(nodeId) ?? 0;
5276
- const diff = Math.abs(value - prevValue);
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
- for (const [nodeId, value] of Array.from(currentVector)) {
5284
- centrality[nodeId] = value;
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 Object.values(centrality)) {
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
- if (range > 0) {
5295
- for (const nodeId of Object.keys(centrality)) {
5296
- const centralityValue = centrality[nodeId];
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
- let modularity = 0;
6562
- const degrees = /* @__PURE__ */ new Map();
6602
+ const degreeSum = /* @__PURE__ */ new Map();
6563
6603
  for (const node of graph.nodes()) {
6564
- degrees.set(node.id, getNodeDegree$1(graph, node.id));
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
- const edgeWeight = edge.weight ?? 1;
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
- return modularity / (2 * totalEdgeWeight);
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
- let totalWeight = 0;
6935
- const degrees = /* @__PURE__ */ new Map();
6936
- for (const [node, neighbors] of currentGraph) {
6937
- let degree = 0;
6938
- for (const weight of neighbors.values()) {
6939
- degree += weight;
6940
- totalWeight += weight;
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
- let improved = false;
6955
- const nodeOrder = [...nodes];
6956
- shuffle(nodeOrder, random);
6957
- for (const node of nodeOrder) {
6958
- const currentCommunity = communities.get(node);
6959
- if (currentCommunity === void 0) {
6960
- continue;
6961
- }
6962
- const neighborCommunities = getNeighborCommunities$1(node, currentGraph, communities);
6963
- let bestCommunity = currentCommunity;
6964
- let bestGain = 0;
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
- createAggregateNetwork(currentGraph, communities);
6990
- const subsetPartition = refinePartition(currentGraph, communities);
6991
- for (const [node, newCommunity] of subsetPartition) {
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 = new Map(communities);
6998
- improved = true;
6999
- }
7000
- if (!improved) {
7001
- break;
7008
+ bestCommunities = candidate;
7002
7009
  }
7003
- const aggregated = aggregateCommunities(currentGraph, communities);
7004
- if (aggregated.graph.size === currentGraph.size) {
7005
- break;
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
- const { graph: aggregatedGraph } = aggregated;
7008
- currentGraph = aggregatedGraph;
7009
- communities.clear();
7010
- let communityId = 0;
7011
- for (const node of currentGraph.keys()) {
7012
- communities.set(node, communityId++);
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
- finalCommunities.set(node, community);
7018
- }
7019
- const communityRenumber = /* @__PURE__ */ new Map();
7020
- let newId = 0;
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 aggregateCommunities(graph, communities) {
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 = communityNodes.get(community);
7234
- if (superNode !== void 0) {
7235
- mapping.set(node, superNode);
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 sourceCommunity = communities.get(node);
7240
- if (sourceCommunity === void 0) {
7241
- continue;
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 targetCommunity = communities.get(neighbor);
7249
- if (targetCommunity === void 0) {
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
- if (sourceSuper !== targetSuper) {
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 = 1,
7297
- maxIterations = 100,
7298
- tolerance = 1e-6,
7299
- pruneLeaves = true,
7300
- importanceOrdering = true,
7301
- pruningThreshold = 0.01,
7302
- thresholdCycling = true
7303
- } = options;
7304
- this.initialize();
7305
- let modularity = this.calculateModularity(resolution);
7306
- let iteration = 0;
7307
- let improved = true;
7308
- while (iteration < maxIterations && improved) {
7309
- const orderedNodes = importanceOrdering ? this.getNodesInImportanceOrder() : Array.from(this.graph.nodes()).map((n) => n.id);
7310
- const threshold = thresholdCycling ? this.getAdaptiveThreshold(iteration, pruningThreshold) : 0;
7311
- improved = this.performLocalMoving(orderedNodes, {
7312
- pruneLeaves,
7313
- threshold,
7314
- resolution
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
- const group = communityGroups.get(community);
7331
- if (group) {
7332
- group.push(nodeId);
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: Array.from(communityGroups.values()),
7337
- modularity,
7338
- iterations: iteration
7338
+ communities: groups.filter((group) => group.length > 0),
7339
+ modularity: levelModularity(level, settings.resolution),
7340
+ iterations
7339
7341
  };
7340
7342
  }
7341
7343
  /**
7342
- * Initialize data structures
7344
+ * Get pruning statistics
7345
+ * @returns Statistics about nodes pruned during optimization
7343
7346
  */
7344
- initialize() {
7345
- let communityId = 0;
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
- * Get nodes ordered by importance (degree * log(weight))
7376
- * @returns Array of node IDs sorted by descending importance
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
- getNodesInImportanceOrder() {
7379
- const nodeImportance = /* @__PURE__ */ new Map();
7380
- for (const [nodeId, degree] of this.nodeDegrees) {
7381
- const weight = this.nodeWeights.get(nodeId) ?? 0;
7382
- const importance = degree * Math.log(1 + weight);
7383
- nodeImportance.set(nodeId, importance);
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
- return Array.from(nodeImportance.entries()).sort((a, b) => b[1] - a[1]).map(([nodeId]) => nodeId);
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
- * Perform local moving phase with optimizations
7389
- * @param nodes - Array of node IDs to process
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
- * @param options.pruneLeaves - Whether to skip leaf nodes
7392
- * @param options.threshold - Minimum gain threshold for moves
7393
- * @param options.resolution - Resolution parameter for modularity
7394
- * @returns True if any improvement was made, false otherwise
7395
- */
7396
- performLocalMoving(nodes, options) {
7397
- const { pruneLeaves, threshold, resolution } = options;
7398
- let improvement = false;
7399
- let hasChanged = true;
7400
- while (hasChanged) {
7401
- hasChanged = false;
7402
- for (const nodeId of nodes) {
7403
- if (pruneLeaves && this.isLeafNode(nodeId)) {
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
- const currentCommunity = this.communities.get(nodeId) ?? 0;
7408
- const neighborCommunities = this.getNeighborCommunities(nodeId);
7409
- if (neighborCommunities.size === 0) {
7421
+ if (end === start) {
7410
7422
  continue;
7411
7423
  }
7412
- let bestCommunity = currentCommunity;
7413
- let bestGain = 0;
7414
- this.removeNodeFromCommunity(nodeId, currentCommunity);
7415
- for (const community of neighborCommunities) {
7416
- const gain = this.calculateModularityGain(nodeId, community, resolution);
7417
- if (gain > bestGain + threshold) {
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
- const currentGain = this.calculateModularityGain(nodeId, currentCommunity, resolution);
7423
- if (currentGain > bestGain + threshold) {
7424
- bestGain = currentGain;
7425
- bestCommunity = currentCommunity;
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
- return improvement;
7435
- }
7436
- /**
7437
- * Check if node is a leaf (degree 1)
7438
- * @param nodeId - The node ID to check
7439
- * @returns True if the node has degree 1, false otherwise
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
- if (!this.graph.isDirected) {
7471
- for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
7472
- if (this.communities.get(neighbor) === targetCommunity && !this.graph.hasEdge(nodeId, neighbor)) {
7473
- const edge = this.graph.getEdge(neighbor, nodeId);
7474
- weightToTarget += edge?.weight ?? 1;
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
- const targetWeight = this.communityWeights.get(targetCommunity) ?? 0;
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
- * Add node to community
7494
- * @param nodeId - The node ID to add
7495
- * @param community - The community ID to add to
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
- addNodeToCommunity(nodeId, community) {
7498
- const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
7499
- this.communityWeights.set(community, (this.communityWeights.get(community) ?? 0) + nodeWeight);
7500
- this.communities.set(nodeId, community);
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 (!this.graph.isDirected) {
7516
- for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
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
- return communities;
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
- let modularity = 0;
7535
- const communityInternalWeights = /* @__PURE__ */ new Map();
7536
- for (const node of this.graph.nodes()) {
7537
- const nodeId = node.id;
7538
- const community = this.communities.get(nodeId) ?? 0;
7539
- for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
7540
- if (this.communities.get(neighbor) === community) {
7541
- const edge = this.graph.getEdge(nodeId, neighbor);
7542
- const weight = edge?.weight ?? 1;
7543
- communityInternalWeights.set(community, (communityInternalWeights.get(community) ?? 0) + weight);
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 [community, internalWeight] of communityInternalWeights) {
7548
- const communityWeight = this.communityWeights.get(community) ?? 0;
7549
- const aIn = this.graph.isDirected ? internalWeight : internalWeight / 2;
7550
- const aTotal = communityWeight;
7551
- const actualFraction = aIn / this.totalWeight;
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
- return modularity;
7548
+ touched.length = 0;
7556
7549
  }
7557
- /**
7558
- * Get pruning statistics
7559
- * @returns Statistics about nodes pruned during optimization
7560
- */
7561
- getPruningStats() {
7562
- return { ...this.pruningStats };
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
- let modularity = 0;
7600
- const countedEdges = /* @__PURE__ */ new Set();
7601
- for (const nodeI of graph.nodes()) {
7602
- for (const nodeJ of graph.nodes()) {
7603
- if (!graph.isDirected) {
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
- return modularity / (2 * totalEdgeWeight);
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@${"d8a898693d15"}`;
18447
+ const WIRE_PRODUCER = `@graphty/graph-format@${"65e7f25588c1"}`;
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,