Introduction

anglemania is a feature selection package that extracts genes from multi-batch scRNA-seq experiments for downstream dataset integration. The goal is to select genes that carry high biological information and low technical noise between the batches. Those genes are extracted from gene pairs that have an invariant and extremely narrow or wide angle between their expression vectors. Conventionally, highly-variable genes (HVGs) or sometimes all genes are used for integration tasks (https://www.nature.com/articles/s41592-021-01336-8). While HVGs are a great and easy way to reduce the noise ands dimensionality of the data, we hypothesize that there are better ways to select genes specifically for integration tasks. HVGs are sensitive to batch effects because the variance is a function of both the technical and biological variance. anglemania improves conventional usage of HVGs for integration tasks, especially when the transcriptional difference between cell types or cell states is subtle (showcased here with de.facLoc and de.facScale set to 0.1, which results in mild differences between “Groups”). The package can be used on top of SingleCellExperiment or Seurat objects.

Under the hood, anglemania works with file-backed big matrices (FBMs) from the bigstatsr package for fast and memory efficient computation.

Simulation

We simulate a scRNA-seq dataset using Splatter with 4 batches and 3 cell types with subtle differences between cell types and rather big batch effects.

batch.facLoc <- 0.3
de.facLoc <- 0.1
nBatches <- 4
nGroups <- 3
nGenes <- 5000
groupCells <- 300

sce_raw <- splatSimulate(
    batchCells = rep(groupCells * nGroups, nBatches),
    batch.facLoc = batch.facLoc,
    group.prob = rep(1 / nGroups, nGroups),
    nGenes = nGenes,
    batch.facScale = 0.1,
    method = "groups",
    verbose = FALSE,
    out.prob = 0.001,
    de.prob = 0.1, # mild
    de.facLoc = de.facLoc,
    de.facScale = 0.1,
    bcv.common = 0.1,
    seed = 42
)
sce <- sce_raw
assays(sce)
## List of length 6
## names(6): BatchCellMeans BaseCellMeans BCV CellMeans TrueCounts counts

Unintegrated data

Here we perform a standard workflow on the unintegrated data. When we perform clustering on the unintegrated data and visualize it in a UMAP, we can see that the clusters are driven by batch effects rather than cell types.

sce_unintegrated <- sce
# Normalization.
sce_unintegrated <- logNormCounts(sce_unintegrated)

# Feature selection.
dec <- modelGeneVar(sce_unintegrated)
hvg <- getTopHVGs(dec, prop = 0.1)

# PCA.
set.seed(1234)
sce_unintegrated <- scater::runPCA(
    sce_unintegrated,
    ncomponents = 50,
    subset_row = hvg
)

# Clustering.
colLabels(sce_unintegrated) <- clusterCells(sce_unintegrated,
    use.dimred = "PCA",
    BLUSPARAM = NNGraphParam(cluster.fun = "louvain")
)

# Visualization.
sce_unintegrated <- scater::runUMAP(sce_unintegrated, dimred = "PCA")
patchwork::wrap_plots(
    plotUMAP(sce_unintegrated, colour_by = "Batch") +
        ggtitle("Unintegrated data, colored by Batch"),
    plotUMAP(sce_unintegrated, colour_by = "Group") +
        ggtitle("Unintegrated data, colored by Group")
)
UMAPs of unintegrated data, colored by Batch and Group. The clusters are driven by batch effects.

UMAPs of unintegrated data, colored by Batch and Group. The clusters are driven by batch effects.

anglemania

anglemania works on a SingleCellExperiment object. The function has a few important arguments: - batch_key: the column in the metadata of the SingleCellExperiment object that indicates which batch the cells belong to. This is required to distinguish between batches, because we compute the angle between gene pairs for each batch. - method: either cosine, spearman or diem - this is the method by which the relationship of the gene pairs is measured. Default is cosine, which is the cosine similarity between the expression vectors of the gene pairs. - zscore_mean_threshold: We compute a mean of the zscore of the relationship between a gene pair, and then we set a minimal cutoff for the (absolute) mean zscore. A cutoff of 2 means that the filtered gene pairs have a relationship, e.g. cosine similarity, that is 2 standard deviations away from the mean of all cosine similarities from this dataset. A higher value means a more stringent cutoff. - zscore_sn_threshold: The SNR or signal-to-noise ratio measures the invariance of the relationship of the relationship between the gene pair. A high SN ratio means that the relationship is constant over multiple batches. - max_n_genes: you can specify a maximum number of extracted genes. They are sorted by decreasing mean zscore after passing the thresholds.

## DataFrame with 6 rows and 4 columns
##              Cell       Batch    Group ExpLibSize
##       <character> <character> <factor>  <numeric>
## Cell1       Cell1      Batch1   Group1    46898.1
## Cell2       Cell2      Batch1   Group1    54688.2
## Cell3       Cell3      Batch1   Group2    52027.9
## Cell4       Cell4      Batch1   Group1    52319.5
## Cell5       Cell5      Batch1   Group3    37774.7
## Cell6       Cell6      Batch1   Group3    58112.3
batch_key <- "Batch"
sce <- anglemania(
    sce,
    batch_key = batch_key,
    zscore_mean_threshold = 2,
    zscore_sn_threshold = 2
)

extract the anglemania genes from the SCE object

anglemania_genes <- get_anglemania_genes(sce)
head(anglemania_genes)
## [1] "Gene394"  "Gene3527" "Gene71"   "Gene5000" "Gene1055" "Gene4201"
length(anglemania_genes)
## [1] 1496

select_genes

once anglemania was run on the SCE, you can adjust the initial zscore mean and zscore SNR thresholds by using the select_genes() function

# If you think the number of selected genes is
# too high or low you can adjust the thresholds:
sce <- select_genes(sce,
    zscore_mean_threshold = 2.5,
    zscore_sn_threshold = 2.5
)
# Inspect the anglemania genes
anglemania_genes <- get_anglemania_genes(sce)
head(anglemania_genes)
## [1] "Gene394"  "Gene3527" "Gene71"   "Gene5000" "Gene1055" "Gene4201"
length(anglemania_genes) # 306 genes are selected with these thresholds
## [1] 306

MNN integration

The anglemania genes can now be used for downstream integration algorithms such as MNN. We compare the integration results using the anglemania genes with the results using 300 and the standard 2000 HVGs. ## HVGs ### 300 HVGs

hvg_300 <- sce %>%
    scater::logNormCounts() %>%
    modelGeneVar(block = colData(sce)[[batch_key]]) %>%
    getTopHVGs(n = 300)

barcodes_by_batch <- split(rownames(colData(sce)), colData(sce)[[batch_key]])
sce_list <- lapply(barcodes_by_batch, function(x) sce[, x])
sce_mnn <- sce %>%
    scater::logNormCounts()
sce_mnn <- batchelor::fastMNN(
    sce_mnn,
    subset.row = hvg_300,
    k = 20,
    batch = factor(colData(sce_mnn)[[batch_key]]),
    d = 50
)
reducedDim(sce, "MNN_hvg_300") <- reducedDim(sce_mnn, "corrected")
sce <- scater::runUMAP(sce, dimred = "MNN_hvg_300", name = "umap_MNN_hvg_300")
# k is the number of nearest neighbours to consider when identifying MNNs

2000 HVGs

hvg_2000 <- sce %>%
    scater::logNormCounts() %>%
    modelGeneVar(block = colData(sce)[[batch_key]]) %>%
    getTopHVGs(n = 2000)

barcodes_by_batch <- split(rownames(colData(sce)), colData(sce)[[batch_key]])
sce_list <- lapply(barcodes_by_batch, function(x) sce[, x])
sce_mnn <- sce %>%
    scater::logNormCounts()
sce_mnn <- batchelor::fastMNN(
    sce_mnn,
    subset.row = hvg_2000,
    k = 20,
    batch = factor(colData(sce_mnn)[[batch_key]]),
    d = 50
)
reducedDim(sce, "MNN_hvg_2000") <- reducedDim(sce_mnn, "corrected")
sce <- scater::runUMAP(sce, dimred = "MNN_hvg_2000", name = "umap_MNN_hvg_2000")

anglemania genes

sce_mnn <- sce %>%
    scater::logNormCounts()
sce_mnn <- batchelor::fastMNN(
    sce_mnn,
    subset.row = anglemania_genes,
    k = 20,
    batch = factor(colData(sce_mnn)[[batch_key]]),
    d = 50
)
reducedDim(sce, "MNN_anglemania") <- reducedDim(sce_mnn, "corrected")
sce <- scater::runUMAP(
    sce,
    dimred = "MNN_anglemania",
    name = "umap_MNN_anglemania"
)

Plot

UMAP embeddings

We can see from the UMAPs that anglemania genes yield the best integration in terms of clustering by cell type and mixing the batches. The goal of an integration and subsequent clustering should be to have low intra cluster variance and high inter cluster variance. This is at least true for most downstream scRNA-seq analyses where the goal is to e.g. differentiate between cell types or cell states and annotate these.

# Use wrap_plots
patchwork::wrap_plots(
    plotReducedDim(sce, colour_by = "Batch", dimred = "umap_MNN_anglemania") +
        ggtitle("MNN integration using anglemania genes, colored by Batch"),
    plotReducedDim(sce, colour_by = "Group", dimred = "umap_MNN_anglemania") +
        ggtitle("MNN integration using anglemania genes, colored by Group"),
    plotReducedDim(sce, colour_by = "Batch", dimred = "umap_MNN_hvg_300") +
        ggtitle("MNN integration using top 300 HVGs, colored by Batch"),
    plotReducedDim(sce, colour_by = "Group", dimred = "umap_MNN_hvg_300") +
        ggtitle("MNN integration using top 300 HVGs, colored by Group"),
    plotReducedDim(sce, colour_by = "Batch", dimred = "umap_MNN_hvg_2000") +
        ggtitle("MNN integration using top 2000 HVGs, colored by Batch"),
    plotReducedDim(sce, colour_by = "Group", dimred = "umap_MNN_hvg_2000") +
        ggtitle("MNN integration using top 2000 HVGs, colored by Group"),
    ncol = 2
)
UMAPs of MNN integrated data. Comparison of UMAP embeddings of integrated data using anglemania genes, top 300 HVGs and top 2000 HVGs.

UMAPs of MNN integrated data. Comparison of UMAP embeddings of integrated data using anglemania genes, top 300 HVGs and top 2000 HVGs.

Overlap

upsetr_df <- fromList(
    list(
        anglemania = anglemania_genes,
        hvg_300 = hvg_300,
        hvg_2000 = hvg_2000
    )
)
upset(upsetr_df, text.scale = 2)
Overlap of selected genes. Additionally, we check the overlap of the anglemania genes with the HVGs. About 33 of the 306 anglemania genes are also found in the top 300 HVGs, and about 179 of the 306 anglemania genes are also found in the top 2000 HVGs.

Overlap of selected genes. Additionally, we check the overlap of the anglemania genes with the HVGs. About 33 of the 306 anglemania genes are also found in the top 300 HVGs, and about 179 of the 306 anglemania genes are also found in the top 2000 HVGs.

Seurat

When using Seurat, the easiest approach is to create an SCE from the counts and metadata of the SeuratObject and then run anglemania on it and optionally save those genes as the VariableFeatures of the SeuratObject.

se <- CreateSeuratObject(
    counts = counts(sce_raw),
    meta.data = as.data.frame(colData(sce_raw))
)
## Warning: Data is of class matrix. Coercing to dgCMatrix.
se
## An object of class Seurat 
## 5000 features across 3600 samples within 1 assay 
## Active assay: RNA (5000 features, 0 variable features)
##  1 layer present: counts
anglemania_genes <- se |>
    as.SingleCellExperiment(assay = "RNA") |>
    anglemania(
        batch_key = "Batch",
        zscore_mean_threshold = 2,
        zscore_sn_threshold = 2
    ) |>
    get_anglemania_genes()
## Warning: `PackageCheck()` was deprecated in SeuratObject 5.0.0.
##  Please use `rlang::check_installed()` instead.
##  The deprecated feature was likely used in the Seurat package.
##   Please report the issue at <https://github.com/satijalab/seurat/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: The `slot` argument of `GetAssayData()` is deprecated as of SeuratObject 5.0.0.
##  Please use the `layer` argument instead.
##  The deprecated feature was likely used in the Seurat package.
##   Please report the issue at <https://github.com/satijalab/seurat/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Layer 'data' is empty
## Warning: Layer 'scale.data' is empty

Integration

Now you can also just use the anglemania genes for other integration algorithms When finding the integration anchors, by default highly variable genes are used. Using the anchor.features argument, you can specify the genes to use for finding the integration anchors, in our case we are gonna use the anglemania genes.

# Split by batch
seurat_list <- SplitObject(se, split.by = "Batch")
seurat_list <- lapply(seurat_list, NormalizeData)
## Normalizing layer: counts
## Normalizing layer: counts
## Normalizing layer: counts
## Normalizing layer: counts
# if the Seurat FindIntegrationAnchors() function does not work,
# change this to the specified size:
options(future.globals.maxSize = 4000 * 1024^2)

message("Finding integration anchors...")
## Finding integration anchors...
anchors_angl <- FindIntegrationAnchors(
    object.list = seurat_list,
    anchor.features = anglemania_genes, # here we use the anglemania genes
    verbose = FALSE,
    reduction = "cca"
)
## Warning: The `slot` argument of `SetAssayData()` is deprecated as of SeuratObject 5.0.0.
##  Please use the `layer` argument instead.
##  The deprecated feature was likely used in the Seurat package.
##   Please report the issue at <https://github.com/satijalab/seurat/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
message("Integrating samples...")
## Integrating samples...
seurat_integrated_angl <- IntegrateData(
    anchorset = anchors_angl,
    verbose = FALSE
)
## Warning: Layer counts isn't present in the assay object; returning NULL
## Warning: Layer counts isn't present in the assay object; returning NULL
## Warning: Layer counts isn't present in the assay object; returning NULL
seurat_integrated_angl <- seurat_integrated_angl %>%
    ScaleData(verbose = FALSE) %>%
    RunPCA(verbose = FALSE) %>%
    RunUMAP(dims = 1:30, verbose = FALSE)
## Warning: The default method for RunUMAP has changed from calling Python UMAP via reticulate to the R-native UWOT using the cosine metric
## To use Python UMAP via reticulate, set umap.method to 'umap-learn' and metric to 'correlation'
## This message will be shown once per session

Plot

patchwork::wrap_plots(
    DimPlot(seurat_integrated_angl, reduction = "umap", group.by = "Batch") +
        ggtitle("Seurat integration using anglemania genes, colored by Batch"),
    DimPlot(seurat_integrated_angl, reduction = "umap", group.by = "Group") +
        ggtitle("Seurat integration using anglemania genes, colored by Group"),
    ncol = 2
)
UMAPs of Seurat integrated data. Here we show that we can use the anglemania genes for integration of a SeuratObject.

UMAPs of Seurat integrated data. Here we show that we can use the anglemania genes for integration of a SeuratObject.

Showcase underlying functions

Normal anglemania workflow

sce_raw <- sce_example()
sce <- sce_raw
batch_key <- "batch"
sce <- anglemania(sce, batch_key = batch_key, verbose = FALSE)

anglemania is run on the SCE object and it basically calls three functions:

  • factorise:
    • creates a permutation of the input matrix whose correlation matrix is used to create a null distribution for each batch.
    • computes the cosine similarity (or spearman coefficient) between gene expression vector pairs matrix for both the original and permuted matrices
    • computes the zscore of the relationship between the gene pairs taking the mean and standard deviation of the null distribution
    • it does this for every batch in the dataset!
  • get_list_stats
    • computes the mean and standard deviation of the zscores across the matrices from the different batches. This creates two important matrices: the mean zscore matrix mean_zscore and the signal-to-noise ratio matrix sn_zscore. These are stored in the metadata of the SCE object.
  • select_genes
    • filters the gene pairs by the mean_zscore and sn_zscore matrices (SN ratio, i.e. the mean divided by the standard deviation).

factorise

barcodes_by_batch <- split(rownames(colData(sce)), colData(sce)[[batch_key]])
counts_by_batch <- lapply(barcodes_by_batch, function(x) {
    counts(sce[, x]) %>% sparse_to_fbm()
})
counts_by_batch[[1]][1:10, 1:6]
##       [,1] [,2] [,3] [,4] [,5] [,6]
##  [1,]    8    5    5    2    4    2
##  [2,]    9    5    4    1    4    9
##  [3,]    4    2    7    4    6    1
##  [4,]    7    4    4    1    3    8
##  [5,]    6    8    5    5    7    4
##  [6,]    5    8    5    9    8    6
##  [7,]    6    4    7    4    7    5
##  [8,]    3    3    3    3    5    4
##  [9,]    6    4    5    5    3    1
## [10,]    6    4    5    2    6    4
# we are working on FBMs (file-backed matrices
# implemented in the bigstatsr package)
class(counts_by_batch[[1]])
## [1] "FBM"
## attr(,"package")
## [1] "bigstatsr"
# factorise produces the correlation matrices transformed to z-scores
factorised <- lapply(counts_by_batch, factorise)
factorised[[1]][1:10, 1:6]
##              [,1]        [,2]        [,3]       [,4]       [,5]        [,6]
##  [1,]  0.00000000 -1.74668209 -0.79613304  1.1776966  0.8601212  0.50193905
##  [2,] -1.73785984  0.00000000  0.59661885  2.5115045  0.5348975  1.43483620
##  [3,] -0.85963025  0.53988067  0.00000000  0.2901283 -1.5767324 -0.06260101
##  [4,]  1.13658651  2.45514150  0.33158301  0.0000000 -0.3715653 -0.57325460
##  [5,]  0.92122274  0.57796947 -1.62420449 -0.3961608  0.0000000 -0.26750572
##  [6,]  0.52925575  1.57028828 -0.02415793 -0.6582967 -0.2979901  0.00000000
##  [7,] -0.41325531 -0.10548957 -1.16180200  0.0055108  0.3444741  1.41598180
##  [8,]  0.10001406 -1.38192310 -1.29577006 -0.2524730 -0.7968541 -1.56575595
##  [9,]  0.51996703  0.05963883 -0.37002326 -0.2927571 -0.9185203 -0.12159157
## [10,]  0.06970488 -0.33637803  1.57056534  0.3728527  0.5142178  1.01784989

get_list_stats

The “list stats” are computed by get_list_stats and take the z-score transformed correlation matrices from factorise as input. The outputs are the mean zscore matrix mean_zscore and the signal-to-noise ratio matrix sn_zscore. These are stored in the metadata of the SCE object.

matrix_list <- metadata(sce)$anglemania$matrix_list
weights <- setNames(
    metadata(sce)$anglemania$weights$weight,
    metadata(sce)$anglemania$weights$batch
)
list_stats <- get_list_stats(
    matrix_list = matrix_list,
    weights = weights,
    verbose = FALSE
)
names(list_stats)
## [1] "mean_zscore" "sds_zscore"  "sn_zscore"
class(list_stats)
## [1] "list"
list_stats$mean_zscore[1:10, 1:6]
##              [,1]       [,2]       [,3]        [,4]        [,5]       [,6]
##  [1,]  0.00000000 -1.4773877 -0.6690115  0.80957395  0.21191114  0.3065459
##  [2,] -1.43297508  0.0000000  0.8094372  1.79591873  0.87491750  0.0903189
##  [3,] -0.70975605  0.8090174  0.0000000 -0.90002758 -0.14215190  0.4217798
##  [4,]  0.81025807  1.7916458 -0.8103351  0.00000000 -0.29386285 -0.2877692
##  [5,]  0.26228504  0.8796571 -0.2190412 -0.31429554  0.00000000 -0.7944232
##  [6,]  0.34210706  0.1727196  0.4235970 -0.32724467 -0.79625202  0.0000000
##  [7,]  0.60813294  0.4576137 -1.4403958 -0.03807431  0.35694728  0.5394522
##  [8,] -0.03868058 -1.0958225 -0.7680151 -0.62207485 -1.42724690 -0.8197054
##  [9,]  1.26715940  0.3486664 -0.5030029 -0.76152650 -0.66668260 -0.4825756
## [10,]  0.49115807  1.1467226  0.7976727 -0.27013900  0.03028304  0.8112288
list_stats$sn_zscore[1:10, 1:6]
##            [,1]       [,2]      [,3]      [,4]        [,5]      [,6]
##  [1,]        NA 3.87928890 3.7213414 1.5550667  0.23116551 1.1093569
##  [2,] 3.3234406         NA 2.6894229 1.7746389  1.81948152 0.0475004
##  [3,] 3.3486305 2.12554305        NA 0.5347330  0.07006687 0.6157208
##  [4,] 1.7557127 1.90940919 0.5017815        NA  2.67420836 0.7127632
##  [5,] 0.2814584 2.06177351 0.1102260 2.7147101          NA 1.0660911
##  [6,] 1.2925884 0.08738832 0.6689559 0.6989744  1.12999842        NA
##  [7,] 0.4210103 0.57464016 3.6559091 0.6177018 20.23533063 0.4351825
##  [8,] 0.1972052 2.70836022 1.0290168 1.1901274  1.60093202 0.7769169
##  [9,] 1.1991785 0.85301337 2.6746701 1.1487110  1.87190277 0.9452842
## [10,] 0.8240564 0.54672981 0.7297777 0.2970755  0.04424840 2.7762187
# Or we can access them directly from the SCE object
# after running anglemania
metadata(sce)$anglemania$list_stats$mean_zscore[1:10, 1:6]
##              [,1]       [,2]       [,3]        [,4]        [,5]       [,6]
##  [1,]  0.00000000 -1.4773877 -0.6690115  0.80957395  0.21191114  0.3065459
##  [2,] -1.43297508  0.0000000  0.8094372  1.79591873  0.87491750  0.0903189
##  [3,] -0.70975605  0.8090174  0.0000000 -0.90002758 -0.14215190  0.4217798
##  [4,]  0.81025807  1.7916458 -0.8103351  0.00000000 -0.29386285 -0.2877692
##  [5,]  0.26228504  0.8796571 -0.2190412 -0.31429554  0.00000000 -0.7944232
##  [6,]  0.34210706  0.1727196  0.4235970 -0.32724467 -0.79625202  0.0000000
##  [7,]  0.60813294  0.4576137 -1.4403958 -0.03807431  0.35694728  0.5394522
##  [8,] -0.03868058 -1.0958225 -0.7680151 -0.62207485 -1.42724690 -0.8197054
##  [9,]  1.26715940  0.3486664 -0.5030029 -0.76152650 -0.66668260 -0.4825756
## [10,]  0.49115807  1.1467226  0.7976727 -0.27013900  0.03028304  0.8112288
metadata(sce)$anglemania$list_stats$sn_zscore[1:10, 1:6]
##            [,1]       [,2]      [,3]      [,4]        [,5]      [,6]
##  [1,]        NA 3.87928890 3.7213414 1.5550667  0.23116551 1.1093569
##  [2,] 3.3234406         NA 2.6894229 1.7746389  1.81948152 0.0475004
##  [3,] 3.3486305 2.12554305        NA 0.5347330  0.07006687 0.6157208
##  [4,] 1.7557127 1.90940919 0.5017815        NA  2.67420836 0.7127632
##  [5,] 0.2814584 2.06177351 0.1102260 2.7147101          NA 1.0660911
##  [6,] 1.2925884 0.08738832 0.6689559 0.6989744  1.12999842        NA
##  [7,] 0.4210103 0.57464016 3.6559091 0.6177018 20.23533063 0.4351825
##  [8,] 0.1972052 2.70836022 1.0290168 1.1901274  1.60093202 0.7769169
##  [9,] 1.1991785 0.85301337 2.6746701 1.1487110  1.87190277 0.9452842
## [10,] 0.8240564 0.54672981 0.7297777 0.2970755  0.04424840 2.7762187

select_genes

  • under the hood, anglemania calls select_genes with the default thresholds zscore_mean_threshold = 2.5, zscore_sn_threshold = 2.5
  • we can use select_genes to change the thresholds without having to run anglemania again
previous_genes <- get_anglemania_genes(sce)
sce <- select_genes(
    sce,
    zscore_mean_threshold = 2,
    zscore_sn_threshold = 2,
    verbose = FALSE
)
# Inspect the anglemania genes
new_genes <- get_anglemania_genes(sce)

length(previous_genes)
## [1] 25
length(new_genes)
## [1] 194
## R version 4.5.1 (2025-06-13)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.2 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.26.so;  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
##  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
##  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
## [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
## 
## time zone: UTC
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats4    stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] future_1.67.0               UpSetR_1.4.0               
##  [3] batchelor_1.24.0            bluster_1.18.0             
##  [5] scran_1.36.0                scater_1.36.0              
##  [7] ggplot2_3.5.2               scuttle_1.18.0             
##  [9] splatter_1.32.0             SingleCellExperiment_1.30.1
## [11] SummarizedExperiment_1.38.1 Biobase_2.68.0             
## [13] GenomicRanges_1.60.0        GenomeInfoDb_1.44.1        
## [15] IRanges_2.42.0              S4Vectors_0.46.0           
## [17] BiocGenerics_0.54.0         generics_0.1.4             
## [19] MatrixGenerics_1.20.0       matrixStats_1.5.0          
## [21] Seurat_5.3.0                SeuratObject_5.1.0         
## [23] sp_2.2-0                    dplyr_1.1.4                
## [25] anglemania_0.99.4           BiocStyle_2.36.0           
## 
## loaded via a namespace (and not attached):
##   [1] RcppAnnoy_0.0.22          splines_4.5.1            
##   [3] later_1.4.2               tibble_3.3.0             
##   [5] polyclip_1.10-7           fastDummies_1.7.5        
##   [7] lifecycle_1.0.4           edgeR_4.6.3              
##   [9] doParallel_1.0.17         globals_0.18.0           
##  [11] lattice_0.22-7            MASS_7.3-65              
##  [13] backports_1.5.0           magrittr_2.0.3           
##  [15] limma_3.64.3              plotly_4.11.0            
##  [17] sass_0.4.10               rmarkdown_2.29           
##  [19] jquerylib_0.1.4           yaml_2.3.10              
##  [21] bigparallelr_0.3.2        metapod_1.16.0           
##  [23] httpuv_1.6.16             sctransform_0.4.2        
##  [25] spam_2.11-1               spatstat.sparse_3.1-0    
##  [27] reticulate_1.43.0         cowplot_1.2.0            
##  [29] pbapply_1.7-4             RColorBrewer_1.1-3       
##  [31] ResidualMatrix_1.18.0     abind_1.4-8              
##  [33] Rtsne_0.17                purrr_1.1.0              
##  [35] bigassertr_0.1.7          GenomeInfoDbData_1.2.14  
##  [37] ggrepel_0.9.6             irlba_2.3.5.1            
##  [39] listenv_0.9.1             spatstat.utils_3.1-5     
##  [41] goftest_1.2-3             RSpectra_0.16-2          
##  [43] dqrng_0.4.1               spatstat.random_3.4-1    
##  [45] fitdistrplus_1.2-4        parallelly_1.45.1        
##  [47] DelayedMatrixStats_1.30.0 pkgdown_2.1.3            
##  [49] codetools_0.2-20          DelayedArray_0.34.1      
##  [51] tidyselect_1.2.1          UCSC.utils_1.4.0         
##  [53] farver_2.1.2              viridis_0.6.5            
##  [55] ScaledMatrix_1.16.0       bigstatsr_1.6.2          
##  [57] spatstat.explore_3.5-2    flock_0.7                
##  [59] jsonlite_2.0.0            BiocNeighbors_2.2.0      
##  [61] progressr_0.15.1          ggridges_0.5.6           
##  [63] survival_3.8-3            iterators_1.0.14         
##  [65] systemfonts_1.2.3         foreach_1.5.2            
##  [67] tools_4.5.1               ragg_1.4.0               
##  [69] ica_1.0-3                 Rcpp_1.1.0               
##  [71] glue_1.8.0                gridExtra_2.3            
##  [73] SparseArray_1.8.1         xfun_0.52                
##  [75] withr_3.0.2               BiocManager_1.30.26      
##  [77] fastmap_1.2.0             rsvd_1.0.5               
##  [79] digest_0.6.37             R6_2.6.1                 
##  [81] mime_0.13                 textshaping_1.0.1        
##  [83] scattermore_1.2           tensor_1.5.1             
##  [85] spatstat.data_3.1-6       tidyr_1.3.1              
##  [87] data.table_1.17.8         FNN_1.1.4.1              
##  [89] httr_1.4.7                htmlwidgets_1.6.4        
##  [91] S4Arrays_1.8.1            uwot_0.2.3               
##  [93] pkgconfig_2.0.3           gtable_0.3.6             
##  [95] lmtest_0.9-40             XVector_0.48.0           
##  [97] htmltools_0.5.8.1         dotCall64_1.2            
##  [99] bookdown_0.43             scales_1.4.0             
## [101] png_0.1-8                 spatstat.univar_3.1-4    
## [103] knitr_1.50                reshape2_1.4.4           
## [105] checkmate_2.3.2           nlme_3.1-168             
## [107] cachem_1.1.0              zoo_1.8-14               
## [109] stringr_1.5.1             rmio_0.4.0               
## [111] KernSmooth_2.23-26        vipor_0.4.7              
## [113] parallel_4.5.1            miniUI_0.1.2             
## [115] desc_1.4.3                pillar_1.11.0            
## [117] grid_4.5.1                vctrs_0.6.5              
## [119] RANN_2.6.2                promises_1.3.3           
## [121] BiocSingular_1.24.0       ff_4.5.2                 
## [123] beachmat_2.24.0           xtable_1.8-4             
## [125] cluster_2.1.8.1           beeswarm_0.4.0           
## [127] evaluate_1.0.4            cli_3.6.5                
## [129] locfit_1.5-9.12           compiler_4.5.1           
## [131] rlang_1.1.6               crayon_1.5.3             
## [133] future.apply_1.20.0       labeling_0.4.3           
## [135] ps_1.9.1                  ggbeeswarm_0.7.2         
## [137] plyr_1.8.9                fs_1.6.6                 
## [139] stringi_1.8.7             viridisLite_0.4.2        
## [141] deldir_2.0-4              BiocParallel_1.42.1      
## [143] lazyeval_0.2.2            spatstat.geom_3.5-0      
## [145] Matrix_1.7-3              RcppHNSW_0.6.0           
## [147] patchwork_1.3.1           sparseMatrixStats_1.20.0 
## [149] statmod_1.5.0             shiny_1.11.1             
## [151] ROCR_1.0-11               igraph_2.1.4             
## [153] bslib_0.9.0               bit_4.6.0