GDSTM Decorrelation on the multiple features Dataset

This document describes the use of the FRESA.CAD::GDSTMDecorrelation() function that runs the feature correlation analysis (FCA) algorithm on the mfeat data set:

https://archive.ics.uci.edu/ml/datasets/Multiple+Features

M. van Breukelen, R.P.W. Duin, D.M.J. Tax, and J.E. den Hartog, Handwritten digit recognition by combined classifiers, Kybernetika, vol. 34, no. 4, 1998, 381-386.

This scrip uses FRESA.CAD and mlbench R packages:

knitr::opts_chunk$set(collapse = TRUE, warning = FALSE, message = FALSE,comment = "#>")

library("FRESA.CAD")
Loading required package: Rcpp
Loading required package: stringr
Loading required package: miscTools
Loading required package: Hmisc
Loading required package: lattice
Loading required package: survival
Loading required package: Formula
Loading required package: ggplot2
Registered S3 methods overwritten by 'htmltools':
  method               from         
  print.html           tools:rstudio
  print.shiny.tag      tools:rstudio
  print.shiny.tag.list tools:rstudio
Registered S3 method overwritten by 'htmlwidgets':
  method           from         
  print.htmlwidget tools:rstudio
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     

Attaching package: ‘Hmisc’

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

    format.pval, units

Loading required package: pROC
Type 'citation("pROC")' for a citation.

Attaching package: ‘pROC’

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

    cov, smooth, var
library(mlbench)

op <- par(no.readonly = TRUE)

I’ll load the mfeat data set

mfeat <- read.delim("../Data/mfeat.txt", stringsAsFactors=TRUE)
mfeat$ID <- NULL
print(table(mfeat$Class))

  0   1   2   3   4   5   6   7   8   9 
200 200 200 200 200 200 200 200 200 200 

Setting some variables for downstream analysis

studyName = "mfeat"
datasetframe <- mfeat
Outcome <- "Class"

trainFraction = 0.50
correlationThreshold = 0.6
featnames <- colnames(datasetframe)[colnames(datasetframe) != Outcome]

Setting the Training and Testing sets


tb <- table(datasetframe[,Outcome])
classNames <- unique(datasetframe[,Outcome])

allrowClass <- datasetframe[,Outcome]
names(allrowClass) <- rownames(datasetframe)

trainsize <- trainFraction*min(tb);
trainSamples <- NULL;
for (theClass in classNames)
{
  classSample <- allrowClass[allrowClass == theClass]
  trainSamples <- c(trainSamples,names(classSample[sample(length(classSample),trainsize)]))
}


datasetframe_train <- datasetframe[trainSamples,]
testSamples <- !(rownames(datasetframe) %in% trainSamples)
datasetframe_test <- datasetframe[testSamples,]

outcomes <- datasetframe_train[,Outcome]

pander::pander(table(datasetframe[,Outcome]),caption="All")
All
0 1 2 3 4 5 6 7 8 9
200 200 200 200 200 200 200 200 200 200
pander::pander(table(datasetframe_train[,Outcome]),caption="Training")
Training
0 1 2 3 4 5 6 7 8 9
100 100 100 100 100 100 100 100 100 100
pander::pander(table(datasetframe_test[,Outcome]),caption="Testing")
Testing
0 1 2 3 4 5 6 7 8 9
100 100 100 100 100 100 100 100 100 100

FCA with default parameters

The default parameters will compute the transformation matrix with a maximum correlation goal of 0.8 using fast matrix multiplication with Pearson correlation and linear models estimation.

Default Parameters: thr=0.80,method=“fast”,type=“LM”



datasetframeDecor<-list();
decortype <- list();
system.time(datasetframeDecor[[1]] <- GDSTMDecorrelation(datasetframe_train))

user system elapsed 1.27 0.11 1.11

decortype[[1]] <- "Default"

Transforming the testing set

To make test predictions we need to transform the testing set. This is done using the FRESA.CAD::predictDecorrelate() function


# Transform the testing set
decor_test <- predictDecorrelate(datasetframeDecor[[1]],datasetframe_test)

Heat maps of the correlation matrices

Here are the heat maps of the correlation matrices before and after decorrelation on the testing set



featnames <- attr(datasetframeDecor[[1]],"varincluded")
cormat <- cor(datasetframe_test[,featnames],method="pearson")
gplots::heatmap.2(abs(cormat),
                  trace = "none",
                  scale = "none",
                  mar = c(10,10),
                  col=rev(heat.colors(5)),
                  main = paste("Raw Correlation:",studyName),
                  cexRow = 0.75,
                  cexCol = 0.75,
                  key.title=NA,
                  key.xlab="Pearson Correlation",
                  xlab="Feature", ylab="Feature")



featnames <- colnames(attr(datasetframeDecor[[1]],"GDSTM"))
cormat <- cor(decor_test[,featnames],method="pearson")
cormat[is.na(cormat)] <- 0;
gplots::heatmap.2(abs(cormat),
                  trace = "none",
                  scale = "none",
                  mar = c(10,10),
                  col=rev(heat.colors(5)),
                  main = paste("After decorrelation:",studyName),
                  cexRow = 0.75,
                  cexCol = 0.75,
                  key.title=NA,
                  key.xlab="Pearson Correlation",
                  xlab="Transformed Feature", ylab="Transformed Feature")

NA
NA

The GDSTMDecorrelation function returns a data frame with the following column names:

The output features after transformation will be named after the original names and:

Furthermore, the returned data frame will have the following attributes:

  1. Transformation matrix: “GDSTM

  2. Features:

    1. fsocre

    2. varincluded

    3. topFeatures

  3. Unaltered Basis:

    1. baseFeatures

      1. correlatedToBase
    2. AbaseFeatures

GDSTM

The “GDSTM attribute stores the spatial transformation matrix. The matrix only includes continuous features that had some correlation greater than the threshold



## The Spatial Transformation Matrix:
GDSTM <- attr(datasetframeDecor[[1]],"GDSTM")

## The heatmap of the matrix
gplots::heatmap.2(1*(abs(GDSTM) > 0),
                  trace = "none",
                  mar = c(10,10),
                  col=rev(heat.colors(2)),
                  main = paste("GDSTM Matrix:",studyName),
                  cexRow = 0.7,
                  cexCol = 0.7,
                  breaks = c(0,0.5,1),
                  key.title=NA,
                  key.xlab="|beta| > 0",
                  xlab="GDSTM Feature", ylab="Input Feature")

NA
NA
NA

The Feature Index Score

The FCA analysis of the data features are stored in three attributes: “varincluded”, “topFeatures”, and “fscore”.

  • “varincluded” returns the list of continuous features that were decorrelated

  • “topFeatures” returns the features that at some point were used as independent variables inside the linear models.

  • “fscore” : returns a named vector with the total feature score, \(F_j\), of the analyzed features.

\[ F_j=∑_{n}∑_{i∈B^{n}_{j}}|ρ_{i,j}|^2(|ρ_{i,j}|>ρ_{th}),~ \forall j \in Ind \]

\[ F_j=F_j-∑_{n}|ρ_{B^n_j,j}|^2),~ \forall j \in Dep \]

where \(B^n_j\) is the set of features statistically associated with feature j at iteration n, \(ρ_{i,j}\) is the correlation between features i,j, and \(ρ_{th}\) is the correlation goal, \({Ind, Dep}\) are the set of independent and dependent features respectively. In other words, the “fscore” indicates the degree of total association of”independent” features to dependent features.



fscore <- attr(datasetframeDecor[[1]],"fscore") 
fscore <- fscore[order(-fscore)];
barplot(fscore,las=2,cex.names = 0.6)

