Contents

0.1 Preamble

library(COTAN)
library(zeallot)

# necessary to solve precedence of overloads
conflicted::conflict_prefer("%<-%", "zeallot")

# enable multi-processing (not on Windows)
prevOptState <- options(parallelly.fork.enable = TRUE)

1 Introduction

This tutorial shows the available tools in COTAN to help with genes’ clustering

1.0.1 Setup

Define a directory where the data and the output will be stored

dataDir <- file.path(tempdir(), "COTAN_vignette_data")
dir.create(dataDir, recursive = TRUE, showWarnings = FALSE)

print(dataDir)
#> [1] "/tmp/RtmpRABiog/COTAN_vignette_data"

outDir <- dataDir

# Log-level 2 was chosen to showcase better how the package works
# In normal usage a level of 0 or 1 is more appropriate
setLoggingLevel(2L)
#> Setting new log level to 2

# This file will contain all the logs produced by the package
# as if at the highest logging level
setLoggingFile(file.path(outDir, "vignette_DEA.log"))
#> Setting log file to be: /tmp/RtmpRABiog/COTAN_vignette_data/vignette_DEA.log

1.1 Retrieving the data-set

Download the data-set for mouse cortex E17.5 from Yuzwa et al. (2017)

GEO <- "GSM2861514"

dir.create(file.path(dataDir, GEO), showWarnings = FALSE)

datasetFileName <-
  file.path(dataDir, GEO, "GSM2861514_E175_All_Cells_DGE.txt.gz")


# retries up to 5 times to get the dataset
attempts <- 0L
maxAttempts <- 5L
ok <- FALSE

while (attempts < maxAttempts && !ok) {
  attempts <- attempts + 1L

  if (!file.exists(datasetFileName)) {
    res <- try(
      GEOquery::getGEOSuppFiles(
        GEO = GEO,
        makeDirectory = TRUE,
        baseDir = dataDir,
        filter_regex = base::basename(datasetFileName),
        fetch_files = TRUE
      ),
      silent = TRUE
    )
  }

  ok <- file.exists(datasetFileName)

  if (!ok && attempts < maxAttempts) {
    Sys.sleep(1)
  }
}

assertthat::assert_that(
  ok,
  msg = paste0(
    "Failed to retrieve file '", datasetFileName,
    "' after ", maxAttempts, " attempts."
  )
)
#> [1] TRUE

rawDataset <- read.csv(datasetFileName, sep = "\t", row.names = 1L)

print(dim(rawDataset))
#> [1] 17085  2000

1.1.1 Create the COTAN object

Initialize the COTAN object with the row count table and the metadata from the experiment.

cond <- "mouse_cortex_E17.5"

obj <- COTAN(raw = rawDataset)
obj <-
  initializeMetaDataset(
    obj,
    GEO = GEO,
    sequencingMethod = "Drop_seq",
    sampleCondition = cond
  )
#> Initializing `COTAN` meta-data

logThis(paste0("Condition ", getMetadataElement(obj, datasetTags()[["cond"]])),
        logLevel = 1L)
#> Condition mouse_cortex_E17.5

Assign to each cell its origin

# mark cells origin
data(vignette.cells.origin)
head(vignette.cells.origin, 18)
#> GCGAATTGTGAA ACCGATAACTGA CCCCGGGTGCGA CTGTAGATGTTA TCATCGAAGCGC TTCTACCGAGTC 
#>        Other        Other        Other        Other        Other        Other 
#> CTGTTCCCGGCG GCGTGTTAGTTC CTCGCGCGTTTA GATGTATAACTT GCGCTATGATTT CGTTTAGTTTAC 
#>        Other        Other        Other        Other        Other     Cortical 
#> GTGGAGGCCCAT TGTCACTACATC TCTAGAACAACG ACCTTTGTTCGT TTGTCTTCTTCG TAAAATATCGCC 
#>        Other        Other     Cortical     Cortical     Cortical     Cortical 
#> Levels: Cortical Other

