@graphty/algorithms 2.0.6 → 2.1.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 (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 +5 -5
  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
@@ -15,7 +15,14 @@ export interface LabelResult {
15
15
  groups(): U32[];
16
16
  }
17
17
 
18
- function withGroups(labels: U32, count: number): LabelResult {
18
+ /**
19
+ * Wrap a dense label array as a `LabelResult`, computing the grouping on first use. Shared with the
20
+ * Louvain port, whose result is a partition with a modularity attached.
21
+ * @param labels - Dense label per node index
22
+ * @param count - Number of distinct labels
23
+ * @returns The partition
24
+ */
25
+ export function withGroups(labels: U32, count: number): LabelResult {
19
26
  let cached: U32[] | null = null;
20
27
  return {
21
28
  labels,
@@ -0,0 +1,126 @@
1
+ import type { F64, GraphSnapshot } from "@graphty/graph-format";
2
+
3
+ /** Options of the index-based HITS, matching the legacy `hits`. @public */
4
+ export interface HitsOptions {
5
+ /** Iteration cap; default 100. */
6
+ readonly maxIterations?: number | undefined;
7
+ /** Convergence tolerance on the largest single-node change; default 1e-6. */
8
+ readonly tolerance?: number | undefined;
9
+ /**
10
+ * `false` rescales both vectors so their largest entry is 1, which is what the legacy function
11
+ * does when normalization is switched OFF -- the iteration itself always divides by the L2
12
+ * norm. Default true, i.e. the L2-normalized vectors are returned unchanged.
13
+ */
14
+ readonly normalized?: boolean | undefined;
15
+ /** Weight a neighbour's contribution by the arc weight; default false. */
16
+ readonly weighted?: boolean | undefined;
17
+ }
18
+
19
+ /** Result of the index-based HITS. @public */
20
+ export interface HitsResult {
21
+ /** Hub score per node index. */
22
+ readonly hubs: F64;
23
+ /** Authority score per node index. */
24
+ readonly authorities: F64;
25
+ /** Iterations actually run. */
26
+ readonly iterations: number;
27
+ /** Whether the largest single-node change fell below the tolerance. */
28
+ readonly converged: boolean;
29
+ }
30
+
31
+ /**
32
+ * Divide a vector by its L2 norm, in place; a zero vector is left alone.
33
+ * @param v - The vector to scale
34
+ */
35
+ function l2Normalize(v: Float64Array): void {
36
+ let sum = 0;
37
+ for (let i = 0; i < v.length; i++) {
38
+ sum += v[i] * v[i];
39
+ }
40
+ const norm = Math.sqrt(sum);
41
+ if (norm > 0) {
42
+ for (let i = 0; i < v.length; i++) {
43
+ v[i] /= norm;
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Divide a vector by its largest entry, in place; left alone when that entry is not positive.
50
+ * @param v - The vector to scale
51
+ */
52
+ function maxNormalize(v: Float64Array): void {
53
+ let max = 0;
54
+ for (let i = 0; i < v.length; i++) {
55
+ if (v[i] > max) {
56
+ max = v[i];
57
+ }
58
+ }
59
+ if (max > 0) {
60
+ for (let i = 0; i < v.length; i++) {
61
+ v[i] /= max;
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * HITS by alternating power iteration: an authority sums the hub scores of the nodes pointing at
68
+ * it, a hub sums the authority scores of the nodes it points at, and both vectors are
69
+ * L2-normalized every round. On an undirected snapshot the two adjacencies are the same array, so
70
+ * the hubs and the authorities coincide.
71
+ * @param s - The snapshot
72
+ * @param o - Algorithm options
73
+ * @returns The two score vectors, the iteration count and the convergence flag
74
+ * @public
75
+ */
76
+ export function hits(s: GraphSnapshot, o: HitsOptions = {}): HitsResult {
77
+ const n = s.nodeCount;
78
+ const maxIter = o.maxIterations ?? 100;
79
+ const tol = o.tolerance ?? 1e-6;
80
+ const rev = s.reverse();
81
+ const fwdW = o.weighted === true ? s.weights : null;
82
+ const revW = o.weighted === true ? rev.weights : null;
83
+ let hubs = new Float64Array(n).fill(n === 0 ? 0 : 1 / Math.sqrt(n));
84
+ let authorities = new Float64Array(n).fill(n === 0 ? 0 : 1 / Math.sqrt(n));
85
+ let nextHubs = new Float64Array(n);
86
+ let nextAuthorities = new Float64Array(n);
87
+ let it = 0;
88
+ let converged = false;
89
+ for (; it < maxIter && !converged; it++) {
90
+ for (let v = 0; v < n; v++) {
91
+ let auth = 0;
92
+ const inEnd = rev.rowPtr[v + 1];
93
+ for (let a = rev.rowPtr[v]; a < inEnd; a++) {
94
+ auth += hubs[rev.colIdx[a]] * (revW === null ? 1 : revW[a]);
95
+ }
96
+ nextAuthorities[v] = auth;
97
+ let hub = 0;
98
+ const outEnd = s.rowPtr[v + 1];
99
+ for (let a = s.rowPtr[v]; a < outEnd; a++) {
100
+ hub += authorities[s.colIdx[a]] * (fwdW === null ? 1 : fwdW[a]);
101
+ }
102
+ nextHubs[v] = hub;
103
+ }
104
+ l2Normalize(nextAuthorities);
105
+ l2Normalize(nextHubs);
106
+ let maxDiff = 0;
107
+ for (let v = 0; v < n; v++) {
108
+ const dh = Math.abs(nextHubs[v] - hubs[v]);
109
+ if (dh > maxDiff) {
110
+ maxDiff = dh;
111
+ }
112
+ const da = Math.abs(nextAuthorities[v] - authorities[v]);
113
+ if (da > maxDiff) {
114
+ maxDiff = da;
115
+ }
116
+ }
117
+ [hubs, nextHubs] = [nextHubs, hubs];
118
+ [authorities, nextAuthorities] = [nextAuthorities, authorities];
119
+ converged = maxDiff < tol;
120
+ }
121
+ if (o.normalized === false) {
122
+ maxNormalize(hubs);
123
+ maxNormalize(authorities);
124
+ }
125
+ return { hubs, authorities, iterations: it, converged };
126
+ }
@@ -16,13 +16,11 @@
16
16
  export { type BfsOptions, type BfsResult, breadthFirstSearch } from "./bfs.js";
17
17
  export { type CommonNeighborsOptions, commonNeighborsScore } from "./common-neighbors.js";
18
18
  export { connectedComponents, type LabelResult, weaklyConnectedComponents } from "./components.js";
19
- export {
20
- dijkstra,
21
- type SsspOptions,
22
- type SsspResult,
23
- walkPredArcs,
24
- walkPredEdges,
25
- } from "./dijkstra.js";
19
+ export { dijkstra, type SsspOptions, type SsspResult, walkPredArcs, walkPredEdges } from "./dijkstra.js";
20
+ export { hits, type HitsOptions, type HitsResult } from "./hits.js";
21
+ export { type CorenessResult, kCoreDecomposition } from "./k-core.js";
22
+ export { katzCentrality, type KatzOptions, type KatzResult } from "./katz.js";
23
+ export { louvain, type LouvainOptions, type LouvainResult } from "./louvain.js";
26
24
  export { kruskalMST, type MstOptions, type MstResult } from "./mst.js";
27
25
  export { pageRank, type PageRankOptions, type PageRankResult } from "./pagerank.js";
28
26
  export { arcSourceIn, IndexedMinHeap, IntUnionFind } from "./structures/index.js";
@@ -0,0 +1,138 @@
1
+ import type { GraphSnapshot, U32 } from "@graphty/graph-format";
2
+
3
+ /**
4
+ * Result of the index-based k-core decomposition. `coreness` is the shape the accelerator seam
5
+ * declares (`CorenessResultLike`); `maxCore` and `cores()` are what the legacy
6
+ * `kCoreDecomposition` returns as `maxCore` and its `cores` map.
7
+ * @public
8
+ */
9
+ export interface CorenessResult {
10
+ /** Core number per node index. */
11
+ readonly coreness: U32;
12
+ /** The largest core number present; 0 on an empty graph. */
13
+ readonly maxCore: number;
14
+ /**
15
+ * Node indices grouped by core number, computed once and cached: `cores()[k]` holds the nodes
16
+ * whose coreness is exactly k, so the array has `maxCore + 1` entries.
17
+ * @returns One array per core number
18
+ */
19
+ cores(): U32[];
20
+ }
21
+
22
+ /**
23
+ * Core number of every node, by the Batagelj-Zaversnik bucket peel: O(nodes + arcs), no priority
24
+ * queue and no per-node object.
25
+ *
26
+ * Core numbers are defined on a SIMPLE graph, so a parallel arc counts once (parallels are
27
+ * adjacent within a row, invariant I4) and a self-loop does not count at all -- which is what the
28
+ * legacy implementation's adjacency-of-sets also does, and what NetworkX requires of its input.
29
+ * @param s - An undirected snapshot
30
+ * @returns The core numbers, the largest of them, and the lazy grouping
31
+ * @public
32
+ */
33
+ export function kCoreDecomposition(s: GraphSnapshot): CorenessResult {
34
+ if (s.directed) {
35
+ throw new Error("k-core requires an undirected graph. Pass s.toUndirected().snapshot.");
36
+ }
37
+ const n = s.nodeCount;
38
+ const { rowPtr, colIdx } = s;
39
+ // degree[] is peeled in place: when a node is visited its entry is already its core number.
40
+ const degree = new Uint32Array(n);
41
+ let maxDegree = 0;
42
+ for (let u = 0; u < n; u++) {
43
+ let d = 0;
44
+ let last = -1;
45
+ const end = rowPtr[u + 1];
46
+ for (let a = rowPtr[u]; a < end; a++) {
47
+ const v = colIdx[a];
48
+ if (v !== u && v !== last) {
49
+ d++;
50
+ }
51
+ last = v;
52
+ }
53
+ degree[u] = d;
54
+ if (d > maxDegree) {
55
+ maxDegree = d;
56
+ }
57
+ }
58
+ // Bucket sort the nodes by degree; bin[d] is the start of degree d's block in vert.
59
+ const bin = new Uint32Array(maxDegree + 1);
60
+ for (let u = 0; u < n; u++) {
61
+ bin[degree[u]]++;
62
+ }
63
+ let start = 0;
64
+ for (let d = 0; d <= maxDegree; d++) {
65
+ const count = bin[d];
66
+ bin[d] = start;
67
+ start += count;
68
+ }
69
+ const vert = new Uint32Array(n);
70
+ const pos = new Uint32Array(n);
71
+ for (let u = 0; u < n; u++) {
72
+ pos[u] = bin[degree[u]];
73
+ vert[pos[u]] = u;
74
+ bin[degree[u]]++;
75
+ }
76
+ for (let d = maxDegree; d > 0; d--) {
77
+ bin[d] = bin[d - 1];
78
+ }
79
+ bin[0] = 0;
80
+ // Peel in nondecreasing degree order. A neighbour of higher degree swaps to the front of its
81
+ // bucket and loses one, which keeps vert sorted without re-sorting it.
82
+ let maxCore = 0;
83
+ for (let i = 0; i < n; i++) {
84
+ const v = vert[i];
85
+ const dv = degree[v];
86
+ if (dv > maxCore) {
87
+ maxCore = dv;
88
+ }
89
+ const end = rowPtr[v + 1];
90
+ let last = -1;
91
+ for (let a = rowPtr[v]; a < end; a++) {
92
+ const u = colIdx[a];
93
+ const skip = u === v || u === last;
94
+ last = u;
95
+ if (skip) {
96
+ continue;
97
+ }
98
+ const du = degree[u];
99
+ if (du > dv) {
100
+ const pu = pos[u];
101
+ const pw = bin[du];
102
+ const w = vert[pw];
103
+ if (u !== w) {
104
+ vert[pu] = w;
105
+ vert[pw] = u;
106
+ pos[u] = pw;
107
+ pos[w] = pu;
108
+ }
109
+ bin[du]++;
110
+ degree[u] = du - 1;
111
+ }
112
+ }
113
+ }
114
+ let cached: U32[] | null = null;
115
+ return {
116
+ coreness: degree,
117
+ maxCore,
118
+ cores(): U32[] {
119
+ if (cached === null) {
120
+ const sizes = new Uint32Array(maxCore + 1);
121
+ for (let u = 0; u < n; u++) {
122
+ sizes[degree[u]]++;
123
+ }
124
+ const out: U32[] = [];
125
+ for (let k = 0; k <= maxCore; k++) {
126
+ out.push(new Uint32Array(sizes[k]));
127
+ }
128
+ const fill = new Uint32Array(maxCore + 1);
129
+ for (let u = 0; u < n; u++) {
130
+ const k = degree[u];
131
+ out[k][fill[k]++] = u;
132
+ }
133
+ cached = out;
134
+ }
135
+ return cached;
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,88 @@
1
+ import type { F64, GraphSnapshot } from "@graphty/graph-format";
2
+
3
+ /** Options of the index-based Katz centrality, matching the legacy `katzCentrality`. @public */
4
+ export interface KatzOptions {
5
+ /** Attenuation factor applied to a neighbour's score; default 0.1. */
6
+ readonly alpha?: number | undefined;
7
+ /** Base score every node starts with and keeps; default 1. */
8
+ readonly beta?: number | undefined;
9
+ /** Iteration cap; default 100. */
10
+ readonly maxIterations?: number | undefined;
11
+ /** Convergence tolerance on the largest single-node change; default 1e-6. */
12
+ readonly tolerance?: number | undefined;
13
+ /** Rescale the scores to [0, 1] by min-max, as the legacy function does; default true. */
14
+ readonly normalized?: boolean | undefined;
15
+ /** Weight a neighbour's contribution by the arc weight; default false. */
16
+ readonly weighted?: boolean | undefined;
17
+ }
18
+
19
+ /** Result of the index-based Katz centrality. @public */
20
+ export interface KatzResult {
21
+ /** Score per node index. */
22
+ readonly scores: F64;
23
+ /** Iterations actually run. */
24
+ readonly iterations: number;
25
+ /** Whether the largest single-node change fell below the tolerance. */
26
+ readonly converged: boolean;
27
+ }
28
+
29
+ /**
30
+ * Katz centrality by power iteration over incoming arcs: `x[v] = alpha * sum(x[u] for u -> v) + beta`.
31
+ *
32
+ * `reverse()` supplies the in-arcs, and is the forward adjacency itself on an undirected snapshot,
33
+ * so one loop serves both directions exactly as the legacy function's in-neighbours / neighbours
34
+ * split does.
35
+ * @param s - The snapshot
36
+ * @param o - Algorithm options
37
+ * @returns The scores, the iteration count and the convergence flag
38
+ * @public
39
+ */
40
+ export function katzCentrality(s: GraphSnapshot, o: KatzOptions = {}): KatzResult {
41
+ const n = s.nodeCount;
42
+ const alpha = o.alpha ?? 0.1;
43
+ const beta = o.beta ?? 1;
44
+ const maxIter = o.maxIterations ?? 100;
45
+ const tol = o.tolerance ?? 1e-6;
46
+ const rev = s.reverse();
47
+ const weights = o.weighted === true ? rev.weights : null;
48
+ let cur = new Float64Array(n).fill(beta);
49
+ let next = new Float64Array(n);
50
+ let it = 0;
51
+ let converged = false;
52
+ for (; it < maxIter && !converged; it++) {
53
+ let maxDiff = 0;
54
+ for (let v = 0; v < n; v++) {
55
+ let sum = 0;
56
+ const end = rev.rowPtr[v + 1];
57
+ for (let a = rev.rowPtr[v]; a < end; a++) {
58
+ sum += cur[rev.colIdx[a]] * (weights === null ? 1 : weights[a]);
59
+ }
60
+ next[v] = alpha * sum + beta;
61
+ const diff = Math.abs(next[v] - cur[v]);
62
+ if (diff > maxDiff) {
63
+ maxDiff = diff;
64
+ }
65
+ }
66
+ [cur, next] = [next, cur];
67
+ converged = maxDiff < tol;
68
+ }
69
+ if (o.normalized !== false) {
70
+ let min = Infinity;
71
+ let max = -Infinity;
72
+ for (let v = 0; v < n; v++) {
73
+ if (cur[v] < min) {
74
+ min = cur[v];
75
+ }
76
+ if (cur[v] > max) {
77
+ max = cur[v];
78
+ }
79
+ }
80
+ const range = max - min;
81
+ if (range > 0) {
82
+ for (let v = 0; v < n; v++) {
83
+ cur[v] = (cur[v] - min) / range;
84
+ }
85
+ }
86
+ }
87
+ return { scores: cur, iterations: it, converged };
88
+ }