@graphty/algorithms 2.0.6 → 2.1.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 (75) hide show
  1. package/README.md +9 -11
  2. package/dist/algorithms.js +499 -30
  3. package/dist/algorithms.js.map +1 -1
  4. package/dist/algorithms.standalone.js +500 -31
  5. package/dist/algorithms.standalone.js.map +1 -1
  6. package/dist/src/algorithms/centrality/betweenness.js +2 -2
  7. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  8. package/dist/src/algorithms/community/girvan-newman.js +1 -1
  9. package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
  10. package/dist/src/algorithms/matching/isomorphism.js +3 -3
  11. package/dist/src/algorithms/matching/isomorphism.js.map +1 -1
  12. package/dist/src/algorithms/mst/prim.js +1 -1
  13. package/dist/src/algorithms/mst/prim.js.map +1 -1
  14. package/dist/src/algorithms/shortest-path/bellman-ford.js +1 -1
  15. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  16. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +1 -1
  17. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -1
  18. package/dist/src/algorithms/shortest-path/dijkstra.js +1 -1
  19. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  20. package/dist/src/algorithms/traversal/bfs-unified.js +2 -2
  21. package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -1
  22. package/dist/src/algorithms/traversal/dfs.js +2 -2
  23. package/dist/src/algorithms/traversal/dfs.js.map +1 -1
  24. package/dist/src/clustering/spectral.js +2 -2
  25. package/dist/src/clustering/spectral.js.map +1 -1
  26. package/dist/src/index.d.ts +4 -0
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/indexed/accelerator.d.ts +14 -2
  30. package/dist/src/indexed/accelerator.d.ts.map +1 -1
  31. package/dist/src/indexed/accelerator.js +8 -0
  32. package/dist/src/indexed/accelerator.js.map +1 -1
  33. package/dist/src/indexed/components.d.ts +8 -0
  34. package/dist/src/indexed/components.d.ts.map +1 -1
  35. package/dist/src/indexed/components.js +8 -1
  36. package/dist/src/indexed/components.js.map +1 -1
  37. package/dist/src/indexed/hits.d.ts +39 -0
  38. package/dist/src/indexed/hits.d.ts.map +1 -0
  39. package/dist/src/indexed/hits.js +95 -0
  40. package/dist/src/indexed/hits.js.map +1 -0
  41. package/dist/src/indexed/index.d.ts +5 -1
  42. package/dist/src/indexed/index.d.ts.map +1 -1
  43. package/dist/src/indexed/index.js +5 -1
  44. package/dist/src/indexed/index.js.map +1 -1
  45. package/dist/src/indexed/k-core.d.ts +32 -0
  46. package/dist/src/indexed/k-core.d.ts.map +1 -0
  47. package/dist/src/indexed/k-core.js +118 -0
  48. package/dist/src/indexed/k-core.js.map +1 -0
  49. package/dist/src/indexed/katz.d.ts +38 -0
  50. package/dist/src/indexed/katz.d.ts.map +1 -0
  51. package/dist/src/indexed/katz.js +61 -0
  52. package/dist/src/indexed/katz.js.map +1 -0
  53. package/dist/src/indexed/louvain.d.ts +33 -0
  54. package/dist/src/indexed/louvain.d.ts.map +1 -0
  55. package/dist/src/indexed/louvain.js +301 -0
  56. package/dist/src/indexed/louvain.js.map +1 -0
  57. package/package.json +6 -6
  58. package/src/algorithms/centrality/betweenness.ts +2 -2
  59. package/src/algorithms/community/girvan-newman.ts +1 -1
  60. package/src/algorithms/matching/isomorphism.ts +3 -3
  61. package/src/algorithms/mst/prim.ts +1 -1
  62. package/src/algorithms/shortest-path/bellman-ford.ts +3 -3
  63. package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +1 -1
  64. package/src/algorithms/shortest-path/dijkstra.ts +1 -1
  65. package/src/algorithms/traversal/bfs-unified.ts +2 -2
  66. package/src/algorithms/traversal/dfs.ts +2 -2
  67. package/src/clustering/spectral.ts +2 -2
  68. package/src/index.ts +10 -0
  69. package/src/indexed/accelerator.ts +26 -2
  70. package/src/indexed/components.ts +8 -1
  71. package/src/indexed/hits.ts +126 -0
  72. package/src/indexed/index.ts +5 -7
  73. package/src/indexed/k-core.ts +138 -0
  74. package/src/indexed/katz.ts +88 -0
  75. package/src/indexed/louvain.ts +345 -0
