Overview

Part 1 of 2 — Global CellChat analysis of the B-ALL microenvironment.

CellChat v2 run separately on BM and CNS then compared. CellChat objects are cached — if pre-computed objects exist they are loaded directly.

This script covers:

  1. IL-17 receptor landscape — resolving IL-17A vs IL-25 receptor biology
  2. Global interaction network — interaction counts, strength, signalling roles
  3. BM vs CNS comparison — differential interactions, information flow
  4. Hypothesis-driven pathways — CXCL, SCF/KIT, EGF, TGFβ, checkpoint, MHC

See 12b for CNS-focused immunosuppressive deep dive (Galectin-9/TIM-3, TREM2/APP myeloid programming, niche retention circuits, cell-type analysis).

Setup

library(CellChat)
library(Seurat)
library(qs2)
library(dplyr)
library(patchwork)
library(ggplot2)
library(ComplexHeatmap)
library(knitr)

if (!requireNamespace("ggalluvial", quietly = TRUE)) {
  cat("NOTE: ggalluvial not installed — river plots will be skipped\n")
}

base_path  <- "/exports/eddie/scratch/aduguid3"
cache_dir  <- file.path(base_path, "harmony_clustering")
tissue_cols <- c("BM" = "steelblue", "CNS" = "firebrick")

# ── Cache file paths ──
bm_cache      <- file.path(cache_dir, "12_cellchat_bm_v1.qs")
cns_cache     <- file.path(cache_dir, "12_cellchat_cns_v1.qs")
merged_cache  <- file.path(cache_dir, "12_cellchat_merged_v1.qs")
combined_cache <- file.path(cache_dir, "12_combined_seurat.qs")

Step 1 — Build or Load Combined Seurat Object

