Deterministic Leiden clustering with clusterLibreville

Introduction

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.

Installation

install.packages("clusterLibreville",
                 repos = c("https://btraven00.r-universe.dev", getOption("repos")))

Building from source needs a Rust toolchain (cargo, rustc >= 1.85).

Clustering a neighbour graph

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 3000

leiden() 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.8131318

The number of threads never changes the result:

identical(leiden(g, seed = 0, threads = 1), leiden(g, seed = 0, threads = 4))
#> [1] TRUE

Higher resolutions give more, smaller communities:

vapply(c(0.5, 1, 2), function(r) max(leiden(g, resolution = r)), integer(1))
#> [1] 15 19 28

Bioconductor workflows

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 100

cluster_libreville() has igraph’s community-function interface, so it plugs into bluster’s graph-based clustering:

param <- bluster::SNNGraphParam(k = 10, cluster.fun = cluster_libreville,
                                cluster.args = list(resolution = 1, seed = 1))
table(bluster::clusterRows(pcs, param))
#> 
#>   1   2 
#> 100 100

Options

  • 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).

Session information

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