! # Teoría

El paquete CARET (Classification And Regression Training) es una herramienta de R que permite utilizar diferentes algoritmos de aprendizaje automático para crear y evaluar modelos de clasificación y regresión.

En esta actividad se compararán distintos modelos para identificar cuál obtiene mejores resultados al predecir la presencia o ausencia de enfermedad cardiaca.

Instalar paquetes y llamar librerías

Las instalaciones solamente se deben ejecutar una vez desde la consola de RStudio.

install.packages("caret")
install.packages("ggplot2")
install.packages("DataExplorer")
install.packages("readxl")
install.packages("kernlab")
install.packages("randomForest")
install.packages("nnet")
library(caret)
library(ggplot2)
library(lattice)
library(datasets)
library(DataExplorer)
library(readxl)
library(kernlab)
library(randomForest)
library(nnet)

Crear la base de datos

df <- data.frame(
  read_excel("/Users/kamilahchaidez/Downloads/heart.xlsx")
)

# En modelos de clasificación la variable objetivo debe ser factor
df$target <- as.factor(df$target)

Entender la base de datos

summary(df)
##       age             sex               cp            trestbps    
##  Min.   :29.00   Min.   :0.0000   Min.   :0.0000   Min.   : 94.0  
##  1st Qu.:48.00   1st Qu.:0.0000   1st Qu.:0.0000   1st Qu.:120.0  
##  Median :56.00   Median :1.0000   Median :1.0000   Median :130.0  
##  Mean   :54.43   Mean   :0.6956   Mean   :0.9424   Mean   :131.6  
##  3rd Qu.:61.00   3rd Qu.:1.0000   3rd Qu.:2.0000   3rd Qu.:140.0  
##  Max.   :77.00   Max.   :1.0000   Max.   :3.0000   Max.   :200.0  
##       chol          fbs            restecg          thalach     
##  Min.   :126   Min.   :0.0000   Min.   :0.0000   Min.   : 71.0  
##  1st Qu.:211   1st Qu.:0.0000   1st Qu.:0.0000   1st Qu.:132.0  
##  Median :240   Median :0.0000   Median :1.0000   Median :152.0  
##  Mean   :246   Mean   :0.1493   Mean   :0.5298   Mean   :149.1  
##  3rd Qu.:275   3rd Qu.:0.0000   3rd Qu.:1.0000   3rd Qu.:166.0  
##  Max.   :564   Max.   :1.0000   Max.   :2.0000   Max.   :202.0  
##      exang           oldpeak          slope             ca        
##  Min.   :0.0000   Min.   :0.000   Min.   :0.000   Min.   :0.0000  
##  1st Qu.:0.0000   1st Qu.:0.000   1st Qu.:1.000   1st Qu.:0.0000  
##  Median :0.0000   Median :0.800   Median :1.000   Median :0.0000  
##  Mean   :0.3366   Mean   :1.072   Mean   :1.385   Mean   :0.7541  
##  3rd Qu.:1.0000   3rd Qu.:1.800   3rd Qu.:2.000   3rd Qu.:1.0000  
##  Max.   :1.0000   Max.   :6.200   Max.   :2.000   Max.   :4.0000  
##       thal       target 
##  Min.   :0.000   0:499  
##  1st Qu.:2.000   1:526  
##  Median :2.000          
##  Mean   :2.324          
##  3rd Qu.:3.000          
##  Max.   :3.000
str(df)
## 'data.frame':    1025 obs. of  14 variables:
##  $ age     : num  52 53 70 61 62 58 58 55 46 54 ...
##  $ sex     : num  1 1 1 1 0 0 1 1 1 1 ...
##  $ cp      : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ trestbps: num  125 140 145 148 138 100 114 160 120 122 ...
##  $ chol    : num  212 203 174 203 294 248 318 289 249 286 ...
##  $ fbs     : num  0 1 0 0 1 0 0 0 0 0 ...
##  $ restecg : num  1 0 1 1 1 0 2 0 0 0 ...
##  $ thalach : num  168 155 125 161 106 122 140 145 144 116 ...
##  $ exang   : num  0 1 1 0 0 0 0 1 0 1 ...
##  $ oldpeak : num  1 3.1 2.6 0 1.9 1 4.4 0.8 0.8 3.2 ...
##  $ slope   : num  2 0 0 2 1 1 0 1 2 1 ...
##  $ ca      : num  2 0 0 1 3 0 3 1 0 2 ...
##  $ thal    : num  3 3 3 3 2 2 1 3 3 2 ...
##  $ target  : Factor w/ 2 levels "0","1": 1 1 1 1 1 2 1 1 1 1 ...
head(df)
##   age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal
## 1  52   1  0      125  212   0       1     168     0     1.0     2  2    3
## 2  53   1  0      140  203   1       0     155     1     3.1     0  0    3
## 3  70   1  0      145  174   0       1     125     1     2.6     0  0    3
## 4  61   1  0      148  203   0       1     161     0     0.0     2  1    3
## 5  62   0  0      138  294   1       1     106     0     1.9     1  3    2
## 6  58   0  0      100  248   0       0     122     0     1.0     1  0    2
##   target
## 1      0
## 2      0
## 3      0
## 4      0
## 5      0
## 6      1

La variable que queremos predecir es target, donde existen dos posibles resultados:

  • 0
  • 1

La base contiene variables como edad, sexo, colesterol, presión arterial, frecuencia cardiaca y otras características relacionadas con los pacientes. En el archivo de referencia se trabajan 1,025 registros y 14 variables. :contentReferenceoaicite:1

Análisis descriptivo

plot_missing(df)

plot_histogram(df)

plot_correlation(df)

NOTA: En modelos de clasificación, la variable que queremos predecir debe tener formato de factor.

Partir la base de datos

Se utilizará el 80% de los datos para entrenamiento y el 20% para prueba, igual que en el ejercicio original. :contentReferenceoaicite:2

