HSPC trajectory (HSC → MEP / GMP / MDP)

HSC 向红系(MEP)、粒系(GMP)、单核(MDP)三条命运。拟时间在 Harmony 空间计算;UMAP 上的线连接各亚型中心,箭头贴在线的末端。

pacman::p_load(
  Seurat, tidyverse, qs2, harmony, tictoc, cowplot, ggh4x,
  CytoTRACE2, slingshot
)
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/plot_colors.R")
source("codes/activity_helpers.R")
summarise <- dplyr::summarise
mutate <- dplyr::mutate
filter <- dplyr::filter
rename <- dplyr::rename
arrange <- dplyr::arrange
group_by <- dplyr::group_by
ungroup <- dplyr::ungroup
res_path <- "results/hspc_trajectory"
obj_path <- "data/rdata/hspc_trajectory"
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)
}
if (!dir.exists(obj_path)) dir.create(obj_path, recursive = TRUE)
alpha <- 0.45
pt_size <- 0.4
raster <- FALSE
raster_dpi <- c(3000, 3000)
n_hvg <- 3000
n_dims <- 20
traj_levels <- c("HSC", "MEP", "GMP", "MDP")
traj_col <- annot_sub_col[traj_levels]

1. Subset HSPC

只保留 HSC、MEP、GMP、MDP。GMP 在 08 还会作为粒系轴的起点;这里问的是 HSC 先分到哪一支。

test_genes <- qs_read("data/rdata/test_genes.qs2", nthreads = 10)
seurat_object <- qs_read("data/rdata/seurat_object_final.qs2", nthreads = 10)
stopifnot(all(c("sample", "group", "annot_sub") %in% colnames(seurat_object[[]])))
seurat_object$group <- factor(seurat_object$group, levels = c("CTRL", "ACI"))
keep <- as.character(seurat_object$annot_sub) %in% traj_levels
print(table(seurat_object$annot_sub[keep], seurat_object$group[keep]))
               
                CTRL  ACI
  HSC            298  106
  MEP            155   24
  GMP            204 1320
  MDP            733  526
  Pro-B            0    0
  Large Pre-B      0    0
  Small Pre-B      0    0
  Trans B          0    0
  Naive B          0    0
  Memory B         0    0
  Plasma           0    0
  CD4 Tnaive       0    0
  CD4 Tcm          0    0
  CD4 Treg         0    0
  CD8 Tnaive       0    0
  CD8 Tem          0    0
  CD8 CTL          0    0
  CD8 NKT          0    0
  MAIT             0    0
  gdT              0    0
  CD56bright NK    0    0
  CD56dim NK       0    0
  CD14 Mono        0    0
  CD16 Mono        0    0
  cDC2             0    0
  pDC              0    0
  Promyelocyte     0    0
  MMP8 Pre-Neut    0    0
  MMP9 Pre-Neut    0    0
  S100A12 Neut     0    0
  SLC8A1 Neut      0    0
  ISG15 Neut       0    0
  Early Ery        0    0
  Late Ery         0    0
seurat_object <- seurat_object[, keep]
seurat_object$annot_sub <- factor(
  as.character(seurat_object$annot_sub),
  levels = traj_levels
)
dim(seurat_object)
[1] 38606  3366
if (length(Layers(seurat_object, search = "counts")) > 1) {
  seurat_object <- JoinLayers(seurat_object)
}
seurat_object[["RNA"]]$data <- NULL
seurat_object[["RNA"]]$scale.data <- NULL

2. Normalize, HVG, PCA, Harmony, UMAP

子集上重新高变基因、PCA、Harmony 和 UMAP。亚型名字沿用 06,不再聚类。

seurat_object <- NormalizeData(seurat_object, verbose = TRUE)
seurat_object <- FindVariableFeatures(
  seurat_object, nfeatures = n_hvg, verbose = TRUE
)
pca_features <- setdiff(
  VariableFeatures(seurat_object),
  test_genes$gene[test_genes$unwant | test_genes$ig | test_genes$tr]
)
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)

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

toc()
Harmony: 1.362 sec elapsed
seurat_object <- RunUMAP(
  seurat_object, dims = 1:n_dims, reduction = "harmony",
  reduction.name = "umap"
)

3. Annotation UMAP

核对 HSC 与三个下游亚型是否分开。分叉要画得住,三类终点不能完全叠在一起。

plot_umap_short(
  seurat_object, group.by = "annot_sub", cols = traj_col,
  pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
  label = TRUE, label.size = 4, legend = TRUE
)

