Functional analysis

We start by reading the gene counts per sample, metadata, and functional annotations (obtained form EGGNOG).

library(mbtools)
library(data.table)

genes <- fread("zcat < data/function_counts.csv.gz", drop=1)
manifest <- fread("data/manifest.csv")
annotations <- fread("data/annotated/denovo.emapper.annotations", 
                     sep = "\t", na.strings = c("", "NA"))
Stopped early on line 495133. Expected 22 fields but found 1. Consider fill=TRUE and comment.char=. First discarded non-empty line: <<# 495128 queries scanned>>
genes <- genes[annotations, on = c(locus_tag = "#query_name"), nomatch = NULL]
genes[, vendor_observation_id := tstrsplit(sample, "\\.")[[1]]]
head(genes)

Genes are not really universal:

library(ggplot2)
theme_set(theme_minimal())

sample_counts <- genes[, .(N=uniqueN(sample)), by="seed_eggNOG_ortholog"]
ggplot(sample_counts, aes(x=N)) + geom_bar()

A better way is to group by unique KO term assignments:

ko_map <- data.table(KEGG_ko = genes[KEGG_ko != "", KEGG_ko])
ko_map <- ko_map[, .(ko_term = strsplit(KEGG_ko, ",")[[1]]), by=KEGG_ko]
expanded <- ko_map[genes, on="KEGG_ko", allow.cartesian = TRUE, nomatch = 0]
ko <- expanded[, .(
    reads = sum(reads),
    tpm = sum(tpm)
), by = c("ko_term", "vendor_observation_id")]

concat <- function(x) {
    x <- unlist(strsplit(x[!is.na(x)], ","))
    paste0(unique(x), collapse=",")
}

anns <- expanded[, .(
    EC = concat(EC),
    bigg_reaction = concat(BiGG_Reaction),
    description = concat(`eggNOG free text desc.`),
    CAZy = concat(CAZy),
    KEGG_Pathway = concat(KEGG_Pathway),
    BRITE = concat(BRITE),
    name = concat(Preferred_name)
), by="ko_term"]
setkey(anns, ko_term)
head(ko)

This generalizes better:

ggplot(ko[, .N, by="ko_term"], aes(x=N)) + geom_bar()

Let’s build up the data structures for testing:

counts <- dcast(ko, ko_term ~ vendor_observation_id, value.var = "reads", fill = 0)
ko_terms <- counts[, ko_term]
counts <- as.matrix(counts[, ko_term := NULL])
rownames(counts) <- ko_terms
meta <- manifest[has_close_microbiome == T] %>% as.data.frame()
rownames(meta) <- as.character(meta$vendor_observation_id)

No we can start building the DESeq analysis:

library(futile.logger)
taxa <- matrix(ko_terms, ncol=1)
colnames(taxa) <- "ko_term"
rownames(taxa) <- ko_terms
meta$subset <- factor(meta$subset)
meta$sex <- factor(meta$sex)

flog.threshold(DEBUG)
NULL
ps <- phyloseq(
    otu_table(t(round(counts)), taxa_are_rows=FALSE),
    tax_table(taxa),
    sample_data(meta)
)

tests <- association(
    ps,
    presence_threshold = 1,
    min_abundance = 10,
    in_samples = 1,
    confounders = c("age", "bmi", "sex"),
    variables = "subset",
    taxa_rank = "ko_term",
    method = "deseq2",
    shrink=F
)
bmi <- association(
    ps, 
    presence_threshold = 1,
    min_abundance = 10,
    in_samples = 1,
    confounders = c("age", "sex"),
    variables = "bmi",
    taxa_rank = "ko_term",
    method = "deseq2",
    shrink=F
)

tests <- rbind(tests, bmi)
tests <- anns[tests, on="ko_term"]
fwrite(tests[order(padj)], "data/tests_metagenome_ko.csv")
tests[variable == "subset"][order(padj)]

Volcano plots

ggplot(tests, aes(log2FoldChange, y=-log10(pvalue), 
                  shape=padj<0.1, col=variable)) +
    geom_point() +
    labs(shape="FDR < 0.1", size="FDR < 0.1")

#ggsave("figures/gene_volcano.png", dpi=300, width=4, height=3)

Corr

tests[, "t" := log2FoldChange / lfcSE]

