Intro

The purpose of this analysis is to find a correlation between the boolean models fitness to a steady state activity profile and their performance in terms of the number of True Positive (TP) synergies predicted and/or the overall MCC score (Matthews Correlation Coefficient score). We want to show that a closer fitness to the steady state suggests more predictive models, corroborating thus our proof of concept of using an ensemble-based approach where models are trained towards a specific steady state signaling pattern for drug combination predictions.

The boolean model datasets we will use are in total \(9\): one for each cell line of interest (8 cell lines) where the models were fitted to a specific steady state in each case and one for the so-called random models which were generated randomly in the sense that were fitted only to a proliferation state (simulations were done using the DrugLogics software modules Gitsbe and Drabme).

Each boolean model dataset constitues of:

  • The model predictions file which has for each model the prediction for each drug combination tested (0 = no synergy predicted, 1 = synergy predicted, NA = couldn’t find stable states in either the drug combination inhibited model or in any of the two single-drug inhibited models)
  • The models stable state (one per model). A fitness score for each model can easily be calculated then by matching the model’s stable state (which is something inherent in the boolean’s model structure, a unique fixpoint attractor) with the steady state of interest, node per node. A higher fitness score would mean a better match of a model’s stable state to the cell line derived steady state (a perfect match would result in a fitness of 1).
  • The models link operators which is a representation of the boolean equations of each model. Each boolean equation is in the form: **Target *= (Activator OR Activator OR…) AND NOT (Inhibitor OR Inhibitor OR…)** and the difference between the models can be found in the link operator (1 = ‘OR NOT’, 0 = ‘AND NOT’, or absent) which has been mutated (changed) through the genetic algorithm in Gitsbe. Note that the equations that do not have link operators are the same for every model and are thus discarded.
  • The observed synergies file which lists the drug combinations that were observed as synergistic for each cell line.
  • The steady state file which lists the network nodes (protein, gene, complexes names, etc) and their activity value (0 or 1, representing an inhibited or active node respectively). This input is provided per cell line and not for the random models since they are just trained to a profileration state.

Input

Loading libraries:

library(DT)
library(ggpubr)
library(emba)
library(usefun)
library(nnet)
library(pscl)
library(ComplexHeatmap)
library(circlize)
library(dplyr)
library(tibble)
library(Ckmeans.1d.dp)
library(RColorBrewer)

First we load the cell-specific input data:

# Cell Lines
cell.lines = c("A498", "AGS", "DU145", "colo205", "SW620", "SF295", "UACC62", "MDA-MB-468")

cell.line.dirs = sapply(cell.lines, function(cell.line) {
  paste0(getwd(), "/", cell.line)
})

# Model predictions
model.predictions.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/model_predictions")
})

model.predictions.per.cell.line = lapply(model.predictions.files, 
  function(file) {
    get_model_predictions(file)
  }
)

# Observed synergies
observed.synergies.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/observed_synergies")
})

observed.synergies.per.cell.line = lapply(observed.synergies.files, 
  function(file) {
    get_observed_synergies(file)
  }
)

# Models Stable State (1 per model)
models.stable.state.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/models_stable_state")
})

models.stable.state.per.cell.line = lapply(models.stable.state.files,
  function(file) {
    as.matrix(read.table(file, check.names = FALSE))
  }
)

# Models Link Operators
models.link.operator.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/models_equations")
})

models.link.operators.per.cell.line = lapply(models.link.operator.files,
  function(file) {
    as.matrix(read.table(file, check.names = FALSE))
  }
)

# the node names used in our analysis
node.names = colnames(models.stable.state.per.cell.line[[1]])

# Steady States
steady.state.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/steady_state")
})

steady.state.per.cell.line = lapply(steady.state.files,
  function(file) {
    ss.df = read.table(file, sep = "\t", stringsAsFactors = FALSE)
    steady.state = ss.df[,2]
    names(steady.state) = ss.df[,1]
    
    # change value to NA for nodes for which there was no activity found (dash)
    steady.state[steady.state == "-"] = NA
    
    # keep only the nodes that are included in the analysis
    steady.state = prune_and_reorder_vector(steady.state, node.names)
    
    # return an integer vector since the activity values are binarized (0,1)
    return(sapply(steady.state, as.integer))
  }
)

The random model input data:

random.dir = paste0(getwd(), "/random")
random.model.predictions = get_model_predictions(paste0(random.dir, "/model_predictions"))

random.models.stable.state = as.matrix(
  read.table(file = paste0(random.dir, "/models_stable_state"), check.names = FALSE)
)

random.models.link.operator =
  as.matrix(read.table(file = paste0(random.dir, "/models_equations"), check.names = FALSE))

Model Analysis

In order to find the number of true positive (TP) predicted synergies, MCC scores and fitness scores for each of the models in each of the 9 datasets, we use functions from the emba R package.

Cell-specific

We find the MCC, TP and fitness values for each model per cell line (note that each model’s stable state in a specific cell line is matched against the steady state from that cell line):

models.tp.per.cell.line = list()
models.mcc.per.cell.line = list()
models.fitness.per.cell.line = list()

for (cell.line in cell.lines) {
  model.predictions = model.predictions.per.cell.line[[cell.line]]
  observed.synergies = observed.synergies.per.cell.line[[cell.line]]
  number.of.drug.comb.tested = ncol(model.predictions.per.cell.line[[cell.line]])
  
  # Split model.predictions to positive (observed) and negative (non-observed) results
  observed.model.predictions =
    get_observed_model_predictions(model.predictions, observed.synergies)
  unobserved.model.predictions =
    get_unobserved_model_predictions(model.predictions, observed.synergies)
  
  # Count the predictions of the observed synergies per model (TP)
  models.tp.per.cell.line[[cell.line]] = calculate_models_synergies_tp(observed.model.predictions)
  
  # Calculate Matthews Correlation Coefficient (MCC) for every model
  models.mcc.per.cell.line[[cell.line]] = 
    calculate_models_mcc(observed.model.predictions,
                         unobserved.model.predictions,
                         number.of.drug.comb.tested)
  
  # Fitness per model contrasted to steady state from cell line
  models.fitness.per.cell.line[[cell.line]] = 
    apply(models.stable.state.per.cell.line[[cell.line]], 1, get_percentage_of_matches, 
          steady.state.per.cell.line[[cell.line]])
}

Random Models

Next, we find the MCC, TP and fitness values for each random model per cell line (note that each random model’s stable state in a specific cell line is matched against the steady state from that cell line and that the random models’ stable state data does not change per cell line, i.e. same random.models.stable.state object):

random.models.mcc.per.cell.line = list()
random.models.tp.per.cell.line = list()
random.models.fitness.per.cell.line = list()

for (cell.line in cell.lines) {
  observed.synergies = observed.synergies.per.cell.line[[cell.line]]
  number.of.drug.comb.tested = ncol(random.model.predictions)
  
  # Split model.predictions to positive (observed) and negative (non-observed) results
  observed.model.predictions =
    get_observed_model_predictions(random.model.predictions, observed.synergies)
  unobserved.model.predictions =
    get_unobserved_model_predictions(random.model.predictions, observed.synergies)
  
   # Count the predictions of the observed synergies per model (TP)
  random.models.tp.per.cell.line[[cell.line]] = 
    calculate_models_synergies_tp(observed.model.predictions)
  
  # Calculate Matthews Correlation Coefficient (MCC) for every model
  random.models.mcc.per.cell.line[[cell.line]] = 
    calculate_models_mcc(observed.model.predictions,
                         unobserved.model.predictions,
                         number.of.drug.comb.tested)
  
  # Fitness per model contrasted to steady state from cell line
  random.models.fitness.per.cell.line[[cell.line]] = 
    apply(random.models.stable.state, 1, get_percentage_of_matches, 
          steady.state.per.cell.line[[cell.line]])
}

Choose best dataset

We now want to find the best dataset & cell line for our subsequent analysis - that is to show the performance vs fitness correlation. The argument here is that we want to choose a boolean model dataset that has a large enough fitness value range combined with a large TP and/or MCC value range, since with smaller value ranges it would be harder to distinguish the difference of the estimated distributions of the fitness scores belonging to different performance classes (i.e. models’ fitnesses that belong to different classification groups with the metric being either the number of TPs or the MCC score).

The next summary statistics tables and box-plots will help us determine exactly which cell line and dataset to use:

cell.specific.model.data = matrix(data = NA, nrow = length(cell.lines), ncol = 11)
rownames(cell.specific.model.data) = cell.lines
colnames(cell.specific.model.data) = c("fitness range", "Min fitness", 
  "Max fitness", "Mean fitness", "Median fitness", "MCC range", "Min MCC", 
  "Max MCC", "Mean MCC", "Median MCC", "Max TPR")

for (cell.line in cell.lines) {
  models.fitness = models.fitness.per.cell.line[[cell.line]]
  models.mcc = models.mcc.per.cell.line[[cell.line]]
  models.tp = models.tp.per.cell.line[[cell.line]]
  
  fit.summary = unclass(summary(models.fitness))
  mcc.summary = unclass(summary(models.mcc))
  max.tpr = max(models.tp) / length(observed.synergies.per.cell.line[[cell.line]])
  
  cell.specific.model.data[cell.line, "fitness range"] = fit.summary["Max."] - fit.summary["Min."]
  cell.specific.model.data[cell.line, "Min fitness"] = fit.summary["Min."]
  cell.specific.model.data[cell.line, "Max fitness"] = fit.summary["Max."]
  cell.specific.model.data[cell.line, "Mean fitness"] = fit.summary["Mean"]
  cell.specific.model.data[cell.line, "Median fitness"] = fit.summary["Median"]
  
  cell.specific.model.data[cell.line, "MCC range"] = mcc.summary["Max."] - mcc.summary["Min."]
  cell.specific.model.data[cell.line, "Min MCC"] = mcc.summary["Min."]
  cell.specific.model.data[cell.line, "Max MCC"] = mcc.summary["Max."]
  cell.specific.model.data[cell.line, "Mean MCC"] = mcc.summary["Mean"]
  cell.specific.model.data[cell.line, "Median MCC"] = mcc.summary["Median"]
  
  cell.specific.model.data[cell.line, "Max TPR"] = max.tpr
}

# color columns
fit.breaks = quantile(cell.specific.model.data[,"fitness range"], probs = seq(.05, .95, .05), na.rm = TRUE)
fit.colors = round(seq(255, 40, length.out = length(fit.breaks) + 1), 0) %>%
  {paste0("rgb(255,", ., ",", ., ")")} # red
mcc.breaks = quantile(cell.specific.model.data[,"MCC range"], probs = seq(.05, .95, .05), na.rm = TRUE)
mcc.colors = round(seq(255, 40, length.out = length(mcc.breaks) + 1), 0) %>%
  {paste0("rgb(", ., ",255,", ., ")")} # green

caption.title = "Table 1: Fitness, MCC scores and TP (True positives) for the Cell-specific model predictions across 8 Cell Lines"
datatable(data = cell.specific.model.data, 
          options = list(dom = "t"), # just show the table
          caption = htmltools::tags$caption(caption.title, style="color:#dd4814; font-size: 18px")) %>% 
  formatRound(1:11, digits = 3) %>%
  formatStyle(columns = c("fitness range"), backgroundColor = styleInterval(fit.breaks, fit.colors)) %>%
  formatStyle(columns = c("MCC range"), backgroundColor = styleInterval(mcc.breaks, mcc.colors))
random.model.data = matrix(data = NA, nrow = length(cell.lines), ncol = 11)
rownames(random.model.data) = cell.lines
colnames(random.model.data) = c("fitness range", "Min fitness", 
  "Max fitness", "Mean fitness", "Median fitness", "MCC range", "Min MCC", 
  "Max MCC", "Mean MCC", "Median MCC", "Max TPR")

for (cell.line in cell.lines) {
  models.fitness = random.models.fitness.per.cell.line[[cell.line]]
  models.mcc = random.models.mcc.per.cell.line[[cell.line]]
  models.tp = random.models.tp.per.cell.line[[cell.line]]
  
  fit.summary = unclass(summary(models.fitness))
  mcc.summary = unclass(summary(models.mcc))
  max.tpr = max(models.tp) / length(observed.synergies.per.cell.line[[cell.line]])
  
  random.model.data[cell.line, "fitness range"] = fit.summary["Max."] - fit.summary["Min."]
  random.model.data[cell.line, "Min fitness"] = fit.summary["Min."]
  random.model.data[cell.line, "Max fitness"] = fit.summary["Max."]
  random.model.data[cell.line, "Mean fitness"] = fit.summary["Mean"]
  random.model.data[cell.line, "Median fitness"] = fit.summary["Median"]
  
  random.model.data[cell.line, "MCC range"] = mcc.summary["Max."] - mcc.summary["Min."]
  random.model.data[cell.line, "Min MCC"] = mcc.summary["Min."]
  random.model.data[cell.line, "Max MCC"] = mcc.summary["Max."]
  random.model.data[cell.line, "Mean MCC"] = mcc.summary["Mean"]
  random.model.data[cell.line, "Median MCC"] = mcc.summary["Median"]
  
  random.model.data[cell.line, "Max TPR"] = max.tpr
}

# color columns
fit.breaks = quantile(random.model.data[,"fitness range"], probs = seq(.05, .95, .05), na.rm = TRUE)
fit.colors = round(seq(255, 40, length.out = length(fit.breaks) + 1), 0) %>%
  {paste0("rgb(255,", ., ",", ., ")")} # red
mcc.breaks = quantile(random.model.data[,"MCC range"], probs = seq(.05, .95, .05), na.rm = TRUE)
mcc.colors = round(seq(255, 40, length.out = length(mcc.breaks) + 1), 0) %>%
  {paste0("rgb(", ., ",255,", ., ")")} # green

caption.title = "Table 2: Fitness, MCC scores and TP (True positives) for the random model predictions across 8 Cell Lines"
datatable(data = random.model.data, 
          options = list(dom = "t"), # just show the table
          caption = htmltools::tags$caption(caption.title, style="color:#dd4814; font-size: 18px")) %>% 
  formatRound(1:11, digits = 3) %>%
  formatStyle(columns = c("fitness range"), backgroundColor = styleInterval(fit.breaks, fit.colors)) %>%
  formatStyle(columns = c("MCC range"), backgroundColor = styleInterval(mcc.breaks, mcc.colors))

The below box plots compare the MCC values and fitness scores across all cell lines between the cell-specific models (trained to steady state) and the random ones (trained to proliferation):

num.of.models = nrow(random.models.stable.state)
data.list = list()

