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?
- datos (\(X\) e \(y\))
- algoritmo o hipótesis (\(\hat{y}=f(X)\)) que contiene
- parámetros (por ejemplo, \(\beta_{0}, \beta_{1}, ..., \beta_{p}\) de un modelo de regresión lineal)
- métrica de costo o cost function, una medida que sirve para evaluar la performance del modelo \(\ell(y_{i} - \hat{y_{i}})\)
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
- tenemos nuestros datos \(x\) e \(y\)
- la hipótesis: \(\hat{y_{i}} = \beta_{0} + \beta_{1}X_{1}\)
- los parámetros: \((\beta_{0}, \beta_{1})\)
- función de costo: \(MSE = \frac{1}{N} \sum_{i=1}^N(y_{i} - \hat{y_{i}})^2\)
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:
n.trees
: es la cantidad de modelos a entrenar; es decir, la cantidad de iteraciones
interaction.depth
: la complejidad de cada árbol de decisión
shrinkage
: también llamada learning rate en otros lenguajes e implementaciones, y puede interpretarse como la contribución de cada árbol al modelo final… al setear un shrinkage
bajo, por ejemplo, shrinkage=0.1
será necesario una mayor cantidad de árboles para entrenar, pero los resultados tenderán a ser mejores.
n.minobsinnode
: el tamaño mínimo para poder hacer un split en cada nodo
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