1 Step 1. Load Libraries

library(readxl)
library(clusterProfiler)
library(org.Hs.eg.db)
library(ReactomePA)
library(enrichplot)
library(msigdbr)
library(openxlsx)
library(stringr)
library(purrr)
library(tibble)
library(tidyr)
library(ggplot2)
library(dplyr)   # load LAST so dplyr verbs win the masking conflict

2 Step 2. Load Top100 Marker Table

markers <- read_excel("../Supplementary_Table_S6.xlsx") %>%
  rename_with(tolower) %>%
  mutate(cluster = as.character(cluster))

# Fix known outdated/renamed gene symbols
symbol_updates <- c("QARS" = "QARS1", "CARS" = "CARS1", "WARS" = "WARS1")
markers <- markers %>%
  mutate(gene = ifelse(gene %in% names(symbol_updates), symbol_updates[gene], gene)) %>%
  filter(gene != "46083.0")   # remove spreadsheet artifact

clusters_list <- as.character(sort(as.numeric(unique(markers$cluster))))
cat("Clusters found (numeric order):", paste(clusters_list, collapse = ", "), "\n")
Clusters found (numeric order): 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 
print(colnames(markers))
[1] "p_val"      "avg_log2fc" "pct.1"      "pct.2"      "p_val_adj"  "cluster"    "gene"      

3 Step 3. Build Background Universe and Per-Cluster Gene Lists

# Background = all genes that appear anywhere in the marker table (all clusters combined)
background_genes <- unique(markers$gene)
cat("Background universe size:", length(background_genes), "genes\n")
Background universe size: 1095 genes
# Map SYMBOL -> ENTREZID (needed for enrichKEGG and ReactomePA)
gene_map <- bitr(background_genes, fromType = "SYMBOL", toType = "ENTREZID",
                  OrgDb = org.Hs.eg.db, drop = TRUE)

background_entrez <- unique(gene_map$ENTREZID)

get_cluster_genes <- function(cluster_id, marker_df, top_n = 100) {
  marker_df %>%
    filter(cluster == cluster_id) %>%
    distinct(gene, avg_log2fc) %>%
    arrange(desc(avg_log2fc)) %>%
    slice_head(n = top_n) %>%
    pull(gene)
}

cluster_gene_lists <- setNames(
  lapply(clusters_list, get_cluster_genes, marker_df = markers),
  clusters_list
)

sapply(cluster_gene_lists, length)
  0   1   2   3   4   5   6   7   8   9  10  11  12  13 
100 100 100 100 100 100 100 100 100 100 100  99 100 100 

4 Step 4. Load Hallmark Gene Sets (for enricher())

hallmark_sets <- msigdbr(species = "Homo sapiens", collection = "H") %>%
  distinct(gs_name, gene_symbol)

5 Step 5. Run ORA for Each Cluster (GO:BP, KEGG, Reactome, Hallmark)

all_results <- list()

for (cl in clusters_list) {
  message("Running ORA for cluster ", cl)

  genes_symbol <- cluster_gene_lists[[cl]]
  genes_entrez <- gene_map$ENTREZID[gene_map$SYMBOL %in% genes_symbol]

  res_list <- list()

  # GO:BP
  res_list$GO_BP <- tryCatch(
    enrichGO(gene = genes_symbol, universe = background_genes,
             OrgDb = org.Hs.eg.db, keyType = "SYMBOL",
             ont = "BP", pAdjustMethod = "BH",
             pvalueCutoff = 1, qvalueCutoff = 1),
    error = function(e) { message("  GO:BP failed: ", e$message); NULL })

  # KEGG
  res_list$KEGG <- tryCatch(
    enrichKEGG(gene = genes_entrez, universe = background_entrez,
               organism = "hsa", pAdjustMethod = "BH",
               pvalueCutoff = 1, qvalueCutoff = 1),
    error = function(e) { message("  KEGG failed: ", e$message); NULL })

  # Reactome
  res_list$Reactome <- tryCatch(
    enrichPathway(gene = genes_entrez, universe = background_entrez,
                   organism = "human", pAdjustMethod = "BH",
                   pvalueCutoff = 1, qvalueCutoff = 1, readable = TRUE),
    error = function(e) { message("  Reactome failed: ", e$message); NULL })

  # Hallmark (via generic enricher)
  res_list$Hallmark <- tryCatch(
    enricher(gene = genes_symbol, universe = background_genes,
             TERM2GENE = hallmark_sets, pAdjustMethod = "BH",
             pvalueCutoff = 1, qvalueCutoff = 1),
    error = function(e) { message("  Hallmark failed: ", e$message); NULL })

  all_results[[cl]] <- res_list
}

6 Step 6. Export All Enrichment Tables to Excel

wb <- createWorkbook()

for (cl in names(all_results)) {
  res <- all_results[[cl]]
  if (is.null(res)) next

  for (ont in names(res)) {
    obj <- res[[ont]]
    if (is.null(obj) || nrow(as.data.frame(obj)) == 0) next

    sheet_name <- substr(paste0("C", cl, "_", ont), 1, 31)
    addWorksheet(wb, sheet_name)
    writeData(wb, sheet_name, as.data.frame(obj))
  }
}

saveWorkbook(wb, "Cluster_ORA_Results_Top100.xlsx", overwrite = TRUE)

7 Step 7. Combine Top Terms per Cluster into One Long Table

build_long_ora <- function(all_results) {
  purrr::map_dfr(names(all_results), function(cl) {
    res <- all_results[[cl]]
    purrr::map_dfr(names(res), function(db) {
      obj <- res[[db]]
      if (is.null(obj)) return(NULL)
      df <- as.data.frame(obj)
      if (nrow(df) == 0) return(NULL)
      df <- as.data.frame(df)   # strip any lingering S4/tibble subclass
      df$cluster <- cl
      df$database <- db
      dplyr::select(df, cluster, database, ID, Description, GeneRatio, BgRatio,
                     pvalue, p.adjust, qvalue, geneID, Count)
    })
  })
}

all_long_ora <- build_long_ora(all_results)
write.csv(all_long_ora, "Cluster_ORA_Results_long.csv", row.names = FALSE)

cat("Total enriched terms across all clusters/databases:", nrow(all_long_ora), "\n")
Total enriched terms across all clusters/databases: 20335 

8 Step 8. Check How Many Significant Terms per Cluster

all_long_ora %>%
  group_by(cluster) %>%
  summarise(n_sig_padj05 = sum(p.adjust < 0.05, na.rm = TRUE),
            n_sig_padj25 = sum(p.adjust < 0.25, na.rm = TRUE),
            n_total_terms = n(),
            min_padj = suppressWarnings(min(p.adjust, na.rm = TRUE))) %>%
  arrange(as.numeric(as.character(cluster)))

9 Step 9. Parse GeneRatio to Numeric (needed for dotplot x-axis)

parse_ratio <- function(x) {
  parts <- str_split(x, "/", simplify = TRUE)
  as.numeric(parts[, 1]) / as.numeric(parts[, 2])
}

all_long_ora <- all_long_ora %>%
  mutate(GeneRatioNum = parse_ratio(GeneRatio))

10 Step 10. Dotplot per Cluster (GeneRatio x, Count size, p.adjust color)

plot_cluster_ora_dotplot <- function(cluster_id, long_df, n_top = 10, sig_cutoff = 0.05) {

  df <- long_df %>%
    filter(cluster == cluster_id, p.adjust < sig_cutoff) %>%
    arrange(p.adjust) %>%
    slice_head(n = n_top)

  used_cutoff <- sig_cutoff

  # Fallback if nothing passes 0.05
  if (nrow(df) == 0) {
    df <- long_df %>%
      filter(cluster == cluster_id, p.adjust < 0.25) %>%
      arrange(p.adjust) %>%
      slice_head(n = n_top)
    used_cutoff <- 0.25
  }

  if (nrow(df) == 0) {
    cat("**No enriched terms reached p.adjust < 0.25 for cluster", cluster_id, "**\n\n")
    return(invisible(NULL))
  }

  df <- df %>%
    mutate(Description = str_wrap(Description, width = 40),
           label = paste0(Description, " [", database, "]"))

  p <- ggplot(df, aes(x = GeneRatioNum, y = reorder(label, GeneRatioNum),
                       size = Count, color = p.adjust)) +
    geom_point() +
    scale_color_gradient(low = "#B2182B", high = "#4393C3", name = "p.adjust") +
    scale_size_continuous(name = "Gene count", range = c(3, 9)) +
    labs(title = paste0("Cluster ", cluster_id, " - Top100 ORA (p.adjust<", used_cutoff, ")"),
         x = "Gene Ratio", y = NULL) +
    theme_minimal(base_size = 11) +
    theme(axis.text.y = element_text(size = 9))

  print(p)
  ggsave(filename = paste0("cluster", cluster_id, "_top100_ORA_dotplot.png"),
         plot = p, width = 9, height = 6, dpi = 300)
  cat("\n\n")
}