obj <- 
  addCondition(obj, condName = "origin", conditions = vignette.cells.origin)

rm(vignette.cells.origin)

1.1.2 Drop low quality cells

# use previously established results to determine
# which cells were dropped in the cleaning stage
data(vignette.split.clusters)

cellsToDrop <-
  getCells(obj)[!(getCells(obj) %in% names(vignette.split.clusters))]

obj <- dropGenesCells(obj, cells = cellsToDrop)

# Log the remaining number of cells

logThis(paste("n cells", getNumCells(obj)), logLevel = 1L)
#> n cells 1783

table(getCondition(obj, condName = "origin"))
#> 
#> Cortical    Other 
#>      844      939

rm(vignette.split.clusters)

1.2 COTAN analysis

In this part, the COTAN model is calibrated, but the COEX matrix is not evaluated as it is not strictly necessary

obj <-
  proceedToCoex(
    obj,
    calcCoex = FALSE,
    optimizeForSpeed = TRUE,
    cores = 3L,
    deviceStr = "cuda"
  )
#> COTAN dataset analysis: START
#> Genes/cells selection done: dropped [4330] genes and [0] cells
#> Working on [12755] genes and [1783] cells
#> Estimate `dispersion`: START
#> Total calculations elapsed time: 8.49046087265015
#> Estimate `dispersion`: DONE
#> Estimate `dispersion`: DONE
#> `dispersion` | min: -0.0349260648055935 | max: 760.574661197634 | % negative: 12.5362602900823
#> COTAN genes' COEX estimation not requested
#> COTAN dataset analysis: DONE

2 Differential Expression Analysis

TODO:

2.0.1 Existing clusterizations

Loading pre-calculated clusterizations from data and call the DEAOnClusters(). This method estimates assess the differential expression of genes when partitioning cells into two subsets: the cluster and all other cells in the dataset. For each cluster COTAN calculates a correlation coefficient based on contingency tables that counts whether each gene occurs or not in cells inside or outside of the cluster.

These numbers are organized as one column for each cluster with a row for each gene and are a measure of the enrichment or depletion of the genes inside the clusters.

data("vignette.split.clusters", package = "COTAN")
data("vignette.merge.clusters", package = "COTAN")

vignette.split.clusters <-
  asClusterization(
    vignette.split.clusters, # a named vector/factor/data.frame
    allCells = getCells(obj) # used only to check names are coherent
  )
vignette.merge.clusters <-
  asClusterization(
    vignette.merge.clusters, # a named vector/factor/data.frame
    allCells = getCells(obj) # used only to check names are coherent
  )

# explicitly calculate the DEA of the clusterization to store it
vignette.split.coexDF <-
  DEAOnClusters(obj, clusters = vignette.split.clusters)
#> Differential Expression Analysis - START
#> ****************
#> Total calculations elapsed time: 2.27090072631836
#> Differential Expression Analysis - DONE
vignette.merge.coexDF <-
  DEAOnClusters(obj, clusters = vignette.merge.clusters)
#> Differential Expression Analysis - START
#> *********
#> Total calculations elapsed time: 2.29036808013916
#> Differential Expression Analysis - DONE

obj <-
  addClusterization(
    obj,
    clName = "split",
    clusters = vignette.split.clusters,
    coexDF = vignette.split.coexDF,
    override = FALSE
  )

obj <-
  addClusterization(
    obj,
    clName = "merge",
    clusters = vignette.merge.clusters,
    coexDF = vignette.merge.coexDF,
    override = FALSE
  )

# these will be recovered from the COTAN obj as needed
rm(vignette.split.clusters, vignette.split.coexDF)
rm(vignette.merge.clusters, vignette.merge.coexDF)

2.1 How to manipulate a clusterization

It is possible to improve on the labels in a clusterization, so that they look nicer in plots and lists, for example to ensure all labels have the same length.

Another improvement is to reorder the labels of the clusterization so that near clusters have near labels

