Objetivos

La idea de este notebook es poder dar una intuición acerca de cómo funcionan los métodos basados en Gradient Boosting. Para ello, utilizaremos un dataset de juguete.

Gradient Descent en una carilla

Recordemos: ¿cuáles son los principales elementos de un algoritmo de Machine Learning?

La función de costo debería servirnos para poder seleccionar cuál es el mejor set de parámetros. En líneas generales, queremos miniminzar la función de costo. Por eso, los problemas de machine learnin (y en buena medida, los de estadística) son problemas de optimización.

Pensemos en una regresión lineal simple

Una opción (poco recomendable) sería probar todos los valores posibles de \((\beta_{0},\beta_{1})\), digamos entre los infinitos negativos y los infitinos positivos, calcular las predicciones para cada uno, calcular \(MSE\) para cada uno y elegir el \(\beta_{1}\) que logra un valor mínimo en \(MSE\), o dicho en lenguaje matemático: \(\underset{\beta_{0},\beta_{1}}{\operatorname{argmax}}(\frac{1}{N} \sum_{i=1}^N(y_{i} - \hat{y_{i}})^2)\)

En este punto, el problema se tranforma en un problema de optimización. Dado que en general, no es computacionalmente posible probar todos los valores de un parámetro (y mucho menos de miles o millones) se recurren a otro tipo de métodos. El más utilizado en sus diversas variables es el llamado Gradient Descent o Descenso de gradiente.

Si bien, no entraremos en sus sutilezas, solamente diremos que se trata de un procedimiento iterativo que hace uso de las propiedades de las derivadas (algo así como el ratio de cambio de la función de costo al cambiar el valor del parámetro) para ir actualizando los valores de los parámetros hasta llegar al mínimo:

Calculando derivadas de la función de costo, llegamos a la siguente fórmula que permite ir actualizando los valores de \((\beta_{0}, \beta_{1})\) en un número fijo de iteraciones:

\(\beta_{0}^1 = \beta_{0}^0 - (y_{i} - \hat{y_{i}}) \times \gamma \times X\)

\(\beta_{1}^1 = \beta_{1}^0 - (y_{i} - \hat{y_{i}}) \times \gamma\)

Cuán pequeños (o grandes) son los cambios, dependerán del parámetro \(\gamma\). también llamada learning rate.

Aquí hay un tutorial sobre el tema, sumamente intuitivo.

Gradient Boosting Machines… ¿qué son?

Ahora bien, la idea general detrás de los Gradient Boosting Machines (GBM, a partir de ahora), es igual a la de los métodos de Boosting en general: entrenar modelos fuertes a partir de un ensamble de modelos débiles. Se hace, igualmente, entrenando clasificadores de manera secuencia, en dónde cada uno de los clasificadores posteriores se centra en los errores del clasificador anterior.

La principal diferencia con AdaBoost es que, en lugar de jugar con los pesos de cada instancia en cada iteración, GBM trata de fitear un modelo sobre los residuos del modelo anterior.

Así, se trata de modelos de tipo aditivo. La idea general es que la predicción final es generada por la suma de diversos modelos parciales:

\(\hat{y} = h_{1}(X) + h_{2}(X) + h_{3}(X) + ... + h_{m}(X)\)

\(\hat{y} = \sum_{m=1}^M h_{m}(X)\)

Puede pensarse de forma análoga a la descomposición de una serie temporal, donde la series es pensada como una composición aditiva de una componente de tendencia \(T_{t}\), una de ciclo \(C_{y}\), una estacional \(S_{t}\) y otra de ruido \(\mu_{t}\):

\(y_{t} = T_{t} + C_{t} + S_{t} + \mu_{t}\)

Para seguir profundizando aquí hay un tutorial de carácter sumamente intuitivo. También pueden consultar el libro de Aurelien Geron del cual tomamos y tradujimos a R este ejemplo, escrito originalmente en Python.

Un ejemplo para construir la intuición

Veamos un ejemplo usando árboles de decisión, recordando que se trata de un meta-algoritmo por lo cual podríamos usar (casi) cualquier otro método.

Generamos nuestros datos

library(tidyverse)
library(rpart)
library(caret)
set.seed(42)
X <- runif(100, 0, 1) - 0.5
y <- 3*X**2 + 0.05 * runif(100,0,4)
df <- cbind(X,y) %>% as_tibble()
rm(X,y)

En primer lugar, entrenemos un árbol de decisión sobre nuestro dataset y agreguemos las predicciones como columnas en el dataset:

tree_1 <- rpart(y~X, data=df, method='anova', control=list(cp=0.0000001, maxdepth=2))                
df <- df %>% mutate(h_1 = predict(tree_1, df),
                p_1 = h_1,
                y_1 = y - p_1)

Ahora, entrenemos un segundo árbol pero sobre los residuos del anterior, es decir, sobre \(y_{i} - \hat{y_{i}}\) o más precisamente: y - predict(tree_1, df) y agreguemos, una vez más, los resultados al dataset:

tree_2 <- rpart(y_1~X, data=df, method='anova', control=list(cp=0.0000001, maxdepth=2))                
df <- df %>% mutate(h_2 = predict(tree_2, df),
                    p_2 = p_1 + h_2,
                    y_2 = y - p_2)

Repitamos el proceso una tercera vez… y una cuarta y una quinta y una sexta…

tree_3 <- rpart(y_2~X, data=df, method='anova', control=list(cp=0.0000001, maxdepth=2))                
df <- df %>% mutate(h_3 = predict(tree_3, df),
                    p_3 = p_2 + h_3,
                    y_3 = y - p_3)
tree_4 <- rpart(y_3~X, data=df, method='anova', control=list(cp=0.0000001, maxdepth=2))                
df <- df %>% mutate(h_4 = predict(tree_4, df),
                    p_4 = p_3 + h_4,
                    y_4 = y - p_4)
tree_5 <- rpart(y_4~X, data=df, method='anova', control=list(cp=0.0000001, maxdepth=2))                
df <- df %>% mutate(h_5 = predict(tree_5, df),
                    p_5 = p_4 + h_5,
                    y_5 = y - p_5)
tree_6 <- rpart(y_5~X, data=df, method='anova', control=list(cp=0.0000001, maxdepth=2))                
df <- df %>% mutate(h_6 = predict(tree_6, df),
                    p_6 = p_5 + h_6,
                    y_6 = y - p_6)