ggsave(file.path(res_path, "01_umap_annot.pdf"), width = 8, height = 7, dpi = 600)
plot_grid(
  ncol = 2,
  plot_umap_short(
    subset(seurat_object, group == "CTRL"),
    group.by = "annot_sub", cols = traj_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 = traj_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi,
    label = FALSE, title = "ACI"
  )
)

ggsave(file.path(res_path, "01_umap_by_group.pdf"), width = 14, height = 7, dpi = 600)
plot_grid(
  ncol = 2,
  DimPlot(
    seurat_object, reduction = "umap", group.by = "sample",
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi
  ) + theme(aspect.ratio = 1),
  DimPlot(
    seurat_object, reduction = "umap", group.by = "group", cols = group_col,
    pt.size = pt_size, alpha = alpha, raster = raster, raster.dpi = raster_dpi
  ) + theme(aspect.ratio = 1)
)

ggsave(file.path(res_path, "01_umap_sample_group.pdf"), width = 14, height = 7, dpi = 600)

4. CytoTRACE2

高分更接近未成熟。HSC 应偏高,MEP / GMP / MDP 应偏低。组间密度差也可能只是组成不同。

cyto_qs2 <- file.path(obj_path, "cytotrace_hspc.qs2")
cyto_cache <- NULL
if (file.exists(cyto_qs2)) {
  cyto_cache <- qs_read(cyto_qs2, nthreads = 10)
}
same_n <- !is.null(cyto_cache) && nrow(cyto_cache) == ncol(seurat_object)
same_cells <- same_n &&
  identical(sort(rownames(cyto_cache)), sort(colnames(seurat_object)))
if (same_cells) {
  message("Reuse CytoTRACE2 qs2. Skip cytotrace2().")
  cyto_cache <- cyto_cache[colnames(seurat_object), , drop = FALSE]
  seurat_object <- AddMetaData(seurat_object, cyto_cache)
} else {
  counts_mat <- LayerData(seurat_object, assay = "RNA", layer = "counts")
  seurat_object[["RNA"]]$counts <- as(counts_mat, "dgCMatrix")
  seurat_object <- cytotrace2(
    seurat_object, species = "human", is_seurat = TRUE, ncores = 10
  )
  cyto_cols <- grep("^CytoTRACE2", colnames(seurat_object[[]]), value = TRUE)
  cyto_out <- FetchData(seurat_object, vars = cyto_cols)
  qs_save(cyto_out, cyto_qs2, nthreads = 10)
}
table(seurat_object$CytoTRACE2_Potency)

Differentiated      Unipotent    Oligopotent    Multipotent    Pluripotent 
           437            464           2212            253              0 
    Totipotent 
             0 
summary(seurat_object$CytoTRACE2_Score)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.01267 0.30852 0.40161 0.36279 0.44279 0.64856 
FeaturePlot(
  seurat_object, reduction = "umap", features = "CytoTRACE2_Score",
  pt.size = pt_size, raster = raster, raster.dpi = raster_dpi
) + theme(aspect.ratio = 1)

ggsave(file.path(res_path, "02_cytotrace_umap.pdf"), width = 8, height = 7, dpi = 600)
cyto_meta <- FetchData(
  seurat_object, vars = c("sample", "group", "annot_sub", "CytoTRACE2_Score")
)
p_cyto_box <- ggplot(cyto_meta, aes(x = annot_sub, y = CytoTRACE2_Score, fill = annot_sub)) +
  geom_boxplot(outlier.size = 0.2, width = 0.7) +
  scale_fill_manual(values = traj_col) +
  theme_classic() +
  theme(legend.position = "none", axis.text.x = element_text(angle = 45, hjust = 1)) +
  xlab(NULL) +
  ylab("CytoTRACE2")
ggsave(file.path(res_path, "02_cytotrace_by_subtype.pdf"), p_cyto_box, width = 6, height = 5, dpi = 600)
p_cyto_box

sample_cyto <- cyto_meta |>
  dplyr::group_by(sample, group) |>
  dplyr::summarise(
    n_cell = dplyr::n(),
    cyto_median = median(CytoTRACE2_Score, na.rm = TRUE),
    .groups = "drop"
  ) |>
  dplyr::filter(n_cell >= min_cells_sample)
cyto_test <- wilcox_ctrl_aci(sample_cyto$cyto_median, sample_cyto$group) |>
  dplyr::mutate(feature = "cyto_median") |>
  add_stars()
