clustSIGNAL tutorial
Pratibha Panwar, Boyi Guo, Haowen Zhou, Stephanie Hicks, Shila Ghazanfar
2024-10-31
Source:vignettes/clustSIGNAL.Rmd
clustSIGNAL.Rmd
Overview
In this vignette, we will demonstrate how to perform spatially-resolved clustering with clustSIGNAL. Following this, we will explore the clusters using pre-defined metrics like adjusted rand index (ARI), normalised mutual information (NMI), and average silhouette width, as well as spatial plots. We will also display the use of entropy measures generated as a by-product of clustSIGNAL process in understanding the tissue structure of a sample. In the end, we will also explore multisample analysis with clustSIGNAL.
Single sample analysis with clustSIGNAL
Here, we use the SeqFISH mouse embryo dataset from Lohoff et al, 2021 , which contains spatial transcriptomics data from 3 mouse embryos, with 351 genes and a total of 57,536 cells. For this vignette, we subset the data by randomly selecting 5000 cells from Embryo 2, excluding cells that were manually annotated as ‘Low quality’.
We begin by creating a SpatialExperiment object from the gene expression and cell information in the data subset, ensuring that the spatial coordinates are stored in spatialCoords within the SpatialExperiment object. If the data are already in a SpatialExperiment object, then the user can directly run clustSIGNAL, after ensuring that the basic requirements like spatial coordinates and normalized counts are met.
data(mEmbryo2)
spe <- SpatialExperiment(assays = list(logcounts = me_expr),
colData = me_data, spatialCoordsNames = c("X", "Y"))
spe
## class: SpatialExperiment
## dim: 351 5000
## metadata(0):
## assays(1): logcounts
## rownames(351): Abcc4 Acp5 ... Zfp57 Zic3
## rowData names(0):
## colnames(5000): embryo2_Pos29_cell100_z2 embryo2_Pos29_cell101_z5 ...
## embryo2_Pos50_cell97_z5 embryo2_Pos50_cell99_z5
## colData names(4): uniqueID pos celltype_mapped_refined sample_id
## reducedDimNames(0):
## mainExpName: NULL
## altExpNames(0):
## spatialCoords names(2) : X Y
## imgData names(0):
For running clustSIGNAL, we need to know the column names in colData of the SpatialExperiment object that contain the sample and cell labels. Here, the sample labels are in the ‘sample_id’ column, and the cell labels are in the ‘uniqueID’ column.
colnames(colData(spe))
## [1] "uniqueID" "pos"
## [3] "celltype_mapped_refined" "sample_id"
Running clustSIGNAL on one sample
Next, we run clustSIGNAL using the sample and cell labels we identified earlier. The simplest clustSIGNAL run requires a SpatialExperiment object, two variables holding colData column names containing sample and cell labels, and the type of output the user would like to see. Other parameters that can be modified include dimRed to specify the low dimension data to use, batch to perform batch correction, batch_by to indicate sample batches contributing to batch effect, NN to specify the neighbourhood size, kernel for weight distribution to use, spread for distribution spread value, sort to sort the neighbourhood, threads to specify the number of cpus to use in parallel runs, and clustParams to specify clustering parameters.
Furthermore, the adaptively smoothed gene expression data generated by clustSIGNAL could be useful for other downstream analyses and will be accessible to the user if they choose to output the final SpatialExperiment object.
set.seed(100)
samples <- "sample_id"
cells <- "uniqueID"
res_emb <- clustSIGNAL(spe, samples, cells, outputs = "a")
## [1] "Calculating PCA. Time 01:21:17"
## [1] "clustSIGNAL run started. Time 01:21:18"
## [1] "Initial nonspatial clustering performed. Clusters = 11 Time 01:21:19"
## [1] "Nonspatial subclustering performed. Subclusters = 50 Time 01:21:21"
## [1] "Regions defined. Time 01:21:23"
## [1] "Region domainness calculated. Time 01:21:24"
## [1] "Smoothing performed. NN = 30 Kernel = G Spread = 0.05 Time 01:22:14"
## [1] "Nonspatial clustering performed on smoothed data. Clusters = 16 Time 01:22:15"
## [1] "clustSIGNAL run completed. 01:22:15"
## Time difference of 58.33652 secs
This returns a list that can contain a dataframe of cluster names, a matrix of cell labels from each region’s neighbourhood, a final SpatialExperiment object, or a combination of these, depending on the choice of ‘outputs’ selected. Here, the output contains all three data types.
names(res_emb)
## [1] "clusters" "spe_final"
The cluster dataframe contains cell labels and their cluster numbers allotted by clustSIGNAL.
head(res_emb$clusters, n = 3)
## Cells Clusters
## 1 embryo2_Pos29_cell100_z2 13
## 2 embryo2_Pos29_cell101_z5 13
## 3 embryo2_Pos29_cell104_z2 13
The final SpatialExperiment object contains the adaptively smoothed gene expression data as an additional assay, as well initial clusters, entropy values, and clustSIGNAL clusters.
spe <- res_emb$spe_final
spe
## class: SpatialExperiment
## dim: 351 5000
## metadata(0):
## assays(2): logcounts smoothed
## rownames(351): Abcc4 Acp5 ... Zfp57 Zic3
## rowData names(0):
## colnames(5000): embryo2_Pos29_cell100_z2 embryo2_Pos29_cell101_z5 ...
## embryo2_Pos50_cell97_z5 embryo2_Pos50_cell99_z5
## colData names(8): uniqueID pos ... entropy clustSIGNAL
## reducedDimNames(2): PCA PCA.smooth
## mainExpName: NULL
## altExpNames(0):
## spatialCoords names(2) : X Y
## imgData names(1): sample_id
Analysing clustSIGNAL results
In this section, we analyse the results from clustSIGNAL through spatial plots and clustering metrics.
Visualising clustSIGNAL clusters
colors <- c("#635547", "#8EC792", "#9e6762", "#FACB12", "#3F84AA", "#0F4A9C",
"#ff891c", "#EF5A9D", "#C594BF", "#DFCDE4", "#139992", "#65A83E",
"#8DB5CE", "#005579", "#C9EBFB", "#B51D8D", "#532C8A", "#8870ad",
"#cc7818", "#FBBE92", "#EF4E22", "#f9decf", "#c9a997", "#C72228",
"#f79083", "#F397C0", "#DABE99", "#c19f70", "#354E23", "#C3C388",
"#647a4f", "#CDE088", "#f7f79e", "#F6BFCB", "#7F6874", "#989898",
"#1A1A1A", "#FFFFFF", "#e6e6e6", "#77441B", "#F90026", "#A10037",
"#DA5921", "#E1C239", "#9DD84A")
We use spatial coordinates of cells and their cluster labels and entropy values to visualize the clustering output.
df_ent <- as.data.frame(colData(spe))
# spatial plot
spt_clust <- df_ent %>%
ggplot(aes(x = spatialCoords(spe)[, 1],
y = -spatialCoords(spe)[, 2])) +
geom_scattermore(pointsize = 3, aes(colour = clustSIGNAL)) +
scale_color_manual(values = colors) +
ggtitle("A") +
labs(x = "x-coordinate", y = "y-coordinate") +
guides(color = guide_legend(title = "Clusters",
override.aes = list(size = 3))) +
theme_classic() +
theme(text = element_text(size = 12))
# calculating median entropy of each cluster
celltype_ent <- df_ent %>%
group_by(as.character(clustSIGNAL)) %>%
summarise(meanEntropy = median(entropy))
# reordering clusters by their median entropy
# low to high median entropy
cellOrder <- celltype_ent$meanEntropy
names(cellOrder) <- celltype_ent$`as.character(clustSIGNAL)`
cellOrder <- sort(cellOrder)
df_ent$clustSIGNAL <- factor(df_ent$clustSIGNAL, levels = names(cellOrder))
# box plot of cluster entropy
colors_ent <- colors[as.numeric(names(cellOrder))]
box_clust <- df_ent %>%
ggplot(aes(x = clustSIGNAL, y = entropy, fill = clustSIGNAL)) +
geom_boxplot() +
scale_fill_manual(values = colors_ent) +
ggtitle("B") +
labs(x = "clustSIGNAL clusters", y = "Entropy") +
theme_classic() +
theme(legend.position = "none",
text = element_text(size = 12),
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1))
spt_clust + box_clust + patchwork::plot_layout(guides = "collect",
widths = c(2, 3))
The spatial location (A) and entropy distribution (B) of the clusters provide spatial context of the cells and their neighbourhoods, as well as the compositions of the neighbourhoods. For example, the low entropy of cluster 4 indicates that the cells in this cluster are generally found in more homogeneous space, whereas the high entropy of cluster 7 cells indicates that they belong to regions with more cell diversity. This can also be visualized in the spatial plot.
Cluster metrics
We assess the clustering efficiency of clustSIGNAL using the commonly used clustering metrics ARI, NMI, and silhouette width. ARI and NMI are usable only when prior cell annotation information is available, and assume that this cell annotation is ground truth. Here, ARI and NMI measure the similarity or agreement (respectively) between cluster labels obtained from clustSIGNAL and manual cell annotation labels. On the contrary, silhouette width is reference-free and evaluates how well a cell fits within its assigned cluster compared to other clusters.
# average silhouette width
clusts <- as.numeric(as.character(spe$clustSIGNAL))
cXg_mat <- t(as.matrix(logcounts(spe)))
distMat <- distances(cXg_mat)
silCluster <- as.matrix(silhouette(clusts, distMat))
spe$rcSil <- silCluster[, 3]
# ARI and NMI
as.data.frame(colData(spe)) %>%
summarise(ARI = aricode::ARI(celltype_mapped_refined, clustSIGNAL),
NMI = aricode::NMI(celltype_mapped_refined, clustSIGNAL),
ASW = mean(rcSil))
## ARI NMI ASW
## 1 0.3420434 0.6281433 0.03767501
Entropy spread and distribution
The entropy values generated through clustSIGNAL process can be useful in analyzing the sample structure. The entropy range can indicate whether the tissue sample contains any homogeneous domain-like structures. For example, here the minimum entropy value is 0, which means some cells are placed in completely homogeneous space when looking at neighbourhood size of 30 cells (NN = 30 was used for generating this entropy data). Moreover, the mean entropy value is low, which can be interpreted as the tissue having at least some domain-like structures.
# Data assessment - Overall entropy
as.data.frame(colData(spe)) %>%
summarise(min_Entropy = min(entropy),
max_Entropy = max(entropy),
mean_Entropy = mean(entropy))
## min_Entropy max_Entropy mean_Entropy
## 1 0 3.05603 1.37066
# Histogram of entropy spread
hst_ent <- as.data.frame(colData(spe)) %>%
ggplot(aes(entropy)) +
geom_histogram(binwidth = 0.05) +
ggtitle("A") +
labs(x = "Entropy", y = "Number of regions") +
theme_classic() +
theme(text = element_text(size = 12))
# Spatial plot showing sample entropy distribution
spt_ent <- as.data.frame(colData(spe)) %>%
ggplot(aes(x = spatialCoords(spe)[, 1],
y = -spatialCoords(spe)[, 2])) +
geom_scattermore(pointsize = 3,
aes(colour = entropy)) +
scale_colour_gradient2("Entropy", low = "grey", high = "blue") +
scale_size_continuous(range = c(0, max(spe$entropy))) +
ggtitle("B") +
labs(x = "x-coordinate", y = "y-coordinate") +
theme_classic() +
theme(text = element_text(size = 12))
hst_ent + spt_ent
The spread (A) and spatial distribution (B) of region entropy measures can be very useful in assessing the tissue composition of samples - low entropy regions are more homogeneous with domain-like structure, whereas high entropy regions are heterogeneous with more uniform distribution of cells.
Generating entropy data only
To evaluate tissue structure using entropy values, we can run clustSIGNAL up to the entropy measurement step, without running the complete method. The entropy values will be added to the SpatialExperiment object and can be used for assessing tissue structure.
data(mEmbryo2)
spe <- SpatialExperiment(assays = list(logcounts = me_expr),
colData = me_data, spatialCoordsNames = c("X", "Y"))
set.seed(100)
spe <- scater::runPCA(spe)
spe <- clustSIGNAL::p1_clustering(spe, dimRed = "PCA", batch = FALSE,
batch_by = "None", clustParams = list(
0, 0, 30, 5, "louvain"))
## [1] "Initial nonspatial clustering performed. Clusters = 11 Time 01:22:25"
## [1] "Nonspatial subclustering performed. Subclusters = 50 Time 01:22:27"
outReg <- clustSIGNAL::neighbourDetect(spe, samples = "sample_id", NN = 30,
cells = "uniqueID", sort = TRUE)
## [1] "Regions defined. Time 01:22:29"
spe <- entropyMeasure(spe, cells = "uniqueID", outReg$regXclust, threads = 1)
## [1] "Region domainness calculated. Time 01:22:29"
head(spe$entropy)
## [1] 1.03701 0.42003 0.62749 0.76651 0.56651 0.21084
Multisample analysis with clustSIGNAL
Here, we use the MERFISH mouse hypothalamic preoptic region dataset from Moffitt et al, 2018, which contains spatial transcriptomics data from 181 samples, with 155 genes and a total of 1,027,080 cells. For this vignette, we subset the data by selecting a total of 6000 random cells from only 3 samples - Animal 1 Bregma -0.09 (2080 cells), Animal 7 Bregma 0.16 (1936 cells), and Animal 7 Bregma -0.09 (1984 cells), excluding cells that were manually annotated as ‘ambiguous’ and 20 genes that were assessed using a different technology.
We start the analysis by creating a SpatialExperiment object from the gene expression and cell information in the data subset, ensuring that the spatial coordinates are stored in spatialCoords within the SpatialExperiment object.
data(mHypothal)
spe2 <- SpatialExperiment(assays = list(logcounts = mh_expr),
colData = mh_data, spatialCoordsNames = c("X", "Y"))
spe2
## class: SpatialExperiment
## dim: 135 6000
## metadata(0):
## assays(1): logcounts
## rownames(135): Ace2 Adora2a ... Ttn Ttyh2
## rowData names(0):
## colnames(6000): 74d3f69d-e8f2-4c33-a8ca-fac3eb65e55a
## 41158ddc-e70c-487b-b891-0cb3c8452555 ...
## 54145623-7071-482c-b9da-d0d2dd31274a
## 96bc85ce-b993-4fb1-8e0c-165f83f0cfd0
## colData names(4): Cell_ID Cell_class sample_id samples
## reducedDimNames(0):
## mainExpName: NULL
## altExpNames(0):
## spatialCoords names(2) : X Y
## imgData names(0):
Here, the cell labels are in the column ‘Cell_ID’ and sample labels are in ‘samples’ column in the SpatialExperiment object.
colnames(colData(spe2))
## [1] "Cell_ID" "Cell_class" "sample_id" "samples"
clustSIGNAL run
One of the important concepts to take into account when running multisample analysis is batch effects. When gathering samples from different sources or through different technologies/procedures, some technical batch effects might be introduced into the dataset. We run clustSIGNAL in batch correction mode simply by setting batch = TRUE. The method then uses harmony internally for batch correction.
set.seed(110)
samples <- "samples"
cells <- "Cell_ID"
res_hyp <- clustSIGNAL(spe2, samples, cells, threads = 4, outputs = "a")
## [1] "Calculating PCA. Time 01:22:30"
## [1] "clustSIGNAL run started. Time 01:22:30"
## [1] "Initial nonspatial clustering performed. Clusters = 11 Time 01:22:31"
## [1] "Nonspatial subclustering performed. Subclusters = 52 Time 01:22:33"
## [1] "Regions defined. Time 01:22:36"
## [1] "Region domainness calculated. Time 01:22:37"
## [1] "Smoothing performed. NN = 30 Kernel = G Spread = 0.05 Time 01:22:59"
## [1] "Nonspatial clustering performed on smoothed data. Clusters = 11 Time 01:23:00"
## [1] "clustSIGNAL run completed. 01:23:00"
## Time difference of 30.55265 secs
spe2 <- res_hyp$spe_final
spe2
## class: SpatialExperiment
## dim: 135 6000
## metadata(0):
## assays(2): logcounts smoothed
## rownames(135): Ace2 Adora2a ... Ttn Ttyh2
## rowData names(0):
## colnames(6000): 74d3f69d-e8f2-4c33-a8ca-fac3eb65e55a
## 41158ddc-e70c-487b-b891-0cb3c8452555 ...
## 54145623-7071-482c-b9da-d0d2dd31274a
## 96bc85ce-b993-4fb1-8e0c-165f83f0cfd0
## colData names(8): Cell_ID Cell_class ... entropy clustSIGNAL
## reducedDimNames(2): PCA PCA.smooth
## mainExpName: NULL
## altExpNames(0):
## spatialCoords names(2) : X Y
## imgData names(1): sample_id
Clustering metrics
Clustering and entropy results can be calculated and visualized for each sample. clustSIGNAL works well with samples that have more uniform distribution of cells.
samplesList <- levels(spe2[[samples]])
samplesList
## [1] "1.-0.09" "7.-0.09" "7.0.16"
# calculating silhouette width per sample
silWidthRC <- matrix(nrow = 0, ncol = 3)
for (s in samplesList) {
speX <- spe2[, spe2[[samples]] == s]
clust_sub <- as.numeric(as.character(speX$clustSIGNAL))
cXg <- t(as.matrix(logcounts(speX)))
distMat <- distances(cXg)
silCluster <- as.matrix(silhouette(clust_sub, distMat))
silWidthRC <- rbind(silWidthRC, silCluster)
}
spe2$rcSil <- silWidthRC[, 3]
as.data.frame(colData(spe2)) %>%
group_by(samples) %>%
summarise(ARI = aricode::ARI(Cell_class, clustSIGNAL),
NMI = aricode::NMI(Cell_class, clustSIGNAL),
ASW = mean(rcSil),
min_Entropy = min(entropy),
max_Entropy = max(entropy),
mean_Entropy = mean(entropy))
## # A tibble: 3 × 7
## samples ARI NMI ASW min_Entropy max_Entropy mean_Entropy
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1.-0.09 0.445 0.624 0.0807 1.32 4.42 3.30
## 2 7.-0.09 0.511 0.679 0.115 0.970 4.64 3.30
## 3 7.0.16 0.645 0.744 0.104 0.970 4.42 3.26
Visualizing clustSIGNAL clusters
clustSIGNAL performs clustering on all cells in the dataset in one run, thereby generating the same clusters across multiple samples. The user does not need to map cluster labels between samples. For example, cluster 1 represents the same cell type in all three samples, without needing explicit mapping between samples.
df_ent <- as.data.frame(colData(spe2))
# spatial plot
spt_clust <- df_ent %>%
ggplot(aes(x = spatialCoords(spe2)[, 1],
y = -spatialCoords(spe2)[, 2])) +
geom_scattermore(pointsize = 3, aes(colour = clustSIGNAL)) +
scale_color_manual(values = colors) +
facet_wrap(vars(samples), scales = "free", nrow = 1) +
labs(x = "x-coordinate", y = "y-coordinate") +
guides(color = guide_legend(title = "Clusters",
override.aes = list(size = 3))) +
theme_classic() +
theme(text = element_text(size = 12),
axis.text.x = element_text(angle = 90, vjust = 0.5))
box_clust <- list()
for (s in samplesList) {
df_ent_sub <- as.data.frame(colData(spe2)[spe2[[samples]] == s, ])
# calculating median entropy of each cluster in a sample
celltype_ent <- df_ent_sub %>%
group_by(as.character(clustSIGNAL)) %>%
summarise(meanEntropy = median(entropy))
# reordering clusters by their median entropy
# low to high median entropy
cellOrder <- celltype_ent$meanEntropy
names(cellOrder) <- celltype_ent$`as.character(clustSIGNAL)`
cellOrder = sort(cellOrder)
df_ent_sub$clustSIGNAL <- factor(df_ent_sub$clustSIGNAL,
levels = names(cellOrder))
# box plot of cluster entropy
colors_ent <- colors[as.numeric(names(cellOrder))]
box_clust[[s]] <- df_ent_sub %>%
ggplot(aes(x = clustSIGNAL, y = entropy, fill = clustSIGNAL)) +
geom_boxplot() +
scale_fill_manual(values = colors_ent) +
facet_wrap(vars(samples), nrow = 1) +
labs(x = "clustSIGNAL clusters", y = "Entropy") +
ylim(0, NA) +
theme_classic() +
theme(strip.text = element_blank(),
legend.position = "none",
text = element_text(size = 12),
axis.text.x = element_text(angle = 90, vjust = 0.5))
}
spt_clust / (patchwork::wrap_plots(box_clust[1:3], nrow = 1) +
plot_layout(axes = "collect")) +
plot_layout(guides = "collect", heights = c(5, 3)) +
plot_annotation(
title = "Spatial (top) and entropy (bottom) distributions of clusters")
The spatial location (top) and entropy distribution (bottom) of the clusters can be compared in a multisample analysis, providing spatial context of the cluster cells and their neighbourhood compositions in the different samples.
Visualising entropy spread and distribution
In multisample analysis, the spread (A) and spatial distribution (B) of region entropy measures can be useful in assessing and comparing the tissue structure in the samples.
# Histogram of entropy spread
hst_ent <- as.data.frame(colData(spe2)) %>%
ggplot(aes(entropy)) +
geom_histogram(binwidth = 0.05) +
facet_wrap(vars(samples), nrow = 1) +
labs(x = "Entropy", y = "Number of regions") +
theme_classic() +
theme(text = element_text(size = 12))
# Spatial plot showing sample entropy distribution
spt_ent <- as.data.frame(colData(spe2)) %>%
ggplot(aes(x = spatialCoords(spe2)[, 1],
y = -spatialCoords(spe2)[, 2])) +
geom_scattermore(pointsize = 3,
aes(colour = entropy)) +
scale_colour_gradient2("Entropy", low = "grey", high = "blue") +
scale_size_continuous(range = c(0, max(spe2$entropy))) +
facet_wrap(vars(samples), scales = "free", nrow = 1) +
labs(x = "x-coordinate", y = "y-coordinate") +
theme_classic() +
theme(strip.text = element_blank(),
text = element_text(size = 12),
axis.text.x = element_text(angle = 90, vjust = 0.5))
hst_ent / spt_ent + plot_layout(heights = c(3,5)) +
plot_annotation(
title = "Entropy spread (top) and spatial distribution (bottom)")
Session Information
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 22.04.5 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.20.so; LAPACK version 3.10.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] scattermore_1.2 patchwork_1.3.0
## [3] ggplot2_3.5.1 dplyr_1.1.4
## [5] aricode_1.0.3 cluster_2.1.6
## [7] distances_0.1.11 clustSIGNAL_0.99.3
## [9] SpatialExperiment_1.16.0 SingleCellExperiment_1.28.0
## [11] SummarizedExperiment_1.36.0 Biobase_2.66.0
## [13] GenomicRanges_1.58.0 GenomeInfoDb_1.42.0
## [15] IRanges_2.40.0 S4Vectors_0.44.0
## [17] BiocGenerics_0.52.0 MatrixGenerics_1.18.0
## [19] matrixStats_1.4.1 BiocStyle_2.34.0
##
## loaded via a namespace (and not attached):
## [1] gridExtra_2.3 rlang_1.1.4 magrittr_2.0.3
## [4] scater_1.34.0 compiler_4.4.1 systemfonts_1.1.0
## [7] vctrs_0.6.5 pkgconfig_2.0.3 crayon_1.5.3
## [10] fastmap_1.2.0 magick_2.8.5 XVector_0.46.0
## [13] labeling_0.4.3 scuttle_1.16.0 utf8_1.2.4
## [16] rmarkdown_2.28 UCSC.utils_1.2.0 ggbeeswarm_0.7.2
## [19] ragg_1.3.3 xfun_0.48 bluster_1.16.0
## [22] zlibbioc_1.52.0 cachem_1.1.0 beachmat_2.22.0
## [25] jsonlite_1.8.9 highr_0.11 DelayedArray_0.32.0
## [28] BiocParallel_1.40.0 irlba_2.3.5.1 parallel_4.4.1
## [31] R6_2.5.1 bslib_0.8.0 rlist_0.4.6.2
## [34] jquerylib_0.1.4 Rcpp_1.0.13 bookdown_0.41
## [37] knitr_1.48 Matrix_1.7-0 igraph_2.1.1
## [40] tidyselect_1.2.1 abind_1.4-8 yaml_2.3.10
## [43] viridis_0.6.5 codetools_0.2-20 lattice_0.22-6
## [46] tibble_3.2.1 withr_3.0.2 evaluate_1.0.1
## [49] desc_1.4.3 pillar_1.9.0 BiocManager_1.30.25
## [52] generics_0.1.3 munsell_0.5.1 scales_1.3.0
## [55] glue_1.8.0 tools_4.4.1 BiocNeighbors_2.0.0
## [58] data.table_1.16.2 ScaledMatrix_1.14.0 fs_1.6.4
## [61] cowplot_1.1.3 grid_4.4.1 colorspace_2.1-1
## [64] GenomeInfoDbData_1.2.13 beeswarm_0.4.0 BiocSingular_1.22.0
## [67] vipor_0.4.7 cli_3.6.3 rsvd_1.0.5
## [70] textshaping_0.4.0 fansi_1.0.6 S4Arrays_1.6.0
## [73] viridisLite_0.4.2 gtable_0.3.6 sass_0.4.9
## [76] digest_0.6.37 SparseArray_1.6.0 ggrepel_0.9.6
## [79] farver_2.1.2 rjson_0.2.23 htmltools_0.5.8.1
## [82] pkgdown_2.1.1 lifecycle_1.0.4 httr_1.4.7
## [85] harmony_1.2.1