Using Data
We will do followings during our Machine learning process:
0. Prepare Environment
1. Import data
2. Data cleansing
3. Train Model
4. Predict
5. Analyse performance
0) Prepare Envirnoment
To do the analysis , we need to import related packages.
rm(list = ls(all = TRUE)) # Removing any exisiting variables
require(dplyr)
require(tidyr)
require(caret)
require(nnet)
require(pROC)
require(e1071)
require(rpart)
## For reproducible results we need to start with the same seed
set.seed(1333)
if(.Platform$OS.type =="windows")
{
setwd("D:/gitRepos/PerfAnalysis/")
}else {
setwd("~/gitRepos/PerfAnalysis/")
##library(doMC)
##registerDoMC(cores = 4)
}
source('myMultiClassSummary.R')
1) Import data
We first read the csv files: ML-MUTAG.csv and ML-GV-MUTAG.csv . Here GV is short for GraphVector and GS is short for GraphletSearch
df.input.GS<-read.csv2(file="./input/ML-MUTAG.csv",
sep=";",
header = TRUE,
dec = ".")
df.input.GV<-read.csv2(file="./input/ML-GV-MUTAG.csv",
sep=";",
header = TRUE,
dec = ".")
Lets see the dimension of each data frame:
Rows Columns
GS 188 47
GV 188 21
2) Data cleansing
- First we need to correct column data types in order our classification algorithms to work. So the GraphID and Class are of the type nominal (or in R called factor).
- Then we find the column with zero variance and remove it from dataset.
df.input.GV$GraphID <-as.factor(df.input.GV$GraphID );
df.input.GV$Class<-as.factor(make.names(df.input.GV$Class));
df.input.GS$GraphID <-as.factor(df.input.GS$GraphID );
df.input.GS$Class<-as.factor(make.names(df.input.GS$Class));
## Finding and removing columns with zero variance
nzv.GS<-nearZeroVar(df.input.GS)
nzv.GV<-nearZeroVar(df.input.GV)
Following columns/features have zero variance, which can be removed:
Rows Columns
GS 188 47
GV 188 21
Columns for GS to be removed: Components; d0; d1; d2; d3; d5; d6; d7; d8; d9; d10; d13; d14; d15; d16; d17; d20; d21; d22; d23; d25; d26; d27; d28; d29; d30; d31; d32; d33; d34; d35; d36; d37; d38; X
Columns for GV to be removed: Components; t.0; t.11; t.12; t.13; t.16; X
Now let see the shape of our datasets after removing zero variance
Rows Columns
GS 188 12
GV 188 14
Columns for GS: GraphID; Class; Verticies; Edges; AvgDegree; Density; d4; d11; d12; d18; d19; d24
Columns for GV: GraphID; Class; t.1; t.2; t.3; t.4; t.5; t.6; t.7; t.8; t.9; t.10; t.14; t.15
The dimensions corresponds to the following Graph type:
- Partitioning the datasets into train (70 % ) and test (30 %) for cross validation
##randomly partition data into two datasets ,training and testing
#GraphletSearch
inTrain.GS<-createDataPartition(y=df.input.GS$Class,p = .7,list=FALSE)
train.GS<-df.input.GS[inTrain.GS,]
test.GS<- df.input.GS[-inTrain.GS,]
#GraphVector
inTrain.GV<-createDataPartition(y=df.input.GV$Class,p = .7,list=FALSE)
train.GV<-df.input.GV[inTrain.GV,]
test.GV<- df.input.GV[-inTrain.GV,]
3) Train Model
Now we train following models:
* SVM
* SVM with Polynomial kernel
* SVM with Radial kernel
* Random forest
##Create a data frame from all combinations of the supplied parameters for Radial and linear Kernels
grid <- expand.grid(sigma = c(.01, .015, 0.2),
C = c(0.75, 0.9, 1, 1.1, 1.25))
##Create a data frame from all combinations of the supplied parameters for Polynomial kernel
grid.poly <- expand.grid(C = c(0.75, 0.9, 1, 1.1, 1.25),
scale=c(.0001),
degree=1:2)
#We need to check if it is binary classification
#or multiclass classifcation, which requires diffrent summary function.
if(length(unique(df.input.GS$Class)) > 2)
{
ctrl <-trainControl(method="repeatedcv",
repeats=10,
classProbs=TRUE,
summaryFunction=myMultiClassSummary)
} else
{
ctrl <- trainControl(method="repeatedcv",
repeats=10,
classProbs=TRUE,
summaryFunction=twoClassSummary)
}
## Parameters for Random Forest algorithm.
RF.control <- trainControl(method="repeatedcv", number=10, repeats=3)
Before training we will center and scale the features for better accuracy.
SVM linear
svmModel.linear.GS <- train(x=train.GS[,-c(1,2)],
y= train.GS[,2],
method = "svmLinear",
preProc = c("center","scale"),
trControl=ctrl)
svmModel.linear.GV <- train(x=train.GV[,-c(1,2)],
y= train.GV[,2],
method = "svmLinear",
preProc = c("center","scale"),
trControl=ctrl)
SVM with polynomial kernel
svmModel.Poly.GS <- train(x=train.GS[,-c(1,2)],
y= train.GS[,2],
method = "svmPoly",
preProc = c("center","scale"),
tuneGrid = grid.poly,trControl=ctrl)
svmModel.Poly.GV <- train(x=train.GV[,-c(1,2)],
y= train.GV[,2],
method = "svmPoly",
preProc = c("center","scale"),
tuneGrid = grid.poly,
trControl=ctrl)
SVM with radial kernel
svmModel.Radial.GS <- train(x=train.GS[,-c(1,2)],
y= train.GS[,2],
method = "svmRadial",
preProc = c("center","scale"),
tuneGrid = grid,
trControl=ctrl)
svmModel.Radial.GV <- train(x=train.GV[,-c(1,2)],
y= train.GV[,2],
method = "svmRadial",
preProc = c("center","scale"),
tuneGrid = grid,
trControl=ctrl)
Random forest
#Random forest has its own naming preference
#GraphletSearch dataset
RfTrainGS<-cbind(y=train.GS[,2],train.GS[,-c(1,2)])
RfModelGS <- train(y~.,data=RfTrainGS,method = "rf",
preProcess=c("center","scale"),
trControl=RF.control,
prox=TRUE,
tuneGrid=expand.grid(mtry = 5),
number=10,
ntree=500)
#GraphVector dataset
RfTrainGV<-cbind(y=train.GV[,2],train.GV[,-c(1,2)])
RfModelGV <- train(y~.,data=RfTrainGV,method = "rf",
preProcess=c("center","scale"),
trControl=RF.control,
prox=TRUE,
tuneGrid=expand.grid(mtry = 5),
number=10,
ntree=500)
4) Predict
We will predict the Class value for each Graph on test dataset so we can later compare to the actual Class with the predicted value and finally calculate performance.
## Predicting for different models for GraphletSearch test dataset
## -c(1,2) means removing GraphID,Class
prediction.y.linear.GS<-predict(svmModel.linear.GS,test.GS[,-c(1,2)])
prediction.y.Radial.GS<-predict(svmModel.Radial.GS,test.GS[,-c(1,2)])
prediction.y.Poly.GS <-predict(svmModel.Poly.GS,test.GS[,-c(1,2)])
prediction.y.RF.GS<-predict(RfModelGS,test.GS[,-c(1,2)])
## Predicting for different models for GraphVector test dataset
prediction.y.linear.GV<-predict(svmModel.linear.GV,test.GV[,-c(1,2)])
prediction.y.Radial.GV<-predict(svmModel.Radial.GV,test.GV[,-c(1,2)])
prediction.y.Poly.GV <-predict(svmModel.Poly.GV,test.GV[,-c(1,2)])
prediction.y.RF.GV<-predict(RfModelGV,test.GV[,-c(1,2)])
5) Analyse performance
First we calculate performance for each model, then we will put them in a table for each dataset:
confMatrix.linear.GS<-confusionMatrix(test.GS[,2],prediction.y.linear.GS)
confMatrix.linear.GV<-confusionMatrix(test.GV[,2],prediction.y.linear.GV)
confMatrix.radial.GS<-confusionMatrix(test.GS[,2],prediction.y.Radial.GS)
confMatrix.radial.GV<-confusionMatrix(test.GV[,2],prediction.y.Radial.GV)
confMatrix.poly.GS<-confusionMatrix(test.GS[,2],prediction.y.Poly.GS)
confMatrix.poly.GV<-confusionMatrix(test.GV[,2],prediction.y.Poly.GV)
confMatrix.rf.GS<-confusionMatrix(test.GS[,2],prediction.y.RF.GS)
confMatrix.rf.GV<-confusionMatrix(test.GV[,2],prediction.y.RF.GV)
acc.GS<-cbind(
Statistic='Accuracy',
SVM_Linear_GS=confMatrix.linear.GS$overall['Accuracy'],
SVM_Radial_GS=confMatrix.radial.GS$overall['Accuracy'],
SVM_Poly_GS=confMatrix.poly.GS$overall['Accuracy'],
RandForest_GS=confMatrix.rf.GS$overall['Accuracy'])
kappa.GS<-cbind(
Statistic='Kappa',
SVM_Linear_GS=confMatrix.linear.GS$overall['Kappa'],
SVM_Radial_GS=confMatrix.radial.GS$overall['Kappa'],
SVM_Poly_GS=confMatrix.poly.GS$overall['Kappa'],
RandForest_GS=confMatrix.rf.GS$overall['Kappa'])
acc.GV<-cbind(
Statistic='Accuracy',
SVM_Linear_GV=confMatrix.linear.GV$overall['Accuracy'],
SVM_Radial_GV=confMatrix.radial.GV$overall['Accuracy'],
SVM_Poly_GV=confMatrix.poly.GV$overall['Accuracy'],
RandForest_GV=confMatrix.rf.GV$overall['Accuracy'])
kappa.GV<-cbind(
Statistic='Kappa',
SVM_Linear_GV=confMatrix.linear.GV$overall['Kappa'],
SVM_Radial_GV=confMatrix.radial.GV$overall['Kappa'],
SVM_Poly_GV=confMatrix.poly.GV$overall['Kappa'],
RandForest_GV=confMatrix.rf.GV$overall['Kappa'])
comparison.GS<-as.data.frame(rbind(acc.GS,kappa.GS))
comparison.GV<-as.data.frame(rbind(acc.GV,kappa.GV))
Confusion matrix provides different information about performance of our model. As an Example for GraphletSearch dataset using Random forest:
Note: X1 means Class label “1” and X.1 refers to the Class label “-1” of the graph.
Confusion Matrix and Statistics
Reference
Prediction X1 X.1
X1 30 7
X.1 3 15
Accuracy : 0.8182
95% CI : (0.691, 0.9092)
No Information Rate : 0.6
P-Value [Acc > NIR] : 0.000462
Kappa : 0.6094
Mcnemar's Test P-Value : 0.342782
Sensitivity : 0.9091
Specificity : 0.6818
Pos Pred Value : 0.8108
Neg Pred Value : 0.8333
Prevalence : 0.6000
Detection Rate : 0.5455
Detection Prevalence : 0.6727
Balanced Accuracy : 0.7955
'Positive' Class : X1
Generally we can compare models on each dataset based on their accuracy and kappa value. For the same range of Accuracy, we would say the method with higher kappa value provides more reliability on the result.
Performance of GraphletSearch and GraphVector datasets shown below:
For GraphletSearch:
SVM_Linear_GS SVM_Radial_GS SVM_Poly_GS RandForest_GS
Accuracy "0.854545454545454" "0.872727272727273" "0.872727272727273" "0.818181818181818"
Kappa "0.66966966966967" "0.715025906735751" "0.715025906735751" "0.609375"
For GraphVector:
SVM_Linear_GV SVM_Radial_GV SVM_Poly_GV RandForest_GV
Accuracy "0.890909090909091" "0.836363636363636" "0.836363636363636" "0.890909090909091"
Kappa "0.759124087591241" "0.611764705882353" "0.63360473723168" "0.752252252252252"


