This notebook will (attempt) to demonstrate the statistical concepts behind the “generative” model employed by the proDA package to account for “dropout” zeros/NAs in proteomics data. Following the mathematics included in the paper, we will simulate a dataset that exactly matches the statistical, distributional expectations of the proDA model, and then examine how the model actually performs with said simulated data. Along the way, we will gain better understanding of what proDA is attempting to accomplish, and how it does so …

library(ggplot2)
library(extraDistr)
library(LaplacesDemon)

Attaching package: ‘LaplacesDemon’

The following objects are masked from ‘package:extraDistr’:

    dbern, dcat, ddirichlet, dgpd, dgpois, dinvchisq, dinvgamma, dlaplace, dpareto, pbern,
    plaplace, ppareto, qbern, qcat, qlaplace, qpareto, rbern, rcat, rdirichlet, rgpd, rinvchisq,
    rinvgamma, rlaplace, rpareto
library(reshape2)
library(plyr)
library(ggbeeswarm)
library(proDA)
library(limma)

To fully (?) understand the proDA generative model, we will work backwards from the various hyperparameters of parent distributions that define the sharing of means and variances across peptides, and (eventually) simulate a phony dataset. This, in essence, is exactly what a generative model represents: an attempt to represent real data via a simulation, which is governed by definable probability distributions. Remember: “all models are wrong; some happen to be useful” (George Box).

The best way to deal with a generative model is to “work backwards”. Let us begin:

Starting with Appendix equation (4), the variance (squared s.d.), \(\sigma_i^2\), of each peptide \(i\) is sampled from an Inverse Scaled Chi-Squared distribution with hyperparameters \(\tt{df}_0\) and \(\tau_0^2\); the rinvchisq R function expects a parameterization of \(\nu\) (positive shape parameter, same as \(\tt{df}_0\)) and \(\tau\) (positive scale, or in this case the inverse of variance, precision). Let’s see what that looks like for a (really and completely arbitrary) pair of hyperparameters:

set.seed(123) # the well-known "Jackson Five" constant.
npep <- 5000 # number of peptides in our simulation
df_0 <- 20
tau_0 <- 4
var_i <- rinvchisq(npep, df_0, tau_0)
names(var_i) <- paste("fragment", 1:npep, sep="-")
hist(var_i, 100, freq=F)

OK, moving further backwards to Appendix equation (3), this is the so-called “non-standardized” version of the Student’s t-distribution, with three parameters: the “normality” parameter \(\tt{df}_{\tt{loc}}\), the central overall “grand mean” peptide log2 abundance \(\mu_0\), and the variance \(\sigma_0^2\) i.e. range expected of the individual peptide abundances for each peptide in each group, \(\mu_{i,j}\) sampled from this distribution. In the proDA paper, they assume that \(\tt{df}_{\tt{loc}}\) is pre-specified, and is not estimated from the data; in the code the default is 3. Let’s see what that looks like for some (again, somewhat arbitrarily selected) set of hyperparameters:

groups <- c("Ctrl", "TrtA", "TrtB")
ngrp <- length(groups)
df_loc <- 3
mu_0 <- 15
sd_0 <- 1
mu_ij <- matrix(rst(n = npep*ngrp, mu = mu_0, sigma = sd_0^2, nu = df_loc),
                ncol=ngrp)
colnames(mu_ij) <- groups
rownames(mu_ij) <- names(var_i)
names(dimnames(mu_ij)) <- c("peptide", "group")


hist(mu_ij, 100, freq=F, xlim=c(5, 25))

OK, so now we have parent or “background” distributions for the means and variance of each peptide: with different means for the same peptide in different experimental groups, but enforcing (“assuming”) shared/constant variance across all experimental groups for each peptide, i.e. “homoscedasticity” – that’s a really big assumption, and one that real data very often violates. We can return to that problem later.

So now we’re ready to simulate the (latent, unobserved) log2 abundance values, \(z_{i,j}\), for every sample replicate in each group (Appendix equation (1), second line). These are the values we would have observed in the MS if there were absolutely no dropout, in which case every log2 abundance would be measured directly, with some biological (and technical) variation \(\sigma_i^2\).

nrep <- 3 # number of replicates per group
z_ij <- matrix(data = NA, nrow=npep, ncol=0)
for (idx in 1:ncol(mu_ij)) {
  group <- groups[idx]
  reps <- matrix(rnorm(nrep*npep, mean=mu_ij[,idx], sd=sqrt(var_i)),
                 byrow = F,
                 ncol=nrep)
  colnames(reps) <- paste(group, rep(1:nrep), sep="-")
  z_ij <- cbind(z_ij, reps)
}
rownames(z_ij) <- rownames(mu_ij)
names(dimnames(z_ij)) <- c("peptide", "sampleID")
print(head(z_ij), digits=4)
            sampleID
peptide      Ctrl-1 Ctrl-2 Ctrl-3 TrtA-1 TrtA-2 TrtA-3 TrtB-1 TrtB-2 TrtB-3
  fragment-1  11.25  10.90  11.71  17.63  13.65  15.24  18.70  19.56  25.45
  fragment-2  15.87  14.57  12.74  14.09  13.43  12.87  14.80  15.21  13.99
  fragment-3  16.32  18.00  14.57  13.84  21.55  17.57  13.54  13.07  11.32
  fragment-4  19.14  14.26  17.61  13.69  11.30  14.26  13.50  15.92  12.76
  fragment-5  14.95  11.66  13.66  12.74  16.65  16.51  13.15  15.34  13.47
  fragment-6  14.71  16.44  16.37  14.57  13.61  19.26  16.16  14.12  11.93

Just to confirm (sanity check) that we did this right, let’s make a plot of the intended group means, the \(\mu_{i,j}\), with (+/- 2 s.d., from \(\sigma_i^2\)) for each group, and overlay the \(z_{i,j}\) values, for the first handful of fragments:

zs <- melt(z_ij[1:6,], value.name = "abundance") # first six peptides
zs$group <- gsub("(.*?)\\-\\d", "\\1", zs$sampleID)
mus <- melt(mu_ij[1:6,], value.name = "abundance")
mus <- merge(mus, melt(var_i[1:6], value.name = "var"), by.x="peptide", by.y=0)
ggplot(zs, aes(x=group, y=abundance, color=sampleID)) + geom_point() + 
  scale_color_brewer(palette="Set1") +
  geom_crossbar(data=mus, mapping = aes(y=abundance,
                                        ymin=abundance-2*sqrt(var),
                                        ymax=abundance+2*sqrt(var)),
                color=I("black")) +
  facet_wrap(~peptide) + theme_bw()

OK, so with that confirmed, let’s remember that this is a rather idealized dataset where data from every sample comes already “pre-normalized” to all the others. Let’s see what I mean:

ggplot(melt(z_ij, value.name = "abundance"),
       aes(x=sampleID, y=abundance, color=sampleID)) + geom_violin() + 
  scale_color_brewer(palette="Set1") + theme_bw()

In real, “raw” data, the distribution of abundances in each sample would vary a little bit, reflecting differences in sample concentration relative to each other (especially in a multiplexed experiment). Between-sample normalization techniques (quantile normalize, median normalization, etc.) are used to “align” the distributions, but we don’t need to do such a step here. However, as discussed in the proDA paper, real “raw” data does generally need to be normalized, and that normalization step can (and does) introduce distortions when the “lower end” of the abundance distribution has a higher dropout rate, leading to more dropout NAs (or zeroes) – especially when the entire distribution for a given sample is shifted lower, due to an overall lower relative sample abundance (more dropouts/zeroes than in other samples).

OK, now to simulate the dropouts in each sample. The proDA model uses a sigmoid curve (using the complement of the Normal CDF function for convenience, equation 2), with parameters \(\rho_j\) (inflection point) and \(\zeta_j\) (scale) to represent the dropoff probability, at any given abundance value. Let’s look at a few example dropout curves for a couple different parameter choices:

ipnorm <- function(...) {
  1 - pnorm(...)
}
ggplot() +
  stat_function(fun = ipnorm,
                color = "blue",
                args = list(mean = 1, sd = 1)) +
  stat_function(fun = ipnorm,
                color="red",
                args = list(mean=10, sd=5)) +
  geom_vline(xintercept=1, color="blue", linetype=2) +
  geom_vline(xintercept=10, color="red", linetype=2) +
  geom_hline(yintercept=0.5, color="grey", linetype=2) +
  xlim(-10, 30) + xlab("abundance") + ylab("dropout probability") + theme_bw()

From these two example dropout curves, we can see that the inflection point (\(\rho_j\), specified as the mean to pnorm Normal CDF function) is where the probability is exactly 0.5 (see dotted lines); and the scale (\(\zeta_j\), specified as the s.d. to pnorm defines the slope of the curve, how “sharp” the demarcation is between high and low dropout probability over a given range of abundances).

OK, with some intuition in hand now, we can start to reason about how the dropout curves might differ for each sample. In the proDA model, the sample-specific inflection points and scales don’t have any common, shared parent distribution, but are estimated individually for each sample, based on an optimization of the likelihood function for each sample; since there are so many peptide fragments to inform that parameter estimation, that is quite computationally efficient. However, we’ve been entirely probabilistic so far in this simulation, with every value of interest derived as a random draw from some parent distribution, so let’s stick with that theme. Given that we specified the “grand mean” \(\mu_0\) of all peptide abundances at 15, with a “range” \(\sigma_0^2\) of 1, let’s say we set the “average” inflection point (where the dropout probability will be 0.5), \(\rho_0\), over all samples to be \(\mu_0 - 4\sigma_0\), 4 times the SD; and then let each sample’s \(\rho_j\) vary from this by \(\sigma_{\rho_0}^2\). Similarly, let’s let the average scale, \(\zeta_0\) be a hyperparameter, and draw each sample’s \(\zeta_j\) from the InvChiSquare distribution as we did earlier for the analagous peptide variances:

rho_0 <- mu_0 - 4 * sd_0
sigma_rho_0 <- 1.2
rho_j <- rnorm(ncol(z_ij), rho_0, sigma_rho_0)
names(rho_j) <- colnames(z_ij)

zeta_0 <- 10
zeta_tau_0 <- 3
zeta_j <- rinvchisq(ncol(z_ij), zeta_0, zeta_tau_0)
names(zeta_j) <- colnames(z_ij)

ggplot() +
  lapply(1:length(zeta_j), function (i) {
     stat_function(fun = ipnorm,
                   mapping = aes(color=colnames(z_ij)[i]),
                   args = list(mean=rho_j[i], sd=zeta_j[i]))
    }) +
  scale_color_brewer(name="sampleID", palette="Set1") +
  xlim(-10, 30) + xlab("abundance") + ylab("dropout probability") + theme_bw()

OK, great; we’ve got the underlying dropout curves, one for each sample. Now we can simulate which of the \(z_{i,j}\) values will be observed (not a dropout) and which will be “NA” (is a dropout), by flipping a coin with the probability of dropout defined for each \(z_{i,j}\) abundance value given by the dropout curve for sample \(j\). This the “dropout matrix”, \(d_{i,j}\).

d_ij <- z_ij
d_ij[,] <- 0
for (j in 1:ncol(d_ij)) {
  d_ij[,j] <- rbern(n=nrow(d_ij), prob=ipnorm(z_ij[,j], rho_j[j], zeta_j[j]))
}
qplot(rho_j, 100 * colSums(d_ij)/nrow(d_ij), color=zeta_j, size=I(5)) +
  geom_label(aes(label=names(zeta_j))) + 
  scale_y_log10() +
  ylab("% of dropouts in sample") + theme_bw()

From the above, we see that the range of dropout % in each sample correlates with the \(\rho_j\) inflection points – the higher (right-shifted) the inflection point, the more abundance values are lost due to dropout; and also, for a given \(\rho_j\) value, the higher the \(\zeta_j\) value, the shallower the dropoff slope, and so even more abundances dropout.

Now we have everything we need from Appendix equation (1) to produce a final, simulated dataset of experimentally observed abundances, \(y_{i,j}\).