if (file.exists(combined_cache)) {
  cat("Loading cached combined Seurat object...\n")
  combined <- qs_read(combined_cache)
  cat("Combined cells:", ncol(combined), "\n")
  
} else {
  cat("Building combined object from scratch...\n")
  
  immune_obj <- qs_read(file.path(cache_dir, "11_immune_annotated_v3.qs"))
  cat("Immune cells:", ncol(immune_obj), "\n")
  
  leuk_path <- file.path(cache_dir, "05_leuk_all_scored.qs")
  leuk_obj  <- qs_read(leuk_path)
  cat("Leukaemia cells:", ncol(leuk_obj), "\n")
  
  leuk_obj$cell_annotation <- "B-ALL"
  
  shared_features <- intersect(rownames(immune_obj), rownames(leuk_obj))
  cat("Shared features:", length(shared_features), "\n")
  
  combined <- merge(
    immune_obj[shared_features, ],
    leuk_obj[shared_features, ],
    add.cell.ids = c("immune", "leuk")
  )
  # JoinLayers only needed for Seurat v5 Assay5 objects
  tryCatch(combined <- JoinLayers(combined),
           error = function(e) cat("JoinLayers not needed (v3 Assay)\n"))
  
  # ── Annotation mapping ──
  annotation_map <- c(
    "Vγ6Vδ4" = "Vg6Vd4", "Vγ4" = "Vg4", "Other γδ" = "Other_gd",
    "CD8_Tex" = "CD8_Tex", "CD8_Tpex" = "CD8_Tpex",
    "T_eff" = "CD8_Teff", "CD8_Teff (unclassified)" = "CD8_Teff",
    "CD8_Naive" = "CD8_Naive",
    "Effector CD4+ T cells" = "CD4_T",
    "NKT-like" = "NKT-like", "CD8+ NKT-like cells" = "NKT-like",
    "Natural killer cells" = "NK",
    "Macrophages" = "Macrophages",
    "Microglia-like macrophages" = "Microglia-like",
    "Non-classical monocytes" = "Monocytes",
    "Neutrophils" = "Neutrophils", "Myeloid Dendritic cells" = "DCs",
    "Pre-B cells" = "Pre-B", "Naive B cells" = "Pre-B",
    "Plasma B cells" = "Plasma_B",
    "Stroma" = "Stroma", "Progenitor cells" = "Progenitors",
    "Basophils" = "Basophils",
    "B-ALL" = "B-ALL"
  )
  
  combined$cellchat_group <- unname(annotation_map[combined$cell_annotation])
  combined$cellchat_group[is.na(combined$cellchat_group)] <- "Other"
  
  qs_save(combined, combined_cache)
  cat("Saved combined object to cache\n")
  
  rm(immune_obj, leuk_obj); gc()
}
## Loading cached combined Seurat object...
## Combined cells: 109322
cat("\nCellChat group distribution:\n")
## 
## CellChat group distribution:
print(sort(table(combined$cellchat_group), decreasing = TRUE))
## 
##          B-ALL          Pre-B    Macrophages      CD8_Naive Microglia-like 
##          91623           3195           2031           1768           1540 
##         Stroma    Neutrophils          Other       Plasma_B          CD4_T 
##           1442           1145            979            844            814 
##    Progenitors       NKT-like      Monocytes      Basophils       Other_gd 
##            714            676            606            555            505 
##        CD8_Tex       CD8_Teff       CD8_Tpex         Vg6Vd4            DCs 
##            284            146            143            123            117 
##            Vg4 
##             72
cat("\nTissue split:\n")
## 
## Tissue split:
print(table(combined$cellchat_group, combined$Tissue))
##                 
##                     BM   CNS
##   B-ALL          44606 47017
##   Basophils        523    32
##   CD4_T            631   183
##   CD8_Naive       1639   129
##   CD8_Teff          81    65
##   CD8_Tex          183   101
##   CD8_Tpex         113    30
##   DCs               76    41
##   Macrophages     1127   904
##   Microglia-like    64  1476
##   Monocytes        356   250
##   Neutrophils     1103    42
##   NKT-like         392   284
##   Other            887    92
##   Other_gd         253   252
##   Plasma_B         754    90
##   Pre-B           2987   208
##   Progenitors      709     5
##   Stroma           719   723
##   Vg4               40    32
##   Vg6Vd4             2   121

Step 2 — Build or Load CellChat Objects

Step 2a — BM

if (file.exists(bm_cache)) {
  cat("Loading cached BM CellChat object...\n")
  cellchat_bm <- qs_read(bm_cache)
  cat("BM pathways:", length(cellchat_bm@netP$pathways), "\n")
  
} else {
  cat("Building BM CellChat object (this takes ~30 min)...\n")
  
  bm_obj <- subset(combined, subset = Tissue == "BM")
  bm_obj <- NormalizeData(bm_obj, verbose = FALSE)
  
  cellchat_bm <- createCellChat(
    object = bm_obj, group.by = "cellchat_group",
    assay = DefaultAssay(bm_obj)
  )
  cellchat_bm@DB <- CellChatDB.mouse
  
  cellchat_bm <- subsetData(cellchat_bm)
  cellchat_bm <- identifyOverExpressedGenes(cellchat_bm)
  cellchat_bm <- identifyOverExpressedInteractions(cellchat_bm)
  cellchat_bm <- computeCommunProb(cellchat_bm, type = "triMean")
  cellchat_bm <- filterCommunication(cellchat_bm, min.cells = 10)
  cellchat_bm <- computeCommunProbPathway(cellchat_bm)
  cellchat_bm <- aggregateNet(cellchat_bm)
  
  qs_save(cellchat_bm, bm_cache)
  cat("Saved BM CellChat to cache\n")
  rm(bm_obj); gc()
}
## Loading cached BM CellChat object...
## BM pathways: 86

Step 2b — CNS

