Convolutional Neural Networks

red convolucional

red convolucional

Vamos a utilizar la librería

KERAS

# devtools::install_github("rstudio/keras")
library(keras)
# install_keras()
library(tidyverse)
── Attaching packages ──────────────────────────────────────────────────────────────────────────── tidyverse 1.2.1 ──
✔ ggplot2 3.0.0     ✔ purrr   0.2.5
✔ tibble  1.4.2     ✔ dplyr   0.7.7
✔ tidyr   0.8.1     ✔ stringr 1.3.1
✔ readr   1.1.1     ✔ forcats 0.3.0
── Conflicts ─────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
library(knitr)

Mnist

Vamos a utilizar nuevamente el dataset de MNIST de la clase de fully connected layers

mnist <- dataset_mnist()
Using TensorFlow backend.
x_train <- mnist$train$x
y_train <- mnist$train$y
x_test <- mnist$test$x
y_test <- mnist$test$y

Recordemos la pinta de los datos

datos de entrada

matrix.rotate <- function(img) { 
    t(apply(img, 2, rev))
}
par(mfrow=c(3, 3))
for (idx in 1:9) {
    label <- y_train[idx]
    image(matrix.rotate(x_train[idx,,]), col = grey(level = seq(1, 0, by=-1/255)), axes=F, main=label)
  
}

El dato esta en un array de 3 dimensiones (imagen,ancho,largo). Como tenemos 60K imágenes, esto tiene la forma de :

dim(x_train)
[1] 60000    28    28

Dimensiones del problema:

Definamos como variables las siguientes dimensiones del problema (nos facilita la reutilización del código):

  • número de clases
  • largo de las imágenes
  • ancho de las imágenes
num_classes <- 10
img_rows <- 28
img_cols <- 28

Data shape

En un problema normal de clasificación para Machine Learning tenemos 2 dimensiones: filas y columnas, donde la 1° representa las observaciones y la segunda la secuencia de features.

En el caso de las redes convolucionales necesitamos datos de 4 dimensiones:

  1. observaciones
  2. largo de la imagen(o matriz)
  3. ancho de la imagen (o matriz)
  4. dimensión del color: en imágenes normales a color: RGB, esta dimensión tiene extensión de 3, por los tres canales de color. En imágenes de blanco y negro, la dimensión es de extensión 1.
x_train <- array_reshape(x_train, c(nrow(x_train), img_rows, img_cols, 1))
x_test <- array_reshape(x_test, c(nrow(x_test), img_rows, img_cols, 1))
input_shape <- c(img_rows, img_cols, 1)
  • Además, necesitamos convertir la escala de los datos de íntegers entre 0 y 255 a números floating point entre 0 y 1
x_train <- x_train / 255
x_test <- x_test / 255
cat('x_train_shape:', dim(x_train), '\n')
x_train_shape: 60000 28 28 1 
cat(nrow(x_train), 'train samples\n')
60000 train samples
cat(nrow(x_test), 'test samples\n')
10000 test samples

datos de salida

necesitamos pasarlo a one-hot encoding esto se hace con la función to_categorical() de Keras

y_train <- to_categorical(y_train, num_classes)
y_test <- to_categorical(y_test, num_classes)

Definción del modelo

Para armar el modelo primero definimos el tipo de modelo. Para eso usamos keras_model_sequential() que nos permite simplemente apilar capas de la red.

  • En la primera capa tenemos que aclarar el input_shape.
  • Las capas se agregan con pipes %>%
  • La última capa tiene la misma cantidad de unidades que categorías nuestro output. La salida del modelo es un vector que asigna una probabilidad a cada una da las categorías
  • En cada capa tenemos que definir una función de activación
  • Además agregamos una regularización layer_droput(x) que lo que hace es, en cada iteración del ajuste, ignorar el x% de las conexiones. Esto evita el sobreajuste del modelo
model <- keras_model_sequential() %>%
  layer_conv_2d(filters = 32, kernel_size = c(3,3), activation = 'relu',
                input_shape = input_shape) %>% 
  layer_conv_2d(filters = 64, kernel_size = c(3,3), activation = 'relu') %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>% 
  layer_dropout(rate = 0.25) %>% 
  layer_flatten() %>% 
  layer_dense(units = 128, activation = 'relu') %>% 
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = num_classes, activation = 'softmax')

La arquitectura de esta red es básicamente la siguiente:

  1. Convolución: 32 filtros (se achica la imagen original, y se multiplica en 32)
  2. Convolución: 64 filtros (se vuelve a achicar y cada resultado del filtro se multiplica por 64)
  3. max_pooling: Se achican las imágenes
  4. dropout: se regulariza para evitar overfitting
  5. flatten: Se aplanan los inputs para poder pasarlos a una red densa
  6. dense: capa densa con 128 neuronas
  7. dropout: se vuelve a regularizar
  8. dense : capa de salida, con tantas neuronas como clases y una activación softmax (para que devuelva probabilidades)

layer_conv_2d

convolución

convolución2

  • La capa de convoluciones construye pequeños filtros o kernels de la dimensión kernel_size() que pasan por el input original realizando una convolución.

  • El filtro barre la imagen original, moviéndose de a strides() posiciones. Por default se mueve de a 1 lugar.

  • Notemos que si el filtro es de 3x3 y el stride es 1, entonces la imagen original va a perder 2 pixels de largo y 2 pixels de ancho.

layer_max_pooling_2d

max pooling

max pooling3

  • Es max pooling es una forma de reducir el tamaño de la matrix.

  • Al igual que la convolución, barre la imagen con una ventana de pool_size() moviéndose de a stride() posiciones, y devuelve el valor más alto.

  • Un pool_size() de 2x2 nos reduce el tamaño de la imagen a la mitad.

layer_dropout

Dropout

Dropout4

  • El dropout es un método de regularización donde para cada iteración del backpropagation, anula el ajuste para una rate proporción de los pesos. De esta forma, no se ajusta todo todo el tiempo, reduciendo los grados de libertad del modelo, y evitando el overfitting

layer_flatten

flatten

flatten5

  • Esta layer lo único que hace es un reshape para que los datos puedan ser utilizados por una capa densa.

layer_dense

dense

dense

  • La capa densa es una fully connected layer que recibe como input el producto aplanado de las capas previas.

Funciones de activación

Para este modelo utilizamos las mismas dos funciones de activación que utilizamos en la FC nn:

  • Rectified Linear Unit: \[f(x)=max(0,x)\]
  • Softmax : \[ f(x)=\frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}}\]

Definidas en código y gráficamente:

