Overview
This is my analysis and model for the Personalized Medicine Kaggle competition: https://www.kaggle.com/c/msk-redefining-cancer-treatment
As there is a bit of drama about external data being used that actually included the test transactions, I will focus only on the data provided and no external data. The goal is not to win the competition but do some exploratory work, feature selection, and have a halfway decent model result.
Data Import
Options & Libraries
options(repos = getOption("repos")["CRAN"])
options(scipen = 999)
rm(list=ls())
#Libraries
library(tidyverse)
library(corrplot)
library(caret)
library(gbm)
library(magrittr)
library(gbm)
library(e1071)
library(randomForest)
library(lubridate)
library(xgboost)
library(Matrix)
library(data.table)
library(stringi)
library(NMOF)
library(tidytext)
library(stringr)
library(forcats)
library(tidytext)
library(SnowballC)
library(wordcloud)
library(tm)
library(syuzhet)
Reading in the data files, there are four files. One set of test/train is a base file with genes and their variations. The other test/train text files include researcher write-up of the case.
d_train <- read.csv('training_variants')
d_test <- read.csv('test_variants')
#these text files have a bad first row and don't import well with read.table functions
d_train_text <- do.call(rbind,strsplit(readLines('training_text'),'||',fixed=T))
d_train_text <- as.data.frame(d_train_text)
d_train_text <- d_train_text[-1,]
colnames(d_train_text) <-c("ID","Text")
d_train_text$ID <- as.numeric(as.character(d_train_text$ID))
d_test_text <- do.call(rbind,strsplit(readLines('test_text'),'||',fixed=T))
d_test_text <- as.data.frame(d_test_text)
d_test_text <- d_test_text[-1,]
colnames(d_test_text) <- c("ID", "Text")
d_test_text$ID <- as.numeric(as.character(d_test_text$ID))
I’ll combine the data for the base files and the text files.
d_train <- d_train %>%
mutate(Class = factor(Class))
d_test$Class <-as.factor(-9)
df <- rbind(d_train,d_test)
rm(list=c('d_train','d_test'))
df$ds <- ifelse(df$Class == '-9','Test','Train')
d_train_text$ds <- "Train"
d_test_text$ds <- "Test"
dt <- rbind(d_train_text,d_test_text)
dt$ds <- as.factor(dt$ds)
rm(list = c('d_train_text','d_test_text'))
# dt %>%
# group_by(ds) %>%
# summarize(count = n())
df <- merge(df,dt,by=c('ID','ds'))
Exploratory Plots
The first view shows us the prediction class distribution for levels 1-9, which is what needs to be predicted. We can see there are many gene, some with high distribution while many small. The variation seems to have very high cardinality with few terms showing up more than once, which will be difficult to manage.
df %>%
filter(ds == 'Train') %>%
select(Gene,Variation,Class) %>%
gather() %>%
ggplot(aes(value)) +
facet_wrap(~ key, scales = 'free') +
geom_bar(fill='blue') +
theme_bw()
attributes are not identical across measure variables; they will be dropped

I will manually look at a few of the higher occurrences to get a sense of the data, there seems to be some discrete values and then lots of hard to understand variations like “Q61R”.
df %>%
group_by(ds, Variation) %>%
summarize(count = n()) %>%
arrange(desc(count)) %>%
head(n=20)
The genes seem to have complicated naming conventions as well, some research and industry knowledge would be very helpful here. It is interesting to see that the most common gene in the test set (F8) is not common in the training set. It is not clear how the training/ test sets were split and sampled at this point.
df %>%
group_by(ds,Gene) %>%
summarize(count = n()) %>%
arrange(desc(count)) %>%
head(n=20)
The distribution of genes to classes is quite interesting, as some classes have very few genes in them.
df %>%
filter(ds == 'Train') %>%
select(Class,Gene) %>%
ggplot(aes(x=Gene)) +
geom_bar() +
facet_wrap(~ Class, scales = 'free')

There are not too many major Gene-Class combinations.
df %>%
group_by(ds,Class,Gene) %>%
summarize(obs = n()) %>%
arrange(Gene,Class,obs) %>%
head(10)
I’ll make a new variable for high occuring genes, and also plot it. There are some high observation ones which may be interesting to research.
df <- df %>%
group_by(Gene) %>%
summarize(obs = n()) %>%
mutate(gene_o_25 = ifelse(obs > 25, 1, 0)) %>%
right_join(df) %>%
ungroup() %>%
select(-obs)
Joining, by = "Gene"
df %>%
group_by(Gene) %>%
summarize(obs = n()) %>%
mutate(gene_o_25 = ifelse(obs > 25, 1, 0)) %>%
filter(gene_o_25 == 1) %>%
ggplot(aes(x=Gene, y = obs)) +
geom_bar(fill = 'green',stat = 'identity') +
coord_flip() +
theme_bw()

When I include the class level it can be quite interesting. Certain genes clearly have very different distributions of the classes they may belong to.
df %>%
filter(gene_o_25 == 1 & ds == 'Train') %>%
ggplot(aes(x=Gene ,fill = Class)) +
geom_bar() +
coord_flip() +
theme_bw() +
scale_fill_brewer(palette = 'Set3')

I’ll do similar plotting for the Variations. It looks like a few buckets dominate and then more technical classifications occur.
df <- df %>%
group_by(Variation) %>%
summarize(obs1 = n()) %>%
mutate(variation_o_2 = ifelse(obs1 > 2, 1, 0)) %>%
right_join(df, by = 'Variation') %>%
ungroup() %>%
select(-obs1)
df %>%
group_by(Variation) %>%
summarize(obs = n()) %>%
mutate(big_vari = ifelse(obs > 2, 1, 0)) %>%
filter(big_vari == 1) %>%
ggplot(aes(x=Variation, y = obs)) +
geom_bar(fill = 'green',stat = 'identity') +
coord_flip() +
theme_bw()

Class 1 seems to dominate the Truncating Mutations as well as Deletion, it will be harder to group the Q61’s up and find a pattern.
df %>%
filter(variation_o_2 == 1 & ds == 'Train') %>%
ggplot(aes(x=Variation, fill = Class)) +
geom_bar() +
coord_flip() +
theme_bw() +
scale_fill_brewer(palette = 'Set1')

This view will show us where the gene-variation combos might occur, and which class they fall into.
df %>%
filter(variation_o_2 == 1 & gene_o_25 == 1 & ds == 'Train') %>%
ggplot(aes(x=Variation, y=Gene, color = Class)) +
geom_point(size = 4, alpha = .8) +
theme_bw() +
scale_size() +
scale_fill_brewer(palette = 'Set1')

Feature Engineering - Base File
I’ll build some basic features for what I have just explored. Let’s start scraping variation for some of these key words as well as the character length of gene and variation.
df <- df %>%
mutate(fusion_in_variation = ifelse(grepl('Fusion',df$Variation),1,0),
mutation_in_variation = ifelse(grepl('Mutation',df$Variation),1,0),
truncating_in_variation = ifelse(grepl('Truncating',df$Variation),1,0),
amplification_in_variation = ifelse(grepl('Amplification',df$Variation),1,0),
insertion_in_variation = ifelse(grepl('insertion',df$Variation),1,0),
del_in_variation = ifelse(grepl('Del',df$Variation),1,0),
gene_length = nchar(as.character(Gene)),
variation_length = nchar(as.character(Variation))
)
Dummy variables for the 10 most common genes, exact matching.
df <- df %>%
mutate(g_brca1 = ifelse(Gene == 'BRCA1',1,0),
g_brca2 = ifelse(Gene == 'BRCA2',1,0),
g_tp53 = ifelse(Gene == 'TP53',1,0),
g_pten = ifelse(Gene == 'PTEN',1,0),
g_kit = ifelse(Gene == 'KIT',1,0),
g_egfr = ifelse(Gene == 'EGFR',1,0),
g_braf = ifelse(Gene == 'BRAF',1,0),
g_cdkn2a = ifelse(Gene == 'CDKN2A',1,0),
g_abl1 = ifelse(Gene == 'ABL1',1,0),
g_tsc2 = ifelse(Gene == 'TSC2',1,0),
g_vhl = ifelse(Gene == 'VHL',1,0))
Dummy variables for truncations.
df <- df %>%
mutate(v_trunc_mutations = ifelse(Variation == 'Truncating Mutations',1,0),
v_t581 = ifelse(Variation == 'T58I',1,0),
v_q61r = ifelse(Variation == 'Q61R',1,0),
v_q61l = ifelse(Variation == 'Q61L',1,0),
v_q61h = ifelse(Variation == 'Q61H',1,0),
v_overexpression = ifelse(Variation == 'Overexpression',1,0),
v_g13d = ifelse(Variation == 'G13D',1,0),
v_g12v = ifelse(Variation == 'G12V',1,0),
v_g12c = ifelse(Variation == 'G12C',1,0),
v_fusions = ifelse(Variation == 'Fusions',1,0),
v_e17k = ifelse(Variation == 'E17K',1,0),
v_Deletion = ifelse(Variation == 'Deletion',1,0),
v_Amplification = ifelse(Variation == 'Amplification',1,0)
)
The first and last values for the genes seem to be significant, lets create dummy variables for them as well. First, the last letter in the gene.
df$gene <- as.character(sapply(strsplit(as.character(df$Gene),""),tail,1))
df$val <- 1
df <- df %>%
spread(key = gene,value = val, fill = 0,sep = '_end_')
We can create the first letter for the gene in a similar way.
df$gene <- as.character(sapply(strsplit(as.character(df$Gene),""),head,1))
df$val <- 1
df <- df %>%
spread(key = gene,value = val, fill = 0,sep = '_begin_')
We can also check what variation end in.
df$vrtn <- as.character(sapply(strsplit(as.character(df$Variation),""),head,1))
df$val <- 1
df <- df %>%
spread(key = vrtn,value = val, fill = 0,sep = '_end_')
The second, third and fourth digits also seem to have some type of similar partterns, lets use the top occurences to create a few more dummy variables. Later, research will be needed as to what this represents.
df$vrtn <- as.character(substr(as.character(df$Variation),2,4))
df$val <- 1
df <- df %>%
group_by(vrtn,val) %>%
summarize(count = n()) %>%
mutate(com_mid_vrtn = ifelse(count>25,vrtn,"")) %>%
right_join(df, by = 'vrtn') %>%
ungroup()
df <- df %>%
spread(key=com_mid_vrtn,value = 'val.x', fill = 0, sep = '_mid3_') %>%
select(-vrtn,-count,-val.y)
Feature Engineering - Text File
The text files provided are quite extensive with very long write ups by the researchers. I will take a very light text mining approach here by converting this to a Corpus and then a sparse matrix for the more common words.
First, the corpus can be created and cleaned up using the standard text-mining approach.
t_corpus <- Corpus(VectorSource(dt$Text))
t_corpus <- t_corpus %>%
tm_map(stripWhitespace) %>%
tm_map(removeNumbers) %>%
tm_map(removePunctuation) %>%
tm_map(content_transformer(tolower)) %>%
tm_map(removeWords, stopwords("english")) %>%
tm_map(stemDocument, language="english")
I’ll now convert it to a matrix and use TF-IDF for word importance. We can join back to our base dataset.
dtm <- DocumentTermMatrix(t_corpus, control = list(weighting = weightTfIdf))
dtm <- removeSparseTerms(dtm, 0.95)
df <- cbind(df,as.matrix(dtm))
Lastly, let’s do sentimenet analysis on all these terms to determine positive/ negative relationships.
df <- cbind(df,get_nrc_sentiment(as.character(df$Text)))
Let’s plot out the sentiments that were created.

Model Building
I will now set up the framework to run a GBT model. I will use k-fold cross-validation to get an estimate.
set.seed(7)
cv_folds <- createFolds(df$Class[df$Class != '-9'], k=5, list=TRUE, returnTrain=FALSE)
With the folds set, I’ll divide the data back into our train and test sets.
train_matrix <- df %>%
filter(ds == 'Train') %>%
select(-ID,-Gene,-Variation,-Class, -ds, - Text) %>%
apply(2,as.character) %>%
apply(2,as.integer) %>%
as.matrix() %>%
Matrix(sparse = T)
test_matrix <- df %>%
filter(ds == 'Test') %>%
select(-ID,-Gene,-Variation,-Class, -ds,-Text) %>%
apply(2,as.character) %>%
apply(2,as.integer) %>%
as.matrix() %>%
Matrix(sparse = T)
Let’s give our training data the correct labels, as well as create the vector of our test ids for later.
train_y <- df %>%
filter(ds == 'Train') %>%
select(Class) %>%
apply(2,as.character) %>%
apply(2,as.integer) -1
test_ids <- df[df$ds == 'Test','ID']
Now lets create our XGB Matrices.
dtrain <- xgb.DMatrix(data = train_matrix, label = train_y)
dtest <- xgb.DMatrix(data = test_matrix)
The paramaters can be found through a grid search or trial and error.
param <- list(booster = 'gbtree',
objective = 'multi:softprob',
eval_metric = 'mlogloss',
num_class = 9,
eta = .2,
gamma = 1,
max_depth = 6,
min_child_weight = 1,
subsample = .7,
colsample_bytree = .7)
I’ll run to find the proper rounds needed, as well get a sense of our average log-loss error on the test sets.
xgb_cv <- xgb.cv(data = dtrain,
params = param,
nrounds = 1000,
maximize = FALSE,
prediction = TRUE,
folds = cv_folds,
print_every_n = 10,
early_stopping_rounds = 100)
[1] train-mlogloss:1.937655+0.009034 test-mlogloss:1.959912+0.018980
Multiple eval metrics are present. Will use test_mlogloss for early stopping.
Will train until test_mlogloss hasn't improved in 100 rounds.
[11] train-mlogloss:1.152867+0.008930 test-mlogloss:1.310597+0.034807
[21] train-mlogloss:0.898848+0.007829 test-mlogloss:1.158172+0.040239
[31] train-mlogloss:0.754805+0.006796 test-mlogloss:1.093189+0.044991
[41] train-mlogloss:0.658517+0.005712 test-mlogloss:1.062522+0.046323
[51] train-mlogloss:0.591242+0.006247 test-mlogloss:1.048288+0.045692
[61] train-mlogloss:0.541578+0.005534 test-mlogloss:1.039373+0.047339
[71] train-mlogloss:0.505758+0.006165 test-mlogloss:1.034065+0.047184
[81] train-mlogloss:0.476895+0.005391 test-mlogloss:1.033773+0.048242
[91] train-mlogloss:0.453790+0.005452 test-mlogloss:1.032013+0.046393
[101] train-mlogloss:0.434546+0.004890 test-mlogloss:1.031118+0.045972
[111] train-mlogloss:0.419225+0.005004 test-mlogloss:1.030887+0.044943
[121] train-mlogloss:0.406539+0.004720 test-mlogloss:1.031755+0.045891
[131] train-mlogloss:0.395407+0.004484 test-mlogloss:1.032509+0.046531
[141] train-mlogloss:0.385014+0.004666 test-mlogloss:1.032902+0.046992
[151] train-mlogloss:0.376440+0.004775 test-mlogloss:1.033313+0.046635
[161] train-mlogloss:0.368263+0.003906 test-mlogloss:1.034437+0.045821
[171] train-mlogloss:0.362079+0.003916 test-mlogloss:1.034673+0.046986
[181] train-mlogloss:0.356015+0.003251 test-mlogloss:1.035144+0.046364
[191] train-mlogloss:0.350541+0.003166 test-mlogloss:1.036582+0.045985
[201] train-mlogloss:0.345986+0.002985 test-mlogloss:1.036591+0.046569
[211] train-mlogloss:0.341979+0.002905 test-mlogloss:1.038154+0.047450
Stopping. Best iteration:
[115] train-mlogloss:0.413861+0.004695 test-mlogloss:1.029883+0.044247
We can now build our final model.
rounds <- which.min(xgb_cv$evaluation_log$test_mlogloss_mean)
xgb_model <- xgb.train(data = dtrain,
params = param,
watchlist = list(train = dtrain),
nrounds = rounds,
verbose = 1,
print_every_n = 5)
[1] train-mlogloss:1.942394
[6] train-mlogloss:1.403119
[11] train-mlogloss:1.162500
[16] train-mlogloss:1.023238
[21] train-mlogloss:0.923765
[26] train-mlogloss:0.844091
[31] train-mlogloss:0.782102
[36] train-mlogloss:0.732347
[41] train-mlogloss:0.694913
[46] train-mlogloss:0.653488
[51] train-mlogloss:0.619108
[56] train-mlogloss:0.588880
[61] train-mlogloss:0.565901
[66] train-mlogloss:0.547181
[71] train-mlogloss:0.530021
[76] train-mlogloss:0.515254
[81] train-mlogloss:0.502279
[86] train-mlogloss:0.489295
[91] train-mlogloss:0.476362
[96] train-mlogloss:0.465331
[101] train-mlogloss:0.455101
[106] train-mlogloss:0.445170
[111] train-mlogloss:0.436663
[115] train-mlogloss:0.430055
The importance matrix is as follows, sentiment analysis helped quite a bit along with a few of our gene/variation variables.
names <- dimnames(train_matrix)[[2]]
importance_matrix <- xgb.importance(names,model=xgb_model)
xgb.plot.importance(importance_matrix,top_n=20)

