Objetivos

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:

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)
-- Attaching packages --------------------------------------- tidyverse 1.2.1 --
v tibble  2.1.3     v purrr   0.2.5
v tidyr   0.8.2     v dplyr   0.8.3
v readr   1.3.1     v stringr 1.3.1
v tibble  2.1.3     v forcats 0.3.0
-- Conflicts ------------------------------------------ tidyverse_conflicts() --
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()
x purrr::lift()   masks caret::lift()

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

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…

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:

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.

rr ###

LS0tDQp0aXRsZTogIkludHJvZHVjY2nDs24gKHNpbXBsZSkgYSBjYXJldCINCmF1dGhvcjogIkdlcm3DoW4gUm9zYXRpIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KIyMgT2JqZXRpdm9zDQoNCi0gSW50cm9kdWNpciBhbGd1bm9zIGNvbmNlcHRvcyBiw6FzaWNvcyBkZWwgZW5mb3F1ZSBkZWwgQXByZW5kaXphamUgQXV0b23DoXRpY28NCi0gTW9zdHJhciBlbCBmcmFtZXdvcmsgYGNhcmV0YCBwYXJhIGF1dG9tYXRpemFyIGFsZ3VuYXMgdGFyZWFzIGRlbCBlbnRyZW5hbWllbnRvDQoNCg0KIyMgRWwgcHJvYmxlbWENCg0KTnVlc3RybyBwcm9ibGVtYSBjZW50cmFsIGVzLCBwb2RlciByZWFsaXphciB1biBtb2RlbG8gcXVlIGxvZ3JlIHByZWRlZGlyICBsb3MgaW5ncmVzb3MgZGUgbGEgb2N1cGFjacOzbiBwcmluY2lwYWwgKGBwMjFgKSBlbiBsYSBFUEggZGVsIHNlZ3VuZG8gdHJpbWVzdHJlIGRlbCAyMDE1Lg0KDQpQdWVkZSBub3RhcnNlIHF1ZSBzZSB0cmF0YSBkZSB1biBwcm9ibGVtYSBiYXN0YW50ZSBhbWlnYWJsZSwgcG9yIGFzw60gZGVjaXJsbywgYWwgZW5mb3F1ZSBkZSBNYWNoaW5lIExlYXJuaW5nOg0KDQotIHRlbmVtb3MgdW4gY29uanVudG8gZGUgY2Fzb3MgZW4gbG9zIHF1ZSBkZXNjb25vY2Vtb3MgbnVlc3RyYSB2YXJpYWJsZSAkWSQgDQotIHF1ZXJlbW9zIHByZWRlY2lybGENCg0KDQojIyBQcmVwcm9jZXNhbmRvIGxvcyBkYXRvcw0KDQpMbyBwcmltZXJvIHF1ZSB0ZW5lbW9zIHF1ZSBoYWNlciBlcyBpbXBvcnRhciBsYXMgbGlicmVyw61hcyBjb24gbGFzIHF1ZSB2YW1vcyBhIHRyYWJhamFyOg0KDQoNCmBgYHtyfQ0KbGlicmFyeShjYXJldCkNCmxpYnJhcnkodGlkeXZlcnNlKQ0KYGBgDQoNCg0KTHVlZ28sIGNhcmdhbW9zIGxvcyBkYXRvcyB5IGZvcm1hdGVhbW9zIHVuIHBvY28gYWxndW5hcyBldGlxdWV0YXM6DQoNCg0KYGBge3J9DQpsb2FkKCcuLi9kYXRhL0VQSF8yMDE1X0lJLlJEYXRhJykNCg0KZGF0YSRwcDAzaTwtZmFjdG9yKGRhdGEkcHAwM2ksIGxhYmVscz1jKCcxLVNJJywgJzItTm8nLCAnOS1OUycpKQ0KDQoNCg0KZGF0YSRpbnRlbnNpPC1mYWN0b3IoZGF0YSRpbnRlbnNpLCBsYWJlbHM9YygnMS1TdWJfZGVtJywgJzItU09fbm9fZGVtJywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICczLU9jdXAucGxlbm8nLCAnNC1Tb2JyZW9jJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzUtTm8gdHJhYmFqbycsICc5LU5TJykpDQoNCmRhdGEkcHAwN2E8LWZhY3RvcihkYXRhJHBwMDdhLCBsYWJlbHM9YygnMC1OQycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzEtTWVub3MgZGUgdW4gbWVzJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnMi0xIGEgMyBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzMtMyBhIDYgbWVzZXMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc0LTYgYSAxMiBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzUtMTIgYSA2MCBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzYtTcOhcyBkZSA2MCBtZXNlcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzktTlMnKSkNCmBgYA0KDQoNCkV4aXN0ZW4gZW4gbnVlc3RybyBkYXRhc2V0LCBkYXRvcyBxdWUgbm8gY29udGVzdGFyb24gaW5ncmVzb3MuIFNvbiBkYXRvcyBwZXJkaWRvcyB5IHRlbmVtb3MgcXVlIHJlc29sdmVyIHF1w6kgaGFjZXIgY29uIGVsbG9zLiBFbiBlc3RlIGVqZW1wbG8gdmFtb3MgYSBlbGltaW5hcmxvcy4gRXN0YSBvcGNpw7NuIGVzdMOhIGxlam9zIGRlIHNlciBsYSDDs3B0aW1hLCBwZXJvIGxhIHNlbGVjY2lvbmFtb3MgcGFyYSBzaW1wbGlmaWNhciBlbCBwcm9ibGVtYSB5IGxhIGV4cG9zaWNpw7NuLiBFbiBjYXNvIGRlIHF1ZSBsZXMgaW50ZXJlc2UgZWwgdGVtYSBkZSBpbXB1dGFjacOzbiBkZSBtaXNzaW5nIGRhdGEgW2FxdcOtXShodHRwczovL3d3dy5zYWJlcmVzLmZjZWNvbi51bnIuZWR1LmFyL2luZGV4LnBocC9yZXZpc3RhL2FydGljbGUvdmlldy8xMzIpIHkgW2FxdcOtXShodHRwczovL2FzZXQub3JnLmFyLzIwMTkvcG9uZW5jaWFzLzIwX1Jvc2F0aS5wZGYpIHB1ZWRlbiBlbmNvbnRyYXIgZG9zIGFwbGljYWNpb25lcyBkZSBNYWNoaW5lIExlYXJuaW5nIGFsIHByb2JsZW1hICh5IHNvYnJlIGVsIG1pc21vIGRhdGFzZXQpLg0KDQoNCmBgYHtyfQ0KZGZfaW1wIDwtIGRhdGEgJT4lDQogICAgICAgIGZpbHRlcihpbXBfaW5nbGFiMT09MSkgJT4lDQogICAgICAgIHNlbGVjdCgtaW1wX2luZ2xhYjEpDQoNCmRmX3RyYWluIDwtIGRhdGEgJT4lDQogICAgICAgIGZpbHRlcihpbXBfaW5nbGFiMT09MCkgJT4lDQogICAgICAgIHNlbGVjdCgtaW1wX2luZ2xhYjEpICU+JQ0KICAgICAgICBtdXRhdGUocDIxID0gY2FzZV93aGVuKA0KICAgICAgICAgICAgICAgICAgICAgICAgcDIxPT0wIH4gMTAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgVFJVRSB+IHAyMSkpDQoNCmBgYA0KDQoNCiMjIyMgQWxndW5hcyBjb3NhcyBhIG5vdGFyDQoNClBvciB1biBsYWRvLCB2ZW1vcyBxdWUgZW5jYWRlbmFtb3MgdW5hcyBjdcOhbnRhcyBvcGVyYWNpb25lcyBtZWRpYW50ZSB1biBvcGVyYWRvciAoYCU+JWApIGxsYW1hZG8gYHBpcGVgLiBFbCBwaXBlIGVzIHVuIHPDrW1ib2xvIHF1ZSByZWxhY2lvbmEgZG9zIGVudGlkYWRlcy4gRGljaG8gZW4gZm9ybWEgbcOhcyBzaW1wbGUsIGVsIHBpcGUgZGUgUiBlc3TDoSBlbiBmYW1pbGlhIGNvbiBvdHJvcyBvcGVyYWRvcmVzIG3DoXMgY29udmVuY2lvbmFsZXMsIGNvbW8gKywgLSBvIC8uIFkgYWwgaWd1YWwgcXVlIGxvcyBvdHJvcyBvcGVyYWRvcmVzLCBlbnRyZWdhIHVuIHJlc3VsdGFkbyBlbiBiYXNlIGEgbG9zIG9wZXJhbmRvcyBxdWUgcmVjaWJlLiANCg0KQWhvcmEgYmllbuKApiDCv1BhcmEgcXXDqSBzaXJ2ZT8gRW4gcmVzdW1pZGFzIGN1ZW50YXMsIGhhY2UgcXVlIGVsIGPDs2RpZ28gbmVjZXNhcmlvIHBhcmEgcmVhbGl6YXIgdW5hIHNlcmllIGRlIG9wZXJhY2lvbmVzIGRlIHRyYW5zZm9ybWFjacOzbiBkZSBkYXRvcyBzZWEgbXVjaG8gbcOhcyBzaW1wbGUgZGUgZXNjcmliaXIgeSBkZSBpbnRlcnByZXRhci4NCg0KUmVwYXNlbW9zIGxhIHByaW1lciBzZWN1ZW5jaWENCg0KLSBmaWx0cmFtb3MgbG9zIGRhdG9zIGNvbiBhbGfDum4gcGVyZGlkbyAoYCU+JSBmaWx0ZXIoaW1wX2luZ2xhYj09MSlgKQ0KLSBlbGltaW5hbW9zIGxhIGNvbHVtbmEgaWRlbnRpZmljYWRvcmEgZGUgbG9zIGNhc29zIHBlcmRpZG9zIChgc2VsZWN0KC1pbXBfaW5nbGFiKWApDQoNCg0KIyMjIEVzdGltYW5kbyBlbCBlcnJvciBkZSBnZW5lcmFsaXphY2nDs24NCg0KUmVjb3JkZW1vczogdGVuZW1vcyBtdWNoYXMgZm9ybWFzIGRlIGVzdGltYXIgZWwgZXJyb3IgZGUgZ2VuZXJhbGl6YWNpw7NuICh0cmFpbi10ZXN0IHNwbGl0LCBjcm9zcyB2YWxpZGF0aW9uLCBib290c3RyYXApLiBVc2FyZW1vcyB1bmEgZXN0cmF0ZWdpYSBkZSB2YWxpZGFjacOzbiBjcnV6YWRhLiBWYW1vcyBhIGdlbmVyYXIgbG9zIMOtbmRpY2VzIG1lZGlhbnRlIGBjYXJldGAuIA0KDQpWYW1vcyBhIHRlbmVyIHF1ZSBmaWphciBkb3MgZXN0cmF0ZWdpYXMgZGUgZXN0aW1hY2nDs24gZGVsIGVycm9yOiBsYSBwcmltZXJhIHBhcmEgZXN0aW1hciBsb3MgaGlwZXJwYXLDoW1ldHJvcyBkZSBsb3MgbW9kZWxvcyB5IGxhIHNlZ3VuZGEgcGFyYSBsYSBlc3RpbWFjacOzbiBmaW5hbCBkZWwgZXJyb3IgZGUgZ2VuZXJhbGl6YWNpw7NuLiBFbiBhbWJvcyBjYXNvcywgdXRpbGl6YXJlbW9zIHZhbGlkYWNpw7NuIGNydXphZGEsIHBlcm8gc29icmUgZG9zIG11ZXN0cmFzIGRpZmVyZW50ZXMuDQoNClByaW1lcm8sIGZpamFtb3MgbGEgc2VtaWxsYSBhbGVhdG9yaWEgKHBhcmEgYXNlZ3VyYXJub3MgbGEgcG9zaWJpbGlkYWQgZGUgcmVwbGljYWJpbGlkYWQpDQoNCg0KYGBge3J9DQpzZXQuc2VlZCg5NTcpDQpgYGANCg0KDQpQb2RlbW9zIHVzYXIgbGEgZnVuY2nDs24gYGNyZWF0ZUZvbGRzKClgIHBhcmEgZ2VuZXJhciBsb3Mgw61uZGljZXMuIEFxdcOtLCBwYXMNCg0KDQpgYGB7cn0NCmN2X2luZGV4IDwtIGNyZWF0ZUZvbGRzKHkgPSBkZl90cmFpbiRwMjEsDQogICAgICAgICAgICAgICAgICAgICAgICBrPTUsDQogICAgICAgICAgICAgICAgICAgICAgICBsaXN0PVRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICByZXR1cm5UcmFpbj1UUlVFKQ0KYGBgDQoNCg0KQXF1w60gdXNhbW9zIHRyZXMgYXJndW1lbnRvczoNCg0KLSBgeSA9IGRmX3RyYWluJHAyMWAsIGVzIGVsIHZlY3RvciBkZSByZXN1bHRhZG9zLiBFbiBudWVzdHJvIGNhc28sIGxvcyBpbmdyZXNvcyBkZSBsYSBvY3VwYWNpw7NuIHByaW5jaXBhbA0KLSBgaz01YCwgZXMgbGEgY2FudGlkYWQgZGUgZ3J1cG9zIHBhcmEgcmVhbGl6YXIgbGEgdmFsaWRhY2nDs24gY3J1emFkYQ0KLSBgcmV0dXJuVHJhaW49VFJVRWAsIGxlIGRlY2ltb3MgcXVlIGxvIHF1ZSBub3MgZGV2dWVsdmEsIHNlYW4gbGFzIHBvc2ljaW9uZXMgZGUgY29ycmVzcG9uZGllbnRlcyBhIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIGVuIGNhZGEgcG9zaWNpw7NuDQoNCkZpbmFsbWVudGUsIGVzcGVjaWZpY2Ftb3MgZWwgZGlzZcOxbyBkZSByZW11ZXN0cmVvIG1lZGlhbnRlIGxhIGZ1bmNpw7NuIGB0cmFpbkNvbnRyb2xgOg0KDQpgYGB7cn0NCmZpdENvbnRyb2wgPC0gdHJhaW5Db250cm9sKA0KICAgICAgICBpbmRleD1jdl9pbmRleCwNCiAgICAgICAgbWV0aG9kPSJjdiIsDQogICAgICAgIG51bWJlcj01KQ0KYGBgDQoNCg0KIyMgRW50cmVuYW5kbyBtb2RlbG9zIChgdHJhaW4oKWApDQoNClRlbmVtb3MgbGlzdG8gbnVlc3RybyBlc3F1ZW1hIGRlIHJlbXVlc3RyZW8uIFBvZGVtb3MgcGFzYXIgYSBlbnRyZW5hciBudWVzdHJvIHByaW1lciBtb2RlbG8uIFBhcmEgZWxsbyBoYXJlbW9zIHVzbyBleHRlbnNpdm8gZGUgbGEgZnVuY2nDs24gYHRyYWluKClgLiBMYSBtaXNtYSBwdWVkZSB1c2Fyc2UgcGFyYSANCg0KLSBldmFsdWFyIG1lZGlhbnRlIHJlbXVlc3RyZW8gZWwgZWZlY3RvIGRlIGNhZGEgaGlwZXJwYXLDoW1ldHJvIGVuIGxhIHBlcmZvcm1hbmNlDQotIGVsZWdpciBlbCBtb2RlbG8gIsOzcHRpbW8iIChsYSBtZWpvciBjb21iaW5hY2nDs24gZGUgcGFyw6FtZXRyb3MpIA0KLSBlc3RpbWFyIGxhIHBlcmZvcm1hbmNlIGRlbCBtb2RlbG8NCg0KUHJpbWVybywgZGViZW1vcyBlbGVnaXIgZWwgbW9kZWxvIHBhcmEgZW50cmVuYXIuIEFjdHVhbG1lbnRlLCBgY2FyZXRgIGRpc3BvbmUgZGUgMjM4IG1vZGVsb3MgZGlzcG9uaWJsZXMuIFB1ZWRlIGNvbnN1bHRhcnNlIFtsYSBzZWNjaW9uIGNvcnJlc3BvbmRpZW50ZSBdKGh0dHA6Ly90b3BlcG8uZ2l0aHViLmlvL2NhcmV0L2F2YWlsYWJsZS1tb2RlbHMuaHRtbCkgZGVsIHNpdGlvIHBhcmEgbWF5b3JlcyBkZXRhbGxlcy4gVGFtYmnDqW4sIGxsZWdhZG8gZWwgY2FzbywgcG9kcsOtYW4gdXNhcnNlIG1vZGVsb3MgYWQtaG9jIGRlZmluaWRvcyBwb3IgZWwgdXN1YXJpby4NCg0KQ29tZW5jZW1vcyBjb24gdW4gbW9kZWxvIHNpbXBsZSwgcGVybyBlZmVjdGl2bzogdW5hIHJlZ3Jlc2nDs24gbGluZWFsLiBDb21vIHBvZHLDoW4gdmVyIGVuIGVsIHNpdGlvLCBjYWRhIG1vZGVsbyBwdWVkZSBzZXIgZXN0aW1hZG8gcG9yIGRpZmVyZW50ZXMgaW1wbGVtZW50YWNpb25lcyBlbiBkaWZlcmVudGVzIHBhcXVldGVzLiBOb3NvdHJvcyB1c2FyZW1vcyBsYSBpbXBsZW1lbnRhY2nDs24gZGUgci1iYXNlIGBsbSgpYCBwb3Igc2ltcGxpY2lkYWQuDQoNCkVudHJlbmVtb3MgdW5hIHJlZ3Jlc2nDs24gbGluZWFsIGNvbiBjYXJldDogY29tZW5jZW1vcyBjb24gdW4gbW9kZWxvIHNpbXBsZSwgc2V4byB5IGVkYWQuDQoNCmBgYHtyfSAgICAgICAgDQpsbV9wMjEgPC0gdHJhaW4ocDIxIH4gY2gwNCArIGNoMDYsIGRhdGEgPSBkZl90cmFpbiwgDQogICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJsbSIsIA0KICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBmaXRDb250cm9sKQ0KDQpsbV9wMjENCmBgYA0KDQpWZWFtb3MgbG9zIGNvZWZpY2llbnRlcy4uLg0KYGBge3J9DQpsbV9wMjEkZmluYWxNb2RlbA0KYGBgDQoNCsK/UXXDqSBzZSBwdWVkZSB2ZXI/DQoNCg0KVmVhbW9zLCBhaG9yYSwgdW4gbW9kZWxvIG3DoXMgY29tcGxlam86DQoNCmBgYHtyIHdhcm5pbmc9RkFMU0V9DQpsbV9wMjFfYiA8LSB0cmFpbihwMjEgfiAuLCBkYXRhID0gZGZfdHJhaW4sIA0KICAgICAgICAgICAgICAgICBtZXRob2QgPSAibG0iLCANCiAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gZml0Q29udHJvbCkNCmBgYA0KDQpgYGB7cn0NCmxtX3AyMV9iDQpgYGANCg0KDQpMb3MgbW9kZWxvcyBkZSBtYWNoaW5lIGxlYXJuaW5nIHRpZW5lbiBjaWVydG9zIHBhcsOhbWV0cm9zIHF1ZSBkZWJlbiBzZXIgc2VsZWNjaW9uYWRvcyBhbnRlcyBkZSBlc3RpbWFyIGVsIG1vZGVsbywgcHJvcGlhbWVudGUgZGljaG86IHNlIGxsYW1hbiBbX19oaXBlcnBhcsOhbWV0cm9zX19dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0h5cGVycGFyYW1ldGVyXyhtYWNoaW5lX2xlYXJuaW5nKSkuIFNpIGJpZW4gbGEgcmVncmVzacOzbiBsaW5lYWwgbm8gZXMgZXN0cmljdGFtZW50ZSBoYWJsYW5kbyB1biBtb2RlbG8gZGUgbWFjaGluZSBsZWFybmluZyAoYXVucXVlIG11Y2hlcyBsbyBjb25zaWRlcmFuIGNvbW8gdGFsKSBzw60gdGllbmUgYWxnbyBxdWUgc2UgbGUgcGFyZWNlIGJhc3RhbnRlIGEgdW4gaGlwZXJwYXLDoW1ldHJvLi4uIGxhIGV4aXN0ZW5jaWEgZGUgdW4gaW50ZXJjZXB0by4gRW4gZWZlY3RvLCBub3NvdHJvcyBlc3RpbWFtb3MgdW4gbW9kZWxvIGRlIGxhIHNpZ3VpZW50ZSBmb3JtYToNCg0KDQokeV97aX0gPSBcYmV0YV97MH0gKyBcc3VtX3twPTF9XlAgXGJldGFfe3B9IFhfe2l9JA0KDQpQZXJvIHBvZHLDrWFtb3MgaGFiZXIgZXN0aW1hZG8NCg0KJHlfe2l9ID0gXHN1bV97cD0xfV5QIFxiZXRhX3twfSBYX3tpfSQNCg0KTcOhcyBhbGzDoSBkZSBsYSBkaXNjdXNpw7NuIHNvYnJlIHNpIGxhIHJlZ3Jlc2nDs24gZXMgTUwgbyBubywgbG8gaW50ZXJlc2FudGUgZXMgdmVyIHF1ZSBsYSBkZWNpc2nDs24gc29icmUgZWwgZW50cmVuYW1pZW50byBkZSB1biBtb2RlbG8gbGluZWFsIGNvbiBpbnRlcmNlcHRvIG8gbm8sIGVzIHVuYSBkZWNpc2nDs24gcXVlIHNlIHRvbWEgYW50ZXMgZGUgZW50cmVuYXIgZWwgbW9kZWxvIHByb3BpYW1lbnRlIGRpY2hvLg0KDQoNCkFob3JhIGJpZW4sIHZhbW9zIGEgYnVzY2FyIG90cm8gbW9kZWxvIGNvbiBtZWpvcmVzIGhpcGVycGFyw6FtZXRyb3MgcGFyYSB0dW5lYXI6IHVuIMOhcmJvbCBkZSBkZWNpc2nDs24uIFNpIGJpZW4sIGxvIHZhbW9zIGEgdmVyIGVuIGRldGFsbGUgbGEgcHLDs3hpbWEgY2xhc2UsIHZhbW9zIGEgcmV2aXNhciBzdSBpbXBsZW1lbnRhY2nDs24gZW4gY2FyZXQuDQoNCg0KIyMgVHVuZWFuZG8gaGlwZXJwYXLDoW1ldHJvcy4uLg0KDQpQb2RlbW9zIGVudG9uY2VzLCBjb21wYXJhciBsYSBwZXJmb3JtYW5jZSBkZSB1biBtb2RlbG8gY29uIHkgc2luIGhpcGVycGFyw6FtZXRyb3MuIFBhcmEgZWxsbywgcHJpbWVybyB0ZW5lbW9zIHF1ZSBjb25zdHJ1aXIgbGEgZ3JpbGxhIGRlIGhpcGVycGFyw6FtZXRyb3MuDQoNCmBgYHtyfQ0KZ3JpZCA8LSBleHBhbmQuZ3JpZChtYXhkZXB0aD1jKDEsIDIsIDQsIDgsIDE2KSkNCmBgYA0KDQpZIHZvbHZlbW9zIGEgZW50cmVuYXIgZWwgbW9kZWxvOg0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KY2FydF9wMjEgPC0gdHJhaW4ocDIxIH4gLiAsIA0KICAgICAgICAgICAgICAgICBkYXRhID0gZGZfdHJhaW4sIA0KICAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgDQogICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGZpdENvbnRyb2wsDQogICAgICAgICAgICAgICAgIHR1bmVHcmlkID1ncmlkKQ0KDQpjYXJ0X3AyMQ0KYGBgDQoNCkVuIGVzdGUgY2FzbywgaGVtb3MgcmVhbGl6YWRvIHVuYSBiw7pzcXVlZGEgZXhoYXVzdGl2YSwgZXMgZGVjaXIsIGhlbW9zIHJlY29ycmlkbyBsYSB0b3RhbGlkYWQgZGUgbGEgZ3JpbGxhIGRlIGhpcGVycGFyw6FtZXRyb3MgeSBoZW1vcyBzZWxlY2Npb25hZG8gZWwgbWVqb3IgbW9kZWxvLiBDb21vIHB1ZWRlIHZlcnNlLCBlc3RvIGhhIGxsZXZhZG8gdW4gdGllbXBvIGRlIGPDs21wdXRvIG5hZGEgZGVzcHJlY2lhYmxlLiANCg0KRXMgcG9yIGVsbG8gcXVlIGV4aXN0ZSB1bmEgc2VndW5kYSBvcGNpw7NuLi4uDQoNCg0KIyMjIFJhbmRvbSBzZWFyY2gNCg0KRW4gZXN0ZSBjYXNvLCBlbiBsdWdhciBkZSByZWFsaXphciB1bmEgYsO6c3F1ZWRhIGV4aGF1c3RpdmEsIHBvZGVtb3MgcmVkdWNpciBub3RhYmxlbWVudGUgZWwgdGllbXBvIGRlIGPDs21wdXRvIGJ1c2NhbmRvIGVuIHVuYSBtdWVzdHJhIGFsZWF0b3JpYSBkZSBsYSBncmlsbGEgZGUgaGlwZXJwYXLDoW1ldHJvcy4gUGFyYSBlc3RvLCBzb2xhbWVudGUgZGViZW1vcyBhZ3JlZ2FyIHVuIHBhcsOhbWV0cm8gZW4gZWwgb2JqZXRvIGBmaXRDb250cm9sYDoNCg0KDQpgYGB7ciB3YXJuaW5nPVRSVUV9DQpmaXRDb250cm9sX3JhbmQgPC0gdHJhaW5Db250cm9sKA0KICAgICAgICBpbmRleD1jdl9pbmRleCwgDQogICAgICAgIG1ldGhvZD0iY3YiLA0KICAgICAgICBudW1iZXI9NSwNCiAgICAgICAgc2VhcmNoID0gJ3JhbmRvbScpDQpgYGANCg0KDQpZIHZvbHZlbW9zIGEgZW50cmVuYXIgZWwgbW9kZWxvOg0KDQoNCmBgYHtyfQ0KY2FydF9wMjFfcmFuZCA8LSB0cmFpbihwMjEgfiAuLCBkYXRhID0gZGZfdHJhaW4sIA0KICAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgDQogICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGZpdENvbnRyb2xfcmFuZCwNCiAgICAgICAgICAgICAgICAgdHVuZUxlbmd0aCA9IDIpDQoNCmNhcnRfcDIxX3JhbmQNCmBgYA0KDQoNCiMjIFNlbGVjY2lvbmFuZG8gZWwgbWVqb3IgbW9kZWxvDQoNClVuYSB2ZXogZmluYWxpemFkbyBlbCBwcm9jZXNvIGRlIHR1bm5pbmcgZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MsIHBvZGVtb3MgcHJvY2VkZXIgYSBlbGVnaXIgY3XDoWwgZXMgZWwgbWVqb3IgbW9kZWxvLiANCg0KYGBge3J9DQpjYXJ0X3AyMQ0KYGBgDQoNCg0KUG9kZW1vcyBwZXJzaXN0aXIgIGVsIG1vZGVsbyBlbiBkaXNjbyAoc2kgcXVpc2nDqXJhbW9zKToNCg0KYGBge3J9DQpzYXZlUkRTKGNhcnRfcDIxLCAnLi4vbW9kZWxzL3AyMV9jYXJ0LnJkcycpDQpgYGANCg0KDQpQb2RlbW9zIHJlYWxpemFyIHVuIHBsb3QgZGVsIGVmZWN0byBkZSBsb3MgaGlwZXJwYXLDoW1ldHJvczoNCg0KYGBge3J9DQpnZ3Bsb3QoY2FydF9wMjEpDQpgYGANCg0KDQpFeGlzdGVuIGRpZmVyZW50ZXMgbcOpdHJpY2FzIGRlIHNlbGVjY2nDs24sIGxhcyBjdWFsZXMgZGViZW4gc2VyIGRlZmluaWRhcyBlbiBsYSBmdW5jacOzbiBgdHJhaW5gLCB1c2FuZG8gZWwgYXJndW1lbnRvIGBzZWxlY3Rpb25GdW5jdGlvbmAgcXVlIHB1ZWRlIHRvbWFyIHRyZXMgdmFsb3JlczoNCg0KLSBgImJlc3QiYDogc2Ugc2VsZWNjaW9uYSBlbCBtZWpvciBtb2RlbG8gY29uIGxhIG1lbm9yIG3DqXRyaWNhIGRlIGVycm9yIChsYSBxdWUgdXNhcmVtb3MgYXF1w60pDQotIGAib25lU0UiYDogdXRpbGl6YSBsYSByZWdsYSBkZSAidW4gZGVzdsOtbyBlc3TDoW5kYXIiIGRlIFtCcmVpbWFuIGV0IGFsICgxOTg2KV0oaHR0cHM6Ly9ib29rcy5nb29nbGUuY29tLmFyL2Jvb2tzL2Fib3V0L0NsYXNzaWZpY2F0aW9uX2FuZF9SZWdyZXNzaW9uX1RyZWVzLmh0bWw/aWQ9SndReC1XT21TeVFDJnJlZGlyX2VzYz15JmhsPWVzKQ0KLSBgInRvbGVyYW5jZWA7IHF1ZSBidXNjYSBzZWxlY2Npb25hciBlbCBtb2RlbG8gbWVub3MgY29tcGxlam8gZGVudHJvIGRlIHVuIG1hcmdlbiBkZSB0b2xlcmFuY2lhIHJlc3BlY3RvIGFsIG1lam9yIG1vZGVsbw0KDQpUYW1iacOpbiBwb2Ryw61hbiBkZWZpbmlyc2UgbcOpdG9kb3MgYWQtaG9jIHBhcmEgZXN0YSBzZWxlY2Npw7NuLg0KDQoNCmBgYHtyfQ0KY2FydF9wMjEkYmVzdFR1bmUNCmBgYA0KDQoNCsK/Q3XDoWwgZXMgZWwgbWVqb3IgbW9kZWxvIChlbiB0w6lybWlub3MgYWJzb2x1dG9zKT8NCg0KDQojIyBSZWFsaXphbmRvIGxhIGV2YWx1YWNpw7NuIGZpbmFsDQoNClVuYSB2ZXogcXVlIGhlbW9zIHNlbGVjY2lvbmFkbyBlbCBtZWpvciBtb2RlbG8sIHBvZGVtb3MgcGFzYXIgYSBsYSBldmFsdWFjacOzbiBmaW5hbCB5IHBlcnNpc3RpbW9zIGVsIG1vZGVsbyBwYXJhIHJldXRpbGl6YXJsbyBlbiBvdHJhcyBhcGxpY2FjaW9uZXMuDQoNClByaW1lcm8sIHRlbmVtb3MgcXVlIHZvbHZlciBhIGdlbmVyYXIgdW4gZXNxdWVtYSBkZSB2YWxpZGFjacOzbiBjcnV6YWRhOg0KDQpgYGB7cn0NCnNldC5zZWVkKDc0MTIpDQpjdl9pbmRleF9maW5hbCA8LSBjcmVhdGVGb2xkcyh5ID0gZGZfdHJhaW4kcDIxLA0KICAgICAgICAgICAgICAgICAgICAgICAgaz01LA0KICAgICAgICAgICAgICAgICAgICAgICAgbGlzdD1UUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuVHJhaW49VFJVRSkNCg0KZml0Q29udHJvbF9maW5hbCA8LSB0cmFpbkNvbnRyb2woDQogICAgICAgIGluZGV4T3V0PWN2X2luZGV4X2ZpbmFsLCANCiAgICAgICAgbWV0aG9kPSJjdiIsDQogICAgICAgIG51bWJlcj01KQ0KYGBgDQoNCg0KWSBlbnRyZW5hbW9zIHVuYSB2ZXogbcOhczoNCg0KDQpgYGB7cn0NCmNhcnRfZmluYWw8LXRyYWluKHAyMSB+IC4sIGRhdGEgPSBkZl90cmFpbiwNCiAgICAgICAgICAgICAgICBtZXRob2QgPSAicnBhcnQyIiwgDQogICAgICAgICAgICAgICAgdHJDb250cm9sID0gZml0Q29udHJvbF9maW5hbCwgDQogICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBjYXJ0X3AyMSRiZXN0VHVuZSwNCiAgICAgICAgICAgICAgICBtZXRyaWM9J1JNU0UnKQ0KDQojc2F2ZVJEUyhyZl9maW5hbCwgJy4uL21vZGVscy9yZl9maW5hbC5SRFMnKQ0KDQpjYXJ0X2ZpbmFsDQpgYGANCg0KVmVtb3MgZW50b25jZXMgcXVlIGVsIG1vZGVsbyBzZWxlY2Npb25hZG8gcGVyZm9ybWEgY29uIHVuICRSXjI9MC4zNiQgeSB1biAkUk1TRT00NjAzJC4gU29sYW1lbnRlIG5vcyBxdWVkYSBlbnRyZW5hciBlbCBtb2RlbG9zIHNvYnJlIGxhIHRvdGFsaWRhZCBkZWwgZGF0YXNldCBkZSBlbnRyZW5hbWllbnRvOg0KDQoNCmBgYHtyfQ0KY2FydF9maW5hbF9mPC10cmFpbihwMjF+LiwgZGF0YT1kZl90cmFpbiwNCiAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJycGFydDIiLA0KICAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBjYXJ0X3AyMSRiZXN0VHVuZSkNCg0KY2FydF9maW5hbF9mDQpgYGANCg0KDQojIyBPYnRlbmllbmRvIGxhcyBwcmVkaWNjaW9uZXMgZmluYWxlcw0KDQpFbCDDumx0aW1vIHBhc28gZXMgb2J0ZW5lciBsYXMgcHJlZGljY2lvbmVzIGZpbmFsZXMgKGVzIGRlY2lyLCBudWVzdHJhcyBpbXB1dGFjaW9uZXMpLiBEZSBmb3JtYSBpbnRlcmVzYW50ZSwgcG9kZW1vcyB1dGlsaXphciBsc28gZGF0b3MgcGVyZGlkb3MgY29tbyBkYXRvcyAibnVldm9zIiB5IGRlc2Nvbm9jaWRvcy4NCg0KRXMgZGVjaXIgcXVlLCBmaW5hbG1lbnRlLCBoYWJyZW1vcyByZWFsaXphZG8gdW5hIGltcHV0YWNpw7NuIGRlIGRhdG9zIHBlcmRpZG9zLiBQYXJhIGVsbG8sIGxsYW1hbW9zIGEgbGEgZnVuY2nDs24gYHByZWRpY3QoKWAgcXVlIHRvbWEgY29tbyBwcmltZXIgYXJndW1lbnRvIGFsIG9iamV0byBxdWUgY29udGllbmUgYWwgbW9kZWxvIGZpbmFsIHkgY29tbyBzZWd1bmRvIGFyZ3VtZW50byBlbCBkYXRhLmZyYW1lIGNvbiBsb3MgZGF0b3MgYSBpbXB1dGFyOg0KDQoNCmBgYHtyfQ0KeV9wcmVkc19jYXJ0IDwtIHByZWRpY3QoY2FydF9maW5hbF9mLCBkZl9pbXApDQpgYGANCg0KQ29tcGFyZW1vcywgYWhvcmEsIGxhcyBkaXN0cmlidWNpb25lcyBkZSBkYXRvcyBpbXB1dGFkb3MgcG9yIGVsIElOREVDIChtZWRpYW50ZSBlbCBtw6l0b2RvIEhvdCBEZWNrKSB5IGxvcyBxdWUgaGVtb3MgaW1wdXRhZG8gY29uIGBycGFydDJgLiBQYXJhIGVsbG8sIG9yZ2FuaXphbW9zIHRvZG8gZW4gdW4gZGF0YSBmcmFtZSBxdWUsIGx1ZWdvLCBsbGV2YW1vcyBhbCBmb3JtYXRvIHRpZHkuDQoNCg0KYGBge3J9DQpwcmVkcyA8LSBjYmluZCh5X3ByZWRzX2NhcnQsDQogICAgICAgICAgICAgICBkZl9pbXAkcDIxDQopDQoNCmNvbG5hbWVzKHByZWRzKSA8LSBjKCdDQVJUJywgJ0hvdF9EZWNrJykNCg0KcHJlZHMgPC0gcHJlZHMgJT4lIGFzLmRhdGEuZnJhbWUoKSAlPiUgZ2F0aGVyKG1vZGVsLCB2YWx1ZSkNCg0KYGBgDQoNCg0KRmluYWxtZW50ZSwgcGxvdGVhbW9zIHVuIGdyw6FmaWNvIGRlIGRlbnNpZGFkIHBhcmEgY29tcGFyYXIgbGFzIGRpc3RyaWJ1Y2lvbmVzIGRlIGxvcyBjYXNvcyBpbXB1dGFkb3MgY29uIGFtYm9zIG3DqXRvZG9zLg0KDQoNCmBgYHtyfQ0KZ2dwbG90KHByZWRzKSArDQogICAgICAgIGdlb21fZGVuc2l0eShhZXMoeD12YWx1ZSwgZmlsbD1tb2RlbCksIGFscGhhPTAuNSkNCmBgYA0KDQpgYGB7cn0NCmdncGxvdChwcmVkcykgKw0KICAgICAgICBnZW9tX2hpc3RvZ3JhbShhZXMoeD12YWx1ZSwgZmlsbD1tb2RlbCksIGFscGhhPTAuNSwNCiAgICAgICAgICAgICAgICAgICAgICAgYmlucz01MCkNCmBgYA0KDQoNCiMjIyBQcsOhY3RpY2EgaW5kZXBlbmRpZW50ZTogZW50cmVuYW5kbyB1biDDoXJib2wgcGFyYSBwcmVkZWNpciBsYSBubyByZXNwdWVzdGEgZW4gaW5ncmVzb3MNCg0KDQpMYSBpZGVhIGFob3JhIGVzIHF1ZSB1c3RlZGVzIGVudHJlbmVuIG90cm8gbW9kZWxvLiBWYW1vcyBhIGVudHJlbmFyIHkgZXZhbHVhciBvdHJvIG1vZGVsbyBlbiBvdHJvIHByb2JsZW1hLiBUcmF0ZW1vcyBkZSBwcmVkZWNpciBsYSBwcm9iYWJpbGlkYWQgZGUgcXVlIHVuYSBwZXJzb25hIG5vIGNvbnRlc3RlIGluZ3Jlc29zLiBVc2Vtb3MgcGFyYSBlbGxvIHVuIGFyYm9sIGRlIGRlY2lzacOzbi4NCg0KYGBge3J9DQojIyMNCmBgYA0KDQo=