Primera parte

library(dplyr)
library(ggplot2)
library(tidyr)
library(broom)
library(ISLR)
library(GGally)
library(modelr)
library(pROC)
library(cowplot)
library(OneR)
library(rlang)
library(purrr)
library(caret)
set.seed(1992)

La regresión logística es útil para problemas de predicción de clases. El problema que vamos a tratar de resolver es predecir si una persona va defaultear su deuda de tarjeta de crédito en base a ciertos predictores.

Conjunto de datos

Este conjunto de datos proviene de la librería ISLR (Introduction to Statistical Learning Using R) de James, Witten, Hastie y Tibshirani.

default <- Default
glimpse(default)
Observations: 10,000
Variables: 4
$ default <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No,...
$ student <fct> No, Yes, No, No, No, Yes, No, Yes, No, No, Yes, Yes, No, No, No, No, No, Yes, No, No, No, No, No, No, No, No, No, No, No, Yes, ...
$ balance <dbl> 729.5265, 817.1804, 1073.5492, 529.2506, 785.6559, 919.5885, 825.5133, 808.6675, 1161.0579, 0.0000, 0.0000, 1220.5838, 237.0451...
$ income  <dbl> 44361.625, 12106.135, 31767.139, 35704.494, 38463.496, 7491.559, 24905.227, 17600.451, 37468.529, 29275.268, 21871.073, 13268.5...

Tiene 4 variables:

  • default: La clase que queremos predecir
  • student: Binaria que indica si la persona es estudiante
  • balance: Balance promedio que le queda a la persona luego de sus pagos mensuales
  • income: Ingreso de la persona

Exploratorias

Analicemos la distribución de la clase

default %>% group_by(default) %>% summarise(numero_casos=n())

Vemos que estamos trabajando con un problema de clasificación con un claro desbalance de clase.

Realizamos un gráfico exploratorio completo para ver el comportamiento y las relaciones entre las variables. El color rojo designa a quienes no defaultean y el azul a los que sí.

ggpairs(default,mapping = aes(colour= default)) + theme(axis.text.x = element_text(angle = 90, hjust = 1)) + theme_bw()

¿Qué pueden decir de la relación entre balance y default?

¿Y entre income y balance?

¿Cuáles parecen ser buenas variables para predecir la probabilidad de default de una persona?

Limpieza

Para modelizar va a ser más necesario tener la variable default como numérica. Definimos la variable como {0,1} para los valores {“No”,“Yes”}

default <- default%>% mutate(default= case_when(default=="No"~0,
                                                default=="Yes"~1))

Problema

Queremos estimar \(P(Default=Yes|X)=P(X)\) para cada individuo y partir de ello poder definir un punto de corte para predecir quienes son los que van a entrar en default.

Regresión lineal

En este caso estamos modelando la probabilidad de la siguiente manera:

\(P(X)= \beta_0 + \sum\limits_{j=1}^p \beta_j X\)

Veamos que tan bueno es el modelo lineal para esto, usando balance como predictor.

test_mco <- default %>% 
              lm(formula = default~balance, data = .) 
tdy <- test_mco %>% tidy()
tdy
test_mco %>% glance()

Ambos estimadores son significativos y el test de significatividad global del modelo también es significativo. Veamos un gráfico de nuestro modelo

Parece tener bastantes problemas para estimar la probabilidad de default de los individuos. Por ejemplo, vemos que hay varios individuos a los cuales les asigna una probabilidad negativa.

Regresión logística

Para evitar estos problemas, usamos la funcion logistica

\(P(X)= \frac{e^{\beta_0 + \sum\limits_{j=1}^p \beta_j X}}{1+e^{\beta_0 + \sum\limits_{j=1}^p \beta_j X}}\)

El lado derecho se llama expit

Esta funcion acota el resultado entre 0 y 1, lo cual es mucho mas adecuado para modelar una probabilidad.

Luego de hacer algunas operaciones, podemos llegar a la expresion:

\(\log {\frac{P(x)}{1-P(x)}}= \beta_0 + \sum\limits_{j=1}^p \beta_j X\)

El lado izquierdo es el logaritmo de los odds y se llama logit

Partición Train y Testing

Realizamos una partición entre dataset de entrenamiento (70%) y testeo (30%) usando la función resample_partition del paquete modelr

train_test <- default %>% resample_partition(c(train=0.7,test=0.3))
default <- train_test$train %>% as_tibble()
test <- train_test$test %>% as_tibble()

Creación de fórmulas

Para aplicar la regresion logistica primero usamos la funcion formulas del paquete modelr para crear un objeto que contiene todas las formulas que vamos a utilizar.

logit_formulas <- formulas(.response = ~default, # único lado derecho de las formulas.
                         bal= ~balance, 
                         stud= ~student,  
                         inc= ~income,  
                         bal_stud=~balance+student, 
                         bal_inc=~balance+income, 
                         stud_inc=~student+income,  
                         full= ~balance + income + student  
                         )

Creación de modelos

Procedemos a crear los modelos a partir de estas fórmulas

models <- data_frame(logit_formulas) %>% # dataframe a partir del objeto formulas
  mutate(models = names(logit_formulas), # columna con los nombres de las formulas
         expression = paste(logit_formulas), # columna con las expresiones de las formulas
         mod = map(logit_formulas, ~glm(.,family = 'binomial', data = default))) # Que estamos haciendo acá? Que vamos a encontrar en la columna?

Modelos simples

Probamos los primeros tres modelos, aquellos que tienen un único predictor. Usamos la función tidy para obtener los parámetros estimados para estos tres modelos.

models %>% 
  filter(models %in% c('bal','stud','inc')) %>%
  mutate(tidy = map(mod,tidy)) %>%  # Qué realizamos en este paso? Que va a tener esta columna?
  unnest(tidy, .drop = TRUE) %>% 
  mutate(estimate=round(estimate,5),
         p.value=round(p.value,4))

¿Son significativos?

¿Qué interpretación pueden darle a estos valores?

Modelo completo

Ahora probamos con un modelo que utiliza las tres predictoras

models %>% 
  filter(models == "full") %>%
  mutate(tidy = map(mod,tidy)) %>%
  unnest(tidy, .drop = TRUE) %>% 
  mutate(estimate=round(estimate,5),
         p.value=round(p.value,4))

¿Que cambios hay respecto a los 3 modelos individuales previos?

Evaluación de todos los modelos

Con map() agregamos la función glance para traernos información relevante para el diagnóstico del modelo.

Con unnest() podemos ver la evaluación de cada modelo. Por último ordenamos las modelos por el deviance.

models <- models %>% 
  mutate(glance = map(mod,glance))
models %>% 
  unnest(glance, .drop = TRUE) %>%
  mutate(perc_explained_dev = 1-deviance/null.deviance) %>% 
  select(-c(models, df.null, AIC, BIC)) %>% 
  arrange(deviance)

El modelo que utiliza las 3 variables es el que minimiza el deviance. Los 3 últimos modelos reducen muy poco el deviance respecto a la deviance nula.

Gráficos de evaluación

Realizamos los gráficos para el modelo completo y uno de los modelos con mayor deviance (student+income).

En este caso estamos:

  1. Agregando las predicciones con augment con el parámetro type=“response” ¿Por qué hacemos esto? ¿Cuál es el valor por default de este parámetro?
  2. Armando las curvas ROC con pROC
models <- models %>% 
  mutate(pred= map(mod,augment, type.predict = "response"))
models$pred[1]
$bal
NA
prediction_full <- models %>% 
  filter(models=="full") %>% 
  unnest(pred, .drop=TRUE)
roc_full <- roc(response=prediction_full$default, predictor=prediction_full$.fitted)
prediction_bad <- models %>% 
  filter(models=="stud_inc") %>% 
  unnest(pred, .drop=TRUE)
roc_bad <- roc(response=prediction_bad$default, predictor=prediction_bad$.fitted)

Violin plots

violin_full=ggplot(prediction_full, aes(x=default, y=.fitted, group=default,fill=factor(default))) + 
  geom_violin() +
  theme_bw() +
  guides(fill=FALSE) +
  labs(title='Violin plot', subtitle='Modelo completo', y='Predicted probability')
violin_bad=ggplot(prediction_bad, aes(x=default, y=.fitted, group=default, fill=factor(default))) + 
  geom_violin() + 
  theme_bw() +
  guides(fill=FALSE) +
  labs(title='Violin plot', subtitle='Modelo malo', y='Predicted probability')
plot_grid(violin_bad, violin_full)

¿Qué es lo que estamos viendo en ellos? (Especial atención al eje de ordenadas)

¿Cuál parece ser un punto de corte adecuado para cada modelo?

Curvas ROC

ggroc(list(full=roc_full, bad=roc_bad), size=1) + geom_abline(slope = 1, intercept = 1, linetype='dashed') + theme_bw() + labs(title='Curvas ROC', color='Modelo')

print(paste('AUC: Modelo completo', roc_full$auc))
[1] "AUC: Modelo completo 0.949391254006722"
print(paste('AUC: Modelo malo', roc_bad$auc))
[1] "AUC: Modelo malo 0.565799201537779"

¿Qué significa cada uno de los ejes?

Gráfico de Hosmer-Lemeshow

