Leiden clustering of a cell-cell neighbour graph is a standard step
of single-cell analysis. Most implementations are either sequential or,
when parallel, nondeterministic: the labels change with the number of
threads. clusterLibreville is a parallel Leiden whose
result depends only on the graph, the options and the seed. The same
seed gives the same labels on a laptop and on a many-core server.
install.packages("clusterLibreville",
repos = c("https://btraven00.r-universe.dev", getOption("repos")))Building from source needs a Rust toolchain (cargo,
rustc >= 1.85).
The package ships a small real graph: scanpy kNN connectivities of 3000 PBMCs (Hao et al. 2021), as a symmetric sparse matrix.
library(clusterLibreville)
f <- system.file("extdata", "hao3k.mtx", package = "clusterLibreville")
g <- Matrix::readMM(f)
dim(g)
#> [1] 3000 3000leiden() returns one integer label per vertex; community
1 is the largest.
labels <- leiden(g, resolution = 1, seed = 0)
table(labels)
#> labels
#> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#> 484 458 294 292 274 247 212 212 182 148 95 29 27 14 11 11 7 2 1
modularity(g, labels)
#> [1] 0.8131318The number of threads never changes the result:
Higher resolutions give more, smaller communities:
On a SingleCellExperiment, leiden()
clusters the shared nearest neighbour graph of a reduced dimension
(built by bluster::makeSNNGraph()) and returns a factor,
ready for colLabels():
library(SingleCellExperiment)
#> Loading required package: SummarizedExperiment
#> Loading required package: MatrixGenerics
#> Loading required package: matrixStats
#>
#> Attaching package: 'MatrixGenerics'
#> The following objects are masked from 'package:matrixStats':
#>
#> colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
#> colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
#> colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
#> colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
#> colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
#> colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
#> colWeightedMeans, colWeightedMedians, colWeightedSds,
#> colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
#> rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
#> rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
#> rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
#> rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
#> rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
#> rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
#> rowWeightedSds, rowWeightedVars
#> Loading required package: GenomicRanges
#> Loading required package: stats4
#> Loading required package: BiocGenerics
#> Loading required package: generics
#>
#> Attaching package: 'generics'
#> The following objects are masked from 'package:base':
#>
#> as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
#> setequal, union
#>
#> Attaching package: 'BiocGenerics'
#> The following objects are masked from 'package:stats':
#>
#> IQR, mad, sd, var, xtabs
#> The following object is masked from 'package:utils':
#>
#> data
#> The following objects are masked from 'package:base':
#>
#> anyDuplicated, aperm, append, as.data.frame, basename, cbind,
#> colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find,
#> get, grep, grepl, is.unsorted, lapply, Map, mapply, match, mget,
#> order, paste, pmax, pmax.int, pmin, pmin.int, Position, rank,
#> rbind, Reduce, rownames, sapply, saveRDS, scale, sequence, table,
#> tapply, transform, unique, unsplit, which.max, which.min
#> Loading required package: S4Vectors
#>
#> Attaching package: 'S4Vectors'
#> The following object is masked from 'package:utils':
#>
#> findMatches
#> The following objects are masked from 'package:base':
#>
#> expand.grid, I, unname
#> Loading required package: IRanges
#> Loading required package: Seqinfo
#> Loading required package: Biobase
#> Welcome to Bioconductor
#>
#> Vignettes contain introductory material; view with
#> 'browseVignettes()'. To cite Bioconductor, see
#> 'citation("Biobase")', and for packages 'citation("pkgname")'.
#>
#> Attaching package: 'Biobase'
#> The following object is masked from 'package:MatrixGenerics':
#>
#> rowMedians
#> The following objects are masked from 'package:matrixStats':
#>
#> anyMissing, rowMedians
set.seed(1)
pcs <- rbind(matrix(rnorm(1000, 0), ncol = 10),
matrix(rnorm(1000, 3), ncol = 10))
sce <- SingleCellExperiment(assays = list(counts = matrix(0, 5, 200)),
reducedDims = list(PCA = pcs))
colLabels(sce) <- leiden(sce, use.dimred = "PCA", k = 10, seed = 1)
table(colLabels(sce))
#>
#> 1 2
#> 100 100cluster_libreville() has igraph’s community-function
interface, so it plugs into bluster’s graph-based clustering:
resolution: the modularity resolution (gamma).seed: a non-negative whole number; the only source of
randomness.n_iterations: Leiden iterations; -1 (the
default) iterates until an iteration changes nothing.theta: randomness of the refinement step.min_gain: with n_iterations = -1, also
stop once an iteration gains less modularity than this.threads: worker threads (default: all cores); does not
change the result.details = TRUE: also return how the run ended
(iterations, whether it converged, why it stopped, modularity).sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] SingleCellExperiment_1.35.2 SummarizedExperiment_1.43.0
#> [3] Biobase_2.73.2 GenomicRanges_1.65.4
#> [5] Seqinfo_1.3.2 IRanges_2.47.5
#> [7] S4Vectors_0.51.10 BiocGenerics_0.59.12
#> [9] generics_0.1.4 MatrixGenerics_1.25.0
#> [11] matrixStats_1.5.0 clusterLibreville_0.99.0
#> [13] BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 SparseArray_1.13.4 lattice_0.23-1
#> [4] magrittr_2.0.5 digest_0.6.39 evaluate_1.0.5
#> [7] grid_4.6.1 fastmap_1.2.0 jsonlite_2.0.0
#> [10] Matrix_1.7-6 BiocManager_1.30.27 codetools_0.2-20
#> [13] jquerylib_0.1.4 abind_1.4-8 cli_3.6.6
#> [16] rlang_1.3.0 XVector_0.53.0 BiocNeighbors_2.7.3
#> [19] cachem_1.1.0 DelayedArray_0.39.8 yaml_2.3.12
#> [22] otel_0.2.0 S4Arrays_1.13.2 tools_4.6.1
#> [25] parallel_4.6.1 BiocParallel_1.47.0 buildtools_1.0.0
#> [28] R6_2.6.1 lifecycle_1.0.5 bluster_1.23.1
#> [31] cluster_2.1.8.3 pkgconfig_2.0.3 bslib_0.12.0
#> [34] Rcpp_1.1.2 xfun_0.62 sys_3.4.3
#> [37] knitr_1.52 htmltools_0.5.9 igraph_2.3.4
#> [40] rmarkdown_2.32 maketools_1.3.2 compiler_4.6.1