Análisis de Riesgo de Enfermamdes Cardiovasculares utilizando Árboles de Decisión

Marcello Eduardo Anchante Fernandez

2024-11-12

Descripción del caso

Las enfermedades cardiovasculares, como los ataques al corazón y los accidentes cerebrovasculares, son una de las principales causas de mortalidad a nivel mundial. Según la Organización Mundial de la Salud (OMS), estas enfermedades son responsables de millones de muertes cada año, lo que subraya la importancia de la prevención y el diagnóstico temprano. En este contexto, el uso de modelos predictivos basados en datos se ha convertido en una herramienta valiosa para identificar a personas con alto riesgo de sufrir eventos cardíacos.

Este estudio se centra en la predicción del riesgo de enfermedades cardíacas utilizando un conjunto de datos que agrupa información de cinco fuentes públicas. El dataset contiene un total de 1,888 registros y 14 características que abarcan tanto factores médicos como demográficos, proporcionando una base sólida para evaluar la probabilidad de un ataque cardíaco. Entre las características analizadas se incluyen la edad, el género, los niveles de colesterol, la presión arterial, el tipo de dolor en el pecho, y otros indicadores relevantes que pueden influir en la salud cardiovascular.

El objetivo principal es desarrollar un modelo basado en los árboles de decisión que permita predecir con precisión si un paciente sufre un ataque al corazón, basándonos en estos factores de riesgo. Esto no solo ayudaría a los profesionales de la salud a tomar decisiones informadas sobre intervenciones preventivas, sino que también podría mejorar los resultados clínicos mediante un tratamiento más temprano y personalizado.

Descripción de las variables

  • age: Edad del paciente (Numérica).

  • sex: Género del paciente. Valores: 1 = hombre, 0 = mujer.

  • cp: Tipo de dolor en el pecho.

    Valores:

    0 = Angina típica

    1 = Angina atípica

    2 = Dolor no anginoso

    3 = Asintomático

  • trestbps: Presión arterial en reposo (en mm Hg) (Numérica).

  • chol: Nivel de colesterol en suero (en mg/dl) (Numérica).

  • fbs: Glucemia en ayunas > 120 mg/dl. Valores: 1 = verdadero, 0 = falso.

  • restecg: Resultados del electrocardiograma en reposo.

    Valores:

    0 = Normal

    1 = Anomalía en la onda ST-T

    2 = Hipertrofia ventricular izquierda

  • thalach: Frecuencia cardíaca máxima alcanzada (Numérica).

  • exang: Angina inducida por ejercicio. Valores: 1 = sí, 0 = no.

  • oldpeak: Depresión del ST inducida por el ejercicio en relación con el reposo (Numérica).

  • slope: Pendiente del segmento ST en el pico del ejercicio.

    Valores:

    0 = Ascendente

    1 = Plano

    2 = Descendente

  • ca: Número de vasos principales (0-3) coloreados por fluoroscopia.

    Valores: 0, 1, 2, 3.

  • thal: Tipos de talasemia.

    Valores:

    1 = Normal

    2 = Defecto fijo

    3 = Defecto reversible

  • target: Variable de resultado (riesgo de ataque cardíaco).

    Valores:

    1 = Mayor probabilidad de ataque cardíaco

    0 = Menor probabilidad de ataque cardíaco

Librerías

library(tidymodels)
library(doParallel)
library(parallel)

Lectura de datos

datos = readr::read_csv('cleaned_merged_heart_dataset.csv',show_col_types = FALSE)

head(datos)
## # A tibble: 6 × 14
##     age   sex    cp trestbps  chol   fbs restecg thalachh exang oldpeak slope
##   <dbl> <dbl> <dbl>    <dbl> <dbl> <dbl>   <dbl>    <dbl> <dbl>   <dbl> <dbl>
## 1    63     1     3      145   233     1       0      150     0     2.3     0
## 2    37     1     2      130   250     0       1      187     0     3.5     0
## 3    41     0     1      130   204     0       0      172     0     1.4     2
## 4    56     1     1      120   236     0       1      178     0     0.8     2
## 5    57     0     0      120   354     0       1      163     1     0.6     2
## 6    57     1     0      140   192     0       1      148     0     0.4     1
## # ℹ 3 more variables: ca <dbl>, thal <dbl>, target <dbl>

Pre-procesamiento de datos

Conversión a factor

datos$sex = factor(datos$sex, levels = c(0,1), labels = c('female','male'))
datos$cp = factor(datos$cp, levels = c(0,1,2,3))
datos$fbs = as.factor(datos$fbs)
datos$restecg = as.factor(datos$restecg)
datos$exang = as.factor(datos$exang)
datos$slope = as.factor(datos$slope)
datos$ca = as.factor(datos$ca)
datos$thal = as.factor(datos$thal)
datos$target = factor(datos$target, levels = c(0,1), labels = c('no','yes'))

head(datos)
## # A tibble: 6 × 14
##     age sex    cp    trestbps  chol fbs   restecg thalachh exang oldpeak slope
##   <dbl> <fct>  <fct>    <dbl> <dbl> <fct> <fct>      <dbl> <fct>   <dbl> <fct>
## 1    63 male   3          145   233 1     0            150 0         2.3 0    
## 2    37 male   2          130   250 0     1            187 0         3.5 0    
## 3    41 female 1          130   204 0     0            172 0         1.4 2    
## 4    56 male   1          120   236 0     1            178 0         0.8 2    
## 5    57 female 0          120   354 0     1            163 1         0.6 2    
## 6    57 male   0          140   192 0     1            148 0         0.4 1    
## # ℹ 3 more variables: ca <fct>, thal <fct>, target <fct>

Partición de datos

Generación de la partición

set.seed(2024)
datos_split = initial_split(datos, prop = .7, strata = target)
datos_split
## <Training/Testing/Total>
## <1320/568/1888>

Separación de datos

datos_train = training(datos_split)
datos_test = testing(datos_split)

Imputación de datos perdidos por k-nn

set.seed(2024)
datos_train = recipe(target~.,data = datos_train) %>% 
  step_impute_knn(all_predictors()) %>% 
  prep() %>% 
  bake(new_data = NULL)

set.seed(2024)
datos_test = recipe(target~.,data = datos_train) %>% 
  step_impute_knn(all_predictors()) %>% 
  prep() %>% 
  bake(new_data = datos_test)

Modelamiento de datos inicial

Validación cruzada

datos_fold <- vfold_cv(datos_train, v = 20, strata = target)

Recipe

set.seed(2024)
datos_recipe_dt = 
  recipe(target~., datos_train) %>%
  step_impute_knn(all_predictors())

Creación del modelo

des_tree = decision_tree() %>% 
  set_engine('rpart') %>% 
  set_mode('classification')

Flujo de trabajo

work_flow_dt = 
  workflow() %>% 
  add_model(des_tree) %>%
  add_recipe(datos_recipe_dt)

El mejor modelo

modelo_final_dt=
  work_flow_dt %>% 
  fit_resamples(resamples = datos_fold) %>% 
  show_best(metric='accuracy')
modelo_final_dt
## # A tibble: 1 × 6
##   .metric  .estimator  mean     n std_err .config             
##   <chr>    <chr>      <dbl> <int>   <dbl> <chr>               
## 1 accuracy binary     0.811    20 0.00816 Preprocessor1_Model1

Ajuste del modelo

modelo_final_dt_fit=
  work_flow_dt %>% 
  finalize_workflow(modelo_final_dt) %>% 
  fit(data = datos_train)
rpart.plot::rpart.plot(extract_fit_engine(modelo_final_dt_fit),roundint = F)

Predicción

class_pred=
  modelo_final_dt_fit %>% 
  predict(new_data = datos_test)

resultados_dt = 
  datos_test %>% 
  select(target) %>% 
  bind_cols(class_pred)

head(resultados_dt,10)
## # A tibble: 10 × 2
##    target .pred_class
##    <fct>  <fct>      
##  1 yes    yes        
##  2 yes    yes        
##  3 yes    yes        
##  4 yes    yes        
##  5 yes    yes        
##  6 yes    yes        
##  7 yes    yes        
##  8 yes    yes        
##  9 yes    yes        
## 10 yes    yes

Métricas

resultados_dt %>%  
  conf_mat(truth = target,
           estimate = .pred_class)
##           Truth
## Prediction  no yes
##        no  225  58
##        yes  49 236
metricas <- metric_set( accuracy, kap, precision, recall, f_meas, sens, spec)

resultados_dt %>% 
  metricas(truth = target, estimate = .pred_class)
## # A tibble: 7 × 3
##   .metric   .estimator .estimate
##   <chr>     <chr>          <dbl>
## 1 accuracy  binary         0.812
## 2 kap       binary         0.623
## 3 precision binary         0.795
## 4 recall    binary         0.821
## 5 f_meas    binary         0.808
## 6 sens      binary         0.821
## 7 spec      binary         0.803

Modelamiento de datos con hiperparámetros

Creación del modelo con tuneo

des_tree = decision_tree(tree_depth = tune(),
                         min_n = tune(),
                         cost_complexity = tune()) %>% 
  set_engine('rpart') %>% 
  set_mode('classification')

Flujo de trabajo

work_flow_dt = 
  workflow() %>% 
  add_model(des_tree) %>%
  add_recipe(datos_recipe_dt)

Hiperparámetros

grid = grid_random(extract_parameter_set_dials(des_tree), size = 500)
modelos_elegidos = tibble(cost_complexity = 
                        c(0.0000000001,6.70e-10,0.000000232,0.00000000309,0.000000998,0.00000978),
                          tree_depth = 
                            c(10,13,14,10,12,15),
                          min_n = c(2,2,3,2,3,4)
                          )

Modelos aleatorios

library(doParallel)
library(parallel)
registerDoParallel(cores = parallel::detectCores())
tictoc::tic()
set.seed(2024)
resultados_tune_aleat = 
  work_flow_dt %>% 
  tune_grid(resamples = datos_fold, grid = grid, metrics = metric_set(roc_auc,sens,spec,accuracy))
stopImplicitCluster()
tictoc::toc()
## 258.05 sec elapsed
best_model_aleat = 
  resultados_tune_aleat %>% 
  select_best(metric = 'accuracy')

best_model_aleat
## # A tibble: 1 × 4
##   cost_complexity tree_depth min_n .config               
##             <dbl>      <int> <int> <chr>                 
## 1   0.00000000778         15     2 Preprocessor1_Model019
best_model_fit_aleat = 
  work_flow_dt %>% 
  finalize_workflow(best_model_aleat) %>% 
  fit(data = datos_train)

set.seed(2024)
work_flow_dt %>%
  finalize_workflow(best_model_aleat) %>%
  last_fit(split = datos_split, metrics = metric_set(roc_auc,sens,spec,accuracy)) %>% 
  collect_metrics(summarize=T)
## # A tibble: 4 × 4
##   .metric  .estimator .estimate .config             
##   <chr>    <chr>          <dbl> <chr>               
## 1 sens     binary         0.949 Preprocessor1_Model1
## 2 spec     binary         0.935 Preprocessor1_Model1
## 3 accuracy binary         0.942 Preprocessor1_Model1
## 4 roc_auc  binary         0.942 Preprocessor1_Model1

Gráfico

rpart.plot::rpart.plot(extract_fit_engine(best_model_fit_aleat),cex = 0.4,roundint = F)

Predicción

class_pred_tune_aleat = 
  best_model_fit_aleat %>% 
  predict(new_data = datos_test)

resultados_dt_tune_aleat = 
  datos_test %>% 
  select(target) %>% 
  bind_cols(class_pred_tune_aleat)

Métricas

resultados_dt_tune_aleat %>%  
  conf_mat(truth = target,
           estimate = .pred_class)
##           Truth
## Prediction  no yes
##        no  260  18
##        yes  14 276
resultados_dt_tune_aleat %>%  
  metricas(truth = target,
           estimate = .pred_class)
## # A tibble: 7 × 3
##   .metric   .estimator .estimate
##   <chr>     <chr>          <dbl>
## 1 accuracy  binary         0.944
## 2 kap       binary         0.887
## 3 precision binary         0.935
## 4 recall    binary         0.949
## 5 f_meas    binary         0.942
## 6 sens      binary         0.949
## 7 spec      binary         0.939

Modelos previamente renderizados

registerDoParallel(cores = parallel::detectCores())
tictoc::tic()
resultados_tune_elegidos = 
  work_flow_dt %>% 
  tune_grid(resamples = datos_fold, grid = modelos_elegidos, metrics = metric_set(roc_auc,sens,spec,accuracy))
stopImplicitCluster()
tictoc::toc()
## 22.33 sec elapsed
best_model_elegido = 
  resultados_tune_elegidos %>% 
  select_best(metric = 'accuracy')

best_model_elegido
## # A tibble: 1 × 4
##   cost_complexity tree_depth min_n .config             
##             <dbl>      <dbl> <dbl> <chr>               
## 1   0.00000000067         13     2 Preprocessor1_Model2
best_model_fit_elegido = 
  work_flow_dt %>% 
  finalize_workflow(best_model_elegido) %>% 
  last_fit(split = datos_split, metrics = metricas)

set.seed(2024)
work_flow_dt %>%
  finalize_workflow(best_model_elegido) %>%
  last_fit(split = datos_split, metrics = metric_set(roc_auc,sens,spec,accuracy)) %>% 
  collect_metrics(summarize=T)
## # A tibble: 4 × 4
##   .metric  .estimator .estimate .config             
##   <chr>    <chr>          <dbl> <chr>               
## 1 sens     binary         0.949 Preprocessor1_Model1
## 2 spec     binary         0.935 Preprocessor1_Model1
## 3 accuracy binary         0.942 Preprocessor1_Model1
## 4 roc_auc  binary         0.942 Preprocessor1_Model1

Gráfico

rpart.plot::rpart.plot(extract_fit_engine(best_model_fit_elegido),cex = 0.3,roundint = F)

Predicción

class_pred_tune_elegido = 
  extract_workflow(best_model_fit_elegido) %>% 
  predict(new_data = datos_test)

resultados_dt_tune_elegido = 
  datos_test %>% 
  select(target) %>% 
  bind_cols(class_pred_tune_elegido)

Métricas

resultados_dt_tune_aleat %>%  
  conf_mat(truth = target,
           estimate = .pred_class)
##           Truth
## Prediction  no yes
##        no  260  18
##        yes  14 276
resultados_dt_tune_aleat %>%  
  metricas(truth = target,
           estimate = .pred_class)
## # A tibble: 7 × 3
##   .metric   .estimator .estimate
##   <chr>     <chr>          <dbl>
## 1 accuracy  binary         0.944
## 2 kap       binary         0.887
## 3 precision binary         0.935
## 4 recall    binary         0.949
## 5 f_meas    binary         0.942
## 6 sens      binary         0.949
## 7 spec      binary         0.939

Resultados

Como se observa en los modelos obtenidos, se tienen las mismas métricas de Accuracy, Sensibilidad, Especificidad y valor ROC mostradas a continuación.

Metrica Valor
Sensibilidad 0.94890511
Especificidad 0.93537415
Accuracy 0.94190141
Valor ROC 0.94213963

Debido a que las métricas son exactamente las mismas en ambos modelos elegidos, los factores para elegir el modelo ideal no se basará en las métricas utilizadas, sino se basarán en los hiperparámetros utilizados.

  • Modelo 1: Aleatorizado
cost_complexity tree_depth min_n .config
0.00000000777722 15 2 Preprocessor1_Model019
  • Modelo 2: Pre-renderizado
cost_complexity tree_depth min_n .config
0.00000000067 13 2 Preprocessor1_Model2

Para explicar los criterios que se utilizarán, es necesario entender el significado de cada hiperparámetro.

  • Costo de complejidad (cost_complexity)

    Este costo indica la penalización de poda de un árbo. Una menor penalización de poda conduce a una mayor complejidad del árbol, lo cual reduce el overfitting.

Modelo 1 Modelo 2
0.00000000777722 0.00000000067

En este caso, el modelo 2: pre-renderizado cuenta con un costo de complejidad menor respecto al modelo 1: aleatorio¨, lo cual indica que el modelo 2 tiene una mayor complejidad a la hora de predecir y hallar patrones de datos.

  • Profundidad del árbol (tree_depth)

    Este hiperparámetro indica la profundidad de niveles en el árbol de decisiones. Una mayor profundidad puede generar problemas de overfitting y de simplicidad del árbol.

Modelo 1 Modelo 2
15 13

En este caso, el modelo 2: Pre-renderizado tiene una menor profundidad respecto al modelo 1: Aleatorio; por lo tanto, nos conviene utilizar este segundo modelo ya que reduce el overfitting, es menos complejo y más robusto que el otro modelo.

  • Número mínimo de observaciones por nodo (min_n)

    Este hiperparámetro indica el mínimo número de observaciones que debe haber para abrir un nodo. Un menor valor conduce a un árbol más detallado y complejo pero con mayor riesgo de caer en overfitting.

Modelo 1 Modelo 2
2 2

En este caso, ambos modelos tienen como valor mínimo por nodo a 2. No hay diferencia en este caso. Una desventaja podría ser que el árbol podría sobreajustarse y volverse muy complejo ya que el valor 2 es muy pequeño.

Conclusión

Como se ha observado en el documento, se ha aplicado etapas de pre-procesamiento como la imputación de datos mediante K-nn y se ha modelado en base a un árbol de decisión con distintos hiperparámetros para hallar un modelo con altos valores de precisión, reduciendo el error en lo mejor posible.

En el primer caso, se optó por realizar un árbol de decisión con hiperparámetros por defecto y se observó que el ajuste del modelo es regularmente alto(aproximadamente el 82%); sin embargo, aún había posibilidades de aumentar los indicadores.

Tanto en el segundo y tercer caso, se optó por modificar los hiperparámetros y se obtuvieron las mismas métricas en ambos casos; por lo cual, se decidió comparar los hiperparámetros para establecer el modelo ideal.

Tras lo mostrado en el capítulo de Resultados, el modelo escogido en este documento es el segundo, el cual ha demostrado caer en menos overfitting debido a que no tiene mucha profundidad y tampoco tiene un alto costo de complejidad.