# COTAn always stores clusterizations as factors.
splitClusters <- getClusters(obj, clName = "split")

# Use the utility factorToVector() to properly decay any named factor
# to a named char array
# splitClusters <- factorToVector(splitClusters)

# In this case the following calls are no-op since the clusterization
# were created by COTAN and so they already made nice and reordered

splitClusters <- niceFactorLevels(splitClusters)

c(splitClusters, splitCoexDF, perm) %<-% 
  reorderClusterization(
    obj,
    clName = "split",
    reverse = FALSE, 
    coexDF = NULL,
    useDEA = TRUE, # T: Cosine dist. on DEA, F: Eucl. dist. on avg. zero/one
    distance = NULL,
    hclustMethod = "ward.D2"
  )
#> Applied reordering to clusterization is:
#> 01  ->  01, 02  ->  02, 03  ->  03, 04  ->  04, 05  ->  05, 06  ->  06, 07  ->  07, 08  ->  08, 09  ->  09, 10  ->  10, 11  ->  11, 12  ->  12, 13  ->  13, 14  ->  14, 15  ->  15, -1  ->  -1

mergeClusters <- getClusters(obj, clName = "merge")

table(splitClusters, mergeClusters)
#>              mergeClusters
#> splitClusters  -1   2   3   4   5   6   7   8   9
#>            -1  76   0   0   0   0   0   0   0   0
#>            01   0 140   0   0   0   0   0   0   0
#>            02   0  86   0   0   0   0   0   0   0
#>            03   0   0  57   0   0   0   0   0   0
#>            04   0   0   0 126   0   0   0   0   0
#>            05   0   0 159   0   0   0   0   0   0
#>            06   0 152   0   0   0   0   0   0   0
#>            07   0   0   0   0  64   0   0   0   0
#>            08   0   0   0   0   0  75   0   0   0
#>            09   0   0   0   0   0  61   0   0   0
#>            10   0   0   0   0   0   0 197   0   0
#>            11   0   0  50   0   0   0   0   0   0
#>            12   0   0   0   0   0   0 137   0   0
#>            13   0   0   0   0   0   0 134   0   0
#>            14   0   0   0   0   0   0   0 145   0
#>            15   0   0   0   0   0   0   0   0 124

COTAN provides also easy facility to convert a clusterization from a factor to a list of array of cells and vice-versa. This can come useful if one wants to iterate separately on the cells of each cluster.

# this has an inverse `fromClustersList()`
splitClustersAsList <- toClustersList(splitClusters)

assertthat::assert_that(length(splitClustersAsList) == nlevels(splitClusters))
#> [1] TRUE

splitClustersOrigin <-
  rlang::set_names(
    rlang::rep_along(x = NA_character_, along = splitClustersAsList),
    names(splitClustersAsList)
  )

origin <- getCondition(obj, "origin")

for (clName in names(splitClustersAsList)) {
  cluster <- splitClustersAsList[[clName]]

  # assign most common origin to the cluster
  splitClustersOrigin[[clName]] <- names(which.max(table(origin[cluster])))

  # print the average non-zero expression in the cluster
  clRawData <- getRawData(obj)[, cluster, drop = FALSE]
  clRawData <- clRawData[clRawData > 0.0]

  cat(
    paste("Cluster", clName, "of", splitClustersOrigin[[clName]],
          "\torigin - average non-zero expression:", mean(clRawData)), "\n")

  rm(clRawData)
}
#> Cluster -1 of Cortical   origin - average non-zero expression: 1.71180287120671 
#> Cluster 01 of Cortical   origin - average non-zero expression: 1.52191105207488 
#> Cluster 02 of Cortical   origin - average non-zero expression: 1.48066495798062 
#> Cluster 03 of Cortical   origin - average non-zero expression: 1.66657934898977 
#> Cluster 04 of Cortical   origin - average non-zero expression: 1.82214332239237 
#> Cluster 05 of Cortical   origin - average non-zero expression: 1.67546880177161 
#> Cluster 06 of Cortical   origin - average non-zero expression: 1.48983230228427 
#> Cluster 07 of Cortical   origin - average non-zero expression: 1.61160678767489 
#> Cluster 08 of Other  origin - average non-zero expression: 1.70266450993636 
#> Cluster 09 of Other  origin - average non-zero expression: 1.56338877443345 
#> Cluster 10 of Other  origin - average non-zero expression: 1.45037851037851 
#> Cluster 11 of Other  origin - average non-zero expression: 1.59020765860164 
#> Cluster 12 of Other  origin - average non-zero expression: 1.4464105396024 
#> Cluster 13 of Other  origin - average non-zero expression: 1.50418066318555 
#> Cluster 14 of Other  origin - average non-zero expression: 1.46783633861169 
#> Cluster 15 of Other  origin - average non-zero expression: 1.52325240621915

