Merge subtypes into final object

合并各谱系亚型,得到 annot_main(大类)和 annot_sub(亚型),并重画官方 UMAP。

从 05 各 lineage 的 annot_subtype 拼出最终对象。Level-1 / Level-2 分别是 annot_main / annot_sub。seurat_object_first_annot.qs2 只提供 counts 和 QC 元数据,粗标 annot 留作对照,不用于后续分析。细胞集合变了,HVG / PCA / Harmony / UMAP 必须重做。不重新聚类。

pacman::p_load(Seurat, tidyverse, qs2, harmony, tictoc, cowplot, SCpubr, ggh4x)
knitr::opts_knit$set(root.dir = "/home/han/Desktop/guoqiang/stroke_bm/stroke_bm_Rproj")
setwd("/home/han/Desktop/guoqiang/stroke_bm/stroke_bm_Rproj")
source("codes/functions.R")
source("codes/plot_colors.R")
res_path <- "results/final"
obj_path <- "data/rdata"
if (dir.exists(res_path)) {
  old_res <- list.files(res_path, full.names = TRUE)
  if (length(old_res) > 0) unlink(old_res, recursive = TRUE)
} else {
  dir.create(res_path, recursive = TRUE)
}
alpha <- .35
pt_size <- .1
raster <- FALSE
raster_dpi <- c(3000, 3000)
n_hvg <- 3000

1. Collect subtype labels

从 05 各篇把亚型名收齐,检查有没有细胞被漏标或标重。

subtype_files <- c(
  hspc = "seurat_object_hspc.qs2",
  b_plasma = "seurat_object_b_plasma.qs2",
  tnk = "seurat_object_tnk.qs2",
  myeloid = "seurat_object_myeloid.qs2",
  neutrophil = "seurat_object_neutrophil.qs2",
  erythrocyte = "seurat_object_erythrocyte.qs2"
)

subtype_to_main <- c(
  HSC = "HSPC",
  MEP = "HSPC",
  GMP = "HSPC",
  MDP = "HSPC",
  "Pro-B" = "B",
  "Large Pre-B" = "B",
  "Small Pre-B" = "B",
  "Trans B" = "B",
  "Naive B" = "B",
  "Memory B" = "B",
  Plasma = "Plasma",
  "CD4 Tnaive" = "T",
  "CD4 Tcm" = "T",
  "CD4 Treg" = "T",
  "CD8 Tnaive" = "T",
  "CD8 Tem" = "T",
  "CD8 CTL" = "T",
  "CD8 NKT" = "T",
  MAIT = "T",
  gdT = "T",
  "CD56bright NK" = "NK",
  "CD56dim NK" = "NK",
  "CD14 Mono" = "Mono/DC",
  "CD16 Mono" = "Mono/DC",
  cDC2 = "Mono/DC",
  pDC = "pDC",
  Promyelocyte = "Pre-Neut",
  "Pre-Neut" = "Pre-Neut",
  "MMP8 Pre-Neut" = "Pre-Neut",
  "MMP9 Pre-Neut" = "Pre-Neut",
  "S100A12 Neut" = "Neut",
  "SLC8A1 Neut" = "Neut",
  "ISG15 Neut" = "Neut",
  "Early Ery" = "Ery",
  "Late Ery" = "Ery"
)

annot_main_levels <- c(
  "HSPC", "B", "Plasma", "T", "NK",
  "Mono/DC", "pDC", "Pre-Neut", "Neut", "Ery"
)
annot_sub_levels <- names(subtype_to_main)

label_tbl <- purrr::imap_dfr(subtype_files, function(file, lineage) {
  obj <- qs_read(file.path("data/rdata/subtype", file), nthreads = 10)
  stopifnot("annot_subtype" %in% colnames(obj[[]]))
  out <- tibble(
    barcode = colnames(obj),
    lineage = lineage,
    annot_sub = as.character(obj$annot_subtype)
  )
  rm(obj)
  gc()
  out
})

