Suport Vectot Machine

Creado: 23-04-2021

En este post se explicara como realiar una clasificación de clientes que van a comprar un cierto producto. Esta clasificación se realizará con el algoritmo SVM.

Introduccion

El método de clasificación-regresión Máquinas de Vector Soporte (Vector Support Machines, SVMs) fue desarrollado en la década de los 90, dentro de campo de la ciencia computacional. Si bien originariamente se desarrolló como un método de clasificación binaria, su aplicación se ha extendido a problemas de clasificación múltiple y regresión. SVMs ha resultado ser uno de los mejores clasificadores para un amplio abanico de situaciones, por lo que se considera uno de los referentes dentro del ámbito de aprendizaje estadístico y machine learning.

Las Máquinas de Vector Soporte se fundamentan en el Maximal Margin Classifier, que a su vez, se basa en el concepto de hiperplano. A lo largo de este ensayo se introducen por orden cada uno de estos conceptos. Comprender los fundamentos de las SVMs requiere de conocimientos sólidos en álgebra lineal. En este ensayo no se profundiza en el aspecto matemático, pero puede encontrarse una descripción detallada en el libro Support Vector Machines Succinctly by Alexandre Kowalczyk.

En R, las librerías e1071 y LiblineaR contienen los algoritmos necesarios para obtener modelos de clasificación simple, múltiple y regresión, basados en Support Vector Machines.

Base de datos

set.seed(2021)
library(readr)
data<- read.csv("Social_Network_Ads.csv")
library(tidyverse)
data%>% DT::datatable(extensions = 'Buttons',
            options = list(dom = 'Blfrtip',
                           buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
                           lengthMenu = list(c(10,25,50,-1),
                                             c(10,25,50,"All"))))
datos= data 
datos$Purchased= ifelse(datos$Purchased==1,"Compra","No compra")
datos %>% ggplot(aes(EstimatedSalary,Age))+
  geom_point(mapping = aes(color=Purchased),size=1.8)+labs(title = "Problema a clasificar")+theme_minimal()

library(cowplot) 
# Main plot
pmain <- ggplot(datos, aes(x = EstimatedSalary, y = Age, color =Purchased))+
  geom_point()+
  ggpubr::color_palette("jco")+theme_cowplot()
# Marginal densities along x axis
xdens <- axis_canvas(pmain, axis = "x")+
  geom_density(data = datos, aes(x = EstimatedSalary, fill = Purchased),
              alpha = 0.7, size = 0.2)+
  ggpubr::fill_palette("jco")
# Marginal densities along y axis
# Need to set coord_flip = TRUE, if you plan to use coord_flip()
ydens <- axis_canvas(pmain, axis = "y", coord_flip = TRUE)+
  geom_density(data = datos, aes(x = Age, fill = Purchased),
                alpha = 0.7, size = 0.2)+
  coord_flip()+
  ggpubr::fill_palette("jco")
p1 <- insert_xaxis_grob(pmain, xdens, grid::unit(.2, "null"), position = "top")
p2<- insert_yaxis_grob(p1, ydens, grid::unit(.2, "null"), position = "right")
ggdraw(p2)

Kernel lineal

data= data[,3:5]
data$Purchased = factor(data$Purchased, levels = c(0, 1))
library(caret)
d<- createDataPartition(data$Purchased,p=0.8,list = FALSE)
data_train<- data[d,]
data_test<- data[-d,]
library(e1071)
classifier = svm(formula = Purchased ~ .,
                 data = data_train,
                 type = 'C-classification',
                 kernel = 'linear',scale = T)
y_pred = predict(classifier, newdata = data_train[-3])
library(hrbrthemes)
rango_X1 <- range(data_train$EstimatedSalary)
rango_X2 <- range(data_train$Age)

# Interpolación de puntos
new_x1 <- seq(from = rango_X1[1], to = rango_X1[2], length = 75)
new_x2 <- seq(from = rango_X2[1], to = rango_X2[2], length = 75)
nuevos_puntos <- expand.grid(EstimatedSalary = new_x1, Age = new_x2)

# Predicción según el modelo de los nuevos puntos
predicciones <- predict(object = classifier, newdata = nuevos_puntos)

# Se almacenan los puntos predichos para el color de las regiones en un dataframe
color_regiones <- data.frame(nuevos_puntos, y = predicciones)

a2<- ggplot() +
  # Representación de las 2 regiones empleando los puntos y coloreándolos
  # según la clase predicha por el modelo
  geom_point(data = color_regiones, aes(x = EstimatedSalary, y = Age, color = as.factor(y)),
             size = 0.5,alpha=2) +
  # Se añaden las observaciones
  geom_point(data = data_train, aes(x = EstimatedSalary, y = Age, color = as.factor(Purchased)),
             size = 2.5)  +
  theme_modern_rc(grid = F) +
  theme(legend.position = "none")
a2

draw_confusion_matrix <- function(cm) {
  
  layout(matrix(c(1,1,2)))
  par(mar=c(2,2,2,2))
  plot(c(100, 345), c(300, 450), type = "n", xlab="", ylab="", xaxt='n', yaxt='n')
  title('CONFUSION MATRIX', cex.main=2)
  
  # create the matrix 
  rect(150, 430, 240, 370, col='#3F97D0')
  text(195, 435, 'Class1', cex=1.2)
  rect(250, 430, 340, 370, col='#F7AD50')
  text(295, 435, 'Class2', cex=1.2)
  text(125, 370, 'Predicted', cex=1.3, srt=90, font=2)
  text(245, 450, 'Actual', cex=1.3, font=2)
  rect(150, 305, 240, 365, col='#F7AD50')
  rect(250, 305, 340, 365, col='#3F97D0')
  text(140, 400, 'Class1', cex=1.2, srt=90)
  text(140, 335, 'Class2', cex=1.2, srt=90)
  
  # add in the cm results 
  res <- as.numeric(cm$table)
  text(195, 400, res[1], cex=1.6, font=2, col='white')
  text(195, 335, res[2], cex=1.6, font=2, col='white')
  text(295, 400, res[3], cex=1.6, font=2, col='white')
  text(295, 335, res[4], cex=1.6, font=2, col='white')
  
  # add in the specifics 
  plot(c(100, 0), c(100, 0), type = "n", xlab="", ylab="", main = "DETAILS", xaxt='n', yaxt='n')
  text(10, 85, names(cm$byClass[1]), cex=1.2, font=2)
  text(10, 70, round(as.numeric(cm$byClass[1]), 3), cex=1.2)
  text(30, 85, names(cm$byClass[2]), cex=1.2, font=2)
  text(30, 70, round(as.numeric(cm$byClass[2]), 3), cex=1.2)
  text(50, 85, names(cm$byClass[5]), cex=1.2, font=2)
  text(50, 70, round(as.numeric(cm$byClass[5]), 3), cex=1.2)
  text(70, 85, names(cm$byClass[6]), cex=1.2, font=2)
  text(70, 70, round(as.numeric(cm$byClass[6]), 3), cex=1.2)
  text(90, 85, names(cm$byClass[7]), cex=1.2, font=2)
  text(90, 70, round(as.numeric(cm$byClass[7]), 3), cex=1.2)
  
  # add in the accuracy information 
  text(30, 35, names(cm$overall[1]), cex=1.5, font=2)
  text(30, 20, round(as.numeric(cm$overall[1]), 3), cex=1.4)
  text(70, 35, names(cm$overall[2]), cex=1.5, font=2)
  text(70, 20, round(as.numeric(cm$overall[2]), 3), cex=1.4)
}  
draw_confusion_matrix(confusionMatrix(data_train$Purchased, y_pred))