write_csv(sample_cyto, file.path(res_path, "02_sample_cytotrace.csv"))
write_csv(cyto_test, file.path(res_path, "02_sample_cytotrace_wilcox.csv"))
print(cyto_test)
# A tibble: 1 × 8
  n_ctrl n_aci median_ctrl median_aci delta_aci_minus_ctrl     p feature   star 
   <int> <int>       <dbl>      <dbl>                <dbl> <dbl> <chr>     <chr>
1      6     5       0.408      0.400             -0.00830 0.315 cyto_med… ns   

5. Slingshot

起点固定为 HSC,三条终点为 MEP、GMP、MDP。拟时间在 Harmony 上算;图上的线走亚型中心,箭头是曲线最后一段的切向。组间看每例细胞落在哪一支的比例。

sling <- list()
sling$dimred_fit <- Embeddings(seurat_object, "harmony")[, 1:n_dims]
sling$dimred <- Embeddings(seurat_object, "umap")[, 1:2]
sling$clustering <- seurat_object$annot_sub
sling$slingshot_fit <- slingshot(
  sling$dimred_fit,
  sling$clustering,
  start.clus = "HSC",
  end.clus = c("MEP", "GMP", "MDP"),
  stretch = 0
)
print(slingLineages(sling$slingshot_fit))
$Lineage1
[1] "HSC" "MDP"

$Lineage2
[1] "HSC" "MEP"

$Lineage3
[1] "HSC" "GMP"
lin_ok <- vapply(
  slingLineages(sling$slingshot_fit),
  function(x) identical(x[[1]], "HSC"),
  logical(1)
)
if (!all(lin_ok)) {
  lin_txt <- vapply(
    slingLineages(sling$slingshot_fit),
    function(x) paste(x, collapse = ">"),
    character(1)
  )
  stop("Slingshot lineages do not all start at HSC: ", paste(lin_txt, collapse = "; "))
}
sling$pseudotime <- as.data.frame(slingPseudotime(sling$slingshot_fit))
sling$slingshot <- sling$slingshot_fit
stopifnot(identical(rownames(sling$dimred_fit), colnames(seurat_object)))
pt_cols <- grep("^Lineage", colnames(sling$pseudotime), value = TRUE)
for (ln in pt_cols) {
  seurat_object[[paste0("sling_", ln)]] <- sling$pseudotime[[ln]]
}

path_from_centroids <- function(xy, n = 120) {
  xy <- as.matrix(xy)
  stopifnot(nrow(xy) >= 2, ncol(xy) == 2)
  if (nrow(xy) == 2) {
    t <- seq(0, 1, length.out = n)
    return(data.frame(
      x = (1 - t) * xy[1, 1] + t * xy[2, 1],
      y = (1 - t) * xy[1, 2] + t * xy[2, 2]
    ))
  }
  grDevices::pdf(NULL)
  sp <- graphics::xspline(
    xy[, 1], xy[, 2],
    shape = -0.5,
    open = TRUE,
    draw = FALSE,
    repEnds = TRUE
  )
  grDevices::dev.off()
  dx <- diff(sp$x)
  dy <- diff(sp$y)
  s <- c(0, cumsum(sqrt(dx * dx + dy * dy)))
  s <- s / max(s)
  t <- seq(0, 1, length.out = n)
  data.frame(
    x = stats::approx(s, sp$x, xout = t)$y,
    y = stats::approx(s, sp$y, xout = t)$y
  )
}

umap_cent <- as.data.frame(sling$dimred)
colnames(umap_cent) <- c("x", "y")
umap_cent$clus <- as.character(sling$clustering)
cent_df <- umap_cent |>
  dplyr::group_by(clus) |>
  dplyr::summarise(x = mean(.data$x), y = mean(.data$y), .groups = "drop")

lin_map <- slingLineages(sling$slingshot_fit)
curve_sm <- purrr::imap_dfr(lin_map, function(clus, nm) {
  cc <- cent_df[match(clus, cent_df$clus), c("x", "y")]
  d <- path_from_centroids(cc)
  d$Lineage <- nm
  d$Order <- seq_len(nrow(d))
  d$end_lab <- tail(clus, 1)
  d
})
lab_df <- curve_sm |>
  dplyr::group_by(Lineage) |>
  dplyr::slice_max(order_by = .data$Order, n = 2, with_ties = FALSE) |>
  dplyr::arrange(.data$Order) |>
  dplyr::summarise(
    x0 = dplyr::first(.data$x),
    y0 = dplyr::first(.data$y),
    x = dplyr::last(.data$x),
    y = dplyr::last(.data$y),
    lab = dplyr::last(.data$end_lab),
    .groups = "drop"
  ) |>
  dplyr::mutate(
    dx = .data$x - .data$x0,
    dy = .data$y - .data$y0,
    x = .data$x + 8 * .data$dx,
    y = .data$y + 8 * .data$dy
  )