# start by just copying z_ij into y_ij:
y_ij <- z_ij

# now dropout the values according to d_ij:
for (j in 1:ncol(y_ij)) {
  y_ij[d_ij[,j] == 1, j] <- NA
}

print(head(y_ij), digits=4)
            sampleID
peptide      Ctrl-1 Ctrl-2 Ctrl-3 TrtA-1 TrtA-2 TrtA-3 TrtB-1 TrtB-2 TrtB-3
  fragment-1  11.25  10.90     NA  17.63     NA  15.24  18.70  19.56  25.45
  fragment-2  15.87  14.57  12.74  14.09  13.43  12.87  14.80  15.21     NA
  fragment-3  16.32     NA     NA  13.84  21.55  17.57  13.54  13.07  11.32
  fragment-4  19.14  14.26  17.61  13.69     NA  14.26  13.50  15.92     NA
  fragment-5     NA  11.66  13.66  12.74  16.65  16.51  13.15  15.34     NA
  fragment-6  14.71  16.44  16.37  14.57  13.61  19.26  16.16     NA     NA

In the first six fragments above, we can already see a handful of NA’s that represent simulated dropouts.

OK, now we can test proDA using our simulated data, for which we know the true values for each single parameter of interest, and we can see how well proDA is able to recover what we’ve setup:

fit <- proDA(y_ij, design = factor(rep(groups, each=nrep)), verbose=T)
Fitting the hyper-parameters for the probabilistic dropout model.
Starting iter: 1

So, perhaps not too unsurprisingly, when we use proDA to generate a fit with data that exactly matches the modeling expectations, it seems to do a pretty good job! Let’s look at the various hyperparameters first:

hps <- fit$hyper_parameters
knitr::kable(
  data.frame(
    name=c("grand mean",
           "grand range",
           "normality parameter",
           "variance scale",
           "variance shape"),
    values=c(mu_0, sd_0^2, df_loc, tau_0^2, df_0),
    estimate=c(hps$location_prior_mean,
               hps$location_prior_scale,
               hps$location_prior_df, # given as a fixed constant, not estimated
               hps$variance_prior_scale,
               hps$variance_prior_df)), format="pipe")
name values estimate
grand mean 15 15.004408
grand range 1 3.581907
normality parameter 3 3.000000
variance scale 16 3.949251
variance shape 20 18.304869

While the parameters (and random seed) of this notebook could change, at least with the current (?) values, we see that nearly all of the hyperparameters match very well, with the exception of the “grand range”, or \(\sigma_0^2\), which proDA has over-estimated as larger than the true distributed range of group-wise mean values; I expect this is an undue influence of the dropout values themselves, which will tend to “inflate” the expected variance of the group means, at the bottom/left end, even though the top/right end is accurately and explictly represented in the data (this amounts to a suggestion for the proDA authors to consider estimating the variance from a left-truncated distribution of group means, to avoid this artifact).

Next, let’s checkout the dropout curve parameters:

ggplot(rbind(data.frame(variable="dropout position",
                        value=rho_j,
                        estimate=hps$dropout_curve_position,
                        sampleID = names(rho_j)),
             data.frame(variable="dropout scale",
                        value=zeta_j,
                        estimate=-hps$dropout_curve_scale,
                        sampleID = names(zeta_j))),
       aes(x=value, y=estimate)) + 
  geom_abline(slope=1, intercept=0) +
  stat_smooth(method="lm", formula = y ~ x) +
  geom_point(aes(color=sampleID)) +
  facet_wrap(~variable, scales="free") +  theme_bw()

Interesting! While both the location (\(\rho\)) and scale (\(\zeta\)) parameters are very strongly correlated, the location is very slightly under-estimated (though not statistically signicant in this case), while the scale is distinctly and systematically over-estimated in all samples. Off the top of my head, I don’t have any rationale for this behavior, so I’m going to leave this as a question for the authors (!?!)

OK, so most importantly, let’s look at how accurate proDA’s estimation of the group means ended up, in the face of all these dropouts, and (mostly) correct hyperparameter estimations:

coef <- fit$coefficients
names(dimnames(coef)) <- c("peptide", "group")
dtall <- melt(d_ij, value.name = "drop")
dtall$group <- gsub("(.*?)\\-\\d+", "\\1", dtall$sampleID)

dropdf <- ddply(dtall, .(peptide, group), function(dx) {
  data.frame(dropnum = sum(dx$drop))
})

ses <- matrix(unlist(lapply(lapply(fit$coefficient_variance_matrices, diag), sqrt)),
              ncol=3, byrow=T)
colnames(ses) <- colnames(fit$coefficient_variance_matrices[[1]])
ses <- as.data.frame(ses)
ses$peptide <- rownames(fit$coefficients)
ses$lowqt <- qt(0.025, fit$feature_parameters$df)
ses$highqt <- qt(0.975, fit$feature_parameters$df)
cis <- melt(ses,
            id=c("peptide", "highqt", "lowqt"),
            variable.name = "group",
            value.name = "ses")
cis$low <- cis$lowqt * cis$ses
cis$high <- cis$highqt * cis$ses

df <- merge(dropdf, merge(melt(coef, value.name = "estimate"), melt(mu_ij, value.name = "value")))
df <- merge(df, cis)

df$estlow <- df$estimate + df$low
df$esthigh <- df$estimate + df$high
df$estrange <- df$esthigh - df$estlow

ggplot(df, aes(x=value, y=estimate, shape=group, color=factor(dropnum))) +
  geom_point() + geom_abline(slope=1, intercept=0) +
  scale_color_brewer(name="# dropouts", palette="Set2") +
  facet_wrap(~dropnum) + theme_bw()

Again, interesting! When the number of dropouts in a group is 0, 1, or even 2, then proDA does a pretty remarkable job recovering the true value of the group mean. But when all of the replicates in a group are dropouts, we’re left with coefficient estimates that are all ~12. The 95% confidence interval ranges look like this:

ggplot(df, aes(x=estrange, fill=factor(dropnum))) +
  stat_bin(bins=50) + 
  scale_color_brewer(name="# dropouts", palette="Set2") +
  facet_wrap(~dropnum, scales="free_y", ncol=1) + theme_bw() +
  xlab("width of 95% confidence interval")

So, as expected: tighter interval ranges for groups having all observations; wider ranges with less observations, and essentially fixed (wide) interval when no observations are made.

Lastly, let’s see how well proDA was able to estimate variances for each peptide:

vardf <- data.frame(value = var_i,
                    estimate = fit$feature_parameters$s2,
                    missing = ncol(y_ij) - fit$feature_parameters$n_obs)

ggplot(vardf, aes(x=value, y=estimate)) + stat_binhex() +
  geom_abline(slope=1, intercept=0) + theme_bw() +
  xlab("true variance") + ylab("proDA estimated variance")

The majority of variances are correctly estimated, but a handful of peptides having the largest variances remain under-estimated, potentially inflating any downstream contrast statistics.

effects of normalization with dropouts

We observed earlier that the simulated data is already perfectly normalized, at least prior to having random low-abundance values removed due to dropout; and each sample has slightly different dropout curves, so now the distributions may not be so perfectly comparable:

ggplot(melt(y_ij, value.name = "abundance"),
       aes(x=sampleID, y=abundance, color=sampleID)) + geom_violin() + 
  scale_color_brewer(palette="Set1") + theme_bw()

What happens if we quantile normalize the observations, and then compare to their original values (removing/ignoring any difference less than 0.2 units, for graphical clarity)?

normy <- normalizeBetweenArrays(y_ij, method="quantile")
medy <- median_normalization(y_ij)
deltas <- colMeans(medy - y_ij, na.rm=T)
deltas <- data.frame(delta=deltas, sampleID = names(deltas))
ggplot(subset(melt(normy - y_ij, value.name = "delta"),
              abs(delta) >= 0.2),
       aes(x=sampleID, y=delta, color=sampleID)) + geom_quasirandom() + 
  scale_color_brewer(palette="Set1") + theme_bw()

It appears, at least for this simulated dataset that doesn’t have gross differences in mean abundances or dropout rates, that quantile normalization has a large effect on only a small number of fragments. If we refit proDA to this (re)normalized data, we’d expect to see some differences:

normfit <- proDA(normy, design = factor(rep(groups, each=nrep)), verbose=T)
Fitting the hyper-parameters for the probabilistic dropout model.
Starting iter: 1
The inferred parameters are:
location_prior_mean:     14.8
location_prior_scale:    3.99
dropout_curve_position:  10.2, 10.7, 11.2, 9.6, 10.7, 11.5, 9.11, 10, 12.5
dropout_curve_scale:     -4.64, -5.09, -5.11, -4.26, -4.89, -5.46, -3.62, -4.5, -6.11
variance_prior_scale:    4.24
variance_prior_df:       12.8
Error: 0.59

Starting iter: 2
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.73106e-16
The inferred parameters are:
location_prior_mean:     14.8
location_prior_scale:    3.94
dropout_curve_position:  9.9, 10.4, 10.9, 9.33, 10.4, 11.3, 8.88, 9.75, 12.3
dropout_curve_scale:     -4.94, -5.48, -5.52, -4.52, -5.26, -5.96, -3.79, -4.79, -6.75
variance_prior_scale:    4.01
variance_prior_df:       16.3
Error: 0.13

Starting iter: 3
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.50063e-16
The inferred parameters are:
location_prior_mean:     14.9
location_prior_scale:    3.79
dropout_curve_position:  9.76, 10.2, 10.8, 9.2, 10.3, 11.1, 8.79, 9.62, 12.2
dropout_curve_scale:     -5.13, -5.74, -5.83, -4.67, -5.52, -6.32, -3.88, -4.97, -7.28
variance_prior_scale:    3.99
variance_prior_df:       16.8
Error: 0.047

Starting iter: 4
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.3672e-16
The inferred parameters are:
location_prior_mean:     14.9
location_prior_scale:    3.68
dropout_curve_position:  9.7, 10.2, 10.7, 9.17, 10.2, 11, 8.76, 9.57, 12.1
dropout_curve_scale:     -5.24, -5.87, -6, -4.74, -5.66, -6.54, -3.93, -5.07, -7.62
variance_prior_scale:    3.98
variance_prior_df:       17.3
Error: 0.017

Starting iter: 5
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.29826e-16
The inferred parameters are:
location_prior_mean:     15
location_prior_scale:    3.61
dropout_curve_position:  9.68, 10.2, 10.7, 9.15, 10.2, 11, 8.76, 9.54, 12
dropout_curve_scale:     -5.3, -5.95, -6.11, -4.78, -5.75, -6.67, -3.96, -5.13, -7.85
variance_prior_scale:    3.97
variance_prior_df:       17.7
Error: 0.0066

Starting iter: 6
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.26042e-16
The inferred parameters are:
location_prior_mean:     15
location_prior_scale:    3.56
dropout_curve_position:  9.67, 10.1, 10.7, 9.14, 10.2, 11, 8.76, 9.53, 12
dropout_curve_scale:     -5.34, -6.01, -6.19, -4.81, -5.8, -6.76, -3.97, -5.16, -8.01
variance_prior_scale:    3.96
variance_prior_df:       17.9
Error: 0.0032

Starting iter: 7
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.23447e-16
The inferred parameters are:
location_prior_mean:     15
location_prior_scale:    3.52
dropout_curve_position:  9.66, 10.1, 10.6, 9.14, 10.2, 11, 8.76, 9.52, 12
dropout_curve_scale:     -5.37, -6.04, -6.24, -4.82, -5.83, -6.82, -3.98, -5.19, -8.09
variance_prior_scale:    3.96
variance_prior_df:       18.2
Error: 0.0016

Starting iter: 8
Warning in solve.default(coef_hessian) :
  system is computationally singular: reciprocal condition number = 1.21986e-16