n_na <- sum(is.na(label_tbl$annot_sub) | label_tbl$annot_sub == "")
n_dup <- sum(duplicated(label_tbl$barcode))
unknown <- setdiff(unique(label_tbl$annot_sub), names(subtype_to_main))
if (n_na > 0) stop(sprintf("%s cells have empty annot_sub.", n_na))
if (n_dup > 0) {
  stop(sprintf("%s barcodes appear in more than one subtype object.", n_dup))
}
if (length(unknown) > 0) {
  stop(sprintf("Unmapped annot_sub: %s", paste(unknown, collapse = ", ")))
}

label_tbl$annot_main <- unname(subtype_to_main[label_tbl$annot_sub])
print(table(label_tbl$lineage))

   b_plasma erythrocyte        hspc     myeloid  neutrophil         tnk 
       4859        5213        3366        4376       31175       12201 
print(table(label_tbl$annot_main, useNA = "ifany"))

       B      Ery     HSPC  Mono/DC     Neut       NK      pDC   Plasma 
    4305     5213     3366     3910    17668     1747      466      554 
Pre-Neut        T 
   13507    10454 
print(table(label_tbl$annot_sub, useNA = "ifany"))

    CD14 Mono     CD16 Mono       CD4 Tcm    CD4 Tnaive      CD4 Treg 
         3428           205          1503          1450           316 
CD56bright NK    CD56dim NK       CD8 CTL       CD8 NKT       CD8 Tem 
          255          1492          1804          1458          2284 
   CD8 Tnaive          cDC2     Early Ery           gdT           GMP 
          778           277          3272           465          1524 
          HSC    ISG15 Neut   Large Pre-B      Late Ery          MAIT 
          404           867           539          1941           396 
          MDP      Memory B           MEP MMP8 Pre-Neut MMP9 Pre-Neut 
         1259           809           179          5990          5589 
      Naive B           pDC        Plasma         Pro-B  Promyelocyte 
         1014           466           554           371          1928 
 S100A12 Neut   SLC8A1 Neut   Small Pre-B       Trans B 
         8056          8745          1059           513 

2. Subset first-annot object

只留下通过亚型注释的细胞。

只保留完成亚型命名的细胞。04 的粗标留在 annot 作对照。

test_genes <- qs_read("data/rdata/test_genes.qs2", nthreads = 10)
seurat_object <- qs_read(
  "data/rdata/seurat_object_first_annot.qs2",
  nthreads = 10
)
n_parent <- ncol(seurat_object)
missing_parent <- setdiff(label_tbl$barcode, colnames(seurat_object))
if (length(missing_parent) > 0) {
  stop(sprintf(
    "%s subtype barcodes are not in first_annot.",
    length(missing_parent)
  ))
}
dropped_cleanup <- setdiff(colnames(seurat_object), label_tbl$barcode)
message(sprintf(
  "first_annot %s cells; keep %s from subtypes; drop %s not in any 05 object.",
  n_parent, nrow(label_tbl), length(dropped_cleanup)
))

seurat_object <- subset(seurat_object, cells = label_tbl$barcode)
label_tbl <- label_tbl[match(colnames(seurat_object), label_tbl$barcode), ]
stopifnot(identical(colnames(seurat_object), label_tbl$barcode))

seurat_object$lineage <- label_tbl$lineage
seurat_object$annot_sub <- factor(
  label_tbl$annot_sub,
  levels = intersect(annot_sub_levels, unique(label_tbl$annot_sub))
)
seurat_object$annot_main <- factor(
  label_tbl$annot_main,
  levels = intersect(annot_main_levels, unique(label_tbl$annot_main))
)

print(table(first = seurat_object$annot, main = seurat_object$annot_main))
             main
first          HSPC     B Plasma     T    NK Mono/DC   pDC Pre-Neut  Neut   Ery
  B               0  4289      2     0     0       0     0        0     0     0
  Ery             0     0      0     0     0       0     0        0     0  5213
  HSPC         3366     0      0     0     0       0     0        0     0     0
  Mono/Mac/DC     0     0      0     0     0    3903     2        0     0     0
  Neut            0     0      0     0     0       0     0     2367 17600     0
  pDC             0     0      0     0     0       7   464        0     0     0
  Plasma          0    16    552     0     0       0     0        0     0     0
  Pre-Neut        0     0      0     0     0       0     0    11140    68     0
  T/NK            0     0      0 10454  1747       0     0        0     0     0
print(table(seurat_object$annot_main, seurat_object$sample))
          
           ACI-1 ACI-2 ACI-3 ACI-4 ACI-5 CTRL-1 CTRL-2 CTRL-3 CTRL-4 CTRL-5
  HSPC       398   619   287   457   215    167    344    168    323    168
  B          139    59    93   172   201    133    530    407    700    417
  Plasma      11    22    62    10     4     52    177     90     72     25
  T          685   722   952   410   791    810   1775    992   1260    340
  NK         131    56   137    89   380     67    205    164     70     92
  Mono/DC    193   257   653   406   341    163    486    296    532    254
  pDC         28    32    15    17    11     67     60     47     56     44
  Pre-Neut  3244  3390  1725  2017  1915     24     56    139    292    121
  Neut      2352  1855  5051  3605  3920      2      5     83    192    143
  Ery         23   234    23    34     9    157   1213    702   2118    252
          
           CTRL-6
  HSPC        220
  B          1454
  Plasma       29
  T          1717
  NK          356
  Mono/DC     329
  pDC          89
  Pre-Neut    584
  Neut        460
  Ery         448
if ("umap_harmony" %in% Reductions(seurat_object)) {
  seurat_object[["umap_first"]] <- seurat_object[["umap_harmony"]]
}
if (length(Layers(seurat_object, search = "counts")) > 1) {
  seurat_object <- JoinLayers(seurat_object)
}
seurat_object[["RNA"]]$data <- NULL
seurat_object[["RNA"]]$scale.data <- NULL
DefaultLayer(seurat_object@assays$RNA)
[1] "counts"
Layers(seurat_object)
[1] "counts"

3. Normalize, HVG, Scale, PCA

合并后细胞集合变了,重新找高变基因并做 PCA。

seurat_object <- NormalizeData(seurat_object, verbose = TRUE)
seurat_object <- FindVariableFeatures(
  seurat_object, nfeatures = n_hvg, verbose = TRUE
)
head(VariableFeatures(seurat_object), 20)
 [1] "IGHG1"    "IGHA1"    "HBG2"     "IGKC"     "IGHG2"    "JCHAIN"  
 [7] "IGHG3"    "IGKV3-20" "IGLC2"    "IGHV3-7"  "PTGDS"    "IGHG4"   
[13] "IGKV1-39" "IGLV1-44" "CCL4L2"   "HBG1"     "IGKV4-1"  "IGHA2"   
[19] "IGHV3-23" "IGHV3-30"
pca_features <- setdiff(
  VariableFeatures(seurat_object),
  test_genes$gene[test_genes$unwant | test_genes$ig | test_genes$tr]
)
sum(VariableFeatures(seurat_object) %in%
  test_genes$gene[test_genes$unwant | test_genes$ig | test_genes$tr])
[1] 289
seurat_object <- ScaleData(
  seurat_object, features = pca_features, assay = "RNA"
)
seurat_object <- RunPCA(
  seurat_object, npcs = 50, features = pca_features,
  verbose = TRUE, reduction.name = "pca"
)
ElbowPlot(seurat_object, reduction = "pca", ndims = 50)

4. Harmony

全数据再整合一次,给官方 UMAP 用。

tic("Harmony integration")
seurat_object <- RunHarmony(
  seurat_object, c("sample"), ncores = 15, plot_convergence = TRUE
)