relu <- function(x) ifelse(x >= 0, x, 0)
softmax <- function(x) exp(x) / sum(exp(x))
data.frame(x= seq(from=-1, to=1, by=0.1)) %>% 
  mutate(softmax = softmax(x),
         relu = relu(x)) %>% 
  gather(variable,value,2:3) %>% 
ggplot(., aes(x=x, y=value, group=variable, colour=variable))+
  geom_line(size=1) +
  ggtitle("ReLU & Softmax")+
  theme_minimal()

ReLu es la función de activación que más se utiliza en la actualidad.

Parametros entrenables del modelo

model

  • Cada vez que el modelo pasa por una convolución

El modelo tiene 1.2 millones de parámetros para optimizar:

La primera capa convolucional tiene que entrenar los filtros. Como estos eran de 3x3, cada uno tiene 9 parámetros para entrenar + 1 bias por filtro

32* (3*3) +32
[1] 320

La segunda convolución tiene que entrenar kernels de 3x3 para 64 filtros \(64*(3*3)\), para cada uno de los 32 filtros de la capa anterior, +1 bias por filtro

64*(3*3)*32 +64
[1] 18496

layer_max_pooling_2d, layer_dropout y layer_flatten no entrenan parámetros.

cuando aplanamos. El shape pasa a:

12*12*64
[1] 9216
  • La primera capa densa tienen que conectar 9216 nodos con los 128, más los 128 bias.
  • La capa de salida tiene que conectar los 128 nodos con los 10 de salida, más 10 bias
128*9216 +128
[1] 1179776
128*10 +10
[1] 1290

Luego necesitamos compilar el modelo indicando la función de loss, qué tipo de optimizador utilizar, y qué métricas nos importan

model <- model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_adadelta(),
  metrics = c('accuracy')
)

Entrenamiento

Para ajustar el modelo usamos la función fit(), acá necesitamos pasar los siguientes parámetros:

  • El array con los datos de entrenamiento
  • El array con los outputs
  • epochs: Cuantas veces va a recorrer el dataset de entrenamiento
  • batch_size: de a cuantas imágenes va a mirar en cada iteración del backpropagation
  • validation_split: Hacemos un split en train y validation para evaluar las métricas.
epochs <- 12
batch_size <- 128
validation_split <- 0.2

fit_history <- model %>% fit(
  x_train, y_train,
  batch_size = batch_size,
  epochs = epochs,
  validation_split = validation_split
)

Mientras entrenamos el modelo, podemos ver la evolución en el gráfico interactivo que se genera en el viewer de Rstudio.

fit_history
Trained on 48,000 samples, validated on 12,000 samples (batch_size=128, epochs=12)
Final epoch (plot to see history):
     acc: 0.9929
    loss: 0.02294
 val_acc: 0.9902
val_loss: 0.0382 

fit() nos devuelve un objeto que incluye las métricas de loss y accuracy.

Este objeto lo podemos graficar con plot() y nos devuelve un objeto de ggplot, sobre el que podemos seguir trabajando

plot(fit_history)+
  theme_minimal()+
  labs(title= "Evolución de Loss y Accuracy en train y validation")

es importante guardar el modelo luego de entrenar, para poder reutilizarlo

model %>% save_model_hdf5("../Resultados/cnn_model.h5")

y para cargarlo

modelo_preentrenado <- load_model_hdf5("../Resultados/cnn_model.h5")
modelo_preentrenado
Model
_____________________________________________________________________________________________________________________
Layer (type)                                        Output Shape                                   Param #           
=====================================================================================================================
conv2d_1 (Conv2D)                                   (None, 26, 26, 32)                             320               
_____________________________________________________________________________________________________________________
conv2d_2 (Conv2D)                                   (None, 24, 24, 64)                             18496             
_____________________________________________________________________________________________________________________
max_pooling2d_1 (MaxPooling2D)                      (None, 12, 12, 64)                             0                 
_____________________________________________________________________________________________________________________
dropout_3 (Dropout)                                 (None, 12, 12, 64)                             0                 
_____________________________________________________________________________________________________________________
flatten_1 (Flatten)                                 (None, 9216)                                   0                 
_____________________________________________________________________________________________________________________
dense_4 (Dense)                                     (None, 128)                                    1179776           
_____________________________________________________________________________________________________________________
dropout_4 (Dropout)                                 (None, 128)                                    0                 
_____________________________________________________________________________________________________________________
dense_5 (Dense)                                     (None, 10)                                     1290              
=====================================================================================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0
_____________________________________________________________________________________________________________________

Si queremos evaluar el modelo sobre el conjunto de test (distinto del de validación) podemos usar la función evaluate()