Converged!
The inferred parameters are:
location_prior_mean:     15
location_prior_scale:    3.5
dropout_curve_position:  9.66, 10.1, 10.6, 9.14, 10.1, 11, 8.77, 9.52, 12
dropout_curve_scale:     -5.37, -6.07, -6.26, -4.83, -5.86, -6.85, -3.98, -5.19, -8.17
variance_prior_scale:    3.96
variance_prior_df:       18.3
Error: 0.00052
normcoef <- normfit$coefficients
names(dimnames(normcoef)) <- c("peptide", "group")

coefdiff <- normcoef - coef
names(dimnames(coefdiff)) <- c("peptide", "group")
ggplot(subset(melt(coefdiff, value.name = "diff"),
              abs(diff) >= 0.2),
       aes(x=group, y=diff)) + geom_quasirandom() +
  theme_bw() + ylab("difference in coefficient estimates")


ggplot(merge(melt(coefdiff, value.name="diff"),
             melt(coef, value.name="estimate")),
       aes(x=estimate, y=diff)) + geom_point() + facet_wrap(~group) + theme_bw() +
  ylab("difference in coefficient estimates") + xlab("original estimate")

Perhaps as expected, the few coefficients that are grossly affected by the normalization procedure are confined to the (mostly upper) extremes on the abundance scale.

But let’s also look at what happens to the dropout curve parameters:

normhps <- normfit$hyper_parameters
ggplot(rbind(data.frame(variable="dropout position",
                        value=rho_j,
                        estimate=normhps$dropout_curve_position,
                        sampleID = names(rho_j)),
             data.frame(variable="dropout scale",
                        value=zeta_j,
                        estimate=-normhps$dropout_curve_scale,
                        sampleID = names(zeta_j))),
       aes(x=value, y=estimate)) + 
  geom_abline(slope=1, intercept=0) +
  stat_smooth(method="lm", formula = y ~ x) +
  geom_point(aes(color=sampleID)) +
  facet_wrap(~variable, scales="free") +  theme_bw()

The consequence of renormalizing forces the dropout midpoint locations to be uniformly over/under estimated for curves having true lower/higher locations (and the scaling factor remains as before, uniformly over-estimated). These small distortions in dropout curves could have knock-on effects for estimation of coefficients from groups having multiple dropout samples:

normses <- matrix(unlist(lapply(lapply(normfit$coefficient_variance_matrices, diag), sqrt)),
                  ncol=3, byrow=T)
colnames(normses) <- colnames(normfit$coefficient_variance_matrices[[1]])
normses <- as.data.frame(normses)
normses$peptide <- rownames(normfit$coefficients)
normses$lowqt <- qt(0.025, normfit$feature_parameters$df)
normses$highqt <- qt(0.975, normfit$feature_parameters$df)
normcis <- melt(normses,
                id=c("peptide", "highqt", "lowqt"),
                variable.name = "group",
                value.name = "ses")
normcis$low <- normcis$lowqt * normcis$ses
normcis$high <- normcis$highqt * normcis$ses

normdf <- merge(dropdf, merge(melt(normcoef, value.name = "estimate"), melt(mu_ij, value.name = "value")))
normdf <- merge(normdf, normcis)

normdf$estlow <- normdf$estimate + normdf$low
normdf$esthigh <- normdf$estimate + normdf$high
normdf$estrange <- normdf$esthigh - normdf$estlow

ggplot(normdf, aes(x=value, y=estimate, shape=group, color=factor(dropnum))) +
  geom_point() + geom_abline(slope=1, intercept=0) +
  scale_color_brewer(name="# dropouts", palette="Set2") +
  facet_wrap(~dropnum) + theme_bw()

Sample QC using distances

We know that differences in dropout abundances can lead to spurious PCA projections during sample-level QC; proDA provides a probabilistic distance measurement that could avoid such. Let’s explore:

maty <- y_ij
maty[is.na(maty)] <- log2(1)
maty <- normalizeBetweenArrays(maty, method="quantile")

pca <- prcomp(maty, scale=T)
ggplot(as.data.frame(pca$rotation), aes(x=PC1, y=PC2, color=colnames(maty))) +
  geom_point() + theme_bw()


dm <- dist_approx(fit)
heatmap(as.matrix(dm$mean))

While each sample has fewer/more dropouts, the heatmap pairwise sample distances appear uniform within/between each experimental group, as expected.

