Presentación

 

En este trabajo se desarrollará un proyecto de analítica relacionado con el modelado de una serie de tiempo. Este tipo de proyectos es muy común en prácticamente todas las empresas, pues en su Plan de Negocios anual, es donde se especifican los objetivos y los escenarios tanto operativos como financieros y éstos se construyen a partir de pronósticos.

El trabajo consistirá en pronosticar la cantidad mensual de clientes, para el año 2019, del Aeropuerto Internacional de la Ciudad de México (AICM), en otras palabras, se hará una predicción de los pasajeros que abordaron vuelos nacionales más vuelos internacionales, pero solo las salidas. El rango histórico de datos abarca desde el año 2008 a 2019 y aunque se dispone de información más actual, por cuestiones relacionadas con la pandemia de Coivd-19, se decidió hacer el ponóstico solo para 2019, ya que esto permitirá medir la precisión del modelo sin el impacto de dicha pandemia. Posteriormente, ya con el modelo final, se hará la predicción para los meses de enero y febrero de 2020, ya que en México el confinamiento empezó a finales de marzo.

Etapas

 

Como este trabajo consistará en la creación de un modelo de aprendizaje supervisado para una serie de tiempo la etapa del análisis exploratorio es un poco más densa que los análisis exploratorios descriptivos comunes. Esta etapa se constituye de las siguientes fases:

Set up

 

Iniciamos configurando las opciones generales que vamos a requerir para el desarrollo de este proyecto.

knitr::opts_chunk$set(echo = TRUE,
                      warning = FALSE,
                      message = FALSE,
                      warning = FALSE,
                      fig.align = "center",
                      out.width = "90%",
                      out.height = "70%")

paquetes <- c('tidyverse',      # manipulación datos
              'lubridate',      # manejar fechas
              'knitr',          # manejo de tablas
              'kableExtra',     # manejo de tablas
              'plotly',         # gráficos interactivos
              'gridExtra',      # layout de graficos ggplot
              'nortest',        # pruebas estadísticas
              "summarytools",   # estadísticas univariantes
              "TSstudio",       # time series
              "forecast",       # time series
              "zoo",            # time series
              "anomalize",      # anomalias
              "tseries",        # serie de tiempo
              "FitAR",          # arima
              "ggfortify",      # diagnosis residuales
              "lmtest",         # test coeficientes arima
              "strucchange",    # gráfico CUSUM test
              'prophet',        # modelo prophet
              'tidymodels')     # manejo de modelos

instalados <- paquetes %in% installed.packages()

if(sum(instalados == FALSE) > 0) {
  install.packages(paquetes[!instalados])
}

lapply(paquetes, require, character.only = TRUE)

 

Carga de datos

 

La información para este análisis es pública y los datos así como su descripción se encuentran en la página del AICM.

Es importante aclarar que los datos originales se encuentran dentro de informes en formato pdf, por lo que, para extraerlos se recurrió a la técnica conocida como Reconocimiento Óptico de Caracteres (OCR). Para el periodo 2012-2019, se utilizó R y para el resto del periodo 2008-2011 se utilizó Python, ya que ofreció los mejores resultados para esos años. Los archivos con todo el código, de R y Python, se pueden encontrar en mi repositorio de github. Habiendo hecho esta aclaración, a continuación, ya solo se importa el archivo final:

salidas <- read.csv("Salidas_Nacionales.csv")

head(salidas)
glimpse(salidas)
## Rows: 144
## Columns: 2
## $ Fecha   <chr> "01/01/2008", "01/02/2008", "01/03/2008", "01/04/2008", "01/05~
## $ Salidas <int> 1034682, 957075, 1132697, 1055504, 1087835, 1089415, 1257439, ~

En este primer resumen podemos ver que la variable Fecha está como character, por lo que, es necesario convertirla al formato correcto:

salidas$Fecha <- dmy(salidas$Fecha)
glimpse(salidas)
## Rows: 144
## Columns: 2
## $ Fecha   <date> 2008-01-01, 2008-02-01, 2008-03-01, 2008-04-01, 2008-05-01, 2~
## $ Salidas <int> 1034682, 957075, 1132697, 1055504, 1087835, 1089415, 1257439, ~

Ya tenemos la variable Fecha en el formato correcto, sin embargo, la mayoría de las funciones relacionadas con el análisis de una serie temporal requieren que los datos estén en formato de serie de tiempo (ts), por lo que, haremos la conversion:

salidas.ts <- ts(salidas[,2], start = c(2008, 1), end = c(2019, 12), frequency = 12)

ts_info(salidas.ts)
##  The salidas.ts series is a ts object with 1 variable and 144 observations
##  Frequency: 12 
##  Start time: 2008 1 
##  End time: 2019 12

Ahora si, ya tenemos nuestra serie de tiempo la cual inicia en enero de 2008 y termina en diciembre de 2019. Pasemos ahora al análisis exploratorio.

 

Análisis exploratorio

Análisis visual

Empecemos por analizar visualmente la serie para conocer qué forma tiene así como sus principales medidas estadísticas:

salidas %>% 
  ggplot(aes(Fecha, Salidas)) +
  geom_line() +
  scale_x_date(date_breaks = "6 month", date_labels = "%Y-%b") +
  theme(axis.text.x = element_text(angle = 90, size = 8)) +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "AICM: Salidas Nacionales e Internacionales 2008-2019")

Lo que se puede apreciar es que hasta finales de 2010 la tendencia es constante y hasta un poco decreciente, sin embargo, desde inicios de 2011 la tendencia es creciente y aditiva. Esta serie claramente muestra un cambio de tendencia, lo que muy probablemente ocasione que no se pueda cumplir el supuesto de estacionariedad incluso si aplicamos diferencias. Conviene de una vez comprobar ese cambio o quiebre, para lo cual haremos lo siguiente:

  1. Crear el gráfico CUSUM, que nos mostrará visualmente si la serie presenta un quiebre y en qué parte.
  2. Crear una variable dicotómica que tomará valores de 0 hasta enero de 2011 y posterior a esa fecha tomara valores de 1.
  3. Hacer una regresión lineal cuyas variables independientes serán: la variable dicotómica creada, el tiempo (que será un consecutivo) y la interacción entre el tiempo y la variable dicotómica.
t <- c(1:length(salidas.ts))

p.salidas <- efp(salidas.ts ~ t, type = "OLS-CUSUM")

plot(p.salidas)

La gráfica nos indica que la serie sí presenta un quiebre en su tendencia al parecer desde 2009 hasta inicios de 2011. Ahora, lo comprobaremos formalmente con una regresión.

t.salidas <- salidas %>% 
  mutate(D = if_else(Fecha < "2011-02-01", 0, 1))

reg <- lm(Salidas ~ D + t + (D*t), data = t.salidas)

summary(reg)
## 
## Call:
## lm(formula = Salidas ~ D + t + (D * t), data = t.salidas)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -338916  -55131   -2537   68120  306220 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  1092503      40545  26.946  < 2e-16 ***
## D            -465389      54447  -8.548 1.94e-14 ***
## t              -3135       1860  -1.685   0.0942 .  
## D:t            13865       1898   7.304 1.93e-11 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 120800 on 140 degrees of freedom
## Multiple R-squared:  0.9103, Adjusted R-squared:  0.9084 
## F-statistic: 473.7 on 3 and 140 DF,  p-value: < 2.2e-16

Los coeficientes de la variable dicotómica y de su interacción con el tiempo salieron significativos. Ahora, el coeficeinte de la variable dicotómica D, es el intercepto diferencial y el coeficiente de la interacción entre el tiempo y la variable dicotómica es el coeficiente de la pendiente diferencial. Lo anterior, significa que los dos periodos son distintos y las causas de la diferencia son tanto el intercepto como la pendiente.

Por lo anterior, la serie que vamos a modelar presenta cambio de tendencia a partir de 2011, lo cual se explica en parte debido a que en 2008-2009 se presentó la crisis financiera y, además, en 2009, en México, inició la crisis sanitaria de la Influenza H1N1, también, a inicios de 2011 empezaron los problemas financieros de la empresa Mexianana de Aviación, la cual terminó por cerrar sus operaciones a finales de agosto de ese mismo año. Todos esos acontecimientos afectaron la economía lo que explica la tendencia a la baja en el periodo 2008-2011.

Además, se puede sospechar que hay presencia de estacionalidad por el patrón que sigue la serie, también, hay que considerar que la data representa a las personas que viajan por avión, por lo que, el sentido común nos indicaría que en periodos vacacionales la cantidad de personas que salen de viaje aumentará. Más adelante lo tendremos que comprobar formalmente.

Datos atípicos (outliers)

 

Algo que afecta mucho a los modelos de series de tiempo son los llamados outliers o datos atípicos, ya que al ser observaciones demasiado grandes o pequeñas afectan la capacidad predictiva de los modelos. Por lo tanto, es de vital importancia identificarlos y tratarlos. Un primer análisis visual se hace con apoyo del boxplot:

salidas %>% 
  ggplot(aes(x = factor(1), y = Salidas)) +
  geom_boxplot(outlier.colour = "red", fill = "grey", alpha = 0.3) +
  stat_summary(fun = mean, 
               geom = "point",
               shape = 20, 
               size = 4, 
               color = "red",
               fill = "red") +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Boxplot Salidas",
    x = "Salidas"
  )

En el boxplot se aprecia que la serie no presenta observaciones atípicas, además, se observa que la media (punto rojo) y la mediana no están muy separadas lo que nos indica que no hay asimetria fuerte. Complementemos la gráfica con algunos estadísiticos básicos:

descr(salidas$Salidas,
      justify = "c",
      headings = T) %>% 
  kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
  kable_paper("hover", full_width = F)
Salidas
Mean 1,456,935.94
Std.Dev 399,210.23
Min 700,290.00
Q1 1,104,285.00
Median 1,373,431.00
Q3 1,788,912.00
Max 2,339,595.00
MAD 451,024.71
IQR 679,670.00
CV 0.27
Skewness 0.34
SE.Skewness 0.20
Kurtosis -1.08
N.Valid 144.00
Pct.Valid 100.00

Pasemos a otro método más robusto para detectar si realmente en la serie hay observaciones atípicas. Para esto nos apoyaremos del paquete anomalize que nos permitirá calibrar el intervalo de confianza y el porcentaje máximo de posibles valores atípicos a mostrar, entre otras cosas. Algo importante a mencionar es que este paquete requiere que el input esté en formato de tibble o tibble_time, por lo que, no usaremos el objeto tipo serie de tiempo creado al inicio del análisis.

salidas.tbl <- salidas

salidas.tbl$Fecha <- as.yearmon(salidas.tbl$Fecha)
salidas.tbl$Salidas <- as.numeric(salidas.tbl$Salidas)

salidas.tbl <- as_tibble(salidas.tbl)

prep_tbl_time(salidas.tbl, message = TRUE)
salidas.tbl %>% 
  time_decompose(Salidas, method = "stl") %>% 
  anomalize(remainder, method = "gesd", max_anoms = 0.2, alpha = 0.03) %>% 
  time_recompose() %>% 
  plot_anomalies(time_recomposed = TRUE, 
                 alpha_dots = 0.4,
                 alpha_ribbon = 0.1,
                 fill_ribbon = "grey80") + 
  scale_y_continuous(labels = scales::comma) + 
  labs(title = "Detectando Anomalías", y = "Salidas")

En la gráfica anterior podemos ver que se detectaron algunas observaciones atípicas, sin embargo, al parecer solo uno de ellos está muy alejado del patrón de la serie. Tendremos que tratarlo para que no nos cause problemas más adelante.

Para identificar qué observaciones son atípicas usaremos la función tsoutliers del paquete forecast, ya que permite hacer la identificación y, además, nos propondrá los valores a sustituir.

tsoutliers(salidas.ts) %>% 
  as.data.frame() %>%
  rename(Observacion = index, Reemplazo = replacements) %>% 
  kbl(align = "c",digits = 0 ,format.args = list(big.mark = ",")) %>%
  kable_paper("hover", full_width = F)
Observacion Reemplazo
14 878,178
17 1,039,968

Vemos que la función identifica las observaciones en las posiciones 14 y 17 como atípicas, y nos muestra los valores por los cuales haría la sustitución (que utiliza el método de interpolación lineal). El trabajo lo hace la función tsclean que no solo sustituye los valores atípicos sino que también identifica valores faltantes (que no es este el caso).

Veamos gráficamente cuáles serán los puntos que se sustituirán:

# nuevos puntos
autoplot(tsclean(ts(salidas$Salidas)), series = "clean", colour = 'red', lwd = 0.9) +
  autolayer(ts(salidas$Salidas), series = "original", color = 'gray', lwd = 1) +
  geom_point(data = tsoutliers(salidas.ts) %>% as.data.frame(), 
             aes(x = index, y = replacements), col = 'blue') +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Sustitución Outliers: nuevos puntos", x = "Tiempo", y = "Salidas")

Los puntos en color azul serán las nuevas observaciones que reemplazarán a los outliers que se identificaron. Se hace la sustitución:

salidas.ts <- tsclean(salidas.ts)

Se hace el reemplazo en la base original para volver a ver la gráfica de las anomalías:

# se actualizan los datos en el dataframe original
salidas$Salidas[14] <- 878178.1
salidas$Salidas[17] <- 1039967.7

# se repite el proceso para ver la grafica

salidas.tbl <- salidas

salidas.tbl$Fecha <- as.yearmon(salidas.tbl$Fecha)
salidas.tbl$Salidas <- as.numeric(salidas.tbl$Salidas)

salidas.tbl <- as_tibble(salidas.tbl)

#prep_tbl_time(salidas.tbl, message = TRUE)

salidas.tbl %>% 
  time_decompose(Salidas, method = "stl") %>% 
  anomalize(remainder, method = "gesd", max_anoms = 0.2, alpha = 0.03) %>% 
  time_recompose() %>% 
  plot_anomalies(time_recomposed = TRUE, 
                 alpha_dots = 0.4,
                 alpha_ribbon = 0.1,
                 fill_ribbon = "grey80") +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Detectando Anomalías", y = "Salidas")

Se siguen detectando datos atípicos, sin embargo, no están muy alejados del intervalo de confianza, por lo que, nos quedaremos con esta serie.

Algo importante a mencionar es que se debe tener bien claro el objetivo del análisis que se esté buscando, ya que en algunos casos podría ser incorrecto sustituir las observaciones atípicas.

Correlogramas

 

Para todos los modelos de series de tiempo es de suma importancia revisar las funciones de autocorrelación tanto simple como parcial con el propósito de empezar a conocer cómo está la estructura de los datos. Los correlogramas en palabras simples nos muestran la memoria de la serie, o sea, qué tanto depende el dato actual de lo que sucedió en el pasado. veamos los correlogramas de la serie con 48 rezagos, que son un tercio de la longitud de la serie (lo recomendado):

ts_cor(salidas.ts, lag.max = 48)

 

Al observar el correlograma ACF podemos sospechar que la serie no es estacionaria, o sea, la media no es constante a lo largo del tiempo, porque los valores de la función de autocorrelación no decaen rápidamente conforme aumentan los rezagos en el tiempo. Además, al parecer también hay presencia de estacionalidad, ya que en PACF hay coeficientes significativos, por lo que, tenemos que descomponer la serie para ver cada componente por separado.

Descomposición de la serie

 

Vamos a generar las gráficas de los componentes de esta serie: tendencia, estacionalidad y residuo.

ts_decompose(salidas.ts, type = "additive")

Lo que se puede apreciar en las gráficas es que la tendencia es aditiva, ya que la serie va en aumento con el tiempo y las oscilaciones no se van haciendo más grandes sino que se mantienen del mismo tamaño. Lo anterior, tenemos que confirmarlo de manera un poco más formal.

Hay presencia de estacionalidad muy marcada, ya que el mismo patrón se repite año tras año. También, hay que confirmarlo e identificar en qué meses se presenta.

Vamos a identificar formalmente el tipo de tendencia que tiene la serie, para lo cual se necesita hacer lo siguiente:

  • calcular el logaritmo de la mediana para cada periodo.
  • calcular el logaritmo de la diferencia entre el percentil 80 y el percentil 20 para cada periodo.
  • comparar ambas series en un gráfico de dispersión.
  • calcular el coeficiente de correlación.
  • probar la significancia estadistica de dicho coeficiente.
tabla <- salidas %>% 
  mutate(
    Año = year(Fecha),
    Mes = month(Fecha)
  ) %>% 
  select(-Fecha) %>% 
  group_by(Año) %>% 
  summarise(ln_Mediana = log(median(Salidas)),
         ln_Rango = log((quantile(Salidas, 0.8) - quantile(Salidas, 0.2))))
  
tabla %>% 
  ggplot(aes(ln_Mediana, ln_Rango)) +
  geom_point() +
  labs(title = "Niveles vs Dispersión",
       x = "Nivel",
       y = "Dispersión")

cor(tabla$ln_Mediana, tabla$ln_Rango)
## [1] 0.8175311
cor.test(tabla$ln_Mediana, tabla$ln_Rango)
## 
##  Pearson's product-moment correlation
## 
## data:  tabla$ln_Mediana and tabla$ln_Rango
## t = 4.4892, df = 10, p-value = 0.001162
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.4589703 0.9470796
## sample estimates:
##       cor 
## 0.8175311

Al ver la gráfica creada podemos ver que existe cierta dependencia y tendencia positiva entre el nivel y la dispersión, lo que nos sugiere que la tendencia es de tipo multiplicativa, sin embargo, al volver a la gráfica en niveles se observa que en los primeros años la serie es constante y a partir de 2011 se presenta tendencia creciente. Por lo anterior, los primeros años están jalando o disfrasando el verdadero tipo de tendencia de la serie. Además, hay que recordar que ya comprobamos que la serie presenta un quiebre de tendencia, lo cual afecta esta gráfica, ya que las primeras observaciones que corresponden al periodo 2008-2011 la relación es positiva y para el periodo 2012-2019 parece que no hay relación.

Análisis de estacionariedad

 

En esta parte, vamos a analizar formalmente la estacionariedad de la serie. En series de tiempo, es necesario que la serie a modelar sea estable, o sea, tenga una media y varianza constantes, sobre todo si el modelo a usar para realizar el pronóstico es un arima. Antes del análisis hay que tener presente que la serie al presentar quiebre de tendencia afectará cualquier prueba de estacionariedad que se le haga, por lo tanto, es de esperar que, aunque se apliquen diferencias, puede que no pase la prueba elegida.

Volvamos a mirar los correlogramas:

ggAcf(salidas.ts, lag.max = 48)

ggPacf(salidas.ts, lag.max = 48)

En el ACF vemos que los coeficientes decaen lentamente, lo cual es un indicativo de que la serie no es estacionaria, sin embargo, para confirmarlo formalmente haremos el test de raíz unitaria de Augmented Dickey-Fuller:

adf.test(salidas.ts)
## 
##  Augmented Dickey-Fuller Test
## 
## data:  salidas.ts
## Dickey-Fuller = -3.984, Lag order = 5, p-value = 0.01203
## alternative hypothesis: stationary

El p-valor de la prueba (0.01203) es menor al nivel de significancia de 0.05, por lo que, la prueba nos indica que la serie es estacionaria. Contrario a lo que nos mostraba el correlograma, o sea, al no tener raíz unitaria la serie no presenta un patrón sistemático impredecible. Sin embargo, vamos a determinar con apoyo de un gráfico y una regresión si debemos hacer alguna transformación a los datos, ya que la gráfica en niveles de la serie sugiere estabilizar la serie. Será la gráfica conocida como rango/media, que consiste en calcular la media de cada periodo (año) y la varianza. Posteriormente, se hace una regresión con ambas variables y si es significativo el coeficiente asociado con la media entonces hay que aplicar alguna transformación a la serie:

tabla2 <- salidas %>% 
  mutate(
    Año = year(Fecha),
    Mes = month(Fecha)
  ) %>% 
  select(-Fecha) %>% 
  group_by(Año) %>% 
  summarise(ln_Media = log(mean(Salidas)),
         ln_varianza = log(var(Salidas)))

ggplot(tabla2, aes(x = ln_Media, y = ln_varianza)) +
  geom_point() +
  labs(title = "Gráfico rango-media")

coeftest(lm(ln_varianza ~ ln_Media, data = tabla2))
## 
## t test of coefficients:
## 
##             Estimate Std. Error t value Pr(>|t|)   
## (Intercept)  5.99895    5.25816  1.1409 0.280504   
## ln_Media     1.24018    0.37127  3.3404 0.007486 **
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

En el gráfico se aprecia que los primeros años están jalando la tendencia de la nube de puntos, además, el resultado de la regresión nos indica que debemos hacer alguna transformación a la serie, ya que el coeficiente asociado con la media resultó significativo.

Detección de estacionalidad

 

Ahora, pasemos al análisis de la estacionalidad empezando por el análisis visual:

salidas %>% 
  mutate(
    Año = year(Fecha),
    Mes = month(Fecha)
  ) %>% 
  select(-Fecha) %>% 
  group_by(Año, Mes) %>% 
  ggplot(aes(x = factor(Mes), y = Salidas)) +
  geom_boxplot() +
  scale_y_continuous(labels = scales::comma) +
  stat_summary(fun = mean, 
               geom = "point",
               shape = 20, 
               size = 4, 
               color = "red",
               fill = "red") +
  labs(title = "Boxplot Meses",
       x = "Meses")

ggsubseriesplot(salidas.ts) +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Salidas Promedio", x = "Meses", y = "Salidas")

Al ver el boxplot y la gráfica de promedios mensuales parece que si hay diferencias entre los meses en cada año y, por lo tanto, presencia de un patrón estacional. Sigamos con el análisis de la serie:

ggseasonplot(salidas.ts) + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Salidas por Mes", y = "Salidas", x = "Mes")

Las gráficas vistas hasta aquí nos indican que es en el mes de julio donde la afluencia de pasajeros es mayor, lo cual coincide con el periodo vacacional de verano. También, se aprecia que el segundo mes más alto de salidas es diciembre como era de esperarse por la temporada navideña.

Sigamos viendo algunas vistas más para confirmar el patrón estacional existente:

ts_seasonal(salidas.ts, type = "all", title = "Gráficos con la Tendencia")

El patrón estacional es el mismo a lo largo del tiempo: alta demanda en el mes de julio y diciembre y baja demanda en febrero y septiembre.

Para tener una visión más clara del patrón estacional vamos a quitar el efecto de la tendencia, ya que la serie crece año tras año desde el año 2011:

ts_seasonal(salidas.ts - decompose(salidas.ts)$trend, 
            type = "all", 
            title = "Gráfico sin Tendencia")

Sin el efecto de la tendencia se aprecia más claramente el patrón estacional. Podemos identificar que julio, agosto y diciembre son los meses de mayores salidas o vuelos, por el contrario, enero, febrero y septiembre los meses de menor demanda o salidas de pasajeros.

Otra vista diferente de la serie es mediante un heatmap:

ts_heatmap(salidas.ts, title = "Salidas")

Se aprecia que la demanda de pasajeros que sale de viaje ya sea al interior del país o al extranjero, desde la Ciudad de México, ha estado creciendo de manera sostenida desde los últimos 8 años. Además, se confirma que julio es el mes con mayor demanda seguido por el mes de diciembre.

Ahora, el siguiente paso es identificar el nivel de correlación entre la serie y sus rezagos:

ts_cor(salidas.ts, lag.max = 48)

Como era de esperarse sí hay una correlación entre la serie y sus rezagos estacionales. El retraso estacional (o retraso 12) de la serie tiene una fuerte relación lineal con la serie. Lo anterior, lo podemos ver más claramente al graficar estos componentes:

ts_lags(salidas.ts, lags = c(12, 24, 36, 48))

Solo nos queda identificar formalmente la estacionalidad. Para ello vamos a crear variables dummies, una por cada mes para, posteriormente, hacer una regresión sin incluir la constante para evitar la colinealidad. Para que el efecto estacional este presente los parámetros estimados del modelo deben ser estadísticamente distintos de cero, individualmente.

tabla3 <- salidas %>% 
  mutate(
    Mes = month(Fecha)
  ) %>% 
  mutate(
    Enero = ifelse(Mes == 1, 1,0),
    Febrero = ifelse(Mes == 2, 1,0),
    Marzo = ifelse(Mes == 3, 1,0),
    Abril = ifelse(Mes == 4, 1,0),
    Mayo = ifelse(Mes == 5, 1,0),
    Junio = ifelse(Mes == 6, 1,0),
    Julio = ifelse(Mes == 7, 1,0),
    Agosto = ifelse(Mes == 8, 1,0),
    Septiembre = ifelse(Mes == 9, 1,0),
    Octubre = ifelse(Mes == 10, 1,0),
    Noviembre = ifelse(Mes == 11, 1,0),
    Diciembre = ifelse(Mes == 12, 1,0)
  ) %>% 
  dplyr::select(-Fecha, -Mes)

modelo <- lm(Salidas ~ 0 +., data = tabla3)
summary(modelo)
## 
## Call:
## lm(formula = Salidas ~ 0 + ., data = tabla3)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -550852 -351625 -101963  345302  726060 
## 
## Coefficients:
##            Estimate Std. Error t value Pr(>|t|)    
## Enero       1354725     113425   11.94   <2e-16 ***
## Febrero     1206017     113425   10.63   <2e-16 ***
## Marzo       1454283     113425   12.82   <2e-16 ***
## Abril       1411900     113425   12.45   <2e-16 ***
## Mayo        1464396     113425   12.91   <2e-16 ***
## Junio       1443255     113425   12.72   <2e-16 ***
## Julio       1696737     113425   14.96   <2e-16 ***
## Agosto      1564176     113425   13.79   <2e-16 ***
## Septiembre  1355666     113425   11.95   <2e-16 ***
## Octubre     1465781     113425   12.92   <2e-16 ***
## Noviembre   1479518     113425   13.04   <2e-16 ***
## Diciembre   1605587     113425   14.16   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 392900 on 132 degrees of freedom
## Multiple R-squared:  0.938,  Adjusted R-squared:  0.9324 
## F-statistic: 166.5 on 12 and 132 DF,  p-value: < 2.2e-16

Los coeficientes resultaron significativos, por lo que, confirmamos la presencia de estacionalidad en la serie.

Ya para terminar esta sección, calcularemos los índices de variación estacional para darnos una idea del impacto en cada mes. Para lo cual, haremos lo siguiente:

  • se descompone la serie.
  • se divide la serie original entre su tendencia.
  • se promedian los periodos.
  • se ajustan los indices para que sumen el total de periodos (vía ponderaciones).
dc <- decompose(salidas.ts, type = "additive")

salidas %>% 
  mutate(
    serie = dc$x,
    tendencia = dc$trend,
    Año = year(Fecha),
    Mes = month(Fecha),
    v1 = serie/tendencia
  ) %>% 
  group_by(Mes) %>% 
  summarise(ie = mean(v1, na.rm = T)) %>% 
  kbl(align = "c", digits = 4) %>% kable_paper("hover", full_width = F)
Mes ie
1 0.9537
2 0.8408
3 1.0149
4 0.9802
5 1.0114
6 0.9887
7 1.1712
8 1.0690
9 0.9147
10 0.9878
11 0.9898
12 1.0758

Los resultados nos indican que en el mes de julio la demanda es mayor al promedio anual un 17%, aproximadamente, por el contrario, en el mes de febrero la demanda de salidas es 16% menor al promedio anual, en otras palabras, en los meses de febrero, en promedio, solo se llega al 84% del promedio anual.

Transformaciones

 

Vimos que la serie en niveles tiene tendencia creciente y de tipo aditiva, porque los picos en cada año no aumentan. Por lo anterior, se tiene que aplicar alguna transformación y la más común son las diferencias.

Hay que identificar cuántas diferenciaciones aplicar a la serie. Se puede hacer aplicando 1 diferencia y revisar el correlograma hasta que veamos la serie sin tendencia, sin embargo, optaremos por un camino más directo con apoyo de la función ndiffs del paquete forecast que nos indicará cuántas diferencias aplicar a la serie:

Número de diferenciaciones a la parte regular de la serie: 1

Ya que confirmamos que hay estacionalidad en la serie tenemos que determinar cuántas diferencias aplicar. Nos apoyaremos de la función nsdiffs del paquete forecast que nos indica el orden de diferenciación (esto tambien se puede hacer manualmente diferenciando la serie en los periodos estacionales y revisando el correlograma):

Número de diferenciaciones a la parte regular de la serie: 1

Por lo tanto, hay que diferenciar 1 vez en la parte regular y 1 vez a la parte estacional de la serie en niveles. Esto se especificará en el algoritmo del modelo.

Conjunto de entrenamiento y prueba

 

Antes de generar el primer modelo, es necesario dividir la serie en una parte para entrenar el modelo y otra parte para validarlo. Cuando se trabaja con series de tiempo dicha partición no se hace de manera aleatoria como para cualquier otro modelo de machine learning, ya que las series de tiempo van ordenadas de manera cronológica, por lo tanto, el conjunto de test siempre será la parte final de la serie. Consideraremos el año 2019 para probar el modelo y el resto para entrenar el modelo.

salidas.ts.train <- window(salidas.ts, start = c(2008,1), end = c(2018,12))
salidas.ts.test <- window(salidas.ts, start = c(2019,1), end = c(2019,12))

Creación del modelo base

 

Cuando el objetivo de un proyecto sea el pronosticar una serie de tiempo, es altamente recomendable hacer un primer modelo que será la base a partir del cual se tratará de mejorarlo. Hay muchas métricas para evaluar este tipo de modelos, la mayoría relacionadas con los errores, o sea, la diferencia entre el valor real y el valor pronosticado. Ahora, lo que se debe evaluar es el dataset de test, o sea, la parte del conjunto de datos que el modelo no ha visto. En el presente análisis usaremos el Mean Absolute Percentage Error (MAPE), ya que es más fácil de interpretar.

Nuestro primer modelo será el modelo básico de suavizado exponencial ets, que está formado por: el componente de error, el componente de tendencia y el componente estacional.

ETS: ExponenTial Smoothing

fit.ets <- ets(salidas.ts.train,
               model = "ZZZ",
               damped = T,
               additive.only = T,
               lambda = "auto"
               )


fit.ets
## ETS(A,Ad,A) 
## 
## Call:
##  ets(y = salidas.ts.train, model = "ZZZ", damped = T, additive.only = T,  
## 
##  Call:
##      lambda = "auto") 
## 
##   Box-Cox transformation: lambda= 0.3839 
## 
##   Smoothing parameters:
##     alpha = 0.6174 
##     beta  = 0.0331 
##     gamma = 1e-04 
##     phi   = 0.98 
## 
##   Initial states:
##     l = 546.3433 
##     b = -1.119 
##     s = 18.0384 -1.8706 -2.3477 -19.5167 16.0081 37.7301
##            -2.2611 2.3417 -4.531 4.3537 -37.3082 -10.6369
## 
##   sigma:  6.3844
## 
##      AIC     AICc      BIC 
## 1151.750 1157.803 1203.641

El resultado del modelo: (A,Ad,A), nos indica que modeló los tres componentes de forma aditiva. Veamos ahora los residuales, para identificar si existe autocorrelación:

checkresiduals(fit.ets, lag = 26)

## 
##  Ljung-Box test
## 
## data:  Residuals from ETS(A,Ad,A)
## Q* = 45.171, df = 9, p-value = 8.579e-07
## 
## Model df: 17.   Total lags used: 26

El p-valor de la prueba nos indica que los errores no son independientes, por lo que, el pasado está influyendo en lo que sucede en el presente. Habrá que corregir esto más adelante, por lo pronto, se sigue anializando este primer modelo.

qqnorm(fit.ets$residuals)
qqline(fit.ets$residuals)

jarque.bera.test(fit.ets$residuals)
## 
##  Jarque Bera Test
## 
## data:  fit.ets$residuals
## X-squared = 25.011, df = 2, p-value = 3.706e-06

El análisis de la normalidad muestra que los errores no se distribuyen de forma normal. Sin embargo, como es el modelo base sigamos adelante con las predicciones para los siguientes 12 meses.

fc.ets <- forecast(fit.ets, h = 12)


fc.ets %>% as.data.frame() %>% 
  kbl(align = "c", format.args = list(big.mark = ",")) %>% 
  kable_paper("hover", full_width = F)
Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
Jan 2019 1,936,463 1,875,897 1,998,220 1,844,313 2,031,397
Feb 2019 1,749,042 1,681,361 1,818,376 1,646,196 1,855,754
Mar 2019 2,062,943 1,976,726 2,151,438 1,932,001 2,199,215
Apr 2019 2,000,518 1,905,382 2,098,526 1,856,172 2,151,582
May 2019 2,059,264 1,951,980 2,170,106 1,896,613 2,230,238
Jun 2019 2,029,460 1,913,046 2,150,139 1,853,127 2,215,770
Jul 2019 2,357,858 2,219,125 2,501,811 2,147,772 2,580,152
Aug 2019 2,184,675 2,042,108 2,333,217 1,969,024 2,414,299
Sep 2019 1,915,273 1,774,587 2,062,630 1,702,772 2,143,376
Oct 2019 2,050,692 1,894,167 2,214,944 1,814,388 2,305,069
Nov 2019 2,059,622 1,893,005 2,234,983 1,808,285 2,331,409
Dec 2019 2,223,165 2,038,292 2,418,025 1,944,401 2,525,288
forecast::accuracy(fc.ets, salidas.ts.test)
                ME     RMSE      MAE      MPE     MAPE      MASE

Training set 4523.606 34262.94 25741.90 0.319050 1.981202 0.2285163 Test set 45827.168 61529.56 48871.01 2.174499 2.304601 0.4338384 ACF1 Theil’s U Training set -0.04758451 NA Test set 0.34334424 0.3529856

autoplot(fc.ets) + scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico Modelo Base: ets(A,Ad,A)", y = "Salidas")

Este primer modelo presenta un MAPE de 2.3% para el conjunto de test, no es muy elevado, sin embargo, no pasó los supuestos de independencia de los errores ni normalidad. Veamos cómo se ve la gráfica de las predicciones con la serie original.

test_forecast(forecast.obj = fc.ets, 
              actual = salidas.ts, 
              test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))

Ajustemos el modelo ets para mejorarlo diferenciando 1 vez la serie:

salidas.ts.diff <- diff(salidas.ts)

salidas.ts.train.diff <- window(salidas.ts.diff, start = c(2008,2), end = c(2018,12))
salidas.ts.test.diff <- window(salidas.ts.diff, start = c(2019,1), end = c(2019,12))

fit.ets.diff <- ets(salidas.ts.train.diff,
               model = "ZZZ",
               damped = T
               )

checkresiduals(fit.ets.diff)

## 
##  Ljung-Box test
## 
## data:  Residuals from ETS(A,Ad,A)
## Q* = 33.525, df = 7, p-value = 2.113e-05
## 
## Model df: 17.   Total lags used: 24
qqnorm(fit.ets.diff$residuals)
qqline(fit.ets.diff$residuals)

