Overview

This R Markdown performs FGSEA-based pathway characterization of malignant CD4+ T-cell clusters using positive markers only (Version A).

Rationale: for cluster annotation, the relevant question is “what biological programs define this cluster?”, not “which pathways are increased or decreased relative to other clusters?”. Using only genes enriched within each cluster (FindAllMarkers(only.pos = TRUE)), ranked by an unsigned statistic combining effect size and significance, keeps FGSEA focused on defining/up-regulated programs and substantially reduces the “unbalanced positive/negative gene-level statistic” warnings that occur when a signed ranking is dominated by one-sided pathways (e.g. interferon-stimulated genes, which are almost universally positive in an IFN-high cluster).

The workflow:

  1. Identify cluster-specific positive markers using Seurat FindAllMarkers(only.pos = TRUE), retaining all tested positive genes (no top-N filtering, since FGSEA requires a full ranked list).
  2. Rank genes per cluster using: \[ Score = avg\_log2FC \times \sqrt{-log10(p_{adj})} \] with an explicit floor on p_val_adj to avoid infinite ranks.
  3. Run FGSEA against Hallmark, Reactome, KEGG, and GO:BP gene sets.
  4. Export enrichment tables including NES, p-value, adjusted p-value, and leading-edge genes – the genes actually driving each enrichment signal.
  5. Sanity-check known clusters (13 = IFN, 7 = G2M) and visualize top pathways.

The resulting evidence chain for cluster naming is: FindAllMarkers(only.pos=TRUE) -> defining marker genes -> FGSEA on those genes -> defining pathways -> leading-edge genes link the pathway back to the specific markers driving it. This is the primary evidence to cite in the manuscript’s cluster annotation table.

1. Load packages

library(Seurat)
library(dplyr)
library(fgsea)
library(msigdbr)
library(ggplot2)
library(tibble)
library(stringr)

set.seed(123)

2. Load Seurat object

All_samples_Merged <- readRDS("../../../1-Seurat_RDS_OBJECT_FINAL/All_samples_Merged_with_Renamed_Clusters_Cell_state-03-12-2025.rds.rds")

All_samples_Merged
DefaultAssay(All_samples_Merged)

Idents(All_samples_Merged) <- "seurat_clusters"

3. Differential expression analysis (positive markers only)

All positive genes are retained for FGSEA – no top-N filtering, since FGSEA requires a full ranked list rather than a short marker set.

markers_pos <- FindAllMarkers(
  object = All_samples_Merged,
  only.pos = TRUE,
  min.pct = 0.1,
  logfc.threshold = 0
)

head(markers_pos)

4. Generate ranking statistic

The ranking combines expression effect size and statistical confidence, without a sign term since only positive markers are retained:

\[ Score = avg\_log2FC \times \sqrt{-log10(p_{adj})} \]

Fix applied: in large single-cell datasets, p_val_adj is frequently exactly 0 due to very large cell numbers, which makes -log10(p_val_adj) = Inf for many genes simultaneously. A numerical floor is applied to p_val_adj before the log transform, preserving relative differences between highly significant genes rather than tying them all to one collapsed maximum value.

p_floor <- min(markers_pos$p_val_adj[markers_pos$p_val_adj > 0], na.rm = TRUE) / 10

markers_pos <- markers_pos %>%
  mutate(
    p_val_adj_floored = ifelse(p_val_adj == 0, p_floor, p_val_adj),
    fgsea_rank = avg_log2FC * sqrt(-log10(p_val_adj_floored))
  )

sum(is.infinite(markers_pos$fgsea_rank))
[1] 0
sum(markers_pos$p_val_adj == 0)
[1] 5175

5. Prepare pathway databases

library(msigdbr)

msigdbr_species <- "Homo sapiens"

# Hallmark
hallmark_sets <- msigdbr(species = msigdbr_species, collection = "H")
hallmark_sets <- split(hallmark_sets$gene_symbol, hallmark_sets$gs_name)

# Reactome
reactome_sets <- msigdbr(species = msigdbr_species, collection = "C2",
                          subcollection = "CP:REACTOME")
reactome_sets <- split(reactome_sets$gene_symbol, reactome_sets$gs_name)

# KEGG (legacy, equivalent to the old "CP:KEGG")
kegg_sets <- msigdbr(species = msigdbr_species, collection = "C2",
                      subcollection = "CP:KEGG_LEGACY")
kegg_sets <- split(kegg_sets$gene_symbol, kegg_sets$gs_name)

# GO Biological Process
go_sets <- msigdbr(species = msigdbr_species, collection = "C5",
                    subcollection = "GO:BP")
go_sets <- split(go_sets$gene_symbol, go_sets$gs_name)

length(hallmark_sets)
[1] 50
length(reactome_sets)
[1] 1839
length(kegg_sets)
[1] 186
length(go_sets)
[1] 7538

Note: if your installed msigdbr version is < 10.0, replace collection = with category = and subcollection = with subcategory = throughout this chunk, and use "CP:KEGG" instead of "CP:KEGG_LEGACY".

6. FGSEA function

run_fgsea_cluster <- function(cluster_id, markers, pathways) {

  message("Running cluster: ", cluster_id)

  cluster_markers <- markers %>% dplyr::filter(cluster == cluster_id)

  ranks <- cluster_markers$fgsea_rank
  names(ranks) <- cluster_markers$gene

  ranks <- ranks[!duplicated(names(ranks))]
  ranks <- ranks[!is.na(ranks) & !is.infinite(ranks)]
  ranks <- sort(ranks, decreasing = TRUE)

  fgsea_result <- fgsea(
    pathways = pathways,
    stats = ranks,
    minSize = 15,
    maxSize = 500,
    eps = 0,
    nPermSimple = 10000
  )

  fgsea_result <- as.data.frame(fgsea_result)

  fgsea_result$leadingEdge_genes <- sapply(
    fgsea_result$leadingEdge,
    function(x) paste(x, collapse = ", ")
  )

  fgsea_result$cluster <- cluster_id
  fgsea_result$n_genes_ranked <- length(ranks)

  return(fgsea_result)
}