modelo_preentrenado %>% evaluate(x_test, y_test)

   32/10000 [..............................] - ETA: 33s
   96/10000 [..............................] - ETA: 16s
  192/10000 [..............................] - ETA: 11s
  288/10000 [..............................] - ETA: 9s 
  384/10000 [>.............................] - ETA: 9s
  480/10000 [>.............................] - ETA: 8s
  576/10000 [>.............................] - ETA: 7s
  672/10000 [=>............................] - ETA: 7s
  768/10000 [=>............................] - ETA: 7s
  864/10000 [=>............................] - ETA: 7s
  960/10000 [=>............................] - ETA: 6s
 1056/10000 [==>...........................] - ETA: 6s
 1152/10000 [==>...........................] - ETA: 6s
 1248/10000 [==>...........................] - ETA: 6s
 1344/10000 [===>..........................] - ETA: 6s
 1440/10000 [===>..........................] - ETA: 5s
 1536/10000 [===>..........................] - ETA: 5s
 1632/10000 [===>..........................] - ETA: 5s
 1728/10000 [====>.........................] - ETA: 5s
 1824/10000 [====>.........................] - ETA: 5s
 1920/10000 [====>.........................] - ETA: 5s
 2016/10000 [=====>........................] - ETA: 5s
 2112/10000 [=====>........................] - ETA: 5s
 2208/10000 [=====>........................] - ETA: 5s
 2304/10000 [=====>........................] - ETA: 5s
 2400/10000 [======>.......................] - ETA: 4s
 2496/10000 [======>.......................] - ETA: 4s
 2592/10000 [======>.......................] - ETA: 4s
 2688/10000 [=======>......................] - ETA: 4s
 2784/10000 [=======>......................] - ETA: 4s
 2880/10000 [=======>......................] - ETA: 4s
 2976/10000 [=======>......................] - ETA: 4s
 3072/10000 [========>.....................] - ETA: 4s
 3168/10000 [========>.....................] - ETA: 4s
 3264/10000 [========>.....................] - ETA: 4s
 3360/10000 [=========>....................] - ETA: 4s
 3456/10000 [=========>....................] - ETA: 4s
 3552/10000 [=========>....................] - ETA: 4s
 3648/10000 [=========>....................] - ETA: 4s
 3744/10000 [==========>...................] - ETA: 3s
 3840/10000 [==========>...................] - ETA: 3s
 3936/10000 [==========>...................] - ETA: 3s
 4032/10000 [===========>..................] - ETA: 3s
 4096/10000 [===========>..................] - ETA: 3s
 4192/10000 [===========>..................] - ETA: 3s
 4288/10000 [===========>..................] - ETA: 3s
 4384/10000 [============>.................] - ETA: 3s
 4480/10000 [============>.................] - ETA: 3s
 4576/10000 [============>.................] - ETA: 3s
 4672/10000 [=============>................] - ETA: 3s
 4768/10000 [=============>................] - ETA: 3s
 4864/10000 [=============>................] - ETA: 3s
 4960/10000 [=============>................] - ETA: 3s
 5056/10000 [==============>...............] - ETA: 3s
 5152/10000 [==============>...............] - ETA: 3s
 5248/10000 [==============>...............] - ETA: 3s
 5344/10000 [===============>..............] - ETA: 2s
 5440/10000 [===============>..............] - ETA: 2s
 5536/10000 [===============>..............] - ETA: 2s
 5632/10000 [===============>..............] - ETA: 2s
 5728/10000 [================>.............] - ETA: 2s
 5824/10000 [================>.............] - ETA: 2s
 5920/10000 [================>.............] - ETA: 2s
 6016/10000 [=================>............] - ETA: 2s
 6112/10000 [=================>............] - ETA: 2s
 6208/10000 [=================>............] - ETA: 2s
 6304/10000 [=================>............] - ETA: 2s
 6400/10000 [==================>...........] - ETA: 2s
 6496/10000 [==================>...........] - ETA: 2s
 6592/10000 [==================>...........] - ETA: 2s
 6688/10000 [===================>..........] - ETA: 2s
 6784/10000 [===================>..........] - ETA: 2s
 6880/10000 [===================>..........] - ETA: 1s
 6976/10000 [===================>..........] - ETA: 1s
 7072/10000 [====================>.........] - ETA: 1s
 7168/10000 [====================>.........] - ETA: 1s
 7264/10000 [====================>.........] - ETA: 1s
 7360/10000 [=====================>........] - ETA: 1s
 7424/10000 [=====================>........] - ETA: 1s
 7520/10000 [=====================>........] - ETA: 1s
 7616/10000 [=====================>........] - ETA: 1s
 7712/10000 [======================>.......] - ETA: 1s
 7776/10000 [======================>.......] - ETA: 1s
 7872/10000 [======================>.......] - ETA: 1s
 7968/10000 [======================>.......] - ETA: 1s
 8064/10000 [=======================>......] - ETA: 1s
 8160/10000 [=======================>......] - ETA: 1s
 8256/10000 [=======================>......] - ETA: 1s
 8352/10000 [========================>.....] - ETA: 1s
 8448/10000 [========================>.....] - ETA: 0s
 8544/10000 [========================>.....] - ETA: 0s
 8640/10000 [========================>.....] - ETA: 0s
 8736/10000 [=========================>....] - ETA: 0s
 8832/10000 [=========================>....] - ETA: 0s
 8928/10000 [=========================>....] - ETA: 0s
 9024/10000 [==========================>...] - ETA: 0s
 9120/10000 [==========================>...] - ETA: 0s
 9216/10000 [==========================>...] - ETA: 0s
 9312/10000 [==========================>...] - ETA: 0s
 9408/10000 [===========================>..] - ETA: 0s
 9504/10000 [===========================>..] - ETA: 0s
 9600/10000 [===========================>..] - ETA: 0s
 9696/10000 [============================>.] - ETA: 0s
 9792/10000 [============================>.] - ETA: 0s
 9888/10000 [============================>.] - ETA: 0s
 9984/10000 [============================>.] - ETA: 0s
10000/10000 [==============================] - 6s 628us/step
$loss
[1] 0.02666184

$acc
[1] 0.9925

Para obtener las predicciones sobre un nuevo conjunto de datos utilizamos predict_classes()

modelo_preentrenado %>% predict_classes(x_test) %>% head(.)
[1] 7 2 1 0 4 1

Otros recursos interesantes:

Visualización de una Red Fully conected para clasificación de dígitos

Tensor Flow Playground