wide <- dcast(tests, ko_term ~ variable, value.var=c("t", "padj"), fill=0)
wide[, "sig" := "none"]
wide[padj_bmi < 0.05, "sig" := "BMI"]
wide[padj_subset < 0.05, "sig" := "weight loss"]
wide[padj_bmi < 0.05 & padj_subset < 0.05, "sig" := "both"]
wide[, "sig" := factor(sig, levels=c("none", "BMI", "weight loss", "both"))]
ggplot(wide, aes(x=t_bmi, y=t_subset, color=sig)) +
    geom_vline(xintercept = 0, color="gray30", lty="dashed") +
    geom_hline(yintercept = 0, color="gray30", lty="dashed") +
    geom_point() + stat_smooth(method="glm", aes(group = 1)) +
    labs(x = "t statistic BMI", y = "t statistic weight loss") +
    scale_color_manual(values=c(none = "black", BMI = "royalblue", `weight loss`="orange", both = "red"))

ggsave("figures/koterm_t.png", dpi=300, width=5, height=3)
cor.test(~ t_bmi + t_subset, data=wide)

    Pearson's product-moment correlation

data:  t_bmi and t_subset
t = -16.829, df = 2973, p-value < 2.2e-16
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
 -0.3273916 -0.2617626
sample estimates:
       cor 
-0.2949249 

Let’s visualize the significant hits.

sig <- tests[variable == "subset" & padj<0.05, ko_term]
ko_counts <- mbtools::normalize(round(counts))
kos <- rownames(ko_counts)
ko_counts <- as.data.table(ko_counts)[, ko_term := kos]
ko_counts <- melt(setDT(ko_counts), id.vars = "ko_term", variable.name = "vendor_observation_id", value.name = "reads")
sig <- ko_counts[ko_term %chin% sig]
sig <- anns[sig, on="ko_term"]
manifest[, vendor_observation_id := as.character(vendor_observation_id)]
sig <- manifest[corebiome == T][sig, on="vendor_observation_id"]
groups <- fread("data/sig_grouping.csv")
tests <- groups[tests, on="ko_term"]

ggplot(sig, aes(x = bmi, y = reads)) + 
    geom_point() +
    scale_y_log10() +
    stat_smooth(method="glm", aes(group = 1), col = "black") +
    facet_wrap(~ ko_term, scales = "free_y") + 
    labs(x="BMI", y="normalized reads") + guides(color = F)
`guides(<scale> = FALSE)` is deprecated. Please use `guides(<scale> = "none")` instead.

ggsave("figures/gene_bmi.png", dpi=300, width=12, height=10)

ggplot(sig, aes(x = subset, y = reads, color=subset)) + 
    geom_jitter(width=0.2) +
    scale_y_log10() +
    stat_summary(fun.y = "mean", geom = "point", pch=23, stroke = 1, size=3, fill = "white") +
    facet_wrap(~ ko_term, scales = "free_y") + 
    labs(x="", y="normalized reads") + guides(color = F)
`fun.y` is deprecated. Use `fun` instead.`guides(<scale> = FALSE)` is deprecated. Please use `guides(<scale> = "none")` instead.

ggsave("figures/gene_abundance.png", dpi=300, width=12, height=10)
pd <- position_dodge(0.3)
ggplot(tests[variable == "subset" & padj<0.05, ], 
       aes(x=log2FoldChange, y=reorder(group, log2FoldChange), color=log10(baseMean), group=ko_term)) +
    geom_vline(xintercept = 0, lty="dashed", color="gray20") +
    geom_linerange(aes(xmin=log2FoldChange - lfcSE, xmax=log2FoldChange + lfcSE), position=pd) + 
    geom_point(size=2, position=pd) +
    labs(x = "log2 fold-change", y = "", color = "abundance") +
    theme(legend.position="bottom")

ggsave("figures/lfcs.png", dpi=300, width=5, height=5)

fwrite(tests[variable == "subset" & padj<0.05, ], "data/significant_functions.csv")

and the same for CAZy terms

pd <- position_dodge(0.3)
ggplot(tests[variable == "subset" & padj<0.05 & CAZy != "", ], 
       aes(x=log2FoldChange, y=reorder(CAZy, log2FoldChange), color=log10(baseMean), group=ko_term)) +
    geom_vline(xintercept = 0, lty="dashed", color="gray20") +
    geom_linerange(aes(xmin=log2FoldChange - lfcSE, xmax=log2FoldChange + lfcSE), position=pd) + 
    geom_point(size=2, position=pd) +
    labs(x = "log2 fold-change", y = "", color = "abundance") +
    theme(legend.position="bottom")

