Objetivos
- Mostrar la implementación en
caret
de diferentes algoritmos de Ensamble Learning para problemas de clasificación
- Mostrar los problemas de los datasets desbalanceados y algunos métodos posibles para lidiar con el problema
Carga de librerías y datos
library(caret)
library(tidyverse)
library(rpart)
Luego, cargamos los datos y formateamos un poco algunas etiquetas:
load('../data/EPH_2015_II.RData')
data$pp03i<-factor(data$pp03i, labels=c('1-SI', '2-No', '9-NS'))
data$intensi<-factor(data$intensi, labels=c('1-Sub_dem', '2-SO_no_dem',
'3-Ocup.pleno', '4-Sobreoc',
'5-No trabajo', '9-NS'))
data$pp07a<-factor(data$pp07a, labels=c('0-NC',
'1-Menos de un mes',
'2-1 a 3 meses',
'3-3 a 6 meses',
'4-6 a 12 meses',
'5-12 a 60 meses',
'6-Más de 60 meses',
'9-NS'))
data <- data %>%
mutate(imp_inglab1=factor(imp_inglab1, labels=c('non_miss','miss')))
Ahora, nuestra variable a predecir es el indicador imp_inglab
. Por ende, elimonamos la \(p21\).
df_train <- data %>%
select(-p21)
Lo primero que vamos a hacer es crear una partición de datos:
set.seed(1234)
tr_index <- createDataPartition(y=df_train$imp_inglab1,
p=0.8,
list=FALSE)
Y generamos dos datasets:
train <- df_train[tr_index,]
test <- df_train[-tr_index,]
Random Forest
La idea ahora es poder entrenar un modelo random forest. Pero… lo van a hacer ustedes. Para ello, deberán consultar la documentación de caret y buscar cuál es la mejor función para entrenar el modelo y sus hiperparámetros.
Pista: buscar el método ranger
.
Usaremos la partición original entre Train-Test. Recordemos los pasos a seguir
- Generar el esquema de validación cruzada
- Generar el grid de hiperparámetros
- Tunear el modelo
- Seleccionar y estimar el modelo final
- Validar y evaluar contra test-set
set.seed(5699)
cv_index_rf <- createFolds(y=train$imp_inglab1,
k=5,
list=TRUE,
returnTrain=TRUE)
fitControlrf <- trainControl(
index=cv_index_rf,
method="cv",
number=5,
summaryFunction = twoClassSummary,
classProbs=TRUE
)
# Generar grid
grid_rf <- expand.grid(mtry=c(5,10,15, 25),
min.node.size=c(5,10,15,20),
splitrule='gini'
)
# Tunear
t0 <- proc.time()
rf_fit_orig <- train(imp_inglab1 ~ . ,
data = train,
method = "ranger",
trControl = fitControlrf,
tuneGrid = grid_rf,
metric='ROC'
)
proc.time() - t0
saveRDS(rf_fit_orig, '../models/rf_fit_orig.RDS')
rf_fit_orig
Random Forest
19519 samples
25 predictor
2 classes: 'non_miss', 'miss'
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15616, 15615, 15615, 15615, 15615
Resampling results across tuning parameters:
mtry min.node.size ROC Sens Spec
5 5 0.7872581 0.9958132 0.04006167
5 10 0.7867366 0.9963285 0.03605447
5 15 0.7872137 0.9966506 0.03730697
5 20 0.7862695 0.9969082 0.03129758
10 5 0.7885261 0.9781643 0.14671880
10 10 0.7902176 0.9789372 0.13645503
10 15 0.7902504 0.9799678 0.13144689
10 20 0.7907318 0.9820290 0.12518875
15 5 0.7872475 0.9691465 0.18202139
15 10 0.7884391 0.9712721 0.17325855
15 15 0.7901788 0.9734622 0.16775167
15 20 0.7899749 0.9751369 0.15998883
25 5 0.7849605 0.9598068 0.20831051
25 10 0.7876549 0.9638647 0.20155050
25 15 0.7880576 0.9661192 0.19253766
25 20 0.7890911 0.9691465 0.18427640
Tuning parameter 'splitrule' was held constant at a value of gini
ROC was used to select the optimal model using the largest value.
The final values used for the model were mtry = 10, splitrule =
gini and min.node.size = 20.
confusionMatrix(predict(rf_fit_orig, test), test$imp_inglab1)
Confusion Matrix and Statistics
Reference
Prediction non_miss miss
non_miss 3787 871
miss 94 127
Accuracy : 0.8022
95% CI : (0.7908, 0.8133)
No Information Rate : 0.7954
P-Value [Acc > NIR] : 0.1241
Kappa : 0.1449
Mcnemar's Test P-Value : <2e-16
Sensitivity : 0.9758
Specificity : 0.1273
Pos Pred Value : 0.8130
Neg Pred Value : 0.5747
Prevalence : 0.7954
Detection Rate : 0.7762
Detection Prevalence : 0.9547
Balanced Accuracy : 0.5515
'Positive' Class : non_miss
- ¿Qué pueden decir de este modelo?
- ¿Funciona mejor que un árbol individual?
- ¿Qué sucede con su performance entre los
miss
Datasets desbalanceados… ¿qué hacer?
Efectivamente, parte del problema de este dataset es que tenemos una variable dependiente sumamente desbalanceada:
ggplot(train) +
geom_bar(aes(x=imp_inglab1)) +
theme_minimal()