LS0tDQp0aXRsZTogIkFuw6FsaXNpcyBkZSBSaWVzZ28gZGUgRW5mZXJtYW1kZXMgQ2FyZGlvdmFzY3VsYXJlcyB1dGlsaXphbmRvIMOBcmJvbGVzIGRlIERlY2lzacOzbiINCmF1dGhvcjogIk1hcmNlbGxvIEVkdWFyZG8gQW5jaGFudGUgRmVybmFuZGV6Ig0KZGF0ZTogImByIFN5cy5EYXRlKClgIg0Kb3V0cHV0Og0KICBybWRmb3JtYXRzOjpkb3duY3V0ZToNCiAgICBsaWdodGJveDogdHJ1ZQ0KICAgIGdhbGxlcnk6IGZhbHNlDQogICAgY29kZV9kb3dubG9hZDogeWVzDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQogICAgY2FyZHM6IGZhbHNlDQogIGVkaXRvcl9vcHRpb25zOiANCiAgICBtYXJrZG93bjogDQogICAgICB3cmFwOiA3Mg0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0Kcm0obGlzdCA9bHMoKSkNCm9wdGlvbnMoc2NpcGVuID0gOTk5KQ0Kb3B0aW9ucyhkaWdpdHMgPSA4KQ0KYGBgDQoNCmBgYHtjc3MsIGVjaG89RkFMU0V9DQouV3JhcCB7DQogIHdpZHRoOiAxMDAlOw0KICBtYXgtd2lkdGg6IDIwMDBweDsNCiAgDQpoMSB7DQogIGZvbnQtd2VpZ2h0OiBib2xkOyAvKiBIYWNlIGVsIHRleHRvIGVuIG5lZ3JpdGEgKi8NCn0NCg0KcCB7DQogICAgZm9udC1zaXplOiAyMA0KfQ0KDQpiYWNrZ3JvdW5kLWNvbG9yOiAjRUJFQkVCOw0KDQoNCn0NCi5TaWRlYmFyIHsNCiAgd2lkdGg6IDI0MHB4Ow0KICBwYWRkaW5nOiAzMHB4IDAgNDBweDsNCiAgYmFja2dyb3VuZDogI2E1MzI2OTsNCn0NCi5Db250ZW50IHsNCiAgcGFkZGluZzogMCAyMHB4IDAgNTBweDsNCn0NCi5NYWluIHsNCiAgcGFkZGluZy1sZWZ0OiA1MDAgcHg7DQp9DQojdG9jID4gdWwgbGkgYSB7DQogIGZvbnQtc2l6ZTogMC45cmVtOw0KICBjb2xvcjogd2hpdGU7DQp9DQpgYGANCg0KIyBEZXNjcmlwY2nDs24gZGVsIGNhc28NCg0KTGFzIGVuZmVybWVkYWRlcyBjYXJkaW92YXNjdWxhcmVzLCBjb21vIGxvcyBhdGFxdWVzIGFsIGNvcmF6w7NuIHkgbG9zIGFjY2lkZW50ZXMgY2VyZWJyb3Zhc2N1bGFyZXMsIHNvbiB1bmEgZGUgbGFzIHByaW5jaXBhbGVzIGNhdXNhcyBkZSBtb3J0YWxpZGFkIGEgbml2ZWwgbXVuZGlhbC4gU2Vnw7puIGxhIE9yZ2FuaXphY2nDs24gTXVuZGlhbCBkZSBsYSBTYWx1ZCAoT01TKSwgZXN0YXMgZW5mZXJtZWRhZGVzIHNvbiByZXNwb25zYWJsZXMgZGUgbWlsbG9uZXMgZGUgbXVlcnRlcyBjYWRhIGHDsW8sIGxvIHF1ZSBzdWJyYXlhIGxhIGltcG9ydGFuY2lhIGRlIGxhIHByZXZlbmNpw7NuIHkgZWwgZGlhZ27Ds3N0aWNvIHRlbXByYW5vLiBFbiBlc3RlIGNvbnRleHRvLCBlbCB1c28gZGUgbW9kZWxvcyBwcmVkaWN0aXZvcyBiYXNhZG9zIGVuIGRhdG9zIHNlIGhhIGNvbnZlcnRpZG8gZW4gdW5hIGhlcnJhbWllbnRhIHZhbGlvc2EgcGFyYSBpZGVudGlmaWNhciBhIHBlcnNvbmFzIGNvbiBhbHRvIHJpZXNnbyBkZSBzdWZyaXIgZXZlbnRvcyBjYXJkw61hY29zLg0KDQpFc3RlIGVzdHVkaW8gc2UgY2VudHJhIGVuIGxhIHByZWRpY2Npw7NuIGRlbCByaWVzZ28gZGUgZW5mZXJtZWRhZGVzIGNhcmTDrWFjYXMgdXRpbGl6YW5kbyB1biBjb25qdW50byBkZSBkYXRvcyBxdWUgYWdydXBhIGluZm9ybWFjacOzbiBkZSBjaW5jbyBmdWVudGVzIHDDumJsaWNhcy4gRWwgZGF0YXNldCBjb250aWVuZSB1biB0b3RhbCBkZSAxLDg4OCByZWdpc3Ryb3MgeSAxNCBjYXJhY3RlcsOtc3RpY2FzIHF1ZSBhYmFyY2FuIHRhbnRvIGZhY3RvcmVzIG3DqWRpY29zIGNvbW8gZGVtb2dyw6FmaWNvcywgcHJvcG9yY2lvbmFuZG8gdW5hIGJhc2Ugc8OzbGlkYSBwYXJhIGV2YWx1YXIgbGEgcHJvYmFiaWxpZGFkIGRlIHVuIGF0YXF1ZSBjYXJkw61hY28uIEVudHJlIGxhcyBjYXJhY3RlcsOtc3RpY2FzIGFuYWxpemFkYXMgc2UgaW5jbHV5ZW4gbGEgZWRhZCwgZWwgZ8OpbmVybywgbG9zIG5pdmVsZXMgZGUgY29sZXN0ZXJvbCwgbGEgcHJlc2nDs24gYXJ0ZXJpYWwsIGVsIHRpcG8gZGUgZG9sb3IgZW4gZWwgcGVjaG8sIHkgb3Ryb3MgaW5kaWNhZG9yZXMgcmVsZXZhbnRlcyBxdWUgcHVlZGVuIGluZmx1aXIgZW4gbGEgc2FsdWQgY2FyZGlvdmFzY3VsYXIuDQoNCkVsIG9iamV0aXZvIHByaW5jaXBhbCBlcyBkZXNhcnJvbGxhciB1biBtb2RlbG8gYmFzYWRvIGVuIGxvcyAqKsOhcmJvbGVzIGRlIGRlY2lzacOzbioqIHF1ZSBwZXJtaXRhIHByZWRlY2lyIGNvbiBwcmVjaXNpw7NuIHNpIHVuIHBhY2llbnRlIHN1ZnJlIHVuIGF0YXF1ZSBhbCBjb3JhesOzbiwgYmFzw6FuZG9ub3MgZW4gZXN0b3MgZmFjdG9yZXMgZGUgcmllc2dvLiBFc3RvIG5vIHNvbG8gYXl1ZGFyw61hIGEgbG9zIHByb2Zlc2lvbmFsZXMgZGUgbGEgc2FsdWQgYSB0b21hciBkZWNpc2lvbmVzIGluZm9ybWFkYXMgc29icmUgaW50ZXJ2ZW5jaW9uZXMgcHJldmVudGl2YXMsIHNpbm8gcXVlIHRhbWJpw6luIHBvZHLDrWEgbWVqb3JhciBsb3MgcmVzdWx0YWRvcyBjbMOtbmljb3MgbWVkaWFudGUgdW4gdHJhdGFtaWVudG8gbcOhcyB0ZW1wcmFubyB5IHBlcnNvbmFsaXphZG8uDQoNCmBgYHtyIGVjaG89RkFMU0UsIG91dC53aWR0aD0iMzAlIn0NCmtuaXRyOjppbmNsdWRlX2dyYXBoaWNzKCJmb3RvLnBuZyIpDQpgYGANCg0KIyBEZXNjcmlwY2nDs24gZGUgbGFzIHZhcmlhYmxlcw0KDQotICoqYWdlKio6IEVkYWQgZGVsIHBhY2llbnRlIChOdW3DqXJpY2EpLg0KDQotICoqc2V4Kio6IEfDqW5lcm8gZGVsIHBhY2llbnRlLiBWYWxvcmVzOiAxID0gaG9tYnJlLCAwID0gbXVqZXIuDQoNCi0gKipjcCoqOiBUaXBvIGRlIGRvbG9yIGVuIGVsIHBlY2hvLiANCg0KICAgIFZhbG9yZXM6DQogICAgDQogICAgMCA9IEFuZ2luYSB0w61waWNhDQogICAgDQogICAgMSA9IEFuZ2luYSBhdMOtcGljYQ0KICAgIA0KICAgIDIgPSBEb2xvciBubyBhbmdpbm9zbw0KICAgIA0KICAgIDMgPSBBc2ludG9tw6F0aWNvDQogICAgDQotICoqdHJlc3RicHMqKjogUHJlc2nDs24gYXJ0ZXJpYWwgZW4gcmVwb3NvIChlbiBtbSBIZykgKE51bcOpcmljYSkuDQoNCi0gKipjaG9sKio6IE5pdmVsIGRlIGNvbGVzdGVyb2wgZW4gc3Vlcm8gKGVuIG1nL2RsKSAoTnVtw6lyaWNhKS4NCg0KLSAqKmZicyoqOiBHbHVjZW1pYSBlbiBheXVuYXMgPiAxMjAgbWcvZGwuIFZhbG9yZXM6IDEgPSB2ZXJkYWRlcm8sIDAgPSBmYWxzby4NCg0KLSAqKnJlc3RlY2cqKjogUmVzdWx0YWRvcyBkZWwgZWxlY3Ryb2NhcmRpb2dyYW1hIGVuIHJlcG9zby4gDQogICAgDQogICAgVmFsb3JlczoNCiAgICANCiAgICAwID0gTm9ybWFsDQogICAgDQogICAgMSA9IEFub21hbMOtYSBlbiBsYSBvbmRhIFNULVQNCiAgICANCiAgICAyID0gSGlwZXJ0cm9maWEgdmVudHJpY3VsYXIgaXpxdWllcmRhDQoNCi0gKip0aGFsYWNoKio6IEZyZWN1ZW5jaWEgY2FyZMOtYWNhIG3DoXhpbWEgYWxjYW56YWRhIChOdW3DqXJpY2EpLg0KDQotICoqZXhhbmcqKjogQW5naW5hIGluZHVjaWRhIHBvciBlamVyY2ljaW8uIFZhbG9yZXM6IDEgPSBzw60sIDAgPSBuby4NCg0KLSAqKm9sZHBlYWsqKjogRGVwcmVzacOzbiBkZWwgU1QgaW5kdWNpZGEgcG9yIGVsIGVqZXJjaWNpbyBlbiByZWxhY2nDs24gY29uIGVsIHJlcG9zbyAoTnVtw6lyaWNhKS4NCg0KLSAqKnNsb3BlKio6IFBlbmRpZW50ZSBkZWwgc2VnbWVudG8gU1QgZW4gZWwgcGljbyBkZWwgZWplcmNpY2lvLiANCg0KICAgIFZhbG9yZXM6DQoNCiAgICAwID0gQXNjZW5kZW50ZQ0KICAgIA0KICAgIDEgPSBQbGFubw0KICAgIA0KICAgIDIgPSBEZXNjZW5kZW50ZQ0KICAgIA0KLSAqKmNhKio6IE7Dum1lcm8gZGUgdmFzb3MgcHJpbmNpcGFsZXMgKDAtMykgY29sb3JlYWRvcyBwb3IgZmx1b3Jvc2NvcGlhLiANCg0KICAgIFZhbG9yZXM6IDAsIDEsIDIsIDMuDQoNCi0gKip0aGFsKio6IFRpcG9zIGRlIHRhbGFzZW1pYS4gDQogICAgDQogICAgVmFsb3JlczoNCg0KICAgIDEgPSBOb3JtYWwNCiAgICANCiAgICAyID0gRGVmZWN0byBmaWpvDQogICAgDQogICAgMyA9IERlZmVjdG8gcmV2ZXJzaWJsZQ0KDQotICoqdGFyZ2V0Kio6IFZhcmlhYmxlIGRlIHJlc3VsdGFkbyAocmllc2dvIGRlIGF0YXF1ZSBjYXJkw61hY28pLiANCg0KICAgIFZhbG9yZXM6DQoNCiAgICAxID0gTWF5b3IgcHJvYmFiaWxpZGFkIGRlIGF0YXF1ZSBjYXJkw61hY28NCiAgICANCiAgICAwID0gTWVub3IgcHJvYmFiaWxpZGFkIGRlIGF0YXF1ZSBjYXJkw61hY28NCg0KIyBMaWJyZXLDrWFzDQoNCmBgYHtyIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9Rn0NCmxpYnJhcnkodGlkeW1vZGVscykNCmxpYnJhcnkoZG9QYXJhbGxlbCkNCmxpYnJhcnkocGFyYWxsZWwpDQpgYGANCg0KDQojIExlY3R1cmEgZGUgZGF0b3MNCg0KDQpgYGB7cn0NCmRhdG9zID0gcmVhZHI6OnJlYWRfY3N2KCdjbGVhbmVkX21lcmdlZF9oZWFydF9kYXRhc2V0LmNzdicsc2hvd19jb2xfdHlwZXMgPSBGQUxTRSkNCg0KaGVhZChkYXRvcykNCmBgYA0KDQojIFByZS1wcm9jZXNhbWllbnRvIGRlIGRhdG9zDQoNCiMjIENvbnZlcnNpw7NuIGEgZmFjdG9yDQoNCmBgYHtyfQ0KZGF0b3Mkc2V4ID0gZmFjdG9yKGRhdG9zJHNleCwgbGV2ZWxzID0gYygwLDEpLCBsYWJlbHMgPSBjKCdmZW1hbGUnLCdtYWxlJykpDQpkYXRvcyRjcCA9IGZhY3RvcihkYXRvcyRjcCwgbGV2ZWxzID0gYygwLDEsMiwzKSkNCmRhdG9zJGZicyA9IGFzLmZhY3RvcihkYXRvcyRmYnMpDQpkYXRvcyRyZXN0ZWNnID0gYXMuZmFjdG9yKGRhdG9zJHJlc3RlY2cpDQpkYXRvcyRleGFuZyA9IGFzLmZhY3RvcihkYXRvcyRleGFuZykNCmRhdG9zJHNsb3BlID0gYXMuZmFjdG9yKGRhdG9zJHNsb3BlKQ0KZGF0b3MkY2EgPSBhcy5mYWN0b3IoZGF0b3MkY2EpDQpkYXRvcyR0aGFsID0gYXMuZmFjdG9yKGRhdG9zJHRoYWwpDQpkYXRvcyR0YXJnZXQgPSBmYWN0b3IoZGF0b3MkdGFyZ2V0LCBsZXZlbHMgPSBjKDAsMSksIGxhYmVscyA9IGMoJ25vJywneWVzJykpDQoNCmhlYWQoZGF0b3MpDQpgYGANCiMjIFBhcnRpY2nDs24gZGUgZGF0b3MNCg0KIyMjIEdlbmVyYWNpw7NuIGRlIGxhIHBhcnRpY2nDs24NCg0KYGBge3J9DQpzZXQuc2VlZCgyMDI0KQ0KZGF0b3Nfc3BsaXQgPSBpbml0aWFsX3NwbGl0KGRhdG9zLCBwcm9wID0gLjcsIHN0cmF0YSA9IHRhcmdldCkNCmRhdG9zX3NwbGl0DQpgYGANCg0KIyMjIFNlcGFyYWNpw7NuIGRlIGRhdG9zDQoNCmBgYHtyfQ0KZGF0b3NfdHJhaW4gPSB0cmFpbmluZyhkYXRvc19zcGxpdCkNCmRhdG9zX3Rlc3QgPSB0ZXN0aW5nKGRhdG9zX3NwbGl0KQ0KYGBgDQoNCiMjIyBJbXB1dGFjacOzbiBkZSBkYXRvcyBwZXJkaWRvcyBwb3Igay1ubg0KDQpgYGB7cn0NCnNldC5zZWVkKDIwMjQpDQpkYXRvc190cmFpbiA9IHJlY2lwZSh0YXJnZXR+LixkYXRhID0gZGF0b3NfdHJhaW4pICU+JSANCiAgc3RlcF9pbXB1dGVfa25uKGFsbF9wcmVkaWN0b3JzKCkpICU+JSANCiAgcHJlcCgpICU+JSANCiAgYmFrZShuZXdfZGF0YSA9IE5VTEwpDQoNCnNldC5zZWVkKDIwMjQpDQpkYXRvc190ZXN0ID0gcmVjaXBlKHRhcmdldH4uLGRhdGEgPSBkYXRvc190cmFpbikgJT4lIA0KICBzdGVwX2ltcHV0ZV9rbm4oYWxsX3ByZWRpY3RvcnMoKSkgJT4lIA0KICBwcmVwKCkgJT4lIA0KICBiYWtlKG5ld19kYXRhID0gZGF0b3NfdGVzdCkNCmBgYA0KDQojIE1vZGVsYW1pZW50byBkZSBkYXRvcyBpbmljaWFsDQoNCiMjIFZhbGlkYWNpw7NuIGNydXphZGENCg0KYGBge3J9DQpkYXRvc19mb2xkIDwtIHZmb2xkX2N2KGRhdG9zX3RyYWluLCB2ID0gMjAsIHN0cmF0YSA9IHRhcmdldCkNCmBgYA0KDQojIyBSZWNpcGUNCg0KYGBge3J9DQpzZXQuc2VlZCgyMDI0KQ0KZGF0b3NfcmVjaXBlX2R0ID0gDQogIHJlY2lwZSh0YXJnZXR+LiwgZGF0b3NfdHJhaW4pICU+JQ0KICBzdGVwX2ltcHV0ZV9rbm4oYWxsX3ByZWRpY3RvcnMoKSkNCmBgYA0KDQojIyBDcmVhY2nDs24gZGVsIG1vZGVsbw0KDQpgYGB7cn0NCmRlc190cmVlID0gZGVjaXNpb25fdHJlZSgpICU+JSANCiAgc2V0X2VuZ2luZSgncnBhcnQnKSAlPiUgDQogIHNldF9tb2RlKCdjbGFzc2lmaWNhdGlvbicpDQpgYGANCg0KIyMgRmx1am8gZGUgdHJhYmFqbw0KDQpgYGB7cn0NCndvcmtfZmxvd19kdCA9IA0KICB3b3JrZmxvdygpICU+JSANCiAgYWRkX21vZGVsKGRlc190cmVlKSAlPiUNCiAgYWRkX3JlY2lwZShkYXRvc19yZWNpcGVfZHQpDQpgYGANCg0KIyMgRWwgbWVqb3IgbW9kZWxvDQoNCmBgYHtyfQ0KbW9kZWxvX2ZpbmFsX2R0PQ0KICB3b3JrX2Zsb3dfZHQgJT4lIA0KICBmaXRfcmVzYW1wbGVzKHJlc2FtcGxlcyA9IGRhdG9zX2ZvbGQpICU+JSANCiAgc2hvd19iZXN0KG1ldHJpYz0nYWNjdXJhY3knKQ0KbW9kZWxvX2ZpbmFsX2R0DQpgYGANCg0KIyMgQWp1c3RlIGRlbCBtb2RlbG8NCg0KYGBge3J9DQptb2RlbG9fZmluYWxfZHRfZml0PQ0KICB3b3JrX2Zsb3dfZHQgJT4lIA0KICBmaW5hbGl6ZV93b3JrZmxvdyhtb2RlbG9fZmluYWxfZHQpICU+JSANCiAgZml0KGRhdGEgPSBkYXRvc190cmFpbikNCnJwYXJ0LnBsb3Q6OnJwYXJ0LnBsb3QoZXh0cmFjdF9maXRfZW5naW5lKG1vZGVsb19maW5hbF9kdF9maXQpLHJvdW5kaW50ID0gRikNCmBgYA0KDQojIyBQcmVkaWNjacOzbg0KDQpgYGB7cn0NCmNsYXNzX3ByZWQ9DQogIG1vZGVsb19maW5hbF9kdF9maXQgJT4lIA0KICBwcmVkaWN0KG5ld19kYXRhID0gZGF0b3NfdGVzdCkNCg0KcmVzdWx0YWRvc19kdCA9IA0KICBkYXRvc190ZXN0ICU+JSANCiAgc2VsZWN0KHRhcmdldCkgJT4lIA0KICBiaW5kX2NvbHMoY2xhc3NfcHJlZCkNCg0KaGVhZChyZXN1bHRhZG9zX2R0LDEwKQ0KYGBgDQoNCiMjIE3DqXRyaWNhcw0KDQpgYGB7cn0NCg0KcmVzdWx0YWRvc19kdCAlPiUgIA0KICBjb25mX21hdCh0cnV0aCA9IHRhcmdldCwNCiAgICAgICAgICAgZXN0aW1hdGUgPSAucHJlZF9jbGFzcykNCg0KbWV0cmljYXMgPC0gbWV0cmljX3NldCggYWNjdXJhY3ksIGthcCwgcHJlY2lzaW9uLCByZWNhbGwsIGZfbWVhcywgc2Vucywgc3BlYykNCg0KcmVzdWx0YWRvc19kdCAlPiUgDQogIG1ldHJpY2FzKHRydXRoID0gdGFyZ2V0LCBlc3RpbWF0ZSA9IC5wcmVkX2NsYXNzKQ0KDQpgYGANCg0KIyBNb2RlbGFtaWVudG8gZGUgZGF0b3MgY29uIGhpcGVycGFyw6FtZXRyb3MNCg0KIyMgQ3JlYWNpw7NuIGRlbCBtb2RlbG8gY29uIHR1bmVvDQoNCmBgYHtyfQ0KZGVzX3RyZWUgPSBkZWNpc2lvbl90cmVlKHRyZWVfZGVwdGggPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgbWluX24gPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgY29zdF9jb21wbGV4aXR5ID0gdHVuZSgpKSAlPiUgDQogIHNldF9lbmdpbmUoJ3JwYXJ0JykgJT4lIA0KICBzZXRfbW9kZSgnY2xhc3NpZmljYXRpb24nKQ0KYGBgDQoNCiMjIEZsdWpvIGRlIHRyYWJham8NCg0KYGBge3J9DQp3b3JrX2Zsb3dfZHQgPSANCiAgd29ya2Zsb3coKSAlPiUgDQogIGFkZF9tb2RlbChkZXNfdHJlZSkgJT4lDQogIGFkZF9yZWNpcGUoZGF0b3NfcmVjaXBlX2R0KQ0KYGBgDQoNCiMjIEhpcGVycGFyw6FtZXRyb3MNCg0KYGBge3J9DQpncmlkID0gZ3JpZF9yYW5kb20oZXh0cmFjdF9wYXJhbWV0ZXJfc2V0X2RpYWxzKGRlc190cmVlKSwgc2l6ZSA9IDUwMCkNCmBgYA0KDQoNCmBgYHtyfQ0KbW9kZWxvc19lbGVnaWRvcyA9IHRpYmJsZShjb3N0X2NvbXBsZXhpdHkgPSANCiAgICAgICAgICAgICAgICAgICAgICAgIGMoMC4wMDAwMDAwMDAxLDYuNzBlLTEwLDAuMDAwMDAwMjMyLDAuMDAwMDAwMDAzMDksMC4wMDAwMDA5OTgsMC4wMDAwMDk3OCksDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRyZWVfZGVwdGggPSANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKDEwLDEzLDE0LDEwLDEyLDE1KSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgbWluX24gPSBjKDIsMiwzLDIsMyw0KQ0KICAgICAgICAgICAgICAgICAgICAgICAgICApDQpgYGANCg0KIyMgTW9kZWxvcyBhbGVhdG9yaW9zDQoNCmBgYHtyIH0NCmxpYnJhcnkoZG9QYXJhbGxlbCkNCmxpYnJhcnkocGFyYWxsZWwpDQpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKSkNCnRpY3RvYzo6dGljKCkNCnNldC5zZWVkKDIwMjQpDQpyZXN1bHRhZG9zX3R1bmVfYWxlYXQgPSANCiAgd29ya19mbG93X2R0ICU+JSANCiAgdHVuZV9ncmlkKHJlc2FtcGxlcyA9IGRhdG9zX2ZvbGQsIGdyaWQgPSBncmlkLCBtZXRyaWNzID0gbWV0cmljX3NldChyb2NfYXVjLHNlbnMsc3BlYyxhY2N1cmFjeSkpDQpzdG9wSW1wbGljaXRDbHVzdGVyKCkNCnRpY3RvYzo6dG9jKCkNCmBgYA0KYGBge3J9DQpiZXN0X21vZGVsX2FsZWF0ID0gDQogIHJlc3VsdGFkb3NfdHVuZV9hbGVhdCAlPiUgDQogIHNlbGVjdF9iZXN0KG1ldHJpYyA9ICdhY2N1cmFjeScpDQoNCmJlc3RfbW9kZWxfYWxlYXQNCg0KYmVzdF9tb2RlbF9maXRfYWxlYXQgPSANCiAgd29ya19mbG93X2R0ICU+JSANCiAgZmluYWxpemVfd29ya2Zsb3coYmVzdF9tb2RlbF9hbGVhdCkgJT4lIA0KICBmaXQoZGF0YSA9IGRhdG9zX3RyYWluKQ0KDQpzZXQuc2VlZCgyMDI0KQ0Kd29ya19mbG93X2R0ICU+JQ0KICBmaW5hbGl6ZV93b3JrZmxvdyhiZXN0X21vZGVsX2FsZWF0KSAlPiUNCiAgbGFzdF9maXQoc3BsaXQgPSBkYXRvc19zcGxpdCwgbWV0cmljcyA9IG1ldHJpY19zZXQocm9jX2F1YyxzZW5zLHNwZWMsYWNjdXJhY3kpKSAlPiUgDQogIGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemU9VCkNCg0KYGBgDQoNCiMjIyBHcsOhZmljbw0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KcnBhcnQucGxvdDo6cnBhcnQucGxvdChleHRyYWN0X2ZpdF9lbmdpbmUoYmVzdF9tb2RlbF9maXRfYWxlYXQpLGNleCA9IDAuNCxyb3VuZGludCA9IEYpDQpgYGANCg0KDQoNCiMjIyBQcmVkaWNjacOzbg0KYGBge3J9DQpjbGFzc19wcmVkX3R1bmVfYWxlYXQgPSANCiAgYmVzdF9tb2RlbF9maXRfYWxlYXQgJT4lIA0KICBwcmVkaWN0KG5ld19kYXRhID0gZGF0b3NfdGVzdCkNCg0KcmVzdWx0YWRvc19kdF90dW5lX2FsZWF0ID0gDQogIGRhdG9zX3Rlc3QgJT4lIA0KICBzZWxlY3QodGFyZ2V0KSAlPiUgDQogIGJpbmRfY29scyhjbGFzc19wcmVkX3R1bmVfYWxlYXQpDQoNCmBgYA0KDQojIyMgTcOpdHJpY2FzDQoNCmBgYHtyfQ0KcmVzdWx0YWRvc19kdF90dW5lX2FsZWF0ICU+JSAgDQogIGNvbmZfbWF0KHRydXRoID0gdGFyZ2V0LA0KICAgICAgICAgICBlc3RpbWF0ZSA9IC5wcmVkX2NsYXNzKQ0KDQpyZXN1bHRhZG9zX2R0X3R1bmVfYWxlYXQgJT4lICANCiAgbWV0cmljYXModHJ1dGggPSB0YXJnZXQsDQogICAgICAgICAgIGVzdGltYXRlID0gLnByZWRfY2xhc3MpDQoNCmBgYA0KDQojIyBNb2RlbG9zIHByZXZpYW1lbnRlIHJlbmRlcml6YWRvcw0KDQpgYGB7cn0NCnJlZ2lzdGVyRG9QYXJhbGxlbChjb3JlcyA9IHBhcmFsbGVsOjpkZXRlY3RDb3JlcygpKQ0KdGljdG9jOjp0aWMoKQ0KcmVzdWx0YWRvc190dW5lX2VsZWdpZG9zID0gDQogIHdvcmtfZmxvd19kdCAlPiUgDQogIHR1bmVfZ3JpZChyZXNhbXBsZXMgPSBkYXRvc19mb2xkLCBncmlkID0gbW9kZWxvc19lbGVnaWRvcywgbWV0cmljcyA9IG1ldHJpY19zZXQocm9jX2F1YyxzZW5zLHNwZWMsYWNjdXJhY3kpKQ0Kc3RvcEltcGxpY2l0Q2x1c3RlcigpDQp0aWN0b2M6OnRvYygpDQpgYGANCmBgYHtyfQ0KYmVzdF9tb2RlbF9lbGVnaWRvID0gDQogIHJlc3VsdGFkb3NfdHVuZV9lbGVnaWRvcyAlPiUgDQogIHNlbGVjdF9iZXN0KG1ldHJpYyA9ICdhY2N1cmFjeScpDQoNCmJlc3RfbW9kZWxfZWxlZ2lkbw0KDQpiZXN0X21vZGVsX2ZpdF9lbGVnaWRvID0gDQogIHdvcmtfZmxvd19kdCAlPiUgDQogIGZpbmFsaXplX3dvcmtmbG93KGJlc3RfbW9kZWxfZWxlZ2lkbykgJT4lIA0KICBsYXN0X2ZpdChzcGxpdCA9IGRhdG9zX3NwbGl0LCBtZXRyaWNzID0gbWV0cmljYXMpDQoNCnNldC5zZWVkKDIwMjQpDQp3b3JrX2Zsb3dfZHQgJT4lDQogIGZpbmFsaXplX3dvcmtmbG93KGJlc3RfbW9kZWxfZWxlZ2lkbykgJT4lDQogIGxhc3RfZml0KHNwbGl0ID0gZGF0b3Nfc3BsaXQsIG1ldHJpY3MgPSBtZXRyaWNfc2V0KHJvY19hdWMsc2VucyxzcGVjLGFjY3VyYWN5KSkgJT4lIA0KICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplPVQpDQoNCmBgYA0KDQojIyMgR3LDoWZpY28NCg0KYGBge3Igd2FybmluZz1GfQ0KcnBhcnQucGxvdDo6cnBhcnQucGxvdChleHRyYWN0X2ZpdF9lbmdpbmUoYmVzdF9tb2RlbF9maXRfZWxlZ2lkbyksY2V4ID0gMC4zLHJvdW5kaW50ID0gRikNCmBgYA0KDQoNCiMjIyBQcmVkaWNjacOzbg0KYGBge3J9DQpjbGFzc19wcmVkX3R1bmVfZWxlZ2lkbyA9IA0KICBleHRyYWN0X3dvcmtmbG93KGJlc3RfbW9kZWxfZml0X2VsZWdpZG8pICU+JSANCiAgcHJlZGljdChuZXdfZGF0YSA9IGRhdG9zX3Rlc3QpDQoNCnJlc3VsdGFkb3NfZHRfdHVuZV9lbGVnaWRvID0gDQogIGRhdG9zX3Rlc3QgJT4lIA0KICBzZWxlY3QodGFyZ2V0KSAlPiUgDQogIGJpbmRfY29scyhjbGFzc19wcmVkX3R1bmVfZWxlZ2lkbykNCg0KYGBgDQoNCiMjIyBNw6l0cmljYXMNCg0KYGBge3J9DQpyZXN1bHRhZG9zX2R0X3R1bmVfYWxlYXQgJT4lICANCiAgY29uZl9tYXQodHJ1dGggPSB0YXJnZXQsDQogICAgICAgICAgIGVzdGltYXRlID0gLnByZWRfY2xhc3MpDQoNCnJlc3VsdGFkb3NfZHRfdHVuZV9hbGVhdCAlPiUgIA0KICBtZXRyaWNhcyh0cnV0aCA9IHRhcmdldCwNCiAgICAgICAgICAgZXN0aW1hdGUgPSAucHJlZF9jbGFzcykNCg0KYGBgDQoNCiMgUmVzdWx0YWRvcw0KDQpDb21vIHNlIG9ic2VydmEgZW4gbG9zIG1vZGVsb3Mgb2J0ZW5pZG9zLCBzZSB0aWVuZW4gbGFzIG1pc21hcyBtw6l0cmljYXMgZGUgQWNjdXJhY3ksIFNlbnNpYmlsaWRhZCwgRXNwZWNpZmljaWRhZCB5IHZhbG9yIFJPQyBtb3N0cmFkYXMgYSBjb250aW51YWNpw7NuLg0KDQpgYGB7ciBlY2hvPUYsIGluY2x1ZGU9VH0NCnNldC5zZWVkKDIwMjQpDQp3b3JrX2Zsb3dfZHQgJT4lDQogIGZpbmFsaXplX3dvcmtmbG93KGJlc3RfbW9kZWxfZWxlZ2lkbykgJT4lDQogIGxhc3RfZml0KHNwbGl0ID0gZGF0b3Nfc3BsaXQsIG1ldHJpY3MgPSBtZXRyaWNfc2V0KHJvY19hdWMsc2VucyxzcGVjLGFjY3VyYWN5KSkgJT4lIA0KICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplPVQpICU+JSANCiAgc2VsZWN0KC0uY29uZmlnLC0uZXN0aW1hdG9yKSAlPiUgDQogIHJlbmFtZShNZXRyaWNhPS5tZXRyaWMsIFZhbG9yID0gLmVzdGltYXRlKSAlPiUgDQogIHJlcGxhY2UoJ01ldHJpY2EnLGMoJ1NlbnNpYmlsaWRhZCcsJ0VzcGVjaWZpY2lkYWQnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnQWNjdXJhY3knLCdWYWxvciBST0MnKSkgJT4lIA0KICBrbml0cjo6a2FibGUoKQ0KDQpgYGANCg0KRGViaWRvIGEgcXVlIGxhcyBtw6l0cmljYXMgc29uIGV4YWN0YW1lbnRlIGxhcyBtaXNtYXMgZW4gYW1ib3MgbW9kZWxvcyBlbGVnaWRvcywgbG9zIGZhY3RvcmVzIHBhcmEgZWxlZ2lyIGVsIG1vZGVsbyBpZGVhbCBubyBzZSBiYXNhcsOhIGVuIGxhcyBtw6l0cmljYXMgdXRpbGl6YWRhcywgc2lubyBzZSBiYXNhcsOhbiBlbiBsb3MgaGlwZXJwYXLDoW1ldHJvcyB1dGlsaXphZG9zLg0KDQotICpNb2RlbG8gMTogQWxlYXRvcml6YWRvKg0KDQpgYGB7ciBlY2hvPUYsIGluY2x1ZGU9VH0NCmJlc3RfbW9kZWxfYWxlYXQgJT4lIGtuaXRyOjprYWJsZShkaWdpdHMgPSAxNCkNCmBgYA0KDQotICpNb2RlbG8gMjogUHJlLXJlbmRlcml6YWRvKg0KDQpgYGB7ciBlY2hvPUYsIGluY2x1ZGU9VH0NCmJlc3RfbW9kZWxfZWxlZ2lkbyAlPiUga25pdHI6OmthYmxlKGRpZ2l0cyA9IDE0KQ0KYGBgDQoNClBhcmEgZXhwbGljYXIgbG9zIGNyaXRlcmlvcyBxdWUgc2UgdXRpbGl6YXLDoW4sIGVzIG5lY2VzYXJpbyBlbnRlbmRlciBlbCBzaWduaWZpY2FkbyBkZSBjYWRhIGhpcGVycGFyw6FtZXRyby4NCg0KLSAqQ29zdG8gZGUgY29tcGxlamlkYWQqIChgY29zdF9jb21wbGV4aXR5YCkNCg0KICBFc3RlIGNvc3RvIGluZGljYSBsYSBwZW5hbGl6YWNpw7NuIGRlIHBvZGEgZGUgdW4gw6FyYm8uIFVuYSBtZW5vciBwZW5hbGl6YWNpw7NuIGRlIHBvZGEgY29uZHVjZSBhIHVuYSBtYXlvciBjb21wbGVqaWRhZCBkZWwgw6FyYm9sLCBsbyBjdWFsIHJlZHVjZSBlbCBvdmVyZml0dGluZy4gDQogIA0KYGBge3IgZWNobz1GLCBpbmNsdWRlPVQsIG1lc3NhZ2U9Rn0NCnIxID0gYmluZF9jb2xzKGJlc3RfbW9kZWxfYWxlYXRbLCdjb3N0X2NvbXBsZXhpdHknXSxiZXN0X21vZGVsX2VsZWdpZG9bLCdjb3N0X2NvbXBsZXhpdHknXSkNCmNvbG5hbWVzKHIxKSA9IGMoJ01vZGVsbyAxJywnTW9kZWxvIDInKQ0KcjEgJT4lIGtuaXRyOjprYWJsZShkaWdpdHMgPSAxNCkNCmBgYA0KICANCiAgDQogIEVuIGVzdGUgY2FzbywgZWwgKm1vZGVsbyAyOiBwcmUtcmVuZGVyaXphZG8qIGN1ZW50YSBjb24gdW4gY29zdG8gZGUgY29tcGxlamlkYWQgbWVub3IgcmVzcGVjdG8gYWwgKm1vZGVsbyAxOiBhbGVhdG9yaW8qwqgsIGxvIGN1YWwgaW5kaWNhIHF1ZSBlbCBtb2RlbG8gMiB0aWVuZSB1bmEgbWF5b3IgY29tcGxlamlkYWQgYSBsYSBob3JhIGRlIHByZWRlY2lyIHkgaGFsbGFyIHBhdHJvbmVzIGRlIGRhdG9zLg0KICANCi0gKlByb2Z1bmRpZGFkIGRlbCDDoXJib2wqIChgdHJlZV9kZXB0aGApDQogIA0KICBFc3RlIGhpcGVycGFyw6FtZXRybyBpbmRpY2EgbGEgcHJvZnVuZGlkYWQgZGUgbml2ZWxlcyBlbiBlbCDDoXJib2wgZGUgZGVjaXNpb25lcy4gVW5hIG1heW9yIHByb2Z1bmRpZGFkIHB1ZWRlIGdlbmVyYXIgcHJvYmxlbWFzIGRlIG92ZXJmaXR0aW5nIHkgZGUgc2ltcGxpY2lkYWQgZGVsIMOhcmJvbC4NCiAgDQpgYGB7ciBlY2hvPUYsIGluY2x1ZGU9VCwgbWVzc2FnZT1GfQ0KcjIgPSBiaW5kX2NvbHMoYmVzdF9tb2RlbF9hbGVhdFssJ3RyZWVfZGVwdGgnXSxiZXN0X21vZGVsX2VsZWdpZG9bLCd0cmVlX2RlcHRoJ10pDQpjb2xuYW1lcyhyMikgPSBjKCdNb2RlbG8gMScsJ01vZGVsbyAyJykNCnIyICU+JSBrbml0cjo6a2FibGUoZGlnaXRzID0gMTQpDQpgYGANCg0KICBFbiBlc3RlIGNhc28sIGVsICptb2RlbG8gMjogUHJlLXJlbmRlcml6YWRvKiB0aWVuZSB1bmEgbWVub3IgcHJvZnVuZGlkYWQgcmVzcGVjdG8gYWwgKm1vZGVsbyAxOiBBbGVhdG9yaW8qOyBwb3IgbG8gdGFudG8sIG5vcyBjb252aWVuZSB1dGlsaXphciBlc3RlIHNlZ3VuZG8gbW9kZWxvIHlhIHF1ZSByZWR1Y2UgZWwgb3ZlcmZpdHRpbmcsIGVzIG1lbm9zIGNvbXBsZWpvIHkgbcOhcyByb2J1c3RvIHF1ZSBlbCBvdHJvIG1vZGVsby4NCiAgDQogIA0KLSAqTsO6bWVybyBtw61uaW1vIGRlIG9ic2VydmFjaW9uZXMgcG9yIG5vZG8qIChgbWluX25gKQ0KDQogIEVzdGUgaGlwZXJwYXLDoW1ldHJvIGluZGljYSBlbCBtw61uaW1vIG7Dum1lcm8gZGUgb2JzZXJ2YWNpb25lcyBxdWUgZGViZSBoYWJlciBwYXJhIGFicmlyIHVuIG5vZG8uIFVuIG1lbm9yIHZhbG9yIGNvbmR1Y2UgYSB1biDDoXJib2wgbcOhcyBkZXRhbGxhZG8geSBjb21wbGVqbyBwZXJvIGNvbiBtYXlvciByaWVzZ28gZGUgY2FlciBlbiBvdmVyZml0dGluZy4NCg0KYGBge3IgZWNobz1GLCBpbmNsdWRlPVQsIG1lc3NhZ2U9Rn0NCnIzID0gYmluZF9jb2xzKGJlc3RfbW9kZWxfYWxlYXRbLCdtaW5fbiddLGJlc3RfbW9kZWxfZWxlZ2lkb1ssJ21pbl9uJ10pDQpjb2xuYW1lcyhyMykgPSBjKCdNb2RlbG8gMScsJ01vZGVsbyAyJykNCnIzICU+JSBrbml0cjo6a2FibGUoZGlnaXRzID0gMTQpDQpgYGANCiAgDQogIEVuIGVzdGUgY2FzbywgYW1ib3MgbW9kZWxvcyB0aWVuZW4gY29tbyB2YWxvciBtw61uaW1vIHBvciBub2RvIGEgMi4gTm8gaGF5IGRpZmVyZW5jaWEgZW4gZXN0ZSBjYXNvLiBVbmEgZGVzdmVudGFqYSBwb2Ryw61hIHNlciBxdWUgZWwgw6FyYm9sIHBvZHLDrWEgc29icmVhanVzdGFyc2UgeSB2b2x2ZXJzZSBtdXkgY29tcGxlam8geWEgcXVlIGVsIHZhbG9yIDIgZXMgbXV5IHBlcXVlw7FvLg0KICANCg0KIyBDb25jbHVzacOzbg0KDQpDb21vIHNlIGhhIG9ic2VydmFkbyBlbiBlbCBkb2N1bWVudG8sIHNlIGhhIGFwbGljYWRvIGV0YXBhcyBkZSBwcmUtcHJvY2VzYW1pZW50byBjb21vIGxhIGltcHV0YWNpw7NuIGRlIGRhdG9zIG1lZGlhbnRlIEstbm4geSBzZSBoYSBtb2RlbGFkbyBlbiBiYXNlIGEgdW4gw6FyYm9sIGRlIGRlY2lzacOzbiBjb24gZGlzdGludG9zIGhpcGVycGFyw6FtZXRyb3MgcGFyYSBoYWxsYXIgdW4gbW9kZWxvIGNvbiBhbHRvcyB2YWxvcmVzIGRlIHByZWNpc2nDs24sIHJlZHVjaWVuZG8gZWwgZXJyb3IgZW4gbG8gbWVqb3IgcG9zaWJsZS4gDQoNCkVuIGVsIHByaW1lciBjYXNvLCBzZSBvcHTDsyBwb3IgcmVhbGl6YXIgdW4gw6FyYm9sIGRlIGRlY2lzacOzbiBjb24gaGlwZXJwYXLDoW1ldHJvcyBwb3IgZGVmZWN0byB5IHNlIG9ic2VydsOzIHF1ZSBlbCBhanVzdGUgZGVsIG1vZGVsbyBlcyByZWd1bGFybWVudGUgYWx0byhhcHJveGltYWRhbWVudGUgZWwgODIlKTsgc2luIGVtYmFyZ28sIGHDum4gaGFiw61hIHBvc2liaWxpZGFkZXMgZGUgYXVtZW50YXIgbG9zIGluZGljYWRvcmVzLg0KDQpUYW50byBlbiBlbCBzZWd1bmRvIHkgdGVyY2VyIGNhc28sIHNlIG9wdMOzIHBvciBtb2RpZmljYXIgbG9zIGhpcGVycGFyw6FtZXRyb3MgeSBzZSBvYnR1dmllcm9uIGxhcyBtaXNtYXMgbcOpdHJpY2FzIGVuIGFtYm9zIGNhc29zOyBwb3IgbG8gY3VhbCwgc2UgZGVjaWRpw7MgY29tcGFyYXIgbG9zIGhpcGVycGFyw6FtZXRyb3MgcGFyYSBlc3RhYmxlY2VyIGVsIG1vZGVsbyBpZGVhbC4NCg0KVHJhcyBsbyBtb3N0cmFkbyBlbiBlbCBjYXDDrXR1bG8gZGUgUmVzdWx0YWRvcywgZWwgbW9kZWxvIGVzY29naWRvIGVuIGVzdGUgZG9jdW1lbnRvIGVzIGVsIHNlZ3VuZG8sIGVsIGN1YWwgaGEgZGVtb3N0cmFkbyBjYWVyIGVuIG1lbm9zIG92ZXJmaXR0aW5nIGRlYmlkbyBhIHF1ZSBubyB0aWVuZSBtdWNoYSBwcm9mdW5kaWRhZCB5IHRhbXBvY28gdGllbmUgdW4gYWx0byBjb3N0byBkZSBjb21wbGVqaWRhZC4NCg0K