@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
@@ -69,15 +69,23 @@ export function getNeighborCommunities(graph: Graph, nodeId: NodeId, communities
69
69
  * Calculate modularity of a partition
70
70
  *
71
71
  * Modularity measures the quality of a community partition. It compares the
72
- * number of edges within communities to what would be expected in a random graph.
72
+ * weight of the edges inside communities to what would be expected in a random graph
73
+ * with the same degrees.
73
74
  *
74
- * Formula: Q = (1/2m) * Σ[A_ij - γ(k_i * k_j)/(2m)] * δ(c_i, c_j)
75
+ * Formula: Q = sum over communities c of [ w_in(c)/m - gamma * (K_c / 2m)^2 ]
75
76
  * where:
76
- * - m = total edge weight
77
- * - A_ij = adjacency matrix entry (edge weight between i and j)
78
- * - k_i, k_j = degrees of nodes i and j
79
- * - γ = resolution parameter (higher values favor smaller communities)
80
- * - δ = Kronecker delta (1 if same community, 0 otherwise)
77
+ * - m = total edge weight, each undirected edge counted once
78
+ * - w_in(c) = summed weight of the edges with both endpoints inside community c
79
+ * - K_c = summed weighted degree of community c's nodes
80
+ * - gamma = resolution parameter (higher values favor smaller communities)
81
+ *
82
+ * This is the per-community form of Q = (1/2m) * sum_ij [A_ij - gamma * k_i * k_j / (2m)] over the
83
+ * pairs in the same community.
84
+ * The double sum runs over every PAIR of nodes inside a community, so collecting the null-model
85
+ * term only where an edge exists leaves the penalty far too small and rewards coarseness: the
86
+ * single-community partition, whose modularity is 0 by definition, would otherwise outscore every
87
+ * real split. The form below visits each community once and each edge once, so it counts every
88
+ * pair exactly once without a pass over the whole adjacency matrix.
81
89
  * @param graph - The input graph
82
90
  * @param communities - Map from node IDs to community IDs
83
91
  * @param resolution - Resolution parameter (default: 1.0)
@@ -89,46 +97,35 @@ export function calculateModularity(graph: Graph, communities: Map<NodeId, numbe
89
97
  return 0;
90
98
  }
91
99
 
92
- let modularity = 0;
100
+ // Summed weighted degree of each community's nodes.
101
+ const degreeSum = new Map<number, number>();
102
+ for (const node of graph.nodes()) {
103
+ const community = communities.get(node.id);
104
+ if (community === undefined) {
105
+ continue;
106
+ }
93
107
 
94
- // For undirected graphs, we need to be careful not to double-count edges
95
- const countedEdges = new Set<string>();
96
-
97
- // Calculate modularity: Q = (1/2m) * Σ[A_ij - γ(k_i * k_j)/(2m)] * δ(c_i, c_j)
98
- for (const nodeI of graph.nodes()) {
99
- for (const nodeJ of graph.nodes()) {
100
- // Skip if already counted this pair in undirected graph
101
- if (!graph.isDirected) {
102
- const nodeIStr = String(nodeI.id);
103
- const nodeJStr = String(nodeJ.id);
104
- const edgeKey = nodeIStr <= nodeJStr ? `${nodeIStr}-${nodeJStr}` : `${nodeJStr}-${nodeIStr}`;
105
- if (countedEdges.has(edgeKey)) {
106
- continue;
107
- }
108
-
109
- countedEdges.add(edgeKey);
110
- }
111
-
112
- if (communities.get(nodeI.id) === communities.get(nodeJ.id)) {
113
- const edge = graph.getEdge(nodeI.id, nodeJ.id);
114
- const reverseEdge = !graph.isDirected ? graph.getEdge(nodeJ.id, nodeI.id) : null;
115
-
116
- let edgeWeight = 0;
117
- if (edge) {
118
- edgeWeight += edge.weight ?? 1;
119
- }
120
-
121
- if (reverseEdge && nodeI.id !== nodeJ.id) {
122
- edgeWeight += reverseEdge.weight ?? 1;
123
- }
124
-
125
- const degreeI = getNodeDegree(graph, nodeI.id);
126
- const degreeJ = getNodeDegree(graph, nodeJ.id);
127
-
128
- modularity += edgeWeight - (resolution * degreeI * degreeJ) / (2 * totalEdgeWeight);
129
- }
108
+ degreeSum.set(community, (degreeSum.get(community) ?? 0) + getNodeDegree(graph, node.id));
109
+ }
110
+
111
+ // Summed weight of the edges that stay inside a community.
112
+ const internalWeight = new Map<number, number>();
113
+ for (const edge of graph.edges()) {
114
+ const communityI = communities.get(edge.source);
115
+ const communityJ = communities.get(edge.target);
116
+
117
+ if (communityI === undefined || communityI !== communityJ) {
118
+ continue;
130
119
  }
120
+
121
+ internalWeight.set(communityI, (internalWeight.get(communityI) ?? 0) + (edge.weight ?? 1));
122
+ }
123
+
124
+ let modularity = 0;
125
+ for (const [community, degrees] of degreeSum) {
126
+ const share = degrees / (2 * totalEdgeWeight);
127
+ modularity += (internalWeight.get(community) ?? 0) / totalEdgeWeight - resolution * share * share;
131
128
  }
132
129
 
133
- return modularity / (2 * totalEdgeWeight);
130
+ return modularity;
134
131
  }
@@ -42,6 +42,43 @@ export interface BellmanFordResult {
42
42
  negativeCycleNodes: NodeId[];
43
43
  }
44
44
 
45
+ /**
46
+ * One relaxable arc: a direction an edge can actually be traversed in.
47
+ */
48
+ interface Arc {
49
+ from: NodeId;
50
+ to: NodeId;
51
+ weight: number;
52
+ }
53
+
54
+ /**
55
+ * Expand the graph's edges into the arcs Bellman-Ford may relax.
56
+ *
57
+ * A directed graph yields one arc per edge. An UNDIRECTED graph yields two, because
58
+ * `graph.edges()` reports each undirected edge once, arbitrarily oriented from one of its
59
+ * endpoints. Relaxing only that orientation makes every edge one-way and leaves most of an
60
+ * undirected graph unreachable from the source -- the distances come back as Infinity for
61
+ * nodes that are plainly connected.
62
+ *
63
+ * Note that expanding is not merely a convenience: on an undirected graph a single
64
+ * negative-weight edge IS a negative cycle, since it can be traversed back and forth forever.
65
+ * Producing both arcs is what lets the cycle check below report that correctly.
66
+ * @param graph - The graph whose edges are being expanded.
67
+ * @returns Every arc that may be relaxed, in edge order.
68
+ */
69
+ function relaxableArcs(graph: Graph): Arc[] {
70
+ const arcs: Arc[] = [];
71
+ for (const edge of graph.edges()) {
72
+ const weight = edge.weight ?? 1;
73
+ arcs.push({ from: edge.source, to: edge.target, weight });
74
+ if (!graph.isDirected) {
75
+ arcs.push({ from: edge.target, to: edge.source, weight });
76
+ }
77
+ }
78
+
79
+ return arcs;
80
+ }
81
+
45
82
  /**
46
83
  * Find shortest paths from source using Bellman-Ford algorithm
47
84
  * @param graph - The graph to search
@@ -57,6 +94,7 @@ export function bellmanFord(graph: Graph, source: NodeId, options: BellmanFordOp
57
94
  const distances = new Map<NodeId, number>();
58
95
  const predecessors = new Map<NodeId, NodeId | null>();
59
96
  const nodes = Array.from(graph.nodes()).map((node) => node.id);
97
+ const arcs = relaxableArcs(graph);
60
98
 
61
99
  // Initialize distances
62
100
  for (const nodeId of nodes) {
@@ -68,10 +106,10 @@ export function bellmanFord(graph: Graph, source: NodeId, options: BellmanFordOp
68
106
  for (let i = 0; i < nodes.length - 1; i++) {
69
107
  let updated = false;
70
108
 
71
- for (const edge of Array.from(graph.edges())) {
72
- const u = edge.source;
73
- const v = edge.target;
74
- const weight = edge.weight ?? 1;
109
+ for (const arc of arcs) {
110
+ const u = arc.from;
111
+ const v = arc.to;
112
+ const {weight} = arc;
75
113
 
76
114
  const distanceU = distances.get(u);
77
115
  const distanceV = distances.get(v);
@@ -102,10 +140,10 @@ export function bellmanFord(graph: Graph, source: NodeId, options: BellmanFordOp
102
140
  const negativeCycleNodes: NodeId[] = [];
103
141
  let hasNegativeCycle = false;
104
142
 
105
- for (const edge of graph.edges()) {
106
- const u = edge.source;
107
- const v = edge.target;
108
- const weight = edge.weight ?? 1;
143
+ for (const arc of arcs) {
144
+ const u = arc.from;
145
+ const v = arc.to;
146
+ const {weight} = arc;
109
147
 
110
148
  const distanceU = distances.get(u);
111
149
  const distanceV = distances.get(v);
package/src/errors.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Thrown when an iterative algorithm reaches its iteration cap before meeting its tolerance.
3
+ *
4
+ * The scores it had reached are not returned: an unconverged vector is not the answer, and
5
+ * returning it in silence would pass it off as one. networkx makes the same choice with
6
+ * `PowerIterationFailedConvergence`. Raise `maxIterations`, or loosen `tolerance`, and call again.
7
+ */
8
+ export class ConvergenceError extends Error {
9
+ override readonly name = "ConvergenceError";
10
+
11
+ /**
12
+ * Builds the error and its message.
13
+ * @param algorithm - The function that gave up, e.g. `"eigenvectorCentrality"`
14
+ * @param iterations - The passes it ran, which is its `maxIterations`
15
+ * @param tolerance - The tolerance it did not meet
16
+ */
17
+ constructor(
18
+ readonly algorithm: string,
19
+ readonly iterations: number,
20
+ readonly tolerance: number,
21
+ ) {
22
+ super(
23
+ `${algorithm} did not converge in ${String(iterations)} iterations (tolerance ${String(tolerance)}); raise maxIterations or tolerance`,
24
+ );
25
+ }
26
+ }
package/src/index.ts CHANGED
@@ -37,6 +37,9 @@ export type {
37
37
  TraversalResult,
38
38
  } from "./types/index.js";
39
39
 
40
+ // Error exports
41
+ export { ConvergenceError } from "./errors.js";
42
+
40
43
  // Algorithm exports
41
44
  export * from "./algorithms/index.js";
42
45