Last updated: 2025-05-23

Checks: 7 0

Knit directory: CosMx_pipeline_LGA/

This reproducible R Markdown analysis was created with workflowr (version 1.7.1). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(20250517) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version 51b3104. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    .Rproj.user/
    Ignored:    NBClust-Plots/
    Ignored:    analysis/figure/
    Ignored:    data/flatFiles/CoronalHemisphere/Run1000_S1_Half_exprMat_file.csv
    Ignored:    data/flatFiles/CoronalHemisphere/Run1000_S1_Half_fov_positions_file.csv
    Ignored:    data/flatFiles/CoronalHemisphere/Run1000_S1_Half_metadata_file.csv
    Ignored:    output/processed_data/Log/
    Ignored:    output/processed_data/RC/
    Ignored:    output/processed_data/SCT/
    Ignored:    output/processed_data/exprMat_unfiltered.RDS
    Ignored:    output/processed_data/fov_positions_unfiltered.RDS
    Ignored:    output/processed_data/metadata_unfiltered.RDS
    Ignored:    output/processed_data/negMat_unfiltered.RDS
    Ignored:    output/processed_data/seu_filtered.RDS
    Ignored:    output/processed_data/seu_semifiltered.RDS

Untracked files:
    Untracked:  analysis/images/

Unstaged changes:
    Modified:   analysis/_site.yml
    Modified:   output/performance_reports/0.0_data_loading_PR.csv
    Modified:   output/performance_reports/1.0_qc_and_filtering_PR.csv
    Modified:   output/performance_reports/2.0_normalization_PR.csv
    Modified:   output/performance_reports/3.0_dimensional_reduction_PR.csv
    Modified:   output/performance_reports/4.0_insitutype_cell_typing_PR.csv
    Modified:   output/performance_reports/4.1_insitutype_unsup_clustering_PR.csv
    Modified:   output/performance_reports/4.2_seurat_unsup_clustering_PR.csv
    Modified:   output/performance_reports/5.0_RC_normalization_PR.csv
    Modified:   output/performance_reports/5.1_RC_dimensional_reduction_PR.csv
    Modified:   output/performance_reports/6.0_Log_normalization_PR.csv
    Modified:   output/performance_reports/6.1_Log_dimensional_reduction_PR.csv
    Modified:   output/performance_reports/pipeline_PR.csv

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (analysis/1.0_qc_and_filtering.Rmd) and HTML (docs/1.0_qc_and_filtering.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
html 51b3104 lidiaga 2025-05-23 Build site.
html 8a9f079 lidiaga 2025-05-20 Build site.
Rmd 8410a10 lidiaga 2025-05-17 Add Rmds files in analysis

Dependencies

library(data.table) # Efficient data management
library(Matrix) # Sparse matrices
library(here) # Enhanced file referencing in project-oriented workflows
library(dplyr) # For the use of pipes %>%
library(kableExtra) # For table formatting
library(Seurat) # Seurat object
library(ggplot2) # Graphics
library(patchwork) # Layout graphics

Load the data

First of all, data needs to be loaded into the session. For this script, only the created Seurat object is needed. However, the pipeline can also be started here if the Seurat object from AtoMx is available.

# Load Seurat object (from previous script or from AtoMx, select in YAML)
option <- "previous" # Select "previous" or "AtoMx"

if (option == "previous") {
  seu <- readRDS(here("output","processed_data","seu_semifiltered.RDS"))
} else if (option == "AtoMx") {
  seuAtoMx_dir <- here("data", "seuAtoMx")
  seuAtoMx_name <- dir(seuAtoMx_dir)
  seu <- readRDS(here(seuAtoMx_dir,seuAtoMx_name))
} else {
  stop("The selected method is invalid, choose: 'previous' or 'AtoMx'")
}

Visualizations

FOV arrangement

In CosMx SMI, slides might contain more than one sample/tissue per slide. If this was the case, this information can be added based on the FOVs of each sample. In this sense, visualizing the FOV arrangement and assign each FOV to its corresponding tissue could be interesting.

## Code adapted from CosMxLite vignette

# FOV arrangement in Slide
ggplot(seu@meta.data, 
       aes(x = seu$x_FOV_px, y = seu$y_FOV_px, label = seu$fov)) +
  geom_point(size = 5, colour = "lightblue", shape = 15) +
  geom_text(size = 3) +
  theme_classic() +
  labs(title = "FOV Arrangement",
       x = "Global X Position (px)",
       y = "Global Y Position (px)") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold", margin = margin(b = 15)),
        axis.title.x = element_text(margin = margin(t = 15)),
        axis.title.y = element_text(margin = margin(r = 15)),
        panel.grid.major = element_line(colour = "grey92", linetype = "solid"))