y_pred<- predict(classifier,data_test[,-3])
draw_confusion_matrix(confusionMatrix(data_test[,3],y_pred))

rango_X1 <- range(data_test$EstimatedSalary)
rango_X2 <- range(data_test$Age)

# Interpolación de puntos
new_x1 <- seq(from = rango_X1[1], to = rango_X1[2], length = 75)
new_x2 <- seq(from = rango_X2[1], to = rango_X2[2], length = 75)
nuevos_puntos <- expand.grid(EstimatedSalary = new_x1, Age = new_x2)

# Predicción según el modelo de los nuevos puntos
predicciones <- predict(object = classifier, newdata = nuevos_puntos)

# Se almacenan los puntos predichos para el color de las regiones en un dataframe
color_regiones <- data.frame(nuevos_puntos, y = predicciones)

a1<- ggplot() +
  # Representación de las 2 regiones empleando los puntos y coloreándolos
  # según la clase predicha por el modelo
  geom_point(data = color_regiones, aes(x = EstimatedSalary, y = Age, color = as.factor(y)),
             size = 0.5,alpha=2) +
  # Se añaden las observaciones
  geom_point(data = data_test, aes(x = EstimatedSalary, y = Age, color = as.factor(Purchased)),
             size = 2.5)  +
  theme_modern_rc(grid = F) +
  theme(legend.position = "none")
a1

Kernel Radial

classifier = svm(formula = Purchased ~ .,
                 data = data_train,
                 type = 'C-classification',
                 kernel = 'radial',scale = T)
y_pred = predict(classifier, newdata = data_train[-3])
rango_X1 <- range(data_train$EstimatedSalary)
rango_X2 <- range(data_train$Age)

# Interpolación de puntos
new_x1 <- seq(from = rango_X1[1], to = rango_X1[2], length = 75)
new_x2 <- seq(from = rango_X2[1], to = rango_X2[2], length = 75)
nuevos_puntos <- expand.grid(EstimatedSalary = new_x1, Age = new_x2)

# Predicción según el modelo de los nuevos puntos
predicciones <- predict(object = classifier, newdata = nuevos_puntos)

# Se almacenan los puntos predichos para el color de las regiones en un dataframe
color_regiones <- data.frame(nuevos_puntos, y = predicciones)

a3<- ggplot() +
  # Representación de las 2 regiones empleando los puntos y coloreándolos
  # según la clase predicha por el modelo
  geom_point(data = color_regiones, aes(x = EstimatedSalary, y = Age, color = as.factor(y)),
             size = 0.5,alpha=2) +
  # Se añaden las observaciones
  geom_point(data = data_train, aes(x = EstimatedSalary, y = Age, color = as.factor(Purchased)),
             size = 2.5)  +
  theme_modern_rc(grid = F) +
  theme(legend.position = "none")
a3

draw_confusion_matrix(confusionMatrix(data_train$Purchased, y_pred))

y_pred<- predict(classifier,data_test[,-3])
draw_confusion_matrix(confusionMatrix(data_test[,3],y_pred))

rango_X1 <- range(data_test$EstimatedSalary)
rango_X2 <- range(data_test$Age)

# Interpolación de puntos
new_x1 <- seq(from = rango_X1[1], to = rango_X1[2], length = 75)
new_x2 <- seq(from = rango_X2[1], to = rango_X2[2], length = 75)
nuevos_puntos <- expand.grid(EstimatedSalary = new_x1, Age = new_x2)

# Predicción según el modelo de los nuevos puntos
predicciones <- predict(object = classifier, newdata = nuevos_puntos)

# Se almacenan los puntos predichos para el color de las regiones en un dataframe
color_regiones <- data.frame(nuevos_puntos, y = predicciones)

a4<- ggplot() +
  # Representación de las 2 regiones empleando los puntos y coloreándolos
  # según la clase predicha por el modelo
  geom_point(data = color_regiones, aes(x = EstimatedSalary, y = Age, color = as.factor(y)),
             size = 0.5,alpha=2) +
  # Se añaden las observaciones
  geom_point(data = data_test, aes(x = EstimatedSalary, y = Age, color = as.factor(Purchased)),
             size = 2.5)  +
  theme_modern_rc(grid = F) +
  theme(legend.position = "none")
a4

Los mejores parámetros

svm_cv<- tune("svm", Purchased ~ ., data = data_train, kernel = 'linear',
               ranges = list(cost = c(0.0001, 0.0005, 0.001, 0.01, 0.1, 1,1.5,2)))

ggplot(data = svm_cv$performances, aes(x = cost, y = error)) +
  geom_line() +
  geom_point() +
  labs(title = "Error de clasificación vs hiperparámetro C") +
  theme_minimal_grid()

svm_cv$best.parameters
##   cost
## 6    1
classifier_1<- svm_cv$best.model
svm_cv <- tune("svm", Purchased ~., data = data_train, kernel = 'radial',
               ranges = list(cost = c(0.001, 0.01, 0.1, 1, 5, 10, 20),
                             gamma = c(0.5, 1, 2, 3, 4, 5, 10)))

ggplot(data = svm_cv$performances, aes(x = cost, y = error, color = as.factor(gamma)))+
  geom_line() +
  geom_point() +
  labs(title = "Error de clasificación vs hiperparámetros C y gamma", color = "gamma") +
  theme_modern_rc() +
  theme(legend.position = "bottom")

svm_cv$best.parameters
##    cost gamma
## 13   10     1
classifier_2<- svm_cv$best.model
rango_X1 <- range(data_test$EstimatedSalary)
rango_X2 <- range(data_test$Age)

# Interpolación de puntos
new_x1 <- seq(from = rango_X1[1], to = rango_X1[2], length = 75)
new_x2 <- seq(from = rango_X2[1], to = rango_X2[2], length = 75)
nuevos_puntos <- expand.grid(EstimatedSalary = new_x1, Age = new_x2)

# Predicción según el modelo de los nuevos puntos
predicciones <- predict(object = classifier_1, newdata = nuevos_puntos)

# Se almacenan los puntos predichos para el color de las regiones en un dataframe
color_regiones <- data.frame(nuevos_puntos, y = predicciones)

a5<- ggplot() +
  # Representación de las 2 regiones empleando los puntos y coloreándolos
  # según la clase predicha por el modelo
  geom_point(data = color_regiones, aes(x = EstimatedSalary, y = Age, color = as.factor(y)),
             size = 0.5,alpha=2) +
  # Se añaden las observaciones
  geom_point(data = data_test, aes(x = EstimatedSalary, y = Age, color = as.factor(Purchased)),
             size = 2.5)  +
  theme_modern_rc(grid = F) +
  theme(legend.position = "none")
a5

rango_X1 <- range(data_test$EstimatedSalary)
rango_X2 <- range(data_test$Age)

# Interpolación de puntos
new_x1 <- seq(from = rango_X1[1], to = rango_X1[2], length = 75)
new_x2 <- seq(from = rango_X2[1], to = rango_X2[2], length = 75)
nuevos_puntos <- expand.grid(EstimatedSalary = new_x1, Age = new_x2)

# Predicción según el modelo de los nuevos puntos
predicciones <- predict(object = classifier_2, newdata = nuevos_puntos)

# Se almacenan los puntos predichos para el color de las regiones en un dataframe
color_regiones <- data.frame(nuevos_puntos, y = predicciones)