Hosmer_Lemeshow_plot <- function(dataset, predicted_column, class_column, bins, positive_value, color='forestgreen', nudge_x=0, nudge_y=0.05){
  "Realiza un grafico de Hosmer-Lemeshow para un dataset"
  
  "* dataset: conjunto de datos
   * predicted_column: columna con la probabilidad predicha
   * class_column: columna con la clase a predecir
   * possitive_value: valor de la clase a predecir
   * bins: cantidad de grupos del gráfico
   * color: color de los puntos
   * nudge_x: desplazamiento de la etiqueta en el eje x
   * nudge_y: desplazamiento de la etiqueta en el eje y"
  
  # Asignar los grupos a las observaciones de acuerdo a la probabilidad predicha
  dataset['group'] <- bin(dataset[predicted_column], nbins = bins, method = 'l', labels=c(1:bins))
  
  # Contar la cantidad de casos positivos por grupo
  positive_class <- dataset %>% filter(!!sym(class_column)==positive_value) %>% group_by(group) %>% count()
  
  # Obtener la media de las predicciones por grupo
  HL_df <- dataset %>% group_by(group) %>% summarise(pred=mean(!!sym(predicted_column)), count=n()) %>%
            inner_join(.,positive_class) %>%
            mutate(freq=n/count)
  
  # Gráfico 
  HM_plot <- ggplot(HL_df, aes(x=pred, y=freq)) + geom_point(aes(size=n), color=color) +
                geom_text(aes(label=n),nudge_y = nudge_y)+
                geom_abline(slope = 1, intercept = 0, linetype='dashed') + 
                theme_bw() +
                labs(title='Hosmer-Lemeshow', size='Casos', x="Probabilidad Predicha", y="Frecuencia observada")
  return(HM_plot)
}
Hosmer_Lemeshow_plot(prediction_full, '.fitted', 'default', 10, 1) + labs(subtitle="Modelo completo")

Hosmer_Lemeshow_plot(prediction_bad, '.fitted', 'default', 10, 1, color = "firebrick", nudge_y = 0.003) + scale_x_continuous(limits = c(0.02,.06)) + scale_y_continuous(limits = c(.02,.06)) + labs(subtitle="Modelo malo")

¿Qué vemos en estos gráficos?

¿Para qué valores parece existir una sobreestimación de la probabilidad? ¿Para cuáles subestimación?

Punto de corte

Hasta ahora hemos evaluado el modelo de manera general, pero el resultado final del modelo debe consistir en asignar al individuo una clase predicha. En nuestro caso debemos establecer un punto de corte según el cual vamos a separar a los indivuos en quienes defaultean y quienes no.

Probamos varios puntos de corte y graficamos el accuracy, la sensibilidad, la especificidad, el recall y la precision para cada uno de ellos.

Clases predichas / Clases Positiva Negativa
Positiva True Pos False Pos
Negativa False Neg True Neg

Recordemos que:

\(sensitivity = recall = \frac{TP}{TP+FN}\)

\(specificity = \frac{TN}{TN+FP}\)

\(precision = \frac{TP}{TP+FP}\)

prediction_metrics <- function(cutoff, predictions=prediction_full){
  table <- predictions %>% 
    mutate(predicted_class=if_else(.fitted>cutoff, 1, 0) %>% as.factor(),
           default= factor(default))
  
  confusionMatrix(table(table$predicted_class, table$default), positive = "1") %>%
    tidy() %>%
    select(term, estimate) %>%
    filter(term %in% c('accuracy', 'sensitivity', 'specificity', 'precision','recall')) %>%
    mutate(cutoff=cutoff)
  
}
cutoffs = seq(0.01,0.95,0.01)
logit_pred= map_dfr(cutoffs, prediction_metrics)%>% mutate(term=as.factor(term))
ggplot(logit_pred, aes(cutoff,estimate, group=term, color=term)) + geom_line(size=1) +
  theme_bw() +
  labs(title= 'Accuracy, Sensitivity, Specificity, Recall y Precision', subtitle= 'Modelo completo', color="")

¿Qué podemos observar en el gráfico?

¿Podemos definir un buen punto de corte? ¿Cuál sería?

¿Por qué la especificidad tiene ese comportamiento?

Dataset de testing

Seleccionamos el modelo completo, ya que es el que maximizaba el porcentaje de deviance explicada y en base a lo que vimos definimos un punto de corte en 0.25 (pueden probar otros)

sel_cutoff = 0.25
# Creamos el modelo
full_model <- glm(logit_formulas$full, family = 'binomial', data = default)
# Agregamos la predicciones al dataset de testeo
table= augment(x=full_model, newdata=test, type.predict='response') 
# Clasificamos utilizamos el punto de corte
table=table %>% mutate(predicted_class=if_else(.fitted>0.25, 1, 0) %>% as.factor(),
           default= factor(default))
# Creamos la matriz de confusión
confusionMatrix(table(table$default, table$predicted_class), positive = "1")
Confusion Matrix and Statistics

   
       0    1
  0 2839   65
  1   44   53
                                          
               Accuracy : 0.9637          
                 95% CI : (0.9564, 0.9701)
    No Information Rate : 0.9607          
    P-Value [Acc > NIR] : 0.21383         
                                          
                  Kappa : 0.4744          
 Mcnemar's Test P-Value : 0.05541         
                                          
            Sensitivity : 0.44915         
            Specificity : 0.98474         
         Pos Pred Value : 0.54639         
         Neg Pred Value : 0.97762         
             Prevalence : 0.03932         
         Detection Rate : 0.01766         
   Detection Prevalence : 0.03232         
      Balanced Accuracy : 0.71695         
                                          
       'Positive' Class : 1               
                                          

Segunda parte

Desbalanceo de la clase

Al explorar el dataset vimos que existía un fuerte desbalance de clase. Sòlo el 3% de las observaciones pertenecen a personas que defaultearon. Esto puede tener un efecto en las estimaciones del modelo y su clasificación final.

Existen dos maneras sencillas con las cuales podemos trabajar con una clase desbalanceada:

  • Sobre-sampleo (oversampling) de la clase minoritaria
  • Sub-sampleo (undersampling) de la clase mayoritaria

La función glm puede tomar como argumento una columna (weigths) de ponderadores para poder hacer esto. Podemos asignar pesos mayores a 1 a la clase minoritaria (oversampling) o menores a 1 a la clase mayoritaria (undersampling). En nuestro problema vamos a realizar un sobresampleo de la clase minoritaria.

# Creamos la columna de ponderadores
default <- default %>% mutate(wt= if_else(default==1,20,1))
# Creamos los modelos con la data 'balanceada'
balanced_models <- data_frame(logit_formulas) %>% # dataframe a partir del objeto formulas
  mutate(models = names(logit_formulas), # columna con los nombres de las formulas
         expression = paste(logit_formulas), # columna con las expresiones de las formulas
         mod = map(logit_formulas, ~glm(.,family = 'binomial', data = default, weights = wt))) #Pasamos la columna wt como ponderadores

Vemos las estimaciones de los parametros para el modelo completo. ¿Existen cambios?

Ahora veamos la evaluación de los modelos ¿Qué pasó con el porcentaje de deviance explicada? ¿Y con la nula?

Violin plots, Curvas ROC y AUCs

Realizamos los gráficos de violin, las curvas ROC y calculamos los AUC

[1] "AUC: Modelo completo 0.949423207532511"
[1] "AUC: Modelo malo 0.56572151061233"

¿Dónde se ven los cambios más notorios respecto a nuestros modelos anteriores que no tenían en cuenta el desbalance de la clase?

Punto de corte

Volvemos a realizar las pruebas para varios puntos de corte y graficamos el accuracy, la sensibilidad, la especificidad, el recall y la precision para cada uno de ellos.

¿Qué cambios vemos respecto al gráfico anterior?

Dataset de testing

Probamos en el dataset de testing nuestro modelo balanceado. No es necesario que le creemos pesos al dataset de testeo.

Confusion Matrix and Statistics

   
       0    1
  0 2287  617
  1    5   92
                                          
               Accuracy : 0.7927          
                 95% CI : (0.7778, 0.8071)
    No Information Rate : 0.7637          
    P-Value [Acc > NIR] : 8.196e-05       
                                          
                  Kappa : 0.1818          
 Mcnemar's Test P-Value : < 2.2e-16       
                                          
            Sensitivity : 0.12976         
            Specificity : 0.99782         
         Pos Pred Value : 0.94845         
         Neg Pred Value : 0.78753         
             Prevalence : 0.23625         
         Detection Rate : 0.03066         
   Detection Prevalence : 0.03232         
      Balanced Accuracy : 0.56379         
                                          
       'Positive' Class : 1               
                                          