for (cl in clusters_list) {
  cat("\n## Cluster", cl, "- ORA Dotplot\n\n")
  plot_cluster_ora_dotplot(cl, all_long_ora)
}

10.1 Cluster 0 - ORA Dotplot

10.2 Cluster 1 - ORA Dotplot

10.3 Cluster 2 - ORA Dotplot

No enriched terms reached p.adjust < 0.25 for cluster 2

10.4 Cluster 3 - ORA Dotplot

10.5 Cluster 4 - ORA Dotplot

10.6 Cluster 5 - ORA Dotplot

10.7 Cluster 6 - ORA Dotplot

10.8 Cluster 7 - ORA Dotplot

10.9 Cluster 8 - ORA Dotplot

10.10 Cluster 9 - ORA Dotplot

No enriched terms reached p.adjust < 0.25 for cluster 9

10.11 Cluster 10 - ORA Dotplot

No enriched terms reached p.adjust < 0.25 for cluster 10

10.12 Cluster 11 - ORA Dotplot

10.13 Cluster 12 - ORA Dotplot

10.14 Cluster 13 - ORA Dotplot

11 Step 11. Table of Top Pathways per Cluster (Printed Individually)

for (cl in clusters_list) {
  cat("\n### Cluster", cl, "- Top Pathways (p.adjust < 0.05, fallback 0.25)\n\n")

  df <- all_long_ora %>%
    dplyr::filter(cluster == cl, p.adjust < 0.05) %>%
    dplyr::arrange(p.adjust)

  if (nrow(df) == 0) {
    df <- all_long_ora %>%
      dplyr::filter(cluster == cl, p.adjust < 0.25) %>%
      dplyr::arrange(p.adjust)
  }

  if (nrow(df) == 0) {
    cat("No enriched pathways found for this cluster.\n\n")
    next
  }

  df_show <- df %>%
    dplyr::slice_head(n = 10) %>%
    dplyr::select(database, Description, GeneRatio, p.adjust, Count, geneID) %>%
    dplyr::mutate(p.adjust = formatC(p.adjust, format = "e", digits = 2))

  print(knitr::kable(df_show, caption = paste0("Cluster ", cl, " top pathways")))
  cat("\n\n")
}

11.0.1 Cluster 0 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 0 top pathways
database Description GeneRatio p.adjust Count geneID
hsa05152 KEGG Tuberculosis 6/42 3.91e-02 6 6772/3117/9902/3123/972/3119
hsa05416 KEGG Viral myocarditis 5/42 3.91e-02 5 3117/1756/3123/958/3119

11.0.2 Cluster 1 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 1 top pathways
database Description GeneRatio p.adjust Count geneID
hsa04650 KEGG Natural killer cell mediated cytotoxicity 7/48 5.00e-02 7 3805/3821/3802/22914/3811/3804/5551

11.0.3 Cluster 2 - Top Pathways (p.adjust < 0.05, fallback 0.25)

No enriched pathways found for this cluster.

11.0.4 Cluster 3 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 3 top pathways
database Description GeneRatio p.adjust Count geneID
R-HSA-74160 Reactome Gene expression (Transcription) 12/48 9.02e-02 12 TCF7/TXNIP/LEF1/MAML2/BTG1/DYRK2/PHF1/FOXO1/SOCS3/ATM/ZNF101/SESN3
R-HSA-212436 Reactome Generic Transcription Pathway 11/48 9.02e-02 11 TCF7/TXNIP/LEF1/MAML2/BTG1/DYRK2/FOXO1/SOCS3/ATM/ZNF101/SESN3
R-HSA-73857 Reactome RNA Polymerase II Transcription 11/48 9.02e-02 11 TCF7/TXNIP/LEF1/MAML2/BTG1/DYRK2/FOXO1/SOCS3/ATM/ZNF101/SESN3
GO:0030217 GO_BP T cell differentiation 12/77 2.19e-01 12 TCF7/LEF1/IL6ST/TGFBR2/CD27/LY9/FOXP1/CD28/IL7R/SOCS3/ITPKB/ZFP36L1
GO:0030098 GO_BP lymphocyte differentiation 13/77 2.19e-01 13 TCF7/LEF1/IL6ST/TGFBR2/CD27/LY9/FOXP1/CD28/IL7R/SOCS3/ATM/ITPKB/ZFP36L1
GO:0019222 GO_BP regulation of metabolic process 46/77 2.19e-01 46 TMIGD2/ADTRP/BEX4/TCF7/LDLRAP1/ITGA6/NCF1/TXK/LBH/TXNIP/PLAC8/FHIT/BEX2/LEF1/IL6ST/UTY/TGFBR2/ABLIM1/SCML4/APBB1/APBA2/TRIM22/SATB1/PLCL1/LY9/MAML2/SNX9/BTG1/DYRK2/PNRC1/SFMBT2/BEX3/FOXP1/CD28/PHF1/FOXO1/TSHZ2/IL7R/ATM/SBNO2/ZNF101/IL16/ZFP36L1/CREBRF/SESN3/HLA-E
GO:0035264 GO_BP multicellular organism growth 6/77 2.19e-01 6 PLAC8/APBA2/SELENOM/PLEKHA1/ATM/ZFP36L1

11.0.5 Cluster 4 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 4 top pathways
database Description GeneRatio p.adjust Count geneID
HALLMARK_HEME_METABOLISM Hallmark HALLMARK_HEME_METABOLISM 5/38 9.75e-02 5 CLIC2/BLVRB/OSBP2/MPP1/GDE1

11.0.6 Cluster 5 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 5 top pathways
database Description GeneRatio p.adjust Count geneID
R-HSA-1266738 Reactome Developmental Biology 20/63 4.91e-02 20 NELL2/MAML2/FOXP1/ARHGEF28/ZNF521/SEMA4A/KRT1/DSC1/IL12RB2/CEBPD/BMP4/PKP2/KITLG/EPHA1/PTK2/EPHB1/CSF1/RUNX1/PRKCA/LRIG1

11.0.7 Cluster 6 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 6 top pathways
database Description GeneRatio p.adjust Count geneID
R-HSA-112316 Reactome Neuronal System 8/50 2.45e-01 8 GRIA4/GNB4/HOMER2/TUBB6/EPB41L2/MAOA/NEFL/NLGN1

