Introducción al análisis no supervisado

El aprendizaje no supervisado es un método de aprendizaje no existe un conocimiento apriori de un fenómeno en los datos. No tenemos una variable dependiente o output. Buscamos entonces encontrar patrones sobre los datos. Las principales técnicas existentes de análisis no supervisado recaen sobre dos categorías:

  • Técnicas de reducción de dimensionalidad


  • Técnicas de agrupamiento o clustering


A lo largo de este taller visitaremos 3 técnicas, 2 de reducción de dimensionalidad y 1 de agrupamiento. Específicamente, traemos un problema entre manos: Con mira a coadyuvar al diseño de política pública ¿cómo agrupamos provincias, en función a algunas de sus características socioeconómicas, niveles de contagio por COVID-19 y número de detenciones durante el toque de queda?. Para resolverlo compararemos los resultados de usar ACP y ACP Robusto sobre nuestro conjunto de datos para luego agruparlos con k-medias.

Técnicas de reducción de dimensionalidad



Los métodos de reducción de dimensionalidad consisten en resumir y visualizar la información más importante contenida en un dataset. Existen varias técnicas alrededor de esta temática, tanto si asumimos que existen patrones lineales como no lineales en los datos. A continuación se enumeran algunas de estas técnicas, clasificándolas acorde al tipo de datos con el que estemos trabajando y algunos de los paquetes existentes para su uso:

TipoVariables Tecnica Librerias
Numéricos Análisis de componentes principales, t-SNE base, FactoMineR
Categóricos Análisis de correspondencias múltiples FactoMiner, ade4, epMCA
Mixtos Análisis factorial mixto FactoMiner

Técnicas de agrupación o clustering



Las técnicas de agrupamiento o clustering nos permiten obtener conocimiento a partir del descubrimiento de patrones existentes en los datos. Específicamente, el objetivo de los métodos de clustering yacen en la identificación de grupos de objetos similares en un conjunto de datos de interés a través de una medida de similaridad entre puntos (e.g la euclideana). A continuación se enumeran algunas de estas técnicas, clasificándolas acorde al tipo de datos con el que estemos trabajando y algunos de los paquetes existentes para su uso:

TipoVariables Tecnica Librerias
Numéricos k-medias, GMM, CLARA cluster, FactoMineR, mclust
Categóricos k-modas, otras medidas de distancia klaR
Mixtos k-prototypes, otras medidas de distancia clustMixType

Cabe notar que algunas técnicas podrían ser útiles para varios tipos de datos, dependiendo de la métrica que sea utilizada como distancia.

Resolución del problema

Para iniciar, carguemos las librerías necesarias:

library(tidyverse) # Manejo de datos
library(sp) # Datos geográficos
library(FactoMineR) # Técnicas de red
library(factoextra) # Complemento de visualización
library(rpca) # ACP Robusto
library(highcharter) # Gráficos interactivos
library(stargazer) # Tablas de resultados

Luego cargaremos nuestros datos:

# Carga de variables socioeconómicas y de contagio a nivel de provincia
CoronavirusProvincia_03 = readRDS("Data/CoronavirusProvincia_03.RDS")
# Carga de datos geográficos
provincias = readRDS("Mapas/ec_provincias.RDS")

Hechemos un rápido vistazo a los datos:

CoronavirusProvincia_03 %>% colnames()
 [1] "CODIGO_PROVINCIA"                      "CONFIRMADO_COVID"                     
 [3] "TOQUE_QUEDA_ECU911"                    "AGLOMERACIONES_ECU911"                
 [5] "DEFUNCIONES_RC"                        "DETENIDOS"                            
 [7] "DETENIDOS_SECUNDARIA"                  "DETENIDOS_HOMBRES"                    
 [9] "DETENIDOS_PAREJA"                      "DETENIDOS_SOLTERO"                    
[11] "DETENIDOS_CUANTIL_1_24A"               "DETENIDOS_CUANTIL_2_30A"              
[13] "DETENIDOS_CUANTIL_3_33A"               "DETENIDOS_CUANTIL_4_69A"              
[15] "PROVINCIA"                             "ANOS_ESCOLARIDAD_IND"                 
[17] "TASA_ANALFABETISMO_IND"                "NET_PRIMARIA_IND"                     
[19] "NET_BASICA_IND"                        "NET_SECUNDARIA_IND"                   
[21] "NET_BACHILLERATO_IND"                  "GROSS_PRIMARIA_IND"                   
[23] "GROSS_BASICA_IND"                      "GROSS_SECUNDARIA_IND"                 
[25] "GROSS_BACHILLERATO_IND"                "EMPLEO_BRUTO_IND"                     
[27] "EMPLEO_GLOBAL_IND"                     "EMPLEO_PLENO_IND"                     
[29] "SUBEMPLEO_IND"                         "EMPLEO_NO_REMUNERADO_IND"             
[31] "OTRO_EMPLEO_NO_PLENO_IND"              "DESEMPLEO_IND"                        
[33] "PARTICIPACION_GLOBAL_IND"              "PARTICIPACION_BRUTA_IND"              
[35] "SECTOR_INFORMAL_IND"                   "POBREZA_INGRESOS_IND"                 
[37] "POBREZA_EXTREMA_INGRESOS_IND"          "GINI_IND"                             
[39] "IPM_IND"                               "POBREZA_NBI_IND"                      
[41] "HACINAMIENTO_IND"                      "AGUA_RED_PUBLICA_SENAGUA_IND"         
[43] "AGUA_RED_PUBLICA_IND"                  "EXCRETAS_SENAGUA_IND"                 
[45] "EXCRETAS_IND"                          "DEFICIT_HABITACIONAL_CUALITATIVO_IND" 
[47] "DEFICIT_HABITACIONAL_CUANTITATIVO_IND" "ELECTRICIDAD_IND"                     
[49] "ALUMBRADO_PUBLICO_IND"                 "RECOLECCION_DESECHOS_SOLIDOS_IND"     
[51] "SERVICIOS_BASICOS_IND"                 "POBLACION_2020"                       
[53] "AREA_KM2"                             

Preprocesemos los datos a ser utilizados:

CoronavirusProvincia_03 = CoronavirusProvincia_03 %>% 
  select(-c(DETENIDOS_SECUNDARIA, DETENIDOS_HOMBRES, DETENIDOS_PAREJA, DETENIDOS_SOLTERO,
            DETENIDOS_CUANTIL_1_24A, DETENIDOS_CUANTIL_2_30A,
            DETENIDOS_CUANTIL_3_33A, DETENIDOS_CUANTIL_4_69A))%>% 
  left_join(data.frame(CODIGO_PROVINCIA=provincias$CC_1, 
                       LONG=coordinates(provincias)[,1], 
                       LAT=coordinates(provincias)[,2]))
Joining, by = "CODIGO_PROVINCIA"
MapaProvincias03 = provincias

Creamos algunas variables que permitan realizar comparaciones entre provincias:

CoronavirusProvincia_03 = CoronavirusProvincia_03 %>%
  mutate(RATIO_CONFIRMADOS = CONFIRMADO_COVID/(POBLACION_2020/AREA_KM2),
         RATIO_DEFUNCIONES = DEFUNCIONES_RC/(POBLACION_2020/AREA_KM2),
         RATIO_DETENIDOS = DETENIDOS/(POBLACION_2020/AREA_KM2),
         AGLOMERACIONES_PC = AGLOMERACIONES_ECU911/(POBLACION_2020/AREA_KM2),
         TOQUE_QUEDA_PC = TOQUE_QUEDA_ECU911/(POBLACION_2020/AREA_KM2))

Realizamos una selección de variables para realizar la reducción de dimensionalidad:

CoronavirusProvincia_03 = CoronavirusProvincia_03 %>% 
  select(CODIGO_PROVINCIA, PROVINCIA,
         RATIO_CONFIRMADOS, TOQUE_QUEDA_PC, AGLOMERACIONES_PC, RATIO_DEFUNCIONES, RATIO_DETENIDOS,
         CONFIRMADO_COVID, TOQUE_QUEDA_ECU911, AGLOMERACIONES_ECU911, DEFUNCIONES_RC, DETENIDOS,
         DESEMPLEO_IND, SUBEMPLEO_IND, SECTOR_INFORMAL_IND,
         ANOS_ESCOLARIDAD_IND, 
         POBREZA_INGRESOS_IND, POBREZA_EXTREMA_INGRESOS_IND,
         GINI_IND, IPM_IND,
         HACINAMIENTO_IND, SERVICIOS_BASICOS_IND,
         LONG, LAT)

Observamos algunas medidas de tendencia central descriptivas:

stargazer(as.data.frame(CoronavirusProvincia_03), type = "text", summary = T)