a6<- ggplot() +
  # Representación de las 2 regiones empleando los puntos y coloreándolos
  # según la clase predicha por el modelo
  geom_point(data = color_regiones, aes(x = EstimatedSalary, y = Age, color = as.factor(y)),
             size = 0.5,alpha=2) +
  # Se añaden las observaciones
  geom_point(data = data_test, aes(x = EstimatedSalary, y = Age, color = as.factor(Purchased)),
             size = 2.5)  +
  theme_modern_rc(grid = F) +
  theme(legend.position = "none")
a6

Comparación de los resultados

ata<- ggpubr::ggarrange(a4,a6,a1,a5)
final_plot <- ggpubr::annotate_figure(ata, top = ggpubr::text_grob("Comparación de modelos", size = 15))
final_plot

LS0tDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICAjIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogemVuYnVybg0KICAgICMgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImZsYXRseSINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFLHdhcm5pbmc9VFJVRSxtZXNzYWdlPVRSVUUsY29tbWVudD1OVUxMfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgY2FjaGUgPSBUUlVFKQ0KDQpgYGANCg0KPGNlbnRlcj4NCiFbXShiZDBjODhiNS0yZWQyLTQ5MmUtYWY3Ny1jODA4ZGU3ZWQ4M2MuanBnKQ0KPC9jZW50ZXI+DQo8Y2VudGVyPg0KPGJyPg0KPGgxPjxzcGFuIHN0eWxlPSJjb2xvcjpibHVlIj4qX19TdXBvcnQgVmVjdG90IE1hY2hpbmUgX18qPC9zcGFuPjwvaDE+DQo8L2NlbnRlcj4NCjxjZW50ZXI+DQo8aT5DcmVhZG86ICAgICAyMy0wNC0yMDIxIA0KPC9jZW50ZXI+DQoNCkVuIGVzdGUgcG9zdCBzZSBleHBsaWNhcmEgY29tbyByZWFsaWFyIHVuYSBjbGFzaWZpY2FjacOzbiBkZSBjbGllbnRlcyBxdWUgdmFuIGEgY29tcHJhciB1biBjaWVydG8gcHJvZHVjdG8uIEVzdGEgKipjbGFzaWZpY2FjacOzbioqIHNlIHJlYWxpemFyw6EgY29uIGVsIGFsZ29yaXRtbyAqKlNWTSoqLiANCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6Ymx1ZSI+Kl9fSW50cm9kdWNjaW9uIF9fKjwvc3Bhbj4NCg0KRWwgbcOpdG9kbyBkZSBjbGFzaWZpY2FjacOzbi1yZWdyZXNpw7NuIE3DoXF1aW5hcyBkZSBWZWN0b3IgU29wb3J0ZSAoVmVjdG9yIFN1cHBvcnQgTWFjaGluZXMsIFNWTXMpIGZ1ZSBkZXNhcnJvbGxhZG8gZW4gbGEgZMOpY2FkYSBkZSBsb3MgOTAsIGRlbnRybyBkZSBjYW1wbyBkZSBsYSBjaWVuY2lhIGNvbXB1dGFjaW9uYWwuIFNpIGJpZW4gb3JpZ2luYXJpYW1lbnRlIHNlIGRlc2Fycm9sbMOzIGNvbW8gdW4gbcOpdG9kbyBkZSBjbGFzaWZpY2FjacOzbiBiaW5hcmlhLCBzdSBhcGxpY2FjacOzbiBzZSBoYSBleHRlbmRpZG8gYSBwcm9ibGVtYXMgZGUgY2xhc2lmaWNhY2nDs24gbcO6bHRpcGxlIHkgcmVncmVzacOzbi4gU1ZNcyBoYSByZXN1bHRhZG8gc2VyIHVubyBkZSBsb3MgbWVqb3JlcyBjbGFzaWZpY2Fkb3JlcyBwYXJhIHVuIGFtcGxpbyBhYmFuaWNvIGRlIHNpdHVhY2lvbmVzLCBwb3IgbG8gcXVlIHNlIGNvbnNpZGVyYSB1bm8gZGUgbG9zIHJlZmVyZW50ZXMgZGVudHJvIGRlbCDDoW1iaXRvIGRlIGFwcmVuZGl6YWplIGVzdGFkw61zdGljbyB5IG1hY2hpbmUgbGVhcm5pbmcuDQoNCkxhcyBNw6FxdWluYXMgZGUgVmVjdG9yIFNvcG9ydGUgc2UgZnVuZGFtZW50YW4gZW4gZWwgTWF4aW1hbCBNYXJnaW4gQ2xhc3NpZmllciwgcXVlIGEgc3UgdmV6LCBzZSBiYXNhIGVuIGVsIGNvbmNlcHRvIGRlIGhpcGVycGxhbm8uIEEgbG8gbGFyZ28gZGUgZXN0ZSBlbnNheW8gc2UgaW50cm9kdWNlbiBwb3Igb3JkZW4gY2FkYSB1bm8gZGUgZXN0b3MgY29uY2VwdG9zLiBDb21wcmVuZGVyIGxvcyBmdW5kYW1lbnRvcyBkZSBsYXMgU1ZNcyByZXF1aWVyZSBkZSBjb25vY2ltaWVudG9zIHPDs2xpZG9zIGVuIMOhbGdlYnJhIGxpbmVhbC4gRW4gZXN0ZSBlbnNheW8gbm8gc2UgcHJvZnVuZGl6YSBlbiBlbCBhc3BlY3RvIG1hdGVtw6F0aWNvLCBwZXJvIHB1ZWRlIGVuY29udHJhcnNlIHVuYSBkZXNjcmlwY2nDs24gZGV0YWxsYWRhIGVuIGVsIGxpYnJvIFN1cHBvcnQgVmVjdG9yIE1hY2hpbmVzIFN1Y2NpbmN0bHkgYnkgQWxleGFuZHJlIEtvd2FsY3p5ay4NCg0KRW4gKipSKiosIGxhcyBsaWJyZXLDrWFzICoqZTEwNzEqKiB5ICoqTGlibGluZWFSKiogY29udGllbmVuIGxvcyBhbGdvcml0bW9zIG5lY2VzYXJpb3MgcGFyYSBvYnRlbmVyIG1vZGVsb3MgZGUgY2xhc2lmaWNhY2nDs24gc2ltcGxlLCBtw7psdGlwbGUgeSByZWdyZXNpw7NuLCBiYXNhZG9zIGVuIFN1cHBvcnQgVmVjdG9yIE1hY2hpbmVzLg0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjpibHVlIj4qX19CYXNlIGRlIGRhdG9zICBfXyo8L3NwYW4+DQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMjAyMSkNCmxpYnJhcnkocmVhZHIpDQpkYXRhPC0gcmVhZC5jc3YoIlNvY2lhbF9OZXR3b3JrX0Fkcy5jc3YiKQ0KYGBgDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpkYXRhJT4lIERUOjpkYXRhdGFibGUoZXh0ZW5zaW9ucyA9ICdCdXR0b25zJywNCiAgICAgICAgICAgIG9wdGlvbnMgPSBsaXN0KGRvbSA9ICdCbGZydGlwJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ1dHRvbnMgPSBjKCdjb3B5JywgJ2NzdicsICdleGNlbCcsICdwZGYnLCAncHJpbnQnKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGxlbmd0aE1lbnUgPSBsaXN0KGMoMTAsMjUsNTAsLTEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygxMCwyNSw1MCwiQWxsIikpKSkNCmBgYA0KDQpgYGB7cn0NCmRhdG9zPSBkYXRhIA0KZGF0b3MkUHVyY2hhc2VkPSBpZmVsc2UoZGF0b3MkUHVyY2hhc2VkPT0xLCJDb21wcmEiLCJObyBjb21wcmEiKQ0KZGF0b3MgJT4lIGdncGxvdChhZXMoRXN0aW1hdGVkU2FsYXJ5LEFnZSkpKw0KICBnZW9tX3BvaW50KG1hcHBpbmcgPSBhZXMoY29sb3I9UHVyY2hhc2VkKSxzaXplPTEuOCkrbGFicyh0aXRsZSA9ICJQcm9ibGVtYSBhIGNsYXNpZmljYXIiKSt0aGVtZV9taW5pbWFsKCkNCmBgYA0KDQpgYGB7cn0NCmxpYnJhcnkoY293cGxvdCkgDQojIE1haW4gcGxvdA0KcG1haW4gPC0gZ2dwbG90KGRhdG9zLCBhZXMoeCA9IEVzdGltYXRlZFNhbGFyeSwgeSA9IEFnZSwgY29sb3IgPVB1cmNoYXNlZCkpKw0KICBnZW9tX3BvaW50KCkrDQogIGdncHVicjo6Y29sb3JfcGFsZXR0ZSgiamNvIikrdGhlbWVfY293cGxvdCgpDQojIE1hcmdpbmFsIGRlbnNpdGllcyBhbG9uZyB4IGF4aXMNCnhkZW5zIDwtIGF4aXNfY2FudmFzKHBtYWluLCBheGlzID0gIngiKSsNCiAgZ2VvbV9kZW5zaXR5KGRhdGEgPSBkYXRvcywgYWVzKHggPSBFc3RpbWF0ZWRTYWxhcnksIGZpbGwgPSBQdXJjaGFzZWQpLA0KICAgICAgICAgICAgICBhbHBoYSA9IDAuNywgc2l6ZSA9IDAuMikrDQogIGdncHVicjo6ZmlsbF9wYWxldHRlKCJqY28iKQ0KIyBNYXJnaW5hbCBkZW5zaXRpZXMgYWxvbmcgeSBheGlzDQojIE5lZWQgdG8gc2V0IGNvb3JkX2ZsaXAgPSBUUlVFLCBpZiB5b3UgcGxhbiB0byB1c2UgY29vcmRfZmxpcCgpDQp5ZGVucyA8LSBheGlzX2NhbnZhcyhwbWFpbiwgYXhpcyA9ICJ5IiwgY29vcmRfZmxpcCA9IFRSVUUpKw0KICBnZW9tX2RlbnNpdHkoZGF0YSA9IGRhdG9zLCBhZXMoeCA9IEFnZSwgZmlsbCA9IFB1cmNoYXNlZCksDQogICAgICAgICAgICAgICAgYWxwaGEgPSAwLjcsIHNpemUgPSAwLjIpKw0KICBjb29yZF9mbGlwKCkrDQogIGdncHVicjo6ZmlsbF9wYWxldHRlKCJqY28iKQ0KcDEgPC0gaW5zZXJ0X3hheGlzX2dyb2IocG1haW4sIHhkZW5zLCBncmlkOjp1bml0KC4yLCAibnVsbCIpLCBwb3NpdGlvbiA9ICJ0b3AiKQ0KcDI8LSBpbnNlcnRfeWF4aXNfZ3JvYihwMSwgeWRlbnMsIGdyaWQ6OnVuaXQoLjIsICJudWxsIiksIHBvc2l0aW9uID0gInJpZ2h0IikNCmdnZHJhdyhwMikNCmBgYA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjpibHVlIj4qX19LZXJuZWwgbGluZWFsIF9fKjwvc3Bhbj4NCg0KYGBge3J9DQpkYXRhPSBkYXRhWywzOjVdDQpkYXRhJFB1cmNoYXNlZCA9IGZhY3RvcihkYXRhJFB1cmNoYXNlZCwgbGV2ZWxzID0gYygwLCAxKSkNCmBgYA0KDQpgYGB7cn0NCmxpYnJhcnkoY2FyZXQpDQpkPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihkYXRhJFB1cmNoYXNlZCxwPTAuOCxsaXN0ID0gRkFMU0UpDQpkYXRhX3RyYWluPC0gZGF0YVtkLF0NCmRhdGFfdGVzdDwtIGRhdGFbLWQsXQ0KYGBgDQoNCmBgYHtyfQ0KbGlicmFyeShlMTA3MSkNCmNsYXNzaWZpZXIgPSBzdm0oZm9ybXVsYSA9IFB1cmNoYXNlZCB+IC4sDQogICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhX3RyYWluLA0KICAgICAgICAgICAgICAgICB0eXBlID0gJ0MtY2xhc3NpZmljYXRpb24nLA0KICAgICAgICAgICAgICAgICBrZXJuZWwgPSAnbGluZWFyJyxzY2FsZSA9IFQpDQp5X3ByZWQgPSBwcmVkaWN0KGNsYXNzaWZpZXIsIG5ld2RhdGEgPSBkYXRhX3RyYWluWy0zXSkNCg0KYGBgDQoNCmBgYHtyfQ0KbGlicmFyeShocmJydGhlbWVzKQ0KcmFuZ29fWDEgPC0gcmFuZ2UoZGF0YV90cmFpbiRFc3RpbWF0ZWRTYWxhcnkpDQpyYW5nb19YMiA8LSByYW5nZShkYXRhX3RyYWluJEFnZSkNCg0KIyBJbnRlcnBvbGFjacOzbiBkZSBwdW50b3MNCm5ld194MSA8LSBzZXEoZnJvbSA9IHJhbmdvX1gxWzFdLCB0byA9IHJhbmdvX1gxWzJdLCBsZW5ndGggPSA3NSkNCm5ld194MiA8LSBzZXEoZnJvbSA9IHJhbmdvX1gyWzFdLCB0byA9IHJhbmdvX1gyWzJdLCBsZW5ndGggPSA3NSkNCm51ZXZvc19wdW50b3MgPC0gZXhwYW5kLmdyaWQoRXN0aW1hdGVkU2FsYXJ5ID0gbmV3X3gxLCBBZ2UgPSBuZXdfeDIpDQoNCiMgUHJlZGljY2nDs24gc2Vnw7puIGVsIG1vZGVsbyBkZSBsb3MgbnVldm9zIHB1bnRvcw0KcHJlZGljY2lvbmVzIDwtIHByZWRpY3Qob2JqZWN0ID0gY2xhc3NpZmllciwgbmV3ZGF0YSA9IG51ZXZvc19wdW50b3MpDQoNCiMgU2UgYWxtYWNlbmFuIGxvcyBwdW50b3MgcHJlZGljaG9zIHBhcmEgZWwgY29sb3IgZGUgbGFzIHJlZ2lvbmVzIGVuIHVuIGRhdGFmcmFtZQ0KY29sb3JfcmVnaW9uZXMgPC0gZGF0YS5mcmFtZShudWV2b3NfcHVudG9zLCB5ID0gcHJlZGljY2lvbmVzKQ0KDQphMjwtIGdncGxvdCgpICsNCiAgIyBSZXByZXNlbnRhY2nDs24gZGUgbGFzIDIgcmVnaW9uZXMgZW1wbGVhbmRvIGxvcyBwdW50b3MgeSBjb2xvcmXDoW5kb2xvcw0KICAjIHNlZ8O6biBsYSBjbGFzZSBwcmVkaWNoYSBwb3IgZWwgbW9kZWxvDQogIGdlb21fcG9pbnQoZGF0YSA9IGNvbG9yX3JlZ2lvbmVzLCBhZXMoeCA9IEVzdGltYXRlZFNhbGFyeSwgeSA9IEFnZSwgY29sb3IgPSBhcy5mYWN0b3IoeSkpLA0KICAgICAgICAgICAgIHNpemUgPSAwLjUsYWxwaGE9MikgKw0KICAjIFNlIGHDsWFkZW4gbGFzIG9ic2VydmFjaW9uZXMNCiAgZ2VvbV9wb2ludChkYXRhID0gZGF0YV90cmFpbiwgYWVzKHggPSBFc3RpbWF0ZWRTYWxhcnksIHkgPSBBZ2UsIGNvbG9yID0gYXMuZmFjdG9yKFB1cmNoYXNlZCkpLA0KICAgICAgICAgICAgIHNpemUgPSAyLjUpICArDQogIHRoZW1lX21vZGVybl9yYyhncmlkID0gRikgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpDQphMg0KYGBgDQoNCmBgYHtyfQ0KZHJhd19jb25mdXNpb25fbWF0cml4IDwtIGZ1bmN0aW9uKGNtKSB7DQogIA0KICBsYXlvdXQobWF0cml4KGMoMSwxLDIpKSkNCiAgcGFyKG1hcj1jKDIsMiwyLDIpKQ0KICBwbG90KGMoMTAwLCAzNDUpLCBjKDMwMCwgNDUwKSwgdHlwZSA9ICJuIiwgeGxhYj0iIiwgeWxhYj0iIiwgeGF4dD0nbicsIHlheHQ9J24nKQ0KICB0aXRsZSgnQ09ORlVTSU9OIE1BVFJJWCcsIGNleC5tYWluPTIpDQogIA0KICAjIGNyZWF0ZSB0aGUgbWF0cml4IA0KICByZWN0KDE1MCwgNDMwLCAyNDAsIDM3MCwgY29sPScjM0Y5N0QwJykNCiAgdGV4dCgxOTUsIDQzNSwgJ0NsYXNzMScsIGNleD0xLjIpDQogIHJlY3QoMjUwLCA0MzAsIDM0MCwgMzcwLCBjb2w9JyNGN0FENTAnKQ0KICB0ZXh0KDI5NSwgNDM1LCAnQ2xhc3MyJywgY2V4PTEuMikNCiAgdGV4dCgxMjUsIDM3MCwgJ1ByZWRpY3RlZCcsIGNleD0xLjMsIHNydD05MCwgZm9udD0yKQ0KICB0ZXh0KDI0NSwgNDUwLCAnQWN0dWFsJywgY2V4PTEuMywgZm9udD0yKQ0KICByZWN0KDE1MCwgMzA1LCAyNDAsIDM2NSwgY29sPScjRjdBRDUwJykNCiAgcmVjdCgyNTAsIDMwNSwgMzQwLCAzNjUsIGNvbD0nIzNGOTdEMCcpDQogIHRleHQoMTQwLCA0MDAsICdDbGFzczEnLCBjZXg9MS4yLCBzcnQ9OTApDQogIHRleHQoMTQwLCAzMzUsICdDbGFzczInLCBjZXg9MS4yLCBzcnQ9OTApDQogIA0KICAjIGFkZCBpbiB0aGUgY20gcmVzdWx0cyANCiAgcmVzIDwtIGFzLm51bWVyaWMoY20kdGFibGUpDQogIHRleHQoMTk1LCA0MDAsIHJlc1sxXSwgY2V4PTEuNiwgZm9udD0yLCBjb2w9J3doaXRlJykNCiAgdGV4dCgxOTUsIDMzNSwgcmVzWzJdLCBjZXg9MS42LCBmb250PTIsIGNvbD0nd2hpdGUnKQ0KICB0ZXh0KDI5NSwgNDAwLCByZXNbM10sIGNleD0xLjYsIGZvbnQ9MiwgY29sPSd3aGl0ZScpDQogIHRleHQoMjk1LCAzMzUsIHJlc1s0XSwgY2V4PTEuNiwgZm9udD0yLCBjb2w9J3doaXRlJykNCiAgDQogICMgYWRkIGluIHRoZSBzcGVjaWZpY3MgDQogIHBsb3QoYygxMDAsIDApLCBjKDEwMCwgMCksIHR5cGUgPSAibiIsIHhsYWI9IiIsIHlsYWI9IiIsIG1haW4gPSAiREVUQUlMUyIsIHhheHQ9J24nLCB5YXh0PSduJykNCiAgdGV4dCgxMCwgODUsIG5hbWVzKGNtJGJ5Q2xhc3NbMV0pLCBjZXg9MS4yLCBmb250PTIpDQogIHRleHQoMTAsIDcwLCByb3VuZChhcy5udW1lcmljKGNtJGJ5Q2xhc3NbMV0pLCAzKSwgY2V4PTEuMikNCiAgdGV4dCgzMCwgODUsIG5hbWVzKGNtJGJ5Q2xhc3NbMl0pLCBjZXg9MS4yLCBmb250PTIpDQogIHRleHQoMzAsIDcwLCByb3VuZChhcy5udW1lcmljKGNtJGJ5Q2xhc3NbMl0pLCAzKSwgY2V4PTEuMikNCiAgdGV4dCg1MCwgODUsIG5hbWVzKGNtJGJ5Q2xhc3NbNV0pLCBjZXg9MS4yLCBmb250PTIpDQogIHRleHQoNTAsIDcwLCByb3VuZChhcy5udW1lcmljKGNtJGJ5Q2xhc3NbNV0pLCAzKSwgY2V4PTEuMikNCiAgdGV4dCg3MCwgODUsIG5hbWVzKGNtJGJ5Q2xhc3NbNl0pLCBjZXg9MS4yLCBmb250PTIpDQogIHRleHQoNzAsIDcwLCByb3VuZChhcy5udW1lcmljKGNtJGJ5Q2xhc3NbNl0pLCAzKSwgY2V4PTEuMikNCiAgdGV4dCg5MCwgODUsIG5hbWVzKGNtJGJ5Q2xhc3NbN10pLCBjZXg9MS4yLCBmb250PTIpDQogIHRleHQoOTAsIDcwLCByb3VuZChhcy5udW1lcmljKGNtJGJ5Q2xhc3NbN10pLCAzKSwgY2V4PTEuMikNCiAgDQogICMgYWRkIGluIHRoZSBhY2N1cmFjeSBpbmZvcm1hdGlvbiANCiAgdGV4dCgzMCwgMzUsIG5hbWVzKGNtJG92ZXJhbGxbMV0pLCBjZXg9MS41LCBmb250PTIpDQogIHRleHQoMzAsIDIwLCByb3VuZChhcy5udW1lcmljKGNtJG92ZXJhbGxbMV0pLCAzKSwgY2V4PTEuNCkNCiAgdGV4dCg3MCwgMzUsIG5hbWVzKGNtJG92ZXJhbGxbMl0pLCBjZXg9MS41LCBmb250PTIpDQogIHRleHQoNzAsIDIwLCByb3VuZChhcy5udW1lcmljKGNtJG92ZXJhbGxbMl0pLCAzKSwgY2V4PTEuNCkNCn0gIA0KYGBgDQoNCmBgYHtyfQ0KZHJhd19jb25mdXNpb25fbWF0cml4KGNvbmZ1c2lvbk1hdHJpeChkYXRhX3RyYWluJFB1cmNoYXNlZCwgeV9wcmVkKSkNCmBgYA0KDQpgYGB7cn0NCnlfcHJlZDwtIHByZWRpY3QoY2xhc3NpZmllcixkYXRhX3Rlc3RbLC0zXSkNCmRyYXdfY29uZnVzaW9uX21hdHJpeChjb25mdXNpb25NYXRyaXgoZGF0YV90ZXN0WywzXSx5X3ByZWQpKQ0KYGBgDQoNCmBgYHtyfQ0KcmFuZ29fWDEgPC0gcmFuZ2UoZGF0YV90ZXN0JEVzdGltYXRlZFNhbGFyeSkNCnJhbmdvX1gyIDwtIHJhbmdlKGRhdGFfdGVzdCRBZ2UpDQoNCiMgSW50ZXJwb2xhY2nDs24gZGUgcHVudG9zDQpuZXdfeDEgPC0gc2VxKGZyb20gPSByYW5nb19YMVsxXSwgdG8gPSByYW5nb19YMVsyXSwgbGVuZ3RoID0gNzUpDQpuZXdfeDIgPC0gc2VxKGZyb20gPSByYW5nb19YMlsxXSwgdG8gPSByYW5nb19YMlsyXSwgbGVuZ3RoID0gNzUpDQpudWV2b3NfcHVudG9zIDwtIGV4cGFuZC5ncmlkKEVzdGltYXRlZFNhbGFyeSA9IG5ld194MSwgQWdlID0gbmV3X3gyKQ0KDQojIFByZWRpY2Npw7NuIHNlZ8O6biBlbCBtb2RlbG8gZGUgbG9zIG51ZXZvcyBwdW50b3MNCnByZWRpY2Npb25lcyA8LSBwcmVkaWN0KG9iamVjdCA9IGNsYXNzaWZpZXIsIG5ld2RhdGEgPSBudWV2b3NfcHVudG9zKQ0KDQojIFNlIGFsbWFjZW5hbiBsb3MgcHVudG9zIHByZWRpY2hvcyBwYXJhIGVsIGNvbG9yIGRlIGxhcyByZWdpb25lcyBlbiB1biBkYXRhZnJhbWUNCmNvbG9yX3JlZ2lvbmVzIDwtIGRhdGEuZnJhbWUobnVldm9zX3B1bnRvcywgeSA9IHByZWRpY2Npb25lcykNCg0KYTE8LSBnZ3Bsb3QoKSArDQogICMgUmVwcmVzZW50YWNpw7NuIGRlIGxhcyAyIHJlZ2lvbmVzIGVtcGxlYW5kbyBsb3MgcHVudG9zIHkgY29sb3Jlw6FuZG9sb3MNCiAgIyBzZWfDum4gbGEgY2xhc2UgcHJlZGljaGEgcG9yIGVsIG1vZGVsbw0KICBnZW9tX3BvaW50KGRhdGEgPSBjb2xvcl9yZWdpb25lcywgYWVzKHggPSBFc3RpbWF0ZWRTYWxhcnksIHkgPSBBZ2UsIGNvbG9yID0gYXMuZmFjdG9yKHkpKSwNCiAgICAgICAgICAgICBzaXplID0gMC41LGFscGhhPTIpICsNCiAgIyBTZSBhw7FhZGVuIGxhcyBvYnNlcnZhY2lvbmVzDQogIGdlb21fcG9pbnQoZGF0YSA9IGRhdGFfdGVzdCwgYWVzKHggPSBFc3RpbWF0ZWRTYWxhcnksIHkgPSBBZ2UsIGNvbG9yID0gYXMuZmFjdG9yKFB1cmNoYXNlZCkpLA0KICAgICAgICAgICAgIHNpemUgPSAyLjUpICArDQogIHRoZW1lX21vZGVybl9yYyhncmlkID0gRikgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpDQphMQ0KYGBgDQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPipfX0tlcm5lbCBSYWRpYWwgX18qPC9zcGFuPg0KDQpgYGB7cn0NCmNsYXNzaWZpZXIgPSBzdm0oZm9ybXVsYSA9IFB1cmNoYXNlZCB+IC4sDQogICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhX3RyYWluLA0KICAgICAgICAgICAgICAgICB0eXBlID0gJ0MtY2xhc3NpZmljYXRpb24nLA0KICAgICAgICAgICAgICAgICBrZXJuZWwgPSAncmFkaWFsJyxzY2FsZSA9IFQpDQp5X3ByZWQgPSBwcmVkaWN0KGNsYXNzaWZpZXIsIG5ld2RhdGEgPSBkYXRhX3RyYWluWy0zXSkNCmBgYA0KDQpgYGB7cn0NCnJhbmdvX1gxIDwtIHJhbmdlKGRhdGFfdHJhaW4kRXN0aW1hdGVkU2FsYXJ5KQ0KcmFuZ29fWDIgPC0gcmFuZ2UoZGF0YV90cmFpbiRBZ2UpDQoNCiMgSW50ZXJwb2xhY2nDs24gZGUgcHVudG9zDQpuZXdfeDEgPC0gc2VxKGZyb20gPSByYW5nb19YMVsxXSwgdG8gPSByYW5nb19YMVsyXSwgbGVuZ3RoID0gNzUpDQpuZXdfeDIgPC0gc2VxKGZyb20gPSByYW5nb19YMlsxXSwgdG8gPSByYW5nb19YMlsyXSwgbGVuZ3RoID0gNzUpDQpudWV2b3NfcHVudG9zIDwtIGV4cGFuZC5ncmlkKEVzdGltYXRlZFNhbGFyeSA9IG5ld194MSwgQWdlID0gbmV3X3gyKQ0KDQojIFByZWRpY2Npw7NuIHNlZ8O6biBlbCBtb2RlbG8gZGUgbG9zIG51ZXZvcyBwdW50b3MNCnByZWRpY2Npb25lcyA8LSBwcmVkaWN0KG9iamVjdCA9IGNsYXNzaWZpZXIsIG5ld2RhdGEgPSBudWV2b3NfcHVudG9zKQ0KDQojIFNlIGFsbWFjZW5hbiBsb3MgcHVudG9zIHByZWRpY2hvcyBwYXJhIGVsIGNvbG9yIGRlIGxhcyByZWdpb25lcyBlbiB1biBkYXRhZnJhbWUNCmNvbG9yX3JlZ2lvbmVzIDwtIGRhdGEuZnJhbWUobnVldm9zX3B1bnRvcywgeSA9IHByZWRpY2Npb25lcykNCg0KYTM8LSBnZ3Bsb3QoKSArDQogICMgUmVwcmVzZW50YWNpw7NuIGRlIGxhcyAyIHJlZ2lvbmVzIGVtcGxlYW5kbyBsb3MgcHVudG9zIHkgY29sb3Jlw6FuZG9sb3MNCiAgIyBzZWfDum4gbGEgY2xhc2UgcHJlZGljaGEgcG9yIGVsIG1vZGVsbw0KICBnZW9tX3BvaW50KGRhdGEgPSBjb2xvcl9yZWdpb25lcywgYWVzKHggPSBFc3RpbWF0ZWRTYWxhcnksIHkgPSBBZ2UsIGNvbG9yID0gYXMuZmFjdG9yKHkpKSwNCiAgICAgICAgICAgICBzaXplID0gMC41LGFscGhhPTIpICsNCiAgIyBTZSBhw7FhZGVuIGxhcyBvYnNlcnZhY2lvbmVzDQogIGdlb21fcG9pbnQoZGF0YSA9IGRhdGFfdHJhaW4sIGFlcyh4ID0gRXN0aW1hdGVkU2FsYXJ5LCB5ID0gQWdlLCBjb2xvciA9IGFzLmZhY3RvcihQdXJjaGFzZWQpKSwNCiAgICAgICAgICAgICBzaXplID0gMi41KSAgKw0KICB0aGVtZV9tb2Rlcm5fcmMoZ3JpZCA9IEYpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KYTMNCmBgYA0KDQpgYGB7cn0NCmRyYXdfY29uZnVzaW9uX21hdHJpeChjb25mdXNpb25NYXRyaXgoZGF0YV90cmFpbiRQdXJjaGFzZWQsIHlfcHJlZCkpDQpgYGANCg0KYGBge3J9DQp5X3ByZWQ8LSBwcmVkaWN0KGNsYXNzaWZpZXIsZGF0YV90ZXN0WywtM10pDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY29uZnVzaW9uTWF0cml4KGRhdGFfdGVzdFssM10seV9wcmVkKSkNCmBgYA0KDQpgYGB7cn0NCnJhbmdvX1gxIDwtIHJhbmdlKGRhdGFfdGVzdCRFc3RpbWF0ZWRTYWxhcnkpDQpyYW5nb19YMiA8LSByYW5nZShkYXRhX3Rlc3QkQWdlKQ0KDQojIEludGVycG9sYWNpw7NuIGRlIHB1bnRvcw0KbmV3X3gxIDwtIHNlcShmcm9tID0gcmFuZ29fWDFbMV0sIHRvID0gcmFuZ29fWDFbMl0sIGxlbmd0aCA9IDc1KQ0KbmV3X3gyIDwtIHNlcShmcm9tID0gcmFuZ29fWDJbMV0sIHRvID0gcmFuZ29fWDJbMl0sIGxlbmd0aCA9IDc1KQ0KbnVldm9zX3B1bnRvcyA8LSBleHBhbmQuZ3JpZChFc3RpbWF0ZWRTYWxhcnkgPSBuZXdfeDEsIEFnZSA9IG5ld194MikNCg0KIyBQcmVkaWNjacOzbiBzZWfDum4gZWwgbW9kZWxvIGRlIGxvcyBudWV2b3MgcHVudG9zDQpwcmVkaWNjaW9uZXMgPC0gcHJlZGljdChvYmplY3QgPSBjbGFzc2lmaWVyLCBuZXdkYXRhID0gbnVldm9zX3B1bnRvcykNCg0KIyBTZSBhbG1hY2VuYW4gbG9zIHB1bnRvcyBwcmVkaWNob3MgcGFyYSBlbCBjb2xvciBkZSBsYXMgcmVnaW9uZXMgZW4gdW4gZGF0YWZyYW1lDQpjb2xvcl9yZWdpb25lcyA8LSBkYXRhLmZyYW1lKG51ZXZvc19wdW50b3MsIHkgPSBwcmVkaWNjaW9uZXMpDQoNCmE0PC0gZ2dwbG90KCkgKw0KICAjIFJlcHJlc2VudGFjacOzbiBkZSBsYXMgMiByZWdpb25lcyBlbXBsZWFuZG8gbG9zIHB1bnRvcyB5IGNvbG9yZcOhbmRvbG9zDQogICMgc2Vnw7puIGxhIGNsYXNlIHByZWRpY2hhIHBvciBlbCBtb2RlbG8NCiAgZ2VvbV9wb2ludChkYXRhID0gY29sb3JfcmVnaW9uZXMsIGFlcyh4ID0gRXN0aW1hdGVkU2FsYXJ5LCB5ID0gQWdlLCBjb2xvciA9IGFzLmZhY3Rvcih5KSksDQogICAgICAgICAgICAgc2l6ZSA9IDAuNSxhbHBoYT0yKSArDQogICMgU2UgYcOxYWRlbiBsYXMgb2JzZXJ2YWNpb25lcw0KICBnZW9tX3BvaW50KGRhdGEgPSBkYXRhX3Rlc3QsIGFlcyh4ID0gRXN0aW1hdGVkU2FsYXJ5LCB5ID0gQWdlLCBjb2xvciA9IGFzLmZhY3RvcihQdXJjaGFzZWQpKSwNCiAgICAgICAgICAgICBzaXplID0gMi41KSAgKw0KICB0aGVtZV9tb2Rlcm5fcmMoZ3JpZCA9IEYpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KYTQNCmBgYA0KDQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPipfX0xvcyBtZWpvcmVzIHBhcsOhbWV0cm9zX18qPC9zcGFuPg0KDQpgYGB7cn0NCnN2bV9jdjwtIHR1bmUoInN2bSIsIFB1cmNoYXNlZCB+IC4sIGRhdGEgPSBkYXRhX3RyYWluLCBrZXJuZWwgPSAnbGluZWFyJywNCiAgICAgICAgICAgICAgIHJhbmdlcyA9IGxpc3QoY29zdCA9IGMoMC4wMDAxLCAwLjAwMDUsIDAuMDAxLCAwLjAxLCAwLjEsIDEsMS41LDIpKSkNCg0KZ2dwbG90KGRhdGEgPSBzdm1fY3YkcGVyZm9ybWFuY2VzLCBhZXMoeCA9IGNvc3QsIHkgPSBlcnJvcikpICsNCiAgZ2VvbV9saW5lKCkgKw0KICBnZW9tX3BvaW50KCkgKw0KICBsYWJzKHRpdGxlID0gIkVycm9yIGRlIGNsYXNpZmljYWNpw7NuIHZzIGhpcGVycGFyw6FtZXRybyBDIikgKw0KICB0aGVtZV9taW5pbWFsX2dyaWQoKQ0KYGBgDQoNCmBgYHtyfQ0Kc3ZtX2N2JGJlc3QucGFyYW1ldGVycw0KYGBgDQoNCmBgYHtyfQ0KY2xhc3NpZmllcl8xPC0gc3ZtX2N2JGJlc3QubW9kZWwNCmBgYA0KDQoNCmBgYHtyfQ0Kc3ZtX2N2IDwtIHR1bmUoInN2bSIsIFB1cmNoYXNlZCB+LiwgZGF0YSA9IGRhdGFfdHJhaW4sIGtlcm5lbCA9ICdyYWRpYWwnLA0KICAgICAgICAgICAgICAgcmFuZ2VzID0gbGlzdChjb3N0ID0gYygwLjAwMSwgMC4wMSwgMC4xLCAxLCA1LCAxMCwgMjApLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBnYW1tYSA9IGMoMC41LCAxLCAyLCAzLCA0LCA1LCAxMCkpKQ0KDQpnZ3Bsb3QoZGF0YSA9IHN2bV9jdiRwZXJmb3JtYW5jZXMsIGFlcyh4ID0gY29zdCwgeSA9IGVycm9yLCBjb2xvciA9IGFzLmZhY3RvcihnYW1tYSkpKSsNCiAgZ2VvbV9saW5lKCkgKw0KICBnZW9tX3BvaW50KCkgKw0KICBsYWJzKHRpdGxlID0gIkVycm9yIGRlIGNsYXNpZmljYWNpw7NuIHZzIGhpcGVycGFyw6FtZXRyb3MgQyB5IGdhbW1hIiwgY29sb3IgPSAiZ2FtbWEiKSArDQogIHRoZW1lX21vZGVybl9yYygpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIpDQpgYGANCg0KYGBge3J9DQpzdm1fY3YkYmVzdC5wYXJhbWV0ZXJzDQpgYGANCg0KYGBge3J9DQpjbGFzc2lmaWVyXzI8LSBzdm1fY3YkYmVzdC5tb2RlbA0KYGBgDQoNCmBgYHtyfQ0KcmFuZ29fWDEgPC0gcmFuZ2UoZGF0YV90ZXN0JEVzdGltYXRlZFNhbGFyeSkNCnJhbmdvX1gyIDwtIHJhbmdlKGRhdGFfdGVzdCRBZ2UpDQoNCiMgSW50ZXJwb2xhY2nDs24gZGUgcHVudG9zDQpuZXdfeDEgPC0gc2VxKGZyb20gPSByYW5nb19YMVsxXSwgdG8gPSByYW5nb19YMVsyXSwgbGVuZ3RoID0gNzUpDQpuZXdfeDIgPC0gc2VxKGZyb20gPSByYW5nb19YMlsxXSwgdG8gPSByYW5nb19YMlsyXSwgbGVuZ3RoID0gNzUpDQpudWV2b3NfcHVudG9zIDwtIGV4cGFuZC5ncmlkKEVzdGltYXRlZFNhbGFyeSA9IG5ld194MSwgQWdlID0gbmV3X3gyKQ0KDQojIFByZWRpY2Npw7NuIHNlZ8O6biBlbCBtb2RlbG8gZGUgbG9zIG51ZXZvcyBwdW50b3MNCnByZWRpY2Npb25lcyA8LSBwcmVkaWN0KG9iamVjdCA9IGNsYXNzaWZpZXJfMSwgbmV3ZGF0YSA9IG51ZXZvc19wdW50b3MpDQoNCiMgU2UgYWxtYWNlbmFuIGxvcyBwdW50b3MgcHJlZGljaG9zIHBhcmEgZWwgY29sb3IgZGUgbGFzIHJlZ2lvbmVzIGVuIHVuIGRhdGFmcmFtZQ0KY29sb3JfcmVnaW9uZXMgPC0gZGF0YS5mcmFtZShudWV2b3NfcHVudG9zLCB5ID0gcHJlZGljY2lvbmVzKQ0KDQphNTwtIGdncGxvdCgpICsNCiAgIyBSZXByZXNlbnRhY2nDs24gZGUgbGFzIDIgcmVnaW9uZXMgZW1wbGVhbmRvIGxvcyBwdW50b3MgeSBjb2xvcmXDoW5kb2xvcw0KICAjIHNlZ8O6biBsYSBjbGFzZSBwcmVkaWNoYSBwb3IgZWwgbW9kZWxvDQogIGdlb21fcG9pbnQoZGF0YSA9IGNvbG9yX3JlZ2lvbmVzLCBhZXMoeCA9IEVzdGltYXRlZFNhbGFyeSwgeSA9IEFnZSwgY29sb3IgPSBhcy5mYWN0b3IoeSkpLA0KICAgICAgICAgICAgIHNpemUgPSAwLjUsYWxwaGE9MikgKw0KICAjIFNlIGHDsWFkZW4gbGFzIG9ic2VydmFjaW9uZXMNCiAgZ2VvbV9wb2ludChkYXRhID0gZGF0YV90ZXN0LCBhZXMoeCA9IEVzdGltYXRlZFNhbGFyeSwgeSA9IEFnZSwgY29sb3IgPSBhcy5mYWN0b3IoUHVyY2hhc2VkKSksDQogICAgICAgICAgICAgc2l6ZSA9IDIuNSkgICsNCiAgdGhlbWVfbW9kZXJuX3JjKGdyaWQgPSBGKSArDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikNCmE1DQpgYGANCg0KYGBge3J9DQpyYW5nb19YMSA8LSByYW5nZShkYXRhX3Rlc3QkRXN0aW1hdGVkU2FsYXJ5KQ0KcmFuZ29fWDIgPC0gcmFuZ2UoZGF0YV90ZXN0JEFnZSkNCg0KIyBJbnRlcnBvbGFjacOzbiBkZSBwdW50b3MNCm5ld194MSA8LSBzZXEoZnJvbSA9IHJhbmdvX1gxWzFdLCB0byA9IHJhbmdvX1gxWzJdLCBsZW5ndGggPSA3NSkNCm5ld194MiA8LSBzZXEoZnJvbSA9IHJhbmdvX1gyWzFdLCB0byA9IHJhbmdvX1gyWzJdLCBsZW5ndGggPSA3NSkNCm51ZXZvc19wdW50b3MgPC0gZXhwYW5kLmdyaWQoRXN0aW1hdGVkU2FsYXJ5ID0gbmV3X3gxLCBBZ2UgPSBuZXdfeDIpDQoNCiMgUHJlZGljY2nDs24gc2Vnw7puIGVsIG1vZGVsbyBkZSBsb3MgbnVldm9zIHB1bnRvcw0KcHJlZGljY2lvbmVzIDwtIHByZWRpY3Qob2JqZWN0ID0gY2xhc3NpZmllcl8yLCBuZXdkYXRhID0gbnVldm9zX3B1bnRvcykNCg0KIyBTZSBhbG1hY2VuYW4gbG9zIHB1bnRvcyBwcmVkaWNob3MgcGFyYSBlbCBjb2xvciBkZSBsYXMgcmVnaW9uZXMgZW4gdW4gZGF0YWZyYW1lDQpjb2xvcl9yZWdpb25lcyA8LSBkYXRhLmZyYW1lKG51ZXZvc19wdW50b3MsIHkgPSBwcmVkaWNjaW9uZXMpDQoNCmE2PC0gZ2dwbG90KCkgKw0KICAjIFJlcHJlc2VudGFjacOzbiBkZSBsYXMgMiByZWdpb25lcyBlbXBsZWFuZG8gbG9zIHB1bnRvcyB5IGNvbG9yZcOhbmRvbG9zDQogICMgc2Vnw7puIGxhIGNsYXNlIHByZWRpY2hhIHBvciBlbCBtb2RlbG8NCiAgZ2VvbV9wb2ludChkYXRhID0gY29sb3JfcmVnaW9uZXMsIGFlcyh4ID0gRXN0aW1hdGVkU2FsYXJ5LCB5ID0gQWdlLCBjb2xvciA9IGFzLmZhY3Rvcih5KSksDQogICAgICAgICAgICAgc2l6ZSA9IDAuNSxhbHBoYT0yKSArDQogICMgU2UgYcOxYWRlbiBsYXMgb2JzZXJ2YWNpb25lcw0KICBnZW9tX3BvaW50KGRhdGEgPSBkYXRhX3Rlc3QsIGFlcyh4ID0gRXN0aW1hdGVkU2FsYXJ5LCB5ID0gQWdlLCBjb2xvciA9IGFzLmZhY3RvcihQdXJjaGFzZWQpKSwNCiAgICAgICAgICAgICBzaXplID0gMi41KSAgKw0KICB0aGVtZV9tb2Rlcm5fcmMoZ3JpZCA9IEYpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KYTYNCmBgYA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjpibHVlIj4qX19Db21wYXJhY2nDs24gZGUgbG9zIHJlc3VsdGFkb3MgX18qPC9zcGFuPg0KDQpgYGB7cn0NCmF0YTwtIGdncHVicjo6Z2dhcnJhbmdlKGE0LGE2LGExLGE1KQ0KZmluYWxfcGxvdCA8LSBnZ3B1YnI6OmFubm90YXRlX2ZpZ3VyZShhdGEsIHRvcCA9IGdncHVicjo6dGV4dF9ncm9iKCJDb21wYXJhY2nDs24gZGUgbW9kZWxvcyIsIHNpemUgPSAxNSkpDQpmaW5hbF9wbG90DQoNCmBgYA0KDQo=