11.0.8 Cluster 7 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 7 top pathways
database Description GeneRatio p.adjust Count geneID
GO:0007059 GO_BP chromosome segregation 41/92 1.08e-32 41 PSRC1/SGO2/NDC80/ASPM/KIF14/NEK2/DLGAP5/NUSAP1/TOP2A/GPSM2/CENPE/ECT2/KIFC1/CDCA2/HJURP/CENPF/CDK1/KIF23/MKI67/CCNB2/CDCA8/KIF4A/BUB1/RACGAP1/SGO1/AURKA/KNL1/FAM83D/BIRC5/UBE2C/NUF2/KIF15/PLK1/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF18B
GO:1903047 GO_BP mitotic cell cycle process 49/92 4.90e-29 49 PSRC1/CDKN2C/NDC80/KIF14/KIF20A/NEK2/DLGAP5/NUSAP1/GPSM2/CENPE/CENPA/ECT2/CDC25C/CCNA2/KIFC1/CDCA2/CENPF/STMN1/CDK1/KIF23/TK1/MKI67/CCNB2/CCNF/CDCA8/KIF4A/RRM2/BUB1/RACGAP1/AURKA/CDKN3/KNL1/BIRC5/UBE2C/NUF2/CIT/KIF15/PLK1/CDKN2A/CEP55/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF20B/KIF18B
GO:0098813 GO_BP nuclear chromosome segregation 36/92 2.56e-28 36 PSRC1/NDC80/ASPM/KIF14/NEK2/DLGAP5/NUSAP1/TOP2A/CENPE/ECT2/KIFC1/CENPF/CDK1/KIF23/CCNB2/CDCA8/KIF4A/BUB1/RACGAP1/SGO1/AURKA/KNL1/FAM83D/BIRC5/UBE2C/NUF2/KIF15/PLK1/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF18B
GO:0051301 GO_BP cell division 46/92 2.56e-28 46 PSRC1/SGO2/NDC80/ASPM/KIF14/KIF20A/NEK2/NUSAP1/TOP2A/GPSM2/CENPE/CENPA/ECT2/CDC25C/CCNA2/KIFC1/CDCA2/CENPF/STMN1/CDK1/KIF23/CDCA3/CCNB2/CCNF/CDCA8/KIF4A/BUB1/RACGAP1/SGO1/AURKA/KNL1/FAM83D/BIRC5/UBE2C/NUF2/CIT/PLK1/CDKN2A/CEP55/INCENP/TPX2/KIF2C/SMC4/SPC24/KIF20B/KIF18B
GO:0022402 GO_BP cell cycle process 55/92 2.56e-28 55 PSRC1/CDKN2C/SGO2/NDC80/ASPM/KIF14/KIF20A/NEK2/DLGAP5/NUSAP1/TOP2A/GPSM2/CENPE/CENPA/ECT2/CDC25C/CCNA2/KIFC1/CDCA2/HJURP/CENPF/STMN1/CDK1/KIF23/TK1/MKI67/CCNB2/CCNF/CDCA8/KIF4A/RRM2/BUB1/RACGAP1/SGO1/AURKA/CDKN3/KNL1/FAM83D/BIRC5/UBE2C/NUF2/CIT/KIF15/PLK1/CDKN2A/CEP55/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF20B/KIF18B
GO:0000278 GO_BP mitotic cell cycle 50/92 4.16e-28 50 PSRC1/CDKN2C/NDC80/KIF14/KIF20A/NEK2/DLGAP5/NUSAP1/GPSM2/CENPE/CENPA/ECT2/CDC25C/CCNA2/KIFC1/CDCA2/CENPF/STMN1/TUBA4A/CDK1/KIF23/TK1/MKI67/CCNB2/CCNF/CDCA8/KIF4A/RRM2/BUB1/RACGAP1/AURKA/CDKN3/KNL1/BIRC5/UBE2C/NUF2/CIT/KIF15/PLK1/CDKN2A/CEP55/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF20B/KIF18B
GO:0000280 GO_BP nuclear division 38/92 1.87e-27 38 PSRC1/NDC80/ASPM/KIF14/NEK2/DLGAP5/NUSAP1/TOP2A/CENPE/CDC25C/KIFC1/CDCA2/CENPF/CDK1/KIF23/MKI67/CCNB2/CDCA8/KIF4A/BUB1/RACGAP1/SGO1/AURKA/KNL1/BIRC5/UBE2C/NUF2/KIF15/PLK1/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF20B/KIF18B
HALLMARK_G2M_CHECKPOINT Hallmark HALLMARK_G2M_CHECKPOINT 33/58 2.87e-27 33 CDKN2C/NDC80/NEK2/NUSAP1/TOP2A/CENPE/CENPA/CCNA2/CENPF/STMN1/HMMR/CDK1/KIF23/MKI67/CCNB2/CCNF/KIF4A/KPNA2/BUB1/RACGAP1/AURKA/CDKN3/KNL1/BIRC5/UBE2C/KIF15/PLK1/INCENP/TPX2/KIF2C/KIF22/SMC4/KIF20B
GO:0007049 GO_BP cell cycle 58/92 7.80e-27 58 PSRC1/CDKN2C/SGO2/NDC80/ASPM/KIF14/KIF20A/NEK2/HPGD/DLGAP5/NUSAP1/TOP2A/GPSM2/CENPE/CENPA/ECT2/CDC25C/CCNA2/KIFC1/CDCA2/HJURP/CENPF/STMN1/TUBA4A/CDK1/KIF23/TK1/MKI67/CCNB2/CCNF/CDCA8/KIF4A/RRM2/BUB1/RACGAP1/SGO1/AURKA/CDKN3/KNL1/FAM83D/BIRC5/UBE2C/NUF2/PRR11/CIT/KIF15/PLK1/CDKN2A/CEP55/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF20B/KIF18B
GO:0048285 GO_BP organelle fission 38/92 3.65e-26 38 PSRC1/NDC80/ASPM/KIF14/NEK2/DLGAP5/NUSAP1/TOP2A/CENPE/CDC25C/KIFC1/CDCA2/CENPF/CDK1/KIF23/MKI67/CCNB2/CDCA8/KIF4A/BUB1/RACGAP1/SGO1/AURKA/KNL1/BIRC5/UBE2C/NUF2/KIF15/PLK1/INCENP/TPX2/KIF2C/KIF22/KIF18A/SMC4/SPC24/KIF20B/KIF18B

11.0.9 Cluster 8 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 8 top pathways
database Description GeneRatio p.adjust Count geneID
GO:0034220 GO_BP monoatomic ion transmembrane transport 13/84 1.25e-01 13 ATP7B/TMEM163/KCNQ2/KCND2/ATP12A/CACNA1D/CHRNA6/ORAI3/GRIA4/NCS1/KCNMA1/SLC26A4/CACNA2D1
R-HSA-9709957 Reactome Sensory Perception 5/50 1.34e-01 5 CACNA1D/LRP2/LRP12/KCNMA1/RDH10
GO:0006811 GO_BP monoatomic ion transport 15/84 1.40e-01 15 LRP2/ATP7B/TMEM163/KCNQ2/KCND2/ATP12A/CACNA1D/CHRNA6/ORAI3/GRIA4/NCS1/STC2/KCNMA1/SLC26A4/CACNA2D1

11.0.10 Cluster 9 - Top Pathways (p.adjust < 0.05, fallback 0.25)

No enriched pathways found for this cluster.

11.0.11 Cluster 10 - Top Pathways (p.adjust < 0.05, fallback 0.25)

No enriched pathways found for this cluster.

11.0.12 Cluster 11 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 11 top pathways
database Description GeneRatio p.adjust Count geneID
HALLMARK_EPITHELIAL_MESENCHYMAL_TRANSITION Hallmark HALLMARK_EPITHELIAL_MESENCHYMAL_TRANSITION 11/60 5.68e-03 11 FUCA1/SAT1/IL32/TIMP1/FN1/SPOCK1/LGALS1/IGFBP3/BASP1/ID2/ITGB1
GO:0009416 GO_BP response to light stimulus 8/93 7.82e-03 8 CDKN1A/RGS9/NMU/TIMP1/BHLHE40/MDM2/ID2/ITGB1
R-HSA-1474228 Reactome Degradation of the extracellular matrix 6/72 2.67e-02 6 FN1/MMP25/ADAM8/TIMP1/CTSD/FURIN
GO:0019058 GO_BP viral life cycle 12/93 5.00e-02 12 APOBEC3G/SLAMF1/IL32/LGALS1/FURIN/GPR15/APOBEC3C/ITGB7/IFITM2/GBP2/ITGB1/IFITM3

11.0.13 Cluster 12 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 12 top pathways
database Description GeneRatio p.adjust Count geneID
HALLMARK_TNFA_SIGNALING_VIA_NFKB Hallmark HALLMARK_TNFA_SIGNALING_VIA_NFKB 19/49 3.02e-06 19 SERPINE1/CSF2/DUSP5/TNFSF9/DUSP4/RNF19B/PHLDA1/IER3/SDC4/PPP1R15A/ATF3/DUSP1/SQSTM1/TNF/TNFAIP8/CFLAR/TRIB1/PTGER4/STAT5A
GO:0008625 GO_BP extrinsic apoptotic signaling pathway via death domain receptors 8/86 2.28e-02 8 SERPINE1/LGALS3/ATF3/BAG3/TNF/DAPK1/CFLAR/TNFSF10
GO:2001237 GO_BP negative regulation of extrinsic apoptotic signaling pathway 8/86 2.28e-02 8 SERPINE1/CSF2/HSPA1B/HSPA1A/LGALS3/LMNA/TNF/CFLAR
GO:0097191 GO_BP extrinsic apoptotic signaling pathway 12/86 2.28e-02 12 SERPINE1/CSF2/HSPA1B/HSPA1A/LGALS3/ATF3/LMNA/BAG3/TNF/DAPK1/CFLAR/TNFSF10
GO:0042981 GO_BP regulation of apoptotic process 30/86 2.28e-02 30 SERPINE1/CSF2/CCL3/HSPA1B/CD40/HSPA1A/TNFSF9/PHLDA1/RGCC/IER3/LGALS3/GADD45G/PTGIS/SERPINB9/CTLA4/MEF2C/ATF3/CYP1B1/LMNA/DUSP1/SQSTM1/BAG3/TNF/DAPK1/TNFAIP8/CFLAR/TNFSF10/ARHGAP10/ZEB2/STAT5A
GO:2001236 GO_BP regulation of extrinsic apoptotic signaling pathway 10/86 2.41e-02 10 SERPINE1/CSF2/HSPA1B/HSPA1A/LGALS3/ATF3/LMNA/TNF/CFLAR/TNFSF10
GO:0043067 GO_BP regulation of programmed cell death 30/86 2.60e-02 30 SERPINE1/CSF2/CCL3/HSPA1B/CD40/HSPA1A/TNFSF9/PHLDA1/RGCC/IER3/LGALS3/GADD45G/PTGIS/SERPINB9/CTLA4/MEF2C/ATF3/CYP1B1/LMNA/DUSP1/SQSTM1/BAG3/TNF/DAPK1/TNFAIP8/CFLAR/TNFSF10/ARHGAP10/ZEB2/STAT5A
GO:2001234 GO_BP negative regulation of apoptotic signaling pathway 9/86 2.60e-02 9 SERPINE1/CSF2/HSPA1B/HSPA1A/IER3/LGALS3/LMNA/TNF/CFLAR
GO:0043065 GO_BP positive regulation of apoptotic process 17/86 3.23e-02 17 CCL3/CD40/PHLDA1/RGCC/GADD45G/PTGIS/CTLA4/MEF2C/ATF3/CYP1B1/DUSP1/SQSTM1/TNF/DAPK1/TNFAIP8/CFLAR/TNFSF10
GO:0006915 GO_BP apoptotic process 33/86 3.23e-02 33 SERPINE1/CSF2/GZMB/CCL3/HSPA1B/CD40/HSPA1A/TNFSF9/PHLDA1/RGCC/IER3/LGALS3/GADD45G/PTGIS/SERPINB9/PPP1R15A/CTLA4/MEF2C/RYR2/ATF3/CYP1B1/LMNA/DUSP1/SQSTM1/BAG3/TNF/DAPK1/TNFAIP8/CFLAR/TNFSF10/ARHGAP10/ZEB2/STAT5A

11.0.14 Cluster 13 - Top Pathways (p.adjust < 0.05, fallback 0.25)

Cluster 13 top pathways
database Description GeneRatio p.adjust Count geneID
HALLMARK_INTERFERON_ALPHA_RESPONSE Hallmark HALLMARK_INTERFERON_ALPHA_RESPONSE 18/55 7.80e-11 18 IFIT2/OASL/IFIT3/CXCL10/IFI44/DHX58/IFIH1/ISG15/DDX60/PARP14/HERC6/EPSTI1/USP18/ISG20/OAS1/WARS1/RTP4/TENT5A
HALLMARK_INTERFERON_GAMMA_RESPONSE Hallmark HALLMARK_INTERFERON_GAMMA_RESPONSE 23/55 5.03e-10 23 IFIT2/OASL/IFIT3/CXCL10/IFIT1/IFI44/DHX58/IFIH1/ISG15/CCL5/PELI1/DDX60/PARP14/HERC6/EPSTI1/OAS2/USP18/ISG20/WARS1/RTP4/TNFAIP3/APOL6/MT2A
GO:0051607 GO_BP defense response to virus 19/88 5.08e-08 19 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/DHX58/PMAIP1/IFIH1/ISG15/ZC3HAV1/DDX60/AIM2/OAS2/USP18/ISG20/OAS1/RTP4/TNFAIP3
GO:0009607 GO_BP response to biotic stimulus 40/88 5.35e-08 40 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/NCF2/ARG2/CCL3/IFI44/DHX58/PMAIP1/IFIH1/IL1A/ISG15/PLCG2/CCL5/PELI1/ZC3HAV1/DDX60/PARP14/AIM2/HERC6/OAS2/USP18/PRKCE/ISG20/RGS1/OAS1/CCR7/SPIRE1/RTP4/TENT5A/NFKBIZ/KYNU/LTA/ABI3/TNFAIP3/RNF19B/IL4I1
GO:0009615 GO_BP response to virus 21/88 8.87e-08 21 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/IFI44/DHX58/PMAIP1/IFIH1/ISG15/CCL5/ZC3HAV1/DDX60/AIM2/OAS2/USP18/ISG20/OAS1/RTP4/TNFAIP3
GO:0045087 GO_BP innate immune response 29/88 1.55e-07 29 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/NCF2/ARG2/CCL3/DHX58/IFIH1/ISG15/PLCG2/CCL5/PELI1/ZC3HAV1/DDX60/PARP14/AIM2/OAS2/USP18/PRKCE/ISG20/OAS1/SPIRE1/NFKBIZ/KYNU/TNFAIP3/RNF19B
GO:0043207 GO_BP response to external biotic stimulus 38/88 1.75e-07 38 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/NCF2/ARG2/CCL3/IFI44/DHX58/PMAIP1/IFIH1/IL1A/ISG15/PLCG2/CCL5/PELI1/ZC3HAV1/DDX60/PARP14/AIM2/HERC6/OAS2/USP18/PRKCE/ISG20/RGS1/OAS1/CCR7/SPIRE1/RTP4/TENT5A/NFKBIZ/KYNU/LTA/TNFAIP3/RNF19B
GO:0051707 GO_BP response to other organism 38/88 1.75e-07 38 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/NCF2/ARG2/CCL3/IFI44/DHX58/PMAIP1/IFIH1/IL1A/ISG15/PLCG2/CCL5/PELI1/ZC3HAV1/DDX60/PARP14/AIM2/HERC6/OAS2/USP18/PRKCE/ISG20/RGS1/OAS1/CCR7/SPIRE1/RTP4/TENT5A/NFKBIZ/KYNU/LTA/TNFAIP3/RNF19B
GO:0140546 GO_BP defense response to symbiont 29/88 5.99e-07 29 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/NCF2/ARG2/CCL3/DHX58/IFIH1/ISG15/PLCG2/CCL5/PELI1/ZC3HAV1/DDX60/PARP14/AIM2/OAS2/USP18/PRKCE/ISG20/OAS1/SPIRE1/NFKBIZ/KYNU/TNFAIP3/RNF19B
GO:0098542 GO_BP defense response to other organism 29/88 6.77e-07 29 IFIT2/OASL/IFIT3/CXCL10/IFIT1/HERC5/NCF2/ARG2/CCL3/DHX58/IFIH1/ISG15/PLCG2/CCL5/PELI1/ZC3HAV1/DDX60/PARP14/AIM2/OAS2/USP18/PRKCE/ISG20/OAS1/SPIRE1/NFKBIZ/KYNU/TNFAIP3/RNF19B

NA

12 Step 12. Cross-Check: Proposed Name vs Top Enriched Terms

proposed_names <- c(
  "0" = "MHC-II high aberrant state", "1" = "NK-like cytotoxic", "2" = "Th2-like",
  "3" = "Naive/CD4 reference", "4" = "Migratory/Adhesion state", "5" = "Stem-like",
  "6" = "Th2-like (Activated)", "7" = "Cycling (G2/M)", "8" = "Metabolically reprogrammed",
  "9" = "GZMA-cytotoxic", "10" = "Central memory/CD4 reference", "11" = "Pro-inflammatory",
  "12" = "GZMB-high inflammatory", "13" = "IFN stimulated"
)

validation_table <- all_long_ora %>%
  dplyr::filter(p.adjust < 0.05) %>%
  dplyr::group_by(cluster) %>%
  dplyr::arrange(p.adjust) %>%
  dplyr::slice_head(n = 3) %>%
  dplyr::summarise(Top3_Terms = paste0(Description, " [", database, "] (p.adj=",
                                  formatC(p.adjust, format = "e", digits = 2), ")",
                                  collapse = "; "),
             .groups = "drop") %>%
  dplyr::mutate(Proposed_Name = proposed_names[as.character(cluster)]) %>%
  dplyr::select(cluster, Proposed_Name, Top3_Terms)

knitr::kable(validation_table, caption = "Proposed name vs. top ORA-enriched terms (top100 markers, all databases)")
Proposed name vs. top ORA-enriched terms (top100 markers, all databases)
cluster Proposed_Name Top3_Terms
0 MHC-II high aberrant state Tuberculosis [KEGG] (p.adj=3.91e-02); Viral myocarditis [KEGG] (p.adj=3.91e-02)
1 NK-like cytotoxic Natural killer cell mediated cytotoxicity [KEGG] (p.adj=5.00e-02)
11 Pro-inflammatory HALLMARK_EPITHELIAL_MESENCHYMAL_TRANSITION [Hallmark] (p.adj=5.68e-03); response to light stimulus [GO_BP] (p.adj=7.82e-03); Degradation of the extracellular matrix [Reactome] (p.adj=2.67e-02)
12 GZMB-high inflammatory HALLMARK_TNFA_SIGNALING_VIA_NFKB [Hallmark] (p.adj=3.02e-06); extrinsic apoptotic signaling pathway via death domain receptors [GO_BP] (p.adj=2.28e-02); negative regulation of extrinsic apoptotic signaling pathway [GO_BP] (p.adj=2.28e-02)
13 IFN stimulated HALLMARK_INTERFERON_ALPHA_RESPONSE [Hallmark] (p.adj=7.80e-11); HALLMARK_INTERFERON_GAMMA_RESPONSE [Hallmark] (p.adj=5.03e-10); defense response to virus [GO_BP] (p.adj=5.08e-08)
5 Stem-like Developmental Biology [Reactome] (p.adj=4.91e-02)
7 Cycling (G2/M) chromosome segregation [GO_BP] (p.adj=1.08e-32); mitotic cell cycle process [GO_BP] (p.adj=4.90e-29); nuclear chromosome segregation [GO_BP] (p.adj=2.56e-28)

