@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
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdjacencyView,
|
|
3
|
+
type GraphSnapshot,
|
|
4
|
+
INVALID_INDEX,
|
|
5
|
+
type NumericVector,
|
|
6
|
+
renumberPartition,
|
|
7
|
+
type U32,
|
|
8
|
+
} from "@graphty/graph-format";
|
|
9
|
+
|
|
10
|
+
import { type LabelResult, withGroups } from "./components.js";
|
|
11
|
+
|
|
12
|
+
/** Options of the index-based label propagation. @public */
|
|
13
|
+
export interface LabelPropagationOptions {
|
|
14
|
+
/** Work cap, in node visits per node (full-sweep equivalents); default 100. */
|
|
15
|
+
readonly maxIterations?: number | undefined;
|
|
16
|
+
/** Seed of the visit order and of the tie draws; default 42. */
|
|
17
|
+
readonly randomSeed?: number | undefined;
|
|
18
|
+
/** Sum arc weights (true, the default) or count each distinct neighbour once (false). */
|
|
19
|
+
readonly weighted?: boolean | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Result of the index-based label propagation. @public */
|
|
23
|
+
export interface LabelPropagationResult extends LabelResult {
|
|
24
|
+
/** Node visits divided by the node count, rounded up. */
|
|
25
|
+
readonly iterations: number;
|
|
26
|
+
/**
|
|
27
|
+
* True when the work queue emptied before the visit cap, which leaves every node's label
|
|
28
|
+
* dominant among its neighbours. False only says the cap was reached with nodes still queued:
|
|
29
|
+
* the labels may already be dominant.
|
|
30
|
+
*/
|
|
31
|
+
readonly converged: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Epochs run 0 .. EPOCH_LIMIT - 1 between resets of the Int32 stamp arrays. */
|
|
35
|
+
const EPOCH_LIMIT = 0x7fffffff;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* mulberry32: 32-bit state, output in [0, 1). The exact sequence is part of the result contract
|
|
39
|
+
* (design section 2.4): changing it changes every partition.
|
|
40
|
+
* @param seed - Generator seed; only its low 32 bits are used
|
|
41
|
+
* @returns The generator
|
|
42
|
+
*/
|
|
43
|
+
export function mulberry32(seed: number): () => number {
|
|
44
|
+
let a = seed >>> 0;
|
|
45
|
+
return () => {
|
|
46
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
47
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
48
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
49
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether any arc other than a self-loop carries a positive vote under the chosen weighting, which
|
|
55
|
+
* is exactly when the identity labelling is NOT dominant everywhere.
|
|
56
|
+
* @param s - The snapshot
|
|
57
|
+
* @param weights - The arc weights in use, or null when every arc votes 1
|
|
58
|
+
* @returns True when some node has a positive-weight neighbour
|
|
59
|
+
*/
|
|
60
|
+
function hasVotingArc(s: GraphSnapshot, weights: NumericVector | null): boolean {
|
|
61
|
+
for (let u = 0; u < s.nodeCount; u++) {
|
|
62
|
+
const end = s.rowPtr[u + 1];
|
|
63
|
+
for (let a = s.rowPtr[u]; a < end; a++) {
|
|
64
|
+
if (s.colIdx[a] !== u && (weights === null || weights[a] > 0)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether every node not held fixed holds a label at the largest vote among its neighbours (or has
|
|
74
|
+
* no neighbour that votes).
|
|
75
|
+
* @param tally - A tally over the voting rows
|
|
76
|
+
* @param label - The label of every node
|
|
77
|
+
* @param fixed - 1 for a node held at its label, or null when none is
|
|
78
|
+
* @returns True when no free node would move
|
|
79
|
+
*/
|
|
80
|
+
function allDominant(tally: Tally, label: U32, fixed: Uint8Array | null): boolean {
|
|
81
|
+
for (let u = 0; u < label.length; u++) {
|
|
82
|
+
if (fixed !== null && fixed[u] === 1) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const max = tally.collect(u, label);
|
|
86
|
+
if (max !== 0 && tally.voteOf(label[u]) !== max) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The per-arc weights of `view` that the votes use: the snapshot's exact f64 weights when it keeps
|
|
95
|
+
* them (graph-format keeps a role-"weight" f64 edge column whenever some weight is not f32-exact),
|
|
96
|
+
* gathered through arcToEdge; otherwise the view's own f32 arc array. Voting on the f32 values would
|
|
97
|
+
* turn 1 and 1 + 1e-9 into a tie, 1e39 into Infinity and 1e-50 into 0.
|
|
98
|
+
* @param view - The forward snapshot or its reverse view
|
|
99
|
+
* @param exact - The exact per-edge weights, or null when the f32 arc array is exact
|
|
100
|
+
* @returns The arc weights, or null when every arc weighs 1
|
|
101
|
+
*/
|
|
102
|
+
export function arcWeightsOf(view: AdjacencyView, exact: Float64Array | null): NumericVector | null {
|
|
103
|
+
if (exact === null) {
|
|
104
|
+
return view.weights;
|
|
105
|
+
}
|
|
106
|
+
const out = new Float64Array(view.arcCount);
|
|
107
|
+
for (let a = 0; a < out.length; a++) {
|
|
108
|
+
out[a] = exact[view.arcToEdge[a]];
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The exact per-edge weights of `s`: its role-"weight" f64 edge column when it keeps one, else null
|
|
115
|
+
* (the f32 arc array is then exact).
|
|
116
|
+
* @param s - The snapshot
|
|
117
|
+
* @returns The exact weights per logical edge, or null
|
|
118
|
+
*/
|
|
119
|
+
export function exactEdgeWeights(s: GraphSnapshot): Float64Array | null {
|
|
120
|
+
const shadow = s.edges.byRole("weight");
|
|
121
|
+
return shadow !== null && shadow.dtype === "f64" ? shadow.data : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The rows a node's votes are read from, with the weights they carry. A node's neighbours are its
|
|
126
|
+
* out-row and, on a directed snapshot, its in-row read from the cached reverse view: the simple
|
|
127
|
+
* symmetric view without materialising it (design section 2.3).
|
|
128
|
+
*/
|
|
129
|
+
interface VotingRows {
|
|
130
|
+
/** 1 on an undirected snapshot, 2 (out-row, in-row) on a directed one. */
|
|
131
|
+
readonly sides: number;
|
|
132
|
+
readonly rowPtrs: readonly U32[];
|
|
133
|
+
readonly colIdxs: readonly U32[];
|
|
134
|
+
/** Per-arc weights of each side, or null when every arc votes 1. */
|
|
135
|
+
readonly arcWeights: readonly (NumericVector | null)[];
|
|
136
|
+
/** The forward weights, or null; what `hasVotingArc` reads. */
|
|
137
|
+
readonly weights: NumericVector | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gather the voting rows of `s` and reject weights a vote cannot use.
|
|
142
|
+
* @param s - The snapshot
|
|
143
|
+
* @param weighted - Sum arc weights (true) or count each arc as 1 (false)
|
|
144
|
+
* @returns The rows and their weights
|
|
145
|
+
* @throws RangeError when a weight is negative, NaN or infinite
|
|
146
|
+
*/
|
|
147
|
+
function votingRows(s: GraphSnapshot, weighted: boolean): VotingRows {
|
|
148
|
+
const weights = weighted ? arcWeightsOf(s, exactEdgeWeights(s)) : null;
|
|
149
|
+
if (weights !== null) {
|
|
150
|
+
for (let a = 0; a < weights.length; a++) {
|
|
151
|
+
const w = weights[a];
|
|
152
|
+
if (!(w >= 0) || w === Infinity) {
|
|
153
|
+
throw new RangeError(`arc ${a} has weight ${w}; label propagation needs finite, non-negative weights`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const rev = s.directed ? s.reverse() : null;
|
|
158
|
+
return {
|
|
159
|
+
sides: rev === null ? 1 : 2,
|
|
160
|
+
rowPtrs: [s.rowPtr, rev === null ? s.rowPtr : rev.rowPtr],
|
|
161
|
+
colIdxs: [s.colIdx, rev === null ? s.colIdx : rev.colIdx],
|
|
162
|
+
arcWeights: [weights, weights === null || rev === null ? weights : arcWeightsOf(rev, exactEdgeWeights(s))],
|
|
163
|
+
weights,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check the options every label propagation shares.
|
|
169
|
+
* @param n - The node count
|
|
170
|
+
* @param maxIterations - The work cap
|
|
171
|
+
* @param randomSeed - The seed, or null when the variant takes none
|
|
172
|
+
* @throws RangeError for a bad option
|
|
173
|
+
*/
|
|
174
|
+
function checkOptions(n: number, maxIterations: number, randomSeed: number | null): void {
|
|
175
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 0) {
|
|
176
|
+
throw new RangeError(`maxIterations must be a non-negative integer, got ${maxIterations}`);
|
|
177
|
+
}
|
|
178
|
+
if (randomSeed !== null && !Number.isInteger(randomSeed)) {
|
|
179
|
+
throw new RangeError(`randomSeed must be a finite integer, got ${randomSeed}`);
|
|
180
|
+
}
|
|
181
|
+
if (maxIterations * n > Number.MAX_SAFE_INTEGER) {
|
|
182
|
+
throw new RangeError(`maxIterations * nodeCount (${maxIterations} * ${n}) exceeds Number.MAX_SAFE_INTEGER`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The per-node vote tally shared by the queue kernel and the synchronous passes: an accumulator
|
|
188
|
+
* per label, stamped by visit so it never needs clearing.
|
|
189
|
+
*/
|
|
190
|
+
class Tally {
|
|
191
|
+
readonly acc: Float64Array;
|
|
192
|
+
// Epoch (visit number modulo the reset below) that last wrote acc[c]. Int32 rather than Float64:
|
|
193
|
+
// half the bytes per label, which is 7-25% of the run once the array outgrows the cache at 1M
|
|
194
|
+
// nodes (design section 4). Started at -1, so label 0 is an ordinary label.
|
|
195
|
+
readonly stamp: Int32Array;
|
|
196
|
+
readonly touched: Uint32Array;
|
|
197
|
+
// weighted: false -- epoch that last counted neighbour v, so each distinct neighbour votes once
|
|
198
|
+
// however many arcs join it.
|
|
199
|
+
readonly seen: Int32Array | null;
|
|
200
|
+
epoch = -1;
|
|
201
|
+
count = 0;
|
|
202
|
+
|
|
203
|
+
constructor(
|
|
204
|
+
n: number,
|
|
205
|
+
private readonly rows: VotingRows,
|
|
206
|
+
weighted: boolean,
|
|
207
|
+
) {
|
|
208
|
+
this.acc = new Float64Array(n);
|
|
209
|
+
this.stamp = new Int32Array(n).fill(-1);
|
|
210
|
+
this.touched = new Uint32Array(n);
|
|
211
|
+
this.seen = weighted ? null : new Int32Array(n).fill(-1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Sum the votes of u's neighbours per label into `acc`, listing the labels met in `touched`.
|
|
216
|
+
* @param u - The node
|
|
217
|
+
* @param label - The label of every node
|
|
218
|
+
* @returns The largest vote, 0 when no neighbour votes
|
|
219
|
+
*/
|
|
220
|
+
collect(u: number, label: U32): number {
|
|
221
|
+
if (++this.epoch === EPOCH_LIMIT) {
|
|
222
|
+
// Reset before the epoch leaves the Int32 range: O(n) once per 2^31 - 1 visits.
|
|
223
|
+
this.stamp.fill(-1);
|
|
224
|
+
this.seen?.fill(-1);
|
|
225
|
+
this.epoch = 0;
|
|
226
|
+
}
|
|
227
|
+
const { acc, stamp, touched, seen, rows } = this;
|
|
228
|
+
const visit = this.epoch;
|
|
229
|
+
let count = 0;
|
|
230
|
+
for (let side = 0; side < rows.sides; side++) {
|
|
231
|
+
const rowPtr = rows.rowPtrs[side];
|
|
232
|
+
const colIdx = rows.colIdxs[side];
|
|
233
|
+
const w = rows.arcWeights[side];
|
|
234
|
+
const end = rowPtr[u + 1];
|
|
235
|
+
for (let a = rowPtr[u]; a < end; a++) {
|
|
236
|
+
const v = colIdx[a];
|
|
237
|
+
if (v === u) {
|
|
238
|
+
continue; // a self-loop does not vote
|
|
239
|
+
}
|
|
240
|
+
if (seen !== null) {
|
|
241
|
+
if (seen[v] === visit) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
seen[v] = visit;
|
|
245
|
+
}
|
|
246
|
+
const c = label[v];
|
|
247
|
+
const vote = w === null ? 1 : w[a];
|
|
248
|
+
if (stamp[c] !== visit) {
|
|
249
|
+
stamp[c] = visit;
|
|
250
|
+
acc[c] = vote;
|
|
251
|
+
touched[count++] = c;
|
|
252
|
+
} else {
|
|
253
|
+
acc[c] += vote;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this.count = count;
|
|
258
|
+
let max = 0;
|
|
259
|
+
for (let i = 0; i < count; i++) {
|
|
260
|
+
if (acc[touched[i]] > max) {
|
|
261
|
+
max = acc[touched[i]];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return max;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The vote label c received in the last `collect`.
|
|
269
|
+
* @param c - A label
|
|
270
|
+
* @returns Its vote, 0 when no neighbour holds it
|
|
271
|
+
*/
|
|
272
|
+
voteOf(c: number): number {
|
|
273
|
+
return this.stamp[c] === this.epoch ? this.acc[c] : 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The FLPA queue kernel over `label`, updated in place. Nodes marked in `fixed` never enter the
|
|
279
|
+
* queue, so they keep their starting label.
|
|
280
|
+
* @param s - The snapshot
|
|
281
|
+
* @param rows - Its voting rows
|
|
282
|
+
* @param weighted - Whether the rows carry weights
|
|
283
|
+
* @param label - Starting label of every node, each below nodeCount; overwritten with the result
|
|
284
|
+
* @param fixed - 1 for a node whose label never changes, or null
|
|
285
|
+
* @param maxIterations - Work cap in node visits per node
|
|
286
|
+
* @param randomSeed - Seed of the visit order and the tie draws
|
|
287
|
+
* @returns The node visits run and the nodes still queued
|
|
288
|
+
*/
|
|
289
|
+
function flpa(
|
|
290
|
+
s: GraphSnapshot,
|
|
291
|
+
rows: VotingRows,
|
|
292
|
+
weighted: boolean,
|
|
293
|
+
label: U32,
|
|
294
|
+
fixed: Uint8Array | null,
|
|
295
|
+
maxIterations: number,
|
|
296
|
+
randomSeed: number,
|
|
297
|
+
): { visits: number; size: number } {
|
|
298
|
+
const n = s.nodeCount;
|
|
299
|
+
const rand = mulberry32(randomSeed);
|
|
300
|
+
const tally = new Tally(n, rows, weighted);
|
|
301
|
+
// A ring of n + 1 slots: `queued` keeps a node in it at most once.
|
|
302
|
+
const capacity = n + 1;
|
|
303
|
+
const queue = new Uint32Array(capacity);
|
|
304
|
+
const queued = new Uint8Array(n);
|
|
305
|
+
let size = 0;
|
|
306
|
+
for (let i = 0; i < n; i++) {
|
|
307
|
+
if (fixed === null || fixed[i] === 0) {
|
|
308
|
+
queue[size++] = i;
|
|
309
|
+
queued[i] = 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (let i = size - 1; i >= 1; i--) {
|
|
313
|
+
const j = Math.floor(rand() * (i + 1));
|
|
314
|
+
const t = queue[i];
|
|
315
|
+
queue[i] = queue[j];
|
|
316
|
+
queue[j] = t;
|
|
317
|
+
}
|
|
318
|
+
let head = 0;
|
|
319
|
+
let tail = size;
|
|
320
|
+
const maxVisits = maxIterations * n;
|
|
321
|
+
let visits = 0;
|
|
322
|
+
const { touched, acc } = tally;
|
|
323
|
+
while (size > 0 && visits < maxVisits) {
|
|
324
|
+
const u = queue[head];
|
|
325
|
+
head = head + 1 === capacity ? 0 : head + 1;
|
|
326
|
+
size--;
|
|
327
|
+
queued[u] = 0;
|
|
328
|
+
visits++;
|
|
329
|
+
const max = tally.collect(u, label);
|
|
330
|
+
if (max === 0) {
|
|
331
|
+
continue; // no positive-weight neighbour: keep the label
|
|
332
|
+
}
|
|
333
|
+
// Reservoir draw over the labels at the maximum: the k-th replaces the pick with
|
|
334
|
+
// probability 1/k, so a single dominant label consumes no random number.
|
|
335
|
+
let pick = 0;
|
|
336
|
+
let tied = 0;
|
|
337
|
+
for (let i = 0; i < tally.count; i++) {
|
|
338
|
+
const c = touched[i];
|
|
339
|
+
if (acc[c] === max) {
|
|
340
|
+
tied++;
|
|
341
|
+
if (tied === 1 || rand() * tied < 1) {
|
|
342
|
+
pick = c;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (pick === label[u]) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
label[u] = pick;
|
|
350
|
+
// A neighbour already holding the new label can only have been strengthened by the move.
|
|
351
|
+
// Both rows, so on a directed snapshot the nodes that read u are requeued too.
|
|
352
|
+
for (let side = 0; side < rows.sides; side++) {
|
|
353
|
+
const rowPtr = rows.rowPtrs[side];
|
|
354
|
+
const colIdx = rows.colIdxs[side];
|
|
355
|
+
const end = rowPtr[u + 1];
|
|
356
|
+
for (let a = rowPtr[u]; a < end; a++) {
|
|
357
|
+
const v = colIdx[a];
|
|
358
|
+
if (queued[v] === 0 && label[v] !== pick && (fixed === null || fixed[v] === 0)) {
|
|
359
|
+
queued[v] = 1;
|
|
360
|
+
queue[tail] = v;
|
|
361
|
+
tail = tail + 1 === capacity ? 0 : tail + 1;
|
|
362
|
+
size++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { visits, size };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Community detection by fast label propagation (FLPA; Traag and Subelj, Sci. Rep. 13:2701, 2023,
|
|
372
|
+
* Algorithm 3), the queue-driven form of Raghavan, Albert and Kumara's asynchronous label
|
|
373
|
+
* propagation.
|
|
374
|
+
*
|
|
375
|
+
* Every node starts in its own community. Nodes are visited from a queue that starts as a seeded
|
|
376
|
+
* shuffle of all of them; a visited node adopts the label with the largest summed arc weight among
|
|
377
|
+
* its neighbours, drawing uniformly at random among tied labels (its current label included, with
|
|
378
|
+
* no priority). When its label changes, every neighbour holding a different label is queued again.
|
|
379
|
+
* The run ends when the queue empties -- then every node's label is dominant among its neighbours
|
|
380
|
+
* and `converged` is true -- or after `maxIterations * nodeCount` node visits.
|
|
381
|
+
*
|
|
382
|
+
* Conventions: self-loops are skipped; parallel arcs are summed; on a directed snapshot a node's
|
|
383
|
+
* neighbours are its out-arcs AND its in-arcs, so a reciprocal pair counts twice. With
|
|
384
|
+
* `weighted: false` each distinct neighbour votes once. A negative, NaN or infinite weight throws.
|
|
385
|
+
* Votes use the exact f64 weights when the snapshot keeps them (as `toSnapshot` does for a legacy
|
|
386
|
+
* graph whose weights are not all f32-exact), so a weight rounded in the f32 arc array never
|
|
387
|
+
* decides a vote.
|
|
388
|
+
*
|
|
389
|
+
* One `randomSeed` gives one result, bit for bit. The partitions differ from the legacy
|
|
390
|
+
* `labelPropagation` for the same seed: the random stream and the stop rule are different
|
|
391
|
+
* (design `design/algorithms/label-propagation-indexed-port-design.md`, section 3).
|
|
392
|
+
* @param s - Any snapshot
|
|
393
|
+
* @param options - Work cap, seed and weighting
|
|
394
|
+
* @returns The partition, the full-sweep equivalents run, and whether the work queue emptied
|
|
395
|
+
* @public
|
|
396
|
+
*/
|
|
397
|
+
export function labelPropagation(s: GraphSnapshot, options: LabelPropagationOptions = {}): LabelPropagationResult {
|
|
398
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
399
|
+
const randomSeed = options.randomSeed ?? 42;
|
|
400
|
+
const weighted = options.weighted ?? true;
|
|
401
|
+
const n = s.nodeCount;
|
|
402
|
+
checkOptions(n, maxIterations, randomSeed);
|
|
403
|
+
const rows = votingRows(s, weighted);
|
|
404
|
+
const label = new Uint32Array(n);
|
|
405
|
+
for (let i = 0; i < n; i++) {
|
|
406
|
+
label[i] = i;
|
|
407
|
+
}
|
|
408
|
+
if (n === 0 || maxIterations === 0) {
|
|
409
|
+
const { labels, count } = renumberPartition(label);
|
|
410
|
+
return { ...withGroups(labels, count), iterations: 0, converged: !hasVotingArc(s, rows.weights) };
|
|
411
|
+
}
|
|
412
|
+
const { visits, size } = flpa(s, rows, weighted, label, null, maxIterations, randomSeed);
|
|
413
|
+
const { labels, count } = renumberPartition(label);
|
|
414
|
+
return { ...withGroups(labels, count), iterations: Math.ceil(visits / n), converged: size === 0 };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Semi-supervised label propagation: the FLPA kernel of {@link labelPropagation} with some nodes
|
|
419
|
+
* held at a given label (the `initial` / `fixed` inputs of igraph's `community_label_propagation`).
|
|
420
|
+
*
|
|
421
|
+
* A seeded node keeps its label for the whole run and is never visited; every other node starts in
|
|
422
|
+
* its own community and moves as in `labelPropagation`. Seeds that share a label share one community
|
|
423
|
+
* from the start, and seeds with different labels are never merged, so in the result two seeds
|
|
424
|
+
* carry the same label exactly when their seed labels are equal. The result is renumbered to
|
|
425
|
+
* `0..count-1` in first-seen node order like every other partition here, so a seed label's VALUE is
|
|
426
|
+
* not kept: read a seed's community through `labels[seedNode]`. With no seed at all this is
|
|
427
|
+
* `labelPropagation` with the same seed and options, bit for bit.
|
|
428
|
+
*
|
|
429
|
+
* `converged` is true when the work queue emptied, or when `maxIterations` is 0 and the starting
|
|
430
|
+
* labels already are dominant: every unseeded node's label is dominant among its neighbours.
|
|
431
|
+
* Seeded nodes are not held to that.
|
|
432
|
+
* @param s - Any snapshot
|
|
433
|
+
* @param seeds - One entry per node: its fixed label, any value but `INVALID_INDEX`, or
|
|
434
|
+
* `INVALID_INDEX` for a node free to move
|
|
435
|
+
* @param options - Work cap, seed of the visit order and weighting
|
|
436
|
+
* @returns The partition, the full-sweep equivalents run, and whether the work queue emptied
|
|
437
|
+
* @throws RangeError when `seeds` is not one entry per node, or for a bad option or weight
|
|
438
|
+
* @public
|
|
439
|
+
*/
|
|
440
|
+
export function labelPropagationSemiSupervised(
|
|
441
|
+
s: GraphSnapshot,
|
|
442
|
+
seeds: U32,
|
|
443
|
+
options: LabelPropagationOptions = {},
|
|
444
|
+
): LabelPropagationResult {
|
|
445
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
446
|
+
const randomSeed = options.randomSeed ?? 42;
|
|
447
|
+
const weighted = options.weighted ?? true;
|
|
448
|
+
const n = s.nodeCount;
|
|
449
|
+
if (seeds.length !== n) {
|
|
450
|
+
throw new RangeError(`seeds has ${seeds.length} entries; the snapshot has ${n} nodes`);
|
|
451
|
+
}
|
|
452
|
+
checkOptions(n, maxIterations, randomSeed);
|
|
453
|
+
const rows = votingRows(s, weighted);
|
|
454
|
+
// The kernel's labels are node indices, so a seed label becomes the index of the first node
|
|
455
|
+
// seeded with it: a free node's own index can never collide with it.
|
|
456
|
+
const label = new Uint32Array(n);
|
|
457
|
+
const fixed = new Uint8Array(n);
|
|
458
|
+
const firstWith = new Map<number, number>();
|
|
459
|
+
for (let u = 0; u < n; u++) {
|
|
460
|
+
const seed = seeds[u];
|
|
461
|
+
if (seed === INVALID_INDEX) {
|
|
462
|
+
label[u] = u;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
let first = firstWith.get(seed);
|
|
466
|
+
if (first === undefined) {
|
|
467
|
+
first = u;
|
|
468
|
+
firstWith.set(seed, u);
|
|
469
|
+
}
|
|
470
|
+
label[u] = first;
|
|
471
|
+
fixed[u] = 1;
|
|
472
|
+
}
|
|
473
|
+
const { visits, size } = flpa(s, rows, weighted, label, fixed, maxIterations, randomSeed);
|
|
474
|
+
// At maxIterations 0 nothing was visited, so the queue says nothing: check the labels, as
|
|
475
|
+
// labelPropagation does.
|
|
476
|
+
const converged = size === 0 || (maxIterations === 0 && allDominant(new Tally(n, rows, weighted), label, fixed));
|
|
477
|
+
const { labels, count } = renumberPartition(label);
|
|
478
|
+
return { ...withGroups(labels, count), iterations: n === 0 ? 0 : Math.ceil(visits / n), converged };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Options of the synchronous label propagation. @public */
|
|
482
|
+
export interface SynchronousLabelPropagationOptions {
|
|
483
|
+
/** Cap on synchronous passes over every node; default 100. */
|
|
484
|
+
readonly maxIterations?: number | undefined;
|
|
485
|
+
/** Sum arc weights (true, the default) or count each distinct neighbour once (false). */
|
|
486
|
+
readonly weighted?: boolean | undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Label propagation in synchronous passes: every node reads its neighbours' labels from the
|
|
491
|
+
* previous pass, and all nodes move at once. Deterministic, with no random stream.
|
|
492
|
+
*
|
|
493
|
+
* A node keeps its label when that label is dominant among its neighbours; otherwise it takes the
|
|
494
|
+
* LOWEST label with the largest summed vote. Synchronous updates alone make two neighbours trade
|
|
495
|
+
* labels for ever (on a single edge a-b, a takes b's label while b takes a's), so passes alternate
|
|
496
|
+
* a swap guard: an even pass (the first is pass 0) lets a node move only to a higher label, an odd
|
|
497
|
+
* pass only to a lower one (the up/down rule of cuGraph's Louvain move phase). The run stops after
|
|
498
|
+
* two passes in a row with no move, one up and one down -- then every node's label is dominant and
|
|
499
|
+
* `converged` is true -- or after `maxIterations` passes.
|
|
500
|
+
*
|
|
501
|
+
* The guard stops the single-edge swap but not every cycle: on some weighted graphs a node climbs to
|
|
502
|
+
* a higher label on each up pass and falls back on each down pass. When a pass returns the labels
|
|
503
|
+
* of two passes before, the run can only repeat itself, so it stops there with `converged` false;
|
|
504
|
+
* the result is then the same for any larger `maxIterations`. Longer cycles run to the cap.
|
|
505
|
+
*
|
|
506
|
+
* This is what the legacy `labelPropagationAsync` does, despite its name, with the swap guard it
|
|
507
|
+
* lacks and the tie rule applied to the finished tally rather than to a running one. Conventions
|
|
508
|
+
* match {@link labelPropagation}: self-loops are skipped, parallel arcs summed, a directed snapshot
|
|
509
|
+
* reads out- and in-neighbours, weights are the exact f64 ones when the snapshot keeps them, and a
|
|
510
|
+
* negative, NaN or infinite weight throws.
|
|
511
|
+
* @param s - Any snapshot
|
|
512
|
+
* @param options - Pass cap and weighting
|
|
513
|
+
* @returns The partition, the passes run, and whether every label ended dominant
|
|
514
|
+
* @public
|
|
515
|
+
*/
|
|
516
|
+
export function labelPropagationSynchronous(
|
|
517
|
+
s: GraphSnapshot,
|
|
518
|
+
options: SynchronousLabelPropagationOptions = {},
|
|
519
|
+
): LabelPropagationResult {
|
|
520
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
521
|
+
const weighted = options.weighted ?? true;
|
|
522
|
+
const n = s.nodeCount;
|
|
523
|
+
checkOptions(n, maxIterations, null);
|
|
524
|
+
const rows = votingRows(s, weighted);
|
|
525
|
+
const tally = new Tally(n, rows, weighted);
|
|
526
|
+
const { touched, acc } = tally;
|
|
527
|
+
let label = new Uint32Array(n);
|
|
528
|
+
let next = new Uint32Array(n);
|
|
529
|
+
// The labels two passes back, to catch a period-2 cycle.
|
|
530
|
+
let before = new Uint32Array(n).fill(INVALID_INDEX);
|
|
531
|
+
for (let i = 0; i < n; i++) {
|
|
532
|
+
label[i] = i;
|
|
533
|
+
}
|
|
534
|
+
let passes = 0;
|
|
535
|
+
let quiet = 0;
|
|
536
|
+
let cycling = false;
|
|
537
|
+
while (quiet < 2 && passes < maxIterations && !cycling) {
|
|
538
|
+
const up = passes % 2 === 0;
|
|
539
|
+
passes++;
|
|
540
|
+
let moved = false;
|
|
541
|
+
for (let u = 0; u < n; u++) {
|
|
542
|
+
const current = label[u];
|
|
543
|
+
next[u] = current;
|
|
544
|
+
const max = tally.collect(u, label);
|
|
545
|
+
if (max === 0 || tally.voteOf(current) === max) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
let best = INVALID_INDEX;
|
|
549
|
+
for (let i = 0; i < tally.count; i++) {
|
|
550
|
+
const c = touched[i];
|
|
551
|
+
if (acc[c] === max && c < best) {
|
|
552
|
+
best = c;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (up ? best > current : best < current) {
|
|
556
|
+
next[u] = best;
|
|
557
|
+
moved = true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
cycling = moved && sameLabels(next, before);
|
|
561
|
+
[before, label, next] = [label, next, before];
|
|
562
|
+
quiet = moved ? 0 : quiet + 1;
|
|
563
|
+
}
|
|
564
|
+
const converged = quiet >= 2 || allDominant(tally, label, null);
|
|
565
|
+
const { labels, count } = renumberPartition(label);
|
|
566
|
+
return { ...withGroups(labels, count), iterations: passes, converged };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Whether two label arrays are equal.
|
|
571
|
+
* @param a - Labels
|
|
572
|
+
* @param b - Labels of the same length
|
|
573
|
+
* @returns True when every entry matches
|
|
574
|
+
*/
|
|
575
|
+
function sameLabels(a: U32, b: U32): boolean {
|
|
576
|
+
for (let i = 0; i < a.length; i++) {
|
|
577
|
+
if (a[i] !== b[i]) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
}
|