Version Author Date
8a9f079 lidiaga 2025-05-20

In this dataset, there is no information of multiple samples being scanned on the same slide, so it has been assumed that all FOVs belong to the same sample/tissue. Another possibility, for single-sample slides like this one, would be to differentiate areas within the tissue, in case you want to explore them separately (see CosMxLite vignette for more information).

Cells

Another interesting preliminary visualization would be to observe how are cells distributed based on different attributes, such as its area or the intensity of a particular stain.

## Code adapted from CosMxLite vignette

# Cell arrangement by size
p1 <- ggplot(seu@meta.data,
             aes(x = seu$CenterX_global_px, y = seu$CenterY_global_px, colour = log2(seu$Area))) +
  geom_point(size = 0.01) + 
  theme_classic() + 
  scale_colour_gradientn(colours = c("grey50", "blue", "yellow", "red")) + 
  labs(title = "Area (log2)",
       x = "Global X Position (px)",
       y = "Global Y Position (px)") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold", margin = margin(b = 15)),
        axis.title.x = element_text(margin = margin(t = 15)),
        axis.title.y = element_text(margin = margin(r = 15)),
        panel.grid.major = element_line(colour = "grey92", linetype = "solid"),
        legend.position = "right")

# Cell arrangement by DAPI stainning
p2 <- ggplot(seu@meta.data,
             aes(x = seu$CenterX_global_px, y = seu$CenterY_global_px, colour = log2(seu$Mean.DAPI))) + 
  geom_point(size = 0.01) + 
  theme_classic() + 
  scale_colour_gradientn(colours = c("grey50", "yellow", "purple")) + 
  labs(title = "DAPI stainning (log2)",
       x = "Global X Position (px)",
       y = "Global Y Position (px)") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold", margin = margin(b = 15)),
        axis.title.x = element_text(margin = margin(t = 15)),
        axis.title.y = element_text(margin = margin(r = 15)),
        panel.grid.major = element_line(colour = "grey92", linetype = "solid"),
        legend.position = "right")

# Arrange plots
p1 + p2

Version Author Date
8a9f079 lidiaga 2025-05-20

Quality control

Nanostring recommendations on how to filter CosMx data can be found in several posts from their CosMx Scratch Space blog. In general, basic QC can be made through filtering bad quality FOVs and/or cells.

Unassigned transcripts

Although no especial recommendations have been found on QC based on unassigned transcripts, visualizing the fraction of unassigned transcripts per FOV might be helpful to understand if any of them have an excessively high amount or if they are all more or less in the same values.

# Summarize Unassigned Transcripts per FOV
un_tx_summary <- seu@meta.data %>%
  select(fov, unassignedTranscripts) %>%
  distinct() %>%
  arrange(fov)

# Plot
ggplot(un_tx_summary, aes(x = as.factor(fov), y = unassignedTranscripts)) +
  geom_col(fill = "lightblue", width = 0.7) +
  scale_y_continuous(limits = c(0, 1),
                     breaks = seq(0, 1, by = 0.1),
                     expand = expansion(mult = c(0, 0.05))) +
  theme_minimal() +
  labs(title = "Fraction of Unassigned Transcripts per FOV",
       x = "Field of View (FOV)",
       y = "Fraction of Unassigned Transcripts") +
  theme(plot.title = element_text(face = "bold", size = 16),
        axis.title.x = element_text(margin = margin(t = 15)),
        axis.title.y = element_text(margin = margin(r = 15)),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1))

Version Author Date
8a9f079 lidiaga 2025-05-20

In this case, all FOVs are more or less within the same range, and none of them present extreme values.

FOV QC