Ahora tenemos un ensable que consiste en seis árboles de decisión. Podemos hacer una predicción sobre una instancia nueva simplemente agregando las predicciones de nuestros seis árboles.

ggpubr::ggarrange(ncol=2, nrow=3,
                  ggplot(df) + 
                          geom_point(aes(x=X, y=y), color='blue') + 
                          geom_line(aes(x=X, y=h_1), color='green'),
                  ggplot(df) + 
                          geom_point(aes(x=X, y=y), color='blue') + 
                          geom_line(aes(x=X, y=p_1), color='red'),
                  ggplot(df) + 
                          geom_point(aes(x=X, y=y_1), color='blue') + 
                          geom_line(aes(x=X, y=h_2), color='green') +
                          scale_y_continuous(limits=c(-0.4,1)),
                  ggplot(df) + 
                          geom_point(aes(x=X, y=y), color='blue') + 
                          geom_line(aes(x=X, y=p_2), color='red'),
                  ggplot(df) + 
                          geom_point(aes(x=X, y=y_2), color='blue') + 
                          geom_line(aes(x=X, y=h_3), color='green') +
                          scale_y_continuous(limits=c(-0.4,1)),
                  ggplot(df) + 
                          geom_point(aes(x=X, y=y), color='blue') + 
                          geom_line(aes(x=X, y=p_3), color='red')
)

El plot muestra cómo va evolucionando el modelo a medida que vamos agregando una mayor cantidad de iteraciones. A la izquierda tenemos el ajuste de cada modelo con su propia variable dependiente (es decir, los residuos del modelo anterior) y a la derecha, el ajuste del ensamble global sobre \(X\) e \(y\).

Puede verse cómo el ensamble se va haciendo cada vez más preciso a medida que vamos agregando modelos.

¿Cuáles son los hiperparámetros de GBM?

Vamos a ver algunos parámetros de GBM en caret y su interpretación. El método se llama “GBM” y se usa, como siempre, en la función train.

Vemos que en tuneGrid hay cuatro parámetros para evaluar:

Entrenaremos dos GBM, uno con pocos árboles y learing rate alto y otro con las características inversas:

gbm_slow_05 <- train(y~X, method='gbm',
             data=df,
             trControl=trainControl,
             tuneGrid=data.frame(n.trees=200,
                                 interaction.depth=3,
                                 shrinkage=0.05,
                                 n.minobsinnode=2)
             )
Iter   TrainDeviance   ValidDeviance   StepSize   Improve
     1        0.0578             nan     0.0500    0.0049
     2        0.0534             nan     0.0500    0.0042
     3        0.0498             nan     0.0500    0.0043
     4        0.0464             nan     0.0500    0.0040
     5        0.0429             nan     0.0500    0.0033
     6        0.0396             nan     0.0500    0.0026
     7        0.0368             nan     0.0500    0.0031
     8        0.0339             nan     0.0500    0.0026
     9        0.0317             nan     0.0500    0.0023
    10        0.0292             nan     0.0500    0.0022
    20        0.0141             nan     0.0500    0.0011
    40        0.0048             nan     0.0500    0.0002
    60        0.0028             nan     0.0500   -0.0000
    80        0.0022             nan     0.0500   -0.0000
   100        0.0020             nan     0.0500   -0.0000
   120        0.0019             nan     0.0500   -0.0000
   140        0.0018             nan     0.0500   -0.0000
   160        0.0017             nan     0.0500   -0.0000
   180        0.0016             nan     0.0500   -0.0000
   200        0.0015             nan     0.0500   -0.0000

Agregamos las predicciones al dataset y ploteamos:

df <- df %>% mutate(y_gbm=predict(gbm, df),
                    y_gbm_n3_s01=predict(gbm_n3_s01, df),
                    y_gbm_n200_s1=predict(gbm_n200_s1, df),
                    y_gbm_slow=predict(gbm_slow, df),
                    y_gbm_slow_05=predict(gbm_slow_05, df),
                    y_gbm_slow_001=predict(gbm_slow_001, df)
                    )
ggpubr::ggarrange(ncol=2, nrow=3,
ggplot(df) + 
        geom_point(aes(x=X, y=y), color='blue') + 
        geom_line(aes(x=X,y=y_gbm)) +
        labs(title='n.trees=3, shrinkage=1'),
ggplot(df) + 
        geom_point(aes(x=X, y=y), color='blue') + 
        geom_line(aes(x=X,y=y_gbm_n3_s01)) +
        labs(title='n.trees=3, shrinkage=0.1'),
ggplot(df) + 
        geom_point(aes(x=X, y=y), color='blue') + 
        geom_line(aes(x=X,y=y_gbm_n200_s1)) +
        labs(title='n.trees=200, shrinkage=1'),
ggplot(df) + 
        geom_point(aes(x=X, y=y), color='blue') + 
        geom_line(aes(x=X,y=y_gbm_slow)) +
        labs(title='n.trees=200, shrinkage=0.1'),
ggplot(df) + 
        geom_point(aes(x=X, y=y), color='blue') + 
        geom_line(aes(x=X,y=y_gbm_slow_05)) +
        labs(title='n.trees=200, shrinkage=0.05'),
ggplot(df) + 
        geom_point(aes(x=X, y=y), color='blue') + 
        geom_line(aes(x=X,y=y_gbm_slow_001)) +
        labs(title='n.trees=200, shrinkage=0.001')
)

¿Cómo describir a cada uno? El de la izquierda parece no ser lo suficientemente flexible, mientras que el de la derecha parece generar overfitting de forma bastante clara.