for (cell.line in cell.lines) {
  cell.line.vec = as.data.frame(rep(cell.line, num.of.models), stringsAsFactors = FALSE)
  models.mcc.cell.specific = remove_rownames(as.data.frame(models.mcc.per.cell.line[[cell.line]]))
  models.mcc.random        = remove_rownames(as.data.frame(random.models.mcc.per.cell.line[[cell.line]]))
  
  models.fitness.cell.specific = remove_rownames(as.data.frame(models.fitness.per.cell.line[[cell.line]]))
  models.fitness.random        = remove_rownames(as.data.frame(random.models.fitness.per.cell.line[[cell.line]]))
  
  data.list[[cell.line]] = bind_cols(cell.line.vec, models.mcc.cell.specific, 
    models.mcc.random, models.fitness.cell.specific, models.fitness.random)
}

data = bind_rows(data.list)
colnames(data) = c("cell.line", "MCC cell-specific", "MCC random", 
                   "fitness cell-specific", "fitness random")
# Note the cell-specific models have NaN MCC values
ggboxplot(data, x = "cell.line", y = c("MCC cell-specific", "MCC random"),
          palette = brewer.pal(3, "Set1"), merge = "asis",
          xlab = "Cell Lines", ylab = "MCC values", add = "point", add.params = list(size = 0.5))

ggboxplot(data, x = "cell.line", y = c("fitness cell-specific", "fitness random"),
          palette = brewer.pal(3, "Set1"), merge = "asis",
          xlab = "Cell Lines", ylab = "Fitness values", add = "point", add.params = list(size = 0.5))

So, from the two tables and the two box plots above we conclude that:

  • In general, the random models offer a larger range of fitness values when their stable states are matched against the steady state of each particular cell line. This is something we expected since these models weren’t fitted to a specific cell-line steady state (meaning that they weren’t chosen from the Gitsbe module as the 3 best from each simulation that match that steady state as best as possible) but rather to a more generic state of proliferation. Thus, they represent a set of models with larger variation in terms of structure (boolean model equations) compared to the cell-specific generated ones.
  • The cell-specific models have always a larger maximum fitness and median value compared to the random ones for each respective cell line.
  • In each cell line (with the expection of SW620) there are always cell-specific models that show better performance than the random ones (have a higher MCC value)


All in all, we will use the random models data, contrasted to the steady state of the A498 cell line (see Table 2). This dataset has the largest fitness and MCC value range (second largest TPR value as well) combined in both tables above.


best.cell.line = "A498"

fit = random.models.fitness.per.cell.line[[best.cell.line]]
tp  = random.models.tp.per.cell.line[[best.cell.line]]
mcc = random.models.mcc.per.cell.line[[best.cell.line]]

Statistical Analysis

Data preprocessing

Firstly, we filter the data by finding the unique models - those that have strictly different boolean equations. Then, we take a random sample out of these (while stabilizing the seed number for reproducibility purposes):

# For reproducibility
set.seed(0)
sample.size = 1000

unique.models = rownames(unique(random.models.link.operator))
unique.models.sample = sample(unique.models, size = sample.size)

fit.unique = fit[names(fit) %in% unique.models.sample]
tp.unique  = tp[names(tp) %in% unique.models.sample]
mcc.unique = mcc[names(mcc) %in% unique.models.sample]

df = as.data.frame(cbind(fit.unique, mcc.unique, tp.unique))

Note that the fitness and MCC score are continuous variables while the number of true positives (TP) is discrete.

Correlation Plots

Then, we check if the our data is normally distributed (using the Shapiro-Wilk test for normality and the Q-Q plots):

ggqqplot(data = df, x = "fit.unique", ylab = "Fitness")

ggqqplot(data = df, x = "mcc.unique", ylab = "MMC")

shapiro.test(x = sample(fit.unique))

    Shapiro-Wilk normality test

data:  sample(fit.unique)
W = 0.99266, p-value = 7.375e-05
shapiro.test(x = sample(mcc.unique))

    Shapiro-Wilk normality test

data:  sample(mcc.unique)
W = 0.91827, p-value < 2.2e-16

From the above results we observe that both the fitness and MCC scores are surely not normally distributed (with statistical significance). Thus, we will use non-parametric correlation scores, namely the Spearman and Kendall rank-based correlation tests, to check the correlation between the two continuous variables (models fitness values and their corresponding MCC score):

ggscatter(df, x = "mcc.unique", y = "fit.unique", 
          title = "Fitness vs Performance (MCC) - Spearman Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "spearman", label.x.npc = 0.7, label.y.npc = 1),
          xlab = "MCC scores", ylab = "Fitness Values")

ggscatter(df, x = "mcc.unique", y = "fit.unique", 
          title = "Fitness vs Performance (MCC) - Kendall Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall", label.x.npc = 0.7, label.y.npc = 1),
          xlab = "MCC scores", ylab = "Fitness Values")

From the correlation plots above, we observe a weak/small positive correlation between performance and fitness to the steady state.

To assess the correlation between the number of TP predictions of the models (discrete variable) and the models fitness (continuous variable), we construct a predictor of the categorical variable from the continuous variable: if the resulting classifier has a high degree of fit we can conclude the two variables share a relationship and are indeed correlated. Since there are more than 2 TP classes (values) in the dataset, we will use Multinomial Logistic Regression and fit log-linear models via neural networks:

model.classifier = multinom(data = df, formula = tp.unique ~ fit.unique)
# weights:  18 (10 variable)
initial  value 1791.759469 
iter  10 value 1361.771381
iter  20 value 1356.825440
iter  30 value 1356.792829
iter  40 value 1356.765176
final  value 1356.764853 
converged
pseudo.r2.measures = pR2(model.classifier)
fitting null model for pseudo-r2
# weights:  12 (5 variable)
initial  value 1791.759469 
iter  10 value 1415.698062
final  value 1415.682090 
converged
pseudo.r2.measures["McFadden"]
  McFadden 
0.04161756 

To measure the goodness-of-fit for our model, there are several measures proposed for logistic regression. We emphasize on the McFaddens’s pseudo-\(R^2\) measure for our classifier, for which a value of \(0.2-0.4\) would indicate an excellent fit [source]. Since we found less, we can also assume that there is only a weak/small positive correlation between the number of TPs and the fitness score of the models.

Next, we will next proceed with a more elaborate analysis, where the models will be split to different performance classes (TP or MCC score-derived) and the statistical correlation between the individual groups will be tested (with regards to their fitness scores).

TP-class vs fitness

First, some box and density plots to see practically what and where is the difference between the models fitness values belonging to different TP-classes:

# Box plots
ggboxplot(df, x = "tp.unique", y = "fit.unique", color = "tp.unique",
          palette = usefun:::colors.100[1:length(unique(tp.unique))],
          xlab = "True Positives (TP)", ylab = "Fitness values")

# Density Plots
densities = list()
for (tp.num in sort(unique(tp.unique))) {
  x = df %>%
    filter(tp.unique == tp.num) %>%
    select_at(.vars = c("fit.unique"))
  den = density(x$fit.unique)
  densities[[paste0(tp.num, " (", den$n, ")")]] = den
}

make_multiple_density_plot(densities, legend.title = "TP classes (#models)",
        title = "Density Estimation", x.axis.label = "Fitness score")

As we can see from the above plots, there is positive correlation between the classes that predicted 0,1 and 2 TP synergies as well as between the 0 TP class and the 3,4,5-TP classes.


Mathematically, we show that the group distributions are indeed different using the Kruskal-Wallis Rank Sum Test:

# Hypothesis testing: Are the location parameters of the distribution of x the same in each group?
kruskal.test(x = df[,"fit.unique"], g = df[, "tp.unique"])

    Kruskal-Wallis rank sum test

data:  df[, "fit.unique"] and df[, "tp.unique"]
Kruskal-Wallis chi-squared = 108.06, df = 5, p-value < 2.2e-16

As the p-value is less than the significance level \(0.05\), we can conclude that there are significant differences between the different groups of fitness values. To see exactly which pair of groups are different we perform pairwise Wilcoxon rank sum tests and we draw a heatmap of the each test’s p-value:

res = pairwise.wilcox.test(x = df[,"fit.unique"], g = df[, "tp.unique"], p.adjust.method = "BH")
p.value.mat = res$p.value
col_fun = colorRamp2(breaks = c(0, 0.05, 0.5, 1), c("green", "white", "orange", "red"))
ht = Heatmap(matrix = p.value.mat, cluster_rows = FALSE, cluster_columns = FALSE,
  na_col = "white", name = "p-value", col = col_fun, column_names_rot = 0,
  row_title = "TP", row_title_rot = 0, row_names_side = "left",
  column_title = "P-values (Pairwise Wilcoxon tests) - TP-classified fitnesses", column_title_gp = gpar(fontsize = 20),
  cell_fun = function(j, i, x, y, width, height, fill) {
    if (!is.na(p.value.mat[i,j]))
        grid.text(sprintf("%.6f", p.value.mat[i, j]), x, y, gp = gpar(fontsize = 16))
})
draw(ht)

As we can see above there is significant difference between groups of fitness values belonging to models that predicted \(0\), \(1\) and \(2\) TP synergies, but not between these and the larger performant groups (\(TP=3,4,5\)) - with the exception of the group with \(TP=2\).

Note that the number of true positive predictions is an one-dimensional metric and by excluding the TN, FP and FNs we have a less-informant (and arguably incorrect) picture of the models performance classification. That’s why we will proceed with the more balanced MCC score for classifing the models fitnesses to different performance groups.

MCC-class vs fitness

Firstly, we perform a univariate k-means clustering to split the models MCC values to different classes and plot the data histogram. The number of MCC classes to split the data can be arbitrarily chosen and so we chose one less than the maximum number of TPs predicted:

num.of.mcc.classes = 5
mcc.class.ids = 1:num.of.mcc.classes

# find the clusters
res = Ckmeans.1d.dp(x = df[,"mcc.unique"], k = num.of.mcc.classes)
mcc.class.id = res$cluster
df = cbind(df, mcc.class.id)

plot_mcc_classes_hist(df[,"mcc.unique"], df[,"mcc.class.id"],
                      num.of.mcc.classes, mcc.class.ids)

Then we show some box and density plots to see practically what and where is the difference between the models fitness values belonging to different MCC-classes:

# Box plots
# if you want to add p-values on the boxplot
#mcc.class.id.cmps = list(c("1", "2"), c("1", "3"), c("3", "5"))
ggboxplot(df, x = "mcc.class.id", y = "fit.unique", color = "mcc.class.id",
          palette = usefun:::colors.100[1:num.of.mcc.classes],
          xlab = "MCC class", ylab = "Fitness values")

# + stat_compare_means(comparisons = mcc.class.id.cmps) + stat_compare_means()

# Density Plots
densities = list()
for (id in mcc.class.ids) {
  x = df %>%
    filter(mcc.class.id == id) %>%
    select_at(.vars = c("fit.unique"))
  den = density(x$fit.unique)
  densities[[paste0(id, " (", res$size[id], ")")]] = den
}

make_multiple_density_plot(densities, legend.title = "MCC classes (#models)",
        title = "Density Estimation", x.axis.label = "Fitness score")

As we can see from the above plots, there is positive correlation between the distribution of MCC scores in each class and the respective fitness scores, though not between the 3rd class and the 4th or 5th (it’s negative).


Note also that the 5th class has a lot less models than the rest. Mathematically, we show that the group distributions are indeed different using the Kruskal-Wallis Rank Sum Test:

# Hypothesis testing: Are the location parameters of the distribution of x the same in each group?
kruskal.test(x = df[,"fit.unique"], g = df[, "mcc.class.id"])

    Kruskal-Wallis rank sum test

data:  df[, "fit.unique"] and df[, "mcc.class.id"]
Kruskal-Wallis chi-squared = 135.52, df = 4, p-value < 2.2e-16

As the p-value is less than the significance level \(0.05\), we can conclude that there are significant differences between the different groups of fitness values.

Next, to show that most of the groups are statistically different, we perform pairwise Wilcoxon rank sum tests and draw a heatmap of the each test’s p-value:

pair.res = pairwise.wilcox.test(x = df[,"fit.unique"], g = df[, "mcc.class.id"], p.adjust.method = "BH")
p.value.mat = pair.res$p.value
# same: compare_means(fit.unique ~ mcc.class.id, data = df)
col_fun = colorRamp2(breaks = c(0, 0.05, 0.5, 1), c("green", "white", "orange", "red"))
ht = Heatmap(matrix = p.value.mat, cluster_rows = FALSE, cluster_columns = FALSE,
  na_col = "white", name = "p-value", col = col_fun, column_names_rot = 0,
  row_title = "MCC class id", row_title_rot = 90, row_names_side = "left",
  column_title = "P-values (Pairwise Wilcoxon tests) - MCC-classified fitnesses", column_title_gp = gpar(fontsize = 20),
  cell_fun = function(j, i, x, y, width, height, fill) {
    if (!is.na(p.value.mat[i,j]))
        grid.text(sprintf("%.6f", p.value.mat[i, j]), x, y, gp = gpar(fontsize = 20))
})
draw(ht)

R session info

xfun::session_info()
R version 3.6.1 (2019-07-05)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.04.3 LTS, RStudio 1.2.5001

Locale:
  LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
  LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
  LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
  LC_PAPER=en_US.UTF-8       LC_NAME=C                 
  LC_ADDRESS=C               LC_TELEPHONE=C            
  LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

Package version:
  assertthat_0.2.1     backports_1.1.5      base64enc_0.1-3     
  BH_1.69.0.1          bibtex_0.4.2         bookdown_0.14       
  circlize_0.4.8       Ckmeans.1d.dp_4.3.0  cli_1.1.0           
  clue_0.3-57          cluster_2.1.0        codetools_0.2-16    
  colorspace_1.4-1     compiler_3.6.1       ComplexHeatmap_2.0.0
  cowplot_1.0.0        crayon_1.3.4         crosstalk_1.0.0     
  datasets_3.6.1       digest_0.6.21        dplyr_0.8.3         
  DT_0.9               ellipsis_0.3.0       emba_0.1.2          
  evaluate_0.14        fansi_0.4.0          fastmap_1.0.1       
  gbRd_0.4-11          GetoptLong_0.1.7     ggplot2_3.2.1       
  ggpubr_0.2.3         ggrepel_0.8.1        ggsci_2.9           
  ggsignif_0.6.0       GlobalOptions_0.1.1  glue_1.3.1          
  graphics_3.6.1       grDevices_3.6.1      grid_3.6.1          
  gridExtra_2.3        gtable_0.3.0         highr_0.8           
  htmltools_0.4.0      htmlwidgets_1.5.1    httpuv_1.5.2        
  igraph_1.2.4.1       jsonlite_1.6         knitr_1.25          
  labeling_0.3         later_1.0.0          lattice_0.20.38     
  lazyeval_0.2.2       lifecycle_0.1.0      magrittr_1.5        
  markdown_1.1         MASS_7.3-51.4        Matrix_1.2.17       
  methods_3.6.1        mgcv_1.8.29          mime_0.7            
  munsell_0.5.0        nlme_3.1.141         nnet_7.3-12         
  packrat_0.5.0        parallel_3.6.1       pillar_1.4.2        
  pkgconfig_2.0.3      plogr_0.2.0          plyr_1.8.4          
  png_0.1-7            polynom_1.4.0        promises_1.1.0      
  pscl_1.5.2           purrr_0.3.2          R6_2.4.0            
  RColorBrewer_1.1-2   Rcpp_1.0.2           Rdpack_0.11-0       
  reshape2_1.4.3       rje_1.10.10          rjson_0.2.20        
  rlang_0.4.0          rmarkdown_1.16       rstudioapi_0.10     
  scales_1.0.0         shape_1.4.4          shiny_1.4.0         
  sourcetools_0.1.7    splines_3.6.1        stats_3.6.1         
  stringi_1.4.3        stringr_1.4.0        tibble_2.1.3        
  tidyr_1.0.0          tidyselect_0.2.5     tinytex_0.16        
  tools_3.6.1          usefun_0.4.3         utf8_1.1.4          
  utils_3.6.1          vctrs_0.2.0          viridisLite_0.3.0   
  visNetwork_2.0.8     withr_2.1.2          xfun_0.10           
  xtable_1.8-4         yaml_2.2.0           zeallot_0.1.0       
