Foreword:About the Machine Learning in Medicine project
This is the 35th case study by MLM team. The MLM project has been initialized in 2016 and aims to:
Encourage using Machine Learning techniques in medical research in Vietnam and
Promote the use of R statistical programming language, an open source and leading tool for practicing data science.
Introduction
In the previous tutorials (X13 and X14), we have explored the variable filtration and linear model selection. Filtration allows excluding the irrelevant features before model training, while model selection aims to optimize the model’s structure and performance and to prevent the overfitting. We have introduced some methods including Bayesian model averaging, Lasso and Elastic net regularisations. These solutions are strictly based on the linear regression.
In the present case study, we will explore a new method that could be used for feature filtration: the bootstrap aggregating or Random Forest algorithm. Given a classification problem, our goal is to fit the best Random Forest model for predictive purpose with strongest variables. We will also compare the performance of this method with logistic classifiers that based on Lasso, Ridge and Elastic net regularizations. Note that the Bayesian Model averaging (BMA) method will not be considered in our comparative study, due to high complexity of our data.
Dataset and Study objective
The present study implies the Cardiac Arrhythmia dataset. This dataset was created by H. Altay Guvenir (Ankara, Turkey) in 1997 for the development of a machine learning based algorithm to detect Arrhythmia from ECG recording.
The original dataset has 452 observations and 279 features, 206 of which are numerical and the rest are nominal. This was an ambitious study as the author want to classify 15 types of cardiac arrhythmia, such as Ischemic change, Myocardial infarction (anterior and Inferior), Sinus bradycardy and tachycardy, Ventricular Premature Contraction (PVC), Supraventricular Premature Contraction, Left/right bundle branch block, AtrioVentricular blocks (3 degrees), Left ventricule hypertrophy, Atrial Fibrillation or Flutter and Others.
For the demonstration purpose, we will simplify the study question to a binary classification problem. Our experiment aims to develop an algorithm to distinguish between the presence and absence of arrhythmia. As the dataset contains a large number of features, and many of them are irrelevant, we also want to select the best combination of important features in order to improve our model’s performance.
A description of the dataset could be found here:
<https://archive.ics.uci.edu/ml/machine-learning-databases/arrhythmia/arrhythmia.names
Material and method
Cardiac Arrhythmia is a famous dataset in the community of Medical datascientists because it’s a good opportunity to practice our skills of data wrangling, feature selection and multilabel modelling. This dataset has been used by Dr. Shirin Glander in her very good tutorial using “Sparkling water” (a tool that combines h2o and Spark):
https://shiring.github.io/machine_learning/2017/02/27/h2o
In our tutorial, we borrowed some of her R codes, but our approach is totally different. First, we treated numeric and nominal features separately, and we dont use neither PCA, nor variance based feature selection but focus on the Random Forest and other regularisation methods.
First, the original dataset D1 was decomposed into Numeric and Nominal parts. Each one was then visually explored in order to detect the missing values, invariance features or missing label problem. Those variables will be removed in the first place. The remaining variables will be gathered in a smaller dataset D2. This one was randomly splitted into two subsets for training and testing. From that moment, we dont touch the test subset as it will be used for the model validation.
On the train subset, we performed our data experiment. It consists of using a replicated Random Forest model training to identify the most important features in the train subset. The h2O package was used for this procedure.
The basic algorithm consisted of a Random Forest with max tree number of 100, max depth of 50, sample rate of 70% (which means each tree is trained on 70% of the input data); and each split only used about ~15% (234^0.5) features as candidates. This algorithm was trained by a 10x10 crossvalidation with balanced stratification for each fold and early stopping criteria.
Such procedure was replicated 100 times. For each step the trained RF model was validated on the test set, and feature importances have been extracted for each model. These information were then averaged for 100 models to provide a full and clear picture of the importance of each features. This allows building the best combination of most relevant variables with a good certainty. This feature combination was used as input for a new Random Forest algorithm. The final version of RF model entered a comparative study against 3 other logistic based algorithms.
We also built 3 other logistic models that based on Lasso, Ridge and Elasticnet regularisations. The Lasso and Ridge models were supported by h2o framework, while the elasticnet model was based on glmnet package. All 3 models were trained by a 10x10 crossvalidation.
Data preparation and exploration
First, we will prepare the ggplot theme for our experiment
library(tidyverse)
theme_bare <- function(base_size=8,base_family="sans"){theme_bw(base_size = base_size, base_family = base_family)+
theme(
axis.line = element_blank(),
axis.text.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks = element_blank(),
axis.title.x = element_blank(),
axis.title.y = element_blank(),
legend.position = "bottom",
panel.background = element_rect(fill = NA),
panel.border = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.margin = unit(c(0,0,0,0), "lines")
)
}
my_theme <- function(base_size =5, base_family = "sans"){
theme_bw(base_size = base_size, base_family = base_family) +
theme(
panel.grid.major = element_line(color = "gray"),
panel.grid.minor = element_blank(),
panel.background = element_rect(fill = NA),
strip.background = element_rect(fill = "#8e0444", color = "#8e0444", size =0.5),
strip.text = element_text(face = "bold", size = 5, color = "white"),
legend.position = "bottom",
legend.justification = "center",
legend.background = element_blank(),
legend.margin = margin(0.5,0.5,0.5,0.5)
)
}
theme_set(my_theme())
Now we load the dataset from UCI database:
#load data
arrhythmia <- read.table("https://archive.ics.uci.edu/ml/machine-learning-databases/arrhythmia/arrhythmia.data", sep = ",")
arrhythmia[arrhythmia == "?"] <- NA
# Extract the nominal features
arrhythmia[-280] <- lapply(arrhythmia[-280], as.character)
arrhythmia[-280] <- lapply(arrhythmia[-280], as.numeric)
dfnominal=arrhythmia[,c(280,2,22:27,34:39,46:51,58:63,70:75,82:87,94:99,106:111,118:123,130:135,142:147,154:159)]
#k1=74
dfnominal[]<-lapply(dfnominal, as.factor)
colnames(dfnominal)[1] <- "class"
The nominal part has 74 categorical variables (all of them are binary factor). The target variable (class) is also included as a categorical factor. A simple visualisation will show us the proportion of 16 labels:
ggplot(dfnominal, aes(x = as.factor(class)))+geom_bar(aes(fill=as.factor(class)), alpha = 0.7,show.legend = F)+my_theme(10)+scale_x_discrete("16 labels of cardiac Arrhythmia")+geom_text(stat='count',aes(label=..count..),vjust=-0.5)

As mentioned above, we will simplify the study question by considering only two labels: healthy or arrhythmia (as Dr. Shirin Glander did).
dfnominal$class<-ifelse(dfnominal$class == "1", "healthy", "arrhythmia")
dfnominal$class =as.factor(dfnominal$class)
ggplot(dfnominal, aes(x = class))+geom_bar(aes(fill=as.factor(class)), alpha = 0.7,show.legend = F)+my_theme(10)+geom_text(stat='count',aes(label=..count..),hjust=5,size=10,color="white")+coord_flip()+scale_fill_manual(values=c("#f70260","#079eef"))

By counting the distribution of two target labels (healthy and arrhythmia) within each label of 73 nominal variables, we can see that some nominal variables don’t contribute to the prediction of the target labels. Those variables could be removed (even if we keep them, they will be later rejected as h2o can detect automatically those variables and remove them from the input before training).
dfnominal%>%gather(V2:V159,key="Feature",value="Level")%>%ggplot(aes(x=Feature,fill=class,color=class))+geom_bar(position="fill",alpha=0.7)+scale_fill_manual(values=c("#f70260","#079eef"))+scale_color_manual(values=c("#d80053","#006fd8"))+facet_wrap(~Level)+coord_polar()+scale_x_discrete("Levels (0 or 1)")

Now we extract the numeric variables from the original dataset. There are 206 numeric variables:
dfnumeric=arrhythmia[,c(1,3:21,28:33,40:45,52:57,64:69,76:81,88:93,100:105,112:117,124:129,136:141,148:153,160:279)]
First, we check whether there are any missing value among those numeric variables
bar_missing <- function(x){
library(dplyr)
library(reshape2)
library(ggplot2)
x %>%
is.na %>%
melt %>%
ggplot(data = .,
aes(x = Var2)) +
geom_bar(aes(y=(..count..),fill=value),alpha=0.7)+scale_fill_manual(values=c("gold","red3"),name = "",
labels = c("Available","Missing"))+
theme_minimal(5)+
theme(axis.text.x = element_text(angle=45, vjust=0.5)) +
labs(x = "Variables in Dataset",
y = "Observations")+coord_flip()
}
dfnumeric%>%bar_missing()

As we could see, the variables V11,V12,V13 and V15 contain some missing values, while the variable V14 has so many missing values. We decided to remove those variables.
dfnumeric=dfnumeric[,-c(11,12,13,14,15)]#remove because too many missing value
The next step consists of visualising the variance of the remaining 205 numeric variables. A variable with small or null variance will be considered irrelevant for modelling so they could also be removed.
Based on the results, following variables have no variance: V20,68,140,152,165,205,265,275.
The graph shows that most of the numeric variable have low variance.
library(matrixStats)
colvars <- data.frame(feature = colnames(dfnumeric),
variance = colVars(as.matrix(dfnumeric)))
subset(colvars, variance==0)
## feature variance
## 14 V20 0
## 38 V68 0
## 74 V140 0
## 80 V152 0
## 87 V165 0
## 127 V205 0
## 187 V265 0
## 197 V275 0
subset(colvars, variance>=20)%>%
mutate(feature = factor(feature, levels = colnames(dfnumeric)))%>%
ggplot(aes(x = reorder(feature,variance), y = variance))+
geom_point(aes(color=variance), alpha = 0.7)+
theme_minimal(5)+
scale_color_gradient2(low="gold",high="purple",mid="red",midpoint = 700)+scale_x_discrete("Numeric variables")+coord_polar()

We decided to remove following variables, as they dont have any contribution to our prediction:
df=cbind(dfnominal,dfnumeric)
removed=c("V22", "V165", "V140", "V20", "V68", "V146", "V133", "V85", "V158", "V145", "V132", "V84", "V157", "V46", "V155", "V142", "V72", "V144", "V152", "V86", "V38", "V70", "V205", "V275", "V265")
df=df[,!(names(df) %in% removed)]
The final dataset contain 452 observations and 248 potential features. It’s a good start but there is still many (200) numeric features, as showed in this correlation matrix and correlation network. Filtering those variable would be difficult without using advanced methods.
library(corrplot)
library(RColorBrewer)
cor_mat=as.matrix(cor(use="pairwise.complete.obs",method="spearman",as.matrix(dfnumeric)))
cor_mat%>%corrplot(.,hclust.method ="ward.D2",type="lower",method="color",tl.col="black", tl.srt=45,tl.cex=0.5,col=rev(brewer.pal(n=10, name="RdBu")))

library(igraph)
diag(cor_mat)<-0
library(ggraph)
m=cor_mat
cdf=data.frame(row=rownames(m)[row(m)[upper.tri(m)]],
col=colnames(m)[col(m)[upper.tri(m)]],
corr=m[upper.tri(m)])
cdf=subset(cdf,abs(corr)>0.5)
cdf$Close=cdf$corr>=0.7
names(cdf)=c("from","to","corr","Close")
graph<-graph_from_data_frame(cdf)
ggraph(graph, layout = 'fr')+geom_edge_density(aes(fill = Close))+geom_node_point()+geom_node_text(aes(label = name),nudge_x=,nudge_y=0.1,color="grey")+geom_edge_link(aes(colour = corr),width=1,alpha=0.5)+theme_bare()+scale_edge_fill_manual(values=c("white","#ffdf14"))+scale_edge_color_gradient(high="#ff003f",low="#0094ff")
To this point, we got a smaller dataset of 248 variables, we will split this into two subsets, one for training and another for testing:
set.seed(1234)
idTrain=caret::createDataPartition(y=df$class,p=351/452,list=FALSE)
trainset=df[idTrain,]
testset=df[-idTrain,]
sp1=df%>%ggplot(aes(x=class,fill=class))+stat_count(color="black",alpha=0.7,show.legend = F)+scale_fill_manual(values=c("#f70260","#079eef"))+coord_flip()+ggtitle("Origin")+geom_text(stat='count',aes(label=..count..),hjust=5,size=5,color="white")+theme_bw(10)
sp2=trainset%>%ggplot(aes(x=class,fill=class))+stat_count(color="black",alpha=0.7,show.legend = F)+scale_fill_manual(values=c("#f70260","#079eef"))+coord_flip()+ggtitle("Train")+geom_text(stat='count',aes(label=..count..),hjust=5,size=5,color="white")+theme_bw(10)
sp3=testset%>%ggplot(aes(x=class,fill=class))+stat_count(color="black",alpha=0.7,show.legend = F)+scale_fill_manual(values=c("#f70260","#079eef"))+coord_flip()+ggtitle("Test")+geom_text(stat='count',aes(label=..count..),hjust=5,size=5,color="white")+theme_bw(10)
gridExtra::grid.arrange(sp1,sp2,sp3,ncol=1)

Now we begin our machine learning experiment. First, we load the h2O package and convert train/test subsets into h2O class:
Feature selection using replicated Random Forest
library(h2o)
h2o.init(nthreads = -1,max_mem_size ="4g")
wtrain=as.h2o(trainset)
wtest=as.h2o(testset)
features=setdiff(colnames(wtrain),"class")
The first solution consists of replicating a basic Random Forest (RF) algorithm 100 times, then extracting the information of variable importances.
Note that each RF model is trained using a different seed number, however this randomness will be reproducible so you will get the same result as given here (unlike the randomforest algorithm in mlr package).
Let’s run these codes and drink a cup of coffee because it will take some times…
set.seed(12345)
rfseed=sample(10000,100,replace=F)
res=NULL
pdf=NULL
for (i in 1:100){
rfmod=h2o.randomForest(x = features,
y = "class",
training_frame = wtrain,nfolds=10,
fold_assignment = "Stratified",
balance_classes = TRUE,
ntrees = 100, max_depth = 50,mtries = -1,sample_rate = 0.7,
stopping_metric = "misclassification",
stopping_tolerance = 0.01,
stopping_rounds = 3,
keep_cross_validation_fold_assignment = F,
keep_cross_validation_predictions=F,
score_each_iteration = TRUE,
seed=rfseed[i])
pdftemp=predict(rfmod,wtest)%>%as_tibble()%>%mutate(.,Id=row.names(testset),Truth=testset$class,Accuracy=ifelse(testset$class ==.$predict, "Correct", "Wrong"),Model=i)
vimp=rfmod@model$variable_importances%>%as_tibble()%>%mutate(.,Seed=rfseed[i],Model=i)
res=rbind(res,vimp)
pdf=rbind(pdf,pdftemp)
}
res=as_tibble(res)
res$Model=as.factor(res$Model)
As you could see in the loop, we have performed 3 tasks on each RF model: Training by crossvalidation, predicting the probability of arrhythmia in test subset, and extracting the variable importance matrix of the trained model. By replicating those tasks 100 times for 100 different versions of RF models, we finally got 2 large dataframes: (1) “pdf” contains information of accuracy of 100 predictions in testset and (2) “res”" dataframe that contains information of ariable importances.
First, we will explore the pdf dataframe, as before exploring the variable importances we want to know how good 100 RF model performed if the input consists of all 248 features. If we dont consider a feature filtration is there a consequence on the model’s performance ?
pdf$Id=as.factor(pdf$Id)
pdf%>%ggplot(aes(x=Model,y=reorder(Id,arrhythmia),fill=arrhythmia))+geom_tile()+theme_bw()+scale_fill_gradient2(low="green",mid="gold",high="red",midpoint = 0.5)+facet_wrap(~Truth,shrink=TRUE,scale="free",ncol=1)+theme_bw(8)+theme(axis.text.x = element_text(angle =45, hjust = 1))