toc()
Harmony integration: 27.196 sec elapsed
plot_harmony_umap <- function(obj, dims, file) {
  umap_temp <- RunUMAP(
    obj, dims = 1:dims, reduction = "harmony",
    reduction.name = "umap_harmony"
  )
  p <- plot_grid(
    ncol = 2,
    DimPlot(
      umap_temp, reduction = "umap_harmony", group.by = "annot_main",
      cols = annot_main_col, label = TRUE,
      pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi
    ) + theme(aspect.ratio = 1),
    DimPlot(
      umap_temp, reduction = "umap_harmony", group.by = "annot_sub",
      cols = annot_sub_col, label = TRUE, label.size = 3,
      pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi
    ) + theme(aspect.ratio = 1),
    DimPlot(
      umap_temp, reduction = "umap_harmony", group.by = "sample",
      pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi
    ) + theme(aspect.ratio = 1),
    DimPlot(
      umap_temp, reduction = "umap_harmony", group.by = "group",
      cols = group_col,
      pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi
    ) + theme(aspect.ratio = 1)
  )
  ggsave(file, plot = p, width = 16, height = 14, dpi = 600)
  p
}
plot_harmony_umap(seurat_object, 20, file.path(res_path, "01_dim_umap20.pdf"))

plot_harmony_umap(seurat_object, 30, file.path(res_path, "01_dim_umap30.pdf"))

plot_harmony_umap(seurat_object, 40, file.path(res_path, "01_dim_umap40.pdf"))

# 看过 20/30/40 后改这里。01 全局对象用 30;05 亚型官方 UMAP 用 20。
n_dims <- 20
seurat_object <- RunUMAP(
  seurat_object, dims = 1:n_dims, reduction = "harmony",
  reduction.name = "umap"
)

5. Annotation UMAPs

大类和亚型上色。团分得开、名字和位置对得上,才说明注释站得住。

短坐标轴同 summary.qmd。颜色来自 codes/plot_colors.R。

plot_umap_short(
  seurat_object, group.by = "annot_main", cols = annot_main_col,
  pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
  label = TRUE, label.size = 5
)

ggsave(
  file.path(res_path, "02_annot_main.pdf"),
  width = 8, height = 8, dpi = 600
)
ggsave(
  file.path(res_path, "02_annot_main.png"),
  width = 8, height = 8, dpi = 600
)
plot_umap_short(
  seurat_object, group.by = "annot_sub", cols = annot_sub_col,
  pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
  label = TRUE, label.size = 3, legend = TRUE
)

ggsave(
  file.path(res_path, "02_annot_sub.pdf"),
  width = 11, height = 8, dpi = 600
)
ggsave(
  file.path(res_path, "02_annot_sub.png"),
  width = 11, height = 8, dpi = 600
)
p_main_group <- list(
  plot_umap_short(
    subset(seurat_object, group == "CTRL"),
    group.by = "annot_main", cols = annot_main_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, title = "CTRL"
  ),
  plot_umap_short(
    subset(seurat_object, group == "ACI"),
    group.by = "annot_main", cols = annot_main_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, title = "ACI"
  )
)
plot_grid(plotlist = p_main_group, ncol = 2)

ggsave(
  file.path(res_path, "02_annot_main_group.pdf"),
  width = 15, height = 8, units = "in", dpi = 600
)
p_sub_group <- list(
  plot_umap_short(
    subset(seurat_object, group == "CTRL"),
    group.by = "annot_sub", cols = annot_sub_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, title = "CTRL"
  ),
  plot_umap_short(
    subset(seurat_object, group == "ACI"),
    group.by = "annot_sub", cols = annot_sub_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, title = "ACI"
  )
)
plot_grid(plotlist = p_sub_group, ncol = 2)