LS0tDQp0aXRsZTogIkVudGVuZGllbmRvIEdyYWRpZW50IEJvb3N0aW5nIE1hY2hpbmVzIg0KYXV0aG9yOiAiR2VybcOhbiBSb3NhdGkiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQoNCiMjIyBPYmpldGl2b3MNCg0KTGEgaWRlYSBkZSBlc3RlIG5vdGVib29rIGVzIHBvZGVyIGRhciB1bmEgaW50dWljacOzbiBhY2VyY2EgZGUgY8OzbW8gZnVuY2lvbmFuIGxvcyBtw6l0b2RvcyBiYXNhZG9zIGVuIEdyYWRpZW50IEJvb3N0aW5nLiBQYXJhIGVsbG8sIHV0aWxpemFyZW1vcyB1biBkYXRhc2V0IGRlIGp1Z3VldGUuDQoNCg0KIyMjIEdyYWRpZW50IERlc2NlbnQgZW4gdW5hIGNhcmlsbGENCg0KUmVjb3JkZW1vczogwr9jdcOhbGVzIHNvbiBsb3MgcHJpbmNpcGFsZXMgZWxlbWVudG9zIGRlIHVuIGFsZ29yaXRtbyBkZSBNYWNoaW5lIExlYXJuaW5nPw0KDQotIGRhdG9zICgkWCQgZSAkeSQpDQotIGFsZ29yaXRtbyBvIGhpcMOzdGVzaXMgKCRcaGF0e3l9PWYoWCkkKSBxdWUgY29udGllbmUgDQotIHBhcsOhbWV0cm9zIChwb3IgZWplbXBsbywgJFxiZXRhX3swfSwgXGJldGFfezF9LCAuLi4sIFxiZXRhX3twfSQgZGUgdW4gbW9kZWxvIGRlIHJlZ3Jlc2nDs24gbGluZWFsKQ0KLSBtw6l0cmljYSBkZSBjb3N0byBvIF9jb3N0IGZ1bmN0aW9uXywgdW5hIG1lZGlkYSBxdWUgc2lydmUgcGFyYSBldmFsdWFyIGxhIHBlcmZvcm1hbmNlIGRlbCBtb2RlbG8gJFxlbGwoeV97aX0gLSBcaGF0e3lfe2l9fSkkDQoNCkxhIGZ1bmNpw7NuIGRlIGNvc3RvIGRlYmVyw61hIHNlcnZpcm5vcyBwYXJhIHBvZGVyIHNlbGVjY2lvbmFyIGN1w6FsIGVzIGVsIG1lam9yIHNldCBkZSBwYXLDoW1ldHJvcy4gRW4gbMOtbmVhcyBnZW5lcmFsZXMsIHF1ZXJlbW9zIF9fbWluaW1pbnphcl9fIGxhIGZ1bmNpw7NuIGRlIGNvc3RvLiBQb3IgZXNvLCBsb3MgcHJvYmxlbWFzIGRlIG1hY2hpbmUgbGVhcm5pbiAoeSBlbiBidWVuYSBtZWRpZGEsIGxvcyBkZSBlc3RhZMOtc3RpY2EpIHNvbiBwcm9ibGVtYXMgZGUgX19vcHRpbWl6YWNpw7NuX18uDQoNClBlbnNlbW9zIGVuIHVuYSByZWdyZXNpw7NuIGxpbmVhbCBzaW1wbGUgDQoNCi0gdGVuZW1vcyBudWVzdHJvcyBkYXRvcyAkeCQgZSAkeSQNCi0gbGEgaGlww7N0ZXNpczogJFxoYXR7eV97aX19ID0gXGJldGFfezB9ICsgXGJldGFfezF9WF97MX0kDQotIGxvcyBwYXLDoW1ldHJvczogJChcYmV0YV97MH0sIFxiZXRhX3sxfSkkDQotIGZ1bmNpw7NuIGRlIGNvc3RvOiAkTVNFID0gXGZyYWN7MX17Tn0gXHN1bV97aT0xfV5OKHlfe2l9IC0gXGhhdHt5X3tpfX0pXjIkDQoNClVuYSBvcGNpw7NuIChwb2NvIHJlY29tZW5kYWJsZSkgc2Vyw61hIHByb2JhciB0b2RvcyBsb3MgdmFsb3JlcyBwb3NpYmxlcyBkZSAkKFxiZXRhX3swfSxcYmV0YV97MX0pJCwgZGlnYW1vcyBlbnRyZSBsb3MgaW5maW5pdG9zIG5lZ2F0aXZvcyB5IGxvcyBpbmZpdGlub3MgcG9zaXRpdm9zLCBjYWxjdWxhciBsYXMgcHJlZGljY2lvbmVzIHBhcmEgY2FkYSB1bm8sIGNhbGN1bGFyICRNU0UkIHBhcmEgY2FkYSB1bm8geSBlbGVnaXIgZWwgJFxiZXRhX3sxfSQgcXVlIGxvZ3JhIHVuIHZhbG9yIG3DrW5pbW8gZW4gJE1TRSQsIG8gZGljaG8gZW4gbGVuZ3VhamUgbWF0ZW3DoXRpY286ICRcdW5kZXJzZXR7XGJldGFfezB9LFxiZXRhX3sxfX17XG9wZXJhdG9ybmFtZXthcmdtYXh9fShcZnJhY3sxfXtOfSBcc3VtX3tpPTF9Xk4oeV97aX0gLSBcaGF0e3lfe2l9fSleMikkICANCg0KRW4gZXN0ZSBwdW50bywgZWwgcHJvYmxlbWEgc2UgdHJhbmZvcm1hIGVuIHVuIHByb2JsZW1hIGRlIG9wdGltaXphY2nDs24uIERhZG8gcXVlIGVuIGdlbmVyYWwsIG5vIGVzIGNvbXB1dGFjaW9uYWxtZW50ZSBwb3NpYmxlIHByb2JhciB0b2RvcyBsb3MgdmFsb3JlcyBkZSB1biBwYXLDoW1ldHJvICh5IG11Y2hvIG1lbm9zIGRlIG1pbGVzIG8gbWlsbG9uZXMpIHNlIHJlY3VycmVuIGEgb3RybyB0aXBvIGRlIG3DqXRvZG9zLiBFbCBtw6FzIHV0aWxpemFkbyBlbiBzdXMgZGl2ZXJzYXMgdmFyaWFibGVzIGVzIGVsIGxsYW1hZG8gX0dyYWRpZW50IERlc2NlbnRfIG8gX0Rlc2NlbnNvIGRlIGdyYWRpZW50ZV8uDQoNClNpIGJpZW4sIG5vIGVudHJhcmVtb3MgZW4gc3VzIHN1dGlsZXphcywgc29sYW1lbnRlIGRpcmVtb3MgcXVlIHNlIHRyYXRhIGRlIHVuIHByb2NlZGltaWVudG8gaXRlcmF0aXZvIHF1ZSBoYWNlIHVzbyBkZSBsYXMgcHJvcGllZGFkZXMgZGUgbGFzIGRlcml2YWRhcyAoYWxnbyBhc8OtIGNvbW8gZWwgcmF0aW8gZGUgY2FtYmlvIGRlIGxhIGZ1bmNpw7NuIGRlIGNvc3RvIGFsIGNhbWJpYXIgZWwgdmFsb3IgZGVsIHBhcsOhbWV0cm8pIHBhcmEgaXIgYWN0dWFsaXphbmRvIGxvcyB2YWxvcmVzIGRlIGxvcyBwYXLDoW1ldHJvcyBoYXN0YSBsbGVnYXIgYWwgbcOtbmltbzoNCg0KIVtdKC4uL2ltZy9nZC5wbmcpDQoNCkNhbGN1bGFuZG8gZGVyaXZhZGFzIGRlIGxhIGZ1bmNpw7NuIGRlIGNvc3RvLCBsbGVnYW1vcyBhIGxhIHNpZ3VlbnRlIGbDs3JtdWxhIHF1ZSBwZXJtaXRlIGlyIGFjdHVhbGl6YW5kbyBsb3MgdmFsb3JlcyBkZSAkKFxiZXRhX3swfSwgXGJldGFfezF9KSQgZW4gdW4gbsO6bWVybyBmaWpvIGRlIGl0ZXJhY2lvbmVzOg0KDQokXGJldGFfezB9XjEgPSBcYmV0YV97MH1eMCAtICh5X3tpfSAtIFxoYXR7eV97aX19KSAgXHRpbWVzIFxnYW1tYSBcdGltZXMgWCQNCg0KJFxiZXRhX3sxfV4xID0gXGJldGFfezF9XjAgLSAoeV97aX0gLSBcaGF0e3lfe2l9fSkgXHRpbWVzIFxnYW1tYSQNCg0KQ3XDoW4gcGVxdWXDsW9zIChvIGdyYW5kZXMpIHNvbiBsb3MgY2FtYmlvcywgZGVwZW5kZXLDoW4gZGVsIHBhcsOhbWV0cm8gJFxnYW1tYSQuIHRhbWJpw6luIGxsYW1hZGEgX2xlYXJuaW5nIHJhdGVfLg0KDQpbQXF1w60gaGF5IHVuIHR1dG9yaWFsXShodHRwczovL3Rvd2FyZHNkYXRhc2NpZW5jZS5jb20vdW5kZXJzdGFuZGluZy10aGUtbWF0aGVtYXRpY3MtYmVoaW5kLWdyYWRpZW50LWRlc2NlbnQtZGRlNWRjOWJlMDZlP2dpPWQyNDEwMzliM2E3MSkgc29icmUgZWwgdGVtYSwgc3VtYW1lbnRlIGludHVpdGl2by4NCg0KIyMjIEdyYWRpZW50IEJvb3N0aW5nIE1hY2hpbmVzLi4uIMK/cXXDqSBzb24/DQoNCkFob3JhIGJpZW4sIGxhIGlkZWEgZ2VuZXJhbCBkZXRyw6FzIGRlIGxvcyBHcmFkaWVudCBCb29zdGluZyBNYWNoaW5lcyAoR0JNLCBhIHBhcnRpciBkZSBhaG9yYSksIGVzIGlndWFsIGEgbGEgZGUgbG9zIG3DqXRvZG9zIGRlIEJvb3N0aW5nIGVuIGdlbmVyYWw6IGVudHJlbmFyIG1vZGVsb3MgZnVlcnRlcyBhIHBhcnRpciBkZSB1biBlbnNhbWJsZSBkZSBtb2RlbG9zIGTDqWJpbGVzLiBTZSBoYWNlLCBpZ3VhbG1lbnRlLCBlbnRyZW5hbmRvIGNsYXNpZmljYWRvcmVzIGRlIG1hbmVyYSBzZWN1ZW5jaWEsIGVuIGTDs25kZSBjYWRhIHVubyBkZSBsb3MgY2xhc2lmaWNhZG9yZXMgcG9zdGVyaW9yZXMgc2UgY2VudHJhIGVuIGxvcyBlcnJvcmVzIGRlbCBjbGFzaWZpY2Fkb3IgYW50ZXJpb3IuDQoNCkxhIHByaW5jaXBhbCBkaWZlcmVuY2lhIGNvbiBBZGFCb29zdCBlcyBxdWUsIGVuIGx1Z2FyIGRlIGp1Z2FyIGNvbiBsb3MgcGVzb3MgZGUgY2FkYSBpbnN0YW5jaWEgZW4gY2FkYSBpdGVyYWNpw7NuLCBHQk0gdHJhdGEgZGUgZml0ZWFyIHVuIG1vZGVsbyBzb2JyZSBsb3MgcmVzaWR1b3MgZGVsIG1vZGVsbyBhbnRlcmlvci4NCg0KQXPDrSwgc2UgdHJhdGEgZGUgbW9kZWxvcyBkZSB0aXBvIGFkaXRpdm8uIExhIGlkZWEgZ2VuZXJhbCBlcyBxdWUgbGEgcHJlZGljY2nDs24gZmluYWwgZXMgZ2VuZXJhZGEgcG9yIGxhIHN1bWEgZGUgZGl2ZXJzb3MgbW9kZWxvcyBwYXJjaWFsZXM6DQoNCiRcaGF0e3l9ID0gIGhfezF9KFgpICsgaF97Mn0oWCkgKyBoX3szfShYKSArIC4uLiArIGhfe219KFgpJA0KDQokXGhhdHt5fSA9IFxzdW1fe209MX1eTSBoX3ttfShYKSQNCg0KUHVlZGUgcGVuc2Fyc2UgZGUgZm9ybWEgYW7DoWxvZ2EgYSBsYSBkZXNjb21wb3NpY2nDs24gZGUgdW5hIHNlcmllIHRlbXBvcmFsLCBkb25kZSBsYSBzZXJpZXMgZXMgcGVuc2FkYSBjb21vIHVuYSBjb21wb3NpY2nDs24gYWRpdGl2YSBkZSB1bmEgY29tcG9uZW50ZSBkZSB0ZW5kZW5jaWEgJFRfe3R9JCwgdW5hIGRlIGNpY2xvICRDX3t5fSQsIHVuYSBlc3RhY2lvbmFsICRTX3t0fSQgeSBvdHJhIGRlIHJ1aWRvICRcbXVfe3R9JDoNCg0KJHlfe3R9ID0gVF97dH0gKyBDX3t0fSArIFNfe3R9ICsgXG11X3t0fSQNCg0KUGFyYSBzZWd1aXIgcHJvZnVuZGl6YW5kbyBbYXF1w61dKGh0dHBzOi8vZXhwbGFpbmVkLmFpL2dyYWRpZW50LWJvb3N0aW5nL2luZGV4Lmh0bWwpIGhheSB1biB0dXRvcmlhbCBkZSBjYXLDoWN0ZXIgc3VtYW1lbnRlIGludHVpdGl2by4gVGFtYmnDqW4gcHVlZGVuIGNvbnN1bHRhciBlbCBbbGlicm8gZGUgQXVyZWxpZW4gR2Vyb25dKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vSGFuZHMtTWFjaGluZS1MZWFybmluZy1TY2lraXQtTGVhcm4tVGVuc29yRmxvdy1lYm9vay9kcC9CMDZYTktWNVRTKSBkZWwgY3VhbCB0b21hbW9zIHkgdHJhZHVqaW1vcyBhIFIgZXN0ZSBlamVtcGxvLCBlc2NyaXRvIG9yaWdpbmFsbWVudGUgZW4gUHl0aG9uLg0KDQoNCiMjIyBVbiBlamVtcGxvIHBhcmEgY29uc3RydWlyIGxhIGludHVpY2nDs24NCg0KVmVhbW9zIHVuIGVqZW1wbG8gdXNhbmRvIMOhcmJvbGVzIGRlIGRlY2lzacOzbiwgcmVjb3JkYW5kbyBxdWUgc2UgdHJhdGEgZGUgdW4gbWV0YS1hbGdvcml0bW8gcG9yIGxvIGN1YWwgcG9kcsOtYW1vcyB1c2FyIChjYXNpKSBjdWFscXVpZXIgb3RybyBtw6l0b2RvLg0KDQpHZW5lcmFtb3MgbnVlc3Ryb3MgZGF0b3MNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShycGFydCkNCmxpYnJhcnkoY2FyZXQpDQoNCnNldC5zZWVkKDQyKQ0KWCA8LSBydW5pZigxMDAsIDAsIDEpIC0gMC41DQp5IDwtIDMqWCoqMiArIDAuMDUgKiBydW5pZigxMDAsMCw0KQ0KDQpkZiA8LSBjYmluZChYLHkpICU+JSBhc190aWJibGUoKQ0KDQpybShYLHkpDQoNCmBgYA0KDQpFbiBwcmltZXIgbHVnYXIsIGVudHJlbmVtb3MgdW4gw6FyYm9sIGRlIGRlY2lzacOzbiBzb2JyZSBudWVzdHJvIGRhdGFzZXQgeSBhZ3JlZ3VlbW9zIGxhcyBwcmVkaWNjaW9uZXMgY29tbyBjb2x1bW5hcyBlbiBlbCBkYXRhc2V0Og0KDQpgYGB7cn0NCnRyZWVfMSA8LSBycGFydCh5flgsIGRhdGE9ZGYsIG1ldGhvZD0nYW5vdmEnLCBjb250cm9sPWxpc3QoY3A9MC4wMDAwMDAxLCBtYXhkZXB0aD0yKSkgICAgICAgICAgICAgICAgDQoNCmRmIDwtIGRmICU+JSBtdXRhdGUoaF8xID0gcHJlZGljdCh0cmVlXzEsIGRmKSwNCiAgICAgICAgICAgICAgICBwXzEgPSBoXzEsDQogICAgICAgICAgICAgICAgeV8xID0geSAtIHBfMSkNCg0KYGBgDQoNCkFob3JhLCBlbnRyZW5lbW9zIHVuIHNlZ3VuZG8gw6FyYm9sIHBlcm8gc29icmUgbG9zIHJlc2lkdW9zIGRlbCBhbnRlcmlvciwgZXMgZGVjaXIsIHNvYnJlICR5X3tpfSAtIFxoYXR7eV97aX19JCBvIG3DoXMgcHJlY2lzYW1lbnRlOiBgeSAtIHByZWRpY3QodHJlZV8xLCBkZilgIHkgYWdyZWd1ZW1vcywgdW5hIHZleiBtw6FzLCBsb3MgcmVzdWx0YWRvcyBhbCBkYXRhc2V0Og0KDQoNCmBgYHtyfQ0KdHJlZV8yIDwtIHJwYXJ0KHlfMX5YLCBkYXRhPWRmLCBtZXRob2Q9J2Fub3ZhJywgY29udHJvbD1saXN0KGNwPTAuMDAwMDAwMSwgbWF4ZGVwdGg9MikpICAgICAgICAgICAgICAgIA0KZGYgPC0gZGYgJT4lIG11dGF0ZShoXzIgPSBwcmVkaWN0KHRyZWVfMiwgZGYpLA0KICAgICAgICAgICAgICAgICAgICBwXzIgPSBwXzEgKyBoXzIsDQogICAgICAgICAgICAgICAgICAgIHlfMiA9IHkgLSBwXzIpDQpgYGANCg0KUmVwaXRhbW9zIGVsIHByb2Nlc28gdW5hIHRlcmNlcmEgdmV6Li4uIHkgdW5hIGN1YXJ0YSB5IHVuYSBxdWludGEgeSB1bmEgc2V4dGEuLi4NCg0KDQpgYGB7cn0NCnRyZWVfMyA8LSBycGFydCh5XzJ+WCwgZGF0YT1kZiwgbWV0aG9kPSdhbm92YScsIGNvbnRyb2w9bGlzdChjcD0wLjAwMDAwMDEsIG1heGRlcHRoPTIpKSAgICAgICAgICAgICAgICANCg0KZGYgPC0gZGYgJT4lIG11dGF0ZShoXzMgPSBwcmVkaWN0KHRyZWVfMywgZGYpLA0KICAgICAgICAgICAgICAgICAgICBwXzMgPSBwXzIgKyBoXzMsDQogICAgICAgICAgICAgICAgICAgIHlfMyA9IHkgLSBwXzMpDQoNCnRyZWVfNCA8LSBycGFydCh5XzN+WCwgZGF0YT1kZiwgbWV0aG9kPSdhbm92YScsIGNvbnRyb2w9bGlzdChjcD0wLjAwMDAwMDEsIG1heGRlcHRoPTIpKSAgICAgICAgICAgICAgICANCg0KZGYgPC0gZGYgJT4lIG11dGF0ZShoXzQgPSBwcmVkaWN0KHRyZWVfNCwgZGYpLA0KICAgICAgICAgICAgICAgICAgICBwXzQgPSBwXzMgKyBoXzQsDQogICAgICAgICAgICAgICAgICAgIHlfNCA9IHkgLSBwXzQpDQoNCnRyZWVfNSA8LSBycGFydCh5XzR+WCwgZGF0YT1kZiwgbWV0aG9kPSdhbm92YScsIGNvbnRyb2w9bGlzdChjcD0wLjAwMDAwMDEsIG1heGRlcHRoPTIpKSAgICAgICAgICAgICAgICANCg0KZGYgPC0gZGYgJT4lIG11dGF0ZShoXzUgPSBwcmVkaWN0KHRyZWVfNSwgZGYpLA0KICAgICAgICAgICAgICAgICAgICBwXzUgPSBwXzQgKyBoXzUsDQogICAgICAgICAgICAgICAgICAgIHlfNSA9IHkgLSBwXzUpDQoNCnRyZWVfNiA8LSBycGFydCh5XzV+WCwgZGF0YT1kZiwgbWV0aG9kPSdhbm92YScsIGNvbnRyb2w9bGlzdChjcD0wLjAwMDAwMDEsIG1heGRlcHRoPTIpKSAgICAgICAgICAgICAgICANCg0KZGYgPC0gZGYgJT4lIG11dGF0ZShoXzYgPSBwcmVkaWN0KHRyZWVfNiwgZGYpLA0KICAgICAgICAgICAgICAgICAgICBwXzYgPSBwXzUgKyBoXzYsDQogICAgICAgICAgICAgICAgICAgIHlfNiA9IHkgLSBwXzYpDQoNCmBgYA0KDQpBaG9yYSB0ZW5lbW9zIHVuIGVuc2FibGUgcXVlIGNvbnNpc3RlIGVuIHNlaXMgw6FyYm9sZXMgZGUgZGVjaXNpw7NuLiBQb2RlbW9zIGhhY2VyIHVuYSBwcmVkaWNjacOzbiBzb2JyZSB1bmEgaW5zdGFuY2lhIG51ZXZhIHNpbXBsZW1lbnRlIGFncmVnYW5kbyBsYXMgcHJlZGljY2lvbmVzIGRlIG51ZXN0cm9zIHNlaXMgw6FyYm9sZXMuDQoNCmBgYHtyIGZpZy5oZWlnaHQ9MTUsIGZpZy53aWR0aD0xMH0NCmdncHVicjo6Z2dhcnJhbmdlKG5jb2w9MiwgbnJvdz0zLA0KICAgICAgICAgICAgICAgICAgZ2dwbG90KGRmKSArIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBnZW9tX3BvaW50KGFlcyh4PVgsIHk9eSksIGNvbG9yPSdibHVlJykgKyANCiAgICAgICAgICAgICAgICAgICAgICAgICAgZ2VvbV9saW5lKGFlcyh4PVgsIHk9aF8xKSwgY29sb3I9J2dyZWVuJyksDQogICAgICAgICAgICAgICAgICBnZ3Bsb3QoZGYpICsgDQogICAgICAgICAgICAgICAgICAgICAgICAgIGdlb21fcG9pbnQoYWVzKHg9WCwgeT15KSwgY29sb3I9J2JsdWUnKSArIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBnZW9tX2xpbmUoYWVzKHg9WCwgeT1wXzEpLCBjb2xvcj0ncmVkJyksDQogICAgICAgICAgICAgICAgICBnZ3Bsb3QoZGYpICsgDQogICAgICAgICAgICAgICAgICAgICAgICAgIGdlb21fcG9pbnQoYWVzKHg9WCwgeT15XzEpLCBjb2xvcj0nYmx1ZScpICsgDQogICAgICAgICAgICAgICAgICAgICAgICAgIGdlb21fbGluZShhZXMoeD1YLCB5PWhfMiksIGNvbG9yPSdncmVlbicpICsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxpbWl0cz1jKC0wLjQsMSkpLA0KICAgICAgICAgICAgICAgICAgZ2dwbG90KGRmKSArIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBnZW9tX3BvaW50KGFlcyh4PVgsIHk9eSksIGNvbG9yPSdibHVlJykgKyANCiAgICAgICAgICAgICAgICAgICAgICAgICAgZ2VvbV9saW5lKGFlcyh4PVgsIHk9cF8yKSwgY29sb3I9J3JlZCcpLA0KICAgICAgICAgICAgICAgICAgZ2dwbG90KGRmKSArIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBnZW9tX3BvaW50KGFlcyh4PVgsIHk9eV8yKSwgY29sb3I9J2JsdWUnKSArIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBnZW9tX2xpbmUoYWVzKHg9WCwgeT1oXzMpLCBjb2xvcj0nZ3JlZW4nKSArDQogICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsaW1pdHM9YygtMC40LDEpKSwNCiAgICAgICAgICAgICAgICAgIGdncGxvdChkZikgKyANCiAgICAgICAgICAgICAgICAgICAgICAgICAgZ2VvbV9wb2ludChhZXMoeD1YLCB5PXkpLCBjb2xvcj0nYmx1ZScpICsgDQogICAgICAgICAgICAgICAgICAgICAgICAgIGdlb21fbGluZShhZXMoeD1YLCB5PXBfMyksIGNvbG9yPSdyZWQnKQ0KKQ0KYGBgDQoNCkVsIHBsb3QgbXVlc3RyYSBjw7NtbyB2YSBldm9sdWNpb25hbmRvIGVsIG1vZGVsbyBhIG1lZGlkYSBxdWUgdmFtb3MgYWdyZWdhbmRvIHVuYSBtYXlvciBjYW50aWRhZCBkZSBpdGVyYWNpb25lcy4gQSBsYSBpenF1aWVyZGEgdGVuZW1vcyBlbCBhanVzdGUgZGUgY2FkYSBtb2RlbG8gY29uIHN1IHByb3BpYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAoZXMgZGVjaXIsIGxvcyByZXNpZHVvcyBkZWwgbW9kZWxvIGFudGVyaW9yKSB5IGEgbGEgZGVyZWNoYSwgZWwgYWp1c3RlIGRlbCBlbnNhbWJsZSBnbG9iYWwgc29icmUgJFgkIGUgJHkkLg0KDQpQdWVkZSB2ZXJzZSBjw7NtbyBlbCBlbnNhbWJsZSBzZSB2YSBoYWNpZW5kbyBjYWRhIHZleiBtw6FzIHByZWNpc28gYSBtZWRpZGEgcXVlIHZhbW9zIGFncmVnYW5kbyBtb2RlbG9zLg0KDQoNCiMjIyDCv0N1w6FsZXMgc29uIGxvcyBoaXBlcnBhcsOhbWV0cm9zIGRlIEdCTT8NCg0KVmFtb3MgYSB2ZXIgYWxndW5vcyBwYXLDoW1ldHJvcyBkZSBHQk0gZW4gY2FyZXQgeSBzdSBpbnRlcnByZXRhY2nDs24uIEVsIG3DqXRvZG8gc2UgbGxhbWEgIkdCTSIgeSBzZSB1c2EsIGNvbW8gc2llbXByZSwgZW4gbGEgZnVuY2nDs24gYHRyYWluYC4gDQoNClZlbW9zIHF1ZSBlbiBgdHVuZUdyaWRgIGhheSBjdWF0cm8gcGFyw6FtZXRyb3MgcGFyYSBldmFsdWFyOg0KDQotIGBuLnRyZWVzYDogZXMgbGEgY2FudGlkYWQgZGUgbW9kZWxvcyBhIGVudHJlbmFyOyBlcyBkZWNpciwgbGEgY2FudGlkYWQgZGUgaXRlcmFjaW9uZXMNCi0gYGludGVyYWN0aW9uLmRlcHRoYDogbGEgY29tcGxlamlkYWQgZGUgY2FkYSDDoXJib2wgZGUgZGVjaXNpw7NuDQotIGBzaHJpbmthZ2VgOiB0YW1iacOpbiBsbGFtYWRhIF9sZWFybmluZyByYXRlXyBlbiBvdHJvcyBsZW5ndWFqZXMgZSBpbXBsZW1lbnRhY2lvbmVzLCB5IHB1ZWRlIGludGVycHJldGFyc2UgY29tbyBsYSBjb250cmlidWNpw7NuIGRlIGNhZGEgw6FyYm9sIGFsIG1vZGVsbyBmaW5hbC4uLiBhbCBzZXRlYXIgdW4gYHNocmlua2FnZWAgYmFqbywgcG9yIGVqZW1wbG8sIGBzaHJpbmthZ2U9MC4xYCBzZXLDoSBuZWNlc2FyaW8gdW5hIG1heW9yIGNhbnRpZGFkIGRlIMOhcmJvbGVzIHBhcmEgZW50cmVuYXIsIHBlcm8gbG9zIHJlc3VsdGFkb3MgdGVuZGVyw6FuIGEgc2VyIG1lam9yZXMuIA0KLSBgbi5taW5vYnNpbm5vZGVgOiBlbCB0YW1hw7FvIG3DrW5pbW8gcGFyYSBwb2RlciBoYWNlciB1biBzcGxpdCBlbiBjYWRhIG5vZG8NCg0KRW50cmVuYXJlbW9zIGRvcyBHQk0sIHVubyBjb24gcG9jb3Mgw6FyYm9sZXMgeSBfbGVhcmluZyByYXRlXyBhbHRvIHkgb3RybyBjb24gbGFzIGNhcmFjdGVyw61zdGljYXMgaW52ZXJzYXM6DQoNCmBgYHtyfQ0KdHJhaW5Db250cm9sIDwtIHRyYWluQ29udHJvbChtZXRob2Q9J25vbmUnKQ0KDQpnYm0gPC0gdHJhaW4oeX5YLCBtZXRob2Q9J2dibScsDQogICAgICAgICAgICAgZGF0YT1kZiwNCiAgICAgICAgICAgICB0ckNvbnRyb2w9dHJhaW5Db250cm9sLA0KICAgICAgICAgICAgIHR1bmVHcmlkPWRhdGEuZnJhbWUobi50cmVlcz0zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW50ZXJhY3Rpb24uZGVwdGg9MywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNocmlua2FnZT0xLjAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuLm1pbm9ic2lubm9kZT0yKQ0KKQ0KDQoNCmdibV9uM19zMDEgPC0gdHJhaW4oeX5YLCBtZXRob2Q9J2dibScsDQogICAgICAgICAgICAgZGF0YT1kZiwNCiAgICAgICAgICAgICB0ckNvbnRyb2w9dHJhaW5Db250cm9sLA0KICAgICAgICAgICAgIHR1bmVHcmlkPWRhdGEuZnJhbWUobi50cmVlcz0zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW50ZXJhY3Rpb24uZGVwdGg9MywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNocmlua2FnZT0wLjEsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuLm1pbm9ic2lubm9kZT0yKQ0KICAgICAgICAgICAgICkNCg0KZ2JtX24yMDBfczEgPC0gdHJhaW4oeX5YLCBtZXRob2Q9J2dibScsDQogICAgICAgICAgICAgZGF0YT1kZiwNCiAgICAgICAgICAgICB0ckNvbnRyb2w9dHJhaW5Db250cm9sLA0KICAgICAgICAgICAgIHR1bmVHcmlkPWRhdGEuZnJhbWUobi50cmVlcz0yMDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbnRlcmFjdGlvbi5kZXB0aD0zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hyaW5rYWdlPTEsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuLm1pbm9ic2lubm9kZT0yKQ0KICAgICAgICAgICAgICkNCg0KZ2JtX3Nsb3cgPC0gdHJhaW4oeX5YLCBtZXRob2Q9J2dibScsDQogICAgICAgICAgICAgZGF0YT1kZiwNCiAgICAgICAgICAgICB0ckNvbnRyb2w9dHJhaW5Db250cm9sLA0KICAgICAgICAgICAgIHR1bmVHcmlkPWRhdGEuZnJhbWUobi50cmVlcz0yMDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbnRlcmFjdGlvbi5kZXB0aD0zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hyaW5rYWdlPTAuMSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG4ubWlub2JzaW5ub2RlPTIpDQogICAgICAgICAgICAgKQ0KDQpnYm1fc2xvd18wNSA8LSB0cmFpbih5flgsIG1ldGhvZD0nZ2JtJywNCiAgICAgICAgICAgICBkYXRhPWRmLA0KICAgICAgICAgICAgIHRyQ29udHJvbD10cmFpbkNvbnRyb2wsDQogICAgICAgICAgICAgdHVuZUdyaWQ9ZGF0YS5mcmFtZShuLnRyZWVzPTIwMCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGludGVyYWN0aW9uLmRlcHRoPTMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaHJpbmthZ2U9MC4wNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG4ubWlub2JzaW5ub2RlPTIpDQogICAgICAgICAgICAgKQ0KDQpnYm1fc2xvd18wMDEgPC0gdHJhaW4oeX5YLCBtZXRob2Q9J2dibScsDQogICAgICAgICAgICAgZGF0YT1kZiwNCiAgICAgICAgICAgICB0ckNvbnRyb2w9dHJhaW5Db250cm9sLA0KICAgICAgICAgICAgIHR1bmVHcmlkPWRhdGEuZnJhbWUobi50cmVlcz0yMDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbnRlcmFjdGlvbi5kZXB0aD0zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hyaW5rYWdlPTAuMDAxLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbi5taW5vYnNpbm5vZGU9MikNCiAgICAgICAgICAgICApDQoNCg0KYGBgDQoNCg0KQWdyZWdhbW9zIGxhcyBwcmVkaWNjaW9uZXMgYWwgZGF0YXNldCB5IHBsb3RlYW1vczoNCg0KYGBge3IgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwfQ0KZGYgPC0gZGYgJT4lIG11dGF0ZSh5X2dibT1wcmVkaWN0KGdibSwgZGYpLA0KICAgICAgICAgICAgICAgICAgICB5X2dibV9uM19zMDE9cHJlZGljdChnYm1fbjNfczAxLCBkZiksDQogICAgICAgICAgICAgICAgICAgIHlfZ2JtX24yMDBfczE9cHJlZGljdChnYm1fbjIwMF9zMSwgZGYpLA0KICAgICAgICAgICAgICAgICAgICB5X2dibV9zbG93PXByZWRpY3QoZ2JtX3Nsb3csIGRmKSwNCiAgICAgICAgICAgICAgICAgICAgeV9nYm1fc2xvd18wNT1wcmVkaWN0KGdibV9zbG93XzA1LCBkZiksDQogICAgICAgICAgICAgICAgICAgIHlfZ2JtX3Nsb3dfMDAxPXByZWRpY3QoZ2JtX3Nsb3dfMDAxLCBkZikNCiAgICAgICAgICAgICAgICAgICAgKQ0KDQpnZ3B1YnI6OmdnYXJyYW5nZShuY29sPTIsIG5yb3c9MywNCmdncGxvdChkZikgKyANCiAgICAgICAgZ2VvbV9wb2ludChhZXMoeD1YLCB5PXkpLCBjb2xvcj0nYmx1ZScpICsgDQogICAgICAgIGdlb21fbGluZShhZXMoeD1YLHk9eV9nYm0pKSArDQogICAgICAgIGxhYnModGl0bGU9J24udHJlZXM9Mywgc2hyaW5rYWdlPTEnKSwNCg0KZ2dwbG90KGRmKSArIA0KICAgICAgICBnZW9tX3BvaW50KGFlcyh4PVgsIHk9eSksIGNvbG9yPSdibHVlJykgKyANCiAgICAgICAgZ2VvbV9saW5lKGFlcyh4PVgseT15X2dibV9uM19zMDEpKSArDQogICAgICAgIGxhYnModGl0bGU9J24udHJlZXM9Mywgc2hyaW5rYWdlPTAuMScpLA0KDQoNCmdncGxvdChkZikgKyANCiAgICAgICAgZ2VvbV9wb2ludChhZXMoeD1YLCB5PXkpLCBjb2xvcj0nYmx1ZScpICsgDQogICAgICAgIGdlb21fbGluZShhZXMoeD1YLHk9eV9nYm1fbjIwMF9zMSkpICsNCiAgICAgICAgbGFicyh0aXRsZT0nbi50cmVlcz0yMDAsIHNocmlua2FnZT0xJyksDQoNCmdncGxvdChkZikgKyANCiAgICAgICAgZ2VvbV9wb2ludChhZXMoeD1YLCB5PXkpLCBjb2xvcj0nYmx1ZScpICsgDQogICAgICAgIGdlb21fbGluZShhZXMoeD1YLHk9eV9nYm1fc2xvdykpICsNCiAgICAgICAgbGFicyh0aXRsZT0nbi50cmVlcz0yMDAsIHNocmlua2FnZT0wLjEnKSwNCg0KZ2dwbG90KGRmKSArIA0KICAgICAgICBnZW9tX3BvaW50KGFlcyh4PVgsIHk9eSksIGNvbG9yPSdibHVlJykgKyANCiAgICAgICAgZ2VvbV9saW5lKGFlcyh4PVgseT15X2dibV9zbG93XzA1KSkgKw0KICAgICAgICBsYWJzKHRpdGxlPSduLnRyZWVzPTIwMCwgc2hyaW5rYWdlPTAuMDUnKSwNCg0KZ2dwbG90KGRmKSArIA0KICAgICAgICBnZW9tX3BvaW50KGFlcyh4PVgsIHk9eSksIGNvbG9yPSdibHVlJykgKyANCiAgICAgICAgZ2VvbV9saW5lKGFlcyh4PVgseT15X2dibV9zbG93XzAwMSkpICsNCiAgICAgICAgbGFicyh0aXRsZT0nbi50cmVlcz0yMDAsIHNocmlua2FnZT0wLjAwMScpDQopDQpgYGANCg0Kwr9Dw7NtbyBkZXNjcmliaXIgYSBjYWRhIHVubz8gRWwgZGUgbGEgaXpxdWllcmRhIHBhcmVjZSBubyBzZXIgbG8gc3VmaWNpZW50ZW1lbnRlIGZsZXhpYmxlLCBtaWVudHJhcyBxdWUgZWwgZGUgbGEgZGVyZWNoYSBwYXJlY2UgZ2VuZXJhciBfb3ZlcmZpdHRpbmdfIGRlIGZvcm1hIGJhc3RhbnRlIGNsYXJhLg0K