@@ -0,0 +1,345 @@
1
+ import { type GraphSnapshot, renumberPartition } from "@graphty/graph-format";
2
+
3
+ import { type LabelResult, withGroups } from "./components.js";
4
+
5
+ /** Options of the index-based Louvain, matching the legacy `louvain`. @public */
6
+ export interface LouvainOptions {
7
+ /** Resolution gamma: above 1 favours smaller communities; default 1. */
8
+ readonly resolution?: number | undefined;
9
+ /** Cap on aggregation levels, and on node visits per node within a level; default 100. */
10
+ readonly maxIterations?: number | undefined;
11
+ /** Stop when a level improves modularity by less than this; default 1e-6. */
12
+ readonly tolerance?: number | undefined;
13
+ }
14
+
15
+ /** Result of the index-based Louvain: a partition plus the modularity it reaches. @public */
16
+ export interface LouvainResult extends LabelResult {
17
+ /** Modularity of the returned partition, at the requested resolution. */
18
+ readonly modularity: number;
19
+ /** Aggregation levels that improved the partition. */
20
+ readonly iterations: number;
21
+ }
22
+
23
+ /**
24
+ * One level of the Louvain hierarchy as a weighted adjacency with the self-loops lifted out.
25
+ *
26
+ * Self-loops live in `loop` rather than in the arc list, so the local-moving loop never meets a
27
+ * node as its own neighbour, and `deg` (the null-model term) counts a self-loop twice, which is
28
+ * the NetworkX weighted-degree convention: summing `deg` gives 2m at every level.
29
+ */
30
+ interface Level {
31
+ /** Node count of this level. */
32
+ readonly n: number;
33
+ /** n + 1 row offsets into colIdx / w. */
34
+ readonly rowPtr: Uint32Array;
35
+ /** Neighbour of every arc; no arc targets its own row. */
36
+ readonly colIdx: Uint32Array;
37
+ /** Weight of every arc. */
38
+ readonly w: Float64Array;
39
+ /** Self-loop weight per node, counted ONCE (as an edge weight). */
40
+ readonly loop: Float64Array;
41
+ /** Weighted degree per node: the row's arc weights plus twice its self-loop weight. */
42
+ readonly deg: Float64Array;
43
+ }
44
+
45
+ /**
46
+ * Level 0: the snapshot's own arcs, with the self-loop arcs moved into `loop`.
47
+ * @param s - An undirected snapshot
48
+ * @returns The first level
49
+ */
50
+ function buildLevel0(s: GraphSnapshot): Level {
51
+ const n = s.nodeCount;
52
+ const rowPtr = new Uint32Array(n + 1);
53
+ for (let u = 0; u < n; u++) {
54
+ let kept = 0;
55
+ const end = s.rowPtr[u + 1];
56
+ for (let a = s.rowPtr[u]; a < end; a++) {
57
+ if (s.colIdx[a] !== u) {
58
+ kept++;
59
+ }
60
+ }
61
+ rowPtr[u + 1] = rowPtr[u] + kept;
62
+ }
63
+ const arcs = rowPtr[n];
64
+ const colIdx = new Uint32Array(arcs);
65
+ const w = new Float64Array(arcs);
66
+ const loop = new Float64Array(n);
67
+ const deg = new Float64Array(n);
68
+ let k = 0;
69
+ for (let u = 0; u < n; u++) {
70
+ let rowWeight = 0;
71
+ const end = s.rowPtr[u + 1];
72
+ for (let a = s.rowPtr[u]; a < end; a++) {
73
+ const v = s.colIdx[a];
74
+ const weight = s.weights === null ? 1 : s.weights[a];
75
+ if (v === u) {
76
+ loop[u] += weight;
77
+ } else {
78
+ colIdx[k] = v;
79
+ w[k] = weight;
80
+ k++;
81
+ rowWeight += weight;
82
+ }
83
+ }
84
+ deg[u] = rowWeight + 2 * loop[u];
85
+ }
86
+ return { n, rowPtr, colIdx, w, loop, deg };
87
+ }
88
+
89
+ /**
90
+ * Modularity of a partition of one level: `sum_c in_c / 2m - gamma * (tot_c / 2m)^2`, with `in_c`
91
+ * counting each internal edge twice. This is the same quantity the legacy `calculateModularity`
92
+ * computes over a `Graph`, written per arc.
93
+ * @param level - The level the partition is over
94
+ * @param comm - Community of every node of that level
95
+ * @param count - Number of communities, so `comm` is within `[0, count)`
96
+ * @param resolution - Resolution gamma
97
+ * @param m2 - Twice the total edge weight
98
+ * @returns The modularity
99
+ */
100
+ function modularityOf(level: Level, comm: Uint32Array, count: number, resolution: number, m2: number): number {
101
+ const inside = new Float64Array(count);
102
+ const tot = new Float64Array(count);
103
+ for (let u = 0; u < level.n; u++) {
104
+ const c = comm[u];
105
+ tot[c] += level.deg[u];
106
+ inside[c] += 2 * level.loop[u];
107
+ const end = level.rowPtr[u + 1];
108
+ for (let a = level.rowPtr[u]; a < end; a++) {
109
+ if (comm[level.colIdx[a]] === c) {
110
+ inside[c] += level.w[a];
111
+ }
112
+ }
113
+ }
114
+ let q = 0;
115
+ for (let c = 0; c < count; c++) {
116
+ const share = tot[c] / m2;
117
+ q += inside[c] / m2 - resolution * share * share;
118
+ }
119
+ return q;
120
+ }
121
+
122
+ /**
123
+ * Phase one: move each node to the neighbouring community with the largest modularity gain, until
124
+ * no move is left to make.
125
+ *
126
+ * Nodes wait in a queue rather than in repeated sweeps over all of them, which is what makes this
127
+ * affordable on a graph with no community structure: there the last moves are worth a millionth of
128
+ * a point each and go on for dozens of passes, and a pass costs every node and every arc whether or
129
+ * not anything can move. A node enters the queue when a neighbour leaves its community, so the tail
130
+ * of the run costs the handful of nodes that can still move. Same fixed point, and it is the rule
131
+ * networkx's `louvain_communities` follows.
132
+ * @param level - The level to optimise
133
+ * @param comm - Output: community of every node, overwritten with the singleton partition first
134
+ * @param resolution - Resolution gamma
135
+ * @param m2 - Twice the total edge weight
136
+ * @param maxVisitsPerNode - Cap on node visits, as a multiple of the node count
137
+ * @returns Whether any node moved
138
+ */
139
+ function localMove(level: Level, comm: Uint32Array, resolution: number, m2: number, maxVisitsPerNode: number): boolean {
140
+ const { n } = level;
141
+ const tot = new Float64Array(n);
142
+ for (let u = 0; u < n; u++) {
143
+ comm[u] = u;
144
+ tot[u] = level.deg[u];
145
+ }
146
+ const acc = new Float64Array(n);
147
+ // A visit stamp rather than a clear pass: `mark` grows monotonically over the whole run, so a
148
+ // value left in acc by an earlier visit is never mistaken for this one's.
149
+ const stamp = new Int32Array(n).fill(-1);
150
+ const touched = new Uint32Array(n);
151
+ // A ring of n + 1 slots: `queued` keeps a node out of the queue twice, so n entries is the most
152
+ // that can be waiting.
153
+ const capacity = n + 1;
154
+ const queue = new Uint32Array(capacity);
155
+ const queued = new Uint8Array(n);
156
+ let head = 0;
157
+ let tail = n;
158
+ for (let u = 0; u < n; u++) {
159
+ queue[u] = u;
160
+ queued[u] = 1;
161
+ }
162
+ let mark = 0;
163
+ let visits = 0;
164
+ const maxVisits = maxVisitsPerNode * n;
165
+ let movedAny = false;
166
+ while (head !== tail && visits < maxVisits) {
167
+ const u = queue[head];
168
+ head = head + 1 === capacity ? 0 : head + 1;
169
+ queued[u] = 0;
170
+ visits++;
171
+ const cu = comm[u];
172
+ const ku = level.deg[u];
173
+ tot[cu] -= ku;
174
+ mark++;
175
+ let cnt = 0;
176
+ const end = level.rowPtr[u + 1];
177
+ for (let a = level.rowPtr[u]; a < end; a++) {
178
+ const c = comm[level.colIdx[a]];
179
+ if (stamp[c] !== mark) {
180
+ stamp[c] = mark;
181
+ acc[c] = 0;
182
+ touched[cnt++] = c;
183
+ }
184
+ acc[c] += level.w[a];
185
+ }
186
+ let best = cu;
187
+ let bestGain = (stamp[cu] === mark ? acc[cu] : 0) - (resolution * ku * tot[cu]) / m2;
188
+ for (let i = 0; i < cnt; i++) {
189
+ const c = touched[i];
190
+ if (c === cu) {
191
+ continue;
192
+ }
193
+ const gain = acc[c] - (resolution * ku * tot[c]) / m2;
194
+ if (gain > bestGain) {
195
+ bestGain = gain;
196
+ best = c;
197
+ }
198
+ }
199
+ tot[best] += ku;
200
+ if (best !== cu) {
201
+ comm[u] = best;
202
+ movedAny = true;
203
+ // Only a neighbour outside the community u just joined can gain from u's move.
204
+ for (let a = level.rowPtr[u]; a < end; a++) {
205
+ const v = level.colIdx[a];
206
+ if (queued[v] === 0 && comm[v] !== best) {
207
+ queued[v] = 1;
208
+ queue[tail] = v;
209
+ tail = tail + 1 === capacity ? 0 : tail + 1;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ return movedAny;
215
+ }
216
+
217
+ /**
218
+ * Phase two: every community becomes one node, the edges between two communities merge into one
219
+ * arc, and the edges inside a community become that node's self-loop. Rows are NOT sorted by
220
+ * neighbour index; nothing downstream of here needs them to be.
221
+ * @param level - The level to collapse
222
+ * @param comm - Dense community of every node of that level
223
+ * @param count - Number of communities
224
+ * @returns The next level, with `count` nodes
225
+ */
226
+ function aggregate(level: Level, comm: Uint32Array, count: number): Level {
227
+ const start = new Uint32Array(count + 1);
228
+ for (let u = 0; u < level.n; u++) {
229
+ start[comm[u] + 1]++;
230
+ }
231
+ for (let c = 0; c < count; c++) {
232
+ start[c + 1] += start[c];
233
+ }
234
+ const members = new Uint32Array(level.n);
235
+ const fill = start.slice(0, count);
236
+ for (let u = 0; u < level.n; u++) {
237
+ members[fill[comm[u]]++] = u;
238
+ }
239
+ const rowPtr = new Uint32Array(count + 1);
240
+ const colIdx = new Uint32Array(level.colIdx.length);
241
+ const w = new Float64Array(level.colIdx.length);
242
+ const loop = new Float64Array(count);
243
+ const deg = new Float64Array(count);
244
+ const acc = new Float64Array(count);
245
+ const stamp = new Int32Array(count).fill(-1);
246
+ const touched = new Uint32Array(count);
247
+ let k = 0;
248
+ for (let c = 0; c < count; c++) {
249
+ rowPtr[c] = k;
250
+ let internalArcs = 0;
251
+ let cnt = 0;
252
+ for (let i = start[c]; i < start[c + 1]; i++) {
253
+ const u = members[i];
254
+ loop[c] += level.loop[u];
255
+ const end = level.rowPtr[u + 1];
256
+ for (let a = level.rowPtr[u]; a < end; a++) {
257
+ const other = comm[level.colIdx[a]];
258
+ if (other === c) {
259
+ internalArcs += level.w[a];
260
+ continue;
261
+ }
262
+ if (stamp[other] !== c) {
263
+ stamp[other] = c;
264
+ acc[other] = 0;
265
+ touched[cnt++] = other;
266
+ }
267
+ acc[other] += level.w[a];
268
+ }
269
+ }
270
+ // An edge between two members shows up as two arcs, one from each end.
271
+ loop[c] += internalArcs / 2;
272
+ let rowWeight = 0;
273
+ for (let i = 0; i < cnt; i++) {
274
+ const other = touched[i];
275
+ colIdx[k] = other;
276
+ w[k] = acc[other];
277
+ rowWeight += acc[other];
278
+ k++;
279
+ }
280
+ deg[c] = rowWeight + 2 * loop[c];
281
+ }
282
+ rowPtr[count] = k;
283
+ return { n: count, rowPtr, colIdx: colIdx.subarray(0, k), w: w.subarray(0, k), loop, deg };
284
+ }
285
+
286
+ /**
287
+ * Louvain community detection over a snapshot: local moving to the best modularity gain, then
288
+ * collapse each community into a node and repeat, until a level stops paying for itself.
289
+ *
290
+ * The legacy `louvain` switches implementation at 50 nodes (`useOptimized`) and its small-graph
291
+ * path has no aggregation phase at all, so this port is not move-for-move identical to either
292
+ * branch of it -- what it guarantees is a partition whose modularity is at least as high, measured
293
+ * by the same formula.
294
+ * @param s - An undirected snapshot
295
+ * @param o - Algorithm options
296
+ * @returns The partition, its modularity, and the number of levels that improved it
297
+ * @public
298
+ */
299
+ export function louvain(s: GraphSnapshot, o: LouvainOptions = {}): LouvainResult {
300
+ if (s.directed) {
301
+ throw new Error("Louvain requires an undirected graph. Pass s.toUndirected().snapshot.");
302
+ }
303
+ const resolution = o.resolution ?? 1;
304
+ const maxIterations = o.maxIterations ?? 100;
305
+ const tolerance = o.tolerance ?? 1e-6;
306
+ const n0 = s.nodeCount;
307
+ const labels = new Uint32Array(n0);
308
+ for (let u = 0; u < n0; u++) {
309
+ labels[u] = u;
310
+ }
311
+ let level = buildLevel0(s);
312
+ let m2 = 0;
313
+ for (let u = 0; u < n0; u++) {
314
+ m2 += level.deg[u];
315
+ }
316
+ if (m2 === 0) {
317
+ // No edge weight to redistribute: every node is its own community and Q is 0 by definition,
318
+ // which is what the legacy `calculateModularity` also answers.
319
+ return { ...withGroups(labels, n0), modularity: 0, iterations: 0 };
320
+ }
321
+ let q = modularityOf(level, labels, n0, resolution, m2);
322
+ let iterations = 0;
323
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
324
+ const comm = new Uint32Array(level.n);
325
+ if (!localMove(level, comm, resolution, m2, maxIterations)) {
326
+ break;
327
+ }
328
+ const dense = renumberPartition(comm);
329
+ const { count } = dense;
330
+ const packed = dense.labels;
331
+ const newQ = modularityOf(level, packed, count, resolution, m2);
332
+ for (let u = 0; u < n0; u++) {
333
+ labels[u] = packed[labels[u]];
334
+ }
335
+ iterations = iteration + 1;
336
+ const gain = newQ - q;
337
+ q = newQ;
338
+ if (gain < tolerance || count === level.n) {
339
+ break;
340
+ }
341
+ level = aggregate(level, packed, count);
342
+ }
343
+ const final = renumberPartition(labels);
344
+ return { ...withGroups(final.labels, final.count), modularity: q, iterations };
345
+ }