We can now get our predictions on the test data.
predictions <- xgb_model %>%
predict(dtest) %>%
matrix(nrow = nrow(dtest), ncol=9,byrow = T) %>%
as.data.frame()
Let’s format them correctly and see if each row sums properly to 1 as a sanity check.
colnames(predictions) <- c("class1","class2","class3","class4","class5","class6","class7","class8","class9")
df_preds <- data.frame(ID=test_ids,predictions)
df_preds %>%
select(-ID) %>%
apply(1,sum) %>%
head(n=25)
[1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
And now our submission file.
write.csv(df_preds,'submission_nb.csv',row.names = F)
With the very light feature engineering and model tuning, I am around the 50th percentile. More research needs to be done around how to understand the genes and mutations, as well as deeper text mining and feature creation.
LS0tDQp0aXRsZTogIlBlcnNvbmFsaXplZCBNZWRpY2luZSAtIEthZ2dsZSINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCiNPdmVydmlldw0KVGhpcyBpcyBteSBhbmFseXNpcyBhbmQgbW9kZWwgZm9yIHRoZSBQZXJzb25hbGl6ZWQgTWVkaWNpbmUgS2FnZ2xlIGNvbXBldGl0aW9uOg0KaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9jL21zay1yZWRlZmluaW5nLWNhbmNlci10cmVhdG1lbnQNCg0KQXMgdGhlcmUgaXMgYSBiaXQgb2YgZHJhbWEgYWJvdXQgZXh0ZXJuYWwgZGF0YSBiZWluZyB1c2VkIHRoYXQgYWN0dWFsbHkgaW5jbHVkZWQgdGhlIHRlc3QgdHJhbnNhY3Rpb25zLCBJIHdpbGwgZm9jdXMgb25seSBvbiB0aGUgZGF0YSBwcm92aWRlZCBhbmQgbm8gZXh0ZXJuYWwgZGF0YS4gVGhlIGdvYWwgaXMgbm90IHRvIHdpbiB0aGUgY29tcGV0aXRpb24gYnV0IGRvIHNvbWUgZXhwbG9yYXRvcnkgd29yaywgZmVhdHVyZSBzZWxlY3Rpb24sIGFuZCBoYXZlIGEgaGFsZndheSBkZWNlbnQgbW9kZWwgcmVzdWx0Lg0KDQojRGF0YSBJbXBvcnQNCg0KT3B0aW9ucyAmIExpYnJhcmllcw0KYGBge3J9DQpvcHRpb25zKHJlcG9zID0gZ2V0T3B0aW9uKCJyZXBvcyIpWyJDUkFOIl0pDQpvcHRpb25zKHNjaXBlbiA9IDk5OSkNCg0Kcm0obGlzdD1scygpKQ0KDQoNCiNMaWJyYXJpZXMNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShjb3JycGxvdCkNCmxpYnJhcnkoY2FyZXQpDQpsaWJyYXJ5KGdibSkNCmxpYnJhcnkobWFncml0dHIpDQpsaWJyYXJ5KGdibSkNCmxpYnJhcnkoZTEwNzEpDQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkNCmxpYnJhcnkobHVicmlkYXRlKQ0KbGlicmFyeSh4Z2Jvb3N0KQ0KbGlicmFyeShNYXRyaXgpDQpsaWJyYXJ5KGRhdGEudGFibGUpDQpsaWJyYXJ5KHN0cmluZ2kpDQpsaWJyYXJ5KE5NT0YpDQpsaWJyYXJ5KHRpZHl0ZXh0KQ0KbGlicmFyeShzdHJpbmdyKQ0KbGlicmFyeShmb3JjYXRzKSANCmxpYnJhcnkodGlkeXRleHQpIA0KbGlicmFyeShTbm93YmFsbEMpIA0KbGlicmFyeSh3b3JkY2xvdWQpIA0KbGlicmFyeSh0bSkNCmxpYnJhcnkoc3l1emhldCkNCmBgYA0KDQpSZWFkaW5nIGluIHRoZSBkYXRhIGZpbGVzLCB0aGVyZSBhcmUgZm91ciBmaWxlcy4gT25lIHNldCBvZiB0ZXN0L3RyYWluIGlzIGEgYmFzZSBmaWxlIHdpdGggZ2VuZXMgYW5kIHRoZWlyIHZhcmlhdGlvbnMuIFRoZSBvdGhlciB0ZXN0L3RyYWluIHRleHQgZmlsZXMgaW5jbHVkZSByZXNlYXJjaGVyIHdyaXRlLXVwIG9mIHRoZSBjYXNlLg0KYGBge3J9DQpkX3RyYWluIDwtIHJlYWQuY3N2KCd0cmFpbmluZ192YXJpYW50cycpDQpkX3Rlc3QgPC0gcmVhZC5jc3YoJ3Rlc3RfdmFyaWFudHMnKQ0KDQojdGhlc2UgdGV4dCBmaWxlcyBoYXZlIGEgYmFkIGZpcnN0IHJvdyBhbmQgZG9uJ3QgaW1wb3J0IHdlbGwgd2l0aCByZWFkLnRhYmxlIGZ1bmN0aW9ucw0KZF90cmFpbl90ZXh0IDwtIGRvLmNhbGwocmJpbmQsc3Ryc3BsaXQocmVhZExpbmVzKCd0cmFpbmluZ190ZXh0JyksJ3x8JyxmaXhlZD1UKSkNCmRfdHJhaW5fdGV4dCA8LSBhcy5kYXRhLmZyYW1lKGRfdHJhaW5fdGV4dCkNCmRfdHJhaW5fdGV4dCA8LSBkX3RyYWluX3RleHRbLTEsXQ0KY29sbmFtZXMoZF90cmFpbl90ZXh0KSA8LWMoIklEIiwiVGV4dCIpDQpkX3RyYWluX3RleHQkSUQgPC0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoZF90cmFpbl90ZXh0JElEKSkNCg0KZF90ZXN0X3RleHQgPC0gZG8uY2FsbChyYmluZCxzdHJzcGxpdChyZWFkTGluZXMoJ3Rlc3RfdGV4dCcpLCd8fCcsZml4ZWQ9VCkpDQpkX3Rlc3RfdGV4dCA8LSBhcy5kYXRhLmZyYW1lKGRfdGVzdF90ZXh0KQ0KZF90ZXN0X3RleHQgPC0gZF90ZXN0X3RleHRbLTEsXQ0KY29sbmFtZXMoZF90ZXN0X3RleHQpIDwtIGMoIklEIiwgIlRleHQiKQ0KZF90ZXN0X3RleHQkSUQgPC0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoZF90ZXN0X3RleHQkSUQpKQ0KYGBgDQpJJ2xsIGNvbWJpbmUgdGhlIGRhdGEgZm9yIHRoZSBiYXNlIGZpbGVzIGFuZCB0aGUgdGV4dCBmaWxlcy4NCmBgYHtyfQ0KZF90cmFpbiA8LSBkX3RyYWluICU+JQ0KICBtdXRhdGUoQ2xhc3MgPSBmYWN0b3IoQ2xhc3MpKQ0KDQpkX3Rlc3QkQ2xhc3MgPC1hcy5mYWN0b3IoLTkpDQoNCmRmIDwtIHJiaW5kKGRfdHJhaW4sZF90ZXN0KQ0Kcm0obGlzdD1jKCdkX3RyYWluJywnZF90ZXN0JykpDQpkZiRkcyA8LSBpZmVsc2UoZGYkQ2xhc3MgPT0gJy05JywnVGVzdCcsJ1RyYWluJykNCg0KDQpkX3RyYWluX3RleHQkZHMgPC0gIlRyYWluIg0KZF90ZXN0X3RleHQkZHMgPC0gIlRlc3QiDQpkdCA8LSByYmluZChkX3RyYWluX3RleHQsZF90ZXN0X3RleHQpDQpkdCRkcyA8LSBhcy5mYWN0b3IoZHQkZHMpDQoNCnJtKGxpc3QgPSBjKCdkX3RyYWluX3RleHQnLCdkX3Rlc3RfdGV4dCcpKQ0KDQojIGR0ICU+JQ0KIyAgIGdyb3VwX2J5KGRzKSAlPiUNCiMgICBzdW1tYXJpemUoY291bnQgPSBuKCkpDQoNCmRmIDwtIG1lcmdlKGRmLGR0LGJ5PWMoJ0lEJywnZHMnKSkNCmBgYA0KI0V4cGxvcmF0b3J5IFBsb3RzDQoNClRoZSBmaXJzdCB2aWV3IHNob3dzIHVzIHRoZSBwcmVkaWN0aW9uIGNsYXNzIGRpc3RyaWJ1dGlvbiBmb3IgbGV2ZWxzIDEtOSwgd2hpY2ggaXMgd2hhdCBuZWVkcyB0byBiZSBwcmVkaWN0ZWQuIFdlIGNhbiBzZWUgdGhlcmUgYXJlIG1hbnkgZ2VuZSwgc29tZSB3aXRoIGhpZ2ggZGlzdHJpYnV0aW9uIHdoaWxlIG1hbnkgc21hbGwuIFRoZSB2YXJpYXRpb24gc2VlbXMgdG8gaGF2ZSB2ZXJ5IGhpZ2ggY2FyZGluYWxpdHkgd2l0aCBmZXcgdGVybXMgc2hvd2luZyB1cCBtb3JlIHRoYW4gb25jZSwgd2hpY2ggd2lsbCBiZSBkaWZmaWN1bHQgdG8gbWFuYWdlLg0KYGBge3J9DQpkZiAlPiUNCiAgZmlsdGVyKGRzID09ICdUcmFpbicpICU+JQ0KICBzZWxlY3QoR2VuZSxWYXJpYXRpb24sQ2xhc3MpICU+JQ0KICBnYXRoZXIoKSAlPiUNCiAgZ2dwbG90KGFlcyh2YWx1ZSkpICsNCiAgZmFjZXRfd3JhcCh+IGtleSwgc2NhbGVzID0gJ2ZyZWUnKSArDQogIGdlb21fYmFyKGZpbGw9J2JsdWUnKSArDQogIHRoZW1lX2J3KCkgDQpgYGANCkkgd2lsbCBtYW51YWxseSBsb29rIGF0IGEgZmV3IG9mIHRoZSBoaWdoZXIgb2NjdXJyZW5jZXMgdG8gZ2V0IGEgc2Vuc2Ugb2YgdGhlIGRhdGEsIHRoZXJlIHNlZW1zIHRvIGJlIHNvbWUgZGlzY3JldGUgdmFsdWVzIGFuZCB0aGVuIGxvdHMgb2YgaGFyZCB0byB1bmRlcnN0YW5kIHZhcmlhdGlvbnMgbGlrZSAiUTYxUiIuDQoNCmBgYHtyfQ0KZGYgJT4lDQogIGdyb3VwX2J5KGRzLCBWYXJpYXRpb24pICU+JQ0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JQ0KICBhcnJhbmdlKGRlc2MoY291bnQpKSAlPiUNCiAgaGVhZChuPTIwKQ0KYGBgDQpUaGUgZ2VuZXMgc2VlbSB0byBoYXZlIGNvbXBsaWNhdGVkIG5hbWluZyBjb252ZW50aW9ucyBhcyB3ZWxsLCBzb21lIHJlc2VhcmNoIGFuZCBpbmR1c3RyeSBrbm93bGVkZ2Ugd291bGQgYmUgdmVyeSBoZWxwZnVsIGhlcmUuIEl0IGlzIGludGVyZXN0aW5nIHRvIHNlZSB0aGF0IHRoZSBtb3N0IGNvbW1vbiBnZW5lIGluIHRoZSB0ZXN0IHNldCAoRjgpIGlzIG5vdCBjb21tb24gaW4gdGhlIHRyYWluaW5nIHNldC4gSXQgaXMgbm90IGNsZWFyIGhvdyB0aGUgdHJhaW5pbmcvIHRlc3Qgc2V0cyB3ZXJlIHNwbGl0IGFuZCBzYW1wbGVkIGF0IHRoaXMgcG9pbnQuDQpgYGB7cn0NCmRmICU+JQ0KICBncm91cF9ieShkcyxHZW5lKSAlPiUNCiAgc3VtbWFyaXplKGNvdW50ID0gbigpKSAlPiUNCiAgYXJyYW5nZShkZXNjKGNvdW50KSkgJT4lDQogIGhlYWQobj0yMCkNCmBgYA0KVGhlIGRpc3RyaWJ1dGlvbiBvZiBnZW5lcyB0byBjbGFzc2VzIGlzIHF1aXRlIGludGVyZXN0aW5nLCBhcyBzb21lIGNsYXNzZXMgaGF2ZSB2ZXJ5IGZldyBnZW5lcyBpbiB0aGVtLg0KYGBge3J9DQpkZiAlPiUNCiAgZmlsdGVyKGRzID09ICdUcmFpbicpICU+JQ0KICBzZWxlY3QoQ2xhc3MsR2VuZSkgJT4lDQogIGdncGxvdChhZXMoeD1HZW5lKSkgKw0KICBnZW9tX2JhcigpICsNCiAgZmFjZXRfd3JhcCh+IENsYXNzLCBzY2FsZXMgPSAnZnJlZScpDQpgYGANClRoZXJlIGFyZSBub3QgdG9vIG1hbnkgbWFqb3IgR2VuZS1DbGFzcyBjb21iaW5hdGlvbnMuDQpgYGB7cn0NCmRmICU+JQ0KICBncm91cF9ieShkcyxDbGFzcyxHZW5lKSAlPiUNCiAgc3VtbWFyaXplKG9icyA9IG4oKSkgJT4lDQogIGFycmFuZ2UoR2VuZSxDbGFzcyxvYnMpICU+JQ0KICBoZWFkKDEwKQ0KYGBgDQpJJ2xsIG1ha2UgYSBuZXcgdmFyaWFibGUgZm9yIGhpZ2ggb2NjdXJpbmcgZ2VuZXMsIGFuZCBhbHNvIHBsb3QgaXQuIFRoZXJlIGFyZSBzb21lIGhpZ2ggb2JzZXJ2YXRpb24gb25lcyB3aGljaCBtYXkgYmUgaW50ZXJlc3RpbmcgdG8gcmVzZWFyY2guDQpgYGB7cn0NCg0KZGYgPC0gZGYgJT4lDQogIGdyb3VwX2J5KEdlbmUpICU+JQ0KICBzdW1tYXJpemUob2JzID0gbigpKSAlPiUNCiAgbXV0YXRlKGdlbmVfb18yNSA9IGlmZWxzZShvYnMgPiAyNSwgMSwgMCkpICU+JQ0KICByaWdodF9qb2luKGRmKSAlPiUNCiAgdW5ncm91cCgpICU+JQ0KICBzZWxlY3QoLW9icykNCg0KZGYgJT4lDQogIGdyb3VwX2J5KEdlbmUpICU+JQ0KICBzdW1tYXJpemUob2JzID0gbigpKSAlPiUNCiAgbXV0YXRlKGdlbmVfb18yNSA9IGlmZWxzZShvYnMgPiAyNSwgMSwgMCkpICU+JQ0KICBmaWx0ZXIoZ2VuZV9vXzI1ID09IDEpICU+JQ0KICBnZ3Bsb3QoYWVzKHg9R2VuZSwgeSA9IG9icykpICsNCiAgZ2VvbV9iYXIoZmlsbCA9ICdncmVlbicsc3RhdCA9ICdpZGVudGl0eScpICsNCiAgY29vcmRfZmxpcCgpICsNCiAgdGhlbWVfYncoKQ0KYGBgDQpXaGVuIEkgaW5jbHVkZSB0aGUgY2xhc3MgbGV2ZWwgaXQgY2FuIGJlIHF1aXRlIGludGVyZXN0aW5nLiBDZXJ0YWluIGdlbmVzIGNsZWFybHkgaGF2ZSB2ZXJ5IGRpZmZlcmVudCBkaXN0cmlidXRpb25zIG9mIHRoZSBjbGFzc2VzIHRoZXkgbWF5IGJlbG9uZyB0by4NCmBgYHtyfQ0KZGYgJT4lDQogIGZpbHRlcihnZW5lX29fMjUgPT0gMSAmIGRzID09ICdUcmFpbicpICU+JQ0KICBnZ3Bsb3QoYWVzKHg9R2VuZSAsZmlsbCA9IENsYXNzKSkgKw0KICBnZW9tX2JhcigpICsNCiAgY29vcmRfZmxpcCgpICsNCiAgdGhlbWVfYncoKSArDQogIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGUgPSAnU2V0MycpDQpgYGANCkknbGwgZG8gc2ltaWxhciBwbG90dGluZyBmb3IgdGhlIFZhcmlhdGlvbnMuIEl0IGxvb2tzIGxpa2UgYSBmZXcgYnVja2V0cyBkb21pbmF0ZSBhbmQgdGhlbiBtb3JlIHRlY2huaWNhbCBjbGFzc2lmaWNhdGlvbnMgb2NjdXIuDQpgYGB7cn0NCmRmIDwtICAgZGYgJT4lDQogIGdyb3VwX2J5KFZhcmlhdGlvbikgJT4lDQogIHN1bW1hcml6ZShvYnMxID0gbigpKSAlPiUNCiAgbXV0YXRlKHZhcmlhdGlvbl9vXzIgPSBpZmVsc2Uob2JzMSA+IDIsIDEsIDApKSAlPiUNCiAgcmlnaHRfam9pbihkZiwgYnkgPSAnVmFyaWF0aW9uJykgJT4lDQogIHVuZ3JvdXAoKSAlPiUNCiAgc2VsZWN0KC1vYnMxKQ0KDQpkZiAlPiUNCiAgZ3JvdXBfYnkoVmFyaWF0aW9uKSAlPiUNCiAgc3VtbWFyaXplKG9icyA9IG4oKSkgJT4lDQogIG11dGF0ZShiaWdfdmFyaSA9IGlmZWxzZShvYnMgPiAyLCAxLCAwKSkgJT4lDQogIGZpbHRlcihiaWdfdmFyaSA9PSAxKSAlPiUNCiAgZ2dwbG90KGFlcyh4PVZhcmlhdGlvbiwgeSA9IG9icykpICsNCiAgZ2VvbV9iYXIoZmlsbCA9ICdncmVlbicsc3RhdCA9ICdpZGVudGl0eScpICsNCiAgY29vcmRfZmxpcCgpICsNCiAgdGhlbWVfYncoKQ0KYGBgDQpDbGFzcyAxIHNlZW1zIHRvIGRvbWluYXRlIHRoZSBUcnVuY2F0aW5nIE11dGF0aW9ucyBhcyB3ZWxsIGFzIERlbGV0aW9uLCBpdCB3aWxsIGJlIGhhcmRlciB0byBncm91cCB0aGUgUTYxJ3MgdXAgYW5kIGZpbmQgYSBwYXR0ZXJuLg0KYGBge3J9DQpkZiAlPiUNCiAgZmlsdGVyKHZhcmlhdGlvbl9vXzIgPT0gMSAmIGRzID09ICdUcmFpbicpICU+JQ0KICBnZ3Bsb3QoYWVzKHg9VmFyaWF0aW9uLCBmaWxsID0gQ2xhc3MpKSArDQogIGdlb21fYmFyKCkgKw0KICBjb29yZF9mbGlwKCkgKw0KICB0aGVtZV9idygpICsNCiAgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZSA9ICdTZXQxJykNCmBgYA0KVGhpcyB2aWV3IHdpbGwgc2hvdyB1cyB3aGVyZSB0aGUgZ2VuZS12YXJpYXRpb24gY29tYm9zIG1pZ2h0IG9jY3VyLCBhbmQgd2hpY2ggY2xhc3MgdGhleSBmYWxsIGludG8uDQpgYGB7cn0NCmRmICU+JQ0KICBmaWx0ZXIodmFyaWF0aW9uX29fMiA9PSAxICYgZ2VuZV9vXzI1ID09IDEgJiBkcyA9PSAnVHJhaW4nKSAlPiUNCiAgZ2dwbG90KGFlcyh4PVZhcmlhdGlvbiwgeT1HZW5lLCBjb2xvciA9IENsYXNzKSkgKw0KICBnZW9tX3BvaW50KHNpemUgPSA0LCBhbHBoYSA9IC44KSArDQogIHRoZW1lX2J3KCkgKw0KICBzY2FsZV9zaXplKCkgKyANCiAgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZSA9ICdTZXQxJykNCmBgYA0KI0ZlYXR1cmUgRW5naW5lZXJpbmcgLSBCYXNlIEZpbGUNCg0KSSdsbCBidWlsZCBzb21lIGJhc2ljIGZlYXR1cmVzIGZvciB3aGF0IEkgaGF2ZSBqdXN0IGV4cGxvcmVkLiBMZXQncyBzdGFydCBzY3JhcGluZyB2YXJpYXRpb24gZm9yIHNvbWUgb2YgdGhlc2Uga2V5IHdvcmRzIGFzIHdlbGwgYXMgdGhlIGNoYXJhY3RlciBsZW5ndGggb2YgZ2VuZSBhbmQgdmFyaWF0aW9uLg0KYGBge3J9DQpkZiA8LSBkZiAlPiUNCiAgbXV0YXRlKGZ1c2lvbl9pbl92YXJpYXRpb24gPSBpZmVsc2UoZ3JlcGwoJ0Z1c2lvbicsZGYkVmFyaWF0aW9uKSwxLDApLA0KICAgICAgICAgbXV0YXRpb25faW5fdmFyaWF0aW9uID0gaWZlbHNlKGdyZXBsKCdNdXRhdGlvbicsZGYkVmFyaWF0aW9uKSwxLDApLA0KICAgICAgICAgdHJ1bmNhdGluZ19pbl92YXJpYXRpb24gPSBpZmVsc2UoZ3JlcGwoJ1RydW5jYXRpbmcnLGRmJFZhcmlhdGlvbiksMSwwKSwNCiAgICAgICAgIGFtcGxpZmljYXRpb25faW5fdmFyaWF0aW9uID0gaWZlbHNlKGdyZXBsKCdBbXBsaWZpY2F0aW9uJyxkZiRWYXJpYXRpb24pLDEsMCksDQogICAgICAgICBpbnNlcnRpb25faW5fdmFyaWF0aW9uID0gaWZlbHNlKGdyZXBsKCdpbnNlcnRpb24nLGRmJFZhcmlhdGlvbiksMSwwKSwNCiAgICAgICAgIGRlbF9pbl92YXJpYXRpb24gPSBpZmVsc2UoZ3JlcGwoJ0RlbCcsZGYkVmFyaWF0aW9uKSwxLDApLA0KICAgICAgICAgZ2VuZV9sZW5ndGggPSBuY2hhcihhcy5jaGFyYWN0ZXIoR2VuZSkpLCANCiAgICAgICAgIHZhcmlhdGlvbl9sZW5ndGggPSBuY2hhcihhcy5jaGFyYWN0ZXIoVmFyaWF0aW9uKSkNCiAgICAgICAgICkNCmBgYA0KRHVtbXkgdmFyaWFibGVzIGZvciB0aGUgMTAgbW9zdCBjb21tb24gZ2VuZXMsIGV4YWN0IG1hdGNoaW5nLg0KYGBge3J9DQpkZiA8LSBkZiAlPiUNCiAgbXV0YXRlKGdfYnJjYTEgPSBpZmVsc2UoR2VuZSA9PSAnQlJDQTEnLDEsMCksDQogICAgICAgICBnX2JyY2EyID0gaWZlbHNlKEdlbmUgPT0gJ0JSQ0EyJywxLDApLA0KICAgICAgICAgZ190cDUzID0gaWZlbHNlKEdlbmUgPT0gJ1RQNTMnLDEsMCksDQogICAgICAgICBnX3B0ZW4gPSBpZmVsc2UoR2VuZSA9PSAnUFRFTicsMSwwKSwNCiAgICAgICAgIGdfa2l0ID0gaWZlbHNlKEdlbmUgPT0gJ0tJVCcsMSwwKSwNCiAgICAgICAgIGdfZWdmciA9IGlmZWxzZShHZW5lID09ICdFR0ZSJywxLDApLA0KICAgICAgICAgZ19icmFmID0gaWZlbHNlKEdlbmUgPT0gJ0JSQUYnLDEsMCksDQogICAgICAgICBnX2Nka24yYSA9IGlmZWxzZShHZW5lID09ICdDREtOMkEnLDEsMCksDQogICAgICAgICBnX2FibDEgPSBpZmVsc2UoR2VuZSA9PSAnQUJMMScsMSwwKSwNCiAgICAgICAgIGdfdHNjMiA9IGlmZWxzZShHZW5lID09ICdUU0MyJywxLDApLA0KICAgICAgICAgZ192aGwgPSBpZmVsc2UoR2VuZSA9PSAnVkhMJywxLDApKQ0KYGBgDQpEdW1teSB2YXJpYWJsZXMgZm9yIHRydW5jYXRpb25zLg0KYGBge3J9DQpkZiA8LSBkZiAlPiUNCiAgbXV0YXRlKHZfdHJ1bmNfbXV0YXRpb25zID0gaWZlbHNlKFZhcmlhdGlvbiA9PSAnVHJ1bmNhdGluZyBNdXRhdGlvbnMnLDEsMCksDQogICAgICAgICB2X3Q1ODEgPSBpZmVsc2UoVmFyaWF0aW9uID09ICdUNThJJywxLDApLA0KICAgICAgICAgdl9xNjFyID0gaWZlbHNlKFZhcmlhdGlvbiA9PSAnUTYxUicsMSwwKSwNCiAgICAgICAgIHZfcTYxbCA9IGlmZWxzZShWYXJpYXRpb24gPT0gJ1E2MUwnLDEsMCksDQogICAgICAgICB2X3E2MWggPSBpZmVsc2UoVmFyaWF0aW9uID09ICdRNjFIJywxLDApLA0KICAgICAgICAgdl9vdmVyZXhwcmVzc2lvbiA9IGlmZWxzZShWYXJpYXRpb24gPT0gJ092ZXJleHByZXNzaW9uJywxLDApLA0KICAgICAgICAgdl9nMTNkID0gaWZlbHNlKFZhcmlhdGlvbiA9PSAnRzEzRCcsMSwwKSwNCiAgICAgICAgIHZfZzEydiA9IGlmZWxzZShWYXJpYXRpb24gPT0gJ0cxMlYnLDEsMCksDQogICAgICAgICB2X2cxMmMgPSBpZmVsc2UoVmFyaWF0aW9uID09ICdHMTJDJywxLDApLA0KICAgICAgICAgdl9mdXNpb25zID0gaWZlbHNlKFZhcmlhdGlvbiA9PSAnRnVzaW9ucycsMSwwKSwNCiAgICAgICAgIHZfZTE3ayA9IGlmZWxzZShWYXJpYXRpb24gPT0gJ0UxN0snLDEsMCksDQogICAgICAgICB2X0RlbGV0aW9uID0gaWZlbHNlKFZhcmlhdGlvbiA9PSAnRGVsZXRpb24nLDEsMCksDQogICAgICAgICB2X0FtcGxpZmljYXRpb24gPSBpZmVsc2UoVmFyaWF0aW9uID09ICdBbXBsaWZpY2F0aW9uJywxLDApDQogICkNCmBgYA0KVGhlIGZpcnN0IGFuZCBsYXN0IHZhbHVlcyBmb3IgdGhlIGdlbmVzIHNlZW0gdG8gYmUgc2lnbmlmaWNhbnQsIGxldHMgY3JlYXRlIGR1bW15IHZhcmlhYmxlcyBmb3IgdGhlbSBhcyB3ZWxsLiBGaXJzdCwgdGhlIGxhc3QgbGV0dGVyIGluIHRoZSBnZW5lLg0KYGBge3J9DQpkZiRnZW5lIDwtIGFzLmNoYXJhY3RlcihzYXBwbHkoc3Ryc3BsaXQoYXMuY2hhcmFjdGVyKGRmJEdlbmUpLCIiKSx0YWlsLDEpKQ0KZGYkdmFsIDwtIDENCg0KZGYgPC0gZGYgJT4lDQogIHNwcmVhZChrZXkgPSBnZW5lLHZhbHVlID0gdmFsLCBmaWxsID0gMCxzZXAgPSAnX2VuZF8nKSANCmBgYA0KV2UgY2FuIGNyZWF0ZSB0aGUgZmlyc3QgbGV0dGVyIGZvciB0aGUgZ2VuZSBpbiBhIHNpbWlsYXIgd2F5Lg0KYGBge3J9DQpkZiRnZW5lIDwtIGFzLmNoYXJhY3RlcihzYXBwbHkoc3Ryc3BsaXQoYXMuY2hhcmFjdGVyKGRmJEdlbmUpLCIiKSxoZWFkLDEpKQ0KZGYkdmFsIDwtIDENCg0KZGYgPC0gZGYgJT4lDQogIHNwcmVhZChrZXkgPSBnZW5lLHZhbHVlID0gdmFsLCBmaWxsID0gMCxzZXAgPSAnX2JlZ2luXycpIA0KYGBgDQpXZSBjYW4gYWxzbyBjaGVjayB3aGF0IHZhcmlhdGlvbiBlbmQgaW4uDQpgYGB7cn0NCmRmJHZydG4gPC0gYXMuY2hhcmFjdGVyKHNhcHBseShzdHJzcGxpdChhcy5jaGFyYWN0ZXIoZGYkVmFyaWF0aW9uKSwiIiksaGVhZCwxKSkNCmRmJHZhbCA8LSAxDQoNCmRmIDwtIGRmICU+JQ0KICBzcHJlYWQoa2V5ID0gdnJ0bix2YWx1ZSA9IHZhbCwgZmlsbCA9IDAsc2VwID0gJ19lbmRfJykgDQpgYGANClRoZSBzZWNvbmQsIHRoaXJkIGFuZCBmb3VydGggZGlnaXRzIGFsc28gc2VlbSB0byBoYXZlIHNvbWUgdHlwZSBvZiBzaW1pbGFyIHBhcnR0ZXJucywgbGV0cyB1c2UgdGhlIHRvcCBvY2N1cmVuY2VzIHRvIGNyZWF0ZSBhIGZldyBtb3JlIGR1bW15IHZhcmlhYmxlcy4gTGF0ZXIsIHJlc2VhcmNoIHdpbGwgYmUgbmVlZGVkIGFzIHRvIHdoYXQgdGhpcyByZXByZXNlbnRzLg0KYGBge3J9DQpkZiR2cnRuIDwtIGFzLmNoYXJhY3RlcihzdWJzdHIoYXMuY2hhcmFjdGVyKGRmJFZhcmlhdGlvbiksMiw0KSkNCmRmJHZhbCA8LSAxDQoNCmRmIDwtIGRmICU+JQ0KICBncm91cF9ieSh2cnRuLHZhbCkgJT4lDQogIHN1bW1hcml6ZShjb3VudCA9IG4oKSkgJT4lDQogIG11dGF0ZShjb21fbWlkX3ZydG4gPSBpZmVsc2UoY291bnQ+MjUsdnJ0biwiIikpICU+JQ0KICByaWdodF9qb2luKGRmLCBieSA9ICd2cnRuJykgJT4lDQogIHVuZ3JvdXAoKQ0KICANCmRmIDwtIGRmICU+JQ0KICBzcHJlYWQoa2V5PWNvbV9taWRfdnJ0bix2YWx1ZSA9ICd2YWwueCcsIGZpbGwgPSAwLCBzZXAgPSAnX21pZDNfJykgJT4lDQogIHNlbGVjdCgtdnJ0biwtY291bnQsLXZhbC55KQ0KYGBgDQoNCiNGZWF0dXJlIEVuZ2luZWVyaW5nIC0gVGV4dCBGaWxlDQoNClRoZSB0ZXh0IGZpbGVzIHByb3ZpZGVkIGFyZSBxdWl0ZSBleHRlbnNpdmUgd2l0aCB2ZXJ5IGxvbmcgd3JpdGUgdXBzIGJ5IHRoZSByZXNlYXJjaGVycy4gSSB3aWxsIHRha2UgYSB2ZXJ5IGxpZ2h0IHRleHQgbWluaW5nIGFwcHJvYWNoIGhlcmUgYnkgY29udmVydGluZyB0aGlzIHRvIGEgQ29ycHVzIGFuZCB0aGVuIGEgc3BhcnNlIG1hdHJpeCBmb3IgdGhlIG1vcmUgY29tbW9uIHdvcmRzLg0KDQpGaXJzdCwgdGhlIGNvcnB1cyBjYW4gYmUgY3JlYXRlZCBhbmQgY2xlYW5lZCB1cCB1c2luZyB0aGUgc3RhbmRhcmQgdGV4dC1taW5pbmcgYXBwcm9hY2guDQpgYGB7cn0NCnRfY29ycHVzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2UoZHQkVGV4dCkpDQp0X2NvcnB1cyA8LSB0X2NvcnB1cyAlPiUNCiAgICAgICAgICAgICAgICB0bV9tYXAoc3RyaXBXaGl0ZXNwYWNlKSAlPiUNCiAgICAgICAgICAgICAgICB0bV9tYXAocmVtb3ZlTnVtYmVycykgJT4lDQogICAgICAgICAgICAgICAgdG1fbWFwKHJlbW92ZVB1bmN0dWF0aW9uKSAlPiUNCiAgICAgICAgICAgICAgICB0bV9tYXAoY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkgJT4lDQogICAgICAgICAgICAgICAgdG1fbWFwKHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoImVuZ2xpc2giKSkgJT4lDQogICAgICAgICAgICAgICAgdG1fbWFwKHN0ZW1Eb2N1bWVudCwgbGFuZ3VhZ2U9ImVuZ2xpc2giKQ0KDQpgYGANCkknbGwgbm93IGNvbnZlcnQgaXQgdG8gYSBtYXRyaXggYW5kIHVzZSBURi1JREYgZm9yIHdvcmQgaW1wb3J0YW5jZS4gV2UgY2FuIGpvaW4gYmFjayB0byBvdXIgYmFzZSBkYXRhc2V0Lg0KYGBge3J9DQpkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KHRfY29ycHVzLCBjb250cm9sID0gbGlzdCh3ZWlnaHRpbmcgPSB3ZWlnaHRUZklkZikpDQpkdG0gPC0gcmVtb3ZlU3BhcnNlVGVybXMoZHRtLCAwLjk1KQ0KZGYgPC0gIGNiaW5kKGRmLGFzLm1hdHJpeChkdG0pKQ0KYGBgDQpMYXN0bHksIGxldCdzIGRvIHNlbnRpbWVuZXQgYW5hbHlzaXMgb24gYWxsIHRoZXNlIHRlcm1zIHRvIGRldGVybWluZSBwb3NpdGl2ZS8gbmVnYXRpdmUgcmVsYXRpb25zaGlwcy4NCmBgYHtyfQ0KZGYgPC0gY2JpbmQoZGYsZ2V0X25yY19zZW50aW1lbnQoYXMuY2hhcmFjdGVyKGRmJFRleHQpKSkgDQpgYGANCkxldCdzIHBsb3Qgb3V0IHRoZSBzZW50aW1lbnRzIHRoYXQgd2VyZSBjcmVhdGVkLg0KYGBge3J9DQpkZlssMzg2NjozODc1XSAlPiUNCiAgZ2F0aGVyKCkgJT4lDQogIGdncGxvdChhZXModmFsdWUpKSArDQogIGdlb21fYmFyKGZpbGwgPSAnZ3JlZW4nKSArDQogIGZhY2V0X3dyYXAofmtleSxzY2FsZXMgPSAnZnJlZScpICsNCiAgdGhlbWVfYncoKQ0KYGBgDQojIE1vZGVsIEJ1aWxkaW5nDQpJIHdpbGwgbm93IHNldCB1cCB0aGUgZnJhbWV3b3JrIHRvIHJ1biBhIEdCVCBtb2RlbC4gSSB3aWxsIHVzZSBrLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbiB0byBnZXQgYW4gZXN0aW1hdGUuDQpgYGB7cn0NCnNldC5zZWVkKDcpDQpjdl9mb2xkcyA8LSBjcmVhdGVGb2xkcyhkZiRDbGFzc1tkZiRDbGFzcyAhPSAnLTknXSwgaz01LCBsaXN0PVRSVUUsIHJldHVyblRyYWluPUZBTFNFKSANCmBgYA0KV2l0aCB0aGUgZm9sZHMgc2V0LCBJJ2xsIGRpdmlkZSB0aGUgZGF0YSBiYWNrIGludG8gb3VyIHRyYWluIGFuZCB0ZXN0IHNldHMuDQpgYGB7cn0NCnRyYWluX21hdHJpeCA8LSBkZiAlPiUNCiAgZmlsdGVyKGRzID09ICdUcmFpbicpICU+JQ0KICBzZWxlY3QoLUlELC1HZW5lLC1WYXJpYXRpb24sLUNsYXNzLCAtZHMsIC0gVGV4dCkgJT4lDQogIGFwcGx5KDIsYXMuY2hhcmFjdGVyKSAlPiUNCiAgYXBwbHkoMixhcy5pbnRlZ2VyKSAlPiUNCiAgYXMubWF0cml4KCkgJT4lDQogIE1hdHJpeChzcGFyc2UgPSBUKQ0KDQp0ZXN0X21hdHJpeCA8LSBkZiAlPiUNCiAgZmlsdGVyKGRzID09ICdUZXN0JykgJT4lDQogIHNlbGVjdCgtSUQsLUdlbmUsLVZhcmlhdGlvbiwtQ2xhc3MsIC1kcywtVGV4dCkgJT4lDQogIGFwcGx5KDIsYXMuY2hhcmFjdGVyKSAlPiUNCiAgYXBwbHkoMixhcy5pbnRlZ2VyKSAlPiUNCiAgYXMubWF0cml4KCkgJT4lDQogIE1hdHJpeChzcGFyc2UgPSBUKQ0KYGBgDQpMZXQncyBnaXZlIG91ciB0cmFpbmluZyBkYXRhIHRoZSBjb3JyZWN0IGxhYmVscywgYXMgd2VsbCBhcyBjcmVhdGUgdGhlIHZlY3RvciBvZiBvdXIgdGVzdCBpZHMgZm9yIGxhdGVyLg0KYGBge3J9DQp0cmFpbl95IDwtIGRmICU+JQ0KICBmaWx0ZXIoZHMgPT0gJ1RyYWluJykgJT4lDQogIHNlbGVjdChDbGFzcykgJT4lDQogIGFwcGx5KDIsYXMuY2hhcmFjdGVyKSAlPiUNCiAgYXBwbHkoMixhcy5pbnRlZ2VyKSAtMQ0KDQp0ZXN0X2lkcyA8LSBkZltkZiRkcyA9PSAnVGVzdCcsJ0lEJ10NCmBgYA0KTm93IGxldHMgY3JlYXRlIG91ciBYR0IgTWF0cmljZXMuDQpgYGB7cn0NCmR0cmFpbiA8LSB4Z2IuRE1hdHJpeChkYXRhID0gdHJhaW5fbWF0cml4LCBsYWJlbCA9IHRyYWluX3kpDQpkdGVzdCA8LSB4Z2IuRE1hdHJpeChkYXRhID0gdGVzdF9tYXRyaXgpICAgDQpgYGANClRoZSBwYXJhbWF0ZXJzIGNhbiBiZSBmb3VuZCB0aHJvdWdoIGEgZ3JpZCBzZWFyY2ggb3IgdHJpYWwgYW5kIGVycm9yLg0KYGBge3J9DQpwYXJhbSA8LSBsaXN0KGJvb3N0ZXIgPSAnZ2J0cmVlJywNCiAgICAgICAgICAgICAgb2JqZWN0aXZlID0gJ211bHRpOnNvZnRwcm9iJywNCiAgICAgICAgICAgICAgZXZhbF9tZXRyaWMgPSAnbWxvZ2xvc3MnLA0KICAgICAgICAgICAgICBudW1fY2xhc3MgPSA5LA0KICAgICAgICAgICAgICBldGEgPSAuMiwNCiAgICAgICAgICAgICAgZ2FtbWEgPSAxLA0KICAgICAgICAgICAgICBtYXhfZGVwdGggPSA2LA0KICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0ID0gMSwNCiAgICAgICAgICAgICAgc3Vic2FtcGxlID0gLjcsDQogICAgICAgICAgICAgIGNvbHNhbXBsZV9ieXRyZWUgPSAuNykgDQpgYGANCkknbGwgcnVuIHRvIGZpbmQgdGhlIHByb3BlciByb3VuZHMgbmVlZGVkLCBhcyB3ZWxsIGdldCBhIHNlbnNlIG9mIG91ciBhdmVyYWdlIGxvZy1sb3NzIGVycm9yIG9uIHRoZSB0ZXN0IHNldHMuDQpgYGB7cn0NCnhnYl9jdiA8LSB4Z2IuY3YoZGF0YSA9IGR0cmFpbiwNCiAgICAgICAgICAgICAgICAgcGFyYW1zID0gcGFyYW0sDQogICAgICAgICAgICAgICAgIG5yb3VuZHMgPSAxMDAwLA0KICAgICAgICAgICAgICAgICBtYXhpbWl6ZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICBwcmVkaWN0aW9uID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgZm9sZHMgPSBjdl9mb2xkcywNCiAgICAgICAgICAgICAgICAgcHJpbnRfZXZlcnlfbiA9IDEwLA0KICAgICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSAxMDApDQpgYGANCldlIGNhbiBub3cgYnVpbGQgb3VyIGZpbmFsIG1vZGVsLg0KYGBge3J9DQpyb3VuZHMgPC0gd2hpY2gubWluKHhnYl9jdiRldmFsdWF0aW9uX2xvZyR0ZXN0X21sb2dsb3NzX21lYW4pDQoNCnhnYl9tb2RlbCA8LSB4Z2IudHJhaW4oZGF0YSA9IGR0cmFpbiwNCiAgICAgICAgICAgICAgICAgICAgICAgcGFyYW1zID0gcGFyYW0sDQogICAgICAgICAgICAgICAgICAgICAgIHdhdGNobGlzdCA9IGxpc3QodHJhaW4gPSBkdHJhaW4pLA0KICAgICAgICAgICAgICAgICAgICAgICBucm91bmRzID0gcm91bmRzLA0KICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gMSwNCiAgICAgICAgICAgICAgICAgICAgICAgcHJpbnRfZXZlcnlfbiA9IDUpDQpgYGANClRoZSBpbXBvcnRhbmNlIG1hdHJpeCBpcyBhcyBmb2xsb3dzLCBzZW50aW1lbnQgYW5hbHlzaXMgaGVscGVkIHF1aXRlIGEgYml0IGFsb25nIHdpdGggYSBmZXcgb2Ygb3VyIGdlbmUvdmFyaWF0aW9uIHZhcmlhYmxlcy4NCmBgYHtyfQ0KbmFtZXMgPC0gZGltbmFtZXModHJhaW5fbWF0cml4KVtbMl1dIA0KDQppbXBvcnRhbmNlX21hdHJpeCA8LSB4Z2IuaW1wb3J0YW5jZShuYW1lcyxtb2RlbD14Z2JfbW9kZWwpDQp4Z2IucGxvdC5pbXBvcnRhbmNlKGltcG9ydGFuY2VfbWF0cml4LHRvcF9uPTIwKQ0KYGBgDQpXZSBjYW4gbm93IGdldCBvdXIgcHJlZGljdGlvbnMgb24gdGhlIHRlc3QgZGF0YS4NCmBgYHtyfQ0KcHJlZGljdGlvbnMgPC0geGdiX21vZGVsICU+JQ0KICBwcmVkaWN0KGR0ZXN0KSAlPiUNCiAgbWF0cml4KG5yb3cgPSBucm93KGR0ZXN0KSwgbmNvbD05LGJ5cm93ID0gVCkgJT4lDQogIGFzLmRhdGEuZnJhbWUoKSANCmBgYA0KTGV0J3MgZm9ybWF0IHRoZW0gY29ycmVjdGx5IGFuZCBzZWUgaWYgZWFjaCByb3cgc3VtcyBwcm9wZXJseSB0byAxIGFzIGEgc2FuaXR5IGNoZWNrLg0KYGBge3J9DQpjb2xuYW1lcyhwcmVkaWN0aW9ucykgPC0gYygiY2xhc3MxIiwiY2xhc3MyIiwiY2xhc3MzIiwiY2xhc3M0IiwiY2xhc3M1IiwiY2xhc3M2IiwiY2xhc3M3IiwiY2xhc3M4IiwiY2xhc3M5IikNCiAgDQpkZl9wcmVkcyA8LSBkYXRhLmZyYW1lKElEPXRlc3RfaWRzLHByZWRpY3Rpb25zKQ0KDQpkZl9wcmVkcyAlPiUNCiAgc2VsZWN0KC1JRCkgJT4lDQogIGFwcGx5KDEsc3VtKSAlPiUNCiAgaGVhZChuPTI1KQ0KYGBgDQpBbmQgbm93IG91ciBzdWJtaXNzaW9uIGZpbGUuDQpgYGB7cn0NCndyaXRlLmNzdihkZl9wcmVkcywnc3VibWlzc2lvbl9uYi5jc3YnLHJvdy5uYW1lcyA9IEYpDQpgYGANCldpdGggdGhlIHZlcnkgbGlnaHQgZmVhdHVyZSBlbmdpbmVlcmluZyBhbmQgbW9kZWwgdHVuaW5nLCBJIGFtIGFyb3VuZCB0aGUgNTB0aCBwZXJjZW50aWxlLiBNb3JlIHJlc2VhcmNoIG5lZWRzIHRvIGJlIGRvbmUgYXJvdW5kIGhvdyB0byB1bmRlcnN0YW5kIHRoZSBnZW5lcyBhbmQgbXV0YXRpb25zLCBhcyB3ZWxsIGFzIGRlZXBlciB0ZXh0IG1pbmluZyBhbmQgZmVhdHVyZSBjcmVhdGlvbi4NCg==