LS0tCnRpdGxlOiAicHJvREEgbW9kZWwgc2ltdWxhdGlvbiIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKVGhpcyBub3RlYm9vayB3aWxsIChhdHRlbXB0KSB0byBkZW1vbnN0cmF0ZSB0aGUgc3RhdGlzdGljYWwgY29uY2VwdHMgYmVoaW5kIHRoZSAiZ2VuZXJhdGl2ZSIgbW9kZWwgZW1wbG95ZWQgYnkgdGhlIHByb0RBIHBhY2thZ2UgdG8gYWNjb3VudCBmb3IgImRyb3BvdXQiIHplcm9zL05BcyBpbiBwcm90ZW9taWNzIGRhdGEuIEZvbGxvd2luZyB0aGUgbWF0aGVtYXRpY3MgaW5jbHVkZWQgaW4gdGhlIHBhcGVyLCB3ZSB3aWxsIHNpbXVsYXRlIGEgZGF0YXNldCB0aGF0IGV4YWN0bHkgbWF0Y2hlcyB0aGUgc3RhdGlzdGljYWwsIGRpc3RyaWJ1dGlvbmFsIGV4cGVjdGF0aW9ucyBvZiB0aGUgcHJvREEgbW9kZWwsIGFuZCB0aGVuIGV4YW1pbmUgaG93IHRoZSBtb2RlbCBhY3R1YWxseSBwZXJmb3JtcyB3aXRoIHNhaWQgc2ltdWxhdGVkIGRhdGEuIEFsb25nIHRoZSB3YXksIHdlIHdpbGwgZ2FpbiBiZXR0ZXIgdW5kZXJzdGFuZGluZyBvZiB3aGF0IHByb0RBIGlzIGF0dGVtcHRpbmcgdG8gYWNjb21wbGlzaCwgYW5kIGhvdyBpdCBkb2VzIHNvIC4uLgoKYGBge3J9CmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShleHRyYURpc3RyKQpsaWJyYXJ5KExhcGxhY2VzRGVtb24pCmxpYnJhcnkocmVzaGFwZTIpCmxpYnJhcnkocGx5cikKbGlicmFyeShnZ2JlZXN3YXJtKQpsaWJyYXJ5KHByb0RBKQpsaWJyYXJ5KGxpbW1hKQpgYGAKClRvIGZ1bGx5ICg/KSB1bmRlcnN0YW5kIHRoZSBwcm9EQSBnZW5lcmF0aXZlIG1vZGVsLCB3ZSB3aWxsIHdvcmsgYmFja3dhcmRzIGZyb20KdGhlIHZhcmlvdXMgaHlwZXJwYXJhbWV0ZXJzIG9mIHBhcmVudCBkaXN0cmlidXRpb25zIHRoYXQgZGVmaW5lIHRoZSBzaGFyaW5nIG9mCm1lYW5zIGFuZCB2YXJpYW5jZXMgYWNyb3NzIHBlcHRpZGVzLCBhbmQgKGV2ZW50dWFsbHkpIHNpbXVsYXRlIGEgcGhvbnkgZGF0YXNldC4gVGhpcywgaW4gZXNzZW5jZSwgaXMgZXhhY3RseSB3aGF0IGEgZ2VuZXJhdGl2ZSBtb2RlbCByZXByZXNlbnRzOiBhbiBhdHRlbXB0IHRvICByZXByZXNlbnQgcmVhbCBkYXRhIHZpYSBhIHNpbXVsYXRpb24sIHdoaWNoIGlzIGdvdmVybmVkIGJ5IGRlZmluYWJsZSBwcm9iYWJpbGl0eSBkaXN0cmlidXRpb25zLiBSZW1lbWJlcjogImFsbCBtb2RlbHMgYXJlIHdyb25nOyBzb21lIGhhcHBlbiB0byBiZSB1c2VmdWwiIChHZW9yZ2UgQm94KS4KClRoZSBiZXN0IHdheSB0byBkZWFsIHdpdGggYSBnZW5lcmF0aXZlIG1vZGVsIGlzIHRvICJ3b3JrIGJhY2t3YXJkcyIuIExldCB1cyBiZWdpbjoKClN0YXJ0aW5nIHdpdGggQXBwZW5kaXggZXF1YXRpb24gKDQpLCB0aGUgdmFyaWFuY2UgKHNxdWFyZWQgcy5kLiksICRcc2lnbWFfaV4yJCwKb2YgZWFjaCBwZXB0aWRlICRpJCBpcyBzYW1wbGVkIGZyb20gYW4gSW52ZXJzZSBTY2FsZWQgQ2hpLVNxdWFyZWQgZGlzdHJpYnV0aW9uCndpdGggaHlwZXJwYXJhbWV0ZXJzICRcdHR7ZGZ9XzAkIGFuZCAkXHRhdV8wXjIkOyB0aGUgYHJpbnZjaGlzcWAgUiBmdW5jdGlvbiBleHBlY3RzIGEgcGFyYW1ldGVyaXphdGlvbiBvZiAkXG51JCAocG9zaXRpdmUgc2hhcGUgcGFyYW1ldGVyLCBzYW1lIGFzICRcdHR7ZGZ9XzAkKSBhbmQgJFx0YXUkIChwb3NpdGl2ZSBzY2FsZSwgb3IgaW4gdGhpcyBjYXNlIHRoZSBpbnZlcnNlIG9mIHZhcmlhbmNlLCBfcHJlY2lzaW9uXykuIExldCdzIHNlZSB3aGF0IHRoYXQgbG9va3MgbGlrZSBmb3IgYSAocmVhbGx5IGFuZCBjb21wbGV0ZWx5IGFyYml0cmFyeSkgcGFpciBvZiBoeXBlcnBhcmFtZXRlcnM6CgpgYGB7cn0Kc2V0LnNlZWQoMTIzKSAjIHRoZSB3ZWxsLWtub3duICJKYWNrc29uIEZpdmUiIGNvbnN0YW50LgpucGVwIDwtIDUwMDAgIyBudW1iZXIgb2YgcGVwdGlkZXMgaW4gb3VyIHNpbXVsYXRpb24KZGZfMCA8LSAyMAp0YXVfMCA8LSA0CnZhcl9pIDwtIHJpbnZjaGlzcShucGVwLCBkZl8wLCB0YXVfMCkKbmFtZXModmFyX2kpIDwtIHBhc3RlKCJmcmFnbWVudCIsIDE6bnBlcCwgc2VwPSItIikKaGlzdCh2YXJfaSwgMTAwLCBmcmVxPUYpCmBgYApPSywgbW92aW5nIGZ1cnRoZXIgYmFja3dhcmRzIHRvIEFwcGVuZGl4IGVxdWF0aW9uICgzKSwgdGhpcyBpcyB0aGUgc28tY2FsbGVkICJub24tc3RhbmRhcmRpemVkIiB2ZXJzaW9uIG9mIHRoZSBTdHVkZW50J3MgdC1kaXN0cmlidXRpb24sIHdpdGggdGhyZWUgcGFyYW1ldGVyczogdGhlICJub3JtYWxpdHkiIHBhcmFtZXRlciAkXHR0e2RmfV97XHR0e2xvY319JCwgdGhlIGNlbnRyYWwgb3ZlcmFsbCAiZ3JhbmQgbWVhbiIgcGVwdGlkZSBsb2cyIGFidW5kYW5jZSAkXG11XzAkLCBhbmQgdGhlIHZhcmlhbmNlICRcc2lnbWFfMF4yJCBpLmUuIF9yYW5nZV8gZXhwZWN0ZWQgb2YgdGhlIGluZGl2aWR1YWwgcGVwdGlkZSBhYnVuZGFuY2VzIGZvciBlYWNoIHBlcHRpZGUgaW4gZWFjaCBncm91cCwgJFxtdV97aSxqfSQgc2FtcGxlZCBmcm9tIHRoaXMgZGlzdHJpYnV0aW9uLiBJbiB0aGUgcHJvREEgcGFwZXIsIHRoZXkgYXNzdW1lIHRoYXQgJFx0dHtkZn1fe1x0dHtsb2N9fSQgaXMgcHJlLXNwZWNpZmllZCwgYW5kIGlzIG5vdCBlc3RpbWF0ZWQgZnJvbSB0aGUgZGF0YTsgaW4gdGhlIGNvZGUgdGhlIGRlZmF1bHQgaXMgMy4gTGV0J3Mgc2VlIHdoYXQgdGhhdCBsb29rcyBsaWtlIGZvciBzb21lIChhZ2Fpbiwgc29tZXdoYXQgYXJiaXRyYXJpbHkgc2VsZWN0ZWQpIHNldCBvZiBoeXBlcnBhcmFtZXRlcnM6CmBgYHtyfQpncm91cHMgPC0gYygiQ3RybCIsICJUcnRBIiwgIlRydEIiKQpuZ3JwIDwtIGxlbmd0aChncm91cHMpCmRmX2xvYyA8LSAzCm11XzAgPC0gMTUKc2RfMCA8LSAxCm11X2lqIDwtIG1hdHJpeChyc3QobiA9IG5wZXAqbmdycCwgbXUgPSBtdV8wLCBzaWdtYSA9IHNkXzBeMiwgbnUgPSBkZl9sb2MpLAogICAgICAgICAgICAgICAgbmNvbD1uZ3JwKQpjb2xuYW1lcyhtdV9paikgPC0gZ3JvdXBzCnJvd25hbWVzKG11X2lqKSA8LSBuYW1lcyh2YXJfaSkKbmFtZXMoZGltbmFtZXMobXVfaWopKSA8LSBjKCJwZXB0aWRlIiwgImdyb3VwIikKCgpoaXN0KG11X2lqLCAxMDAsIGZyZXE9RiwgeGxpbT1jKDUsIDI1KSkKYGBgCk9LLCBzbyBub3cgd2UgaGF2ZSBwYXJlbnQgb3IgImJhY2tncm91bmQiIGRpc3RyaWJ1dGlvbnMgZm9yIHRoZSBtZWFucyBhbmQgdmFyaWFuY2Ugb2YgZWFjaCBwZXB0aWRlOiB3aXRoIGRpZmZlcmVudCBtZWFucyBmb3IgdGhlIHNhbWUgcGVwdGlkZSBpbiBkaWZmZXJlbnQgZXhwZXJpbWVudGFsIGdyb3VwcywgYnV0IGVuZm9yY2luZyAoImFzc3VtaW5nIikgc2hhcmVkL2NvbnN0YW50IHZhcmlhbmNlIGFjcm9zcyBhbGwgZXhwZXJpbWVudGFsIGdyb3VwcyBmb3IgZWFjaCBwZXB0aWRlLCBpLmUuICJob21vc2NlZGFzdGljaXR5IiAtLSB0aGF0J3MgYSByZWFsbHkgYmlnIGFzc3VtcHRpb24sIGFuZCBvbmUgdGhhdCByZWFsIGRhdGEgdmVyeSBvZnRlbiB2aW9sYXRlcy4gV2UgY2FuIHJldHVybiB0byB0aGF0IHByb2JsZW0gbGF0ZXIuCgpTbyBub3cgd2UncmUgcmVhZHkgdG8gc2ltdWxhdGUgdGhlIChsYXRlbnQsIHVub2JzZXJ2ZWQpIGxvZzIgYWJ1bmRhbmNlIHZhbHVlcywgJHpfe2ksan0kLCBmb3IgZXZlcnkgc2FtcGxlIHJlcGxpY2F0ZSBpbiBlYWNoIGdyb3VwIChBcHBlbmRpeCBlcXVhdGlvbiAoMSksIHNlY29uZCBsaW5lKS4gVGhlc2UgYXJlIHRoZSB2YWx1ZXMgd2Ugd291bGQgaGF2ZSBvYnNlcnZlZCBpbiB0aGUgTVMgaWYgdGhlcmUgd2VyZSBhYnNvbHV0ZWx5IG5vIGRyb3BvdXQsIGluIHdoaWNoIGNhc2UgZXZlcnkgbG9nMiBhYnVuZGFuY2Ugd291bGQgYmUgbWVhc3VyZWQgZGlyZWN0bHksIHdpdGggc29tZSBiaW9sb2dpY2FsIChhbmQgdGVjaG5pY2FsKSB2YXJpYXRpb24gJFxzaWdtYV9pXjIkLgoKYGBge3J9Cm5yZXAgPC0gMyAjIG51bWJlciBvZiByZXBsaWNhdGVzIHBlciBncm91cAp6X2lqIDwtIG1hdHJpeChkYXRhID0gTkEsIG5yb3c9bnBlcCwgbmNvbD0wKQpmb3IgKGlkeCBpbiAxOm5jb2wobXVfaWopKSB7CiAgZ3JvdXAgPC0gZ3JvdXBzW2lkeF0KICByZXBzIDwtIG1hdHJpeChybm9ybShucmVwKm5wZXAsIG1lYW49bXVfaWpbLGlkeF0sIHNkPXNxcnQodmFyX2kpKSwKICAgICAgICAgICAgICAgICBieXJvdyA9IEYsCiAgICAgICAgICAgICAgICAgbmNvbD1ucmVwKQogIGNvbG5hbWVzKHJlcHMpIDwtIHBhc3RlKGdyb3VwLCByZXAoMTpucmVwKSwgc2VwPSItIikKICB6X2lqIDwtIGNiaW5kKHpfaWosIHJlcHMpCn0Kcm93bmFtZXMoel9paikgPC0gcm93bmFtZXMobXVfaWopCm5hbWVzKGRpbW5hbWVzKHpfaWopKSA8LSBjKCJwZXB0aWRlIiwgInNhbXBsZUlEIikKcHJpbnQoaGVhZCh6X2lqKSwgZGlnaXRzPTQpCmBgYAoKSnVzdCB0byBjb25maXJtIChzYW5pdHkgY2hlY2spIHRoYXQgd2UgZGlkIHRoaXMgcmlnaHQsIGxldCdzIG1ha2UgYSBwbG90IG9mIHRoZSBpbnRlbmRlZCBncm91cCBtZWFucywgdGhlICRcbXVfe2ksan0kLCB3aXRoICgrLy0gMiBzLmQuLCBmcm9tICRcc2lnbWFfaV4yJCkgZm9yIGVhY2ggZ3JvdXAsIGFuZCBvdmVybGF5IHRoZSAkel97aSxqfSQgdmFsdWVzLCBmb3IgdGhlIGZpcnN0IGhhbmRmdWwgb2YgZnJhZ21lbnRzOgpgYGB7cn0KenMgPC0gbWVsdCh6X2lqWzE6NixdLCB2YWx1ZS5uYW1lID0gImFidW5kYW5jZSIpICMgZmlyc3Qgc2l4IHBlcHRpZGVzCnpzJGdyb3VwIDwtIGdzdWIoIiguKj8pXFwtXFxkIiwgIlxcMSIsIHpzJHNhbXBsZUlEKQptdXMgPC0gbWVsdChtdV9palsxOjYsXSwgdmFsdWUubmFtZSA9ICJhYnVuZGFuY2UiKQptdXMgPC0gbWVyZ2UobXVzLCBtZWx0KHZhcl9pWzE6Nl0sIHZhbHVlLm5hbWUgPSAidmFyIiksIGJ5Lng9InBlcHRpZGUiLCBieS55PTApCmdncGxvdCh6cywgYWVzKHg9Z3JvdXAsIHk9YWJ1bmRhbmNlLCBjb2xvcj1zYW1wbGVJRCkpICsgZ2VvbV9wb2ludCgpICsgCiAgc2NhbGVfY29sb3JfYnJld2VyKHBhbGV0dGU9IlNldDEiKSArCiAgZ2VvbV9jcm9zc2JhcihkYXRhPW11cywgbWFwcGluZyA9IGFlcyh5PWFidW5kYW5jZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHltaW49YWJ1bmRhbmNlLTIqc3FydCh2YXIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeW1heD1hYnVuZGFuY2UrMipzcXJ0KHZhcikpLAogICAgICAgICAgICAgICAgY29sb3I9SSgiYmxhY2siKSkgKwogIGZhY2V0X3dyYXAofnBlcHRpZGUpICsgdGhlbWVfYncoKQpgYGAKCk9LLCBzbyB3aXRoIHRoYXQgY29uZmlybWVkLCBsZXQncyByZW1lbWJlciB0aGF0IHRoaXMgaXMgYSByYXRoZXIgaWRlYWxpemVkIGRhdGFzZXQgd2hlcmUgZGF0YSBmcm9tIGV2ZXJ5IHNhbXBsZSBjb21lcyBhbHJlYWR5ICJwcmUtbm9ybWFsaXplZCIgdG8gYWxsIHRoZSBvdGhlcnMuIExldCdzIHNlZSB3aGF0IEkgbWVhbjoKYGBge3J9CmdncGxvdChtZWx0KHpfaWosIHZhbHVlLm5hbWUgPSAiYWJ1bmRhbmNlIiksCiAgICAgICBhZXMoeD1zYW1wbGVJRCwgeT1hYnVuZGFuY2UsIGNvbG9yPXNhbXBsZUlEKSkgKyBnZW9tX3Zpb2xpbigpICsgCiAgc2NhbGVfY29sb3JfYnJld2VyKHBhbGV0dGU9IlNldDEiKSArIHRoZW1lX2J3KCkKYGBgCkluIHJlYWwsICJyYXciIGRhdGEsIHRoZSBkaXN0cmlidXRpb24gb2YgYWJ1bmRhbmNlcyBpbiBlYWNoIHNhbXBsZSB3b3VsZCB2YXJ5IGEgbGl0dGxlIGJpdCwgcmVmbGVjdGluZyBkaWZmZXJlbmNlcyBpbiBzYW1wbGUgY29uY2VudHJhdGlvbiByZWxhdGl2ZSB0byBlYWNoIG90aGVyIChlc3BlY2lhbGx5IGluIGEgbXVsdGlwbGV4ZWQgZXhwZXJpbWVudCkuIEJldHdlZW4tc2FtcGxlIG5vcm1hbGl6YXRpb24gdGVjaG5pcXVlcyAocXVhbnRpbGUgbm9ybWFsaXplLCBtZWRpYW4gbm9ybWFsaXphdGlvbiwgZXRjLikgYXJlIHVzZWQgdG8gImFsaWduIiB0aGUgZGlzdHJpYnV0aW9ucywgYnV0IHdlIGRvbid0IG5lZWQgdG8gZG8gc3VjaCBhIHN0ZXAgaGVyZS4gSG93ZXZlciwgYXMgZGlzY3Vzc2VkIGluIHRoZSBwcm9EQSBwYXBlciwgcmVhbCAicmF3IiBkYXRhIF9kb2VzXyBnZW5lcmFsbHkgbmVlZCB0byBiZSBub3JtYWxpemVkLCBhbmQgdGhhdCBub3JtYWxpemF0aW9uIHN0ZXAgY2FuIChhbmQgZG9lcykgaW50cm9kdWNlIGRpc3RvcnRpb25zIHdoZW4gdGhlICJsb3dlciBlbmQiIG9mIHRoZSBhYnVuZGFuY2UgZGlzdHJpYnV0aW9uIGhhcyBhIGhpZ2hlciBkcm9wb3V0IHJhdGUsIGxlYWRpbmcgdG8gbW9yZSBkcm9wb3V0IE5BcyAob3IgemVyb2VzKSAtLSBlc3BlY2lhbGx5IHdoZW4gdGhlIGVudGlyZSBkaXN0cmlidXRpb24gZm9yIGEgZ2l2ZW4gc2FtcGxlIGlzIHNoaWZ0ZWQgbG93ZXIsIGR1ZSB0byBhbiBvdmVyYWxsIGxvd2VyIHJlbGF0aXZlIHNhbXBsZSBhYnVuZGFuY2UgKG1vcmUgZHJvcG91dHMvemVyb2VzIHRoYW4gaW4gb3RoZXIgc2FtcGxlcykuCgpPSywgbm93IHRvIHNpbXVsYXRlIHRoZSBkcm9wb3V0cyBpbiBlYWNoIHNhbXBsZS4gVGhlIHByb0RBIG1vZGVsIHVzZXMgYSBzaWdtb2lkIGN1cnZlICh1c2luZyB0aGUgY29tcGxlbWVudCBvZiB0aGUgTm9ybWFsIENERiBmdW5jdGlvbiBmb3IgY29udmVuaWVuY2UsIGVxdWF0aW9uIDIpLCB3aXRoIHBhcmFtZXRlcnMgJFxyaG9faiQgKGluZmxlY3Rpb24gcG9pbnQpIGFuZCAkXHpldGFfaiQgKHNjYWxlKSB0byByZXByZXNlbnQgdGhlIGRyb3BvZmYgcHJvYmFiaWxpdHksIGF0IGFueSBnaXZlbiBhYnVuZGFuY2UgdmFsdWUuIExldCdzIGxvb2sgYXQgYSBmZXcgZXhhbXBsZSBkcm9wb3V0IGN1cnZlcyBmb3IgYSBjb3VwbGUgZGlmZmVyZW50IHBhcmFtZXRlciBjaG9pY2VzOgpgYGB7cn0KaXBub3JtIDwtIGZ1bmN0aW9uKC4uLikgewogIDEgLSBwbm9ybSguLi4pCn0KZ2dwbG90KCkgKwogIHN0YXRfZnVuY3Rpb24oZnVuID0gaXBub3JtLAogICAgICAgICAgICAgICAgY29sb3IgPSAiYmx1ZSIsCiAgICAgICAgICAgICAgICBhcmdzID0gbGlzdChtZWFuID0gMSwgc2QgPSAxKSkgKwogIHN0YXRfZnVuY3Rpb24oZnVuID0gaXBub3JtLAogICAgICAgICAgICAgICAgY29sb3I9InJlZCIsCiAgICAgICAgICAgICAgICBhcmdzID0gbGlzdChtZWFuPTEwLCBzZD01KSkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD0xLCBjb2xvcj0iYmx1ZSIsIGxpbmV0eXBlPTIpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9MTAsIGNvbG9yPSJyZWQiLCBsaW5ldHlwZT0yKSArCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0PTAuNSwgY29sb3I9ImdyZXkiLCBsaW5ldHlwZT0yKSArCiAgeGxpbSgtMTAsIDMwKSArIHhsYWIoImFidW5kYW5jZSIpICsgeWxhYigiZHJvcG91dCBwcm9iYWJpbGl0eSIpICsgdGhlbWVfYncoKQpgYGAKRnJvbSB0aGVzZSB0d28gZXhhbXBsZSBkcm9wb3V0IGN1cnZlcywgd2UgY2FuIHNlZSB0aGF0IHRoZSBpbmZsZWN0aW9uIHBvaW50ICgkXHJob19qJCwgc3BlY2lmaWVkIGFzIHRoZSBtZWFuIHRvIGBwbm9ybWAgTm9ybWFsIENERiBmdW5jdGlvbikgaXMgd2hlcmUgdGhlIHByb2JhYmlsaXR5IGlzIGV4YWN0bHkgMC41IChzZWUgZG90dGVkIGxpbmVzKTsgYW5kIHRoZSBzY2FsZSAoJFx6ZXRhX2okLCBzcGVjaWZpZWQgYXMgdGhlIHMuZC4gdG8gYHBub3JtYCBkZWZpbmVzIHRoZSBzbG9wZSBvZiB0aGUgY3VydmUsIGhvdyAic2hhcnAiIHRoZSBkZW1hcmNhdGlvbiBpcyBiZXR3ZWVuIGhpZ2ggYW5kIGxvdyBkcm9wb3V0IHByb2JhYmlsaXR5IG92ZXIgYSBnaXZlbiByYW5nZSBvZiBhYnVuZGFuY2VzKS4KCk9LLCB3aXRoIHNvbWUgaW50dWl0aW9uIGluIGhhbmQgbm93LCB3ZSBjYW4gc3RhcnQgdG8gcmVhc29uIGFib3V0IGhvdyB0aGUgZHJvcG91dCBjdXJ2ZXMgbWlnaHQgZGlmZmVyIGZvciBlYWNoIHNhbXBsZS4gSW4gdGhlIHByb0RBIG1vZGVsLCB0aGUgc2FtcGxlLXNwZWNpZmljIGluZmxlY3Rpb24gcG9pbnRzIGFuZCBzY2FsZXMgZG9uJ3QgaGF2ZSBhbnkgY29tbW9uLCBzaGFyZWQgcGFyZW50IGRpc3RyaWJ1dGlvbiwgYnV0IGFyZSBlc3RpbWF0ZWQgaW5kaXZpZHVhbGx5IGZvciBlYWNoIHNhbXBsZSwgYmFzZWQgb24gYW4gb3B0aW1pemF0aW9uIG9mIHRoZSBsaWtlbGlob29kIGZ1bmN0aW9uIGZvciBlYWNoIHNhbXBsZTsgc2luY2UgdGhlcmUgYXJlIHNvIG1hbnkgcGVwdGlkZSBmcmFnbWVudHMgdG8gaW5mb3JtIHRoYXQgcGFyYW1ldGVyIGVzdGltYXRpb24sIHRoYXQgaXMgcXVpdGUgY29tcHV0YXRpb25hbGx5IGVmZmljaWVudC4gSG93ZXZlciwgd2UndmUgYmVlbiBlbnRpcmVseSBwcm9iYWJpbGlzdGljIHNvIGZhciBpbiB0aGlzIHNpbXVsYXRpb24sIHdpdGggZXZlcnkgdmFsdWUgb2YgaW50ZXJlc3QgZGVyaXZlZCBhcyBhIHJhbmRvbSBkcmF3IGZyb20gc29tZSBwYXJlbnQgZGlzdHJpYnV0aW9uLCBzbyBsZXQncyBzdGljayB3aXRoIHRoYXQgdGhlbWUuIEdpdmVuIHRoYXQgd2Ugc3BlY2lmaWVkIHRoZSAiZ3JhbmQgbWVhbiIgJFxtdV8wJCBvZiBhbGwgcGVwdGlkZSBhYnVuZGFuY2VzIGF0IGByIG11XzBgLCB3aXRoIGEgInJhbmdlIiAkXHNpZ21hXzBeMiQgb2YgYHIgc2RfMF4yYCwgbGV0J3Mgc2F5IHdlIHNldCB0aGUgImF2ZXJhZ2UiIGluZmxlY3Rpb24gcG9pbnQgKHdoZXJlIHRoZSBkcm9wb3V0IHByb2JhYmlsaXR5IHdpbGwgYmUgMC41KSwgJFxyaG9fMCQsIG92ZXIgYWxsIHNhbXBsZXMgdG8gYmUgJFxtdV8wIC0gNFxzaWdtYV8wJCwgNCB0aW1lcyB0aGUgU0Q7IGFuZCB0aGVuIGxldCBlYWNoIHNhbXBsZSdzICRccmhvX2okIHZhcnkgZnJvbSB0aGlzIGJ5ICRcc2lnbWFfe1xyaG9fMH1eMiQuIFNpbWlsYXJseSwgbGV0J3MgbGV0IHRoZSBhdmVyYWdlIHNjYWxlLCAkXHpldGFfMCQgYmUgYSBoeXBlcnBhcmFtZXRlciwgYW5kIGRyYXcgZWFjaCBzYW1wbGUncyAkXHpldGFfaiQgZnJvbSB0aGUgSW52Q2hpU3F1YXJlIGRpc3RyaWJ1dGlvbiBhcyB3ZSBkaWQgZWFybGllciBmb3IgdGhlIGFuYWxhZ291cyBwZXB0aWRlIHZhcmlhbmNlczoKCmBgYHtyfQpyaG9fMCA8LSBtdV8wIC0gNCAqIHNkXzAKc2lnbWFfcmhvXzAgPC0gMS4yCnJob19qIDwtIHJub3JtKG5jb2woel9paiksIHJob18wLCBzaWdtYV9yaG9fMCkKbmFtZXMocmhvX2opIDwtIGNvbG5hbWVzKHpfaWopCgp6ZXRhXzAgPC0gMTAKemV0YV90YXVfMCA8LSAzCnpldGFfaiA8LSByaW52Y2hpc3EobmNvbCh6X2lqKSwgemV0YV8wLCB6ZXRhX3RhdV8wKQpuYW1lcyh6ZXRhX2opIDwtIGNvbG5hbWVzKHpfaWopCgpnZ3Bsb3QoKSArCiAgbGFwcGx5KDE6bGVuZ3RoKHpldGFfaiksIGZ1bmN0aW9uIChpKSB7CiAgICAgc3RhdF9mdW5jdGlvbihmdW4gPSBpcG5vcm0sCiAgICAgICAgICAgICAgICAgICBtYXBwaW5nID0gYWVzKGNvbG9yPWNvbG5hbWVzKHpfaWopW2ldKSwKICAgICAgICAgICAgICAgICAgIGFyZ3MgPSBsaXN0KG1lYW49cmhvX2pbaV0sIHNkPXpldGFfaltpXSkpCiAgICB9KSArCiAgc2NhbGVfY29sb3JfYnJld2VyKG5hbWU9InNhbXBsZUlEIiwgcGFsZXR0ZT0iU2V0MSIpICsKICB4bGltKC0xMCwgMzApICsgeGxhYigiYWJ1bmRhbmNlIikgKyB5bGFiKCJkcm9wb3V0IHByb2JhYmlsaXR5IikgKyB0aGVtZV9idygpCgpgYGAKT0ssIGdyZWF0OyB3ZSd2ZSBnb3QgdGhlIHVuZGVybHlpbmcgZHJvcG91dCBjdXJ2ZXMsIG9uZSBmb3IgZWFjaCBzYW1wbGUuIE5vdyB3ZSBjYW4gc2ltdWxhdGUgd2hpY2ggb2YgdGhlICR6X3tpLGp9JCB2YWx1ZXMgd2lsbCBiZSBvYnNlcnZlZCAobm90IGEgZHJvcG91dCkgYW5kIHdoaWNoIHdpbGwgYmUgIk5BIiAoaXMgYSBkcm9wb3V0KSwgYnkgZmxpcHBpbmcgYSBjb2luIHdpdGggdGhlIHByb2JhYmlsaXR5IG9mIGRyb3BvdXQgZGVmaW5lZCBmb3IgZWFjaCAkel97aSxqfSQgYWJ1bmRhbmNlIHZhbHVlIGdpdmVuIGJ5IHRoZSBkcm9wb3V0IGN1cnZlIGZvciBzYW1wbGUgJGokLiBUaGlzIHRoZSAiZHJvcG91dCBtYXRyaXgiLCAkZF97aSxqfSQuCgpgYGB7cn0KZF9paiA8LSB6X2lqCmRfaWpbLF0gPC0gMApmb3IgKGogaW4gMTpuY29sKGRfaWopKSB7CiAgZF9palssal0gPC0gcmJlcm4obj1ucm93KGRfaWopLCBwcm9iPWlwbm9ybSh6X2lqWyxqXSwgcmhvX2pbal0sIHpldGFfaltqXSkpCn0KcXBsb3QocmhvX2osIDEwMCAqIGNvbFN1bXMoZF9paikvbnJvdyhkX2lqKSwgY29sb3I9emV0YV9qLCBzaXplPUkoNSkpICsKICBnZW9tX2xhYmVsKGFlcyhsYWJlbD1uYW1lcyh6ZXRhX2opKSkgKyAKICBzY2FsZV95X2xvZzEwKCkgKwogIHlsYWIoIiUgb2YgZHJvcG91dHMgaW4gc2FtcGxlIikgKyB0aGVtZV9idygpCmBgYAoKRnJvbSB0aGUgYWJvdmUsIHdlIHNlZSB0aGF0IHRoZSByYW5nZSBvZiBkcm9wb3V0ICUgaW4gZWFjaCBzYW1wbGUgY29ycmVsYXRlcyB3aXRoIHRoZSAkXHJob19qJCBpbmZsZWN0aW9uIHBvaW50cyAtLSB0aGUgaGlnaGVyIChyaWdodC1zaGlmdGVkKSB0aGUgaW5mbGVjdGlvbiBwb2ludCwgdGhlIG1vcmUgYWJ1bmRhbmNlIHZhbHVlcyBhcmUgbG9zdCBkdWUgdG8gZHJvcG91dDsgYW5kIGFsc28sIGZvciBhIGdpdmVuICRccmhvX2okIHZhbHVlLCB0aGUgaGlnaGVyIHRoZSAkXHpldGFfaiQgdmFsdWUsIHRoZSBzaGFsbG93ZXIgdGhlIGRyb3BvZmYgc2xvcGUsIGFuZCBzbyBldmVuIG1vcmUgYWJ1bmRhbmNlcyBkcm9wb3V0LgoKTm93IHdlIGhhdmUgZXZlcnl0aGluZyB3ZSBuZWVkIGZyb20gQXBwZW5kaXggZXF1YXRpb24gKDEpIHRvIHByb2R1Y2UgYSBmaW5hbCwgc2ltdWxhdGVkIGRhdGFzZXQgb2YgZXhwZXJpbWVudGFsbHkgb2JzZXJ2ZWQgYWJ1bmRhbmNlcywgJHlfe2ksan0kLgoKYGBge3J9CiMgc3RhcnQgYnkganVzdCBjb3B5aW5nIHpfaWogaW50byB5X2lqOgp5X2lqIDwtIHpfaWoKCiMgbm93IGRyb3BvdXQgdGhlIHZhbHVlcyBhY2NvcmRpbmcgdG8gZF9pajoKZm9yIChqIGluIDE6bmNvbCh5X2lqKSkgewogIHlfaWpbZF9palssal0gPT0gMSwgal0gPC0gTkEKfQoKcHJpbnQoaGVhZCh5X2lqKSwgZGlnaXRzPTQpCmBgYAoKSW4gdGhlIGZpcnN0IHNpeCBmcmFnbWVudHMgYWJvdmUsIHdlIGNhbiBhbHJlYWR5IHNlZSBhIGhhbmRmdWwgb2YgTkEncyB0aGF0IHJlcHJlc2VudCBzaW11bGF0ZWQgZHJvcG91dHMuCgpPSywgbm93IHdlIGNhbiB0ZXN0IHByb0RBIHVzaW5nIG91ciBzaW11bGF0ZWQgZGF0YSwgZm9yIHdoaWNoIHdlIGtub3cgdGhlIHRydWUgdmFsdWVzIGZvciBlYWNoIHNpbmdsZSBwYXJhbWV0ZXIgb2YgaW50ZXJlc3QsIGFuZCB3ZSBjYW4gc2VlIGhvdyB3ZWxsIHByb0RBIGlzIGFibGUgdG8gcmVjb3ZlciB3aGF0IHdlJ3ZlIHNldHVwOgoKYGBge3J9CmZpdCA8LSBwcm9EQSh5X2lqLCBkZXNpZ24gPSBmYWN0b3IocmVwKGdyb3VwcywgZWFjaD1ucmVwKSksIHZlcmJvc2U9VCkKYGBgClNvLCBwZXJoYXBzIG5vdCB0b28gdW5zdXJwcmlzaW5nbHksIHdoZW4gd2UgdXNlIHByb0RBIHRvIGdlbmVyYXRlIGEgZml0IHdpdGggZGF0YSB0aGF0IGV4YWN0bHkgbWF0Y2hlcyB0aGUgbW9kZWxpbmcgZXhwZWN0YXRpb25zLCBpdCBzZWVtcyB0byBkbyBhIHByZXR0eSBnb29kIGpvYiEgTGV0J3MgbG9vayBhdCB0aGUgdmFyaW91cyBoeXBlcnBhcmFtZXRlcnMgZmlyc3Q6CmBgYHtyfQpocHMgPC0gZml0JGh5cGVyX3BhcmFtZXRlcnMKa25pdHI6OmthYmxlKAogIGRhdGEuZnJhbWUoCiAgICBuYW1lPWMoImdyYW5kIG1lYW4iLAogICAgICAgICAgICJncmFuZCByYW5nZSIsCiAgICAgICAgICAgIm5vcm1hbGl0eSBwYXJhbWV0ZXIiLAogICAgICAgICAgICJ2YXJpYW5jZSBzY2FsZSIsCiAgICAgICAgICAgInZhcmlhbmNlIHNoYXBlIiksCiAgICB2YWx1ZXM9YyhtdV8wLCBzZF8wXjIsIGRmX2xvYywgdGF1XzBeMiwgZGZfMCksCiAgICBlc3RpbWF0ZT1jKGhwcyRsb2NhdGlvbl9wcmlvcl9tZWFuLAogICAgICAgICAgICAgICBocHMkbG9jYXRpb25fcHJpb3Jfc2NhbGUsCiAgICAgICAgICAgICAgIGhwcyRsb2NhdGlvbl9wcmlvcl9kZiwgIyBnaXZlbiBhcyBhIGZpeGVkIGNvbnN0YW50LCBub3QgZXN0aW1hdGVkCiAgICAgICAgICAgICAgIGhwcyR2YXJpYW5jZV9wcmlvcl9zY2FsZSwKICAgICAgICAgICAgICAgaHBzJHZhcmlhbmNlX3ByaW9yX2RmKSksIGZvcm1hdD0icGlwZSIpCmBgYAoKV2hpbGUgdGhlIHBhcmFtZXRlcnMgKGFuZCByYW5kb20gc2VlZCkgb2YgdGhpcyBub3RlYm9vayBjb3VsZCBjaGFuZ2UsIGF0IGxlYXN0IHdpdGggdGhlIGN1cnJlbnQgKD8pIHZhbHVlcywgd2Ugc2VlIHRoYXQgbmVhcmx5IGFsbCBvZiB0aGUgaHlwZXJwYXJhbWV0ZXJzIG1hdGNoIHZlcnkgd2VsbCwgd2l0aCB0aGUgZXhjZXB0aW9uIG9mIHRoZSAiZ3JhbmQgcmFuZ2UiLCBvciAkXHNpZ21hXzBeMiQsIHdoaWNoIHByb0RBIGhhcyBvdmVyLWVzdGltYXRlZCBhcyBsYXJnZXIgdGhhbiB0aGUgdHJ1ZSBkaXN0cmlidXRlZCByYW5nZSBvZiBncm91cC13aXNlIG1lYW4gdmFsdWVzOyBJIGV4cGVjdCB0aGlzIGlzIGFuIHVuZHVlIGluZmx1ZW5jZSBvZiB0aGUgZHJvcG91dCB2YWx1ZXMgdGhlbXNlbHZlcywgd2hpY2ggd2lsbCB0ZW5kIHRvICJpbmZsYXRlIiB0aGUgZXhwZWN0ZWQgdmFyaWFuY2Ugb2YgdGhlIGdyb3VwIG1lYW5zLCBhdCB0aGUgYm90dG9tL2xlZnQgZW5kLCBldmVuIHRob3VnaCB0aGUgdG9wL3JpZ2h0IGVuZCBpcyBhY2N1cmF0ZWx5IGFuZCBleHBsaWN0bHkgcmVwcmVzZW50ZWQgaW4gdGhlIGRhdGEgKHRoaXMgYW1vdW50cyB0byBhIHN1Z2dlc3Rpb24gZm9yIHRoZSBwcm9EQSBhdXRob3JzIHRvIGNvbnNpZGVyIGVzdGltYXRpbmcgdGhlIHZhcmlhbmNlIGZyb20gYSBsZWZ0LXRydW5jYXRlZCBkaXN0cmlidXRpb24gb2YgZ3JvdXAgbWVhbnMsIHRvIGF2b2lkIHRoaXMgYXJ0aWZhY3QpLgoKTmV4dCwgbGV0J3MgY2hlY2tvdXQgdGhlIGRyb3BvdXQgY3VydmUgcGFyYW1ldGVyczoKYGBge3J9CmdncGxvdChyYmluZChkYXRhLmZyYW1lKHZhcmlhYmxlPSJkcm9wb3V0IHBvc2l0aW9uIiwKICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWU9cmhvX2osCiAgICAgICAgICAgICAgICAgICAgICAgIGVzdGltYXRlPWhwcyRkcm9wb3V0X2N1cnZlX3Bvc2l0aW9uLAogICAgICAgICAgICAgICAgICAgICAgICBzYW1wbGVJRCA9IG5hbWVzKHJob19qKSksCiAgICAgICAgICAgICBkYXRhLmZyYW1lKHZhcmlhYmxlPSJkcm9wb3V0IHNjYWxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWU9emV0YV9qLAogICAgICAgICAgICAgICAgICAgICAgICBlc3RpbWF0ZT0taHBzJGRyb3BvdXRfY3VydmVfc2NhbGUsCiAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZUlEID0gbmFtZXMoemV0YV9qKSkpLAogICAgICAgYWVzKHg9dmFsdWUsIHk9ZXN0aW1hdGUpKSArIAogIGdlb21fYWJsaW5lKHNsb3BlPTEsIGludGVyY2VwdD0wKSArCiAgc3RhdF9zbW9vdGgobWV0aG9kPSJsbSIsIGZvcm11bGEgPSB5IH4geCkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG9yPXNhbXBsZUlEKSkgKwogIGZhY2V0X3dyYXAofnZhcmlhYmxlLCBzY2FsZXM9ImZyZWUiKSArICB0aGVtZV9idygpCmBgYApJbnRlcmVzdGluZyEgV2hpbGUgYm90aCB0aGUgbG9jYXRpb24gKCRccmhvJCkgYW5kIHNjYWxlICgkXHpldGEkKSBwYXJhbWV0ZXJzIGFyZSB2ZXJ5IHN0cm9uZ2x5IGNvcnJlbGF0ZWQsIHRoZSBsb2NhdGlvbiBpcyB2ZXJ5IHNsaWdodGx5IHVuZGVyLWVzdGltYXRlZCAodGhvdWdoIG5vdCBzdGF0aXN0aWNhbGx5IHNpZ25pY2FudCBpbiB0aGlzIGNhc2UpLCB3aGlsZSB0aGUgc2NhbGUgaXMgZGlzdGluY3RseSBhbmQgc3lzdGVtYXRpY2FsbHkgX292ZXItZXN0aW1hdGVkXyBpbiBhbGwgc2FtcGxlcy4gT2ZmIHRoZSB0b3Agb2YgbXkgaGVhZCwgSSBkb24ndCBoYXZlIGFueSByYXRpb25hbGUgZm9yIHRoaXMgYmVoYXZpb3IsIHNvIEknbSBnb2luZyB0byBsZWF2ZSB0aGlzIGFzIGEgcXVlc3Rpb24gZm9yIHRoZSBhdXRob3JzICghPyEpCgpPSywgc28gbW9zdCBpbXBvcnRhbnRseSwgbGV0J3MgbG9vayBhdCBob3cgYWNjdXJhdGUgcHJvREEncyBlc3RpbWF0aW9uIG9mIHRoZSBncm91cCBtZWFucyBlbmRlZCB1cCwgaW4gdGhlIGZhY2Ugb2YgYWxsIHRoZXNlIGRyb3BvdXRzLCBhbmQgKG1vc3RseSkgY29ycmVjdCBoeXBlcnBhcmFtZXRlciBlc3RpbWF0aW9uczoKCmBgYHtyfQpjb2VmIDwtIGZpdCRjb2VmZmljaWVudHMKbmFtZXMoZGltbmFtZXMoY29lZikpIDwtIGMoInBlcHRpZGUiLCAiZ3JvdXAiKQpkdGFsbCA8LSBtZWx0KGRfaWosIHZhbHVlLm5hbWUgPSAiZHJvcCIpCmR0YWxsJGdyb3VwIDwtIGdzdWIoIiguKj8pXFwtXFxkKyIsICJcXDEiLCBkdGFsbCRzYW1wbGVJRCkKCmRyb3BkZiA8LSBkZHBseShkdGFsbCwgLihwZXB0aWRlLCBncm91cCksIGZ1bmN0aW9uKGR4KSB7CiAgZGF0YS5mcmFtZShkcm9wbnVtID0gc3VtKGR4JGRyb3ApKQp9KQoKc2VzIDwtIG1hdHJpeCh1bmxpc3QobGFwcGx5KGxhcHBseShmaXQkY29lZmZpY2llbnRfdmFyaWFuY2VfbWF0cmljZXMsIGRpYWcpLCBzcXJ0KSksCiAgICAgICAgICAgICAgbmNvbD0zLCBieXJvdz1UKQpjb2xuYW1lcyhzZXMpIDwtIGNvbG5hbWVzKGZpdCRjb2VmZmljaWVudF92YXJpYW5jZV9tYXRyaWNlc1tbMV1dKQpzZXMgPC0gYXMuZGF0YS5mcmFtZShzZXMpCnNlcyRwZXB0aWRlIDwtIHJvd25hbWVzKGZpdCRjb2VmZmljaWVudHMpCnNlcyRsb3dxdCA8LSBxdCgwLjAyNSwgZml0JGZlYXR1cmVfcGFyYW1ldGVycyRkZikKc2VzJGhpZ2hxdCA8LSBxdCgwLjk3NSwgZml0JGZlYXR1cmVfcGFyYW1ldGVycyRkZikKY2lzIDwtIG1lbHQoc2VzLAogICAgICAgICAgICBpZD1jKCJwZXB0aWRlIiwgImhpZ2hxdCIsICJsb3dxdCIpLAogICAgICAgICAgICB2YXJpYWJsZS5uYW1lID0gImdyb3VwIiwKICAgICAgICAgICAgdmFsdWUubmFtZSA9ICJzZXMiKQpjaXMkbG93IDwtIGNpcyRsb3dxdCAqIGNpcyRzZXMKY2lzJGhpZ2ggPC0gY2lzJGhpZ2hxdCAqIGNpcyRzZXMKCmRmIDwtIG1lcmdlKGRyb3BkZiwgbWVyZ2UobWVsdChjb2VmLCB2YWx1ZS5uYW1lID0gImVzdGltYXRlIiksIG1lbHQobXVfaWosIHZhbHVlLm5hbWUgPSAidmFsdWUiKSkpCmRmIDwtIG1lcmdlKGRmLCBjaXMpCgpkZiRlc3Rsb3cgPC0gZGYkZXN0aW1hdGUgKyBkZiRsb3cKZGYkZXN0aGlnaCA8LSBkZiRlc3RpbWF0ZSArIGRmJGhpZ2gKZGYkZXN0cmFuZ2UgPC0gZGYkZXN0aGlnaCAtIGRmJGVzdGxvdwoKZ2dwbG90KGRmLCBhZXMoeD12YWx1ZSwgeT1lc3RpbWF0ZSwgc2hhcGU9Z3JvdXAsIGNvbG9yPWZhY3Rvcihkcm9wbnVtKSkpICsKICBnZW9tX3BvaW50KCkgKyBnZW9tX2FibGluZShzbG9wZT0xLCBpbnRlcmNlcHQ9MCkgKwogIHNjYWxlX2NvbG9yX2JyZXdlcihuYW1lPSIjIGRyb3BvdXRzIiwgcGFsZXR0ZT0iU2V0MiIpICsKICBmYWNldF93cmFwKH5kcm9wbnVtKSArIHRoZW1lX2J3KCkKYGBgCgpBZ2FpbiwgaW50ZXJlc3RpbmchIFdoZW4gdGhlIG51bWJlciBvZiBkcm9wb3V0cyBpbiBhIGdyb3VwIGlzIDAsIDEsIG9yIGV2ZW4gMiwgdGhlbiBwcm9EQSBkb2VzIGEgcHJldHR5IHJlbWFya2FibGUgam9iIHJlY292ZXJpbmcgdGhlIHRydWUgdmFsdWUgb2YgdGhlIGdyb3VwIG1lYW4uIEJ1dCB3aGVuIGFsbCBvZiB0aGUgcmVwbGljYXRlcyBpbiBhIGdyb3VwIGFyZSBkcm9wb3V0cywgd2UncmUgbGVmdCB3aXRoIGNvZWZmaWNpZW50IGVzdGltYXRlcyB0aGF0IGFyZSBhbGwgfjEyLiBUaGUgOTUlIGNvbmZpZGVuY2UgaW50ZXJ2YWwgcmFuZ2VzIGxvb2sgbGlrZSB0aGlzOgoKYGBge3J9CmdncGxvdChkZiwgYWVzKHg9ZXN0cmFuZ2UsIGZpbGw9ZmFjdG9yKGRyb3BudW0pKSkgKwogIHN0YXRfYmluKGJpbnM9NTApICsgCiAgc2NhbGVfY29sb3JfYnJld2VyKG5hbWU9IiMgZHJvcG91dHMiLCBwYWxldHRlPSJTZXQyIikgKwogIGZhY2V0X3dyYXAofmRyb3BudW0sIHNjYWxlcz0iZnJlZV95IiwgbmNvbD0xKSArIHRoZW1lX2J3KCkgKwogIHhsYWIoIndpZHRoIG9mIDk1JSBjb25maWRlbmNlIGludGVydmFsIikKYGBgCgpTbywgYXMgZXhwZWN0ZWQ6IHRpZ2h0ZXIgaW50ZXJ2YWwgcmFuZ2VzIGZvciBncm91cHMgaGF2aW5nIGFsbCBvYnNlcnZhdGlvbnM7IHdpZGVyIHJhbmdlcyB3aXRoIGxlc3Mgb2JzZXJ2YXRpb25zLCBhbmQgZXNzZW50aWFsbHkgZml4ZWQgKHdpZGUpIGludGVydmFsIHdoZW4gbm8gb2JzZXJ2YXRpb25zIGFyZSBtYWRlLgoKTGFzdGx5LCBsZXQncyBzZWUgaG93IHdlbGwgcHJvREEgd2FzIGFibGUgdG8gZXN0aW1hdGUgdmFyaWFuY2VzIGZvciBlYWNoIHBlcHRpZGU6CmBgYHtyfQp2YXJkZiA8LSBkYXRhLmZyYW1lKHZhbHVlID0gdmFyX2ksCiAgICAgICAgICAgICAgICAgICAgZXN0aW1hdGUgPSBmaXQkZmVhdHVyZV9wYXJhbWV0ZXJzJHMyLAogICAgICAgICAgICAgICAgICAgIG1pc3NpbmcgPSBuY29sKHlfaWopIC0gZml0JGZlYXR1cmVfcGFyYW1ldGVycyRuX29icykKCmdncGxvdCh2YXJkZiwgYWVzKHg9dmFsdWUsIHk9ZXN0aW1hdGUpKSArIHN0YXRfYmluaGV4KCkgKwogIGdlb21fYWJsaW5lKHNsb3BlPTEsIGludGVyY2VwdD0wKSArIHRoZW1lX2J3KCkgKwogIHhsYWIoInRydWUgdmFyaWFuY2UiKSArIHlsYWIoInByb0RBIGVzdGltYXRlZCB2YXJpYW5jZSIpCgpgYGAKVGhlIG1ham9yaXR5IG9mIHZhcmlhbmNlcyBhcmUgY29ycmVjdGx5IGVzdGltYXRlZCwgYnV0IGEgaGFuZGZ1bCBvZiBwZXB0aWRlcyBoYXZpbmcgdGhlIGxhcmdlc3QgdmFyaWFuY2VzIHJlbWFpbiB1bmRlci1lc3RpbWF0ZWQsIHBvdGVudGlhbGx5IGluZmxhdGluZyBhbnkgZG93bnN0cmVhbSBjb250cmFzdCBzdGF0aXN0aWNzLgoKIyBlZmZlY3RzIG9mIG5vcm1hbGl6YXRpb24gd2l0aCBkcm9wb3V0cwoKV2Ugb2JzZXJ2ZWQgZWFybGllciB0aGF0IHRoZSBzaW11bGF0ZWQgZGF0YSBpcyBhbHJlYWR5IHBlcmZlY3RseSBub3JtYWxpemVkLCBhdCBsZWFzdCBwcmlvciB0byBoYXZpbmcgcmFuZG9tIGxvdy1hYnVuZGFuY2UgdmFsdWVzIHJlbW92ZWQgZHVlIHRvIGRyb3BvdXQ7IGFuZCBlYWNoIHNhbXBsZSBoYXMgc2xpZ2h0bHkgZGlmZmVyZW50IGRyb3BvdXQgY3VydmVzLCBzbyBub3cgdGhlIGRpc3RyaWJ1dGlvbnMgbWF5IG5vdCBiZSBzbyBwZXJmZWN0bHkgY29tcGFyYWJsZToKCmBgYHtyfQpnZ3Bsb3QobWVsdCh5X2lqLCB2YWx1ZS5uYW1lID0gImFidW5kYW5jZSIpLAogICAgICAgYWVzKHg9c2FtcGxlSUQsIHk9YWJ1bmRhbmNlLCBjb2xvcj1zYW1wbGVJRCkpICsgZ2VvbV92aW9saW4oKSArIAogIHNjYWxlX2NvbG9yX2JyZXdlcihwYWxldHRlPSJTZXQxIikgKyB0aGVtZV9idygpCmBgYAoKV2hhdCBoYXBwZW5zIGlmIHdlIHF1YW50aWxlIG5vcm1hbGl6ZSB0aGUgb2JzZXJ2YXRpb25zLCBhbmQgdGhlbiBjb21wYXJlIHRvIHRoZWlyIG9yaWdpbmFsIHZhbHVlcyAocmVtb3ZpbmcvaWdub3JpbmcgYW55IGRpZmZlcmVuY2UgbGVzcyB0aGFuIDAuMiB1bml0cywgZm9yIGdyYXBoaWNhbCBjbGFyaXR5KT8KCmBgYHtyfQpub3JteSA8LSBub3JtYWxpemVCZXR3ZWVuQXJyYXlzKHlfaWosIG1ldGhvZD0icXVhbnRpbGUiKQptZWR5IDwtIG1lZGlhbl9ub3JtYWxpemF0aW9uKHlfaWopCmRlbHRhcyA8LSBjb2xNZWFucyhtZWR5IC0geV9paiwgbmEucm09VCkKZGVsdGFzIDwtIGRhdGEuZnJhbWUoZGVsdGE9ZGVsdGFzLCBzYW1wbGVJRCA9IG5hbWVzKGRlbHRhcykpCmdncGxvdChzdWJzZXQobWVsdChub3JteSAtIHlfaWosIHZhbHVlLm5hbWUgPSAiZGVsdGEiKSwKICAgICAgICAgICAgICBhYnMoZGVsdGEpID49IDAuMiksCiAgICAgICBhZXMoeD1zYW1wbGVJRCwgeT1kZWx0YSwgY29sb3I9c2FtcGxlSUQpKSArIGdlb21fcXVhc2lyYW5kb20oKSArIAogIHNjYWxlX2NvbG9yX2JyZXdlcihwYWxldHRlPSJTZXQxIikgKyB0aGVtZV9idygpCmBgYApJdCBhcHBlYXJzLCBhdCBsZWFzdCBmb3IgdGhpcyBzaW11bGF0ZWQgZGF0YXNldCB0aGF0IGRvZXNuJ3QgaGF2ZSBncm9zcyBkaWZmZXJlbmNlcyBpbiBtZWFuIGFidW5kYW5jZXMgb3IgZHJvcG91dCByYXRlcywgdGhhdCBxdWFudGlsZSBub3JtYWxpemF0aW9uIGhhcyBhIGxhcmdlIGVmZmVjdCBvbiBvbmx5IGEgc21hbGwgbnVtYmVyIG9mIGZyYWdtZW50cy4gSWYgd2UgcmVmaXQgYHByb0RBYCB0byB0aGlzIChyZSlub3JtYWxpemVkIGRhdGEsIHdlJ2QgZXhwZWN0IHRvIHNlZSBzb21lIGRpZmZlcmVuY2VzOgpgYGB7cn0Kbm9ybWZpdCA8LSBwcm9EQShub3JteSwgZGVzaWduID0gZmFjdG9yKHJlcChncm91cHMsIGVhY2g9bnJlcCkpLCB2ZXJib3NlPVQpCmBgYAoKYGBge3J9Cm5vcm1jb2VmIDwtIG5vcm1maXQkY29lZmZpY2llbnRzCm5hbWVzKGRpbW5hbWVzKG5vcm1jb2VmKSkgPC0gYygicGVwdGlkZSIsICJncm91cCIpCgpjb2VmZGlmZiA8LSBub3JtY29lZiAtIGNvZWYKbmFtZXMoZGltbmFtZXMoY29lZmRpZmYpKSA8LSBjKCJwZXB0aWRlIiwgImdyb3VwIikKZ2dwbG90KHN1YnNldChtZWx0KGNvZWZkaWZmLCB2YWx1ZS5uYW1lID0gImRpZmYiKSwKICAgICAgICAgICAgICBhYnMoZGlmZikgPj0gMC4yKSwKICAgICAgIGFlcyh4PWdyb3VwLCB5PWRpZmYpKSArIGdlb21fcXVhc2lyYW5kb20oKSArCiAgdGhlbWVfYncoKSArIHlsYWIoImRpZmZlcmVuY2UgaW4gY29lZmZpY2llbnQgZXN0aW1hdGVzIikKCmdncGxvdChtZXJnZShtZWx0KGNvZWZkaWZmLCB2YWx1ZS5uYW1lPSJkaWZmIiksCiAgICAgICAgICAgICBtZWx0KGNvZWYsIHZhbHVlLm5hbWU9ImVzdGltYXRlIikpLAogICAgICAgYWVzKHg9ZXN0aW1hdGUsIHk9ZGlmZikpICsgZ2VvbV9wb2ludCgpICsgZmFjZXRfd3JhcCh+Z3JvdXApICsgdGhlbWVfYncoKSArCiAgeWxhYigiZGlmZmVyZW5jZSBpbiBjb2VmZmljaWVudCBlc3RpbWF0ZXMiKSArIHhsYWIoIm9yaWdpbmFsIGVzdGltYXRlIikKYGBgClBlcmhhcHMgYXMgZXhwZWN0ZWQsIHRoZSBmZXcgY29lZmZpY2llbnRzIHRoYXQgYXJlIGdyb3NzbHkgYWZmZWN0ZWQgYnkgdGhlIG5vcm1hbGl6YXRpb24gcHJvY2VkdXJlIGFyZSBjb25maW5lZCB0byB0aGUgKG1vc3RseSB1cHBlcikgZXh0cmVtZXMgb24gdGhlIGFidW5kYW5jZSBzY2FsZS4KCkJ1dCBsZXQncyBhbHNvIGxvb2sgYXQgd2hhdCBoYXBwZW5zIHRvIHRoZSBkcm9wb3V0IGN1cnZlIHBhcmFtZXRlcnM6CmBgYHtyfQpub3JtaHBzIDwtIG5vcm1maXQkaHlwZXJfcGFyYW1ldGVycwpnZ3Bsb3QocmJpbmQoZGF0YS5mcmFtZSh2YXJpYWJsZT0iZHJvcG91dCBwb3NpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlPXJob19qLAogICAgICAgICAgICAgICAgICAgICAgICBlc3RpbWF0ZT1ub3JtaHBzJGRyb3BvdXRfY3VydmVfcG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZUlEID0gbmFtZXMocmhvX2opKSwKICAgICAgICAgICAgIGRhdGEuZnJhbWUodmFyaWFibGU9ImRyb3BvdXQgc2NhbGUiLAogICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZT16ZXRhX2osCiAgICAgICAgICAgICAgICAgICAgICAgIGVzdGltYXRlPS1ub3JtaHBzJGRyb3BvdXRfY3VydmVfc2NhbGUsCiAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZUlEID0gbmFtZXMoemV0YV9qKSkpLAogICAgICAgYWVzKHg9dmFsdWUsIHk9ZXN0aW1hdGUpKSArIAogIGdlb21fYWJsaW5lKHNsb3BlPTEsIGludGVyY2VwdD0wKSArCiAgc3RhdF9zbW9vdGgobWV0aG9kPSJsbSIsIGZvcm11bGEgPSB5IH4geCkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG9yPXNhbXBsZUlEKSkgKwogIGZhY2V0X3dyYXAofnZhcmlhYmxlLCBzY2FsZXM9ImZyZWUiKSArICB0aGVtZV9idygpCmBgYApUaGUgY29uc2VxdWVuY2Ugb2YgcmVub3JtYWxpemluZyBmb3JjZXMgdGhlIGRyb3BvdXQgbWlkcG9pbnQgbG9jYXRpb25zIHRvIGJlIHVuaWZvcm1seSBvdmVyL3VuZGVyIGVzdGltYXRlZCBmb3IgY3VydmVzIGhhdmluZyB0cnVlIGxvd2VyL2hpZ2hlciBsb2NhdGlvbnMgKGFuZCB0aGUgc2NhbGluZyBmYWN0b3IgcmVtYWlucyBhcyBiZWZvcmUsIHVuaWZvcm1seSBvdmVyLWVzdGltYXRlZCkuIFRoZXNlIHNtYWxsIGRpc3RvcnRpb25zIGluIGRyb3BvdXQgY3VydmVzIGNvdWxkIGhhdmUga25vY2stb24gZWZmZWN0cyBmb3IgZXN0aW1hdGlvbiBvZiBjb2VmZmljaWVudHMgZnJvbSBncm91cHMgaGF2aW5nIG11bHRpcGxlIGRyb3BvdXQgc2FtcGxlczoKCmBgYHtyfQpub3Jtc2VzIDwtIG1hdHJpeCh1bmxpc3QobGFwcGx5KGxhcHBseShub3JtZml0JGNvZWZmaWNpZW50X3ZhcmlhbmNlX21hdHJpY2VzLCBkaWFnKSwgc3FydCkpLAogICAgICAgICAgICAgICAgICBuY29sPTMsIGJ5cm93PVQpCmNvbG5hbWVzKG5vcm1zZXMpIDwtIGNvbG5hbWVzKG5vcm1maXQkY29lZmZpY2llbnRfdmFyaWFuY2VfbWF0cmljZXNbWzFdXSkKbm9ybXNlcyA8LSBhcy5kYXRhLmZyYW1lKG5vcm1zZXMpCm5vcm1zZXMkcGVwdGlkZSA8LSByb3duYW1lcyhub3JtZml0JGNvZWZmaWNpZW50cykKbm9ybXNlcyRsb3dxdCA8LSBxdCgwLjAyNSwgbm9ybWZpdCRmZWF0dXJlX3BhcmFtZXRlcnMkZGYpCm5vcm1zZXMkaGlnaHF0IDwtIHF0KDAuOTc1LCBub3JtZml0JGZlYXR1cmVfcGFyYW1ldGVycyRkZikKbm9ybWNpcyA8LSBtZWx0KG5vcm1zZXMsCiAgICAgICAgICAgICAgICBpZD1jKCJwZXB0aWRlIiwgImhpZ2hxdCIsICJsb3dxdCIpLAogICAgICAgICAgICAgICAgdmFyaWFibGUubmFtZSA9ICJncm91cCIsCiAgICAgICAgICAgICAgICB2YWx1ZS5uYW1lID0gInNlcyIpCm5vcm1jaXMkbG93IDwtIG5vcm1jaXMkbG93cXQgKiBub3JtY2lzJHNlcwpub3JtY2lzJGhpZ2ggPC0gbm9ybWNpcyRoaWdocXQgKiBub3JtY2lzJHNlcwoKbm9ybWRmIDwtIG1lcmdlKGRyb3BkZiwgbWVyZ2UobWVsdChub3JtY29lZiwgdmFsdWUubmFtZSA9ICJlc3RpbWF0ZSIpLCBtZWx0KG11X2lqLCB2YWx1ZS5uYW1lID0gInZhbHVlIikpKQpub3JtZGYgPC0gbWVyZ2Uobm9ybWRmLCBub3JtY2lzKQoKbm9ybWRmJGVzdGxvdyA8LSBub3JtZGYkZXN0aW1hdGUgKyBub3JtZGYkbG93Cm5vcm1kZiRlc3RoaWdoIDwtIG5vcm1kZiRlc3RpbWF0ZSArIG5vcm1kZiRoaWdoCm5vcm1kZiRlc3RyYW5nZSA8LSBub3JtZGYkZXN0aGlnaCAtIG5vcm1kZiRlc3Rsb3cKCmdncGxvdChub3JtZGYsIGFlcyh4PXZhbHVlLCB5PWVzdGltYXRlLCBzaGFwZT1ncm91cCwgY29sb3I9ZmFjdG9yKGRyb3BudW0pKSkgKwogIGdlb21fcG9pbnQoKSArIGdlb21fYWJsaW5lKHNsb3BlPTEsIGludGVyY2VwdD0wKSArCiAgc2NhbGVfY29sb3JfYnJld2VyKG5hbWU9IiMgZHJvcG91dHMiLCBwYWxldHRlPSJTZXQyIikgKwogIGZhY2V0X3dyYXAofmRyb3BudW0pICsgdGhlbWVfYncoKQpgYGAKIyBTYW1wbGUgUUMgdXNpbmcgZGlzdGFuY2VzCgpXZSBrbm93IHRoYXQgZGlmZmVyZW5jZXMgaW4gZHJvcG91dCBhYnVuZGFuY2VzIGNhbiBsZWFkIHRvIHNwdXJpb3VzIFBDQSBwcm9qZWN0aW9ucyBkdXJpbmcgc2FtcGxlLWxldmVsIFFDOyBgcHJvREFgIHByb3ZpZGVzIGEgcHJvYmFiaWxpc3RpYyBkaXN0YW5jZSBtZWFzdXJlbWVudCB0aGF0IGNvdWxkIGF2b2lkIHN1Y2guIExldCdzIGV4cGxvcmU6CgpgYGB7cn0KbWF0eSA8LSB5X2lqCm1hdHlbaXMubmEobWF0eSldIDwtIGxvZzIoMSkKbWF0eSA8LSBub3JtYWxpemVCZXR3ZWVuQXJyYXlzKG1hdHksIG1ldGhvZD0icXVhbnRpbGUiKQoKcGNhIDwtIHByY29tcChtYXR5LCBzY2FsZT1UKQpnZ3Bsb3QoYXMuZGF0YS5mcmFtZShwY2Ekcm90YXRpb24pLCBhZXMoeD1QQzEsIHk9UEMyLCBjb2xvcj1jb2xuYW1lcyhtYXR5KSkpICsKICBnZW9tX3BvaW50KCkgKyB0aGVtZV9idygpCgpkbSA8LSBkaXN0X2FwcHJveChmaXQpCmhlYXRtYXAoYXMubWF0cml4KGRtJG1lYW4pKQpgYGAKCldoaWxlIGVhY2ggc2FtcGxlIGhhcyBmZXdlci9tb3JlIGRyb3BvdXRzLCB0aGUgaGVhdG1hcCBwYWlyd2lzZSBzYW1wbGUgZGlzdGFuY2VzIGFwcGVhciB1bmlmb3JtIHdpdGhpbi9iZXR3ZWVuIGVhY2ggZXhwZXJpbWVudGFsIGdyb3VwLCBhcyBleHBlY3RlZC4=