jarque.bera.test(fit.ets.diff$residuals)
## 
##  Jarque Bera Test
## 
## data:  fit.ets.diff$residuals
## X-squared = 0.86664, df = 2, p-value = 0.6484

En el correlograma de los residuales podemos ver que al inicio solo un coeficiente sale de las bandas, sin embargo, el test no lo pasa. En cuanto a la prueba de normalidad vemos que ya la pasa al tener un p-valor mayor al 0.05. Nos quedamos con este modelo, por lo que, ahora se pronosticarán los próximos 12 meses.

fc.ets.diff <- forecast(fit.ets.diff, h = 12)

last_salidas <- salidas.ts.train[132]
pred <- fc.ets.diff$mean

de_diff_salidas_fore <- cumsum(pred) + last_salidas

ddiff_sfore_ts <- ts(de_diff_salidas_fore, start = 133)

ddiff_sfore_ts <- ts(ddiff_sfore_ts, start = c(2019, 1), frequency = 12)

ddiff_sfore_ts %>% as.data.frame() %>% 
  rename(Pronostico = x) %>%  mutate(Fecha = salidas$Fecha[133:144]) %>% 
  select(Fecha, Pronostico) %>% 
  kbl(align = "c", format.args = list(big.mark = ",")) %>% 
  kable_paper("hover", full_width = F)
Fecha Pronostico
2019-01-01 1,966,643
2019-02-01 1,819,524
2019-03-01 2,072,125
2019-04-01 2,031,114
2019-05-01 2,084,730
2019-06-01 2,069,841
2019-07-01 2,335,097
2019-08-01 2,208,419
2019-09-01 2,010,270
2019-10-01 2,119,293
2019-11-01 2,136,397
2019-12-01 2,269,743
forecast::accuracy(ddiff_sfore_ts, salidas.ts.test)
##                ME     RMSE      MAE       MPE     MAPE      ACF1 Theil's U
## Test set 4642.229 45350.73 32506.31 0.1398487 1.588626 0.5558924 0.2636444
ts.plot(salidas.ts, ddiff_sfore_ts,
        lty = c(1,3), col = c("steelblue", "red"), 
        main = "Pronóstico modelo en 1ras Diferencias: ets(A,Ad,A)", 
        ylab = "Salidas")

El modelo con diferencias mejoró, ya que se reduce el error de test de 2.3 a 1.58.

Ahora, optimizaremos los parámetros.

Este algoritmo tiene tres parámetros que están relacionados con el suavisado (alpha), la tendencia (beta) y la estacionalidad (gamma). Por tanto, vamos a optimizarlos para encontrar el valor que nos arroje el modelo con el menor error y una vez encontrado el valor del componente volveremos a reajustar el modelo.

Alpha: componente de suavisado

# identificar el alpha óptimo
alpha <- seq(.01, .99, by = .001)

MAPE <- NA

for(i in seq_along(alpha)) {
  fit <- ets(salidas.ts.train, 
             alpha = alpha[i],
             model = "ZZZ",
             damped = T,
             additive.only = T,
             lambda = "auto")
  
  fc.fit <- forecast(fit, h = 12)

  MAPE[i] <- forecast::accuracy(fc.fit, salidas.ts.test)[2,5]
}

# convertir a data frame e identificar el valor del alpha mínimo
alpha.fit <- tibble(alpha, MAPE)
alpha.min <- filter(alpha.fit, MAPE == min(MAPE))
alpha.min
# gráfico MAPE vs. alpha
ggplot(alpha.fit, aes(alpha, MAPE)) +
  geom_line() +
  geom_point(data = alpha.min, aes(alpha, MAPE), size = 2, color = "blue") +
  labs(title = "Mean Absolute Percentage Error")

Beta: componente de tendencia

# identificar el beta óptimo
beta <- seq(.01, .99, by = .001)

MAPE <- NA

for(i in seq_along(beta)) {
  fit <- ets(salidas.ts.train, 
             beta = beta[i],
             model = "ZZZ",
             damped = T,
             additive.only = T,
             lambda = "auto")
  
  fc.fit <- forecast(fit, h = 12)
  
  MAPE[i] <- forecast::accuracy(fc.fit, salidas.ts.test)[2,5]
}
# convertir a data frame e identificar el valor del beta mínimo
beta.fit <- tibble(beta, MAPE)
beta.min <- filter(beta.fit, MAPE == min(MAPE))
beta.min
# gráfico MAPE vs. beta
ggplot(beta.fit, aes(beta, MAPE)) +
  geom_line() +
  geom_point(data = beta.min, aes(beta, MAPE), size = 2, color = "blue") +
  labs(title = "Mean Absolute Percentage Error")

Gamma: componente de estacionalidad

# identificar el gamma óptimo
gamma <- seq(.01, .99, by = .001)

MAPE <- NA

for(i in seq_along(gamma)) {
  fit <- ets(salidas.ts.train, 
             gamma = gamma[i],
             model = "ZZZ",
             damped = T,
             additive.only = T,
             lambda = "auto")
  
  fc.fit <- forecast(fit, h = 12)
  
  MAPE[i] <- forecast::accuracy(fc.fit, salidas.ts.test)[2,5]
}

# convertir a data frame e identificar el valor del gamma mínimo
gamma.fit <- tibble(gamma, MAPE)
gamma.min <- filter(gamma.fit, MAPE == min(MAPE))
gamma.min
# gráfico MAPE vs. gamma
ggplot(gamma.fit, aes(gamma, MAPE)) +
  geom_line() +
  geom_point(data = gamma.min, aes(gamma, MAPE), size = 2, color = "blue") +
  labs(title = "Mean Absolute Percentage Error")

Al optimizar los tres componentes el que generó el menor error fue alpha con 1.53. Se reajusta del modelo con ese parámetro:

# reajuste del modelo con alpha optimo
fit.ets <- ets(salidas.ts.train,
               model = "ZZZ",
               damped = T,
               additive.only = T,
               lambda = "auto",
               alpha = alpha.min$alpha
               )

fit.ets
## ETS(A,Ad,A) 
## 
## Call:
##  ets(y = salidas.ts.train, model = "ZZZ", damped = T, alpha = alpha.min$alpha,  
## 
##  Call:
##      additive.only = T, lambda = "auto") 
## 
##   Box-Cox transformation: lambda= 0.3839 
## 
##   Smoothing parameters:
##     alpha = 0.249 
##     beta  = 0.0294 
##     gamma = 1e-04 
##     phi   = 0.98 
## 
##   Initial states:
##     l = 546.569 
##     b = -0.9689 
##     s = 17.7407 -1.8181 -2.1958 -19.3507 15.9644 37.4089
##            -2.1358 2.2996 -4.6524 4.2632 -36.9145 -10.6095
## 
##   sigma:  7.1591
## 
##      AIC     AICc      BIC 
## 1179.983 1185.352 1228.991
checkresiduals(fit.ets, lag = 26)

## 
##  Ljung-Box test
## 
## data:  Residuals from ETS(A,Ad,A)
## Q* = 109.41, df = 9, p-value < 2.2e-16
## 
## Model df: 17.   Total lags used: 26
qqnorm(fit.ets$residuals)
qqline(fit.ets$residuals)

jarque.bera.test(fit.ets$residuals)
## 
##  Jarque Bera Test
## 
## data:  fit.ets$residuals
## X-squared = 11.782, df = 2, p-value = 0.002764
fc.ets <- forecast(fit.ets, h = 12)

forecast::accuracy(fc.ets, salidas.ts.test)
##                     ME     RMSE      MAE       MPE     MAPE      MASE      ACF1
## Training set  6165.468 37909.77 30182.91 0.4347513 2.339992 0.2679402 0.3831377
## Test set     20412.619 45755.96 32768.79 0.9654602 1.537096 0.2908955 0.3492182
##              Theil's U
## Training set        NA
## Test set     0.2639074
test_forecast(forecast.obj = fc.ets, 
              actual = salidas.ts, 
              test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))
autoplot(fc.ets) + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico ETS Salidas 2019", y = "Salidas")

ARIMA: AutoRegressive Integrated Moving Average

 

Ahora se optará por un modelo arima para ver si podemos mejorar el error de test y, además, que sus residuales pasen las pruebas de normalidad y autocorrelación. Se comenzará por la generación automática del modelo con apoyo de la función auto.arima del paquete forecast:

fitarima1 <- auto.arima(salidas.ts.train)
fitarima1
## Series: salidas.ts.train 
## ARIMA(0,1,1)(0,1,2)[12] 
## 
## Coefficients:
##           ma1     sma1    sma2
##       -0.3665  -0.8231  0.2908
## s.e.   0.0890   0.1126  0.1504
## 
## sigma^2 estimated as 1.744e+09:  log likelihood=-1437.71
## AIC=2883.42   AICc=2883.78   BIC=2894.54
coeftest(fitarima1)
## 
## z test of coefficients:
## 
##       Estimate Std. Error z value  Pr(>|z|)    
## ma1  -0.366467   0.089045 -4.1155 3.863e-05 ***
## sma1 -0.823140   0.112562 -7.3128 2.617e-13 ***
## sma2  0.290750   0.150392  1.9333    0.0532 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(fitarima1)
## Series: salidas.ts.train 
## ARIMA(0,1,1)(0,1,2)[12] 
## 
## Coefficients:
##           ma1     sma1    sma2
##       -0.3665  -0.8231  0.2908
## s.e.   0.0890   0.1126  0.1504
## 
## sigma^2 estimated as 1.744e+09:  log likelihood=-1437.71
## AIC=2883.42   AICc=2883.78   BIC=2894.54
## 
## Training set error measures:
##                    ME     RMSE      MAE       MPE     MAPE      MASE       ACF1
## Training set 2775.622 39145.38 28595.76 0.1475052 2.184726 0.2538507 0.01535602

En este primer modelo los tres coeficientes son significativos. Sin embargo, hay que hacer la diagnosis a los residuales:

Autocorrelación:

checkresiduals(fitarima1)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(0,1,1)(0,1,2)[12]
## Q* = 23.542, df = 21, p-value = 0.3158
## 
## Model df: 3.   Total lags used: 24
autoplot(acf(fitarima1$residuals, plot = FALSE))

autoplot(pacf(fitarima1$residuals, plot = FALSE))

ggtsdiag(fitarima1)

La gráfica de los residuales no muestra tendencia lo cual es bueno y en los correlogramas solo un coeficiente sale de los límites. El p-value de la prueba Ljung-Box es mayor al 0.05, lo cual nos indica que los residuales no están correlacionados. Además, los p-valores para la prueba Q de Ljung-Box están por encima de 0,05, lo que indica “no significativo”.

Normalidad:

qqnorm(fitarima1$residuals)
qqline(fitarima1$residuals)

jarque.bera.test(fitarima1$residuals)
## 
##  Jarque Bera Test
## 
## data:  fitarima1$residuals
## X-squared = 3.6914, df = 2, p-value = 0.1579

El p-value es mayor al 0.05, por lo tanto, podemos afirmar que los residuales de este modelo siguen una distribución normal.

Este modelo pasa las pruebas de autocorrelación y normalidad, por lo que, podemos darlo como válido y es apto para hacer pronósticos:

Predicción y Evaluación:

fc.fitarima1 <- forecast(fitarima1, h = 12)
forecast::accuracy(fc.fitarima1, salidas.ts.test)
##                     ME     RMSE      MAE       MPE     MAPE      MASE
## Training set  2775.622 39145.38 28595.76 0.1475052 2.184726 0.2538507
## Test set     16397.681 38113.62 23194.48 0.7660115 1.089580 0.2059023
##                    ACF1 Theil's U
## Training set 0.01535602        NA
## Test set     0.47835744 0.2214405

El error de test es de 1.09, que es menor al mejor modelo ets que se generó anteriormente. Veamos los resultados gráficamente:

test_forecast(forecast.obj = fc.fitarima1, actual = salidas.ts, test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))
autoplot(fc.fitarima1) + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico auto.arima1 Salidas 2019", y = "Salidas")

Este será nuestro modelo arima base, ya que pasa las pruebas de autocorrelación y normalidad de sus residuales.

Veamos si podemos mejorarlo modificando algunos de sus parámetros:

fitarima2 <- auto.arima(salidas.ts.train, 
                        trace = T,
                        stepwise = T,
                        approximation = T,
                        test = "adf",
                        allowdrift = T,
                        optim.method ="BFGS"
                        )
## 
##  Fitting models using approximations to speed things up...
## 
##  ARIMA(2,1,2)(1,1,1)[12]                    : Inf
##  ARIMA(0,1,0)(0,1,0)[12]                    : 2661.918
##  ARIMA(1,1,0)(1,1,0)[12]                    : 2613.67
##  ARIMA(0,1,1)(0,1,1)[12]                    : 2603.277
##  ARIMA(0,1,1)(0,1,0)[12]                    : 2644.167
##  ARIMA(0,1,1)(1,1,1)[12]                    : 2603.142
##  ARIMA(0,1,1)(1,1,0)[12]                    : 2611.132
##  ARIMA(0,1,1)(2,1,1)[12]                    : 2577.624
##  ARIMA(0,1,1)(2,1,0)[12]                    : 2576.204
##  ARIMA(0,1,0)(2,1,0)[12]                    : 2586.307
##  ARIMA(1,1,1)(2,1,0)[12]                    : Inf
##  ARIMA(0,1,2)(2,1,0)[12]                    : 2578.272
##  ARIMA(1,1,0)(2,1,0)[12]                    : 2576.742
##  ARIMA(1,1,2)(2,1,0)[12]                    : Inf
## 
##  Now re-fitting the best model(s) without approximations...
## 
##  ARIMA(0,1,1)(2,1,0)[12]                    : 2875.321
## 
##  Best model: ARIMA(0,1,1)(2,1,0)[12]
coeftest(fitarima2)
## 
## z test of coefficients:
## 
##       Estimate Std. Error z value  Pr(>|z|)    
## ma1  -0.356835   0.089151 -4.0026 6.266e-05 ***
## sar1 -0.724047   0.083730 -8.6474 < 2.2e-16 ***
## sar2 -0.526351   0.087282 -6.0305 1.635e-09 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(fitarima2)
## Series: salidas.ts.train 
## ARIMA(0,1,1)(2,1,0)[12] 
## 
## Coefficients:
##           ma1     sar1     sar2
##       -0.3568  -0.7240  -0.5264
## s.e.   0.0892   0.0837   0.0873
## 
## sigma^2 estimated as 1.591e+09:  log likelihood=-1433.48
## AIC=2874.97   AICc=2875.32   BIC=2886.09
## 
## Training set error measures:
##                    ME     RMSE      MAE       MPE     MAPE      MASE       ACF1
## Training set 3023.755 37392.03 27177.97 0.1593727 2.080588 0.2412646 0.01732966

En esta corrida el mejor modelo fue un ARIMA(0,1,1)(2,1,0), el cual tiene todos los coeficientes significativos. Analicemos sus residuales.

Autocorrelación:

checkresiduals(fitarima2)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(0,1,1)(2,1,0)[12]
## Q* = 15.748, df = 21, p-value = 0.7836
## 
## Model df: 3.   Total lags used: 24
autoplot(acf(fitarima2$residuals, plot = FALSE))

autoplot(pacf(fitarima2$residuals, plot = FALSE))

ggtsdiag(fitarima2)

El p-value de la prueba Ljung-Box es mayor al 0.05, por lo tanto, los residuales no están correlacionados, además, en su gráfica no se aprecia ningún patrón. Todos los coeficiente están dentro de las bandas en los correlogramas. Los p-valores para la prueba Q de Ljung-Box están por encima de 0,05, lo que indica “no significativo”.

Normalidad:

qqnorm(fitarima2$residuals)
qqline(fitarima2$residuals)

jarque.bera.test(fitarima2$residuals)
## 
##  Jarque Bera Test
## 
## data:  fitarima2$residuals
## X-squared = 5.3535, df = 2, p-value = 0.06879

Los residuales no presentan autocorrelación y se distribuyen normalmente. por tanto, pasamos a hacer las predicciones para conocer el error de test:

Predicción y Evaluación:

fc.fitarima2 <- forecast(fitarima2, h = 12)
forecast::accuracy(fc.fitarima2, salidas.ts.test)
##                    ME     RMSE      MAE       MPE     MAPE      MASE       ACF1
## Training set 3023.755 37392.03 27177.97 0.1593727 2.080588 0.2412646 0.01732966
## Test set     7730.809 45468.44 31736.19 0.3480207 1.495838 0.2817289 0.51810977
##              Theil's U
## Training set        NA
## Test set     0.2636668

Arroja un error de 1.49, ligeramente mayor que el modelo anterior (1.08)

veamos las gráficas:

test_forecast(forecast.obj = fc.fitarima2, actual = salidas.ts, test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))
autoplot(fc.fitarima2) + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico auto.arima2 Salidas 2019", y = "Salidas")

Hagamos un último modelo arima, pero ahora usaremos la función arima, para construirlo por partes:

Primero recordemos la forma de los correlogramas, puesto que nos ayudarán a definir la estructura de modelo:

tsdisplay(salidas.ts.train)

Empecemos por aplicar una diferencia y ver de nuevo los correlogramas:

tsdisplay(diff(salidas.ts.train))

Ahora con 12 rezagos para ver la estructura estacional de la serie:

tsdisplay(diff(salidas.ts.train, lag = 12))

Con la información proporcionada por los correlogramas se creará un modelo arima que para la parte regular sera (2,1,2) y para la parte estacional (0,1,1):

fitarima3 <-  arima(log(salidas.ts.train), 
                  order = c(2,1,2), 
                  seasonal = list(order = c(0,1,1),
                                  period = 12),
                  method = "ML")


coeftest(fitarima3)
## 
## z test of coefficients:
## 
##       Estimate Std. Error  z value  Pr(>|z|)    
## ar1   0.631598   0.082481   7.6575 1.896e-14 ***
## ar2  -0.661005   0.075036  -8.8092 < 2.2e-16 ***
## ma1  -1.068908   0.040476 -26.4085 < 2.2e-16 ***
## ma2   0.999969   0.049311  20.2789 < 2.2e-16 ***
## sma1 -0.844861   0.122300  -6.9081 4.912e-12 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(fitarima3)
## 
## Call:
## arima(x = log(salidas.ts.train), order = c(2, 1, 2), seasonal = list(order = c(0, 
##     1, 1), period = 12), method = "ML")
## 
## Coefficients:
##          ar1     ar2      ma1     ma2     sma1
##       0.6316  -0.661  -1.0689  1.0000  -0.8449
## s.e.  0.0825   0.075   0.0405  0.0493   0.1223
## 
## sigma^2 estimated as 0.0008767:  log likelihood = 238.08,  aic = -464.15
## 
## Training set error measures:
##                       ME       RMSE        MAE        MPE      MAPE      MASE
## Training set 0.002089955 0.02844674 0.02053924 0.01471124 0.1458633 0.2158159
##                     ACF1
## Training set 0.003126655

Todos los coeficiente de este modelo son significativos. Ahora se analizan los residuales.

Autocorrelación:

checkresiduals(fitarima3)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(2,1,2)(0,1,1)[12]
## Q* = 28.18, df = 19, p-value = 0.08003
## 
## Model df: 5.   Total lags used: 24
autoplot(acf(fitarima3$residuals, plot = FALSE))

autoplot(pacf(fitarima3$residuals, plot = FALSE))

ggtsdiag(fitarima3)

Este modelo pasa la prueba de Ljung-Box al tener un p-valor mayor al 0.05, además, la gráfica de los residuales no muestran ningún patrón o tendencia. En los correlogramas solo un coeficiente sale de la banda superior. Los p-valores para la prueba Q de Ljung-Box están por encima de 0,05, lo que indica “no significativo”.

Normalidad:

qqnorm(fitarima3$residuals)
qqline(fitarima3$residuals)

jarque.bera.test(fitarima3$residuals)
## 
##  Jarque Bera Test
## 
## data:  fitarima3$residuals
## X-squared = 58.79, df = 2, p-value = 1.713e-13

Los residuales no presentan autocorrelación pero no se distribuyen normalmente. Pasamos a hacer las predicciones para conocer el error de test:

Predicción y Evaluación:

fc.fitarima3 <- forecast(fitarima3, h = 12)

# se invierten los logaritmos
fc.fitarima3$mean <- exp(fc.fitarima3$mean)
fc.fitarima3$fitted <- exp(fc.fitarima3$fitted)
fc.fitarima3$x <- exp(fc.fitarima3$x)

forecast::accuracy(fc.fitarima3, salidas.ts.test)
##                     ME     RMSE      MAE      MPE     MAPE      MASE
## Training set  2282.009 38588.06 28080.81 0.168258 2.057594 0.2492793
## Test set     41325.189 76980.19 59010.25 2.012655 2.784381 0.5238465
##                     ACF1 Theil's U
## Training set -0.04429377        NA
## Test set      0.35523522 0.4472975

Arroja un error de 2.78, mayor que el mejor modelo arima (1.08)

veamos las gráficas:

test_forecast(forecast.obj = fc.fitarima3, actual = salidas.ts, test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))
fc.fitarima3$lower[,1:2] <- exp(fc.fitarima3$lower[,1:2])
fc.fitarima3$upper[,1:2] <- exp(fc.fitarima3$upper[,1:2])

autoplot(fc.fitarima3) + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico arima3 Salidas 2019", y = "Salidas")

Tenemos tres modelos arima los cuales todos pasaron la prueba de autocorrelación en los residuales y solo el modelo arima 3 no pasa la prueba de normalidad. El modelo que presenta el menor error es el 1 con un MAPE de 1.08. Descartaremos el modelo 3 ya que sus residuales no se distribuyen normalmente.

De estos dos modelos elegiremos el modelo 1, porque generó el menor error de test, además, ambos modelos tienen 3 coeficientes, por lo que, no podemos basarnos en el principio de parsimonia (el más simple).

TSLM: Time Series Linear Model

Se generará el modelo tslm que está diseñado para ajustar modelos lineales con datos de series de tiempo y en el cual se pueden incluir variables predictoras, en particular la tendencia y la estacionalidad.

salidas.lm  <- tslm(salidas.ts.train ~  trend + season, lambda = "auto")

summary(salidas.lm)
## 
## Call:
## tslm(formula = salidas.ts.train ~ trend + season, lambda = "auto")
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -37.666  -9.218  -2.374   7.687  47.286 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 489.00480    5.40367  90.495  < 2e-16 ***
## trend         1.37152    0.03729  36.782  < 2e-16 ***
## season2     -26.73830    6.93206  -3.857 0.000187 ***
## season3      13.96798    6.93236   2.015 0.046170 *  
## season4       4.40207    6.93286   0.635 0.526675    
## season5      10.97165    6.93356   1.582 0.116213    
## season6       6.50020    6.93446   0.937 0.350464    
## season7      45.70716    6.93556   6.590 1.26e-09 ***
## season8      23.90810    6.93687   3.447 0.000785 ***
## season9     -11.49589    6.93837  -1.657 0.100183    
## season10      5.29325    6.94007   0.763 0.447147    
## season11      5.73127    6.94198   0.826 0.410685    
## season12     24.57878    6.94408   3.540 0.000573 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 16.26 on 119 degrees of freedom
## Multiple R-squared:  0.9285, Adjusted R-squared:  0.9213 
## F-statistic: 128.8 on 12 and 119 DF,  p-value: < 2.2e-16
salidas.fcast.lm <- forecast(salidas.lm, h = 12)

test_forecast(forecast.obj = salidas.fcast.lm, actual = salidas.ts, test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))
salidas.fcast.lm$x <- salidas.ts.train
forecast::accuracy(salidas.fcast.lm, salidas.ts.test)
##                     ME     RMSE      MAE        MPE     MAPE      MASE
## Training set  2087.944 82526.70 67371.24 -0.3447535 5.501881 0.5980688
## Test set     42027.132 59494.84 45237.35  2.0010454 2.138258 0.4015816
##                   ACF1 Theil's U
## Training set 0.8867416        NA
## Test set     0.3842807 0.3403756
autoplot(salidas.fcast.lm) + labs(title = "Pronóstico Modelo tslm1 Salidas 2019")

Este primer modelo generó un MAPE de 2.13. Haremos un segundo modelo pero ahora incluyendo la estacionalidad con variables dicotómicas:

# Usando variables dummies para modelar la estacionalidad
meses <- seasonaldummy(salidas.ts.train)

salidas.lm2  <- tslm(salidas.ts.train ~ meses + trend + season)

tsdisplay(residuals(salidas.lm2))

salidas.fcast2 <- forecast(salidas.lm2,
    data.frame(meses = I(seasonaldummy(salidas.ts.train, 12))))

test_forecast(forecast.obj = salidas.fcast2, actual = salidas.ts, test = salidas.ts.test) %>% 
  layout(legend = list(x = 0.1, y = 0.95))
autoplot(salidas.fcast2) + labs(title = "Pronóstico modelo tslm2 Salidas 2019")

forecast::accuracy(salidas.fcast2, salidas.ts.test)
##                         ME     RMSE      MAE        MPE     MAPE     MASE
## Training set -2.468953e-11 99422.98 81301.20 -0.4033314 6.569292 0.721728
## Test set      8.681211e+04 98467.53 88712.75  4.0379108 4.146165 0.787522
##                   ACF1 Theil's U
## Training set 0.8921495        NA
## Test set     0.5237033 0.5636143

El segundo modelo tslm no mejoró el error de test, ya que este tipo de algoritmo no es adecuado para el tipo de datos que tenemos. Por tanto, probaremos un último modelo para ver si podemos mejorar el error del modelo arima1, que es hasta el momento el que ha generado el menor error de test.

PROPHET

Será el modelo Prophet,creado por Facebook para hacer sus pronósticos comerciales, a ver qué resultados nos genera.

Antes de iniciar con el modelo, es preciso aclarar que este modelo necesita que las columnas donde están las fechas y la variable a pronosticar tengan determinados nombres, por lo que, antes que nada, se procede a renombrar dichas columnas:

# Se verifica el tipo Fecha para el tiempo y se renombran las columnas: "ds" y "y"
df <- salidas

cleaned_df <- df %>% 
  rename(ds = Fecha,
         y = Salidas) %>% 
  mutate(ds = as.Date(ds, "%d/%m/%Y"))

cleaned_df %>% head()

Se establece la partición de los datos en train y test:

correct_split <- initial_time_split(cleaned_df %>% arrange(ds), prop = 1-(12/144))
correct_split
## <Analysis/Assess/Total>
## <132/12/144>
cleaned_train <- cleaned_df %>%
  filter(ds <"2019-01-01")

cleaned_test <- cleaned_df %>%
  filter(ds >="2019-01-01")

# se visualiza la división
bind_rows(
  training(correct_split) %>% mutate(type = "train"),
  testing(correct_split) %>% mutate(type = "test")
) %>% 
  ggplot(aes(x = ds, y= y, color = type, group = NA)) + 
  geom_line() + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Split", y = "Salidas")

Se crea el modelo y se hace la predicción:

m <- prophet(df = cleaned_train %>% arrange(ds), yearly.seasonality = T)

future <- make_future_dataframe(m, periods = 12, freq = "month", include_history = TRUE)

forecast <- predict(m, future)

Se grafica la serie con el pronostico:

plot(m, forecast) + 
  add_changepoints_to_plot(m) +
  labs(title = "Modelo Prophet: Pronóstico a 12 meses")

Veamos otra vista de las predicciones, en cuántos meses quedó por debajo y por arriba del valor real:

# se valida el set de test
s <- forecast %>% 
  inner_join(cleaned_test, by = "ds") %>% 
  mutate(
    dif = (y-yhat), 
    esPositivo = dif >= 0)

s %>% 
  ggplot(aes(x = ds, y = dif, fill = esPositivo)) + 
  geom_col(position = "identity") +
  scale_fill_manual(values = c("firebrick4", "dodgerblue4")) +
  scale_y_continuous(labels = scales::comma) +
  theme_minimal() + 
  labs(title = "Diferencia Entre Salidas Esperadas y Actuales",
          subtitle = "Las Salidas pronosticadas quedaron por debajo durante la mayor parte del año", y = "Diferencia", x = "Año")

En el siguiente gráfico de dispersión, se compara el ajuste de las observaciones pronosticadas contra las observaciones reales. Se aprecia como, al igual que en los demás modelos, el pronóstico del mes de febrero quedó muy por debajo del valor real. En los demás meses los pronósticos se ajustaron relativamente bien.

qplot(s$y, s$yhat, log = "xy") +
  geom_abline() +
  scale_y_continuous(labels = scales::comma) +
  scale_x_continuous(labels = scales::comma) +
  labs(title = "Comparando las Observaciones Esperadas vs Reales", 
       y = "Salidas Esperadas",
       x = "Salidas Reales") +
  theme_minimal()

Se revisan los errores:

mape <- function(y, yhat) {
  return(mean(abs(y - yhat)/ y)*100)
}

eval <- cleaned_test %>% 
  left_join(forecast, by = "ds") %>% 
  select(ds, y, yhat, yhat_upper, yhat_lower)

mape.prophet1 <- mape(eval$y, eval$yhat)

cat("MAPE del modelo Prophet 1: ",`mape.prophet1`)

MAPE del modelo Prophet 1: 2.181772

Se grafica el pronóstico:

plot(m, forecast) +
  add_changepoints_to_plot(m) +
  geom_point(cleaned_test, mapping = aes(as.POSIXct(ds), y), col = "red") +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico Modelo Prophet 1", y = "Salidas", x = "Año")

Vemos que este modelo también subestima el mes de febrero, por lo que, vamos a probar la técnica de validación cruzada para crear modelos aditivos y multiplicativos para ver cuáles generan el menor error de generalización:

Se creann los k-folds:

timeseries_k_folds <- sliding_period(cleaned_df %>% arrange(ds), 
                                     ds,
                                     period = "year",
                                     lookback = Inf,
                                     assess_stop = 1)

Con apoyo de una función se crearán los diferentes tipos de modelos:

tune_prophet <- function(splits){
  train_data <- analysis(splits)
  test_data <- assessment(splits)
  
  m1 <- prophet(df = train_data, seasonality.mode = "additive")
  m2 <- prophet(df = train_data, seasonality.mode = "multiplicative")
  
  future <- make_future_dataframe(m1, 
                                  periods = nrow(test_data), 
                                  freq = "month",
                                  include_history = FALSE)
  
  
  bind_rows(
    predict(m1, future) %>% select(ds, yhat) %>% mutate(type = "additive"),
    predict(m2, future) %>% select(ds, yhat) %>% mutate(type = "multiplicative")
  ) %>% left_join(test_data, by = "ds")
}

Se muestran los forecast, su tipo y métrica de error:

# se hacen los k-folds
ts_tune <- timeseries_k_folds %>% 
  slice_tail(n = 10) %>%
  mutate(res = map(splits, tune_prophet))
# errores
ts_tune %>% 
  select(id, res) %>% 
  unnest(res) %>% 
  group_by(id, type) %>% 
  arrange(ds) %>% 
  mutate(forecast = paste0("forecast_", row_number())) %>% 
  ungroup() %>% 
  select(forecast, type, ds, yhat, y) %>% 
  group_by(forecast, type) %>% 
  yardstick::mape(truth = y, estimate = yhat) %>% 
  ungroup() %>% 
  group_by(forecast) %>% 
  slice_min(.estimate) %>% 
  ungroup()

Veamos la mediana de los errores (MASE) en los modelos aditivos y multiplicativos, pero comparando los pronósticos con un modelo ingenuo (naive):

ts_tune %>% 
  select(id, res) %>% 
  unnest(res) %>% 
  group_by(id, type) %>% 
  arrange(ds) %>% 
  mutate(forecast = paste0("forecast_", row_number())) %>% 
  ungroup() %>% 
  select(forecast, type, ds, yhat, y) %>% 
  left_join(cleaned_df %>% 
              mutate(naive = lag(y, n = 1, order_by = ds)) %>% 
              drop_na() %>% 
              select(ds, naive),
            by =  "ds") %>% 
  group_by(forecast, type) %>% 
  summarise(mase = mean(abs(yhat - y)) / mean(abs(naive - y))) %>% 
  ungroup() %>% 
  group_by(type) %>% 
  summarise(median = median(mase))
ts_tune %>% 
  select(id, res) %>% 
  unnest(res) %>% 
  group_by(id, type) %>% 
  arrange(ds) %>% 
  mutate(forecast = paste0("forecast_", row_number())) %>% 
  ungroup() %>% 
  select(id, forecast, type, ds, yhat, y) %>% 
  group_by(id, type) %>% 
  mutate(naive_date = min(ds) - months(1)) %>% 
  ungroup() %>% 
  left_join(cleaned_df %>% 
              mutate(naive = lag(y, n = 1, order_by = ds)) %>% 
              drop_na() %>% 
              select(ds, naive),
            by =  c("naive_date" = "ds")) %>% 
  group_by(forecast, type) %>% 
  summarise(mase = mean(abs(yhat - y)) / mean(abs(naive - y))) %>% 
  ungroup() %>% 
  group_by(type) %>% 
  summarise(median = median(mase))

En ambos casos el menor error se presenta en el tipo multiplicativo, por lo que, crearemos un segundo modelo pero ahora con el tipo de estacionalidad multiplicativa:

m2 <- prophet(df = cleaned_train %>% arrange(ds),
              yearly.seasonality = T, 
              seasonality.mode = "multiplicative")

future2 <- make_future_dataframe(m2, periods = 12, freq = "month", include_history = TRUE)

forecast2 <- predict(m2, future2)

Se grafica la serie con el pronostico:

plot(m2, forecast2) + 
  add_changepoints_to_plot(m2) +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Modelo Prophet 2: Pronóstico a 12 meses", y = "Salidas", x = "Año")

Volvamos a ver cómo quedo el pronóstico, o sea, en cuántos meses quedó por debajo y por arriba del valor real:

# se valida el set de test
s2 <- forecast2 %>% 
  inner_join(cleaned_test, by = "ds") %>% 
  mutate(
    dif = (y-yhat), 
    esPositivo = dif >= 0)

s2 %>% 
  ggplot(aes(x = ds, y = dif, fill = esPositivo)) + 
  geom_col(position = "identity") +
  scale_fill_manual(values = c("firebrick4", "dodgerblue4")) +
  scale_y_continuous(labels = scales::comma) +
  theme_minimal() + 
  labs(title = "Diferencia Entre Salidas Esperadas y Actuales",
          subtitle = "Las Salidas pronosticadas quedaron por debajo durante la mayor parte del año", y = "Diferencia", x = "Año")

En la gráfica anterior, ya se aprecian los datos para los primeros meses más cercanos a las observaciones reales, sin embargo, este modelo subestimó el mes de julio. Veamos la gráfica de dispersión de las observaciones esperadas y reales:

qplot(s2$y, s2$yhat, log = "xy") +
  geom_abline() +
  scale_y_continuous(labels = scales::comma) +
  scale_x_continuous(labels = scales::comma) +
  labs(title = "Comparando las Observaciones Esperadas vs Reales", 
       y = "Salidas Esperadas",
       x = "Salidas Reales") +
  theme_minimal()

Se aprecia que las predicciones se ajustan mejor a los datos reales.

Se revisan los errores:

mape <- function(y, yhat) {
  return(mean(abs(y - yhat)/ y)*100)
}

eval2 <- cleaned_test %>% 
  left_join(forecast2, by = "ds") %>% 
  select(ds, y, yhat, yhat_upper, yhat_lower) 

mape.prophet.f <- mape(eval2$y, eval2$yhat)

cat("MAPE del modelo Prophet: ",`mape.prophet.f`)
## MAPE del modelo Prophet:  1.88846

Se grafica el pronóstico:

plot(m2, forecast2) +
  add_changepoints_to_plot(m2) +
  geom_point(cleaned_test, mapping = aes(as.POSIXct(ds), y), col = "red") +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico Modelo Prophet Multiplicativo", y = "Salidas", x = "Año")

Este modelo se ve y se ajusta mejor, además, el error de test bajó a 1.88, por lo que, nos quedamos con este segundo modelo multiplicativo.

Ensamble

Son tres los modelos que generaron un bajo error MAPE en el conjunto de prueba, por lo que, los vamos a combinar esperando obtener un modelo más robusto. Para hacer lo anterior, crearemos una función que haga el ensamble de los siguientes modelos:

Se crea la función:

ensamble <- function(df, n_pronosticos) {
  
  N <- nrow(df)
  n <- n_pronosticos
  fin <- N - n
  
  inicio.train <- df$Fecha[fin]
  inicio.test <- df$Fecha[fin]+1
  
  df_train <- df[1:fin,]
  df_train <- ts(df_train[,2], start = inicio.train, frequency = 12)
  
  df_test <- df[(fin + 1):N,] %>% ts(start = inicio.test, frequency = 12)
  df_test <- ts(df_test[,2], start = inicio.test, frequency = 12)
  
  h <- n
  
  fit.ets <- ets(df_train,
                model = "ZZZ",
                damped = T,
                additive.only = T,
                lambda = "auto",
                alpha = alpha.min$alpha
                )
  prediccion.ets <- forecast(fit.ets, h = h)$mean %>% as.vector()
  
  
  arima <- auto.arima(df_train)
  prediccion.arima <- forecast(arima, h = h)$mean %>% as.vector()
  
  
  
  cleaned_train <- df[1:fin,] %>% 
    rename(ds = Fecha,
         y = Salidas) %>% 
  mutate(ds = as.Date(ds, "%d/%m/%Y"))

  cleaned_test <- df[(fin + 1):N,] %>% 
    rename(ds = Fecha,
         y = Salidas) %>% 
  mutate(ds = as.Date(ds, "%d/%m/%Y"))
  
  m.prophet <- prophet(df = cleaned_train %>% arrange(ds),
              yearly.seasonality = T, 
              seasonality.mode = "multiplicative")

  future <- make_future_dataframe(m.prophet, 
                                   periods = h,
                                   freq = "month",
                                   include_history = TRUE)

  fc.prophet <- predict(m.prophet, future)

  prediccion.prophet <- cleaned_test %>% 
  left_join(fc.prophet, by = "ds") %>% 
  select(yhat) %>% as.vector()
  
  
  
  prediccion.ensamble <- (prediccion.ets + prediccion.arima + prediccion.prophet)/3
  
  return(data_frame(Fecha = cleaned_test$ds,
                    Actual = df_test %>% as.vector(), 
                    Prediccion = prediccion.ensamble %>% as.vector() %>% round(0), 
                    Error = (Prediccion - Actual) %>% as.vector(),
                    Error_Porcentual = round((Error / Actual)*100, 2)))
} 

Veamos los resultados del ensamble:

# Usamos la función: 
df_ensamble <- ensamble(salidas, 12)

df_ensamble$Prediccion <- df_ensamble$Prediccion$yhat
df_ensamble$Error <- df_ensamble$Error$yhat


df_ensamble %>% kbl(align = "c",
                    digits = 2 , 
                    format.args = list(big.mark = ",")) %>%
  kable_paper("hover", full_width = F)
Fecha Actual Prediccion Error Error_Porcentual
2019-01-01 1,956,707 1,952,676 -4,031 -0.21
2019-02-01 1,755,724 1,763,365 7,641 0.44
2019-03-01 2,072,594 2,080,001 7,407 0.36
2019-04-01 2,091,546 2,034,959 -56,587 -2.71
2019-05-01 2,190,456 2,096,812 -93,644 -4.28
2019-06-01 2,121,607 2,066,573 -55,034 -2.59
2019-07-01 2,339,595 2,397,076 57,481 2.46
2019-08-01 2,207,163 2,231,714 24,551 1.11
2019-09-01 1,962,079 1,954,227 -7,852 -0.40
2019-10-01 2,102,570 2,102,308 -262 -0.01
2019-11-01 2,124,365 2,123,315 -1,050 -0.05
2019-12-01 2,254,497 2,275,157 20,660 0.92

Los pronósticos se acercan a los datos reales de test, que corresponden al año 2019, sin embargo, el mes de mayo quedó subestimado y, por el contrario, el mes de julio quedó sobreestimado esto es debido a la influencia del modelo Prophet. Ahora, comparemos los errores de los tres modelos con el del ensamble para ver si valio la pena aplicar esta técnica.

# Se crea una function que calcule algunas medidas de evaluación: 
eval_medidas <- function(df_modelo) {
  
  act <- df_modelo %>% pull(Actual)
  pred <- df_modelo %>% pull(Prediccion)
  err <- act - pred 
  per_err <- abs(err / act)
  
  # Mean Absolute Error (MAE): 
  mae <- err %>% abs() %>% mean()
  
  # Mean Squared Error (MSE): 
  mse <- mean(err^2)
  
  # Mean Absolute Percentage Error (MAPE): 
  mape <- mean(per_err)*100
  
  # Retorna los resultadoss: 
  return(data_frame(MAE = mae, MSE = mse, MAPE = mape, N = length(act)))
}

# Data Frame ets: 
df_ets <- data_frame(Actual = salidas.ts.test %>% as.vector(), 
                       Prediccion = fc.ets$mean %>% as.vector() %>% round(0), 
                       Error = Prediccion - Actual,
                       Error_Porcentual = round(Error / Actual, 2))

# Data Frame arima: 
df_arima <- data_frame(Actual = salidas.ts.test %>% as.vector(), 
                       Prediccion = fc.fitarima1$mean %>% as.vector() %>% round(0), 
                       Error = Prediccion - Actual,
                       Error_Porcentual = round(Error / Actual, 2))

# Data Frame prophet: 
df_prophet <- data_frame(Actual = salidas.ts.test %>% as.vector(), 
                       Prediccion = s2$yhat %>% as.vector() %>% round(0), 
                       Error = Prediccion - Actual,
                       Error_Porcentual = round(Error / Actual, 2))

# Comparando los tres modelos: 

bind_rows(df_ets %>% eval_medidas(), 
          df_arima %>% eval_medidas(), 
          df_prophet %>% eval_medidas(),
          df_ensamble %>% eval_medidas) %>%
  mutate(MODELO = c("ETS", "ARIMA", "PROPHET", "ENSAMBLE")) %>% 
  select(MODELO, everything()) %>% 
  mutate_if(is.numeric, function(x) {round(x, 2)}) %>%
  arrange(desc(-MAPE)) %>% 
  kbl(align = "c",
                    digits = 2 , 
                    format.args = list(big.mark = ",")) %>%
  kable_paper("hover", full_width = F)
MODELO MAE MSE MAPE N
ARIMA 23,194.50 1,452,653,203 1.09 12
ENSAMBLE 28,016.67 1,627,166,964 1.29 12
ETS 32,768.83 2,093,607,703 1.54 12
PROPHET 41,345.83 2,639,896,159 1.89 12

 

Después de hacer el ensamble de los tres mejores modelos a final de cuentas no se pudo mejorar el MAPE del mejor modelo, el arima, lo anterior, no es de sorprender, pues el ensamble se recomienda cuando los modelos que se juntarán presentan diferencias entre ellos, lo que en este caso no ocurrió.

Modelo final

 

Por lo anterior, nos quedamos con el modelo arima, que tuvo un MAPE de test de 1.09%.

Vamos a pronosticar con ese modelo el primer bimestre de 2020, ya que esos meses no están afectados por la pandemia en México, ya que el confinamiento empezó a finales de marzo:

# se cargan los datos
salidas.2020.C <- read.csv("Salidas_Nacionales_C.csv")
salidas.2020.C.ts <- ts(salidas.2020.C[,2], 
                      start = c(2008, 1),
                      end = c(2020, 2),
                      frequency = 12)

ts_info(salidas.2020.C.ts)
##  The salidas.2020.C.ts series is a ts object with 1 variable and 146 observations
##  Frequency: 12 
##  Start time: 2008 1 
##  End time: 2020 2
# Outliers
tsoutliers(salidas.2020.C.ts)
## $index
## [1] 14 17
## 
## $replacements
## [1]  874902.5 1039538.2
salidas.2020.C.ts <- tsclean(salidas.2020.C.ts)


# data real a pronosticar
salidas.2020 <- read.csv("Salidas_Nacionales_2020.csv")

salidas.2020.ts <- ts(salidas.2020[,2], 
                      start = c(2020, 1),
                      end = c(2020, 2),
                      frequency = 12)

ts_info(salidas.2020.ts)
##  The salidas.2020.ts series is a ts object with 1 variable and 2 observations
##  Frequency: 12 
##  Start time: 2020 1 
##  End time: 2020 2

Se crea el modelo:

fitarima.final <- auto.arima(salidas.ts)
fitarima.final
## Series: salidas.ts 
## ARIMA(0,1,1)(2,1,0)[12] 
## 
## Coefficients:
##           ma1     sar1     sar2
##       -0.3451  -0.7327  -0.4491
## s.e.   0.0917   0.0813   0.0844
## 
## sigma^2 estimated as 1.645e+09:  log likelihood=-1578.88
## AIC=3165.75   AICc=3166.07   BIC=3177.25
coeftest(fitarima.final)
## 
## z test of coefficients:
## 
##       Estimate Std. Error z value  Pr(>|z|)    
## ma1  -0.345105   0.091695 -3.7636 0.0001675 ***
## sar1 -0.732749   0.081267 -9.0165 < 2.2e-16 ***
## sar2 -0.449059   0.084408 -5.3201 1.037e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(fitarima.final)
## Series: salidas.ts 
## ARIMA(0,1,1)(2,1,0)[12] 
## 
## Coefficients:
##           ma1     sar1     sar2
##       -0.3451  -0.7327  -0.4491
## s.e.   0.0917   0.0813   0.0844
## 
## sigma^2 estimated as 1.645e+09:  log likelihood=-1578.88
## AIC=3165.75   AICc=3166.07   BIC=3177.25
## 
## Training set error measures:
##                    ME     RMSE      MAE       MPE     MAPE      MASE       ACF1
## Training set 2409.473 38240.05 27829.13 0.1233916 2.058852 0.2475824 0.04054564

Análisis de los residuales:

checkresiduals(fitarima.final)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(0,1,1)(2,1,0)[12]
## Q* = 14.857, df = 21, p-value = 0.83
## 
## Model df: 3.   Total lags used: 24

Análisis de la normalidad:

qqnorm(fitarima.final$residuals)
qqline(fitarima.final$residuals)

jarque.bera.test(fitarima.final$residuals)
## 
##  Jarque Bera Test
## 
## data:  fitarima.final$residuals
## X-squared = 4.6355, df = 2, p-value = 0.09849
ggtsdiag(fitarima.final)

Este modelo final no presenta autocorrelación en sus residuales, ya que su gráfica no muestra tendencia y sí pasa el test de Ljung-Boxy, además, éstos se distribuyen normalmente. En su correlograma los coeficientes no rebasan los límites. Por lo anterior, el modelo es apto para pronosticar.

Se hace la predicción de los próximos 2 meses:

fc.fitarima.final <- forecast(fitarima.final, h = 2)
forecast::accuracy(fc.fitarima.final, salidas.2020.ts)
##                    ME     RMSE       MAE       MPE     MAPE       MASE
## Training set 2409.473 38240.05 27829.128 0.1233916 2.058852 0.24758238
## Test set     8773.898 13153.31  9799.399 0.4636181 0.512745 0.08718054
##                     ACF1 Theil's U
## Training set  0.04054564        NA
## Test set     -0.50000000 0.1003116
autoplot(fc.fitarima.final) + 
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Pronóstico Arima Final: Salidas 2020", y = "Salidas")

test_forecast(forecast.obj = fc.fitarima.final, 
              actual = salidas.2020.C.ts,
              test = salidas.2020.ts) %>%
  layout(legend = list(x = 0.1, y = 0.95))

El error MAPE del modelo es: 0.512745. Hay que tener presente que los argumentos de este modelo son diferentes al modelo arima1 creado anteriormente, esto debido a que para su construcción se utilizó toda la serie histórica, desde enero de 2008 hasta diciembre de 2019, por lo tanto, ya no evaluamos el modelo con un conjunto de test, sino que directamente lo evaluamos sobre los datos reales de enero-febrero de 2020 (que son las observaciones que se pronosticaron).

# Data Frame arima final: 
data_frame(Fecha = salidas.2020$Fecha %>% as.vector(),
           Actual = salidas.2020.ts %>% as.vector(), 
           Prediccion = fc.fitarima.final$mean %>% as.vector() %>% round(0), 
           Error = abs(Prediccion - Actual),
           Error_Porcentual = round(Error / Actual, 2)) %>% 
  kbl(caption = "PRONÓSTICO ENERO-FEBRERO 2020", align = "c",
                    digits = 4 , 
                    format.args = list(big.mark = ",")) %>%
  kable_paper("hover", full_width = F)
PRONÓSTICO ENERO-FEBRERO 2020
Fecha Actual Prediccion Error Error_Porcentual
01/01/2020 2,087,450 2,088,476 1,026 0.00
01/02/2020 1,902,294 1,883,721 18,573 0.01

Conclusiones

La serie presentó cambio de tendencia en el año 2011 debido a diversos factores coyunturales que se presentaron a lo largo del periodo 2008-2011, como la crisis financiera de 2009, la pandemia de Influenza H1N1 y la quiebra de la empresa Mexinana de Aviación.

Se detectaron patrones estacionales en los meses de julio, agosto y diciembre (periodo de alta demanda) y en enero, febrero y septiembre (periodo de baja demanda).

Cuando se vaya a trabajar con series de tiempo es de suma importancia el hacer primero un análisis visual a la serie o series, ya que eso permitirá conocer si hay presencia de tendencia, su tipo y si existe sospecha de algún patrón estacional. Lo anterior, permitirá identificar el tipo de algoritmo a usar, ya que hay algunos que no están diseñados para modelar más de un patrón estacional como los de suavisado exponencial o arima.

Es altamente recomendable hacer un primer modelo que será la base a partir del cual se tratará de mejorarlo de acuerdo con la métrica elegida para su evaluación y, además, no olvidar que la evaluación se debe de hacer en la parte de prueba, es decir, sobre el conjunto de datos que el algoritmo no ha visto.

Cuando se opte por hacer un ensamble de modelos se debe tener en cuenta que esta estratégia funcionará mejor si los modelos a juntar presentan diferencias marcadas entre sí, porque de lo contrario se puede generar un peor resultado.

