Objetivos

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

  1. Generar el esquema de validación cruzada
  2. Generar el grid de hiperparámetros
  3. Tunear el modelo
  4. Seleccionar y estimar el modelo final
  5. 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        
                                          

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=