LS0tCnRpdGxlOiAiQ2xhc2UgMTMuIFJlZGVzIE5ldXJvbmFsZXMgSUleW0VzdGFzIG5vdGFzIGVzdGFuIGJhc2FkYXMgZW4gaHR0cHM6Ly90ZW5zb3JmbG93LnJzdHVkaW8uY29tL2tlcmFzLyN0dXRvcmlhbHNdIgphdXRob3I6ICJEaWVnbyBLb3psb3dza2kgeSBKdWFuIEJhcnJpb2xhIgpkYXRlOiA4LTEyLTIwMTgKb3V0cHV0OiAKICBodG1sX25vdGVib29rOiAKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBkZXB0aDogMgotLS0KCmBgYHtyIHBhY2thZ2Vfb3B0aW9ucywgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfa25pdCRzZXQocHJvZ3Jlc3MgPSBUUlVFLCB2ZXJib3NlID0gVFJVRSkKYGBgCgojIENvbnZvbHV0aW9uYWwgTmV1cmFsIE5ldHdvcmtzCgoKIVtyZWQgY29udm9sdWNpb25hbF0oaW1nL2Nubi5wbmcpCgpWYW1vcyBhIHV0aWxpemFyIGxhIGxpYnJlcsOtYSAKCiMjIyMgW19fS0VSQVNfX10oaHR0cHM6Ly9rZXJhcy5yc3R1ZGlvLmNvbS8pCgpgYGB7cn0KIyBkZXZ0b29sczo6aW5zdGFsbF9naXRodWIoInJzdHVkaW8va2VyYXMiKQpsaWJyYXJ5KGtlcmFzKQojIGluc3RhbGxfa2VyYXMoKQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShrbml0cikKYGBgCgoKIyMgTW5pc3QKClZhbW9zIGEgdXRpbGl6YXIgbnVldmFtZW50ZSBlbCBkYXRhc2V0IGRlIE1OSVNUIGRlIGxhIGNsYXNlIGRlIF9mdWxseSBjb25uZWN0ZWQgbGF5ZXJzXwoKYGBge3J9Cm1uaXN0IDwtIGRhdGFzZXRfbW5pc3QoKQp4X3RyYWluIDwtIG1uaXN0JHRyYWluJHgKeV90cmFpbiA8LSBtbmlzdCR0cmFpbiR5CnhfdGVzdCA8LSBtbmlzdCR0ZXN0JHgKeV90ZXN0IDwtIG1uaXN0JHRlc3QkeQpgYGAKCgpSZWNvcmRlbW9zIGxhIHBpbnRhIGRlIGxvcyBkYXRvcwoKX19kYXRvcyBkZSBlbnRyYWRhX18KCmBgYHtyfQptYXRyaXgucm90YXRlIDwtIGZ1bmN0aW9uKGltZykgeyAKICAgIHQoYXBwbHkoaW1nLCAyLCByZXYpKQp9CgpwYXIobWZyb3c9YygzLCAzKSkKZm9yIChpZHggaW4gMTo5KSB7CiAgICBsYWJlbCA8LSB5X3RyYWluW2lkeF0KICAgIGltYWdlKG1hdHJpeC5yb3RhdGUoeF90cmFpbltpZHgsLF0pLCBjb2wgPSBncmV5KGxldmVsID0gc2VxKDEsIDAsIGJ5PS0xLzI1NSkpLCBheGVzPUYsIG1haW49bGFiZWwpCiAgCn0KYGBgCgoKCkVsIGRhdG8gZXN0YSBlbiB1biBhcnJheSBkZSAzIGRpbWVuc2lvbmVzIChpbWFnZW4sYW5jaG8sbGFyZ28pLiBDb21vIHRlbmVtb3MgNjBLIGltw6FnZW5lcywgZXN0byB0aWVuZSBsYSBmb3JtYSBkZSA6CgpgYGB7cn0KZGltKHhfdHJhaW4pCmBgYAoKCkRpbWVuc2lvbmVzIGRlbCBwcm9ibGVtYTogCgpEZWZpbmFtb3MgY29tbyB2YXJpYWJsZXMgbGFzIHNpZ3VpZW50ZXMgZGltZW5zaW9uZXMgZGVsIHByb2JsZW1hIChub3MgZmFjaWxpdGEgbGEgcmV1dGlsaXphY2nDs24gZGVsIGPDs2RpZ28pOgoKLSBuw7ptZXJvIGRlIGNsYXNlcwotIGxhcmdvIGRlIGxhcyBpbcOhZ2VuZXMKLSBhbmNobyBkZSBsYXMgaW3DoWdlbmVzCgpgYGB7cn0KbnVtX2NsYXNzZXMgPC0gMTAKaW1nX3Jvd3MgPC0gMjgKaW1nX2NvbHMgPC0gMjgKYGBgCgoKIyMjIERhdGEgc2hhcGUKCkVuIHVuIHByb2JsZW1hIG5vcm1hbCBkZSBjbGFzaWZpY2FjacOzbiBwYXJhIE1hY2hpbmUgTGVhcm5pbmcgdGVuZW1vcyAyIGRpbWVuc2lvbmVzOiBfZmlsYXMgeSBjb2x1bW5hc18sIGRvbmRlIGxhIDHCsCByZXByZXNlbnRhIGxhcyBvYnNlcnZhY2lvbmVzIHkgbGEgc2VndW5kYSBsYSBzZWN1ZW5jaWEgZGUgZmVhdHVyZXMuIAoKRW4gZWwgY2FzbyBkZSBsYXMgcmVkZXMgY29udm9sdWNpb25hbGVzIG5lY2VzaXRhbW9zIGRhdG9zIGRlIDQgZGltZW5zaW9uZXM6CgoxLiBvYnNlcnZhY2lvbmVzCjIuIGxhcmdvIGRlIGxhIGltYWdlbihvIG1hdHJpeikKMy4gYW5jaG8gZGUgbGEgaW1hZ2VuIChvIG1hdHJpeikKNC4gZGltZW5zacOzbiBkZWwgY29sb3I6IGVuIGltw6FnZW5lcyBub3JtYWxlcyBhIGNvbG9yOiBSR0IsIGVzdGEgZGltZW5zacOzbiB0aWVuZSBleHRlbnNpw7NuIGRlIDMsIHBvciBsb3MgdHJlcyBjYW5hbGVzIGRlIGNvbG9yLiBFbiBpbcOhZ2VuZXMgZGUgYmxhbmNvIHkgbmVncm8sIGxhIGRpbWVuc2nDs24gZXMgZGUgZXh0ZW5zacOzbiAxLiAKCmBgYHtyfQp4X3RyYWluIDwtIGFycmF5X3Jlc2hhcGUoeF90cmFpbiwgYyhucm93KHhfdHJhaW4pLCBpbWdfcm93cywgaW1nX2NvbHMsIDEpKQp4X3Rlc3QgPC0gYXJyYXlfcmVzaGFwZSh4X3Rlc3QsIGMobnJvdyh4X3Rlc3QpLCBpbWdfcm93cywgaW1nX2NvbHMsIDEpKQppbnB1dF9zaGFwZSA8LSBjKGltZ19yb3dzLCBpbWdfY29scywgMSkKYGBgCgotIEFkZW3DoXMsIG5lY2VzaXRhbW9zIGNvbnZlcnRpciBsYSBlc2NhbGEgZGUgbG9zIGRhdG9zIGRlIMOtbnRlZ2VycyBlbnRyZSAwIHkgMjU1IGEgbsO6bWVyb3MgZmxvYXRpbmcgcG9pbnQgZW50cmUgMCB5IDEKCmBgYHtyfQoKeF90cmFpbiA8LSB4X3RyYWluIC8gMjU1CnhfdGVzdCA8LSB4X3Rlc3QgLyAyNTUKCmBgYAoKYGBge3J9CmNhdCgneF90cmFpbl9zaGFwZTonLCBkaW0oeF90cmFpbiksICdcbicpCmNhdChucm93KHhfdHJhaW4pLCAndHJhaW4gc2FtcGxlc1xuJykKY2F0KG5yb3coeF90ZXN0KSwgJ3Rlc3Qgc2FtcGxlc1xuJykKCmBgYAoKCl9fZGF0b3MgZGUgc2FsaWRhX18KCm5lY2VzaXRhbW9zIHBhc2FybG8gYSBfX29uZS1ob3QgZW5jb2RpbmdfXyBlc3RvIHNlIGhhY2UgY29uIGxhIGZ1bmNpw7NuIGB0b19jYXRlZ29yaWNhbCgpYCBkZSBLZXJhcwoKYGBge3J9CnlfdHJhaW4gPC0gdG9fY2F0ZWdvcmljYWwoeV90cmFpbiwgbnVtX2NsYXNzZXMpCnlfdGVzdCA8LSB0b19jYXRlZ29yaWNhbCh5X3Rlc3QsIG51bV9jbGFzc2VzKQpgYGAKCiMjIERlZmluY2nDs24gZGVsIG1vZGVsbwoKClBhcmEgYXJtYXIgZWwgbW9kZWxvIHByaW1lcm8gZGVmaW5pbW9zIGVsIHRpcG8gZGUgbW9kZWxvLiBQYXJhIGVzbyB1c2Ftb3MgYGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKWAgcXVlIG5vcyBwZXJtaXRlIHNpbXBsZW1lbnRlIGFwaWxhciBjYXBhcyBkZSBsYSByZWQuIAoKLSBFbiBsYSBwcmltZXJhIGNhcGEgdGVuZW1vcyBxdWUgYWNsYXJhciBlbCBpbnB1dF9zaGFwZS4KLSBMYXMgY2FwYXMgc2UgYWdyZWdhbiBjb24gcGlwZXMgYCU+JWAKLSBMYSDDumx0aW1hIGNhcGEgdGllbmUgbGEgbWlzbWEgY2FudGlkYWQgZGUgdW5pZGFkZXMgcXVlIGNhdGVnb3LDrWFzIG51ZXN0cm8gb3V0cHV0LiBMYSBzYWxpZGEgZGVsIG1vZGVsbyBlcyB1biB2ZWN0b3IgcXVlIGFzaWduYSB1bmEgcHJvYmFiaWxpZGFkIGEgY2FkYSB1bmEgZGEgbGFzIGNhdGVnb3LDrWFzCi0gRW4gY2FkYSBjYXBhIHRlbmVtb3MgcXVlIGRlZmluaXIgdW5hIGZ1bmNpw7NuIGRlIGFjdGl2YWNpw7NuCi0gQWRlbcOhcyBhZ3JlZ2Ftb3MgdW5hIHJlZ3VsYXJpemFjacOzbiBgbGF5ZXJfZHJvcHV0KHgpYCBxdWUgbG8gcXVlIGhhY2UgZXMsIGVuIGNhZGEgaXRlcmFjacOzbiBkZWwgYWp1c3RlLCBpZ25vcmFyIGVsIHglIGRlIGxhcyBjb25leGlvbmVzLiBFc3RvIGV2aXRhIGVsIHNvYnJlYWp1c3RlIGRlbCBtb2RlbG8KCmBgYHtyfQoKbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDMyLCBrZXJuZWxfc2l6ZSA9IGMoMywzKSwgYWN0aXZhdGlvbiA9ICdyZWx1JywKICAgICAgICAgICAgICAgIGlucHV0X3NoYXBlID0gaW5wdXRfc2hhcGUpICU+JSAKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSA2NCwga2VybmVsX3NpemUgPSBjKDMsMyksIGFjdGl2YXRpb24gPSAncmVsdScpICU+JSAKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUgCiAgbGF5ZXJfZHJvcG91dChyYXRlID0gMC4yNSkgJT4lIAogIGxheWVyX2ZsYXR0ZW4oKSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMjgsIGFjdGl2YXRpb24gPSAncmVsdScpICU+JSAKICBsYXllcl9kcm9wb3V0KHJhdGUgPSAwLjUpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IG51bV9jbGFzc2VzLCBhY3RpdmF0aW9uID0gJ3NvZnRtYXgnKQoKCmBgYAoKCkxhIGFycXVpdGVjdHVyYSBkZSBlc3RhIHJlZCBlcyBiw6FzaWNhbWVudGUgbGEgc2lndWllbnRlOgoKMS4gX19Db252b2x1Y2nDs25fXzogMzIgZmlsdHJvcyAoc2UgYWNoaWNhIGxhIGltYWdlbiBvcmlnaW5hbCwgeSBzZSBtdWx0aXBsaWNhIGVuIDMyKQoyLiBfX0NvbnZvbHVjacOzbl9fOiA2NCBmaWx0cm9zIChzZSB2dWVsdmUgYSBhY2hpY2FyIHkgY2FkYSByZXN1bHRhZG8gZGVsIGZpbHRybyBzZSBtdWx0aXBsaWNhIHBvciA2NCkKMy4gX19tYXhfcG9vbGluZ19fOiBTZSBhY2hpY2FuIGxhcyBpbcOhZ2VuZXMKNC4gX19kcm9wb3V0X186IHNlIHJlZ3VsYXJpemEgcGFyYSBldml0YXIgb3ZlcmZpdHRpbmcKNS4gX19mbGF0dGVuX186IFNlIGFwbGFuYW4gbG9zIGlucHV0cyBwYXJhIHBvZGVyIHBhc2FybG9zIGEgdW5hIHJlZCBkZW5zYQo2LiBfX2RlbnNlX186IGNhcGEgZGVuc2EgY29uIDEyOCBuZXVyb25hcwo3LiBfX2Ryb3BvdXRfXzogc2UgdnVlbHZlIGEgcmVndWxhcml6YXIKOC4gX19kZW5zZV9fIDogY2FwYSBkZSBzYWxpZGEsIGNvbiB0YW50YXMgbmV1cm9uYXMgY29tbyBjbGFzZXMgeSB1bmEgYWN0aXZhY2nDs24gc29mdG1heCAocGFyYSBxdWUgZGV2dWVsdmEgcHJvYmFiaWxpZGFkZXMpCgoKIyMjIGBsYXllcl9jb252XzJkYAoKIVtjb252b2x1Y2nDs25eW2h0dHA6Ly93d3cudGJsdWNoZS5jb20vZmlsZXMvTWVldHVwU2FvUGF1bG8yMDE3LnBkZl1dKGltZy9jb252b2x1dGlvbi5wbmcpCgoKLSBMYSBjYXBhIGRlIGNvbnZvbHVjaW9uZXMgY29uc3RydXllIHBlcXVlw7FvcyBfZmlsdHJvc18gbyBfa2VybmVsc18gZGUgbGEgZGltZW5zacOzbiBga2VybmVsX3NpemUoKWAgcXVlIHBhc2FuIHBvciBlbCBpbnB1dCBvcmlnaW5hbCByZWFsaXphbmRvIHVuYSBfY29udm9sdWNpw7NuXy4gCgotIEVsIGZpbHRybyBfYmFycmVfIGxhIGltYWdlbiBvcmlnaW5hbCwgbW92acOpbmRvc2UgZGUgYSBgc3RyaWRlcygpYCBwb3NpY2lvbmVzLiBQb3IgZGVmYXVsdCBzZSBtdWV2ZSBkZSBhIDEgbHVnYXIuCgotIE5vdGVtb3MgcXVlIHNpIGVsIGZpbHRybyBlcyBkZSAzeDMgeSBlbCBzdHJpZGUgZXMgMSwgZW50b25jZXMgbGEgaW1hZ2VuIG9yaWdpbmFsIHZhIGEgcGVyZGVyIDIgcGl4ZWxzIGRlIGxhcmdvIHkgMiBwaXhlbHMgZGUgYW5jaG8uCgoKIyMjIGBsYXllcl9tYXhfcG9vbGluZ18yZGAKCiFbbWF4IHBvb2xpbmdeW2h0dHBzOi8vY29tcHV0ZXJzY2llbmNld2lraS5vcmcvaW5kZXgucGhwL0ZpbGU6TWF4cG9vbFNhbXBsZTIucG5nXV0oaW1nL01heHBvb2xTYW1wbGUyLnBuZykKCi0gRXMgbWF4IHBvb2xpbmcgZXMgdW5hIGZvcm1hIGRlIHJlZHVjaXIgZWwgdGFtYcOxbyBkZSBsYSBtYXRyaXguCgotIEFsIGlndWFsIHF1ZSBsYSBjb252b2x1Y2nDs24sIF9iYXJyZV8gbGEgaW1hZ2VuIGNvbiB1bmEgdmVudGFuYSBkZSBgcG9vbF9zaXplKClgIG1vdmnDqW5kb3NlIGRlIGEgYHN0cmlkZSgpYCBwb3NpY2lvbmVzLCB5IGRldnVlbHZlIGVsIHZhbG9yIG3DoXMgYWx0by4gCgotIFVuIGBwb29sX3NpemUoKWAgZGUgMngyIG5vcyByZWR1Y2UgZWwgdGFtYcOxbyBkZSBsYSBpbWFnZW4gYSBsYSBtaXRhZC4KCiMjIyBgbGF5ZXJfZHJvcG91dGAKCiFbRHJvcG91dF5baHR0cDovL2ptbHIub3JnL3BhcGVycy92b2x1bWUxNS9zcml2YXN0YXZhMTRhLm9sZC9zcml2YXN0YXZhMTRhLnBkZl1dKGltZy9kcm9wb3V0LnBuZykKCi0gRWwgZHJvcG91dCBlcyB1biBtw6l0b2RvIGRlIHJlZ3VsYXJpemFjacOzbiBkb25kZSBwYXJhIGNhZGEgaXRlcmFjacOzbiBkZWwgYmFja3Byb3BhZ2F0aW9uLCBhbnVsYSBlbCBhanVzdGUgcGFyYSB1bmEgYHJhdGVgIHByb3BvcmNpw7NuIGRlIGxvcyBwZXNvcy4gRGUgZXN0YSBmb3JtYSwgbm8gc2UgYWp1c3RhIHRvZG8gdG9kbyBlbCB0aWVtcG8sIHJlZHVjaWVuZG8gbG9zIGdyYWRvcyBkZSBsaWJlcnRhZCBkZWwgbW9kZWxvLCB5IGV2aXRhbmRvIGVsIG92ZXJmaXR0aW5nCgoKIyMjIGBsYXllcl9mbGF0dGVuYAoKIVtmbGF0dGVuXltodHRwczovL3J1Ymlrc2NvZGUubmV0LzIwMTgvMDIvMjYvaW50cm9kdWN0aW9uLXRvLWNvbnZvbHV0aW9uYWwtbmV1cmFsLW5ldHdvcmtzL11dKGltZy9mbGF0dGVuLnBuZykKCgotIEVzdGEgbGF5ZXIgbG8gw7puaWNvIHF1ZSBoYWNlIGVzIHVuIHJlc2hhcGUgcGFyYSBxdWUgbG9zIGRhdG9zIHB1ZWRhbiBzZXIgdXRpbGl6YWRvcyBwb3IgdW5hIGNhcGEgZGVuc2EuCgoKIyMjIGBsYXllcl9kZW5zZWAKCiFbZGVuc2VdKGltZy9sYXllcl9kZW5zZS5wbmcpCgotIExhIGNhcGEgZGVuc2EgZXMgdW5hIF9mdWxseSBjb25uZWN0ZWQgbGF5ZXJfIHF1ZSByZWNpYmUgY29tbyBpbnB1dCBlbCBwcm9kdWN0byBhcGxhbmFkbyBkZSBsYXMgY2FwYXMgcHJldmlhcy4KCgojIyMjIEZ1bmNpb25lcyBkZSBhY3RpdmFjacOzbgoKUGFyYSBlc3RlIG1vZGVsbyB1dGlsaXphbW9zIGxhcyBtaXNtYXMgZG9zIGZ1bmNpb25lcyBkZSBhY3RpdmFjacOzbiBxdWUgdXRpbGl6YW1vcyBlbiBsYSBGQyBubjogCgotIFJlY3RpZmllZCBMaW5lYXIgVW5pdDogJCRmKHgpPW1heCgwLHgpJCQKLSBTb2Z0bWF4IDogJCQgZih4KT1cZnJhY3tlXnt4X2l9fXtcc3VtX3tqPTF9Xm4gZV57eF9qfX0kJAoKRGVmaW5pZGFzIGVuIGPDs2RpZ28geSBncsOhZmljYW1lbnRlOgoKYGBge3J9CgpyZWx1IDwtIGZ1bmN0aW9uKHgpIGlmZWxzZSh4ID49IDAsIHgsIDApCnNvZnRtYXggPC0gZnVuY3Rpb24oeCkgZXhwKHgpIC8gc3VtKGV4cCh4KSkKCmRhdGEuZnJhbWUoeD0gc2VxKGZyb209LTEsIHRvPTEsIGJ5PTAuMSkpICU+JSAKICBtdXRhdGUoc29mdG1heCA9IHNvZnRtYXgoeCksCiAgICAgICAgIHJlbHUgPSByZWx1KHgpKSAlPiUgCiAgZ2F0aGVyKHZhcmlhYmxlLHZhbHVlLDI6MykgJT4lIAoKZ2dwbG90KC4sIGFlcyh4PXgsIHk9dmFsdWUsIGdyb3VwPXZhcmlhYmxlLCBjb2xvdXI9dmFyaWFibGUpKSsKICBnZW9tX2xpbmUoc2l6ZT0xKSArCiAgZ2d0aXRsZSgiUmVMVSAmIFNvZnRtYXgiKSsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgoKX19SZUx1X18gZXMgbGEgZnVuY2nDs24gZGUgYWN0aXZhY2nDs24gcXVlIG3DoXMgc2UgdXRpbGl6YSBlbiBsYSBhY3R1YWxpZGFkLiAKCiMjIyMgUGFyYW1ldHJvcyBlbnRyZW5hYmxlcyBkZWwgbW9kZWxvCgoKCmBgYHtyIGVjaG89VCwgcmVzdWx0cz0naGlkZScsZXZhbD1GfQptb2RlbApgYGAKCiFbXShpbWcvY25uX21vZGVsX3N1bW1hcnkucG5nKQoKCi0gQ2FkYSB2ZXogcXVlIGVsIG1vZGVsbyBwYXNhIHBvciB1bmEgY29udm9sdWNpw7NuCgoKCkVsIG1vZGVsbyB0aWVuZSAxLjIgbWlsbG9uZXMgZGUgcGFyw6FtZXRyb3MgcGFyYSBvcHRpbWl6YXI6CgpMYSBwcmltZXJhIGNhcGEgY29udm9sdWNpb25hbCB0aWVuZSBxdWUgZW50cmVuYXIgbG9zIGZpbHRyb3MuIENvbW8gZXN0b3MgZXJhbiBkZSAzeDMsIGNhZGEgdW5vIHRpZW5lIDkgcGFyw6FtZXRyb3MgcGFyYSBlbnRyZW5hciArIDEgYmlhcyBwb3IgZmlsdHJvCgpgYGB7cn0KMzIqICgzKjMpICszMgoKYGBgCgoKTGEgc2VndW5kYSBjb252b2x1Y2nDs24gdGllbmUgcXVlIGVudHJlbmFyIGtlcm5lbHMgZGUgM3gzIHBhcmEgNjQgZmlsdHJvcyAkNjQqKDMqMykkLCBwYXJhIGNhZGEgdW5vIGRlIGxvcyAzMiBmaWx0cm9zIGRlIGxhIGNhcGEgYW50ZXJpb3IsICsxIGJpYXMgcG9yIGZpbHRybwoKYGBge3J9CjY0KigzKjMpKjMyICs2NApgYGAKCiBgbGF5ZXJfbWF4X3Bvb2xpbmdfMmRgLCBgbGF5ZXJfZHJvcG91dGAgeSBgbGF5ZXJfZmxhdHRlbmAgbm8gZW50cmVuYW4gcGFyw6FtZXRyb3MuCiAKIGN1YW5kbyBhcGxhbmFtb3MuIEVsIHNoYXBlIHBhc2EgYToKIApgYGB7cn0KMTIqMTIqNjQKYGBgCiAKIAotIExhICBwcmltZXJhIGNhcGEgZGVuc2EgdGllbmVuIHF1ZSBjb25lY3RhciA5MjE2IG5vZG9zIGNvbiBsb3MgMTI4LCBtw6FzIGxvcyAxMjggYmlhcy4KLSBMYSBjYXBhIGRlIHNhbGlkYSB0aWVuZSBxdWUgY29uZWN0YXIgbG9zIDEyOCBub2RvcyBjb24gbG9zIDEwIGRlIHNhbGlkYSwgbcOhcyAxMCBiaWFzCgpgYGB7cn0KMTI4KjkyMTYgKzEyOAoxMjgqMTAgKzEwCmBgYAoKLS0tLS0tLS0tCgpMdWVnbyBuZWNlc2l0YW1vcyBfX2NvbXBpbGFyIGVsIG1vZGVsb19fIGluZGljYW5kbyBsYSBmdW5jacOzbiBkZSBfbG9zc18sIHF1w6kgdGlwbyBkZSBvcHRpbWl6YWRvciB1dGlsaXphciwgeSBxdcOpIG3DqXRyaWNhcyBub3MgaW1wb3J0YW4KCmBgYHtyfQoKbW9kZWwgPC0gbW9kZWwgJT4lIGNvbXBpbGUoCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLAogIG9wdGltaXplciA9IG9wdGltaXplcl9hZGFkZWx0YSgpLAogIG1ldHJpY3MgPSBjKCdhY2N1cmFjeScpCikKYGBgCgojIyBFbnRyZW5hbWllbnRvCgpQYXJhIGFqdXN0YXIgZWwgbW9kZWxvIHVzYW1vcyBsYSBmdW5jacOzbiBgZml0KClgLCBhY8OhIG5lY2VzaXRhbW9zIHBhc2FyIGxvcyBzaWd1aWVudGVzIHBhcsOhbWV0cm9zOgoKLSBFbCBhcnJheSBjb24gbG9zIGRhdG9zIGRlIGVudHJlbmFtaWVudG8KLSBFbCBhcnJheSBjb24gbG9zIG91dHB1dHMKLSBgZXBvY2hzYDogQ3VhbnRhcyB2ZWNlcyB2YSBhIHJlY29ycmVyIGVsIGRhdGFzZXQgZGUgZW50cmVuYW1pZW50bwotIGBiYXRjaF9zaXplYDogZGUgYSBjdWFudGFzIGltw6FnZW5lcyB2YSBhIG1pcmFyIGVuIGNhZGEgaXRlcmFjacOzbiBkZWwgYmFja3Byb3BhZ2F0aW9uCi0gYHZhbGlkYXRpb25fc3BsaXRgOiBIYWNlbW9zIHVuIHNwbGl0IGVuIHRyYWluIHkgdmFsaWRhdGlvbiBwYXJhIGV2YWx1YXIgbGFzIG3DqXRyaWNhcy4KCmBgYHtyIGVjaG89VCwgcmVzdWx0cz0naGlkZScsZXZhbD1GfQoKZXBvY2hzIDwtIDEyCmJhdGNoX3NpemUgPC0gMTI4CnZhbGlkYXRpb25fc3BsaXQgPC0gMC4yCgpmaXRfaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHhfdHJhaW4sIHlfdHJhaW4sCiAgYmF0Y2hfc2l6ZSA9IGJhdGNoX3NpemUsCiAgZXBvY2hzID0gZXBvY2hzLAogIHZhbGlkYXRpb25fc3BsaXQgPSB2YWxpZGF0aW9uX3NwbGl0CikKCgpgYGAKTWllbnRyYXMgZW50cmVuYW1vcyBlbCBtb2RlbG8sIHBvZGVtb3MgdmVyIGxhIGV2b2x1Y2nDs24gZW4gZWwgZ3LDoWZpY28gaW50ZXJhY3Rpdm8gcXVlIHNlIGdlbmVyYSBlbiBlbCB2aWV3ZXIgZGUgUnN0dWRpby4KCgohW10oaW1nL2Nubl90cmFpbmluZy5wbmcpCgoKCmBgYHtyIGluY2x1ZGU9RkFMU0UsIGV2YWw9Rn0KI2d1YXJkbyBsYSBoaXN0b3JpYS4gTm8gbG8gbXVlc3RybywgbmkgbG8gY29ycm8gcG9yIGRlZmF1bHQKc2F2ZVJEUyhmaXRfaGlzdG9yeSwiLi4vUmVzdWx0YWRvcy9jbm5faGlzdC5SRFMiKQpgYGAKCmBgYHtyIGluY2x1ZGU9RkFMU0UsIGV2YWw9VH0KI2xldmFudG8gbGEgaGlzdG9yaWEuIE5vIGxvIG11ZXN0cm8sIHBlcm8gbG8gY29ycm8gcG9yIGRlZmF1bHQKZml0X2hpc3RvcnkgPC0gcmVhZF9yZHMoIi4uL1Jlc3VsdGFkb3MvY25uX2hpc3QuUkRTIikKYGBgCgoKCmBgYHtyfQpmaXRfaGlzdG9yeQpgYGAKCgoKYGZpdCgpYCBub3MgZGV2dWVsdmUgdW4gb2JqZXRvIHF1ZSBpbmNsdXllIGxhcyBtw6l0cmljYXMgZGUgbG9zcyB5IGFjY3VyYWN5LgoKRXN0ZSBvYmpldG8gbG8gcG9kZW1vcyBncmFmaWNhciBjb24gYHBsb3QoKWAgeSBub3MgZGV2dWVsdmUgdW4gb2JqZXRvIGRlIF9nZ3Bsb3RfLCBzb2JyZSBlbCBxdWUgcG9kZW1vcyBzZWd1aXIgdHJhYmFqYW5kbwoKYGBge3J9CnBsb3QoZml0X2hpc3RvcnkpKwogIHRoZW1lX21pbmltYWwoKSsKICBsYWJzKHRpdGxlPSAiRXZvbHVjacOzbiBkZSBMb3NzIHkgQWNjdXJhY3kgZW4gdHJhaW4geSB2YWxpZGF0aW9uIikKYGBgCgoKCl9fZXMgaW1wb3J0YW50ZSBndWFyZGFyIGVsIG1vZGVsbyBsdWVnbyBkZSBlbnRyZW5hciwgcGFyYSBwb2RlciByZXV0aWxpemFybG9fXwoKCmBgYHtyIGV2YWw9RkFMU0UsIGVjaG89VH0KbW9kZWwgJT4lIHNhdmVfbW9kZWxfaGRmNSgiLi4vUmVzdWx0YWRvcy9jbm5fbW9kZWwuaDUiKQpgYGAKCnkgcGFyYSBjYXJnYXJsbwoKYGBge3J9Cm1vZGVsb19wcmVlbnRyZW5hZG8gPC0gbG9hZF9tb2RlbF9oZGY1KCIuLi9SZXN1bHRhZG9zL2Nubl9tb2RlbC5oNSIpCmBgYAoKYGBge3J9Cm1vZGVsb19wcmVlbnRyZW5hZG8KYGBgCgoKU2kgcXVlcmVtb3MgZXZhbHVhciBlbCBtb2RlbG8gc29icmUgZWwgY29uanVudG8gZGUgdGVzdCAoZGlzdGludG8gZGVsIGRlIHZhbGlkYWNpw7NuKSBwb2RlbW9zIHVzYXIgbGEgZnVuY2nDs24gYGV2YWx1YXRlKClgCgoKYGBge3J9Cgptb2RlbG9fcHJlZW50cmVuYWRvICU+JSBldmFsdWF0ZSh4X3Rlc3QsIHlfdGVzdCkKYGBgCgpQYXJhIG9idGVuZXIgbGFzIHByZWRpY2Npb25lcyBzb2JyZSB1biBudWV2byBjb25qdW50byBkZSBkYXRvcyB1dGlsaXphbW9zIGBwcmVkaWN0X2NsYXNzZXMoKWAKCmBgYHtyfQptb2RlbG9fcHJlZW50cmVuYWRvICU+JSBwcmVkaWN0X2NsYXNzZXMoeF90ZXN0KSAlPiUgaGVhZCguKQpgYGAKCgoKLS0tLS0tLQoKT3Ryb3MgcmVjdXJzb3MgaW50ZXJlc2FudGVzOgoKCltWaXN1YWxpemFjacOzbiBkZSB1bmEgUmVkIEZ1bGx5IGNvbmVjdGVkIHBhcmEgY2xhc2lmaWNhY2nDs24gZGUgZMOtZ2l0b3NdKGh0dHA6Ly9zY3MucnllcnNvbi5jYS9+YWhhcmxleS92aXMvY29udi8pCgoKW1RlbnNvciBGbG93IFBsYXlncm91bmRdKGh0dHA6Ly9wbGF5Z3JvdW5kLnRlbnNvcmZsb3cub3JnLyNhY3RpdmF0aW9uPXRhbmgmYmF0Y2hTaXplPTEwJmRhdGFzZXQ9Y2lyY2xlJnJlZ0RhdGFzZXQ9cmVnLXBsYW5lJmxlYXJuaW5nUmF0ZT0wLjAzJnJlZ3VsYXJpemF0aW9uUmF0ZT0wJm5vaXNlPTAmbmV0d29ya1NoYXBlPTQsMiZzZWVkPTAuNTk3OTQmc2hvd1Rlc3REYXRhPWZhbHNlJmRpc2NyZXRpemU9ZmFsc2UmcGVyY1RyYWluRGF0YT01MCZ4PXRydWUmeT10cnVlJnhUaW1lc1k9ZmFsc2UmeFNxdWFyZWQ9ZmFsc2UmeVNxdWFyZWQ9ZmFsc2UmY29zWD1mYWxzZSZzaW5YPWZhbHNlJmNvc1k9ZmFsc2Umc2luWT1mYWxzZSZjb2xsZWN0U3RhdHM9ZmFsc2UmcHJvYmxlbT1jbGFzc2lmaWNhdGlvbiZpbml0WmVybz1mYWxzZSZoaWRlVGV4dD1mYWxzZQopCgo=