LS0tDQp0aXRsZTogIlByb27Ds3N0aWNvIGRlIENsaWVudGVzIGVuIGVsIEFlcm9wdWVydG8gSW50ZXJuYWNpb25hbCBkZSBsYSBDaXVkYWQgZGUgTcOpeGljbyINCnN1YnRpdGxlOiAiUHJveWVjdG86IFByb27Ds3N0aWNvIGRlIHVuYSBTZXJpZSBkZSBUaWVtcG8iDQphdXRob3I6ICJBbGVqYW5kcm8gUm9qYXMgTW9yZW5vIg0KZGF0ZTogIjYvMTAvMjAyMSINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICBjb2RlX2ZvbGRpbmc6IHNob3cNCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogICAgdGhlbWU6IHNwYWNlbGFiDQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiBmYWxzZQ0KZWRpdG9yX29wdGlvbnM6DQogIGNodW5rX291dHB1dF90eXBlOiBjb25zb2xlDQotLS0NCg0KJm5ic3A7DQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5QcmVzZW50YWNpw7NuPC9zcGFuPg0KJm5ic3A7DQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRW4gZXN0ZSB0cmFiYWpvIHNlIGRlc2Fycm9sbGFyw6EgdW4gcHJveWVjdG8gZGUgYW5hbMOtdGljYSByZWxhY2lvbmFkbyBjb24gZWwgbW9kZWxhZG8gZGUgdW5hIHNlcmllIGRlIHRpZW1wby4gRXN0ZSB0aXBvIGRlIHByb3llY3RvcyBlcyBtdXkgY29tw7puIGVuIHByw6FjdGljYW1lbnRlIHRvZGFzIGxhcyBlbXByZXNhcywgcHVlcyBlbiBzdSBQbGFuIGRlIE5lZ29jaW9zIGFudWFsLCBlcyBkb25kZSBzZSBlc3BlY2lmaWNhbiBsb3Mgb2JqZXRpdm9zIHkgbG9zIGVzY2VuYXJpb3MgdGFudG8gb3BlcmF0aXZvcyBjb21vIGZpbmFuY2llcm9zIHkgw6lzdG9zIHNlIGNvbnN0cnV5ZW4gYSBwYXJ0aXIgZGUgcHJvbsOzc3RpY29zLg0KDQpFbCB0cmFiYWpvIGNvbnNpc3RpcsOhIGVuIHByb25vc3RpY2FyIGxhIGNhbnRpZGFkIG1lbnN1YWwgZGUgY2xpZW50ZXMsIHBhcmEgZWwgYcOxbyAyMDE5LCBkZWwgQWVyb3B1ZXJ0byBJbnRlcm5hY2lvbmFsIGRlIGxhIENpdWRhZCBkZSBNw6l4aWNvIChBSUNNKSwgZW4gb3RyYXMgcGFsYWJyYXMsIHNlIGhhcsOhIHVuYSBwcmVkaWNjacOzbiBkZSBsb3MgcGFzYWplcm9zIHF1ZSBhYm9yZGFyb24gdnVlbG9zIG5hY2lvbmFsZXMgbcOhcyB2dWVsb3MgaW50ZXJuYWNpb25hbGVzLCBwZXJvIHNvbG8gbGFzIHNhbGlkYXMuIEVsIHJhbmdvIGhpc3TDs3JpY28gZGUgZGF0b3MgYWJhcmNhIGRlc2RlIGVsIGHDsW8gMjAwOCBhIDIwMTkgeSBhdW5xdWUgc2UgZGlzcG9uZSBkZSBpbmZvcm1hY2nDs24gbcOhcyBhY3R1YWwsIHBvciBjdWVzdGlvbmVzIHJlbGFjaW9uYWRhcyBjb24gbGEgcGFuZGVtaWEgZGUgQ29pdmQtMTksIHNlIGRlY2lkacOzIGhhY2VyIGVsIHBvbsOzc3RpY28gc29sbyBwYXJhIDIwMTksIHlhIHF1ZSBlc3RvIHBlcm1pdGlyw6EgbWVkaXIgbGEgcHJlY2lzacOzbiBkZWwgbW9kZWxvIHNpbiBlbCBpbXBhY3RvIGRlIGRpY2hhIHBhbmRlbWlhLiBQb3N0ZXJpb3JtZW50ZSwgeWEgY29uIGVsIG1vZGVsbyBmaW5hbCwgc2UgaGFyw6EgbGEgcHJlZGljY2nDs24gcGFyYSBsb3MgbWVzZXMgZGUgZW5lcm8geSBmZWJyZXJvIGRlIDIwMjAsIHlhIHF1ZSBlbiBNw6l4aWNvIGVsIGNvbmZpbmFtaWVudG8gZW1wZXrDsyBhIGZpbmFsZXMgZGUgbWFyem8uDQo8L2Rpdj4NCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkV0YXBhczwvc3Bhbj4NCiZuYnNwOw0KDQpDb21vIGVzdGUgdHJhYmFqbyBjb25zaXN0YXLDoSBlbiBsYSBjcmVhY2nDs24gZGUgdW4gbW9kZWxvIGRlIGFwcmVuZGl6YWplIHN1cGVydmlzYWRvIHBhcmEgdW5hIHNlcmllIGRlIHRpZW1wbyBsYSBldGFwYSBkZWwgYW7DoWxpc2lzIGV4cGxvcmF0b3JpbyBlcyB1biBwb2NvIG3DoXMgZGVuc2EgcXVlIGxvcyBhbsOhbGlzaXMgZXhwbG9yYXRvcmlvcyBkZXNjcmlwdGl2b3MgY29tdW5lcy4gRXN0YSBldGFwYSBzZSBjb25zdGl0dXllIGRlIGxhcyBzaWd1aWVudGVzIGZhc2VzOiANCg0KKiBBbsOhbGlzaXMgRXhwbG9yYXRvcmlvDQogICsgQW7DoWxpc2lzIHZpc3VhbA0KICArIERldGVjY2nDs24gZGUgb3V0bGllcnMNCiAgKyBDb3JyZWxvZ3JhbWFzDQogICsgRGVzY29tcG9zaWNpw7NuIGRlIGxhIHNlcmllDQogICsgQW7DoWxpc2lzIGRlIGVzdGFjaW9uYXJpZWRhZA0KICArIERldGVjY2nDs24gZGUgZXN0YWNpb25hbGlkYWQNCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPlNldCB1cDwvc3Bhbj4NCiZuYnNwOw0KDQpJbmljaWFtb3MgY29uZmlndXJhbmRvIGxhcyBvcGNpb25lcyBnZW5lcmFsZXMgcXVlIHZhbW9zIGEgcmVxdWVyaXIgcGFyYSBlbCBkZXNhcnJvbGxvIGRlIGVzdGUgcHJveWVjdG8uDQoNCmBgYHtyIHNldHVwLCBtZXNzYWdlPUZBTFNFLCBjb21tZW50PSIiLCB3YXJuaW5nPUZBTFNFLCByZXN1bHRzPSdoaWRlJ30NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICB3YXJuaW5nID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgbWVzc2FnZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgIHdhcm5pbmcgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICBmaWcuYWxpZ24gPSAiY2VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICBvdXQud2lkdGggPSAiOTAlIiwNCiAgICAgICAgICAgICAgICAgICAgICBvdXQuaGVpZ2h0ID0gIjcwJSIpDQoNCnBhcXVldGVzIDwtIGMoJ3RpZHl2ZXJzZScsICAgICAgIyBtYW5pcHVsYWNpw7NuIGRhdG9zDQogICAgICAgICAgICAgICdsdWJyaWRhdGUnLCAgICAgICMgbWFuZWphciBmZWNoYXMNCiAgICAgICAgICAgICAgJ2tuaXRyJywgICAgICAgICAgIyBtYW5lam8gZGUgdGFibGFzDQogICAgICAgICAgICAgICdrYWJsZUV4dHJhJywgICAgICMgbWFuZWpvIGRlIHRhYmxhcw0KICAgICAgICAgICAgICAncGxvdGx5JywgICAgICAgICAjIGdyw6FmaWNvcyBpbnRlcmFjdGl2b3MNCiAgICAgICAgICAgICAgJ2dyaWRFeHRyYScsICAgICAgIyBsYXlvdXQgZGUgZ3JhZmljb3MgZ2dwbG90DQogICAgICAgICAgICAgICdub3J0ZXN0JywgICAgICAgICMgcHJ1ZWJhcyBlc3RhZMOtc3RpY2FzDQogICAgICAgICAgICAgICJzdW1tYXJ5dG9vbHMiLCAgICMgZXN0YWTDrXN0aWNhcyB1bml2YXJpYW50ZXMNCiAgICAgICAgICAgICAgIlRTc3R1ZGlvIiwgICAgICAgIyB0aW1lIHNlcmllcw0KICAgICAgICAgICAgICAiZm9yZWNhc3QiLCAgICAgICAjIHRpbWUgc2VyaWVzDQogICAgICAgICAgICAgICJ6b28iLCAgICAgICAgICAgICMgdGltZSBzZXJpZXMNCiAgICAgICAgICAgICAgImFub21hbGl6ZSIsICAgICAgIyBhbm9tYWxpYXMNCiAgICAgICAgICAgICAgInRzZXJpZXMiLCAgICAgICAgIyBzZXJpZSBkZSB0aWVtcG8NCiAgICAgICAgICAgICAgIkZpdEFSIiwgICAgICAgICAgIyBhcmltYQ0KICAgICAgICAgICAgICAiZ2dmb3J0aWZ5IiwgICAgICAjIGRpYWdub3NpcyByZXNpZHVhbGVzDQogICAgICAgICAgICAgICJsbXRlc3QiLCAgICAgICAgICMgdGVzdCBjb2VmaWNpZW50ZXMgYXJpbWENCiAgICAgICAgICAgICAgInN0cnVjY2hhbmdlIiwgICAgIyBncsOhZmljbyBDVVNVTSB0ZXN0DQogICAgICAgICAgICAgICdwcm9waGV0JywgICAgICAgICMgbW9kZWxvIHByb3BoZXQNCiAgICAgICAgICAgICAgJ3RpZHltb2RlbHMnKSAgICAgIyBtYW5lam8gZGUgbW9kZWxvcw0KDQppbnN0YWxhZG9zIDwtIHBhcXVldGVzICVpbiUgaW5zdGFsbGVkLnBhY2thZ2VzKCkNCg0KaWYoc3VtKGluc3RhbGFkb3MgPT0gRkFMU0UpID4gMCkgew0KICBpbnN0YWxsLnBhY2thZ2VzKHBhcXVldGVzWyFpbnN0YWxhZG9zXSkNCn0NCg0KbGFwcGx5KHBhcXVldGVzLCByZXF1aXJlLCBjaGFyYWN0ZXIub25seSA9IFRSVUUpDQoNCmBgYA0KDQombmJzcDsNCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkNhcmdhIGRlIGRhdG9zPC9zcGFuPg0KJm5ic3A7DQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KTGEgaW5mb3JtYWNpw7NuIHBhcmEgZXN0ZSBhbsOhbGlzaXMgZXMgcMO6YmxpY2EgeSBsb3MgZGF0b3MgYXPDrSBjb21vIHN1IGRlc2NyaXBjacOzbiBzZSBlbmN1ZW50cmFuIGVuIGxhIHDDoWdpbmEgZGVsIFtBSUNNXShodHRwczovL3d3dy5haWNtLmNvbS5teC9lc3RhZGlzdGljYXMtZGVsLWFpY20vMTctMDktMjAxMykuDQoNCkVzIGltcG9ydGFudGUgYWNsYXJhciBxdWUgbG9zIGRhdG9zIG9yaWdpbmFsZXMgc2UgZW5jdWVudHJhbiBkZW50cm8gZGUgaW5mb3JtZXMgZW4gZm9ybWF0byBwZGYsIHBvciBsbyBxdWUsIHBhcmEgZXh0cmFlcmxvcyBzZSByZWN1cnJpw7MgYSBsYSB0w6ljbmljYSBjb25vY2lkYSBjb21vICoqUmVjb25vY2ltaWVudG8gw5NwdGljbyBkZSBDYXJhY3RlcmVzIChPQ1IpKiouIFBhcmEgZWwgcGVyaW9kbyAyMDEyLTIwMTksIHNlIHV0aWxpesOzIFIgeSBwYXJhIGVsIHJlc3RvIGRlbCBwZXJpb2RvIDIwMDgtMjAxMSBzZSB1dGlsaXrDsyBQeXRob24sIHlhIHF1ZSBvZnJlY2nDsyBsb3MgbWVqb3JlcyByZXN1bHRhZG9zIHBhcmEgZXNvcyBhw7Fvcy4gTG9zIGFyY2hpdm9zIGNvbiB0b2RvIGVsIGPDs2RpZ28sIGRlIFIgeSBQeXRob24sIHNlIHB1ZWRlbiBlbmNvbnRyYXIgZW4gbWkgcmVwb3NpdG9yaW8gZGUgW2dpdGh1Yl0oaHR0cHM6Ly9naXRodWIuY29tL2Fyb2phc21vci9QYXNhamVyb3MpLiBIYWJpZW5kbyBoZWNobyBlc3RhIGFjbGFyYWNpw7NuLCBhIGNvbnRpbnVhY2nDs24sIHlhIHNvbG8gc2UgaW1wb3J0YSBlbCBhcmNoaXZvIGZpbmFsOg0KPC9kaXY+DQoNCmBgYHtyIHZhcmlhYmxlc30NCnNhbGlkYXMgPC0gcmVhZC5jc3YoIlNhbGlkYXNfTmFjaW9uYWxlcy5jc3YiKQ0KDQpoZWFkKHNhbGlkYXMpDQoNCmdsaW1wc2Uoc2FsaWRhcykNCmBgYA0KDQpFbiBlc3RlIHByaW1lciByZXN1bWVuIHBvZGVtb3MgdmVyIHF1ZSBsYSB2YXJpYWJsZSBGZWNoYSBlc3TDoSBjb21vIGNoYXJhY3RlciwgcG9yIGxvIHF1ZSwgZXMgbmVjZXNhcmlvIGNvbnZlcnRpcmxhIGFsIGZvcm1hdG8gY29ycmVjdG86DQoNCmBgYHtyfQ0Kc2FsaWRhcyRGZWNoYSA8LSBkbXkoc2FsaWRhcyRGZWNoYSkNCmdsaW1wc2Uoc2FsaWRhcykNCmBgYA0KDQpZYSB0ZW5lbW9zIGxhIHZhcmlhYmxlIEZlY2hhIGVuIGVsIGZvcm1hdG8gY29ycmVjdG8sIHNpbiBlbWJhcmdvLCBsYSBtYXlvcsOtYSBkZSBsYXMgZnVuY2lvbmVzIHJlbGFjaW9uYWRhcyBjb24gZWwgYW7DoWxpc2lzIGRlIHVuYSBzZXJpZSB0ZW1wb3JhbCByZXF1aWVyZW4gcXVlIGxvcyBkYXRvcyBlc3TDqW4gZW4gZm9ybWF0byBkZSBzZXJpZSBkZSB0aWVtcG8gKHRzKSwgcG9yIGxvIHF1ZSwgaGFyZW1vcyBsYSBjb252ZXJzaW9uOg0KDQpgYGB7cn0NCnNhbGlkYXMudHMgPC0gdHMoc2FsaWRhc1ssMl0sIHN0YXJ0ID0gYygyMDA4LCAxKSwgZW5kID0gYygyMDE5LCAxMiksIGZyZXF1ZW5jeSA9IDEyKQ0KDQp0c19pbmZvKHNhbGlkYXMudHMpDQpgYGANCg0KQWhvcmEgc2ksIHlhIHRlbmVtb3MgbnVlc3RyYSBzZXJpZSBkZSB0aWVtcG8gbGEgY3VhbCBpbmljaWEgZW4gZW5lcm8gZGUgMjAwOCB5IHRlcm1pbmEgZW4gZGljaWVtYnJlIGRlIDIwMTkuIFBhc2Vtb3MgYWhvcmEgYWwgYW7DoWxpc2lzIGV4cGxvcmF0b3Jpby4NCg0KJm5ic3A7DQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5BbsOhbGlzaXMgZXhwbG9yYXRvcmlvPC9zcGFuPg0KDQojIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkFuw6FsaXNpcyB2aXN1YWw8L3NwYW4+DQoNCkVtcGVjZW1vcyBwb3IgYW5hbGl6YXIgdmlzdWFsbWVudGUgbGEgc2VyaWUgcGFyYSBjb25vY2VyIHF1w6kgZm9ybWEgdGllbmUgYXPDrSBjb21vIHN1cyBwcmluY2lwYWxlcyBtZWRpZGFzIGVzdGFkw61zdGljYXM6DQoNCmBgYHtyfQ0Kc2FsaWRhcyAlPiUgDQogIGdncGxvdChhZXMoRmVjaGEsIFNhbGlkYXMpKSArDQogIGdlb21fbGluZSgpICsNCiAgc2NhbGVfeF9kYXRlKGRhdGVfYnJlYWtzID0gIjYgbW9udGgiLCBkYXRlX2xhYmVscyA9ICIlWS0lYiIpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA5MCwgc2l6ZSA9IDgpKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiQUlDTTogU2FsaWRhcyBOYWNpb25hbGVzIGUgSW50ZXJuYWNpb25hbGVzIDIwMDgtMjAxOSIpDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpMbyBxdWUgc2UgcHVlZGUgYXByZWNpYXIgZXMgcXVlIGhhc3RhIGZpbmFsZXMgZGUgMjAxMCBsYSB0ZW5kZW5jaWEgZXMgY29uc3RhbnRlIHkgaGFzdGEgdW4gcG9jbyBkZWNyZWNpZW50ZSwgc2luIGVtYmFyZ28sIGRlc2RlIGluaWNpb3MgZGUgMjAxMSBsYSB0ZW5kZW5jaWEgZXMgY3JlY2llbnRlIHkgYWRpdGl2YS4gRXN0YSBzZXJpZSBjbGFyYW1lbnRlIG11ZXN0cmEgdW4gY2FtYmlvIGRlIHRlbmRlbmNpYSwgbG8gcXVlIG11eSBwcm9iYWJsZW1lbnRlIG9jYXNpb25lIHF1ZSBubyBzZSBwdWVkYSBjdW1wbGlyIGVsIHN1cHVlc3RvIGRlIGVzdGFjaW9uYXJpZWRhZCBpbmNsdXNvIHNpIGFwbGljYW1vcyBkaWZlcmVuY2lhcy4gQ29udmllbmUgZGUgdW5hIHZleiBjb21wcm9iYXIgZXNlIGNhbWJpbyBvIHF1aWVicmUsIHBhcmEgbG8gY3VhbCBoYXJlbW9zIGxvIHNpZ3VpZW50ZToNCg0KMS4gQ3JlYXIgZWwgZ3LDoWZpY28gQ1VTVU0sIHF1ZSBub3MgbW9zdHJhcsOhIHZpc3VhbG1lbnRlIHNpIGxhIHNlcmllIHByZXNlbnRhIHVuIHF1aWVicmUgeSBlbiBxdcOpIHBhcnRlLg0KMi4gQ3JlYXIgdW5hIHZhcmlhYmxlIGRpY290w7NtaWNhIHF1ZSB0b21hcsOhIHZhbG9yZXMgZGUgMCBoYXN0YSBlbmVybyBkZSAyMDExIHkgcG9zdGVyaW9yIGEgZXNhIGZlY2hhIHRvbWFyYSB2YWxvcmVzIGRlIDEuDQozLiBIYWNlciB1bmEgcmVncmVzacOzbiBsaW5lYWwgY3V5YXMgdmFyaWFibGVzIGluZGVwZW5kaWVudGVzIHNlcsOhbjogbGEgdmFyaWFibGUgZGljb3TDs21pY2EgY3JlYWRhLCBlbCB0aWVtcG8gKHF1ZSBzZXLDoSB1biBjb25zZWN1dGl2bykgeSBsYSBpbnRlcmFjY2nDs24gZW50cmUgZWwgdGllbXBvIHkgbGEgdmFyaWFibGUgZGljb3TDs21pY2EuDQo8L2Rpdj4NCg0KYGBge3J9DQp0IDwtIGMoMTpsZW5ndGgoc2FsaWRhcy50cykpDQoNCnAuc2FsaWRhcyA8LSBlZnAoc2FsaWRhcy50cyB+IHQsIHR5cGUgPSAiT0xTLUNVU1VNIikNCg0KcGxvdChwLnNhbGlkYXMpDQoNCmBgYA0KDQpMYSBncsOhZmljYSBub3MgaW5kaWNhIHF1ZSBsYSBzZXJpZSBzw60gcHJlc2VudGEgdW4gcXVpZWJyZSBlbiBzdSB0ZW5kZW5jaWEgYWwgcGFyZWNlciBkZXNkZSAyMDA5IGhhc3RhIGluaWNpb3MgZGUgMjAxMS4gQWhvcmEsIGxvIGNvbXByb2JhcmVtb3MgZm9ybWFsbWVudGUgY29uIHVuYSByZWdyZXNpw7NuLg0KDQpgYGB7cn0NCnQuc2FsaWRhcyA8LSBzYWxpZGFzICU+JSANCiAgbXV0YXRlKEQgPSBpZl9lbHNlKEZlY2hhIDwgIjIwMTEtMDItMDEiLCAwLCAxKSkNCg0KcmVnIDwtIGxtKFNhbGlkYXMgfiBEICsgdCArIChEKnQpLCBkYXRhID0gdC5zYWxpZGFzKQ0KDQpzdW1tYXJ5KHJlZykNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkxvcyBjb2VmaWNpZW50ZXMgZGUgbGEgdmFyaWFibGUgZGljb3TDs21pY2EgeSBkZSBzdSBpbnRlcmFjY2nDs24gY29uIGVsIHRpZW1wbyBzYWxpZXJvbiBzaWduaWZpY2F0aXZvcy4gQWhvcmEsIGVsIGNvZWZpY2VpbnRlIGRlIGxhIHZhcmlhYmxlIGRpY290w7NtaWNhIEQsIGVzIGVsIGludGVyY2VwdG8gZGlmZXJlbmNpYWwgeSBlbCBjb2VmaWNpZW50ZSBkZSBsYSBpbnRlcmFjY2nDs24gZW50cmUgZWwgdGllbXBvIHkgbGEgdmFyaWFibGUgZGljb3TDs21pY2EgZXMgZWwgY29lZmljaWVudGUgZGUgbGEgcGVuZGllbnRlIGRpZmVyZW5jaWFsLiBMbyBhbnRlcmlvciwgc2lnbmlmaWNhIHF1ZSBsb3MgZG9zIHBlcmlvZG9zIHNvbiBkaXN0aW50b3MgeSBsYXMgY2F1c2FzIGRlIGxhIGRpZmVyZW5jaWEgc29uIHRhbnRvIGVsIGludGVyY2VwdG8gY29tbyBsYSBwZW5kaWVudGUuDQoNClBvciBsbyBhbnRlcmlvciwgbGEgc2VyaWUgcXVlIHZhbW9zIGEgbW9kZWxhciBwcmVzZW50YSBjYW1iaW8gZGUgdGVuZGVuY2lhIGEgcGFydGlyIGRlIDIwMTEsIGxvIGN1YWwgc2UgZXhwbGljYSBlbiBwYXJ0ZSBkZWJpZG8gYSBxdWUgZW4gMjAwOC0yMDA5IHNlIHByZXNlbnTDsyBsYSBjcmlzaXMgZmluYW5jaWVyYSB5LCBhZGVtw6FzLCBlbiAyMDA5LCBlbiBNw6l4aWNvLCBpbmljacOzIGxhIGNyaXNpcyBzYW5pdGFyaWEgZGUgbGEgSW5mbHVlbnphIEgxTjEsIHRhbWJpw6luLCBhIGluaWNpb3MgZGUgMjAxMSBlbXBlemFyb24gbG9zIHByb2JsZW1hcyBmaW5hbmNpZXJvcyBkZSBsYSBlbXByZXNhIE1leGlhbmFuYSBkZSBBdmlhY2nDs24sIGxhIGN1YWwgdGVybWluw7MgcG9yIGNlcnJhciBzdXMgb3BlcmFjaW9uZXMgYSBmaW5hbGVzIGRlIGFnb3N0byBkZSBlc2UgbWlzbW8gYcOxby4gVG9kb3MgZXNvcyBhY29udGVjaW1pZW50b3MgYWZlY3Rhcm9uIGxhIGVjb25vbcOtYSBsbyBxdWUgZXhwbGljYSBsYSB0ZW5kZW5jaWEgYSBsYSBiYWphIGVuIGVsIHBlcmlvZG8gMjAwOC0yMDExLg0KDQpBZGVtw6FzLCBzZSBwdWVkZSBzb3NwZWNoYXIgcXVlIGhheSBwcmVzZW5jaWEgZGUgZXN0YWNpb25hbGlkYWQgcG9yIGVsIHBhdHLDs24gcXVlIHNpZ3VlIGxhIHNlcmllLCB0YW1iacOpbiwgaGF5IHF1ZSBjb25zaWRlcmFyIHF1ZSBsYSBkYXRhIHJlcHJlc2VudGEgYSBsYXMgcGVyc29uYXMgcXVlIHZpYWphbiBwb3IgYXZpw7NuLCBwb3IgbG8gcXVlLCBlbCBzZW50aWRvIGNvbcO6biBub3MgaW5kaWNhcsOtYSBxdWUgZW4gcGVyaW9kb3MgdmFjYWNpb25hbGVzIGxhIGNhbnRpZGFkIGRlIHBlcnNvbmFzIHF1ZSBzYWxlbiBkZSB2aWFqZSBhdW1lbnRhcsOhLiBNw6FzIGFkZWxhbnRlIGxvIHRlbmRyZW1vcyBxdWUgY29tcHJvYmFyIGZvcm1hbG1lbnRlLg0KPC9kaXY+DQoNCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+RGF0b3MgYXTDrXBpY29zIChvdXRsaWVycyk8L3NwYW4+DQombmJzcDsNCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpBbGdvIHF1ZSBhZmVjdGEgbXVjaG8gYSBsb3MgbW9kZWxvcyBkZSBzZXJpZXMgZGUgdGllbXBvIHNvbiBsb3MgbGxhbWFkb3Mgb3V0bGllcnMgbyBkYXRvcyBhdMOtcGljb3MsIHlhIHF1ZSBhbCBzZXIgb2JzZXJ2YWNpb25lcyBkZW1hc2lhZG8gZ3JhbmRlcyBvIHBlcXVlw7FhcyBhZmVjdGFuIGxhIGNhcGFjaWRhZCBwcmVkaWN0aXZhIGRlIGxvcyBtb2RlbG9zLiBQb3IgbG8gdGFudG8sIGVzIGRlIHZpdGFsIGltcG9ydGFuY2lhIGlkZW50aWZpY2FybG9zIHkgdHJhdGFybG9zLiBVbiBwcmltZXIgYW7DoWxpc2lzIHZpc3VhbCBzZSBoYWNlIGNvbiBhcG95byBkZWwgYm94cGxvdDoNCjwvZGl2Pg0KDQpgYGB7cn0NCnNhbGlkYXMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBmYWN0b3IoMSksIHkgPSBTYWxpZGFzKSkgKw0KICBnZW9tX2JveHBsb3Qob3V0bGllci5jb2xvdXIgPSAicmVkIiwgZmlsbCA9ICJncmV5IiwgYWxwaGEgPSAwLjMpICsNCiAgc3RhdF9zdW1tYXJ5KGZ1biA9IG1lYW4sIA0KICAgICAgICAgICAgICAgZ2VvbSA9ICJwb2ludCIsDQogICAgICAgICAgICAgICBzaGFwZSA9IDIwLCANCiAgICAgICAgICAgICAgIHNpemUgPSA0LCANCiAgICAgICAgICAgICAgIGNvbG9yID0gInJlZCIsDQogICAgICAgICAgICAgICBmaWxsID0gInJlZCIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicygNCiAgICB0aXRsZSA9ICJCb3hwbG90IFNhbGlkYXMiLA0KICAgIHggPSAiU2FsaWRhcyINCiAgKQ0KYGBgDQoNCkVuIGVsIGJveHBsb3Qgc2UgYXByZWNpYSBxdWUgbGEgc2VyaWUgbm8gcHJlc2VudGEgb2JzZXJ2YWNpb25lcyBhdMOtcGljYXMsIGFkZW3DoXMsIHNlIG9ic2VydmEgcXVlIGxhIG1lZGlhIChwdW50byByb2pvKSB5IGxhIG1lZGlhbmEgbm8gZXN0w6FuIG11eSBzZXBhcmFkYXMgbG8gcXVlIG5vcyBpbmRpY2EgcXVlIG5vIGhheSBhc2ltZXRyaWEgZnVlcnRlLiBDb21wbGVtZW50ZW1vcyBsYSBncsOhZmljYSBjb24gYWxndW5vcyBlc3RhZMOtc2l0aWNvcyBiw6FzaWNvczoNCg0KYGBge3J9DQpkZXNjcihzYWxpZGFzJFNhbGlkYXMsDQogICAgICBqdXN0aWZ5ID0gImMiLA0KICAgICAgaGVhZGluZ3MgPSBUKSAlPiUgDQogIGtibChhbGlnbiA9ICJjIiwgZGlnaXRzID0gMiAsZm9ybWF0LmFyZ3MgPSBsaXN0KGJpZy5tYXJrID0gIiwiKSkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KUGFzZW1vcyBhIG90cm8gbcOpdG9kbyBtw6FzIHJvYnVzdG8gcGFyYSBkZXRlY3RhciBzaSByZWFsbWVudGUgZW4gbGEgc2VyaWUgaGF5IG9ic2VydmFjaW9uZXMgYXTDrXBpY2FzLiBQYXJhIGVzdG8gbm9zIGFwb3lhcmVtb3MgZGVsIHBhcXVldGUgYW5vbWFsaXplIHF1ZSBub3MgcGVybWl0aXLDoSBjYWxpYnJhciBlbCBpbnRlcnZhbG8gZGUgY29uZmlhbnphIHkgZWwgcG9yY2VudGFqZSBtw6F4aW1vIGRlIHBvc2libGVzIHZhbG9yZXMgYXTDrXBpY29zIGEgbW9zdHJhciwgZW50cmUgb3RyYXMgY29zYXMuIEFsZ28gaW1wb3J0YW50ZSBhIG1lbmNpb25hciBlcyBxdWUgZXN0ZSBwYXF1ZXRlIHJlcXVpZXJlIHF1ZSBlbCBpbnB1dCBlc3TDqSBlbiBmb3JtYXRvIGRlIHRpYmJsZSBvIHRpYmJsZV90aW1lLCBwb3IgbG8gcXVlLCBubyB1c2FyZW1vcyBlbCBvYmpldG8gdGlwbyBzZXJpZSBkZSB0aWVtcG8gY3JlYWRvIGFsIGluaWNpbyBkZWwgYW7DoWxpc2lzLg0KPC9kaXY+DQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0V9DQpzYWxpZGFzLnRibCA8LSBzYWxpZGFzDQoNCnNhbGlkYXMudGJsJEZlY2hhIDwtIGFzLnllYXJtb24oc2FsaWRhcy50YmwkRmVjaGEpDQpzYWxpZGFzLnRibCRTYWxpZGFzIDwtIGFzLm51bWVyaWMoc2FsaWRhcy50YmwkU2FsaWRhcykNCg0Kc2FsaWRhcy50YmwgPC0gYXNfdGliYmxlKHNhbGlkYXMudGJsKQ0KDQpwcmVwX3RibF90aW1lKHNhbGlkYXMudGJsLCBtZXNzYWdlID0gVFJVRSkNCg0Kc2FsaWRhcy50YmwgJT4lIA0KICB0aW1lX2RlY29tcG9zZShTYWxpZGFzLCBtZXRob2QgPSAic3RsIikgJT4lIA0KICBhbm9tYWxpemUocmVtYWluZGVyLCBtZXRob2QgPSAiZ2VzZCIsIG1heF9hbm9tcyA9IDAuMiwgYWxwaGEgPSAwLjAzKSAlPiUgDQogIHRpbWVfcmVjb21wb3NlKCkgJT4lIA0KICBwbG90X2Fub21hbGllcyh0aW1lX3JlY29tcG9zZWQgPSBUUlVFLCANCiAgICAgICAgICAgICAgICAgYWxwaGFfZG90cyA9IDAuNCwNCiAgICAgICAgICAgICAgICAgYWxwaGFfcmliYm9uID0gMC4xLA0KICAgICAgICAgICAgICAgICBmaWxsX3JpYmJvbiA9ICJncmV5ODAiKSArIA0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKyANCiAgbGFicyh0aXRsZSA9ICJEZXRlY3RhbmRvIEFub21hbMOtYXMiLCB5ID0gIlNhbGlkYXMiKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpFbiBsYSBncsOhZmljYSBhbnRlcmlvciBwb2RlbW9zIHZlciBxdWUgc2UgZGV0ZWN0YXJvbiBhbGd1bmFzIG9ic2VydmFjaW9uZXMgYXTDrXBpY2FzLCBzaW4gZW1iYXJnbywgYWwgcGFyZWNlciBzb2xvIHVubyBkZSBlbGxvcyBlc3TDoSBtdXkgYWxlamFkbyBkZWwgcGF0csOzbiBkZSBsYSBzZXJpZS4gVGVuZHJlbW9zIHF1ZSB0cmF0YXJsbyBwYXJhIHF1ZSBubyBub3MgY2F1c2UgcHJvYmxlbWFzIG3DoXMgYWRlbGFudGUuDQoNClBhcmEgaWRlbnRpZmljYXIgcXXDqSBvYnNlcnZhY2lvbmVzIHNvbiBhdMOtcGljYXMgdXNhcmVtb3MgbGEgZnVuY2nDs24gdHNvdXRsaWVycyBkZWwgcGFxdWV0ZSBmb3JlY2FzdCwgeWEgcXVlIHBlcm1pdGUgaGFjZXIgbGEgaWRlbnRpZmljYWNpw7NuIHksIGFkZW3DoXMsIG5vcyBwcm9wb25kcsOhIGxvcyB2YWxvcmVzIGEgc3VzdGl0dWlyLg0KPC9kaXY+DQoNCmBgYHtyfQ0KdHNvdXRsaWVycyhzYWxpZGFzLnRzKSAlPiUgDQogIGFzLmRhdGEuZnJhbWUoKSAlPiUNCiAgcmVuYW1lKE9ic2VydmFjaW9uID0gaW5kZXgsIFJlZW1wbGF6byA9IHJlcGxhY2VtZW50cykgJT4lIA0KICBrYmwoYWxpZ24gPSAiYyIsZGlnaXRzID0gMCAsZm9ybWF0LmFyZ3MgPSBsaXN0KGJpZy5tYXJrID0gIiwiKSkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KVmVtb3MgcXVlIGxhIGZ1bmNpw7NuIGlkZW50aWZpY2EgbGFzIG9ic2VydmFjaW9uZXMgZW4gbGFzIHBvc2ljaW9uZXMgMTQgeSAxNyBjb21vIGF0w61waWNhcywgeSBub3MgbXVlc3RyYSBsb3MgdmFsb3JlcyBwb3IgbG9zIGN1YWxlcyBoYXLDrWEgbGEgc3VzdGl0dWNpw7NuIChxdWUgdXRpbGl6YSBlbCBtw6l0b2RvIGRlIGludGVycG9sYWNpw7NuIGxpbmVhbCkuIEVsIHRyYWJham8gbG8gaGFjZSBsYSBmdW5jacOzbiB0c2NsZWFuIHF1ZSBubyBzb2xvIHN1c3RpdHV5ZSBsb3MgdmFsb3JlcyBhdMOtcGljb3Mgc2lubyBxdWUgdGFtYmnDqW4gaWRlbnRpZmljYSB2YWxvcmVzIGZhbHRhbnRlcyAocXVlIG5vIGVzIGVzdGUgZWwgY2FzbykuDQo8L2Rpdj4NCg0KVmVhbW9zIGdyw6FmaWNhbWVudGUgY3XDoWxlcyBzZXLDoW4gbG9zIHB1bnRvcyBxdWUgc2Ugc3VzdGl0dWlyw6FuOg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCiMgbnVldm9zIHB1bnRvcw0KYXV0b3Bsb3QodHNjbGVhbih0cyhzYWxpZGFzJFNhbGlkYXMpKSwgc2VyaWVzID0gImNsZWFuIiwgY29sb3VyID0gJ3JlZCcsIGx3ZCA9IDAuOSkgKw0KICBhdXRvbGF5ZXIodHMoc2FsaWRhcyRTYWxpZGFzKSwgc2VyaWVzID0gIm9yaWdpbmFsIiwgY29sb3IgPSAnZ3JheScsIGx3ZCA9IDEpICsNCiAgZ2VvbV9wb2ludChkYXRhID0gdHNvdXRsaWVycyhzYWxpZGFzLnRzKSAlPiUgYXMuZGF0YS5mcmFtZSgpLCANCiAgICAgICAgICAgICBhZXMoeCA9IGluZGV4LCB5ID0gcmVwbGFjZW1lbnRzKSwgY29sID0gJ2JsdWUnKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiU3VzdGl0dWNpw7NuIE91dGxpZXJzOiBudWV2b3MgcHVudG9zIiwgeCA9ICJUaWVtcG8iLCB5ID0gIlNhbGlkYXMiKQ0KYGBgDQoNCkxvcyBwdW50b3MgZW4gY29sb3IgYXp1bCBzZXLDoW4gbGFzIG51ZXZhcyBvYnNlcnZhY2lvbmVzIHF1ZSByZWVtcGxhemFyw6FuIGEgbG9zIG91dGxpZXJzIHF1ZSBzZSBpZGVudGlmaWNhcm9uLiBTZSBoYWNlIGxhIHN1c3RpdHVjacOzbjoNCg0KYGBge3J9DQpzYWxpZGFzLnRzIDwtIHRzY2xlYW4oc2FsaWRhcy50cykNCg0KYGBgDQoNClNlIGhhY2UgZWwgcmVlbXBsYXpvIGVuIGxhIGJhc2Ugb3JpZ2luYWwgcGFyYSB2b2x2ZXIgYSB2ZXIgbGEgZ3LDoWZpY2EgZGUgbGFzIGFub21hbMOtYXM6DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KIyBzZSBhY3R1YWxpemFuIGxvcyBkYXRvcyBlbiBlbCBkYXRhZnJhbWUgb3JpZ2luYWwNCnNhbGlkYXMkU2FsaWRhc1sxNF0gPC0gODc4MTc4LjENCnNhbGlkYXMkU2FsaWRhc1sxN10gPC0gMTAzOTk2Ny43DQoNCiMgc2UgcmVwaXRlIGVsIHByb2Nlc28gcGFyYSB2ZXIgbGEgZ3JhZmljYQ0KDQpzYWxpZGFzLnRibCA8LSBzYWxpZGFzDQoNCnNhbGlkYXMudGJsJEZlY2hhIDwtIGFzLnllYXJtb24oc2FsaWRhcy50YmwkRmVjaGEpDQpzYWxpZGFzLnRibCRTYWxpZGFzIDwtIGFzLm51bWVyaWMoc2FsaWRhcy50YmwkU2FsaWRhcykNCg0Kc2FsaWRhcy50YmwgPC0gYXNfdGliYmxlKHNhbGlkYXMudGJsKQ0KDQojcHJlcF90YmxfdGltZShzYWxpZGFzLnRibCwgbWVzc2FnZSA9IFRSVUUpDQoNCnNhbGlkYXMudGJsICU+JSANCiAgdGltZV9kZWNvbXBvc2UoU2FsaWRhcywgbWV0aG9kID0gInN0bCIpICU+JSANCiAgYW5vbWFsaXplKHJlbWFpbmRlciwgbWV0aG9kID0gImdlc2QiLCBtYXhfYW5vbXMgPSAwLjIsIGFscGhhID0gMC4wMykgJT4lIA0KICB0aW1lX3JlY29tcG9zZSgpICU+JSANCiAgcGxvdF9hbm9tYWxpZXModGltZV9yZWNvbXBvc2VkID0gVFJVRSwgDQogICAgICAgICAgICAgICAgIGFscGhhX2RvdHMgPSAwLjQsDQogICAgICAgICAgICAgICAgIGFscGhhX3JpYmJvbiA9IDAuMSwNCiAgICAgICAgICAgICAgICAgZmlsbF9yaWJib24gPSAiZ3JleTgwIikgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKw0KICBsYWJzKHRpdGxlID0gIkRldGVjdGFuZG8gQW5vbWFsw61hcyIsIHkgPSAiU2FsaWRhcyIpDQoNCmBgYA0KDQpTZSBzaWd1ZW4gZGV0ZWN0YW5kbyBkYXRvcyBhdMOtcGljb3MsIHNpbiBlbWJhcmdvLCBubyBlc3TDoW4gbXV5IGFsZWphZG9zIGRlbCBpbnRlcnZhbG8gZGUgY29uZmlhbnphLCBwb3IgbG8gcXVlLCBub3MgcXVlZGFyZW1vcyBjb24gZXN0YSBzZXJpZS4NCg0KQWxnbyBpbXBvcnRhbnRlIGEgbWVuY2lvbmFyIGVzIHF1ZSBzZSBkZWJlIHRlbmVyIGJpZW4gY2xhcm8gZWwgb2JqZXRpdm8gZGVsIGFuw6FsaXNpcyBxdWUgc2UgZXN0w6kgYnVzY2FuZG8sIHlhIHF1ZSBlbiBhbGd1bm9zIGNhc29zIHBvZHLDrWEgc2VyICoqaW5jb3JyZWN0byBzdXN0aXR1aXIgbGFzIG9ic2VydmFjaW9uZXMgYXTDrXBpY2FzLioqDQoNCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+Q29ycmVsb2dyYW1hczwvc3Bhbj4NCiZuYnNwOw0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NClBhcmEgdG9kb3MgbG9zIG1vZGVsb3MgZGUgc2VyaWVzIGRlIHRpZW1wbyBlcyBkZSBzdW1hIGltcG9ydGFuY2lhIHJldmlzYXIgbGFzIGZ1bmNpb25lcyBkZSBhdXRvY29ycmVsYWNpw7NuIHRhbnRvIHNpbXBsZSBjb21vIHBhcmNpYWwgY29uIGVsIHByb3DDs3NpdG8gZGUgZW1wZXphciBhIGNvbm9jZXIgY8OzbW8gZXN0w6EgbGEgZXN0cnVjdHVyYSBkZSBsb3MgZGF0b3MuIExvcyBjb3JyZWxvZ3JhbWFzIGVuIHBhbGFicmFzIHNpbXBsZXMgbm9zIG11ZXN0cmFuIGxhIG1lbW9yaWEgZGUgbGEgc2VyaWUsIG8gc2VhLCBxdcOpIHRhbnRvIGRlcGVuZGUgZWwgZGF0byBhY3R1YWwgZGUgbG8gcXVlIHN1Y2VkacOzIGVuIGVsIHBhc2Fkby4gdmVhbW9zIGxvcyBjb3JyZWxvZ3JhbWFzIGRlIGxhIHNlcmllIGNvbiA0OCByZXphZ29zLCBxdWUgc29uIHVuIHRlcmNpbyBkZSBsYSBsb25naXR1ZCBkZSBsYSBzZXJpZSAobG8gcmVjb21lbmRhZG8pOg0KPC9kaXY+DQoNCmBgYHtyfQ0KdHNfY29yKHNhbGlkYXMudHMsIGxhZy5tYXggPSA0OCkNCmBgYA0KJm5ic3A7DQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KQWwgb2JzZXJ2YXIgZWwgY29ycmVsb2dyYW1hIEFDRiBwb2RlbW9zIHNvc3BlY2hhciBxdWUgbGEgc2VyaWUgbm8gZXMgZXN0YWNpb25hcmlhLCBvIHNlYSwgbGEgbWVkaWEgbm8gZXMgY29uc3RhbnRlIGEgbG8gbGFyZ28gZGVsIHRpZW1wbywgcG9ycXVlIGxvcyB2YWxvcmVzIGRlIGxhIGZ1bmNpw7NuIGRlIGF1dG9jb3JyZWxhY2nDs24gbm8gZGVjYWVuIHLDoXBpZGFtZW50ZSBjb25mb3JtZSBhdW1lbnRhbiBsb3MgcmV6YWdvcyBlbiBlbCB0aWVtcG8uIEFkZW3DoXMsIGFsIHBhcmVjZXIgdGFtYmnDqW4gaGF5IHByZXNlbmNpYSBkZSBlc3RhY2lvbmFsaWRhZCwgeWEgcXVlIGVuIFBBQ0YgaGF5IGNvZWZpY2llbnRlcyBzaWduaWZpY2F0aXZvcywgcG9yIGxvIHF1ZSwgdGVuZW1vcyBxdWUgZGVzY29tcG9uZXIgbGEgc2VyaWUgcGFyYSB2ZXIgY2FkYSBjb21wb25lbnRlIHBvciBzZXBhcmFkby4NCjwvZGl2Pg0KDQojIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkRlc2NvbXBvc2ljacOzbiBkZSBsYSBzZXJpZTwvc3Bhbj4NCiZuYnNwOw0KDQpWYW1vcyBhIGdlbmVyYXIgbGFzIGdyw6FmaWNhcyBkZSBsb3MgY29tcG9uZW50ZXMgZGUgZXN0YSBzZXJpZTogdGVuZGVuY2lhLCBlc3RhY2lvbmFsaWRhZCB5IHJlc2lkdW8uDQoNCmBgYHtyfQ0KdHNfZGVjb21wb3NlKHNhbGlkYXMudHMsIHR5cGUgPSAiYWRkaXRpdmUiKQ0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KTG8gcXVlIHNlIHB1ZWRlIGFwcmVjaWFyIGVuIGxhcyBncsOhZmljYXMgZXMgcXVlIGxhIHRlbmRlbmNpYSBlcyBhZGl0aXZhLCB5YSBxdWUgbGEgc2VyaWUgdmEgZW4gYXVtZW50byBjb24gZWwgdGllbXBvIHkgbGFzIG9zY2lsYWNpb25lcyBubyBzZSB2YW4gaGFjaWVuZG8gbcOhcyBncmFuZGVzIHNpbm8gcXVlIHNlIG1hbnRpZW5lbiBkZWwgbWlzbW8gdGFtYcOxby4gTG8gYW50ZXJpb3IsIHRlbmVtb3MgcXVlIGNvbmZpcm1hcmxvIGRlIG1hbmVyYSB1biBwb2NvIG3DoXMgZm9ybWFsLg0KDQpIYXkgcHJlc2VuY2lhIGRlIGVzdGFjaW9uYWxpZGFkIG11eSBtYXJjYWRhLCB5YSBxdWUgZWwgbWlzbW8gcGF0csOzbiBzZSByZXBpdGUgYcOxbyB0cmFzIGHDsW8uIFRhbWJpw6luLCBoYXkgcXVlIGNvbmZpcm1hcmxvIGUgaWRlbnRpZmljYXIgZW4gcXXDqSBtZXNlcyBzZSBwcmVzZW50YS4NCg0KVmFtb3MgYSBpZGVudGlmaWNhciBmb3JtYWxtZW50ZSBlbCB0aXBvIGRlIHRlbmRlbmNpYSBxdWUgdGllbmUgbGEgc2VyaWUsIHBhcmEgbG8gY3VhbCBzZSBuZWNlc2l0YSBoYWNlciBsbyBzaWd1aWVudGU6DQoNCiogY2FsY3VsYXIgZWwgbG9nYXJpdG1vIGRlIGxhIG1lZGlhbmEgcGFyYSBjYWRhIHBlcmlvZG8uDQoqIGNhbGN1bGFyIGVsIGxvZ2FyaXRtbyBkZSBsYSBkaWZlcmVuY2lhIGVudHJlIGVsIHBlcmNlbnRpbCA4MCB5IGVsIHBlcmNlbnRpbCAyMCBwYXJhIGNhZGEgcGVyaW9kby4NCiogY29tcGFyYXIgYW1iYXMgc2VyaWVzIGVuIHVuIGdyw6FmaWNvIGRlIGRpc3BlcnNpw7NuLg0KKiBjYWxjdWxhciBlbCBjb2VmaWNpZW50ZSBkZSBjb3JyZWxhY2nDs24uDQoqIHByb2JhciBsYSBzaWduaWZpY2FuY2lhIGVzdGFkaXN0aWNhIGRlIGRpY2hvIGNvZWZpY2llbnRlLg0KPC9kaXY+DQoNCmBgYHtyfQ0KdGFibGEgPC0gc2FsaWRhcyAlPiUgDQogIG11dGF0ZSgNCiAgICBBw7FvID0geWVhcihGZWNoYSksDQogICAgTWVzID0gbW9udGgoRmVjaGEpDQogICkgJT4lIA0KICBzZWxlY3QoLUZlY2hhKSAlPiUgDQogIGdyb3VwX2J5KEHDsW8pICU+JSANCiAgc3VtbWFyaXNlKGxuX01lZGlhbmEgPSBsb2cobWVkaWFuKFNhbGlkYXMpKSwNCiAgICAgICAgIGxuX1JhbmdvID0gbG9nKChxdWFudGlsZShTYWxpZGFzLCAwLjgpIC0gcXVhbnRpbGUoU2FsaWRhcywgMC4yKSkpKQ0KICANCnRhYmxhICU+JSANCiAgZ2dwbG90KGFlcyhsbl9NZWRpYW5hLCBsbl9SYW5nbykpICsNCiAgZ2VvbV9wb2ludCgpICsNCiAgbGFicyh0aXRsZSA9ICJOaXZlbGVzIHZzIERpc3BlcnNpw7NuIiwNCiAgICAgICB4ID0gIk5pdmVsIiwNCiAgICAgICB5ID0gIkRpc3BlcnNpw7NuIikNCg0KYGBgDQoNCmBgYHtyfQ0KY29yKHRhYmxhJGxuX01lZGlhbmEsIHRhYmxhJGxuX1JhbmdvKQ0KY29yLnRlc3QodGFibGEkbG5fTWVkaWFuYSwgdGFibGEkbG5fUmFuZ28pDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpBbCB2ZXIgbGEgZ3LDoWZpY2EgY3JlYWRhIHBvZGVtb3MgdmVyIHF1ZSBleGlzdGUgY2llcnRhIGRlcGVuZGVuY2lhIHkgdGVuZGVuY2lhIHBvc2l0aXZhIGVudHJlIGVsIG5pdmVsIHkgbGEgZGlzcGVyc2nDs24sIGxvIHF1ZSBub3Mgc3VnaWVyZSBxdWUgbGEgdGVuZGVuY2lhIGVzIGRlIHRpcG8gbXVsdGlwbGljYXRpdmEsIHNpbiBlbWJhcmdvLCBhbCB2b2x2ZXIgYSBsYSBncsOhZmljYSBlbiBuaXZlbGVzIHNlIG9ic2VydmEgcXVlIGVuIGxvcyBwcmltZXJvcyBhw7FvcyBsYSBzZXJpZSBlcyBjb25zdGFudGUgeSBhIHBhcnRpciBkZSAyMDExIHNlIHByZXNlbnRhIHRlbmRlbmNpYSBjcmVjaWVudGUuIFBvciBsbyBhbnRlcmlvciwgbG9zIHByaW1lcm9zIGHDsW9zIGVzdMOhbiBqYWxhbmRvIG8gZGlzZnJhc2FuZG8gZWwgdmVyZGFkZXJvIHRpcG8gZGUgdGVuZGVuY2lhIGRlIGxhIHNlcmllLiBBZGVtw6FzLCBoYXkgcXVlIHJlY29yZGFyIHF1ZSB5YSBjb21wcm9iYW1vcyBxdWUgbGEgc2VyaWUgcHJlc2VudGEgdW4gcXVpZWJyZSBkZSB0ZW5kZW5jaWEsIGxvIGN1YWwgYWZlY3RhIGVzdGEgZ3LDoWZpY2EsIHlhIHF1ZSBsYXMgcHJpbWVyYXMgb2JzZXJ2YWNpb25lcyBxdWUgY29ycmVzcG9uZGVuIGFsIHBlcmlvZG8gMjAwOC0yMDExIGxhIHJlbGFjacOzbiBlcyBwb3NpdGl2YSB5IHBhcmEgZWwgcGVyaW9kbyAyMDEyLTIwMTkgcGFyZWNlIHF1ZSBubyBoYXkgcmVsYWNpw7NuLg0KPC9kaXY+DQoNCiMjIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+QW7DoWxpc2lzIGRlIGVzdGFjaW9uYXJpZWRhZDwvc3Bhbj4NCiZuYnNwOw0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVuIGVzdGEgcGFydGUsIHZhbW9zIGEgYW5hbGl6YXIgZm9ybWFsbWVudGUgbGEgZXN0YWNpb25hcmllZGFkIGRlIGxhIHNlcmllLiBFbiBzZXJpZXMgZGUgdGllbXBvLCBlcyBuZWNlc2FyaW8gcXVlIGxhIHNlcmllIGEgbW9kZWxhciBzZWEgZXN0YWJsZSwgbyBzZWEsIHRlbmdhIHVuYSBtZWRpYSB5IHZhcmlhbnphIGNvbnN0YW50ZXMsIHNvYnJlIHRvZG8gc2kgZWwgbW9kZWxvIGEgdXNhciBwYXJhIHJlYWxpemFyIGVsIHByb27Ds3N0aWNvIGVzIHVuIGFyaW1hLiBBbnRlcyBkZWwgYW7DoWxpc2lzIGhheSBxdWUgdGVuZXIgcHJlc2VudGUgcXVlIGxhIHNlcmllIGFsIHByZXNlbnRhciBxdWllYnJlIGRlIHRlbmRlbmNpYSBhZmVjdGFyw6EgY3VhbHF1aWVyIHBydWViYSBkZSBlc3RhY2lvbmFyaWVkYWQgcXVlIHNlIGxlIGhhZ2EsIHBvciBsbyB0YW50bywgZXMgZGUgZXNwZXJhciBxdWUsIGF1bnF1ZSBzZSBhcGxpcXVlbiBkaWZlcmVuY2lhcywgcHVlZGUgcXVlIG5vIHBhc2UgbGEgcHJ1ZWJhIGVsZWdpZGEuDQo8L2Rpdj4NCg0KVm9sdmFtb3MgYSBtaXJhciBsb3MgY29ycmVsb2dyYW1hczoNCg0KYGBge3J9DQpnZ0FjZihzYWxpZGFzLnRzLCBsYWcubWF4ID0gNDgpDQoNCmdnUGFjZihzYWxpZGFzLnRzLCBsYWcubWF4ID0gNDgpDQoNCmBgYA0KDQpFbiBlbCBBQ0YgdmVtb3MgcXVlIGxvcyBjb2VmaWNpZW50ZXMgZGVjYWVuIGxlbnRhbWVudGUsIGxvIGN1YWwgZXMgdW4gaW5kaWNhdGl2byBkZSBxdWUgbGEgc2VyaWUgbm8gZXMgZXN0YWNpb25hcmlhLCBzaW4gZW1iYXJnbywgcGFyYSBjb25maXJtYXJsbyBmb3JtYWxtZW50ZSBoYXJlbW9zIGVsIHRlc3QgZGUgcmHDrXogdW5pdGFyaWEgZGUgQXVnbWVudGVkIERpY2tleS1GdWxsZXI6DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KYWRmLnRlc3Qoc2FsaWRhcy50cykNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVsIHAtdmFsb3IgZGUgbGEgcHJ1ZWJhICgwLjAxMjAzKSBlcyBtZW5vciBhbCBuaXZlbCBkZSBzaWduaWZpY2FuY2lhIGRlIDAuMDUsIHBvciBsbyBxdWUsIGxhIHBydWViYSBub3MgaW5kaWNhIHF1ZSBsYSBzZXJpZSBlcyBlc3RhY2lvbmFyaWEuIENvbnRyYXJpbyBhIGxvIHF1ZSBub3MgbW9zdHJhYmEgZWwgY29ycmVsb2dyYW1hLCBvIHNlYSwgYWwgbm8gdGVuZXIgcmHDrXogdW5pdGFyaWEgbGEgc2VyaWUgbm8gcHJlc2VudGEgdW4gcGF0csOzbiBzaXN0ZW3DoXRpY28gaW1wcmVkZWNpYmxlLiBTaW4gZW1iYXJnbywgdmFtb3MgYSBkZXRlcm1pbmFyIGNvbiBhcG95byBkZSB1biBncsOhZmljbyB5IHVuYSByZWdyZXNpw7NuIHNpIGRlYmVtb3MgaGFjZXIgYWxndW5hIHRyYW5zZm9ybWFjacOzbiBhIGxvcyBkYXRvcywgeWEgcXVlIGxhIGdyw6FmaWNhIGVuIG5pdmVsZXMgZGUgbGEgc2VyaWUgc3VnaWVyZSBlc3RhYmlsaXphciBsYSBzZXJpZS4gU2Vyw6EgbGEgZ3LDoWZpY2EgY29ub2NpZGEgY29tbyByYW5nby9tZWRpYSwgcXVlIGNvbnNpc3RlIGVuIGNhbGN1bGFyIGxhIG1lZGlhIGRlIGNhZGEgcGVyaW9kbyAoYcOxbykgeSBsYSB2YXJpYW56YS4gUG9zdGVyaW9ybWVudGUsIHNlIGhhY2UgdW5hIHJlZ3Jlc2nDs24gY29uIGFtYmFzIHZhcmlhYmxlcyB5IHNpIGVzIHNpZ25pZmljYXRpdm8gZWwgY29lZmljaWVudGUgYXNvY2lhZG8gY29uIGxhIG1lZGlhIGVudG9uY2VzIGhheSBxdWUgYXBsaWNhciBhbGd1bmEgdHJhbnNmb3JtYWNpw7NuIGEgbGEgc2VyaWU6DQo8L2Rpdj4NCg0KYGBge3J9DQp0YWJsYTIgPC0gc2FsaWRhcyAlPiUgDQogIG11dGF0ZSgNCiAgICBBw7FvID0geWVhcihGZWNoYSksDQogICAgTWVzID0gbW9udGgoRmVjaGEpDQogICkgJT4lIA0KICBzZWxlY3QoLUZlY2hhKSAlPiUgDQogIGdyb3VwX2J5KEHDsW8pICU+JSANCiAgc3VtbWFyaXNlKGxuX01lZGlhID0gbG9nKG1lYW4oU2FsaWRhcykpLA0KICAgICAgICAgbG5fdmFyaWFuemEgPSBsb2codmFyKFNhbGlkYXMpKSkNCg0KZ2dwbG90KHRhYmxhMiwgYWVzKHggPSBsbl9NZWRpYSwgeSA9IGxuX3ZhcmlhbnphKSkgKw0KICBnZW9tX3BvaW50KCkgKw0KICBsYWJzKHRpdGxlID0gIkdyw6FmaWNvIHJhbmdvLW1lZGlhIikNCg0KY29lZnRlc3QobG0obG5fdmFyaWFuemEgfiBsbl9NZWRpYSwgZGF0YSA9IHRhYmxhMikpDQpgYGANCg0KRW4gZWwgZ3LDoWZpY28gc2UgYXByZWNpYSBxdWUgbG9zIHByaW1lcm9zIGHDsW9zIGVzdMOhbiBqYWxhbmRvIGxhIHRlbmRlbmNpYSBkZSBsYSBudWJlIGRlIHB1bnRvcywgYWRlbcOhcywgZWwgcmVzdWx0YWRvIGRlIGxhIHJlZ3Jlc2nDs24gbm9zIGluZGljYSBxdWUgZGViZW1vcyBoYWNlciBhbGd1bmEgdHJhbnNmb3JtYWNpw7NuIGEgbGEgc2VyaWUsIHlhIHF1ZSBlbCBjb2VmaWNpZW50ZSBhc29jaWFkbyBjb24gbGEgbWVkaWEgcmVzdWx0w7Mgc2lnbmlmaWNhdGl2by4NCg0KIyMgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5EZXRlY2Npw7NuIGRlIGVzdGFjaW9uYWxpZGFkPC9zcGFuPg0KJm5ic3A7DQoNCkFob3JhLCBwYXNlbW9zIGFsIGFuw6FsaXNpcyBkZSBsYSBlc3RhY2lvbmFsaWRhZCBlbXBlemFuZG8gcG9yIGVsIGFuw6FsaXNpcyB2aXN1YWw6DQoNCmBgYHtyfQ0Kc2FsaWRhcyAlPiUgDQogIG11dGF0ZSgNCiAgICBBw7FvID0geWVhcihGZWNoYSksDQogICAgTWVzID0gbW9udGgoRmVjaGEpDQogICkgJT4lIA0KICBzZWxlY3QoLUZlY2hhKSAlPiUgDQogIGdyb3VwX2J5KEHDsW8sIE1lcykgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBmYWN0b3IoTWVzKSwgeSA9IFNhbGlkYXMpKSArDQogIGdlb21fYm94cGxvdCgpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgc3RhdF9zdW1tYXJ5KGZ1biA9IG1lYW4sIA0KICAgICAgICAgICAgICAgZ2VvbSA9ICJwb2ludCIsDQogICAgICAgICAgICAgICBzaGFwZSA9IDIwLCANCiAgICAgICAgICAgICAgIHNpemUgPSA0LCANCiAgICAgICAgICAgICAgIGNvbG9yID0gInJlZCIsDQogICAgICAgICAgICAgICBmaWxsID0gInJlZCIpICsNCiAgbGFicyh0aXRsZSA9ICJCb3hwbG90IE1lc2VzIiwNCiAgICAgICB4ID0gIk1lc2VzIikNCg0KZ2dzdWJzZXJpZXNwbG90KHNhbGlkYXMudHMpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicyh0aXRsZSA9ICJTYWxpZGFzIFByb21lZGlvIiwgeCA9ICJNZXNlcyIsIHkgPSAiU2FsaWRhcyIpDQpgYGANCg0KQWwgdmVyIGVsIGJveHBsb3QgeSBsYSBncsOhZmljYSBkZSBwcm9tZWRpb3MgbWVuc3VhbGVzIHBhcmVjZSBxdWUgc2kgaGF5IGRpZmVyZW5jaWFzIGVudHJlIGxvcyBtZXNlcyBlbiBjYWRhIGHDsW8geSwgcG9yIGxvIHRhbnRvLCBwcmVzZW5jaWEgZGUgdW4gcGF0csOzbiBlc3RhY2lvbmFsLiBTaWdhbW9zIGNvbiBlbCBhbsOhbGlzaXMgZGUgbGEgc2VyaWU6DQoNCmBgYHtyfQ0KZ2dzZWFzb25wbG90KHNhbGlkYXMudHMpICsgDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiU2FsaWRhcyBwb3IgTWVzIiwgeSA9ICJTYWxpZGFzIiwgeCA9ICJNZXMiKQ0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KTGFzIGdyw6FmaWNhcyB2aXN0YXMgaGFzdGEgYXF1w60gbm9zIGluZGljYW4gcXVlIGVzIGVuIGVsIG1lcyBkZSBqdWxpbyBkb25kZSBsYSBhZmx1ZW5jaWEgZGUgcGFzYWplcm9zIGVzIG1heW9yLCBsbyBjdWFsIGNvaW5jaWRlIGNvbiBlbCBwZXJpb2RvIHZhY2FjaW9uYWwgZGUgdmVyYW5vLiBUYW1iacOpbiwgc2UgYXByZWNpYSBxdWUgZWwgc2VndW5kbyBtZXMgbcOhcyBhbHRvIGRlIHNhbGlkYXMgZXMgZGljaWVtYnJlIGNvbW8gZXJhIGRlIGVzcGVyYXJzZSBwb3IgbGEgdGVtcG9yYWRhIG5hdmlkZcOxYS4NCjwvZGl2Pg0KDQpTaWdhbW9zIHZpZW5kbyBhbGd1bmFzIHZpc3RhcyBtw6FzIHBhcmEgY29uZmlybWFyIGVsIHBhdHLDs24gZXN0YWNpb25hbCBleGlzdGVudGU6DQoNCmBgYHtyfQ0KdHNfc2Vhc29uYWwoc2FsaWRhcy50cywgdHlwZSA9ICJhbGwiLCB0aXRsZSA9ICJHcsOhZmljb3MgY29uIGxhIFRlbmRlbmNpYSIpDQpgYGANCg0KRWwgcGF0csOzbiBlc3RhY2lvbmFsIGVzIGVsIG1pc21vIGEgbG8gbGFyZ28gZGVsIHRpZW1wbzogYWx0YSBkZW1hbmRhIGVuIGVsIG1lcyBkZSBqdWxpbyB5IGRpY2llbWJyZSB5IGJhamEgZGVtYW5kYSBlbiBmZWJyZXJvIHkgc2VwdGllbWJyZS4NCg0KUGFyYSB0ZW5lciB1bmEgdmlzacOzbiBtw6FzIGNsYXJhIGRlbCBwYXRyw7NuIGVzdGFjaW9uYWwgdmFtb3MgYSBxdWl0YXIgZWwgZWZlY3RvIGRlIGxhIHRlbmRlbmNpYSwgeWEgcXVlIGxhIHNlcmllIGNyZWNlIGHDsW8gdHJhcyBhw7FvIGRlc2RlIGVsIGHDsW8gMjAxMToNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0V9DQp0c19zZWFzb25hbChzYWxpZGFzLnRzIC0gZGVjb21wb3NlKHNhbGlkYXMudHMpJHRyZW5kLCANCiAgICAgICAgICAgIHR5cGUgPSAiYWxsIiwgDQogICAgICAgICAgICB0aXRsZSA9ICJHcsOhZmljbyBzaW4gVGVuZGVuY2lhIikNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NClNpbiBlbCBlZmVjdG8gZGUgbGEgdGVuZGVuY2lhIHNlIGFwcmVjaWEgbcOhcyBjbGFyYW1lbnRlIGVsIHBhdHLDs24gZXN0YWNpb25hbC4gUG9kZW1vcyBpZGVudGlmaWNhciBxdWUganVsaW8sIGFnb3N0byB5IGRpY2llbWJyZSBzb24gbG9zIG1lc2VzIGRlIG1heW9yZXMgc2FsaWRhcyBvIHZ1ZWxvcywgcG9yIGVsIGNvbnRyYXJpbywgZW5lcm8sIGZlYnJlcm8geSBzZXB0aWVtYnJlIGxvcyBtZXNlcyBkZSBtZW5vciBkZW1hbmRhIG8gc2FsaWRhcyBkZSBwYXNhamVyb3MuDQo8L2Rpdj4NCg0KT3RyYSB2aXN0YSBkaWZlcmVudGUgZGUgbGEgc2VyaWUgZXMgbWVkaWFudGUgdW4gaGVhdG1hcDoNCg0KYGBge3J9DQp0c19oZWF0bWFwKHNhbGlkYXMudHMsIHRpdGxlID0gIlNhbGlkYXMiKQ0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KU2UgYXByZWNpYSBxdWUgbGEgZGVtYW5kYSBkZSBwYXNhamVyb3MgcXVlIHNhbGUgZGUgdmlhamUgeWEgc2VhIGFsIGludGVyaW9yIGRlbCBwYcOtcyBvIGFsIGV4dHJhbmplcm8sIGRlc2RlIGxhIENpdWRhZCBkZSBNw6l4aWNvLCBoYSBlc3RhZG8gY3JlY2llbmRvIGRlIG1hbmVyYSBzb3N0ZW5pZGEgZGVzZGUgbG9zIMO6bHRpbW9zIDggYcOxb3MuIEFkZW3DoXMsIHNlIGNvbmZpcm1hIHF1ZSBqdWxpbyBlcyBlbCBtZXMgY29uIG1heW9yIGRlbWFuZGEgc2VndWlkbyBwb3IgZWwgbWVzIGRlIGRpY2llbWJyZS4NCjwvZGl2Pg0KDQpBaG9yYSwgZWwgc2lndWllbnRlIHBhc28gZXMgaWRlbnRpZmljYXIgZWwgbml2ZWwgZGUgY29ycmVsYWNpw7NuIGVudHJlIGxhIHNlcmllIHkgc3VzIHJlemFnb3M6DQoNCmBgYHtyfQ0KdHNfY29yKHNhbGlkYXMudHMsIGxhZy5tYXggPSA0OCkNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkNvbW8gZXJhIGRlIGVzcGVyYXJzZSBzw60gaGF5IHVuYSBjb3JyZWxhY2nDs24gZW50cmUgbGEgc2VyaWUgeSBzdXMgcmV6YWdvcyBlc3RhY2lvbmFsZXMuIEVsIHJldHJhc28gZXN0YWNpb25hbCAobyByZXRyYXNvIDEyKSBkZSBsYSBzZXJpZSB0aWVuZSB1bmEgZnVlcnRlIHJlbGFjacOzbiBsaW5lYWwgY29uIGxhIHNlcmllLiBMbyBhbnRlcmlvciwgbG8gcG9kZW1vcyB2ZXIgbcOhcyBjbGFyYW1lbnRlIGFsIGdyYWZpY2FyIGVzdG9zIGNvbXBvbmVudGVzOg0KPC9kaXY+DQoNCmBgYHtyfQ0KdHNfbGFncyhzYWxpZGFzLnRzLCBsYWdzID0gYygxMiwgMjQsIDM2LCA0OCkpDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpTb2xvIG5vcyBxdWVkYSBpZGVudGlmaWNhciBmb3JtYWxtZW50ZSBsYSBlc3RhY2lvbmFsaWRhZC4gUGFyYSBlbGxvIHZhbW9zIGEgY3JlYXIgdmFyaWFibGVzIGR1bW1pZXMsIHVuYSBwb3IgY2FkYSBtZXMgcGFyYSwgcG9zdGVyaW9ybWVudGUsIGhhY2VyIHVuYSByZWdyZXNpw7NuIHNpbiBpbmNsdWlyIGxhIGNvbnN0YW50ZSBwYXJhIGV2aXRhciBsYSBjb2xpbmVhbGlkYWQuIFBhcmEgcXVlIGVsIGVmZWN0byBlc3RhY2lvbmFsIGVzdGUgcHJlc2VudGUgbG9zIHBhcsOhbWV0cm9zIGVzdGltYWRvcyBkZWwgbW9kZWxvIGRlYmVuIHNlciBlc3RhZMOtc3RpY2FtZW50ZSBkaXN0aW50b3MgZGUgY2VybywgaW5kaXZpZHVhbG1lbnRlLg0KPC9kaXY+DQoNCmBgYHtyfQ0KdGFibGEzIDwtIHNhbGlkYXMgJT4lIA0KICBtdXRhdGUoDQogICAgTWVzID0gbW9udGgoRmVjaGEpDQogICkgJT4lIA0KICBtdXRhdGUoDQogICAgRW5lcm8gPSBpZmVsc2UoTWVzID09IDEsIDEsMCksDQogICAgRmVicmVybyA9IGlmZWxzZShNZXMgPT0gMiwgMSwwKSwNCiAgICBNYXJ6byA9IGlmZWxzZShNZXMgPT0gMywgMSwwKSwNCiAgICBBYnJpbCA9IGlmZWxzZShNZXMgPT0gNCwgMSwwKSwNCiAgICBNYXlvID0gaWZlbHNlKE1lcyA9PSA1LCAxLDApLA0KICAgIEp1bmlvID0gaWZlbHNlKE1lcyA9PSA2LCAxLDApLA0KICAgIEp1bGlvID0gaWZlbHNlKE1lcyA9PSA3LCAxLDApLA0KICAgIEFnb3N0byA9IGlmZWxzZShNZXMgPT0gOCwgMSwwKSwNCiAgICBTZXB0aWVtYnJlID0gaWZlbHNlKE1lcyA9PSA5LCAxLDApLA0KICAgIE9jdHVicmUgPSBpZmVsc2UoTWVzID09IDEwLCAxLDApLA0KICAgIE5vdmllbWJyZSA9IGlmZWxzZShNZXMgPT0gMTEsIDEsMCksDQogICAgRGljaWVtYnJlID0gaWZlbHNlKE1lcyA9PSAxMiwgMSwwKQ0KICApICU+JSANCiAgZHBseXI6OnNlbGVjdCgtRmVjaGEsIC1NZXMpDQoNCm1vZGVsbyA8LSBsbShTYWxpZGFzIH4gMCArLiwgZGF0YSA9IHRhYmxhMykNCnN1bW1hcnkobW9kZWxvKQ0KYGBgDQoNCkxvcyBjb2VmaWNpZW50ZXMgcmVzdWx0YXJvbiBzaWduaWZpY2F0aXZvcywgcG9yIGxvIHF1ZSwgY29uZmlybWFtb3MgbGEgcHJlc2VuY2lhIGRlIGVzdGFjaW9uYWxpZGFkIGVuIGxhIHNlcmllLg0KDQpZYSBwYXJhIHRlcm1pbmFyIGVzdGEgc2VjY2nDs24sIGNhbGN1bGFyZW1vcyBsb3Mgw61uZGljZXMgZGUgdmFyaWFjacOzbiBlc3RhY2lvbmFsIHBhcmEgZGFybm9zIHVuYSBpZGVhIGRlbCBpbXBhY3RvIGVuIGNhZGEgbWVzLiBQYXJhIGxvIGN1YWwsIGhhcmVtb3MgbG8gc2lndWllbnRlOg0KDQoqIHNlIGRlc2NvbXBvbmUgbGEgc2VyaWUuDQoqIHNlIGRpdmlkZSBsYSBzZXJpZSBvcmlnaW5hbCBlbnRyZSBzdSB0ZW5kZW5jaWEuDQoqIHNlIHByb21lZGlhbiBsb3MgcGVyaW9kb3MuDQoqIHNlIGFqdXN0YW4gbG9zIGluZGljZXMgcGFyYSBxdWUgc3VtZW4gZWwgdG90YWwgZGUgcGVyaW9kb3MgKHbDrWEgcG9uZGVyYWNpb25lcykuDQoNCmBgYHtyfQ0KZGMgPC0gZGVjb21wb3NlKHNhbGlkYXMudHMsIHR5cGUgPSAiYWRkaXRpdmUiKQ0KDQpzYWxpZGFzICU+JSANCiAgbXV0YXRlKA0KICAgIHNlcmllID0gZGMkeCwNCiAgICB0ZW5kZW5jaWEgPSBkYyR0cmVuZCwNCiAgICBBw7FvID0geWVhcihGZWNoYSksDQogICAgTWVzID0gbW9udGgoRmVjaGEpLA0KICAgIHYxID0gc2VyaWUvdGVuZGVuY2lhDQogICkgJT4lIA0KICBncm91cF9ieShNZXMpICU+JSANCiAgc3VtbWFyaXNlKGllID0gbWVhbih2MSwgbmEucm0gPSBUKSkgJT4lIA0KICBrYmwoYWxpZ24gPSAiYyIsIGRpZ2l0cyA9IDQpICU+JSBrYWJsZV9wYXBlcigiaG92ZXIiLCBmdWxsX3dpZHRoID0gRikNCmBgYA0KDQpMb3MgcmVzdWx0YWRvcyBub3MgaW5kaWNhbiBxdWUgZW4gZWwgbWVzIGRlIGp1bGlvIGxhIGRlbWFuZGEgZXMgbWF5b3IgYWwgcHJvbWVkaW8gYW51YWwgdW4gMTclLCBhcHJveGltYWRhbWVudGUsIHBvciBlbCBjb250cmFyaW8sIGVuIGVsIG1lcyBkZSBmZWJyZXJvIGxhIGRlbWFuZGEgZGUgc2FsaWRhcyBlcyAxNiUgbWVub3IgYWwgcHJvbWVkaW8gYW51YWwsIGVuIG90cmFzIHBhbGFicmFzLCBlbiBsb3MgbWVzZXMgZGUgZmVicmVybywgZW4gcHJvbWVkaW8sICBzb2xvIHNlIGxsZWdhIGFsIDg0JSBkZWwgcHJvbWVkaW8gYW51YWwuDQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5UcmFuc2Zvcm1hY2lvbmVzPC9zcGFuPg0KJm5ic3A7DQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KVmltb3MgcXVlIGxhIHNlcmllIGVuIG5pdmVsZXMgdGllbmUgdGVuZGVuY2lhIGNyZWNpZW50ZSB5IGRlIHRpcG8gYWRpdGl2YSwgcG9ycXVlIGxvcyBwaWNvcyBlbiBjYWRhIGHDsW8gbm8gYXVtZW50YW4uIFBvciBsbyBhbnRlcmlvciwgc2UgdGllbmUgcXVlIGFwbGljYXIgYWxndW5hIHRyYW5zZm9ybWFjacOzbiB5IGxhIG3DoXMgY29tw7puIHNvbiBsYXMgZGlmZXJlbmNpYXMuDQoNCkhheSBxdWUgaWRlbnRpZmljYXIgY3XDoW50YXMgZGlmZXJlbmNpYWNpb25lcyBhcGxpY2FyIGEgbGEgc2VyaWUuIFNlIHB1ZWRlIGhhY2VyIGFwbGljYW5kbyAxIGRpZmVyZW5jaWEgeSByZXZpc2FyIGVsIGNvcnJlbG9ncmFtYSBoYXN0YSBxdWUgdmVhbW9zIGxhIHNlcmllIHNpbiB0ZW5kZW5jaWEsIHNpbiBlbWJhcmdvLCBvcHRhcmVtb3MgcG9yIHVuIGNhbWlubyBtw6FzIGRpcmVjdG8gY29uIGFwb3lvIGRlIGxhIGZ1bmNpw7NuIG5kaWZmcyBkZWwgcGFxdWV0ZSBmb3JlY2FzdCBxdWUgbm9zIGluZGljYXLDoSBjdcOhbnRhcyBkaWZlcmVuY2lhcyBhcGxpY2FyIGEgbGEgc2VyaWU6DQoNCk7Dum1lcm8gZGUgZGlmZXJlbmNpYWNpb25lcyBhIGxhIHBhcnRlIHJlZ3VsYXIgZGUgbGEgc2VyaWU6IGByIG5kaWZmcyhzYWxpZGFzLnRzKWANCg0KWWEgcXVlIGNvbmZpcm1hbW9zIHF1ZSBoYXkgZXN0YWNpb25hbGlkYWQgZW4gbGEgc2VyaWUgdGVuZW1vcyBxdWUgZGV0ZXJtaW5hciBjdcOhbnRhcyBkaWZlcmVuY2lhcyBhcGxpY2FyLiBOb3MgYXBveWFyZW1vcyBkZSBsYSBmdW5jacOzbiBuc2RpZmZzIGRlbCBwYXF1ZXRlIGZvcmVjYXN0IHF1ZSBub3MgaW5kaWNhIGVsIG9yZGVuIGRlIGRpZmVyZW5jaWFjacOzbiAoZXN0byB0YW1iaWVuIHNlIHB1ZWRlIGhhY2VyIG1hbnVhbG1lbnRlIGRpZmVyZW5jaWFuZG8gbGEgc2VyaWUgZW4gbG9zIHBlcmlvZG9zIGVzdGFjaW9uYWxlcyB5IHJldmlzYW5kbyBlbCBjb3JyZWxvZ3JhbWEpOg0KDQpOw7ptZXJvIGRlIGRpZmVyZW5jaWFjaW9uZXMgYSBsYSBwYXJ0ZSByZWd1bGFyIGRlIGxhIHNlcmllOiBgciBuc2RpZmZzKHNhbGlkYXMudHMpYA0KDQpQb3IgbG8gdGFudG8sIGhheSBxdWUgZGlmZXJlbmNpYXIgMSB2ZXogZW4gbGEgcGFydGUgcmVndWxhciB5IDEgdmV6IGEgbGEgcGFydGUgZXN0YWNpb25hbCBkZSBsYSBzZXJpZSBlbiBuaXZlbGVzLiBFc3RvIHNlIGVzcGVjaWZpY2Fyw6EgZW4gZWwgYWxnb3JpdG1vIGRlbCBtb2RlbG8uDQo8L2Rpdj4NCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8geSBwcnVlYmE8L3NwYW4+DQombmJzcDsNCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpBbnRlcyBkZSBnZW5lcmFyIGVsIHByaW1lciBtb2RlbG8sIGVzIG5lY2VzYXJpbyBkaXZpZGlyIGxhIHNlcmllIGVuIHVuYSBwYXJ0ZSBwYXJhIGVudHJlbmFyIGVsIG1vZGVsbyB5IG90cmEgcGFydGUgcGFyYSB2YWxpZGFybG8uIEN1YW5kbyBzZSB0cmFiYWphIGNvbiBzZXJpZXMgZGUgdGllbXBvIGRpY2hhIHBhcnRpY2nDs24gbm8gc2UgaGFjZSBkZSBtYW5lcmEgYWxlYXRvcmlhIGNvbW8gcGFyYSBjdWFscXVpZXIgb3RybyBtb2RlbG8gZGUgbWFjaGluZSBsZWFybmluZywgeWEgcXVlIGxhcyBzZXJpZXMgZGUgdGllbXBvIHZhbiBvcmRlbmFkYXMgZGUgbWFuZXJhIGNyb25vbMOzZ2ljYSwgcG9yIGxvIHRhbnRvLCBlbCBjb25qdW50byBkZSB0ZXN0IHNpZW1wcmUgc2Vyw6EgbGEgcGFydGUgZmluYWwgZGUgbGEgc2VyaWUuIENvbnNpZGVyYXJlbW9zIGVsIGHDsW8gMjAxOSBwYXJhIHByb2JhciBlbCBtb2RlbG8geSBlbCByZXN0byBwYXJhIGVudHJlbmFyIGVsIG1vZGVsby4NCjwvZGl2Pg0KDQpgYGB7cn0NCnNhbGlkYXMudHMudHJhaW4gPC0gd2luZG93KHNhbGlkYXMudHMsIHN0YXJ0ID0gYygyMDA4LDEpLCBlbmQgPSBjKDIwMTgsMTIpKQ0Kc2FsaWRhcy50cy50ZXN0IDwtIHdpbmRvdyhzYWxpZGFzLnRzLCBzdGFydCA9IGMoMjAxOSwxKSwgZW5kID0gYygyMDE5LDEyKSkNCmBgYA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+Q3JlYWNpw7NuIGRlbCBtb2RlbG8gYmFzZTwvc3Bhbj4NCiZuYnNwOw0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkN1YW5kbyBlbCBvYmpldGl2byBkZSB1biBwcm95ZWN0byBzZWEgZWwgcHJvbm9zdGljYXIgdW5hIHNlcmllIGRlIHRpZW1wbywgZXMgYWx0YW1lbnRlIHJlY29tZW5kYWJsZSBoYWNlciB1biBwcmltZXIgbW9kZWxvIHF1ZSBzZXLDoSBsYSBiYXNlIGEgcGFydGlyIGRlbCBjdWFsIHNlIHRyYXRhcsOhIGRlIG1lam9yYXJsby4gSGF5IG11Y2hhcyBtw6l0cmljYXMgcGFyYSBldmFsdWFyIGVzdGUgdGlwbyBkZSBtb2RlbG9zLCBsYSBtYXlvcsOtYSByZWxhY2lvbmFkYXMgY29uIGxvcyBlcnJvcmVzLCBvIHNlYSwgbGEgZGlmZXJlbmNpYSBlbnRyZSBlbCB2YWxvciByZWFsIHkgZWwgdmFsb3IgcHJvbm9zdGljYWRvLiBBaG9yYSwgbG8gcXVlIHNlIGRlYmUgZXZhbHVhciBlcyBlbCBkYXRhc2V0IGRlIHRlc3QsIG8gc2VhLCBsYSBwYXJ0ZSBkZWwgY29uanVudG8gZGUgZGF0b3MgcXVlIGVsIG1vZGVsbyBubyBoYSB2aXN0by4gRW4gZWwgcHJlc2VudGUgYW7DoWxpc2lzIHVzYXJlbW9zIGVsIE1lYW4gQWJzb2x1dGUgUGVyY2VudGFnZSBFcnJvciAoTUFQRSksIHlhIHF1ZSBlcyBtw6FzIGbDoWNpbCBkZSBpbnRlcnByZXRhci4NCg0KTnVlc3RybyBwcmltZXIgbW9kZWxvIHNlcsOhIGVsIG1vZGVsbyBiw6FzaWNvIGRlIHN1YXZpemFkbyBleHBvbmVuY2lhbCBldHMsIHF1ZSBlc3TDoSBmb3JtYWRvIHBvcjogZWwgY29tcG9uZW50ZSBkZSBlcnJvciwgZWwgY29tcG9uZW50ZSBkZSB0ZW5kZW5jaWEgeSBlbCBjb21wb25lbnRlIGVzdGFjaW9uYWwuDQo8L2Rpdj4NCg0KIyAqKkVUUyoqOiAqKkUqKnhwb25lbioqVCoqaWFsICoqUyoqbW9vdGhpbmcNCg0KYGBge3J9DQpmaXQuZXRzIDwtIGV0cyhzYWxpZGFzLnRzLnRyYWluLA0KICAgICAgICAgICAgICAgbW9kZWwgPSAiWlpaIiwNCiAgICAgICAgICAgICAgIGRhbXBlZCA9IFQsDQogICAgICAgICAgICAgICBhZGRpdGl2ZS5vbmx5ID0gVCwNCiAgICAgICAgICAgICAgIGxhbWJkYSA9ICJhdXRvIg0KICAgICAgICAgICAgICAgKQ0KDQoNCmZpdC5ldHMNCg0KYGBgDQoNCkVsIHJlc3VsdGFkbyBkZWwgbW9kZWxvOiAoQSxBZCxBKSwgbm9zIGluZGljYSBxdWUgbW9kZWzDsyBsb3MgdHJlcyBjb21wb25lbnRlcyBkZSBmb3JtYSBhZGl0aXZhLiBWZWFtb3MgYWhvcmEgbG9zIHJlc2lkdWFsZXMsIHBhcmEgaWRlbnRpZmljYXIgc2kgZXhpc3RlIGF1dG9jb3JyZWxhY2nDs246DQoNCmBgYHtyfQ0KY2hlY2tyZXNpZHVhbHMoZml0LmV0cywgbGFnID0gMjYpDQpgYGANCg0KRWwgcC12YWxvciBkZSBsYSBwcnVlYmEgbm9zIGluZGljYSBxdWUgbG9zIGVycm9yZXMgbm8gc29uIGluZGVwZW5kaWVudGVzLCBwb3IgbG8gcXVlLCBlbCBwYXNhZG8gZXN0w6EgaW5mbHV5ZW5kbyBlbiBsbyBxdWUgc3VjZWRlIGVuIGVsIHByZXNlbnRlLiBIYWJyw6EgcXVlIGNvcnJlZ2lyIGVzdG8gbcOhcyBhZGVsYW50ZSwgcG9yIGxvIHByb250bywgc2Ugc2lndWUgYW5pYWxpemFuZG8gZXN0ZSBwcmltZXIgbW9kZWxvLg0KDQpgYGB7cn0NCnFxbm9ybShmaXQuZXRzJHJlc2lkdWFscykNCnFxbGluZShmaXQuZXRzJHJlc2lkdWFscykNCg0KamFycXVlLmJlcmEudGVzdChmaXQuZXRzJHJlc2lkdWFscykNCmBgYA0KDQpFbCBhbsOhbGlzaXMgZGUgbGEgbm9ybWFsaWRhZCBtdWVzdHJhIHF1ZSBsb3MgZXJyb3JlcyBubyBzZSBkaXN0cmlidXllbiBkZSBmb3JtYSBub3JtYWwuIFNpbiBlbWJhcmdvLCBjb21vIGVzIGVsIG1vZGVsbyBiYXNlIHNpZ2Ftb3MgYWRlbGFudGUgY29uIGxhcyBwcmVkaWNjaW9uZXMgcGFyYSBsb3Mgc2lndWllbnRlcyAxMiBtZXNlcy4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHJlc3VsdHM9ImFzaXMifQ0KZmMuZXRzIDwtIGZvcmVjYXN0KGZpdC5ldHMsIGggPSAxMikNCg0KDQpmYy5ldHMgJT4lIGFzLmRhdGEuZnJhbWUoKSAlPiUgDQogIGtibChhbGlnbiA9ICJjIiwgZm9ybWF0LmFyZ3MgPSBsaXN0KGJpZy5tYXJrID0gIiwiKSkgJT4lIA0KICBrYWJsZV9wYXBlcigiaG92ZXIiLCBmdWxsX3dpZHRoID0gRikNCg0KZm9yZWNhc3Q6OmFjY3VyYWN5KGZjLmV0cywgc2FsaWRhcy50cy50ZXN0KQ0KDQphdXRvcGxvdChmYy5ldHMpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicyh0aXRsZSA9ICJQcm9uw7NzdGljbyBNb2RlbG8gQmFzZTogZXRzKEEsQWQsQSkiLCB5ID0gIlNhbGlkYXMiKQ0KDQpgYGANCg0KRXN0ZSBwcmltZXIgbW9kZWxvIHByZXNlbnRhIHVuIE1BUEUgZGUgMi4zJSBwYXJhIGVsIGNvbmp1bnRvIGRlIHRlc3QsIG5vIGVzIG11eSBlbGV2YWRvLCBzaW4gZW1iYXJnbywgbm8gcGFzw7MgbG9zIHN1cHVlc3RvcyBkZSBpbmRlcGVuZGVuY2lhIGRlIGxvcyBlcnJvcmVzIG5pIG5vcm1hbGlkYWQuIFZlYW1vcyBjw7NtbyBzZSB2ZSBsYSBncsOhZmljYSBkZSBsYXMgcHJlZGljY2lvbmVzIGNvbiBsYSBzZXJpZSBvcmlnaW5hbC4NCg0KYGBge3J9DQp0ZXN0X2ZvcmVjYXN0KGZvcmVjYXN0Lm9iaiA9IGZjLmV0cywgDQogICAgICAgICAgICAgIGFjdHVhbCA9IHNhbGlkYXMudHMsIA0KICAgICAgICAgICAgICB0ZXN0ID0gc2FsaWRhcy50cy50ZXN0KSAlPiUgDQogIGxheW91dChsZWdlbmQgPSBsaXN0KHggPSAwLjEsIHkgPSAwLjk1KSkNCg0KYGBgDQoNCkFqdXN0ZW1vcyBlbCBtb2RlbG8gZXRzIHBhcmEgbWVqb3JhcmxvIGRpZmVyZW5jaWFuZG8gMSB2ZXogbGEgc2VyaWU6DQoNCmBgYHtyfQ0Kc2FsaWRhcy50cy5kaWZmIDwtIGRpZmYoc2FsaWRhcy50cykNCg0Kc2FsaWRhcy50cy50cmFpbi5kaWZmIDwtIHdpbmRvdyhzYWxpZGFzLnRzLmRpZmYsIHN0YXJ0ID0gYygyMDA4LDIpLCBlbmQgPSBjKDIwMTgsMTIpKQ0Kc2FsaWRhcy50cy50ZXN0LmRpZmYgPC0gd2luZG93KHNhbGlkYXMudHMuZGlmZiwgc3RhcnQgPSBjKDIwMTksMSksIGVuZCA9IGMoMjAxOSwxMikpDQoNCmZpdC5ldHMuZGlmZiA8LSBldHMoc2FsaWRhcy50cy50cmFpbi5kaWZmLA0KICAgICAgICAgICAgICAgbW9kZWwgPSAiWlpaIiwNCiAgICAgICAgICAgICAgIGRhbXBlZCA9IFQNCiAgICAgICAgICAgICAgICkNCg0KY2hlY2tyZXNpZHVhbHMoZml0LmV0cy5kaWZmKQ0KDQpxcW5vcm0oZml0LmV0cy5kaWZmJHJlc2lkdWFscykNCnFxbGluZShmaXQuZXRzLmRpZmYkcmVzaWR1YWxzKQ0KDQpqYXJxdWUuYmVyYS50ZXN0KGZpdC5ldHMuZGlmZiRyZXNpZHVhbHMpDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpFbiBlbCBjb3JyZWxvZ3JhbWEgZGUgbG9zIHJlc2lkdWFsZXMgcG9kZW1vcyB2ZXIgcXVlIGFsIGluaWNpbyBzb2xvIHVuIGNvZWZpY2llbnRlIHNhbGUgZGUgbGFzIGJhbmRhcywgc2luIGVtYmFyZ28sIGVsIHRlc3Qgbm8gbG8gcGFzYS4gRW4gY3VhbnRvIGEgbGEgcHJ1ZWJhIGRlIG5vcm1hbGlkYWQgdmVtb3MgcXVlIHlhIGxhIHBhc2EgYWwgdGVuZXIgdW4gcC12YWxvciBtYXlvciBhbCAwLjA1LiBOb3MgcXVlZGFtb3MgY29uIGVzdGUgbW9kZWxvLCBwb3IgbG8gcXVlLCBhaG9yYSBzZSBwcm9ub3N0aWNhcsOhbiBsb3MgcHLDs3hpbW9zIDEyIG1lc2VzLg0KPC9kaXY+DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KZmMuZXRzLmRpZmYgPC0gZm9yZWNhc3QoZml0LmV0cy5kaWZmLCBoID0gMTIpDQoNCmxhc3Rfc2FsaWRhcyA8LSBzYWxpZGFzLnRzLnRyYWluWzEzMl0NCnByZWQgPC0gZmMuZXRzLmRpZmYkbWVhbg0KDQpkZV9kaWZmX3NhbGlkYXNfZm9yZSA8LSBjdW1zdW0ocHJlZCkgKyBsYXN0X3NhbGlkYXMNCg0KZGRpZmZfc2ZvcmVfdHMgPC0gdHMoZGVfZGlmZl9zYWxpZGFzX2ZvcmUsIHN0YXJ0ID0gMTMzKQ0KDQpkZGlmZl9zZm9yZV90cyA8LSB0cyhkZGlmZl9zZm9yZV90cywgc3RhcnQgPSBjKDIwMTksIDEpLCBmcmVxdWVuY3kgPSAxMikNCg0KZGRpZmZfc2ZvcmVfdHMgJT4lIGFzLmRhdGEuZnJhbWUoKSAlPiUgDQogIHJlbmFtZShQcm9ub3N0aWNvID0geCkgJT4lICBtdXRhdGUoRmVjaGEgPSBzYWxpZGFzJEZlY2hhWzEzMzoxNDRdKSAlPiUgDQogIHNlbGVjdChGZWNoYSwgUHJvbm9zdGljbykgJT4lIA0KICBrYmwoYWxpZ24gPSAiYyIsIGZvcm1hdC5hcmdzID0gbGlzdChiaWcubWFyayA9ICIsIikpICU+JSANCiAga2FibGVfcGFwZXIoImhvdmVyIiwgZnVsbF93aWR0aCA9IEYpDQoNCmZvcmVjYXN0OjphY2N1cmFjeShkZGlmZl9zZm9yZV90cywgc2FsaWRhcy50cy50ZXN0KQ0KDQp0cy5wbG90KHNhbGlkYXMudHMsIGRkaWZmX3Nmb3JlX3RzLA0KICAgICAgICBsdHkgPSBjKDEsMyksIGNvbCA9IGMoInN0ZWVsYmx1ZSIsICJyZWQiKSwgDQogICAgICAgIG1haW4gPSAiUHJvbsOzc3RpY28gbW9kZWxvIGVuIDFyYXMgRGlmZXJlbmNpYXM6IGV0cyhBLEFkLEEpIiwgDQogICAgICAgIHlsYWIgPSAiU2FsaWRhcyIpDQoNCmBgYA0KDQpFbCBtb2RlbG8gY29uIGRpZmVyZW5jaWFzIG1lam9yw7MsIHlhIHF1ZSBzZSByZWR1Y2UgZWwgZXJyb3IgZGUgdGVzdCBkZSAyLjMgYSAxLjU4Lg0KDQpBaG9yYSwgb3B0aW1pemFyZW1vcyBsb3MgcGFyw6FtZXRyb3MuDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRXN0ZSBhbGdvcml0bW8gdGllbmUgdHJlcyBwYXLDoW1ldHJvcyBxdWUgZXN0w6FuIHJlbGFjaW9uYWRvcyBjb24gZWwgc3VhdmlzYWRvIChhbHBoYSksIGxhIHRlbmRlbmNpYSAoYmV0YSkgeSBsYSBlc3RhY2lvbmFsaWRhZCAoZ2FtbWEpLiBQb3IgdGFudG8sIHZhbW9zIGEgb3B0aW1pemFybG9zIHBhcmEgZW5jb250cmFyIGVsIHZhbG9yIHF1ZSBub3MgYXJyb2plIGVsIG1vZGVsbyBjb24gZWwgbWVub3IgZXJyb3IgeSB1bmEgdmV6IGVuY29udHJhZG8gZWwgdmFsb3IgZGVsIGNvbXBvbmVudGUgdm9sdmVyZW1vcyBhIHJlYWp1c3RhciBlbCBtb2RlbG8uDQo8L2Rpdj4NCg0KQWxwaGE6IGNvbXBvbmVudGUgZGUgc3VhdmlzYWRvDQoNCmBgYHtyfQ0KIyBpZGVudGlmaWNhciBlbCBhbHBoYSDDs3B0aW1vDQphbHBoYSA8LSBzZXEoLjAxLCAuOTksIGJ5ID0gLjAwMSkNCg0KTUFQRSA8LSBOQQ0KDQpmb3IoaSBpbiBzZXFfYWxvbmcoYWxwaGEpKSB7DQogIGZpdCA8LSBldHMoc2FsaWRhcy50cy50cmFpbiwgDQogICAgICAgICAgICAgYWxwaGEgPSBhbHBoYVtpXSwNCiAgICAgICAgICAgICBtb2RlbCA9ICJaWloiLA0KICAgICAgICAgICAgIGRhbXBlZCA9IFQsDQogICAgICAgICAgICAgYWRkaXRpdmUub25seSA9IFQsDQogICAgICAgICAgICAgbGFtYmRhID0gImF1dG8iKQ0KICANCiAgZmMuZml0IDwtIGZvcmVjYXN0KGZpdCwgaCA9IDEyKQ0KDQogIE1BUEVbaV0gPC0gZm9yZWNhc3Q6OmFjY3VyYWN5KGZjLmZpdCwgc2FsaWRhcy50cy50ZXN0KVsyLDVdDQp9DQoNCiMgY29udmVydGlyIGEgZGF0YSBmcmFtZSBlIGlkZW50aWZpY2FyIGVsIHZhbG9yIGRlbCBhbHBoYSBtw61uaW1vDQphbHBoYS5maXQgPC0gdGliYmxlKGFscGhhLCBNQVBFKQ0KYWxwaGEubWluIDwtIGZpbHRlcihhbHBoYS5maXQsIE1BUEUgPT0gbWluKE1BUEUpKQ0KYWxwaGEubWluDQoNCiMgZ3LDoWZpY28gTUFQRSB2cy4gYWxwaGENCmdncGxvdChhbHBoYS5maXQsIGFlcyhhbHBoYSwgTUFQRSkpICsNCiAgZ2VvbV9saW5lKCkgKw0KICBnZW9tX3BvaW50KGRhdGEgPSBhbHBoYS5taW4sIGFlcyhhbHBoYSwgTUFQRSksIHNpemUgPSAyLCBjb2xvciA9ICJibHVlIikgKw0KICBsYWJzKHRpdGxlID0gIk1lYW4gQWJzb2x1dGUgUGVyY2VudGFnZSBFcnJvciIpDQoNCmBgYA0KDQpCZXRhOiBjb21wb25lbnRlIGRlIHRlbmRlbmNpYQ0KDQpgYGB7ciwgcmVzdWx0cz0iaGlkZSJ9DQojIGlkZW50aWZpY2FyIGVsIGJldGEgw7NwdGltbw0KYmV0YSA8LSBzZXEoLjAxLCAuOTksIGJ5ID0gLjAwMSkNCg0KTUFQRSA8LSBOQQ0KDQpmb3IoaSBpbiBzZXFfYWxvbmcoYmV0YSkpIHsNCiAgZml0IDwtIGV0cyhzYWxpZGFzLnRzLnRyYWluLCANCiAgICAgICAgICAgICBiZXRhID0gYmV0YVtpXSwNCiAgICAgICAgICAgICBtb2RlbCA9ICJaWloiLA0KICAgICAgICAgICAgIGRhbXBlZCA9IFQsDQogICAgICAgICAgICAgYWRkaXRpdmUub25seSA9IFQsDQogICAgICAgICAgICAgbGFtYmRhID0gImF1dG8iKQ0KICANCiAgZmMuZml0IDwtIGZvcmVjYXN0KGZpdCwgaCA9IDEyKQ0KICANCiAgTUFQRVtpXSA8LSBmb3JlY2FzdDo6YWNjdXJhY3koZmMuZml0LCBzYWxpZGFzLnRzLnRlc3QpWzIsNV0NCn0NCg0KYGBgDQoNCmBgYHtyfQ0KIyBjb252ZXJ0aXIgYSBkYXRhIGZyYW1lIGUgaWRlbnRpZmljYXIgZWwgdmFsb3IgZGVsIGJldGEgbcOtbmltbw0KYmV0YS5maXQgPC0gdGliYmxlKGJldGEsIE1BUEUpDQpiZXRhLm1pbiA8LSBmaWx0ZXIoYmV0YS5maXQsIE1BUEUgPT0gbWluKE1BUEUpKQ0KYmV0YS5taW4NCg0KIyBncsOhZmljbyBNQVBFIHZzLiBiZXRhDQpnZ3Bsb3QoYmV0YS5maXQsIGFlcyhiZXRhLCBNQVBFKSkgKw0KICBnZW9tX2xpbmUoKSArDQogIGdlb21fcG9pbnQoZGF0YSA9IGJldGEubWluLCBhZXMoYmV0YSwgTUFQRSksIHNpemUgPSAyLCBjb2xvciA9ICJibHVlIikgKw0KICBsYWJzKHRpdGxlID0gIk1lYW4gQWJzb2x1dGUgUGVyY2VudGFnZSBFcnJvciIpDQoNCmBgYA0KDQpHYW1tYTogY29tcG9uZW50ZSBkZSBlc3RhY2lvbmFsaWRhZA0KDQpgYGB7cn0NCiMgaWRlbnRpZmljYXIgZWwgZ2FtbWEgw7NwdGltbw0KZ2FtbWEgPC0gc2VxKC4wMSwgLjk5LCBieSA9IC4wMDEpDQoNCk1BUEUgPC0gTkENCg0KZm9yKGkgaW4gc2VxX2Fsb25nKGdhbW1hKSkgew0KICBmaXQgPC0gZXRzKHNhbGlkYXMudHMudHJhaW4sIA0KICAgICAgICAgICAgIGdhbW1hID0gZ2FtbWFbaV0sDQogICAgICAgICAgICAgbW9kZWwgPSAiWlpaIiwNCiAgICAgICAgICAgICBkYW1wZWQgPSBULA0KICAgICAgICAgICAgIGFkZGl0aXZlLm9ubHkgPSBULA0KICAgICAgICAgICAgIGxhbWJkYSA9ICJhdXRvIikNCiAgDQogIGZjLmZpdCA8LSBmb3JlY2FzdChmaXQsIGggPSAxMikNCiAgDQogIE1BUEVbaV0gPC0gZm9yZWNhc3Q6OmFjY3VyYWN5KGZjLmZpdCwgc2FsaWRhcy50cy50ZXN0KVsyLDVdDQp9DQoNCiMgY29udmVydGlyIGEgZGF0YSBmcmFtZSBlIGlkZW50aWZpY2FyIGVsIHZhbG9yIGRlbCBnYW1tYSBtw61uaW1vDQpnYW1tYS5maXQgPC0gdGliYmxlKGdhbW1hLCBNQVBFKQ0KZ2FtbWEubWluIDwtIGZpbHRlcihnYW1tYS5maXQsIE1BUEUgPT0gbWluKE1BUEUpKQ0KZ2FtbWEubWluDQoNCiMgZ3LDoWZpY28gTUFQRSB2cy4gZ2FtbWENCmdncGxvdChnYW1tYS5maXQsIGFlcyhnYW1tYSwgTUFQRSkpICsNCiAgZ2VvbV9saW5lKCkgKw0KICBnZW9tX3BvaW50KGRhdGEgPSBnYW1tYS5taW4sIGFlcyhnYW1tYSwgTUFQRSksIHNpemUgPSAyLCBjb2xvciA9ICJibHVlIikgKw0KICBsYWJzKHRpdGxlID0gIk1lYW4gQWJzb2x1dGUgUGVyY2VudGFnZSBFcnJvciIpDQoNCmBgYA0KDQpBbCBvcHRpbWl6YXIgbG9zIHRyZXMgY29tcG9uZW50ZXMgZWwgcXVlIGdlbmVyw7MgZWwgbWVub3IgZXJyb3IgZnVlIGFscGhhIGNvbiAxLjUzLiBTZSByZWFqdXN0YSBkZWwgbW9kZWxvIGNvbiBlc2UgcGFyw6FtZXRybzoNCg0KYGBge3J9DQojIHJlYWp1c3RlIGRlbCBtb2RlbG8gY29uIGFscGhhIG9wdGltbw0KZml0LmV0cyA8LSBldHMoc2FsaWRhcy50cy50cmFpbiwNCiAgICAgICAgICAgICAgIG1vZGVsID0gIlpaWiIsDQogICAgICAgICAgICAgICBkYW1wZWQgPSBULA0KICAgICAgICAgICAgICAgYWRkaXRpdmUub25seSA9IFQsDQogICAgICAgICAgICAgICBsYW1iZGEgPSAiYXV0byIsDQogICAgICAgICAgICAgICBhbHBoYSA9IGFscGhhLm1pbiRhbHBoYQ0KICAgICAgICAgICAgICAgKQ0KDQpmaXQuZXRzDQoNCmBgYA0KDQpgYGB7cn0NCmNoZWNrcmVzaWR1YWxzKGZpdC5ldHMsIGxhZyA9IDI2KQ0KYGBgDQoNCmBgYHtyfQ0KcXFub3JtKGZpdC5ldHMkcmVzaWR1YWxzKQ0KcXFsaW5lKGZpdC5ldHMkcmVzaWR1YWxzKQ0KDQpqYXJxdWUuYmVyYS50ZXN0KGZpdC5ldHMkcmVzaWR1YWxzKQ0KYGBgDQoNCmBgYHtyfQ0KZmMuZXRzIDwtIGZvcmVjYXN0KGZpdC5ldHMsIGggPSAxMikNCg0KZm9yZWNhc3Q6OmFjY3VyYWN5KGZjLmV0cywgc2FsaWRhcy50cy50ZXN0KQ0KYGBgDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KdGVzdF9mb3JlY2FzdChmb3JlY2FzdC5vYmogPSBmYy5ldHMsIA0KICAgICAgICAgICAgICBhY3R1YWwgPSBzYWxpZGFzLnRzLCANCiAgICAgICAgICAgICAgdGVzdCA9IHNhbGlkYXMudHMudGVzdCkgJT4lIA0KICBsYXlvdXQobGVnZW5kID0gbGlzdCh4ID0gMC4xLCB5ID0gMC45NSkpDQoNCg0KYXV0b3Bsb3QoZmMuZXRzKSArIA0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKw0KICBsYWJzKHRpdGxlID0gIlByb27Ds3N0aWNvIEVUUyBTYWxpZGFzIDIwMTkiLCB5ID0gIlNhbGlkYXMiKQ0KDQpgYGANCg0KIyAqKkFSSU1BKio6ICoqQSoqdXRvKipSKiplZ3Jlc3NpdmUgKipJKipudGVncmF0ZWQgKipNKipvdmluZyAqKkEqKnZlcmFnZQ0KJm5ic3A7DQoNCkFob3JhIHNlIG9wdGFyw6EgcG9yIHVuIG1vZGVsbyBhcmltYSBwYXJhIHZlciBzaSBwb2RlbW9zIG1lam9yYXIgZWwgZXJyb3IgZGUgdGVzdCB5LCBhZGVtw6FzLCBxdWUgc3VzIHJlc2lkdWFsZXMgcGFzZW4gbGFzIHBydWViYXMgZGUgbm9ybWFsaWRhZCB5IGF1dG9jb3JyZWxhY2nDs24uIFNlIGNvbWVuemFyw6EgcG9yIGxhIGdlbmVyYWNpw7NuIGF1dG9tw6F0aWNhIGRlbCBtb2RlbG8gY29uIGFwb3lvIGRlIGxhIGZ1bmNpw7NuIGF1dG8uYXJpbWEgZGVsIHBhcXVldGUgZm9yZWNhc3Q6DQoNCmBgYHtyfQ0KZml0YXJpbWExIDwtIGF1dG8uYXJpbWEoc2FsaWRhcy50cy50cmFpbikNCmZpdGFyaW1hMQ0KDQpjb2VmdGVzdChmaXRhcmltYTEpDQoNCnN1bW1hcnkoZml0YXJpbWExKQ0KDQpgYGANCg0KRW4gZXN0ZSBwcmltZXIgbW9kZWxvIGxvcyB0cmVzIGNvZWZpY2llbnRlcyBzb24gc2lnbmlmaWNhdGl2b3MuIFNpbiBlbWJhcmdvLCBoYXkgcXVlIGhhY2VyIGxhIGRpYWdub3NpcyBhIGxvcyByZXNpZHVhbGVzOg0KDQpBdXRvY29ycmVsYWNpw7NuOg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCmNoZWNrcmVzaWR1YWxzKGZpdGFyaW1hMSkNCg0KYXV0b3Bsb3QoYWNmKGZpdGFyaW1hMSRyZXNpZHVhbHMsIHBsb3QgPSBGQUxTRSkpDQphdXRvcGxvdChwYWNmKGZpdGFyaW1hMSRyZXNpZHVhbHMsIHBsb3QgPSBGQUxTRSkpDQoNCmdndHNkaWFnKGZpdGFyaW1hMSkNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KTGEgZ3LDoWZpY2EgZGUgbG9zIHJlc2lkdWFsZXMgbm8gbXVlc3RyYSB0ZW5kZW5jaWEgbG8gY3VhbCBlcyBidWVubyB5IGVuIGxvcyBjb3JyZWxvZ3JhbWFzIHNvbG8gdW4gY29lZmljaWVudGUgc2FsZSBkZSBsb3MgbMOtbWl0ZXMuIEVsIHAtdmFsdWUgZGUgbGEgcHJ1ZWJhIExqdW5nLUJveCBlcyBtYXlvciBhbCAwLjA1LCBsbyBjdWFsIG5vcyBpbmRpY2EgcXVlIGxvcyByZXNpZHVhbGVzIG5vIGVzdMOhbiBjb3JyZWxhY2lvbmFkb3MuIEFkZW3DoXMsIGxvcyBwLXZhbG9yZXMgcGFyYSBsYSBwcnVlYmEgUSBkZSBManVuZy1Cb3ggZXN0w6FuIHBvciBlbmNpbWEgZGUgMCwwNSwgbG8gcXVlIGluZGljYSAibm8gc2lnbmlmaWNhdGl2byIuDQo8L2Rpdj4NCg0KTm9ybWFsaWRhZDoNCg0KYGBge3J9DQpxcW5vcm0oZml0YXJpbWExJHJlc2lkdWFscykNCnFxbGluZShmaXRhcmltYTEkcmVzaWR1YWxzKQ0KDQpqYXJxdWUuYmVyYS50ZXN0KGZpdGFyaW1hMSRyZXNpZHVhbHMpDQoNCmBgYA0KDQpFbCBwLXZhbHVlIGVzIG1heW9yIGFsIDAuMDUsIHBvciBsbyB0YW50bywgcG9kZW1vcyBhZmlybWFyIHF1ZSBsb3MgcmVzaWR1YWxlcyBkZSBlc3RlIG1vZGVsbyBzaWd1ZW4gdW5hIGRpc3RyaWJ1Y2nDs24gbm9ybWFsLiANCg0KRXN0ZSBtb2RlbG8gcGFzYSBsYXMgcHJ1ZWJhcyBkZSBhdXRvY29ycmVsYWNpw7NuIHkgbm9ybWFsaWRhZCwgcG9yIGxvIHF1ZSwgcG9kZW1vcyBkYXJsbyBjb21vIHbDoWxpZG8geSBlcyBhcHRvIHBhcmEgaGFjZXIgcHJvbsOzc3RpY29zOg0KDQpQcmVkaWNjacOzbiB5IEV2YWx1YWNpw7NuOg0KDQpgYGB7cn0NCmZjLmZpdGFyaW1hMSA8LSBmb3JlY2FzdChmaXRhcmltYTEsIGggPSAxMikNCmZvcmVjYXN0OjphY2N1cmFjeShmYy5maXRhcmltYTEsIHNhbGlkYXMudHMudGVzdCkNCmBgYA0KDQpFbCBlcnJvciBkZSB0ZXN0IGVzIGRlIDEuMDksIHF1ZSBlcyBtZW5vciBhbCBtZWpvciBtb2RlbG8gZXRzIHF1ZSBzZSBnZW5lcsOzIGFudGVyaW9ybWVudGUuIFZlYW1vcyBsb3MgcmVzdWx0YWRvcyBncsOhZmljYW1lbnRlOg0KDQpgYGB7cn0NCnRlc3RfZm9yZWNhc3QoZm9yZWNhc3Qub2JqID0gZmMuZml0YXJpbWExLCBhY3R1YWwgPSBzYWxpZGFzLnRzLCB0ZXN0ID0gc2FsaWRhcy50cy50ZXN0KSAlPiUgDQogIGxheW91dChsZWdlbmQgPSBsaXN0KHggPSAwLjEsIHkgPSAwLjk1KSkNCmBgYA0KDQpgYGB7cn0NCmF1dG9wbG90KGZjLmZpdGFyaW1hMSkgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicyh0aXRsZSA9ICJQcm9uw7NzdGljbyBhdXRvLmFyaW1hMSBTYWxpZGFzIDIwMTkiLCB5ID0gIlNhbGlkYXMiKQ0KYGBgDQoNCkVzdGUgc2Vyw6EgbnVlc3RybyBtb2RlbG8gYXJpbWEgYmFzZSwgeWEgcXVlIHBhc2EgbGFzIHBydWViYXMgZGUgYXV0b2NvcnJlbGFjacOzbiB5IG5vcm1hbGlkYWQgZGUgc3VzIHJlc2lkdWFsZXMuDQoNClZlYW1vcyBzaSBwb2RlbW9zIG1lam9yYXJsbyBtb2RpZmljYW5kbyBhbGd1bm9zIGRlIHN1cyBwYXLDoW1ldHJvczoNCg0KYGBge3J9DQpmaXRhcmltYTIgPC0gYXV0by5hcmltYShzYWxpZGFzLnRzLnRyYWluLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHRyYWNlID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgIHN0ZXB3aXNlID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgIGFwcHJveGltYXRpb24gPSBULA0KICAgICAgICAgICAgICAgICAgICAgICAgdGVzdCA9ICJhZGYiLA0KICAgICAgICAgICAgICAgICAgICAgICAgYWxsb3dkcmlmdCA9IFQsDQogICAgICAgICAgICAgICAgICAgICAgICBvcHRpbS5tZXRob2QgPSJCRkdTIg0KICAgICAgICAgICAgICAgICAgICAgICAgKQ0KDQpjb2VmdGVzdChmaXRhcmltYTIpDQoNCnN1bW1hcnkoZml0YXJpbWEyKQ0KDQpgYGANCg0KRW4gZXN0YSBjb3JyaWRhIGVsIG1lam9yIG1vZGVsbyBmdWUgdW4gQVJJTUEoMCwxLDEpKDIsMSwwKSwgZWwgY3VhbCB0aWVuZSB0b2RvcyBsb3MgY29lZmljaWVudGVzIHNpZ25pZmljYXRpdm9zLiBBbmFsaWNlbW9zIHN1cyByZXNpZHVhbGVzLg0KDQpBdXRvY29ycmVsYWNpw7NuOg0KDQpgYGB7cn0NCmNoZWNrcmVzaWR1YWxzKGZpdGFyaW1hMikNCg0KYXV0b3Bsb3QoYWNmKGZpdGFyaW1hMiRyZXNpZHVhbHMsIHBsb3QgPSBGQUxTRSkpDQphdXRvcGxvdChwYWNmKGZpdGFyaW1hMiRyZXNpZHVhbHMsIHBsb3QgPSBGQUxTRSkpDQoNCmdndHNkaWFnKGZpdGFyaW1hMikNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRWwgcC12YWx1ZSBkZSBsYSBwcnVlYmEgTGp1bmctQm94IGVzIG1heW9yIGFsIDAuMDUsIHBvciBsbyB0YW50bywgbG9zIHJlc2lkdWFsZXMgbm8gZXN0w6FuIGNvcnJlbGFjaW9uYWRvcywgYWRlbcOhcywgZW4gc3UgZ3LDoWZpY2Egbm8gc2UgYXByZWNpYSBuaW5nw7puIHBhdHLDs24uIFRvZG9zIGxvcyBjb2VmaWNpZW50ZSBlc3TDoW4gZGVudHJvIGRlIGxhcyBiYW5kYXMgZW4gbG9zIGNvcnJlbG9ncmFtYXMuIExvcyBwLXZhbG9yZXMgcGFyYSBsYSBwcnVlYmEgUSBkZSBManVuZy1Cb3ggZXN0w6FuIHBvciBlbmNpbWEgZGUgMCwwNSwgbG8gcXVlIGluZGljYSAibm8gc2lnbmlmaWNhdGl2byIuDQo8L2Rpdj4NCg0KTm9ybWFsaWRhZDoNCg0KYGBge3J9DQpxcW5vcm0oZml0YXJpbWEyJHJlc2lkdWFscykNCnFxbGluZShmaXRhcmltYTIkcmVzaWR1YWxzKQ0KDQpqYXJxdWUuYmVyYS50ZXN0KGZpdGFyaW1hMiRyZXNpZHVhbHMpDQoNCmBgYA0KDQpMb3MgcmVzaWR1YWxlcyBubyBwcmVzZW50YW4gYXV0b2NvcnJlbGFjacOzbiB5IHNlIGRpc3RyaWJ1eWVuIG5vcm1hbG1lbnRlLiBwb3IgdGFudG8sIHBhc2Ftb3MgYSBoYWNlciBsYXMgcHJlZGljY2lvbmVzIHBhcmEgY29ub2NlciBlbCBlcnJvciBkZSB0ZXN0Og0KDQpQcmVkaWNjacOzbiB5IEV2YWx1YWNpw7NuOg0KDQpgYGB7cn0NCmZjLmZpdGFyaW1hMiA8LSBmb3JlY2FzdChmaXRhcmltYTIsIGggPSAxMikNCmZvcmVjYXN0OjphY2N1cmFjeShmYy5maXRhcmltYTIsIHNhbGlkYXMudHMudGVzdCkNCmBgYA0KDQpBcnJvamEgdW4gZXJyb3IgZGUgMS40OSwgbGlnZXJhbWVudGUgbWF5b3IgcXVlIGVsIG1vZGVsbyBhbnRlcmlvciAoMS4wOCkNCg0KdmVhbW9zIGxhcyBncsOhZmljYXM6DQoNCmBgYHtyfQ0KdGVzdF9mb3JlY2FzdChmb3JlY2FzdC5vYmogPSBmYy5maXRhcmltYTIsIGFjdHVhbCA9IHNhbGlkYXMudHMsIHRlc3QgPSBzYWxpZGFzLnRzLnRlc3QpICU+JSANCiAgbGF5b3V0KGxlZ2VuZCA9IGxpc3QoeCA9IDAuMSwgeSA9IDAuOTUpKQ0KYGBgDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KYXV0b3Bsb3QoZmMuZml0YXJpbWEyKSArIA0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKw0KICBsYWJzKHRpdGxlID0gIlByb27Ds3N0aWNvIGF1dG8uYXJpbWEyIFNhbGlkYXMgMjAxOSIsIHkgPSAiU2FsaWRhcyIpDQoNCmBgYA0KDQpIYWdhbW9zIHVuIMO6bHRpbW8gbW9kZWxvIGFyaW1hLCBwZXJvIGFob3JhIHVzYXJlbW9zIGxhIGZ1bmNpw7NuIGFyaW1hLCBwYXJhIGNvbnN0cnVpcmxvIHBvciBwYXJ0ZXM6DQoNClByaW1lcm8gcmVjb3JkZW1vcyBsYSBmb3JtYSBkZSBsb3MgY29ycmVsb2dyYW1hcywgcHVlc3RvIHF1ZSBub3MgYXl1ZGFyw6FuIGEgZGVmaW5pciBsYSBlc3RydWN0dXJhIGRlIG1vZGVsbzoNCg0KYGBge3J9DQp0c2Rpc3BsYXkoc2FsaWRhcy50cy50cmFpbikNCmBgYA0KDQpFbXBlY2Vtb3MgcG9yIGFwbGljYXIgdW5hIGRpZmVyZW5jaWEgeSB2ZXIgZGUgbnVldm8gbG9zIGNvcnJlbG9ncmFtYXM6DQoNCmBgYHtyfQ0KdHNkaXNwbGF5KGRpZmYoc2FsaWRhcy50cy50cmFpbikpDQpgYGANCg0KQWhvcmEgY29uIDEyIHJlemFnb3MgcGFyYSB2ZXIgbGEgZXN0cnVjdHVyYSBlc3RhY2lvbmFsIGRlIGxhIHNlcmllOg0KDQpgYGB7cn0NCnRzZGlzcGxheShkaWZmKHNhbGlkYXMudHMudHJhaW4sIGxhZyA9IDEyKSkNCmBgYA0KDQpDb24gbGEgaW5mb3JtYWNpw7NuIHByb3BvcmNpb25hZGEgcG9yIGxvcyBjb3JyZWxvZ3JhbWFzIHNlIGNyZWFyw6EgdW4gbW9kZWxvIGFyaW1hIHF1ZSBwYXJhIGxhIHBhcnRlIHJlZ3VsYXIgc2VyYSAoMiwxLDIpIHkgcGFyYSBsYSBwYXJ0ZSBlc3RhY2lvbmFsICgwLDEsMSk6DQoNCmBgYHtyfQ0KZml0YXJpbWEzIDwtICBhcmltYShsb2coc2FsaWRhcy50cy50cmFpbiksIA0KICAgICAgICAgICAgICAgICAgb3JkZXIgPSBjKDIsMSwyKSwgDQogICAgICAgICAgICAgICAgICBzZWFzb25hbCA9IGxpc3Qob3JkZXIgPSBjKDAsMSwxKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwZXJpb2QgPSAxMiksDQogICAgICAgICAgICAgICAgICBtZXRob2QgPSAiTUwiKQ0KDQoNCmNvZWZ0ZXN0KGZpdGFyaW1hMykNCg0Kc3VtbWFyeShmaXRhcmltYTMpDQpgYGANCg0KVG9kb3MgbG9zIGNvZWZpY2llbnRlIGRlIGVzdGUgbW9kZWxvIHNvbiBzaWduaWZpY2F0aXZvcy4gQWhvcmEgc2UgYW5hbGl6YW4gbG9zIHJlc2lkdWFsZXMuDQoNCkF1dG9jb3JyZWxhY2nDs246DQoNCmBgYHtyfQ0KY2hlY2tyZXNpZHVhbHMoZml0YXJpbWEzKQ0KDQphdXRvcGxvdChhY2YoZml0YXJpbWEzJHJlc2lkdWFscywgcGxvdCA9IEZBTFNFKSkNCmF1dG9wbG90KHBhY2YoZml0YXJpbWEzJHJlc2lkdWFscywgcGxvdCA9IEZBTFNFKSkNCg0KZ2d0c2RpYWcoZml0YXJpbWEzKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpFc3RlIG1vZGVsbyBwYXNhIGxhIHBydWViYSBkZSBManVuZy1Cb3ggYWwgdGVuZXIgdW4gcC12YWxvciBtYXlvciBhbCAwLjA1LCBhZGVtw6FzLCBsYSBncsOhZmljYSBkZSBsb3MgcmVzaWR1YWxlcyBubyBtdWVzdHJhbiBuaW5nw7puIHBhdHLDs24gbyB0ZW5kZW5jaWEuIEVuIGxvcyBjb3JyZWxvZ3JhbWFzIHNvbG8gdW4gY29lZmljaWVudGUgc2FsZSBkZSBsYSBiYW5kYSBzdXBlcmlvci4gTG9zIHAtdmFsb3JlcyBwYXJhIGxhIHBydWViYSBRIGRlIExqdW5nLUJveCBlc3TDoW4gcG9yIGVuY2ltYSBkZSAwLDA1LCBsbyBxdWUgaW5kaWNhICJubyBzaWduaWZpY2F0aXZvIi4NCjwvZGl2Pg0KDQpOb3JtYWxpZGFkOg0KDQpgYGB7cn0NCnFxbm9ybShmaXRhcmltYTMkcmVzaWR1YWxzKQ0KcXFsaW5lKGZpdGFyaW1hMyRyZXNpZHVhbHMpDQoNCmphcnF1ZS5iZXJhLnRlc3QoZml0YXJpbWEzJHJlc2lkdWFscykNCmBgYA0KDQpMb3MgcmVzaWR1YWxlcyBubyBwcmVzZW50YW4gYXV0b2NvcnJlbGFjacOzbiBwZXJvIG5vIHNlIGRpc3RyaWJ1eWVuIG5vcm1hbG1lbnRlLiBQYXNhbW9zIGEgaGFjZXIgbGFzIHByZWRpY2Npb25lcyBwYXJhIGNvbm9jZXIgZWwgZXJyb3IgZGUgdGVzdDoNCg0KUHJlZGljY2nDs24geSBFdmFsdWFjacOzbjoNCg0KYGBge3J9DQpmYy5maXRhcmltYTMgPC0gZm9yZWNhc3QoZml0YXJpbWEzLCBoID0gMTIpDQoNCiMgc2UgaW52aWVydGVuIGxvcyBsb2dhcml0bW9zDQpmYy5maXRhcmltYTMkbWVhbiA8LSBleHAoZmMuZml0YXJpbWEzJG1lYW4pDQpmYy5maXRhcmltYTMkZml0dGVkIDwtIGV4cChmYy5maXRhcmltYTMkZml0dGVkKQ0KZmMuZml0YXJpbWEzJHggPC0gZXhwKGZjLmZpdGFyaW1hMyR4KQ0KDQpmb3JlY2FzdDo6YWNjdXJhY3koZmMuZml0YXJpbWEzLCBzYWxpZGFzLnRzLnRlc3QpDQpgYGANCg0KQXJyb2phIHVuIGVycm9yIGRlIDIuNzgsIG1heW9yIHF1ZSBlbCBtZWpvciBtb2RlbG8gYXJpbWEgKDEuMDgpDQoNCnZlYW1vcyBsYXMgZ3LDoWZpY2FzOg0KDQpgYGB7cn0NCnRlc3RfZm9yZWNhc3QoZm9yZWNhc3Qub2JqID0gZmMuZml0YXJpbWEzLCBhY3R1YWwgPSBzYWxpZGFzLnRzLCB0ZXN0ID0gc2FsaWRhcy50cy50ZXN0KSAlPiUgDQogIGxheW91dChsZWdlbmQgPSBsaXN0KHggPSAwLjEsIHkgPSAwLjk1KSkNCmBgYA0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCmZjLmZpdGFyaW1hMyRsb3dlclssMToyXSA8LSBleHAoZmMuZml0YXJpbWEzJGxvd2VyWywxOjJdKQ0KZmMuZml0YXJpbWEzJHVwcGVyWywxOjJdIDwtIGV4cChmYy5maXRhcmltYTMkdXBwZXJbLDE6Ml0pDQoNCmF1dG9wbG90KGZjLmZpdGFyaW1hMykgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicyh0aXRsZSA9ICJQcm9uw7NzdGljbyBhcmltYTMgU2FsaWRhcyAyMDE5IiwgeSA9ICJTYWxpZGFzIikNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KVGVuZW1vcyB0cmVzIG1vZGVsb3MgYXJpbWEgbG9zIGN1YWxlcyB0b2RvcyBwYXNhcm9uIGxhIHBydWViYSBkZSBhdXRvY29ycmVsYWNpw7NuIGVuIGxvcyByZXNpZHVhbGVzIHkgc29sbyBlbCBtb2RlbG8gYXJpbWEgMyBubyBwYXNhIGxhIHBydWViYSBkZSBub3JtYWxpZGFkLiBFbCBtb2RlbG8gcXVlIHByZXNlbnRhIGVsIG1lbm9yIGVycm9yIGVzIGVsIDEgY29uIHVuIE1BUEUgZGUgMS4wOC4gRGVzY2FydGFyZW1vcyBlbCBtb2RlbG8gMyB5YSBxdWUgc3VzIHJlc2lkdWFsZXMgbm8gc2UgZGlzdHJpYnV5ZW4gbm9ybWFsbWVudGUuDQoNCkRlIGVzdG9zIGRvcyBtb2RlbG9zIGVsZWdpcmVtb3MgZWwgbW9kZWxvIDEsIHBvcnF1ZSBnZW5lcsOzIGVsIG1lbm9yIGVycm9yIGRlIHRlc3QsIGFkZW3DoXMsIGFtYm9zIG1vZGVsb3MgdGllbmVuIDMgY29lZmljaWVudGVzLCBwb3IgbG8gcXVlLCBubyBwb2RlbW9zIGJhc2Fybm9zIGVuIGVsIHByaW5jaXBpbyBkZSBwYXJzaW1vbmlhIChlbCBtw6FzIHNpbXBsZSkuDQo8L2Rpdj4NCg0KIyAqKlRTTE0qKjogKipUKippbWUgKipTKiplcmllcyAqKkwqKmluZWFyICoqTSoqb2RlbA0KDQpTZSBnZW5lcmFyw6EgZWwgbW9kZWxvIHRzbG0gcXVlIGVzdMOhIGRpc2XDsWFkbyBwYXJhIGFqdXN0YXIgbW9kZWxvcyBsaW5lYWxlcyBjb24gZGF0b3MgZGUgc2VyaWVzIGRlIHRpZW1wbyB5IGVuIGVsIGN1YWwgc2UgcHVlZGVuIGluY2x1aXIgdmFyaWFibGVzIHByZWRpY3RvcmFzLCBlbiBwYXJ0aWN1bGFyIGxhIHRlbmRlbmNpYSB5IGxhIGVzdGFjaW9uYWxpZGFkLg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCnNhbGlkYXMubG0gIDwtIHRzbG0oc2FsaWRhcy50cy50cmFpbiB+ICB0cmVuZCArIHNlYXNvbiwgbGFtYmRhID0gImF1dG8iKQ0KDQpzdW1tYXJ5KHNhbGlkYXMubG0pDQoNCnNhbGlkYXMuZmNhc3QubG0gPC0gZm9yZWNhc3Qoc2FsaWRhcy5sbSwgaCA9IDEyKQ0KDQp0ZXN0X2ZvcmVjYXN0KGZvcmVjYXN0Lm9iaiA9IHNhbGlkYXMuZmNhc3QubG0sIGFjdHVhbCA9IHNhbGlkYXMudHMsIHRlc3QgPSBzYWxpZGFzLnRzLnRlc3QpICU+JSANCiAgbGF5b3V0KGxlZ2VuZCA9IGxpc3QoeCA9IDAuMSwgeSA9IDAuOTUpKQ0KDQpzYWxpZGFzLmZjYXN0LmxtJHggPC0gc2FsaWRhcy50cy50cmFpbg0KZm9yZWNhc3Q6OmFjY3VyYWN5KHNhbGlkYXMuZmNhc3QubG0sIHNhbGlkYXMudHMudGVzdCkNCg0KYXV0b3Bsb3Qoc2FsaWRhcy5mY2FzdC5sbSkgKyBsYWJzKHRpdGxlID0gIlByb27Ds3N0aWNvIE1vZGVsbyB0c2xtMSBTYWxpZGFzIDIwMTkiKQ0KDQpgYGANCg0KRXN0ZSBwcmltZXIgbW9kZWxvIGdlbmVyw7MgdW4gTUFQRSBkZSAyLjEzLiBIYXJlbW9zIHVuIHNlZ3VuZG8gbW9kZWxvIHBlcm8gYWhvcmEgaW5jbHV5ZW5kbyBsYSBlc3RhY2lvbmFsaWRhZCBjb24gdmFyaWFibGVzIGRpY290w7NtaWNhczoNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0V9DQojIFVzYW5kbyB2YXJpYWJsZXMgZHVtbWllcyBwYXJhIG1vZGVsYXIgbGEgZXN0YWNpb25hbGlkYWQNCm1lc2VzIDwtIHNlYXNvbmFsZHVtbXkoc2FsaWRhcy50cy50cmFpbikNCg0Kc2FsaWRhcy5sbTIgIDwtIHRzbG0oc2FsaWRhcy50cy50cmFpbiB+IG1lc2VzICsgdHJlbmQgKyBzZWFzb24pDQoNCnRzZGlzcGxheShyZXNpZHVhbHMoc2FsaWRhcy5sbTIpKQ0KDQpzYWxpZGFzLmZjYXN0MiA8LSBmb3JlY2FzdChzYWxpZGFzLmxtMiwNCiAgICBkYXRhLmZyYW1lKG1lc2VzID0gSShzZWFzb25hbGR1bW15KHNhbGlkYXMudHMudHJhaW4sIDEyKSkpKQ0KDQp0ZXN0X2ZvcmVjYXN0KGZvcmVjYXN0Lm9iaiA9IHNhbGlkYXMuZmNhc3QyLCBhY3R1YWwgPSBzYWxpZGFzLnRzLCB0ZXN0ID0gc2FsaWRhcy50cy50ZXN0KSAlPiUgDQogIGxheW91dChsZWdlbmQgPSBsaXN0KHggPSAwLjEsIHkgPSAwLjk1KSkNCg0KYXV0b3Bsb3Qoc2FsaWRhcy5mY2FzdDIpICsgbGFicyh0aXRsZSA9ICJQcm9uw7NzdGljbyBtb2RlbG8gdHNsbTIgU2FsaWRhcyAyMDE5IikNCg0KZm9yZWNhc3Q6OmFjY3VyYWN5KHNhbGlkYXMuZmNhc3QyLCBzYWxpZGFzLnRzLnRlc3QpDQoNCmBgYA0KDQpFbCBzZWd1bmRvIG1vZGVsbyB0c2xtIG5vIG1lam9yw7MgZWwgZXJyb3IgZGUgdGVzdCwgeWEgcXVlIGVzdGUgdGlwbyBkZSBhbGdvcml0bW8gbm8gZXMgYWRlY3VhZG8gcGFyYSBlbCB0aXBvIGRlIGRhdG9zIHF1ZSB0ZW5lbW9zLiBQb3IgdGFudG8sIHByb2JhcmVtb3MgdW4gw7psdGltbyBtb2RlbG8gcGFyYSB2ZXIgc2kgcG9kZW1vcyBtZWpvcmFyIGVsIGVycm9yIGRlbCBtb2RlbG8gYXJpbWExLCBxdWUgZXMgaGFzdGEgZWwgbW9tZW50byBlbCBxdWUgaGEgZ2VuZXJhZG8gZWwgbWVub3IgZXJyb3IgZGUgdGVzdC4NCg0KIyAqKlBST1BIRVQqKg0KDQpTZXLDoSBlbCBtb2RlbG8gUHJvcGhldCxjcmVhZG8gcG9yIEZhY2Vib29rIHBhcmEgaGFjZXIgc3VzIHByb27Ds3N0aWNvcyBjb21lcmNpYWxlcywgYSB2ZXIgcXXDqSByZXN1bHRhZG9zIG5vcyBnZW5lcmEuDQoNCkFudGVzIGRlIGluaWNpYXIgY29uIGVsIG1vZGVsbywgZXMgcHJlY2lzbyBhY2xhcmFyIHF1ZSBlc3RlIG1vZGVsbyBuZWNlc2l0YSBxdWUgbGFzIGNvbHVtbmFzIGRvbmRlIGVzdMOhbiBsYXMgZmVjaGFzIHkgbGEgdmFyaWFibGUgYSBwcm9ub3N0aWNhciB0ZW5nYW4gZGV0ZXJtaW5hZG9zIG5vbWJyZXMsIHBvciBsbyBxdWUsIGFudGVzIHF1ZSBuYWRhLCBzZSBwcm9jZWRlIGEgcmVub21icmFyIGRpY2hhcyBjb2x1bW5hczoNCg0KYGBge3J9DQojIFNlIHZlcmlmaWNhIGVsIHRpcG8gRmVjaGEgcGFyYSBlbCB0aWVtcG8geSBzZSByZW5vbWJyYW4gbGFzIGNvbHVtbmFzOiAiZHMiIHkgInkiDQpkZiA8LSBzYWxpZGFzDQoNCmNsZWFuZWRfZGYgPC0gZGYgJT4lIA0KICByZW5hbWUoZHMgPSBGZWNoYSwNCiAgICAgICAgIHkgPSBTYWxpZGFzKSAlPiUgDQogIG11dGF0ZShkcyA9IGFzLkRhdGUoZHMsICIlZC8lbS8lWSIpKQ0KDQpjbGVhbmVkX2RmICU+JSBoZWFkKCkNCmBgYA0KDQpTZSBlc3RhYmxlY2UgbGEgcGFydGljacOzbiBkZSBsb3MgZGF0b3MgZW4gdHJhaW4geSB0ZXN0Og0KDQpgYGB7cn0NCmNvcnJlY3Rfc3BsaXQgPC0gaW5pdGlhbF90aW1lX3NwbGl0KGNsZWFuZWRfZGYgJT4lIGFycmFuZ2UoZHMpLCBwcm9wID0gMS0oMTIvMTQ0KSkNCmNvcnJlY3Rfc3BsaXQNCg0KY2xlYW5lZF90cmFpbiA8LSBjbGVhbmVkX2RmICU+JQ0KICBmaWx0ZXIoZHMgPCIyMDE5LTAxLTAxIikNCg0KY2xlYW5lZF90ZXN0IDwtIGNsZWFuZWRfZGYgJT4lDQogIGZpbHRlcihkcyA+PSIyMDE5LTAxLTAxIikNCg0KIyBzZSB2aXN1YWxpemEgbGEgZGl2aXNpw7NuDQpiaW5kX3Jvd3MoDQogIHRyYWluaW5nKGNvcnJlY3Rfc3BsaXQpICU+JSBtdXRhdGUodHlwZSA9ICJ0cmFpbiIpLA0KICB0ZXN0aW5nKGNvcnJlY3Rfc3BsaXQpICU+JSBtdXRhdGUodHlwZSA9ICJ0ZXN0IikNCikgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBkcywgeT0geSwgY29sb3IgPSB0eXBlLCBncm91cCA9IE5BKSkgKyANCiAgZ2VvbV9saW5lKCkgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicyh0aXRsZSA9ICJTcGxpdCIsIHkgPSAiU2FsaWRhcyIpDQoNCmBgYA0KDQpTZSBjcmVhIGVsIG1vZGVsbyB5IHNlIGhhY2UgbGEgcHJlZGljY2nDs246DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KbSA8LSBwcm9waGV0KGRmID0gY2xlYW5lZF90cmFpbiAlPiUgYXJyYW5nZShkcyksIHllYXJseS5zZWFzb25hbGl0eSA9IFQpDQoNCmZ1dHVyZSA8LSBtYWtlX2Z1dHVyZV9kYXRhZnJhbWUobSwgcGVyaW9kcyA9IDEyLCBmcmVxID0gIm1vbnRoIiwgaW5jbHVkZV9oaXN0b3J5ID0gVFJVRSkNCg0KZm9yZWNhc3QgPC0gcHJlZGljdChtLCBmdXR1cmUpDQpgYGANCg0KU2UgZ3JhZmljYSBsYSBzZXJpZSBjb24gZWwgcHJvbm9zdGljbzoNCg0KYGBge3J9DQpwbG90KG0sIGZvcmVjYXN0KSArIA0KICBhZGRfY2hhbmdlcG9pbnRzX3RvX3Bsb3QobSkgKw0KICBsYWJzKHRpdGxlID0gIk1vZGVsbyBQcm9waGV0OiBQcm9uw7NzdGljbyBhIDEyIG1lc2VzIikNCmBgYA0KDQpWZWFtb3Mgb3RyYSB2aXN0YSBkZSBsYXMgcHJlZGljY2lvbmVzLCBlbiBjdcOhbnRvcyBtZXNlcyBxdWVkw7MgcG9yIGRlYmFqbyB5IHBvciBhcnJpYmEgZGVsIHZhbG9yIHJlYWw6DQoNCmBgYHtyfQ0KIyBzZSB2YWxpZGEgZWwgc2V0IGRlIHRlc3QNCnMgPC0gZm9yZWNhc3QgJT4lIA0KICBpbm5lcl9qb2luKGNsZWFuZWRfdGVzdCwgYnkgPSAiZHMiKSAlPiUgDQogIG11dGF0ZSgNCiAgICBkaWYgPSAoeS15aGF0KSwgDQogICAgZXNQb3NpdGl2byA9IGRpZiA+PSAwKQ0KDQpzICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gZHMsIHkgPSBkaWYsIGZpbGwgPSBlc1Bvc2l0aXZvKSkgKyANCiAgZ2VvbV9jb2wocG9zaXRpb24gPSAiaWRlbnRpdHkiKSArDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoImZpcmVicmljazQiLCAiZG9kZ2VyYmx1ZTQiKSkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKw0KICB0aGVtZV9taW5pbWFsKCkgKyANCiAgbGFicyh0aXRsZSA9ICJEaWZlcmVuY2lhIEVudHJlIFNhbGlkYXMgRXNwZXJhZGFzIHkgQWN0dWFsZXMiLA0KICAgICAgICAgIHN1YnRpdGxlID0gIkxhcyBTYWxpZGFzIHByb25vc3RpY2FkYXMgcXVlZGFyb24gcG9yIGRlYmFqbyBkdXJhbnRlIGxhIG1heW9yIHBhcnRlIGRlbCBhw7FvIiwgeSA9ICJEaWZlcmVuY2lhIiwgeCA9ICJBw7FvIikNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVuIGVsIHNpZ3VpZW50ZSBncsOhZmljbyBkZSBkaXNwZXJzacOzbiwgc2UgY29tcGFyYSBlbCBhanVzdGUgZGUgbGFzIG9ic2VydmFjaW9uZXMgcHJvbm9zdGljYWRhcyBjb250cmEgbGFzIG9ic2VydmFjaW9uZXMgcmVhbGVzLiBTZSBhcHJlY2lhIGNvbW8sIGFsIGlndWFsIHF1ZSBlbiBsb3MgZGVtw6FzIG1vZGVsb3MsIGVsIHByb27Ds3N0aWNvIGRlbCBtZXMgZGUgZmVicmVybyBxdWVkw7MgbXV5IHBvciBkZWJham8gZGVsIHZhbG9yIHJlYWwuIEVuIGxvcyBkZW3DoXMgbWVzZXMgbG9zIHByb27Ds3N0aWNvcyBzZSBhanVzdGFyb24gcmVsYXRpdmFtZW50ZSBiaWVuLg0KPC9kaXY+DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KcXBsb3QocyR5LCBzJHloYXQsIGxvZyA9ICJ4eSIpICsNCiAgZ2VvbV9hYmxpbmUoKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIHNjYWxlX3hfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiQ29tcGFyYW5kbyBsYXMgT2JzZXJ2YWNpb25lcyBFc3BlcmFkYXMgdnMgUmVhbGVzIiwgDQogICAgICAgeSA9ICJTYWxpZGFzIEVzcGVyYWRhcyIsDQogICAgICAgeCA9ICJTYWxpZGFzIFJlYWxlcyIpICsNCiAgdGhlbWVfbWluaW1hbCgpDQpgYGANCg0KU2UgcmV2aXNhbiBsb3MgZXJyb3JlczoNCg0KYGBge3IsIHJlc3VsdHM9ImFzaXMifQ0KbWFwZSA8LSBmdW5jdGlvbih5LCB5aGF0KSB7DQogIHJldHVybihtZWFuKGFicyh5IC0geWhhdCkvIHkpKjEwMCkNCn0NCg0KZXZhbCA8LSBjbGVhbmVkX3Rlc3QgJT4lIA0KICBsZWZ0X2pvaW4oZm9yZWNhc3QsIGJ5ID0gImRzIikgJT4lIA0KICBzZWxlY3QoZHMsIHksIHloYXQsIHloYXRfdXBwZXIsIHloYXRfbG93ZXIpDQoNCm1hcGUucHJvcGhldDEgPC0gbWFwZShldmFsJHksIGV2YWwkeWhhdCkNCg0KY2F0KCJNQVBFIGRlbCBtb2RlbG8gUHJvcGhldCAxOiAiLGBtYXBlLnByb3BoZXQxYCkNCg0KYGBgDQoNClNlIGdyYWZpY2EgZWwgcHJvbsOzc3RpY286DQoNCmBgYHtyfQ0KcGxvdChtLCBmb3JlY2FzdCkgKw0KICBhZGRfY2hhbmdlcG9pbnRzX3RvX3Bsb3QobSkgKw0KICBnZW9tX3BvaW50KGNsZWFuZWRfdGVzdCwgbWFwcGluZyA9IGFlcyhhcy5QT1NJWGN0KGRzKSwgeSksIGNvbCA9ICJyZWQiKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiUHJvbsOzc3RpY28gTW9kZWxvIFByb3BoZXQgMSIsIHkgPSAiU2FsaWRhcyIsIHggPSAiQcOxbyIpDQpgYGANCg0KVmVtb3MgcXVlIGVzdGUgbW9kZWxvIHRhbWJpw6luIHN1YmVzdGltYSBlbCBtZXMgZGUgZmVicmVybywgcG9yIGxvIHF1ZSwgdmFtb3MgYSBwcm9iYXIgbGEgdMOpY25pY2EgZGUgdmFsaWRhY2nDs24gY3J1emFkYSBwYXJhIGNyZWFyIG1vZGVsb3MgYWRpdGl2b3MgeSBtdWx0aXBsaWNhdGl2b3MgcGFyYSB2ZXIgY3XDoWxlcyBnZW5lcmFuIGVsIG1lbm9yIGVycm9yIGRlIGdlbmVyYWxpemFjacOzbjoNCg0KU2UgY3JlYW5uIGxvcyBrLWZvbGRzOg0KDQpgYGB7cn0NCnRpbWVzZXJpZXNfa19mb2xkcyA8LSBzbGlkaW5nX3BlcmlvZChjbGVhbmVkX2RmICU+JSBhcnJhbmdlKGRzKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZHMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGVyaW9kID0gInllYXIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxvb2tiYWNrID0gSW5mLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzc2Vzc19zdG9wID0gMSkNCg0KYGBgDQoNCkNvbiBhcG95byBkZSB1bmEgZnVuY2nDs24gc2UgY3JlYXLDoW4gbG9zIGRpZmVyZW50ZXMgdGlwb3MgZGUgbW9kZWxvczoNCg0KYGBge3J9DQp0dW5lX3Byb3BoZXQgPC0gZnVuY3Rpb24oc3BsaXRzKXsNCiAgdHJhaW5fZGF0YSA8LSBhbmFseXNpcyhzcGxpdHMpDQogIHRlc3RfZGF0YSA8LSBhc3Nlc3NtZW50KHNwbGl0cykNCiAgDQogIG0xIDwtIHByb3BoZXQoZGYgPSB0cmFpbl9kYXRhLCBzZWFzb25hbGl0eS5tb2RlID0gImFkZGl0aXZlIikNCiAgbTIgPC0gcHJvcGhldChkZiA9IHRyYWluX2RhdGEsIHNlYXNvbmFsaXR5Lm1vZGUgPSAibXVsdGlwbGljYXRpdmUiKQ0KICANCiAgZnV0dXJlIDwtIG1ha2VfZnV0dXJlX2RhdGFmcmFtZShtMSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGVyaW9kcyA9IG5yb3codGVzdF9kYXRhKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZnJlcSA9ICJtb250aCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5jbHVkZV9oaXN0b3J5ID0gRkFMU0UpDQogIA0KICANCiAgYmluZF9yb3dzKA0KICAgIHByZWRpY3QobTEsIGZ1dHVyZSkgJT4lIHNlbGVjdChkcywgeWhhdCkgJT4lIG11dGF0ZSh0eXBlID0gImFkZGl0aXZlIiksDQogICAgcHJlZGljdChtMiwgZnV0dXJlKSAlPiUgc2VsZWN0KGRzLCB5aGF0KSAlPiUgbXV0YXRlKHR5cGUgPSAibXVsdGlwbGljYXRpdmUiKQ0KICApICU+JSBsZWZ0X2pvaW4odGVzdF9kYXRhLCBieSA9ICJkcyIpDQp9DQoNCmBgYA0KDQpTZSBtdWVzdHJhbiBsb3MgZm9yZWNhc3QsIHN1IHRpcG8geSBtw6l0cmljYSBkZSBlcnJvcjoNCg0KYGBge3IsIHJlc3VsdHM9ImhpZGUifQ0KIyBzZSBoYWNlbiBsb3Mgay1mb2xkcw0KdHNfdHVuZSA8LSB0aW1lc2VyaWVzX2tfZm9sZHMgJT4lIA0KICBzbGljZV90YWlsKG4gPSAxMCkgJT4lDQogIG11dGF0ZShyZXMgPSBtYXAoc3BsaXRzLCB0dW5lX3Byb3BoZXQpKQ0KDQpgYGANCg0KYGBge3J9DQojIGVycm9yZXMNCnRzX3R1bmUgJT4lIA0KICBzZWxlY3QoaWQsIHJlcykgJT4lIA0KICB1bm5lc3QocmVzKSAlPiUgDQogIGdyb3VwX2J5KGlkLCB0eXBlKSAlPiUgDQogIGFycmFuZ2UoZHMpICU+JSANCiAgbXV0YXRlKGZvcmVjYXN0ID0gcGFzdGUwKCJmb3JlY2FzdF8iLCByb3dfbnVtYmVyKCkpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIHNlbGVjdChmb3JlY2FzdCwgdHlwZSwgZHMsIHloYXQsIHkpICU+JSANCiAgZ3JvdXBfYnkoZm9yZWNhc3QsIHR5cGUpICU+JSANCiAgeWFyZHN0aWNrOjptYXBlKHRydXRoID0geSwgZXN0aW1hdGUgPSB5aGF0KSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIGdyb3VwX2J5KGZvcmVjYXN0KSAlPiUgDQogIHNsaWNlX21pbiguZXN0aW1hdGUpICU+JSANCiAgdW5ncm91cCgpDQoNCmBgYA0KDQpWZWFtb3MgbGEgbWVkaWFuYSBkZSBsb3MgZXJyb3JlcyAoTUFTRSkgZW4gbG9zIG1vZGVsb3MgYWRpdGl2b3MgeSBtdWx0aXBsaWNhdGl2b3MsIHBlcm8gY29tcGFyYW5kbyBsb3MgcHJvbsOzc3RpY29zIGNvbiB1biBtb2RlbG8gaW5nZW51byAobmFpdmUpOg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCnRzX3R1bmUgJT4lIA0KICBzZWxlY3QoaWQsIHJlcykgJT4lIA0KICB1bm5lc3QocmVzKSAlPiUgDQogIGdyb3VwX2J5KGlkLCB0eXBlKSAlPiUgDQogIGFycmFuZ2UoZHMpICU+JSANCiAgbXV0YXRlKGZvcmVjYXN0ID0gcGFzdGUwKCJmb3JlY2FzdF8iLCByb3dfbnVtYmVyKCkpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIHNlbGVjdChmb3JlY2FzdCwgdHlwZSwgZHMsIHloYXQsIHkpICU+JSANCiAgbGVmdF9qb2luKGNsZWFuZWRfZGYgJT4lIA0KICAgICAgICAgICAgICBtdXRhdGUobmFpdmUgPSBsYWcoeSwgbiA9IDEsIG9yZGVyX2J5ID0gZHMpKSAlPiUgDQogICAgICAgICAgICAgIGRyb3BfbmEoKSAlPiUgDQogICAgICAgICAgICAgIHNlbGVjdChkcywgbmFpdmUpLA0KICAgICAgICAgICAgYnkgPSAgImRzIikgJT4lIA0KICBncm91cF9ieShmb3JlY2FzdCwgdHlwZSkgJT4lIA0KICBzdW1tYXJpc2UobWFzZSA9IG1lYW4oYWJzKHloYXQgLSB5KSkgLyBtZWFuKGFicyhuYWl2ZSAtIHkpKSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBncm91cF9ieSh0eXBlKSAlPiUgDQogIHN1bW1hcmlzZShtZWRpYW4gPSBtZWRpYW4obWFzZSkpDQoNCmBgYA0KDQpgYGB7cn0NCnRzX3R1bmUgJT4lIA0KICBzZWxlY3QoaWQsIHJlcykgJT4lIA0KICB1bm5lc3QocmVzKSAlPiUgDQogIGdyb3VwX2J5KGlkLCB0eXBlKSAlPiUgDQogIGFycmFuZ2UoZHMpICU+JSANCiAgbXV0YXRlKGZvcmVjYXN0ID0gcGFzdGUwKCJmb3JlY2FzdF8iLCByb3dfbnVtYmVyKCkpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIHNlbGVjdChpZCwgZm9yZWNhc3QsIHR5cGUsIGRzLCB5aGF0LCB5KSAlPiUgDQogIGdyb3VwX2J5KGlkLCB0eXBlKSAlPiUgDQogIG11dGF0ZShuYWl2ZV9kYXRlID0gbWluKGRzKSAtIG1vbnRocygxKSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBsZWZ0X2pvaW4oY2xlYW5lZF9kZiAlPiUgDQogICAgICAgICAgICAgIG11dGF0ZShuYWl2ZSA9IGxhZyh5LCBuID0gMSwgb3JkZXJfYnkgPSBkcykpICU+JSANCiAgICAgICAgICAgICAgZHJvcF9uYSgpICU+JSANCiAgICAgICAgICAgICAgc2VsZWN0KGRzLCBuYWl2ZSksDQogICAgICAgICAgICBieSA9ICBjKCJuYWl2ZV9kYXRlIiA9ICJkcyIpKSAlPiUgDQogIGdyb3VwX2J5KGZvcmVjYXN0LCB0eXBlKSAlPiUgDQogIHN1bW1hcmlzZShtYXNlID0gbWVhbihhYnMoeWhhdCAtIHkpKSAvIG1lYW4oYWJzKG5haXZlIC0geSkpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIGdyb3VwX2J5KHR5cGUpICU+JSANCiAgc3VtbWFyaXNlKG1lZGlhbiA9IG1lZGlhbihtYXNlKSkNCg0KYGBgDQoNCkVuIGFtYm9zIGNhc29zIGVsIG1lbm9yIGVycm9yIHNlIHByZXNlbnRhIGVuIGVsIHRpcG8gbXVsdGlwbGljYXRpdm8sIHBvciBsbyBxdWUsIGNyZWFyZW1vcyB1biBzZWd1bmRvIG1vZGVsbyBwZXJvIGFob3JhIGNvbiBlbCB0aXBvIGRlIGVzdGFjaW9uYWxpZGFkIG11bHRpcGxpY2F0aXZhOg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCm0yIDwtIHByb3BoZXQoZGYgPSBjbGVhbmVkX3RyYWluICU+JSBhcnJhbmdlKGRzKSwNCiAgICAgICAgICAgICAgeWVhcmx5LnNlYXNvbmFsaXR5ID0gVCwgDQogICAgICAgICAgICAgIHNlYXNvbmFsaXR5Lm1vZGUgPSAibXVsdGlwbGljYXRpdmUiKQ0KDQpmdXR1cmUyIDwtIG1ha2VfZnV0dXJlX2RhdGFmcmFtZShtMiwgcGVyaW9kcyA9IDEyLCBmcmVxID0gIm1vbnRoIiwgaW5jbHVkZV9oaXN0b3J5ID0gVFJVRSkNCg0KZm9yZWNhc3QyIDwtIHByZWRpY3QobTIsIGZ1dHVyZTIpDQpgYGANCg0KU2UgZ3JhZmljYSBsYSBzZXJpZSBjb24gZWwgcHJvbm9zdGljbzoNCg0KYGBge3J9DQpwbG90KG0yLCBmb3JlY2FzdDIpICsgDQogIGFkZF9jaGFuZ2Vwb2ludHNfdG9fcGxvdChtMikgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKw0KICBsYWJzKHRpdGxlID0gIk1vZGVsbyBQcm9waGV0IDI6IFByb27Ds3N0aWNvIGEgMTIgbWVzZXMiLCB5ID0gIlNhbGlkYXMiLCB4ID0gIkHDsW8iKQ0KYGBgDQoNClZvbHZhbW9zIGEgdmVyIGPDs21vIHF1ZWRvIGVsIHByb27Ds3N0aWNvLCBvIHNlYSwgZW4gY3XDoW50b3MgbWVzZXMgcXVlZMOzIHBvciBkZWJham8geSBwb3IgYXJyaWJhIGRlbCB2YWxvciByZWFsOg0KDQpgYGB7cn0NCiMgc2UgdmFsaWRhIGVsIHNldCBkZSB0ZXN0DQpzMiA8LSBmb3JlY2FzdDIgJT4lIA0KICBpbm5lcl9qb2luKGNsZWFuZWRfdGVzdCwgYnkgPSAiZHMiKSAlPiUgDQogIG11dGF0ZSgNCiAgICBkaWYgPSAoeS15aGF0KSwgDQogICAgZXNQb3NpdGl2byA9IGRpZiA+PSAwKQ0KDQpzMiAlPiUgDQogIGdncGxvdChhZXMoeCA9IGRzLCB5ID0gZGlmLCBmaWxsID0gZXNQb3NpdGl2bykpICsgDQogIGdlb21fY29sKHBvc2l0aW9uID0gImlkZW50aXR5IikgKw0KICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjKCJmaXJlYnJpY2s0IiwgImRvZGdlcmJsdWU0IikpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgdGhlbWVfbWluaW1hbCgpICsgDQogIGxhYnModGl0bGUgPSAiRGlmZXJlbmNpYSBFbnRyZSBTYWxpZGFzIEVzcGVyYWRhcyB5IEFjdHVhbGVzIiwNCiAgICAgICAgICBzdWJ0aXRsZSA9ICJMYXMgU2FsaWRhcyBwcm9ub3N0aWNhZGFzIHF1ZWRhcm9uIHBvciBkZWJham8gZHVyYW50ZSBsYSBtYXlvciBwYXJ0ZSBkZWwgYcOxbyIsIHkgPSAiRGlmZXJlbmNpYSIsIHggPSAiQcOxbyIpDQpgYGANCg0KRW4gbGEgZ3LDoWZpY2EgYW50ZXJpb3IsIHlhIHNlIGFwcmVjaWFuIGxvcyBkYXRvcyBwYXJhIGxvcyBwcmltZXJvcyBtZXNlcyBtw6FzIGNlcmNhbm9zIGEgbGFzIG9ic2VydmFjaW9uZXMgcmVhbGVzLCBzaW4gZW1iYXJnbywgZXN0ZSBtb2RlbG8gc3ViZXN0aW3DsyBlbCBtZXMgZGUganVsaW8uIFZlYW1vcyBsYSBncsOhZmljYSBkZSBkaXNwZXJzacOzbiBkZSBsYXMgb2JzZXJ2YWNpb25lcyBlc3BlcmFkYXMgeSByZWFsZXM6IA0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCnFwbG90KHMyJHksIHMyJHloYXQsIGxvZyA9ICJ4eSIpICsNCiAgZ2VvbV9hYmxpbmUoKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIHNjYWxlX3hfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiQ29tcGFyYW5kbyBsYXMgT2JzZXJ2YWNpb25lcyBFc3BlcmFkYXMgdnMgUmVhbGVzIiwgDQogICAgICAgeSA9ICJTYWxpZGFzIEVzcGVyYWRhcyIsDQogICAgICAgeCA9ICJTYWxpZGFzIFJlYWxlcyIpICsNCiAgdGhlbWVfbWluaW1hbCgpDQpgYGANCg0KU2UgYXByZWNpYSBxdWUgbGFzIHByZWRpY2Npb25lcyBzZSBhanVzdGFuIG1lam9yIGEgbG9zIGRhdG9zIHJlYWxlcy4NCg0KU2UgcmV2aXNhbiBsb3MgZXJyb3JlczoNCg0KYGBge3J9DQptYXBlIDwtIGZ1bmN0aW9uKHksIHloYXQpIHsNCiAgcmV0dXJuKG1lYW4oYWJzKHkgLSB5aGF0KS8geSkqMTAwKQ0KfQ0KDQpldmFsMiA8LSBjbGVhbmVkX3Rlc3QgJT4lIA0KICBsZWZ0X2pvaW4oZm9yZWNhc3QyLCBieSA9ICJkcyIpICU+JSANCiAgc2VsZWN0KGRzLCB5LCB5aGF0LCB5aGF0X3VwcGVyLCB5aGF0X2xvd2VyKSANCg0KbWFwZS5wcm9waGV0LmYgPC0gbWFwZShldmFsMiR5LCBldmFsMiR5aGF0KQ0KDQpjYXQoIk1BUEUgZGVsIG1vZGVsbyBQcm9waGV0OiAiLGBtYXBlLnByb3BoZXQuZmApDQoNCmBgYA0KDQpTZSBncmFmaWNhIGVsIHByb27Ds3N0aWNvOg0KDQpgYGB7cn0NCnBsb3QobTIsIGZvcmVjYXN0MikgKw0KICBhZGRfY2hhbmdlcG9pbnRzX3RvX3Bsb3QobTIpICsNCiAgZ2VvbV9wb2ludChjbGVhbmVkX3Rlc3QsIG1hcHBpbmcgPSBhZXMoYXMuUE9TSVhjdChkcyksIHkpLCBjb2wgPSAicmVkIikgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKw0KICBsYWJzKHRpdGxlID0gIlByb27Ds3N0aWNvIE1vZGVsbyBQcm9waGV0IE11bHRpcGxpY2F0aXZvIiwgeSA9ICJTYWxpZGFzIiwgeCA9ICJBw7FvIikNCmBgYA0KDQpFc3RlIG1vZGVsbyBzZSB2ZSB5IHNlIGFqdXN0YSBtZWpvciwgYWRlbcOhcywgZWwgZXJyb3IgZGUgdGVzdCBiYWrDsyBhIDEuODgsIHBvciBsbyBxdWUsIG5vcyBxdWVkYW1vcyBjb24gZXN0ZSBzZWd1bmRvIG1vZGVsbyBtdWx0aXBsaWNhdGl2by4NCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkVuc2FtYmxlPC9zcGFuPg0KDQpTb24gdHJlcyBsb3MgbW9kZWxvcyBxdWUgZ2VuZXJhcm9uIHVuIGJham8gZXJyb3IgTUFQRSBlbiBlbCBjb25qdW50byBkZSBwcnVlYmEsIHBvciBsbyBxdWUsIGxvcyB2YW1vcyBhIGNvbWJpbmFyIGVzcGVyYW5kbyBvYnRlbmVyIHVuIG1vZGVsbyBtw6FzIHJvYnVzdG8uIFBhcmEgaGFjZXIgbG8gYW50ZXJpb3IsIGNyZWFyZW1vcyB1bmEgZnVuY2nDs24gcXVlIGhhZ2EgZWwgZW5zYW1ibGUgZGUgbG9zIHNpZ3VpZW50ZXMgbW9kZWxvczogDQoNCiogZXRzIGNvbiBlbCBhbHBoYSDDs3B0aW1vDQoqIGFyaW1hIDENCiogcHJvcGhldCBtdWx0aXBsaWNhdGl2bw0KDQpTZSBjcmVhIGxhIGZ1bmNpw7NuOg0KDQpgYGB7cn0NCmVuc2FtYmxlIDwtIGZ1bmN0aW9uKGRmLCBuX3Byb25vc3RpY29zKSB7DQogIA0KICBOIDwtIG5yb3coZGYpDQogIG4gPC0gbl9wcm9ub3N0aWNvcw0KICBmaW4gPC0gTiAtIG4NCiAgDQogIGluaWNpby50cmFpbiA8LSBkZiRGZWNoYVtmaW5dDQogIGluaWNpby50ZXN0IDwtIGRmJEZlY2hhW2Zpbl0rMQ0KICANCiAgZGZfdHJhaW4gPC0gZGZbMTpmaW4sXQ0KICBkZl90cmFpbiA8LSB0cyhkZl90cmFpblssMl0sIHN0YXJ0ID0gaW5pY2lvLnRyYWluLCBmcmVxdWVuY3kgPSAxMikNCiAgDQogIGRmX3Rlc3QgPC0gZGZbKGZpbiArIDEpOk4sXSAlPiUgdHMoc3RhcnQgPSBpbmljaW8udGVzdCwgZnJlcXVlbmN5ID0gMTIpDQogIGRmX3Rlc3QgPC0gdHMoZGZfdGVzdFssMl0sIHN0YXJ0ID0gaW5pY2lvLnRlc3QsIGZyZXF1ZW5jeSA9IDEyKQ0KICANCiAgaCA8LSBuDQogIA0KICBmaXQuZXRzIDwtIGV0cyhkZl90cmFpbiwNCiAgICAgICAgICAgICAgICBtb2RlbCA9ICJaWloiLA0KICAgICAgICAgICAgICAgIGRhbXBlZCA9IFQsDQogICAgICAgICAgICAgICAgYWRkaXRpdmUub25seSA9IFQsDQogICAgICAgICAgICAgICAgbGFtYmRhID0gImF1dG8iLA0KICAgICAgICAgICAgICAgIGFscGhhID0gYWxwaGEubWluJGFscGhhDQogICAgICAgICAgICAgICAgKQ0KICBwcmVkaWNjaW9uLmV0cyA8LSBmb3JlY2FzdChmaXQuZXRzLCBoID0gaCkkbWVhbiAlPiUgYXMudmVjdG9yKCkNCiAgDQogIA0KICBhcmltYSA8LSBhdXRvLmFyaW1hKGRmX3RyYWluKQ0KICBwcmVkaWNjaW9uLmFyaW1hIDwtIGZvcmVjYXN0KGFyaW1hLCBoID0gaCkkbWVhbiAlPiUgYXMudmVjdG9yKCkNCiAgDQogIA0KICANCiAgY2xlYW5lZF90cmFpbiA8LSBkZlsxOmZpbixdICU+JSANCiAgICByZW5hbWUoZHMgPSBGZWNoYSwNCiAgICAgICAgIHkgPSBTYWxpZGFzKSAlPiUgDQogIG11dGF0ZShkcyA9IGFzLkRhdGUoZHMsICIlZC8lbS8lWSIpKQ0KDQogIGNsZWFuZWRfdGVzdCA8LSBkZlsoZmluICsgMSk6TixdICU+JSANCiAgICByZW5hbWUoZHMgPSBGZWNoYSwNCiAgICAgICAgIHkgPSBTYWxpZGFzKSAlPiUgDQogIG11dGF0ZShkcyA9IGFzLkRhdGUoZHMsICIlZC8lbS8lWSIpKQ0KICANCiAgbS5wcm9waGV0IDwtIHByb3BoZXQoZGYgPSBjbGVhbmVkX3RyYWluICU+JSBhcnJhbmdlKGRzKSwNCiAgICAgICAgICAgICAgeWVhcmx5LnNlYXNvbmFsaXR5ID0gVCwgDQogICAgICAgICAgICAgIHNlYXNvbmFsaXR5Lm1vZGUgPSAibXVsdGlwbGljYXRpdmUiKQ0KDQogIGZ1dHVyZSA8LSBtYWtlX2Z1dHVyZV9kYXRhZnJhbWUobS5wcm9waGV0LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGVyaW9kcyA9IGgsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZyZXEgPSAibW9udGgiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbmNsdWRlX2hpc3RvcnkgPSBUUlVFKQ0KDQogIGZjLnByb3BoZXQgPC0gcHJlZGljdChtLnByb3BoZXQsIGZ1dHVyZSkNCg0KICBwcmVkaWNjaW9uLnByb3BoZXQgPC0gY2xlYW5lZF90ZXN0ICU+JSANCiAgbGVmdF9qb2luKGZjLnByb3BoZXQsIGJ5ID0gImRzIikgJT4lIA0KICBzZWxlY3QoeWhhdCkgJT4lIGFzLnZlY3RvcigpDQogIA0KICANCiAgDQogIHByZWRpY2Npb24uZW5zYW1ibGUgPC0gKHByZWRpY2Npb24uZXRzICsgcHJlZGljY2lvbi5hcmltYSArIHByZWRpY2Npb24ucHJvcGhldCkvMw0KICANCiAgcmV0dXJuKGRhdGFfZnJhbWUoRmVjaGEgPSBjbGVhbmVkX3Rlc3QkZHMsDQogICAgICAgICAgICAgICAgICAgIEFjdHVhbCA9IGRmX3Rlc3QgJT4lIGFzLnZlY3RvcigpLCANCiAgICAgICAgICAgICAgICAgICAgUHJlZGljY2lvbiA9IHByZWRpY2Npb24uZW5zYW1ibGUgJT4lIGFzLnZlY3RvcigpICU+JSByb3VuZCgwKSwgDQogICAgICAgICAgICAgICAgICAgIEVycm9yID0gKFByZWRpY2Npb24gLSBBY3R1YWwpICU+JSBhcy52ZWN0b3IoKSwNCiAgICAgICAgICAgICAgICAgICAgRXJyb3JfUG9yY2VudHVhbCA9IHJvdW5kKChFcnJvciAvIEFjdHVhbCkqMTAwLCAyKSkpDQp9IA0KDQpgYGANCg0KVmVhbW9zIGxvcyByZXN1bHRhZG9zIGRlbCBlbnNhbWJsZToNCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCiMgVXNhbW9zIGxhIGZ1bmNpw7NuOiANCmRmX2Vuc2FtYmxlIDwtIGVuc2FtYmxlKHNhbGlkYXMsIDEyKQ0KDQpkZl9lbnNhbWJsZSRQcmVkaWNjaW9uIDwtIGRmX2Vuc2FtYmxlJFByZWRpY2Npb24keWhhdA0KZGZfZW5zYW1ibGUkRXJyb3IgPC0gZGZfZW5zYW1ibGUkRXJyb3IkeWhhdA0KDQoNCmRmX2Vuc2FtYmxlICU+JSBrYmwoYWxpZ24gPSAiYyIsDQogICAgICAgICAgICAgICAgICAgIGRpZ2l0cyA9IDIgLCANCiAgICAgICAgICAgICAgICAgICAgZm9ybWF0LmFyZ3MgPSBsaXN0KGJpZy5tYXJrID0gIiwiKSkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpMb3MgcHJvbsOzc3RpY29zIHNlIGFjZXJjYW4gYSBsb3MgZGF0b3MgcmVhbGVzIGRlIHRlc3QsIHF1ZSBjb3JyZXNwb25kZW4gYWwgYcOxbyAyMDE5LCBzaW4gZW1iYXJnbywgZWwgbWVzIGRlIG1heW8gcXVlZMOzIHN1YmVzdGltYWRvIHksIHBvciBlbCBjb250cmFyaW8sIGVsIG1lcyBkZSBqdWxpbyBxdWVkw7Mgc29icmVlc3RpbWFkbyBlc3RvIGVzIGRlYmlkbyBhIGxhIGluZmx1ZW5jaWEgZGVsIG1vZGVsbyBQcm9waGV0LiBBaG9yYSwgY29tcGFyZW1vcyBsb3MgZXJyb3JlcyBkZSBsb3MgdHJlcyBtb2RlbG9zIGNvbiBlbCBkZWwgZW5zYW1ibGUgcGFyYSB2ZXIgc2kgdmFsaW8gbGEgcGVuYSBhcGxpY2FyIGVzdGEgdMOpY25pY2EuDQo8L2Rpdj4NCg0KYGBge3J9DQojIFNlIGNyZWEgdW5hIGZ1bmN0aW9uIHF1ZSBjYWxjdWxlIGFsZ3VuYXMgbWVkaWRhcyBkZSBldmFsdWFjacOzbjogDQpldmFsX21lZGlkYXMgPC0gZnVuY3Rpb24oZGZfbW9kZWxvKSB7DQogIA0KICBhY3QgPC0gZGZfbW9kZWxvICU+JSBwdWxsKEFjdHVhbCkNCiAgcHJlZCA8LSBkZl9tb2RlbG8gJT4lIHB1bGwoUHJlZGljY2lvbikNCiAgZXJyIDwtIGFjdCAtIHByZWQgDQogIHBlcl9lcnIgPC0gYWJzKGVyciAvIGFjdCkNCiAgDQogICMgTWVhbiBBYnNvbHV0ZSBFcnJvciAoTUFFKTogDQogIG1hZSA8LSBlcnIgJT4lIGFicygpICU+JSBtZWFuKCkNCiAgDQogICMgTWVhbiBTcXVhcmVkIEVycm9yIChNU0UpOiANCiAgbXNlIDwtIG1lYW4oZXJyXjIpDQogIA0KICAjIE1lYW4gQWJzb2x1dGUgUGVyY2VudGFnZSBFcnJvciAoTUFQRSk6IA0KICBtYXBlIDwtIG1lYW4ocGVyX2VycikqMTAwDQogIA0KICAjIFJldG9ybmEgbG9zIHJlc3VsdGFkb3NzOiANCiAgcmV0dXJuKGRhdGFfZnJhbWUoTUFFID0gbWFlLCBNU0UgPSBtc2UsIE1BUEUgPSBtYXBlLCBOID0gbGVuZ3RoKGFjdCkpKQ0KfQ0KDQojIERhdGEgRnJhbWUgZXRzOiANCmRmX2V0cyA8LSBkYXRhX2ZyYW1lKEFjdHVhbCA9IHNhbGlkYXMudHMudGVzdCAlPiUgYXMudmVjdG9yKCksIA0KICAgICAgICAgICAgICAgICAgICAgICBQcmVkaWNjaW9uID0gZmMuZXRzJG1lYW4gJT4lIGFzLnZlY3RvcigpICU+JSByb3VuZCgwKSwgDQogICAgICAgICAgICAgICAgICAgICAgIEVycm9yID0gUHJlZGljY2lvbiAtIEFjdHVhbCwNCiAgICAgICAgICAgICAgICAgICAgICAgRXJyb3JfUG9yY2VudHVhbCA9IHJvdW5kKEVycm9yIC8gQWN0dWFsLCAyKSkNCg0KIyBEYXRhIEZyYW1lIGFyaW1hOiANCmRmX2FyaW1hIDwtIGRhdGFfZnJhbWUoQWN0dWFsID0gc2FsaWRhcy50cy50ZXN0ICU+JSBhcy52ZWN0b3IoKSwgDQogICAgICAgICAgICAgICAgICAgICAgIFByZWRpY2Npb24gPSBmYy5maXRhcmltYTEkbWVhbiAlPiUgYXMudmVjdG9yKCkgJT4lIHJvdW5kKDApLCANCiAgICAgICAgICAgICAgICAgICAgICAgRXJyb3IgPSBQcmVkaWNjaW9uIC0gQWN0dWFsLA0KICAgICAgICAgICAgICAgICAgICAgICBFcnJvcl9Qb3JjZW50dWFsID0gcm91bmQoRXJyb3IgLyBBY3R1YWwsIDIpKQ0KDQojIERhdGEgRnJhbWUgcHJvcGhldDogDQpkZl9wcm9waGV0IDwtIGRhdGFfZnJhbWUoQWN0dWFsID0gc2FsaWRhcy50cy50ZXN0ICU+JSBhcy52ZWN0b3IoKSwgDQogICAgICAgICAgICAgICAgICAgICAgIFByZWRpY2Npb24gPSBzMiR5aGF0ICU+JSBhcy52ZWN0b3IoKSAlPiUgcm91bmQoMCksIA0KICAgICAgICAgICAgICAgICAgICAgICBFcnJvciA9IFByZWRpY2Npb24gLSBBY3R1YWwsDQogICAgICAgICAgICAgICAgICAgICAgIEVycm9yX1BvcmNlbnR1YWwgPSByb3VuZChFcnJvciAvIEFjdHVhbCwgMikpDQoNCiMgQ29tcGFyYW5kbyBsb3MgdHJlcyBtb2RlbG9zOiANCg0KYmluZF9yb3dzKGRmX2V0cyAlPiUgZXZhbF9tZWRpZGFzKCksIA0KICAgICAgICAgIGRmX2FyaW1hICU+JSBldmFsX21lZGlkYXMoKSwgDQogICAgICAgICAgZGZfcHJvcGhldCAlPiUgZXZhbF9tZWRpZGFzKCksDQogICAgICAgICAgZGZfZW5zYW1ibGUgJT4lIGV2YWxfbWVkaWRhcykgJT4lDQogIG11dGF0ZShNT0RFTE8gPSBjKCJFVFMiLCAiQVJJTUEiLCAiUFJPUEhFVCIsICJFTlNBTUJMRSIpKSAlPiUgDQogIHNlbGVjdChNT0RFTE8sIGV2ZXJ5dGhpbmcoKSkgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkge3JvdW5kKHgsIDIpfSkgJT4lDQogIGFycmFuZ2UoZGVzYygtTUFQRSkpICU+JSANCiAga2JsKGFsaWduID0gImMiLA0KICAgICAgICAgICAgICAgICAgICBkaWdpdHMgPSAyICwgDQogICAgICAgICAgICAgICAgICAgIGZvcm1hdC5hcmdzID0gbGlzdChiaWcubWFyayA9ICIsIikpICU+JQ0KICBrYWJsZV9wYXBlcigiaG92ZXIiLCBmdWxsX3dpZHRoID0gRikNCg0KYGBgDQombmJzcDsNCg0KRGVzcHXDqXMgZGUgaGFjZXIgZWwgZW5zYW1ibGUgZGUgbG9zIHRyZXMgbWVqb3JlcyBtb2RlbG9zIGEgZmluYWwgZGUgY3VlbnRhcyBubyBzZSBwdWRvIG1lam9yYXIgZWwgTUFQRSBkZWwgbWVqb3IgbW9kZWxvLCBlbCBhcmltYSwgbG8gYW50ZXJpb3IsIG5vIGVzIGRlIHNvcnByZW5kZXIsIHB1ZXMgZWwgZW5zYW1ibGUgc2UgcmVjb21pZW5kYSBjdWFuZG8gbG9zIG1vZGVsb3MgcXVlIHNlIGp1bnRhcsOhbiBwcmVzZW50YW4gZGlmZXJlbmNpYXMgZW50cmUgZWxsb3MsIGxvIHF1ZSBlbiBlc3RlIGNhc28gbm8gb2N1cnJpw7MuDQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5Nb2RlbG8gZmluYWw8L3NwYW4+DQombmJzcDsNCg0KUG9yIGxvIGFudGVyaW9yLCBub3MgcXVlZGFtb3MgY29uIGVsIG1vZGVsbyAqKmFyaW1hKiosIHF1ZSB0dXZvIHVuIE1BUEUgZGUgdGVzdCBkZSAxLjA5JS4NCg0KVmFtb3MgYSBwcm9ub3N0aWNhciBjb24gZXNlIG1vZGVsbyBlbCBwcmltZXIgYmltZXN0cmUgZGUgMjAyMCwgeWEgcXVlIGVzb3MgbWVzZXMgbm8gZXN0w6FuIGFmZWN0YWRvcyBwb3IgbGEgcGFuZGVtaWEgZW4gTcOpeGljbywgeWEgcXVlIGVsIGNvbmZpbmFtaWVudG8gZW1wZXrDsyBhIGZpbmFsZXMgZGUgbWFyem86DQoNCmBgYHtyfQ0KIyBzZSBjYXJnYW4gbG9zIGRhdG9zDQpzYWxpZGFzLjIwMjAuQyA8LSByZWFkLmNzdigiU2FsaWRhc19OYWNpb25hbGVzX0MuY3N2IikNCnNhbGlkYXMuMjAyMC5DLnRzIDwtIHRzKHNhbGlkYXMuMjAyMC5DWywyXSwgDQogICAgICAgICAgICAgICAgICAgICAgc3RhcnQgPSBjKDIwMDgsIDEpLA0KICAgICAgICAgICAgICAgICAgICAgIGVuZCA9IGMoMjAyMCwgMiksDQogICAgICAgICAgICAgICAgICAgICAgZnJlcXVlbmN5ID0gMTIpDQoNCnRzX2luZm8oc2FsaWRhcy4yMDIwLkMudHMpDQoNCiMgT3V0bGllcnMNCnRzb3V0bGllcnMoc2FsaWRhcy4yMDIwLkMudHMpDQpzYWxpZGFzLjIwMjAuQy50cyA8LSB0c2NsZWFuKHNhbGlkYXMuMjAyMC5DLnRzKQ0KDQoNCiMgZGF0YSByZWFsIGEgcHJvbm9zdGljYXINCnNhbGlkYXMuMjAyMCA8LSByZWFkLmNzdigiU2FsaWRhc19OYWNpb25hbGVzXzIwMjAuY3N2IikNCg0Kc2FsaWRhcy4yMDIwLnRzIDwtIHRzKHNhbGlkYXMuMjAyMFssMl0sIA0KICAgICAgICAgICAgICAgICAgICAgIHN0YXJ0ID0gYygyMDIwLCAxKSwNCiAgICAgICAgICAgICAgICAgICAgICBlbmQgPSBjKDIwMjAsIDIpLA0KICAgICAgICAgICAgICAgICAgICAgIGZyZXF1ZW5jeSA9IDEyKQ0KDQp0c19pbmZvKHNhbGlkYXMuMjAyMC50cykNCg0KYGBgDQoNClNlIGNyZWEgZWwgbW9kZWxvOg0KDQpgYGB7cn0NCmZpdGFyaW1hLmZpbmFsIDwtIGF1dG8uYXJpbWEoc2FsaWRhcy50cykNCmZpdGFyaW1hLmZpbmFsDQoNCmNvZWZ0ZXN0KGZpdGFyaW1hLmZpbmFsKQ0Kc3VtbWFyeShmaXRhcmltYS5maW5hbCkNCmBgYA0KDQpBbsOhbGlzaXMgZGUgbG9zIHJlc2lkdWFsZXM6DQoNCmBgYHtyfQ0KY2hlY2tyZXNpZHVhbHMoZml0YXJpbWEuZmluYWwpDQoNCmBgYA0KDQpBbsOhbGlzaXMgZGUgbGEgbm9ybWFsaWRhZDoNCg0KYGBge3J9DQpxcW5vcm0oZml0YXJpbWEuZmluYWwkcmVzaWR1YWxzKQ0KcXFsaW5lKGZpdGFyaW1hLmZpbmFsJHJlc2lkdWFscykNCg0KamFycXVlLmJlcmEudGVzdChmaXRhcmltYS5maW5hbCRyZXNpZHVhbHMpDQpgYGANCg0KYGBge3J9DQpnZ3RzZGlhZyhmaXRhcmltYS5maW5hbCkNCmBgYA0KDQpFc3RlIG1vZGVsbyBmaW5hbCBubyBwcmVzZW50YSBhdXRvY29ycmVsYWNpw7NuIGVuIHN1cyByZXNpZHVhbGVzLCB5YSBxdWUgc3UgZ3LDoWZpY2Egbm8gbXVlc3RyYSB0ZW5kZW5jaWEgeSBzw60gcGFzYSBlbCB0ZXN0IGRlIExqdW5nLUJveHksIGFkZW3DoXMsIMOpc3RvcyBzZSBkaXN0cmlidXllbiBub3JtYWxtZW50ZS4gRW4gc3UgY29ycmVsb2dyYW1hIGxvcyBjb2VmaWNpZW50ZXMgbm8gcmViYXNhbiBsb3MgbMOtbWl0ZXMuIFBvciBsbyBhbnRlcmlvciwgZWwgbW9kZWxvIGVzIGFwdG8gcGFyYSBwcm9ub3N0aWNhci4NCg0KU2UgaGFjZSBsYSBwcmVkaWNjacOzbiBkZSBsb3MgcHLDs3hpbW9zIDIgbWVzZXM6DQoNCmBgYHtyfQ0KZmMuZml0YXJpbWEuZmluYWwgPC0gZm9yZWNhc3QoZml0YXJpbWEuZmluYWwsIGggPSAyKQ0KZm9yZWNhc3Q6OmFjY3VyYWN5KGZjLmZpdGFyaW1hLmZpbmFsLCBzYWxpZGFzLjIwMjAudHMpDQpgYGANCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0V9DQphdXRvcGxvdChmYy5maXRhcmltYS5maW5hbCkgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicyh0aXRsZSA9ICJQcm9uw7NzdGljbyBBcmltYSBGaW5hbDogU2FsaWRhcyAyMDIwIiwgeSA9ICJTYWxpZGFzIikNCmBgYA0KDQpgYGB7cn0NCnRlc3RfZm9yZWNhc3QoZm9yZWNhc3Qub2JqID0gZmMuZml0YXJpbWEuZmluYWwsIA0KICAgICAgICAgICAgICBhY3R1YWwgPSBzYWxpZGFzLjIwMjAuQy50cywNCiAgICAgICAgICAgICAgdGVzdCA9IHNhbGlkYXMuMjAyMC50cykgJT4lDQogIGxheW91dChsZWdlbmQgPSBsaXN0KHggPSAwLjEsIHkgPSAwLjk1KSkNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRWwgZXJyb3IgTUFQRSBkZWwgbW9kZWxvIGVzOiBgciBmb3JlY2FzdDo6YWNjdXJhY3koZmMuZml0YXJpbWEuZmluYWwsIHNhbGlkYXMuMjAyMC50cylbMiw1XWAuIEhheSBxdWUgdGVuZXIgcHJlc2VudGUgcXVlIGxvcyBhcmd1bWVudG9zIGRlIGVzdGUgbW9kZWxvIHNvbiBkaWZlcmVudGVzIGFsIG1vZGVsbyBhcmltYTEgY3JlYWRvIGFudGVyaW9ybWVudGUsIGVzdG8gZGViaWRvIGEgcXVlIHBhcmEgc3UgY29uc3RydWNjacOzbiBzZSB1dGlsaXrDsyB0b2RhIGxhIHNlcmllIGhpc3TDs3JpY2EsIGRlc2RlIGVuZXJvIGRlIDIwMDggaGFzdGEgZGljaWVtYnJlIGRlIDIwMTksIHBvciBsbyB0YW50bywgeWEgbm8gZXZhbHVhbW9zIGVsIG1vZGVsbyBjb24gdW4gY29uanVudG8gZGUgdGVzdCwgc2lubyBxdWUgZGlyZWN0YW1lbnRlIGxvIGV2YWx1YW1vcyBzb2JyZSBsb3MgZGF0b3MgcmVhbGVzIGRlIGVuZXJvLWZlYnJlcm8gZGUgMjAyMCAocXVlIHNvbiBsYXMgb2JzZXJ2YWNpb25lcyBxdWUgc2UgcHJvbm9zdGljYXJvbikuDQo8L2Rpdj4NCg0KYGBge3J9DQojIERhdGEgRnJhbWUgYXJpbWEgZmluYWw6IA0KZGF0YV9mcmFtZShGZWNoYSA9IHNhbGlkYXMuMjAyMCRGZWNoYSAlPiUgYXMudmVjdG9yKCksDQogICAgICAgICAgIEFjdHVhbCA9IHNhbGlkYXMuMjAyMC50cyAlPiUgYXMudmVjdG9yKCksIA0KICAgICAgICAgICBQcmVkaWNjaW9uID0gZmMuZml0YXJpbWEuZmluYWwkbWVhbiAlPiUgYXMudmVjdG9yKCkgJT4lIHJvdW5kKDApLCANCiAgICAgICAgICAgRXJyb3IgPSBhYnMoUHJlZGljY2lvbiAtIEFjdHVhbCksDQogICAgICAgICAgIEVycm9yX1BvcmNlbnR1YWwgPSByb3VuZChFcnJvciAvIEFjdHVhbCwgMikpICU+JSANCiAga2JsKGNhcHRpb24gPSAiUFJPTsOTU1RJQ08gRU5FUk8tRkVCUkVSTyAyMDIwIiwgYWxpZ24gPSAiYyIsDQogICAgICAgICAgICAgICAgICAgIGRpZ2l0cyA9IDQgLCANCiAgICAgICAgICAgICAgICAgICAgZm9ybWF0LmFyZ3MgPSBsaXN0KGJpZy5tYXJrID0gIiwiKSkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpgYGANCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkNvbmNsdXNpb25lczwvc3Bhbj4NCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpMYSBzZXJpZSBwcmVzZW50w7MgY2FtYmlvIGRlIHRlbmRlbmNpYSBlbiBlbCBhw7FvIDIwMTEgZGViaWRvIGEgZGl2ZXJzb3MgZmFjdG9yZXMgY295dW50dXJhbGVzIHF1ZSBzZSBwcmVzZW50YXJvbiBhIGxvIGxhcmdvIGRlbCBwZXJpb2RvIDIwMDgtMjAxMSwgY29tbyBsYSBjcmlzaXMgZmluYW5jaWVyYSBkZSAyMDA5LCBsYSBwYW5kZW1pYSBkZSBJbmZsdWVuemEgSDFOMSB5IGxhIHF1aWVicmEgZGUgbGEgZW1wcmVzYSBNZXhpbmFuYSBkZSBBdmlhY2nDs24uDQoNClNlIGRldGVjdGFyb24gcGF0cm9uZXMgZXN0YWNpb25hbGVzIGVuIGxvcyBtZXNlcyBkZSBqdWxpbywgYWdvc3RvIHkgZGljaWVtYnJlIChwZXJpb2RvIGRlIGFsdGEgZGVtYW5kYSkgeSBlbiBlbmVybywgZmVicmVybyB5IHNlcHRpZW1icmUgKHBlcmlvZG8gZGUgYmFqYSBkZW1hbmRhKS4NCg0KQ3VhbmRvIHNlIHZheWEgYSB0cmFiYWphciBjb24gc2VyaWVzIGRlIHRpZW1wbyBlcyBkZSBzdW1hIGltcG9ydGFuY2lhIGVsIGhhY2VyIHByaW1lcm8gdW4gYW7DoWxpc2lzIHZpc3VhbCBhIGxhIHNlcmllIG8gc2VyaWVzLCB5YSBxdWUgZXNvIHBlcm1pdGlyw6EgY29ub2NlciBzaSBoYXkgcHJlc2VuY2lhIGRlIHRlbmRlbmNpYSwgc3UgdGlwbyB5IHNpIGV4aXN0ZSBzb3NwZWNoYSBkZSBhbGfDum4gcGF0csOzbiBlc3RhY2lvbmFsLiBMbyBhbnRlcmlvciwgcGVybWl0aXLDoSBpZGVudGlmaWNhciBlbCB0aXBvIGRlIGFsZ29yaXRtbyBhIHVzYXIsIHlhIHF1ZSBoYXkgYWxndW5vcyBxdWUgbm8gZXN0w6FuIGRpc2XDsWFkb3MgcGFyYSBtb2RlbGFyIG3DoXMgZGUgdW4gcGF0csOzbiBlc3RhY2lvbmFsIGNvbW8gbG9zIGRlIHN1YXZpc2FkbyBleHBvbmVuY2lhbCBvIGFyaW1hLg0KDQpFcyBhbHRhbWVudGUgcmVjb21lbmRhYmxlIGhhY2VyIHVuIHByaW1lciBtb2RlbG8gcXVlIHNlcsOhIGxhIGJhc2UgYSBwYXJ0aXIgZGVsIGN1YWwgc2UgdHJhdGFyw6EgZGUgbWVqb3JhcmxvIGRlIGFjdWVyZG8gY29uIGxhIG3DqXRyaWNhIGVsZWdpZGEgcGFyYSBzdSBldmFsdWFjacOzbiB5LCBhZGVtw6FzLCBubyBvbHZpZGFyIHF1ZSBsYSBldmFsdWFjacOzbiBzZSBkZWJlIGRlIGhhY2VyIGVuIGxhIHBhcnRlIGRlIHBydWViYSwgZXMgZGVjaXIsIHNvYnJlIGVsIGNvbmp1bnRvIGRlIGRhdG9zIHF1ZSBlbCBhbGdvcml0bW8gbm8gaGEgdmlzdG8uDQoNCkN1YW5kbyBzZSBvcHRlIHBvciBoYWNlciB1biBlbnNhbWJsZSBkZSBtb2RlbG9zIHNlIGRlYmUgdGVuZXIgZW4gY3VlbnRhIHF1ZSBlc3RhIGVzdHJhdMOpZ2lhIGZ1bmNpb25hcsOhIG1lam9yIHNpIGxvcyBtb2RlbG9zIGEganVudGFyIHByZXNlbnRhbiBkaWZlcmVuY2lhcyBtYXJjYWRhcyBlbnRyZSBzw60sIHBvcnF1ZSBkZSBsbyBjb250cmFyaW8gc2UgcHVlZGUgZ2VuZXJhciB1biBwZW9yIHJlc3VsdGFkby4NCjwvZGl2Pg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQo=