Install Missing
Packages (run once, outside knit)
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
BiocManager::install(c("fgsea", "org.Hs.eg.db"), update = FALSE, ask = FALSE)
install.packages(c("readxl", "dplyr", "tidyr", "ggplot2", "msigdbr",
"tibble", "stringr"))
Load Libraries
library(readxl)
library(dplyr)
library(tidyr)
library(tibble)
library(fgsea)
library(msigdbr)
library(ggplot2)
library(stringr)
library(purrr)
library(openxlsx)
Load Top100 Marker
Table
markers <- read_excel("../Supplementary_Table_S6.xlsx") %>%
rename_with(tolower) %>%
mutate(cluster = as.character(cluster))
# Fix known outdated/renamed 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")
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
Load Gene Set Databases
(Hallmark + GO:BP)
hallmark_sets <- msigdbr(species = "Homo sapiens", collection = "H") %>%
distinct(gs_name, gene_symbol) %>%
split(x = .$gene_symbol, f = .$gs_name)
go_bp_sets <- msigdbr(species = "Homo sapiens", collection = "C5", subcollection = "GO:BP") %>%
distinct(gs_name, gene_symbol) %>%
split(x = .$gene_symbol, f = .$gs_name)
kegg_df <- msigdbr(species = "Homo sapiens", collection = "C2", subcollection = "CP:KEGG_LEGACY")
if (nrow(kegg_df) == 0) {
kegg_df <- msigdbr(species = "Homo sapiens", collection = "C2", subcollection = "CP:KEGG")
}
kegg_sets <- kegg_df %>%
distinct(gs_name, gene_symbol) %>%
split(x = .$gene_symbol, f = .$gs_name)
reactome_sets <- msigdbr(species = "Homo sapiens", collection = "C2", subcollection = "CP:REACTOME") %>%
distinct(gs_name, gene_symbol) %>%
split(x = .$gene_symbol, f = .$gs_name)
Build Ranked Top100
List per Cluster and Run fgsea
run_top100_fgsea <- function(cluster_id, marker_df, pathway_sets, min_size = 3, max_size = 100) {
sub <- marker_df %>%
filter(cluster == cluster_id) %>%
distinct(gene, avg_log2fc) %>%
group_by(gene) %>%
summarise(avg_log2fc = mean(avg_log2fc), .groups = "drop") %>%
arrange(desc(avg_log2fc))
ranks <- setNames(sub$avg_log2fc, sub$gene)
set.seed(42)
ranks <- ranks + rnorm(length(ranks), mean = 0, sd = 1e-6)
ranks <- sort(ranks, decreasing = TRUE)
res <- fgsea(pathways = pathway_sets,
stats = ranks,
minSize = min_size,
maxSize = max_size,
eps = 0,
scoreType = "pos")
res <- res %>% arrange(padj)
list(ranks = ranks, result = res)
}
fgsea_hallmark <- list()
fgsea_gobp <- list()
fgsea_kegg <- list()
fgsea_reactome <- list()
for (cl in clusters_list) {
message("Running top100 fgsea for cluster ", cl)
fgsea_hallmark[[cl]] <- tryCatch(
run_top100_fgsea(cl, markers, hallmark_sets),
error = function(e) { message(" Hallmark failed: ", e$message); NULL })
fgsea_gobp[[cl]] <- tryCatch(
run_top100_fgsea(cl, markers, go_bp_sets),
error = function(e) { message(" GO:BP failed: ", e$message); NULL })
fgsea_kegg[[cl]] <- tryCatch(
run_top100_fgsea(cl, markers, kegg_sets),
error = function(e) { message(" KEGG failed: ", e$message); NULL })
fgsea_reactome[[cl]] <- tryCatch(
run_top100_fgsea(cl, markers, reactome_sets),
error = function(e) { message(" Reactome failed: ", e$message); NULL })
}
Export Results to
Excel
library(openxlsx)
wb <- createWorkbook()
export_map <- list(Hallmark = fgsea_hallmark, GOBP = fgsea_gobp,
KEGG = fgsea_kegg, Reactome = fgsea_reactome)
for (cl in clusters_list) {
for (db in names(export_map)) {
obj <- export_map[[db]][[cl]]
if (is.null(obj)) next
df <- obj$result %>%
mutate(leadingEdge = sapply(leadingEdge, paste, collapse = ";")) %>%
as.data.frame()
if (nrow(df) == 0) next
sheet_name <- substr(paste0("C", cl, "_", db), 1, 31)
addWorksheet(wb, sheet_name)
writeData(wb, sheet_name, df)
}
}
saveWorkbook(wb, "Cluster_Top100_fgsea_Results.xlsx", overwrite = TRUE)
Combine All Clusters
into One Long Dataframe (for faceted dotplot)
build_long_df <- function(fgsea_list, db_name) {
purrr::map_dfr(names(fgsea_list), function(cl) {
obj <- fgsea_list[[cl]]
if (is.null(obj) || nrow(obj$result) == 0) return(NULL)
obj$result %>%
mutate(cluster = cl, database = db_name) %>%
select(cluster, database, pathway, NES, padj, size)
})
}
library(purrr)
long_hallmark <- build_long_df(fgsea_hallmark, "Hallmark")
long_gobp <- build_long_df(fgsea_gobp, "GOBP")
long_kegg <- build_long_df(fgsea_kegg, "KEGG")
long_reactome <- build_long_df(fgsea_reactome, "Reactome")
all_long <- bind_rows(long_hallmark, long_gobp, long_kegg, long_reactome) %>%
mutate(cluster = factor(cluster, levels = clusters_list))
write.csv(all_long, "Cluster_Top100_fgsea_long.csv", row.names = FALSE)
Dotplot per Cluster
Based on NES (Top Pathways)
plot_cluster_dotplot <- function(cluster_id, long_df, n_top = 10, sig_cutoff = 0.05) {
df <- long_df %>%
filter(cluster == cluster_id, padj < sig_cutoff) %>%
arrange(desc(abs(NES))) %>%
slice_head(n = n_top) %>%
mutate(pathway = str_replace_all(pathway, "_", " "),
pathway = str_wrap(pathway, width = 40))
if (nrow(df) == 0) {
message("No significant pathways for cluster ", cluster_id)
return(NULL)
}
p <- ggplot(df, aes(x = NES, y = reorder(pathway, NES),
size = size, color = -log10(padj))) +
geom_point() +
scale_color_gradient(low = "#F4A582", high = "#B2182B", name = "-log10(padj)") +
scale_size_continuous(name = "Gene set size", range = c(3, 9)) +
geom_vline(xintercept = 0, linetype = "dashed", color = "grey50") +
labs(title = paste0("Cluster ", cluster_id, " - Top100 fgsea (NES-ranked)"),
x = "Normalized Enrichment Score (NES)", y = NULL) +
theme_minimal(base_size = 11) +
theme(axis.text.y = element_text(size = 9))
print(p)
ggsave(filename = paste0("cluster", cluster_id, "_top100_NES_dotplot.png"),
plot = p, width = 9, height = 6, dpi = 300)
}
for (cl in clusters_list) {
cat("\n## Cluster", cl, "- NES Dotplot\n")
plot_cluster_dotplot(cl, all_long)
}
Cluster 0 - NES
Dotplot
Cluster 1 - NES
Dotplot
Cluster 2 - NES
Dotplot
Cluster 3 - NES
Dotplot
Cluster 4 - NES
Dotplot
Cluster 5 - NES
Dotplot
Cluster 6 - NES
Dotplot
Cluster 7 - NES
Dotplot
Cluster 8 - NES
Dotplot
Cluster 9 - NES
Dotplot
Cluster 10 - NES
Dotplot
Cluster 11 - NES
Dotplot
Cluster 12 - NES
Dotplot
Cross-Check: Proposed
Name vs Top NES Pathway
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 %>%
filter(padj < 0.05) %>%
group_by(cluster) %>%
arrange(desc(abs(NES))) %>%
slice_head(n = 3) %>%
summarise(Top3_Pathways_NES = paste0(pathway, " (NES=", round(NES, 2), ")", collapse = "; "),
.groups = "drop") %>%
mutate(Proposed_Name = proposed_names[as.character(cluster)]) %>%
select(cluster, Proposed_Name, Top3_Pathways_NES)
knitr::kable(validation_table, caption = "Proposed name vs. top NES-ranked pathways (top100 fgsea)")
Proposed name vs. top NES-ranked pathways (top100
fgsea)
| cluster |
Proposed_Name |
Top3_Pathways_NES |
| 1 |
NK-like cytotoxic |
GOBP_DEFENSE_RESPONSE_TO_OTHER_ORGANISM (NES=2.83);
REACTOME_IMMUNOREGULATORY_INTERACTIONS_BETWEEN_A_LYMPHOID_AND_A_NON_LYMPHOID_CELL
(NES=2.18); KEGG_CHEMOKINE_SIGNALING_PATHWAY (NES=2.15) |
| 12 |
GZMB-high inflammatory |
GOBP_RESPONSE_TO_TUMOR_NECROSIS_FACTOR (NES=2.77);
GOBP_POSITIVE_REGULATION_OF_LOCOMOTION (NES=2.76);
GOBP_POSITIVE_REGULATION_OF_CHEMOTAXIS (NES=2.64) |
| 13 |
IFN stimulated |
GOBP_ANTIVIRAL_INNATE_IMMUNE_RESPONSE (NES=2.33);
KEGG_RIG_I_LIKE_RECEPTOR_SIGNALING_PATHWAY (NES=2.24);
GOBP_DEFENSE_RESPONSE_TO_OTHER_ORGANISM (NES=2.22) |
Notes
- fgsea was run using only the top100 marker genes per
cluster, ranked by
avg_log2FC, per your request.
This differs from standard fgsea practice (which uses the full ranked
transcriptome), so results should be interpreted as a
directional validation signal rather than a fully
powered enrichment test.
maxSize = 100 ensures no gene set larger than the
ranked list itself is tested, avoiding meaningless overlaps.
- A tiny jitter (
sd = 1e-6) breaks exact ties in
avg_log2FC, required by fgsea’s ranking algorithm.
- Dotplots show the top 10 significant pathways (padj < 0.05) per
cluster, sized by gene set overlap and colored by significance, x-axis
positioned by NES (positive = enriched toward top-ranked/most
upregulated genes in that cluster).
- If a cluster shows “No significant pathways” in the dotplot loop,
this typically means the top100 genes for that cluster are too sparse or
heterogeneous relative to Hallmark/GO:BP
gene set sizes – worth checking with a relaxed
padj cutoff
(e.g., 0.25) before concluding the name lacks support.
LS0tCnRpdGxlOiAiZmdzZWEgb24gVG9wMTAwIE1hcmtlciBHZW5lcyBwZXIgQ2x1c3RlciIKc3VidGl0bGU6ICJIYWxsbWFyayArIEdPOkJQICsgS0VHRyArIFJlYWN0b21lLCByYW5rZWQgYnkgYXZnX2xvZzJGQywgdmFsaWRhdGVkIHZpYSBORVMgZG90cGxvdHMiCmF1dGhvcjogIk5hc2lyIE1haG1vb2QgQWJiYXNpIgpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclQiAlZCwgJVknKWAiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgbnVtYmVyX3NlY3Rpb25zOiB0cnVlCiAgICB0b2M6IHRydWUKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiB0cnVlCiAgICB0aGVtZTogam91cm5hbAotLS0KCgojIEluc3RhbGwgTWlzc2luZyBQYWNrYWdlcyAocnVuIG9uY2UsIG91dHNpZGUga25pdCkKCmBgYHtyIGluc3RhbGwtcGFja2FnZXMsIGV2YWw9RkFMU0V9CmlmICghcmVxdWlyZU5hbWVzcGFjZSgiQmlvY01hbmFnZXIiLCBxdWlldGx5ID0gVFJVRSkpCiAgaW5zdGFsbC5wYWNrYWdlcygiQmlvY01hbmFnZXIiKQoKQmlvY01hbmFnZXI6Omluc3RhbGwoYygiZmdzZWEiLCAib3JnLkhzLmVnLmRiIiksIHVwZGF0ZSA9IEZBTFNFLCBhc2sgPSBGQUxTRSkKCmluc3RhbGwucGFja2FnZXMoYygicmVhZHhsIiwgImRwbHlyIiwgInRpZHlyIiwgImdncGxvdDIiLCAibXNpZ2RiciIsCiAgICAgICAgICAgICAgICAgICAgInRpYmJsZSIsICJzdHJpbmdyIikpCmBgYAoKIyBMb2FkIExpYnJhcmllcwoKYGBge3IgbGlicmFyaWVzfQpsaWJyYXJ5KHJlYWR4bCkKbGlicmFyeShkcGx5cikKbGlicmFyeSh0aWR5cikKbGlicmFyeSh0aWJibGUpCmxpYnJhcnkoZmdzZWEpCmxpYnJhcnkobXNpZ2RicikKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KHN0cmluZ3IpCmxpYnJhcnkocHVycnIpCmxpYnJhcnkob3Blbnhsc3gpCmBgYAoKIyBMb2FkIFRvcDEwMCBNYXJrZXIgVGFibGUKCmBgYHtyIGxvYWQtZGF0YX0KbWFya2VycyA8LSByZWFkX2V4Y2VsKCIuLi9TdXBwbGVtZW50YXJ5X1RhYmxlX1M2Lnhsc3giKSAlPiUKICByZW5hbWVfd2l0aCh0b2xvd2VyKSAlPiUKICBtdXRhdGUoY2x1c3RlciA9IGFzLmNoYXJhY3RlcihjbHVzdGVyKSkKCiMgRml4IGtub3duIG91dGRhdGVkL3JlbmFtZWQgc3ltYm9scwpzeW1ib2xfdXBkYXRlcyA8LSBjKCJRQVJTIiA9ICJRQVJTMSIsICJDQVJTIiA9ICJDQVJTMSIsICJXQVJTIiA9ICJXQVJTMSIpCm1hcmtlcnMgPC0gbWFya2VycyAlPiUKICBtdXRhdGUoZ2VuZSA9IGlmZWxzZShnZW5lICVpbiUgbmFtZXMoc3ltYm9sX3VwZGF0ZXMpLCBzeW1ib2xfdXBkYXRlc1tnZW5lXSwgZ2VuZSkpICU+JQogIGZpbHRlcihnZW5lICE9ICI0NjA4My4wIikKCmNsdXN0ZXJzX2xpc3QgPC0gYXMuY2hhcmFjdGVyKHNvcnQoYXMubnVtZXJpYyh1bmlxdWUobWFya2VycyRjbHVzdGVyKSkpKQpjYXQoIkNsdXN0ZXJzIGZvdW5kIChudW1lcmljIG9yZGVyKToiLCBwYXN0ZShjbHVzdGVyc19saXN0LCBjb2xsYXBzZSA9ICIsICIpLCAiXG4iKQpgYGAKCiMgTG9hZCBHZW5lIFNldCBEYXRhYmFzZXMgKEhhbGxtYXJrICsgR086QlApCgpgYGB7ciBnZW5lc2V0c30KaGFsbG1hcmtfc2V0cyA8LSBtc2lnZGJyKHNwZWNpZXMgPSAiSG9tbyBzYXBpZW5zIiwgY29sbGVjdGlvbiA9ICJIIikgJT4lCiAgZGlzdGluY3QoZ3NfbmFtZSwgZ2VuZV9zeW1ib2wpICU+JQogIHNwbGl0KHggPSAuJGdlbmVfc3ltYm9sLCBmID0gLiRnc19uYW1lKQoKZ29fYnBfc2V0cyA8LSBtc2lnZGJyKHNwZWNpZXMgPSAiSG9tbyBzYXBpZW5zIiwgY29sbGVjdGlvbiA9ICJDNSIsIHN1YmNvbGxlY3Rpb24gPSAiR086QlAiKSAlPiUKICBkaXN0aW5jdChnc19uYW1lLCBnZW5lX3N5bWJvbCkgJT4lCiAgc3BsaXQoeCA9IC4kZ2VuZV9zeW1ib2wsIGYgPSAuJGdzX25hbWUpCgprZWdnX2RmIDwtIG1zaWdkYnIoc3BlY2llcyA9ICJIb21vIHNhcGllbnMiLCBjb2xsZWN0aW9uID0gIkMyIiwgc3ViY29sbGVjdGlvbiA9ICJDUDpLRUdHX0xFR0FDWSIpCmlmIChucm93KGtlZ2dfZGYpID09IDApIHsKICBrZWdnX2RmIDwtIG1zaWdkYnIoc3BlY2llcyA9ICJIb21vIHNhcGllbnMiLCBjb2xsZWN0aW9uID0gIkMyIiwgc3ViY29sbGVjdGlvbiA9ICJDUDpLRUdHIikKfQprZWdnX3NldHMgPC0ga2VnZ19kZiAlPiUKICBkaXN0aW5jdChnc19uYW1lLCBnZW5lX3N5bWJvbCkgJT4lCiAgc3BsaXQoeCA9IC4kZ2VuZV9zeW1ib2wsIGYgPSAuJGdzX25hbWUpCgpyZWFjdG9tZV9zZXRzIDwtIG1zaWdkYnIoc3BlY2llcyA9ICJIb21vIHNhcGllbnMiLCBjb2xsZWN0aW9uID0gIkMyIiwgc3ViY29sbGVjdGlvbiA9ICJDUDpSRUFDVE9NRSIpICU+JQogIGRpc3RpbmN0KGdzX25hbWUsIGdlbmVfc3ltYm9sKSAlPiUKICBzcGxpdCh4ID0gLiRnZW5lX3N5bWJvbCwgZiA9IC4kZ3NfbmFtZSkKYGBgCgojIEJ1aWxkIFJhbmtlZCBUb3AxMDAgTGlzdCBwZXIgQ2x1c3RlciBhbmQgUnVuIGZnc2VhCgpgYGB7ciBmZ3NlYS1mdW5jdGlvbn0KcnVuX3RvcDEwMF9mZ3NlYSA8LSBmdW5jdGlvbihjbHVzdGVyX2lkLCBtYXJrZXJfZGYsIHBhdGh3YXlfc2V0cywgbWluX3NpemUgPSAzLCBtYXhfc2l6ZSA9IDEwMCkgewoKICBzdWIgPC0gbWFya2VyX2RmICU+JQogICAgZmlsdGVyKGNsdXN0ZXIgPT0gY2x1c3Rlcl9pZCkgJT4lCiAgICBkaXN0aW5jdChnZW5lLCBhdmdfbG9nMmZjKSAlPiUKICAgIGdyb3VwX2J5KGdlbmUpICU+JQogICAgc3VtbWFyaXNlKGF2Z19sb2cyZmMgPSBtZWFuKGF2Z19sb2cyZmMpLCAuZ3JvdXBzID0gImRyb3AiKSAlPiUKICAgIGFycmFuZ2UoZGVzYyhhdmdfbG9nMmZjKSkKCiAgcmFua3MgPC0gc2V0TmFtZXMoc3ViJGF2Z19sb2cyZmMsIHN1YiRnZW5lKQoKICBzZXQuc2VlZCg0MikKICByYW5rcyA8LSByYW5rcyArIHJub3JtKGxlbmd0aChyYW5rcyksIG1lYW4gPSAwLCBzZCA9IDFlLTYpCiAgcmFua3MgPC0gc29ydChyYW5rcywgZGVjcmVhc2luZyA9IFRSVUUpCgogIHJlcyA8LSBmZ3NlYShwYXRod2F5cyA9IHBhdGh3YXlfc2V0cywKICAgICAgICAgICAgICAgc3RhdHMgPSByYW5rcywKICAgICAgICAgICAgICAgbWluU2l6ZSA9IG1pbl9zaXplLAogICAgICAgICAgICAgICBtYXhTaXplID0gbWF4X3NpemUsCiAgICAgICAgICAgICAgIGVwcyA9IDAsCiAgICAgICAgICAgICAgIHNjb3JlVHlwZSA9ICJwb3MiKQoKICByZXMgPC0gcmVzICU+JSBhcnJhbmdlKHBhZGopCiAgbGlzdChyYW5rcyA9IHJhbmtzLCByZXN1bHQgPSByZXMpCn0KYGBgCgpgYGB7ciBydW4tYWxsLWZnc2VhfQpmZ3NlYV9oYWxsbWFyayA8LSBsaXN0KCkKZmdzZWFfZ29icCA8LSBsaXN0KCkKZmdzZWFfa2VnZyA8LSBsaXN0KCkKZmdzZWFfcmVhY3RvbWUgPC0gbGlzdCgpCgpmb3IgKGNsIGluIGNsdXN0ZXJzX2xpc3QpIHsKICBtZXNzYWdlKCJSdW5uaW5nIHRvcDEwMCBmZ3NlYSBmb3IgY2x1c3RlciAiLCBjbCkKCiAgZmdzZWFfaGFsbG1hcmtbW2NsXV0gPC0gdHJ5Q2F0Y2goCiAgICBydW5fdG9wMTAwX2Znc2VhKGNsLCBtYXJrZXJzLCBoYWxsbWFya19zZXRzKSwKICAgIGVycm9yID0gZnVuY3Rpb24oZSkgeyBtZXNzYWdlKCIgIEhhbGxtYXJrIGZhaWxlZDogIiwgZSRtZXNzYWdlKTsgTlVMTCB9KQoKICBmZ3NlYV9nb2JwW1tjbF1dIDwtIHRyeUNhdGNoKAogICAgcnVuX3RvcDEwMF9mZ3NlYShjbCwgbWFya2VycywgZ29fYnBfc2V0cyksCiAgICBlcnJvciA9IGZ1bmN0aW9uKGUpIHsgbWVzc2FnZSgiICBHTzpCUCBmYWlsZWQ6ICIsIGUkbWVzc2FnZSk7IE5VTEwgfSkKCiAgZmdzZWFfa2VnZ1tbY2xdXSA8LSB0cnlDYXRjaCgKICAgIHJ1bl90b3AxMDBfZmdzZWEoY2wsIG1hcmtlcnMsIGtlZ2dfc2V0cyksCiAgICBlcnJvciA9IGZ1bmN0aW9uKGUpIHsgbWVzc2FnZSgiICBLRUdHIGZhaWxlZDogIiwgZSRtZXNzYWdlKTsgTlVMTCB9KQoKICBmZ3NlYV9yZWFjdG9tZVtbY2xdXSA8LSB0cnlDYXRjaCgKICAgIHJ1bl90b3AxMDBfZmdzZWEoY2wsIG1hcmtlcnMsIHJlYWN0b21lX3NldHMpLAogICAgZXJyb3IgPSBmdW5jdGlvbihlKSB7IG1lc3NhZ2UoIiAgUmVhY3RvbWUgZmFpbGVkOiAiLCBlJG1lc3NhZ2UpOyBOVUxMIH0pCn0KYGBgCgojIEV4cG9ydCBSZXN1bHRzIHRvIEV4Y2VsCgpgYGB7ciBleHBvcnQtcmVzdWx0c30KbGlicmFyeShvcGVueGxzeCkKd2IgPC0gY3JlYXRlV29ya2Jvb2soKQoKZXhwb3J0X21hcCA8LSBsaXN0KEhhbGxtYXJrID0gZmdzZWFfaGFsbG1hcmssIEdPQlAgPSBmZ3NlYV9nb2JwLAogICAgICAgICAgICAgICAgICAgIEtFR0cgPSBmZ3NlYV9rZWdnLCBSZWFjdG9tZSA9IGZnc2VhX3JlYWN0b21lKQoKZm9yIChjbCBpbiBjbHVzdGVyc19saXN0KSB7CiAgZm9yIChkYiBpbiBuYW1lcyhleHBvcnRfbWFwKSkgewogICAgb2JqIDwtIGV4cG9ydF9tYXBbW2RiXV1bW2NsXV0KICAgIGlmIChpcy5udWxsKG9iaikpIG5leHQKCiAgICBkZiA8LSBvYmokcmVzdWx0ICU+JQogICAgICBtdXRhdGUobGVhZGluZ0VkZ2UgPSBzYXBwbHkobGVhZGluZ0VkZ2UsIHBhc3RlLCBjb2xsYXBzZSA9ICI7IikpICU+JQogICAgICBhcy5kYXRhLmZyYW1lKCkKICAgIGlmIChucm93KGRmKSA9PSAwKSBuZXh0CgogICAgc2hlZXRfbmFtZSA8LSBzdWJzdHIocGFzdGUwKCJDIiwgY2wsICJfIiwgZGIpLCAxLCAzMSkKICAgIGFkZFdvcmtzaGVldCh3Yiwgc2hlZXRfbmFtZSkKICAgIHdyaXRlRGF0YSh3Yiwgc2hlZXRfbmFtZSwgZGYpCiAgfQp9CgpzYXZlV29ya2Jvb2sod2IsICJDbHVzdGVyX1RvcDEwMF9mZ3NlYV9SZXN1bHRzLnhsc3giLCBvdmVyd3JpdGUgPSBUUlVFKQpgYGAKCiMgQ29tYmluZSBBbGwgQ2x1c3RlcnMgaW50byBPbmUgTG9uZyBEYXRhZnJhbWUgKGZvciBmYWNldGVkIGRvdHBsb3QpCgpgYGB7ciBjb21iaW5lLXJlc3VsdHN9CmJ1aWxkX2xvbmdfZGYgPC0gZnVuY3Rpb24oZmdzZWFfbGlzdCwgZGJfbmFtZSkgewogIHB1cnJyOjptYXBfZGZyKG5hbWVzKGZnc2VhX2xpc3QpLCBmdW5jdGlvbihjbCkgewogICAgb2JqIDwtIGZnc2VhX2xpc3RbW2NsXV0KICAgIGlmIChpcy5udWxsKG9iaikgfHwgbnJvdyhvYmokcmVzdWx0KSA9PSAwKSByZXR1cm4oTlVMTCkKICAgIG9iaiRyZXN1bHQgJT4lCiAgICAgIG11dGF0ZShjbHVzdGVyID0gY2wsIGRhdGFiYXNlID0gZGJfbmFtZSkgJT4lCiAgICAgIHNlbGVjdChjbHVzdGVyLCBkYXRhYmFzZSwgcGF0aHdheSwgTkVTLCBwYWRqLCBzaXplKQogIH0pCn0KCmxpYnJhcnkocHVycnIpCmxvbmdfaGFsbG1hcmsgPC0gYnVpbGRfbG9uZ19kZihmZ3NlYV9oYWxsbWFyaywgIkhhbGxtYXJrIikKbG9uZ19nb2JwIDwtIGJ1aWxkX2xvbmdfZGYoZmdzZWFfZ29icCwgIkdPQlAiKQpsb25nX2tlZ2cgPC0gYnVpbGRfbG9uZ19kZihmZ3NlYV9rZWdnLCAiS0VHRyIpCmxvbmdfcmVhY3RvbWUgPC0gYnVpbGRfbG9uZ19kZihmZ3NlYV9yZWFjdG9tZSwgIlJlYWN0b21lIikKCmFsbF9sb25nIDwtIGJpbmRfcm93cyhsb25nX2hhbGxtYXJrLCBsb25nX2dvYnAsIGxvbmdfa2VnZywgbG9uZ19yZWFjdG9tZSkgJT4lCiAgbXV0YXRlKGNsdXN0ZXIgPSBmYWN0b3IoY2x1c3RlciwgbGV2ZWxzID0gY2x1c3RlcnNfbGlzdCkpCgp3cml0ZS5jc3YoYWxsX2xvbmcsICJDbHVzdGVyX1RvcDEwMF9mZ3NlYV9sb25nLmNzdiIsIHJvdy5uYW1lcyA9IEZBTFNFKQpgYGAKCiMgRG90cGxvdCBwZXIgQ2x1c3RlciBCYXNlZCBvbiBORVMgKFRvcCBQYXRod2F5cykKCmBgYHtyIGRvdHBsb3QtcGVyLWNsdXN0ZXIsIHJlc3VsdHM9J2FzaXMnLCBmaWcud2lkdGg9OSwgZmlnLmhlaWdodD02fQpwbG90X2NsdXN0ZXJfZG90cGxvdCA8LSBmdW5jdGlvbihjbHVzdGVyX2lkLCBsb25nX2RmLCBuX3RvcCA9IDEwLCBzaWdfY3V0b2ZmID0gMC4wNSkgewoKICBkZiA8LSBsb25nX2RmICU+JQogICAgZmlsdGVyKGNsdXN0ZXIgPT0gY2x1c3Rlcl9pZCwgcGFkaiA8IHNpZ19jdXRvZmYpICU+JQogICAgYXJyYW5nZShkZXNjKGFicyhORVMpKSkgJT4lCiAgICBzbGljZV9oZWFkKG4gPSBuX3RvcCkgJT4lCiAgICBtdXRhdGUocGF0aHdheSA9IHN0cl9yZXBsYWNlX2FsbChwYXRod2F5LCAiXyIsICIgIiksCiAgICAgICAgICAgcGF0aHdheSA9IHN0cl93cmFwKHBhdGh3YXksIHdpZHRoID0gNDApKQoKICBpZiAobnJvdyhkZikgPT0gMCkgewogICAgbWVzc2FnZSgiTm8gc2lnbmlmaWNhbnQgcGF0aHdheXMgZm9yIGNsdXN0ZXIgIiwgY2x1c3Rlcl9pZCkKICAgIHJldHVybihOVUxMKQogIH0KCiAgcCA8LSBnZ3Bsb3QoZGYsIGFlcyh4ID0gTkVTLCB5ID0gcmVvcmRlcihwYXRod2F5LCBORVMpLAogICAgICAgICAgICAgICAgICAgICAgIHNpemUgPSBzaXplLCBjb2xvciA9IC1sb2cxMChwYWRqKSkpICsKICAgIGdlb21fcG9pbnQoKSArCiAgICBzY2FsZV9jb2xvcl9ncmFkaWVudChsb3cgPSAiI0Y0QTU4MiIsIGhpZ2ggPSAiI0IyMTgyQiIsIG5hbWUgPSAiLWxvZzEwKHBhZGopIikgKwogICAgc2NhbGVfc2l6ZV9jb250aW51b3VzKG5hbWUgPSAiR2VuZSBzZXQgc2l6ZSIsIHJhbmdlID0gYygzLCA5KSkgKwogICAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gMCwgbGluZXR5cGUgPSAiZGFzaGVkIiwgY29sb3IgPSAiZ3JleTUwIikgKwogICAgbGFicyh0aXRsZSA9IHBhc3RlMCgiQ2x1c3RlciAiLCBjbHVzdGVyX2lkLCAiIC0gVG9wMTAwIGZnc2VhIChORVMtcmFua2VkKSIpLAogICAgICAgICB4ID0gIk5vcm1hbGl6ZWQgRW5yaWNobWVudCBTY29yZSAoTkVTKSIsIHkgPSBOVUxMKSArCiAgICB0aGVtZV9taW5pbWFsKGJhc2Vfc2l6ZSA9IDExKSArCiAgICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfdGV4dChzaXplID0gOSkpCgogIHByaW50KHApCiAgZ2dzYXZlKGZpbGVuYW1lID0gcGFzdGUwKCJjbHVzdGVyIiwgY2x1c3Rlcl9pZCwgIl90b3AxMDBfTkVTX2RvdHBsb3QucG5nIiksCiAgICAgICAgIHBsb3QgPSBwLCB3aWR0aCA9IDksIGhlaWdodCA9IDYsIGRwaSA9IDMwMCkKfQoKZm9yIChjbCBpbiBjbHVzdGVyc19saXN0KSB7CiAgY2F0KCJcbiMjIENsdXN0ZXIiLCBjbCwgIi0gTkVTIERvdHBsb3RcbiIpCiAgcGxvdF9jbHVzdGVyX2RvdHBsb3QoY2wsIGFsbF9sb25nKQp9CmBgYAoKIyBDcm9zcy1DaGVjazogUHJvcG9zZWQgTmFtZSB2cyBUb3AgTkVTIFBhdGh3YXkKCmBgYHtyIGFubm90YXRpb24tY2hlY2t9CnByb3Bvc2VkX25hbWVzIDwtIGMoCiAgIjAiID0gIk1IQy1JSSBoaWdoIGFiZXJyYW50IHN0YXRlIiwgIjEiID0gIk5LLWxpa2UgY3l0b3RveGljIiwgIjIiID0gIlRoMi1saWtlIiwKICAiMyIgPSAiTmFpdmUvQ0Q0IHJlZmVyZW5jZSIsICI0IiA9ICJNaWdyYXRvcnkvQWRoZXNpb24gc3RhdGUiLCAiNSIgPSAiU3RlbS1saWtlIiwKICAiNiIgPSAiVGgyLWxpa2UgKEFjdGl2YXRlZCkiLCAiNyIgPSAiQ3ljbGluZyAoRzIvTSkiLCAiOCIgPSAiTWV0YWJvbGljYWxseSByZXByb2dyYW1tZWQiLAogICI5IiA9ICJHWk1BLWN5dG90b3hpYyIsICIxMCIgPSAiQ2VudHJhbCBtZW1vcnkvQ0Q0IHJlZmVyZW5jZSIsICIxMSIgPSAiUHJvLWluZmxhbW1hdG9yeSIsCiAgIjEyIiA9ICJHWk1CLWhpZ2ggaW5mbGFtbWF0b3J5IiwgIjEzIiA9ICJJRk4gc3RpbXVsYXRlZCIKKQoKdmFsaWRhdGlvbl90YWJsZSA8LSBhbGxfbG9uZyAlPiUKICBmaWx0ZXIocGFkaiA8IDAuMDUpICU+JQogIGdyb3VwX2J5KGNsdXN0ZXIpICU+JQogIGFycmFuZ2UoZGVzYyhhYnMoTkVTKSkpICU+JQogIHNsaWNlX2hlYWQobiA9IDMpICU+JQogIHN1bW1hcmlzZShUb3AzX1BhdGh3YXlzX05FUyA9IHBhc3RlMChwYXRod2F5LCAiIChORVM9Iiwgcm91bmQoTkVTLCAyKSwgIikiLCBjb2xsYXBzZSA9ICI7ICIpLAogICAgICAgICAgICAuZ3JvdXBzID0gImRyb3AiKSAlPiUKICBtdXRhdGUoUHJvcG9zZWRfTmFtZSA9IHByb3Bvc2VkX25hbWVzW2FzLmNoYXJhY3RlcihjbHVzdGVyKV0pICU+JQogIHNlbGVjdChjbHVzdGVyLCBQcm9wb3NlZF9OYW1lLCBUb3AzX1BhdGh3YXlzX05FUykKCmtuaXRyOjprYWJsZSh2YWxpZGF0aW9uX3RhYmxlLCBjYXB0aW9uID0gIlByb3Bvc2VkIG5hbWUgdnMuIHRvcCBORVMtcmFua2VkIHBhdGh3YXlzICh0b3AxMDAgZmdzZWEpIikKYGBgCgojIE5vdGVzCgotIGZnc2VhIHdhcyBydW4gdXNpbmcgb25seSB0aGUgKip0b3AxMDAgbWFya2VyIGdlbmVzIHBlciBjbHVzdGVyKiosIHJhbmtlZCBieSBgYXZnX2xvZzJGQ2AsCiAgcGVyIHlvdXIgcmVxdWVzdC4gVGhpcyBkaWZmZXJzIGZyb20gc3RhbmRhcmQgZmdzZWEgcHJhY3RpY2UgKHdoaWNoIHVzZXMgdGhlIGZ1bGwgcmFua2VkCiAgdHJhbnNjcmlwdG9tZSksIHNvIHJlc3VsdHMgc2hvdWxkIGJlIGludGVycHJldGVkIGFzIGEgKipkaXJlY3Rpb25hbCB2YWxpZGF0aW9uIHNpZ25hbCoqCiAgcmF0aGVyIHRoYW4gYSBmdWxseSBwb3dlcmVkIGVucmljaG1lbnQgdGVzdC4KLSBgbWF4U2l6ZSA9IDEwMGAgZW5zdXJlcyBubyBnZW5lIHNldCBsYXJnZXIgdGhhbiB0aGUgcmFua2VkIGxpc3QgaXRzZWxmIGlzIHRlc3RlZCwgYXZvaWRpbmcKICBtZWFuaW5nbGVzcyBvdmVybGFwcy4KLSBBIHRpbnkgaml0dGVyIChgc2QgPSAxZS02YCkgYnJlYWtzIGV4YWN0IHRpZXMgaW4gYGF2Z19sb2cyRkNgLCByZXF1aXJlZCBieSBmZ3NlYSdzIHJhbmtpbmcKICBhbGdvcml0aG0uCi0gRG90cGxvdHMgc2hvdyB0aGUgdG9wIDEwIHNpZ25pZmljYW50IHBhdGh3YXlzIChwYWRqIDwgMC4wNSkgcGVyIGNsdXN0ZXIsIHNpemVkIGJ5IGdlbmUgc2V0CiAgb3ZlcmxhcCBhbmQgY29sb3JlZCBieSBzaWduaWZpY2FuY2UsIHgtYXhpcyBwb3NpdGlvbmVkIGJ5IE5FUyAocG9zaXRpdmUgPSBlbnJpY2hlZCB0b3dhcmQKICB0b3AtcmFua2VkL21vc3QgdXByZWd1bGF0ZWQgZ2VuZXMgaW4gdGhhdCBjbHVzdGVyKS4KLSBJZiBhIGNsdXN0ZXIgc2hvd3MgIk5vIHNpZ25pZmljYW50IHBhdGh3YXlzIiBpbiB0aGUgZG90cGxvdCBsb29wLCB0aGlzIHR5cGljYWxseSBtZWFucwogIHRoZSB0b3AxMDAgZ2VuZXMgZm9yIHRoYXQgY2x1c3RlciBhcmUgdG9vIHNwYXJzZSBvciBoZXRlcm9nZW5lb3VzIHJlbGF0aXZlIHRvIEhhbGxtYXJrL0dPOkJQCiAgZ2VuZSBzZXQgc2l6ZXMgLS0gd29ydGggY2hlY2tpbmcgd2l0aCBhIHJlbGF4ZWQgYHBhZGpgIGN1dG9mZiAoZS5nLiwgMC4yNSkgYmVmb3JlIGNvbmNsdWRpbmcKICB0aGUgbmFtZSBsYWNrcyBzdXBwb3J0LgoK