13 Step 13. Notes

  • This script uses over-representation analysis (ORA) – the standard method used in single-cell RNA-seq workflows to validate/name clusters from marker gene lists.
  • Background universe = all genes appearing anywhere in the marker table across all clusters.
  • Four databases tested per cluster: GO:BP (enrichGO), KEGG (enrichKEGG), Reactome (enrichPathway), Hallmark (enricher with msigdbr Hallmark sets).
  • Dotplots follow standard clusterProfiler::dotplot() convention: x-axis = GeneRatio, size = gene count, color = p.adjust.
  • Step 11 adds a readable results table per cluster (not just a plot), listing top pathway name, database, GeneRatio, p.adjust, gene count, and the actual overlapping gene IDs.
  • If a cluster shows “No enriched pathways found,” this is a genuine result – the top100 genes don’t strongly match any curated pathway/GO term, common for transitional or poorly characterized states.
LS0tCnRpdGxlOiAiT3Zlci1SZXByZXNlbnRhdGlvbiBBbmFseXNpcyAoT1JBKSBvbiBUb3AxMDAgTWFya2VyIEdlbmVzIHBlciBDbHVzdGVyIgpzdWJ0aXRsZTogIkdPOkJQICsgS0VHRyArIFJlYWN0b21lICsgSGFsbG1hcmssIHdpdGggcGVyLWNsdXN0ZXIgZG90cGxvdHMiCmF1dGhvcjogIk5hc2lyIE1haG1vb2QgQWJiYXNpIgpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclQiAlZCwgJVknKWAiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgbnVtYmVyX3NlY3Rpb25zOiB0cnVlCiAgICB0b2M6IHRydWUKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiB0cnVlCiAgICB0aGVtZTogam91cm5hbAotLS0KCgoKIyBTdGVwIDEuIExvYWQgTGlicmFyaWVzCgpgYGB7ciBsaWJyYXJpZXN9CmxpYnJhcnkocmVhZHhsKQpsaWJyYXJ5KGNsdXN0ZXJQcm9maWxlcikKbGlicmFyeShvcmcuSHMuZWcuZGIpCmxpYnJhcnkoUmVhY3RvbWVQQSkKbGlicmFyeShlbnJpY2hwbG90KQpsaWJyYXJ5KG1zaWdkYnIpCmxpYnJhcnkob3Blbnhsc3gpCmxpYnJhcnkoc3RyaW5ncikKbGlicmFyeShwdXJycikKbGlicmFyeSh0aWJibGUpCmxpYnJhcnkodGlkeXIpCmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShkcGx5cikgICAjIGxvYWQgTEFTVCBzbyBkcGx5ciB2ZXJicyB3aW4gdGhlIG1hc2tpbmcgY29uZmxpY3QKYGBgCgojIFN0ZXAgMi4gTG9hZCBUb3AxMDAgTWFya2VyIFRhYmxlCgpgYGB7ciBsb2FkLWRhdGF9Cm1hcmtlcnMgPC0gcmVhZF9leGNlbCgiLi4vU3VwcGxlbWVudGFyeV9UYWJsZV9TNi54bHN4IikgJT4lCiAgcmVuYW1lX3dpdGgodG9sb3dlcikgJT4lCiAgbXV0YXRlKGNsdXN0ZXIgPSBhcy5jaGFyYWN0ZXIoY2x1c3RlcikpCgojIEZpeCBrbm93biBvdXRkYXRlZC9yZW5hbWVkIGdlbmUgc3ltYm9scwpzeW1ib2xfdXBkYXRlcyA8LSBjKCJRQVJTIiA9ICJRQVJTMSIsICJDQVJTIiA9ICJDQVJTMSIsICJXQVJTIiA9ICJXQVJTMSIpCm1hcmtlcnMgPC0gbWFya2VycyAlPiUKICBtdXRhdGUoZ2VuZSA9IGlmZWxzZShnZW5lICVpbiUgbmFtZXMoc3ltYm9sX3VwZGF0ZXMpLCBzeW1ib2xfdXBkYXRlc1tnZW5lXSwgZ2VuZSkpICU+JQogIGZpbHRlcihnZW5lICE9ICI0NjA4My4wIikgICAjIHJlbW92ZSBzcHJlYWRzaGVldCBhcnRpZmFjdAoKY2x1c3RlcnNfbGlzdCA8LSBhcy5jaGFyYWN0ZXIoc29ydChhcy5udW1lcmljKHVuaXF1ZShtYXJrZXJzJGNsdXN0ZXIpKSkpCmNhdCgiQ2x1c3RlcnMgZm91bmQgKG51bWVyaWMgb3JkZXIpOiIsIHBhc3RlKGNsdXN0ZXJzX2xpc3QsIGNvbGxhcHNlID0gIiwgIiksICJcbiIpCgpwcmludChjb2xuYW1lcyhtYXJrZXJzKSkKYGBgCgojIFN0ZXAgMy4gQnVpbGQgQmFja2dyb3VuZCBVbml2ZXJzZSBhbmQgUGVyLUNsdXN0ZXIgR2VuZSBMaXN0cwoKYGBge3IgYmFja2dyb3VuZC11bml2ZXJzZX0KIyBCYWNrZ3JvdW5kID0gYWxsIGdlbmVzIHRoYXQgYXBwZWFyIGFueXdoZXJlIGluIHRoZSBtYXJrZXIgdGFibGUgKGFsbCBjbHVzdGVycyBjb21iaW5lZCkKYmFja2dyb3VuZF9nZW5lcyA8LSB1bmlxdWUobWFya2VycyRnZW5lKQpjYXQoIkJhY2tncm91bmQgdW5pdmVyc2Ugc2l6ZToiLCBsZW5ndGgoYmFja2dyb3VuZF9nZW5lcyksICJnZW5lc1xuIikKCiMgTWFwIFNZTUJPTCAtPiBFTlRSRVpJRCAobmVlZGVkIGZvciBlbnJpY2hLRUdHIGFuZCBSZWFjdG9tZVBBKQpnZW5lX21hcCA8LSBiaXRyKGJhY2tncm91bmRfZ2VuZXMsIGZyb21UeXBlID0gIlNZTUJPTCIsIHRvVHlwZSA9ICJFTlRSRVpJRCIsCiAgICAgICAgICAgICAgICAgIE9yZ0RiID0gb3JnLkhzLmVnLmRiLCBkcm9wID0gVFJVRSkKCmJhY2tncm91bmRfZW50cmV6IDwtIHVuaXF1ZShnZW5lX21hcCRFTlRSRVpJRCkKCmdldF9jbHVzdGVyX2dlbmVzIDwtIGZ1bmN0aW9uKGNsdXN0ZXJfaWQsIG1hcmtlcl9kZiwgdG9wX24gPSAxMDApIHsKICBtYXJrZXJfZGYgJT4lCiAgICBmaWx0ZXIoY2x1c3RlciA9PSBjbHVzdGVyX2lkKSAlPiUKICAgIGRpc3RpbmN0KGdlbmUsIGF2Z19sb2cyZmMpICU+JQogICAgYXJyYW5nZShkZXNjKGF2Z19sb2cyZmMpKSAlPiUKICAgIHNsaWNlX2hlYWQobiA9IHRvcF9uKSAlPiUKICAgIHB1bGwoZ2VuZSkKfQoKY2x1c3Rlcl9nZW5lX2xpc3RzIDwtIHNldE5hbWVzKAogIGxhcHBseShjbHVzdGVyc19saXN0LCBnZXRfY2x1c3Rlcl9nZW5lcywgbWFya2VyX2RmID0gbWFya2VycyksCiAgY2x1c3RlcnNfbGlzdAopCgpzYXBwbHkoY2x1c3Rlcl9nZW5lX2xpc3RzLCBsZW5ndGgpCmBgYAoKIyBTdGVwIDQuIExvYWQgSGFsbG1hcmsgR2VuZSBTZXRzIChmb3IgZW5yaWNoZXIoKSkKCmBgYHtyIGhhbGxtYXJrLXNldHN9CmhhbGxtYXJrX3NldHMgPC0gbXNpZ2RicihzcGVjaWVzID0gIkhvbW8gc2FwaWVucyIsIGNvbGxlY3Rpb24gPSAiSCIpICU+JQogIGRpc3RpbmN0KGdzX25hbWUsIGdlbmVfc3ltYm9sKQpgYGAKCiMgU3RlcCA1LiBSdW4gT1JBIGZvciBFYWNoIENsdXN0ZXIgKEdPOkJQLCBLRUdHLCBSZWFjdG9tZSwgSGFsbG1hcmspCgpgYGB7ciBydW4tb3JhfQphbGxfcmVzdWx0cyA8LSBsaXN0KCkKCmZvciAoY2wgaW4gY2x1c3RlcnNfbGlzdCkgewogIG1lc3NhZ2UoIlJ1bm5pbmcgT1JBIGZvciBjbHVzdGVyICIsIGNsKQoKICBnZW5lc19zeW1ib2wgPC0gY2x1c3Rlcl9nZW5lX2xpc3RzW1tjbF1dCiAgZ2VuZXNfZW50cmV6IDwtIGdlbmVfbWFwJEVOVFJFWklEW2dlbmVfbWFwJFNZTUJPTCAlaW4lIGdlbmVzX3N5bWJvbF0KCiAgcmVzX2xpc3QgPC0gbGlzdCgpCgogICMgR086QlAKICByZXNfbGlzdCRHT19CUCA8LSB0cnlDYXRjaCgKICAgIGVucmljaEdPKGdlbmUgPSBnZW5lc19zeW1ib2wsIHVuaXZlcnNlID0gYmFja2dyb3VuZF9nZW5lcywKICAgICAgICAgICAgIE9yZ0RiID0gb3JnLkhzLmVnLmRiLCBrZXlUeXBlID0gIlNZTUJPTCIsCiAgICAgICAgICAgICBvbnQgPSAiQlAiLCBwQWRqdXN0TWV0aG9kID0gIkJIIiwKICAgICAgICAgICAgIHB2YWx1ZUN1dG9mZiA9IDEsIHF2YWx1ZUN1dG9mZiA9IDEpLAogICAgZXJyb3IgPSBmdW5jdGlvbihlKSB7IG1lc3NhZ2UoIiAgR086QlAgZmFpbGVkOiAiLCBlJG1lc3NhZ2UpOyBOVUxMIH0pCgogICMgS0VHRwogIHJlc19saXN0JEtFR0cgPC0gdHJ5Q2F0Y2goCiAgICBlbnJpY2hLRUdHKGdlbmUgPSBnZW5lc19lbnRyZXosIHVuaXZlcnNlID0gYmFja2dyb3VuZF9lbnRyZXosCiAgICAgICAgICAgICAgIG9yZ2FuaXNtID0gImhzYSIsIHBBZGp1c3RNZXRob2QgPSAiQkgiLAogICAgICAgICAgICAgICBwdmFsdWVDdXRvZmYgPSAxLCBxdmFsdWVDdXRvZmYgPSAxKSwKICAgIGVycm9yID0gZnVuY3Rpb24oZSkgeyBtZXNzYWdlKCIgIEtFR0cgZmFpbGVkOiAiLCBlJG1lc3NhZ2UpOyBOVUxMIH0pCgogICMgUmVhY3RvbWUKICByZXNfbGlzdCRSZWFjdG9tZSA8LSB0cnlDYXRjaCgKICAgIGVucmljaFBhdGh3YXkoZ2VuZSA9IGdlbmVzX2VudHJleiwgdW5pdmVyc2UgPSBiYWNrZ3JvdW5kX2VudHJleiwKICAgICAgICAgICAgICAgICAgIG9yZ2FuaXNtID0gImh1bWFuIiwgcEFkanVzdE1ldGhvZCA9ICJCSCIsCiAgICAgICAgICAgICAgICAgICBwdmFsdWVDdXRvZmYgPSAxLCBxdmFsdWVDdXRvZmYgPSAxLCByZWFkYWJsZSA9IFRSVUUpLAogICAgZXJyb3IgPSBmdW5jdGlvbihlKSB7IG1lc3NhZ2UoIiAgUmVhY3RvbWUgZmFpbGVkOiAiLCBlJG1lc3NhZ2UpOyBOVUxMIH0pCgogICMgSGFsbG1hcmsgKHZpYSBnZW5lcmljIGVucmljaGVyKQogIHJlc19saXN0JEhhbGxtYXJrIDwtIHRyeUNhdGNoKAogICAgZW5yaWNoZXIoZ2VuZSA9IGdlbmVzX3N5bWJvbCwgdW5pdmVyc2UgPSBiYWNrZ3JvdW5kX2dlbmVzLAogICAgICAgICAgICAgVEVSTTJHRU5FID0gaGFsbG1hcmtfc2V0cywgcEFkanVzdE1ldGhvZCA9ICJCSCIsCiAgICAgICAgICAgICBwdmFsdWVDdXRvZmYgPSAxLCBxdmFsdWVDdXRvZmYgPSAxKSwKICAgIGVycm9yID0gZnVuY3Rpb24oZSkgeyBtZXNzYWdlKCIgIEhhbGxtYXJrIGZhaWxlZDogIiwgZSRtZXNzYWdlKTsgTlVMTCB9KQoKICBhbGxfcmVzdWx0c1tbY2xdXSA8LSByZXNfbGlzdAp9CmBgYAoKIyBTdGVwIDYuIEV4cG9ydCBBbGwgRW5yaWNobWVudCBUYWJsZXMgdG8gRXhjZWwKCmBgYHtyIGV4cG9ydC1leGNlbH0Kd2IgPC0gY3JlYXRlV29ya2Jvb2soKQoKZm9yIChjbCBpbiBuYW1lcyhhbGxfcmVzdWx0cykpIHsKICByZXMgPC0gYWxsX3Jlc3VsdHNbW2NsXV0KICBpZiAoaXMubnVsbChyZXMpKSBuZXh0CgogIGZvciAob250IGluIG5hbWVzKHJlcykpIHsKICAgIG9iaiA8LSByZXNbW29udF1dCiAgICBpZiAoaXMubnVsbChvYmopIHx8IG5yb3coYXMuZGF0YS5mcmFtZShvYmopKSA9PSAwKSBuZXh0CgogICAgc2hlZXRfbmFtZSA8LSBzdWJzdHIocGFzdGUwKCJDIiwgY2wsICJfIiwgb250KSwgMSwgMzEpCiAgICBhZGRXb3Jrc2hlZXQod2IsIHNoZWV0X25hbWUpCiAgICB3cml0ZURhdGEod2IsIHNoZWV0X25hbWUsIGFzLmRhdGEuZnJhbWUob2JqKSkKICB9Cn0KCnNhdmVXb3JrYm9vayh3YiwgIkNsdXN0ZXJfT1JBX1Jlc3VsdHNfVG9wMTAwLnhsc3giLCBvdmVyd3JpdGUgPSBUUlVFKQpgYGAKCiMgU3RlcCA3LiBDb21iaW5lIFRvcCBUZXJtcyBwZXIgQ2x1c3RlciBpbnRvIE9uZSBMb25nIFRhYmxlCgpgYGB7ciBjb21iaW5lLWxvbmd9CmJ1aWxkX2xvbmdfb3JhIDwtIGZ1bmN0aW9uKGFsbF9yZXN1bHRzKSB7CiAgcHVycnI6Om1hcF9kZnIobmFtZXMoYWxsX3Jlc3VsdHMpLCBmdW5jdGlvbihjbCkgewogICAgcmVzIDwtIGFsbF9yZXN1bHRzW1tjbF1dCiAgICBwdXJycjo6bWFwX2RmcihuYW1lcyhyZXMpLCBmdW5jdGlvbihkYikgewogICAgICBvYmogPC0gcmVzW1tkYl1dCiAgICAgIGlmIChpcy5udWxsKG9iaikpIHJldHVybihOVUxMKQogICAgICBkZiA8LSBhcy5kYXRhLmZyYW1lKG9iaikKICAgICAgaWYgKG5yb3coZGYpID09IDApIHJldHVybihOVUxMKQogICAgICBkZiA8LSBhcy5kYXRhLmZyYW1lKGRmKSAgICMgc3RyaXAgYW55IGxpbmdlcmluZyBTNC90aWJibGUgc3ViY2xhc3MKICAgICAgZGYkY2x1c3RlciA8LSBjbAogICAgICBkZiRkYXRhYmFzZSA8LSBkYgogICAgICBkcGx5cjo6c2VsZWN0KGRmLCBjbHVzdGVyLCBkYXRhYmFzZSwgSUQsIERlc2NyaXB0aW9uLCBHZW5lUmF0aW8sIEJnUmF0aW8sCiAgICAgICAgICAgICAgICAgICAgIHB2YWx1ZSwgcC5hZGp1c3QsIHF2YWx1ZSwgZ2VuZUlELCBDb3VudCkKICAgIH0pCiAgfSkKfQoKYWxsX2xvbmdfb3JhIDwtIGJ1aWxkX2xvbmdfb3JhKGFsbF9yZXN1bHRzKQp3cml0ZS5jc3YoYWxsX2xvbmdfb3JhLCAiQ2x1c3Rlcl9PUkFfUmVzdWx0c19sb25nLmNzdiIsIHJvdy5uYW1lcyA9IEZBTFNFKQoKY2F0KCJUb3RhbCBlbnJpY2hlZCB0ZXJtcyBhY3Jvc3MgYWxsIGNsdXN0ZXJzL2RhdGFiYXNlczoiLCBucm93KGFsbF9sb25nX29yYSksICJcbiIpCmBgYAoKIyBTdGVwIDguIENoZWNrIEhvdyBNYW55IFNpZ25pZmljYW50IFRlcm1zIHBlciBDbHVzdGVyCgpgYGB7ciBjaGVjay1zaWduaWZpY2FuY2V9CmFsbF9sb25nX29yYSAlPiUKICBncm91cF9ieShjbHVzdGVyKSAlPiUKICBzdW1tYXJpc2Uobl9zaWdfcGFkajA1ID0gc3VtKHAuYWRqdXN0IDwgMC4wNSwgbmEucm0gPSBUUlVFKSwKICAgICAgICAgICAgbl9zaWdfcGFkajI1ID0gc3VtKHAuYWRqdXN0IDwgMC4yNSwgbmEucm0gPSBUUlVFKSwKICAgICAgICAgICAgbl90b3RhbF90ZXJtcyA9IG4oKSwKICAgICAgICAgICAgbWluX3BhZGogPSBzdXBwcmVzc1dhcm5pbmdzKG1pbihwLmFkanVzdCwgbmEucm0gPSBUUlVFKSkpICU+JQogIGFycmFuZ2UoYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoY2x1c3RlcikpKQpgYGAKCiMgU3RlcCA5LiBQYXJzZSBHZW5lUmF0aW8gdG8gTnVtZXJpYyAobmVlZGVkIGZvciBkb3RwbG90IHgtYXhpcykKCmBgYHtyIHBhcnNlLWdlbmVyYXRpb30KcGFyc2VfcmF0aW8gPC0gZnVuY3Rpb24oeCkgewogIHBhcnRzIDwtIHN0cl9zcGxpdCh4LCAiLyIsIHNpbXBsaWZ5ID0gVFJVRSkKICBhcy5udW1lcmljKHBhcnRzWywgMV0pIC8gYXMubnVtZXJpYyhwYXJ0c1ssIDJdKQp9CgphbGxfbG9uZ19vcmEgPC0gYWxsX2xvbmdfb3JhICU+JQogIG11dGF0ZShHZW5lUmF0aW9OdW0gPSBwYXJzZV9yYXRpbyhHZW5lUmF0aW8pKQpgYGAKCiMgU3RlcCAxMC4gRG90cGxvdCBwZXIgQ2x1c3RlciAoR2VuZVJhdGlvIHgsIENvdW50IHNpemUsIHAuYWRqdXN0IGNvbG9yKQoKYGBge3IgZG90cGxvdC1wZXItY2x1c3RlciwgcmVzdWx0cz0nYXNpcycsIGZpZy53aWR0aD05LCBmaWcuaGVpZ2h0PTZ9CnBsb3RfY2x1c3Rlcl9vcmFfZG90cGxvdCA8LSBmdW5jdGlvbihjbHVzdGVyX2lkLCBsb25nX2RmLCBuX3RvcCA9IDEwLCBzaWdfY3V0b2ZmID0gMC4wNSkgewoKICBkZiA8LSBsb25nX2RmICU+JQogICAgZmlsdGVyKGNsdXN0ZXIgPT0gY2x1c3Rlcl9pZCwgcC5hZGp1c3QgPCBzaWdfY3V0b2ZmKSAlPiUKICAgIGFycmFuZ2UocC5hZGp1c3QpICU+JQogICAgc2xpY2VfaGVhZChuID0gbl90b3ApCgogIHVzZWRfY3V0b2ZmIDwtIHNpZ19jdXRvZmYKCiAgIyBGYWxsYmFjayBpZiBub3RoaW5nIHBhc3NlcyAwLjA1CiAgaWYgKG5yb3coZGYpID09IDApIHsKICAgIGRmIDwtIGxvbmdfZGYgJT4lCiAgICAgIGZpbHRlcihjbHVzdGVyID09IGNsdXN0ZXJfaWQsIHAuYWRqdXN0IDwgMC4yNSkgJT4lCiAgICAgIGFycmFuZ2UocC5hZGp1c3QpICU+JQogICAgICBzbGljZV9oZWFkKG4gPSBuX3RvcCkKICAgIHVzZWRfY3V0b2ZmIDwtIDAuMjUKICB9CgogIGlmIChucm93KGRmKSA9PSAwKSB7CiAgICBjYXQoIioqTm8gZW5yaWNoZWQgdGVybXMgcmVhY2hlZCBwLmFkanVzdCA8IDAuMjUgZm9yIGNsdXN0ZXIiLCBjbHVzdGVyX2lkLCAiKipcblxuIikKICAgIHJldHVybihpbnZpc2libGUoTlVMTCkpCiAgfQoKICBkZiA8LSBkZiAlPiUKICAgIG11dGF0ZShEZXNjcmlwdGlvbiA9IHN0cl93cmFwKERlc2NyaXB0aW9uLCB3aWR0aCA9IDQwKSwKICAgICAgICAgICBsYWJlbCA9IHBhc3RlMChEZXNjcmlwdGlvbiwgIiBbIiwgZGF0YWJhc2UsICJdIikpCgogIHAgPC0gZ2dwbG90KGRmLCBhZXMoeCA9IEdlbmVSYXRpb051bSwgeSA9IHJlb3JkZXIobGFiZWwsIEdlbmVSYXRpb051bSksCiAgICAgICAgICAgICAgICAgICAgICAgc2l6ZSA9IENvdW50LCBjb2xvciA9IHAuYWRqdXN0KSkgKwogICAgZ2VvbV9wb2ludCgpICsKICAgIHNjYWxlX2NvbG9yX2dyYWRpZW50KGxvdyA9ICIjQjIxODJCIiwgaGlnaCA9ICIjNDM5M0MzIiwgbmFtZSA9ICJwLmFkanVzdCIpICsKICAgIHNjYWxlX3NpemVfY29udGludW91cyhuYW1lID0gIkdlbmUgY291bnQiLCByYW5nZSA9IGMoMywgOSkpICsKICAgIGxhYnModGl0bGUgPSBwYXN0ZTAoIkNsdXN0ZXIgIiwgY2x1c3Rlcl9pZCwgIiAtIFRvcDEwMCBPUkEgKHAuYWRqdXN0PCIsIHVzZWRfY3V0b2ZmLCAiKSIpLAogICAgICAgICB4ID0gIkdlbmUgUmF0aW8iLCB5ID0gTlVMTCkgKwogICAgdGhlbWVfbWluaW1hbChiYXNlX3NpemUgPSAxMSkgKwogICAgdGhlbWUoYXhpcy50ZXh0LnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDkpKQoKICBwcmludChwKQogIGdnc2F2ZShmaWxlbmFtZSA9IHBhc3RlMCgiY2x1c3RlciIsIGNsdXN0ZXJfaWQsICJfdG9wMTAwX09SQV9kb3RwbG90LnBuZyIpLAogICAgICAgICBwbG90ID0gcCwgd2lkdGggPSA5LCBoZWlnaHQgPSA2LCBkcGkgPSAzMDApCiAgY2F0KCJcblxuIikKfQoKZm9yIChjbCBpbiBjbHVzdGVyc19saXN0KSB7CiAgY2F0KCJcbiMjIENsdXN0ZXIiLCBjbCwgIi0gT1JBIERvdHBsb3RcblxuIikKICBwbG90X2NsdXN0ZXJfb3JhX2RvdHBsb3QoY2wsIGFsbF9sb25nX29yYSkKfQpgYGAKCiMgU3RlcCAxMS4gVGFibGUgb2YgVG9wIFBhdGh3YXlzIHBlciBDbHVzdGVyIChQcmludGVkIEluZGl2aWR1YWxseSkKCmBgYHtyIHBhdGh3YXlzLXRhYmxlLXBlci1jbHVzdGVyLCByZXN1bHRzPSdhc2lzJ30KZm9yIChjbCBpbiBjbHVzdGVyc19saXN0KSB7CiAgY2F0KCJcbiMjIyBDbHVzdGVyIiwgY2wsICItIFRvcCBQYXRod2F5cyAocC5hZGp1c3QgPCAwLjA1LCBmYWxsYmFjayAwLjI1KVxuXG4iKQoKICBkZiA8LSBhbGxfbG9uZ19vcmEgJT4lCiAgICBkcGx5cjo6ZmlsdGVyKGNsdXN0ZXIgPT0gY2wsIHAuYWRqdXN0IDwgMC4wNSkgJT4lCiAgICBkcGx5cjo6YXJyYW5nZShwLmFkanVzdCkKCiAgaWYgKG5yb3coZGYpID09IDApIHsKICAgIGRmIDwtIGFsbF9sb25nX29yYSAlPiUKICAgICAgZHBseXI6OmZpbHRlcihjbHVzdGVyID09IGNsLCBwLmFkanVzdCA8IDAuMjUpICU+JQogICAgICBkcGx5cjo6YXJyYW5nZShwLmFkanVzdCkKICB9CgogIGlmIChucm93KGRmKSA9PSAwKSB7CiAgICBjYXQoIk5vIGVucmljaGVkIHBhdGh3YXlzIGZvdW5kIGZvciB0aGlzIGNsdXN0ZXIuXG5cbiIpCiAgICBuZXh0CiAgfQoKICBkZl9zaG93IDwtIGRmICU+JQogICAgZHBseXI6OnNsaWNlX2hlYWQobiA9IDEwKSAlPiUKICAgIGRwbHlyOjpzZWxlY3QoZGF0YWJhc2UsIERlc2NyaXB0aW9uLCBHZW5lUmF0aW8sIHAuYWRqdXN0LCBDb3VudCwgZ2VuZUlEKSAlPiUKICAgIGRwbHlyOjptdXRhdGUocC5hZGp1c3QgPSBmb3JtYXRDKHAuYWRqdXN0LCBmb3JtYXQgPSAiZSIsIGRpZ2l0cyA9IDIpKQoKICBwcmludChrbml0cjo6a2FibGUoZGZfc2hvdywgY2FwdGlvbiA9IHBhc3RlMCgiQ2x1c3RlciAiLCBjbCwgIiB0b3AgcGF0aHdheXMiKSkpCiAgY2F0KCJcblxuIikKfQoKYGBgCgojIFN0ZXAgMTIuIENyb3NzLUNoZWNrOiBQcm9wb3NlZCBOYW1lIHZzIFRvcCBFbnJpY2hlZCBUZXJtcwoKYGBge3IgYW5ub3RhdGlvbi1jaGVja30KcHJvcG9zZWRfbmFtZXMgPC0gYygKICAiMCIgPSAiTUhDLUlJIGhpZ2ggYWJlcnJhbnQgc3RhdGUiLCAiMSIgPSAiTkstbGlrZSBjeXRvdG94aWMiLCAiMiIgPSAiVGgyLWxpa2UiLAogICIzIiA9ICJOYWl2ZS9DRDQgcmVmZXJlbmNlIiwgIjQiID0gIk1pZ3JhdG9yeS9BZGhlc2lvbiBzdGF0ZSIsICI1IiA9ICJTdGVtLWxpa2UiLAogICI2IiA9ICJUaDItbGlrZSAoQWN0aXZhdGVkKSIsICI3IiA9ICJDeWNsaW5nIChHMi9NKSIsICI4IiA9ICJNZXRhYm9saWNhbGx5IHJlcHJvZ3JhbW1lZCIsCiAgIjkiID0gIkdaTUEtY3l0b3RveGljIiwgIjEwIiA9ICJDZW50cmFsIG1lbW9yeS9DRDQgcmVmZXJlbmNlIiwgIjExIiA9ICJQcm8taW5mbGFtbWF0b3J5IiwKICAiMTIiID0gIkdaTUItaGlnaCBpbmZsYW1tYXRvcnkiLCAiMTMiID0gIklGTiBzdGltdWxhdGVkIgopCgp2YWxpZGF0aW9uX3RhYmxlIDwtIGFsbF9sb25nX29yYSAlPiUKICBkcGx5cjo6ZmlsdGVyKHAuYWRqdXN0IDwgMC4wNSkgJT4lCiAgZHBseXI6Omdyb3VwX2J5KGNsdXN0ZXIpICU+JQogIGRwbHlyOjphcnJhbmdlKHAuYWRqdXN0KSAlPiUKICBkcGx5cjo6c2xpY2VfaGVhZChuID0gMykgJT4lCiAgZHBseXI6OnN1bW1hcmlzZShUb3AzX1Rlcm1zID0gcGFzdGUwKERlc2NyaXB0aW9uLCAiIFsiLCBkYXRhYmFzZSwgIl0gKHAuYWRqPSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmb3JtYXRDKHAuYWRqdXN0LCBmb3JtYXQgPSAiZSIsIGRpZ2l0cyA9IDIpLCAiKSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xsYXBzZSA9ICI7ICIpLAogICAgICAgICAgICAgLmdyb3VwcyA9ICJkcm9wIikgJT4lCiAgZHBseXI6Om11dGF0ZShQcm9wb3NlZF9OYW1lID0gcHJvcG9zZWRfbmFtZXNbYXMuY2hhcmFjdGVyKGNsdXN0ZXIpXSkgJT4lCiAgZHBseXI6OnNlbGVjdChjbHVzdGVyLCBQcm9wb3NlZF9OYW1lLCBUb3AzX1Rlcm1zKQoKa25pdHI6OmthYmxlKHZhbGlkYXRpb25fdGFibGUsIGNhcHRpb24gPSAiUHJvcG9zZWQgbmFtZSB2cy4gdG9wIE9SQS1lbnJpY2hlZCB0ZXJtcyAodG9wMTAwIG1hcmtlcnMsIGFsbCBkYXRhYmFzZXMpIikKYGBgCgojIFN0ZXAgMTMuIE5vdGVzCgotIFRoaXMgc2NyaXB0IHVzZXMgKipvdmVyLXJlcHJlc2VudGF0aW9uIGFuYWx5c2lzIChPUkEpKiogLS0gdGhlIHN0YW5kYXJkIG1ldGhvZCB1c2VkIGluCiAgc2luZ2xlLWNlbGwgUk5BLXNlcSB3b3JrZmxvd3MgdG8gdmFsaWRhdGUvbmFtZSBjbHVzdGVycyBmcm9tIG1hcmtlciBnZW5lIGxpc3RzLgotIEJhY2tncm91bmQgdW5pdmVyc2UgPSBhbGwgZ2VuZXMgYXBwZWFyaW5nIGFueXdoZXJlIGluIHRoZSBtYXJrZXIgdGFibGUgYWNyb3NzIGFsbCBjbHVzdGVycy4KLSBGb3VyIGRhdGFiYXNlcyB0ZXN0ZWQgcGVyIGNsdXN0ZXI6IEdPOkJQIChgZW5yaWNoR09gKSwgS0VHRyAoYGVucmljaEtFR0dgKSwgUmVhY3RvbWUKICAoYGVucmljaFBhdGh3YXlgKSwgSGFsbG1hcmsgKGBlbnJpY2hlcmAgd2l0aCBtc2lnZGJyIEhhbGxtYXJrIHNldHMpLgotIERvdHBsb3RzIGZvbGxvdyBzdGFuZGFyZCBgY2x1c3RlclByb2ZpbGVyOjpkb3RwbG90KClgIGNvbnZlbnRpb246IHgtYXhpcyA9IEdlbmVSYXRpbywKICBzaXplID0gZ2VuZSBjb3VudCwgY29sb3IgPSBwLmFkanVzdC4KLSBTdGVwIDExIGFkZHMgYSByZWFkYWJsZSByZXN1bHRzIHRhYmxlIHBlciBjbHVzdGVyIChub3QganVzdCBhIHBsb3QpLCBsaXN0aW5nIHRvcCBwYXRod2F5CiAgbmFtZSwgZGF0YWJhc2UsIEdlbmVSYXRpbywgcC5hZGp1c3QsIGdlbmUgY291bnQsIGFuZCB0aGUgYWN0dWFsIG92ZXJsYXBwaW5nIGdlbmUgSURzLgotIElmIGEgY2x1c3RlciBzaG93cyAiTm8gZW5yaWNoZWQgcGF0aHdheXMgZm91bmQsIiB0aGlzIGlzIGEgZ2VudWluZSByZXN1bHQgLS0gdGhlIHRvcDEwMAogIGdlbmVzIGRvbid0IHN0cm9uZ2x5IG1hdGNoIGFueSBjdXJhdGVkIHBhdGh3YXkvR08gdGVybSwgY29tbW9uIGZvciB0cmFuc2l0aW9uYWwgb3IKICBwb29ybHkgY2hhcmFjdGVyaXplZCBzdGF0ZXMuCgo=