LS0tCnRpdGxlOiAnQ2xhc3NpZnlpbmcgR3JhcGggZGF0YXNldHMgdXNpbmcgTWFjaGluZSBsZWFybmluZyBhbGdvcml0aG1zIDogU1ZNIGFuZCBSYW5kb20KICBGb3Jlc3QnCmF1dGhvcjogIlJlemEgTmlydW1hbmQiCm91dHB1dDoKICBodG1sX25vdGVib29rOiBkZWZhdWx0CiAgcGRmX2RvY3VtZW50OiBkZWZhdWx0Ci0tLQoKIyBDcmVhdGluZyBEYXRhClRoZSBkYXRhc2V0IGZvciB0aGlzIG1hY2hpbmUgbGVhcm5pbmcgdGFzayBpcyBjcmVhdGVkIHVzaW5nIGJlbG93IEphdmEgUHJvZ3JhbS4gVGhlIHByb2dyYW0gY3JlYXRlcyB0d28gZGF0YXNldHM6ICAKCiogVXNpbmcgYWxsIGstR3JhcGhsZXRzICB3aXRoIGs9Myw0LDUgd2l0aCB0aGUgbmFtZSBjb252ZW50aW9uIE1MLTxmaWxlTmFtZT4uY3N2IC4gRm9yIGV4YW1wbGUgTUwtTVVUQUcuY3N2ICAKICAgQ29sdW1uIG5hbWVzOiBHcmFwaElEOyBDbGFzczsgQ29tcG9uZW50cztBdmdEZWdyZWU7IERlbnNpdHk7IFZlcnRpY2VzOyBFZGdlczsgdC0wOyB0LTEuLi4uLi50LTM3OyB0LTM4OyAgCgoqIFVzaW5nIFNpbXBsZUdyYXBoVmVjdG9yIHdpdGggdGhlIG5hbWUgY29udmVudGlvbiBNTC1HVi08ZmlsZU5hbWU+LmNzdi4gRm9yIEV4YW1wbGUgTUwtR1YtTVVUQUcuY3N2LiAgCiAgIENvbHVtbiBuYW1lczogR3JhcGhJRDsgQ2xhc3M7IENvbXBvbmVudHMgOyB0LTA7IHQtMS4uLi4uIHQtMTUgOyB0LTE2IDsgIAoKCmBgYHt9CnB1YmxpYyBjbGFzcyBtbENyZWF0ZUZlYXR1cmVzIHsKCglwcml2YXRlIHN0YXRpYyBTdHJpbmcgZmlsZVBhdGg9IklucHV0LyI7CgkKCXB1YmxpYyBzdGF0aWMgdm9pZCBtYWluKFN0cmluZ1tdIGFyZ3MpIHsKCQkKCQlTdHJpbmcgZmlsZU5hbWUgPSAiTVVUQUciOy8vSU1EQi1CSU5BUlkgLCBDT0xMQUIgLCBFTlpZTUVTCgkJb3V0UHV0RmVhdHVyZXMoZmlsZU5hbWUpOwoJCW91dFB1dEZlYXR1cmVzR3JhcGhWZWN0b3IoZmlsZU5hbWUpOwoJCS8vIEZpbGVzIGFyZSBhdmFpbGJsZSBpbiBQcm9qZWN0IGZvbGRlciAvT3V0cHV0Cgl9CmBgYAoKIyMgRGVzY3JpcHRpb24KKiBHcmFwaElEOiBJdCBpcyB0byBpZGVudGlmeSBhbmQgbWF0Y2ggdG8gdGhlIGlucHV0IGZpbGUuIER1cmluZyBtYWNoaW5lIGxlYXJuaW5nIHRhc2ssIHRoaXMgY29sdW1uIHdpbGwgYmUgaWdub3JlZAoqIENsYXNzOiBPdXIgdGFzayB0byBwcmVkaWN0IHRoZSB2YWx1ZSBvZiBjbGFzcyBMYWJlbCB1c2luZyB0cmFpbmluZyBkYXRhLgoqIENvbXBvbmVudHM6IEludGVnZXIgdmFsdWUgaW5kaWNhdGluZyBob3cgbWFueSBjb25uZWN0ZWQgY29tcG9uZW50IGV4aXN0cyBpbiB0aGUgZWFjaCBHcmFwaCAKKiBBdmdEZWdyZWUgOiBBdmVyYWdlIG91dGdvaW5nIGRlZ3JlZSBvZiBhbGwgbm9kZXMgIAoqIERlbnNpdHkgOiBEZW5zaXR5IG9mIEdyYXBoICggc2VlIGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0RlbnNlX2dyYXBoKQoqIHQtMC4uLnQtMzggOiBjb3JyZXNwb25kcyB0byB0aGUgYXJyYXkgb3V0cHV0IG9mIEdyYXBobGV0IHNlYXJjaC4gKG9uIE1MLTxmaWxlTmFtZT4uY3N2IGRhdGFzZXRzKQoqIHQtMC4uLnQtMTYgOiBjb3JyZXNwb25kcyB0byB0aGUgYXJyYXkgb3V0cHV0IG9mIFNpbXBsZUdyYXBoVmVjdG9yLnRvQXJyYXkgLiAob24gTUwtR1YtPGZpbGVOYW1lPi5jc3YgZGF0YXNldHMpCgoKIyBVc2luZyBEYXRhCldlIHdpbGwgZG8gZm9sbG93aW5ncyBkdXJpbmcgb3VyIE1hY2hpbmUgbGVhcm5pbmcgcHJvY2VzczogIAowLiBQcmVwYXJlIEVudmlyb25tZW50ICAKMS4gSW1wb3J0IGRhdGEgICAKMi4gRGF0YSBjbGVhbnNpbmcgIAozLiBUcmFpbiBNb2RlbCAgIAo0LiBQcmVkaWN0ICAgCjUuIEFuYWx5c2UgcGVyZm9ybWFuY2UgICAKCiMjIDApIFByZXBhcmUgRW52aXJub21lbnQKVG8gZG8gdGhlIGFuYWx5c2lzICwgd2UgbmVlZCB0byBpbXBvcnQgcmVsYXRlZCBwYWNrYWdlcy4gIApgYGB7ciBlbnYsbWVzc2FnZT1GQUxTRSxlY2hvPVRSVUUscmVzdWx0cz0naGlkZScsZXJyb3I9RkFMU0V9CnJtKGxpc3QgPSBscyhhbGwgPSBUUlVFKSkgIyBSZW1vdmluZyBhbnkgZXhpc2l0aW5nIHZhcmlhYmxlcwoKcmVxdWlyZShkcGx5cikKcmVxdWlyZSh0aWR5cikKcmVxdWlyZShjYXJldCkKcmVxdWlyZShubmV0KQpyZXF1aXJlKHBST0MpCnJlcXVpcmUoZTEwNzEpCnJlcXVpcmUocnBhcnQpCgojIyBGb3IgcmVwcm9kdWNpYmxlIHJlc3VsdHMgd2UgbmVlZCB0byBzdGFydCB3aXRoIHRoZSBzYW1lIHNlZWQKc2V0LnNlZWQoMTMzMykgCmBgYAoKYGBge3IgbWVzc2FnZT1GQUxTRSxlY2hvPVRSVUUscmVzdWx0cz0naGlkZScsZXJyb3I9RkFMU0V9CmlmKC5QbGF0Zm9ybSRPUy50eXBlID09IndpbmRvd3MiKQp7CiAgc2V0d2QoIkQ6L2dpdFJlcG9zL1BlcmZBbmFseXNpcy8iKQogIAp9ZWxzZSB7CiAgc2V0d2QoIn4vZ2l0UmVwb3MvUGVyZkFuYWx5c2lzLyIpCiAgIyNsaWJyYXJ5KGRvTUMpCiAgIyNyZWdpc3RlckRvTUMoY29yZXMgPSA0KQp9CnNvdXJjZSgnbXlNdWx0aUNsYXNzU3VtbWFyeS5SJykKYGBgCiMjIDEpIEltcG9ydCBkYXRhCldlIGZpcnN0IHJlYWQgdGhlIGNzdiBmaWxlczogIE1MLU1VVEFHLmNzdiBhbmQgTUwtR1YtTVVUQUcuY3N2IC4KSGVyZSBHViBpcyBzaG9ydCBmb3IgR3JhcGhWZWN0b3IgYW5kIEdTIGlzIHNob3J0IGZvciBHcmFwaGxldFNlYXJjaAoKYGBge3J9CmRmLmlucHV0LkdTPC1yZWFkLmNzdjIoZmlsZT0iLi9pbnB1dC9NTC1NVVRBRy5jc3YiLAogICAgICAgICAgICAgICAgICAgICAgIHNlcD0iOyIsCiAgICAgICAgICAgICAgICAgICAgICAgaGVhZGVyID0gVFJVRSwKICAgICAgICAgICAgICAgICAgICAgICBkZWMgPSAiLiIpCgpkZi5pbnB1dC5HVjwtcmVhZC5jc3YyKGZpbGU9Ii4vaW5wdXQvTUwtR1YtTVVUQUcuY3N2IiwKICAgICAgICAgICAgICAgICAgICAgICBzZXA9IjsiLAogICAgICAgICAgICAgICAgICAgICAgIGhlYWRlciA9IFRSVUUsCiAgICAgICAgICAgICAgICAgICAgICAgZGVjID0gIi4iKQpgYGAKTGV0cyBzZWUgdGhlIGRpbWVuc2lvbiBvZiBlYWNoIGRhdGEgZnJhbWU6CgpgYGB7ciBlY2hvPUZBTFNFIH0KY29tcGFyZTwtcmJpbmQoR1M9ZGltKGRmLmlucHV0LkdTKSxHVj1kaW0oZGYuaW5wdXQuR1YpKQpjb2xuYW1lcyhjb21wYXJlKTwtYygiUm93cyIsIkNvbHVtbnMiKQoKcHJpbnQoY29tcGFyZSkKYGBgCgojIyAyKSBEYXRhIGNsZWFuc2luZyAgCjEuIEZpcnN0IHdlIG5lZWQgdG8gY29ycmVjdCBjb2x1bW4gZGF0YSB0eXBlcyBpbiBvcmRlciBvdXIgY2xhc3NpZmljYXRpb24gYWxnb3JpdGhtcyB0byB3b3JrLiBTbyB0aGUgR3JhcGhJRCBhbmQgQ2xhc3MgYXJlIG9mIHRoZSB0eXBlIG5vbWluYWwgKG9yIGluIFIgY2FsbGVkIGZhY3RvcikuICAKMi4gVGhlbiB3ZSBmaW5kIHRoZSBjb2x1bW4gd2l0aCB6ZXJvIHZhcmlhbmNlIGFuZCByZW1vdmUgaXQgZnJvbSBkYXRhc2V0LiAgCgpgYGB7ciB0aWR5PUZBTFNFLHdhcm5pbmc9RkFMU0V9CmRmLmlucHV0LkdWJEdyYXBoSUQgPC1hcy5mYWN0b3IoZGYuaW5wdXQuR1YkR3JhcGhJRCApOwpkZi5pbnB1dC5HViRDbGFzczwtYXMuZmFjdG9yKG1ha2UubmFtZXMoZGYuaW5wdXQuR1YkQ2xhc3MpKTsgCgpkZi5pbnB1dC5HUyRHcmFwaElEIDwtYXMuZmFjdG9yKGRmLmlucHV0LkdTJEdyYXBoSUQgKTsKZGYuaW5wdXQuR1MkQ2xhc3M8LWFzLmZhY3RvcihtYWtlLm5hbWVzKGRmLmlucHV0LkdTJENsYXNzKSk7IAoKIyMgRmluZGluZyBhbmQgcmVtb3ZpbmcgY29sdW1ucyB3aXRoIHplcm8gdmFyaWFuY2UKbnp2LkdTPC1uZWFyWmVyb1ZhcihkZi5pbnB1dC5HUykKbnp2LkdWPC1uZWFyWmVyb1ZhcihkZi5pbnB1dC5HVikKCmBgYAoKRm9sbG93aW5nIGNvbHVtbnMvZmVhdHVyZXMgaGF2ZSB6ZXJvIHZhcmlhbmNlLCB3aGljaCBjYW4gYmUgcmVtb3ZlZDogIAoKYGBge3IgZWNobz1GQUxTRSx0aWR5PUZBTFNFLHdhcm5pbmc9RkFMU0V9CnByaW50KGNvbXBhcmUpCmNhdCgiXG5Db2x1bW5zIGZvciBHUyB0byBiZSByZW1vdmVkOiAgIikKY2F0KHBhc3RlKG5hbWVzKGRmLmlucHV0LkdTWyxuenYuR1NdKSxjb2xsYXBzZT0iOyAiKSkKY2F0KCJcbkNvbHVtbnMgZm9yIEdWIHRvIGJlIHJlbW92ZWQ6ICAiKQpjYXQocGFzdGUobmFtZXMoZGYuaW5wdXQuR1ZbLG56di5HVl0pLGNvbGxhcHNlPSI7ICIpKQoKZGYuaW5wdXQuR1M8LWRmLmlucHV0LkdTWywtbnp2LkdTXQpkZi5pbnB1dC5HVjwtZGYuaW5wdXQuR1ZbLC1uenYuR1ZdCgpgYGAKTm93IGxldCBzZWUgdGhlIHNoYXBlIG9mIG91ciBkYXRhc2V0cyBhZnRlciByZW1vdmluZyB6ZXJvIHZhcmlhbmNlCgpgYGB7ciBlY2hvPUZBTFNFLHRpZHk9RkFMU0Usd2FybmluZz1GQUxTRSB9Cgpjb21wYXJlPC1yYmluZChHUz1kaW0oZGYuaW5wdXQuR1MpLEdWPWRpbShkZi5pbnB1dC5HVikpCmNvbG5hbWVzKGNvbXBhcmUpPC1jKCJSb3dzIiwiQ29sdW1ucyIpCgpwcmludChjb21wYXJlKQpjYXQoIlxuQ29sdW1ucyBmb3IgR1M6ICAiKQpjYXQocGFzdGUobmFtZXMoZGYuaW5wdXQuR1MpLGNvbGxhcHNlPSI7ICIpKQpjYXQoIlxuQ29sdW1ucyBmb3IgR1Y6ICAiKQpjYXQocGFzdGUobmFtZXMoZGYuaW5wdXQuR1YpLGNvbGxhcHNlPSI7ICIpKQpgYGAKClRoZSBkaW1lbnNpb25zIGNvcnJlc3BvbmRzIHRvIHRoZSBmb2xsb3dpbmcgR3JhcGggdHlwZTogIAoKIVtBbGwgdHlwZSAzLDQsNSBHcmFwaGxldHMuXShncmFwaGxldHNSZWYuUE5HKQoKMy4gUGFydGl0aW9uaW5nIHRoZSBkYXRhc2V0cyBpbnRvIHRyYWluICg3MCAlICkgYW5kIHRlc3QgKDMwICUpIGZvciBjcm9zcyB2YWxpZGF0aW9uCgpgYGB7ciB9CiMjcmFuZG9tbHkgcGFydGl0aW9uIGRhdGEgaW50byB0d28gZGF0YXNldHMgLHRyYWluaW5nIGFuZCB0ZXN0aW5nCiNHcmFwaGxldFNlYXJjaAppblRyYWluLkdTPC1jcmVhdGVEYXRhUGFydGl0aW9uKHk9ZGYuaW5wdXQuR1MkQ2xhc3MscCA9IC43LGxpc3Q9RkFMU0UpCnRyYWluLkdTPC1kZi5pbnB1dC5HU1tpblRyYWluLkdTLF0gICAKdGVzdC5HUzwtIGRmLmlucHV0LkdTWy1pblRyYWluLkdTLF0gCgojR3JhcGhWZWN0b3IKaW5UcmFpbi5HVjwtY3JlYXRlRGF0YVBhcnRpdGlvbih5PWRmLmlucHV0LkdWJENsYXNzLHAgPSAuNyxsaXN0PUZBTFNFKQp0cmFpbi5HVjwtZGYuaW5wdXQuR1ZbaW5UcmFpbi5HVixdICAgCnRlc3QuR1Y8LSBkZi5pbnB1dC5HVlstaW5UcmFpbi5HVixdIAoKYGBgCgojIyAzKSBUcmFpbiBNb2RlbCAgCk5vdyB3ZSB0cmFpbiBmb2xsb3dpbmcgbW9kZWxzOiAgCiogU1ZNICAgCiogU1ZNIHdpdGggUG9seW5vbWlhbCBrZXJuZWwgIAoqIFNWTSB3aXRoIFJhZGlhbCBrZXJuZWwgIAoqIFJhbmRvbSBmb3Jlc3QgIAoKYGBge3IgcGFyYW1ldGVycyx0aWR5PUZBTFNFfQoKIyNDcmVhdGUgYSBkYXRhIGZyYW1lIGZyb20gYWxsIGNvbWJpbmF0aW9ucyBvZiB0aGUgc3VwcGxpZWQgcGFyYW1ldGVycyBmb3IgUmFkaWFsIGFuZCBsaW5lYXIgS2VybmVscwpncmlkIDwtIGV4cGFuZC5ncmlkKHNpZ21hID0gYyguMDEsIC4wMTUsIDAuMiksCiAgICAgICAgICAgICAgICAgICAgQyA9IGMoMC43NSwgMC45LCAxLCAxLjEsIDEuMjUpKSAgCgojI0NyZWF0ZSBhIGRhdGEgZnJhbWUgZnJvbSBhbGwgY29tYmluYXRpb25zIG9mIHRoZSBzdXBwbGllZCBwYXJhbWV0ZXJzIGZvciBQb2x5bm9taWFsIGtlcm5lbApncmlkLnBvbHkgPC0gZXhwYW5kLmdyaWQoQyA9IGMoMC43NSwgMC45LCAxLCAxLjEsIDEuMjUpLAogICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9YyguMDAwMSksCiAgICAgICAgICAgICAgICAgICAgICAgICBkZWdyZWU9MToyKSAKCiNXZSBuZWVkIHRvIGNoZWNrIGlmIGl0IGlzIGJpbmFyeSBjbGFzc2lmaWNhdGlvbiAKI29yIG11bHRpY2xhc3MgY2xhc3NpZmNhdGlvbiwgd2hpY2ggcmVxdWlyZXMgZGlmZnJlbnQgc3VtbWFyeSBmdW5jdGlvbi4KaWYobGVuZ3RoKHVuaXF1ZShkZi5pbnB1dC5HUyRDbGFzcykpID4gMikgCnsKICBjdHJsIDwtdHJhaW5Db250cm9sKG1ldGhvZD0icmVwZWF0ZWRjdiIsCiAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzPTEwLAogICAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icz1UUlVFLAogICAgICAgICAgICAgICAgICAgICAgc3VtbWFyeUZ1bmN0aW9uPW15TXVsdGlDbGFzc1N1bW1hcnkpCn0gZWxzZQp7CiAgY3RybCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kPSJyZXBlYXRlZGN2IiwKICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzPTEwLAogICAgICAgICAgICAgICAgICAgICAgIGNsYXNzUHJvYnM9VFJVRSwKICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJ5RnVuY3Rpb249dHdvQ2xhc3NTdW1tYXJ5KQp9CgojIyBQYXJhbWV0ZXJzIGZvciBSYW5kb20gRm9yZXN0IGFsZ29yaXRobS4KUkYuY29udHJvbCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kPSJyZXBlYXRlZGN2IiwgbnVtYmVyPTEwLCByZXBlYXRzPTMpCgpgYGAKCkJlZm9yZSB0cmFpbmluZyB3ZSB3aWxsIGNlbnRlciBhbmQgc2NhbGUgdGhlIGZlYXR1cmVzIGZvciBiZXR0ZXIgYWNjdXJhY3kuCgojIyMgU1ZNIGxpbmVhciAgCmBgYHtyIHN2bUxpbmVhciwgbWVzc2FnZT1GQUxTRSxlcnJvcj1GQUxTRSxyZXN1bHRzPSdoaWRlJyxjYWNoZT1UUlVFLHRpZHk9RkFMU0Usd2FybmluZz1GQUxTRX0Kc3ZtTW9kZWwubGluZWFyLkdTIDwtIHRyYWluKHg9dHJhaW4uR1NbLC1jKDEsMildLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgeT0gdHJhaW4uR1NbLDJdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInN2bUxpbmVhciIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVQcm9jID0gYygiY2VudGVyIiwic2NhbGUiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbD1jdHJsKSAgCgpzdm1Nb2RlbC5saW5lYXIuR1YgPC0gdHJhaW4oeD10cmFpbi5HVlssLWMoMSwyKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB5PSB0cmFpbi5HVlssMl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAic3ZtTGluZWFyIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByZVByb2MgPSBjKCJjZW50ZXIiLCJzY2FsZSIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJDb250cm9sPWN0cmwpICAKYGBgCgojIyMgU1ZNIHdpdGggcG9seW5vbWlhbCBrZXJuZWwgIApgYGB7ciBzdm1Qb2x5LG1lc3NhZ2U9RkFMU0UsZXJyb3I9RkFMU0UscmVzdWx0cz0naGlkZScsY2FjaGU9VFJVRSx0aWR5PUZBTFNFLHdhcm5pbmc9RkFMU0V9CnN2bU1vZGVsLlBvbHkuR1MgPC0gdHJhaW4oeD10cmFpbi5HU1ssLWMoMSwyKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgeT0gdHJhaW4uR1NbLDJdLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAic3ZtUG9seSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgcHJlUHJvYyA9IGMoImNlbnRlciIsInNjYWxlIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBncmlkLnBvbHksdHJDb250cm9sPWN0cmwpICAKCnN2bU1vZGVsLlBvbHkuR1YgPC0gdHJhaW4oeD10cmFpbi5HVlssLWMoMSwyKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgeT0gdHJhaW4uR1ZbLDJdLAogICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJzdm1Qb2x5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVQcm9jID0gYygiY2VudGVyIiwic2NhbGUiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGdyaWQucG9seSwKICAgICAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2w9Y3RybCkgIApgYGAKIyMjIFNWTSB3aXRoIHJhZGlhbCBrZXJuZWwgIApgYGB7ciBzdm1SYWRpYWwsbWVzc2FnZT1GQUxTRSxlcnJvcj1GQUxTRSxyZXN1bHRzPSdoaWRlJyxjYWNoZT1UUlVFLHRpZHk9RkFMU0Usd2FybmluZz1GQUxTRX0Kc3ZtTW9kZWwuUmFkaWFsLkdTIDwtIHRyYWluKHg9dHJhaW4uR1NbLC1jKDEsMildLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgeT0gdHJhaW4uR1NbLDJdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInN2bVJhZGlhbCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVQcm9jID0gYygiY2VudGVyIiwic2NhbGUiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHR1bmVHcmlkID0gZ3JpZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbD1jdHJsKSAgCgpzdm1Nb2RlbC5SYWRpYWwuR1YgPC0gdHJhaW4oeD10cmFpbi5HVlssLWMoMSwyKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB5PSB0cmFpbi5HVlssMl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAic3ZtUmFkaWFsIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByZVByb2MgPSBjKCJjZW50ZXIiLCJzY2FsZSIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBncmlkLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJDb250cm9sPWN0cmwpICAKYGBgCgojIFJhbmRvbSBmb3Jlc3QgIApgYGB7ciBSYW5kb21Gb3Jlc3QsbWVzc2FnZT1GQUxTRSxlcnJvcj1GQUxTRSxyZXN1bHRzPSdoaWRlJyxjYWNoZT1UUlVFLHRpZHk9RkFMU0Usd2FybmluZz1GQUxTRX0KI1JhbmRvbSBmb3Jlc3QgaGFzIGl0cyBvd24gbmFtaW5nIHByZWZlcmVuY2UKI0dyYXBobGV0U2VhcmNoIGRhdGFzZXQKUmZUcmFpbkdTPC1jYmluZCh5PXRyYWluLkdTWywyXSx0cmFpbi5HU1ssLWMoMSwyKV0pIAoKUmZNb2RlbEdTIDwtIHRyYWluKHl+LixkYXRhPVJmVHJhaW5HUyxtZXRob2QgPSAicmYiLAogICAgICAgICAgICAgICAgICAgcHJlUHJvY2Vzcz1jKCJjZW50ZXIiLCJzY2FsZSIpLAogICAgICAgICAgICAgICAgICAgdHJDb250cm9sPVJGLmNvbnRyb2wsCiAgICAgICAgICAgICAgICAgICBwcm94PVRSVUUsIAogICAgICAgICAgICAgICAgICAgdHVuZUdyaWQ9ZXhwYW5kLmdyaWQobXRyeSA9IDUpLAogICAgICAgICAgICAgICAgICAgbnVtYmVyPTEwLAogICAgICAgICAgICAgICAgICAgbnRyZWU9NTAwKSAKCiNHcmFwaFZlY3RvciBkYXRhc2V0ClJmVHJhaW5HVjwtY2JpbmQoeT10cmFpbi5HVlssMl0sdHJhaW4uR1ZbLC1jKDEsMildKSAKUmZNb2RlbEdWIDwtIHRyYWluKHl+LixkYXRhPVJmVHJhaW5HVixtZXRob2QgPSAicmYiLAogICAgICAgICAgICAgICAgICAgcHJlUHJvY2Vzcz1jKCJjZW50ZXIiLCJzY2FsZSIpLAogICAgICAgICAgICAgICAgICAgdHJDb250cm9sPVJGLmNvbnRyb2wsCiAgICAgICAgICAgICAgICAgICBwcm94PVRSVUUsCiAgICAgICAgICAgICAgICAgICB0dW5lR3JpZD1leHBhbmQuZ3JpZChtdHJ5ID0gNSksCiAgICAgICAgICAgICAgICAgICBudW1iZXI9MTAsCiAgICAgICAgICAgICAgICAgICBudHJlZT01MDApIAoKYGBgCgojIyA0KSBQcmVkaWN0ICAKV2Ugd2lsbCBwcmVkaWN0IHRoZSBDbGFzcyB2YWx1ZSBmb3IgZWFjaCBHcmFwaCBvbiB0ZXN0IGRhdGFzZXQgc28gd2UgY2FuIGxhdGVyIGNvbXBhcmUgdG8gdGhlIGFjdHVhbCBDbGFzcyB3aXRoIHRoZSBwcmVkaWN0ZWQgdmFsdWUgYW5kIGZpbmFsbHkgY2FsY3VsYXRlIHBlcmZvcm1hbmNlLgpgYGB7ciB0aWR5PUZBTFNFLG1lc3NhZ2U9RkFMU0UsZXJyb3I9RkFMU0Usd2FybmluZz1GQUxTRX0KIyMgUHJlZGljdGluZyBmb3IgZGlmZmVyZW50IG1vZGVscyBmb3IgR3JhcGhsZXRTZWFyY2ggdGVzdCBkYXRhc2V0CiMjIC1jKDEsMikgbWVhbnMgcmVtb3ZpbmcgR3JhcGhJRCxDbGFzcwpwcmVkaWN0aW9uLnkubGluZWFyLkdTPC1wcmVkaWN0KHN2bU1vZGVsLmxpbmVhci5HUyx0ZXN0LkdTWywtYygxLDIpXSkgCnByZWRpY3Rpb24ueS5SYWRpYWwuR1M8LXByZWRpY3Qoc3ZtTW9kZWwuUmFkaWFsLkdTLHRlc3QuR1NbLC1jKDEsMildKQpwcmVkaWN0aW9uLnkuUG9seS5HUyAgPC1wcmVkaWN0KHN2bU1vZGVsLlBvbHkuR1MsdGVzdC5HU1ssLWMoMSwyKV0pCnByZWRpY3Rpb24ueS5SRi5HUzwtcHJlZGljdChSZk1vZGVsR1MsdGVzdC5HU1ssLWMoMSwyKV0pCgojIyBQcmVkaWN0aW5nIGZvciBkaWZmZXJlbnQgbW9kZWxzIGZvciBHcmFwaFZlY3RvciB0ZXN0IGRhdGFzZXQKcHJlZGljdGlvbi55LmxpbmVhci5HVjwtcHJlZGljdChzdm1Nb2RlbC5saW5lYXIuR1YsdGVzdC5HVlssLWMoMSwyKV0pCnByZWRpY3Rpb24ueS5SYWRpYWwuR1Y8LXByZWRpY3Qoc3ZtTW9kZWwuUmFkaWFsLkdWLHRlc3QuR1ZbLC1jKDEsMildKQpwcmVkaWN0aW9uLnkuUG9seS5HViAgPC1wcmVkaWN0KHN2bU1vZGVsLlBvbHkuR1YsdGVzdC5HVlssLWMoMSwyKV0pCnByZWRpY3Rpb24ueS5SRi5HVjwtcHJlZGljdChSZk1vZGVsR1YsdGVzdC5HVlssLWMoMSwyKV0pCmBgYAoKIyMgNSkgQW5hbHlzZSBwZXJmb3JtYW5jZSAgCkZpcnN0IHdlIGNhbGN1bGF0ZSBwZXJmb3JtYW5jZSBmb3IgZWFjaCBtb2RlbCwgdGhlbiB3ZSB3aWxsIHB1dCB0aGVtIGluIGEgdGFibGUgZm9yIGVhY2ggZGF0YXNldDoKYGBge3J9CmNvbmZNYXRyaXgubGluZWFyLkdTPC1jb25mdXNpb25NYXRyaXgodGVzdC5HU1ssMl0scHJlZGljdGlvbi55LmxpbmVhci5HUykKY29uZk1hdHJpeC5saW5lYXIuR1Y8LWNvbmZ1c2lvbk1hdHJpeCh0ZXN0LkdWWywyXSxwcmVkaWN0aW9uLnkubGluZWFyLkdWKQoKY29uZk1hdHJpeC5yYWRpYWwuR1M8LWNvbmZ1c2lvbk1hdHJpeCh0ZXN0LkdTWywyXSxwcmVkaWN0aW9uLnkuUmFkaWFsLkdTKQpjb25mTWF0cml4LnJhZGlhbC5HVjwtY29uZnVzaW9uTWF0cml4KHRlc3QuR1ZbLDJdLHByZWRpY3Rpb24ueS5SYWRpYWwuR1YpCgpjb25mTWF0cml4LnBvbHkuR1M8LWNvbmZ1c2lvbk1hdHJpeCh0ZXN0LkdTWywyXSxwcmVkaWN0aW9uLnkuUG9seS5HUykKY29uZk1hdHJpeC5wb2x5LkdWPC1jb25mdXNpb25NYXRyaXgodGVzdC5HVlssMl0scHJlZGljdGlvbi55LlBvbHkuR1YpCgpjb25mTWF0cml4LnJmLkdTPC1jb25mdXNpb25NYXRyaXgodGVzdC5HU1ssMl0scHJlZGljdGlvbi55LlJGLkdTKQpjb25mTWF0cml4LnJmLkdWPC1jb25mdXNpb25NYXRyaXgodGVzdC5HVlssMl0scHJlZGljdGlvbi55LlJGLkdWKQoKYWNjLkdTPC1jYmluZCgKICAgICAgICBTdGF0aXN0aWM9J0FjY3VyYWN5JywKICAgICAgICBTVk1fTGluZWFyX0dTPWNvbmZNYXRyaXgubGluZWFyLkdTJG92ZXJhbGxbJ0FjY3VyYWN5J10sCiAgICAgICAgU1ZNX1JhZGlhbF9HUz1jb25mTWF0cml4LnJhZGlhbC5HUyRvdmVyYWxsWydBY2N1cmFjeSddLAogICAgICAgIFNWTV9Qb2x5X0dTPWNvbmZNYXRyaXgucG9seS5HUyRvdmVyYWxsWydBY2N1cmFjeSddLAogICAgICAgIFJhbmRGb3Jlc3RfR1M9Y29uZk1hdHJpeC5yZi5HUyRvdmVyYWxsWydBY2N1cmFjeSddKQoKa2FwcGEuR1M8LWNiaW5kKAogICAgICAgIFN0YXRpc3RpYz0nS2FwcGEnLAogICAgICAgIFNWTV9MaW5lYXJfR1M9Y29uZk1hdHJpeC5saW5lYXIuR1Mkb3ZlcmFsbFsnS2FwcGEnXSwKICAgICAgICBTVk1fUmFkaWFsX0dTPWNvbmZNYXRyaXgucmFkaWFsLkdTJG92ZXJhbGxbJ0thcHBhJ10sCiAgICAgICAgU1ZNX1BvbHlfR1M9Y29uZk1hdHJpeC5wb2x5LkdTJG92ZXJhbGxbJ0thcHBhJ10sCiAgICAgICAgUmFuZEZvcmVzdF9HUz1jb25mTWF0cml4LnJmLkdTJG92ZXJhbGxbJ0thcHBhJ10pCgoKYWNjLkdWPC1jYmluZCgKICAgICAgICBTdGF0aXN0aWM9J0FjY3VyYWN5JywKICAgICAgICBTVk1fTGluZWFyX0dWPWNvbmZNYXRyaXgubGluZWFyLkdWJG92ZXJhbGxbJ0FjY3VyYWN5J10sCiAgICAgICAgU1ZNX1JhZGlhbF9HVj1jb25mTWF0cml4LnJhZGlhbC5HViRvdmVyYWxsWydBY2N1cmFjeSddLAogICAgICAgIFNWTV9Qb2x5X0dWPWNvbmZNYXRyaXgucG9seS5HViRvdmVyYWxsWydBY2N1cmFjeSddLAogICAgICAgIFJhbmRGb3Jlc3RfR1Y9Y29uZk1hdHJpeC5yZi5HViRvdmVyYWxsWydBY2N1cmFjeSddKQoKa2FwcGEuR1Y8LWNiaW5kKAogICAgICAgIFN0YXRpc3RpYz0nS2FwcGEnLAogICAgICAgIFNWTV9MaW5lYXJfR1Y9Y29uZk1hdHJpeC5saW5lYXIuR1Ykb3ZlcmFsbFsnS2FwcGEnXSwKICAgICAgICBTVk1fUmFkaWFsX0dWPWNvbmZNYXRyaXgucmFkaWFsLkdWJG92ZXJhbGxbJ0thcHBhJ10sCiAgICAgICAgU1ZNX1BvbHlfR1Y9Y29uZk1hdHJpeC5wb2x5LkdWJG92ZXJhbGxbJ0thcHBhJ10sCiAgICAgICAgUmFuZEZvcmVzdF9HVj1jb25mTWF0cml4LnJmLkdWJG92ZXJhbGxbJ0thcHBhJ10pCgpjb21wYXJpc29uLkdTPC1hcy5kYXRhLmZyYW1lKHJiaW5kKGFjYy5HUyxrYXBwYS5HUykpCmNvbXBhcmlzb24uR1Y8LWFzLmRhdGEuZnJhbWUocmJpbmQoYWNjLkdWLGthcHBhLkdWKSkKYGBgCgpDb25mdXNpb24gbWF0cml4IHByb3ZpZGVzIGRpZmZlcmVudCBpbmZvcm1hdGlvbiBhYm91dCBwZXJmb3JtYW5jZSBvZiBvdXIgbW9kZWwuIEFzIGFuIEV4YW1wbGUgZm9yIEdyYXBobGV0U2VhcmNoIGRhdGFzZXQgdXNpbmcgUmFuZG9tIGZvcmVzdDoKCk5vdGU6IFgxIG1lYW5zIENsYXNzIGxhYmVsICIxIiBhbmQgWC4xIHJlZmVycyB0byB0aGUgQ2xhc3MgbGFiZWwgIi0xIiBvZiB0aGUgZ3JhcGguCgpgYGB7ciBlY2hvPUZBTFNFfQpjb25mTWF0cml4LnJmLkdTCmBgYAoKR2VuZXJhbGx5IHdlIGNhbiBjb21wYXJlIG1vZGVscyBvbiBlYWNoIGRhdGFzZXQgYmFzZWQgb24gdGhlaXIgYWNjdXJhY3kgYW5kIGthcHBhIHZhbHVlLiBGb3IgdGhlIHNhbWUgcmFuZ2Ugb2YgQWNjdXJhY3ksIHdlIHdvdWxkIHNheSB0aGUgbWV0aG9kIHdpdGggaGlnaGVyIGthcHBhIHZhbHVlIHByb3ZpZGVzIG1vcmUgcmVsaWFiaWxpdHkgb24gdGhlIHJlc3VsdC4KClBlcmZvcm1hbmNlIG9mIEdyYXBobGV0U2VhcmNoIGFuZCBHcmFwaFZlY3RvciBkYXRhc2V0cyBzaG93biBiZWxvdzoKCmBgYHtyIGVycm9yPUZBTFNFLG1lc3NhZ2U9RkFMU0UscmVzdWx0cz0naGlkZScsaW5jbHVkZT1GQUxTRSx0aWR5PUZBTFNFfQojIyBSZXNoYXBpbmcgZGF0YSBmcm9tIHdpZGUgdG8gbG9uZyBmb3JtYXQgZm9yIHBsb3R0aW5nLCBhbmQgY29ycmVjdGluZyBkYXRhIHR5cGUgZm9yIHZhbHVlIGNvbHVtbgojI0dyYXBobGV0U2VhcmNoCmdzTG9uZ0Zvcm1hdDwtZ2F0aGVyKGRhdGEgPSBjb21wYXJpc29uLkdTLGtleSA9ICBNb2RlbE5hbWUsdmFsdWUgPSAndmFsdWUnLFNWTV9MaW5lYXJfR1M6UmFuZEZvcmVzdF9HUyxmYWN0b3Jfa2V5ID1UUlVFICkKZ3NMb25nRm9ybWF0JHZhbHVlPC1hcy5udW1lcmljKGdzTG9uZ0Zvcm1hdCR2YWx1ZSkKCiMjR3JhcGhWZWN0b3IKZ3ZMb25nRm9ybWF0PC1nYXRoZXIoZGF0YSA9IGNvbXBhcmlzb24uR1Ysa2V5ID0gIE1vZGVsTmFtZSx2YWx1ZSA9ICd2YWx1ZScsU1ZNX0xpbmVhcl9HVjpSYW5kRm9yZXN0X0dWLGZhY3Rvcl9rZXkgPVRSVUUgKQpndkxvbmdGb3JtYXQkdmFsdWU8LWFzLm51bWVyaWMoZ3ZMb25nRm9ybWF0JHZhbHVlKQoKCmBgYAoKYGBge3IgZWNobz1GQUxTRSxtZXNzYWdlPUZBTFNFLGVycm9yPUZBTFNFLHRpZHk9RkFMU0V9CgojIyBDcmVhdGluZyB0aGUgY2hhcnRzIGFmdGVyIGNvbnZlcnRpbmcgZGF0YSBmcm9tIHdpZGUgdG8gbG9uZyBmb3JtYXQKZ3M8LWdncGxvdChkYXRhPWdzTG9uZ0Zvcm1hdCxhZXMoeD1Nb2RlbE5hbWUseT12YWx1ZSxmaWxsPVN0YXRpc3RpYykpKwogICAgICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5Iixwb3NpdGlvbj1wb3NpdGlvbl9udWRnZSgpKSsKICAgICAgICBnZ3RpdGxlKCJQZXJmb3JtYW5jZSBjb21wYXJpc29uIGZvciBkYXRhc2V0IEdyYXBobGV0U2VhcmNoW011dGFnXSIpCgoKZ3Y8LWdncGxvdChkYXRhPWd2TG9uZ0Zvcm1hdCxhZXMoeD1Nb2RlbE5hbWUseT12YWx1ZSxmaWxsPVN0YXRpc3RpYykpKwogICAgICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5Iixwb3NpdGlvbj1wb3NpdGlvbl9udWRnZSgpKSsKICAgICAgICBnZ3RpdGxlKCJQZXJmb3JtYW5jZSBjb21wYXJpc29uIGZvciBkYXRhc2V0IEdyYXBoVmVjdG9yW011dGFnXSIpCgpjYXQoIkZvciBHcmFwaGxldFNlYXJjaDpcblxuIikKcHJpbnQoYXMubWF0cml4KGNvbXBhcmlzb24uR1NbLC0xXSkpCmNhdCgiXG5cbkZvciBHcmFwaFZlY3RvcjpcblxuIikKcHJpbnQoYXMubWF0cml4KGNvbXBhcmlzb24uR1ZbLC0xXSkpCgpwcmludChncykKY2F0KCJcblxuXG5cblxuIikKcHJpbnQoZ3YpCgpgYGA=