library(TargetDecoy)
library(ggplot2)

1 Introduction

Slide courtesy to Lennart Martens

1.1 Why did Lennart mention that “statistics were out of the window”

library(TargetDecoy)
library(tidyverse)
#> ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
#> ✔ dplyr     1.1.4     ✔ readr     2.1.5
#> ✔ forcats   1.0.0     ✔ stringr   1.5.1
#> ✔ lubridate 1.9.3     ✔ tibble    3.2.1
#> ✔ purrr     1.0.2     ✔ tidyr     1.3.1
#> ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
#> ✖ dplyr::filter() masks stats::filter()
#> ✖ dplyr::lag()    masks stats::lag()
#> ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
data("ModSwissXT")
hlp <- TargetDecoy:::.getDF(ModSwissXT) 
names(hlp) <- names(hlp) %>%
    str_replace(pattern=":",replacement="_")
hlp <- hlp %>% 
mutate(omssa_evalue=as.double(omssa_evalue))
  • E-values: Probability that a random candidate peptide produces a higher score than the observed PSM score.

  • Probability that a random candidate peptide produces a higher score that the observed PSM score for a real search with OMSSA.

library(TargetDecoy)
library(tidyverse)
data("ModSwissXT")
hlp <- TargetDecoy:::.getDF(ModSwissXT) 
names(hlp) <- names(hlp) %>%
    str_replace(pattern=":",replacement="_")
hlp <- hlp %>% 
mutate(omssa_evalue=as.double(omssa_evalue))
hlp %>% 
    filter(!is.na(omssa_evalue)) %>% 
    ggplot(aes(omssa_evalue)) +
    geom_histogram(breaks=seq(0,100,5)) +
    xlab("E-value (%)") + 
    ylab("frequency")

  • E-value we expect for an observed spectrum and a random candidate peptide.
data.frame(evalue=runif(20000,0,100)) %>%
    ggplot(aes(evalue)) +
    geom_histogram(breaks=seq(0,100,5)) +
    xlab("E-value (%)") + 
    ylab("frequency")

  • Probability that a random candidate peptide produces a higher score than the observed PSM score for decoys in a real search with OMSSA.
hlp %>% 
    filter(isdecoy & !is.na(omssa_evalue)) %>% 
    ggplot(aes(omssa_evalue)) +
    geom_histogram(breaks=seq(0,100,5)) +
    xlab("E-value (%)") + 
    ylab("frequency")

  • A bad hit is the random hit with the best score so it is also bound to have a low E-value.

  • If we look at E-values for all PSMs they are only useful as a score.

  • We should know the distribution of the maximum score of random candidate peptides when we want to do the statistics.

hlp %>% 
    filter(!is.na(omssa_evalue)) %>% 
    ggplot(aes(-log10(omssa_evalue))) +
    geom_histogram(breaks=seq(0,1000,.5)) +
    xlab("-log10(E-value)") + 
    ylab("frequency") +
    xlim(0,20) 

2 Concepts

2.1 Basic Statistical Concepts

names(hlp) <- names(hlp) %>% str_replace(pattern="-",replacement = "_")
hlp <- hlp %>% mutate(ms_gf_specevalue=as.double(ms_gf_specevalue))
library(mgcv)
#> Loading required package: nlme
#> 
#> Attaching package: 'nlme'
#> The following object is masked from 'package:dplyr':
#> 
#>     collapse
#> This is mgcv 1.9-1. For overview type 'help("mgcv-package")'.
dec <- -log10(hlp$ms_gf_specevalue[hlp$isdecoy]) %>% na.exclude()
tar <- -log10(hlp$ms_gf_specevalue[!hlp$isdecoy]) %>% na.exclude()

breaks <- seq(0,30,.5)
#binWidth <-2
#breaks <- seq(floor(min(c(dec,tar))/binWidth)*binWidth,ceiling(max(c(dec,tar))/binWidth)*binWidth,binWidth)
#code if we register the modes by substracting the mode from the target scores and  the decoy scores.
#breaks=seq(-(ceiling(abs(min(c(dec,tar))/binWidth))+.5)*binWidth,(ceiling(max(c(dec,tar))/binWidth)+.5)*binWidth,binWidth)
histDec <- hist(dec,breaks=breaks,plot = FALSE)
histTar <- hist(tar,breaks=breaks,plot=FALSE)
histSam <- hist(c(dec,tar),breaks=breaks, plot = FALSE)

grid<-seq(0,30,.1)
countsTarG<-data.frame(y=histTar$counts-histDec$counts,x=histTar$mids)
countsTarG$y[countsTarG$y<0]<-0
fitTarG<-gam(y~s(x),data=countsTarG,family=poisson)
fitTarGrid<-exp(predict(fitTarG,newdata=data.frame(x=grid)))

countsDec<-data.frame(y=histDec$counts,x=histDec$mids)
fitDec<-gam(y~s(x),data=countsDec,family=poisson)
fitSamBad<-exp(predict(fitDec,newdata=data.frame(x=grid)))*2

plot(histSam,xlab="MS-GF+ Score",ylab="# PSMs",main="Pyrococcus Search",border="white",col="grey",cex.axis=1.5,cex.main=1.5,cex.lab=1.5,ylim=c(0,1500), axes =FALSE)
axis(side=2,at=c(0,750,1500))
axis(side=1,at=c(0,10,20,30))
lines(grid,fitSamBad+fitTarGrid,col="black",lwd=2)
lines(grid,fitSamBad,col="#FF9900",lwd=2)
lines(grid,fitTarGrid,col="#009900",lwd=2)

Let \(x\) be the PSM score

The scores will follow a mixture distribution:

\[f(x) = \pi_b \mathbin{\color{orange}{ f_b(x)}}+(1-\pi_b)\mathbin{\color{green}{ f_g(x)}},\] The local fdr is also referred to as the posterior error probability (PEP), and is the probability that a PSM with a score \(x\) is a bad hit.

\[ \begin{array}{lcl} \text{lfdr}(x) &=&\text{PEP(x)}\\ &=& \text{P}[\text{bad hit} \vert X=x]\\ &=& \frac{\pi_b f_b(x)}{f(x)} \end{array} \]

We will return a list of PSMs by using the FDR:

\[FDR = E\left[\frac{FP}{TP + FP}\right]\]

  • FP: number of false positives, bad hits
  • TP: number of true positives, good hits

\[ \begin{array}{lcl} \text{FDR}(x\geq t) &=& \text{P}[\text{bad hit }\vert X \geq t]\\\\ &=&\frac{\pi_b\int\limits_{x=t}^{+\infty}f_b(x)}{\int\limits_{x=t}^{+\infty}f(x)}\\\\ &=&\frac{\pi_b[1-F_b(t)]}{1-F(t)} \end{array} \]

  • So the FDR is a set property, it measure the probability on a bad hit in the set of PSMs with scores \(X\geq t\).

Our list, thus consists of all PSMs with a score \(x\) above a threshold t.

plot(histSam,xlab="MS-GF+ Score",ylab="# PSMs",main="Pyrococcus Search",border="white",col="grey",cex.axis=1.5,cex.main=1.5,cex.lab=1.5,ylim=c(0,1500), axes =FALSE)
axis(side=2,at=c(0,750,1500))
axis(side=1,at=c(0,10,20,30))
lines(grid,fitSamBad+fitTarGrid,col="black",lwd=2)
lines(grid,fitSamBad,col="#FF9900",lwd=2)
lines(grid,fitTarGrid,col="#009900",lwd=2)
text(pos=4,5,1430,label=expression(x >= t),col="darkorchid4",cex=2)
rect(5,-10,30,1500,lwd=2,border="darkorchid4")

  • So we know how many PSMs we return, i.e. TP + FP: \[\text{#PSMs with } x \geq t\]

and we can also estimate the probability on a PSM above the threshold empirically:

\[1-\hat{\text{F}}(t) = \frac{\text{#PSMs with } x \geq t}{\text{#PSMs}}\]

  • So to estimate the FDR we only have to estimate the expected number of PSMs that are bad hits with a score \(x\) above the threshold \(t\).

\[\widehat{\text{FDR}}(t) = \frac{E\left[\#\text{Bad PSM hits with } X \geq t\right]}{\text{#PSMs with } x \geq t}\]

2.2 Competitive target decoy approach

  • Search against decoy database to generate representative bad hits
  • Reverse database is popular
  • Concatenated search is most popular
  • Advantage, a number of bad hits already matches with decoys

\(\rightarrow\) we know that these are bad hits

\(\rightarrow\) we have to infer on less target PSMs.

hlp %>% 
    filter(!is.na(ms_gf_specevalue)) %>% 
    ggplot(aes(-log10(ms_gf_specevalue))) +
    geom_histogram(breaks=seq(0,1000,.5)) +
    xlab("-log10(E-value)") + 
    ylab("frequency") +
    xlim(0,40) +
    facet_grid(isdecoy~.)

We estimate that by using the decoys: \[\text{# Decoys with x} \geq t\] So our estimated FDR becomes

\[\widehat{\text{FDR}}(x\geq t) = \frac{\text{# Decoys} \geq t}{\text{# Targets} \geq t}\] If we rewrite the FDR we can see the TDA assumptions:

\[ \begin{array}{lcl} \widehat{\text{FDR}}(x\geq t)&=&\frac{\text{# Decoys} \geq t}{\text{# Targets} \geq t}\\\\ &=&\frac{\frac{\text{# Decoys}}{\text{# Targets}}\frac{\text{# Decoys with }x \geq t}{\text{# Decoys}}}{\frac{\text{# Targets with } x \geq t}{\text{# Targets}}}\\\\ &=&\frac{\frac{\hat{\text{E}}\left[\text{# Bad Targets}\right]}{\text{# Targets}}\frac{\hat{\text{E}}\left[\text{# Bad Targets with }X \geq t\right]}{\hat{\text{E}}\left[\text{# Bad targets}\right]}}{\frac{\text{# Targets with } x \geq t}{\text{# Targets}}} \\\\ &=&\frac{\hat{\text{P}}\left[\text{Bad Target} \right]\times\hat{\text{P}}\left[\text{Bad Target}\vert X \geq t \right]}{\hat{\text{P}}\left[\text{Target}\vert X \geq t\right]}\\\\ &=&\frac{\hat\pi_b [1-\hat{F}_b(t)]}{1-\hat{F}(t)} \end{array} \]

So the TDA has the following assumptions:

  1. A bad hit is equally likely to match to a decoy as to a target sequence. \(\rightarrow\) we can thus estimate the fraction of bad hits or the probability on a bad hit as \[\hat{\pi}_b = \frac{\# \text{Decoys}}{\# \text{Targets}}\]

  2. Bad target PSM scores and decoy PSM scores are equaly distributed.

3 Diagnostic plots for the TDA

We will evaluate the TDA assumptions using diagnostic plots that compares the empirical distribution of decoy and target PSM scores.

  • Histograms
  • P-P plots

With P-P plots will plot for each observed PSM score \(t\) the empirical probability to observe a decoy with score \(x \leq t\) to the empirical probability to observe a target with score \(x \leq t\):

\[\hat{\text{P}}\left[\text{decoy with score } x \leq t\right] = \frac{\# \text{decoys with score } x \leq t}{\# \text{decoys}}\]

\[ \hat{\text{P}}\left[\text{target with score } x \leq t\right] = \frac{\# \text{target with score } x \leq t}{\# \text{targets}} \]

If the two distributions are the same the dots of the P-P plot should follow the 1-1 line.

This will not be the case. We expect the distribution of the target PSMs:

  • to be similarly distributed as the decoys for low scores
  • and enriched with many high scores corresponding to target PSMs which are matching to the proper peptide sequence in the data base.
hlp <- hlp %>% 
  mutate(score=-log10(hlp$ms_gf_specevalue))

y1<-hlp$score[hlp$isdecoy] %>% na.exclude
y2<-hlp$score[!hlp$isdecoy] %>% na.exclude
F1<-ecdf(y1)
F2<-ecdf(y2)
breaks<-seq(floor(min(c(y1,y2))),ceiling(max(c(y1,y2))),length.out=50)
pi0<-length(y1)/length(y2)
for (x in quantile(c(y1,y2),c(0,.01,.02,seq(0.1,1,.1))))
{
par(mfrow=c(1,2))
hist(y2,breaks=breaks,main="Pyrococcus MSGF+",cex.axis=1.5,cex.lab=1.5,cex.main=1.5,col="grey")
decHist<-hist(y1,breaks=breaks,plot=FALSE)
points(decHist$mids,decHist$counts,col="#FF9900",type="h",lwd=2)
abline(v=x,col="blue",lwd=2)
plot(F1(y2),F2(y2),xlab="ECDF Targets",ylab="ECDF Decoys",cex=.4,main="P-P plot",cex.axis=1.5,cex.lab=1.5,cex.main=1.5,col="grey",pch=19)
abline(a=0,b=pi0)
abline(v=F1(x),col="blue")
abline(h=F2(x),col="blue")
points(F1(x),F2(x),col="blue",cex=2,pch=19)
}

Note, that

  • the points corresponding to low score values follow a straight line indicating that targets and decoys scores are similarly distributed
  • this line has an angle equal to fraction of bad hits, which is estimated as \(\hat\pi_b=\frac{\# \text{decoys}}{\#\text{targets}}\) and is indicated by the black line in the plot.
  • This line can be used to assess the assumption that bad hits are equally likely matching to targets sequences as to decoy sequences.
LS0tCnRpdGxlOiAiSW50cm9kdWN0aW9uIHRvIFRhcmdldERlY295IgphdXRob3I6IAogIC0gbmFtZTogTGlldmVuIENsZW1lbnQKICAgIGFmZmlsaWF0aW9uOgogICAgLSBHaGVudCBVbml2ZXJzaXR5Cm91dHB1dDogCiAgICBodG1sX2RvY3VtZW50OgogICAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgICAgIHRoZW1lOiBmbGF0bHkKICAgICAgdG9jOiB0cnVlCiAgICAgIHRvY19mbG9hdDogdHJ1ZQogICAgICBoaWdobGlnaHQ6IHRhbmdvCiAgICAgIG51bWJlcl9zZWN0aW9uczogdHJ1ZQogICAgICBjb2RlX2ZvbGRpbmc6ICJoaWRlIgpsaW5rY29sb3I6IGJsdWUKdXJsY29sb3I6IGJsdWUKY2l0ZWNvbG9yOiBibHVlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmxpYnJhcnkoa25pdHIpCm9wdHNfY2h1bmskc2V0KAogICAgY29sbGFwc2UgPSBUUlVFLAogICAgY29tbWVudCA9ICIjPiIsCiAgICBjcm9wID0gTlVMTCwKICAgIGZpZy53aWR0aCA9IDYsCiAgICBkcGkgPSA3MgopCmBgYAoKYGBge3IgImxpYnJhcmllcyIsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkoVGFyZ2V0RGVjb3kpCmxpYnJhcnkoZ2dwbG90MikKYGBgCgojIEludHJvZHVjdGlvbgoKIVtdKGZpZ3VyZXMvUHJvdGVvbWljc1dvcmtmbG93LnBuZykKCiFbXShmaWd1cmVzL3BlcHRpZGVTcGVjdHJhbE1hdGNoaW5nLnBuZykKU2xpZGUgY291cnRlc3kgdG8gTGVubmFydCBNYXJ0ZW5zIAoKIyMgV2h5IGRpZCBMZW5uYXJ0IG1lbnRpb24gdGhhdCAic3RhdGlzdGljcyB3ZXJlIG91dCBvZiB0aGUgd2luZG93IgoKYGBge3J9CmxpYnJhcnkoVGFyZ2V0RGVjb3kpCmxpYnJhcnkodGlkeXZlcnNlKQpkYXRhKCJNb2RTd2lzc1hUIikKaGxwIDwtIFRhcmdldERlY295Ojo6LmdldERGKE1vZFN3aXNzWFQpIApuYW1lcyhobHApIDwtIG5hbWVzKGhscCkgJT4lCiAgICBzdHJfcmVwbGFjZShwYXR0ZXJuPSI6IixyZXBsYWNlbWVudD0iXyIpCmhscCA8LSBobHAgJT4lIAptdXRhdGUob21zc2FfZXZhbHVlPWFzLmRvdWJsZShvbXNzYV9ldmFsdWUpKQpgYGAKCgotIEUtdmFsdWVzOiBQcm9iYWJpbGl0eSB0aGF0IGEgcmFuZG9tIGNhbmRpZGF0ZSBwZXB0aWRlIHByb2R1Y2VzIGEgaGlnaGVyIHNjb3JlIHRoYW4gdGhlIG9ic2VydmVkIFBTTSBzY29yZS4KCgotIFByb2JhYmlsaXR5IHRoYXQgYSByYW5kb20gY2FuZGlkYXRlIHBlcHRpZGUgcHJvZHVjZXMgYSBoaWdoZXIgc2NvcmUgdGhhdCB0aGUgb2JzZXJ2ZWQgUFNNIHNjb3JlIGZvciBhIHJlYWwgc2VhcmNoIHdpdGggT01TU0EuCgpgYGB7cn0KbGlicmFyeShUYXJnZXREZWNveSkKbGlicmFyeSh0aWR5dmVyc2UpCmRhdGEoIk1vZFN3aXNzWFQiKQpobHAgPC0gVGFyZ2V0RGVjb3k6OjouZ2V0REYoTW9kU3dpc3NYVCkgCm5hbWVzKGhscCkgPC0gbmFtZXMoaGxwKSAlPiUKICAgIHN0cl9yZXBsYWNlKHBhdHRlcm49IjoiLHJlcGxhY2VtZW50PSJfIikKaGxwIDwtIGhscCAlPiUgCm11dGF0ZShvbXNzYV9ldmFsdWU9YXMuZG91YmxlKG9tc3NhX2V2YWx1ZSkpCmhscCAlPiUgCiAgICBmaWx0ZXIoIWlzLm5hKG9tc3NhX2V2YWx1ZSkpICU+JSAKICAgIGdncGxvdChhZXMob21zc2FfZXZhbHVlKSkgKwogICAgZ2VvbV9oaXN0b2dyYW0oYnJlYWtzPXNlcSgwLDEwMCw1KSkgKwogICAgeGxhYigiRS12YWx1ZSAoJSkiKSArIAogICAgeWxhYigiZnJlcXVlbmN5IikKYGBgCgotIEUtdmFsdWUgd2UgZXhwZWN0IGZvciBhbiBvYnNlcnZlZCBzcGVjdHJ1bSBhbmQgYSByYW5kb20gY2FuZGlkYXRlIHBlcHRpZGUuCgpgYGB7cn0KZGF0YS5mcmFtZShldmFsdWU9cnVuaWYoMjAwMDAsMCwxMDApKSAlPiUKICAgIGdncGxvdChhZXMoZXZhbHVlKSkgKwogICAgZ2VvbV9oaXN0b2dyYW0oYnJlYWtzPXNlcSgwLDEwMCw1KSkgKwogICAgeGxhYigiRS12YWx1ZSAoJSkiKSArIAogICAgeWxhYigiZnJlcXVlbmN5IikKYGBgCgotIFByb2JhYmlsaXR5IHRoYXQgYSByYW5kb20gY2FuZGlkYXRlIHBlcHRpZGUgcHJvZHVjZXMgYSBoaWdoZXIgc2NvcmUgdGhhbiB0aGUgb2JzZXJ2ZWQgUFNNIHNjb3JlIGZvciBkZWNveXMgaW4gYSByZWFsIHNlYXJjaCB3aXRoIE9NU1NBLgoKYGBge3J9CmhscCAlPiUgCiAgICBmaWx0ZXIoaXNkZWNveSAmICFpcy5uYShvbXNzYV9ldmFsdWUpKSAlPiUgCiAgICBnZ3Bsb3QoYWVzKG9tc3NhX2V2YWx1ZSkpICsKICAgIGdlb21faGlzdG9ncmFtKGJyZWFrcz1zZXEoMCwxMDAsNSkpICsKICAgIHhsYWIoIkUtdmFsdWUgKCUpIikgKyAKICAgIHlsYWIoImZyZXF1ZW5jeSIpCmBgYAoKLSBBIGJhZCBoaXQgaXMgdGhlIHJhbmRvbSBoaXQgd2l0aCB0aGUgYmVzdCBzY29yZSBzbyBpdCBpcyBhbHNvIGJvdW5kIHRvIGhhdmUgYSBsb3cgRS12YWx1ZS4KCi0gSWYgd2UgbG9vayBhdCBFLXZhbHVlcyBmb3IgYWxsIFBTTXMgdGhleSBhcmUgb25seSB1c2VmdWwgYXMgYSBzY29yZS4KCi0gV2Ugc2hvdWxkIGtub3cgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGUgbWF4aW11bSBzY29yZSBvZgpyYW5kb20gY2FuZGlkYXRlIHBlcHRpZGVzIHdoZW4gd2Ugd2FudCB0byBkbyB0aGUgc3RhdGlzdGljcy4KCmBgYHtyIHdhcm5pbmc9RkFMU0V9CmhscCAlPiUgCiAgICBmaWx0ZXIoIWlzLm5hKG9tc3NhX2V2YWx1ZSkpICU+JSAKICAgIGdncGxvdChhZXMoLWxvZzEwKG9tc3NhX2V2YWx1ZSkpKSArCiAgICBnZW9tX2hpc3RvZ3JhbShicmVha3M9c2VxKDAsMTAwMCwuNSkpICsKICAgIHhsYWIoIi1sb2cxMChFLXZhbHVlKSIpICsgCiAgICB5bGFiKCJmcmVxdWVuY3kiKSArCiAgICB4bGltKDAsMjApIApgYGAKCiMgQ29uY2VwdHMKCiMjIEJhc2ljIFN0YXRpc3RpY2FsIENvbmNlcHRzCgpgYGB7cn0KbmFtZXMoaGxwKSA8LSBuYW1lcyhobHApICU+JSBzdHJfcmVwbGFjZShwYXR0ZXJuPSItIixyZXBsYWNlbWVudCA9ICJfIikKaGxwIDwtIGhscCAlPiUgbXV0YXRlKG1zX2dmX3NwZWNldmFsdWU9YXMuZG91YmxlKG1zX2dmX3NwZWNldmFsdWUpKQpgYGAKCmBgYHtyfQpsaWJyYXJ5KG1nY3YpCmRlYyA8LSAtbG9nMTAoaGxwJG1zX2dmX3NwZWNldmFsdWVbaGxwJGlzZGVjb3ldKSAlPiUgbmEuZXhjbHVkZSgpCnRhciA8LSAtbG9nMTAoaGxwJG1zX2dmX3NwZWNldmFsdWVbIWhscCRpc2RlY295XSkgJT4lIG5hLmV4Y2x1ZGUoKQoKYnJlYWtzIDwtIHNlcSgwLDMwLC41KQojYmluV2lkdGggPC0yCiNicmVha3MgPC0gc2VxKGZsb29yKG1pbihjKGRlYyx0YXIpKS9iaW5XaWR0aCkqYmluV2lkdGgsY2VpbGluZyhtYXgoYyhkZWMsdGFyKSkvYmluV2lkdGgpKmJpbldpZHRoLGJpbldpZHRoKQojY29kZSBpZiB3ZSByZWdpc3RlciB0aGUgbW9kZXMgYnkgc3Vic3RyYWN0aW5nIHRoZSBtb2RlIGZyb20gdGhlIHRhcmdldCBzY29yZXMgYW5kICB0aGUgZGVjb3kgc2NvcmVzLgojYnJlYWtzPXNlcSgtKGNlaWxpbmcoYWJzKG1pbihjKGRlYyx0YXIpKS9iaW5XaWR0aCkpKy41KSpiaW5XaWR0aCwoY2VpbGluZyhtYXgoYyhkZWMsdGFyKSkvYmluV2lkdGgpKy41KSpiaW5XaWR0aCxiaW5XaWR0aCkKaGlzdERlYyA8LSBoaXN0KGRlYyxicmVha3M9YnJlYWtzLHBsb3QgPSBGQUxTRSkKaGlzdFRhciA8LSBoaXN0KHRhcixicmVha3M9YnJlYWtzLHBsb3Q9RkFMU0UpCmhpc3RTYW0gPC0gaGlzdChjKGRlYyx0YXIpLGJyZWFrcz1icmVha3MsIHBsb3QgPSBGQUxTRSkKCmdyaWQ8LXNlcSgwLDMwLC4xKQpjb3VudHNUYXJHPC1kYXRhLmZyYW1lKHk9aGlzdFRhciRjb3VudHMtaGlzdERlYyRjb3VudHMseD1oaXN0VGFyJG1pZHMpCmNvdW50c1RhckckeVtjb3VudHNUYXJHJHk8MF08LTAKZml0VGFyRzwtZ2FtKHl+cyh4KSxkYXRhPWNvdW50c1RhckcsZmFtaWx5PXBvaXNzb24pCmZpdFRhckdyaWQ8LWV4cChwcmVkaWN0KGZpdFRhckcsbmV3ZGF0YT1kYXRhLmZyYW1lKHg9Z3JpZCkpKQoKY291bnRzRGVjPC1kYXRhLmZyYW1lKHk9aGlzdERlYyRjb3VudHMseD1oaXN0RGVjJG1pZHMpCmZpdERlYzwtZ2FtKHl+cyh4KSxkYXRhPWNvdW50c0RlYyxmYW1pbHk9cG9pc3NvbikKZml0U2FtQmFkPC1leHAocHJlZGljdChmaXREZWMsbmV3ZGF0YT1kYXRhLmZyYW1lKHg9Z3JpZCkpKSoyCgpwbG90KGhpc3RTYW0seGxhYj0iTVMtR0YrIFNjb3JlIix5bGFiPSIjIFBTTXMiLG1haW49IlB5cm9jb2NjdXMgU2VhcmNoIixib3JkZXI9IndoaXRlIixjb2w9ImdyZXkiLGNleC5heGlzPTEuNSxjZXgubWFpbj0xLjUsY2V4LmxhYj0xLjUseWxpbT1jKDAsMTUwMCksIGF4ZXMgPUZBTFNFKQpheGlzKHNpZGU9MixhdD1jKDAsNzUwLDE1MDApKQpheGlzKHNpZGU9MSxhdD1jKDAsMTAsMjAsMzApKQpsaW5lcyhncmlkLGZpdFNhbUJhZCtmaXRUYXJHcmlkLGNvbD0iYmxhY2siLGx3ZD0yKQpsaW5lcyhncmlkLGZpdFNhbUJhZCxjb2w9IiNGRjk5MDAiLGx3ZD0yKQpsaW5lcyhncmlkLGZpdFRhckdyaWQsY29sPSIjMDA5OTAwIixsd2Q9MikKYGBgCgpMZXQgJHgkIGJlIHRoZSBQU00gc2NvcmUgCgpUaGUgc2NvcmVzIHdpbGwgZm9sbG93IGEgbWl4dHVyZSBkaXN0cmlidXRpb246CgokJGYoeCkgPSBccGlfYiBcbWF0aGJpbntcY29sb3J7b3JhbmdlfXsgZl9iKHgpfX0rKDEtXHBpX2IpXG1hdGhiaW57XGNvbG9ye2dyZWVufXsgZl9nKHgpfX0sJCQKVGhlIGxvY2FsIGZkciBpcyBhbHNvIHJlZmVycmVkIHRvIGFzIHRoZSBwb3N0ZXJpb3IgZXJyb3IgcHJvYmFiaWxpdHkgKFBFUCksIGFuZCBpcyB0aGUgcHJvYmFiaWxpdHkgdGhhdCBhIFBTTSB3aXRoIGEgc2NvcmUgJHgkIGlzIGEgYmFkIGhpdC4gCgokJApcYmVnaW57YXJyYXl9e2xjbH0KXHRleHR7bGZkcn0oeCkgJj0mXHRleHR7UEVQKHgpfVxcCiY9JiBcdGV4dHtQfVtcdGV4dHtiYWQgaGl0fSBcdmVydCBYPXhdXFwKJj0mIFxmcmFje1xwaV9iIGZfYih4KX17Zih4KX0KXGVuZHthcnJheX0KJCQKCgpXZSB3aWxsIHJldHVybiBhIGxpc3Qgb2YgUFNNcyBieSB1c2luZyB0aGUgRkRSOgoKJCRGRFIgPSBFXGxlZnRbXGZyYWN7RlB9e1RQICsgRlB9XHJpZ2h0XSQkCgotIEZQOiBudW1iZXIgb2YgZmFsc2UgcG9zaXRpdmVzLCBiYWQgaGl0cwotIFRQOiBudW1iZXIgb2YgdHJ1ZSBwb3NpdGl2ZXMsIGdvb2QgaGl0cwoKJCQKXGJlZ2lue2FycmF5fXtsY2x9Clx0ZXh0e0ZEUn0oeFxnZXEgdCkgJj0mClx0ZXh0e1B9W1x0ZXh0e2JhZCBoaXQgfVx2ZXJ0IFggXGdlcSB0XVxcXFwKJj0mXGZyYWN7XHBpX2JcaW50XGxpbWl0c197eD10fV57K1xpbmZ0eX1mX2IoeCl9e1xpbnRcbGltaXRzX3t4PXR9XnsrXGluZnR5fWYoeCl9XFxcXAomPSZcZnJhY3tccGlfYlsxLUZfYih0KV19ezEtRih0KX0KXGVuZHthcnJheX0KJCQKCgoKCi0gU28gdGhlIEZEUiBpcyBhIHNldCBwcm9wZXJ0eSwgaXQgbWVhc3VyZSB0aGUgcHJvYmFiaWxpdHkgb24gYSBiYWQgaGl0IGluIHRoZSBzZXQgb2YgUFNNcyB3aXRoIHNjb3JlcyAkWFxnZXEgdCQuCgpPdXIgbGlzdCwgdGh1cyBjb25zaXN0cyBvZiBhbGwgUFNNcyB3aXRoIGEgc2NvcmUgJHgkIGFib3ZlIGEgdGhyZXNob2xkIHQuIAoKYGBge3Igd2FybmluZz1GQUxTRX0KcGxvdChoaXN0U2FtLHhsYWI9Ik1TLUdGKyBTY29yZSIseWxhYj0iIyBQU01zIixtYWluPSJQeXJvY29jY3VzIFNlYXJjaCIsYm9yZGVyPSJ3aGl0ZSIsY29sPSJncmV5IixjZXguYXhpcz0xLjUsY2V4Lm1haW49MS41LGNleC5sYWI9MS41LHlsaW09YygwLDE1MDApLCBheGVzID1GQUxTRSkKYXhpcyhzaWRlPTIsYXQ9YygwLDc1MCwxNTAwKSkKYXhpcyhzaWRlPTEsYXQ9YygwLDEwLDIwLDMwKSkKbGluZXMoZ3JpZCxmaXRTYW1CYWQrZml0VGFyR3JpZCxjb2w9ImJsYWNrIixsd2Q9MikKbGluZXMoZ3JpZCxmaXRTYW1CYWQsY29sPSIjRkY5OTAwIixsd2Q9MikKbGluZXMoZ3JpZCxmaXRUYXJHcmlkLGNvbD0iIzAwOTkwMCIsbHdkPTIpCnRleHQocG9zPTQsNSwxNDMwLGxhYmVsPWV4cHJlc3Npb24oeCA+PSB0KSxjb2w9ImRhcmtvcmNoaWQ0IixjZXg9MikKcmVjdCg1LC0xMCwzMCwxNTAwLGx3ZD0yLGJvcmRlcj0iZGFya29yY2hpZDQiKQpgYGAKCgotIFNvIHdlIGtub3cgaG93IG1hbnkgUFNNcyB3ZSByZXR1cm4sIGkuZS4gVFAgKyBGUDogCiQkXHRleHR7I1BTTXMgd2l0aCB9IHggXGdlcSB0JCQgCgphbmQgd2UgY2FuIGFsc28gZXN0aW1hdGUgdGhlIHByb2JhYmlsaXR5IG9uIGEgUFNNIGFib3ZlIHRoZSB0aHJlc2hvbGQgZW1waXJpY2FsbHk6CgokJDEtXGhhdHtcdGV4dHtGfX0odCkgPSBcZnJhY3tcdGV4dHsjUFNNcyB3aXRoIH0geCBcZ2VxIHR9e1x0ZXh0eyNQU01zfX0kJCAKCgoKCi0gU28gdG8gZXN0aW1hdGUgdGhlIEZEUiB3ZSBvbmx5IGhhdmUgdG8gZXN0aW1hdGUgdGhlIGV4cGVjdGVkIG51bWJlciBvZiBQU01zIHRoYXQgYXJlIGJhZCBoaXRzIHdpdGggYSBzY29yZSAkeCQgYWJvdmUgdGhlIHRocmVzaG9sZCAkdCQuCgokJFx3aWRlaGF0e1x0ZXh0e0ZEUn19KHQpID0gXGZyYWN7RVxsZWZ0W1wjXHRleHR7QmFkIFBTTSBoaXRzIHdpdGggfSBYIFxnZXEgdFxyaWdodF19e1x0ZXh0eyNQU01zIHdpdGggfSB4IFxnZXEgdH0kJAoKCiMjIENvbXBldGl0aXZlIHRhcmdldCBkZWNveSBhcHByb2FjaAoKLSBTZWFyY2ggYWdhaW5zdCBkZWNveSBkYXRhYmFzZSB0byBnZW5lcmF0ZSByZXByZXNlbnRhdGl2ZSBiYWQgaGl0cwotIFJldmVyc2UgZGF0YWJhc2UgaXMgcG9wdWxhcgotIENvbmNhdGVuYXRlZCBzZWFyY2ggaXMgbW9zdCBwb3B1bGFyCi0gQWR2YW50YWdlLCBhIG51bWJlciBvZiBiYWQgaGl0cyBhbHJlYWR5IG1hdGNoZXMgd2l0aCBkZWNveXMKCiRccmlnaHRhcnJvdyQgd2Uga25vdyB0aGF0IHRoZXNlIGFyZSBiYWQgaGl0cwoKJFxyaWdodGFycm93JCB3ZSBoYXZlIHRvIGluZmVyIG9uIGxlc3MgdGFyZ2V0IFBTTXMuCgpgYGB7ciB3YXJuaW5nPUZBTFNFfQpobHAgJT4lIAogICAgZmlsdGVyKCFpcy5uYShtc19nZl9zcGVjZXZhbHVlKSkgJT4lIAogICAgZ2dwbG90KGFlcygtbG9nMTAobXNfZ2Zfc3BlY2V2YWx1ZSkpKSArCiAgICBnZW9tX2hpc3RvZ3JhbShicmVha3M9c2VxKDAsMTAwMCwuNSkpICsKICAgIHhsYWIoIi1sb2cxMChFLXZhbHVlKSIpICsgCiAgICB5bGFiKCJmcmVxdWVuY3kiKSArCiAgICB4bGltKDAsNDApICsKICAgIGZhY2V0X2dyaWQoaXNkZWNveX4uKQpgYGAKCldlIGVzdGltYXRlIHRoYXQgYnkgdXNpbmcgdGhlIGRlY295czogCiQkXHRleHR7IyBEZWNveXMgd2l0aCB4fSBcZ2VxIHQkJApTbyBvdXIgZXN0aW1hdGVkIEZEUiBiZWNvbWVzIAoKJCRcd2lkZWhhdHtcdGV4dHtGRFJ9fSh4XGdlcSB0KSA9IFxmcmFje1x0ZXh0eyMgRGVjb3lzfSBcZ2VxIHR9e1x0ZXh0eyMgVGFyZ2V0c30gXGdlcSB0fSQkCklmIHdlIHJld3JpdGUgdGhlIEZEUiB3ZSBjYW4gc2VlIHRoZSBUREEgYXNzdW1wdGlvbnM6CgokJApcYmVnaW57YXJyYXl9e2xjbH0KXHdpZGVoYXR7XHRleHR7RkRSfX0oeFxnZXEgdCkmPSZcZnJhY3tcdGV4dHsjIERlY295c30gXGdlcSB0fXtcdGV4dHsjIFRhcmdldHN9IFxnZXEgdH1cXFxcCiY9JlxmcmFje1xmcmFje1x0ZXh0eyMgRGVjb3lzfX17XHRleHR7IyBUYXJnZXRzfX1cZnJhY3tcdGV4dHsjIERlY295cyB3aXRoIH14IFxnZXEgdH17XHRleHR7IyBEZWNveXN9fX17XGZyYWN7XHRleHR7IyBUYXJnZXRzIHdpdGggfSB4IFxnZXEgdH17XHRleHR7IyBUYXJnZXRzfX19XFxcXAomPSZcZnJhY3tcZnJhY3tcaGF0e1x0ZXh0e0V9fVxsZWZ0W1x0ZXh0eyMgQmFkIFRhcmdldHN9XHJpZ2h0XX17XHRleHR7IyBUYXJnZXRzfX1cZnJhY3tcaGF0e1x0ZXh0e0V9fVxsZWZ0W1x0ZXh0eyMgQmFkIFRhcmdldHMgd2l0aCB9WCBcZ2VxIHRccmlnaHRdfXtcaGF0e1x0ZXh0e0V9fVxsZWZ0W1x0ZXh0eyMgQmFkIHRhcmdldHN9XHJpZ2h0XX19e1xmcmFje1x0ZXh0eyMgVGFyZ2V0cyB3aXRoIH0geCBcZ2VxIHR9e1x0ZXh0eyMgVGFyZ2V0c319fSBcXFxcCiY9JlxmcmFje1xoYXR7XHRleHR7UH19XGxlZnRbXHRleHR7QmFkICBUYXJnZXR9IFxyaWdodF1cdGltZXNcaGF0e1x0ZXh0e1B9fVxsZWZ0W1x0ZXh0e0JhZCAgVGFyZ2V0fVx2ZXJ0IFggXGdlcSB0IFxyaWdodF19e1xoYXR7XHRleHR7UH19XGxlZnRbXHRleHR7VGFyZ2V0fVx2ZXJ0IFggXGdlcSB0XHJpZ2h0XX1cXFxcCiY9JlxmcmFje1xoYXRccGlfYiBbMS1caGF0e0Z9X2IodCldfXsxLVxoYXR7Rn0odCl9ClxlbmR7YXJyYXl9CiQkCgoKU28gdGhlIFREQSBoYXMgdGhlIGZvbGxvd2luZyBhc3N1bXB0aW9uczogCgoxLiBBIGJhZCBoaXQgaXMgZXF1YWxseSBsaWtlbHkgdG8gbWF0Y2ggdG8gYSBkZWNveSBhcyB0byBhIHRhcmdldCBzZXF1ZW5jZS4gJFxyaWdodGFycm93JCB3ZSBjYW4gdGh1cyBlc3RpbWF0ZSB0aGUgZnJhY3Rpb24gb2YgYmFkIGhpdHMgb3IgdGhlIHByb2JhYmlsaXR5IG9uIGEgYmFkIGhpdCBhcyAkJFxoYXR7XHBpfV9iID0gXGZyYWN7XCMgXHRleHR7RGVjb3lzfX17XCMgXHRleHR7VGFyZ2V0c319JCQKCjIuIEJhZCB0YXJnZXQgUFNNIHNjb3JlcyBhbmQgZGVjb3kgUFNNIHNjb3JlcyBhcmUgZXF1YWx5IGRpc3RyaWJ1dGVkLgoKIyBEaWFnbm9zdGljIHBsb3RzIGZvciB0aGUgVERBCgpXZSB3aWxsIGV2YWx1YXRlIHRoZSBUREEgYXNzdW1wdGlvbnMgdXNpbmcgZGlhZ25vc3RpYyBwbG90cyB0aGF0IGNvbXBhcmVzIHRoZSBlbXBpcmljYWwgZGlzdHJpYnV0aW9uIG9mIGRlY295IGFuZCB0YXJnZXQgUFNNIHNjb3Jlcy4gCgotIEhpc3RvZ3JhbXMKLSBQLVAgcGxvdHMKCgpXaXRoIFAtUCBwbG90cyB3aWxsIHBsb3QgZm9yIGVhY2ggb2JzZXJ2ZWQgUFNNIHNjb3JlICR0JCB0aGUgZW1waXJpY2FsIHByb2JhYmlsaXR5IHRvIG9ic2VydmUgYSBkZWNveSB3aXRoIHNjb3JlICR4IFxsZXEgdCQgdG8gdGhlIGVtcGlyaWNhbCBwcm9iYWJpbGl0eSB0byBvYnNlcnZlIGEgdGFyZ2V0IHdpdGggc2NvcmUgJHggXGxlcSB0JDogCgokJFxoYXR7XHRleHR7UH19XGxlZnRbXHRleHR7ZGVjb3kgd2l0aCBzY29yZSB9IHggXGxlcSB0XHJpZ2h0XSA9IFxmcmFje1wjIFx0ZXh0e2RlY295cyB3aXRoIHNjb3JlIH0geCBcbGVxIHR9e1wjIFx0ZXh0e2RlY295c319JCQKCiQkClxoYXR7XHRleHR7UH19XGxlZnRbXHRleHR7dGFyZ2V0IHdpdGggc2NvcmUgfSB4IFxsZXEgdFxyaWdodF0gPSBcZnJhY3tcIyBcdGV4dHt0YXJnZXQgd2l0aCBzY29yZSB9IHggXGxlcSB0fXtcIyBcdGV4dHt0YXJnZXRzfX0KJCQKCklmIHRoZSB0d28gZGlzdHJpYnV0aW9ucyBhcmUgdGhlIHNhbWUgdGhlIGRvdHMgb2YgdGhlIFAtUCBwbG90IHNob3VsZCBmb2xsb3cgdGhlIDEtMSBsaW5lLiAKClRoaXMgd2lsbCBub3QgYmUgdGhlIGNhc2UuIFdlIGV4cGVjdCB0aGUgZGlzdHJpYnV0aW9uIG9mIHRoZSB0YXJnZXQgUFNNczogCgotIHRvIGJlIHNpbWlsYXJseSBkaXN0cmlidXRlZCBhcyB0aGUgZGVjb3lzIGZvciBsb3cgc2NvcmVzCi0gYW5kIGVucmljaGVkIHdpdGggbWFueSBoaWdoIHNjb3JlcyBjb3JyZXNwb25kaW5nIHRvIHRhcmdldCBQU01zIHdoaWNoIGFyZSBtYXRjaGluZyB0byB0aGUgcHJvcGVyIHBlcHRpZGUgc2VxdWVuY2UgaW4gdGhlIGRhdGEgYmFzZS4KCgpgYGB7cn0KaGxwIDwtIGhscCAlPiUgCiAgbXV0YXRlKHNjb3JlPS1sb2cxMChobHAkbXNfZ2Zfc3BlY2V2YWx1ZSkpCgp5MTwtaGxwJHNjb3JlW2hscCRpc2RlY295XSAlPiUgbmEuZXhjbHVkZQp5MjwtaGxwJHNjb3JlWyFobHAkaXNkZWNveV0gJT4lIG5hLmV4Y2x1ZGUKRjE8LWVjZGYoeTEpCkYyPC1lY2RmKHkyKQpicmVha3M8LXNlcShmbG9vcihtaW4oYyh5MSx5MikpKSxjZWlsaW5nKG1heChjKHkxLHkyKSkpLGxlbmd0aC5vdXQ9NTApCnBpMDwtbGVuZ3RoKHkxKS9sZW5ndGgoeTIpCmZvciAoeCBpbiBxdWFudGlsZShjKHkxLHkyKSxjKDAsLjAxLC4wMixzZXEoMC4xLDEsLjEpKSkpCnsKcGFyKG1mcm93PWMoMSwyKSkKaGlzdCh5MixicmVha3M9YnJlYWtzLG1haW49IlB5cm9jb2NjdXMgTVNHRisiLGNleC5heGlzPTEuNSxjZXgubGFiPTEuNSxjZXgubWFpbj0xLjUsY29sPSJncmV5IikKZGVjSGlzdDwtaGlzdCh5MSxicmVha3M9YnJlYWtzLHBsb3Q9RkFMU0UpCnBvaW50cyhkZWNIaXN0JG1pZHMsZGVjSGlzdCRjb3VudHMsY29sPSIjRkY5OTAwIix0eXBlPSJoIixsd2Q9MikKYWJsaW5lKHY9eCxjb2w9ImJsdWUiLGx3ZD0yKQpwbG90KEYxKHkyKSxGMih5MikseGxhYj0iRUNERiBUYXJnZXRzIix5bGFiPSJFQ0RGIERlY295cyIsY2V4PS40LG1haW49IlAtUCBwbG90IixjZXguYXhpcz0xLjUsY2V4LmxhYj0xLjUsY2V4Lm1haW49MS41LGNvbD0iZ3JleSIscGNoPTE5KQphYmxpbmUoYT0wLGI9cGkwKQphYmxpbmUodj1GMSh4KSxjb2w9ImJsdWUiKQphYmxpbmUoaD1GMih4KSxjb2w9ImJsdWUiKQpwb2ludHMoRjEoeCksRjIoeCksY29sPSJibHVlIixjZXg9MixwY2g9MTkpCn0KYGBgCgpOb3RlLCB0aGF0IAoKLSB0aGUgcG9pbnRzIGNvcnJlc3BvbmRpbmcgdG8gbG93IHNjb3JlIHZhbHVlcyBmb2xsb3cgYSBzdHJhaWdodCBsaW5lIGluZGljYXRpbmcgdGhhdCB0YXJnZXRzIGFuZCBkZWNveXMgc2NvcmVzIGFyZSBzaW1pbGFybHkgZGlzdHJpYnV0ZWQgCi0gdGhpcyBsaW5lIGhhcyBhbiBhbmdsZSBlcXVhbCB0byBmcmFjdGlvbiBvZiBiYWQgaGl0cywgd2hpY2ggaXMgZXN0aW1hdGVkIGFzICRcaGF0XHBpX2I9XGZyYWN7XCMgXHRleHR7ZGVjb3lzfX17XCNcdGV4dHt0YXJnZXRzfX0kIGFuZCBpcyBpbmRpY2F0ZWQgYnkgdGhlIGJsYWNrIGxpbmUgaW4gdGhlIHBsb3QuIAotIFRoaXMgbGluZSBjYW4gYmUgdXNlZCB0byBhc3Nlc3MgdGhlIGFzc3VtcHRpb24gdGhhdCBiYWQgaGl0cyBhcmUgZXF1YWxseSBsaWtlbHkgbWF0Y2hpbmcgdG8gdGFyZ2V0cyBzZXF1ZW5jZXMgYXMgdG8gZGVjb3kgc2VxdWVuY2VzLiA=