The first heatmap plot represents the predicted probability of arrhythmia by 100 RF models (horizontal axis) for each case among 100 patients (vertical axis), when all 248 features have been used as input. A good model would give high probability for patients with true arrhythmia (orange or red colors) and lower probability in healthy subjects (green color). The presence of green color in Arrhythmic group indicates a false negative, while the presence of red color in Healthy group means false positive. Both of them are wrong and affect the balanced accuracy of the model.
Overall, We can see that the Random Forest algorithm handled well the problem without any feature filtration. In fact, the randomness in RF algorithm already removed the irrelevant features. Such performance is consistent through 100 different versions, so we can trust the variable importance results given by these models. However, there are still false positive and false negative predictions.
The accuracy of 100 RF models can also be evaluated qualitatively, as follows:
pdf%>%ggplot(aes(x=Model,y=reorder(Id,1-arrhythmia),fill=Accuracy))+geom_tile()+theme_bw()+scale_fill_manual(values=c("skyblue","red"))+theme_bw(8)+theme(axis.text.x = element_text(angle =45, hjust = 1))+facet_wrap(~Truth,shrink=TRUE,scale="free",ncol=1)+scale_y_discrete("Idx")

We will run a second loop for estimating the averaged predicted probability and averaged accuracy of 100 models:
pdf2=NULL
for (i in 1:100){
Idt=pdf$Id[i]
dtemp=subset(pdf,Id==Idt)
sum=Hmisc::describe(dtemp)
predprob=sum$arrhythmia$counts[[5]]
acc=sum$Accuracy$values[[2]][1]/100
wrong=sum$Accuracy$values[[2]][2]/100
pdftemp=cbind.data.frame(Id=Idt,predprob,acc,wrong)
pdf2=rbind(pdf2,pdftemp)
}
The 2 following graphs display the median of predicted probability and percentage of accurate/wrong predictions when 100 models were applied to the test subset (you can feel it likes reading a confusion matrix).
pdf2$class=testset$class
pdf2$predprob=as.numeric(as.character(pdf2$predprob))
pdf2$Id=as.factor(pdf2$Id)
pdf2$wrong[is.na(pdf2$wrong)] <- 0
pdf2%>%ggplot(aes(x=reorder(Id,predprob),y=predprob,fill=predprob))+geom_bar(alpha=0.7,stat="identity")+theme_bw(8)+scale_fill_gradient2(low="gold",mid="red",high="purple",midpoint = 0.4)+facet_wrap(~class,shrink = TRUE,ncol=1,scales = "free")+geom_hline(yintercept = 0.5,linetype=2,size=1)

pdf2%>%gather(wrong,acc,key="Accuracy",value="Prediction")%>%ggplot(aes(x=reorder(Id,-predprob),y=Prediction,fill=Accuracy))+geom_bar(alpha=0.7,position="fill",stat="Identity")+theme_bw(8)+facet_wrap(~class,shrink = TRUE,ncol=1,scales = "free")+scale_fill_manual(values=c("skyblue","red"))+scale_x_discrete("Id")

So, we can trust our 100 RF models in terms of prediction, now we will see what information they give us about the importances of 248 features:
The following heatmap plots indicate the relative importance and scaled importance scores, respectively. More important a feature is, more saturated in color that feature would be. Based on those graphs, we can make a conclusion that up to 83/248 features (1/3) could be removed, as they did not contribute to the prediction. We only need about 150 to 160 features to build a good Random Forest model.
res%>%ggplot(aes(x=reorder(Model,relative_importance),y=reorder(variable,-relative_importance),fill=relative_importance,color=relative_importance))+geom_tile(show.legend = F)+theme_bw()+scale_fill_gradient2(low="white",mid="red",high="purple",midpoint = 50)+scale_color_gradient2(low="white",mid="red",high="purple",midpoint = 50)+theme_bw(5)+theme(axis.text.x = element_text(angle =45, hjust = 1))+scale_x_discrete("Models")+scale_y_discrete("Features")

res%>%ggplot(aes(x=reorder(Model,scaled_importance),y=reorder(variable,-scaled_importance),fill=scaled_importance,color=scaled_importance))+geom_tile(show.legend = F)+theme_bw()+scale_fill_gradient2(low="white",mid="gold",high="red",midpoint = 0.5)+scale_color_gradient2(low="white",mid="gold",high="red",midpoint =0.5)+theme_bw(5)+theme(axis.text.x = element_text(angle =45, hjust = 1))+scale_x_discrete("Models")+scale_y_discrete("Features")

As above, we will also run another loop to extract the averaged importance score for each features:
res2=NULL
for (i in 1:253){
dtemp=subset(res,variable==features[i])
sum=psych::describe(dtemp)
rei=sum[[5]][2]
sci=sum[[5]][3]
pct=sum[[5]][4]
rtemp=cbind.data.frame(rei,sci,pct,var=dtemp[[1]][1])
res2=rbind(res2,rtemp)
}
res2%>%ggplot(aes(x=reorder(var,rei),y=rei,fill=rei,color=rei))+geom_segment(aes(x=reorder(var,rei),xend=var,y=0,yend=rei),size=1,show.legend = F)+geom_point(size=1,show.legend = F)+scale_x_discrete("Features")+scale_y_continuous("Relative importance")+coord_flip()+scale_fill_gradient(low="blue",high="red")+scale_color_gradient(low="blue",high="red")+ggtitle("Relative importance")+theme_bw(5)

We decided to keep only 150 strongest variables in our input:
res3=res2%>%arrange(desc(rei))%>%.[c(1:150),]
res3%>%ggplot(aes(x=reorder(var,rei),y=rei,fill=rei,color=rei))+geom_segment(aes(x=reorder(var,rei),xend=var,y=0,yend=rei),size=1,show.legend = F)+geom_point(size=1,show.legend = F)+scale_x_discrete("Features")+scale_y_continuous("Relative importance")+coord_flip()+scale_fill_gradient(low="blue",high="red")+scale_color_gradient(low="blue",high="red")+ggtitle("Relative importance")+theme_bw(5)

Based on a new input that contains only 150 strongest features, we will train another model (Note that the seed number is reset to 333)
features2=as.vector(res3$var)
rfmod=h2o.randomForest(x = features2,
y = "class",
training_frame = wtrain,nfolds=10,
fold_assignment = "Stratified",
balance_classes = TRUE,
ntrees = 100, max_depth = 50,mtries = -1,sample_rate = 0.8,
stopping_metric = "misclassification",
stopping_tolerance = 0.01,
stopping_rounds = 3,
keep_cross_validation_fold_assignment = F,
keep_cross_validation_predictions=F,
score_each_iteration = TRUE,
seed=333)
Logistic models
We have finish our first task - building a Random Forest model for our classification problem. Now, we will consider another 3 solutions that based on Logistic regression.
The first one will be LASSO logistic model (seed =546)
lassomod=h2o.glm(x=features,
y="class",
training_frame=wtrain,
nfolds = 10,seed = 546,
keep_cross_validation_predictions = FALSE,
keep_cross_validation_fold_assignment = FALSE,
fold_assignment = "Stratified",
family = "binomial",
alpha=1,
lambda_search = TRUE,nlambdas =20,
early_stopping = TRUE,
standardize = TRUE,
missing_values_handling ="Skip",
compute_p_values = FALSE,
remove_collinear_columns = FALSE,
intercept = TRUE,
non_negative = FALSE
)
Here are the coefficients of selected features for this Lasso logistic model, note that the Lasso regularisation has automatically removed all the features that were found to be not important. The final logistic model only contains 25 features.
coefs=lassomod@model$coefficients_table%>%as.tibble()%>%subset(.,coefficients!=0)
coefs
## # A tibble: 36 x 3
## names coefficients standardized_coefficients
## <chr> <dbl> <dbl>
## 1 Intercept 2.494735507 0.12566888
## 2 V5 -0.019842112 -0.29095732
## 3 V8 -0.001109552 -0.03821251
## 4 V9 0.006421963 0.16184794
## 5 V21 -0.011460841 -0.11614159
## 6 V30 -0.001667630 -0.03562249
## 7 V40 -0.002662741 -0.05973024
## 8 V44 0.003609711 0.01170101
## 9 V56 0.011496913 0.01977849
## 10 V65 -0.002871800 -0.06530719
## # ... with 26 more rows
The second solution is Ridge regularisation (seed=999). This could also be trained in h2O. Unlikes LASSO, Ridge regularisation only shrink the coefficients close to zero, but not zero, so basically the Ridge model cannot be used for feature selection.
ridgemod=h2o.glm(x=features,
y="class",
training_frame=wtrain,
nfolds = 10,seed = 999,
keep_cross_validation_predictions = FALSE,
keep_cross_validation_fold_assignment = FALSE,
fold_assignment = "Stratified",
family = "binomial",
alpha=0,
lambda_search = TRUE,nlambdas =20,
early_stopping = TRUE,
standardize = TRUE,
missing_values_handling ="Skip",
compute_p_values = FALSE,
remove_collinear_columns = FALSE,
intercept = TRUE,
non_negative = FALSE
)
The last solution will be Elastic net model (seed =5566). The training of this model is done using glmnet package. Note that this package requires transforming input data to matrix, and it does not accept missing values.
#Elastic net
library(glmnet)
set.seed(5566)
trainsetenet=trainset%>%na.omit()
testsetenet=testset%>%na.omit()
trainsetenet[-1]<-lapply(trainsetenet[-1], as.numeric)
testsetenet[-1]<-lapply(testsetenet[-1], as.numeric)
x=as.matrix(trainsetenet[,-1])
y=trainsetenet[,1]
enet = cv.glmnet(x,y,family = "binomial",type.measure = "class")
plot(enet)