# If one needs to reorder the cells by cluster,
# labels are ordered as in the clusterization
orderCellsByMergeCluster <- groupByClusters(mergeClusters)

plot(getNumExpressedGenes(obj)[orderCellsByMergeCluster],
     ylab = "Num expressed genes", xlab = NA_character_)


mergeClustersAsList <- toClustersList(mergeClusters)

mergeClustersOrigin <- 
  vapply(
    mergeClustersAsList,
    \(cluster, origin) {
      names(which.max(table(origin[cluster])))
    },
    FUN.VALUE = character(1L),
    origin
  )
names(mergeClustersOrigin) <- names(mergeClustersAsList)


# It is also possible to reorder a subset of cells using
orderCellsBySomeMergeClusters <- 
  groupByClustersList(
    getCells(obj),
    mergeClustersAsList[c(2, 4, 6)]
  )

plot(getNumExpressedGenes(obj)[orderCellsBySomeMergeClusters],
     ylab = "Library Size", xlab = NA_character_)

2.1.1 Clusters dendogram

It is possible to create a dendogram of the clusterization that uses a cluster distance based on the clusters’ COEX.

treePlot <-
  clustersTreePlot(
    obj,
    kCuts = 2L,
    clName = "split",
    useDEA = TRUE  # T: Cosine dist. on DEA, F: Eucl. dist. on avg. zero/one
  )[["dend"]]

plot(treePlot)


# use origin to mark the clusters
dendextend::labels(treePlot) <- splitClustersOrigin[base::labels(treePlot)]

plot(treePlot)

It is easy to see that the dendogram splits according to origin except for cluster 07 that is deemed more similar to the some of the non cortical cells.

2.2 Relevance of given marker genes

It is possible to visualize how relevant are some marker genes for the clusters comprising a given clusterization

# these are some genes associated to each cortical layer
layersGenes <- list(
  "L1"   = c("Reln",   "Lhx5"),
  "L2/3" = c("Cux1",   "Satb2"),
  "L4"   = c("Rorb",   "Sox5"),
  "L5/6" = c("Bcl11b", "Fezf2"),
  "Prog" = c("Hes1",    "Vim")
)

neuralTypeGenes <- list(
  # Neural Progenitor Genes
  "NPGs" = c("Nes", "Vim", "Sox2", "Sox1", "Notch1", "Hes1", "Hes5", "Pax6"),
  # Pan Neural Genes
  "PNGs" = c("Map2", "Tubb3", "Neurod1", "Nefm", "Nefl", "Dcx", "Tbr1"),
  # House Keeping
  "hk"   = c("Calm1", "Cox6b1", "Ppia", "Rpl18", "Cox7c", "Erh", "H3f3a",
             "Taf1", "Taf2", "Gapdh", "Actb", "Golph3", "Zfr", "Sub1",
             "Tars", "Amacr")
)

The following is one of the most comprehensive plot available in COTAN to visually summarize whether any of the given genes is strongly over/under expressed in the clusters. The heat-map shows the COEX score and the characters signal the corresponding adjusted p-value: *** for p < 0.001, ** for p < 0.01, * for p < 0.05, . for p < 0.1.