7. Define clusters

clusters <- sort(unique(markers_pos$cluster))
clusters
 [1] 0  1  2  3  4  5  6  7  8  9  10 11 12 13
Levels: 0 1 2 3 4 5 6 7 8 9 10 11 12 13

8. Run FGSEA across all four databases

hallmark_results <- bind_rows(lapply(clusters, run_fgsea_cluster,
                                      markers = markers_pos, pathways = hallmark_sets))
hallmark_results$database <- "Hallmark"

reactome_results <- bind_rows(lapply(clusters, run_fgsea_cluster,
                                      markers = markers_pos, pathways = reactome_sets))
reactome_results$database <- "Reactome"

kegg_results <- bind_rows(lapply(clusters, run_fgsea_cluster,
                                  markers = markers_pos, pathways = kegg_sets))
kegg_results$database <- "KEGG"

go_results <- bind_rows(lapply(clusters, run_fgsea_cluster,
                                markers = markers_pos, pathways = go_sets))
go_results$database <- "GO_BP"

9. Combine all databases

all_fgsea_results <- bind_rows(hallmark_results, reactome_results, kegg_results, go_results)

all_fgsea_results <- all_fgsea_results %>%
  dplyr::select(cluster, database, pathway, NES, pval, padj, size,
                n_genes_ranked, leadingEdge_genes) %>%
  dplyr::arrange(cluster, padj)

head(all_fgsea_results)

10. Export complete FGSEA table

write.csv(all_fgsea_results, "FGSEA_PositiveMarkers_All_Pathways_with_LeadingEdge_Genes.csv",
          row.names = FALSE)

11. Significant pathways

significant_fgsea <- all_fgsea_results %>% dplyr::filter(padj < 0.05)
write.csv(significant_fgsea, "FGSEA_PositiveMarkers_Significant_Pathways.csv", row.names = FALSE)

12. Top pathways per cluster

top_pathways <- all_fgsea_results %>%
  dplyr::group_by(cluster) %>%
  dplyr::arrange(padj) %>%
  dplyr::slice_head(n = 10)

write.csv(top_pathways, "FGSEA_PositiveMarkers_Top10_Pathways_Per_Cluster.csv", row.names = FALSE)
top_pathways

13. Sanity check on known clusters

Expected: Cluster 13 should show Hallmark INTERFERON_ALPHA_RESPONSE / INTERFERON_GAMMA_RESPONSE with leading-edge genes IFIT2, OASL, ISG15, IFIT3, MX1; Cluster 7 should show G2M_CHECKPOINT, E2F_TARGETS, MITOTIC_SPINDLE.

hallmark_results %>%
  dplyr::filter(cluster == 13) %>%
  dplyr::arrange(padj) %>%
  dplyr::select(pathway, NES, padj, leadingEdge_genes) %>%
  head(10)
hallmark_results %>%
  dplyr::filter(cluster == 7) %>%
  dplyr::arrange(padj) %>%
  dplyr::select(pathway, NES, padj, leadingEdge_genes) %>%
  head(10)

14. Plot Hallmark enrichment per cluster

plot_data <- hallmark_results %>%
  dplyr::filter(padj < 0.05) %>%
  dplyr::group_by(cluster) %>%
  dplyr::slice_max(order_by = NES, n = 5)

if (nrow(plot_data) > 0) {
  ggplot(plot_data, aes(x = reorder(pathway, NES), y = NES, fill = NES > 0)) +
    geom_col() +
    facet_wrap(~cluster, scales = "free") +
    coord_flip() +
    scale_fill_manual(values = c("TRUE" = "#B2182B", "FALSE" = "#4393C3"), guide = "none") +
    labs(x = NULL, y = "Normalized Enrichment Score (NES)",
         title = "FGSEA on positive markers only (Version A)") +
    theme_classic(base_size = 10)
} else {
  cat("No Hallmark pathways passed padj < 0.05 in any cluster.\n")
}

15. Save R objects

save(markers_pos, all_fgsea_results,
     hallmark_results, reactome_results, kegg_results, go_results,
     file = "FGSEA_SS_Malignant_CD4_PositiveMarkers_results.RData")

16. Notes on using this output for cluster naming justification

  • FGSEA_PositiveMarkers_All_Pathways_with_LeadingEdge_Genes.csv is the primary file to cite from, since it links each pathway’s NES/FDR directly to the specific leading-edge genes driving the signal (e.g. “driven by IFIT2, IFIT3, OASL”).
  • Report NES magnitude alongside FDR, not significance alone: a larger NES means the pathway’s genes are more strongly concentrated toward the top of that cluster’s positive-marker ranking.
  • This positive-marker FGSEA approach and your existing ORA (enrichGO/enrichKEGG/enrichPathway) approach answer closely related but distinct questions and should be reported as complementary: ORA asks “are my top-N marker genes overrepresented in a pathway?”; FGSEA asks “is a pathway’s gene set skewed toward the top of my full positive-marker ranking for this cluster?” Agreement between the two provides stronger support for a cluster name than either method alone.
  • The evidence chain to report per cluster: marker genes (from FindAllMarkers(only.pos=TRUE)) -> defining pathway (from FGSEA) -> leading-edge genes (confirming which markers actually drive the pathway signal). Example: Cluster 13 marker genes IFIT2/OASL/ISG15 -> Hallmark INTERFERON_ALPHA_RESPONSE/Reactome interferon signaling -> annotation “IFN-stimulated malignant state”.