The elastic net regularisation is a hybrid method that combines both l1 and l2 regularisations (Lasso and Ridge). The results showed that Elastic net model only kept 30 features
coef(enet)%>%as.matrix()%>%as.data.frame()%>%subset(.,`1`!=0)
## 1
## (Intercept) 2.8119512883
## V25 -0.1274432489
## V49 0.1015183400
## V61 -0.5036084377
## V95 -0.0273590394
## V109 -0.1351489123
## V5 -0.0239069823
## V9 0.0041789599
## V21 -0.0016934672
## V30 -0.0010216309
## V69 -0.0053685839
## V81 -0.0073229387
## V90 0.0035919755
## V91 -0.0226190775
## V93 -0.0087924941
## V100 -0.0101638548
## V103 -0.0283531476
## V112 -0.0126353743
## V136 -0.0061550696
## V163 0.0108526736
## V167 0.2202514658
## V176 0.0415050940
## V199 -0.0174245959
## V211 0.1937751294
## V239 0.0008013161
## V252 -0.0071867336
## V256 0.0646729497
## V260 0.1065758008
Validation and comparative study
Finally, we got 4 candidates of classifiers. In order to determine which has the best performance, we will perform a comparative study. We will evaluate 14 performance metrics for each model when applied to the test subset, then plot the results on a radar chart to visualize the difference.
The performance metrics can be divided into two groups : the first group has best value of 1, including: Balanced accuracy(BAC), Area under ROC curve (AUC), Accuracy (ACC), F1 score, Positive/Negative predictive value and True negative/positive rates. These metrics indicate the accuracy, sensitivity and specificity of the model. The second group contains the metrics that have best value of 0 and worst value of 1, including Balance error rate (BER), Brier score, False negative/positive rates, Mean misclassification error and False discovery rate (FDR). Those scores measure the weakness of each model. For ECG reading, false negative prediction is more dangerous than false positive.
library(mlr)
task=makeClassifTask(data=testset,target="class")
dummylrner=makeLearner("classif.rpart",predict.type ="prob")
dummymod=train(dummylrner,task)
dummypred=predict(dummymod,task)
LASSOPRED=predict(lassomod,wtest)%>%as.data.frame()
LASSOPRED=LASSOPRED%>%mutate(.,id=c(1:100),truth=testset$class,prob.arrhythmia=.[,2],prob.healthy=.[,3],response=.$predict)%>%.[,-c(1:3)]
RIDGEPRED=predict(ridgemod,wtest)%>%as.data.frame()
RIDGEPRED=RIDGEPRED%>%mutate(.,id=c(1:100),truth=testset$class,prob.arrhythmia=.[,2],prob.healthy=.[,3],response=.$predict)%>%.[,-c(1:3)]
enetclass=predict(enet,as.matrix(testsetenet[,-1]),type="class")%>%as.data.frame()
ENETPRED=predict(enet,as.matrix(testsetenet[,-1]),type="response")%>%as.data.frame()%>%mutate(.,id=c(1:98),truth=testsetenet$class,prob.arrhythmia=1-.[,1],prob.healthy=.[,1],response=enetclass[,1])%>%.[,-1]
RFPRED=predict(rfmod,wtest)%>%as.data.frame()
RFPRED=RFPRED%>%mutate(.,id=c(1:100),truth=testset$class,prob.arrhythmia=.[,2],prob.healthy=.[,3],response=.$predict)%>%.[,-c(1:3)]
pred1=dummypred
pred1$data=LASSOPRED
pred2=dummypred
pred2$data=ENETPRED
pred3=dummypred
pred3$data=RFPRED
pred4=dummypred
pred4$data=RIDGEPRED%>%na.omit()
measure=list(mlr::bac,mlr::auc,mlr::acc,mlr::f1,mlr::ppv,mlr::npv,mlr::tnr,mlr::tpr)
p1=performance(pred1,measure)
p2=performance(pred2,measure)
p3=performance(pred3,measure)
p4=performance(pred4,measure)
lasso=p1%>%as.vector()
enet=p2%>%as.vector()
rf=p3%>%as.vector()
rdg=p4%>%as.vector()
labs=c("BAC","AUC","ACC","F1","PPV","NPV","TPR","TNR")
scores=list("Lasso"=lasso*10,"Elasticnet"=enet*10,"RandomForest"=rf*10,"Ridge"=rdg*10)
library(radarchart)
chartJSRadar(scores = scores, labs = labs, maxScale = 10)
measure2=list(ber,brier,fnr,fpr,mmce,fdr)
e1=performance(pred1,measure2)
e2=performance(pred2,measure2)
e3=performance(pred3,measure2)
e4=performance(pred4,measure2)
lasso=e1%>%as.vector()
enet=e2%>%as.vector()
rf=e3%>%as.vector()
rdg=e4%>%as.vector()
labs=c("BER","BRIER","FNR","FPR","MMCE","FDR")
scores=list("Lasso"=lasso,"Elasticnet"=enet,"RandomForest"=rf,"Ridge"=rdg)
chartJSRadar(scores = scores, labs = labs, maxScale =0.8)
The results suggest that the performances was almost similar for 4 methods.
Conclusion
In this case study, we have travelled a long journey from a messy dataset to the final classifier that could correctly distinguish patients with arrhythmia from healthy subjects in 78.6% of cases. Following points could be kept in mind:
Data exploration and visualisation is very important, this procedure allows to detect some obvious problem with our data, including the missing value, invariance variables, outliers, missing labels… and these information can already help us to eliminate the irrelevant features in our dataset, without using any special technique.
Random Forest is a powerful method for both feature selection and overfitting control. Due to the randomness in bootstrap aggregating, only one model could be not trustful; however by replicating the model training many times, we can reach a high level of certainty. In this tutorial, we provided a reproducible loop function for this method. Such procedure allows training 100 or more basic Random Forest algorithm with both randomness and reproducibility.
LASSO, Ridge and Elasticnet regularisations are also good for the feature selection task. These methods dont require too much computation cost but can also give a good performance. Lasso and Elasticnet also keep smaller number of features and as they are based on logistic regression, the coefficients of those variables could be interpreted, not like the Random Forest which is a Blackbox.
Thank you for joining us and see you in the next tutorial.
LS0tDQp0aXRsZTogIk1MTSBDYXNlIHN0dWR5IFgxNSINCnN1YnRpdGxlOiAiTW9kZWwgc2VsZWN0aW9uLiBQYXJ0IDI6IENsYXNzaWZpY2F0aW9uIHByb2JsZW0iDQphdXRob3I6ICJMZSBOZ29jIEtoYSBOaGkgKE1ELCBQaEQpIg0KZGF0ZTogIkp1bHkgMDEsIDIwMTciDQpvdXRwdXQ6IA0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogam91cm5hbA0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCBlcnJvciA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkNCg0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KG1hdHJpeFN0YXRzKQ0KbGlicmFyeShjb3JycGxvdCkNCmxpYnJhcnkoUkNvbG9yQnJld2VyKQ0KbGlicmFyeShpZ3JhcGgpDQpsaWJyYXJ5KGdncmFwaCkNCmxpYnJhcnkoZ2xtbmV0KQ0KbGlicmFyeShoMm8pDQpsaWJyYXJ5KG1scikNCmxpYnJhcnkocmFkYXJjaGFydCkNCg0KYGBgDQoNCiFbXShNb2RlbFNlbENsYXNzaWYucG5nKQ0KDQojIyBGb3Jld29yZDpBYm91dCB0aGUgTWFjaGluZSBMZWFybmluZyBpbiBNZWRpY2luZSBwcm9qZWN0IA0KDQpUaGlzIGlzIHRoZSAzNXRoIGNhc2Ugc3R1ZHkgYnkgTUxNIHRlYW0uIFRoZSBNTE0gcHJvamVjdCBoYXMgYmVlbiBpbml0aWFsaXplZCBpbiAyMDE2IGFuZCBhaW1zIHRvOiANCg0KKDEpIEVuY291cmFnZSB1c2luZyBNYWNoaW5lIExlYXJuaW5nIHRlY2huaXF1ZXMgaW4gbWVkaWNhbCByZXNlYXJjaCBpbiBWaWV0bmFtIGFuZA0KDQooMikgUHJvbW90ZSB0aGUgdXNlIG9mIFIgc3RhdGlzdGljYWwgcHJvZ3JhbW1pbmcgbGFuZ3VhZ2UsIGFuIG9wZW4gc291cmNlIGFuZCBsZWFkaW5nIHRvb2wgZm9yIHByYWN0aWNpbmcgZGF0YSBzY2llbmNlLg0KICAgIA0KIyMgSW50cm9kdWN0aW9uDQoNCkluIHRoZSBwcmV2aW91cyB0dXRvcmlhbHMgKFgxMyBhbmQgWDE0KSwgd2UgaGF2ZSBleHBsb3JlZCB0aGUgdmFyaWFibGUgZmlsdHJhdGlvbiBhbmQgbGluZWFyIG1vZGVsIHNlbGVjdGlvbi4gRmlsdHJhdGlvbiBhbGxvd3MgZXhjbHVkaW5nIHRoZSBpcnJlbGV2YW50IGZlYXR1cmVzIGJlZm9yZSBtb2RlbCB0cmFpbmluZywgd2hpbGUgbW9kZWwgc2VsZWN0aW9uIGFpbXMgdG8gb3B0aW1pemUgdGhlIG1vZGVsJ3Mgc3RydWN0dXJlIGFuZCBwZXJmb3JtYW5jZSBhbmQgdG8gcHJldmVudCB0aGUgb3ZlcmZpdHRpbmcuIFdlIGhhdmUgaW50cm9kdWNlZCBzb21lIG1ldGhvZHMgaW5jbHVkaW5nIEJheWVzaWFuIG1vZGVsIGF2ZXJhZ2luZywgTGFzc28gYW5kIEVsYXN0aWMgbmV0IHJlZ3VsYXJpc2F0aW9ucy4gVGhlc2Ugc29sdXRpb25zIGFyZSBzdHJpY3RseSBiYXNlZCBvbiB0aGUgbGluZWFyIHJlZ3Jlc3Npb24uDQoNCkluIHRoZSBwcmVzZW50IGNhc2Ugc3R1ZHksIHdlIHdpbGwgZXhwbG9yZSBhIG5ldyBtZXRob2QgdGhhdCBjb3VsZCBiZSB1c2VkIGZvciBmZWF0dXJlIGZpbHRyYXRpb246IHRoZSBib290c3RyYXAgYWdncmVnYXRpbmcgb3IgUmFuZG9tIEZvcmVzdCBhbGdvcml0aG0uIEdpdmVuIGEgY2xhc3NpZmljYXRpb24gcHJvYmxlbSwgb3VyIGdvYWwgaXMgdG8gZml0IHRoZSBiZXN0IFJhbmRvbSBGb3Jlc3QgbW9kZWwgZm9yIHByZWRpY3RpdmUgcHVycG9zZSB3aXRoIHN0cm9uZ2VzdCB2YXJpYWJsZXMuIFdlIHdpbGwgYWxzbyBjb21wYXJlIHRoZSBwZXJmb3JtYW5jZSBvZiB0aGlzIG1ldGhvZCB3aXRoIGxvZ2lzdGljIGNsYXNzaWZpZXJzIHRoYXQgYmFzZWQgb24gTGFzc28sIFJpZGdlIGFuZCBFbGFzdGljIG5ldCByZWd1bGFyaXphdGlvbnMuIE5vdGUgdGhhdCB0aGUgQmF5ZXNpYW4gTW9kZWwgYXZlcmFnaW5nIChCTUEpIG1ldGhvZCB3aWxsIG5vdCBiZSBjb25zaWRlcmVkIGluIG91ciBjb21wYXJhdGl2ZSBzdHVkeSwgZHVlIHRvIGhpZ2ggY29tcGxleGl0eSBvZiBvdXIgZGF0YS4NCg0KIyMgRGF0YXNldCBhbmQgU3R1ZHkgb2JqZWN0aXZlDQoNClRoZSBwcmVzZW50IHN0dWR5IGltcGxpZXMgdGhlIENhcmRpYWMgQXJyaHl0aG1pYSBkYXRhc2V0LiBUaGlzIGRhdGFzZXQgd2FzIGNyZWF0ZWQgYnkgSC4gQWx0YXkgR3V2ZW5pciAoQW5rYXJhLCBUdXJrZXkpIGluIDE5OTcgZm9yIHRoZSBkZXZlbG9wbWVudCBvZiBhIG1hY2hpbmUgbGVhcm5pbmcgYmFzZWQgYWxnb3JpdGhtIHRvIGRldGVjdCBBcnJoeXRobWlhIGZyb20gRUNHIHJlY29yZGluZy4gDQoNClRoZSBvcmlnaW5hbCBkYXRhc2V0IGhhcyA0NTIgb2JzZXJ2YXRpb25zIGFuZCAyNzkgZmVhdHVyZXMsIDIwNiBvZiB3aGljaCBhcmUgbnVtZXJpY2FsIGFuZCB0aGUgcmVzdCBhcmUgbm9taW5hbC4gVGhpcyB3YXMgYW4gYW1iaXRpb3VzIHN0dWR5IGFzIHRoZSBhdXRob3Igd2FudCB0byBjbGFzc2lmeSAxNSB0eXBlcyBvZiBjYXJkaWFjIGFycmh5dGhtaWEsIHN1Y2ggYXMgSXNjaGVtaWMgY2hhbmdlLCBNeW9jYXJkaWFsIGluZmFyY3Rpb24gKGFudGVyaW9yIGFuZCBJbmZlcmlvciksIFNpbnVzIGJyYWR5Y2FyZHkgYW5kIHRhY2h5Y2FyZHksIFZlbnRyaWN1bGFyIFByZW1hdHVyZSBDb250cmFjdGlvbiAoUFZDKSwgU3VwcmF2ZW50cmljdWxhciBQcmVtYXR1cmUgQ29udHJhY3Rpb24sIExlZnQvcmlnaHQgYnVuZGxlIGJyYW5jaCBibG9jaywgQXRyaW9WZW50cmljdWxhciBibG9ja3MgKDMgZGVncmVlcyksIExlZnQgdmVudHJpY3VsZSBoeXBlcnRyb3BoeSwgQXRyaWFsIEZpYnJpbGxhdGlvbiBvciBGbHV0dGVyIGFuZCBPdGhlcnMuDQoNCkZvciB0aGUgZGVtb25zdHJhdGlvbiBwdXJwb3NlLCB3ZSB3aWxsIHNpbXBsaWZ5IHRoZSBzdHVkeSBxdWVzdGlvbiB0byBhIGJpbmFyeSBjbGFzc2lmaWNhdGlvbiBwcm9ibGVtLiBPdXIgZXhwZXJpbWVudCBhaW1zIHRvIGRldmVsb3AgYW4gYWxnb3JpdGhtIHRvIGRpc3Rpbmd1aXNoIGJldHdlZW4gdGhlIHByZXNlbmNlIGFuZCBhYnNlbmNlIG9mIGFycmh5dGhtaWEuIEFzIHRoZSBkYXRhc2V0IGNvbnRhaW5zIGEgbGFyZ2UgbnVtYmVyIG9mIGZlYXR1cmVzLCBhbmQgbWFueSBvZiB0aGVtIGFyZSBpcnJlbGV2YW50LCB3ZSBhbHNvIHdhbnQgdG8gc2VsZWN0IHRoZSBiZXN0IGNvbWJpbmF0aW9uIG9mIGltcG9ydGFudCBmZWF0dXJlcyBpbiBvcmRlciB0byBpbXByb3ZlIG91ciBtb2RlbOKAmXMgcGVyZm9ybWFuY2UuDQoNCkEgZGVzY3JpcHRpb24gb2YgdGhlIGRhdGFzZXQgY291bGQgYmUgZm91bmQgaGVyZTogDQoNCjxodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvbWFjaGluZS1sZWFybmluZy1kYXRhYmFzZXMvYXJyaHl0aG1pYS9hcnJoeXRobWlhLm5hbWVzDQoNCiMjIE1hdGVyaWFsIGFuZCBtZXRob2QNCg0KQ2FyZGlhYyBBcnJoeXRobWlhIGlzIGEgZmFtb3VzIGRhdGFzZXQgaW4gdGhlIGNvbW11bml0eSBvZiBNZWRpY2FsIGRhdGFzY2llbnRpc3RzIGJlY2F1c2UgaXQncyBhIGdvb2Qgb3Bwb3J0dW5pdHkgdG8gcHJhY3RpY2Ugb3VyIHNraWxscyBvZiBkYXRhIHdyYW5nbGluZywgZmVhdHVyZSBzZWxlY3Rpb24gYW5kIG11bHRpbGFiZWwgbW9kZWxsaW5nLiBUaGlzIGRhdGFzZXQgaGFzIGJlZW4gdXNlZCBieSBEci4gU2hpcmluIEdsYW5kZXIgaW4gaGVyIHZlcnkgZ29vZCB0dXRvcmlhbCB1c2luZyAiU3BhcmtsaW5nIHdhdGVyIiAoYSB0b29sIHRoYXQgY29tYmluZXMgaDJvIGFuZCBTcGFyayk6DQoNCjxodHRwczovL3NoaXJpbmcuZ2l0aHViLmlvL21hY2hpbmVfbGVhcm5pbmcvMjAxNy8wMi8yNy9oMm8+DQoNCg0KSW4gb3VyIHR1dG9yaWFsLCB3ZSBib3Jyb3dlZCBzb21lIG9mIGhlciBSIGNvZGVzLCBidXQgb3VyIGFwcHJvYWNoIGlzIHRvdGFsbHkgZGlmZmVyZW50LiBGaXJzdCwgd2UgdHJlYXRlZCBudW1lcmljIGFuZCBub21pbmFsIGZlYXR1cmVzIHNlcGFyYXRlbHksIGFuZCB3ZSBkb250IHVzZSAgbmVpdGhlciBQQ0EsIG5vciB2YXJpYW5jZSBiYXNlZCBmZWF0dXJlIHNlbGVjdGlvbiBidXQgZm9jdXMgb24gdGhlIFJhbmRvbSBGb3Jlc3QgYW5kIG90aGVyIHJlZ3VsYXJpc2F0aW9uIG1ldGhvZHMuIA0KDQohW10oTW9kZWxzZWxlY3Rpb25kYXRhLnBuZykNCg0KRmlyc3QsIHRoZSBvcmlnaW5hbCBkYXRhc2V0IEQxIHdhcyBkZWNvbXBvc2VkIGludG8gTnVtZXJpYyBhbmQgTm9taW5hbCBwYXJ0cy4gRWFjaCBvbmUgd2FzIHRoZW4gdmlzdWFsbHkgZXhwbG9yZWQgaW4gb3JkZXIgdG8gZGV0ZWN0IHRoZSBtaXNzaW5nIHZhbHVlcywgaW52YXJpYW5jZSBmZWF0dXJlcyBvciBtaXNzaW5nIGxhYmVsIHByb2JsZW0uIFRob3NlIHZhcmlhYmxlcyB3aWxsIGJlIHJlbW92ZWQgaW4gdGhlIGZpcnN0IHBsYWNlLiBUaGUgcmVtYWluaW5nIHZhcmlhYmxlcyB3aWxsIGJlIGdhdGhlcmVkIGluIGEgc21hbGxlciBkYXRhc2V0IEQyLiBUaGlzIG9uZSB3YXMgcmFuZG9tbHkgc3BsaXR0ZWQgaW50byB0d28gc3Vic2V0cyBmb3IgdHJhaW5pbmcgYW5kIHRlc3RpbmcuIEZyb20gdGhhdCBtb21lbnQsIHdlIGRvbnQgdG91Y2ggdGhlIHRlc3Qgc3Vic2V0IGFzIGl0IHdpbGwgYmUgdXNlZCBmb3IgdGhlIG1vZGVsIHZhbGlkYXRpb24uDQoNCk9uIHRoZSB0cmFpbiBzdWJzZXQsIHdlIHBlcmZvcm1lZCBvdXIgZGF0YSBleHBlcmltZW50LiBJdCBjb25zaXN0cyBvZiB1c2luZyBhIHJlcGxpY2F0ZWQgUmFuZG9tIEZvcmVzdCBtb2RlbCB0cmFpbmluZyB0byBpZGVudGlmeSB0aGUgbW9zdCBpbXBvcnRhbnQgZmVhdHVyZXMgaW4gdGhlIHRyYWluIHN1YnNldC4gVGhlIGgyTyBwYWNrYWdlIHdhcyB1c2VkIGZvciB0aGlzIHByb2NlZHVyZS4gDQoNClRoZSBiYXNpYyBhbGdvcml0aG0gY29uc2lzdGVkIG9mIGEgUmFuZG9tIEZvcmVzdCB3aXRoIG1heCB0cmVlIG51bWJlciBvZiAxMDAsIG1heCBkZXB0aCBvZiA1MCwgc2FtcGxlIHJhdGUgb2YgNzAlICh3aGljaCBtZWFucyBlYWNoIHRyZWUgaXMgdHJhaW5lZCBvbiA3MCUgb2YgdGhlIGlucHV0IGRhdGEpOyBhbmQgZWFjaCBzcGxpdCBvbmx5IHVzZWQgYWJvdXQgfjE1JSAoMjM0XjAuNSkgZmVhdHVyZXMgYXMgY2FuZGlkYXRlcy4gVGhpcyBhbGdvcml0aG0gd2FzIHRyYWluZWQgYnkgYSAxMHgxMCBjcm9zc3ZhbGlkYXRpb24gd2l0aCBiYWxhbmNlZCBzdHJhdGlmaWNhdGlvbiBmb3IgZWFjaCBmb2xkIGFuZCBlYXJseSBzdG9wcGluZyBjcml0ZXJpYS4NCg0KU3VjaCBwcm9jZWR1cmUgd2FzIHJlcGxpY2F0ZWQgMTAwIHRpbWVzLiBGb3IgZWFjaCBzdGVwIHRoZSB0cmFpbmVkIFJGIG1vZGVsIHdhcyB2YWxpZGF0ZWQgb24gdGhlIHRlc3Qgc2V0LCBhbmQgZmVhdHVyZSBpbXBvcnRhbmNlcyBoYXZlIGJlZW4gZXh0cmFjdGVkIGZvciBlYWNoIG1vZGVsLiBUaGVzZSBpbmZvcm1hdGlvbiB3ZXJlIHRoZW4gYXZlcmFnZWQgZm9yIDEwMCBtb2RlbHMgdG8gcHJvdmlkZSBhIGZ1bGwgYW5kIGNsZWFyIHBpY3R1cmUgb2YgdGhlIGltcG9ydGFuY2Ugb2YgZWFjaCBmZWF0dXJlcy4gVGhpcyBhbGxvd3MgYnVpbGRpbmcgdGhlIGJlc3QgY29tYmluYXRpb24gb2YgbW9zdCByZWxldmFudCB2YXJpYWJsZXMgd2l0aCBhIGdvb2QgY2VydGFpbnR5LiBUaGlzIGZlYXR1cmUgY29tYmluYXRpb24gd2FzIHVzZWQgYXMgaW5wdXQgZm9yIGEgbmV3IFJhbmRvbSBGb3Jlc3QgYWxnb3JpdGhtLiBUaGUgZmluYWwgdmVyc2lvbiBvZiBSRiBtb2RlbCBlbnRlcmVkIGEgY29tcGFyYXRpdmUgc3R1ZHkgYWdhaW5zdCAzIG90aGVyIGxvZ2lzdGljIGJhc2VkIGFsZ29yaXRobXMuDQoNCldlIGFsc28gYnVpbHQgMyBvdGhlciBsb2dpc3RpYyBtb2RlbHMgdGhhdCBiYXNlZCBvbiBMYXNzbywgUmlkZ2UgYW5kIEVsYXN0aWNuZXQgcmVndWxhcmlzYXRpb25zLiBUaGUgTGFzc28gYW5kIFJpZGdlIG1vZGVscyB3ZXJlIHN1cHBvcnRlZCBieSBoMm8gZnJhbWV3b3JrLCB3aGlsZSB0aGUgZWxhc3RpY25ldCBtb2RlbCB3YXMgYmFzZWQgb24gZ2xtbmV0IHBhY2thZ2UuIEFsbCAzIG1vZGVscyB3ZXJlIHRyYWluZWQgYnkgYSAxMHgxMCBjcm9zc3ZhbGlkYXRpb24uDQoNCiMjIERhdGEgcHJlcGFyYXRpb24gYW5kIGV4cGxvcmF0aW9uDQoNCkZpcnN0LCB3ZSB3aWxsIHByZXBhcmUgdGhlIGdncGxvdCB0aGVtZSBmb3Igb3VyIGV4cGVyaW1lbnQNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQoNCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQp0aGVtZV9iYXJlIDwtIGZ1bmN0aW9uKGJhc2Vfc2l6ZT04LGJhc2VfZmFtaWx5PSJzYW5zIil7dGhlbWVfYncoYmFzZV9zaXplID0gYmFzZV9zaXplLCBiYXNlX2ZhbWlseSA9IGJhc2VfZmFtaWx5KSsNCiAgICB0aGVtZSgNCiAgICAgIGF4aXMubGluZSA9IGVsZW1lbnRfYmxhbmsoKSwgDQogICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwgDQogICAgICBheGlzLnRleHQueSA9IGVsZW1lbnRfYmxhbmsoKSwNCiAgICAgIGF4aXMudGlja3MgPSBlbGVtZW50X2JsYW5rKCksIA0KICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLCANCiAgICAgIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfYmxhbmsoKSwgDQogICAgICBsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgDQogICAgICBwYW5lbC5iYWNrZ3JvdW5kID0gZWxlbWVudF9yZWN0KGZpbGwgPSBOQSksIA0KICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpLCANCiAgICAgIHBhbmVsLmdyaWQubWFqb3IgPSBlbGVtZW50X2JsYW5rKCksIA0KICAgICAgcGFuZWwuZ3JpZC5taW5vciA9IGVsZW1lbnRfYmxhbmsoKSwgDQogICAgICBwbG90Lm1hcmdpbiA9IHVuaXQoYygwLDAsMCwwKSwgImxpbmVzIikNCiAgICApDQp9DQoNCm15X3RoZW1lIDwtIGZ1bmN0aW9uKGJhc2Vfc2l6ZSA9NSwgYmFzZV9mYW1pbHkgPSAic2FucyIpew0KICB0aGVtZV9idyhiYXNlX3NpemUgPSBiYXNlX3NpemUsIGJhc2VfZmFtaWx5ID0gYmFzZV9mYW1pbHkpICsNCiAgICB0aGVtZSgNCiAgICAgIHBhbmVsLmdyaWQubWFqb3IgPSBlbGVtZW50X2xpbmUoY29sb3IgPSAiZ3JheSIpLA0KICAgICAgcGFuZWwuZ3JpZC5taW5vciA9IGVsZW1lbnRfYmxhbmsoKSwNCiAgICAgIHBhbmVsLmJhY2tncm91bmQgPSBlbGVtZW50X3JlY3QoZmlsbCA9IE5BKSwNCiAgICAgIHN0cmlwLmJhY2tncm91bmQgPSBlbGVtZW50X3JlY3QoZmlsbCA9ICIjOGUwNDQ0IiwgY29sb3IgPSAiIzhlMDQ0NCIsIHNpemUgPTAuNSksDQogICAgICBzdHJpcC50ZXh0ID0gZWxlbWVudF90ZXh0KGZhY2UgPSAiYm9sZCIsIHNpemUgPSA1LCBjb2xvciA9ICJ3aGl0ZSIpLA0KICAgICAgbGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIsDQogICAgICBsZWdlbmQuanVzdGlmaWNhdGlvbiA9ICJjZW50ZXIiLA0KICAgICAgbGVnZW5kLmJhY2tncm91bmQgPSBlbGVtZW50X2JsYW5rKCksDQogICAgICBsZWdlbmQubWFyZ2luID0gbWFyZ2luKDAuNSwwLjUsMC41LDAuNSkNCiAgICApDQp9DQoNCnRoZW1lX3NldChteV90aGVtZSgpKQ0KDQpgYGANCg0KTm93IHdlIGxvYWQgdGhlIGRhdGFzZXQgZnJvbSBVQ0kgZGF0YWJhc2U6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KI2xvYWQgZGF0YQ0KDQphcnJoeXRobWlhIDwtIHJlYWQudGFibGUoImh0dHBzOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9tYWNoaW5lLWxlYXJuaW5nLWRhdGFiYXNlcy9hcnJoeXRobWlhL2Fycmh5dGhtaWEuZGF0YSIsIHNlcCA9ICIsIikNCmFycmh5dGhtaWFbYXJyaHl0aG1pYSA9PSAiPyJdIDwtIE5BDQoNCg0KIyBFeHRyYWN0IHRoZSBub21pbmFsIGZlYXR1cmVzDQoNCmFycmh5dGhtaWFbLTI4MF0gPC0gbGFwcGx5KGFycmh5dGhtaWFbLTI4MF0sIGFzLmNoYXJhY3RlcikNCmFycmh5dGhtaWFbLTI4MF0gPC0gbGFwcGx5KGFycmh5dGhtaWFbLTI4MF0sIGFzLm51bWVyaWMpDQoNCmRmbm9taW5hbD1hcnJoeXRobWlhWyxjKDI4MCwyLDIyOjI3LDM0OjM5LDQ2OjUxLDU4OjYzLDcwOjc1LDgyOjg3LDk0Ojk5LDEwNjoxMTEsMTE4OjEyMywxMzA6MTM1LDE0MjoxNDcsMTU0OjE1OSldDQoNCiNrMT03NA0KDQpkZm5vbWluYWxbXTwtbGFwcGx5KGRmbm9taW5hbCwgYXMuZmFjdG9yKQ0KDQpjb2xuYW1lcyhkZm5vbWluYWwpWzFdIDwtICJjbGFzcyINCg0KYGBgDQoNClRoZSBub21pbmFsIHBhcnQgaGFzIDc0IGNhdGVnb3JpY2FsIHZhcmlhYmxlcyAoYWxsIG9mIHRoZW0gYXJlIGJpbmFyeSBmYWN0b3IpLiBUaGUgdGFyZ2V0IHZhcmlhYmxlIChjbGFzcykgaXMgYWxzbyBpbmNsdWRlZCBhcyBhIGNhdGVnb3JpY2FsIGZhY3Rvci4gQSBzaW1wbGUgdmlzdWFsaXNhdGlvbiB3aWxsIHNob3cgdXMgdGhlIHByb3BvcnRpb24gb2YgMTYgbGFiZWxzOg0KDQpgYGB7cn0NCmdncGxvdChkZm5vbWluYWwsIGFlcyh4ID0gYXMuZmFjdG9yKGNsYXNzKSkpK2dlb21fYmFyKGFlcyhmaWxsPWFzLmZhY3RvcihjbGFzcykpLCBhbHBoYSA9IDAuNyxzaG93LmxlZ2VuZCA9IEYpK215X3RoZW1lKDEwKStzY2FsZV94X2Rpc2NyZXRlKCIxNiBsYWJlbHMgb2YgY2FyZGlhYyBBcnJoeXRobWlhIikrZ2VvbV90ZXh0KHN0YXQ9J2NvdW50JyxhZXMobGFiZWw9Li5jb3VudC4uKSx2anVzdD0tMC41KQ0KDQpgYGANCg0KQXMgbWVudGlvbmVkIGFib3ZlLCB3ZSB3aWxsIHNpbXBsaWZ5IHRoZSBzdHVkeSBxdWVzdGlvbiBieSBjb25zaWRlcmluZyBvbmx5IHR3byBsYWJlbHM6IGhlYWx0aHkgb3IgYXJyaHl0aG1pYSAoYXMgRHIuIFNoaXJpbiBHbGFuZGVyIGRpZCkuIA0KDQpgYGB7cn0NCmRmbm9taW5hbCRjbGFzczwtaWZlbHNlKGRmbm9taW5hbCRjbGFzcyA9PSAiMSIsICJoZWFsdGh5IiwgImFycmh5dGhtaWEiKQ0KZGZub21pbmFsJGNsYXNzID1hcy5mYWN0b3IoZGZub21pbmFsJGNsYXNzKQ0KYGBgDQoNCmBgYHtyfQ0KZ2dwbG90KGRmbm9taW5hbCwgYWVzKHggPSBjbGFzcykpK2dlb21fYmFyKGFlcyhmaWxsPWFzLmZhY3RvcihjbGFzcykpLCBhbHBoYSA9IDAuNyxzaG93LmxlZ2VuZCA9IEYpK215X3RoZW1lKDEwKStnZW9tX3RleHQoc3RhdD0nY291bnQnLGFlcyhsYWJlbD0uLmNvdW50Li4pLGhqdXN0PTUsc2l6ZT0xMCxjb2xvcj0id2hpdGUiKStjb29yZF9mbGlwKCkrc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoIiNmNzAyNjAiLCIjMDc5ZWVmIikpDQoNCmBgYA0KDQpCeSBjb3VudGluZyB0aGUgZGlzdHJpYnV0aW9uIG9mIHR3byB0YXJnZXQgbGFiZWxzIChoZWFsdGh5IGFuZCBhcnJoeXRobWlhKSB3aXRoaW4gZWFjaCBsYWJlbCBvZiA3MyBub21pbmFsIHZhcmlhYmxlcywgd2UgY2FuIHNlZSB0aGF0IHNvbWUgbm9taW5hbCB2YXJpYWJsZXMgZG9uJ3QgY29udHJpYnV0ZSB0byB0aGUgcHJlZGljdGlvbiBvZiB0aGUgdGFyZ2V0IGxhYmVscy4gVGhvc2UgdmFyaWFibGVzIGNvdWxkIGJlIHJlbW92ZWQgKGV2ZW4gaWYgd2Uga2VlcCB0aGVtLCB0aGV5IHdpbGwgYmUgbGF0ZXIgcmVqZWN0ZWQgYXMgaDJvIGNhbiBkZXRlY3QgYXV0b21hdGljYWxseSB0aG9zZSB2YXJpYWJsZXMgYW5kIHJlbW92ZSB0aGVtIGZyb20gdGhlIGlucHV0IGJlZm9yZSB0cmFpbmluZykuDQoNCmBgYHtyfQ0KZGZub21pbmFsJT4lZ2F0aGVyKFYyOlYxNTksa2V5PSJGZWF0dXJlIix2YWx1ZT0iTGV2ZWwiKSU+JWdncGxvdChhZXMoeD1GZWF0dXJlLGZpbGw9Y2xhc3MsY29sb3I9Y2xhc3MpKStnZW9tX2Jhcihwb3NpdGlvbj0iZmlsbCIsYWxwaGE9MC43KStzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiI2Y3MDI2MCIsIiMwNzllZWYiKSkrc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jKCIjZDgwMDUzIiwiIzAwNmZkOCIpKStmYWNldF93cmFwKH5MZXZlbCkrY29vcmRfcG9sYXIoKStzY2FsZV94X2Rpc2NyZXRlKCJMZXZlbHMgKDAgb3IgMSkiKQ0KDQpgYGANCg0KTm93IHdlIGV4dHJhY3QgdGhlIG51bWVyaWMgdmFyaWFibGVzIGZyb20gdGhlIG9yaWdpbmFsIGRhdGFzZXQuIFRoZXJlIGFyZSAyMDYgbnVtZXJpYyB2YXJpYWJsZXM6DQoNCmBgYHtyfQ0KZGZudW1lcmljPWFycmh5dGhtaWFbLGMoMSwzOjIxLDI4OjMzLDQwOjQ1LDUyOjU3LDY0OjY5LDc2OjgxLDg4OjkzLDEwMDoxMDUsMTEyOjExNywxMjQ6MTI5LDEzNjoxNDEsMTQ4OjE1MywxNjA6Mjc5KV0NCg0KYGBgDQoNCkZpcnN0LCB3ZSBjaGVjayB3aGV0aGVyIHRoZXJlIGFyZSBhbnkgbWlzc2luZyB2YWx1ZSBhbW9uZyB0aG9zZSBudW1lcmljIHZhcmlhYmxlcw0KDQpgYGB7cn0NCmJhcl9taXNzaW5nIDwtIGZ1bmN0aW9uKHgpew0KICBsaWJyYXJ5KGRwbHlyKQ0KICBsaWJyYXJ5KHJlc2hhcGUyKQ0KICBsaWJyYXJ5KGdncGxvdDIpDQogIHggJT4lDQogICAgaXMubmEgJT4lDQogICAgbWVsdCAlPiUNCiAgICBnZ3Bsb3QoZGF0YSA9IC4sDQogICAgICAgICAgIGFlcyh4ID0gVmFyMikpICsNCiAgICBnZW9tX2JhcihhZXMoeT0oLi5jb3VudC4uKSxmaWxsPXZhbHVlKSxhbHBoYT0wLjcpK3NjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCJnb2xkIiwicmVkMyIpLG5hbWUgPSAiIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIkF2YWlsYWJsZSIsIk1pc3NpbmciKSkrDQogICAgdGhlbWVfbWluaW1hbCg1KSsNCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZT00NSwgdmp1c3Q9MC41KSkgKw0KICAgIGxhYnMoeCA9ICJWYXJpYWJsZXMgaW4gRGF0YXNldCIsDQogICAgICAgICB5ID0gIk9ic2VydmF0aW9ucyIpK2Nvb3JkX2ZsaXAoKQ0KfQ0KDQpkZm51bWVyaWMlPiViYXJfbWlzc2luZygpDQpgYGANCg0KQXMgd2UgY291bGQgc2VlLCB0aGUgdmFyaWFibGVzIFYxMSxWMTIsVjEzIGFuZCBWMTUgY29udGFpbiBzb21lIG1pc3NpbmcgdmFsdWVzLCB3aGlsZSB0aGUgdmFyaWFibGUgVjE0IGhhcyBzbyBtYW55IG1pc3NpbmcgdmFsdWVzLiBXZSBkZWNpZGVkIHRvIHJlbW92ZSB0aG9zZSB2YXJpYWJsZXMuDQoNCiFbXShtaXNzaW5nLnBuZykNCg0KYGBge3J9DQpkZm51bWVyaWM9ZGZudW1lcmljWywtYygxMSwxMiwxMywxNCwxNSldI3JlbW92ZSBiZWNhdXNlIHRvbyBtYW55IG1pc3NpbmcgdmFsdWUNCmBgYA0KDQpUaGUgbmV4dCBzdGVwIGNvbnNpc3RzIG9mIHZpc3VhbGlzaW5nIHRoZSB2YXJpYW5jZSBvZiB0aGUgcmVtYWluaW5nIDIwNSBudW1lcmljIHZhcmlhYmxlcy4gQSB2YXJpYWJsZSB3aXRoIHNtYWxsIG9yIG51bGwgdmFyaWFuY2Ugd2lsbCBiZSBjb25zaWRlcmVkIGlycmVsZXZhbnQgZm9yIG1vZGVsbGluZyBzbyB0aGV5IGNvdWxkIGFsc28gYmUgcmVtb3ZlZC4NCg0KQmFzZWQgb24gdGhlIHJlc3VsdHMsIGZvbGxvd2luZyB2YXJpYWJsZXMgaGF2ZSBubyB2YXJpYW5jZTogVjIwLDY4LDE0MCwxNTIsMTY1LDIwNSwyNjUsMjc1LiANCg0KVGhlIGdyYXBoIHNob3dzIHRoYXQgbW9zdCBvZiB0aGUgbnVtZXJpYyB2YXJpYWJsZSBoYXZlIGxvdyB2YXJpYW5jZS4NCg0KYGBge3J9DQpsaWJyYXJ5KG1hdHJpeFN0YXRzKQ0KDQpjb2x2YXJzIDwtIGRhdGEuZnJhbWUoZmVhdHVyZSA9IGNvbG5hbWVzKGRmbnVtZXJpYyksDQogICAgICAgICAgICAgICAgICAgICAgdmFyaWFuY2UgPSBjb2xWYXJzKGFzLm1hdHJpeChkZm51bWVyaWMpKSkNCg0Kc3Vic2V0KGNvbHZhcnMsIHZhcmlhbmNlPT0wKQ0KDQpzdWJzZXQoY29sdmFycywgdmFyaWFuY2U+PTIwKSU+JQ0KICBtdXRhdGUoZmVhdHVyZSA9IGZhY3RvcihmZWF0dXJlLCBsZXZlbHMgPSBjb2xuYW1lcyhkZm51bWVyaWMpKSklPiUNCiAgZ2dwbG90KGFlcyh4ID0gcmVvcmRlcihmZWF0dXJlLHZhcmlhbmNlKSwgeSA9IHZhcmlhbmNlKSkrDQogIGdlb21fcG9pbnQoYWVzKGNvbG9yPXZhcmlhbmNlKSwgYWxwaGEgPSAwLjcpKw0KICB0aGVtZV9taW5pbWFsKDUpKw0KICBzY2FsZV9jb2xvcl9ncmFkaWVudDIobG93PSJnb2xkIixoaWdoPSJwdXJwbGUiLG1pZD0icmVkIixtaWRwb2ludCA9IDcwMCkrc2NhbGVfeF9kaXNjcmV0ZSgiTnVtZXJpYyB2YXJpYWJsZXMiKStjb29yZF9wb2xhcigpDQoNCmBgYA0KDQpXZSBkZWNpZGVkIHRvIHJlbW92ZSBmb2xsb3dpbmcgdmFyaWFibGVzLCBhcyB0aGV5IGRvbnQgaGF2ZSBhbnkgY29udHJpYnV0aW9uIHRvIG91ciBwcmVkaWN0aW9uOg0KDQpgYGB7cn0NCmRmPWNiaW5kKGRmbm9taW5hbCxkZm51bWVyaWMpDQoNCnJlbW92ZWQ9YygiVjIyIiwgIlYxNjUiLCAiVjE0MCIsICJWMjAiLCAiVjY4IiwgIlYxNDYiLCAiVjEzMyIsICJWODUiLCAiVjE1OCIsICJWMTQ1IiwgIlYxMzIiLCAiVjg0IiwgIlYxNTciLCAiVjQ2IiwgIlYxNTUiLCAiVjE0MiIsICJWNzIiLCAiVjE0NCIsICJWMTUyIiwgIlY4NiIsICJWMzgiLCAiVjcwIiwgIlYyMDUiLCAiVjI3NSIsICJWMjY1IikNCg0KZGY9ZGZbLCEobmFtZXMoZGYpICVpbiUgcmVtb3ZlZCldIA0KDQpgYGANCg0KVGhlIGZpbmFsIGRhdGFzZXQgY29udGFpbiA0NTIgb2JzZXJ2YXRpb25zIGFuZCAyNDggcG90ZW50aWFsIGZlYXR1cmVzLiBJdCdzIGEgZ29vZCBzdGFydCBidXQgdGhlcmUgaXMgc3RpbGwgbWFueSAoMjAwKSBudW1lcmljIGZlYXR1cmVzLCBhcyBzaG93ZWQgaW4gdGhpcyBjb3JyZWxhdGlvbiBtYXRyaXggYW5kIGNvcnJlbGF0aW9uIG5ldHdvcmsuIEZpbHRlcmluZyB0aG9zZSB2YXJpYWJsZSB3b3VsZCBiZSBkaWZmaWN1bHQgd2l0aG91dCB1c2luZyBhZHZhbmNlZCBtZXRob2RzLg0KDQpgYGB7cn0NCmxpYnJhcnkoY29ycnBsb3QpDQpsaWJyYXJ5KFJDb2xvckJyZXdlcikNCg0KY29yX21hdD1hcy5tYXRyaXgoY29yKHVzZT0icGFpcndpc2UuY29tcGxldGUub2JzIixtZXRob2Q9InNwZWFybWFuIixhcy5tYXRyaXgoZGZudW1lcmljKSkpDQoNCmNvcl9tYXQlPiVjb3JycGxvdCguLGhjbHVzdC5tZXRob2QgPSJ3YXJkLkQyIix0eXBlPSJsb3dlciIsbWV0aG9kPSJjb2xvciIsdGwuY29sPSJibGFjayIsIHRsLnNydD00NSx0bC5jZXg9MC41LGNvbD1yZXYoYnJld2VyLnBhbChuPTEwLCBuYW1lPSJSZEJ1IikpKQ0KDQpgYGANCg0KDQpgYGB7cixmaWcuc2hvdyA9ImhpZGUifQ0KbGlicmFyeShpZ3JhcGgpDQoNCmRpYWcoY29yX21hdCk8LTANCg0KbGlicmFyeShnZ3JhcGgpDQoNCm09Y29yX21hdA0KDQpjZGY9ZGF0YS5mcmFtZShyb3c9cm93bmFtZXMobSlbcm93KG0pW3VwcGVyLnRyaShtKV1dLCANCiAgICAgICAgICAgICAgY29sPWNvbG5hbWVzKG0pW2NvbChtKVt1cHBlci50cmkobSldXSwgDQogICAgICAgICAgICAgIGNvcnI9bVt1cHBlci50cmkobSldKQ0KDQpjZGY9c3Vic2V0KGNkZixhYnMoY29ycik+MC41KQ0KDQpjZGYkQ2xvc2U9Y2RmJGNvcnI+PTAuNw0KbmFtZXMoY2RmKT1jKCJmcm9tIiwidG8iLCJjb3JyIiwiQ2xvc2UiKQ0KDQoNCmdyYXBoPC1ncmFwaF9mcm9tX2RhdGFfZnJhbWUoY2RmKQ0KDQpnZ3JhcGgoZ3JhcGgsIGxheW91dCA9ICdmcicpK2dlb21fZWRnZV9kZW5zaXR5KGFlcyhmaWxsID0gQ2xvc2UpKStnZW9tX25vZGVfcG9pbnQoKStnZW9tX25vZGVfdGV4dChhZXMobGFiZWwgPSBuYW1lKSxudWRnZV94PSxudWRnZV95PTAuMSxjb2xvcj0iZ3JleSIpK2dlb21fZWRnZV9saW5rKGFlcyhjb2xvdXIgPSBjb3JyKSx3aWR0aD0xLGFscGhhPTAuNSkrdGhlbWVfYmFyZSgpK3NjYWxlX2VkZ2VfZmlsbF9tYW51YWwodmFsdWVzPWMoIndoaXRlIiwiI2ZmZGYxNCIpKStzY2FsZV9lZGdlX2NvbG9yX2dyYWRpZW50KGhpZ2g9IiNmZjAwM2YiLGxvdz0iIzAwOTRmZiIpDQoNCmBgYA0KDQohW10oY21hdC5wbmcpDQoNClRvIHRoaXMgcG9pbnQsIHdlIGdvdCBhIHNtYWxsZXIgZGF0YXNldCBvZiAyNDggdmFyaWFibGVzLCB3ZSB3aWxsIHNwbGl0IHRoaXMgaW50byB0d28gc3Vic2V0cywgb25lIGZvciB0cmFpbmluZyBhbmQgYW5vdGhlciBmb3IgdGVzdGluZzoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHJlc3VsdHM9ImhpZGUifQ0KDQpzZXQuc2VlZCgxMjM0KQ0KDQppZFRyYWluPWNhcmV0OjpjcmVhdGVEYXRhUGFydGl0aW9uKHk9ZGYkY2xhc3MscD0zNTEvNDUyLGxpc3Q9RkFMU0UpDQp0cmFpbnNldD1kZltpZFRyYWluLF0NCnRlc3RzZXQ9ZGZbLWlkVHJhaW4sXQ0KDQpzcDE9ZGYlPiVnZ3Bsb3QoYWVzKHg9Y2xhc3MsZmlsbD1jbGFzcykpK3N0YXRfY291bnQoY29sb3I9ImJsYWNrIixhbHBoYT0wLjcsc2hvdy5sZWdlbmQgPSBGKStzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiI2Y3MDI2MCIsIiMwNzllZWYiKSkrY29vcmRfZmxpcCgpK2dndGl0bGUoIk9yaWdpbiIpK2dlb21fdGV4dChzdGF0PSdjb3VudCcsYWVzKGxhYmVsPS4uY291bnQuLiksaGp1c3Q9NSxzaXplPTUsY29sb3I9IndoaXRlIikrdGhlbWVfYncoMTApDQoNCnNwMj10cmFpbnNldCU+JWdncGxvdChhZXMoeD1jbGFzcyxmaWxsPWNsYXNzKSkrc3RhdF9jb3VudChjb2xvcj0iYmxhY2siLGFscGhhPTAuNyxzaG93LmxlZ2VuZCA9IEYpK3NjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCIjZjcwMjYwIiwiIzA3OWVlZiIpKStjb29yZF9mbGlwKCkrZ2d0aXRsZSgiVHJhaW4iKStnZW9tX3RleHQoc3RhdD0nY291bnQnLGFlcyhsYWJlbD0uLmNvdW50Li4pLGhqdXN0PTUsc2l6ZT01LGNvbG9yPSJ3aGl0ZSIpK3RoZW1lX2J3KDEwKQ0KDQpzcDM9dGVzdHNldCU+JWdncGxvdChhZXMoeD1jbGFzcyxmaWxsPWNsYXNzKSkrc3RhdF9jb3VudChjb2xvcj0iYmxhY2siLGFscGhhPTAuNyxzaG93LmxlZ2VuZCA9IEYpK3NjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCIjZjcwMjYwIiwiIzA3OWVlZiIpKStjb29yZF9mbGlwKCkrZ2d0aXRsZSgiVGVzdCIpK2dlb21fdGV4dChzdGF0PSdjb3VudCcsYWVzKGxhYmVsPS4uY291bnQuLiksaGp1c3Q9NSxzaXplPTUsY29sb3I9IndoaXRlIikrdGhlbWVfYncoMTApDQoNCmdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlKHNwMSxzcDIsc3AzLG5jb2w9MSkNCg0KDQpgYGANCg0KTm93IHdlIGJlZ2luIG91ciBtYWNoaW5lIGxlYXJuaW5nIGV4cGVyaW1lbnQuIEZpcnN0LCB3ZSBsb2FkIHRoZSBoMk8gcGFja2FnZSBhbmQgY29udmVydCB0cmFpbi90ZXN0IHN1YnNldHMgaW50byBoMk8gY2xhc3M6DQoNCiMjIEZlYXR1cmUgc2VsZWN0aW9uIHVzaW5nIHJlcGxpY2F0ZWQgUmFuZG9tIEZvcmVzdCANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHJlc3VsdHM9ImhpZGUifQ0KDQpsaWJyYXJ5KGgybykNCg0KaDJvLmluaXQobnRocmVhZHMgPSAtMSxtYXhfbWVtX3NpemUgPSI0ZyIpDQoNCnd0cmFpbj1hcy5oMm8odHJhaW5zZXQpDQp3dGVzdD1hcy5oMm8odGVzdHNldCkNCg0KZmVhdHVyZXM9c2V0ZGlmZihjb2xuYW1lcyh3dHJhaW4pLCJjbGFzcyIpDQoNCmBgYA0KDQpUaGUgZmlyc3Qgc29sdXRpb24gY29uc2lzdHMgb2YgcmVwbGljYXRpbmcgYSBiYXNpYyBSYW5kb20gRm9yZXN0IChSRikgYWxnb3JpdGhtIDEwMCB0aW1lcywgdGhlbiBleHRyYWN0aW5nIHRoZSBpbmZvcm1hdGlvbiBvZiB2YXJpYWJsZSBpbXBvcnRhbmNlcy4NCg0KTm90ZSB0aGF0IGVhY2ggUkYgbW9kZWwgaXMgdHJhaW5lZCB1c2luZyBhIGRpZmZlcmVudCBzZWVkIG51bWJlciwgaG93ZXZlciB0aGlzIHJhbmRvbW5lc3Mgd2lsbCBiZSByZXByb2R1Y2libGUgc28geW91IHdpbGwgZ2V0IHRoZSBzYW1lIHJlc3VsdCBhcyBnaXZlbiBoZXJlICh1bmxpa2UgdGhlIHJhbmRvbWZvcmVzdCBhbGdvcml0aG0gaW4gbWxyIHBhY2thZ2UpLg0KDQpMZXQncyBydW4gdGhlc2UgY29kZXMgYW5kIGRyaW5rIGEgY3VwIG9mIGNvZmZlZSBiZWNhdXNlIGl0IHdpbGwgdGFrZSBzb21lIHRpbWVzLi4uDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSxyZXN1bHRzPSJoaWRlIn0NCg0Kc2V0LnNlZWQoMTIzNDUpDQpyZnNlZWQ9c2FtcGxlKDEwMDAwLDEwMCxyZXBsYWNlPUYpDQoNCnJlcz1OVUxMDQpwZGY9TlVMTA0KDQpmb3IgKGkgaW4gMToxMDApew0KICANCiAgcmZtb2Q9aDJvLnJhbmRvbUZvcmVzdCh4ID0gZmVhdHVyZXMsDQogICAgICAgICAgICAgICAgICAgICAgICAgeSA9ICJjbGFzcyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5pbmdfZnJhbWUgPSB3dHJhaW4sbmZvbGRzPTEwLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGZvbGRfYXNzaWdubWVudCA9ICJTdHJhdGlmaWVkIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICBiYWxhbmNlX2NsYXNzZXMgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgIG50cmVlcyA9IDEwMCwgbWF4X2RlcHRoID0gNTAsbXRyaWVzID0gLTEsc2FtcGxlX3JhdGUgPSAwLjcsDQogICAgICAgICAgICAgICAgICAgICAgICAgc3RvcHBpbmdfbWV0cmljID0gIm1pc2NsYXNzaWZpY2F0aW9uIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ190b2xlcmFuY2UgPSAwLjAxLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX3JvdW5kcyA9IDMsDQogICAgICAgICAgICAgICAgICAgICAgICAga2VlcF9jcm9zc192YWxpZGF0aW9uX2ZvbGRfYXNzaWdubWVudCA9IEYsDQogICAgICAgICAgICAgICAgICAgICAgICAga2VlcF9jcm9zc192YWxpZGF0aW9uX3ByZWRpY3Rpb25zPUYsDQogICAgICAgICAgICAgICAgICAgICAgICAgc2NvcmVfZWFjaF9pdGVyYXRpb24gPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWQ9cmZzZWVkW2ldKQ0KICANCiAgcGRmdGVtcD1wcmVkaWN0KHJmbW9kLHd0ZXN0KSU+JWFzX3RpYmJsZSgpJT4lbXV0YXRlKC4sSWQ9cm93Lm5hbWVzKHRlc3RzZXQpLFRydXRoPXRlc3RzZXQkY2xhc3MsQWNjdXJhY3k9aWZlbHNlKHRlc3RzZXQkY2xhc3MgPT0uJHByZWRpY3QsICJDb3JyZWN0IiwgIldyb25nIiksTW9kZWw9aSkNCiAgDQogIHZpbXA9cmZtb2RAbW9kZWwkdmFyaWFibGVfaW1wb3J0YW5jZXMlPiVhc190aWJibGUoKSU+JW11dGF0ZSguLFNlZWQ9cmZzZWVkW2ldLE1vZGVsPWkpDQogIA0KICByZXM9cmJpbmQocmVzLHZpbXApDQogIA0KICBwZGY9cmJpbmQocGRmLHBkZnRlbXApDQoNCn0NCg0KcmVzPWFzX3RpYmJsZShyZXMpDQoNCnJlcyRNb2RlbD1hcy5mYWN0b3IocmVzJE1vZGVsKQ0KYGBgDQoNCkFzIHlvdSBjb3VsZCBzZWUgaW4gdGhlIGxvb3AsIHdlIGhhdmUgcGVyZm9ybWVkIDMgdGFza3Mgb24gZWFjaCBSRiBtb2RlbDogVHJhaW5pbmcgYnkgY3Jvc3N2YWxpZGF0aW9uLCBwcmVkaWN0aW5nIHRoZSBwcm9iYWJpbGl0eSBvZiBhcnJoeXRobWlhIGluIHRlc3Qgc3Vic2V0LCBhbmQgZXh0cmFjdGluZyB0aGUgdmFyaWFibGUgaW1wb3J0YW5jZSBtYXRyaXggb2YgdGhlIHRyYWluZWQgbW9kZWwuIEJ5IHJlcGxpY2F0aW5nIHRob3NlIHRhc2tzIDEwMCB0aW1lcyBmb3IgMTAwIGRpZmZlcmVudCB2ZXJzaW9ucyBvZiBSRiBtb2RlbHMsIHdlIGZpbmFsbHkgZ290IDIgbGFyZ2UgZGF0YWZyYW1lczogKDEpICJwZGYiIGNvbnRhaW5zIGluZm9ybWF0aW9uIG9mIGFjY3VyYWN5IG9mIDEwMCBwcmVkaWN0aW9ucyBpbiB0ZXN0c2V0IGFuZCAoMikgInJlcyIiIGRhdGFmcmFtZSB0aGF0IGNvbnRhaW5zIGluZm9ybWF0aW9uIG9mIGFyaWFibGUgaW1wb3J0YW5jZXMuDQoNCkZpcnN0LCB3ZSB3aWxsIGV4cGxvcmUgdGhlIHBkZiBkYXRhZnJhbWUsIGFzIGJlZm9yZSBleHBsb3JpbmcgdGhlIHZhcmlhYmxlIGltcG9ydGFuY2VzIHdlIHdhbnQgdG8ga25vdyBob3cgZ29vZCAxMDAgUkYgbW9kZWwgcGVyZm9ybWVkIGlmIHRoZSBpbnB1dCBjb25zaXN0cyBvZiBhbGwgMjQ4IGZlYXR1cmVzLiBJZiB3ZSBkb250IGNvbnNpZGVyIGEgZmVhdHVyZSBmaWx0cmF0aW9uIGlzIHRoZXJlIGEgY29uc2VxdWVuY2Ugb24gdGhlIG1vZGVsJ3MgcGVyZm9ybWFuY2UgPw0KDQpgYGB7cn0NCnBkZiRJZD1hcy5mYWN0b3IocGRmJElkKQ0KDQpwZGYlPiVnZ3Bsb3QoYWVzKHg9TW9kZWwseT1yZW9yZGVyKElkLGFycmh5dGhtaWEpLGZpbGw9YXJyaHl0aG1pYSkpK2dlb21fdGlsZSgpK3RoZW1lX2J3KCkrc2NhbGVfZmlsbF9ncmFkaWVudDIobG93PSJncmVlbiIsbWlkPSJnb2xkIixoaWdoPSJyZWQiLG1pZHBvaW50ID0gMC41KStmYWNldF93cmFwKH5UcnV0aCxzaHJpbms9VFJVRSxzY2FsZT0iZnJlZSIsbmNvbD0xKSt0aGVtZV9idyg4KSt0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9NDUsIGhqdXN0ID0gMSkpDQoNCmBgYA0KDQpUaGUgZmlyc3QgaGVhdG1hcCBwbG90IHJlcHJlc2VudHMgdGhlIHByZWRpY3RlZCBwcm9iYWJpbGl0eSBvZiBhcnJoeXRobWlhIGJ5IDEwMCBSRiBtb2RlbHMgKGhvcml6b250YWwgYXhpcykgZm9yIGVhY2ggY2FzZSBhbW9uZyAxMDAgcGF0aWVudHMgKHZlcnRpY2FsIGF4aXMpLCB3aGVuIGFsbCAyNDggZmVhdHVyZXMgaGF2ZSBiZWVuIHVzZWQgYXMgaW5wdXQuIEEgZ29vZCBtb2RlbCB3b3VsZCBnaXZlIGhpZ2ggcHJvYmFiaWxpdHkgZm9yIHBhdGllbnRzIHdpdGggdHJ1ZSBhcnJoeXRobWlhIChvcmFuZ2Ugb3IgcmVkIGNvbG9ycykgYW5kIGxvd2VyIHByb2JhYmlsaXR5IGluIGhlYWx0aHkgc3ViamVjdHMgKGdyZWVuIGNvbG9yKS4gVGhlIHByZXNlbmNlIG9mIGdyZWVuIGNvbG9yIGluIEFycmh5dGhtaWMgZ3JvdXAgaW5kaWNhdGVzIGEgZmFsc2UgbmVnYXRpdmUsIHdoaWxlIHRoZSBwcmVzZW5jZSBvZiByZWQgY29sb3IgaW4gSGVhbHRoeSBncm91cCBtZWFucyBmYWxzZSBwb3NpdGl2ZS4gQm90aCBvZiB0aGVtIGFyZSB3cm9uZyBhbmQgYWZmZWN0IHRoZSBiYWxhbmNlZCBhY2N1cmFjeSBvZiB0aGUgbW9kZWwuDQoNCk92ZXJhbGwsIFdlIGNhbiBzZWUgdGhhdCB0aGUgUmFuZG9tIEZvcmVzdCBhbGdvcml0aG0gaGFuZGxlZCB3ZWxsIHRoZSBwcm9ibGVtIHdpdGhvdXQgYW55IGZlYXR1cmUgZmlsdHJhdGlvbi4gSW4gZmFjdCwgdGhlIHJhbmRvbW5lc3MgaW4gUkYgYWxnb3JpdGhtIGFscmVhZHkgcmVtb3ZlZCB0aGUgaXJyZWxldmFudCBmZWF0dXJlcy4gU3VjaCBwZXJmb3JtYW5jZSBpcyBjb25zaXN0ZW50IHRocm91Z2ggMTAwIGRpZmZlcmVudCB2ZXJzaW9ucywgc28gd2UgY2FuIHRydXN0IHRoZSB2YXJpYWJsZSBpbXBvcnRhbmNlIHJlc3VsdHMgZ2l2ZW4gYnkgdGhlc2UgbW9kZWxzLiBIb3dldmVyLCB0aGVyZSBhcmUgc3RpbGwgZmFsc2UgcG9zaXRpdmUgYW5kIGZhbHNlIG5lZ2F0aXZlIHByZWRpY3Rpb25zLg0KDQpUaGUgYWNjdXJhY3kgb2YgMTAwIFJGIG1vZGVscyBjYW4gYWxzbyBiZSBldmFsdWF0ZWQgcXVhbGl0YXRpdmVseSwgYXMgZm9sbG93czoNCg0KYGBge3J9DQoNCnBkZiU+JWdncGxvdChhZXMoeD1Nb2RlbCx5PXJlb3JkZXIoSWQsMS1hcnJoeXRobWlhKSxmaWxsPUFjY3VyYWN5KSkrZ2VvbV90aWxlKCkrdGhlbWVfYncoKStzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9Yygic2t5Ymx1ZSIsInJlZCIpKSt0aGVtZV9idyg4KSt0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9NDUsIGhqdXN0ID0gMSkpK2ZhY2V0X3dyYXAoflRydXRoLHNocmluaz1UUlVFLHNjYWxlPSJmcmVlIixuY29sPTEpK3NjYWxlX3lfZGlzY3JldGUoIklkeCIpDQpgYGANCg0KV2Ugd2lsbCBydW4gYSBzZWNvbmQgbG9vcCBmb3IgZXN0aW1hdGluZyB0aGUgYXZlcmFnZWQgcHJlZGljdGVkIHByb2JhYmlsaXR5IGFuZCBhdmVyYWdlZCBhY2N1cmFjeSBvZiAxMDAgbW9kZWxzOg0KDQpgYGB7cn0NCnBkZjI9TlVMTA0KDQpmb3IgKGkgaW4gMToxMDApew0KICAgSWR0PXBkZiRJZFtpXQ0KICAgZHRlbXA9c3Vic2V0KHBkZixJZD09SWR0KQ0KICAgc3VtPUhtaXNjOjpkZXNjcmliZShkdGVtcCkNCiAgIA0KICAgcHJlZHByb2I9c3VtJGFycmh5dGhtaWEkY291bnRzW1s1XV0NCiAgIGFjYz1zdW0kQWNjdXJhY3kkdmFsdWVzW1syXV1bMV0vMTAwDQogICB3cm9uZz1zdW0kQWNjdXJhY3kkdmFsdWVzW1syXV1bMl0vMTAwDQoNCiAgIHBkZnRlbXA9Y2JpbmQuZGF0YS5mcmFtZShJZD1JZHQscHJlZHByb2IsYWNjLHdyb25nKQ0KICAgcGRmMj1yYmluZChwZGYyLHBkZnRlbXApDQp9DQoNCmBgYA0KDQpUaGUgMiBmb2xsb3dpbmcgZ3JhcGhzIGRpc3BsYXkgdGhlIG1lZGlhbiBvZiBwcmVkaWN0ZWQgcHJvYmFiaWxpdHkgYW5kIHBlcmNlbnRhZ2Ugb2YgYWNjdXJhdGUvd3JvbmcgcHJlZGljdGlvbnMgd2hlbiAxMDAgbW9kZWxzIHdlcmUgYXBwbGllZCB0byB0aGUgdGVzdCBzdWJzZXQgKHlvdSBjYW4gZmVlbCBpdCBsaWtlcyByZWFkaW5nIGEgY29uZnVzaW9uIG1hdHJpeCkuDQoNCmBgYHtyfQ0KcGRmMiRjbGFzcz10ZXN0c2V0JGNsYXNzDQpwZGYyJHByZWRwcm9iPWFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHBkZjIkcHJlZHByb2IpKQ0KDQpwZGYyJElkPWFzLmZhY3RvcihwZGYyJElkKQ0KDQpwZGYyJHdyb25nW2lzLm5hKHBkZjIkd3JvbmcpXSA8LSAwDQoNCg0KcGRmMiU+JWdncGxvdChhZXMoeD1yZW9yZGVyKElkLHByZWRwcm9iKSx5PXByZWRwcm9iLGZpbGw9cHJlZHByb2IpKStnZW9tX2JhcihhbHBoYT0wLjcsc3RhdD0iaWRlbnRpdHkiKSt0aGVtZV9idyg4KStzY2FsZV9maWxsX2dyYWRpZW50Mihsb3c9ImdvbGQiLG1pZD0icmVkIixoaWdoPSJwdXJwbGUiLG1pZHBvaW50ID0gMC40KStmYWNldF93cmFwKH5jbGFzcyxzaHJpbmsgPSBUUlVFLG5jb2w9MSxzY2FsZXMgPSAiZnJlZSIpK2dlb21faGxpbmUoeWludGVyY2VwdCA9IDAuNSxsaW5ldHlwZT0yLHNpemU9MSkNCg0KcGRmMiU+JWdhdGhlcih3cm9uZyxhY2Msa2V5PSJBY2N1cmFjeSIsdmFsdWU9IlByZWRpY3Rpb24iKSU+JWdncGxvdChhZXMoeD1yZW9yZGVyKElkLC1wcmVkcHJvYikseT1QcmVkaWN0aW9uLGZpbGw9QWNjdXJhY3kpKStnZW9tX2JhcihhbHBoYT0wLjcscG9zaXRpb249ImZpbGwiLHN0YXQ9IklkZW50aXR5IikrdGhlbWVfYncoOCkrZmFjZXRfd3JhcCh+Y2xhc3Msc2hyaW5rID0gVFJVRSxuY29sPTEsc2NhbGVzID0gImZyZWUiKStzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9Yygic2t5Ymx1ZSIsInJlZCIpKStzY2FsZV94X2Rpc2NyZXRlKCJJZCIpDQoNCmBgYA0KDQpTbywgd2UgY2FuIHRydXN0IG91ciAxMDAgUkYgbW9kZWxzIGluIHRlcm1zIG9mIHByZWRpY3Rpb24sIG5vdyB3ZSB3aWxsIHNlZSB3aGF0IGluZm9ybWF0aW9uIHRoZXkgZ2l2ZSB1cyBhYm91dCB0aGUgaW1wb3J0YW5jZXMgb2YgMjQ4IGZlYXR1cmVzOg0KDQpUaGUgZm9sbG93aW5nIGhlYXRtYXAgcGxvdHMgaW5kaWNhdGUgdGhlIHJlbGF0aXZlIGltcG9ydGFuY2UgYW5kIHNjYWxlZCBpbXBvcnRhbmNlIHNjb3JlcywgcmVzcGVjdGl2ZWx5LiBNb3JlIGltcG9ydGFudCBhIGZlYXR1cmUgaXMsIG1vcmUgc2F0dXJhdGVkIGluIGNvbG9yIHRoYXQgZmVhdHVyZSB3b3VsZCBiZS4gQmFzZWQgb24gdGhvc2UgZ3JhcGhzLCB3ZSBjYW4gbWFrZSBhIGNvbmNsdXNpb24gdGhhdCB1cCB0byA4My8yNDggZmVhdHVyZXMgKDEvMykgY291bGQgYmUgcmVtb3ZlZCwgYXMgdGhleSBkaWQgbm90IGNvbnRyaWJ1dGUgdG8gdGhlIHByZWRpY3Rpb24uIFdlIG9ubHkgbmVlZCBhYm91dCAxNTAgdG8gMTYwIGZlYXR1cmVzIHRvIGJ1aWxkIGEgZ29vZCBSYW5kb20gRm9yZXN0IG1vZGVsLg0KDQpgYGB7cn0NCnJlcyU+JWdncGxvdChhZXMoeD1yZW9yZGVyKE1vZGVsLHJlbGF0aXZlX2ltcG9ydGFuY2UpLHk9cmVvcmRlcih2YXJpYWJsZSwtcmVsYXRpdmVfaW1wb3J0YW5jZSksZmlsbD1yZWxhdGl2ZV9pbXBvcnRhbmNlLGNvbG9yPXJlbGF0aXZlX2ltcG9ydGFuY2UpKStnZW9tX3RpbGUoc2hvdy5sZWdlbmQgPSBGKSt0aGVtZV9idygpK3NjYWxlX2ZpbGxfZ3JhZGllbnQyKGxvdz0id2hpdGUiLG1pZD0icmVkIixoaWdoPSJwdXJwbGUiLG1pZHBvaW50ID0gNTApK3NjYWxlX2NvbG9yX2dyYWRpZW50Mihsb3c9IndoaXRlIixtaWQ9InJlZCIsaGlnaD0icHVycGxlIixtaWRwb2ludCA9IDUwKSt0aGVtZV9idyg1KSt0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9NDUsIGhqdXN0ID0gMSkpK3NjYWxlX3hfZGlzY3JldGUoIk1vZGVscyIpK3NjYWxlX3lfZGlzY3JldGUoIkZlYXR1cmVzIikNCg0KcmVzJT4lZ2dwbG90KGFlcyh4PXJlb3JkZXIoTW9kZWwsc2NhbGVkX2ltcG9ydGFuY2UpLHk9cmVvcmRlcih2YXJpYWJsZSwtc2NhbGVkX2ltcG9ydGFuY2UpLGZpbGw9c2NhbGVkX2ltcG9ydGFuY2UsY29sb3I9c2NhbGVkX2ltcG9ydGFuY2UpKStnZW9tX3RpbGUoc2hvdy5sZWdlbmQgPSBGKSt0aGVtZV9idygpK3NjYWxlX2ZpbGxfZ3JhZGllbnQyKGxvdz0id2hpdGUiLG1pZD0iZ29sZCIsaGlnaD0icmVkIixtaWRwb2ludCA9IDAuNSkrc2NhbGVfY29sb3JfZ3JhZGllbnQyKGxvdz0id2hpdGUiLG1pZD0iZ29sZCIsaGlnaD0icmVkIixtaWRwb2ludCA9MC41KSt0aGVtZV9idyg1KSt0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9NDUsIGhqdXN0ID0gMSkpK3NjYWxlX3hfZGlzY3JldGUoIk1vZGVscyIpK3NjYWxlX3lfZGlzY3JldGUoIkZlYXR1cmVzIikNCg0KDQpgYGANCg0KQXMgYWJvdmUsIHdlIHdpbGwgYWxzbyBydW4gYW5vdGhlciBsb29wIHRvIGV4dHJhY3QgdGhlIGF2ZXJhZ2VkIGltcG9ydGFuY2Ugc2NvcmUgZm9yIGVhY2ggZmVhdHVyZXM6DQoNCmBgYHtyfQ0KcmVzMj1OVUxMDQoNCmZvciAoaSBpbiAxOjI1Myl7DQogIGR0ZW1wPXN1YnNldChyZXMsdmFyaWFibGU9PWZlYXR1cmVzW2ldKQ0KICBzdW09cHN5Y2g6OmRlc2NyaWJlKGR0ZW1wKQ0KICByZWk9c3VtW1s1XV1bMl0NCiAgc2NpPXN1bVtbNV1dWzNdDQogIHBjdD1zdW1bWzVdXVs0XQ0KDQogIHJ0ZW1wPWNiaW5kLmRhdGEuZnJhbWUocmVpLHNjaSxwY3QsdmFyPWR0ZW1wW1sxXV1bMV0pDQogIHJlczI9cmJpbmQocmVzMixydGVtcCkNCiAgDQp9DQpgYGANCg0KYGBge3J9DQpyZXMyJT4lZ2dwbG90KGFlcyh4PXJlb3JkZXIodmFyLHJlaSkseT1yZWksZmlsbD1yZWksY29sb3I9cmVpKSkrZ2VvbV9zZWdtZW50KGFlcyh4PXJlb3JkZXIodmFyLHJlaSkseGVuZD12YXIseT0wLHllbmQ9cmVpKSxzaXplPTEsc2hvdy5sZWdlbmQgPSBGKStnZW9tX3BvaW50KHNpemU9MSxzaG93LmxlZ2VuZCA9IEYpK3NjYWxlX3hfZGlzY3JldGUoIkZlYXR1cmVzIikrc2NhbGVfeV9jb250aW51b3VzKCJSZWxhdGl2ZSBpbXBvcnRhbmNlIikrY29vcmRfZmxpcCgpK3NjYWxlX2ZpbGxfZ3JhZGllbnQobG93PSJibHVlIixoaWdoPSJyZWQiKStzY2FsZV9jb2xvcl9ncmFkaWVudChsb3c9ImJsdWUiLGhpZ2g9InJlZCIpK2dndGl0bGUoIlJlbGF0aXZlIGltcG9ydGFuY2UiKSt0aGVtZV9idyg1KQ0KDQpgYGANCg0KV2UgZGVjaWRlZCB0byBrZWVwIG9ubHkgMTUwIHN0cm9uZ2VzdCB2YXJpYWJsZXMgaW4gb3VyIGlucHV0Og0KDQpgYGB7cn0NCnJlczM9cmVzMiU+JWFycmFuZ2UoZGVzYyhyZWkpKSU+JS5bYygxOjE1MCksXQ0KDQpyZXMzJT4lZ2dwbG90KGFlcyh4PXJlb3JkZXIodmFyLHJlaSkseT1yZWksZmlsbD1yZWksY29sb3I9cmVpKSkrZ2VvbV9zZWdtZW50KGFlcyh4PXJlb3JkZXIodmFyLHJlaSkseGVuZD12YXIseT0wLHllbmQ9cmVpKSxzaXplPTEsc2hvdy5sZWdlbmQgPSBGKStnZW9tX3BvaW50KHNpemU9MSxzaG93LmxlZ2VuZCA9IEYpK3NjYWxlX3hfZGlzY3JldGUoIkZlYXR1cmVzIikrc2NhbGVfeV9jb250aW51b3VzKCJSZWxhdGl2ZSBpbXBvcnRhbmNlIikrY29vcmRfZmxpcCgpK3NjYWxlX2ZpbGxfZ3JhZGllbnQobG93PSJibHVlIixoaWdoPSJyZWQiKStzY2FsZV9jb2xvcl9ncmFkaWVudChsb3c9ImJsdWUiLGhpZ2g9InJlZCIpK2dndGl0bGUoIlJlbGF0aXZlIGltcG9ydGFuY2UiKSt0aGVtZV9idyg1KQ0KDQpgYGANCg0KQmFzZWQgb24gYSBuZXcgaW5wdXQgdGhhdCBjb250YWlucyBvbmx5IDE1MCBzdHJvbmdlc3QgZmVhdHVyZXMsIHdlIHdpbGwgdHJhaW4gYW5vdGhlciBtb2RlbCAoTm90ZSB0aGF0IHRoZSBzZWVkIG51bWJlciBpcyByZXNldCB0byAzMzMpDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSxyZXN1bHRzPSJoaWRlIn0NCmZlYXR1cmVzMj1hcy52ZWN0b3IocmVzMyR2YXIpDQoNCnJmbW9kPWgyby5yYW5kb21Gb3Jlc3QoeCA9IGZlYXR1cmVzMiwNCiAgICAgICAgICAgICAgICAgICAgICAgeSA9ICJjbGFzcyIsDQogICAgICAgICAgICAgICAgICAgICAgIHRyYWluaW5nX2ZyYW1lID0gd3RyYWluLG5mb2xkcz0xMCwNCiAgICAgICAgICAgICAgICAgICAgICAgZm9sZF9hc3NpZ25tZW50ID0gIlN0cmF0aWZpZWQiLA0KICAgICAgICAgICAgICAgICAgICAgICBiYWxhbmNlX2NsYXNzZXMgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICBudHJlZXMgPSAxMDAsIG1heF9kZXB0aCA9IDUwLG10cmllcyA9IC0xLHNhbXBsZV9yYXRlID0gMC44LA0KICAgICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ19tZXRyaWMgPSAibWlzY2xhc3NpZmljYXRpb24iLA0KICAgICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ190b2xlcmFuY2UgPSAwLjAxLA0KICAgICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ19yb3VuZHMgPSAzLA0KICAgICAgICAgICAgICAgICAgICAgICBrZWVwX2Nyb3NzX3ZhbGlkYXRpb25fZm9sZF9hc3NpZ25tZW50ID0gRiwNCiAgICAgICAgICAgICAgICAgICAgICAga2VlcF9jcm9zc192YWxpZGF0aW9uX3ByZWRpY3Rpb25zPUYsDQogICAgICAgICAgICAgICAgICAgICAgIHNjb3JlX2VhY2hfaXRlcmF0aW9uID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgc2VlZD0zMzMpDQoNCmBgYA0KDQojIyBMb2dpc3RpYyBtb2RlbHMNCg0KV2UgaGF2ZSBmaW5pc2ggb3VyIGZpcnN0IHRhc2sgLSBidWlsZGluZyBhIFJhbmRvbSBGb3Jlc3QgbW9kZWwgZm9yIG91ciBjbGFzc2lmaWNhdGlvbiBwcm9ibGVtLiBOb3csIHdlIHdpbGwgY29uc2lkZXIgYW5vdGhlciAzIHNvbHV0aW9ucyB0aGF0IGJhc2VkIG9uIExvZ2lzdGljIHJlZ3Jlc3Npb24uIA0KDQpUaGUgZmlyc3Qgb25lIHdpbGwgYmUgTEFTU08gbG9naXN0aWMgbW9kZWwgKHNlZWQgPTU0NikNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHJlc3VsdHM9ImhpZGUifQ0KbGFzc29tb2Q9aDJvLmdsbSh4PWZlYXR1cmVzLA0KICAgICAgICAgICAgICAgeT0iY2xhc3MiLA0KICAgICAgICAgICAgICAgdHJhaW5pbmdfZnJhbWU9d3RyYWluLCANCiAgICAgICAgICAgICAgIG5mb2xkcyA9IDEwLHNlZWQgPSA1NDYsIA0KICAgICAgICAgICAgICAga2VlcF9jcm9zc192YWxpZGF0aW9uX3ByZWRpY3Rpb25zID0gRkFMU0UsDQogICAgICAgICAgICAgICBrZWVwX2Nyb3NzX3ZhbGlkYXRpb25fZm9sZF9hc3NpZ25tZW50ID0gRkFMU0UsIA0KICAgICAgICAgICAgICAgZm9sZF9hc3NpZ25tZW50ID0gIlN0cmF0aWZpZWQiLA0KICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIiwNCiAgICAgICAgICAgICAgIGFscGhhPTEsDQogICAgICAgICAgICAgICBsYW1iZGFfc2VhcmNoID0gVFJVRSxubGFtYmRhcyA9MjAsDQogICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZyA9IFRSVUUsDQogICAgICAgICAgICAgICBzdGFuZGFyZGl6ZSA9IFRSVUUsDQogICAgICAgICAgICAgICBtaXNzaW5nX3ZhbHVlc19oYW5kbGluZyA9IlNraXAiLA0KICAgICAgICAgICAgICAgY29tcHV0ZV9wX3ZhbHVlcyA9IEZBTFNFLCANCiAgICAgICAgICAgICAgIHJlbW92ZV9jb2xsaW5lYXJfY29sdW1ucyA9IEZBTFNFLA0KICAgICAgICAgICAgICAgaW50ZXJjZXB0ID0gVFJVRSwgDQogICAgICAgICAgICAgICBub25fbmVnYXRpdmUgPSBGQUxTRQ0KKQ0KYGBgDQoNCkhlcmUgYXJlIHRoZSBjb2VmZmljaWVudHMgb2Ygc2VsZWN0ZWQgZmVhdHVyZXMgZm9yIHRoaXMgTGFzc28gbG9naXN0aWMgbW9kZWwsIG5vdGUgdGhhdCB0aGUgTGFzc28gcmVndWxhcmlzYXRpb24gaGFzIGF1dG9tYXRpY2FsbHkgcmVtb3ZlZCBhbGwgdGhlIGZlYXR1cmVzIHRoYXQgd2VyZSBmb3VuZCB0byBiZSBub3QgaW1wb3J0YW50LiBUaGUgZmluYWwgbG9naXN0aWMgbW9kZWwgb25seSBjb250YWlucyAyNSBmZWF0dXJlcy4NCg0KYGBge3J9DQpjb2Vmcz1sYXNzb21vZEBtb2RlbCRjb2VmZmljaWVudHNfdGFibGUlPiVhcy50aWJibGUoKSU+JXN1YnNldCguLGNvZWZmaWNpZW50cyE9MCkNCg0KY29lZnMNCmBgYA0KDQpUaGUgc2Vjb25kIHNvbHV0aW9uIGlzIFJpZGdlIHJlZ3VsYXJpc2F0aW9uIChzZWVkPTk5OSkuIFRoaXMgY291bGQgYWxzbyBiZSB0cmFpbmVkIGluIGgyTy4gVW5saWtlcyBMQVNTTywgUmlkZ2UgcmVndWxhcmlzYXRpb24gb25seSBzaHJpbmsgdGhlIGNvZWZmaWNpZW50cyBjbG9zZSB0byB6ZXJvLCBidXQgbm90IHplcm8sIHNvIGJhc2ljYWxseSB0aGUgUmlkZ2UgbW9kZWwgY2Fubm90IGJlIHVzZWQgZm9yIGZlYXR1cmUgc2VsZWN0aW9uLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0UscmVzdWx0cz0iaGlkZSJ9DQpyaWRnZW1vZD1oMm8uZ2xtKHg9ZmVhdHVyZXMsDQogICAgICAgICAgICAgICAgIHk9ImNsYXNzIiwNCiAgICAgICAgICAgICAgICAgdHJhaW5pbmdfZnJhbWU9d3RyYWluLCANCiAgICAgICAgICAgICAgICAgbmZvbGRzID0gMTAsc2VlZCA9IDk5OSwgDQogICAgICAgICAgICAgICAgIGtlZXBfY3Jvc3NfdmFsaWRhdGlvbl9wcmVkaWN0aW9ucyA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICBrZWVwX2Nyb3NzX3ZhbGlkYXRpb25fZm9sZF9hc3NpZ25tZW50ID0gRkFMU0UsIA0KICAgICAgICAgICAgICAgICBmb2xkX2Fzc2lnbm1lbnQgPSAiU3RyYXRpZmllZCIsDQogICAgICAgICAgICAgICAgIGZhbWlseSA9ICJiaW5vbWlhbCIsDQogICAgICAgICAgICAgICAgIGFscGhhPTAsDQogICAgICAgICAgICAgICAgIGxhbWJkYV9zZWFyY2ggPSBUUlVFLG5sYW1iZGFzID0yMCwNCiAgICAgICAgICAgICAgICAgZWFybHlfc3RvcHBpbmcgPSBUUlVFLA0KICAgICAgICAgICAgICAgICBzdGFuZGFyZGl6ZSA9IFRSVUUsDQogICAgICAgICAgICAgICAgIG1pc3NpbmdfdmFsdWVzX2hhbmRsaW5nID0iU2tpcCIsDQogICAgICAgICAgICAgICAgIGNvbXB1dGVfcF92YWx1ZXMgPSBGQUxTRSwgDQogICAgICAgICAgICAgICAgIHJlbW92ZV9jb2xsaW5lYXJfY29sdW1ucyA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICBpbnRlcmNlcHQgPSBUUlVFLCANCiAgICAgICAgICAgICAgICAgbm9uX25lZ2F0aXZlID0gRkFMU0UNCikNCmBgYA0KDQpUaGUgbGFzdCBzb2x1dGlvbiB3aWxsIGJlIEVsYXN0aWMgbmV0IG1vZGVsIChzZWVkID01NTY2KS4gVGhlIHRyYWluaW5nIG9mIHRoaXMgbW9kZWwgaXMgZG9uZSB1c2luZyBnbG1uZXQgcGFja2FnZS4gTm90ZSB0aGF0IHRoaXMgcGFja2FnZSByZXF1aXJlcyB0cmFuc2Zvcm1pbmcgaW5wdXQgZGF0YSB0byBtYXRyaXgsIGFuZCBpdCBkb2VzIG5vdCBhY2NlcHQgbWlzc2luZyB2YWx1ZXMuDQoNCmBgYHtyfQ0KI0VsYXN0aWMgbmV0DQoNCmxpYnJhcnkoZ2xtbmV0KQ0KDQpzZXQuc2VlZCg1NTY2KQ0KDQp0cmFpbnNldGVuZXQ9dHJhaW5zZXQlPiVuYS5vbWl0KCkNCnRlc3RzZXRlbmV0PXRlc3RzZXQlPiVuYS5vbWl0KCkNCg0KdHJhaW5zZXRlbmV0Wy0xXTwtbGFwcGx5KHRyYWluc2V0ZW5ldFstMV0sIGFzLm51bWVyaWMpDQp0ZXN0c2V0ZW5ldFstMV08LWxhcHBseSh0ZXN0c2V0ZW5ldFstMV0sIGFzLm51bWVyaWMpDQoNCng9YXMubWF0cml4KHRyYWluc2V0ZW5ldFssLTFdKQ0KeT10cmFpbnNldGVuZXRbLDFdDQoNCmVuZXQgPSBjdi5nbG1uZXQoeCx5LGZhbWlseSA9ICJiaW5vbWlhbCIsdHlwZS5tZWFzdXJlID0gImNsYXNzIikNCg0KcGxvdChlbmV0KQ0KYGBgDQoNClRoZSBlbGFzdGljIG5ldCByZWd1bGFyaXNhdGlvbiBpcyBhIGh5YnJpZCBtZXRob2QgdGhhdCBjb21iaW5lcyBib3RoIGwxIGFuZCBsMiByZWd1bGFyaXNhdGlvbnMgKExhc3NvIGFuZCBSaWRnZSkuIFRoZSByZXN1bHRzIHNob3dlZCB0aGF0IEVsYXN0aWMgbmV0IG1vZGVsIG9ubHkga2VwdCAzMCBmZWF0dXJlcw0KDQpgYGB7cn0NCg0KY29lZihlbmV0KSU+JWFzLm1hdHJpeCgpJT4lYXMuZGF0YS5mcmFtZSgpJT4lc3Vic2V0KC4sYDFgIT0wKQ0KDQoNCmBgYA0KDQojIyBWYWxpZGF0aW9uIGFuZCBjb21wYXJhdGl2ZSBzdHVkeQ0KDQpGaW5hbGx5LCB3ZSBnb3QgNCBjYW5kaWRhdGVzIG9mIGNsYXNzaWZpZXJzLiBJbiBvcmRlciB0byBkZXRlcm1pbmUgd2hpY2ggaGFzIHRoZSBiZXN0IHBlcmZvcm1hbmNlLCB3ZSB3aWxsIHBlcmZvcm0gYSBjb21wYXJhdGl2ZSBzdHVkeS4gV2Ugd2lsbCBldmFsdWF0ZSAxNCBwZXJmb3JtYW5jZSBtZXRyaWNzIGZvciBlYWNoIG1vZGVsIHdoZW4gYXBwbGllZCB0byB0aGUgdGVzdCBzdWJzZXQsIHRoZW4gcGxvdCB0aGUgcmVzdWx0cyBvbiBhIHJhZGFyIGNoYXJ0IHRvIHZpc3VhbGl6ZSB0aGUgZGlmZmVyZW5jZS4NCg0KVGhlIHBlcmZvcm1hbmNlIG1ldHJpY3MgY2FuIGJlIGRpdmlkZWQgaW50byB0d28gZ3JvdXBzIDogdGhlIGZpcnN0IGdyb3VwIGhhcyBiZXN0IHZhbHVlIG9mIDEsIGluY2x1ZGluZzogQmFsYW5jZWQgYWNjdXJhY3koQkFDKSwgQXJlYSB1bmRlciBST0MgY3VydmUgKEFVQyksIEFjY3VyYWN5IChBQ0MpLCBGMSBzY29yZSwgUG9zaXRpdmUvTmVnYXRpdmUgcHJlZGljdGl2ZSB2YWx1ZSBhbmQgVHJ1ZSBuZWdhdGl2ZS9wb3NpdGl2ZSByYXRlcy4gVGhlc2UgbWV0cmljcyBpbmRpY2F0ZSB0aGUgYWNjdXJhY3ksIHNlbnNpdGl2aXR5IGFuZCBzcGVjaWZpY2l0eSBvZiB0aGUgbW9kZWwuIFRoZSBzZWNvbmQgZ3JvdXAgY29udGFpbnMgdGhlIG1ldHJpY3MgdGhhdCBoYXZlIGJlc3QgdmFsdWUgb2YgMCBhbmQgd29yc3QgdmFsdWUgb2YgMSwgaW5jbHVkaW5nIEJhbGFuY2UgZXJyb3IgcmF0ZSAoQkVSKSwgQnJpZXIgc2NvcmUsIEZhbHNlIG5lZ2F0aXZlL3Bvc2l0aXZlIHJhdGVzLCBNZWFuIG1pc2NsYXNzaWZpY2F0aW9uIGVycm9yIGFuZCBGYWxzZSBkaXNjb3ZlcnkgcmF0ZSAoRkRSKS4gVGhvc2Ugc2NvcmVzIG1lYXN1cmUgdGhlIHdlYWtuZXNzIG9mIGVhY2ggbW9kZWwuIEZvciBFQ0cgcmVhZGluZywgZmFsc2UgbmVnYXRpdmUgcHJlZGljdGlvbiBpcyBtb3JlIGRhbmdlcm91cyB0aGFuIGZhbHNlIHBvc2l0aXZlLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0UscmVzdWx0cz0iaGlkZSJ9DQpsaWJyYXJ5KG1scikNCg0KdGFzaz1tYWtlQ2xhc3NpZlRhc2soZGF0YT10ZXN0c2V0LHRhcmdldD0iY2xhc3MiKQ0KDQpkdW1teWxybmVyPW1ha2VMZWFybmVyKCJjbGFzc2lmLnJwYXJ0IixwcmVkaWN0LnR5cGUgPSJwcm9iIikNCmR1bW15bW9kPXRyYWluKGR1bW15bHJuZXIsdGFzaykNCmR1bW15cHJlZD1wcmVkaWN0KGR1bW15bW9kLHRhc2spDQoNCkxBU1NPUFJFRD1wcmVkaWN0KGxhc3NvbW9kLHd0ZXN0KSU+JWFzLmRhdGEuZnJhbWUoKQ0KTEFTU09QUkVEPUxBU1NPUFJFRCU+JW11dGF0ZSguLGlkPWMoMToxMDApLHRydXRoPXRlc3RzZXQkY2xhc3MscHJvYi5hcnJoeXRobWlhPS5bLDJdLHByb2IuaGVhbHRoeT0uWywzXSxyZXNwb25zZT0uJHByZWRpY3QpJT4lLlssLWMoMTozKV0NCg0KDQpSSURHRVBSRUQ9cHJlZGljdChyaWRnZW1vZCx3dGVzdCklPiVhcy5kYXRhLmZyYW1lKCkNClJJREdFUFJFRD1SSURHRVBSRUQlPiVtdXRhdGUoLixpZD1jKDE6MTAwKSx0cnV0aD10ZXN0c2V0JGNsYXNzLHByb2IuYXJyaHl0aG1pYT0uWywyXSxwcm9iLmhlYWx0aHk9LlssM10scmVzcG9uc2U9LiRwcmVkaWN0KSU+JS5bLC1jKDE6MyldDQoNCmVuZXRjbGFzcz1wcmVkaWN0KGVuZXQsYXMubWF0cml4KHRlc3RzZXRlbmV0WywtMV0pLHR5cGU9ImNsYXNzIiklPiVhcy5kYXRhLmZyYW1lKCkNCkVORVRQUkVEPXByZWRpY3QoZW5ldCxhcy5tYXRyaXgodGVzdHNldGVuZXRbLC0xXSksdHlwZT0icmVzcG9uc2UiKSU+JWFzLmRhdGEuZnJhbWUoKSU+JW11dGF0ZSguLGlkPWMoMTo5OCksdHJ1dGg9dGVzdHNldGVuZXQkY2xhc3MscHJvYi5hcnJoeXRobWlhPTEtLlssMV0scHJvYi5oZWFsdGh5PS5bLDFdLHJlc3BvbnNlPWVuZXRjbGFzc1ssMV0pJT4lLlssLTFdDQoNClJGUFJFRD1wcmVkaWN0KHJmbW9kLHd0ZXN0KSU+JWFzLmRhdGEuZnJhbWUoKQ0KUkZQUkVEPVJGUFJFRCU+JW11dGF0ZSguLGlkPWMoMToxMDApLHRydXRoPXRlc3RzZXQkY2xhc3MscHJvYi5hcnJoeXRobWlhPS5bLDJdLHByb2IuaGVhbHRoeT0uWywzXSxyZXNwb25zZT0uJHByZWRpY3QpJT4lLlssLWMoMTozKV0NCg0KYGBgDQoNCmBgYHtyfQ0KDQpwcmVkMT1kdW1teXByZWQNCnByZWQxJGRhdGE9TEFTU09QUkVEDQpwcmVkMj1kdW1teXByZWQNCnByZWQyJGRhdGE9RU5FVFBSRUQNCnByZWQzPWR1bW15cHJlZA0KcHJlZDMkZGF0YT1SRlBSRUQNCnByZWQ0PWR1bW15cHJlZA0KcHJlZDQkZGF0YT1SSURHRVBSRUQlPiVuYS5vbWl0KCkNCg0KbWVhc3VyZT1saXN0KG1scjo6YmFjLG1scjo6YXVjLG1scjo6YWNjLG1scjo6ZjEsbWxyOjpwcHYsbWxyOjpucHYsbWxyOjp0bnIsbWxyOjp0cHIpDQoNCnAxPXBlcmZvcm1hbmNlKHByZWQxLG1lYXN1cmUpDQpwMj1wZXJmb3JtYW5jZShwcmVkMixtZWFzdXJlKQ0KcDM9cGVyZm9ybWFuY2UocHJlZDMsbWVhc3VyZSkNCnA0PXBlcmZvcm1hbmNlKHByZWQ0LG1lYXN1cmUpDQoNCmxhc3NvPXAxJT4lYXMudmVjdG9yKCkNCmVuZXQ9cDIlPiVhcy52ZWN0b3IoKQ0KcmY9cDMlPiVhcy52ZWN0b3IoKQ0KcmRnPXA0JT4lYXMudmVjdG9yKCkNCg0KDQpgYGANCg0KDQpgYGB7cn0NCmxhYnM9YygiQkFDIiwiQVVDIiwiQUNDIiwiRjEiLCJQUFYiLCJOUFYiLCJUUFIiLCJUTlIiKQ0KDQpzY29yZXM9bGlzdCgiTGFzc28iPWxhc3NvKjEwLCJFbGFzdGljbmV0Ij1lbmV0KjEwLCJSYW5kb21Gb3Jlc3QiPXJmKjEwLCJSaWRnZSI9cmRnKjEwKQ0KDQoNCmxpYnJhcnkocmFkYXJjaGFydCkNCg0KY2hhcnRKU1JhZGFyKHNjb3JlcyA9IHNjb3JlcywgbGFicyA9IGxhYnMsIG1heFNjYWxlID0gMTApDQpgYGANCg0KYGBge3J9DQptZWFzdXJlMj1saXN0KGJlcixicmllcixmbnIsZnByLG1tY2UsZmRyKQ0KZTE9cGVyZm9ybWFuY2UocHJlZDEsbWVhc3VyZTIpDQplMj1wZXJmb3JtYW5jZShwcmVkMixtZWFzdXJlMikNCmUzPXBlcmZvcm1hbmNlKHByZWQzLG1lYXN1cmUyKQ0KZTQ9cGVyZm9ybWFuY2UocHJlZDQsbWVhc3VyZTIpDQoNCmxhc3NvPWUxJT4lYXMudmVjdG9yKCkNCmVuZXQ9ZTIlPiVhcy52ZWN0b3IoKQ0KcmY9ZTMlPiVhcy52ZWN0b3IoKQ0KcmRnPWU0JT4lYXMudmVjdG9yKCkNCg0KbGFicz1jKCJCRVIiLCJCUklFUiIsIkZOUiIsIkZQUiIsIk1NQ0UiLCJGRFIiKQ0KDQpzY29yZXM9bGlzdCgiTGFzc28iPWxhc3NvLCJFbGFzdGljbmV0Ij1lbmV0LCJSYW5kb21Gb3Jlc3QiPXJmLCJSaWRnZSI9cmRnKQ0KDQpjaGFydEpTUmFkYXIoc2NvcmVzID0gc2NvcmVzLCBsYWJzID0gbGFicywgbWF4U2NhbGUgPTAuOCkNCmBgYA0KDQpUaGUgcmVzdWx0cyBzdWdnZXN0IHRoYXQgdGhlIHBlcmZvcm1hbmNlcyB3YXMgYWxtb3N0IHNpbWlsYXIgZm9yIDQgbWV0aG9kcy4NCg0KIyMgQ29uY2x1c2lvbg0KDQpJbiB0aGlzIGNhc2Ugc3R1ZHksIHdlIGhhdmUgdHJhdmVsbGVkIGEgbG9uZyBqb3VybmV5IGZyb20gYSBtZXNzeSBkYXRhc2V0IHRvIHRoZSBmaW5hbCBjbGFzc2lmaWVyIHRoYXQgY291bGQgY29ycmVjdGx5IGRpc3Rpbmd1aXNoIHBhdGllbnRzIHdpdGggYXJyaHl0aG1pYSBmcm9tIGhlYWx0aHkgc3ViamVjdHMgaW4gNzguNiUgb2YgY2FzZXMuIEZvbGxvd2luZyBwb2ludHMgY291bGQgYmUga2VwdCBpbiBtaW5kOg0KDQoxKSBEYXRhIGV4cGxvcmF0aW9uIGFuZCB2aXN1YWxpc2F0aW9uIGlzIHZlcnkgaW1wb3J0YW50LCB0aGlzIHByb2NlZHVyZSBhbGxvd3MgdG8gZGV0ZWN0IHNvbWUgb2J2aW91cyBwcm9ibGVtIHdpdGggb3VyIGRhdGEsIGluY2x1ZGluZyB0aGUgbWlzc2luZyB2YWx1ZSwgaW52YXJpYW5jZSB2YXJpYWJsZXMsIG91dGxpZXJzLCBtaXNzaW5nIGxhYmVscy4uLiBhbmQgdGhlc2UgaW5mb3JtYXRpb24gY2FuIGFscmVhZHkgaGVscCB1cyB0byBlbGltaW5hdGUgdGhlIGlycmVsZXZhbnQgZmVhdHVyZXMgaW4gb3VyIGRhdGFzZXQsIHdpdGhvdXQgdXNpbmcgYW55IHNwZWNpYWwgdGVjaG5pcXVlLg0KDQoyKSBSYW5kb20gRm9yZXN0IGlzIGEgcG93ZXJmdWwgbWV0aG9kIGZvciBib3RoIGZlYXR1cmUgc2VsZWN0aW9uIGFuZCBvdmVyZml0dGluZyBjb250cm9sLiBEdWUgdG8gdGhlIHJhbmRvbW5lc3MgaW4gYm9vdHN0cmFwIGFnZ3JlZ2F0aW5nLCBvbmx5IG9uZSBtb2RlbCBjb3VsZCBiZSBub3QgdHJ1c3RmdWw7IGhvd2V2ZXIgYnkgcmVwbGljYXRpbmcgdGhlIG1vZGVsIHRyYWluaW5nIG1hbnkgdGltZXMsIHdlIGNhbiByZWFjaCBhIGhpZ2ggbGV2ZWwgb2YgY2VydGFpbnR5LiBJbiB0aGlzIHR1dG9yaWFsLCB3ZSBwcm92aWRlZCBhIHJlcHJvZHVjaWJsZSBsb29wIGZ1bmN0aW9uIGZvciB0aGlzIG1ldGhvZC4gU3VjaCBwcm9jZWR1cmUgYWxsb3dzIHRyYWluaW5nIDEwMCBvciBtb3JlIGJhc2ljIFJhbmRvbSBGb3Jlc3QgYWxnb3JpdGhtIHdpdGggYm90aCByYW5kb21uZXNzIGFuZCByZXByb2R1Y2liaWxpdHkuDQoNCjMpIExBU1NPLCBSaWRnZSBhbmQgRWxhc3RpY25ldCByZWd1bGFyaXNhdGlvbnMgYXJlIGFsc28gZ29vZCBmb3IgdGhlIGZlYXR1cmUgc2VsZWN0aW9uIHRhc2suIFRoZXNlIG1ldGhvZHMgZG9udCByZXF1aXJlIHRvbyBtdWNoIGNvbXB1dGF0aW9uIGNvc3QgYnV0IGNhbiBhbHNvIGdpdmUgYSBnb29kIHBlcmZvcm1hbmNlLiBMYXNzbyBhbmQgRWxhc3RpY25ldCBhbHNvIGtlZXAgc21hbGxlciBudW1iZXIgb2YgZmVhdHVyZXMgYW5kIGFzIHRoZXkgYXJlIGJhc2VkIG9uIGxvZ2lzdGljIHJlZ3Jlc3Npb24sIHRoZSBjb2VmZmljaWVudHMgb2YgdGhvc2UgdmFyaWFibGVzIGNvdWxkIGJlIGludGVycHJldGVkLCBub3QgbGlrZSB0aGUgUmFuZG9tIEZvcmVzdCB3aGljaCBpcyBhIEJsYWNrYm94Lg0KDQoNCioqVGhhbmsgeW91IGZvciBqb2luaW5nIHVzIGFuZCBzZWUgeW91IGluIHRoZSBuZXh0IHR1dG9yaWFsLioqDQo=