ggsave(
  file.path(res_path, "02_annot_sub_group.pdf"),
  width = 15, height = 8, units = "in", dpi = 600
)
plot_grid(
  ncol = 2,
  plot_umap_short(
    seurat_object, group.by = "annot_main", cols = annot_main_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = TRUE, label.size = 4
  ),
  plot_umap_short(
    seurat_object, group.by = "annot_sub", cols = annot_sub_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = TRUE, label.size = 3
  ),
  plot_umap_short(
    seurat_object, group.by = "sample", cols = NULL,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, legend = TRUE
  ),
  plot_umap_short(
    seurat_object, group.by = "group", cols = group_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, legend = TRUE
  )
)

ggsave(
  file.path(res_path, "02_annot_overview.pdf"),
  width = 16, height = 14, dpi = 600
)
if ("umap_first" %in% Reductions(seurat_object)) {
  plot_grid(
    ncol = 2,
    plot_umap_short(
      seurat_object, reduction = "umap_first", group.by = "annot",
      cols = NULL,
      pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
      label = TRUE, title = "first annot (old UMAP)"
    ),
    plot_umap_short(
      seurat_object, reduction = "umap_first", group.by = "annot_main",
      cols = annot_main_col,
      pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
      label = TRUE, title = "annot_main on old UMAP"
    )
  )
  ggsave(
    file.path(res_path, "02_annot_first_umap.pdf"),
    width = 16, height = 8, dpi = 600
  )
}

6. Marker DotPlots

用 marker 再核对一遍命名。

marker_groups_main <- list(
  HSPC = c("CD34", "SPINK2", "AVP"),
  B = c("MS4A1", "CD79A"),
  Plasma = c("JCHAIN", "MZB1"),
  T = c("CD3D", "CD4", "CD8A", "IL7R"),
  NK = c("NKG7", "GNLY", "KLRF1"),
  `Mono/DC` = c("CD14", "FCN1", "LYZ", "CD1C"),
  pDC = c("CLEC4C", "LILRA4"),
  `Pre-Neut` = c("AZU1", "MPO", "LTF", "CAMP"),
  Neut = c("FCGR3B", "CSF3R", "NAMPT"),
  Ery = c("CA1", "HBD", "GYPA")
)

Idents(seurat_object) <- "annot_main"
do_DotPlot(
  sample = seurat_object,
  flip = FALSE,
  legend.title = "Average Expression",
  legend.title.face = "plain",
  features = marker_groups_main,
  legend.length = 10
)

ggsave(
  file.path(res_path, "03_markers_main.pdf"),
  width = 12, height = 5, dpi = 600
)
Idents(seurat_object) <- "annot_sub"
do_DotPlot(
  sample = seurat_object,
  flip = FALSE,
  legend.title = "Average Expression",
  legend.title.face = "plain",
  features = marker_groups_main,
  legend.length = 10
)

ggsave(
  file.path(res_path, "03_markers_sub.pdf"),
  width = 12, height = 10, dpi = 600
)

7. Save

写出后面 07–11 都读的最终对象。

写出 seurat_object_final.qs2:annot_main 为大类,annot_sub 为亚型。后面组成和状态分析都读这份。

Idents(seurat_object) <- "annot_main"
qs_save(
  seurat_object,
  file = file.path(obj_path, "seurat_object_final.qs2"),
  nthreads = 10
)
dim(seurat_object)
[1] 38606 61190
table(seurat_object$annot_main, useNA = "ifany")

    HSPC        B   Plasma        T       NK  Mono/DC      pDC Pre-Neut 
    3366     4305      554    10454     1747     3910      466    13507 
    Neut      Ery 
   17668     5213 