ggsave("figures/lfcs_cazy.png", dpi=300, width=3, height=3)

Overall explained variance

library(vegan)

m <- as(otu_table(ps), "matrix")
perm <- adonis(mbtools::normalize(m) ~ age + sex + bmi + subset, data=as(sample_data(ps), "data.frame"), method="bray")
perm

Call:
adonis(formula = mbtools::normalize(m) ~ age + sex + bmi + subset,      data = as(sample_data(ps), "data.frame"), method = "bray") 

Permutation: free
Number of permutations: 999

Terms added sequentially (first to last)

          Df SumsOfSqs  MeanSqs F.Model      R2 Pr(>F)   
age        1   0.04256 0.042564 1.74059 0.06627  0.014 * 
sex        1   0.01850 0.018504 0.75668 0.02881  0.829   
bmi        1   0.06054 0.060537 2.47556 0.09426  0.005 **
subset     1   0.03157 0.031566 1.29084 0.04915  0.139   
Residuals 20   0.48908 0.024454         0.76151          
Total     24   0.64225                  1.00000          
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Gene richness

rich <- ps %>% rarefy_even_depth() %>% estimate_richness()
You set `rngseed` to FALSE. Make sure you've set & recorded
 the random seed of your session for reproducibility.
See `?set.seed`

...
14OTUs were removed because they are no longer 
present in any sample after random subsampling

...
rich[["vendor_observation_id"]] <- substr(rownames(rich), 2, 1e6)
sdf <- sample_data(ps) %>% as("data.frame")
sdf$vendor_observation_id <- as.character(sdf$vendor_observation_id)
merged <- merge(rich, sdf, by="vendor_observation_id")

mod <- glm(Observed ~ sex + age + bmi + subset, data=merged)
summary(mod)

Call:
glm(formula = Observed ~ sex + age + bmi + subset, data = merged)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-662.43   -70.39    50.38   130.30   343.86  

Coefficients:
                  Estimate Std. Error t value Pr(>|t|)    
(Intercept)       4862.435    466.469  10.424 1.57e-09 ***
sexM              -104.165    140.195  -0.743   0.4661    
age                  6.358      6.562   0.969   0.3441    
bmi                -18.150      9.657  -1.879   0.0748 .  
subsetweight loss  -57.203    141.956  -0.403   0.6912    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for gaussian family taken to be 65640.23)

    Null deviance: 2026646  on 24  degrees of freedom
Residual deviance: 1312805  on 20  degrees of freedom
AIC: 354.67