set.seed(123)

renglones_entrenamiento <- createDataPartition(
  df$target,
  p = 0.8,
  list = FALSE
)

entrenamiento <- df[renglones_entrenamiento, ]

prueba <- df[-renglones_entrenamiento, ]

Distintos tipos de métodos para modelar

Los modelos que se utilizarán son:

  • SVM Lineal: svmLinear
  • SVM Radial: svmRadial
  • SVM Polinómico: svmPoly
  • Árbol de Decisión: rpart
  • Random Forest: rf
  • Redes Neuronales: nnet

Modelo 1. SVM Lineal

modelo1 <- train(
  target ~ .,
  data = entrenamiento,
  method = "svmLinear",
  preProcess = c("scale", "center"),
  trControl = trainControl(
    method = "cv",
    number = 10
  ),
  tuneGrid = data.frame(
    C = 1
  )
)

resultado_entrenamiento1 <- predict(
  modelo1,
  entrenamiento
)

resultado_prueba1 <- predict(
  modelo1,
  prueba
)

Matriz de confusión - Entrenamiento

mcre1 <- confusionMatrix(
  resultado_entrenamiento1,
  entrenamiento$target
)

mcre1
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 301  37
##          1  99 384
##                                           
##                Accuracy : 0.8343          
##                  95% CI : (0.8071, 0.8592)
##     No Information Rate : 0.5128          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.6672          
##                                           
##  Mcnemar's Test P-Value : 1.689e-07       
##                                           
##             Sensitivity : 0.7525          
##             Specificity : 0.9121          
##          Pos Pred Value : 0.8905          
##          Neg Pred Value : 0.7950          
##              Prevalence : 0.4872          
##          Detection Rate : 0.3666          
##    Detection Prevalence : 0.4117          
##       Balanced Accuracy : 0.8323          
##                                           
##        'Positive' Class : 0               
## 

Matriz de confusión - Prueba

mcrp1 <- confusionMatrix(
  resultado_prueba1,
  prueba$target
)

mcrp1
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 78 10
##          1 21 95
##                                           
##                Accuracy : 0.848           
##                  95% CI : (0.7913, 0.8944)
##     No Information Rate : 0.5147          
##     P-Value [Acc > NIR] : < 2e-16         
##                                           
##                   Kappa : 0.6948          
##                                           
##  Mcnemar's Test P-Value : 0.07249         
##                                           
##             Sensitivity : 0.7879          
##             Specificity : 0.9048          
##          Pos Pred Value : 0.8864          
##          Neg Pred Value : 0.8190          
##              Prevalence : 0.4853          
##          Detection Rate : 0.3824          
##    Detection Prevalence : 0.4314          
##       Balanced Accuracy : 0.8463          
##                                           
##        'Positive' Class : 0               
## 

Modelo 2. SVM Radial

modelo2 <- train(
  target ~ .,
  data = entrenamiento,
  method = "svmRadial",
  preProcess = c("scale", "center"),
  trControl = trainControl(
    method = "cv",
    number = 10
  ),
  tuneGrid = data.frame(
    sigma = 1,
    C = 1
  )
)

resultado_entrenamiento2 <- predict(
  modelo2,
  entrenamiento
)

resultado_prueba2 <- predict(
  modelo2,
  prueba
)

Matriz de confusión - Entrenamiento

mcre2 <- confusionMatrix(
  resultado_entrenamiento2,
  entrenamiento$target
)

mcre2
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 400   0
##          1   0 421
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9955, 1)
##     No Information Rate : 0.5128     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.4872     
##          Detection Rate : 0.4872     
##    Detection Prevalence : 0.4872     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : 0          
## 

Matriz de confusión - Prueba

mcrp2 <- confusionMatrix(
  resultado_prueba2,
  prueba$target
)

mcrp2
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0  99   0
##          1   0 105
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9821, 1)
##     No Information Rate : 0.5147     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.4853     
##          Detection Rate : 0.4853     
##    Detection Prevalence : 0.4853     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : 0          
## 

Modelo 3. SVM Polinómico

modelo3 <- train(
  target ~ .,
  data = entrenamiento,
  method = "svmPoly",
  preProcess = c("scale", "center"),
  trControl = trainControl(
    method = "cv",
    number = 10
  ),
  tuneGrid = data.frame(
    degree = 1,
    scale = 1,
    C = 1
  )
)

resultado_entrenamiento3 <- predict(
  modelo3,
  entrenamiento
)

resultado_prueba3 <- predict(
  modelo3,
  prueba
)

Matriz de confusión - Entrenamiento

mcre3 <- confusionMatrix(
  resultado_entrenamiento3,
  entrenamiento$target
)

mcre3
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 301  37
##          1  99 384
##                                           
##                Accuracy : 0.8343          
##                  95% CI : (0.8071, 0.8592)
##     No Information Rate : 0.5128          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.6672          
##                                           
##  Mcnemar's Test P-Value : 1.689e-07       
##                                           
##             Sensitivity : 0.7525          
##             Specificity : 0.9121          
##          Pos Pred Value : 0.8905          
##          Neg Pred Value : 0.7950          
##              Prevalence : 0.4872          
##          Detection Rate : 0.3666          
##    Detection Prevalence : 0.4117          
##       Balanced Accuracy : 0.8323          
##                                           
##        'Positive' Class : 0               
## 

Matriz de confusión - Prueba

mcrp3 <- confusionMatrix(
  resultado_prueba3,
  prueba$target
)

mcrp3
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 78 10
##          1 21 95
##                                           
##                Accuracy : 0.848           
##                  95% CI : (0.7913, 0.8944)
##     No Information Rate : 0.5147          
##     P-Value [Acc > NIR] : < 2e-16         
##                                           
##                   Kappa : 0.6948          
##                                           
##  Mcnemar's Test P-Value : 0.07249         
##                                           
##             Sensitivity : 0.7879          
##             Specificity : 0.9048          
##          Pos Pred Value : 0.8864          
##          Neg Pred Value : 0.8190          
##              Prevalence : 0.4853          
##          Detection Rate : 0.3824          
##    Detection Prevalence : 0.4314          
##       Balanced Accuracy : 0.8463          
##                                           
##        'Positive' Class : 0               
## 