Following Nanostring CosMx Scratch Space blog post of FOV QC and the Nanostring’s CosMx Scratch Space vignette, quality control of bad quality FOVs can be made based on the following indicators of artifacts: diminished total counts and/or distorted expression profiles. In practice, this means:

  • Filter FOVs where total expression is suppressed, typically more than a 60% signal loss.
  • Filter FOVs where at multiple reporter probes are underexpressed.

Special functions from NanoString to perform FOV QC can be found here.

## Code adapted from Scratch Space vignette

# Load tools for FOV QC from Nanostring CosMx Scratch Space github
source("https://raw.githubusercontent.com/Nanostring-Biostats/CosMx-Analysis-Scratch-Space/Main/_code/FOV%20QC/FOV%20QC%20utils.R")

CosMx_barcodes <- readRDS(url("https://github.com/Nanostring-Biostats/CosMx-Analysis-Scratch-Space/raw/Main/_code/FOV%20QC/barcodes_by_panel.RDS"))

panel_barcode <- CosMx_barcodes$Mm_Neuro # Select the appropriate panel here

# Prepare the necessary data to run the FOV QC method
counts <- t(GetAssayData(seu, assay = "RNA", slot = "counts")) # Cells x genes
xy <- as.matrix(data.table(X_px = seu$CenterX_global_px, 
                           Y_px = seu$CenterY_global_px))
rownames(xy) <- rownames(seu@meta.data)

# Run the FOV QC method
fovqc <- runFOVQC(counts = counts, xy = xy, fov = seu$fov, barcodemap = panel_barcode,
                  max_prop_loss = 0.6, max_totalcounts_loss = 0.6) # default

fovqc50 <- runFOVQC(counts = counts, xy = xy, fov = seu$fov, barcodemap = panel_barcode,
                  max_prop_loss = 0.6, max_totalcounts_loss = 0.5) # trial

# Results
if (length(fovqc$flaggedfovs) == 0) {
  flaggedFOVs <- 0
  flaggedFOVs_signal <- 0
  flaggedFOVs_bias <- 0
  flaggedFOVsCells <- character(0)
} else {
  flaggedFOVs <- fovqc$flaggedfovs
  flaggedFOVs_signal <- fovqc$flaggedfovs_fortotalcounts
  flaggedFOVs_bias <- fovqc$flaggedfovs_forbias
  flaggedFOVsCells <- rownames(seu@meta.data[(seu@meta.data$fov %in% flaggedFOVs), ])
}
  • FOVs with > 60% of signal loss: 69, 95.
  • FOVs with reporter bias: .
  • Flagged FOVs: 69, 95.
  • Flagged FOVs’ Cells: 481 -> c_1_69_1, c_1_69_2, c_1_69_3, c_1_69_4, c_1_69_5, c_1_69_6…

In this case, two bad quality FOVs have been found, reported signal loss. The following visualization will provide some more depth into the results:

## Code adapted from Scratch Space vignette

# Plot results from signal loss
par(mfrow = c(1, 2),
    oma = c(0, 0, 4, 0))

FOVSignalLossSpatialPlot(fovqc)
mapFlaggedFOVs(fovqc)

mtext("Signal loss threshold: > 60%", outer = TRUE, line = 1, at = 0.5, cex = 1.5, font = 2)

Version Author Date
8a9f079 lidiaga 2025-05-20
par(mfrow = c(1, 1))

As it can be observed, signal strength varies across FOVs relatively smoothly. However, two FOVs appear to have an overall lower signal.

In terms of reporter bias, no reporter probes have been detected to be underexpressed and, therefore, no FOVs have been flagged based on bias reporters:

## Code adapted from Scratch Space vignette

# Plot results from reporter bias
FOVEffectsHeatmap(fovqc50) 

Version Author Date
8a9f079 lidiaga 2025-05-20

Cell QC

As recommended by Nanostring in their CosMx Scratch Space blog, cell QC can be made by:

  • Filtering cells with very low counts, to avoid undersampled cells: <= 20 for the 1000-plex panel and <= 50 for the 6000-plex panel.
  • Filtering cells with high outlier areas, to avoid multiplets: evaluate appropriate area cutoff with an outliers test or an histogram.