if (file.exists(cns_cache)) {
  cat("Loading cached CNS CellChat object...\n")
  cellchat_cns <- qs_read(cns_cache)
  cat("CNS pathways:", length(cellchat_cns@netP$pathways), "\n")
  
} else {
  cat("Building CNS CellChat object (this takes ~30 min)...\n")
  
  cns_obj <- subset(combined, subset = Tissue == "CNS")
  cns_obj <- NormalizeData(cns_obj, verbose = FALSE)
  
  cellchat_cns <- createCellChat(
    object = cns_obj, group.by = "cellchat_group",
    assay = DefaultAssay(cns_obj)
  )
  cellchat_cns@DB <- CellChatDB.mouse
  
  cellchat_cns <- subsetData(cellchat_cns)
  cellchat_cns <- identifyOverExpressedGenes(cellchat_cns)
  cellchat_cns <- identifyOverExpressedInteractions(cellchat_cns)
  cellchat_cns <- computeCommunProb(cellchat_cns, type = "triMean")
  cellchat_cns <- filterCommunication(cellchat_cns, min.cells = 10)
  cellchat_cns <- computeCommunProbPathway(cellchat_cns)
  cellchat_cns <- aggregateNet(cellchat_cns)
  
  qs_save(cellchat_cns, cns_cache)
  cat("Saved CNS CellChat to cache\n")
  rm(cns_obj); gc()
}
## Loading cached CNS CellChat object...
## CNS pathways: 122

Step 2c — Merged (Comparative)

if (file.exists(merged_cache)) {
  cat("Loading cached merged CellChat object...\n")
  cellchat_merged <- qs_read(merged_cache)
  
} else {
  cat("Building merged CellChat object...\n")
  
  # Compute centrality (needed for downstream)
  cellchat_bm  <- netAnalysis_computeCentrality(cellchat_bm)
  cellchat_cns <- netAnalysis_computeCentrality(cellchat_cns)
  
  # Lift to shared groups
  all_groups <- union(
    names(table(cellchat_bm@idents)),
    names(table(cellchat_cns@idents))
  )
  
  cellchat_bm_l  <- liftCellChat(cellchat_bm,  all_groups)
  cellchat_cns_l <- liftCellChat(cellchat_cns, all_groups)
  
  cellchat_merged <- mergeCellChat(
    list(BM = cellchat_bm_l, CNS = cellchat_cns_l),
    add.names = c("BM", "CNS")
  )
  
  qs_save(cellchat_merged, merged_cache)
  cat("Saved merged CellChat to cache\n")
}
## Loading cached merged CellChat object...

Step 3 — IL-17 Receptor Landscape

CellChat did not detect IL-17 signalling as significant (sparse Il17a transcript). Before moving to the broader landscape, we characterise the IL-17 receptor expression to clarify which cell types are wired to receive IL-17 family signals.

Critical finding from CellChatDB: The IL17RA/IL17RB heterodimer is the receptor for IL-25 (Il25), not IL-17A. IL-17A signals through IL17RA/IL17RC. This changes the mechanistic interpretation for leukaemia cells.

Step 3a — IL-17 Receptor Detection by Cell Type

il17_genes <- c("Il17a", "Il17f", "Il17ra", "Il17rb", "Il17rc", "Il25",
                "Il17rd", "Il17re", "Rorc", "Il23r", "Ccr6")
il17_present <- il17_genes[il17_genes %in% rownames(combined)]

# Detection rates per group per tissue
results <- list()
for (tissue in c("BM", "CNS")) {
  meta <- combined@meta.data[combined$Tissue == tissue, ]
  counts <- GetAssayData(combined, layer = "counts")[, rownames(meta)]
  
  for (gene in il17_present) {
    det <- tapply(counts[gene, ], meta$cellchat_group,
                  function(x) round(mean(x > 0) * 100, 1))
    for (grp in names(det)) {
      results[[length(results) + 1]] <- data.frame(
        Gene = gene, Group = grp, Tissue = tissue,
        Pct_Detected = det[grp], stringsAsFactors = FALSE
      )
    }
  }
}