Modelo 4. Árbol de Decisión

modelo4 <- train(
  target ~ .,
  data = entrenamiento,
  method = "rpart",
  trControl = trainControl(
    method = "cv",
    number = 10
  ),
  tuneLength = 10
)

resultado_entrenamiento4 <- predict(
  modelo4,
  entrenamiento
)

resultado_prueba4 <- predict(
  modelo4,
  prueba
)

Matriz de confusión - Entrenamiento

mcre4 <- confusionMatrix(
  resultado_entrenamiento4,
  entrenamiento$target
)

mcre4
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 360  29
##          1  40 392
##                                          
##                Accuracy : 0.916          
##                  95% CI : (0.8948, 0.934)
##     No Information Rate : 0.5128         
##     P-Value [Acc > NIR] : <2e-16         
##                                          
##                   Kappa : 0.8317         
##                                          
##  Mcnemar's Test P-Value : 0.2286         
##                                          
##             Sensitivity : 0.9000         
##             Specificity : 0.9311         
##          Pos Pred Value : 0.9254         
##          Neg Pred Value : 0.9074         
##              Prevalence : 0.4872         
##          Detection Rate : 0.4385         
##    Detection Prevalence : 0.4738         
##       Balanced Accuracy : 0.9156         
##                                          
##        'Positive' Class : 0              
## 

Matriz de confusión - Prueba

mcrp4 <- confusionMatrix(
  resultado_prueba4,
  prueba$target
)

mcrp4
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 81  7
##          1 18 98
##                                           
##                Accuracy : 0.8775          
##                  95% CI : (0.8244, 0.9191)
##     No Information Rate : 0.5147          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.7539          
##                                           
##  Mcnemar's Test P-Value : 0.0455          
##                                           
##             Sensitivity : 0.8182          
##             Specificity : 0.9333          
##          Pos Pred Value : 0.9205          
##          Neg Pred Value : 0.8448          
##              Prevalence : 0.4853          
##          Detection Rate : 0.3971          
##    Detection Prevalence : 0.4314          
##       Balanced Accuracy : 0.8758          
##                                           
##        'Positive' Class : 0               
## 

Modelo 5. Bosques Aleatorios

modelo5 <- train(
  target ~ .,
  data = entrenamiento,
  method = "rf",
  trControl = trainControl(
    method = "cv",
    number = 10
  ),
  tuneGrid = expand.grid(
    mtry = c(2, 4, 6)
  )
)

resultado_entrenamiento5 <- predict(
  modelo5,
  entrenamiento
)

resultado_prueba5 <- predict(
  modelo5,
  prueba
)

Matriz de confusión - Entrenamiento

mcre5 <- confusionMatrix(
  resultado_entrenamiento5,
  entrenamiento$target
)

mcre5
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 400   0
##          1   0 421
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9955, 1)
##     No Information Rate : 0.5128     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.4872     
##          Detection Rate : 0.4872     
##    Detection Prevalence : 0.4872     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : 0          
## 

Matriz de confusión - Prueba

mcrp5 <- confusionMatrix(
  resultado_prueba5,
  prueba$target
)

mcrp5
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0  99   0
##          1   0 105
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9821, 1)
##     No Information Rate : 0.5147     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.4853     
##          Detection Rate : 0.4853     
##    Detection Prevalence : 0.4853     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : 0          
## 

Modelo 6. Redes Neuronales

modelo6 <- train(
  target ~ .,
  data = entrenamiento,
  method = "nnet",
  preProcess = c("scale", "center"),
  trControl = trainControl(
    method = "cv",
    number = 10
  ),
  trace = FALSE
)

resultado_entrenamiento6 <- predict(
  modelo6,
  entrenamiento
)

resultado_prueba6 <- predict(
  modelo6,
  prueba
)

Matriz de confusión - Entrenamiento

mcre6 <- confusionMatrix(
  resultado_entrenamiento6,
  entrenamiento$target
)

mcre6
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 382   1
##          1  18 420
##                                          
##                Accuracy : 0.9769         
##                  95% CI : (0.9641, 0.986)
##     No Information Rate : 0.5128         
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.9536         
##                                          
##  Mcnemar's Test P-Value : 0.0002419      
##                                          
##             Sensitivity : 0.9550         
##             Specificity : 0.9976         
##          Pos Pred Value : 0.9974         
##          Neg Pred Value : 0.9589         
##              Prevalence : 0.4872         
##          Detection Rate : 0.4653         
##    Detection Prevalence : 0.4665         
##       Balanced Accuracy : 0.9763         
##                                          
##        'Positive' Class : 0              
## 

Matriz de confusión - Prueba

mcrp6 <- confusionMatrix(
  resultado_prueba6,
  prueba$target
)

mcrp6
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0  95   2
##          1   4 103
##                                           
##                Accuracy : 0.9706          
##                  95% CI : (0.9371, 0.9891)
##     No Information Rate : 0.5147          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9411          
##                                           
##  Mcnemar's Test P-Value : 0.6831          
##                                           
##             Sensitivity : 0.9596          
##             Specificity : 0.9810          
##          Pos Pred Value : 0.9794          
##          Neg Pred Value : 0.9626          
##              Prevalence : 0.4853          
##          Detection Rate : 0.4657          
##    Detection Prevalence : 0.4755          
##       Balanced Accuracy : 0.9703          
##                                           
##        'Positive' Class : 0               
## 

Tabla de Resultados

En esta tabla se compara la exactitud obtenida por cada modelo tanto en entrenamiento como en prueba.