On the left it also allows to see how much each cluster is characterized by the passed conditions.

c(splitHeatmap, splitScoreDF, splitPValueDF) %<-%
  clustersMarkersHeatmapPlot(
    obj,
    groupMarkers = layersGenes,
    clName = "split",
    kCuts = 2L,
    adjustmentMethod = "bonferroni",
    condNameList = list("origin")
  )

ComplexHeatmap::draw(splitHeatmap)


c(mergeHeatmap, mergeScoreDF, mergePValueDF) %<-%
  clustersMarkersHeatmapPlot(
    obj,
    groupMarkers = neuralTypeGenes,
    clName = "merge",
    kCuts = 3L,
    adjustmentMethod = "bonferroni",
    condNameList = list("origin")
  )

ComplexHeatmap::draw(mergeHeatmap)

In the above graph, it is possible to see that the found clusters align well to the expression of the layers’ genes. Again it is possible to see that cluster 07 of the "split" clusterization (05 of the "merge") is likely composed by a progenitor cells instead of mature layers cells, like it happens for cluster 08 and 09 (06 of the "merge") even if they have supposedly different origin.

2.3 Find differentially expressed genes

The following function uses the calculated clusters’ COEX to select, for each cluster, the n genes that are over-expressed and the n genes that are under-expressed with respect to a neutral model assumption.

For each gene found, the function returns the COEX value, the adjusted p-value and the log-fold-change. It also flags whether the found gene is one of the markers provided by the user.

mergeClusterMarkers <-
  findClustersMarkers(
    obj,
    clName = "merge",
    n = 5L,
    markers = unlist(layersGenes),
    adjustmentMethod = "bonferroni"
  )
#> findClustersMarkers - START
#> Log Fold Change Analysis - START
#> *********
#> Total calculations elapsed time: 8.1580331325531
#> Log Fold Change Analysis - DONE
#> Total calculations elapsed time: 8.24346661567688
#> findClustersMarkers - DONE

foundMarkers <- list()

# All relevant genes with strong `p-values`
geneIsEnriched <- mergeClusterMarkers[, "adjPVal"] < 1e-10

for (clName in levels(mergeClusters)) {
  geneIsInCluster <- mergeClusterMarkers[, "CL"] == clName
  foundMarkers[[clName]] <-
    mergeClusterMarkers[geneIsEnriched & geneIsInCluster, "Gene", drop = TRUE]
}

# number of genes per cluster
lengths(foundMarkers)
#> -1  2  3  4  5  6  7  8  9 
#>  8 10  8  5 10 10 10  5  5

2.3.1 Relevance of marker genes for the merge clusters

Again, it is possible to visualize how relevant are the marker genes from above:

c(mergeHeatmap, ..) %<-%
  clustersMarkersHeatmapPlot(
    obj,
    groupMarkers = foundMarkers,
    clName = "merge",
    condNameList = list("origin")
  )

ComplexHeatmap::draw(mergeHeatmap)

2.4 DEA cluster vs. cluster

Sometimes it is useful to compare the genes’ expression of a pair of clusters.

In such cases the simplest thing to do is to drop all other cells so to obtain a leaner COTAN object with just the 2 clusters and then use the procedures shown above to analyze the resulting object.

# We will focus on the clusters `03` (likely part of layers 5/6) and
# `05` (likely part of layers 2/3) of the `split` clusterization

cellsToDrop <- getCells(obj)[!(splitClusters %in% c("03", "05"))]

obj2 <- dropGenesCells(obj, cells = cellsToDrop)

obj2 <- proceedToCoex(obj2, calcCoex = FALSE)
#> COTAN dataset analysis: START
#> Genes/cells selection done: dropped [1847] genes and [0] cells
#> Working on [10908] genes and [216] cells
#> Estimate `dispersion`: START
#> Total calculations elapsed time: 10.5938677787781
#> Estimate `dispersion`: DONE
#> Estimate `dispersion`: DONE
#> `dispersion` | min: -0.255047679698161 | max: 329.177575870151 | % negative: 39.035570223689
#> COTAN genes' COEX estimation not requested
#> COTAN dataset analysis: DONE