start_df <- cent_df |>
  dplyr::filter(.data$clus == "HSC") |>
  dplyr::select(x, y)

plot_df <- as.data.frame(sling$dimred) |>
  mutate(annot_sub = sling$clustering)
x_umap <- colnames(plot_df)[1]
y_umap <- colnames(plot_df)[2]

p_sling <- ggplot(plot_df, aes(.data[[x_umap]], .data[[y_umap]])) +
  geom_point(aes(color = annot_sub), shape = 19, size = 0.35, alpha = 0.45) +
  geom_path(
    data = curve_sm,
    aes(x = x, y = y, group = Lineage),
    linewidth = 1.15,
    lineend = "butt",
    linejoin = "round",
    arrow = arrow(type = "closed", length = unit(0.28, "cm"), angle = 20)
  ) +
  geom_point(
    data = start_df, aes(x = x, y = y),
    inherit.aes = FALSE,
    size = 4, shape = 21, fill = "white", color = "black", stroke = 1.1
  ) +
  geom_text(
    data = start_df, aes(x = x, y = y),
    inherit.aes = FALSE,
    label = "HSC", fontface = "bold", size = 3.5, vjust = -1.2
  ) +
  geom_text(
    data = lab_df, aes(x = x, y = y, label = lab),
    size = 4.5, fontface = "bold"
  ) +
  scale_color_manual(values = traj_col) +
  theme_classic() +
  theme_umap_short() +
  theme(legend.position = "right") +
  xlab("UMAP-1") +
  ylab("UMAP-2") +
  scale_x_continuous(breaks = NULL) +
  scale_y_continuous(breaks = NULL) +
  guides(x = umap_trunc_axis(), y = umap_trunc_axis())
ggsave(file.path(res_path, "03_slingshot_umap.pdf"), p_sling, width = 8, height = 7, dpi = 600)
p_sling

if (length(pt_cols) > 0) {
  for (ln in pt_cols) {
    FeaturePlot(
      seurat_object, reduction = "umap", features = paste0("sling_", ln),
      pt.size = pt_size, raster = raster, raster.dpi = raster_dpi
    ) + theme(aspect.ratio = 1)
    ggsave(
      file.path(res_path, paste0("03_slingshot_pt_", ln, ".pdf")),
      width = 8, height = 7, dpi = 600
    )
  }
}

pt_long <- FetchData(
  seurat_object,
  vars = c("annot_sub", paste0("sling_", pt_cols))
) |>
  tidyr::pivot_longer(
    dplyr::all_of(paste0("sling_", pt_cols)),
    names_to = "lineage", values_to = "pt"
  )
p_pt_box <- ggplot(pt_long, aes(x = annot_sub, y = pt, fill = annot_sub)) +
  geom_boxplot(outlier.size = 0.15, width = 0.7, na.rm = TRUE) +
  facet_wrap(~lineage) +
  scale_fill_manual(values = traj_col) +
  theme_classic() +
  theme(
    legend.position = "none",
    axis.text.x = element_text(angle = 45, hjust = 1)
  ) +
  xlab(NULL) +
  ylab("Pseudotime")
ggsave(file.path(res_path, "03_pt_by_subtype.pdf"), p_pt_box, width = 10, height = 4.2, dpi = 600)
p_pt_box

hspc_meta <- FetchData(
  seurat_object,
  vars = c("sample", "group", "annot_sub", paste0("sling_", pt_cols))
) |>
  rownames_to_column("barcode")
write_csv(hspc_meta, file.path(res_path, "03_hspc_pseudotime_cells.csv"))

# Fate: among cells assigned to a lineage, fraction ending toward GMP vs MEP vs MDP.
lin_map <- slingLineages(sling$slingshot)
end_clus <- vapply(lin_map, function(x) tail(x, 1), character(1))
print(end_clus)
Lineage1 Lineage2 Lineage3 
   "MDP"    "MEP"    "GMP" 