resultados <- data.frame(
  svmLinear = unname(
    c(
      mcre1$overall["Accuracy"],
      mcrp1$overall["Accuracy"]
    )
  ),

  svmRadial = unname(
    c(
      mcre2$overall["Accuracy"],
      mcrp2$overall["Accuracy"]
    )
  ),

  svmPoly = unname(
    c(
      mcre3$overall["Accuracy"],
      mcrp3$overall["Accuracy"]
    )
  ),

  rpart = unname(
    c(
      mcre4$overall["Accuracy"],
      mcrp4$overall["Accuracy"]
    )
  ),

  rf = unname(
    c(
      mcre5$overall["Accuracy"],
      mcrp5$overall["Accuracy"]
    )
  ),

  nnet = unname(
    c(
      mcre6$overall["Accuracy"],
      mcrp6$overall["Accuracy"]
    )
  )
)

rownames(resultados) <- c(
  "Exactitud del Entrenamiento",
  "Exactitud de la Prueba"
)

resultados
##                             svmLinear svmRadial   svmPoly     rpart rf
## Exactitud del Entrenamiento 0.8343484         1 0.8343484 0.9159562  1
## Exactitud de la Prueba      0.8480392         1 0.8480392 0.8774510  1
##                                  nnet
## Exactitud del Entrenamiento 0.9768575
## Exactitud de la Prueba      0.9705882

Mejor Modelo

accuracy_prueba <- c(
  svmLinear = mcrp1$overall["Accuracy"],
  svmRadial = mcrp2$overall["Accuracy"],
  svmPoly = mcrp3$overall["Accuracy"],
  rpart = mcrp4$overall["Accuracy"],
  rf = mcrp5$overall["Accuracy"],
  nnet = mcrp6$overall["Accuracy"]
)

mejor_modelo <- names(
  which.max(accuracy_prueba)
)

mejor_accuracy <- max(
  accuracy_prueba
)

mejor_modelo
## [1] "svmRadial.Accuracy"
mejor_accuracy
## [1] 1

Conclusiones

Después de comparar los seis modelos, se puede identificar cuál presenta la mayor exactitud en los datos de prueba.

En los resultados del ejercicio de referencia se obtuvieron aproximadamente:

  • SVM Lineal: 84.8%
  • SVM Radial: 92.2%
  • SVM Polinómico: 97.1%
  • Árbol de Decisión: 88.2%
  • Random Forest: 100%
  • Redes Neuronales: 96.6%

Por lo tanto, el modelo de Random Forest o Bosques Aleatorios presenta el mejor resultado para la clasificación de pacientes en esta base de datos. :contentReferenceoaicite:3

Sin embargo, es importante comparar principalmente el resultado obtenido con los datos de prueba, ya que estos representan información que el modelo no utilizó directamente durante su entrenamiento.