il17_det <- bind_rows(results) %>%
  filter(Pct_Detected > 0)

# Heatmap of detection rates — CNS
il17_cns <- il17_det %>%
  filter(Tissue == "CNS") %>%
  tidyr::pivot_wider(id_cols = Group, names_from = Gene,
                     values_from = Pct_Detected, values_fill = 0)

il17_mat <- as.matrix(il17_cns[, -1])
rownames(il17_mat) <- il17_cns$Group

# Only show rows with any detection
il17_mat <- il17_mat[rowSums(il17_mat) > 0, , drop = FALSE]

ht <- Heatmap(il17_mat,
              name = "% Detected",
              column_title = "IL-17 family gene detection — CNS",
              col = circlize::colorRamp2(c(0, 5, 20, 50), 
                                          c("white", "lightyellow", "orange", "red")),
              cluster_rows = TRUE, cluster_columns = FALSE,
              row_names_gp = gpar(fontsize = 10),
              column_names_gp = gpar(fontsize = 10),
              cell_fun = function(j, i, x, y, width, height, fill) {
                val <- il17_mat[i, j]
                if (val > 0) {
                  grid.text(sprintf("%.1f", val), x, y,
                            gp = gpar(fontsize = 7))
                }
              })
ComplexHeatmap::draw(ht)

Step 3b — Key Receptor Check on B-ALL

cat("=== IL-17 receptor expression on B-ALL cells ===\n\n")
## === IL-17 receptor expression on B-ALL cells ===
for (tissue in c("BM", "CNS")) {
  meta <- combined@meta.data[combined$Tissue == tissue & 
                              combined$cellchat_group == "B-ALL", ]
  if (nrow(meta) == 0) next
  counts <- GetAssayData(combined, layer = "counts")[, rownames(meta)]
  
  cat(tissue, "B-ALL (n =", nrow(meta), "):\n")
  for (g in c("Il17ra", "Il17rb", "Il17rc", "Il25")) {
    if (g %in% rownames(counts)) {
      pct <- round(mean(counts[g, ] > 0) * 100, 1)
      cat("  ", g, ":", pct, "%\n")
    }
  }
  cat("\n")
}
## BM B-ALL (n = 44606 ):
##    Il17ra : 47.8 %
##    Il17rb : 47.3 %
##    Il17rc : 0.1 %
##    Il25 : 0 %
## 
## CNS B-ALL (n = 47017 ):
##    Il17ra : 44.8 %
##    Il17rb : 47.3 %
##    Il17rc : 0.1 %
##    Il25 : 0 %
cat("Interpretation:\n")
## Interpretation:
cat("- IL17RA + IL17RC = canonical IL-17A/F receptor\n")
## - IL17RA + IL17RC = canonical IL-17A/F receptor
cat("- IL17RA + IL17RB = IL-25 receptor (NOT IL-17A)\n")
## - IL17RA + IL17RB = IL-25 receptor (NOT IL-17A)
cat("- Check which complex the leukaemia cells express to clarify signalling\n")
## - Check which complex the leukaemia cells express to clarify signalling

Step 4 — Global Interaction Landscape

Step 4a — Interaction Counts

netVisual_circle(cellchat_bm@net$count,
                 weight.scale = TRUE, label.edge = FALSE,
                 title.name = "Number of interactions — BM")

netVisual_circle(cellchat_cns@net$count,
                 weight.scale = TRUE, label.edge = FALSE,
                 title.name = "Number of interactions — CNS")

Step 4b — Interaction Strength

netVisual_circle(cellchat_bm@net$weight,
                 weight.scale = TRUE, label.edge = FALSE,
                 title.name = "Interaction strength — BM")

netVisual_circle(cellchat_cns@net$weight,
                 weight.scale = TRUE, label.edge = FALSE,
                 title.name = "Interaction strength — CNS")