Another common filtering methods seen in CosMx related literature and also supported by the CosMxLite vignette are:

  • Filter cells with <= 10 features, to avoid cells with very low number of detected genes that would be difficult to annotate.
  • Filter cells with >= 5% of negative probes, as these account for hybridization to non existing genes and suggest bad quality.
# Calculate percentage of negative and System Control probes
seu[["percent.NegPrb"]] <- seu$nCount_negprobes / (seu$nCount_RNA + seu$nCount_negprobes) * 100
seu[["percent.SysCon"]] <- seu$nCount_falsecode / (seu$nCount_RNA + seu$nCount_falsecode) * 100
## Code adapted from CosMxLite vignette
## Points are removed as CosMx datasets can contain over 1 million cells and may hide the violin plot

# Violin plot
p1 <- VlnPlot(seu, features = c("nFeature_RNA", "nCount_RNA", "percent.NegPrb", "Area"), ncol = 4, pt.size = 0)

# Feature Scatter plots
p2 <- FeatureScatter(seu, feature1 = "nCount_RNA", feature2 = "percent.NegPrb") + NoLegend()
p3 <- FeatureScatter(seu, feature1 = "nCount_RNA", feature2 = "nFeature_RNA") + NoLegend()
p4 <- FeatureScatter(seu, feature1 = "Area", feature2 = "nCount_RNA") + NoLegend()

# Arrange plots
p1 / (p2 + p3 + p4)

Version Author Date
8a9f079 lidiaga 2025-05-20

In sight of the violin plots, this dataset presents a low percentage of Negative probes, with some outliers that match those cells with the lower transcript counts and lower unique features. Therefore, the suggested filter cutoffs seem appropriate for this case.

Some additional cutoff could be set in >= 9000 total transcript counts and >= 60000 of area, approximately, to remove some potential outliers or multiplets.

## Code inspired by CosMxLite vignette

# Select filter thresholds
min_counts <- 20
max_counts <- 9000
min_features <- 10
max_neg_perc <- 5
max_area <- 60000

# Counts
flag1 <- sum(seu$nCount_RNA <= min_counts)
flag2 <- sum(seu$nCount_RNA >= max_counts)
p1 <- ggplot(seu@meta.data, aes(x = seu$nCount_RNA)) +
  geom_histogram(bins = 100, fill = "lightblue", colour = "black") +
  geom_vline(xintercept = min_counts, colour = "red", lwd = 1) +
  geom_vline(xintercept = max_counts, colour = "red", lwd = 1) +
  ggtitle(paste("Nº cells with nCount_RNA <=", min_counts, ":", flag1,
                "\nNº cells with nCount_RNA >=", max_counts, ":", flag2)) + 
  theme_minimal()

# Feature
flag <- sum(seu$nFeature_RNA <= min_features)
p2 <- ggplot(seu@meta.data, aes(x = seu$nFeature_RNA)) +
  geom_histogram(bins = 100, fill = "lightblue", colour = "black") +
  geom_vline(xintercept = min_features, colour = "red", lwd = 1) +
  ggtitle(paste("Nº cells with nFeature_RNA <=", min_features, ":", flag)) + 
  theme_minimal()

# NegPrb percentage
flag <- sum(seu$percent.NegPrb >= max_neg_perc)
p3 <- ggplot(seu@meta.data, aes(x = seu$percent.NegPrb)) +
  geom_histogram(bins = 100, fill = "lightblue", colour = "black") +
  geom_vline(xintercept = max_neg_perc, colour = "red", lwd = 1) +
  ggtitle(paste("Nº cells with percent.NegPrb >=", max_neg_perc, ":", flag)) + 
  theme_minimal()

# Area
flag <- sum(seu$Area >= max_area)
p4 <- ggplot(seu@meta.data, aes(x = seu$Area)) +
  geom_histogram(bins = 100, fill = "lightblue", colour = "black") +
  geom_vline(xintercept = max_area, colour = "red", lwd = 1) +
  ggtitle(paste("Nº cells with Area >=", max_area, ":", flag)) + 
  theme_minimal()

# Arrange plots
(p1 + p2) / (p3 + p4)

Version Author Date
8a9f079 lidiaga 2025-05-20