Number of Fisher Scoring iterations: 2
LS0tCnRpdGxlOiAiRnVuY3Rpb25hbCBhbmFseXNpcyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKIyBGdW5jdGlvbmFsIGFuYWx5c2lzCgpXZSBzdGFydCBieSByZWFkaW5nIHRoZSBnZW5lIGNvdW50cyBwZXIgc2FtcGxlLCBtZXRhZGF0YSwgYW5kIGZ1bmN0aW9uYWwgYW5ub3RhdGlvbnMgKG9idGFpbmVkIGZvcm0gRUdHTk9HKS4KCmBgYHtyfQpsaWJyYXJ5KG1idG9vbHMpCmxpYnJhcnkoZGF0YS50YWJsZSkKCmdlbmVzIDwtIGZyZWFkKCJ6Y2F0IDwgZGF0YS9mdW5jdGlvbl9jb3VudHMuY3N2Lmd6IiwgZHJvcD0xKQptYW5pZmVzdCA8LSBmcmVhZCgiZGF0YS9tYW5pZmVzdC5jc3YiKQphbm5vdGF0aW9ucyA8LSBmcmVhZCgiZGF0YS9hbm5vdGF0ZWQvZGVub3ZvLmVtYXBwZXIuYW5ub3RhdGlvbnMiLCAKICAgICAgICAgICAgICAgICAgICAgc2VwID0gIlx0IiwgbmEuc3RyaW5ncyA9IGMoIiIsICJOQSIpKQpnZW5lcyA8LSBnZW5lc1thbm5vdGF0aW9ucywgb24gPSBjKGxvY3VzX3RhZyA9ICIjcXVlcnlfbmFtZSIpLCBub21hdGNoID0gTlVMTF0KZ2VuZXNbLCB2ZW5kb3Jfb2JzZXJ2YXRpb25faWQgOj0gdHN0cnNwbGl0KHNhbXBsZSwgIlxcLiIpW1sxXV1dCmhlYWQoZ2VuZXMpCmBgYAoKR2VuZXMgYXJlIG5vdCByZWFsbHkgdW5pdmVyc2FsOgoKYGBge3J9CmxpYnJhcnkoZ2dwbG90MikKdGhlbWVfc2V0KHRoZW1lX21pbmltYWwoKSkKCnNhbXBsZV9jb3VudHMgPC0gZ2VuZXNbLCAuKE49dW5pcXVlTihzYW1wbGUpKSwgYnk9InNlZWRfZWdnTk9HX29ydGhvbG9nIl0KZ2dwbG90KHNhbXBsZV9jb3VudHMsIGFlcyh4PU4pKSArIGdlb21fYmFyKCkKYGBgCgpBIGJldHRlciB3YXkgaXMgdG8gZ3JvdXAgYnkgdW5pcXVlIEtPIHRlcm0gYXNzaWdubWVudHM6CgpgYGB7cn0Ka29fbWFwIDwtIGRhdGEudGFibGUoS0VHR19rbyA9IGdlbmVzW0tFR0dfa28gIT0gIiIsIEtFR0dfa29dKQprb19tYXAgPC0ga29fbWFwWywgLihrb190ZXJtID0gc3Ryc3BsaXQoS0VHR19rbywgIiwiKVtbMV1dKSwgYnk9S0VHR19rb10KZXhwYW5kZWQgPC0ga29fbWFwW2dlbmVzLCBvbj0iS0VHR19rbyIsIGFsbG93LmNhcnRlc2lhbiA9IFRSVUUsIG5vbWF0Y2ggPSAwXQprbyA8LSBleHBhbmRlZFssIC4oCiAgICByZWFkcyA9IHN1bShyZWFkcyksCiAgICB0cG0gPSBzdW0odHBtKQopLCBieSA9IGMoImtvX3Rlcm0iLCAidmVuZG9yX29ic2VydmF0aW9uX2lkIildCgpjb25jYXQgPC0gZnVuY3Rpb24oeCkgewogICAgeCA8LSB1bmxpc3Qoc3Ryc3BsaXQoeFshaXMubmEoeCldLCAiLCIpKQogICAgcGFzdGUwKHVuaXF1ZSh4KSwgY29sbGFwc2U9IiwiKQp9Cgphbm5zIDwtIGV4cGFuZGVkWywgLigKICAgIEVDID0gY29uY2F0KEVDKSwKICAgIGJpZ2dfcmVhY3Rpb24gPSBjb25jYXQoQmlHR19SZWFjdGlvbiksCiAgICBkZXNjcmlwdGlvbiA9IGNvbmNhdChgZWdnTk9HIGZyZWUgdGV4dCBkZXNjLmApLAogICAgQ0FaeSA9IGNvbmNhdChDQVp5KSwKICAgIEtFR0dfUGF0aHdheSA9IGNvbmNhdChLRUdHX1BhdGh3YXkpLAogICAgQlJJVEUgPSBjb25jYXQoQlJJVEUpLAogICAgbmFtZSA9IGNvbmNhdChQcmVmZXJyZWRfbmFtZSkKKSwgYnk9ImtvX3Rlcm0iXQpzZXRrZXkoYW5ucywga29fdGVybSkKaGVhZChrbykKYGBgCgpUaGlzIGdlbmVyYWxpemVzIGJldHRlcjoKCmBgYHtyfQpnZ3Bsb3Qoa29bLCAuTiwgYnk9ImtvX3Rlcm0iXSwgYWVzKHg9TikpICsgZ2VvbV9iYXIoKQpgYGAKCkxldCdzIGJ1aWxkIHVwIHRoZSBkYXRhIHN0cnVjdHVyZXMgZm9yIHRlc3Rpbmc6CgpgYGB7cn0KY291bnRzIDwtIGRjYXN0KGtvLCBrb190ZXJtIH4gdmVuZG9yX29ic2VydmF0aW9uX2lkLCB2YWx1ZS52YXIgPSAicmVhZHMiLCBmaWxsID0gMCkKa29fdGVybXMgPC0gY291bnRzWywga29fdGVybV0KY291bnRzIDwtIGFzLm1hdHJpeChjb3VudHNbLCBrb190ZXJtIDo9IE5VTExdKQpyb3duYW1lcyhjb3VudHMpIDwtIGtvX3Rlcm1zCm1ldGEgPC0gbWFuaWZlc3RbaGFzX2Nsb3NlX21pY3JvYmlvbWUgPT0gVF0gJT4lIGFzLmRhdGEuZnJhbWUoKQpyb3duYW1lcyhtZXRhKSA8LSBhcy5jaGFyYWN0ZXIobWV0YSR2ZW5kb3Jfb2JzZXJ2YXRpb25faWQpCmBgYAoKTm8gd2UgY2FuIHN0YXJ0IGJ1aWxkaW5nIHRoZSBERVNlcSBhbmFseXNpczoKCmBgYHtyfQpsaWJyYXJ5KGZ1dGlsZS5sb2dnZXIpCnRheGEgPC0gbWF0cml4KGtvX3Rlcm1zLCBuY29sPTEpCmNvbG5hbWVzKHRheGEpIDwtICJrb190ZXJtIgpyb3duYW1lcyh0YXhhKSA8LSBrb190ZXJtcwptZXRhJHN1YnNldCA8LSBmYWN0b3IobWV0YSRzdWJzZXQpCm1ldGEkc2V4IDwtIGZhY3RvcihtZXRhJHNleCkKCmZsb2cudGhyZXNob2xkKERFQlVHKQoKcHMgPC0gcGh5bG9zZXEoCiAgICBvdHVfdGFibGUodChyb3VuZChjb3VudHMpKSwgdGF4YV9hcmVfcm93cz1GQUxTRSksCiAgICB0YXhfdGFibGUodGF4YSksCiAgICBzYW1wbGVfZGF0YShtZXRhKQopCgp0ZXN0cyA8LSBhc3NvY2lhdGlvbigKICAgIHBzLAogICAgcHJlc2VuY2VfdGhyZXNob2xkID0gMSwKICAgIG1pbl9hYnVuZGFuY2UgPSAxMCwKICAgIGluX3NhbXBsZXMgPSAxLAogICAgY29uZm91bmRlcnMgPSBjKCJhZ2UiLCAiYm1pIiwgInNleCIpLAogICAgdmFyaWFibGVzID0gInN1YnNldCIsCiAgICB0YXhhX3JhbmsgPSAia29fdGVybSIsCiAgICBtZXRob2QgPSAiZGVzZXEyIiwKICAgIHNocmluaz1GCikKYm1pIDwtIGFzc29jaWF0aW9uKAogICAgcHMsIAogICAgcHJlc2VuY2VfdGhyZXNob2xkID0gMSwKICAgIG1pbl9hYnVuZGFuY2UgPSAxMCwKICAgIGluX3NhbXBsZXMgPSAxLAogICAgY29uZm91bmRlcnMgPSBjKCJhZ2UiLCAic2V4IiksCiAgICB2YXJpYWJsZXMgPSAiYm1pIiwKICAgIHRheGFfcmFuayA9ICJrb190ZXJtIiwKICAgIG1ldGhvZCA9ICJkZXNlcTIiLAogICAgc2hyaW5rPUYKKQoKdGVzdHMgPC0gcmJpbmQodGVzdHMsIGJtaSkKdGVzdHMgPC0gYW5uc1t0ZXN0cywgb249ImtvX3Rlcm0iXQpmd3JpdGUodGVzdHNbb3JkZXIocGFkaildLCAiZGF0YS90ZXN0c19tZXRhZ2Vub21lX2tvLmNzdiIpCnRlc3RzW3ZhcmlhYmxlID09ICJzdWJzZXQiXVtvcmRlcihwYWRqKV0KYGBgCgpWb2xjYW5vIHBsb3RzCgpgYGB7cn0KZ2dwbG90KHRlc3RzLCBhZXMobG9nMkZvbGRDaGFuZ2UsIHk9LWxvZzEwKHB2YWx1ZSksIAogICAgICAgICAgICAgICAgICBzaGFwZT1wYWRqPDAuMSwgY29sPXZhcmlhYmxlKSkgKwogICAgZ2VvbV9wb2ludCgpICsKICAgIGxhYnMoc2hhcGU9IkZEUiA8IDAuMSIsIHNpemU9IkZEUiA8IDAuMSIpCiNnZ3NhdmUoImZpZ3VyZXMvZ2VuZV92b2xjYW5vLnBuZyIsIGRwaT0zMDAsIHdpZHRoPTQsIGhlaWdodD0zKQpgYGAKCkNvcnIKCmBgYHtyLCBmaWcud2lkdGg9NSwgZmlnLmhlaWdodD0zfQp0ZXN0c1ssICJ0IiA6PSBsb2cyRm9sZENoYW5nZSAvIGxmY1NFXQoKd2lkZSA8LSBkY2FzdCh0ZXN0cywga29fdGVybSB+IHZhcmlhYmxlLCB2YWx1ZS52YXI9YygidCIsICJwYWRqIiksIGZpbGw9MCkKd2lkZVssICJzaWciIDo9ICJub25lIl0Kd2lkZVtwYWRqX2JtaSA8IDAuMDUsICJzaWciIDo9ICJCTUkiXQp3aWRlW3BhZGpfc3Vic2V0IDwgMC4wNSwgInNpZyIgOj0gIndlaWdodCBsb3NzIl0Kd2lkZVtwYWRqX2JtaSA8IDAuMDUgJiBwYWRqX3N1YnNldCA8IDAuMDUsICJzaWciIDo9ICJib3RoIl0Kd2lkZVssICJzaWciIDo9IGZhY3RvcihzaWcsIGxldmVscz1jKCJub25lIiwgIkJNSSIsICJ3ZWlnaHQgbG9zcyIsICJib3RoIikpXQpnZ3Bsb3Qod2lkZSwgYWVzKHg9dF9ibWksIHk9dF9zdWJzZXQsIGNvbG9yPXNpZykpICsKICAgIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGNvbG9yPSJncmF5MzAiLCBsdHk9ImRhc2hlZCIpICsKICAgIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDAsIGNvbG9yPSJncmF5MzAiLCBsdHk9ImRhc2hlZCIpICsKICAgIGdlb21fcG9pbnQoKSArIHN0YXRfc21vb3RoKG1ldGhvZD0iZ2xtIiwgYWVzKGdyb3VwID0gMSkpICsKICAgIGxhYnMoeCA9ICJ0IHN0YXRpc3RpYyBCTUkiLCB5ID0gInQgc3RhdGlzdGljIHdlaWdodCBsb3NzIikgKwogICAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jKG5vbmUgPSAiYmxhY2siLCBCTUkgPSAicm95YWxibHVlIiwgYHdlaWdodCBsb3NzYD0ib3JhbmdlIiwgYm90aCA9ICJyZWQiKSkKZ2dzYXZlKCJmaWd1cmVzL2tvdGVybV90LnBuZyIsIGRwaT0zMDAsIHdpZHRoPTUsIGhlaWdodD0zKQpjb3IudGVzdCh+IHRfYm1pICsgdF9zdWJzZXQsIGRhdGE9d2lkZSkKYGBgCgpMZXQncyB2aXN1YWxpemUgdGhlIHNpZ25pZmljYW50IGhpdHMuCgpgYGB7ciwgZmlnLndpZHRoID0gMTIsIGZpZy5oZWlnaHQgPSAxMH0Kc2lnIDwtIHRlc3RzW3ZhcmlhYmxlID09ICJzdWJzZXQiICYgcGFkajwwLjA1LCBrb190ZXJtXQprb19jb3VudHMgPC0gbWJ0b29sczo6bm9ybWFsaXplKHJvdW5kKGNvdW50cykpCmtvcyA8LSByb3duYW1lcyhrb19jb3VudHMpCmtvX2NvdW50cyA8LSBhcy5kYXRhLnRhYmxlKGtvX2NvdW50cylbLCBrb190ZXJtIDo9IGtvc10Ka29fY291bnRzIDwtIG1lbHQoc2V0RFQoa29fY291bnRzKSwgaWQudmFycyA9ICJrb190ZXJtIiwgdmFyaWFibGUubmFtZSA9ICJ2ZW5kb3Jfb2JzZXJ2YXRpb25faWQiLCB2YWx1ZS5uYW1lID0gInJlYWRzIikKc2lnIDwtIGtvX2NvdW50c1trb190ZXJtICVjaGluJSBzaWddCnNpZyA8LSBhbm5zW3NpZywgb249ImtvX3Rlcm0iXQptYW5pZmVzdFssIHZlbmRvcl9vYnNlcnZhdGlvbl9pZCA6PSBhcy5jaGFyYWN0ZXIodmVuZG9yX29ic2VydmF0aW9uX2lkKV0Kc2lnIDwtIG1hbmlmZXN0W2NvcmViaW9tZSA9PSBUXVtzaWcsIG9uPSJ2ZW5kb3Jfb2JzZXJ2YXRpb25faWQiXQpncm91cHMgPC0gZnJlYWQoImRhdGEvc2lnX2dyb3VwaW5nLmNzdiIpCnRlc3RzIDwtIGdyb3Vwc1t0ZXN0cywgb249ImtvX3Rlcm0iXQoKZ2dwbG90KHNpZywgYWVzKHggPSBibWksIHkgPSByZWFkcykpICsgCiAgICBnZW9tX3BvaW50KCkgKwogICAgc2NhbGVfeV9sb2cxMCgpICsKICAgIHN0YXRfc21vb3RoKG1ldGhvZD0iZ2xtIiwgYWVzKGdyb3VwID0gMSksIGNvbCA9ICJibGFjayIpICsKICAgIGZhY2V0X3dyYXAofiBrb190ZXJtLCBzY2FsZXMgPSAiZnJlZV95IikgKyAKICAgIGxhYnMoeD0iQk1JIiwgeT0ibm9ybWFsaXplZCByZWFkcyIpICsgZ3VpZGVzKGNvbG9yID0gRikKZ2dzYXZlKCJmaWd1cmVzL2dlbmVfYm1pLnBuZyIsIGRwaT0zMDAsIHdpZHRoPTEyLCBoZWlnaHQ9MTApCgpnZ3Bsb3Qoc2lnLCBhZXMoeCA9IHN1YnNldCwgeSA9IHJlYWRzLCBjb2xvcj1zdWJzZXQpKSArIAogICAgZ2VvbV9qaXR0ZXIod2lkdGg9MC4yKSArCiAgICBzY2FsZV95X2xvZzEwKCkgKwogICAgc3RhdF9zdW1tYXJ5KGZ1bi55ID0gIm1lYW4iLCBnZW9tID0gInBvaW50IiwgcGNoPTIzLCBzdHJva2UgPSAxLCBzaXplPTMsIGZpbGwgPSAid2hpdGUiKSArCiAgICBmYWNldF93cmFwKH4ga29fdGVybSwgc2NhbGVzID0gImZyZWVfeSIpICsgCiAgICBsYWJzKHg9IiIsIHk9Im5vcm1hbGl6ZWQgcmVhZHMiKSArIGd1aWRlcyhjb2xvciA9IEYpCmdnc2F2ZSgiZmlndXJlcy9nZW5lX2FidW5kYW5jZS5wbmciLCBkcGk9MzAwLCB3aWR0aD0xMiwgaGVpZ2h0PTEwKQpgYGAKCmBgYHtyLCBmaWcud2lkdGg9NSwgZmlnLmhlaWdodD01fQpwZCA8LSBwb3NpdGlvbl9kb2RnZSgwLjMpCmdncGxvdCh0ZXN0c1t2YXJpYWJsZSA9PSAic3Vic2V0IiAmIHBhZGo8MC4wNSwgXSwgCiAgICAgICBhZXMoeD1sb2cyRm9sZENoYW5nZSwgeT1yZW9yZGVyKGdyb3VwLCBsb2cyRm9sZENoYW5nZSksIGNvbG9yPWxvZzEwKGJhc2VNZWFuKSwgZ3JvdXA9a29fdGVybSkpICsKICAgIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGx0eT0iZGFzaGVkIiwgY29sb3I9ImdyYXkyMCIpICsKICAgIGdlb21fbGluZXJhbmdlKGFlcyh4bWluPWxvZzJGb2xkQ2hhbmdlIC0gbGZjU0UsIHhtYXg9bG9nMkZvbGRDaGFuZ2UgKyBsZmNTRSksIHBvc2l0aW9uPXBkKSArIAogICAgZ2VvbV9wb2ludChzaXplPTIsIHBvc2l0aW9uPXBkKSArCiAgICBsYWJzKHggPSAibG9nMiBmb2xkLWNoYW5nZSIsIHkgPSAiIiwgY29sb3IgPSAiYWJ1bmRhbmNlIikgKwogICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uPSJib3R0b20iKQpnZ3NhdmUoImZpZ3VyZXMvbGZjcy5wbmciLCBkcGk9MzAwLCB3aWR0aD01LCBoZWlnaHQ9NSkKCmZ3cml0ZSh0ZXN0c1t2YXJpYWJsZSA9PSAic3Vic2V0IiAmIHBhZGo8MC4wNSwgXSwgImRhdGEvc2lnbmlmaWNhbnRfZnVuY3Rpb25zLmNzdiIpCmBgYAoKYW5kIHRoZSBzYW1lIGZvciBDQVp5IHRlcm1zCgpgYGB7ciwgZmlnLndpZHRoPTMsIGZpZy5oZWlnaHQ9M30KcGQgPC0gcG9zaXRpb25fZG9kZ2UoMC4zKQpnZ3Bsb3QodGVzdHNbdmFyaWFibGUgPT0gInN1YnNldCIgJiBwYWRqPDAuMDUgJiBDQVp5ICE9ICIiLCBdLCAKICAgICAgIGFlcyh4PWxvZzJGb2xkQ2hhbmdlLCB5PXJlb3JkZXIoQ0FaeSwgbG9nMkZvbGRDaGFuZ2UpLCBjb2xvcj1sb2cxMChiYXNlTWVhbiksIGdyb3VwPWtvX3Rlcm0pKSArCiAgICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLCBsdHk9ImRhc2hlZCIsIGNvbG9yPSJncmF5MjAiKSArCiAgICBnZW9tX2xpbmVyYW5nZShhZXMoeG1pbj1sb2cyRm9sZENoYW5nZSAtIGxmY1NFLCB4bWF4PWxvZzJGb2xkQ2hhbmdlICsgbGZjU0UpLCBwb3NpdGlvbj1wZCkgKyAKICAgIGdlb21fcG9pbnQoc2l6ZT0yLCBwb3NpdGlvbj1wZCkgKwogICAgbGFicyh4ID0gImxvZzIgZm9sZC1jaGFuZ2UiLCB5ID0gIiIsIGNvbG9yID0gImFidW5kYW5jZSIpICsKICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbj0iYm90dG9tIikKZ2dzYXZlKCJmaWd1cmVzL2xmY3NfY2F6eS5wbmciLCBkcGk9MzAwLCB3aWR0aD0zLCBoZWlnaHQ9MykKYGBgCgojIyBPdmVyYWxsIGV4cGxhaW5lZCB2YXJpYW5jZQoKYGBge3J9CmxpYnJhcnkodmVnYW4pCgptIDwtIGFzKG90dV90YWJsZShwcyksICJtYXRyaXgiKQpwZXJtIDwtIGFkb25pcyhtYnRvb2xzOjpub3JtYWxpemUobSkgfiBhZ2UgKyBzZXggKyBibWkgKyBzdWJzZXQsIGRhdGE9YXMoc2FtcGxlX2RhdGEocHMpLCAiZGF0YS5mcmFtZSIpLCBtZXRob2Q9ImJyYXkiKQpwZXJtCmBgYAoKIyMgR2VuZSByaWNobmVzcwoKYGBge3J9CnJpY2ggPC0gcHMgJT4lIHJhcmVmeV9ldmVuX2RlcHRoKCkgJT4lIGVzdGltYXRlX3JpY2huZXNzKCkKcmljaFtbInZlbmRvcl9vYnNlcnZhdGlvbl9pZCJdXSA8LSBzdWJzdHIocm93bmFtZXMocmljaCksIDIsIDFlNikKc2RmIDwtIHNhbXBsZV9kYXRhKHBzKSAlPiUgYXMoImRhdGEuZnJhbWUiKQpzZGYkdmVuZG9yX29ic2VydmF0aW9uX2lkIDwtIGFzLmNoYXJhY3RlcihzZGYkdmVuZG9yX29ic2VydmF0aW9uX2lkKQptZXJnZWQgPC0gbWVyZ2UocmljaCwgc2RmLCBieT0idmVuZG9yX29ic2VydmF0aW9uX2lkIikKCm1vZCA8LSBnbG0oT2JzZXJ2ZWQgfiBzZXggKyBhZ2UgKyBibWkgKyBzdWJzZXQsIGRhdGE9bWVyZ2VkKQpzdW1tYXJ5KG1vZCkKYGBg