Machine Learning and GDSTM

Train a simple NB model on the raw dataset

mNBRaw <- filteredFit(paste(Outcome,"~."),
                   datasetframe_train,
                   fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_Wilcoxon,
                     filtermethod.control=list(pvalue=0.05,limit= 50),
                     pca=FALSE
                   )

# With PCA
mNBPCA <- filteredFit(paste(Outcome,"~."),
                   datasetframe_train,
                   fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_Wilcoxon,
                     filtermethod.control=list(pvalue=0.05,limit= 50),
                     pca=TRUE
                   )

Now using the decorrelated data

mNBDecor <- filteredFit(paste(Outcome,"~."),
                   datasetframeDecor[[1]],
                    fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_Wilcoxon,
                     filtermethod.control=list(pvalue=0.05,limit= 50),
                     pca=FALSE
                   )

The testing set predictions comparison


# Predict the raw testing set
prRAW <- attr(predict(mNBRaw,datasetframe_test),"prob")

# Predict with PCA
prPCA <- attr(predict(mNBPCA,datasetframe_test),"prob")

# Predict the transformed dataset
prDecor <- attr(predict(mNBDecor,decor_test),"prob")


par(mfrow=c(1,3))
meanROCAUC <- numeric(3);
meanPCAROCAUC <- numeric(3);
classNames <- as.character(classNames)
for (theClass in classNames)
{
  classoutcomes <- 1*(datasetframe_test[,Outcome] == theClass)
  psRaw <- predictionStats_binary(cbind(classoutcomes,prRAW[,theClass]),
                                paste("Raw :",theClass),cex=0.75)
  pander::pander(psRaw$aucs)
  psPCA <- predictionStats_binary(cbind(classoutcomes,prPCA[,theClass]),
                                paste("PCA :",theClass),cex=0.75)
  pander::pander(psPCA$aucs)
  psDecor <- predictionStats_binary(cbind(classoutcomes,prDecor[,theClass]),
                                paste("GDSTM :",theClass),cex=0.75)
  pander::pander(psDecor$aucs)
  meanROCAUC <- meanROCAUC + psRaw$aucs;
  meanPCAROCAUC <- meanPCAROCAUC + psPCA$aucs;
}

Raw : 0

est lower upper
1 1 1

PCA : 0

est lower upper
0.9922 0.9829 1

GDSTM : 0

est lower upper
1 1 1

Raw : 1

est lower upper
0.9988 0.9978 0.9999

PCA : 1

est lower upper
0.9869 0.9733 1

GDSTM : 1

est lower upper
0.9993 0.9987 1

Raw : 2

est lower upper
0.9998 0.9994 1

PCA : 2

est lower upper
0.9926 0.9864 0.9987

GDSTM : 2

est lower upper
0.9997 0.9993 1

Raw : 3

est lower upper
0.9943 0.9855 1

PCA : 3

est lower upper
0.9869 0.9771 0.9968

GDSTM : 3

est lower upper
0.9987 0.9974 1

Raw : 4

est lower upper
0.9995 0.999 1

PCA : 4

est lower upper
0.9771 0.9531 1

GDSTM : 4

est lower upper
0.9997 0.9993 1

Raw : 5

est lower upper
0.999 0.9979 1

PCA : 5

est lower upper
0.9799 0.9593 1

GDSTM : 5

est lower upper
0.9988 0.9973 1

Raw : 6

est lower upper
0.9999 0.9998 1

PCA : 6

est lower upper
0.9844 0.9731 0.9957

GDSTM : 6

est lower upper
0.9999 0.9997 1

Raw : 7

est lower upper
0.9992 0.9984 1

PCA : 7

est lower upper
0.9804 0.9606 1

GDSTM : 7

est lower upper
0.998 0.9945 1

Raw : 8

est lower upper
1 1 1

PCA : 8

est lower upper
0.9914 0.98 1

GDSTM : 8

est lower upper
1 0.9999 1

Raw : 9

est lower upper
0.9998 0.9995 1

PCA : 9

est lower upper
0.9529 0.922 0.9838

GDSTM : 9

est lower upper
0.9994 0.9986 1

meanROCAUC <- meanROCAUC/length(classNames)
AllRocAUC <- meanROCAUC;
meanPCAROCAUC <- meanPCAROCAUC/length(classNames)
AllRocAUC <- rbind(AllRocAUC,meanPCAROCAUC);

Comparing FCA Options predictions

The following code runs the function and selecting some of the possible options:



# Change the maximum correlation goal

decortype[[2]] <- "AtThr"
system.time(datasetframeDecor[[2]] <- GDSTMDecorrelation(
  datasetframe_train,
  thr = correlationThreshold
  ))

user system elapsed 2.10 0.12 2.05

# Change the maximum correlation goal, and set to Robust Liner Models
decortype[[3]] <- "RLM_Pearson"
system.time(datasetframeDecor[[3]] <- GDSTMDecorrelation(
  datasetframe_train,
  type="RLM",
  method="pearson"))

user system elapsed 7.59 0.11 7.68

# Change the maximum correlation goal, and change to Spearman correlation
decortype[[4]] <- "LM_Spearman"
system.time(datasetframeDecor[[4]] <- GDSTMDecorrelation(
  datasetframe_train,
  type="LM",
  method="spearman"))

user system elapsed 2.94 0.11 2.87

# Change the maximum correlation goal, and set Spearman correlation with robust liner model
decortype[[5]] <- "RLM_Spearman"
system.time(datasetframeDecor[[5]] <- GDSTMDecorrelation(
  datasetframe_train,
  type="RLM",
  method="spearman"))

user system elapsed 8.11 0.05 8.27

# The following are for supervised basis learning

# Set the target class for association learning
decortype[[6]] <- "Sup_Default"
system.time(datasetframeDecor[[6]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome))

user system elapsed 1.48 0.08 1.44

# Change the maximum correlation goal
decortype[[7]] <- "Sup_AtThr"
system.time(datasetframeDecor[[7]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome))

user system elapsed 1.13 0.06 1.19

# Change the maximum correlation goal, and set to Robust Liner Models
decortype[[8]] <- "Sup_RLM_Pearson"
system.time(datasetframeDecor[[8]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  type="RLM",
  method="pearson"))

user system elapsed 8.05 0.07 7.96

# Change the maximum correlation goal, and change to Spearman correlation
decortype[[9]] <- "Sup_LM_Spearman"
system.time(datasetframeDecor[[9]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  type="LM",
  method="spearman"))

user system elapsed 4.03 0.09 3.94

# Change the maximum correlation goal, and set to Spearman correlation with robust liner model
decortype[[10]] <- "Sup_RLM_Spearman"
system.time(datasetframeDecor[[10]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  type="RLM",
  method="spearman"))

user system elapsed 8.58 0.16 8.67

# With user defined supervised basis 
decortype[[11]] <- "Sup_KS_RLM_Spearman"
baseKS <- names(univariate_KS(datasetframe_train,
                        Outcome=Outcome,
                        pvalue=0.20,
                        limit=0,
                        thr=correlationThreshold))

system.time(datasetframeDecor[[11]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  baseFeatures=baseKS,
  type="RLM",
  method="spearman"))

user system elapsed 10.21 0.11 10.31

Predict on all set of transformations

par(mfrow=c(2,5))