As it can be observed, there are not many cells affected by the suggested cutoffs, therefore, the majority of the cells present good enough quality to go ahead with downstream analysis.

Filtering

Filter bad quality FOVs

# Filtering FOVs
pre <- dim(seu)[2] # Cells pre filtering

keep_cells <- setdiff(Cells(seu), flaggedFOVsCells) # Keep cells from unflagged FOVs
seu <- subset(seu, cells = keep_cells)

post <- dim(seu)[2] # Cells post filtering

After filtering all the cells from bad quality FOVs the dataset has been reduced from 48549 cells to 48068, a 1% of cells have been removed.

## Code adapted from CosMxLite vignette

# FOV arrangement in Slide
ggplot(seu@meta.data, 
       aes(x = seu$x_FOV_px, y = seu$y_FOV_px, label = seu$fov)) +
  geom_point(size = 5, colour = "lightblue", shape = 15) +
  geom_text(size = 3) +
  theme_classic() +
  labs(title = "FOV Arrangement",
       x = "Global X Position (px)",
       y = "Global Y Position (px)") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold", margin = margin(b = 15)),
        axis.title.x = element_text(margin = margin(t = 15)),
        axis.title.y = element_text(margin = margin(r = 15)),
        panel.grid.major = element_line(colour = "grey92", linetype = "solid"))

Version Author Date
8a9f079 lidiaga 2025-05-20

As it can be observed, FOVs 69, 95 are now not present in the dataset.

Filter bad quality Cells

## Code adapted from CosMxLite vignette

# Filtering cells
pre <- dim(seu)[2] # Cells pre filtering

seu <- subset(seu, 
              subset = nCount_RNA > min_counts & 
                nCount_RNA < max_counts & 
                nFeature_RNA > min_features & 
                percent.NegPrb < max_neg_perc &
                Area < max_area)

post <- dim(seu)[2] # Cells post filtering

After filtering the dataset has been reduced from 48068 cells to 47918. Therefore, an additional 0.3% of cells have been filtered out.

## Code adapted from CosMxLite vignette
## Points are removed from violin plots as CosMx datasets can contain over 1 million cells and may hide the violin plot

# Violin plot
p1 <- VlnPlot(seu, features = c("nFeature_RNA", "nCount_RNA", "percent.NegPrb", "Area"), ncol = 4, pt.size = 0)

# Feature Scatter plots
p2 <- FeatureScatter(seu, feature1 = "nCount_RNA", feature2 = "percent.NegPrb") + NoLegend()
p3 <- FeatureScatter(seu, feature1 = "nCount_RNA", feature2 = "nFeature_RNA") + NoLegend()
p4 <- FeatureScatter(seu, feature1 = "Area", feature2 = "nCount_RNA") + NoLegend()

# Arrange plots
p1 / (p2 + p3 + p4)

Version Author Date
8a9f079 lidiaga 2025-05-20

Save the Seurat object

# Save the Seurat object
if(!dir.exists(here("output","processed_data"))){
  dir.create(here("output","processed_data"))
}

saveRDS(seu, here("output","processed_data", "seu_filtered.RDS"))

Performance and Session Info

Performance Report
Chunk Time_sec Memory_Mb
Libraries 2.13 146.8
LoadData 1.93 427.0
VizFOV 2.09 23.6
VizCells 3.36 13.8
VizUnTx 0.46 4.2
FOVQc 6.57 215.1
VizFOVQC1 1.14 5.3
VizFOVQC2 0.46 2.4
FalseProbesPerc 0.23 1.4
VizCellQC1 7.61 27.8
VizCellQC2 0.93 -5.2
FOVFiltering 1.12 8.2
VizFOVPostFilt 2.05 13.2
CellFiltering 0.91 8.5
VizPostFilt 7.45 6.8
SavingSeuObj 15.24 0.0
Total 53.68 898.9

sessionInfo()
R version 4.4.3 (2025-02-28 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22000)

Matrix products: default


locale:
[1] LC_COLLATE=Spanish_Spain.utf8  LC_CTYPE=Spanish_Spain.utf8   
[3] LC_MONETARY=Spanish_Spain.utf8 LC_NUMERIC=C                  
[5] LC_TIME=Spanish_Spain.utf8    