LS0tCnRpdGxlOiAiQXRvcG8gUGVyZm9ybWFuY2UgdnMgRml0bmVzcyBNb2RlbCBBbmFseXNpcyIKYXV0aG9yOiAiW0pvaG4gWm9ib2xhc10oaHR0cHM6Ly9naXRodWIuY29tL2JibG9kZm9uKSIKZGF0ZTogIkxhc3QgdXBkYXRlZDogYHIgZm9ybWF0KFN5cy50aW1lKCksICclZCAlQiwgJVknKWAiCm91dHB1dDoKICBodG1sX2RvY3VtZW50OgogICAgY3NzOiBjc3Mvc3R5bGUuY3NzCiAgICB0aGVtZTogdW5pdGVkCiAgICB0b2M6IHRydWUKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiBmYWxzZQogICAgICBzbW9vdGhfc2Nyb2xsOiB0cnVlCiAgICBudW1iZXJfc2VjdGlvbnM6IGZhbHNlCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUKICAgIGNvZGVfZG93bmxvYWQ6IHRydWUKLS0tCgpgYGB7ciBSZW5kZXIgY29tbWFuZCwgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0KI3JtYXJrZG93bjo6cmVuZGVyKGlucHV0ID0gIi4vcGVyZm9ybWFuY2VfdnNfZml0bmVzcy5SbWQiLCBvdXRwdXRfZm9ybWF0ID0gImh0bWxfZG9jdW1lbnQiLCBvdXRwdXRfZGlyID0gIi4uLy4uL2RvY3MvYXRvcG8vY2VsbC1saW5lcy0yNTAwLyIpCmBgYAoKIyMgSW50cm8gey19CgpUaGUgcHVycG9zZSBvZiB0aGlzIGFuYWx5c2lzIGlzIHRvIGZpbmQgYSBjb3JyZWxhdGlvbiBiZXR3ZWVuIHRoZSBib29sZWFuIG1vZGVscwoqKmZpdG5lc3MgdG8gYSBzdGVhZHkgc3RhdGUgYWN0aXZpdHkgcHJvZmlsZSoqIGFuZCB0aGVpciAqKnBlcmZvcm1hbmNlKiogaW4gdGVybXMgb2YgdGhlCm51bWJlciBvZiAqVHJ1ZSBQb3NpdGl2ZSogKFRQKSBzeW5lcmdpZXMgcHJlZGljdGVkIGFuZC9vciB0aGUgb3ZlcmFsbCAqTUNDIHNjb3JlKgooTWF0dGhld3MgQ29ycmVsYXRpb24gQ29lZmZpY2llbnQgc2NvcmUpLiBXZSB3YW50IHRvIHNob3cgdGhhdCAqKmEgY2xvc2VyIGZpdG5lc3MgCnRvIHRoZSBzdGVhZHkgc3RhdGUgc3VnZ2VzdHMgbW9yZSBwcmVkaWN0aXZlIG1vZGVscyoqLCBjb3Jyb2JvcmF0aW5nIHRodXMgb3VyIHByb29mIG9mIGNvbmNlcHQgb2YgdXNpbmcgYW4gZW5zZW1ibGUtYmFzZWQgYXBwcm9hY2ggd2hlcmUgbW9kZWxzIGFyZSB0cmFpbmVkIHRvd2FyZHMgYSAKc3BlY2lmaWMgc3RlYWR5IHN0YXRlIHNpZ25hbGluZyBwYXR0ZXJuIGZvciBkcnVnIGNvbWJpbmF0aW9uIHByZWRpY3Rpb25zLgoKVGhlIGJvb2xlYW4gbW9kZWwgZGF0YXNldHMgd2Ugd2lsbCB1c2UgYXJlIGluIHRvdGFsICQ5JDogb25lIGZvciBlYWNoIGNlbGwgbGluZSAKb2YgaW50ZXJlc3QgKDggY2VsbCBsaW5lcykgd2hlcmUgdGhlIG1vZGVscyB3ZXJlICoqZml0dGVkIHRvIGEgc3BlY2lmaWMgc3RlYWR5IHN0YXRlKiogaW4gZWFjaCAKY2FzZSBhbmQgb25lIGZvciB0aGUgc28tY2FsbGVkICoqcmFuZG9tIG1vZGVscyoqIHdoaWNoIHdlcmUgZ2VuZXJhdGVkICpyYW5kb21seSogaW4gCnRoZSBzZW5zZSB0aGF0IHdlcmUgZml0dGVkIG9ubHkgdG8gYSBwcm9saWZlcmF0aW9uIHN0YXRlIChzaW11bGF0aW9ucyB3ZXJlIGRvbmUgdXNpbmcgCnRoZSBEcnVnTG9naWNzIHNvZnR3YXJlIG1vZHVsZXMgYEdpdHNiZWAgYW5kIGBEcmFibWVgKS4KCkVhY2ggYm9vbGVhbiBtb2RlbCBkYXRhc2V0IGNvbnN0aXR1ZXMgb2Y6CgotIFRoZSAqKm1vZGVsIHByZWRpY3Rpb25zKiogZmlsZSB3aGljaCBoYXMgZm9yIGVhY2ggbW9kZWwgdGhlIHByZWRpY3Rpb24gZm9yIAplYWNoIGRydWcgY29tYmluYXRpb24gdGVzdGVkICgqMCogPSBubyBzeW5lcmd5IHByZWRpY3RlZCwgKjEqID0gc3luZXJneSAKcHJlZGljdGVkLCAqTkEqID0gY291bGRuJ3QgZmluZCBzdGFibGUgc3RhdGVzIGluIGVpdGhlciB0aGUgZHJ1ZyBjb21iaW5hdGlvbiAKaW5oaWJpdGVkIG1vZGVsIG9yIGluIGFueSBvZiB0aGUgdHdvIHNpbmdsZS1kcnVnIGluaGliaXRlZCBtb2RlbHMpCi0gVGhlICoqbW9kZWxzIHN0YWJsZSBzdGF0ZSoqIChvbmUgcGVyIG1vZGVsKS4gQSAqKmZpdG5lc3Mgc2NvcmUqKgpmb3IgZWFjaCBtb2RlbCBjYW4gZWFzaWx5IGJlIGNhbGN1bGF0ZWQgdGhlbiBieSBtYXRjaGluZyB0aGUgbW9kZWwncyBzdGFibGUgCnN0YXRlICh3aGljaCBpcyBzb21ldGhpbmcgaW5oZXJlbnQgaW4gdGhlIGJvb2xlYW4ncyBtb2RlbCBzdHJ1Y3R1cmUsIGEgdW5pcXVlIApmaXhwb2ludCBhdHRyYWN0b3IpIHdpdGggdGhlIHN0ZWFkeSBzdGF0ZSBvZiBpbnRlcmVzdCwgbm9kZSBwZXIgbm9kZS4KQSAqKmhpZ2hlciBmaXRuZXNzIHNjb3JlKiogd291bGQgbWVhbiBhIGJldHRlciBtYXRjaCBvZiBhIG1vZGVsJ3MgCnN0YWJsZSBzdGF0ZSB0byB0aGUgY2VsbCBsaW5lIGRlcml2ZWQgc3RlYWR5IHN0YXRlIChhIHBlcmZlY3QgbWF0Y2ggd291bGQgcmVzdWx0IAppbiBhIGZpdG5lc3Mgb2YgMSkuCi0gVGhlICoqbW9kZWxzIGxpbmsgb3BlcmF0b3JzKiogd2hpY2ggaXMgYSByZXByZXNlbnRhdGlvbiBvZiB0aGUgYm9vbGVhbiBlcXVhdGlvbnMKb2YgZWFjaCBtb2RlbC4gRWFjaCBib29sZWFuIGVxdWF0aW9uIGlzIGluIHRoZSBmb3JtOiAqKlRhcmdldCAqPSAoQWN0aXZhdG9yIE9SIEFjdGl2YXRvciBPUi4uLikgQU5EIE5PVCAoSW5oaWJpdG9yIE9SIEluaGliaXRvciBPUi4uLikqKiBhbmQgdGhlIGRpZmZlcmVuY2UgYmV0d2VlbiB0aGUgbW9kZWxzIGNhbiBiZSBmb3VuZCBpbiB0aGUgCipsaW5rIG9wZXJhdG9yKiAoKjEqID0gJ09SIE5PVCcsICowKiA9ICdBTkQgTk9UJywgb3IgYWJzZW50KSB3aGljaCBoYXMgYmVlbiAKbXV0YXRlZCAoY2hhbmdlZCkgdGhyb3VnaCB0aGUgZ2VuZXRpYyBhbGdvcml0aG0gaW4gYEdpdHNiZWAuIE5vdGUgdGhhdCB0aGUgZXF1YXRpb25zIHRoYXQgZG8gCm5vdCBoYXZlIGxpbmsgb3BlcmF0b3JzIGFyZSAqdGhlIHNhbWUgZm9yIGV2ZXJ5IG1vZGVsKiBhbmQgYXJlIHRodXMgZGlzY2FyZGVkLgotIFRoZSAqKm9ic2VydmVkIHN5bmVyZ2llcyoqIGZpbGUgd2hpY2ggbGlzdHMgdGhlIGRydWcgY29tYmluYXRpb25zIHRoYXQgd2VyZSAKb2JzZXJ2ZWQgYXMgc3luZXJnaXN0aWMgZm9yIGVhY2ggY2VsbCBsaW5lLgotIFRoZSAqKnN0ZWFkeSBzdGF0ZSoqIGZpbGUgd2hpY2ggbGlzdHMgdGhlIG5ldHdvcmsgbm9kZXMgKHByb3RlaW4sIGdlbmUsIGNvbXBsZXhlcwpuYW1lcywgZXRjKSBhbmQgdGhlaXIgYWN0aXZpdHkgdmFsdWUgKDAgb3IgMSwgcmVwcmVzZW50aW5nIGFuIGluaGliaXRlZCBvciBhY3RpdmUKbm9kZSByZXNwZWN0aXZlbHkpLiAKVGhpcyBpbnB1dCBpcyBwcm92aWRlZCBwZXIgY2VsbCBsaW5lIGFuZCBub3QgZm9yIHRoZSByYW5kb20gbW9kZWxzIHNpbmNlIHRoZXkgYXJlIGp1c3QgdHJhaW5lZCB0byBhIHByb2ZpbGVyYXRpb24gc3RhdGUuCgojIyBJbnB1dCB7LX0KCkxvYWRpbmcgbGlicmFyaWVzOgpgYGB7ciBMb2FkIGxpYnJhcmllcywgbWVzc2FnZSA9IEZBTFNFfQpsaWJyYXJ5KERUKQpsaWJyYXJ5KGdncHVicikKbGlicmFyeShlbWJhKQpsaWJyYXJ5KHVzZWZ1bikKbGlicmFyeShubmV0KQpsaWJyYXJ5KHBzY2wpCmxpYnJhcnkoQ29tcGxleEhlYXRtYXApCmxpYnJhcnkoY2lyY2xpemUpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkodGliYmxlKQpsaWJyYXJ5KENrbWVhbnMuMWQuZHApCmxpYnJhcnkoUkNvbG9yQnJld2VyKQpgYGAKCkZpcnN0IHdlIGxvYWQgdGhlIGNlbGwtc3BlY2lmaWMgaW5wdXQgZGF0YToKYGBge3IgQ2VsbC1zcGVjaWZpYyBJbnB1dCwgY2FjaGU9VFJVRX0KIyBDZWxsIExpbmVzCmNlbGwubGluZXMgPSBjKCJBNDk4IiwgIkFHUyIsICJEVTE0NSIsICJjb2xvMjA1IiwgIlNXNjIwIiwgIlNGMjk1IiwgIlVBQ0M2MiIsICJNREEtTUItNDY4IikKCmNlbGwubGluZS5kaXJzID0gc2FwcGx5KGNlbGwubGluZXMsIGZ1bmN0aW9uKGNlbGwubGluZSkgewogIHBhc3RlMChnZXR3ZCgpLCAiLyIsIGNlbGwubGluZSkKfSkKCiMgTW9kZWwgcHJlZGljdGlvbnMKbW9kZWwucHJlZGljdGlvbnMuZmlsZXMgPSBzYXBwbHkoY2VsbC5saW5lLmRpcnMsIGZ1bmN0aW9uKGNlbGwubGluZS5kaXIpIHsKICBwYXN0ZTAoY2VsbC5saW5lLmRpciwgIi9tb2RlbF9wcmVkaWN0aW9ucyIpCn0pCgptb2RlbC5wcmVkaWN0aW9ucy5wZXIuY2VsbC5saW5lID0gbGFwcGx5KG1vZGVsLnByZWRpY3Rpb25zLmZpbGVzLCAKICBmdW5jdGlvbihmaWxlKSB7CiAgICBnZXRfbW9kZWxfcHJlZGljdGlvbnMoZmlsZSkKICB9CikKCiMgT2JzZXJ2ZWQgc3luZXJnaWVzCm9ic2VydmVkLnN5bmVyZ2llcy5maWxlcyA9IHNhcHBseShjZWxsLmxpbmUuZGlycywgZnVuY3Rpb24oY2VsbC5saW5lLmRpcikgewogIHBhc3RlMChjZWxsLmxpbmUuZGlyLCAiL29ic2VydmVkX3N5bmVyZ2llcyIpCn0pCgpvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZSA9IGxhcHBseShvYnNlcnZlZC5zeW5lcmdpZXMuZmlsZXMsIAogIGZ1bmN0aW9uKGZpbGUpIHsKICAgIGdldF9vYnNlcnZlZF9zeW5lcmdpZXMoZmlsZSkKICB9CikKCiMgTW9kZWxzIFN0YWJsZSBTdGF0ZSAoMSBwZXIgbW9kZWwpCm1vZGVscy5zdGFibGUuc3RhdGUuZmlsZXMgPSBzYXBwbHkoY2VsbC5saW5lLmRpcnMsIGZ1bmN0aW9uKGNlbGwubGluZS5kaXIpIHsKICBwYXN0ZTAoY2VsbC5saW5lLmRpciwgIi9tb2RlbHNfc3RhYmxlX3N0YXRlIikKfSkKCm1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZSA9IGxhcHBseShtb2RlbHMuc3RhYmxlLnN0YXRlLmZpbGVzLAogIGZ1bmN0aW9uKGZpbGUpIHsKICAgIGFzLm1hdHJpeChyZWFkLnRhYmxlKGZpbGUsIGNoZWNrLm5hbWVzID0gRkFMU0UpKQogIH0KKQoKIyBNb2RlbHMgTGluayBPcGVyYXRvcnMKbW9kZWxzLmxpbmsub3BlcmF0b3IuZmlsZXMgPSBzYXBwbHkoY2VsbC5saW5lLmRpcnMsIGZ1bmN0aW9uKGNlbGwubGluZS5kaXIpIHsKICBwYXN0ZTAoY2VsbC5saW5lLmRpciwgIi9tb2RlbHNfZXF1YXRpb25zIikKfSkKCm1vZGVscy5saW5rLm9wZXJhdG9ycy5wZXIuY2VsbC5saW5lID0gbGFwcGx5KG1vZGVscy5saW5rLm9wZXJhdG9yLmZpbGVzLAogIGZ1bmN0aW9uKGZpbGUpIHsKICAgIGFzLm1hdHJpeChyZWFkLnRhYmxlKGZpbGUsIGNoZWNrLm5hbWVzID0gRkFMU0UpKQogIH0KKQoKIyB0aGUgbm9kZSBuYW1lcyB1c2VkIGluIG91ciBhbmFseXNpcwpub2RlLm5hbWVzID0gY29sbmFtZXMobW9kZWxzLnN0YWJsZS5zdGF0ZS5wZXIuY2VsbC5saW5lW1sxXV0pCgojIFN0ZWFkeSBTdGF0ZXMKc3RlYWR5LnN0YXRlLmZpbGVzID0gc2FwcGx5KGNlbGwubGluZS5kaXJzLCBmdW5jdGlvbihjZWxsLmxpbmUuZGlyKSB7CiAgcGFzdGUwKGNlbGwubGluZS5kaXIsICIvc3RlYWR5X3N0YXRlIikKfSkKCnN0ZWFkeS5zdGF0ZS5wZXIuY2VsbC5saW5lID0gbGFwcGx5KHN0ZWFkeS5zdGF0ZS5maWxlcywKICBmdW5jdGlvbihmaWxlKSB7CiAgICBzcy5kZiA9IHJlYWQudGFibGUoZmlsZSwgc2VwID0gIlx0Iiwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKQogICAgc3RlYWR5LnN0YXRlID0gc3MuZGZbLDJdCiAgICBuYW1lcyhzdGVhZHkuc3RhdGUpID0gc3MuZGZbLDFdCiAgICAKICAgICMgY2hhbmdlIHZhbHVlIHRvIE5BIGZvciBub2RlcyBmb3Igd2hpY2ggdGhlcmUgd2FzIG5vIGFjdGl2aXR5IGZvdW5kIChkYXNoKQogICAgc3RlYWR5LnN0YXRlW3N0ZWFkeS5zdGF0ZSA9PSAiLSJdID0gTkEKICAgIAogICAgIyBrZWVwIG9ubHkgdGhlIG5vZGVzIHRoYXQgYXJlIGluY2x1ZGVkIGluIHRoZSBhbmFseXNpcwogICAgc3RlYWR5LnN0YXRlID0gcHJ1bmVfYW5kX3Jlb3JkZXJfdmVjdG9yKHN0ZWFkeS5zdGF0ZSwgbm9kZS5uYW1lcykKICAgIAogICAgIyByZXR1cm4gYW4gaW50ZWdlciB2ZWN0b3Igc2luY2UgdGhlIGFjdGl2aXR5IHZhbHVlcyBhcmUgYmluYXJpemVkICgwLDEpCiAgICByZXR1cm4oc2FwcGx5KHN0ZWFkeS5zdGF0ZSwgYXMuaW50ZWdlcikpCiAgfQopCmBgYAoKVGhlIHJhbmRvbSBtb2RlbCBpbnB1dCBkYXRhOgpgYGB7ciBSYW5kb20gbW9kZWwgSW5wdXR9CnJhbmRvbS5kaXIgPSBwYXN0ZTAoZ2V0d2QoKSwgIi9yYW5kb20iKQpyYW5kb20ubW9kZWwucHJlZGljdGlvbnMgPSBnZXRfbW9kZWxfcHJlZGljdGlvbnMocGFzdGUwKHJhbmRvbS5kaXIsICIvbW9kZWxfcHJlZGljdGlvbnMiKSkKCnJhbmRvbS5tb2RlbHMuc3RhYmxlLnN0YXRlID0gYXMubWF0cml4KAogIHJlYWQudGFibGUoZmlsZSA9IHBhc3RlMChyYW5kb20uZGlyLCAiL21vZGVsc19zdGFibGVfc3RhdGUiKSwgY2hlY2submFtZXMgPSBGQUxTRSkKKQoKcmFuZG9tLm1vZGVscy5saW5rLm9wZXJhdG9yID0KICBhcy5tYXRyaXgocmVhZC50YWJsZShmaWxlID0gcGFzdGUwKHJhbmRvbS5kaXIsICIvbW9kZWxzX2VxdWF0aW9ucyIpLCBjaGVjay5uYW1lcyA9IEZBTFNFKSkKYGBgCgojIyBNb2RlbCBBbmFseXNpcyB7LX0KCkluIG9yZGVyIHRvIGZpbmQgdGhlIG51bWJlciBvZiB0cnVlIHBvc2l0aXZlIChUUCkgcHJlZGljdGVkIHN5bmVyZ2llcywgTUNDIHNjb3JlcyBhbmQgZml0bmVzcyBzY29yZXMgZm9yIGVhY2ggb2YgdGhlIG1vZGVscyBpbiBlYWNoIG9mIHRoZSA5IGRhdGFzZXRzLCB3ZSB1c2UgZnVuY3Rpb25zIGZyb20gdGhlIFtlbWJhXShodHRwczovL2dpdGh1Yi5jb20vYmJsb2Rmb24vZW1iYSkgUiBwYWNrYWdlLgoKIyMjIENlbGwtc3BlY2lmaWMgey19CgpXZSBmaW5kIHRoZSBNQ0MsIFRQIGFuZCBmaXRuZXNzIHZhbHVlcyBmb3IgZWFjaCBtb2RlbCBwZXIgY2VsbCBsaW5lIChub3RlCnRoYXQgZWFjaCBtb2RlbCdzIHN0YWJsZSBzdGF0ZSBpbiBhIHNwZWNpZmljIGNlbGwgbGluZSBpcyBtYXRjaGVkIGFnYWluc3QgdGhlIApzdGVhZHkgc3RhdGUgZnJvbSB0aGF0IGNlbGwgbGluZSk6CmBgYHtyIENlbGwtc3BlY2lmaWMgTW9kZWxzIFRQICsgTUNDICsgZml0bmVzcyBwZXIgY2VsbCBsaW5lfQptb2RlbHMudHAucGVyLmNlbGwubGluZSA9IGxpc3QoKQptb2RlbHMubWNjLnBlci5jZWxsLmxpbmUgPSBsaXN0KCkKbW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZSA9IGxpc3QoKQoKZm9yIChjZWxsLmxpbmUgaW4gY2VsbC5saW5lcykgewogIG1vZGVsLnByZWRpY3Rpb25zID0gbW9kZWwucHJlZGljdGlvbnMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBvYnNlcnZlZC5zeW5lcmdpZXMgPSBvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBudW1iZXIub2YuZHJ1Zy5jb21iLnRlc3RlZCA9IG5jb2wobW9kZWwucHJlZGljdGlvbnMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pCiAgCiAgIyBTcGxpdCBtb2RlbC5wcmVkaWN0aW9ucyB0byBwb3NpdGl2ZSAob2JzZXJ2ZWQpIGFuZCBuZWdhdGl2ZSAobm9uLW9ic2VydmVkKSByZXN1bHRzCiAgb2JzZXJ2ZWQubW9kZWwucHJlZGljdGlvbnMgPQogICAgZ2V0X29ic2VydmVkX21vZGVsX3ByZWRpY3Rpb25zKG1vZGVsLnByZWRpY3Rpb25zLCBvYnNlcnZlZC5zeW5lcmdpZXMpCiAgdW5vYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucyA9CiAgICBnZXRfdW5vYnNlcnZlZF9tb2RlbF9wcmVkaWN0aW9ucyhtb2RlbC5wcmVkaWN0aW9ucywgb2JzZXJ2ZWQuc3luZXJnaWVzKQogIAogICMgQ291bnQgdGhlIHByZWRpY3Rpb25zIG9mIHRoZSBvYnNlcnZlZCBzeW5lcmdpZXMgcGVyIG1vZGVsIChUUCkKICBtb2RlbHMudHAucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0gPSBjYWxjdWxhdGVfbW9kZWxzX3N5bmVyZ2llc190cChvYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucykKICAKICAjIENhbGN1bGF0ZSBNYXR0aGV3cyBDb3JyZWxhdGlvbiBDb2VmZmljaWVudCAoTUNDKSBmb3IgZXZlcnkgbW9kZWwKICBtb2RlbHMubWNjLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dID0gCiAgICBjYWxjdWxhdGVfbW9kZWxzX21jYyhvYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucywKICAgICAgICAgICAgICAgICAgICAgICAgIHVub2JzZXJ2ZWQubW9kZWwucHJlZGljdGlvbnMsCiAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIub2YuZHJ1Zy5jb21iLnRlc3RlZCkKICAKICAjIEZpdG5lc3MgcGVyIG1vZGVsIGNvbnRyYXN0ZWQgdG8gc3RlYWR5IHN0YXRlIGZyb20gY2VsbCBsaW5lCiAgbW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0gPSAKICAgIGFwcGx5KG1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0sIDEsIGdldF9wZXJjZW50YWdlX29mX21hdGNoZXMsIAogICAgICAgICAgc3RlYWR5LnN0YXRlLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dKQp9CmBgYAoKIyMjIFJhbmRvbSBNb2RlbHMgey19CgpOZXh0LCB3ZSBmaW5kIHRoZSBNQ0MsIFRQIGFuZCBmaXRuZXNzIHZhbHVlcyBmb3IgZWFjaCByYW5kb20gbW9kZWwgcGVyIGNlbGwgbGluZSAKKG5vdGUgdGhhdCBlYWNoIHJhbmRvbSBtb2RlbCdzIHN0YWJsZSBzdGF0ZSBpbiBhIHNwZWNpZmljIGNlbGwgbGluZSBpcyBtYXRjaGVkIGFnYWluc3QgCnRoZSBzdGVhZHkgc3RhdGUgZnJvbSB0aGF0IGNlbGwgbGluZSBhbmQgdGhhdCB0aGUgcmFuZG9tIG1vZGVscycgc3RhYmxlIHN0YXRlIGRhdGEKZG9lcyBub3QgY2hhbmdlIHBlciBjZWxsIGxpbmUsIGkuZS4gc2FtZSBgcmFuZG9tLm1vZGVscy5zdGFibGUuc3RhdGVgIG9iamVjdCk6CmBgYHtyIFJhbmRvbSBNb2RlbHMgVFAgKyBNQ0MgKyBmaXRuZXNzIHBlciBjZWxsIGxpbmV9CnJhbmRvbS5tb2RlbHMubWNjLnBlci5jZWxsLmxpbmUgPSBsaXN0KCkKcmFuZG9tLm1vZGVscy50cC5wZXIuY2VsbC5saW5lID0gbGlzdCgpCnJhbmRvbS5tb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lID0gbGlzdCgpCgpmb3IgKGNlbGwubGluZSBpbiBjZWxsLmxpbmVzKSB7CiAgb2JzZXJ2ZWQuc3luZXJnaWVzID0gb2JzZXJ2ZWQuc3luZXJnaWVzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgbnVtYmVyLm9mLmRydWcuY29tYi50ZXN0ZWQgPSBuY29sKHJhbmRvbS5tb2RlbC5wcmVkaWN0aW9ucykKICAKICAjIFNwbGl0IG1vZGVsLnByZWRpY3Rpb25zIHRvIHBvc2l0aXZlIChvYnNlcnZlZCkgYW5kIG5lZ2F0aXZlIChub24tb2JzZXJ2ZWQpIHJlc3VsdHMKICBvYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucyA9CiAgICBnZXRfb2JzZXJ2ZWRfbW9kZWxfcHJlZGljdGlvbnMocmFuZG9tLm1vZGVsLnByZWRpY3Rpb25zLCBvYnNlcnZlZC5zeW5lcmdpZXMpCiAgdW5vYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucyA9CiAgICBnZXRfdW5vYnNlcnZlZF9tb2RlbF9wcmVkaWN0aW9ucyhyYW5kb20ubW9kZWwucHJlZGljdGlvbnMsIG9ic2VydmVkLnN5bmVyZ2llcykKICAKICAgIyBDb3VudCB0aGUgcHJlZGljdGlvbnMgb2YgdGhlIG9ic2VydmVkIHN5bmVyZ2llcyBwZXIgbW9kZWwgKFRQKQogIHJhbmRvbS5tb2RlbHMudHAucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0gPSAKICAgIGNhbGN1bGF0ZV9tb2RlbHNfc3luZXJnaWVzX3RwKG9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zKQogIAogICMgQ2FsY3VsYXRlIE1hdHRoZXdzIENvcnJlbGF0aW9uIENvZWZmaWNpZW50IChNQ0MpIGZvciBldmVyeSBtb2RlbAogIHJhbmRvbS5tb2RlbHMubWNjLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dID0gCiAgICBjYWxjdWxhdGVfbW9kZWxzX21jYyhvYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucywKICAgICAgICAgICAgICAgICAgICAgICAgIHVub2JzZXJ2ZWQubW9kZWwucHJlZGljdGlvbnMsCiAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIub2YuZHJ1Zy5jb21iLnRlc3RlZCkKICAKICAjIEZpdG5lc3MgcGVyIG1vZGVsIGNvbnRyYXN0ZWQgdG8gc3RlYWR5IHN0YXRlIGZyb20gY2VsbCBsaW5lCiAgcmFuZG9tLm1vZGVscy5maXRuZXNzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dID0gCiAgICBhcHBseShyYW5kb20ubW9kZWxzLnN0YWJsZS5zdGF0ZSwgMSwgZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcywgCiAgICAgICAgICBzdGVhZHkuc3RhdGUucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pCn0KYGBgCgojIyBDaG9vc2UgYmVzdCBkYXRhc2V0IHstfQoKV2Ugbm93IHdhbnQgdG8gKipmaW5kIHRoZSBiZXN0IGRhdGFzZXQgJiBjZWxsIGxpbmUqKiBmb3Igb3VyIHN1YnNlcXVlbnQgYW5hbHlzaXMgLSB0aGF0IGlzIHRvIHNob3cgdGhlIHBlcmZvcm1hbmNlIHZzIGZpdG5lc3MgY29ycmVsYXRpb24uIApUaGUgYXJndW1lbnQgaGVyZSBpcyB0aGF0IHdlIHdhbnQgdG8gY2hvb3NlIGEgYm9vbGVhbiBtb2RlbCBkYXRhc2V0IHRoYXQgaGFzIGEgKipsYXJnZSBlbm91Z2ggZml0bmVzcyB2YWx1ZSByYW5nZSoqIGNvbWJpbmVkIHdpdGggYSBsYXJnZSAqKlRQIGFuZC9vciBNQ0MgdmFsdWUgcmFuZ2UqKiwgc2luY2Ugd2l0aCBzbWFsbGVyIHZhbHVlIHJhbmdlcyBpdCB3b3VsZCBiZSBoYXJkZXIgdG8gZGlzdGluZ3Vpc2ggdGhlIGRpZmZlcmVuY2Ugb2YgdGhlIGVzdGltYXRlZCBkaXN0cmlidXRpb25zIG9mIHRoZSBmaXRuZXNzIHNjb3JlcyBiZWxvbmdpbmcgdG8gZGlmZmVyZW50IHBlcmZvcm1hbmNlIGNsYXNzZXMgKGkuZS4gbW9kZWxzJyBmaXRuZXNzZXMgdGhhdCBiZWxvbmcgdG8gZGlmZmVyZW50IGNsYXNzaWZpY2F0aW9uIGdyb3VwcyB3aXRoIHRoZSBtZXRyaWMgYmVpbmcgZWl0aGVyIHRoZSBudW1iZXIgb2YgVFBzIG9yIHRoZSBNQ0Mgc2NvcmUpLgoKVGhlIG5leHQgc3VtbWFyeSBzdGF0aXN0aWNzIHRhYmxlcyBhbmQgYm94LXBsb3RzIHdpbGwgaGVscCB1cyBkZXRlcm1pbmUgZXhhY3RseSB3aGljaCBjZWxsIGxpbmUgYW5kIGRhdGFzZXQKdG8gdXNlOgpgYGB7ciBDZWxsLXNwZWNpZmljIE1vZGVscyBUUCArIE1DQyArIGZpdG5lc3MgcGVyIGNlbGwgbGluZSBzdGF0c30KY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhID0gbWF0cml4KGRhdGEgPSBOQSwgbnJvdyA9IGxlbmd0aChjZWxsLmxpbmVzKSwgbmNvbCA9IDExKQpyb3duYW1lcyhjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGEpID0gY2VsbC5saW5lcwpjb2xuYW1lcyhjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGEpID0gYygiZml0bmVzcyByYW5nZSIsICJNaW4gZml0bmVzcyIsIAogICJNYXggZml0bmVzcyIsICJNZWFuIGZpdG5lc3MiLCAiTWVkaWFuIGZpdG5lc3MiLCAiTUNDIHJhbmdlIiwgIk1pbiBNQ0MiLCAKICAiTWF4IE1DQyIsICJNZWFuIE1DQyIsICJNZWRpYW4gTUNDIiwgIk1heCBUUFIiKQoKZm9yIChjZWxsLmxpbmUgaW4gY2VsbC5saW5lcykgewogIG1vZGVscy5maXRuZXNzID0gbW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBtb2RlbHMubWNjID0gbW9kZWxzLm1jYy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIG1vZGVscy50cCA9IG1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIAogIGZpdC5zdW1tYXJ5ID0gdW5jbGFzcyhzdW1tYXJ5KG1vZGVscy5maXRuZXNzKSkKICBtY2Muc3VtbWFyeSA9IHVuY2xhc3Moc3VtbWFyeShtb2RlbHMubWNjKSkKICBtYXgudHByID0gbWF4KG1vZGVscy50cCkgLyBsZW5ndGgob2JzZXJ2ZWQuc3luZXJnaWVzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dKQogIAogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJmaXRuZXNzIHJhbmdlIl0gPSBmaXQuc3VtbWFyeVsiTWF4LiJdIC0gZml0LnN1bW1hcnlbIk1pbi4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNaW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1pbi4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1heC4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWFuIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNZWFuIl0KICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWVkaWFuIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNZWRpYW4iXQogIAogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNQ0MgcmFuZ2UiXSA9IG1jYy5zdW1tYXJ5WyJNYXguIl0gLSBtY2Muc3VtbWFyeVsiTWluLiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1pbiBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNaW4uIl0KICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IE1DQyJdID0gbWNjLnN1bW1hcnlbIk1heC4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWFuIE1DQyJdID0gbWNjLnN1bW1hcnlbIk1lYW4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWRpYW4gTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWVkaWFuIl0KICAKICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IFRQUiJdID0gbWF4LnRwcgp9CgojIGNvbG9yIGNvbHVtbnMKZml0LmJyZWFrcyA9IHF1YW50aWxlKGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVssImZpdG5lc3MgcmFuZ2UiXSwgcHJvYnMgPSBzZXEoLjA1LCAuOTUsIC4wNSksIG5hLnJtID0gVFJVRSkKZml0LmNvbG9ycyA9IHJvdW5kKHNlcSgyNTUsIDQwLCBsZW5ndGgub3V0ID0gbGVuZ3RoKGZpdC5icmVha3MpICsgMSksIDApICU+JQogIHtwYXN0ZTAoInJnYigyNTUsIiwgLiwgIiwiLCAuLCAiKSIpfSAjIHJlZAptY2MuYnJlYWtzID0gcXVhbnRpbGUoY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhWywiTUNDIHJhbmdlIl0sIHByb2JzID0gc2VxKC4wNSwgLjk1LCAuMDUpLCBuYS5ybSA9IFRSVUUpCm1jYy5jb2xvcnMgPSByb3VuZChzZXEoMjU1LCA0MCwgbGVuZ3RoLm91dCA9IGxlbmd0aChtY2MuYnJlYWtzKSArIDEpLCAwKSAlPiUKICB7cGFzdGUwKCJyZ2IoIiwgLiwgIiwyNTUsIiwgLiwgIikiKX0gIyBncmVlbgoKY2FwdGlvbi50aXRsZSA9ICJUYWJsZSAxOiBGaXRuZXNzLCBNQ0Mgc2NvcmVzIGFuZCBUUCAoVHJ1ZSBwb3NpdGl2ZXMpIGZvciB0aGUgQ2VsbC1zcGVjaWZpYyBtb2RlbCBwcmVkaWN0aW9ucyBhY3Jvc3MgOCBDZWxsIExpbmVzIgpkYXRhdGFibGUoZGF0YSA9IGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSwgCiAgICAgICAgICBvcHRpb25zID0gbGlzdChkb20gPSAidCIpLCAjIGp1c3Qgc2hvdyB0aGUgdGFibGUKICAgICAgICAgIGNhcHRpb24gPSBodG1sdG9vbHM6OnRhZ3MkY2FwdGlvbihjYXB0aW9uLnRpdGxlLCBzdHlsZT0iY29sb3I6I2RkNDgxNDsgZm9udC1zaXplOiAxOHB4IikpICU+JSAKICBmb3JtYXRSb3VuZCgxOjExLCBkaWdpdHMgPSAzKSAlPiUKICBmb3JtYXRTdHlsZShjb2x1bW5zID0gYygiZml0bmVzcyByYW5nZSIpLCBiYWNrZ3JvdW5kQ29sb3IgPSBzdHlsZUludGVydmFsKGZpdC5icmVha3MsIGZpdC5jb2xvcnMpKSAlPiUKICBmb3JtYXRTdHlsZShjb2x1bW5zID0gYygiTUNDIHJhbmdlIiksIGJhY2tncm91bmRDb2xvciA9IHN0eWxlSW50ZXJ2YWwobWNjLmJyZWFrcywgbWNjLmNvbG9ycykpCmBgYAoKYGBge3IgUmFuZG9tIE1vZGVscyBUUCArIE1DQyArIGZpdG5lc3MgcGVyIGNlbGwgbGluZSBzdGF0c30KcmFuZG9tLm1vZGVsLmRhdGEgPSBtYXRyaXgoZGF0YSA9IE5BLCBucm93ID0gbGVuZ3RoKGNlbGwubGluZXMpLCBuY29sID0gMTEpCnJvd25hbWVzKHJhbmRvbS5tb2RlbC5kYXRhKSA9IGNlbGwubGluZXMKY29sbmFtZXMocmFuZG9tLm1vZGVsLmRhdGEpID0gYygiZml0bmVzcyByYW5nZSIsICJNaW4gZml0bmVzcyIsIAogICJNYXggZml0bmVzcyIsICJNZWFuIGZpdG5lc3MiLCAiTWVkaWFuIGZpdG5lc3MiLCAiTUNDIHJhbmdlIiwgIk1pbiBNQ0MiLCAKICAiTWF4IE1DQyIsICJNZWFuIE1DQyIsICJNZWRpYW4gTUNDIiwgIk1heCBUUFIiKQoKZm9yIChjZWxsLmxpbmUgaW4gY2VsbC5saW5lcykgewogIG1vZGVscy5maXRuZXNzID0gcmFuZG9tLm1vZGVscy5maXRuZXNzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgbW9kZWxzLm1jYyA9IHJhbmRvbS5tb2RlbHMubWNjLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgbW9kZWxzLnRwID0gcmFuZG9tLm1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIAogIGZpdC5zdW1tYXJ5ID0gdW5jbGFzcyhzdW1tYXJ5KG1vZGVscy5maXRuZXNzKSkKICBtY2Muc3VtbWFyeSA9IHVuY2xhc3Moc3VtbWFyeShtb2RlbHMubWNjKSkKICBtYXgudHByID0gbWF4KG1vZGVscy50cCkgLyBsZW5ndGgob2JzZXJ2ZWQuc3luZXJnaWVzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dKQogIAogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgImZpdG5lc3MgcmFuZ2UiXSA9IGZpdC5zdW1tYXJ5WyJNYXguIl0gLSBmaXQuc3VtbWFyeVsiTWluLiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWluIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNaW4uIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1heC4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lYW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1lYW4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lZGlhbiBmaXRuZXNzIl0gPSBmaXQuc3VtbWFyeVsiTWVkaWFuIl0KICAKICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNQ0MgcmFuZ2UiXSA9IG1jYy5zdW1tYXJ5WyJNYXguIl0gLSBtY2Muc3VtbWFyeVsiTWluLiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWluIE1DQyJdID0gbWNjLnN1bW1hcnlbIk1pbi4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1heCBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNYXguIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWFuIE1DQyJdID0gbWNjLnN1bW1hcnlbIk1lYW4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lZGlhbiBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNZWRpYW4iXQogIAogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1heCBUUFIiXSA9IG1heC50cHIKfQoKIyBjb2xvciBjb2x1bW5zCmZpdC5icmVha3MgPSBxdWFudGlsZShyYW5kb20ubW9kZWwuZGF0YVssImZpdG5lc3MgcmFuZ2UiXSwgcHJvYnMgPSBzZXEoLjA1LCAuOTUsIC4wNSksIG5hLnJtID0gVFJVRSkKZml0LmNvbG9ycyA9IHJvdW5kKHNlcSgyNTUsIDQwLCBsZW5ndGgub3V0ID0gbGVuZ3RoKGZpdC5icmVha3MpICsgMSksIDApICU+JQogIHtwYXN0ZTAoInJnYigyNTUsIiwgLiwgIiwiLCAuLCAiKSIpfSAjIHJlZAptY2MuYnJlYWtzID0gcXVhbnRpbGUocmFuZG9tLm1vZGVsLmRhdGFbLCJNQ0MgcmFuZ2UiXSwgcHJvYnMgPSBzZXEoLjA1LCAuOTUsIC4wNSksIG5hLnJtID0gVFJVRSkKbWNjLmNvbG9ycyA9IHJvdW5kKHNlcSgyNTUsIDQwLCBsZW5ndGgub3V0ID0gbGVuZ3RoKG1jYy5icmVha3MpICsgMSksIDApICU+JQogIHtwYXN0ZTAoInJnYigiLCAuLCAiLDI1NSwiLCAuLCAiKSIpfSAjIGdyZWVuCgpjYXB0aW9uLnRpdGxlID0gIlRhYmxlIDI6IEZpdG5lc3MsIE1DQyBzY29yZXMgYW5kIFRQIChUcnVlIHBvc2l0aXZlcykgZm9yIHRoZSByYW5kb20gbW9kZWwgcHJlZGljdGlvbnMgYWNyb3NzIDggQ2VsbCBMaW5lcyIKZGF0YXRhYmxlKGRhdGEgPSByYW5kb20ubW9kZWwuZGF0YSwgCiAgICAgICAgICBvcHRpb25zID0gbGlzdChkb20gPSAidCIpLCAjIGp1c3Qgc2hvdyB0aGUgdGFibGUKICAgICAgICAgIGNhcHRpb24gPSBodG1sdG9vbHM6OnRhZ3MkY2FwdGlvbihjYXB0aW9uLnRpdGxlLCBzdHlsZT0iY29sb3I6I2RkNDgxNDsgZm9udC1zaXplOiAxOHB4IikpICU+JSAKICBmb3JtYXRSb3VuZCgxOjExLCBkaWdpdHMgPSAzKSAlPiUKICBmb3JtYXRTdHlsZShjb2x1bW5zID0gYygiZml0bmVzcyByYW5nZSIpLCBiYWNrZ3JvdW5kQ29sb3IgPSBzdHlsZUludGVydmFsKGZpdC5icmVha3MsIGZpdC5jb2xvcnMpKSAlPiUKICBmb3JtYXRTdHlsZShjb2x1bW5zID0gYygiTUNDIHJhbmdlIiksIGJhY2tncm91bmRDb2xvciA9IHN0eWxlSW50ZXJ2YWwobWNjLmJyZWFrcywgbWNjLmNvbG9ycykpCmBgYAoKVGhlIGJlbG93IGJveCBwbG90cyBjb21wYXJlIHRoZSBNQ0MgdmFsdWVzIGFuZCBmaXRuZXNzIHNjb3JlcyBhY3Jvc3MgYWxsIGNlbGwgbGluZXMgYmV0d2Vlbgp0aGUgY2VsbC1zcGVjaWZpYyBtb2RlbHMgKHRyYWluZWQgdG8gc3RlYWR5IHN0YXRlKSBhbmQgdGhlIHJhbmRvbSBvbmVzICh0cmFpbmVkIHRvIHByb2xpZmVyYXRpb24pOgoKYGBge3IgQ29tYmluZSBkYXRhIGludG8gb25lIGRhdGEgZnJhbWV9Cm51bS5vZi5tb2RlbHMgPSBucm93KHJhbmRvbS5tb2RlbHMuc3RhYmxlLnN0YXRlKQpkYXRhLmxpc3QgPSBsaXN0KCkKCmZvciAoY2VsbC5saW5lIGluIGNlbGwubGluZXMpIHsKICBjZWxsLmxpbmUudmVjID0gYXMuZGF0YS5mcmFtZShyZXAoY2VsbC5saW5lLCBudW0ub2YubW9kZWxzKSwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKQogIG1vZGVscy5tY2MuY2VsbC5zcGVjaWZpYyA9IHJlbW92ZV9yb3duYW1lcyhhcy5kYXRhLmZyYW1lKG1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pKQogIG1vZGVscy5tY2MucmFuZG9tICAgICAgICA9IHJlbW92ZV9yb3duYW1lcyhhcy5kYXRhLmZyYW1lKHJhbmRvbS5tb2RlbHMubWNjLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dKSkKICAKICBtb2RlbHMuZml0bmVzcy5jZWxsLnNwZWNpZmljID0gcmVtb3ZlX3Jvd25hbWVzKGFzLmRhdGEuZnJhbWUobW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pKQogIG1vZGVscy5maXRuZXNzLnJhbmRvbSAgICAgICAgPSByZW1vdmVfcm93bmFtZXMoYXMuZGF0YS5mcmFtZShyYW5kb20ubW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pKQogIAogIGRhdGEubGlzdFtbY2VsbC5saW5lXV0gPSBiaW5kX2NvbHMoY2VsbC5saW5lLnZlYywgbW9kZWxzLm1jYy5jZWxsLnNwZWNpZmljLCAKICAgIG1vZGVscy5tY2MucmFuZG9tLCBtb2RlbHMuZml0bmVzcy5jZWxsLnNwZWNpZmljLCBtb2RlbHMuZml0bmVzcy5yYW5kb20pCn0KCmRhdGEgPSBiaW5kX3Jvd3MoZGF0YS5saXN0KQpjb2xuYW1lcyhkYXRhKSA9IGMoImNlbGwubGluZSIsICJNQ0MgY2VsbC1zcGVjaWZpYyIsICJNQ0MgcmFuZG9tIiwgCiAgICAgICAgICAgICAgICAgICAiZml0bmVzcyBjZWxsLXNwZWNpZmljIiwgImZpdG5lc3MgcmFuZG9tIikKYGBgCgpgYGB7ciBCb3hwbG90czogTUNDIGFuZCBmaXRuZXNzIHZhbHVlcyBiZXR3ZWVuIGNlbGwtc3BlY2lmaWMgYW5kIHJhbmRvbSBtb2RlbHMsIGZpZy53aWR0aD05LCB3YXJuaW5nPUZBTFNFLCBjYWNoZT1UUlVFfQojIE5vdGUgdGhlIGNlbGwtc3BlY2lmaWMgbW9kZWxzIGhhdmUgTmFOIE1DQyB2YWx1ZXMKZ2dib3hwbG90KGRhdGEsIHggPSAiY2VsbC5saW5lIiwgeSA9IGMoIk1DQyBjZWxsLXNwZWNpZmljIiwgIk1DQyByYW5kb20iKSwKICAgICAgICAgIHBhbGV0dGUgPSBicmV3ZXIucGFsKDMsICJTZXQxIiksIG1lcmdlID0gImFzaXMiLAogICAgICAgICAgeGxhYiA9ICJDZWxsIExpbmVzIiwgeWxhYiA9ICJNQ0MgdmFsdWVzIiwgYWRkID0gInBvaW50IiwgYWRkLnBhcmFtcyA9IGxpc3Qoc2l6ZSA9IDAuNSkpCmdnYm94cGxvdChkYXRhLCB4ID0gImNlbGwubGluZSIsIHkgPSBjKCJmaXRuZXNzIGNlbGwtc3BlY2lmaWMiLCAiZml0bmVzcyByYW5kb20iKSwKICAgICAgICAgIHBhbGV0dGUgPSBicmV3ZXIucGFsKDMsICJTZXQxIiksIG1lcmdlID0gImFzaXMiLAogICAgICAgICAgeGxhYiA9ICJDZWxsIExpbmVzIiwgeWxhYiA9ICJGaXRuZXNzIHZhbHVlcyIsIGFkZCA9ICJwb2ludCIsIGFkZC5wYXJhbXMgPSBsaXN0KHNpemUgPSAwLjUpKQpgYGAKClNvLCBmcm9tIHRoZSB0d28gdGFibGVzIGFuZCB0aGUgdHdvIGJveCBwbG90cyBhYm92ZSB3ZSBjb25jbHVkZSB0aGF0OgoKPGRpdiBjbGFzcz0iYmx1ZS1ib3giPgotIEluIGdlbmVyYWwsIHRoZSAqKnJhbmRvbSBtb2RlbHMgb2ZmZXIgYSBsYXJnZXIgcmFuZ2Ugb2YgCmZpdG5lc3MgdmFsdWVzKiogd2hlbiB0aGVpciBzdGFibGUgc3RhdGVzIGFyZSBtYXRjaGVkIGFnYWluc3QgdGhlIHN0ZWFkeSBzdGF0ZSAKb2YgZWFjaCBwYXJ0aWN1bGFyIGNlbGwgbGluZS4gClRoaXMgaXMgc29tZXRoaW5nIHdlIGV4cGVjdGVkIHNpbmNlIHRoZXNlIG1vZGVscyB3ZXJlbid0IGZpdHRlZCB0byBhIApzcGVjaWZpYyBjZWxsLWxpbmUgc3RlYWR5IHN0YXRlIChtZWFuaW5nIHRoYXQgdGhleSB3ZXJlbid0IGNob3NlbiBmcm9tIHRoZSAKYEdpdHNiZWAgbW9kdWxlIGFzIHRoZSAzIGJlc3QgZnJvbSBlYWNoIHNpbXVsYXRpb24gdGhhdCBtYXRjaCB0aGF0IHN0ZWFkeSBzdGF0ZSAKYXMgYmVzdCBhcyBwb3NzaWJsZSkgYnV0IHJhdGhlciB0byBhIG1vcmUgZ2VuZXJpYyBzdGF0ZSBvZiBwcm9saWZlcmF0aW9uLiAKVGh1cywgdGhleSByZXByZXNlbnQgYSBzZXQgb2YgbW9kZWxzIHdpdGggbGFyZ2VyIHZhcmlhdGlvbiBpbiB0ZXJtcyBvZiAKc3RydWN0dXJlIChib29sZWFuIG1vZGVsIGVxdWF0aW9ucykgY29tcGFyZWQgdG8gdGhlIGNlbGwtc3BlY2lmaWMgZ2VuZXJhdGVkIG9uZXMuCi0gKipUaGUgY2VsbC1zcGVjaWZpYyBtb2RlbHMgaGF2ZSBhbHdheXMgYSBsYXJnZXIgbWF4aW11bSBmaXRuZXNzIGFuZCBtZWRpYW4gdmFsdWUqKiAKY29tcGFyZWQgdG8gdGhlIHJhbmRvbSBvbmVzIGZvciBlYWNoIHJlc3BlY3RpdmUgY2VsbCBsaW5lLgotIEluIGVhY2ggY2VsbCBsaW5lICh3aXRoIHRoZSBleHBlY3Rpb24gb2YgU1c2MjApICoqdGhlcmUgYXJlIGFsd2F5cyBjZWxsLXNwZWNpZmljIAptb2RlbHMgdGhhdCBzaG93IGJldHRlciBwZXJmb3JtYW5jZSB0aGFuIHRoZSByYW5kb20gb25lcyAoaGF2ZSBhIGhpZ2hlciBNQ0MgdmFsdWUpKioKPC9kaXY+CjwvYnI+Cgo8ZGl2IGNsYXNzPSJvcmFuZ2UtYm94Ij4KQWxsIGluIGFsbCwgd2Ugd2lsbCB1c2UgdGhlICoqcmFuZG9tIG1vZGVscyBkYXRhKiosIGNvbnRyYXN0ZWQgdG8gdGhlIHN0ZWFkeSAKc3RhdGUgb2YgdGhlIGBBNDk4YCBjZWxsIGxpbmUgKHNlZSBUYWJsZSAyKS4gVGhpcyBkYXRhc2V0IGhhcyB0aGUgbGFyZ2VzdCAKZml0bmVzcyBhbmQgTUNDIHZhbHVlIHJhbmdlIChzZWNvbmQgbGFyZ2VzdCBUUFIgdmFsdWUgYXMgd2VsbCkgY29tYmluZWQgaW4gYm90aCAKdGFibGVzIGFib3ZlLgo8L2Rpdj4KPC9icj4KCmBgYHtyIFNhdmUgYmVzdCBkYXRhc2V0fQpiZXN0LmNlbGwubGluZSA9ICJBNDk4IgoKZml0ID0gcmFuZG9tLm1vZGVscy5maXRuZXNzLnBlci5jZWxsLmxpbmVbW2Jlc3QuY2VsbC5saW5lXV0KdHAgID0gcmFuZG9tLm1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tiZXN0LmNlbGwubGluZV1dCm1jYyA9IHJhbmRvbS5tb2RlbHMubWNjLnBlci5jZWxsLmxpbmVbW2Jlc3QuY2VsbC5saW5lXV0KYGBgCgojIyBTdGF0aXN0aWNhbCBBbmFseXNpcyB7LX0KCiMjIyBEYXRhIHByZXByb2Nlc3Npbmcgey19CgpGaXJzdGx5LCB3ZSBmaWx0ZXIgdGhlIGRhdGEgYnkgZmluZGluZyB0aGUgKip1bmlxdWUgbW9kZWxzKiogLSB0aG9zZSB0aGF0IGhhdmUgCnN0cmljdGx5IGRpZmZlcmVudCBib29sZWFuIGVxdWF0aW9ucy4gVGhlbiwgd2UgdGFrZSBhIHJhbmRvbSBzYW1wbGUgb3V0IG9mIHRoZXNlIAood2hpbGUgc3RhYmlsaXppbmcgdGhlIHNlZWQgbnVtYmVyIGZvciByZXByb2R1Y2liaWxpdHkgcHVycG9zZXMpOgpgYGB7ciBTYW1wbGUgYmVzdCBkYXRhc2V0fQojIEZvciByZXByb2R1Y2liaWxpdHkKc2V0LnNlZWQoMCkKc2FtcGxlLnNpemUgPSAxMDAwCgp1bmlxdWUubW9kZWxzID0gcm93bmFtZXModW5pcXVlKHJhbmRvbS5tb2RlbHMubGluay5vcGVyYXRvcikpCnVuaXF1ZS5tb2RlbHMuc2FtcGxlID0gc2FtcGxlKHVuaXF1ZS5tb2RlbHMsIHNpemUgPSBzYW1wbGUuc2l6ZSkKCmZpdC51bmlxdWUgPSBmaXRbbmFtZXMoZml0KSAlaW4lIHVuaXF1ZS5tb2RlbHMuc2FtcGxlXQp0cC51bmlxdWUgID0gdHBbbmFtZXModHApICVpbiUgdW5pcXVlLm1vZGVscy5zYW1wbGVdCm1jYy51bmlxdWUgPSBtY2NbbmFtZXMobWNjKSAlaW4lIHVuaXF1ZS5tb2RlbHMuc2FtcGxlXQoKZGYgPSBhcy5kYXRhLmZyYW1lKGNiaW5kKGZpdC51bmlxdWUsIG1jYy51bmlxdWUsIHRwLnVuaXF1ZSkpCmBgYAoKTm90ZSB0aGF0IHRoZSAqKmZpdG5lc3MgYW5kIE1DQyBzY29yZSBhcmUgY29udGludW91cyB2YXJpYWJsZXMgd2hpbGUgdGhlIG51bWJlciAKb2YgdHJ1ZSBwb3NpdGl2ZXMgKFRQKSBpcyBkaXNjcmV0ZSoqLgoKIyMjIENvcnJlbGF0aW9uIFBsb3RzIHstfQoKVGhlbiwgd2UgY2hlY2sgaWYgdGhlIG91ciBkYXRhIGlzIG5vcm1hbGx5IGRpc3RyaWJ1dGVkICh1c2luZyB0aGUgU2hhcGlyby1XaWxrCnRlc3QgZm9yIG5vcm1hbGl0eSBhbmQgdGhlIFEtUSBwbG90cyk6CmBgYHtyIE5vcm1hbGl0eSB0ZXN0aW5nLCBjb21tZW50PSIifQpnZ3FxcGxvdChkYXRhID0gZGYsIHggPSAiZml0LnVuaXF1ZSIsIHlsYWIgPSAiRml0bmVzcyIpCmdncXFwbG90KGRhdGEgPSBkZiwgeCA9ICJtY2MudW5pcXVlIiwgeWxhYiA9ICJNTUMiKQoKc2hhcGlyby50ZXN0KHggPSBzYW1wbGUoZml0LnVuaXF1ZSkpCnNoYXBpcm8udGVzdCh4ID0gc2FtcGxlKG1jYy51bmlxdWUpKQpgYGAKCkZyb20gdGhlIGFib3ZlIHJlc3VsdHMgd2Ugb2JzZXJ2ZSB0aGF0IGJvdGggdGhlIGZpdG5lc3MgYW5kIE1DQyBzY29yZXMgYXJlIHN1cmVseQpub3Qgbm9ybWFsbHkgZGlzdHJpYnV0ZWQgKHdpdGggc3RhdGlzdGljYWwgc2lnbmlmaWNhbmNlKS4KVGh1cywgd2Ugd2lsbCB1c2UgKipub24tcGFyYW1ldHJpYyBjb3JyZWxhdGlvbiBzY29yZXMsIG5hbWVseSB0aGUgU3BlYXJtYW4gYW5kIEtlbmRhbGwgcmFuay1iYXNlZCBjb3JyZWxhdGlvbiB0ZXN0cyoqLAp0byBjaGVjayB0aGUgY29ycmVsYXRpb24gYmV0d2VlbiB0aGUgdHdvIGNvbnRpbnVvdXMgdmFyaWFibGVzIChtb2RlbHMgZml0bmVzcyAKdmFsdWVzIGFuZCB0aGVpciBjb3JyZXNwb25kaW5nIE1DQyBzY29yZSk6CmBgYHtyIFJhbmsgY29ycmVsYXRpb24gcGxvdHM6IEZpdG5lc3MgdnMgTUNDfQpnZ3NjYXR0ZXIoZGYsIHggPSAibWNjLnVuaXF1ZSIsIHkgPSAiZml0LnVuaXF1ZSIsIAogICAgICAgICAgdGl0bGUgPSAiRml0bmVzcyB2cyBQZXJmb3JtYW5jZSAoTUNDKSAtIFNwZWFybWFuIENvcnJlbGF0aW9uIiwKICAgICAgICAgIGFkZCA9ICJyZWcubGluZSIsIGNvbmYuaW50ID0gVFJVRSwgCiAgICAgICAgICBjb3IuY29lZiA9IFRSVUUsIGNvci5jb2VmZi5hcmdzID0gbGlzdChtZXRob2QgPSAic3BlYXJtYW4iLCBsYWJlbC54Lm5wYyA9IDAuNywgbGFiZWwueS5ucGMgPSAxKSwKICAgICAgICAgIHhsYWIgPSAiTUNDIHNjb3JlcyIsIHlsYWIgPSAiRml0bmVzcyBWYWx1ZXMiKQpnZ3NjYXR0ZXIoZGYsIHggPSAibWNjLnVuaXF1ZSIsIHkgPSAiZml0LnVuaXF1ZSIsIAogICAgICAgICAgdGl0bGUgPSAiRml0bmVzcyB2cyBQZXJmb3JtYW5jZSAoTUNDKSAtIEtlbmRhbGwgQ29ycmVsYXRpb24iLAogICAgICAgICAgYWRkID0gInJlZy5saW5lIiwgY29uZi5pbnQgPSBUUlVFLCAKICAgICAgICAgIGNvci5jb2VmID0gVFJVRSwgY29yLmNvZWZmLmFyZ3MgPSBsaXN0KG1ldGhvZCA9ICJrZW5kYWxsIiwgbGFiZWwueC5ucGMgPSAwLjcsIGxhYmVsLnkubnBjID0gMSksCiAgICAgICAgICB4bGFiID0gIk1DQyBzY29yZXMiLCB5bGFiID0gIkZpdG5lc3MgVmFsdWVzIikKYGBgCgpGcm9tIHRoZSBjb3JyZWxhdGlvbiBwbG90cyBhYm92ZSwgd2Ugb2JzZXJ2ZSBhICoqd2Vhay9zbWFsbCBwb3NpdGl2ZSBjb3JyZWxhdGlvbiBiZXR3ZWVuCnBlcmZvcm1hbmNlIGFuZCBmaXRuZXNzIHRvIHRoZSBzdGVhZHkgc3RhdGUqKi4KClRvIGFzc2VzcyB0aGUgKipjb3JyZWxhdGlvbiBiZXR3ZWVuIHRoZSBudW1iZXIgb2YgVFAgcHJlZGljdGlvbnMgb2YgdGhlIG1vZGVscyAKKGRpc2NyZXRlIHZhcmlhYmxlKSBhbmQgdGhlIG1vZGVscyBmaXRuZXNzIChjb250aW51b3VzIHZhcmlhYmxlKSoqLCB3ZSBjb25zdHJ1Y3QgYSAKcHJlZGljdG9yIG9mIHRoZSBjYXRlZ29yaWNhbCB2YXJpYWJsZSBmcm9tIHRoZSBjb250aW51b3VzIHZhcmlhYmxlOiBpZiB0aGUgCnJlc3VsdGluZyBjbGFzc2lmaWVyIGhhcyBhICoqaGlnaCBkZWdyZWUgb2YgZml0Kiogd2UgY2FuIGNvbmNsdWRlIHRoZSB0d28gdmFyaWFibGVzIApzaGFyZSBhIHJlbGF0aW9uc2hpcCBhbmQgYXJlIGluZGVlZCBjb3JyZWxhdGVkLiBTaW5jZSB0aGVyZSBhcmUgbW9yZSB0aGFuIDIgVFAKY2xhc3NlcyAodmFsdWVzKSBpbiB0aGUgZGF0YXNldCwgd2Ugd2lsbCB1c2UgKipNdWx0aW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uKioKYW5kIGZpdCBsb2ctbGluZWFyIG1vZGVscyB2aWEgbmV1cmFsIG5ldHdvcmtzOgoKYGBge3IgTXVsdGlub21pYWwgTG9naXN0aWMgUmVncmVzc2lvbjogVFAgfiBGaXRuZXNzLCBjb21tZW50PSIifQptb2RlbC5jbGFzc2lmaWVyID0gbXVsdGlub20oZGF0YSA9IGRmLCBmb3JtdWxhID0gdHAudW5pcXVlIH4gZml0LnVuaXF1ZSkKcHNldWRvLnIyLm1lYXN1cmVzID0gcFIyKG1vZGVsLmNsYXNzaWZpZXIpCnBzZXVkby5yMi5tZWFzdXJlc1siTWNGYWRkZW4iXQpgYGAKCmBgYHtyIFRlc3QgcHJlZGljdGlvbiBhY2N1cmFjeSBvbiBzYW1lIGRhdGFzZXQsIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmxpYnJhcnkoY2FyZXQpCgphcHJlZGljdGlvbnMgPSBwcmVkaWN0KG9iamVjdCA9IG1vZGVsLmNsYXNzaWZpZXIsIG5ld2RhdGEgPSBkZiR0cC51bmlxdWUsIHR5cGUgPSAiY2xhc3MiKQpwb3N0UmVzYW1wbGUocHJlZCA9IHByZWRpY3Rpb25zLCBvYnMgPSBmYWN0b3IoZGYkdHAudW5pcXVlKSkKYGBgCgpUbyBtZWFzdXJlIHRoZSBnb29kbmVzcy1vZi1maXQgZm9yIG91ciBtb2RlbCwgdGhlcmUgYXJlIHNldmVyYWwgbWVhc3VyZXMgcHJvcG9zZWQgCmZvciBsb2dpc3RpYyByZWdyZXNzaW9uLiBXZSBlbXBoYXNpemUgb24gdGhlIE1jRmFkZGVucydzIHBzZXVkby0kUl4yJCBtZWFzdXJlIApmb3Igb3VyIGNsYXNzaWZpZXIsIGZvciB3aGljaCBhIHZhbHVlIG9mICQwLjItMC40JCB3b3VsZCBpbmRpY2F0ZSBhbiBleGNlbGxlbnQgZml0IFtbc291cmNlXShodHRwczovL3N0YXRzLnN0YWNrZXhjaGFuZ2UuY29tL3F1ZXN0aW9ucy84MjEwNS9tY2ZhZGRlbnMtcHNldWRvLXIyLWludGVycHJldGF0aW9uKV0uClNpbmNlIHdlIGZvdW5kIGxlc3MsIHdlIGNhbiBhbHNvIGFzc3VtZSB0aGF0IHRoZXJlIGlzIG9ubHkgYSAqKndlYWsvc21hbGwgcG9zaXRpdmUgCmNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIG51bWJlciBvZiBUUHMgYW5kIHRoZSBmaXRuZXNzIHNjb3JlIG9mIHRoZSBtb2RlbHMqKi4KCk5leHQsIHdlIHdpbGwgbmV4dCBwcm9jZWVkIHdpdGggYSBtb3JlIGVsYWJvcmF0ZSBhbmFseXNpcywgd2hlcmUgdGhlIG1vZGVscyB3aWxsIGJlIHNwbGl0IHRvIGRpZmZlcmVudCAKcGVyZm9ybWFuY2UgY2xhc3NlcyAoVFAgb3IgTUNDIHNjb3JlLWRlcml2ZWQpIGFuZCB0aGUgc3RhdGlzdGljYWwgY29ycmVsYXRpb24gYmV0d2VlbiB0aGUgCmluZGl2aWR1YWwgZ3JvdXBzIHdpbGwgYmUgdGVzdGVkICh3aXRoIHJlZ2FyZHMgdG8gdGhlaXIgZml0bmVzcyBzY29yZXMpLgoKIyMjIFRQLWNsYXNzIHZzIGZpdG5lc3Mgey19CgpGaXJzdCwgc29tZSBib3ggYW5kIGRlbnNpdHkgcGxvdHMgdG8gc2VlIHByYWN0aWNhbGx5ICoqd2hhdCBhbmQgd2hlcmUgaXMgdGhlIGRpZmZlcmVuY2UgCmJldHdlZW4gdGhlIG1vZGVscyBmaXRuZXNzIHZhbHVlcyBiZWxvbmdpbmcgdG8gZGlmZmVyZW50IFRQLWNsYXNzZXMqKjoKYGBge3IgVFAtY2xhc3NpZmljYXRpb246IFBsb3RzfQojIEJveCBwbG90cwpnZ2JveHBsb3QoZGYsIHggPSAidHAudW5pcXVlIiwgeSA9ICJmaXQudW5pcXVlIiwgY29sb3IgPSAidHAudW5pcXVlIiwKICAgICAgICAgIHBhbGV0dGUgPSB1c2VmdW46Ojpjb2xvcnMuMTAwWzE6bGVuZ3RoKHVuaXF1ZSh0cC51bmlxdWUpKV0sCiAgICAgICAgICB4bGFiID0gIlRydWUgUG9zaXRpdmVzIChUUCkiLCB5bGFiID0gIkZpdG5lc3MgdmFsdWVzIikKCiMgRGVuc2l0eSBQbG90cwpkZW5zaXRpZXMgPSBsaXN0KCkKZm9yICh0cC5udW0gaW4gc29ydCh1bmlxdWUodHAudW5pcXVlKSkpIHsKICB4ID0gZGYgJT4lCiAgICBmaWx0ZXIodHAudW5pcXVlID09IHRwLm51bSkgJT4lCiAgICBzZWxlY3RfYXQoLnZhcnMgPSBjKCJmaXQudW5pcXVlIikpCiAgZGVuID0gZGVuc2l0eSh4JGZpdC51bmlxdWUpCiAgZGVuc2l0aWVzW1twYXN0ZTAodHAubnVtLCAiICgiLCBkZW4kbiwgIikiKV1dID0gZGVuCn0KCm1ha2VfbXVsdGlwbGVfZGVuc2l0eV9wbG90KGRlbnNpdGllcywgbGVnZW5kLnRpdGxlID0gIlRQIGNsYXNzZXMgKCNtb2RlbHMpIiwKICAgICAgICB0aXRsZSA9ICJEZW5zaXR5IEVzdGltYXRpb24iLCB4LmF4aXMubGFiZWwgPSAiRml0bmVzcyBzY29yZSIpCmBgYAoKPGRpdiBjbGFzcz0iZ3JlZW4tYm94Ij4KQXMgd2UgY2FuIHNlZSBmcm9tIHRoZSBhYm92ZSBwbG90cywgdGhlcmUgaXMgcG9zaXRpdmUgY29ycmVsYXRpb24gYmV0d2VlbiB0aGUgCmNsYXNzZXMgdGhhdCBwcmVkaWN0ZWQgMCwxIGFuZCAyIFRQIHN5bmVyZ2llcyBhcyB3ZWxsIGFzIGJldHdlZW4gdGhlIDAgVFAgY2xhc3MKYW5kIHRoZSAzLDQsNS1UUCBjbGFzc2VzLgo8L2Rpdj4KPC9icj4KCk1hdGhlbWF0aWNhbGx5LCB3ZSBzaG93IHRoYXQgKip0aGUgZ3JvdXAgZGlzdHJpYnV0aW9ucyBhcmUgaW5kZWVkIGRpZmZlcmVudCB1c2luZyB0aGUgS3J1c2thbC1XYWxsaXMgUmFuayBTdW0gVGVzdCoqOgoKYGBge3IgVFAtY2xhc3NpZmljYXRpb246IEtydXNrYWwgVGVzdCwgY29tbWVudD0iIn0KIyBIeXBvdGhlc2lzIHRlc3Rpbmc6IEFyZSB0aGUgbG9jYXRpb24gcGFyYW1ldGVycyBvZiB0aGUgZGlzdHJpYnV0aW9uIG9mIHggdGhlIHNhbWUgaW4gZWFjaCBncm91cD8Ka3J1c2thbC50ZXN0KHggPSBkZlssImZpdC51bmlxdWUiXSwgZyA9IGRmWywgInRwLnVuaXF1ZSJdKQpgYGAKCkFzIHRoZSBwLXZhbHVlIGlzIGxlc3MgdGhhbiB0aGUgc2lnbmlmaWNhbmNlIGxldmVsICQwLjA1JCwgd2UgY2FuIGNvbmNsdWRlIAp0aGF0IHRoZXJlIGFyZSBzaWduaWZpY2FudCBkaWZmZXJlbmNlcyBiZXR3ZWVuIHRoZSBkaWZmZXJlbnQgZ3JvdXBzIG9mIGZpdG5lc3MgCnZhbHVlcy4gVG8gc2VlIGV4YWN0bHkgd2hpY2ggcGFpciBvZiBncm91cHMgYXJlIGRpZmZlcmVudCAqKndlIHBlcmZvcm0gcGFpcndpc2UKV2lsY294b24gcmFuayBzdW0gdGVzdHMgYW5kIHdlIGRyYXcgYSBoZWF0bWFwIG9mIHRoZSBlYWNoIHRlc3QncyBwLXZhbHVlKio6CmBgYHtyIFRQLWNsYXNzaWZpY2F0aW9uOiBQYWlyd2lzZSBXaWxjb3ggVGVzdHMsIHdhcm5pbmc9RkFMU0V9CnJlcyA9IHBhaXJ3aXNlLndpbGNveC50ZXN0KHggPSBkZlssImZpdC51bmlxdWUiXSwgZyA9IGRmWywgInRwLnVuaXF1ZSJdLCBwLmFkanVzdC5tZXRob2QgPSAiQkgiKQpwLnZhbHVlLm1hdCA9IHJlcyRwLnZhbHVlCmBgYAoKYGBge3IgVFAtY2xhc3NpZmljYXRpb246IHAtdmFsdWUgSGVhdG1hcCBvZiBQYWlyd2lzZSBXaWxjb3ggVGVzdHMsIHdhcm5pbmc9RkFMU0UsIGZpZy53aWR0aD05LCBkcGk9MzAwfQpjb2xfZnVuID0gY29sb3JSYW1wMihicmVha3MgPSBjKDAsIDAuMDUsIDAuNSwgMSksIGMoImdyZWVuIiwgIndoaXRlIiwgIm9yYW5nZSIsICJyZWQiKSkKaHQgPSBIZWF0bWFwKG1hdHJpeCA9IHAudmFsdWUubWF0LCBjbHVzdGVyX3Jvd3MgPSBGQUxTRSwgY2x1c3Rlcl9jb2x1bW5zID0gRkFMU0UsCiAgbmFfY29sID0gIndoaXRlIiwgbmFtZSA9ICJwLXZhbHVlIiwgY29sID0gY29sX2Z1biwgY29sdW1uX25hbWVzX3JvdCA9IDAsCiAgcm93X3RpdGxlID0gIlRQIiwgcm93X3RpdGxlX3JvdCA9IDAsIHJvd19uYW1lc19zaWRlID0gImxlZnQiLAogIGNvbHVtbl90aXRsZSA9ICJQLXZhbHVlcyAoUGFpcndpc2UgV2lsY294b24gdGVzdHMpIC0gVFAtY2xhc3NpZmllZCBmaXRuZXNzZXMiLCBjb2x1bW5fdGl0bGVfZ3AgPSBncGFyKGZvbnRzaXplID0gMjApLAogIGNlbGxfZnVuID0gZnVuY3Rpb24oaiwgaSwgeCwgeSwgd2lkdGgsIGhlaWdodCwgZmlsbCkgewogICAgaWYgKCFpcy5uYShwLnZhbHVlLm1hdFtpLGpdKSkKICAgICAgICBncmlkLnRleHQoc3ByaW50ZigiJS42ZiIsIHAudmFsdWUubWF0W2ksIGpdKSwgeCwgeSwgZ3AgPSBncGFyKGZvbnRzaXplID0gMTYpKQp9KQpkcmF3KGh0KQpgYGAKCkFzIHdlIGNhbiBzZWUgYWJvdmUgdGhlcmUgaXMgc2lnbmlmaWNhbnQgZGlmZmVyZW5jZSBiZXR3ZWVuIGdyb3VwcyBvZiBmaXRuZXNzCnZhbHVlcyBiZWxvbmdpbmcgdG8gbW9kZWxzIHRoYXQgcHJlZGljdGVkICQwJCwgJDEkIGFuZCAkMiQgVFAgc3luZXJnaWVzLCBidXQKbm90IGJldHdlZW4gdGhlc2UgYW5kIHRoZSBsYXJnZXIgcGVyZm9ybWFudCBncm91cHMgKCRUUD0zLDQsNSQpIC0gd2l0aCB0aGUgZXhjZXB0aW9uCm9mIHRoZSBncm91cCB3aXRoICRUUD0yJC4gCgpOb3RlIHRoYXQgdGhlICoqbnVtYmVyIG9mIHRydWUgcG9zaXRpdmUgcHJlZGljdGlvbnMgaXMgYW4gb25lLWRpbWVuc2lvbmFsIG1ldHJpYyoqIGFuZCBieSAKZXhjbHVkaW5nIHRoZSBUTiwgRlAgYW5kIEZOcyB3ZSBoYXZlIGEgbGVzcy1pbmZvcm1hbnQgKGFuZCBhcmd1YWJseSBpbmNvcnJlY3QpIApwaWN0dXJlIG9mIHRoZSBtb2RlbHMgcGVyZm9ybWFuY2UgY2xhc3NpZmljYXRpb24uIApUaGF0J3Mgd2h5IHdlIHdpbGwgcHJvY2VlZCB3aXRoIHRoZSBtb3JlIGJhbGFuY2VkIE1DQyBzY29yZSBmb3IgY2xhc3NpZmluZyB0aGUgbW9kZWxzIGZpdG5lc3NlcyAKdG8gZGlmZmVyZW50IHBlcmZvcm1hbmNlIGdyb3Vwcy4KCiMjIyBNQ0MtY2xhc3MgdnMgZml0bmVzcyB7LX0KCkZpcnN0bHksIHdlIHBlcmZvcm0gYSAqdW5pdmFyaWF0ZSBrLW1lYW5zIGNsdXN0ZXJpbmcqIHRvIAoqKnNwbGl0IHRoZSBtb2RlbHMgTUNDIHZhbHVlcyB0byBkaWZmZXJlbnQgY2xhc3NlcyoqIGFuZCBwbG90IHRoZSBkYXRhIGhpc3RvZ3JhbS4KVGhlICoqbnVtYmVyIG9mIE1DQyBjbGFzc2VzKiogdG8gc3BsaXQgdGhlIGRhdGEgY2FuIGJlIGFyYml0cmFyaWx5IGNob3NlbiBhbmQgc28Kd2UgY2hvc2Ugb25lIGxlc3MgdGhhbiB0aGUgbWF4aW11bSBudW1iZXIgb2YgVFBzIHByZWRpY3RlZDoKYGBge3IgTUNDLWNsYXNzaWZpY2F0aW9uOiBGaW5kIHRoZSBjbHVzdGVyc30KbnVtLm9mLm1jYy5jbGFzc2VzID0gNQptY2MuY2xhc3MuaWRzID0gMTpudW0ub2YubWNjLmNsYXNzZXMKCiMgZmluZCB0aGUgY2x1c3RlcnMKcmVzID0gQ2ttZWFucy4xZC5kcCh4ID0gZGZbLCJtY2MudW5pcXVlIl0sIGsgPSBudW0ub2YubWNjLmNsYXNzZXMpCm1jYy5jbGFzcy5pZCA9IHJlcyRjbHVzdGVyCmRmID0gY2JpbmQoZGYsIG1jYy5jbGFzcy5pZCkKCnBsb3RfbWNjX2NsYXNzZXNfaGlzdChkZlssIm1jYy51bmlxdWUiXSwgZGZbLCJtY2MuY2xhc3MuaWQiXSwKICAgICAgICAgICAgICAgICAgICAgIG51bS5vZi5tY2MuY2xhc3NlcywgbWNjLmNsYXNzLmlkcykKYGBgCgpUaGVuIHdlIHNob3cgc29tZSBib3ggYW5kIGRlbnNpdHkgcGxvdHMgdG8gc2VlIHByYWN0aWNhbGx5ICoqd2hhdCBhbmQgd2hlcmUgaXMgCnRoZSBkaWZmZXJlbmNlIGJldHdlZW4gdGhlIG1vZGVscyBmaXRuZXNzIHZhbHVlcyBiZWxvbmdpbmcgdG8gZGlmZmVyZW50IE1DQy1jbGFzc2VzKio6CmBgYHtyIE1DQy1jbGFzc2lmaWNhdGlvbjogUGxvdHN9CiMgQm94IHBsb3RzCiMgaWYgeW91IHdhbnQgdG8gYWRkIHAtdmFsdWVzIG9uIHRoZSBib3hwbG90CiNtY2MuY2xhc3MuaWQuY21wcyA9IGxpc3QoYygiMSIsICIyIiksIGMoIjEiLCAiMyIpLCBjKCIzIiwgIjUiKSkKZ2dib3hwbG90KGRmLCB4ID0gIm1jYy5jbGFzcy5pZCIsIHkgPSAiZml0LnVuaXF1ZSIsIGNvbG9yID0gIm1jYy5jbGFzcy5pZCIsCiAgICAgICAgICBwYWxldHRlID0gdXNlZnVuOjo6Y29sb3JzLjEwMFsxOm51bS5vZi5tY2MuY2xhc3Nlc10sCiAgICAgICAgICB4bGFiID0gIk1DQyBjbGFzcyIsIHlsYWIgPSAiRml0bmVzcyB2YWx1ZXMiKQojICsgc3RhdF9jb21wYXJlX21lYW5zKGNvbXBhcmlzb25zID0gbWNjLmNsYXNzLmlkLmNtcHMpICsgc3RhdF9jb21wYXJlX21lYW5zKCkKCiMgRGVuc2l0eSBQbG90cwpkZW5zaXRpZXMgPSBsaXN0KCkKZm9yIChpZCBpbiBtY2MuY2xhc3MuaWRzKSB7CiAgeCA9IGRmICU+JQogICAgZmlsdGVyKG1jYy5jbGFzcy5pZCA9PSBpZCkgJT4lCiAgICBzZWxlY3RfYXQoLnZhcnMgPSBjKCJmaXQudW5pcXVlIikpCiAgZGVuID0gZGVuc2l0eSh4JGZpdC51bmlxdWUpCiAgZGVuc2l0aWVzW1twYXN0ZTAoaWQsICIgKCIsIHJlcyRzaXplW2lkXSwgIikiKV1dID0gZGVuCn0KCm1ha2VfbXVsdGlwbGVfZGVuc2l0eV9wbG90KGRlbnNpdGllcywgbGVnZW5kLnRpdGxlID0gIk1DQyBjbGFzc2VzICgjbW9kZWxzKSIsCiAgICAgICAgdGl0bGUgPSAiRGVuc2l0eSBFc3RpbWF0aW9uIiwgeC5heGlzLmxhYmVsID0gIkZpdG5lc3Mgc2NvcmUiKQpgYGAKCjxkaXYgY2xhc3M9ImdyZWVuLWJveCI+CkFzIHdlIGNhbiBzZWUgZnJvbSB0aGUgYWJvdmUgcGxvdHMsICoqdGhlcmUgaXMgcG9zaXRpdmUgY29ycmVsYXRpb24gYmV0d2VlbiB0aGUgCmRpc3RyaWJ1dGlvbiBvZiBNQ0Mgc2NvcmVzIGluIGVhY2ggY2xhc3MgYW5kIHRoZSByZXNwZWN0aXZlIGZpdG5lc3Mgc2NvcmVzKiosIAp0aG91Z2ggbm90IGJldHdlZW4gdGhlIDNyZCBjbGFzcyBhbmQgdGhlIDR0aCBvciA1dGggKGl0J3MgbmVnYXRpdmUpLiA8L2Rpdj48L2JyPgpOb3RlIGFsc28gdGhhdCB0aGUgNXRoIGNsYXNzIGhhcyBhIGxvdCBsZXNzIG1vZGVscyB0aGFuIHRoZSByZXN0LgpNYXRoZW1hdGljYWxseSwgd2Ugc2hvdyB0aGF0ICoqdGhlIGdyb3VwIGRpc3RyaWJ1dGlvbnMgYXJlIGluZGVlZCBkaWZmZXJlbnQgCnVzaW5nIHRoZSBLcnVza2FsLVdhbGxpcyBSYW5rIFN1bSBUZXN0Kio6CmBgYHtyIE1DQy1jbGFzc2lmaWNhdGlvbjogS3J1c2thbCBUZXN0LCBjb21tZW50PSIifQojIEh5cG90aGVzaXMgdGVzdGluZzogQXJlIHRoZSBsb2NhdGlvbiBwYXJhbWV0ZXJzIG9mIHRoZSBkaXN0cmlidXRpb24gb2YgeCB0aGUgc2FtZSBpbiBlYWNoIGdyb3VwPwprcnVza2FsLnRlc3QoeCA9IGRmWywiZml0LnVuaXF1ZSJdLCBnID0gZGZbLCAibWNjLmNsYXNzLmlkIl0pCmBgYAoKQXMgdGhlIHAtdmFsdWUgaXMgbGVzcyB0aGFuIHRoZSBzaWduaWZpY2FuY2UgbGV2ZWwgJDAuMDUkLCB3ZSBjYW4gY29uY2x1ZGUgCnRoYXQgdGhlcmUgYXJlIHNpZ25pZmljYW50IGRpZmZlcmVuY2VzIGJldHdlZW4gdGhlIGRpZmZlcmVudCBncm91cHMgb2YgZml0bmVzcyAKdmFsdWVzLgoKTmV4dCwgdG8gc2hvdyB0aGF0IG1vc3Qgb2YgdGhlIGdyb3VwcyBhcmUgc3RhdGlzdGljYWxseSBkaWZmZXJlbnQsICoqd2UgcGVyZm9ybSAKcGFpcndpc2UgV2lsY294b24gcmFuayBzdW0gdGVzdHMgYW5kIGRyYXcgYSBoZWF0bWFwIG9mIHRoZSBlYWNoIHRlc3TigJlzIHAtdmFsdWUqKjoKYGBge3IgTUNDLWNsYXNzaWZpY2F0aW9uOiBQYWlyd2lzZSBXaWxjb3ggVGVzdHMsIHdhcm5pbmc9RkFMU0V9CnBhaXIucmVzID0gcGFpcndpc2Uud2lsY294LnRlc3QoeCA9IGRmWywiZml0LnVuaXF1ZSJdLCBnID0gZGZbLCAibWNjLmNsYXNzLmlkIl0sIHAuYWRqdXN0Lm1ldGhvZCA9ICJCSCIpCnAudmFsdWUubWF0ID0gcGFpci5yZXMkcC52YWx1ZQojIHNhbWU6IGNvbXBhcmVfbWVhbnMoZml0LnVuaXF1ZSB+IG1jYy5jbGFzcy5pZCwgZGF0YSA9IGRmKQpgYGAKCmBgYHtyIE1DQy1jbGFzc2lmaWNhdGlvbjogcC12YWx1ZSBIZWF0bWFwIG9mIFBhaXJ3aXNlIFdpbGNveCBUZXN0cywgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTksIGRwaT0zMDB9CmNvbF9mdW4gPSBjb2xvclJhbXAyKGJyZWFrcyA9IGMoMCwgMC4wNSwgMC41LCAxKSwgYygiZ3JlZW4iLCAid2hpdGUiLCAib3JhbmdlIiwgInJlZCIpKQpodCA9IEhlYXRtYXAobWF0cml4ID0gcC52YWx1ZS5tYXQsIGNsdXN0ZXJfcm93cyA9IEZBTFNFLCBjbHVzdGVyX2NvbHVtbnMgPSBGQUxTRSwKICBuYV9jb2wgPSAid2hpdGUiLCBuYW1lID0gInAtdmFsdWUiLCBjb2wgPSBjb2xfZnVuLCBjb2x1bW5fbmFtZXNfcm90ID0gMCwKICByb3dfdGl0bGUgPSAiTUNDIGNsYXNzIGlkIiwgcm93X3RpdGxlX3JvdCA9IDkwLCByb3dfbmFtZXNfc2lkZSA9ICJsZWZ0IiwKICBjb2x1bW5fdGl0bGUgPSAiUC12YWx1ZXMgKFBhaXJ3aXNlIFdpbGNveG9uIHRlc3RzKSAtIE1DQy1jbGFzc2lmaWVkIGZpdG5lc3NlcyIsIGNvbHVtbl90aXRsZV9ncCA9IGdwYXIoZm9udHNpemUgPSAyMCksCiAgY2VsbF9mdW4gPSBmdW5jdGlvbihqLCBpLCB4LCB5LCB3aWR0aCwgaGVpZ2h0LCBmaWxsKSB7CiAgICBpZiAoIWlzLm5hKHAudmFsdWUubWF0W2ksal0pKQogICAgICAgIGdyaWQudGV4dChzcHJpbnRmKCIlLjZmIiwgcC52YWx1ZS5tYXRbaSwgal0pLCB4LCB5LCBncCA9IGdwYXIoZm9udHNpemUgPSAyMCkpCn0pCmRyYXcoaHQpCmBgYAoKIyMgUiBzZXNzaW9uIGluZm8gey19CgpgYGB7ciBzZXNzaW9uIGluZm8sIGNvbW1lbnQ9IiJ9CnhmdW46OnNlc3Npb25faW5mbygpCmBgYAo=