==================================================================================
Statistic                    N   Mean   St. Dev.   Min   Pctl(25) Pctl(75)   Max  
----------------------------------------------------------------------------------
RATIO_CONFIRMADOS            23  0.553   1.016    0.000   0.097    0.502    4.839 
TOQUE_QUEDA_PC               23  8.517   10.831   0.000   2.261    9.442   49.072 
AGLOMERACIONES_PC            23  4.458   2.834    0.883   2.486    5.576   12.549 
RATIO_DEFUNCIONES            23  1.242   1.695    0.324   0.575    1.112    8.627 
RATIO_DETENIDOS              23  0.892   0.890    0.056   0.231    1.199    2.824 
CONFIRMADO_COVID             23 82.000  284.390     0       5        25     1,376 
TOQUE_QUEDA_ECU911           23 517.522 570.718     0     121.5     673     1,864 
AGLOMERACIONES_ECU911        23 394.478 513.760    27      73.5    405.5    1,944 
DEFUNCIONES_RC               23 178.913 505.040     5       12       96     2,453 
DETENIDOS                    23 48.217   65.642     3       14       44      307  
DESEMPLEO_IND                23  0.035   0.018    0.013   0.025    0.042    0.086 
SUBEMPLEO_IND                23  0.168   0.046    0.099   0.143    0.181    0.275 
SECTOR_INFORMAL_IND          23  0.529   0.115    0.237   0.461    0.583    0.750 
ANOS_ESCOLARIDAD_IND         23  9.473   0.964    7.174   9.070    10.051  11.979 
POBREZA_INGRESOS_IND         23  0.308   0.116    0.127   0.232    0.396    0.530 
POBREZA_EXTREMA_INGRESOS_IND 23  0.121   0.084    0.035   0.060    0.159    0.327 
GINI_IND                     23  0.485   0.040    0.424   0.449    0.509    0.575 
IPM_IND                      23  0.448   0.141    0.102   0.333    0.537    0.670 
HACINAMIENTO_IND             23  0.094   0.038    0.030   0.067    0.119    0.174 
SERVICIOS_BASICOS_IND        23  0.628   0.144    0.337   0.513    0.760    0.924 
LONG                         23 -78.738  1.083   -80.578 -79.357  -78.217  -76.375
LAT                          23 -1.461   1.427   -4.170   -2.336   -0.471   0.752 
----------------------------------------------------------------------------------

Escalemos las variables de análisis:

CoronavirusProvincia_03_Scaled = CoronavirusProvincia_03 %>% 
  select("CONFIRMADO_COVID", "TOQUE_QUEDA_ECU911", "AGLOMERACIONES_ECU911", 
         "DEFUNCIONES_RC", "DETENIDOS",
         "RATIO_CONFIRMADOS", "TOQUE_QUEDA_PC", "AGLOMERACIONES_PC", 
         "RATIO_DEFUNCIONES", "RATIO_DETENIDOS",
         "ANOS_ESCOLARIDAD_IND", 
         "POBREZA_INGRESOS_IND", "POBREZA_EXTREMA_INGRESOS_IND",
         "GINI_IND", "IPM_IND",
         "HACINAMIENTO_IND") %>% 
  scale()

Utilicemos ACP sobre los datos:

PCA_res3 = PCA(CoronavirusProvincia_03_Scaled %>% 
                   data.frame() %>% 
                   select(RATIO_DETENIDOS,
                          ANOS_ESCOLARIDAD_IND, 
                          POBREZA_INGRESOS_IND) %>% 
                   as.matrix())

Y ACP Robusto:

RPCA_res3 = rpca(CoronavirusProvincia_03_Scaled %>% 
                   data.frame() %>%
                   select(RATIO_DETENIDOS,
                          ANOS_ESCOLARIDAD_IND, 
                          POBREZA_INGRESOS_IND) %>% 
                   as.matrix(), 
                 trace = F)
L_matrix3 = data.frame(RPCA_res3$L)
names(L_matrix3) = paste0("Dim",1:ncol(L_matrix3))
rownames(L_matrix3) = paste0(CoronavirusProvincia_03$CODIGO_PROVINCIA)

Observemos la matriz del ACP:

PCA_res3$ind$coord %>% head()
        Dim.1      Dim.2       Dim.3
1 -1.21558240  0.5066878 -0.63249670
2  0.03386516 -1.4821559  0.17143625
3 -0.40084948 -1.1562322 -0.48386835
4 -0.41619415 -0.7214811  0.18192222
5  0.04916951 -1.3123051 -0.05146349
6  1.64599620 -2.5000927  0.50378592

La matriz L del ACPR:

RPCA_res3$L %>% head()
            [,1]         [,2]        [,3]
[1,] -0.02765675  0.018291207 -0.03245406
[2,] -0.01705788 -0.014471168 -0.00536073
[3,] -0.43594302  0.032869155 -0.36618408
[4,] -0.18762382  0.004805349 -0.15228448
[5,] -0.01705788 -0.014471168 -0.00536073
[6,] -0.01705788 -0.014471168 -0.00536073

Y la matriz S del ACPR:

RPCA_res3$S %>% head()
     RATIO_DETENIDOS ANOS_ESCOLARIDAD_IND POBREZA_INGRESOS_IND
[1,]       0.0000000            0.5764712           -1.2664365
[2,]      -0.9213479           -1.0892828            0.1831394
[3,]      -0.1891067           -0.9964548           -0.2145111
[4,]      -0.5641851           -0.3316678            0.0000000
[5,]      -0.6899042           -1.0584686            0.0388222
[6,]      -0.8035747           -2.3698880            1.5720040

Realicemos ahora el agrupamiento con ambas matrices.

Primero seleccionemos el número de clusters con la métrica del ancho de silueta para la matriz generada por el ACP:

fviz_nbclust(PCA_res3$ind$coord, kmeans)

Y luego para la matriz L:

fviz_nbclust(L_matrix3, kmeans)

Para la matriz del ACP se sugieren entre 2 y 3 grupos, y para la matriz L se sugieren 3 grupos. Por fines de comparación utilizaremos 3 grupos.

Primero realicemos el k-medias sobre la matriz del ACP:

PCA_KM3 = eclust(PCA_res3$ind$coord, "kmeans", 3, nboot = 20, graph = F, seed = 123)
fviz_cluster(PCA_KM3, L_matrix3)

Y a la par sobre la matriz L:

RPCA_KM3 = eclust(L_matrix3, "kmeans", 3, nboot = 20, graph = F, seed = 123)
fviz_cluster(RPCA_KM3, L_matrix3)

Ahora observemos medidas de validación para los clusters.

Primero la varianza explicada con la el algoritmo hecho sobre matriz del ACP:

PCA_KM3$betweenss/PCA_KM3$totss
[1] 0.6475804

Y sobre la matriz L:

RPCA_KM3$betweenss/RPCA_KM3$totss
[1] 0.9356374

¿Y si observamos el ancho de silueta?

Sobre la matriz del ACP:

fviz_silhouette(PCA_KM3)

Y sobre la matriz L:

fviz_silhouette(RPCA_KM3)

Finalmente, ¿qué tal si lo observamos en un mapa?

Para ello, primero coloquemos un nombre a cada cluster acorde a sus medidas de tendencia central:

CoronavirusProvincia_03 = CoronavirusProvincia_03 %>% 
  mutate(ClusterPCA=paste0(PCA_KM3$cluster),
         ClusterRPCA = paste0(RPCA_KM3$cluster))
CoronavirusProvincia_03 %>%
  select("CONFIRMADO_COVID", "TOQUE_QUEDA_ECU911", "AGLOMERACIONES_ECU911", 
         "DEFUNCIONES_RC", "DETENIDOS",
         "RATIO_CONFIRMADOS", "TOQUE_QUEDA_PC", "AGLOMERACIONES_PC", 
         "RATIO_DEFUNCIONES", "RATIO_DETENIDOS",
         "ANOS_ESCOLARIDAD_IND", 
         "POBREZA_INGRESOS_IND", "POBREZA_EXTREMA_INGRESOS_IND",
         "GINI_IND", "IPM_IND",
         "HACINAMIENTO_IND",ClusterPCA) %>% 
  group_by(ClusterPCA) %>% 
  summarise_all(mean) %>% 
  arrange(RATIO_DETENIDOS)
CoronavirusProvincia_03 %>%
  select("CONFIRMADO_COVID", "TOQUE_QUEDA_ECU911", "AGLOMERACIONES_ECU911", 
         "DEFUNCIONES_RC", "DETENIDOS",
         "RATIO_CONFIRMADOS", "TOQUE_QUEDA_PC", "AGLOMERACIONES_PC", 
         "RATIO_DEFUNCIONES", "RATIO_DETENIDOS",
         "ANOS_ESCOLARIDAD_IND", 
         "POBREZA_INGRESOS_IND", "POBREZA_EXTREMA_INGRESOS_IND",
         "GINI_IND", "IPM_IND",
         "HACINAMIENTO_IND",ClusterRPCA) %>% 
  group_by(ClusterRPCA) %>% 
  summarise_all(mean) %>% 
  arrange(RATIO_DETENIDOS)

Conociendo las descripciones por cluster, observemos un mapa:

library(rjson)
library(tidyverse)
library(highcharter)
library(stringi)
library(viridisLite)
library(scales)

rename_map = function(x){
  x$properties$name=stri_trans_toupper(stri_trans_general(x$properties$name, "Latin-ASCII"))
  return(x)
}

ecuador = fromJSON(file = "https://raw.githubusercontent.com/Rusersgroup/mapa_ecuador/master/ec-all.geo.json")
ecuador$features = lapply(ecuador$features,rename_map)

CoronavirusProvincia_03$PROVINCIA = stri_trans_toupper(stri_trans_general(CoronavirusProvincia_03$PROVINCIA, "Latin-ASCII"))
CoronavirusProvincia_03$PROVINCIA[CoronavirusProvincia_03$PROVINCIA=="SANTO DOMINGO"] = "SANTO DOMINGO DE LOS TSACHILAS"
CoronavirusProvincia_03$ClusterPCA = factor(CoronavirusProvincia_03$ClusterPCA)
CoronavirusProvincia_03$ClusterRPCA = factor(CoronavirusProvincia_03$ClusterRPCA)

CoronavirusProvincia_03$RATIO_DETENIDOS = round(CoronavirusProvincia_03$RATIO_DETENIDOS, 2)
CoronavirusProvincia_03$ANOS_ESCOLARIDAD_IND = number(CoronavirusProvincia_03$ANOS_ESCOLARIDAD_IND, accuracy = 1)
CoronavirusProvincia_03$POBREZA_INGRESOS_IND = paste0(round(CoronavirusProvincia_03$POBREZA_INGRESOS_IND*100,1)," %")
CoronavirusProvincia_03$SECTOR_INFORMAL_IND = paste0(round(CoronavirusProvincia_03$SECTOR_INFORMAL_IND*100,1)," %")
CoronavirusProvincia_03 = plyr::rbind.fill(CoronavirusProvincia_03, data.frame(PROVINCIA="GALAPAGOS"))
hc_map_ec =highchart(type = "map") %>% 
  hc_plotOptions(map = list(
    borderColor="white",
    allAreas = FALSE,
    joinBy = c("name", "PROVINCIA"),
    mapData = ecuador,
    dataLabels = list(enabled = TRUE,
                      format = '{point.name}'))) %>% 
  hc_title(text = "<b>COVID-19: Grupos de pobreza, años de escolaridad y número de detenciones por violación del toque de queda, matriz ACP</b>",
           margin = 20, align = "center",
           style = list(color = "black", useHTML = TRUE)) %>%
  hc_add_series(name = "Baja", data = CoronavirusProvincia_03 %>% filter(ClusterPCA=="2"), color = "yellow") %>% 
  hc_add_series(name = "Media", data = CoronavirusProvincia_03 %>% filter(ClusterPCA=="1"), color = "orange") %>%
  hc_add_series(name = "Alta", data = CoronavirusProvincia_03 %>% filter(ClusterPCA=="3"), color = "brown") %>%
  hc_add_series(name = "Sin dato", data = CoronavirusProvincia_03 %>% filter(is.na(ClusterPCA)), color = "gray") %>%
  hc_tooltip(followPointer =  FALSE, useHTML=TRUE,
             pointFormat="{point.name} <br>
                          <b> {point.RATIO_DETENIDOS} </b> detenidos / personas por km2.  <br>
                          <b> {point.ANOS_ESCOLARIDAD_IND} </b> años de escolaridad promedio. <br>
                          <b> {point.POBREZA_INGRESOS_IND} </b> de personas bajo la línea de pobreza. <br>
                          <b> {point.SECTOR_INFORMAL_IND} </b> de personas con empleo informal.") %>%
  hc_add_theme(hc_theme_economist()) %>%
  hc_add_annotation(labels = list(list(point = list(x = 9000, y = -8500, xAxis = 0, yAxis = 0), text = "COLOMBIA"),
                                  list(point = list(x = 7000, y = 0, xAxis = 0, yAxis = 0), text = "PERÚ"),
                                  list(point = list(x = 0, y = -5000, xAxis = 0, yAxis = 0), text = "OCEANO PACÍFICO")))
hc_map_ec
hc_map_ec =highchart(type = "map") %>% 
  hc_plotOptions(map = list(
    borderColor="white",
    allAreas = FALSE,
    joinBy = c("name", "PROVINCIA"),
    mapData = ecuador,
    dataLabels = list(enabled = TRUE,
                      format = '{point.name}'))) %>% 
  hc_title(text = "<b>COVID-19: Grupos de pobreza, años de escolaridad y número de detenciones por violación del toque de queda, matriz L</b>",
           margin = 20, align = "center",
           style = list(color = "black", useHTML = TRUE)) %>%
  hc_add_series(name = "Baja", data = CoronavirusProvincia_03 %>% filter(ClusterRPCA=="2"), color = "yellow") %>% 
  hc_add_series(name = "Media", data = CoronavirusProvincia_03 %>% filter(ClusterRPCA=="1"), color = "orange") %>%
  hc_add_series(name = "Alta", data = CoronavirusProvincia_03 %>% filter(ClusterRPCA=="3"), color = "brown") %>%
  hc_add_series(name = "Sin dato", data = CoronavirusProvincia_03 %>% filter(is.na(ClusterRPCA)), color = "gray") %>%
  hc_tooltip(followPointer =  FALSE, useHTML=TRUE,
             pointFormat="{point.name} <br>
                          <b> {point.RATIO_DETENIDOS} </b> detenidos / personas por km2.  <br>
                          <b> {point.ANOS_ESCOLARIDAD_IND} </b> años de escolaridad promedio. <br>
                          <b> {point.POBREZA_INGRESOS_IND} </b> de personas bajo la línea de pobreza. <br>
                          <b> {point.SECTOR_INFORMAL_IND} </b> de personas con empleo informal.") %>%
  hc_add_theme(hc_theme_economist()) %>%
  hc_add_annotation(labels = list(list(point = list(x = 9000, y = -8500, xAxis = 0, yAxis = 0), text = "COLOMBIA"),
                                  list(point = list(x = 7000, y = 0, xAxis = 0, yAxis = 0), text = "PERÚ"),
                                  list(point = list(x = 0, y = -5000, xAxis = 0, yAxis = 0), text = "OCEANO PACÍFICO")))
hc_map_ec

Conclusiones

  • El ACP normal es sensible a datos atípicos mientras que el ACPR separa los datos atípicos en una matriz dispersa.
  • El realizar k-medias sobre la matriz robusta lleva a mejores indicadores de validación que hacerlo sobre la matriz del ACP.
  • ¿Respecto a los resultados económicos?
LS0tDQp0aXRsZTogIlTDqWNuaWNhcyBkZSByZWR1Y2Npw7NuIGRlIGRpbWVuc2lvbmFsaWRhZCByb2J1c3RhcyB5IGNsdXN0ZXJpbmciDQpzdWJ0aXRsZTogJ1JXZWVrZW5kIFNlZ3VuZGEgRWRpY2nDs24sIFNvY2llZGFkIEVjdWF0b3JpYW5hIGRlIEVzdGFkw61zdGljYScNCmF1dGhvcjogQ2FsdmEgS2FyZW4sIFBvcnJhcyBIdWdvDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIGNzczogRXN0aWxvcy5jc3MNCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZGVwdGg6IDINCiAgICB0b2NfZmxvYXQ6DQogICAgICBjb2xsYXBzZWQ6IHRydWUNCiAgICAgIHNtb290aF9zY3JvbGw6IGZhbHNlDQojYmlibGlvZ3JhcGh5OiBCaWJsaW9ncmFmaWEuYmliDQpjc2w6IGNlcGFsLnhtbA0KLS0tDQoNCiMgSW50cm9kdWNjacOzbiBhbCBhbsOhbGlzaXMgbm8gc3VwZXJ2aXNhZG8NCg0KRWwgYXByZW5kaXphamUgbm8gc3VwZXJ2aXNhZG8gZXMgdW4gbcOpdG9kbyBkZSBhcHJlbmRpemFqZSBubyBleGlzdGUgdW4gY29ub2NpbWllbnRvIGFwcmlvcmkgZGUgdW4gZmVuw7NtZW5vIGVuIGxvcyBkYXRvcy4gKipObyB0ZW5lbW9zIHVuYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSBvIG91dHB1dCoqLiBCdXNjYW1vcyBlbnRvbmNlcyBlbmNvbnRyYXIgcGF0cm9uZXMgc29icmUgbG9zIGRhdG9zLiBMYXMgcHJpbmNpcGFsZXMgdMOpY25pY2FzIGV4aXN0ZW50ZXMgZGUgYW7DoWxpc2lzIG5vIHN1cGVydmlzYWRvIHJlY2FlbiBzb2JyZSBkb3MgY2F0ZWdvcsOtYXM6DQoNCisgVMOpY25pY2FzIGRlIHJlZHVjY2nDs24gZGUgZGltZW5zaW9uYWxpZGFkDQoNCjxicj48L2JyPg0KPGNlbnRlcj48YT48aW1nIHdpZHRoPSIzNTAiIGhlaWdodD0iMzUwIiBzcmM9IlBDQS5qcGciPjwvYT48L2NlbnRlcj4NCjxicj48L2JyPg0KDQorIFTDqWNuaWNhcyBkZSBhZ3J1cGFtaWVudG8gbyBjbHVzdGVyaW5nDQoNCjxicj48L2JyPg0KPGNlbnRlcj48YT48aW1nIHdpZHRoPSI0NTAiIGhlaWdodD0iNDAwIiBzcmM9IktNZWFucy5qcGciPjwvYT48L2NlbnRlcj4NCjxicj48L2JyPg0KDQpBIGxvIGxhcmdvIGRlIGVzdGUgdGFsbGVyIHZpc2l0YXJlbW9zIDMgdMOpY25pY2FzLCAyIGRlIHJlZHVjY2nDs24gZGUgZGltZW5zaW9uYWxpZGFkIHkgMSBkZSBhZ3J1cGFtaWVudG8uIEVzcGVjw61maWNhbWVudGUsIHRyYWVtb3MgdW4gcHJvYmxlbWEgZW50cmUgbWFub3M6ICoqQ29uIG1pcmEgYSBjb2FkeXV2YXIgYWwgZGlzZcOxbyBkZSBwb2zDrXRpY2EgcMO6YmxpY2Egwr9jw7NtbyBhZ3J1cGFtb3MgcHJvdmluY2lhcywgZW4gZnVuY2nDs24gYSBhbGd1bmFzIGRlIHN1cyBjYXJhY3RlcsOtc3RpY2FzIHNvY2lvZWNvbsOzbWljYXMsIG5pdmVsZXMgZGUgY29udGFnaW8gcG9yIENPVklELTE5IHkgbsO6bWVybyBkZSBkZXRlbmNpb25lcyBkdXJhbnRlIGVsIHRvcXVlIGRlIHF1ZWRhPyoqLiBQYXJhIHJlc29sdmVybG8gY29tcGFyYXJlbW9zIGxvcyByZXN1bHRhZG9zIGRlIHVzYXIgQUNQIHkgQUNQIFJvYnVzdG8gc29icmUgbnVlc3RybyBjb25qdW50byBkZSBkYXRvcyBwYXJhIGx1ZWdvIGFncnVwYXJsb3MgY29uIGstbWVkaWFzLg0KDQojIFTDqWNuaWNhcyBkZSByZWR1Y2Npw7NuIGRlIGRpbWVuc2lvbmFsaWRhZA0KDQo8YnI+PC9icj4NCjxjZW50ZXI+PGE+PGltZyBzcmM9IkRpbWVuc2lvbmFsaXR5UmVkdWN0aW9uLnBuZyI+PC9hPjwvY2VudGVyPg0KPGJyPjwvYnI+DQoNCkxvcyBtw6l0b2RvcyBkZSByZWR1Y2Npw7NuIGRlIGRpbWVuc2lvbmFsaWRhZCBjb25zaXN0ZW4gZW4gcmVzdW1pciB5IHZpc3VhbGl6YXIgbGEgaW5mb3JtYWNpw7NuIG3DoXMgaW1wb3J0YW50ZSBjb250ZW5pZGEgZW4gdW4gZGF0YXNldC4gRXhpc3RlbiB2YXJpYXMgdMOpY25pY2FzIGFscmVkZWRvciBkZSBlc3RhIHRlbcOhdGljYSwgdGFudG8gc2kgYXN1bWltb3MgcXVlIGV4aXN0ZW4gcGF0cm9uZXMgbGluZWFsZXMgY29tbyBubyBsaW5lYWxlcyBlbiBsb3MgZGF0b3MuIEEgY29udGludWFjacOzbiBzZSBlbnVtZXJhbiBhbGd1bmFzIGRlIGVzdGFzIHTDqWNuaWNhcywgY2xhc2lmaWPDoW5kb2xhcyBhY29yZGUgYWwgdGlwbyBkZSBkYXRvcyBjb24gZWwgcXVlIGVzdGVtb3MgdHJhYmFqYW5kbyB5IGFsZ3Vub3MgZGUgbG9zIHBhcXVldGVzIGV4aXN0ZW50ZXMgcGFyYSBzdSB1c286DQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KbGlicmFyeShrYWJsZUV4dHJhKQ0Ka2FibGVfc3R5bGluZyhrYmwoZGF0YS5mcmFtZSgiVGlwb1ZhcmlhYmxlcyI9YygiTnVtw6lyaWNvcyIsIkNhdGVnw7NyaWNvcyIsIk1peHRvcyIpLA0KICAgICAgICAgICAgICAgIlRlY25pY2EiPWMoIkFuw6FsaXNpcyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcywgdC1TTkUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFuw6FsaXNpcyBkZSBjb3JyZXNwb25kZW5jaWFzIG3Dumx0aXBsZXMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFuw6FsaXNpcyBmYWN0b3JpYWwgbWl4dG8iKSwNCiAgICAgICAgICAgICAgICJMaWJyZXJpYXMiID0gYygiYmFzZSwgRmFjdG9NaW5lUiIsICJGYWN0b01pbmVyLCBhZGU0LCBlcE1DQSIsICJGYWN0b01pbmVyIiksIA0KICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpKSkNCmBgYA0KDQojIFTDqWNuaWNhcyBkZSBhZ3J1cGFjacOzbiBvICpjbHVzdGVyaW5nKg0KDQo8YnI+PC9icj4NCjxjZW50ZXI+PGE+PGltZyBzcmM9IkNsdXN0ZXJpbmcuanBnIj48L2E+PC9jZW50ZXI+DQo8YnI+PC9icj4NCg0KTGFzIHTDqWNuaWNhcyBkZSBhZ3J1cGFtaWVudG8gbyAqY2x1c3RlcmluZyogbm9zIHBlcm1pdGVuIG9idGVuZXIgY29ub2NpbWllbnRvIGEgcGFydGlyIGRlbCBkZXNjdWJyaW1pZW50byBkZSBwYXRyb25lcyBleGlzdGVudGVzIGVuIGxvcyBkYXRvcy4gRXNwZWPDrWZpY2FtZW50ZSwgZWwgb2JqZXRpdm8gZGUgbG9zIG3DqXRvZG9zIGRlIGNsdXN0ZXJpbmcgeWFjZW4gZW4gbGEgaWRlbnRpZmljYWNpw7NuIGRlIGdydXBvcyBkZSBvYmpldG9zIHNpbWlsYXJlcyBlbiB1biBjb25qdW50byBkZSBkYXRvcyBkZSBpbnRlcsOpcyBhIHRyYXbDqXMgZGUgdW5hIG1lZGlkYSBkZSBzaW1pbGFyaWRhZCBlbnRyZSBwdW50b3MgKGUuZyBsYSBldWNsaWRlYW5hKS4gQSBjb250aW51YWNpw7NuIHNlIGVudW1lcmFuIGFsZ3VuYXMgZGUgZXN0YXMgdMOpY25pY2FzLCBjbGFzaWZpY8OhbmRvbGFzIGFjb3JkZSBhbCB0aXBvIGRlIGRhdG9zIGNvbiBlbCBxdWUgZXN0ZW1vcyB0cmFiYWphbmRvIHkgYWxndW5vcyBkZSBsb3MgcGFxdWV0ZXMgZXhpc3RlbnRlcyBwYXJhIHN1IHVzbzoNCg0KYGBge3IsIGVjaG89RkFMU0V9DQpsaWJyYXJ5KGthYmxlRXh0cmEpDQprYWJsZV9zdHlsaW5nKGtibChkYXRhLmZyYW1lKCJUaXBvVmFyaWFibGVzIj1jKCJOdW3DqXJpY29zIiwiQ2F0ZWfDs3JpY29zIiwiTWl4dG9zIiksDQogICAgICAgICAgICAgICAiVGVjbmljYSI9Yygiay1tZWRpYXMsIEdNTSwgQ0xBUkEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgImstbW9kYXMsIG90cmFzIG1lZGlkYXMgZGUgZGlzdGFuY2lhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJrLXByb3RvdHlwZXMsIG90cmFzIG1lZGlkYXMgZGUgZGlzdGFuY2lhIiksDQogICAgICAgICAgICAgICAiTGlicmVyaWFzIiA9IGMoImNsdXN0ZXIsIEZhY3RvTWluZVIsIG1jbHVzdCIsICJrbGFSIiwgImNsdXN0TWl4VHlwZSIpLCANCiAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKSkpDQpgYGANCg0KQ2FiZSBub3RhciBxdWUgYWxndW5hcyB0w6ljbmljYXMgcG9kcsOtYW4gc2VyIMO6dGlsZXMgcGFyYSB2YXJpb3MgdGlwb3MgZGUgZGF0b3MsIGRlcGVuZGllbmRvIGRlIGxhIG3DqXRyaWNhIHF1ZSBzZWEgdXRpbGl6YWRhIGNvbW8gZGlzdGFuY2lhLg0KDQojIFJlc29sdWNpw7NuIGRlbCBwcm9ibGVtYQ0KDQpQYXJhIGluaWNpYXIsIGNhcmd1ZW1vcyBsYXMgbGlicmVyw61hcyBuZWNlc2FyaWFzOg0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXZlcnNlKSAjIE1hbmVqbyBkZSBkYXRvcw0KbGlicmFyeShzcCkgIyBEYXRvcyBnZW9ncsOhZmljb3MNCmxpYnJhcnkoRmFjdG9NaW5lUikgIyBUw6ljbmljYXMgZGUgcmVkDQpsaWJyYXJ5KGZhY3RvZXh0cmEpICMgQ29tcGxlbWVudG8gZGUgdmlzdWFsaXphY2nDs24NCmxpYnJhcnkocnBjYSkgIyBBQ1AgUm9idXN0bw0KbGlicmFyeShoaWdoY2hhcnRlcikgIyBHcsOhZmljb3MgaW50ZXJhY3Rpdm9zDQpsaWJyYXJ5KHN0YXJnYXplcikgIyBUYWJsYXMgZGUgcmVzdWx0YWRvcw0KYGBgDQoNCg0KTHVlZ28gY2FyZ2FyZW1vcyBudWVzdHJvcyBkYXRvczoNCg0KYGBge3J9DQojIENhcmdhIGRlIHZhcmlhYmxlcyBzb2Npb2Vjb27Ds21pY2FzIHkgZGUgY29udGFnaW8gYSBuaXZlbCBkZSBwcm92aW5jaWENCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzID0gcmVhZFJEUygiRGF0YS9Db3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMy5SRFMiKQ0KIyBDYXJnYSBkZSBkYXRvcyBnZW9ncsOhZmljb3MNCnByb3ZpbmNpYXMgPSByZWFkUkRTKCJNYXBhcy9lY19wcm92aW5jaWFzLlJEUyIpDQpgYGANCg0KSGVjaGVtb3MgdW4gcsOhcGlkbyB2aXN0YXpvIGEgbG9zIGRhdG9zOg0KDQpgYGB7cn0NCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzICU+JSBjb2xuYW1lcygpDQpgYGANCg0KUHJlcHJvY2VzZW1vcyBsb3MgZGF0b3MgYSBzZXIgdXRpbGl6YWRvczoNCg0KYGBge3J9DQpDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyA9IENvcm9uYXZpcnVzUHJvdmluY2lhXzAzICU+JSANCiAgc2VsZWN0KC1jKERFVEVOSURPU19TRUNVTkRBUklBLCBERVRFTklET1NfSE9NQlJFUywgREVURU5JRE9TX1BBUkVKQSwgREVURU5JRE9TX1NPTFRFUk8sDQogICAgICAgICAgICBERVRFTklET1NfQ1VBTlRJTF8xXzI0QSwgREVURU5JRE9TX0NVQU5USUxfMl8zMEEsDQogICAgICAgICAgICBERVRFTklET1NfQ1VBTlRJTF8zXzMzQSwgREVURU5JRE9TX0NVQU5USUxfNF82OUEpKSU+JSANCiAgbGVmdF9qb2luKGRhdGEuZnJhbWUoQ09ESUdPX1BST1ZJTkNJQT1wcm92aW5jaWFzJENDXzEsIA0KICAgICAgICAgICAgICAgICAgICAgICBMT05HPWNvb3JkaW5hdGVzKHByb3ZpbmNpYXMpWywxXSwgDQogICAgICAgICAgICAgICAgICAgICAgIExBVD1jb29yZGluYXRlcyhwcm92aW5jaWFzKVssMl0pKQ0KDQpNYXBhUHJvdmluY2lhczAzID0gcHJvdmluY2lhcw0KYGBgDQoNCkNyZWFtb3MgYWxndW5hcyB2YXJpYWJsZXMgcXVlIHBlcm1pdGFuIHJlYWxpemFyIGNvbXBhcmFjaW9uZXMgZW50cmUgcHJvdmluY2lhczoNCg0KYGBge3J9DQpDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyA9IENvcm9uYXZpcnVzUHJvdmluY2lhXzAzICU+JQ0KICBtdXRhdGUoUkFUSU9fQ09ORklSTUFET1MgPSBDT05GSVJNQURPX0NPVklELyhQT0JMQUNJT05fMjAyMC9BUkVBX0tNMiksDQogICAgICAgICBSQVRJT19ERUZVTkNJT05FUyA9IERFRlVOQ0lPTkVTX1JDLyhQT0JMQUNJT05fMjAyMC9BUkVBX0tNMiksDQogICAgICAgICBSQVRJT19ERVRFTklET1MgPSBERVRFTklET1MvKFBPQkxBQ0lPTl8yMDIwL0FSRUFfS00yKSwNCiAgICAgICAgIEFHTE9NRVJBQ0lPTkVTX1BDID0gQUdMT01FUkFDSU9ORVNfRUNVOTExLyhQT0JMQUNJT05fMjAyMC9BUkVBX0tNMiksDQogICAgICAgICBUT1FVRV9RVUVEQV9QQyA9IFRPUVVFX1FVRURBX0VDVTkxMS8oUE9CTEFDSU9OXzIwMjAvQVJFQV9LTTIpKQ0KYGBgDQoNClJlYWxpemFtb3MgdW5hIHNlbGVjY2nDs24gZGUgdmFyaWFibGVzIHBhcmEgcmVhbGl6YXIgbGEgcmVkdWNjacOzbiBkZSBkaW1lbnNpb25hbGlkYWQ6DQoNCmBgYHtyfQ0KQ29yb25hdmlydXNQcm92aW5jaWFfMDMgPSBDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyAlPiUgDQogIHNlbGVjdChDT0RJR09fUFJPVklOQ0lBLCBQUk9WSU5DSUEsDQogICAgICAgICBSQVRJT19DT05GSVJNQURPUywgVE9RVUVfUVVFREFfUEMsIEFHTE9NRVJBQ0lPTkVTX1BDLCBSQVRJT19ERUZVTkNJT05FUywgUkFUSU9fREVURU5JRE9TLA0KICAgICAgICAgQ09ORklSTUFET19DT1ZJRCwgVE9RVUVfUVVFREFfRUNVOTExLCBBR0xPTUVSQUNJT05FU19FQ1U5MTEsIERFRlVOQ0lPTkVTX1JDLCBERVRFTklET1MsDQogICAgICAgICBERVNFTVBMRU9fSU5ELCBTVUJFTVBMRU9fSU5ELCBTRUNUT1JfSU5GT1JNQUxfSU5ELA0KICAgICAgICAgQU5PU19FU0NPTEFSSURBRF9JTkQsIA0KICAgICAgICAgUE9CUkVaQV9JTkdSRVNPU19JTkQsIFBPQlJFWkFfRVhUUkVNQV9JTkdSRVNPU19JTkQsDQogICAgICAgICBHSU5JX0lORCwgSVBNX0lORCwNCiAgICAgICAgIEhBQ0lOQU1JRU5UT19JTkQsIFNFUlZJQ0lPU19CQVNJQ09TX0lORCwNCiAgICAgICAgIExPTkcsIExBVCkNCmBgYA0KDQpPYnNlcnZhbW9zIGFsZ3VuYXMgbWVkaWRhcyBkZSB0ZW5kZW5jaWEgY2VudHJhbCBkZXNjcmlwdGl2YXM6DQoNCmBgYHtyfQ0Kc3RhcmdhemVyKGFzLmRhdGEuZnJhbWUoQ29yb25hdmlydXNQcm92aW5jaWFfMDMpLCB0eXBlID0gInRleHQiLCBzdW1tYXJ5ID0gVCkNCmBgYA0KDQpFc2NhbGVtb3MgbGFzIHZhcmlhYmxlcyBkZSBhbsOhbGlzaXM6DQoNCmBgYHtyfQ0KQ29yb25hdmlydXNQcm92aW5jaWFfMDNfU2NhbGVkID0gQ29yb25hdmlydXNQcm92aW5jaWFfMDMgJT4lIA0KICBzZWxlY3QoIkNPTkZJUk1BRE9fQ09WSUQiLCAiVE9RVUVfUVVFREFfRUNVOTExIiwgIkFHTE9NRVJBQ0lPTkVTX0VDVTkxMSIsIA0KICAgICAgICAgIkRFRlVOQ0lPTkVTX1JDIiwgIkRFVEVOSURPUyIsDQogICAgICAgICAiUkFUSU9fQ09ORklSTUFET1MiLCAiVE9RVUVfUVVFREFfUEMiLCAiQUdMT01FUkFDSU9ORVNfUEMiLCANCiAgICAgICAgICJSQVRJT19ERUZVTkNJT05FUyIsICJSQVRJT19ERVRFTklET1MiLA0KICAgICAgICAgIkFOT1NfRVNDT0xBUklEQURfSU5EIiwgDQogICAgICAgICAiUE9CUkVaQV9JTkdSRVNPU19JTkQiLCAiUE9CUkVaQV9FWFRSRU1BX0lOR1JFU09TX0lORCIsDQogICAgICAgICAiR0lOSV9JTkQiLCAiSVBNX0lORCIsDQogICAgICAgICAiSEFDSU5BTUlFTlRPX0lORCIpICU+JSANCiAgc2NhbGUoKQ0KYGBgDQoNClV0aWxpY2Vtb3MgQUNQIHNvYnJlIGxvcyBkYXRvczoNCg0KYGBge3J9DQpQQ0FfcmVzMyA9IFBDQShDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wM19TY2FsZWQgJT4lIA0KICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUoKSAlPiUgDQogICAgICAgICAgICAgICAgICAgc2VsZWN0KFJBVElPX0RFVEVOSURPUywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgQU5PU19FU0NPTEFSSURBRF9JTkQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBQT0JSRVpBX0lOR1JFU09TX0lORCkgJT4lIA0KICAgICAgICAgICAgICAgICAgIGFzLm1hdHJpeCgpKQ0KYGBgDQoNCg0KWSBBQ1AgUm9idXN0bzoNCg0KYGBge3J9DQpSUENBX3JlczMgPSBycGNhKENvcm9uYXZpcnVzUHJvdmluY2lhXzAzX1NjYWxlZCAlPiUgDQogICAgICAgICAgICAgICAgICAgZGF0YS5mcmFtZSgpICU+JQ0KICAgICAgICAgICAgICAgICAgIHNlbGVjdChSQVRJT19ERVRFTklET1MsDQogICAgICAgICAgICAgICAgICAgICAgICAgIEFOT1NfRVNDT0xBUklEQURfSU5ELCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgUE9CUkVaQV9JTkdSRVNPU19JTkQpICU+JSANCiAgICAgICAgICAgICAgICAgICBhcy5tYXRyaXgoKSwgDQogICAgICAgICAgICAgICAgIHRyYWNlID0gRikNCkxfbWF0cml4MyA9IGRhdGEuZnJhbWUoUlBDQV9yZXMzJEwpDQpuYW1lcyhMX21hdHJpeDMpID0gcGFzdGUwKCJEaW0iLDE6bmNvbChMX21hdHJpeDMpKQ0Kcm93bmFtZXMoTF9tYXRyaXgzKSA9IHBhc3RlMChDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyRDT0RJR09fUFJPVklOQ0lBKQ0KYGBgDQoNCk9ic2VydmVtb3MgbGEgbWF0cml6IGRlbCBBQ1A6DQoNCmBgYHtyfQ0KUENBX3JlczMkaW5kJGNvb3JkICU+JSBoZWFkKCkNCmBgYA0KDQpMYSBtYXRyaXogTCBkZWwgQUNQUjoNCg0KYGBge3J9DQpSUENBX3JlczMkTCAlPiUgaGVhZCgpDQpgYGANCg0KWSBsYSBtYXRyaXogUyBkZWwgQUNQUjoNCg0KYGBge3J9DQpSUENBX3JlczMkUyAlPiUgaGVhZCgpDQpgYGANCg0KUmVhbGljZW1vcyBhaG9yYSBlbCBhZ3J1cGFtaWVudG8gY29uIGFtYmFzIG1hdHJpY2VzLg0KDQpQcmltZXJvIHNlbGVjY2lvbmVtb3MgZWwgbsO6bWVybyBkZSBjbHVzdGVycyBjb24gbGEgbcOpdHJpY2EgZGVsIGFuY2hvIGRlIHNpbHVldGEgcGFyYSBsYSBtYXRyaXogZ2VuZXJhZGEgcG9yIGVsIEFDUDoNCg0KYGBge3J9DQpmdml6X25iY2x1c3QoUENBX3JlczMkaW5kJGNvb3JkLCBrbWVhbnMpDQpgYGANCg0KWSBsdWVnbyBwYXJhIGxhIG1hdHJpeiBMOg0KDQpgYGB7cn0NCmZ2aXpfbmJjbHVzdChMX21hdHJpeDMsIGttZWFucykNCmBgYA0KDQpQYXJhIGxhIG1hdHJpeiBkZWwgQUNQIHNlIHN1Z2llcmVuIGVudHJlIDIgeSAzIGdydXBvcywgeSBwYXJhIGxhIG1hdHJpeiBMIHNlIHN1Z2llcmVuIDMgZ3J1cG9zLiBQb3IgZmluZXMgZGUgY29tcGFyYWNpw7NuIHV0aWxpemFyZW1vcyAzIGdydXBvcy4NCg0KUHJpbWVybyByZWFsaWNlbW9zIGVsIGstbWVkaWFzIHNvYnJlIGxhIG1hdHJpeiBkZWwgQUNQOg0KDQpgYGB7cn0NClBDQV9LTTMgPSBlY2x1c3QoUENBX3JlczMkaW5kJGNvb3JkLCAia21lYW5zIiwgMywgbmJvb3QgPSAyMCwgZ3JhcGggPSBGLCBzZWVkID0gMTIzKQ0KZnZpel9jbHVzdGVyKFBDQV9LTTMsIExfbWF0cml4MykNCmBgYA0KDQpZIGEgbGEgcGFyIHNvYnJlIGxhIG1hdHJpeiBMOg0KDQpgYGB7cn0NClJQQ0FfS00zID0gZWNsdXN0KExfbWF0cml4MywgImttZWFucyIsIDMsIG5ib290ID0gMjAsIGdyYXBoID0gRiwgc2VlZCA9IDEyMykNCmZ2aXpfY2x1c3RlcihSUENBX0tNMywgTF9tYXRyaXgzKQ0KYGBgDQoNCkFob3JhIG9ic2VydmVtb3MgbWVkaWRhcyBkZSB2YWxpZGFjacOzbiBwYXJhIGxvcyBjbHVzdGVycy4NCg0KUHJpbWVybyBsYSB2YXJpYW56YSBleHBsaWNhZGEgY29uIGxhIGVsIGFsZ29yaXRtbyBoZWNobyBzb2JyZSBtYXRyaXogZGVsIEFDUDoNCg0KYGBge3J9DQpQQ0FfS00zJGJldHdlZW5zcy9QQ0FfS00zJHRvdHNzDQpgYGANCg0KWSBzb2JyZSBsYSBtYXRyaXogTDoNCg0KYGBge3J9DQpSUENBX0tNMyRiZXR3ZWVuc3MvUlBDQV9LTTMkdG90c3MNCmBgYA0KDQrCv1kgc2kgb2JzZXJ2YW1vcyBlbCBhbmNobyBkZSBzaWx1ZXRhPw0KDQpTb2JyZSBsYSBtYXRyaXogZGVsIEFDUDoNCg0KYGBge3J9DQpmdml6X3NpbGhvdWV0dGUoUENBX0tNMykNCmBgYA0KDQpZIHNvYnJlIGxhIG1hdHJpeiBMOg0KDQpgYGB7cn0NCmZ2aXpfc2lsaG91ZXR0ZShSUENBX0tNMykNCmBgYA0KDQpGaW5hbG1lbnRlLCDCv3F1w6kgdGFsIHNpIGxvIG9ic2VydmFtb3MgZW4gdW4gbWFwYT8NCg0KUGFyYSBlbGxvLCBwcmltZXJvIGNvbG9xdWVtb3MgdW4gbm9tYnJlIGEgY2FkYSBjbHVzdGVyIGFjb3JkZSBhIHN1cyBtZWRpZGFzIGRlIHRlbmRlbmNpYSBjZW50cmFsOg0KDQpgYGB7cn0NCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzID0gQ29yb25hdmlydXNQcm92aW5jaWFfMDMgJT4lIA0KICBtdXRhdGUoQ2x1c3RlclBDQT1wYXN0ZTAoUENBX0tNMyRjbHVzdGVyKSwNCiAgICAgICAgIENsdXN0ZXJSUENBID0gcGFzdGUwKFJQQ0FfS00zJGNsdXN0ZXIpKQ0KYGBgDQoNCmBgYHtyfQ0KQ29yb25hdmlydXNQcm92aW5jaWFfMDMgJT4lDQogIHNlbGVjdCgiQ09ORklSTUFET19DT1ZJRCIsICJUT1FVRV9RVUVEQV9FQ1U5MTEiLCAiQUdMT01FUkFDSU9ORVNfRUNVOTExIiwgDQogICAgICAgICAiREVGVU5DSU9ORVNfUkMiLCAiREVURU5JRE9TIiwNCiAgICAgICAgICJSQVRJT19DT05GSVJNQURPUyIsICJUT1FVRV9RVUVEQV9QQyIsICJBR0xPTUVSQUNJT05FU19QQyIsIA0KICAgICAgICAgIlJBVElPX0RFRlVOQ0lPTkVTIiwgIlJBVElPX0RFVEVOSURPUyIsDQogICAgICAgICAiQU5PU19FU0NPTEFSSURBRF9JTkQiLCANCiAgICAgICAgICJQT0JSRVpBX0lOR1JFU09TX0lORCIsICJQT0JSRVpBX0VYVFJFTUFfSU5HUkVTT1NfSU5EIiwNCiAgICAgICAgICJHSU5JX0lORCIsICJJUE1fSU5EIiwNCiAgICAgICAgICJIQUNJTkFNSUVOVE9fSU5EIixDbHVzdGVyUENBKSAlPiUgDQogIGdyb3VwX2J5KENsdXN0ZXJQQ0EpICU+JSANCiAgc3VtbWFyaXNlX2FsbChtZWFuKSAlPiUgDQogIGFycmFuZ2UoUkFUSU9fREVURU5JRE9TKQ0KYGBgDQoNCmBgYHtyfQ0KQ29yb25hdmlydXNQcm92aW5jaWFfMDMgJT4lDQogIHNlbGVjdCgiQ09ORklSTUFET19DT1ZJRCIsICJUT1FVRV9RVUVEQV9FQ1U5MTEiLCAiQUdMT01FUkFDSU9ORVNfRUNVOTExIiwgDQogICAgICAgICAiREVGVU5DSU9ORVNfUkMiLCAiREVURU5JRE9TIiwNCiAgICAgICAgICJSQVRJT19DT05GSVJNQURPUyIsICJUT1FVRV9RVUVEQV9QQyIsICJBR0xPTUVSQUNJT05FU19QQyIsIA0KICAgICAgICAgIlJBVElPX0RFRlVOQ0lPTkVTIiwgIlJBVElPX0RFVEVOSURPUyIsDQogICAgICAgICAiQU5PU19FU0NPTEFSSURBRF9JTkQiLCANCiAgICAgICAgICJQT0JSRVpBX0lOR1JFU09TX0lORCIsICJQT0JSRVpBX0VYVFJFTUFfSU5HUkVTT1NfSU5EIiwNCiAgICAgICAgICJHSU5JX0lORCIsICJJUE1fSU5EIiwNCiAgICAgICAgICJIQUNJTkFNSUVOVE9fSU5EIixDbHVzdGVyUlBDQSkgJT4lIA0KICBncm91cF9ieShDbHVzdGVyUlBDQSkgJT4lIA0KICBzdW1tYXJpc2VfYWxsKG1lYW4pICU+JSANCiAgYXJyYW5nZShSQVRJT19ERVRFTklET1MpDQpgYGANCg0KQ29ub2NpZW5kbyBsYXMgZGVzY3JpcGNpb25lcyBwb3IgY2x1c3Rlciwgb2JzZXJ2ZW1vcyB1biBtYXBhOg0KDQpgYGB7cn0NCmxpYnJhcnkocmpzb24pDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoaGlnaGNoYXJ0ZXIpDQpsaWJyYXJ5KHN0cmluZ2kpDQpsaWJyYXJ5KHZpcmlkaXNMaXRlKQ0KbGlicmFyeShzY2FsZXMpDQoNCnJlbmFtZV9tYXAgPSBmdW5jdGlvbih4KXsNCiAgeCRwcm9wZXJ0aWVzJG5hbWU9c3RyaV90cmFuc190b3VwcGVyKHN0cmlfdHJhbnNfZ2VuZXJhbCh4JHByb3BlcnRpZXMkbmFtZSwgIkxhdGluLUFTQ0lJIikpDQogIHJldHVybih4KQ0KfQ0KDQplY3VhZG9yID0gZnJvbUpTT04oZmlsZSA9ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vUnVzZXJzZ3JvdXAvbWFwYV9lY3VhZG9yL21hc3Rlci9lYy1hbGwuZ2VvLmpzb24iKQ0KZWN1YWRvciRmZWF0dXJlcyA9IGxhcHBseShlY3VhZG9yJGZlYXR1cmVzLHJlbmFtZV9tYXApDQoNCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzJFBST1ZJTkNJQSA9IHN0cmlfdHJhbnNfdG91cHBlcihzdHJpX3RyYW5zX2dlbmVyYWwoQ29yb25hdmlydXNQcm92aW5jaWFfMDMkUFJPVklOQ0lBLCAiTGF0aW4tQVNDSUkiKSkNCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzJFBST1ZJTkNJQVtDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyRQUk9WSU5DSUE9PSJTQU5UTyBET01JTkdPIl0gPSAiU0FOVE8gRE9NSU5HTyBERSBMT1MgVFNBQ0hJTEFTIg0KQ29yb25hdmlydXNQcm92aW5jaWFfMDMkQ2x1c3RlclBDQSA9IGZhY3RvcihDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyRDbHVzdGVyUENBKQ0KQ29yb25hdmlydXNQcm92aW5jaWFfMDMkQ2x1c3RlclJQQ0EgPSBmYWN0b3IoQ29yb25hdmlydXNQcm92aW5jaWFfMDMkQ2x1c3RlclJQQ0EpDQoNCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzJFJBVElPX0RFVEVOSURPUyA9IHJvdW5kKENvcm9uYXZpcnVzUHJvdmluY2lhXzAzJFJBVElPX0RFVEVOSURPUywgMikNCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzJEFOT1NfRVNDT0xBUklEQURfSU5EID0gbnVtYmVyKENvcm9uYXZpcnVzUHJvdmluY2lhXzAzJEFOT1NfRVNDT0xBUklEQURfSU5ELCBhY2N1cmFjeSA9IDEpDQpDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyRQT0JSRVpBX0lOR1JFU09TX0lORCA9IHBhc3RlMChyb3VuZChDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyRQT0JSRVpBX0lOR1JFU09TX0lORCoxMDAsMSksIiAlIikNCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzJFNFQ1RPUl9JTkZPUk1BTF9JTkQgPSBwYXN0ZTAocm91bmQoQ29yb25hdmlydXNQcm92aW5jaWFfMDMkU0VDVE9SX0lORk9STUFMX0lORCoxMDAsMSksIiAlIikNCkNvcm9uYXZpcnVzUHJvdmluY2lhXzAzID0gcGx5cjo6cmJpbmQuZmlsbChDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMywgZGF0YS5mcmFtZShQUk9WSU5DSUE9IkdBTEFQQUdPUyIpKQ0KYGBgDQoNCmBgYHtyfQ0KaGNfbWFwX2VjID1oaWdoY2hhcnQodHlwZSA9ICJtYXAiKSAlPiUgDQogIGhjX3Bsb3RPcHRpb25zKG1hcCA9IGxpc3QoDQogICAgYm9yZGVyQ29sb3I9IndoaXRlIiwNCiAgICBhbGxBcmVhcyA9IEZBTFNFLA0KICAgIGpvaW5CeSA9IGMoIm5hbWUiLCAiUFJPVklOQ0lBIiksDQogICAgbWFwRGF0YSA9IGVjdWFkb3IsDQogICAgZGF0YUxhYmVscyA9IGxpc3QoZW5hYmxlZCA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgZm9ybWF0ID0gJ3twb2ludC5uYW1lfScpKSkgJT4lIA0KICBoY190aXRsZSh0ZXh0ID0gIjxiPkNPVklELTE5OiBHcnVwb3MgZGUgcG9icmV6YSwgYcOxb3MgZGUgZXNjb2xhcmlkYWQgeSBuw7ptZXJvIGRlIGRldGVuY2lvbmVzIHBvciB2aW9sYWNpw7NuIGRlbCB0b3F1ZSBkZSBxdWVkYSwgbWF0cml6IEFDUDwvYj4iLA0KICAgICAgICAgICBtYXJnaW4gPSAyMCwgYWxpZ24gPSAiY2VudGVyIiwNCiAgICAgICAgICAgc3R5bGUgPSBsaXN0KGNvbG9yID0gImJsYWNrIiwgdXNlSFRNTCA9IFRSVUUpKSAlPiUNCiAgaGNfYWRkX3NlcmllcyhuYW1lID0gIkJhamEiLCBkYXRhID0gQ29yb25hdmlydXNQcm92aW5jaWFfMDMgJT4lIGZpbHRlcihDbHVzdGVyUENBPT0iMiIpLCBjb2xvciA9ICJ5ZWxsb3ciKSAlPiUgDQogIGhjX2FkZF9zZXJpZXMobmFtZSA9ICJNZWRpYSIsIGRhdGEgPSBDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyAlPiUgZmlsdGVyKENsdXN0ZXJQQ0E9PSIxIiksIGNvbG9yID0gIm9yYW5nZSIpICU+JQ0KICBoY19hZGRfc2VyaWVzKG5hbWUgPSAiQWx0YSIsIGRhdGEgPSBDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyAlPiUgZmlsdGVyKENsdXN0ZXJQQ0E9PSIzIiksIGNvbG9yID0gImJyb3duIikgJT4lDQogIGhjX2FkZF9zZXJpZXMobmFtZSA9ICJTaW4gZGF0byIsIGRhdGEgPSBDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyAlPiUgZmlsdGVyKGlzLm5hKENsdXN0ZXJQQ0EpKSwgY29sb3IgPSAiZ3JheSIpICU+JQ0KICBoY190b29sdGlwKGZvbGxvd1BvaW50ZXIgPSAgRkFMU0UsIHVzZUhUTUw9VFJVRSwNCiAgICAgICAgICAgICBwb2ludEZvcm1hdD0ie3BvaW50Lm5hbWV9IDxicj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgPGI+IHtwb2ludC5SQVRJT19ERVRFTklET1N9IDwvYj4gZGV0ZW5pZG9zIC8gcGVyc29uYXMgcG9yIGttMi4gIDxicj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgPGI+IHtwb2ludC5BTk9TX0VTQ09MQVJJREFEX0lORH0gPC9iPiBhw7FvcyBkZSBlc2NvbGFyaWRhZCBwcm9tZWRpby4gPGJyPg0KICAgICAgICAgICAgICAgICAgICAgICAgICA8Yj4ge3BvaW50LlBPQlJFWkFfSU5HUkVTT1NfSU5EfSA8L2I+IGRlIHBlcnNvbmFzIGJham8gbGEgbMOtbmVhIGRlIHBvYnJlemEuIDxicj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgPGI+IHtwb2ludC5TRUNUT1JfSU5GT1JNQUxfSU5EfSA8L2I+IGRlIHBlcnNvbmFzIGNvbiBlbXBsZW8gaW5mb3JtYWwuIikgJT4lDQogIGhjX2FkZF90aGVtZShoY190aGVtZV9lY29ub21pc3QoKSkgJT4lDQogIGhjX2FkZF9hbm5vdGF0aW9uKGxhYmVscyA9IGxpc3QobGlzdChwb2ludCA9IGxpc3QoeCA9IDkwMDAsIHkgPSAtODUwMCwgeEF4aXMgPSAwLCB5QXhpcyA9IDApLCB0ZXh0ID0gIkNPTE9NQklBIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGlzdChwb2ludCA9IGxpc3QoeCA9IDcwMDAsIHkgPSAwLCB4QXhpcyA9IDAsIHlBeGlzID0gMCksIHRleHQgPSAiUEVSw5oiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0KHBvaW50ID0gbGlzdCh4ID0gMCwgeSA9IC01MDAwLCB4QXhpcyA9IDAsIHlBeGlzID0gMCksIHRleHQgPSAiT0NFQU5PIFBBQ8ONRklDTyIpKSkNCmhjX21hcF9lYw0KYGBgDQpgYGB7cn0NCmhjX21hcF9lYyA9aGlnaGNoYXJ0KHR5cGUgPSAibWFwIikgJT4lIA0KICBoY19wbG90T3B0aW9ucyhtYXAgPSBsaXN0KA0KICAgIGJvcmRlckNvbG9yPSJ3aGl0ZSIsDQogICAgYWxsQXJlYXMgPSBGQUxTRSwNCiAgICBqb2luQnkgPSBjKCJuYW1lIiwgIlBST1ZJTkNJQSIpLA0KICAgIG1hcERhdGEgPSBlY3VhZG9yLA0KICAgIGRhdGFMYWJlbHMgPSBsaXN0KGVuYWJsZWQgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgIGZvcm1hdCA9ICd7cG9pbnQubmFtZX0nKSkpICU+JSANCiAgaGNfdGl0bGUodGV4dCA9ICI8Yj5DT1ZJRC0xOTogR3J1cG9zIGRlIHBvYnJlemEsIGHDsW9zIGRlIGVzY29sYXJpZGFkIHkgbsO6bWVybyBkZSBkZXRlbmNpb25lcyBwb3IgdmlvbGFjacOzbiBkZWwgdG9xdWUgZGUgcXVlZGEsIG1hdHJpeiBMPC9iPiIsDQogICAgICAgICAgIG1hcmdpbiA9IDIwLCBhbGlnbiA9ICJjZW50ZXIiLA0KICAgICAgICAgICBzdHlsZSA9IGxpc3QoY29sb3IgPSAiYmxhY2siLCB1c2VIVE1MID0gVFJVRSkpICU+JQ0KICBoY19hZGRfc2VyaWVzKG5hbWUgPSAiQmFqYSIsIGRhdGEgPSBDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyAlPiUgZmlsdGVyKENsdXN0ZXJSUENBPT0iMiIpLCBjb2xvciA9ICJ5ZWxsb3ciKSAlPiUgDQogIGhjX2FkZF9zZXJpZXMobmFtZSA9ICJNZWRpYSIsIGRhdGEgPSBDb3JvbmF2aXJ1c1Byb3ZpbmNpYV8wMyAlPiUgZmlsdGVyKENsdXN0ZXJSUENBPT0iMSIpLCBjb2xvciA9ICJvcmFuZ2UiKSAlPiUNCiAgaGNfYWRkX3NlcmllcyhuYW1lID0gIkFsdGEiLCBkYXRhID0gQ29yb25hdmlydXNQcm92aW5jaWFfMDMgJT4lIGZpbHRlcihDbHVzdGVyUlBDQT09IjMiKSwgY29sb3IgPSAiYnJvd24iKSAlPiUNCiAgaGNfYWRkX3NlcmllcyhuYW1lID0gIlNpbiBkYXRvIiwgZGF0YSA9IENvcm9uYXZpcnVzUHJvdmluY2lhXzAzICU+JSBmaWx0ZXIoaXMubmEoQ2x1c3RlclJQQ0EpKSwgY29sb3IgPSAiZ3JheSIpICU+JQ0KICBoY190b29sdGlwKGZvbGxvd1BvaW50ZXIgPSAgRkFMU0UsIHVzZUhUTUw9VFJVRSwNCiAgICAgICAgICAgICBwb2ludEZvcm1hdD0ie3BvaW50Lm5hbWV9IDxicj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgPGI+IHtwb2ludC5SQVRJT19ERVRFTklET1N9IDwvYj4gZGV0ZW5pZG9zIC8gcGVyc29uYXMgcG9yIGttMi4gIDxicj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgPGI+IHtwb2ludC5BTk9TX0VTQ09MQVJJREFEX0lORH0gPC9iPiBhw7FvcyBkZSBlc2NvbGFyaWRhZCBwcm9tZWRpby4gPGJyPg0KICAgICAgICAgICAgICAgICAgICAgICAgICA8Yj4ge3BvaW50LlBPQlJFWkFfSU5HUkVTT1NfSU5EfSA8L2I+IGRlIHBlcnNvbmFzIGJham8gbGEgbMOtbmVhIGRlIHBvYnJlemEuIDxicj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgPGI+IHtwb2ludC5TRUNUT1JfSU5GT1JNQUxfSU5EfSA8L2I+IGRlIHBlcnNvbmFzIGNvbiBlbXBsZW8gaW5mb3JtYWwuIikgJT4lDQogIGhjX2FkZF90aGVtZShoY190aGVtZV9lY29ub21pc3QoKSkgJT4lDQogIGhjX2FkZF9hbm5vdGF0aW9uKGxhYmVscyA9IGxpc3QobGlzdChwb2ludCA9IGxpc3QoeCA9IDkwMDAsIHkgPSAtODUwMCwgeEF4aXMgPSAwLCB5QXhpcyA9IDApLCB0ZXh0ID0gIkNPTE9NQklBIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGlzdChwb2ludCA9IGxpc3QoeCA9IDcwMDAsIHkgPSAwLCB4QXhpcyA9IDAsIHlBeGlzID0gMCksIHRleHQgPSAiUEVSw5oiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0KHBvaW50ID0gbGlzdCh4ID0gMCwgeSA9IC01MDAwLCB4QXhpcyA9IDAsIHlBeGlzID0gMCksIHRleHQgPSAiT0NFQU5PIFBBQ8ONRklDTyIpKSkNCmhjX21hcF9lYw0KYGBgDQoNCiMgQ29uY2x1c2lvbmVzDQoNCisgRWwgQUNQIG5vcm1hbCBlcyBzZW5zaWJsZSBhIGRhdG9zIGF0w61waWNvcyBtaWVudHJhcyBxdWUgZWwgQUNQUiBzZXBhcmEgbG9zIGRhdG9zIGF0w61waWNvcyBlbiB1bmEgbWF0cml6IGRpc3BlcnNhLg0KKyBFbCByZWFsaXphciBrLW1lZGlhcyBzb2JyZSBsYSBtYXRyaXogcm9idXN0YSBsbGV2YSBhIG1lam9yZXMgaW5kaWNhZG9yZXMgZGUgdmFsaWRhY2nDs24gcXVlIGhhY2VybG8gc29icmUgbGEgbWF0cml6IGRlbCBBQ1AuDQorIMK/UmVzcGVjdG8gYSBsb3MgcmVzdWx0YWRvcyBlY29uw7NtaWNvcz8=