17. Session Info

sessionInfo()
LS0tCnRpdGxlOiAiRkdTRUEgQW5hbHlzaXMgb2YgTWFsaWduYW50IENENCBULWNlbGwgU3RhdGVzIChQb3NpdGl2ZSBNYXJrZXJzIE9ubHkpIGluIFNlemFyeSBTeW5kcm9tZSIKYXV0aG9yOiAiTmFzaXIgTWFobW9vZCBBYmJhc2kiCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKICAgIHRvY19jb2xsYXBzZWQ6IHllcwogICAgdGhlbWU6IGZsYXRseQogICAgaGlnaGxpZ2h0OiB0YW5nbwogIGh0bWxfZG9jdW1lbnQ6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKICAgIGRmX3ByaW50OiBwYWdlZAogIHdvcmRfZG9jdW1lbnQ6CiAgICB0b2M6IHllcwogIHBkZl9kb2N1bWVudDoKICAgIHRvYzogeWVzCi0tLQoKIyBPdmVydmlldwoKVGhpcyBSIE1hcmtkb3duIHBlcmZvcm1zIEZHU0VBLWJhc2VkIHBhdGh3YXkgY2hhcmFjdGVyaXphdGlvbiBvZiBtYWxpZ25hbnQgQ0Q0KwpULWNlbGwgY2x1c3RlcnMgdXNpbmcgKipwb3NpdGl2ZSBtYXJrZXJzIG9ubHkqKiAoVmVyc2lvbiBBKS4KClJhdGlvbmFsZTogZm9yIGNsdXN0ZXIgYW5ub3RhdGlvbiwgdGhlIHJlbGV2YW50IHF1ZXN0aW9uIGlzICJ3aGF0IGJpb2xvZ2ljYWwKcHJvZ3JhbXMgZGVmaW5lIHRoaXMgY2x1c3Rlcj8iLCBub3QgIndoaWNoIHBhdGh3YXlzIGFyZSBpbmNyZWFzZWQgb3IgZGVjcmVhc2VkCnJlbGF0aXZlIHRvIG90aGVyIGNsdXN0ZXJzPyIuIFVzaW5nIG9ubHkgZ2VuZXMgZW5yaWNoZWQgd2l0aGluIGVhY2ggY2x1c3RlcgooYEZpbmRBbGxNYXJrZXJzKG9ubHkucG9zID0gVFJVRSlgKSwgcmFua2VkIGJ5IGFuIHVuc2lnbmVkIHN0YXRpc3RpYyBjb21iaW5pbmcKZWZmZWN0IHNpemUgYW5kIHNpZ25pZmljYW5jZSwga2VlcHMgRkdTRUEgZm9jdXNlZCBvbiBkZWZpbmluZy91cC1yZWd1bGF0ZWQKcHJvZ3JhbXMgYW5kIHN1YnN0YW50aWFsbHkgcmVkdWNlcyB0aGUgInVuYmFsYW5jZWQgcG9zaXRpdmUvbmVnYXRpdmUgZ2VuZS1sZXZlbApzdGF0aXN0aWMiIHdhcm5pbmdzIHRoYXQgb2NjdXIgd2hlbiBhIHNpZ25lZCByYW5raW5nIGlzIGRvbWluYXRlZCBieSBvbmUtc2lkZWQKcGF0aHdheXMgKGUuZy4gaW50ZXJmZXJvbi1zdGltdWxhdGVkIGdlbmVzLCB3aGljaCBhcmUgYWxtb3N0IHVuaXZlcnNhbGx5IHBvc2l0aXZlCmluIGFuIElGTi1oaWdoIGNsdXN0ZXIpLgoKVGhlIHdvcmtmbG93OgoKMS4gSWRlbnRpZnkgY2x1c3Rlci1zcGVjaWZpYyBwb3NpdGl2ZSBtYXJrZXJzIHVzaW5nIFNldXJhdAogICBgRmluZEFsbE1hcmtlcnMob25seS5wb3MgPSBUUlVFKWAsIHJldGFpbmluZyBhbGwgdGVzdGVkIHBvc2l0aXZlIGdlbmVzCiAgIChubyB0b3AtTiBmaWx0ZXJpbmcsIHNpbmNlIEZHU0VBIHJlcXVpcmVzIGEgZnVsbCByYW5rZWQgbGlzdCkuCjIuIFJhbmsgZ2VuZXMgcGVyIGNsdXN0ZXIgdXNpbmc6CiAgIFxbCiAgIFNjb3JlID0gYXZnXF9sb2cyRkMgXHRpbWVzIFxzcXJ0ey1sb2cxMChwX3thZGp9KX0KICAgXF0KICAgd2l0aCBhbiBleHBsaWNpdCBmbG9vciBvbiBgcF92YWxfYWRqYCB0byBhdm9pZCBpbmZpbml0ZSByYW5rcy4KMy4gUnVuIEZHU0VBIGFnYWluc3QgSGFsbG1hcmssIFJlYWN0b21lLCBLRUdHLCBhbmQgR086QlAgZ2VuZSBzZXRzLgo0LiBFeHBvcnQgZW5yaWNobWVudCB0YWJsZXMgaW5jbHVkaW5nIE5FUywgcC12YWx1ZSwgYWRqdXN0ZWQgcC12YWx1ZSwgYW5kCiAgIGxlYWRpbmctZWRnZSBnZW5lcyAtLSB0aGUgZ2VuZXMgYWN0dWFsbHkgZHJpdmluZyBlYWNoIGVucmljaG1lbnQgc2lnbmFsLgo1LiBTYW5pdHktY2hlY2sga25vd24gY2x1c3RlcnMgKDEzID0gSUZOLCA3ID0gRzJNKSBhbmQgdmlzdWFsaXplIHRvcCBwYXRod2F5cy4KClRoZSByZXN1bHRpbmcgZXZpZGVuY2UgY2hhaW4gZm9yIGNsdXN0ZXIgbmFtaW5nIGlzOgpgRmluZEFsbE1hcmtlcnMob25seS5wb3M9VFJVRSlgIC0+IGRlZmluaW5nIG1hcmtlciBnZW5lcyAtPiBGR1NFQSBvbiB0aG9zZSBnZW5lcwotPiBkZWZpbmluZyBwYXRod2F5cyAtPiBsZWFkaW5nLWVkZ2UgZ2VuZXMgbGluayB0aGUgcGF0aHdheSBiYWNrIHRvIHRoZSBzcGVjaWZpYwptYXJrZXJzIGRyaXZpbmcgaXQuIFRoaXMgaXMgdGhlIHByaW1hcnkgZXZpZGVuY2UgdG8gY2l0ZSBpbiB0aGUgbWFudXNjcmlwdCdzCmNsdXN0ZXIgYW5ub3RhdGlvbiB0YWJsZS4KCiMgMS4gTG9hZCBwYWNrYWdlcwoKYGBge3Igc2V0dXAsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkoU2V1cmF0KQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGZnc2VhKQpsaWJyYXJ5KG1zaWdkYnIpCmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeSh0aWJibGUpCmxpYnJhcnkoc3RyaW5ncikKCnNldC5zZWVkKDEyMykKYGBgCgojIDIuIExvYWQgU2V1cmF0IG9iamVjdAoKYGBge3IgbG9hZC1vYmplY3R9CkFsbF9zYW1wbGVzX01lcmdlZCA8LSByZWFkUkRTKCIuLi8uLi8uLi8xLVNldXJhdF9SRFNfT0JKRUNUX0ZJTkFML0FsbF9zYW1wbGVzX01lcmdlZF93aXRoX1JlbmFtZWRfQ2x1c3RlcnNfQ2VsbF9zdGF0ZS0wMy0xMi0yMDI1LnJkcy5yZHMiKQoKQWxsX3NhbXBsZXNfTWVyZ2VkCkRlZmF1bHRBc3NheShBbGxfc2FtcGxlc19NZXJnZWQpCgpJZGVudHMoQWxsX3NhbXBsZXNfTWVyZ2VkKSA8LSAic2V1cmF0X2NsdXN0ZXJzIgoKYGBgCgojIDMuIERpZmZlcmVudGlhbCBleHByZXNzaW9uIGFuYWx5c2lzIChwb3NpdGl2ZSBtYXJrZXJzIG9ubHkpCgpBbGwgcG9zaXRpdmUgZ2VuZXMgYXJlIHJldGFpbmVkIGZvciBGR1NFQSAtLSBubyB0b3AtTiBmaWx0ZXJpbmcsIHNpbmNlIEZHU0VBCnJlcXVpcmVzIGEgZnVsbCByYW5rZWQgbGlzdCByYXRoZXIgdGhhbiBhIHNob3J0IG1hcmtlciBzZXQuCgpgYGB7ciBmaW5kLWFsbC1tYXJrZXJzfQptYXJrZXJzX3BvcyA8LSBGaW5kQWxsTWFya2VycygKICBvYmplY3QgPSBBbGxfc2FtcGxlc19NZXJnZWQsCiAgb25seS5wb3MgPSBUUlVFLAogIG1pbi5wY3QgPSAwLjEsCiAgbG9nZmMudGhyZXNob2xkID0gMAopCgpoZWFkKG1hcmtlcnNfcG9zKQpgYGAKCiMgNC4gR2VuZXJhdGUgcmFua2luZyBzdGF0aXN0aWMKClRoZSByYW5raW5nIGNvbWJpbmVzIGV4cHJlc3Npb24gZWZmZWN0IHNpemUgYW5kIHN0YXRpc3RpY2FsIGNvbmZpZGVuY2UsIHdpdGhvdXQKYSBzaWduIHRlcm0gc2luY2Ugb25seSBwb3NpdGl2ZSBtYXJrZXJzIGFyZSByZXRhaW5lZDoKClxbClNjb3JlID0gYXZnXF9sb2cyRkMgXHRpbWVzIFxzcXJ0ey1sb2cxMChwX3thZGp9KX0KXF0KCioqRml4IGFwcGxpZWQ6KiogaW4gbGFyZ2Ugc2luZ2xlLWNlbGwgZGF0YXNldHMsIGBwX3ZhbF9hZGpgIGlzIGZyZXF1ZW50bHkgZXhhY3RseQowIGR1ZSB0byB2ZXJ5IGxhcmdlIGNlbGwgbnVtYmVycywgd2hpY2ggbWFrZXMgYC1sb2cxMChwX3ZhbF9hZGopID0gSW5mYCBmb3IgbWFueQpnZW5lcyBzaW11bHRhbmVvdXNseS4gQSBudW1lcmljYWwgZmxvb3IgaXMgYXBwbGllZCB0byBgcF92YWxfYWRqYCAqYmVmb3JlKiB0aGUgbG9nCnRyYW5zZm9ybSwgcHJlc2VydmluZyByZWxhdGl2ZSBkaWZmZXJlbmNlcyBiZXR3ZWVuIGhpZ2hseSBzaWduaWZpY2FudCBnZW5lcwpyYXRoZXIgdGhhbiB0eWluZyB0aGVtIGFsbCB0byBvbmUgY29sbGFwc2VkIG1heGltdW0gdmFsdWUuCgpgYGB7ciByYW5raW5nLXN0YXRpc3RpY30KcF9mbG9vciA8LSBtaW4obWFya2Vyc19wb3MkcF92YWxfYWRqW21hcmtlcnNfcG9zJHBfdmFsX2FkaiA+IDBdLCBuYS5ybSA9IFRSVUUpIC8gMTAKCm1hcmtlcnNfcG9zIDwtIG1hcmtlcnNfcG9zICU+JQogIG11dGF0ZSgKICAgIHBfdmFsX2Fkal9mbG9vcmVkID0gaWZlbHNlKHBfdmFsX2FkaiA9PSAwLCBwX2Zsb29yLCBwX3ZhbF9hZGopLAogICAgZmdzZWFfcmFuayA9IGF2Z19sb2cyRkMgKiBzcXJ0KC1sb2cxMChwX3ZhbF9hZGpfZmxvb3JlZCkpCiAgKQoKc3VtKGlzLmluZmluaXRlKG1hcmtlcnNfcG9zJGZnc2VhX3JhbmspKQpzdW0obWFya2Vyc19wb3MkcF92YWxfYWRqID09IDApCmBgYAoKIyA1LiBQcmVwYXJlIHBhdGh3YXkgZGF0YWJhc2VzCgpgYGB7ciBwYXRod2F5LWRhdGFiYXNlc30KbGlicmFyeShtc2lnZGJyKQoKbXNpZ2Ricl9zcGVjaWVzIDwtICJIb21vIHNhcGllbnMiCgojIEhhbGxtYXJrCmhhbGxtYXJrX3NldHMgPC0gbXNpZ2RicihzcGVjaWVzID0gbXNpZ2Ricl9zcGVjaWVzLCBjb2xsZWN0aW9uID0gIkgiKQpoYWxsbWFya19zZXRzIDwtIHNwbGl0KGhhbGxtYXJrX3NldHMkZ2VuZV9zeW1ib2wsIGhhbGxtYXJrX3NldHMkZ3NfbmFtZSkKCiMgUmVhY3RvbWUKcmVhY3RvbWVfc2V0cyA8LSBtc2lnZGJyKHNwZWNpZXMgPSBtc2lnZGJyX3NwZWNpZXMsIGNvbGxlY3Rpb24gPSAiQzIiLAogICAgICAgICAgICAgICAgICAgICAgICAgIHN1YmNvbGxlY3Rpb24gPSAiQ1A6UkVBQ1RPTUUiKQpyZWFjdG9tZV9zZXRzIDwtIHNwbGl0KHJlYWN0b21lX3NldHMkZ2VuZV9zeW1ib2wsIHJlYWN0b21lX3NldHMkZ3NfbmFtZSkKCiMgS0VHRyAobGVnYWN5LCBlcXVpdmFsZW50IHRvIHRoZSBvbGQgIkNQOktFR0ciKQprZWdnX3NldHMgPC0gbXNpZ2RicihzcGVjaWVzID0gbXNpZ2Ricl9zcGVjaWVzLCBjb2xsZWN0aW9uID0gIkMyIiwKICAgICAgICAgICAgICAgICAgICAgIHN1YmNvbGxlY3Rpb24gPSAiQ1A6S0VHR19MRUdBQ1kiKQprZWdnX3NldHMgPC0gc3BsaXQoa2VnZ19zZXRzJGdlbmVfc3ltYm9sLCBrZWdnX3NldHMkZ3NfbmFtZSkKCiMgR08gQmlvbG9naWNhbCBQcm9jZXNzCmdvX3NldHMgPC0gbXNpZ2RicihzcGVjaWVzID0gbXNpZ2Ricl9zcGVjaWVzLCBjb2xsZWN0aW9uID0gIkM1IiwKICAgICAgICAgICAgICAgICAgICBzdWJjb2xsZWN0aW9uID0gIkdPOkJQIikKZ29fc2V0cyA8LSBzcGxpdChnb19zZXRzJGdlbmVfc3ltYm9sLCBnb19zZXRzJGdzX25hbWUpCgpsZW5ndGgoaGFsbG1hcmtfc2V0cykKbGVuZ3RoKHJlYWN0b21lX3NldHMpCmxlbmd0aChrZWdnX3NldHMpCmxlbmd0aChnb19zZXRzKQpgYGAKCk5vdGU6IGlmIHlvdXIgaW5zdGFsbGVkIGBtc2lnZGJyYCB2ZXJzaW9uIGlzIDwgMTAuMCwgcmVwbGFjZSBgY29sbGVjdGlvbiA9YCB3aXRoCmBjYXRlZ29yeSA9YCBhbmQgYHN1YmNvbGxlY3Rpb24gPWAgd2l0aCBgc3ViY2F0ZWdvcnkgPWAgdGhyb3VnaG91dCB0aGlzIGNodW5rLAphbmQgdXNlIGAiQ1A6S0VHRyJgIGluc3RlYWQgb2YgYCJDUDpLRUdHX0xFR0FDWSJgLgoKIyA2LiBGR1NFQSBmdW5jdGlvbgoKYGBge3IgZmdzZWEtZnVuY3Rpb259CnJ1bl9mZ3NlYV9jbHVzdGVyIDwtIGZ1bmN0aW9uKGNsdXN0ZXJfaWQsIG1hcmtlcnMsIHBhdGh3YXlzKSB7CgogIG1lc3NhZ2UoIlJ1bm5pbmcgY2x1c3RlcjogIiwgY2x1c3Rlcl9pZCkKCiAgY2x1c3Rlcl9tYXJrZXJzIDwtIG1hcmtlcnMgJT4lIGRwbHlyOjpmaWx0ZXIoY2x1c3RlciA9PSBjbHVzdGVyX2lkKQoKICByYW5rcyA8LSBjbHVzdGVyX21hcmtlcnMkZmdzZWFfcmFuawogIG5hbWVzKHJhbmtzKSA8LSBjbHVzdGVyX21hcmtlcnMkZ2VuZQoKICByYW5rcyA8LSByYW5rc1shZHVwbGljYXRlZChuYW1lcyhyYW5rcykpXQogIHJhbmtzIDwtIHJhbmtzWyFpcy5uYShyYW5rcykgJiAhaXMuaW5maW5pdGUocmFua3MpXQogIHJhbmtzIDwtIHNvcnQocmFua3MsIGRlY3JlYXNpbmcgPSBUUlVFKQoKICBmZ3NlYV9yZXN1bHQgPC0gZmdzZWEoCiAgICBwYXRod2F5cyA9IHBhdGh3YXlzLAogICAgc3RhdHMgPSByYW5rcywKICAgIG1pblNpemUgPSAxNSwKICAgIG1heFNpemUgPSA1MDAsCiAgICBlcHMgPSAwLAogICAgblBlcm1TaW1wbGUgPSAxMDAwMAogICkKCiAgZmdzZWFfcmVzdWx0IDwtIGFzLmRhdGEuZnJhbWUoZmdzZWFfcmVzdWx0KQoKICBmZ3NlYV9yZXN1bHQkbGVhZGluZ0VkZ2VfZ2VuZXMgPC0gc2FwcGx5KAogICAgZmdzZWFfcmVzdWx0JGxlYWRpbmdFZGdlLAogICAgZnVuY3Rpb24oeCkgcGFzdGUoeCwgY29sbGFwc2UgPSAiLCAiKQogICkKCiAgZmdzZWFfcmVzdWx0JGNsdXN0ZXIgPC0gY2x1c3Rlcl9pZAogIGZnc2VhX3Jlc3VsdCRuX2dlbmVzX3JhbmtlZCA8LSBsZW5ndGgocmFua3MpCgogIHJldHVybihmZ3NlYV9yZXN1bHQpCn0KYGBgCgojIDcuIERlZmluZSBjbHVzdGVycwoKYGBge3IgZGVmaW5lLWNsdXN0ZXJzfQpjbHVzdGVycyA8LSBzb3J0KHVuaXF1ZShtYXJrZXJzX3BvcyRjbHVzdGVyKSkKY2x1c3RlcnMKYGBgCgojIDguIFJ1biBGR1NFQSBhY3Jvc3MgYWxsIGZvdXIgZGF0YWJhc2VzCgpgYGB7ciBydW4tZmdzZWEtYWxsfQpoYWxsbWFya19yZXN1bHRzIDwtIGJpbmRfcm93cyhsYXBwbHkoY2x1c3RlcnMsIHJ1bl9mZ3NlYV9jbHVzdGVyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcmtlcnMgPSBtYXJrZXJzX3BvcywgcGF0aHdheXMgPSBoYWxsbWFya19zZXRzKSkKaGFsbG1hcmtfcmVzdWx0cyRkYXRhYmFzZSA8LSAiSGFsbG1hcmsiCgpyZWFjdG9tZV9yZXN1bHRzIDwtIGJpbmRfcm93cyhsYXBwbHkoY2x1c3RlcnMsIHJ1bl9mZ3NlYV9jbHVzdGVyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcmtlcnMgPSBtYXJrZXJzX3BvcywgcGF0aHdheXMgPSByZWFjdG9tZV9zZXRzKSkKcmVhY3RvbWVfcmVzdWx0cyRkYXRhYmFzZSA8LSAiUmVhY3RvbWUiCgprZWdnX3Jlc3VsdHMgPC0gYmluZF9yb3dzKGxhcHBseShjbHVzdGVycywgcnVuX2Znc2VhX2NsdXN0ZXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXJrZXJzID0gbWFya2Vyc19wb3MsIHBhdGh3YXlzID0ga2VnZ19zZXRzKSkKa2VnZ19yZXN1bHRzJGRhdGFiYXNlIDwtICJLRUdHIgoKZ29fcmVzdWx0cyA8LSBiaW5kX3Jvd3MobGFwcGx5KGNsdXN0ZXJzLCBydW5fZmdzZWFfY2x1c3RlciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXJrZXJzID0gbWFya2Vyc19wb3MsIHBhdGh3YXlzID0gZ29fc2V0cykpCmdvX3Jlc3VsdHMkZGF0YWJhc2UgPC0gIkdPX0JQIgpgYGAKCiMgOS4gQ29tYmluZSBhbGwgZGF0YWJhc2VzCgpgYGB7ciBjb21iaW5lLXJlc3VsdHN9CmFsbF9mZ3NlYV9yZXN1bHRzIDwtIGJpbmRfcm93cyhoYWxsbWFya19yZXN1bHRzLCByZWFjdG9tZV9yZXN1bHRzLCBrZWdnX3Jlc3VsdHMsIGdvX3Jlc3VsdHMpCgphbGxfZmdzZWFfcmVzdWx0cyA8LSBhbGxfZmdzZWFfcmVzdWx0cyAlPiUKICBkcGx5cjo6c2VsZWN0KGNsdXN0ZXIsIGRhdGFiYXNlLCBwYXRod2F5LCBORVMsIHB2YWwsIHBhZGosIHNpemUsCiAgICAgICAgICAgICAgICBuX2dlbmVzX3JhbmtlZCwgbGVhZGluZ0VkZ2VfZ2VuZXMpICU+JQogIGRwbHlyOjphcnJhbmdlKGNsdXN0ZXIsIHBhZGopCgpoZWFkKGFsbF9mZ3NlYV9yZXN1bHRzKQpgYGAKCiMgMTAuIEV4cG9ydCBjb21wbGV0ZSBGR1NFQSB0YWJsZQoKYGBge3IgZXhwb3J0LWFsbH0Kd3JpdGUuY3N2KGFsbF9mZ3NlYV9yZXN1bHRzLCAiRkdTRUFfUG9zaXRpdmVNYXJrZXJzX0FsbF9QYXRod2F5c193aXRoX0xlYWRpbmdFZGdlX0dlbmVzLmNzdiIsCiAgICAgICAgICByb3cubmFtZXMgPSBGQUxTRSkKYGBgCgojIDExLiBTaWduaWZpY2FudCBwYXRod2F5cwoKYGBge3IgZXhwb3J0LXNpZ25pZmljYW50fQpzaWduaWZpY2FudF9mZ3NlYSA8LSBhbGxfZmdzZWFfcmVzdWx0cyAlPiUgZHBseXI6OmZpbHRlcihwYWRqIDwgMC4wNSkKd3JpdGUuY3N2KHNpZ25pZmljYW50X2Znc2VhLCAiRkdTRUFfUG9zaXRpdmVNYXJrZXJzX1NpZ25pZmljYW50X1BhdGh3YXlzLmNzdiIsIHJvdy5uYW1lcyA9IEZBTFNFKQpgYGAKCiMgMTIuIFRvcCBwYXRod2F5cyBwZXIgY2x1c3RlcgoKYGBge3IgdG9wLXBhdGh3YXlzfQp0b3BfcGF0aHdheXMgPC0gYWxsX2Znc2VhX3Jlc3VsdHMgJT4lCiAgZHBseXI6Omdyb3VwX2J5KGNsdXN0ZXIpICU+JQogIGRwbHlyOjphcnJhbmdlKHBhZGopICU+JQogIGRwbHlyOjpzbGljZV9oZWFkKG4gPSAxMCkKCndyaXRlLmNzdih0b3BfcGF0aHdheXMsICJGR1NFQV9Qb3NpdGl2ZU1hcmtlcnNfVG9wMTBfUGF0aHdheXNfUGVyX0NsdXN0ZXIuY3N2Iiwgcm93Lm5hbWVzID0gRkFMU0UpCnRvcF9wYXRod2F5cwpgYGAKCiMgMTMuIFNhbml0eSBjaGVjayBvbiBrbm93biBjbHVzdGVycwoKRXhwZWN0ZWQ6IENsdXN0ZXIgMTMgc2hvdWxkIHNob3cgSGFsbG1hcmsgSU5URVJGRVJPTl9BTFBIQV9SRVNQT05TRSAvCklOVEVSRkVST05fR0FNTUFfUkVTUE9OU0Ugd2l0aCBsZWFkaW5nLWVkZ2UgZ2VuZXMgSUZJVDIsIE9BU0wsIElTRzE1LCBJRklUMywgTVgxOwpDbHVzdGVyIDcgc2hvdWxkIHNob3cgRzJNX0NIRUNLUE9JTlQsIEUyRl9UQVJHRVRTLCBNSVRPVElDX1NQSU5ETEUuCgpgYGB7ciBzYW5pdHktY2hlY2stMTN9CmhhbGxtYXJrX3Jlc3VsdHMgJT4lCiAgZHBseXI6OmZpbHRlcihjbHVzdGVyID09IDEzKSAlPiUKICBkcGx5cjo6YXJyYW5nZShwYWRqKSAlPiUKICBkcGx5cjo6c2VsZWN0KHBhdGh3YXksIE5FUywgcGFkaiwgbGVhZGluZ0VkZ2VfZ2VuZXMpICU+JQogIGhlYWQoMTApCmBgYAoKYGBge3Igc2FuaXR5LWNoZWNrLTd9CmhhbGxtYXJrX3Jlc3VsdHMgJT4lCiAgZHBseXI6OmZpbHRlcihjbHVzdGVyID09IDcpICU+JQogIGRwbHlyOjphcnJhbmdlKHBhZGopICU+JQogIGRwbHlyOjpzZWxlY3QocGF0aHdheSwgTkVTLCBwYWRqLCBsZWFkaW5nRWRnZV9nZW5lcykgJT4lCiAgaGVhZCgxMCkKYGBgCgojIDE0LiBQbG90IEhhbGxtYXJrIGVucmljaG1lbnQgcGVyIGNsdXN0ZXIKCmBgYHtyIHBsb3QtaGFsbG1hcmssIGZpZy53aWR0aD0xOCwgZmlnLmhlaWdodD04fQpwbG90X2RhdGEgPC0gaGFsbG1hcmtfcmVzdWx0cyAlPiUKICBkcGx5cjo6ZmlsdGVyKHBhZGogPCAwLjA1KSAlPiUKICBkcGx5cjo6Z3JvdXBfYnkoY2x1c3RlcikgJT4lCiAgZHBseXI6OnNsaWNlX21heChvcmRlcl9ieSA9IE5FUywgbiA9IDUpCgppZiAobnJvdyhwbG90X2RhdGEpID4gMCkgewogIGdncGxvdChwbG90X2RhdGEsIGFlcyh4ID0gcmVvcmRlcihwYXRod2F5LCBORVMpLCB5ID0gTkVTLCBmaWxsID0gTkVTID4gMCkpICsKICAgIGdlb21fY29sKCkgKwogICAgZmFjZXRfd3JhcCh+Y2x1c3Rlciwgc2NhbGVzID0gImZyZWUiKSArCiAgICBjb29yZF9mbGlwKCkgKwogICAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygiVFJVRSIgPSAiI0IyMTgyQiIsICJGQUxTRSIgPSAiIzQzOTNDMyIpLCBndWlkZSA9ICJub25lIikgKwogICAgbGFicyh4ID0gTlVMTCwgeSA9ICJOb3JtYWxpemVkIEVucmljaG1lbnQgU2NvcmUgKE5FUykiLAogICAgICAgICB0aXRsZSA9ICJGR1NFQSBvbiBwb3NpdGl2ZSBtYXJrZXJzIG9ubHkgKFZlcnNpb24gQSkiKSArCiAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDEwKQp9IGVsc2UgewogIGNhdCgiTm8gSGFsbG1hcmsgcGF0aHdheXMgcGFzc2VkIHBhZGogPCAwLjA1IGluIGFueSBjbHVzdGVyLlxuIikKfQpgYGAKCiMgMTUuIFNhdmUgUiBvYmplY3RzCgpgYGB7ciBzYXZlLW9iamVjdHN9CnNhdmUobWFya2Vyc19wb3MsIGFsbF9mZ3NlYV9yZXN1bHRzLAogICAgIGhhbGxtYXJrX3Jlc3VsdHMsIHJlYWN0b21lX3Jlc3VsdHMsIGtlZ2dfcmVzdWx0cywgZ29fcmVzdWx0cywKICAgICBmaWxlID0gIkZHU0VBX1NTX01hbGlnbmFudF9DRDRfUG9zaXRpdmVNYXJrZXJzX3Jlc3VsdHMuUkRhdGEiKQpgYGAKCiMgMTYuIE5vdGVzIG9uIHVzaW5nIHRoaXMgb3V0cHV0IGZvciBjbHVzdGVyIG5hbWluZyBqdXN0aWZpY2F0aW9uCgotIGBGR1NFQV9Qb3NpdGl2ZU1hcmtlcnNfQWxsX1BhdGh3YXlzX3dpdGhfTGVhZGluZ0VkZ2VfR2VuZXMuY3N2YCBpcyB0aGUgcHJpbWFyeQogIGZpbGUgdG8gY2l0ZSBmcm9tLCBzaW5jZSBpdCBsaW5rcyBlYWNoIHBhdGh3YXkncyBORVMvRkRSIGRpcmVjdGx5IHRvIHRoZQogIHNwZWNpZmljIGxlYWRpbmctZWRnZSBnZW5lcyBkcml2aW5nIHRoZSBzaWduYWwgKGUuZy4gImRyaXZlbiBieSBJRklUMiwgSUZJVDMsCiAgT0FTTCIpLgotIFJlcG9ydCBORVMgbWFnbml0dWRlIGFsb25nc2lkZSBGRFIsIG5vdCBzaWduaWZpY2FuY2UgYWxvbmU6IGEgbGFyZ2VyIE5FUyBtZWFucwogIHRoZSBwYXRod2F5J3MgZ2VuZXMgYXJlIG1vcmUgc3Ryb25nbHkgY29uY2VudHJhdGVkIHRvd2FyZCB0aGUgdG9wIG9mIHRoYXQKICBjbHVzdGVyJ3MgcG9zaXRpdmUtbWFya2VyIHJhbmtpbmcuCi0gVGhpcyBwb3NpdGl2ZS1tYXJrZXIgRkdTRUEgYXBwcm9hY2ggYW5kIHlvdXIgZXhpc3RpbmcgT1JBCiAgKGBlbnJpY2hHT2AvYGVucmljaEtFR0dgL2BlbnJpY2hQYXRod2F5YCkgYXBwcm9hY2ggYW5zd2VyIGNsb3NlbHkgcmVsYXRlZCBidXQKICBkaXN0aW5jdCBxdWVzdGlvbnMgYW5kIHNob3VsZCBiZSByZXBvcnRlZCBhcyBjb21wbGVtZW50YXJ5OiBPUkEgYXNrcyAiYXJlIG15CiAgdG9wLU4gbWFya2VyIGdlbmVzIG92ZXJyZXByZXNlbnRlZCBpbiBhIHBhdGh3YXk/IjsgRkdTRUEgYXNrcyAiaXMgYSBwYXRod2F5J3MKICBnZW5lIHNldCBza2V3ZWQgdG93YXJkIHRoZSB0b3Agb2YgbXkgZnVsbCBwb3NpdGl2ZS1tYXJrZXIgcmFua2luZyBmb3IgdGhpcwogIGNsdXN0ZXI/IiBBZ3JlZW1lbnQgYmV0d2VlbiB0aGUgdHdvIHByb3ZpZGVzIHN0cm9uZ2VyIHN1cHBvcnQgZm9yIGEgY2x1c3RlcgogIG5hbWUgdGhhbiBlaXRoZXIgbWV0aG9kIGFsb25lLgotIFRoZSBldmlkZW5jZSBjaGFpbiB0byByZXBvcnQgcGVyIGNsdXN0ZXI6IG1hcmtlciBnZW5lcyAoZnJvbQogIGBGaW5kQWxsTWFya2Vycyhvbmx5LnBvcz1UUlVFKWApIC0+IGRlZmluaW5nIHBhdGh3YXkgKGZyb20gRkdTRUEpIC0+IGxlYWRpbmctZWRnZQogIGdlbmVzIChjb25maXJtaW5nIHdoaWNoIG1hcmtlcnMgYWN0dWFsbHkgZHJpdmUgdGhlIHBhdGh3YXkgc2lnbmFsKS4gRXhhbXBsZToKICBDbHVzdGVyIDEzIG1hcmtlciBnZW5lcyBJRklUMi9PQVNML0lTRzE1IC0+IEhhbGxtYXJrCiAgSU5URVJGRVJPTl9BTFBIQV9SRVNQT05TRS9SZWFjdG9tZSBpbnRlcmZlcm9uIHNpZ25hbGluZyAtPiBhbm5vdGF0aW9uCiAgIklGTi1zdGltdWxhdGVkIG1hbGlnbmFudCBzdGF0ZSIuCgojIDE3LiBTZXNzaW9uIEluZm8KCmBgYHtyIHNlc3Npb24taW5mb30Kc2Vzc2lvbkluZm8oKQpgYGA=