time zone: Europe/Madrid
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] patchwork_1.3.0    ggplot2_3.5.1      SeuratObject_4.1.4 Seurat_4.4.0      
 [5] kableExtra_1.4.0   dplyr_1.1.4        here_1.0.1         Matrix_1.7-2      
 [9] data.table_1.17.0  workflowr_1.7.1   

loaded via a namespace (and not attached):
  [1] RColorBrewer_1.1-3     rstudioapi_0.17.1      jsonlite_1.9.1        
  [4] magrittr_2.0.3         ggbeeswarm_0.7.2       spatstat.utils_3.1-2  
  [7] farver_2.1.2           rmarkdown_2.29         fs_1.6.5              
 [10] vctrs_0.6.5            ROCR_1.0-11            spatstat.explore_3.3-4
 [13] htmltools_0.5.8.1      sass_0.4.9             sctransform_0.4.1     
 [16] parallelly_1.43.0      KernSmooth_2.23-26     bslib_0.9.0           
 [19] htmlwidgets_1.6.4      ica_1.0-3              plyr_1.8.9            
 [22] plotly_4.10.4          zoo_1.8-13             cachem_1.1.0          
 [25] whisker_0.4.1          igraph_2.1.4           mime_0.12             
 [28] lifecycle_1.0.4        pkgconfig_2.0.3        R6_2.6.1              
 [31] fastmap_1.2.0          fitdistrplus_1.2-2     future_1.34.0         
 [34] shiny_1.10.0           digest_0.6.37          colorspace_2.1-1      
 [37] ps_1.9.0               rprojroot_2.0.4        tensor_1.5            
 [40] irlba_2.3.5.1          labeling_0.4.3         progressr_0.15.1      
 [43] spatstat.sparse_3.1-0  httr_1.4.7             polyclip_1.10-7       
 [46] abind_1.4-8            compiler_4.4.3         withr_3.0.2           
 [49] MASS_7.3-65            tools_4.4.3            vipor_0.4.7           
 [52] lmtest_0.9-40          beeswarm_0.4.0         httpuv_1.6.15         
 [55] future.apply_1.11.3    goftest_1.2-3          glue_1.8.0            
 [58] callr_3.7.6            nlme_3.1-167           promises_1.3.2        
 [61] grid_4.4.3             Rtsne_0.17             getPass_0.2-4         
 [64] cluster_2.1.8          reshape2_1.4.4         generics_0.1.3        
 [67] gtable_0.3.6           spatstat.data_3.1-6    tidyr_1.3.1           
 [70] sp_2.2-0               xml2_1.3.7             spatstat.geom_3.3-5   
 [73] RcppAnnoy_0.0.22       ggrepel_0.9.6          RANN_2.6.2            
 [76] pillar_1.10.1          stringr_1.5.1          later_1.4.1           
 [79] splines_4.4.3          lattice_0.22-6         survival_3.8-3        
 [82] FNN_1.1.4.1            deldir_2.0-4           tidyselect_1.2.1      
 [85] miniUI_0.1.1.1         pbapply_1.7-2          knitr_1.50            
 [88] git2r_0.35.0           gridExtra_2.3          svglite_2.1.3         
 [91] scattermore_1.2        xfun_0.51              matrixStats_1.5.0     
 [94] pheatmap_1.0.12        stringi_1.8.4          lazyeval_0.2.2        
 [97] yaml_2.3.10            evaluate_1.0.3         codetools_0.2-20      
[100] tibble_3.2.1           cli_3.6.4              uwot_0.2.3            
[103] xtable_1.8-4           reticulate_1.41.0.1    systemfonts_1.2.1     
[106] munsell_0.5.1          processx_3.8.6         jquerylib_0.1.4       
[109] Rcpp_1.0.14            globals_0.16.3         spatstat.random_3.3-2 
[112] png_0.1-8              ggrastr_1.0.2          spatstat.univar_3.1-2 
[115] parallel_4.4.3         listenv_0.9.1          viridisLite_0.4.2     
[118] scales_1.3.0           ggridges_0.5.6         leiden_0.4.3.1        
[121] purrr_1.0.4            rlang_1.1.6            cowplot_1.1.3