LS0tCnRpdGxlOiAiUmVncmVzaW9uIExvZ2lzdGljYSIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOiAKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwphdXRob3I6ICJKdWFuIE1hbnVlbCBCYXJyaW9sYSB5IERpZWdvIEtvemxvd3NraSIKZGF0ZTogMjctMTAtMjAxOAotLS0KCiMgUHJpbWVyYSBwYXJ0ZQoKYGBge3IsIGVjaG89VFJVRSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShkcGx5cikKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KHRpZHlyKQpsaWJyYXJ5KGJyb29tKQpsaWJyYXJ5KElTTFIpCmxpYnJhcnkoR0dhbGx5KQpsaWJyYXJ5KG1vZGVscikKbGlicmFyeShwUk9DKQpsaWJyYXJ5KGNvd3Bsb3QpCmxpYnJhcnkoT25lUikKbGlicmFyeShybGFuZykKbGlicmFyeShwdXJycikKbGlicmFyeShjYXJldCkKc2V0LnNlZWQoMTk5MikKYGBgCgpMYSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EgZXMgw7p0aWwgcGFyYSBwcm9ibGVtYXMgZGUgcHJlZGljY2nDs24gZGUgY2xhc2VzLiBFbCBwcm9ibGVtYSBxdWUgdmFtb3MgYSB0cmF0YXIgZGUgcmVzb2x2ZXIgZXMgcHJlZGVjaXIgc2kgdW5hIHBlcnNvbmEgdmEgZGVmYXVsdGVhciBzdSBkZXVkYSBkZSB0YXJqZXRhIGRlIGNyw6lkaXRvIGVuIGJhc2UgYSBjaWVydG9zIHByZWRpY3RvcmVzLgoKIyMgQ29uanVudG8gZGUgZGF0b3MKCkVzdGUgY29uanVudG8gZGUgZGF0b3MgcHJvdmllbmUgZGUgbGEgbGlicmVyw61hIFtJU0xSXShodHRwOi8vd3d3LWJjZi51c2MuZWR1LyU3RWdhcmV0aC9JU0wvKSAgKEludHJvZHVjdGlvbiB0byBTdGF0aXN0aWNhbCBMZWFybmluZyBVc2luZyBSKSBkZSBKYW1lcywgV2l0dGVuLCBIYXN0aWUgeSBUaWJzaGlyYW5pLgoKYGBge3J9CmRlZmF1bHQgPC0gRGVmYXVsdApnbGltcHNlKGRlZmF1bHQpCmBgYAoKVGllbmUgNCB2YXJpYWJsZXM6IAoKKiAqKmRlZmF1bHQqKjogTGEgY2xhc2UgcXVlIHF1ZXJlbW9zIHByZWRlY2lyCiogKipzdHVkZW50Kio6IEJpbmFyaWEgcXVlIGluZGljYSBzaSBsYSBwZXJzb25hIGVzIGVzdHVkaWFudGUKKiAqKmJhbGFuY2UqKjogQmFsYW5jZSBwcm9tZWRpbyBxdWUgbGUgcXVlZGEgYSBsYSBwZXJzb25hIGx1ZWdvIGRlIHN1cyBwYWdvcyBtZW5zdWFsZXMKKiAqKmluY29tZSoqOiBJbmdyZXNvIGRlIGxhIHBlcnNvbmEKCiMjIEV4cGxvcmF0b3JpYXMKCkFuYWxpY2Vtb3MgbGEgZGlzdHJpYnVjacOzbiBkZSBsYSBjbGFzZQoKYGBge3J9CmRlZmF1bHQgJT4lIGdyb3VwX2J5KGRlZmF1bHQpICU+JSBzdW1tYXJpc2UobnVtZXJvX2Nhc29zPW4oKSkKYGBgCgpWZW1vcyBxdWUgZXN0YW1vcyB0cmFiYWphbmRvIGNvbiB1biBwcm9ibGVtYSBkZSBjbGFzaWZpY2FjacOzbiBjb24gdW4gY2xhcm8gZGVzYmFsYW5jZSBkZSBjbGFzZS4KClJlYWxpemFtb3MgdW4gZ3LDoWZpY28gZXhwbG9yYXRvcmlvIGNvbXBsZXRvIHBhcmEgdmVyIGVsIGNvbXBvcnRhbWllbnRvIHkgbGFzIHJlbGFjaW9uZXMgZW50cmUgbGFzIHZhcmlhYmxlcy4gRWwgY29sb3Igcm9qbyBkZXNpZ25hIGEgcXVpZW5lcyBubyBkZWZhdWx0ZWFuIHkgZWwgYXp1bCBhIGxvcyBxdWUgc8OtLgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CmdncGFpcnMoZGVmYXVsdCxtYXBwaW5nID0gYWVzKGNvbG91cj0gZGVmYXVsdCkpICsgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA5MCwgaGp1c3QgPSAxKSkgKyB0aGVtZV9idygpCmBgYAoKwr9RdcOpIHB1ZWRlbiBkZWNpciBkZSBsYSByZWxhY2nDs24gZW50cmUgYmFsYW5jZSB5IGRlZmF1bHQ/CgrCv1kgZW50cmUgaW5jb21lIHkgYmFsYW5jZT8KCsK/Q3XDoWxlcyBwYXJlY2VuIHNlciBidWVuYXMgdmFyaWFibGVzIHBhcmEgcHJlZGVjaXIgbGEgcHJvYmFiaWxpZGFkIGRlIGRlZmF1bHQgZGUgdW5hIHBlcnNvbmE/CgoKIyMjIExpbXBpZXphCgpQYXJhIG1vZGVsaXphciB2YSBhIHNlciBtw6FzIG5lY2VzYXJpbyB0ZW5lciBsYSB2YXJpYWJsZSBkZWZhdWx0IGNvbW8gbnVtw6lyaWNhLiAKRGVmaW5pbW9zIGxhIHZhcmlhYmxlIGNvbW8gezAsMX0gcGFyYSBsb3MgdmFsb3JlcyB7Ik5vIiwiWWVzIn0KCmBgYHtyfQpkZWZhdWx0IDwtIGRlZmF1bHQlPiUgbXV0YXRlKGRlZmF1bHQ9IGNhc2Vfd2hlbihkZWZhdWx0PT0iTm8ifjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRlZmF1bHQ9PSJZZXMifjEpKQpgYGAKCiMjIFByb2JsZW1hCgpRdWVyZW1vcyBlc3RpbWFyICRQKERlZmF1bHQ9WWVzfFgpPVAoWCkkIHBhcmEgY2FkYSBpbmRpdmlkdW8geSBwYXJ0aXIgZGUgZWxsbyBwb2RlciBkZWZpbmlyIHVuIHB1bnRvIGRlIGNvcnRlIHBhcmEgcHJlZGVjaXIgcXVpZW5lcyBzb24gbG9zIHF1ZSB2YW4gYSBlbnRyYXIgZW4gZGVmYXVsdC4KCiMjIyBSZWdyZXNpw7NuIGxpbmVhbAoKRW4gZXN0ZSBjYXNvIGVzdGFtb3MgbW9kZWxhbmRvIGxhIHByb2JhYmlsaWRhZCBkZSBsYSBzaWd1aWVudGUgbWFuZXJhOiAKCiRQKFgpPSBcYmV0YV8wICsgXHN1bVxsaW1pdHNfe2o9MX1ecCBcYmV0YV9qIFgkCgpWZWFtb3MgcXVlIHRhbiBidWVubyBlcyBlbCBtb2RlbG8gbGluZWFsIHBhcmEgZXN0bywgdXNhbmRvIGJhbGFuY2UgY29tbyBwcmVkaWN0b3IuCgpgYGB7cn0KdGVzdF9tY28gPC0gZGVmYXVsdCAlPiUgCiAgICAgICAgICAgICAgbG0oZm9ybXVsYSA9IGRlZmF1bHR+YmFsYW5jZSwgZGF0YSA9IC4pIAoKYGBgCgpgYGB7cn0KdGR5IDwtIHRlc3RfbWNvICU+JSB0aWR5KCkKdGR5CnRlc3RfbWNvICU+JSBnbGFuY2UoKQpgYGAKCkFtYm9zIGVzdGltYWRvcmVzIHNvbiBzaWduaWZpY2F0aXZvcyB5IGVsIHRlc3QgZGUgc2lnbmlmaWNhdGl2aWRhZCBnbG9iYWwgZGVsIG1vZGVsbyB0YW1iacOpbiBlcyBzaWduaWZpY2F0aXZvLgpWZWFtb3MgdW4gZ3LDoWZpY28gZGUgbnVlc3RybyBtb2RlbG8KCmBgYHtyLCBlY2hvPUZBTFNFfQpnZ3Bsb3QoZGVmYXVsdCwgYWVzKGJhbGFuY2UsIGRlZmF1bHQpKSArIAogIGdlb21fcG9pbnQoYWVzKGNvbG9yPWZhY3RvcihkZWZhdWx0KSkpICsKICBnZW9tX2FibGluZShpbnRlcmNlcHQgPSB0ZHkkZXN0aW1hdGVbMV0sIHNsb3BlID0gdGR5JGVzdGltYXRlWzJdLCBjb2xvcj0nZm9yZXN0Z3JlZW4nLCBzaXplPTIpICsgCiAgbGFicyh0aXRsZT0iTW9kZWxvIGxpbmVhbCBzaW1wbGUiLCBjb2xvcj0nQ2xhc2UnKSArCiAgbGltcyh5PWMoLTEsMikpKwogIHRoZW1lX2J3KCkKYGBgCgpQYXJlY2UgdGVuZXIgYmFzdGFudGVzIHByb2JsZW1hcyBwYXJhIGVzdGltYXIgbGEgcHJvYmFiaWxpZGFkIGRlIGRlZmF1bHQgZGUgbG9zIGluZGl2aWR1b3MuIFBvciBlamVtcGxvLCB2ZW1vcyBxdWUgaGF5IHZhcmlvcyBpbmRpdmlkdW9zIGEgbG9zIGN1YWxlcyBsZXMgYXNpZ25hIHVuYSBwcm9iYWJpbGlkYWQgbmVnYXRpdmEuCgojIyMgUmVncmVzacOzbiBsb2fDrXN0aWNhCgpQYXJhIGV2aXRhciBlc3RvcyBwcm9ibGVtYXMsIHVzYW1vcyBsYSAqKmZ1bmNpb24gbG9naXN0aWNhKioKCiRQKFgpPSBcZnJhY3tlXntcYmV0YV8wICsgXHN1bVxsaW1pdHNfe2o9MX1ecCBcYmV0YV9qIFh9fXsxK2Vee1xiZXRhXzAgKyBcc3VtXGxpbWl0c197aj0xfV5wIFxiZXRhX2ogWH19JAoKRWwgbGFkbyBkZXJlY2hvIHNlIGxsYW1hICoqZXhwaXQqKgoKRXN0YSBmdW5jaW9uIGFjb3RhIGVsIHJlc3VsdGFkbyBlbnRyZSAwIHkgMSwgbG8gY3VhbCBlcyBtdWNobyBtYXMgYWRlY3VhZG8gcGFyYSBtb2RlbGFyIHVuYSBwcm9iYWJpbGlkYWQuCgpMdWVnbyBkZSBoYWNlciBhbGd1bmFzIG9wZXJhY2lvbmVzLCBwb2RlbW9zIGxsZWdhciBhIGxhIGV4cHJlc2lvbjoKCiRcbG9nIHtcZnJhY3tQKHgpfXsxLVAoeCl9fT0gXGJldGFfMCArIFxzdW1cbGltaXRzX3tqPTF9XnAgXGJldGFfaiBYJAoKRWwgbGFkbyBpenF1aWVyZG8gZXMgZWwgbG9nYXJpdG1vIGRlIGxvcyAqKm9kZHMqKiB5IHNlIGxsYW1hICoqbG9naXQqKgoKIyMgUGFydGljacOzbiBUcmFpbiB5IFRlc3RpbmcKClJlYWxpemFtb3MgdW5hIHBhcnRpY2nDs24gZW50cmUgZGF0YXNldCBkZSBlbnRyZW5hbWllbnRvICg3MCUpIHkgdGVzdGVvICgzMCUpIHVzYW5kbyBsYSBmdW5jacOzbiBgcmVzYW1wbGVfcGFydGl0aW9uYCBkZWwgcGFxdWV0ZSAqKm1vZGVscioqCgpgYGB7cn0KdHJhaW5fdGVzdCA8LSBkZWZhdWx0ICU+JSByZXNhbXBsZV9wYXJ0aXRpb24oYyh0cmFpbj0wLjcsdGVzdD0wLjMpKQoKZGVmYXVsdCA8LSB0cmFpbl90ZXN0JHRyYWluICU+JSBhc190aWJibGUoKQp0ZXN0IDwtIHRyYWluX3Rlc3QkdGVzdCAlPiUgYXNfdGliYmxlKCkKYGBgCgoKIyMgQ3JlYWNpw7NuIGRlIGbDs3JtdWxhcwoKUGFyYSBhcGxpY2FyIGxhIHJlZ3Jlc2lvbiBsb2dpc3RpY2EgcHJpbWVybyB1c2Ftb3MgbGEgZnVuY2lvbiBgZm9ybXVsYXNgIGRlbCBwYXF1ZXRlICoqbW9kZWxyKiogcGFyYSBjcmVhciB1biBvYmpldG8gcXVlIGNvbnRpZW5lIHRvZGFzIGxhcyBmb3JtdWxhcyBxdWUgdmFtb3MgYSB1dGlsaXphci4KCmBgYHtyfQpsb2dpdF9mb3JtdWxhcyA8LSBmb3JtdWxhcygucmVzcG9uc2UgPSB+ZGVmYXVsdCwgIyDDum5pY28gbGFkbyBkZXJlY2hvIGRlIGxhcyBmb3JtdWxhcy4KICAgICAgICAgICAgICAgICAgICAgICAgIGJhbD0gfmJhbGFuY2UsIAogICAgICAgICAgICAgICAgICAgICAgICAgc3R1ZD0gfnN0dWRlbnQsICAKICAgICAgICAgICAgICAgICAgICAgICAgIGluYz0gfmluY29tZSwgIAogICAgICAgICAgICAgICAgICAgICAgICAgYmFsX3N0dWQ9fmJhbGFuY2Urc3R1ZGVudCwgCiAgICAgICAgICAgICAgICAgICAgICAgICBiYWxfaW5jPX5iYWxhbmNlK2luY29tZSwgCiAgICAgICAgICAgICAgICAgICAgICAgICBzdHVkX2luYz1+c3R1ZGVudCtpbmNvbWUsICAKICAgICAgICAgICAgICAgICAgICAgICAgIGZ1bGw9IH5iYWxhbmNlICsgaW5jb21lICsgc3R1ZGVudCAgCiAgICAgICAgICAgICAgICAgICAgICAgICApCmBgYAoKIyMgQ3JlYWNpw7NuIGRlIG1vZGVsb3MKClByb2NlZGVtb3MgYSBjcmVhciBsb3MgbW9kZWxvcyBhIHBhcnRpciBkZSBlc3RhcyBmw7NybXVsYXMKCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQptb2RlbHMgPC0gZGF0YV9mcmFtZShsb2dpdF9mb3JtdWxhcykgJT4lICMgZGF0YWZyYW1lIGEgcGFydGlyIGRlbCBvYmpldG8gZm9ybXVsYXMKICBtdXRhdGUobW9kZWxzID0gbmFtZXMobG9naXRfZm9ybXVsYXMpLCAjIGNvbHVtbmEgY29uIGxvcyBub21icmVzIGRlIGxhcyBmb3JtdWxhcwogICAgICAgICBleHByZXNzaW9uID0gcGFzdGUobG9naXRfZm9ybXVsYXMpLCAjIGNvbHVtbmEgY29uIGxhcyBleHByZXNpb25lcyBkZSBsYXMgZm9ybXVsYXMKICAgICAgICAgbW9kID0gbWFwKGxvZ2l0X2Zvcm11bGFzLCB+Z2xtKC4sZmFtaWx5ID0gJ2Jpbm9taWFsJywgZGF0YSA9IGRlZmF1bHQpKSkgIyBRdWUgZXN0YW1vcyBoYWNpZW5kbyBhY8OhPyBRdWUgdmFtb3MgYSBlbmNvbnRyYXIgZW4gbGEgY29sdW1uYT8KYGBgCgojIyMgTW9kZWxvcyBzaW1wbGVzCgpQcm9iYW1vcyBsb3MgcHJpbWVyb3MgdHJlcyBtb2RlbG9zLCBhcXVlbGxvcyBxdWUgdGllbmVuIHVuIMO6bmljbyBwcmVkaWN0b3IuIFVzYW1vcyBsYSBmdW5jacOzbiBfdGlkeV8gcGFyYSBvYnRlbmVyIGxvcyBwYXLDoW1ldHJvcyBlc3RpbWFkb3MgcGFyYSBlc3RvcyB0cmVzIG1vZGVsb3MuCgpgYGB7ciwgd2FybmluZz1GQUxTRX0KbW9kZWxzICU+JSAKICBmaWx0ZXIobW9kZWxzICVpbiUgYygnYmFsJywnc3R1ZCcsJ2luYycpKSAlPiUKICBtdXRhdGUodGlkeSA9IG1hcChtb2QsdGlkeSkpICU+JSAgIyBRdcOpIHJlYWxpemFtb3MgZW4gZXN0ZSBwYXNvPyBRdWUgdmEgYSB0ZW5lciBlc3RhIGNvbHVtbmE/CiAgdW5uZXN0KHRpZHksIC5kcm9wID0gVFJVRSkgJT4lIAogIG11dGF0ZShlc3RpbWF0ZT1yb3VuZChlc3RpbWF0ZSw1KSwKICAgICAgICAgcC52YWx1ZT1yb3VuZChwLnZhbHVlLDQpKQpgYGAKCsK/U29uIHNpZ25pZmljYXRpdm9zPwoKwr9RdcOpIGludGVycHJldGFjacOzbiBwdWVkZW4gZGFybGUgYSBlc3RvcyB2YWxvcmVzPwoKIyMjIE1vZGVsbyBjb21wbGV0bwoKQWhvcmEgcHJvYmFtb3MgY29uIHVuIG1vZGVsbyBxdWUgdXRpbGl6YSBsYXMgdHJlcyBwcmVkaWN0b3JhcwoKYGBge3IsICB3YXJuaW5nPUZBTFNFfQptb2RlbHMgJT4lIAogIGZpbHRlcihtb2RlbHMgPT0gImZ1bGwiKSAlPiUKICBtdXRhdGUodGlkeSA9IG1hcChtb2QsdGlkeSkpICU+JQogIHVubmVzdCh0aWR5LCAuZHJvcCA9IFRSVUUpICU+JSAKICBtdXRhdGUoZXN0aW1hdGU9cm91bmQoZXN0aW1hdGUsNSksCiAgICAgICAgIHAudmFsdWU9cm91bmQocC52YWx1ZSw0KSkKYGBgCgrCv1F1ZSBjYW1iaW9zIGhheSByZXNwZWN0byBhIGxvcyAzIG1vZGVsb3MgaW5kaXZpZHVhbGVzIHByZXZpb3M/CgojIyMgRXZhbHVhY2nDs24gZGUgdG9kb3MgbG9zIG1vZGVsb3MKCkNvbiBgbWFwKClgIGFncmVnYW1vcyBsYSBmdW5jacOzbiBgZ2xhbmNlYCBwYXJhIHRyYWVybm9zIGluZm9ybWFjacOzbiByZWxldmFudGUgcGFyYSBlbCBkaWFnbsOzc3RpY28gZGVsIG1vZGVsby4KCkNvbiBgdW5uZXN0KClgIHBvZGVtb3MgdmVyIGxhIGV2YWx1YWNpw7NuIGRlIGNhZGEgbW9kZWxvLiBQb3Igw7psdGltbyBvcmRlbmFtb3MgbGFzIG1vZGVsb3MgcG9yIGVsIGRldmlhbmNlLgoKYGBge3IsIHdhcm5pbmc9RkFMU0V9Cm1vZGVscyA8LSBtb2RlbHMgJT4lIAogIG11dGF0ZShnbGFuY2UgPSBtYXAobW9kLGdsYW5jZSkpCgptb2RlbHMgJT4lIAogIHVubmVzdChnbGFuY2UsIC5kcm9wID0gVFJVRSkgJT4lCiAgbXV0YXRlKHBlcmNfZXhwbGFpbmVkX2RldiA9IDEtZGV2aWFuY2UvbnVsbC5kZXZpYW5jZSkgJT4lIAogIHNlbGVjdCgtYyhtb2RlbHMsIGRmLm51bGwsIEFJQywgQklDKSkgJT4lIAogIGFycmFuZ2UoZGV2aWFuY2UpCmBgYAoKCkVsIG1vZGVsbyBxdWUgdXRpbGl6YSBsYXMgMyB2YXJpYWJsZXMgZXMgZWwgcXVlIG1pbmltaXphIGVsIGRldmlhbmNlLiAKTG9zIDMgw7psdGltb3MgbW9kZWxvcyByZWR1Y2VuIG11eSBwb2NvIGVsIGRldmlhbmNlIHJlc3BlY3RvIGEgbGEgZGV2aWFuY2UgbnVsYS4KCiMjIyBHcsOhZmljb3MgZGUgZXZhbHVhY2nDs24KClJlYWxpemFtb3MgbG9zIGdyw6FmaWNvcyBwYXJhIGVsIG1vZGVsbyBjb21wbGV0byB5IHVubyBkZSBsb3MgbW9kZWxvcyBjb24gbWF5b3IgZGV2aWFuY2UgKHN0dWRlbnQraW5jb21lKS4KCkVuIGVzdGUgY2FzbyBlc3RhbW9zOgoKMSkgQWdyZWdhbmRvIGxhcyBwcmVkaWNjaW9uZXMgY29uIGBhdWdtZW50YCBjb24gZWwgcGFyw6FtZXRybyB0eXBlPSJyZXNwb25zZSIgwr9Qb3IgcXXDqSBoYWNlbW9zIGVzdG8/IMK/Q3XDoWwgZXMgZWwgdmFsb3IgcG9yIGRlZmF1bHQgZGUgZXN0ZSBwYXLDoW1ldHJvPyAKMikgQXJtYW5kbyBsYXMgY3VydmFzIFJPQyBjb24gYHBST0NgCgoKYGBge3IsIHdhcm5pbmc9RkFMU0V9Cm1vZGVscyA8LSBtb2RlbHMgJT4lIAogIG11dGF0ZShwcmVkPSBtYXAobW9kLGF1Z21lbnQsIHR5cGUucHJlZGljdCA9ICJyZXNwb25zZSIpKQoKbW9kZWxzJHByZWRbMV0KYGBgCgoKYGBge3J9CnByZWRpY3Rpb25fZnVsbCA8LSBtb2RlbHMgJT4lIAogIGZpbHRlcihtb2RlbHM9PSJmdWxsIikgJT4lIAogIHVubmVzdChwcmVkLCAuZHJvcD1UUlVFKQpyb2NfZnVsbCA8LSByb2MocmVzcG9uc2U9cHJlZGljdGlvbl9mdWxsJGRlZmF1bHQsIHByZWRpY3Rvcj1wcmVkaWN0aW9uX2Z1bGwkLmZpdHRlZCkKCnByZWRpY3Rpb25fYmFkIDwtIG1vZGVscyAlPiUgCiAgZmlsdGVyKG1vZGVscz09InN0dWRfaW5jIikgJT4lIAogIHVubmVzdChwcmVkLCAuZHJvcD1UUlVFKQoKcm9jX2JhZCA8LSByb2MocmVzcG9uc2U9cHJlZGljdGlvbl9iYWQkZGVmYXVsdCwgcHJlZGljdG9yPXByZWRpY3Rpb25fYmFkJC5maXR0ZWQpCgpgYGAKCiMjIyMgVmlvbGluIHBsb3RzCgpgYGB7cn0KdmlvbGluX2Z1bGw9Z2dwbG90KHByZWRpY3Rpb25fZnVsbCwgYWVzKHg9ZGVmYXVsdCwgeT0uZml0dGVkLCBncm91cD1kZWZhdWx0LGZpbGw9ZmFjdG9yKGRlZmF1bHQpKSkgKyAKICBnZW9tX3Zpb2xpbigpICsKICB0aGVtZV9idygpICsKICBndWlkZXMoZmlsbD1GQUxTRSkgKwogIGxhYnModGl0bGU9J1Zpb2xpbiBwbG90Jywgc3VidGl0bGU9J01vZGVsbyBjb21wbGV0bycsIHk9J1ByZWRpY3RlZCBwcm9iYWJpbGl0eScpCgp2aW9saW5fYmFkPWdncGxvdChwcmVkaWN0aW9uX2JhZCwgYWVzKHg9ZGVmYXVsdCwgeT0uZml0dGVkLCBncm91cD1kZWZhdWx0LCBmaWxsPWZhY3RvcihkZWZhdWx0KSkpICsgCiAgZ2VvbV92aW9saW4oKSArIAogIHRoZW1lX2J3KCkgKwogIGd1aWRlcyhmaWxsPUZBTFNFKSArCiAgbGFicyh0aXRsZT0nVmlvbGluIHBsb3QnLCBzdWJ0aXRsZT0nTW9kZWxvIG1hbG8nLCB5PSdQcmVkaWN0ZWQgcHJvYmFiaWxpdHknKQoKcGxvdF9ncmlkKHZpb2xpbl9iYWQsIHZpb2xpbl9mdWxsKQpgYGAKCsK/UXXDqSBlcyBsbyBxdWUgZXN0YW1vcyB2aWVuZG8gZW4gZWxsb3M/IChFc3BlY2lhbCBhdGVuY2nDs24gYWwgZWplIGRlIG9yZGVuYWRhcykKCsK/Q3XDoWwgcGFyZWNlIHNlciB1biBwdW50byBkZSBjb3J0ZSBhZGVjdWFkbyBwYXJhIGNhZGEgbW9kZWxvPwoKIyMjIyBDdXJ2YXMgUk9DCgpgYGB7cn0KZ2dyb2MobGlzdChmdWxsPXJvY19mdWxsLCBiYWQ9cm9jX2JhZCksIHNpemU9MSkgKyBnZW9tX2FibGluZShzbG9wZSA9IDEsIGludGVyY2VwdCA9IDEsIGxpbmV0eXBlPSdkYXNoZWQnKSArIHRoZW1lX2J3KCkgKyBsYWJzKHRpdGxlPSdDdXJ2YXMgUk9DJywgY29sb3I9J01vZGVsbycpCgpwcmludChwYXN0ZSgnQVVDOiBNb2RlbG8gY29tcGxldG8nLCByb2NfZnVsbCRhdWMpKQoKcHJpbnQocGFzdGUoJ0FVQzogTW9kZWxvIG1hbG8nLCByb2NfYmFkJGF1YykpCgpgYGAKCsK/UXXDqSBzaWduaWZpY2EgY2FkYSB1bm8gZGUgbG9zIGVqZXM/CgojIyMjIEdyw6FmaWNvIGRlIEhvc21lci1MZW1lc2hvdwoKYGBge3IsIG1lc3NhZ2U9RkFMU0V9CgpIb3NtZXJfTGVtZXNob3dfcGxvdCA8LSBmdW5jdGlvbihkYXRhc2V0LCBwcmVkaWN0ZWRfY29sdW1uLCBjbGFzc19jb2x1bW4sIGJpbnMsIHBvc2l0aXZlX3ZhbHVlLCBjb2xvcj0nZm9yZXN0Z3JlZW4nLCBudWRnZV94PTAsIG51ZGdlX3k9MC4wNSl7CiAgIlJlYWxpemEgdW4gZ3JhZmljbyBkZSBIb3NtZXItTGVtZXNob3cgcGFyYSB1biBkYXRhc2V0IgogIAogICIqIGRhdGFzZXQ6IGNvbmp1bnRvIGRlIGRhdG9zCiAgICogcHJlZGljdGVkX2NvbHVtbjogY29sdW1uYSBjb24gbGEgcHJvYmFiaWxpZGFkIHByZWRpY2hhCiAgICogY2xhc3NfY29sdW1uOiBjb2x1bW5hIGNvbiBsYSBjbGFzZSBhIHByZWRlY2lyCiAgICogcG9zc2l0aXZlX3ZhbHVlOiB2YWxvciBkZSBsYSBjbGFzZSBhIHByZWRlY2lyCiAgICogYmluczogY2FudGlkYWQgZGUgZ3J1cG9zIGRlbCBncsOhZmljbwogICAqIGNvbG9yOiBjb2xvciBkZSBsb3MgcHVudG9zCiAgICogbnVkZ2VfeDogZGVzcGxhemFtaWVudG8gZGUgbGEgZXRpcXVldGEgZW4gZWwgZWplIHgKICAgKiBudWRnZV95OiBkZXNwbGF6YW1pZW50byBkZSBsYSBldGlxdWV0YSBlbiBlbCBlamUgeSIKICAKICAjIEFzaWduYXIgbG9zIGdydXBvcyBhIGxhcyBvYnNlcnZhY2lvbmVzIGRlIGFjdWVyZG8gYSBsYSBwcm9iYWJpbGlkYWQgcHJlZGljaGEKICBkYXRhc2V0Wydncm91cCddIDwtIGJpbihkYXRhc2V0W3ByZWRpY3RlZF9jb2x1bW5dLCBuYmlucyA9IGJpbnMsIG1ldGhvZCA9ICdsJywgbGFiZWxzPWMoMTpiaW5zKSkKICAKICAjIENvbnRhciBsYSBjYW50aWRhZCBkZSBjYXNvcyBwb3NpdGl2b3MgcG9yIGdydXBvCiAgcG9zaXRpdmVfY2xhc3MgPC0gZGF0YXNldCAlPiUgZmlsdGVyKCEhc3ltKGNsYXNzX2NvbHVtbik9PXBvc2l0aXZlX3ZhbHVlKSAlPiUgZ3JvdXBfYnkoZ3JvdXApICU+JSBjb3VudCgpCiAgCiAgIyBPYnRlbmVyIGxhIG1lZGlhIGRlIGxhcyBwcmVkaWNjaW9uZXMgcG9yIGdydXBvCiAgSExfZGYgPC0gZGF0YXNldCAlPiUgZ3JvdXBfYnkoZ3JvdXApICU+JSBzdW1tYXJpc2UocHJlZD1tZWFuKCEhc3ltKHByZWRpY3RlZF9jb2x1bW4pKSwgY291bnQ9bigpKSAlPiUKICAgICAgICAgICAgaW5uZXJfam9pbiguLHBvc2l0aXZlX2NsYXNzKSAlPiUKICAgICAgICAgICAgbXV0YXRlKGZyZXE9bi9jb3VudCkKICAKICAjIEdyw6FmaWNvIAogIEhNX3Bsb3QgPC0gZ2dwbG90KEhMX2RmLCBhZXMoeD1wcmVkLCB5PWZyZXEpKSArIGdlb21fcG9pbnQoYWVzKHNpemU9biksIGNvbG9yPWNvbG9yKSArCiAgICAgICAgICAgICAgICBnZW9tX3RleHQoYWVzKGxhYmVsPW4pLG51ZGdlX3kgPSBudWRnZV95KSsKICAgICAgICAgICAgICAgIGdlb21fYWJsaW5lKHNsb3BlID0gMSwgaW50ZXJjZXB0ID0gMCwgbGluZXR5cGU9J2Rhc2hlZCcpICsgCiAgICAgICAgICAgICAgICB0aGVtZV9idygpICsKICAgICAgICAgICAgICAgIGxhYnModGl0bGU9J0hvc21lci1MZW1lc2hvdycsIHNpemU9J0Nhc29zJywgeD0iUHJvYmFiaWxpZGFkIFByZWRpY2hhIiwgeT0iRnJlY3VlbmNpYSBvYnNlcnZhZGEiKQogIHJldHVybihITV9wbG90KQp9CgpIb3NtZXJfTGVtZXNob3dfcGxvdChwcmVkaWN0aW9uX2Z1bGwsICcuZml0dGVkJywgJ2RlZmF1bHQnLCAxMCwgMSkgKyBsYWJzKHN1YnRpdGxlPSJNb2RlbG8gY29tcGxldG8iKQoKSG9zbWVyX0xlbWVzaG93X3Bsb3QocHJlZGljdGlvbl9iYWQsICcuZml0dGVkJywgJ2RlZmF1bHQnLCAxMCwgMSwgY29sb3IgPSAiZmlyZWJyaWNrIiwgbnVkZ2VfeSA9IDAuMDAzKSArIHNjYWxlX3hfY29udGludW91cyhsaW1pdHMgPSBjKDAuMDIsLjA2KSkgKyBzY2FsZV95X2NvbnRpbnVvdXMobGltaXRzID0gYyguMDIsLjA2KSkgKyBsYWJzKHN1YnRpdGxlPSJNb2RlbG8gbWFsbyIpCgpgYGAKCsK/UXXDqSB2ZW1vcyBlbiBlc3RvcyBncsOhZmljb3M/CgrCv1BhcmEgcXXDqSB2YWxvcmVzIHBhcmVjZSBleGlzdGlyIHVuYSBzb2JyZWVzdGltYWNpw7NuIGRlIGxhIHByb2JhYmlsaWRhZD8gwr9QYXJhIGN1w6FsZXMgc3ViZXN0aW1hY2nDs24/CgojIyMgUHVudG8gZGUgY29ydGUKCkhhc3RhIGFob3JhIGhlbW9zIGV2YWx1YWRvIGVsIG1vZGVsbyBkZSBtYW5lcmEgZ2VuZXJhbCwgcGVybyBlbCByZXN1bHRhZG8gZmluYWwgZGVsIG1vZGVsbyBkZWJlIGNvbnNpc3RpciBlbiBhc2lnbmFyIGFsIGluZGl2aWR1byB1bmEgY2xhc2UgcHJlZGljaGEuIEVuIG51ZXN0cm8gY2FzbyBkZWJlbW9zIGVzdGFibGVjZXIgdW4gcHVudG8gZGUgY29ydGUgc2Vnw7puIGVsIGN1YWwgdmFtb3MgYSBzZXBhcmFyIGEgbG9zIGluZGl2dW9zIGVuIHF1aWVuZXMgZGVmYXVsdGVhbiB5IHF1aWVuZXMgbm8uCgpQcm9iYW1vcyB2YXJpb3MgcHVudG9zIGRlIGNvcnRlIHkgZ3JhZmljYW1vcyBlbCBhY2N1cmFjeSwgbGEgc2Vuc2liaWxpZGFkLCBsYSBlc3BlY2lmaWNpZGFkLCBlbCByZWNhbGwgeSBsYSBwcmVjaXNpb24gcGFyYSBjYWRhIHVubyBkZSBlbGxvcy4KCnwgQ2xhc2VzIHByZWRpY2hhcyAvIENsYXNlcyB8IFBvc2l0aXZhIHwgTmVnYXRpdmEgIHwKfC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tfC0tLS0tLS0tLXwtLS0tLS0tLS0tfAp8IFBvc2l0aXZhICAgICAgICAgICAgICAgICB8IFRydWUgUG9zIHwgRmFsc2UgUG9zIHwKfCBOZWdhdGl2YSAgICAgICAgICAgICAgICAgfCBGYWxzZSBOZWcgfCBUcnVlIE5lZyB8CgpSZWNvcmRlbW9zIHF1ZToKCiRzZW5zaXRpdml0eSA9IHJlY2FsbCA9IFxmcmFje1RQfXtUUCtGTn0kCgokc3BlY2lmaWNpdHkgPSBcZnJhY3tUTn17VE4rRlB9JAoKJHByZWNpc2lvbiA9IFxmcmFje1RQfXtUUCtGUH0kCgoKYGBge3J9CgpwcmVkaWN0aW9uX21ldHJpY3MgPC0gZnVuY3Rpb24oY3V0b2ZmLCBwcmVkaWN0aW9ucz1wcmVkaWN0aW9uX2Z1bGwpewogIHRhYmxlIDwtIHByZWRpY3Rpb25zICU+JSAKICAgIG11dGF0ZShwcmVkaWN0ZWRfY2xhc3M9aWZfZWxzZSguZml0dGVkPmN1dG9mZiwgMSwgMCkgJT4lIGFzLmZhY3RvcigpLAogICAgICAgICAgIGRlZmF1bHQ9IGZhY3RvcihkZWZhdWx0KSkKICAKICBjb25mdXNpb25NYXRyaXgodGFibGUodGFibGUkcHJlZGljdGVkX2NsYXNzLCB0YWJsZSRkZWZhdWx0KSwgcG9zaXRpdmUgPSAiMSIpICU+JQogICAgdGlkeSgpICU+JQogICAgc2VsZWN0KHRlcm0sIGVzdGltYXRlKSAlPiUKICAgIGZpbHRlcih0ZXJtICVpbiUgYygnYWNjdXJhY3knLCAnc2Vuc2l0aXZpdHknLCAnc3BlY2lmaWNpdHknLCAncHJlY2lzaW9uJywncmVjYWxsJykpICU+JQogICAgbXV0YXRlKGN1dG9mZj1jdXRvZmYpCiAgCn0KCmN1dG9mZnMgPSBzZXEoMC4wMSwwLjk1LDAuMDEpCmxvZ2l0X3ByZWQ9IG1hcF9kZnIoY3V0b2ZmcywgcHJlZGljdGlvbl9tZXRyaWNzKSU+JSBtdXRhdGUodGVybT1hcy5mYWN0b3IodGVybSkpCgpnZ3Bsb3QobG9naXRfcHJlZCwgYWVzKGN1dG9mZixlc3RpbWF0ZSwgZ3JvdXA9dGVybSwgY29sb3I9dGVybSkpICsgZ2VvbV9saW5lKHNpemU9MSkgKwogIHRoZW1lX2J3KCkgKwogIGxhYnModGl0bGU9ICdBY2N1cmFjeSwgU2Vuc2l0aXZpdHksIFNwZWNpZmljaXR5LCBSZWNhbGwgeSBQcmVjaXNpb24nLCBzdWJ0aXRsZT0gJ01vZGVsbyBjb21wbGV0bycsIGNvbG9yPSIiKQpgYGAKCsK/UXXDqSBwb2RlbW9zIG9ic2VydmFyIGVuIGVsIGdyw6FmaWNvPwoKwr9Qb2RlbW9zIGRlZmluaXIgdW4gYnVlbiBwdW50byBkZSBjb3J0ZT8gwr9DdcOhbCBzZXLDrWE/CgrCv1BvciBxdcOpIGxhIGVzcGVjaWZpY2lkYWQgdGllbmUgZXNlIGNvbXBvcnRhbWllbnRvPwoKIyMjIERhdGFzZXQgZGUgdGVzdGluZwoKU2VsZWNjaW9uYW1vcyBlbCBtb2RlbG8gY29tcGxldG8sIHlhIHF1ZSBlcyBlbCBxdWUgbWF4aW1pemFiYSBlbCBwb3JjZW50YWplIGRlIGRldmlhbmNlIGV4cGxpY2FkYSB5IGVuIGJhc2UgYSBsbyBxdWUgdmltb3MgZGVmaW5pbW9zIHVuIHB1bnRvIGRlIGNvcnRlIGVuIDAuMjUgKHB1ZWRlbiBwcm9iYXIgb3Ryb3MpCgpgYGB7cn0Kc2VsX2N1dG9mZiA9IDAuMjUKIyBDcmVhbW9zIGVsIG1vZGVsbwpmdWxsX21vZGVsIDwtIGdsbShsb2dpdF9mb3JtdWxhcyRmdWxsLCBmYW1pbHkgPSAnYmlub21pYWwnLCBkYXRhID0gZGVmYXVsdCkKIyBBZ3JlZ2Ftb3MgbGEgcHJlZGljY2lvbmVzIGFsIGRhdGFzZXQgZGUgdGVzdGVvCnRhYmxlPSBhdWdtZW50KHg9ZnVsbF9tb2RlbCwgbmV3ZGF0YT10ZXN0LCB0eXBlLnByZWRpY3Q9J3Jlc3BvbnNlJykgCiMgQ2xhc2lmaWNhbW9zIHV0aWxpemFtb3MgZWwgcHVudG8gZGUgY29ydGUKdGFibGU9dGFibGUgJT4lIG11dGF0ZShwcmVkaWN0ZWRfY2xhc3M9aWZfZWxzZSguZml0dGVkPjAuMjUsIDEsIDApICU+JSBhcy5mYWN0b3IoKSwKICAgICAgICAgICBkZWZhdWx0PSBmYWN0b3IoZGVmYXVsdCkpCiMgQ3JlYW1vcyBsYSBtYXRyaXogZGUgY29uZnVzacOzbgpjb25mdXNpb25NYXRyaXgodGFibGUodGFibGUkZGVmYXVsdCwgdGFibGUkcHJlZGljdGVkX2NsYXNzKSwgcG9zaXRpdmUgPSAiMSIpCmBgYAoKIyBTZWd1bmRhIHBhcnRlCgojIyBEZXNiYWxhbmNlbyBkZSBsYSBjbGFzZQoKQWwgZXhwbG9yYXIgZWwgZGF0YXNldCB2aW1vcyBxdWUgZXhpc3TDrWEgdW4gZnVlcnRlIGRlc2JhbGFuY2UgZGUgY2xhc2UuIFPDsmxvIGVsIDMlIGRlIGxhcyBvYnNlcnZhY2lvbmVzIHBlcnRlbmVjZW4gYSBwZXJzb25hcyBxdWUgZGVmYXVsdGVhcm9uLiBFc3RvIHB1ZWRlIHRlbmVyIHVuIGVmZWN0byBlbiBsYXMgZXN0aW1hY2lvbmVzIGRlbCBtb2RlbG8geSBzdSBjbGFzaWZpY2FjacOzbiBmaW5hbC4KCkV4aXN0ZW4gZG9zIG1hbmVyYXMgc2VuY2lsbGFzIGNvbiBsYXMgY3VhbGVzIHBvZGVtb3MgdHJhYmFqYXIgY29uIHVuYSBjbGFzZSBkZXNiYWxhbmNlYWRhOgoKICAqIFNvYnJlLXNhbXBsZW8gKG92ZXJzYW1wbGluZykgZGUgbGEgY2xhc2UgbWlub3JpdGFyaWEKICAqIFN1Yi1zYW1wbGVvICh1bmRlcnNhbXBsaW5nKSBkZSBsYSBjbGFzZSBtYXlvcml0YXJpYQogIApMYSBmdW5jacOzbiBgZ2xtYCBwdWVkZSB0b21hciBjb21vIGFyZ3VtZW50byB1bmEgY29sdW1uYSAoYHdlaWd0aHNgKSBkZSBwb25kZXJhZG9yZXMgcGFyYSBwb2RlciBoYWNlciBlc3RvLiBQb2RlbW9zIGFzaWduYXIgcGVzb3MgbWF5b3JlcyBhIDEgYSBsYSBjbGFzZSBtaW5vcml0YXJpYSAob3ZlcnNhbXBsaW5nKSBvIG1lbm9yZXMgYSAxIGEgbGEgY2xhc2UgbWF5b3JpdGFyaWEgKHVuZGVyc2FtcGxpbmcpLiBFbiBudWVzdHJvIHByb2JsZW1hIHZhbW9zIGEgcmVhbGl6YXIgdW4gc29icmVzYW1wbGVvIGRlIGxhIGNsYXNlIG1pbm9yaXRhcmlhLgoKYGBge3IsIHdhcm5pbmc9RkFMU0V9CiMgQ3JlYW1vcyBsYSBjb2x1bW5hIGRlIHBvbmRlcmFkb3JlcwpkZWZhdWx0IDwtIGRlZmF1bHQgJT4lIG11dGF0ZSh3dD0gaWZfZWxzZShkZWZhdWx0PT0xLDIwLDEpKQoKIyBDcmVhbW9zIGxvcyBtb2RlbG9zIGNvbiBsYSBkYXRhICdiYWxhbmNlYWRhJwpiYWxhbmNlZF9tb2RlbHMgPC0gZGF0YV9mcmFtZShsb2dpdF9mb3JtdWxhcykgJT4lICMgZGF0YWZyYW1lIGEgcGFydGlyIGRlbCBvYmpldG8gZm9ybXVsYXMKICBtdXRhdGUobW9kZWxzID0gbmFtZXMobG9naXRfZm9ybXVsYXMpLCAjIGNvbHVtbmEgY29uIGxvcyBub21icmVzIGRlIGxhcyBmb3JtdWxhcwogICAgICAgICBleHByZXNzaW9uID0gcGFzdGUobG9naXRfZm9ybXVsYXMpLCAjIGNvbHVtbmEgY29uIGxhcyBleHByZXNpb25lcyBkZSBsYXMgZm9ybXVsYXMKICAgICAgICAgbW9kID0gbWFwKGxvZ2l0X2Zvcm11bGFzLCB+Z2xtKC4sZmFtaWx5ID0gJ2Jpbm9taWFsJywgZGF0YSA9IGRlZmF1bHQsIHdlaWdodHMgPSB3dCkpKSAjUGFzYW1vcyBsYSBjb2x1bW5hIHd0IGNvbW8gcG9uZGVyYWRvcmVzCmBgYAoKVmVtb3MgbGFzIGVzdGltYWNpb25lcyBkZSBsb3MgcGFyYW1ldHJvcyBwYXJhIGVsIG1vZGVsbyBjb21wbGV0by4gwr9FeGlzdGVuIGNhbWJpb3M/CgpgYGB7ciwgIHdhcm5pbmc9RkFMU0UsIGVjaG89RkFMU0V9CgpiYWxhbmNlZF9tb2RlbHMgJT4lIAogIGZpbHRlcihtb2RlbHMgPT0gImZ1bGwiKSAlPiUKICBtdXRhdGUodGlkeSA9IG1hcChtb2QsdGlkeSkpICU+JQogIHVubmVzdCh0aWR5LCAuZHJvcCA9IFRSVUUpICU+JSAKICBtdXRhdGUoZXN0aW1hdGU9cm91bmQoZXN0aW1hdGUsNSksCiAgICAgICAgIHAudmFsdWU9cm91bmQocC52YWx1ZSw0KSkKYGBgCgpBaG9yYSB2ZWFtb3MgbGEgZXZhbHVhY2nDs24gZGUgbG9zIG1vZGVsb3Mgwr9RdcOpIHBhc8OzIGNvbiBlbCBwb3JjZW50YWplIGRlIGRldmlhbmNlIGV4cGxpY2FkYT8gwr9ZIGNvbiBsYSBudWxhPwoKYGBge3IsIGVjaG89RkFMU0V9CmJhbGFuY2VkX21vZGVscyA8LSBiYWxhbmNlZF9tb2RlbHMgJT4lIAogIG11dGF0ZShnbGFuY2UgPSBtYXAobW9kLGdsYW5jZSkpCgpiYWxhbmNlZF9tb2RlbHMgJT4lIAogIHVubmVzdChnbGFuY2UsIC5kcm9wID0gVFJVRSkgJT4lCiAgbXV0YXRlKHBlcmNfZXhwbGFpbmVkX2RldiA9IDEtZGV2aWFuY2UvbnVsbC5kZXZpYW5jZSkgJT4lIAogIHNlbGVjdCgtYyhtb2RlbHMsIGRmLm51bGwsIEFJQywgQklDKSkgJT4lIAogIGFycmFuZ2UoZGV2aWFuY2UpCmBgYAoKYGBge3IsIGVjaG89RkFMU0V9CmJhbGFuY2VkX21vZGVscyA8LSBiYWxhbmNlZF9tb2RlbHMgJT4lIAogIG11dGF0ZShwcmVkPSBtYXAobW9kLGF1Z21lbnQsIHR5cGUucHJlZGljdCA9ICJyZXNwb25zZSIpKQoKCnByZWRpY3Rpb25fZnVsbCA8LSBiYWxhbmNlZF9tb2RlbHMgJT4lIAogIGZpbHRlcihtb2RlbHM9PSJmdWxsIikgJT4lIAogIHVubmVzdChwcmVkLCAuZHJvcD1UUlVFKQpyb2NfZnVsbCA8LSByb2MocmVzcG9uc2U9cHJlZGljdGlvbl9mdWxsJGRlZmF1bHQsIHByZWRpY3Rvcj1wcmVkaWN0aW9uX2Z1bGwkLmZpdHRlZCkKCgpwcmVkaWN0aW9uX2JhZCA8LSBiYWxhbmNlZF9tb2RlbHMgJT4lIAogIGZpbHRlcihtb2RlbHM9PSJzdHVkX2luYyIpICU+JSAKICB1bm5lc3QocHJlZCwgLmRyb3A9VFJVRSkKCnJvY19iYWQgPC0gcm9jKHJlc3BvbnNlPXByZWRpY3Rpb25fYmFkJGRlZmF1bHQsIHByZWRpY3Rvcj1wcmVkaWN0aW9uX2JhZCQuZml0dGVkKQoKYGBgCgojIyMgVmlvbGluIHBsb3RzLCBDdXJ2YXMgUk9DIHkgQVVDcwoKUmVhbGl6YW1vcyBsb3MgZ3LDoWZpY29zIGRlIHZpb2xpbiwgbGFzIGN1cnZhcyBST0MgeSBjYWxjdWxhbW9zIGxvcyBBVUMKCmBgYHtyLCB3YXJuaW5nPUYsIGVjaG89RkFMU0V9Cgp2aW9saW5fZnVsbCA9IGdncGxvdChwcmVkaWN0aW9uX2Z1bGwsIGFlcyh4PWRlZmF1bHQsIHk9LmZpdHRlZCwgZ3JvdXA9ZGVmYXVsdCxmaWxsPWZhY3RvcihkZWZhdWx0KSkpICsgCiAgZ2VvbV92aW9saW4oKSArCiAgdGhlbWVfYncoKSArCiAgZ3VpZGVzKGZpbGw9RkFMU0UpICsKICBsYWJzKHRpdGxlPSdWaW9saW4gcGxvdCcsIHN1YnRpdGxlPSdNb2RlbG8gY29tcGxldG8nLCB5PSdQcmVkaWN0ZWQgcHJvYmFiaWxpdHknKQoKdmlvbGluX2JhZD1nZ3Bsb3QocHJlZGljdGlvbl9iYWQsIGFlcyh4PWRlZmF1bHQsIHk9LmZpdHRlZCwgZ3JvdXA9ZGVmYXVsdCwgZmlsbD1mYWN0b3IoZGVmYXVsdCkpKSArIAogIGdlb21fdmlvbGluKCkgKyAKICB0aGVtZV9idygpICsKICBndWlkZXMoZmlsbD1GQUxTRSkgKwogIGxhYnModGl0bGU9J1Zpb2xpbiBwbG90Jywgc3VidGl0bGU9J01vZGVsbyBtYWxvJywgeT0nUHJlZGljdGVkIHByb2JhYmlsaXR5JykKCnBsb3RfZ3JpZCh2aW9saW5fYmFkLCB2aW9saW5fZnVsbCkKCmdncm9jKGxpc3QoZnVsbD1yb2NfZnVsbCwgYmFkPXJvY19iYWQpLCBzaXplPTEpICsgZ2VvbV9hYmxpbmUoc2xvcGUgPSAxLCBpbnRlcmNlcHQgPSAxLCBsaW5ldHlwZT0nZGFzaGVkJykgKyB0aGVtZV9idygpICsgbGFicyh0aXRsZT0nQ3VydmFzIFJPQycsIGNvbG9yPSdNb2RlbG8nKQoKcHJpbnQocGFzdGUoJ0FVQzogTW9kZWxvIGNvbXBsZXRvJywgcm9jX2Z1bGwkYXVjKSkKCnByaW50KHBhc3RlKCdBVUM6IE1vZGVsbyBtYWxvJywgcm9jX2JhZCRhdWMpKQoKYGBgCgo+IMK/RMOzbmRlIHNlIHZlbiBsb3MgY2FtYmlvcyBtw6FzIG5vdG9yaW9zIHJlc3BlY3RvIGEgbnVlc3Ryb3MgbW9kZWxvcyBhbnRlcmlvcmVzIHF1ZSBubyB0ZW7DrWFuIGVuIGN1ZW50YSBlbCBkZXNiYWxhbmNlIGRlIGxhIGNsYXNlPwoKIyMjIFB1bnRvIGRlIGNvcnRlCgpWb2x2ZW1vcyBhIHJlYWxpemFyIGxhcyBwcnVlYmFzIHBhcmEgdmFyaW9zIHB1bnRvcyBkZSBjb3J0ZSB5IGdyYWZpY2Ftb3MgZWwgYWNjdXJhY3ksIGxhIHNlbnNpYmlsaWRhZCwgbGEgZXNwZWNpZmljaWRhZCwgZWwgcmVjYWxsIHkgbGEgcHJlY2lzaW9uIHBhcmEgY2FkYSB1bm8gZGUgZWxsb3MuCgpgYGB7ciwgZWNobz1GQUxTRX0KCmN1dG9mZnMgPSBzZXEoMC4wMSwwLjk5LDAuMDEpCmxvZ2l0X3ByZWQ9IG1hcF9kZnIoY3V0b2ZmcywgcHJlZGljdGlvbl9tZXRyaWNzKSU+JSBtdXRhdGUodGVybT1hcy5mYWN0b3IodGVybSkpCgpnZ3Bsb3QobG9naXRfcHJlZCwgYWVzKGN1dG9mZixlc3RpbWF0ZSwgZ3JvdXA9dGVybSwgY29sb3I9dGVybSkpICsgZ2VvbV9saW5lKHNpemU9MSkgKwogIHRoZW1lX2J3KCkgKwogIGxhYnModGl0bGU9ICdBY2N1cmFjeSwgU2Vuc2l0aXZpdHkgeSBTcGVjaWZpY2l0eScsIHN1YnRpdGxlPSAnTW9kZWxvIGNvbXBsZXRvJywgY29sb3I9IiIpCmBgYAoKwr9RdcOpIGNhbWJpb3MgdmVtb3MgcmVzcGVjdG8gYWwgZ3LDoWZpY28gYW50ZXJpb3I/CgojIyMgRGF0YXNldCBkZSB0ZXN0aW5nCgpQcm9iYW1vcyBlbiBlbCBkYXRhc2V0IGRlIHRlc3RpbmcgbnVlc3RybyBtb2RlbG8gYmFsYW5jZWFkby4gTm8gZXMgbmVjZXNhcmlvIHF1ZSBsZSBjcmVlbW9zIHBlc29zIGFsIGRhdGFzZXQgZGUgdGVzdGVvLgoKYGBge3IsIGVjaG89RkFMU0V9CmZ1bGxfbW9kZWwgPC0gZ2xtKGxvZ2l0X2Zvcm11bGFzJGZ1bGwsIGZhbWlseSA9ICdiaW5vbWlhbCcsIGRhdGEgPSBkZWZhdWx0LCB3ZWlnaHRzID0gd3QpCgp0YWJsZT0gYXVnbWVudCh4PWZ1bGxfbW9kZWwsIG5ld2RhdGE9dGVzdCwgdHlwZS5wcmVkaWN0PSdyZXNwb25zZScpIAoKdGFibGU9dGFibGUgJT4lIG11dGF0ZShwcmVkaWN0ZWRfY2xhc3M9aWZfZWxzZSguZml0dGVkPjAuMjUsIDEsIDApICU+JSBhcy5mYWN0b3IoKSwKICAgICAgICAgICBkZWZhdWx0PSBmYWN0b3IoZGVmYXVsdCkpCgpjb25mdXNpb25NYXRyaXgodGFibGUodGFibGUkZGVmYXVsdCwgdGFibGUkcHJlZGljdGVkX2NsYXNzKSwgcG9zaXRpdmUgPSAiMSIpCmBgYAoKCg==