@graphty/algorithms 2.1.1 → 2.2.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.
- package/README.md +139 -50
- package/dist/algorithms.js +15260 -10390
- package/dist/algorithms.js.map +1 -1
- package/dist/algorithms.standalone.js +26675 -21542
- package/dist/algorithms.standalone.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +18 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +40 -1
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +20 -0
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +93 -8
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.d.ts +9 -0
- package/dist/src/algorithms/centrality/degree.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +18 -0
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +10 -0
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.js +29 -0
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
- package/dist/src/algorithms/centrality/hits.d.ts +9 -0
- package/dist/src/algorithms/centrality/hits.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/hits.js +23 -0
- package/dist/src/algorithms/centrality/hits.js.map +1 -1
- package/dist/src/algorithms/centrality/katz.d.ts +9 -0
- package/dist/src/algorithms/centrality/katz.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/katz.js +31 -0
- package/dist/src/algorithms/centrality/katz.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +13 -13
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +47 -10
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman-legacy.d.ts +27 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.d.ts.map +1 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.js +335 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.js.map +1 -0
- package/dist/src/algorithms/community/girvan-newman.d.ts +5 -3
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +34 -308
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +9 -6
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +24 -110
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/components/connected.d.ts.map +1 -1
- package/dist/src/algorithms/components/connected.js +28 -77
- package/dist/src/algorithms/components/connected.js.map +1 -1
- package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -1
- package/dist/src/algorithms/mst/kruskal.js +59 -3
- package/dist/src/algorithms/mst/kruskal.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +46 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +27 -0
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +15 -5
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.js +97 -107
- package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +2 -12
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.js +47 -308
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs-variants.js +5 -25
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs.d.ts +0 -3
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +0 -4
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/dfs.js +33 -154
- package/dist/src/algorithms/traversal/dfs.js.map +1 -1
- package/dist/src/clustering/hierarchical-legacy.d.ts +56 -0
- package/dist/src/clustering/hierarchical-legacy.d.ts.map +1 -0
- package/dist/src/clustering/hierarchical-legacy.js +441 -0
- package/dist/src/clustering/hierarchical-legacy.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +7 -39
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +56 -417
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/k-core-legacy.d.ts +30 -0
- package/dist/src/clustering/k-core-legacy.d.ts.map +1 -0
- package/dist/src/clustering/k-core-legacy.js +191 -0
- package/dist/src/clustering/k-core-legacy.js.map +1 -0
- package/dist/src/clustering/k-core.d.ts +8 -7
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +33 -174
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/mcl-legacy.d.ts +42 -0
- package/dist/src/clustering/mcl-legacy.d.ts.map +1 -0
- package/dist/src/clustering/mcl-legacy.js +386 -0
- package/dist/src/clustering/mcl-legacy.js.map +1 -0
- package/dist/src/clustering/mcl.d.ts +9 -32
- package/dist/src/clustering/mcl.d.ts.map +1 -1
- package/dist/src/clustering/mcl.js +48 -374
- package/dist/src/clustering/mcl.js.map +1 -1
- package/dist/src/index.d.ts +29 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/indexed/accelerator.d.ts +141 -11
- package/dist/src/indexed/accelerator.d.ts.map +1 -1
- package/dist/src/indexed/accelerator.js +319 -5
- package/dist/src/indexed/accelerator.js.map +1 -1
- package/dist/src/indexed/all-pairs.d.ts +59 -0
- package/dist/src/indexed/all-pairs.d.ts.map +1 -0
- package/dist/src/indexed/all-pairs.js +274 -0
- package/dist/src/indexed/all-pairs.js.map +1 -0
- package/dist/src/indexed/bellman-ford.d.ts +25 -0
- package/dist/src/indexed/bellman-ford.d.ts.map +1 -0
- package/dist/src/indexed/bellman-ford.js +59 -0
- package/dist/src/indexed/bellman-ford.js.map +1 -0
- package/dist/src/indexed/betweenness.d.ts +85 -0
- package/dist/src/indexed/betweenness.d.ts.map +1 -0
- package/dist/src/indexed/betweenness.js +188 -0
- package/dist/src/indexed/betweenness.js.map +1 -0
- package/dist/src/indexed/bfs.d.ts +69 -2
- package/dist/src/indexed/bfs.d.ts.map +1 -1
- package/dist/src/indexed/bfs.js +148 -1
- package/dist/src/indexed/bfs.js.map +1 -1
- package/dist/src/indexed/bipartite.d.ts +37 -0
- package/dist/src/indexed/bipartite.d.ts.map +1 -0
- package/dist/src/indexed/bipartite.js +54 -0
- package/dist/src/indexed/bipartite.js.map +1 -0
- package/dist/src/indexed/closeness.d.ts +49 -0
- package/dist/src/indexed/closeness.d.ts.map +1 -0
- package/dist/src/indexed/closeness.js +127 -0
- package/dist/src/indexed/closeness.js.map +1 -0
- package/dist/src/indexed/common-neighbors.d.ts +14 -1
- package/dist/src/indexed/common-neighbors.d.ts.map +1 -1
- package/dist/src/indexed/common-neighbors.js +24 -14
- package/dist/src/indexed/common-neighbors.js.map +1 -1
- package/dist/src/indexed/degree.d.ts +19 -0
- package/dist/src/indexed/degree.d.ts.map +1 -0
- package/dist/src/indexed/degree.js +45 -0
- package/dist/src/indexed/degree.js.map +1 -0
- package/dist/src/indexed/delta-pagerank.d.ts +145 -0
- package/dist/src/indexed/delta-pagerank.d.ts.map +1 -0
- package/dist/src/indexed/delta-pagerank.js +428 -0
- package/dist/src/indexed/delta-pagerank.js.map +1 -0
- package/dist/src/indexed/dfs.d.ts +55 -0
- package/dist/src/indexed/dfs.d.ts.map +1 -0
- package/dist/src/indexed/dfs.js +146 -0
- package/dist/src/indexed/dfs.js.map +1 -0
- package/dist/src/indexed/eigenvector.d.ts +48 -0
- package/dist/src/indexed/eigenvector.d.ts.map +1 -0
- package/dist/src/indexed/eigenvector.js +162 -0
- package/dist/src/indexed/eigenvector.js.map +1 -0
- package/dist/src/indexed/facade.d.ts +109 -0
- package/dist/src/indexed/facade.d.ts.map +1 -0
- package/dist/src/indexed/facade.js +204 -0
- package/dist/src/indexed/facade.js.map +1 -0
- package/dist/src/indexed/flow.d.ts +120 -0
- package/dist/src/indexed/flow.d.ts.map +1 -0
- package/dist/src/indexed/flow.js +377 -0
- package/dist/src/indexed/flow.js.map +1 -0
- package/dist/src/indexed/girvan-newman.d.ts +42 -0
- package/dist/src/indexed/girvan-newman.d.ts.map +1 -0
- package/dist/src/indexed/girvan-newman.js +90 -0
- package/dist/src/indexed/girvan-newman.js.map +1 -0
- package/dist/src/indexed/grsbm.d.ts +85 -0
- package/dist/src/indexed/grsbm.d.ts.map +1 -0
- package/dist/src/indexed/grsbm.js +287 -0
- package/dist/src/indexed/grsbm.js.map +1 -0
- package/dist/src/indexed/hierarchical.d.ts +69 -0
- package/dist/src/indexed/hierarchical.d.ts.map +1 -0
- package/dist/src/indexed/hierarchical.js +245 -0
- package/dist/src/indexed/hierarchical.js.map +1 -0
- package/dist/src/indexed/index.d.ts +29 -3
- package/dist/src/indexed/index.d.ts.map +1 -1
- package/dist/src/indexed/index.js +29 -3
- package/dist/src/indexed/index.js.map +1 -1
- package/dist/src/indexed/isomorphism.d.ts +42 -0
- package/dist/src/indexed/isomorphism.d.ts.map +1 -0
- package/dist/src/indexed/isomorphism.js +269 -0
- package/dist/src/indexed/isomorphism.js.map +1 -0
- package/dist/src/indexed/label-propagation.d.ts +134 -0
- package/dist/src/indexed/label-propagation.d.ts.map +1 -0
- package/dist/src/indexed/label-propagation.js +486 -0
- package/dist/src/indexed/label-propagation.js.map +1 -0
- package/dist/src/indexed/leiden.d.ts +52 -0
- package/dist/src/indexed/leiden.d.ts.map +1 -0
- package/dist/src/indexed/leiden.js +384 -0
- package/dist/src/indexed/leiden.js.map +1 -0
- package/dist/src/indexed/link-prediction.d.ts +147 -0
- package/dist/src/indexed/link-prediction.d.ts.map +1 -0
- package/dist/src/indexed/link-prediction.js +317 -0
- package/dist/src/indexed/link-prediction.js.map +1 -0
- package/dist/src/indexed/markov.d.ts +54 -0
- package/dist/src/indexed/markov.d.ts.map +1 -0
- package/dist/src/indexed/markov.js +284 -0
- package/dist/src/indexed/markov.js.map +1 -0
- package/dist/src/indexed/matching.d.ts +53 -0
- package/dist/src/indexed/matching.d.ts.map +1 -0
- package/dist/src/indexed/matching.js +139 -0
- package/dist/src/indexed/matching.js.map +1 -0
- package/dist/src/indexed/min-cut.d.ts +48 -0
- package/dist/src/indexed/min-cut.d.ts.map +1 -0
- package/dist/src/indexed/min-cut.js +175 -0
- package/dist/src/indexed/min-cut.js.map +1 -0
- package/dist/src/indexed/modularity.d.ts +34 -0
- package/dist/src/indexed/modularity.d.ts.map +1 -0
- package/dist/src/indexed/modularity.js +74 -0
- package/dist/src/indexed/modularity.js.map +1 -0
- package/dist/src/indexed/mst.d.ts +29 -1
- package/dist/src/indexed/mst.d.ts.map +1 -1
- package/dist/src/indexed/mst.js +62 -0
- package/dist/src/indexed/mst.js.map +1 -1
- package/dist/src/indexed/pagerank.d.ts +29 -6
- package/dist/src/indexed/pagerank.d.ts.map +1 -1
- package/dist/src/indexed/pagerank.js +107 -11
- package/dist/src/indexed/pagerank.js.map +1 -1
- package/dist/src/indexed/point-to-point.d.ts +53 -0
- package/dist/src/indexed/point-to-point.d.ts.map +1 -0
- package/dist/src/indexed/point-to-point.js +173 -0
- package/dist/src/indexed/point-to-point.js.map +1 -0
- package/dist/src/indexed/scc.d.ts +53 -0
- package/dist/src/indexed/scc.d.ts.map +1 -0
- package/dist/src/indexed/scc.js +110 -0
- package/dist/src/indexed/scc.js.map +1 -0
- package/dist/src/indexed/spectral.d.ts +61 -0
- package/dist/src/indexed/spectral.d.ts.map +1 -0
- package/dist/src/indexed/spectral.js +476 -0
- package/dist/src/indexed/spectral.js.map +1 -0
- package/dist/src/indexed/structures/bit-set.d.ts +45 -0
- package/dist/src/indexed/structures/bit-set.d.ts.map +1 -0
- package/dist/src/indexed/structures/bit-set.js +57 -0
- package/dist/src/indexed/structures/bit-set.js.map +1 -0
- package/dist/src/indexed/structures/max-heap.d.ts +52 -0
- package/dist/src/indexed/structures/max-heap.d.ts.map +1 -0
- package/dist/src/indexed/structures/max-heap.js +66 -0
- package/dist/src/indexed/structures/max-heap.js.map +1 -0
- package/dist/src/indexed/structures/min-heap.d.ts +30 -1
- package/dist/src/indexed/structures/min-heap.d.ts.map +1 -1
- package/dist/src/indexed/structures/min-heap.js +46 -6
- package/dist/src/indexed/structures/min-heap.js.map +1 -1
- package/dist/src/indexed/structures/ring-queue.d.ts +41 -0
- package/dist/src/indexed/structures/ring-queue.d.ts.map +1 -0
- package/dist/src/indexed/structures/ring-queue.js +64 -0
- package/dist/src/indexed/structures/ring-queue.js.map +1 -0
- package/dist/src/indexed/sync.d.ts +50 -0
- package/dist/src/indexed/sync.d.ts.map +1 -0
- package/dist/src/indexed/sync.js +175 -0
- package/dist/src/indexed/sync.js.map +1 -0
- package/dist/src/indexed/terahac.d.ts +58 -0
- package/dist/src/indexed/terahac.d.ts.map +1 -0
- package/dist/src/indexed/terahac.js +244 -0
- package/dist/src/indexed/terahac.js.map +1 -0
- package/dist/src/indexed/to-snapshot.d.ts +45 -2
- package/dist/src/indexed/to-snapshot.d.ts.map +1 -1
- package/dist/src/indexed/to-snapshot.js +140 -6
- package/dist/src/indexed/to-snapshot.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors-legacy.d.ts +71 -0
- package/dist/src/link-prediction/common-neighbors-legacy.d.ts.map +1 -0
- package/dist/src/link-prediction/common-neighbors-legacy.js +172 -0
- package/dist/src/link-prediction/common-neighbors-legacy.js.map +1 -0
- package/dist/src/link-prediction/common-neighbors.d.ts +11 -19
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +56 -123
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/optimized/direction-optimized-bfs.d.ts +6 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -1
- package/dist/src/optimized/direction-optimized-bfs.js +6 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -1
- package/dist/src/research/grsbm-legacy.d.ts +82 -0
- package/dist/src/research/grsbm-legacy.d.ts.map +1 -0
- package/dist/src/research/grsbm-legacy.js +416 -0
- package/dist/src/research/grsbm-legacy.js.map +1 -0
- package/dist/src/research/grsbm.d.ts +9 -66
- package/dist/src/research/grsbm.d.ts.map +1 -1
- package/dist/src/research/grsbm.js +67 -391
- package/dist/src/research/grsbm.js.map +1 -1
- package/dist/src/research/sync-legacy.d.ts +48 -0
- package/dist/src/research/sync-legacy.d.ts.map +1 -0
- package/dist/src/research/sync-legacy.js +337 -0
- package/dist/src/research/sync-legacy.js.map +1 -0
- package/dist/src/research/sync.d.ts +10 -32
- package/dist/src/research/sync.d.ts.map +1 -1
- package/dist/src/research/sync.js +28 -316
- package/dist/src/research/sync.js.map +1 -1
- package/package.json +4 -3
- package/src/algorithms/centrality/betweenness.ts +51 -1
- package/src/algorithms/centrality/closeness.ts +112 -12
- package/src/algorithms/centrality/degree.ts +19 -0
- package/src/algorithms/centrality/eigenvector.ts +34 -0
- package/src/algorithms/centrality/hits.ts +24 -0
- package/src/algorithms/centrality/katz.ts +32 -0
- package/src/algorithms/centrality/pagerank.ts +53 -13
- package/src/algorithms/community/girvan-newman-legacy.ts +414 -0
- package/src/algorithms/community/girvan-newman.ts +36 -386
- package/src/algorithms/community/label-propagation.ts +24 -133
- package/src/algorithms/components/connected.ts +28 -95
- package/src/algorithms/mst/kruskal.ts +65 -4
- package/src/algorithms/shortest-path/bellman-ford.ts +49 -0
- package/src/algorithms/shortest-path/dijkstra.ts +29 -0
- package/src/algorithms/shortest-path/floyd-warshall.ts +106 -136
- package/src/algorithms/traversal/bfs-unified.ts +52 -366
- package/src/algorithms/traversal/bfs-variants.ts +5 -28
- package/src/algorithms/traversal/bfs.ts +0 -4
- package/src/algorithms/traversal/dfs.ts +39 -182
- package/src/clustering/hierarchical-legacy.ts +551 -0
- package/src/clustering/hierarchical.ts +64 -520
- package/src/clustering/k-core-legacy.ts +229 -0
- package/src/clustering/k-core.ts +38 -209
- package/src/clustering/mcl-legacy.ts +498 -0
- package/src/clustering/mcl.ts +46 -479
- package/src/index.ts +90 -3
- package/src/indexed/accelerator.ts +502 -16
- package/src/indexed/all-pairs.ts +342 -0
- package/src/indexed/bellman-ford.ts +72 -0
- package/src/indexed/betweenness.ts +258 -0
- package/src/indexed/bfs.ts +189 -3
- package/src/indexed/bipartite.ts +77 -0
- package/src/indexed/closeness.ts +160 -0
- package/src/indexed/common-neighbors.ts +31 -14
- package/src/indexed/degree.ts +55 -0
- package/src/indexed/delta-pagerank.ts +513 -0
- package/src/indexed/dfs.ts +189 -0
- package/src/indexed/eigenvector.ts +202 -0
- package/src/indexed/facade.ts +229 -0
- package/src/indexed/flow.ts +500 -0
- package/src/indexed/girvan-newman.ts +120 -0
- package/src/indexed/grsbm.ts +390 -0
- package/src/indexed/hierarchical.ts +295 -0
- package/src/indexed/index.ts +94 -3
- package/src/indexed/isomorphism.ts +305 -0
- package/src/indexed/label-propagation.ts +582 -0
- package/src/indexed/leiden.ts +436 -0
- package/src/indexed/link-prediction.ts +399 -0
- package/src/indexed/markov.ts +324 -0
- package/src/indexed/matching.ts +192 -0
- package/src/indexed/min-cut.ts +201 -0
- package/src/indexed/modularity.ts +81 -0
- package/src/indexed/mst.ts +79 -1
- package/src/indexed/pagerank.ts +128 -15
- package/src/indexed/point-to-point.ts +220 -0
- package/src/indexed/scc.ts +132 -0
- package/src/indexed/spectral.ts +550 -0
- package/src/indexed/structures/bit-set.ts +65 -0
- package/src/indexed/structures/max-heap.ts +74 -0
- package/src/indexed/structures/min-heap.ts +51 -6
- package/src/indexed/structures/ring-queue.ts +70 -0
- package/src/indexed/sync.ts +229 -0
- package/src/indexed/terahac.ts +290 -0
- package/src/indexed/to-snapshot.ts +156 -6
- package/src/link-prediction/common-neighbors-legacy.ts +252 -0
- package/src/link-prediction/common-neighbors.ts +65 -168
- package/src/optimized/direction-optimized-bfs.ts +6 -1
- package/src/research/grsbm-legacy.ts +586 -0
- package/src/research/grsbm.ts +76 -555
- package/src/research/sync-legacy.ts +456 -0
- package/src/research/sync.ts +30 -431
|
@@ -11,13 +11,18 @@ export class IndexedMinHeap {
|
|
|
11
11
|
private readonly heap: U32; // slot -> node
|
|
12
12
|
private readonly slot: U32; // node -> slot, INVALID_INDEX when absent
|
|
13
13
|
private readonly key: F64; // node -> key
|
|
14
|
+
private readonly tieBreakByIndex: boolean;
|
|
14
15
|
private size = 0;
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Create an empty heap over node indices in `[0, capacity)`.
|
|
18
19
|
* @param capacity - The number of distinct node indices the heap may hold
|
|
20
|
+
* @param tieBreakByIndex - When true, of two equal keys the lower node index pops first, so
|
|
21
|
+
* the pop order is fully determined by the keys; when false (the default) equal keys pop in
|
|
22
|
+
* an order that depends on the push history
|
|
19
23
|
*/
|
|
20
|
-
constructor(capacity: number) {
|
|
24
|
+
constructor(capacity: number, tieBreakByIndex = false) {
|
|
25
|
+
this.tieBreakByIndex = tieBreakByIndex;
|
|
21
26
|
this.heap = new Uint32Array(capacity);
|
|
22
27
|
this.slot = new Uint32Array(capacity).fill(INVALID_INDEX);
|
|
23
28
|
this.key = new Float64Array(capacity);
|
|
@@ -31,12 +36,42 @@ export class IndexedMinHeap {
|
|
|
31
36
|
return this.size === 0;
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Whether a node is currently in the heap.
|
|
41
|
+
* @param node - The node index
|
|
42
|
+
* @returns True when the node was pushed and has not been popped since
|
|
43
|
+
*/
|
|
44
|
+
has(node: number): boolean {
|
|
45
|
+
return this.slot[node] !== INVALID_INDEX;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The key a node was last given. Meaningful only while `has(node)` is true.
|
|
50
|
+
* @param node - The node index
|
|
51
|
+
* @returns Its key
|
|
52
|
+
*/
|
|
53
|
+
keyOf(node: number): number {
|
|
54
|
+
return this.key[node];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The smallest key in the heap, without removing it.
|
|
59
|
+
* @returns The minimum key, or Infinity when the heap is empty
|
|
60
|
+
*/
|
|
61
|
+
peekKey(): number {
|
|
62
|
+
return this.size === 0 ? Infinity : this.key[this.heap[0]];
|
|
63
|
+
}
|
|
64
|
+
|
|
34
65
|
/**
|
|
35
66
|
* Insert a node that is not in the heap.
|
|
36
67
|
* @param node - The node index
|
|
37
68
|
* @param key - Its key
|
|
69
|
+
* @throws RangeError when the node index is outside `[0, capacity)`
|
|
38
70
|
*/
|
|
39
71
|
push(node: number, key: number): void {
|
|
72
|
+
if (!(node >= 0 && node < this.slot.length)) {
|
|
73
|
+
throw new RangeError(`Node index ${String(node)} is outside the heap's ${String(this.slot.length)} nodes`);
|
|
74
|
+
}
|
|
40
75
|
this.key[node] = key;
|
|
41
76
|
this.heap[this.size] = node;
|
|
42
77
|
this.slot[node] = this.size;
|
|
@@ -79,14 +114,25 @@ export class IndexedMinHeap {
|
|
|
79
114
|
return top;
|
|
80
115
|
}
|
|
81
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Whether node `a` pops before node `b`.
|
|
119
|
+
* @param a - A node index in the heap
|
|
120
|
+
* @param b - A node index in the heap
|
|
121
|
+
* @returns True when a's key is smaller, or equal with the tie-break on and a lower index
|
|
122
|
+
*/
|
|
123
|
+
private before(a: number, b: number): boolean {
|
|
124
|
+
const ka = this.key[a];
|
|
125
|
+
const kb = this.key[b];
|
|
126
|
+
return ka < kb || (this.tieBreakByIndex && ka === kb && a < b);
|
|
127
|
+
}
|
|
128
|
+
|
|
82
129
|
private siftUp(from: number): void {
|
|
83
130
|
let at = from;
|
|
84
131
|
const node = this.heap[at];
|
|
85
|
-
const key = this.key[node];
|
|
86
132
|
while (at > 0) {
|
|
87
133
|
const parent = (at - 1) >> 1;
|
|
88
134
|
const parentNode = this.heap[parent];
|
|
89
|
-
if (this.
|
|
135
|
+
if (!this.before(node, parentNode)) {
|
|
90
136
|
break;
|
|
91
137
|
}
|
|
92
138
|
this.heap[at] = parentNode;
|
|
@@ -100,16 +146,15 @@ export class IndexedMinHeap {
|
|
|
100
146
|
private siftDown(from: number): void {
|
|
101
147
|
let at = from;
|
|
102
148
|
const node = this.heap[at];
|
|
103
|
-
const key = this.key[node];
|
|
104
149
|
for (;;) {
|
|
105
150
|
const left = 2 * at + 1;
|
|
106
151
|
if (left >= this.size) {
|
|
107
152
|
break;
|
|
108
153
|
}
|
|
109
154
|
const right = left + 1;
|
|
110
|
-
const child = right < this.size && this.
|
|
155
|
+
const child = right < this.size && this.before(this.heap[right], this.heap[left]) ? right : left;
|
|
111
156
|
const childNode = this.heap[child];
|
|
112
|
-
if (
|
|
157
|
+
if (!this.before(childNode, node)) {
|
|
113
158
|
break;
|
|
114
159
|
}
|
|
115
160
|
this.heap[at] = childNode;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-capacity FIFO queue of node indices over one `Uint32Array`, for the frontier of a
|
|
3
|
+
* breadth-first sweep that may re-enqueue a node (label propagation, SPFA-style relaxation) and so
|
|
4
|
+
* cannot use the one-pass `order` array plain BFS does. Overflow and underflow throw rather than
|
|
5
|
+
* silently overwrite: a wrong capacity is a bug in the caller.
|
|
6
|
+
*/
|
|
7
|
+
export class RingQueue {
|
|
8
|
+
private readonly buffer: Uint32Array;
|
|
9
|
+
private head = 0;
|
|
10
|
+
private count = 0;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create an empty queue.
|
|
14
|
+
* @param capacity - The most entries the queue holds at once
|
|
15
|
+
*/
|
|
16
|
+
constructor(capacity: number) {
|
|
17
|
+
this.buffer = new Uint32Array(capacity);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The number of queued entries.
|
|
22
|
+
* @returns The entry count
|
|
23
|
+
*/
|
|
24
|
+
get size(): number {
|
|
25
|
+
return this.count;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the queue holds no entries.
|
|
30
|
+
* @returns True when empty
|
|
31
|
+
*/
|
|
32
|
+
isEmpty(): boolean {
|
|
33
|
+
return this.count === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append a value at the tail.
|
|
38
|
+
* @param value - A node index
|
|
39
|
+
* @throws RangeError when the queue is full
|
|
40
|
+
*/
|
|
41
|
+
push(value: number): void {
|
|
42
|
+
const capacity = this.buffer.length;
|
|
43
|
+
if (this.count === capacity) {
|
|
44
|
+
throw new RangeError(`RingQueue: full at capacity ${String(capacity)}`);
|
|
45
|
+
}
|
|
46
|
+
this.buffer[(this.head + this.count) % capacity] = value;
|
|
47
|
+
this.count++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove and return the value at the head.
|
|
52
|
+
* @returns The oldest queued node index
|
|
53
|
+
* @throws RangeError when the queue is empty
|
|
54
|
+
*/
|
|
55
|
+
shift(): number {
|
|
56
|
+
if (this.count === 0) {
|
|
57
|
+
throw new RangeError("RingQueue: shift on an empty queue");
|
|
58
|
+
}
|
|
59
|
+
const value = this.buffer[this.head];
|
|
60
|
+
this.head = (this.head + 1) % this.buffer.length;
|
|
61
|
+
this.count--;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Drop every entry, keeping the buffer for reuse. */
|
|
66
|
+
clear(): void {
|
|
67
|
+
this.head = 0;
|
|
68
|
+
this.count = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { F64, GraphSnapshot, U32 } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
import { SeededRandom } from "../utils/math-utilities.js";
|
|
4
|
+
|
|
5
|
+
/** Options of the index-based SynC clustering, matching the legacy `syncClustering`. @public */
|
|
6
|
+
export interface SyncClusteringOptions {
|
|
7
|
+
/** Number of cluster centres: an integer in `[1, nodeCount]` (legacy rounds a fraction up; the port throws). */
|
|
8
|
+
readonly numClusters: number;
|
|
9
|
+
/** Iteration cap; default 100. */
|
|
10
|
+
readonly maxIterations?: number | undefined;
|
|
11
|
+
/** Stop when the loss changes by less than this between iterations; default 1e-6. */
|
|
12
|
+
readonly tolerance?: number | undefined;
|
|
13
|
+
/** Seed of the embedding initialisation and the centre draws; default 42. */
|
|
14
|
+
readonly seed?: number | undefined;
|
|
15
|
+
/** Gradient step; default 0.01. */
|
|
16
|
+
readonly learningRate?: number | undefined;
|
|
17
|
+
/** Weight of the neighbour-reconstruction and regularisation terms; default 0.1. */
|
|
18
|
+
readonly lambda?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Result of the index-based SynC clustering. @public */
|
|
22
|
+
export interface SyncClusteringResult {
|
|
23
|
+
/** The centre each node is closest to, in `[0, count)`. A centre may end up with no node. */
|
|
24
|
+
readonly labels: U32;
|
|
25
|
+
/** Number of cluster centres: `numClusters`. */
|
|
26
|
+
readonly count: number;
|
|
27
|
+
/** Node embeddings, row-major: node i is `embeddings.subarray(i * dimensions, (i + 1) * dimensions)`. */
|
|
28
|
+
readonly embeddings: F64;
|
|
29
|
+
/** Embedding length: `min(64, nodeCount)`. */
|
|
30
|
+
readonly dimensions: number;
|
|
31
|
+
/** Loss of the last iteration run (Infinity when none ran). */
|
|
32
|
+
readonly loss: number;
|
|
33
|
+
/** Loss of the iteration before the last (Infinity when fewer than two ran). */
|
|
34
|
+
readonly previousLoss: number;
|
|
35
|
+
/** Iterations run. */
|
|
36
|
+
readonly iterations: number;
|
|
37
|
+
/** Whether the loss settled within the tolerance before the cap. */
|
|
38
|
+
readonly converged: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Euclidean distance between two rows of a row-major matrix.
|
|
43
|
+
* @param a - First matrix
|
|
44
|
+
* @param i - Row of a
|
|
45
|
+
* @param b - Second matrix
|
|
46
|
+
* @param j - Row of b
|
|
47
|
+
* @param dim - Row length
|
|
48
|
+
* @returns The distance
|
|
49
|
+
*/
|
|
50
|
+
function distance(a: F64, i: number, b: F64, j: number, dim: number): number {
|
|
51
|
+
let sum = 0;
|
|
52
|
+
for (let k = 0; k < dim; k++) {
|
|
53
|
+
const diff = a[i * dim + k] - b[j * dim + k];
|
|
54
|
+
sum += diff * diff;
|
|
55
|
+
}
|
|
56
|
+
return Math.sqrt(sum);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* SynC over a snapshot: node embeddings seeded from degree and a seeded generator, pulled toward
|
|
61
|
+
* their out-neighbours by gradient steps, and clustered by k-means with k-means++ centres. The
|
|
62
|
+
* arithmetic, and the order of every random draw, are the legacy `syncClustering`'s, so for the same
|
|
63
|
+
* seed the labels and embeddings agree with it to rounding; the port draws from its own generator
|
|
64
|
+
* instead of replacing `Math.random` for the duration of the call.
|
|
65
|
+
*
|
|
66
|
+
* Weights are ignored and a parallel arc pulls once per arc. The seeding degree is legacy's: in plus
|
|
67
|
+
* out when directed, the stored arc count when undirected, so an undirected self-loop counts once.
|
|
68
|
+
* @param s - The snapshot
|
|
69
|
+
* @param options - {@link SyncClusteringOptions}
|
|
70
|
+
* @returns Assignments, embeddings and convergence
|
|
71
|
+
*/
|
|
72
|
+
export function syncClustering(s: GraphSnapshot, options: SyncClusteringOptions): SyncClusteringResult {
|
|
73
|
+
const {
|
|
74
|
+
numClusters,
|
|
75
|
+
maxIterations = 100,
|
|
76
|
+
tolerance = 1e-6,
|
|
77
|
+
seed = 42,
|
|
78
|
+
learningRate = 0.01,
|
|
79
|
+
lambda = 0.1,
|
|
80
|
+
} = options;
|
|
81
|
+
const n = s.nodeCount;
|
|
82
|
+
if (n === 0) {
|
|
83
|
+
return {
|
|
84
|
+
labels: new Uint32Array(0),
|
|
85
|
+
count: numClusters,
|
|
86
|
+
embeddings: new Float64Array(0),
|
|
87
|
+
dimensions: 0,
|
|
88
|
+
loss: 0,
|
|
89
|
+
previousLoss: Infinity,
|
|
90
|
+
iterations: 0,
|
|
91
|
+
converged: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (!Number.isInteger(numClusters) || numClusters <= 0 || numClusters > n) {
|
|
95
|
+
throw new Error(`Invalid number of clusters: ${String(numClusters)}. Must be between 1 and ${String(n)}`);
|
|
96
|
+
}
|
|
97
|
+
const random = SeededRandom.createGenerator(seed);
|
|
98
|
+
const dim = Math.min(64, n);
|
|
99
|
+
const { rowPtr, colIdx } = s;
|
|
100
|
+
|
|
101
|
+
const emb = new Float64Array(n * dim);
|
|
102
|
+
// Legacy's degree: an undirected self-loop counts once (its single stored arc), not twice.
|
|
103
|
+
const degree = s.directed ? s.degree() : s.outDegree();
|
|
104
|
+
for (let u = 0; u < n; u++) {
|
|
105
|
+
const normalized = degree[u] / Math.max(1, n - 1);
|
|
106
|
+
for (let k = 0; k < dim; k++) {
|
|
107
|
+
emb[u * dim + k] = (random() - 0.5) * 0.1 + normalized * 0.1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// k-means++: the first centre uniformly, each next one with probability proportional to the
|
|
112
|
+
// squared distance to the nearest centre so far. A draw that rounding carries past the last
|
|
113
|
+
// node adds no centre, as in legacy, so there may be fewer than numClusters.
|
|
114
|
+
const centres = new Float64Array(numClusters * dim);
|
|
115
|
+
const pick = Math.floor(random() * n);
|
|
116
|
+
centres.set(emb.subarray(pick * dim, (pick + 1) * dim), 0);
|
|
117
|
+
let centreCount = 1;
|
|
118
|
+
const weight = new Float64Array(n);
|
|
119
|
+
for (let k = 1; k < numClusters; k++) {
|
|
120
|
+
let total = 0;
|
|
121
|
+
for (let u = 0; u < n; u++) {
|
|
122
|
+
let nearest = Infinity;
|
|
123
|
+
for (let c = 0; c < centreCount; c++) {
|
|
124
|
+
nearest = Math.min(nearest, distance(emb, u, centres, c, dim));
|
|
125
|
+
}
|
|
126
|
+
weight[u] = nearest * nearest;
|
|
127
|
+
total += nearest * nearest;
|
|
128
|
+
}
|
|
129
|
+
let draw = random() * total;
|
|
130
|
+
for (let u = 0; u < n; u++) {
|
|
131
|
+
draw -= weight[u];
|
|
132
|
+
if (draw <= 0) {
|
|
133
|
+
centres.set(emb.subarray(u * dim, (u + 1) * dim), centreCount * dim);
|
|
134
|
+
centreCount++;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const labels = new Uint32Array(n);
|
|
141
|
+
const assign = (): void => {
|
|
142
|
+
for (let u = 0; u < n; u++) {
|
|
143
|
+
let bestCentre = 0;
|
|
144
|
+
let bestDistance = Infinity;
|
|
145
|
+
for (let c = 0; c < centreCount; c++) {
|
|
146
|
+
const d = distance(emb, u, centres, c, dim);
|
|
147
|
+
if (d < bestDistance) {
|
|
148
|
+
bestDistance = d;
|
|
149
|
+
bestCentre = c;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
labels[u] = bestCentre;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const gradient = new Float64Array(n * dim);
|
|
157
|
+
const sums = new Float64Array(numClusters * dim);
|
|
158
|
+
const sizes = new Uint32Array(numClusters);
|
|
159
|
+
let previousLoss = Infinity;
|
|
160
|
+
let loss = Infinity;
|
|
161
|
+
let iterations = 0;
|
|
162
|
+
let converged = false;
|
|
163
|
+
while (iterations < maxIterations) {
|
|
164
|
+
iterations++;
|
|
165
|
+
assign();
|
|
166
|
+
|
|
167
|
+
// Every gradient from the embeddings before this step, then one step for all.
|
|
168
|
+
gradient.fill(0);
|
|
169
|
+
for (let u = 0; u < n; u++) {
|
|
170
|
+
const row = u * dim;
|
|
171
|
+
for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
|
|
172
|
+
const other = colIdx[a] * dim;
|
|
173
|
+
for (let k = 0; k < dim; k++) {
|
|
174
|
+
gradient[row + k] += lambda * (emb[row + k] - emb[other + k]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (let k = 0; k < dim; k++) {
|
|
178
|
+
gradient[row + k] += lambda * emb[row + k];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (let i = 0; i < emb.length; i++) {
|
|
182
|
+
emb[i] -= learningRate * gradient[i];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
sums.fill(0);
|
|
186
|
+
sizes.fill(0);
|
|
187
|
+
for (let u = 0; u < n; u++) {
|
|
188
|
+
const c = labels[u];
|
|
189
|
+
for (let k = 0; k < dim; k++) {
|
|
190
|
+
sums[c * dim + k] += emb[u * dim + k];
|
|
191
|
+
}
|
|
192
|
+
sizes[c]++;
|
|
193
|
+
}
|
|
194
|
+
for (let c = 0; c < centreCount; c++) {
|
|
195
|
+
if (sizes[c] > 0) {
|
|
196
|
+
for (let k = 0; k < dim; k++) {
|
|
197
|
+
centres[c * dim + k] = sums[c * dim + k] / sizes[c];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let clustering = 0;
|
|
203
|
+
let reconstruction = 0;
|
|
204
|
+
let regularization = 0;
|
|
205
|
+
for (let u = 0; u < n; u++) {
|
|
206
|
+
clustering += distance(emb, u, centres, labels[u], dim) ** 2;
|
|
207
|
+
}
|
|
208
|
+
for (let u = 0; u < n; u++) {
|
|
209
|
+
for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
|
|
210
|
+
reconstruction += distance(emb, u, emb, colIdx[a], dim) ** 2;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const value of emb) {
|
|
214
|
+
regularization += value ** 2;
|
|
215
|
+
}
|
|
216
|
+
const current = clustering + lambda * reconstruction + lambda * regularization;
|
|
217
|
+
if (iterations > 1) {
|
|
218
|
+
previousLoss = loss;
|
|
219
|
+
}
|
|
220
|
+
loss = current;
|
|
221
|
+
if (Math.abs(previousLoss - loss) < tolerance) {
|
|
222
|
+
converged = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
assign();
|
|
227
|
+
|
|
228
|
+
return { labels, count: numClusters, embeddings: emb, dimensions: dim, loss, previousLoss, iterations, converged };
|
|
229
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { type F64, type GraphSnapshot, renumberPartition, type U32 } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
import { type LabelResult, withGroups } from "./components.js";
|
|
4
|
+
|
|
5
|
+
/** Options of the index-based TeraHAC, matching the legacy `teraHAC`. @public */
|
|
6
|
+
export interface TeraHacOptions {
|
|
7
|
+
/**
|
|
8
|
+
* How the distance between two clusters is read from their members' pairwise distances: the
|
|
9
|
+
* minimum, the maximum, the mean, or (`"ward"`) the mean of the squares; default `"average"`.
|
|
10
|
+
*/
|
|
11
|
+
readonly linkage?: "single" | "complete" | "average" | "ward" | undefined;
|
|
12
|
+
/** Stop merging at this many clusters; a positive integer. Default: merge until one is left. */
|
|
13
|
+
readonly numClusters?: number | undefined;
|
|
14
|
+
/** Stop at the first merge whose linkage distance is above this. Default: no threshold. */
|
|
15
|
+
readonly distanceThreshold?: number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Pairwise distance: hop count along out-arcs (true, the default), or 1 for an arc from the
|
|
18
|
+
* lower to the higher node index and 2 for none (false).
|
|
19
|
+
*/
|
|
20
|
+
readonly useGraphDistance?: boolean | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Result of the index-based TeraHAC: the flat clustering plus the dendrogram. Dendrogram node
|
|
25
|
+
* `i < n` is node index i; node `n + k` is the k-th join, with children `left[k]` and `right[k]`.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export interface TeraHacResult extends LabelResult {
|
|
29
|
+
/** Left child of each join (the legacy `ClusterNode.left`). */
|
|
30
|
+
readonly left: U32;
|
|
31
|
+
/** Right child of each join. */
|
|
32
|
+
readonly right: U32;
|
|
33
|
+
/** Linkage distance of each join; Infinity for the joins that tie the remaining clusters under one root. */
|
|
34
|
+
readonly distance: F64;
|
|
35
|
+
/** Node count under each join. */
|
|
36
|
+
readonly size: U32;
|
|
37
|
+
/** Joins made by distance: `distance.subarray(0, merges)` is the legacy `distances` list. */
|
|
38
|
+
readonly merges: number;
|
|
39
|
+
/** The dendrogram root: `2n - 2`, or 0 for a single node. */
|
|
40
|
+
readonly root: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The distance legacy gives an unreachable pair, so that disconnected clusters still merge last. */
|
|
44
|
+
const UNREACHABLE = 100;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* All-pairs distances into a row-major n x n matrix: BFS hop counts along out-arcs (Infinity when
|
|
48
|
+
* unreachable) or the 1 / 2 adjacency distance.
|
|
49
|
+
* @param s - The snapshot
|
|
50
|
+
* @param useGraphDistance - Which distance
|
|
51
|
+
* @returns The matrix
|
|
52
|
+
*/
|
|
53
|
+
function pairwiseDistances(s: GraphSnapshot, useGraphDistance: boolean): Float64Array {
|
|
54
|
+
const n = s.nodeCount;
|
|
55
|
+
const d = new Float64Array(n * n);
|
|
56
|
+
if (!useGraphDistance) {
|
|
57
|
+
for (let i = 0; i < n; i++) {
|
|
58
|
+
for (let j = i + 1; j < n; j++) {
|
|
59
|
+
d[i * n + j] = d[j * n + i] = s.hasArc(i, j) ? 1 : 2;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return d;
|
|
63
|
+
}
|
|
64
|
+
d.fill(Infinity);
|
|
65
|
+
const queue = new Uint32Array(n);
|
|
66
|
+
for (let source = 0; source < n; source++) {
|
|
67
|
+
const row = source * n;
|
|
68
|
+
d[row + source] = 0;
|
|
69
|
+
queue[0] = source;
|
|
70
|
+
let head = 0;
|
|
71
|
+
let tail = 1;
|
|
72
|
+
while (head < tail) {
|
|
73
|
+
const u = queue[head++];
|
|
74
|
+
const next = d[row + u] + 1;
|
|
75
|
+
for (let a = s.rowPtr[u]; a < s.rowPtr[u + 1]; a++) {
|
|
76
|
+
const v = s.colIdx[a];
|
|
77
|
+
if (d[row + v] === Infinity) {
|
|
78
|
+
d[row + v] = next;
|
|
79
|
+
queue[tail++] = v;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return d;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* TeraHAC over a snapshot: agglomerative clustering that repeatedly merges the closest pair of
|
|
89
|
+
* clusters, with the same distances, merge order and tie-breaks as the legacy `teraHAC` -- but
|
|
90
|
+
* over node indices, so the result no longer depends on what the node ids are.
|
|
91
|
+
*
|
|
92
|
+
* The linkage is kept per ordered cluster pair as an aggregate (minimum, maximum, sum of distances
|
|
93
|
+
* or sum of squared distances) and updated on each merge, instead of rescanning every member pair;
|
|
94
|
+
* the pairwise distances are integers, so the sums, and the means divided out of them, are exact.
|
|
95
|
+
* A pair's distance reads the aggregate in the orientation legacy reads it, which matters on a
|
|
96
|
+
* directed snapshot, and ties go to the pair legacy's stable sort puts first: two original nodes
|
|
97
|
+
* before any pair with a merged cluster, and among those the most recent merge last.
|
|
98
|
+
*
|
|
99
|
+
* Weights are ignored (legacy counts hops). O(n^2) memory and O(n (n + m)) for the distances; the
|
|
100
|
+
* merging keeps each cluster's closest partner and rescans a row only when that partner merges.
|
|
101
|
+
* @param s - The snapshot
|
|
102
|
+
* @param options - {@link TeraHacOptions}
|
|
103
|
+
* @returns The flat clustering and the dendrogram
|
|
104
|
+
*/
|
|
105
|
+
export function teraHAC(s: GraphSnapshot, options: TeraHacOptions = {}): TeraHacResult {
|
|
106
|
+
const { linkage = "average", numClusters, distanceThreshold, useGraphDistance = true } = options;
|
|
107
|
+
if (numClusters !== undefined && (numClusters < 1 || !Number.isInteger(numClusters))) {
|
|
108
|
+
throw new Error("numClusters must be a positive integer");
|
|
109
|
+
}
|
|
110
|
+
const n = s.nodeCount;
|
|
111
|
+
if (n === 0) {
|
|
112
|
+
throw new Error("Cannot cluster empty graph");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ponytail: dense n x n aggregate matrix, the same memory as legacy's distance matrix; a sparse
|
|
116
|
+
// candidate structure is the upgrade if graphs past ~10k nodes need this.
|
|
117
|
+
const agg = pairwiseDistances(s, useGraphDistance);
|
|
118
|
+
if (linkage === "ward") {
|
|
119
|
+
for (let i = 0; i < agg.length; i++) {
|
|
120
|
+
agg[i] *= agg[i];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const extremes = { single: Math.min, complete: Math.max, average: null, ward: null };
|
|
124
|
+
const extreme = extremes[linkage];
|
|
125
|
+
|
|
126
|
+
const joins = Math.max(0, n - 1);
|
|
127
|
+
const left = new Uint32Array(joins);
|
|
128
|
+
const right = new Uint32Array(joins);
|
|
129
|
+
const distance = new Float64Array(joins);
|
|
130
|
+
const size = new Uint32Array(joins);
|
|
131
|
+
|
|
132
|
+
// Slot i holds one active cluster: its dendrogram id, its size, and its closest partner.
|
|
133
|
+
const clusterId = new Uint32Array(n);
|
|
134
|
+
const members = new Uint32Array(n).fill(1);
|
|
135
|
+
const active = new Uint8Array(n).fill(1);
|
|
136
|
+
for (let i = 0; i < n; i++) {
|
|
137
|
+
clusterId[i] = i;
|
|
138
|
+
}
|
|
139
|
+
const orient = (x: number, y: number): boolean => {
|
|
140
|
+
// true when x is the pair's first cluster: the lower id of two originals, else the newer one
|
|
141
|
+
const a = clusterId[x];
|
|
142
|
+
const b = clusterId[y];
|
|
143
|
+
return a < n && b < n ? a < b : a > b;
|
|
144
|
+
};
|
|
145
|
+
const pairDistance = (x: number, y: number): number => {
|
|
146
|
+
const [p, q] = orient(x, y) ? [x, y] : [y, x];
|
|
147
|
+
let value = agg[p * n + q];
|
|
148
|
+
if (clusterId[p] < n && clusterId[q] < n) {
|
|
149
|
+
// Legacy reads two original nodes' distance straight from the matrix, unsquared even for ward.
|
|
150
|
+
value = linkage === "ward" ? Math.sqrt(value) : value;
|
|
151
|
+
} else if (!extreme) {
|
|
152
|
+
value /= members[p] * members[q];
|
|
153
|
+
}
|
|
154
|
+
return value === Infinity ? UNREACHABLE : value;
|
|
155
|
+
};
|
|
156
|
+
const pairOrder = (x: number, y: number): number => {
|
|
157
|
+
const a = clusterId[x];
|
|
158
|
+
const b = clusterId[y];
|
|
159
|
+
const lo = Math.min(a, b);
|
|
160
|
+
const hi = Math.max(a, b);
|
|
161
|
+
return hi < n ? lo * n + hi : n * n + hi * 2 * n + lo;
|
|
162
|
+
};
|
|
163
|
+
const best = new Int32Array(n).fill(-1);
|
|
164
|
+
const bestDistance = new Float64Array(n);
|
|
165
|
+
const bestOrder = new Float64Array(n);
|
|
166
|
+
const offer = (x: number, y: number): void => {
|
|
167
|
+
const d = pairDistance(x, y);
|
|
168
|
+
const o = pairOrder(x, y);
|
|
169
|
+
if (best[x] === -1 || d < bestDistance[x] || (d === bestDistance[x] && o < bestOrder[x])) {
|
|
170
|
+
best[x] = y;
|
|
171
|
+
bestDistance[x] = d;
|
|
172
|
+
bestOrder[x] = o;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const rescan = (x: number): void => {
|
|
176
|
+
best[x] = -1;
|
|
177
|
+
for (let y = 0; y < n; y++) {
|
|
178
|
+
if (active[y] && y !== x) {
|
|
179
|
+
offer(x, y);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
for (let x = 0; x < n; x++) {
|
|
184
|
+
rescan(x);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let remaining = n;
|
|
188
|
+
let merges = 0;
|
|
189
|
+
while (remaining > 1 && (numClusters === undefined || remaining > numClusters)) {
|
|
190
|
+
let x = -1;
|
|
191
|
+
for (let i = 0; i < n; i++) {
|
|
192
|
+
if (
|
|
193
|
+
active[i] &&
|
|
194
|
+
(x === -1 ||
|
|
195
|
+
bestDistance[i] < bestDistance[x] ||
|
|
196
|
+
(bestDistance[i] === bestDistance[x] && bestOrder[i] < bestOrder[x]))
|
|
197
|
+
) {
|
|
198
|
+
x = i;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const d = bestDistance[x];
|
|
202
|
+
if (distanceThreshold !== undefined && d > distanceThreshold) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const [keep, drop] = orient(x, best[x]) ? [x, best[x]] : [best[x], x];
|
|
206
|
+
left[merges] = clusterId[keep];
|
|
207
|
+
right[merges] = clusterId[drop];
|
|
208
|
+
distance[merges] = d;
|
|
209
|
+
size[merges] = members[keep] + members[drop];
|
|
210
|
+
for (let c = 0; c < n; c++) {
|
|
211
|
+
if (active[c] && c !== keep && c !== drop) {
|
|
212
|
+
const kc = keep * n + c;
|
|
213
|
+
const dc = drop * n + c;
|
|
214
|
+
const ck = c * n + keep;
|
|
215
|
+
const cd = c * n + drop;
|
|
216
|
+
agg[kc] = extreme ? extreme(agg[kc], agg[dc]) : agg[kc] + agg[dc];
|
|
217
|
+
agg[ck] = extreme ? extreme(agg[ck], agg[cd]) : agg[ck] + agg[cd];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
clusterId[keep] = n + merges;
|
|
221
|
+
members[keep] = size[merges];
|
|
222
|
+
active[drop] = 0;
|
|
223
|
+
merges++;
|
|
224
|
+
remaining--;
|
|
225
|
+
rescan(keep);
|
|
226
|
+
for (let c = 0; c < n; c++) {
|
|
227
|
+
if (active[c] && c !== keep) {
|
|
228
|
+
if (best[c] === keep || best[c] === drop) {
|
|
229
|
+
rescan(c);
|
|
230
|
+
} else {
|
|
231
|
+
offer(c, keep);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Tie what is left under one root, in ascending cluster id, as legacy does.
|
|
238
|
+
const rest = Array.from(clusterId.filter((_, i) => active[i] === 1)).sort((a, b) => a - b);
|
|
239
|
+
let root = rest[0];
|
|
240
|
+
const sizeOf = (c: number): number => (c < n ? 1 : size[c - n]);
|
|
241
|
+
for (let k = 1; k < rest.length; k++) {
|
|
242
|
+
const j = merges + k - 1;
|
|
243
|
+
left[j] = root;
|
|
244
|
+
right[j] = rest[k];
|
|
245
|
+
distance[j] = Infinity;
|
|
246
|
+
size[j] = sizeOf(root) + sizeOf(rest[k]);
|
|
247
|
+
root = n + j;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const cut = cutDendrogram(n, left, right, root, numClusters ?? remaining);
|
|
251
|
+
const { labels, count } = renumberPartition(cut);
|
|
252
|
+
return { ...withGroups(labels, count), left, right, distance, size, merges, root };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Legacy's flat clustering: split dendrogram nodes breadth-first from the root until splitting the
|
|
257
|
+
* next one would exceed `k` clusters, then label every node by the cluster above it.
|
|
258
|
+
* @param n - Node count
|
|
259
|
+
* @param left - Left child of each join
|
|
260
|
+
* @param right - Right child of each join
|
|
261
|
+
* @param root - The root
|
|
262
|
+
* @param k - The cluster count asked for
|
|
263
|
+
* @returns A cluster number per node index
|
|
264
|
+
*/
|
|
265
|
+
function cutDendrogram(n: number, left: U32, right: U32, root: number, k: number): U32 {
|
|
266
|
+
const kept: number[] = [];
|
|
267
|
+
const queue = [root];
|
|
268
|
+
let head = 0;
|
|
269
|
+
while (head < queue.length && kept.length < k) {
|
|
270
|
+
const c = queue[head++];
|
|
271
|
+
if (c < n || k === 1 || kept.length + (queue.length - head) + 1 >= k) {
|
|
272
|
+
kept.push(c);
|
|
273
|
+
} else {
|
|
274
|
+
queue.push(left[c - n], right[c - n]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const labels = new Uint32Array(n);
|
|
278
|
+
kept.forEach((top, label) => {
|
|
279
|
+
const stack = [top];
|
|
280
|
+
while (stack.length > 0) {
|
|
281
|
+
const c = stack.pop() ?? 0;
|
|
282
|
+
if (c < n) {
|
|
283
|
+
labels[c] = label;
|
|
284
|
+
} else {
|
|
285
|
+
stack.push(left[c - n], right[c - n]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return labels;
|
|
290
|
+
}
|