for (i in c(1:length(datasetframeDecor)))
{
  mNBDecor <- filteredFit(paste(Outcome,"~."),
                   datasetframeDecor[[i]],
                    fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_Wilcoxon,
                     filtermethod.control=list(pvalue=0.05,limit= 50),
                     pca=FALSE
                   )

  decor_test <- predictDecorrelate(datasetframeDecor[[i]],datasetframe_test)
  prDecor <- attr(predict(mNBDecor,decor_test),"prob")
  meanROCAUC <- numeric(3);
  for (theClass in classNames)
  {
    classoutcomes <- 1*(datasetframe_test[,Outcome] == theClass)
    psDecor <- predictionStats_binary(cbind(classoutcomes,prDecor[,theClass]),
                                  paste(decortype[[i]],theClass,sep=":"),cex=0.5)
    meanROCAUC <- meanROCAUC + psDecor$aucs;
  }
  meanROCAUC <- meanROCAUC/length(classNames)
  pander::pander(meanROCAUC)
  AllRocAUC <- rbind(AllRocAUC,meanROCAUC)

}

Default:0 Default:1 Default:2 Default:3 Default:4 Default:5 Default:6 Default:7 Default:8 Default:9

est lower upper
0.9993 0.9985 1

AtThr:0

AtThr:1 AtThr:2 AtThr:3 AtThr:4 AtThr:5 AtThr:6 AtThr:7 AtThr:8 AtThr:9

est lower upper
0.9991 0.9981 1

RLM_Pearson:0

RLM_Pearson:1 RLM_Pearson:2 RLM_Pearson:3 RLM_Pearson:4 RLM_Pearson:5 RLM_Pearson:6 RLM_Pearson:7 RLM_Pearson:8 RLM_Pearson:9

est lower upper
0.9994 0.9987 1

LM_Spearman:0

LM_Spearman:1 LM_Spearman:2 LM_Spearman:3 LM_Spearman:4 LM_Spearman:5 LM_Spearman:6 LM_Spearman:7 LM_Spearman:8 LM_Spearman:9

est lower upper
0.9992 0.9982 1

RLM_Spearman:0

RLM_Spearman:1 RLM_Spearman:2 RLM_Spearman:3 RLM_Spearman:4 RLM_Spearman:5 RLM_Spearman:6 RLM_Spearman:7 RLM_Spearman:8 RLM_Spearman:9

est lower upper
0.9987 0.9975 0.9999

Sup_Default:0

Sup_Default:1 Sup_Default:2 Sup_Default:3 Sup_Default:4 Sup_Default:5 Sup_Default:6 Sup_Default:7 Sup_Default:8 Sup_Default:9

est lower upper
0.9996 0.9991 1

Sup_AtThr:0

Sup_AtThr:1 Sup_AtThr:2 Sup_AtThr:3 Sup_AtThr:4 Sup_AtThr:5 Sup_AtThr:6 Sup_AtThr:7 Sup_AtThr:8 Sup_AtThr:9

est lower upper
0.9996 0.9991 1

Sup_RLM_Pearson:0

Sup_RLM_Pearson:1 Sup_RLM_Pearson:2 Sup_RLM_Pearson:3 Sup_RLM_Pearson:4 Sup_RLM_Pearson:5 Sup_RLM_Pearson:6 Sup_RLM_Pearson:7 Sup_RLM_Pearson:8 Sup_RLM_Pearson:9

est lower upper
0.9989 0.9975 1

Sup_LM_Spearman:0

Sup_LM_Spearman:1 Sup_LM_Spearman:2 Sup_LM_Spearman:3 Sup_LM_Spearman:4 Sup_LM_Spearman:5 Sup_LM_Spearman:6 Sup_LM_Spearman:7 Sup_LM_Spearman:8 Sup_LM_Spearman:9

est lower upper
0.9991 0.9981 0.9999

Sup_RLM_Spearman:0

Sup_RLM_Spearman:1 Sup_RLM_Spearman:2 Sup_RLM_Spearman:3 Sup_RLM_Spearman:4 Sup_RLM_Spearman:5 Sup_RLM_Spearman:6 Sup_RLM_Spearman:7 Sup_RLM_Spearman:8 Sup_RLM_Spearman:9

est lower upper
0.9992 0.998 1

Sup_KS_RLM_Spearman:0

Sup_KS_RLM_Spearman:1 Sup_KS_RLM_Spearman:2 Sup_KS_RLM_Spearman:3 Sup_KS_RLM_Spearman:4 Sup_KS_RLM_Spearman:5 Sup_KS_RLM_Spearman:6 Sup_KS_RLM_Spearman:7 Sup_KS_RLM_Spearman:8 Sup_KS_RLM_Spearman:9

est lower upper
0.9986 0.9968 1

The Bar plot of all AUC


rownames(AllRocAUC) <- c("Raw","PCA",unlist(decortype))
pander::pander(AllRocAUC)
  est lower upper
Raw 0.999 0.9977 1
PCA 0.9825 0.9668 0.9975
Default 0.9993 0.9985 1
AtThr 0.9991 0.9981 1
RLM_Pearson 0.9994 0.9987 1
LM_Spearman 0.9992 0.9982 1
RLM_Spearman 0.9987 0.9975 0.9999
Sup_Default 0.9996 0.9991 1
Sup_AtThr 0.9996 0.9991 1
Sup_RLM_Pearson 0.9989 0.9975 1
Sup_LM_Spearman 0.9991 0.9981 0.9999
Sup_RLM_Spearman 0.9992 0.998 1
Sup_KS_RLM_Spearman 0.9986 0.9968 1
par(mfrow=c(1,1))

bpROCAUC <- barPlotCiError(as.matrix(AllRocAUC),
                          metricname = "ROCAUC",
                          thesets = "ROC AUC",
                          themethod = rownames(AllRocAUC),
                          main = "ROC AUC",
                          offsets = c(0.5,1),
                          scoreDirection = ">",
                          ho=0.5,
                          args.legend = list(bg = "white",x="bottomright",inset=c(0.0,0),cex=0.75),
                          col = terrain.colors(nrow(AllRocAUC))
                          )

