+
+
+
+
+
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.
+
+
## [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, 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 … for additional parameters for clustering steps.
+
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 09:16:29"
+## [1] "clustSIGNAL run started. Time 09:16:30"
+## [1] "Initial nonspatial clustering performed. Clusters = 11 Time 09:16:30"
+## [1] "Nonspatial subclustering performed. Subclusters = 52 Time 09:16:33"
+## [1] "Regions defined. Time 09:16:35"
+## [1] "Region domainness calculated. Time 09:16:36"
+## [1] "Smoothing performed. NN = 30 Kernel = G Spread = 0.05 Time 09:17:51"
+## [1] "Nonspatial clustering performed on smoothed data. Clusters = 14 Time 09:17:53"
+## [1] "clustSIGNAL run completed. 09:17:53"
+## Time difference of 1.405417 mins
+
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.
+
+
## [1] "clusters" "neighbours" "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 10
+## 2 embryo2_Pos29_cell101_z5 10
+## 3 embryo2_Pos29_cell104_z2 10
+
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.
+
+
## ARI NMI ASW
+## 1 0.3668453 0.6469266 0.05075243
+
+
+
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.
+
+
## min_Entropy max_Entropy mean_Entropy
+## 1 0 3.109686 1.420307
+
+
+
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::nsClustering(spe, samples = "sample_id",
+ dimRed = "PCA", reclust = FALSE)
+
## [1] "Initial nonspatial clustering performed. Clusters = 11 Time 09:18:02"
+## [1] "Nonspatial subclustering performed. Subclusters = 52 Time 09:18:05"
+
+outReg <- clustSIGNAL::neighbourDetect(spe, samples = "sample_id", NN = 30,
+ cells = "uniqueID", sort = TRUE)
+
## [1] "Regions defined. Time 09:18:07"
+
+
## [1] "Region domainness calculated. Time 09:18:07"
+
+
## [1] 1.3033074 0.5608252 0.8785142 0.9703438 0.9480782 0.3533593
+
+
+
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.
+
+
## [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(101)
+samples <- "samples"
+cells <- "Cell_ID"
+res_hyp <- clustSIGNAL(spe2, samples, cells, batch = TRUE, threads = 4, outputs = "a")
+
## [1] "Calculating PCA. Time 09:18:08"
+## [1] "clustSIGNAL run started. Time 09:18:08"
+## [1] "Initial nonspatial clustering performed. Clusters = 11 Time 09:18:12"
+
## Warning in (function (A, nv = 5, nu = nv, maxit = 1000, work = nv + 7, reorth =
+## TRUE, : You're computing too large a percentage of total singular values, use a
+## standard svd instead.
+
## [1] "Nonspatial subclustering performed. Subclusters = 53 Time 09:18:14"
+## [1] "Regions defined. Time 09:18:17"
+## [1] "Region domainness calculated. Time 09:18:18"
+## [1] "Smoothing performed. NN = 30 Kernel = G Spread = 0.05 Time 09:38:44"
+## [1] "Nonspatial clustering performed on smoothed data. Clusters = 10 Time 09:38:45"
+## [1] "clustSIGNAL run completed. 09:38:45"
+## Time difference of 20.62043 mins
+
+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.413 0.607 0.0826 1.08 4.42 3.32
+## 2 7.-0.09 0.463 0.656 0.124 0.904 4.51 3.30
+## 3 7.0.16 0.644 0.735 0.0996 0.904 4.48 3.29
+
+
+
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.0
+## [9] SpatialExperiment_1.14.0 SingleCellExperiment_1.26.0
+## [11] SummarizedExperiment_1.34.0 Biobase_2.64.0
+## [13] GenomicRanges_1.56.1 GenomeInfoDb_1.40.1
+## [15] IRanges_2.38.1 S4Vectors_0.42.1
+## [17] BiocGenerics_0.50.0 MatrixGenerics_1.16.0
+## [19] matrixStats_1.4.1 BiocStyle_2.32.1
+##
+## loaded via a namespace (and not attached):
+## [1] gridExtra_2.3 rlang_1.1.4
+## [3] magrittr_2.0.3 scater_1.32.1
+## [5] compiler_4.4.1 DelayedMatrixStats_1.26.0
+## [7] systemfonts_1.1.0 vctrs_0.6.5
+## [9] pkgconfig_2.0.3 crayon_1.5.3
+## [11] fastmap_1.2.0 magick_2.8.4
+## [13] XVector_0.44.0 labeling_0.4.3
+## [15] scuttle_1.14.0 utf8_1.2.4
+## [17] rmarkdown_2.28 UCSC.utils_1.0.0
+## [19] ggbeeswarm_0.7.2 ragg_1.3.3
+## [21] xfun_0.47 bluster_1.14.0
+## [23] zlibbioc_1.50.0 cachem_1.1.0
+## [25] beachmat_2.20.0 jsonlite_1.8.8
+## [27] highr_0.11 DelayedArray_0.30.1
+## [29] BiocParallel_1.38.0 irlba_2.3.5.1
+## [31] parallel_4.4.1 R6_2.5.1
+## [33] bslib_0.8.0 jquerylib_0.1.4
+## [35] Rcpp_1.0.13 bookdown_0.40
+## [37] knitr_1.48 Matrix_1.7-0
+## [39] igraph_2.0.3 tidyselect_1.2.1
+## [41] abind_1.4-8 yaml_2.3.10
+## [43] viridis_0.6.5 codetools_0.2-20
+## [45] lattice_0.22-6 tibble_3.2.1
+## [47] withr_3.0.1 evaluate_1.0.0
+## [49] desc_1.4.3 pillar_1.9.0
+## [51] BiocManager_1.30.25 generics_0.1.3
+## [53] sparseMatrixStats_1.16.0 munsell_0.5.1
+## [55] scales_1.3.0 RhpcBLASctl_0.23-42
+## [57] glue_1.7.0 tools_4.4.1
+## [59] BiocNeighbors_1.22.0 ScaledMatrix_1.12.0
+## [61] fs_1.6.4 cowplot_1.1.3
+## [63] grid_4.4.1 colorspace_2.1-1
+## [65] GenomeInfoDbData_1.2.12 beeswarm_0.4.0
+## [67] BiocSingular_1.20.0 vipor_0.4.7
+## [69] cli_3.6.3 rsvd_1.0.5
+## [71] textshaping_0.4.0 fansi_1.0.6
+## [73] S4Arrays_1.4.1 viridisLite_0.4.2
+## [75] gtable_0.3.5 sass_0.4.9
+## [77] digest_0.6.37 SparseArray_1.4.8
+## [79] ggrepel_0.9.6 farver_2.1.2
+## [81] rjson_0.2.23 htmltools_0.5.8.1
+## [83] pkgdown_2.1.1 lifecycle_1.0.4
+## [85] httr_1.4.7 harmony_1.2.1
+
+
+
+