pt_mat <- as.matrix(sling$pseudotime)
idx <- max.col(replace(pt_mat, is.na(pt_mat), -Inf), ties.method = "first")
assigned <- colnames(pt_mat)[idx]
ok_pt <- is.finite(pt_mat[cbind(seq_len(nrow(pt_mat)), idx)])
assigned[!ok_pt] <- NA
hspc_meta$lineage_end <- unname(end_clus[assigned])
fate <- hspc_meta |>
  dplyr::filter(!is.na(.data$lineage_end)) |>
  dplyr::count(sample, group, lineage_end, name = "n") |>
  dplyr::group_by(sample, group) |>
  dplyr::mutate(frac = n / sum(n)) |>
  dplyr::ungroup()
write_csv(fate, file.path(res_path, "03_sample_fate_frac.csv"))
print(fate)
# A tibble: 33 × 5
   sample group lineage_end     n   frac
   <chr>  <fct> <chr>       <int>  <dbl>
 1 ACI-1  ACI   GMP           286 0.719 
 2 ACI-1  ACI   MDP            74 0.186 
 3 ACI-1  ACI   MEP            38 0.0955
 4 ACI-2  ACI   GMP           427 0.690 
 5 ACI-2  ACI   MDP           158 0.255 
 6 ACI-2  ACI   MEP            34 0.0549
 7 ACI-3  ACI   GMP           188 0.655 
 8 ACI-3  ACI   MDP            83 0.289 
 9 ACI-3  ACI   MEP            16 0.0557
10 ACI-4  ACI   GMP           311 0.681 
# ℹ 23 more rows
p_fate <- ggplot(fate, aes(x = group, y = frac, fill = group)) +
  geom_boxplot(width = 0.55, outlier.shape = NA) +
  geom_jitter(width = 0.12, size = 2) +
  facet_wrap(~lineage_end) +
  scale_fill_manual(values = group_col) +
  theme_classic() +
  theme(legend.position = "none") +
  xlab(NULL) +
  ylab("Fraction of HSPC on this lineage")
ggsave(file.path(res_path, "03_fate_frac.pdf"), p_fate, width = 9, height = 4.5, dpi = 600)
p_fate

sample_group <- dplyr::distinct(hspc_meta, sample, group)
fate_test <- fate |>
  dplyr::select(-group) |>
  tidyr::complete(sample, lineage_end, fill = list(frac = 0, n = 0)) |>
  dplyr::left_join(sample_group, by = "sample") |>
  dplyr::group_by(lineage_end) |>
  dplyr::group_modify(function(d, k) {
    wilcox_ctrl_aci(d$frac, d$group)
  }) |>
  dplyr::ungroup() |>
  add_stars()
write_csv(fate_test, file.path(res_path, "03_fate_frac_wilcox.csv"))
print(fate_test)
# A tibble: 3 × 8
  lineage_end n_ctrl n_aci median_ctrl median_aci delta_aci_minus_ctrl       p
  <chr>        <int> <int>       <dbl>      <dbl>                <dbl>   <dbl>
1 GMP              6     5       0.287     0.690                 0.403 0.00811
2 MDP              6     5       0.437     0.255                -0.182 0.00811
3 MEP              6     5       0.241     0.0549               -0.186 0.00811
# ℹ 1 more variable: star <chr>
qs_save(sling, file.path(obj_path, "slingshot_hspc.qs2"), nthreads = 10)
qs_save(
  seurat_object,
  file.path(obj_path, "seurat_object_hspc_traj.qs2"),
  nthreads = 10
)
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] stats4    parallel  stats     graphics  grDevices utils     datasets 
[8] methods   base     

other attached packages:
 [1] slingshot_2.14.0            TrajectoryUtils_1.14.0     
 [3] SingleCellExperiment_1.28.1 SummarizedExperiment_1.36.0
 [5] Biobase_2.66.0              GenomicRanges_1.58.0       
 [7] GenomeInfoDb_1.42.0         IRanges_2.40.0             
 [9] S4Vectors_0.44.0            BiocGenerics_0.52.0        
[11] MatrixGenerics_1.18.0       matrixStats_1.4.1          
[13] princurve_2.1.6             CytoTRACE2_1.1.0           
[15] RSpectra_0.16-2             Rfast_2.1.0                
[17] RcppParallel_5.1.9          RcppZiggurat_0.1.6         
[19] plyr_1.8.9                  Matrix_1.7-3               
[21] magrittr_2.0.3              HiClimR_2.2.1              
[23] doParallel_1.0.17           iterators_1.0.14           
[25] foreach_1.5.2               data.table_1.16.2          
[27] ggh4x_0.2.8                 cowplot_1.1.3              
[29] tictoc_1.2.1                harmony_1.2.3              
[31] Rcpp_1.0.13-1               qs2_0.1.7                  
[33] lubridate_1.9.3             forcats_1.0.0              
[35] stringr_1.5.1               dplyr_1.1.4                
[37] purrr_1.0.2                 readr_2.1.5                
[39] tidyr_1.3.1                 tibble_3.2.1               
[41] ggplot2_3.5.1               tidyverse_2.0.0            
[43] Seurat_5.2.1                SeuratObject_5.0.2         
[45] sp_2.1-4                   