Matriz de confusión
Ya estamos familiarizades con la matriz de confusión. Básicamente, es una forma rápida de comparar las predicciones del modelo con los valores reales.
Así, la métrica más simple es la siguiente:
\(Accuracy = \frac{TP + TN}{N} = \frac{TP + TN}{TP + TN + FP + FN}\)
Simplemente, calcula la proporción de casos bien clasificados (\(TP + TN\)) sobre el total. Pero esta métrica tiene algunos problemas. El más importante es que tiende a ser una medida engañosa de la performance de un modelo en un dataset desbalanceado.
Un primer punto importante es poder seleccionar una métrica adecuada para evaluar nuestro modelo. Existen dos grandes familias de medidas:
- Dependiente del umbral: Esto incluye métricas como precisión, recall y puntaje F1, que requieren una matriz de confusión para ser calculada usando un límite duro en las probabilidades pronosticadas. Estas métricas suelen ser bastante pobres en el caso de clases desequilibradas, ya que el software estadístico utiliza de manera inapropiada un umbral predeterminado de 0,50, lo que hace que el modelo prediga que todas las observaciones pertenecen a la clase mayoritaria.
\(Precision = \frac{TP}{TP+FP}\) Ratio entre verdaderos positivos y los que el modelo clasifica como positivos. ¿Qué proporción de los que clasifcamos como positivos son efectivamente positivos?
\(Recall = \frac{TP}{TP+FN}\) También llamada, \(Sensitividad\), Ratio entre los positivos bien clasificados y el total de los que son positivos en realidad. ¿Qué proporción de los que efectivamente positivos hemos clasificado bien?
\(F1-Score = 2 \times \frac{Precision \times Recall}{Recall + Precision}\)
- Independiente del umbral: Esto incluye métricas como área bajo la curva ROC (AUC), que cuantifica la tasa positiva verdadera en función de la tasa de falsos positivos para una variedad de umbrales de clasificación. Otra forma de interpretar esta métrica es la probabilidad de que una instancia positiva aleatoria tenga una probabilidad estimada más alta que una instancia negativa aleatoria.
En líneas generales, existen cuatro formas generales de lidiar con el problema de las clases desbalanceadas:
- Usar modelos ponderados
- Down-sampling es decir, submuestrear las instancias sobrerrepresentadas
- Up-sampling, es decir, sobremuestrar las instancias subrrepresentadas
- Synthetic minority sampling technique (SMOTE): hace down-sampling de la clase mayoritaria y “sintetiza” nuevas instancias minoritarias mediante interpolación
Utilizar pesos en el modelo
Para esto podemos utilizar el argumento weights
en la función train
(esto supone que el modelo seleccionado puede manejar pesos).
model_weights <- ifelse(train$imp_inglab1 == "non_miss",
(1/table(train$imp_inglab1)[1]) * 0.5,
(1/table(train$imp_inglab1)[2]) * 0.5)
fitControlrf$seeds <- rf_fit_orig$control$seeds
Y entrenamos el modelo:
t0 <- proc.time()
rf_fit_wei <- train(imp_inglab1 ~ . ,
data = train,
method = "ranger",
trControl = fitControlrf,
tuneGrid = grid_rf,
weights = model_weights,
metric='ROC'
)
proc.time() - t0
user system elapsed
2863.763 13.410 481.441
saveRDS(rf_fit_wei, '../models/rf_fit_wei.RDS')
Métodos de remuestreo
Probemos, ahora, métodos de remuestreo: up sampling y down sampling.
# Nuevo fitControl con upsample
fitControlrf_imb <- trainControl(
index=cv_index_rf,
method="cv",
number=5,
summaryFunction = twoClassSummary,
classProbs=TRUE,
sampling = 'up'
)
# Tunear upsample
fitControlrf_imb$seeds <- rf_fit_orig$control$seeds
t0 <- proc.time()
rf_fit_up <- train(imp_inglab1 ~ . ,
data = train,
method = "ranger",
trControl = fitControlrf_imb,
tuneGrid = grid_rf,
metric='ROC'
)
proc.time() - t0
saveRDS(rf_fit_up, '../models/rf_fit_up.RDS')
# Tunear downsample
fitControlrf_imb$sampling <- "down"
fitControlrf_imb$seeds <- rf_fit_orig$control$seeds
t0 <- proc.time()
rf_fit_down <- train(imp_inglab1 ~ . ,
data = train,
method = "ranger",
trControl = fitControlrf_imb,
tuneGrid = grid_rf,
metric='ROC'
)
proc.time() - t0
saveRDS(rf_fit_down, '../models/rf_fit_down.RDS')
Comparemos todos los modelos entrenados hasta ahora. Pero ya que estamos, incorporemos el modelo de árbol simple que habíamos entrenado la semana pasada:
cart_final <- train(imp_inglab1 ~ . ,
data = train,
method = "rpart2",
tuneGrid = data.frame(maxdepth=6),
control = rpart.control(cp=0.000001)
)
Colocamos todos los modelos en una lista:
model_list <- list(cart = cart_final,
rf_original = rf_fit_orig,
rf_weighted = rf_fit_wei,
rf_down = rf_fit_down,
rf_up = rf_fit_up)
Extraigamos algunas métricas resumen de las matrices de confusión:
extract_conf_metrics <- function(model, data, obs){
preds <- predict(model, data)
c<-confusionMatrix(preds, obs)
results <- c(c$overall[1], c$byClass)
return(results)
}
model_metrics <- model_list %>%
map(extract_conf_metrics, data=test, obs = test$imp_inglab1) %>%
do.call(rbind,.) %>%
as.data.frame()
Puede verse, entonces, cómo la performace de los diferentes modelos cambia en los diferentes tunnings…
model_metrics %>%
select('Accuracy', 'Precision', 'Recall') %>%
t()
cart rf_original rf_weighted rf_down rf_up
Accuracy 0.7913507 0.8022136 0.7427752 0.6632507 0.7675753
Precision 0.8076510 0.8130099 0.8819081 0.8951271 0.8642270
Recall 0.9683071 0.9757794 0.7812419 0.6531822 0.8397320
Observando específicamente las métricas de \(Precision\), \(Recall\) y \(Accuracy\) puede verse que el modelo de random forest original parece mejorar las diferentes métricas.
AdaBoost
Por último, vamos a entrenar un modelo de Adaboost.
fitControlrf$seeds <- rf_fit_orig$control$seeds
#grid_ada <- expand.grid(mfinal=c(100,150), maxdepth=c(10,20), coeflearn='Breiman')
grid_ada <- expand.grid(nIter=c(100,150), method='Adaboost.M1')
t0 <- proc.time()
adaboost_fit_orig <- train(imp_inglab1 ~ . ,
data = train,
method = "adaboost",
trControl = fitControlrf,
tuneGrid = grid_ada,
metric='ROC'
)
adaboost_fit_orig
AdaBoost Classification Trees
19519 samples
25 predictor
2 classes: 'non_miss', 'miss'
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15616, 15615, 15615, 15615, 15615
Resampling results across tuning parameters:
nIter ROC Sens Spec
100 0.7661744 0.9148470 0.3064605
150 0.7683175 0.9162641 0.3037039
Tuning parameter 'method' was held constant at a value
of Adaboost.M1
ROC was used to select the optimal model using the
largest value.
The final values used for the model were nIter = 150 and
method = Adaboost.M1.
Comparando los modelos generados
Volvamos a armar la lista de modelos:
model_list <- list(cart = cart_final,
rf_original = rf_fit_orig,
rf_weighted = rf_fit_wei,
rf_down = rf_fit_down,
rf_up = rf_fit_up,
adaboost = adaboost_fit_orig)
Y, finalmente, generemos una tabla que contenga todo:
model_metrics <- model_list %>%
map(extract_conf_metrics, data=test, obs = test$imp_inglab1) %>%
do.call(rbind,.) %>%
as.data.frame() %>%
t()
model_metrics
cart rf_original rf_weighted
Accuracy 0.7913507 0.8022136 0.7427752
Sensitivity 0.9683071 0.9757794 0.7812419
Specificity 0.1032064 0.1272545 0.5931864
Pos Pred Value 0.8076510 0.8130099 0.8819081
Neg Pred Value 0.4557522 0.5746606 0.4108258
Precision 0.8076510 0.8130099 0.8819081
Recall 0.9683071 0.9757794 0.7812419
F1 0.8807124 0.8869891 0.8285285
Prevalence 0.7954499 0.7954499 0.7954499
Detection Rate 0.7702398 0.7761836 0.6214388
Detection Prevalence 0.9536790 0.9547038 0.7046526
Balanced Accuracy 0.5357568 0.5515170 0.6872142
rf_down rf_up adaboost
Accuracy 0.6632507 0.7675753 0.7893011
Sensitivity 0.6531822 0.8397320 0.9149704
Specificity 0.7024048 0.4869739 0.3006012
Pos Pred Value 0.8951271 0.8642270 0.8357261
Neg Pred Value 0.3424524 0.4386282 0.4761905
Precision 0.8951271 0.8642270 0.8357261
Recall 0.6531822 0.8397320 0.9149704
F1 0.7552510 0.8518035 0.8735547
Prevalence 0.7954499 0.7954499 0.7954499
Detection Rate 0.5195737 0.6679647 0.7278131
Detection Prevalence 0.5804468 0.7729043 0.8708752
Balanced Accuracy 0.6777935 0.6633530 0.6077858
Puede verse que cart, rf_original y adaboost presentan valores similares en accuracy. A su vez, al observar la relación entre precision y recall puede verse que el modelo que mejor balancea los dos es rf_up. Cuál elegir dependerá del objetivo del análisis.
LS0tCnRpdGxlOiAiRW5zYW1ibGUgTGVhcm5pbmcgLSBCYWdnaW5nLCBSYW5kb20gRm9yZXN0IHkgQWRhQm9vc3QiCmF1dGhvcjogIkdlcm3DoW4gUm9zYXRpIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgojIyBPYmpldGl2b3MKCi0gTW9zdHJhciBsYSBpbXBsZW1lbnRhY2nDs24gZW4gYGNhcmV0YCBkZSBkaWZlcmVudGVzIGFsZ29yaXRtb3MgZGUgRW5zYW1ibGUgTGVhcm5pbmcgcGFyYSBwcm9ibGVtYXMgZGUgY2xhc2lmaWNhY2nDs24KLSBNb3N0cmFyIGxvcyBwcm9ibGVtYXMgZGUgbG9zIGRhdGFzZXRzIGRlc2JhbGFuY2VhZG9zIHkgYWxndW5vcyBtw6l0b2RvcyBwb3NpYmxlcyBwYXJhIGxpZGlhciBjb24gZWwgcHJvYmxlbWEKCiMjIENhcmdhIGRlIGxpYnJlcsOtYXMgeSBkYXRvcwpgYGB7ciwgbWVzc2FnZT1GQUxTRX0KbGlicmFyeShjYXJldCkKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkocnBhcnQpCmBgYAoKTHVlZ28sIGNhcmdhbW9zIGxvcyBkYXRvcyB5IGZvcm1hdGVhbW9zIHVuIHBvY28gYWxndW5hcyBldGlxdWV0YXM6CmBgYHtyfQpsb2FkKCcuLi9kYXRhL0VQSF8yMDE1X0lJLlJEYXRhJykKCmRhdGEkcHAwM2k8LWZhY3RvcihkYXRhJHBwMDNpLCBsYWJlbHM9YygnMS1TSScsICcyLU5vJywgJzktTlMnKSkKCgoKZGF0YSRpbnRlbnNpPC1mYWN0b3IoZGF0YSRpbnRlbnNpLCBsYWJlbHM9YygnMS1TdWJfZGVtJywgJzItU09fbm9fZGVtJywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzMtT2N1cC5wbGVubycsICc0LVNvYnJlb2MnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc1LU5vIHRyYWJham8nLCAnOS1OUycpKQoKZGF0YSRwcDA3YTwtZmFjdG9yKGRhdGEkcHAwN2EsIGxhYmVscz1jKCcwLU5DJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICcxLU1lbm9zIGRlIHVuIG1lcycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnMi0xIGEgMyBtZXNlcycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnMy0zIGEgNiBtZXNlcycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnNC02IGEgMTIgbWVzZXMnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzUtMTIgYSA2MCBtZXNlcycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnNi1Nw6FzIGRlIDYwIG1lc2VzJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc5LU5TJykpCgoKCmRhdGEgPC0gZGF0YSAlPiUKICAgICAgICBtdXRhdGUoaW1wX2luZ2xhYjE9ZmFjdG9yKGltcF9pbmdsYWIxLCBsYWJlbHM9Yygnbm9uX21pc3MnLCdtaXNzJykpKQpgYGAKCkFob3JhLCBudWVzdHJhIHZhcmlhYmxlIGEgcHJlZGVjaXIgZXMgZWwgaW5kaWNhZG9yIGBpbXBfaW5nbGFiYC4gUG9yIGVuZGUsIGVsaW1vbmFtb3MgbGEgJHAyMSQuCmBgYHtyfQpkZl90cmFpbiA8LSBkYXRhICU+JQogICAgICAgIHNlbGVjdCgtcDIxKQpgYGAKCkxvIHByaW1lcm8gcXVlIHZhbW9zIGEgaGFjZXIgZXMgY3JlYXIgdW5hIHBhcnRpY2nDs24gZGUgZGF0b3M6CmBgYHtyfQpzZXQuc2VlZCgxMjM0KQp0cl9pbmRleCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHk9ZGZfdHJhaW4kaW1wX2luZ2xhYjEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcD0wLjgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGlzdD1GQUxTRSkKYGBgCgpZIGdlbmVyYW1vcyBkb3MgZGF0YXNldHM6CmBgYHtyfQp0cmFpbiA8LSBkZl90cmFpblt0cl9pbmRleCxdCnRlc3QgPC0gZGZfdHJhaW5bLXRyX2luZGV4LF0KYGBgCgojIyBSYW5kb20gRm9yZXN0CkxhIGlkZWEgYWhvcmEgZXMgcG9kZXIgZW50cmVuYXIgdW4gbW9kZWxvIHJhbmRvbSBmb3Jlc3QuIFBlcm8uLi4gbG8gdmFuIGEgaGFjZXIgdXN0ZWRlcy4gUGFyYSBlbGxvLCBkZWJlcsOhbiBjb25zdWx0YXIgW2xhIGRvY3VtZW50YWNpw7NuIGRlIGNhcmV0XShodHRwOi8vdG9wZXBvLmdpdGh1Yi5pby9jYXJldC9hdmFpbGFibGUtbW9kZWxzLmh0bWwpIHkgYnVzY2FyIGN1w6FsIGVzIGxhIG1lam9yIGZ1bmNpw7NuIHBhcmEgZW50cmVuYXIgZWwgbW9kZWxvIHkgc3VzIGhpcGVycGFyw6FtZXRyb3MuCgpQaXN0YTogYnVzY2FyIGVsIG3DqXRvZG8gYHJhbmdlcmAuCgpVc2FyZW1vcyBsYSBwYXJ0aWNpw7NuIG9yaWdpbmFsIGVudHJlIFRyYWluLVRlc3QuIFJlY29yZGVtb3MgbG9zIHBhc29zIGEgc2VndWlyCgoxLiBHZW5lcmFyIGVsIGVzcXVlbWEgZGUgdmFsaWRhY2nDs24gY3J1emFkYQoyLiBHZW5lcmFyIGVsIGdyaWQgZGUgaGlwZXJwYXLDoW1ldHJvcwozLiBUdW5lYXIgZWwgbW9kZWxvCjQuIFNlbGVjY2lvbmFyIHkgZXN0aW1hciBlbCBtb2RlbG8gZmluYWwKNS4gVmFsaWRhciB5IGV2YWx1YXIgY29udHJhIHRlc3Qtc2V0CmBgYHtyfQpzZXQuc2VlZCg1Njk5KQpjdl9pbmRleF9yZiA8LSBjcmVhdGVGb2xkcyh5PXRyYWluJGltcF9pbmdsYWIxLAogICAgICAgICAgICAgICAgICAgICAgICBrPTUsCiAgICAgICAgICAgICAgICAgICAgICAgIGxpc3Q9VFJVRSwKICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuVHJhaW49VFJVRSkKCmZpdENvbnRyb2xyZiA8LSB0cmFpbkNvbnRyb2woCiAgICAgICAgaW5kZXg9Y3ZfaW5kZXhfcmYsCiAgICAgICAgbWV0aG9kPSJjdiIsCiAgICAgICAgbnVtYmVyPTUsCiAgICAgICAgc3VtbWFyeUZ1bmN0aW9uID0gdHdvQ2xhc3NTdW1tYXJ5LAogICAgICAgIGNsYXNzUHJvYnM9VFJVRSwKICAgICAgICBhbGxvd1BhcmFsbGVsID0gRkFMU0UKICAgICAgICApCgojIEdlbmVyYXIgZ3JpZAoKZ3JpZF9yZiA8LSBleHBhbmQuZ3JpZChtdHJ5PWMoNSwxMCwxNSwgMjUpLAogICAgICAgICAgICAgICAgICAgICAgIG1pbi5ub2RlLnNpemU9Yyg1LDEwLDE1LDIwKSwKICAgICAgICAgICAgICAgICAgICAgICBzcGxpdHJ1bGU9J2dpbmknCiAgICAgICAgKQoKYGBgCgpgYGB7ciBlY2hvPVRSVUV9CiMgVHVuZWFyCnQwIDwtIHByb2MudGltZSgpCnJmX2ZpdF9vcmlnIDwtICB0cmFpbihpbXBfaW5nbGFiMSB+IC4gLCAKICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sIAogICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJyYW5nZXIiLCAKICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBmaXRDb250cm9scmYsCiAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBncmlkX3JmLAogICAgICAgICAgICAgICAgIG1ldHJpYz0nUk9DJwopCnByb2MudGltZSgpIC0gIHQwCgpzYXZlUkRTKHJmX2ZpdF9vcmlnLCAnLi4vbW9kZWxzL3JmX2ZpdF9vcmlnLlJEUycpCmBgYAoKYGBge3IgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0KcmZfZml0X29yaWcgPC0gcmVhZFJEUygnLi4vbW9kZWxzL3JmX2ZpdF9vcmlnLlJEUycpCmBgYAoKYGBge3J9CnJmX2ZpdF9vcmlnCmBgYAoKCmBgYHtyfQpjb25mdXNpb25NYXRyaXgocHJlZGljdChyZl9maXRfb3JpZywgdGVzdCksIHRlc3QkaW1wX2luZ2xhYjEpCmBgYAoKCi0gwr9RdcOpIHB1ZWRlbiBkZWNpciBkZSBlc3RlIG1vZGVsbz8gCi0gwr9GdW5jaW9uYSBtZWpvciBxdWUgdW4gw6FyYm9sIGluZGl2aWR1YWw/IAotIMK/UXXDqSBzdWNlZGUgY29uIHN1IHBlcmZvcm1hbmNlIGVudHJlIGxvcyBgbWlzc2AKCgojIyBEYXRhc2V0cyBkZXNiYWxhbmNlYWRvcy4uLiDCv3F1w6kgaGFjZXI/CgpFZmVjdGl2YW1lbnRlLCBwYXJ0ZSBkZWwgcHJvYmxlbWEgZGUgZXN0ZSBkYXRhc2V0IGVzIHF1ZSB0ZW5lbW9zIHVuYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSBzdW1hbWVudGUgZGVzYmFsYW5jZWFkYToKCmBgYHtyfQpnZ3Bsb3QodHJhaW4pICsgCiAgICAgICAgZ2VvbV9iYXIoYWVzKHg9aW1wX2luZ2xhYjEpKSArCiAgICAgICAgdGhlbWVfbWluaW1hbCgpCmBgYAoKIyMjIyBNYXRyaXogZGUgY29uZnVzacOzbgoKWWEgZXN0YW1vcyBmYW1pbGlhcml6YWRlcyBjb24gbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24uIELDoXNpY2FtZW50ZSwgZXMgdW5hIGZvcm1hIHLDoXBpZGEgZGUgY29tcGFyYXIgbGFzIHByZWRpY2Npb25lcyBkZWwgbW9kZWxvIGNvbiBsb3MgdmFsb3JlcyByZWFsZXMuIAoKQXPDrSwgbGEgbcOpdHJpY2EgbcOhcyBzaW1wbGUgZXMgbGEgc2lndWllbnRlOgoKJEFjY3VyYWN5ID0gXGZyYWN7VFAgKyBUTn17Tn0gPSBcZnJhY3tUUCArIFROfXtUUCArIFROICsgRlAgKyBGTn0kCgpTaW1wbGVtZW50ZSwgY2FsY3VsYSBsYSBwcm9wb3JjacOzbiBkZSBjYXNvcyBiaWVuIGNsYXNpZmljYWRvcyAoJFRQICsgVE4kKSBzb2JyZSBlbCB0b3RhbC4gUGVybyBlc3RhIG3DqXRyaWNhIHRpZW5lIGFsZ3Vub3MgcHJvYmxlbWFzLiBFbCBtw6FzIGltcG9ydGFudGUgZXMgcXVlIHRpZW5kZSBhIHNlciB1bmEgbWVkaWRhIGVuZ2HDsW9zYSBkZSBsYSBwZXJmb3JtYW5jZSBkZSB1biBtb2RlbG8gZW4gdW4gZGF0YXNldCBkZXNiYWxhbmNlYWRvLgoKVW4gcHJpbWVyIHB1bnRvIGltcG9ydGFudGUgZXMgcG9kZXIgc2VsZWNjaW9uYXIgdW5hIG3DqXRyaWNhIGFkZWN1YWRhIHBhcmEgZXZhbHVhciBudWVzdHJvIG1vZGVsby4gRXhpc3RlbiBkb3MgZ3JhbmRlcyBmYW1pbGlhcyBkZSBtZWRpZGFzOgoKLSBEZXBlbmRpZW50ZSBkZWwgdW1icmFsOiBFc3RvIGluY2x1eWUgbcOpdHJpY2FzIGNvbW8gcHJlY2lzacOzbiwgcmVjYWxsIHkgcHVudGFqZSBGMSwgcXVlIHJlcXVpZXJlbiB1bmEgbWF0cml6IGRlIGNvbmZ1c2nDs24gcGFyYSBzZXIgY2FsY3VsYWRhIHVzYW5kbyB1biBsw61taXRlIGR1cm8gZW4gbGFzIHByb2JhYmlsaWRhZGVzIHByb25vc3RpY2FkYXMuIEVzdGFzIG3DqXRyaWNhcyBzdWVsZW4gc2VyIGJhc3RhbnRlIHBvYnJlcyBlbiBlbCBjYXNvIGRlIGNsYXNlcyBkZXNlcXVpbGlicmFkYXMsIHlhIHF1ZSBlbCBzb2Z0d2FyZSBlc3RhZMOtc3RpY28gdXRpbGl6YSBkZSBtYW5lcmEgaW5hcHJvcGlhZGEgdW4gdW1icmFsIHByZWRldGVybWluYWRvIGRlIDAsNTAsIGxvIHF1ZSBoYWNlIHF1ZSBlbCBtb2RlbG8gcHJlZGlnYSBxdWUgdG9kYXMgbGFzIG9ic2VydmFjaW9uZXMgcGVydGVuZWNlbiBhIGxhIGNsYXNlIG1heW9yaXRhcmlhLgoKJFByZWNpc2lvbiA9IFxmcmFje1RQfXtUUCtGUH0kIFJhdGlvIGVudHJlIHZlcmRhZGVyb3MgcG9zaXRpdm9zIHkgbG9zIHF1ZSBlbCBtb2RlbG8gIGNsYXNpZmljYSBjb21vIHBvc2l0aXZvcy4gwr9RdcOpIHByb3BvcmNpw7NuIGRlIGxvcyBxdWUgY2xhc2lmY2Ftb3MgY29tbyBwb3NpdGl2b3Mgc29uIGVmZWN0aXZhbWVudGUgcG9zaXRpdm9zPwoKJFJlY2FsbCA9ICBcZnJhY3tUUH17VFArRk59JCBUYW1iacOpbiBsbGFtYWRhLCAkU2Vuc2l0aXZpZGFkJCwgUmF0aW8gZW50cmUgbG9zIHBvc2l0aXZvcyBiaWVuIGNsYXNpZmljYWRvcyB5IGVsIHRvdGFsIGRlIGxvcyBxdWUgc29uIHBvc2l0aXZvcyBlbiByZWFsaWRhZC4gwr9RdcOpIHByb3BvcmNpw7NuIGRlIGxvcyBxdWUgZWZlY3RpdmFtZW50ZSBwb3NpdGl2b3MgaGVtb3MgY2xhc2lmaWNhZG8gYmllbj8KCiRGMS1TY29yZSA9IDIgXHRpbWVzIFxmcmFje1ByZWNpc2lvbiBcdGltZXMgUmVjYWxsfXtSZWNhbGwgKyBQcmVjaXNpb259JAoKLSBJbmRlcGVuZGllbnRlIGRlbCB1bWJyYWw6IEVzdG8gaW5jbHV5ZSBtw6l0cmljYXMgY29tbyDDoXJlYSBiYWpvIGxhIGN1cnZhIFJPQyAoQVVDKSwgcXVlIGN1YW50aWZpY2EgbGEgdGFzYSBwb3NpdGl2YSB2ZXJkYWRlcmEgZW4gZnVuY2nDs24gZGUgbGEgdGFzYSBkZSBmYWxzb3MgcG9zaXRpdm9zIHBhcmEgdW5hIHZhcmllZGFkIGRlIHVtYnJhbGVzIGRlIGNsYXNpZmljYWNpw7NuLiBPdHJhIGZvcm1hIGRlIGludGVycHJldGFyIGVzdGEgbcOpdHJpY2EgZXMgbGEgcHJvYmFiaWxpZGFkIGRlIHF1ZSB1bmEgaW5zdGFuY2lhIHBvc2l0aXZhIGFsZWF0b3JpYSB0ZW5nYSB1bmEgcHJvYmFiaWxpZGFkIGVzdGltYWRhIG3DoXMgYWx0YSBxdWUgdW5hIGluc3RhbmNpYSBuZWdhdGl2YSBhbGVhdG9yaWEuCgpFbiBsw61uZWFzIGdlbmVyYWxlcywgZXhpc3RlbiBjdWF0cm8gZm9ybWFzIGdlbmVyYWxlcyBkZSBsaWRpYXIgY29uIGVsIHByb2JsZW1hIGRlIGxhcyBjbGFzZXMgZGVzYmFsYW5jZWFkYXM6CgotIFVzYXIgbW9kZWxvcyBwb25kZXJhZG9zCi0gX0Rvd24tc2FtcGxpbmdfIGVzIGRlY2lyLCBzdWJtdWVzdHJlYXIgbGFzIGluc3RhbmNpYXMgc29icmVycmVwcmVzZW50YWRhcwotIF9VcC1zYW1wbGluZ18sIGVzIGRlY2lyLCBzb2JyZW11ZXN0cmFyIGxhcyBpbnN0YW5jaWFzIHN1YnJyZXByZXNlbnRhZGFzCi0gX1N5bnRoZXRpYyBtaW5vcml0eSBzYW1wbGluZyB0ZWNobmlxdWUgKFNNT1RFKV86IGhhY2UgX2Rvd24tc2FtcGxpbmdfIGRlIGxhIGNsYXNlIG1heW9yaXRhcmlhIHkgInNpbnRldGl6YSIgbnVldmFzIGluc3RhbmNpYXMgbWlub3JpdGFyaWFzIG1lZGlhbnRlIGludGVycG9sYWNpw7NuCgoKIyMjIFV0aWxpemFyIHBlc29zIGVuIGVsIG1vZGVsbwoKUGFyYSBlc3RvIHBvZGVtb3MgdXRpbGl6YXIgZWwgYXJndW1lbnRvIGB3ZWlnaHRzYCBlbiBsYSBmdW5jacOzbiBgdHJhaW5gIChlc3RvIHN1cG9uZSBxdWUgZWwgbW9kZWxvIHNlbGVjY2lvbmFkbyBwdWVkZSBtYW5lamFyIHBlc29zKS4KYGBge3J9Cm1vZGVsX3dlaWdodHMgPC0gaWZlbHNlKHRyYWluJGltcF9pbmdsYWIxID09ICJub25fbWlzcyIsCiAgICAgICAgICAgICAgICAgICAgICAgICgxL3RhYmxlKHRyYWluJGltcF9pbmdsYWIxKVsxXSkgKiAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAgICgxL3RhYmxlKHRyYWluJGltcF9pbmdsYWIxKVsyXSkgKiAwLjUpCgpmaXRDb250cm9scmYkc2VlZHMgPC0gcmZfZml0X29yaWckY29udHJvbCRzZWVkcwpgYGAKClkgZW50cmVuYW1vcyBlbCBtb2RlbG86CmBgYHtyfQp0MCA8LSBwcm9jLnRpbWUoKQpyZl9maXRfd2VpIDwtICB0cmFpbihpbXBfaW5nbGFiMSB+IC4gLCAKICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sIAogICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJyYW5nZXIiLCAKICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBmaXRDb250cm9scmYsCiAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBncmlkX3JmLAogICAgICAgICAgICAgICAgIHdlaWdodHMgPSBtb2RlbF93ZWlnaHRzLAogICAgICAgICAgICAgICAgIG1ldHJpYz0nUk9DJwopCnByb2MudGltZSgpIC0gIHQwCgpzYXZlUkRTKHJmX2ZpdF93ZWksICcuLi9tb2RlbHMvcmZfZml0X3dlaS5SRFMnKQpgYGAKCmBgYHtyIGluY2x1ZGU9RkFMU0V9CnJmX2ZpdF93ZWkgPC0gcmVhZFJEUygnLi4vbW9kZWxzL3JmX2ZpdF93ZWkuUkRTJykKYGBgCiAgICAgICAgCgojIyMgTcOpdG9kb3MgZGUgcmVtdWVzdHJlbwpQcm9iZW1vcywgYWhvcmEsIG3DqXRvZG9zIGRlIHJlbXVlc3RyZW86IF91cCBzYW1wbGluZ18geSBfZG93biBzYW1wbGluZ18uCmBgYHtyfQojIE51ZXZvIGZpdENvbnRyb2wgY29uIHVwc2FtcGxlCmZpdENvbnRyb2xyZl9pbWIgPC0gdHJhaW5Db250cm9sKAogICAgICAgIGluZGV4PWN2X2luZGV4X3JmLAogICAgICAgIG1ldGhvZD0iY3YiLAogICAgICAgIG51bWJlcj01LAogICAgICAgIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSwKICAgICAgICBjbGFzc1Byb2JzPVRSVUUsCiAgICAgICAgc2FtcGxpbmcgPSAndXAnCiAgICAgICAgKQoKYGBgCgpgYGB7ciBldmFsPUZBTFNFLCBpbmNsdWRlPVRSVUV9CgojIFR1bmVhciB1cHNhbXBsZQoKZml0Q29udHJvbHJmX2ltYiRzZWVkcyA8LSByZl9maXRfb3JpZyRjb250cm9sJHNlZWRzCgp0MCA8LSBwcm9jLnRpbWUoKQpyZl9maXRfdXAgPC0gIHRyYWluKGltcF9pbmdsYWIxIH4gLiAsIAogICAgICAgICAgICAgICAgIGRhdGEgPSB0cmFpbiwgCiAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJhbmdlciIsIAogICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGZpdENvbnRyb2xyZl9pbWIsCiAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBncmlkX3JmLAogICAgICAgICAgICAgICAgIG1ldHJpYz0nUk9DJwopCnByb2MudGltZSgpIC0gIHQwCgpzYXZlUkRTKHJmX2ZpdF91cCwgJy4uL21vZGVscy9yZl9maXRfdXAuUkRTJykKYGBgCgpgYGB7ciBpbmNsdWRlPUZBTFNFfQpyZl9maXRfdXAgPC0gcmVhZFJEUygnLi4vbW9kZWxzL3JmX2ZpdF91cC5SRFMnKQpgYGAKCgoKYGBge3IgZXZhbD1GQUxTRSwgaW5jbHVkZT1UUlVFfQojIFR1bmVhciBkb3duc2FtcGxlCgpmaXRDb250cm9scmZfaW1iJHNhbXBsaW5nIDwtICJkb3duIgpmaXRDb250cm9scmZfaW1iJHNlZWRzIDwtIHJmX2ZpdF9vcmlnJGNvbnRyb2wkc2VlZHMKCgp0MCA8LSBwcm9jLnRpbWUoKQpyZl9maXRfZG93biA8LSAgdHJhaW4oaW1wX2luZ2xhYjEgfiAuICwgCiAgICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluLCAKICAgICAgICAgICAgICAgICBtZXRob2QgPSAicmFuZ2VyIiwgCiAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gZml0Q29udHJvbHJmX2ltYiwKICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGdyaWRfcmYsCiAgICAgICAgICAgICAgICAgbWV0cmljPSdST0MnCikKcHJvYy50aW1lKCkgLSAgdDAKCnNhdmVSRFMocmZfZml0X2Rvd24sICcuLi9tb2RlbHMvcmZfZml0X2Rvd24uUkRTJykKCmBgYAoKYGBge3IgaW5jbHVkZT1GQUxTRX0KcmZfZml0X2Rvd24gPC0gcmVhZFJEUygnLi4vbW9kZWxzL3JmX2ZpdF9kb3duLlJEUycpCmBgYAoKCkNvbXBhcmVtb3MgdG9kb3MgbG9zIG1vZGVsb3MgZW50cmVuYWRvcyBoYXN0YSBhaG9yYS4gUGVybyB5YSBxdWUgZXN0YW1vcywgaW5jb3Jwb3JlbW9zIGVsIG1vZGVsbyBkZSDDoXJib2wgc2ltcGxlIHF1ZSBoYWLDrWFtb3MgZW50cmVuYWRvIGxhIHNlbWFuYSBwYXNhZGE6CgoKYGBge3J9CmNhcnRfZmluYWwgPC0gdHJhaW4oaW1wX2luZ2xhYjEgfiAuICwgCiAgICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluLCAKICAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgCiAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBkYXRhLmZyYW1lKG1heGRlcHRoPTYpLAogICAgICAgICAgICAgICAgIGNvbnRyb2wgPSBycGFydC5jb250cm9sKGNwPTAuMDAwMDAxKQopCmBgYAoKCkNvbG9jYW1vcyB0b2RvcyBsb3MgbW9kZWxvcyBlbiB1bmEgbGlzdGE6CgpgYGB7cn0KbW9kZWxfbGlzdCA8LSBsaXN0KGNhcnQgPSBjYXJ0X2ZpbmFsLAogICAgICAgICAgICAgICAgICAgcmZfb3JpZ2luYWwgPSByZl9maXRfb3JpZywKICAgICAgICAgICAgICAgICAgIHJmX3dlaWdodGVkID0gcmZfZml0X3dlaSwKICAgICAgICAgICAgICAgICAgIHJmX2Rvd24gPSByZl9maXRfZG93biwKICAgICAgICAgICAgICAgICAgIHJmX3VwID0gcmZfZml0X3VwKQoKYGBgCgoKRXh0cmFpZ2Ftb3MgYWxndW5hcyBtw6l0cmljYXMgcmVzdW1lbiBkZSBsYXMgbWF0cmljZXMgZGUgY29uZnVzacOzbjoKCmBgYHtyfQpleHRyYWN0X2NvbmZfbWV0cmljcyA8LSBmdW5jdGlvbihtb2RlbCwgZGF0YSwgb2JzKXsKICAgICAgICBwcmVkcyA8LSBwcmVkaWN0KG1vZGVsLCBkYXRhKSAgICAgICAgCiAgICAgICAgYzwtY29uZnVzaW9uTWF0cml4KHByZWRzLCBvYnMpCiAgICAgICAgcmVzdWx0cyA8LSBjKGMkb3ZlcmFsbFsxXSwgYyRieUNsYXNzKQogICAgICAgIHJldHVybihyZXN1bHRzKQp9CgpgYGAKCgpgYGB7ciBldmFsPUZBTFNFLCBpbmNsdWRlPVRSVUV9Cgptb2RlbF9tZXRyaWNzIDwtIG1vZGVsX2xpc3QgJT4lCiAgICAgICAgbWFwKGV4dHJhY3RfY29uZl9tZXRyaWNzLCBkYXRhPXRlc3QsIG9icyA9IHRlc3QkaW1wX2luZ2xhYjEpICU+JQogICAgICAgIGRvLmNhbGwocmJpbmQsLikgJT4lCiAgICAgICAgYXMuZGF0YS5mcmFtZSgpCgpgYGAKClB1ZWRlIHZlcnNlLCBlbnRvbmNlcywgY8OzbW8gbGEgcGVyZm9ybWFjZSBkZSBsb3MgZGlmZXJlbnRlcyBtb2RlbG9zIGNhbWJpYSBlbiBsb3MgZGlmZXJlbnRlcyB0dW5uaW5ncy4uLgoKYGBge3J9Cm1vZGVsX21ldHJpY3MgJT4lCiAgICAgICAgc2VsZWN0KCdBY2N1cmFjeScsICdQcmVjaXNpb24nLCAnUmVjYWxsJykgJT4lCiAgICAgICAgdCgpCmBgYAoKT2JzZXJ2YW5kbyBlc3BlY8OtZmljYW1lbnRlIGxhcyBtw6l0cmljYXMgZGUgJFByZWNpc2lvbiQsICRSZWNhbGwkIHkgJEFjY3VyYWN5JCBwdWVkZSB2ZXJzZSBxdWUgZWwgbW9kZWxvIGRlIHJhbmRvbSBmb3Jlc3Qgb3JpZ2luYWwgcGFyZWNlIG1lam9yYXIgbGFzIGRpZmVyZW50ZXMgbcOpdHJpY2FzLgoKCiMjIEFkYUJvb3N0CgpQb3Igw7psdGltbywgdmFtb3MgYSBlbnRyZW5hciB1biBtb2RlbG8gZGUgQWRhYm9vc3QuCgpgYGB7cn0KZml0Q29udHJvbHJmJHNlZWRzIDwtIHJmX2ZpdF9vcmlnJGNvbnRyb2wkc2VlZHMKI2dyaWRfYWRhIDwtIGV4cGFuZC5ncmlkKG1maW5hbD1jKDEwMCwxNTApLCBtYXhkZXB0aD1jKDEwLDIwKSwgY29lZmxlYXJuPSdCcmVpbWFuJykKZ3JpZF9hZGEgPC0gZXhwYW5kLmdyaWQobkl0ZXI9YygxMDAsMTUwKSwgbWV0aG9kPSdBZGFib29zdC5NMScpCmBgYAoKYGBge3IgaW5jbHVkZT1UUlVFfQp0MCA8LSBwcm9jLnRpbWUoKQphZGFib29zdF9maXRfb3JpZyA8LSAgdHJhaW4oaW1wX2luZ2xhYjEgfiAuICwgCiAgICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluLCAKICAgICAgICAgICAgICAgICBtZXRob2QgPSAiYWRhYm9vc3QiLCAKICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBmaXRDb250cm9scmYsCiAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBncmlkX2FkYSwKICAgICAgICAgICAgICAgICBtZXRyaWM9J1JPQycKKQpwcm9jLnRpbWUoKSAtICB0MAoKCnNhdmVSRFMoYWRhYm9vc3RfZml0X29yaWcsICcuLi9tb2RlbHMvYWRhYm9vc3RfZml0X29yaWcuUkRTJykKCmBgYAoKYGBge3IsIGluY2x1ZGU9RkFMU0V9CmFkYWJvb3N0X2ZpdF9vcmlnIDwtIHJlYWRSRFMoJy4uL21vZGVscy9hZGFib29zdF9maXRfb3JpZy5SRFMnKQpgYGAKCmBgYHtyfQphZGFib29zdF9maXRfb3JpZwpgYGAKCiMjIENvbXBhcmFuZG8gbG9zIG1vZGVsb3MgZ2VuZXJhZG9zCgpWb2x2YW1vcyBhIGFybWFyIGxhIGxpc3RhIGRlIG1vZGVsb3M6CmBgYHtyfQptb2RlbF9saXN0IDwtIGxpc3QoY2FydCA9IGNhcnRfZmluYWwsCiAgICAgICAgICAgICAgICAgICByZl9vcmlnaW5hbCA9IHJmX2ZpdF9vcmlnLAogICAgICAgICAgICAgICAgICAgcmZfd2VpZ2h0ZWQgPSByZl9maXRfd2VpLAogICAgICAgICAgICAgICAgICAgcmZfZG93biA9IHJmX2ZpdF9kb3duLAogICAgICAgICAgICAgICAgICAgcmZfdXAgPSByZl9maXRfdXAsCiAgICAgICAgICAgICAgICAgICBhZGFib29zdCA9IGFkYWJvb3N0X2ZpdF9vcmlnKQpgYGAKClksIGZpbmFsbWVudGUsIGdlbmVyZW1vcyB1bmEgdGFibGEgcXVlIGNvbnRlbmdhIHRvZG86CgpgYGB7cn0KbW9kZWxfbWV0cmljcyA8LSBtb2RlbF9saXN0ICU+JQogICAgICAgIG1hcChleHRyYWN0X2NvbmZfbWV0cmljcywgZGF0YT10ZXN0LCBvYnMgPSB0ZXN0JGltcF9pbmdsYWIxKSAlPiUKICAgICAgICBkby5jYWxsKHJiaW5kLC4pICU+JQogICAgICAgIGFzLmRhdGEuZnJhbWUoKSAlPiUKICAgICAgICB0KCkKCm1vZGVsX21ldHJpY3MKYGBgCgpQdWVkZSB2ZXJzZSBxdWUgY2FydCwgcmZfb3JpZ2luYWwgeSBhZGFib29zdCBwcmVzZW50YW4gdmFsb3JlcyBzaW1pbGFyZXMgZW4gYWNjdXJhY3kuIEEgc3UgdmV6LCBhbCBvYnNlcnZhciBsYSByZWxhY2nDs24gZW50cmUgcHJlY2lzaW9uIHkgcmVjYWxsIHB1ZWRlIHZlcnNlIHF1ZSBlbCBtb2RlbG8gcXVlIG1lam9yIGJhbGFuY2VhIGxvcyBkb3MgZXMgcmZfdXAuIEN1w6FsIGVsZWdpciBkZXBlbmRlcsOhIGRlbCBvYmpldGl2byBkZWwgYW7DoWxpc2lzLgo=