table(getClusters(obj2, clName = "split"))
#> 
#>  03  05 
#>  57 159
# this does not give more information than the same full-plot above

c(splitHeatmap2, ., .) %<-%
  clustersMarkersHeatmapPlot(
    obj2,
    groupMarkers = layersGenes,
    clName = "split",
    kCuts = 2L,
    adjustmentMethod = "bonferroni"
  )

ComplexHeatmap::draw(splitHeatmap2)

In the case of only two clusters, over-expressed genes in a cluster correspond exactly to under-expressed genes in the other, so one can just look at the former

deaMarkers <- findClustersMarkers(
  obj2,
  n = 10L,
  clName = "split",
  adjustmentMethod = "bonferroni",
  markers = layersGenes[c("L2/3", "L5/6")]
)
#> findClustersMarkers - START
#> Differential Expression Analysis - START
#> **
#> Total calculations elapsed time: 0.112128973007202
#> Differential Expression Analysis - DONE
#> Log Fold Change Analysis - START
#> **
#> Total calculations elapsed time: 1.71864628791809
#> Log Fold Change Analysis - DONE
#> Total calculations elapsed time: 1.85508584976196
#> findClustersMarkers - DONE

# over-expressed genes follow the under-expressed ones
deaMarkers[11:20, ]
#>    CL   Gene       DEA      adjPVal IsMarker logFoldCh
#> 11 03   Tle4 0.6217894 6.913771e-16        0 1.8051700
#> 12 03   Sox5 0.5951961 2.378094e-14        0 1.0266744
#> 13 03  Fezf2 0.5728092 4.158114e-13        1 1.1712330
#> 14 03  Islr2 0.5653865 1.048763e-12        0 0.9064809
#> 15 03 Igfbp3 0.5410088 2.015290e-11        0 1.8890878
#> 16 03   Meg3 0.4998109 2.232866e-09        0 0.8873548
#> 17 03   Rprm 0.4844676 1.175645e-08        0 0.8973052
#> 18 03   Lmo3 0.4827412 1.412830e-08        0 0.9724055
#> 19 03   Xpr1 0.4753437 3.083012e-08        0 0.8104060
#> 20 03 Bcl11b 0.4735215 3.729749e-08        1 0.9109299
deaMarkers[31:40, ]
#>    CL          Gene       DEA      adjPVal IsMarker logFoldCh
#> 31 05           Ptn 0.6066787 5.258093e-15        0 0.8646113
#> 32 05 2610017I09Rik 0.5719150 4.651280e-13        0 0.8172259
#> 33 05 9130024F11Rik 0.5436780 1.467117e-11        0 0.7805391
#> 34 05         Satb2 0.4949984 3.779829e-09        1 0.8713031
#> 35 05         Eif1b 0.4836402 1.283989e-08        0 0.9989785
#> 36 05        Abracl 0.4442443 7.220736e-07        0 0.8424504
#> 37 05          Cux1 0.4055112 2.755236e-05        1 1.4075842
#> 38 05         Ttc28 0.3995315 4.699765e-05        0 0.7459037
#> 39 05         Hmgn1 0.3332606 1.056564e-02        0 0.5423027
#> 40 05       Macrod2 0.3172522 3.405310e-02        0 0.8404755

We can see that in this case we recover the layers genes.

2.5 Vignette clean-up stage

The next few lines are just to clean.