loaded via a namespace (and not attached):
  [1] RcppAnnoy_0.0.22          splines_4.4.3            
  [3] later_1.4.1               polyclip_1.10-7          
  [5] fastDummies_1.7.4         lifecycle_1.0.4          
  [7] vroom_1.6.5               globals_0.16.3           
  [9] lattice_0.22-6            MASS_7.3-65              
 [11] plotly_4.10.4             rmarkdown_2.29           
 [13] yaml_2.3.10               httpuv_1.6.15            
 [15] sctransform_0.4.1         spam_2.11-0              
 [17] spatstat.sparse_3.1-0     reticulate_1.42.0        
 [19] pbapply_1.7-2             RColorBrewer_1.1-3       
 [21] abind_1.4-8               zlibbioc_1.52.0          
 [23] Rtsne_0.17                GenomeInfoDbData_1.2.13  
 [25] ggrepel_0.9.6             irlba_2.3.5.1            
 [27] listenv_0.9.1             spatstat.utils_3.1-1     
 [29] goftest_1.2-3             spatstat.random_3.3-2    
 [31] fitdistrplus_1.2-1        parallelly_1.39.0        
 [33] DelayedMatrixStats_1.28.0 ncdf4_1.23               
 [35] codetools_0.2-20          DelayedArray_0.32.0      
 [37] tidyselect_1.2.1          UCSC.utils_1.2.0         
 [39] farver_2.1.2              spatstat.explore_3.3-3   
 [41] jsonlite_1.8.9            progressr_0.15.1         
 [43] ggridges_0.5.6            survival_3.8-3           
 [45] systemfonts_1.1.0         tools_4.4.3              
 [47] ragg_1.3.3                ica_1.0-3                
 [49] glue_1.8.0                gridExtra_2.3            
 [51] SparseArray_1.6.0         xfun_0.49                
 [53] withr_3.0.2               fastmap_1.2.0            
 [55] fansi_1.0.6               digest_0.6.37            
 [57] timechange_0.3.0          R6_2.5.1                 
 [59] mime_0.12                 textshaping_0.4.0        
 [61] colorspace_2.1-1          scattermore_1.2          
 [63] tensor_1.5                spatstat.data_3.1-4      
 [65] RhpcBLASctl_0.23-42       utf8_1.2.4               
 [67] generics_0.1.3            httr_1.4.7               
 [69] htmlwidgets_1.6.4         S4Arrays_1.6.0           
 [71] uwot_0.2.2                pkgconfig_2.0.3          
 [73] gtable_0.3.6              lmtest_0.9-40            
 [75] XVector_0.46.0            htmltools_0.5.8.1        
 [77] dotCall64_1.2             scales_1.3.0             
 [79] png_0.1-8                 spatstat.univar_3.1-1    
 [81] knitr_1.49                tzdb_0.4.0               
 [83] reshape2_1.4.4            nlme_3.1-166             
 [85] zoo_1.8-12                KernSmooth_2.23-26       
 [87] miniUI_0.1.1.1            pillar_1.9.0             
 [89] grid_4.4.3                vctrs_0.6.5              
 [91] RANN_2.6.2                promises_1.3.2           
 [93] stringfish_0.18.0         xtable_1.8-4             
 [95] cluster_2.1.8.1           evaluate_1.0.1           
 [97] cli_3.6.3                 compiler_4.4.3           
 [99] rlang_1.1.4               crayon_1.5.3             
[101] future.apply_1.11.3       labeling_0.4.3           
[103] stringi_1.8.4             viridisLite_0.4.2        
[105] deldir_2.0-4              munsell_0.5.1            
[107] lazyeval_0.2.2            spatstat.geom_3.3-4      
[109] pacman_0.5.1              RcppHNSW_0.6.0           
[111] hms_1.1.3                 patchwork_1.3.0          
[113] sparseMatrixStats_1.18.0  bit64_4.5.2              
[115] future_1.34.0             shiny_1.9.1              
[117] ROCR_1.0-11               igraph_2.1.2             
[119] bit_4.5.0