Step 4c — Signalling Roles

cellchat_cns <- netAnalysis_computeCentrality(cellchat_cns)
ht <- netAnalysis_signalingRole_heatmap(cellchat_cns, pattern = "outgoing",
                                         title = "Outgoing signals — CNS", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)

ht <- netAnalysis_signalingRole_heatmap(cellchat_cns, pattern = "incoming",
                                         title = "Incoming signals — CNS", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)

cellchat_bm <- netAnalysis_computeCentrality(cellchat_bm)
ht <- netAnalysis_signalingRole_heatmap(cellchat_bm, pattern = "outgoing",
                                         title = "Outgoing signals — BM", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)

ht <- netAnalysis_signalingRole_heatmap(cellchat_bm, pattern = "incoming",
                                         title = "Incoming signals — BM", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)

Step 5 — Comparative Analysis: BM vs CNS

Step 5a — Differential Interactions

print(compareInteractions(cellchat_merged, show.legend = TRUE))

netVisual_diffInteraction(cellchat_merged, weight.scale = TRUE,
                          title.name = "Differential interaction count (CNS - BM)")

netVisual_diffInteraction(cellchat_merged, weight.scale = TRUE,
                          measure = "weight",
                          title.name = "Differential interaction strength (CNS - BM)")

Step 5b — Information Flow

print(rankNet(cellchat_merged, mode = "comparison",
              stacked = TRUE, do.stat = TRUE) +
        ggtitle("Information flow — BM vs CNS"))

print(rankNet(cellchat_merged, mode = "comparison",
              stacked = FALSE, do.stat = TRUE) +
        ggtitle("Information flow (unstacked) — BM vs CNS"))

Step 5c — Signalling Changes: Key Populations

tryCatch({
  print(netAnalysis_signalingChanges_scatter(cellchat_merged,
                                             idents.use = "B-ALL") +
          ggtitle("B-ALL — signalling change (BM vs CNS)"))
}, error = function(e) cat("B-ALL scatter failed:", e$message, "\n"))

tryCatch({
  print(netAnalysis_signalingChanges_scatter(cellchat_merged,
                                             idents.use = "Macrophages") +
          ggtitle("Macrophages — signalling change (BM vs CNS)"))
}, error = function(e) cat("Macrophages scatter failed:", e$message, "\n"))

tryCatch({
  print(netAnalysis_signalingChanges_scatter(cellchat_merged,
                                             idents.use = "Stroma") +
          ggtitle("Stroma — signalling change (BM vs CNS)"))
}, error = function(e) cat("Stroma scatter failed:", e$message, "\n"))

tryCatch({
  print(netAnalysis_signalingChanges_scatter(cellchat_merged,
                                             idents.use = "Microglia-like") +
          ggtitle("Microglia-like — signalling change (BM vs CNS)"))
}, error = function(e) cat("Microglia-like scatter failed:", e$message, "\n"))

Step 6 — Hypothesis-Driven Pathway Analysis

Step 6a — CXCL Signalling (Niche → Leukaemia)