LS0tDQp0aXRsZTogIkZDQSBhbmQgdGhlIEdEU1RNIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KIyMgR0RTVE0gRGVjb3JyZWxhdGlvbiBvbiB0aGUgbXVsdGlwbGUgZmVhdHVyZXMgRGF0YXNldA0KDQpUaGlzIGRvY3VtZW50IGRlc2NyaWJlcyB0aGUgdXNlIG9mIHRoZSAqKkZSRVNBLkNBRDo6R0RTVE1EZWNvcnJlbGF0aW9uKCkqKiBmdW5jdGlvbiB0aGF0IHJ1bnMgdGhlIGZlYXR1cmUgY29ycmVsYXRpb24gYW5hbHlzaXMgKCoqRkNBKiopIGFsZ29yaXRobSBvbiB0aGUgbWZlYXQgZGF0YSBzZXQ6DQoNCj4gPGh0dHBzOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9kYXRhc2V0cy9NdWx0aXBsZStGZWF0dXJlcz4NCj4NCj4gTS4gdmFuIEJyZXVrZWxlbiwgUi5QLlcuIER1aW4sIEQuTS5KLiBUYXgsIGFuZCBKLkUuIGRlbiBIYXJ0b2csIEhhbmR3cml0dGVuIGRpZ2l0IHJlY29nbml0aW9uIGJ5IGNvbWJpbmVkIGNsYXNzaWZpZXJzLCBLeWJlcm5ldGlrYSwgdm9sLiAzNCwgbm8uIDQsIDE5OTgsIDM4MS0zODYuDQoNClRoaXMgc2NyaXAgdXNlcyBGUkVTQS5DQUQgYW5kIG1sYmVuY2ggUiBwYWNrYWdlczoNCg0KYGBge3IgZnVuY3Rpb25zLGVjaG8gPSBUUlVFIH0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChjb2xsYXBzZSA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLGNvbW1lbnQgPSAiIz4iKQ0KDQpsaWJyYXJ5KCJGUkVTQS5DQUQiKQ0KbGlicmFyeShtbGJlbmNoKQ0KDQpvcCA8LSBwYXIobm8ucmVhZG9ubHkgPSBUUlVFKQ0KDQpgYGANCg0KSSdsbCBsb2FkIHRoZSBtZmVhdCBkYXRhIHNldA0KDQpgYGB7cn0NCm1mZWF0IDwtIHJlYWQuZGVsaW0oIi4uL0RhdGEvbWZlYXQudHh0Iiwgc3RyaW5nc0FzRmFjdG9ycz1UUlVFKQ0KbWZlYXQkSUQgPC0gTlVMTA0KcHJpbnQodGFibGUobWZlYXQkQ2xhc3MpKQ0KDQoNCmBgYA0KDQpTZXR0aW5nIHNvbWUgdmFyaWFibGVzIGZvciBkb3duc3RyZWFtIGFuYWx5c2lzDQoNCmBgYHtyfQ0Kc3R1ZHlOYW1lID0gIm1mZWF0Ig0KZGF0YXNldGZyYW1lIDwtIG1mZWF0DQpPdXRjb21lIDwtICJDbGFzcyINCg0KdHJhaW5GcmFjdGlvbiA9IDAuNTANCmNvcnJlbGF0aW9uVGhyZXNob2xkID0gMC42DQpmZWF0bmFtZXMgPC0gY29sbmFtZXMoZGF0YXNldGZyYW1lKVtjb2xuYW1lcyhkYXRhc2V0ZnJhbWUpICE9IE91dGNvbWVdDQoNCmBgYA0KDQpTZXR0aW5nIHRoZSBUcmFpbmluZyBhbmQgVGVzdGluZyBzZXRzDQoNCmBgYHtyLCByZXN1bHRzID0gImFzaXMiLCBkcGk9NjAwLCBmaWcuaGVpZ2h0PSA2LjAsIGZpZy53aWR0aD0gOC4wfQ0KDQp0YiA8LSB0YWJsZShkYXRhc2V0ZnJhbWVbLE91dGNvbWVdKQ0KY2xhc3NOYW1lcyA8LSB1bmlxdWUoZGF0YXNldGZyYW1lWyxPdXRjb21lXSkNCg0KYWxscm93Q2xhc3MgPC0gZGF0YXNldGZyYW1lWyxPdXRjb21lXQ0KbmFtZXMoYWxscm93Q2xhc3MpIDwtIHJvd25hbWVzKGRhdGFzZXRmcmFtZSkNCg0KdHJhaW5zaXplIDwtIHRyYWluRnJhY3Rpb24qbWluKHRiKTsNCnRyYWluU2FtcGxlcyA8LSBOVUxMOw0KZm9yICh0aGVDbGFzcyBpbiBjbGFzc05hbWVzKQ0Kew0KICBjbGFzc1NhbXBsZSA8LSBhbGxyb3dDbGFzc1thbGxyb3dDbGFzcyA9PSB0aGVDbGFzc10NCiAgdHJhaW5TYW1wbGVzIDwtIGModHJhaW5TYW1wbGVzLG5hbWVzKGNsYXNzU2FtcGxlW3NhbXBsZShsZW5ndGgoY2xhc3NTYW1wbGUpLHRyYWluc2l6ZSldKSkNCn0NCg0KDQpkYXRhc2V0ZnJhbWVfdHJhaW4gPC0gZGF0YXNldGZyYW1lW3RyYWluU2FtcGxlcyxdDQp0ZXN0U2FtcGxlcyA8LSAhKHJvd25hbWVzKGRhdGFzZXRmcmFtZSkgJWluJSB0cmFpblNhbXBsZXMpDQpkYXRhc2V0ZnJhbWVfdGVzdCA8LSBkYXRhc2V0ZnJhbWVbdGVzdFNhbXBsZXMsXQ0KDQpvdXRjb21lcyA8LSBkYXRhc2V0ZnJhbWVfdHJhaW5bLE91dGNvbWVdDQoNCnBhbmRlcjo6cGFuZGVyKHRhYmxlKGRhdGFzZXRmcmFtZVssT3V0Y29tZV0pLGNhcHRpb249IkFsbCIpDQpwYW5kZXI6OnBhbmRlcih0YWJsZShkYXRhc2V0ZnJhbWVfdHJhaW5bLE91dGNvbWVdKSxjYXB0aW9uPSJUcmFpbmluZyIpDQpwYW5kZXI6OnBhbmRlcih0YWJsZShkYXRhc2V0ZnJhbWVfdGVzdFssT3V0Y29tZV0pLGNhcHRpb249IlRlc3RpbmciKQ0KDQoNCmBgYA0KDQojIyBGQ0Egd2l0aCBkZWZhdWx0IHBhcmFtZXRlcnMNCg0KVGhlIGRlZmF1bHQgcGFyYW1ldGVycyB3aWxsIGNvbXB1dGUgdGhlIHRyYW5zZm9ybWF0aW9uIG1hdHJpeCB3aXRoIGEgbWF4aW11bSBjb3JyZWxhdGlvbiBnb2FsIG9mIDAuOCB1c2luZyBmYXN0IG1hdHJpeCBtdWx0aXBsaWNhdGlvbiB3aXRoIFBlYXJzb24gY29ycmVsYXRpb24gYW5kIGxpbmVhciBtb2RlbHMgZXN0aW1hdGlvbi4NCg0KRGVmYXVsdCBQYXJhbWV0ZXJzOiB0aHI9MC44MCxtZXRob2Q9ImZhc3QiLHR5cGU9IkxNIg0KDQpgYGB7ciwgcmVzdWx0cyA9ICJhc2lzIiwgd2FybmluZyA9IEZBTFNFLCBkcGk9NjAwLCBmaWcuaGVpZ2h0PSA2LjAsIGZpZy53aWR0aD0gOC4wfQ0KDQoNCmRhdGFzZXRmcmFtZURlY29yPC1saXN0KCk7DQpkZWNvcnR5cGUgPC0gbGlzdCgpOw0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzFdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oZGF0YXNldGZyYW1lX3RyYWluKSkNCmRlY29ydHlwZVtbMV1dIDwtICJEZWZhdWx0Ig0KDQoNCmBgYA0KDQojIyMgVHJhbnNmb3JtaW5nIHRoZSB0ZXN0aW5nIHNldA0KDQpUbyBtYWtlIHRlc3QgcHJlZGljdGlvbnMgd2UgbmVlZCB0byB0cmFuc2Zvcm0gdGhlIHRlc3Rpbmcgc2V0LiBUaGlzIGlzIGRvbmUgdXNpbmcgdGhlICoqRlJFU0EuQ0FEOjpwcmVkaWN0RGVjb3JyZWxhdGUoKSoqIGZ1bmN0aW9uDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KIyBUcmFuc2Zvcm0gdGhlIHRlc3Rpbmcgc2V0DQpkZWNvcl90ZXN0IDwtIHByZWRpY3REZWNvcnJlbGF0ZShkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLGRhdGFzZXRmcmFtZV90ZXN0KQ0KDQpgYGANCg0KIyMgSGVhdCBtYXBzIG9mIHRoZSBjb3JyZWxhdGlvbiBtYXRyaWNlcw0KDQpIZXJlIGFyZSB0aGUgaGVhdCBtYXBzIG9mIHRoZSBjb3JyZWxhdGlvbiBtYXRyaWNlcyBiZWZvcmUgYW5kIGFmdGVyIGRlY29ycmVsYXRpb24gb24gdGhlIHRlc3Rpbmcgc2V0DQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KDQpmZWF0bmFtZXMgPC0gYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLCJ2YXJpbmNsdWRlZCIpDQpjb3JtYXQgPC0gY29yKGRhdGFzZXRmcmFtZV90ZXN0WyxmZWF0bmFtZXNdLG1ldGhvZD0icGVhcnNvbiIpDQpncGxvdHM6OmhlYXRtYXAuMihhYnMoY29ybWF0KSwNCiAgICAgICAgICAgICAgICAgIHRyYWNlID0gIm5vbmUiLA0KICAgICAgICAgICAgICAgICAgc2NhbGUgPSAibm9uZSIsDQogICAgICAgICAgICAgICAgICBtYXIgPSBjKDEwLDEwKSwNCiAgICAgICAgICAgICAgICAgIGNvbD1yZXYoaGVhdC5jb2xvcnMoNSkpLA0KICAgICAgICAgICAgICAgICAgbWFpbiA9IHBhc3RlKCJSYXcgQ29ycmVsYXRpb246IixzdHVkeU5hbWUpLA0KICAgICAgICAgICAgICAgICAgY2V4Um93ID0gMC43NSwNCiAgICAgICAgICAgICAgICAgIGNleENvbCA9IDAuNzUsDQogICAgICAgICAgICAgICAgICBrZXkudGl0bGU9TkEsDQogICAgICAgICAgICAgICAgICBrZXkueGxhYj0iUGVhcnNvbiBDb3JyZWxhdGlvbiIsDQogICAgICAgICAgICAgICAgICB4bGFiPSJGZWF0dXJlIiwgeWxhYj0iRmVhdHVyZSIpDQoNCg0KZmVhdG5hbWVzIDwtIGNvbG5hbWVzKGF0dHIoZGF0YXNldGZyYW1lRGVjb3JbWzFdXSwiR0RTVE0iKSkNCmNvcm1hdCA8LSBjb3IoZGVjb3JfdGVzdFssZmVhdG5hbWVzXSxtZXRob2Q9InBlYXJzb24iKQ0KY29ybWF0W2lzLm5hKGNvcm1hdCldIDwtIDA7DQpncGxvdHM6OmhlYXRtYXAuMihhYnMoY29ybWF0KSwNCiAgICAgICAgICAgICAgICAgIHRyYWNlID0gIm5vbmUiLA0KICAgICAgICAgICAgICAgICAgc2NhbGUgPSAibm9uZSIsDQogICAgICAgICAgICAgICAgICBtYXIgPSBjKDEwLDEwKSwNCiAgICAgICAgICAgICAgICAgIGNvbD1yZXYoaGVhdC5jb2xvcnMoNSkpLA0KICAgICAgICAgICAgICAgICAgbWFpbiA9IHBhc3RlKCJBZnRlciBkZWNvcnJlbGF0aW9uOiIsc3R1ZHlOYW1lKSwNCiAgICAgICAgICAgICAgICAgIGNleFJvdyA9IDAuNzUsDQogICAgICAgICAgICAgICAgICBjZXhDb2wgPSAwLjc1LA0KICAgICAgICAgICAgICAgICAga2V5LnRpdGxlPU5BLA0KICAgICAgICAgICAgICAgICAga2V5LnhsYWI9IlBlYXJzb24gQ29ycmVsYXRpb24iLA0KICAgICAgICAgICAgICAgICAgeGxhYj0iVHJhbnNmb3JtZWQgRmVhdHVyZSIsIHlsYWI9IlRyYW5zZm9ybWVkIEZlYXR1cmUiKQ0KDQoNCmBgYA0KDQpUaGUgKipHRFNUTURlY29ycmVsYXRpb24qKiBmdW5jdGlvbiByZXR1cm5zIGEgZGF0YSBmcmFtZSB3aXRoIHRoZSBmb2xsb3dpbmcgY29sdW1uIG5hbWVzOg0KDQpUaGUgb3V0cHV0IGZlYXR1cmVzIGFmdGVyIHRyYW5zZm9ybWF0aW9uIHdpbGwgYmUgbmFtZWQgYWZ0ZXIgdGhlIG9yaWdpbmFsIG5hbWVzIGFuZDoNCg0KLSAgIFRoZSBuYW1lIHdpbGwgYmUgdW5hbHRlcmVkIGlmIHRoZWlyIG1heGltdW0gY29ycmVsYXRpb24gdG8gb3RoZXIgZmVhdHVyZXMgd2FzIGxvd2VyIHRoYW4gdGhlIHRocmVzaG9sZC4NCg0KLSAgIFRoZSBuYW1lIHdpbGwgaGF2ZSB0aGUgIkJhXF8iIHByZWZpeCBpcyB0aGUgZmVhdHVyZSB3YXMgY29ycmVsYXRlZCBidXQgdXNlZCBhcyB1bmFsdGVyZWQgYmFzaXMNCg0KLSAgIFRoZSBuYW1lIHdpbGwgaGF2ZSB0aGUgIkRlXF8iIHByZWZpeCBpcyB0aGUgZmVhdHVyZSB3YXMgb3JpZ2luYWwgY29ycmVsYXRlZCBhbmQgaXRzIGNvcnJlbGF0aW9uIHRvICJCYVxfIiBmZWF0dXJlcyBoYXMgYmVlbiByZW1vdmVkLg0KDQpGdXJ0aGVybW9yZSwgdGhlIHJldHVybmVkIGRhdGEgZnJhbWUgd2lsbCBoYXZlIHRoZSBmb2xsb3dpbmcgYXR0cmlidXRlczoNCg0KMSkgIFRyYW5zZm9ybWF0aW9uIG1hdHJpeDogIipHRFNUTSoiDQoNCjIpICBGZWF0dXJlczoNCg0KICAgIDEuICAiKmZzb2NyZSoiDQoNCiAgICAyLiAgIip2YXJpbmNsdWRlZCoiDQoNCiAgICAzLiAgIip0b3BGZWF0dXJlcyoiDQoNCjMpICBVbmFsdGVyZWQgQmFzaXM6DQoNCiAgICAxLiAgIipiYXNlRmVhdHVyZXMqIg0KDQogICAgICAgIDEuICAiKmNvcnJlbGF0ZWRUb0Jhc2UqIg0KDQogICAgMi4gICIqQWJhc2VGZWF0dXJlcyoiDQoNCiMjIyBHRFNUTQ0KDQpUaGUgIipHRFNUTSoqKiIqKiBhdHRyaWJ1dGUgc3RvcmVzIHRoZSBzcGF0aWFsIHRyYW5zZm9ybWF0aW9uIG1hdHJpeC4gVGhlIG1hdHJpeCBvbmx5IGluY2x1ZGVzIGNvbnRpbnVvdXMgZmVhdHVyZXMgdGhhdCBoYWQgc29tZSBjb3JyZWxhdGlvbiBncmVhdGVyIHRoYW4gdGhlIHRocmVzaG9sZA0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCB3YXJuaW5nID0gRkFMU0UsIGRwaT02MDAsIGZpZy5oZWlnaHQ9IDYuMCwgZmlnLndpZHRoPSA4LjB9DQoNCg0KIyMgVGhlIFNwYXRpYWwgVHJhbnNmb3JtYXRpb24gTWF0cml4Og0KR0RTVE0gPC0gYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLCJHRFNUTSIpDQoNCiMjIFRoZSBoZWF0bWFwIG9mIHRoZSBtYXRyaXgNCmdwbG90czo6aGVhdG1hcC4yKDEqKGFicyhHRFNUTSkgPiAwKSwNCiAgICAgICAgICAgICAgICAgIHRyYWNlID0gIm5vbmUiLA0KICAgICAgICAgICAgICAgICAgbWFyID0gYygxMCwxMCksDQogICAgICAgICAgICAgICAgICBjb2w9cmV2KGhlYXQuY29sb3JzKDIpKSwNCiAgICAgICAgICAgICAgICAgIG1haW4gPSBwYXN0ZSgiR0RTVE0gTWF0cml4OiIsc3R1ZHlOYW1lKSwNCiAgICAgICAgICAgICAgICAgIGNleFJvdyA9IDAuNywNCiAgICAgICAgICAgICAgICAgIGNleENvbCA9IDAuNywNCiAgICAgICAgICAgICAgICAgIGJyZWFrcyA9IGMoMCwwLjUsMSksDQogICAgICAgICAgICAgICAgICBrZXkudGl0bGU9TkEsDQogICAgICAgICAgICAgICAgICBrZXkueGxhYj0ifGJldGF8ID4gMCIsDQogICAgICAgICAgICAgICAgICB4bGFiPSJHRFNUTSBGZWF0dXJlIiwgeWxhYj0iSW5wdXQgRmVhdHVyZSIpDQoNCg0KDQpgYGANCg0KIyMjIFRoZSBGZWF0dXJlIEluZGV4IFNjb3JlDQoNClRoZSAqKkZDQSoqIGFuYWx5c2lzIG9mIHRoZSBkYXRhIGZlYXR1cmVzIGFyZSBzdG9yZWQgaW4gdGhyZWUgYXR0cmlidXRlczogIip2YXJpbmNsdWRlZCoiLCAiKnRvcEZlYXR1cmVzKiIsIGFuZCAiKmZzY29yZSoiLg0KDQotICAgInZhcmluY2x1ZGVkIiByZXR1cm5zIHRoZSBsaXN0IG9mIGNvbnRpbnVvdXMgZmVhdHVyZXMgdGhhdCB3ZXJlIGRlY29ycmVsYXRlZA0KDQotICAgInRvcEZlYXR1cmVzIiByZXR1cm5zIHRoZSBmZWF0dXJlcyB0aGF0IGF0IHNvbWUgcG9pbnQgd2VyZSB1c2VkIGFzIGluZGVwZW5kZW50IHZhcmlhYmxlcyBpbnNpZGUgdGhlIGxpbmVhciBtb2RlbHMuDQoNCi0gICAiZnNjb3JlIiA6IHJldHVybnMgYSBuYW1lZCB2ZWN0b3Igd2l0aCB0aGUgdG90YWwgZmVhdHVyZSBzY29yZSwgJEZfaiQsIG9mIHRoZSBhbmFseXplZCBmZWF0dXJlcy4NCg0KJCQNCkZfaj3iiJFfe2594oiRX3tp4oiIQl57bn1fe2p9fXzPgV97aSxqfXxeMih8z4Ffe2ksan18Ps+BX3t0aH0pLH4gXGZvcmFsbCBqIFxpbiBJbmQNCiQkDQoNCiQkDQpGX2o9Rl9qLeKIkV97bn18z4Ffe0Jebl9qLGp9fF4yKSx+IFxmb3JhbGwgaiBcaW4gRGVwDQokJA0KDQp3aGVyZSAkQl5uX2okIGlzIHRoZSBzZXQgb2YgZmVhdHVyZXMgc3RhdGlzdGljYWxseSBhc3NvY2lhdGVkIHdpdGggZmVhdHVyZSAqaiogYXQgaXRlcmF0aW9uICpuLCogJM+BX3tpLGp9JCBpcyB0aGUgY29ycmVsYXRpb24gYmV0d2VlbiBmZWF0dXJlcyAqaSxqKiwgYW5kICTPgV97dGh9JCBpcyB0aGUgY29ycmVsYXRpb24gZ29hbCwgJHtJbmQsIERlcH0kIGFyZSB0aGUgc2V0IG9mIGluZGVwZW5kZW50IGFuZCBkZXBlbmRlbnQgZmVhdHVyZXMgcmVzcGVjdGl2ZWx5LiBJbiBvdGhlciB3b3JkcywgdGhlICIqZnNjb3JlIiogaW5kaWNhdGVzIHRoZSBkZWdyZWUgb2YgdG90YWwgYXNzb2NpYXRpb24gb2YgImluZGVwZW5kZW50IiBmZWF0dXJlcyB0byBkZXBlbmRlbnQgZmVhdHVyZXMuDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KDQpmc2NvcmUgPC0gYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLCJmc2NvcmUiKSANCmZzY29yZSA8LSBmc2NvcmVbb3JkZXIoLWZzY29yZSldOw0KYmFycGxvdChmc2NvcmUsbGFzPTIsY2V4Lm5hbWVzID0gMC42KQ0KDQpgYGANCg0KIyMgTWFjaGluZSBMZWFybmluZyBhbmQgR0RTVE0NCg0KVHJhaW4gYSBzaW1wbGUgTkIgbW9kZWwgb24gdGhlIHJhdyBkYXRhc2V0DQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCm1OQlJhdyA8LSBmaWx0ZXJlZEZpdChwYXN0ZShPdXRjb21lLCJ+LiIpLA0KICAgICAgICAgICAgICAgICAgIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgICAgICAgICAgICAgICAgICBmaXRtZXRob2Q9TkFJVkVfQkFZRVMsDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2Q9dW5pdmFyaWF0ZV9XaWxjb3hvbiwNCiAgICAgICAgICAgICAgICAgICAgIGZpbHRlcm1ldGhvZC5jb250cm9sPWxpc3QocHZhbHVlPTAuMDUsbGltaXQ9IDUwKSwNCiAgICAgICAgICAgICAgICAgICAgIHBjYT1GQUxTRQ0KICAgICAgICAgICAgICAgICAgICkNCg0KIyBXaXRoIFBDQQ0KbU5CUENBIDwtIGZpbHRlcmVkRml0KHBhc3RlKE91dGNvbWUsIn4uIiksDQogICAgICAgICAgICAgICAgICAgZGF0YXNldGZyYW1lX3RyYWluLA0KICAgICAgICAgICAgICAgICAgIGZpdG1ldGhvZD1OQUlWRV9CQVlFUywNCiAgICAgICAgICAgICAgICAgICAgIGZpbHRlcm1ldGhvZD11bml2YXJpYXRlX1dpbGNveG9uLA0KICAgICAgICAgICAgICAgICAgICAgZmlsdGVybWV0aG9kLmNvbnRyb2w9bGlzdChwdmFsdWU9MC4wNSxsaW1pdD0gNTApLA0KICAgICAgICAgICAgICAgICAgICAgcGNhPVRSVUUNCiAgICAgICAgICAgICAgICAgICApDQoNCg0KYGBgDQoNCk5vdyB1c2luZyB0aGUgZGVjb3JyZWxhdGVkIGRhdGENCg0KYGBge3IgcmVzdWx0cyA9ICJhc2lzIiwgd2FybmluZyA9IEZBTFNFLCBkcGk9NjAwLCBmaWcuaGVpZ2h0PSA2LjAsIGZpZy53aWR0aD0gOC4wfQ0KbU5CRGVjb3IgPC0gZmlsdGVyZWRGaXQocGFzdGUoT3V0Y29tZSwifi4iKSwNCiAgICAgICAgICAgICAgICAgICBkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLA0KICAgICAgICAgICAgICAgICAgICBmaXRtZXRob2Q9TkFJVkVfQkFZRVMsDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2Q9dW5pdmFyaWF0ZV9XaWxjb3hvbiwNCiAgICAgICAgICAgICAgICAgICAgIGZpbHRlcm1ldGhvZC5jb250cm9sPWxpc3QocHZhbHVlPTAuMDUsbGltaXQ9IDUwKSwNCiAgICAgICAgICAgICAgICAgICAgIHBjYT1GQUxTRQ0KICAgICAgICAgICAgICAgICAgICkNCg0KDQoNCg0KYGBgDQoNCiMjIyBUaGUgdGVzdGluZyBzZXQgcHJlZGljdGlvbnMgY29tcGFyaXNvbg0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCB3YXJuaW5nID0gRkFMU0UsIGRwaT02MDAsIGZpZy5oZWlnaHQ9IDQuMCwgZmlnLndpZHRoPSAxMi4wfQ0KDQojIFByZWRpY3QgdGhlIHJhdyB0ZXN0aW5nIHNldA0KcHJSQVcgPC0gYXR0cihwcmVkaWN0KG1OQlJhdyxkYXRhc2V0ZnJhbWVfdGVzdCksInByb2IiKQ0KDQojIFByZWRpY3Qgd2l0aCBQQ0ENCnByUENBIDwtIGF0dHIocHJlZGljdChtTkJQQ0EsZGF0YXNldGZyYW1lX3Rlc3QpLCJwcm9iIikNCg0KIyBQcmVkaWN0IHRoZSB0cmFuc2Zvcm1lZCBkYXRhc2V0DQpwckRlY29yIDwtIGF0dHIocHJlZGljdChtTkJEZWNvcixkZWNvcl90ZXN0KSwicHJvYiIpDQoNCg0KcGFyKG1mcm93PWMoMSwzKSkNCm1lYW5ST0NBVUMgPC0gbnVtZXJpYygzKTsNCm1lYW5QQ0FST0NBVUMgPC0gbnVtZXJpYygzKTsNCmNsYXNzTmFtZXMgPC0gYXMuY2hhcmFjdGVyKGNsYXNzTmFtZXMpDQpmb3IgKHRoZUNsYXNzIGluIGNsYXNzTmFtZXMpDQp7DQogIGNsYXNzb3V0Y29tZXMgPC0gMSooZGF0YXNldGZyYW1lX3Rlc3RbLE91dGNvbWVdID09IHRoZUNsYXNzKQ0KICBwc1JhdyA8LSBwcmVkaWN0aW9uU3RhdHNfYmluYXJ5KGNiaW5kKGNsYXNzb3V0Y29tZXMscHJSQVdbLHRoZUNsYXNzXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlKCJSYXcgOiIsdGhlQ2xhc3MpLGNleD0wLjc1KQ0KICBwYW5kZXI6OnBhbmRlcihwc1JhdyRhdWNzKQ0KICBwc1BDQSA8LSBwcmVkaWN0aW9uU3RhdHNfYmluYXJ5KGNiaW5kKGNsYXNzb3V0Y29tZXMscHJQQ0FbLHRoZUNsYXNzXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlKCJQQ0EgOiIsdGhlQ2xhc3MpLGNleD0wLjc1KQ0KICBwYW5kZXI6OnBhbmRlcihwc1BDQSRhdWNzKQ0KICBwc0RlY29yIDwtIHByZWRpY3Rpb25TdGF0c19iaW5hcnkoY2JpbmQoY2xhc3NvdXRjb21lcyxwckRlY29yWyx0aGVDbGFzc10pLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZSgiR0RTVE0gOiIsdGhlQ2xhc3MpLGNleD0wLjc1KQ0KICBwYW5kZXI6OnBhbmRlcihwc0RlY29yJGF1Y3MpDQogIG1lYW5ST0NBVUMgPC0gbWVhblJPQ0FVQyArIHBzUmF3JGF1Y3M7DQogIG1lYW5QQ0FST0NBVUMgPC0gbWVhblBDQVJPQ0FVQyArIHBzUENBJGF1Y3M7DQp9DQptZWFuUk9DQVVDIDwtIG1lYW5ST0NBVUMvbGVuZ3RoKGNsYXNzTmFtZXMpDQpBbGxSb2NBVUMgPC0gbWVhblJPQ0FVQzsNCm1lYW5QQ0FST0NBVUMgPC0gbWVhblBDQVJPQ0FVQy9sZW5ndGgoY2xhc3NOYW1lcykNCkFsbFJvY0FVQyA8LSByYmluZChBbGxSb2NBVUMsbWVhblBDQVJPQ0FVQyk7DQoNCmBgYA0KDQojIyBDb21wYXJpbmcgRkNBIE9wdGlvbnMgcHJlZGljdGlvbnMNCg0KVGhlIGZvbGxvd2luZyBjb2RlIHJ1bnMgdGhlIGZ1bmN0aW9uIGFuZCBzZWxlY3Rpbmcgc29tZSBvZiB0aGUgcG9zc2libGUgb3B0aW9uczoNCg0KYGBge3IsIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KDQojIENoYW5nZSB0aGUgbWF4aW11bSBjb3JyZWxhdGlvbiBnb2FsDQoNCmRlY29ydHlwZVtbMl1dIDwtICJBdFRociINCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1syXV0gPC0gR0RTVE1EZWNvcnJlbGF0aW9uKA0KICBkYXRhc2V0ZnJhbWVfdHJhaW4sDQogIHRociA9IGNvcnJlbGF0aW9uVGhyZXNob2xkDQogICkpDQoNCiMgQ2hhbmdlIHRoZSBtYXhpbXVtIGNvcnJlbGF0aW9uIGdvYWwsIGFuZCBzZXQgdG8gUm9idXN0IExpbmVyIE1vZGVscw0KZGVjb3J0eXBlW1szXV0gPC0gIlJMTV9QZWFyc29uIg0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzNdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgdHlwZT0iUkxNIiwNCiAgbWV0aG9kPSJwZWFyc29uIikpDQoNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIGNoYW5nZSB0byBTcGVhcm1hbiBjb3JyZWxhdGlvbg0KZGVjb3J0eXBlW1s0XV0gPC0gIkxNX1NwZWFybWFuIg0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzRdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgdHlwZT0iTE0iLA0KICBtZXRob2Q9InNwZWFybWFuIikpDQoNCiMgQ2hhbmdlIHRoZSBtYXhpbXVtIGNvcnJlbGF0aW9uIGdvYWwsIGFuZCBzZXQgU3BlYXJtYW4gY29ycmVsYXRpb24gd2l0aCByb2J1c3QgbGluZXIgbW9kZWwNCmRlY29ydHlwZVtbNV1dIDwtICJSTE1fU3BlYXJtYW4iDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbNV1dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICB0eXBlPSJSTE0iLA0KICBtZXRob2Q9InNwZWFybWFuIikpDQoNCg0KIyBUaGUgZm9sbG93aW5nIGFyZSBmb3Igc3VwZXJ2aXNlZCBiYXNpcyBsZWFybmluZw0KDQojIFNldCB0aGUgdGFyZ2V0IGNsYXNzIGZvciBhc3NvY2lhdGlvbiBsZWFybmluZw0KZGVjb3J0eXBlW1s2XV0gPC0gIlN1cF9EZWZhdWx0Ig0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzZdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgT3V0Y29tZT1PdXRjb21lKSkNCg0KDQojIENoYW5nZSB0aGUgbWF4aW11bSBjb3JyZWxhdGlvbiBnb2FsDQpkZWNvcnR5cGVbWzddXSA8LSAiU3VwX0F0VGhyIg0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzddXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgT3V0Y29tZT1PdXRjb21lKSkNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIHNldCB0byBSb2J1c3QgTGluZXIgTW9kZWxzDQpkZWNvcnR5cGVbWzhdXSA8LSAiU3VwX1JMTV9QZWFyc29uIg0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzhdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgT3V0Y29tZT1PdXRjb21lLA0KICB0eXBlPSJSTE0iLA0KICBtZXRob2Q9InBlYXJzb24iKSkNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIGNoYW5nZSB0byBTcGVhcm1hbiBjb3JyZWxhdGlvbg0KZGVjb3J0eXBlW1s5XV0gPC0gIlN1cF9MTV9TcGVhcm1hbiINCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1s5XV0gPC0gR0RTVE1EZWNvcnJlbGF0aW9uKA0KICBkYXRhc2V0ZnJhbWVfdHJhaW4sDQogIE91dGNvbWU9T3V0Y29tZSwNCiAgdHlwZT0iTE0iLA0KICBtZXRob2Q9InNwZWFybWFuIikpDQoNCiMgQ2hhbmdlIHRoZSBtYXhpbXVtIGNvcnJlbGF0aW9uIGdvYWwsIGFuZCBzZXQgdG8gU3BlYXJtYW4gY29ycmVsYXRpb24gd2l0aCByb2J1c3QgbGluZXIgbW9kZWwNCmRlY29ydHlwZVtbMTBdXSA8LSAiU3VwX1JMTV9TcGVhcm1hbiINCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1sxMF1dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICBPdXRjb21lPU91dGNvbWUsDQogIHR5cGU9IlJMTSIsDQogIG1ldGhvZD0ic3BlYXJtYW4iKSkNCg0KDQojIFdpdGggdXNlciBkZWZpbmVkIHN1cGVydmlzZWQgYmFzaXMgDQpkZWNvcnR5cGVbWzExXV0gPC0gIlN1cF9LU19STE1fU3BlYXJtYW4iDQpiYXNlS1MgPC0gbmFtZXModW5pdmFyaWF0ZV9LUyhkYXRhc2V0ZnJhbWVfdHJhaW4sDQogICAgICAgICAgICAgICAgICAgICAgICBPdXRjb21lPU91dGNvbWUsDQogICAgICAgICAgICAgICAgICAgICAgICBwdmFsdWU9MC4yMCwNCiAgICAgICAgICAgICAgICAgICAgICAgIGxpbWl0PTAsDQogICAgICAgICAgICAgICAgICAgICAgICB0aHI9Y29ycmVsYXRpb25UaHJlc2hvbGQpKQ0KDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbMTFdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgT3V0Y29tZT1PdXRjb21lLA0KICBiYXNlRmVhdHVyZXM9YmFzZUtTLA0KICB0eXBlPSJSTE0iLA0KICBtZXRob2Q9InNwZWFybWFuIikpDQoNCg0KYGBgDQoNCiMjIFByZWRpY3Qgb24gYWxsIHNldCBvZiB0cmFuc2Zvcm1hdGlvbnMNCg0KYGBge3IgUk9DIFBMT1RTLCBmaWcuaGVpZ2h0PTMuMCwgZmlnLndpZHRoPTEyLCB3YXJuaW5nPUZBTFNFLCBkcGk9NjAwLCByZXN1bHRzPSJhc2lzIn0NCnBhcihtZnJvdz1jKDIsNSkpDQoNCmZvciAoaSBpbiBjKDE6bGVuZ3RoKGRhdGFzZXRmcmFtZURlY29yKSkpDQp7DQogIG1OQkRlY29yIDwtIGZpbHRlcmVkRml0KHBhc3RlKE91dGNvbWUsIn4uIiksDQogICAgICAgICAgICAgICAgICAgZGF0YXNldGZyYW1lRGVjb3JbW2ldXSwNCiAgICAgICAgICAgICAgICAgICAgZml0bWV0aG9kPU5BSVZFX0JBWUVTLA0KICAgICAgICAgICAgICAgICAgICAgZmlsdGVybWV0aG9kPXVuaXZhcmlhdGVfV2lsY294b24sDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2QuY29udHJvbD1saXN0KHB2YWx1ZT0wLjA1LGxpbWl0PSA1MCksDQogICAgICAgICAgICAgICAgICAgICBwY2E9RkFMU0UNCiAgICAgICAgICAgICAgICAgICApDQoNCiAgZGVjb3JfdGVzdCA8LSBwcmVkaWN0RGVjb3JyZWxhdGUoZGF0YXNldGZyYW1lRGVjb3JbW2ldXSxkYXRhc2V0ZnJhbWVfdGVzdCkNCiAgcHJEZWNvciA8LSBhdHRyKHByZWRpY3QobU5CRGVjb3IsZGVjb3JfdGVzdCksInByb2IiKQ0KICBtZWFuUk9DQVVDIDwtIG51bWVyaWMoMyk7DQogIGZvciAodGhlQ2xhc3MgaW4gY2xhc3NOYW1lcykNCiAgew0KICAgIGNsYXNzb3V0Y29tZXMgPC0gMSooZGF0YXNldGZyYW1lX3Rlc3RbLE91dGNvbWVdID09IHRoZUNsYXNzKQ0KICAgIHBzRGVjb3IgPC0gcHJlZGljdGlvblN0YXRzX2JpbmFyeShjYmluZChjbGFzc291dGNvbWVzLHByRGVjb3JbLHRoZUNsYXNzXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUoZGVjb3J0eXBlW1tpXV0sdGhlQ2xhc3Msc2VwPSI6IiksY2V4PTAuNSkNCiAgICBtZWFuUk9DQVVDIDwtIG1lYW5ST0NBVUMgKyBwc0RlY29yJGF1Y3M7DQogIH0NCiAgbWVhblJPQ0FVQyA8LSBtZWFuUk9DQVVDL2xlbmd0aChjbGFzc05hbWVzKQ0KICBwYW5kZXI6OnBhbmRlcihtZWFuUk9DQVVDKQ0KICBBbGxSb2NBVUMgPC0gcmJpbmQoQWxsUm9jQVVDLG1lYW5ST0NBVUMpDQoNCn0NCg0KYGBgDQoNCiMjIyBUaGUgQmFyIHBsb3Qgb2YgYWxsIEFVQw0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCB3YXJuaW5nID0gRkFMU0UsIGRwaT02MDAsIGZpZy5oZWlnaHQ9IDYuMCwgZmlnLndpZHRoPSA4LjB9DQoNCnJvd25hbWVzKEFsbFJvY0FVQykgPC0gYygiUmF3IiwiUENBIix1bmxpc3QoZGVjb3J0eXBlKSkNCnBhbmRlcjo6cGFuZGVyKEFsbFJvY0FVQykNCnBhcihtZnJvdz1jKDEsMSkpDQoNCmJwUk9DQVVDIDwtIGJhclBsb3RDaUVycm9yKGFzLm1hdHJpeChBbGxSb2NBVUMpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRyaWNuYW1lID0gIlJPQ0FVQyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRoZXNldHMgPSAiUk9DIEFVQyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRoZW1ldGhvZCA9IHJvd25hbWVzKEFsbFJvY0FVQyksDQogICAgICAgICAgICAgICAgICAgICAgICAgIG1haW4gPSAiUk9DIEFVQyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIG9mZnNldHMgPSBjKDAuNSwxKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2NvcmVEaXJlY3Rpb24gPSAiPiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGhvPTAuNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgYXJncy5sZWdlbmQgPSBsaXN0KGJnID0gIndoaXRlIix4PSJib3R0b21yaWdodCIsaW5zZXQ9YygwLjAsMCksY2V4PTAuNzUpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBjb2wgPSB0ZXJyYWluLmNvbG9ycyhucm93KEFsbFJvY0FVQykpDQogICAgICAgICAgICAgICAgICAgICAgICAgICkNCg0KYGBgDQo=