Objetivos
- Introducir algunos conceptos básicos del enfoque del Aprendizaje Automático
- Mostrar el framework
caret
para automatizar algunas tareas del entrenamiento
El problema
Nuestro problema central es, poder realizar un modelo que logre prededir los ingresos de la ocupación principal (p21
) en la EPH del segundo trimestre del 2015.
Puede notarse que se trata de un problema bastante amigable, por así decirlo, al enfoque de Machine Learning:
- tenemos un conjunto de casos en los que desconocemos nuestra variable \(Y\)
- queremos predecirla
Preprocesando los datos
Lo primero que tenemos que hacer es importar las librerías con las que vamos a trabajar:
library(caret)
Loading required package: lattice
Loading required package: ggplot2
library(tidyverse)
[37m-- [1mAttaching packages[22m --------------------------------------- tidyverse 1.2.1 --[39m
[37m[32mv[37m [34mtibble [37m 2.1.3 [32mv[37m [34mpurrr [37m 0.2.5
[32mv[37m [34mtidyr [37m 0.8.2 [32mv[37m [34mdplyr [37m 0.8.3
[32mv[37m [34mreadr [37m 1.3.1 [32mv[37m [34mstringr[37m 1.3.1
[32mv[37m [34mtibble [37m 2.1.3 [32mv[37m [34mforcats[37m 0.3.0[39m
[37m-- [1mConflicts[22m ------------------------------------------ tidyverse_conflicts() --
[31mx[37m [34mdplyr[37m::[32mfilter()[37m masks [34mstats[37m::filter()
[31mx[37m [34mdplyr[37m::[32mlag()[37m masks [34mstats[37m::lag()
[31mx[37m [34mpurrr[37m::[32mlift()[37m masks [34mcaret[37m::lift()[39m
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'))
Existen en nuestro dataset, datos que no contestaron ingresos. Son datos perdidos y tenemos que resolver qué hacer con ellos. En este ejemplo vamos a eliminarlos. Esta opción está lejos de ser la óptima, pero la seleccionamos para simplificar el problema y la exposición. En caso de que les interese el tema de imputación de missing data aquí y aquí pueden encontrar dos aplicaciones de Machine Learning al problema (y sobre el mismo dataset).
df_imp <- data %>%
filter(imp_inglab1==1) %>%
select(-imp_inglab1)
df_train <- data %>%
filter(imp_inglab1==0) %>%
select(-imp_inglab1) %>%
mutate(p21 = case_when(
p21==0 ~ 100,
TRUE ~ p21))
Algunas cosas a notar
Por un lado, vemos que encadenamos unas cuántas operaciones mediante un operador (%>%
) llamado pipe
. El pipe es un símbolo que relaciona dos entidades. Dicho en forma más simple, el pipe de R está en familia con otros operadores más convencionales, como +, - o /. Y al igual que los otros operadores, entrega un resultado en base a los operandos que recibe.
Ahora bien… ¿Para qué sirve? En resumidas cuentas, hace que el código necesario para realizar una serie de operaciones de transformación de datos sea mucho más simple de escribir y de interpretar.
Repasemos la primer secuencia
- filtramos los datos con algún perdido (
%>% filter(imp_inglab==1)
)
- eliminamos la columna identificadora de los casos perdidos (
select(-imp_inglab)
)
Estimando el error de generalización
Recordemos: tenemos muchas formas de estimar el error de generalización (train-test split, cross validation, bootstrap). Usaremos una estrategia de validación cruzada. Vamos a generar los índices mediante caret
.
Vamos a tener que fijar dos estrategias de estimación del error: la primera para estimar los hiperparámetros de los modelos y la segunda para la estimación final del error de generalización. En ambos casos, utilizaremos validación cruzada, pero sobre dos muestras diferentes.
Primero, fijamos la semilla aleatoria (para asegurarnos la posibilidad de replicabilidad)
set.seed(957)
Podemos usar la función createFolds()
para generar los índices. Aquí, pas
cv_index <- createFolds(y = df_train$p21,
k=5,
list=TRUE,
returnTrain=TRUE)
Aquí usamos tres argumentos:
y = df_train$p21
, es el vector de resultados. En nuestro caso, los ingresos de la ocupación principal
k=5
, es la cantidad de grupos para realizar la validación cruzada
returnTrain=TRUE
, le decimos que lo que nos devuelva, sean las posiciones de correspondientes a los datos de entrenamiento en cada posición
Finalmente, especificamos el diseño de remuestreo mediante la función trainControl
:
fitControl <- trainControl(
index=cv_index,
method="cv",
number=5)
Entrenando modelos (train()
)
Tenemos listo nuestro esquema de remuestreo. Podemos pasar a entrenar nuestro primer modelo. Para ello haremos uso extensivo de la función train()
. La misma puede usarse para
- evaluar mediante remuestreo el efecto de cada hiperparámetro en la performance
- elegir el modelo “óptimo” (la mejor combinación de parámetros)
- estimar la performance del modelo
Primero, debemos elegir el modelo para entrenar. Actualmente, caret
dispone de 238 modelos disponibles. Puede consultarse la seccion correspondiente del sitio para mayores detalles. También, llegado el caso, podrían usarse modelos ad-hoc definidos por el usuario.
Comencemos con un modelo simple, pero efectivo: una regresión lineal. Como podrán ver en el sitio, cada modelo puede ser estimado por diferentes implementaciones en diferentes paquetes. Nosotros usaremos la implementación de r-base lm()
por simplicidad.
Entrenemos una regresión lineal con caret: comencemos con un modelo simple, sexo y edad.
lm_p21 <- train(p21 ~ ch04 + ch06, data = df_train,
method = "lm",
trControl = fitControl)
lm_p21
Linear Regression
19406 samples
2 predictor
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15523, 15525, 15526, 15525, 15525
Resampling results:
RMSE Rsquared MAE
5600.527 0.05340693 3836.812
Tuning parameter 'intercept' was held constant at a
value of TRUE
Veamos los coeficientes…
lm_p21$finalModel
Call:
lm(formula = .outcome ~ ., data = dat)
Coefficients:
(Intercept) ch04Mujer ch06
5022.91 -2019.82 70.68
¿Qué se puede ver?
Veamos, ahora, un modelo más complejo:
lm_p21_b <- train(p21 ~ ., data = df_train,
method = "lm",
trControl = fitControl)
lm_p21_b
Linear Regression
19406 samples
25 predictor
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15523, 15525, 15526, 15525, 15525
Resampling results:
RMSE Rsquared MAE
4019.713 0.5127815 2476.719
Tuning parameter 'intercept' was held constant at a
value of TRUE
Los modelos de machine learning tienen ciertos parámetros que deben ser seleccionados antes de estimar el modelo, propiamente dicho: se llaman hiperparámetros. Si bien la regresión lineal no es estrictamente hablando un modelo de machine learning (aunque muches lo consideran como tal) sí tiene algo que se le parece bastante a un hiperparámetro… la existencia de un intercepto. En efecto, nosotros estimamos un modelo de la siguiente forma:
\(y_{i} = \beta_{0} + \sum_{p=1}^P \beta_{p} X_{i}\)
Pero podríamos haber estimado
\(y_{i} = \sum_{p=1}^P \beta_{p} X_{i}\)
Más allá de la discusión sobre si la regresión es ML o no, lo interesante es ver que la decisión sobre el entrenamiento de un modelo lineal con intercepto o no, es una decisión que se toma antes de entrenar el modelo propiamente dicho.
Ahora bien, vamos a buscar otro modelo con mejores hiperparámetros para tunear: un árbol de decisión. Si bien, lo vamos a ver en detalle la próxima clase, vamos a revisar su implementación en caret.
Tuneando hiperparámetros…
Podemos entonces, comparar la performance de un modelo con y sin hiperparámetros. Para ello, primero tenemos que construir la grilla de hiperparámetros.
grid <- expand.grid(maxdepth=c(1, 2, 4, 8, 16))
Y volvemos a entrenar el modelo:
cart_p21 <- train(p21 ~ . ,
data = df_train,
method = "rpart2",
trControl = fitControl,
tuneGrid =grid)
cart_p21
CART
19406 samples
25 predictor
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15523, 15525, 15526, 15525, 15525
Resampling results across tuning parameters:
maxdepth RMSE Rsquared MAE
1 5254.093 0.1672134 3380.275
2 5119.021 0.2100596 3288.701
4 4874.119 0.2837009 3146.744
8 4672.406 0.3415589 2923.876
16 4647.562 0.3485327 2901.520
RMSE was used to select the optimal model using
the smallest value.
The final value used for the model was maxdepth = 16.
En este caso, hemos realizado una búsqueda exhaustiva, es decir, hemos recorrido la totalidad de la grilla de hiperparámetros y hemos seleccionado el mejor modelo. Como puede verse, esto ha llevado un tiempo de cómputo nada despreciable.
Es por ello que existe una segunda opción…
Random search
En este caso, en lugar de realizar una búsqueda exhaustiva, podemos reducir notablemente el tiempo de cómputo buscando en una muestra aleatoria de la grilla de hiperparámetros. Para esto, solamente debemos agregar un parámetro en el objeto fitControl
:
fitControl_rand <- trainControl(
index=cv_index,
method="cv",
number=5,
search = 'random')
Y volvemos a entrenar el modelo:
cart_p21_rand <- train(p21 ~ ., data = df_train,
method = "rpart2",
trControl = fitControl_rand,
tuneLength = 2)
cart_p21_rand
CART
19406 samples
25 predictor
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15523, 15525, 15526, 15525, 15525
Resampling results across tuning parameters:
maxdepth RMSE Rsquared MAE
5 4791.211 0.3078338 3019.845
8 4647.562 0.3485327 2901.520
RMSE was used to select the optimal model using
the smallest value.
The final value used for the model was maxdepth = 8.
Seleccionando el mejor modelo
Una vez finalizado el proceso de tunning de los hiperparámetros, podemos proceder a elegir cuál es el mejor modelo.
cart_p21
CART
19406 samples
25 predictor
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15523, 15525, 15526, 15525, 15525
Resampling results across tuning parameters:
maxdepth RMSE Rsquared MAE
1 5254.093 0.1672134 3380.275
2 5119.021 0.2100596 3288.701
4 4874.119 0.2837009 3146.744
8 4672.406 0.3415589 2923.876
16 4647.562 0.3485327 2901.520
RMSE was used to select the optimal model using
the smallest value.
The final value used for the model was maxdepth = 16.
Podemos persistir el modelo en disco (si quisiéramos):
saveRDS(cart_p21, '../models/p21_cart.rds')
Podemos realizar un plot del efecto de los hiperparámetros:
ggplot(cart_p21)

Existen diferentes métricas de selección, las cuales deben ser definidas en la función train
, usando el argumento selectionFunction
que puede tomar tres valores:
"best"
: se selecciona el mejor modelo con la menor métrica de error (la que usaremos aquí)
"oneSE"
: utiliza la regla de “un desvío estándar” de Breiman et al (1986)
"tolerance
; que busca seleccionar el modelo menos complejo dentro de un margen de tolerancia respecto al mejor modelo
También podrían definirse métodos ad-hoc para esta selección.
cart_p21$bestTune
¿Cuál es el mejor modelo (en términos absolutos)?
Realizando la evaluación final
Una vez que hemos seleccionado el mejor modelo, podemos pasar a la evaluación final y persistimos el modelo para reutilizarlo en otras aplicaciones.
Primero, tenemos que volver a generar un esquema de validación cruzada:
set.seed(7412)
cv_index_final <- createFolds(y = df_train$p21,
k=5,
list=TRUE,
returnTrain=TRUE)
fitControl_final <- trainControl(
indexOut=cv_index_final,
method="cv",
number=5)
Y entrenamos una vez más:
cart_final<-train(p21 ~ ., data = df_train,
method = "rpart2",
trControl = fitControl_final,
tuneGrid = cart_p21$bestTune,
metric='RMSE')
#saveRDS(rf_final, '../models/rf_final.RDS')
cart_final
CART
19406 samples
25 predictor
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 15524, 15526, 15525, 15525, 15524
Resampling results:
RMSE Rsquared MAE
4608.328 0.3594745 2888.506
Tuning parameter 'maxdepth' was held constant at a
value of 16
Vemos entonces que el modelo seleccionado performa con un \(R^2=0.36\) y un \(RMSE=4603\). Solamente nos queda entrenar el modelos sobre la totalidad del dataset de entrenamiento:
cart_final_f<-train(p21~., data=df_train,
method = "rpart2",
tuneGrid = cart_p21$bestTune)
cart_final_f
CART
19406 samples
25 predictor
No pre-processing
Resampling: Bootstrapped (25 reps)
Summary of sample sizes: 19406, 19406, 19406, 19406, 19406, 19406, ...
Resampling results:
RMSE Rsquared MAE
4679.353 0.344868 2915.385
Tuning parameter 'maxdepth' was held constant at a
value of 16
Obteniendo las predicciones finales
El último paso es obtener las predicciones finales (es decir, nuestras imputaciones). De forma interesante, podemos utilizar lso datos perdidos como datos “nuevos” y desconocidos.
Es decir que, finalmente, habremos realizado una imputación de datos perdidos. Para ello, llamamos a la función predict()
que toma como primer argumento al objeto que contiene al modelo final y como segundo argumento el data.frame con los datos a imputar:
y_preds_cart <- predict(cart_final_f, df_imp)
Comparemos, ahora, las distribuciones de datos imputados por el INDEC (mediante el método Hot Deck) y los que hemos imputado con rpart2
. Para ello, organizamos todo en un data frame que, luego, llevamos al formato tidy.
preds <- cbind(y_preds_cart,
df_imp$p21
)
colnames(preds) <- c('CART', 'Hot_Deck')
preds <- preds %>% as.data.frame() %>% gather(model, value)
Finalmente, ploteamos un gráfico de densidad para comparar las distribuciones de los casos imputados con ambos métodos.
ggplot(preds) +
geom_density(aes(x=value, fill=model), alpha=0.5)

ggplot(preds) +
geom_histogram(aes(x=value, fill=model), alpha=0.5,
bins=50)

Práctica independiente: entrenando un árbol para predecir la no respuesta en ingresos
La idea ahora es que ustedes entrenen otro modelo. Vamos a entrenar y evaluar otro modelo en otro problema. Tratemos de predecir la probabilidad de que una persona no conteste ingresos. Usemos para ello un arbol de decisión.
r
r ###
LS0tDQp0aXRsZTogIkludHJvZHVjY2nDs24gKHNpbXBsZSkgYSBjYXJldCINCmF1dGhvcjogIkdlcm3DoW4gUm9zYXRpIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KIyMgT2JqZXRpdm9zDQoNCi0gSW50cm9kdWNpciBhbGd1bm9zIGNvbmNlcHRvcyBiw6FzaWNvcyBkZWwgZW5mb3F1ZSBkZWwgQXByZW5kaXphamUgQXV0b23DoXRpY28NCi0gTW9zdHJhciBlbCBmcmFtZXdvcmsgYGNhcmV0YCBwYXJhIGF1dG9tYXRpemFyIGFsZ3VuYXMgdGFyZWFzIGRlbCBlbnRyZW5hbWllbnRvDQoNCg0KIyMgRWwgcHJvYmxlbWENCg0KTnVlc3RybyBwcm9ibGVtYSBjZW50cmFsIGVzLCBwb2RlciByZWFsaXphciB1biBtb2RlbG8gcXVlIGxvZ3JlIHByZWRlZGlyICBsb3MgaW5ncmVzb3MgZGUgbGEgb2N1cGFjacOzbiBwcmluY2lwYWwgKGBwMjFgKSBlbiBsYSBFUEggZGVsIHNlZ3VuZG8gdHJpbWVzdHJlIGRlbCAyMDE1Lg0KDQpQdWVkZSBub3RhcnNlIHF1ZSBzZSB0cmF0YSBkZSB1biBwcm9ibGVtYSBiYXN0YW50ZSBhbWlnYWJsZSwgcG9yIGFzw60gZGVjaXJsbywgYWwgZW5mb3F1ZSBkZSBNYWNoaW5lIExlYXJuaW5nOg0KDQotIHRlbmVtb3MgdW4gY29uanVudG8gZGUgY2Fzb3MgZW4gbG9zIHF1ZSBkZXNjb25vY2Vtb3MgbnVlc3RyYSB2YXJpYWJsZSAkWSQgDQotIHF1ZXJlbW9zIHByZWRlY2lybGENCg0KDQojIyBQcmVwcm9jZXNhbmRvIGxvcyBkYXRvcw0KDQpMbyBwcmltZXJvIHF1ZSB0ZW5lbW9zIHF1ZSBoYWNlciBlcyBpbXBvcnRhciBsYXMgbGlicmVyw61hcyBjb24gbGFzIHF1ZSB2YW1vcyBhIHRyYWJhamFyOg0KDQoNCmBgYHtyfQ0KbGlicmFyeShjYXJldCkNCmxpYnJhcnkodGlkeXZlcnNlKQ0KYGBgDQoNCg0KTHVlZ28sIGNhcmdhbW9zIGxvcyBkYXRvcyB5IGZvcm1hdGVhbW9zIHVuIHBvY28gYWxndW5hcyBldGlxdWV0YXM6DQoNCg0KYGBge3J9DQpsb2FkKCcuLi9kYXRhL0VQSF8yMDE1X0lJLlJEYXRhJykNCg0KZGF0YSRwcDAzaTwtZmFjdG9yKGRhdGEkcHAwM2ksIGxhYmVscz1jKCcxLVNJJywgJzItTm8nLCAnOS1OUycpKQ0KDQoNCg0KZGF0YSRpbnRlbnNpPC1mYWN0b3IoZGF0YSRpbnRlbnNpLCBsYWJlbHM9YygnMS1TdWJfZGVtJywgJzItU09fbm9fZGVtJywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICczLU9jdXAucGxlbm8nLCAnNC1Tb2JyZW9jJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzUtTm8gdHJhYmFqbycsICc5LU5TJykpDQoNCmRhdGEkcHAwN2E8LWZhY3RvcihkYXRhJHBwMDdhLCBsYWJlbHM9YygnMC1OQycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzEtTWVub3MgZGUgdW4gbWVzJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnMi0xIGEgMyBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzMtMyBhIDYgbWVzZXMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc0LTYgYSAxMiBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzUtMTIgYSA2MCBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzYtTcOhcyBkZSA2MCBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzktTlMnKSkNCmBgYA0KDQoNCkV4aXN0ZW4gZW4gbnVlc3RybyBkYXRhc2V0LCBkYXRvcyBxdWUgbm8gY29udGVzdGFyb24gaW5ncmVzb3MuIFNvbiBkYXRvcyBwZXJkaWRvcyB5IHRlbmVtb3MgcXVlIHJlc29sdmVyIHF1w6kgaGFjZXIgY29uIGVsbG9zLiBFbiBlc3RlIGVqZW1wbG8gdmFtb3MgYSBlbGltaW5hcmxvcy4gRXN0YSBvcGNpw7NuIGVzdMOhIGxlam9zIGRlIHNlciBsYSDDs3B0aW1hLCBwZXJvIGxhIHNlbGVjY2lvbmFtb3MgcGFyYSBzaW1wbGlmaWNhciBlbCBwcm9ibGVtYSB5IGxhIGV4cG9zaWNpw7NuLiBFbiBjYXNvIGRlIHF1ZSBsZXMgaW50ZXJlc2UgZWwgdGVtYSBkZSBpbXB1dGFjacOzbiBkZSBtaXNzaW5nIGRhdGEgW2FxdcOtXShodHRwczovL3d3dy5zYWJlcmVzLmZjZWNvbi51bnIuZWR1LmFyL2luZGV4LnBocC9yZXZpc3RhL2FydGljbGUvdmlldy8xMzIpIHkgW2FxdcOtXShodHRwczovL2FzZXQub3JnLmFyLzIwMTkvcG9uZW5jaWFzLzIwX1Jvc2F0aS5wZGYpIHB1ZWRlbiBlbmNvbnRyYXIgZG9zIGFwbGljYWNpb25lcyBkZSBNYWNoaW5lIExlYXJuaW5nIGFsIHByb2JsZW1hICh5IHNvYnJlIGVsIG1pc21vIGRhdGFzZXQpLg0KDQoNCmBgYHtyfQ0KZGZfaW1wIDwtIGRhdGEgJT4lDQogICAgICAgIGZpbHRlcihpbXBfaW5nbGFiMT09MSkgJT4lDQogICAgICAgIHNlbGVjdCgtaW1wX2luZ2xhYjEpDQoNCmRmX3RyYWluIDwtIGRhdGEgJT4lDQogICAgICAgIGZpbHRlcihpbXBfaW5nbGFiMT09MCkgJT4lDQogICAgICAgIHNlbGVjdCgtaW1wX2luZ2xhYjEpICU+JQ0KICAgICAgICBtdXRhdGUocDIxID0gY2FzZV93aGVuKA0KICAgICAgICAgICAgICAgICAgICAgICAgcDIxPT0wIH4gMTAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgVFJVRSB+IHAyMSkpDQoNCmBgYA0KDQoNCiMjIyMgQWxndW5hcyBjb3NhcyBhIG5vdGFyDQoNClBvciB1biBsYWRvLCB2ZW1vcyBxdWUgZW5jYWRlbmFtb3MgdW5hcyBjdcOhbnRhcyBvcGVyYWNpb25lcyBtZWRpYW50ZSB1biBvcGVyYWRvciAoYCU+JWApIGxsYW1hZG8gYHBpcGVgLiBFbCBwaXBlIGVzIHVuIHPDrW1ib2xvIHF1ZSByZWxhY2lvbmEgZG9zIGVudGlkYWRlcy4gRGljaG8gZW4gZm9ybWEgbcOhcyBzaW1wbGUsIGVsIHBpcGUgZGUgUiBlc3TDoSBlbiBmYW1pbGlhIGNvbiBvdHJvcyBvcGVyYWRvcmVzIG3DoXMgY29udmVuY2lvbmFsZXMsIGNvbW8gKywgLSBvIC8uIFkgYWwgaWd1YWwgcXVlIGxvcyBvdHJvcyBvcGVyYWRvcmVzLCBlbnRyZWdhIHVuIHJlc3VsdGFkbyBlbiBiYXNlIGEgbG9zIG9wZXJhbmRvcyBxdWUgcmVjaWJlLiANCg0KQWhvcmEgYmllbuKApiDCv1BhcmEgcXXDqSBzaXJ2ZT8gRW4gcmVzdW1pZGFzIGN1ZW50YXMsIGhhY2UgcXVlIGVsIGPDs2RpZ28gbmVjZXNhcmlvIHBhcmEgcmVhbGl6YXIgdW5hIHNlcmllIGRlIG9wZXJhY2lvbmVzIGRlIHRyYW5zZm9ybWFjacOzbiBkZSBkYXRvcyBzZWEgbXVjaG8gbcOhcyBzaW1wbGUgZGUgZXNjcmliaXIgeSBkZSBpbnRlcnByZXRhci4NCg0KUmVwYXNlbW9zIGxhIHByaW1lciBzZWN1ZW5jaWENCg0KLSBmaWx0cmFtb3MgbG9zIGRhdG9zIGNvbiBhbGfDum4gcGVyZGlkbyAoYCU+JSBmaWx0ZXIoaW1wX2luZ2xhYj09MSlgKQ0KLSBlbGltaW5hbW9zIGxhIGNvbHVtbmEgaWRlbnRpZmljYWRvcmEgZGUgbG9zIGNhc29zIHBlcmRpZG9zIChgc2VsZWN0KC1pbXBfaW5nbGFiKWApDQoNCg0KIyMjIEVzdGltYW5kbyBlbCBlcnJvciBkZSBnZW5lcmFsaXphY2nDs24NCg0KUmVjb3JkZW1vczogdGVuZW1vcyBtdWNoYXMgZm9ybWFzIGRlIGVzdGltYXIgZWwgZXJyb3IgZGUgZ2VuZXJhbGl6YWNpw7NuICh0cmFpbi10ZXN0IHNwbGl0LCBjcm9zcyB2YWxpZGF0aW9uLCBib290c3RyYXApLiBVc2FyZW1vcyB1bmEgZXN0cmF0ZWdpYSBkZSB2YWxpZGFjacOzbiBjcnV6YWRhLiBWYW1vcyBhIGdlbmVyYXIgbG9zIMOtbmRpY2VzIG1lZGlhbnRlIGBjYXJldGAuIA0KDQpWYW1vcyBhIHRlbmVyIHF1ZSBmaWphciBkb3MgZXN0cmF0ZWdpYXMgZGUgZXN0aW1hY2nDs24gZGVsIGVycm9yOiBsYSBwcmltZXJhIHBhcmEgZXN0aW1hciBsb3MgaGlwZXJwYXLDoW1ldHJvcyBkZSBsb3MgbW9kZWxvcyB5IGxhIHNlZ3VuZGEgcGFyYSBsYSBlc3RpbWFjacOzbiBmaW5hbCBkZWwgZXJyb3IgZGUgZ2VuZXJhbGl6YWNpw7NuLiBFbiBhbWJvcyBjYXNvcywgdXRpbGl6YXJlbW9zIHZhbGlkYWNpw7NuIGNydXphZGEsIHBlcm8gc29icmUgZG9zIG11ZXN0cmFzIGRpZmVyZW50ZXMuDQoNClByaW1lcm8sIGZpamFtb3MgbGEgc2VtaWxsYSBhbGVhdG9yaWEgKHBhcmEgYXNlZ3VyYXJub3MgbGEgcG9zaWJpbGlkYWQgZGUgcmVwbGljYWJpbGlkYWQpDQoNCg0KYGBge3J9DQpzZXQuc2VlZCg5NTcpDQpgYGANCg0KDQpQb2RlbW9zIHVzYXIgbGEgZnVuY2nDs24gYGNyZWF0ZUZvbGRzKClgIHBhcmEgZ2VuZXJhciBsb3Mgw61uZGljZXMuIEFxdcOtLCBwYXMNCg0KDQpgYGB7cn0NCmN2X2luZGV4IDwtIGNyZWF0ZUZvbGRzKHkgPSBkZl90cmFpbiRwMjEsDQogICAgICAgICAgICAgICAgICAgICAgICBrPTUsDQogICAgICAgICAgICAgICAgICAgICAgICBsaXN0PVRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICByZXR1cm5UcmFpbj1UUlVFKQ0KYGBgDQoNCg0KQXF1w60gdXNhbW9zIHRyZXMgYXJndW1lbnRvczoNCg0KLSBgeSA9IGRmX3RyYWluJHAyMWAsIGVzIGVsIHZlY3RvciBkZSByZXN1bHRhZG9zLiBFbiBudWVzdHJvIGNhc28sIGxvcyBpbmdyZXNvcyBkZSBsYSBvY3VwYWNpw7NuIHByaW5jaXBhbA0KLSBgaz01YCwgZXMgbGEgY2FudGlkYWQgZGUgZ3J1cG9zIHBhcmEgcmVhbGl6YXIgbGEgdmFsaWRhY2nDs24gY3J1emFkYQ0KLSBgcmV0dXJuVHJhaW49VFJVRWAsIGxlIGRlY2ltb3MgcXVlIGxvIHF1ZSBub3MgZGV2dWVsdmEsIHNlYW4gbGFzIHBvc2ljaW9uZXMgZGUgY29ycmVzcG9uZGllbnRlcyBhIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIGVuIGNhZGEgcG9zaWNpw7NuDQoNCkZpbmFsbWVudGUsIGVzcGVjaWZpY2Ftb3MgZWwgZGlzZcOxbyBkZSByZW11ZXN0cmVvIG1lZGlhbnRlIGxhIGZ1bmNpw7NuIGB0cmFpbkNvbnRyb2xgOg0KDQpgYGB7cn0NCmZpdENvbnRyb2wgPC0gdHJhaW5Db250cm9sKA0KICAgICAgICBpbmRleD1jdl9pbmRleCwNCiAgICAgICAgbWV0aG9kPSJjdiIsDQogICAgICAgIG51bWJlcj01KQ0KYGBgDQoNCg0KIyMgRW50cmVuYW5kbyBtb2RlbG9zIChgdHJhaW4oKWApDQoNClRlbmVtb3MgbGlzdG8gbnVlc3RybyBlc3F1ZW1hIGRlIHJlbXVlc3RyZW8uIFBvZGVtb3MgcGFzYXIgYSBlbnRyZW5hciBudWVzdHJvIHByaW1lciBtb2RlbG8uIFBhcmEgZWxsbyBoYXJlbW9zIHVzbyBleHRlbnNpdm8gZGUgbGEgZnVuY2nDs24gYHRyYWluKClgLiBMYSBtaXNtYSBwdWVkZSB1c2Fyc2UgcGFyYSANCg0KLSBldmFsdWFyIG1lZGlhbnRlIHJlbXVlc3RyZW8gZWwgZWZlY3RvIGRlIGNhZGEgaGlwZXJwYXLDoW1ldHJvIGVuIGxhIHBlcmZvcm1hbmNlDQotIGVsZWdpciBlbCBtb2RlbG8gIsOzcHRpbW8iIChsYSBtZWpvciBjb21iaW5hY2nDs24gZGUgcGFyw6FtZXRyb3MpIA0KLSBlc3RpbWFyIGxhIHBlcmZvcm1hbmNlIGRlbCBtb2RlbG8NCg0KUHJpbWVybywgZGViZW1vcyBlbGVnaXIgZWwgbW9kZWxvIHBhcmEgZW50cmVuYXIuIEFjdHVhbG1lbnRlLCBgY2FyZXRgIGRpc3BvbmUgZGUgMjM4IG1vZGVsb3MgZGlzcG9uaWJsZXMuIFB1ZWRlIGNvbnN1bHRhcnNlIFtsYSBzZWNjaW9uIGNvcnJlc3BvbmRpZW50ZSBdKGh0dHA6Ly90b3BlcG8uZ2l0aHViLmlvL2NhcmV0L2F2YWlsYWJsZS1tb2RlbHMuaHRtbCkgZGVsIHNpdGlvIHBhcmEgbWF5b3JlcyBkZXRhbGxlcy4gVGFtYmnDqW4sIGxsZWdhZG8gZWwgY2FzbywgcG9kcsOtYW4gdXNhcnNlIG1vZGVsb3MgYWQtaG9jIGRlZmluaWRvcyBwb3IgZWwgdXN1YXJpby4NCg0KQ29tZW5jZW1vcyBjb24gdW4gbW9kZWxvIHNpbXBsZSwgcGVybyBlZmVjdGl2bzogdW5hIHJlZ3Jlc2nDs24gbGluZWFsLiBDb21vIHBvZHLDoW4gdmVyIGVuIGVsIHNpdGlvLCBjYWRhIG1vZGVsbyBwdWVkZSBzZXIgZXN0aW1hZG8gcG9yIGRpZmVyZW50ZXMgaW1wbGVtZW50YWNpb25lcyBlbiBkaWZlcmVudGVzIHBhcXVldGVzLiBOb3NvdHJvcyB1c2FyZW1vcyBsYSBpbXBsZW1lbnRhY2nDs24gZGUgci1iYXNlIGBsbSgpYCBwb3Igc2ltcGxpY2lkYWQuDQoNCkVudHJlbmVtb3MgdW5hIHJlZ3Jlc2nDs24gbGluZWFsIGNvbiBjYXJldDogY29tZW5jZW1vcyBjb24gdW4gbW9kZWxvIHNpbXBsZSwgc2V4byB5IGVkYWQuDQoNCmBgYHtyfSAgICAgICAgDQpsbV9wMjEgPC0gdHJhaW4ocDIxIH4gY2gwNCArIGNoMDYsIGRhdGEgPSBkZl90cmFpbiwgDQogICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJsbSIsIA0KICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBmaXRDb250cm9sKQ0KDQpsbV9wMjENCmBgYA0KDQpWZWFtb3MgbG9zIGNvZWZpY2llbnRlcy4uLg0KYGBge3J9DQpsbV9wMjEkZmluYWxNb2RlbA0KYGBgDQoNCsK/UXXDqSBzZSBwdWVkZSB2ZXI/DQoNCg0KVmVhbW9zLCBhaG9yYSwgdW4gbW9kZWxvIG3DoXMgY29tcGxlam86DQoNCmBgYHtyIHdhcm5pbmc9RkFMU0V9DQpsbV9wMjFfYiA8LSB0cmFpbihwMjEgfiAuLCBkYXRhID0gZGZfdHJhaW4sIA0KICAgICAgICAgICAgICAgICBtZXRob2QgPSAibG0iLCANCiAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gZml0Q29udHJvbCkNCmBgYA0KDQpgYGB7cn0NCmxtX3AyMV9iDQpgYGANCg0KDQpMb3MgbW9kZWxvcyBkZSBtYWNoaW5lIGxlYXJuaW5nIHRpZW5lbiBjaWVydG9zIHBhcsOhbWV0cm9zIHF1ZSBkZWJlbiBzZXIgc2VsZWNjaW9uYWRvcyBhbnRlcyBkZSBlc3RpbWFyIGVsIG1vZGVsbywgcHJvcGlhbWVudGUgZGljaG86IHNlIGxsYW1hbiBbX19oaXBlcnBhcsOhbWV0cm9zX19dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0h5cGVycGFyYW1ldGVyXyhtYWNoaW5lX2xlYXJuaW5nKSkuIFNpIGJpZW4gbGEgcmVncmVzacOzbiBsaW5lYWwgbm8gZXMgZXN0cmljdGFtZW50ZSBoYWJsYW5kbyB1biBtb2RlbG8gZGUgbWFjaGluZSBsZWFybmluZyAoYXVucXVlIG11Y2hlcyBsbyBjb25zaWRlcmFuIGNvbW8gdGFsKSBzw60gdGllbmUgYWxnbyBxdWUgc2UgbGUgcGFyZWNlIGJhc3RhbnRlIGEgdW4gaGlwZXJwYXLDoW1ldHJvLi4uIGxhIGV4aXN0ZW5jaWEgZGUgdW4gaW50ZXJjZXB0by4gRW4gZWZlY3RvLCBub3NvdHJvcyBlc3RpbWFtb3MgdW4gbW9kZWxvIGRlIGxhIHNpZ3VpZW50ZSBmb3JtYToNCg0KDQokeV97aX0gPSBcYmV0YV97MH0gKyBcc3VtX3twPTF9XlAgXGJldGFfe3B9IFhfe2l9JA0KDQpQZXJvIHBvZHLDrWFtb3MgaGFiZXIgZXN0aW1hZG8NCg0KJHlfe2l9ID0gXHN1bV97cD0xfV5QIFxiZXRhX3twfSBYX3tpfSQNCg0KTcOhcyBhbGzDoSBkZSBsYSBkaXNjdXNpw7NuIHNvYnJlIHNpIGxhIHJlZ3Jlc2nDs24gZXMgTUwgbyBubywgbG8gaW50ZXJlc2FudGUgZXMgdmVyIHF1ZSBsYSBkZWNpc2nDs24gc29icmUgZWwgZW50cmVuYW1pZW50byBkZSB1biBtb2RlbG8gbGluZWFsIGNvbiBpbnRlcmNlcHRvIG8gbm8sIGVzIHVuYSBkZWNpc2nDs24gcXVlIHNlIHRvbWEgYW50ZXMgZGUgZW50cmVuYXIgZWwgbW9kZWxvIHByb3BpYW1lbnRlIGRpY2hvLg0KDQoNCkFob3JhIGJpZW4sIHZhbW9zIGEgYnVzY2FyIG90cm8gbW9kZWxvIGNvbiBtZWpvcmVzIGhpcGVycGFyw6FtZXRyb3MgcGFyYSB0dW5lYXI6IHVuIMOhcmJvbCBkZSBkZWNpc2nDs24uIFNpIGJpZW4sIGxvIHZhbW9zIGEgdmVyIGVuIGRldGFsbGUgbGEgcHLDs3hpbWEgY2xhc2UsIHZhbW9zIGEgcmV2aXNhciBzdSBpbXBsZW1lbnRhY2nDs24gZW4gY2FyZXQuDQoNCg0KIyMgVHVuZWFuZG8gaGlwZXJwYXLDoW1ldHJvcy4uLg0KDQpQb2RlbW9zIGVudG9uY2VzLCBjb21wYXJhciBsYSBwZXJmb3JtYW5jZSBkZSB1biBtb2RlbG8gY29uIHkgc2luIGhpcGVycGFyw6FtZXRyb3MuIFBhcmEgZWxsbywgcHJpbWVybyB0ZW5lbW9zIHF1ZSBjb25zdHJ1aXIgbGEgZ3JpbGxhIGRlIGhpcGVycGFyw6FtZXRyb3MuDQoNCmBgYHtyfQ0KZ3JpZCA8LSBleHBhbmQuZ3JpZChtYXhkZXB0aD1jKDEsIDIsIDQsIDgsIDE2KSkNCmBgYA0KDQpZIHZvbHZlbW9zIGEgZW50cmVuYXIgZWwgbW9kZWxvOg0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KY2FydF9wMjEgPC0gdHJhaW4ocDIxIH4gLiAsIA0KICAgICAgICAgICAgICAgICBkYXRhID0gZGZfdHJhaW4sIA0KICAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgDQogICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGZpdENvbnRyb2wsDQogICAgICAgICAgICAgICAgIHR1bmVHcmlkID1ncmlkKQ0KDQpjYXJ0X3AyMQ0KYGBgDQoNCkVuIGVzdGUgY2FzbywgaGVtb3MgcmVhbGl6YWRvIHVuYSBiw7pzcXVlZGEgZXhoYXVzdGl2YSwgZXMgZGVjaXIsIGhlbW9zIHJlY29ycmlkbyBsYSB0b3RhbGlkYWQgZGUgbGEgZ3JpbGxhIGRlIGhpcGVycGFyw6FtZXRyb3MgeSBoZW1vcyBzZWxlY2Npb25hZG8gZWwgbWVqb3IgbW9kZWxvLiBDb21vIHB1ZWRlIHZlcnNlLCBlc3RvIGhhIGxsZXZhZG8gdW4gdGllbXBvIGRlIGPDs21wdXRvIG5hZGEgZGVzcHJlY2lhYmxlLiANCg0KRXMgcG9yIGVsbG8gcXVlIGV4aXN0ZSB1bmEgc2VndW5kYSBvcGNpw7NuLi4uDQoNCg0KIyMjIFJhbmRvbSBzZWFyY2gNCg0KRW4gZXN0ZSBjYXNvLCBlbiBsdWdhciBkZSByZWFsaXphciB1bmEgYsO6c3F1ZWRhIGV4aGF1c3RpdmEsIHBvZGVtb3MgcmVkdWNpciBub3RhYmxlbWVudGUgZWwgdGllbXBvIGRlIGPDs21wdXRvIGJ1c2NhbmRvIGVuIHVuYSBtdWVzdHJhIGFsZWF0b3JpYSBkZSBsYSBncmlsbGEgZGUgaGlwZXJwYXLDoW1ldHJvcy4gUGFyYSBlc3RvLCBzb2xhbWVudGUgZGViZW1vcyBhZ3JlZ2FyIHVuIHBhcsOhbWV0cm8gZW4gZWwgb2JqZXRvIGBmaXRDb250cm9sYDoNCg0KDQpgYGB7ciB3YXJuaW5nPVRSVUV9DQpmaXRDb250cm9sX3JhbmQgPC0gdHJhaW5Db250cm9sKA0KICAgICAgICBpbmRleD1jdl9pbmRleCwgDQogICAgICAgIG1ldGhvZD0iY3YiLA0KICAgICAgICBudW1iZXI9NSwNCiAgICAgICAgc2VhcmNoID0gJ3JhbmRvbScpDQpgYGANCg0KDQpZIHZvbHZlbW9zIGEgZW50cmVuYXIgZWwgbW9kZWxvOg0KDQoNCmBgYHtyfQ0KY2FydF9wMjFfcmFuZCA8LSB0cmFpbihwMjEgfiAuLCBkYXRhID0gZGZfdHJhaW4sIA0KICAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgDQogICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGZpdENvbnRyb2xfcmFuZCwNCiAgICAgICAgICAgICAgICAgdHVuZUxlbmd0aCA9IDIpDQoNCmNhcnRfcDIxX3JhbmQNCmBgYA0KDQoNCiMjIFNlbGVjY2lvbmFuZG8gZWwgbWVqb3IgbW9kZWxvDQoNClVuYSB2ZXogZmluYWxpemFkbyBlbCBwcm9jZXNvIGRlIHR1bm5pbmcgZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MsIHBvZGVtb3MgcHJvY2VkZXIgYSBlbGVnaXIgY3XDoWwgZXMgZWwgbWVqb3IgbW9kZWxvLiANCg0KYGBge3J9DQpjYXJ0X3AyMQ0KYGBgDQoNCg0KUG9kZW1vcyBwZXJzaXN0aXIgIGVsIG1vZGVsbyBlbiBkaXNjbyAoc2kgcXVpc2nDqXJhbW9zKToNCg0KYGBge3J9DQpzYXZlUkRTKGNhcnRfcDIxLCAnLi4vbW9kZWxzL3AyMV9jYXJ0LnJkcycpDQpgYGANCg0KDQpQb2RlbW9zIHJlYWxpemFyIHVuIHBsb3QgZGVsIGVmZWN0byBkZSBsb3MgaGlwZXJwYXLDoW1ldHJvczoNCg0KYGBge3J9DQpnZ3Bsb3QoY2FydF9wMjEpDQpgYGANCg0KDQpFeGlzdGVuIGRpZmVyZW50ZXMgbcOpdHJpY2FzIGRlIHNlbGVjY2nDs24sIGxhcyBjdWFsZXMgZGViZW4gc2VyIGRlZmluaWRhcyBlbiBsYSBmdW5jacOzbiBgdHJhaW5gLCB1c2FuZG8gZWwgYXJndW1lbnRvIGBzZWxlY3Rpb25GdW5jdGlvbmAgcXVlIHB1ZWRlIHRvbWFyIHRyZXMgdmFsb3JlczoNCg0KLSBgImJlc3QiYDogc2Ugc2VsZWNjaW9uYSBlbCBtZWpvciBtb2RlbG8gY29uIGxhIG1lbm9yIG3DqXRyaWNhIGRlIGVycm9yIChsYSBxdWUgdXNhcmVtb3MgYXF1w60pDQotIGAib25lU0UiYDogdXRpbGl6YSBsYSByZWdsYSBkZSAidW4gZGVzdsOtbyBlc3TDoW5kYXIiIGRlIFtCcmVpbWFuIGV0IGFsICgxOTg2KV0oaHR0cHM6Ly9ib29rcy5nb29nbGUuY29tLmFyL2Jvb2tzL2Fib3V0L0NsYXNzaWZpY2F0aW9uX2FuZF9SZWdyZXNzaW9uX1RyZWVzLmh0bWw/aWQ9SndReC1XT21TeVFDJnJlZGlyX2VzYz15JmhsPWVzKQ0KLSBgInRvbGVyYW5jZWA7IHF1ZSBidXNjYSBzZWxlY2Npb25hciBlbCBtb2RlbG8gbWVub3MgY29tcGxlam8gZGVudHJvIGRlIHVuIG1hcmdlbiBkZSB0b2xlcmFuY2lhIHJlc3BlY3RvIGFsIG1lam9yIG1vZGVsbw0KDQpUYW1iacOpbiBwb2Ryw61hbiBkZWZpbmlyc2UgbcOpdG9kb3MgYWQtaG9jIHBhcmEgZXN0YSBzZWxlY2Npw7NuLg0KDQoNCmBgYHtyfQ0KY2FydF9wMjEkYmVzdFR1bmUNCmBgYA0KDQoNCsK/Q3XDoWwgZXMgZWwgbWVqb3IgbW9kZWxvIChlbiB0w6lybWlub3MgYWJzb2x1dG9zKT8NCg0KDQojIyBSZWFsaXphbmRvIGxhIGV2YWx1YWNpw7NuIGZpbmFsDQoNClVuYSB2ZXogcXVlIGhlbW9zIHNlbGVjY2lvbmFkbyBlbCBtZWpvciBtb2RlbG8sIHBvZGVtb3MgcGFzYXIgYSBsYSBldmFsdWFjacOzbiBmaW5hbCB5IHBlcnNpc3RpbW9zIGVsIG1vZGVsbyBwYXJhIHJldXRpbGl6YXJsbyBlbiBvdHJhcyBhcGxpY2FjaW9uZXMuDQoNClByaW1lcm8sIHRlbmVtb3MgcXVlIHZvbHZlciBhIGdlbmVyYXIgdW4gZXNxdWVtYSBkZSB2YWxpZGFjacOzbiBjcnV6YWRhOg0KDQpgYGB7cn0NCnNldC5zZWVkKDc0MTIpDQpjdl9pbmRleF9maW5hbCA8LSBjcmVhdGVGb2xkcyh5ID0gZGZfdHJhaW4kcDIxLA0KICAgICAgICAgICAgICAgICAgICAgICAgaz01LA0KICAgICAgICAgICAgICAgICAgICAgICAgbGlzdD1UUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuVHJhaW49VFJVRSkNCg0KZml0Q29udHJvbF9maW5hbCA8LSB0cmFpbkNvbnRyb2woDQogICAgICAgIGluZGV4T3V0PWN2X2luZGV4X2ZpbmFsLCANCiAgICAgICAgbWV0aG9kPSJjdiIsDQogICAgICAgIG51bWJlcj01KQ0KYGBgDQoNCg0KWSBlbnRyZW5hbW9zIHVuYSB2ZXogbcOhczoNCg0KDQpgYGB7cn0NCmNhcnRfZmluYWw8LXRyYWluKHAyMSB+IC4sIGRhdGEgPSBkZl90cmFpbiwNCiAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgDQogICAgICAgICAgICAgICAgdHJDb250cm9sID0gZml0Q29udHJvbF9maW5hbCwgDQogICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBjYXJ0X3AyMSRiZXN0VHVuZSwNCiAgICAgICAgICAgICAgICBtZXRyaWM9J1JNU0UnKQ0KDQojc2F2ZVJEUyhyZl9maW5hbCwgJy4uL21vZGVscy9yZl9maW5hbC5SRFMnKQ0KDQpjYXJ0X2ZpbmFsDQpgYGANCg0KVmVtb3MgZW50b25jZXMgcXVlIGVsIG1vZGVsbyBzZWxlY2Npb25hZG8gcGVyZm9ybWEgY29uIHVuICRSXjI9MC4zNiQgeSB1biAkUk1TRT00NjAzJC4gU29sYW1lbnRlIG5vcyBxdWVkYSBlbnRyZW5hciBlbCBtb2RlbG9zIHNvYnJlIGxhIHRvdGFsaWRhZCBkZWwgZGF0YXNldCBkZSBlbnRyZW5hbWllbnRvOg0KDQoNCmBgYHtyfQ0KY2FydF9maW5hbF9mPC10cmFpbihwMjF+LiwgZGF0YT1kZl90cmFpbiwNCiAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJycGFydDIiLA0KICAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBjYXJ0X3AyMSRiZXN0VHVuZSkNCg0KY2FydF9maW5hbF9mDQpgYGANCg0KDQojIyBPYnRlbmllbmRvIGxhcyBwcmVkaWNjaW9uZXMgZmluYWxlcw0KDQpFbCDDumx0aW1vIHBhc28gZXMgb2J0ZW5lciBsYXMgcHJlZGljY2lvbmVzIGZpbmFsZXMgKGVzIGRlY2lyLCBudWVzdHJhcyBpbXB1dGFjaW9uZXMpLiBEZSBmb3JtYSBpbnRlcmVzYW50ZSwgcG9kZW1vcyB1dGlsaXphciBsc28gZGF0b3MgcGVyZGlkb3MgY29tbyBkYXRvcyAibnVldm9zIiB5IGRlc2Nvbm9jaWRvcy4NCg0KRXMgZGVjaXIgcXVlLCBmaW5hbG1lbnRlLCBoYWJyZW1vcyByZWFsaXphZG8gdW5hIGltcHV0YWNpw7NuIGRlIGRhdG9zIHBlcmRpZG9zLiBQYXJhIGVsbG8sIGxsYW1hbW9zIGEgbGEgZnVuY2nDs24gYHByZWRpY3QoKWAgcXVlIHRvbWEgY29tbyBwcmltZXIgYXJndW1lbnRvIGFsIG9iamV0byBxdWUgY29udGllbmUgYWwgbW9kZWxvIGZpbmFsIHkgY29tbyBzZWd1bmRvIGFyZ3VtZW50byBlbCBkYXRhLmZyYW1lIGNvbiBsb3MgZGF0b3MgYSBpbXB1dGFyOg0KDQoNCmBgYHtyfQ0KeV9wcmVkc19jYXJ0IDwtIHByZWRpY3QoY2FydF9maW5hbF9mLCBkZl9pbXApDQpgYGANCg0KQ29tcGFyZW1vcywgYWhvcmEsIGxhcyBkaXN0cmlidWNpb25lcyBkZSBkYXRvcyBpbXB1dGFkb3MgcG9yIGVsIElOREVDIChtZWRpYW50ZSBlbCBtw6l0b2RvIEhvdCBEZWNrKSB5IGxvcyBxdWUgaGVtb3MgaW1wdXRhZG8gY29uIGBycGFydDJgLiBQYXJhIGVsbG8sIG9yZ2FuaXphbW9zIHRvZG8gZW4gdW4gZGF0YSBmcmFtZSBxdWUsIGx1ZWdvLCBsbGV2YW1vcyBhbCBmb3JtYXRvIHRpZHkuDQoNCg0KYGBge3J9DQpwcmVkcyA8LSBjYmluZCh5X3ByZWRzX2NhcnQsDQogICAgICAgICAgICAgICBkZl9pbXAkcDIxDQopDQoNCmNvbG5hbWVzKHByZWRzKSA8LSBjKCdDQVJUJywgJ0hvdF9EZWNrJykNCg0KcHJlZHMgPC0gcHJlZHMgJT4lIGFzLmRhdGEuZnJhbWUoKSAlPiUgZ2F0aGVyKG1vZGVsLCB2YWx1ZSkNCg0KYGBgDQoNCg0KRmluYWxtZW50ZSwgcGxvdGVhbW9zIHVuIGdyw6FmaWNvIGRlIGRlbnNpZGFkIHBhcmEgY29tcGFyYXIgbGFzIGRpc3RyaWJ1Y2lvbmVzIGRlIGxvcyBjYXNvcyBpbXB1dGFkb3MgY29uIGFtYm9zIG3DqXRvZG9zLg0KDQoNCmBgYHtyfQ0KZ2dwbG90KHByZWRzKSArDQogICAgICAgIGdlb21fZGVuc2l0eShhZXMoeD12YWx1ZSwgZmlsbD1tb2RlbCksIGFscGhhPTAuNSkNCmBgYA0KDQpgYGB7cn0NCmdncGxvdChwcmVkcykgKw0KICAgICAgICBnZW9tX2hpc3RvZ3JhbShhZXMoeD12YWx1ZSwgZmlsbD1tb2RlbCksIGFscGhhPTAuNSwNCiAgICAgICAgICAgICAgICAgICAgICAgYmlucz01MCkNCmBgYA0KDQoNCiMjIyBQcsOhY3RpY2EgaW5kZXBlbmRpZW50ZTogZW50cmVuYW5kbyB1biDDoXJib2wgcGFyYSBwcmVkZWNpciBsYSBubyByZXNwdWVzdGEgZW4gaW5ncmVzb3MNCg0KDQpMYSBpZGVhIGFob3JhIGVzIHF1ZSB1c3RlZGVzIGVudHJlbmVuIG90cm8gbW9kZWxvLiBWYW1vcyBhIGVudHJlbmFyIHkgZXZhbHVhciBvdHJvIG1vZGVsbyBlbiBvdHJvIHByb2JsZW1hLiBUcmF0ZW1vcyBkZSBwcmVkZWNpciBsYSBwcm9iYWJpbGlkYWQgZGUgcXVlIHVuYSBwZXJzb25hIG5vIGNvbnRlc3RlIGluZ3Jlc29zLiBVc2Vtb3MgcGFyYSBlbGxvIHVuIGFyYm9sIGRlIGRlY2lzacOzbi4NCg0KYGBge3J9DQojIyMNCmBgYA0KDQo=