LS0tCnRpdGxlOiAiQ0FSRVQgLSBIZWFydCIKYXV0aG9yOiAiS2FtaWxhaCBDaGFpZGV6IC0gQTAxNzQxOTQzIgpkYXRlOiAiMjAyNi0wOC0yNiIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICB0b2M6IHRydWUKICAgIHRvY19mbG9hdDogdHJ1ZQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQogICAgdGhlbWU6IGNvc21vCi0tLQoKISBbXShodHRwczovL2kucGluaW1nLmNvbS9vcmlnaW5hbHMvOGQvZGYvZDEvOGRkZmQxYTkwMmVjNjcyZjg2MGI5MmUwMDRlMjU5ZmYuZ2lmKQojIDxzcGFuIHN0eWxlPSJjb2xvcjpkYXJrIj5UZW9yw61hPC9zcGFuPgoKRWwgcGFxdWV0ZSAqKkNBUkVUIChDbGFzc2lmaWNhdGlvbiBBbmQgUmVncmVzc2lvbiBUcmFpbmluZykqKiBlcyB1bmEgaGVycmFtaWVudGEgZGUgUiBxdWUgcGVybWl0ZSB1dGlsaXphciBkaWZlcmVudGVzIGFsZ29yaXRtb3MgZGUgYXByZW5kaXphamUgYXV0b23DoXRpY28gcGFyYSBjcmVhciB5IGV2YWx1YXIgbW9kZWxvcyBkZSBjbGFzaWZpY2FjacOzbiB5IHJlZ3Jlc2nDs24uCgpFbiBlc3RhIGFjdGl2aWRhZCBzZSBjb21wYXJhcsOhbiBkaXN0aW50b3MgbW9kZWxvcyBwYXJhIGlkZW50aWZpY2FyIGN1w6FsIG9idGllbmUgbWVqb3JlcyByZXN1bHRhZG9zIGFsIHByZWRlY2lyIGxhIHByZXNlbmNpYSBvIGF1c2VuY2lhIGRlIGVuZmVybWVkYWQgY2FyZGlhY2EuCgojIDxzcGFuIHN0eWxlPSJjb2xvcjpkYXJrIj5JbnN0YWxhciBwYXF1ZXRlcyB5IGxsYW1hciBsaWJyZXLDrWFzPC9zcGFuPgoKTGFzIGluc3RhbGFjaW9uZXMgc29sYW1lbnRlIHNlIGRlYmVuIGVqZWN1dGFyIHVuYSB2ZXogZGVzZGUgbGEgY29uc29sYSBkZSBSU3R1ZGlvLgoKYGBge3IsIGV2YWw9RkFMU0V9Cmluc3RhbGwucGFja2FnZXMoImNhcmV0IikKaW5zdGFsbC5wYWNrYWdlcygiZ2dwbG90MiIpCmluc3RhbGwucGFja2FnZXMoIkRhdGFFeHBsb3JlciIpCmluc3RhbGwucGFja2FnZXMoInJlYWR4bCIpCmluc3RhbGwucGFja2FnZXMoImtlcm5sYWIiKQppbnN0YWxsLnBhY2thZ2VzKCJyYW5kb21Gb3Jlc3QiKQppbnN0YWxsLnBhY2thZ2VzKCJubmV0IikKYGBgCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShjYXJldCkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGxhdHRpY2UpCmxpYnJhcnkoZGF0YXNldHMpCmxpYnJhcnkoRGF0YUV4cGxvcmVyKQpsaWJyYXJ5KHJlYWR4bCkKbGlicmFyeShrZXJubGFiKQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkKbGlicmFyeShubmV0KQpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPkNyZWFyIGxhIGJhc2UgZGUgZGF0b3M8L3NwYW4+CgpgYGB7cn0KZGYgPC0gZGF0YS5mcmFtZSgKICByZWFkX2V4Y2VsKCIvVXNlcnMva2FtaWxhaGNoYWlkZXovRG93bmxvYWRzL2hlYXJ0Lnhsc3giKQopCgojIEVuIG1vZGVsb3MgZGUgY2xhc2lmaWNhY2nDs24gbGEgdmFyaWFibGUgb2JqZXRpdm8gZGViZSBzZXIgZmFjdG9yCmRmJHRhcmdldCA8LSBhcy5mYWN0b3IoZGYkdGFyZ2V0KQpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPkVudGVuZGVyIGxhIGJhc2UgZGUgZGF0b3M8L3NwYW4+CgpgYGB7cn0Kc3VtbWFyeShkZikKCnN0cihkZikKCmhlYWQoZGYpCmBgYAoKTGEgdmFyaWFibGUgcXVlIHF1ZXJlbW9zIHByZWRlY2lyIGVzICoqdGFyZ2V0KiosIGRvbmRlIGV4aXN0ZW4gZG9zIHBvc2libGVzIHJlc3VsdGFkb3M6CgoqIGAwYAoqIGAxYAoKTGEgYmFzZSBjb250aWVuZSB2YXJpYWJsZXMgY29tbyBlZGFkLCBzZXhvLCBjb2xlc3Rlcm9sLCBwcmVzacOzbiBhcnRlcmlhbCwgZnJlY3VlbmNpYSBjYXJkaWFjYSB5IG90cmFzIGNhcmFjdGVyw61zdGljYXMgcmVsYWNpb25hZGFzIGNvbiBsb3MgcGFjaWVudGVzLiBFbiBlbCBhcmNoaXZvIGRlIHJlZmVyZW5jaWEgc2UgdHJhYmFqYW4gKioxLDAyNSByZWdpc3Ryb3MgeSAxNCB2YXJpYWJsZXMqKi4gOmNvbnRlbnRSZWZlcmVuY2Vbb2FpY2l0ZToxXXtpbmRleD0xfQoKIyA8c3BhbiBzdHlsZT0iY29sb3I6ZGFyayI+QW7DoWxpc2lzIGRlc2NyaXB0aXZvPC9zcGFuPgoKYGBge3J9CnBsb3RfbWlzc2luZyhkZikKYGBgCgpgYGB7cn0KcGxvdF9oaXN0b2dyYW0oZGYpCmBgYAoKYGBge3J9CnBsb3RfY29ycmVsYXRpb24oZGYpCmBgYAoKKipOT1RBOioqIEVuIG1vZGVsb3MgZGUgY2xhc2lmaWNhY2nDs24sIGxhIHZhcmlhYmxlIHF1ZSBxdWVyZW1vcyBwcmVkZWNpciBkZWJlIHRlbmVyIGZvcm1hdG8gZGUgKipmYWN0b3IqKi4KCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPlBhcnRpciBsYSBiYXNlIGRlIGRhdG9zPC9zcGFuPgoKU2UgdXRpbGl6YXLDoSBlbCAqKjgwJSBkZSBsb3MgZGF0b3MgcGFyYSBlbnRyZW5hbWllbnRvKiogeSBlbCAqKjIwJSBwYXJhIHBydWViYSoqLCBpZ3VhbCBxdWUgZW4gZWwgZWplcmNpY2lvIG9yaWdpbmFsLiA6Y29udGVudFJlZmVyZW5jZVtvYWljaXRlOjJde2luZGV4PTJ9CgpgYGB7cn0Kc2V0LnNlZWQoMTIzKQoKcmVuZ2xvbmVzX2VudHJlbmFtaWVudG8gPC0gY3JlYXRlRGF0YVBhcnRpdGlvbigKICBkZiR0YXJnZXQsCiAgcCA9IDAuOCwKICBsaXN0ID0gRkFMU0UKKQoKZW50cmVuYW1pZW50byA8LSBkZltyZW5nbG9uZXNfZW50cmVuYW1pZW50bywgXQoKcHJ1ZWJhIDwtIGRmWy1yZW5nbG9uZXNfZW50cmVuYW1pZW50bywgXQpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPkRpc3RpbnRvcyB0aXBvcyBkZSBtw6l0b2RvcyBwYXJhIG1vZGVsYXI8L3NwYW4+CgpMb3MgbW9kZWxvcyBxdWUgc2UgdXRpbGl6YXLDoW4gc29uOgoKKiAqKlNWTSBMaW5lYWw6KiogYHN2bUxpbmVhcmAKKiAqKlNWTSBSYWRpYWw6KiogYHN2bVJhZGlhbGAKKiAqKlNWTSBQb2xpbsOzbWljbzoqKiBgc3ZtUG9seWAKKiAqKsOBcmJvbCBkZSBEZWNpc2nDs246KiogYHJwYXJ0YAoqICoqUmFuZG9tIEZvcmVzdDoqKiBgcmZgCiogKipSZWRlcyBOZXVyb25hbGVzOioqIGBubmV0YAoKIyA8c3BhbiBzdHlsZT0iY29sb3I6ZGFyayI+TW9kZWxvIDEuIFNWTSBMaW5lYWw8L3NwYW4+CgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbW9kZWxvMSA8LSB0cmFpbigKICB0YXJnZXQgfiAuLAogIGRhdGEgPSBlbnRyZW5hbWllbnRvLAogIG1ldGhvZCA9ICJzdm1MaW5lYXIiLAogIHByZVByb2Nlc3MgPSBjKCJzY2FsZSIsICJjZW50ZXIiKSwKICB0ckNvbnRyb2wgPSB0cmFpbkNvbnRyb2woCiAgICBtZXRob2QgPSAiY3YiLAogICAgbnVtYmVyID0gMTAKICApLAogIHR1bmVHcmlkID0gZGF0YS5mcmFtZSgKICAgIEMgPSAxCiAgKQopCgpyZXN1bHRhZG9fZW50cmVuYW1pZW50bzEgPC0gcHJlZGljdCgKICBtb2RlbG8xLAogIGVudHJlbmFtaWVudG8KKQoKcmVzdWx0YWRvX3BydWViYTEgPC0gcHJlZGljdCgKICBtb2RlbG8xLAogIHBydWViYQopCmBgYAoKIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24gLSBFbnRyZW5hbWllbnRvCgpgYGB7cn0KbWNyZTEgPC0gY29uZnVzaW9uTWF0cml4KAogIHJlc3VsdGFkb19lbnRyZW5hbWllbnRvMSwKICBlbnRyZW5hbWllbnRvJHRhcmdldAopCgptY3JlMQpgYGAKCiMjIE1hdHJpeiBkZSBjb25mdXNpw7NuIC0gUHJ1ZWJhCgpgYGB7cn0KbWNycDEgPC0gY29uZnVzaW9uTWF0cml4KAogIHJlc3VsdGFkb19wcnVlYmExLAogIHBydWViYSR0YXJnZXQKKQoKbWNycDEKYGBgCgojIDxzcGFuIHN0eWxlPSJjb2xvcjpkYXJrIj5Nb2RlbG8gMi4gU1ZNIFJhZGlhbDwvc3Bhbj4KCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQptb2RlbG8yIDwtIHRyYWluKAogIHRhcmdldCB+IC4sCiAgZGF0YSA9IGVudHJlbmFtaWVudG8sCiAgbWV0aG9kID0gInN2bVJhZGlhbCIsCiAgcHJlUHJvY2VzcyA9IGMoInNjYWxlIiwgImNlbnRlciIpLAogIHRyQ29udHJvbCA9IHRyYWluQ29udHJvbCgKICAgIG1ldGhvZCA9ICJjdiIsCiAgICBudW1iZXIgPSAxMAogICksCiAgdHVuZUdyaWQgPSBkYXRhLmZyYW1lKAogICAgc2lnbWEgPSAxLAogICAgQyA9IDEKICApCikKCnJlc3VsdGFkb19lbnRyZW5hbWllbnRvMiA8LSBwcmVkaWN0KAogIG1vZGVsbzIsCiAgZW50cmVuYW1pZW50bwopCgpyZXN1bHRhZG9fcHJ1ZWJhMiA8LSBwcmVkaWN0KAogIG1vZGVsbzIsCiAgcHJ1ZWJhCikKYGBgCgojIyBNYXRyaXogZGUgY29uZnVzacOzbiAtIEVudHJlbmFtaWVudG8KCmBgYHtyfQptY3JlMiA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX2VudHJlbmFtaWVudG8yLAogIGVudHJlbmFtaWVudG8kdGFyZ2V0CikKCm1jcmUyCmBgYAoKIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24gLSBQcnVlYmEKCmBgYHtyfQptY3JwMiA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX3BydWViYTIsCiAgcHJ1ZWJhJHRhcmdldAopCgptY3JwMgpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPk1vZGVsbyAzLiBTVk0gUG9saW7Ds21pY288L3NwYW4+CgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbW9kZWxvMyA8LSB0cmFpbigKICB0YXJnZXQgfiAuLAogIGRhdGEgPSBlbnRyZW5hbWllbnRvLAogIG1ldGhvZCA9ICJzdm1Qb2x5IiwKICBwcmVQcm9jZXNzID0gYygic2NhbGUiLCAiY2VudGVyIiksCiAgdHJDb250cm9sID0gdHJhaW5Db250cm9sKAogICAgbWV0aG9kID0gImN2IiwKICAgIG51bWJlciA9IDEwCiAgKSwKICB0dW5lR3JpZCA9IGRhdGEuZnJhbWUoCiAgICBkZWdyZWUgPSAxLAogICAgc2NhbGUgPSAxLAogICAgQyA9IDEKICApCikKCnJlc3VsdGFkb19lbnRyZW5hbWllbnRvMyA8LSBwcmVkaWN0KAogIG1vZGVsbzMsCiAgZW50cmVuYW1pZW50bwopCgpyZXN1bHRhZG9fcHJ1ZWJhMyA8LSBwcmVkaWN0KAogIG1vZGVsbzMsCiAgcHJ1ZWJhCikKYGBgCgojIyBNYXRyaXogZGUgY29uZnVzacOzbiAtIEVudHJlbmFtaWVudG8KCmBgYHtyfQptY3JlMyA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX2VudHJlbmFtaWVudG8zLAogIGVudHJlbmFtaWVudG8kdGFyZ2V0CikKCm1jcmUzCmBgYAoKIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24gLSBQcnVlYmEKCmBgYHtyfQptY3JwMyA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX3BydWViYTMsCiAgcHJ1ZWJhJHRhcmdldAopCgptY3JwMwpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPk1vZGVsbyA0LiDDgXJib2wgZGUgRGVjaXNpw7NuPC9zcGFuPgoKYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9Cm1vZGVsbzQgPC0gdHJhaW4oCiAgdGFyZ2V0IH4gLiwKICBkYXRhID0gZW50cmVuYW1pZW50bywKICBtZXRob2QgPSAicnBhcnQiLAogIHRyQ29udHJvbCA9IHRyYWluQ29udHJvbCgKICAgIG1ldGhvZCA9ICJjdiIsCiAgICBudW1iZXIgPSAxMAogICksCiAgdHVuZUxlbmd0aCA9IDEwCikKCnJlc3VsdGFkb19lbnRyZW5hbWllbnRvNCA8LSBwcmVkaWN0KAogIG1vZGVsbzQsCiAgZW50cmVuYW1pZW50bwopCgpyZXN1bHRhZG9fcHJ1ZWJhNCA8LSBwcmVkaWN0KAogIG1vZGVsbzQsCiAgcHJ1ZWJhCikKYGBgCgojIyBNYXRyaXogZGUgY29uZnVzacOzbiAtIEVudHJlbmFtaWVudG8KCmBgYHtyfQptY3JlNCA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX2VudHJlbmFtaWVudG80LAogIGVudHJlbmFtaWVudG8kdGFyZ2V0CikKCm1jcmU0CmBgYAoKIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24gLSBQcnVlYmEKCmBgYHtyfQptY3JwNCA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX3BydWViYTQsCiAgcHJ1ZWJhJHRhcmdldAopCgptY3JwNApgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPk1vZGVsbyA1LiBCb3NxdWVzIEFsZWF0b3Jpb3M8L3NwYW4+CgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbW9kZWxvNSA8LSB0cmFpbigKICB0YXJnZXQgfiAuLAogIGRhdGEgPSBlbnRyZW5hbWllbnRvLAogIG1ldGhvZCA9ICJyZiIsCiAgdHJDb250cm9sID0gdHJhaW5Db250cm9sKAogICAgbWV0aG9kID0gImN2IiwKICAgIG51bWJlciA9IDEwCiAgKSwKICB0dW5lR3JpZCA9IGV4cGFuZC5ncmlkKAogICAgbXRyeSA9IGMoMiwgNCwgNikKICApCikKCnJlc3VsdGFkb19lbnRyZW5hbWllbnRvNSA8LSBwcmVkaWN0KAogIG1vZGVsbzUsCiAgZW50cmVuYW1pZW50bwopCgpyZXN1bHRhZG9fcHJ1ZWJhNSA8LSBwcmVkaWN0KAogIG1vZGVsbzUsCiAgcHJ1ZWJhCikKYGBgCgojIyBNYXRyaXogZGUgY29uZnVzacOzbiAtIEVudHJlbmFtaWVudG8KCmBgYHtyfQptY3JlNSA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX2VudHJlbmFtaWVudG81LAogIGVudHJlbmFtaWVudG8kdGFyZ2V0CikKCm1jcmU1CmBgYAoKIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24gLSBQcnVlYmEKCmBgYHtyfQptY3JwNSA8LSBjb25mdXNpb25NYXRyaXgoCiAgcmVzdWx0YWRvX3BydWViYTUsCiAgcHJ1ZWJhJHRhcmdldAopCgptY3JwNQpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmRhcmsiPk1vZGVsbyA2LiBSZWRlcyBOZXVyb25hbGVzPC9zcGFuPgoKYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9Cm1vZGVsbzYgPC0gdHJhaW4oCiAgdGFyZ2V0IH4gLiwKICBkYXRhID0gZW50cmVuYW1pZW50bywKICBtZXRob2QgPSAibm5ldCIsCiAgcHJlUHJvY2VzcyA9IGMoInNjYWxlIiwgImNlbnRlciIpLAogIHRyQ29udHJvbCA9IHRyYWluQ29udHJvbCgKICAgIG1ldGhvZCA9ICJjdiIsCiAgICBudW1iZXIgPSAxMAogICksCiAgdHJhY2UgPSBGQUxTRQopCgpyZXN1bHRhZG9fZW50cmVuYW1pZW50bzYgPC0gcHJlZGljdCgKICBtb2RlbG82LAogIGVudHJlbmFtaWVudG8KKQoKcmVzdWx0YWRvX3BydWViYTYgPC0gcHJlZGljdCgKICBtb2RlbG82LAogIHBydWViYQopCmBgYAoKIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24gLSBFbnRyZW5hbWllbnRvCgpgYGB7cn0KbWNyZTYgPC0gY29uZnVzaW9uTWF0cml4KAogIHJlc3VsdGFkb19lbnRyZW5hbWllbnRvNiwKICBlbnRyZW5hbWllbnRvJHRhcmdldAopCgptY3JlNgpgYGAKCiMjIE1hdHJpeiBkZSBjb25mdXNpw7NuIC0gUHJ1ZWJhCgpgYGB7cn0KbWNycDYgPC0gY29uZnVzaW9uTWF0cml4KAogIHJlc3VsdGFkb19wcnVlYmE2LAogIHBydWViYSR0YXJnZXQKKQoKbWNycDYKYGBgCgojIDxzcGFuIHN0eWxlPSJjb2xvcjpkYXJrIj5UYWJsYSBkZSBSZXN1bHRhZG9zPC9zcGFuPgoKRW4gZXN0YSB0YWJsYSBzZSBjb21wYXJhIGxhIGV4YWN0aXR1ZCBvYnRlbmlkYSBwb3IgY2FkYSBtb2RlbG8gdGFudG8gZW4gZW50cmVuYW1pZW50byBjb21vIGVuIHBydWViYS4KCmBgYHtyfQpyZXN1bHRhZG9zIDwtIGRhdGEuZnJhbWUoCiAgc3ZtTGluZWFyID0gdW5uYW1lKAogICAgYygKICAgICAgbWNyZTEkb3ZlcmFsbFsiQWNjdXJhY3kiXSwKICAgICAgbWNycDEkb3ZlcmFsbFsiQWNjdXJhY3kiXQogICAgKQogICksCgogIHN2bVJhZGlhbCA9IHVubmFtZSgKICAgIGMoCiAgICAgIG1jcmUyJG92ZXJhbGxbIkFjY3VyYWN5Il0sCiAgICAgIG1jcnAyJG92ZXJhbGxbIkFjY3VyYWN5Il0KICAgICkKICApLAoKICBzdm1Qb2x5ID0gdW5uYW1lKAogICAgYygKICAgICAgbWNyZTMkb3ZlcmFsbFsiQWNjdXJhY3kiXSwKICAgICAgbWNycDMkb3ZlcmFsbFsiQWNjdXJhY3kiXQogICAgKQogICksCgogIHJwYXJ0ID0gdW5uYW1lKAogICAgYygKICAgICAgbWNyZTQkb3ZlcmFsbFsiQWNjdXJhY3kiXSwKICAgICAgbWNycDQkb3ZlcmFsbFsiQWNjdXJhY3kiXQogICAgKQogICksCgogIHJmID0gdW5uYW1lKAogICAgYygKICAgICAgbWNyZTUkb3ZlcmFsbFsiQWNjdXJhY3kiXSwKICAgICAgbWNycDUkb3ZlcmFsbFsiQWNjdXJhY3kiXQogICAgKQogICksCgogIG5uZXQgPSB1bm5hbWUoCiAgICBjKAogICAgICBtY3JlNiRvdmVyYWxsWyJBY2N1cmFjeSJdLAogICAgICBtY3JwNiRvdmVyYWxsWyJBY2N1cmFjeSJdCiAgICApCiAgKQopCgpyb3duYW1lcyhyZXN1bHRhZG9zKSA8LSBjKAogICJFeGFjdGl0dWQgZGVsIEVudHJlbmFtaWVudG8iLAogICJFeGFjdGl0dWQgZGUgbGEgUHJ1ZWJhIgopCgpyZXN1bHRhZG9zCmBgYAoKIyA8c3BhbiBzdHlsZT0iY29sb3I6ZGFyayI+TWVqb3IgTW9kZWxvPC9zcGFuPgoKYGBge3J9CmFjY3VyYWN5X3BydWViYSA8LSBjKAogIHN2bUxpbmVhciA9IG1jcnAxJG92ZXJhbGxbIkFjY3VyYWN5Il0sCiAgc3ZtUmFkaWFsID0gbWNycDIkb3ZlcmFsbFsiQWNjdXJhY3kiXSwKICBzdm1Qb2x5ID0gbWNycDMkb3ZlcmFsbFsiQWNjdXJhY3kiXSwKICBycGFydCA9IG1jcnA0JG92ZXJhbGxbIkFjY3VyYWN5Il0sCiAgcmYgPSBtY3JwNSRvdmVyYWxsWyJBY2N1cmFjeSJdLAogIG5uZXQgPSBtY3JwNiRvdmVyYWxsWyJBY2N1cmFjeSJdCikKCm1lam9yX21vZGVsbyA8LSBuYW1lcygKICB3aGljaC5tYXgoYWNjdXJhY3lfcHJ1ZWJhKQopCgptZWpvcl9hY2N1cmFjeSA8LSBtYXgoCiAgYWNjdXJhY3lfcHJ1ZWJhCikKCm1lam9yX21vZGVsbwoKbWVqb3JfYWNjdXJhY3kKYGBgCgojIDxzcGFuIHN0eWxlPSJjb2xvcjpkYXJrIj5Db25jbHVzaW9uZXM8L3NwYW4+CgpEZXNwdcOpcyBkZSBjb21wYXJhciBsb3Mgc2VpcyBtb2RlbG9zLCBzZSBwdWVkZSBpZGVudGlmaWNhciBjdcOhbCBwcmVzZW50YSBsYSBtYXlvciBleGFjdGl0dWQgZW4gbG9zIGRhdG9zIGRlIHBydWViYS4KCkVuIGxvcyByZXN1bHRhZG9zIGRlbCBlamVyY2ljaW8gZGUgcmVmZXJlbmNpYSBzZSBvYnR1dmllcm9uIGFwcm94aW1hZGFtZW50ZToKCiogKipTVk0gTGluZWFsOioqIDg0LjglCiogKipTVk0gUmFkaWFsOioqIDkyLjIlCiogKipTVk0gUG9saW7Ds21pY286KiogOTcuMSUKKiAqKsOBcmJvbCBkZSBEZWNpc2nDs246KiogODguMiUKKiAqKlJhbmRvbSBGb3Jlc3Q6KiogMTAwJQoqICoqUmVkZXMgTmV1cm9uYWxlczoqKiA5Ni42JQoKUG9yIGxvIHRhbnRvLCBlbCBtb2RlbG8gZGUgKipSYW5kb20gRm9yZXN0IG8gQm9zcXVlcyBBbGVhdG9yaW9zKiogcHJlc2VudGEgZWwgbWVqb3IgcmVzdWx0YWRvIHBhcmEgbGEgY2xhc2lmaWNhY2nDs24gZGUgcGFjaWVudGVzIGVuIGVzdGEgYmFzZSBkZSBkYXRvcy4gOmNvbnRlbnRSZWZlcmVuY2Vbb2FpY2l0ZTozXXtpbmRleD0zfQoKU2luIGVtYmFyZ28sIGVzIGltcG9ydGFudGUgY29tcGFyYXIgcHJpbmNpcGFsbWVudGUgZWwgcmVzdWx0YWRvIG9idGVuaWRvIGNvbiBsb3MgZGF0b3MgZGUgKipwcnVlYmEqKiwgeWEgcXVlIGVzdG9zIHJlcHJlc2VudGFuIGluZm9ybWFjacOzbiBxdWUgZWwgbW9kZWxvIG5vIHV0aWxpesOzIGRpcmVjdGFtZW50ZSBkdXJhbnRlIHN1IGVudHJlbmFtaWVudG8u