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.
| 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.
| 0.00000000777722 |
15 |
2 |
Preprocessor1_Model019 |
- Modelo 2: Pre-renderizado
| 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.
| 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.
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.
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