if (file.exists(file.path(outDir, paste0(cond, ".cotan.RDS")))) {
  # delete file if it exists
  file.remove(file.path(outDir, paste0(cond, ".cotan.RDS")))
}
if (file.exists(file.path(outDir, paste0(cond, "_times.csv")))) {
  # delete file if it exists
  file.remove(file.path(outDir, paste0(cond, "_times.csv")))
}
if (dir.exists(file.path(outDir, cond))) {
  unlink(file.path(outDir, cond), recursive = TRUE)
}
# if (dir.exists(file.path(outDir, GEO))) {
#   unlink(file.path(outDir, GEO), recursive = TRUE)
# }

# stop logging to file
setLoggingFile("")
#> Closing previous log file - Setting log file to be:
file.remove(file.path(outDir, "vignette_uniform_clustering.log"))
#> Warning in file.remove(file.path(outDir, "vignette_uniform_clustering.log")):
#> cannot remove file
#> '/tmp/RtmpRABiog/COTAN_vignette_data/vignette_uniform_clustering.log', reason
#> 'No such file or directory'
#> [1] FALSE

options(prevOptState)
Sys.time()
#> [1] "2026-03-29 17:03:08 EDT"

sessionInfo()
#> R Under development (unstable) (2026-03-05 r89546)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#> 
#> Matrix products: default
#> BLAS:   /home/biocbuild/bbs-3.23-bioc/R/lib/libRblas.so 
#> LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_GB              LC_COLLATE=C              
#>  [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: America/New_York
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] zeallot_0.2.0    COTAN_2.11.4     BiocStyle_2.39.0
#> 
#> loaded via a namespace (and not attached):
#>   [1] RcppAnnoy_0.0.23            splines_4.6.0              
#>   [3] later_1.4.8                 tibble_3.3.1               
#>   [5] polyclip_1.10-7             XML_3.99-0.23              
#>   [7] fastDummies_1.7.5           httr2_1.2.2                
#>   [9] lifecycle_1.0.5             doParallel_1.0.17          
#>  [11] globals_0.19.1              lattice_0.22-9             
#>  [13] MASS_7.3-65                 ggdist_3.3.3               
#>  [15] dendextend_1.19.1           magrittr_2.0.4             
#>  [17] limma_3.67.0                plotly_4.12.0              
#>  [19] sass_0.4.10                 rmarkdown_2.31             
#>  [21] jquerylib_0.1.4             yaml_2.3.12                
#>  [23] httpuv_1.6.17               otel_0.2.0                 
#>  [25] Seurat_5.4.0                sctransform_0.4.3          
#>  [27] spam_2.11-3                 sp_2.2-1                   
#>  [29] spatstat.sparse_3.1-0       reticulate_1.45.0          
#>  [31] cowplot_1.2.0               pbapply_1.7-4              
#>  [33] RColorBrewer_1.1-3          abind_1.4-8                
#>  [35] Rtsne_0.17                  GenomicRanges_1.63.1       
#>  [37] purrr_1.2.1                 BiocGenerics_0.57.0        
#>  [39] rappdirs_0.3.4              circlize_0.4.17            
#>  [41] IRanges_2.45.0              S4Vectors_0.49.0           
#>  [43] ggrepel_0.9.8               irlba_2.3.7                
#>  [45] listenv_0.10.1              spatstat.utils_3.2-2       
#>  [47] rentrez_1.2.4               goftest_1.2-3              
#>  [49] RSpectra_0.16-2             spatstat.random_3.4-5      
#>  [51] fitdistrplus_1.2-6          parallelly_1.46.1          
#>  [53] codetools_0.2-20            DelayedArray_0.37.0        
#>  [55] xml2_1.5.2                  tidyselect_1.2.1           
#>  [57] shape_1.4.6.1               farver_2.1.2               
#>  [59] ScaledMatrix_1.19.0         viridis_0.6.5              
#>  [61] matrixStats_1.5.0           stats4_4.6.0               
#>  [63] spatstat.explore_3.8-0      Seqinfo_1.1.0              
#>  [65] jsonlite_2.0.0              GetoptLong_1.1.0           
#>  [67] progressr_0.18.0            ggridges_0.5.7             
#>  [69] survival_3.8-6              iterators_1.0.14           
#>  [71] systemfonts_1.3.2           foreach_1.5.2              
#>  [73] tools_4.6.0                 ragg_1.5.2                 
#>  [75] ica_1.0-3                   Rcpp_1.1.1                 
#>  [77] glue_1.8.0                  gridExtra_2.3              
#>  [79] SparseArray_1.11.11         xfun_0.57                  
#>  [81] distributional_0.7.0        MatrixGenerics_1.23.0      
#>  [83] ggthemes_5.2.0              dplyr_1.2.0                
#>  [85] withr_3.0.2                 BiocManager_1.30.27        
#>  [87] fastmap_1.2.0               digest_0.6.39              
#>  [89] rsvd_1.0.5                  parallelDist_0.2.7         
#>  [91] R6_2.6.1                    mime_0.13                  
#>  [93] textshaping_1.0.5           colorspace_2.1-2           
#>  [95] Cairo_1.7-0                 scattermore_1.2            
#>  [97] tensor_1.5.1                dichromat_2.0-0.1          
#>  [99] spatstat.data_3.1-9         tidyr_1.3.2                
#> [101] generics_0.1.4              data.table_1.18.2.1        
#> [103] httr_1.4.8                  htmlwidgets_1.6.4          
#> [105] S4Arrays_1.11.1             uwot_0.2.4                 
#> [107] pkgconfig_2.0.3             gtable_0.3.6               
#> [109] ComplexHeatmap_2.27.1       lmtest_0.9-40              
#> [111] S7_0.2.1                    SingleCellExperiment_1.33.2
#> [113] XVector_0.51.0              htmltools_0.5.9            
#> [115] dotCall64_1.2               bookdown_0.46              
#> [117] zigg_0.0.2                  clue_0.3-68                
#> [119] SeuratObject_5.3.0          scales_1.4.0               
#> [121] Biobase_2.71.0              png_0.1-9                  
#> [123] spatstat.univar_3.1-7       knitr_1.51                 
#> [125] tzdb_0.5.0                  reshape2_1.4.5             
#> [127] rjson_0.2.23                curl_7.0.0                 
#> [129] nlme_3.1-169                proxy_0.4-29               
#> [131] cachem_1.1.0                zoo_1.8-15                 
#> [133] GlobalOptions_0.1.3         stringr_1.6.0              
#> [135] KernSmooth_2.23-26          parallel_4.6.0             
#> [137] miniUI_0.1.2                GEOquery_2.79.0            
#> [139] pillar_1.11.1               grid_4.6.0                 
#> [141] vctrs_0.7.1                 RANN_2.6.2                 
#> [143] promises_1.5.0              BiocSingular_1.27.1        
#> [145] beachmat_2.27.3             xtable_1.8-8               
#> [147] cluster_2.1.8.2             evaluate_1.0.5             
#> [149] magick_2.9.1                tinytex_0.59               
#> [151] readr_2.2.0                 cli_3.6.5                  
#> [153] compiler_4.6.0              rlang_1.1.7                
#> [155] crayon_1.5.3                future.apply_1.20.2        
#> [157] labeling_0.4.3              plyr_1.8.9                 
#> [159] stringi_1.8.7               viridisLite_0.4.3          
#> [161] deldir_2.0-4                BiocParallel_1.45.0        
#> [163] assertthat_0.2.1            lazyeval_0.2.2             
#> [165] spatstat.geom_3.7-3         Matrix_1.7-5               
#> [167] RcppHNSW_0.6.0              hms_1.1.4                  
#> [169] patchwork_1.3.2             future_1.70.0              
#> [171] conflicted_1.2.0            ggplot2_4.0.2              
#> [173] statmod_1.5.1               shiny_1.13.0               
#> [175] SummarizedExperiment_1.41.1 ROCR_1.0-12                
#> [177] Rfast_2.1.5.2               memoise_2.0.1              
#> [179] igraph_2.2.2                RcppParallel_5.1.11-2      
#> [181] bslib_0.10.0