cxcl_paths <- grep("CXCL", cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
cat("CXCL pathways in CNS:", paste(cxcl_paths, collapse = ", "), "\n")
## CXCL pathways in CNS: CXCL
for (p in cxcl_paths) {
  netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}

for (p in cxcl_paths) {
  print(netAnalysis_contribution(cellchat_cns, signaling = p) +
          ggtitle(paste0(p, " L-R contribution — CNS")))
}

cxcl_bm <- grep("CXCL", cellchat_bm@netP$pathways, value = TRUE, ignore.case = TRUE)
for (p in cxcl_bm) {
  netVisual_aggregate(cellchat_bm, signaling = p, layout = "circle")
}

Step 6b — SCF / KIT

kit_paths <- grep("KIT|SCF", cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
cat("KIT/SCF pathways in CNS:", paste(kit_paths, collapse = ", "), "\n")
## KIT/SCF pathways in CNS: KIT
for (p in kit_paths) {
  netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}

Step 6c — EGF / AREG

egf_paths <- grep("EGF|AREG", cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
if (length(egf_paths) > 0) {
  cat("EGF/AREG pathways:", paste(egf_paths, collapse = ", "), "\n")
  for (p in egf_paths) {
    netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
  }
} else {
  cat("No EGF/AREG pathways detected in CNS\n")
}
## EGF/AREG pathways: EGF

Step 6d — TGFβ / IL-10 (Immunosuppressive)

supp_paths <- grep("TGFb|TGFB|IL10", cellchat_cns@netP$pathways,
                    value = TRUE, ignore.case = TRUE)
cat("Immunosuppressive pathways in CNS:", paste(supp_paths, collapse = ", "), "\n")
## Immunosuppressive pathways in CNS: TGFb
for (p in supp_paths) {
  netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}

Step 6e — Checkpoint (PD-L1, TIGIT, Galectin)

ckpt_paths <- grep("PD-L|PDL|TIGIT|CTLA|GALECTIN|CD226|PVR|CD80|CD86",
                    cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
cat("Checkpoint pathways in CNS:", paste(ckpt_paths, collapse = ", "), "\n")
## Checkpoint pathways in CNS: GALECTIN, CD86, PD-L1, CD80, PDL2
for (p in ckpt_paths) {
  netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}

Step 6f — MHC / Antigen Presentation

mhc_paths <- grep("MHC|MHCI|MHCII", cellchat_cns@netP$pathways,
                    value = TRUE, ignore.case = TRUE)
cat("MHC pathways in CNS:", paste(mhc_paths, collapse = ", "), "\n")
## MHC pathways in CNS: MHC-I, MHC-II
for (p in mhc_paths) {
  netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}

Session Information

sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Rocky Linux 9.5 (Blue Onyx)
## 
## Matrix products: default
## BLAS/LAPACK: /opt/intel/oneapi/mkl/2024.0/lib/libmkl_gf_lp64.so.2;  LAPACK version 3.10.1
## 
## locale:
##  [1] LC_CTYPE=en_GB.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_GB.UTF-8        LC_COLLATE=en_GB.UTF-8    
##  [5] LC_MONETARY=en_GB.UTF-8    LC_MESSAGES=en_GB.UTF-8   
##  [7] LC_PAPER=en_GB.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_GB.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Europe/London
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] grid      stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] knitr_1.51            ComplexHeatmap_2.22.0 patchwork_1.3.2      
##  [4] qs2_0.1.7             Seurat_5.4.0          SeuratObject_5.3.0   
##  [7] sp_2.1-4              CellChat_2.2.0.9001   Biobase_2.66.0       
## [10] BiocGenerics_0.52.0   ggplot2_4.0.2         igraph_2.2.2         
## [13] dplyr_1.1.4          
## 
## loaded via a namespace (and not attached):
##   [1] RcppAnnoy_0.0.23       splines_4.4.1          later_1.4.6           
##   [4] tibble_3.3.1           polyclip_1.10-7        ggnetwork_0.5.14      
##   [7] fastDummies_1.7.5      lifecycle_1.0.5        rstatix_0.7.3         
##  [10] doParallel_1.0.17      globals_0.16.3         lattice_0.22-6        
##  [13] MASS_7.3-60.2          backports_1.4.1        magrittr_2.0.3        
##  [16] plotly_4.12.0          sass_0.4.9             rmarkdown_2.30        
##  [19] jquerylib_0.1.4        yaml_2.3.12            httpuv_1.6.16         
##  [22] otel_0.2.0             collapse_2.1.6         NMF_0.28              
##  [25] sctransform_0.4.3      spam_2.11-3            spatstat.sparse_3.1-0 
##  [28] reticulate_1.45.0      cowplot_1.2.0          pbapply_1.7-4         
##  [31] RColorBrewer_1.1-3     abind_1.4-5            Rtsne_0.17            
##  [34] purrr_1.0.2            circlize_0.4.17        IRanges_2.40.1        
##  [37] S4Vectors_0.44.0       ggrepel_0.9.6          irlba_2.3.7           
##  [40] listenv_0.9.1          spatstat.utils_3.2-1   goftest_1.2-3         
##  [43] RSpectra_0.16-2        spatstat.random_3.4-4  fitdistrplus_1.2-6    
##  [46] parallelly_1.37.1      svglite_2.2.2          codetools_0.2-20      
##  [49] tidyselect_1.2.1       shape_1.4.6.1          farver_2.1.2          
##  [52] matrixStats_1.5.0      stats4_4.4.1           spatstat.explore_3.7-0
##  [55] jsonlite_2.0.0         GetoptLong_1.1.0       BiocNeighbors_2.0.1   
##  [58] progressr_0.18.0       Formula_1.2-5          ggridges_0.5.6        
##  [61] ggalluvial_0.12.6      survival_3.6-4         iterators_1.0.14      
##  [64] systemfonts_1.3.1      foreach_1.5.2          tools_4.4.1           
##  [67] sna_2.8                ica_1.0-3              Rcpp_1.0.12           
##  [70] glue_1.8.0             gridExtra_2.3          xfun_0.56             
##  [73] withr_3.0.2            BiocManager_1.30.27    fastmap_1.2.0         
##  [76] fansi_1.0.6            digest_0.6.35          R6_2.6.1              
##  [79] mime_0.12              textshaping_0.3.7      colorspace_2.1-0      
##  [82] Cairo_1.7-0            scattermore_1.2        tensor_1.5.1          
##  [85] dichromat_2.0-0.1      spatstat.data_3.1-9    utf8_1.2.4            
##  [88] tidyr_1.3.1            generics_0.1.3         data.table_1.15.4     
##  [91] FNN_1.1.4.1            httr_1.4.7             htmlwidgets_1.6.4     
##  [94] uwot_0.2.4             pkgconfig_2.0.3        gtable_0.3.6          
##  [97] registry_0.5-1         lmtest_0.9-40          S7_0.2.1              
## [100] htmltools_0.5.8.1      carData_3.0-6          dotCall64_1.2         
## [103] clue_0.3-67            scales_1.4.0           png_0.1-8             
## [106] spatstat.univar_3.1-6  rstudioapi_0.16.0      reshape2_1.4.4        
## [109] rjson_0.2.23           nlme_3.1-164           coda_0.19-4.1         
## [112] statnet.common_4.13.0  cachem_1.1.0           zoo_1.8-15            
## [115] GlobalOptions_0.1.3    stringr_1.5.1          KernSmooth_2.23-24    
## [118] parallel_4.4.1         miniUI_0.1.2           pillar_1.9.0          
## [121] vctrs_0.6.5            RANN_2.6.2             promises_1.5.0        
## [124] ggpubr_0.6.2           stringfish_0.18.0      car_3.1-5             
## [127] xtable_1.8-4           cluster_2.1.6          evaluate_1.0.5        
## [130] cli_3.6.5              compiler_4.4.1         rlang_1.1.7           
## [133] crayon_1.5.2           rngtools_1.5.2         future.apply_1.11.2   
## [136] ggsignif_0.6.4         labeling_0.4.3         plyr_1.8.9            
## [139] stringi_1.8.4          deldir_2.0-4           viridisLite_0.4.2     
## [142] network_1.20.0         gridBase_0.4-7         lazyeval_0.2.2        
## [145] spatstat.geom_3.7-0    Matrix_1.7-0           RcppHNSW_0.6.0        
## [148] future_1.33.2          shiny_1.12.1           ROCR_1.0-12           
## [151] broom_1.0.5            RcppParallel_5.1.8     bslib_0.7.0