Objetivos

Disclaimer

Mucho del contenido de este tutorial está basado en Interpretable Machine Learning. A Guide for Making Black Box Models Explainable de Cristpher Molnar, un texto súper recomendable.

Interpretabilidad

Hasta aquí hemos venido tratando a los modelos vistos como cajas negras. Hemos priorizado capacidad predictiva por sobre interpretabilidad. No obstante, el problema de extraer conocimiento de los modelos que hemos llamado de “caja negra” no es una cuestión trivial. En ese sentido, si bien la performance predictiva de un modelo es sumamente importante, no otorga la información completa sobre el problema abordado. Si bien, en muchos casos es posible que nos interese apoyarnos en un modelo con mayor capacidad predictiva, en otros, también podría ser necesario entender qué nos dice dicho modelo sobre el dataset en cuestión. Entender los “por qué” del funcionamiento de un modelo, además, nos puede dar información valiosa para entender cuándo y cómo el mismo puede fallar. Los modelos de machine learning pueden tomar sesgos de los datos sobre los que se entrenan, es por ello, que la “interpretabilidad” puede ser un buen insumo para entender dichos sesgos.

Tipos de métodos de interpretabilidad

En líneas generales, podemos pensar que existen modelos qué son interpretables intrínsecamente: una regresión lineal, un árbol de decisión son métodos que no requieren de aproximaciones complicadas para entender qué nos dicen sobre un problema. En estos casos, los parámetros del modelo o ciertos estadísticos resumen suelen ser suficientes para extraer la información que nos brindan.

En algunos casos, existen herramientas de interpretación específicas: se limitan a clases de modelo específicas. La interpretación de los pesos de regresión en un modelo lineal es una interpretación específica del modelo, ya que, por definición, la interpretación de modelos intrínsecamente interpretables siempre es específica del modelo. Las redes neuronales son específicas del modelo.

Las herramientas agnósticas de modelo se pueden usar en cualquier modelo de aprendizaje automático y se aplican después de que el modelo haya sido entrenado (post hoc). Estos métodos agnósticos generalmente funcionan analizando los pares de entrada y salida de características. Por definición, estos métodos no pueden tener acceso a los componentes internos del modelo, como los pesos o la información estructural.

Carga de librerías y datos

library(caret)
library(tidyverse)
library(rpart)

Datos

Vamos a trabajar con un dataset nuevo y “foráneo”: está incluido en la librería MASS y que es ampliamente utilizado para ilustrar y testear modelos de Machine Learning y afines:

df <- MASS::Boston %>% mutate(chas=factor(chas, labels=c('No','Si')))
head(df)

Cada fila es un distrito del estado de Boston y cada variable mide diferentes atributos:

El objetivo será predecir el valor mediano de las propiedades en el condado -MEDV- según el resto de las varibles.

Random Forest

Entrenemos un modelo Random Forest con este dataset:

set.seed(282)
tr_index <- createDataPartition(y=df$medv,
                                p=0.8,
                                list=FALSE)

train <- df[tr_index,]
test <- df[-tr_index,]

set.seed(655)
cv_index <- createFolds(y=train$medv,
                                k=5,
                                list=TRUE,
                                returnTrain=TRUE)

rf_trControl <- trainControl(
        index=cv_index,
        method="cv",
        number=5        
        )

rf_grid <- expand.grid(mtry=1:13,
                       min.node.size=c(5,10,15,20),
                       splitrule='variance'
                       )

t0 <- proc.time()
rf_fit <-  train(medv ~ . , 
                 data = train, 
                 method = "ranger", 
                 trControl = rf_trControl,
                 tuneGrid = rf_grid,
                 importance='impurity')
proc.time() -  t0
   user  system elapsed 
 73.037   1.468  14.298 

Variable Importance

Un primer instrumento para comenzar a entender e interpretar los modelos de “caja negra” es preguntarnos por la importancia de las variables. Esta pregunta en modelos de regresión tienen una respuesta más bien evidente: aquellos \(\beta_{p}\) más grandes definirán las variables de mayor importancia, es decir, las que más “influyen” en \(y\).

En el caso de los modelos de ensamble la pregunta es ligeramente diferente: una variable es importante si “mezclando” sus valores el error de predicción se incrementa. Si una variable no es importante, entonces, mezclar sus valores no debería alterar el error. Un método para lograr esto es el siguiente:

  1. Calcular el error original (\(e^\text{orig}\ell(y, f(X))\))
  2. Para cada feature \(j=1,..., p\); 2.1 Generar una matriz nueva permutando \(X_{j}\). Esto rompe la asociación entre \(X_{j}\) e \(y\). 2.2 Caclular \(e^\text{perm}\ell(y, f(X^\text{perm}))\) sobre los datos permutados 2.3 Calcular la permutation feature importance \(FI_{j}=\frac{e^\text{perm}}{e^\text{orig}}\). También podría calcularse \(FI_{j}=e^\text{perm}-e^\text{orig}\)
  3. Ordenar las variables según \(FI_{j}\)

Veamos. Usemos un paquete llamado iml. Vamos a encapsular el proceso en una sola función.

varimp <- function(data, y, model, loss='mse'){
        bool <- !names(data) %in% y
        X <- data[,bool]
        predic <- iml::Predictor$new(model, data=X, y=data[y])
        vi <- iml::FeatureImp$new(predic, loss='mse')
        return(vi)
}

Calculemos \(FI\) sobre ambos sets de datos:

ggpubr::ggarrange(
        plot(varimp(data=train, y='medv', model=rf_fit)),
        plot(varimp(data=test, y='medv', model=rf_fit, loss='mse'))
        )

Ventajas

Se trata de una métrica sumamente intuitiva e interpretable: la importancia de una variable está definida como el incremento en el error cuando “desaparece” la información de la variable. Da una visión bastante resumida del modelo.

Es comparable entre diferentes modelos, algoritmos y problemas,

Incorpora en el análisis la interacción con otros features. Al romper la información de la variable analizada, también se rompen las interacciones entre esa variable y el resto. Esto puede ser visto como un problema también…

No requiere reentrenar todo el modelo. De esta forma, se reduce el tiempo de cómputo.

Desventajas

No está claro si debe usarse en el train set o el test set.

Dado que utiliza un proceso de randomización, es necesario tener en cuenta que los resultados pueden variar si la randomización cambia. Suele ser bueno repetir la randomización y chequear los resultados pero esto incrementa el tiempo de cómputo.

Si existe una correlación fuerte entre dos o más features la métrica puede engañosa. Al permutar las variables pueden generarse ejemplos “irreales”. Por ejemplo, si tuviéramos como variables peso y edad, al permutar una de las dos, podría encontrar un caso en el que hay una persona de 2 años y 75 kg.

Por una razón parecida, incorporar features muy correlacionadas puede hacer que la importancia de un feature muy relevante se “divida” entre ellos.

Partial Dependence Plots

Los gráficos de dependencia parcial permiten visualizar el efecto marginal de una o dos variables independientes sobre la variable predicha de un modelo de Machine Learning. Un plot de dependencia parcial puede mostrar si la relación entre \(X\) e \(y\) es lineal, monotónica, o algún patrón más complejo. Si aplicáramos un pdp a una regresión lineal, ¿cuál sería el patrón del gráfico?

Los PDP ayudan a visualizar la relación entre un subconjunto de las características (generalmente 1-3) y la respuesta al tiempo que explican el efecto promedio de los otros predictores en el modelo. Son particularmente efectivos con modelos de caja negra.

Una forma intuitiva (no necesariamente eficiente) de construir un PDP es la siguiente:

Para un predictor \(X_{p}\), cuyos valores van de \(X_{p}^1, ..., X_{p}^k\) su PDP puede ser construido de la siguiente forma:

  1. Para cada valor de \(X_{p}^i\), tal que \(i \in \{1,2,3,...,k\}\): 1.1 Copiar los datos de entrenamiento y reemplazar los valores originales de \(X_{p}\) por la constante \(X_{p}^i\) 1.2 Hacer las predicciones del modelo sobre este dataset modificado 1.3 Calcular la predicción promedio
  2. Plotar los pares \(\{X_{p}^i, \hat{f}(X_{p}^i\}\) para cada valor

Vamos a usar pdp, una librería específica para este tipo de cálculos.

library(pdp)

Y calculemos las dependencias parciales:

partial(rf_fit, pred.var='rm')

El output es un data.frame que contiene cada valor de rm y la estimación efecto promedio de la variable dependiente medv. Podemos plotear esto de forma simple:

partial(rf_fit, pred.var='rm', plot=TRUE, plot.engine='ggplot', rug=TRUE)

O podemos hacerlo con ggpplot desde cero:

rf_fit %>%
        partial(pred.var='rm') %>%
        ggplot(aes(x=rm, y=yhat)) +
                geom_line() +
                geom_smooth(se=FALSE) +
                theme_minimal()

Podemos ver, también dos variables al mismo tiempo:

pd <- partial(rf_fit, pred.var=c('rm', 'lstat'))
plotPartial(pd)

Veamos ahora qué pasa si ploteamos una variable cualitativa como predictor:

partial(rf_fit, pred.var='chas') %>%
        ggplot(aes(x=chas, y=yhat)) +
                geom_bar(stat='identity') +
                theme_minimal()

Ventajas

El cálculo de las PDP es sumamente intuitivo: la función de dependencia parcial en una feature particular es algo así como la predicción promedio si forzamos a que todos los puntos asuman el mismo valor en esa feature.

Si la feature para la cual se ha computado la PDP no está correlacionada con el resto, entonces, las PDS representan perfectamente cómo es la influencia de \(X_{p}\) en promedio. En este caso la interpretación es clara: el plot muestra cómo cambia la predicción promedio en el dataset cuando el j-ésimo feature cambia. Si los features están correlacionados, entonces, es más complicado.

Son fáciles de implementar.

Desventajas

Hay un límite claro a la cantidad de features que pueden plotearse: 2.

En algunos PDP no se muestra la distribución de los features. Esto puede ser engañoso porque es posible que se “sobreinterprete” alguna región sin datos. Por eso, suele agregarse el “rug” a los PDPs.

El supuesto de independencia es el mayor problema con los PDP. Sucede algo parecido al caso de las \(FI\): al asumir independencia y “promediar” sobre el resto del feature, incluimos ciertos valores que pueden ser irreales para los valores del feature que estamos ploteando. En otras palabras: cuando las características están correlacionadas, creamos nuevos puntos de datos en áreas de la distribución de características donde la probabilidad real es muy baja (por ejemplo, es poco probable que alguien mida 2 metros de altura pero pese menos de 50 kg). Una solución a este problema son las gráficas de efecto local acumulado o ALE cortas que funcionan con la distribución condicional en lugar de la marginal.

Cierto tipo de efectos pueden quedar ocultos porque los PDP solo muestran el efecto marginal promedio. Supongamos que la mitad de los datos tienen una relación positiva con el feature -a mayor feature, mayor es la predicción- y la otra mitad, una relación inversa. El PDP va a mostrar una curva horizontal, porque muestra el promedio y los efectos se cancelan. Podría concluirse que el feature en cuestión no tiene efectos. Una solución a esto son los Individual Conditional Expectation Curves que plotean cada efecto individual, en lugar de los agregados.

Individual Conditional Expectation (ICE)

El gráfico de dependencia parcial para el efecto promedio de una característica es un método global porque no se enfoca en instancias específicas, sino en un promedio general. El equivalente a un PDP para instancias de datos individuales se llama gráfico de expectativa condicional individual (ICE). Un gráfico ICE visualiza la dependencia de la predicción en una característica para cada instancia por separado, lo que da como resultado una línea por instancia, en comparación con una línea general en los gráficos de dependencia parcial.

Un PDP es el promedio de las líneas de un diagrama ICE. Los valores para una línea (y una instancia) se pueden calcular manteniendo todas las otras características iguales, creando variantes de esta instancia reemplazando el valor de la característica con valores de una cuadrícula y haciendo predicciones con el modelo de caja negra para estas instancias recién creadas. El resultado es un conjunto de puntos para una instancia con el valor de la característica de la cuadrícula y las predicciones respectivas.

¿Cuál es el punto de mirar las expectativas individuales en lugar de las dependencias parciales? Los plots de dependencia parcial pueden oscurecer una relación heterogénea creada por las interacciones. Los PDP pueden mostrarle cómo se ve la relación promedio entre una característica y la predicción. Esto solo funciona bien si las interacciones entre las características para las cuales se calcula el PDP y las otras características son débiles. En caso de interacciones, la trama ICE proporcionará mucha más información.

Veamos un ICE plot para las dos variables con mayor \(FI\):

ggpubr::ggarrange(
        partial(rf_fit, pred.var='rm', plot=TRUE, ice=TRUE, rug=TRUE, 
        plot.engine = 'ggplot', alpha=0.1),
        partial(rf_fit, pred.var='lstat', plot=TRUE, ice=TRUE, rug=TRUE, 
        plot.engine = 'ggplot', alpha=0.1)
)

Podemos ver que en el primer plot, en la mayoría de los casos nos encontramos con que se mantiene la pauta original: a mayor rm -mayor cantidad promedio de ambientes por hogar- se observa un mayor precio mediano en los condados. Y, de hecho, parece haber un salto hacia los 7 ambientes. No obstante, existen algunos casos en los que esto no es así: de hecho, el precio mediano en algunos condados cae a partir de los 7 ambientes por hogar.

En el segundo gráfico, y más allá de algunas diferencias menores, puede verse que todos los casos parecen cumplir la pauta original.

LS0tCnRpdGxlOiAiSW50ZXJwcmV0YWJsZSBNYWNoaW5lIExlYXJuaW5nIgphdXRob3I6ICJHZXJtw6FuIFJvc2F0aSIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKIyMgT2JqZXRpdm9zICAgIAoKLSBNb3N0cmFyIGxhIGltcGxlbWVudGFjacOzbiBkZSBhbGd1bm9zIG3DqXRvZG9zIGRlIGludGVycHJldGFjacOzbiBwYXJhIG1vZGVsb3MgZGUgTWFjaGluZSBMZWFybmluZwoKCiMjIERpc2NsYWltZXIKTXVjaG8gZGVsIGNvbnRlbmlkbyBkZSBlc3RlIHR1dG9yaWFsIGVzdMOhIGJhc2FkbyBlbiBbSW50ZXJwcmV0YWJsZSBNYWNoaW5lIExlYXJuaW5nLiBBIEd1aWRlIGZvciBNYWtpbmcgQmxhY2sgQm94IE1vZGVscyBFeHBsYWluYWJsZV0oaHR0cHM6Ly9jaHJpc3RvcGhtLmdpdGh1Yi5pby9pbnRlcnByZXRhYmxlLW1sLWJvb2svKSBkZSBDcmlzdHBoZXIgTW9sbmFyLCB1biB0ZXh0byBzw7pwZXIgcmVjb21lbmRhYmxlLgoKIyMgSW50ZXJwcmV0YWJpbGlkYWQKSGFzdGEgYXF1w60gaGVtb3MgdmVuaWRvIHRyYXRhbmRvIGEgbG9zIG1vZGVsb3MgdmlzdG9zIGNvbW8gY2FqYXMgbmVncmFzLiBIZW1vcyBwcmlvcml6YWRvIGNhcGFjaWRhZCBwcmVkaWN0aXZhIHBvciBzb2JyZSBpbnRlcnByZXRhYmlsaWRhZC4gTm8gb2JzdGFudGUsIGVsIHByb2JsZW1hIGRlIGV4dHJhZXIgY29ub2NpbWllbnRvIGRlIGxvcyBtb2RlbG9zIHF1ZSBoZW1vcyBsbGFtYWRvIGRlICJjYWphIG5lZ3JhIiBubyBlcyB1bmEgY3Vlc3Rpw7NuIHRyaXZpYWwuIApFbiBlc2Ugc2VudGlkbywgc2kgYmllbiBsYSBwZXJmb3JtYW5jZSBwcmVkaWN0aXZhIGRlIHVuIG1vZGVsbyBlcyBzdW1hbWVudGUgaW1wb3J0YW50ZSwgbm8gb3RvcmdhIGxhIGluZm9ybWFjacOzbiBjb21wbGV0YSBzb2JyZSBlbCBwcm9ibGVtYSBhYm9yZGFkby4gU2kgYmllbiwgZW4gbXVjaG9zIGNhc29zIGVzIHBvc2libGUgcXVlIG5vcyBpbnRlcmVzZSBhcG95YXJub3MgZW4gdW4gbW9kZWxvIGNvbiBtYXlvciBjYXBhY2lkYWQgcHJlZGljdGl2YSwgZW4gb3Ryb3MsIHRhbWJpw6luIHBvZHLDrWEgc2VyIG5lY2VzYXJpbyBlbnRlbmRlciBxdcOpIG5vcyBkaWNlIGRpY2hvIG1vZGVsbyBzb2JyZSBlbCBkYXRhc2V0IGVuIGN1ZXN0acOzbi4gRW50ZW5kZXIgbG9zICJwb3IgcXXDqSIgZGVsIGZ1bmNpb25hbWllbnRvIGRlIHVuIG1vZGVsbywgYWRlbcOhcywgbm9zIHB1ZWRlIGRhciBpbmZvcm1hY2nDs24gdmFsaW9zYSBwYXJhIGVudGVuZGVyIGN1w6FuZG8geSBjw7NtbyBlbCBtaXNtbyBwdWVkZSBmYWxsYXIuIExvcyBtb2RlbG9zIGRlIG1hY2hpbmUgbGVhcm5pbmcgcHVlZGVuIHRvbWFyIHNlc2dvcyBkZSBsb3MgZGF0b3Mgc29icmUgbG9zIHF1ZSBzZSBlbnRyZW5hbiwgZXMgcG9yIGVsbG8sIHF1ZSBsYSAiaW50ZXJwcmV0YWJpbGlkYWQiIHB1ZWRlIHNlciB1biBidWVuIGluc3VtbyBwYXJhIGVudGVuZGVyIGRpY2hvcyBzZXNnb3MuIAoKIyMjIFRpcG9zIGRlIG3DqXRvZG9zIGRlIGludGVycHJldGFiaWxpZGFkCkVuIGzDrW5lYXMgZ2VuZXJhbGVzLCBwb2RlbW9zIHBlbnNhciBxdWUgZXhpc3RlbiBtb2RlbG9zIHF1w6kgc29uIGludGVycHJldGFibGVzIGludHLDrW5zZWNhbWVudGU6IHVuYSByZWdyZXNpw7NuIGxpbmVhbCwgdW4gw6FyYm9sIGRlIGRlY2lzacOzbiBzb24gbcOpdG9kb3MgcXVlIG5vIHJlcXVpZXJlbiBkZSBhcHJveGltYWNpb25lcyBjb21wbGljYWRhcyBwYXJhIGVudGVuZGVyIHF1w6kgbm9zIGRpY2VuIHNvYnJlIHVuIHByb2JsZW1hLiBFbiBlc3RvcyBjYXNvcywgbG9zIHBhcsOhbWV0cm9zIGRlbCBtb2RlbG8gbyBjaWVydG9zIGVzdGFkw61zdGljb3MgcmVzdW1lbiBzdWVsZW4gc2VyIHN1ZmljaWVudGVzIHBhcmEgZXh0cmFlciBsYSBpbmZvcm1hY2nDs24gcXVlIG5vcyBicmluZGFuLgoKRW4gYWxndW5vcyBjYXNvcywgZXhpc3RlbiBfaGVycmFtaWVudGFzIGRlIGludGVycHJldGFjacOzbiBlc3BlY8OtZmljYXNfOiBzZSBsaW1pdGFuIGEgY2xhc2VzIGRlIG1vZGVsbyBlc3BlY8OtZmljYXMuIExhIGludGVycHJldGFjacOzbiBkZSBsb3MgcGVzb3MgZGUgcmVncmVzacOzbiBlbiB1biBtb2RlbG8gbGluZWFsIGVzIHVuYSBpbnRlcnByZXRhY2nDs24gZXNwZWPDrWZpY2EgZGVsIG1vZGVsbywgeWEgcXVlLCBwb3IgZGVmaW5pY2nDs24sIGxhIGludGVycHJldGFjacOzbiBkZSBtb2RlbG9zIGludHLDrW5zZWNhbWVudGUgaW50ZXJwcmV0YWJsZXMgc2llbXByZSBlcyBlc3BlY8OtZmljYSBkZWwgbW9kZWxvLiBMYXMgcmVkZXMgbmV1cm9uYWxlcyBzb24gZXNwZWPDrWZpY2FzIGRlbCBtb2RlbG8uIAogICAgICAgICAgICAgICAgCkxhcyBoZXJyYW1pZW50YXMgX2FnbsOzc3RpY2FzIGRlIG1vZGVsb18gc2UgcHVlZGVuIHVzYXIgZW4gY3VhbHF1aWVyIG1vZGVsbyBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyB5IHNlIGFwbGljYW4gZGVzcHXDqXMgZGUgcXVlIGVsIG1vZGVsbyBoYXlhIHNpZG8gZW50cmVuYWRvIChwb3N0IGhvYykuIEVzdG9zIG3DqXRvZG9zIGFnbsOzc3RpY29zIGdlbmVyYWxtZW50ZSBmdW5jaW9uYW4gYW5hbGl6YW5kbyBsb3MgcGFyZXMgZGUgZW50cmFkYSB5IHNhbGlkYSBkZSBjYXJhY3RlcsOtc3RpY2FzLiBQb3IgZGVmaW5pY2nDs24sIGVzdG9zIG3DqXRvZG9zIG5vIHB1ZWRlbiB0ZW5lciBhY2Nlc28gYSBsb3MgY29tcG9uZW50ZXMgaW50ZXJub3MgZGVsIG1vZGVsbywgY29tbyBsb3MgcGVzb3MgbyBsYSBpbmZvcm1hY2nDs24gZXN0cnVjdHVyYWwuCgojIyBDYXJnYSBkZSBsaWJyZXLDrWFzIHkgZGF0b3MKYGBge3IsIG1lc3NhZ2U9RkFMU0V9CmxpYnJhcnkoY2FyZXQpCmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KHJwYXJ0KQpgYGAKCiMjIERhdG9zClZhbW9zIGEgdHJhYmFqYXIgY29uIHVuIGRhdGFzZXQgbnVldm8geSAiZm9yw6FuZW8iOiBlc3TDoSBpbmNsdWlkbyBlbiBsYSBsaWJyZXLDrWEgYE1BU1NgIHkgcXVlIGVzIGFtcGxpYW1lbnRlIHV0aWxpemFkbyBwYXJhIGlsdXN0cmFyIHkgdGVzdGVhciBtb2RlbG9zIGRlIE1hY2hpbmUgTGVhcm5pbmcgeSBhZmluZXM6CgpgYGB7ciBlY2hvPVRSVUV9CmRmIDwtIE1BU1M6OkJvc3RvbiAlPiUgbXV0YXRlKGNoYXM9ZmFjdG9yKGNoYXMsIGxhYmVscz1jKCdObycsJ1NpJykpKQpoZWFkKGRmKQpgYGAKCkNhZGEgZmlsYSBlcyB1biBkaXN0cml0byBkZWwgZXN0YWRvIGRlIEJvc3RvbiB5IGNhZGEgdmFyaWFibGUgbWlkZSBkaWZlcmVudGVzIGF0cmlidXRvczogCgotIGBDUklNYDogcGVyIGNhcGl0YSBjcmltZSByYXRlIGJ5IHRvd24KLSBgWk5gOiBwcm9wb3J0aW9uIG9mIHJlc2lkZW50aWFsIGxhbmQgem9uZWQgZm9yIGxvdHMgb3ZlciAyNSwwMDAgc3EuZnQuCi0gYElORFVTYDogcHJvcG9ydGlvbiBvZiBub24tcmV0YWlsIGJ1c2luZXNzIGFjcmVzIHBlciB0b3duLgotIGBDSEFTYDogQ2hhcmxlcyBSaXZlciBkdW1teSB2YXJpYWJsZSAoMSBpZiB0cmFjdCBib3VuZHMgcml2ZXI7IDAgb3RoZXJ3aXNlKQotIGBOT1hgOiBuaXRyaWMgb3hpZGVzIGNvbmNlbnRyYXRpb24gKHBhcnRzIHBlciAxMCBtaWxsaW9uKQotIGBSTWA6IGF2ZXJhZ2UgbnVtYmVyIG9mIHJvb21zIHBlciBkd2VsbGluZwotIGBBR0VgOiBwcm9wb3J0aW9uIG9mIG93bmVyLW9jY3VwaWVkIHVuaXRzIGJ1aWx0IHByaW9yIHRvIDE5NDAKLSBgRElTYDogd2VpZ2h0ZWQgZGlzdGFuY2VzIHRvIGZpdmUgQm9zdG9uIGVtcGxveW1lbnQgY2VudHJlcwotIGBSQURgOiBpbmRleCBvZiBhY2Nlc3NpYmlsaXR5IHRvIHJhZGlhbCBoaWdod2F5cwotIGBUQVhgOiBmdWxsLXZhbHVlIHByb3BlcnR5LXRheCByYXRlIHBlciAkMTAsMDAwCi0gYFBUUkFUSU9gOiBwdXBpbC10ZWFjaGVyIHJhdGlvIGJ5IHRvd24KLSBgQmA6IDEwMDAoQmsgLSAwLjYzKV4yIHdoZXJlIEJrIGlzIHRoZSBwcm9wb3J0aW9uIG9mIGJsYWNrcyBieSB0b3duCi0gYExTVEFUYDogJSBsb3dlciBzdGF0dXMgb2YgdGhlIHBvcHVsYXRpb24KLSBgTUVEVmA6IE1lZGlhbiB2YWx1ZSBvZiBvd25lci1vY2N1cGllZCBob21lcyBpbiAkMTAwMCdzCgpFbCBvYmpldGl2byBzZXLDoSBwcmVkZWNpciBlbCB2YWxvciBtZWRpYW5vIGRlIGxhcyBwcm9waWVkYWRlcyBlbiBlbCBjb25kYWRvIC1gTUVEVmAtIHNlZ8O6biBlbCByZXN0byBkZSBsYXMgdmFyaWJsZXMuCgojIyBSYW5kb20gRm9yZXN0CkVudHJlbmVtb3MgdW4gbW9kZWxvIFJhbmRvbSBGb3Jlc3QgY29uIGVzdGUgZGF0YXNldDoKYGBge3J9CnNldC5zZWVkKDI4MikKdHJfaW5kZXggPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih5PWRmJG1lZHYsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcD0wLjgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGlzdD1GQUxTRSkKCnRyYWluIDwtIGRmW3RyX2luZGV4LF0KdGVzdCA8LSBkZlstdHJfaW5kZXgsXQoKc2V0LnNlZWQoNjU1KQpjdl9pbmRleCA8LSBjcmVhdGVGb2xkcyh5PXRyYWluJG1lZHYsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaz01LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxpc3Q9VFJVRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm5UcmFpbj1UUlVFKQoKcmZfdHJDb250cm9sIDwtIHRyYWluQ29udHJvbCgKICAgICAgICBpbmRleD1jdl9pbmRleCwKICAgICAgICBtZXRob2Q9ImN2IiwKICAgICAgICBudW1iZXI9NSAgICAgICAgCiAgICAgICAgKQoKcmZfZ3JpZCA8LSBleHBhbmQuZ3JpZChtdHJ5PTE6MTMsCiAgICAgICAgICAgICAgICAgICAgICAgbWluLm5vZGUuc2l6ZT1jKDUsMTAsMTUsMjApLAogICAgICAgICAgICAgICAgICAgICAgIHNwbGl0cnVsZT0ndmFyaWFuY2UnCiAgICAgICAgICAgICAgICAgICAgICAgKQoKdDAgPC0gcHJvYy50aW1lKCkKcmZfZml0IDwtICB0cmFpbihtZWR2IH4gLiAsIAogICAgICAgICAgICAgICAgIGRhdGEgPSB0cmFpbiwgCiAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJhbmdlciIsIAogICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IHJmX3RyQ29udHJvbCwKICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IHJmX2dyaWQsCiAgICAgICAgICAgICAgICAgaW1wb3J0YW5jZT0naW1wdXJpdHknKQpwcm9jLnRpbWUoKSAtICB0MApgYGAKCiMjIFZhcmlhYmxlIEltcG9ydGFuY2UKVW4gcHJpbWVyIGluc3RydW1lbnRvIHBhcmEgY29tZW56YXIgYSBlbnRlbmRlciBlIGludGVycHJldGFyIGxvcyBtb2RlbG9zIGRlICJjYWphIG5lZ3JhIiBlcyBwcmVndW50YXJub3MgcG9yIGxhIGltcG9ydGFuY2lhIGRlIGxhcyB2YXJpYWJsZXMuIEVzdGEgcHJlZ3VudGEgZW4gbW9kZWxvcyBkZSByZWdyZXNpw7NuIHRpZW5lbiB1bmEgcmVzcHVlc3RhIG3DoXMgYmllbiBldmlkZW50ZTogYXF1ZWxsb3MgJFxiZXRhX3twfSQgbcOhcyBncmFuZGVzIGRlZmluaXLDoW4gbGFzIHZhcmlhYmxlcyBkZSBtYXlvciBpbXBvcnRhbmNpYSwgZXMgZGVjaXIsIGxhcyBxdWUgbcOhcyAiaW5mbHV5ZW4iIGVuICR5JC4KCkVuIGVsIGNhc28gZGUgbG9zIG1vZGVsb3MgZGUgZW5zYW1ibGUgbGEgcHJlZ3VudGEgZXMgbGlnZXJhbWVudGUgZGlmZXJlbnRlOiB1bmEgdmFyaWFibGUgZXMgaW1wb3J0YW50ZSBzaSAibWV6Y2xhbmRvIiBzdXMgdmFsb3JlcyBlbCBlcnJvciBkZSBwcmVkaWNjacOzbiBzZSBpbmNyZW1lbnRhLiBTaSB1bmEgdmFyaWFibGUgbm8gZXMgaW1wb3J0YW50ZSwgZW50b25jZXMsIG1lemNsYXIgc3VzIHZhbG9yZXMgbm8gZGViZXLDrWEgYWx0ZXJhciBlbCBlcnJvci4gVW4gbcOpdG9kbyBwYXJhIGxvZ3JhciBlc3RvIGVzIGVsIHNpZ3VpZW50ZToKCgoxLiBDYWxjdWxhciBlbCBlcnJvciBvcmlnaW5hbCAoJGVeXHRleHR7b3JpZ31cZWxsKHksIGYoWCkpJCkKMi4gUGFyYSBjYWRhIGZlYXR1cmUgJGo9MSwuLi4sIHAkOwogICAgICAgIDIuMSBHZW5lcmFyIHVuYSBtYXRyaXogbnVldmEgcGVybXV0YW5kbyAkWF97an0kLiBFc3RvIHJvbXBlIGxhIGFzb2NpYWNpw7NuIGVudHJlICRYX3tqfSQgZSAkeSQuCiAgICAgICAgMi4yIENhY2x1bGFyICRlXlx0ZXh0e3Blcm19XGVsbCh5LCBmKFheXHRleHR7cGVybX0pKSQgc29icmUgbG9zIGRhdG9zIHBlcm11dGFkb3MKICAgICAgICAyLjMgQ2FsY3VsYXIgbGEgX3Blcm11dGF0aW9uIGZlYXR1cmUgaW1wb3J0YW5jZV8gJEZJX3tqfT1cZnJhY3tlXlx0ZXh0e3Blcm19fXtlXlx0ZXh0e29yaWd9fSQuIFRhbWJpw6luIHBvZHLDrWEgY2FsY3VsYXJzZSAkRklfe2p9PWVeXHRleHR7cGVybX0tZV5cdGV4dHtvcmlnfSQKMy4gT3JkZW5hciBsYXMgdmFyaWFibGVzIHNlZ8O6biAkRklfe2p9JAoKVmVhbW9zLiBVc2Vtb3MgdW4gcGFxdWV0ZSBsbGFtYWRvIGBpbWxgLiBWYW1vcyBhIGVuY2Fwc3VsYXIgZWwgcHJvY2VzbyBlbiB1bmEgc29sYSBmdW5jacOzbi4KYGBge3J9CnZhcmltcCA8LSBmdW5jdGlvbihkYXRhLCB5LCBtb2RlbCwgbG9zcz0nbXNlJyl7CiAgICAgICAgYm9vbCA8LSAhbmFtZXMoZGF0YSkgJWluJSB5CiAgICAgICAgWCA8LSBkYXRhWyxib29sXQogICAgICAgIHByZWRpYyA8LSBpbWw6OlByZWRpY3RvciRuZXcobW9kZWwsIGRhdGE9WCwgeT1kYXRhW3ldKQogICAgICAgIHZpIDwtIGltbDo6RmVhdHVyZUltcCRuZXcocHJlZGljLCBsb3NzPSdtc2UnKQogICAgICAgIHJldHVybih2aSkKfQpgYGAKCkNhbGN1bGVtb3MgJEZJJCBzb2JyZSBhbWJvcyBzZXRzIGRlIGRhdG9zOgpgYGB7cn0KZ2dwdWJyOjpnZ2FycmFuZ2UoCiAgICAgICAgcGxvdCh2YXJpbXAoZGF0YT10cmFpbiwgeT0nbWVkdicsIG1vZGVsPXJmX2ZpdCkpLAogICAgICAgIHBsb3QodmFyaW1wKGRhdGE9dGVzdCwgeT0nbWVkdicsIG1vZGVsPXJmX2ZpdCwgbG9zcz0nbXNlJykpCiAgICAgICAgKQpgYGAKCiMjIyBWZW50YWphcwpTZSB0cmF0YSBkZSB1bmEgbcOpdHJpY2Egc3VtYW1lbnRlIGludHVpdGl2YSBlIGludGVycHJldGFibGU6IGxhIGltcG9ydGFuY2lhIGRlIHVuYSB2YXJpYWJsZSBlc3TDoSBkZWZpbmlkYSBjb21vIGVsIGluY3JlbWVudG8gZW4gZWwgZXJyb3IgY3VhbmRvICJkZXNhcGFyZWNlIiBsYSBpbmZvcm1hY2nDs24gZGUgbGEgdmFyaWFibGUuIERhIHVuYSB2aXNpw7NuIGJhc3RhbnRlIHJlc3VtaWRhIGRlbCBtb2RlbG8uCgpFcyBjb21wYXJhYmxlIGVudHJlIGRpZmVyZW50ZXMgbW9kZWxvcywgYWxnb3JpdG1vcyB5IHByb2JsZW1hcywKCkluY29ycG9yYSBlbiBlbCBhbsOhbGlzaXMgbGEgaW50ZXJhY2Npw7NuIGNvbiBvdHJvcyBmZWF0dXJlcy4gQWwgcm9tcGVyIGxhIGluZm9ybWFjacOzbiBkZSBsYSB2YXJpYWJsZSBhbmFsaXphZGEsIHRhbWJpw6luIHNlIHJvbXBlbiBsYXMgaW50ZXJhY2Npb25lcyBlbnRyZSBlc2EgdmFyaWFibGUgeSBlbCByZXN0by4gRXN0byBwdWVkZSBzZXIgdmlzdG8gY29tbyB1biBwcm9ibGVtYSB0YW1iacOpbi4uLgoKTm8gcmVxdWllcmUgcmVlbnRyZW5hciB0b2RvIGVsIG1vZGVsby4gRGUgZXN0YSBmb3JtYSwgc2UgcmVkdWNlIGVsIHRpZW1wbyBkZSBjw7NtcHV0by4gCgojIyMgRGVzdmVudGFqYXMKTm8gZXN0w6EgY2xhcm8gc2kgZGViZSB1c2Fyc2UgZW4gZWwgdHJhaW4gc2V0IG8gZWwgdGVzdCBzZXQuIAoKRGFkbyBxdWUgdXRpbGl6YSB1biBwcm9jZXNvIGRlIHJhbmRvbWl6YWNpw7NuLCBlcyBuZWNlc2FyaW8gdGVuZXIgZW4gY3VlbnRhIHF1ZSBsb3MgcmVzdWx0YWRvcyBwdWVkZW4gdmFyaWFyIHNpIGxhIHJhbmRvbWl6YWNpw7NuIGNhbWJpYS4gU3VlbGUgc2VyIGJ1ZW5vIHJlcGV0aXIgbGEgcmFuZG9taXphY2nDs24geSBjaGVxdWVhciBsb3MgcmVzdWx0YWRvcyBwZXJvIGVzdG8gaW5jcmVtZW50YSBlbCB0aWVtcG8gZGUgY8OzbXB1dG8uCgpTaSBleGlzdGUgdW5hIGNvcnJlbGFjacOzbiBmdWVydGUgZW50cmUgZG9zIG8gbcOhcyBmZWF0dXJlcyBsYSBtw6l0cmljYSBwdWVkZSBlbmdhw7Fvc2EuIEFsIHBlcm11dGFyIGxhcyB2YXJpYWJsZXMgcHVlZGVuIGdlbmVyYXJzZSBlamVtcGxvcyAiaXJyZWFsZXMiLiBQb3IgZWplbXBsbywgc2kgdHV2acOpcmFtb3MgY29tbyB2YXJpYWJsZXMgYHBlc29gIHkgYGVkYWRgLCBhbCBwZXJtdXRhciB1bmEgZGUgbGFzIGRvcywgcG9kcsOtYSBlbmNvbnRyYXIgdW4gY2FzbyBlbiBlbCBxdWUgaGF5IHVuYSBwZXJzb25hIGRlIDIgYcOxb3MgeSA3NSBrZy4gCgpQb3IgdW5hIHJhesOzbiBwYXJlY2lkYSwgaW5jb3Jwb3JhciBmZWF0dXJlcyBtdXkgY29ycmVsYWNpb25hZGFzIHB1ZWRlIGhhY2VyIHF1ZSBsYSBpbXBvcnRhbmNpYSBkZSB1biBmZWF0dXJlIG11eSByZWxldmFudGUgc2UgImRpdmlkYSIgZW50cmUgZWxsb3MuCgojIyBQYXJ0aWFsIERlcGVuZGVuY2UgUGxvdHMKTG9zIGdyw6FmaWNvcyBkZSBkZXBlbmRlbmNpYSBwYXJjaWFsIHBlcm1pdGVuIHZpc3VhbGl6YXIgZWwgZWZlY3RvIG1hcmdpbmFsIGRlIHVuYSBvIGRvcyB2YXJpYWJsZXMgaW5kZXBlbmRpZW50ZXMgc29icmUgbGEgdmFyaWFibGUgcHJlZGljaGEgZGUgdW4gbW9kZWxvIGRlIE1hY2hpbmUgTGVhcm5pbmcuIFVuIHBsb3QgZGUgZGVwZW5kZW5jaWEgcGFyY2lhbCBwdWVkZSBtb3N0cmFyIHNpIGxhIHJlbGFjacOzbiBlbnRyZSAkWCQgZSAkeSQgZXMgbGluZWFsLCBtb25vdMOzbmljYSwgbyBhbGfDum4gcGF0csOzbiBtw6FzIGNvbXBsZWpvLiBTaSBhcGxpY8OhcmFtb3MgdW4gcGRwIGEgdW5hIHJlZ3Jlc2nDs24gbGluZWFsLCDCv2N1w6FsIHNlcsOtYSBlbCBwYXRyw7NuIGRlbCBncsOhZmljbz8KCkxvcyBQRFAgYXl1ZGFuIGEgdmlzdWFsaXphciBsYSByZWxhY2nDs24gZW50cmUgdW4gc3ViY29uanVudG8gZGUgbGFzIGNhcmFjdGVyw61zdGljYXMgKGdlbmVyYWxtZW50ZSAxLTMpIHkgbGEgcmVzcHVlc3RhIGFsIHRpZW1wbyBxdWUgZXhwbGljYW4gZWwgZWZlY3RvIHByb21lZGlvIGRlIGxvcyBvdHJvcyBwcmVkaWN0b3JlcyBlbiBlbCBtb2RlbG8uIFNvbiBwYXJ0aWN1bGFybWVudGUgZWZlY3Rpdm9zIGNvbiBtb2RlbG9zIGRlIGNhamEgbmVncmEuCgpVbmEgZm9ybWEgaW50dWl0aXZhIChubyBuZWNlc2FyaWFtZW50ZSBlZmljaWVudGUpIGRlIGNvbnN0cnVpciB1biBQRFAgZXMgbGEgc2lndWllbnRlOgoKUGFyYSB1biBwcmVkaWN0b3IgJFhfe3B9JCwgY3V5b3MgdmFsb3JlcyB2YW4gZGUgJFhfe3B9XjEsIC4uLiwgWF97cH1eayQgc3UgUERQIHB1ZWRlIHNlciBjb25zdHJ1aWRvIGRlIGxhIHNpZ3VpZW50ZSBmb3JtYToKCjEuIFBhcmEgY2FkYSB2YWxvciBkZSAkWF97cH1eaSQsIHRhbCBxdWUgJGkgXGluIFx7MSwyLDMsLi4uLGtcfSQ6CiAgICAgICAgMS4xIENvcGlhciBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50byB5IHJlZW1wbGF6YXIgbG9zIHZhbG9yZXMgb3JpZ2luYWxlcyBkZSAkWF97cH0kIHBvciBsYSBjb25zdGFudGUgJFhfe3B9XmkkCiAgICAgICAgMS4yIEhhY2VyIGxhcyBwcmVkaWNjaW9uZXMgZGVsIG1vZGVsbyBzb2JyZSBlc3RlIGRhdGFzZXQgbW9kaWZpY2FkbwogICAgICAgIDEuMyBDYWxjdWxhciBsYSBwcmVkaWNjacOzbiBwcm9tZWRpbwoyLiBQbG90YXIgbG9zIHBhcmVzICRce1hfe3B9XmksIFxoYXR7Zn0oWF97cH1eaVx9JCBwYXJhIGNhZGEgdmFsb3IKClZhbW9zIGEgdXNhciBgcGRwYCwgdW5hIGxpYnJlcsOtYSBlc3BlY8OtZmljYSBwYXJhIGVzdGUgdGlwbyBkZSBjw6FsY3Vsb3MuCmBgYHtyIG1lc3NhZ2U9RkFMU0V9CmxpYnJhcnkocGRwKQpgYGAKClkgY2FsY3VsZW1vcyBsYXMgZGVwZW5kZW5jaWFzIHBhcmNpYWxlczoKYGBge3J9CnBhcnRpYWwocmZfZml0LCBwcmVkLnZhcj0ncm0nKQpgYGAKCkVsIG91dHB1dCBlcyB1biBkYXRhLmZyYW1lIHF1ZSBjb250aWVuZSBjYWRhIHZhbG9yIGRlIGBybWAgeSBsYSBlc3RpbWFjacOzbiBlZmVjdG8gcHJvbWVkaW8gZGUgbGEgdmFyaWFibGUgZGVwZW5kaWVudGUgYG1lZHZgLiBQb2RlbW9zIHBsb3RlYXIgZXN0byBkZSBmb3JtYSBzaW1wbGU6CgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpwYXJ0aWFsKHJmX2ZpdCwgcHJlZC52YXI9J3JtJywgcGxvdD1UUlVFLCBwbG90LmVuZ2luZT0nZ2dwbG90JywgcnVnPVRSVUUpCmBgYAoKTyBwb2RlbW9zIGhhY2VybG8gY29uIGBnZ3BwbG90YCBkZXNkZSBjZXJvOgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpyZl9maXQgJT4lCiAgICAgICAgcGFydGlhbChwcmVkLnZhcj0ncm0nKSAlPiUKICAgICAgICBnZ3Bsb3QoYWVzKHg9cm0sIHk9eWhhdCkpICsKICAgICAgICAgICAgICAgIGdlb21fbGluZSgpICsKICAgICAgICAgICAgICAgIGdlb21fc21vb3RoKHNlPUZBTFNFKSArCiAgICAgICAgICAgICAgICB0aGVtZV9taW5pbWFsKCkKYGBgCgpQb2RlbW9zIHZlciwgdGFtYmnDqW4gZG9zIHZhcmlhYmxlcyBhbCBtaXNtbyB0aWVtcG86CmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CnBkIDwtIHBhcnRpYWwocmZfZml0LCBwcmVkLnZhcj1jKCdybScsICdsc3RhdCcpKQpwbG90UGFydGlhbChwZCkKYGBgCgpWZWFtb3MgYWhvcmEgcXXDqSBwYXNhIHNpIHBsb3RlYW1vcyB1bmEgdmFyaWFibGUgY3VhbGl0YXRpdmEgY29tbyBwcmVkaWN0b3I6CmBgYHtyfQpwYXJ0aWFsKHJmX2ZpdCwgcHJlZC52YXI9J2NoYXMnKSAlPiUKICAgICAgICBnZ3Bsb3QoYWVzKHg9Y2hhcywgeT15aGF0KSkgKwogICAgICAgICAgICAgICAgZ2VvbV9iYXIoc3RhdD0naWRlbnRpdHknKSArCiAgICAgICAgICAgICAgICB0aGVtZV9taW5pbWFsKCkKYGBgICAgICAKCiMjIyBWZW50YWphcwpFbCBjw6FsY3VsbyBkZSBsYXMgUERQIGVzIHN1bWFtZW50ZSBpbnR1aXRpdm86IGxhIGZ1bmNpw7NuIGRlIGRlcGVuZGVuY2lhIHBhcmNpYWwgZW4gdW5hIGZlYXR1cmUgIHBhcnRpY3VsYXIgZXMgYWxnbyBhc8OtIGNvbW8gbGEgcHJlZGljY2nDs24gcHJvbWVkaW8gc2kgZm9yemFtb3MgYSBxdWUgdG9kb3MgbG9zIHB1bnRvcyBhc3VtYW4gZWwgbWlzbW8gdmFsb3IgZW4gZXNhIGZlYXR1cmUuIAoKU2kgbGEgZmVhdHVyZSBwYXJhIGxhIGN1YWwgc2UgaGEgY29tcHV0YWRvIGxhIFBEUCBubyBlc3TDoSBjb3JyZWxhY2lvbmFkYSBjb24gZWwgcmVzdG8sIGVudG9uY2VzLCBsYXMgUERTIHJlcHJlc2VudGFuIHBlcmZlY3RhbWVudGUgY8OzbW8gZXMgbGEgaW5mbHVlbmNpYSBkZSAkWF97cH0kIGVuIHByb21lZGlvLiBFbiBlc3RlIGNhc28gbGEgaW50ZXJwcmV0YWNpw7NuIGVzIGNsYXJhOiBlbCBwbG90IG11ZXN0cmEgY8OzbW8gY2FtYmlhIGxhIHByZWRpY2Npw7NuIHByb21lZGlvIGVuIGVsIGRhdGFzZXQgY3VhbmRvIGVsIGotw6lzaW1vIGZlYXR1cmUgY2FtYmlhLiBTaSBsb3MgZmVhdHVyZXMgZXN0w6FuIGNvcnJlbGFjaW9uYWRvcywgZW50b25jZXMsIGVzIG3DoXMgY29tcGxpY2Fkby4KClNvbiBmw6FjaWxlcyBkZSBpbXBsZW1lbnRhci4KCiMjIyBEZXN2ZW50YWphcwpIYXkgdW4gbMOtbWl0ZSBjbGFybyBhIGxhIGNhbnRpZGFkIGRlIGZlYXR1cmVzIHF1ZSBwdWVkZW4gcGxvdGVhcnNlOiAyLiAKCkVuIGFsZ3Vub3MgUERQIG5vIHNlIG11ZXN0cmEgbGEgZGlzdHJpYnVjacOzbiBkZSBsb3MgZmVhdHVyZXMuIEVzdG8gcHVlZGUgc2VyIGVuZ2HDsW9zbyBwb3JxdWUgZXMgcG9zaWJsZSBxdWUgc2UgInNvYnJlaW50ZXJwcmV0ZSIgYWxndW5hIHJlZ2nDs24gc2luIGRhdG9zLiBQb3IgZXNvLCBzdWVsZSBhZ3JlZ2Fyc2UgZWwgInJ1ZyIgYSBsb3MgUERQcy4KCkVsIHN1cHVlc3RvIGRlIGluZGVwZW5kZW5jaWEgZXMgZWwgbWF5b3IgcHJvYmxlbWEgY29uIGxvcyBQRFAuIFN1Y2VkZSBhbGdvIHBhcmVjaWRvIGFsIGNhc28gZGUgbGFzICRGSSQ6IGFsIGFzdW1pciBpbmRlcGVuZGVuY2lhIHkgInByb21lZGlhciIgc29icmUgZWwgcmVzdG8gZGVsIGZlYXR1cmUsIGluY2x1aW1vcyBjaWVydG9zIHZhbG9yZXMgcXVlIHB1ZWRlbiBzZXIgaXJyZWFsZXMgcGFyYSBsb3MgdmFsb3JlcyBkZWwgZmVhdHVyZSBxdWUgZXN0YW1vcyBwbG90ZWFuZG8uICBFbiBvdHJhcyBwYWxhYnJhczogY3VhbmRvIGxhcyBjYXJhY3RlcsOtc3RpY2FzIGVzdMOhbiBjb3JyZWxhY2lvbmFkYXMsIGNyZWFtb3MgbnVldm9zIHB1bnRvcyBkZSBkYXRvcyBlbiDDoXJlYXMgZGUgbGEgZGlzdHJpYnVjacOzbiBkZSBjYXJhY3RlcsOtc3RpY2FzIGRvbmRlIGxhIHByb2JhYmlsaWRhZCByZWFsIGVzIG11eSBiYWphIChwb3IgZWplbXBsbywgZXMgcG9jbyBwcm9iYWJsZSBxdWUgYWxndWllbiBtaWRhIDIgbWV0cm9zIGRlIGFsdHVyYSBwZXJvIHBlc2UgbWVub3MgZGUgNTAga2cpLiBVbmEgc29sdWNpw7NuIGEgZXN0ZSBwcm9ibGVtYSBzb24gbGFzIGdyw6FmaWNhcyBkZSBlZmVjdG8gbG9jYWwgYWN1bXVsYWRvIG8gIEFMRSBjb3J0YXMgcXVlIGZ1bmNpb25hbiBjb24gbGEgZGlzdHJpYnVjacOzbiBjb25kaWNpb25hbCBlbiBsdWdhciBkZSBsYSBtYXJnaW5hbC4KCkNpZXJ0byB0aXBvIGRlIGVmZWN0b3MgcHVlZGVuIHF1ZWRhciBvY3VsdG9zIHBvcnF1ZSBsb3MgUERQIHNvbG8gbXVlc3RyYW4gZWwgZWZlY3RvIG1hcmdpbmFsIHByb21lZGlvLiBTdXBvbmdhbW9zIHF1ZSBsYSBtaXRhZCBkZSBsb3MgZGF0b3MgdGllbmVuIHVuYSByZWxhY2nDs24gcG9zaXRpdmEgY29uIGVsIGZlYXR1cmUgLWEgbWF5b3IgZmVhdHVyZSwgbWF5b3IgZXMgbGEgcHJlZGljY2nDs24tIHkgbGEgb3RyYSBtaXRhZCwgdW5hIHJlbGFjacOzbiBpbnZlcnNhLiBFbCBQRFAgdmEgYSBtb3N0cmFyIHVuYSBjdXJ2YSBob3Jpem9udGFsLCBwb3JxdWUgbXVlc3RyYSBlbCBwcm9tZWRpbyB5IGxvcyBlZmVjdG9zIHNlIGNhbmNlbGFuLiBQb2Ryw61hIGNvbmNsdWlyc2UgcXVlIGVsIGZlYXR1cmUgZW4gY3Vlc3Rpw7NuIG5vIHRpZW5lIGVmZWN0b3MuIFVuYSBzb2x1Y2nDs24gYSBlc3RvIHNvbiBsb3MgSW5kaXZpZHVhbCBDb25kaXRpb25hbCBFeHBlY3RhdGlvbiBDdXJ2ZXMgcXVlIHBsb3RlYW4gY2FkYSBlZmVjdG8gaW5kaXZpZHVhbCwgZW4gbHVnYXIgZGUgbG9zIGFncmVnYWRvcy4KICAgICAgICAKCiMjIEluZGl2aWR1YWwgQ29uZGl0aW9uYWwgRXhwZWN0YXRpb24gKElDRSkKRWwgZ3LDoWZpY28gZGUgZGVwZW5kZW5jaWEgcGFyY2lhbCBwYXJhIGVsIGVmZWN0byBwcm9tZWRpbyBkZSB1bmEgY2FyYWN0ZXLDrXN0aWNhIGVzIHVuIG3DqXRvZG8gZ2xvYmFsIHBvcnF1ZSBubyBzZSBlbmZvY2EgZW4gaW5zdGFuY2lhcyBlc3BlY8OtZmljYXMsIHNpbm8gZW4gdW4gcHJvbWVkaW8gZ2VuZXJhbC4gRWwgZXF1aXZhbGVudGUgYSB1biBQRFAgcGFyYSBpbnN0YW5jaWFzIGRlIGRhdG9zIGluZGl2aWR1YWxlcyBzZSBsbGFtYSBncsOhZmljbyBkZSBleHBlY3RhdGl2YSBjb25kaWNpb25hbCBpbmRpdmlkdWFsIChJQ0UpLiBVbiBncsOhZmljbyBJQ0UgdmlzdWFsaXphIGxhIGRlcGVuZGVuY2lhIGRlIGxhIHByZWRpY2Npw7NuIGVuIHVuYSBjYXJhY3RlcsOtc3RpY2EgcGFyYSBjYWRhIGluc3RhbmNpYSBwb3Igc2VwYXJhZG8sIGxvIHF1ZSBkYSBjb21vIHJlc3VsdGFkbyB1bmEgbMOtbmVhIHBvciBpbnN0YW5jaWEsIGVuIGNvbXBhcmFjacOzbiBjb24gdW5hIGzDrW5lYSBnZW5lcmFsIGVuIGxvcyBncsOhZmljb3MgZGUgZGVwZW5kZW5jaWEgcGFyY2lhbC4gCgpVbiBQRFAgZXMgZWwgcHJvbWVkaW8gZGUgbGFzIGzDrW5lYXMgZGUgdW4gZGlhZ3JhbWEgSUNFLiBMb3MgdmFsb3JlcyBwYXJhIHVuYSBsw61uZWEgKHkgdW5hIGluc3RhbmNpYSkgc2UgcHVlZGVuIGNhbGN1bGFyIG1hbnRlbmllbmRvIHRvZGFzIGxhcyBvdHJhcyBjYXJhY3RlcsOtc3RpY2FzIGlndWFsZXMsIGNyZWFuZG8gdmFyaWFudGVzIGRlIGVzdGEgaW5zdGFuY2lhIHJlZW1wbGF6YW5kbyBlbCB2YWxvciBkZSBsYSBjYXJhY3RlcsOtc3RpY2EgY29uIHZhbG9yZXMgZGUgdW5hIGN1YWRyw61jdWxhIHkgaGFjaWVuZG8gcHJlZGljY2lvbmVzIGNvbiBlbCBtb2RlbG8gZGUgY2FqYSBuZWdyYSBwYXJhIGVzdGFzIGluc3RhbmNpYXMgcmVjacOpbiBjcmVhZGFzLiBFbCByZXN1bHRhZG8gZXMgdW4gY29uanVudG8gZGUgcHVudG9zIHBhcmEgdW5hIGluc3RhbmNpYSBjb24gZWwgdmFsb3IgZGUgbGEgY2FyYWN0ZXLDrXN0aWNhIGRlIGxhIGN1YWRyw61jdWxhIHkgbGFzIHByZWRpY2Npb25lcyByZXNwZWN0aXZhcy4KCsK/Q3XDoWwgZXMgZWwgcHVudG8gZGUgbWlyYXIgbGFzIGV4cGVjdGF0aXZhcyBpbmRpdmlkdWFsZXMgZW4gbHVnYXIgZGUgbGFzIGRlcGVuZGVuY2lhcyBwYXJjaWFsZXM/IExvcyBwbG90cyBkZSBkZXBlbmRlbmNpYSBwYXJjaWFsIHB1ZWRlbiBvc2N1cmVjZXIgdW5hIHJlbGFjacOzbiBoZXRlcm9nw6luZWEgY3JlYWRhIHBvciBsYXMgaW50ZXJhY2Npb25lcy4gTG9zIFBEUCBwdWVkZW4gbW9zdHJhcmxlIGPDs21vIHNlIHZlIGxhIHJlbGFjacOzbiBwcm9tZWRpbyBlbnRyZSB1bmEgY2FyYWN0ZXLDrXN0aWNhIHkgbGEgcHJlZGljY2nDs24uIEVzdG8gc29sbyBmdW5jaW9uYSBiaWVuIHNpIGxhcyBpbnRlcmFjY2lvbmVzIGVudHJlIGxhcyBjYXJhY3RlcsOtc3RpY2FzIHBhcmEgbGFzIGN1YWxlcyBzZSBjYWxjdWxhIGVsIFBEUCB5IGxhcyBvdHJhcyBjYXJhY3RlcsOtc3RpY2FzIHNvbiBkw6liaWxlcy4gRW4gY2FzbyBkZSBpbnRlcmFjY2lvbmVzLCBsYSB0cmFtYSBJQ0UgcHJvcG9yY2lvbmFyw6EgbXVjaGEgbcOhcyBpbmZvcm1hY2nDs24uCgpWZWFtb3MgdW4gSUNFIHBsb3QgcGFyYSBsYXMgZG9zIHZhcmlhYmxlcyBjb24gbWF5b3IgJEZJJDoKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmdncHVicjo6Z2dhcnJhbmdlKAogICAgICAgIHBhcnRpYWwocmZfZml0LCBwcmVkLnZhcj0ncm0nLCBwbG90PVRSVUUsIGljZT1UUlVFLCBydWc9VFJVRSwgCiAgICAgICAgcGxvdC5lbmdpbmUgPSAnZ2dwbG90JywgYWxwaGE9MC4xKSwKICAgICAgICBwYXJ0aWFsKHJmX2ZpdCwgcHJlZC52YXI9J2xzdGF0JywgcGxvdD1UUlVFLCBpY2U9VFJVRSwgcnVnPVRSVUUsIAogICAgICAgIHBsb3QuZW5naW5lID0gJ2dncGxvdCcsIGFscGhhPTAuMSkKKQpgYGAKClBvZGVtb3MgdmVyIHF1ZSBlbiBlbCBwcmltZXIgcGxvdCwgZW4gbGEgbWF5b3LDrWEgZGUgbG9zIGNhc29zIG5vcyBlbmNvbnRyYW1vcyBjb24gcXVlIHNlIG1hbnRpZW5lIGxhIHBhdXRhIG9yaWdpbmFsOiBhIG1heW9yIGBybWAgLW1heW9yIGNhbnRpZGFkIHByb21lZGlvIGRlIGFtYmllbnRlcyBwb3IgaG9nYXItIHNlIG9ic2VydmEgdW4gbWF5b3IgcHJlY2lvIG1lZGlhbm8gZW4gbG9zIGNvbmRhZG9zLiBZLCBkZSBoZWNobywgcGFyZWNlIGhhYmVyIHVuIHNhbHRvIGhhY2lhIGxvcyA3IGFtYmllbnRlcy4gTm8gb2JzdGFudGUsIGV4aXN0ZW4gYWxndW5vcyBjYXNvcyBlbiBsb3MgcXVlIGVzdG8gbm8gZXMgYXPDrTogZGUgaGVjaG8sIGVsIHByZWNpbyBtZWRpYW5vIGVuIGFsZ3Vub3MgY29uZGFkb3MgY2FlIGEgcGFydGlyIGRlIGxvcyA3IGFtYmllbnRlcyBwb3IgaG9nYXIuCgpFbiBlbCBzZWd1bmRvIGdyw6FmaWNvLCB5IG3DoXMgYWxsw6EgZGUgYWxndW5hcyBkaWZlcmVuY2lhcyBtZW5vcmVzLCBwdWVkZSB2ZXJzZSBxdWUgdG9kb3MgbG9zIGNhc29zIHBhcmVjZW4gY3VtcGxpciBsYSBwYXV0YSBvcmlnaW5hbC4K