sessionInfo()
R version 4.4.3 (2025-02-28)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.3 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [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: Asia/Shanghai
tzcode source: system (glibc)

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

other attached packages:
 [1] ggh4x_0.2.8        SCpubr_2.0.2.9000  cowplot_1.1.3      tictoc_1.2.1      
 [5] harmony_1.2.3      Rcpp_1.0.13-1      qs2_0.1.7          lubridate_1.9.3   
 [9] forcats_1.0.0      stringr_1.5.1      dplyr_1.1.4        purrr_1.0.2       
[13] readr_2.1.5        tidyr_1.3.1        tibble_3.2.1       ggplot2_3.5.1     
[17] tidyverse_2.0.0    Seurat_5.2.1       SeuratObject_5.0.2 sp_2.1-4          

loaded via a namespace (and not attached):
  [1] RColorBrewer_1.1-3     jsonlite_1.8.9         magrittr_2.0.3        
  [4] spatstat.utils_3.1-1   farver_2.1.2           rmarkdown_2.29        
  [7] ragg_1.3.3             vctrs_0.6.5            ROCR_1.0-11           
 [10] spatstat.explore_3.3-3 htmltools_0.5.8.1      sctransform_0.4.1     
 [13] parallelly_1.39.0      KernSmooth_2.23-26     htmlwidgets_1.6.4     
 [16] ica_1.0-3              plyr_1.8.9             plotly_4.10.4         
 [19] zoo_1.8-12             igraph_2.1.2           mime_0.12             
 [22] lifecycle_1.0.4        pkgconfig_2.0.3        Matrix_1.7-3          
 [25] R6_2.5.1               fastmap_1.2.0          fitdistrplus_1.2-1    
 [28] future_1.34.0          shiny_1.9.1            digest_0.6.37         
 [31] colorspace_2.1-1       patchwork_1.3.0        tensor_1.5            
 [34] RSpectra_0.16-2        irlba_2.3.5.1          textshaping_0.4.0     
 [37] labeling_0.4.3         progressr_0.15.1       fansi_1.0.6           
 [40] spatstat.sparse_3.1-0  timechange_0.3.0       httr_1.4.7            
 [43] polyclip_1.10-7        abind_1.4-8            compiler_4.4.3        
 [46] withr_3.0.2            viridis_0.6.5          fastDummies_1.7.4     
 [49] MASS_7.3-65            tools_4.4.3            lmtest_0.9-40         
 [52] httpuv_1.6.15          future.apply_1.11.3    goftest_1.2-3         
 [55] glue_1.8.0             nlme_3.1-166           promises_1.3.2        
 [58] grid_4.4.3             Rtsne_0.17             cluster_2.1.8.1       
 [61] reshape2_1.4.4         generics_0.1.3         gtable_0.3.6          
 [64] spatstat.data_3.1-4    tzdb_0.4.0             data.table_1.16.2     
 [67] hms_1.1.3              stringfish_0.18.0      utf8_1.2.4            
 [70] spatstat.geom_3.3-4    RcppAnnoy_0.0.22       ggrepel_0.9.6         
 [73] RANN_2.6.2             pillar_1.9.0           spam_2.11-0           
 [76] RcppHNSW_0.6.0         later_1.4.1            splines_4.4.3         
 [79] lattice_0.22-6         survival_3.8-3         deldir_2.0-4          
 [82] tidyselect_1.2.1       miniUI_0.1.1.1         pbapply_1.7-2         
 [85] knitr_1.49             gridExtra_2.3          scattermore_1.2       
 [88] RhpcBLASctl_0.23-42    xfun_0.49              matrixStats_1.4.1     
 [91] stringi_1.8.4          lazyeval_0.2.2         yaml_2.3.10           
 [94] pacman_0.5.1           evaluate_1.0.1         codetools_0.2-20      
 [97] cli_3.6.3              uwot_0.2.2             RcppParallel_5.1.9    
[100] systemfonts_1.1.0      xtable_1.8-4           reticulate_1.42.0     
[103] munsell_0.5.1          globals_0.16.3         spatstat.random_3.3-2 
[106] png_0.1-8              spatstat.univar_3.1-1  parallel_4.4.3        
[109] assertthat_0.2.1       dotCall64_1.2          listenv_0.9.1         
[112] viridisLite_0.4.2      scales_1.3.0           ggridges_0.5.6        
[115] rlang_1.1.4