Introducción

El presente cuaderno resume los temas vistos en el curso de estadística multivariante de R y Python.

Primero, Primero ajustamos librerías de Python y R, para poder trabajar con ambos de forma simultanea

Cargamos librerias de Python

reticulate::repl_python()
# ===============================
# 📦 Librerías necesarias en Python
# ===============================

import numpy as np                        # Álgebra lineal y operaciones numéricas
import pandas as pd                       # Manipulación y análisis de datos en DataFrames
import matplotlib.pyplot as plt           # Visualización básica y gráficos 2D
import seaborn as sns                     # Visualización estadística (gráficos más elaborados)
import plotly.express as px               # Visualización interactiva (2D y 3D)

# --------------------------------
# 🔹 Estadística y distribuciones
# --------------------------------
from scipy.stats import norm              # Distribución normal y funciones estadísticas
from scipy.stats import multivariate_normal # Distribución normal multivariante
from scipy.stats import chi2              # Distribución chi-cuadrado
from scipy.stats import bartlett          # Test de esfericidad de Bartlett
from scipy.stats import multivariate_t    # Distribución t multivariante
from scipy.spatial.distance import mahalanobis  # Distancia de Mahalanobis
from scipy.spatial.distance import pdist        # Distancias por pares entre observaciones

# --------------------------------
# 🔹 Modelos estadísticos
# --------------------------------
import statsmodels.api as sm              # Modelos estadísticos (regresión, tests, etc.)
from statsmodels.multivariate.manova import MANOVA  # MANOVA (Análisis multivariante de varianza)

# --------------------------------
# 🔹 Machine Learning (scikit-learn)
# --------------------------------
from sklearn.datasets import load_iris    # Dataset de ejemplo (Iris)
from sklearn.datasets import fetch_openml # Descarga de datasets de OpenML
from sklearn.preprocessing import StandardScaler  # Escalamiento de datos
from sklearn.preprocessing import MinMaxScaler    # Normalización de datos [0,1]
from sklearn.decomposition import PCA     # Análisis de Componentes Principales
from sklearn.manifold import MDS          # Escalamiento multidimensional
from sklearn.cluster import KMeans        # Clustering k-means
from sklearn_extra.cluster import KMedoids  # Clustering k-medoids
from sklearn.mixture import GaussianMixture     # Modelos de mezcla gaussiana (clustering)
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis # Análisis discriminante lineal (LDA)
from sklearn.neighbors import KNeighborsClassifier # Clasificación k-NN
from sklearn.naive_bayes import GaussianNB       # Clasificador Naive Bayes Gaussiano
from sklearn.linear_model import LogisticRegression # Regresión logística
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet  # Modelos de regresión
from sklearn.linear_model import RidgeCV, LassoCV, ElasticNetCV   # Regularización con validación cruzada
from sklearn.metrics import mean_squared_error, r2_score           # Métricas de desempeño de modelos
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV  # Validación de modelos

# --------------------------------
# 🔹 Balanceo y pipelines (ML)
# --------------------------------
from imblearn.over_sampling import SMOTE   # Oversampling para clases desbalanceadas
from imblearn.pipeline import Pipeline     # Pipelines con técnicas de balanceo

# --------------------------------
# 🔹 Análisis factorial
# --------------------------------
from factor_analyzer import FactorAnalyzer, calculate_bartlett_sphericity, calculate_kmo  
# Análisis factorial y tests de adecuación (Bartlett y KMO)

# --------------------------------
# 🔹 Herramientas adicionales
# --------------------------------
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster  # Clustering jerárquico
from math import pi                        # Constantes matemáticas
from pandas.plotting import scatter_matrix, andrews_curves, parallel_coordinates  
# Gráficos multivariantes con pandas

Cargamos librerías de R

quit
# Cargar librerias
# Cargar librerías necesarias
library(dplyr)        # Manipulación y transformación de datos (select, filter, mutate, summarise)
library(ggplot2)      # Visualización de datos (gráficos estadísticos y personalizables)
library(GGally)       # Extiende ggplot2 con gráficos multivariantes (pairs, ggpairs)

library(MASS)         # Modelos estadísticos y funciones (mvrnorm, kde2d, LDA)
library(ggthemes)     # Temas y estilos para mejorar la estética en ggplot2
library(plotly)       # Visualización interactiva (2D y 3D)
library(ggrepel)      # Etiquetas en gráficos sin solapamiento
library(mvtnorm)      # Simulación y densidades de distribuciones normales multivariantes
library(mclust)       # Modelos de mezcla gaussiana y clustering
library(copula)       # Modelos de dependencia entre variables (cópulas)
library(Hotelling)    # Test estadístico T² de Hotelling
library(ellipse)      # Gráficos de elipses de confianza multivariantes
library(psych)        # Análisis psicométrico y de factores
library(GPArotation)  # Rotaciones de factores en análisis factorial
library(corrplot)     # Visualización de matrices de correlación
library(tidyr)        # Transformación de datos (pivot_longer, pivot_wider, separate)
library(vegan)        # Análisis multivariante (ordenaciones, diversidad, ecología)
library(cluster)      # Métodos de clustering (k-means, PAM, silhouette, etc.)
library(factoextra)   # Visualización de resultados de clustering y PCA
library(caret)        # Entrenamiento, validación y ajuste de modelos
library(class)        # Clasificación supervisada (kNN)
library(e1071)        # Algoritmos de machine learning (Naive Bayes, SVM)
library(glmnet)       # Regularización (Ridge, Lasso, Elastic Net)
library(ggpubr)       # Gráficos listos para publicaciones (estadísticos y comparaciones)
library(broom)        # Convierte resultados de modelos en data frames ordenados
library(car)          # Herramientas para regresión (VIF, Anova, etc.)
library(leaps)        # Selección de variables en regresión (best subset)
library(ddalpha)      # Métodos de profundidad de datos y clasificación robusta
library(scatterplot3d)# Gráficos 3D simples
library(tibble)       # Data frames modernos (tibbles)
library(patchwork)    # Composición de múltiples gráficos ggplot2
library(aplpack)      # Gráficos robustos (bagplots, faces plots)
library(viridis)      # Paletas de color perceptualmente uniformes
library(tidyverse)    # Colección de paquetes para ciencia de datos (dplyr, ggplot2, tidyr, readr, etc.)
library(janitor)      # Limpieza de datos (clean_names, tabyl)
library(scales)       # Etiquetas y escalas en gráficos (percent, comma, etc.)


library(MASS)
library(ICSNP)
library(ggplot2)


# Quitar notacion cientifica
options(scipen = 999)  # Cuanto mayor el número, menos notación científica

1) Espacio multivariante

La estadística multivariante sirve para analizar simultáneamente múltiples variables con el fin de entender las relaciones que existen entre ellas y cómo estas influyen en fenómenos complejos. Es especialmente útil cuando los datos contienen muchos atributos interrelacionados, ya que permite identificar patrones, reducir dimensiones, clasificar observaciones, agrupar elementos similares o predecir resultados. Se aplica en diversas áreas como el marketing, la economía, la biología o las ciencias sociales, donde los fenómenos no pueden explicarse adecuadamente con una sola variable, y se requiere una visión integral y estructurada de los datos.

Los métodos de la estadística multivariante suelen agruparse en dos grandes enfoques: la minería de datos y la estadística inferencial. La minería de datos se enfoca en descubrir patrones, relaciones ocultas o estructuras dentro de grandes volúmenes de datos, muchas veces sin una hipótesis previa; es más exploratoria y se basa en algoritmos como los árboles de decisión, redes neuronales o métodos de clustering. Por otro lado, la estadística inferencial parte de un marco teórico más riguroso y busca hacer generalizaciones sobre una población a partir de una muestra, usando modelos que permiten estimar parámetros, contrastar hipótesis y establecer relaciones causales, como la regresión multivariante o el análisis discriminante. Ambos enfoques son complementarios y permiten obtener una visión más completa y robusta del fenómeno estudiado.

Análisis con “Bagplot”

1.1) Ejemplo con datos departamentales de USA (R)

El dataset US State Facts and Figures de R reúne información geográfica, demográfica y socioeconómica sobre los 50 estados de Estados Unidos, organizada alfabéticamente. Incluye abreviaciones, nombres completos, áreas en millas cuadradas, centros geográficos aproximados, divisiones y regiones oficiales del país, así como una matriz llamada state.x77 con estadísticas clave de cada estado, como población estimada en 1975, ingreso per cápita en 1974, tasa de analfabetismo, esperanza de vida, tasa de homicidios, porcentaje de graduados de secundaria, días promedio de heladas y superficie territorial. Esta información proviene del Departamento de Comercio de EE.UU., con datos recolectados principalmente en los años 1970s, y es útil para análisis comparativos y visualizaciones espaciales o estadísticas multivariadas.

# Cargar dataset
X <- as.data.frame(state.x77)
glimpse(X)
Rows: 50
Columns: 8
$ Population <dbl> 3615, 365, 2212, 2110, 21198, 2541, 3100, 5…
$ Income     <dbl> 3624, 6315, 4530, 3378, 5114, 4884, 5348, 4…
$ Illiteracy <dbl> 2.1, 1.5, 1.8, 1.9, 1.1, 0.7, 1.1, 0.9, 1.3…
$ `Life Exp` <dbl> 69.05, 69.31, 70.55, 70.66, 71.71, 72.06, 7…
$ Murder     <dbl> 15.1, 11.3, 7.8, 10.1, 10.3, 6.8, 3.1, 6.2,…
$ `HS Grad`  <dbl> 41.3, 66.7, 58.1, 39.9, 62.6, 63.9, 56.0, 5…
$ Frost      <dbl> 20, 152, 15, 65, 20, 166, 139, 103, 11, 60,…
$ Area       <dbl> 50708, 566432, 113417, 51945, 156361, 10376…
n.X <- nrow(X)
p.X <- ncol(X)

Vemos que el conjunto de datos tiene 50 filas (observaciones de cada estado) y 8 columnas (Variables de cada observacion)

1.1.1) Visualización de datos

Graficamos boxplot por variable

X_long <- X %>% 
  rownames_to_column("State") %>%
  pivot_longer(-State, names_to = "Variable", values_to = "Value")

ggplot(X_long, aes(x = Variable, y = Value)) +
  geom_boxplot(fill = "skyblue") +
  facet_wrap(~Variable, scales = "free") +
  theme_minimal()

NA
NA

Vemos Bagplot entre ingresos y expectativa de vida

bagplot(X$Income, X$`Life Exp`,
        xlab = "Income",
        ylab = "Life Expectancy",
        main = "Bagplot: Income vs Life Exp")

El gráfico bagplot de “Income vs Life Expectancy” muestra visualmente cómo se relacionan el ingreso y la esperanza de vida, destacando tanto la tendencia central como la dispersión de los datos: la región central oscura (el “bag”) encierra el 50% de las observaciones, indicando una fuerte concentración de datos en ese rango; el área más clara (el “fence”) delimita el rango extendido y permite identificar valores atípicos, mientras que las líneas rojas (“rays”) señalan la dirección y magnitud de estos valores extremos, lo que facilita detectar patrones, simetrías o anomalías en la distribución conjunta de ambas variables.

Histogramas de la variable “Murder” con diferentes binwidths

El binwidth es el ancho de cada barra en un histograma y determina cómo se agrupan los datos en intervalos; un valor pequeño muestra más detalle y variabilidad, mientras que uno grande agrupa más datos en menos barras, dando una visión más general de la distribución.

binwidths <- c(1, 2, 3)

plots <- lapply(binwidths, function(bw) {
  # Secuencia de cortes basada en binwidth y el rango de los datos
  breaks_seq <- seq(floor(min(X$Murder)), ceiling(max(X$Murder)), by = bw)
  
  ggplot(X, aes(x = Murder)) +
    geom_histogram(binwidth = bw, fill = "blue", alpha = 0.6, color = "black") +
    scale_x_continuous(breaks = breaks_seq) +
    labs(
      title = paste("Murder con binwidth =", bw),
      x = "Tasa de homicidios",
      y = "Frecuencia"
    ) +
    theme_minimal()
})

wrap_plots(plots, ncol = 1)

El gráfico muestra tres histogramas que representan la distribución de la variable Murder (tasa de homicidios por estado en EE. UU.), cada uno con un diferente valor de binwidth (1, 2 y 3), lo cual controla el ancho de las barras y, por tanto, el nivel de detalle con que se agrupan los datos. Al comparar los paneles, se observa que un binwidth más pequeño (izquierda) revela más detalle pero puede generar ruido visual, mientras que uno más grande (derecha) suaviza la distribución, facilitando la interpretación general pero ocultando variaciones sutiles. Este ejercicio nos ayuda a entender cómo el tamaño del bin influye en la percepción de la forma de la distribución y permite elegir un nivel de agregación adecuado según el objetivo del análisis.

Histogramas de todas las variables (Sturges)

X_long %>%
  ggplot(aes(x = Value)) +
  geom_histogram(bins = 30, fill = "blue", color = "white") +
  facet_wrap(~Variable, scales = "free") +
  theme_minimal()

Densidades kernel por variable

X_long %>%
  ggplot(aes(x = Value)) +
  geom_density(fill = "blue", alpha = 0.4) +
  facet_wrap(~Variable, scales = "free") +
  labs(title = "Densidad kernel por variable") +
  theme_minimal()

El gráfico muestra las curvas de densidad kernel para cada variable cuantitativa del dataset state.x77, lo que permite visualizar la forma de su distribución sin depender de los histogramas. Al usar facet_wrap, cada variable se representa en un panel independiente con escalas libres, lo que facilita comparar sus formas. Por ejemplo, variables como IlliteracyPopulation y Area presentan distribuciones asimétricas con sesgo a la derecha (concentración de valores bajos y pocos valores altos), mientras que variables como Life Exp o Income tienen formas más simétricas o con una leve asimetría. Este tipo de gráfico permite identificar patrones como bimodalidad, concentración de valores y sesgos, siendo útil para decidir transformaciones o verificar supuestos estadísticos previos a un análisis multivariante.

Vemos mapas de calor de densidad bivariada con su respectivo contorno


# Cargar dataset
X <- as.data.frame(state.x77)

# Extraer variables de interés
x <- X$Income
y <- X$`Life Exp`


bivkde <- kde2d(x, y, n = 100)


bivkde_df <- expand.grid(x = bivkde$x, y = bivkde$y)
bivkde_df$z <- as.vector(bivkde$z)


p1 <- ggplot(bivkde_df, aes(x = x, y = y, fill = z)) +
  geom_tile() +
  scale_fill_viridis_c() +
  labs(title = "Mapa de calor de densidad bivariada",
       x = "Income", y = "Life Expectancy", fill = "Densidad") +
  theme_minimal()

print(p1)



p2 <- ggplot(bivkde_df, aes(x = x, y = y, z = z)) +
  geom_contour_filled() +
  labs(title = "Contornos de densidad bivariada",
       x = "Income", y = "Life Expectancy", fill = "Nivel") +
  theme_minimal()

print(p2)

NA
NA

Los gráficos de densidad bivariada de “Income” versus “Life Expectancy” permiten visualizar cómo se distribuyen conjuntamente estas dos variables en el conjunto de datos: el mapa de calor muestra que la mayor concentración de observaciones se encuentra en un rango de ingresos entre aproximadamente 4500 y 6000 unidades monetarias y una esperanza de vida entre 70 y 71 años, lo cual se representa con colores más claros (amarillo); por su parte, el gráfico de contornos refuerza esta interpretación al delimitar zonas de igual densidad, destacando un núcleo central de alta densidad rodeado por niveles decrecientes, lo que sugiere una relación positiva moderada entre ingreso y esperanza de vida, con pocos valores extremos alejados de esta concentración principal.

Visualizamos lo anterior en en un grafico dinamico

p3 <- plot_ly(x = bivkde$x, y = bivkde$y, z = bivkde$z) %>%
  add_surface(colorscale = 'Viridis') %>%
  layout(title = "Superficie de densidad bivariada (3D)",
         scene = list(
           xaxis = list(title = "Income"),
           yaxis = list(title = "Life Expectancy"),
           zaxis = list(title = "Densidad")
         ))

p3  
NA

Comparación de kernel Gaussiano vs Epanechnikov

La principal diferencia entre el kernel Gaussiano y el Epanechnikov radica en la forma en que suavizan la distribución de los datos: el kernel Gaussiano utiliza una función de densidad normal que asigna peso a todos los puntos, aunque decrece exponencialmente con la distancia, lo que produce curvas suaves y extendidas; en cambio, el kernel Epanechnikov es más eficiente computacionalmente, tiene soporte compacto (solo considera puntos dentro de un cierto rango) y genera curvas más concentradas alrededor de los datos, con bordes más marcados. En la práctica, ambos estimadores suelen producir resultados similares, pero el Epanechnikov es óptimo en términos de varianza mínima para muchos casos, mientras que el Gaussiano es preferido por su suavidad y familiaridad.

plot_list <- lapply(unique(X_long$Variable), function(var){
  ggplot(filter(X_long, Variable == var), aes(x = Value)) +
    geom_density(kernel = "gaussian", color = "blue", fill = "blue", alpha = 0.3) +
    geom_density(kernel = "epanechnikov", color = "green", fill = "green", alpha = 0.2) +
    labs(title = paste("Kernel Gaussiano vs Epanechnikov:", var)) +
    theme_minimal()
})
wrap_plots(plot_list)

El gráfico compara dos estimaciones de densidad kernel para cada variable del dataset state.x77 usando los núcleos Gaussiano (línea azul) y Epanechnikov (línea verde). Cada panel muestra cómo varía la forma de la distribución según el tipo de kernel, aunque en la mayoría de los casos las diferencias son sutiles. Ambos métodos suavizan la distribución, pero el kernel gaussiano suele generar curvas más suaves y extendidas, mientras que el Epanechnikov tiende a ser más eficiente y local, marcando picos ligeramente más agudos. Esta comparación permite ver que, si bien el núcleo elegido puede influir levemente en la forma estimada, en distribuciones bien definidas (como Income o Life Exp) ambas curvas son muy similares, mientras que en otras más irregulares o asimétricas (como Area o Population) las diferencias pueden ser más evidentes. En general, el análisis resalta la importancia de considerar el tipo de kernel al interpretar estructuras en los datos.

Scatterplot Income vs Life Exp

ggplot(X, aes(x = Income, y = `Life Exp`)) +
  geom_point(color = "blue") +
  labs(title = "Income vs Life Expectancy") +
  theme_minimal()

El gráfico de dispersión muestra la relación entre el ingreso per cápita (Income) y la esperanza de vida (Life Exp) en los estados de EE. UU., revelando una tendencia positiva general, donde los estados con mayores ingresos tienden a tener una mayor esperanza de vida. Sin embargo, la relación no es perfectamente lineal, ya que hay cierta dispersión y algunos estados con ingresos altos no necesariamente tienen la mayor esperanza de vida, lo que sugiere que aunque el ingreso influye, otros factores también juegan un papel importante en la longevidad. Esta visualización permite identificar patrones generales, así como posibles excepciones o valores atípicos.

Gráfico 3D con plotly

plot_ly(data = X, x = ~Income, y = ~`Life Exp`, z = ~Murder, 
        type = "scatter3d", mode = "markers",
        marker = list(size = 5, color = "blue"))
NA

Pairs plot (scatterplot matrix)

GGally::ggpairs(X)

El gráfico generado con GGally::ggpairs(X) muestra una matriz de correlaciones y relaciones bivariadas entre todas las variables numéricas del dataset state.x77. Cada celda inferior presenta un diagrama de dispersión que permite visualizar la relación entre dos variables, mientras que las diagonales muestran la distribución univariada (densidad) de cada variable, y las celdas superiores presentan los coeficientes de correlación de Pearson, incluyendo su significancia estadística (******). Se destacan correlaciones fuertes como la negativa entre Illiteracy y HS Grad (-0.657), y entre Murder y Life Exp (-0.781), lo que sugiere que mayores tasas de alfabetización y educación secundaria están asociadas con menor criminalidad y mayor esperanza de vida. También se observa una correlación positiva moderada entre IncomeHS Grad (0.620), y entre Income y Area (0.363), lo que puede indicar una asociación entre desarrollo económico y extensión territorial. Este tipo de gráfico facilita la detección de patrones, asociaciones clave y posibles variables explicativas para análisis posteriores como regresión o segmentación.

Coordenadas paralelas

parcoord(X, col = "blue", var.label = TRUE)

Este es un gráfico de coordenadas paralelas que representa todas las variables numéricas del dataset state.x77 para los 50 estados de EE. UU. Cada línea azul representa un estado, y su recorrido conecta los valores que tiene ese estado en variables como PopulationIncomeIlliteracyLife ExpMurderHS GradFrost y Area. Al observar la trama de líneas, se pueden identificar patrones generales: por ejemplo, estados con alta tasa de Illiteracy tienden a tener menor Life Exp y mayor Murder, mientras que aquellos con mayor HS Grad suelen tener menor Illiteracy y mayor Income. También se observan algunos estados que se comportan como outliers, con valores extremos en Population o Area. Este tipo de gráfico es útil para explorar relaciones multivariadas y contrastar perfiles de observaciones sin necesidad de reducir la dimensionalidad.

1.1.2) Medidas descriptivas multivariantes

Calculamos las medias para cada unas de las variables

mu.X <- colMeans(X)
mu.X
Population     Income Illiteracy   Life Exp     Murder 
 4246.4200  4435.8000     1.1700    70.8786     7.3780 
   HS Grad      Frost       Area 
   53.1080   104.4600 70735.8800 

Calculamos profundidad de Tukey

depth.X <- depth.halfspace(X, X, num.directions = 100000, seed = 1)
sort.depth.X <- sort(depth.X, decreasing = TRUE, index.return = TRUE)

depth.X.sort <- sort.depth.X$x
depth.X.sort.index <- sort.depth.X$ix

# Estado más profundo (más central)
X[depth.X.sort.index[1], ]
NA

El análisis aplica la medida de profundidad de halfspace (o Tukey depth) al dataset state.x77, una técnica de estadística robusta que evalúa cuán “central” es cada observación dentro del conjunto multivariado. Se calcularon las profundidades de todos los estados con respecto al conjunto completo, usando 100.000 direcciones aleatorias, y luego se ordenaron en orden descendente para identificar el estado más profundo, es decir, el más representativo o central según la distribución multivariada de las variables. El resultado muestra que Wisconsin es el estado más profundo, lo que indica que sus valores en variables como PopulationIncomeIlliteracyLife ExpMurderHS GradFrost y Area están equilibradamente ubicados respecto a las demás observaciones, sin ser extremos en ninguna dimensión, y por tanto representa bien el “centro” del conjunto de datos.

Calculamos matriz de covarianza y correlación

S.X <- cov(X)
R.X <- cor(X)

eigen_S <- eigen(S.X)
eigen_R <- eigen(R.X)

list(
  Covarianza = S.X,
  Autovalores_S = eigen_S$values,
  Autovectores_S = eigen_S$vectors,
  Correlación = R.X,
  Autovalores_R = eigen_R$values,
  Autovectores_R = eigen_R$vectors,
  Traza_S = sum(eigen_S$values),
  Determinante_S = det(S.X),
  Traza_R = sum(eigen_R$values),
  Determinante_R = det(R.X)
)
$Covarianza
              Population        Income   Illiteracy
Population 19931683.7588   571229.7796  292.8679592
Income       571229.7796   377573.3061 -163.7020408
Illiteracy      292.8680     -163.7020    0.3715306
Life Exp       -407.8425      280.6632   -0.4815122
Murder         5663.5237     -521.8943    1.5817755
HS Grad       -3551.5096     3076.7690   -3.2354694
Frost        -77081.9727     7227.6041  -21.2900000
Area        8587916.9494 19049013.7510 4018.3371429
                 Life Exp       Murder       HS Grad
Population   -407.8424612  5663.523714  -3551.509551
Income        280.6631837  -521.894286   3076.768980
Illiteracy     -0.4815122     1.581776     -3.235469
Life Exp        1.8020204    -3.869480      6.312685
Murder         -3.8694804    13.627465    -14.549616
HS Grad         6.3126849   -14.549616     65.237894
Frost          18.2867796  -103.406000    153.992163
Area       -12294.1022122 71940.429959 229873.192816
                  Frost           Area
Population -77081.97265    8587916.949
Income       7227.60408   19049013.751
Illiteracy    -21.29000       4018.337
Life Exp       18.28678     -12294.102
Murder       -103.40600      71940.430
HS Grad       153.99216     229873.193
Frost        2702.00857     262703.893
Area       262703.89306 7280748060.842

$Autovalores_S
[1] 7280808080.47470760   19937180.72785066     312643.58817004
[4]       2153.06886252         36.51904597          6.05909969
[7]          0.43298101          0.08409483

$Autovectores_S
                 [,1]           [,2]          [,3]
[1,] -0.0011829664738  0.99960045732  0.0278490777
[2,] -0.0026165501162  0.02796866363 -0.9991766328
[3,] -0.0000005518945  0.00001420515  0.0005844687
[4,]  0.0000016885212 -0.00001928393 -0.0010367078
[5,] -0.0000098815218  0.00027871279  0.0027764911
[6,] -0.0000315728830 -0.00018825453 -0.0082661337
[7,] -0.0000360716330 -0.00387162980 -0.0280421226
[8,] -0.9999958759200 -0.00125553761  0.0025827049
               [,4]            [,5]           [,6]
[1,] -0.00467125370  0.000334939315  0.00013866612
[2,]  0.02821731578 -0.007792881707 -0.00011195624
[3,]  0.00710074735 -0.040547431084 -0.03091522407
[4,] -0.00387596557  0.119329484966  0.28553570067
[5,]  0.02816091766 -0.238663760959 -0.92008523057
[6,] -0.02784544637  0.962238474694 -0.26563509895
[7,] -0.99877330340 -0.034529199839 -0.01986814388
[8,] -0.00003168841 -0.000006558672  0.00001882356
                 [,7]            [,8]
[1,] -0.0000518345382  0.000021918497
[2,]  0.0000385050593  0.000062904035
[3,]  0.0255065614586  0.998348039342
[4,]  0.9508427231156 -0.010576167082
[5,]  0.3058552186350 -0.046201071046
[6,] -0.0407555641724  0.032099248065
[7,]  0.0062527013155  0.004942863645
[8,] -0.0000004090819 -0.000001494594

$Correlación
            Population     Income  Illiteracy    Life Exp
Population  1.00000000  0.2082276  0.10762237 -0.06805195
Income      0.20822756  1.0000000 -0.43707519  0.34025534
Illiteracy  0.10762237 -0.4370752  1.00000000 -0.58847793
Life Exp   -0.06805195  0.3402553 -0.58847793  1.00000000
Murder      0.34364275 -0.2300776  0.70297520 -0.78084575
HS Grad    -0.09848975  0.6199323 -0.65718861  0.58221620
Frost      -0.33215245  0.2262822 -0.67194697  0.26206801
Area        0.02254384  0.3633154  0.07726113 -0.10733194
               Murder     HS Grad      Frost        Area
Population  0.3436428 -0.09848975 -0.3321525  0.02254384
Income     -0.2300776  0.61993232  0.2262822  0.36331544
Illiteracy  0.7029752 -0.65718861 -0.6719470  0.07726113
Life Exp   -0.7808458  0.58221620  0.2620680 -0.10733194
Murder      1.0000000 -0.48797102 -0.5388834  0.22839021
HS Grad    -0.4879710  1.00000000  0.3667797  0.33354187
Frost      -0.5388834  0.36677970  1.0000000  0.05922910
Area        0.2283902  0.33354187  0.0592291  1.00000000

$Autovalores_R
[1] 3.5988956 1.6319192 1.1119412 0.7075042 0.3846417 0.3074617
[7] 0.1444488 0.1131877

$Autovectores_R
            [,1]        [,2]        [,3]        [,4]
[1,] -0.12642809  0.41087417  0.65632546  0.40938555
[2,]  0.29882991  0.51897884  0.10035919  0.08844658
[3,] -0.46766917  0.05296872 -0.07089849 -0.35282802
[4,]  0.41161037 -0.08165611  0.35993297 -0.44256334
[5,] -0.44425672  0.30694934 -0.10846751  0.16560017
[6,]  0.42468442  0.29876662 -0.04970850 -0.23157412
[7,]  0.35741244 -0.15358409 -0.38711447  0.61865119
[8,]  0.03338461  0.58762446 -0.51038499 -0.20112550
             [,5]        [,6]         [,7]        [,8]
[1,] -0.405946365 -0.01065617 -0.062158658  0.21924645
[2,]  0.637586953  0.46177023  0.009104712 -0.06029200
[3,] -0.003525994  0.38741578 -0.619800310  0.33868838
[4,] -0.326599685  0.21908161 -0.256213054 -0.52743331
[5,]  0.128068739 -0.32519611 -0.295043151 -0.67825134
[6,]  0.099264551 -0.64464647 -0.393019181  0.30724183
[7,] -0.217363791  0.21268413 -0.472013140 -0.02834442
[8,] -0.498506338  0.14836054  0.286260213 -0.01320320

$Traza_S
[1] 7301060101

$Determinante_S
[1] 787256890978966565159960576

$Traza_R
[1] 8

$Determinante_R
[1] 0.00893386

Este análisis explora las propiedades estructurales del conjunto de datos state.x77 mediante la matriz de covarianza y la matriz de correlación, junto con sus autovalores y autovectores. La matriz de covarianza (S.X) refleja cómo varían conjuntamente las variables en sus unidades originales, mostrando que Area y Population presentan una variabilidad especialmente alta, lo que también se evidencia en su autovalor dominante (≈ 7.28e+09). Los autovalores de S.X indican cuánta varianza explica cada componente principal, mientras que sus autovectores determinan la dirección de estos componentes. La traza (suma de autovalores) de S.X representa la varianza total del sistema (≈ 7.3e+09) y su determinante es muy alto, lo cual sugiere que las variables no están fuertemente correlacionadas linealmente. En contraste, la matriz de correlación (R.X) estandariza las relaciones, eliminando las unidades y permitiendo comparar relaciones entre variables de diferente escala. Sus autovalores suman 8 (por tener 8 variables estandarizadas), y el determinante bajo (≈ 0.0089) indica que hay redundancia o alta correlación entre algunas variables. Este análisis es fundamental en análisis de componentes principales (PCA) y permite comprender tanto la magnitud como la estructura de las relaciones entre variables.

Estandarización univariante y multivariante

# Univariante
sX <- scale(X)
GGally::ggpairs(as.data.frame(sX))


# Multivariante (esferización)
iS.X <- solve(S.X)
e <- eigen(iS.X)
V <- e$vectors
B <- V %*% diag(sqrt(e$values)) %*% t(V)
Xtil <- scale(X, scale = FALSE)
SX <- Xtil %*% B

colMeans(SX)
[1] -0.00000000000000001998401 -0.00000000000000027811087
[3] -0.00000000000000256059063 -0.00000000000000651478871
[5] -0.00000000000000181632487 -0.00000000000000276667578
[7]  0.00000000000000018207658 -0.00000000000000006994405
cov(SX)
                        [,1]                   [,2]
[1,]  1.00000000000194666505  0.0000000000101811071
[2,]  0.00000000001018110709  1.0000000000532698330
[3,] -0.00000000000009322022 -0.0000000000004854091
[4,]  0.00000000000007448464  0.0000000000003931379
[5,] -0.00000000000008317610 -0.0000000000004438734
[6,]  0.00000000000013484452  0.0000000000007066683
[7,]  0.00000000000031656310  0.0000000000016571359
[8,]  0.00000000000398631808  0.0000000000208568953
                          [,3]                      [,4]
[1,] -0.0000000000000932202161  0.0000000000000744846361
[2,] -0.0000000000004854090840  0.0000000000003931379032
[3,]  0.9999999999999990007993  0.0000000000000001524433
[4,]  0.0000000000000001524433  1.0000000000000006661338
[5,]  0.0000000000000006186236 -0.0000000000000038291366
[6,] -0.0000000000000040987621  0.0000000000000036660017
[7,] -0.0000000000000111948432  0.0000000000000124888761
[8,] -0.0000000000001905627371  0.0000000000001743412670
                          [,5]                     [,6]
[1,] -0.0000000000000831760964  0.000000000000134844517
[2,] -0.0000000000004438733961  0.000000000000706668284
[3,]  0.0000000000000006186236 -0.000000000000004098762
[4,] -0.0000000000000038291366  0.000000000000003666002
[5,]  1.0000000000000044408921 -0.000000000000007712651
[6,] -0.0000000000000077126514  1.000000000000005551115
[7,] -0.0000000000000164607557  0.000000000000022820748
[8,] -0.0000000000001683483285  0.000000000000276124928
                        [,7]                   [,8]
[1,]  0.00000000000031656310  0.0000000000039863181
[2,]  0.00000000000165713588  0.0000000000208568953
[3,] -0.00000000000001119484 -0.0000000000001905627
[4,]  0.00000000000001248888  0.0000000000001743413
[5,] -0.00000000000001646076 -0.0000000000001683483
[6,]  0.00000000000002282075  0.0000000000002761249
[7,]  1.00000000000004973799  0.0000000000006496595
[8,]  0.00000000000064965946  1.0000000000081668006
GGally::ggpairs(as.data.frame(SX))

Finalmente, graficamos la mediana multivariada por región

#  Carfamos Dataset  nuevmaente
X <- as.data.frame(state.x77)
X <- rownames_to_column(X, "Estado")

# Si tienes una variable de agrupación como Región, puedes agregarla aquí
# Por ejemplo:
X$Region <- state.region  # vector disponible en R base

# Calcular la mediana multivariada por grupo (por ejemplo, por Región)
medianas_region <- X %>%
  group_by(Region) %>%
  group_modify(~ {
    d <- depth.halfspace(.x[, c("Population", "Income", "Illiteracy", "Life Exp")],
                         .x[, c("Population", "Income", "Illiteracy", "Life Exp")],
                         num.directions = 100000, seed = 1)
    idx <- which.max(d)
    .x[idx, ]
  })


# Gráfico con etiquetas de región en las medianas
ggplot(X, aes(x = Income, y = `Life Exp`, color = Region)) +
  geom_point(alpha = 0.6) +
  geom_point(data = medianas_region,
             aes(x = Income, y = `Life Exp`),
             shape = 21, size = 5, fill = "black", color = "white") +
  geom_text_repel(data = medianas_region,
                  aes(x = Income, y = `Life Exp`, label = Region),
                  color = "black", size = 4, fontface = "bold") +
  labs(title = "Mediana multivariada por región con etiquetas") +
  theme_minimal()

NA
NA

Este código en R carga el conjunto de datos state.x77, añade el nombre del estado como columna y asigna a cada estado una región (Region) usando el vector state.region. Luego, calcula la mediana multivariada por región utilizando la función depth.halfspace, que identifica el punto más central (con mayor profundidad estadística) dentro de las variables Population, Income, Illiteracy y Life Exp. Finalmente, se genera un gráfico con ggplot2 que muestra la relación entre Income (ingreso) y Life Exp (esperanza de vida), donde cada punto representa un estado y está coloreado según su región. Los puntos grandes en negro indican la mediana multivariada de cada región, lo que permite comparar visualmente la posición central de cada grupo regional en el espacio de las dos variables seleccionadas.

1.2) Ejemplo con Python:

El dataset de Iris es un conjunto de datos clásico en estadística y aprendizaje automático que contiene 150 observaciones de flores del género Iris, divididas en tres especies: setosa, versicolor y virginica. Para cada flor se registran cuatro características numéricas: largo y ancho del sépalo, y largo y ancho del pétalo (en centímetros), lo que permite analizar diferencias entre especies y aplicar técnicas de clasificación, reducción de dimensión y visualización multivariada.

Primero, cargamos datos

reticulate::repl_python()


# Cargar el dataset
iris_bunch = load_iris()

# Crear el DataFrame con las variables + columna 'Species'
iris = pd.DataFrame(
    data=iris_bunch.data,
    columns=iris_bunch.feature_names
)

# Agregar directamente la columna 'Species' con nombres legibles
iris['Species'] = iris_bunch.target_names[iris_bunch.target]


# Validams los nombres de las columnas del df
iris.columns
Index(['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)',
       'petal width (cm)', 'Species'],
      dtype='object')

Ahora vamos a hacernos las primeras preguntas:

  1. ¿Cuántos datos hay en el dataset?
  2. ¿Cuántas variables?
  3. ¿Cuántos grupos de variables o categorías?
  4. ¿Cuántas observaciones corresponden a cada grupo?
iris.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   sepal length (cm)  150 non-null    float64
 1   sepal width (cm)   150 non-null    float64
 2   petal length (cm)  150 non-null    float64
 3   petal width (cm)   150 non-null    float64
 4   Species            150 non-null    object 
dtypes: float64(4), object(1)
memory usage: 6.0+ KB

Podemos ver que el conjunto de datos tine 150 filas por 5 columnas. De las cuales 4 son numericas y una es categorica

iris.head
<bound method NDFrame.head of      sepal length (cm)  sepal width (cm)  ...  petal width (cm)     Species
0                  5.1               3.5  ...               0.2      setosa
1                  4.9               3.0  ...               0.2      setosa
2                  4.7               3.2  ...               0.2      setosa
3                  4.6               3.1  ...               0.2      setosa
4                  5.0               3.6  ...               0.2      setosa
5                  5.4               3.9  ...               0.4      setosa
6                  4.6               3.4  ...               0.3      setosa
7                  5.0               3.4  ...               0.2      setosa
8                  4.4               2.9  ...               0.2      setosa
9                  4.9               3.1  ...               0.1      setosa
10                 5.4               3.7  ...               0.2      setosa
11                 4.8               3.4  ...               0.2      setosa
12                 4.8               3.0  ...               0.1      setosa
13                 4.3               3.0  ...               0.1      setosa
14                 5.8               4.0  ...               0.2      setosa
15                 5.7               4.4  ...               0.4      setosa
16                 5.4               3.9  ...               0.4      setosa
17                 5.1               3.5  ...               0.3      setosa
18                 5.7               3.8  ...               0.3      setosa
19                 5.1               3.8  ...               0.3      setosa
20                 5.4               3.4  ...               0.2      setosa
21                 5.1               3.7  ...               0.4      setosa
22                 4.6               3.6  ...               0.2      setosa
23                 5.1               3.3  ...               0.5      setosa
24                 4.8               3.4  ...               0.2      setosa
25                 5.0               3.0  ...               0.2      setosa
26                 5.0               3.4  ...               0.4      setosa
27                 5.2               3.5  ...               0.2      setosa
28                 5.2               3.4  ...               0.2      setosa
29                 4.7               3.2  ...               0.2      setosa
30                 4.8               3.1  ...               0.2      setosa
31                 5.4               3.4  ...               0.4      setosa
32                 5.2               4.1  ...               0.1      setosa
33                 5.5               4.2  ...               0.2      setosa
34                 4.9               3.1  ...               0.2      setosa
35                 5.0               3.2  ...               0.2      setosa
36                 5.5               3.5  ...               0.2      setosa
37                 4.9               3.6  ...               0.1      setosa
38                 4.4               3.0  ...               0.2      setosa
39                 5.1               3.4  ...               0.2      setosa
40                 5.0               3.5  ...               0.3      setosa
41                 4.5               2.3  ...               0.3      setosa
42                 4.4               3.2  ...               0.2      setosa
43                 5.0               3.5  ...               0.6      setosa
44                 5.1               3.8  ...               0.4      setosa
45                 4.8               3.0  ...               0.3      setosa
46                 5.1               3.8  ...               0.2      setosa
47                 4.6               3.2  ...               0.2      setosa
48                 5.3               3.7  ...               0.2      setosa
49                 5.0               3.3  ...               0.2      setosa
50                 7.0               3.2  ...               1.4  versicolor
51                 6.4               3.2  ...               1.5  versicolor
52                 6.9               3.1  ...               1.5  versicolor
53                 5.5               2.3  ...               1.3  versicolor
54                 6.5               2.8  ...               1.5  versicolor
55                 5.7               2.8  ...               1.3  versicolor
56                 6.3               3.3  ...               1.6  versicolor
57                 4.9               2.4  ...               1.0  versicolor
58                 6.6               2.9  ...               1.3  versicolor
59                 5.2               2.7  ...               1.4  versicolor
60                 5.0               2.0  ...               1.0  versicolor
61                 5.9               3.0  ...               1.5  versicolor
62                 6.0               2.2  ...               1.0  versicolor
63                 6.1               2.9  ...               1.4  versicolor
64                 5.6               2.9  ...               1.3  versicolor
65                 6.7               3.1  ...               1.4  versicolor
66                 5.6               3.0  ...               1.5  versicolor
67                 5.8               2.7  ...               1.0  versicolor
68                 6.2               2.2  ...               1.5  versicolor
69                 5.6               2.5  ...               1.1  versicolor
70                 5.9               3.2  ...               1.8  versicolor
71                 6.1               2.8  ...               1.3  versicolor
72                 6.3               2.5  ...               1.5  versicolor
73                 6.1               2.8  ...               1.2  versicolor
74                 6.4               2.9  ...               1.3  versicolor
75                 6.6               3.0  ...               1.4  versicolor
76                 6.8               2.8  ...               1.4  versicolor
77                 6.7               3.0  ...               1.7  versicolor
78                 6.0               2.9  ...               1.5  versicolor
79                 5.7               2.6  ...               1.0  versicolor
80                 5.5               2.4  ...               1.1  versicolor
81                 5.5               2.4  ...               1.0  versicolor
82                 5.8               2.7  ...               1.2  versicolor
83                 6.0               2.7  ...               1.6  versicolor
84                 5.4               3.0  ...               1.5  versicolor
85                 6.0               3.4  ...               1.6  versicolor
86                 6.7               3.1  ...               1.5  versicolor
87                 6.3               2.3  ...               1.3  versicolor
88                 5.6               3.0  ...               1.3  versicolor
89                 5.5               2.5  ...               1.3  versicolor
90                 5.5               2.6  ...               1.2  versicolor
91                 6.1               3.0  ...               1.4  versicolor
92                 5.8               2.6  ...               1.2  versicolor
93                 5.0               2.3  ...               1.0  versicolor
94                 5.6               2.7  ...               1.3  versicolor
95                 5.7               3.0  ...               1.2  versicolor
96                 5.7               2.9  ...               1.3  versicolor
97                 6.2               2.9  ...               1.3  versicolor
98                 5.1               2.5  ...               1.1  versicolor
99                 5.7               2.8  ...               1.3  versicolor
100                6.3               3.3  ...               2.5   virginica
101                5.8               2.7  ...               1.9   virginica
102                7.1               3.0  ...               2.1   virginica
103                6.3               2.9  ...               1.8   virginica
104                6.5               3.0  ...               2.2   virginica
105                7.6               3.0  ...               2.1   virginica
106                4.9               2.5  ...               1.7   virginica
107                7.3               2.9  ...               1.8   virginica
108                6.7               2.5  ...               1.8   virginica
109                7.2               3.6  ...               2.5   virginica
110                6.5               3.2  ...               2.0   virginica
111                6.4               2.7  ...               1.9   virginica
112                6.8               3.0  ...               2.1   virginica
113                5.7               2.5  ...               2.0   virginica
114                5.8               2.8  ...               2.4   virginica
115                6.4               3.2  ...               2.3   virginica
116                6.5               3.0  ...               1.8   virginica
117                7.7               3.8  ...               2.2   virginica
118                7.7               2.6  ...               2.3   virginica
119                6.0               2.2  ...               1.5   virginica
120                6.9               3.2  ...               2.3   virginica
121                5.6               2.8  ...               2.0   virginica
122                7.7               2.8  ...               2.0   virginica
123                6.3               2.7  ...               1.8   virginica
124                6.7               3.3  ...               2.1   virginica
125                7.2               3.2  ...               1.8   virginica
126                6.2               2.8  ...               1.8   virginica
127                6.1               3.0  ...               1.8   virginica
128                6.4               2.8  ...               2.1   virginica
129                7.2               3.0  ...               1.6   virginica
130                7.4               2.8  ...               1.9   virginica
131                7.9               3.8  ...               2.0   virginica
132                6.4               2.8  ...               2.2   virginica
133                6.3               2.8  ...               1.5   virginica
134                6.1               2.6  ...               1.4   virginica
135                7.7               3.0  ...               2.3   virginica
136                6.3               3.4  ...               2.4   virginica
137                6.4               3.1  ...               1.8   virginica
138                6.0               3.0  ...               1.8   virginica
139                6.9               3.1  ...               2.1   virginica
140                6.7               3.1  ...               2.4   virginica
141                6.9               3.1  ...               2.3   virginica
142                5.8               2.7  ...               1.9   virginica
143                6.8               3.2  ...               2.3   virginica
144                6.7               3.3  ...               2.5   virginica
145                6.7               3.0  ...               2.3   virginica
146                6.3               2.5  ...               1.9   virginica
147                6.5               3.0  ...               2.0   virginica
148                6.2               3.4  ...               2.3   virginica
149                5.9               3.0  ...               1.8   virginica

[150 rows x 5 columns]>
iris.shape
(150, 5)

Vemos el formato de del df que es 150 x 5

Renombramos df

iris_num=iris

Vemos los valores unicos de la variable especie

iris_num['Species'].value_counts()
Species
setosa        50
versicolor    50
virginica     50
Name: count, dtype: int64

¿Qué medidas numéricas podemos obtener para describir los datos?

iris_num.describe()
       sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)
count         150.000000        150.000000         150.000000        150.000000
mean            5.843333          3.057333           3.758000          1.199333
std             0.828066          0.435866           1.765298          0.762238
min             4.300000          2.000000           1.000000          0.100000
25%             5.100000          2.800000           1.600000          0.300000
50%             5.800000          3.000000           4.350000          1.300000
75%             6.400000          3.300000           5.100000          1.800000
max             7.900000          4.400000           6.900000          2.500000

Gráficamente, ¿cómo podemos ver la cantidad de observaciones en cada grupo?

sns.countplot('Species',data=iris)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
TypeError: countplot() got multiple values for argument 'data'
g
plt.show()

Veamos cómo hacer boxplots por grupos

 Boxplots
sns.boxplot(x="Species", y="PetalLengthCm", data=iris  ,linewidth=2.5 )
plt.show()

# Boxplots para cada variable divididos por grupos en "Species"
iris_num.boxplot(by="Species", figsize=(12, 6))
plt.show() #esto último se pone cuando no se quiere que salga el último mensaje informativo
array([[<Axes: title={'center': 'petal length (cm)'}, xlabel='[Species]'>,
        <Axes: title={'center': 'petal width (cm)'}, xlabel='[Species]'>],
       [<Axes: title={'center': 'sepal length (cm)'}, xlabel='[Species]'>,
        <Axes: title={'center': 'sepal width (cm)'}, xlabel='[Species]'>]],
      dtype=object)

Veamos cómo hacer histogramas por grupos

# Histogramas 
iris_num.hist(edgecolor='black', linewidth=1.2, grid=False, figsize=(12, 6))
array([[<Axes: title={'center': 'sepal length (cm)'}>,
        <Axes: title={'center': 'sepal width (cm)'}>],
       [<Axes: title={'center': 'petal length (cm)'}>,
        <Axes: title={'center': 'petal width (cm)'}>]], dtype=object)
plt.show()

Con scipy(sc).stats podemos obtener medidas numéricas que describen las variables

irissk = load_iris()

#irissk es ahora un numpy array 
X = irissk.data
Y = irissk.target

# Descripcion estadistica
sc.stats.describe(X)

Densidades kernels

sns.distplot(X[Y == 0,2],bins=10)
<string>:1: UserWarning: 

`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
sns.distplot(X[Y == 1,2],bins=10)
<Axes: title={'center': 'petal width (cm)'}, ylabel='Density'>
<string>:1: UserWarning: 

`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
<Axes: title={'center': 'petal width (cm)'}, ylabel='Density'>
sns.distplot(X[Y == 2,2],bins=10)
<string>:1: UserWarning: 

`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
plt.show()
<Axes: title={'center': 'petal width (cm)'}, ylabel='Density'>

KDE (Kernel density estimation) de una sola variable

sns.distplot(X[:,1],bins=10)
<string>:1: UserWarning: 

`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
<Axes: title={'center': 'petal width (cm)'}, ylabel='Density'>
plt.show()

Realizamos grafico de dispersion

#Scatterplot donde salen también histogramas


sns.jointplot(
    x='sepal length (cm)',
    y='sepal width (cm)',
    data=iris,
    kind='scatter',   # (opcional: 'scatter', 'kde', 'hex', etc.)
    color='steelblue'
)
<seaborn.axisgrid.JointGrid object at 0x32ee99090>
plt.show()

Creamos graficos de pares

# Scatterplot matrix (kde en la diagonal)
sns.pairplot(iris_num,hue='Species')
<seaborn.axisgrid.PairGrid object at 0x31b13f1d0>
plt.show()

Venos otro tipo de grafico de la desnidad de kernel

# Otra forma de cargar los datos IRIS (con seaborn - sns)
iris = sns.load_dataset("iris")
print(iris.head()) # no tiene variable Id, las demas vbles se llaman diferentes y las categorias tambien
   sepal_length  sepal_width  petal_length  petal_width species
0           5.1          3.5           1.4          0.2  setosa
1           4.9          3.0           1.4          0.2  setosa
2           4.7          3.2           1.3          0.2  setosa
3           4.6          3.1           1.5          0.2  setosa
4           5.0          3.6           1.4          0.2  setosa

Densidad de kernel bimidensional

# Densidad kernel bidimensional


# Subconjuntos
setosa = iris[iris.species == "setosa"]
virginica = iris[iris.species == "virginica"]

# Gráfico
sns.kdeplot(x=setosa.sepal_length, y=setosa.sepal_width, cmap="Reds", fill=True, alpha=0.5)
sns.kdeplot(x=virginica.sepal_length, y=virginica.sepal_width, cmap="Blues", fill=True, alpha=0.5)
<Axes: xlabel='petal width (cm)', ylabel='Density'>
plt.xlabel("Sepal Length")
<Axes: xlabel='petal width (cm)', ylabel='Density'>
plt.ylabel("Sepal Width")
Text(0.5, 0, 'Sepal Length')
plt.title("Distribución de Setosa vs Virginica")
Text(0, 0.5, 'Sepal Width')
plt.show()
Text(0.5, 1.0, 'Distribución de Setosa vs Virginica')

Graficamos matriz de covarianza numerica



# Cargar el dataset iris y convertir a DataFrame
iris_bunch = load_iris()
iris = pd.DataFrame(iris_bunch.data, columns=iris_bunch.feature_names)

# Matriz de correlación
correlation_matrix = iris.corr()

# Graficar heatmap
plt.figure(figsize=(10, 10))
<Figure size 1000x1000 with 0 Axes>
ax = sns.heatmap(
    correlation_matrix,
    vmin=-1, vmax=1, center=0,
    cmap="GnBu", annot=True, fmt=".2f",
    square=True, cbar_kws={"shrink": 0.8},
    linewidths=0.5, linecolor='white'
)

# Corregir desalineación visual (el bug común en seaborn/matplotlib)
bottom, top = ax.get_ylim()
ax.set_ylim(top + 0.5, bottom - 0.5)
(0.5, 3.5)
plt.show()

Grafico de coordenadas paralelas



# Cargar el dataset iris
iris_bunch = load_iris()
iris = pd.DataFrame(iris_bunch.data, columns=iris_bunch.feature_names)
iris["species"] = pd.Categorical.from_codes(iris_bunch.target, iris_bunch.target_names)

# Escalar variables numéricas
scaler = MinMaxScaler()
iris_scaled = pd.DataFrame(scaler.fit_transform(iris.iloc[:, :-1]), columns=iris.columns[:-1])
iris_scaled["species"] = iris["species"]

# Graficar coordenadas paralelas
plt.figure(figsize=(10, 6))
<string>:1: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). Consider using `matplotlib.pyplot.close()`.
<Figure size 1000x600 with 0 Axes>
parallel_coordinates(iris_scaled, "species", colormap='cool')
plt.show()
<Axes: >

Grafico de Andrews

#Curvas de Andrews

andrews_curves(iris,"species",colormap='rainbow')
<Axes: >
plt.show()

Finalmente, ilustramos un grafico radial

#Grafico Radial
new_iris=iris.drop('species',axis=1)
print(new_iris.head()) # le quitamos la variable species
   sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)
0                5.1               3.5                1.4               0.2
1                4.9               3.0                1.4               0.2
2                4.7               3.2                1.3               0.2
3                4.6               3.1                1.5               0.2
4                5.0               3.6                1.4               0.2
categories = list(iris)[:4]
N = len(categories)
angles = [ n / float(N)*2*pi for n in range(N)]
angles = angles + angles[:1]
plt.figure(figsize = (10,10))
<Figure size 1000x1000 with 0 Axes>
ax = plt.subplot(111,polar = True)
ax.set_theta_offset(pi/2)
ax.set_theta_direction(-1)
plt.xticks(angles[:-1],categories)
([<matplotlib.projections.polar.ThetaTick object at 0x333c9aa90>, <matplotlib.projections.polar.ThetaTick object at 0x34a3d6850>, <matplotlib.projections.polar.ThetaTick object at 0x35509dc50>, <matplotlib.projections.polar.ThetaTick object at 0x35509f1d0>], [Text(0.0, 0, 'sepal length (cm)'), Text(1.5707963267948966, 0, 'sepal width (cm)'), Text(3.141592653589793, 0, 'petal length (cm)'), Text(4.71238898038469, 0, 'petal width (cm)')])
ax.set_rlabel_position(0)
plt.yticks([0,2,4,6],["0","2","4","6"],color= "red", size = 7)
([<matplotlib.projections.polar.RadialTick object at 0x35504ced0>, <matplotlib.projections.polar.RadialTick object at 0x35507cbd0>, <matplotlib.projections.polar.RadialTick object at 0x3550a7950>, <matplotlib.projections.polar.RadialTick object at 0x3550a9d50>], [Text(0, 0, '0'), Text(0, 2, '2'), Text(0, 4, '4'), Text(0, 6, '6')])
plt.ylim(0,6)
(0.0, 6.0)
values = new_iris.loc[0].values.flatten().tolist()
values = values + values[:1]
ax.plot(angles,values,linewidth = 1,linestyle="solid",label='setosa' )
[<matplotlib.lines.Line2D object at 0x35507db50>]
ax.fill(angles,values,"b",alpha=0.1)
[<matplotlib.patches.Polygon object at 0x35508a750>]
values = new_iris.loc[79].values.flatten().tolist()
values = values + values[:1]
ax.plot(angles,values,linewidth = 1,linestyle="solid",label='versicolor' )
[<matplotlib.lines.Line2D object at 0x34a3ddf10>]
ax.fill(angles,values,"orange",alpha=0.1)
[<matplotlib.patches.Polygon object at 0x35509fe10>]
values = new_iris.loc[149].values.flatten().tolist()
values = values + values[:1]
ax.plot(angles,values,linewidth = 1,linestyle="solid",label='virginica' )
ax.fill(angles,values,"green",alpha=0.1)
[<matplotlib.lines.Line2D object at 0x3550ae990>]
[<matplotlib.patches.Polygon object at 0x35507da90>]
plt.legend(loc = "upper left",bbox_to_anchor = (0.1,0.1))
<matplotlib.legend.Legend object at 0x355026550>
plt.show()

2) Variables multivariantes

Definicion

Las variables multivariantes se refieren a situaciones donde se observan múltiples variables aleatorias al mismo tiempo para cada individuo, unidad o caso. En lugar de analizar una sola variable (como la estatura), el análisis multivariante considera varias simultáneamente (como estatura, peso y edad). Este enfoque permite estudiar relaciones, patrones y dependencias entre las variables, lo cual es fundamental en contextos complejos donde los fenómenos no pueden explicarse por una sola dimensión.

Por ejemplo, en un estudio de salud, podrías observar variables como presión arterialnivel de colesterolfrecuencia cardíaca y edad de cada paciente. En economía, podrías analizar simultáneamente el PIB per cápita, la tasa de natalidad, la esperanza de vida y la tasa de mortalidad infantil de cada país. Estas variables, consideradas en conjunto, forman una variable multivariante y permiten realizar análisis como regresión múltiple, análisis de componentes principales (PCA), clustering o análisis discriminante.

¿Que es una función de distribución conjunta?

La función de distribución conjunta es una herramienta estadística que describe la probabilidad de que dos o más variables aleatorias tomen simultáneamente ciertos valores menores o iguales a los dados. Sirve para analizar el comportamiento conjunto de variables, permitiendo calcular probabilidades conjuntas, marginales y condicionales, así como entender la dependencia entre ellas. Por ejemplo, se puede usar para conocer la probabilidad de que una persona tenga un ingreso mensual menor a $2.000.000 y al mismo tiempo un gasto menor a $1.500.000, o para evaluar cómo se relacionan la temperatura y la humedad en un mismo día.

2.1) Distribución condicional e independencia

La distribución condicional describe el comportamiento de una variable aleatoria dado que otra ha tomado un valor específico. Permite responder preguntas como: “¿cómo se distribuye YY si sabemos que X=xX=x?”. Por ejemplo, si sabemos que una persona tiene un ingreso de $2.000.000, la distribución condicional del gasto muestra cómo varían los niveles de gasto dentro de ese grupo específico. Se usa para analizar relaciones entre variables y calcular probabilidades más específicas dentro de subconjuntos del espacio muestral.

Dos variables aleatorias son independientes si el valor de una no afecta la distribución de la otra. Formalmente, XX y YY son independientes si la distribución condicional de YY dado X=xX=x es igual a la distribución marginal de YY, para todo xx. En la práctica, esto significa que conocer el valor de XX no da información adicional sobre YY. Por ejemplo, si el género de una persona no afecta su consumo de cierto producto, entonces esas dos variables son independientes.

2.2) Teorema de Bayes

El Teorema de Bayes es una regla fundamental de la probabilidad que permite actualizar nuestras creencias sobre un evento a partir de nueva información. Establece cómo calcular la probabilidad de un evento AA dado que ocurrió otro evento BB, usando la relación:

\[ P(A \mid B) = \frac{P(B \mid A) \cdot P(A)}{P(B)} \]

Aquí, P(A)P(A) es la probabilidad inicial (a priori) de AA, P(B∣A)P(B∣A) es la probabilidad de observar BB si AA es cierto (verosimilitud), y P(A∣B)P(A∣B) es la probabilidad actualizada (a posteriori) de AA después de observar BB.

Este teorema es clave en situaciones donde tenemos información parcial o incierta y queremos mejorar nuestras predicciones. Por ejemplo, si un test médico da positivo (BB), el Teorema de Bayes permite calcular la probabilidad de que una persona realmente tenga la enfermedad (AA) considerando tanto la precisión del test como la prevalencia de la enfermedad. Se aplica ampliamente en estadística bayesiana, diagnóstico médico, inteligencia artificial y toma de decisiones bajo incertidumbre.

2.3)El valor esperado, la covarianaza y la correlacion

El valor esperado (o esperanza matemática) es el promedio ponderado de todos los posibles valores de una variable aleatoria, donde cada valor se multiplica por su probabilidad. Representa el resultado promedio que se esperaría si un experimento se repitiera infinitas veces. Por ejemplo, en economía, el valor esperado se usa para estimar ingresos futuros o evaluar decisiones bajo incertidumbre, como en modelos de riesgo o teoría de juegos.

La covarianza mide cómo varían dos variables aleatorias juntas. Si es positiva, significa que tienden a aumentar o disminuir simultáneamente; si es negativa, cuando una sube, la otra tiende a bajar. Aunque indica la dirección de la relación, su magnitud depende de las unidades de medida, por lo que no siempre es fácil de interpretar directamente. Se utiliza, por ejemplo, en portafolios financieros para medir cómo se mueven juntas dos acciones.

La correlación, en cambio, es una versión estandarizada de la covarianza que siempre toma valores entre -1 y 1, y permite comparar relaciones entre variables sin importar sus unidades. Una correlación cercana a 1 indica una relación lineal positiva fuerte; cercana a -1, una relación negativa fuerte; y cercana a 0, poca o ninguna relación lineal. Es fundamental en análisis estadístico, machine learning, ciencia de datos y econometría para identificar patrones, seleccionar variables o evaluar asociaciones entre fenómenos.

2.4) Esperanza condicional y la ley de varianza total

La esperanza condicional es el valor esperado de una variable aleatoria dado que conocemos el valor (o información) de otra. En lugar de calcular el promedio general de una variable, la esperanza condicional responde a la pregunta: “¿Cuál es el valor esperado de YY si sé que X=xX=x?”. Se denota como E(Y∣X=x)E(Y∣X=x) y puede interpretarse como una función de xx. Esta herramienta es muy útil cuando se dispone de información parcial y se quiere hacer una predicción más ajustada con base en esa información.

Por ejemplo, si YY es el ingreso mensual y XX es el nivel educativo, E(Y∣X=universitario)E(Y∣X=universitario)representa el ingreso promedio entre quienes tienen formación universitaria. En este sentido, la esperanza condicional permite construir modelos predictivos o de regresión, ya que se enfoca en cómo cambia el valor esperado de una variable en función de otra.

La ley de la varianza total (también llamada descomposición de la varianza) permite dividir la varianza de una variable aleatoria YY en dos componentes: una parte explicada por la variación en la media condicional, y otra por la variabilidad residual dentro de cada grupo condicional. Su fórmula es:

\[ \mathrm{Var}(Y) = \mathbb{E}[\mathrm{Var}(Y \mid X)] + \mathrm{Var}(\mathbb{E}[Y \mid X]) \]

Esta ley muestra que la variabilidad total de YY se puede entender como la suma de la variabilidad dentro de los subgrupos definidos por XX (primer término) y la variabilidad entre las medias de esos subgrupos (segundo término). Es clave en estadística y machine learning para entender qué tanta variación se explica por un predictor y cuánta queda sin explicar, lo que es esencial en análisis de regresión, inferencia y modelos jerárquicos.

2.5) Codigo ilustrativo en R:

El código ilustra los conceptos anteriormente visto:

quit
# Cargar y limpiar dataset
data("mtcars")
df <- mtcars %>%
  rownames_to_column("modelo") %>%
  clean_names() %>%
  mutate(cyl = as.factor(cyl))  # tratamos cyl como variable categórica

# 1. Variables multivariantes - gráfico de pares
df %>%
  dplyr::select(mpg, hp, wt, qsec) %>%
  ggpairs(title = "Gráfico de pares: Variables multivariantes")


# 2. Función de distribución conjunta: mpg vs hp
ggplot(df, aes(x = mpg, y = hp)) +
  geom_bin2d(bins = 15) +
  scale_fill_gradient(low = "lightblue", high = "darkblue") +
  labs(title = "Distribución conjunta: mpg vs hp", x = "Millas por galón", y = "Caballos de fuerza")


# 3. Distribución condicional: Boxplot de mpg según cyl
ggplot(df, aes(x = cyl, y = mpg, fill = cyl)) +
  geom_boxplot() +
  labs(title = "Distribución condicional de mpg dado cyl")


# 4. Independencia: scatterplot de wt vs qsec por cyl
ggplot(df, aes(x = wt, y = qsec, color = cyl)) +
  geom_point(size = 3) +
  labs(title = "¿Hay independencia entre peso (wt) y tiempo 1/4 milla (qsec)?")


# 5. Teorema de Bayes - simulación simple
set.seed(123)
N <- 10000
# Evento A: enfermedad (5%)
# Evento B: test positivo (90% sensibilidad, 10% falso positivo)
personas <- tibble(
  enfermedad = rbinom(N, 1, 0.05),
  test_positivo = if_else(enfermedad == 1,
                          rbinom(N, 1, 0.9),
                          rbinom(N, 1, 0.1))
)
# P(enfermedad | test positivo)
bayes_prob <- personas %>%
  filter(test_positivo == 1) %>%
  summarise(prob = mean(enfermedad == 1))
G1;H1;Errorh in summarise.data.frame(., prob = mean(enfermedad == 1)) : 
  argument "y" is missing, with no default
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
print(bayes_prob)
G1;H1;Errorh in h(simpleError(msg, call)) : 
  error in evaluating the argument 'x' in selecting a method for function 'print': object 'bayes_prob' not found
g
# 6. Valor esperado, covarianza y correlación
df %>%
  summarise(
    valor_esperado_mpg = mean(mpg),
    var_hp = var(hp),
    cov_mpg_hp = cov(mpg, hp),
    cor_mpg_hp = cor(mpg, hp)
  )
G1;H1;Errorh in summarise.data.frame(., valor_esperado_mpg = mean(mpg), var_hp = var(hp),  : 
  argument "y" is missing, with no default
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
# 7. Esperanza condicional y ley de varianza total
esperanza_condicional <- df %>%
  group_by(cyl) %>%
  summarise(media_mpg = mean(mpg), varianza_mpg = var(mpg), .groups = "drop")
G1;H1;Errorh in summarise.data.frame(., media_mpg = mean(mpg), varianza_mpg = var(mpg),  : 
  argument "y" is missing, with no default
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
var_total <- var(df$mpg)
esperanza_cond <- mean(esperanza_condicional$varianza_mpg)
G1;H1;Errorh in h(simpleError(msg, call)) : 
  error in evaluating the argument 'x' in selecting a method for function 'mean': object 'esperanza_condicional' not found
g
var_esperanzas <- var(esperanza_condicional$media_mpg)
G1;H1;Errorh: object 'esperanza_condicional' not found
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
cat("Varianza total: ", var_total, "\n")
Varianza total:  36.3241 
cat("E[Var(Y|X)]: ", esperanza_cond, "\n")
G1;H1;Errorh: object 'esperanza_cond' not found
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
cat("Var(E[Y|X]): ", var_esperanzas, "\n")
G1;H1;Errorh: object 'var_esperanzas' not found
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
cat("Suma: ", esperanza_cond + var_esperanzas, "\n")
G1;H1;Errorh: object 'esperanza_cond' not found
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g

2.6) Codigo ilustrativo en Python:

reticulate::repl_python()


# 1. Cargar y limpiar dataset
from pydataset import data  # mtcars está disponible en pydataset
df = data('mtcars').copy()
df['modelo'] = df.index
df = df.reset_index(drop=True)
df['cyl'] = df['cyl'].astype(str)  # tratamos cyl como categórica (como factor)

# 2. Variables multivariantes - gráfico de pares
sns.pairplot(df[['mpg', 'hp', 'wt', 'qsec']])
<seaborn.axisgrid.PairGrid object at 0x35507e510>
plt.suptitle("Gráfico de pares: Variables multivariantes", y=1.02)
Text(0.5, 1.02, 'Gráfico de pares: Variables multivariantes')
plt.show()

# 3. Función de distribución conjunta: mpg vs hp
plt.figure(figsize=(8, 6))
hb = plt.hist2d(df['mpg'], df['hp'], bins=15, cmap='Blues')
<Figure size 800x600 with 0 Axes>
plt.colorbar(hb[3])
<matplotlib.colorbar.Colorbar object at 0x359d1cd10>
plt.xlabel('Millas por galón (mpg)')
Text(0.5, 0, 'Millas por galón (mpg)')
plt.ylabel('Caballos de fuerza (hp)')
plt.title('Distribución conjunta: mpg vs hp')
Text(0, 0.5, 'Caballos de fuerza (hp)')
plt.show()
Text(0.5, 1.0, 'Distribución conjunta: mpg vs hp')

# 4. Distribución condicional: Boxplot de mpg según cyl
plt.figure(figsize=(8, 6))
<Figure size 800x600 with 0 Axes>
sns.boxplot(x='cyl', y='mpg', data=df, palette="Set2")
<string>:1: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.
<Axes: xlabel='cyl', ylabel='mpg'>
plt.title("Distribución condicional de mpg dado cyl")
Text(0.5, 1.0, 'Distribución condicional de mpg dado cyl')
plt.xlabel("Cilindros")
Text(0.5, 0, 'Cilindros')
plt.ylabel("Millas por galón (mpg)")
Text(0, 0.5, 'Millas por galón (mpg)')
plt.show()


# 5. Independencia: scatterplot de wt vs qsec por cyl
plt.figure(figsize=(8, 6))
<Figure size 800x600 with 0 Axes>
sns.scatterplot(x='wt', y='qsec', hue='cyl', data=df, s=100)
<Axes: xlabel='wt', ylabel='qsec'>
plt.title("¿Hay independencia entre peso (wt) y tiempo 1/4 milla (qsec)?")
Text(0.5, 1.0, '¿Hay independencia entre peso (wt) y tiempo 1/4 milla (qsec)?')
plt.xlabel("Peso (wt)")
Text(0.5, 0, 'Peso (wt)')
plt.ylabel("Tiempo 1/4 milla (qsec)")
Text(0, 0.5, 'Tiempo 1/4 milla (qsec)')
plt.show()


# 6. Teorema de Bayes - simulación simple
np.random.seed(123)
N = 10000
enfermedad = np.random.binomial(1, 0.05, N)
test_positivo = np.where(enfermedad == 1,
                         np.random.binomial(1, 0.9, N),
                         np.random.binomial(1, 0.1, N))
personas = pd.DataFrame({'enfermedad': enfermedad, 'test_positivo': test_positivo})

# P(enfermedad | test positivo)
bayes_prob = personas[personas['test_positivo'] == 1]['enfermedad'].mean()
print(f"P(enfermedad | test positivo) ≈ {bayes_prob:.4f}")
P(enfermedad | test positivo) ≈ 0.3293
# 7. Valor esperado, covarianza y correlación
valor_esperado_mpg = df['mpg'].mean()
var_hp = df['hp'].var()
cov_mpg_hp = df[['mpg', 'hp']].cov().iloc[0, 1]
cor_mpg_hp = df[['mpg', 'hp']].corr().iloc[0, 1]

print(f"Valor esperado de mpg: {valor_esperado_mpg:.2f}")
Valor esperado de mpg: 20.09
print(f"Varianza de hp: {var_hp:.2f}")
print(f"Covarianza mpg-hp: {cov_mpg_hp:.2f}")
Varianza de hp: 4700.87
Covarianza mpg-hp: -320.73
print(f"Correlación mpg-hp: {cor_mpg_hp:.2f}")
Correlación mpg-hp: -0.78
# 8. Esperanza condicional y ley de varianza total
cond_stats = df.groupby('cyl').agg(media_mpg=('mpg', 'mean'), varianza_mpg=('mpg', 'var')).reset_index()

var_total = df['mpg'].var()
esperanza_cond = cond_stats['varianza_mpg'].mean()
var_esperanzas = cond_stats['media_mpg'].var()

print(f"\nVarianza total: {var_total:.4f}")

Varianza total: 36.3241
print(f"E[Var(Y|X)]: {esperanza_cond:.4f}")
E[Var(Y|X)]: 9.6684
print(f"Var(E[Y|X]): {var_esperanzas:.4f}")
print(f"Suma (E[Var(Y|X)] + Var(E[Y|X])): {esperanza_cond + var_esperanzas:.4f}")
Var(E[Y|X]): 33.8618
Suma (E[Var(Y|X)] + Var(E[Y|X])): 43.5302

3) Distribución multivariante

3.1) Distribucion normal multivariante

Distribucion Normal Univariante

La densidad normal univariante (también conocida como distribución normal o gaussiana) es una función de probabilidad que describe cómo se distribuyen los valores de una variable aleatoria continua en torno a su media. Tiene una forma de campana simétrica, donde los valores cercanos a la media son los más probables y la probabilidad disminuye a medida que nos alejamos de ella. Está completamente determinada por dos parámetros: la media (μ), que indica el centro de la distribución, y la desviación estándar (σ), que refleja la dispersión o el ancho de la campana.

Matemáticamente, su función de densidad se expresa como:

\[ f(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \, e^{-\frac{(x - \mu)^2}{2\sigma^2}} \]

Esta distribución es fundamental en estadística porque modela muchos fenómenos naturales y es la base del teorema central del límite, que establece que la suma de muchas variables aleatorias tiende a seguir una distribución normal, incluso si esas variables no son normales individualmente.

En un caso bivariante, se analizan dos variables al mismo tiempo para entender la relación entre ellas. Por ejemplo, si estudiamos el ingreso mensual y el nivel educativo de una muestra de personas, buscamos ver cómo cambia una variable respecto a la otra. Este análisis permite identificar correlaciones (positivas, negativas o nulas), visualizar la relación con gráficos como el diagrama de dispersión y ajustar modelos como la regresión lineal para predecir una variable en función de la otra, ayudando así a comprender patrones conjuntos y dependencias entre ambas dimensiones.

La distribución normal estándar es una versión específica de la distribución normal en la que la media es 0 y la desviación estándar es 1, y se utiliza como referencia para comparar distintos conjuntos de datos. Esta transformación se logra mediante la estandarización, un proceso que convierte cualquier valor de una distribución normal general a un valor de la normal estándar mediante la fórmula \(Z = \frac{X - \mu}{\sigma}\), donde \(X\) es el valor original, \(\mu\) la media y \(\sigma\) la desviación estándar. Esta distribución es simétrica en torno a la media, tiene forma de campana, y permite calcular probabilidades y realizar comparaciones al expresar cuán lejos está un dato respecto a la media en unidades de desviación estándar.

La estandarización inversa es el proceso mediante el cual se transforma un valor estandarizado (puntaje z) de la distribución normal estándar de vuelta a su escala original. Esto se logra usando la fórmula inversa: \(X = Z \cdot \sigma + \mu\), donde \(Z\) es el valor estandarizado, \(\sigma\) es la desviación estándar y \(\mu\) la media del conjunto original. Sirve para interpretar los resultados en unidades reales después de haber trabajado con datos estandarizados, por ejemplo, al predecir o simular valores en su contexto original tras aplicar un modelo basado en la normal estándar.

La transformación lineal es una operación matemática que ajusta los valores de una variable mediante una fórmula de la forma \(X' = aX + b\), donde \(a\) y \(b\) son constantes que escalan y trasladan los datos, respectivamente. La estandarización y su inversa son casos específicos de transformación lineal: al estandarizar, usamos \(Z = \frac{X - \mu}{\sigma}\), lo cual equivale a restar la media (traslación) y dividir por la desviación estándar (escalamiento); al revertirla, usamos \(X = Z \cdot \sigma + \mu\), otra transformación lineal. Por tanto, la transformación lineal es el marco general que permite pasar de una escala original a una escala deseada (como la normal estándar), facilitando comparaciones, normalización y análisis estadístico.

quit


# Parámetros de la distribución normal
mu <- 50     # media
sigma <- 10  # desviación estándar

# Secuencia de valores x
x <- seq(mu - 4*sigma, mu + 4*sigma, length.out = 1000)

# Densidad normal con media y desviación dada
densidad_normal <- dnorm(x, mean = mu, sd = sigma)

# Densidad de la normal estándar (mu = 0, sigma = 1)
x_std <- seq(-4, 4, length.out = 1000)
densidad_estandar <- dnorm(x_std)

# Crear data frame para graficar ambas
df <- data.frame(
  x = x,
  densidad = densidad_normal,
  x_std = x_std,
  densidad_std = densidad_estandar
)

# Graficar ambas curvas de densidad
ggplot() +
  geom_line(data = df, aes(x = x, y = densidad), color = "blue", size = 1.2) +
  geom_line(data = df, aes(x = x_std * sigma + mu, y = densidad_std), 
            color = "red", linetype = "dashed", size = 1.2) +
  labs(title = "Distribución Normal Univariante y Normal Estándar",
       subtitle = "Azul: N(50,10) | Rojo: N(0,1) reescalada",
       x = "x", y = "Densidad") +
  theme_minimal()


# --- Estandarización de un valor X ---
X <- 70
Z <- (X - mu) / sigma  # Estandarización
cat("Valor original:", X, "\nPuntaje Z:", Z, "\n")
Valor original: 70 
Puntaje Z: 2 
# --- Estandarización inversa ---
X_recuperado <- Z * sigma + mu
cat("Valor recuperado desde Z:", X_recuperado, "\n")
Valor recuperado desde Z: 70 
# --- Transformación lineal general ---
# Por ejemplo: X' = 2X + 5
a <- 2
b <- 5
X_transformado <- a * X + b
cat("Transformación lineal X' = 2X + 5:\nResultado:", X_transformado, "\n")
Transformación lineal X' = 2X + 5:
Resultado: 145 

3.2) Curvas de nivel o contorno

Una curva de nivel o línea de contorno es una representación gráfica que conecta puntos que tienen el mismo valor de una función en un plano bidimensional. En estadística y análisis multivariado, estas curvas se utilizan comúnmente para visualizar funciones de densidad conjunta, especialmente en distribuciones bivariadas como la normal bivariante. En este contexto, cada curva indica una región del espacio donde la combinación de valores de las dos variables tiene la misma densidad de probabilidad, similar a cómo un mapa topográfico muestra alturas constantes.

Estas curvas son útiles porque permiten identificar visualmente la forma, orientación y concentración de la distribución conjunta. Por ejemplo, en una normal bivariante, las curvas de nivel suelen tomar la forma de elipses, cuya orientación y alargamiento reflejan la dirección y fuerza de la correlación entre las variables. Así, las curvas de nivel permiten entender cómo se relacionan dos variables, detectar patrones de dependencia, y comparar distribuciones multivariantes de manera intuitiva, sin necesidad de interpretar directamente fórmulas o tablas numéricas.

El caso bivariado se refiere al análisis conjunto de dos variables aleatorias, permitiendo estudiar cómo se relacionan entre sí en términos de dependencia, asociación o correlación. A diferencia del caso univariado, que describe una sola variable, el análisis bivariado examina simultáneamente la distribución conjunta, como en el caso de la distribución normal bivariante, donde cada par de valores tiene una probabilidad asociada. Esto permite visualizar relaciones mediante diagramas de dispersión y curvas de nivel (como elipses en una normal bivariante), identificar patrones lineales o no lineales, y ajustar modelos como la regresión para predecir una variable en función de la otra, siendo esencial para entender interacciones entre dimensiones en conjuntos de datos multivariados.



# Parámetros de la normal bivariada
mu <- c(0, 0)  # medias de X y Y
sigma <- matrix(c(1, 0.8, 
                  0.8, 1), nrow = 2)  # matriz de covarianza con correlación positiva

# Simular datos bivariados
set.seed(123)
datos <- as.data.frame(mvrnorm(n = 1000, mu = mu, Sigma = sigma))
colnames(datos) <- c("X", "Y")

# Graficar curvas de nivel (densidad conjunta) con scatterplot
ggplot(datos, aes(x = X, y = Y)) +
  stat_density_2d(aes(fill = ..level..), geom = "polygon", color = "black", alpha = 0.6) +
  geom_point(alpha = 0.3, color = "blue", size = 0.8) +
  scale_fill_viridis_c(option = "D") +
  coord_fixed() +
  theme_minimal() +
  labs(
    title = "Curvas de Nivel de la Distribución Normal Bivariada",
    subtitle = "Elipses muestran regiones con igual densidad conjunta",
    x = "Variable X",
    y = "Variable Y",
    fill = "Densidad"
  )

La densidad de kernel es un método no paramétrico para estimar la función de densidad de probabilidad de una variable aleatoria continua a partir de una muestra de datos. A diferencia de los histogramas, que agrupan los datos en intervalos, la densidad de kernel genera una curva suave que aproxima cómo se distribuyen los datos, sin asumir una forma específica como la normal. Este método coloca una función simétrica (el “kernel”, comúnmente una normal) centrada en cada punto de los datos y las suma para formar la estimación global. La suavidad de la curva depende del ancho de banda o parámetro de suavización: un valor pequeño produce una curva muy irregular (sobreajuste), mientras que uno muy grande genera una curva demasiado lisa (subajuste). Es especialmente útil para explorar la distribución subyacente de los datos de manera flexible y visual.



# Simular datos bivariados (con cierta correlación)
set.seed(42)
n <- 500
x <- rnorm(n, mean = 0, sd = 1)
y <- 0.5 * x + rnorm(n, mean = 0, sd = 1)
datos <- data.frame(x, y)

# Estimar densidad kernel bidimensional
densidad <- kde2d(datos$x, datos$y, n = 50)

# Preparar la gráfica 3D con contornos
fig <- plot_ly(
  x = densidad$x, y = densidad$y, z = densidad$z,
  type = "surface",
  contours = list(
    z = list(show = TRUE, usecolormap = TRUE, highlightcolor = "#ff0000", highlightwidth = 2)
  )
) %>%
  layout(
    title = "Densidad Kernel 2D con superficie 3D y contornos",
    scene = list(
      xaxis = list(title = "X"),
      yaxis = list(title = "Y"),
      zaxis = list(title = "Densidad")
    )
  )

# Mostrar gráfico interactivo
fig
NA

3.3) Distancia Euclidiana

La distancia euclidiana es una medida geométrica que calcula la longitud del segmento recto entre dos puntos en un espacio euclidiano. En dos dimensiones, si se tienen dos puntos \(A = (x_1, y_1)\) y \(B = (x_2, y_2)\), su distancia euclidiana se define como:

\[ d(A, B) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} \]

Este concepto se extiende naturalmente a espacios de más dimensiones. En general, para dos puntos \(A = (x_1, x_2, \dots, x_n)\) y \(B = (y_1, y_2, \dots, y_n)\) en un espacio \(n\)-dimensional, la fórmula es:

\[ d(A, B) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2} \]

La distancia euclidiana es ampliamente utilizada en estadística multivariada, aprendizaje automático y análisis de datos para evaluar la similitud o disimilitud entre observaciones. Sirve, por ejemplo, en algoritmos de clustering como k-means, clasificación como k-NN, y análisis de conglomerados jerárquicos, donde ayuda a agrupar datos que están “cerca” unos de otros en el espacio de características. Su simplicidad y significado geométrico la convierten en una de las medidas de distancia más intuitivas y universales.



# Definir los puntos A y B
A <- c(2, 3)
B <- c(7, 6)

# Calcular distancia euclidiana
distancia <- sqrt((B[1] - A[1])^2 + (B[2] - A[2])^2)

# Crear data frame con los puntos
puntos <- data.frame(
  x = c(A[1], B[1]),
  y = c(A[2], B[2]),
  label = c("A", "B")
)

# Gráfico
ggplot(puntos, aes(x = x, y = y)) +
  geom_point(size = 4, color = "blue") +
  geom_text(aes(label = label), hjust = -0.3, vjust = -0.5, size = 5) +
  geom_segment(aes(x = A[1], y = A[2], xend = B[1], yend = B[2]), 
               arrow = arrow(length = unit(0.2, "cm")), 
               color = "red", size = 1.2) +
  annotate("text", x = (A[1] + B[1])/2, y = (A[2] + B[2])/2 + 0.5,
           label = paste0("Distancia Euclidiana = ", round(distancia, 2)),
           color = "black", size = 5) +
  xlim(0, 10) + ylim(0, 10) +
  coord_fixed() +
  theme_minimal() +
  labs(title = "Visualización de la Distancia Euclidiana entre dos puntos",
       x = "Eje X", y = "Eje Y")

3.4) Distancia de Mahalanobis

Esta medida es importante ya que la distancia euclidiana tiene la desventaja de en el espacio multivariante las variables pueden estar correlacionadas y la euclidiana no lo tienen en cuenta. Por lo tanto, cuando la distribución no es esférica es mejor usar una distribución que si tenga en cuenta las correlaciones y covarinazas

La distancia de Mahalanobis es una medida estadística utilizada para calcular cuán lejos está un punto respecto a una distribución de datos multivariada, considerando las correlaciones entre las variables. A diferencia de la distancia euclidiana, que simplemente mide la distancia recta entre dos puntos en el espacio, la distancia de Mahalanobis ajusta esta medida teniendo en cuenta la estructura de varianza y covarianza de los datos. Esto la hace especialmente útil cuando las variables tienen distintas escalas o están correlacionadas.

Esta distancia fue introducida en 1936 por el estadístico indio Prasanta Chandra Mahalanobis, quien la desarrolló inicialmente para estudios antropométricos, comparando características físicas entre diferentes grupos poblacionales. Desde entonces, se ha convertido en una herramienta fundamental en diversas áreas como el análisis discriminante, la detección de valores atípicos (outliers), el análisis de conglomerados (clustering), y el reconocimiento de patrones en inteligencia artificial y machine learning.

La fórmula de la distancia de Mahalanobis para un vector de observaciones x, respecto a una distribución con media μ y matriz de covarianza S, se expresa de la siguiente manera:

\(D_M(\mathbf{x}) = \sqrt{(\mathbf{x} - \boldsymbol{\mu})^\top \mathbf{S}^{-1} (\mathbf{x} - \boldsymbol{\mu})}\)

La distancia de Mahalanobis es una herramienta eficaz para detectar valores atípicos (también llamados outliers) en conjuntos de datos con múltiples variables. A diferencia de métodos univariantes como el z-score, esta distancia considera simultáneamente todas las variables y las correlaciones entre ellas, por lo que permite una detección más precisa en contextos multivariados.

Para utilizar esta técnica, primero se debe calcular la media multivariada del conjunto de datos y la matriz de covarianza entre las variables. Luego, para cada observación, se calcula su distancia de Mahalanobis al centro de los datos, usando la fórmula: \(D_M(\mathbf{x}_i) = \sqrt{(\mathbf{x}_i - \boldsymbol{\mu})^\top \mathbf{S}^{-1} (\mathbf{x}_i - \boldsymbol{\mu})}\). Si una observación está muy alejada —en términos de esta distancia— se considera un potencial atípico.

Un paso clave es definir un umbral de corte para determinar qué tan “lejos” es demasiado lejos. La distancia de Mahalanobis al cuadrado (\(D_M^2\)) sigue, bajo ciertas condiciones, una distribución chi-cuadrado (\(\chi^2\)) con grados de libertad iguales al número de variables (\(p\)). Por tanto, se puede usar un valor crítico de esa distribución como referencia. Por ejemplo, si se tienen \(3\) variables y se desea un nivel de confianza del \(95%\), cualquier observación con una distancia al cuadrado mayor que \(\chi^2_{3, 0.95} \approx 7.81\) será considerada un atípico.

Este enfoque es especialmente útil cuando los datos son aproximadamente normales multivariados. Si esa condición no se cumple, la distancia de Mahalanobis puede dar falsos positivos o negativos. Por eso, es buena práctica complementarla con visualizaciones como gráficos de dispersión, análisis de componentes principales (PCA) o métodos robustos.

En resumen, la distancia de Mahalanobis permite detectar observaciones inusuales considerando la forma, escala y correlación del conjunto de datos. Es un método clásico, estadísticamente sólido y ampliamente utilizado en áreas como la bioestadística, la detección de fraudes, la calidad industrial y la minería de datos.



# Usamos un subconjunto multivariado del dataset mtcars
data <- mtcars[, c("mpg", "disp", "hp")]

# Calcular media y matriz de covarianza
mu <- colMeans(data)
S <- cov(data)

# Calcular distancia de Mahalanobis para cada observación
mahal_dist <- mahalanobis(data, center = mu, cov = S)

# Calcular distancia euclidiana desde la media
eucl_dist <- apply(data, 1, function(x) sqrt(sum((x - mu)^2)))

# Agregar resultados al dataframe
data_plot <- data %>%
  mutate(
    Car = rownames(.),
    Mahalanobis = mahal_dist,
    Euclidean = eucl_dist
  )

# Determinar umbral de corte para outliers (Chi-cuadrado con 3 variables y 95% de confianza)
p <- ncol(data)
alpha <- 0.05
threshold <- qchisq(1 - alpha, df = p)

data_plot <- data_plot %>%
  mutate(Outlier = Mahalanobis > threshold)

# Imprimir los outliers
cat("Observaciones consideradas outliers (Mahalanobis > ", round(threshold, 2), "):\n")
Observaciones consideradas outliers (Mahalanobis >  7.81 ):
print(data_plot %>% filter(Outlier) %>% select(Car, Mahalanobis))

# Gráfico: comparación entre distancias Euclidianas y Mahalanobis
ggplot(data_plot, aes(x = Euclidean, y = Mahalanobis, label = Car, color = Outlier)) +
  geom_point(size = 3) +
  geom_text(hjust = 1.2, vjust = 0.5, size = 3.5, show.legend = FALSE) +
  geom_hline(yintercept = threshold, linetype = "dashed", color = "red") +
  labs(
    title = "Comparación entre distancia Euclidiana y Mahalanobis",
    subtitle = paste("Línea roja = Umbral Chi-cuadrado (", round(threshold, 2), ")"),
    x = "Distancia Euclidiana a la media",
    y = "Distancia Mahalanobis"
  ) +
  scale_color_manual(values = c("black", "red")) +
  theme_minimal()

Este gráfico compara la distancia Euclidiana (eje X) y la distancia de Mahalanobis (eje Y) desde la media multivariada para cada automóvil en el conjunto de datos mtcars, utilizando las variables mpg, disp y hp. La línea roja discontinua representa el umbral de corte correspondiente al percentil 95% de una distribución chi-cuadrado con 3 grados de libertad (\(\chi^2_{3, 0.95} \approx 7.81\)), usado para identificar outliers multivariados. Cada punto representa un vehículo, y su color indica si fue clasificado como atípico: negro para observaciones normales y rojo para outliers. En este caso, el Maserati Bora tiene una alta distancia de Mahalanobis (mayor a 15), lo que lo convierte en un claro atípico, aunque otros autos como el Cadillac Fleetwood o el Lincoln Continental están más alejados en distancia euclidiana, pero dentro del rango aceptable al considerar las correlaciones entre variables. Esto ilustra cómo la distancia de Mahalanobis es más adecuada para detectar valores extremos en datos correlacionados.



# Simular datos correlacionados en 2D
set.seed(123)
mu <- c(0, 0)
Sigma <- matrix(c(4, 3, 3, 4), nrow = 2)  # matriz con correlación
data <- as.data.frame(mvrnorm(200, mu = mu, Sigma = Sigma))
colnames(data) <- c("X1", "X2")

# Centro (media)
center <- colMeans(data)

# Punto de referencia alejado del centro
punto <- c(4, 4)

# Función de distancia Euclidiana
euclidean_distance <- function(x, center) {
  sqrt(sum((x - center)^2))
}

# Función de distancia Mahalanobis
mahal_distance <- function(x, center, cov_matrix) {
  sqrt(t(x - center) %*% solve(cov_matrix) %*% (x - center))
}

# Calcular ambas distancias
d_euc <- euclidean_distance(punto, center)
d_mah <- mahal_distance(punto, center, cov(data))

# Crear el gráfico
ggplot(data, aes(x = X1, y = X2)) +
  geom_point(alpha = 0.5) +
  geom_point(aes(x = punto[1], y = punto[2]), color = "red", size = 4) +
  geom_point(aes(x = center[1], y = center[2]), color = "blue", size = 4) +
  geom_segment(aes(x = center[1], y = center[2], xend = punto[1], yend = punto[2]),
               arrow = arrow(length = unit(0.3, "cm")), color = "black", linetype = "dotted") +
  stat_ellipse(level = 0.5, color = "darkgreen", linetype = "dashed", size = 1) +  # Mahalanobis
  annotate("text", x = punto[1], y = punto[2] + 1,
           label = paste0("Dist. Euclidiana: ", round(d_euc, 2), "\nDist. Mahalanobis: ", round(d_mah, 2)),
           color = "red", size = 4) +
  labs(
    title = "Comparación entre distancia Euclidiana y Mahalanobis",
    subtitle = "Elipse = misma distancia Mahalanobis; línea recta = distancia Euclidiana",
    x = "Variable X1",
    y = "Variable X2"
  ) +
  theme_minimal()

NA
NA

Este gráfico ilustra visualmente la diferencia conceptual entre la distancia Euclidiana y la distancia de Mahalanobis en un conjunto de datos bidimensional (variables X1 y X2). Los puntos grises representan observaciones generadas con una distribución elíptica y correlación positiva entre variables. El punto azul marca el centro de los datos (la media multivariada), mientras que el punto rojo es una observación específica cuya distancia al centro se está midiendo.

La línea punteada negra muestra la distancia Euclidiana, que es la línea recta entre el centro y el punto rojo, e ignora completamente la estructura de varianza y correlación de los datos. En cambio, la elipse verde representa una línea de igual distancia Mahalanobis, que toma en cuenta la dispersión y forma del conjunto. En este caso, aunque el punto rojo está a una distancia euclidiana considerable del centro (\(5.68\)), su distancia de Mahalanobis es mucho menor (\(2.28\)), porque se encuentra alineado con la dirección principal de dispersión de los datos. Esto demuestra cómo la distancia de Mahalanobis “deforma” el espacio según la estructura interna de los datos, y por eso es mucho más adecuada para identificar qué tan inusual es realmente una observación en contextos multivariados.

3.5) Distribución t-student multivariante

La distribución t de Student multivariante es una extensión de la distribución t univariada al caso en el que se trabaja con vectores aleatorios en lugar de variables individuales. Esta distribución permite modelar datos multivariados con colas más pesadas que las de la distribución normal multivariada, lo que la hace más adecuada para situaciones donde pueden aparecer valores extremos (outliers) o donde la normalidad estricta no es una buena suposición. Es ampliamente utilizada en estadística robusta, modelos bayesianos, econometría y análisis de riesgos financieros.

Formalmente, decimos que un vector aleatorio \(\mathbf{X} \in \mathbb{R}^p\) tiene una distribución t de Student multivariante con \(\nu\) grados de libertad, media \(\boldsymbol{\mu}\) y matriz de escala \(\boldsymbol{\Sigma}\) si:

$ t_(, ) $

La función de densidad de probabilidad de esta distribución está dada por:

$ f() = ^{-} $

En esta fórmula, \(\Gamma(\cdot)\) es la función gamma, \(p\) es la dimensión del vector \(\mathbf{x}\), y \(|\boldsymbol{\Sigma}|\) representa el determinante de la matriz de escala. Como se puede ver, la forma de la densidad es muy similar a la de la normal multivariante, pero incluye un factor adicional que hace que la densidad decrezca más lentamente conforme \(\mathbf{x}\) se aleja de la media \(\boldsymbol{\mu}\). Por eso, esta distribución tiene colas más pesadas, lo cual permite una mayor probabilidad de valores extremos.

Una forma útil de entender esta distribución es su relación con la normal multivariante. Si \(\mathbf{Z} \sim \mathcal{N}_p(\mathbf{0}, \boldsymbol{\Sigma})\) y \(W \sim \chi^2(\nu)\) de forma independiente, entonces:

$ = + $

sigue una distribución \(t_\nu(\boldsymbol{\mu}, \boldsymbol{\Sigma})\). Esto muestra que puede verse como una normal multivariante escalada aleatoriamente por una variable chi-cuadrado, lo que introduce mayor variabilidad.

Además, si los grados de libertad \(\nu\) tienden a infinito, es decir, \(\nu \to \infty\), entonces la distribución t multivariante converge a la normal multivariante, lo cual refuerza la idea de que es una versión más flexible y robusta de la misma.

En resumen, la distribución t de Student multivariante es muy útil cuando se desea modelar datos multivariados en los que hay incertidumbre sobre la varianza, colas pesadas o riesgo de observaciones extremas. Al igual que la normal multivariante, incorpora una matriz de escala que describe la forma y la correlación de las variables, pero su comportamiento es más conservador y realista en presencia de datos anómalos.


# Creamos semillas
set.seed(123)

# Parámetros
mu <- c(0, 0)
Sigma <- matrix(c(1, 0.7, 0.7, 1), 2, 2)
n <- 1000
df <- 3  # Grados de libertad para la t multivariante

# Simulación
norm_data <- mvrnorm(n, mu, Sigma)
t_data <- rmvt(n, sigma = Sigma, df = df, delta = mu)

# Convertir a data frame
norm_df <- as.data.frame(norm_data)
t_df <- as.data.frame(t_data)
norm_df$tipo <- "Normal"
t_df$tipo <- "t-Student"

names(norm_df)[1:2] <- names(t_df)[1:2] <- c("X1", "X2")
data <- rbind(norm_df, t_df)

# Graficar
ggplot(data, aes(x = X1, y = X2, color = tipo)) +
  geom_point(alpha = 0.5) +
  stat_ellipse(level = 0.95, linetype = "dashed") +
  theme_minimal(base_size = 14) +
  labs(
    title = "Comparación entre distribución normal y t-Student multivariantes",
    subtitle = paste0("Ambas con misma media y covarianza; t-Student con df = ", df),
    color = "Distribución",
    x = "X1", y = "X2"
  )

Las distribuciones esféricas y elípticas son clases generales de distribuciones multivariadas que extienden la idea de simetría en el espacio. Se caracterizan por tener niveles de densidad constantes en formas geométricas específicas: esferas y elipses, respectivamente.

Una distribución esférica es aquella cuya función de densidad depende únicamente de la distancia euclidiana al centro de la distribución. Es decir, la probabilidad de observar un punto depende solamente de qué tan lejos está del centro, no de la dirección. Formalmente, si \(\mathbf{x} \in \mathbb{R}^p\) es un vector aleatorio, decimos que tiene una distribución esférica si su densidad (cuando existe) puede escribirse como:

\[ f(\mathbf{x}) = g\left( |\mathbf{x} - \boldsymbol{\mu} |^2 \right) \]

donde \(\boldsymbol{\mu}\) es el vector de media (centro de simetría), \(| \cdot |\) denota la norma euclidiana, y \(g(\cdot)\) es una función escalar que determina la forma de la distribución. Un ejemplo clásico de distribución esférica es la normal multivariada estándar, con media cero y matriz de covarianza igual a la identidad.

Por otro lado, las distribuciones elípticas generalizan las esféricas permitiendo diferentes escalas y correlaciones entre las variables. En lugar de tener isodensidades en forma de esferas, las tienen en forma de elipses (o elipsoides en dimensiones mayores). Esto las hace mucho más flexibles para modelar datos reales. La forma general de la densidad de una distribución elíptica es:

\[ f(\mathbf{x}) = |\boldsymbol{\Sigma}|^{-1/2} \cdot h\left((\mathbf{x} - \boldsymbol{\mu})^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu})\right) \]

donde \(\boldsymbol{\mu}\) es el vector de media, \(\boldsymbol{\Sigma}\) es la matriz de dispersión (proporcional a la covarianza), y \(h(\cdot)\) es una función generadora escalar. Este tipo de estructura incluye como casos particulares a muchas distribuciones multivariadas conocidas, como:

La normal multivariada (cuando \(h(z) = \exp(-z/2)\)), La t de Student multivariante (cuando \(h(z)\) decae más lentamente), Distribuciones robustas como la de tipo Laplace multivariada o Cauchy. Una característica clave de las distribuciones elípticas es que permiten conservar la idea de “dirección” y “forma” de la dispersión de los datos, pero ajustando mejor las colas pesadas o las formas no normales. Esto las hace muy útiles en análisis robusto, detección de atípicos, y modelos financieros multivariados.

En resumen, las distribuciones esféricas son un caso particular dentro de las elípticas, y ambas ofrecen herramientas poderosas para modelar datos multidimensionales con estructuras de simetría y correlación.


# Crear datos para el círculo (esférica) y la elipse (elíptica)
theta <- seq(0, 2*pi, length.out = 200)

# Círculo: radio 1 (distribución esférica)
circle <- data.frame(
  x = cos(theta),
  y = sin(theta),
  tipo = "Distribución esférica"
)

# Elipse: semiejes a = 2, b = 1 (distribución elíptica)
ellipse <- data.frame(
  x = 2 * cos(theta),
  y = 1 * sin(theta),
  tipo = "Distribución elíptica"
)

# Unir ambos data frames
df <- rbind(circle, ellipse)

# Graficar
ggplot(df, aes(x = x, y = y, color = tipo)) +
  geom_path(size = 1) +
  coord_equal() +
  theme_minimal() +
  labs(
    title = "Distribuciones Esférica vs Elíptica",
    x = "Eje X", y = "Eje Y",
    color = "Tipo de distribución"
  )

3.6) Mixtura de distribuciones

Las mixturas de distribuciones (o mixture models) son modelos estadísticos utilizados para representar poblaciones heterogéneas, en las cuales se supone que los datos provienen de varias subpoblaciones diferentes, cada una con su propia distribución. En lugar de modelar todos los datos como provenientes de una sola distribución, se asume que cada observación es generada por una de varias distribuciones con cierta probabilidad.

La fórmula general de una mezcla de distribuciones es:

\[ f(\mathbf{x}) = \sum_{k=1}^{K} \pi_k f_k(\mathbf{x}) \]

donde:

\(\mathbf{x}\) es el vector de datos de entrada, \(f_k(\mathbf{x})\) es la densidad de la \(k\)-ésima componente del modelo, \(\pi_k\) es el peso o proporción de la componente \(k\) en la mezcla, con:

\[ 0 < \pi_k < 1, \quad \sum_{k=1}^{K} \pi_k = 1 \] Este modelo indica que la probabilidad de observar \(\mathbf{x}\) se obtiene como una combinación ponderada de las probabilidades de cada componente.

Una de las aplicaciones más comunes es la mezcla de distribuciones normales multivariadas. En ese caso, cada \(f_k(\mathbf{x})\) es una densidad normal multivariada con media \(\boldsymbol{\mu}_k\) y matriz de covarianza \(\boldsymbol{\Sigma}_k\), y se expresa como:

\[ f_k(\mathbf{x}) = \frac{1}{(2\pi)^{p/2} |\boldsymbol{\Sigma}_k|^{1/2}} \exp\left( -\frac{1}{2} (\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}_k^{-1} (\mathbf{x} - \boldsymbol{\mu}_k) \right) \]

donde:

\(p\) es la dimensión de los datos, \(\boldsymbol{\mu}_k\) es el vector de medias para la componente \(k\), \(\boldsymbol{\Sigma}_k\) es la matriz de covarianza para la componente \(k\), \(|\boldsymbol{\Sigma}_k|\) es el determinante de la matriz de covarianza. Este enfoque permite modelar poblaciones que tienen subgrupos estructurados, lo cual es especialmente útil en aplicaciones como:

Segmentación de clientes, Detección de anomalías, Clasificación no supervisada (clustering), Reconocimiento de patrones. Las mixturas de gaussianas, por ejemplo, son la base de algoritmos como el EM (Expectation-Maximization), que estima iterativamente los parámetros del modelo y asigna probabilidades de pertenencia a cada observación.



# Simula datos desde una mezcla de 3 distribuciones normales
set.seed(123)
n <- 500
mu1 <- c(0, 0)
mu2 <- c(5, 5)
mu3 <- c(0, 5)

sigma1 <- matrix(c(1, 0.5, 0.5, 1), ncol = 2)
sigma2 <- matrix(c(1, -0.3, -0.3, 1), ncol = 2)
sigma3 <- matrix(c(0.5, 0, 0, 0.5), ncol = 2)

x1 <- MASS::mvrnorm(n = round(n * 0.4), mu = mu1, Sigma = sigma1)
x2 <- MASS::mvrnorm(n = round(n * 0.35), mu = mu2, Sigma = sigma2)
x3 <- MASS::mvrnorm(n = round(n * 0.25), mu = mu3, Sigma = sigma3)

data <- rbind(x1, x2, x3)

# Ajusta un modelo de mezcla gaussiana
modelo <- Mclust(data)
fitting ...

  |                                                            
  |                                                      |   0%
  |                                                            
  |                                                      |   1%
  |                                                            
  |=                                                     |   2%
  |                                                            
  |==                                                    |   3%
  |                                                            
  |==                                                    |   4%
  |                                                            
  |===                                                   |   5%
  |                                                            
  |===                                                   |   6%
  |                                                            
  |====                                                  |   7%
  |                                                            
  |====                                                  |   8%
  |                                                            
  |=====                                                 |   9%
  |                                                            
  |======                                                |  10%
  |                                                            
  |======                                                |  11%
  |                                                            
  |======                                                |  12%
  |                                                            
  |=======                                               |  13%
  |                                                            
  |========                                              |  14%
  |                                                            
  |========                                              |  15%
  |                                                            
  |=========                                             |  16%
  |                                                            
  |=========                                             |  17%
  |                                                            
  |==========                                            |  18%
  |                                                            
  |==========                                            |  19%
  |                                                            
  |===========                                           |  20%
  |                                                            
  |===========                                           |  21%
  |                                                            
  |============                                          |  22%
  |                                                            
  |============                                          |  23%
  |                                                            
  |=============                                         |  24%
  |                                                            
  |==============                                        |  25%
  |                                                            
  |==============                                        |  26%
  |                                                            
  |==============                                        |  27%
  |                                                            
  |===============                                       |  28%
  |                                                            
  |================                                      |  29%
  |                                                            
  |================                                      |  30%
  |                                                            
  |=================                                     |  31%
  |                                                            
  |=================                                     |  32%
  |                                                            
  |==================                                    |  33%
  |                                                            
  |==================                                    |  34%
  |                                                            
  |===================                                   |  35%
  |                                                            
  |====================                                  |  36%
  |                                                            
  |====================                                  |  37%
  |                                                            
  |====================                                  |  38%
  |                                                            
  |=====================                                 |  39%
  |                                                            
  |======================                                |  40%
  |                                                            
  |======================                                |  41%
  |                                                            
  |=======================                               |  42%
  |                                                            
  |=======================                               |  43%
  |                                                            
  |========================                              |  44%
  |                                                            
  |========================                              |  45%
  |                                                            
  |=========================                             |  46%
  |                                                            
  |==========================                            |  47%
  |                                                            
  |==========================                            |  48%
  |                                                            
  |==========================                            |  49%
  |                                                            
  |===========================                           |  50%
  |                                                            
  |============================                          |  51%
  |                                                            
  |============================                          |  52%
  |                                                            
  |============================                          |  53%
  |                                                            
  |=============================                         |  54%
  |                                                            
  |==============================                        |  55%
  |                                                            
  |==============================                        |  56%
  |                                                            
  |===============================                       |  57%
  |                                                            
  |===============================                       |  58%
  |                                                            
  |================================                      |  59%
  |                                                            
  |================================                      |  60%
  |                                                            
  |=================================                     |  61%
  |                                                            
  |==================================                    |  62%
  |                                                            
  |==================================                    |  63%
  |                                                            
  |==================================                    |  64%
  |                                                            
  |===================================                   |  65%
  |                                                            
  |====================================                  |  66%
  |                                                            
  |====================================                  |  67%
  |                                                            
  |=====================================                 |  68%
  |                                                            
  |=====================================                 |  69%
  |                                                            
  |======================================                |  70%
  |                                                            
  |======================================                |  71%
  |                                                            
  |=======================================               |  72%
  |                                                            
  |========================================              |  73%
  |                                                            
  |========================================              |  74%
  |                                                            
  |========================================              |  75%
  |                                                            
  |=========================================             |  76%
  |                                                            
  |==========================================            |  77%
  |                                                            
  |==========================================            |  78%
  |                                                            
  |===========================================           |  79%
  |                                                            
  |===========================================           |  80%
  |                                                            
  |============================================          |  81%
  |                                                            
  |============================================          |  82%
  |                                                            
  |=============================================         |  83%
  |                                                            
  |=============================================         |  84%
  |                                                            
  |==============================================        |  85%
  |                                                            
  |==============================================        |  86%
  |                                                            
  |===============================================       |  87%
  |                                                            
  |================================================      |  88%
  |                                                            
  |================================================      |  89%
  |                                                            
  |================================================      |  90%
  |                                                            
  |=================================================     |  91%
  |                                                            
  |==================================================    |  92%
  |                                                            
  |==================================================    |  93%
  |                                                            
  |===================================================   |  94%
  |                                                            
  |===================================================   |  95%
  |                                                            
  |====================================================  |  96%
  |                                                            
  |====================================================  |  97%
  |                                                            
  |===================================================== |  98%
  |                                                            
  |======================================================|  99%
  |                                                            
  |======================================================| 100%
# Visualiza los resultados
plot(modelo, what = "classification")

3.7) Cópulas

Una cópula es una función matemática que permite modelar la dependencia entre variables aleatorias de forma separada a sus distribuciones marginales. En otras palabras, las cópulas nos permiten construir distribuciones conjuntas a partir de las distribuciones individuales (marginales), capturando exclusivamente cómo estas variables se relacionan entre sí. Esto es muy útil en contextos donde las variables tienen diferentes comportamientos marginales pero comparten cierta estructura de dependencia, como en finanzas, seguros o análisis de riesgos.

Formalmente, una cópula es una función multivariada

\(C: [0,1]^d \rightarrow [0,1]\)

que asocia un vector de probabilidades marginales univariadas que asocia un vector de probabilidades marginales univariadas

\((u_1, u_2, \dots, u_d)\), cada una en el intervalo \([0,1]\), con una probabilidad conjunta.

La base teórica de las cópulas se encuentra en el Teorema de Sklar, propuesto por Abe Sklar en 1959. Este teorema establece que:

Si \(F(x_1, \dots, x_d)\) es una función de distribución conjunta continua con marginales \(F_1(x_1), \dots, F_d(x_d)\), entonces existe una única cópula \(C\) tal que:

\[ F(x_1, \dots, x_d) = C(F_1(x_1), \dots, F_d(x_d)) \]

Este resultado implica que cualquier función de distribución conjunta continua puede descomponerse en sus marginales univariadas y una cópula que modela la estructura de dependencia. Además, si las marginales son continuas, la cópula es única; si no lo son, existe pero puede no ser única.

Por el contrario, si conocemos una cópula \(C\) y las funciones de distribución marginales \(F_1, \dots, F_d\), también podemos reconstruir la función de distribución conjunta mediante la misma relación:

\[ F(x_1, \dots, x_d) = C(F_1(x_1), \dots, F_d(x_d)) \]

Esto permite una enorme flexibilidad, ya que se puede modelar cada variable con la distribución que mejor se ajuste a sus características y luego usar una cópula para unirlas según su dependencia.

Uno de los ejemplos más utilizados es la cópula gaussiana, que se basa en la distribución normal multivariada. Su expresión es:

\[ C_\Sigma(u_1, \dots, u_d) = \Phi_\Sigma(\Phi^{-1}(u_1), \dots, \Phi^{-1}(u_d)) \]

donde:

\(\Phi^{-1}\) es la inversa de la función de distribución normal estándar (también llamada función cuantílica o quantile function), \(\Phi_\Sigma\) es la función de distribución de una normal multivariada con matriz de correlación \(\Sigma\). Esta cópula transforma las variables uniformes marginales a normales estándar, luego calcula la probabilidad conjunta bajo una normal multivariada, y así genera la dependencia.

Las cópulas son fundamentales en campos como:

Finanzas, para modelar riesgo conjunto de portafolios o dependencias extremas en crisis. Actuaría, para modelar eventos dependientes como longevidad y enfermedades. Hidrología, para estudiar la dependencia entre precipitaciones y caudales extremos. En conclusión, las cópulas ofrecen un marco teórico y práctico muy potente para estudiar la dependencia entre variables aleatorias. Gracias al teorema de Sklar, se pueden separar los efectos marginales de la estructura de dependencia, permitiendo modelos más flexibles, interpretables y adecuados para fenómenos reales donde la correlación lineal resulta insuficiente.


# Definir copulas
normal_cop <- normalCopula(param = 0.7, dim = 2)    # dependencia positiva
clayton_cop <- claytonCopula(param = 3, dim = 2)    # dependencia en colas inferiores

# Simular datos
set.seed(123)
u_normal <- rCopula(1000, normal_cop)
u_clayton <- rCopula(1000, clayton_cop)

# Dibujar gráficos
par(mfrow = c(1, 2))  # dividir ventana en 2 gráficos

plot(u_normal, main = "Cópula Gaussiana (ρ = 0.7)", 
     xlab = expression(U[1]), ylab = expression(U[2]), pch = 20, col = rgb(0, 0, 1, 0.5))

plot(u_clayton, main = "Cópula de Clayton (θ = 3)", 
     xlab = expression(U[1]), ylab = expression(U[2]), pch = 20, col = rgb(1, 0, 0, 0.5))

3.8) Ejemplo con python

reticulate::repl_python()


# Configuración general
sns.set(style="whitegrid")
np.random.seed(42)

# 1. Distribución normal univariante vs estándar
mu, sigma = 50, 10
x = np.linspace(mu - 4*sigma, mu + 4*sigma, 1000)
x_std = np.linspace(-4, 4, 1000)
pdf = norm.pdf(x, mu, sigma)
pdf_std = norm.pdf(x_std)

plt.figure(figsize=(8, 4))
plt.plot(x, pdf, label="N(50,10)", color="blue")
<Figure size 800x400 with 0 Axes>
[<matplotlib.lines.Line2D object at 0x35aed56d0>]
plt.plot(x_std*sigma + mu, pdf_std, '--', label="N(0,1) reescalada", color="red")
[<matplotlib.lines.Line2D object at 0x35af010d0>]
plt.title("Distribución Normal Univariante y Estándar")
Text(0.5, 1.0, 'Distribución Normal Univariante y Estándar')
plt.xlabel("x")
plt.ylabel("Densidad")
Text(0.5, 0, 'x')
plt.legend()
Text(0, 0.5, 'Densidad')
plt.grid(True)
<matplotlib.legend.Legend object at 0x359d35010>
plt.tight_layout()
plt.show()

# Estandarización y transformaciones
X = 70
Z = (X - mu) / sigma
X_inv = Z * sigma + mu
X_linear = 2 * X + 5

print("Puntaje Z:", Z)
print("Recuperado desde Z:", X_inv)
Puntaje Z: 2.0
Recuperado desde Z: 70.0
print("Transformación lineal (2X + 5):", X_linear)
Transformación lineal (2X + 5): 145
# 2. Curvas de nivel de una normal bivariada
mean = [0, 0]
cov = [[1, 0.8], [0.8, 1]]
data = np.random.multivariate_normal(mean, cov, 1000)
df = pd.DataFrame(data, columns=["X", "Y"])

sns.kdeplot(data=df, x="X", y="Y", fill=True, thresh=0.01, levels=10, cmap="viridis")
<Axes: title={'center': 'Distribución Normal Univariante y Estándar'}, xlabel='x', ylabel='Densidad'>
plt.scatter(df["X"], df["Y"], s=5, alpha=0.3, color="blue")
<matplotlib.collections.PathCollection object at 0x359d65cd0>
plt.title("Curvas de Nivel - Normal Bivariada")
Text(0.5, 1.0, 'Curvas de Nivel - Normal Bivariada')
plt.xlabel("X")
plt.ylabel("Y")
Text(0.5, 36.50313465698394, 'X')
Text(22.916859374999966, 0.5, 'Y')
plt.axis("equal")
(-4.193109921847041, 94.70965549609235, -3.9006272783218767, 4.01230606991363)
plt.tight_layout()
plt.show()


# 3. Distancia Euclidiana
A = np.array([2, 3])
B = np.array([7, 6])
dist = np.linalg.norm(B - A)

plt.figure(figsize=(6, 6))
<Figure size 600x600 with 0 Axes>
plt.plot([A[0], B[0]], [A[1], B[1]], 'r--', lw=2)
[<matplotlib.lines.Line2D object at 0x35af67a10>]
plt.scatter(*A, color="blue", s=100, label="A")
<matplotlib.collections.PathCollection object at 0x35af65cd0>
plt.scatter(*B, color="green", s=100, label="B")
<matplotlib.collections.PathCollection object at 0x35af9dc10>
plt.text((A[0]+B[0])/2, (A[1]+B[1])/2+0.5, f"Distancia Euclidiana = {dist:.2f}", ha="center")
Text(4.5, 5.0, 'Distancia Euclidiana = 5.83')
plt.xlim(0, 10)
(0.0, 10.0)
plt.ylim(0, 10)
(0.0, 10.0)
plt.legend()
<matplotlib.legend.Legend object at 0x35aee4f10>
plt.grid(True)
plt.title("Distancia Euclidiana entre A y B")
Text(0.5, 1.0, 'Distancia Euclidiana entre A y B')
plt.tight_layout()
plt.show()


# 4. Distancia de Mahalanobis
iris = load_iris()
X_iris = iris.data[:, :3]
mu_iris = np.mean(X_iris, axis=0)
S_iris = np.cov(X_iris.T)

mahal_dist = [mahalanobis(x, mu_iris, np.linalg.inv(S_iris)) for x in X_iris]
eucl_dist = [np.linalg.norm(x - mu_iris) for x in X_iris]
mahal_dist = np.array(mahal_dist)
eucl_dist = np.array(eucl_dist)

threshold = np.sqrt(chi2.ppf(0.95, df=3))
outliers = mahal_dist > threshold

df_iris = pd.DataFrame({
    "Mahalanobis": mahal_dist,
    "Euclidean": eucl_dist,
    "Outlier": outliers
})

plt.figure(figsize=(8, 5))
<Figure size 800x500 with 0 Axes>
sns.scatterplot(data=df_iris, x="Euclidean", y="Mahalanobis", hue="Outlier", palette={False: "black", True: "red"})
<Axes: xlabel='Euclidean', ylabel='Mahalanobis'>
plt.axhline(threshold, linestyle="--", color="red", label=f"Umbral χ² (95%) = {threshold:.2f}")
<matplotlib.lines.Line2D object at 0x35aee7350>
plt.legend()
<matplotlib.legend.Legend object at 0x35a6d8690>
plt.title("Distancia Mahalanobis vs Euclidiana")
Text(0.5, 1.0, 'Distancia Mahalanobis vs Euclidiana')
plt.tight_layout()
plt.show()


# 5. Comparación Normal vs t multivariadas
mu = np.array([0, 0])
Sigma = np.array([[1, 0.7], [0.7, 1]])
n = 1000
df_t = 3

t_data = multivariate_t.rvs(loc=mu, shape=Sigma, df=df_t, size=n)
norm_data = np.random.multivariate_normal(mu, Sigma, size=n)

t_df = pd.DataFrame(t_data, columns=["X1", "X2"])
t_df["Distribución"] = "t-Student"

norm_df = pd.DataFrame(norm_data, columns=["X1", "X2"])
norm_df["Distribución"] = "Normal"

all_df = pd.concat([t_df, norm_df])

sns.scatterplot(data=all_df, x="X1", y="X2", hue="Distribución", alpha=0.5)
<Axes: title={'center': 'Distancia Mahalanobis vs Euclidiana'}, xlabel='Euclidean', ylabel='Mahalanobis'>
plt.title("Comparación: Normal vs t-Student multivariadas")
Text(0.5, 1.0, 'Comparación: Normal vs t-Student multivariadas')
plt.tight_layout()
plt.show()

NA
NA

4) Inferencia multivariante

La estadística inferencial multivariante es una rama de la estadística que permite hacer inferencias o generalizaciones sobre una población a partir de muestras, considerando más de una variable al mismo tiempo. A diferencia de la estadística univariante o bivariante, que analiza una o dos variables por separado, la multivariante busca entender cómo se relacionan varias variables entre sí y cómo esas relaciones afectan un fenómeno o resultado. Su objetivo no es solo describir los datos observados, sino estimar parámetros poblacionales, contrastar hipótesis y predecir comportamientos, considerando la interacción conjunta de las variables.

Entre los casos de uso más comunes de la estadística inferencial multivariante están:

  • Análisis de componentes principales (PCA), que reduce la dimensión de los datos manteniendo la mayor varianza posible.

  • Análisis discriminante, que permite clasificar observaciones en grupos previamente definidos.

  • Análisis de conglomerados (cluster analysis), que agrupa observaciones similares entre sí.

  • Modelos multivariantes de regresión, que explican una o más variables dependientes usando múltiples variables independientes.

  • MANOVA (análisis multivariante de varianza), que compara grupos considerando varias variables de respuesta a la vez.

Estos métodos se aplican en diversas áreas: en marketing, para segmentar clientes según sus características de consumo; en biología, para clasificar especies o evaluar efectos de tratamientos; en psicología, para analizar perfiles de personalidad; o en finanzas, para evaluar el riesgo y rendimiento de portafolios considerando múltiples indicadores. En resumen, la estadística inferencial multivariante es clave para tomar decisiones fundamentadas en contextos complejos con múltiples variables simultáneas.

4.1) Estadísticos muéstrales multivariantes

Los estadísticos muestrales multivariantes son medidas calculadas a partir de una muestra que resumen y describen las características conjuntas de varias variables al mismo tiempo. Estos estadísticos permiten analizar la estructura interna de los datos multivariados, es decir, cómo se relacionan y varían las variables entre sí dentro de la muestra. Entre los más comunes están el vector de medias muestral, que resume el valor promedio de cada variable; la matriz de varianzas y covarianzas, que indica cómo varían las variables conjuntamente; y la matriz de correlaciones, que estandariza esa relación para facilitar la comparación.

Estos estadísticos sirven como base para realizar inferencias sobre la población, por ejemplo, al contrastar hipótesis sobre la igualdad de medias entre grupos, evaluar la homogeneidad de covarianzas o construir modelos predictivos. Además, se utilizan en técnicas como el análisis de componentes principales, análisis discriminante y regresión múltiple, donde la comprensión de la estructura multivariada de los datos es esencial.

quit


# Usar el dataset iris (solo las variables numéricas)
data(iris)
datos_numericos <- iris %>% select(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)

# 1. Vector de medias muestral
medias <- colMeans(datos_numericos)
print("Vector de medias muestral:")
[1] "Vector de medias muestral:"
print(medias)
Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
    5.843333     3.057333     3.758000     1.199333 
# 2. Matriz de covarianzas muestral
covarianzas <- cov(datos_numericos)
print("Matriz de varianzas y covarianzas:")
[1] "Matriz de varianzas y covarianzas:"
print(covarianzas)
             Sepal.Length Sepal.Width Petal.Length Petal.Width
Sepal.Length    0.6856935  -0.0424340    1.2743154   0.5162707
Sepal.Width    -0.0424340   0.1899794   -0.3296564  -0.1216394
Petal.Length    1.2743154  -0.3296564    3.1162779   1.2956094
Petal.Width     0.5162707  -0.1216394    1.2956094   0.5810063
# 3. Matriz de correlaciones (opcional)
correlaciones <- cor(datos_numericos)
print("Matriz de correlaciones:")
[1] "Matriz de correlaciones:"
print(correlaciones)
             Sepal.Length Sepal.Width Petal.Length Petal.Width
Sepal.Length    1.0000000  -0.1175698    0.8717538   0.8179411
Sepal.Width    -0.1175698   1.0000000   -0.4284401  -0.3661259
Petal.Length    0.8717538  -0.4284401    1.0000000   0.9628654
Petal.Width     0.8179411  -0.3661259    0.9628654   1.0000000
# 4. Gráfico de dispersión multivariado
ggpairs(datos_numericos, title = "Gráfico de dispersión multivariado - Dataset Iris")

4.2) Teorema central del limite multivariante

El Teorema Central del Límite Multivariante (TCLM) extiende el clásico Teorema Central del Límite al caso de vectores aleatorios. Establece que, si se tienen variables aleatorias vectoriales independientes e idénticamente distribuidas (i.i.d.) con media vectorial \(\mu\) y matriz de covarianza \(\Sigma\), entonces la media muestral vectorial —al ser centrada y escalada adecuadamente— converge en distribución a una distribución normal multivariada conforme el tamaño de muestra crece. Específicamente, si \(\mathbf{X}_1, \dots, \mathbf{X}_n\) son vectores aleatorios i.i.d., entonces:

\[ \sqrt{n}(\bar{\mathbf{X}} - \boldsymbol{\mu}) \xrightarrow{d} \mathcal{N}_k(\mathbf{0}, \boldsymbol{\Sigma}) \]

donde \(\bar{\mathbf{X}}\) es la media muestral vectorial de dimensión \(k\), y \(\mathcal{N}_k(0, \Sigma)\) denota la distribución normal multivariada de media cero y matriz de covarianza \(\Sigma\). Este teorema es fundamental porque permite usar técnicas de inferencia estadística multivariada aun cuando las distribuciones originales de los datos no sean normales, siempre que el tamaño de muestra sea suficientemente grande. Gracias al TCLM, se pueden construir intervalos de confianza multivariados, realizar contrastes de hipótesis (como el test de Hotelling), y aplicar modelos lineales multivariados bajo el supuesto de normalidad asintótica.

En la práctica, el TCLM multivariante se usa en campos como la econometría (para comparar medias vectoriales de indicadores económicos), la psicología (donde se analizan puntajes en múltiples escalas), o la bioestadística (por ejemplo, al evaluar múltiples biomarcadores en ensayos clínicos). También es clave en el análisis de componentes principales y en técnicas de reducción de dimensionalidad, donde se parte del supuesto de normalidad multivariada para simplificar la estructura de los datos.



# Parámetros de la distribución original
mu <- c(3, 7)
Sigma <- matrix(c(1, 0.8,
                  0.8, 2), nrow = 2)

# Configuración de simulación
n <- 50                # Tamaño de muestra
reps <- 1000           # Número de repeticiones

# Simulación: generar medias muestrales
set.seed(123)
medias <- replicate(reps, {
  muestra <- mvrnorm(n = n, mu = mu, Sigma = Sigma)
  colMeans(muestra)
})

# Convertimos a data frame
df <- as.data.frame(t(medias))
colnames(df) <- c("X1", "X2")

# Gráfico
ggplot(df, aes(x = X1, y = X2)) +
  geom_point(alpha = 0.3, color = "steelblue") +
  stat_ellipse(type = "norm", color = "red", size = 1.2) +
  labs(title = "Distribución de medias muestrales bivariadas",
       subtitle = paste("Simulación de", reps, "medias muestrales con n =", n),
       x = "Media muestral de X1",
       y = "Media muestral de X2") +
  theme_minimal()

4.3) Máxima verosimilitud

La máxima verosimilitud es un método estadístico utilizado para estimar los parámetros desconocidos de un modelo probabilístico, dados unos datos observados. La idea es encontrar el valor del parámetro que hace más probable haber observado los datos disponibles. Si la función de densidad o de probabilidad es f(x;θ), donde θ es el parámetro a estimar y x son los datos observados, entonces la función de verosimilitud se define como:

\[ L(\theta) = \prod_{i=1}^n f(x_i; \theta) \]

El estimador de máxima verosimilitud es el valor \(\hat{\theta}\) que maximiza esta función:

\[ \hat{\theta} = \arg\max_{\theta} L(\theta) \]

Para facilitar los cálculos, se usa generalmente la log-verosimilitud, que transforma productos en sumas:

\[ \ell(\theta) = \log L(\theta) = \sum_{i=1}^n \log f(x_i; \theta) \]

Luego, se derivan las condiciones de primer orden resolviendo:

\[ \frac{d\ell(\theta)}{d\theta} = 0 \]

y se verifica que se trata de un máximo. Este enfoque proporciona estimaciones que, bajo condiciones generales, son consistentes, insesgadas y eficientes asintóticamente.

El método de máxima verosimilitud se utiliza ampliamente en estadística, econometría y machine learning. Por ejemplo, se aplica para estimar parámetros en modelos de regresión (como la regresión logística), ajustar distribuciones a datos (como normal, binomial o Poisson), o entrenar modelos probabilísticos complejos como mezclas de gaussianas o modelos ocultos de Markov. Su popularidad se debe a su generalidad y a las buenas propiedades estadísticas de los estimadores obtenidos.



# Paso 1: Simular datos
set.seed(123)
n <- 100
mu_verdadero <- 5
sigma_verdadero <- 2
datos <- rnorm(n, mean = mu_verdadero, sd = sigma_verdadero)

# Paso 2: Función de log-verosimilitud en función de mu (sigma conocido)
log_verosimilitud <- function(mu, datos, sigma) {
  n <- length(datos)
  suma <- sum((datos - mu)^2)
  ll <- -n/2 * log(2 * pi * sigma^2) - suma / (2 * sigma^2)
  return(ll)
}

# Paso 3: Evaluar log-verosimilitud para muchos valores de mu
mu_vals <- seq(3, 7, length.out = 200)
ll_vals <- sapply(mu_vals, log_verosimilitud, datos = datos, sigma = sigma_verdadero)

# Paso 4: Crear data frame para graficar
df <- data.frame(mu = mu_vals, logL = ll_vals)
mu_MLE <- mu_vals[which.max(ll_vals)]

# Paso 5: Gráfico
ggplot(df, aes(x = mu, y = logL)) +
  geom_line(color = "steelblue", size = 1) +
  geom_vline(xintercept = mu_MLE, linetype = "dashed", color = "red") +
  geom_point(aes(x = mu_MLE, y = max(logL)), color = "red", size = 2) +
  labs(
    title = "Log-verosimilitud para distintos valores de $$\\mu$$",
    subtitle = paste("Máxima verosimilitud en mu =", round(mu_MLE, 3)),
    x = expression(mu),
    y = "Log-verosimilitud"
  ) +
  theme_minimal()

4.4) Estimadores MLE para una normal

Los estimadores de máxima verosimilitud (MLE) para una distribución normal permiten estimar los parámetros desconocidos de la distribución: la media \(\mu\) y la varianza \(\sigma^2\). Dados datos independientes \(x_1, x_2, \dots, x_n\) provenientes de una distribución \(\mathcal{N}(\mu, \sigma^2)\), el objetivo es encontrar los valores de \(\mu\) y \(\sigma^2\) que maximizan la función de verosimilitud, es decir, que hacen más probable haber observado esos datos. La función de log-verosimilitud para estos datos es:

\[ \ell(\mu, \sigma^2) = -\frac{n}{2} \log(2\pi) - \frac{n}{2} \log(\sigma^2) - \frac{1}{2\sigma^2} \sum_{i=1}^n (x_i - \mu)^2 \]

Al derivar esta expresión respecto a \(\mu\) y \(\sigma^2\) y resolver las ecuaciones de primer orden, se obtienen los siguientes estimadores MLE:

\[ \hat{\mu} = \frac{1}{n} \sum_{i=1}^n x_i \]

Estos estimadores se usan ampliamente en estadística porque son consistentes, eficientes y tienen buenas propiedades asintóticas. Se aplican en múltiples áreas como econometría, psicología, biología y machine learning, especialmente al ajustar modelos probabilísticos, realizar inferencias y construir intervalos de confianza. Es importante notar que \(\hat{\sigma}^2\) (MLE) es un estimador sesgado porque divide entre \(n\), pero es el adecuado dentro del marco de máxima verosimilitud.

Estimador de máxima verosimilitud para la varianza:

\[ \hat{\sigma}^2 = \frac{1}{n} \sum_{i=1}^n (x_i - \hat{\mu})^2 \]



# Paso 1: Simular datos de una normal
set.seed(123)
n <- 100
mu_real <- 5
sigma_real <- 2
datos <- rnorm(n, mean = mu_real, sd = sigma_real)

# Paso 2: Estimadores MLE
mu_mle <- mean(datos)
sigma2_mle <- mean((datos - mu_mle)^2)
sigma_mle <- sqrt(sigma2_mle)

# Paso 3: Graficar histograma + densidad normal con parámetros MLE
df <- data.frame(x = datos)

ggplot(df, aes(x = x)) +
  geom_histogram(aes(y = ..density..), bins = 30, fill = "lightblue", color = "black", alpha = 0.7) +
  stat_function(fun = dnorm, args = list(mean = mu_mle, sd = sigma_mle),
                color = "red", size = 1.2) +
  labs(title = "Estimación por máxima verosimilitud",
       subtitle = bquote(hat(mu) == .(round(mu_mle, 2)) ~ "," ~ hat(sigma)^2 == .(round(sigma2_mle, 2))),
       x = "Datos simulados",
       y = "Densidad") +
  theme_minimal()

4.5) Test de hipotesis multivariante

El test de hipótesis multivariante es una extensión del contraste de hipótesis univariado al caso donde se analizan múltiples variables aleatorias simultáneamente. Este tipo de pruebas permite determinar si existen diferencias significativas entre grupos o si un conjunto de medias vectoriales es igual a un valor específico, considerando además la correlación entre variables. En lugar de trabajar con una media escalar, se trabaja con una media vectorial \(\boldsymbol{\mu}\) y una matriz de covarianza \(\boldsymbol{\Sigma}\). Uno de los casos más comunes es el test de Hotelling \(T^2\), que es el análogo multivariante de la prueba \(t\) de Student. Por ejemplo, si se tiene una muestra de vectores aleatorios \(\mathbf{X}_1, \dots, \mathbf{X}_n\) provenientes de una distribución normal multivariada con media \(\boldsymbol{\mu}\) y se desea contrastar la hipótesis nula \(H_0: \boldsymbol{\mu} = \boldsymbol{\mu}_0\), el estadístico de prueba es:

\[ T^2 = n(\bar{\mathbf{X}} - \boldsymbol{\mu}_0)^\top \mathbf{S}^{-1}(\bar{\mathbf{X}} - \boldsymbol{\mu}_0) \]



# Simular datos bivariados
set.seed(123)
n <- 30
mu <- c(5, 10)
sigma <- matrix(c(1, 0.8, 0.8, 1), nrow = 2)

X <- mvrnorm(n, mu = mu, Sigma = sigma)
X_df <- as.data.frame(X)
colnames(X_df) <- c("X1", "X2")

# Hipótesis nula: mu = (5, 10)
mu0 <- c(5, 10)

# Calcular media muestral
X_bar <- colMeans(X_df)

# Calcular matriz de covarianza
S <- cov(X_df)

# Calcular estadístico T² de Hotelling manualmente
diff <- X_bar - mu0
T2 <- n * t(diff) %*% solve(S) %*% diff

# Transformar a estadístico F
p <- ncol(X_df)
F_stat <- ((n - p) / (p * (n - 1))) * T2
p_valor <- 1 - pf(F_stat, p, n - p)

# Mostrar resultados
cat("Estadístico T²:", T2, "\n")
Estadístico T²: 1.37443 
cat("Estadístico F:", F_stat, "\n")
Estadístico F: 0.6635179 
cat("Valor p:", p_valor, "\n")
Valor p: 0.5229475 
# Graficar los datos y las medias
ggplot(X_df, aes(x = X1, y = X2)) +
  geom_point(alpha = 0.6, color = "blue") +
  geom_point(aes(x = mu0[1], y = mu0[2]), color = "red", size = 4, shape = 4) +
  geom_point(aes(x = X_bar[1], y = X_bar[2]), color = "darkgreen", size = 4, shape = 18) +
  labs(
    title = "Test de Hotelling T²",
    subtitle = paste0("T² = ", round(T2, 2), 
                      ", p-valor = ", round(p_valor, 4)),
    x = "Variable 1", y = "Variable 2"
  ) +
  theme_minimal()

donde \(\bar{\mathbf{X}}\) es la media muestral y \(\mathbf{S}\) es la matriz de covarianza muestral. Este estadístico, bajo \(H_0\), se distribuye como una F de Fisher ajustada. Los tests de hipótesis multivariantes son ampliamente usados en análisis discriminante, experimentos con varias respuestas dependientes, y validación de modelos con múltiples predictores.

4.6) Contraste para la media de una normal multivariante

El contraste para la media de una normal multivariante es una prueba estadística que permite determinar si la media poblacional de un vector aleatorio multivariante \(\mathbf{X} \sim \mathcal{N}_p(\boldsymbol{\mu}, \boldsymbol{\Sigma})\) es igual a un valor específico \(\boldsymbol{\mu}_0\). Este contraste es una extensión multivariada de la prueba t de Student univariada y se utiliza cuando se desea comprobar si un conjunto de medias de varias variables relacionadas coincide con un vector de medias teóricas. Es ampliamente usado en análisis de perfiles, calidad industrial, estudios clínicos y economía multivariada.

El procedimiento se basa en el estadístico de Hotelling \(T^2\), definido como:

\[ T^2 = n (\bar{\mathbf{X}} - \boldsymbol{\mu}_0)^\top \mathbf{S}^{-1} (\bar{\mathbf{X}} - \boldsymbol{\mu}_0) \]

donde \(n\) es el tamaño muestral, \(\bar{\mathbf{X}}\) es la media muestral, \(\mathbf{S}\) es la matriz de covarianza muestral y \(\boldsymbol{\mu}_0\) es el vector de medias bajo la hipótesis nula \(H_0: \boldsymbol{\mu} = \boldsymbol{\mu}_0\). Bajo \(H_0\), el estadístico \(T^2\) puede transformarse en una estadística \(F\) con \(p\) y \(n-p\) grados de libertad, permitiendo calcular el valor-p para tomar una decisión.

Este contraste es útil cuando se trabaja con datos donde las variables están correlacionadas entre sí y se necesita evaluar hipótesis sobre múltiples medias simultáneamente. Por ejemplo, en un estudio nutricional con varias mediciones (calorías, proteínas, grasas, etc.), podríamos probar si el promedio conjunto de estas variables en una muestra coincide con las recomendaciones nutricionales oficiales.

# ============================
# 1. Simular datos
# ============================
set.seed(123)
n <- 50
mu <- c(2, 3)
Sigma <- matrix(c(1, 0.5, 0.5, 2), nrow = 2)
datos <- mvrnorm(n, mu, Sigma)

# ============================
# 2. Contraste de hipótesis
# ============================
mu0 <- c(2, 3)
test <- HotellingsT2(datos, mu = mu0)
print(test)

    Hotelling's one sample T2-test

data:  datos
T.2 = 0.68556, df1 = 2, df2 = 48, p-value = 0.5087
alternative hypothesis: true location is not equal to c(2,3)
# ============================
# 3. Media muestral
# ============================
media_muestral <- colMeans(datos)

# ============================
# 4. Gráfico con elipse 95%
# ============================
ggplot(as.data.frame(datos), aes(x = V1, y = V2)) +
  geom_point(color = "steelblue", alpha = 0.6) +
  stat_ellipse(level = 0.95, color = "black", linetype = "dashed") +
  geom_point(aes(x = mu0[1], y = mu0[2]), color = "red", size = 3) +
  geom_point(aes(x = media_muestral[1], y = media_muestral[2]), color = "darkgreen", size = 3) +
  labs(x = "Variable 1", y = "Variable 2",
       title = "Elipse de confianza del 95% para la media multivariada",
       subtitle = "Rojo: μ₀ bajo H₀ | Verde: media muestral") +
  theme_minimal()

4.7) Contraste para la matriz de covarianza de una normal multivariante

El contraste para la matriz de covarianza de una normal multivariante permite evaluar si la matriz de covarianza poblacional \(\boldsymbol{\Sigma}\) cumple con una estructura específica. Este test es fundamental en análisis multivariado, ya que muchos métodos (como análisis discriminante o MANOVA) suponen que las poblaciones tienen matrices de covarianza iguales. La hipótesis nula típica es:

\[ H_0 : \boldsymbol{\Sigma} = \boldsymbol{\Sigma}_0 \]

donde \(\boldsymbol{\Sigma}_0\) es una matriz de covarianza conocida o bajo alguna estructura (por ejemplo, matriz diagonal, identidad, o común entre grupos).

Un caso muy usado es el test de esfericidad, en el cual se contrasta:

\[ H_0 : \boldsymbol{\Sigma} = \sigma^2 \mathbf{I} \]

lo cual implica que todas las variables tienen igual varianza y son incorreladas. Otro test común es el de igualdad de matrices de covarianza entre grupos, fundamental en técnicas como la LDA (Linear Discriminant Analysis). Estos contrastes suelen basarse en el estadístico de razón de verosimilitud, que bajo ciertas condiciones se aproxima a una distribución \(\chi^2\).

Este tipo de pruebas es crucial para validar supuestos antes de aplicar modelos multivariantes. Por ejemplo, en un análisis discriminante, si las matrices de covarianza difieren entre grupos, la técnica clásica (LDA) no es adecuada, y se prefiere QDA (Quadratic Discriminant Analysis). También se usa para validar estructuras en modelos como el análisis factorial o modelos de ecuaciones estructurales.




# Simular datos normales multivariantes
set.seed(123)
mu <- c(0, 0, 0)
Sigma <- matrix(c(1, 0.5, 0.3,
                  0.5, 1, 0.4,
                  0.3, 0.4, 1), nrow = 3)
datos <- mvrnorm(n = 100, mu = mu, Sigma = Sigma)

# Aplicar el test de esfericidad de Bartlett
resultado <- cortest.bartlett(cor(datos), n = nrow(datos))
print(resultado)
$chisq
[1] 39.97193

$p.value
[1] 0.00000001080207

$df
[1] 3
# Visualización: pares de dispersión
pairs(datos, main = "Dispersión de variables multivariadas")

4.8) Contraste de igualdad de varias medias

El contraste de igualdad de varias medias en el contexto multivariante evalúa si varios grupos poblacionales comparten la misma media vectorial multivariada. Esta prueba generaliza el análisis univariado de varianza (ANOVA) al caso en que cada observación es un vector en lugar de un solo valor escalar. Es especialmente útil cuando se tienen múltiples variables dependientes y se desea saber si la media conjunta de esas variables difiere entre grupos. La hipótesis nula plantea que todas las poblaciones tienen la misma media vectorial:

\[ H_0: \boldsymbol{\mu}_1 = \boldsymbol{\mu}_2 = \dots = \boldsymbol{\mu}_g \]

frente a la alternativa

\[ H_1: \text{al menos una } \boldsymbol{\mu}_i \text{ difiere} \]

Para realizar este contraste se utiliza el MANOVA (Multivariate Analysis of Variance). La estadística de prueba más común es el criterio de Wilks’ Lambda (\(\Lambda\)), definido como la razón entre el determinante del residuo (dentro de los grupos) y el determinante total:

\[ \Lambda = \frac{|\mathbf{W}|}{|\mathbf{T}|} \]

donde \(\mathbf{W}\) es la matriz de sumas de cuadrados y productos dentro de los grupos, y \(\mathbf{T}\) es la matriz total (suma de \(\mathbf{W}\) y \(\mathbf{B}\), la matriz entre grupos). Un valor pequeño de \(\Lambda\) sugiere diferencias significativas entre las medias.

Este tipo de prueba es útil en contextos como psicología, medicina o economía, donde se mide a varios grupos en varias dimensiones simultáneamente. Por ejemplo, comparar distintos métodos de enseñanza evaluando múltiples habilidades académicas en estudiantes.

4.9) Ejemplo con python

reticulate::repl_python()

# 4.1) Estadísticos muestrales multivariantes
iris = load_iris()
df_iris = pd.DataFrame(iris.data, columns=iris.feature_names)

print("Vector de medias muestral:")
Vector de medias muestral:
print(df_iris.mean())
sepal length (cm)    5.843333
sepal width (cm)     3.057333
petal length (cm)    3.758000
petal width (cm)     1.199333
dtype: float64
print("\nMatriz de covarianzas:")

Matriz de covarianzas:
print(df_iris.cov())
                   sepal length (cm)  ...  petal width (cm)
sepal length (cm)           0.685694  ...          0.516271
sepal width (cm)           -0.042434  ...         -0.121639
petal length (cm)           1.274315  ...          1.295609
petal width (cm)            0.516271  ...          0.581006

[4 rows x 4 columns]
print("\nMatriz de correlaciones:")

Matriz de correlaciones:
print(df_iris.corr())
                   sepal length (cm)  ...  petal width (cm)
sepal length (cm)           1.000000  ...          0.817941
sepal width (cm)           -0.117570  ...         -0.366126
petal length (cm)           0.871754  ...          0.962865
petal width (cm)            0.817941  ...          1.000000

[4 rows x 4 columns]
sns.pairplot(df_iris)
<seaborn.axisgrid.PairGrid object at 0x358ea0e10>
plt.suptitle("Gráfico de dispersión multivariado - Dataset Iris", y=1.02)
Text(0.5, 1.02, 'Gráfico de dispersión multivariado - Dataset Iris')
plt.show()

# 4.2) Teorema central del límite multivariante
np.random.seed(123)
mu = np.array([3, 7])
Sigma = np.array([[1, 0.8], [0.8, 2]])
n = 50
reps = 1000

samples = np.random.multivariate_normal(mu, Sigma, size=(reps, n))
sample_means = samples.mean(axis=1)

df_means = pd.DataFrame(sample_means, columns=["X1", "X2"])
sns.scatterplot(data=df_means, x="X1", y="X2", alpha=0.3, color="steelblue")
<Axes: xlabel='petal width (cm)', ylabel='Count'>
sns.kdeplot(data=df_means, x="X1", y="X2", levels=5, color="red")
<Axes: xlabel='petal width (cm)', ylabel='Count'>
plt.title(f"Distribución de medias muestrales bivariadas\nSimulación de {reps} medias muestrales con n = {n}")
Text(0.5, 1.0, 'Distribución de medias muestrales bivariadas\nSimulación de 1000 medias muestrales con n = 50')
plt.xlabel("Media muestral de X1")
Text(0.5, 0, 'Media muestral de X1')
plt.ylabel("Media muestral de X2")
plt.show()
Text(0, 0.5, 'Media muestral de X2')

# 4.3) Máxima verosimilitud
np.random.seed(123)
n = 100
mu_true = 5
sigma_true = 2
data = np.random.normal(mu_true, sigma_true, n)

def log_likelihood(mu, data, sigma):
    n = len(data)
    return -n/2 * np.log(2 * np.pi * sigma**2) - np.sum((data - mu)**2) / (2 * sigma**2)
    
mu_vals = np.linspace(3, 7, 200)
ll_vals = [log_likelihood(mu, data, sigma_true) for mu in mu_vals]
mu_mle = mu_vals[np.argmax(ll_vals)]

plt.plot(mu_vals, ll_vals, color="steelblue")
[<matplotlib.lines.Line2D object at 0x32d8a2dd0>]
plt.axvline(mu_mle, color="red", linestyle="--")
plt.scatter(mu_mle, max(ll_vals), color="red")
<matplotlib.lines.Line2D object at 0x363a0d850>
<matplotlib.collections.PathCollection object at 0x359d526d0>
plt.title("Log-verosimilitud para distintos valores de μ")
Text(0.5, 1.0, 'Log-verosimilitud para distintos valores de μ')
plt.xlabel("μ")
Text(0.5, 0, 'μ')
plt.ylabel("Log-verosimilitud")
Text(1071.855708333333, 0.5, 'Log-verosimilitud')
plt.show()


# 4.4) Estimadores MLE para una normal
mu_mle = np.mean(data)
sigma2_mle = np.mean((data - mu_mle)**2)
sigma_mle = np.sqrt(sigma2_mle)

plt.hist(data, bins=30, density=True, alpha=0.7, color="lightblue", edgecolor="black")
(array([0.02889642, 0.        , 0.02889642, 0.02889642, 0.        ,
       0.02889642, 0.05779284, 0.05779284, 0.1444821 , 0.17337852,
       0.05779284, 0.26006778, 0.17337852, 0.17337852, 0.11558568,
       0.11558568, 0.11558568, 0.17337852, 0.1444821 , 0.02889642,
       0.1444821 , 0.17337852, 0.23117136, 0.05779284, 0.11558568,
       0.05779284, 0.05779284, 0.        , 0.08668926, 0.05779284]), array([-0.59717821, -0.25111459,  0.09494904,  0.44101266,  0.78707629,
        1.13313991,  1.47920354,  1.82526716,  2.17133079,  2.51739441,
        2.86345804,  3.20952166,  3.55558529,  3.90164891,  4.24771254,
        4.59377616,  4.93983979,  5.28590341,  5.63196703,  5.97803066,
        6.32409428,  6.67015791,  7.01622153,  7.36228516,  7.70834878,
        8.05441241,  8.40047603,  8.74653966,  9.09260328,  9.43866691,
        9.78473053]), <BarContainer object of 30 artists>)
x_vals = np.linspace(min(data), max(data), 100)
plt.plot(x_vals, norm.pdf(x_vals, mu_mle, sigma_mle), color="red", linewidth=2)
[<matplotlib.lines.Line2D object at 0x363a2ab50>]
plt.title("Estimación por máxima verosimilitud")
Text(0.5, 1.0, 'Estimación por máxima verosimilitud')
plt.xlabel("Datos simulados")
Text(0.5, 0, 'Datos simulados')
plt.ylabel("Densidad")
Text(1106.980708333333, 0.5, 'Densidad')
plt.show()


# 4.5) Test de hipótesis multivariante (Hotelling T²)
np.random.seed(123)
n = 30
mu = np.array([5, 10])
Sigma = np.array([[1, 0.8], [0.8, 1]])
X = np.random.multivariate_normal(mu, Sigma, n)
X_bar = np.mean(X, axis=0)
S = np.cov(X, rowvar=False)
mu0 = np.array([5, 10])
diff = X_bar - mu0
T2 = n * diff.T @ np.linalg.inv(S) @ diff
p = X.shape[1]
F_stat = ((n - p) / (p * (n - 1))) * T2
p_value = 1 - chi2.cdf(T2, p)

print(f"Estadístico T²: {T2}")
Estadístico T²: 0.34766945783457043
print(f"Estadístico F: {F_stat}")
Estadístico F: 0.16784042792013745
print(f"Valor p: {p_value}")
Valor p: 0.8404357859132993
plt.scatter(X[:, 0], X[:, 1], alpha=0.6, color="blue")
<matplotlib.collections.PathCollection object at 0x36399a4d0>
plt.scatter(*mu0, color="red", s=100, marker="x")
<matplotlib.collections.PathCollection object at 0x36391f350>
plt.scatter(*X_bar, color="green", s=100, marker="D")
<matplotlib.collections.PathCollection object at 0x363a4e690>
plt.title(f"Test de Hotelling T²\nT² = {round(T2, 2)}, p-valor = {round(p_value, 4)}")
Text(0.5, 1.0, 'Test de Hotelling T²\nT² = 0.35, p-valor = 0.8404')
plt.xlabel("Variable 1")
Text(0.5, 0, 'Variable 1')
plt.ylabel("Variable 2")
Text(1106.980708333333, 0.5, 'Variable 2')
plt.show()


# 4.6) Contraste para la media de una normal multivariante
np.random.seed(123)
n = 50
mu = np.array([2, 3])
Sigma = np.array([[1, 0.5], [0.5, 2]])
data_mv = np.random.multivariate_normal(mu, Sigma, n)
mu0 = np.array([2, 3])
X_bar = np.mean(data_mv, axis=0)
S = np.cov(data_mv, rowvar=False)
diff = X_bar - mu0
T2 = n * diff.T @ np.linalg.inv(S) @ diff
p = data_mv.shape[1]
p_value = 1 - chi2.cdf(T2, p)

print(f"Hotelling T²: {T2}, p-valor: {p_value}")
Hotelling T²: 0.866061297748732, p-valor: 0.6485406144605548
plt.scatter(data_mv[:, 0], data_mv[:, 1], alpha=0.6, color="steelblue")
<matplotlib.collections.PathCollection object at 0x363a83fd0>
plt.scatter(*mu0, color="red", s=100, marker="x")
<matplotlib.collections.PathCollection object at 0x363a8aed0>
plt.scatter(*X_bar, color="green", s=100, marker="D")
plt.title("Elipse de confianza del 95% para la media multivariada")
<matplotlib.collections.PathCollection object at 0x363a84d90>
plt.xlabel("Variable 1")
Text(0.5, 1.0, 'Elipse de confianza del 95% para la media multivariada')
plt.ylabel("Variable 2")
Text(0.5, 0, 'Variable 1')
plt.show()
Text(1106.980708333333, 0.5, 'Variable 2')

# 4.7) Contraste para la matriz de covarianza
np.random.seed(123)
mu = np.array([0, 0, 0])
Sigma = np.array([[1, 0.5, 0.3], [0.5, 1, 0.4], [0.3, 0.4, 1]])
data_cov = np.random.multivariate_normal(mu, Sigma, 100)
bartlett_test = bartlett(*data_cov.T)
print("Test de esfericidad de Bartlett:")
print(bartlett_test)
Test de esfericidad de Bartlett:
BartlettResult(statistic=2.9174855533658524, pvalue=0.23252843120390307)
# 4.8) Contraste de igualdad de varias medias (MANOVA)
df_manova = pd.DataFrame(data_cov, columns=["X1", "X2", "X3"])
df_manova["Group"] = ["A"] * 50 + ["B"] * 50
maov = MANOVA.from_formula("X1 + X2 + X3 ~ Group", data=df_manova)
print(maov.mv_test())
                 Multivariate linear model
============================================================
                                                            
------------------------------------------------------------
       Intercept        Value  Num DF  Den DF F Value Pr > F
------------------------------------------------------------
          Wilks' lambda 0.9813 3.0000 96.0000  0.6111 0.6094
         Pillai's trace 0.0187 3.0000 96.0000  0.6111 0.6094
 Hotelling-Lawley trace 0.0191 3.0000 96.0000  0.6111 0.6094
    Roy's greatest root 0.0191 3.0000 96.0000  0.6111 0.6094
------------------------------------------------------------
                                                            
------------------------------------------------------------
         Group          Value  Num DF  Den DF F Value Pr > F
------------------------------------------------------------
          Wilks' lambda 0.9626 3.0000 96.0000  1.2447 0.2979
         Pillai's trace 0.0374 3.0000 96.0000  1.2447 0.2979
 Hotelling-Lawley trace 0.0389 3.0000 96.0000  1.2447 0.2979
    Roy's greatest root 0.0389 3.0000 96.0000  1.2447 0.2979
============================================================

5) Análisis con componentes principales

El Análisis con Componentes Principales (PCA, por sus siglas en inglés) es una técnica estadística que transforma un conjunto de variables posiblemente correlacionadas en un nuevo conjunto de variables no correlacionadas llamadas componentes principales. Cada componente principal es una combinación lineal de las variables originales, y se ordenan de manera que el primer componente (\(PC_1\)) capture la mayor varianza posible de los datos, el segundo (\(PC_2\)) capture la mayor varianza restante, y así sucesivamente. Matemáticamente, si tenemos una matriz de datos \(X\) (centrada), el PCA se basa en descomponer su matriz de covarianzas \(\Sigma\) en valores y vectores propios, de modo que \(\Sigma v_k = \lambda_k v_k\), donde \(v_k\) es el vector propio y \(\lambda_k\) su varianza asociada.

Su utilidad principal es reducir la dimensionalidad del conjunto de datos manteniendo la mayor cantidad posible de información (varianza). Esto es valioso cuando se tienen muchas variables, ya que ayuda a simplificar el análisis, reducir ruido y evitar problemas como la multicolinealidad. La proyección de los datos sobre los primeros \(m\) componentes principales (\(m < p\), siendo \(p\) el número original de variables) genera una representación más compacta, pero con la mayor varianza preservada. El nuevo conjunto transformado se expresa como \(Z = XW\), donde \(W\) es la matriz formada por los vectores propios seleccionados.

En la práctica, el PCA se usa para visualización de datos en 2D o 3D, preprocesamiento en modelos de machine learning, compresión de datos, identificación de patrones y eliminación de ruido. Por ejemplo, en genética se usa para diferenciar grupos poblacionales, en reconocimiento facial para reducir el número de píxeles a analizar, y en economía para sintetizar indicadores macroeconómicos correlacionados en unos pocos factores que expliquen la mayor parte de la variabilidad.

5.1) La maldición de la dimensionalidad

La maldición de la dimensionalidad describe cómo, al aumentar el número de dimensiones \(p\) en un conjunto de datos, el volumen del espacio crece exponencialmente y los puntos se vuelven escasos, lo que degrada medidas de proximidad como la distancia euclidiana \(d(x_i, x_j) = \sqrt{\sum_{k=1}^p (x_{ik} - x_{jk})^2}\) que tiende a ser similar para todos los pares de puntos; esto provoca problemas como sobreajuste, pérdida de capacidad discriminativa y necesidad de un número de observaciones que crece exponencialmente con \(p\), motivo por el cual se aplican técnicas de reducción de dimensionalidad como PCA, t-SNE o UMAP.

quit
# Creamos semilla
set.seed(123)

# ===============================
# 1. Generar datos simulados
# ===============================
n <- 100
x1 <- rnorm(n, mean = 5, sd = 2)
x2 <- 0.5 * x1 + rnorm(n, mean = 0, sd = 1)  # Correlacionada con x1
x3 <- -0.3 * x1 + 0.8 * x2 + rnorm(n, sd = 0.5)

datos <- data.frame(X1 = x1, X2 = x2, X3 = x3)

# ===============================
# 2. Gráfico 3D datos originales
# ===============================
fig3d <- plot_ly(datos, x = ~X1, y = ~X2, z = ~X3,
                 type = "scatter3d", mode = "markers",
                 marker = list(size = 4, color = 'blue', opacity = 0.7)) %>%
  layout(title = "Datos originales en 3D")
fig3d

# ===============================
# 3. PCA
# ===============================
pca <- prcomp(datos, center = TRUE, scale. = TRUE)

# Varianza explicada
var_exp <- pca$sdev^2 / sum(pca$sdev^2)
df_var <- data.frame(Componente = paste0("PC", 1:length(var_exp)),
                     Varianza = var_exp)

# Scree plot
ggplot(df_var, aes(x = Componente, y = Varianza)) +
  geom_bar(stat = "identity", fill = "skyblue") +
  geom_text(aes(label = round(Varianza, 2)), vjust = -0.5) +
  theme_minimal() +
  labs(title = "Varianza explicada por componente",
       y = "Proporción de varianza")


# ===============================
# 4. Proyección en los dos primeros PCs
# ===============================
scores <- as.data.frame(pca$x)
ggplot(scores, aes(x = PC1, y = PC2)) +
  geom_point(color = "red", alpha = 0.7) +
  theme_minimal() +
  labs(title = "Proyección en los dos primeros componentes principales")


# ===============================
# 5. Maldición de la dimensionalidad
# ===============================
dimensiones <- 1:50
distancias_rel <- sapply(dimensiones, function(p) {
  datos_hd <- matrix(runif(n * p), nrow = n) # Datos aleatorios en alta dimensión
  d <- dist(datos_hd)
  d_min <- min(d)
  d_max <- max(d)
  (d_max - d_min) / d_min # Diferencia relativa
})

df_md <- data.frame(Dimensiones = dimensiones,
                    DifRelativa = distancias_rel)

ggplot(df_md, aes(x = Dimensiones, y = DifRelativa)) +
  geom_line(color = "darkgreen", size = 1) +
  theme_minimal() +
  labs(title = "Maldición de la dimensionalidad",
       y = "Diferencia relativa entre distancias")

5.2) Selección de componentes

Para seleccionar el número ideal de componentes principales, un método muy usado es el criterio de Kaiser, que consiste en conservar aquellos componentes cuyo valor propio (\(\lambda_k\)) sea mayor que \(1\) cuando se trabaja con la matriz de correlaciones. La lógica es que cada variable estandarizada aporta una varianza de \(1\) a la varianza total; por tanto, un componente con \(\lambda_k > 1\) explica más varianza que una variable original y se justifica conservarlo. Otro método ampliamente empleado es observar el gráfico de sedimentación (scree plot), donde se grafican los valores propios en orden descendente y se busca el “codo” de la curva: el punto a partir del cual la ganancia de varianza explicada por añadir nuevos componentes es marginal. Este enfoque es intuitivo y permite balancear la simplicidad del modelo con la retención de información.

Ambos métodos son prácticos porque combinan criterios cuantitativos (valores propios) y visuales (codo del scree plot), evitando retener componentes que solo capturan ruido. En aplicaciones reales, también puede considerarse un umbral de varianza acumulada (por ejemplo, conservar los primeros \(m\) componentes que expliquen al menos un \(80%\) de la varianza), lo que permite asegurar que la reducción dimensional mantenga la mayor parte de la información relevante para el análisis.

# ==========================
# 1. Datos y PCA
# ==========================
data("USArrests")
datos <- na.omit(USArrests)

pca <- prcomp(datos, center = TRUE, scale. = TRUE)

# ==========================
# 2. Valores propios (Eigenvalues)
# ==========================
eigenvalues <- pca$sdev^2
names(eigenvalues) <- paste0("PC", 1:length(eigenvalues))

# Criterio de Kaiser (λ > 1)
kaiser_components <- sum(eigenvalues > 1)

# Varianza explicada
var_exp <- eigenvalues / sum(eigenvalues)
var_acum <- cumsum(var_exp)

# Número de componentes para ≥ 80% varianza
m_80 <- which(var_acum >= 0.80)[1]

# ==========================
# 3. Scree plot
# ==========================
df_pca <- data.frame(
  Componente = factor(names(eigenvalues), levels = names(eigenvalues)),
  ValorPropio = eigenvalues,
  VarExp = var_exp,
  VarAcum = var_acum
)

ggplot(df_pca, aes(x = Componente, y = ValorPropio)) +
  geom_point(size = 3) +
  geom_line(aes(group = 1)) +
  geom_hline(yintercept = 1, color = "red", linetype = "dashed") +
  theme_minimal() +
  labs(title = "Scree plot y Criterio de Kaiser",
       y = "Valor propio (λ)",
       x = "Componente principal") +
  annotate("text", x = kaiser_components, y = 1.1,
           label = paste("Kaiser:", kaiser_components, "componentes"),
           hjust = -0.1, color = "red")


# ==========================
# 4. Varianza acumulada
# ==========================
ggplot(df_pca, aes(x = Componente, y = VarAcum)) +
  geom_point(size = 3, color = "blue") +
  geom_line(aes(group = 1), color = "blue") +
  geom_hline(yintercept = 0.80, color = "darkgreen", linetype = "dashed") +
  theme_minimal() +
  labs(title = "Varianza acumulada",
       y = "Proporción acumulada de varianza",
       x = "Componente principal") +
  annotate("text", x = m_80, y = 0.82,
           label = paste("≥80% con", m_80, "componentes"),
           hjust = -0.1, color = "darkgreen")

5.3) Graficos para el entendimiento de dimensiones

En PCA, el gráfico de magnitudes y direcciones (o biplot de cargas) muestra cómo cada variable original contribuye y se orienta respecto a los primeros componentes principales. Las flechas indican la dirección en la que aumenta cada variable y su longitud refleja la magnitud de su contribución: flechas largas apuntan a variables que tienen mayor peso en la construcción de esos componentes. Cuando dos flechas están próximas y en la misma dirección, sus variables están positivamente correlacionadas; si están opuestas, están negativamente correlacionadas.

La versión en dispersión (o biplot con observaciones) proyecta simultáneamente las observaciones y las variables en el espacio definido por \(PC_1\) y \(PC_2\). Así, se puede ver no solo cómo se relacionan las variables, sino también cómo se distribuyen las unidades de análisis (por ejemplo, países, personas, productos) en función de esas combinaciones. Los puntos representan las observaciones y las flechas representan las variables; la proximidad entre un punto y una flecha sugiere que esa observación tiene un valor alto en esa variable.



data("USArrests")
pca <- prcomp(USArrests, scale. = TRUE)

# 1. Gráfico de magnitudes y direcciones (solo variables)
ggbiplot(pca, choices = c(1,2), obs.scale = 0, var.scale = 1,
         groups = NULL, ellipse = FALSE, circle = TRUE) +
  ggtitle("PCA - Magnitudes y direcciones (variables)") +
  theme_minimal()
G1;H1;Errorh in ggbiplot(pca, choices = c(1, 2), obs.scale = 0, var.scale = 1,  : 
  could not find function "ggbiplot"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
# 2. Biplot en dispersión (variables + observaciones)
ggbiplot(pca, choices = c(1,2), obs.scale = 1, var.scale = 1,
         groups = NULL, ellipse = FALSE, circle = TRUE) +
  ggtitle("PCA - Biplot con observaciones y variables") +
  theme_minimal()
G1;H1;Errorh in ggbiplot(pca, choices = c(1, 2), obs.scale = 1, var.scale = 1,  : 
  could not find function "ggbiplot"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g

5.4) Ejemplo en python

reticulate::repl_python()

# Semilla de reproducibilidad
np.random.seed(123)

# ===============================
# 1. Generar datos simulados
# ===============================
n = 100
x1 = np.random.normal(5, 2, n)
x2 = 0.5 * x1 + np.random.normal(0, 1, n)
x3 = -0.3 * x1 + 0.8 * x2 + np.random.normal(0, 0.5, n)

datos = pd.DataFrame({"X1": x1, "X2": x2, "X3": x3})

# ===============================
# 2. Gráfico 3D datos originales
# ===============================
fig3d = px.scatter_3d(datos, x="X1", y="X2", z="X3",
                      opacity=0.7, color_discrete_sequence=["blue"])
fig3d.update_traces(marker=dict(size=4))
Figure({
    'data': [{'hovertemplate': 'X1=%{x}<br>X2=%{y}<br>X3=%{z}<extra></extra>',
              'legendgroup': '',
              'marker': {'color': 'blue', 'opacity': 0.7, 'size': 4, 'symbol': 'circle'},
              'mode': 'markers',
              'name': '',
              'scene': 'scene',
              'showlegend': False,
              'type': 'scatter3d',
              'x': {'bdata': ('8pj0zUGhBkDNIN8/kPobQAfWFDuKQx' ... '12OC8CQPCpJzADCRdAqsZwWHL3EEA='),
                    'dtype': 'f8'},
              'y': {'bdata': ('W6CfeI5zAEC7uO2usk/4P8dhMAhC9g' ... 'IOLWnwP3bK88MbTgRAU0WheC9y/j8='),
                    'dtype': 'f8'},
              'z': {'bdata': ('3gyEU+pe8j/PwMeZCenyv1i1+7xm0A' ... 'GBmizWP1hq7Qxqhdg/2iqaBioc5T8='),
                    'dtype': 'f8'}}],
    'layout': {'legend': {'tracegroupgap': 0},
               'margin': {'t': 60},
               'scene': {'domain': {'x': [0.0, 1.0], 'y': [0.0, 1.0]},
                         'xaxis': {'title': {'text': 'X1'}},
                         'yaxis': {'title': {'text': 'X2'}},
                         'zaxis': {'title': {'text': 'X3'}}},
               'template': '...'}
})
fig3d.update_layout(title="Datos originales en 3D")
Figure({
    'data': [{'hovertemplate': 'X1=%{x}<br>X2=%{y}<br>X3=%{z}<extra></extra>',
              'legendgroup': '',
              'marker': {'color': 'blue', 'opacity': 0.7, 'size': 4, 'symbol': 'circle'},
              'mode': 'markers',
              'name': '',
              'scene': 'scene',
              'showlegend': False,
              'type': 'scatter3d',
              'x': {'bdata': ('8pj0zUGhBkDNIN8/kPobQAfWFDuKQx' ... '12OC8CQPCpJzADCRdAqsZwWHL3EEA='),
                    'dtype': 'f8'},
              'y': {'bdata': ('W6CfeI5zAEC7uO2usk/4P8dhMAhC9g' ... 'IOLWnwP3bK88MbTgRAU0WheC9y/j8='),
                    'dtype': 'f8'},
              'z': {'bdata': ('3gyEU+pe8j/PwMeZCenyv1i1+7xm0A' ... 'GBmizWP1hq7Qxqhdg/2iqaBioc5T8='),
                    'dtype': 'f8'}}],
    'layout': {'legend': {'tracegroupgap': 0},
               'margin': {'t': 60},
               'scene': {'domain': {'x': [0.0, 1.0], 'y': [0.0, 1.0]},
                         'xaxis': {'title': {'text': 'X1'}},
                         'yaxis': {'title': {'text': 'X2'}},
                         'zaxis': {'title': {'text': 'X3'}}},
               'template': '...',
               'title': {'text': 'Datos originales en 3D'}}
})
fig3d.show()

# ===============================
# 3. PCA
# ===============================
scaler = StandardScaler()
datos_scaled = scaler.fit_transform(datos)

pca = PCA()
pca.fit(datos_scaled)
PCA()
# Varianza explicada
var_exp = pca.explained_variance_ratio_
df_var = pd.DataFrame({
    "Componente": [f"PC{i+1}" for i in range(len(var_exp))],
    "Varianza": var_exp
})

# Scree plot
plt.figure(figsize=(6,4))
<Figure size 600x400 with 0 Axes>
sns.barplot(x="Componente", y="Varianza", data=df_var, color="skyblue")
for i, v in enumerate(var_exp):
<Axes: xlabel='Componente', ylabel='Varianza'>
    plt.text(i, v + 0.01, f"{v:.2f}", ha="center")
plt.title("Varianza explicada por componente")
Text(0, 0.718498394089065, '0.71')
Text(1, 0.2793002263341916, '0.27')
Text(2, 0.03220137957674345, '0.02')
Text(0.5, 1.0, 'Varianza explicada por componente')
plt.ylabel("Proporción de varianza")
Text(0, 0.5, 'Proporción de varianza')
plt.show()

# ===============================
# 4. Proyección en los dos primeros PCs
# ===============================
scores = pca.transform(datos_scaled)
plt.figure(figsize=(6,4))
<Figure size 600x400 with 0 Axes>
sns.scatterplot(x=scores[:,0], y=scores[:,1], color="red", alpha=0.7)
<Axes: >
plt.xlabel("PC1")
Text(0.5, 0, 'PC1')
plt.ylabel("PC2")
Text(0, 0.5, 'PC2')
plt.title("Proyección en los dos primeros componentes principales")
Text(0.5, 1.0, 'Proyección en los dos primeros componentes principales')
plt.show()


# ===============================
# 5. Maldición de la dimensionalidad
# ===============================
dimensiones = range(1, 51)
distancias_rel = []

for p in dimensiones:
    datos_hd = np.random.rand(n, p)
    d = pdist(datos_hd)
    d_min = np.min(d)
    d_max = np.max(d)
    distancias_rel.append((d_max - d_min) / d_min)
    
df_md = pd.DataFrame({"Dimensiones": dimensiones,
                      "DifRelativa": distancias_rel})

plt.figure(figsize=(6,4))
<Figure size 600x400 with 0 Axes>
sns.lineplot(x="Dimensiones", y="DifRelativa", data=df_md, color="darkgreen")
<Axes: xlabel='Dimensiones', ylabel='DifRelativa'>
plt.title("Maldición de la dimensionalidad")
plt.ylabel("Diferencia relativa entre distancias")
Text(0.5, 1.0, 'Maldición de la dimensionalidad')
plt.show()

Text(0, 0.5, 'Diferencia relativa entre distancias')

6) Análisis factorial

El análisis factorial es una técnica estadística multivariante que busca identificar factores latentes (variables no observadas) que explican la correlación entre un conjunto de variables observadas. Matemáticamente, se asume que cada variable observada \(x_i\) puede expresarse como una combinación lineal de factores comunes \(F_j\) más un término de error:

\[ x_i = \lambda_{i1}F_1 + \lambda_{i2}F_2 + \dots + \lambda_{im}F_m + \varepsilon_i \]

donde \(\lambda_{ij}\) son las cargas factoriales, \(F_j\) los factores y \(\varepsilon_i\) el error específico.

Sirve principalmente para reducir la dimensionalidad y descubrir estructuras subyacentes en los datos, agrupando variables correlacionadas en factores que representan constructos teóricos (por ejemplo, “inteligencia”, “satisfacción”, “riesgo”). Esto permite simplificar modelos, eliminar redundancia y facilitar la interpretación de fenómenos complejos.

Entre los casos de uso más comunes están:

  • Psicometría: identificar dimensiones como ansiedad, depresión o motivación a partir de ítems de cuestionarios.
  • Marketing: agrupar atributos de productos en factores como “calidad percibida” o “precio-beneficio”.
  • Análisis socioeconómico: construir índices compuestos (ej. desarrollo humano) a partir de múltiples indicadores.

Diferencia con componentes principales

La diferencia central es que el análisis factorial es un modelo estadístico que parte de la hipótesis de que las variables observadas están influidas por un conjunto reducido de factores latentes no observables, por lo que busca explicar las correlaciones entre variables a través de estas dimensiones subyacentes; en cambio, el análisis de componentes principales (PCA) es una herramienta descriptiva que transforma las variables originales en combinaciones lineales ortogonales llamadas componentes, maximizando la varianza explicada sin asumir la existencia de factores latentes, siendo más útil para reducción de dimensionalidad y visualización que para inferir estructuras teóricas.

Ejemplo con R

quit




# ---------- 1) Datos ----------
# Usaremos los 25 ítems de personalidad tipo Likert del dataset bfi que viene en psych
data(bfi, package = "psych")
df <- bfi[, 1:25]              # ítems A1:A5, C1:C5, E1:E5, N1:N5, O1:O5
df <- na.omit(df)              # removemos casos con NA
n  <- nrow(df)

message(sprintf("Observaciones tras NA-omit: %s; Variables: %s", n, ncol(df)))

# ---------- 2) Correlaciones, KMO y Bartlett ----------
# Para Likert, opcionalmente podrías usar correlación policórica; aquí usamos Pearson para velocidad.
R <- cor(df)

# Mapa de calor de correlaciones
corrplot(R,
         method = "color",
         order = "hclust",
         addrect = 5,                 # dibuja 5 bloques por clustering
         tl.cex = 0.6,
         number.cex = 0.6,
         mar = c(0,0,2,0),
         title = "Mapa de calor de correlaciones (agrupadas)")


# Adecuación muestral (KMO) y Esfericidad (Bartlett)
message("==> KMO:")
print(KMO(R))
Kaiser-Meyer-Olkin factor adequacy
Call: KMO(r = R)
Overall MSA =  0.85
MSA for each item = 
  A1   A2   A3   A4   A5   C1   C2   C3   C4   C5   E1   E2 
0.75 0.84 0.87 0.88 0.90 0.84 0.80 0.85 0.83 0.86 0.84 0.88 
  E3   E4   E5   N1   N2   N3   N4   N5   O1   O2   O3   O4 
0.90 0.88 0.89 0.78 0.78 0.86 0.89 0.86 0.86 0.78 0.84 0.77 
  O5 
0.76 
message("==> Test de esfericidad de Bartlett:")
print(cortest.bartlett(R, n = n))
$chisq
[1] 18146.07

$p.value
[1] 0

$df
[1] 300
# ---------- 3) Número de factores: Scree + Análisis paralelo ----------
# fa.parallel devuelve gráficos y ayuda a decidir cuántos factores retener
fa.parallel(df, fa = "fa", fm = "ml",
            main = "Scree + Análisis paralelo (FA-ML)")
Parallel analysis suggests that the number of factors =  6  and the number of components =  NA 

# Sugerencia: para bfi suelen ser ~5 factores (Big Five)
nfactors <- 5   # Ajusta si el análisis paralelo sugiere otro número
message(sprintf("Número de factores seleccionado: %d", nfactors))

# ---------- 4) AFE (ML) con rotación oblicua (oblimin) ----------
fa_fit <- fa(df,
             nfactors = nfactors,
             rotate   = "oblimin",  # permite correlación entre factores
             fm       = "ml",
             scores   = "regression")

cat("\n==> RESUMEN DE CARGAS (cutoff = 0.30):\n")

==> RESUMEN DE CARGAS (cutoff = 0.30):
print(fa_fit$loadings, cutoff = 0.30)

Loadings:
   ML2    ML5    ML3    ML1    ML4   
A1        -0.394                     
A2         0.626                     
A3         0.688                     
A4         0.471                     
A5         0.575                     
C1                0.542              
C2                0.642              
C3                0.572              
C4               -0.669              
C5               -0.578              
E1                       0.564       
E2                       0.655       
E3                      -0.343  0.331
E4         0.355        -0.537       
E5                      -0.393       
N1  0.865                            
N2  0.818                            
N3  0.664                            
N4  0.413                0.435       
N5  0.438                            
O1                              0.538
O2                             -0.461
O3                              0.639
O4                       0.360  0.367
O5                             -0.522

                 ML2   ML5   ML3   ML1   ML4
SS loadings    2.428 1.990 1.988 1.862 1.609
Proportion Var 0.097 0.080 0.080 0.074 0.064
Cumulative Var 0.097 0.177 0.256 0.331 0.395
cat("\n==> Varianza explicada:\n")

==> Varianza explicada:
print(fa_fit$Vaccounted)
                            ML2        ML5        ML3
SS loadings           2.5049054 2.17047275 2.10957419
Proportion Var        0.1001962 0.08681891 0.08438297
Cumulative Var        0.1001962 0.18701513 0.27139810
Proportion Explained  0.2368711 0.20524620 0.19948746
Cumulative Proportion 0.2368711 0.44211733 0.64160478
                             ML1        ML4
SS loadings           2.10097115 1.68904811
Proportion Var        0.08403885 0.06756192
Cumulative Var        0.35543694 0.42299887
Proportion Explained  0.19867393 0.15972129
Cumulative Proportion 0.84027871 1.00000000
cat("\n==> Primeras filas de puntajes factoriales:\n")

==> Primeras filas de puntajes factoriales:
print(head(fa_fit$scores))
              ML2          ML5         ML3         ML1
61617 -0.13689455 -0.847682914 -1.28182293 -0.25501349
61618  0.08159975  0.002253053 -0.58847796 -0.45968605
61620  0.70378073 -0.524259426 -0.06197118 -0.14866234
61621 -0.08901081 -0.000256587 -1.08260195 -0.04692905
61622 -0.25059288 -0.586646020 -0.08680802 -0.50976906
61623  0.27623246  0.453898140  1.37447842 -1.03075362
             ML4
61617 -1.6297864
61618 -0.1303406
61620  0.2328693
61621 -1.0221110
61622 -0.6452909
61623  0.6742498
# ---------- 5) Heatmap de cargas factoriales ----------
# Pasamos la matriz de cargas a formato largo y ordenamos por factor dominante
L <- as.data.frame(unclass(fa_fit$loadings))
L$Item <- rownames(L)
fact_cols <- setdiff(names(L), "Item")

L$Dominant <- apply(L[, fact_cols], 1, function(x) names(which.max(abs(x))))
L$MaxAbs   <- apply(abs(L[, fact_cols]), 1, max)
L <- L[order(L$Dominant, -L$MaxAbs), ]

L_long <- L |>
  select(Item, all_of(fact_cols)) |>
  pivot_longer(cols = all_of(fact_cols), names_to = "Factor", values_to = "Loading")

L_long$Item <- factor(L_long$Item, levels = unique(L$Item))

p_loadings <- ggplot(L_long, aes(x = Factor, y = Item, fill = Loading)) +
  geom_tile(color = "white") +
  scale_fill_gradient2(low = "#313695", mid = "white", high = "#A50026", midpoint = 0) +
  labs(title = "Heatmap de cargas factoriales (oblimin)",
       x = "Factor", y = "Ítem", fill = "Carga") +
  theme_minimal(base_size = 11) +
  theme(axis.text.y = element_text(size = 7))
print(p_loadings)


# ---------- 6) Diagrama del modelo factorial ----------
# Muestra factores, ítems, cargas y correlaciones entre factores
fa.diagram(fa_fit, simple = FALSE, digits = 2,
           main = "Diagrama del modelo factorial (AFE, ML + oblimin)")


# ---------- 7) Dispersión de puntajes factoriales ----------
scores <- as.data.frame(fa_fit$scores)
if (!is.null(scores) && ncol(scores) >= 2) {
  f1 <- names(scores)[1]
  f2 <- names(scores)[2]
  p_scores <- ggplot(scores, aes_string(x = f1, y = f2)) +
    geom_point(alpha = 0.25, color = "#2C7FB8") +
    geom_hline(yintercept = 0, linetype = 2, color = "grey60") +
    geom_vline(xintercept = 0, linetype = 2, color = "grey60") +
    labs(title = "Dispersión de puntajes factoriales", x = f1, y = f2) +
    theme_minimal(base_size = 11)
  print(p_scores)
} else {
  message("No se generaron puntajes factoriales para graficar.")
}

NA
NA

6.1) Modelo factorial

El modelo factorial es un modelo estadístico que busca explicar la variabilidad y las correlaciones observadas en un conjunto de variables mediante un número reducido de factores latentes que no se pueden medir directamente. En este enfoque, cada variable observada \(X_i\) se expresa como una combinación lineal de los factores comunes \(F_j\), más un término de error específico. Matemáticamente, el modelo puede escribirse como:

\[X = \Lambda F + \varepsilon\]

donde \(X\) es el vector de variables observadas, \(\Lambda\) es la matriz de cargas factoriales que refleja cuánto aporta cada factor a cada variable, \(F\) es el vector de factores comunes y \(\varepsilon\) representa los errores o varianzas específicas.

El objetivo del modelo factorial es identificar estructuras subyacentes que expliquen las correlaciones entre las variables observadas. A diferencia de un análisis puramente descriptivo, aquí se asume explícitamente la existencia de estos factores latentes. Por ejemplo, en psicometría, los puntajes de varias pruebas de razonamiento, memoria o atención pueden explicarse por un factor común llamado \(g\) (inteligencia general). En este caso, el modelo factorial permite inferir qué parte de la varianza de cada prueba corresponde a la influencia de ese factor común y cuál a aspectos propios de la prueba.

En cuanto a los casos de uso, el modelo factorial se aplica en campos como psicología (para medir constructos como personalidad, inteligencia o motivaciones), economía (para identificar factores latentes como “confianza del consumidor” a partir de varias encuestas), biología (para agrupar rasgos genéticos relacionados), y ciencias sociales en general. Su utilidad principal es reducir complejidad e identificar dimensiones ocultas detrás de un gran número de variables correlacionadas, lo cual permite construir escalas, validar cuestionarios o elaborar indicadores compuestos.

Ejemplo con R




# 1. Simulamos datos con 6 variables y 2 factores latentes
set.seed(123)
sim_data <- sim.structure(fx = matrix(c(
  0.8, 0,   # Var1 depende del Factor1
  0.7, 0,   # Var2 depende del Factor1
  0.6, 0,   # Var3 depende del Factor1
  0, 0.7,   # Var4 depende del Factor2
  0, 0.8,   # Var5 depende del Factor2
  0, 0.6    # Var6 depende del Factor2
), nrow = 6, ncol = 2, byrow = TRUE), n = 500)

# Extraemos la matriz de datos
data <- sim_data$observed

# 2. Estimamos un análisis factorial con 2 factores
fa_result <- fa(data, nfactors = 2, rotate = "varimax")

# 3. Mostramos resultados
print(fa_result)
Factor Analysis using method =  minres
Call: fa(r = data, nfactors = 2, rotate = "varimax")
Standardized loadings (pattern matrix) based upon correlation matrix

                       MR1  MR2
SS loadings           1.51 1.50
Proportion Var        0.25 0.25
Cumulative Var        0.25 0.50
Proportion Explained  0.50 0.50
Cumulative Proportion 0.50 1.00

Mean item complexity =  1
Test of the hypothesis that 2 factors are sufficient.

df null model =  15  with the objective function =  1.44 with Chi Square =  715.22
df of  the model are 4  and the objective function was  0.04 

The root mean square of the residuals (RMSR) is  0.03 
The df corrected root mean square of the residuals is  0.06 

The harmonic n.obs is  500 with the empirical chi square  13.63  with prob <  0.0086 
The total n.obs was  500  with Likelihood Chi Square =  22.07  with prob <  0.00019 

Tucker Lewis Index of factoring reliability =  0.903
RMSEA index =  0.095  and the 90 % confidence intervals are  0.059 0.136
BIC =  -2.79
Fit based upon off diagonal values = 0.99
Measures of factor score adequacy             
                                                   MR1  MR2
Correlation of (regression) scores with factors   0.87 0.89
Multiple R square of scores with factors          0.76 0.79
Minimum correlation of possible factor scores     0.53 0.58
# 4. Graficamos el modelo factorial
fa.diagram(fa_result, main = "Diagrama del Modelo Factorial")

NA
NA

6.2) Propiedades y unicidad

En el contexto del análisis factorial, el concepto de propiedades y unicidad se refiere a la parte de la varianza de cada variable observada que no es explicada por los factores comunes. Cada variable \(X_i\) se descompone en una parte compartida con los factores (comunalidad) y otra propia o única. Matemáticamente, si \(h_i^2\) es la comunalidad de la variable \(X_i\), la unicidad se define como \(u_i^2 = (1- h_i^2)\) asumiendo que las variables están estandarizadas.

La unicidad \(u_i^2\) representa la varianza específica y el error de medición asociados a \(X_i\), es decir, lo que no puede ser atribuido a los factores comunes. Si \(u_i^2\) es cercano a 0, significa que casi toda la variabilidad de la variable se explica por los factores; en cambio, un valor alto indica que esa variable está influida mayormente por aspectos propios y no comparte estructura con el resto.

Este concepto es clave para evaluar la calidad del modelo factorial: permite decidir qué variables conviene mantener (aquellas con baja unicidad y alta comunalidad) y cuáles aportan poca información al análisis. En la práctica se usa en psicometría para validar ítems de un test, en economía para depurar indicadores redundantes y en biología para seleccionar rasgos relevantes, asegurando que los factores extraídos reflejen verdaderamente dimensiones comunes y no ruido individual.

Ejemplo Grafico



# 1. Usamos algunas variables de mtcars
data <- mtcars[, c("mpg", "hp", "wt", "qsec", "drat")]

# 2. Ajustamos un modelo factorial con 2 factores
fa_result <- fa(data, nfactors = 2, rotate = "varimax")

# 3. Extraemos comunalidades y unicidades
comunalidades <- fa_result$communality
unicidades <- fa_result$uniquenesses

# 4. Mostramos tabla
resultados <- data.frame(
  Variable = names(comunalidades),
  Comunalidad = round(comunalidades, 3),
  Unicidad = round(unicidades, 3)
)
print(resultados)

# 5. Graficamos comunalidad vs unicidad
barplot(
  rbind(comunalidades, unicidades),
  beside = TRUE,
  names.arg = names(comunalidades),
  col = c("steelblue", "tomato"),
  legend.text = c("Comunalidad (h^2)", "Unicidad (u^2)"),
  main = "Comunalidades y Unicidades por Variable"
)

6.3) Criterio Varimax

El criterio varimax es un método de rotación ortogonal en el análisis factorial y en el análisis de componentes principales. Su objetivo es simplificar la estructura de las cargas factoriales (\(\lambda_{ij}\)) maximizando la varianza de los cuadrados de las cargas dentro de cada factor. Dicho de otra forma, busca que cada variable tenga cargas altas en un solo factor y bajas en los demás, lo que facilita la interpretación de los factores.

Matemáticamente, el criterio varimax consiste en maximizar la función:

\[V = \sum_{j=1}^m \left[ \frac{1}{p} \sum_{i=1}^p \lambda_{ij}^4 \;-\; \left( \frac{1}{p} \sum_{i=1}^p \lambda_{ij}^2 \right)^2 \right] \]

donde \(p\) es el número de variables y \(m\) el número de factores. La idea es que, al maximizar \(V\), se obtienen cargas factoriales más “polarizadas”: algunas muy cercanas a 0 y otras más grandes, lo que refuerza la claridad en la asociación variable–factor.

El varimax es una técnica que se aplica después de extraer factores en un análisis factorial y sirve para que los resultados sean más fáciles de interpretar: lo que hace es “girar” los factores de manera que cada variable quede asociada principalmente con un solo factor y tenga valores cercanos a cero en los demás; así, en vez de tener cargas mezcladas y confusas, se obtiene una estructura más clara donde cada factor representa un grupo de variables bien definido, lo que ayuda a darle un significado más sencillo y útil a los factores.

El criterio varimax se usa ampliamente en psicología (para interpretar cuestionarios y escalas), en economía (para identificar dimensiones claras en encuestas o indicadores), y en biología o ciencias sociales en general (para agrupar rasgos o características). Su principal ventaja es que mejora la legibilidad de los resultados sin alterar la cantidad de varianza explicada, haciendo que los factores rotados sean más fáciles de etiquetar e interpretar.

Ejemplo con R



# 1. Simulamos datos: 6 variables, 2 factores
set.seed(123)
sim <- sim.structure(
  fx = matrix(c(
    0.8, 0,   # Var1 depende de Factor1
    0.7, 0,   # Var2 depende de Factor1
    0.6, 0,   # Var3 depende de Factor1
    0, 0.7,   # Var4 depende de Factor2
    0, 0.8,   # Var5 depende de Factor2
    0, 0.6    # Var6 depende de Factor2
  ), nrow = 6, ncol = 2, byrow = TRUE),
  n = 500
)

data <- sim$observed

# 2. Análisis factorial SIN rotación
fa_sin <- fa(data, nfactors = 2, rotate = "none")

# 3. Análisis factorial CON rotación varimax
fa_varimax <- fa(data, nfactors = 2, rotate = "varimax")

# 4. Comparación de cargas factoriales
print(fa_sin$loadings, cutoff = 0.3)

Loadings:
   MR1    MR2   
V1 -0.552  0.634
V2 -0.457  0.495
V3 -0.475  0.348
V4  0.537  0.534
V5  0.582  0.486
V6  0.470  0.367

                 MR1   MR2
SS loadings    1.588 1.424
Proportion Var 0.265 0.237
Cumulative Var 0.265 0.502
print(fa_varimax$loadings, cutoff = 0.3)

Loadings:
   MR1    MR2   
V1         0.840
V2         0.674
V3         0.578
V4  0.756       
V5  0.757       
V6  0.595       

                 MR1   MR2
SS loadings    1.512 1.500
Proportion Var 0.252 0.250
Cumulative Var 0.252 0.502
# 5. Diagramas gráficos
par(mfrow = c(1,2))  # dos gráficos lado a lado
fa.diagram(fa_sin, main = "Sin rotación")
fa.diagram(fa_varimax, main = "Con rotación Varimax")

6.4) Análisis de factor componente principal

El análisis de factor componente principal (también llamado principal component factor analysis o principal component method) es un enfoque dentro del análisis factorial en el que los factores se extraen a partir de las componentes principales de la matriz de correlaciones. A diferencia del modelo factorial clásico, que asume factores latentes y unicidades, este método parte de la idea de que la varianza de cada variable puede explicarse inicialmente por una combinación lineal de componentes, y se aprovecha esa descomposición para estimar las cargas factoriales iniciales.

El procedimiento consiste en calcular las componentes principales de los datos, que son combinaciones lineales de las variables originales, de manera que la primera componente explica la mayor parte de la varianza total, la segunda explica la máxima varianza restante, y así sucesivamente. Si llamamos \(X\) a la matriz de datos estandarizados, el método se basa en la descomposición espectral de la matriz de correlaciones \(R\), de forma que:

\[Q = Q \Lambda Q´\]

donde \(Q\) es la matriz de vectores propios (direcciones principales) y \(\Lambda\) es la matriz diagonal con los valores propios (varianzas explicadas). Las cargas factoriales iniciales se obtienen a partir de estos autovalores y autovectores. El análisis de factor componente principal se utiliza principalmente para reducir la dimensionalidad y para identificar estructuras latentes aproximadas, cuando no se busca un modelo estricto de factores pero sí una representación más manejable de los datos. Además, es útil como paso previo a rotaciones (como varimax) que mejoran la interpretación de los factores extraídos.

En cuanto a casos de uso, se aplica en psicometría para construir y depurar cuestionarios, en economía para resumir indicadores en factores como “actividad económica” o “confianza”, en biología para agrupar variables genéticas o de rasgos, y en marketing para identificar patrones de comportamiento del consumidor. Su ventaja es que es computacionalmente sencillo y capta gran parte de la variabilidad de los datos, aunque su interpretación como “factores latentes” es menos rigurosa que en el análisis factorial común.




# 1. Seleccionamos algunas variables de mtcars
data <- mtcars[, c("mpg", "hp", "wt", "qsec", "drat")]

# 2. Análisis factorial clásico (método "minres")
fa_classic <- fa(data, nfactors = 2, fm = "minres", rotate = "varimax")

# 3. Análisis de factor componente principal (fm = "pc")
fa_pc <- fa(data, nfactors = 2, fm = "pc", rotate = "varimax")
G3;factor method not specified correctly, minimum residual (unweighted least squares  used
g
# 4. Mostrar cargas factoriales
cat("=== Análisis factorial clásico (minres) ===\n")
=== Análisis factorial clásico (minres) ===
print(fa_classic$loadings, cutoff = 0.3)

Loadings:
     MR1    MR2   
mpg   0.864  0.400
hp   -0.567 -0.721
wt   -0.943       
qsec         0.950
drat  0.748       

                 MR1   MR2
SS loadings    2.520 1.609
Proportion Var 0.504 0.322
Cumulative Var 0.504 0.826
cat("\n=== Análisis factor componente principal (pc) ===\n")

=== Análisis factor componente principal (pc) ===
print(fa_pc$loadings, cutoff = 0.3)

Loadings:
     MR1    MR2   
mpg   0.864  0.400
hp   -0.567 -0.721
wt   -0.943       
qsec         0.950
drat  0.748       

                 MR1   MR2
SS loadings    2.520 1.609
Proportion Var 0.504 0.322
Cumulative Var 0.504 0.826
# 5. Graficar resultados
par(mfrow = c(1,2))
fa.diagram(fa_classic, main = "Factorial clásico (minres)")
fa.diagram(fa_pc, main = "Factor Componente Principal (pc)")

6.5) Análisis de factor principal

El análisis de factor principal es un método dentro del análisis factorial que busca estimar los factores a partir de la varianza común entre las variables, diferenciándose del método de componentes principales que usa toda la varianza. La idea central es que cada variable observada \(X_i\) puede descomponerse en una parte explicada por los factores comunes y otra por su unicidad \(u_i^2\). Formalmente, si \(\Sigma\) es la matriz de correlaciones, se intenta aproximarla como:

\[\Sigma \approx \Lambda \Lambda' + \Psi\] donde \(\Lambda\) es la matriz de cargas factoriales y \(\Psi\) es una matriz diagonal con las unicidades.

Este método se llama de factor principal porque las comunalidades iniciales se estiman como las correlaciones múltiples de cada variable con las demás, en lugar de suponer que toda la varianza es común (como en PCA). Así, en cada iteración, se recalculan las cargas factoriales ajustando las comunalidades hasta que se estabilizan, logrando un modelo que refleja mejor la existencia de factores latentes.

El análisis de factor principal se utiliza cuando se quiere un modelo que represente factores comunes subyacentes, más cercano al planteamiento teórico del análisis factorial clásico. Es especialmente útil en psicometría, ciencias sociales y economía, cuando el interés es inferir constructos no observables como “inteligencia”, “motivación” o “confianza”, que explican las correlaciones entre variables.

En cuanto a sus casos de uso, aparece en la construcción y validación de cuestionarios (para identificar ítems que miden la misma dimensión), en estudios de mercado (para detectar factores ocultos en las preferencias del consumidor), y en análisis socioeconómicos (para extraer dimensiones latentes a partir de indicadores regionales o macroeconómicos). Su ventaja es que se enfoca en la varianza compartida entre variables, evitando el sesgo de incluir varianza única o de error en la estimación de factores.



# 1. Usamos algunas variables de mtcars
data <- mtcars[, c("mpg", "hp", "wt", "qsec", "drat")]

# 2. Análisis factorial con método de factor principal (Principal Axis)
fa_pa <- fa(data, nfactors = 2, fm = "pa", rotate = "varimax")

# 3. Análisis factorial con método de componentes principales (PC)
fa_pc <- fa(data, nfactors = 2, fm = "pc", rotate = "varimax")
G3;factor method not specified correctly, minimum residual (unweighted least squares  used
g
# 4. Mostramos cargas factoriales
cat("=== Factor Principal (pa) ===\n")
=== Factor Principal (pa) ===
print(fa_pa$loadings, cutoff = 0.3)

Loadings:
     PA1    PA2   
mpg   0.857  0.412
hp   -0.555 -0.747
wt   -0.942       
qsec         0.917
drat  0.751       

                 PA1   PA2
SS loadings    2.496 1.598
Proportion Var 0.499 0.320
Cumulative Var 0.499 0.819
cat("\n=== Factor Componente Principal (pc) ===\n")

=== Factor Componente Principal (pc) ===
print(fa_pc$loadings, cutoff = 0.3)

Loadings:
     MR1    MR2   
mpg   0.864  0.400
hp   -0.567 -0.721
wt   -0.943       
qsec         0.950
drat  0.748       

                 MR1   MR2
SS loadings    2.520 1.609
Proportion Var 0.504 0.322
Cumulative Var 0.504 0.826
# 5. Graficamos los resultados
par(mfrow = c(1,2))  # dos gráficos lado a lado
fa.diagram(fa_pa, main = "Método Factor Principal (pa)")
fa.diagram(fa_pc, main = "Método Componente Principal (pc)")

6.6) Estimacion de los factores

La estimación de factores es una técnica estadística utilizada para reducir la dimensionalidad de un conjunto de variables observadas, identificando un número menor de variables latentes llamadas factores. Estas variables latentes explican la mayor parte de la varianza común entre las variables originales.

Matemáticamente, se asume que cada variable observada \(X_i\) puede expresarse como una combinación lineal de factores \(F_j\) más un término de error:

\[ X_i = \lambda*{i1}F_1 +* \lambda{i2}F_2 + \dots + \lambda\_{im}F_m + \epsilon\_i\]

donde \(\lambda_{ij}\) son las cargas factoriales y \(\epsilon_i\) es el error específico.

Su principal utilidad es identificar estructuras subyacentes en datos complejos, simplificando el análisis sin perder información esencial. Esto permite agrupar variables correlacionadas en factores comunes, lo que facilita la interpretación y reduce problemas de multicolinealidad. Además, ayuda a construir escalas o índices a partir de múltiples indicadores, muy usado en ciencias sociales, psicometría y economía

Entre los métodos de estimación más comunes están:

  • Máxima verosimilitud (ML): busca estimar parámetros que maximizan la probabilidad de los datos observados.

  • Componentes principales (PCA): aunque no es estrictamente análisis factorial, se usa para estimar factores iniciales.

  • Método de mínimos cuadrados: minimiza la diferencia entre la matriz de correlaciones observada y la reproducida por el modelo factorial.

Casos de uso:

  • En psicología, para identificar dimensiones latentes como rasgos de personalidad a partir de cuestionarios.

  • En marketing, para segmentar consumidores según patrones de comportamiento.

  • En finanzas, para reducir el número de indicadores económicos en modelos predictivos.

  • En investigación social, para construir índices como el de calidad de vida a partir de múltiples variables.

Metodo de Bartlett

El método de Bartlett es una técnica para estimar los puntajes factoriales en análisis factorial, buscando obtener estimaciones insesgadas y eficientes bajo el supuesto de normalidad multivariante. A diferencia del método de regresión, que minimiza errores en las variables observadas, Bartlett minimiza la varianza de los errores en los factores, lo que produce estimaciones más precisas cuando el modelo factorial es correcto. La fórmula general para los puntajes factoriales es:

\[\hat{\mathbf{F}} = (\mathbf{\Lambda}^\top \mathbf{\Psi}^{-1} \mathbf{\Lambda})^{-1} \mathbf{\Lambda}^\top \mathbf{\Psi}^{-1} \mathbf{X}\]

donde \(\mathbf{\Lambda}\) es la matriz de cargas factoriales, \(\mathbf{\Psi}\) la matriz diagonal de varianzas de error y \(\mathbf{X}\) el vector de variables estandarizadas. Este método se usa en psicometría, ciencias sociales y economía cuando se requiere interpretar factores latentes con mínima distorsión.

Ejemplo con R

# ============================
# Ejemplo gráfico de Estimación de Factores en R
# ============================


# Establecemos semilla de reproducibilidad
set.seed(123)

# 1) Simulación de datos con estructura factorial conocida (2 factores, 8 variables)
n  <- 500   # número de observaciones
p  <- 8     # número de variables observadas
m  <- 2     # número de factores

# Matriz de cargas (Lambda): variables 1-4 cargan en F1; 5-8 en F2, con pequeñas cargas cruzadas
Lambda <- matrix(0, nrow = p, ncol = m)
Lambda[1:4, 1] <- c(0.8, 0.75, 0.7, 0.65)
Lambda[1:4, 2] <- c(0.10, 0.05, 0.10, 0.05)
Lambda[5:8, 1] <- c(0.10, 0.05, 0.10, 0.05)
Lambda[5:8, 2] <- c(0.8, 0.75, 0.7, 0.65)

# Varianzas específicas (Psi) — errores idiosincráticos
psi <- rep(0.35, p)  # cada variable con ruido moderado

# Factores latentes ~ N(0, I)
F_scores_true <- matrix(rnorm(n * m), nrow = n, ncol = m)

# Errores ~ N(0, Psi)
E <- matrix(rnorm(n * p), nrow = n, ncol = p) %*% diag(sqrt(psi))

# Generación de datos: X = F * Lambda' + E
X <- F_scores_true %*% t(Lambda) + E
colnames(X) <- paste0("V", 1:p)

# Estandarización (recomendado antes de FA)
X <- scale(X)

# ============================
# 2) Gráfico 1: Matriz de correlaciones (estructura por bloques)
# ============================
cor_mat <- cor(X)
corrplot::corrplot(cor_mat, method = "color", type = "full",
                   tl.col = "black", tl.srt = 45, addCoef.col = NA,
                   col = colorRampPalette(c("#2c7fb8", "white", "#d7191c"))(200),
                   mar = c(0,0,1,0), title = "Matriz de correlaciones")


# ============================
# 3) Scree plot (autovalores de la matriz de correlación)
# ============================
eig <- eigen(cor_mat)$values
plot(seq_along(eig), eig, type = "b", pch = 19, col = "#2c7fb8",
     xlab = "Componente/Factor (orden)", ylab = "Autovalor",
     main = "Scree plot (autovalores)")
abline(h = 1, lty = 2, col = "gray40") # Regla de Kaiser
text(1:length(eig), eig, labels = round(eig, 2), pos = 3, cex = 0.8)


# ============================
# 4) Estimación de factores (ML) + rotación oblicua (oblimin)
#    Puntajes por Método de Bartlett
# ============================
fa_fit <- psych::fa(r = cor_mat, nfactors = 2, n.obs = n, fm = "ml",
                    rotate = "oblimin", scores = "Bartlett")

print(fa_fit, digits = 3)
Factor Analysis using method =  ml
Call: psych::fa(r = cor_mat, nfactors = 2, n.obs = n, rotate = "oblimin", 
    scores = "Bartlett", fm = "ml")
Standardized loadings (pattern matrix) based upon correlation matrix

                        ML1   ML2
SS loadings           2.441 2.301
Proportion Var        0.305 0.288
Cumulative Var        0.305 0.593
Proportion Explained  0.515 0.485
Cumulative Proportion 0.515 1.000

 With factor correlations of 
      ML1   ML2
ML1 1.000 0.204
ML2 0.204 1.000

Mean item complexity =  1
Test of the hypothesis that 2 factors are sufficient.

df null model =  28  with the objective function =  3.473 with Chi Square =  1720.906
df of  the model are 13  and the objective function was  0.071 

The root mean square of the residuals (RMSR) is  0.02 
The df corrected root mean square of the residuals is  0.029 

The harmonic n.obs is  500 with the empirical chi square  11.259  with prob <  0.589 
The total n.obs was  500  with Likelihood Chi Square =  34.847  with prob <  0.000894 

Tucker Lewis Index of factoring reliability =  0.9721
RMSEA index =  0.0579  and the 90 % confidence intervals are  0.0352 0.0817
BIC =  -45.943
Fit based upon off diagonal values = 0.997
Measures of factor score adequacy             
                                                    ML1   ML2
Correlation of (regression) scores with factors   0.931 0.921
Multiple R square of scores with factors          0.866 0.849
Minimum correlation of possible factor scores     0.732 0.697
# ============================
# 5) Gráfico 2: Mapa de calor de cargas factoriales
# ============================
loadings <- as.data.frame(unclass(fa_fit$loadings))
loadings$Variable <- rownames(loadings)
library(ggplot2)
library(reshape2)
G3;
Attaching package: ‘reshape2’

gG3;The following object is masked from ‘package:tidyr’:

    smiths

g
L_long <- reshape2::melt(loadings, id.vars = "Variable",
                         variable.name = "Factor", value.name = "Carga")

ggplot(L_long, aes(x = Factor, y = Variable, fill = Carga)) +
  geom_tile(color = "white") +
  scale_fill_gradient2(low = "#2c7fb8", mid = "white", high = "#d7191c",
                       limits = c(-1, 1), name = "Carga") +
  labs(title = "Mapa de calor de cargas factoriales (rotación oblimin)",
       x = "Factor", y = "Variable") +
  theme_minimal(base_size = 12)


# ============================
# 6) Gráfico 3: Diagrama del modelo factorial
# ============================
psych::fa.diagram(fa_fit, main = "Diagrama del modelo: factores, cargas y errores")


# ============================
# 7) Gráfico 4: Dispersión de puntajes factoriales (Bartlett)
# ============================
# Estimación de puntajes sobre los datos individuales:
# NOTA: psych::fa() con r = cor_mat calcula sobre matriz de correlación.
# Para puntajes individuales, ejecutamos también sobre los datos:
fa_fit_data <- psych::fa(X, nfactors = 2, fm = "ml", rotate = "oblimin", scores = "Bartlett")
scores_df <- as.data.frame(fa_fit_data$scores)
colnames(scores_df) <- c("F1", "F2")

ggplot(scores_df, aes(x = F1, y = F2)) +
  geom_point(alpha = 0.5, color = "#2c7fb8") +
  stat_ellipse(level = 0.90, linetype = 2, color = "gray40") +
  labs(title = "Puntajes factoriales (método de Bartlett)", x = "Factor 1", y = "Factor 2") +
  theme_minimal(base_size = 12)


# ============================
# 8) Bonus: Biplot simple (variables como flechas en el plano F1–F2)
# ============================
# Usamos las cargas como vectores en el plano de los factores
L <- as.data.frame(unclass(fa_fit_data$loadings))
L$Variable <- rownames(L)

ggplot() +
  geom_point(data = scores_df, aes(F1, F2), alpha = 0.2, color = "gray60") +
  geom_segment(data = L, aes(x = 0, y = 0, xend = ML1, yend = ML2),
               arrow = arrow(length = unit(0.25, "cm")), color = "#d7191c", size = 0.8) +
  geom_text(data = L, aes(x = ML1, y = ML2, label = Variable),
            hjust = 0.5, vjust = -0.8, color = "#d7191c") +
  labs(title = "Biplot: puntajes vs. vectores de carga",
       x = "Factor 1", y = "Factor 2") +
  theme_minimal(base_size = 12)

6.7) Ejemplo con Python

reticulate::repl_python()
# ===========================
# Análisis Factorial en Python
# ===========================

# ---------- 1) Datos ----------
# Usamos el dataset "bfi" de personalidad (similar al de R psych::bfi)
bfi = fetch_openml("bfi", version=1, as_frame=True).frame
G3;Traceback (most recent call last):
  File "]8;line = 326;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py]8;;", line 326, in _get_data_info_by_name
    json_data = _get_json_content_from_openml_api(
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 251;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py]8;;", line 251, in _get_json_content_from_openml_api
    raise OpenMLError(error_message)
sklearn.datasets._openml.OpenMLError: None

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 213;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/utils/_param_validation.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/utils/_param_validation.py]8;;", line 213, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1008;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py]8;;", line 1008, in fetch_openml
    data_info = _get_data_info_by_name(
                ^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 340;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py]8;;", line 340, in _get_data_info_by_name
    json_data = _get_json_content_from_openml_api(
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 251;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/datasets/_openml.py]8;;", line 251, in _get_json_content_from_openml_api
    raise OpenMLError(error_message)
sklearn.datasets._openml.OpenMLError: Dataset bfi with version 1 not found.
g
df = bfi.iloc[:, :25]      # ítems A1:A5, C1:C5, E1:E5, N1:N5, O1:O5
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'bfi' is not defined
g
df = df.dropna()
n, p = df.shape
print(f"Observaciones: {n}, Variables: {p}")
Observaciones: 1000, Variables: 2
# Estandarizamos (factor_analyzer lo recomienda)
scaler = StandardScaler()
df_std = scaler.fit_transform(df)

# ---------- 2) Correlaciones, KMO y Bartlett ----------
corr = np.corrcoef(df_std, rowvar=False)

plt.figure(figsize=(10, 8))
sns.heatmap(corr, cmap="coolwarm", center=0)
<Figure size 1000x800 with 0 Axes>
<Axes: >
plt.title("Mapa de calor de correlaciones (agrupadas)")
Text(0.5, 1.0, 'Mapa de calor de correlaciones (agrupadas)')
plt.show()

chi2, p_value = calculate_bartlett_sphericity(df_std)
print("==> Bartlett test: chi2 =", chi2, "p =", p_value)
==> Bartlett test: chi2 = 933.9922356069778 p = 4.003756411010438e-205
kmo_all, kmo_model = calculate_kmo(df_std)
print("==> KMO general:", round(kmo_model, 3))
==> KMO general: 0.5
# ---------- 3) Número de factores: Scree + Análisis paralelo ----------
fa = FactorAnalyzer(rotation=None)
fa.fit(df_std)
FactorAnalyzer(rotation=None, rotation_kwargs={})
ev, v = fa.get_eigenvalues()

plt.figure(figsize=(8, 5))
<Figure size 800x500 with 0 Axes>
plt.scatter(range(1, df_std.shape[1] + 1), ev)
<matplotlib.collections.PathCollection object at 0x32fd8cf10>
plt.plot(range(1, df_std.shape[1] + 1), ev)
[<matplotlib.lines.Line2D object at 0x32fd39290>]
plt.title("Scree plot")
Text(0.5, 1.0, 'Scree plot')
plt.xlabel("Factor")
Text(0.5, 0, 'Factor')
plt.ylabel("Eigenvalue")
Text(0, 0.5, 'Eigenvalue')
plt.axhline(y=1, color="r", linestyle="--")
<matplotlib.lines.Line2D object at 0x32fd8e650>
plt.show()


# Aquí podrías usar análisis paralelo (con fa.parallel equivalente) → librería adicional

nfactors = 5   # ajusta según el gráfico o test paralelo
print(f"Número de factores seleccionado: {nfactors}")
Número de factores seleccionado: 5
# ---------- 4) Ajuste del modelo factorial ----------
fa = FactorAnalyzer(n_factors=nfactors, rotation="oblimin", method="ml")
fa.fit(df_std)
FactorAnalyzer(method='ml', n_factors=5, rotation='oblimin', rotation_kwargs={})
loadings = pd.DataFrame(fa.loadings_, index=df.columns)
print("\n==> Cargas factoriales (cutoff=0.30):\n", loadings.round(3))

==> Cargas factoriales (cutoff=0.30):
        0    1
X  0.833  0.0
Y  0.833  0.0
communalities = fa.get_communalities()
uniqueness = fa.get_uniquenesses()
print("\n==> Varianza explicada total:", sum(fa.get_factor_variance()[1]))

==> Varianza explicada total: 0.6938201806421798
print("\n==> Comunalidades y Unicidades:")

==> Comunalidades y Unicidades:
print(pd.DataFrame({"Comunalidad": communalities, "Unicidad": uniqueness}, index=df.columns))
   Comunalidad  Unicidad
X      0.69382   0.30618
Y      0.69382   0.30618
# ---------- 5) Heatmap de cargas factoriales ----------
plt.figure(figsize=(10, 12))
<Figure size 1000x1200 with 0 Axes>
sns.heatmap(loadings, annot=True, cmap="coolwarm", center=0, fmt=".2f")
<Axes: >
plt.title("Heatmap de cargas factoriales (oblimin)")
plt.show()
Text(0.5, 1.0, 'Heatmap de cargas factoriales (oblimin)')

# ---------- 6) Puntajes factoriales ----------
# Nota: factor_analyzer no da scores directos como en R,
# pero podemos calcularlos por regresión aproximada.
scores = pd.DataFrame(np.dot(df_std, fa.loadings_), columns=[f"Factor{i+1}" for i in range(nfactors)])
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 827;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py]8;;", line 827, in __init__
    mgr = ndarray_to_mgr(
          ^^^^^^^^^^^^^^^
  File "]8;line = 336;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/internals/construction.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/internals/construction.py]8;;", line 336, in ndarray_to_mgr
    _check_values_indices_shape_match(values, index, columns)
  File "]8;line = 420;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/internals/construction.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/internals/construction.py]8;;", line 420, in _check_values_indices_shape_match
    raise ValueError(f"Shape of passed values is {passed}, indices imply {implied}")
ValueError: Shape of passed values is (1000, 2), indices imply (1000, 5)
g
if scores.shape[1] >= 2:
    plt.figure(figsize=(7, 6))
    plt.scatter(scores.iloc[:, 0], scores.iloc[:, 1], alpha=0.3, color="#2C7FB8")
    plt.axhline(0, color="grey", linestyle="--")
    plt.axvline(0, color="grey", linestyle="--")
    plt.xlabel("Factor1")
    plt.ylabel("Factor2")
    plt.title("Dispersión de puntajes factoriales")
    plt.show()
    
# ---------- 7) Modelo factorial (esquema simplificado) ----------
# Nota: Python no trae un fa.diagram() como R.
# Aquí un diagrama sencillo de cargas > 0.3
<Figure size 700x600 with 0 Axes>
G3;Traceback (most recent call last):
  File "<string>", line 3, in <module>
AttributeError: 'numpy.ndarray' object has no attribute 'iloc'
g
import networkx as nx
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 122;file:///Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/reticulate/python/rpytools/loader.py/Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/reticulate/python/rpytools/loader.py]8;;", line 122, in _find_and_load_hook
    return _run_hook(name, _hook)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 96;file:///Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/reticulate/python/rpytools/loader.py/Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/reticulate/python/rpytools/loader.py]8;;", line 96, in _run_hook
    module = hook()
             ^^^^^^
  File "]8;line = 120;file:///Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/reticulate/python/rpytools/loader.py/Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/reticulate/python/rpytools/loader.py]8;;", line 120, in _hook
    return _find_and_load(name, import_)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'networkx'
g
G = nx.Graph()
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'nx' is not defined
g
threshold = 0.3
for var, row in loadings.iterrows():
    for f, val in row.items():
        if abs(val) >= threshold:
            G.add_edge(var, f, weight=abs(val))
            
pos = nx.spring_layout(G, seed=123)
G3;Traceback (most recent call last):
  File "<string>", line 4, in <module>
NameError: name 'G' is not defined
gG3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'nx' is not defined
g
plt.figure(figsize=(12, 10))
nx.draw(G, pos, with_labels=True, node_size=1500, node_color="lightblue", font_size=8, width=1)
<Figure size 1200x1000 with 0 Axes>
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'nx' is not defined
g
plt.title("Diagrama simplificado del modelo factorial (AFE)")
Text(0.5, 1.0, 'Diagrama simplificado del modelo factorial (AFE)')
plt.show()

NA
NA

7) Escalamiento multidimensional

El escalado multidimensional (MDS, por sus siglas en inglés) es una técnica estadística que busca representar objetos o individuos en un espacio de baja dimensión (generalmente 2D o 3D) de tal manera que las distancias entre ellos en ese espacio reducido reflejen lo mejor posible sus distancias originales en un espacio de alta dimensión. Su objetivo es traducir una matriz de disimilaridades \(D = (d_{ij})\) entre pares de elementos en una configuración geométrica de puntos \(x_i \in \mathbb{R}^k\) que conserve esas relaciones.

Formalmente, el problema consiste en encontrar coordenadas \(x_1, x_2, \dots, x_n\) en \(\mathbb{R}^k\) que minimicen una función de pérdida llamada estrés (stress), definida como:

\[ \text{Stress}(X) = \sqrt{\frac{\sum_{i<j} \left( d_{ij} - \delta_{ij}(X) \right)^2}{\sum_{i<j} d_{ij}^2}} \]

donde \(d_{ij}\) es la disimilaridad original entre los elementos \(i\) y \(j\), y \(\delta_{ij}(X)\) es la distancia euclidiana entre \(x_i\) y \(x_j\) en el espacio reducido.

El MDS sirve para visualizar relaciones complejas entre variables o individuos, detectando estructuras latentes de proximidad o agrupamiento. A diferencia del análisis de componentes principales (PCA), que se basa en la varianza y la estructura lineal de los datos, el MDS puede trabajar directamente con matrices de distancias o disimilaridades, lo que lo hace útil en contextos donde no se tienen variables métricas tradicionales.

Entre los casos de uso más frecuentes están: en psicología, representar percepciones de similitud entre estímulos (colores, sonidos, palabras); en marketing, mapear cómo los consumidores perciben la cercanía entre marcas o productos; en biología, visualizar distancias genéticas entre especies; y en ciencias sociales, analizar redes de afinidad o de cooperación entre individuos. En todos estos casos, el MDS permite convertir relaciones abstractas de semejanza en representaciones geométricas claras y fáciles de interpretar.

Ejemplo con R

quit
# ===============================
# Ejemplo básico de MDS en R
# ===============================

# 1. Distancias (en km) entre algunas ciudades
ciudades <- c("Roma", "París", "Berlín", "Madrid", "Londres")
distancias <- matrix(c(
  0, 1105, 1181, 1365, 1435,
  1105, 0, 1050, 1276, 343,
  1181, 1050, 0, 1870, 930,
  1365, 1276, 1870, 0, 1260,
  1435, 343, 930, 1260, 0
), nrow = 5, byrow = TRUE)

rownames(distancias) <- ciudades
colnames(distancias) <- ciudades

# 2. Aplicamos Escalado Multidimensional (MDS clásico)
mds <- cmdscale(as.dist(distancias), k = 2)

# 3. Graficamos
plot(mds, type = "n", main = "Escalado Multidimensional (MDS)")
text(mds, labels = ciudades, cex = 1.2, col = "blue")

En este ejemplo, el escalado multidimensional (MDS) tomó la matriz de distancias reales entre las cinco ciudades y las proyectó en un plano bidimensional, de manera que las posiciones relativas de los puntos reflejan lo mejor posible esas distancias originales: se observa que Madrid queda separado hacia la izquierda porque está más lejos del resto, París y Londres aparecen cercanos indicando su proximidad geográfica, Berlín se sitúa a la derecha representando su lejanía respecto a Madrid y Roma, mientras que Roma se ubica en la parte superior mostrando su distancia intermedia con los demás; en conjunto, la gráfica permite interpretar rápidamente la similitud o separación relativa entre ciudades sin necesidad de consultar directamente la tabla de distancias.

7.1) Escalados métricos: coordenas principales

El escalado métrico por coordenadas principales (Classical MDS o PCoA) es un método que busca representar objetos descritos por una matriz de distancias \(\mathbf{D} = (d_{ij})\) en un espacio de menor dimensión, típicamente \(\mathbb{R}^2\) o \(\mathbb{R}^3\), preservando lo mejor posible esas distancias. A diferencia del MDS general que optimiza una función de estrés, el enfoque de coordenadas principales tiene una solución analítica basada en álgebra lineal, lo que lo hace más directo y eficiente.

El procedimiento comienza convirtiendo la matriz de distancias \(\mathbf{D}\) en una matriz de producto interno centrada \(\mathbf{B}\) mediante doble centrado:

\[\mathbf{B} = -\tfrac{1}{2} \, \mathbf{J} \, \mathbf{D}^2 \, \mathbf{J} \]

donde \(\mathbf{J} = \mathbf{I} - \tfrac{1}{n}\mathbf{1}\mathbf{1}^\top\) es la matriz de centrado, \(\mathbf{I}\) la identidad y \(\mathbf{1}\) el vector de unos. Luego se realiza una descomposición espectral de \(\mathbf{B}\), obteniendo autovalores \(\lambda_1, \lambda_2, \dots, \lambda_p\) y autovectores correspondientes.

Las coordenadas principales de los objetos en el espacio reducido de dimensión \(k\) se calculan como:

\[ \mathbf{X}_k = \mathbf{V}_k \, \Lambda_k^{1/2}\]

donde \(\mathbf{V}_k\) contiene los \(k\) autovectores principales de \(\mathbf{B}\) y \(\Lambda_k^{1/2}\) es la matriz diagonal con las raíces cuadradas de los \(k\) autovalores positivos. Estas coordenadas son las representaciones geométricas buscadas que preservan lo mejor posible las distancias originales.

Los casos de uso del escalado por coordenadas principales son amplios: en ecología, para representar comunidades biológicas a partir de medidas de disimilitud entre especies; en genética, para visualizar distancias entre poblaciones; en psicología y ciencias sociales, para mapear percepciones de similitud entre estímulos; y en mercadeo, para construir mapas perceptuales de marcas o productos. Su ventaja principal es que ofrece una solución algebraica exacta cuando las distancias provienen de un espacio euclidiano, facilitando una interpretación clara de las relaciones entre objetos.



# Usamos el dataset iris
data(iris)

# Calculamos matriz de distancias euclidianas sobre las variables numéricas
dist_iris <- dist(iris[, 1:4])

# Escalado Métrico (PCoA) en 2 dimensiones
pcoa <- cmdscale(dist_iris, k = 2, eig = TRUE)

# Extraemos coordenadas
coords <- as.data.frame(pcoa$points)
colnames(coords) <- c("Dim1", "Dim2")
coords$Species <- iris$Species

# Gráfico con ggplot2
ggplot(coords, aes(x = Dim1, y = Dim2, color = Species)) +
  geom_point(size = 3, alpha = 0.7) +
  stat_ellipse(level = 0.95, linetype = 2) +
  theme_minimal(base_size = 14) +
  labs(title = "Escalado Métrico (PCoA) en iris",
       x = "Coordenada Principal 1",
       y = "Coordenada Principal 2") +
  scale_color_brewer(palette = "Dark2")

7.2) Construcción de las coordenadas principales

La construcción de las coordenadas principales es el procedimiento matemático que permite obtener una representación en un espacio euclidiano de baja dimensión a partir de una matriz de distancias \(\mathbf{D}\). La idea central es transformar estas distancias en un conjunto de coordenadas ortogonales que preserven, lo mejor posible, la estructura geométrica de los datos.

El proceso comienza con el doble centrado de la matriz de distancias cuadradas. A partir de \(\mathbf{D}^2 = (d_{ij}^2)\) se obtiene una matriz de producto interno \(\mathbf{B}\) mediante

\[\mathbf{B} = -\tfrac{1}{2} \, \mathbf{J} \, \mathbf{D}^2 \, \mathbf{J} \] donde \(\mathbf{J} = \mathbf{I} - \tfrac{1}{n}\mathbf{11}^\top\) es la matriz de centrado, \(\mathbf{I}\) la matriz identidad y \(\mathbf{1}\) el vector de unos. Esta transformación convierte las distancias en un objeto apto para análisis espectral.

Luego, se realiza la descomposición espectral de \(\mathbf{B}\):

\[\mathbf{B} = \mathbf{V}\,\mathbf{\Lambda}\,\mathbf{V}^\top \]

donde \(\mathbf{\Lambda}\) contiene los autovalores \(\lambda_1 \geq \lambda_2 \geq \dots \geq \lambda_n\) y \(\mathbf{V}\) los autovectores correspondientes. Las coordenadas principales se construyen tomando los \(k\) autovalores positivos más grandes y sus autovectores asociados.

Finalmente, las coordenadas principales de los objetos son:

\[ \mathbf{X} = \mathbf{V}_k \,\mathbf{\Lambda}_k^{1/2}\]

donde \(\mathbf{V}_k\) son los autovectores seleccionados y \(\mathbf{\Lambda}_k^{1/2}\) es la raíz cuadrada de los autovalores. Este método se utiliza ampliamente en ecología (análisis de comunidades), genética (estructuras poblacionales), y ciencias sociales (percepciones, similitudes), porque permite visualizar datos complejos de distancias en un espacio reducido e interpretable.

Medida de precisión

La medida de precisión es un indicador estadístico que evalúa qué tan cercanos están los valores observados o estimados respecto al valor verdadero o entre sí mismos, reflejando el grado de consistencia en los resultados; matemáticamente puede expresarse como la inversa de la dispersión de los datos, por ejemplo a través de la varianza \(\sigma^2\) o la desviación estándar \(\sigma\), donde valores bajos indican mayor precisión, lo cual es fundamental en áreas como la estadística, el aprendizaje automático y la investigación experimental, ya que permite determinar la confiabilidad de un método de medición o de un modelo predictivo.

Ejemplo en R



# Creamos un dataset sencillo (4 puntos en 2D)
set.seed(123)
data <- matrix(c(1, 1,
                 5, 2,
                 2, 4,
                 6, 5), 
               ncol = 2, byrow = TRUE)

rownames(data) <- c("A", "B", "C", "D")

# 1. Matriz de distancias euclidianas
D <- dist(data)

# 2. Construcción de la matriz B = -1/2 J D^2 J
n <- attr(D, "Size")
D2 <- as.matrix(D)^2
J <- diag(n) - (1/n) * matrix(1, n, n)
B <- -0.5 * J %*% D2 %*% J

# 3. Descomposición espectral de B
eig <- eigen(B)
Lambda <- diag(eig$values)
V <- eig$vectors

# 4. Coordenadas principales con los 2 mayores autovalores
k <- 2
X <- V[, 1:k] %*% sqrt(Lambda[1:k, 1:k])
coords <- as.data.frame(X)
coords$label <- rownames(data)

# 5. Graficamos
ggplot(coords, aes(x = V1, y = V2, label = label)) +
  geom_point(size = 4, color = "steelblue") +
  geom_text(vjust = -1, size = 5) +
  theme_minimal(base_size = 14) +
  labs(title = "Construcción de Coordenadas Principales (PCoA)",
       x = "Coordenada Principal 1",
       y = "Coordenada Principal 2")

7.3) Escalado no métrico

El escalado no métrico es una técnica de reducción de dimensionalidad dentro del análisis de escalamiento multidimensional (MDS) que busca representar objetos en un espacio de baja dimensión preservando principalmente el orden de las distancias o similitudes entre ellos, más que sus valores exactos. A diferencia del escalado métrico, que intenta aproximar directamente las distancias originales \(d_{ij}\), el no métrico se enfoca en mantener la correspondencia entre las relaciones ordinales (qué tan cercanos o lejanos son los objetos entre sí).

El procedimiento se basa en encontrar una configuración de puntos en un espacio de dimensión reducida donde las distancias euclidianas \(d_{ij}^*\) preserven el orden de las disimilaridades \(\delta_{ij}\). La calidad del ajuste se evalúa a través de la función de estrés, usualmente definida como:

\[\text{Stress} = \sqrt{\frac{\sum_{i<j} \left( f(\delta_{ij}) - d_{ij}^* \right)^2}{\sum_{i<j} d_{ij}^{*2}}} \] donde \(f(\delta_{ij})\) es una función monótona que transforma las disimilaridades originales para ajustarlas al espacio proyectado.

Este enfoque resulta útil cuando los datos provienen de mediciones ordinales o cualitativas, en las cuales no se puede asumir que las distancias numéricas sean significativas, pero sí importa el orden de cercanía entre los objetos. Ejemplos típicos son encuestas de percepción, juicios de preferencia, datos psicológicos o rankings.

En la práctica, el escalado no métrico se aplica para visualizar relaciones subyacentes en datos complejos, como la proximidad entre marcas en estudios de mercado, la similitud en patrones de comportamiento de usuarios, o la organización perceptual de estímulos en psicología; en estos casos, lo relevante es conservar la estructura relacional u ordinal más que la magnitud exacta de las distancias.

Ejemplo en R


# Datos de vegetación de ejemplo
data(dune)   # dataset disponible en vegan

# Calcular matriz de disimilitudes (distancia de Bray-Curtis)
dune.dist <- vegdist(dune, method = "bray")

# Aplicar NMDS con 2 dimensiones
set.seed(123) # Para reproducibilidad
nmds <- metaMDS(dune.dist, k = 2, trymax = 100)
Run 0 stress 0.1192678 
Run 1 stress 0.1192679 
... Procrustes: rmse 0.0001940161  max resid 0.0005952259 
... Similar to previous best
Run 2 stress 0.1889642 
Run 3 stress 0.1183186 
... New best solution
... Procrustes: rmse 0.02027001  max resid 0.06496047 
Run 4 stress 0.1183186 
... New best solution
... Procrustes: rmse 0.000001236482  max resid 0.000004281732 
... Similar to previous best
Run 5 stress 0.1183186 
... Procrustes: rmse 0.000004179438  max resid 0.00001118535 
... Similar to previous best
Run 6 stress 0.2252534 
Run 7 stress 0.1183186 
... Procrustes: rmse 0.000002378251  max resid 0.000007589179 
... Similar to previous best
Run 8 stress 0.1183186 
... Procrustes: rmse 0.000002196168  max resid 0.000005819283 
... Similar to previous best
Run 9 stress 0.1183186 
... Procrustes: rmse 0.000005112697  max resid 0.00001537431 
... Similar to previous best
Run 10 stress 0.1192679 
Run 11 stress 0.1192678 
Run 12 stress 0.1183186 
... Procrustes: rmse 0.000003656436  max resid 0.00001291508 
... Similar to previous best
Run 13 stress 0.1809578 
Run 14 stress 0.1183186 
... Procrustes: rmse 0.000006681822  max resid 0.0000201533 
... Similar to previous best
Run 15 stress 0.1183186 
... Procrustes: rmse 0.00001051499  max resid 0.00003430307 
... Similar to previous best
Run 16 stress 0.1183186 
... New best solution
... Procrustes: rmse 0.0000008560325  max resid 0.000002056929 
... Similar to previous best
Run 17 stress 0.1183186 
... Procrustes: rmse 0.000007109277  max resid 0.00002108387 
... Similar to previous best
Run 18 stress 0.1183186 
... New best solution
... Procrustes: rmse 0.000001270686  max resid 0.000002766554 
... Similar to previous best
Run 19 stress 0.1183186 
... Procrustes: rmse 0.0000185466  max resid 0.00005750478 
... Similar to previous best
Run 20 stress 0.1183186 
... Procrustes: rmse 0.000002621703  max resid 0.000009170702 
... Similar to previous best
*** Best solution repeated 3 times
# Mostrar resultados
print(nmds)

Call:
metaMDS(comm = dune.dist, k = 2, trymax = 100) 

global Multidimensional Scaling using monoMDS

Data:     dune.dist 
Distance: bray 

Dimensions: 2 
Stress:     0.1183186 
Stress type 1, weak ties
Best solution was repeated 3 times in 20 tries
The best solution was from try 18 (random start)
Scaling: centring, PC rotation, halfchange scaling 
Species: scores missing
# Graficar el resultado
plot(nmds, type = "t", main = "Escalado No Métrico (NMDS) - Ejemplo gráfico")
G3;species scores not available
g

7.4) Ejemplo Python

7.4.1) Escalamiento Multidimensional (MDS clásico)

reticulate::repl_python()


# ===============================
# Ejemplo básico de MDS en Python
# ===============================

# 1. Distancias (en km) entre algunas ciudades
ciudades = ["Roma", "París", "Berlín", "Madrid", "Londres"]
distancias = np.array([
    [0, 1105, 1181, 1365, 1435],
    [1105, 0, 1050, 1276, 343],
    [1181, 1050, 0, 1870, 930],
    [1365, 1276, 1870, 0, 1260],
    [1435, 343, 930, 1260, 0]
])

# 2. Aplicamos MDS clásico (2 dimensiones)
mds = MDS(n_components=2, dissimilarity="precomputed", random_state=123)
coords = mds.fit_transform(distancias)

# 3. Graficamos
plt.figure(figsize=(8,6))
<Figure size 800x600 with 0 Axes>
plt.scatter(coords[:,0], coords[:,1], color="blue")
<matplotlib.collections.PathCollection object at 0x32fe00250>
for i, ciudad in enumerate(ciudades):
    plt.text(coords[i,0]+10, coords[i,1]+10, ciudad, fontsize=12, color="darkblue")
plt.title("Escalado Multidimensional (MDS)", fontsize=14)
Text(-813.0723354736006, -94.61624572942233, 'Roma')
Text(321.80844083542553, 12.212582151519454, 'París')
Text(54.81727898928769, -876.4752777221095, 'Berlín')
Text(-116.70083262891129, 1064.0672641016415, 'Madrid')
Text(603.1474482777984, -55.18832280162904, 'Londres')
Text(0.5, 1.0, 'Escalado Multidimensional (MDS)')
plt.xlabel("Dim 1")
Text(0.5, 0, 'Dim 1')
plt.ylabel("Dim 2")
Text(0, 0.5, 'Dim 2')
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()

7.4.2) Escalados métricos: coordenadas principales (PCoA)


# Dataset iris

iris = load_iris()
X = iris.data
y = iris.target
labels = iris.target_names

# 1. Matriz de distancias euclidianas
dist_iris = pairwise_distances(X, metric="euclidean")
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'pairwise_distances' is not defined
g
# 2. Escalado Métrico (PCoA) ~ se implementa con MDS "precomputed"
pcoa = MDS(n_components=2, dissimilarity="precomputed", random_state=42)
coords = pcoa.fit_transform(dist_iris)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'dist_iris' is not defined
g
# Convertimos a DataFrame
df_coords = pd.DataFrame(coords, columns=["Dim1", "Dim2"])
df_coords["Species"] = [labels[i] for i in y]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 4316;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py]8;;", line 4316, in __setitem__
    self._set_item(key, value)
  File "]8;line = 4529;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py]8;;", line 4529, in _set_item
    value, refs = self._sanitize_column(value)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 5273;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/frame.py]8;;", line 5273, in _sanitize_column
    com.require_length_match(value, self.index)
  File "]8;line = 573;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/common.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/pandas/core/common.py]8;;", line 573, in require_length_match
    raise ValueError(
ValueError: Length of values (150) does not match length of index (5)
g
# 3. Graficamos
plt.figure(figsize=(8,6))
<Figure size 800x600 with 0 Axes>
sns.scatterplot(data=df_coords, x="Dim1", y="Dim2", hue="Species", s=60, alpha=0.8)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 615;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/relational.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/relational.py]8;;", line 615, in scatterplot
    p = _ScatterPlotter(
        ^^^^^^^^^^^^^^^^
  File "]8;line = 396;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/relational.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/relational.py]8;;", line 396, in __init__
    super().__init__(data=data, variables=variables)
  File "]8;line = 634;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_base.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_base.py]8;;", line 634, in __init__
    self.assign_variables(data, variables)
  File "]8;line = 679;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_base.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_base.py]8;;", line 679, in assign_variables
    plot_data = PlotData(data, variables)
                ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 58;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_core/data.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_core/data.py]8;;", line 58, in __init__
    frame, names, ids = self._assign_variables(data, variables)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 232;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_core/data.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/seaborn/_core/data.py]8;;", line 232, in _assign_variables
    raise ValueError(err)
ValueError: Could not interpret value `Species` for `hue`. An entry with this name does not appear in `data`.
g
plt.title("Escalado Métrico (PCoA) en iris", fontsize=14)
Text(0.5, 1.0, 'Escalado Métrico (PCoA) en iris')
plt.xlabel("Coordenada Principal 1")
Text(0.5, 0, 'Coordenada Principal 1')
plt.ylabel("Coordenada Principal 2")
Text(0, 0.5, 'Coordenada Principal 2')
plt.show()

7.4.3) Construcción de las coordenadas principales

# Ejemplo sencillo con 4 puntos en 2D
data = np.array([
    [1, 1],
    [5, 2],
    [2, 4],
    [6, 5]
])
labels = ["A", "B", "C", "D"]

# 1. Matriz de distancias
D = pairwise_distances(data, metric="euclidean")
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'pairwise_distances' is not defined
g
D2 = D**2
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'D' is not defined
g
n = D.shape[0]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'D' is not defined
g
# 2. Matriz de centrado J
J = np.eye(n) - np.ones((n,n))/n

# 3. Matriz B
B = -0.5 * J.dot(D2).dot(J)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'D2' is not defined
g
# 4. Descomposición espectral
eigvals, eigvecs = np.linalg.eigh(B)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 1475;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/linalg/linalg.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/linalg/linalg.py]8;;", line 1475, in eigh
    _assert_stacked_2d(a)
  File "]8;line = 206;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/linalg/linalg.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/linalg/linalg.py]8;;", line 206, in _assert_stacked_2d
    raise LinAlgError('%d-dimensional array given. Array must be '
numpy.linalg.LinAlgError: 1-dimensional array given. Array must be at least two-dimensional
g
# Ordenamos de mayor a menor
idx = np.argsort(eigvals)[::-1]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'eigvals' is not defined
g
eigvals = eigvals[idx]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'eigvals' is not defined
g
eigvecs = eigvecs[:,idx]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'eigvecs' is not defined
g
# 5. Coordenadas principales (2D)
k = 2
L = np.diag(np.sqrt(eigvals[:k]))
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'eigvals' is not defined
g
V = eigvecs[:, :k]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'eigvecs' is not defined
g
X_coords = V.dot(L)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'V' is not defined
g
# 6. Graficamos
plt.figure(figsize=(6,6))
<Figure size 600x600 with 0 Axes>
plt.scatter(X_coords[:,0], X_coords[:,1], color="steelblue", s=80)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'X_coords' is not defined
g
for i, label in enumerate(labels):
    plt.text(X_coords[i,0]+0.05, X_coords[i,1]+0.05, label, fontsize=12)
plt.title("Construcción de Coordenadas Principales (PCoA)")
G3;Traceback (most recent call last):
  File "<string>", line 2, in <module>
NameError: name 'X_coords' is not defined
g
Text(0.5, 1.0, 'Construcción de Coordenadas Principales (PCoA)')
plt.xlabel("Coord. Principal 1")
Text(0.5, 0, 'Coord. Principal 1')
plt.ylabel("Coord. Principal 2")
Text(0, 0.5, 'Coord. Principal 2')
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()

7.4.4) Escalado No Métrico (NMDS)

# Escalado No Métrico con sklearn (metric=False)
nmds = MDS(n_components=2, dissimilarity="precomputed", metric=False, random_state=123, n_init=10, max_iter=1000)
coords_nmds = nmds.fit_transform(dist_iris)
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'dist_iris' is not defined
g
# DataFrame para graficar
df_nmds = pd.DataFrame(coords_nmds, columns=["Dim1", "Dim2"])
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'coords_nmds' is not defined
g
df_nmds["Species"] = [labels[i] for i in y]
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'df_nmds' is not defined
g
# Graficamos
plt.figure(figsize=(8,6))
sns.scatterplot(data=df_nmds, x="Dim1", y="Dim2", hue="Species", s=60, alpha=0.8)
<Figure size 800x600 with 0 Axes>
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'df_nmds' is not defined
g
plt.title("Escalado No Métrico (NMDS) en iris", fontsize=14)
plt.xlabel("NMDS 1")
Text(0.5, 1.0, 'Escalado No Métrico (NMDS) en iris')
plt.ylabel("NMDS 2")
Text(0.5, 0, 'NMDS 1')
plt.show()
Text(0, 0.5, 'NMDS 2')

8) Clusterización

El análisis de clúster o conglomerados es una técnica de estadística multivariante que busca agrupar objetos o individuos en función de su similitud, de modo que los elementos dentro de un mismo grupo sean más parecidos entre sí que con los de otros grupos. A diferencia de métodos supervisados, no requiere variables dependientes predefinidas, sino que trabaja únicamente con medidas de similitud o disimilitud, comúnmente basadas en distancias como la euclidiana \(d_{ij} = \sqrt{\sum_{k=1}^p (x_{ik} - x_{jk})^2}\) entre dos observaciones \(i\) y \(j\) en un espacio de \(p\) dimensiones.

Este análisis sirve para descubrir estructuras ocultas en los datos y facilitar la interpretación de patrones. Existen diferentes enfoques, entre ellos los métodos jerárquicos, que construyen una estructura en forma de árbol o dendrograma, y los métodos no jerárquicos, como \(k\)-means, que dividen los datos en un número fijo de grupos minimizando una función de variación interna, por ejemplo:

\[J = \sum_{i=1}^k \sum_{x \in C_i} \| x - \mu_i \|^2\]

donde \(C_i\) es el clúster \(i\) y \(\mu_i\) es su centroide

Los casos de uso del análisis de clúster son amplios: en marketing permite segmentar clientes según hábitos de consumo, en biología se utiliza para clasificar especies o analizar perfiles genéticos, en economía se pueden identificar grupos de países con estructuras económicas similares, y en seguridad ayuda a detectar patrones de comportamiento anómalo. En todos estos casos, el objetivo es simplificar la complejidad de los datos para facilitar la toma de decisiones.

En resumen, el análisis de conglomerados es una herramienta exploratoria esencial cuando se quiere pasar de una colección de datos sin estructura aparente a agrupaciones interpretables, revelando relaciones y tendencias que no serían visibles a simple vista.

8.1) K-medias

El método de \(k\)-medias es una técnica de análisis de conglomerados que busca particionar un conjunto de observaciones en \(k\) grupos o clústeres de tal manera que los elementos dentro de cada grupo sean lo más similares posible entre sí y lo más diferentes posible respecto a los de otros grupos. Cada clúster se representa a través de su centroide, es decir, la media de las observaciones asignadas a ese grupo.

El objetivo del algoritmo es minimizar la suma de las distancias cuadradas entre cada punto \(x\) y el centroide \(\mu\) de su clúster. Matemáticamente, se expresa como:

donde \(C_i\) representa el \(i\)-ésimo clúster y \(\mu_i\) su centroide.

Este método se utiliza para segmentación de mercados, clasificación de clientes, reducción de datos y agrupamiento en bioinformática, entre otros. Su fortaleza es la simplicidad y la rapidez, aunque presenta limitaciones como la sensibilidad a valores atípicos y a la elección inicial de los centroides.

Un caso de uso común es en empresas que buscan segmentar clientes según hábitos de compra: a través de \(k\)-medias, se pueden identificar grupos de consumidores con patrones similares de gasto, lo que permite diseñar estrategias de marketing diferenciadas y personalizadas para cada segmento.

Ejemplo con R

quit
# Ejemplo gráfico de K-medias en R

set.seed(123)  # Reproducibilidad

# 1. Generar datos simulados (3 grupos en 2D)
x <- rbind(
  matrix(rnorm(100, mean = 0, sd = 0.3), ncol = 2),
  matrix(rnorm(100, mean = 2, sd = 0.3), ncol = 2),
  matrix(rnorm(100, mean = -2, sd = 0.3), ncol = 2)
)

# 2. Aplicar K-medias con k = 3
kmeans_res <- kmeans(x, centers = 3, nstart = 20)

# 3. Graficar los resultados
plot(x, col = kmeans_res$cluster, pch = 19,
     main = "Ejemplo gráfico de K-medias",
     xlab = "X1", ylab = "X2")
points(kmeans_res$centers, col = 1:3, pch = 8, cex = 3, lwd = 2) # centroides

8.2) K-medoides: algoritmo pam

El método de \(k\)-medoides, también conocido como algoritmo PAM (Partitioning Around Medoids), es una técnica de análisis de conglomerados similar a \(k\)-medias, pero en lugar de usar centroides (promedios), selecciona como representativo de cada grupo un medoide, es decir, el objeto real del conjunto de datos más central dentro del clúster. Esto lo hace más robusto frente a valores atípicos, ya que los medoides no se ven tan afectados por extremos como las medias.

El objetivo del algoritmo PAM es minimizar la suma de distancias entre los elementos de cada clúster y su respectivo medoide. Formalmente, si tenemos un conjunto de datos \(X = {x_1, x_2, \dots, x_n}\) y un conjunto de medoides \(M = {m_1, m_2, \dots, m_k}\), la función a minimizar es:

\[\sum_{i=1}^{k} \sum_{x \in C_i} d(x, m_i)\]

donde \(d(x, m_i)\) representa la distancia (por ejemplo, euclidiana o Manhattan) entre un punto \(x\) y su medoide \(m_i\), y \(C_i\) es el conjunto de puntos asignados al medoide \(m_i\).

El algoritmo PAM funciona en dos fases: (1) Selección inicial de \(k\) medoides que representan a los datos, y (2) iteración de intercambio, donde se evalúa si sustituir un medoide por otro punto mejora la función objetivo. El proceso continúa hasta que no se logra una mejora.

En cuanto a sus casos de uso, PAM es útil cuando se trabaja con datos que contienen valores atípicos, cuando las distancias no son necesariamente euclidianas o cuando se requiere un representante real de los datos (como en bioinformática, minería de textos o segmentación de clientes). A diferencia de \(k\)-medias, PAM puede trabajar con cualquier matriz de disimilaridad y no se restringe al espacio vectorial tradicional.

Ejemplo en R



# Generar datos simulados con 3 grupos
set.seed(123)
datos <- rbind(
  matrix(rnorm(100, mean = 0, sd = 0.5), ncol = 2),
  matrix(rnorm(100, mean = 3, sd = 0.5), ncol = 2),
  matrix(rnorm(100, mean = 6, sd = 0.5), ncol = 2)
)
colnames(datos) <- c("X1", "X2")

# Aplicar PAM con 3 clústeres
pam_res <- pam(datos, k = 3)

# Visualizar los clústeres
fviz_cluster(pam_res, geom = "point", ellipse.type = "norm",
             palette = "jco", repel = TRUE, ggtheme = theme_minimal()) +
  ggtitle("Clustering con K-medoides (PAM)")

8.3) Método jerárquico

El método jerárquico es una técnica de análisis de conglomerados que organiza los datos en una estructura en forma de árbol llamada dendrograma, donde las observaciones se van agrupando de manera progresiva. A diferencia de métodos como \(k\)-medias, no es necesario fijar previamente el número de clústeres, sino que este se puede decidir observando el nivel de corte del dendrograma.

Existen dos enfoques principales: el aglomerativo (bottom-up), donde cada observación inicia como un clúster independiente y se van fusionando según su similitud, y el divisivo (top-down), donde se parte de un solo grupo y se divide recursivamente. El criterio de similitud se basa en una medida de distancia, como la distancia euclidiana \(d_{ij} = \sqrt{\sum_{k=1}^{p}(x_{ik} - x_{jk})^2}\) entre dos puntos \(i\) y \(j\).

El paso crucial es definir cómo medir la distancia entre grupos. Por ejemplo, en el enlace sencillo se toma la mínima distancia entre pares de observaciones, mientras que en el enlace completo se toma la máxima. En el promedio de grupo se usa la media de todas las distancias, y en el método de Ward se minimiza la varianza interna de los clústeres, buscando minimizar la suma de cuadrados dentro de los grupos (\(SS_{W}\)).

Este método se utiliza ampliamente en biología (filogenia), ciencias sociales y marketing, para descubrir jerarquías naturales en los datos, analizar similitudes progresivas y representar visualmente la estructura de los grupos. Su ventaja es que ofrece una visión más flexible y detallada de cómo se relacionan las observaciones en diferentes niveles de agrupación.

Tipos de modelos

El clustering jerárquico aglomerativo (HAC) es el más utilizado y sigue un enfoque “de abajo hacia arriba” (\(bottom\text{-}up\)). Comienza considerando cada observación como un clúster individual, es decir, si hay \(n\) observaciones, se parte con \(n\) grupos. Luego, en cada paso, se fusionan los dos clústeres más similares según una medida de distancia (por ejemplo, distancia euclidiana) y un criterio de enlace (\(single\), \(complete\), \(average\), \(ward\), etc.). El proceso continúa hasta que todos los puntos quedan en un único clúster.

Por otro lado, el clustering jerárquico divisivo (DIANA en R) utiliza un enfoque “de arriba hacia abajo” (\(top\text{-}down\)). Parte con todas las observaciones en un único clúster y, en cada paso, divide el grupo más heterogéneo en dos subgrupos según una medida de disimilitud. El proceso continúa de forma recursiva hasta que cada observación está en su propio clúster, o hasta llegar a un número de clústeres definido.

La diferencia esencial es entonces la dirección del agrupamiento: el aglomerativo fusiona clústeres pequeños hasta formar grupos más grandes, mientras que el divisivo separa clústeres grandes en grupos más pequeños. El primero suele ser más eficiente en la práctica, ya que manejar divisiones sucesivas requiere más cómputo.

En cuanto a casos de uso, el método aglomerativo es preferido cuando se quiere entender cómo se forman relaciones entre observaciones individuales (ejemplo: análisis de especies, segmentación de clientes). El divisivo puede ser útil cuando se sospecha que existe una estructura grande bien definida que conviene subdividir (ejemplo: partir un mercado completo en nichos más pequeños). Ambos producen un dendrograma, pero la forma de construcción es distinta.

Dendograma

Un dendrograma es una representación gráfica en forma de árbol que se utiliza para mostrar los resultados de un análisis de clúster jerárquico. En este gráfico, cada observación (punto, individuo, variable) aparece como una “hoja” en la base del árbol, y las uniones o ramas representan los pasos de agrupamiento o división que se realizaron durante el proceso jerárquico.

En el método aglomerativo, el dendrograma se construye de abajo hacia arriba: al inicio, cada observación está separada, y a medida que subimos en el gráfico, vemos cómo se van uniendo en grupos más grandes. En cambio, en el método divisivo, se lee de arriba hacia abajo: comenzamos con un único grupo y el árbol va mostrando las divisiones sucesivas en ramas más pequeñas.

El eje vertical del dendrograma representa la distancia o la diferencia entre los clústeres unidos (\(d_{ij}\)). Cuanto más alto se produce la unión de dos ramas, más diferentes eran esos clústeres. El eje horizontal muestra las observaciones o grupos resultantes, sin un orden especial más allá de la visualización.

En la práctica, el dendrograma permite al analista decidir el número óptimo de clústeres: se puede “cortar” el árbol en un nivel de distancia determinado (trazando una línea horizontal) y ver cuántos grupos se forman. Por eso es una herramienta clave en la interpretación del análisis jerárquico, pues combina la lógica matemática con una visualización intuitiva.

Ejemplo en R

# Ejemplo muy gráfico de Clustering Jerárquico en R

# 1. Generamos datos de ejemplo (dos clústeres claros)
set.seed(123)
grupo1 <- matrix(rnorm(50, mean = 0), ncol = 2)
grupo2 <- matrix(rnorm(50, mean = 5), ncol = 2)
datos <- rbind(grupo1, grupo2)
colnames(datos) <- c("X", "Y")

# 2. Calcular distancias euclidianas
distancias <- dist(datos, method = "euclidean")

# 3. Aplicar clustering jerárquico con método de Ward
hc <- hclust(distancias, method = "ward.D2")

# 4. Graficar el dendrograma
plot(hc, labels = FALSE, main = "Dendrograma - Método Jerárquico (Ward)",
     xlab = "", sub = "", cex = 0.8)

# 5. Resaltar 2 clústeres con rectángulos
rect.hclust(hc, k = 2, border = "red")


# 6. Visualizar los puntos originales coloreados según clúster
grupos <- cutree(hc, k = 2) # asignar clústeres
plot(datos, col = grupos, pch = 19,
     main = "Clustering Jerárquico - Representación en el plano")

8.4) Método aglomerativo

El método aglomerativo de clusterización es una técnica jerárquica que construye grupos (clusters) de manera ascendente. Se inicia considerando cada observación como un clúster independiente y, en cada paso, se fusionan los dos clústeres más similares hasta que todos los elementos forman un único grupo o hasta cumplir un criterio de parada. La similitud o distancia entre clústeres puede medirse con diferentes métricas, como la distancia euclidiana \(d(x_i, x_j)\) o la correlación.

Este método sirve para descubrir estructuras jerárquicas en los datos y visualizar las relaciones entre observaciones mediante un dendrograma. A diferencia de métodos como \(k\)-means, no requiere especificar de antemano el número de clústeres, ya que la jerarquía completa queda representada y el analista puede decidir el nivel de corte. Su objetivo es organizar los datos de forma que los elementos más parecidos estén en el mismo clúster.

En la práctica, se utiliza en casos donde interesa entender la relación jerárquica entre grupos, como en biología para construir árboles filogenéticos, en análisis de textos para agrupar documentos similares, en marketing para segmentar clientes o en psicología para clasificar respuestas de encuestas. El método es especialmente útil cuando el número de clústeres es incierto y se requiere una estructura flexible.

Formalmente, en cada paso se define la distancia entre clústeres \(C_a\) y \(C_b\) de varias formas: en single linkage se usa \(\min { d(x_i, x_j): x_i \in C_a, x_j \in C_b }\); en complete linkage, el máximo; y en average linkage, el promedio. Estas definiciones determinan cómo se fusionan los clústeres y producen distintas estructuras jerárquicas en los datos.

Ejemplo con R

# Ejemplo en R: Método aglomerativo de clusterización

# 1. Simulamos datos
set.seed(123)
x <- matrix(rnorm(30), ncol = 2)  # 15 puntos en 2D
rownames(x) <- paste0("P", 1:nrow(x))

# 2. Calculamos la matriz de distancias
dist_matrix <- dist(x, method = "euclidean")

# 3. Aplicamos el clustering jerárquico aglomerativo
hc <- hclust(dist_matrix, method = "complete")

# 4. Graficamos el dendrograma
plot(hc, main = "Dendrograma - Clustering aglomerativo",
     xlab = "Observaciones", ylab = "Distancia")

# 5. (Opcional) Cortamos el dendrograma en 3 clústeres
rect.hclust(hc, k = 3, border = "red")

8.5) Dendograma

Un dendrograma es una representación gráfica en forma de árbol que muestra cómo se van fusionando los clústeres en un proceso de clustering jerárquico. Cada hoja del árbol corresponde a una observación individual, y las ramas indican las uniones sucesivas de grupos. La altura a la que se unen dos ramas representa la distancia o diferencia entre esos clústeres, medida mediante una métrica como la distancia euclidiana \(d(x_i, x_j)\).

El dendrograma sirve principalmente para visualizar la estructura jerárquica de los datos, facilitando la identificación de patrones y relaciones. Gracias a él, un analista puede decidir dónde “cortar” el árbol para obtener un número determinado de clústeres. Por ejemplo, al trazar una línea horizontal en el nivel de distancia \(\delta\), todos los grupos formados por debajo de esa altura se consideran clústeres distintos.

En cuanto a sus aplicaciones, el dendrograma es ampliamente utilizado en biología para representar árboles filogenéticos, en marketing para segmentar clientes, en lingüística para mostrar similitudes entre idiomas, y en psicología o sociología para clasificar perfiles de encuestados. Su capacidad de mostrar relaciones jerárquicas hace que sea una herramienta valiosa en cualquier disciplina que requiera agrupación exploratoria.

Formalmente, si \(C_a\) y \(C_b\) son dos clústeres, la altura del nodo que los une refleja la distancia \(d(C_a, C_b)\) según el criterio elegido: single linkage usa \(\min { d(x_i, x_j): x_i \in C_a, x_j \in C_b }\), mientras que complete linkage emplea el máximo, y average linkage utiliza el promedio. Así, el dendrograma encapsula en un solo gráfico todo el proceso de formación de clústeres.

Ejemplo con R

# Ejemplo gráfico de dendrograma en R

# 1. Creamos datos simulados (puntos en 2D)
set.seed(42)
x <- matrix(rnorm(20), ncol = 2)
rownames(x) <- paste0("Obs", 1:nrow(x))

# 2. Calculamos matriz de distancias (euclidiana)
dist_matrix <- dist(x, method = "euclidean")

# 3. Aplicamos clustering jerárquico aglomerativo
hc <- hclust(dist_matrix, method = "average")

# 4. Graficamos el dendrograma
plot(hc, main = "Dendrograma de Clustering Jerárquico",
     xlab = "Observaciones", ylab = "Distancia")

# 5. Opcional: cortar en 3 clústeres y resaltarlos
rect.hclust(hc, k = 3, border = "blue")

8.6) Metodo divisivo

El método divisivo de clusterización es una técnica jerárquica que funciona de manera opuesta al método aglomerativo. En lugar de empezar con cada observación como un clúster independiente, comienza con un único clúster que contiene a todas las observaciones, y en cada paso lo va dividiendo en subconjuntos más pequeños hasta llegar a clústeres individuales o hasta cumplir un criterio de parada. Es un enfoque top-down, mientras que el aglomerativo es bottom-up.

Este método sirve para descubrir estructuras jerárquicas en los datos cuando se supone que existe un gran grupo inicial que puede descomponerse en subgrupos más homogéneos. A nivel formal, se selecciona en cada iteración la partición que maximiza la disimilitud entre los nuevos grupos. La medida de disimilitud suele estar basada en distancias como la euclidiana \(d(x_i, x_j)\) o en índices de calidad de clúster como la varianza intra-grupo.

Los casos de uso más comunes incluyen clasificación de documentos (donde una colección completa se va dividiendo en temas más específicos), segmentación de mercados (un mercado global se subdivide en nichos más homogéneos), biología (división de especies en subcategorías) y análisis social (separación progresiva de poblaciones según variables demográficas). Es útil cuando interesa una visión global inicial y luego un refinamiento progresivo.

Una representación típica de este método también se muestra con un dendrograma, pero invertido en el sentido conceptual: se parte de un nodo raíz que representa el clúster total \(C\), y se divide sucesivamente en subclústeres \(C_a, C_b, \dots\). La altura de la división refleja la distancia \(d(C_a, C_b)\) o el criterio de disimilitud empleado. Así, el método divisivo permite descomponer jerárquicamente un conjunto grande en componentes más homogéneos.

Ejemplo con R:

# Ejemplo gráfico del método divisivo en R



# 2. Datos simulados
set.seed(123)
x <- matrix(rnorm(20), ncol = 2)
rownames(x) <- paste0("Obs", 1:nrow(x))

# 3. Aplicar clustering divisivo (DIANA)
divisivo <- diana(x)

# 4. Graficar el dendrograma
plot(divisivo, main = "Dendrograma - Método divisivo (DIANA)")


# 5. (Opcional) Cortar en 3 clústeres
clusters <- cutree(as.hclust(divisivo), k = 3)

# 6. Graficar los puntos coloreados por clúster
plot(x, col = clusters, pch = 19,
     main = "Datos agrupados con método divisivo")

8.7) Clúster basado en modelos

El clúster basado en modelos es un enfoque en el que se asume que los datos provienen de una combinación de distribuciones estadísticas subyacentes, y cada clúster corresponde a una de esas distribuciones. El objetivo es estimar los parámetros de esas distribuciones y asignar cada observación a la distribución (y por ende al clúster) con la mayor probabilidad. El caso más común es el modelo de mezclas gaussianas (Gaussian Mixture Models, GMM), donde cada clúster se modela como una distribución normal multivariada.

Este método sirve para capturar estructuras de datos más flexibles que las que permiten métodos como \(k\)-means, ya que no obliga a que los clústeres sean esféricos ni de igual tamaño. Cada clúster tiene su propia media \(\mu_k\) y matriz de covarianza \(\Sigma_k\), lo que permite modelar formas elípticas y relaciones entre variables. La estimación de parámetros suele hacerse con el algoritmo Expectation-Maximization (EM), que maximiza la verosimilitud de los datos bajo la mezcla de distribuciones.

Los casos de uso son amplios: en biología para identificar subpoblaciones en datos genómicos, en visión por computador para segmentación de imágenes, en finanzas para clasificar perfiles de riesgo de clientes, y en procesamiento de lenguaje natural para detectar tópicos latentes. Su ventaja es que además de segmentar, proporciona una interpretación probabilística de la pertenencia de cada observación a los clústeres.

Formalmente, se supone que los datos provienen de una mezcla:

\[p(x) = \sum_{k=1}^K \pi_k \, f(x \mid \theta_k)\]

donde \(\pi_k\) son los pesos de mezcla con \(\sum_{k=1}^K \pi_k = 1\), \(f(x \mid \theta_k)\) es la distribución del clúster \(k\) con parámetros \(\theta_k\), y \(K\) es el número de clústeres. En el caso de mezclas gaussianas, \(f(x \mid \theta_k)\) corresponde a la densidad normal multivariada con media \(\mu_k\) y covarianza \(\Sigma_k\).

Ejemplor en R

# Ejemplo gráfico: Clustering basado en modelos (Gaussian Mixture Models)



# 2. Generamos datos simulados con 3 clústeres elípticos
set.seed(123)
x <- cbind(c(rnorm(50, 0, 1), rnorm(50, 5, 1), rnorm(50, 3, 0.5)),
           c(rnorm(50, 0, 1), rnorm(50, 0, 1), rnorm(50, 5, 1)))

# 3. Ajustamos el modelo de mezcla gaussiana
modelo <- Mclust(x)
fitting ...

  |                                                            
  |                                                      |   0%
  |                                                            
  |                                                      |   1%
  |                                                            
  |=                                                     |   2%
  |                                                            
  |==                                                    |   3%
  |                                                            
  |==                                                    |   4%
  |                                                            
  |===                                                   |   5%
  |                                                            
  |===                                                   |   6%
  |                                                            
  |====                                                  |   7%
  |                                                            
  |====                                                  |   8%
  |                                                            
  |=====                                                 |   9%
  |                                                            
  |======                                                |  10%
  |                                                            
  |======                                                |  11%
  |                                                            
  |======                                                |  12%
  |                                                            
  |=======                                               |  13%
  |                                                            
  |========                                              |  14%
  |                                                            
  |========                                              |  15%
  |                                                            
  |=========                                             |  16%
  |                                                            
  |=========                                             |  17%
  |                                                            
  |==========                                            |  18%
  |                                                            
  |==========                                            |  19%
  |                                                            
  |===========                                           |  20%
  |                                                            
  |===========                                           |  21%
  |                                                            
  |============                                          |  22%
  |                                                            
  |============                                          |  23%
  |                                                            
  |=============                                         |  24%
  |                                                            
  |==============                                        |  25%
  |                                                            
  |==============                                        |  26%
  |                                                            
  |==============                                        |  27%
  |                                                            
  |===============                                       |  28%
  |                                                            
  |================                                      |  29%
  |                                                            
  |================                                      |  30%
  |                                                            
  |=================                                     |  31%
  |                                                            
  |=================                                     |  32%
  |                                                            
  |==================                                    |  33%
  |                                                            
  |==================                                    |  34%
  |                                                            
  |===================                                   |  35%
  |                                                            
  |====================                                  |  36%
  |                                                            
  |====================                                  |  37%
  |                                                            
  |====================                                  |  38%
  |                                                            
  |=====================                                 |  39%
  |                                                            
  |======================                                |  40%
  |                                                            
  |======================                                |  41%
  |                                                            
  |=======================                               |  42%
  |                                                            
  |=======================                               |  43%
  |                                                            
  |========================                              |  44%
  |                                                            
  |========================                              |  45%
  |                                                            
  |=========================                             |  46%
  |                                                            
  |==========================                            |  47%
  |                                                            
  |==========================                            |  48%
  |                                                            
  |==========================                            |  49%
  |                                                            
  |===========================                           |  50%
  |                                                            
  |============================                          |  51%
  |                                                            
  |============================                          |  52%
  |                                                            
  |============================                          |  53%
  |                                                            
  |=============================                         |  54%
  |                                                            
  |==============================                        |  55%
  |                                                            
  |==============================                        |  56%
  |                                                            
  |===============================                       |  57%
  |                                                            
  |===============================                       |  58%
  |                                                            
  |================================                      |  59%
  |                                                            
  |================================                      |  60%
  |                                                            
  |=================================                     |  61%
  |                                                            
  |==================================                    |  62%
  |                                                            
  |==================================                    |  63%
  |                                                            
  |==================================                    |  64%
  |                                                            
  |===================================                   |  65%
  |                                                            
  |====================================                  |  66%
  |                                                            
  |====================================                  |  67%
  |                                                            
  |=====================================                 |  68%
  |                                                            
  |=====================================                 |  69%
  |                                                            
  |======================================                |  70%
  |                                                            
  |======================================                |  71%
  |                                                            
  |=======================================               |  72%
  |                                                            
  |========================================              |  73%
  |                                                            
  |========================================              |  74%
  |                                                            
  |========================================              |  75%
  |                                                            
  |=========================================             |  76%
  |                                                            
  |==========================================            |  77%
  |                                                            
  |==========================================            |  78%
  |                                                            
  |===========================================           |  79%
  |                                                            
  |===========================================           |  80%
  |                                                            
  |============================================          |  81%
  |                                                            
  |============================================          |  82%
  |                                                            
  |=============================================         |  83%
  |                                                            
  |=============================================         |  84%
  |                                                            
  |==============================================        |  85%
  |                                                            
  |==============================================        |  86%
  |                                                            
  |===============================================       |  87%
  |                                                            
  |================================================      |  88%
  |                                                            
  |================================================      |  89%
  |                                                            
  |================================================      |  90%
  |                                                            
  |=================================================     |  91%
  |                                                            
  |==================================================    |  92%
  |                                                            
  |==================================================    |  93%
  |                                                            
  |===================================================   |  94%
  |                                                            
  |===================================================   |  95%
  |                                                            
  |====================================================  |  96%
  |                                                            
  |====================================================  |  97%
  |                                                            
  |===================================================== |  98%
  |                                                            
  |======================================================|  99%
  |                                                            
  |======================================================| 100%
# 4. Resumen del modelo
summary(modelo)
---------------------------------------------------- 
Gaussian finite mixture model fitted by EM algorithm 
---------------------------------------------------- 

Mclust EII (spherical, equal volume) model with 3 components: 

Clustering table:
 1  2  3 
50 50 50 
# 5. Gráfico de clasificación por clústeres
plot(modelo, what = "classification")


# 6. Gráfico de densidad estimada
plot(modelo, what = "density")

8.8) Ejemplo con Python

Librerias necesarias

8.8.1) K-medias

reticulate::repl_python()
# Semilla
np.random.seed(123)

# Datos simulados (3 grupos en 2D)
X = np.vstack([
    np.random.normal(loc=0, scale=0.3, size=(50, 2)),
    np.random.normal(loc=2, scale=0.3, size=(50, 2)),
    np.random.normal(loc=-2, scale=0.3, size=(50, 2))
])

# Aplicar K-means
kmeans = KMeans(n_clusters=3, n_init=20, random_state=123)
labels_kmeans = kmeans.fit_predict(X)

# Graficar
plt.scatter(X[:,0], X[:,1], c=labels_kmeans, cmap="viridis", s=50)
<matplotlib.collections.PathCollection object at 0x34a03c590>
plt.scatter(kmeans.cluster_centers_[:,0], kmeans.cluster_centers_[:,1], 
            c="red", marker="X", s=200, label="Centroides")
<matplotlib.collections.PathCollection object at 0x32fddc390>
plt.title("Ejemplo gráfico de K-medias")
Text(0.5, 1.0, 'Ejemplo gráfico de K-medias')
plt.legend()
<matplotlib.legend.Legend object at 0x34b4ff6d0>
plt.show()

8.8.2) K-medoides (PAM)

X2 = np.vstack([
    np.random.normal(loc=0, scale=0.5, size=(50, 2)),
    np.random.normal(loc=3, scale=0.5, size=(50, 2)),
    np.random.normal(loc=6, scale=0.5, size=(50, 2))
])

kmedoids = KMedoids(n_clusters=3, random_state=123, metric="euclidean")
labels_kmedoids = kmedoids.fit_predict(X2)

plt.scatter(X2[:,0], X2[:,1], c=labels_kmedoids, cmap="plasma", s=50)
plt.scatter(X2[kmedoids.medoid_indices_,0], X2[kmedoids.medoid_indices_,1], 
<matplotlib.collections.PathCollection object at 0x34a086250>
            c="black", marker="D", s=200, label="Medoides")
<matplotlib.collections.PathCollection object at 0x34bb14a90>
plt.title("Clustering con K-medoides (PAM)")
Text(0.5, 1.0, 'Clustering con K-medoides (PAM)')
plt.legend()
<matplotlib.legend.Legend object at 0x34b44ce10>
plt.show()

8.8.3) Método jerárquico aglomerativo

X3 = np.vstack([
    np.random.normal(loc=0, scale=1, size=(25, 2)),
    np.random.normal(loc=5, scale=1, size=(25, 2))
])

# Matriz de distancias + clustering
Z = linkage(X3, method="ward")

# Dendrograma
plt.figure(figsize=(8,5))
<Figure size 800x500 with 0 Axes>
dendrogram(Z)
{'icoord': [[5.0, 5.0, 15.0, 15.0], [35.0, 35.0, 45.0, 45.0], [25.0, 25.0, 40.0, 40.0], [10.0, 10.0, 32.5, 32.5], [75.0, 75.0, 85.0, 85.0], [95.0, 95.0, 105.0, 105.0], [115.0, 115.0, 125.0, 125.0], [100.0, 100.0, 120.0, 120.0], [80.0, 80.0, 110.0, 110.0], [65.0, 65.0, 95.0, 95.0], [55.0, 55.0, 80.0, 80.0], [135.0, 135.0, 145.0, 145.0], [155.0, 155.0, 165.0, 165.0], [140.0, 140.0, 160.0, 160.0], [175.0, 175.0, 185.0, 185.0], [215.0, 215.0, 225.0, 225.0], [205.0, 205.0, 220.0, 220.0], [195.0, 195.0, 212.5, 212.5], [235.0, 235.0, 245.0, 245.0], [203.75, 203.75, 240.0, 240.0], [180.0, 180.0, 221.875, 221.875], [150.0, 150.0, 200.9375, 200.9375], [67.5, 67.5, 175.46875, 175.46875], [21.25, 21.25, 121.484375, 121.484375], [275.0, 275.0, 285.0, 285.0], [265.0, 265.0, 280.0, 280.0], [255.0, 255.0, 272.5, 272.5], [305.0, 305.0, 315.0, 315.0], [295.0, 295.0, 310.0, 310.0], [355.0, 355.0, 365.0, 365.0], [345.0, 345.0, 360.0, 360.0], [335.0, 335.0, 352.5, 352.5], [325.0, 325.0, 343.75, 343.75], [302.5, 302.5, 334.375, 334.375], [263.75, 263.75, 318.4375, 318.4375], [375.0, 375.0, 385.0, 385.0], [395.0, 395.0, 405.0, 405.0], [380.0, 380.0, 400.0, 400.0], [425.0, 425.0, 435.0, 435.0], [415.0, 415.0, 430.0, 430.0], [390.0, 390.0, 422.5, 422.5], [465.0, 465.0, 475.0, 475.0], [485.0, 485.0, 495.0, 495.0], [470.0, 470.0, 490.0, 490.0], [455.0, 455.0, 480.0, 480.0], [445.0, 445.0, 467.5, 467.5], [406.25, 406.25, 456.25, 456.25], [291.09375, 291.09375, 431.25, 431.25], [71.3671875, 71.3671875, 361.171875, 361.171875]], 'dcoord': [[0.0, 0.6353064547562574, 0.6353064547562574, 0.0], [0.0, 0.4172638851062251, 0.4172638851062251, 0.0], [0.0, 0.862031329084084, 0.862031329084084, 0.4172638851062251], [0.6353064547562574, 1.9520479818276195, 1.9520479818276195, 0.862031329084084], [0.0, 0.06905356873277826, 0.06905356873277826, 0.0], [0.0, 0.20530182128905813, 0.20530182128905813, 0.0], [0.0, 0.4634913213571551, 0.4634913213571551, 0.0], [0.20530182128905813, 0.9022541570047887, 0.9022541570047887, 0.4634913213571551], [0.06905356873277826, 1.1935298434737889, 1.1935298434737889, 0.9022541570047887], [0.0, 1.5174603681033452, 1.5174603681033452, 1.1935298434737889], [0.0, 2.0281090180961048, 2.0281090180961048, 1.5174603681033452], [0.0, 0.39669276091578726, 0.39669276091578726, 0.0], [0.0, 0.5090379218159828, 0.5090379218159828, 0.0], [0.39669276091578726, 1.0079616001393257, 1.0079616001393257, 0.5090379218159828], [0.0, 0.7645533260718674, 0.7645533260718674, 0.0], [0.0, 0.13588680858660343, 0.13588680858660343, 0.0], [0.0, 0.3284688599782302, 0.3284688599782302, 0.13588680858660343], [0.0, 0.3971770839438781, 0.3971770839438781, 0.3284688599782302], [0.0, 0.8941963447549053, 0.8941963447549053, 0.0], [0.3971770839438781, 1.4407006104694555, 1.4407006104694555, 0.8941963447549053], [0.7645533260718674, 1.661907655528787, 1.661907655528787, 1.4407006104694555], [1.0079616001393257, 2.5504093945104183, 2.5504093945104183, 1.661907655528787], [2.0281090180961048, 4.594963436838478, 4.594963436838478, 2.5504093945104183], [1.9520479818276195, 5.954113752636451, 5.954113752636451, 4.594963436838478], [0.0, 0.40677595587768267, 0.40677595587768267, 0.0], [0.0, 0.7699566449072738, 0.7699566449072738, 0.40677595587768267], [0.0, 1.5628519403618257, 1.5628519403618257, 0.7699566449072738], [0.0, 0.49064104078825377, 0.49064104078825377, 0.0], [0.0, 0.8781757145657899, 0.8781757145657899, 0.49064104078825377], [0.0, 0.18747438921714635, 0.18747438921714635, 0.0], [0.0, 0.34191976795291124, 0.34191976795291124, 0.18747438921714635], [0.0, 0.4786993641585391, 0.4786993641585391, 0.34191976795291124], [0.0, 1.1480245805497424, 1.1480245805497424, 0.4786993641585391], [0.8781757145657899, 1.9574990309342943, 1.9574990309342943, 1.1480245805497424], [1.5628519403618257, 2.9179199932790185, 2.9179199932790185, 1.9574990309342943], [0.0, 0.05412176066824535, 0.05412176066824535, 0.0], [0.0, 0.15853485239976747, 0.15853485239976747, 0.0], [0.05412176066824535, 0.785544301445569, 0.785544301445569, 0.15853485239976747], [0.0, 0.5085423428048073, 0.5085423428048073, 0.0], [0.0, 0.8439310593506676, 0.8439310593506676, 0.5085423428048073], [0.785544301445569, 1.529487534895939, 1.529487534895939, 0.8439310593506676], [0.0, 0.11129841842297827, 0.11129841842297827, 0.0], [0.0, 0.3209812159609646, 0.3209812159609646, 0.0], [0.11129841842297827, 0.687359064679709, 0.687359064679709, 0.3209812159609646], [0.0, 1.1995103488837604, 1.1995103488837604, 0.687359064679709], [0.0, 2.2172980270219433, 2.2172980270219433, 1.1995103488837604], [1.529487534895939, 5.000973075917236, 5.000973075917236, 2.2172980270219433], [2.9179199932790185, 6.40845207662586, 6.40845207662586, 5.000973075917236], [5.954113752636451, 36.05964969742333, 36.05964969742333, 6.40845207662586]], 'ivl': ['29', '45', '38', '30', '47', '34', '33', '32', '37', '39', '40', '43', '49', '25', '48', '27', '36', '35', '44', '46', '28', '26', '41', '31', '42', '18', '3', '0', '20', '6', '12', '22', '2', '4', '1', '7', '11', '8', '24', '10', '19', '5', '14', '21', '16', '17', '9', '15', '13', '23'], 'leaves': [29, 45, 38, 30, 47, 34, 33, 32, 37, 39, 40, 43, 49, 25, 48, 27, 36, 35, 44, 46, 28, 26, 41, 31, 42, 18, 3, 0, 20, 6, 12, 22, 2, 4, 1, 7, 11, 8, 24, 10, 19, 5, 14, 21, 16, 17, 9, 15, 13, 23], 'color_list': ['C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C0'], 'leaves_color_list': ['C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C1', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2', 'C2']}
plt.title("Dendrograma - Método Jerárquico (Ward)")
Text(0.5, 1.0, 'Dendrograma - Método Jerárquico (Ward)')
plt.xlabel("Observaciones")
plt.ylabel("Distancia")
Text(0.5, 0, 'Observaciones')
Text(0, 0.5, 'Distancia')
plt.show()

# Cortar en 2 clústeres
labels_hc = fcluster(Z, t=2, criterion="maxclust")

# Visualizar clusters en el plano
plt.scatter(X3[:,0], X3[:,1], c=labels_hc, cmap="Accent", s=50)
plt.title("Clustering Jerárquico - Representación en el plano")
<matplotlib.collections.PathCollection object at 0x34bbc4f10>
plt.show()

Text(0.5, 1.0, 'Clustering Jerárquico - Representación en el plano')

8.8.3) Clúster basado en modelos (GMM)

X4 = np.vstack([
    np.random.multivariate_normal([0,0], [[1,0.5],[0.5,1]], size=50),
    np.random.multivariate_normal([5,0], [[1,-0.5],[-0.5,1]], size=50),
    np.random.multivariate_normal([3,5], [[1,0.2],[0.2,1]], size=50)
])

gmm = GaussianMixture(n_components=3, random_state=123)
labels_gmm = gmm.fit_predict(X4)

plt.scatter(X4[:,0], X4[:,1], c=labels_gmm, cmap="Set1", s=50)
<matplotlib.collections.PathCollection object at 0x34bb5c290>
plt.title("Clustering basado en modelos (GMM)")
Text(0.5, 1.0, 'Clustering basado en modelos (GMM)')
plt.show()

9) Clasificación

Un modelo de clasificación multivariada es una técnica estadística y de aprendizaje automático que busca asignar observaciones a categorías o clases, cuando dichas observaciones están descritas por múltiples variables independientes (predictoras). Formalmente, se tiene un conjunto de variables explicativas \(X = (x_1, x_2, \dots, x_p)\) y una variable respuesta categórica \(Y \in {1,2,\dots,K}\), donde \(K\) es el número de clases posibles. El objetivo es construir una regla de decisión que minimice el error de clasificación al predecir \(Y\) en función de \(X\).

Estos modelos sirven para reconocer patrones y tomar decisiones basadas en datos multidimensionales. Por ejemplo, un modelo de clasificación multivariada puede estimar la probabilidad de que un cliente compre un producto (\(Y=1\)) o no (\(Y=0\)) en función de variables como edad, ingresos y hábitos de consumo. Matemáticamente, se busca una función de decisión \(f(X)\) que aproxime \(\mathbb{P}(Y=k|X)\) para cada clase \(k\), y luego se clasifique observando cuál de esas probabilidades es mayor:

\[\hat{y} = \arg\max_{k \in \{1,\dots,K\}} \mathbb{P}(Y=k \mid X)\]

Entre los métodos más usados están la regresión logística multiclase, el análisis discriminante lineal (LDA), los árboles de decisión, las máquinas de soporte vectorial (SVM) y las redes neuronales. Todos ellos modelan de distinta forma la relación entre las variables predictoras y la probabilidad de pertenencia a cada clase. Por ejemplo, en LDA se asume que los datos provienen de distribuciones normales multivariadas con medias distintas \(\mu_k\) y una matriz de covarianza común \(\Sigma\), lo que genera fronteras de decisión lineales entre las clases.

Los casos de uso abarcan múltiples campos: en medicina, para clasificar si un paciente presenta una enfermedad o no según resultados clínicos; en finanzas, para determinar el riesgo crediticio de un solicitante; en marketing, para segmentar clientes en perfiles de consumo; y en seguridad, para sistemas de reconocimiento facial o detección de fraudes. En general, se aplican siempre que se tenga un conjunto de datos con varias variables y se busque predecir una categoría discreta a partir de ellas.

Ejemplo con R

quit

# Semilla
set.seed(123)

# 1. Generar datos simulados
n <- 200
x1 <- rnorm(n, mean = rep(c(2, 5), each = n/2), sd = 1)
x2 <- rnorm(n, mean = rep(c(2, 5), each = n/2), sd = 1)
clase <- factor(rep(c("A", "B"), each = n/2))

datos <- data.frame(x1, x2, clase)

# 2. Entrenar modelo LDA
modelo_lda <- lda(clase ~ x1 + x2, data = datos)

# 3. Predicciones y frontera
grid <- expand.grid(
  x1 = seq(min(datos$x1)-1, max(datos$x1)+1, length=200),
  x2 = seq(min(datos$x2)-1, max(datos$x2)+1, length=200)
)

grid$clase_pred <- predict(modelo_lda, newdata = grid)$class

# 4. Visualización
ggplot() +
  geom_point(data = datos, aes(x = x1, y = x2, color = clase), alpha = 0.7, size = 3) +
  geom_contour(data = cbind(grid, z = as.numeric(grid$clase_pred)),
               aes(x = x1, y = x2, z = z),
               color = "black", linewidth = 1) +
  scale_color_manual(values = c("tomato", "steelblue")) +
  theme_minimal() +
  labs(title = "Clasificación Multivariada con LDA",
       subtitle = "Frontera de decisión entre dos clases",
       x = "Variable X1", y = "Variable X2")

9.1) kNN

El k-Nearest Neighbors (kNN) es un algoritmo de clasificación supervisada que asigna una clase a una nueva observación basándose en la proximidad a sus vecinos más cercanos en el espacio de las variables. Formalmente, dado un conjunto de entrenamiento con pares \((x_i, y_i)\) donde \(x_i \in \mathbb{R}^p\) son las variables predictoras y \(y_i \in {1,2,\dots,K}\) las clases, cuando aparece un nuevo punto \(x\), se buscan sus \(k\) vecinos más cercanos según una métrica de distancia (comúnmente la distancia euclidiana).

El algoritmo sirve para clasificación y también para regresión. En el caso de clasificación, la regla de decisión se basa en la mayoría de clases entre los \(k\) vecinos más cercanos. Matemáticamente: \[\hat{y}(x) = \arg\max_{c \in \{1,\dots,K\}} \sum_{i \in N_k(x)} \mathbf{1}(y_i = c)\]

donde \(N_k(x)\) es el conjunto de índices de los \(k\) vecinos más cercanos a \(x\) y \(\mathbf{1}(\cdot)\) es la función indicadora.

El kNN es útil porque no requiere un modelo paramétrico ni suposiciones fuertes sobre la distribución de los datos: simplemente se basa en la geometría de los puntos en el espacio. Es especialmente útil en problemas donde la frontera entre clases es compleja o no lineal, ya que se adapta de manera local a la estructura de los datos.

Entre sus casos de uso están: en medicina, para clasificar tumores como benignos o malignos según medidas clínicas; en finanzas, para detectar fraudes en tarjetas de crédito; en marketing, para segmentar clientes con base en similitudes de consumo; y en sistemas de recomendación, donde se identifican usuarios con comportamientos cercanos para predecir qué producto puede interesarle a otro usuario.

Clases balanceadas

En modelos de clasificación como kNN, el tema de las clases balanceadas es muy importante porque el algoritmo decide la clase de un nuevo punto contando cuántos vecinos pertenecen a cada categoría. Si una clase es mucho más numerosa que otra (clases desbalanceadas), es más probable que la mayoría de los vecinos cercanos pertenezcan a esa clase dominante, lo que lleva a un sesgo sistemático en la predicción.

Por ejemplo, si en un dataset el \(90%\) de las observaciones son de la clase \(A\) y solo el \(10%\) son de la clase \(B\), el modelo kNN tenderá a clasificar casi todos los puntos como \(A\), incluso si los puntos realmente pertenecen a \(B\). Esto hace que las métricas globales como la precisión parezcan buenas (porque acierta en la mayoría), pero el desempeño sobre la clase minoritaria será muy pobre.

Para manejar este problema, existen varias estrategias:

  • Balanceo del dataset antes de entrenar: mediante técnicas de submuestreo de la clase mayoritaria o sobremuestreo de la minoritaria (por ejemplo con SMOTE).

  • Ponderación en kNN: asignando más peso a los vecinos de clases minoritarias, de manera que su voto cuente más en la decisión.

  • Evaluación con métricas adecuadas: usar recall, F1-score o matriz de confusión, que reflejen mejor el desempeño en ambas clases, en lugar de solo la precisión.

En resumen, en kNN (y otros clasificadores) el balance de clases es crucial porque el modelo se basa en la frecuencia local de las observaciones, y un desbalance fuerte puede ocultar el mal desempeño en la clase minoritaria.

Validación Cruzada La validación cruzada en modelos de clasificación kNN es una técnica de evaluación que permite estimar el rendimiento real del modelo sin depender de una única partición entrenamiento–prueba. La idea es dividir el conjunto de datos en \(K\) subconjuntos (o folds), entrenar el modelo kNN con \(K-1\) de ellos y evaluar en el subconjunto restante. Este proceso se repite \(K\) veces, cambiando el fold de validación en cada iteración, y luego se promedian las métricas (precisión, error, sensibilidad, etc.).

En el caso específico de kNN, la validación cruzada se usa también para seleccionar el valor óptimo de \(k\) (número de vecinos). Se prueban distintos valores de \(k\) y para cada uno se calcula el error promedio de clasificación a través de los folds. El \(k\) que minimiza el error de validación cruzada se considera el más adecuado, ya que balancea sesgo y varianza del modelo.

Este método es especialmente importante en kNN porque su rendimiento depende fuertemente de la elección de \(k\) y de la distribución de los datos. Con validación cruzada, se obtiene una estimación más robusta y confiable de qué tan bien generalizará el modelo a nuevos datos, reduciendo el riesgo de sobreajuste o subajuste.

Ejemplo con R


# Semila
set.seed(123)

# 1. Generar datos simulados (desbalanceados)
n <- 200
x1 <- rnorm(n, mean = rep(c(2, 5), times = c(160, 40)), sd = 1)
x2 <- rnorm(n, mean = rep(c(2, 5), times = c(160, 40)), sd = 1)
clase <- factor(c(rep("A", 160), rep("B", 40)))  # Desbalanceado: 80% A, 20% B

datos <- data.frame(x1, x2, clase)

# 2. Validación cruzada con balanceo (oversampling)
control <- trainControl(
  method = "cv", 
  number = 10,        # 10-fold CV
  sampling = "up"     # Balanceo: oversampling de la clase minoritaria
)

grid_k <- expand.grid(k = 1:20)

modelo_cv <- train(
  clase ~ ., 
  data = datos,
  method = "knn",
  trControl = control,
  tuneGrid = grid_k
)

print(modelo_cv)
k-Nearest Neighbors 

200 samples
  2 predictor
  2 classes: 'A', 'B' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 180, 180, 180, 180, 180, 180, ... 
Addtional sampling using up-sampling

Resampling results across tuning parameters:

  k   Accuracy  Kappa    
   1  0.980     0.9369458
   2  0.980     0.9451128
   3  0.980     0.9451128
   4  0.980     0.9451128
   5  0.980     0.9451128
   6  0.975     0.9308271
   7  0.980     0.9451128
   8  0.975     0.9308271
   9  0.975     0.9308271
  10  0.980     0.9451128
  11  0.980     0.9451128
  12  0.980     0.9451128
  13  0.975     0.9308271
  14  0.980     0.9451128
  15  0.980     0.9451128
  16  0.975     0.9308271
  17  0.975     0.9308271
  18  0.975     0.9308271
  19  0.975     0.9308271
  20  0.975     0.9308271

Accuracy was used to select the optimal model using the
 largest value.
The final value used for the model was k = 15.
plot(modelo_cv)


# Mejor valor de k según CV balanceada
mejor_k <- modelo_cv$bestTune$k
cat("Mejor valor de k:", mejor_k, "\n")
Mejor valor de k: 15 
# 3. Crear grilla para visualizar frontera de decisión
grid <- expand.grid(
  x1 = seq(min(datos$x1)-1, max(datos$x1)+1, length = 200),
  x2 = seq(min(datos$x2)-1, max(datos$x2)+1, length = 200)
)

# 4. Aplicar kNN con el mejor k
pred_grid <- knn(
  train = datos[, c("x1", "x2")],
  test = grid,
  cl = datos$clase,
  k = mejor_k
)

grid$clase_pred <- pred_grid

# 5. Visualización
ggplot() +
  geom_tile(data = grid, aes(x = x1, y = x2, fill = clase_pred), alpha = 0.3) +
  geom_point(data = datos, aes(x = x1, y = x2, color = clase), size = 3, alpha = 0.8) +
  scale_fill_manual(values = c("tomato", "steelblue")) +
  scale_color_manual(values = c("tomato", "steelblue")) +
  theme_minimal() +
  labs(
    title = paste("Clasificación con kNN balanceado (k =", mejor_k, ")"),
    subtitle = "Validación cruzada 10-fold con oversampling",
    x = "Variable X1", y = "Variable X2"
  )

9.2) Clasificación Bayesiana

La clasificación bayesiana es una técnica de clasificación supervisada basada en el teorema de Bayes, el cual relaciona la probabilidad condicional de una clase dado un conjunto de variables observadas. En este enfoque, el objetivo es estimar la probabilidad posterior \(\mathbb{P}(Y=k \mid X)\) para cada clase \(k\), donde \(X = (x_1, x_2, \dots, x_p)\) son las variables predictoras y \(Y\) es la variable categórica a predecir. La regla de decisión consiste en asignar a una nueva observación la clase con mayor probabilidad posterior:

\[\hat{y} = \arg\max_{k \in \{1,\dots,K\}} \mathbb{P}(Y=k \mid X)\]

El teorema de Bayes establece que:

\[\mathbb{P}(Y=k \mid X) = \frac{\mathbb{P}(X \mid Y=k) \, \mathbb{P}(Y=k)}{\mathbb{P}(X)}\]

Aquí \(\mathbb{P}(X \mid Y=k)\) es la verosimilitud de observar los datos dado que pertenecen a la clase \(k\), \(\mathbb{P}(Y=k)\) es la probabilidad a priori de esa clase, y \(\mathbb{P}(X)\) es la probabilidad marginal de los datos. En la práctica, no se calcula el denominador porque es común a todas las clases.

El clasificador bayesiano más popular es el Naive Bayes, que asume independencia condicional entre las variables \(x_j\) dado \(Y\). Bajo esta suposición, la verosimilitud se factoriza como:

\[\mathbb{P}(X \mid Y=k) = \prod_{j=1}^p \mathbb{P}(x_j \mid Y=k)\]

Esto simplifica enormemente el cálculo, aunque la suposición de independencia rara vez se cumple estrictamente.

Entre los casos de uso destacan: en filtrado de spam, donde los correos se clasifican como spam o no spam a partir de las palabras que contienen; en procesamiento de lenguaje natural, para clasificar sentimientos de textos; en medicina, para diagnosticar enfermedades a partir de síntomas; y en finanzas, para evaluar riesgo crediticio. Su principal ventaja es que es un método rápido, interpretable y muy efectivo incluso con grandes volúmenes de datos y variables categóricas.

Análisis descriminante lineal

El análisis discriminante lineal (LDA) es un método de clasificación que surge de la perspectiva bayesiana, donde se asume que las variables predictoras \(X\) siguen una distribución normal multivariada en cada clase, con la misma matriz de covarianza \(\Sigma\) para todas ellas. Usando el teorema de Bayes, se derivan funciones discriminantes que permiten calcular, para cada clase \(k\), un puntaje de pertenencia y asignar una nueva observación a la clase con mayor valor.

En términos matemáticos, la función discriminante de LDA para una clase \(k\) es

\[\delta_k(x) = x^\top \Sigma^{-1} \mu_k \;-\; \tfrac{1}{2}\mu_k^\top \Sigma^{-1}\mu_k \;+\; \log \pi_k\] onde \(\mu_k\) es el vector de medias de la clase \(k\), \(\Sigma\) la matriz de covarianza común y \(\pi_k\) la probabilidad a priori de la clase. La regla de decisión es asignar \(x\) a la clase que maximice \(\delta_k(x)\).

Este enfoque es muy útil cuando las clases son aproximadamente normales y linealmente separables. Se aplica en áreas como el reconocimiento de patrones, clasificación de imágenes, diagnóstico médico o segmentación de clientes, donde además de predecir etiquetas, LDA permite interpretar cómo las variables contribuyen a la separación entre grupos.

Análisis descriminante cuadratico

El análisis discriminante cuadrático (QDA) es una extensión del análisis discriminante lineal (LDA) dentro del marco de la clasificación bayesiana. La diferencia principal es que en QDA no se asume que todas las clases comparten la misma matriz de covarianza. En lugar de una única \(\Sigma\), cada clase \(k\) tiene su propia matriz \(\Sigma_k\). Esto permite mayor flexibilidad en la forma de las fronteras de decisión, que dejan de ser lineales y se vuelven curvas (generalmente elípticas).

La función discriminante en QDA para una clase \(k\) se expresa como:

\[ \delta_k(x) = -\tfrac{1}{2} \log |\Sigma_k| \;-\; \tfrac{1}{2}(x - \mu_k)^\top \Sigma_k^{-1} (x - \mu_k) \;+\; \log \pi_k\]

donde \(\mu_k\) es el vector de medias de la clase \(k\), \(\Sigma_k\) la matriz de covarianza de la clase, y \(\pi_k\) la probabilidad a priori de pertenecer a dicha clase.

QDA resulta especialmente útil cuando las clases tienen formas y varianzas muy diferentes, ya que LDA puede fallar al forzar fronteras lineales. Por ello, QDA se aplica en problemas complejos de clasificación como biología computacional, visión por computador o finanzas, donde los datos de distintas clases no se distribuyen con la misma estructura de dispersión.

Ejemplo con R



set.seed(123)

# 1. Generar datos simulados
n <- 200
x1 <- c(rnorm(n/2, mean = 2, sd = 1),
        rnorm(n/2, mean = 5, sd = 1.2))
x2 <- c(rnorm(n/2, mean = 3, sd = 1),
        rnorm(n/2, mean = 6, sd = 1.2))
clase <- factor(rep(c("A", "B"), each = n/2))

datos <- data.frame(x1, x2, clase)

# 2. Entrenar modelo Naive Bayes
modelo_nb <- naiveBayes(clase ~ x1 + x2, data = datos)

# 3. Crear grilla para la frontera
grid <- expand.grid(
  x1 = seq(min(datos$x1)-1, max(datos$x1)+1, length = 200),
  x2 = seq(min(datos$x2)-1, max(datos$x2)+1, length = 200)
)

grid$clase_pred <- predict(modelo_nb, newdata = grid)

# 4. Visualización
ggplot() +
  geom_tile(data = grid, aes(x = x1, y = x2, fill = clase_pred), alpha = 0.3) +
  geom_point(data = datos, aes(x = x1, y = x2, color = clase), size = 3, alpha = 0.8) +
  scale_fill_manual(values = c("tomato", "steelblue")) +
  scale_color_manual(values = c("tomato", "steelblue")) +
  theme_minimal() +
  labs(title = "Clasificación Bayesiana (Naive Bayes)",
       subtitle = "Colores de fondo = frontera de decisión",
       x = "Variable X1", y = "Variable X2")

9.3) Regresión Logística

La regresión logística es un modelo estadístico utilizado para predecir una variable dependiente categórica, típicamente binaria (\(Y \in {0,1}\)), a partir de un conjunto de variables independientes \(X = (x_1, x_2, \dots, x_p)\).

A diferencia de la regresión lineal, que predice valores continuos, la regresión logística modela la probabilidad de que ocurra un evento.

El modelo se basa en la función logit, que relaciona la probabilidad \(\mathbb{P}(Y=1 \mid X)\) con una combinación lineal de las variables:

\[\log\left(\frac{\mathbb{P}(Y=1 \mid X)}{1 - \mathbb{P}(Y=1 \mid X)}\right) = \beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p\]

De forma equivalente, la probabilidad se expresa con la función sigmoide:

\[\mathbb{P}(Y=1 \mid X) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p)}}\]

La regresión logística sirve para clasificar observaciones según sus características, asignando probabilidades a los posibles resultados. Una regla común es predecir \(\hat{y}=1\) si la probabilidad estimada es mayor que un umbral (por ejemplo, \(0.5\)). El modelo se ajusta maximizando la verosimilitud de los datos observados, lo cual permite estimar los parámetros \(\beta_j\).

Entre los casos de uso están: en medicina, para predecir si un paciente tiene una enfermedad (sí/no) según variables clínicas; en finanzas, para estimar la probabilidad de incumplimiento de un crédito; en marketing, para modelar la probabilidad de que un cliente compre un producto; y en seguridad informática, para detectar accesos fraudulentos. Es un modelo muy interpretado, ya que los coeficientes se traducen en odds ratios que indican la influencia de cada variable.

Ejemplo con R



set.seed(123)

# 1. Generar datos simulados
n <- 200
x1 <- runif(n, 0, 10)
x2 <- runif(n, 0, 10)

# Variable dependiente generada con probabilidad logística
prob <- 1 / (1 + exp(-( -5 + 0.8*x1 + 0.6*x2)))
y <- rbinom(n, 1, prob)
clase <- factor(y, labels = c("No", "Sí"))

datos <- data.frame(x1, x2, clase)

# 2. Ajustar modelo logístico
modelo_logit <- glm(clase ~ x1 + x2, data = datos, family = binomial)

# 3. Crear grilla para frontera
grid <- expand.grid(
  x1 = seq(min(datos$x1), max(datos$x1), length = 200),
  x2 = seq(min(datos$x2), max(datos$x2), length = 200)
)

grid$prob <- predict(modelo_logit, newdata = grid, type = "response")
grid$clase_pred <- ifelse(grid$prob > 0.5, "Sí", "No")

# 4. Visualización
ggplot() +
  geom_tile(data = grid, aes(x = x1, y = x2, fill = clase_pred, alpha = prob)) +
  geom_point(data = datos, aes(x = x1, y = x2, color = clase), size = 3) +
  scale_fill_manual(values = c("tomato", "steelblue")) +
  scale_color_manual(values = c("tomato", "steelblue")) +
  theme_minimal() +
  labs(title = "Clasificación con Regresión Logística",
       subtitle = "Colores de fondo = región de clasificación, transparencia = probabilidad",
       x = "Variable X1", y = "Variable X2") +
  guides(alpha = "none")

9.4) Análisis discriminante

El análisis discriminante es una técnica estadística multivariada utilizada para clasificar observaciones en diferentes grupos o clases, a partir de un conjunto de variables predictoras. Su objetivo es encontrar funciones lineales de las variables \(X = (x_1, x_2, \dots, x_p)\) que maximicen la separación entre las clases, es decir, que discriminen lo mejor posible entre los grupos previamente definidos.

El método más conocido es el análisis discriminante lineal (LDA), que asume que cada clase \(k\) sigue una distribución normal multivariada con media \(\mu_k\) y una matriz de covarianza común \(\Sigma\). En este caso, la regla de decisión asigna una observación \(x\) a la clase que maximiza la función discriminante:

\[\delta_k(x) = x^\top \Sigma^{-1} \mu_k \;-\; \tfrac{1}{2}\mu_k^\top \Sigma^{-1}\mu_k \;+\; \log \pi_k \]

donde \(\pi_k\) es la probabilidad a priori de la clase \(k\).

También existe el análisis discriminante cuadrático (QDA), que surge cuando no se asume una matriz de covarianza común, lo que genera fronteras de decisión más flexibles y no necesariamente lineales. La diferencia clave entre LDA y QDA es la complejidad de la frontera: lineal en el primero, cuadrática en el segundo.

Los casos de uso son muy variados: en medicina, para clasificar pacientes según diagnóstico a partir de pruebas clínicas; en finanzas, para detectar clientes con alto o bajo riesgo de crédito; en biología, para distinguir especies a partir de características morfológicas (por ejemplo, el clásico dataset iris); y en marketing, para segmentar clientes en grupos de comportamiento de compra. Es una técnica ampliamente usada porque combina interpretabilidad estadística con una buena capacidad de clasificación.

Ejemplo con R



set.seed(123)

# 1. Dataset iris (usamos 2 variables para graficar fácilmente)
datos <- iris %>%
  select(Sepal.Length, Sepal.Width, Species)

# 2. Entrenar modelo LDA
modelo_lda <- lda(Species ~ Sepal.Length + Sepal.Width, data = datos)

# 3. Crear grilla para visualizar frontera
grid <- expand.grid(
  Sepal.Length = seq(min(datos$Sepal.Length)-0.5, max(datos$Sepal.Length)+0.5, length = 200),
  Sepal.Width  = seq(min(datos$Sepal.Width)-0.5,  max(datos$Sepal.Width)+0.5,  length = 200)
)

grid$clase_pred <- predict(modelo_lda, newdata = grid)$class

# 4. Visualización
ggplot() +
  geom_tile(data = grid, aes(x = Sepal.Length, y = Sepal.Width, fill = clase_pred), alpha = 0.3) +
  geom_point(data = datos, aes(x = Sepal.Length, y = Sepal.Width, color = Species), size = 3) +
  scale_fill_manual(values = c("tomato", "steelblue", "seagreen")) +
  scale_color_manual(values = c("tomato", "steelblue", "seagreen")) +
  theme_minimal() +
  labs(title = "Análisis Discriminante Lineal (LDA) con iris",
       subtitle = "Fronteras de decisión entre especies usando Sepal.Length y Sepal.Width",
       x = "Sepal Length", y = "Sepal Width")

9.5) Ejemplo con Python

9.5.1) Clasificación multivariada

reticulate::repl_python()

# 1. Generar datos simulados
np.random.seed(123)
n = 200
x1 = np.concatenate([np.random.normal(2, 1, n//2), np.random.normal(5, 1, n//2)])
x2 = np.concatenate([np.random.normal(2, 1, n//2), np.random.normal(5, 1, n//2)])
clase = np.array(["A"]*(n//2) + ["B"]*(n//2))

datos = pd.DataFrame({"x1": x1, "x2": x2, "clase": clase})

# 2. Entrenar modelo LDA
lda = LinearDiscriminantAnalysis()
lda.fit(datos[["x1", "x2"]], datos["clase"])
LinearDiscriminantAnalysis()
# 3. Crear grilla para frontera
xx, yy = np.meshgrid(
    np.linspace(datos.x1.min()-1, datos.x1.max()+1, 200),
    np.linspace(datos.x2.min()-1, datos.x2.max()+1, 200)
)
grid = np.c_[xx.ravel(), yy.ravel()]
clase_pred = lda.predict(grid)
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/base.py:493: UserWarning:

X does not have valid feature names, but LinearDiscriminantAnalysis was fitted with feature names
# 4. Visualización
plt.contourf(xx, yy, clase_pred.reshape(xx.shape), alpha=0.3, cmap="coolwarm")
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 3190;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py]8;;", line 3190, in contourf
    __ret = gca().contourf(
            ^^^^^^^^^^^^^^^
  File "]8;line = 1524;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py]8;;", line 1524, in inner
    return func(
           ^^^^^
  File "]8;line = 6797;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py]8;;", line 6797, in contourf
    contours = mcontour.QuadContourSet(self, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 701;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 701, in __init__
    kwargs = self._process_args(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1319;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1319, in _process_args
    x, y, z = self._contour_args(args, kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1363;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1363, in _contour_args
    z = ma.masked_invalid(z, copy=False)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 2360;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py]8;;", line 2360, in masked_invalid
    res = masked_where(~(np.isfinite(a)), a, copy=copy)
                         ^^^^^^^^^^^^^^
TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
g
plt.scatter(datos.x1, datos.x2, c=(datos.clase=="A"), cmap="coolwarm", edgecolor="k", s=50)
<matplotlib.collections.PathCollection object at 0x32e9812d0>
plt.title("Clasificación Multivariada con LDA")
Text(0.5, 1.0, 'Clasificación Multivariada con LDA')
plt.xlabel("Variable X1")
Text(0.5, 6.280995055624219, 'Variable X1')
plt.ylabel("Variable X2")
Text(54.96562499999998, 0.5, 'Variable X2')
plt.show()

9.5.2) kNN con clases balanceadas y validación cruzada



# 1. Generar datos desbalanceados
x1 = np.concatenate([np.random.normal(2, 1, 160), np.random.normal(5, 1, 40)])
x2 = np.concatenate([np.random.normal(2, 1, 160), np.random.normal(5, 1, 40)])
clase = np.array(["A"]*160 + ["B"]*40)

datos = pd.DataFrame({"x1": x1, "x2": x2, "clase": clase})

# 2. Pipeline con SMOTE (oversampling) + kNN
pipeline = Pipeline([
    ("smote", SMOTE(random_state=123)),
    ("knn", KNeighborsClassifier())
])

# 3. Validación cruzada para elegir k óptimo
param_grid = {"knn__n_neighbors": range(1, 21)}
grid_search = GridSearchCV(pipeline, param_grid, cv=10, scoring="accuracy")
grid_search.fit(datos[["x1", "x2"]], datos["clase"])
GridSearchCV(cv=10,
             estimator=Pipeline(steps=[('smote', SMOTE(random_state=123)),
                                       ('knn', KNeighborsClassifier())]),
             param_grid={'knn__n_neighbors': range(1, 21)}, scoring='accuracy')
mejor_k = grid_search.best_params_["knn__n_neighbors"]
print("Mejor valor de k:", mejor_k)
Mejor valor de k: 6
# 4. Visualización de frontera
xx, yy = np.meshgrid(
    np.linspace(datos.x1.min()-1, datos.x1.max()+1, 200),
    np.linspace(datos.x2.min()-1, datos.x2.max()+1, 200)
)
grid = np.c_[xx.ravel(), yy.ravel()]
clase_pred = grid_search.predict(grid)
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/base.py:493: UserWarning:

X does not have valid feature names, but KNeighborsClassifier was fitted with feature names
plt.contourf(xx, yy, clase_pred.reshape(xx.shape), alpha=0.3, cmap="coolwarm")
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 3190;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py]8;;", line 3190, in contourf
    __ret = gca().contourf(
            ^^^^^^^^^^^^^^^
  File "]8;line = 1524;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py]8;;", line 1524, in inner
    return func(
           ^^^^^
  File "]8;line = 6797;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py]8;;", line 6797, in contourf
    contours = mcontour.QuadContourSet(self, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 701;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 701, in __init__
    kwargs = self._process_args(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1319;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1319, in _process_args
    x, y, z = self._contour_args(args, kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1363;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1363, in _contour_args
    z = ma.masked_invalid(z, copy=False)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 2360;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py]8;;", line 2360, in masked_invalid
    res = masked_where(~(np.isfinite(a)), a, copy=copy)
                         ^^^^^^^^^^^^^^
TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
g
plt.scatter(datos.x1, datos.x2, c=(datos.clase=="A"), cmap="coolwarm", edgecolor="k", s=50)
<matplotlib.collections.PathCollection object at 0x35453c450>
plt.title(f"Clasificación con kNN balanceado (k = {mejor_k})")
Text(0.5, 1.0, 'Clasificación con kNN balanceado (k = 6)')
plt.xlabel("Variable X1")
plt.ylabel("Variable X2")
Text(0.5, 6.280995055624219, 'Variable X1')
Text(54.96562499999998, 0.5, 'Variable X2')
plt.show()

9.5.3) Clasificación Bayesiana (Naive Bayes)


# 1. Generar datos simulados
x1 = np.concatenate([np.random.normal(2, 1, n//2), np.random.normal(5, 1.2, n//2)])
x2 = np.concatenate([np.random.normal(3, 1, n//2), np.random.normal(6, 1.2, n//2)])
clase = np.array(["A"]*(n//2) + ["B"]*(n//2))

datos = pd.DataFrame({"x1": x1, "x2": x2, "clase": clase})

# 2. Entrenar modelo Naive Bayes
nb = GaussianNB()
nb.fit(datos[["x1", "x2"]], datos["clase"])
GaussianNB()
# 3. Frontera de decisión
xx, yy = np.meshgrid(
    np.linspace(datos.x1.min()-1, datos.x1.max()+1, 200),
    np.linspace(datos.x2.min()-1, datos.x2.max()+1, 200)
)
grid = np.c_[xx.ravel(), yy.ravel()]
clase_pred = nb.predict(grid)
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/base.py:493: UserWarning:

X does not have valid feature names, but GaussianNB was fitted with feature names
# 4. Visualización
plt.contourf(xx, yy, clase_pred.reshape(xx.shape), alpha=0.3, cmap="coolwarm")
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 3190;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py]8;;", line 3190, in contourf
    __ret = gca().contourf(
            ^^^^^^^^^^^^^^^
  File "]8;line = 1524;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py]8;;", line 1524, in inner
    return func(
           ^^^^^
  File "]8;line = 6797;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py]8;;", line 6797, in contourf
    contours = mcontour.QuadContourSet(self, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 701;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 701, in __init__
    kwargs = self._process_args(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1319;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1319, in _process_args
    x, y, z = self._contour_args(args, kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1363;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1363, in _contour_args
    z = ma.masked_invalid(z, copy=False)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 2360;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py]8;;", line 2360, in masked_invalid
    res = masked_where(~(np.isfinite(a)), a, copy=copy)
                         ^^^^^^^^^^^^^^
TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
g
plt.scatter(datos.x1, datos.x2, c=(datos.clase=="A"), cmap="coolwarm", edgecolor="k", s=50)
<matplotlib.collections.PathCollection object at 0x34bba8a90>
plt.title("Clasificación Bayesiana (Naive Bayes)")
Text(0.5, 1.0, 'Clasificación Bayesiana (Naive Bayes)')
plt.xlabel("Variable X1")
plt.ylabel("Variable X2")
Text(0.5, 6.280995055624219, 'Variable X1')
Text(54.96562499999998, 0.5, 'Variable X2')
plt.show()

9.5.4) Regresión Logística


# 1. Generar datos simulados
np.random.seed(123)
x1 = np.random.uniform(0, 10, n)
x2 = np.random.uniform(0, 10, n)
prob = 1 / (1 + np.exp(-(-5 + 0.8*x1 + 0.6*x2)))
y = np.random.binomial(1, prob, n)
clase = np.where(y == 1, "Sí", "No")

datos = pd.DataFrame({"x1": x1, "x2": x2, "clase": clase})

# 2. Ajustar modelo logístico
logit = LogisticRegression()
logit.fit(datos[["x1", "x2"]], datos["clase"])
LogisticRegression()
# 3. Crear grilla y predecir probabilidades
xx, yy = np.meshgrid(
    np.linspace(datos.x1.min(), datos.x1.max(), 200),
    np.linspace(datos.x2.min(), datos.x2.max(), 200)
)
grid = np.c_[xx.ravel(), yy.ravel()]
prob_grid = logit.predict_proba(grid)[:, 1]
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/base.py:493: UserWarning:

X does not have valid feature names, but LogisticRegression was fitted with feature names
clase_pred = logit.predict(grid)
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/base.py:493: UserWarning:

X does not have valid feature names, but LogisticRegression was fitted with feature names
# 4. Visualización
plt.contourf(xx, yy, clase_pred.reshape(xx.shape), alpha=0.3, cmap="coolwarm")
G3;Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "]8;line = 3190;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/pyplot.py]8;;", line 3190, in contourf
    __ret = gca().contourf(
            ^^^^^^^^^^^^^^^
  File "]8;line = 1524;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/__init__.py]8;;", line 1524, in inner
    return func(
           ^^^^^
  File "]8;line = 6797;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/axes/_axes.py]8;;", line 6797, in contourf
    contours = mcontour.QuadContourSet(self, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 701;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 701, in __init__
    kwargs = self._process_args(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1319;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1319, in _process_args
    x, y, z = self._contour_args(args, kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 1363;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/matplotlib/contour.py]8;;", line 1363, in _contour_args
    z = ma.masked_invalid(z, copy=False)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "]8;line = 2360;file:///opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/numpy/ma/core.py]8;;", line 2360, in masked_invalid
    res = masked_where(~(np.isfinite(a)), a, copy=copy)
                         ^^^^^^^^^^^^^^
TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
g
plt.scatter(datos.x1, datos.x2, c=(datos.clase=="Sí"), cmap="coolwarm", edgecolor="k", s=50)
plt.title("Clasificación con Regresión Logística")
<matplotlib.collections.PathCollection object at 0x354576250>
Text(0.5, 1.0, 'Clasificación con Regresión Logística')
plt.xlabel("Variable X1")
Text(0.5, 6.280995055624219, 'Variable X1')
plt.ylabel("Variable X2")
Text(54.96562499999998, 0.5, 'Variable X2')
plt.show()

9.5.5) Análisis Discriminante (LDA con iris)


# 1. Dataset iris (2 variables para graficar)
iris = load_iris(as_frame=True)
datos = iris.data[["sepal length (cm)", "sepal width (cm)"]]
clases = iris.target

# 2. Entrenar modelo LDA
lda = LinearDiscriminantAnalysis()
lda.fit(datos, clases)
LinearDiscriminantAnalysis()
# 3. Crear grilla
xx, yy = np.meshgrid(
    np.linspace(datos.iloc[:,0].min()-0.5, datos.iloc[:,0].max()+0.5, 200),
    np.linspace(datos.iloc[:,1].min()-0.5, datos.iloc[:,1].max()+0.5, 200)
)
grid = np.c_[xx.ravel(), yy.ravel()]
clase_pred = lda.predict(grid)
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/base.py:493: UserWarning:

X does not have valid feature names, but LinearDiscriminantAnalysis was fitted with feature names
# 4. Visualización
plt.contourf(xx, yy, clase_pred.reshape(xx.shape), alpha=0.3, cmap="coolwarm")
<matplotlib.contour.QuadContourSet object at 0x34a040e90>
plt.scatter(datos.iloc[:,0], datos.iloc[:,1], c=clases, cmap="coolwarm", edgecolor="k", s=50)
<matplotlib.collections.PathCollection object at 0x34b4fd210>
plt.title("Análisis Discriminante Lineal (LDA) con iris")
Text(0.5, 1.0, 'Análisis Discriminante Lineal (LDA) con iris')
plt.xlabel("Sepal Length")
Text(0.5, 6.280995055624219, 'Sepal Length')
plt.ylabel("Sepal Width")
Text(54.96562499999998, 0.5, 'Sepal Width')
plt.show()

10) Regresión lineal múltiple y regularización

La regresión lineal múltiple es una extensión de la regresión lineal simple que permite modelar la relación entre una variable dependiente \(y\) y varias variables independientes \(x_1, x_2, \dots, x_p\). La idea es estimar cómo cada predictor influye en \(y\), controlando por los demás. Matemáticamente se expresa como:

\[y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \dots + \beta_p x_p + \varepsilon\]

donde \(\beta_0\) es la intersección, \(\beta_j\) son los coeficientes de regresión y \(\varepsilon\) es el término de error.

El problema aparece cuando hay multicolinealidad (correlación alta entre predictores) o cuando el número de variables es muy grande en comparación con el número de observaciones. En estos casos, los estimadores \(\hat{\beta}\) pueden ser inestables y generar modelos poco interpretables o con sobreajuste. Aquí es donde entra la regularización.

La regularización agrega un término de penalización a la función de pérdida, con el fin de reducir la complejidad del modelo. Dos métodos comunes son Ridge y Lasso. Ridge utiliza la norma cuadrática \(L_2\) para penalizar grandes valores de los coeficientes:

\[\text{RSS} + \lambda \sum_{j=1}^{p} \beta_j^2\]

mientras que Lasso utiliza la norma absoluta \(L_1\):

\[\text{RSS} + \lambda \sum_{j=1}^{p} |\beta_j|\]

donde \(\lambda\) es un hiperparámetro que controla el nivel de penalización.

Estos métodos sirven tanto para mejorar la capacidad predictiva como para seleccionar variables (especialmente Lasso, que puede llevar algunos coeficientes exactamente a cero). Sus aplicaciones incluyen predicción en economía, biología (expresión genética), marketing (análisis de clientes) y cualquier escenario con muchos predictores, donde se busca un modelo más robusto y generalizable.

Ejemplo con R

quit

set.seed(123)

# 1. Generar datos simulados
n <- 100
p <- 10
X <- matrix(rnorm(n * p), n, p)               # 10 predictores
beta <- c(3, -2, 0, 0, 1.5, rep(0, p - 5))    # solo 3 predictores relevantes
y <- X %*% beta + rnorm(n)                    # variable respuesta con ruido

# 2. Ajustar modelos con glmnet
# alpha = 0 -> Ridge, alpha = 1 -> Lasso
ridge_fit <- glmnet(X, y, alpha = 0)  
lasso_fit <- glmnet(X, y, alpha = 1)

# 3. Visualización: trayectoria de coeficientes
par(mfrow = c(1, 2))   # Dos gráficos lado a lado

plot(ridge_fit, xvar = "lambda", main = "Trayectoria Ridge (L2)")
plot(lasso_fit, xvar = "lambda", main = "Trayectoria Lasso (L1)")

10.1) Regresión lineal simple

La regresión lineal simple es un modelo estadístico que busca describir la relación entre dos variables: una independiente o explicativa (\(X\)) y una dependiente o de respuesta (\(Y\)). Su objetivo es estimar cómo cambia \(Y\) en función de los valores de \(X\). Matemáticamente, se expresa como:

\[Y = \beta_0 + \beta_1 X + \varepsilon\]

donde \(\beta_0\) es la intersección (ordenada al origen), \(\beta_1\) es la pendiente que mide el efecto de \(X\) sobre \(Y\), y \(\varepsilon\) es el término de error aleatorio.

Este modelo sirve principalmente para explicar y predecir. Explica al identificar si existe una relación significativa entre dos variables, y predice al permitir estimar el valor esperado de \(Y\) para un determinado valor de \(X\). Por ejemplo, si \(X\) es la cantidad de horas de estudio y \(Y\) es la calificación en un examen, la regresión lineal simple puede mostrar qué tanto influyen las horas de estudio en los resultados.

Entre los casos de uso más comunes están: economía (relación entre ingreso y consumo), salud (efecto de una dosis de medicamento sobre una respuesta clínica), educación (impacto de la asistencia a clases en el rendimiento), e ingeniería (relación entre la temperatura y la resistencia de un material). En todos estos casos, lo importante es que haya una relación que se pueda aproximar de manera lineal.

En la práctica, la regresión lineal simple se estima mediante el método de mínimos cuadrados ordinarios (MCO), que consiste en encontrar los valores de \(\hat{\beta}_0\) y \(\hat{\beta}_1\) que minimicen la suma de los errores cuadráticos:

\[\min \sum_{i=1}^n (y_i - \hat{y}_i)^2 \;=\; \min \sum_{i=1}^n \left(y_i - (\hat{\beta}_0 + \hat{\beta}_1 x_i)\right)^2\] Este procedimiento garantiza la mejor recta de ajuste posible bajo los supuestos del modelo (linealidad, independencia, homocedasticidad y normalidad de errores).

Ejemplo con R



# Simulación de datos: Horas de estudio (X) y Nota de examen (Y)
set.seed(123)
horas <- runif(50, 0, 10)  # 50 observaciones entre 0 y 10 horas
nota <- 40 + 5 * horas + rnorm(50, 0, 5)  # Relación lineal con algo de ruido
datos <- data.frame(horas, nota)

# Ajuste del modelo de regresión lineal simple
modelo <- lm(nota ~ horas, data = datos)

# Resumen "limpio" del modelo
tidy(modelo)

# Gráfico con recta de regresión
p <- ggplot(datos, aes(x = horas, y = nota)) +
  geom_point(color = "steelblue", size = 3, alpha = 0.7) +   # puntos
  geom_smooth(method = "lm", se = TRUE, color = "red", size = 1) + # recta
  stat_regline_equation(                                   # ecuación en gráfico
    aes(label = paste(..eq.label.., ..rr.label.., sep = "~~~")),
    formula = y ~ x
  ) +
  labs(
    title = "Ejemplo de Regresión Lineal Simple",
    x = "Horas de estudio",
    y = "Nota en el examen"
  ) +
  theme_minimal(base_size = 14)

print(p)

10.2) Regresión lineal múltiple

La regresión lineal múltiple es una extensión de la regresión lineal simple que permite analizar la relación entre una variable dependiente \(Y\) y varias variables independientes \(X_1, X_2, \dots, X_p\). Su forma general es:

\[Y = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \dots + \beta_p X_p + \varepsilon \]

donde \(\beta_0\) es la constante, \(\beta_j\) los coeficientes que representan el efecto parcial de cada \(X_j\) sobre \(Y\), y \(\varepsilon\) el error aleatorio.

Este modelo sirve para explicar, cuantificar y predecir fenómenos más complejos que no dependen de una sola variable. Cada coeficiente \(\beta_j\) indica cuánto cambia en promedio \(Y\) al variar \(X_j\) en una unidad, manteniendo constantes las demás variables. De esta forma, se pueden identificar los factores más influyentes y cómo interactúan entre sí.

Los casos de uso son amplios: en economía, predecir el consumo a partir de ingreso, tasa de interés y desempleo; en salud, explicar la presión arterial según edad, peso y nivel de actividad física; en marketing, estimar las ventas a partir de inversión en publicidad, precio y competencia; en ingeniería, modelar la resistencia de un material en función de temperatura, humedad y presión.

El modelo se estima generalmente con mínimos cuadrados ordinarios (MCO), minimizando la suma de los errores al cuadrado:

\[\min \sum_{i=1}^n \left( y_i - \left( \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \dots + \beta_p x_{ip} \right) \right)^2 \] Bajo supuestos como linealidad, independencia, homocedasticidad y normalidad de los errores, el estimador obtenido es insesgado y eficiente, lo que hace de la regresión múltiple una herramienta fundamental en estadística aplicada.

Ejemplo con R



# Dataset
data(mtcars)

# Ajuste del modelo: mpg explicado por peso (wt) y potencia (hp)
modelo <- lm(mpg ~ wt + hp, data = mtcars)

# Resumen "limpio" del modelo
tidy(modelo)

# Diagnóstico de multicolinealidad
vif(modelo)
      wt       hp 
1.766625 1.766625 
# Gráfico: dispersión 3D proyectada (mpg ~ wt + hp)
ggplot(mtcars, aes(x = wt, y = mpg, color = hp)) +
  geom_point(size = 3, alpha = 0.8) +
  geom_smooth(method = "lm", se = FALSE, aes(group = 1), color = "red") +
  scale_color_gradient(low = "skyblue", high = "darkblue") +
  labs(
    title = "Regresión Lineal Múltiple: mpg ~ wt + hp",
    x = "Peso del vehículo (1000 lbs)",
    y = "Millas por galón (mpg)",
    color = "Potencia (hp)"
  ) +
  theme_minimal(base_size = 14)


# Pares de relaciones entre variables (útil para explorar el modelo)
ggpairs(mtcars[, c("mpg", "wt", "hp")])

Mejor forma de elegir los predictores que aportan mas a el modelo

La mejor forma de elegir predictores que más aportan a un modelo se conoce como selección de variables o selección de características. El objetivo es identificar qué subconjunto de variables explicativas \(X_1, X_2, \dots, X_p\) tiene mayor poder explicativo sobre la variable respuesta \(Y\). Esto ayuda a mejorar la interpretabilidad, reducir la complejidad del modelo y, en muchos casos, aumentar su capacidad de generalización.

Existen varias estrategias: la selección hacia adelante (añadiendo variables una a una), la selección hacia atrás (eliminando predictores irrelevantes), y la selección exhaustiva, que evalúa todos los subconjuntos posibles de predictores. Los criterios más usados para elegir el mejor subconjunto son medidas como \(R^2\), \(R^2\) ajustado, \(C_p\) de Mallows, AIC y BIC.

Formalmente, se busca encontrar el subconjunto de variables \(S \subseteq {1,\dots,p}\) que minimiza una medida de error de ajuste: \[S^{*} = \arg\min_{S} \; \text{AIC}(S)\] y \[S^{*} = \arg\min_{S} \; \text{BIC}(S)\]

donde \(\text{AIC}\) y \(\text{BIC}\) son criterios de información que equilibran el ajuste y la complejidad del modelo.

Los casos de uso son amplios: en economía, seleccionar indicadores clave para un modelo de crecimiento; en salud, identificar factores de riesgo más relevantes para una enfermedad; en marketing, escoger las variables de campañas que más explican las ventas; y en machine learning, reducir la dimensionalidad de datos para evitar sobreajuste.

Ejemplo con R



# Dataset: mtcars, predecir mpg a partir de varias variables
data(mtcars)

# Selección de subconjuntos con leaps
ajuste <- regsubsets(mpg ~ wt + hp + disp + cyl + drat + qsec, 
                     data = mtcars, 
                     nbest = 1,   # mejor subconjunto por tamaño
                     nvmax = 6)   # máximo número de variables

# Resumen de resultados
res <- summary(ajuste)

# Visualización del R^2 ajustado
df_plot <- data.frame(
  Num_Variables = 1:length(res$adjr2),
  Adj_R2 = res$adjr2
)

ggplot(df_plot, aes(x = Num_Variables, y = Adj_R2)) +
  geom_line(color = "steelblue", size = 1.2) +
  geom_point(color = "red", size = 3) +
  labs(
    title = "Selección de variables con leaps",
    x = "Número de variables en el modelo",
    y = "R² Ajustado"
  ) +
  theme_minimal(base_size = 14)

10.3) Regularización

La regularización es una técnica utilizada en modelos estadísticos y de machine learning, especialmente en la regresión lineal múltiple, para evitar el sobreajuste (overfitting). El sobreajuste ocurre cuando un modelo se adapta demasiado a los datos de entrenamiento, perdiendo capacidad de generalización. La idea central de la regularización es añadir un término de penalización a la función de error, de modo que los coeficientes \(\beta_j\) no crezcan demasiado.

Existen dos métodos clásicos: Ridge (L2) y Lasso (L1). En Ridge, la penalización es proporcional a la suma de los cuadrados de los coeficientes:

\[\min \sum_{i=1}^n \left(y_i - \hat{y}_i\right)^2 + \lambda \sum_{j=1}^p \beta_j^2\]

En Lasso, la penalización es proporcional a la suma del valor absoluto de los coeficientes:

\[\min \sum_{i=1}^n \left(y_i - \hat{y}_i\right)^2 + \lambda \sum_{j=1}^p \left|\beta_j\right| \] donde \(\lambda \geq 0\) es un hiperparámetro que controla la fuerza de la penalización.

La regularización sirve para mejorar la estabilidad del modelo y seleccionar variables relevantes. En Ridge, los coeficientes se reducen pero nunca son exactamente cero; en Lasso, algunos coeficientes pueden volverse exactamente cero, funcionando además como método de selección de variables.

Los casos de uso incluyen modelos con muchas variables (p.ej., en genómica, texto, marketing digital), problemas de multicolinealidad donde varias \(X_j\) están correlacionadas, y situaciones donde interesa reducir la complejidad del modelo sin perder capacidad predictiva. En práctica, la regularización ayuda a construir modelos más robustos y con mejor desempeño en datos nuevos.

Ejemplo con R



# Dataset: mtcars, variables predictoras (wt, hp, disp, cyl) para predecir mpg
x <- as.matrix(mtcars[, c("wt", "hp", "disp", "cyl")])
y <- mtcars$mpg

# Ajuste con Lasso (alpha = 1)
modelo_lasso <- glmnet(x, y, alpha = 1)

# Ajuste con Ridge (alpha = 0)
modelo_ridge <- glmnet(x, y, alpha = 0)

# Visualización de trayectorias de coeficientes (path plot)
par(mfrow = c(1,2))
plot(modelo_lasso, xvar = "lambda", label = TRUE, main = "Lasso (L1)")
plot(modelo_ridge, xvar = "lambda", label = TRUE, main = "Ridge (L2)")

10.4) Regularización Ridge

La regularización Ridge, también llamada regresión de cresta, es una técnica de regresión lineal regularizada que busca mejorar la estabilidad y capacidad de predicción del modelo cuando existen muchas variables o problemas de multicolinealidad. A diferencia de la regresión lineal tradicional, Ridge añade un término de penalización cuadrático a la función de error, lo que impide que los coeficientes \(\beta_j\) crezcan demasiado.

Formalmente, el problema de Ridge se plantea como:

\[\min \sum_{i=1}^n \left( y_i - \hat{y}_i \right)^2 + \lambda \sum_{j=1}^p \left| \beta_j \right| \] donde \(\lambda \geq 0\) es el hiperparámetro de regularización. A mayor \(\lambda\), más fuerte es la penalización, y más se reducen los coeficientes hacia cero, aunque nunca llegan a ser exactamente cero.

Ridge sirve para mejorar la generalización del modelo, reduciendo la varianza de los estimadores a costa de introducir un pequeño sesgo. Es útil en contextos donde hay muchas variables predictoras y correlaciones altas entre ellas, ya que controla la inestabilidad de los coeficientes.

Los casos de uso son comunes en problemas de predicción con alta dimensionalidad: análisis de datos genómicos, series de tiempo con múltiples predictores, modelos de riesgo en finanzas y aplicaciones de machine learning donde se requiere un modelo robusto sin sobreajuste.

Ejemplo con R



# Dataset: mtcars, variables predictoras para predecir mpg
x <- as.matrix(mtcars[, c("wt", "hp", "disp", "cyl")])
y <- mtcars$mpg

# Ajuste con Ridge (alpha = 0)
modelo_ridge <- glmnet(x, y, alpha = 0)

# Validación cruzada para elegir el mejor lambda
cv_ridge <- cv.glmnet(x, y, alpha = 0)

# Mejor valor de lambda
cv_ridge$lambda.min
[1] 0.5146981
# Gráfico de trayectorias de coeficientes
plot(modelo_ridge, xvar = "lambda", label = TRUE, main = "Trayectoria de coeficientes (Ridge)")


# Gráfico de validación cruzada
plot(cv_ridge)

10.5) Regularización Lazo

La regularización Lasso (Least Absolute Shrinkage and Selection Operator) es una variante de la regresión lineal regularizada que, además de controlar el sobreajuste, permite realizar selección automática de variables. Al igual que Ridge, añade un término de penalización a la función de error, pero en este caso se utiliza la norma \(L1\) en lugar de la \(L2\).

Formalmente, el problema de Lasso se define como: \[\min \sum_{i=1}^n \left( y_i - \hat{y}_i \right)^2 \]

donde \(\lambda \geq 0\) controla la intensidad de la penalización. A medida que \(\lambda\) crece, algunos coeficientes \(\beta_j\) se reducen exactamente a cero, lo que significa que esas variables quedan excluidas del modelo.

Lasso sirve no solo para evitar sobreajuste, sino también para simplificar modelos complejos, ya que selecciona automáticamente las variables más relevantes. Esto lo hace especialmente útil en contextos donde el número de predictores \(p\) es grande y se sospecha que solo un subconjunto de ellos tiene un impacto real en la variable respuesta.

Los casos de uso incluyen áreas como la bioinformática (selección de genes asociados a una enfermedad), el marketing (detectar qué variables explican mejor las ventas), la economía (selección de indicadores clave en modelos macroeconómicos) y el machine learning en general cuando se busca un modelo parsimonioso y robusto.

Ejemplo con R



# Dataset: mtcars, variables predictoras para predecir mpg
x <- as.matrix(mtcars[, c("wt", "hp", "disp", "cyl")])
y <- mtcars$mpg

# Ajuste con Lasso (alpha = 1)
modelo_lasso <- glmnet(x, y, alpha = 1)

# Validación cruzada para elegir el mejor lambda
cv_lasso <- cv.glmnet(x, y, alpha = 1)

# Mejor valor de lambda
cv_lasso$lambda.min
[1] 0.2176783
# Gráfico de trayectorias de coeficientes
plot(modelo_lasso, xvar = "lambda", label = TRUE, main = "Trayectoria de coeficientes (Lasso)")


# Gráfico de validación cruzada
plot(cv_lasso)

10.6) Elastic Net

El Elastic Net es una técnica de regularización que combina lo mejor de Ridge (L2) y Lasso (L1). Su objetivo es controlar el sobreajuste y mejorar la capacidad predictiva de los modelos de regresión cuando hay muchas variables o fuerte correlación entre ellas. La función de costo incluye ambos términos de penalización:

\[\min \sum_{i=1}^n \left( y_i - \hat{y}_i \right)^2 + \lambda \left( \alpha \sum_{j=1}^p \left| \beta_j \right| + (1-\alpha) \sum_{j=1}^p \beta_j^2 \right)\]

donde \(\lambda \geq 0\) regula la fuerza de la penalización y \(0 \leq \alpha \leq 1\) controla la mezcla entre Lasso y Ridge.

Sirve para evitar el sobreajuste, manejar multicolinealidad y a la vez realizar una selección parcial de variables. Cuando \(\alpha = 1\), Elastic Net se comporta como Lasso; cuando \(\alpha = 0\), se comporta como Ridge; y para valores intermedios combina sus efectos.

Los casos de uso más comunes incluyen problemas con datos de alta dimensión (\(p \gg n\)), como en bioinformática (selección de genes), análisis de texto (miles de variables de palabras), predicción financiera con muchos indicadores correlacionados, y modelos de marketing digital donde hay que filtrar predictores relevantes de grandes bases de datos.

En la práctica, Elastic Net es especialmente útil cuando hay grupos de variables altamente correlacionadas: Lasso tendería a escoger solo una de ellas, mientras que Elastic Net puede retener el grupo completo, logrando un modelo más estable y robusto.

Ejemplo grafico con R



# Dataset: mtcars, variables predictoras para predecir mpg
x <- as.matrix(mtcars[, c("wt", "hp", "disp", "cyl")])
y <- mtcars$mpg

# Ajuste con Elastic Net (alpha = 0.5 mezcla de Lasso y Ridge)
modelo_en <- glmnet(x, y, alpha = 0.5)

# Validación cruzada para elegir el mejor lambda
cv_en <- cv.glmnet(x, y, alpha = 0.5)

# Mejor valor de lambda
cv_en$lambda.min
[1] 0.2491271
# Gráfico de trayectorias de coeficientes
plot(modelo_en, xvar = "lambda", label = TRUE, main = "Trayectoria de coeficientes (Elastic Net)")


# Gráfico de validación cruzada
plot(cv_en)

10.7) Ejemplo con Python

10.7.1) Regresión lineal simple

reticulate::repl_python()
# Simulación de datos: Horas de estudio (X) y Nota de examen (Y)
np.random.seed(123)
horas = np.random.uniform(0, 10, 50)
nota = 40 + 5 * horas + np.random.normal(0, 5, 50)

# DataFrame
datos = pd.DataFrame({"horas": horas, "nota": nota})

# Ajuste con scikit-learn
X = datos[["horas"]]
y = datos["nota"]

modelo = LinearRegression()
modelo.fit(X, y)
LinearRegression()
print("Intercepto:", modelo.intercept_)
Intercepto: 41.35831788498974
print("Coeficiente:", modelo.coef_[0])
Coeficiente: 4.834232181386337
print("R²:", modelo.score(X, y))
R²: 0.8043944991100573
# Visualización
sns.lmplot(x="horas", y="nota", data=datos, ci=95, line_kws={"color": "red"})
plt.title("Ejemplo de Regresión Lineal Simple", fontsize=14)
<seaborn.axisgrid.FacetGrid object at 0x34b51e410>
plt.show()
Text(0.5, 1.0, 'Ejemplo de Regresión Lineal Simple')

10.7.2) Regresión lineal múltiple

# Dataset mtcars en Python (se carga desde seaborn)
mtcars = sns.load_dataset("mpg").dropna()

# Variables predictoras: peso (weight) y potencia (horsepower)
X = mtcars[["weight", "horsepower"]]
y = mtcars["mpg"]

# Ajuste del modelo
modelo = LinearRegression()
modelo.fit(X, y)
LinearRegression()
print("Intercepto:", modelo.intercept_)
Intercepto: 45.64021084017712
print("Coeficientes:", dict(zip(X.columns, modelo.coef_)))
Coeficientes: {'weight': -0.005794157364802919, 'horsepower': -0.04730286308619168}
print("R²:", modelo.score(X, y))
R²: 0.7063752737298348
# Diagnóstico con statsmodels
X_sm = sm.add_constant(X)
modelo_sm = sm.OLS(y, X_sm).fit()
print(modelo_sm.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    mpg   R-squared:                       0.706
Model:                            OLS   Adj. R-squared:                  0.705
Method:                 Least Squares   F-statistic:                     467.9
Date:                Thu, 11 Sep 2025   Prob (F-statistic):          3.06e-104
Time:                        21:15:33   Log-Likelihood:                -1121.0
No. Observations:                 392   AIC:                             2248.
Df Residuals:                     389   BIC:                             2260.
Df Model:                           2                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         45.6402      0.793     57.540      0.000      44.081      47.200
weight        -0.0058      0.001    -11.535      0.000      -0.007      -0.005
horsepower    -0.0473      0.011     -4.267      0.000      -0.069      -0.026
==============================================================================
Omnibus:                       35.336   Durbin-Watson:                   0.858
Prob(Omnibus):                  0.000   Jarque-Bera (JB):               45.973
Skew:                           0.683   Prob(JB):                     1.04e-10
Kurtosis:                       3.974   Cond. No.                     1.15e+04
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.15e+04. This might indicate that there are
strong multicollinearity or other numerical problems.
# Visualización
plt.figure(figsize=(8,6))
<Figure size 800x600 with 0 Axes>
sns.scatterplot(x=mtcars["weight"], y=mtcars["mpg"], hue=mtcars["horsepower"], palette="Blues", s=80)
sns.regplot(x="weight", y="mpg", data=mtcars, scatter=False, color="red")
<Axes: xlabel='weight', ylabel='mpg'>
<Axes: xlabel='weight', ylabel='mpg'>
plt.title("Regresión Lineal Múltiple: mpg ~ weight + horsepower", fontsize=14)
Text(0.5, 1.0, 'Regresión Lineal Múltiple: mpg ~ weight + horsepower')
plt.show()

10.7.3) Regularización (Ridge y Lasso)

quit
# Dataset: mtcars con varias variables predictoras
X = mtcars[["weight", "horsepower", "displacement", "cylinders"]]
y = mtcars["mpg"]

# Ridge
ridge = Ridge(alpha=1.0)
G1;H1;Errorh in Ridge(alpha = 1) : could not find function "Ridge"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
ridge.fit(X, y)
G1;H1;Errorh in ridge.fit(X, y) : could not find function "ridge.fit"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
# Lasso
lasso = Lasso(alpha=0.1)
G1;H1;Errorh in Lasso(alpha = 0.1) : could not find function "Lasso"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
lasso.fit(X, y)
G1;H1;Errorh in lasso.fit(X, y) : could not find function "lasso.fit"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
print("Coeficientes Ridge:", dict(zip(X.columns, ridge.coef_)))
G1;H1;Errorh: object 'ridge.coef_' not found
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
print("Coeficientes Lasso:", dict(zip(X.columns, lasso.coef_)))
G1;H1;Errorh: object 'lasso.coef_' not found
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
# Trayectoria de coeficientes para Lasso
alphas = np.logspace(-3, 2, 100)
G1;H1;Errorh in np.logspace(-3, 2, 100) : could not find function "np.logspace"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
coefs = []
G1;H1;Errorh: unexpected '[' in "coefs = ["
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
for a in alphas:
G1;H1;Errorh: unexpected symbol in "for a"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
    lasso = Lasso(alpha=a, max_iter=5000)
G1;H1;Errorh in Lasso(alpha = a, max_iter = 5000) : 
  could not find function "Lasso"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
    lasso.fit(X, y)
G1;H1;Errorh in lasso.fit(X, y) : could not find function "lasso.fit"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
    coefs.append(lasso.coef_)
G1;H1;Errorh in coefs.append(lasso.coef_) : 
  could not find function "coefs.append"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
plt.figure(figsize=(8,6))
G1;H1;Errorh: unexpected ',' in "plt.figure(figsize=(8,"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
ax = plt.gca()
G1;H1;Errorh in plt.gca() : could not find function "plt.gca"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
ax.plot(alphas, coefs)
G1;H1;Errorh in ax.plot(alphas, coefs) : could not find function "ax.plot"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
ax.set_xscale("log")
G1;H1;Errorh in ax.set_xscale("log") : could not find function "ax.set_xscale"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
plt.xlabel("Lambda (α)")
G1;H1;Errorh in plt.xlabel("Lambda (α)") : could not find function "plt.xlabel"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
plt.ylabel("Coeficientes")
G1;H1;Errorh in plt.ylabel("Coeficientes") : 
  could not find function "plt.ylabel"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
plt.title("Trayectoria de coeficientes (Lasso)")
G1;H1;Errorh in plt.title("Trayectoria de coeficientes (Lasso)") : 
  could not find function "plt.title"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g
plt.show()
G1;H1;Errorh in plt.show() : could not find function "plt.show"
Error during wrapup: not that many frames on the stack
Error: no more error handlers available (recursive errors?); invoking 'abort' restart
g

10.7.4) Regularización Ridge con validación cruzada

reticulate::repl_python()

# Ridge con validación cruzada
alphas = np.logspace(-3, 3, 100)
ridge_cv = RidgeCV(alphas=alphas, store_cv_values=True)
ridge_cv.fit(X, y)
/opt/anaconda3/envs/py311_arm/lib/python3.11/site-packages/sklearn/linear_model/_ridge.py:2341: FutureWarning:

'store_cv_values' is deprecated in version 1.5 and will be removed in 1.7. Use 'store_cv_results' instead.
RidgeCV(alphas=array([1.00000000e-03, 1.14975700e-03, 1.32194115e-03, 1.51991108e-03,
       1.74752840e-03, 2.00923300e-03, 2.31012970e-03, 2.65608778e-03,
       3.05385551e-03, 3.51119173e-03, 4.03701726e-03, 4.64158883e-03,
       5.33669923e-03, 6.13590727e-03, 7.05480231e-03, 8.11130831e-03,
       9.32603347e-03, 1.07226722e-02, 1.23284674e-02, 1.41747416e-02,
       1.62975083e-02, 1.87381742e-0...
       4.03701726e+01, 4.64158883e+01, 5.33669923e+01, 6.13590727e+01,
       7.05480231e+01, 8.11130831e+01, 9.32603347e+01, 1.07226722e+02,
       1.23284674e+02, 1.41747416e+02, 1.62975083e+02, 1.87381742e+02,
       2.15443469e+02, 2.47707636e+02, 2.84803587e+02, 3.27454916e+02,
       3.76493581e+02, 4.32876128e+02, 4.97702356e+02, 5.72236766e+02,
       6.57933225e+02, 7.56463328e+02, 8.69749003e+02, 1.00000000e+03]),
        store_cv_values=True)
print("Mejor lambda (α):", ridge_cv.alpha_)
print("Coeficientes Ridge:", dict(zip(X.columns, ridge_cv.coef_)))
Mejor lambda (α): 1000.0
Coeficientes Ridge: {'weight': -0.005806656046885905, 'horsepower': -0.04698330676418083}
# Gráfico de coeficientes vs lambda
coefs = []
for a in alphas:
    ridge = Ridge(alpha=a)
    ridge.fit(X, y)
    coefs.append(ridge.coef_)
    
plt.figure(figsize=(8,6))
Ridge(alpha=0.001)
Ridge(alpha=0.0011497569953977356)
Ridge(alpha=0.0013219411484660286)
Ridge(alpha=0.0015199110829529332)
Ridge(alpha=0.001747528400007683)
Ridge(alpha=0.002009233002565048)
Ridge(alpha=0.0023101297000831605)
Ridge(alpha=0.0026560877829466868)
Ridge(alpha=0.0030538555088334154)
Ridge(alpha=0.003511191734215131)
Ridge(alpha=0.004037017258596553)
Ridge(alpha=0.004641588833612782)
Ridge(alpha=0.005336699231206312)
Ridge(alpha=0.006135907273413176)
Ridge(alpha=0.007054802310718645)
Ridge(alpha=0.008111308307896872)
Ridge(alpha=0.0093260334688322)
Ridge(alpha=0.010722672220103232)
Ridge(alpha=0.012328467394420659)
Ridge(alpha=0.014174741629268055)
Ridge(alpha=0.016297508346206444)
Ridge(alpha=0.01873817422860384)
Ridge(alpha=0.021544346900318846)
Ridge(alpha=0.024770763559917114)
Ridge(alpha=0.02848035868435802)
Ridge(alpha=0.03274549162877728)
Ridge(alpha=0.037649358067924674)
Ridge(alpha=0.04328761281083059)
Ridge(alpha=0.049770235643321115)
Ridge(alpha=0.05722367659350217)
Ridge(alpha=0.06579332246575682)
Ridge(alpha=0.07564633275546291)
Ridge(alpha=0.08697490026177834)
Ridge(alpha=0.1)
Ridge(alpha=0.11497569953977356)
Ridge(alpha=0.13219411484660287)
Ridge(alpha=0.1519911082952933)
Ridge(alpha=0.1747528400007685)
Ridge(alpha=0.2009233002565048)
Ridge(alpha=0.23101297000831603)
Ridge(alpha=0.26560877829466867)
Ridge(alpha=0.30538555088334157)
Ridge(alpha=0.3511191734215131)
Ridge(alpha=0.40370172585965536)
Ridge(alpha=0.4641588833612782)
Ridge(alpha=0.5336699231206312)
Ridge(alpha=0.6135907273413176)
Ridge(alpha=0.7054802310718645)
Ridge(alpha=0.8111308307896873)
Ridge(alpha=0.9326033468832199)
Ridge(alpha=1.072267222010323)
Ridge(alpha=1.232846739442066)
Ridge(alpha=1.4174741629268048)
Ridge(alpha=1.629750834620645)
Ridge(alpha=1.873817422860385)
Ridge(alpha=2.1544346900318843)
Ridge(alpha=2.4770763559917115)
Ridge(alpha=2.848035868435802)
Ridge(alpha=3.2745491628777286)
Ridge(alpha=3.7649358067924674)
Ridge(alpha=4.328761281083062)
Ridge(alpha=4.9770235643321135)
Ridge(alpha=5.72236765935022)
Ridge(alpha=6.5793322465756825)
Ridge(alpha=7.56463327554629)
Ridge(alpha=8.697490026177835)
Ridge(alpha=10.0)
Ridge(alpha=11.497569953977356)
Ridge(alpha=13.219411484660288)
Ridge(alpha=15.199110829529332)
Ridge(alpha=17.47528400007683)
Ridge(alpha=20.09233002565046)
Ridge(alpha=23.10129700083158)
Ridge(alpha=26.560877829466893)
Ridge(alpha=30.538555088334185)
Ridge(alpha=35.111917342151344)
Ridge(alpha=40.37017258596558)
Ridge(alpha=46.41588833612782)
Ridge(alpha=53.366992312063125)
Ridge(alpha=61.35907273413176)
Ridge(alpha=70.54802310718645)
Ridge(alpha=81.11308307896873)
Ridge(alpha=93.26033468832199)
Ridge(alpha=107.22672220103232)
Ridge(alpha=123.28467394420659)
Ridge(alpha=141.74741629268047)
Ridge(alpha=162.97508346206433)
Ridge(alpha=187.3817422860383)
Ridge(alpha=215.44346900318865)
Ridge(alpha=247.7076355991714)
Ridge(alpha=284.8035868435805)
Ridge(alpha=327.4549162877732)
Ridge(alpha=376.49358067924715)
Ridge(alpha=432.87612810830615)
Ridge(alpha=497.7023564332114)
Ridge(alpha=572.236765935022)
Ridge(alpha=657.9332246575682)
Ridge(alpha=756.463327554629)
Ridge(alpha=869.7490026177834)
Ridge(alpha=1000.0)
<Figure size 800x600 with 0 Axes>
ax = plt.gca()
ax.plot(alphas, coefs)
[<matplotlib.lines.Line2D object at 0x34bbbe290>, <matplotlib.lines.Line2D object at 0x3587b7210>]
ax.set_xscale("log")
plt.xlabel("Lambda (α)")
plt.ylabel("Coeficientes")
Text(0.5, 0, 'Lambda (α)')
Text(0, 0.5, 'Coeficientes')
plt.title("Trayectoria de coeficientes (Ridge)")
Text(0.5, 1.0, 'Trayectoria de coeficientes (Ridge)')
plt.show()

10.7.5) Regularización Lasso con validación cruzada


# Lasso con validación cruzada
lasso_cv = LassoCV(alphas=alphas, cv=5, max_iter=5000)
lasso_cv.fit(X, y)
LassoCV(alphas=array([1.00000000e-03, 1.14975700e-03, 1.32194115e-03, 1.51991108e-03,
       1.74752840e-03, 2.00923300e-03, 2.31012970e-03, 2.65608778e-03,
       3.05385551e-03, 3.51119173e-03, 4.03701726e-03, 4.64158883e-03,
       5.33669923e-03, 6.13590727e-03, 7.05480231e-03, 8.11130831e-03,
       9.32603347e-03, 1.07226722e-02, 1.23284674e-02, 1.41747416e-02,
       1.62975083e-02, 1.87381742e-0...
       4.03701726e+01, 4.64158883e+01, 5.33669923e+01, 6.13590727e+01,
       7.05480231e+01, 8.11130831e+01, 9.32603347e+01, 1.07226722e+02,
       1.23284674e+02, 1.41747416e+02, 1.62975083e+02, 1.87381742e+02,
       2.15443469e+02, 2.47707636e+02, 2.84803587e+02, 3.27454916e+02,
       3.76493581e+02, 4.32876128e+02, 4.97702356e+02, 5.72236766e+02,
       6.57933225e+02, 7.56463328e+02, 8.69749003e+02, 1.00000000e+03]),
        cv=5, max_iter=5000)
print("Mejor lambda (α):", lasso_cv.alpha_)
Mejor lambda (α): 0.001
print("Coeficientes Lasso:", dict(zip(X.columns, lasso_cv.coef_)))
Coeficientes Lasso: {'weight': -0.0057942658973828675, 'horsepower': -0.047300115797909784}
# Gráfico de coeficientes vs lambda
plt.figure(figsize=(8,6))
<Figure size 800x600 with 0 Axes>
ax = plt.gca()
ax.plot(lasso_cv.alphas_, lasso_cv.path(X, y, alphas=alphas, max_iter=5000)[1].T)
ax.set_xscale("log")
[<matplotlib.lines.Line2D object at 0x358869e50>, <matplotlib.lines.Line2D object at 0x34b53a110>]
plt.xlabel("Lambda (α)")
plt.ylabel("Coeficientes")
Text(0.5, 0, 'Lambda (α)')
plt.title("Trayectoria de coeficientes (Lasso)")
Text(0, 0.5, 'Coeficientes')
plt.show()
Text(0.5, 1.0, 'Trayectoria de coeficientes (Lasso)')

10.7.6) Elastic Net


# Elastic Net con validación cruzada
elastic_cv = ElasticNetCV(l1_ratio=0.5, alphas=alphas, cv=5, max_iter=5000)
elastic_cv.fit(X, y)
ElasticNetCV(alphas=array([1.00000000e-03, 1.14975700e-03, 1.32194115e-03, 1.51991108e-03,
       1.74752840e-03, 2.00923300e-03, 2.31012970e-03, 2.65608778e-03,
       3.05385551e-03, 3.51119173e-03, 4.03701726e-03, 4.64158883e-03,
       5.33669923e-03, 6.13590727e-03, 7.05480231e-03, 8.11130831e-03,
       9.32603347e-03, 1.07226722e-02, 1.23284674e-02, 1.41747416e-02,
       1.62975083e-02, 1.873817...
       4.03701726e+01, 4.64158883e+01, 5.33669923e+01, 6.13590727e+01,
       7.05480231e+01, 8.11130831e+01, 9.32603347e+01, 1.07226722e+02,
       1.23284674e+02, 1.41747416e+02, 1.62975083e+02, 1.87381742e+02,
       2.15443469e+02, 2.47707636e+02, 2.84803587e+02, 3.27454916e+02,
       3.76493581e+02, 4.32876128e+02, 4.97702356e+02, 5.72236766e+02,
       6.57933225e+02, 7.56463328e+02, 8.69749003e+02, 1.00000000e+03]),
             cv=5, max_iter=5000)
print("Mejor lambda (α):", elastic_cv.alpha_)
Mejor lambda (α): 0.08697490026177834
print("Coeficientes Elastic Net:", dict(zip(X.columns, elastic_cv.coef_)))
Coeficientes Elastic Net: {'weight': -0.005799094651925317, 'horsepower': -0.04717785304846735}
# Trayectoria de coeficientes
coefs = []
for a in alphas:
    en = ElasticNet(alpha=a, l1_ratio=0.5, max_iter=5000)
    en.fit(X, y)
    coefs.append(en.coef_)
    
plt.figure(figsize=(8,6))
ElasticNet(alpha=0.001, max_iter=5000)
ElasticNet(alpha=0.0011497569953977356, max_iter=5000)
ElasticNet(alpha=0.0013219411484660286, max_iter=5000)
ElasticNet(alpha=0.0015199110829529332, max_iter=5000)
ElasticNet(alpha=0.001747528400007683, max_iter=5000)
ElasticNet(alpha=0.002009233002565048, max_iter=5000)
ElasticNet(alpha=0.0023101297000831605, max_iter=5000)
ElasticNet(alpha=0.0026560877829466868, max_iter=5000)
ElasticNet(alpha=0.0030538555088334154, max_iter=5000)
ElasticNet(alpha=0.003511191734215131, max_iter=5000)
ElasticNet(alpha=0.004037017258596553, max_iter=5000)
ElasticNet(alpha=0.004641588833612782, max_iter=5000)
ElasticNet(alpha=0.005336699231206312, max_iter=5000)
ElasticNet(alpha=0.006135907273413176, max_iter=5000)
ElasticNet(alpha=0.007054802310718645, max_iter=5000)
ElasticNet(alpha=0.008111308307896872, max_iter=5000)
ElasticNet(alpha=0.0093260334688322, max_iter=5000)
ElasticNet(alpha=0.010722672220103232, max_iter=5000)
ElasticNet(alpha=0.012328467394420659, max_iter=5000)
ElasticNet(alpha=0.014174741629268055, max_iter=5000)
ElasticNet(alpha=0.016297508346206444, max_iter=5000)
ElasticNet(alpha=0.01873817422860384, max_iter=5000)
ElasticNet(alpha=0.021544346900318846, max_iter=5000)
ElasticNet(alpha=0.024770763559917114, max_iter=5000)
ElasticNet(alpha=0.02848035868435802, max_iter=5000)
ElasticNet(alpha=0.03274549162877728, max_iter=5000)
ElasticNet(alpha=0.037649358067924674, max_iter=5000)
ElasticNet(alpha=0.04328761281083059, max_iter=5000)
ElasticNet(alpha=0.049770235643321115, max_iter=5000)
ElasticNet(alpha=0.05722367659350217, max_iter=5000)
ElasticNet(alpha=0.06579332246575682, max_iter=5000)
ElasticNet(alpha=0.07564633275546291, max_iter=5000)
ElasticNet(alpha=0.08697490026177834, max_iter=5000)
ElasticNet(alpha=0.1, max_iter=5000)
ElasticNet(alpha=0.11497569953977356, max_iter=5000)
ElasticNet(alpha=0.13219411484660287, max_iter=5000)
ElasticNet(alpha=0.1519911082952933, max_iter=5000)
ElasticNet(alpha=0.1747528400007685, max_iter=5000)
ElasticNet(alpha=0.2009233002565048, max_iter=5000)
ElasticNet(alpha=0.23101297000831603, max_iter=5000)
ElasticNet(alpha=0.26560877829466867, max_iter=5000)
ElasticNet(alpha=0.30538555088334157, max_iter=5000)
ElasticNet(alpha=0.3511191734215131, max_iter=5000)
ElasticNet(alpha=0.40370172585965536, max_iter=5000)
ElasticNet(alpha=0.4641588833612782, max_iter=5000)
ElasticNet(alpha=0.5336699231206312, max_iter=5000)
ElasticNet(alpha=0.6135907273413176, max_iter=5000)
ElasticNet(alpha=0.7054802310718645, max_iter=5000)
ElasticNet(alpha=0.8111308307896873, max_iter=5000)
ElasticNet(alpha=0.9326033468832199, max_iter=5000)
ElasticNet(alpha=1.072267222010323, max_iter=5000)
ElasticNet(alpha=1.232846739442066, max_iter=5000)
ElasticNet(alpha=1.4174741629268048, max_iter=5000)
ElasticNet(alpha=1.629750834620645, max_iter=5000)
ElasticNet(alpha=1.873817422860385, max_iter=5000)
ElasticNet(alpha=2.1544346900318843, max_iter=5000)
ElasticNet(alpha=2.4770763559917115, max_iter=5000)
ElasticNet(alpha=2.848035868435802, max_iter=5000)
ElasticNet(alpha=3.2745491628777286, max_iter=5000)
ElasticNet(alpha=3.7649358067924674, max_iter=5000)
ElasticNet(alpha=4.328761281083062, max_iter=5000)
ElasticNet(alpha=4.9770235643321135, max_iter=5000)
ElasticNet(alpha=5.72236765935022, max_iter=5000)
ElasticNet(alpha=6.5793322465756825, max_iter=5000)
ElasticNet(alpha=7.56463327554629, max_iter=5000)
ElasticNet(alpha=8.697490026177835, max_iter=5000)
ElasticNet(alpha=10.0, max_iter=5000)
ElasticNet(alpha=11.497569953977356, max_iter=5000)
ElasticNet(alpha=13.219411484660288, max_iter=5000)
ElasticNet(alpha=15.199110829529332, max_iter=5000)
ElasticNet(alpha=17.47528400007683, max_iter=5000)
ElasticNet(alpha=20.09233002565046, max_iter=5000)
ElasticNet(alpha=23.10129700083158, max_iter=5000)
ElasticNet(alpha=26.560877829466893, max_iter=5000)
ElasticNet(alpha=30.538555088334185, max_iter=5000)
ElasticNet(alpha=35.111917342151344, max_iter=5000)
ElasticNet(alpha=40.37017258596558, max_iter=5000)
ElasticNet(alpha=46.41588833612782, max_iter=5000)
ElasticNet(alpha=53.366992312063125, max_iter=5000)
ElasticNet(alpha=61.35907273413176, max_iter=5000)
ElasticNet(alpha=70.54802310718645, max_iter=5000)
ElasticNet(alpha=81.11308307896873, max_iter=5000)
ElasticNet(alpha=93.26033468832199, max_iter=5000)
ElasticNet(alpha=107.22672220103232, max_iter=5000)
ElasticNet(alpha=123.28467394420659, max_iter=5000)
ElasticNet(alpha=141.74741629268047, max_iter=5000)
ElasticNet(alpha=162.97508346206433, max_iter=5000)
ElasticNet(alpha=187.3817422860383, max_iter=5000)
ElasticNet(alpha=215.44346900318865, max_iter=5000)
ElasticNet(alpha=247.7076355991714, max_iter=5000)
ElasticNet(alpha=284.8035868435805, max_iter=5000)
ElasticNet(alpha=327.4549162877732, max_iter=5000)
ElasticNet(alpha=376.49358067924715, max_iter=5000)
ElasticNet(alpha=432.87612810830615, max_iter=5000)
ElasticNet(alpha=497.7023564332114, max_iter=5000)
ElasticNet(alpha=572.236765935022, max_iter=5000)
ElasticNet(alpha=657.9332246575682, max_iter=5000)
ElasticNet(alpha=756.463327554629, max_iter=5000)
ElasticNet(alpha=869.7490026177834, max_iter=5000)
ElasticNet(alpha=1000.0, max_iter=5000)
<Figure size 800x600 with 0 Axes>
ax = plt.gca()
ax.plot(alphas, coefs)
[<matplotlib.lines.Line2D object at 0x34b4da150>, <matplotlib.lines.Line2D object at 0x34b512cd0>]
ax.set_xscale("log")
plt.xlabel("Lambda (α)")
Text(0.5, 0, 'Lambda (α)')
plt.ylabel("Coeficientes")
Text(0, 0.5, 'Coeficientes')
plt.title("Trayectoria de coeficientes (Elastic Net)")
Text(0.5, 1.0, 'Trayectoria de coeficientes (Elastic Net)')
plt.show()

LS0tCnRpdGxlOiAiRXN0YWRpc3RpY2EgbXVsdGl2YXJpYW50ZSBhdmFuemFkYSBlbiBSIHkgUHl0aG9uIgphdXRob3I6ICJKdWFuIEpvc8OpIEVjaGV2ZXJyeSIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlICAgICAgICAgICMgQWN0aXZhIHRhYmxhIGRlIGNvbnRlbmlkbwogICAgdG9jX2RlcHRoOiAzICAgICAgICMgUHJvZnVuZGlkYWQgZGUgbGEgVE9DIChwb3IgZGVmZWN0byBlcyAzKQogICAgdG9jX2Zsb2F0OiB0cnVlICAgICMgVGFibGEgZGUgY29udGVuaWRvIGZsb3RhbnRlCiAgICAjbnVtYmVyX3NlY3Rpb25zOiB0cnVlICAjIE51bWVyYSBzZWNjaW9uZXMKICAgIGZpZ193aWR0aDogMTAgICAgICAgIyBBbmNobyBwcmVkZXRlcm1pbmFkbyBkZSBmaWd1cmFzIChlbiBwdWxnYWRhcykKICAgIGZpZ19oZWlnaHQ6IDEwICAgICAjIEFsdG8gcHJlZGV0ZXJtaW5hZG8gZGUgZmlndXJhcyAoZW4gcHVsZ2FkYXMpCiAgICAjdGhlbWU6IHVuaXRlZCAgICAgICMgVGVtYSB2aXN1YWwgZGUgQm9vdHN0cmFwIChvcGNpb25hbCkKICAgIGhpZ2hsaWdodDogdGFuZ28gICAgIyBUZW1hIGRlIHJlc2FsdGFkbyBkZSBjw7NkaWdvCi0tLQoKIyBJbnRyb2R1Y2Npw7NuCgoKRWwgcHJlc2VudGUgY3VhZGVybm8gcmVzdW1lIGxvcyB0ZW1hcyB2aXN0b3MgZW4gZWwgW2N1cnNvXShodHRwczovL2dpdGh1Yi5jb20vam9hbmJ5L2N1cnNvLWVzdGFkaXN0aWNhLW11bHRpdmFyaWFudGUpIGRlIGVzdGFkw61zdGljYSBtdWx0aXZhcmlhbnRlIGRlIFIgeSBQeXRob24uCgpQcmltZXJvLCBQcmltZXJvIGFqdXN0YW1vcyBsaWJyZXLDrWFzIGRlIFB5dGhvbiB5IFIsIHBhcmEgcG9kZXIgdHJhYmFqYXIgY29uIGFtYm9zIGRlIGZvcm1hIHNpbXVsdGFuZWEKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmxpYnJhcnkocmV0aWN1bGF0ZSkKCiMgRm9yemFyIGEgdXNhciBlbCBhbWJpZW50ZSBjb3JyZWN0bwp1c2VfY29uZGFlbnYoInB5MzExX2FybSIsIHJlcXVpcmVkID0gVFJVRSkKCiMgQ29uZmlybWFyIHF1ZSBlc3TDoSBiaWVuIGVubGF6YWRvCnB5X2NvbmZpZygpCgojICMgSW5zdGFsYXIgbG9zIHBhcXVldGVzIGRlIFB5dGhvbiBuZWNlc2FyaW9zCiMgcHlfaW5zdGFsbChjKAojICAgIm51bXB5IiwgCiMgICAic2NpcHkiLCAKIyAgICJwYW5kYXMiLCAKIyAgICJzZWFib3JuIiwgCiMgICAibWF0cGxvdGxpYiIsIAojICAgInBsb3RseSIsIAojICAgInNjaWtpdC1sZWFybiIsIAojICAgInNjaWtpdC1sZWFybi1leHRyYSIsICAgIyBQYXJhIEtNZWRvaWRzCiMgICAiaW1ibGVhcm4iLCAgICAgICAgICAgICAjIFBhcmEgU01PVEUgeSBwaXBlbGluZXMKIyAgICJzdGF0c21vZGVscyIsICAgICAgICAgICMgTW9kZWxvcyBlc3RhZMOtc3RpY29zCiMgICAiZmFjdG9yLWFuYWx5emVyIiwgICAgICAjIEFuw6FsaXNpcyBmYWN0b3JpYWwKIyAgICJweWRhdGFzZXQiICAgICAgICAgICAgICMgRGF0YXNldHMgZGUgZWplbXBsbwojICksIHBpcCA9IFRSVUUpCgoKCmBgYAoKQ2FyZ2Ftb3MgbGlicmVyaWFzIGRlIFB5dGhvbgoKYGBge3B5dGhvbiBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyDwn5OmIExpYnJlcsOtYXMgbmVjZXNhcmlhcyBlbiBQeXRob24KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgppbXBvcnQgbnVtcHkgYXMgbnAgICAgICAgICAgICAgICAgICAgICAgICAjIMOBbGdlYnJhIGxpbmVhbCB5IG9wZXJhY2lvbmVzIG51bcOpcmljYXMKaW1wb3J0IHBhbmRhcyBhcyBwZCAgICAgICAgICAgICAgICAgICAgICAgIyBNYW5pcHVsYWNpw7NuIHkgYW7DoWxpc2lzIGRlIGRhdG9zIGVuIERhdGFGcmFtZXMKaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdCAgICAgICAgICAgIyBWaXN1YWxpemFjacOzbiBiw6FzaWNhIHkgZ3LDoWZpY29zIDJECmltcG9ydCBzZWFib3JuIGFzIHNucyAgICAgICAgICAgICAgICAgICAgICMgVmlzdWFsaXphY2nDs24gZXN0YWTDrXN0aWNhIChncsOhZmljb3MgbcOhcyBlbGFib3JhZG9zKQppbXBvcnQgcGxvdGx5LmV4cHJlc3MgYXMgcHggICAgICAgICAgICAgICAjIFZpc3VhbGl6YWNpw7NuIGludGVyYWN0aXZhICgyRCB5IDNEKQoKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQojIPCflLkgRXN0YWTDrXN0aWNhIHkgZGlzdHJpYnVjaW9uZXMKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmcm9tIHNjaXB5LnN0YXRzIGltcG9ydCBub3JtICAgICAgICAgICAgICAjIERpc3RyaWJ1Y2nDs24gbm9ybWFsIHkgZnVuY2lvbmVzIGVzdGFkw61zdGljYXMKZnJvbSBzY2lweS5zdGF0cyBpbXBvcnQgbXVsdGl2YXJpYXRlX25vcm1hbCAjIERpc3RyaWJ1Y2nDs24gbm9ybWFsIG11bHRpdmFyaWFudGUKZnJvbSBzY2lweS5zdGF0cyBpbXBvcnQgY2hpMiAgICAgICAgICAgICAgIyBEaXN0cmlidWNpw7NuIGNoaS1jdWFkcmFkbwpmcm9tIHNjaXB5LnN0YXRzIGltcG9ydCBiYXJ0bGV0dCAgICAgICAgICAjIFRlc3QgZGUgZXNmZXJpY2lkYWQgZGUgQmFydGxldHQKZnJvbSBzY2lweS5zdGF0cyBpbXBvcnQgbXVsdGl2YXJpYXRlX3QgICAgIyBEaXN0cmlidWNpw7NuIHQgbXVsdGl2YXJpYW50ZQpmcm9tIHNjaXB5LnNwYXRpYWwuZGlzdGFuY2UgaW1wb3J0IG1haGFsYW5vYmlzICAjIERpc3RhbmNpYSBkZSBNYWhhbGFub2Jpcwpmcm9tIHNjaXB5LnNwYXRpYWwuZGlzdGFuY2UgaW1wb3J0IHBkaXN0ICAgICAgICAjIERpc3RhbmNpYXMgcG9yIHBhcmVzIGVudHJlIG9ic2VydmFjaW9uZXMKCiMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KIyDwn5S5IE1vZGVsb3MgZXN0YWTDrXN0aWNvcwojIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmltcG9ydCBzdGF0c21vZGVscy5hcGkgYXMgc20gICAgICAgICAgICAgICMgTW9kZWxvcyBlc3RhZMOtc3RpY29zIChyZWdyZXNpw7NuLCB0ZXN0cywgZXRjLikKZnJvbSBzdGF0c21vZGVscy5tdWx0aXZhcmlhdGUubWFub3ZhIGltcG9ydCBNQU5PVkEgICMgTUFOT1ZBIChBbsOhbGlzaXMgbXVsdGl2YXJpYW50ZSBkZSB2YXJpYW56YSkKCiMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KIyDwn5S5IE1hY2hpbmUgTGVhcm5pbmcgKHNjaWtpdC1sZWFybikKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmcm9tIHNrbGVhcm4uZGF0YXNldHMgaW1wb3J0IGxvYWRfaXJpcyAgICAjIERhdGFzZXQgZGUgZWplbXBsbyAoSXJpcykKZnJvbSBza2xlYXJuLmRhdGFzZXRzIGltcG9ydCBmZXRjaF9vcGVubWwgIyBEZXNjYXJnYSBkZSBkYXRhc2V0cyBkZSBPcGVuTUwKZnJvbSBza2xlYXJuLnByZXByb2Nlc3NpbmcgaW1wb3J0IFN0YW5kYXJkU2NhbGVyICAjIEVzY2FsYW1pZW50byBkZSBkYXRvcwpmcm9tIHNrbGVhcm4ucHJlcHJvY2Vzc2luZyBpbXBvcnQgTWluTWF4U2NhbGVyICAgICMgTm9ybWFsaXphY2nDs24gZGUgZGF0b3MgWzAsMV0KZnJvbSBza2xlYXJuLmRlY29tcG9zaXRpb24gaW1wb3J0IFBDQSAgICAgIyBBbsOhbGlzaXMgZGUgQ29tcG9uZW50ZXMgUHJpbmNpcGFsZXMKZnJvbSBza2xlYXJuLm1hbmlmb2xkIGltcG9ydCBNRFMgICAgICAgICAgIyBFc2NhbGFtaWVudG8gbXVsdGlkaW1lbnNpb25hbApmcm9tIHNrbGVhcm4uY2x1c3RlciBpbXBvcnQgS01lYW5zICAgICAgICAjIENsdXN0ZXJpbmcgay1tZWFucwpmcm9tIHNrbGVhcm5fZXh0cmEuY2x1c3RlciBpbXBvcnQgS01lZG9pZHMgICMgQ2x1c3RlcmluZyBrLW1lZG9pZHMKZnJvbSBza2xlYXJuLm1peHR1cmUgaW1wb3J0IEdhdXNzaWFuTWl4dHVyZSAgICAgIyBNb2RlbG9zIGRlIG1lemNsYSBnYXVzc2lhbmEgKGNsdXN0ZXJpbmcpCmZyb20gc2tsZWFybi5kaXNjcmltaW5hbnRfYW5hbHlzaXMgaW1wb3J0IExpbmVhckRpc2NyaW1pbmFudEFuYWx5c2lzICMgQW7DoWxpc2lzIGRpc2NyaW1pbmFudGUgbGluZWFsIChMREEpCmZyb20gc2tsZWFybi5uZWlnaGJvcnMgaW1wb3J0IEtOZWlnaGJvcnNDbGFzc2lmaWVyICMgQ2xhc2lmaWNhY2nDs24gay1OTgpmcm9tIHNrbGVhcm4ubmFpdmVfYmF5ZXMgaW1wb3J0IEdhdXNzaWFuTkIgICAgICAgIyBDbGFzaWZpY2Fkb3IgTmFpdmUgQmF5ZXMgR2F1c3NpYW5vCmZyb20gc2tsZWFybi5saW5lYXJfbW9kZWwgaW1wb3J0IExvZ2lzdGljUmVncmVzc2lvbiAjIFJlZ3Jlc2nDs24gbG9nw61zdGljYQpmcm9tIHNrbGVhcm4ubGluZWFyX21vZGVsIGltcG9ydCBMaW5lYXJSZWdyZXNzaW9uLCBSaWRnZSwgTGFzc28sIEVsYXN0aWNOZXQgICMgTW9kZWxvcyBkZSByZWdyZXNpw7NuCmZyb20gc2tsZWFybi5saW5lYXJfbW9kZWwgaW1wb3J0IFJpZGdlQ1YsIExhc3NvQ1YsIEVsYXN0aWNOZXRDViAgICMgUmVndWxhcml6YWNpw7NuIGNvbiB2YWxpZGFjacOzbiBjcnV6YWRhCmZyb20gc2tsZWFybi5tZXRyaWNzIGltcG9ydCBtZWFuX3NxdWFyZWRfZXJyb3IsIHIyX3Njb3JlICAgICAgICAgICAjIE3DqXRyaWNhcyBkZSBkZXNlbXBlw7FvIGRlIG1vZGVsb3MKZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgdHJhaW5fdGVzdF9zcGxpdCwgY3Jvc3NfdmFsX3Njb3JlLCBHcmlkU2VhcmNoQ1YgICMgVmFsaWRhY2nDs24gZGUgbW9kZWxvcwoKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQojIPCflLkgQmFsYW5jZW8geSBwaXBlbGluZXMgKE1MKQojIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmZyb20gaW1ibGVhcm4ub3Zlcl9zYW1wbGluZyBpbXBvcnQgU01PVEUgICAjIE92ZXJzYW1wbGluZyBwYXJhIGNsYXNlcyBkZXNiYWxhbmNlYWRhcwpmcm9tIGltYmxlYXJuLnBpcGVsaW5lIGltcG9ydCBQaXBlbGluZSAgICAgIyBQaXBlbGluZXMgY29uIHTDqWNuaWNhcyBkZSBiYWxhbmNlbwoKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQojIPCflLkgQW7DoWxpc2lzIGZhY3RvcmlhbAojIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmZyb20gZmFjdG9yX2FuYWx5emVyIGltcG9ydCBGYWN0b3JBbmFseXplciwgY2FsY3VsYXRlX2JhcnRsZXR0X3NwaGVyaWNpdHksIGNhbGN1bGF0ZV9rbW8gIAojIEFuw6FsaXNpcyBmYWN0b3JpYWwgeSB0ZXN0cyBkZSBhZGVjdWFjacOzbiAoQmFydGxldHQgeSBLTU8pCgojIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiMg8J+UuSBIZXJyYW1pZW50YXMgYWRpY2lvbmFsZXMKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmcm9tIHNjaXB5LmNsdXN0ZXIuaGllcmFyY2h5IGltcG9ydCBkZW5kcm9ncmFtLCBsaW5rYWdlLCBmY2x1c3RlciAgIyBDbHVzdGVyaW5nIGplcsOhcnF1aWNvCmZyb20gbWF0aCBpbXBvcnQgcGkgICAgICAgICAgICAgICAgICAgICAgICAjIENvbnN0YW50ZXMgbWF0ZW3DoXRpY2FzCmZyb20gcGFuZGFzLnBsb3R0aW5nIGltcG9ydCBzY2F0dGVyX21hdHJpeCwgYW5kcmV3c19jdXJ2ZXMsIHBhcmFsbGVsX2Nvb3JkaW5hdGVzICAKIyBHcsOhZmljb3MgbXVsdGl2YXJpYW50ZXMgY29uIHBhbmRhcwoKCgpgYGAKCkNhcmdhbW9zIGxpYnJlcsOtYXMgZGUgUgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KIyBDYXJnYXIgbGlicmVyaWFzCiMgQ2FyZ2FyIGxpYnJlcsOtYXMgbmVjZXNhcmlhcwpsaWJyYXJ5KGRwbHlyKSAgICAgICAgIyBNYW5pcHVsYWNpw7NuIHkgdHJhbnNmb3JtYWNpw7NuIGRlIGRhdG9zIChzZWxlY3QsIGZpbHRlciwgbXV0YXRlLCBzdW1tYXJpc2UpCmxpYnJhcnkoZ2dwbG90MikgICAgICAjIFZpc3VhbGl6YWNpw7NuIGRlIGRhdG9zIChncsOhZmljb3MgZXN0YWTDrXN0aWNvcyB5IHBlcnNvbmFsaXphYmxlcykKbGlicmFyeShHR2FsbHkpICAgICAgICMgRXh0aWVuZGUgZ2dwbG90MiBjb24gZ3LDoWZpY29zIG11bHRpdmFyaWFudGVzIChwYWlycywgZ2dwYWlycykKCmxpYnJhcnkoTUFTUykgICAgICAgICAjIE1vZGVsb3MgZXN0YWTDrXN0aWNvcyB5IGZ1bmNpb25lcyAobXZybm9ybSwga2RlMmQsIExEQSkKbGlicmFyeShnZ3RoZW1lcykgICAgICMgVGVtYXMgeSBlc3RpbG9zIHBhcmEgbWVqb3JhciBsYSBlc3TDqXRpY2EgZW4gZ2dwbG90MgpsaWJyYXJ5KHBsb3RseSkgICAgICAgIyBWaXN1YWxpemFjacOzbiBpbnRlcmFjdGl2YSAoMkQgeSAzRCkKbGlicmFyeShnZ3JlcGVsKSAgICAgICMgRXRpcXVldGFzIGVuIGdyw6FmaWNvcyBzaW4gc29sYXBhbWllbnRvCmxpYnJhcnkobXZ0bm9ybSkgICAgICAjIFNpbXVsYWNpw7NuIHkgZGVuc2lkYWRlcyBkZSBkaXN0cmlidWNpb25lcyBub3JtYWxlcyBtdWx0aXZhcmlhbnRlcwpsaWJyYXJ5KG1jbHVzdCkgICAgICAgIyBNb2RlbG9zIGRlIG1lemNsYSBnYXVzc2lhbmEgeSBjbHVzdGVyaW5nCmxpYnJhcnkoY29wdWxhKSAgICAgICAjIE1vZGVsb3MgZGUgZGVwZW5kZW5jaWEgZW50cmUgdmFyaWFibGVzIChjw7NwdWxhcykKbGlicmFyeShIb3RlbGxpbmcpICAgICMgVGVzdCBlc3RhZMOtc3RpY28gVMKyIGRlIEhvdGVsbGluZwpsaWJyYXJ5KGVsbGlwc2UpICAgICAgIyBHcsOhZmljb3MgZGUgZWxpcHNlcyBkZSBjb25maWFuemEgbXVsdGl2YXJpYW50ZXMKbGlicmFyeShwc3ljaCkgICAgICAgICMgQW7DoWxpc2lzIHBzaWNvbcOpdHJpY28geSBkZSBmYWN0b3JlcwpsaWJyYXJ5KEdQQXJvdGF0aW9uKSAgIyBSb3RhY2lvbmVzIGRlIGZhY3RvcmVzIGVuIGFuw6FsaXNpcyBmYWN0b3JpYWwKbGlicmFyeShjb3JycGxvdCkgICAgICMgVmlzdWFsaXphY2nDs24gZGUgbWF0cmljZXMgZGUgY29ycmVsYWNpw7NuCmxpYnJhcnkodGlkeXIpICAgICAgICAjIFRyYW5zZm9ybWFjacOzbiBkZSBkYXRvcyAocGl2b3RfbG9uZ2VyLCBwaXZvdF93aWRlciwgc2VwYXJhdGUpCmxpYnJhcnkodmVnYW4pICAgICAgICAjIEFuw6FsaXNpcyBtdWx0aXZhcmlhbnRlIChvcmRlbmFjaW9uZXMsIGRpdmVyc2lkYWQsIGVjb2xvZ8OtYSkKbGlicmFyeShjbHVzdGVyKSAgICAgICMgTcOpdG9kb3MgZGUgY2x1c3RlcmluZyAoay1tZWFucywgUEFNLCBzaWxob3VldHRlLCBldGMuKQpsaWJyYXJ5KGZhY3RvZXh0cmEpICAgIyBWaXN1YWxpemFjacOzbiBkZSByZXN1bHRhZG9zIGRlIGNsdXN0ZXJpbmcgeSBQQ0EKbGlicmFyeShjYXJldCkgICAgICAgICMgRW50cmVuYW1pZW50bywgdmFsaWRhY2nDs24geSBhanVzdGUgZGUgbW9kZWxvcwpsaWJyYXJ5KGNsYXNzKSAgICAgICAgIyBDbGFzaWZpY2FjacOzbiBzdXBlcnZpc2FkYSAoa05OKQpsaWJyYXJ5KGUxMDcxKSAgICAgICAgIyBBbGdvcml0bW9zIGRlIG1hY2hpbmUgbGVhcm5pbmcgKE5haXZlIEJheWVzLCBTVk0pCmxpYnJhcnkoZ2xtbmV0KSAgICAgICAjIFJlZ3VsYXJpemFjacOzbiAoUmlkZ2UsIExhc3NvLCBFbGFzdGljIE5ldCkKbGlicmFyeShnZ3B1YnIpICAgICAgICMgR3LDoWZpY29zIGxpc3RvcyBwYXJhIHB1YmxpY2FjaW9uZXMgKGVzdGFkw61zdGljb3MgeSBjb21wYXJhY2lvbmVzKQpsaWJyYXJ5KGJyb29tKSAgICAgICAgIyBDb252aWVydGUgcmVzdWx0YWRvcyBkZSBtb2RlbG9zIGVuIGRhdGEgZnJhbWVzIG9yZGVuYWRvcwpsaWJyYXJ5KGNhcikgICAgICAgICAgIyBIZXJyYW1pZW50YXMgcGFyYSByZWdyZXNpw7NuIChWSUYsIEFub3ZhLCBldGMuKQpsaWJyYXJ5KGxlYXBzKSAgICAgICAgIyBTZWxlY2Npw7NuIGRlIHZhcmlhYmxlcyBlbiByZWdyZXNpw7NuIChiZXN0IHN1YnNldCkKbGlicmFyeShkZGFscGhhKSAgICAgICMgTcOpdG9kb3MgZGUgcHJvZnVuZGlkYWQgZGUgZGF0b3MgeSBjbGFzaWZpY2FjacOzbiByb2J1c3RhCmxpYnJhcnkoc2NhdHRlcnBsb3QzZCkjIEdyw6FmaWNvcyAzRCBzaW1wbGVzCmxpYnJhcnkodGliYmxlKSAgICAgICAjIERhdGEgZnJhbWVzIG1vZGVybm9zICh0aWJibGVzKQpsaWJyYXJ5KHBhdGNod29yaykgICAgIyBDb21wb3NpY2nDs24gZGUgbcO6bHRpcGxlcyBncsOhZmljb3MgZ2dwbG90MgpsaWJyYXJ5KGFwbHBhY2spICAgICAgIyBHcsOhZmljb3Mgcm9idXN0b3MgKGJhZ3Bsb3RzLCBmYWNlcyBwbG90cykKbGlicmFyeSh2aXJpZGlzKSAgICAgICMgUGFsZXRhcyBkZSBjb2xvciBwZXJjZXB0dWFsbWVudGUgdW5pZm9ybWVzCmxpYnJhcnkodGlkeXZlcnNlKSAgICAjIENvbGVjY2nDs24gZGUgcGFxdWV0ZXMgcGFyYSBjaWVuY2lhIGRlIGRhdG9zIChkcGx5ciwgZ2dwbG90MiwgdGlkeXIsIHJlYWRyLCBldGMuKQpsaWJyYXJ5KGphbml0b3IpICAgICAgIyBMaW1waWV6YSBkZSBkYXRvcyAoY2xlYW5fbmFtZXMsIHRhYnlsKQpsaWJyYXJ5KHNjYWxlcykgICAgICAgIyBFdGlxdWV0YXMgeSBlc2NhbGFzIGVuIGdyw6FmaWNvcyAocGVyY2VudCwgY29tbWEsIGV0Yy4pCgoKbGlicmFyeShNQVNTKQpsaWJyYXJ5KElDU05QKQpsaWJyYXJ5KGdncGxvdDIpCgoKIyBRdWl0YXIgbm90YWNpb24gY2llbnRpZmljYQpvcHRpb25zKHNjaXBlbiA9IDk5OSkgICMgQ3VhbnRvIG1heW9yIGVsIG7Dum1lcm8sIG1lbm9zIG5vdGFjacOzbiBjaWVudMOtZmljYQpgYGAKCiMgMSkgRXNwYWNpbyBtdWx0aXZhcmlhbnRlCgohW10oaW1hZ2VzL2NsaXBib2FyZC0yMTI5MjM1NDY0LnBuZykKCkxhIGVzdGFkw61zdGljYSBtdWx0aXZhcmlhbnRlIHNpcnZlIHBhcmEgYW5hbGl6YXIgc2ltdWx0w6FuZWFtZW50ZSBtw7psdGlwbGVzIHZhcmlhYmxlcyBjb24gZWwgZmluIGRlIGVudGVuZGVyIGxhcyByZWxhY2lvbmVzIHF1ZSBleGlzdGVuIGVudHJlIGVsbGFzIHkgY8OzbW8gZXN0YXMgaW5mbHV5ZW4gZW4gZmVuw7NtZW5vcyBjb21wbGVqb3MuIEVzIGVzcGVjaWFsbWVudGUgw7p0aWwgY3VhbmRvIGxvcyBkYXRvcyBjb250aWVuZW4gbXVjaG9zIGF0cmlidXRvcyBpbnRlcnJlbGFjaW9uYWRvcywgeWEgcXVlIHBlcm1pdGUgaWRlbnRpZmljYXIgcGF0cm9uZXMsIHJlZHVjaXIgZGltZW5zaW9uZXMsIGNsYXNpZmljYXIgb2JzZXJ2YWNpb25lcywgYWdydXBhciBlbGVtZW50b3Mgc2ltaWxhcmVzIG8gcHJlZGVjaXIgcmVzdWx0YWRvcy4gU2UgYXBsaWNhIGVuIGRpdmVyc2FzIMOhcmVhcyBjb21vIGVsIG1hcmtldGluZywgbGEgZWNvbm9tw61hLCBsYSBiaW9sb2fDrWEgbyBsYXMgY2llbmNpYXMgc29jaWFsZXMsIGRvbmRlIGxvcyBmZW7Ds21lbm9zIG5vIHB1ZWRlbiBleHBsaWNhcnNlIGFkZWN1YWRhbWVudGUgY29uIHVuYSBzb2xhIHZhcmlhYmxlLCB5IHNlIHJlcXVpZXJlIHVuYSB2aXNpw7NuIGludGVncmFsIHkgZXN0cnVjdHVyYWRhIGRlIGxvcyBkYXRvcy4KCiFbXShpbWFnZXMvY2xpcGJvYXJkLTE3ODM5NzU2NTUucG5nKQoKTG9zIG3DqXRvZG9zIGRlIGxhIGVzdGFkw61zdGljYSBtdWx0aXZhcmlhbnRlIHN1ZWxlbiBhZ3J1cGFyc2UgZW4gZG9zIGdyYW5kZXMgZW5mb3F1ZXM6IGxhIG1pbmVyw61hIGRlIGRhdG9zIHkgbGEgZXN0YWTDrXN0aWNhIGluZmVyZW5jaWFsLiBMYcKgKiptaW5lcsOtYSBkZSBkYXRvcyoqwqBzZSBlbmZvY2EgZW4gZGVzY3VicmlyIHBhdHJvbmVzLCByZWxhY2lvbmVzIG9jdWx0YXMgbyBlc3RydWN0dXJhcyBkZW50cm8gZGUgZ3JhbmRlcyB2b2zDum1lbmVzIGRlIGRhdG9zLCBtdWNoYXMgdmVjZXMgc2luIHVuYSBoaXDDs3Rlc2lzIHByZXZpYTsgZXMgbcOhcyBleHBsb3JhdG9yaWEgeSBzZSBiYXNhIGVuIGFsZ29yaXRtb3MgY29tbyBsb3Mgw6FyYm9sZXMgZGUgZGVjaXNpw7NuLCByZWRlcyBuZXVyb25hbGVzIG8gbcOpdG9kb3MgZGUgY2x1c3RlcmluZy4gUG9yIG90cm8gbGFkbywgbGHCoCoqZXN0YWTDrXN0aWNhIGluZmVyZW5jaWFsKirCoHBhcnRlIGRlIHVuIG1hcmNvIHRlw7NyaWNvIG3DoXMgcmlndXJvc28geSBidXNjYSBoYWNlciBnZW5lcmFsaXphY2lvbmVzIHNvYnJlIHVuYSBwb2JsYWNpw7NuIGEgcGFydGlyIGRlIHVuYSBtdWVzdHJhLCB1c2FuZG8gbW9kZWxvcyBxdWUgcGVybWl0ZW4gZXN0aW1hciBwYXLDoW1ldHJvcywgY29udHJhc3RhciBoaXDDs3Rlc2lzIHkgZXN0YWJsZWNlciByZWxhY2lvbmVzIGNhdXNhbGVzLCBjb21vIGxhIHJlZ3Jlc2nDs24gbXVsdGl2YXJpYW50ZSBvIGVsIGFuw6FsaXNpcyBkaXNjcmltaW5hbnRlLiBBbWJvcyBlbmZvcXVlcyBzb24gY29tcGxlbWVudGFyaW9zIHkgcGVybWl0ZW4gb2J0ZW5lciB1bmEgdmlzacOzbiBtw6FzIGNvbXBsZXRhIHkgcm9idXN0YSBkZWwgZmVuw7NtZW5vIGVzdHVkaWFkby4KCiFbXShpbWFnZXMvY2xpcGJvYXJkLTE1Mjg2NTgzNzgucG5nKQoKQW7DoWxpc2lzIGNvbiAiQmFncGxvdCIKCiFbXShpbWFnZXMvY2xpcGJvYXJkLTI1NDIyNDMzMTUucG5nKQoKIyMjIDEuMSkgRWplbXBsbyBjb24gZGF0b3MgZGVwYXJ0YW1lbnRhbGVzIGRlIFVTQSAoUikKCkVsIGRhdGFzZXTCoCpVUyBTdGF0ZSBGYWN0cyBhbmQgRmlndXJlcyrCoGRlIFIgcmXDum5lIGluZm9ybWFjacOzbiBnZW9ncsOhZmljYSwgZGVtb2dyw6FmaWNhIHkgc29jaW9lY29uw7NtaWNhIHNvYnJlIGxvcyA1MCBlc3RhZG9zIGRlIEVzdGFkb3MgVW5pZG9zLCBvcmdhbml6YWRhIGFsZmFiw6l0aWNhbWVudGUuIEluY2x1eWUgYWJyZXZpYWNpb25lcywgbm9tYnJlcyBjb21wbGV0b3MsIMOhcmVhcyBlbiBtaWxsYXMgY3VhZHJhZGFzLCBjZW50cm9zIGdlb2dyw6FmaWNvcyBhcHJveGltYWRvcywgZGl2aXNpb25lcyB5IHJlZ2lvbmVzIG9maWNpYWxlcyBkZWwgcGHDrXMsIGFzw60gY29tbyB1bmEgbWF0cml6IGxsYW1hZGHCoGBzdGF0ZS54NzdgwqBjb24gZXN0YWTDrXN0aWNhcyBjbGF2ZSBkZSBjYWRhIGVzdGFkbywgY29tbyBwb2JsYWNpw7NuIGVzdGltYWRhIGVuIDE5NzUsIGluZ3Jlc28gcGVyIGPDoXBpdGEgZW4gMTk3NCwgdGFzYSBkZSBhbmFsZmFiZXRpc21vLCBlc3BlcmFuemEgZGUgdmlkYSwgdGFzYSBkZSBob21pY2lkaW9zLCBwb3JjZW50YWplIGRlIGdyYWR1YWRvcyBkZSBzZWN1bmRhcmlhLCBkw61hcyBwcm9tZWRpbyBkZSBoZWxhZGFzIHkgc3VwZXJmaWNpZSB0ZXJyaXRvcmlhbC4gRXN0YSBpbmZvcm1hY2nDs24gcHJvdmllbmUgZGVsIERlcGFydGFtZW50byBkZSBDb21lcmNpbyBkZSBFRS5VVS4sIGNvbiBkYXRvcyByZWNvbGVjdGFkb3MgcHJpbmNpcGFsbWVudGUgZW4gbG9zIGHDsW9zIDE5NzBzLCB5IGVzIMO6dGlsIHBhcmEgYW7DoWxpc2lzIGNvbXBhcmF0aXZvcyB5IHZpc3VhbGl6YWNpb25lcyBlc3BhY2lhbGVzIG8gZXN0YWTDrXN0aWNhcyBtdWx0aXZhcmlhZGFzLgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgcGFnZWQucHJpbnQ9RkFMU0V9CiMgQ2FyZ2FyIGRhdGFzZXQKWCA8LSBhcy5kYXRhLmZyYW1lKHN0YXRlLng3NykKZ2xpbXBzZShYKQoKbi5YIDwtIG5yb3coWCkKcC5YIDwtIG5jb2woWCkKCmBgYAoKVmVtb3MgcXVlIGVsIGNvbmp1bnRvIGRlIGRhdG9zIHRpZW5lIDUwIGZpbGFzIChvYnNlcnZhY2lvbmVzIGRlIGNhZGEgZXN0YWRvKSB5IDggY29sdW1uYXMgKFZhcmlhYmxlcyBkZSBjYWRhIG9ic2VydmFjaW9uKQoKIyMjIDEuMS4xKSBWaXN1YWxpemFjacOzbiBkZSBkYXRvcwoKR3JhZmljYW1vcyBib3hwbG90IHBvciB2YXJpYWJsZQoKYGBge3IgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpYX2xvbmcgPC0gWCAlPiUgCiAgcm93bmFtZXNfdG9fY29sdW1uKCJTdGF0ZSIpICU+JQogIHBpdm90X2xvbmdlcigtU3RhdGUsIG5hbWVzX3RvID0gIlZhcmlhYmxlIiwgdmFsdWVzX3RvID0gIlZhbHVlIikKCmdncGxvdChYX2xvbmcsIGFlcyh4ID0gVmFyaWFibGUsIHkgPSBWYWx1ZSkpICsKICBnZW9tX2JveHBsb3QoZmlsbCA9ICJza3libHVlIikgKwogIGZhY2V0X3dyYXAoflZhcmlhYmxlLCBzY2FsZXMgPSAiZnJlZSIpICsKICB0aGVtZV9taW5pbWFsKCkKCgpgYGAKClZlbW9zIEJhZ3Bsb3QgZW50cmUgaW5ncmVzb3MgeSBleHBlY3RhdGl2YSBkZSB2aWRhCgpgYGB7cn0KYmFncGxvdChYJEluY29tZSwgWCRgTGlmZSBFeHBgLAogICAgICAgIHhsYWIgPSAiSW5jb21lIiwKICAgICAgICB5bGFiID0gIkxpZmUgRXhwZWN0YW5jeSIsCiAgICAgICAgbWFpbiA9ICJCYWdwbG90OiBJbmNvbWUgdnMgTGlmZSBFeHAiKQpgYGAKCkVsIGdyw6FmaWNvIGJhZ3Bsb3QgZGUgIkluY29tZSB2cyBMaWZlIEV4cGVjdGFuY3kiIG11ZXN0cmEgdmlzdWFsbWVudGUgY8OzbW8gc2UgcmVsYWNpb25hbiBlbCBpbmdyZXNvIHkgbGEgZXNwZXJhbnphIGRlIHZpZGEsIGRlc3RhY2FuZG8gdGFudG8gbGEgdGVuZGVuY2lhIGNlbnRyYWwgY29tbyBsYSBkaXNwZXJzacOzbiBkZSBsb3MgZGF0b3M6IGxhIHJlZ2nDs24gY2VudHJhbCBvc2N1cmEgKGVsICJiYWciKSBlbmNpZXJyYSBlbCA1MCUgZGUgbGFzIG9ic2VydmFjaW9uZXMsIGluZGljYW5kbyB1bmEgZnVlcnRlIGNvbmNlbnRyYWNpw7NuIGRlIGRhdG9zIGVuIGVzZSByYW5nbzsgZWwgw6FyZWEgbcOhcyBjbGFyYSAoZWwgImZlbmNlIikgZGVsaW1pdGEgZWwgcmFuZ28gZXh0ZW5kaWRvIHkgcGVybWl0ZSBpZGVudGlmaWNhciB2YWxvcmVzIGF0w61waWNvcywgbWllbnRyYXMgcXVlIGxhcyBsw61uZWFzIHJvamFzICgicmF5cyIpIHNlw7FhbGFuIGxhIGRpcmVjY2nDs24geSBtYWduaXR1ZCBkZSBlc3RvcyB2YWxvcmVzIGV4dHJlbW9zLCBsbyBxdWUgZmFjaWxpdGEgZGV0ZWN0YXIgcGF0cm9uZXMsIHNpbWV0csOtYXMgbyBhbm9tYWzDrWFzIGVuIGxhIGRpc3RyaWJ1Y2nDs24gY29uanVudGEgZGUgYW1iYXMgdmFyaWFibGVzLgoKSGlzdG9ncmFtYXMgZGUgbGEgdmFyaWFibGUgIk11cmRlciIgY29uIGRpZmVyZW50ZXMgYmlud2lkdGhzCgpFbMKgYGJpbndpZHRoYMKgZXMgZWwgYW5jaG8gZGUgY2FkYSBiYXJyYSBlbiB1biBoaXN0b2dyYW1hIHkgZGV0ZXJtaW5hIGPDs21vIHNlIGFncnVwYW4gbG9zIGRhdG9zIGVuIGludGVydmFsb3M7IHVuIHZhbG9yIHBlcXVlw7FvIG11ZXN0cmEgbcOhcyBkZXRhbGxlIHkgdmFyaWFiaWxpZGFkLCBtaWVudHJhcyBxdWUgdW5vIGdyYW5kZSBhZ3J1cGEgbcOhcyBkYXRvcyBlbiBtZW5vcyBiYXJyYXMsIGRhbmRvIHVuYSB2aXNpw7NuIG3DoXMgZ2VuZXJhbCBkZSBsYSBkaXN0cmlidWNpw7NuLgoKYGBge3IgZmlnLmhlaWdodD03LCBmaWcud2lkdGg9MTQsIG1lc3NhZ2U9VFJVRSwgd2FybmluZz1UUlVFfQpiaW53aWR0aHMgPC0gYygxLCAyLCAzKQoKcGxvdHMgPC0gbGFwcGx5KGJpbndpZHRocywgZnVuY3Rpb24oYncpIHsKICAjIFNlY3VlbmNpYSBkZSBjb3J0ZXMgYmFzYWRhIGVuIGJpbndpZHRoIHkgZWwgcmFuZ28gZGUgbG9zIGRhdG9zCiAgYnJlYWtzX3NlcSA8LSBzZXEoZmxvb3IobWluKFgkTXVyZGVyKSksIGNlaWxpbmcobWF4KFgkTXVyZGVyKSksIGJ5ID0gYncpCiAgCiAgZ2dwbG90KFgsIGFlcyh4ID0gTXVyZGVyKSkgKwogICAgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSBidywgZmlsbCA9ICJibHVlIiwgYWxwaGEgPSAwLjYsIGNvbG9yID0gImJsYWNrIikgKwogICAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IGJyZWFrc19zZXEpICsKICAgIGxhYnMoCiAgICAgIHRpdGxlID0gcGFzdGUoIk11cmRlciBjb24gYmlud2lkdGggPSIsIGJ3KSwKICAgICAgeCA9ICJUYXNhIGRlIGhvbWljaWRpb3MiLAogICAgICB5ID0gIkZyZWN1ZW5jaWEiCiAgICApICsKICAgIHRoZW1lX21pbmltYWwoKQp9KQoKd3JhcF9wbG90cyhwbG90cywgbmNvbCA9IDEpCgpgYGAKCkVsIGdyw6FmaWNvIG11ZXN0cmEgdHJlcyBoaXN0b2dyYW1hcyBxdWUgcmVwcmVzZW50YW4gbGEgZGlzdHJpYnVjacOzbiBkZSBsYSB2YXJpYWJsZcKgYE11cmRlcmDCoCh0YXNhIGRlIGhvbWljaWRpb3MgcG9yIGVzdGFkbyBlbiBFRS4gVVUuKSwgY2FkYSB1bm8gY29uIHVuIGRpZmVyZW50ZSB2YWxvciBkZcKgYGJpbndpZHRoYMKgKDEsIDIgeSAzKSwgbG8gY3VhbCBjb250cm9sYSBlbCBhbmNobyBkZSBsYXMgYmFycmFzIHksIHBvciB0YW50bywgZWwgbml2ZWwgZGUgZGV0YWxsZSBjb24gcXVlIHNlIGFncnVwYW4gbG9zIGRhdG9zLiBBbCBjb21wYXJhciBsb3MgcGFuZWxlcywgc2Ugb2JzZXJ2YSBxdWUgdW4gYmlud2lkdGggbcOhcyBwZXF1ZcOxbyAoaXpxdWllcmRhKSByZXZlbGEgbcOhcyBkZXRhbGxlIHBlcm8gcHVlZGUgZ2VuZXJhciBydWlkbyB2aXN1YWwsIG1pZW50cmFzIHF1ZSB1bm8gbcOhcyBncmFuZGUgKGRlcmVjaGEpIHN1YXZpemEgbGEgZGlzdHJpYnVjacOzbiwgZmFjaWxpdGFuZG8gbGEgaW50ZXJwcmV0YWNpw7NuIGdlbmVyYWwgcGVybyBvY3VsdGFuZG8gdmFyaWFjaW9uZXMgc3V0aWxlcy4gRXN0ZSBlamVyY2ljaW8gbm9zIGF5dWRhIGEgZW50ZW5kZXIgY8OzbW8gZWwgdGFtYcOxbyBkZWwgYmluIGluZmx1eWUgZW4gbGEgcGVyY2VwY2nDs24gZGUgbGEgZm9ybWEgZGUgbGEgZGlzdHJpYnVjacOzbiB5IHBlcm1pdGUgZWxlZ2lyIHVuIG5pdmVsIGRlIGFncmVnYWNpw7NuIGFkZWN1YWRvIHNlZ8O6biBlbCBvYmpldGl2byBkZWwgYW7DoWxpc2lzLgoKSGlzdG9ncmFtYXMgZGUgdG9kYXMgbGFzIHZhcmlhYmxlcyAoU3R1cmdlcykKCmBgYHtyIGZpZy5oZWlnaHQ9OSwgZmlnLndpZHRoPTEyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpYX2xvbmcgJT4lCiAgZ2dwbG90KGFlcyh4ID0gVmFsdWUpKSArCiAgZ2VvbV9oaXN0b2dyYW0oYmlucyA9IDMwLCBmaWxsID0gImJsdWUiLCBjb2xvciA9ICJ3aGl0ZSIpICsKICBmYWNldF93cmFwKH5WYXJpYWJsZSwgc2NhbGVzID0gImZyZWUiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpgYGAKCkRlbnNpZGFkZXMga2VybmVsIHBvciB2YXJpYWJsZQoKYGBge3IgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpYX2xvbmcgJT4lCiAgZ2dwbG90KGFlcyh4ID0gVmFsdWUpKSArCiAgZ2VvbV9kZW5zaXR5KGZpbGwgPSAiYmx1ZSIsIGFscGhhID0gMC40KSArCiAgZmFjZXRfd3JhcCh+VmFyaWFibGUsIHNjYWxlcyA9ICJmcmVlIikgKwogIGxhYnModGl0bGUgPSAiRGVuc2lkYWQga2VybmVsIHBvciB2YXJpYWJsZSIpICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKRWwgZ3LDoWZpY28gbXVlc3RyYSBsYXMgY3VydmFzIGRlIGRlbnNpZGFkIGtlcm5lbCBwYXJhIGNhZGEgdmFyaWFibGUgY3VhbnRpdGF0aXZhIGRlbCBkYXRhc2V0wqBgc3RhdGUueDc3YCwgbG8gcXVlIHBlcm1pdGUgdmlzdWFsaXphciBsYSBmb3JtYSBkZSBzdSBkaXN0cmlidWNpw7NuIHNpbiBkZXBlbmRlciBkZSBsb3MgaGlzdG9ncmFtYXMuIEFsIHVzYXLCoGBmYWNldF93cmFwYCwgY2FkYSB2YXJpYWJsZSBzZSByZXByZXNlbnRhIGVuIHVuIHBhbmVsIGluZGVwZW5kaWVudGUgY29uIGVzY2FsYXMgbGlicmVzLCBsbyBxdWUgZmFjaWxpdGEgY29tcGFyYXIgc3VzIGZvcm1hcy4gUG9yIGVqZW1wbG8sIHZhcmlhYmxlcyBjb21vwqBgSWxsaXRlcmFjeWAswqBgUG9wdWxhdGlvbmDCoHnCoGBBcmVhYMKgcHJlc2VudGFuIGRpc3RyaWJ1Y2lvbmVzIGFzaW3DqXRyaWNhcyBjb24gc2VzZ28gYSBsYSBkZXJlY2hhIChjb25jZW50cmFjacOzbiBkZSB2YWxvcmVzIGJham9zIHkgcG9jb3MgdmFsb3JlcyBhbHRvcyksIG1pZW50cmFzIHF1ZSB2YXJpYWJsZXMgY29tb8KgYExpZmUgRXhwYMKgb8KgYEluY29tZWDCoHRpZW5lbiBmb3JtYXMgbcOhcyBzaW3DqXRyaWNhcyBvIGNvbiB1bmEgbGV2ZSBhc2ltZXRyw61hLiBFc3RlIHRpcG8gZGUgZ3LDoWZpY28gcGVybWl0ZSBpZGVudGlmaWNhciBwYXRyb25lcyBjb21vIGJpbW9kYWxpZGFkLCBjb25jZW50cmFjacOzbiBkZSB2YWxvcmVzIHkgc2VzZ29zLCBzaWVuZG8gw7p0aWwgcGFyYSBkZWNpZGlyIHRyYW5zZm9ybWFjaW9uZXMgbyB2ZXJpZmljYXIgc3VwdWVzdG9zIGVzdGFkw61zdGljb3MgcHJldmlvcyBhIHVuIGFuw6FsaXNpcyBtdWx0aXZhcmlhbnRlLgoKVmVtb3MgbWFwYXMgZGUgY2Fsb3IgZGUgZGVuc2lkYWQgYml2YXJpYWRhIGNvbiBzdSByZXNwZWN0aXZvIGNvbnRvcm5vCgpgYGB7cn0KCiMgQ2FyZ2FyIGRhdGFzZXQKWCA8LSBhcy5kYXRhLmZyYW1lKHN0YXRlLng3NykKCiMgRXh0cmFlciB2YXJpYWJsZXMgZGUgaW50ZXLDqXMKeCA8LSBYJEluY29tZQp5IDwtIFgkYExpZmUgRXhwYAoKCmJpdmtkZSA8LSBrZGUyZCh4LCB5LCBuID0gMTAwKQoKCmJpdmtkZV9kZiA8LSBleHBhbmQuZ3JpZCh4ID0gYml2a2RlJHgsIHkgPSBiaXZrZGUkeSkKYml2a2RlX2RmJHogPC0gYXMudmVjdG9yKGJpdmtkZSR6KQoKCnAxIDwtIGdncGxvdChiaXZrZGVfZGYsIGFlcyh4ID0geCwgeSA9IHksIGZpbGwgPSB6KSkgKwogIGdlb21fdGlsZSgpICsKICBzY2FsZV9maWxsX3ZpcmlkaXNfYygpICsKICBsYWJzKHRpdGxlID0gIk1hcGEgZGUgY2Fsb3IgZGUgZGVuc2lkYWQgYml2YXJpYWRhIiwKICAgICAgIHggPSAiSW5jb21lIiwgeSA9ICJMaWZlIEV4cGVjdGFuY3kiLCBmaWxsID0gIkRlbnNpZGFkIikgKwogIHRoZW1lX21pbmltYWwoKQoKcHJpbnQocDEpCgoKcDIgPC0gZ2dwbG90KGJpdmtkZV9kZiwgYWVzKHggPSB4LCB5ID0geSwgeiA9IHopKSArCiAgZ2VvbV9jb250b3VyX2ZpbGxlZCgpICsKICBsYWJzKHRpdGxlID0gIkNvbnRvcm5vcyBkZSBkZW5zaWRhZCBiaXZhcmlhZGEiLAogICAgICAgeCA9ICJJbmNvbWUiLCB5ID0gIkxpZmUgRXhwZWN0YW5jeSIsIGZpbGwgPSAiTml2ZWwiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpwcmludChwMikKCgpgYGAKCkxvcyBncsOhZmljb3MgZGUgZGVuc2lkYWQgYml2YXJpYWRhIGRlICJJbmNvbWUiIHZlcnN1cyAiTGlmZSBFeHBlY3RhbmN5IiBwZXJtaXRlbiB2aXN1YWxpemFyIGPDs21vIHNlIGRpc3RyaWJ1eWVuIGNvbmp1bnRhbWVudGUgZXN0YXMgZG9zIHZhcmlhYmxlcyBlbiBlbCBjb25qdW50byBkZSBkYXRvczogZWwgbWFwYSBkZSBjYWxvciBtdWVzdHJhIHF1ZSBsYSBtYXlvciBjb25jZW50cmFjacOzbiBkZSBvYnNlcnZhY2lvbmVzIHNlIGVuY3VlbnRyYSBlbiB1biByYW5nbyBkZSBpbmdyZXNvcyBlbnRyZSBhcHJveGltYWRhbWVudGUgNDUwMCB5IDYwMDAgdW5pZGFkZXMgbW9uZXRhcmlhcyB5IHVuYSBlc3BlcmFuemEgZGUgdmlkYSBlbnRyZSA3MCB5IDcxIGHDsW9zLCBsbyBjdWFsIHNlIHJlcHJlc2VudGEgY29uIGNvbG9yZXMgbcOhcyBjbGFyb3MgKGFtYXJpbGxvKTsgcG9yIHN1IHBhcnRlLCBlbCBncsOhZmljbyBkZSBjb250b3Jub3MgcmVmdWVyemEgZXN0YSBpbnRlcnByZXRhY2nDs24gYWwgZGVsaW1pdGFyIHpvbmFzIGRlIGlndWFsIGRlbnNpZGFkLCBkZXN0YWNhbmRvIHVuIG7DumNsZW8gY2VudHJhbCBkZSBhbHRhIGRlbnNpZGFkIHJvZGVhZG8gcG9yIG5pdmVsZXMgZGVjcmVjaWVudGVzLCBsbyBxdWUgc3VnaWVyZSB1bmEgcmVsYWNpw7NuIHBvc2l0aXZhIG1vZGVyYWRhIGVudHJlIGluZ3Jlc28geSBlc3BlcmFuemEgZGUgdmlkYSwgY29uIHBvY29zIHZhbG9yZXMgZXh0cmVtb3MgYWxlamFkb3MgZGUgZXN0YSBjb25jZW50cmFjacOzbiBwcmluY2lwYWwuCgpWaXN1YWxpemFtb3MgbG8gYW50ZXJpb3IgZW4gZW4gdW4gZ3JhZmljbyBkaW5hbWljbwoKYGBge3J9CnAzIDwtIHBsb3RfbHkoeCA9IGJpdmtkZSR4LCB5ID0gYml2a2RlJHksIHogPSBiaXZrZGUkeikgJT4lCiAgYWRkX3N1cmZhY2UoY29sb3JzY2FsZSA9ICdWaXJpZGlzJykgJT4lCiAgbGF5b3V0KHRpdGxlID0gIlN1cGVyZmljaWUgZGUgZGVuc2lkYWQgYml2YXJpYWRhICgzRCkiLAogICAgICAgICBzY2VuZSA9IGxpc3QoCiAgICAgICAgICAgeGF4aXMgPSBsaXN0KHRpdGxlID0gIkluY29tZSIpLAogICAgICAgICAgIHlheGlzID0gbGlzdCh0aXRsZSA9ICJMaWZlIEV4cGVjdGFuY3kiKSwKICAgICAgICAgICB6YXhpcyA9IGxpc3QodGl0bGUgPSAiRGVuc2lkYWQiKQogICAgICAgICApKQoKcDMgIAoKYGBgCgpDb21wYXJhY2nDs24gZGUga2VybmVsIEdhdXNzaWFubyB2cyBFcGFuZWNobmlrb3YKCkxhIHByaW5jaXBhbCBkaWZlcmVuY2lhIGVudHJlIGVsIGtlcm5lbCBHYXVzc2lhbm8geSBlbCBFcGFuZWNobmlrb3YgcmFkaWNhIGVuIGxhIGZvcm1hIGVuIHF1ZSBzdWF2aXphbiBsYSBkaXN0cmlidWNpw7NuIGRlIGxvcyBkYXRvczogZWzCoCoqa2VybmVsIEdhdXNzaWFubyoqwqB1dGlsaXphIHVuYSBmdW5jacOzbiBkZSBkZW5zaWRhZCBub3JtYWwgcXVlIGFzaWduYSBwZXNvIGEgdG9kb3MgbG9zIHB1bnRvcywgYXVucXVlIGRlY3JlY2UgZXhwb25lbmNpYWxtZW50ZSBjb24gbGEgZGlzdGFuY2lhLCBsbyBxdWUgcHJvZHVjZSBjdXJ2YXMgc3VhdmVzIHkgZXh0ZW5kaWRhczsgZW4gY2FtYmlvLCBlbMKgKiprZXJuZWwgRXBhbmVjaG5pa292KirCoGVzIG3DoXMgZWZpY2llbnRlIGNvbXB1dGFjaW9uYWxtZW50ZSwgdGllbmUgc29wb3J0ZSBjb21wYWN0byAoc29sbyBjb25zaWRlcmEgcHVudG9zIGRlbnRybyBkZSB1biBjaWVydG8gcmFuZ28pIHkgZ2VuZXJhIGN1cnZhcyBtw6FzIGNvbmNlbnRyYWRhcyBhbHJlZGVkb3IgZGUgbG9zIGRhdG9zLCBjb24gYm9yZGVzIG3DoXMgbWFyY2Fkb3MuIEVuIGxhIHByw6FjdGljYSwgYW1ib3MgZXN0aW1hZG9yZXMgc3VlbGVuIHByb2R1Y2lyIHJlc3VsdGFkb3Mgc2ltaWxhcmVzLCBwZXJvIGVsIEVwYW5lY2huaWtvdiBlcyDDs3B0aW1vIGVuIHTDqXJtaW5vcyBkZSB2YXJpYW56YSBtw61uaW1hIHBhcmEgbXVjaG9zIGNhc29zLCBtaWVudHJhcyBxdWUgZWwgR2F1c3NpYW5vIGVzIHByZWZlcmlkbyBwb3Igc3Ugc3VhdmlkYWQgeSBmYW1pbGlhcmlkYWQuCgpgYGB7ciBmaWcuaGVpZ2h0PTEyLCBmaWcud2lkdGg9MTIsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CnBsb3RfbGlzdCA8LSBsYXBwbHkodW5pcXVlKFhfbG9uZyRWYXJpYWJsZSksIGZ1bmN0aW9uKHZhcil7CiAgZ2dwbG90KGZpbHRlcihYX2xvbmcsIFZhcmlhYmxlID09IHZhciksIGFlcyh4ID0gVmFsdWUpKSArCiAgICBnZW9tX2RlbnNpdHkoa2VybmVsID0gImdhdXNzaWFuIiwgY29sb3IgPSAiYmx1ZSIsIGZpbGwgPSAiYmx1ZSIsIGFscGhhID0gMC4zKSArCiAgICBnZW9tX2RlbnNpdHkoa2VybmVsID0gImVwYW5lY2huaWtvdiIsIGNvbG9yID0gImdyZWVuIiwgZmlsbCA9ICJncmVlbiIsIGFscGhhID0gMC4yKSArCiAgICBsYWJzKHRpdGxlID0gcGFzdGUoIktlcm5lbCBHYXVzc2lhbm8gdnMgRXBhbmVjaG5pa292OiIsIHZhcikpICsKICAgIHRoZW1lX21pbmltYWwoKQp9KQp3cmFwX3Bsb3RzKHBsb3RfbGlzdCkKCmBgYAoKRWwgZ3LDoWZpY28gY29tcGFyYSBkb3MgZXN0aW1hY2lvbmVzIGRlIGRlbnNpZGFkIGtlcm5lbCBwYXJhIGNhZGEgdmFyaWFibGUgZGVsIGRhdGFzZXTCoGBzdGF0ZS54NzdgwqB1c2FuZG8gbG9zIG7DumNsZW9zwqAqKkdhdXNzaWFubyoqwqAobMOtbmVhIGF6dWwpIHnCoCoqRXBhbmVjaG5pa292KirCoChsw61uZWEgdmVyZGUpLiBDYWRhIHBhbmVsIG11ZXN0cmEgY8OzbW8gdmFyw61hIGxhIGZvcm1hIGRlIGxhIGRpc3RyaWJ1Y2nDs24gc2Vnw7puIGVsIHRpcG8gZGUga2VybmVsLCBhdW5xdWUgZW4gbGEgbWF5b3LDrWEgZGUgbG9zIGNhc29zIGxhcyBkaWZlcmVuY2lhcyBzb24gc3V0aWxlcy4gQW1ib3MgbcOpdG9kb3Mgc3Vhdml6YW4gbGEgZGlzdHJpYnVjacOzbiwgcGVybyBlbCBrZXJuZWwgZ2F1c3NpYW5vIHN1ZWxlIGdlbmVyYXIgY3VydmFzIG3DoXMgc3VhdmVzIHkgZXh0ZW5kaWRhcywgbWllbnRyYXMgcXVlIGVsIEVwYW5lY2huaWtvdiB0aWVuZGUgYSBzZXIgbcOhcyBlZmljaWVudGUgeSBsb2NhbCwgbWFyY2FuZG8gcGljb3MgbGlnZXJhbWVudGUgbcOhcyBhZ3Vkb3MuIEVzdGEgY29tcGFyYWNpw7NuIHBlcm1pdGUgdmVyIHF1ZSwgc2kgYmllbiBlbCBuw7pjbGVvIGVsZWdpZG8gcHVlZGUgaW5mbHVpciBsZXZlbWVudGUgZW4gbGEgZm9ybWEgZXN0aW1hZGEsIGVuIGRpc3RyaWJ1Y2lvbmVzIGJpZW4gZGVmaW5pZGFzIChjb21vwqBgSW5jb21lYMKgb8KgYExpZmUgRXhwYCkgYW1iYXMgY3VydmFzIHNvbiBtdXkgc2ltaWxhcmVzLCBtaWVudHJhcyBxdWUgZW4gb3RyYXMgbcOhcyBpcnJlZ3VsYXJlcyBvIGFzaW3DqXRyaWNhcyAoY29tb8KgYEFyZWFgwqBvwqBgUG9wdWxhdGlvbmApIGxhcyBkaWZlcmVuY2lhcyBwdWVkZW4gc2VyIG3DoXMgZXZpZGVudGVzLiBFbiBnZW5lcmFsLCBlbCBhbsOhbGlzaXMgcmVzYWx0YSBsYSBpbXBvcnRhbmNpYSBkZSBjb25zaWRlcmFyIGVsIHRpcG8gZGUga2VybmVsIGFsIGludGVycHJldGFyIGVzdHJ1Y3R1cmFzIGVuIGxvcyBkYXRvcy4KClNjYXR0ZXJwbG90IEluY29tZSB2cyBMaWZlIEV4cAoKYGBge3J9CmdncGxvdChYLCBhZXMoeCA9IEluY29tZSwgeSA9IGBMaWZlIEV4cGApKSArCiAgZ2VvbV9wb2ludChjb2xvciA9ICJibHVlIikgKwogIGxhYnModGl0bGUgPSAiSW5jb21lIHZzIExpZmUgRXhwZWN0YW5jeSIpICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKRWwgZ3LDoWZpY28gZGUgZGlzcGVyc2nDs24gbXVlc3RyYSBsYSByZWxhY2nDs24gZW50cmUgZWwgaW5ncmVzbyBwZXIgY8OhcGl0YSAoYEluY29tZWApIHkgbGEgZXNwZXJhbnphIGRlIHZpZGEgKGBMaWZlIEV4cGApIGVuIGxvcyBlc3RhZG9zIGRlIEVFLiBVVS4sIHJldmVsYW5kbyB1bmHCoCoqdGVuZGVuY2lhIHBvc2l0aXZhIGdlbmVyYWwqKiwgZG9uZGUgbG9zIGVzdGFkb3MgY29uIG1heW9yZXMgaW5ncmVzb3MgdGllbmRlbiBhIHRlbmVyIHVuYSBtYXlvciBlc3BlcmFuemEgZGUgdmlkYS4gU2luIGVtYmFyZ28sIGxhIHJlbGFjacOzbiBubyBlcyBwZXJmZWN0YW1lbnRlIGxpbmVhbCwgeWEgcXVlIGhheSBjaWVydGEgZGlzcGVyc2nDs24geSBhbGd1bm9zIGVzdGFkb3MgY29uIGluZ3Jlc29zIGFsdG9zIG5vIG5lY2VzYXJpYW1lbnRlIHRpZW5lbiBsYSBtYXlvciBlc3BlcmFuemEgZGUgdmlkYSwgbG8gcXVlIHN1Z2llcmUgcXVlIGF1bnF1ZSBlbCBpbmdyZXNvIGluZmx1eWUsIG90cm9zIGZhY3RvcmVzIHRhbWJpw6luIGp1ZWdhbiB1biBwYXBlbCBpbXBvcnRhbnRlIGVuIGxhIGxvbmdldmlkYWQuIEVzdGEgdmlzdWFsaXphY2nDs24gcGVybWl0ZSBpZGVudGlmaWNhciBwYXRyb25lcyBnZW5lcmFsZXMsIGFzw60gY29tbyBwb3NpYmxlcyBleGNlcGNpb25lcyBvIHZhbG9yZXMgYXTDrXBpY29zLgoKR3LDoWZpY28gM0QgY29uIHBsb3RseQoKYGBge3J9CnBsb3RfbHkoZGF0YSA9IFgsIHggPSB+SW5jb21lLCB5ID0gfmBMaWZlIEV4cGAsIHogPSB+TXVyZGVyLCAKICAgICAgICB0eXBlID0gInNjYXR0ZXIzZCIsIG1vZGUgPSAibWFya2VycyIsCiAgICAgICAgbWFya2VyID0gbGlzdChzaXplID0gNSwgY29sb3IgPSAiYmx1ZSIpKQoKYGBgCgpQYWlycyBwbG90IChzY2F0dGVycGxvdCBtYXRyaXgpCgpgYGB7ciBmaWcuaGVpZ2h0PTEyLCBmaWcud2lkdGg9MTIsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CkdHYWxseTo6Z2dwYWlycyhYKQoKYGBgCgpFbCBncsOhZmljbyBnZW5lcmFkbyBjb27CoGBHR2FsbHk6OmdncGFpcnMoWClgwqBtdWVzdHJhIHVuYSBtYXRyaXogZGUgY29ycmVsYWNpb25lcyB5IHJlbGFjaW9uZXMgYml2YXJpYWRhcyBlbnRyZSB0b2RhcyBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMgZGVsIGRhdGFzZXTCoGBzdGF0ZS54NzdgLiBDYWRhIGNlbGRhIGluZmVyaW9yIHByZXNlbnRhIHVuIGRpYWdyYW1hIGRlIGRpc3BlcnNpw7NuIHF1ZSBwZXJtaXRlIHZpc3VhbGl6YXIgbGEgcmVsYWNpw7NuIGVudHJlIGRvcyB2YXJpYWJsZXMsIG1pZW50cmFzIHF1ZSBsYXMgZGlhZ29uYWxlcyBtdWVzdHJhbiBsYSBkaXN0cmlidWNpw7NuIHVuaXZhcmlhZGEgKGRlbnNpZGFkKSBkZSBjYWRhIHZhcmlhYmxlLCB5IGxhcyBjZWxkYXMgc3VwZXJpb3JlcyBwcmVzZW50YW4gbG9zIGNvZWZpY2llbnRlcyBkZSBjb3JyZWxhY2nDs24gZGUgUGVhcnNvbiwgaW5jbHV5ZW5kbyBzdSBzaWduaWZpY2FuY2lhIGVzdGFkw61zdGljYSAoYCpgLMKgYCoqYCzCoGAqKipgKS4gU2UgZGVzdGFjYW4gY29ycmVsYWNpb25lcyBmdWVydGVzIGNvbW8gbGEgbmVnYXRpdmEgZW50cmXCoGBJbGxpdGVyYWN5YMKgecKgYEhTIEdyYWRgwqAoLTAuNjU3KSwgeSBlbnRyZcKgYE11cmRlcmDCoHnCoGBMaWZlIEV4cGDCoCgtMC43ODEpLCBsbyBxdWUgc3VnaWVyZSBxdWUgbWF5b3JlcyB0YXNhcyBkZSBhbGZhYmV0aXphY2nDs24geSBlZHVjYWNpw7NuIHNlY3VuZGFyaWEgZXN0w6FuIGFzb2NpYWRhcyBjb24gbWVub3IgY3JpbWluYWxpZGFkIHkgbWF5b3IgZXNwZXJhbnphIGRlIHZpZGEuIFRhbWJpw6luIHNlIG9ic2VydmEgdW5hIGNvcnJlbGFjacOzbiBwb3NpdGl2YSBtb2RlcmFkYSBlbnRyZcKgYEluY29tZWB5wqBgSFMgR3JhZGDCoCgwLjYyMCksIHkgZW50cmXCoGBJbmNvbWVgwqB5wqBgQXJlYWDCoCgwLjM2MyksIGxvIHF1ZSBwdWVkZSBpbmRpY2FyIHVuYSBhc29jaWFjacOzbiBlbnRyZSBkZXNhcnJvbGxvIGVjb27Ds21pY28geSBleHRlbnNpw7NuIHRlcnJpdG9yaWFsLiBFc3RlIHRpcG8gZGUgZ3LDoWZpY28gZmFjaWxpdGEgbGEgZGV0ZWNjacOzbiBkZSBwYXRyb25lcywgYXNvY2lhY2lvbmVzIGNsYXZlIHkgcG9zaWJsZXMgdmFyaWFibGVzIGV4cGxpY2F0aXZhcyBwYXJhIGFuw6FsaXNpcyBwb3N0ZXJpb3JlcyBjb21vIHJlZ3Jlc2nDs24gbyBzZWdtZW50YWNpw7NuLgoKQ29vcmRlbmFkYXMgcGFyYWxlbGFzCgpgYGB7ciBmaWcuaGVpZ2h0PTcsIGZpZy53aWR0aD0xNSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KcGFyY29vcmQoWCwgY29sID0gImJsdWUiLCB2YXIubGFiZWwgPSBUUlVFKQoKYGBgCgpFc3RlIGVzIHVuwqAqKmdyw6FmaWNvIGRlIGNvb3JkZW5hZGFzIHBhcmFsZWxhcyoqwqBxdWUgcmVwcmVzZW50YSB0b2RhcyBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMgZGVsIGRhdGFzZXTCoGBzdGF0ZS54NzdgwqBwYXJhIGxvcyA1MCBlc3RhZG9zIGRlIEVFLiBVVS4gQ2FkYSBsw61uZWEgYXp1bCByZXByZXNlbnRhIHVuIGVzdGFkbywgeSBzdSByZWNvcnJpZG8gY29uZWN0YSBsb3MgdmFsb3JlcyBxdWUgdGllbmUgZXNlIGVzdGFkbyBlbiB2YXJpYWJsZXMgY29tb8KgYFBvcHVsYXRpb25gLMKgYEluY29tZWAswqBgSWxsaXRlcmFjeWAswqBgTGlmZSBFeHBgLMKgYE11cmRlcmAswqBgSFMgR3JhZGAswqBgRnJvc3RgwqB5wqBgQXJlYWAuIEFsIG9ic2VydmFyIGxhIHRyYW1hIGRlIGzDrW5lYXMsIHNlIHB1ZWRlbiBpZGVudGlmaWNhciBwYXRyb25lcyBnZW5lcmFsZXM6IHBvciBlamVtcGxvLCBlc3RhZG9zIGNvbiBhbHRhIHRhc2EgZGXCoGBJbGxpdGVyYWN5YMKgdGllbmRlbiBhIHRlbmVyIG1lbm9ywqBgTGlmZSBFeHBgwqB5IG1heW9ywqBgTXVyZGVyYCwgbWllbnRyYXMgcXVlIGFxdWVsbG9zIGNvbiBtYXlvcsKgYEhTIEdyYWRgwqBzdWVsZW4gdGVuZXIgbWVub3LCoGBJbGxpdGVyYWN5YMKgeSBtYXlvcsKgYEluY29tZWAuIFRhbWJpw6luIHNlIG9ic2VydmFuIGFsZ3Vub3MgZXN0YWRvcyBxdWUgc2UgY29tcG9ydGFuIGNvbW8gb3V0bGllcnMsIGNvbiB2YWxvcmVzIGV4dHJlbW9zIGVuwqBgUG9wdWxhdGlvbmDCoG/CoGBBcmVhYC4gRXN0ZSB0aXBvIGRlIGdyw6FmaWNvIGVzIMO6dGlsIHBhcmEgZXhwbG9yYXIgcmVsYWNpb25lcyBtdWx0aXZhcmlhZGFzIHkgY29udHJhc3RhciBwZXJmaWxlcyBkZSBvYnNlcnZhY2lvbmVzIHNpbiBuZWNlc2lkYWQgZGUgcmVkdWNpciBsYSBkaW1lbnNpb25hbGlkYWQuCgojIyMgMS4xLjIpIE1lZGlkYXMgZGVzY3JpcHRpdmFzIG11bHRpdmFyaWFudGVzCgpDYWxjdWxhbW9zIGxhcyBtZWRpYXMgcGFyYSBjYWRhIHVuYXMgZGUgbGFzIHZhcmlhYmxlcwoKYGBge3J9Cm11LlggPC0gY29sTWVhbnMoWCkKbXUuWAoKYGBgCgpDYWxjdWxhbW9zIHByb2Z1bmRpZGFkIGRlIFR1a2V5CgpgYGB7cn0KZGVwdGguWCA8LSBkZXB0aC5oYWxmc3BhY2UoWCwgWCwgbnVtLmRpcmVjdGlvbnMgPSAxMDAwMDAsIHNlZWQgPSAxKQpzb3J0LmRlcHRoLlggPC0gc29ydChkZXB0aC5YLCBkZWNyZWFzaW5nID0gVFJVRSwgaW5kZXgucmV0dXJuID0gVFJVRSkKCmRlcHRoLlguc29ydCA8LSBzb3J0LmRlcHRoLlgkeApkZXB0aC5YLnNvcnQuaW5kZXggPC0gc29ydC5kZXB0aC5YJGl4CgojIEVzdGFkbyBtw6FzIHByb2Z1bmRvIChtw6FzIGNlbnRyYWwpClhbZGVwdGguWC5zb3J0LmluZGV4WzFdLCBdCgpgYGAKCkVsIGFuw6FsaXNpcyBhcGxpY2EgbGEgbWVkaWRhIGRlwqAqKnByb2Z1bmRpZGFkIGRlIGhhbGZzcGFjZSAobyBUdWtleSBkZXB0aCkqKsKgYWwgZGF0YXNldMKgYHN0YXRlLng3N2AsIHVuYSB0w6ljbmljYSBkZSBlc3RhZMOtc3RpY2Egcm9idXN0YSBxdWUgZXZhbMO6YSBjdcOhbiAiY2VudHJhbCIgZXMgY2FkYSBvYnNlcnZhY2nDs24gZGVudHJvIGRlbCBjb25qdW50byBtdWx0aXZhcmlhZG8uIFNlIGNhbGN1bGFyb24gbGFzIHByb2Z1bmRpZGFkZXMgZGUgdG9kb3MgbG9zIGVzdGFkb3MgY29uIHJlc3BlY3RvIGFsIGNvbmp1bnRvIGNvbXBsZXRvLCB1c2FuZG8gMTAwLjAwMCBkaXJlY2Npb25lcyBhbGVhdG9yaWFzLCB5IGx1ZWdvIHNlIG9yZGVuYXJvbiBlbiBvcmRlbiBkZXNjZW5kZW50ZSBwYXJhIGlkZW50aWZpY2FyIGVsIGVzdGFkbyBtw6FzIHByb2Z1bmRvLCBlcyBkZWNpciwgZWwgbcOhcyByZXByZXNlbnRhdGl2byBvIGNlbnRyYWwgc2Vnw7puIGxhIGRpc3RyaWJ1Y2nDs24gbXVsdGl2YXJpYWRhIGRlIGxhcyB2YXJpYWJsZXMuIEVsIHJlc3VsdGFkbyBtdWVzdHJhIHF1ZcKgKipXaXNjb25zaW4qKsKgZXMgZWwgZXN0YWRvIG3DoXMgcHJvZnVuZG8sIGxvIHF1ZSBpbmRpY2EgcXVlIHN1cyB2YWxvcmVzIGVuIHZhcmlhYmxlcyBjb21vwqBgUG9wdWxhdGlvbmAswqBgSW5jb21lYCzCoGBJbGxpdGVyYWN5YCzCoGBMaWZlIEV4cGAswqBgTXVyZGVyYCzCoGBIUyBHcmFkYCzCoGBGcm9zdGDCoHnCoGBBcmVhYMKgZXN0w6FuIGVxdWlsaWJyYWRhbWVudGUgdWJpY2Fkb3MgcmVzcGVjdG8gYSBsYXMgZGVtw6FzIG9ic2VydmFjaW9uZXMsIHNpbiBzZXIgZXh0cmVtb3MgZW4gbmluZ3VuYSBkaW1lbnNpw7NuLCB5IHBvciB0YW50byByZXByZXNlbnRhIGJpZW4gZWwgImNlbnRybyIgZGVsIGNvbmp1bnRvIGRlIGRhdG9zLgoKQ2FsY3VsYW1vcyBtYXRyaXogZGUgY292YXJpYW56YSB5IGNvcnJlbGFjacOzbgoKYGBge3J9ClMuWCA8LSBjb3YoWCkKUi5YIDwtIGNvcihYKQoKZWlnZW5fUyA8LSBlaWdlbihTLlgpCmVpZ2VuX1IgPC0gZWlnZW4oUi5YKQoKbGlzdCgKICBDb3ZhcmlhbnphID0gUy5YLAogIEF1dG92YWxvcmVzX1MgPSBlaWdlbl9TJHZhbHVlcywKICBBdXRvdmVjdG9yZXNfUyA9IGVpZ2VuX1MkdmVjdG9ycywKICBDb3JyZWxhY2nDs24gPSBSLlgsCiAgQXV0b3ZhbG9yZXNfUiA9IGVpZ2VuX1IkdmFsdWVzLAogIEF1dG92ZWN0b3Jlc19SID0gZWlnZW5fUiR2ZWN0b3JzLAogIFRyYXphX1MgPSBzdW0oZWlnZW5fUyR2YWx1ZXMpLAogIERldGVybWluYW50ZV9TID0gZGV0KFMuWCksCiAgVHJhemFfUiA9IHN1bShlaWdlbl9SJHZhbHVlcyksCiAgRGV0ZXJtaW5hbnRlX1IgPSBkZXQoUi5YKQopCgpgYGAKCkVzdGUgYW7DoWxpc2lzIGV4cGxvcmEgbGFzIHByb3BpZWRhZGVzIGVzdHJ1Y3R1cmFsZXMgZGVsIGNvbmp1bnRvIGRlIGRhdG9zwqBgc3RhdGUueDc3YMKgbWVkaWFudGUgbGEgbWF0cml6IGRlwqAqKmNvdmFyaWFuemEqKsKgeSBsYSBtYXRyaXogZGXCoCoqY29ycmVsYWNpw7NuKiosIGp1bnRvIGNvbiBzdXPCoCoqYXV0b3ZhbG9yZXMqKsKgecKgKiphdXRvdmVjdG9yZXMqKi4gTGEgbWF0cml6IGRlIGNvdmFyaWFuemEgKGBTLlhgKSByZWZsZWphIGPDs21vIHZhcsOtYW4gY29uanVudGFtZW50ZSBsYXMgdmFyaWFibGVzIGVuIHN1cyB1bmlkYWRlcyBvcmlnaW5hbGVzLCBtb3N0cmFuZG8gcXVlwqBgQXJlYWDCoHnCoGBQb3B1bGF0aW9uYMKgcHJlc2VudGFuIHVuYSB2YXJpYWJpbGlkYWQgZXNwZWNpYWxtZW50ZSBhbHRhLCBsbyBxdWUgdGFtYmnDqW4gc2UgZXZpZGVuY2lhIGVuIHN1IGF1dG92YWxvciBkb21pbmFudGUgKOKJiCA3LjI4ZSswOSkuIExvcyBhdXRvdmFsb3JlcyBkZcKgYFMuWGDCoGluZGljYW4gY3XDoW50YSB2YXJpYW56YSBleHBsaWNhIGNhZGEgY29tcG9uZW50ZSBwcmluY2lwYWwsIG1pZW50cmFzIHF1ZSBzdXMgYXV0b3ZlY3RvcmVzIGRldGVybWluYW4gbGEgZGlyZWNjacOzbiBkZSBlc3RvcyBjb21wb25lbnRlcy4gTGHCoCoqdHJhemEqKsKgKHN1bWEgZGUgYXV0b3ZhbG9yZXMpIGRlwqBgUy5YYMKgcmVwcmVzZW50YSBsYSB2YXJpYW56YSB0b3RhbCBkZWwgc2lzdGVtYSAo4omIIDcuM2UrMDkpIHkgc3XCoCoqZGV0ZXJtaW5hbnRlKirCoGVzIG11eSBhbHRvLCBsbyBjdWFsIHN1Z2llcmUgcXVlIGxhcyB2YXJpYWJsZXMgbm8gZXN0w6FuIGZ1ZXJ0ZW1lbnRlIGNvcnJlbGFjaW9uYWRhcyBsaW5lYWxtZW50ZS4gRW4gY29udHJhc3RlLCBsYSBtYXRyaXogZGXCoCoqY29ycmVsYWNpw7NuKirCoChgUi5YYCkgZXN0YW5kYXJpemEgbGFzIHJlbGFjaW9uZXMsIGVsaW1pbmFuZG8gbGFzIHVuaWRhZGVzIHkgcGVybWl0aWVuZG8gY29tcGFyYXIgcmVsYWNpb25lcyBlbnRyZSB2YXJpYWJsZXMgZGUgZGlmZXJlbnRlIGVzY2FsYS4gU3VzIGF1dG92YWxvcmVzIHN1bWFuIDggKHBvciB0ZW5lciA4IHZhcmlhYmxlcyBlc3RhbmRhcml6YWRhcyksIHkgZWwgZGV0ZXJtaW5hbnRlIGJham8gKOKJiCAwLjAwODkpIGluZGljYSBxdWUgaGF5IHJlZHVuZGFuY2lhIG8gYWx0YSBjb3JyZWxhY2nDs24gZW50cmUgYWxndW5hcyB2YXJpYWJsZXMuIEVzdGUgYW7DoWxpc2lzIGVzIGZ1bmRhbWVudGFsIGVuwqAqKmFuw6FsaXNpcyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcyAoUENBKSoqwqB5IHBlcm1pdGUgY29tcHJlbmRlciB0YW50byBsYSBtYWduaXR1ZCBjb21vIGxhIGVzdHJ1Y3R1cmEgZGUgbGFzIHJlbGFjaW9uZXMgZW50cmUgdmFyaWFibGVzLgoKRXN0YW5kYXJpemFjacOzbiB1bml2YXJpYW50ZSB5IG11bHRpdmFyaWFudGUKCmBgYHtyIGZpZy5oZWlnaHQ9MTAsIGZpZy53aWR0aD0xNSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KIyBVbml2YXJpYW50ZQpzWCA8LSBzY2FsZShYKQpHR2FsbHk6OmdncGFpcnMoYXMuZGF0YS5mcmFtZShzWCkpCgojIE11bHRpdmFyaWFudGUgKGVzZmVyaXphY2nDs24pCmlTLlggPC0gc29sdmUoUy5YKQplIDwtIGVpZ2VuKGlTLlgpClYgPC0gZSR2ZWN0b3JzCkIgPC0gViAlKiUgZGlhZyhzcXJ0KGUkdmFsdWVzKSkgJSolIHQoVikKWHRpbCA8LSBzY2FsZShYLCBzY2FsZSA9IEZBTFNFKQpTWCA8LSBYdGlsICUqJSBCCgpjb2xNZWFucyhTWCkKY292KFNYKQpHR2FsbHk6OmdncGFpcnMoYXMuZGF0YS5mcmFtZShTWCkpCgpgYGAKCkZpbmFsbWVudGUsIGdyYWZpY2Ftb3MgbGEgbWVkaWFuYSBtdWx0aXZhcmlhZGEgcG9yIHJlZ2nDs24KCmBgYHtyfQojICBDYXJmYW1vcyBEYXRhc2V0ICBudWV2bWFlbnRlClggPC0gYXMuZGF0YS5mcmFtZShzdGF0ZS54NzcpClggPC0gcm93bmFtZXNfdG9fY29sdW1uKFgsICJFc3RhZG8iKQoKIyBTaSB0aWVuZXMgdW5hIHZhcmlhYmxlIGRlIGFncnVwYWNpw7NuIGNvbW8gUmVnacOzbiwgcHVlZGVzIGFncmVnYXJsYSBhcXXDrQojIFBvciBlamVtcGxvOgpYJFJlZ2lvbiA8LSBzdGF0ZS5yZWdpb24gICMgdmVjdG9yIGRpc3BvbmlibGUgZW4gUiBiYXNlCgojIENhbGN1bGFyIGxhIG1lZGlhbmEgbXVsdGl2YXJpYWRhIHBvciBncnVwbyAocG9yIGVqZW1wbG8sIHBvciBSZWdpw7NuKQptZWRpYW5hc19yZWdpb24gPC0gWCAlPiUKICBncm91cF9ieShSZWdpb24pICU+JQogIGdyb3VwX21vZGlmeSh+IHsKICAgIGQgPC0gZGVwdGguaGFsZnNwYWNlKC54WywgYygiUG9wdWxhdGlvbiIsICJJbmNvbWUiLCAiSWxsaXRlcmFjeSIsICJMaWZlIEV4cCIpXSwKICAgICAgICAgICAgICAgICAgICAgICAgIC54WywgYygiUG9wdWxhdGlvbiIsICJJbmNvbWUiLCAiSWxsaXRlcmFjeSIsICJMaWZlIEV4cCIpXSwKICAgICAgICAgICAgICAgICAgICAgICAgIG51bS5kaXJlY3Rpb25zID0gMTAwMDAwLCBzZWVkID0gMSkKICAgIGlkeCA8LSB3aGljaC5tYXgoZCkKICAgIC54W2lkeCwgXQogIH0pCgoKIyBHcsOhZmljbyBjb24gZXRpcXVldGFzIGRlIHJlZ2nDs24gZW4gbGFzIG1lZGlhbmFzCmdncGxvdChYLCBhZXMoeCA9IEluY29tZSwgeSA9IGBMaWZlIEV4cGAsIGNvbG9yID0gUmVnaW9uKSkgKwogIGdlb21fcG9pbnQoYWxwaGEgPSAwLjYpICsKICBnZW9tX3BvaW50KGRhdGEgPSBtZWRpYW5hc19yZWdpb24sCiAgICAgICAgICAgICBhZXMoeCA9IEluY29tZSwgeSA9IGBMaWZlIEV4cGApLAogICAgICAgICAgICAgc2hhcGUgPSAyMSwgc2l6ZSA9IDUsIGZpbGwgPSAiYmxhY2siLCBjb2xvciA9ICJ3aGl0ZSIpICsKICBnZW9tX3RleHRfcmVwZWwoZGF0YSA9IG1lZGlhbmFzX3JlZ2lvbiwKICAgICAgICAgICAgICAgICAgYWVzKHggPSBJbmNvbWUsIHkgPSBgTGlmZSBFeHBgLCBsYWJlbCA9IFJlZ2lvbiksCiAgICAgICAgICAgICAgICAgIGNvbG9yID0gImJsYWNrIiwgc2l6ZSA9IDQsIGZvbnRmYWNlID0gImJvbGQiKSArCiAgbGFicyh0aXRsZSA9ICJNZWRpYW5hIG11bHRpdmFyaWFkYSBwb3IgcmVnacOzbiBjb24gZXRpcXVldGFzIikgKwogIHRoZW1lX21pbmltYWwoKQoKCmBgYAoKRXN0ZSBjw7NkaWdvIGVuIFIgY2FyZ2EgZWwgY29uanVudG8gZGUgZGF0b3Mgc3RhdGUueDc3LCBhw7FhZGUgZWwgbm9tYnJlIGRlbCBlc3RhZG8gY29tbyBjb2x1bW5hIHkgYXNpZ25hIGEgY2FkYSBlc3RhZG8gdW5hIHJlZ2nDs24gKFJlZ2lvbikgdXNhbmRvIGVsIHZlY3RvciBzdGF0ZS5yZWdpb24uIEx1ZWdvLCBjYWxjdWxhIGxhIG1lZGlhbmEgbXVsdGl2YXJpYWRhIHBvciByZWdpw7NuIHV0aWxpemFuZG8gbGEgZnVuY2nDs24gZGVwdGguaGFsZnNwYWNlLCBxdWUgaWRlbnRpZmljYSBlbCBwdW50byBtw6FzIGNlbnRyYWwgKGNvbiBtYXlvciBwcm9mdW5kaWRhZCBlc3RhZMOtc3RpY2EpIGRlbnRybyBkZSBsYXMgdmFyaWFibGVzIFBvcHVsYXRpb24sIEluY29tZSwgSWxsaXRlcmFjeSB5IExpZmUgRXhwLiBGaW5hbG1lbnRlLCBzZSBnZW5lcmEgdW4gZ3LDoWZpY28gY29uIGdncGxvdDIgcXVlIG11ZXN0cmEgbGEgcmVsYWNpw7NuIGVudHJlIEluY29tZSAoaW5ncmVzbykgeSBMaWZlIEV4cCAoZXNwZXJhbnphIGRlIHZpZGEpLCBkb25kZSBjYWRhIHB1bnRvIHJlcHJlc2VudGEgdW4gZXN0YWRvIHkgZXN0w6EgY29sb3JlYWRvIHNlZ8O6biBzdSByZWdpw7NuLiBMb3MgcHVudG9zIGdyYW5kZXMgZW4gbmVncm8gaW5kaWNhbiBsYSBtZWRpYW5hIG11bHRpdmFyaWFkYSBkZSBjYWRhIHJlZ2nDs24sIGxvIHF1ZSBwZXJtaXRlIGNvbXBhcmFyIHZpc3VhbG1lbnRlIGxhIHBvc2ljacOzbiBjZW50cmFsIGRlIGNhZGEgZ3J1cG8gcmVnaW9uYWwgZW4gZWwgZXNwYWNpbyBkZSBsYXMgZG9zIHZhcmlhYmxlcyBzZWxlY2Npb25hZGFzLgoKIyMgMS4yKSBFamVtcGxvIGNvbiBQeXRob246CgpFbCBkYXRhc2V0IGRlIElyaXMgZXMgdW4gY29uanVudG8gZGUgZGF0b3MgY2zDoXNpY28gZW4gZXN0YWTDrXN0aWNhIHkgYXByZW5kaXphamUgYXV0b23DoXRpY28gcXVlIGNvbnRpZW5lIDE1MCBvYnNlcnZhY2lvbmVzIGRlIGZsb3JlcyBkZWwgZ8OpbmVybyBJcmlzLCBkaXZpZGlkYXMgZW4gdHJlcyBlc3BlY2llczogc2V0b3NhLCB2ZXJzaWNvbG9yIHkgdmlyZ2luaWNhLiBQYXJhIGNhZGEgZmxvciBzZSByZWdpc3RyYW4gY3VhdHJvIGNhcmFjdGVyw61zdGljYXMgbnVtw6lyaWNhczogbGFyZ28geSBhbmNobyBkZWwgc8OpcGFsbywgeSBsYXJnbyB5IGFuY2hvIGRlbCBww6l0YWxvIChlbiBjZW50w61tZXRyb3MpLCBsbyBxdWUgcGVybWl0ZSBhbmFsaXphciBkaWZlcmVuY2lhcyBlbnRyZSBlc3BlY2llcyB5IGFwbGljYXIgdMOpY25pY2FzIGRlIGNsYXNpZmljYWNpw7NuLCByZWR1Y2Npw7NuIGRlIGRpbWVuc2nDs24geSB2aXN1YWxpemFjacOzbiBtdWx0aXZhcmlhZGEuCgoqKlByaW1lcm8qKiwgY2FyZ2Ftb3MgZGF0b3MKCmBgYHtweXRob259CgoKIyBDYXJnYXIgZWwgZGF0YXNldAppcmlzX2J1bmNoID0gbG9hZF9pcmlzKCkKCiMgQ3JlYXIgZWwgRGF0YUZyYW1lIGNvbiBsYXMgdmFyaWFibGVzICsgY29sdW1uYSAnU3BlY2llcycKaXJpcyA9IHBkLkRhdGFGcmFtZSgKICAgIGRhdGE9aXJpc19idW5jaC5kYXRhLAogICAgY29sdW1ucz1pcmlzX2J1bmNoLmZlYXR1cmVfbmFtZXMKKQoKIyBBZ3JlZ2FyIGRpcmVjdGFtZW50ZSBsYSBjb2x1bW5hICdTcGVjaWVzJyBjb24gbm9tYnJlcyBsZWdpYmxlcwppcmlzWydTcGVjaWVzJ10gPSBpcmlzX2J1bmNoLnRhcmdldF9uYW1lc1tpcmlzX2J1bmNoLnRhcmdldF0KCgojIFZhbGlkYW1zIGxvcyBub21icmVzIGRlIGxhcyBjb2x1bW5hcyBkZWwgZGYKaXJpcy5jb2x1bW5zCgpgYGAKCkFob3JhIHZhbW9zIGEgaGFjZXJub3MgbGFzIHByaW1lcmFzIHByZWd1bnRhczoKCjEuICDCv0N1w6FudG9zIGRhdG9zIGhheSBlbiBlbCBkYXRhc2V0PwoyLiAgwr9DdcOhbnRhcyB2YXJpYWJsZXM/CjMuICDCv0N1w6FudG9zIGdydXBvcyBkZSB2YXJpYWJsZXMgbyBjYXRlZ29yw61hcz8KNC4gIMK/Q3XDoW50YXMgb2JzZXJ2YWNpb25lcyBjb3JyZXNwb25kZW4gYSBjYWRhIGdydXBvPwoKYGBge3B5dGhvbn0KaXJpcy5pbmZvKCkKYGBgCgpQb2RlbW9zIHZlciBxdWUgZWwgY29uanVudG8gZGUgZGF0b3MgdGluZSAxNTAgZmlsYXMgcG9yIDUgY29sdW1uYXMuIERlIGxhcyBjdWFsZXMgNCBzb24gbnVtZXJpY2FzIHkgdW5hIGVzIGNhdGVnb3JpY2EKCmBgYHtweXRob259CmlyaXMuaGVhZApgYGAKCmBgYHtweXRob259CmlyaXMuc2hhcGUKYGBgCgpWZW1vcyBlbCBmb3JtYXRvIGRlIGRlbCBkZiBxdWUgZXMgMTUwIHggNQoKUmVub21icmFtb3MgZGYKCmBgYHtweXRob259CmlyaXNfbnVtPWlyaXMKYGBgCgpWZW1vcyBsb3MgdmFsb3JlcyB1bmljb3MgZGUgbGEgdmFyaWFibGUgZXNwZWNpZQoKYGBge3B5dGhvbn0KaXJpc19udW1bJ1NwZWNpZXMnXS52YWx1ZV9jb3VudHMoKQpgYGAKCsK/UXXDqSBtZWRpZGFzIG51bcOpcmljYXMgcG9kZW1vcyBvYnRlbmVyIHBhcmEgZGVzY3JpYmlyIGxvcyBkYXRvcz8KCmBgYHtweXRob259CmlyaXNfbnVtLmRlc2NyaWJlKCkKYGBgCgpHcsOhZmljYW1lbnRlLCDCv2PDs21vIHBvZGVtb3MgdmVyIGxhIGNhbnRpZGFkIGRlIG9ic2VydmFjaW9uZXMgZW4gY2FkYSBncnVwbz8KCmBgYHtweXRob259CnNucy5jb3VudHBsb3QoJ1NwZWNpZXMnLGRhdGE9aXJpcykKcGx0LnNob3coKQpgYGAKClZlYW1vcyBjw7NtbyBoYWNlciBib3hwbG90cyBwb3IgZ3J1cG9zCgpgYGB7cHl0aG9uIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CiBCb3hwbG90cwpzbnMuYm94cGxvdCh4PSJTcGVjaWVzIiwgeT0iUGV0YWxMZW5ndGhDbSIsIGRhdGE9aXJpcyAgLGxpbmV3aWR0aD0yLjUgKQpwbHQuc2hvdygpCmBgYAoKYGBge3B5dGhvbiBmaWcuaGVpZ2h0PTE1LCBmaWcud2lkdGg9MTUsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CiMgQm94cGxvdHMgcGFyYSBjYWRhIHZhcmlhYmxlIGRpdmlkaWRvcyBwb3IgZ3J1cG9zIGVuICJTcGVjaWVzIgppcmlzX251bS5ib3hwbG90KGJ5PSJTcGVjaWVzIiwgZmlnc2l6ZT0oMTIsIDYpKQpwbHQuc2hvdygpICNlc3RvIMO6bHRpbW8gc2UgcG9uZSBjdWFuZG8gbm8gc2UgcXVpZXJlIHF1ZSBzYWxnYSBlbCDDumx0aW1vIG1lbnNhamUgaW5mb3JtYXRpdm8KYGBgCgpWZWFtb3MgY8OzbW8gaGFjZXIgaGlzdG9ncmFtYXMgcG9yIGdydXBvcwoKYGBge3B5dGhvbiBmaWcuaGVpZ2h0PTE1LCBmaWcud2lkdGg9MTUsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CiMgSGlzdG9ncmFtYXMgCmlyaXNfbnVtLmhpc3QoZWRnZWNvbG9yPSdibGFjaycsIGxpbmV3aWR0aD0xLjIsIGdyaWQ9RmFsc2UsIGZpZ3NpemU9KDEyLCA2KSkKcGx0LnNob3coKQpgYGAKCkNvbiBzY2lweShzYykuc3RhdHMgcG9kZW1vcyBvYnRlbmVyIG1lZGlkYXMgbnVtw6lyaWNhcyBxdWUgZGVzY3JpYmVuIGxhcyB2YXJpYWJsZXMKCmBgYHtweXRob24gZmlnLmhlaWdodD0xNSwgZmlnLndpZHRoPTE1LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQppcmlzc2sgPSBsb2FkX2lyaXMoKQoKI2lyaXNzayBlcyBhaG9yYSB1biBudW1weSBhcnJheSAKWCA9IGlyaXNzay5kYXRhClkgPSBpcmlzc2sudGFyZ2V0CgojIERlc2NyaXBjaW9uIGVzdGFkaXN0aWNhCnNjLnN0YXRzLmRlc2NyaWJlKFgpCmBgYAoKRGVuc2lkYWRlcyBrZXJuZWxzCgpgYGB7cHl0aG9uIGZpZy5oZWlnaHQ9MTUsIGZpZy53aWR0aD0xNSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0Kc25zLmRpc3RwbG90KFhbWSA9PSAwLDJdLGJpbnM9MTApCnNucy5kaXN0cGxvdChYW1kgPT0gMSwyXSxiaW5zPTEwKQpzbnMuZGlzdHBsb3QoWFtZID09IDIsMl0sYmlucz0xMCkKcGx0LnNob3coKQpgYGAKCktERSAoS2VybmVsIGRlbnNpdHkgZXN0aW1hdGlvbikgZGUgdW5hIHNvbGEgdmFyaWFibGUKCmBgYHtweXRob24gZmlnLmhlaWdodD0xNSwgZmlnLndpZHRoPTE1LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpzbnMuZGlzdHBsb3QoWFs6LDFdLGJpbnM9MTApCnBsdC5zaG93KCkKYGBgCgpSZWFsaXphbW9zIGdyYWZpY28gZGUgZGlzcGVyc2lvbgoKYGBge3B5dGhvbn0KI1NjYXR0ZXJwbG90IGRvbmRlIHNhbGVuIHRhbWJpw6luIGhpc3RvZ3JhbWFzCgoKc25zLmpvaW50cGxvdCgKICAgIHg9J3NlcGFsIGxlbmd0aCAoY20pJywKICAgIHk9J3NlcGFsIHdpZHRoIChjbSknLAogICAgZGF0YT1pcmlzLAogICAga2luZD0nc2NhdHRlcicsICAgIyAob3BjaW9uYWw6ICdzY2F0dGVyJywgJ2tkZScsICdoZXgnLCBldGMuKQogICAgY29sb3I9J3N0ZWVsYmx1ZScKKQpwbHQuc2hvdygpCmBgYAoKQ3JlYW1vcyBncmFmaWNvcyBkZSBwYXJlcwoKYGBge3B5dGhvbiBmaWcuaGVpZ2h0PTE1LCBmaWcud2lkdGg9MTUsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CiMgU2NhdHRlcnBsb3QgbWF0cml4IChrZGUgZW4gbGEgZGlhZ29uYWwpCnNucy5wYWlycGxvdChpcmlzX251bSxodWU9J1NwZWNpZXMnKQpwbHQuc2hvdygpCmBgYAoKVmVub3Mgb3RybyB0aXBvIGRlIGdyYWZpY28gZGUgbGEgZGVzbmlkYWQgZGUga2VybmVsCgpgYGB7cHl0aG9uIGZpZy5oZWlnaHQ9MTUsIGZpZy53aWR0aD0xNSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KIyBPdHJhIGZvcm1hIGRlIGNhcmdhciBsb3MgZGF0b3MgSVJJUyAoY29uIHNlYWJvcm4gLSBzbnMpCmlyaXMgPSBzbnMubG9hZF9kYXRhc2V0KCJpcmlzIikKcHJpbnQoaXJpcy5oZWFkKCkpICMgbm8gdGllbmUgdmFyaWFibGUgSWQsIGxhcyBkZW1hcyB2YmxlcyBzZSBsbGFtYW4gZGlmZXJlbnRlcyB5IGxhcyBjYXRlZ29yaWFzIHRhbWJpZW4KYGBgCgpEZW5zaWRhZCBkZSBrZXJuZWwgYmltaWRlbnNpb25hbAoKYGBge3B5dGhvbiBmaWcuaGVpZ2h0PTE1LCBmaWcud2lkdGg9MTUsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CiMgRGVuc2lkYWQga2VybmVsIGJpZGltZW5zaW9uYWwKCgojIFN1YmNvbmp1bnRvcwpzZXRvc2EgPSBpcmlzW2lyaXMuc3BlY2llcyA9PSAic2V0b3NhIl0KdmlyZ2luaWNhID0gaXJpc1tpcmlzLnNwZWNpZXMgPT0gInZpcmdpbmljYSJdCgojIEdyw6FmaWNvCnNucy5rZGVwbG90KHg9c2V0b3NhLnNlcGFsX2xlbmd0aCwgeT1zZXRvc2Euc2VwYWxfd2lkdGgsIGNtYXA9IlJlZHMiLCBmaWxsPVRydWUsIGFscGhhPTAuNSkKc25zLmtkZXBsb3QoeD12aXJnaW5pY2Euc2VwYWxfbGVuZ3RoLCB5PXZpcmdpbmljYS5zZXBhbF93aWR0aCwgY21hcD0iQmx1ZXMiLCBmaWxsPVRydWUsIGFscGhhPTAuNSkKCnBsdC54bGFiZWwoIlNlcGFsIExlbmd0aCIpCnBsdC55bGFiZWwoIlNlcGFsIFdpZHRoIikKcGx0LnRpdGxlKCJEaXN0cmlidWNpw7NuIGRlIFNldG9zYSB2cyBWaXJnaW5pY2EiKQpwbHQuc2hvdygpCgpgYGAKCkdyYWZpY2Ftb3MgbWF0cml6IGRlIGNvdmFyaWFuemEgbnVtZXJpY2EKCmBgYHtweXRob24gZmlnLmhlaWdodD0xNSwgZmlnLndpZHRoPTE1LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgQ2FyZ2FyIGVsIGRhdGFzZXQgaXJpcyB5IGNvbnZlcnRpciBhIERhdGFGcmFtZQppcmlzX2J1bmNoID0gbG9hZF9pcmlzKCkKaXJpcyA9IHBkLkRhdGFGcmFtZShpcmlzX2J1bmNoLmRhdGEsIGNvbHVtbnM9aXJpc19idW5jaC5mZWF0dXJlX25hbWVzKQoKIyBNYXRyaXogZGUgY29ycmVsYWNpw7NuCmNvcnJlbGF0aW9uX21hdHJpeCA9IGlyaXMuY29ycigpCgojIEdyYWZpY2FyIGhlYXRtYXAKcGx0LmZpZ3VyZShmaWdzaXplPSgxMCwgMTApKQpheCA9IHNucy5oZWF0bWFwKAogICAgY29ycmVsYXRpb25fbWF0cml4LAogICAgdm1pbj0tMSwgdm1heD0xLCBjZW50ZXI9MCwKICAgIGNtYXA9IkduQnUiLCBhbm5vdD1UcnVlLCBmbXQ9Ii4yZiIsCiAgICBzcXVhcmU9VHJ1ZSwgY2Jhcl9rd3M9eyJzaHJpbmsiOiAwLjh9LAogICAgbGluZXdpZHRocz0wLjUsIGxpbmVjb2xvcj0nd2hpdGUnCikKCiMgQ29ycmVnaXIgZGVzYWxpbmVhY2nDs24gdmlzdWFsIChlbCBidWcgY29tw7puIGVuIHNlYWJvcm4vbWF0cGxvdGxpYikKYm90dG9tLCB0b3AgPSBheC5nZXRfeWxpbSgpCmF4LnNldF95bGltKHRvcCArIDAuNSwgYm90dG9tIC0gMC41KQoKcGx0LnNob3coKQoKCmBgYAoKR3JhZmljbyBkZSBjb29yZGVuYWRhcyBwYXJhbGVsYXMKCmBgYHtweXRob24gZmlnLmhlaWdodD0xNSwgZmlnLndpZHRoPTE1LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgQ2FyZ2FyIGVsIGRhdGFzZXQgaXJpcwppcmlzX2J1bmNoID0gbG9hZF9pcmlzKCkKaXJpcyA9IHBkLkRhdGFGcmFtZShpcmlzX2J1bmNoLmRhdGEsIGNvbHVtbnM9aXJpc19idW5jaC5mZWF0dXJlX25hbWVzKQppcmlzWyJzcGVjaWVzIl0gPSBwZC5DYXRlZ29yaWNhbC5mcm9tX2NvZGVzKGlyaXNfYnVuY2gudGFyZ2V0LCBpcmlzX2J1bmNoLnRhcmdldF9uYW1lcykKCiMgRXNjYWxhciB2YXJpYWJsZXMgbnVtw6lyaWNhcwpzY2FsZXIgPSBNaW5NYXhTY2FsZXIoKQppcmlzX3NjYWxlZCA9IHBkLkRhdGFGcmFtZShzY2FsZXIuZml0X3RyYW5zZm9ybShpcmlzLmlsb2NbOiwgOi0xXSksIGNvbHVtbnM9aXJpcy5jb2x1bW5zWzotMV0pCmlyaXNfc2NhbGVkWyJzcGVjaWVzIl0gPSBpcmlzWyJzcGVjaWVzIl0KCiMgR3JhZmljYXIgY29vcmRlbmFkYXMgcGFyYWxlbGFzCnBsdC5maWd1cmUoZmlnc2l6ZT0oMTAsIDYpKQpwYXJhbGxlbF9jb29yZGluYXRlcyhpcmlzX3NjYWxlZCwgInNwZWNpZXMiLCBjb2xvcm1hcD0nY29vbCcpCnBsdC5zaG93KCkKCmBgYAoKR3JhZmljbyBkZSBBbmRyZXdzCgpgYGB7cHl0aG9uIGZpZy5oZWlnaHQ9MTUsIGZpZy53aWR0aD0xNSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KI0N1cnZhcyBkZSBBbmRyZXdzCgphbmRyZXdzX2N1cnZlcyhpcmlzLCJzcGVjaWVzIixjb2xvcm1hcD0ncmFpbmJvdycpCnBsdC5zaG93KCkKYGBgCgpGaW5hbG1lbnRlLCBpbHVzdHJhbW9zIHVuIGdyYWZpY28gcmFkaWFsCgpgYGB7cHl0aG9uIGZpZy5oZWlnaHQ9MTUsIGZpZy53aWR0aD0xNSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KI0dyYWZpY28gUmFkaWFsCm5ld19pcmlzPWlyaXMuZHJvcCgnc3BlY2llcycsYXhpcz0xKQpwcmludChuZXdfaXJpcy5oZWFkKCkpICMgbGUgcXVpdGFtb3MgbGEgdmFyaWFibGUgc3BlY2llcwpjYXRlZ29yaWVzID0gbGlzdChpcmlzKVs6NF0KTiA9IGxlbihjYXRlZ29yaWVzKQphbmdsZXMgPSBbIG4gLyBmbG9hdChOKSoyKnBpIGZvciBuIGluIHJhbmdlKE4pXQphbmdsZXMgPSBhbmdsZXMgKyBhbmdsZXNbOjFdCnBsdC5maWd1cmUoZmlnc2l6ZSA9ICgxMCwxMCkpCmF4ID0gcGx0LnN1YnBsb3QoMTExLHBvbGFyID0gVHJ1ZSkKYXguc2V0X3RoZXRhX29mZnNldChwaS8yKQpheC5zZXRfdGhldGFfZGlyZWN0aW9uKC0xKQpwbHQueHRpY2tzKGFuZ2xlc1s6LTFdLGNhdGVnb3JpZXMpCmF4LnNldF9ybGFiZWxfcG9zaXRpb24oMCkKcGx0Lnl0aWNrcyhbMCwyLDQsNl0sWyIwIiwiMiIsIjQiLCI2Il0sY29sb3I9ICJyZWQiLCBzaXplID0gNykKcGx0LnlsaW0oMCw2KQoKdmFsdWVzID0gbmV3X2lyaXMubG9jWzBdLnZhbHVlcy5mbGF0dGVuKCkudG9saXN0KCkKdmFsdWVzID0gdmFsdWVzICsgdmFsdWVzWzoxXQpheC5wbG90KGFuZ2xlcyx2YWx1ZXMsbGluZXdpZHRoID0gMSxsaW5lc3R5bGU9InNvbGlkIixsYWJlbD0nc2V0b3NhJyApCmF4LmZpbGwoYW5nbGVzLHZhbHVlcywiYiIsYWxwaGE9MC4xKQoKCnZhbHVlcyA9IG5ld19pcmlzLmxvY1s3OV0udmFsdWVzLmZsYXR0ZW4oKS50b2xpc3QoKQp2YWx1ZXMgPSB2YWx1ZXMgKyB2YWx1ZXNbOjFdCmF4LnBsb3QoYW5nbGVzLHZhbHVlcyxsaW5ld2lkdGggPSAxLGxpbmVzdHlsZT0ic29saWQiLGxhYmVsPSd2ZXJzaWNvbG9yJyApCmF4LmZpbGwoYW5nbGVzLHZhbHVlcywib3JhbmdlIixhbHBoYT0wLjEpCgoKdmFsdWVzID0gbmV3X2lyaXMubG9jWzE0OV0udmFsdWVzLmZsYXR0ZW4oKS50b2xpc3QoKQp2YWx1ZXMgPSB2YWx1ZXMgKyB2YWx1ZXNbOjFdCmF4LnBsb3QoYW5nbGVzLHZhbHVlcyxsaW5ld2lkdGggPSAxLGxpbmVzdHlsZT0ic29saWQiLGxhYmVsPSd2aXJnaW5pY2EnICkKYXguZmlsbChhbmdsZXMsdmFsdWVzLCJncmVlbiIsYWxwaGE9MC4xKQoKcGx0LmxlZ2VuZChsb2MgPSAidXBwZXIgbGVmdCIsYmJveF90b19hbmNob3IgPSAoMC4xLDAuMSkpCnBsdC5zaG93KCkKYGBgCgojIDIpIFZhcmlhYmxlcyBtdWx0aXZhcmlhbnRlcwoKKipEZWZpbmljaW9uKioKCkxhc8KgKip2YXJpYWJsZXMgbXVsdGl2YXJpYW50ZXMqKsKgc2UgcmVmaWVyZW4gYSBzaXR1YWNpb25lcyBkb25kZSBzZSBvYnNlcnZhbsKgKiptw7psdGlwbGVzIHZhcmlhYmxlcyBhbGVhdG9yaWFzIGFsIG1pc21vIHRpZW1wbyoqwqBwYXJhIGNhZGEgaW5kaXZpZHVvLCB1bmlkYWQgbyBjYXNvLiBFbiBsdWdhciBkZSBhbmFsaXphciB1bmEgc29sYSB2YXJpYWJsZSAoY29tbyBsYSBlc3RhdHVyYSksIGVsIGFuw6FsaXNpcyBtdWx0aXZhcmlhbnRlIGNvbnNpZGVyYSB2YXJpYXMgc2ltdWx0w6FuZWFtZW50ZSAoY29tbyBlc3RhdHVyYSwgcGVzbyB5IGVkYWQpLiBFc3RlIGVuZm9xdWUgcGVybWl0ZSBlc3R1ZGlhciByZWxhY2lvbmVzLCBwYXRyb25lcyB5IGRlcGVuZGVuY2lhcyBlbnRyZSBsYXMgdmFyaWFibGVzLCBsbyBjdWFsIGVzIGZ1bmRhbWVudGFsIGVuIGNvbnRleHRvcyBjb21wbGVqb3MgZG9uZGUgbG9zIGZlbsOzbWVub3Mgbm8gcHVlZGVuIGV4cGxpY2Fyc2UgcG9yIHVuYSBzb2xhIGRpbWVuc2nDs24uCgpQb3IgZWplbXBsbywgZW4gdW4gZXN0dWRpbyBkZSBzYWx1ZCwgcG9kcsOtYXMgb2JzZXJ2YXIgdmFyaWFibGVzIGNvbW/CoCoqcHJlc2nDs24gYXJ0ZXJpYWwqKizCoCoqbml2ZWwgZGUgY29sZXN0ZXJvbCoqLMKgKipmcmVjdWVuY2lhIGNhcmTDrWFjYSoqwqB5wqAqKmVkYWQqKsKgZGUgY2FkYSBwYWNpZW50ZS4gRW4gZWNvbm9tw61hLCBwb2Ryw61hcyBhbmFsaXphciBzaW11bHTDoW5lYW1lbnRlIGVswqAqKlBJQiBwZXIgY8OhcGl0YSoqLCBsYcKgKip0YXNhIGRlIG5hdGFsaWRhZCoqLCBsYcKgKiplc3BlcmFuemEgZGUgdmlkYSoqwqB5IGxhwqAqKnRhc2EgZGUgbW9ydGFsaWRhZCBpbmZhbnRpbCoqwqBkZSBjYWRhIHBhw61zLiBFc3RhcyB2YXJpYWJsZXMsIGNvbnNpZGVyYWRhcyBlbiBjb25qdW50bywgZm9ybWFuIHVuYcKgKip2YXJpYWJsZSBtdWx0aXZhcmlhbnRlKirCoHkgcGVybWl0ZW4gcmVhbGl6YXIgYW7DoWxpc2lzIGNvbW8gcmVncmVzacOzbiBtw7psdGlwbGUsIGFuw6FsaXNpcyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcyAoUENBKSwgY2x1c3RlcmluZyBvIGFuw6FsaXNpcyBkaXNjcmltaW5hbnRlLgoKKirCv1F1ZSBlcyB1bmEgZnVuY2nDs24gZGUgZGlzdHJpYnVjacOzbiBjb25qdW50YT8qKgoKTGHCoGZ1bmNpw7NuIGRlIGRpc3RyaWJ1Y2nDs24gY29uanVudGHCoGVzIHVuYSBoZXJyYW1pZW50YSBlc3RhZMOtc3RpY2EgcXVlIGRlc2NyaWJlIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgZG9zIG8gbcOhcyB2YXJpYWJsZXMgYWxlYXRvcmlhcyB0b21lbiBzaW11bHTDoW5lYW1lbnRlIGNpZXJ0b3MgdmFsb3JlcyBtZW5vcmVzIG8gaWd1YWxlcyBhIGxvcyBkYWRvcy4gU2lydmUgcGFyYSBhbmFsaXphciBlbCBjb21wb3J0YW1pZW50byBjb25qdW50byBkZSB2YXJpYWJsZXMsIHBlcm1pdGllbmRvIGNhbGN1bGFyIHByb2JhYmlsaWRhZGVzIGNvbmp1bnRhcywgbWFyZ2luYWxlcyB5IGNvbmRpY2lvbmFsZXMsIGFzw60gY29tbyBlbnRlbmRlciBsYSBkZXBlbmRlbmNpYSBlbnRyZSBlbGxhcy4gUG9yIGVqZW1wbG8sIHNlIHB1ZWRlIHVzYXIgcGFyYSBjb25vY2VyIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgdW5hIHBlcnNvbmEgdGVuZ2EgdW4gaW5ncmVzbyBtZW5zdWFsIG1lbm9yIGEgXCQyLjAwMC4wMDAgeSBhbCBtaXNtbyB0aWVtcG8gdW4gZ2FzdG8gbWVub3IgYSBcJDEuNTAwLjAwMCwgbyBwYXJhIGV2YWx1YXIgY8OzbW8gc2UgcmVsYWNpb25hbiBsYSB0ZW1wZXJhdHVyYSB5IGxhIGh1bWVkYWQgZW4gdW4gbWlzbW8gZMOtYS4KCiMjIDIuMSkgKipEaXN0cmlidWNpw7NuIGNvbmRpY2lvbmFsIGUgaW5kZXBlbmRlbmNpYSoqCgpMYcKgZGlzdHJpYnVjacOzbiBjb25kaWNpb25hbMKgZGVzY3JpYmUgZWwgY29tcG9ydGFtaWVudG8gZGUgdW5hIHZhcmlhYmxlIGFsZWF0b3JpYSBkYWRvIHF1ZSBvdHJhIGhhIHRvbWFkbyB1biB2YWxvciBlc3BlY8OtZmljby4gUGVybWl0ZSByZXNwb25kZXIgcHJlZ3VudGFzIGNvbW86IOKAnMK/Y8OzbW8gc2UgZGlzdHJpYnV5ZcKgWVnCoHNpIHNhYmVtb3MgcXVlwqBYPXhYPXg/4oCdLiBQb3IgZWplbXBsbywgc2kgc2FiZW1vcyBxdWUgdW5hIHBlcnNvbmEgdGllbmUgdW4gaW5ncmVzbyBkZSBcJDIuMDAwLjAwMCwgbGEgZGlzdHJpYnVjacOzbiBjb25kaWNpb25hbCBkZWwgZ2FzdG8gbXVlc3RyYSBjw7NtbyB2YXLDrWFuIGxvcyBuaXZlbGVzIGRlIGdhc3RvwqBkZW50cm8gZGUgZXNlIGdydXBvIGVzcGVjw61maWNvLiBTZSB1c2EgcGFyYSBhbmFsaXphciByZWxhY2lvbmVzIGVudHJlIHZhcmlhYmxlcyB5IGNhbGN1bGFyIHByb2JhYmlsaWRhZGVzIG3DoXMgZXNwZWPDrWZpY2FzIGRlbnRybyBkZSBzdWJjb25qdW50b3MgZGVsIGVzcGFjaW8gbXVlc3RyYWwuCgpEb3MgdmFyaWFibGVzIGFsZWF0b3JpYXMgc29uwqBpbmRlcGVuZGllbnRlc8Kgc2kgZWwgdmFsb3IgZGUgdW5hwqBubyBhZmVjdGHCoGxhIGRpc3RyaWJ1Y2nDs24gZGUgbGEgb3RyYS4gRm9ybWFsbWVudGUswqBYWMKgecKgWVnCoHNvbiBpbmRlcGVuZGllbnRlcyBzaSBsYSBkaXN0cmlidWNpw7NuIGNvbmRpY2lvbmFsIGRlwqBZWcKgZGFkb8KgWD14WD14wqBlcyBpZ3VhbCBhIGxhIGRpc3RyaWJ1Y2nDs24gbWFyZ2luYWwgZGXCoFlZLCBwYXJhIHRvZG/CoHh4LiBFbiBsYSBwcsOhY3RpY2EsIGVzdG8gc2lnbmlmaWNhIHF1ZSBjb25vY2VyIGVsIHZhbG9yIGRlwqBYWMKgbm8gZGEgaW5mb3JtYWNpw7NuIGFkaWNpb25hbMKgc29icmXCoFlZLiBQb3IgZWplbXBsbywgc2kgZWwgZ8OpbmVybyBkZSB1bmEgcGVyc29uYSBubyBhZmVjdGEgc3UgY29uc3VtbyBkZSBjaWVydG8gcHJvZHVjdG8sIGVudG9uY2VzIGVzYXMgZG9zIHZhcmlhYmxlcyBzb24gaW5kZXBlbmRpZW50ZXMuCgojIyAyLjIpIFRlb3JlbWEgZGUgQmF5ZXMKCkVswqAqKlRlb3JlbWEgZGUgQmF5ZXMqKsKgZXMgdW5hIHJlZ2xhIGZ1bmRhbWVudGFsIGRlIGxhIHByb2JhYmlsaWRhZCBxdWUgcGVybWl0ZSBhY3R1YWxpemFyIG51ZXN0cmFzIGNyZWVuY2lhcyBzb2JyZSB1biBldmVudG8gYSBwYXJ0aXIgZGUgbnVldmEgaW5mb3JtYWNpw7NuLiBFc3RhYmxlY2UgY8OzbW8gY2FsY3VsYXIgbGEgcHJvYmFiaWxpZGFkIGRlIHVuIGV2ZW50b8KgQUHCoGRhZG8gcXVlIG9jdXJyacOzIG90cm8gZXZlbnRvwqBCQiwgdXNhbmRvIGxhIHJlbGFjacOzbjoKCiQkClAoQSBcbWlkIEIpID0gXGZyYWN7UChCIFxtaWQgQSkgXGNkb3QgUChBKX17UChCKX0KJCQKCkFxdcOtLMKgUChBKVAoQSnCoGVzIGxhIHByb2JhYmlsaWRhZCBpbmljaWFsIChhIHByaW9yaSkgZGXCoEFBLMKgUChC4oijQSlQKELiiKNBKcKgZXMgbGEgcHJvYmFiaWxpZGFkIGRlIG9ic2VydmFywqBCQsKgc2nCoEFBwqBlcyBjaWVydG8gKHZlcm9zaW1pbGl0dWQpLCB5wqBQKEHiiKNCKVAoQeKIo0IpwqBlcyBsYSBwcm9iYWJpbGlkYWQgYWN0dWFsaXphZGEgKGEgcG9zdGVyaW9yaSkgZGXCoEFBwqBkZXNwdcOpcyBkZSBvYnNlcnZhcsKgQkIuCgpFc3RlIHRlb3JlbWEgZXMgY2xhdmUgZW4gc2l0dWFjaW9uZXMgZG9uZGUgdGVuZW1vcyBpbmZvcm1hY2nDs24gcGFyY2lhbCBvIGluY2llcnRhIHkgcXVlcmVtb3MgbWVqb3JhciBudWVzdHJhcyBwcmVkaWNjaW9uZXMuIFBvciBlamVtcGxvLCBzaSB1biB0ZXN0IG3DqWRpY28gZGEgcG9zaXRpdm8gKEJCKSwgZWwgVGVvcmVtYSBkZSBCYXllcyBwZXJtaXRlIGNhbGN1bGFyIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgdW5hIHBlcnNvbmEgcmVhbG1lbnRlIHRlbmdhIGxhIGVuZmVybWVkYWQgKEFBKSBjb25zaWRlcmFuZG8gdGFudG8gbGEgcHJlY2lzacOzbiBkZWwgdGVzdCBjb21vIGxhIHByZXZhbGVuY2lhIGRlIGxhIGVuZmVybWVkYWQuIFNlIGFwbGljYSBhbXBsaWFtZW50ZSBlbiBlc3RhZMOtc3RpY2EgYmF5ZXNpYW5hLCBkaWFnbsOzc3RpY28gbcOpZGljbywgaW50ZWxpZ2VuY2lhIGFydGlmaWNpYWwgeSB0b21hIGRlIGRlY2lzaW9uZXMgYmFqbyBpbmNlcnRpZHVtYnJlLgoKIyMgMi4zKSoqRWwgdmFsb3IgZXNwZXJhZG8sIGxhIGNvdmFyaWFuYXphIHkgbGEgY29ycmVsYWNpb24qKgoKRWzCoCoqdmFsb3IgZXNwZXJhZG8qKsKgKG8gZXNwZXJhbnphIG1hdGVtw6F0aWNhKSBlcyBlbCBwcm9tZWRpbyBwb25kZXJhZG8gZGUgdG9kb3MgbG9zIHBvc2libGVzIHZhbG9yZXMgZGUgdW5hIHZhcmlhYmxlIGFsZWF0b3JpYSwgZG9uZGUgY2FkYSB2YWxvciBzZSBtdWx0aXBsaWNhIHBvciBzdSBwcm9iYWJpbGlkYWQuIFJlcHJlc2VudGEgZWwgcmVzdWx0YWRvIHByb21lZGlvIHF1ZSBzZSBlc3BlcmFyw61hIHNpIHVuIGV4cGVyaW1lbnRvIHNlIHJlcGl0aWVyYSBpbmZpbml0YXMgdmVjZXMuIFBvciBlamVtcGxvLCBlbiBlY29ub23DrWEsIGVsIHZhbG9yIGVzcGVyYWRvIHNlIHVzYSBwYXJhIGVzdGltYXIgaW5ncmVzb3MgZnV0dXJvcyBvIGV2YWx1YXIgZGVjaXNpb25lcyBiYWpvIGluY2VydGlkdW1icmUsIGNvbW8gZW4gbW9kZWxvcyBkZSByaWVzZ28gbyB0ZW9yw61hIGRlIGp1ZWdvcy4KCkxhwqAqKmNvdmFyaWFuemEqKsKgbWlkZSBjw7NtbyB2YXLDrWFuIGRvcyB2YXJpYWJsZXMgYWxlYXRvcmlhcyBqdW50YXMuIFNpIGVzIHBvc2l0aXZhLCBzaWduaWZpY2EgcXVlIHRpZW5kZW4gYSBhdW1lbnRhciBvIGRpc21pbnVpciBzaW11bHTDoW5lYW1lbnRlOyBzaSBlcyBuZWdhdGl2YSwgY3VhbmRvIHVuYSBzdWJlLCBsYSBvdHJhIHRpZW5kZSBhIGJhamFyLiBBdW5xdWUgaW5kaWNhIGxhIGRpcmVjY2nDs24gZGUgbGEgcmVsYWNpw7NuLCBzdSBtYWduaXR1ZCBkZXBlbmRlIGRlIGxhcyB1bmlkYWRlcyBkZSBtZWRpZGEsIHBvciBsbyBxdWUgbm8gc2llbXByZSBlcyBmw6FjaWwgZGUgaW50ZXJwcmV0YXIgZGlyZWN0YW1lbnRlLiBTZSB1dGlsaXphLCBwb3IgZWplbXBsbywgZW4gcG9ydGFmb2xpb3MgZmluYW5jaWVyb3MgcGFyYSBtZWRpciBjw7NtbyBzZSBtdWV2ZW4ganVudGFzIGRvcyBhY2Npb25lcy4KCkxhwqAqKmNvcnJlbGFjacOzbioqLCBlbiBjYW1iaW8sIGVzIHVuYSB2ZXJzacOzbiBlc3RhbmRhcml6YWRhIGRlIGxhIGNvdmFyaWFuemEgcXVlIHNpZW1wcmUgdG9tYSB2YWxvcmVzIGVudHJlIC0xIHkgMSwgeSBwZXJtaXRlIGNvbXBhcmFyIHJlbGFjaW9uZXMgZW50cmUgdmFyaWFibGVzIHNpbiBpbXBvcnRhciBzdXMgdW5pZGFkZXMuIFVuYSBjb3JyZWxhY2nDs24gY2VyY2FuYSBhIDEgaW5kaWNhIHVuYSByZWxhY2nDs24gbGluZWFsIHBvc2l0aXZhIGZ1ZXJ0ZTsgY2VyY2FuYSBhIC0xLCB1bmEgcmVsYWNpw7NuIG5lZ2F0aXZhIGZ1ZXJ0ZTsgeSBjZXJjYW5hIGEgMCwgcG9jYSBvIG5pbmd1bmEgcmVsYWNpw7NuIGxpbmVhbC4gRXMgZnVuZGFtZW50YWwgZW4gYW7DoWxpc2lzIGVzdGFkw61zdGljbywgbWFjaGluZSBsZWFybmluZywgY2llbmNpYSBkZSBkYXRvcyB5IGVjb25vbWV0csOtYSBwYXJhIGlkZW50aWZpY2FyIHBhdHJvbmVzLCBzZWxlY2Npb25hciB2YXJpYWJsZXMgbyBldmFsdWFyIGFzb2NpYWNpb25lcyBlbnRyZSBmZW7Ds21lbm9zLgoKIyMgMi40KSAqKkVzcGVyYW56YSBjb25kaWNpb25hbCB5IGxhIGxleSBkZSB2YXJpYW56YSB0b3RhbCoqCgpMYcKgKiplc3BlcmFuemEgY29uZGljaW9uYWwqKsKgZXMgZWwgdmFsb3IgZXNwZXJhZG8gZGUgdW5hIHZhcmlhYmxlIGFsZWF0b3JpYSBkYWRvIHF1ZSBjb25vY2Vtb3MgZWwgdmFsb3IgKG8gaW5mb3JtYWNpw7NuKSBkZSBvdHJhLiBFbiBsdWdhciBkZSBjYWxjdWxhciBlbCBwcm9tZWRpbyBnZW5lcmFsIGRlIHVuYSB2YXJpYWJsZSwgbGEgZXNwZXJhbnphIGNvbmRpY2lvbmFsIHJlc3BvbmRlIGEgbGEgcHJlZ3VudGE6IOKAnMK/Q3XDoWwgZXMgZWwgdmFsb3IgZXNwZXJhZG8gZGXCoFlZwqBzaSBzw6kgcXVlwqBYPXhYPXg/4oCdLiBTZSBkZW5vdGEgY29tb8KgRShZ4oijWD14KUUoWeKIo1g9eCnCoHkgcHVlZGUgaW50ZXJwcmV0YXJzZSBjb21vIHVuYSBmdW5jacOzbiBkZcKgeHguIEVzdGEgaGVycmFtaWVudGEgZXMgbXV5IMO6dGlsIGN1YW5kbyBzZSBkaXNwb25lIGRlIGluZm9ybWFjacOzbiBwYXJjaWFsIHkgc2UgcXVpZXJlIGhhY2VyIHVuYSBwcmVkaWNjacOzbiBtw6FzIGFqdXN0YWRhIGNvbiBiYXNlIGVuIGVzYSBpbmZvcm1hY2nDs24uCgpQb3IgZWplbXBsbywgc2nCoFlZwqBlcyBlbCBpbmdyZXNvIG1lbnN1YWwgecKgWFjCoGVzIGVsIG5pdmVsIGVkdWNhdGl2byzCoEUoWeKIo1g9dW5pdmVyc2l0YXJpbylFKFniiKNYPXVuaXZlcnNpdGFyaW8pcmVwcmVzZW50YSBlbCBpbmdyZXNvIHByb21lZGlvIGVudHJlIHF1aWVuZXMgdGllbmVuIGZvcm1hY2nDs24gdW5pdmVyc2l0YXJpYS4gRW4gZXN0ZSBzZW50aWRvLCBsYSBlc3BlcmFuemEgY29uZGljaW9uYWwgcGVybWl0ZSBjb25zdHJ1aXIgbW9kZWxvcyBwcmVkaWN0aXZvcyBvIGRlIHJlZ3Jlc2nDs24sIHlhIHF1ZSBzZSBlbmZvY2EgZW4gY8OzbW8gY2FtYmlhIGVsIHZhbG9yIGVzcGVyYWRvIGRlIHVuYSB2YXJpYWJsZSBlbiBmdW5jacOzbiBkZSBvdHJhLgoKTGHCoCoqbGV5IGRlIGxhIHZhcmlhbnphIHRvdGFsKirCoCh0YW1iacOpbiBsbGFtYWRhIGRlc2NvbXBvc2ljacOzbiBkZSBsYSB2YXJpYW56YSkgcGVybWl0ZSBkaXZpZGlyIGxhIHZhcmlhbnphIGRlIHVuYSB2YXJpYWJsZSBhbGVhdG9yaWHCoFlZwqBlbiBkb3MgY29tcG9uZW50ZXM6IHVuYSBwYXJ0ZSBleHBsaWNhZGEgcG9yIGxhIHZhcmlhY2nDs24gZW4gbGEgbWVkaWEgY29uZGljaW9uYWwsIHkgb3RyYSBwb3IgbGEgdmFyaWFiaWxpZGFkIHJlc2lkdWFsIGRlbnRybyBkZSBjYWRhIGdydXBvIGNvbmRpY2lvbmFsLiBTdSBmw7NybXVsYSBlczoKCiQkClxtYXRocm17VmFyfShZKSA9IFxtYXRoYmJ7RX1bXG1hdGhybXtWYXJ9KFkgXG1pZCBYKV0gKyBcbWF0aHJte1Zhcn0oXG1hdGhiYntFfVtZIFxtaWQgWF0pCiQkCgpFc3RhIGxleSBtdWVzdHJhIHF1ZSBsYSB2YXJpYWJpbGlkYWQgdG90YWwgZGXCoFlZwqBzZSBwdWVkZSBlbnRlbmRlciBjb21vIGxhIHN1bWEgZGUgbGEgdmFyaWFiaWxpZGFkwqAqKmRlbnRybyBkZSBsb3Mgc3ViZ3J1cG9zIGRlZmluaWRvcyBwb3IqKiBYWMKgKHByaW1lciB0w6lybWlubykgeSBsYSB2YXJpYWJpbGlkYWTCoCoqZW50cmUgbGFzIG1lZGlhcyBkZSBlc29zIHN1YmdydXBvcyoqwqAoc2VndW5kbyB0w6lybWlubykuIEVzIGNsYXZlIGVuIGVzdGFkw61zdGljYSB5IG1hY2hpbmUgbGVhcm5pbmcgcGFyYSBlbnRlbmRlciBxdcOpIHRhbnRhIHZhcmlhY2nDs24gc2UgZXhwbGljYSBwb3IgdW4gcHJlZGljdG9yIHkgY3XDoW50YSBxdWVkYSBzaW4gZXhwbGljYXIsIGxvIHF1ZSBlcyBlc2VuY2lhbCBlbiBhbsOhbGlzaXMgZGUgcmVncmVzacOzbiwgaW5mZXJlbmNpYSB5IG1vZGVsb3MgamVyw6FycXVpY29zLgoKIyMgMi41KSBDb2RpZ28gaWx1c3RyYXRpdm8gZW4gUjoKCkVsIGPDs2RpZ28gaWx1c3RyYSBsb3MgY29uY2VwdG9zIGFudGVyaW9ybWVudGUgdmlzdG86CgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQojIENhcmdhciB5IGxpbXBpYXIgZGF0YXNldApkYXRhKCJtdGNhcnMiKQpkZiA8LSBtdGNhcnMgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKCJtb2RlbG8iKSAlPiUKICBjbGVhbl9uYW1lcygpICU+JQogIG11dGF0ZShjeWwgPSBhcy5mYWN0b3IoY3lsKSkgICMgdHJhdGFtb3MgY3lsIGNvbW8gdmFyaWFibGUgY2F0ZWfDs3JpY2EKCiMgMS4gVmFyaWFibGVzIG11bHRpdmFyaWFudGVzIC0gZ3LDoWZpY28gZGUgcGFyZXMKZGYgJT4lCiAgZHBseXI6OnNlbGVjdChtcGcsIGhwLCB3dCwgcXNlYykgJT4lCiAgZ2dwYWlycyh0aXRsZSA9ICJHcsOhZmljbyBkZSBwYXJlczogVmFyaWFibGVzIG11bHRpdmFyaWFudGVzIikKCiMgMi4gRnVuY2nDs24gZGUgZGlzdHJpYnVjacOzbiBjb25qdW50YTogbXBnIHZzIGhwCmdncGxvdChkZiwgYWVzKHggPSBtcGcsIHkgPSBocCkpICsKICBnZW9tX2JpbjJkKGJpbnMgPSAxNSkgKwogIHNjYWxlX2ZpbGxfZ3JhZGllbnQobG93ID0gImxpZ2h0Ymx1ZSIsIGhpZ2ggPSAiZGFya2JsdWUiKSArCiAgbGFicyh0aXRsZSA9ICJEaXN0cmlidWNpw7NuIGNvbmp1bnRhOiBtcGcgdnMgaHAiLCB4ID0gIk1pbGxhcyBwb3IgZ2Fsw7NuIiwgeSA9ICJDYWJhbGxvcyBkZSBmdWVyemEiKQoKIyAzLiBEaXN0cmlidWNpw7NuIGNvbmRpY2lvbmFsOiBCb3hwbG90IGRlIG1wZyBzZWfDum4gY3lsCmdncGxvdChkZiwgYWVzKHggPSBjeWwsIHkgPSBtcGcsIGZpbGwgPSBjeWwpKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIGxhYnModGl0bGUgPSAiRGlzdHJpYnVjacOzbiBjb25kaWNpb25hbCBkZSBtcGcgZGFkbyBjeWwiKQoKIyA0LiBJbmRlcGVuZGVuY2lhOiBzY2F0dGVycGxvdCBkZSB3dCB2cyBxc2VjIHBvciBjeWwKZ2dwbG90KGRmLCBhZXMoeCA9IHd0LCB5ID0gcXNlYywgY29sb3IgPSBjeWwpKSArCiAgZ2VvbV9wb2ludChzaXplID0gMykgKwogIGxhYnModGl0bGUgPSAiwr9IYXkgaW5kZXBlbmRlbmNpYSBlbnRyZSBwZXNvICh3dCkgeSB0aWVtcG8gMS80IG1pbGxhIChxc2VjKT8iKQoKIyA1LiBUZW9yZW1hIGRlIEJheWVzIC0gc2ltdWxhY2nDs24gc2ltcGxlCnNldC5zZWVkKDEyMykKTiA8LSAxMDAwMAojIEV2ZW50byBBOiBlbmZlcm1lZGFkICg1JSkKIyBFdmVudG8gQjogdGVzdCBwb3NpdGl2byAoOTAlIHNlbnNpYmlsaWRhZCwgMTAlIGZhbHNvIHBvc2l0aXZvKQpwZXJzb25hcyA8LSB0aWJibGUoCiAgZW5mZXJtZWRhZCA9IHJiaW5vbShOLCAxLCAwLjA1KSwKICB0ZXN0X3Bvc2l0aXZvID0gaWZfZWxzZShlbmZlcm1lZGFkID09IDEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgcmJpbm9tKE4sIDEsIDAuOSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgcmJpbm9tKE4sIDEsIDAuMSkpCikKIyBQKGVuZmVybWVkYWQgfCB0ZXN0IHBvc2l0aXZvKQpiYXllc19wcm9iIDwtIHBlcnNvbmFzICU+JQogIGZpbHRlcih0ZXN0X3Bvc2l0aXZvID09IDEpICU+JQogIHN1bW1hcmlzZShwcm9iID0gbWVhbihlbmZlcm1lZGFkID09IDEpKQoKcHJpbnQoYmF5ZXNfcHJvYikKCiMgNi4gVmFsb3IgZXNwZXJhZG8sIGNvdmFyaWFuemEgeSBjb3JyZWxhY2nDs24KZGYgJT4lCiAgc3VtbWFyaXNlKAogICAgdmFsb3JfZXNwZXJhZG9fbXBnID0gbWVhbihtcGcpLAogICAgdmFyX2hwID0gdmFyKGhwKSwKICAgIGNvdl9tcGdfaHAgPSBjb3YobXBnLCBocCksCiAgICBjb3JfbXBnX2hwID0gY29yKG1wZywgaHApCiAgKQoKIyA3LiBFc3BlcmFuemEgY29uZGljaW9uYWwgeSBsZXkgZGUgdmFyaWFuemEgdG90YWwKZXNwZXJhbnphX2NvbmRpY2lvbmFsIDwtIGRmICU+JQogIGdyb3VwX2J5KGN5bCkgJT4lCiAgc3VtbWFyaXNlKG1lZGlhX21wZyA9IG1lYW4obXBnKSwgdmFyaWFuemFfbXBnID0gdmFyKG1wZyksIC5ncm91cHMgPSAiZHJvcCIpCgp2YXJfdG90YWwgPC0gdmFyKGRmJG1wZykKZXNwZXJhbnphX2NvbmQgPC0gbWVhbihlc3BlcmFuemFfY29uZGljaW9uYWwkdmFyaWFuemFfbXBnKQp2YXJfZXNwZXJhbnphcyA8LSB2YXIoZXNwZXJhbnphX2NvbmRpY2lvbmFsJG1lZGlhX21wZykKCmNhdCgiVmFyaWFuemEgdG90YWw6ICIsIHZhcl90b3RhbCwgIlxuIikKY2F0KCJFW1ZhcihZfFgpXTogIiwgZXNwZXJhbnphX2NvbmQsICJcbiIpCmNhdCgiVmFyKEVbWXxYXSk6ICIsIHZhcl9lc3BlcmFuemFzLCAiXG4iKQpjYXQoIlN1bWE6ICIsIGVzcGVyYW56YV9jb25kICsgdmFyX2VzcGVyYW56YXMsICJcbiIpCgpgYGAKCiMjIDIuNikgQ29kaWdvIGlsdXN0cmF0aXZvIGVuIFB5dGhvbjoKCmBgYHtweXRob24gZmlnLmhlaWdodD0xNSwgZmlnLndpZHRoPTE1LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgMS4gQ2FyZ2FyIHkgbGltcGlhciBkYXRhc2V0CmZyb20gcHlkYXRhc2V0IGltcG9ydCBkYXRhICAjIG10Y2FycyBlc3TDoSBkaXNwb25pYmxlIGVuIHB5ZGF0YXNldApkZiA9IGRhdGEoJ210Y2FycycpLmNvcHkoKQpkZlsnbW9kZWxvJ10gPSBkZi5pbmRleApkZiA9IGRmLnJlc2V0X2luZGV4KGRyb3A9VHJ1ZSkKZGZbJ2N5bCddID0gZGZbJ2N5bCddLmFzdHlwZShzdHIpICAjIHRyYXRhbW9zIGN5bCBjb21vIGNhdGVnw7NyaWNhIChjb21vIGZhY3RvcikKCiMgMi4gVmFyaWFibGVzIG11bHRpdmFyaWFudGVzIC0gZ3LDoWZpY28gZGUgcGFyZXMKc25zLnBhaXJwbG90KGRmW1snbXBnJywgJ2hwJywgJ3d0JywgJ3FzZWMnXV0pCnBsdC5zdXB0aXRsZSgiR3LDoWZpY28gZGUgcGFyZXM6IFZhcmlhYmxlcyBtdWx0aXZhcmlhbnRlcyIsIHk9MS4wMikKcGx0LnNob3coKQoKIyAzLiBGdW5jacOzbiBkZSBkaXN0cmlidWNpw7NuIGNvbmp1bnRhOiBtcGcgdnMgaHAKcGx0LmZpZ3VyZShmaWdzaXplPSg4LCA2KSkKaGIgPSBwbHQuaGlzdDJkKGRmWydtcGcnXSwgZGZbJ2hwJ10sIGJpbnM9MTUsIGNtYXA9J0JsdWVzJykKcGx0LmNvbG9yYmFyKGhiWzNdKQpwbHQueGxhYmVsKCdNaWxsYXMgcG9yIGdhbMOzbiAobXBnKScpCnBsdC55bGFiZWwoJ0NhYmFsbG9zIGRlIGZ1ZXJ6YSAoaHApJykKcGx0LnRpdGxlKCdEaXN0cmlidWNpw7NuIGNvbmp1bnRhOiBtcGcgdnMgaHAnKQpwbHQuc2hvdygpCgojIDQuIERpc3RyaWJ1Y2nDs24gY29uZGljaW9uYWw6IEJveHBsb3QgZGUgbXBnIHNlZ8O6biBjeWwKcGx0LmZpZ3VyZShmaWdzaXplPSg4LCA2KSkKc25zLmJveHBsb3QoeD0nY3lsJywgeT0nbXBnJywgZGF0YT1kZiwgcGFsZXR0ZT0iU2V0MiIpCnBsdC50aXRsZSgiRGlzdHJpYnVjacOzbiBjb25kaWNpb25hbCBkZSBtcGcgZGFkbyBjeWwiKQpwbHQueGxhYmVsKCJDaWxpbmRyb3MiKQpwbHQueWxhYmVsKCJNaWxsYXMgcG9yIGdhbMOzbiAobXBnKSIpCnBsdC5zaG93KCkKCiMgNS4gSW5kZXBlbmRlbmNpYTogc2NhdHRlcnBsb3QgZGUgd3QgdnMgcXNlYyBwb3IgY3lsCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCwgNikpCnNucy5zY2F0dGVycGxvdCh4PSd3dCcsIHk9J3FzZWMnLCBodWU9J2N5bCcsIGRhdGE9ZGYsIHM9MTAwKQpwbHQudGl0bGUoIsK/SGF5IGluZGVwZW5kZW5jaWEgZW50cmUgcGVzbyAod3QpIHkgdGllbXBvIDEvNCBtaWxsYSAocXNlYyk/IikKcGx0LnhsYWJlbCgiUGVzbyAod3QpIikKcGx0LnlsYWJlbCgiVGllbXBvIDEvNCBtaWxsYSAocXNlYykiKQpwbHQuc2hvdygpCgojIDYuIFRlb3JlbWEgZGUgQmF5ZXMgLSBzaW11bGFjacOzbiBzaW1wbGUKbnAucmFuZG9tLnNlZWQoMTIzKQpOID0gMTAwMDAKZW5mZXJtZWRhZCA9IG5wLnJhbmRvbS5iaW5vbWlhbCgxLCAwLjA1LCBOKQp0ZXN0X3Bvc2l0aXZvID0gbnAud2hlcmUoZW5mZXJtZWRhZCA9PSAxLAogICAgICAgICAgICAgICAgICAgICAgICAgbnAucmFuZG9tLmJpbm9taWFsKDEsIDAuOSwgTiksCiAgICAgICAgICAgICAgICAgICAgICAgICBucC5yYW5kb20uYmlub21pYWwoMSwgMC4xLCBOKSkKcGVyc29uYXMgPSBwZC5EYXRhRnJhbWUoeydlbmZlcm1lZGFkJzogZW5mZXJtZWRhZCwgJ3Rlc3RfcG9zaXRpdm8nOiB0ZXN0X3Bvc2l0aXZvfSkKCiMgUChlbmZlcm1lZGFkIHwgdGVzdCBwb3NpdGl2bykKYmF5ZXNfcHJvYiA9IHBlcnNvbmFzW3BlcnNvbmFzWyd0ZXN0X3Bvc2l0aXZvJ10gPT0gMV1bJ2VuZmVybWVkYWQnXS5tZWFuKCkKcHJpbnQoZiJQKGVuZmVybWVkYWQgfCB0ZXN0IHBvc2l0aXZvKSDiiYgge2JheWVzX3Byb2I6LjRmfSIpCgojIDcuIFZhbG9yIGVzcGVyYWRvLCBjb3ZhcmlhbnphIHkgY29ycmVsYWNpw7NuCnZhbG9yX2VzcGVyYWRvX21wZyA9IGRmWydtcGcnXS5tZWFuKCkKdmFyX2hwID0gZGZbJ2hwJ10udmFyKCkKY292X21wZ19ocCA9IGRmW1snbXBnJywgJ2hwJ11dLmNvdigpLmlsb2NbMCwgMV0KY29yX21wZ19ocCA9IGRmW1snbXBnJywgJ2hwJ11dLmNvcnIoKS5pbG9jWzAsIDFdCgpwcmludChmIlZhbG9yIGVzcGVyYWRvIGRlIG1wZzoge3ZhbG9yX2VzcGVyYWRvX21wZzouMmZ9IikKcHJpbnQoZiJWYXJpYW56YSBkZSBocDoge3Zhcl9ocDouMmZ9IikKcHJpbnQoZiJDb3ZhcmlhbnphIG1wZy1ocDoge2Nvdl9tcGdfaHA6LjJmfSIpCnByaW50KGYiQ29ycmVsYWNpw7NuIG1wZy1ocDoge2Nvcl9tcGdfaHA6LjJmfSIpCgojIDguIEVzcGVyYW56YSBjb25kaWNpb25hbCB5IGxleSBkZSB2YXJpYW56YSB0b3RhbApjb25kX3N0YXRzID0gZGYuZ3JvdXBieSgnY3lsJykuYWdnKG1lZGlhX21wZz0oJ21wZycsICdtZWFuJyksIHZhcmlhbnphX21wZz0oJ21wZycsICd2YXInKSkucmVzZXRfaW5kZXgoKQoKdmFyX3RvdGFsID0gZGZbJ21wZyddLnZhcigpCmVzcGVyYW56YV9jb25kID0gY29uZF9zdGF0c1sndmFyaWFuemFfbXBnJ10ubWVhbigpCnZhcl9lc3BlcmFuemFzID0gY29uZF9zdGF0c1snbWVkaWFfbXBnJ10udmFyKCkKCnByaW50KGYiXG5WYXJpYW56YSB0b3RhbDoge3Zhcl90b3RhbDouNGZ9IikKcHJpbnQoZiJFW1ZhcihZfFgpXToge2VzcGVyYW56YV9jb25kOi40Zn0iKQpwcmludChmIlZhcihFW1l8WF0pOiB7dmFyX2VzcGVyYW56YXM6LjRmfSIpCnByaW50KGYiU3VtYSAoRVtWYXIoWXxYKV0gKyBWYXIoRVtZfFhdKSk6IHtlc3BlcmFuemFfY29uZCArIHZhcl9lc3BlcmFuemFzOi40Zn0iKQoKYGBgCgojIDMpIERpc3RyaWJ1Y2nDs24gbXVsdGl2YXJpYW50ZQoKIyMgMy4xKSBEaXN0cmlidWNpb24gbm9ybWFsIG11bHRpdmFyaWFudGUKCioqRGlzdHJpYnVjaW9uIE5vcm1hbCBVbml2YXJpYW50ZSoqCgpMYSBkZW5zaWRhZCBub3JtYWwgdW5pdmFyaWFudGUgKHRhbWJpw6luIGNvbm9jaWRhIGNvbW8gZGlzdHJpYnVjacOzbiBub3JtYWwgbyBnYXVzc2lhbmEpIGVzIHVuYSBmdW5jacOzbiBkZSBwcm9iYWJpbGlkYWQgcXVlIGRlc2NyaWJlIGPDs21vIHNlIGRpc3RyaWJ1eWVuIGxvcyB2YWxvcmVzIGRlIHVuYSB2YXJpYWJsZSBhbGVhdG9yaWEgY29udGludWEgZW4gdG9ybm8gYSBzdSBtZWRpYS4gVGllbmUgdW5hIGZvcm1hIGRlIGNhbXBhbmEgc2ltw6l0cmljYSwgZG9uZGUgbG9zIHZhbG9yZXMgY2VyY2Fub3MgYSBsYSBtZWRpYSBzb24gbG9zIG3DoXMgcHJvYmFibGVzIHkgbGEgcHJvYmFiaWxpZGFkIGRpc21pbnV5ZSBhIG1lZGlkYSBxdWUgbm9zIGFsZWphbW9zIGRlIGVsbGEuIEVzdMOhIGNvbXBsZXRhbWVudGUgZGV0ZXJtaW5hZGEgcG9yIGRvcyBwYXLDoW1ldHJvczogbGEgbWVkaWEgKM68KSwgcXVlIGluZGljYSBlbCBjZW50cm8gZGUgbGEgZGlzdHJpYnVjacOzbiwgeSBsYSBkZXN2aWFjacOzbiBlc3TDoW5kYXIgKM+DKSwgcXVlIHJlZmxlamEgbGEgZGlzcGVyc2nDs24gbyBlbCBhbmNobyBkZSBsYSBjYW1wYW5hLgoKTWF0ZW3DoXRpY2FtZW50ZSwgc3UgZnVuY2nDs24gZGUgZGVuc2lkYWQgc2UgZXhwcmVzYSBjb21vOgoKJCQKZih4KSA9IFxmcmFjezF9e1xzcXJ0ezJccGlcc2lnbWFeMn19IFwsIGVeey1cZnJhY3soeCAtIFxtdSleMn17MlxzaWdtYV4yfX0KJCQKCkVzdGEgZGlzdHJpYnVjacOzbiBlcyBmdW5kYW1lbnRhbCBlbiBlc3RhZMOtc3RpY2EgcG9ycXVlIG1vZGVsYSBtdWNob3MgZmVuw7NtZW5vcyBuYXR1cmFsZXMgeSBlcyBsYSBiYXNlIGRlbCB0ZW9yZW1hIGNlbnRyYWwgZGVsIGzDrW1pdGUsIHF1ZSBlc3RhYmxlY2UgcXVlIGxhIHN1bWEgZGUgbXVjaGFzIHZhcmlhYmxlcyBhbGVhdG9yaWFzIHRpZW5kZSBhIHNlZ3VpciB1bmEgZGlzdHJpYnVjacOzbiBub3JtYWwsIGluY2x1c28gc2kgZXNhcyB2YXJpYWJsZXMgbm8gc29uIG5vcm1hbGVzIGluZGl2aWR1YWxtZW50ZS4KCkVuIHVuIGNhc2/CoCoqYml2YXJpYW50ZSoqLCBzZSBhbmFsaXphbiBkb3MgdmFyaWFibGVzIGFsIG1pc21vIHRpZW1wbyBwYXJhIGVudGVuZGVyIGxhIHJlbGFjacOzbiBlbnRyZSBlbGxhcy4gUG9yIGVqZW1wbG8sIHNpIGVzdHVkaWFtb3MgZWwgaW5ncmVzbyBtZW5zdWFsIHkgZWwgbml2ZWwgZWR1Y2F0aXZvIGRlIHVuYSBtdWVzdHJhIGRlIHBlcnNvbmFzLCBidXNjYW1vcyB2ZXIgY8OzbW8gY2FtYmlhIHVuYSB2YXJpYWJsZSByZXNwZWN0byBhIGxhIG90cmEuIEVzdGUgYW7DoWxpc2lzIHBlcm1pdGUgaWRlbnRpZmljYXIgY29ycmVsYWNpb25lcyAocG9zaXRpdmFzLCBuZWdhdGl2YXMgbyBudWxhcyksIHZpc3VhbGl6YXIgbGEgcmVsYWNpw7NuIGNvbiBncsOhZmljb3MgY29tbyBlbCBkaWFncmFtYSBkZSBkaXNwZXJzacOzbiB5IGFqdXN0YXIgbW9kZWxvcyBjb21vIGxhIHJlZ3Jlc2nDs24gbGluZWFsIHBhcmEgcHJlZGVjaXIgdW5hIHZhcmlhYmxlIGVuIGZ1bmNpw7NuIGRlIGxhIG90cmEsIGF5dWRhbmRvIGFzw60gYSBjb21wcmVuZGVyIHBhdHJvbmVzIGNvbmp1bnRvcyB5IGRlcGVuZGVuY2lhcyBlbnRyZSBhbWJhcyBkaW1lbnNpb25lcy4KCkxhIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIGVzdMOhbmRhciBlcyB1bmEgdmVyc2nDs24gZXNwZWPDrWZpY2EgZGUgbGEgZGlzdHJpYnVjacOzbiBub3JtYWwgZW4gbGEgcXVlIGxhIG1lZGlhIGVzIDAgeSBsYSBkZXN2aWFjacOzbiBlc3TDoW5kYXIgZXMgMSwgeSBzZSB1dGlsaXphIGNvbW8gcmVmZXJlbmNpYSBwYXJhIGNvbXBhcmFyIGRpc3RpbnRvcyBjb25qdW50b3MgZGUgZGF0b3MuIEVzdGEgdHJhbnNmb3JtYWNpw7NuIHNlIGxvZ3JhIG1lZGlhbnRlIGxhIGVzdGFuZGFyaXphY2nDs24sIHVuIHByb2Nlc28gcXVlIGNvbnZpZXJ0ZSBjdWFscXVpZXIgdmFsb3IgZGUgdW5hIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIGdlbmVyYWwgYSB1biB2YWxvciBkZSBsYSBub3JtYWwgZXN0w6FuZGFyIG1lZGlhbnRlIGxhIGbDs3JtdWxhICRaID0gXGZyYWN7WCAtIFxtdX17XHNpZ21hfSQsIGRvbmRlICRYJCBlcyBlbCB2YWxvciBvcmlnaW5hbCwgJFxtdSQgbGEgbWVkaWEgeSAkXHNpZ21hJCBsYSBkZXN2aWFjacOzbiBlc3TDoW5kYXIuIEVzdGEgZGlzdHJpYnVjacOzbiBlcyBzaW3DqXRyaWNhIGVuIHRvcm5vIGEgbGEgbWVkaWEsIHRpZW5lIGZvcm1hIGRlIGNhbXBhbmEsIHkgcGVybWl0ZSBjYWxjdWxhciBwcm9iYWJpbGlkYWRlcyB5IHJlYWxpemFyIGNvbXBhcmFjaW9uZXMgYWwgZXhwcmVzYXIgY3XDoW4gbGVqb3MgZXN0w6EgdW4gZGF0byByZXNwZWN0byBhIGxhIG1lZGlhIGVuIHVuaWRhZGVzIGRlIGRlc3ZpYWNpw7NuIGVzdMOhbmRhci4KCkxhIGVzdGFuZGFyaXphY2nDs24gaW52ZXJzYSBlcyBlbCBwcm9jZXNvIG1lZGlhbnRlIGVsIGN1YWwgc2UgdHJhbnNmb3JtYSB1biB2YWxvciBlc3RhbmRhcml6YWRvIChwdW50YWplIHopIGRlIGxhIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIGVzdMOhbmRhciBkZSB2dWVsdGEgYSBzdSBlc2NhbGEgb3JpZ2luYWwuIEVzdG8gc2UgbG9ncmEgdXNhbmRvIGxhIGbDs3JtdWxhIGludmVyc2E6ICRYID0gWiBcY2RvdCBcc2lnbWEgKyBcbXUkLCBkb25kZSAkWiQgZXMgZWwgdmFsb3IgZXN0YW5kYXJpemFkbywgJFxzaWdtYSQgZXMgbGEgZGVzdmlhY2nDs24gZXN0w6FuZGFyIHkgJFxtdSQgbGEgbWVkaWEgZGVsIGNvbmp1bnRvIG9yaWdpbmFsLiBTaXJ2ZSBwYXJhIGludGVycHJldGFyIGxvcyByZXN1bHRhZG9zIGVuIHVuaWRhZGVzIHJlYWxlcyBkZXNwdcOpcyBkZSBoYWJlciB0cmFiYWphZG8gY29uIGRhdG9zIGVzdGFuZGFyaXphZG9zLCBwb3IgZWplbXBsbywgYWwgcHJlZGVjaXIgbyBzaW11bGFyIHZhbG9yZXMgZW4gc3UgY29udGV4dG8gb3JpZ2luYWwgdHJhcyBhcGxpY2FyIHVuIG1vZGVsbyBiYXNhZG8gZW4gbGEgbm9ybWFsIGVzdMOhbmRhci4KCkxhIHRyYW5zZm9ybWFjacOzbiBsaW5lYWwgZXMgdW5hIG9wZXJhY2nDs24gbWF0ZW3DoXRpY2EgcXVlIGFqdXN0YSBsb3MgdmFsb3JlcyBkZSB1bmEgdmFyaWFibGUgbWVkaWFudGUgdW5hIGbDs3JtdWxhIGRlIGxhIGZvcm1hICRYJyA9IGFYICsgYiQsIGRvbmRlICRhJCB5ICRiJCBzb24gY29uc3RhbnRlcyBxdWUgZXNjYWxhbiB5IHRyYXNsYWRhbiBsb3MgZGF0b3MsIHJlc3BlY3RpdmFtZW50ZS4gTGEgZXN0YW5kYXJpemFjacOzbiB5IHN1IGludmVyc2Egc29uIGNhc29zIGVzcGVjw61maWNvcyBkZSB0cmFuc2Zvcm1hY2nDs24gbGluZWFsOiBhbCBlc3RhbmRhcml6YXIsIHVzYW1vcyAkWiA9IFxmcmFje1ggLSBcbXV9e1xzaWdtYX0kLCBsbyBjdWFsIGVxdWl2YWxlIGEgcmVzdGFyIGxhIG1lZGlhICh0cmFzbGFjacOzbikgeSBkaXZpZGlyIHBvciBsYSBkZXN2aWFjacOzbiBlc3TDoW5kYXIgKGVzY2FsYW1pZW50byk7IGFsIHJldmVydGlybGEsIHVzYW1vcyAkWCA9IFogXGNkb3QgXHNpZ21hICsgXG11JCwgb3RyYSB0cmFuc2Zvcm1hY2nDs24gbGluZWFsLiBQb3IgdGFudG8sIGxhIHRyYW5zZm9ybWFjacOzbiBsaW5lYWwgZXMgZWwgbWFyY28gZ2VuZXJhbCBxdWUgcGVybWl0ZSBwYXNhciBkZSB1bmEgZXNjYWxhIG9yaWdpbmFsIGEgdW5hIGVzY2FsYSBkZXNlYWRhIChjb21vIGxhIG5vcm1hbCBlc3TDoW5kYXIpLCBmYWNpbGl0YW5kbyBjb21wYXJhY2lvbmVzLCBub3JtYWxpemFjacOzbiB5IGFuw6FsaXNpcyBlc3RhZMOtc3RpY28uCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgUGFyw6FtZXRyb3MgZGUgbGEgZGlzdHJpYnVjacOzbiBub3JtYWwKbXUgPC0gNTAgICAgICMgbWVkaWEKc2lnbWEgPC0gMTAgICMgZGVzdmlhY2nDs24gZXN0w6FuZGFyCgojIFNlY3VlbmNpYSBkZSB2YWxvcmVzIHgKeCA8LSBzZXEobXUgLSA0KnNpZ21hLCBtdSArIDQqc2lnbWEsIGxlbmd0aC5vdXQgPSAxMDAwKQoKIyBEZW5zaWRhZCBub3JtYWwgY29uIG1lZGlhIHkgZGVzdmlhY2nDs24gZGFkYQpkZW5zaWRhZF9ub3JtYWwgPC0gZG5vcm0oeCwgbWVhbiA9IG11LCBzZCA9IHNpZ21hKQoKIyBEZW5zaWRhZCBkZSBsYSBub3JtYWwgZXN0w6FuZGFyIChtdSA9IDAsIHNpZ21hID0gMSkKeF9zdGQgPC0gc2VxKC00LCA0LCBsZW5ndGgub3V0ID0gMTAwMCkKZGVuc2lkYWRfZXN0YW5kYXIgPC0gZG5vcm0oeF9zdGQpCgojIENyZWFyIGRhdGEgZnJhbWUgcGFyYSBncmFmaWNhciBhbWJhcwpkZiA8LSBkYXRhLmZyYW1lKAogIHggPSB4LAogIGRlbnNpZGFkID0gZGVuc2lkYWRfbm9ybWFsLAogIHhfc3RkID0geF9zdGQsCiAgZGVuc2lkYWRfc3RkID0gZGVuc2lkYWRfZXN0YW5kYXIKKQoKIyBHcmFmaWNhciBhbWJhcyBjdXJ2YXMgZGUgZGVuc2lkYWQKZ2dwbG90KCkgKwogIGdlb21fbGluZShkYXRhID0gZGYsIGFlcyh4ID0geCwgeSA9IGRlbnNpZGFkKSwgY29sb3IgPSAiYmx1ZSIsIHNpemUgPSAxLjIpICsKICBnZW9tX2xpbmUoZGF0YSA9IGRmLCBhZXMoeCA9IHhfc3RkICogc2lnbWEgKyBtdSwgeSA9IGRlbnNpZGFkX3N0ZCksIAogICAgICAgICAgICBjb2xvciA9ICJyZWQiLCBsaW5ldHlwZSA9ICJkYXNoZWQiLCBzaXplID0gMS4yKSArCiAgbGFicyh0aXRsZSA9ICJEaXN0cmlidWNpw7NuIE5vcm1hbCBVbml2YXJpYW50ZSB5IE5vcm1hbCBFc3TDoW5kYXIiLAogICAgICAgc3VidGl0bGUgPSAiQXp1bDogTig1MCwxMCkgfCBSb2pvOiBOKDAsMSkgcmVlc2NhbGFkYSIsCiAgICAgICB4ID0gIngiLCB5ID0gIkRlbnNpZGFkIikgKwogIHRoZW1lX21pbmltYWwoKQoKIyAtLS0gRXN0YW5kYXJpemFjacOzbiBkZSB1biB2YWxvciBYIC0tLQpYIDwtIDcwClogPC0gKFggLSBtdSkgLyBzaWdtYSAgIyBFc3RhbmRhcml6YWNpw7NuCmNhdCgiVmFsb3Igb3JpZ2luYWw6IiwgWCwgIlxuUHVudGFqZSBaOiIsIFosICJcbiIpCgojIC0tLSBFc3RhbmRhcml6YWNpw7NuIGludmVyc2EgLS0tClhfcmVjdXBlcmFkbyA8LSBaICogc2lnbWEgKyBtdQpjYXQoIlZhbG9yIHJlY3VwZXJhZG8gZGVzZGUgWjoiLCBYX3JlY3VwZXJhZG8sICJcbiIpCgojIC0tLSBUcmFuc2Zvcm1hY2nDs24gbGluZWFsIGdlbmVyYWwgLS0tCiMgUG9yIGVqZW1wbG86IFgnID0gMlggKyA1CmEgPC0gMgpiIDwtIDUKWF90cmFuc2Zvcm1hZG8gPC0gYSAqIFggKyBiCmNhdCgiVHJhbnNmb3JtYWNpw7NuIGxpbmVhbCBYJyA9IDJYICsgNTpcblJlc3VsdGFkbzoiLCBYX3RyYW5zZm9ybWFkbywgIlxuIikKCmBgYAoKIyMgMy4yKSBDdXJ2YXMgZGUgbml2ZWwgbyBjb250b3JubwoKVW5hIGN1cnZhIGRlIG5pdmVsIG8gbMOtbmVhIGRlIGNvbnRvcm5vIGVzIHVuYSByZXByZXNlbnRhY2nDs24gZ3LDoWZpY2EgcXVlIGNvbmVjdGEgcHVudG9zIHF1ZSB0aWVuZW4gZWwgbWlzbW8gdmFsb3IgZGUgdW5hIGZ1bmNpw7NuIGVuIHVuIHBsYW5vIGJpZGltZW5zaW9uYWwuIEVuIGVzdGFkw61zdGljYSB5IGFuw6FsaXNpcyBtdWx0aXZhcmlhZG8sIGVzdGFzIGN1cnZhcyBzZSB1dGlsaXphbiBjb23Dum5tZW50ZSBwYXJhIHZpc3VhbGl6YXIgZnVuY2lvbmVzIGRlIGRlbnNpZGFkIGNvbmp1bnRhLCBlc3BlY2lhbG1lbnRlIGVuIGRpc3RyaWJ1Y2lvbmVzIGJpdmFyaWFkYXMgY29tbyBsYSBub3JtYWwgYml2YXJpYW50ZS4gRW4gZXN0ZSBjb250ZXh0bywgY2FkYSBjdXJ2YSBpbmRpY2EgdW5hIHJlZ2nDs24gZGVsIGVzcGFjaW8gZG9uZGUgbGEgY29tYmluYWNpw7NuIGRlIHZhbG9yZXMgZGUgbGFzIGRvcyB2YXJpYWJsZXMgdGllbmUgbGEgbWlzbWEgZGVuc2lkYWQgZGUgcHJvYmFiaWxpZGFkLCBzaW1pbGFyIGEgY8OzbW8gdW4gbWFwYSB0b3BvZ3LDoWZpY28gbXVlc3RyYSBhbHR1cmFzIGNvbnN0YW50ZXMuCgpFc3RhcyBjdXJ2YXMgc29uIMO6dGlsZXMgcG9ycXVlIHBlcm1pdGVuIGlkZW50aWZpY2FyIHZpc3VhbG1lbnRlIGxhIGZvcm1hLCBvcmllbnRhY2nDs24geSBjb25jZW50cmFjacOzbiBkZSBsYSBkaXN0cmlidWNpw7NuIGNvbmp1bnRhLiBQb3IgZWplbXBsbywgZW4gdW5hIG5vcm1hbCBiaXZhcmlhbnRlLCBsYXMgY3VydmFzIGRlIG5pdmVsIHN1ZWxlbiB0b21hciBsYSBmb3JtYSBkZSBlbGlwc2VzLCBjdXlhIG9yaWVudGFjacOzbiB5IGFsYXJnYW1pZW50byByZWZsZWphbiBsYSBkaXJlY2Npw7NuIHkgZnVlcnphIGRlIGxhIGNvcnJlbGFjacOzbiBlbnRyZSBsYXMgdmFyaWFibGVzLiBBc8OtLCBsYXMgY3VydmFzIGRlIG5pdmVsIHBlcm1pdGVuIGVudGVuZGVyIGPDs21vIHNlIHJlbGFjaW9uYW4gZG9zIHZhcmlhYmxlcywgZGV0ZWN0YXIgcGF0cm9uZXMgZGUgZGVwZW5kZW5jaWEsIHkgY29tcGFyYXIgZGlzdHJpYnVjaW9uZXMgbXVsdGl2YXJpYW50ZXMgZGUgbWFuZXJhIGludHVpdGl2YSwgc2luIG5lY2VzaWRhZCBkZSBpbnRlcnByZXRhciBkaXJlY3RhbWVudGUgZsOzcm11bGFzIG8gdGFibGFzIG51bcOpcmljYXMuCgpFbCBjYXNvIGJpdmFyaWFkbyBzZSByZWZpZXJlIGFsIGFuw6FsaXNpcyBjb25qdW50byBkZSBkb3MgdmFyaWFibGVzIGFsZWF0b3JpYXMsIHBlcm1pdGllbmRvIGVzdHVkaWFyIGPDs21vIHNlIHJlbGFjaW9uYW4gZW50cmUgc8OtIGVuIHTDqXJtaW5vcyBkZSBkZXBlbmRlbmNpYSwgYXNvY2lhY2nDs24gbyBjb3JyZWxhY2nDs24uIEEgZGlmZXJlbmNpYSBkZWwgY2FzbyB1bml2YXJpYWRvLCBxdWUgZGVzY3JpYmUgdW5hIHNvbGEgdmFyaWFibGUsIGVsIGFuw6FsaXNpcyBiaXZhcmlhZG8gZXhhbWluYSBzaW11bHTDoW5lYW1lbnRlIGxhIGRpc3RyaWJ1Y2nDs24gY29uanVudGEsIGNvbW8gZW4gZWwgY2FzbyBkZSBsYSBkaXN0cmlidWNpw7NuIG5vcm1hbCBiaXZhcmlhbnRlLCBkb25kZSBjYWRhIHBhciBkZSB2YWxvcmVzIHRpZW5lIHVuYSBwcm9iYWJpbGlkYWQgYXNvY2lhZGEuIEVzdG8gcGVybWl0ZSB2aXN1YWxpemFyIHJlbGFjaW9uZXMgbWVkaWFudGUgZGlhZ3JhbWFzIGRlIGRpc3BlcnNpw7NuIHkgY3VydmFzIGRlIG5pdmVsIChjb21vIGVsaXBzZXMgZW4gdW5hIG5vcm1hbCBiaXZhcmlhbnRlKSwgaWRlbnRpZmljYXIgcGF0cm9uZXMgbGluZWFsZXMgbyBubyBsaW5lYWxlcywgeSBhanVzdGFyIG1vZGVsb3MgY29tbyBsYSByZWdyZXNpw7NuIHBhcmEgcHJlZGVjaXIgdW5hIHZhcmlhYmxlIGVuIGZ1bmNpw7NuIGRlIGxhIG90cmEsIHNpZW5kbyBlc2VuY2lhbCBwYXJhIGVudGVuZGVyIGludGVyYWNjaW9uZXMgZW50cmUgZGltZW5zaW9uZXMgZW4gY29uanVudG9zIGRlIGRhdG9zIG11bHRpdmFyaWFkb3MuCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgUGFyw6FtZXRyb3MgZGUgbGEgbm9ybWFsIGJpdmFyaWFkYQptdSA8LSBjKDAsIDApICAjIG1lZGlhcyBkZSBYIHkgWQpzaWdtYSA8LSBtYXRyaXgoYygxLCAwLjgsIAogICAgICAgICAgICAgICAgICAwLjgsIDEpLCBucm93ID0gMikgICMgbWF0cml6IGRlIGNvdmFyaWFuemEgY29uIGNvcnJlbGFjacOzbiBwb3NpdGl2YQoKIyBTaW11bGFyIGRhdG9zIGJpdmFyaWFkb3MKc2V0LnNlZWQoMTIzKQpkYXRvcyA8LSBhcy5kYXRhLmZyYW1lKG12cm5vcm0obiA9IDEwMDAsIG11ID0gbXUsIFNpZ21hID0gc2lnbWEpKQpjb2xuYW1lcyhkYXRvcykgPC0gYygiWCIsICJZIikKCiMgR3JhZmljYXIgY3VydmFzIGRlIG5pdmVsIChkZW5zaWRhZCBjb25qdW50YSkgY29uIHNjYXR0ZXJwbG90CmdncGxvdChkYXRvcywgYWVzKHggPSBYLCB5ID0gWSkpICsKICBzdGF0X2RlbnNpdHlfMmQoYWVzKGZpbGwgPSAuLmxldmVsLi4pLCBnZW9tID0gInBvbHlnb24iLCBjb2xvciA9ICJibGFjayIsIGFscGhhID0gMC42KSArCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMywgY29sb3IgPSAiYmx1ZSIsIHNpemUgPSAwLjgpICsKICBzY2FsZV9maWxsX3ZpcmlkaXNfYyhvcHRpb24gPSAiRCIpICsKICBjb29yZF9maXhlZCgpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIGxhYnMoCiAgICB0aXRsZSA9ICJDdXJ2YXMgZGUgTml2ZWwgZGUgbGEgRGlzdHJpYnVjacOzbiBOb3JtYWwgQml2YXJpYWRhIiwKICAgIHN1YnRpdGxlID0gIkVsaXBzZXMgbXVlc3RyYW4gcmVnaW9uZXMgY29uIGlndWFsIGRlbnNpZGFkIGNvbmp1bnRhIiwKICAgIHggPSAiVmFyaWFibGUgWCIsCiAgICB5ID0gIlZhcmlhYmxlIFkiLAogICAgZmlsbCA9ICJEZW5zaWRhZCIKICApCgpgYGAKCkxhIGRlbnNpZGFkIGRlIGtlcm5lbCBlcyB1biBtw6l0b2RvIG5vIHBhcmFtw6l0cmljbyBwYXJhIGVzdGltYXIgbGEgZnVuY2nDs24gZGUgZGVuc2lkYWQgZGUgcHJvYmFiaWxpZGFkIGRlIHVuYSB2YXJpYWJsZSBhbGVhdG9yaWEgY29udGludWEgYSBwYXJ0aXIgZGUgdW5hIG11ZXN0cmEgZGUgZGF0b3MuIEEgZGlmZXJlbmNpYSBkZSBsb3MgaGlzdG9ncmFtYXMsIHF1ZSBhZ3J1cGFuIGxvcyBkYXRvcyBlbiBpbnRlcnZhbG9zLCBsYSBkZW5zaWRhZCBkZSBrZXJuZWwgZ2VuZXJhIHVuYSBjdXJ2YSBzdWF2ZSBxdWUgYXByb3hpbWEgY8OzbW8gc2UgZGlzdHJpYnV5ZW4gbG9zIGRhdG9zLCBzaW4gYXN1bWlyIHVuYSBmb3JtYSBlc3BlY8OtZmljYSBjb21vIGxhIG5vcm1hbC4gRXN0ZSBtw6l0b2RvIGNvbG9jYSB1bmEgZnVuY2nDs24gc2ltw6l0cmljYSAoZWwgImtlcm5lbCIsIGNvbcO6bm1lbnRlIHVuYSBub3JtYWwpIGNlbnRyYWRhIGVuIGNhZGEgcHVudG8gZGUgbG9zIGRhdG9zIHkgbGFzIHN1bWEgcGFyYSBmb3JtYXIgbGEgZXN0aW1hY2nDs24gZ2xvYmFsLiBMYSBzdWF2aWRhZCBkZSBsYSBjdXJ2YSBkZXBlbmRlIGRlbCBhbmNobyBkZSBiYW5kYSBvIHBhcsOhbWV0cm8gZGUgc3Vhdml6YWNpw7NuOiB1biB2YWxvciBwZXF1ZcOxbyBwcm9kdWNlIHVuYSBjdXJ2YSBtdXkgaXJyZWd1bGFyIChzb2JyZWFqdXN0ZSksIG1pZW50cmFzIHF1ZSB1bm8gbXV5IGdyYW5kZSBnZW5lcmEgdW5hIGN1cnZhIGRlbWFzaWFkbyBsaXNhIChzdWJhanVzdGUpLiBFcyBlc3BlY2lhbG1lbnRlIMO6dGlsIHBhcmEgZXhwbG9yYXIgbGEgZGlzdHJpYnVjacOzbiBzdWJ5YWNlbnRlIGRlIGxvcyBkYXRvcyBkZSBtYW5lcmEgZmxleGlibGUgeSB2aXN1YWwuCgpgYGB7cn0KCgojIFNpbXVsYXIgZGF0b3MgYml2YXJpYWRvcyAoY29uIGNpZXJ0YSBjb3JyZWxhY2nDs24pCnNldC5zZWVkKDQyKQpuIDwtIDUwMAp4IDwtIHJub3JtKG4sIG1lYW4gPSAwLCBzZCA9IDEpCnkgPC0gMC41ICogeCArIHJub3JtKG4sIG1lYW4gPSAwLCBzZCA9IDEpCmRhdG9zIDwtIGRhdGEuZnJhbWUoeCwgeSkKCiMgRXN0aW1hciBkZW5zaWRhZCBrZXJuZWwgYmlkaW1lbnNpb25hbApkZW5zaWRhZCA8LSBrZGUyZChkYXRvcyR4LCBkYXRvcyR5LCBuID0gNTApCgojIFByZXBhcmFyIGxhIGdyw6FmaWNhIDNEIGNvbiBjb250b3Jub3MKZmlnIDwtIHBsb3RfbHkoCiAgeCA9IGRlbnNpZGFkJHgsIHkgPSBkZW5zaWRhZCR5LCB6ID0gZGVuc2lkYWQkeiwKICB0eXBlID0gInN1cmZhY2UiLAogIGNvbnRvdXJzID0gbGlzdCgKICAgIHogPSBsaXN0KHNob3cgPSBUUlVFLCB1c2Vjb2xvcm1hcCA9IFRSVUUsIGhpZ2hsaWdodGNvbG9yID0gIiNmZjAwMDAiLCBoaWdobGlnaHR3aWR0aCA9IDIpCiAgKQopICU+JQogIGxheW91dCgKICAgIHRpdGxlID0gIkRlbnNpZGFkIEtlcm5lbCAyRCBjb24gc3VwZXJmaWNpZSAzRCB5IGNvbnRvcm5vcyIsCiAgICBzY2VuZSA9IGxpc3QoCiAgICAgIHhheGlzID0gbGlzdCh0aXRsZSA9ICJYIiksCiAgICAgIHlheGlzID0gbGlzdCh0aXRsZSA9ICJZIiksCiAgICAgIHpheGlzID0gbGlzdCh0aXRsZSA9ICJEZW5zaWRhZCIpCiAgICApCiAgKQoKIyBNb3N0cmFyIGdyw6FmaWNvIGludGVyYWN0aXZvCmZpZwoKYGBgCgojIyAzLjMpIERpc3RhbmNpYSBFdWNsaWRpYW5hCgpMYSBkaXN0YW5jaWEgZXVjbGlkaWFuYSBlcyB1bmEgbWVkaWRhIGdlb23DqXRyaWNhIHF1ZSBjYWxjdWxhIGxhIGxvbmdpdHVkIGRlbCBzZWdtZW50byByZWN0byBlbnRyZSBkb3MgcHVudG9zIGVuIHVuIGVzcGFjaW8gZXVjbGlkaWFuby4gRW4gZG9zIGRpbWVuc2lvbmVzLCBzaSBzZSB0aWVuZW4gZG9zIHB1bnRvcyAkQSA9ICh4XzEsIHlfMSkkIHkgJEIgPSAoeF8yLCB5XzIpJCwgc3UgZGlzdGFuY2lhIGV1Y2xpZGlhbmEgc2UgZGVmaW5lIGNvbW86CgokJApkKEEsIEIpID0gXHNxcnR7KHhfMiAtIHhfMSleMiArICh5XzIgLSB5XzEpXjJ9CiQkCgpFc3RlIGNvbmNlcHRvIHNlIGV4dGllbmRlIG5hdHVyYWxtZW50ZSBhIGVzcGFjaW9zIGRlIG3DoXMgZGltZW5zaW9uZXMuIEVuIGdlbmVyYWwsIHBhcmEgZG9zIHB1bnRvcyAkQSA9ICh4XzEsIHhfMiwgXGRvdHMsIHhfbikkIHkgJEIgPSAoeV8xLCB5XzIsIFxkb3RzLCB5X24pJCBlbiB1biBlc3BhY2lvICRuJC1kaW1lbnNpb25hbCwgbGEgZsOzcm11bGEgZXM6CgokJApkKEEsIEIpID0gXHNxcnR7XHN1bV97aT0xfV57bn0gKHhfaSAtIHlfaSleMn0KJCQKCkxhIGRpc3RhbmNpYSBldWNsaWRpYW5hIGVzIGFtcGxpYW1lbnRlIHV0aWxpemFkYSBlbiBlc3RhZMOtc3RpY2EgbXVsdGl2YXJpYWRhLCBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyB5IGFuw6FsaXNpcyBkZSBkYXRvcyBwYXJhIGV2YWx1YXIgbGEgc2ltaWxpdHVkIG8gZGlzaW1pbGl0dWQgZW50cmUgb2JzZXJ2YWNpb25lcy4gU2lydmUsIHBvciBlamVtcGxvLCBlbiBhbGdvcml0bW9zIGRlIGNsdXN0ZXJpbmcgY29tbyBrLW1lYW5zLCBjbGFzaWZpY2FjacOzbiBjb21vIGstTk4sIHkgYW7DoWxpc2lzIGRlIGNvbmdsb21lcmFkb3MgamVyw6FycXVpY29zLCBkb25kZSBheXVkYSBhIGFncnVwYXIgZGF0b3MgcXVlIGVzdMOhbiDigJxjZXJjYeKAnSB1bm9zIGRlIG90cm9zIGVuIGVsIGVzcGFjaW8gZGUgY2FyYWN0ZXLDrXN0aWNhcy4gU3Ugc2ltcGxpY2lkYWQgeSBzaWduaWZpY2FkbyBnZW9tw6l0cmljbyBsYSBjb252aWVydGVuIGVuIHVuYSBkZSBsYXMgbWVkaWRhcyBkZSBkaXN0YW5jaWEgbcOhcyBpbnR1aXRpdmFzIHkgdW5pdmVyc2FsZXMuCgpgYGB7cn0KCgojIERlZmluaXIgbG9zIHB1bnRvcyBBIHkgQgpBIDwtIGMoMiwgMykKQiA8LSBjKDcsIDYpCgojIENhbGN1bGFyIGRpc3RhbmNpYSBldWNsaWRpYW5hCmRpc3RhbmNpYSA8LSBzcXJ0KChCWzFdIC0gQVsxXSleMiArIChCWzJdIC0gQVsyXSleMikKCiMgQ3JlYXIgZGF0YSBmcmFtZSBjb24gbG9zIHB1bnRvcwpwdW50b3MgPC0gZGF0YS5mcmFtZSgKICB4ID0gYyhBWzFdLCBCWzFdKSwKICB5ID0gYyhBWzJdLCBCWzJdKSwKICBsYWJlbCA9IGMoIkEiLCAiQiIpCikKCiMgR3LDoWZpY28KZ2dwbG90KHB1bnRvcywgYWVzKHggPSB4LCB5ID0geSkpICsKICBnZW9tX3BvaW50KHNpemUgPSA0LCBjb2xvciA9ICJibHVlIikgKwogIGdlb21fdGV4dChhZXMobGFiZWwgPSBsYWJlbCksIGhqdXN0ID0gLTAuMywgdmp1c3QgPSAtMC41LCBzaXplID0gNSkgKwogIGdlb21fc2VnbWVudChhZXMoeCA9IEFbMV0sIHkgPSBBWzJdLCB4ZW5kID0gQlsxXSwgeWVuZCA9IEJbMl0pLCAKICAgICAgICAgICAgICAgYXJyb3cgPSBhcnJvdyhsZW5ndGggPSB1bml0KDAuMiwgImNtIikpLCAKICAgICAgICAgICAgICAgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDEuMikgKwogIGFubm90YXRlKCJ0ZXh0IiwgeCA9IChBWzFdICsgQlsxXSkvMiwgeSA9IChBWzJdICsgQlsyXSkvMiArIDAuNSwKICAgICAgICAgICBsYWJlbCA9IHBhc3RlMCgiRGlzdGFuY2lhIEV1Y2xpZGlhbmEgPSAiLCByb3VuZChkaXN0YW5jaWEsIDIpKSwKICAgICAgICAgICBjb2xvciA9ICJibGFjayIsIHNpemUgPSA1KSArCiAgeGxpbSgwLCAxMCkgKyB5bGltKDAsIDEwKSArCiAgY29vcmRfZml4ZWQoKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICBsYWJzKHRpdGxlID0gIlZpc3VhbGl6YWNpw7NuIGRlIGxhIERpc3RhbmNpYSBFdWNsaWRpYW5hIGVudHJlIGRvcyBwdW50b3MiLAogICAgICAgeCA9ICJFamUgWCIsIHkgPSAiRWplIFkiKQoKYGBgCgojIyAzLjQpIERpc3RhbmNpYSBkZSBNYWhhbGFub2JpcwoKRXN0YSBtZWRpZGEgZXMgaW1wb3J0YW50ZSB5YSBxdWUgbGEgZGlzdGFuY2lhIGV1Y2xpZGlhbmEgdGllbmUgbGEgZGVzdmVudGFqYSBkZSBlbiBlbCBlc3BhY2lvIG11bHRpdmFyaWFudGUgbGFzIHZhcmlhYmxlcyBwdWVkZW4gZXN0YXIgY29ycmVsYWNpb25hZGFzIHkgbGEgZXVjbGlkaWFuYSBubyBsbyB0aWVuZW4gZW4gY3VlbnRhLiBQb3IgbG8gdGFudG8sIGN1YW5kbyBsYSBkaXN0cmlidWNpw7NuIG5vIGVzIGVzZsOpcmljYSBlcyBtZWpvciB1c2FyIHVuYSBkaXN0cmlidWNpw7NuIHF1ZSBzaSB0ZW5nYSBlbiBjdWVudGEgbGFzIGNvcnJlbGFjaW9uZXMgeSBjb3ZhcmluYXphcwoKIVtdKGltYWdlcy9jbGlwYm9hcmQtMjE1NTU4MTQ4NC5wbmcpCgpMYSBkaXN0YW5jaWEgZGUgTWFoYWxhbm9iaXMgZXMgdW5hIG1lZGlkYSBlc3RhZMOtc3RpY2EgdXRpbGl6YWRhIHBhcmEgY2FsY3VsYXIgY3XDoW4gbGVqb3MgZXN0w6EgdW4gcHVudG8gcmVzcGVjdG8gYSB1bmEgZGlzdHJpYnVjacOzbiBkZSBkYXRvcyBtdWx0aXZhcmlhZGEsIGNvbnNpZGVyYW5kbyBsYXMgY29ycmVsYWNpb25lcyBlbnRyZSBsYXMgdmFyaWFibGVzLiBBIGRpZmVyZW5jaWEgZGUgbGEgZGlzdGFuY2lhIGV1Y2xpZGlhbmEsIHF1ZSBzaW1wbGVtZW50ZSBtaWRlIGxhIGRpc3RhbmNpYSByZWN0YSBlbnRyZSBkb3MgcHVudG9zIGVuIGVsIGVzcGFjaW8sIGxhIGRpc3RhbmNpYSBkZSBNYWhhbGFub2JpcyBhanVzdGEgZXN0YSBtZWRpZGEgdGVuaWVuZG8gZW4gY3VlbnRhIGxhIGVzdHJ1Y3R1cmEgZGUgdmFyaWFuemEgeSBjb3ZhcmlhbnphIGRlIGxvcyBkYXRvcy4gRXN0byBsYSBoYWNlIGVzcGVjaWFsbWVudGUgw7p0aWwgY3VhbmRvIGxhcyB2YXJpYWJsZXMgdGllbmVuIGRpc3RpbnRhcyBlc2NhbGFzIG8gZXN0w6FuIGNvcnJlbGFjaW9uYWRhcy4KCkVzdGEgZGlzdGFuY2lhIGZ1ZSBpbnRyb2R1Y2lkYSBlbiAxOTM2IHBvciBlbCBlc3RhZMOtc3RpY28gaW5kaW8gUHJhc2FudGEgQ2hhbmRyYSBNYWhhbGFub2JpcywgcXVpZW4gbGEgZGVzYXJyb2xsw7MgaW5pY2lhbG1lbnRlIHBhcmEgZXN0dWRpb3MgYW50cm9wb23DqXRyaWNvcywgY29tcGFyYW5kbyBjYXJhY3RlcsOtc3RpY2FzIGbDrXNpY2FzIGVudHJlIGRpZmVyZW50ZXMgZ3J1cG9zIHBvYmxhY2lvbmFsZXMuIERlc2RlIGVudG9uY2VzLCBzZSBoYSBjb252ZXJ0aWRvIGVuIHVuYSBoZXJyYW1pZW50YSBmdW5kYW1lbnRhbCBlbiBkaXZlcnNhcyDDoXJlYXMgY29tbyBlbCBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSwgbGEgZGV0ZWNjacOzbiBkZSB2YWxvcmVzIGF0w61waWNvcyAob3V0bGllcnMpLCBlbCBhbsOhbGlzaXMgZGUgY29uZ2xvbWVyYWRvcyAoY2x1c3RlcmluZyksIHkgZWwgcmVjb25vY2ltaWVudG8gZGUgcGF0cm9uZXMgZW4gaW50ZWxpZ2VuY2lhIGFydGlmaWNpYWwgeSBtYWNoaW5lIGxlYXJuaW5nLgoKTGEgZsOzcm11bGEgZGUgbGEgZGlzdGFuY2lhIGRlIE1haGFsYW5vYmlzIHBhcmEgdW4gdmVjdG9yIGRlIG9ic2VydmFjaW9uZXMgeCwgcmVzcGVjdG8gYSB1bmEgZGlzdHJpYnVjacOzbiBjb24gbWVkaWEgzrwgeSBtYXRyaXogZGUgY292YXJpYW56YSBTLCBzZSBleHByZXNhIGRlIGxhIHNpZ3VpZW50ZSBtYW5lcmE6CgokRF9NKFxtYXRoYmZ7eH0pID0gXHNxcnR7KFxtYXRoYmZ7eH0gLSBcYm9sZHN5bWJvbHtcbXV9KV5cdG9wIFxtYXRoYmZ7U31eey0xfSAoXG1hdGhiZnt4fSAtIFxib2xkc3ltYm9se1xtdX0pfSQKCkxhIGRpc3RhbmNpYSBkZSBNYWhhbGFub2JpcyBlcyB1bmEgaGVycmFtaWVudGEgZWZpY2F6IHBhcmEgZGV0ZWN0YXIgdmFsb3JlcyBhdMOtcGljb3MgKHRhbWJpw6luIGxsYW1hZG9zIG91dGxpZXJzKSBlbiBjb25qdW50b3MgZGUgZGF0b3MgY29uIG3Dumx0aXBsZXMgdmFyaWFibGVzLiBBIGRpZmVyZW5jaWEgZGUgbcOpdG9kb3MgdW5pdmFyaWFudGVzIGNvbW8gZWwgei1zY29yZSwgZXN0YSBkaXN0YW5jaWEgY29uc2lkZXJhIHNpbXVsdMOhbmVhbWVudGUgdG9kYXMgbGFzIHZhcmlhYmxlcyB5IGxhcyBjb3JyZWxhY2lvbmVzIGVudHJlIGVsbGFzLCBwb3IgbG8gcXVlIHBlcm1pdGUgdW5hIGRldGVjY2nDs24gbcOhcyBwcmVjaXNhIGVuIGNvbnRleHRvcyBtdWx0aXZhcmlhZG9zLgoKUGFyYSB1dGlsaXphciBlc3RhIHTDqWNuaWNhLCBwcmltZXJvIHNlIGRlYmUgY2FsY3VsYXIgbGEgbWVkaWEgbXVsdGl2YXJpYWRhIGRlbCBjb25qdW50byBkZSBkYXRvcyB5IGxhIG1hdHJpeiBkZSBjb3ZhcmlhbnphIGVudHJlIGxhcyB2YXJpYWJsZXMuIEx1ZWdvLCBwYXJhIGNhZGEgb2JzZXJ2YWNpw7NuLCBzZSBjYWxjdWxhIHN1IGRpc3RhbmNpYSBkZSBNYWhhbGFub2JpcyBhbCBjZW50cm8gZGUgbG9zIGRhdG9zLCB1c2FuZG8gbGEgZsOzcm11bGE6ICREX00oXG1hdGhiZnt4fV9pKSA9IFxzcXJ0eyhcbWF0aGJme3h9X2kgLSBcYm9sZHN5bWJvbHtcbXV9KV5cdG9wIFxtYXRoYmZ7U31eey0xfSAoXG1hdGhiZnt4fV9pIC0gXGJvbGRzeW1ib2x7XG11fSl9JC4gU2kgdW5hIG9ic2VydmFjacOzbiBlc3TDoSBtdXkgYWxlamFkYSDigJRlbiB0w6lybWlub3MgZGUgZXN0YSBkaXN0YW5jaWHigJQgc2UgY29uc2lkZXJhIHVuIHBvdGVuY2lhbCBhdMOtcGljby4KClVuIHBhc28gY2xhdmUgZXMgZGVmaW5pciB1biB1bWJyYWwgZGUgY29ydGUgcGFyYSBkZXRlcm1pbmFyIHF1w6kgdGFuIOKAnGxlam9z4oCdIGVzIGRlbWFzaWFkbyBsZWpvcy4gTGEgZGlzdGFuY2lhIGRlIE1haGFsYW5vYmlzIGFsIGN1YWRyYWRvICgkRF9NXjIkKSBzaWd1ZSwgYmFqbyBjaWVydGFzIGNvbmRpY2lvbmVzLCB1bmEgZGlzdHJpYnVjacOzbiBjaGktY3VhZHJhZG8gKCRcY2hpXjIkKSBjb24gZ3JhZG9zIGRlIGxpYmVydGFkIGlndWFsZXMgYWwgbsO6bWVybyBkZSB2YXJpYWJsZXMgKCRwJCkuIFBvciB0YW50bywgc2UgcHVlZGUgdXNhciB1biB2YWxvciBjcsOtdGljbyBkZSBlc2EgZGlzdHJpYnVjacOzbiBjb21vIHJlZmVyZW5jaWEuIFBvciBlamVtcGxvLCBzaSBzZSB0aWVuZW4gJDMkIHZhcmlhYmxlcyB5IHNlIGRlc2VhIHVuIG5pdmVsIGRlIGNvbmZpYW56YSBkZWwgJDk1JSQsIGN1YWxxdWllciBvYnNlcnZhY2nDs24gY29uIHVuYSBkaXN0YW5jaWEgYWwgY3VhZHJhZG8gbWF5b3IgcXVlICRcY2hpXjJfezMsIDAuOTV9IFxhcHByb3ggNy44MSQgc2Vyw6EgY29uc2lkZXJhZGEgdW4gYXTDrXBpY28uCgpFc3RlIGVuZm9xdWUgZXMgZXNwZWNpYWxtZW50ZSDDunRpbCBjdWFuZG8gbG9zIGRhdG9zIHNvbiBhcHJveGltYWRhbWVudGUgbm9ybWFsZXMgbXVsdGl2YXJpYWRvcy4gU2kgZXNhIGNvbmRpY2nDs24gbm8gc2UgY3VtcGxlLCBsYSBkaXN0YW5jaWEgZGUgTWFoYWxhbm9iaXMgcHVlZGUgZGFyIGZhbHNvcyBwb3NpdGl2b3MgbyBuZWdhdGl2b3MuIFBvciBlc28sIGVzIGJ1ZW5hIHByw6FjdGljYSBjb21wbGVtZW50YXJsYSBjb24gdmlzdWFsaXphY2lvbmVzIGNvbW8gZ3LDoWZpY29zIGRlIGRpc3BlcnNpw7NuLCBhbsOhbGlzaXMgZGUgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMgKFBDQSkgbyBtw6l0b2RvcyByb2J1c3Rvcy4KCkVuIHJlc3VtZW4sIGxhIGRpc3RhbmNpYSBkZSBNYWhhbGFub2JpcyBwZXJtaXRlIGRldGVjdGFyIG9ic2VydmFjaW9uZXMgaW51c3VhbGVzIGNvbnNpZGVyYW5kbyBsYSBmb3JtYSwgZXNjYWxhIHkgY29ycmVsYWNpw7NuIGRlbCBjb25qdW50byBkZSBkYXRvcy4gRXMgdW4gbcOpdG9kbyBjbMOhc2ljbywgZXN0YWTDrXN0aWNhbWVudGUgc8OzbGlkbyB5IGFtcGxpYW1lbnRlIHV0aWxpemFkbyBlbiDDoXJlYXMgY29tbyBsYSBiaW9lc3RhZMOtc3RpY2EsIGxhIGRldGVjY2nDs24gZGUgZnJhdWRlcywgbGEgY2FsaWRhZCBpbmR1c3RyaWFsIHkgbGEgbWluZXLDrWEgZGUgZGF0b3MuCgpgYGB7cn0KCgojIFVzYW1vcyB1biBzdWJjb25qdW50byBtdWx0aXZhcmlhZG8gZGVsIGRhdGFzZXQgbXRjYXJzCmRhdGEgPC0gbXRjYXJzWywgYygibXBnIiwgImRpc3AiLCAiaHAiKV0KCiMgQ2FsY3VsYXIgbWVkaWEgeSBtYXRyaXogZGUgY292YXJpYW56YQptdSA8LSBjb2xNZWFucyhkYXRhKQpTIDwtIGNvdihkYXRhKQoKIyBDYWxjdWxhciBkaXN0YW5jaWEgZGUgTWFoYWxhbm9iaXMgcGFyYSBjYWRhIG9ic2VydmFjacOzbgptYWhhbF9kaXN0IDwtIG1haGFsYW5vYmlzKGRhdGEsIGNlbnRlciA9IG11LCBjb3YgPSBTKQoKIyBDYWxjdWxhciBkaXN0YW5jaWEgZXVjbGlkaWFuYSBkZXNkZSBsYSBtZWRpYQpldWNsX2Rpc3QgPC0gYXBwbHkoZGF0YSwgMSwgZnVuY3Rpb24oeCkgc3FydChzdW0oKHggLSBtdSleMikpKQoKIyBBZ3JlZ2FyIHJlc3VsdGFkb3MgYWwgZGF0YWZyYW1lCmRhdGFfcGxvdCA8LSBkYXRhICU+JQogIG11dGF0ZSgKICAgIENhciA9IHJvd25hbWVzKC4pLAogICAgTWFoYWxhbm9iaXMgPSBtYWhhbF9kaXN0LAogICAgRXVjbGlkZWFuID0gZXVjbF9kaXN0CiAgKQoKIyBEZXRlcm1pbmFyIHVtYnJhbCBkZSBjb3J0ZSBwYXJhIG91dGxpZXJzIChDaGktY3VhZHJhZG8gY29uIDMgdmFyaWFibGVzIHkgOTUlIGRlIGNvbmZpYW56YSkKcCA8LSBuY29sKGRhdGEpCmFscGhhIDwtIDAuMDUKdGhyZXNob2xkIDwtIHFjaGlzcSgxIC0gYWxwaGEsIGRmID0gcCkKCmRhdGFfcGxvdCA8LSBkYXRhX3Bsb3QgJT4lCiAgbXV0YXRlKE91dGxpZXIgPSBNYWhhbGFub2JpcyA+IHRocmVzaG9sZCkKCiMgSW1wcmltaXIgbG9zIG91dGxpZXJzCmNhdCgiT2JzZXJ2YWNpb25lcyBjb25zaWRlcmFkYXMgb3V0bGllcnMgKE1haGFsYW5vYmlzID4gIiwgcm91bmQodGhyZXNob2xkLCAyKSwgIik6XG4iKQpwcmludChkYXRhX3Bsb3QgJT4lIGZpbHRlcihPdXRsaWVyKSAlPiUgc2VsZWN0KENhciwgTWFoYWxhbm9iaXMpKQoKIyBHcsOhZmljbzogY29tcGFyYWNpw7NuIGVudHJlIGRpc3RhbmNpYXMgRXVjbGlkaWFuYXMgeSBNYWhhbGFub2JpcwpnZ3Bsb3QoZGF0YV9wbG90LCBhZXMoeCA9IEV1Y2xpZGVhbiwgeSA9IE1haGFsYW5vYmlzLCBsYWJlbCA9IENhciwgY29sb3IgPSBPdXRsaWVyKSkgKwogIGdlb21fcG9pbnQoc2l6ZSA9IDMpICsKICBnZW9tX3RleHQoaGp1c3QgPSAxLjIsIHZqdXN0ID0gMC41LCBzaXplID0gMy41LCBzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gdGhyZXNob2xkLCBsaW5ldHlwZSA9ICJkYXNoZWQiLCBjb2xvciA9ICJyZWQiKSArCiAgbGFicygKICAgIHRpdGxlID0gIkNvbXBhcmFjacOzbiBlbnRyZSBkaXN0YW5jaWEgRXVjbGlkaWFuYSB5IE1haGFsYW5vYmlzIiwKICAgIHN1YnRpdGxlID0gcGFzdGUoIkzDrW5lYSByb2phID0gVW1icmFsIENoaS1jdWFkcmFkbyAoIiwgcm91bmQodGhyZXNob2xkLCAyKSwgIikiKSwKICAgIHggPSAiRGlzdGFuY2lhIEV1Y2xpZGlhbmEgYSBsYSBtZWRpYSIsCiAgICB5ID0gIkRpc3RhbmNpYSBNYWhhbGFub2JpcyIKICApICsKICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygiYmxhY2siLCAicmVkIikpICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKRXN0ZSBncsOhZmljbyBjb21wYXJhIGxhIGRpc3RhbmNpYSBFdWNsaWRpYW5hIChlamUgWCkgeSBsYSBkaXN0YW5jaWEgZGUgTWFoYWxhbm9iaXMgKGVqZSBZKSBkZXNkZSBsYSBtZWRpYSBtdWx0aXZhcmlhZGEgcGFyYSBjYWRhIGF1dG9tw7N2aWwgZW4gZWwgY29uanVudG8gZGUgZGF0b3MgbXRjYXJzLCB1dGlsaXphbmRvIGxhcyB2YXJpYWJsZXMgbXBnLCBkaXNwIHkgaHAuIExhIGzDrW5lYSByb2phIGRpc2NvbnRpbnVhIHJlcHJlc2VudGEgZWwgdW1icmFsIGRlIGNvcnRlIGNvcnJlc3BvbmRpZW50ZSBhbCBwZXJjZW50aWwgOTUlIGRlIHVuYSBkaXN0cmlidWNpw7NuIGNoaS1jdWFkcmFkbyBjb24gMyBncmFkb3MgZGUgbGliZXJ0YWQgKCRcY2hpXjJfezMsIDAuOTV9IFxhcHByb3ggNy44MSQpLCB1c2FkbyBwYXJhIGlkZW50aWZpY2FyIG91dGxpZXJzIG11bHRpdmFyaWFkb3MuIENhZGEgcHVudG8gcmVwcmVzZW50YSB1biB2ZWjDrWN1bG8sIHkgc3UgY29sb3IgaW5kaWNhIHNpIGZ1ZSBjbGFzaWZpY2FkbyBjb21vIGF0w61waWNvOiBuZWdybyBwYXJhIG9ic2VydmFjaW9uZXMgbm9ybWFsZXMgeSByb2pvIHBhcmEgb3V0bGllcnMuIEVuIGVzdGUgY2FzbywgZWwgTWFzZXJhdGkgQm9yYSB0aWVuZSB1bmEgYWx0YSBkaXN0YW5jaWEgZGUgTWFoYWxhbm9iaXMgKG1heW9yIGEgMTUpLCBsbyBxdWUgbG8gY29udmllcnRlIGVuIHVuIGNsYXJvIGF0w61waWNvLCBhdW5xdWUgb3Ryb3MgYXV0b3MgY29tbyBlbCBDYWRpbGxhYyBGbGVldHdvb2QgbyBlbCBMaW5jb2xuIENvbnRpbmVudGFsIGVzdMOhbiBtw6FzIGFsZWphZG9zIGVuIGRpc3RhbmNpYSBldWNsaWRpYW5hLCBwZXJvIGRlbnRybyBkZWwgcmFuZ28gYWNlcHRhYmxlIGFsIGNvbnNpZGVyYXIgbGFzIGNvcnJlbGFjaW9uZXMgZW50cmUgdmFyaWFibGVzLiBFc3RvIGlsdXN0cmEgY8OzbW8gbGEgZGlzdGFuY2lhIGRlIE1haGFsYW5vYmlzIGVzIG3DoXMgYWRlY3VhZGEgcGFyYSBkZXRlY3RhciB2YWxvcmVzIGV4dHJlbW9zIGVuIGRhdG9zIGNvcnJlbGFjaW9uYWRvcy4KCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CgoKIyBTaW11bGFyIGRhdG9zIGNvcnJlbGFjaW9uYWRvcyBlbiAyRApzZXQuc2VlZCgxMjMpCm11IDwtIGMoMCwgMCkKU2lnbWEgPC0gbWF0cml4KGMoNCwgMywgMywgNCksIG5yb3cgPSAyKSAgIyBtYXRyaXogY29uIGNvcnJlbGFjacOzbgpkYXRhIDwtIGFzLmRhdGEuZnJhbWUobXZybm9ybSgyMDAsIG11ID0gbXUsIFNpZ21hID0gU2lnbWEpKQpjb2xuYW1lcyhkYXRhKSA8LSBjKCJYMSIsICJYMiIpCgojIENlbnRybyAobWVkaWEpCmNlbnRlciA8LSBjb2xNZWFucyhkYXRhKQoKIyBQdW50byBkZSByZWZlcmVuY2lhIGFsZWphZG8gZGVsIGNlbnRybwpwdW50byA8LSBjKDQsIDQpCgojIEZ1bmNpw7NuIGRlIGRpc3RhbmNpYSBFdWNsaWRpYW5hCmV1Y2xpZGVhbl9kaXN0YW5jZSA8LSBmdW5jdGlvbih4LCBjZW50ZXIpIHsKICBzcXJ0KHN1bSgoeCAtIGNlbnRlcileMikpCn0KCiMgRnVuY2nDs24gZGUgZGlzdGFuY2lhIE1haGFsYW5vYmlzCm1haGFsX2Rpc3RhbmNlIDwtIGZ1bmN0aW9uKHgsIGNlbnRlciwgY292X21hdHJpeCkgewogIHNxcnQodCh4IC0gY2VudGVyKSAlKiUgc29sdmUoY292X21hdHJpeCkgJSolICh4IC0gY2VudGVyKSkKfQoKIyBDYWxjdWxhciBhbWJhcyBkaXN0YW5jaWFzCmRfZXVjIDwtIGV1Y2xpZGVhbl9kaXN0YW5jZShwdW50bywgY2VudGVyKQpkX21haCA8LSBtYWhhbF9kaXN0YW5jZShwdW50bywgY2VudGVyLCBjb3YoZGF0YSkpCgojIENyZWFyIGVsIGdyw6FmaWNvCmdncGxvdChkYXRhLCBhZXMoeCA9IFgxLCB5ID0gWDIpKSArCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuNSkgKwogIGdlb21fcG9pbnQoYWVzKHggPSBwdW50b1sxXSwgeSA9IHB1bnRvWzJdKSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDQpICsKICBnZW9tX3BvaW50KGFlcyh4ID0gY2VudGVyWzFdLCB5ID0gY2VudGVyWzJdKSwgY29sb3IgPSAiYmx1ZSIsIHNpemUgPSA0KSArCiAgZ2VvbV9zZWdtZW50KGFlcyh4ID0gY2VudGVyWzFdLCB5ID0gY2VudGVyWzJdLCB4ZW5kID0gcHVudG9bMV0sIHllbmQgPSBwdW50b1syXSksCiAgICAgICAgICAgICAgIGFycm93ID0gYXJyb3cobGVuZ3RoID0gdW5pdCgwLjMsICJjbSIpKSwgY29sb3IgPSAiYmxhY2siLCBsaW5ldHlwZSA9ICJkb3R0ZWQiKSArCiAgc3RhdF9lbGxpcHNlKGxldmVsID0gMC41LCBjb2xvciA9ICJkYXJrZ3JlZW4iLCBsaW5ldHlwZSA9ICJkYXNoZWQiLCBzaXplID0gMSkgKyAgIyBNYWhhbGFub2JpcwogIGFubm90YXRlKCJ0ZXh0IiwgeCA9IHB1bnRvWzFdLCB5ID0gcHVudG9bMl0gKyAxLAogICAgICAgICAgIGxhYmVsID0gcGFzdGUwKCJEaXN0LiBFdWNsaWRpYW5hOiAiLCByb3VuZChkX2V1YywgMiksICJcbkRpc3QuIE1haGFsYW5vYmlzOiAiLCByb3VuZChkX21haCwgMikpLAogICAgICAgICAgIGNvbG9yID0gInJlZCIsIHNpemUgPSA0KSArCiAgbGFicygKICAgIHRpdGxlID0gIkNvbXBhcmFjacOzbiBlbnRyZSBkaXN0YW5jaWEgRXVjbGlkaWFuYSB5IE1haGFsYW5vYmlzIiwKICAgIHN1YnRpdGxlID0gIkVsaXBzZSA9IG1pc21hIGRpc3RhbmNpYSBNYWhhbGFub2JpczsgbMOtbmVhIHJlY3RhID0gZGlzdGFuY2lhIEV1Y2xpZGlhbmEiLAogICAgeCA9ICJWYXJpYWJsZSBYMSIsCiAgICB5ID0gIlZhcmlhYmxlIFgyIgogICkgKwogIHRoZW1lX21pbmltYWwoKQoKCmBgYAoKRXN0ZSBncsOhZmljbyBpbHVzdHJhIHZpc3VhbG1lbnRlIGxhIGRpZmVyZW5jaWEgY29uY2VwdHVhbCBlbnRyZSBsYSBkaXN0YW5jaWEgRXVjbGlkaWFuYSB5IGxhIGRpc3RhbmNpYSBkZSBNYWhhbGFub2JpcyBlbiB1biBjb25qdW50byBkZSBkYXRvcyBiaWRpbWVuc2lvbmFsICh2YXJpYWJsZXMgWDEgeSBYMikuIExvcyBwdW50b3MgZ3Jpc2VzIHJlcHJlc2VudGFuIG9ic2VydmFjaW9uZXMgZ2VuZXJhZGFzIGNvbiB1bmEgZGlzdHJpYnVjacOzbiBlbMOtcHRpY2EgeSBjb3JyZWxhY2nDs24gcG9zaXRpdmEgZW50cmUgdmFyaWFibGVzLiBFbCBwdW50byBhenVsIG1hcmNhIGVsIGNlbnRybyBkZSBsb3MgZGF0b3MgKGxhIG1lZGlhIG11bHRpdmFyaWFkYSksIG1pZW50cmFzIHF1ZSBlbCBwdW50byByb2pvIGVzIHVuYSBvYnNlcnZhY2nDs24gZXNwZWPDrWZpY2EgY3V5YSBkaXN0YW5jaWEgYWwgY2VudHJvIHNlIGVzdMOhIG1pZGllbmRvLgoKTGEgbMOtbmVhIHB1bnRlYWRhIG5lZ3JhIG11ZXN0cmEgbGEgZGlzdGFuY2lhIEV1Y2xpZGlhbmEsIHF1ZSBlcyBsYSBsw61uZWEgcmVjdGEgZW50cmUgZWwgY2VudHJvIHkgZWwgcHVudG8gcm9qbywgZSBpZ25vcmEgY29tcGxldGFtZW50ZSBsYSBlc3RydWN0dXJhIGRlIHZhcmlhbnphIHkgY29ycmVsYWNpw7NuIGRlIGxvcyBkYXRvcy4gRW4gY2FtYmlvLCBsYSBlbGlwc2UgdmVyZGUgcmVwcmVzZW50YSB1bmEgbMOtbmVhIGRlIGlndWFsIGRpc3RhbmNpYSBNYWhhbGFub2JpcywgcXVlIHRvbWEgZW4gY3VlbnRhIGxhIGRpc3BlcnNpw7NuIHkgZm9ybWEgZGVsIGNvbmp1bnRvLiBFbiBlc3RlIGNhc28sIGF1bnF1ZSBlbCBwdW50byByb2pvIGVzdMOhIGEgdW5hIGRpc3RhbmNpYSBldWNsaWRpYW5hIGNvbnNpZGVyYWJsZSBkZWwgY2VudHJvICgkNS42OCQpLCBzdSBkaXN0YW5jaWEgZGUgTWFoYWxhbm9iaXMgZXMgbXVjaG8gbWVub3IgKCQyLjI4JCksIHBvcnF1ZSBzZSBlbmN1ZW50cmEgYWxpbmVhZG8gY29uIGxhIGRpcmVjY2nDs24gcHJpbmNpcGFsIGRlIGRpc3BlcnNpw7NuIGRlIGxvcyBkYXRvcy4gRXN0byBkZW11ZXN0cmEgY8OzbW8gbGEgZGlzdGFuY2lhIGRlIE1haGFsYW5vYmlzICJkZWZvcm1hIiBlbCBlc3BhY2lvIHNlZ8O6biBsYSBlc3RydWN0dXJhIGludGVybmEgZGUgbG9zIGRhdG9zLCB5IHBvciBlc28gZXMgbXVjaG8gbcOhcyBhZGVjdWFkYSBwYXJhIGlkZW50aWZpY2FyIHF1w6kgdGFuIGludXN1YWwgZXMgcmVhbG1lbnRlIHVuYSBvYnNlcnZhY2nDs24gZW4gY29udGV4dG9zIG11bHRpdmFyaWFkb3MuCgojIyAzLjUpIERpc3RyaWJ1Y2nDs24gdC1zdHVkZW50IG11bHRpdmFyaWFudGUKCkxhIGRpc3RyaWJ1Y2nDs24gdCBkZSBTdHVkZW50IG11bHRpdmFyaWFudGUgZXMgdW5hIGV4dGVuc2nDs24gZGUgbGEgZGlzdHJpYnVjacOzbiB0IHVuaXZhcmlhZGEgYWwgY2FzbyBlbiBlbCBxdWUgc2UgdHJhYmFqYSBjb24gdmVjdG9yZXMgYWxlYXRvcmlvcyBlbiBsdWdhciBkZSB2YXJpYWJsZXMgaW5kaXZpZHVhbGVzLiBFc3RhIGRpc3RyaWJ1Y2nDs24gcGVybWl0ZSBtb2RlbGFyIGRhdG9zIG11bHRpdmFyaWFkb3MgY29uIGNvbGFzIG3DoXMgcGVzYWRhcyBxdWUgbGFzIGRlIGxhIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIG11bHRpdmFyaWFkYSwgbG8gcXVlIGxhIGhhY2UgbcOhcyBhZGVjdWFkYSBwYXJhIHNpdHVhY2lvbmVzIGRvbmRlIHB1ZWRlbiBhcGFyZWNlciB2YWxvcmVzIGV4dHJlbW9zIChvdXRsaWVycykgbyBkb25kZSBsYSBub3JtYWxpZGFkIGVzdHJpY3RhIG5vIGVzIHVuYSBidWVuYSBzdXBvc2ljacOzbi4gRXMgYW1wbGlhbWVudGUgdXRpbGl6YWRhIGVuIGVzdGFkw61zdGljYSByb2J1c3RhLCBtb2RlbG9zIGJheWVzaWFub3MsIGVjb25vbWV0csOtYSB5IGFuw6FsaXNpcyBkZSByaWVzZ29zIGZpbmFuY2llcm9zLgoKRm9ybWFsbWVudGUsIGRlY2ltb3MgcXVlIHVuIHZlY3RvciBhbGVhdG9yaW8gJFxtYXRoYmZ7WH0gXGluIFxtYXRoYmJ7Un1ecCQgdGllbmUgdW5hIGRpc3RyaWJ1Y2nDs24gdCBkZSBTdHVkZW50IG11bHRpdmFyaWFudGUgY29uICRcbnUkIGdyYWRvcyBkZSBsaWJlcnRhZCwgbWVkaWEgJFxib2xkc3ltYm9se1xtdX0kIHkgbWF0cml6IGRlIGVzY2FsYSAkXGJvbGRzeW1ib2x7XFNpZ21hfSQgc2k6CgpcJCBcbWF0aGJme1h9IFxzaW0gdFxfXG51KFxib2xkc3ltYm9se1xtdX0sIFxib2xkc3ltYm9se1xTaWdtYX0pIFwkCgpMYSBmdW5jacOzbiBkZSBkZW5zaWRhZCBkZSBwcm9iYWJpbGlkYWQgZGUgZXN0YSBkaXN0cmlidWNpw7NuIGVzdMOhIGRhZGEgcG9yOgoKXCQgZihcbWF0aGJme3h9KSA9IFxmcmFje1xHYW1tYVxsZWZ0KCBcZnJhY3tcbnUgKyBwfXsyfSBccmlnaHQpfXtcR2FtbWFcbGVmdCggXGZyYWN7XG51fXsyfSBccmlnaHQpIFxudV57cC8yfSBccGlee3AvMn0gfFxib2xkc3ltYm9se1xTaWdtYX18XnsxLzJ9fSBcbGVmdFsgMSArIFxmcmFjezF9e1xudX0gKFxtYXRoYmZ7eH0gLSBcYm9sZHN5bWJvbHtcbXV9KV5cdG9wIFxib2xkc3ltYm9se1xTaWdtYX1eey0xfSAoXG1hdGhiZnt4fSAtIFxib2xkc3ltYm9se1xtdX0pIFxyaWdodF1cXnstXGZyYWN7XG51ICsgcH17Mn19IFwkCgpFbiBlc3RhIGbDs3JtdWxhLCAkXEdhbW1hKFxjZG90KSQgZXMgbGEgZnVuY2nDs24gZ2FtbWEsICRwJCBlcyBsYSBkaW1lbnNpw7NuIGRlbCB2ZWN0b3IgJFxtYXRoYmZ7eH0kLCB5ICR8XGJvbGRzeW1ib2x7XFNpZ21hfXwkIHJlcHJlc2VudGEgZWwgZGV0ZXJtaW5hbnRlIGRlIGxhIG1hdHJpeiBkZSBlc2NhbGEuIENvbW8gc2UgcHVlZGUgdmVyLCBsYSBmb3JtYSBkZSBsYSBkZW5zaWRhZCBlcyBtdXkgc2ltaWxhciBhIGxhIGRlIGxhIG5vcm1hbCBtdWx0aXZhcmlhbnRlLCBwZXJvIGluY2x1eWUgdW4gZmFjdG9yIGFkaWNpb25hbCBxdWUgaGFjZSBxdWUgbGEgZGVuc2lkYWQgZGVjcmV6Y2EgbcOhcyBsZW50YW1lbnRlIGNvbmZvcm1lICRcbWF0aGJme3h9JCBzZSBhbGVqYSBkZSBsYSBtZWRpYSAkXGJvbGRzeW1ib2x7XG11fSQuIFBvciBlc28sIGVzdGEgZGlzdHJpYnVjacOzbiB0aWVuZSBjb2xhcyBtw6FzIHBlc2FkYXMsIGxvIGN1YWwgcGVybWl0ZSB1bmEgbWF5b3IgcHJvYmFiaWxpZGFkIGRlIHZhbG9yZXMgZXh0cmVtb3MuCgpVbmEgZm9ybWEgw7p0aWwgZGUgZW50ZW5kZXIgZXN0YSBkaXN0cmlidWNpw7NuIGVzIHN1IHJlbGFjacOzbiBjb24gbGEgbm9ybWFsIG11bHRpdmFyaWFudGUuIFNpICRcbWF0aGJme1p9IFxzaW0gXG1hdGhjYWx7Tn1fcChcbWF0aGJmezB9LCBcYm9sZHN5bWJvbHtcU2lnbWF9KSQgeSAkVyBcc2ltIFxjaGleMihcbnUpJCBkZSBmb3JtYSBpbmRlcGVuZGllbnRlLCBlbnRvbmNlczoKClwkIFxtYXRoYmZ7WH0gPSBcYm9sZHN5bWJvbHtcbXV9ICsgXGZyYWN7XG1hdGhiZntafX17XHNxcnR7Vy9cbnV9fSBcJAoKc2lndWUgdW5hIGRpc3RyaWJ1Y2nDs24gJHRfXG51KFxib2xkc3ltYm9se1xtdX0sIFxib2xkc3ltYm9se1xTaWdtYX0pJC4gRXN0byBtdWVzdHJhIHF1ZSBwdWVkZSB2ZXJzZSBjb21vIHVuYSBub3JtYWwgbXVsdGl2YXJpYW50ZSBlc2NhbGFkYSBhbGVhdG9yaWFtZW50ZSBwb3IgdW5hIHZhcmlhYmxlIGNoaS1jdWFkcmFkbywgbG8gcXVlIGludHJvZHVjZSBtYXlvciB2YXJpYWJpbGlkYWQuCgpBZGVtw6FzLCBzaSBsb3MgZ3JhZG9zIGRlIGxpYmVydGFkICRcbnUkIHRpZW5kZW4gYSBpbmZpbml0bywgZXMgZGVjaXIsICRcbnUgXHRvIFxpbmZ0eSQsIGVudG9uY2VzIGxhIGRpc3RyaWJ1Y2nDs24gdCBtdWx0aXZhcmlhbnRlIGNvbnZlcmdlIGEgbGEgbm9ybWFsIG11bHRpdmFyaWFudGUsIGxvIGN1YWwgcmVmdWVyemEgbGEgaWRlYSBkZSBxdWUgZXMgdW5hIHZlcnNpw7NuIG3DoXMgZmxleGlibGUgeSByb2J1c3RhIGRlIGxhIG1pc21hLgoKRW4gcmVzdW1lbiwgbGEgZGlzdHJpYnVjacOzbiB0IGRlIFN0dWRlbnQgbXVsdGl2YXJpYW50ZSBlcyBtdXkgw7p0aWwgY3VhbmRvIHNlIGRlc2VhIG1vZGVsYXIgZGF0b3MgbXVsdGl2YXJpYWRvcyBlbiBsb3MgcXVlIGhheSBpbmNlcnRpZHVtYnJlIHNvYnJlIGxhIHZhcmlhbnphLCBjb2xhcyBwZXNhZGFzIG8gcmllc2dvIGRlIG9ic2VydmFjaW9uZXMgZXh0cmVtYXMuIEFsIGlndWFsIHF1ZSBsYSBub3JtYWwgbXVsdGl2YXJpYW50ZSwgaW5jb3Jwb3JhIHVuYSBtYXRyaXogZGUgZXNjYWxhIHF1ZSBkZXNjcmliZSBsYSBmb3JtYSB5IGxhIGNvcnJlbGFjacOzbiBkZSBsYXMgdmFyaWFibGVzLCBwZXJvIHN1IGNvbXBvcnRhbWllbnRvIGVzIG3DoXMgY29uc2VydmFkb3IgeSByZWFsaXN0YSBlbiBwcmVzZW5jaWEgZGUgZGF0b3MgYW7Ds21hbG9zLgoKYGBge3J9CgojIENyZWFtb3Mgc2VtaWxsYXMKc2V0LnNlZWQoMTIzKQoKIyBQYXLDoW1ldHJvcwptdSA8LSBjKDAsIDApClNpZ21hIDwtIG1hdHJpeChjKDEsIDAuNywgMC43LCAxKSwgMiwgMikKbiA8LSAxMDAwCmRmIDwtIDMgICMgR3JhZG9zIGRlIGxpYmVydGFkIHBhcmEgbGEgdCBtdWx0aXZhcmlhbnRlCgojIFNpbXVsYWNpw7NuCm5vcm1fZGF0YSA8LSBtdnJub3JtKG4sIG11LCBTaWdtYSkKdF9kYXRhIDwtIHJtdnQobiwgc2lnbWEgPSBTaWdtYSwgZGYgPSBkZiwgZGVsdGEgPSBtdSkKCiMgQ29udmVydGlyIGEgZGF0YSBmcmFtZQpub3JtX2RmIDwtIGFzLmRhdGEuZnJhbWUobm9ybV9kYXRhKQp0X2RmIDwtIGFzLmRhdGEuZnJhbWUodF9kYXRhKQpub3JtX2RmJHRpcG8gPC0gIk5vcm1hbCIKdF9kZiR0aXBvIDwtICJ0LVN0dWRlbnQiCgpuYW1lcyhub3JtX2RmKVsxOjJdIDwtIG5hbWVzKHRfZGYpWzE6Ml0gPC0gYygiWDEiLCAiWDIiKQpkYXRhIDwtIHJiaW5kKG5vcm1fZGYsIHRfZGYpCgojIEdyYWZpY2FyCmdncGxvdChkYXRhLCBhZXMoeCA9IFgxLCB5ID0gWDIsIGNvbG9yID0gdGlwbykpICsKICBnZW9tX3BvaW50KGFscGhhID0gMC41KSArCiAgc3RhdF9lbGxpcHNlKGxldmVsID0gMC45NSwgbGluZXR5cGUgPSAiZGFzaGVkIikgKwogIHRoZW1lX21pbmltYWwoYmFzZV9zaXplID0gMTQpICsKICBsYWJzKAogICAgdGl0bGUgPSAiQ29tcGFyYWNpw7NuIGVudHJlIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIHkgdC1TdHVkZW50IG11bHRpdmFyaWFudGVzIiwKICAgIHN1YnRpdGxlID0gcGFzdGUwKCJBbWJhcyBjb24gbWlzbWEgbWVkaWEgeSBjb3ZhcmlhbnphOyB0LVN0dWRlbnQgY29uIGRmID0gIiwgZGYpLAogICAgY29sb3IgPSAiRGlzdHJpYnVjacOzbiIsCiAgICB4ID0gIlgxIiwgeSA9ICJYMiIKICApCgpgYGAKCkxhcyBkaXN0cmlidWNpb25lcyBlc2bDqXJpY2FzIHkgZWzDrXB0aWNhcyBzb24gY2xhc2VzIGdlbmVyYWxlcyBkZSBkaXN0cmlidWNpb25lcyBtdWx0aXZhcmlhZGFzIHF1ZSBleHRpZW5kZW4gbGEgaWRlYSBkZSBzaW1ldHLDrWEgZW4gZWwgZXNwYWNpby4gU2UgY2FyYWN0ZXJpemFuIHBvciB0ZW5lciBuaXZlbGVzIGRlIGRlbnNpZGFkIGNvbnN0YW50ZXMgZW4gZm9ybWFzIGdlb23DqXRyaWNhcyBlc3BlY8OtZmljYXM6IGVzZmVyYXMgeSBlbGlwc2VzLCByZXNwZWN0aXZhbWVudGUuCgpVbmEgZGlzdHJpYnVjacOzbiBlc2bDqXJpY2EgZXMgYXF1ZWxsYSBjdXlhIGZ1bmNpw7NuIGRlIGRlbnNpZGFkIGRlcGVuZGUgw7puaWNhbWVudGUgZGUgbGEgZGlzdGFuY2lhIGV1Y2xpZGlhbmEgYWwgY2VudHJvIGRlIGxhIGRpc3RyaWJ1Y2nDs24uIEVzIGRlY2lyLCBsYSBwcm9iYWJpbGlkYWQgZGUgb2JzZXJ2YXIgdW4gcHVudG8gZGVwZW5kZSBzb2xhbWVudGUgZGUgcXXDqSB0YW4gbGVqb3MgZXN0w6EgZGVsIGNlbnRybywgbm8gZGUgbGEgZGlyZWNjacOzbi4gRm9ybWFsbWVudGUsIHNpICRcbWF0aGJme3h9IFxpbiBcbWF0aGJie1J9XnAkIGVzIHVuIHZlY3RvciBhbGVhdG9yaW8sIGRlY2ltb3MgcXVlIHRpZW5lIHVuYSBkaXN0cmlidWNpw7NuIGVzZsOpcmljYSBzaSBzdSBkZW5zaWRhZCAoY3VhbmRvIGV4aXN0ZSkgcHVlZGUgZXNjcmliaXJzZSBjb21vOgoKJCQKZihcbWF0aGJme3h9KSA9IGdcbGVmdCggfFxtYXRoYmZ7eH0gLSBcYm9sZHN5bWJvbHtcbXV9IHxeMiBccmlnaHQpCiQkCgpkb25kZSAkXGJvbGRzeW1ib2x7XG11fSQgZXMgZWwgdmVjdG9yIGRlIG1lZGlhIChjZW50cm8gZGUgc2ltZXRyw61hKSwgJHwgXGNkb3QgfCQgZGVub3RhIGxhIG5vcm1hIGV1Y2xpZGlhbmEsIHkgJGcoXGNkb3QpJCBlcyB1bmEgZnVuY2nDs24gZXNjYWxhciBxdWUgZGV0ZXJtaW5hIGxhIGZvcm1hIGRlIGxhIGRpc3RyaWJ1Y2nDs24uIFVuIGVqZW1wbG8gY2zDoXNpY28gZGUgZGlzdHJpYnVjacOzbiBlc2bDqXJpY2EgZXMgbGEgbm9ybWFsIG11bHRpdmFyaWFkYSBlc3TDoW5kYXIsIGNvbiBtZWRpYSBjZXJvIHkgbWF0cml6IGRlIGNvdmFyaWFuemEgaWd1YWwgYSBsYSBpZGVudGlkYWQuCgpQb3Igb3RybyBsYWRvLCBsYXMgZGlzdHJpYnVjaW9uZXMgZWzDrXB0aWNhcyBnZW5lcmFsaXphbiBsYXMgZXNmw6lyaWNhcyBwZXJtaXRpZW5kbyBkaWZlcmVudGVzIGVzY2FsYXMgeSBjb3JyZWxhY2lvbmVzIGVudHJlIGxhcyB2YXJpYWJsZXMuIEVuIGx1Z2FyIGRlIHRlbmVyIGlzb2RlbnNpZGFkZXMgZW4gZm9ybWEgZGUgZXNmZXJhcywgbGFzIHRpZW5lbiBlbiBmb3JtYSBkZSBlbGlwc2VzIChvIGVsaXBzb2lkZXMgZW4gZGltZW5zaW9uZXMgbWF5b3JlcykuIEVzdG8gbGFzIGhhY2UgbXVjaG8gbcOhcyBmbGV4aWJsZXMgcGFyYSBtb2RlbGFyIGRhdG9zIHJlYWxlcy4gTGEgZm9ybWEgZ2VuZXJhbCBkZSBsYSBkZW5zaWRhZCBkZSB1bmEgZGlzdHJpYnVjacOzbiBlbMOtcHRpY2EgZXM6CgokJApmKFxtYXRoYmZ7eH0pID0gfFxib2xkc3ltYm9se1xTaWdtYX18XnstMS8yfSBcY2RvdCBoXGxlZnQoKFxtYXRoYmZ7eH0gLSBcYm9sZHN5bWJvbHtcbXV9KV5cdG9wIFxib2xkc3ltYm9se1xTaWdtYX1eey0xfSAoXG1hdGhiZnt4fSAtIFxib2xkc3ltYm9se1xtdX0pXHJpZ2h0KQokJAoKZG9uZGUgJFxib2xkc3ltYm9se1xtdX0kIGVzIGVsIHZlY3RvciBkZSBtZWRpYSwgJFxib2xkc3ltYm9se1xTaWdtYX0kIGVzIGxhIG1hdHJpeiBkZSBkaXNwZXJzacOzbiAocHJvcG9yY2lvbmFsIGEgbGEgY292YXJpYW56YSksIHkgJGgoXGNkb3QpJCBlcyB1bmEgZnVuY2nDs24gZ2VuZXJhZG9yYSBlc2NhbGFyLiBFc3RlIHRpcG8gZGUgZXN0cnVjdHVyYSBpbmNsdXllIGNvbW8gY2Fzb3MgcGFydGljdWxhcmVzIGEgbXVjaGFzIGRpc3RyaWJ1Y2lvbmVzIG11bHRpdmFyaWFkYXMgY29ub2NpZGFzLCBjb21vOgoKTGEgbm9ybWFsIG11bHRpdmFyaWFkYSAoY3VhbmRvICRoKHopID0gXGV4cCgtei8yKSQpLCBMYSB0IGRlIFN0dWRlbnQgbXVsdGl2YXJpYW50ZSAoY3VhbmRvICRoKHopJCBkZWNhZSBtw6FzIGxlbnRhbWVudGUpLCBEaXN0cmlidWNpb25lcyByb2J1c3RhcyBjb21vIGxhIGRlIHRpcG8gTGFwbGFjZSBtdWx0aXZhcmlhZGEgbyBDYXVjaHkuIFVuYSBjYXJhY3RlcsOtc3RpY2EgY2xhdmUgZGUgbGFzIGRpc3RyaWJ1Y2lvbmVzIGVsw61wdGljYXMgZXMgcXVlIHBlcm1pdGVuIGNvbnNlcnZhciBsYSBpZGVhIGRlICJkaXJlY2Npw7NuIiB5ICJmb3JtYSIgZGUgbGEgZGlzcGVyc2nDs24gZGUgbG9zIGRhdG9zLCBwZXJvIGFqdXN0YW5kbyBtZWpvciBsYXMgY29sYXMgcGVzYWRhcyBvIGxhcyBmb3JtYXMgbm8gbm9ybWFsZXMuIEVzdG8gbGFzIGhhY2UgbXV5IMO6dGlsZXMgZW4gYW7DoWxpc2lzIHJvYnVzdG8sIGRldGVjY2nDs24gZGUgYXTDrXBpY29zLCB5IG1vZGVsb3MgZmluYW5jaWVyb3MgbXVsdGl2YXJpYWRvcy4KCkVuIHJlc3VtZW4sIGxhcyBkaXN0cmlidWNpb25lcyBlc2bDqXJpY2FzIHNvbiB1biBjYXNvIHBhcnRpY3VsYXIgZGVudHJvIGRlIGxhcyBlbMOtcHRpY2FzLCB5IGFtYmFzIG9mcmVjZW4gaGVycmFtaWVudGFzIHBvZGVyb3NhcyBwYXJhIG1vZGVsYXIgZGF0b3MgbXVsdGlkaW1lbnNpb25hbGVzIGNvbiBlc3RydWN0dXJhcyBkZSBzaW1ldHLDrWEgeSBjb3JyZWxhY2nDs24uCgpgYGB7cn0KCiMgQ3JlYXIgZGF0b3MgcGFyYSBlbCBjw61yY3VsbyAoZXNmw6lyaWNhKSB5IGxhIGVsaXBzZSAoZWzDrXB0aWNhKQp0aGV0YSA8LSBzZXEoMCwgMipwaSwgbGVuZ3RoLm91dCA9IDIwMCkKCiMgQ8OtcmN1bG86IHJhZGlvIDEgKGRpc3RyaWJ1Y2nDs24gZXNmw6lyaWNhKQpjaXJjbGUgPC0gZGF0YS5mcmFtZSgKICB4ID0gY29zKHRoZXRhKSwKICB5ID0gc2luKHRoZXRhKSwKICB0aXBvID0gIkRpc3RyaWJ1Y2nDs24gZXNmw6lyaWNhIgopCgojIEVsaXBzZTogc2VtaWVqZXMgYSA9IDIsIGIgPSAxIChkaXN0cmlidWNpw7NuIGVsw61wdGljYSkKZWxsaXBzZSA8LSBkYXRhLmZyYW1lKAogIHggPSAyICogY29zKHRoZXRhKSwKICB5ID0gMSAqIHNpbih0aGV0YSksCiAgdGlwbyA9ICJEaXN0cmlidWNpw7NuIGVsw61wdGljYSIKKQoKIyBVbmlyIGFtYm9zIGRhdGEgZnJhbWVzCmRmIDwtIHJiaW5kKGNpcmNsZSwgZWxsaXBzZSkKCiMgR3JhZmljYXIKZ2dwbG90KGRmLCBhZXMoeCA9IHgsIHkgPSB5LCBjb2xvciA9IHRpcG8pKSArCiAgZ2VvbV9wYXRoKHNpemUgPSAxKSArCiAgY29vcmRfZXF1YWwoKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICBsYWJzKAogICAgdGl0bGUgPSAiRGlzdHJpYnVjaW9uZXMgRXNmw6lyaWNhIHZzIEVsw61wdGljYSIsCiAgICB4ID0gIkVqZSBYIiwgeSA9ICJFamUgWSIsCiAgICBjb2xvciA9ICJUaXBvIGRlIGRpc3RyaWJ1Y2nDs24iCiAgKQoKYGBgCgojIyAzLjYpIE1peHR1cmEgZGUgZGlzdHJpYnVjaW9uZXMKCkxhcyBtaXh0dXJhcyBkZSBkaXN0cmlidWNpb25lcyAobyBtaXh0dXJlIG1vZGVscykgc29uIG1vZGVsb3MgZXN0YWTDrXN0aWNvcyB1dGlsaXphZG9zIHBhcmEgcmVwcmVzZW50YXIgcG9ibGFjaW9uZXMgaGV0ZXJvZ8OpbmVhcywgZW4gbGFzIGN1YWxlcyBzZSBzdXBvbmUgcXVlIGxvcyBkYXRvcyBwcm92aWVuZW4gZGUgdmFyaWFzIHN1YnBvYmxhY2lvbmVzIGRpZmVyZW50ZXMsIGNhZGEgdW5hIGNvbiBzdSBwcm9waWEgZGlzdHJpYnVjacOzbi4gRW4gbHVnYXIgZGUgbW9kZWxhciB0b2RvcyBsb3MgZGF0b3MgY29tbyBwcm92ZW5pZW50ZXMgZGUgdW5hIHNvbGEgZGlzdHJpYnVjacOzbiwgc2UgYXN1bWUgcXVlIGNhZGEgb2JzZXJ2YWNpw7NuIGVzIGdlbmVyYWRhIHBvciB1bmEgZGUgdmFyaWFzIGRpc3RyaWJ1Y2lvbmVzIGNvbiBjaWVydGEgcHJvYmFiaWxpZGFkLgoKTGEgZsOzcm11bGEgZ2VuZXJhbCBkZSB1bmEgbWV6Y2xhIGRlIGRpc3RyaWJ1Y2lvbmVzIGVzOgoKJCQKZihcbWF0aGJme3h9KSA9IFxzdW1fe2s9MX1ee0t9IFxwaV9rIGZfayhcbWF0aGJme3h9KQokJAoKZG9uZGU6CgokXG1hdGhiZnt4fSQgZXMgZWwgdmVjdG9yIGRlIGRhdG9zIGRlIGVudHJhZGEsICRmX2soXG1hdGhiZnt4fSkkIGVzIGxhIGRlbnNpZGFkIGRlIGxhICRrJC3DqXNpbWEgY29tcG9uZW50ZSBkZWwgbW9kZWxvLCAkXHBpX2skIGVzIGVsIHBlc28gbyBwcm9wb3JjacOzbiBkZSBsYSBjb21wb25lbnRlICRrJCBlbiBsYSBtZXpjbGEsIGNvbjoKCiQkCjAgPCBccGlfayA8IDEsIFxxdWFkIFxzdW1fe2s9MX1ee0t9IFxwaV9rID0gMQokJCBFc3RlIG1vZGVsbyBpbmRpY2EgcXVlIGxhIHByb2JhYmlsaWRhZCBkZSBvYnNlcnZhciAkXG1hdGhiZnt4fSQgc2Ugb2J0aWVuZSBjb21vIHVuYSBjb21iaW5hY2nDs24gcG9uZGVyYWRhIGRlIGxhcyBwcm9iYWJpbGlkYWRlcyBkZSBjYWRhIGNvbXBvbmVudGUuCgpVbmEgZGUgbGFzIGFwbGljYWNpb25lcyBtw6FzIGNvbXVuZXMgZXMgbGEgbWV6Y2xhIGRlIGRpc3RyaWJ1Y2lvbmVzIG5vcm1hbGVzIG11bHRpdmFyaWFkYXMuIEVuIGVzZSBjYXNvLCBjYWRhICRmX2soXG1hdGhiZnt4fSkkIGVzIHVuYSBkZW5zaWRhZCBub3JtYWwgbXVsdGl2YXJpYWRhIGNvbiBtZWRpYSAkXGJvbGRzeW1ib2x7XG11fV9rJCB5IG1hdHJpeiBkZSBjb3ZhcmlhbnphICRcYm9sZHN5bWJvbHtcU2lnbWF9X2skLCB5IHNlIGV4cHJlc2EgY29tbzoKCiQkCmZfayhcbWF0aGJme3h9KSA9IFxmcmFjezF9eygyXHBpKV57cC8yfSB8XGJvbGRzeW1ib2x7XFNpZ21hfV9rfF57MS8yfX0gXGV4cFxsZWZ0KCAtXGZyYWN7MX17Mn0gKFxtYXRoYmZ7eH0gLSBcYm9sZHN5bWJvbHtcbXV9X2spXlx0b3AgXGJvbGRzeW1ib2x7XFNpZ21hfV9rXnstMX0gKFxtYXRoYmZ7eH0gLSBcYm9sZHN5bWJvbHtcbXV9X2spIFxyaWdodCkKJCQKCmRvbmRlOgoKJHAkIGVzIGxhIGRpbWVuc2nDs24gZGUgbG9zIGRhdG9zLCAkXGJvbGRzeW1ib2x7XG11fV9rJCBlcyBlbCB2ZWN0b3IgZGUgbWVkaWFzIHBhcmEgbGEgY29tcG9uZW50ZSAkayQsICRcYm9sZHN5bWJvbHtcU2lnbWF9X2skIGVzIGxhIG1hdHJpeiBkZSBjb3ZhcmlhbnphIHBhcmEgbGEgY29tcG9uZW50ZSAkayQsICR8XGJvbGRzeW1ib2x7XFNpZ21hfV9rfCQgZXMgZWwgZGV0ZXJtaW5hbnRlIGRlIGxhIG1hdHJpeiBkZSBjb3ZhcmlhbnphLiBFc3RlIGVuZm9xdWUgcGVybWl0ZSBtb2RlbGFyIHBvYmxhY2lvbmVzIHF1ZSB0aWVuZW4gc3ViZ3J1cG9zIGVzdHJ1Y3R1cmFkb3MsIGxvIGN1YWwgZXMgZXNwZWNpYWxtZW50ZSDDunRpbCBlbiBhcGxpY2FjaW9uZXMgY29tbzoKClNlZ21lbnRhY2nDs24gZGUgY2xpZW50ZXMsIERldGVjY2nDs24gZGUgYW5vbWFsw61hcywgQ2xhc2lmaWNhY2nDs24gbm8gc3VwZXJ2aXNhZGEgKGNsdXN0ZXJpbmcpLCBSZWNvbm9jaW1pZW50byBkZSBwYXRyb25lcy4gTGFzIG1peHR1cmFzIGRlIGdhdXNzaWFuYXMsIHBvciBlamVtcGxvLCBzb24gbGEgYmFzZSBkZSBhbGdvcml0bW9zIGNvbW8gZWwgRU0gKEV4cGVjdGF0aW9uLU1heGltaXphdGlvbiksIHF1ZSBlc3RpbWEgaXRlcmF0aXZhbWVudGUgbG9zIHBhcsOhbWV0cm9zIGRlbCBtb2RlbG8geSBhc2lnbmEgcHJvYmFiaWxpZGFkZXMgZGUgcGVydGVuZW5jaWEgYSBjYWRhIG9ic2VydmFjacOzbi4KCmBgYHtyfQoKCiMgU2ltdWxhIGRhdG9zIGRlc2RlIHVuYSBtZXpjbGEgZGUgMyBkaXN0cmlidWNpb25lcyBub3JtYWxlcwpzZXQuc2VlZCgxMjMpCm4gPC0gNTAwCm11MSA8LSBjKDAsIDApCm11MiA8LSBjKDUsIDUpCm11MyA8LSBjKDAsIDUpCgpzaWdtYTEgPC0gbWF0cml4KGMoMSwgMC41LCAwLjUsIDEpLCBuY29sID0gMikKc2lnbWEyIDwtIG1hdHJpeChjKDEsIC0wLjMsIC0wLjMsIDEpLCBuY29sID0gMikKc2lnbWEzIDwtIG1hdHJpeChjKDAuNSwgMCwgMCwgMC41KSwgbmNvbCA9IDIpCgp4MSA8LSBNQVNTOjptdnJub3JtKG4gPSByb3VuZChuICogMC40KSwgbXUgPSBtdTEsIFNpZ21hID0gc2lnbWExKQp4MiA8LSBNQVNTOjptdnJub3JtKG4gPSByb3VuZChuICogMC4zNSksIG11ID0gbXUyLCBTaWdtYSA9IHNpZ21hMikKeDMgPC0gTUFTUzo6bXZybm9ybShuID0gcm91bmQobiAqIDAuMjUpLCBtdSA9IG11MywgU2lnbWEgPSBzaWdtYTMpCgpkYXRhIDwtIHJiaW5kKHgxLCB4MiwgeDMpCgojIEFqdXN0YSB1biBtb2RlbG8gZGUgbWV6Y2xhIGdhdXNzaWFuYQptb2RlbG8gPC0gTWNsdXN0KGRhdGEpCgojIFZpc3VhbGl6YSBsb3MgcmVzdWx0YWRvcwpwbG90KG1vZGVsbywgd2hhdCA9ICJjbGFzc2lmaWNhdGlvbiIpCgpgYGAKCiMjIDMuNykgQ8OzcHVsYXMKClVuYSBjw7NwdWxhIGVzIHVuYSBmdW5jacOzbiBtYXRlbcOhdGljYSBxdWUgcGVybWl0ZSBtb2RlbGFyIGxhIGRlcGVuZGVuY2lhIGVudHJlIHZhcmlhYmxlcyBhbGVhdG9yaWFzIGRlIGZvcm1hIHNlcGFyYWRhIGEgc3VzIGRpc3RyaWJ1Y2lvbmVzIG1hcmdpbmFsZXMuIEVuIG90cmFzIHBhbGFicmFzLCBsYXMgY8OzcHVsYXMgbm9zIHBlcm1pdGVuIGNvbnN0cnVpciBkaXN0cmlidWNpb25lcyBjb25qdW50YXMgYSBwYXJ0aXIgZGUgbGFzIGRpc3RyaWJ1Y2lvbmVzIGluZGl2aWR1YWxlcyAobWFyZ2luYWxlcyksIGNhcHR1cmFuZG8gZXhjbHVzaXZhbWVudGUgY8OzbW8gZXN0YXMgdmFyaWFibGVzIHNlIHJlbGFjaW9uYW4gZW50cmUgc8OtLiBFc3RvIGVzIG11eSDDunRpbCBlbiBjb250ZXh0b3MgZG9uZGUgbGFzIHZhcmlhYmxlcyB0aWVuZW4gZGlmZXJlbnRlcyBjb21wb3J0YW1pZW50b3MgbWFyZ2luYWxlcyBwZXJvIGNvbXBhcnRlbiBjaWVydGEgZXN0cnVjdHVyYSBkZSBkZXBlbmRlbmNpYSwgY29tbyBlbiBmaW5hbnphcywgc2VndXJvcyBvIGFuw6FsaXNpcyBkZSByaWVzZ29zLgoKRm9ybWFsbWVudGUsIHVuYSBjw7NwdWxhIGVzIHVuYSBmdW5jacOzbiBtdWx0aXZhcmlhZGEKCiRDOiBbMCwxXV5kIFxyaWdodGFycm93IFswLDFdJAoKcXVlIGFzb2NpYSB1biB2ZWN0b3IgZGUgcHJvYmFiaWxpZGFkZXMgbWFyZ2luYWxlcyB1bml2YXJpYWRhcyBxdWUgYXNvY2lhIHVuIHZlY3RvciBkZSBwcm9iYWJpbGlkYWRlcyBtYXJnaW5hbGVzIHVuaXZhcmlhZGFzCgokKHVfMSwgdV8yLCBcZG90cywgdV9kKSQsIGNhZGEgdW5hIGVuIGVsIGludGVydmFsbyAkWzAsMV0kLCBjb24gdW5hIHByb2JhYmlsaWRhZCBjb25qdW50YS4KCkxhIGJhc2UgdGXDs3JpY2EgZGUgbGFzIGPDs3B1bGFzIHNlIGVuY3VlbnRyYSBlbiBlbCBUZW9yZW1hIGRlIFNrbGFyLCBwcm9wdWVzdG8gcG9yIEFiZSBTa2xhciBlbiAxOTU5LiBFc3RlIHRlb3JlbWEgZXN0YWJsZWNlIHF1ZToKClNpICRGKHhfMSwgXGRvdHMsIHhfZCkkIGVzIHVuYSBmdW5jacOzbiBkZSBkaXN0cmlidWNpw7NuIGNvbmp1bnRhIGNvbnRpbnVhIGNvbiBtYXJnaW5hbGVzICRGXzEoeF8xKSwgXGRvdHMsIEZfZCh4X2QpJCwgZW50b25jZXMgZXhpc3RlIHVuYSDDum5pY2EgY8OzcHVsYSAkQyQgdGFsIHF1ZToKCiQkCkYoeF8xLCBcZG90cywgeF9kKSA9IEMoRl8xKHhfMSksIFxkb3RzLCBGX2QoeF9kKSkKJCQKCkVzdGUgcmVzdWx0YWRvIGltcGxpY2EgcXVlIGN1YWxxdWllciBmdW5jacOzbiBkZSBkaXN0cmlidWNpw7NuIGNvbmp1bnRhIGNvbnRpbnVhIHB1ZWRlIGRlc2NvbXBvbmVyc2UgZW4gc3VzIG1hcmdpbmFsZXMgdW5pdmFyaWFkYXMgeSB1bmEgY8OzcHVsYSBxdWUgbW9kZWxhIGxhIGVzdHJ1Y3R1cmEgZGUgZGVwZW5kZW5jaWEuIEFkZW3DoXMsIHNpIGxhcyBtYXJnaW5hbGVzIHNvbiBjb250aW51YXMsIGxhIGPDs3B1bGEgZXMgw7puaWNhOyBzaSBubyBsbyBzb24sIGV4aXN0ZSBwZXJvIHB1ZWRlIG5vIHNlciDDum5pY2EuCgpQb3IgZWwgY29udHJhcmlvLCBzaSBjb25vY2Vtb3MgdW5hIGPDs3B1bGEgJEMkIHkgbGFzIGZ1bmNpb25lcyBkZSBkaXN0cmlidWNpw7NuIG1hcmdpbmFsZXMgJEZfMSwgXGRvdHMsIEZfZCQsIHRhbWJpw6luIHBvZGVtb3MgcmVjb25zdHJ1aXIgbGEgZnVuY2nDs24gZGUgZGlzdHJpYnVjacOzbiBjb25qdW50YSBtZWRpYW50ZSBsYSBtaXNtYSByZWxhY2nDs246CgokJApGKHhfMSwgXGRvdHMsIHhfZCkgPSBDKEZfMSh4XzEpLCBcZG90cywgRl9kKHhfZCkpCiQkCgpFc3RvIHBlcm1pdGUgdW5hIGVub3JtZSBmbGV4aWJpbGlkYWQsIHlhIHF1ZSBzZSBwdWVkZSBtb2RlbGFyIGNhZGEgdmFyaWFibGUgY29uIGxhIGRpc3RyaWJ1Y2nDs24gcXVlIG1lam9yIHNlIGFqdXN0ZSBhIHN1cyBjYXJhY3RlcsOtc3RpY2FzIHkgbHVlZ28gdXNhciB1bmEgY8OzcHVsYSBwYXJhIHVuaXJsYXMgc2Vnw7puIHN1IGRlcGVuZGVuY2lhLgoKVW5vIGRlIGxvcyBlamVtcGxvcyBtw6FzIHV0aWxpemFkb3MgZXMgbGEgY8OzcHVsYSBnYXVzc2lhbmEsIHF1ZSBzZSBiYXNhIGVuIGxhIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIG11bHRpdmFyaWFkYS4gU3UgZXhwcmVzacOzbiBlczoKCiQkCkNfXFNpZ21hKHVfMSwgXGRvdHMsIHVfZCkgPSBcUGhpX1xTaWdtYShcUGhpXnstMX0odV8xKSwgXGRvdHMsIFxQaGleey0xfSh1X2QpKQokJAoKZG9uZGU6CgokXFBoaV57LTF9JCBlcyBsYSBpbnZlcnNhIGRlIGxhIGZ1bmNpw7NuIGRlIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIGVzdMOhbmRhciAodGFtYmnDqW4gbGxhbWFkYSBmdW5jacOzbiBjdWFudMOtbGljYSBvIHF1YW50aWxlIGZ1bmN0aW9uKSwgJFxQaGlfXFNpZ21hJCBlcyBsYSBmdW5jacOzbiBkZSBkaXN0cmlidWNpw7NuIGRlIHVuYSBub3JtYWwgbXVsdGl2YXJpYWRhIGNvbiBtYXRyaXogZGUgY29ycmVsYWNpw7NuICRcU2lnbWEkLiBFc3RhIGPDs3B1bGEgdHJhbnNmb3JtYSBsYXMgdmFyaWFibGVzIHVuaWZvcm1lcyBtYXJnaW5hbGVzIGEgbm9ybWFsZXMgZXN0w6FuZGFyLCBsdWVnbyBjYWxjdWxhIGxhIHByb2JhYmlsaWRhZCBjb25qdW50YSBiYWpvIHVuYSBub3JtYWwgbXVsdGl2YXJpYWRhLCB5IGFzw60gZ2VuZXJhIGxhIGRlcGVuZGVuY2lhLgoKTGFzIGPDs3B1bGFzIHNvbiBmdW5kYW1lbnRhbGVzIGVuIGNhbXBvcyBjb21vOgoKRmluYW56YXMsIHBhcmEgbW9kZWxhciByaWVzZ28gY29uanVudG8gZGUgcG9ydGFmb2xpb3MgbyBkZXBlbmRlbmNpYXMgZXh0cmVtYXMgZW4gY3Jpc2lzLiBBY3R1YXLDrWEsIHBhcmEgbW9kZWxhciBldmVudG9zIGRlcGVuZGllbnRlcyBjb21vIGxvbmdldmlkYWQgeSBlbmZlcm1lZGFkZXMuIEhpZHJvbG9nw61hLCBwYXJhIGVzdHVkaWFyIGxhIGRlcGVuZGVuY2lhIGVudHJlIHByZWNpcGl0YWNpb25lcyB5IGNhdWRhbGVzIGV4dHJlbW9zLiBFbiBjb25jbHVzacOzbiwgbGFzIGPDs3B1bGFzIG9mcmVjZW4gdW4gbWFyY28gdGXDs3JpY28geSBwcsOhY3RpY28gbXV5IHBvdGVudGUgcGFyYSBlc3R1ZGlhciBsYSBkZXBlbmRlbmNpYSBlbnRyZSB2YXJpYWJsZXMgYWxlYXRvcmlhcy4gR3JhY2lhcyBhbCB0ZW9yZW1hIGRlIFNrbGFyLCBzZSBwdWVkZW4gc2VwYXJhciBsb3MgZWZlY3RvcyBtYXJnaW5hbGVzIGRlIGxhIGVzdHJ1Y3R1cmEgZGUgZGVwZW5kZW5jaWEsIHBlcm1pdGllbmRvIG1vZGVsb3MgbcOhcyBmbGV4aWJsZXMsIGludGVycHJldGFibGVzIHkgYWRlY3VhZG9zIHBhcmEgZmVuw7NtZW5vcyByZWFsZXMgZG9uZGUgbGEgY29ycmVsYWNpw7NuIGxpbmVhbCByZXN1bHRhIGluc3VmaWNpZW50ZS4KCmBgYHtyfQoKIyBEZWZpbmlyIGNvcHVsYXMKbm9ybWFsX2NvcCA8LSBub3JtYWxDb3B1bGEocGFyYW0gPSAwLjcsIGRpbSA9IDIpICAgICMgZGVwZW5kZW5jaWEgcG9zaXRpdmEKY2xheXRvbl9jb3AgPC0gY2xheXRvbkNvcHVsYShwYXJhbSA9IDMsIGRpbSA9IDIpICAgICMgZGVwZW5kZW5jaWEgZW4gY29sYXMgaW5mZXJpb3JlcwoKIyBTaW11bGFyIGRhdG9zCnNldC5zZWVkKDEyMykKdV9ub3JtYWwgPC0gckNvcHVsYSgxMDAwLCBub3JtYWxfY29wKQp1X2NsYXl0b24gPC0gckNvcHVsYSgxMDAwLCBjbGF5dG9uX2NvcCkKCiMgRGlidWphciBncsOhZmljb3MKcGFyKG1mcm93ID0gYygxLCAyKSkgICMgZGl2aWRpciB2ZW50YW5hIGVuIDIgZ3LDoWZpY29zCgpwbG90KHVfbm9ybWFsLCBtYWluID0gIkPDs3B1bGEgR2F1c3NpYW5hICjPgSA9IDAuNykiLCAKICAgICB4bGFiID0gZXhwcmVzc2lvbihVWzFdKSwgeWxhYiA9IGV4cHJlc3Npb24oVVsyXSksIHBjaCA9IDIwLCBjb2wgPSByZ2IoMCwgMCwgMSwgMC41KSkKCnBsb3QodV9jbGF5dG9uLCBtYWluID0gIkPDs3B1bGEgZGUgQ2xheXRvbiAozrggPSAzKSIsIAogICAgIHhsYWIgPSBleHByZXNzaW9uKFVbMV0pLCB5bGFiID0gZXhwcmVzc2lvbihVWzJdKSwgcGNoID0gMjAsIGNvbCA9IHJnYigxLCAwLCAwLCAwLjUpKQoKYGBgCgojIyAzLjgpIEVqZW1wbG8gY29uIHB5dGhvbgoKYGBge3B5dGhvbn0KCgojIENvbmZpZ3VyYWNpw7NuIGdlbmVyYWwKc25zLnNldChzdHlsZT0id2hpdGVncmlkIikKbnAucmFuZG9tLnNlZWQoNDIpCgojIDEuIERpc3RyaWJ1Y2nDs24gbm9ybWFsIHVuaXZhcmlhbnRlIHZzIGVzdMOhbmRhcgptdSwgc2lnbWEgPSA1MCwgMTAKeCA9IG5wLmxpbnNwYWNlKG11IC0gNCpzaWdtYSwgbXUgKyA0KnNpZ21hLCAxMDAwKQp4X3N0ZCA9IG5wLmxpbnNwYWNlKC00LCA0LCAxMDAwKQpwZGYgPSBub3JtLnBkZih4LCBtdSwgc2lnbWEpCnBkZl9zdGQgPSBub3JtLnBkZih4X3N0ZCkKCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCwgNCkpCnBsdC5wbG90KHgsIHBkZiwgbGFiZWw9Ik4oNTAsMTApIiwgY29sb3I9ImJsdWUiKQpwbHQucGxvdCh4X3N0ZCpzaWdtYSArIG11LCBwZGZfc3RkLCAnLS0nLCBsYWJlbD0iTigwLDEpIHJlZXNjYWxhZGEiLCBjb2xvcj0icmVkIikKcGx0LnRpdGxlKCJEaXN0cmlidWNpw7NuIE5vcm1hbCBVbml2YXJpYW50ZSB5IEVzdMOhbmRhciIpCnBsdC54bGFiZWwoIngiKQpwbHQueWxhYmVsKCJEZW5zaWRhZCIpCnBsdC5sZWdlbmQoKQpwbHQuZ3JpZChUcnVlKQpwbHQudGlnaHRfbGF5b3V0KCkKcGx0LnNob3coKQoKIyBFc3RhbmRhcml6YWNpw7NuIHkgdHJhbnNmb3JtYWNpb25lcwpYID0gNzAKWiA9IChYIC0gbXUpIC8gc2lnbWEKWF9pbnYgPSBaICogc2lnbWEgKyBtdQpYX2xpbmVhciA9IDIgKiBYICsgNQoKcHJpbnQoIlB1bnRhamUgWjoiLCBaKQpwcmludCgiUmVjdXBlcmFkbyBkZXNkZSBaOiIsIFhfaW52KQpwcmludCgiVHJhbnNmb3JtYWNpw7NuIGxpbmVhbCAoMlggKyA1KToiLCBYX2xpbmVhcikKCiMgMi4gQ3VydmFzIGRlIG5pdmVsIGRlIHVuYSBub3JtYWwgYml2YXJpYWRhCm1lYW4gPSBbMCwgMF0KY292ID0gW1sxLCAwLjhdLCBbMC44LCAxXV0KZGF0YSA9IG5wLnJhbmRvbS5tdWx0aXZhcmlhdGVfbm9ybWFsKG1lYW4sIGNvdiwgMTAwMCkKZGYgPSBwZC5EYXRhRnJhbWUoZGF0YSwgY29sdW1ucz1bIlgiLCAiWSJdKQoKc25zLmtkZXBsb3QoZGF0YT1kZiwgeD0iWCIsIHk9IlkiLCBmaWxsPVRydWUsIHRocmVzaD0wLjAxLCBsZXZlbHM9MTAsIGNtYXA9InZpcmlkaXMiKQpwbHQuc2NhdHRlcihkZlsiWCJdLCBkZlsiWSJdLCBzPTUsIGFscGhhPTAuMywgY29sb3I9ImJsdWUiKQpwbHQudGl0bGUoIkN1cnZhcyBkZSBOaXZlbCAtIE5vcm1hbCBCaXZhcmlhZGEiKQpwbHQueGxhYmVsKCJYIikKcGx0LnlsYWJlbCgiWSIpCnBsdC5heGlzKCJlcXVhbCIpCnBsdC50aWdodF9sYXlvdXQoKQpwbHQuc2hvdygpCgojIDMuIERpc3RhbmNpYSBFdWNsaWRpYW5hCkEgPSBucC5hcnJheShbMiwgM10pCkIgPSBucC5hcnJheShbNywgNl0pCmRpc3QgPSBucC5saW5hbGcubm9ybShCIC0gQSkKCnBsdC5maWd1cmUoZmlnc2l6ZT0oNiwgNikpCnBsdC5wbG90KFtBWzBdLCBCWzBdXSwgW0FbMV0sIEJbMV1dLCAnci0tJywgbHc9MikKcGx0LnNjYXR0ZXIoKkEsIGNvbG9yPSJibHVlIiwgcz0xMDAsIGxhYmVsPSJBIikKcGx0LnNjYXR0ZXIoKkIsIGNvbG9yPSJncmVlbiIsIHM9MTAwLCBsYWJlbD0iQiIpCnBsdC50ZXh0KChBWzBdK0JbMF0pLzIsIChBWzFdK0JbMV0pLzIrMC41LCBmIkRpc3RhbmNpYSBFdWNsaWRpYW5hID0ge2Rpc3Q6LjJmfSIsIGhhPSJjZW50ZXIiKQpwbHQueGxpbSgwLCAxMCkKcGx0LnlsaW0oMCwgMTApCnBsdC5sZWdlbmQoKQpwbHQuZ3JpZChUcnVlKQpwbHQudGl0bGUoIkRpc3RhbmNpYSBFdWNsaWRpYW5hIGVudHJlIEEgeSBCIikKcGx0LnRpZ2h0X2xheW91dCgpCnBsdC5zaG93KCkKCiMgNC4gRGlzdGFuY2lhIGRlIE1haGFsYW5vYmlzCmlyaXMgPSBsb2FkX2lyaXMoKQpYX2lyaXMgPSBpcmlzLmRhdGFbOiwgOjNdCm11X2lyaXMgPSBucC5tZWFuKFhfaXJpcywgYXhpcz0wKQpTX2lyaXMgPSBucC5jb3YoWF9pcmlzLlQpCgptYWhhbF9kaXN0ID0gW21haGFsYW5vYmlzKHgsIG11X2lyaXMsIG5wLmxpbmFsZy5pbnYoU19pcmlzKSkgZm9yIHggaW4gWF9pcmlzXQpldWNsX2Rpc3QgPSBbbnAubGluYWxnLm5vcm0oeCAtIG11X2lyaXMpIGZvciB4IGluIFhfaXJpc10KbWFoYWxfZGlzdCA9IG5wLmFycmF5KG1haGFsX2Rpc3QpCmV1Y2xfZGlzdCA9IG5wLmFycmF5KGV1Y2xfZGlzdCkKCnRocmVzaG9sZCA9IG5wLnNxcnQoY2hpMi5wcGYoMC45NSwgZGY9MykpCm91dGxpZXJzID0gbWFoYWxfZGlzdCA+IHRocmVzaG9sZAoKZGZfaXJpcyA9IHBkLkRhdGFGcmFtZSh7CiAgICAiTWFoYWxhbm9iaXMiOiBtYWhhbF9kaXN0LAogICAgIkV1Y2xpZGVhbiI6IGV1Y2xfZGlzdCwKICAgICJPdXRsaWVyIjogb3V0bGllcnMKfSkKCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCwgNSkpCnNucy5zY2F0dGVycGxvdChkYXRhPWRmX2lyaXMsIHg9IkV1Y2xpZGVhbiIsIHk9Ik1haGFsYW5vYmlzIiwgaHVlPSJPdXRsaWVyIiwgcGFsZXR0ZT17RmFsc2U6ICJibGFjayIsIFRydWU6ICJyZWQifSkKcGx0LmF4aGxpbmUodGhyZXNob2xkLCBsaW5lc3R5bGU9Ii0tIiwgY29sb3I9InJlZCIsIGxhYmVsPWYiVW1icmFsIM+HwrIgKDk1JSkgPSB7dGhyZXNob2xkOi4yZn0iKQpwbHQubGVnZW5kKCkKcGx0LnRpdGxlKCJEaXN0YW5jaWEgTWFoYWxhbm9iaXMgdnMgRXVjbGlkaWFuYSIpCnBsdC50aWdodF9sYXlvdXQoKQpwbHQuc2hvdygpCgojIDUuIENvbXBhcmFjacOzbiBOb3JtYWwgdnMgdCBtdWx0aXZhcmlhZGFzCm11ID0gbnAuYXJyYXkoWzAsIDBdKQpTaWdtYSA9IG5wLmFycmF5KFtbMSwgMC43XSwgWzAuNywgMV1dKQpuID0gMTAwMApkZl90ID0gMwoKdF9kYXRhID0gbXVsdGl2YXJpYXRlX3QucnZzKGxvYz1tdSwgc2hhcGU9U2lnbWEsIGRmPWRmX3QsIHNpemU9bikKbm9ybV9kYXRhID0gbnAucmFuZG9tLm11bHRpdmFyaWF0ZV9ub3JtYWwobXUsIFNpZ21hLCBzaXplPW4pCgp0X2RmID0gcGQuRGF0YUZyYW1lKHRfZGF0YSwgY29sdW1ucz1bIlgxIiwgIlgyIl0pCnRfZGZbIkRpc3RyaWJ1Y2nDs24iXSA9ICJ0LVN0dWRlbnQiCgpub3JtX2RmID0gcGQuRGF0YUZyYW1lKG5vcm1fZGF0YSwgY29sdW1ucz1bIlgxIiwgIlgyIl0pCm5vcm1fZGZbIkRpc3RyaWJ1Y2nDs24iXSA9ICJOb3JtYWwiCgphbGxfZGYgPSBwZC5jb25jYXQoW3RfZGYsIG5vcm1fZGZdKQoKc25zLnNjYXR0ZXJwbG90KGRhdGE9YWxsX2RmLCB4PSJYMSIsIHk9IlgyIiwgaHVlPSJEaXN0cmlidWNpw7NuIiwgYWxwaGE9MC41KQpwbHQudGl0bGUoIkNvbXBhcmFjacOzbjogTm9ybWFsIHZzIHQtU3R1ZGVudCBtdWx0aXZhcmlhZGFzIikKcGx0LnRpZ2h0X2xheW91dCgpCnBsdC5zaG93KCkKCmBgYAoKIyA0KSBJbmZlcmVuY2lhIG11bHRpdmFyaWFudGUKCkxhwqAqKmVzdGFkw61zdGljYSBpbmZlcmVuY2lhbCBtdWx0aXZhcmlhbnRlKirCoGVzIHVuYSByYW1hIGRlIGxhIGVzdGFkw61zdGljYSBxdWUgcGVybWl0ZSBoYWNlciBpbmZlcmVuY2lhcyBvIGdlbmVyYWxpemFjaW9uZXMgc29icmUgdW5hIHBvYmxhY2nDs24gYSBwYXJ0aXIgZGUgbXVlc3RyYXMsIGNvbnNpZGVyYW5kb8KgKiptw6FzIGRlIHVuYSB2YXJpYWJsZSBhbCBtaXNtbyB0aWVtcG8qKi4gQSBkaWZlcmVuY2lhIGRlIGxhIGVzdGFkw61zdGljYSB1bml2YXJpYW50ZSBvIGJpdmFyaWFudGUsIHF1ZSBhbmFsaXphIHVuYSBvIGRvcyB2YXJpYWJsZXMgcG9yIHNlcGFyYWRvLCBsYSBtdWx0aXZhcmlhbnRlIGJ1c2NhIGVudGVuZGVywqAqKmPDs21vIHNlIHJlbGFjaW9uYW4gdmFyaWFzIHZhcmlhYmxlcyBlbnRyZSBzw60geSBjw7NtbyBlc2FzIHJlbGFjaW9uZXMgYWZlY3RhbiB1biBmZW7Ds21lbm8gbyByZXN1bHRhZG8qKi4gU3Ugb2JqZXRpdm8gbm8gZXMgc29sbyBkZXNjcmliaXIgbG9zIGRhdG9zIG9ic2VydmFkb3MsIHNpbm/CoCoqZXN0aW1hciBwYXLDoW1ldHJvcyBwb2JsYWNpb25hbGVzLCBjb250cmFzdGFyIGhpcMOzdGVzaXMgeSBwcmVkZWNpciBjb21wb3J0YW1pZW50b3MqKiwgY29uc2lkZXJhbmRvIGxhIGludGVyYWNjacOzbiBjb25qdW50YSBkZSBsYXMgdmFyaWFibGVzLgoKRW50cmUgbG9zwqAqKmNhc29zIGRlIHVzbyoqwqBtw6FzIGNvbXVuZXMgZGUgbGEgZXN0YWTDrXN0aWNhIGluZmVyZW5jaWFsIG11bHRpdmFyaWFudGUgZXN0w6FuOgoKLSAgICoqQW7DoWxpc2lzIGRlIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzIChQQ0EpKiosIHF1ZSByZWR1Y2UgbGEgZGltZW5zacOzbiBkZSBsb3MgZGF0b3MgbWFudGVuaWVuZG8gbGEgbWF5b3IgdmFyaWFuemEgcG9zaWJsZS4KCi0gICAqKkFuw6FsaXNpcyBkaXNjcmltaW5hbnRlKiosIHF1ZSBwZXJtaXRlIGNsYXNpZmljYXIgb2JzZXJ2YWNpb25lcyBlbiBncnVwb3MgcHJldmlhbWVudGUgZGVmaW5pZG9zLgoKLSAgICoqQW7DoWxpc2lzIGRlIGNvbmdsb21lcmFkb3MgKGNsdXN0ZXIgYW5hbHlzaXMpKiosIHF1ZSBhZ3J1cGEgb2JzZXJ2YWNpb25lcyBzaW1pbGFyZXMgZW50cmUgc8OtLgoKLSAgICoqTW9kZWxvcyBtdWx0aXZhcmlhbnRlcyBkZSByZWdyZXNpw7NuKiosIHF1ZSBleHBsaWNhbiB1bmEgbyBtw6FzIHZhcmlhYmxlcyBkZXBlbmRpZW50ZXMgdXNhbmRvIG3Dumx0aXBsZXMgdmFyaWFibGVzIGluZGVwZW5kaWVudGVzLgoKLSAgICoqTUFOT1ZBIChhbsOhbGlzaXMgbXVsdGl2YXJpYW50ZSBkZSB2YXJpYW56YSkqKiwgcXVlIGNvbXBhcmEgZ3J1cG9zIGNvbnNpZGVyYW5kbyB2YXJpYXMgdmFyaWFibGVzIGRlIHJlc3B1ZXN0YSBhIGxhIHZlei4KCkVzdG9zIG3DqXRvZG9zIHNlIGFwbGljYW4gZW4gZGl2ZXJzYXMgw6FyZWFzOiBlbsKgKiptYXJrZXRpbmcqKiwgcGFyYSBzZWdtZW50YXIgY2xpZW50ZXMgc2Vnw7puIHN1cyBjYXJhY3RlcsOtc3RpY2FzIGRlIGNvbnN1bW87IGVuwqAqKmJpb2xvZ8OtYSoqLCBwYXJhIGNsYXNpZmljYXIgZXNwZWNpZXMgbyBldmFsdWFyIGVmZWN0b3MgZGUgdHJhdGFtaWVudG9zOyBlbsKgKipwc2ljb2xvZ8OtYSoqLCBwYXJhIGFuYWxpemFyIHBlcmZpbGVzIGRlIHBlcnNvbmFsaWRhZDsgbyBlbsKgKipmaW5hbnphcyoqLCBwYXJhIGV2YWx1YXIgZWwgcmllc2dvIHkgcmVuZGltaWVudG8gZGUgcG9ydGFmb2xpb3MgY29uc2lkZXJhbmRvIG3Dumx0aXBsZXMgaW5kaWNhZG9yZXMuIEVuIHJlc3VtZW4sIGxhIGVzdGFkw61zdGljYSBpbmZlcmVuY2lhbCBtdWx0aXZhcmlhbnRlIGVzIGNsYXZlIHBhcmEgdG9tYXIgZGVjaXNpb25lcyBmdW5kYW1lbnRhZGFzIGVuIGNvbnRleHRvcyBjb21wbGVqb3MgY29uIG3Dumx0aXBsZXMgdmFyaWFibGVzIHNpbXVsdMOhbmVhcy4KCiMjIDQuMSkgRXN0YWTDrXN0aWNvcyBtdcOpc3RyYWxlcyBtdWx0aXZhcmlhbnRlcwoKTG9zwqAqKmVzdGFkw61zdGljb3MgbXVlc3RyYWxlcyBtdWx0aXZhcmlhbnRlcyoqwqBzb24gbWVkaWRhcyBjYWxjdWxhZGFzIGEgcGFydGlyIGRlIHVuYSBtdWVzdHJhIHF1ZSByZXN1bWVuIHkgZGVzY3JpYmVuIGxhcyBjYXJhY3RlcsOtc3RpY2FzIGNvbmp1bnRhcyBkZSB2YXJpYXMgdmFyaWFibGVzIGFsIG1pc21vIHRpZW1wby4gRXN0b3MgZXN0YWTDrXN0aWNvcyBwZXJtaXRlbiBhbmFsaXphciBsYSBlc3RydWN0dXJhIGludGVybmEgZGUgbG9zIGRhdG9zIG11bHRpdmFyaWFkb3MsIGVzIGRlY2lyLCBjw7NtbyBzZSByZWxhY2lvbmFuIHkgdmFyw61hbiBsYXMgdmFyaWFibGVzIGVudHJlIHPDrSBkZW50cm8gZGUgbGEgbXVlc3RyYS4gRW50cmUgbG9zIG3DoXMgY29tdW5lcyBlc3TDoW4gZWzCoCoqdmVjdG9yIGRlIG1lZGlhcyBtdWVzdHJhbCoqLCBxdWUgcmVzdW1lIGVsIHZhbG9yIHByb21lZGlvIGRlIGNhZGEgdmFyaWFibGU7IGxhwqAqKm1hdHJpeiBkZSB2YXJpYW56YXMgeSBjb3ZhcmlhbnphcyoqLCBxdWUgaW5kaWNhIGPDs21vIHZhcsOtYW4gbGFzIHZhcmlhYmxlcyBjb25qdW50YW1lbnRlOyB5IGxhwqAqKm1hdHJpeiBkZSBjb3JyZWxhY2lvbmVzKiosIHF1ZSBlc3RhbmRhcml6YSBlc2EgcmVsYWNpw7NuIHBhcmEgZmFjaWxpdGFyIGxhIGNvbXBhcmFjacOzbi4KCkVzdG9zIGVzdGFkw61zdGljb3Mgc2lydmVuIGNvbW8gYmFzZSBwYXJhIHJlYWxpemFyIGluZmVyZW5jaWFzIHNvYnJlIGxhIHBvYmxhY2nDs24sIHBvciBlamVtcGxvLCBhbCBjb250cmFzdGFyIGhpcMOzdGVzaXMgc29icmUgbGEgaWd1YWxkYWQgZGUgbWVkaWFzIGVudHJlIGdydXBvcywgZXZhbHVhciBsYSBob21vZ2VuZWlkYWQgZGUgY292YXJpYW56YXMgbyBjb25zdHJ1aXIgbW9kZWxvcyBwcmVkaWN0aXZvcy4gQWRlbcOhcywgc2UgdXRpbGl6YW4gZW4gdMOpY25pY2FzIGNvbW8gZWwgYW7DoWxpc2lzIGRlIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzLCBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSB5IHJlZ3Jlc2nDs24gbcO6bHRpcGxlLCBkb25kZSBsYSBjb21wcmVuc2nDs24gZGUgbGEgZXN0cnVjdHVyYSBtdWx0aXZhcmlhZGEgZGUgbG9zIGRhdG9zIGVzIGVzZW5jaWFsLgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCgojIFVzYXIgZWwgZGF0YXNldCBpcmlzIChzb2xvIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcykKZGF0YShpcmlzKQpkYXRvc19udW1lcmljb3MgPC0gaXJpcyAlPiUgc2VsZWN0KFNlcGFsLkxlbmd0aCwgU2VwYWwuV2lkdGgsIFBldGFsLkxlbmd0aCwgUGV0YWwuV2lkdGgpCgojIDEuIFZlY3RvciBkZSBtZWRpYXMgbXVlc3RyYWwKbWVkaWFzIDwtIGNvbE1lYW5zKGRhdG9zX251bWVyaWNvcykKcHJpbnQoIlZlY3RvciBkZSBtZWRpYXMgbXVlc3RyYWw6IikKcHJpbnQobWVkaWFzKQoKIyAyLiBNYXRyaXogZGUgY292YXJpYW56YXMgbXVlc3RyYWwKY292YXJpYW56YXMgPC0gY292KGRhdG9zX251bWVyaWNvcykKcHJpbnQoIk1hdHJpeiBkZSB2YXJpYW56YXMgeSBjb3ZhcmlhbnphczoiKQpwcmludChjb3ZhcmlhbnphcykKCiMgMy4gTWF0cml6IGRlIGNvcnJlbGFjaW9uZXMgKG9wY2lvbmFsKQpjb3JyZWxhY2lvbmVzIDwtIGNvcihkYXRvc19udW1lcmljb3MpCnByaW50KCJNYXRyaXogZGUgY29ycmVsYWNpb25lczoiKQpwcmludChjb3JyZWxhY2lvbmVzKQoKIyA0LiBHcsOhZmljbyBkZSBkaXNwZXJzacOzbiBtdWx0aXZhcmlhZG8KZ2dwYWlycyhkYXRvc19udW1lcmljb3MsIHRpdGxlID0gIkdyw6FmaWNvIGRlIGRpc3BlcnNpw7NuIG11bHRpdmFyaWFkbyAtIERhdGFzZXQgSXJpcyIpCgpgYGAKCiMjIDQuMikgVGVvcmVtYSBjZW50cmFsIGRlbCBsaW1pdGUgbXVsdGl2YXJpYW50ZQoKRWwgVGVvcmVtYSBDZW50cmFsIGRlbCBMw61taXRlIE11bHRpdmFyaWFudGUgKFRDTE0pIGV4dGllbmRlIGVsIGNsw6FzaWNvIFRlb3JlbWEgQ2VudHJhbCBkZWwgTMOtbWl0ZSBhbCBjYXNvIGRlIHZlY3RvcmVzIGFsZWF0b3Jpb3MuIEVzdGFibGVjZSBxdWUsIHNpIHNlIHRpZW5lbiB2YXJpYWJsZXMgYWxlYXRvcmlhcyB2ZWN0b3JpYWxlcyBpbmRlcGVuZGllbnRlcyBlIGlkw6ludGljYW1lbnRlIGRpc3RyaWJ1aWRhcyAoaS5pLmQuKSBjb24gbWVkaWEgdmVjdG9yaWFsICRcbXUkIHkgbWF0cml6IGRlIGNvdmFyaWFuemEgJFxTaWdtYSQsIGVudG9uY2VzIGxhIG1lZGlhIG11ZXN0cmFsIHZlY3RvcmlhbCDigJRhbCBzZXIgY2VudHJhZGEgeSBlc2NhbGFkYSBhZGVjdWFkYW1lbnRl4oCUIGNvbnZlcmdlIGVuIGRpc3RyaWJ1Y2nDs24gYSB1bmEgZGlzdHJpYnVjacOzbiBub3JtYWwgbXVsdGl2YXJpYWRhIGNvbmZvcm1lIGVsIHRhbWHDsW8gZGUgbXVlc3RyYSBjcmVjZS4gRXNwZWPDrWZpY2FtZW50ZSwgc2kgJFxtYXRoYmZ7WH1fMSwgXGRvdHMsIFxtYXRoYmZ7WH1fbiQgc29uIHZlY3RvcmVzIGFsZWF0b3Jpb3MgaS5pLmQuLCBlbnRvbmNlczoKCiQkClxzcXJ0e259KFxiYXJ7XG1hdGhiZntYfX0gLSBcYm9sZHN5bWJvbHtcbXV9KSBceHJpZ2h0YXJyb3d7ZH0gXG1hdGhjYWx7Tn1fayhcbWF0aGJmezB9LCBcYm9sZHN5bWJvbHtcU2lnbWF9KQokJAoKZG9uZGUgJFxiYXJ7XG1hdGhiZntYfX0kIGVzIGxhIG1lZGlhIG11ZXN0cmFsIHZlY3RvcmlhbCBkZSBkaW1lbnNpw7NuICRrJCwgeSAkXG1hdGhjYWx7Tn1faygwLCBcU2lnbWEpJCBkZW5vdGEgbGEgZGlzdHJpYnVjacOzbiBub3JtYWwgbXVsdGl2YXJpYWRhIGRlIG1lZGlhIGNlcm8geSBtYXRyaXogZGUgY292YXJpYW56YSAkXFNpZ21hJC4gRXN0ZSB0ZW9yZW1hIGVzIGZ1bmRhbWVudGFsIHBvcnF1ZSBwZXJtaXRlIHVzYXIgdMOpY25pY2FzIGRlIGluZmVyZW5jaWEgZXN0YWTDrXN0aWNhIG11bHRpdmFyaWFkYSBhdW4gY3VhbmRvIGxhcyBkaXN0cmlidWNpb25lcyBvcmlnaW5hbGVzIGRlIGxvcyBkYXRvcyBubyBzZWFuIG5vcm1hbGVzLCBzaWVtcHJlIHF1ZSBlbCB0YW1hw7FvIGRlIG11ZXN0cmEgc2VhIHN1ZmljaWVudGVtZW50ZSBncmFuZGUuIEdyYWNpYXMgYWwgVENMTSwgc2UgcHVlZGVuIGNvbnN0cnVpciBpbnRlcnZhbG9zIGRlIGNvbmZpYW56YSBtdWx0aXZhcmlhZG9zLCByZWFsaXphciBjb250cmFzdGVzIGRlIGhpcMOzdGVzaXMgKGNvbW8gZWwgdGVzdCBkZSBIb3RlbGxpbmcpLCB5IGFwbGljYXIgbW9kZWxvcyBsaW5lYWxlcyBtdWx0aXZhcmlhZG9zIGJham8gZWwgc3VwdWVzdG8gZGUgbm9ybWFsaWRhZCBhc2ludMOzdGljYS4KCkVuIGxhIHByw6FjdGljYSwgZWwgVENMTSBtdWx0aXZhcmlhbnRlIHNlIHVzYSBlbiBjYW1wb3MgY29tbyBsYSBlY29ub21ldHLDrWEgKHBhcmEgY29tcGFyYXIgbWVkaWFzIHZlY3RvcmlhbGVzIGRlIGluZGljYWRvcmVzIGVjb27Ds21pY29zKSwgbGEgcHNpY29sb2fDrWEgKGRvbmRlIHNlIGFuYWxpemFuIHB1bnRhamVzIGVuIG3Dumx0aXBsZXMgZXNjYWxhcyksIG8gbGEgYmlvZXN0YWTDrXN0aWNhIChwb3IgZWplbXBsbywgYWwgZXZhbHVhciBtw7psdGlwbGVzIGJpb21hcmNhZG9yZXMgZW4gZW5zYXlvcyBjbMOtbmljb3MpLiBUYW1iacOpbiBlcyBjbGF2ZSBlbiBlbCBhbsOhbGlzaXMgZGUgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMgeSBlbiB0w6ljbmljYXMgZGUgcmVkdWNjacOzbiBkZSBkaW1lbnNpb25hbGlkYWQsIGRvbmRlIHNlIHBhcnRlIGRlbCBzdXB1ZXN0byBkZSBub3JtYWxpZGFkIG11bHRpdmFyaWFkYSBwYXJhIHNpbXBsaWZpY2FyIGxhIGVzdHJ1Y3R1cmEgZGUgbG9zIGRhdG9zLgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCgojIFBhcsOhbWV0cm9zIGRlIGxhIGRpc3RyaWJ1Y2nDs24gb3JpZ2luYWwKbXUgPC0gYygzLCA3KQpTaWdtYSA8LSBtYXRyaXgoYygxLCAwLjgsCiAgICAgICAgICAgICAgICAgIDAuOCwgMiksIG5yb3cgPSAyKQoKIyBDb25maWd1cmFjacOzbiBkZSBzaW11bGFjacOzbgpuIDwtIDUwICAgICAgICAgICAgICAgICMgVGFtYcOxbyBkZSBtdWVzdHJhCnJlcHMgPC0gMTAwMCAgICAgICAgICAgIyBOw7ptZXJvIGRlIHJlcGV0aWNpb25lcwoKIyBTaW11bGFjacOzbjogZ2VuZXJhciBtZWRpYXMgbXVlc3RyYWxlcwpzZXQuc2VlZCgxMjMpCm1lZGlhcyA8LSByZXBsaWNhdGUocmVwcywgewogIG11ZXN0cmEgPC0gbXZybm9ybShuID0gbiwgbXUgPSBtdSwgU2lnbWEgPSBTaWdtYSkKICBjb2xNZWFucyhtdWVzdHJhKQp9KQoKIyBDb252ZXJ0aW1vcyBhIGRhdGEgZnJhbWUKZGYgPC0gYXMuZGF0YS5mcmFtZSh0KG1lZGlhcykpCmNvbG5hbWVzKGRmKSA8LSBjKCJYMSIsICJYMiIpCgojIEdyw6FmaWNvCmdncGxvdChkZiwgYWVzKHggPSBYMSwgeSA9IFgyKSkgKwogIGdlb21fcG9pbnQoYWxwaGEgPSAwLjMsIGNvbG9yID0gInN0ZWVsYmx1ZSIpICsKICBzdGF0X2VsbGlwc2UodHlwZSA9ICJub3JtIiwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDEuMikgKwogIGxhYnModGl0bGUgPSAiRGlzdHJpYnVjacOzbiBkZSBtZWRpYXMgbXVlc3RyYWxlcyBiaXZhcmlhZGFzIiwKICAgICAgIHN1YnRpdGxlID0gcGFzdGUoIlNpbXVsYWNpw7NuIGRlIiwgcmVwcywgIm1lZGlhcyBtdWVzdHJhbGVzIGNvbiBuID0iLCBuKSwKICAgICAgIHggPSAiTWVkaWEgbXVlc3RyYWwgZGUgWDEiLAogICAgICAgeSA9ICJNZWRpYSBtdWVzdHJhbCBkZSBYMiIpICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKIyMgNC4zKSBNw6F4aW1hIHZlcm9zaW1pbGl0dWQKCkxhIG3DoXhpbWEgdmVyb3NpbWlsaXR1ZCBlcyB1biBtw6l0b2RvIGVzdGFkw61zdGljbyB1dGlsaXphZG8gcGFyYSBlc3RpbWFyIGxvcyBwYXLDoW1ldHJvcyBkZXNjb25vY2lkb3MgZGUgdW4gbW9kZWxvIHByb2JhYmlsw61zdGljbywgZGFkb3MgdW5vcyBkYXRvcyBvYnNlcnZhZG9zLiBMYSBpZGVhIGVzIGVuY29udHJhciBlbCB2YWxvciBkZWwgcGFyw6FtZXRybyBxdWUgaGFjZSBtw6FzIHByb2JhYmxlIGhhYmVyIG9ic2VydmFkbyBsb3MgZGF0b3MgZGlzcG9uaWJsZXMuIFNpIGxhIGZ1bmNpw7NuIGRlIGRlbnNpZGFkIG8gZGUgcHJvYmFiaWxpZGFkIGVzIGYoeDvOuCksIGRvbmRlIM64IGVzIGVsIHBhcsOhbWV0cm8gYSBlc3RpbWFyIHkgeCBzb24gbG9zIGRhdG9zIG9ic2VydmFkb3MsIGVudG9uY2VzIGxhIGZ1bmNpw7NuIGRlIHZlcm9zaW1pbGl0dWQgc2UgZGVmaW5lIGNvbW86CgokJApMKFx0aGV0YSkgPSBccHJvZF97aT0xfV5uIGYoeF9pOyBcdGhldGEpCiQkCgpFbCBlc3RpbWFkb3IgZGUgbcOheGltYSB2ZXJvc2ltaWxpdHVkIGVzIGVsIHZhbG9yICRcaGF0e1x0aGV0YX0kIHF1ZSBtYXhpbWl6YSBlc3RhIGZ1bmNpw7NuOgoKJCQKXGhhdHtcdGhldGF9ID0gXGFyZ1xtYXhfe1x0aGV0YX0gTChcdGhldGEpCiQkCgpQYXJhIGZhY2lsaXRhciBsb3MgY8OhbGN1bG9zLCBzZSB1c2EgZ2VuZXJhbG1lbnRlIGxhIGxvZy12ZXJvc2ltaWxpdHVkLCBxdWUgdHJhbnNmb3JtYSBwcm9kdWN0b3MgZW4gc3VtYXM6CgokJApcZWxsKFx0aGV0YSkgPSBcbG9nIEwoXHRoZXRhKSA9IFxzdW1fe2k9MX1ebiBcbG9nIGYoeF9pOyBcdGhldGEpCiQkCgpMdWVnbywgc2UgZGVyaXZhbiBsYXMgY29uZGljaW9uZXMgZGUgcHJpbWVyIG9yZGVuIHJlc29sdmllbmRvOgoKJCQKXGZyYWN7ZFxlbGwoXHRoZXRhKX17ZFx0aGV0YX0gPSAwCiQkCgp5IHNlIHZlcmlmaWNhIHF1ZSBzZSB0cmF0YSBkZSB1biBtw6F4aW1vLiBFc3RlIGVuZm9xdWUgcHJvcG9yY2lvbmEgZXN0aW1hY2lvbmVzIHF1ZSwgYmFqbyBjb25kaWNpb25lcyBnZW5lcmFsZXMsIHNvbiBjb25zaXN0ZW50ZXMsIGluc2VzZ2FkYXMgeSBlZmljaWVudGVzIGFzaW50w7N0aWNhbWVudGUuCgpFbCBtw6l0b2RvIGRlIG3DoXhpbWEgdmVyb3NpbWlsaXR1ZCBzZSB1dGlsaXphIGFtcGxpYW1lbnRlIGVuIGVzdGFkw61zdGljYSwgZWNvbm9tZXRyw61hIHkgbWFjaGluZSBsZWFybmluZy4gUG9yIGVqZW1wbG8sIHNlIGFwbGljYSBwYXJhIGVzdGltYXIgcGFyw6FtZXRyb3MgZW4gbW9kZWxvcyBkZSByZWdyZXNpw7NuIChjb21vIGxhIHJlZ3Jlc2nDs24gbG9nw61zdGljYSksIGFqdXN0YXIgZGlzdHJpYnVjaW9uZXMgYSBkYXRvcyAoY29tbyBub3JtYWwsIGJpbm9taWFsIG8gUG9pc3NvbiksIG8gZW50cmVuYXIgbW9kZWxvcyBwcm9iYWJpbMOtc3RpY29zIGNvbXBsZWpvcyBjb21vIG1lemNsYXMgZGUgZ2F1c3NpYW5hcyBvIG1vZGVsb3Mgb2N1bHRvcyBkZSBNYXJrb3YuIFN1IHBvcHVsYXJpZGFkIHNlIGRlYmUgYSBzdSBnZW5lcmFsaWRhZCB5IGEgbGFzIGJ1ZW5hcyBwcm9waWVkYWRlcyBlc3RhZMOtc3RpY2FzIGRlIGxvcyBlc3RpbWFkb3JlcyBvYnRlbmlkb3MuCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgUGFzbyAxOiBTaW11bGFyIGRhdG9zCnNldC5zZWVkKDEyMykKbiA8LSAxMDAKbXVfdmVyZGFkZXJvIDwtIDUKc2lnbWFfdmVyZGFkZXJvIDwtIDIKZGF0b3MgPC0gcm5vcm0obiwgbWVhbiA9IG11X3ZlcmRhZGVybywgc2QgPSBzaWdtYV92ZXJkYWRlcm8pCgojIFBhc28gMjogRnVuY2nDs24gZGUgbG9nLXZlcm9zaW1pbGl0dWQgZW4gZnVuY2nDs24gZGUgbXUgKHNpZ21hIGNvbm9jaWRvKQpsb2dfdmVyb3NpbWlsaXR1ZCA8LSBmdW5jdGlvbihtdSwgZGF0b3MsIHNpZ21hKSB7CiAgbiA8LSBsZW5ndGgoZGF0b3MpCiAgc3VtYSA8LSBzdW0oKGRhdG9zIC0gbXUpXjIpCiAgbGwgPC0gLW4vMiAqIGxvZygyICogcGkgKiBzaWdtYV4yKSAtIHN1bWEgLyAoMiAqIHNpZ21hXjIpCiAgcmV0dXJuKGxsKQp9CgojIFBhc28gMzogRXZhbHVhciBsb2ctdmVyb3NpbWlsaXR1ZCBwYXJhIG11Y2hvcyB2YWxvcmVzIGRlIG11Cm11X3ZhbHMgPC0gc2VxKDMsIDcsIGxlbmd0aC5vdXQgPSAyMDApCmxsX3ZhbHMgPC0gc2FwcGx5KG11X3ZhbHMsIGxvZ192ZXJvc2ltaWxpdHVkLCBkYXRvcyA9IGRhdG9zLCBzaWdtYSA9IHNpZ21hX3ZlcmRhZGVybykKCiMgUGFzbyA0OiBDcmVhciBkYXRhIGZyYW1lIHBhcmEgZ3JhZmljYXIKZGYgPC0gZGF0YS5mcmFtZShtdSA9IG11X3ZhbHMsIGxvZ0wgPSBsbF92YWxzKQptdV9NTEUgPC0gbXVfdmFsc1t3aGljaC5tYXgobGxfdmFscyldCgojIFBhc28gNTogR3LDoWZpY28KZ2dwbG90KGRmLCBhZXMoeCA9IG11LCB5ID0gbG9nTCkpICsKICBnZW9tX2xpbmUoY29sb3IgPSAic3RlZWxibHVlIiwgc2l6ZSA9IDEpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSBtdV9NTEUsIGxpbmV0eXBlID0gImRhc2hlZCIsIGNvbG9yID0gInJlZCIpICsKICBnZW9tX3BvaW50KGFlcyh4ID0gbXVfTUxFLCB5ID0gbWF4KGxvZ0wpKSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDIpICsKICBsYWJzKAogICAgdGl0bGUgPSAiTG9nLXZlcm9zaW1pbGl0dWQgcGFyYSBkaXN0aW50b3MgdmFsb3JlcyBkZSAkJFxcbXUkJCIsCiAgICBzdWJ0aXRsZSA9IHBhc3RlKCJNw6F4aW1hIHZlcm9zaW1pbGl0dWQgZW4gbXUgPSIsIHJvdW5kKG11X01MRSwgMykpLAogICAgeCA9IGV4cHJlc3Npb24obXUpLAogICAgeSA9ICJMb2ctdmVyb3NpbWlsaXR1ZCIKICApICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKIyMgNC40KSBFc3RpbWFkb3JlcyBNTEUgcGFyYSB1bmEgbm9ybWFsCgpMb3MgZXN0aW1hZG9yZXMgZGUgbcOheGltYSB2ZXJvc2ltaWxpdHVkIChNTEUpIHBhcmEgdW5hIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIHBlcm1pdGVuIGVzdGltYXIgbG9zIHBhcsOhbWV0cm9zIGRlc2Nvbm9jaWRvcyBkZSBsYSBkaXN0cmlidWNpw7NuOiBsYSBtZWRpYSAkXG11JCB5IGxhIHZhcmlhbnphICRcc2lnbWFeMiQuIERhZG9zIGRhdG9zIGluZGVwZW5kaWVudGVzICR4XzEsIHhfMiwgXGRvdHMsIHhfbiQgcHJvdmVuaWVudGVzIGRlIHVuYSBkaXN0cmlidWNpw7NuICRcbWF0aGNhbHtOfShcbXUsIFxzaWdtYV4yKSQsIGVsIG9iamV0aXZvIGVzIGVuY29udHJhciBsb3MgdmFsb3JlcyBkZSAkXG11JCB5ICRcc2lnbWFeMiQgcXVlIG1heGltaXphbiBsYSBmdW5jacOzbiBkZSB2ZXJvc2ltaWxpdHVkLCBlcyBkZWNpciwgcXVlIGhhY2VuIG3DoXMgcHJvYmFibGUgaGFiZXIgb2JzZXJ2YWRvIGVzb3MgZGF0b3MuIExhIGZ1bmNpw7NuIGRlIGxvZy12ZXJvc2ltaWxpdHVkIHBhcmEgZXN0b3MgZGF0b3MgZXM6CgokJApcZWxsKFxtdSwgXHNpZ21hXjIpID0gLVxmcmFje259ezJ9IFxsb2coMlxwaSkgLSBcZnJhY3tufXsyfSBcbG9nKFxzaWdtYV4yKSAtIFxmcmFjezF9ezJcc2lnbWFeMn0gXHN1bV97aT0xfV5uICh4X2kgLSBcbXUpXjIKJCQKCkFsIGRlcml2YXIgZXN0YSBleHByZXNpw7NuIHJlc3BlY3RvIGEgJFxtdSQgeSAkXHNpZ21hXjIkIHkgcmVzb2x2ZXIgbGFzIGVjdWFjaW9uZXMgZGUgcHJpbWVyIG9yZGVuLCBzZSBvYnRpZW5lbiBsb3Mgc2lndWllbnRlcyBlc3RpbWFkb3JlcyBNTEU6CgokJApcaGF0e1xtdX0gPSBcZnJhY3sxfXtufSBcc3VtX3tpPTF9Xm4geF9pCiQkCgpFc3RvcyBlc3RpbWFkb3JlcyBzZSB1c2FuIGFtcGxpYW1lbnRlIGVuIGVzdGFkw61zdGljYSBwb3JxdWUgc29uIGNvbnNpc3RlbnRlcywgZWZpY2llbnRlcyB5IHRpZW5lbiBidWVuYXMgcHJvcGllZGFkZXMgYXNpbnTDs3RpY2FzLiBTZSBhcGxpY2FuIGVuIG3Dumx0aXBsZXMgw6FyZWFzIGNvbW8gZWNvbm9tZXRyw61hLCBwc2ljb2xvZ8OtYSwgYmlvbG9nw61hIHkgbWFjaGluZSBsZWFybmluZywgZXNwZWNpYWxtZW50ZSBhbCBhanVzdGFyIG1vZGVsb3MgcHJvYmFiaWzDrXN0aWNvcywgcmVhbGl6YXIgaW5mZXJlbmNpYXMgeSBjb25zdHJ1aXIgaW50ZXJ2YWxvcyBkZSBjb25maWFuemEuIEVzIGltcG9ydGFudGUgbm90YXIgcXVlICRcaGF0e1xzaWdtYX1eMiQgKE1MRSkgZXMgdW4gZXN0aW1hZG9yIHNlc2dhZG8gcG9ycXVlIGRpdmlkZSBlbnRyZSAkbiQsIHBlcm8gZXMgZWwgYWRlY3VhZG8gZGVudHJvIGRlbCBtYXJjbyBkZSBtw6F4aW1hIHZlcm9zaW1pbGl0dWQuCgoqKkVzdGltYWRvciBkZSBtw6F4aW1hIHZlcm9zaW1pbGl0dWQgcGFyYSBsYSB2YXJpYW56YToqKgoKJCQKXGhhdHtcc2lnbWF9XjIgPSBcZnJhY3sxfXtufSBcc3VtX3tpPTF9Xm4gKHhfaSAtIFxoYXR7XG11fSleMgokJAoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCgojIFBhc28gMTogU2ltdWxhciBkYXRvcyBkZSB1bmEgbm9ybWFsCnNldC5zZWVkKDEyMykKbiA8LSAxMDAKbXVfcmVhbCA8LSA1CnNpZ21hX3JlYWwgPC0gMgpkYXRvcyA8LSBybm9ybShuLCBtZWFuID0gbXVfcmVhbCwgc2QgPSBzaWdtYV9yZWFsKQoKIyBQYXNvIDI6IEVzdGltYWRvcmVzIE1MRQptdV9tbGUgPC0gbWVhbihkYXRvcykKc2lnbWEyX21sZSA8LSBtZWFuKChkYXRvcyAtIG11X21sZSleMikKc2lnbWFfbWxlIDwtIHNxcnQoc2lnbWEyX21sZSkKCiMgUGFzbyAzOiBHcmFmaWNhciBoaXN0b2dyYW1hICsgZGVuc2lkYWQgbm9ybWFsIGNvbiBwYXLDoW1ldHJvcyBNTEUKZGYgPC0gZGF0YS5mcmFtZSh4ID0gZGF0b3MpCgpnZ3Bsb3QoZGYsIGFlcyh4ID0geCkpICsKICBnZW9tX2hpc3RvZ3JhbShhZXMoeSA9IC4uZGVuc2l0eS4uKSwgYmlucyA9IDMwLCBmaWxsID0gImxpZ2h0Ymx1ZSIsIGNvbG9yID0gImJsYWNrIiwgYWxwaGEgPSAwLjcpICsKICBzdGF0X2Z1bmN0aW9uKGZ1biA9IGRub3JtLCBhcmdzID0gbGlzdChtZWFuID0gbXVfbWxlLCBzZCA9IHNpZ21hX21sZSksCiAgICAgICAgICAgICAgICBjb2xvciA9ICJyZWQiLCBzaXplID0gMS4yKSArCiAgbGFicyh0aXRsZSA9ICJFc3RpbWFjacOzbiBwb3IgbcOheGltYSB2ZXJvc2ltaWxpdHVkIiwKICAgICAgIHN1YnRpdGxlID0gYnF1b3RlKGhhdChtdSkgPT0gLihyb3VuZChtdV9tbGUsIDIpKSB+ICIsIiB+IGhhdChzaWdtYSleMiA9PSAuKHJvdW5kKHNpZ21hMl9tbGUsIDIpKSksCiAgICAgICB4ID0gIkRhdG9zIHNpbXVsYWRvcyIsCiAgICAgICB5ID0gIkRlbnNpZGFkIikgKwogIHRoZW1lX21pbmltYWwoKQoKYGBgCgojIyA0LjUpIFRlc3QgZGUgaGlwb3Rlc2lzIG11bHRpdmFyaWFudGUKCkVsIHRlc3QgZGUgaGlww7N0ZXNpcyBtdWx0aXZhcmlhbnRlIGVzIHVuYSBleHRlbnNpw7NuIGRlbCBjb250cmFzdGUgZGUgaGlww7N0ZXNpcyB1bml2YXJpYWRvIGFsIGNhc28gZG9uZGUgc2UgYW5hbGl6YW4gbcO6bHRpcGxlcyB2YXJpYWJsZXMgYWxlYXRvcmlhcyBzaW11bHTDoW5lYW1lbnRlLiBFc3RlIHRpcG8gZGUgcHJ1ZWJhcyBwZXJtaXRlIGRldGVybWluYXIgc2kgZXhpc3RlbiBkaWZlcmVuY2lhcyBzaWduaWZpY2F0aXZhcyBlbnRyZSBncnVwb3MgbyBzaSB1biBjb25qdW50byBkZSBtZWRpYXMgdmVjdG9yaWFsZXMgZXMgaWd1YWwgYSB1biB2YWxvciBlc3BlY8OtZmljbywgY29uc2lkZXJhbmRvIGFkZW3DoXMgbGEgY29ycmVsYWNpw7NuIGVudHJlIHZhcmlhYmxlcy4gRW4gbHVnYXIgZGUgdHJhYmFqYXIgY29uIHVuYSBtZWRpYSBlc2NhbGFyLCBzZSB0cmFiYWphIGNvbiB1bmEgbWVkaWEgdmVjdG9yaWFsICRcYm9sZHN5bWJvbHtcbXV9JCB5IHVuYSBtYXRyaXogZGUgY292YXJpYW56YSAkXGJvbGRzeW1ib2x7XFNpZ21hfSQuIFVubyBkZSBsb3MgY2Fzb3MgbcOhcyBjb211bmVzIGVzIGVsIHRlc3QgZGUgSG90ZWxsaW5nICRUXjIkLCBxdWUgZXMgZWwgYW7DoWxvZ28gbXVsdGl2YXJpYW50ZSBkZSBsYSBwcnVlYmEgJHQkIGRlIFN0dWRlbnQuIFBvciBlamVtcGxvLCBzaSBzZSB0aWVuZSB1bmEgbXVlc3RyYSBkZSB2ZWN0b3JlcyBhbGVhdG9yaW9zICRcbWF0aGJme1h9XzEsIFxkb3RzLCBcbWF0aGJme1h9X24kIHByb3ZlbmllbnRlcyBkZSB1bmEgZGlzdHJpYnVjacOzbiBub3JtYWwgbXVsdGl2YXJpYWRhIGNvbiBtZWRpYSAkXGJvbGRzeW1ib2x7XG11fSQgeSBzZSBkZXNlYSBjb250cmFzdGFyIGxhIGhpcMOzdGVzaXMgbnVsYSAkSF8wOiBcYm9sZHN5bWJvbHtcbXV9ID0gXGJvbGRzeW1ib2x7XG11fV8wJCwgZWwgZXN0YWTDrXN0aWNvIGRlIHBydWViYSBlczoKCiQkClReMiA9IG4oXGJhcntcbWF0aGJme1h9fSAtIFxib2xkc3ltYm9se1xtdX1fMCleXHRvcCBcbWF0aGJme1N9XnstMX0oXGJhcntcbWF0aGJme1h9fSAtIFxib2xkc3ltYm9se1xtdX1fMCkKJCQKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CgoKIyBTaW11bGFyIGRhdG9zIGJpdmFyaWFkb3MKc2V0LnNlZWQoMTIzKQpuIDwtIDMwCm11IDwtIGMoNSwgMTApCnNpZ21hIDwtIG1hdHJpeChjKDEsIDAuOCwgMC44LCAxKSwgbnJvdyA9IDIpCgpYIDwtIG12cm5vcm0obiwgbXUgPSBtdSwgU2lnbWEgPSBzaWdtYSkKWF9kZiA8LSBhcy5kYXRhLmZyYW1lKFgpCmNvbG5hbWVzKFhfZGYpIDwtIGMoIlgxIiwgIlgyIikKCiMgSGlww7N0ZXNpcyBudWxhOiBtdSA9ICg1LCAxMCkKbXUwIDwtIGMoNSwgMTApCgojIENhbGN1bGFyIG1lZGlhIG11ZXN0cmFsClhfYmFyIDwtIGNvbE1lYW5zKFhfZGYpCgojIENhbGN1bGFyIG1hdHJpeiBkZSBjb3ZhcmlhbnphClMgPC0gY292KFhfZGYpCgojIENhbGN1bGFyIGVzdGFkw61zdGljbyBUwrIgZGUgSG90ZWxsaW5nIG1hbnVhbG1lbnRlCmRpZmYgPC0gWF9iYXIgLSBtdTAKVDIgPC0gbiAqIHQoZGlmZikgJSolIHNvbHZlKFMpICUqJSBkaWZmCgojIFRyYW5zZm9ybWFyIGEgZXN0YWTDrXN0aWNvIEYKcCA8LSBuY29sKFhfZGYpCkZfc3RhdCA8LSAoKG4gLSBwKSAvIChwICogKG4gLSAxKSkpICogVDIKcF92YWxvciA8LSAxIC0gcGYoRl9zdGF0LCBwLCBuIC0gcCkKCiMgTW9zdHJhciByZXN1bHRhZG9zCmNhdCgiRXN0YWTDrXN0aWNvIFTCsjoiLCBUMiwgIlxuIikKY2F0KCJFc3RhZMOtc3RpY28gRjoiLCBGX3N0YXQsICJcbiIpCmNhdCgiVmFsb3IgcDoiLCBwX3ZhbG9yLCAiXG4iKQoKIyBHcmFmaWNhciBsb3MgZGF0b3MgeSBsYXMgbWVkaWFzCmdncGxvdChYX2RmLCBhZXMoeCA9IFgxLCB5ID0gWDIpKSArCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuNiwgY29sb3IgPSAiYmx1ZSIpICsKICBnZW9tX3BvaW50KGFlcyh4ID0gbXUwWzFdLCB5ID0gbXUwWzJdKSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDQsIHNoYXBlID0gNCkgKwogIGdlb21fcG9pbnQoYWVzKHggPSBYX2JhclsxXSwgeSA9IFhfYmFyWzJdKSwgY29sb3IgPSAiZGFya2dyZWVuIiwgc2l6ZSA9IDQsIHNoYXBlID0gMTgpICsKICBsYWJzKAogICAgdGl0bGUgPSAiVGVzdCBkZSBIb3RlbGxpbmcgVMKyIiwKICAgIHN1YnRpdGxlID0gcGFzdGUwKCJUwrIgPSAiLCByb3VuZChUMiwgMiksIAogICAgICAgICAgICAgICAgICAgICAgIiwgcC12YWxvciA9ICIsIHJvdW5kKHBfdmFsb3IsIDQpKSwKICAgIHggPSAiVmFyaWFibGUgMSIsIHkgPSAiVmFyaWFibGUgMiIKICApICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKZG9uZGUgJFxiYXJ7XG1hdGhiZntYfX0kIGVzIGxhIG1lZGlhIG11ZXN0cmFsIHkgJFxtYXRoYmZ7U30kIGVzIGxhIG1hdHJpeiBkZSBjb3ZhcmlhbnphIG11ZXN0cmFsLiBFc3RlIGVzdGFkw61zdGljbywgYmFqbyAkSF8wJCwgc2UgZGlzdHJpYnV5ZSBjb21vIHVuYSBGIGRlIEZpc2hlciBhanVzdGFkYS4gTG9zIHRlc3RzIGRlIGhpcMOzdGVzaXMgbXVsdGl2YXJpYW50ZXMgc29uIGFtcGxpYW1lbnRlIHVzYWRvcyBlbiBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSwgZXhwZXJpbWVudG9zIGNvbiB2YXJpYXMgcmVzcHVlc3RhcyBkZXBlbmRpZW50ZXMsIHkgdmFsaWRhY2nDs24gZGUgbW9kZWxvcyBjb24gbcO6bHRpcGxlcyBwcmVkaWN0b3Jlcy4KCiMjIDQuNikgQ29udHJhc3RlIHBhcmEgbGEgbWVkaWEgZGUgdW5hIG5vcm1hbCBtdWx0aXZhcmlhbnRlCgpFbCBjb250cmFzdGUgcGFyYSBsYSBtZWRpYSBkZSB1bmEgbm9ybWFsIG11bHRpdmFyaWFudGUgZXMgdW5hIHBydWViYSBlc3RhZMOtc3RpY2EgcXVlIHBlcm1pdGUgZGV0ZXJtaW5hciBzaSBsYSBtZWRpYSBwb2JsYWNpb25hbCBkZSB1biB2ZWN0b3IgYWxlYXRvcmlvIG11bHRpdmFyaWFudGUgJFxtYXRoYmZ7WH0gXHNpbSBcbWF0aGNhbHtOfV9wKFxib2xkc3ltYm9se1xtdX0sIFxib2xkc3ltYm9se1xTaWdtYX0pJCBlcyBpZ3VhbCBhIHVuIHZhbG9yIGVzcGVjw61maWNvICRcYm9sZHN5bWJvbHtcbXV9XzAkLiBFc3RlIGNvbnRyYXN0ZSBlcyB1bmEgZXh0ZW5zacOzbiBtdWx0aXZhcmlhZGEgZGUgbGEgcHJ1ZWJhIHQgZGUgU3R1ZGVudCB1bml2YXJpYWRhIHkgc2UgdXRpbGl6YSBjdWFuZG8gc2UgZGVzZWEgY29tcHJvYmFyIHNpIHVuIGNvbmp1bnRvIGRlIG1lZGlhcyBkZSB2YXJpYXMgdmFyaWFibGVzIHJlbGFjaW9uYWRhcyBjb2luY2lkZSBjb24gdW4gdmVjdG9yIGRlIG1lZGlhcyB0ZcOzcmljYXMuIEVzIGFtcGxpYW1lbnRlIHVzYWRvIGVuIGFuw6FsaXNpcyBkZSBwZXJmaWxlcywgY2FsaWRhZCBpbmR1c3RyaWFsLCBlc3R1ZGlvcyBjbMOtbmljb3MgeSBlY29ub23DrWEgbXVsdGl2YXJpYWRhLgoKRWwgcHJvY2VkaW1pZW50byBzZSBiYXNhIGVuIGVsIGVzdGFkw61zdGljbyBkZSBIb3RlbGxpbmcgJFReMiQsIGRlZmluaWRvIGNvbW86CgokJApUXjIgPSBuIChcYmFye1xtYXRoYmZ7WH19IC0gXGJvbGRzeW1ib2x7XG11fV8wKV5cdG9wIFxtYXRoYmZ7U31eey0xfSAoXGJhcntcbWF0aGJme1h9fSAtIFxib2xkc3ltYm9se1xtdX1fMCkKJCQKCmRvbmRlICRuJCBlcyBlbCB0YW1hw7FvIG11ZXN0cmFsLCAkXGJhcntcbWF0aGJme1h9fSQgZXMgbGEgbWVkaWEgbXVlc3RyYWwsICRcbWF0aGJme1N9JCBlcyBsYSBtYXRyaXogZGUgY292YXJpYW56YSBtdWVzdHJhbCB5ICRcYm9sZHN5bWJvbHtcbXV9XzAkIGVzIGVsIHZlY3RvciBkZSBtZWRpYXMgYmFqbyBsYSBoaXDDs3Rlc2lzIG51bGEgJEhfMDogXGJvbGRzeW1ib2x7XG11fSA9IFxib2xkc3ltYm9se1xtdX1fMCQuIEJham8gJEhfMCQsIGVsIGVzdGFkw61zdGljbyAkVF4yJCBwdWVkZSB0cmFuc2Zvcm1hcnNlIGVuIHVuYSBlc3RhZMOtc3RpY2EgJEYkIGNvbiAkcCQgeSAkbi1wJCBncmFkb3MgZGUgbGliZXJ0YWQsIHBlcm1pdGllbmRvIGNhbGN1bGFyIGVsIHZhbG9yLXAgcGFyYSB0b21hciB1bmEgZGVjaXNpw7NuLgoKRXN0ZSBjb250cmFzdGUgZXMgw7p0aWwgY3VhbmRvIHNlIHRyYWJhamEgY29uIGRhdG9zIGRvbmRlIGxhcyB2YXJpYWJsZXMgZXN0w6FuIGNvcnJlbGFjaW9uYWRhcyBlbnRyZSBzw60geSBzZSBuZWNlc2l0YSBldmFsdWFyIGhpcMOzdGVzaXMgc29icmUgbcO6bHRpcGxlcyBtZWRpYXMgc2ltdWx0w6FuZWFtZW50ZS4gUG9yIGVqZW1wbG8sIGVuIHVuIGVzdHVkaW8gbnV0cmljaW9uYWwgY29uIHZhcmlhcyBtZWRpY2lvbmVzIChjYWxvcsOtYXMsIHByb3Rlw61uYXMsIGdyYXNhcywgZXRjLiksIHBvZHLDrWFtb3MgcHJvYmFyIHNpIGVsIHByb21lZGlvIGNvbmp1bnRvIGRlIGVzdGFzIHZhcmlhYmxlcyBlbiB1bmEgbXVlc3RyYSBjb2luY2lkZSBjb24gbGFzIHJlY29tZW5kYWNpb25lcyBudXRyaWNpb25hbGVzIG9maWNpYWxlcy4KCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDEuIFNpbXVsYXIgZGF0b3MKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CnNldC5zZWVkKDEyMykKbiA8LSA1MAptdSA8LSBjKDIsIDMpClNpZ21hIDwtIG1hdHJpeChjKDEsIDAuNSwgMC41LCAyKSwgbnJvdyA9IDIpCmRhdG9zIDwtIG12cm5vcm0obiwgbXUsIFNpZ21hKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMi4gQ29udHJhc3RlIGRlIGhpcMOzdGVzaXMKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09Cm11MCA8LSBjKDIsIDMpCnRlc3QgPC0gSG90ZWxsaW5nc1QyKGRhdG9zLCBtdSA9IG11MCkKcHJpbnQodGVzdCkKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDMuIE1lZGlhIG11ZXN0cmFsCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQptZWRpYV9tdWVzdHJhbCA8LSBjb2xNZWFucyhkYXRvcykKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDQuIEdyw6FmaWNvIGNvbiBlbGlwc2UgOTUlCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQpnZ3Bsb3QoYXMuZGF0YS5mcmFtZShkYXRvcyksIGFlcyh4ID0gVjEsIHkgPSBWMikpICsKICBnZW9tX3BvaW50KGNvbG9yID0gInN0ZWVsYmx1ZSIsIGFscGhhID0gMC42KSArCiAgc3RhdF9lbGxpcHNlKGxldmVsID0gMC45NSwgY29sb3IgPSAiYmxhY2siLCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArCiAgZ2VvbV9wb2ludChhZXMoeCA9IG11MFsxXSwgeSA9IG11MFsyXSksIGNvbG9yID0gInJlZCIsIHNpemUgPSAzKSArCiAgZ2VvbV9wb2ludChhZXMoeCA9IG1lZGlhX211ZXN0cmFsWzFdLCB5ID0gbWVkaWFfbXVlc3RyYWxbMl0pLCBjb2xvciA9ICJkYXJrZ3JlZW4iLCBzaXplID0gMykgKwogIGxhYnMoeCA9ICJWYXJpYWJsZSAxIiwgeSA9ICJWYXJpYWJsZSAyIiwKICAgICAgIHRpdGxlID0gIkVsaXBzZSBkZSBjb25maWFuemEgZGVsIDk1JSBwYXJhIGxhIG1lZGlhIG11bHRpdmFyaWFkYSIsCiAgICAgICBzdWJ0aXRsZSA9ICJSb2pvOiDOvOKCgCBiYWpvIEjigoAgfCBWZXJkZTogbWVkaWEgbXVlc3RyYWwiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpgYGAKCiMjIDQuNykgQ29udHJhc3RlIHBhcmEgbGEgbWF0cml6IGRlIGNvdmFyaWFuemEgZGUgdW5hIG5vcm1hbCBtdWx0aXZhcmlhbnRlCgpFbCBjb250cmFzdGUgcGFyYSBsYSBtYXRyaXogZGUgY292YXJpYW56YSBkZSB1bmEgbm9ybWFsIG11bHRpdmFyaWFudGUgcGVybWl0ZSBldmFsdWFyIHNpIGxhIG1hdHJpeiBkZSBjb3ZhcmlhbnphIHBvYmxhY2lvbmFsICRcYm9sZHN5bWJvbHtcU2lnbWF9JCBjdW1wbGUgY29uIHVuYSBlc3RydWN0dXJhIGVzcGVjw61maWNhLiBFc3RlIHRlc3QgZXMgZnVuZGFtZW50YWwgZW4gYW7DoWxpc2lzIG11bHRpdmFyaWFkbywgeWEgcXVlIG11Y2hvcyBtw6l0b2RvcyAoY29tbyBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSBvIE1BTk9WQSkgc3Vwb25lbiBxdWUgbGFzIHBvYmxhY2lvbmVzIHRpZW5lbiBtYXRyaWNlcyBkZSBjb3ZhcmlhbnphIGlndWFsZXMuIExhIGhpcMOzdGVzaXMgbnVsYSB0w61waWNhIGVzOgoKJCQKSF8wIDogXGJvbGRzeW1ib2x7XFNpZ21hfSA9IFxib2xkc3ltYm9se1xTaWdtYX1fMAokJAoKZG9uZGUgJFxib2xkc3ltYm9se1xTaWdtYX1fMCQgZXMgdW5hIG1hdHJpeiBkZSBjb3ZhcmlhbnphIGNvbm9jaWRhIG8gYmFqbyBhbGd1bmEgZXN0cnVjdHVyYSAocG9yIGVqZW1wbG8sIG1hdHJpeiBkaWFnb25hbCwgaWRlbnRpZGFkLCBvIGNvbcO6biBlbnRyZSBncnVwb3MpLgoKVW4gY2FzbyBtdXkgdXNhZG8gZXMgZWwgdGVzdCBkZSBlc2ZlcmljaWRhZCwgZW4gZWwgY3VhbCBzZSBjb250cmFzdGE6CgokJApIXzAgOiBcYm9sZHN5bWJvbHtcU2lnbWF9ID0gXHNpZ21hXjIgXG1hdGhiZntJfQokJAoKbG8gY3VhbCBpbXBsaWNhIHF1ZSB0b2RhcyBsYXMgdmFyaWFibGVzIHRpZW5lbiBpZ3VhbCB2YXJpYW56YSB5IHNvbiBpbmNvcnJlbGFkYXMuIE90cm8gdGVzdCBjb23Dum4gZXMgZWwgZGUgaWd1YWxkYWQgZGUgbWF0cmljZXMgZGUgY292YXJpYW56YSBlbnRyZSBncnVwb3MsIGZ1bmRhbWVudGFsIGVuIHTDqWNuaWNhcyBjb21vIGxhIExEQSAoTGluZWFyIERpc2NyaW1pbmFudCBBbmFseXNpcykuIEVzdG9zIGNvbnRyYXN0ZXMgc3VlbGVuIGJhc2Fyc2UgZW4gZWwgZXN0YWTDrXN0aWNvIGRlIHJhesOzbiBkZSB2ZXJvc2ltaWxpdHVkLCBxdWUgYmFqbyBjaWVydGFzIGNvbmRpY2lvbmVzIHNlIGFwcm94aW1hIGEgdW5hIGRpc3RyaWJ1Y2nDs24gJFxjaGleMiQuCgpFc3RlIHRpcG8gZGUgcHJ1ZWJhcyBlcyBjcnVjaWFsIHBhcmEgdmFsaWRhciBzdXB1ZXN0b3MgYW50ZXMgZGUgYXBsaWNhciBtb2RlbG9zIG11bHRpdmFyaWFudGVzLiBQb3IgZWplbXBsbywgZW4gdW4gYW7DoWxpc2lzIGRpc2NyaW1pbmFudGUsIHNpIGxhcyBtYXRyaWNlcyBkZSBjb3ZhcmlhbnphIGRpZmllcmVuIGVudHJlIGdydXBvcywgbGEgdMOpY25pY2EgY2zDoXNpY2EgKExEQSkgbm8gZXMgYWRlY3VhZGEsIHkgc2UgcHJlZmllcmUgUURBIChRdWFkcmF0aWMgRGlzY3JpbWluYW50IEFuYWx5c2lzKS4gVGFtYmnDqW4gc2UgdXNhIHBhcmEgdmFsaWRhciBlc3RydWN0dXJhcyBlbiBtb2RlbG9zIGNvbW8gZWwgYW7DoWxpc2lzIGZhY3RvcmlhbCBvIG1vZGVsb3MgZGUgZWN1YWNpb25lcyBlc3RydWN0dXJhbGVzLgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCgoKIyBTaW11bGFyIGRhdG9zIG5vcm1hbGVzIG11bHRpdmFyaWFudGVzCnNldC5zZWVkKDEyMykKbXUgPC0gYygwLCAwLCAwKQpTaWdtYSA8LSBtYXRyaXgoYygxLCAwLjUsIDAuMywKICAgICAgICAgICAgICAgICAgMC41LCAxLCAwLjQsCiAgICAgICAgICAgICAgICAgIDAuMywgMC40LCAxKSwgbnJvdyA9IDMpCmRhdG9zIDwtIG12cm5vcm0obiA9IDEwMCwgbXUgPSBtdSwgU2lnbWEgPSBTaWdtYSkKCiMgQXBsaWNhciBlbCB0ZXN0IGRlIGVzZmVyaWNpZGFkIGRlIEJhcnRsZXR0CnJlc3VsdGFkbyA8LSBjb3J0ZXN0LmJhcnRsZXR0KGNvcihkYXRvcyksIG4gPSBucm93KGRhdG9zKSkKcHJpbnQocmVzdWx0YWRvKQoKIyBWaXN1YWxpemFjacOzbjogcGFyZXMgZGUgZGlzcGVyc2nDs24KcGFpcnMoZGF0b3MsIG1haW4gPSAiRGlzcGVyc2nDs24gZGUgdmFyaWFibGVzIG11bHRpdmFyaWFkYXMiKQoKYGBgCgojIyA0LjgpIENvbnRyYXN0ZSBkZSBpZ3VhbGRhZCBkZSB2YXJpYXMgbWVkaWFzCgpFbCBjb250cmFzdGUgZGUgaWd1YWxkYWQgZGUgdmFyaWFzIG1lZGlhcyBlbiBlbCBjb250ZXh0byBtdWx0aXZhcmlhbnRlIGV2YWzDumEgc2kgdmFyaW9zIGdydXBvcyBwb2JsYWNpb25hbGVzIGNvbXBhcnRlbiBsYSBtaXNtYSBtZWRpYSB2ZWN0b3JpYWwgbXVsdGl2YXJpYWRhLiBFc3RhIHBydWViYSBnZW5lcmFsaXphIGVsIGFuw6FsaXNpcyB1bml2YXJpYWRvIGRlIHZhcmlhbnphIChBTk9WQSkgYWwgY2FzbyBlbiBxdWUgY2FkYSBvYnNlcnZhY2nDs24gZXMgdW4gdmVjdG9yIGVuIGx1Z2FyIGRlIHVuIHNvbG8gdmFsb3IgZXNjYWxhci4gRXMgZXNwZWNpYWxtZW50ZSDDunRpbCBjdWFuZG8gc2UgdGllbmVuIG3Dumx0aXBsZXMgdmFyaWFibGVzIGRlcGVuZGllbnRlcyB5IHNlIGRlc2VhIHNhYmVyIHNpIGxhIG1lZGlhIGNvbmp1bnRhIGRlIGVzYXMgdmFyaWFibGVzIGRpZmllcmUgZW50cmUgZ3J1cG9zLiBMYSBoaXDDs3Rlc2lzIG51bGEgcGxhbnRlYSBxdWUgdG9kYXMgbGFzIHBvYmxhY2lvbmVzIHRpZW5lbiBsYSBtaXNtYSBtZWRpYSB2ZWN0b3JpYWw6CgokJApIXzA6IFxib2xkc3ltYm9se1xtdX1fMSA9IFxib2xkc3ltYm9se1xtdX1fMiA9IFxkb3RzID0gXGJvbGRzeW1ib2x7XG11fV9nCiQkCgpmcmVudGUgYSBsYSBhbHRlcm5hdGl2YQoKJCQKSF8xOiBcdGV4dHthbCBtZW5vcyB1bmEgfSBcYm9sZHN5bWJvbHtcbXV9X2kgXHRleHR7IGRpZmllcmV9CiQkCgpQYXJhIHJlYWxpemFyIGVzdGUgY29udHJhc3RlIHNlIHV0aWxpemEgZWwgTUFOT1ZBIChNdWx0aXZhcmlhdGUgQW5hbHlzaXMgb2YgVmFyaWFuY2UpLiBMYSBlc3RhZMOtc3RpY2EgZGUgcHJ1ZWJhIG3DoXMgY29tw7puIGVzIGVsIGNyaXRlcmlvIGRlIFdpbGtz4oCZIExhbWJkYSAoJFxMYW1iZGEkKSwgZGVmaW5pZG8gY29tbyBsYSByYXrDs24gZW50cmUgZWwgZGV0ZXJtaW5hbnRlIGRlbCByZXNpZHVvIChkZW50cm8gZGUgbG9zIGdydXBvcykgeSBlbCBkZXRlcm1pbmFudGUgdG90YWw6CgokJApcTGFtYmRhID0gXGZyYWN7fFxtYXRoYmZ7V318fXt8XG1hdGhiZntUfXx9CiQkCgpkb25kZSAkXG1hdGhiZntXfSQgZXMgbGEgbWF0cml6IGRlIHN1bWFzIGRlIGN1YWRyYWRvcyB5IHByb2R1Y3RvcyBkZW50cm8gZGUgbG9zIGdydXBvcywgeSAkXG1hdGhiZntUfSQgZXMgbGEgbWF0cml6IHRvdGFsIChzdW1hIGRlICRcbWF0aGJme1d9JCB5ICRcbWF0aGJme0J9JCwgbGEgbWF0cml6IGVudHJlIGdydXBvcykuIFVuIHZhbG9yIHBlcXVlw7FvIGRlICRcTGFtYmRhJCBzdWdpZXJlIGRpZmVyZW5jaWFzIHNpZ25pZmljYXRpdmFzIGVudHJlIGxhcyBtZWRpYXMuCgpFc3RlIHRpcG8gZGUgcHJ1ZWJhIGVzIMO6dGlsIGVuIGNvbnRleHRvcyBjb21vIHBzaWNvbG9nw61hLCBtZWRpY2luYSBvIGVjb25vbcOtYSwgZG9uZGUgc2UgbWlkZSBhIHZhcmlvcyBncnVwb3MgZW4gdmFyaWFzIGRpbWVuc2lvbmVzIHNpbXVsdMOhbmVhbWVudGUuIFBvciBlamVtcGxvLCBjb21wYXJhciBkaXN0aW50b3MgbcOpdG9kb3MgZGUgZW5zZcOxYW56YSBldmFsdWFuZG8gbcO6bHRpcGxlcyBoYWJpbGlkYWRlcyBhY2Fkw6ltaWNhcyBlbiBlc3R1ZGlhbnRlcy4KCiMjIDQuOSkgRWplbXBsbyBjb24gcHl0aG9uCgoKCmBgYHtweXRob259CgojIDQuMSkgRXN0YWTDrXN0aWNvcyBtdWVzdHJhbGVzIG11bHRpdmFyaWFudGVzCmlyaXMgPSBsb2FkX2lyaXMoKQpkZl9pcmlzID0gcGQuRGF0YUZyYW1lKGlyaXMuZGF0YSwgY29sdW1ucz1pcmlzLmZlYXR1cmVfbmFtZXMpCgpwcmludCgiVmVjdG9yIGRlIG1lZGlhcyBtdWVzdHJhbDoiKQpwcmludChkZl9pcmlzLm1lYW4oKSkKCnByaW50KCJcbk1hdHJpeiBkZSBjb3ZhcmlhbnphczoiKQpwcmludChkZl9pcmlzLmNvdigpKQoKcHJpbnQoIlxuTWF0cml6IGRlIGNvcnJlbGFjaW9uZXM6IikKcHJpbnQoZGZfaXJpcy5jb3JyKCkpCgpzbnMucGFpcnBsb3QoZGZfaXJpcykKcGx0LnN1cHRpdGxlKCJHcsOhZmljbyBkZSBkaXNwZXJzacOzbiBtdWx0aXZhcmlhZG8gLSBEYXRhc2V0IElyaXMiLCB5PTEuMDIpCnBsdC5zaG93KCkKCiMgNC4yKSBUZW9yZW1hIGNlbnRyYWwgZGVsIGzDrW1pdGUgbXVsdGl2YXJpYW50ZQpucC5yYW5kb20uc2VlZCgxMjMpCm11ID0gbnAuYXJyYXkoWzMsIDddKQpTaWdtYSA9IG5wLmFycmF5KFtbMSwgMC44XSwgWzAuOCwgMl1dKQpuID0gNTAKcmVwcyA9IDEwMDAKCnNhbXBsZXMgPSBucC5yYW5kb20ubXVsdGl2YXJpYXRlX25vcm1hbChtdSwgU2lnbWEsIHNpemU9KHJlcHMsIG4pKQpzYW1wbGVfbWVhbnMgPSBzYW1wbGVzLm1lYW4oYXhpcz0xKQoKZGZfbWVhbnMgPSBwZC5EYXRhRnJhbWUoc2FtcGxlX21lYW5zLCBjb2x1bW5zPVsiWDEiLCAiWDIiXSkKc25zLnNjYXR0ZXJwbG90KGRhdGE9ZGZfbWVhbnMsIHg9IlgxIiwgeT0iWDIiLCBhbHBoYT0wLjMsIGNvbG9yPSJzdGVlbGJsdWUiKQpzbnMua2RlcGxvdChkYXRhPWRmX21lYW5zLCB4PSJYMSIsIHk9IlgyIiwgbGV2ZWxzPTUsIGNvbG9yPSJyZWQiKQpwbHQudGl0bGUoZiJEaXN0cmlidWNpw7NuIGRlIG1lZGlhcyBtdWVzdHJhbGVzIGJpdmFyaWFkYXNcblNpbXVsYWNpw7NuIGRlIHtyZXBzfSBtZWRpYXMgbXVlc3RyYWxlcyBjb24gbiA9IHtufSIpCnBsdC54bGFiZWwoIk1lZGlhIG11ZXN0cmFsIGRlIFgxIikKcGx0LnlsYWJlbCgiTWVkaWEgbXVlc3RyYWwgZGUgWDIiKQpwbHQuc2hvdygpCgojIDQuMykgTcOheGltYSB2ZXJvc2ltaWxpdHVkCm5wLnJhbmRvbS5zZWVkKDEyMykKbiA9IDEwMAptdV90cnVlID0gNQpzaWdtYV90cnVlID0gMgpkYXRhID0gbnAucmFuZG9tLm5vcm1hbChtdV90cnVlLCBzaWdtYV90cnVlLCBuKQoKZGVmIGxvZ19saWtlbGlob29kKG11LCBkYXRhLCBzaWdtYSk6CiAgICBuID0gbGVuKGRhdGEpCiAgICByZXR1cm4gLW4vMiAqIG5wLmxvZygyICogbnAucGkgKiBzaWdtYSoqMikgLSBucC5zdW0oKGRhdGEgLSBtdSkqKjIpIC8gKDIgKiBzaWdtYSoqMikKCm11X3ZhbHMgPSBucC5saW5zcGFjZSgzLCA3LCAyMDApCmxsX3ZhbHMgPSBbbG9nX2xpa2VsaWhvb2QobXUsIGRhdGEsIHNpZ21hX3RydWUpIGZvciBtdSBpbiBtdV92YWxzXQptdV9tbGUgPSBtdV92YWxzW25wLmFyZ21heChsbF92YWxzKV0KCnBsdC5wbG90KG11X3ZhbHMsIGxsX3ZhbHMsIGNvbG9yPSJzdGVlbGJsdWUiKQpwbHQuYXh2bGluZShtdV9tbGUsIGNvbG9yPSJyZWQiLCBsaW5lc3R5bGU9Ii0tIikKcGx0LnNjYXR0ZXIobXVfbWxlLCBtYXgobGxfdmFscyksIGNvbG9yPSJyZWQiKQpwbHQudGl0bGUoIkxvZy12ZXJvc2ltaWxpdHVkIHBhcmEgZGlzdGludG9zIHZhbG9yZXMgZGUgzrwiKQpwbHQueGxhYmVsKCLOvCIpCnBsdC55bGFiZWwoIkxvZy12ZXJvc2ltaWxpdHVkIikKcGx0LnNob3coKQoKIyA0LjQpIEVzdGltYWRvcmVzIE1MRSBwYXJhIHVuYSBub3JtYWwKbXVfbWxlID0gbnAubWVhbihkYXRhKQpzaWdtYTJfbWxlID0gbnAubWVhbigoZGF0YSAtIG11X21sZSkqKjIpCnNpZ21hX21sZSA9IG5wLnNxcnQoc2lnbWEyX21sZSkKCnBsdC5oaXN0KGRhdGEsIGJpbnM9MzAsIGRlbnNpdHk9VHJ1ZSwgYWxwaGE9MC43LCBjb2xvcj0ibGlnaHRibHVlIiwgZWRnZWNvbG9yPSJibGFjayIpCnhfdmFscyA9IG5wLmxpbnNwYWNlKG1pbihkYXRhKSwgbWF4KGRhdGEpLCAxMDApCnBsdC5wbG90KHhfdmFscywgbm9ybS5wZGYoeF92YWxzLCBtdV9tbGUsIHNpZ21hX21sZSksIGNvbG9yPSJyZWQiLCBsaW5ld2lkdGg9MikKcGx0LnRpdGxlKCJFc3RpbWFjacOzbiBwb3IgbcOheGltYSB2ZXJvc2ltaWxpdHVkIikKcGx0LnhsYWJlbCgiRGF0b3Mgc2ltdWxhZG9zIikKcGx0LnlsYWJlbCgiRGVuc2lkYWQiKQpwbHQuc2hvdygpCgojIDQuNSkgVGVzdCBkZSBoaXDDs3Rlc2lzIG11bHRpdmFyaWFudGUgKEhvdGVsbGluZyBUwrIpCm5wLnJhbmRvbS5zZWVkKDEyMykKbiA9IDMwCm11ID0gbnAuYXJyYXkoWzUsIDEwXSkKU2lnbWEgPSBucC5hcnJheShbWzEsIDAuOF0sIFswLjgsIDFdXSkKWCA9IG5wLnJhbmRvbS5tdWx0aXZhcmlhdGVfbm9ybWFsKG11LCBTaWdtYSwgbikKWF9iYXIgPSBucC5tZWFuKFgsIGF4aXM9MCkKUyA9IG5wLmNvdihYLCByb3d2YXI9RmFsc2UpCm11MCA9IG5wLmFycmF5KFs1LCAxMF0pCmRpZmYgPSBYX2JhciAtIG11MApUMiA9IG4gKiBkaWZmLlQgQCBucC5saW5hbGcuaW52KFMpIEAgZGlmZgpwID0gWC5zaGFwZVsxXQpGX3N0YXQgPSAoKG4gLSBwKSAvIChwICogKG4gLSAxKSkpICogVDIKcF92YWx1ZSA9IDEgLSBjaGkyLmNkZihUMiwgcCkKCnByaW50KGYiRXN0YWTDrXN0aWNvIFTCsjoge1QyfSIpCnByaW50KGYiRXN0YWTDrXN0aWNvIEY6IHtGX3N0YXR9IikKcHJpbnQoZiJWYWxvciBwOiB7cF92YWx1ZX0iKQoKcGx0LnNjYXR0ZXIoWFs6LCAwXSwgWFs6LCAxXSwgYWxwaGE9MC42LCBjb2xvcj0iYmx1ZSIpCnBsdC5zY2F0dGVyKCptdTAsIGNvbG9yPSJyZWQiLCBzPTEwMCwgbWFya2VyPSJ4IikKcGx0LnNjYXR0ZXIoKlhfYmFyLCBjb2xvcj0iZ3JlZW4iLCBzPTEwMCwgbWFya2VyPSJEIikKcGx0LnRpdGxlKGYiVGVzdCBkZSBIb3RlbGxpbmcgVMKyXG5UwrIgPSB7cm91bmQoVDIsIDIpfSwgcC12YWxvciA9IHtyb3VuZChwX3ZhbHVlLCA0KX0iKQpwbHQueGxhYmVsKCJWYXJpYWJsZSAxIikKcGx0LnlsYWJlbCgiVmFyaWFibGUgMiIpCnBsdC5zaG93KCkKCiMgNC42KSBDb250cmFzdGUgcGFyYSBsYSBtZWRpYSBkZSB1bmEgbm9ybWFsIG11bHRpdmFyaWFudGUKbnAucmFuZG9tLnNlZWQoMTIzKQpuID0gNTAKbXUgPSBucC5hcnJheShbMiwgM10pClNpZ21hID0gbnAuYXJyYXkoW1sxLCAwLjVdLCBbMC41LCAyXV0pCmRhdGFfbXYgPSBucC5yYW5kb20ubXVsdGl2YXJpYXRlX25vcm1hbChtdSwgU2lnbWEsIG4pCm11MCA9IG5wLmFycmF5KFsyLCAzXSkKWF9iYXIgPSBucC5tZWFuKGRhdGFfbXYsIGF4aXM9MCkKUyA9IG5wLmNvdihkYXRhX212LCByb3d2YXI9RmFsc2UpCmRpZmYgPSBYX2JhciAtIG11MApUMiA9IG4gKiBkaWZmLlQgQCBucC5saW5hbGcuaW52KFMpIEAgZGlmZgpwID0gZGF0YV9tdi5zaGFwZVsxXQpwX3ZhbHVlID0gMSAtIGNoaTIuY2RmKFQyLCBwKQoKcHJpbnQoZiJIb3RlbGxpbmcgVMKyOiB7VDJ9LCBwLXZhbG9yOiB7cF92YWx1ZX0iKQoKcGx0LnNjYXR0ZXIoZGF0YV9tdls6LCAwXSwgZGF0YV9tdls6LCAxXSwgYWxwaGE9MC42LCBjb2xvcj0ic3RlZWxibHVlIikKcGx0LnNjYXR0ZXIoKm11MCwgY29sb3I9InJlZCIsIHM9MTAwLCBtYXJrZXI9IngiKQpwbHQuc2NhdHRlcigqWF9iYXIsIGNvbG9yPSJncmVlbiIsIHM9MTAwLCBtYXJrZXI9IkQiKQpwbHQudGl0bGUoIkVsaXBzZSBkZSBjb25maWFuemEgZGVsIDk1JSBwYXJhIGxhIG1lZGlhIG11bHRpdmFyaWFkYSIpCnBsdC54bGFiZWwoIlZhcmlhYmxlIDEiKQpwbHQueWxhYmVsKCJWYXJpYWJsZSAyIikKcGx0LnNob3coKQoKIyA0LjcpIENvbnRyYXN0ZSBwYXJhIGxhIG1hdHJpeiBkZSBjb3ZhcmlhbnphCm5wLnJhbmRvbS5zZWVkKDEyMykKbXUgPSBucC5hcnJheShbMCwgMCwgMF0pClNpZ21hID0gbnAuYXJyYXkoW1sxLCAwLjUsIDAuM10sIFswLjUsIDEsIDAuNF0sIFswLjMsIDAuNCwgMV1dKQpkYXRhX2NvdiA9IG5wLnJhbmRvbS5tdWx0aXZhcmlhdGVfbm9ybWFsKG11LCBTaWdtYSwgMTAwKQpiYXJ0bGV0dF90ZXN0ID0gYmFydGxldHQoKmRhdGFfY292LlQpCnByaW50KCJUZXN0IGRlIGVzZmVyaWNpZGFkIGRlIEJhcnRsZXR0OiIpCnByaW50KGJhcnRsZXR0X3Rlc3QpCgojIDQuOCkgQ29udHJhc3RlIGRlIGlndWFsZGFkIGRlIHZhcmlhcyBtZWRpYXMgKE1BTk9WQSkKZGZfbWFub3ZhID0gcGQuRGF0YUZyYW1lKGRhdGFfY292LCBjb2x1bW5zPVsiWDEiLCAiWDIiLCAiWDMiXSkKZGZfbWFub3ZhWyJHcm91cCJdID0gWyJBIl0gKiA1MCArIFsiQiJdICogNTAKbWFvdiA9IE1BTk9WQS5mcm9tX2Zvcm11bGEoIlgxICsgWDIgKyBYMyB+IEdyb3VwIiwgZGF0YT1kZl9tYW5vdmEpCnByaW50KG1hb3YubXZfdGVzdCgpKQoKYGBgCgojIDUpIEFuw6FsaXNpcyBjb24gY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMKCkVsIEFuw6FsaXNpcyBjb24gQ29tcG9uZW50ZXMgUHJpbmNpcGFsZXMgKFBDQSwgcG9yIHN1cyBzaWdsYXMgZW4gaW5nbMOpcykgZXMgdW5hIHTDqWNuaWNhIGVzdGFkw61zdGljYSBxdWUgdHJhbnNmb3JtYSB1biBjb25qdW50byBkZSB2YXJpYWJsZXMgcG9zaWJsZW1lbnRlIGNvcnJlbGFjaW9uYWRhcyBlbiB1biBudWV2byBjb25qdW50byBkZSB2YXJpYWJsZXMgbm8gY29ycmVsYWNpb25hZGFzIGxsYW1hZGFzIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzLiBDYWRhIGNvbXBvbmVudGUgcHJpbmNpcGFsIGVzIHVuYSBjb21iaW5hY2nDs24gbGluZWFsIGRlIGxhcyB2YXJpYWJsZXMgb3JpZ2luYWxlcywgeSBzZSBvcmRlbmFuIGRlIG1hbmVyYSBxdWUgZWwgcHJpbWVyIGNvbXBvbmVudGUgKCRQQ18xJCkgY2FwdHVyZSBsYSBtYXlvciB2YXJpYW56YSBwb3NpYmxlIGRlIGxvcyBkYXRvcywgZWwgc2VndW5kbyAoJFBDXzIkKSBjYXB0dXJlIGxhIG1heW9yIHZhcmlhbnphIHJlc3RhbnRlLCB5IGFzw60gc3VjZXNpdmFtZW50ZS4gTWF0ZW3DoXRpY2FtZW50ZSwgc2kgdGVuZW1vcyB1bmEgbWF0cml6IGRlIGRhdG9zICRYJCAoY2VudHJhZGEpLCBlbCBQQ0Egc2UgYmFzYSBlbiBkZXNjb21wb25lciBzdSBtYXRyaXogZGUgY292YXJpYW56YXMgJFxTaWdtYSQgZW4gdmFsb3JlcyB5IHZlY3RvcmVzIHByb3Bpb3MsIGRlIG1vZG8gcXVlICRcU2lnbWEgdl9rID0gXGxhbWJkYV9rIHZfayQsIGRvbmRlICR2X2skIGVzIGVsIHZlY3RvciBwcm9waW8geSAkXGxhbWJkYV9rJCBzdSB2YXJpYW56YSBhc29jaWFkYS4KClN1IHV0aWxpZGFkIHByaW5jaXBhbCBlcyByZWR1Y2lyIGxhIGRpbWVuc2lvbmFsaWRhZCBkZWwgY29uanVudG8gZGUgZGF0b3MgbWFudGVuaWVuZG8gbGEgbWF5b3IgY2FudGlkYWQgcG9zaWJsZSBkZSBpbmZvcm1hY2nDs24gKHZhcmlhbnphKS4gRXN0byBlcyB2YWxpb3NvIGN1YW5kbyBzZSB0aWVuZW4gbXVjaGFzIHZhcmlhYmxlcywgeWEgcXVlIGF5dWRhIGEgc2ltcGxpZmljYXIgZWwgYW7DoWxpc2lzLCByZWR1Y2lyIHJ1aWRvIHkgZXZpdGFyIHByb2JsZW1hcyBjb21vIGxhIG11bHRpY29saW5lYWxpZGFkLiBMYSBwcm95ZWNjacOzbiBkZSBsb3MgZGF0b3Mgc29icmUgbG9zIHByaW1lcm9zICRtJCBjb21wb25lbnRlcyBwcmluY2lwYWxlcyAoJG0gPCBwJCwgc2llbmRvICRwJCBlbCBuw7ptZXJvIG9yaWdpbmFsIGRlIHZhcmlhYmxlcykgZ2VuZXJhIHVuYSByZXByZXNlbnRhY2nDs24gbcOhcyBjb21wYWN0YSwgcGVybyBjb24gbGEgbWF5b3IgdmFyaWFuemEgcHJlc2VydmFkYS4gRWwgbnVldm8gY29uanVudG8gdHJhbnNmb3JtYWRvIHNlIGV4cHJlc2EgY29tbyAkWiA9IFhXJCwgZG9uZGUgJFckIGVzIGxhIG1hdHJpeiBmb3JtYWRhIHBvciBsb3MgdmVjdG9yZXMgcHJvcGlvcyBzZWxlY2Npb25hZG9zLgoKRW4gbGEgcHLDoWN0aWNhLCBlbCBQQ0Egc2UgdXNhIHBhcmEgdmlzdWFsaXphY2nDs24gZGUgZGF0b3MgZW4gMkQgbyAzRCwgcHJlcHJvY2VzYW1pZW50byBlbiBtb2RlbG9zIGRlIG1hY2hpbmUgbGVhcm5pbmcsIGNvbXByZXNpw7NuIGRlIGRhdG9zLCBpZGVudGlmaWNhY2nDs24gZGUgcGF0cm9uZXMgeSBlbGltaW5hY2nDs24gZGUgcnVpZG8uIFBvciBlamVtcGxvLCBlbiBnZW7DqXRpY2Egc2UgdXNhIHBhcmEgZGlmZXJlbmNpYXIgZ3J1cG9zIHBvYmxhY2lvbmFsZXMsIGVuIHJlY29ub2NpbWllbnRvIGZhY2lhbCBwYXJhIHJlZHVjaXIgZWwgbsO6bWVybyBkZSBww614ZWxlcyBhIGFuYWxpemFyLCB5IGVuIGVjb25vbcOtYSBwYXJhIHNpbnRldGl6YXIgaW5kaWNhZG9yZXMgbWFjcm9lY29uw7NtaWNvcyBjb3JyZWxhY2lvbmFkb3MgZW4gdW5vcyBwb2NvcyBmYWN0b3JlcyBxdWUgZXhwbGlxdWVuIGxhIG1heW9yIHBhcnRlIGRlIGxhIHZhcmlhYmlsaWRhZC4KCiMjIDUuMSkgTGEgbWFsZGljacOzbiBkZSBsYSBkaW1lbnNpb25hbGlkYWQKCkxhIG1hbGRpY2nDs24gZGUgbGEgZGltZW5zaW9uYWxpZGFkIGRlc2NyaWJlIGPDs21vLCBhbCBhdW1lbnRhciBlbCBuw7ptZXJvIGRlIGRpbWVuc2lvbmVzICRwJCBlbiB1biBjb25qdW50byBkZSBkYXRvcywgZWwgdm9sdW1lbiBkZWwgZXNwYWNpbyBjcmVjZSBleHBvbmVuY2lhbG1lbnRlIHkgbG9zIHB1bnRvcyBzZSB2dWVsdmVuIGVzY2Fzb3MsIGxvIHF1ZSBkZWdyYWRhIG1lZGlkYXMgZGUgcHJveGltaWRhZCBjb21vIGxhIGRpc3RhbmNpYSBldWNsaWRpYW5hICRkKHhfaSwgeF9qKSA9IFxzcXJ0e1xzdW1fe2s9MX1ecCAoeF97aWt9IC0geF97amt9KV4yfSQgcXVlIHRpZW5kZSBhIHNlciBzaW1pbGFyIHBhcmEgdG9kb3MgbG9zIHBhcmVzIGRlIHB1bnRvczsgZXN0byBwcm92b2NhIHByb2JsZW1hcyBjb21vIHNvYnJlYWp1c3RlLCBww6lyZGlkYSBkZSBjYXBhY2lkYWQgZGlzY3JpbWluYXRpdmEgeSBuZWNlc2lkYWQgZGUgdW4gbsO6bWVybyBkZSBvYnNlcnZhY2lvbmVzIHF1ZSBjcmVjZSBleHBvbmVuY2lhbG1lbnRlIGNvbiAkcCQsIG1vdGl2byBwb3IgZWwgY3VhbCBzZSBhcGxpY2FuIHTDqWNuaWNhcyBkZSByZWR1Y2Npw7NuIGRlIGRpbWVuc2lvbmFsaWRhZCBjb21vIFBDQSwgdC1TTkUgbyBVTUFQLgoKYGBge3J9CiMgQ3JlYW1vcyBzZW1pbGxhCnNldC5zZWVkKDEyMykKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDEuIEdlbmVyYXIgZGF0b3Mgc2ltdWxhZG9zCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQpuIDwtIDEwMAp4MSA8LSBybm9ybShuLCBtZWFuID0gNSwgc2QgPSAyKQp4MiA8LSAwLjUgKiB4MSArIHJub3JtKG4sIG1lYW4gPSAwLCBzZCA9IDEpICAjIENvcnJlbGFjaW9uYWRhIGNvbiB4MQp4MyA8LSAtMC4zICogeDEgKyAwLjggKiB4MiArIHJub3JtKG4sIHNkID0gMC41KQoKZGF0b3MgPC0gZGF0YS5mcmFtZShYMSA9IHgxLCBYMiA9IHgyLCBYMyA9IHgzKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMi4gR3LDoWZpY28gM0QgZGF0b3Mgb3JpZ2luYWxlcwojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KZmlnM2QgPC0gcGxvdF9seShkYXRvcywgeCA9IH5YMSwgeSA9IH5YMiwgeiA9IH5YMywKICAgICAgICAgICAgICAgICB0eXBlID0gInNjYXR0ZXIzZCIsIG1vZGUgPSAibWFya2VycyIsCiAgICAgICAgICAgICAgICAgbWFya2VyID0gbGlzdChzaXplID0gNCwgY29sb3IgPSAnYmx1ZScsIG9wYWNpdHkgPSAwLjcpKSAlPiUKICBsYXlvdXQodGl0bGUgPSAiRGF0b3Mgb3JpZ2luYWxlcyBlbiAzRCIpCmZpZzNkCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyAzLiBQQ0EKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CnBjYSA8LSBwcmNvbXAoZGF0b3MsIGNlbnRlciA9IFRSVUUsIHNjYWxlLiA9IFRSVUUpCgojIFZhcmlhbnphIGV4cGxpY2FkYQp2YXJfZXhwIDwtIHBjYSRzZGV2XjIgLyBzdW0ocGNhJHNkZXZeMikKZGZfdmFyIDwtIGRhdGEuZnJhbWUoQ29tcG9uZW50ZSA9IHBhc3RlMCgiUEMiLCAxOmxlbmd0aCh2YXJfZXhwKSksCiAgICAgICAgICAgICAgICAgICAgIFZhcmlhbnphID0gdmFyX2V4cCkKCiMgU2NyZWUgcGxvdApnZ3Bsb3QoZGZfdmFyLCBhZXMoeCA9IENvbXBvbmVudGUsIHkgPSBWYXJpYW56YSkpICsKICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJza3libHVlIikgKwogIGdlb21fdGV4dChhZXMobGFiZWwgPSByb3VuZChWYXJpYW56YSwgMikpLCB2anVzdCA9IC0wLjUpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIGxhYnModGl0bGUgPSAiVmFyaWFuemEgZXhwbGljYWRhIHBvciBjb21wb25lbnRlIiwKICAgICAgIHkgPSAiUHJvcG9yY2nDs24gZGUgdmFyaWFuemEiKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgNC4gUHJveWVjY2nDs24gZW4gbG9zIGRvcyBwcmltZXJvcyBQQ3MKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CnNjb3JlcyA8LSBhcy5kYXRhLmZyYW1lKHBjYSR4KQpnZ3Bsb3Qoc2NvcmVzLCBhZXMoeCA9IFBDMSwgeSA9IFBDMikpICsKICBnZW9tX3BvaW50KGNvbG9yID0gInJlZCIsIGFscGhhID0gMC43KSArCiAgdGhlbWVfbWluaW1hbCgpICsKICBsYWJzKHRpdGxlID0gIlByb3llY2Npw7NuIGVuIGxvcyBkb3MgcHJpbWVyb3MgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMiKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgNS4gTWFsZGljacOzbiBkZSBsYSBkaW1lbnNpb25hbGlkYWQKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CmRpbWVuc2lvbmVzIDwtIDE6NTAKZGlzdGFuY2lhc19yZWwgPC0gc2FwcGx5KGRpbWVuc2lvbmVzLCBmdW5jdGlvbihwKSB7CiAgZGF0b3NfaGQgPC0gbWF0cml4KHJ1bmlmKG4gKiBwKSwgbnJvdyA9IG4pICMgRGF0b3MgYWxlYXRvcmlvcyBlbiBhbHRhIGRpbWVuc2nDs24KICBkIDwtIGRpc3QoZGF0b3NfaGQpCiAgZF9taW4gPC0gbWluKGQpCiAgZF9tYXggPC0gbWF4KGQpCiAgKGRfbWF4IC0gZF9taW4pIC8gZF9taW4gIyBEaWZlcmVuY2lhIHJlbGF0aXZhCn0pCgpkZl9tZCA8LSBkYXRhLmZyYW1lKERpbWVuc2lvbmVzID0gZGltZW5zaW9uZXMsCiAgICAgICAgICAgICAgICAgICAgRGlmUmVsYXRpdmEgPSBkaXN0YW5jaWFzX3JlbCkKCmdncGxvdChkZl9tZCwgYWVzKHggPSBEaW1lbnNpb25lcywgeSA9IERpZlJlbGF0aXZhKSkgKwogIGdlb21fbGluZShjb2xvciA9ICJkYXJrZ3JlZW4iLCBzaXplID0gMSkgKwogIHRoZW1lX21pbmltYWwoKSArCiAgbGFicyh0aXRsZSA9ICJNYWxkaWNpw7NuIGRlIGxhIGRpbWVuc2lvbmFsaWRhZCIsCiAgICAgICB5ID0gIkRpZmVyZW5jaWEgcmVsYXRpdmEgZW50cmUgZGlzdGFuY2lhcyIpCgpgYGAKCiMjIDUuMikgU2VsZWNjacOzbiBkZSBjb21wb25lbnRlcwoKUGFyYSBzZWxlY2Npb25hciBlbCBuw7ptZXJvIGlkZWFsIGRlIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzLCB1biBtw6l0b2RvIG11eSB1c2FkbyBlcyBlbCBjcml0ZXJpbyBkZSBLYWlzZXIsIHF1ZSBjb25zaXN0ZSBlbiBjb25zZXJ2YXIgYXF1ZWxsb3MgY29tcG9uZW50ZXMgY3V5byB2YWxvciBwcm9waW8gKCRcbGFtYmRhX2skKSBzZWEgbWF5b3IgcXVlICQxJCBjdWFuZG8gc2UgdHJhYmFqYSBjb24gbGEgbWF0cml6IGRlIGNvcnJlbGFjaW9uZXMuIExhIGzDs2dpY2EgZXMgcXVlIGNhZGEgdmFyaWFibGUgZXN0YW5kYXJpemFkYSBhcG9ydGEgdW5hIHZhcmlhbnphIGRlICQxJCBhIGxhIHZhcmlhbnphIHRvdGFsOyBwb3IgdGFudG8sIHVuIGNvbXBvbmVudGUgY29uICRcbGFtYmRhX2sgPiAxJCBleHBsaWNhIG3DoXMgdmFyaWFuemEgcXVlIHVuYSB2YXJpYWJsZSBvcmlnaW5hbCB5IHNlIGp1c3RpZmljYSBjb25zZXJ2YXJsby4gT3RybyBtw6l0b2RvIGFtcGxpYW1lbnRlIGVtcGxlYWRvIGVzIG9ic2VydmFyIGVsIGdyw6FmaWNvIGRlIHNlZGltZW50YWNpw7NuIChzY3JlZSBwbG90KSwgZG9uZGUgc2UgZ3JhZmljYW4gbG9zIHZhbG9yZXMgcHJvcGlvcyBlbiBvcmRlbiBkZXNjZW5kZW50ZSB5IHNlIGJ1c2NhIGVsIOKAnGNvZG/igJ0gZGUgbGEgY3VydmE6IGVsIHB1bnRvIGEgcGFydGlyIGRlbCBjdWFsIGxhIGdhbmFuY2lhIGRlIHZhcmlhbnphIGV4cGxpY2FkYSBwb3IgYcOxYWRpciBudWV2b3MgY29tcG9uZW50ZXMgZXMgbWFyZ2luYWwuIEVzdGUgZW5mb3F1ZSBlcyBpbnR1aXRpdm8geSBwZXJtaXRlIGJhbGFuY2VhciBsYSBzaW1wbGljaWRhZCBkZWwgbW9kZWxvIGNvbiBsYSByZXRlbmNpw7NuIGRlIGluZm9ybWFjacOzbi4KCkFtYm9zIG3DqXRvZG9zIHNvbiBwcsOhY3RpY29zIHBvcnF1ZSBjb21iaW5hbiBjcml0ZXJpb3MgY3VhbnRpdGF0aXZvcyAodmFsb3JlcyBwcm9waW9zKSB5IHZpc3VhbGVzIChjb2RvIGRlbCBzY3JlZSBwbG90KSwgZXZpdGFuZG8gcmV0ZW5lciBjb21wb25lbnRlcyBxdWUgc29sbyBjYXB0dXJhbiBydWlkby4gRW4gYXBsaWNhY2lvbmVzIHJlYWxlcywgdGFtYmnDqW4gcHVlZGUgY29uc2lkZXJhcnNlIHVuIHVtYnJhbCBkZSB2YXJpYW56YSBhY3VtdWxhZGEgKHBvciBlamVtcGxvLCBjb25zZXJ2YXIgbG9zIHByaW1lcm9zICRtJCBjb21wb25lbnRlcyBxdWUgZXhwbGlxdWVuIGFsIG1lbm9zIHVuICQ4MCUkIGRlIGxhIHZhcmlhbnphKSwgbG8gcXVlIHBlcm1pdGUgYXNlZ3VyYXIgcXVlIGxhIHJlZHVjY2nDs24gZGltZW5zaW9uYWwgbWFudGVuZ2EgbGEgbWF5b3IgcGFydGUgZGUgbGEgaW5mb3JtYWNpw7NuIHJlbGV2YW50ZSBwYXJhIGVsIGFuw6FsaXNpcy4KCmBgYHtyfQojID09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMS4gRGF0b3MgeSBQQ0EKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PQpkYXRhKCJVU0FycmVzdHMiKQpkYXRvcyA8LSBuYS5vbWl0KFVTQXJyZXN0cykKCnBjYSA8LSBwcmNvbXAoZGF0b3MsIGNlbnRlciA9IFRSVUUsIHNjYWxlLiA9IFRSVUUpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMi4gVmFsb3JlcyBwcm9waW9zIChFaWdlbnZhbHVlcykKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PQplaWdlbnZhbHVlcyA8LSBwY2Ekc2Rldl4yCm5hbWVzKGVpZ2VudmFsdWVzKSA8LSBwYXN0ZTAoIlBDIiwgMTpsZW5ndGgoZWlnZW52YWx1ZXMpKQoKIyBDcml0ZXJpbyBkZSBLYWlzZXIgKM67ID4gMSkKa2Fpc2VyX2NvbXBvbmVudHMgPC0gc3VtKGVpZ2VudmFsdWVzID4gMSkKCiMgVmFyaWFuemEgZXhwbGljYWRhCnZhcl9leHAgPC0gZWlnZW52YWx1ZXMgLyBzdW0oZWlnZW52YWx1ZXMpCnZhcl9hY3VtIDwtIGN1bXN1bSh2YXJfZXhwKQoKIyBOw7ptZXJvIGRlIGNvbXBvbmVudGVzIHBhcmEg4omlIDgwJSB2YXJpYW56YQptXzgwIDwtIHdoaWNoKHZhcl9hY3VtID49IDAuODApWzFdCgojID09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMy4gU2NyZWUgcGxvdAojID09PT09PT09PT09PT09PT09PT09PT09PT09CmRmX3BjYSA8LSBkYXRhLmZyYW1lKAogIENvbXBvbmVudGUgPSBmYWN0b3IobmFtZXMoZWlnZW52YWx1ZXMpLCBsZXZlbHMgPSBuYW1lcyhlaWdlbnZhbHVlcykpLAogIFZhbG9yUHJvcGlvID0gZWlnZW52YWx1ZXMsCiAgVmFyRXhwID0gdmFyX2V4cCwKICBWYXJBY3VtID0gdmFyX2FjdW0KKQoKZ2dwbG90KGRmX3BjYSwgYWVzKHggPSBDb21wb25lbnRlLCB5ID0gVmFsb3JQcm9waW8pKSArCiAgZ2VvbV9wb2ludChzaXplID0gMykgKwogIGdlb21fbGluZShhZXMoZ3JvdXAgPSAxKSkgKwogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDEsIGNvbG9yID0gInJlZCIsIGxpbmV0eXBlID0gImRhc2hlZCIpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIGxhYnModGl0bGUgPSAiU2NyZWUgcGxvdCB5IENyaXRlcmlvIGRlIEthaXNlciIsCiAgICAgICB5ID0gIlZhbG9yIHByb3BpbyAozrspIiwKICAgICAgIHggPSAiQ29tcG9uZW50ZSBwcmluY2lwYWwiKSArCiAgYW5ub3RhdGUoInRleHQiLCB4ID0ga2Fpc2VyX2NvbXBvbmVudHMsIHkgPSAxLjEsCiAgICAgICAgICAgbGFiZWwgPSBwYXN0ZSgiS2Fpc2VyOiIsIGthaXNlcl9jb21wb25lbnRzLCAiY29tcG9uZW50ZXMiKSwKICAgICAgICAgICBoanVzdCA9IC0wLjEsIGNvbG9yID0gInJlZCIpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09CiMgNC4gVmFyaWFuemEgYWN1bXVsYWRhCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT0KZ2dwbG90KGRmX3BjYSwgYWVzKHggPSBDb21wb25lbnRlLCB5ID0gVmFyQWN1bSkpICsKICBnZW9tX3BvaW50KHNpemUgPSAzLCBjb2xvciA9ICJibHVlIikgKwogIGdlb21fbGluZShhZXMoZ3JvdXAgPSAxKSwgY29sb3IgPSAiYmx1ZSIpICsKICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLjgwLCBjb2xvciA9ICJkYXJrZ3JlZW4iLCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICBsYWJzKHRpdGxlID0gIlZhcmlhbnphIGFjdW11bGFkYSIsCiAgICAgICB5ID0gIlByb3BvcmNpw7NuIGFjdW11bGFkYSBkZSB2YXJpYW56YSIsCiAgICAgICB4ID0gIkNvbXBvbmVudGUgcHJpbmNpcGFsIikgKwogIGFubm90YXRlKCJ0ZXh0IiwgeCA9IG1fODAsIHkgPSAwLjgyLAogICAgICAgICAgIGxhYmVsID0gcGFzdGUoIuKJpTgwJSBjb24iLCBtXzgwLCAiY29tcG9uZW50ZXMiKSwKICAgICAgICAgICBoanVzdCA9IC0wLjEsIGNvbG9yID0gImRhcmtncmVlbiIpCgpgYGAKCiMjIDUuMykgR3JhZmljb3MgcGFyYSBlbCBlbnRlbmRpbWllbnRvIGRlIGRpbWVuc2lvbmVzCgpFbiBQQ0EsIGVsIGdyw6FmaWNvIGRlIG1hZ25pdHVkZXMgeSBkaXJlY2Npb25lcyAobyBiaXBsb3QgZGUgY2FyZ2FzKSBtdWVzdHJhIGPDs21vIGNhZGEgdmFyaWFibGUgb3JpZ2luYWwgY29udHJpYnV5ZSB5IHNlIG9yaWVudGEgcmVzcGVjdG8gYSBsb3MgcHJpbWVyb3MgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMuIExhcyBmbGVjaGFzIGluZGljYW4gbGEgZGlyZWNjacOzbiBlbiBsYSBxdWUgYXVtZW50YSBjYWRhIHZhcmlhYmxlIHkgc3UgbG9uZ2l0dWQgcmVmbGVqYSBsYSBtYWduaXR1ZCBkZSBzdSBjb250cmlidWNpw7NuOiBmbGVjaGFzIGxhcmdhcyBhcHVudGFuIGEgdmFyaWFibGVzIHF1ZSB0aWVuZW4gbWF5b3IgcGVzbyBlbiBsYSBjb25zdHJ1Y2Npw7NuIGRlIGVzb3MgY29tcG9uZW50ZXMuIEN1YW5kbyBkb3MgZmxlY2hhcyBlc3TDoW4gcHLDs3hpbWFzIHkgZW4gbGEgbWlzbWEgZGlyZWNjacOzbiwgc3VzIHZhcmlhYmxlcyBlc3TDoW4gcG9zaXRpdmFtZW50ZSBjb3JyZWxhY2lvbmFkYXM7IHNpIGVzdMOhbiBvcHVlc3RhcywgZXN0w6FuIG5lZ2F0aXZhbWVudGUgY29ycmVsYWNpb25hZGFzLgoKTGEgdmVyc2nDs24gZW4gZGlzcGVyc2nDs24gKG8gYmlwbG90IGNvbiBvYnNlcnZhY2lvbmVzKSBwcm95ZWN0YSBzaW11bHTDoW5lYW1lbnRlIGxhcyBvYnNlcnZhY2lvbmVzIHkgbGFzIHZhcmlhYmxlcyBlbiBlbCBlc3BhY2lvIGRlZmluaWRvIHBvciAkUENfMSQgeSAkUENfMiQuIEFzw60sIHNlIHB1ZWRlIHZlciBubyBzb2xvIGPDs21vIHNlIHJlbGFjaW9uYW4gbGFzIHZhcmlhYmxlcywgc2lubyB0YW1iacOpbiBjw7NtbyBzZSBkaXN0cmlidXllbiBsYXMgdW5pZGFkZXMgZGUgYW7DoWxpc2lzIChwb3IgZWplbXBsbywgcGHDrXNlcywgcGVyc29uYXMsIHByb2R1Y3RvcykgZW4gZnVuY2nDs24gZGUgZXNhcyBjb21iaW5hY2lvbmVzLiBMb3MgcHVudG9zIHJlcHJlc2VudGFuIGxhcyBvYnNlcnZhY2lvbmVzIHkgbGFzIGZsZWNoYXMgcmVwcmVzZW50YW4gbGFzIHZhcmlhYmxlczsgbGEgcHJveGltaWRhZCBlbnRyZSB1biBwdW50byB5IHVuYSBmbGVjaGEgc3VnaWVyZSBxdWUgZXNhIG9ic2VydmFjacOzbiB0aWVuZSB1biB2YWxvciBhbHRvIGVuIGVzYSB2YXJpYWJsZS4KCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CgoKZGF0YSgiVVNBcnJlc3RzIikKcGNhIDwtIHByY29tcChVU0FycmVzdHMsIHNjYWxlLiA9IFRSVUUpCgojIDEuIEdyw6FmaWNvIGRlIG1hZ25pdHVkZXMgeSBkaXJlY2Npb25lcyAoc29sbyB2YXJpYWJsZXMpCmdnYmlwbG90KHBjYSwgY2hvaWNlcyA9IGMoMSwyKSwgb2JzLnNjYWxlID0gMCwgdmFyLnNjYWxlID0gMSwKICAgICAgICAgZ3JvdXBzID0gTlVMTCwgZWxsaXBzZSA9IEZBTFNFLCBjaXJjbGUgPSBUUlVFKSArCiAgZ2d0aXRsZSgiUENBIC0gTWFnbml0dWRlcyB5IGRpcmVjY2lvbmVzICh2YXJpYWJsZXMpIikgKwogIHRoZW1lX21pbmltYWwoKQoKIyAyLiBCaXBsb3QgZW4gZGlzcGVyc2nDs24gKHZhcmlhYmxlcyArIG9ic2VydmFjaW9uZXMpCmdnYmlwbG90KHBjYSwgY2hvaWNlcyA9IGMoMSwyKSwgb2JzLnNjYWxlID0gMSwgdmFyLnNjYWxlID0gMSwKICAgICAgICAgZ3JvdXBzID0gTlVMTCwgZWxsaXBzZSA9IEZBTFNFLCBjaXJjbGUgPSBUUlVFKSArCiAgZ2d0aXRsZSgiUENBIC0gQmlwbG90IGNvbiBvYnNlcnZhY2lvbmVzIHkgdmFyaWFibGVzIikgKwogIHRoZW1lX21pbmltYWwoKQoKYGBgCgojIyA1LjQpIEVqZW1wbG8gZW4gcHl0aG9uCgpgYGB7cHl0aG9ufQoKIyBTZW1pbGxhIGRlIHJlcHJvZHVjaWJpbGlkYWQKbnAucmFuZG9tLnNlZWQoMTIzKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMS4gR2VuZXJhciBkYXRvcyBzaW11bGFkb3MKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09Cm4gPSAxMDAKeDEgPSBucC5yYW5kb20ubm9ybWFsKDUsIDIsIG4pCngyID0gMC41ICogeDEgKyBucC5yYW5kb20ubm9ybWFsKDAsIDEsIG4pCngzID0gLTAuMyAqIHgxICsgMC44ICogeDIgKyBucC5yYW5kb20ubm9ybWFsKDAsIDAuNSwgbikKCmRhdG9zID0gcGQuRGF0YUZyYW1lKHsiWDEiOiB4MSwgIlgyIjogeDIsICJYMyI6IHgzfSkKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDIuIEdyw6FmaWNvIDNEIGRhdG9zIG9yaWdpbmFsZXMKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CmZpZzNkID0gcHguc2NhdHRlcl8zZChkYXRvcywgeD0iWDEiLCB5PSJYMiIsIHo9IlgzIiwKICAgICAgICAgICAgICAgICAgICAgIG9wYWNpdHk9MC43LCBjb2xvcl9kaXNjcmV0ZV9zZXF1ZW5jZT1bImJsdWUiXSkKZmlnM2QudXBkYXRlX3RyYWNlcyhtYXJrZXI9ZGljdChzaXplPTQpKQpmaWczZC51cGRhdGVfbGF5b3V0KHRpdGxlPSJEYXRvcyBvcmlnaW5hbGVzIGVuIDNEIikKZmlnM2Quc2hvdygpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyAzLiBQQ0EKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CnNjYWxlciA9IFN0YW5kYXJkU2NhbGVyKCkKZGF0b3Nfc2NhbGVkID0gc2NhbGVyLmZpdF90cmFuc2Zvcm0oZGF0b3MpCgpwY2EgPSBQQ0EoKQpwY2EuZml0KGRhdG9zX3NjYWxlZCkKCiMgVmFyaWFuemEgZXhwbGljYWRhCnZhcl9leHAgPSBwY2EuZXhwbGFpbmVkX3ZhcmlhbmNlX3JhdGlvXwpkZl92YXIgPSBwZC5EYXRhRnJhbWUoewogICAgIkNvbXBvbmVudGUiOiBbZiJQQ3tpKzF9IiBmb3IgaSBpbiByYW5nZShsZW4odmFyX2V4cCkpXSwKICAgICJWYXJpYW56YSI6IHZhcl9leHAKfSkKCiMgU2NyZWUgcGxvdApwbHQuZmlndXJlKGZpZ3NpemU9KDYsNCkpCnNucy5iYXJwbG90KHg9IkNvbXBvbmVudGUiLCB5PSJWYXJpYW56YSIsIGRhdGE9ZGZfdmFyLCBjb2xvcj0ic2t5Ymx1ZSIpCmZvciBpLCB2IGluIGVudW1lcmF0ZSh2YXJfZXhwKToKICAgIHBsdC50ZXh0KGksIHYgKyAwLjAxLCBmInt2Oi4yZn0iLCBoYT0iY2VudGVyIikKcGx0LnRpdGxlKCJWYXJpYW56YSBleHBsaWNhZGEgcG9yIGNvbXBvbmVudGUiKQpwbHQueWxhYmVsKCJQcm9wb3JjacOzbiBkZSB2YXJpYW56YSIpCnBsdC5zaG93KCkKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDQuIFByb3llY2Npw7NuIGVuIGxvcyBkb3MgcHJpbWVyb3MgUENzCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQpzY29yZXMgPSBwY2EudHJhbnNmb3JtKGRhdG9zX3NjYWxlZCkKcGx0LmZpZ3VyZShmaWdzaXplPSg2LDQpKQpzbnMuc2NhdHRlcnBsb3QoeD1zY29yZXNbOiwwXSwgeT1zY29yZXNbOiwxXSwgY29sb3I9InJlZCIsIGFscGhhPTAuNykKcGx0LnhsYWJlbCgiUEMxIikKcGx0LnlsYWJlbCgiUEMyIikKcGx0LnRpdGxlKCJQcm95ZWNjacOzbiBlbiBsb3MgZG9zIHByaW1lcm9zIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzIikKcGx0LnNob3coKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgNS4gTWFsZGljacOzbiBkZSBsYSBkaW1lbnNpb25hbGlkYWQKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CmRpbWVuc2lvbmVzID0gcmFuZ2UoMSwgNTEpCmRpc3RhbmNpYXNfcmVsID0gW10KCmZvciBwIGluIGRpbWVuc2lvbmVzOgogICAgZGF0b3NfaGQgPSBucC5yYW5kb20ucmFuZChuLCBwKQogICAgZCA9IHBkaXN0KGRhdG9zX2hkKQogICAgZF9taW4gPSBucC5taW4oZCkKICAgIGRfbWF4ID0gbnAubWF4KGQpCiAgICBkaXN0YW5jaWFzX3JlbC5hcHBlbmQoKGRfbWF4IC0gZF9taW4pIC8gZF9taW4pCgpkZl9tZCA9IHBkLkRhdGFGcmFtZSh7IkRpbWVuc2lvbmVzIjogZGltZW5zaW9uZXMsCiAgICAgICAgICAgICAgICAgICAgICAiRGlmUmVsYXRpdmEiOiBkaXN0YW5jaWFzX3JlbH0pCgpwbHQuZmlndXJlKGZpZ3NpemU9KDYsNCkpCnNucy5saW5lcGxvdCh4PSJEaW1lbnNpb25lcyIsIHk9IkRpZlJlbGF0aXZhIiwgZGF0YT1kZl9tZCwgY29sb3I9ImRhcmtncmVlbiIpCnBsdC50aXRsZSgiTWFsZGljacOzbiBkZSBsYSBkaW1lbnNpb25hbGlkYWQiKQpwbHQueWxhYmVsKCJEaWZlcmVuY2lhIHJlbGF0aXZhIGVudHJlIGRpc3RhbmNpYXMiKQpwbHQuc2hvdygpCgpgYGAKCiMgNikgQW7DoWxpc2lzIGZhY3RvcmlhbAoKRWwgYW7DoWxpc2lzIGZhY3RvcmlhbCBlcyB1bmEgdMOpY25pY2EgZXN0YWTDrXN0aWNhIG11bHRpdmFyaWFudGUgcXVlIGJ1c2NhIGlkZW50aWZpY2FyIGZhY3RvcmVzIGxhdGVudGVzICh2YXJpYWJsZXMgbm8gb2JzZXJ2YWRhcykgcXVlIGV4cGxpY2FuIGxhIGNvcnJlbGFjacOzbiBlbnRyZSB1biBjb25qdW50byBkZSB2YXJpYWJsZXMgb2JzZXJ2YWRhcy4gTWF0ZW3DoXRpY2FtZW50ZSwgc2UgYXN1bWUgcXVlIGNhZGEgdmFyaWFibGUgb2JzZXJ2YWRhICR4X2kkIHB1ZWRlIGV4cHJlc2Fyc2UgY29tbyB1bmEgY29tYmluYWNpw7NuIGxpbmVhbCBkZSBmYWN0b3JlcyBjb211bmVzICRGX2okIG3DoXMgdW4gdMOpcm1pbm8gZGUgZXJyb3I6CgokJAp4X2kgPSBcbGFtYmRhX3tpMX1GXzEgKyBcbGFtYmRhX3tpMn1GXzIgKyBcZG90cyArIFxsYW1iZGFfe2ltfUZfbSArIFx2YXJlcHNpbG9uX2kKJCQKCmRvbmRlICRcbGFtYmRhX3tpan0kIHNvbiBsYXMgY2FyZ2FzIGZhY3RvcmlhbGVzLCAkRl9qJCBsb3MgZmFjdG9yZXMgeSAkXHZhcmVwc2lsb25faSQgZWwgZXJyb3IgZXNwZWPDrWZpY28uCgpTaXJ2ZSBwcmluY2lwYWxtZW50ZSBwYXJhIHJlZHVjaXIgbGEgZGltZW5zaW9uYWxpZGFkIHkgZGVzY3VicmlyIGVzdHJ1Y3R1cmFzIHN1YnlhY2VudGVzIGVuIGxvcyBkYXRvcywgYWdydXBhbmRvIHZhcmlhYmxlcyBjb3JyZWxhY2lvbmFkYXMgZW4gZmFjdG9yZXMgcXVlIHJlcHJlc2VudGFuIGNvbnN0cnVjdG9zIHRlw7NyaWNvcyAocG9yIGVqZW1wbG8sIOKAnGludGVsaWdlbmNpYeKAnSwg4oCcc2F0aXNmYWNjacOzbuKAnSwg4oCccmllc2dv4oCdKS4gRXN0byBwZXJtaXRlIHNpbXBsaWZpY2FyIG1vZGVsb3MsIGVsaW1pbmFyIHJlZHVuZGFuY2lhIHkgZmFjaWxpdGFyIGxhIGludGVycHJldGFjacOzbiBkZSBmZW7Ds21lbm9zIGNvbXBsZWpvcy4KCkVudHJlIGxvcyBjYXNvcyBkZSB1c28gbcOhcyBjb211bmVzIGVzdMOhbjoKCi0gICBQc2ljb21ldHLDrWE6IGlkZW50aWZpY2FyIGRpbWVuc2lvbmVzIGNvbW8gYW5zaWVkYWQsIGRlcHJlc2nDs24gbyBtb3RpdmFjacOzbiBhIHBhcnRpciBkZSDDrXRlbXMgZGUgY3Vlc3Rpb25hcmlvcy4KLSAgIE1hcmtldGluZzogYWdydXBhciBhdHJpYnV0b3MgZGUgcHJvZHVjdG9zIGVuIGZhY3RvcmVzIGNvbW8g4oCcY2FsaWRhZCBwZXJjaWJpZGHigJ0gbyDigJxwcmVjaW8tYmVuZWZpY2lv4oCdLgotICAgQW7DoWxpc2lzIHNvY2lvZWNvbsOzbWljbzogY29uc3RydWlyIMOtbmRpY2VzIGNvbXB1ZXN0b3MgKGVqLiBkZXNhcnJvbGxvIGh1bWFubykgYSBwYXJ0aXIgZGUgbcO6bHRpcGxlcyBpbmRpY2Fkb3Jlcy4KCioqRGlmZXJlbmNpYSBjb24gY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMqKgoKTGEgZGlmZXJlbmNpYSBjZW50cmFsIGVzIHF1ZSBlbMKgKiphbsOhbGlzaXMgZmFjdG9yaWFsKirCoGVzIHVuwqAqKm1vZGVsbyBlc3RhZMOtc3RpY28qKsKgcXVlIHBhcnRlIGRlIGxhIGhpcMOzdGVzaXMgZGUgcXVlIGxhcyB2YXJpYWJsZXMgb2JzZXJ2YWRhcyBlc3TDoW4gaW5mbHVpZGFzIHBvciB1biBjb25qdW50byByZWR1Y2lkbyBkZSBmYWN0b3JlcyBsYXRlbnRlcyBubyBvYnNlcnZhYmxlcywgcG9yIGxvIHF1ZSBidXNjYSBleHBsaWNhciBsYXMgY29ycmVsYWNpb25lcyBlbnRyZSB2YXJpYWJsZXMgYSB0cmF2w6lzIGRlIGVzdGFzIGRpbWVuc2lvbmVzIHN1YnlhY2VudGVzOyBlbiBjYW1iaW8sIGVswqAqKmFuw6FsaXNpcyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcyAoUENBKSoqwqBlcyB1bmHCoCoqaGVycmFtaWVudGEgZGVzY3JpcHRpdmEqKsKgcXVlIHRyYW5zZm9ybWEgbGFzIHZhcmlhYmxlcyBvcmlnaW5hbGVzIGVuIGNvbWJpbmFjaW9uZXMgbGluZWFsZXMgb3J0b2dvbmFsZXMgbGxhbWFkYXMgY29tcG9uZW50ZXMsIG1heGltaXphbmRvIGxhIHZhcmlhbnphIGV4cGxpY2FkYSBzaW4gYXN1bWlyIGxhIGV4aXN0ZW5jaWEgZGUgZmFjdG9yZXMgbGF0ZW50ZXMsIHNpZW5kbyBtw6FzIMO6dGlsIHBhcmEgcmVkdWNjacOzbiBkZSBkaW1lbnNpb25hbGlkYWQgeSB2aXN1YWxpemFjacOzbiBxdWUgcGFyYSBpbmZlcmlyIGVzdHJ1Y3R1cmFzIHRlw7NyaWNhcy4KCioqRWplbXBsbyBjb24gUioqCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCgoKIyAtLS0tLS0tLS0tIDEpIERhdG9zIC0tLS0tLS0tLS0KIyBVc2FyZW1vcyBsb3MgMjUgw610ZW1zIGRlIHBlcnNvbmFsaWRhZCB0aXBvIExpa2VydCBkZWwgZGF0YXNldCBiZmkgcXVlIHZpZW5lIGVuIHBzeWNoCmRhdGEoYmZpLCBwYWNrYWdlID0gInBzeWNoIikKZGYgPC0gYmZpWywgMToyNV0gICAgICAgICAgICAgICMgw610ZW1zIEExOkE1LCBDMTpDNSwgRTE6RTUsIE4xOk41LCBPMTpPNQpkZiA8LSBuYS5vbWl0KGRmKSAgICAgICAgICAgICAgIyByZW1vdmVtb3MgY2Fzb3MgY29uIE5BCm4gIDwtIG5yb3coZGYpCgptZXNzYWdlKHNwcmludGYoIk9ic2VydmFjaW9uZXMgdHJhcyBOQS1vbWl0OiAlczsgVmFyaWFibGVzOiAlcyIsIG4sIG5jb2woZGYpKSkKCiMgLS0tLS0tLS0tLSAyKSBDb3JyZWxhY2lvbmVzLCBLTU8geSBCYXJ0bGV0dCAtLS0tLS0tLS0tCiMgUGFyYSBMaWtlcnQsIG9wY2lvbmFsbWVudGUgcG9kcsOtYXMgdXNhciBjb3JyZWxhY2nDs24gcG9saWPDs3JpY2E7IGFxdcOtIHVzYW1vcyBQZWFyc29uIHBhcmEgdmVsb2NpZGFkLgpSIDwtIGNvcihkZikKCiMgTWFwYSBkZSBjYWxvciBkZSBjb3JyZWxhY2lvbmVzCmNvcnJwbG90KFIsCiAgICAgICAgIG1ldGhvZCA9ICJjb2xvciIsCiAgICAgICAgIG9yZGVyID0gImhjbHVzdCIsCiAgICAgICAgIGFkZHJlY3QgPSA1LCAgICAgICAgICAgICAgICAgIyBkaWJ1amEgNSBibG9xdWVzIHBvciBjbHVzdGVyaW5nCiAgICAgICAgIHRsLmNleCA9IDAuNiwKICAgICAgICAgbnVtYmVyLmNleCA9IDAuNiwKICAgICAgICAgbWFyID0gYygwLDAsMiwwKSwKICAgICAgICAgdGl0bGUgPSAiTWFwYSBkZSBjYWxvciBkZSBjb3JyZWxhY2lvbmVzIChhZ3J1cGFkYXMpIikKCiMgQWRlY3VhY2nDs24gbXVlc3RyYWwgKEtNTykgeSBFc2ZlcmljaWRhZCAoQmFydGxldHQpCm1lc3NhZ2UoIj09PiBLTU86IikKcHJpbnQoS01PKFIpKQptZXNzYWdlKCI9PT4gVGVzdCBkZSBlc2ZlcmljaWRhZCBkZSBCYXJ0bGV0dDoiKQpwcmludChjb3J0ZXN0LmJhcnRsZXR0KFIsIG4gPSBuKSkKCiMgLS0tLS0tLS0tLSAzKSBOw7ptZXJvIGRlIGZhY3RvcmVzOiBTY3JlZSArIEFuw6FsaXNpcyBwYXJhbGVsbyAtLS0tLS0tLS0tCiMgZmEucGFyYWxsZWwgZGV2dWVsdmUgZ3LDoWZpY29zIHkgYXl1ZGEgYSBkZWNpZGlyIGN1w6FudG9zIGZhY3RvcmVzIHJldGVuZXIKZmEucGFyYWxsZWwoZGYsIGZhID0gImZhIiwgZm0gPSAibWwiLAogICAgICAgICAgICBtYWluID0gIlNjcmVlICsgQW7DoWxpc2lzIHBhcmFsZWxvIChGQS1NTCkiKQoKIyBTdWdlcmVuY2lhOiBwYXJhIGJmaSBzdWVsZW4gc2VyIH41IGZhY3RvcmVzIChCaWcgRml2ZSkKbmZhY3RvcnMgPC0gNSAgICMgQWp1c3RhIHNpIGVsIGFuw6FsaXNpcyBwYXJhbGVsbyBzdWdpZXJlIG90cm8gbsO6bWVybwptZXNzYWdlKHNwcmludGYoIk7Dum1lcm8gZGUgZmFjdG9yZXMgc2VsZWNjaW9uYWRvOiAlZCIsIG5mYWN0b3JzKSkKCiMgLS0tLS0tLS0tLSA0KSBBRkUgKE1MKSBjb24gcm90YWNpw7NuIG9ibGljdWEgKG9ibGltaW4pIC0tLS0tLS0tLS0KZmFfZml0IDwtIGZhKGRmLAogICAgICAgICAgICAgbmZhY3RvcnMgPSBuZmFjdG9ycywKICAgICAgICAgICAgIHJvdGF0ZSAgID0gIm9ibGltaW4iLCAgIyBwZXJtaXRlIGNvcnJlbGFjacOzbiBlbnRyZSBmYWN0b3JlcwogICAgICAgICAgICAgZm0gICAgICAgPSAibWwiLAogICAgICAgICAgICAgc2NvcmVzICAgPSAicmVncmVzc2lvbiIpCgpjYXQoIlxuPT0+IFJFU1VNRU4gREUgQ0FSR0FTIChjdXRvZmYgPSAwLjMwKTpcbiIpCnByaW50KGZhX2ZpdCRsb2FkaW5ncywgY3V0b2ZmID0gMC4zMCkKY2F0KCJcbj09PiBWYXJpYW56YSBleHBsaWNhZGE6XG4iKQpwcmludChmYV9maXQkVmFjY291bnRlZCkKY2F0KCJcbj09PiBQcmltZXJhcyBmaWxhcyBkZSBwdW50YWplcyBmYWN0b3JpYWxlczpcbiIpCnByaW50KGhlYWQoZmFfZml0JHNjb3JlcykpCgojIC0tLS0tLS0tLS0gNSkgSGVhdG1hcCBkZSBjYXJnYXMgZmFjdG9yaWFsZXMgLS0tLS0tLS0tLQojIFBhc2Ftb3MgbGEgbWF0cml6IGRlIGNhcmdhcyBhIGZvcm1hdG8gbGFyZ28geSBvcmRlbmFtb3MgcG9yIGZhY3RvciBkb21pbmFudGUKTCA8LSBhcy5kYXRhLmZyYW1lKHVuY2xhc3MoZmFfZml0JGxvYWRpbmdzKSkKTCRJdGVtIDwtIHJvd25hbWVzKEwpCmZhY3RfY29scyA8LSBzZXRkaWZmKG5hbWVzKEwpLCAiSXRlbSIpCgpMJERvbWluYW50IDwtIGFwcGx5KExbLCBmYWN0X2NvbHNdLCAxLCBmdW5jdGlvbih4KSBuYW1lcyh3aGljaC5tYXgoYWJzKHgpKSkpCkwkTWF4QWJzICAgPC0gYXBwbHkoYWJzKExbLCBmYWN0X2NvbHNdKSwgMSwgbWF4KQpMIDwtIExbb3JkZXIoTCREb21pbmFudCwgLUwkTWF4QWJzKSwgXQoKTF9sb25nIDwtIEwgfD4KICBzZWxlY3QoSXRlbSwgYWxsX29mKGZhY3RfY29scykpIHw+CiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSBhbGxfb2YoZmFjdF9jb2xzKSwgbmFtZXNfdG8gPSAiRmFjdG9yIiwgdmFsdWVzX3RvID0gIkxvYWRpbmciKQoKTF9sb25nJEl0ZW0gPC0gZmFjdG9yKExfbG9uZyRJdGVtLCBsZXZlbHMgPSB1bmlxdWUoTCRJdGVtKSkKCnBfbG9hZGluZ3MgPC0gZ2dwbG90KExfbG9uZywgYWVzKHggPSBGYWN0b3IsIHkgPSBJdGVtLCBmaWxsID0gTG9hZGluZykpICsKICBnZW9tX3RpbGUoY29sb3IgPSAid2hpdGUiKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIobG93ID0gIiMzMTM2OTUiLCBtaWQgPSAid2hpdGUiLCBoaWdoID0gIiNBNTAwMjYiLCBtaWRwb2ludCA9IDApICsKICBsYWJzKHRpdGxlID0gIkhlYXRtYXAgZGUgY2FyZ2FzIGZhY3RvcmlhbGVzIChvYmxpbWluKSIsCiAgICAgICB4ID0gIkZhY3RvciIsIHkgPSAiw410ZW0iLCBmaWxsID0gIkNhcmdhIikgKwogIHRoZW1lX21pbmltYWwoYmFzZV9zaXplID0gMTEpICsKICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfdGV4dChzaXplID0gNykpCnByaW50KHBfbG9hZGluZ3MpCgojIC0tLS0tLS0tLS0gNikgRGlhZ3JhbWEgZGVsIG1vZGVsbyBmYWN0b3JpYWwgLS0tLS0tLS0tLQojIE11ZXN0cmEgZmFjdG9yZXMsIMOtdGVtcywgY2FyZ2FzIHkgY29ycmVsYWNpb25lcyBlbnRyZSBmYWN0b3JlcwpmYS5kaWFncmFtKGZhX2ZpdCwgc2ltcGxlID0gRkFMU0UsIGRpZ2l0cyA9IDIsCiAgICAgICAgICAgbWFpbiA9ICJEaWFncmFtYSBkZWwgbW9kZWxvIGZhY3RvcmlhbCAoQUZFLCBNTCArIG9ibGltaW4pIikKCiMgLS0tLS0tLS0tLSA3KSBEaXNwZXJzacOzbiBkZSBwdW50YWplcyBmYWN0b3JpYWxlcyAtLS0tLS0tLS0tCnNjb3JlcyA8LSBhcy5kYXRhLmZyYW1lKGZhX2ZpdCRzY29yZXMpCmlmICghaXMubnVsbChzY29yZXMpICYmIG5jb2woc2NvcmVzKSA+PSAyKSB7CiAgZjEgPC0gbmFtZXMoc2NvcmVzKVsxXQogIGYyIDwtIG5hbWVzKHNjb3JlcylbMl0KICBwX3Njb3JlcyA8LSBnZ3Bsb3Qoc2NvcmVzLCBhZXNfc3RyaW5nKHggPSBmMSwgeSA9IGYyKSkgKwogICAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMjUsIGNvbG9yID0gIiMyQzdGQjgiKSArCiAgICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLCBsaW5ldHlwZSA9IDIsIGNvbG9yID0gImdyZXk2MCIpICsKICAgIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGxpbmV0eXBlID0gMiwgY29sb3IgPSAiZ3JleTYwIikgKwogICAgbGFicyh0aXRsZSA9ICJEaXNwZXJzacOzbiBkZSBwdW50YWplcyBmYWN0b3JpYWxlcyIsIHggPSBmMSwgeSA9IGYyKSArCiAgICB0aGVtZV9taW5pbWFsKGJhc2Vfc2l6ZSA9IDExKQogIHByaW50KHBfc2NvcmVzKQp9IGVsc2UgewogIG1lc3NhZ2UoIk5vIHNlIGdlbmVyYXJvbiBwdW50YWplcyBmYWN0b3JpYWxlcyBwYXJhIGdyYWZpY2FyLiIpCn0KCgpgYGAKCiMjIDYuMSkgTW9kZWxvIGZhY3RvcmlhbAoKRWwgbW9kZWxvIGZhY3RvcmlhbCBlcyB1biBtb2RlbG8gZXN0YWTDrXN0aWNvIHF1ZSBidXNjYSBleHBsaWNhciBsYSB2YXJpYWJpbGlkYWQgeSBsYXMgY29ycmVsYWNpb25lcyBvYnNlcnZhZGFzIGVuIHVuIGNvbmp1bnRvIGRlIHZhcmlhYmxlcyBtZWRpYW50ZSB1biBuw7ptZXJvIHJlZHVjaWRvIGRlIGZhY3RvcmVzIGxhdGVudGVzIHF1ZSBubyBzZSBwdWVkZW4gbWVkaXIgZGlyZWN0YW1lbnRlLiBFbiBlc3RlIGVuZm9xdWUsIGNhZGEgdmFyaWFibGUgb2JzZXJ2YWRhICRYX2kkIHNlIGV4cHJlc2EgY29tbyB1bmEgY29tYmluYWNpw7NuIGxpbmVhbCBkZSBsb3MgZmFjdG9yZXMgY29tdW5lcyAkRl9qJCwgbcOhcyB1biB0w6lybWlubyBkZSBlcnJvciBlc3BlY8OtZmljby4gTWF0ZW3DoXRpY2FtZW50ZSwgZWwgbW9kZWxvIHB1ZWRlIGVzY3JpYmlyc2UgY29tbzoKCiQkWCA9IFxMYW1iZGEgRiArIFx2YXJlcHNpbG9uJCQKCmRvbmRlICRYJCBlcyBlbCB2ZWN0b3IgZGUgdmFyaWFibGVzIG9ic2VydmFkYXMsICRcTGFtYmRhJCBlcyBsYSBtYXRyaXogZGUgY2FyZ2FzIGZhY3RvcmlhbGVzIHF1ZSByZWZsZWphIGN1w6FudG8gYXBvcnRhIGNhZGEgZmFjdG9yIGEgY2FkYSB2YXJpYWJsZSwgJEYkIGVzIGVsIHZlY3RvciBkZSBmYWN0b3JlcyBjb211bmVzIHkgJFx2YXJlcHNpbG9uJCByZXByZXNlbnRhIGxvcyBlcnJvcmVzIG8gdmFyaWFuemFzIGVzcGVjw61maWNhcy4KCkVsIG9iamV0aXZvIGRlbCBtb2RlbG8gZmFjdG9yaWFsIGVzIGlkZW50aWZpY2FyIGVzdHJ1Y3R1cmFzIHN1YnlhY2VudGVzIHF1ZSBleHBsaXF1ZW4gbGFzIGNvcnJlbGFjaW9uZXMgZW50cmUgbGFzIHZhcmlhYmxlcyBvYnNlcnZhZGFzLiBBIGRpZmVyZW5jaWEgZGUgdW4gYW7DoWxpc2lzIHB1cmFtZW50ZSBkZXNjcmlwdGl2bywgYXF1w60gc2UgYXN1bWUgZXhwbMOtY2l0YW1lbnRlIGxhIGV4aXN0ZW5jaWEgZGUgZXN0b3MgZmFjdG9yZXMgbGF0ZW50ZXMuIFBvciBlamVtcGxvLCBlbiBwc2ljb21ldHLDrWEsIGxvcyBwdW50YWplcyBkZSB2YXJpYXMgcHJ1ZWJhcyBkZSByYXpvbmFtaWVudG8sIG1lbW9yaWEgbyBhdGVuY2nDs24gcHVlZGVuIGV4cGxpY2Fyc2UgcG9yIHVuIGZhY3RvciBjb23Dum4gbGxhbWFkbyAkZyQgKGludGVsaWdlbmNpYSBnZW5lcmFsKS4gRW4gZXN0ZSBjYXNvLCBlbCBtb2RlbG8gZmFjdG9yaWFsIHBlcm1pdGUgaW5mZXJpciBxdcOpIHBhcnRlIGRlIGxhIHZhcmlhbnphIGRlIGNhZGEgcHJ1ZWJhIGNvcnJlc3BvbmRlIGEgbGEgaW5mbHVlbmNpYSBkZSBlc2UgZmFjdG9yIGNvbcO6biB5IGN1w6FsIGEgYXNwZWN0b3MgcHJvcGlvcyBkZSBsYSBwcnVlYmEuCgpFbiBjdWFudG8gYSBsb3MgY2Fzb3MgZGUgdXNvLCBlbCBtb2RlbG8gZmFjdG9yaWFsIHNlIGFwbGljYSBlbiBjYW1wb3MgY29tbyBwc2ljb2xvZ8OtYSAocGFyYSBtZWRpciBjb25zdHJ1Y3RvcyBjb21vIHBlcnNvbmFsaWRhZCwgaW50ZWxpZ2VuY2lhIG8gbW90aXZhY2lvbmVzKSwgZWNvbm9tw61hIChwYXJhIGlkZW50aWZpY2FyIGZhY3RvcmVzIGxhdGVudGVzIGNvbW8g4oCcY29uZmlhbnphIGRlbCBjb25zdW1pZG9y4oCdIGEgcGFydGlyIGRlIHZhcmlhcyBlbmN1ZXN0YXMpLCBiaW9sb2fDrWEgKHBhcmEgYWdydXBhciByYXNnb3MgZ2Vuw6l0aWNvcyByZWxhY2lvbmFkb3MpLCB5IGNpZW5jaWFzIHNvY2lhbGVzIGVuIGdlbmVyYWwuIFN1IHV0aWxpZGFkIHByaW5jaXBhbCBlcyByZWR1Y2lyIGNvbXBsZWppZGFkIGUgaWRlbnRpZmljYXIgZGltZW5zaW9uZXMgb2N1bHRhcyBkZXRyw6FzIGRlIHVuIGdyYW4gbsO6bWVybyBkZSB2YXJpYWJsZXMgY29ycmVsYWNpb25hZGFzLCBsbyBjdWFsIHBlcm1pdGUgY29uc3RydWlyIGVzY2FsYXMsIHZhbGlkYXIgY3Vlc3Rpb25hcmlvcyBvIGVsYWJvcmFyIGluZGljYWRvcmVzIGNvbXB1ZXN0b3MuCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CgoKCiMgMS4gU2ltdWxhbW9zIGRhdG9zIGNvbiA2IHZhcmlhYmxlcyB5IDIgZmFjdG9yZXMgbGF0ZW50ZXMKc2V0LnNlZWQoMTIzKQpzaW1fZGF0YSA8LSBzaW0uc3RydWN0dXJlKGZ4ID0gbWF0cml4KGMoCiAgMC44LCAwLCAgICMgVmFyMSBkZXBlbmRlIGRlbCBGYWN0b3IxCiAgMC43LCAwLCAgICMgVmFyMiBkZXBlbmRlIGRlbCBGYWN0b3IxCiAgMC42LCAwLCAgICMgVmFyMyBkZXBlbmRlIGRlbCBGYWN0b3IxCiAgMCwgMC43LCAgICMgVmFyNCBkZXBlbmRlIGRlbCBGYWN0b3IyCiAgMCwgMC44LCAgICMgVmFyNSBkZXBlbmRlIGRlbCBGYWN0b3IyCiAgMCwgMC42ICAgICMgVmFyNiBkZXBlbmRlIGRlbCBGYWN0b3IyCiksIG5yb3cgPSA2LCBuY29sID0gMiwgYnlyb3cgPSBUUlVFKSwgbiA9IDUwMCkKCiMgRXh0cmFlbW9zIGxhIG1hdHJpeiBkZSBkYXRvcwpkYXRhIDwtIHNpbV9kYXRhJG9ic2VydmVkCgojIDIuIEVzdGltYW1vcyB1biBhbsOhbGlzaXMgZmFjdG9yaWFsIGNvbiAyIGZhY3RvcmVzCmZhX3Jlc3VsdCA8LSBmYShkYXRhLCBuZmFjdG9ycyA9IDIsIHJvdGF0ZSA9ICJ2YXJpbWF4IikKCiMgMy4gTW9zdHJhbW9zIHJlc3VsdGFkb3MKcHJpbnQoZmFfcmVzdWx0KQoKIyA0LiBHcmFmaWNhbW9zIGVsIG1vZGVsbyBmYWN0b3JpYWwKZmEuZGlhZ3JhbShmYV9yZXN1bHQsIG1haW4gPSAiRGlhZ3JhbWEgZGVsIE1vZGVsbyBGYWN0b3JpYWwiKQoKCmBgYAoKIyMgNi4yKSBQcm9waWVkYWRlcyB5IHVuaWNpZGFkCgpFbiBlbCBjb250ZXh0byBkZWwgYW7DoWxpc2lzIGZhY3RvcmlhbCwgZWwgY29uY2VwdG8gZGUgcHJvcGllZGFkZXMgeSB1bmljaWRhZCBzZSByZWZpZXJlIGEgbGEgcGFydGUgZGUgbGEgdmFyaWFuemEgZGUgY2FkYSB2YXJpYWJsZSBvYnNlcnZhZGEgcXVlIG5vIGVzIGV4cGxpY2FkYSBwb3IgbG9zIGZhY3RvcmVzIGNvbXVuZXMuIENhZGEgdmFyaWFibGUgJFhfaSQgc2UgZGVzY29tcG9uZSBlbiB1bmEgcGFydGUgY29tcGFydGlkYSBjb24gbG9zIGZhY3RvcmVzIChjb211bmFsaWRhZCkgeSBvdHJhIHByb3BpYSBvIMO6bmljYS4gTWF0ZW3DoXRpY2FtZW50ZSwgc2kgJGhfaV4yJCBlcyBsYSBjb211bmFsaWRhZCBkZSBsYSB2YXJpYWJsZSAkWF9pJCwgbGEgdW5pY2lkYWQgc2UgZGVmaW5lIGNvbW8gJHVfaV4yID0gKDEtIGhfaV4yKSQgYXN1bWllbmRvIHF1ZSBsYXMgdmFyaWFibGVzIGVzdMOhbiBlc3RhbmRhcml6YWRhcy4KCkxhIHVuaWNpZGFkICR1X2leMiQgcmVwcmVzZW50YSBsYSB2YXJpYW56YSBlc3BlY8OtZmljYSB5IGVsIGVycm9yIGRlIG1lZGljacOzbiBhc29jaWFkb3MgYSAkWF9pJCwgZXMgZGVjaXIsIGxvIHF1ZSBubyBwdWVkZSBzZXIgYXRyaWJ1aWRvIGEgbG9zIGZhY3RvcmVzIGNvbXVuZXMuIFNpICR1X2leMiQgZXMgY2VyY2FubyBhIDAsIHNpZ25pZmljYSBxdWUgY2FzaSB0b2RhIGxhIHZhcmlhYmlsaWRhZCBkZSBsYSB2YXJpYWJsZSBzZSBleHBsaWNhIHBvciBsb3MgZmFjdG9yZXM7IGVuIGNhbWJpbywgdW4gdmFsb3IgYWx0byBpbmRpY2EgcXVlIGVzYSB2YXJpYWJsZSBlc3TDoSBpbmZsdWlkYSBtYXlvcm1lbnRlIHBvciBhc3BlY3RvcyBwcm9waW9zIHkgbm8gY29tcGFydGUgZXN0cnVjdHVyYSBjb24gZWwgcmVzdG8uCgpFc3RlIGNvbmNlcHRvIGVzIGNsYXZlIHBhcmEgZXZhbHVhciBsYSBjYWxpZGFkIGRlbCBtb2RlbG8gZmFjdG9yaWFsOiBwZXJtaXRlIGRlY2lkaXIgcXXDqSB2YXJpYWJsZXMgY29udmllbmUgbWFudGVuZXIgKGFxdWVsbGFzIGNvbiBiYWphIHVuaWNpZGFkIHkgYWx0YSBjb211bmFsaWRhZCkgeSBjdcOhbGVzIGFwb3J0YW4gcG9jYSBpbmZvcm1hY2nDs24gYWwgYW7DoWxpc2lzLiBFbiBsYSBwcsOhY3RpY2Egc2UgdXNhIGVuIHBzaWNvbWV0csOtYSBwYXJhIHZhbGlkYXIgw610ZW1zIGRlIHVuIHRlc3QsIGVuIGVjb25vbcOtYSBwYXJhIGRlcHVyYXIgaW5kaWNhZG9yZXMgcmVkdW5kYW50ZXMgeSBlbiBiaW9sb2fDrWEgcGFyYSBzZWxlY2Npb25hciByYXNnb3MgcmVsZXZhbnRlcywgYXNlZ3VyYW5kbyBxdWUgbG9zIGZhY3RvcmVzIGV4dHJhw61kb3MgcmVmbGVqZW4gdmVyZGFkZXJhbWVudGUgZGltZW5zaW9uZXMgY29tdW5lcyB5IG5vIHJ1aWRvIGluZGl2aWR1YWwuCgoqKkVqZW1wbG8gR3JhZmljbyoqCgpgYGB7cn0KCgojIDEuIFVzYW1vcyBhbGd1bmFzIHZhcmlhYmxlcyBkZSBtdGNhcnMKZGF0YSA8LSBtdGNhcnNbLCBjKCJtcGciLCAiaHAiLCAid3QiLCAicXNlYyIsICJkcmF0IildCgojIDIuIEFqdXN0YW1vcyB1biBtb2RlbG8gZmFjdG9yaWFsIGNvbiAyIGZhY3RvcmVzCmZhX3Jlc3VsdCA8LSBmYShkYXRhLCBuZmFjdG9ycyA9IDIsIHJvdGF0ZSA9ICJ2YXJpbWF4IikKCiMgMy4gRXh0cmFlbW9zIGNvbXVuYWxpZGFkZXMgeSB1bmljaWRhZGVzCmNvbXVuYWxpZGFkZXMgPC0gZmFfcmVzdWx0JGNvbW11bmFsaXR5CnVuaWNpZGFkZXMgPC0gZmFfcmVzdWx0JHVuaXF1ZW5lc3NlcwoKIyA0LiBNb3N0cmFtb3MgdGFibGEKcmVzdWx0YWRvcyA8LSBkYXRhLmZyYW1lKAogIFZhcmlhYmxlID0gbmFtZXMoY29tdW5hbGlkYWRlcyksCiAgQ29tdW5hbGlkYWQgPSByb3VuZChjb211bmFsaWRhZGVzLCAzKSwKICBVbmljaWRhZCA9IHJvdW5kKHVuaWNpZGFkZXMsIDMpCikKcHJpbnQocmVzdWx0YWRvcykKCiMgNS4gR3JhZmljYW1vcyBjb211bmFsaWRhZCB2cyB1bmljaWRhZApiYXJwbG90KAogIHJiaW5kKGNvbXVuYWxpZGFkZXMsIHVuaWNpZGFkZXMpLAogIGJlc2lkZSA9IFRSVUUsCiAgbmFtZXMuYXJnID0gbmFtZXMoY29tdW5hbGlkYWRlcyksCiAgY29sID0gYygic3RlZWxibHVlIiwgInRvbWF0byIpLAogIGxlZ2VuZC50ZXh0ID0gYygiQ29tdW5hbGlkYWQgKGheMikiLCAiVW5pY2lkYWQgKHVeMikiKSwKICBtYWluID0gIkNvbXVuYWxpZGFkZXMgeSBVbmljaWRhZGVzIHBvciBWYXJpYWJsZSIKKQoKYGBgCgojIyA2LjMpIENyaXRlcmlvIFZhcmltYXgKCkVsIGNyaXRlcmlvIHZhcmltYXggZXMgdW4gbcOpdG9kbyBkZSByb3RhY2nDs24gb3J0b2dvbmFsIGVuIGVsIGFuw6FsaXNpcyBmYWN0b3JpYWwgeSBlbiBlbCBhbsOhbGlzaXMgZGUgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMuIFN1IG9iamV0aXZvIGVzIHNpbXBsaWZpY2FyIGxhIGVzdHJ1Y3R1cmEgZGUgbGFzIGNhcmdhcyBmYWN0b3JpYWxlcyAoJFxsYW1iZGFfe2lqfSQpIG1heGltaXphbmRvIGxhIHZhcmlhbnphIGRlIGxvcyBjdWFkcmFkb3MgZGUgbGFzIGNhcmdhcyBkZW50cm8gZGUgY2FkYSBmYWN0b3IuIERpY2hvIGRlIG90cmEgZm9ybWEsIGJ1c2NhIHF1ZSBjYWRhIHZhcmlhYmxlIHRlbmdhIGNhcmdhcyBhbHRhcyBlbiB1biBzb2xvIGZhY3RvciB5IGJhamFzIGVuIGxvcyBkZW3DoXMsIGxvIHF1ZSBmYWNpbGl0YSBsYSBpbnRlcnByZXRhY2nDs24gZGUgbG9zIGZhY3RvcmVzLgoKTWF0ZW3DoXRpY2FtZW50ZSwgZWwgY3JpdGVyaW8gdmFyaW1heCBjb25zaXN0ZSBlbiBtYXhpbWl6YXIgbGEgZnVuY2nDs246CgokJFYgPSBcc3VtX3tqPTF9Xm0gXGxlZnRbIFxmcmFjezF9e3B9IFxzdW1fe2k9MX1ecCBcbGFtYmRhX3tpan1eNCBcOy1cOyBcbGVmdCggXGZyYWN7MX17cH0gXHN1bV97aT0xfV5wIFxsYW1iZGFfe2lqfV4yIFxyaWdodCleMiBccmlnaHRdCiQkCgpkb25kZSAkcCQgZXMgZWwgbsO6bWVybyBkZSB2YXJpYWJsZXMgeSAkbSQgZWwgbsO6bWVybyBkZSBmYWN0b3Jlcy4gTGEgaWRlYSBlcyBxdWUsIGFsIG1heGltaXphciAkViQsIHNlIG9idGllbmVuIGNhcmdhcyBmYWN0b3JpYWxlcyBtw6FzIOKAnHBvbGFyaXphZGFz4oCdOiBhbGd1bmFzIG11eSBjZXJjYW5hcyBhIDAgeSBvdHJhcyBtw6FzIGdyYW5kZXMsIGxvIHF1ZSByZWZ1ZXJ6YSBsYSBjbGFyaWRhZCBlbiBsYSBhc29jaWFjacOzbiB2YXJpYWJsZeKAk2ZhY3Rvci4KCkVswqAqKnZhcmltYXgqKsKgZXMgdW5hIHTDqWNuaWNhIHF1ZSBzZSBhcGxpY2EgZGVzcHXDqXMgZGUgZXh0cmFlciBmYWN0b3JlcyBlbiB1biBhbsOhbGlzaXMgZmFjdG9yaWFsIHkgc2lydmUgcGFyYSBxdWUgbG9zIHJlc3VsdGFkb3Mgc2VhbiBtw6FzIGbDoWNpbGVzIGRlIGludGVycHJldGFyOiBsbyBxdWUgaGFjZSBlcyDigJxnaXJhcuKAnSBsb3MgZmFjdG9yZXMgZGUgbWFuZXJhIHF1ZSBjYWRhIHZhcmlhYmxlIHF1ZWRlIGFzb2NpYWRhIHByaW5jaXBhbG1lbnRlIGNvbiB1biBzb2xvIGZhY3RvciB5IHRlbmdhIHZhbG9yZXMgY2VyY2Fub3MgYSBjZXJvIGVuIGxvcyBkZW3DoXM7IGFzw60sIGVuIHZleiBkZSB0ZW5lciBjYXJnYXMgbWV6Y2xhZGFzIHkgY29uZnVzYXMsIHNlIG9idGllbmUgdW5hIGVzdHJ1Y3R1cmEgbcOhcyBjbGFyYSBkb25kZSBjYWRhIGZhY3RvciByZXByZXNlbnRhIHVuIGdydXBvIGRlIHZhcmlhYmxlcyBiaWVuIGRlZmluaWRvLCBsbyBxdWUgYXl1ZGEgYSBkYXJsZSB1biBzaWduaWZpY2FkbyBtw6FzIHNlbmNpbGxvIHkgw7p0aWwgYSBsb3MgZmFjdG9yZXMuCgpFbCBjcml0ZXJpbyB2YXJpbWF4IHNlIHVzYSBhbXBsaWFtZW50ZSBlbiBwc2ljb2xvZ8OtYSAocGFyYSBpbnRlcnByZXRhciBjdWVzdGlvbmFyaW9zIHkgZXNjYWxhcyksIGVuIGVjb25vbcOtYSAocGFyYSBpZGVudGlmaWNhciBkaW1lbnNpb25lcyBjbGFyYXMgZW4gZW5jdWVzdGFzIG8gaW5kaWNhZG9yZXMpLCB5IGVuIGJpb2xvZ8OtYSBvIGNpZW5jaWFzIHNvY2lhbGVzIGVuIGdlbmVyYWwgKHBhcmEgYWdydXBhciByYXNnb3MgbyBjYXJhY3RlcsOtc3RpY2FzKS4gU3UgcHJpbmNpcGFsIHZlbnRhamEgZXMgcXVlIG1lam9yYSBsYSBsZWdpYmlsaWRhZCBkZSBsb3MgcmVzdWx0YWRvcyBzaW4gYWx0ZXJhciBsYSBjYW50aWRhZCBkZSB2YXJpYW56YSBleHBsaWNhZGEsIGhhY2llbmRvIHF1ZSBsb3MgZmFjdG9yZXMgcm90YWRvcyBzZWFuIG3DoXMgZsOhY2lsZXMgZGUgZXRpcXVldGFyIGUgaW50ZXJwcmV0YXIuCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CgoKIyAxLiBTaW11bGFtb3MgZGF0b3M6IDYgdmFyaWFibGVzLCAyIGZhY3RvcmVzCnNldC5zZWVkKDEyMykKc2ltIDwtIHNpbS5zdHJ1Y3R1cmUoCiAgZnggPSBtYXRyaXgoYygKICAgIDAuOCwgMCwgICAjIFZhcjEgZGVwZW5kZSBkZSBGYWN0b3IxCiAgICAwLjcsIDAsICAgIyBWYXIyIGRlcGVuZGUgZGUgRmFjdG9yMQogICAgMC42LCAwLCAgICMgVmFyMyBkZXBlbmRlIGRlIEZhY3RvcjEKICAgIDAsIDAuNywgICAjIFZhcjQgZGVwZW5kZSBkZSBGYWN0b3IyCiAgICAwLCAwLjgsICAgIyBWYXI1IGRlcGVuZGUgZGUgRmFjdG9yMgogICAgMCwgMC42ICAgICMgVmFyNiBkZXBlbmRlIGRlIEZhY3RvcjIKICApLCBucm93ID0gNiwgbmNvbCA9IDIsIGJ5cm93ID0gVFJVRSksCiAgbiA9IDUwMAopCgpkYXRhIDwtIHNpbSRvYnNlcnZlZAoKIyAyLiBBbsOhbGlzaXMgZmFjdG9yaWFsIFNJTiByb3RhY2nDs24KZmFfc2luIDwtIGZhKGRhdGEsIG5mYWN0b3JzID0gMiwgcm90YXRlID0gIm5vbmUiKQoKIyAzLiBBbsOhbGlzaXMgZmFjdG9yaWFsIENPTiByb3RhY2nDs24gdmFyaW1heApmYV92YXJpbWF4IDwtIGZhKGRhdGEsIG5mYWN0b3JzID0gMiwgcm90YXRlID0gInZhcmltYXgiKQoKIyA0LiBDb21wYXJhY2nDs24gZGUgY2FyZ2FzIGZhY3RvcmlhbGVzCnByaW50KGZhX3NpbiRsb2FkaW5ncywgY3V0b2ZmID0gMC4zKQpwcmludChmYV92YXJpbWF4JGxvYWRpbmdzLCBjdXRvZmYgPSAwLjMpCgojIDUuIERpYWdyYW1hcyBncsOhZmljb3MKcGFyKG1mcm93ID0gYygxLDIpKSAgIyBkb3MgZ3LDoWZpY29zIGxhZG8gYSBsYWRvCmZhLmRpYWdyYW0oZmFfc2luLCBtYWluID0gIlNpbiByb3RhY2nDs24iKQpmYS5kaWFncmFtKGZhX3ZhcmltYXgsIG1haW4gPSAiQ29uIHJvdGFjacOzbiBWYXJpbWF4IikKCmBgYAoKIyMgNi40KSBBbsOhbGlzaXMgZGUgZmFjdG9yIGNvbXBvbmVudGUgcHJpbmNpcGFsCgpFbCBhbsOhbGlzaXMgZGUgZmFjdG9yIGNvbXBvbmVudGUgcHJpbmNpcGFsICh0YW1iacOpbiBsbGFtYWRvIHByaW5jaXBhbCBjb21wb25lbnQgZmFjdG9yIGFuYWx5c2lzIG8gcHJpbmNpcGFsIGNvbXBvbmVudCBtZXRob2QpIGVzIHVuIGVuZm9xdWUgZGVudHJvIGRlbCBhbsOhbGlzaXMgZmFjdG9yaWFsIGVuIGVsIHF1ZSBsb3MgZmFjdG9yZXMgc2UgZXh0cmFlbiBhIHBhcnRpciBkZSBsYXMgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMgZGUgbGEgbWF0cml6IGRlIGNvcnJlbGFjaW9uZXMuIEEgZGlmZXJlbmNpYSBkZWwgbW9kZWxvIGZhY3RvcmlhbCBjbMOhc2ljbywgcXVlIGFzdW1lIGZhY3RvcmVzIGxhdGVudGVzIHkgdW5pY2lkYWRlcywgZXN0ZSBtw6l0b2RvIHBhcnRlIGRlIGxhIGlkZWEgZGUgcXVlIGxhIHZhcmlhbnphIGRlIGNhZGEgdmFyaWFibGUgcHVlZGUgZXhwbGljYXJzZSBpbmljaWFsbWVudGUgcG9yIHVuYSBjb21iaW5hY2nDs24gbGluZWFsIGRlIGNvbXBvbmVudGVzLCB5IHNlIGFwcm92ZWNoYSBlc2EgZGVzY29tcG9zaWNpw7NuIHBhcmEgZXN0aW1hciBsYXMgY2FyZ2FzIGZhY3RvcmlhbGVzIGluaWNpYWxlcy4KCkVsIHByb2NlZGltaWVudG8gY29uc2lzdGUgZW4gY2FsY3VsYXIgbGFzIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzIGRlIGxvcyBkYXRvcywgcXVlIHNvbiBjb21iaW5hY2lvbmVzIGxpbmVhbGVzIGRlIGxhcyB2YXJpYWJsZXMgb3JpZ2luYWxlcywgZGUgbWFuZXJhIHF1ZSBsYSBwcmltZXJhIGNvbXBvbmVudGUgZXhwbGljYSBsYSBtYXlvciBwYXJ0ZSBkZSBsYSB2YXJpYW56YSB0b3RhbCwgbGEgc2VndW5kYSBleHBsaWNhIGxhIG3DoXhpbWEgdmFyaWFuemEgcmVzdGFudGUsIHkgYXPDrSBzdWNlc2l2YW1lbnRlLiBTaSBsbGFtYW1vcyAkWCQgYSBsYSBtYXRyaXogZGUgZGF0b3MgZXN0YW5kYXJpemFkb3MsIGVsIG3DqXRvZG8gc2UgYmFzYSBlbiBsYSBkZXNjb21wb3NpY2nDs24gZXNwZWN0cmFsIGRlIGxhIG1hdHJpeiBkZSBjb3JyZWxhY2lvbmVzICRSJCwgZGUgZm9ybWEgcXVlOgoKJCRRID0gUSBcTGFtYmRhIFHCtCQkCgpkb25kZSAkUSQgZXMgbGEgbWF0cml6IGRlIHZlY3RvcmVzIHByb3Bpb3MgKGRpcmVjY2lvbmVzIHByaW5jaXBhbGVzKSB5ICRcTGFtYmRhJCBlcyBsYSBtYXRyaXogZGlhZ29uYWwgY29uIGxvcyB2YWxvcmVzIHByb3Bpb3MgKHZhcmlhbnphcyBleHBsaWNhZGFzKS4gTGFzIGNhcmdhcyBmYWN0b3JpYWxlcyBpbmljaWFsZXMgc2Ugb2J0aWVuZW4gYSBwYXJ0aXIgZGUgZXN0b3MgYXV0b3ZhbG9yZXMgeSBhdXRvdmVjdG9yZXMuIEVsIGFuw6FsaXNpcyBkZSBmYWN0b3IgY29tcG9uZW50ZSBwcmluY2lwYWwgc2UgdXRpbGl6YSBwcmluY2lwYWxtZW50ZSBwYXJhIHJlZHVjaXIgbGEgZGltZW5zaW9uYWxpZGFkIHkgcGFyYSBpZGVudGlmaWNhciBlc3RydWN0dXJhcyBsYXRlbnRlcyBhcHJveGltYWRhcywgY3VhbmRvIG5vIHNlIGJ1c2NhIHVuIG1vZGVsbyBlc3RyaWN0byBkZSBmYWN0b3JlcyBwZXJvIHPDrSB1bmEgcmVwcmVzZW50YWNpw7NuIG3DoXMgbWFuZWphYmxlIGRlIGxvcyBkYXRvcy4gQWRlbcOhcywgZXMgw7p0aWwgY29tbyBwYXNvIHByZXZpbyBhIHJvdGFjaW9uZXMgKGNvbW8gdmFyaW1heCkgcXVlIG1lam9yYW4gbGEgaW50ZXJwcmV0YWNpw7NuIGRlIGxvcyBmYWN0b3JlcyBleHRyYcOtZG9zLgoKRW4gY3VhbnRvIGEgY2Fzb3MgZGUgdXNvLCBzZSBhcGxpY2EgZW4gcHNpY29tZXRyw61hIHBhcmEgY29uc3RydWlyIHkgZGVwdXJhciBjdWVzdGlvbmFyaW9zLCBlbiBlY29ub23DrWEgcGFyYSByZXN1bWlyIGluZGljYWRvcmVzIGVuIGZhY3RvcmVzIGNvbW8g4oCcYWN0aXZpZGFkIGVjb27Ds21pY2HigJ0gbyDigJxjb25maWFuemHigJ0sIGVuIGJpb2xvZ8OtYSBwYXJhIGFncnVwYXIgdmFyaWFibGVzIGdlbsOpdGljYXMgbyBkZSByYXNnb3MsIHkgZW4gbWFya2V0aW5nIHBhcmEgaWRlbnRpZmljYXIgcGF0cm9uZXMgZGUgY29tcG9ydGFtaWVudG8gZGVsIGNvbnN1bWlkb3IuIFN1IHZlbnRhamEgZXMgcXVlIGVzIGNvbXB1dGFjaW9uYWxtZW50ZSBzZW5jaWxsbyB5IGNhcHRhIGdyYW4gcGFydGUgZGUgbGEgdmFyaWFiaWxpZGFkIGRlIGxvcyBkYXRvcywgYXVucXVlIHN1IGludGVycHJldGFjacOzbiBjb21vIOKAnGZhY3RvcmVzIGxhdGVudGVz4oCdIGVzIG1lbm9zIHJpZ3Vyb3NhIHF1ZSBlbiBlbCBhbsOhbGlzaXMgZmFjdG9yaWFsIGNvbcO6bi4KCmBgYHtyfQoKCgojIDEuIFNlbGVjY2lvbmFtb3MgYWxndW5hcyB2YXJpYWJsZXMgZGUgbXRjYXJzCmRhdGEgPC0gbXRjYXJzWywgYygibXBnIiwgImhwIiwgInd0IiwgInFzZWMiLCAiZHJhdCIpXQoKIyAyLiBBbsOhbGlzaXMgZmFjdG9yaWFsIGNsw6FzaWNvIChtw6l0b2RvICJtaW5yZXMiKQpmYV9jbGFzc2ljIDwtIGZhKGRhdGEsIG5mYWN0b3JzID0gMiwgZm0gPSAibWlucmVzIiwgcm90YXRlID0gInZhcmltYXgiKQoKIyAzLiBBbsOhbGlzaXMgZGUgZmFjdG9yIGNvbXBvbmVudGUgcHJpbmNpcGFsIChmbSA9ICJwYyIpCmZhX3BjIDwtIGZhKGRhdGEsIG5mYWN0b3JzID0gMiwgZm0gPSAicGMiLCByb3RhdGUgPSAidmFyaW1heCIpCgojIDQuIE1vc3RyYXIgY2FyZ2FzIGZhY3RvcmlhbGVzCmNhdCgiPT09IEFuw6FsaXNpcyBmYWN0b3JpYWwgY2zDoXNpY28gKG1pbnJlcykgPT09XG4iKQpwcmludChmYV9jbGFzc2ljJGxvYWRpbmdzLCBjdXRvZmYgPSAwLjMpCgpjYXQoIlxuPT09IEFuw6FsaXNpcyBmYWN0b3IgY29tcG9uZW50ZSBwcmluY2lwYWwgKHBjKSA9PT1cbiIpCnByaW50KGZhX3BjJGxvYWRpbmdzLCBjdXRvZmYgPSAwLjMpCgojIDUuIEdyYWZpY2FyIHJlc3VsdGFkb3MKcGFyKG1mcm93ID0gYygxLDIpKQpmYS5kaWFncmFtKGZhX2NsYXNzaWMsIG1haW4gPSAiRmFjdG9yaWFsIGNsw6FzaWNvIChtaW5yZXMpIikKZmEuZGlhZ3JhbShmYV9wYywgbWFpbiA9ICJGYWN0b3IgQ29tcG9uZW50ZSBQcmluY2lwYWwgKHBjKSIpCgpgYGAKCiMjIDYuNSkgQW7DoWxpc2lzIGRlIGZhY3RvciBwcmluY2lwYWwKCkVsIGFuw6FsaXNpcyBkZSBmYWN0b3IgcHJpbmNpcGFsIGVzIHVuIG3DqXRvZG8gZGVudHJvIGRlbCBhbsOhbGlzaXMgZmFjdG9yaWFsIHF1ZSBidXNjYSBlc3RpbWFyIGxvcyBmYWN0b3JlcyBhIHBhcnRpciBkZSBsYSB2YXJpYW56YSBjb23Dum4gZW50cmUgbGFzIHZhcmlhYmxlcywgZGlmZXJlbmNpw6FuZG9zZSBkZWwgbcOpdG9kbyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcyBxdWUgdXNhIHRvZGEgbGEgdmFyaWFuemEuIExhIGlkZWEgY2VudHJhbCBlcyBxdWUgY2FkYSB2YXJpYWJsZSBvYnNlcnZhZGEgJFhfaSQgcHVlZGUgZGVzY29tcG9uZXJzZSBlbiB1bmEgcGFydGUgZXhwbGljYWRhIHBvciBsb3MgZmFjdG9yZXMgY29tdW5lcyB5IG90cmEgcG9yIHN1IHVuaWNpZGFkICR1X2leMiQuIEZvcm1hbG1lbnRlLCBzaSAkXFNpZ21hJCBlcyBsYSBtYXRyaXogZGUgY29ycmVsYWNpb25lcywgc2UgaW50ZW50YSBhcHJveGltYXJsYSBjb21vOgoKJCRcU2lnbWEgXGFwcHJveCBcTGFtYmRhIFxMYW1iZGEnICsgXFBzaSQkIGRvbmRlICRcTGFtYmRhJCBlcyBsYSBtYXRyaXogZGUgY2FyZ2FzIGZhY3RvcmlhbGVzIHkgJFxQc2kkIGVzIHVuYSBtYXRyaXogZGlhZ29uYWwgY29uIGxhcyB1bmljaWRhZGVzLgoKRXN0ZSBtw6l0b2RvIHNlIGxsYW1hIGRlIGZhY3RvciBwcmluY2lwYWwgcG9ycXVlIGxhcyBjb211bmFsaWRhZGVzIGluaWNpYWxlcyBzZSBlc3RpbWFuIGNvbW8gbGFzIGNvcnJlbGFjaW9uZXMgbcO6bHRpcGxlcyBkZSBjYWRhIHZhcmlhYmxlIGNvbiBsYXMgZGVtw6FzLCBlbiBsdWdhciBkZSBzdXBvbmVyIHF1ZSB0b2RhIGxhIHZhcmlhbnphIGVzIGNvbcO6biAoY29tbyBlbiBQQ0EpLiBBc8OtLCBlbiBjYWRhIGl0ZXJhY2nDs24sIHNlIHJlY2FsY3VsYW4gbGFzIGNhcmdhcyBmYWN0b3JpYWxlcyBhanVzdGFuZG8gbGFzIGNvbXVuYWxpZGFkZXMgaGFzdGEgcXVlIHNlIGVzdGFiaWxpemFuLCBsb2dyYW5kbyB1biBtb2RlbG8gcXVlIHJlZmxlamEgbWVqb3IgbGEgZXhpc3RlbmNpYSBkZSBmYWN0b3JlcyBsYXRlbnRlcy4KCkVsIGFuw6FsaXNpcyBkZSBmYWN0b3IgcHJpbmNpcGFsIHNlIHV0aWxpemEgY3VhbmRvIHNlIHF1aWVyZSB1biBtb2RlbG8gcXVlIHJlcHJlc2VudGUgZmFjdG9yZXMgY29tdW5lcyBzdWJ5YWNlbnRlcywgbcOhcyBjZXJjYW5vIGFsIHBsYW50ZWFtaWVudG8gdGXDs3JpY28gZGVsIGFuw6FsaXNpcyBmYWN0b3JpYWwgY2zDoXNpY28uIEVzIGVzcGVjaWFsbWVudGUgw7p0aWwgZW4gcHNpY29tZXRyw61hLCBjaWVuY2lhcyBzb2NpYWxlcyB5IGVjb25vbcOtYSwgY3VhbmRvIGVsIGludGVyw6lzIGVzIGluZmVyaXIgY29uc3RydWN0b3Mgbm8gb2JzZXJ2YWJsZXMgY29tbyDigJxpbnRlbGlnZW5jaWHigJ0sIOKAnG1vdGl2YWNpw7Nu4oCdIG8g4oCcY29uZmlhbnph4oCdLCBxdWUgZXhwbGljYW4gbGFzIGNvcnJlbGFjaW9uZXMgZW50cmUgdmFyaWFibGVzLgoKRW4gY3VhbnRvIGEgc3VzIGNhc29zIGRlIHVzbywgYXBhcmVjZSBlbiBsYSBjb25zdHJ1Y2Npw7NuIHkgdmFsaWRhY2nDs24gZGUgY3Vlc3Rpb25hcmlvcyAocGFyYSBpZGVudGlmaWNhciDDrXRlbXMgcXVlIG1pZGVuIGxhIG1pc21hIGRpbWVuc2nDs24pLCBlbiBlc3R1ZGlvcyBkZSBtZXJjYWRvIChwYXJhIGRldGVjdGFyIGZhY3RvcmVzIG9jdWx0b3MgZW4gbGFzIHByZWZlcmVuY2lhcyBkZWwgY29uc3VtaWRvciksIHkgZW4gYW7DoWxpc2lzIHNvY2lvZWNvbsOzbWljb3MgKHBhcmEgZXh0cmFlciBkaW1lbnNpb25lcyBsYXRlbnRlcyBhIHBhcnRpciBkZSBpbmRpY2Fkb3JlcyByZWdpb25hbGVzIG8gbWFjcm9lY29uw7NtaWNvcykuIFN1IHZlbnRhamEgZXMgcXVlIHNlIGVuZm9jYSBlbiBsYSB2YXJpYW56YSBjb21wYXJ0aWRhIGVudHJlIHZhcmlhYmxlcywgZXZpdGFuZG8gZWwgc2VzZ28gZGUgaW5jbHVpciB2YXJpYW56YSDDum5pY2EgbyBkZSBlcnJvciBlbiBsYSBlc3RpbWFjacOzbiBkZSBmYWN0b3Jlcy4KCmBgYHtyfQoKCiMgMS4gVXNhbW9zIGFsZ3VuYXMgdmFyaWFibGVzIGRlIG10Y2FycwpkYXRhIDwtIG10Y2Fyc1ssIGMoIm1wZyIsICJocCIsICJ3dCIsICJxc2VjIiwgImRyYXQiKV0KCiMgMi4gQW7DoWxpc2lzIGZhY3RvcmlhbCBjb24gbcOpdG9kbyBkZSBmYWN0b3IgcHJpbmNpcGFsIChQcmluY2lwYWwgQXhpcykKZmFfcGEgPC0gZmEoZGF0YSwgbmZhY3RvcnMgPSAyLCBmbSA9ICJwYSIsIHJvdGF0ZSA9ICJ2YXJpbWF4IikKCiMgMy4gQW7DoWxpc2lzIGZhY3RvcmlhbCBjb24gbcOpdG9kbyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcyAoUEMpCmZhX3BjIDwtIGZhKGRhdGEsIG5mYWN0b3JzID0gMiwgZm0gPSAicGMiLCByb3RhdGUgPSAidmFyaW1heCIpCgojIDQuIE1vc3RyYW1vcyBjYXJnYXMgZmFjdG9yaWFsZXMKY2F0KCI9PT0gRmFjdG9yIFByaW5jaXBhbCAocGEpID09PVxuIikKcHJpbnQoZmFfcGEkbG9hZGluZ3MsIGN1dG9mZiA9IDAuMykKCmNhdCgiXG49PT0gRmFjdG9yIENvbXBvbmVudGUgUHJpbmNpcGFsIChwYykgPT09XG4iKQpwcmludChmYV9wYyRsb2FkaW5ncywgY3V0b2ZmID0gMC4zKQoKIyA1LiBHcmFmaWNhbW9zIGxvcyByZXN1bHRhZG9zCnBhcihtZnJvdyA9IGMoMSwyKSkgICMgZG9zIGdyw6FmaWNvcyBsYWRvIGEgbGFkbwpmYS5kaWFncmFtKGZhX3BhLCBtYWluID0gIk3DqXRvZG8gRmFjdG9yIFByaW5jaXBhbCAocGEpIikKZmEuZGlhZ3JhbShmYV9wYywgbWFpbiA9ICJNw6l0b2RvIENvbXBvbmVudGUgUHJpbmNpcGFsIChwYykiKQoKYGBgCgojIyA2LjYpIEVzdGltYWNpb24gZGUgbG9zIGZhY3RvcmVzCgpMYSBlc3RpbWFjacOzbiBkZSBmYWN0b3JlcyBlcyB1bmEgdMOpY25pY2EgZXN0YWTDrXN0aWNhIHV0aWxpemFkYSBwYXJhIHJlZHVjaXIgbGEgZGltZW5zaW9uYWxpZGFkIGRlIHVuIGNvbmp1bnRvIGRlIHZhcmlhYmxlcyBvYnNlcnZhZGFzLCBpZGVudGlmaWNhbmRvIHVuIG7Dum1lcm8gbWVub3IgZGUgdmFyaWFibGVzIGxhdGVudGVzIGxsYW1hZGFzIGZhY3RvcmVzLiBFc3RhcyB2YXJpYWJsZXMgbGF0ZW50ZXMgZXhwbGljYW4gbGEgbWF5b3IgcGFydGUgZGUgbGEgdmFyaWFuemEgY29tw7puIGVudHJlIGxhcyB2YXJpYWJsZXMgb3JpZ2luYWxlcy4KCk1hdGVtw6F0aWNhbWVudGUsIHNlIGFzdW1lIHF1ZSBjYWRhIHZhcmlhYmxlIG9ic2VydmFkYSAkWF9pJCBwdWVkZSBleHByZXNhcnNlIGNvbW8gdW5hIGNvbWJpbmFjacOzbiBsaW5lYWwgZGUgZmFjdG9yZXMgJEZfaiQgbcOhcyB1biB0w6lybWlubyBkZSBlcnJvcjoKCiQkIFhfaSA9IFxsYW1iZGEqe2kxfUZfMSArKiBcbGFtYmRhe2kyfUZfMiArIFxkb3RzICsgXGxhbWJkYVxfe2ltfUZfbSArIFxlcHNpbG9uXF9pJCQKCmRvbmRlICRcbGFtYmRhX3tpan0kIHNvbiBsYXMgY2FyZ2FzIGZhY3RvcmlhbGVzIHkgJFxlcHNpbG9uX2kkIGVzIGVsIGVycm9yIGVzcGVjw61maWNvLgoKU3UgcHJpbmNpcGFsIHV0aWxpZGFkIGVzIGlkZW50aWZpY2FyIGVzdHJ1Y3R1cmFzIHN1YnlhY2VudGVzIGVuIGRhdG9zIGNvbXBsZWpvcywgc2ltcGxpZmljYW5kbyBlbCBhbsOhbGlzaXMgc2luIHBlcmRlciBpbmZvcm1hY2nDs24gZXNlbmNpYWwuIEVzdG8gcGVybWl0ZSBhZ3J1cGFyIHZhcmlhYmxlcyBjb3JyZWxhY2lvbmFkYXMgZW4gZmFjdG9yZXMgY29tdW5lcywgbG8gcXVlIGZhY2lsaXRhIGxhIGludGVycHJldGFjacOzbiB5IHJlZHVjZSBwcm9ibGVtYXMgZGUgbXVsdGljb2xpbmVhbGlkYWQuIEFkZW3DoXMsIGF5dWRhIGEgY29uc3RydWlyIGVzY2FsYXMgbyDDrW5kaWNlcyBhIHBhcnRpciBkZSBtw7psdGlwbGVzIGluZGljYWRvcmVzLCBtdXkgdXNhZG8gZW4gY2llbmNpYXMgc29jaWFsZXMsIHBzaWNvbWV0csOtYSB5IGVjb25vbcOtYQoKRW50cmUgbG9zICoqbcOpdG9kb3MgZGUgZXN0aW1hY2nDs24qKiBtw6FzIGNvbXVuZXMgZXN0w6FuOgoKLSAgICoqTcOheGltYSB2ZXJvc2ltaWxpdHVkIChNTCkqKjogYnVzY2EgZXN0aW1hciBwYXLDoW1ldHJvcyBxdWUgbWF4aW1pemFuIGxhIHByb2JhYmlsaWRhZCBkZSBsb3MgZGF0b3Mgb2JzZXJ2YWRvcy4KCi0gICAqKkNvbXBvbmVudGVzIHByaW5jaXBhbGVzIChQQ0EpKio6IGF1bnF1ZSBubyBlcyBlc3RyaWN0YW1lbnRlIGFuw6FsaXNpcyBmYWN0b3JpYWwsIHNlIHVzYSBwYXJhIGVzdGltYXIgZmFjdG9yZXMgaW5pY2lhbGVzLgoKLSAgICoqTcOpdG9kbyBkZSBtw61uaW1vcyBjdWFkcmFkb3MqKjogbWluaW1pemEgbGEgZGlmZXJlbmNpYSBlbnRyZSBsYSBtYXRyaXogZGUgY29ycmVsYWNpb25lcyBvYnNlcnZhZGEgeSBsYSByZXByb2R1Y2lkYSBwb3IgZWwgbW9kZWxvIGZhY3RvcmlhbC4KCioqQ2Fzb3MgZGUgdXNvKio6CgotICAgRW4gcHNpY29sb2fDrWEsIHBhcmEgaWRlbnRpZmljYXIgZGltZW5zaW9uZXMgbGF0ZW50ZXMgY29tbyByYXNnb3MgZGUgcGVyc29uYWxpZGFkIGEgcGFydGlyIGRlIGN1ZXN0aW9uYXJpb3MuCgotICAgRW4gbWFya2V0aW5nLCBwYXJhIHNlZ21lbnRhciBjb25zdW1pZG9yZXMgc2Vnw7puIHBhdHJvbmVzIGRlIGNvbXBvcnRhbWllbnRvLgoKLSAgIEVuIGZpbmFuemFzLCBwYXJhIHJlZHVjaXIgZWwgbsO6bWVybyBkZSBpbmRpY2Fkb3JlcyBlY29uw7NtaWNvcyBlbiBtb2RlbG9zIHByZWRpY3Rpdm9zLgoKLSAgIEVuIGludmVzdGlnYWNpw7NuIHNvY2lhbCwgcGFyYSBjb25zdHJ1aXIgw61uZGljZXMgY29tbyBlbCBkZSBjYWxpZGFkIGRlIHZpZGEgYSBwYXJ0aXIgZGUgbcO6bHRpcGxlcyB2YXJpYWJsZXMuCgoqKk1ldG9kbyBkZSBCYXJ0bGV0dCoqCgpFbCBtw6l0b2RvIGRlIEJhcnRsZXR0IGVzIHVuYSB0w6ljbmljYSBwYXJhIGVzdGltYXIgbG9zIHB1bnRhamVzIGZhY3RvcmlhbGVzIGVuIGFuw6FsaXNpcyBmYWN0b3JpYWwsIGJ1c2NhbmRvIG9idGVuZXIgZXN0aW1hY2lvbmVzIGluc2VzZ2FkYXMgeSBlZmljaWVudGVzIGJham8gZWwgc3VwdWVzdG8gZGUgbm9ybWFsaWRhZCBtdWx0aXZhcmlhbnRlLiBBIGRpZmVyZW5jaWEgZGVsIG3DqXRvZG8gZGUgcmVncmVzacOzbiwgcXVlIG1pbmltaXphIGVycm9yZXMgZW4gbGFzIHZhcmlhYmxlcyBvYnNlcnZhZGFzLCBCYXJ0bGV0dCBtaW5pbWl6YSBsYSB2YXJpYW56YSBkZSBsb3MgZXJyb3JlcyBlbiBsb3MgZmFjdG9yZXMsIGxvIHF1ZSBwcm9kdWNlIGVzdGltYWNpb25lcyBtw6FzIHByZWNpc2FzIGN1YW5kbyBlbCBtb2RlbG8gZmFjdG9yaWFsIGVzIGNvcnJlY3RvLiBMYSBmw7NybXVsYSBnZW5lcmFsIHBhcmEgbG9zIHB1bnRhamVzIGZhY3RvcmlhbGVzIGVzOgoKJCRcaGF0e1xtYXRoYmZ7Rn19ID0gKFxtYXRoYmZ7XExhbWJkYX1eXHRvcCBcbWF0aGJme1xQc2l9XnstMX0gXG1hdGhiZntcTGFtYmRhfSleey0xfSBcbWF0aGJme1xMYW1iZGF9Xlx0b3AgXG1hdGhiZntcUHNpfV57LTF9IFxtYXRoYmZ7WH0kJAoKZG9uZGUgJFxtYXRoYmZ7XExhbWJkYX0kIGVzIGxhIG1hdHJpeiBkZSBjYXJnYXMgZmFjdG9yaWFsZXMsICRcbWF0aGJme1xQc2l9JCBsYSBtYXRyaXogZGlhZ29uYWwgZGUgdmFyaWFuemFzIGRlIGVycm9yIHkgJFxtYXRoYmZ7WH0kIGVsIHZlY3RvciBkZSB2YXJpYWJsZXMgZXN0YW5kYXJpemFkYXMuIEVzdGUgbcOpdG9kbyBzZSB1c2EgZW4gcHNpY29tZXRyw61hLCBjaWVuY2lhcyBzb2NpYWxlcyB5IGVjb25vbcOtYSBjdWFuZG8gc2UgcmVxdWllcmUgaW50ZXJwcmV0YXIgZmFjdG9yZXMgbGF0ZW50ZXMgY29uIG3DrW5pbWEgZGlzdG9yc2nDs24uCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIEVqZW1wbG8gZ3LDoWZpY28gZGUgRXN0aW1hY2nDs24gZGUgRmFjdG9yZXMgZW4gUgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KCgojIEVzdGFibGVjZW1vcyBzZW1pbGxhIGRlIHJlcHJvZHVjaWJpbGlkYWQKc2V0LnNlZWQoMTIzKQoKIyAxKSBTaW11bGFjacOzbiBkZSBkYXRvcyBjb24gZXN0cnVjdHVyYSBmYWN0b3JpYWwgY29ub2NpZGEgKDIgZmFjdG9yZXMsIDggdmFyaWFibGVzKQpuICA8LSA1MDAgICAjIG7Dum1lcm8gZGUgb2JzZXJ2YWNpb25lcwpwICA8LSA4ICAgICAjIG7Dum1lcm8gZGUgdmFyaWFibGVzIG9ic2VydmFkYXMKbSAgPC0gMiAgICAgIyBuw7ptZXJvIGRlIGZhY3RvcmVzCgojIE1hdHJpeiBkZSBjYXJnYXMgKExhbWJkYSk6IHZhcmlhYmxlcyAxLTQgY2FyZ2FuIGVuIEYxOyA1LTggZW4gRjIsIGNvbiBwZXF1ZcOxYXMgY2FyZ2FzIGNydXphZGFzCkxhbWJkYSA8LSBtYXRyaXgoMCwgbnJvdyA9IHAsIG5jb2wgPSBtKQpMYW1iZGFbMTo0LCAxXSA8LSBjKDAuOCwgMC43NSwgMC43LCAwLjY1KQpMYW1iZGFbMTo0LCAyXSA8LSBjKDAuMTAsIDAuMDUsIDAuMTAsIDAuMDUpCkxhbWJkYVs1OjgsIDFdIDwtIGMoMC4xMCwgMC4wNSwgMC4xMCwgMC4wNSkKTGFtYmRhWzU6OCwgMl0gPC0gYygwLjgsIDAuNzUsIDAuNywgMC42NSkKCiMgVmFyaWFuemFzIGVzcGVjw61maWNhcyAoUHNpKSDigJQgZXJyb3JlcyBpZGlvc2luY3LDoXRpY29zCnBzaSA8LSByZXAoMC4zNSwgcCkgICMgY2FkYSB2YXJpYWJsZSBjb24gcnVpZG8gbW9kZXJhZG8KCiMgRmFjdG9yZXMgbGF0ZW50ZXMgfiBOKDAsIEkpCkZfc2NvcmVzX3RydWUgPC0gbWF0cml4KHJub3JtKG4gKiBtKSwgbnJvdyA9IG4sIG5jb2wgPSBtKQoKIyBFcnJvcmVzIH4gTigwLCBQc2kpCkUgPC0gbWF0cml4KHJub3JtKG4gKiBwKSwgbnJvdyA9IG4sIG5jb2wgPSBwKSAlKiUgZGlhZyhzcXJ0KHBzaSkpCgojIEdlbmVyYWNpw7NuIGRlIGRhdG9zOiBYID0gRiAqIExhbWJkYScgKyBFClggPC0gRl9zY29yZXNfdHJ1ZSAlKiUgdChMYW1iZGEpICsgRQpjb2xuYW1lcyhYKSA8LSBwYXN0ZTAoIlYiLCAxOnApCgojIEVzdGFuZGFyaXphY2nDs24gKHJlY29tZW5kYWRvIGFudGVzIGRlIEZBKQpYIDwtIHNjYWxlKFgpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyAyKSBHcsOhZmljbyAxOiBNYXRyaXogZGUgY29ycmVsYWNpb25lcyAoZXN0cnVjdHVyYSBwb3IgYmxvcXVlcykKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CmNvcl9tYXQgPC0gY29yKFgpCmNvcnJwbG90Ojpjb3JycGxvdChjb3JfbWF0LCBtZXRob2QgPSAiY29sb3IiLCB0eXBlID0gImZ1bGwiLAogICAgICAgICAgICAgICAgICAgdGwuY29sID0gImJsYWNrIiwgdGwuc3J0ID0gNDUsIGFkZENvZWYuY29sID0gTkEsCiAgICAgICAgICAgICAgICAgICBjb2wgPSBjb2xvclJhbXBQYWxldHRlKGMoIiMyYzdmYjgiLCAid2hpdGUiLCAiI2Q3MTkxYyIpKSgyMDApLAogICAgICAgICAgICAgICAgICAgbWFyID0gYygwLDAsMSwwKSwgdGl0bGUgPSAiTWF0cml6IGRlIGNvcnJlbGFjaW9uZXMiKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgMykgU2NyZWUgcGxvdCAoYXV0b3ZhbG9yZXMgZGUgbGEgbWF0cml6IGRlIGNvcnJlbGFjacOzbikKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CmVpZyA8LSBlaWdlbihjb3JfbWF0KSR2YWx1ZXMKcGxvdChzZXFfYWxvbmcoZWlnKSwgZWlnLCB0eXBlID0gImIiLCBwY2ggPSAxOSwgY29sID0gIiMyYzdmYjgiLAogICAgIHhsYWIgPSAiQ29tcG9uZW50ZS9GYWN0b3IgKG9yZGVuKSIsIHlsYWIgPSAiQXV0b3ZhbG9yIiwKICAgICBtYWluID0gIlNjcmVlIHBsb3QgKGF1dG92YWxvcmVzKSIpCmFibGluZShoID0gMSwgbHR5ID0gMiwgY29sID0gImdyYXk0MCIpICMgUmVnbGEgZGUgS2Fpc2VyCnRleHQoMTpsZW5ndGgoZWlnKSwgZWlnLCBsYWJlbHMgPSByb3VuZChlaWcsIDIpLCBwb3MgPSAzLCBjZXggPSAwLjgpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyA0KSBFc3RpbWFjacOzbiBkZSBmYWN0b3JlcyAoTUwpICsgcm90YWNpw7NuIG9ibGljdWEgKG9ibGltaW4pCiMgICAgUHVudGFqZXMgcG9yIE3DqXRvZG8gZGUgQmFydGxldHQKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CmZhX2ZpdCA8LSBwc3ljaDo6ZmEociA9IGNvcl9tYXQsIG5mYWN0b3JzID0gMiwgbi5vYnMgPSBuLCBmbSA9ICJtbCIsCiAgICAgICAgICAgICAgICAgICAgcm90YXRlID0gIm9ibGltaW4iLCBzY29yZXMgPSAiQmFydGxldHQiKQoKcHJpbnQoZmFfZml0LCBkaWdpdHMgPSAzKQoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgNSkgR3LDoWZpY28gMjogTWFwYSBkZSBjYWxvciBkZSBjYXJnYXMgZmFjdG9yaWFsZXMKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CmxvYWRpbmdzIDwtIGFzLmRhdGEuZnJhbWUodW5jbGFzcyhmYV9maXQkbG9hZGluZ3MpKQpsb2FkaW5ncyRWYXJpYWJsZSA8LSByb3duYW1lcyhsb2FkaW5ncykKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KHJlc2hhcGUyKQoKTF9sb25nIDwtIHJlc2hhcGUyOjptZWx0KGxvYWRpbmdzLCBpZC52YXJzID0gIlZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgIHZhcmlhYmxlLm5hbWUgPSAiRmFjdG9yIiwgdmFsdWUubmFtZSA9ICJDYXJnYSIpCgpnZ3Bsb3QoTF9sb25nLCBhZXMoeCA9IEZhY3RvciwgeSA9IFZhcmlhYmxlLCBmaWxsID0gQ2FyZ2EpKSArCiAgZ2VvbV90aWxlKGNvbG9yID0gIndoaXRlIikgKwogIHNjYWxlX2ZpbGxfZ3JhZGllbnQyKGxvdyA9ICIjMmM3ZmI4IiwgbWlkID0gIndoaXRlIiwgaGlnaCA9ICIjZDcxOTFjIiwKICAgICAgICAgICAgICAgICAgICAgICBsaW1pdHMgPSBjKC0xLCAxKSwgbmFtZSA9ICJDYXJnYSIpICsKICBsYWJzKHRpdGxlID0gIk1hcGEgZGUgY2Fsb3IgZGUgY2FyZ2FzIGZhY3RvcmlhbGVzIChyb3RhY2nDs24gb2JsaW1pbikiLAogICAgICAgeCA9ICJGYWN0b3IiLCB5ID0gIlZhcmlhYmxlIikgKwogIHRoZW1lX21pbmltYWwoYmFzZV9zaXplID0gMTIpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyA2KSBHcsOhZmljbyAzOiBEaWFncmFtYSBkZWwgbW9kZWxvIGZhY3RvcmlhbAojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KcHN5Y2g6OmZhLmRpYWdyYW0oZmFfZml0LCBtYWluID0gIkRpYWdyYW1hIGRlbCBtb2RlbG86IGZhY3RvcmVzLCBjYXJnYXMgeSBlcnJvcmVzIikKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIDcpIEdyw6FmaWNvIDQ6IERpc3BlcnNpw7NuIGRlIHB1bnRhamVzIGZhY3RvcmlhbGVzIChCYXJ0bGV0dCkKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgRXN0aW1hY2nDs24gZGUgcHVudGFqZXMgc29icmUgbG9zIGRhdG9zIGluZGl2aWR1YWxlczoKIyBOT1RBOiBwc3ljaDo6ZmEoKSBjb24gciA9IGNvcl9tYXQgY2FsY3VsYSBzb2JyZSBtYXRyaXogZGUgY29ycmVsYWNpw7NuLgojIFBhcmEgcHVudGFqZXMgaW5kaXZpZHVhbGVzLCBlamVjdXRhbW9zIHRhbWJpw6luIHNvYnJlIGxvcyBkYXRvczoKZmFfZml0X2RhdGEgPC0gcHN5Y2g6OmZhKFgsIG5mYWN0b3JzID0gMiwgZm0gPSAibWwiLCByb3RhdGUgPSAib2JsaW1pbiIsIHNjb3JlcyA9ICJCYXJ0bGV0dCIpCnNjb3Jlc19kZiA8LSBhcy5kYXRhLmZyYW1lKGZhX2ZpdF9kYXRhJHNjb3JlcykKY29sbmFtZXMoc2NvcmVzX2RmKSA8LSBjKCJGMSIsICJGMiIpCgpnZ3Bsb3Qoc2NvcmVzX2RmLCBhZXMoeCA9IEYxLCB5ID0gRjIpKSArCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuNSwgY29sb3IgPSAiIzJjN2ZiOCIpICsKICBzdGF0X2VsbGlwc2UobGV2ZWwgPSAwLjkwLCBsaW5ldHlwZSA9IDIsIGNvbG9yID0gImdyYXk0MCIpICsKICBsYWJzKHRpdGxlID0gIlB1bnRhamVzIGZhY3RvcmlhbGVzIChtw6l0b2RvIGRlIEJhcnRsZXR0KSIsIHggPSAiRmFjdG9yIDEiLCB5ID0gIkZhY3RvciAyIikgKwogIHRoZW1lX21pbmltYWwoYmFzZV9zaXplID0gMTIpCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyA4KSBCb251czogQmlwbG90IHNpbXBsZSAodmFyaWFibGVzIGNvbW8gZmxlY2hhcyBlbiBlbCBwbGFubyBGMeKAk0YyKQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyBVc2Ftb3MgbGFzIGNhcmdhcyBjb21vIHZlY3RvcmVzIGVuIGVsIHBsYW5vIGRlIGxvcyBmYWN0b3JlcwpMIDwtIGFzLmRhdGEuZnJhbWUodW5jbGFzcyhmYV9maXRfZGF0YSRsb2FkaW5ncykpCkwkVmFyaWFibGUgPC0gcm93bmFtZXMoTCkKCmdncGxvdCgpICsKICBnZW9tX3BvaW50KGRhdGEgPSBzY29yZXNfZGYsIGFlcyhGMSwgRjIpLCBhbHBoYSA9IDAuMiwgY29sb3IgPSAiZ3JheTYwIikgKwogIGdlb21fc2VnbWVudChkYXRhID0gTCwgYWVzKHggPSAwLCB5ID0gMCwgeGVuZCA9IE1MMSwgeWVuZCA9IE1MMiksCiAgICAgICAgICAgICAgIGFycm93ID0gYXJyb3cobGVuZ3RoID0gdW5pdCgwLjI1LCAiY20iKSksIGNvbG9yID0gIiNkNzE5MWMiLCBzaXplID0gMC44KSArCiAgZ2VvbV90ZXh0KGRhdGEgPSBMLCBhZXMoeCA9IE1MMSwgeSA9IE1MMiwgbGFiZWwgPSBWYXJpYWJsZSksCiAgICAgICAgICAgIGhqdXN0ID0gMC41LCB2anVzdCA9IC0wLjgsIGNvbG9yID0gIiNkNzE5MWMiKSArCiAgbGFicyh0aXRsZSA9ICJCaXBsb3Q6IHB1bnRhamVzIHZzLiB2ZWN0b3JlcyBkZSBjYXJnYSIsCiAgICAgICB4ID0gIkZhY3RvciAxIiwgeSA9ICJGYWN0b3IgMiIpICsKICB0aGVtZV9taW5pbWFsKGJhc2Vfc2l6ZSA9IDEyKQpgYGAKCiMjIDYuNykgRWplbXBsbyBjb24gUHl0aG9uCgpgYGB7cHl0aG9ufQojID09PT09PT09PT09PT09PT09PT09PT09PT09PQojIEFuw6FsaXNpcyBGYWN0b3JpYWwgZW4gUHl0aG9uCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09CgojIC0tLS0tLS0tLS0gMSkgRGF0b3MgLS0tLS0tLS0tLQojIFVzYW1vcyBlbCBkYXRhc2V0ICJiZmkiIGRlIHBlcnNvbmFsaWRhZCAoc2ltaWxhciBhbCBkZSBSIHBzeWNoOjpiZmkpCmJmaSA9IGZldGNoX29wZW5tbCgiYmZpIiwgdmVyc2lvbj0xLCBhc19mcmFtZT1UcnVlKS5mcmFtZQpkZiA9IGJmaS5pbG9jWzosIDoyNV0gICAgICAjIMOtdGVtcyBBMTpBNSwgQzE6QzUsIEUxOkU1LCBOMTpONSwgTzE6TzUKZGYgPSBkZi5kcm9wbmEoKQpuLCBwID0gZGYuc2hhcGUKcHJpbnQoZiJPYnNlcnZhY2lvbmVzOiB7bn0sIFZhcmlhYmxlczoge3B9IikKCiMgRXN0YW5kYXJpemFtb3MgKGZhY3Rvcl9hbmFseXplciBsbyByZWNvbWllbmRhKQpzY2FsZXIgPSBTdGFuZGFyZFNjYWxlcigpCmRmX3N0ZCA9IHNjYWxlci5maXRfdHJhbnNmb3JtKGRmKQoKIyAtLS0tLS0tLS0tIDIpIENvcnJlbGFjaW9uZXMsIEtNTyB5IEJhcnRsZXR0IC0tLS0tLS0tLS0KY29yciA9IG5wLmNvcnJjb2VmKGRmX3N0ZCwgcm93dmFyPUZhbHNlKQoKcGx0LmZpZ3VyZShmaWdzaXplPSgxMCwgOCkpCnNucy5oZWF0bWFwKGNvcnIsIGNtYXA9ImNvb2x3YXJtIiwgY2VudGVyPTApCnBsdC50aXRsZSgiTWFwYSBkZSBjYWxvciBkZSBjb3JyZWxhY2lvbmVzIChhZ3J1cGFkYXMpIikKcGx0LnNob3coKQoKY2hpMiwgcF92YWx1ZSA9IGNhbGN1bGF0ZV9iYXJ0bGV0dF9zcGhlcmljaXR5KGRmX3N0ZCkKcHJpbnQoIj09PiBCYXJ0bGV0dCB0ZXN0OiBjaGkyID0iLCBjaGkyLCAicCA9IiwgcF92YWx1ZSkKCmttb19hbGwsIGttb19tb2RlbCA9IGNhbGN1bGF0ZV9rbW8oZGZfc3RkKQpwcmludCgiPT0+IEtNTyBnZW5lcmFsOiIsIHJvdW5kKGttb19tb2RlbCwgMykpCgojIC0tLS0tLS0tLS0gMykgTsO6bWVybyBkZSBmYWN0b3JlczogU2NyZWUgKyBBbsOhbGlzaXMgcGFyYWxlbG8gLS0tLS0tLS0tLQpmYSA9IEZhY3RvckFuYWx5emVyKHJvdGF0aW9uPU5vbmUpCmZhLmZpdChkZl9zdGQpCgpldiwgdiA9IGZhLmdldF9laWdlbnZhbHVlcygpCgpwbHQuZmlndXJlKGZpZ3NpemU9KDgsIDUpKQpwbHQuc2NhdHRlcihyYW5nZSgxLCBkZl9zdGQuc2hhcGVbMV0gKyAxKSwgZXYpCnBsdC5wbG90KHJhbmdlKDEsIGRmX3N0ZC5zaGFwZVsxXSArIDEpLCBldikKcGx0LnRpdGxlKCJTY3JlZSBwbG90IikKcGx0LnhsYWJlbCgiRmFjdG9yIikKcGx0LnlsYWJlbCgiRWlnZW52YWx1ZSIpCnBsdC5heGhsaW5lKHk9MSwgY29sb3I9InIiLCBsaW5lc3R5bGU9Ii0tIikKcGx0LnNob3coKQoKIyBBcXXDrSBwb2Ryw61hcyB1c2FyIGFuw6FsaXNpcyBwYXJhbGVsbyAoY29uIGZhLnBhcmFsbGVsIGVxdWl2YWxlbnRlKSDihpIgbGlicmVyw61hIGFkaWNpb25hbAoKbmZhY3RvcnMgPSA1ICAgIyBhanVzdGEgc2Vnw7puIGVsIGdyw6FmaWNvIG8gdGVzdCBwYXJhbGVsbwpwcmludChmIk7Dum1lcm8gZGUgZmFjdG9yZXMgc2VsZWNjaW9uYWRvOiB7bmZhY3RvcnN9IikKCiMgLS0tLS0tLS0tLSA0KSBBanVzdGUgZGVsIG1vZGVsbyBmYWN0b3JpYWwgLS0tLS0tLS0tLQpmYSA9IEZhY3RvckFuYWx5emVyKG5fZmFjdG9ycz1uZmFjdG9ycywgcm90YXRpb249Im9ibGltaW4iLCBtZXRob2Q9Im1sIikKZmEuZml0KGRmX3N0ZCkKCmxvYWRpbmdzID0gcGQuRGF0YUZyYW1lKGZhLmxvYWRpbmdzXywgaW5kZXg9ZGYuY29sdW1ucykKcHJpbnQoIlxuPT0+IENhcmdhcyBmYWN0b3JpYWxlcyAoY3V0b2ZmPTAuMzApOlxuIiwgbG9hZGluZ3Mucm91bmQoMykpCgpjb21tdW5hbGl0aWVzID0gZmEuZ2V0X2NvbW11bmFsaXRpZXMoKQp1bmlxdWVuZXNzID0gZmEuZ2V0X3VuaXF1ZW5lc3NlcygpCnByaW50KCJcbj09PiBWYXJpYW56YSBleHBsaWNhZGEgdG90YWw6Iiwgc3VtKGZhLmdldF9mYWN0b3JfdmFyaWFuY2UoKVsxXSkpCnByaW50KCJcbj09PiBDb211bmFsaWRhZGVzIHkgVW5pY2lkYWRlczoiKQpwcmludChwZC5EYXRhRnJhbWUoeyJDb211bmFsaWRhZCI6IGNvbW11bmFsaXRpZXMsICJVbmljaWRhZCI6IHVuaXF1ZW5lc3N9LCBpbmRleD1kZi5jb2x1bW5zKSkKCiMgLS0tLS0tLS0tLSA1KSBIZWF0bWFwIGRlIGNhcmdhcyBmYWN0b3JpYWxlcyAtLS0tLS0tLS0tCnBsdC5maWd1cmUoZmlnc2l6ZT0oMTAsIDEyKSkKc25zLmhlYXRtYXAobG9hZGluZ3MsIGFubm90PVRydWUsIGNtYXA9ImNvb2x3YXJtIiwgY2VudGVyPTAsIGZtdD0iLjJmIikKcGx0LnRpdGxlKCJIZWF0bWFwIGRlIGNhcmdhcyBmYWN0b3JpYWxlcyAob2JsaW1pbikiKQpwbHQuc2hvdygpCgojIC0tLS0tLS0tLS0gNikgUHVudGFqZXMgZmFjdG9yaWFsZXMgLS0tLS0tLS0tLQojIE5vdGE6IGZhY3Rvcl9hbmFseXplciBubyBkYSBzY29yZXMgZGlyZWN0b3MgY29tbyBlbiBSLAojIHBlcm8gcG9kZW1vcyBjYWxjdWxhcmxvcyBwb3IgcmVncmVzacOzbiBhcHJveGltYWRhLgpzY29yZXMgPSBwZC5EYXRhRnJhbWUobnAuZG90KGRmX3N0ZCwgZmEubG9hZGluZ3NfKSwgY29sdW1ucz1bZiJGYWN0b3J7aSsxfSIgZm9yIGkgaW4gcmFuZ2UobmZhY3RvcnMpXSkKCmlmIHNjb3Jlcy5zaGFwZVsxXSA+PSAyOgogICAgcGx0LmZpZ3VyZShmaWdzaXplPSg3LCA2KSkKICAgIHBsdC5zY2F0dGVyKHNjb3Jlcy5pbG9jWzosIDBdLCBzY29yZXMuaWxvY1s6LCAxXSwgYWxwaGE9MC4zLCBjb2xvcj0iIzJDN0ZCOCIpCiAgICBwbHQuYXhobGluZSgwLCBjb2xvcj0iZ3JleSIsIGxpbmVzdHlsZT0iLS0iKQogICAgcGx0LmF4dmxpbmUoMCwgY29sb3I9ImdyZXkiLCBsaW5lc3R5bGU9Ii0tIikKICAgIHBsdC54bGFiZWwoIkZhY3RvcjEiKQogICAgcGx0LnlsYWJlbCgiRmFjdG9yMiIpCiAgICBwbHQudGl0bGUoIkRpc3BlcnNpw7NuIGRlIHB1bnRhamVzIGZhY3RvcmlhbGVzIikKICAgIHBsdC5zaG93KCkKCiMgLS0tLS0tLS0tLSA3KSBNb2RlbG8gZmFjdG9yaWFsIChlc3F1ZW1hIHNpbXBsaWZpY2FkbykgLS0tLS0tLS0tLQojIE5vdGE6IFB5dGhvbiBubyB0cmFlIHVuIGZhLmRpYWdyYW0oKSBjb21vIFIuCiMgQXF1w60gdW4gZGlhZ3JhbWEgc2VuY2lsbG8gZGUgY2FyZ2FzID4gMC4zCgppbXBvcnQgbmV0d29ya3ggYXMgbngKCkcgPSBueC5HcmFwaCgpCnRocmVzaG9sZCA9IDAuMwpmb3IgdmFyLCByb3cgaW4gbG9hZGluZ3MuaXRlcnJvd3MoKToKICAgIGZvciBmLCB2YWwgaW4gcm93Lml0ZW1zKCk6CiAgICAgICAgaWYgYWJzKHZhbCkgPj0gdGhyZXNob2xkOgogICAgICAgICAgICBHLmFkZF9lZGdlKHZhciwgZiwgd2VpZ2h0PWFicyh2YWwpKQoKcG9zID0gbnguc3ByaW5nX2xheW91dChHLCBzZWVkPTEyMykKcGx0LmZpZ3VyZShmaWdzaXplPSgxMiwgMTApKQpueC5kcmF3KEcsIHBvcywgd2l0aF9sYWJlbHM9VHJ1ZSwgbm9kZV9zaXplPTE1MDAsIG5vZGVfY29sb3I9ImxpZ2h0Ymx1ZSIsIGZvbnRfc2l6ZT04LCB3aWR0aD0xKQpwbHQudGl0bGUoIkRpYWdyYW1hIHNpbXBsaWZpY2FkbyBkZWwgbW9kZWxvIGZhY3RvcmlhbCAoQUZFKSIpCnBsdC5zaG93KCkKCmBgYAoKIyA3KSBFc2NhbGFtaWVudG8gbXVsdGlkaW1lbnNpb25hbAoKRWwgZXNjYWxhZG8gbXVsdGlkaW1lbnNpb25hbCAoTURTLCBwb3Igc3VzIHNpZ2xhcyBlbiBpbmdsw6lzKSBlcyB1bmEgdMOpY25pY2EgZXN0YWTDrXN0aWNhIHF1ZSBidXNjYSByZXByZXNlbnRhciBvYmpldG9zIG8gaW5kaXZpZHVvcyBlbiB1biBlc3BhY2lvIGRlIGJhamEgZGltZW5zacOzbiAoZ2VuZXJhbG1lbnRlIDJEIG8gM0QpIGRlIHRhbCBtYW5lcmEgcXVlIGxhcyBkaXN0YW5jaWFzIGVudHJlIGVsbG9zIGVuIGVzZSBlc3BhY2lvIHJlZHVjaWRvIHJlZmxlamVuIGxvIG1lam9yIHBvc2libGUgc3VzIGRpc3RhbmNpYXMgb3JpZ2luYWxlcyBlbiB1biBlc3BhY2lvIGRlIGFsdGEgZGltZW5zacOzbi4gU3Ugb2JqZXRpdm8gZXMgdHJhZHVjaXIgdW5hIG1hdHJpeiBkZSBkaXNpbWlsYXJpZGFkZXMgJEQgPSAoZF97aWp9KSQgZW50cmUgcGFyZXMgZGUgZWxlbWVudG9zIGVuIHVuYSBjb25maWd1cmFjacOzbiBnZW9tw6l0cmljYSBkZSBwdW50b3MgJHhfaSBcaW4gXG1hdGhiYntSfV5rJCBxdWUgY29uc2VydmUgZXNhcyByZWxhY2lvbmVzLgoKRm9ybWFsbWVudGUsIGVsIHByb2JsZW1hIGNvbnNpc3RlIGVuIGVuY29udHJhciBjb29yZGVuYWRhcyAkeF8xLCB4XzIsIFxkb3RzLCB4X24kIGVuICRcbWF0aGJie1J9XmskIHF1ZSBtaW5pbWljZW4gdW5hIGZ1bmNpw7NuIGRlIHDDqXJkaWRhIGxsYW1hZGEgZXN0csOpcyAoc3RyZXNzKSwgZGVmaW5pZGEgY29tbzoKCiQkClx0ZXh0e1N0cmVzc30oWCkgPSBcc3FydHtcZnJhY3tcc3VtX3tpPGp9IFxsZWZ0KCBkX3tpan0gLSBcZGVsdGFfe2lqfShYKSBccmlnaHQpXjJ9e1xzdW1fe2k8an0gZF97aWp9XjJ9fQokJAoKZG9uZGUgJGRfe2lqfSQgZXMgbGEgZGlzaW1pbGFyaWRhZCBvcmlnaW5hbCBlbnRyZSBsb3MgZWxlbWVudG9zICRpJCB5ICRqJCwgeSAkXGRlbHRhX3tpan0oWCkkIGVzIGxhIGRpc3RhbmNpYSBldWNsaWRpYW5hIGVudHJlICR4X2kkIHkgJHhfaiQgZW4gZWwgZXNwYWNpbyByZWR1Y2lkby4KCkVsIE1EUyBzaXJ2ZSBwYXJhIHZpc3VhbGl6YXIgcmVsYWNpb25lcyBjb21wbGVqYXMgZW50cmUgdmFyaWFibGVzIG8gaW5kaXZpZHVvcywgZGV0ZWN0YW5kbyBlc3RydWN0dXJhcyBsYXRlbnRlcyBkZSBwcm94aW1pZGFkIG8gYWdydXBhbWllbnRvLiBBIGRpZmVyZW5jaWEgZGVsIGFuw6FsaXNpcyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlcyAoUENBKSwgcXVlIHNlIGJhc2EgZW4gbGEgdmFyaWFuemEgeSBsYSBlc3RydWN0dXJhIGxpbmVhbCBkZSBsb3MgZGF0b3MsIGVsIE1EUyBwdWVkZSB0cmFiYWphciBkaXJlY3RhbWVudGUgY29uIG1hdHJpY2VzIGRlIGRpc3RhbmNpYXMgbyBkaXNpbWlsYXJpZGFkZXMsIGxvIHF1ZSBsbyBoYWNlIMO6dGlsIGVuIGNvbnRleHRvcyBkb25kZSBubyBzZSB0aWVuZW4gdmFyaWFibGVzIG3DqXRyaWNhcyB0cmFkaWNpb25hbGVzLgoKRW50cmUgbG9zIGNhc29zIGRlIHVzbyBtw6FzIGZyZWN1ZW50ZXMgZXN0w6FuOiBlbiBwc2ljb2xvZ8OtYSwgcmVwcmVzZW50YXIgcGVyY2VwY2lvbmVzIGRlIHNpbWlsaXR1ZCBlbnRyZSBlc3TDrW11bG9zIChjb2xvcmVzLCBzb25pZG9zLCBwYWxhYnJhcyk7IGVuIG1hcmtldGluZywgbWFwZWFyIGPDs21vIGxvcyBjb25zdW1pZG9yZXMgcGVyY2liZW4gbGEgY2VyY2Fuw61hIGVudHJlIG1hcmNhcyBvIHByb2R1Y3RvczsgZW4gYmlvbG9nw61hLCB2aXN1YWxpemFyIGRpc3RhbmNpYXMgZ2Vuw6l0aWNhcyBlbnRyZSBlc3BlY2llczsgeSBlbiBjaWVuY2lhcyBzb2NpYWxlcywgYW5hbGl6YXIgcmVkZXMgZGUgYWZpbmlkYWQgbyBkZSBjb29wZXJhY2nDs24gZW50cmUgaW5kaXZpZHVvcy4gRW4gdG9kb3MgZXN0b3MgY2Fzb3MsIGVsIE1EUyBwZXJtaXRlIGNvbnZlcnRpciByZWxhY2lvbmVzIGFic3RyYWN0YXMgZGUgc2VtZWphbnphIGVuIHJlcHJlc2VudGFjaW9uZXMgZ2VvbcOpdHJpY2FzIGNsYXJhcyB5IGbDoWNpbGVzIGRlIGludGVycHJldGFyLgoKKipFamVtcGxvIGNvbiBSKioKCmBgYHtyfQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyBFamVtcGxvIGLDoXNpY28gZGUgTURTIGVuIFIKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgojIDEuIERpc3RhbmNpYXMgKGVuIGttKSBlbnRyZSBhbGd1bmFzIGNpdWRhZGVzCmNpdWRhZGVzIDwtIGMoIlJvbWEiLCAiUGFyw61zIiwgIkJlcmzDrW4iLCAiTWFkcmlkIiwgIkxvbmRyZXMiKQpkaXN0YW5jaWFzIDwtIG1hdHJpeChjKAogIDAsIDExMDUsIDExODEsIDEzNjUsIDE0MzUsCiAgMTEwNSwgMCwgMTA1MCwgMTI3NiwgMzQzLAogIDExODEsIDEwNTAsIDAsIDE4NzAsIDkzMCwKICAxMzY1LCAxMjc2LCAxODcwLCAwLCAxMjYwLAogIDE0MzUsIDM0MywgOTMwLCAxMjYwLCAwCiksIG5yb3cgPSA1LCBieXJvdyA9IFRSVUUpCgpyb3duYW1lcyhkaXN0YW5jaWFzKSA8LSBjaXVkYWRlcwpjb2xuYW1lcyhkaXN0YW5jaWFzKSA8LSBjaXVkYWRlcwoKIyAyLiBBcGxpY2Ftb3MgRXNjYWxhZG8gTXVsdGlkaW1lbnNpb25hbCAoTURTIGNsw6FzaWNvKQptZHMgPC0gY21kc2NhbGUoYXMuZGlzdChkaXN0YW5jaWFzKSwgayA9IDIpCgojIDMuIEdyYWZpY2Ftb3MKcGxvdChtZHMsIHR5cGUgPSAibiIsIG1haW4gPSAiRXNjYWxhZG8gTXVsdGlkaW1lbnNpb25hbCAoTURTKSIpCnRleHQobWRzLCBsYWJlbHMgPSBjaXVkYWRlcywgY2V4ID0gMS4yLCBjb2wgPSAiYmx1ZSIpCgpgYGAKCkVuIGVzdGUgZWplbXBsbywgZWwgZXNjYWxhZG8gbXVsdGlkaW1lbnNpb25hbCAoTURTKSB0b23DsyBsYSBtYXRyaXogZGUgZGlzdGFuY2lhcyByZWFsZXMgZW50cmUgbGFzIGNpbmNvIGNpdWRhZGVzIHkgbGFzIHByb3llY3TDsyBlbiB1biBwbGFubyBiaWRpbWVuc2lvbmFsLCBkZSBtYW5lcmEgcXVlIGxhcyBwb3NpY2lvbmVzIHJlbGF0aXZhcyBkZSBsb3MgcHVudG9zIHJlZmxlamFuIGxvIG1lam9yIHBvc2libGUgZXNhcyBkaXN0YW5jaWFzIG9yaWdpbmFsZXM6IHNlIG9ic2VydmEgcXVlIE1hZHJpZCBxdWVkYSBzZXBhcmFkbyBoYWNpYSBsYSBpenF1aWVyZGEgcG9ycXVlIGVzdMOhIG3DoXMgbGVqb3MgZGVsIHJlc3RvLCBQYXLDrXMgeSBMb25kcmVzIGFwYXJlY2VuIGNlcmNhbm9zIGluZGljYW5kbyBzdSBwcm94aW1pZGFkIGdlb2dyw6FmaWNhLCBCZXJsw61uIHNlIHNpdMO6YSBhIGxhIGRlcmVjaGEgcmVwcmVzZW50YW5kbyBzdSBsZWphbsOtYSByZXNwZWN0byBhIE1hZHJpZCB5IFJvbWEsIG1pZW50cmFzIHF1ZSBSb21hIHNlIHViaWNhIGVuIGxhIHBhcnRlIHN1cGVyaW9yIG1vc3RyYW5kbyBzdSBkaXN0YW5jaWEgaW50ZXJtZWRpYSBjb24gbG9zIGRlbcOhczsgZW4gY29uanVudG8sIGxhIGdyw6FmaWNhIHBlcm1pdGUgaW50ZXJwcmV0YXIgcsOhcGlkYW1lbnRlIGxhIHNpbWlsaXR1ZCBvIHNlcGFyYWNpw7NuIHJlbGF0aXZhIGVudHJlIGNpdWRhZGVzIHNpbiBuZWNlc2lkYWQgZGUgY29uc3VsdGFyIGRpcmVjdGFtZW50ZSBsYSB0YWJsYSBkZSBkaXN0YW5jaWFzLgoKIyMgNy4xKSBFc2NhbGFkb3MgbcOpdHJpY29zOiBjb29yZGVuYXMgcHJpbmNpcGFsZXMKCkVsIGVzY2FsYWRvIG3DqXRyaWNvIHBvciBjb29yZGVuYWRhcyBwcmluY2lwYWxlcyAoQ2xhc3NpY2FsIE1EUyBvIFBDb0EpIGVzIHVuIG3DqXRvZG8gcXVlIGJ1c2NhIHJlcHJlc2VudGFyIG9iamV0b3MgZGVzY3JpdG9zIHBvciB1bmEgbWF0cml6IGRlIGRpc3RhbmNpYXMgJFxtYXRoYmZ7RH0gPSAoZF97aWp9KSQgZW4gdW4gZXNwYWNpbyBkZSBtZW5vciBkaW1lbnNpw7NuLCB0w61waWNhbWVudGUgJFxtYXRoYmJ7Un1eMiQgbyAkXG1hdGhiYntSfV4zJCwgcHJlc2VydmFuZG8gbG8gbWVqb3IgcG9zaWJsZSBlc2FzIGRpc3RhbmNpYXMuIEEgZGlmZXJlbmNpYSBkZWwgTURTIGdlbmVyYWwgcXVlIG9wdGltaXphIHVuYSBmdW5jacOzbiBkZSBlc3Ryw6lzLCBlbCBlbmZvcXVlIGRlIGNvb3JkZW5hZGFzIHByaW5jaXBhbGVzIHRpZW5lIHVuYSBzb2x1Y2nDs24gYW5hbMOtdGljYSBiYXNhZGEgZW4gw6FsZ2VicmEgbGluZWFsLCBsbyBxdWUgbG8gaGFjZSBtw6FzIGRpcmVjdG8geSBlZmljaWVudGUuCgpFbCBwcm9jZWRpbWllbnRvIGNvbWllbnphIGNvbnZpcnRpZW5kbyBsYSBtYXRyaXogZGUgZGlzdGFuY2lhcyAkXG1hdGhiZntEfSQgZW4gdW5hIG1hdHJpeiBkZSBwcm9kdWN0byBpbnRlcm5vIGNlbnRyYWRhICRcbWF0aGJme0J9JCBtZWRpYW50ZSBkb2JsZSBjZW50cmFkbzoKCiQkXG1hdGhiZntCfSA9IC1cdGZyYWN7MX17Mn0gXCwgXG1hdGhiZntKfSBcLCBcbWF0aGJme0R9XjIgXCwgXG1hdGhiZntKfQogJCQKCmRvbmRlICRcbWF0aGJme0p9ID0gXG1hdGhiZntJfSAtIFx0ZnJhY3sxfXtufVxtYXRoYmZ7MX1cbWF0aGJmezF9Xlx0b3AkIGVzIGxhIG1hdHJpeiBkZSBjZW50cmFkbywgJFxtYXRoYmZ7SX0kIGxhIGlkZW50aWRhZCB5ICRcbWF0aGJmezF9JCBlbCB2ZWN0b3IgZGUgdW5vcy4gTHVlZ28gc2UgcmVhbGl6YSB1bmEgZGVzY29tcG9zaWNpw7NuIGVzcGVjdHJhbCBkZSAkXG1hdGhiZntCfSQsIG9idGVuaWVuZG8gYXV0b3ZhbG9yZXMgJFxsYW1iZGFfMSwgXGxhbWJkYV8yLCBcZG90cywgXGxhbWJkYV9wJCB5IGF1dG92ZWN0b3JlcyBjb3JyZXNwb25kaWVudGVzLgoKTGFzIGNvb3JkZW5hZGFzIHByaW5jaXBhbGVzIGRlIGxvcyBvYmpldG9zIGVuIGVsIGVzcGFjaW8gcmVkdWNpZG8gZGUgZGltZW5zacOzbiAkayQgc2UgY2FsY3VsYW4gY29tbzoKCiQkIFxtYXRoYmZ7WH1fayA9IFxtYXRoYmZ7Vn1fayBcLCBcTGFtYmRhX2teezEvMn0kJAoKZG9uZGUgJFxtYXRoYmZ7Vn1fayQgY29udGllbmUgbG9zICRrJCBhdXRvdmVjdG9yZXMgcHJpbmNpcGFsZXMgZGUgJFxtYXRoYmZ7Qn0kIHkgJFxMYW1iZGFfa157MS8yfSQgZXMgbGEgbWF0cml6IGRpYWdvbmFsIGNvbiBsYXMgcmHDrWNlcyBjdWFkcmFkYXMgZGUgbG9zICRrJCBhdXRvdmFsb3JlcyBwb3NpdGl2b3MuIEVzdGFzIGNvb3JkZW5hZGFzIHNvbiBsYXMgcmVwcmVzZW50YWNpb25lcyBnZW9tw6l0cmljYXMgYnVzY2FkYXMgcXVlIHByZXNlcnZhbiBsbyBtZWpvciBwb3NpYmxlIGxhcyBkaXN0YW5jaWFzIG9yaWdpbmFsZXMuCgpMb3MgY2Fzb3MgZGUgdXNvIGRlbCBlc2NhbGFkbyBwb3IgY29vcmRlbmFkYXMgcHJpbmNpcGFsZXMgc29uIGFtcGxpb3M6IGVuIGVjb2xvZ8OtYSwgcGFyYSByZXByZXNlbnRhciBjb211bmlkYWRlcyBiaW9sw7NnaWNhcyBhIHBhcnRpciBkZSBtZWRpZGFzIGRlIGRpc2ltaWxpdHVkIGVudHJlIGVzcGVjaWVzOyBlbiBnZW7DqXRpY2EsIHBhcmEgdmlzdWFsaXphciBkaXN0YW5jaWFzIGVudHJlIHBvYmxhY2lvbmVzOyBlbiBwc2ljb2xvZ8OtYSB5IGNpZW5jaWFzIHNvY2lhbGVzLCBwYXJhIG1hcGVhciBwZXJjZXBjaW9uZXMgZGUgc2ltaWxpdHVkIGVudHJlIGVzdMOtbXVsb3M7IHkgZW4gbWVyY2FkZW8sIHBhcmEgY29uc3RydWlyIG1hcGFzIHBlcmNlcHR1YWxlcyBkZSBtYXJjYXMgbyBwcm9kdWN0b3MuIFN1IHZlbnRhamEgcHJpbmNpcGFsIGVzIHF1ZSBvZnJlY2UgdW5hIHNvbHVjacOzbiBhbGdlYnJhaWNhIGV4YWN0YSBjdWFuZG8gbGFzIGRpc3RhbmNpYXMgcHJvdmllbmVuIGRlIHVuIGVzcGFjaW8gZXVjbGlkaWFubywgZmFjaWxpdGFuZG8gdW5hIGludGVycHJldGFjacOzbiBjbGFyYSBkZSBsYXMgcmVsYWNpb25lcyBlbnRyZSBvYmpldG9zLgoKYGBge3IgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgVXNhbW9zIGVsIGRhdGFzZXQgaXJpcwpkYXRhKGlyaXMpCgojIENhbGN1bGFtb3MgbWF0cml6IGRlIGRpc3RhbmNpYXMgZXVjbGlkaWFuYXMgc29icmUgbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzCmRpc3RfaXJpcyA8LSBkaXN0KGlyaXNbLCAxOjRdKQoKIyBFc2NhbGFkbyBNw6l0cmljbyAoUENvQSkgZW4gMiBkaW1lbnNpb25lcwpwY29hIDwtIGNtZHNjYWxlKGRpc3RfaXJpcywgayA9IDIsIGVpZyA9IFRSVUUpCgojIEV4dHJhZW1vcyBjb29yZGVuYWRhcwpjb29yZHMgPC0gYXMuZGF0YS5mcmFtZShwY29hJHBvaW50cykKY29sbmFtZXMoY29vcmRzKSA8LSBjKCJEaW0xIiwgIkRpbTIiKQpjb29yZHMkU3BlY2llcyA8LSBpcmlzJFNwZWNpZXMKCiMgR3LDoWZpY28gY29uIGdncGxvdDIKZ2dwbG90KGNvb3JkcywgYWVzKHggPSBEaW0xLCB5ID0gRGltMiwgY29sb3IgPSBTcGVjaWVzKSkgKwogIGdlb21fcG9pbnQoc2l6ZSA9IDMsIGFscGhhID0gMC43KSArCiAgc3RhdF9lbGxpcHNlKGxldmVsID0gMC45NSwgbGluZXR5cGUgPSAyKSArCiAgdGhlbWVfbWluaW1hbChiYXNlX3NpemUgPSAxNCkgKwogIGxhYnModGl0bGUgPSAiRXNjYWxhZG8gTcOpdHJpY28gKFBDb0EpIGVuIGlyaXMiLAogICAgICAgeCA9ICJDb29yZGVuYWRhIFByaW5jaXBhbCAxIiwKICAgICAgIHkgPSAiQ29vcmRlbmFkYSBQcmluY2lwYWwgMiIpICsKICBzY2FsZV9jb2xvcl9icmV3ZXIocGFsZXR0ZSA9ICJEYXJrMiIpCgpgYGAKCiMjIDcuMikgQ29uc3RydWNjacOzbiBkZSBsYXMgY29vcmRlbmFkYXMgcHJpbmNpcGFsZXMKCkxhIGNvbnN0cnVjY2nDs24gZGUgbGFzIGNvb3JkZW5hZGFzIHByaW5jaXBhbGVzIGVzIGVsIHByb2NlZGltaWVudG8gbWF0ZW3DoXRpY28gcXVlIHBlcm1pdGUgb2J0ZW5lciB1bmEgcmVwcmVzZW50YWNpw7NuIGVuIHVuIGVzcGFjaW8gZXVjbGlkaWFubyBkZSBiYWphIGRpbWVuc2nDs24gYSBwYXJ0aXIgZGUgdW5hIG1hdHJpeiBkZSBkaXN0YW5jaWFzICRcbWF0aGJme0R9JC4gTGEgaWRlYSBjZW50cmFsIGVzIHRyYW5zZm9ybWFyIGVzdGFzIGRpc3RhbmNpYXMgZW4gdW4gY29uanVudG8gZGUgY29vcmRlbmFkYXMgb3J0b2dvbmFsZXMgcXVlIHByZXNlcnZlbiwgbG8gbWVqb3IgcG9zaWJsZSwgbGEgZXN0cnVjdHVyYSBnZW9tw6l0cmljYSBkZSBsb3MgZGF0b3MuCgpFbCBwcm9jZXNvIGNvbWllbnphIGNvbiBlbCBkb2JsZSBjZW50cmFkbyBkZSBsYSBtYXRyaXogZGUgZGlzdGFuY2lhcyBjdWFkcmFkYXMuIEEgcGFydGlyIGRlICRcbWF0aGJme0R9XjIgPSAoZF97aWp9XjIpJCBzZSBvYnRpZW5lIHVuYSBtYXRyaXogZGUgcHJvZHVjdG8gaW50ZXJubyAkXG1hdGhiZntCfSQgbWVkaWFudGUKCiQkXG1hdGhiZntCfSA9IC1cdGZyYWN7MX17Mn0gXCwgXG1hdGhiZntKfSBcLCBcbWF0aGJme0R9XjIgXCwgXG1hdGhiZntKfQogJCQgZG9uZGUgJFxtYXRoYmZ7Sn0gPSBcbWF0aGJme0l9IC0gXHRmcmFjezF9e259XG1hdGhiZnsxMX1eXHRvcCQgZXMgbGEgbWF0cml6IGRlIGNlbnRyYWRvLCAkXG1hdGhiZntJfSQgbGEgbWF0cml6IGlkZW50aWRhZCB5ICRcbWF0aGJmezF9JCBlbCB2ZWN0b3IgZGUgdW5vcy4gRXN0YSB0cmFuc2Zvcm1hY2nDs24gY29udmllcnRlIGxhcyBkaXN0YW5jaWFzIGVuIHVuIG9iamV0byBhcHRvIHBhcmEgYW7DoWxpc2lzIGVzcGVjdHJhbC4KCkx1ZWdvLCBzZSByZWFsaXphIGxhIGRlc2NvbXBvc2ljacOzbiBlc3BlY3RyYWwgZGUgJFxtYXRoYmZ7Qn0kOgoKJCRcbWF0aGJme0J9ID0gXG1hdGhiZntWfVwsXG1hdGhiZntcTGFtYmRhfVwsXG1hdGhiZntWfV5cdG9wCiAkJAoKZG9uZGUgJFxtYXRoYmZ7XExhbWJkYX0kIGNvbnRpZW5lIGxvcyBhdXRvdmFsb3JlcyAkXGxhbWJkYV8xIFxnZXEgXGxhbWJkYV8yIFxnZXEgXGRvdHMgXGdlcSBcbGFtYmRhX24kIHkgJFxtYXRoYmZ7Vn0kIGxvcyBhdXRvdmVjdG9yZXMgY29ycmVzcG9uZGllbnRlcy4gTGFzIGNvb3JkZW5hZGFzIHByaW5jaXBhbGVzIHNlIGNvbnN0cnV5ZW4gdG9tYW5kbyBsb3MgJGskIGF1dG92YWxvcmVzIHBvc2l0aXZvcyBtw6FzIGdyYW5kZXMgeSBzdXMgYXV0b3ZlY3RvcmVzIGFzb2NpYWRvcy4KCkZpbmFsbWVudGUsIGxhcyBjb29yZGVuYWRhcyBwcmluY2lwYWxlcyBkZSBsb3Mgb2JqZXRvcyBzb246CgokJCBcbWF0aGJme1h9ID0gXG1hdGhiZntWfV9rIFwsXG1hdGhiZntcTGFtYmRhfV9rXnsxLzJ9JCQKCmRvbmRlICRcbWF0aGJme1Z9X2skIHNvbiBsb3MgYXV0b3ZlY3RvcmVzIHNlbGVjY2lvbmFkb3MgeSAkXG1hdGhiZntcTGFtYmRhfV9rXnsxLzJ9JCBlcyBsYSByYcOteiBjdWFkcmFkYSBkZSBsb3MgYXV0b3ZhbG9yZXMuIEVzdGUgbcOpdG9kbyBzZSB1dGlsaXphIGFtcGxpYW1lbnRlIGVuIGVjb2xvZ8OtYSAoYW7DoWxpc2lzIGRlIGNvbXVuaWRhZGVzKSwgZ2Vuw6l0aWNhIChlc3RydWN0dXJhcyBwb2JsYWNpb25hbGVzKSwgeSBjaWVuY2lhcyBzb2NpYWxlcyAocGVyY2VwY2lvbmVzLCBzaW1pbGl0dWRlcyksIHBvcnF1ZSBwZXJtaXRlIHZpc3VhbGl6YXIgZGF0b3MgY29tcGxlam9zIGRlIGRpc3RhbmNpYXMgZW4gdW4gZXNwYWNpbyByZWR1Y2lkbyBlIGludGVycHJldGFibGUuCgoqKk1lZGlkYSBkZSBwcmVjaXNpw7NuKioKCkxhIG1lZGlkYSBkZSBwcmVjaXNpw7NuIGVzIHVuIGluZGljYWRvciBlc3RhZMOtc3RpY28gcXVlIGV2YWzDumEgcXXDqSB0YW4gY2VyY2Fub3MgZXN0w6FuIGxvcyB2YWxvcmVzIG9ic2VydmFkb3MgbyBlc3RpbWFkb3MgcmVzcGVjdG8gYWwgdmFsb3IgdmVyZGFkZXJvIG8gZW50cmUgc8OtIG1pc21vcywgcmVmbGVqYW5kbyBlbCBncmFkbyBkZSBjb25zaXN0ZW5jaWEgZW4gbG9zIHJlc3VsdGFkb3M7IG1hdGVtw6F0aWNhbWVudGUgcHVlZGUgZXhwcmVzYXJzZSBjb21vIGxhIGludmVyc2EgZGUgbGEgZGlzcGVyc2nDs24gZGUgbG9zIGRhdG9zLCBwb3IgZWplbXBsbyBhIHRyYXbDqXMgZGUgbGEgdmFyaWFuemEgJFxzaWdtYV4yJCBvIGxhIGRlc3ZpYWNpw7NuIGVzdMOhbmRhciAkXHNpZ21hJCwgZG9uZGUgdmFsb3JlcyBiYWpvcyBpbmRpY2FuIG1heW9yIHByZWNpc2nDs24sIGxvIGN1YWwgZXMgZnVuZGFtZW50YWwgZW4gw6FyZWFzIGNvbW8gbGEgZXN0YWTDrXN0aWNhLCBlbCBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyB5IGxhIGludmVzdGlnYWNpw7NuIGV4cGVyaW1lbnRhbCwgeWEgcXVlIHBlcm1pdGUgZGV0ZXJtaW5hciBsYSBjb25maWFiaWxpZGFkIGRlIHVuIG3DqXRvZG8gZGUgbWVkaWNpw7NuIG8gZGUgdW4gbW9kZWxvIHByZWRpY3Rpdm8uCgoqKkVqZW1wbG8gZW4gUioqCgpgYGB7ciBmaWcuaGVpZ2h0PTEwLCBmaWcud2lkdGg9MTAsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CgoKIyBDcmVhbW9zIHVuIGRhdGFzZXQgc2VuY2lsbG8gKDQgcHVudG9zIGVuIDJEKQpzZXQuc2VlZCgxMjMpCmRhdGEgPC0gbWF0cml4KGMoMSwgMSwKICAgICAgICAgICAgICAgICA1LCAyLAogICAgICAgICAgICAgICAgIDIsIDQsCiAgICAgICAgICAgICAgICAgNiwgNSksIAogICAgICAgICAgICAgICBuY29sID0gMiwgYnlyb3cgPSBUUlVFKQoKcm93bmFtZXMoZGF0YSkgPC0gYygiQSIsICJCIiwgIkMiLCAiRCIpCgojIDEuIE1hdHJpeiBkZSBkaXN0YW5jaWFzIGV1Y2xpZGlhbmFzCkQgPC0gZGlzdChkYXRhKQoKIyAyLiBDb25zdHJ1Y2Npw7NuIGRlIGxhIG1hdHJpeiBCID0gLTEvMiBKIEReMiBKCm4gPC0gYXR0cihELCAiU2l6ZSIpCkQyIDwtIGFzLm1hdHJpeChEKV4yCkogPC0gZGlhZyhuKSAtICgxL24pICogbWF0cml4KDEsIG4sIG4pCkIgPC0gLTAuNSAqIEogJSolIEQyICUqJSBKCgojIDMuIERlc2NvbXBvc2ljacOzbiBlc3BlY3RyYWwgZGUgQgplaWcgPC0gZWlnZW4oQikKTGFtYmRhIDwtIGRpYWcoZWlnJHZhbHVlcykKViA8LSBlaWckdmVjdG9ycwoKIyA0LiBDb29yZGVuYWRhcyBwcmluY2lwYWxlcyBjb24gbG9zIDIgbWF5b3JlcyBhdXRvdmFsb3JlcwprIDwtIDIKWCA8LSBWWywgMTprXSAlKiUgc3FydChMYW1iZGFbMTprLCAxOmtdKQpjb29yZHMgPC0gYXMuZGF0YS5mcmFtZShYKQpjb29yZHMkbGFiZWwgPC0gcm93bmFtZXMoZGF0YSkKCiMgNS4gR3JhZmljYW1vcwpnZ3Bsb3QoY29vcmRzLCBhZXMoeCA9IFYxLCB5ID0gVjIsIGxhYmVsID0gbGFiZWwpKSArCiAgZ2VvbV9wb2ludChzaXplID0gNCwgY29sb3IgPSAic3RlZWxibHVlIikgKwogIGdlb21fdGV4dCh2anVzdCA9IC0xLCBzaXplID0gNSkgKwogIHRoZW1lX21pbmltYWwoYmFzZV9zaXplID0gMTQpICsKICBsYWJzKHRpdGxlID0gIkNvbnN0cnVjY2nDs24gZGUgQ29vcmRlbmFkYXMgUHJpbmNpcGFsZXMgKFBDb0EpIiwKICAgICAgIHggPSAiQ29vcmRlbmFkYSBQcmluY2lwYWwgMSIsCiAgICAgICB5ID0gIkNvb3JkZW5hZGEgUHJpbmNpcGFsIDIiKQoKYGBgCgojIyA3LjMpIEVzY2FsYWRvIG5vIG3DqXRyaWNvCgpFbCBlc2NhbGFkbyBubyBtw6l0cmljbyBlcyB1bmEgdMOpY25pY2EgZGUgcmVkdWNjacOzbiBkZSBkaW1lbnNpb25hbGlkYWQgZGVudHJvIGRlbCBhbsOhbGlzaXMgZGUgZXNjYWxhbWllbnRvIG11bHRpZGltZW5zaW9uYWwgKE1EUykgcXVlIGJ1c2NhIHJlcHJlc2VudGFyIG9iamV0b3MgZW4gdW4gZXNwYWNpbyBkZSBiYWphIGRpbWVuc2nDs24gcHJlc2VydmFuZG8gcHJpbmNpcGFsbWVudGUgZWwgb3JkZW4gZGUgbGFzIGRpc3RhbmNpYXMgbyBzaW1pbGl0dWRlcyBlbnRyZSBlbGxvcywgbcOhcyBxdWUgc3VzIHZhbG9yZXMgZXhhY3Rvcy4gQSBkaWZlcmVuY2lhIGRlbCBlc2NhbGFkbyBtw6l0cmljbywgcXVlIGludGVudGEgYXByb3hpbWFyIGRpcmVjdGFtZW50ZSBsYXMgZGlzdGFuY2lhcyBvcmlnaW5hbGVzICRkX3tpan0kLCBlbCBubyBtw6l0cmljbyBzZSBlbmZvY2EgZW4gbWFudGVuZXIgbGEgY29ycmVzcG9uZGVuY2lhIGVudHJlIGxhcyByZWxhY2lvbmVzIG9yZGluYWxlcyAocXXDqSB0YW4gY2VyY2Fub3MgbyBsZWphbm9zIHNvbiBsb3Mgb2JqZXRvcyBlbnRyZSBzw60pLgoKRWwgcHJvY2VkaW1pZW50byBzZSBiYXNhIGVuIGVuY29udHJhciB1bmEgY29uZmlndXJhY2nDs24gZGUgcHVudG9zIGVuIHVuIGVzcGFjaW8gZGUgZGltZW5zacOzbiByZWR1Y2lkYSBkb25kZSBsYXMgZGlzdGFuY2lhcyBldWNsaWRpYW5hcyAkZF97aWp9XiokIHByZXNlcnZlbiBlbCBvcmRlbiBkZSBsYXMgZGlzaW1pbGFyaWRhZGVzICRcZGVsdGFfe2lqfSQuIExhIGNhbGlkYWQgZGVsIGFqdXN0ZSBzZSBldmFsw7phIGEgdHJhdsOpcyBkZSBsYSBmdW5jacOzbiBkZSBlc3Ryw6lzLCB1c3VhbG1lbnRlIGRlZmluaWRhIGNvbW86CgokJFx0ZXh0e1N0cmVzc30gPSBcc3FydHtcZnJhY3tcc3VtX3tpPGp9IFxsZWZ0KCBmKFxkZWx0YV97aWp9KSAtIGRfe2lqfV4qIFxyaWdodCleMn17XHN1bV97aTxqfSBkX3tpan1eeyoyfX19CiQkIGRvbmRlICRmKFxkZWx0YV97aWp9KSQgZXMgdW5hIGZ1bmNpw7NuIG1vbsOzdG9uYSBxdWUgdHJhbnNmb3JtYSBsYXMgZGlzaW1pbGFyaWRhZGVzIG9yaWdpbmFsZXMgcGFyYSBhanVzdGFybGFzIGFsIGVzcGFjaW8gcHJveWVjdGFkby4KCkVzdGUgZW5mb3F1ZSByZXN1bHRhIMO6dGlsIGN1YW5kbyBsb3MgZGF0b3MgcHJvdmllbmVuIGRlIG1lZGljaW9uZXMgb3JkaW5hbGVzIG8gY3VhbGl0YXRpdmFzLCBlbiBsYXMgY3VhbGVzIG5vIHNlIHB1ZWRlIGFzdW1pciBxdWUgbGFzIGRpc3RhbmNpYXMgbnVtw6lyaWNhcyBzZWFuIHNpZ25pZmljYXRpdmFzLCBwZXJvIHPDrSBpbXBvcnRhIGVsIG9yZGVuIGRlIGNlcmNhbsOtYSBlbnRyZSBsb3Mgb2JqZXRvcy4gRWplbXBsb3MgdMOtcGljb3Mgc29uIGVuY3Vlc3RhcyBkZSBwZXJjZXBjacOzbiwganVpY2lvcyBkZSBwcmVmZXJlbmNpYSwgZGF0b3MgcHNpY29sw7NnaWNvcyBvIHJhbmtpbmdzLgoKRW4gbGEgcHLDoWN0aWNhLCBlbCBlc2NhbGFkbyBubyBtw6l0cmljbyBzZSBhcGxpY2EgcGFyYSB2aXN1YWxpemFyIHJlbGFjaW9uZXMgc3VieWFjZW50ZXMgZW4gZGF0b3MgY29tcGxlam9zLCBjb21vIGxhIHByb3hpbWlkYWQgZW50cmUgbWFyY2FzIGVuIGVzdHVkaW9zIGRlIG1lcmNhZG8sIGxhIHNpbWlsaXR1ZCBlbiBwYXRyb25lcyBkZSBjb21wb3J0YW1pZW50byBkZSB1c3VhcmlvcywgbyBsYSBvcmdhbml6YWNpw7NuIHBlcmNlcHR1YWwgZGUgZXN0w61tdWxvcyBlbiBwc2ljb2xvZ8OtYTsgZW4gZXN0b3MgY2Fzb3MsIGxvIHJlbGV2YW50ZSBlcyBjb25zZXJ2YXIgbGEgZXN0cnVjdHVyYSByZWxhY2lvbmFsIHUgb3JkaW5hbCBtw6FzIHF1ZSBsYSBtYWduaXR1ZCBleGFjdGEgZGUgbGFzIGRpc3RhbmNpYXMuCgoqKkVqZW1wbG8gZW4gUioqCgpgYGB7cn0KCiMgRGF0b3MgZGUgdmVnZXRhY2nDs24gZGUgZWplbXBsbwpkYXRhKGR1bmUpICAgIyBkYXRhc2V0IGRpc3BvbmlibGUgZW4gdmVnYW4KCiMgQ2FsY3VsYXIgbWF0cml6IGRlIGRpc2ltaWxpdHVkZXMgKGRpc3RhbmNpYSBkZSBCcmF5LUN1cnRpcykKZHVuZS5kaXN0IDwtIHZlZ2Rpc3QoZHVuZSwgbWV0aG9kID0gImJyYXkiKQoKIyBBcGxpY2FyIE5NRFMgY29uIDIgZGltZW5zaW9uZXMKc2V0LnNlZWQoMTIzKSAjIFBhcmEgcmVwcm9kdWNpYmlsaWRhZApubWRzIDwtIG1ldGFNRFMoZHVuZS5kaXN0LCBrID0gMiwgdHJ5bWF4ID0gMTAwKQoKIyBNb3N0cmFyIHJlc3VsdGFkb3MKcHJpbnQobm1kcykKCiMgR3JhZmljYXIgZWwgcmVzdWx0YWRvCnBsb3Qobm1kcywgdHlwZSA9ICJ0IiwgbWFpbiA9ICJFc2NhbGFkbyBObyBNw6l0cmljbyAoTk1EUykgLSBFamVtcGxvIGdyw6FmaWNvIikKCmBgYAoKIyMgNy40KSBFamVtcGxvIFB5dGhvbgoKIyMjIDcuNC4xKSBFc2NhbGFtaWVudG8gTXVsdGlkaW1lbnNpb25hbCAoTURTIGNsw6FzaWNvKQoKYGBge3B5dGhvbn0KCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyBFamVtcGxvIGLDoXNpY28gZGUgTURTIGVuIFB5dGhvbgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KCiMgMS4gRGlzdGFuY2lhcyAoZW4ga20pIGVudHJlIGFsZ3VuYXMgY2l1ZGFkZXMKY2l1ZGFkZXMgPSBbIlJvbWEiLCAiUGFyw61zIiwgIkJlcmzDrW4iLCAiTWFkcmlkIiwgIkxvbmRyZXMiXQpkaXN0YW5jaWFzID0gbnAuYXJyYXkoWwogICAgWzAsIDExMDUsIDExODEsIDEzNjUsIDE0MzVdLAogICAgWzExMDUsIDAsIDEwNTAsIDEyNzYsIDM0M10sCiAgICBbMTE4MSwgMTA1MCwgMCwgMTg3MCwgOTMwXSwKICAgIFsxMzY1LCAxMjc2LCAxODcwLCAwLCAxMjYwXSwKICAgIFsxNDM1LCAzNDMsIDkzMCwgMTI2MCwgMF0KXSkKCiMgMi4gQXBsaWNhbW9zIE1EUyBjbMOhc2ljbyAoMiBkaW1lbnNpb25lcykKbWRzID0gTURTKG5fY29tcG9uZW50cz0yLCBkaXNzaW1pbGFyaXR5PSJwcmVjb21wdXRlZCIsIHJhbmRvbV9zdGF0ZT0xMjMpCmNvb3JkcyA9IG1kcy5maXRfdHJhbnNmb3JtKGRpc3RhbmNpYXMpCgojIDMuIEdyYWZpY2Ftb3MKcGx0LmZpZ3VyZShmaWdzaXplPSg4LDYpKQpwbHQuc2NhdHRlcihjb29yZHNbOiwwXSwgY29vcmRzWzosMV0sIGNvbG9yPSJibHVlIikKZm9yIGksIGNpdWRhZCBpbiBlbnVtZXJhdGUoY2l1ZGFkZXMpOgogICAgcGx0LnRleHQoY29vcmRzW2ksMF0rMTAsIGNvb3Jkc1tpLDFdKzEwLCBjaXVkYWQsIGZvbnRzaXplPTEyLCBjb2xvcj0iZGFya2JsdWUiKQpwbHQudGl0bGUoIkVzY2FsYWRvIE11bHRpZGltZW5zaW9uYWwgKE1EUykiLCBmb250c2l6ZT0xNCkKcGx0LnhsYWJlbCgiRGltIDEiKQpwbHQueWxhYmVsKCJEaW0gMiIpCnBsdC5ncmlkKFRydWUsIGxpbmVzdHlsZT0iLS0iLCBhbHBoYT0wLjUpCnBsdC5zaG93KCkKCmBgYAoKIyMjIDcuNC4yKSBFc2NhbGFkb3MgbcOpdHJpY29zOiBjb29yZGVuYWRhcyBwcmluY2lwYWxlcyAoUENvQSkKCmBgYHtweXRob259CgojIERhdGFzZXQgaXJpcwoKaXJpcyA9IGxvYWRfaXJpcygpClggPSBpcmlzLmRhdGEKeSA9IGlyaXMudGFyZ2V0CmxhYmVscyA9IGlyaXMudGFyZ2V0X25hbWVzCgojIDEuIE1hdHJpeiBkZSBkaXN0YW5jaWFzIGV1Y2xpZGlhbmFzCmRpc3RfaXJpcyA9IHBhaXJ3aXNlX2Rpc3RhbmNlcyhYLCBtZXRyaWM9ImV1Y2xpZGVhbiIpCgojIDIuIEVzY2FsYWRvIE3DqXRyaWNvIChQQ29BKSB+IHNlIGltcGxlbWVudGEgY29uIE1EUyAicHJlY29tcHV0ZWQiCnBjb2EgPSBNRFMobl9jb21wb25lbnRzPTIsIGRpc3NpbWlsYXJpdHk9InByZWNvbXB1dGVkIiwgcmFuZG9tX3N0YXRlPTQyKQpjb29yZHMgPSBwY29hLmZpdF90cmFuc2Zvcm0oZGlzdF9pcmlzKQoKIyBDb252ZXJ0aW1vcyBhIERhdGFGcmFtZQpkZl9jb29yZHMgPSBwZC5EYXRhRnJhbWUoY29vcmRzLCBjb2x1bW5zPVsiRGltMSIsICJEaW0yIl0pCmRmX2Nvb3Jkc1siU3BlY2llcyJdID0gW2xhYmVsc1tpXSBmb3IgaSBpbiB5XQoKIyAzLiBHcmFmaWNhbW9zCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCw2KSkKc25zLnNjYXR0ZXJwbG90KGRhdGE9ZGZfY29vcmRzLCB4PSJEaW0xIiwgeT0iRGltMiIsIGh1ZT0iU3BlY2llcyIsIHM9NjAsIGFscGhhPTAuOCkKcGx0LnRpdGxlKCJFc2NhbGFkbyBNw6l0cmljbyAoUENvQSkgZW4gaXJpcyIsIGZvbnRzaXplPTE0KQpwbHQueGxhYmVsKCJDb29yZGVuYWRhIFByaW5jaXBhbCAxIikKcGx0LnlsYWJlbCgiQ29vcmRlbmFkYSBQcmluY2lwYWwgMiIpCnBsdC5zaG93KCkKCmBgYAoKIyMjIDcuNC4zKSBDb25zdHJ1Y2Npw7NuIGRlIGxhcyBjb29yZGVuYWRhcyBwcmluY2lwYWxlcwoKYGBge3B5dGhvbn0KIyBFamVtcGxvIHNlbmNpbGxvIGNvbiA0IHB1bnRvcyBlbiAyRApkYXRhID0gbnAuYXJyYXkoWwogICAgWzEsIDFdLAogICAgWzUsIDJdLAogICAgWzIsIDRdLAogICAgWzYsIDVdCl0pCmxhYmVscyA9IFsiQSIsICJCIiwgIkMiLCAiRCJdCgojIDEuIE1hdHJpeiBkZSBkaXN0YW5jaWFzCkQgPSBwYWlyd2lzZV9kaXN0YW5jZXMoZGF0YSwgbWV0cmljPSJldWNsaWRlYW4iKQpEMiA9IEQqKjIKbiA9IEQuc2hhcGVbMF0KCiMgMi4gTWF0cml6IGRlIGNlbnRyYWRvIEoKSiA9IG5wLmV5ZShuKSAtIG5wLm9uZXMoKG4sbikpL24KCiMgMy4gTWF0cml6IEIKQiA9IC0wLjUgKiBKLmRvdChEMikuZG90KEopCgojIDQuIERlc2NvbXBvc2ljacOzbiBlc3BlY3RyYWwKZWlndmFscywgZWlndmVjcyA9IG5wLmxpbmFsZy5laWdoKEIpCgojIE9yZGVuYW1vcyBkZSBtYXlvciBhIG1lbm9yCmlkeCA9IG5wLmFyZ3NvcnQoZWlndmFscylbOjotMV0KZWlndmFscyA9IGVpZ3ZhbHNbaWR4XQplaWd2ZWNzID0gZWlndmVjc1s6LGlkeF0KCiMgNS4gQ29vcmRlbmFkYXMgcHJpbmNpcGFsZXMgKDJEKQprID0gMgpMID0gbnAuZGlhZyhucC5zcXJ0KGVpZ3ZhbHNbOmtdKSkKViA9IGVpZ3ZlY3NbOiwgOmtdClhfY29vcmRzID0gVi5kb3QoTCkKCiMgNi4gR3JhZmljYW1vcwpwbHQuZmlndXJlKGZpZ3NpemU9KDYsNikpCnBsdC5zY2F0dGVyKFhfY29vcmRzWzosMF0sIFhfY29vcmRzWzosMV0sIGNvbG9yPSJzdGVlbGJsdWUiLCBzPTgwKQpmb3IgaSwgbGFiZWwgaW4gZW51bWVyYXRlKGxhYmVscyk6CiAgICBwbHQudGV4dChYX2Nvb3Jkc1tpLDBdKzAuMDUsIFhfY29vcmRzW2ksMV0rMC4wNSwgbGFiZWwsIGZvbnRzaXplPTEyKQpwbHQudGl0bGUoIkNvbnN0cnVjY2nDs24gZGUgQ29vcmRlbmFkYXMgUHJpbmNpcGFsZXMgKFBDb0EpIikKcGx0LnhsYWJlbCgiQ29vcmQuIFByaW5jaXBhbCAxIikKcGx0LnlsYWJlbCgiQ29vcmQuIFByaW5jaXBhbCAyIikKcGx0LmdyaWQoVHJ1ZSwgbGluZXN0eWxlPSItLSIsIGFscGhhPTAuNSkKcGx0LnNob3coKQoKYGBgCgojIyMgNy40LjQpIEVzY2FsYWRvIE5vIE3DqXRyaWNvIChOTURTKQoKYGBge3B5dGhvbn0KIyBFc2NhbGFkbyBObyBNw6l0cmljbyBjb24gc2tsZWFybiAobWV0cmljPUZhbHNlKQpubWRzID0gTURTKG5fY29tcG9uZW50cz0yLCBkaXNzaW1pbGFyaXR5PSJwcmVjb21wdXRlZCIsIG1ldHJpYz1GYWxzZSwgcmFuZG9tX3N0YXRlPTEyMywgbl9pbml0PTEwLCBtYXhfaXRlcj0xMDAwKQpjb29yZHNfbm1kcyA9IG5tZHMuZml0X3RyYW5zZm9ybShkaXN0X2lyaXMpCgojIERhdGFGcmFtZSBwYXJhIGdyYWZpY2FyCmRmX25tZHMgPSBwZC5EYXRhRnJhbWUoY29vcmRzX25tZHMsIGNvbHVtbnM9WyJEaW0xIiwgIkRpbTIiXSkKZGZfbm1kc1siU3BlY2llcyJdID0gW2xhYmVsc1tpXSBmb3IgaSBpbiB5XQoKIyBHcmFmaWNhbW9zCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCw2KSkKc25zLnNjYXR0ZXJwbG90KGRhdGE9ZGZfbm1kcywgeD0iRGltMSIsIHk9IkRpbTIiLCBodWU9IlNwZWNpZXMiLCBzPTYwLCBhbHBoYT0wLjgpCnBsdC50aXRsZSgiRXNjYWxhZG8gTm8gTcOpdHJpY28gKE5NRFMpIGVuIGlyaXMiLCBmb250c2l6ZT0xNCkKcGx0LnhsYWJlbCgiTk1EUyAxIikKcGx0LnlsYWJlbCgiTk1EUyAyIikKcGx0LnNob3coKQoKYGBgCgojIDgpIENsdXN0ZXJpemFjacOzbgoKRWwgYW7DoWxpc2lzIGRlIGNsw7pzdGVyIG8gY29uZ2xvbWVyYWRvcyBlcyB1bmEgdMOpY25pY2EgZGUgZXN0YWTDrXN0aWNhIG11bHRpdmFyaWFudGUgcXVlIGJ1c2NhIGFncnVwYXIgb2JqZXRvcyBvIGluZGl2aWR1b3MgZW4gZnVuY2nDs24gZGUgc3Ugc2ltaWxpdHVkLCBkZSBtb2RvIHF1ZSBsb3MgZWxlbWVudG9zIGRlbnRybyBkZSB1biBtaXNtbyBncnVwbyBzZWFuIG3DoXMgcGFyZWNpZG9zIGVudHJlIHPDrSBxdWUgY29uIGxvcyBkZSBvdHJvcyBncnVwb3MuIEEgZGlmZXJlbmNpYSBkZSBtw6l0b2RvcyBzdXBlcnZpc2Fkb3MsIG5vIHJlcXVpZXJlIHZhcmlhYmxlcyBkZXBlbmRpZW50ZXMgcHJlZGVmaW5pZGFzLCBzaW5vIHF1ZSB0cmFiYWphIMO6bmljYW1lbnRlIGNvbiBtZWRpZGFzIGRlIHNpbWlsaXR1ZCBvIGRpc2ltaWxpdHVkLCBjb23Dum5tZW50ZSBiYXNhZGFzIGVuIGRpc3RhbmNpYXMgY29tbyBsYSBldWNsaWRpYW5hICRkX3tpan0gPSBcc3FydHtcc3VtX3trPTF9XnAgKHhfe2lrfSAtIHhfe2prfSleMn0kIGVudHJlIGRvcyBvYnNlcnZhY2lvbmVzICRpJCB5ICRqJCBlbiB1biBlc3BhY2lvIGRlICRwJCBkaW1lbnNpb25lcy4KCkVzdGUgYW7DoWxpc2lzIHNpcnZlIHBhcmEgZGVzY3VicmlyIGVzdHJ1Y3R1cmFzIG9jdWx0YXMgZW4gbG9zIGRhdG9zIHkgZmFjaWxpdGFyIGxhIGludGVycHJldGFjacOzbiBkZSBwYXRyb25lcy4gRXhpc3RlbiBkaWZlcmVudGVzIGVuZm9xdWVzLCBlbnRyZSBlbGxvcyBsb3MgbcOpdG9kb3MgamVyw6FycXVpY29zLCBxdWUgY29uc3RydXllbiB1bmEgZXN0cnVjdHVyYSBlbiBmb3JtYSBkZSDDoXJib2wgbyBkZW5kcm9ncmFtYSwgeSBsb3MgbcOpdG9kb3Mgbm8gamVyw6FycXVpY29zLCBjb21vICRrJC1tZWFucywgcXVlIGRpdmlkZW4gbG9zIGRhdG9zIGVuIHVuIG7Dum1lcm8gZmlqbyBkZSBncnVwb3MgbWluaW1pemFuZG8gdW5hIGZ1bmNpw7NuIGRlIHZhcmlhY2nDs24gaW50ZXJuYSwgcG9yIGVqZW1wbG86CgokJEogPSBcc3VtX3tpPTF9XmsgXHN1bV97eCBcaW4gQ19pfSBcfCB4IC0gXG11X2kgXHxeMiQkCgpkb25kZSAkQ19pJCBlcyBlbCBjbMO6c3RlciAkaSQgeSAkXG11X2kkIGVzIHN1IGNlbnRyb2lkZQoKTG9zIGNhc29zIGRlIHVzbyBkZWwgYW7DoWxpc2lzIGRlIGNsw7pzdGVyIHNvbiBhbXBsaW9zOiBlbiBtYXJrZXRpbmcgcGVybWl0ZSBzZWdtZW50YXIgY2xpZW50ZXMgc2Vnw7puIGjDoWJpdG9zIGRlIGNvbnN1bW8sIGVuIGJpb2xvZ8OtYSBzZSB1dGlsaXphIHBhcmEgY2xhc2lmaWNhciBlc3BlY2llcyBvIGFuYWxpemFyIHBlcmZpbGVzIGdlbsOpdGljb3MsIGVuIGVjb25vbcOtYSBzZSBwdWVkZW4gaWRlbnRpZmljYXIgZ3J1cG9zIGRlIHBhw61zZXMgY29uIGVzdHJ1Y3R1cmFzIGVjb27Ds21pY2FzIHNpbWlsYXJlcywgeSBlbiBzZWd1cmlkYWQgYXl1ZGEgYSBkZXRlY3RhciBwYXRyb25lcyBkZSBjb21wb3J0YW1pZW50byBhbsOzbWFsby4gRW4gdG9kb3MgZXN0b3MgY2Fzb3MsIGVsIG9iamV0aXZvIGVzIHNpbXBsaWZpY2FyIGxhIGNvbXBsZWppZGFkIGRlIGxvcyBkYXRvcyBwYXJhIGZhY2lsaXRhciBsYSB0b21hIGRlIGRlY2lzaW9uZXMuCgpFbiByZXN1bWVuLCBlbCBhbsOhbGlzaXMgZGUgY29uZ2xvbWVyYWRvcyBlcyB1bmEgaGVycmFtaWVudGEgZXhwbG9yYXRvcmlhIGVzZW5jaWFsIGN1YW5kbyBzZSBxdWllcmUgcGFzYXIgZGUgdW5hIGNvbGVjY2nDs24gZGUgZGF0b3Mgc2luIGVzdHJ1Y3R1cmEgYXBhcmVudGUgYSBhZ3J1cGFjaW9uZXMgaW50ZXJwcmV0YWJsZXMsIHJldmVsYW5kbyByZWxhY2lvbmVzIHkgdGVuZGVuY2lhcyBxdWUgbm8gc2Vyw61hbiB2aXNpYmxlcyBhIHNpbXBsZSB2aXN0YS4KCiMjIDguMSkgSy1tZWRpYXMKCkVsIG3DqXRvZG8gZGUgJGskLW1lZGlhcyBlcyB1bmEgdMOpY25pY2EgZGUgYW7DoWxpc2lzIGRlIGNvbmdsb21lcmFkb3MgcXVlIGJ1c2NhIHBhcnRpY2lvbmFyIHVuIGNvbmp1bnRvIGRlIG9ic2VydmFjaW9uZXMgZW4gJGskIGdydXBvcyBvIGNsw7pzdGVyZXMgZGUgdGFsIG1hbmVyYSBxdWUgbG9zIGVsZW1lbnRvcyBkZW50cm8gZGUgY2FkYSBncnVwbyBzZWFuIGxvIG3DoXMgc2ltaWxhcmVzIHBvc2libGUgZW50cmUgc8OtIHkgbG8gbcOhcyBkaWZlcmVudGVzIHBvc2libGUgcmVzcGVjdG8gYSBsb3MgZGUgb3Ryb3MgZ3J1cG9zLiBDYWRhIGNsw7pzdGVyIHNlIHJlcHJlc2VudGEgYSB0cmF2w6lzIGRlIHN1IGNlbnRyb2lkZSwgZXMgZGVjaXIsIGxhIG1lZGlhIGRlIGxhcyBvYnNlcnZhY2lvbmVzIGFzaWduYWRhcyBhIGVzZSBncnVwby4KCkVsIG9iamV0aXZvIGRlbCBhbGdvcml0bW8gZXMgbWluaW1pemFyIGxhIHN1bWEgZGUgbGFzIGRpc3RhbmNpYXMgY3VhZHJhZGFzIGVudHJlIGNhZGEgcHVudG8gJHgkIHkgZWwgY2VudHJvaWRlICRcbXUkIGRlIHN1IGNsw7pzdGVyLiBNYXRlbcOhdGljYW1lbnRlLCBzZSBleHByZXNhIGNvbW86Cgpkb25kZSAkQ19pJCByZXByZXNlbnRhIGVsICRpJC3DqXNpbW8gY2zDunN0ZXIgeSAkXG11X2kkIHN1IGNlbnRyb2lkZS4KCkVzdGUgbcOpdG9kbyBzZSB1dGlsaXphIHBhcmEgc2VnbWVudGFjacOzbiBkZSBtZXJjYWRvcywgY2xhc2lmaWNhY2nDs24gZGUgY2xpZW50ZXMsIHJlZHVjY2nDs24gZGUgZGF0b3MgeSBhZ3J1cGFtaWVudG8gZW4gYmlvaW5mb3Jtw6F0aWNhLCBlbnRyZSBvdHJvcy4gU3UgZm9ydGFsZXphIGVzIGxhIHNpbXBsaWNpZGFkIHkgbGEgcmFwaWRleiwgYXVucXVlIHByZXNlbnRhIGxpbWl0YWNpb25lcyBjb21vIGxhIHNlbnNpYmlsaWRhZCBhIHZhbG9yZXMgYXTDrXBpY29zIHkgYSBsYSBlbGVjY2nDs24gaW5pY2lhbCBkZSBsb3MgY2VudHJvaWRlcy4KClVuIGNhc28gZGUgdXNvIGNvbcO6biBlcyBlbiBlbXByZXNhcyBxdWUgYnVzY2FuIHNlZ21lbnRhciBjbGllbnRlcyBzZWfDum4gaMOhYml0b3MgZGUgY29tcHJhOiBhIHRyYXbDqXMgZGUgJGskLW1lZGlhcywgc2UgcHVlZGVuIGlkZW50aWZpY2FyIGdydXBvcyBkZSBjb25zdW1pZG9yZXMgY29uIHBhdHJvbmVzIHNpbWlsYXJlcyBkZSBnYXN0bywgbG8gcXVlIHBlcm1pdGUgZGlzZcOxYXIgZXN0cmF0ZWdpYXMgZGUgbWFya2V0aW5nIGRpZmVyZW5jaWFkYXMgeSBwZXJzb25hbGl6YWRhcyBwYXJhIGNhZGEgc2VnbWVudG8uCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CiMgRWplbXBsbyBncsOhZmljbyBkZSBLLW1lZGlhcyBlbiBSCgpzZXQuc2VlZCgxMjMpICAjIFJlcHJvZHVjaWJpbGlkYWQKCiMgMS4gR2VuZXJhciBkYXRvcyBzaW11bGFkb3MgKDMgZ3J1cG9zIGVuIDJEKQp4IDwtIHJiaW5kKAogIG1hdHJpeChybm9ybSgxMDAsIG1lYW4gPSAwLCBzZCA9IDAuMyksIG5jb2wgPSAyKSwKICBtYXRyaXgocm5vcm0oMTAwLCBtZWFuID0gMiwgc2QgPSAwLjMpLCBuY29sID0gMiksCiAgbWF0cml4KHJub3JtKDEwMCwgbWVhbiA9IC0yLCBzZCA9IDAuMyksIG5jb2wgPSAyKQopCgojIDIuIEFwbGljYXIgSy1tZWRpYXMgY29uIGsgPSAzCmttZWFuc19yZXMgPC0ga21lYW5zKHgsIGNlbnRlcnMgPSAzLCBuc3RhcnQgPSAyMCkKCiMgMy4gR3JhZmljYXIgbG9zIHJlc3VsdGFkb3MKcGxvdCh4LCBjb2wgPSBrbWVhbnNfcmVzJGNsdXN0ZXIsIHBjaCA9IDE5LAogICAgIG1haW4gPSAiRWplbXBsbyBncsOhZmljbyBkZSBLLW1lZGlhcyIsCiAgICAgeGxhYiA9ICJYMSIsIHlsYWIgPSAiWDIiKQpwb2ludHMoa21lYW5zX3JlcyRjZW50ZXJzLCBjb2wgPSAxOjMsIHBjaCA9IDgsIGNleCA9IDMsIGx3ZCA9IDIpICMgY2VudHJvaWRlcwoKYGBgCgojIyA4LjIpIEstbWVkb2lkZXM6IGFsZ29yaXRtbyBwYW0KCkVsIG3DqXRvZG8gZGUgJGskLW1lZG9pZGVzLCB0YW1iacOpbiBjb25vY2lkbyBjb21vIGFsZ29yaXRtbyBQQU0gKFBhcnRpdGlvbmluZyBBcm91bmQgTWVkb2lkcyksIGVzIHVuYSB0w6ljbmljYSBkZSBhbsOhbGlzaXMgZGUgY29uZ2xvbWVyYWRvcyBzaW1pbGFyIGEgJGskLW1lZGlhcywgcGVybyBlbiBsdWdhciBkZSB1c2FyIGNlbnRyb2lkZXMgKHByb21lZGlvcyksIHNlbGVjY2lvbmEgY29tbyByZXByZXNlbnRhdGl2byBkZSBjYWRhIGdydXBvIHVuIG1lZG9pZGUsIGVzIGRlY2lyLCBlbCBvYmpldG8gcmVhbCBkZWwgY29uanVudG8gZGUgZGF0b3MgbcOhcyBjZW50cmFsIGRlbnRybyBkZWwgY2zDunN0ZXIuIEVzdG8gbG8gaGFjZSBtw6FzIHJvYnVzdG8gZnJlbnRlIGEgdmFsb3JlcyBhdMOtcGljb3MsIHlhIHF1ZSBsb3MgbWVkb2lkZXMgbm8gc2UgdmVuIHRhbiBhZmVjdGFkb3MgcG9yIGV4dHJlbW9zIGNvbW8gbGFzIG1lZGlhcy4KCkVsIG9iamV0aXZvIGRlbCBhbGdvcml0bW8gUEFNIGVzIG1pbmltaXphciBsYSBzdW1hIGRlIGRpc3RhbmNpYXMgZW50cmUgbG9zIGVsZW1lbnRvcyBkZSBjYWRhIGNsw7pzdGVyIHkgc3UgcmVzcGVjdGl2byBtZWRvaWRlLiBGb3JtYWxtZW50ZSwgc2kgdGVuZW1vcyB1biBjb25qdW50byBkZSBkYXRvcyAkWCA9IHt4XzEsIHhfMiwgXGRvdHMsIHhfbn0kIHkgdW4gY29uanVudG8gZGUgbWVkb2lkZXMgJE0gPSB7bV8xLCBtXzIsIFxkb3RzLCBtX2t9JCwgbGEgZnVuY2nDs24gYSBtaW5pbWl6YXIgZXM6CgokJFxzdW1fe2k9MX1ee2t9IFxzdW1fe3ggXGluIENfaX0gZCh4LCBtX2kpJCQKCmRvbmRlICRkKHgsIG1faSkkIHJlcHJlc2VudGEgbGEgZGlzdGFuY2lhIChwb3IgZWplbXBsbywgZXVjbGlkaWFuYSBvIE1hbmhhdHRhbikgZW50cmUgdW4gcHVudG8gJHgkIHkgc3UgbWVkb2lkZSAkbV9pJCwgeSAkQ19pJCBlcyBlbCBjb25qdW50byBkZSBwdW50b3MgYXNpZ25hZG9zIGFsIG1lZG9pZGUgJG1faSQuCgpFbCBhbGdvcml0bW8gUEFNIGZ1bmNpb25hIGVuIGRvcyBmYXNlczogKDEpIFNlbGVjY2nDs24gaW5pY2lhbCBkZSAkayQgbWVkb2lkZXMgcXVlIHJlcHJlc2VudGFuIGEgbG9zIGRhdG9zLCB5ICgyKSBpdGVyYWNpw7NuIGRlIGludGVyY2FtYmlvLCBkb25kZSBzZSBldmFsw7phIHNpIHN1c3RpdHVpciB1biBtZWRvaWRlIHBvciBvdHJvIHB1bnRvIG1lam9yYSBsYSBmdW5jacOzbiBvYmpldGl2by4gRWwgcHJvY2VzbyBjb250aW7DumEgaGFzdGEgcXVlIG5vIHNlIGxvZ3JhIHVuYSBtZWpvcmEuCgpFbiBjdWFudG8gYSBzdXMgY2Fzb3MgZGUgdXNvLCBQQU0gZXMgw7p0aWwgY3VhbmRvIHNlIHRyYWJhamEgY29uIGRhdG9zIHF1ZSBjb250aWVuZW4gdmFsb3JlcyBhdMOtcGljb3MsIGN1YW5kbyBsYXMgZGlzdGFuY2lhcyBubyBzb24gbmVjZXNhcmlhbWVudGUgZXVjbGlkaWFuYXMgbyBjdWFuZG8gc2UgcmVxdWllcmUgdW4gcmVwcmVzZW50YW50ZSByZWFsIGRlIGxvcyBkYXRvcyAoY29tbyBlbiBiaW9pbmZvcm3DoXRpY2EsIG1pbmVyw61hIGRlIHRleHRvcyBvIHNlZ21lbnRhY2nDs24gZGUgY2xpZW50ZXMpLiBBIGRpZmVyZW5jaWEgZGUgJGskLW1lZGlhcywgUEFNIHB1ZWRlIHRyYWJhamFyIGNvbiBjdWFscXVpZXIgbWF0cml6IGRlIGRpc2ltaWxhcmlkYWQgeSBubyBzZSByZXN0cmluZ2UgYWwgZXNwYWNpbyB2ZWN0b3JpYWwgdHJhZGljaW9uYWwuCgoqKkVqZW1wbG8gZW4gUioqCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQoKCiMgR2VuZXJhciBkYXRvcyBzaW11bGFkb3MgY29uIDMgZ3J1cG9zCnNldC5zZWVkKDEyMykKZGF0b3MgPC0gcmJpbmQoCiAgbWF0cml4KHJub3JtKDEwMCwgbWVhbiA9IDAsIHNkID0gMC41KSwgbmNvbCA9IDIpLAogIG1hdHJpeChybm9ybSgxMDAsIG1lYW4gPSAzLCBzZCA9IDAuNSksIG5jb2wgPSAyKSwKICBtYXRyaXgocm5vcm0oMTAwLCBtZWFuID0gNiwgc2QgPSAwLjUpLCBuY29sID0gMikKKQpjb2xuYW1lcyhkYXRvcykgPC0gYygiWDEiLCAiWDIiKQoKIyBBcGxpY2FyIFBBTSBjb24gMyBjbMO6c3RlcmVzCnBhbV9yZXMgPC0gcGFtKGRhdG9zLCBrID0gMykKCiMgVmlzdWFsaXphciBsb3MgY2zDunN0ZXJlcwpmdml6X2NsdXN0ZXIocGFtX3JlcywgZ2VvbSA9ICJwb2ludCIsIGVsbGlwc2UudHlwZSA9ICJub3JtIiwKICAgICAgICAgICAgIHBhbGV0dGUgPSAiamNvIiwgcmVwZWwgPSBUUlVFLCBnZ3RoZW1lID0gdGhlbWVfbWluaW1hbCgpKSArCiAgZ2d0aXRsZSgiQ2x1c3RlcmluZyBjb24gSy1tZWRvaWRlcyAoUEFNKSIpCgpgYGAKCiMjIDguMykgTcOpdG9kbyBqZXLDoXJxdWljbwoKRWwgbcOpdG9kbyBqZXLDoXJxdWljbyBlcyB1bmEgdMOpY25pY2EgZGUgYW7DoWxpc2lzIGRlIGNvbmdsb21lcmFkb3MgcXVlIG9yZ2FuaXphIGxvcyBkYXRvcyBlbiB1bmEgZXN0cnVjdHVyYSBlbiBmb3JtYSBkZSDDoXJib2wgbGxhbWFkYSBkZW5kcm9ncmFtYSwgZG9uZGUgbGFzIG9ic2VydmFjaW9uZXMgc2UgdmFuIGFncnVwYW5kbyBkZSBtYW5lcmEgcHJvZ3Jlc2l2YS4gQSBkaWZlcmVuY2lhIGRlIG3DqXRvZG9zIGNvbW8gJGskLW1lZGlhcywgbm8gZXMgbmVjZXNhcmlvIGZpamFyIHByZXZpYW1lbnRlIGVsIG7Dum1lcm8gZGUgY2zDunN0ZXJlcywgc2lubyBxdWUgZXN0ZSBzZSBwdWVkZSBkZWNpZGlyIG9ic2VydmFuZG8gZWwgbml2ZWwgZGUgY29ydGUgZGVsIGRlbmRyb2dyYW1hLgoKRXhpc3RlbiBkb3MgZW5mb3F1ZXMgcHJpbmNpcGFsZXM6IGVsIGFnbG9tZXJhdGl2byAoYm90dG9tLXVwKSwgZG9uZGUgY2FkYSBvYnNlcnZhY2nDs24gaW5pY2lhIGNvbW8gdW4gY2zDunN0ZXIgaW5kZXBlbmRpZW50ZSB5IHNlIHZhbiBmdXNpb25hbmRvIHNlZ8O6biBzdSBzaW1pbGl0dWQsIHkgZWwgZGl2aXNpdm8gKHRvcC1kb3duKSwgZG9uZGUgc2UgcGFydGUgZGUgdW4gc29sbyBncnVwbyB5IHNlIGRpdmlkZSByZWN1cnNpdmFtZW50ZS4gRWwgY3JpdGVyaW8gZGUgc2ltaWxpdHVkIHNlIGJhc2EgZW4gdW5hIG1lZGlkYSBkZSBkaXN0YW5jaWEsIGNvbW8gbGEgZGlzdGFuY2lhIGV1Y2xpZGlhbmEgJGRfe2lqfSA9IFxzcXJ0e1xzdW1fe2s9MX1ee3B9KHhfe2lrfSAtIHhfe2prfSleMn0kIGVudHJlIGRvcyBwdW50b3MgJGkkIHkgJGokLgoKRWwgcGFzbyBjcnVjaWFsIGVzIGRlZmluaXIgY8OzbW8gbWVkaXIgbGEgZGlzdGFuY2lhIGVudHJlIGdydXBvcy4gUG9yIGVqZW1wbG8sIGVuIGVsIGVubGFjZSBzZW5jaWxsbyBzZSB0b21hIGxhIG3DrW5pbWEgZGlzdGFuY2lhIGVudHJlIHBhcmVzIGRlIG9ic2VydmFjaW9uZXMsIG1pZW50cmFzIHF1ZSBlbiBlbCBlbmxhY2UgY29tcGxldG8gc2UgdG9tYSBsYSBtw6F4aW1hLiBFbiBlbCBwcm9tZWRpbyBkZSBncnVwbyBzZSB1c2EgbGEgbWVkaWEgZGUgdG9kYXMgbGFzIGRpc3RhbmNpYXMsIHkgZW4gZWwgbcOpdG9kbyBkZSBXYXJkIHNlIG1pbmltaXphIGxhIHZhcmlhbnphIGludGVybmEgZGUgbG9zIGNsw7pzdGVyZXMsIGJ1c2NhbmRvIG1pbmltaXphciBsYSBzdW1hIGRlIGN1YWRyYWRvcyBkZW50cm8gZGUgbG9zIGdydXBvcyAoJFNTX3tXfSQpLgoKRXN0ZSBtw6l0b2RvIHNlIHV0aWxpemEgYW1wbGlhbWVudGUgZW4gYmlvbG9nw61hIChmaWxvZ2VuaWEpLCBjaWVuY2lhcyBzb2NpYWxlcyB5IG1hcmtldGluZywgcGFyYSBkZXNjdWJyaXIgamVyYXJxdcOtYXMgbmF0dXJhbGVzIGVuIGxvcyBkYXRvcywgYW5hbGl6YXIgc2ltaWxpdHVkZXMgcHJvZ3Jlc2l2YXMgeSByZXByZXNlbnRhciB2aXN1YWxtZW50ZSBsYSBlc3RydWN0dXJhIGRlIGxvcyBncnVwb3MuIFN1IHZlbnRhamEgZXMgcXVlIG9mcmVjZSB1bmEgdmlzacOzbiBtw6FzIGZsZXhpYmxlIHkgZGV0YWxsYWRhIGRlIGPDs21vIHNlIHJlbGFjaW9uYW4gbGFzIG9ic2VydmFjaW9uZXMgZW4gZGlmZXJlbnRlcyBuaXZlbGVzIGRlIGFncnVwYWNpw7NuLgoKKipUaXBvcyBkZSBtb2RlbG9zKioKCkVsIGNsdXN0ZXJpbmcgamVyw6FycXVpY28gYWdsb21lcmF0aXZvIChIQUMpIGVzIGVsIG3DoXMgdXRpbGl6YWRvIHkgc2lndWUgdW4gZW5mb3F1ZSDigJxkZSBhYmFqbyBoYWNpYSBhcnJpYmHigJ0gKCRib3R0b21cdGV4dHstfXVwJCkuIENvbWllbnphIGNvbnNpZGVyYW5kbyBjYWRhIG9ic2VydmFjacOzbiBjb21vIHVuIGNsw7pzdGVyIGluZGl2aWR1YWwsIGVzIGRlY2lyLCBzaSBoYXkgJG4kIG9ic2VydmFjaW9uZXMsIHNlIHBhcnRlIGNvbiAkbiQgZ3J1cG9zLiBMdWVnbywgZW4gY2FkYSBwYXNvLCBzZSBmdXNpb25hbiBsb3MgZG9zIGNsw7pzdGVyZXMgbcOhcyBzaW1pbGFyZXMgc2Vnw7puIHVuYSBtZWRpZGEgZGUgZGlzdGFuY2lhIChwb3IgZWplbXBsbywgZGlzdGFuY2lhIGV1Y2xpZGlhbmEpIHkgdW4gY3JpdGVyaW8gZGUgZW5sYWNlICgkc2luZ2xlJCwgJGNvbXBsZXRlJCwgJGF2ZXJhZ2UkLCAkd2FyZCQsIGV0Yy4pLiBFbCBwcm9jZXNvIGNvbnRpbsO6YSBoYXN0YSBxdWUgdG9kb3MgbG9zIHB1bnRvcyBxdWVkYW4gZW4gdW4gw7puaWNvIGNsw7pzdGVyLgoKUG9yIG90cm8gbGFkbywgZWwgY2x1c3RlcmluZyBqZXLDoXJxdWljbyBkaXZpc2l2byAoRElBTkEgZW4gUikgdXRpbGl6YSB1biBlbmZvcXVlIOKAnGRlIGFycmliYSBoYWNpYSBhYmFqb+KAnSAoJHRvcFx0ZXh0ey19ZG93biQpLiBQYXJ0ZSBjb24gdG9kYXMgbGFzIG9ic2VydmFjaW9uZXMgZW4gdW4gw7puaWNvIGNsw7pzdGVyIHksIGVuIGNhZGEgcGFzbywgZGl2aWRlIGVsIGdydXBvIG3DoXMgaGV0ZXJvZ8OpbmVvIGVuIGRvcyBzdWJncnVwb3Mgc2Vnw7puIHVuYSBtZWRpZGEgZGUgZGlzaW1pbGl0dWQuIEVsIHByb2Nlc28gY29udGluw7phIGRlIGZvcm1hIHJlY3Vyc2l2YSBoYXN0YSBxdWUgY2FkYSBvYnNlcnZhY2nDs24gZXN0w6EgZW4gc3UgcHJvcGlvIGNsw7pzdGVyLCBvIGhhc3RhIGxsZWdhciBhIHVuIG7Dum1lcm8gZGUgY2zDunN0ZXJlcyBkZWZpbmlkby4KCkxhIGRpZmVyZW5jaWEgZXNlbmNpYWwgZXMgZW50b25jZXMgbGEgZGlyZWNjacOzbiBkZWwgYWdydXBhbWllbnRvOiBlbCBhZ2xvbWVyYXRpdm8gZnVzaW9uYSBjbMO6c3RlcmVzIHBlcXVlw7FvcyBoYXN0YSBmb3JtYXIgZ3J1cG9zIG3DoXMgZ3JhbmRlcywgbWllbnRyYXMgcXVlIGVsIGRpdmlzaXZvIHNlcGFyYSBjbMO6c3RlcmVzIGdyYW5kZXMgZW4gZ3J1cG9zIG3DoXMgcGVxdWXDsW9zLiBFbCBwcmltZXJvIHN1ZWxlIHNlciBtw6FzIGVmaWNpZW50ZSBlbiBsYSBwcsOhY3RpY2EsIHlhIHF1ZSBtYW5lamFyIGRpdmlzaW9uZXMgc3VjZXNpdmFzIHJlcXVpZXJlIG3DoXMgY8OzbXB1dG8uCgpFbiBjdWFudG8gYSBjYXNvcyBkZSB1c28sIGVsIG3DqXRvZG8gYWdsb21lcmF0aXZvIGVzIHByZWZlcmlkbyBjdWFuZG8gc2UgcXVpZXJlIGVudGVuZGVyIGPDs21vIHNlIGZvcm1hbiByZWxhY2lvbmVzIGVudHJlIG9ic2VydmFjaW9uZXMgaW5kaXZpZHVhbGVzIChlamVtcGxvOiBhbsOhbGlzaXMgZGUgZXNwZWNpZXMsIHNlZ21lbnRhY2nDs24gZGUgY2xpZW50ZXMpLiBFbCBkaXZpc2l2byBwdWVkZSBzZXIgw7p0aWwgY3VhbmRvIHNlIHNvc3BlY2hhIHF1ZSBleGlzdGUgdW5hIGVzdHJ1Y3R1cmEgZ3JhbmRlIGJpZW4gZGVmaW5pZGEgcXVlIGNvbnZpZW5lIHN1YmRpdmlkaXIgKGVqZW1wbG86IHBhcnRpciB1biBtZXJjYWRvIGNvbXBsZXRvIGVuIG5pY2hvcyBtw6FzIHBlcXVlw7FvcykuIEFtYm9zIHByb2R1Y2VuIHVuIGRlbmRyb2dyYW1hLCBwZXJvIGxhIGZvcm1hIGRlIGNvbnN0cnVjY2nDs24gZXMgZGlzdGludGEuCgoqKkRlbmRvZ3JhbWEqKgoKVW4gZGVuZHJvZ3JhbWEgZXMgdW5hIHJlcHJlc2VudGFjacOzbiBncsOhZmljYSBlbiBmb3JtYSBkZSDDoXJib2wgcXVlIHNlIHV0aWxpemEgcGFyYSBtb3N0cmFyIGxvcyByZXN1bHRhZG9zIGRlIHVuIGFuw6FsaXNpcyBkZSBjbMO6c3RlciBqZXLDoXJxdWljby4gRW4gZXN0ZSBncsOhZmljbywgY2FkYSBvYnNlcnZhY2nDs24gKHB1bnRvLCBpbmRpdmlkdW8sIHZhcmlhYmxlKSBhcGFyZWNlIGNvbW8gdW5hIOKAnGhvamHigJ0gZW4gbGEgYmFzZSBkZWwgw6FyYm9sLCB5IGxhcyB1bmlvbmVzIG8gcmFtYXMgcmVwcmVzZW50YW4gbG9zIHBhc29zIGRlIGFncnVwYW1pZW50byBvIGRpdmlzacOzbiBxdWUgc2UgcmVhbGl6YXJvbiBkdXJhbnRlIGVsIHByb2Nlc28gamVyw6FycXVpY28uCgpFbiBlbCBtw6l0b2RvIGFnbG9tZXJhdGl2bywgZWwgZGVuZHJvZ3JhbWEgc2UgY29uc3RydXllIGRlIGFiYWpvIGhhY2lhIGFycmliYTogYWwgaW5pY2lvLCBjYWRhIG9ic2VydmFjacOzbiBlc3TDoSBzZXBhcmFkYSwgeSBhIG1lZGlkYSBxdWUgc3ViaW1vcyBlbiBlbCBncsOhZmljbywgdmVtb3MgY8OzbW8gc2UgdmFuIHVuaWVuZG8gZW4gZ3J1cG9zIG3DoXMgZ3JhbmRlcy4gRW4gY2FtYmlvLCBlbiBlbCBtw6l0b2RvIGRpdmlzaXZvLCBzZSBsZWUgZGUgYXJyaWJhIGhhY2lhIGFiYWpvOiBjb21lbnphbW9zIGNvbiB1biDDum5pY28gZ3J1cG8geSBlbCDDoXJib2wgdmEgbW9zdHJhbmRvIGxhcyBkaXZpc2lvbmVzIHN1Y2VzaXZhcyBlbiByYW1hcyBtw6FzIHBlcXVlw7Fhcy4KCkVsIGVqZSB2ZXJ0aWNhbCBkZWwgZGVuZHJvZ3JhbWEgcmVwcmVzZW50YSBsYSBkaXN0YW5jaWEgbyBsYSBkaWZlcmVuY2lhIGVudHJlIGxvcyBjbMO6c3RlcmVzIHVuaWRvcyAoJGRfe2lqfSQpLiBDdWFudG8gbcOhcyBhbHRvIHNlIHByb2R1Y2UgbGEgdW5pw7NuIGRlIGRvcyByYW1hcywgbcOhcyBkaWZlcmVudGVzIGVyYW4gZXNvcyBjbMO6c3RlcmVzLiBFbCBlamUgaG9yaXpvbnRhbCBtdWVzdHJhIGxhcyBvYnNlcnZhY2lvbmVzIG8gZ3J1cG9zIHJlc3VsdGFudGVzLCBzaW4gdW4gb3JkZW4gZXNwZWNpYWwgbcOhcyBhbGzDoSBkZSBsYSB2aXN1YWxpemFjacOzbi4KCkVuIGxhIHByw6FjdGljYSwgZWwgZGVuZHJvZ3JhbWEgcGVybWl0ZSBhbCBhbmFsaXN0YSBkZWNpZGlyIGVsIG7Dum1lcm8gw7NwdGltbyBkZSBjbMO6c3RlcmVzOiBzZSBwdWVkZSDigJxjb3J0YXLigJ0gZWwgw6FyYm9sIGVuIHVuIG5pdmVsIGRlIGRpc3RhbmNpYSBkZXRlcm1pbmFkbyAodHJhemFuZG8gdW5hIGzDrW5lYSBob3Jpem9udGFsKSB5IHZlciBjdcOhbnRvcyBncnVwb3Mgc2UgZm9ybWFuLiBQb3IgZXNvIGVzIHVuYSBoZXJyYW1pZW50YSBjbGF2ZSBlbiBsYSBpbnRlcnByZXRhY2nDs24gZGVsIGFuw6FsaXNpcyBqZXLDoXJxdWljbywgcHVlcyBjb21iaW5hIGxhIGzDs2dpY2EgbWF0ZW3DoXRpY2EgY29uIHVuYSB2aXN1YWxpemFjacOzbiBpbnR1aXRpdmEuCgoqKkVqZW1wbG8gZW4gUioqCgpgYGB7cn0KIyBFamVtcGxvIG11eSBncsOhZmljbyBkZSBDbHVzdGVyaW5nIEplcsOhcnF1aWNvIGVuIFIKCiMgMS4gR2VuZXJhbW9zIGRhdG9zIGRlIGVqZW1wbG8gKGRvcyBjbMO6c3RlcmVzIGNsYXJvcykKc2V0LnNlZWQoMTIzKQpncnVwbzEgPC0gbWF0cml4KHJub3JtKDUwLCBtZWFuID0gMCksIG5jb2wgPSAyKQpncnVwbzIgPC0gbWF0cml4KHJub3JtKDUwLCBtZWFuID0gNSksIG5jb2wgPSAyKQpkYXRvcyA8LSByYmluZChncnVwbzEsIGdydXBvMikKY29sbmFtZXMoZGF0b3MpIDwtIGMoIlgiLCAiWSIpCgojIDIuIENhbGN1bGFyIGRpc3RhbmNpYXMgZXVjbGlkaWFuYXMKZGlzdGFuY2lhcyA8LSBkaXN0KGRhdG9zLCBtZXRob2QgPSAiZXVjbGlkZWFuIikKCiMgMy4gQXBsaWNhciBjbHVzdGVyaW5nIGplcsOhcnF1aWNvIGNvbiBtw6l0b2RvIGRlIFdhcmQKaGMgPC0gaGNsdXN0KGRpc3RhbmNpYXMsIG1ldGhvZCA9ICJ3YXJkLkQyIikKCiMgNC4gR3JhZmljYXIgZWwgZGVuZHJvZ3JhbWEKcGxvdChoYywgbGFiZWxzID0gRkFMU0UsIG1haW4gPSAiRGVuZHJvZ3JhbWEgLSBNw6l0b2RvIEplcsOhcnF1aWNvIChXYXJkKSIsCiAgICAgeGxhYiA9ICIiLCBzdWIgPSAiIiwgY2V4ID0gMC44KQoKIyA1LiBSZXNhbHRhciAyIGNsw7pzdGVyZXMgY29uIHJlY3TDoW5ndWxvcwpyZWN0LmhjbHVzdChoYywgayA9IDIsIGJvcmRlciA9ICJyZWQiKQoKIyA2LiBWaXN1YWxpemFyIGxvcyBwdW50b3Mgb3JpZ2luYWxlcyBjb2xvcmVhZG9zIHNlZ8O6biBjbMO6c3RlcgpncnVwb3MgPC0gY3V0cmVlKGhjLCBrID0gMikgIyBhc2lnbmFyIGNsw7pzdGVyZXMKcGxvdChkYXRvcywgY29sID0gZ3J1cG9zLCBwY2ggPSAxOSwKICAgICBtYWluID0gIkNsdXN0ZXJpbmcgSmVyw6FycXVpY28gLSBSZXByZXNlbnRhY2nDs24gZW4gZWwgcGxhbm8iKQoKYGBgCgojIyA4LjQpIE3DqXRvZG8gYWdsb21lcmF0aXZvCgpFbCBtw6l0b2RvIGFnbG9tZXJhdGl2byBkZSBjbHVzdGVyaXphY2nDs24gZXMgdW5hIHTDqWNuaWNhIGplcsOhcnF1aWNhIHF1ZSBjb25zdHJ1eWUgZ3J1cG9zIChjbHVzdGVycykgZGUgbWFuZXJhIGFzY2VuZGVudGUuIFNlIGluaWNpYSBjb25zaWRlcmFuZG8gY2FkYSBvYnNlcnZhY2nDs24gY29tbyB1biBjbMO6c3RlciBpbmRlcGVuZGllbnRlIHksIGVuIGNhZGEgcGFzbywgc2UgZnVzaW9uYW4gbG9zIGRvcyBjbMO6c3RlcmVzIG3DoXMgc2ltaWxhcmVzIGhhc3RhIHF1ZSB0b2RvcyBsb3MgZWxlbWVudG9zIGZvcm1hbiB1biDDum5pY28gZ3J1cG8gbyBoYXN0YSBjdW1wbGlyIHVuIGNyaXRlcmlvIGRlIHBhcmFkYS4gTGEgc2ltaWxpdHVkIG8gZGlzdGFuY2lhIGVudHJlIGNsw7pzdGVyZXMgcHVlZGUgbWVkaXJzZSBjb24gZGlmZXJlbnRlcyBtw6l0cmljYXMsIGNvbW8gbGEgZGlzdGFuY2lhIGV1Y2xpZGlhbmEgJGQoeF9pLCB4X2opJCBvIGxhIGNvcnJlbGFjacOzbi4KCkVzdGUgbcOpdG9kbyBzaXJ2ZSBwYXJhIGRlc2N1YnJpciBlc3RydWN0dXJhcyBqZXLDoXJxdWljYXMgZW4gbG9zIGRhdG9zIHkgdmlzdWFsaXphciBsYXMgcmVsYWNpb25lcyBlbnRyZSBvYnNlcnZhY2lvbmVzIG1lZGlhbnRlIHVuIGRlbmRyb2dyYW1hLiBBIGRpZmVyZW5jaWEgZGUgbcOpdG9kb3MgY29tbyAkayQtbWVhbnMsIG5vIHJlcXVpZXJlIGVzcGVjaWZpY2FyIGRlIGFudGVtYW5vIGVsIG7Dum1lcm8gZGUgY2zDunN0ZXJlcywgeWEgcXVlIGxhIGplcmFycXXDrWEgY29tcGxldGEgcXVlZGEgcmVwcmVzZW50YWRhIHkgZWwgYW5hbGlzdGEgcHVlZGUgZGVjaWRpciBlbCBuaXZlbCBkZSBjb3J0ZS4gU3Ugb2JqZXRpdm8gZXMgb3JnYW5pemFyIGxvcyBkYXRvcyBkZSBmb3JtYSBxdWUgbG9zIGVsZW1lbnRvcyBtw6FzIHBhcmVjaWRvcyBlc3TDqW4gZW4gZWwgbWlzbW8gY2zDunN0ZXIuCgpFbiBsYSBwcsOhY3RpY2EsIHNlIHV0aWxpemEgZW4gY2Fzb3MgZG9uZGUgaW50ZXJlc2EgZW50ZW5kZXIgbGEgcmVsYWNpw7NuIGplcsOhcnF1aWNhIGVudHJlIGdydXBvcywgY29tbyBlbiBiaW9sb2fDrWEgcGFyYSBjb25zdHJ1aXIgw6FyYm9sZXMgZmlsb2dlbsOpdGljb3MsIGVuIGFuw6FsaXNpcyBkZSB0ZXh0b3MgcGFyYSBhZ3J1cGFyIGRvY3VtZW50b3Mgc2ltaWxhcmVzLCBlbiBtYXJrZXRpbmcgcGFyYSBzZWdtZW50YXIgY2xpZW50ZXMgbyBlbiBwc2ljb2xvZ8OtYSBwYXJhIGNsYXNpZmljYXIgcmVzcHVlc3RhcyBkZSBlbmN1ZXN0YXMuIEVsIG3DqXRvZG8gZXMgZXNwZWNpYWxtZW50ZSDDunRpbCBjdWFuZG8gZWwgbsO6bWVybyBkZSBjbMO6c3RlcmVzIGVzIGluY2llcnRvIHkgc2UgcmVxdWllcmUgdW5hIGVzdHJ1Y3R1cmEgZmxleGlibGUuCgpGb3JtYWxtZW50ZSwgZW4gY2FkYSBwYXNvIHNlIGRlZmluZSBsYSBkaXN0YW5jaWEgZW50cmUgY2zDunN0ZXJlcyAkQ19hJCB5ICRDX2IkIGRlIHZhcmlhcyBmb3JtYXM6IGVuIHNpbmdsZSBsaW5rYWdlIHNlIHVzYSAkXG1pbiB7IGQoeF9pLCB4X2opOiB4X2kgXGluIENfYSwgeF9qIFxpbiBDX2IgfSQ7IGVuIGNvbXBsZXRlIGxpbmthZ2UsIGVsIG3DoXhpbW87IHkgZW4gYXZlcmFnZSBsaW5rYWdlLCBlbCBwcm9tZWRpby4gRXN0YXMgZGVmaW5pY2lvbmVzIGRldGVybWluYW4gY8OzbW8gc2UgZnVzaW9uYW4gbG9zIGNsw7pzdGVyZXMgeSBwcm9kdWNlbiBkaXN0aW50YXMgZXN0cnVjdHVyYXMgamVyw6FycXVpY2FzIGVuIGxvcyBkYXRvcy4KCioqRWplbXBsbyBjb24gUioqCgpgYGB7cn0KIyBFamVtcGxvIGVuIFI6IE3DqXRvZG8gYWdsb21lcmF0aXZvIGRlIGNsdXN0ZXJpemFjacOzbgoKIyAxLiBTaW11bGFtb3MgZGF0b3MKc2V0LnNlZWQoMTIzKQp4IDwtIG1hdHJpeChybm9ybSgzMCksIG5jb2wgPSAyKSAgIyAxNSBwdW50b3MgZW4gMkQKcm93bmFtZXMoeCkgPC0gcGFzdGUwKCJQIiwgMTpucm93KHgpKQoKIyAyLiBDYWxjdWxhbW9zIGxhIG1hdHJpeiBkZSBkaXN0YW5jaWFzCmRpc3RfbWF0cml4IDwtIGRpc3QoeCwgbWV0aG9kID0gImV1Y2xpZGVhbiIpCgojIDMuIEFwbGljYW1vcyBlbCBjbHVzdGVyaW5nIGplcsOhcnF1aWNvIGFnbG9tZXJhdGl2bwpoYyA8LSBoY2x1c3QoZGlzdF9tYXRyaXgsIG1ldGhvZCA9ICJjb21wbGV0ZSIpCgojIDQuIEdyYWZpY2Ftb3MgZWwgZGVuZHJvZ3JhbWEKcGxvdChoYywgbWFpbiA9ICJEZW5kcm9ncmFtYSAtIENsdXN0ZXJpbmcgYWdsb21lcmF0aXZvIiwKICAgICB4bGFiID0gIk9ic2VydmFjaW9uZXMiLCB5bGFiID0gIkRpc3RhbmNpYSIpCgojIDUuIChPcGNpb25hbCkgQ29ydGFtb3MgZWwgZGVuZHJvZ3JhbWEgZW4gMyBjbMO6c3RlcmVzCnJlY3QuaGNsdXN0KGhjLCBrID0gMywgYm9yZGVyID0gInJlZCIpCgpgYGAKCiMjIDguNSkgRGVuZG9ncmFtYQoKVW4gZGVuZHJvZ3JhbWEgZXMgdW5hIHJlcHJlc2VudGFjacOzbiBncsOhZmljYSBlbiBmb3JtYSBkZSDDoXJib2wgcXVlIG11ZXN0cmEgY8OzbW8gc2UgdmFuIGZ1c2lvbmFuZG8gbG9zIGNsw7pzdGVyZXMgZW4gdW4gcHJvY2VzbyBkZSBjbHVzdGVyaW5nIGplcsOhcnF1aWNvLiBDYWRhIGhvamEgZGVsIMOhcmJvbCBjb3JyZXNwb25kZSBhIHVuYSBvYnNlcnZhY2nDs24gaW5kaXZpZHVhbCwgeSBsYXMgcmFtYXMgaW5kaWNhbiBsYXMgdW5pb25lcyBzdWNlc2l2YXMgZGUgZ3J1cG9zLiBMYSBhbHR1cmEgYSBsYSBxdWUgc2UgdW5lbiBkb3MgcmFtYXMgcmVwcmVzZW50YSBsYSBkaXN0YW5jaWEgbyBkaWZlcmVuY2lhIGVudHJlIGVzb3MgY2zDunN0ZXJlcywgbWVkaWRhIG1lZGlhbnRlIHVuYSBtw6l0cmljYSBjb21vIGxhIGRpc3RhbmNpYSBldWNsaWRpYW5hICRkKHhfaSwgeF9qKSQuCgpFbCBkZW5kcm9ncmFtYSBzaXJ2ZSBwcmluY2lwYWxtZW50ZSBwYXJhIHZpc3VhbGl6YXIgbGEgZXN0cnVjdHVyYSBqZXLDoXJxdWljYSBkZSBsb3MgZGF0b3MsIGZhY2lsaXRhbmRvIGxhIGlkZW50aWZpY2FjacOzbiBkZSBwYXRyb25lcyB5IHJlbGFjaW9uZXMuIEdyYWNpYXMgYSDDqWwsIHVuIGFuYWxpc3RhIHB1ZWRlIGRlY2lkaXIgZMOzbmRlIOKAnGNvcnRhcuKAnSBlbCDDoXJib2wgcGFyYSBvYnRlbmVyIHVuIG7Dum1lcm8gZGV0ZXJtaW5hZG8gZGUgY2zDunN0ZXJlcy4gUG9yIGVqZW1wbG8sIGFsIHRyYXphciB1bmEgbMOtbmVhIGhvcml6b250YWwgZW4gZWwgbml2ZWwgZGUgZGlzdGFuY2lhICRcZGVsdGEkLCB0b2RvcyBsb3MgZ3J1cG9zIGZvcm1hZG9zIHBvciBkZWJham8gZGUgZXNhIGFsdHVyYSBzZSBjb25zaWRlcmFuIGNsw7pzdGVyZXMgZGlzdGludG9zLgoKRW4gY3VhbnRvIGEgc3VzIGFwbGljYWNpb25lcywgZWwgZGVuZHJvZ3JhbWEgZXMgYW1wbGlhbWVudGUgdXRpbGl6YWRvIGVuIGJpb2xvZ8OtYSBwYXJhIHJlcHJlc2VudGFyIMOhcmJvbGVzIGZpbG9nZW7DqXRpY29zLCBlbiBtYXJrZXRpbmcgcGFyYSBzZWdtZW50YXIgY2xpZW50ZXMsIGVuIGxpbmfDvMOtc3RpY2EgcGFyYSBtb3N0cmFyIHNpbWlsaXR1ZGVzIGVudHJlIGlkaW9tYXMsIHkgZW4gcHNpY29sb2fDrWEgbyBzb2Npb2xvZ8OtYSBwYXJhIGNsYXNpZmljYXIgcGVyZmlsZXMgZGUgZW5jdWVzdGFkb3MuIFN1IGNhcGFjaWRhZCBkZSBtb3N0cmFyIHJlbGFjaW9uZXMgamVyw6FycXVpY2FzIGhhY2UgcXVlIHNlYSB1bmEgaGVycmFtaWVudGEgdmFsaW9zYSBlbiBjdWFscXVpZXIgZGlzY2lwbGluYSBxdWUgcmVxdWllcmEgYWdydXBhY2nDs24gZXhwbG9yYXRvcmlhLgoKRm9ybWFsbWVudGUsIHNpICRDX2EkIHkgJENfYiQgc29uIGRvcyBjbMO6c3RlcmVzLCBsYSBhbHR1cmEgZGVsIG5vZG8gcXVlIGxvcyB1bmUgcmVmbGVqYSBsYSBkaXN0YW5jaWEgJGQoQ19hLCBDX2IpJCBzZWfDum4gZWwgY3JpdGVyaW8gZWxlZ2lkbzogc2luZ2xlIGxpbmthZ2UgdXNhICRcbWluIHsgZCh4X2ksIHhfaik6IHhfaSBcaW4gQ19hLCB4X2ogXGluIENfYiB9JCwgbWllbnRyYXMgcXVlIGNvbXBsZXRlIGxpbmthZ2UgZW1wbGVhIGVsIG3DoXhpbW8sIHkgYXZlcmFnZSBsaW5rYWdlIHV0aWxpemEgZWwgcHJvbWVkaW8uIEFzw60sIGVsIGRlbmRyb2dyYW1hIGVuY2Fwc3VsYSBlbiB1biBzb2xvIGdyw6FmaWNvIHRvZG8gZWwgcHJvY2VzbyBkZSBmb3JtYWNpw7NuIGRlIGNsw7pzdGVyZXMuCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CiMgRWplbXBsbyBncsOhZmljbyBkZSBkZW5kcm9ncmFtYSBlbiBSCgojIDEuIENyZWFtb3MgZGF0b3Mgc2ltdWxhZG9zIChwdW50b3MgZW4gMkQpCnNldC5zZWVkKDQyKQp4IDwtIG1hdHJpeChybm9ybSgyMCksIG5jb2wgPSAyKQpyb3duYW1lcyh4KSA8LSBwYXN0ZTAoIk9icyIsIDE6bnJvdyh4KSkKCiMgMi4gQ2FsY3VsYW1vcyBtYXRyaXogZGUgZGlzdGFuY2lhcyAoZXVjbGlkaWFuYSkKZGlzdF9tYXRyaXggPC0gZGlzdCh4LCBtZXRob2QgPSAiZXVjbGlkZWFuIikKCiMgMy4gQXBsaWNhbW9zIGNsdXN0ZXJpbmcgamVyw6FycXVpY28gYWdsb21lcmF0aXZvCmhjIDwtIGhjbHVzdChkaXN0X21hdHJpeCwgbWV0aG9kID0gImF2ZXJhZ2UiKQoKIyA0LiBHcmFmaWNhbW9zIGVsIGRlbmRyb2dyYW1hCnBsb3QoaGMsIG1haW4gPSAiRGVuZHJvZ3JhbWEgZGUgQ2x1c3RlcmluZyBKZXLDoXJxdWljbyIsCiAgICAgeGxhYiA9ICJPYnNlcnZhY2lvbmVzIiwgeWxhYiA9ICJEaXN0YW5jaWEiKQoKIyA1LiBPcGNpb25hbDogY29ydGFyIGVuIDMgY2zDunN0ZXJlcyB5IHJlc2FsdGFybG9zCnJlY3QuaGNsdXN0KGhjLCBrID0gMywgYm9yZGVyID0gImJsdWUiKQoKYGBgCgojIyA4LjYpIE1ldG9kbyBkaXZpc2l2bwoKRWwgbcOpdG9kbyBkaXZpc2l2byBkZSBjbHVzdGVyaXphY2nDs24gZXMgdW5hIHTDqWNuaWNhIGplcsOhcnF1aWNhIHF1ZSBmdW5jaW9uYSBkZSBtYW5lcmEgb3B1ZXN0YSBhbCBtw6l0b2RvIGFnbG9tZXJhdGl2by4gRW4gbHVnYXIgZGUgZW1wZXphciBjb24gY2FkYSBvYnNlcnZhY2nDs24gY29tbyB1biBjbMO6c3RlciBpbmRlcGVuZGllbnRlLCBjb21pZW56YSBjb24gdW4gw7puaWNvIGNsw7pzdGVyIHF1ZSBjb250aWVuZSBhIHRvZGFzIGxhcyBvYnNlcnZhY2lvbmVzLCB5IGVuIGNhZGEgcGFzbyBsbyB2YSBkaXZpZGllbmRvIGVuIHN1YmNvbmp1bnRvcyBtw6FzIHBlcXVlw7FvcyBoYXN0YSBsbGVnYXIgYSBjbMO6c3RlcmVzIGluZGl2aWR1YWxlcyBvIGhhc3RhIGN1bXBsaXIgdW4gY3JpdGVyaW8gZGUgcGFyYWRhLiBFcyB1biBlbmZvcXVlIHRvcC1kb3duLCBtaWVudHJhcyBxdWUgZWwgYWdsb21lcmF0aXZvIGVzIGJvdHRvbS11cC4KCkVzdGUgbcOpdG9kbyBzaXJ2ZSBwYXJhIGRlc2N1YnJpciBlc3RydWN0dXJhcyBqZXLDoXJxdWljYXMgZW4gbG9zIGRhdG9zIGN1YW5kbyBzZSBzdXBvbmUgcXVlIGV4aXN0ZSB1biBncmFuIGdydXBvIGluaWNpYWwgcXVlIHB1ZWRlIGRlc2NvbXBvbmVyc2UgZW4gc3ViZ3J1cG9zIG3DoXMgaG9tb2fDqW5lb3MuIEEgbml2ZWwgZm9ybWFsLCBzZSBzZWxlY2Npb25hIGVuIGNhZGEgaXRlcmFjacOzbiBsYSBwYXJ0aWNpw7NuIHF1ZSBtYXhpbWl6YSBsYSBkaXNpbWlsaXR1ZCBlbnRyZSBsb3MgbnVldm9zIGdydXBvcy4gTGEgbWVkaWRhIGRlIGRpc2ltaWxpdHVkIHN1ZWxlIGVzdGFyIGJhc2FkYSBlbiBkaXN0YW5jaWFzIGNvbW8gbGEgZXVjbGlkaWFuYSAkZCh4X2ksIHhfaikkIG8gZW4gw61uZGljZXMgZGUgY2FsaWRhZCBkZSBjbMO6c3RlciBjb21vIGxhIHZhcmlhbnphIGludHJhLWdydXBvLgoKTG9zIGNhc29zIGRlIHVzbyBtw6FzIGNvbXVuZXMgaW5jbHV5ZW4gY2xhc2lmaWNhY2nDs24gZGUgZG9jdW1lbnRvcyAoZG9uZGUgdW5hIGNvbGVjY2nDs24gY29tcGxldGEgc2UgdmEgZGl2aWRpZW5kbyBlbiB0ZW1hcyBtw6FzIGVzcGVjw61maWNvcyksIHNlZ21lbnRhY2nDs24gZGUgbWVyY2Fkb3MgKHVuIG1lcmNhZG8gZ2xvYmFsIHNlIHN1YmRpdmlkZSBlbiBuaWNob3MgbcOhcyBob21vZ8OpbmVvcyksIGJpb2xvZ8OtYSAoZGl2aXNpw7NuIGRlIGVzcGVjaWVzIGVuIHN1YmNhdGVnb3LDrWFzKSB5IGFuw6FsaXNpcyBzb2NpYWwgKHNlcGFyYWNpw7NuIHByb2dyZXNpdmEgZGUgcG9ibGFjaW9uZXMgc2Vnw7puIHZhcmlhYmxlcyBkZW1vZ3LDoWZpY2FzKS4gRXMgw7p0aWwgY3VhbmRvIGludGVyZXNhIHVuYSB2aXNpw7NuIGdsb2JhbCBpbmljaWFsIHkgbHVlZ28gdW4gcmVmaW5hbWllbnRvIHByb2dyZXNpdm8uCgpVbmEgcmVwcmVzZW50YWNpw7NuIHTDrXBpY2EgZGUgZXN0ZSBtw6l0b2RvIHRhbWJpw6luIHNlIG11ZXN0cmEgY29uIHVuIGRlbmRyb2dyYW1hLCBwZXJvIGludmVydGlkbyBlbiBlbCBzZW50aWRvIGNvbmNlcHR1YWw6IHNlIHBhcnRlIGRlIHVuIG5vZG8gcmHDrXogcXVlIHJlcHJlc2VudGEgZWwgY2zDunN0ZXIgdG90YWwgJEMkLCB5IHNlIGRpdmlkZSBzdWNlc2l2YW1lbnRlIGVuIHN1YmNsw7pzdGVyZXMgJENfYSwgQ19iLCBcZG90cyQuIExhIGFsdHVyYSBkZSBsYSBkaXZpc2nDs24gcmVmbGVqYSBsYSBkaXN0YW5jaWEgJGQoQ19hLCBDX2IpJCBvIGVsIGNyaXRlcmlvIGRlIGRpc2ltaWxpdHVkIGVtcGxlYWRvLiBBc8OtLCBlbCBtw6l0b2RvIGRpdmlzaXZvIHBlcm1pdGUgZGVzY29tcG9uZXIgamVyw6FycXVpY2FtZW50ZSB1biBjb25qdW50byBncmFuZGUgZW4gY29tcG9uZW50ZXMgbcOhcyBob21vZ8OpbmVvcy4KCioqRWplbXBsbyBjb24gUjoqKgoKYGBge3J9CiMgRWplbXBsbyBncsOhZmljbyBkZWwgbcOpdG9kbyBkaXZpc2l2byBlbiBSCgoKCiMgMi4gRGF0b3Mgc2ltdWxhZG9zCnNldC5zZWVkKDEyMykKeCA8LSBtYXRyaXgocm5vcm0oMjApLCBuY29sID0gMikKcm93bmFtZXMoeCkgPC0gcGFzdGUwKCJPYnMiLCAxOm5yb3coeCkpCgojIDMuIEFwbGljYXIgY2x1c3RlcmluZyBkaXZpc2l2byAoRElBTkEpCmRpdmlzaXZvIDwtIGRpYW5hKHgpCgojIDQuIEdyYWZpY2FyIGVsIGRlbmRyb2dyYW1hCnBsb3QoZGl2aXNpdm8sIG1haW4gPSAiRGVuZHJvZ3JhbWEgLSBNw6l0b2RvIGRpdmlzaXZvIChESUFOQSkiKQoKIyA1LiAoT3BjaW9uYWwpIENvcnRhciBlbiAzIGNsw7pzdGVyZXMKY2x1c3RlcnMgPC0gY3V0cmVlKGFzLmhjbHVzdChkaXZpc2l2byksIGsgPSAzKQoKIyA2LiBHcmFmaWNhciBsb3MgcHVudG9zIGNvbG9yZWFkb3MgcG9yIGNsw7pzdGVyCnBsb3QoeCwgY29sID0gY2x1c3RlcnMsIHBjaCA9IDE5LAogICAgIG1haW4gPSAiRGF0b3MgYWdydXBhZG9zIGNvbiBtw6l0b2RvIGRpdmlzaXZvIikKCmBgYAoKIyMgOC43KSBDbMO6c3RlciBiYXNhZG8gZW4gbW9kZWxvcwoKRWwgY2zDunN0ZXIgYmFzYWRvIGVuIG1vZGVsb3MgZXMgdW4gZW5mb3F1ZSBlbiBlbCBxdWUgc2UgYXN1bWUgcXVlIGxvcyBkYXRvcyBwcm92aWVuZW4gZGUgdW5hIGNvbWJpbmFjacOzbiBkZSBkaXN0cmlidWNpb25lcyBlc3RhZMOtc3RpY2FzIHN1YnlhY2VudGVzLCB5IGNhZGEgY2zDunN0ZXIgY29ycmVzcG9uZGUgYSB1bmEgZGUgZXNhcyBkaXN0cmlidWNpb25lcy4gRWwgb2JqZXRpdm8gZXMgZXN0aW1hciBsb3MgcGFyw6FtZXRyb3MgZGUgZXNhcyBkaXN0cmlidWNpb25lcyB5IGFzaWduYXIgY2FkYSBvYnNlcnZhY2nDs24gYSBsYSBkaXN0cmlidWNpw7NuICh5IHBvciBlbmRlIGFsIGNsw7pzdGVyKSBjb24gbGEgbWF5b3IgcHJvYmFiaWxpZGFkLiBFbCBjYXNvIG3DoXMgY29tw7puIGVzIGVsIG1vZGVsbyBkZSBtZXpjbGFzIGdhdXNzaWFuYXMgKEdhdXNzaWFuIE1peHR1cmUgTW9kZWxzLCBHTU0pLCBkb25kZSBjYWRhIGNsw7pzdGVyIHNlIG1vZGVsYSBjb21vIHVuYSBkaXN0cmlidWNpw7NuIG5vcm1hbCBtdWx0aXZhcmlhZGEuCgpFc3RlIG3DqXRvZG8gc2lydmUgcGFyYSBjYXB0dXJhciBlc3RydWN0dXJhcyBkZSBkYXRvcyBtw6FzIGZsZXhpYmxlcyBxdWUgbGFzIHF1ZSBwZXJtaXRlbiBtw6l0b2RvcyBjb21vICRrJC1tZWFucywgeWEgcXVlIG5vIG9ibGlnYSBhIHF1ZSBsb3MgY2zDunN0ZXJlcyBzZWFuIGVzZsOpcmljb3MgbmkgZGUgaWd1YWwgdGFtYcOxby4gQ2FkYSBjbMO6c3RlciB0aWVuZSBzdSBwcm9waWEgbWVkaWEgJFxtdV9rJCB5IG1hdHJpeiBkZSBjb3ZhcmlhbnphICRcU2lnbWFfayQsIGxvIHF1ZSBwZXJtaXRlIG1vZGVsYXIgZm9ybWFzIGVsw61wdGljYXMgeSByZWxhY2lvbmVzIGVudHJlIHZhcmlhYmxlcy4gTGEgZXN0aW1hY2nDs24gZGUgcGFyw6FtZXRyb3Mgc3VlbGUgaGFjZXJzZSBjb24gZWwgYWxnb3JpdG1vIEV4cGVjdGF0aW9uLU1heGltaXphdGlvbiAoRU0pLCBxdWUgbWF4aW1pemEgbGEgdmVyb3NpbWlsaXR1ZCBkZSBsb3MgZGF0b3MgYmFqbyBsYSBtZXpjbGEgZGUgZGlzdHJpYnVjaW9uZXMuCgpMb3MgY2Fzb3MgZGUgdXNvIHNvbiBhbXBsaW9zOiBlbiBiaW9sb2fDrWEgcGFyYSBpZGVudGlmaWNhciBzdWJwb2JsYWNpb25lcyBlbiBkYXRvcyBnZW7Ds21pY29zLCBlbiB2aXNpw7NuIHBvciBjb21wdXRhZG9yIHBhcmEgc2VnbWVudGFjacOzbiBkZSBpbcOhZ2VuZXMsIGVuIGZpbmFuemFzIHBhcmEgY2xhc2lmaWNhciBwZXJmaWxlcyBkZSByaWVzZ28gZGUgY2xpZW50ZXMsIHkgZW4gcHJvY2VzYW1pZW50byBkZSBsZW5ndWFqZSBuYXR1cmFsIHBhcmEgZGV0ZWN0YXIgdMOzcGljb3MgbGF0ZW50ZXMuIFN1IHZlbnRhamEgZXMgcXVlIGFkZW3DoXMgZGUgc2VnbWVudGFyLCBwcm9wb3JjaW9uYSB1bmEgaW50ZXJwcmV0YWNpw7NuIHByb2JhYmlsw61zdGljYSBkZSBsYSBwZXJ0ZW5lbmNpYSBkZSBjYWRhIG9ic2VydmFjacOzbiBhIGxvcyBjbMO6c3RlcmVzLgoKRm9ybWFsbWVudGUsIHNlIHN1cG9uZSBxdWUgbG9zIGRhdG9zIHByb3ZpZW5lbiBkZSB1bmEgbWV6Y2xhOgoKJCRwKHgpID0gXHN1bV97az0xfV5LIFxwaV9rIFwsIGYoeCBcbWlkIFx0aGV0YV9rKSQkCgpkb25kZSAkXHBpX2skIHNvbiBsb3MgcGVzb3MgZGUgbWV6Y2xhIGNvbiAkXHN1bV97az0xfV5LIFxwaV9rID0gMSQsICRmKHggXG1pZCBcdGhldGFfaykkIGVzIGxhIGRpc3RyaWJ1Y2nDs24gZGVsIGNsw7pzdGVyICRrJCBjb24gcGFyw6FtZXRyb3MgJFx0aGV0YV9rJCwgeSAkSyQgZXMgZWwgbsO6bWVybyBkZSBjbMO6c3RlcmVzLiBFbiBlbCBjYXNvIGRlIG1lemNsYXMgZ2F1c3NpYW5hcywgJGYoeCBcbWlkIFx0aGV0YV9rKSQgY29ycmVzcG9uZGUgYSBsYSBkZW5zaWRhZCBub3JtYWwgbXVsdGl2YXJpYWRhIGNvbiBtZWRpYSAkXG11X2skIHkgY292YXJpYW56YSAkXFNpZ21hX2skLgoKKipFamVtcGxvciBlbiBSKioKCmBgYHtyfQojIEVqZW1wbG8gZ3LDoWZpY286IENsdXN0ZXJpbmcgYmFzYWRvIGVuIG1vZGVsb3MgKEdhdXNzaWFuIE1peHR1cmUgTW9kZWxzKQoKCgojIDIuIEdlbmVyYW1vcyBkYXRvcyBzaW11bGFkb3MgY29uIDMgY2zDunN0ZXJlcyBlbMOtcHRpY29zCnNldC5zZWVkKDEyMykKeCA8LSBjYmluZChjKHJub3JtKDUwLCAwLCAxKSwgcm5vcm0oNTAsIDUsIDEpLCBybm9ybSg1MCwgMywgMC41KSksCiAgICAgICAgICAgYyhybm9ybSg1MCwgMCwgMSksIHJub3JtKDUwLCAwLCAxKSwgcm5vcm0oNTAsIDUsIDEpKSkKCiMgMy4gQWp1c3RhbW9zIGVsIG1vZGVsbyBkZSBtZXpjbGEgZ2F1c3NpYW5hCm1vZGVsbyA8LSBNY2x1c3QoeCkKCiMgNC4gUmVzdW1lbiBkZWwgbW9kZWxvCnN1bW1hcnkobW9kZWxvKQoKIyA1LiBHcsOhZmljbyBkZSBjbGFzaWZpY2FjacOzbiBwb3IgY2zDunN0ZXJlcwpwbG90KG1vZGVsbywgd2hhdCA9ICJjbGFzc2lmaWNhdGlvbiIpCgojIDYuIEdyw6FmaWNvIGRlIGRlbnNpZGFkIGVzdGltYWRhCnBsb3QobW9kZWxvLCB3aGF0ID0gImRlbnNpdHkiKQoKYGBgCgojIyA4LjgpIEVqZW1wbG8gY29uIFB5dGhvbgoKKipMaWJyZXJpYXMgbmVjZXNhcmlhcyoqCgoKCiMjIyA4LjguMSkgSy1tZWRpYXMKCmBgYHtweXRob259CiMgU2VtaWxsYQpucC5yYW5kb20uc2VlZCgxMjMpCgojIERhdG9zIHNpbXVsYWRvcyAoMyBncnVwb3MgZW4gMkQpClggPSBucC52c3RhY2soWwogICAgbnAucmFuZG9tLm5vcm1hbChsb2M9MCwgc2NhbGU9MC4zLCBzaXplPSg1MCwgMikpLAogICAgbnAucmFuZG9tLm5vcm1hbChsb2M9Miwgc2NhbGU9MC4zLCBzaXplPSg1MCwgMikpLAogICAgbnAucmFuZG9tLm5vcm1hbChsb2M9LTIsIHNjYWxlPTAuMywgc2l6ZT0oNTAsIDIpKQpdKQoKIyBBcGxpY2FyIEstbWVhbnMKa21lYW5zID0gS01lYW5zKG5fY2x1c3RlcnM9Mywgbl9pbml0PTIwLCByYW5kb21fc3RhdGU9MTIzKQpsYWJlbHNfa21lYW5zID0ga21lYW5zLmZpdF9wcmVkaWN0KFgpCgojIEdyYWZpY2FyCnBsdC5zY2F0dGVyKFhbOiwwXSwgWFs6LDFdLCBjPWxhYmVsc19rbWVhbnMsIGNtYXA9InZpcmlkaXMiLCBzPTUwKQpwbHQuc2NhdHRlcihrbWVhbnMuY2x1c3Rlcl9jZW50ZXJzX1s6LDBdLCBrbWVhbnMuY2x1c3Rlcl9jZW50ZXJzX1s6LDFdLCAKICAgICAgICAgICAgYz0icmVkIiwgbWFya2VyPSJYIiwgcz0yMDAsIGxhYmVsPSJDZW50cm9pZGVzIikKcGx0LnRpdGxlKCJFamVtcGxvIGdyw6FmaWNvIGRlIEstbWVkaWFzIikKcGx0LmxlZ2VuZCgpCnBsdC5zaG93KCkKCmBgYAoKIyMjIDguOC4yKSBLLW1lZG9pZGVzIChQQU0pCgpgYGB7cHl0aG9ufQpYMiA9IG5wLnZzdGFjayhbCiAgICBucC5yYW5kb20ubm9ybWFsKGxvYz0wLCBzY2FsZT0wLjUsIHNpemU9KDUwLCAyKSksCiAgICBucC5yYW5kb20ubm9ybWFsKGxvYz0zLCBzY2FsZT0wLjUsIHNpemU9KDUwLCAyKSksCiAgICBucC5yYW5kb20ubm9ybWFsKGxvYz02LCBzY2FsZT0wLjUsIHNpemU9KDUwLCAyKSkKXSkKCmttZWRvaWRzID0gS01lZG9pZHMobl9jbHVzdGVycz0zLCByYW5kb21fc3RhdGU9MTIzLCBtZXRyaWM9ImV1Y2xpZGVhbiIpCmxhYmVsc19rbWVkb2lkcyA9IGttZWRvaWRzLmZpdF9wcmVkaWN0KFgyKQoKcGx0LnNjYXR0ZXIoWDJbOiwwXSwgWDJbOiwxXSwgYz1sYWJlbHNfa21lZG9pZHMsIGNtYXA9InBsYXNtYSIsIHM9NTApCnBsdC5zY2F0dGVyKFgyW2ttZWRvaWRzLm1lZG9pZF9pbmRpY2VzXywwXSwgWDJba21lZG9pZHMubWVkb2lkX2luZGljZXNfLDFdLCAKICAgICAgICAgICAgYz0iYmxhY2siLCBtYXJrZXI9IkQiLCBzPTIwMCwgbGFiZWw9Ik1lZG9pZGVzIikKcGx0LnRpdGxlKCJDbHVzdGVyaW5nIGNvbiBLLW1lZG9pZGVzIChQQU0pIikKcGx0LmxlZ2VuZCgpCnBsdC5zaG93KCkKCgpgYGAKCiMjIyA4LjguMykgTcOpdG9kbyBqZXLDoXJxdWljbyBhZ2xvbWVyYXRpdm8KCmBgYHtweXRob259ClgzID0gbnAudnN0YWNrKFsKICAgIG5wLnJhbmRvbS5ub3JtYWwobG9jPTAsIHNjYWxlPTEsIHNpemU9KDI1LCAyKSksCiAgICBucC5yYW5kb20ubm9ybWFsKGxvYz01LCBzY2FsZT0xLCBzaXplPSgyNSwgMikpCl0pCgojIE1hdHJpeiBkZSBkaXN0YW5jaWFzICsgY2x1c3RlcmluZwpaID0gbGlua2FnZShYMywgbWV0aG9kPSJ3YXJkIikKCiMgRGVuZHJvZ3JhbWEKcGx0LmZpZ3VyZShmaWdzaXplPSg4LDUpKQpkZW5kcm9ncmFtKFopCnBsdC50aXRsZSgiRGVuZHJvZ3JhbWEgLSBNw6l0b2RvIEplcsOhcnF1aWNvIChXYXJkKSIpCnBsdC54bGFiZWwoIk9ic2VydmFjaW9uZXMiKQpwbHQueWxhYmVsKCJEaXN0YW5jaWEiKQpwbHQuc2hvdygpCgojIENvcnRhciBlbiAyIGNsw7pzdGVyZXMKbGFiZWxzX2hjID0gZmNsdXN0ZXIoWiwgdD0yLCBjcml0ZXJpb249Im1heGNsdXN0IikKCiMgVmlzdWFsaXphciBjbHVzdGVycyBlbiBlbCBwbGFubwpwbHQuc2NhdHRlcihYM1s6LDBdLCBYM1s6LDFdLCBjPWxhYmVsc19oYywgY21hcD0iQWNjZW50Iiwgcz01MCkKcGx0LnRpdGxlKCJDbHVzdGVyaW5nIEplcsOhcnF1aWNvIC0gUmVwcmVzZW50YWNpw7NuIGVuIGVsIHBsYW5vIikKcGx0LnNob3coKQoKCmBgYAoKIyMjIDguOC4zKSBDbMO6c3RlciBiYXNhZG8gZW4gbW9kZWxvcyAoR01NKQoKYGBge3B5dGhvbn0KWDQgPSBucC52c3RhY2soWwogICAgbnAucmFuZG9tLm11bHRpdmFyaWF0ZV9ub3JtYWwoWzAsMF0sIFtbMSwwLjVdLFswLjUsMV1dLCBzaXplPTUwKSwKICAgIG5wLnJhbmRvbS5tdWx0aXZhcmlhdGVfbm9ybWFsKFs1LDBdLCBbWzEsLTAuNV0sWy0wLjUsMV1dLCBzaXplPTUwKSwKICAgIG5wLnJhbmRvbS5tdWx0aXZhcmlhdGVfbm9ybWFsKFszLDVdLCBbWzEsMC4yXSxbMC4yLDFdXSwgc2l6ZT01MCkKXSkKCmdtbSA9IEdhdXNzaWFuTWl4dHVyZShuX2NvbXBvbmVudHM9MywgcmFuZG9tX3N0YXRlPTEyMykKbGFiZWxzX2dtbSA9IGdtbS5maXRfcHJlZGljdChYNCkKCnBsdC5zY2F0dGVyKFg0WzosMF0sIFg0WzosMV0sIGM9bGFiZWxzX2dtbSwgY21hcD0iU2V0MSIsIHM9NTApCnBsdC50aXRsZSgiQ2x1c3RlcmluZyBiYXNhZG8gZW4gbW9kZWxvcyAoR01NKSIpCnBsdC5zaG93KCkKYGBgCgojIDkpIENsYXNpZmljYWNpw7NuCgpVbiBtb2RlbG8gZGUgY2xhc2lmaWNhY2nDs24gbXVsdGl2YXJpYWRhIGVzIHVuYSB0w6ljbmljYSBlc3RhZMOtc3RpY2EgeSBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyBxdWUgYnVzY2EgYXNpZ25hciBvYnNlcnZhY2lvbmVzIGEgY2F0ZWdvcsOtYXMgbyBjbGFzZXMsIGN1YW5kbyBkaWNoYXMgb2JzZXJ2YWNpb25lcyBlc3TDoW4gZGVzY3JpdGFzIHBvciBtw7psdGlwbGVzIHZhcmlhYmxlcyBpbmRlcGVuZGllbnRlcyAocHJlZGljdG9yYXMpLiBGb3JtYWxtZW50ZSwgc2UgdGllbmUgdW4gY29uanVudG8gZGUgdmFyaWFibGVzIGV4cGxpY2F0aXZhcyAkWCA9ICh4XzEsIHhfMiwgXGRvdHMsIHhfcCkkIHkgdW5hIHZhcmlhYmxlIHJlc3B1ZXN0YSBjYXRlZ8OzcmljYSAkWSBcaW4gezEsMixcZG90cyxLfSQsIGRvbmRlICRLJCBlcyBlbCBuw7ptZXJvIGRlIGNsYXNlcyBwb3NpYmxlcy4gRWwgb2JqZXRpdm8gZXMgY29uc3RydWlyIHVuYSByZWdsYSBkZSBkZWNpc2nDs24gcXVlIG1pbmltaWNlIGVsIGVycm9yIGRlIGNsYXNpZmljYWNpw7NuIGFsIHByZWRlY2lyICRZJCBlbiBmdW5jacOzbiBkZSAkWCQuCgpFc3RvcyBtb2RlbG9zIHNpcnZlbiBwYXJhIHJlY29ub2NlciBwYXRyb25lcyB5IHRvbWFyIGRlY2lzaW9uZXMgYmFzYWRhcyBlbiBkYXRvcyBtdWx0aWRpbWVuc2lvbmFsZXMuIFBvciBlamVtcGxvLCB1biBtb2RlbG8gZGUgY2xhc2lmaWNhY2nDs24gbXVsdGl2YXJpYWRhIHB1ZWRlIGVzdGltYXIgbGEgcHJvYmFiaWxpZGFkIGRlIHF1ZSB1biBjbGllbnRlIGNvbXByZSB1biBwcm9kdWN0byAoJFk9MSQpIG8gbm8gKCRZPTAkKSBlbiBmdW5jacOzbiBkZSB2YXJpYWJsZXMgY29tbyBlZGFkLCBpbmdyZXNvcyB5IGjDoWJpdG9zIGRlIGNvbnN1bW8uIE1hdGVtw6F0aWNhbWVudGUsIHNlIGJ1c2NhIHVuYSBmdW5jacOzbiBkZSBkZWNpc2nDs24gJGYoWCkkIHF1ZSBhcHJveGltZSAkXG1hdGhiYntQfShZPWt8WCkkIHBhcmEgY2FkYSBjbGFzZSAkayQsIHkgbHVlZ28gc2UgY2xhc2lmaXF1ZSBvYnNlcnZhbmRvIGN1w6FsIGRlIGVzYXMgcHJvYmFiaWxpZGFkZXMgZXMgbWF5b3I6CgokJFxoYXR7eX0gPSBcYXJnXG1heF97ayBcaW4gXHsxLFxkb3RzLEtcfX0gXG1hdGhiYntQfShZPWsgXG1pZCBYKSQkCgpFbnRyZSBsb3MgbcOpdG9kb3MgbcOhcyB1c2Fkb3MgZXN0w6FuIGxhIHJlZ3Jlc2nDs24gbG9nw61zdGljYSBtdWx0aWNsYXNlLCBlbCBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSBsaW5lYWwgKExEQSksIGxvcyDDoXJib2xlcyBkZSBkZWNpc2nDs24sIGxhcyBtw6FxdWluYXMgZGUgc29wb3J0ZSB2ZWN0b3JpYWwgKFNWTSkgeSBsYXMgcmVkZXMgbmV1cm9uYWxlcy4gVG9kb3MgZWxsb3MgbW9kZWxhbiBkZSBkaXN0aW50YSBmb3JtYSBsYSByZWxhY2nDs24gZW50cmUgbGFzIHZhcmlhYmxlcyBwcmVkaWN0b3JhcyB5IGxhIHByb2JhYmlsaWRhZCBkZSBwZXJ0ZW5lbmNpYSBhIGNhZGEgY2xhc2UuIFBvciBlamVtcGxvLCBlbiBMREEgc2UgYXN1bWUgcXVlIGxvcyBkYXRvcyBwcm92aWVuZW4gZGUgZGlzdHJpYnVjaW9uZXMgbm9ybWFsZXMgbXVsdGl2YXJpYWRhcyBjb24gbWVkaWFzIGRpc3RpbnRhcyAkXG11X2skIHkgdW5hIG1hdHJpeiBkZSBjb3ZhcmlhbnphIGNvbcO6biAkXFNpZ21hJCwgbG8gcXVlIGdlbmVyYSBmcm9udGVyYXMgZGUgZGVjaXNpw7NuIGxpbmVhbGVzIGVudHJlIGxhcyBjbGFzZXMuCgpMb3MgY2Fzb3MgZGUgdXNvIGFiYXJjYW4gbcO6bHRpcGxlcyBjYW1wb3M6IGVuIG1lZGljaW5hLCBwYXJhIGNsYXNpZmljYXIgc2kgdW4gcGFjaWVudGUgcHJlc2VudGEgdW5hIGVuZmVybWVkYWQgbyBubyBzZWfDum4gcmVzdWx0YWRvcyBjbMOtbmljb3M7IGVuIGZpbmFuemFzLCBwYXJhIGRldGVybWluYXIgZWwgcmllc2dvIGNyZWRpdGljaW8gZGUgdW4gc29saWNpdGFudGU7IGVuIG1hcmtldGluZywgcGFyYSBzZWdtZW50YXIgY2xpZW50ZXMgZW4gcGVyZmlsZXMgZGUgY29uc3VtbzsgeSBlbiBzZWd1cmlkYWQsIHBhcmEgc2lzdGVtYXMgZGUgcmVjb25vY2ltaWVudG8gZmFjaWFsIG8gZGV0ZWNjacOzbiBkZSBmcmF1ZGVzLiBFbiBnZW5lcmFsLCBzZSBhcGxpY2FuIHNpZW1wcmUgcXVlIHNlIHRlbmdhIHVuIGNvbmp1bnRvIGRlIGRhdG9zIGNvbiB2YXJpYXMgdmFyaWFibGVzIHkgc2UgYnVzcXVlIHByZWRlY2lyIHVuYSBjYXRlZ29yw61hIGRpc2NyZXRhIGEgcGFydGlyIGRlIGVsbGFzLgoKKipFamVtcGxvIGNvbiBSKioKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CgojIFNlbWlsbGEKc2V0LnNlZWQoMTIzKQoKIyAxLiBHZW5lcmFyIGRhdG9zIHNpbXVsYWRvcwpuIDwtIDIwMAp4MSA8LSBybm9ybShuLCBtZWFuID0gcmVwKGMoMiwgNSksIGVhY2ggPSBuLzIpLCBzZCA9IDEpCngyIDwtIHJub3JtKG4sIG1lYW4gPSByZXAoYygyLCA1KSwgZWFjaCA9IG4vMiksIHNkID0gMSkKY2xhc2UgPC0gZmFjdG9yKHJlcChjKCJBIiwgIkIiKSwgZWFjaCA9IG4vMikpCgpkYXRvcyA8LSBkYXRhLmZyYW1lKHgxLCB4MiwgY2xhc2UpCgojIDIuIEVudHJlbmFyIG1vZGVsbyBMREEKbW9kZWxvX2xkYSA8LSBsZGEoY2xhc2UgfiB4MSArIHgyLCBkYXRhID0gZGF0b3MpCgojIDMuIFByZWRpY2Npb25lcyB5IGZyb250ZXJhCmdyaWQgPC0gZXhwYW5kLmdyaWQoCiAgeDEgPSBzZXEobWluKGRhdG9zJHgxKS0xLCBtYXgoZGF0b3MkeDEpKzEsIGxlbmd0aD0yMDApLAogIHgyID0gc2VxKG1pbihkYXRvcyR4MiktMSwgbWF4KGRhdG9zJHgyKSsxLCBsZW5ndGg9MjAwKQopCgpncmlkJGNsYXNlX3ByZWQgPC0gcHJlZGljdChtb2RlbG9fbGRhLCBuZXdkYXRhID0gZ3JpZCkkY2xhc3MKCiMgNC4gVmlzdWFsaXphY2nDs24KZ2dwbG90KCkgKwogIGdlb21fcG9pbnQoZGF0YSA9IGRhdG9zLCBhZXMoeCA9IHgxLCB5ID0geDIsIGNvbG9yID0gY2xhc2UpLCBhbHBoYSA9IDAuNywgc2l6ZSA9IDMpICsKICBnZW9tX2NvbnRvdXIoZGF0YSA9IGNiaW5kKGdyaWQsIHogPSBhcy5udW1lcmljKGdyaWQkY2xhc2VfcHJlZCkpLAogICAgICAgICAgICAgICBhZXMoeCA9IHgxLCB5ID0geDIsIHogPSB6KSwKICAgICAgICAgICAgICAgY29sb3IgPSAiYmxhY2siLCBsaW5ld2lkdGggPSAxKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGMoInRvbWF0byIsICJzdGVlbGJsdWUiKSkgKwogIHRoZW1lX21pbmltYWwoKSArCiAgbGFicyh0aXRsZSA9ICJDbGFzaWZpY2FjacOzbiBNdWx0aXZhcmlhZGEgY29uIExEQSIsCiAgICAgICBzdWJ0aXRsZSA9ICJGcm9udGVyYSBkZSBkZWNpc2nDs24gZW50cmUgZG9zIGNsYXNlcyIsCiAgICAgICB4ID0gIlZhcmlhYmxlIFgxIiwgeSA9ICJWYXJpYWJsZSBYMiIpCgpgYGAKCiMjIDkuMSkga05OCgpFbCBrLU5lYXJlc3QgTmVpZ2hib3JzIChrTk4pIGVzIHVuIGFsZ29yaXRtbyBkZSBjbGFzaWZpY2FjacOzbiBzdXBlcnZpc2FkYSBxdWUgYXNpZ25hIHVuYSBjbGFzZSBhIHVuYSBudWV2YSBvYnNlcnZhY2nDs24gYmFzw6FuZG9zZSBlbiBsYSBwcm94aW1pZGFkIGEgc3VzIHZlY2lub3MgbcOhcyBjZXJjYW5vcyBlbiBlbCBlc3BhY2lvIGRlIGxhcyB2YXJpYWJsZXMuIEZvcm1hbG1lbnRlLCBkYWRvIHVuIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8gY29uIHBhcmVzICQoeF9pLCB5X2kpJCBkb25kZSAkeF9pIFxpbiBcbWF0aGJie1J9XnAkIHNvbiBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzIHkgJHlfaSBcaW4gezEsMixcZG90cyxLfSQgbGFzIGNsYXNlcywgY3VhbmRvIGFwYXJlY2UgdW4gbnVldm8gcHVudG8gJHgkLCBzZSBidXNjYW4gc3VzICRrJCB2ZWNpbm9zIG3DoXMgY2VyY2Fub3Mgc2Vnw7puIHVuYSBtw6l0cmljYSBkZSBkaXN0YW5jaWEgKGNvbcO6bm1lbnRlIGxhIGRpc3RhbmNpYSBldWNsaWRpYW5hKS4KCkVsIGFsZ29yaXRtbyBzaXJ2ZSBwYXJhIGNsYXNpZmljYWNpw7NuIHkgdGFtYmnDqW4gcGFyYSByZWdyZXNpw7NuLiBFbiBlbCBjYXNvIGRlIGNsYXNpZmljYWNpw7NuLCBsYSByZWdsYSBkZSBkZWNpc2nDs24gc2UgYmFzYSBlbiBsYSBtYXlvcsOtYSBkZSBjbGFzZXMgZW50cmUgbG9zICRrJCB2ZWNpbm9zIG3DoXMgY2VyY2Fub3MuIE1hdGVtw6F0aWNhbWVudGU6ICQkXGhhdHt5fSh4KSA9IFxhcmdcbWF4X3tjIFxpbiBcezEsXGRvdHMsS1x9fSBcc3VtX3tpIFxpbiBOX2soeCl9IFxtYXRoYmZ7MX0oeV9pID0gYykkJAoKZG9uZGUgJE5fayh4KSQgZXMgZWwgY29uanVudG8gZGUgw61uZGljZXMgZGUgbG9zICRrJCB2ZWNpbm9zIG3DoXMgY2VyY2Fub3MgYSAkeCQgeSAkXG1hdGhiZnsxfShcY2RvdCkkIGVzIGxhIGZ1bmNpw7NuIGluZGljYWRvcmEuCgpFbCBrTk4gZXMgw7p0aWwgcG9ycXVlIG5vIHJlcXVpZXJlIHVuIG1vZGVsbyBwYXJhbcOpdHJpY28gbmkgc3Vwb3NpY2lvbmVzIGZ1ZXJ0ZXMgc29icmUgbGEgZGlzdHJpYnVjacOzbiBkZSBsb3MgZGF0b3M6IHNpbXBsZW1lbnRlIHNlIGJhc2EgZW4gbGEgZ2VvbWV0csOtYSBkZSBsb3MgcHVudG9zIGVuIGVsIGVzcGFjaW8uIEVzIGVzcGVjaWFsbWVudGUgw7p0aWwgZW4gcHJvYmxlbWFzIGRvbmRlIGxhIGZyb250ZXJhIGVudHJlIGNsYXNlcyBlcyBjb21wbGVqYSBvIG5vIGxpbmVhbCwgeWEgcXVlIHNlIGFkYXB0YSBkZSBtYW5lcmEgbG9jYWwgYSBsYSBlc3RydWN0dXJhIGRlIGxvcyBkYXRvcy4KCkVudHJlIHN1cyBjYXNvcyBkZSB1c28gZXN0w6FuOiBlbiBtZWRpY2luYSwgcGFyYSBjbGFzaWZpY2FyIHR1bW9yZXMgY29tbyBiZW5pZ25vcyBvIG1hbGlnbm9zIHNlZ8O6biBtZWRpZGFzIGNsw61uaWNhczsgZW4gZmluYW56YXMsIHBhcmEgZGV0ZWN0YXIgZnJhdWRlcyBlbiB0YXJqZXRhcyBkZSBjcsOpZGl0bzsgZW4gbWFya2V0aW5nLCBwYXJhIHNlZ21lbnRhciBjbGllbnRlcyBjb24gYmFzZSBlbiBzaW1pbGl0dWRlcyBkZSBjb25zdW1vOyB5IGVuIHNpc3RlbWFzIGRlIHJlY29tZW5kYWNpw7NuLCBkb25kZSBzZSBpZGVudGlmaWNhbiB1c3VhcmlvcyBjb24gY29tcG9ydGFtaWVudG9zIGNlcmNhbm9zIHBhcmEgcHJlZGVjaXIgcXXDqSBwcm9kdWN0byBwdWVkZSBpbnRlcmVzYXJsZSBhIG90cm8gdXN1YXJpby4KCioqQ2xhc2VzIGJhbGFuY2VhZGFzKioKCkVuIG1vZGVsb3MgZGUgY2xhc2lmaWNhY2nDs24gY29tbyBrTk4sIGVsIHRlbWEgZGUgbGFzIGNsYXNlcyBiYWxhbmNlYWRhcyBlcyBtdXkgaW1wb3J0YW50ZSBwb3JxdWUgZWwgYWxnb3JpdG1vIGRlY2lkZSBsYSBjbGFzZSBkZSB1biBudWV2byBwdW50byBjb250YW5kbyBjdcOhbnRvcyB2ZWNpbm9zIHBlcnRlbmVjZW4gYSBjYWRhIGNhdGVnb3LDrWEuIFNpIHVuYSBjbGFzZSBlcyBtdWNobyBtw6FzIG51bWVyb3NhIHF1ZSBvdHJhIChjbGFzZXMgZGVzYmFsYW5jZWFkYXMpLCBlcyBtw6FzIHByb2JhYmxlIHF1ZSBsYSBtYXlvcsOtYSBkZSBsb3MgdmVjaW5vcyBjZXJjYW5vcyBwZXJ0ZW5lemNhbiBhIGVzYSBjbGFzZSBkb21pbmFudGUsIGxvIHF1ZSBsbGV2YSBhIHVuIHNlc2dvIHNpc3RlbcOhdGljbyBlbiBsYSBwcmVkaWNjacOzbi4KClBvciBlamVtcGxvLCBzaSBlbiB1biBkYXRhc2V0IGVsICQ5MCUkIGRlIGxhcyBvYnNlcnZhY2lvbmVzIHNvbiBkZSBsYSBjbGFzZSAkQSQgeSBzb2xvIGVsICQxMCUkIHNvbiBkZSBsYSBjbGFzZSAkQiQsIGVsIG1vZGVsbyBrTk4gdGVuZGVyw6EgYSBjbGFzaWZpY2FyIGNhc2kgdG9kb3MgbG9zIHB1bnRvcyBjb21vICRBJCwgaW5jbHVzbyBzaSBsb3MgcHVudG9zIHJlYWxtZW50ZSBwZXJ0ZW5lY2VuIGEgJEIkLiBFc3RvIGhhY2UgcXVlIGxhcyBtw6l0cmljYXMgZ2xvYmFsZXMgY29tbyBsYSBwcmVjaXNpw7NuIHBhcmV6Y2FuIGJ1ZW5hcyAocG9ycXVlIGFjaWVydGEgZW4gbGEgbWF5b3LDrWEpLCBwZXJvIGVsIGRlc2VtcGXDsW8gc29icmUgbGEgY2xhc2UgbWlub3JpdGFyaWEgc2Vyw6EgbXV5IHBvYnJlLgoKUGFyYSBtYW5lamFyIGVzdGUgcHJvYmxlbWEsIGV4aXN0ZW4gdmFyaWFzIGVzdHJhdGVnaWFzOgoKKiBCYWxhbmNlbyBkZWwgZGF0YXNldCBhbnRlcyBkZSBlbnRyZW5hcjogbWVkaWFudGUgdMOpY25pY2FzIGRlIHN1Ym11ZXN0cmVvIGRlIGxhIGNsYXNlIG1heW9yaXRhcmlhIG8gc29icmVtdWVzdHJlbyBkZSBsYSBtaW5vcml0YXJpYSAocG9yIGVqZW1wbG8gY29uIFNNT1RFKS4KCiogUG9uZGVyYWNpw7NuIGVuIGtOTjogYXNpZ25hbmRvIG3DoXMgcGVzbyBhIGxvcyB2ZWNpbm9zIGRlIGNsYXNlcyBtaW5vcml0YXJpYXMsIGRlIG1hbmVyYSBxdWUgc3Ugdm90byBjdWVudGUgbcOhcyBlbiBsYSBkZWNpc2nDs24uCgoqIEV2YWx1YWNpw7NuIGNvbiBtw6l0cmljYXMgYWRlY3VhZGFzOiB1c2FyIHJlY2FsbCwgRjEtc2NvcmUgbyBtYXRyaXogZGUgY29uZnVzacOzbiwgcXVlIHJlZmxlamVuIG1lam9yIGVsIGRlc2VtcGXDsW8gZW4gYW1iYXMgY2xhc2VzLCBlbiBsdWdhciBkZSBzb2xvIGxhIHByZWNpc2nDs24uCgoKRW4gcmVzdW1lbiwgZW4ga05OICh5IG90cm9zIGNsYXNpZmljYWRvcmVzKSBlbCBiYWxhbmNlIGRlIGNsYXNlcyBlcyBjcnVjaWFsIHBvcnF1ZSBlbCBtb2RlbG8gc2UgYmFzYSBlbiBsYSBmcmVjdWVuY2lhIGxvY2FsIGRlIGxhcyBvYnNlcnZhY2lvbmVzLCB5IHVuIGRlc2JhbGFuY2UgZnVlcnRlIHB1ZWRlIG9jdWx0YXIgZWwgbWFsIGRlc2VtcGXDsW8gZW4gbGEgY2xhc2UgbWlub3JpdGFyaWEuCgoqKlZhbGlkYWNpw7NuIENydXphZGEqKgpMYSB2YWxpZGFjacOzbiBjcnV6YWRhIGVuIG1vZGVsb3MgZGUgY2xhc2lmaWNhY2nDs24ga05OIGVzIHVuYSB0w6ljbmljYSBkZSBldmFsdWFjacOzbiBxdWUgcGVybWl0ZSBlc3RpbWFyIGVsIHJlbmRpbWllbnRvIHJlYWwgZGVsIG1vZGVsbyBzaW4gZGVwZW5kZXIgZGUgdW5hIMO6bmljYSBwYXJ0aWNpw7NuIGVudHJlbmFtaWVudG/igJNwcnVlYmEuIExhIGlkZWEgZXMgZGl2aWRpciBlbCBjb25qdW50byBkZSBkYXRvcyBlbiAkSyQgc3ViY29uanVudG9zIChvIGZvbGRzKSwgZW50cmVuYXIgZWwgbW9kZWxvIGtOTiBjb24gJEstMSQgZGUgZWxsb3MgeSBldmFsdWFyIGVuIGVsIHN1YmNvbmp1bnRvIHJlc3RhbnRlLiBFc3RlIHByb2Nlc28gc2UgcmVwaXRlICRLJCB2ZWNlcywgY2FtYmlhbmRvIGVsIGZvbGQgZGUgdmFsaWRhY2nDs24gZW4gY2FkYSBpdGVyYWNpw7NuLCB5IGx1ZWdvIHNlIHByb21lZGlhbiBsYXMgbcOpdHJpY2FzIChwcmVjaXNpw7NuLCBlcnJvciwgc2Vuc2liaWxpZGFkLCBldGMuKS4KCkVuIGVsIGNhc28gZXNwZWPDrWZpY28gZGUga05OLCBsYSB2YWxpZGFjacOzbiBjcnV6YWRhIHNlIHVzYSB0YW1iacOpbiBwYXJhIHNlbGVjY2lvbmFyIGVsIHZhbG9yIMOzcHRpbW8gZGUgJGskIChuw7ptZXJvIGRlIHZlY2lub3MpLiBTZSBwcnVlYmFuIGRpc3RpbnRvcyB2YWxvcmVzIGRlICRrJCB5IHBhcmEgY2FkYSB1bm8gc2UgY2FsY3VsYSBlbCBlcnJvciBwcm9tZWRpbyBkZSBjbGFzaWZpY2FjacOzbiBhIHRyYXbDqXMgZGUgbG9zIGZvbGRzLiBFbCAkayQgcXVlIG1pbmltaXphIGVsIGVycm9yIGRlIHZhbGlkYWNpw7NuIGNydXphZGEgc2UgY29uc2lkZXJhIGVsIG3DoXMgYWRlY3VhZG8sIHlhIHF1ZSBiYWxhbmNlYSBzZXNnbyB5IHZhcmlhbnphIGRlbCBtb2RlbG8uCgpFc3RlIG3DqXRvZG8gZXMgZXNwZWNpYWxtZW50ZSBpbXBvcnRhbnRlIGVuIGtOTiBwb3JxdWUgc3UgcmVuZGltaWVudG8gZGVwZW5kZSBmdWVydGVtZW50ZSBkZSBsYSBlbGVjY2nDs24gZGUgJGskIHkgZGUgbGEgZGlzdHJpYnVjacOzbiBkZSBsb3MgZGF0b3MuIENvbiB2YWxpZGFjacOzbiBjcnV6YWRhLCBzZSBvYnRpZW5lIHVuYSBlc3RpbWFjacOzbiBtw6FzIHJvYnVzdGEgeSBjb25maWFibGUgZGUgcXXDqSB0YW4gYmllbiBnZW5lcmFsaXphcsOhIGVsIG1vZGVsbyBhIG51ZXZvcyBkYXRvcywgcmVkdWNpZW5kbyBlbCByaWVzZ28gZGUgc29icmVhanVzdGUgbyBzdWJhanVzdGUuCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CgojIFNlbWlsYQpzZXQuc2VlZCgxMjMpCgojIDEuIEdlbmVyYXIgZGF0b3Mgc2ltdWxhZG9zIChkZXNiYWxhbmNlYWRvcykKbiA8LSAyMDAKeDEgPC0gcm5vcm0obiwgbWVhbiA9IHJlcChjKDIsIDUpLCB0aW1lcyA9IGMoMTYwLCA0MCkpLCBzZCA9IDEpCngyIDwtIHJub3JtKG4sIG1lYW4gPSByZXAoYygyLCA1KSwgdGltZXMgPSBjKDE2MCwgNDApKSwgc2QgPSAxKQpjbGFzZSA8LSBmYWN0b3IoYyhyZXAoIkEiLCAxNjApLCByZXAoIkIiLCA0MCkpKSAgIyBEZXNiYWxhbmNlYWRvOiA4MCUgQSwgMjAlIEIKCmRhdG9zIDwtIGRhdGEuZnJhbWUoeDEsIHgyLCBjbGFzZSkKCiMgMi4gVmFsaWRhY2nDs24gY3J1emFkYSBjb24gYmFsYW5jZW8gKG92ZXJzYW1wbGluZykKY29udHJvbCA8LSB0cmFpbkNvbnRyb2woCiAgbWV0aG9kID0gImN2IiwgCiAgbnVtYmVyID0gMTAsICAgICAgICAjIDEwLWZvbGQgQ1YKICBzYW1wbGluZyA9ICJ1cCIgICAgICMgQmFsYW5jZW86IG92ZXJzYW1wbGluZyBkZSBsYSBjbGFzZSBtaW5vcml0YXJpYQopCgpncmlkX2sgPC0gZXhwYW5kLmdyaWQoayA9IDE6MjApCgptb2RlbG9fY3YgPC0gdHJhaW4oCiAgY2xhc2UgfiAuLCAKICBkYXRhID0gZGF0b3MsCiAgbWV0aG9kID0gImtubiIsCiAgdHJDb250cm9sID0gY29udHJvbCwKICB0dW5lR3JpZCA9IGdyaWRfawopCgpwcmludChtb2RlbG9fY3YpCnBsb3QobW9kZWxvX2N2KQoKIyBNZWpvciB2YWxvciBkZSBrIHNlZ8O6biBDViBiYWxhbmNlYWRhCm1lam9yX2sgPC0gbW9kZWxvX2N2JGJlc3RUdW5lJGsKY2F0KCJNZWpvciB2YWxvciBkZSBrOiIsIG1lam9yX2ssICJcbiIpCgojIDMuIENyZWFyIGdyaWxsYSBwYXJhIHZpc3VhbGl6YXIgZnJvbnRlcmEgZGUgZGVjaXNpw7NuCmdyaWQgPC0gZXhwYW5kLmdyaWQoCiAgeDEgPSBzZXEobWluKGRhdG9zJHgxKS0xLCBtYXgoZGF0b3MkeDEpKzEsIGxlbmd0aCA9IDIwMCksCiAgeDIgPSBzZXEobWluKGRhdG9zJHgyKS0xLCBtYXgoZGF0b3MkeDIpKzEsIGxlbmd0aCA9IDIwMCkKKQoKIyA0LiBBcGxpY2FyIGtOTiBjb24gZWwgbWVqb3IgawpwcmVkX2dyaWQgPC0ga25uKAogIHRyYWluID0gZGF0b3NbLCBjKCJ4MSIsICJ4MiIpXSwKICB0ZXN0ID0gZ3JpZCwKICBjbCA9IGRhdG9zJGNsYXNlLAogIGsgPSBtZWpvcl9rCikKCmdyaWQkY2xhc2VfcHJlZCA8LSBwcmVkX2dyaWQKCiMgNS4gVmlzdWFsaXphY2nDs24KZ2dwbG90KCkgKwogIGdlb21fdGlsZShkYXRhID0gZ3JpZCwgYWVzKHggPSB4MSwgeSA9IHgyLCBmaWxsID0gY2xhc2VfcHJlZCksIGFscGhhID0gMC4zKSArCiAgZ2VvbV9wb2ludChkYXRhID0gZGF0b3MsIGFlcyh4ID0geDEsIHkgPSB4MiwgY29sb3IgPSBjbGFzZSksIHNpemUgPSAzLCBhbHBoYSA9IDAuOCkgKwogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoInRvbWF0byIsICJzdGVlbGJsdWUiKSkgKwogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjKCJ0b21hdG8iLCAic3RlZWxibHVlIikpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIGxhYnMoCiAgICB0aXRsZSA9IHBhc3RlKCJDbGFzaWZpY2FjacOzbiBjb24ga05OIGJhbGFuY2VhZG8gKGsgPSIsIG1lam9yX2ssICIpIiksCiAgICBzdWJ0aXRsZSA9ICJWYWxpZGFjacOzbiBjcnV6YWRhIDEwLWZvbGQgY29uIG92ZXJzYW1wbGluZyIsCiAgICB4ID0gIlZhcmlhYmxlIFgxIiwgeSA9ICJWYXJpYWJsZSBYMiIKICApCgpgYGAKCiMjIDkuMikgQ2xhc2lmaWNhY2nDs24gQmF5ZXNpYW5hCgpMYSBjbGFzaWZpY2FjacOzbiBiYXllc2lhbmEgZXMgdW5hIHTDqWNuaWNhIGRlIGNsYXNpZmljYWNpw7NuIHN1cGVydmlzYWRhIGJhc2FkYSBlbiBlbCB0ZW9yZW1hIGRlIEJheWVzLCBlbCBjdWFsIHJlbGFjaW9uYSBsYSBwcm9iYWJpbGlkYWQgY29uZGljaW9uYWwgZGUgdW5hIGNsYXNlIGRhZG8gdW4gY29uanVudG8gZGUgdmFyaWFibGVzIG9ic2VydmFkYXMuIEVuIGVzdGUgZW5mb3F1ZSwgZWwgb2JqZXRpdm8gZXMgZXN0aW1hciBsYSBwcm9iYWJpbGlkYWQgcG9zdGVyaW9yICRcbWF0aGJie1B9KFk9ayBcbWlkIFgpJCBwYXJhIGNhZGEgY2xhc2UgJGskLCBkb25kZSAkWCA9ICh4XzEsIHhfMiwgXGRvdHMsIHhfcCkkIHNvbiBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzIHkgJFkkIGVzIGxhIHZhcmlhYmxlIGNhdGVnw7NyaWNhIGEgcHJlZGVjaXIuIExhIHJlZ2xhIGRlIGRlY2lzacOzbiBjb25zaXN0ZSBlbiBhc2lnbmFyIGEgdW5hIG51ZXZhIG9ic2VydmFjacOzbiBsYSBjbGFzZSBjb24gbWF5b3IgcHJvYmFiaWxpZGFkIHBvc3RlcmlvcjoKCiQkXGhhdHt5fSA9IFxhcmdcbWF4X3trIFxpbiBcezEsXGRvdHMsS1x9fSBcbWF0aGJie1B9KFk9ayBcbWlkIFgpJCQKCkVsIHRlb3JlbWEgZGUgQmF5ZXMgZXN0YWJsZWNlIHF1ZToKCiQkXG1hdGhiYntQfShZPWsgXG1pZCBYKSA9IFxmcmFje1xtYXRoYmJ7UH0oWCBcbWlkIFk9aykgXCwgXG1hdGhiYntQfShZPWspfXtcbWF0aGJie1B9KFgpfSQkCgpBcXXDrSAkXG1hdGhiYntQfShYIFxtaWQgWT1rKSQgZXMgbGEgdmVyb3NpbWlsaXR1ZCBkZSBvYnNlcnZhciBsb3MgZGF0b3MgZGFkbyBxdWUgcGVydGVuZWNlbiBhIGxhIGNsYXNlICRrJCwgJFxtYXRoYmJ7UH0oWT1rKSQgZXMgbGEgcHJvYmFiaWxpZGFkIGEgcHJpb3JpIGRlIGVzYSBjbGFzZSwgeSAkXG1hdGhiYntQfShYKSQgZXMgbGEgcHJvYmFiaWxpZGFkIG1hcmdpbmFsIGRlIGxvcyBkYXRvcy4gRW4gbGEgcHLDoWN0aWNhLCBubyBzZSBjYWxjdWxhIGVsIGRlbm9taW5hZG9yIHBvcnF1ZSBlcyBjb23Dum4gYSB0b2RhcyBsYXMgY2xhc2VzLgoKRWwgY2xhc2lmaWNhZG9yIGJheWVzaWFubyBtw6FzIHBvcHVsYXIgZXMgZWwgTmFpdmUgQmF5ZXMsIHF1ZSBhc3VtZSBpbmRlcGVuZGVuY2lhIGNvbmRpY2lvbmFsIGVudHJlIGxhcyB2YXJpYWJsZXMgJHhfaiQgZGFkbyAkWSQuIEJham8gZXN0YSBzdXBvc2ljacOzbiwgbGEgdmVyb3NpbWlsaXR1ZCBzZSBmYWN0b3JpemEgY29tbzoKCiQkXG1hdGhiYntQfShYIFxtaWQgWT1rKSA9IFxwcm9kX3tqPTF9XnAgXG1hdGhiYntQfSh4X2ogXG1pZCBZPWspJCQKCkVzdG8gc2ltcGxpZmljYSBlbm9ybWVtZW50ZSBlbCBjw6FsY3VsbywgYXVucXVlIGxhIHN1cG9zaWNpw7NuIGRlIGluZGVwZW5kZW5jaWEgcmFyYSB2ZXogc2UgY3VtcGxlIGVzdHJpY3RhbWVudGUuCgpFbnRyZSBsb3MgY2Fzb3MgZGUgdXNvIGRlc3RhY2FuOiBlbiBmaWx0cmFkbyBkZSBzcGFtLCBkb25kZSBsb3MgY29ycmVvcyBzZSBjbGFzaWZpY2FuIGNvbW8gc3BhbSBvIG5vIHNwYW0gYSBwYXJ0aXIgZGUgbGFzIHBhbGFicmFzIHF1ZSBjb250aWVuZW47IGVuIHByb2Nlc2FtaWVudG8gZGUgbGVuZ3VhamUgbmF0dXJhbCwgcGFyYSBjbGFzaWZpY2FyIHNlbnRpbWllbnRvcyBkZSB0ZXh0b3M7IGVuIG1lZGljaW5hLCBwYXJhIGRpYWdub3N0aWNhciBlbmZlcm1lZGFkZXMgYSBwYXJ0aXIgZGUgc8OtbnRvbWFzOyB5IGVuIGZpbmFuemFzLCBwYXJhIGV2YWx1YXIgcmllc2dvIGNyZWRpdGljaW8uIFN1IHByaW5jaXBhbCB2ZW50YWphIGVzIHF1ZSBlcyB1biBtw6l0b2RvIHLDoXBpZG8sIGludGVycHJldGFibGUgeSBtdXkgZWZlY3Rpdm8gaW5jbHVzbyBjb24gZ3JhbmRlcyB2b2zDum1lbmVzIGRlIGRhdG9zIHkgdmFyaWFibGVzIGNhdGVnw7NyaWNhcy4KCioqQW7DoWxpc2lzIGRlc2NyaW1pbmFudGUgbGluZWFsKioKCkVsIGFuw6FsaXNpcyBkaXNjcmltaW5hbnRlIGxpbmVhbCAoTERBKSBlcyB1biBtw6l0b2RvIGRlIGNsYXNpZmljYWNpw7NuIHF1ZSBzdXJnZSBkZSBsYSBwZXJzcGVjdGl2YSBiYXllc2lhbmEsIGRvbmRlIHNlIGFzdW1lIHF1ZSBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzICRYJCBzaWd1ZW4gdW5hIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIG11bHRpdmFyaWFkYSBlbiBjYWRhIGNsYXNlLCBjb24gbGEgbWlzbWEgbWF0cml6IGRlIGNvdmFyaWFuemEgJFxTaWdtYSQgcGFyYSB0b2RhcyBlbGxhcy4gVXNhbmRvIGVsIHRlb3JlbWEgZGUgQmF5ZXMsIHNlIGRlcml2YW4gZnVuY2lvbmVzIGRpc2NyaW1pbmFudGVzIHF1ZSBwZXJtaXRlbiBjYWxjdWxhciwgcGFyYSBjYWRhIGNsYXNlICRrJCwgdW4gcHVudGFqZSBkZSBwZXJ0ZW5lbmNpYSB5IGFzaWduYXIgdW5hIG51ZXZhIG9ic2VydmFjacOzbiBhIGxhIGNsYXNlIGNvbiBtYXlvciB2YWxvci4KCkVuIHTDqXJtaW5vcyBtYXRlbcOhdGljb3MsIGxhIGZ1bmNpw7NuIGRpc2NyaW1pbmFudGUgZGUgTERBIHBhcmEgdW5hIGNsYXNlICRrJCBlcwoKJCRcZGVsdGFfayh4KSA9IHheXHRvcCBcU2lnbWFeey0xfSBcbXVfayBcOy1cOyBcdGZyYWN7MX17Mn1cbXVfa15cdG9wIFxTaWdtYV57LTF9XG11X2sgXDsrXDsgXGxvZyBccGlfayQkCm9uZGUgJFxtdV9rJCBlcyBlbCB2ZWN0b3IgZGUgbWVkaWFzIGRlIGxhIGNsYXNlICRrJCwgJFxTaWdtYSQgbGEgbWF0cml6IGRlIGNvdmFyaWFuemEgY29tw7puIHkgJFxwaV9rJCBsYSBwcm9iYWJpbGlkYWQgYSBwcmlvcmkgZGUgbGEgY2xhc2UuIExhIHJlZ2xhIGRlIGRlY2lzacOzbiBlcyBhc2lnbmFyICR4JCBhIGxhIGNsYXNlIHF1ZSBtYXhpbWljZSAkXGRlbHRhX2soeCkkLgoKRXN0ZSBlbmZvcXVlIGVzIG11eSDDunRpbCBjdWFuZG8gbGFzIGNsYXNlcyBzb24gYXByb3hpbWFkYW1lbnRlIG5vcm1hbGVzIHkgbGluZWFsbWVudGUgc2VwYXJhYmxlcy4gU2UgYXBsaWNhIGVuIMOhcmVhcyBjb21vIGVsIHJlY29ub2NpbWllbnRvIGRlIHBhdHJvbmVzLCBjbGFzaWZpY2FjacOzbiBkZSBpbcOhZ2VuZXMsIGRpYWduw7NzdGljbyBtw6lkaWNvIG8gc2VnbWVudGFjacOzbiBkZSBjbGllbnRlcywgZG9uZGUgYWRlbcOhcyBkZSBwcmVkZWNpciBldGlxdWV0YXMsIExEQSBwZXJtaXRlIGludGVycHJldGFyIGPDs21vIGxhcyB2YXJpYWJsZXMgY29udHJpYnV5ZW4gYSBsYSBzZXBhcmFjacOzbiBlbnRyZSBncnVwb3MuCgoqKkFuw6FsaXNpcyBkZXNjcmltaW5hbnRlIGN1YWRyYXRpY28qKgoKRWwgYW7DoWxpc2lzIGRpc2NyaW1pbmFudGUgY3VhZHLDoXRpY28gKFFEQSkgZXMgdW5hIGV4dGVuc2nDs24gZGVsIGFuw6FsaXNpcyBkaXNjcmltaW5hbnRlIGxpbmVhbCAoTERBKSBkZW50cm8gZGVsIG1hcmNvIGRlIGxhIGNsYXNpZmljYWNpw7NuIGJheWVzaWFuYS4gTGEgZGlmZXJlbmNpYSBwcmluY2lwYWwgZXMgcXVlIGVuIFFEQSBubyBzZSBhc3VtZSBxdWUgdG9kYXMgbGFzIGNsYXNlcyBjb21wYXJ0ZW4gbGEgbWlzbWEgbWF0cml6IGRlIGNvdmFyaWFuemEuIEVuIGx1Z2FyIGRlIHVuYSDDum5pY2EgJFxTaWdtYSQsIGNhZGEgY2xhc2UgJGskIHRpZW5lIHN1IHByb3BpYSBtYXRyaXogJFxTaWdtYV9rJC4gRXN0byBwZXJtaXRlIG1heW9yIGZsZXhpYmlsaWRhZCBlbiBsYSBmb3JtYSBkZSBsYXMgZnJvbnRlcmFzIGRlIGRlY2lzacOzbiwgcXVlIGRlamFuIGRlIHNlciBsaW5lYWxlcyB5IHNlIHZ1ZWx2ZW4gY3VydmFzIChnZW5lcmFsbWVudGUgZWzDrXB0aWNhcykuCgpMYSBmdW5jacOzbiBkaXNjcmltaW5hbnRlIGVuIFFEQSBwYXJhIHVuYSBjbGFzZSAkayQgc2UgZXhwcmVzYSBjb21vOgoKJCQgXGRlbHRhX2soeCkgPSAtXHRmcmFjezF9ezJ9IFxsb2cgfFxTaWdtYV9rfAogICAgICAgICAgICAgIFw7LVw7IFx0ZnJhY3sxfXsyfSh4IC0gXG11X2spXlx0b3AgXFNpZ21hX2teey0xfSAoeCAtIFxtdV9rKQogICAgICAgICAgICAgIFw7K1w7IFxsb2cgXHBpX2skJAoKZG9uZGUgJFxtdV9rJCBlcyBlbCB2ZWN0b3IgZGUgbWVkaWFzIGRlIGxhIGNsYXNlICRrJCwgJFxTaWdtYV9rJCBsYSBtYXRyaXogZGUgY292YXJpYW56YSBkZSBsYSBjbGFzZSwgeSAkXHBpX2skIGxhIHByb2JhYmlsaWRhZCBhIHByaW9yaSBkZSBwZXJ0ZW5lY2VyIGEgZGljaGEgY2xhc2UuCgpRREEgcmVzdWx0YSBlc3BlY2lhbG1lbnRlIMO6dGlsIGN1YW5kbyBsYXMgY2xhc2VzIHRpZW5lbiBmb3JtYXMgeSB2YXJpYW56YXMgbXV5IGRpZmVyZW50ZXMsIHlhIHF1ZSBMREEgcHVlZGUgZmFsbGFyIGFsIGZvcnphciBmcm9udGVyYXMgbGluZWFsZXMuIFBvciBlbGxvLCBRREEgc2UgYXBsaWNhIGVuIHByb2JsZW1hcyBjb21wbGVqb3MgZGUgY2xhc2lmaWNhY2nDs24gY29tbyBiaW9sb2fDrWEgY29tcHV0YWNpb25hbCwgdmlzacOzbiBwb3IgY29tcHV0YWRvciBvIGZpbmFuemFzLCBkb25kZSBsb3MgZGF0b3MgZGUgZGlzdGludGFzIGNsYXNlcyBubyBzZSBkaXN0cmlidXllbiBjb24gbGEgbWlzbWEgZXN0cnVjdHVyYSBkZSBkaXNwZXJzacOzbi4KCioqRWplbXBsbyBjb24gUioqCgpgYGB7cn0KCgpzZXQuc2VlZCgxMjMpCgojIDEuIEdlbmVyYXIgZGF0b3Mgc2ltdWxhZG9zCm4gPC0gMjAwCngxIDwtIGMocm5vcm0obi8yLCBtZWFuID0gMiwgc2QgPSAxKSwKICAgICAgICBybm9ybShuLzIsIG1lYW4gPSA1LCBzZCA9IDEuMikpCngyIDwtIGMocm5vcm0obi8yLCBtZWFuID0gMywgc2QgPSAxKSwKICAgICAgICBybm9ybShuLzIsIG1lYW4gPSA2LCBzZCA9IDEuMikpCmNsYXNlIDwtIGZhY3RvcihyZXAoYygiQSIsICJCIiksIGVhY2ggPSBuLzIpKQoKZGF0b3MgPC0gZGF0YS5mcmFtZSh4MSwgeDIsIGNsYXNlKQoKIyAyLiBFbnRyZW5hciBtb2RlbG8gTmFpdmUgQmF5ZXMKbW9kZWxvX25iIDwtIG5haXZlQmF5ZXMoY2xhc2UgfiB4MSArIHgyLCBkYXRhID0gZGF0b3MpCgojIDMuIENyZWFyIGdyaWxsYSBwYXJhIGxhIGZyb250ZXJhCmdyaWQgPC0gZXhwYW5kLmdyaWQoCiAgeDEgPSBzZXEobWluKGRhdG9zJHgxKS0xLCBtYXgoZGF0b3MkeDEpKzEsIGxlbmd0aCA9IDIwMCksCiAgeDIgPSBzZXEobWluKGRhdG9zJHgyKS0xLCBtYXgoZGF0b3MkeDIpKzEsIGxlbmd0aCA9IDIwMCkKKQoKZ3JpZCRjbGFzZV9wcmVkIDwtIHByZWRpY3QobW9kZWxvX25iLCBuZXdkYXRhID0gZ3JpZCkKCiMgNC4gVmlzdWFsaXphY2nDs24KZ2dwbG90KCkgKwogIGdlb21fdGlsZShkYXRhID0gZ3JpZCwgYWVzKHggPSB4MSwgeSA9IHgyLCBmaWxsID0gY2xhc2VfcHJlZCksIGFscGhhID0gMC4zKSArCiAgZ2VvbV9wb2ludChkYXRhID0gZGF0b3MsIGFlcyh4ID0geDEsIHkgPSB4MiwgY29sb3IgPSBjbGFzZSksIHNpemUgPSAzLCBhbHBoYSA9IDAuOCkgKwogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoInRvbWF0byIsICJzdGVlbGJsdWUiKSkgKwogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjKCJ0b21hdG8iLCAic3RlZWxibHVlIikpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIGxhYnModGl0bGUgPSAiQ2xhc2lmaWNhY2nDs24gQmF5ZXNpYW5hIChOYWl2ZSBCYXllcykiLAogICAgICAgc3VidGl0bGUgPSAiQ29sb3JlcyBkZSBmb25kbyA9IGZyb250ZXJhIGRlIGRlY2lzacOzbiIsCiAgICAgICB4ID0gIlZhcmlhYmxlIFgxIiwgeSA9ICJWYXJpYWJsZSBYMiIpCgpgYGAKCiMjIDkuMykgUmVncmVzacOzbiBMb2fDrXN0aWNhCgpMYSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EgZXMgdW4gbW9kZWxvIGVzdGFkw61zdGljbyB1dGlsaXphZG8gcGFyYSBwcmVkZWNpciB1bmEgdmFyaWFibGUgZGVwZW5kaWVudGUgY2F0ZWfDs3JpY2EsIHTDrXBpY2FtZW50ZSBiaW5hcmlhICgkWSBcaW4gezAsMX0kKSwgYSBwYXJ0aXIgZGUgdW4gY29uanVudG8gZGUgdmFyaWFibGVzIGluZGVwZW5kaWVudGVzICRYID0gKHhfMSwgeF8yLCBcZG90cywgeF9wKSQuCgpBIGRpZmVyZW5jaWEgZGUgbGEgcmVncmVzacOzbiBsaW5lYWwsIHF1ZSBwcmVkaWNlIHZhbG9yZXMgY29udGludW9zLCBsYSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EgbW9kZWxhIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgb2N1cnJhIHVuIGV2ZW50by4KCkVsIG1vZGVsbyBzZSBiYXNhIGVuIGxhIGZ1bmNpw7NuIGxvZ2l0LCBxdWUgcmVsYWNpb25hIGxhIHByb2JhYmlsaWRhZCAkXG1hdGhiYntQfShZPTEgXG1pZCBYKSQgY29uIHVuYSBjb21iaW5hY2nDs24gbGluZWFsIGRlIGxhcyB2YXJpYWJsZXM6CgokJFxsb2dcbGVmdChcZnJhY3tcbWF0aGJie1B9KFk9MSBcbWlkIFgpfXsxIC0gXG1hdGhiYntQfShZPTEgXG1pZCBYKX1ccmlnaHQpID0gXGJldGFfMCArIFxiZXRhXzEgeF8xICsgXGNkb3RzICsgXGJldGFfcCB4X3AkJAoKRGUgZm9ybWEgZXF1aXZhbGVudGUsIGxhIHByb2JhYmlsaWRhZCBzZSBleHByZXNhIGNvbiBsYSBmdW5jacOzbiBzaWdtb2lkZToKCiQkXG1hdGhiYntQfShZPTEgXG1pZCBYKSA9IFxmcmFjezF9ezEgKyBlXnstKFxiZXRhXzAgKyBcYmV0YV8xIHhfMSArIFxjZG90cyArIFxiZXRhX3AgeF9wKX19JCQKCkxhIHJlZ3Jlc2nDs24gbG9nw61zdGljYSBzaXJ2ZSBwYXJhIGNsYXNpZmljYXIgb2JzZXJ2YWNpb25lcyBzZWfDum4gc3VzIGNhcmFjdGVyw61zdGljYXMsIGFzaWduYW5kbyBwcm9iYWJpbGlkYWRlcyBhIGxvcyBwb3NpYmxlcyByZXN1bHRhZG9zLiBVbmEgcmVnbGEgY29tw7puIGVzIHByZWRlY2lyICRcaGF0e3l9PTEkIHNpIGxhIHByb2JhYmlsaWRhZCBlc3RpbWFkYSBlcyBtYXlvciBxdWUgdW4gdW1icmFsIChwb3IgZWplbXBsbywgJDAuNSQpLiBFbCBtb2RlbG8gc2UgYWp1c3RhIG1heGltaXphbmRvIGxhIHZlcm9zaW1pbGl0dWQgZGUgbG9zIGRhdG9zIG9ic2VydmFkb3MsIGxvIGN1YWwgcGVybWl0ZSBlc3RpbWFyIGxvcyBwYXLDoW1ldHJvcyAkXGJldGFfaiQuCgpFbnRyZSBsb3MgY2Fzb3MgZGUgdXNvIGVzdMOhbjogZW4gbWVkaWNpbmEsIHBhcmEgcHJlZGVjaXIgc2kgdW4gcGFjaWVudGUgdGllbmUgdW5hIGVuZmVybWVkYWQgKHPDrS9ubykgc2Vnw7puIHZhcmlhYmxlcyBjbMOtbmljYXM7IGVuIGZpbmFuemFzLCBwYXJhIGVzdGltYXIgbGEgcHJvYmFiaWxpZGFkIGRlIGluY3VtcGxpbWllbnRvIGRlIHVuIGNyw6lkaXRvOyBlbiBtYXJrZXRpbmcsIHBhcmEgbW9kZWxhciBsYSBwcm9iYWJpbGlkYWQgZGUgcXVlIHVuIGNsaWVudGUgY29tcHJlIHVuIHByb2R1Y3RvOyB5IGVuIHNlZ3VyaWRhZCBpbmZvcm3DoXRpY2EsIHBhcmEgZGV0ZWN0YXIgYWNjZXNvcyBmcmF1ZHVsZW50b3MuIEVzIHVuIG1vZGVsbyBtdXkgaW50ZXJwcmV0YWRvLCB5YSBxdWUgbG9zIGNvZWZpY2llbnRlcyBzZSB0cmFkdWNlbiBlbiBvZGRzIHJhdGlvcyBxdWUgaW5kaWNhbiBsYSBpbmZsdWVuY2lhIGRlIGNhZGEgdmFyaWFibGUuCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3J9CgoKc2V0LnNlZWQoMTIzKQoKIyAxLiBHZW5lcmFyIGRhdG9zIHNpbXVsYWRvcwpuIDwtIDIwMAp4MSA8LSBydW5pZihuLCAwLCAxMCkKeDIgPC0gcnVuaWYobiwgMCwgMTApCgojIFZhcmlhYmxlIGRlcGVuZGllbnRlIGdlbmVyYWRhIGNvbiBwcm9iYWJpbGlkYWQgbG9nw61zdGljYQpwcm9iIDwtIDEgLyAoMSArIGV4cCgtKCAtNSArIDAuOCp4MSArIDAuNip4MikpKQp5IDwtIHJiaW5vbShuLCAxLCBwcm9iKQpjbGFzZSA8LSBmYWN0b3IoeSwgbGFiZWxzID0gYygiTm8iLCAiU8OtIikpCgpkYXRvcyA8LSBkYXRhLmZyYW1lKHgxLCB4MiwgY2xhc2UpCgojIDIuIEFqdXN0YXIgbW9kZWxvIGxvZ8Otc3RpY28KbW9kZWxvX2xvZ2l0IDwtIGdsbShjbGFzZSB+IHgxICsgeDIsIGRhdGEgPSBkYXRvcywgZmFtaWx5ID0gYmlub21pYWwpCgojIDMuIENyZWFyIGdyaWxsYSBwYXJhIGZyb250ZXJhCmdyaWQgPC0gZXhwYW5kLmdyaWQoCiAgeDEgPSBzZXEobWluKGRhdG9zJHgxKSwgbWF4KGRhdG9zJHgxKSwgbGVuZ3RoID0gMjAwKSwKICB4MiA9IHNlcShtaW4oZGF0b3MkeDIpLCBtYXgoZGF0b3MkeDIpLCBsZW5ndGggPSAyMDApCikKCmdyaWQkcHJvYiA8LSBwcmVkaWN0KG1vZGVsb19sb2dpdCwgbmV3ZGF0YSA9IGdyaWQsIHR5cGUgPSAicmVzcG9uc2UiKQpncmlkJGNsYXNlX3ByZWQgPC0gaWZlbHNlKGdyaWQkcHJvYiA+IDAuNSwgIlPDrSIsICJObyIpCgojIDQuIFZpc3VhbGl6YWNpw7NuCmdncGxvdCgpICsKICBnZW9tX3RpbGUoZGF0YSA9IGdyaWQsIGFlcyh4ID0geDEsIHkgPSB4MiwgZmlsbCA9IGNsYXNlX3ByZWQsIGFscGhhID0gcHJvYikpICsKICBnZW9tX3BvaW50KGRhdGEgPSBkYXRvcywgYWVzKHggPSB4MSwgeSA9IHgyLCBjb2xvciA9IGNsYXNlKSwgc2l6ZSA9IDMpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjKCJ0b21hdG8iLCAic3RlZWxibHVlIikpICsKICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygidG9tYXRvIiwgInN0ZWVsYmx1ZSIpKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICBsYWJzKHRpdGxlID0gIkNsYXNpZmljYWNpw7NuIGNvbiBSZWdyZXNpw7NuIExvZ8Otc3RpY2EiLAogICAgICAgc3VidGl0bGUgPSAiQ29sb3JlcyBkZSBmb25kbyA9IHJlZ2nDs24gZGUgY2xhc2lmaWNhY2nDs24sIHRyYW5zcGFyZW5jaWEgPSBwcm9iYWJpbGlkYWQiLAogICAgICAgeCA9ICJWYXJpYWJsZSBYMSIsIHkgPSAiVmFyaWFibGUgWDIiKSArCiAgZ3VpZGVzKGFscGhhID0gIm5vbmUiKQoKYGBgCgojIyA5LjQpIEFuw6FsaXNpcyBkaXNjcmltaW5hbnRlCgpFbCBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSBlcyB1bmEgdMOpY25pY2EgZXN0YWTDrXN0aWNhIG11bHRpdmFyaWFkYSB1dGlsaXphZGEgcGFyYSBjbGFzaWZpY2FyIG9ic2VydmFjaW9uZXMgZW4gZGlmZXJlbnRlcyBncnVwb3MgbyBjbGFzZXMsIGEgcGFydGlyIGRlIHVuIGNvbmp1bnRvIGRlIHZhcmlhYmxlcyBwcmVkaWN0b3Jhcy4gU3Ugb2JqZXRpdm8gZXMgZW5jb250cmFyIGZ1bmNpb25lcyBsaW5lYWxlcyBkZSBsYXMgdmFyaWFibGVzICRYID0gKHhfMSwgeF8yLCBcZG90cywgeF9wKSQgcXVlIG1heGltaWNlbiBsYSBzZXBhcmFjacOzbiBlbnRyZSBsYXMgY2xhc2VzLCBlcyBkZWNpciwgcXVlIGRpc2NyaW1pbmVuIGxvIG1lam9yIHBvc2libGUgZW50cmUgbG9zIGdydXBvcyBwcmV2aWFtZW50ZSBkZWZpbmlkb3MuCgpFbCBtw6l0b2RvIG3DoXMgY29ub2NpZG8gZXMgZWwgYW7DoWxpc2lzIGRpc2NyaW1pbmFudGUgbGluZWFsIChMREEpLCBxdWUgYXN1bWUgcXVlIGNhZGEgY2xhc2UgJGskIHNpZ3VlIHVuYSBkaXN0cmlidWNpw7NuIG5vcm1hbCBtdWx0aXZhcmlhZGEgY29uIG1lZGlhICRcbXVfayQgeSB1bmEgbWF0cml6IGRlIGNvdmFyaWFuemEgY29tw7puICRcU2lnbWEkLiBFbiBlc3RlIGNhc28sIGxhIHJlZ2xhIGRlIGRlY2lzacOzbiBhc2lnbmEgdW5hIG9ic2VydmFjacOzbiAkeCQgYSBsYSBjbGFzZSBxdWUgbWF4aW1pemEgbGEgZnVuY2nDs24gZGlzY3JpbWluYW50ZToKCiQkXGRlbHRhX2soeCkgPSB4Xlx0b3AgXFNpZ21hXnstMX0gXG11X2sgXDstXDsgXHRmcmFjezF9ezJ9XG11X2teXHRvcCBcU2lnbWFeey0xfVxtdV9rIFw7K1w7IFxsb2cgXHBpX2sKJCQKCmRvbmRlICRccGlfayQgZXMgbGEgcHJvYmFiaWxpZGFkIGEgcHJpb3JpIGRlIGxhIGNsYXNlICRrJC4KClRhbWJpw6luIGV4aXN0ZSBlbCBhbsOhbGlzaXMgZGlzY3JpbWluYW50ZSBjdWFkcsOhdGljbyAoUURBKSwgcXVlIHN1cmdlIGN1YW5kbyBubyBzZSBhc3VtZSB1bmEgbWF0cml6IGRlIGNvdmFyaWFuemEgY29tw7puLCBsbyBxdWUgZ2VuZXJhIGZyb250ZXJhcyBkZSBkZWNpc2nDs24gbcOhcyBmbGV4aWJsZXMgeSBubyBuZWNlc2FyaWFtZW50ZSBsaW5lYWxlcy4gTGEgZGlmZXJlbmNpYSBjbGF2ZSBlbnRyZSBMREEgeSBRREEgZXMgbGEgY29tcGxlamlkYWQgZGUgbGEgZnJvbnRlcmE6IGxpbmVhbCBlbiBlbCBwcmltZXJvLCBjdWFkcsOhdGljYSBlbiBlbCBzZWd1bmRvLgoKTG9zIGNhc29zIGRlIHVzbyBzb24gbXV5IHZhcmlhZG9zOiBlbiBtZWRpY2luYSwgcGFyYSBjbGFzaWZpY2FyIHBhY2llbnRlcyBzZWfDum4gZGlhZ27Ds3N0aWNvIGEgcGFydGlyIGRlIHBydWViYXMgY2zDrW5pY2FzOyBlbiBmaW5hbnphcywgcGFyYSBkZXRlY3RhciBjbGllbnRlcyBjb24gYWx0byBvIGJham8gcmllc2dvIGRlIGNyw6lkaXRvOyBlbiBiaW9sb2fDrWEsIHBhcmEgZGlzdGluZ3VpciBlc3BlY2llcyBhIHBhcnRpciBkZSBjYXJhY3RlcsOtc3RpY2FzIG1vcmZvbMOzZ2ljYXMgKHBvciBlamVtcGxvLCBlbCBjbMOhc2ljbyBkYXRhc2V0IGlyaXMpOyB5IGVuIG1hcmtldGluZywgcGFyYSBzZWdtZW50YXIgY2xpZW50ZXMgZW4gZ3J1cG9zIGRlIGNvbXBvcnRhbWllbnRvIGRlIGNvbXByYS4gRXMgdW5hIHTDqWNuaWNhIGFtcGxpYW1lbnRlIHVzYWRhIHBvcnF1ZSBjb21iaW5hIGludGVycHJldGFiaWxpZGFkIGVzdGFkw61zdGljYSBjb24gdW5hIGJ1ZW5hIGNhcGFjaWRhZCBkZSBjbGFzaWZpY2FjacOzbi4KCioqRWplbXBsbyBjb24gUioqCgpgYGB7cn0KCgpzZXQuc2VlZCgxMjMpCgojIDEuIERhdGFzZXQgaXJpcyAodXNhbW9zIDIgdmFyaWFibGVzIHBhcmEgZ3JhZmljYXIgZsOhY2lsbWVudGUpCmRhdG9zIDwtIGlyaXMgJT4lCiAgc2VsZWN0KFNlcGFsLkxlbmd0aCwgU2VwYWwuV2lkdGgsIFNwZWNpZXMpCgojIDIuIEVudHJlbmFyIG1vZGVsbyBMREEKbW9kZWxvX2xkYSA8LSBsZGEoU3BlY2llcyB+IFNlcGFsLkxlbmd0aCArIFNlcGFsLldpZHRoLCBkYXRhID0gZGF0b3MpCgojIDMuIENyZWFyIGdyaWxsYSBwYXJhIHZpc3VhbGl6YXIgZnJvbnRlcmEKZ3JpZCA8LSBleHBhbmQuZ3JpZCgKICBTZXBhbC5MZW5ndGggPSBzZXEobWluKGRhdG9zJFNlcGFsLkxlbmd0aCktMC41LCBtYXgoZGF0b3MkU2VwYWwuTGVuZ3RoKSswLjUsIGxlbmd0aCA9IDIwMCksCiAgU2VwYWwuV2lkdGggID0gc2VxKG1pbihkYXRvcyRTZXBhbC5XaWR0aCktMC41LCAgbWF4KGRhdG9zJFNlcGFsLldpZHRoKSswLjUsICBsZW5ndGggPSAyMDApCikKCmdyaWQkY2xhc2VfcHJlZCA8LSBwcmVkaWN0KG1vZGVsb19sZGEsIG5ld2RhdGEgPSBncmlkKSRjbGFzcwoKIyA0LiBWaXN1YWxpemFjacOzbgpnZ3Bsb3QoKSArCiAgZ2VvbV90aWxlKGRhdGEgPSBncmlkLCBhZXMoeCA9IFNlcGFsLkxlbmd0aCwgeSA9IFNlcGFsLldpZHRoLCBmaWxsID0gY2xhc2VfcHJlZCksIGFscGhhID0gMC4zKSArCiAgZ2VvbV9wb2ludChkYXRhID0gZGF0b3MsIGFlcyh4ID0gU2VwYWwuTGVuZ3RoLCB5ID0gU2VwYWwuV2lkdGgsIGNvbG9yID0gU3BlY2llcyksIHNpemUgPSAzKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygidG9tYXRvIiwgInN0ZWVsYmx1ZSIsICJzZWFncmVlbiIpKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGMoInRvbWF0byIsICJzdGVlbGJsdWUiLCAic2VhZ3JlZW4iKSkgKwogIHRoZW1lX21pbmltYWwoKSArCiAgbGFicyh0aXRsZSA9ICJBbsOhbGlzaXMgRGlzY3JpbWluYW50ZSBMaW5lYWwgKExEQSkgY29uIGlyaXMiLAogICAgICAgc3VidGl0bGUgPSAiRnJvbnRlcmFzIGRlIGRlY2lzacOzbiBlbnRyZSBlc3BlY2llcyB1c2FuZG8gU2VwYWwuTGVuZ3RoIHkgU2VwYWwuV2lkdGgiLAogICAgICAgeCA9ICJTZXBhbCBMZW5ndGgiLCB5ID0gIlNlcGFsIFdpZHRoIikKCmBgYAoKIyMgOS41KSBFamVtcGxvIGNvbiBQeXRob24KCiMjIyA5LjUuMSkgQ2xhc2lmaWNhY2nDs24gbXVsdGl2YXJpYWRhIApgYGB7cHl0aG9ufQoKIyAxLiBHZW5lcmFyIGRhdG9zIHNpbXVsYWRvcwpucC5yYW5kb20uc2VlZCgxMjMpCm4gPSAyMDAKeDEgPSBucC5jb25jYXRlbmF0ZShbbnAucmFuZG9tLm5vcm1hbCgyLCAxLCBuLy8yKSwgbnAucmFuZG9tLm5vcm1hbCg1LCAxLCBuLy8yKV0pCngyID0gbnAuY29uY2F0ZW5hdGUoW25wLnJhbmRvbS5ub3JtYWwoMiwgMSwgbi8vMiksIG5wLnJhbmRvbS5ub3JtYWwoNSwgMSwgbi8vMildKQpjbGFzZSA9IG5wLmFycmF5KFsiQSJdKihuLy8yKSArIFsiQiJdKihuLy8yKSkKCmRhdG9zID0gcGQuRGF0YUZyYW1lKHsieDEiOiB4MSwgIngyIjogeDIsICJjbGFzZSI6IGNsYXNlfSkKCiMgMi4gRW50cmVuYXIgbW9kZWxvIExEQQpsZGEgPSBMaW5lYXJEaXNjcmltaW5hbnRBbmFseXNpcygpCmxkYS5maXQoZGF0b3NbWyJ4MSIsICJ4MiJdXSwgZGF0b3NbImNsYXNlIl0pCgojIDMuIENyZWFyIGdyaWxsYSBwYXJhIGZyb250ZXJhCnh4LCB5eSA9IG5wLm1lc2hncmlkKAogICAgbnAubGluc3BhY2UoZGF0b3MueDEubWluKCktMSwgZGF0b3MueDEubWF4KCkrMSwgMjAwKSwKICAgIG5wLmxpbnNwYWNlKGRhdG9zLngyLm1pbigpLTEsIGRhdG9zLngyLm1heCgpKzEsIDIwMCkKKQpncmlkID0gbnAuY19beHgucmF2ZWwoKSwgeXkucmF2ZWwoKV0KY2xhc2VfcHJlZCA9IGxkYS5wcmVkaWN0KGdyaWQpCgojIDQuIFZpc3VhbGl6YWNpw7NuCnBsdC5jb250b3VyZih4eCwgeXksIGNsYXNlX3ByZWQucmVzaGFwZSh4eC5zaGFwZSksIGFscGhhPTAuMywgY21hcD0iY29vbHdhcm0iKQpwbHQuc2NhdHRlcihkYXRvcy54MSwgZGF0b3MueDIsIGM9KGRhdG9zLmNsYXNlPT0iQSIpLCBjbWFwPSJjb29sd2FybSIsIGVkZ2Vjb2xvcj0iayIsIHM9NTApCnBsdC50aXRsZSgiQ2xhc2lmaWNhY2nDs24gTXVsdGl2YXJpYWRhIGNvbiBMREEiKQpwbHQueGxhYmVsKCJWYXJpYWJsZSBYMSIpCnBsdC55bGFiZWwoIlZhcmlhYmxlIFgyIikKcGx0LnNob3coKQoKYGBgCgojIyMgOS41LjIpIGtOTiBjb24gY2xhc2VzIGJhbGFuY2VhZGFzIHkgdmFsaWRhY2nDs24gY3J1emFkYQoKYGBge3B5dGhvbn0KCgojIDEuIEdlbmVyYXIgZGF0b3MgZGVzYmFsYW5jZWFkb3MKeDEgPSBucC5jb25jYXRlbmF0ZShbbnAucmFuZG9tLm5vcm1hbCgyLCAxLCAxNjApLCBucC5yYW5kb20ubm9ybWFsKDUsIDEsIDQwKV0pCngyID0gbnAuY29uY2F0ZW5hdGUoW25wLnJhbmRvbS5ub3JtYWwoMiwgMSwgMTYwKSwgbnAucmFuZG9tLm5vcm1hbCg1LCAxLCA0MCldKQpjbGFzZSA9IG5wLmFycmF5KFsiQSJdKjE2MCArIFsiQiJdKjQwKQoKZGF0b3MgPSBwZC5EYXRhRnJhbWUoeyJ4MSI6IHgxLCAieDIiOiB4MiwgImNsYXNlIjogY2xhc2V9KQoKIyAyLiBQaXBlbGluZSBjb24gU01PVEUgKG92ZXJzYW1wbGluZykgKyBrTk4KcGlwZWxpbmUgPSBQaXBlbGluZShbCiAgICAoInNtb3RlIiwgU01PVEUocmFuZG9tX3N0YXRlPTEyMykpLAogICAgKCJrbm4iLCBLTmVpZ2hib3JzQ2xhc3NpZmllcigpKQpdKQoKIyAzLiBWYWxpZGFjacOzbiBjcnV6YWRhIHBhcmEgZWxlZ2lyIGsgw7NwdGltbwpwYXJhbV9ncmlkID0geyJrbm5fX25fbmVpZ2hib3JzIjogcmFuZ2UoMSwgMjEpfQpncmlkX3NlYXJjaCA9IEdyaWRTZWFyY2hDVihwaXBlbGluZSwgcGFyYW1fZ3JpZCwgY3Y9MTAsIHNjb3Jpbmc9ImFjY3VyYWN5IikKZ3JpZF9zZWFyY2guZml0KGRhdG9zW1sieDEiLCAieDIiXV0sIGRhdG9zWyJjbGFzZSJdKQoKbWVqb3JfayA9IGdyaWRfc2VhcmNoLmJlc3RfcGFyYW1zX1sia25uX19uX25laWdoYm9ycyJdCnByaW50KCJNZWpvciB2YWxvciBkZSBrOiIsIG1lam9yX2spCgojIDQuIFZpc3VhbGl6YWNpw7NuIGRlIGZyb250ZXJhCnh4LCB5eSA9IG5wLm1lc2hncmlkKAogICAgbnAubGluc3BhY2UoZGF0b3MueDEubWluKCktMSwgZGF0b3MueDEubWF4KCkrMSwgMjAwKSwKICAgIG5wLmxpbnNwYWNlKGRhdG9zLngyLm1pbigpLTEsIGRhdG9zLngyLm1heCgpKzEsIDIwMCkKKQpncmlkID0gbnAuY19beHgucmF2ZWwoKSwgeXkucmF2ZWwoKV0KY2xhc2VfcHJlZCA9IGdyaWRfc2VhcmNoLnByZWRpY3QoZ3JpZCkKCnBsdC5jb250b3VyZih4eCwgeXksIGNsYXNlX3ByZWQucmVzaGFwZSh4eC5zaGFwZSksIGFscGhhPTAuMywgY21hcD0iY29vbHdhcm0iKQpwbHQuc2NhdHRlcihkYXRvcy54MSwgZGF0b3MueDIsIGM9KGRhdG9zLmNsYXNlPT0iQSIpLCBjbWFwPSJjb29sd2FybSIsIGVkZ2Vjb2xvcj0iayIsIHM9NTApCnBsdC50aXRsZShmIkNsYXNpZmljYWNpw7NuIGNvbiBrTk4gYmFsYW5jZWFkbyAoayA9IHttZWpvcl9rfSkiKQpwbHQueGxhYmVsKCJWYXJpYWJsZSBYMSIpCnBsdC55bGFiZWwoIlZhcmlhYmxlIFgyIikKcGx0LnNob3coKQoKYGBgCgojIyMgOS41LjMpIENsYXNpZmljYWNpw7NuIEJheWVzaWFuYSAoTmFpdmUgQmF5ZXMpCmBgYHtweXRob259CgojIDEuIEdlbmVyYXIgZGF0b3Mgc2ltdWxhZG9zCngxID0gbnAuY29uY2F0ZW5hdGUoW25wLnJhbmRvbS5ub3JtYWwoMiwgMSwgbi8vMiksIG5wLnJhbmRvbS5ub3JtYWwoNSwgMS4yLCBuLy8yKV0pCngyID0gbnAuY29uY2F0ZW5hdGUoW25wLnJhbmRvbS5ub3JtYWwoMywgMSwgbi8vMiksIG5wLnJhbmRvbS5ub3JtYWwoNiwgMS4yLCBuLy8yKV0pCmNsYXNlID0gbnAuYXJyYXkoWyJBIl0qKG4vLzIpICsgWyJCIl0qKG4vLzIpKQoKZGF0b3MgPSBwZC5EYXRhRnJhbWUoeyJ4MSI6IHgxLCAieDIiOiB4MiwgImNsYXNlIjogY2xhc2V9KQoKIyAyLiBFbnRyZW5hciBtb2RlbG8gTmFpdmUgQmF5ZXMKbmIgPSBHYXVzc2lhbk5CKCkKbmIuZml0KGRhdG9zW1sieDEiLCAieDIiXV0sIGRhdG9zWyJjbGFzZSJdKQoKIyAzLiBGcm9udGVyYSBkZSBkZWNpc2nDs24KeHgsIHl5ID0gbnAubWVzaGdyaWQoCiAgICBucC5saW5zcGFjZShkYXRvcy54MS5taW4oKS0xLCBkYXRvcy54MS5tYXgoKSsxLCAyMDApLAogICAgbnAubGluc3BhY2UoZGF0b3MueDIubWluKCktMSwgZGF0b3MueDIubWF4KCkrMSwgMjAwKQopCmdyaWQgPSBucC5jX1t4eC5yYXZlbCgpLCB5eS5yYXZlbCgpXQpjbGFzZV9wcmVkID0gbmIucHJlZGljdChncmlkKQoKIyA0LiBWaXN1YWxpemFjacOzbgpwbHQuY29udG91cmYoeHgsIHl5LCBjbGFzZV9wcmVkLnJlc2hhcGUoeHguc2hhcGUpLCBhbHBoYT0wLjMsIGNtYXA9ImNvb2x3YXJtIikKcGx0LnNjYXR0ZXIoZGF0b3MueDEsIGRhdG9zLngyLCBjPShkYXRvcy5jbGFzZT09IkEiKSwgY21hcD0iY29vbHdhcm0iLCBlZGdlY29sb3I9ImsiLCBzPTUwKQpwbHQudGl0bGUoIkNsYXNpZmljYWNpw7NuIEJheWVzaWFuYSAoTmFpdmUgQmF5ZXMpIikKcGx0LnhsYWJlbCgiVmFyaWFibGUgWDEiKQpwbHQueWxhYmVsKCJWYXJpYWJsZSBYMiIpCnBsdC5zaG93KCkKCmBgYAoKIyMjIDkuNS40KSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EKCmBgYHtweXRob259CgojIDEuIEdlbmVyYXIgZGF0b3Mgc2ltdWxhZG9zCm5wLnJhbmRvbS5zZWVkKDEyMykKeDEgPSBucC5yYW5kb20udW5pZm9ybSgwLCAxMCwgbikKeDIgPSBucC5yYW5kb20udW5pZm9ybSgwLCAxMCwgbikKcHJvYiA9IDEgLyAoMSArIG5wLmV4cCgtKC01ICsgMC44KngxICsgMC42KngyKSkpCnkgPSBucC5yYW5kb20uYmlub21pYWwoMSwgcHJvYiwgbikKY2xhc2UgPSBucC53aGVyZSh5ID09IDEsICJTw60iLCAiTm8iKQoKZGF0b3MgPSBwZC5EYXRhRnJhbWUoeyJ4MSI6IHgxLCAieDIiOiB4MiwgImNsYXNlIjogY2xhc2V9KQoKIyAyLiBBanVzdGFyIG1vZGVsbyBsb2fDrXN0aWNvCmxvZ2l0ID0gTG9naXN0aWNSZWdyZXNzaW9uKCkKbG9naXQuZml0KGRhdG9zW1sieDEiLCAieDIiXV0sIGRhdG9zWyJjbGFzZSJdKQoKIyAzLiBDcmVhciBncmlsbGEgeSBwcmVkZWNpciBwcm9iYWJpbGlkYWRlcwp4eCwgeXkgPSBucC5tZXNoZ3JpZCgKICAgIG5wLmxpbnNwYWNlKGRhdG9zLngxLm1pbigpLCBkYXRvcy54MS5tYXgoKSwgMjAwKSwKICAgIG5wLmxpbnNwYWNlKGRhdG9zLngyLm1pbigpLCBkYXRvcy54Mi5tYXgoKSwgMjAwKQopCmdyaWQgPSBucC5jX1t4eC5yYXZlbCgpLCB5eS5yYXZlbCgpXQpwcm9iX2dyaWQgPSBsb2dpdC5wcmVkaWN0X3Byb2JhKGdyaWQpWzosIDFdCmNsYXNlX3ByZWQgPSBsb2dpdC5wcmVkaWN0KGdyaWQpCgojIDQuIFZpc3VhbGl6YWNpw7NuCnBsdC5jb250b3VyZih4eCwgeXksIGNsYXNlX3ByZWQucmVzaGFwZSh4eC5zaGFwZSksIGFscGhhPTAuMywgY21hcD0iY29vbHdhcm0iKQpwbHQuc2NhdHRlcihkYXRvcy54MSwgZGF0b3MueDIsIGM9KGRhdG9zLmNsYXNlPT0iU8OtIiksIGNtYXA9ImNvb2x3YXJtIiwgZWRnZWNvbG9yPSJrIiwgcz01MCkKcGx0LnRpdGxlKCJDbGFzaWZpY2FjacOzbiBjb24gUmVncmVzacOzbiBMb2fDrXN0aWNhIikKcGx0LnhsYWJlbCgiVmFyaWFibGUgWDEiKQpwbHQueWxhYmVsKCJWYXJpYWJsZSBYMiIpCnBsdC5zaG93KCkKCmBgYAoKIyMjIDkuNS41KSBBbsOhbGlzaXMgRGlzY3JpbWluYW50ZSAoTERBIGNvbiBpcmlzKQpgYGB7cHl0aG9ufQoKIyAxLiBEYXRhc2V0IGlyaXMgKDIgdmFyaWFibGVzIHBhcmEgZ3JhZmljYXIpCmlyaXMgPSBsb2FkX2lyaXMoYXNfZnJhbWU9VHJ1ZSkKZGF0b3MgPSBpcmlzLmRhdGFbWyJzZXBhbCBsZW5ndGggKGNtKSIsICJzZXBhbCB3aWR0aCAoY20pIl1dCmNsYXNlcyA9IGlyaXMudGFyZ2V0CgojIDIuIEVudHJlbmFyIG1vZGVsbyBMREEKbGRhID0gTGluZWFyRGlzY3JpbWluYW50QW5hbHlzaXMoKQpsZGEuZml0KGRhdG9zLCBjbGFzZXMpCgojIDMuIENyZWFyIGdyaWxsYQp4eCwgeXkgPSBucC5tZXNoZ3JpZCgKICAgIG5wLmxpbnNwYWNlKGRhdG9zLmlsb2NbOiwwXS5taW4oKS0wLjUsIGRhdG9zLmlsb2NbOiwwXS5tYXgoKSswLjUsIDIwMCksCiAgICBucC5saW5zcGFjZShkYXRvcy5pbG9jWzosMV0ubWluKCktMC41LCBkYXRvcy5pbG9jWzosMV0ubWF4KCkrMC41LCAyMDApCikKZ3JpZCA9IG5wLmNfW3h4LnJhdmVsKCksIHl5LnJhdmVsKCldCmNsYXNlX3ByZWQgPSBsZGEucHJlZGljdChncmlkKQoKIyA0LiBWaXN1YWxpemFjacOzbgpwbHQuY29udG91cmYoeHgsIHl5LCBjbGFzZV9wcmVkLnJlc2hhcGUoeHguc2hhcGUpLCBhbHBoYT0wLjMsIGNtYXA9ImNvb2x3YXJtIikKcGx0LnNjYXR0ZXIoZGF0b3MuaWxvY1s6LDBdLCBkYXRvcy5pbG9jWzosMV0sIGM9Y2xhc2VzLCBjbWFwPSJjb29sd2FybSIsIGVkZ2Vjb2xvcj0iayIsIHM9NTApCnBsdC50aXRsZSgiQW7DoWxpc2lzIERpc2NyaW1pbmFudGUgTGluZWFsIChMREEpIGNvbiBpcmlzIikKcGx0LnhsYWJlbCgiU2VwYWwgTGVuZ3RoIikKcGx0LnlsYWJlbCgiU2VwYWwgV2lkdGgiKQpwbHQuc2hvdygpCgpgYGAKCgoKIyAxMCkgUmVncmVzacOzbiBsaW5lYWwgbcO6bHRpcGxlIHkgcmVndWxhcml6YWNpw7NuCgpMYSByZWdyZXNpw7NuIGxpbmVhbCBtw7psdGlwbGUgZXMgdW5hIGV4dGVuc2nDs24gZGUgbGEgcmVncmVzacOzbiBsaW5lYWwgc2ltcGxlIHF1ZSBwZXJtaXRlIG1vZGVsYXIgbGEgcmVsYWNpw7NuIGVudHJlIHVuYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAkeSQgeSB2YXJpYXMgdmFyaWFibGVzIGluZGVwZW5kaWVudGVzICR4XzEsIHhfMiwgXGRvdHMsIHhfcCQuIExhIGlkZWEgZXMgZXN0aW1hciBjw7NtbyBjYWRhIHByZWRpY3RvciBpbmZsdXllIGVuICR5JCwgY29udHJvbGFuZG8gcG9yIGxvcyBkZW3DoXMuIE1hdGVtw6F0aWNhbWVudGUgc2UgZXhwcmVzYSBjb21vOgoKJCR5ID0gXGJldGFfMCArIFxiZXRhXzEgeF8xICsgXGJldGFfMiB4XzIgKyBcZG90cyArIFxiZXRhX3AgeF9wICsgXHZhcmVwc2lsb24kJAoKZG9uZGUgJFxiZXRhXzAkIGVzIGxhIGludGVyc2VjY2nDs24sICRcYmV0YV9qJCBzb24gbG9zIGNvZWZpY2llbnRlcyBkZSByZWdyZXNpw7NuIHkgJFx2YXJlcHNpbG9uJCBlcyBlbCB0w6lybWlubyBkZSBlcnJvci4KCkVsIHByb2JsZW1hIGFwYXJlY2UgY3VhbmRvIGhheSBtdWx0aWNvbGluZWFsaWRhZCAoY29ycmVsYWNpw7NuIGFsdGEgZW50cmUgcHJlZGljdG9yZXMpIG8gY3VhbmRvIGVsIG7Dum1lcm8gZGUgdmFyaWFibGVzIGVzIG11eSBncmFuZGUgZW4gY29tcGFyYWNpw7NuIGNvbiBlbCBuw7ptZXJvIGRlIG9ic2VydmFjaW9uZXMuIEVuIGVzdG9zIGNhc29zLCBsb3MgZXN0aW1hZG9yZXMgJFxoYXR7XGJldGF9JCBwdWVkZW4gc2VyIGluZXN0YWJsZXMgeSBnZW5lcmFyIG1vZGVsb3MgcG9jbyBpbnRlcnByZXRhYmxlcyBvIGNvbiBzb2JyZWFqdXN0ZS4gQXF1w60gZXMgZG9uZGUgZW50cmEgbGEgcmVndWxhcml6YWNpw7NuLgoKTGEgcmVndWxhcml6YWNpw7NuIGFncmVnYSB1biB0w6lybWlubyBkZSBwZW5hbGl6YWNpw7NuIGEgbGEgZnVuY2nDs24gZGUgcMOpcmRpZGEsIGNvbiBlbCBmaW4gZGUgcmVkdWNpciBsYSBjb21wbGVqaWRhZCBkZWwgbW9kZWxvLiBEb3MgbcOpdG9kb3MgY29tdW5lcyBzb24gUmlkZ2UgeSBMYXNzby4gUmlkZ2UgdXRpbGl6YSBsYSBub3JtYSBjdWFkcsOhdGljYSAkTF8yJCBwYXJhIHBlbmFsaXphciBncmFuZGVzIHZhbG9yZXMgZGUgbG9zIGNvZWZpY2llbnRlczoKCiQkXHRleHR7UlNTfSArIFxsYW1iZGEgXHN1bV97aj0xfV57cH0gXGJldGFfal4yJCQKCm1pZW50cmFzIHF1ZSBMYXNzbyB1dGlsaXphIGxhIG5vcm1hIGFic29sdXRhICRMXzEkOgoKJCRcdGV4dHtSU1N9ICsgXGxhbWJkYSBcc3VtX3tqPTF9XntwfSB8XGJldGFfanwkJAoKZG9uZGUgJFxsYW1iZGEkIGVzIHVuIGhpcGVycGFyw6FtZXRybyBxdWUgY29udHJvbGEgZWwgbml2ZWwgZGUgcGVuYWxpemFjacOzbi4KCkVzdG9zIG3DqXRvZG9zIHNpcnZlbiB0YW50byBwYXJhIG1lam9yYXIgbGEgY2FwYWNpZGFkIHByZWRpY3RpdmEgY29tbyBwYXJhIHNlbGVjY2lvbmFyIHZhcmlhYmxlcyAoZXNwZWNpYWxtZW50ZSBMYXNzbywgcXVlIHB1ZWRlIGxsZXZhciBhbGd1bm9zIGNvZWZpY2llbnRlcyBleGFjdGFtZW50ZSBhIGNlcm8pLiBTdXMgYXBsaWNhY2lvbmVzIGluY2x1eWVuIHByZWRpY2Npw7NuIGVuIGVjb25vbcOtYSwgYmlvbG9nw61hIChleHByZXNpw7NuIGdlbsOpdGljYSksIG1hcmtldGluZyAoYW7DoWxpc2lzIGRlIGNsaWVudGVzKSB5IGN1YWxxdWllciBlc2NlbmFyaW8gY29uIG11Y2hvcyBwcmVkaWN0b3JlcywgZG9uZGUgc2UgYnVzY2EgdW4gbW9kZWxvIG3DoXMgcm9idXN0byB5IGdlbmVyYWxpemFibGUuCgoqKkVqZW1wbG8gY29uIFIqKgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCnNldC5zZWVkKDEyMykKCiMgMS4gR2VuZXJhciBkYXRvcyBzaW11bGFkb3MKbiA8LSAxMDAKcCA8LSAxMApYIDwtIG1hdHJpeChybm9ybShuICogcCksIG4sIHApICAgICAgICAgICAgICAgIyAxMCBwcmVkaWN0b3JlcwpiZXRhIDwtIGMoMywgLTIsIDAsIDAsIDEuNSwgcmVwKDAsIHAgLSA1KSkgICAgIyBzb2xvIDMgcHJlZGljdG9yZXMgcmVsZXZhbnRlcwp5IDwtIFggJSolIGJldGEgKyBybm9ybShuKSAgICAgICAgICAgICAgICAgICAgIyB2YXJpYWJsZSByZXNwdWVzdGEgY29uIHJ1aWRvCgojIDIuIEFqdXN0YXIgbW9kZWxvcyBjb24gZ2xtbmV0CiMgYWxwaGEgPSAwIC0+IFJpZGdlLCBhbHBoYSA9IDEgLT4gTGFzc28KcmlkZ2VfZml0IDwtIGdsbW5ldChYLCB5LCBhbHBoYSA9IDApICAKbGFzc29fZml0IDwtIGdsbW5ldChYLCB5LCBhbHBoYSA9IDEpCgojIDMuIFZpc3VhbGl6YWNpw7NuOiB0cmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMKcGFyKG1mcm93ID0gYygxLCAyKSkgICAjIERvcyBncsOhZmljb3MgbGFkbyBhIGxhZG8KCnBsb3QocmlkZ2VfZml0LCB4dmFyID0gImxhbWJkYSIsIG1haW4gPSAiVHJheWVjdG9yaWEgUmlkZ2UgKEwyKSIpCnBsb3QobGFzc29fZml0LCB4dmFyID0gImxhbWJkYSIsIG1haW4gPSAiVHJheWVjdG9yaWEgTGFzc28gKEwxKSIpCgpgYGAKCgojIyAxMC4xKSBSZWdyZXNpw7NuIGxpbmVhbCBzaW1wbGUKCkxhIHJlZ3Jlc2nDs24gbGluZWFsIHNpbXBsZSBlcyB1biBtb2RlbG8gZXN0YWTDrXN0aWNvIHF1ZSBidXNjYSBkZXNjcmliaXIgbGEgcmVsYWNpw7NuIGVudHJlIGRvcyB2YXJpYWJsZXM6IHVuYSBpbmRlcGVuZGllbnRlIG8gZXhwbGljYXRpdmEgKCRYJCkgeSB1bmEgZGVwZW5kaWVudGUgbyBkZSByZXNwdWVzdGEgKCRZJCkuIFN1IG9iamV0aXZvIGVzIGVzdGltYXIgY8OzbW8gY2FtYmlhICRZJCBlbiBmdW5jacOzbiBkZSBsb3MgdmFsb3JlcyBkZSAkWCQuIE1hdGVtw6F0aWNhbWVudGUsIHNlIGV4cHJlc2EgY29tbzoKCiQkWSA9IFxiZXRhXzAgKyBcYmV0YV8xIFggKyBcdmFyZXBzaWxvbiQkCgpkb25kZSAkXGJldGFfMCQgZXMgbGEgaW50ZXJzZWNjacOzbiAob3JkZW5hZGEgYWwgb3JpZ2VuKSwgJFxiZXRhXzEkIGVzIGxhIHBlbmRpZW50ZSBxdWUgbWlkZSBlbCBlZmVjdG8gZGUgJFgkIHNvYnJlICRZJCwgeSAkXHZhcmVwc2lsb24kIGVzIGVsIHTDqXJtaW5vIGRlIGVycm9yIGFsZWF0b3Jpby4KCkVzdGUgbW9kZWxvIHNpcnZlIHByaW5jaXBhbG1lbnRlIHBhcmEgZXhwbGljYXIgeSBwcmVkZWNpci4gRXhwbGljYSBhbCBpZGVudGlmaWNhciBzaSBleGlzdGUgdW5hIHJlbGFjacOzbiBzaWduaWZpY2F0aXZhIGVudHJlIGRvcyB2YXJpYWJsZXMsIHkgcHJlZGljZSBhbCBwZXJtaXRpciBlc3RpbWFyIGVsIHZhbG9yIGVzcGVyYWRvIGRlICRZJCBwYXJhIHVuIGRldGVybWluYWRvIHZhbG9yIGRlICRYJC4gUG9yIGVqZW1wbG8sIHNpICRYJCBlcyBsYSBjYW50aWRhZCBkZSBob3JhcyBkZSBlc3R1ZGlvIHkgJFkkIGVzIGxhIGNhbGlmaWNhY2nDs24gZW4gdW4gZXhhbWVuLCBsYSByZWdyZXNpw7NuIGxpbmVhbCBzaW1wbGUgcHVlZGUgbW9zdHJhciBxdcOpIHRhbnRvIGluZmx1eWVuIGxhcyBob3JhcyBkZSBlc3R1ZGlvIGVuIGxvcyByZXN1bHRhZG9zLgoKRW50cmUgbG9zIGNhc29zIGRlIHVzbyBtw6FzIGNvbXVuZXMgZXN0w6FuOiBlY29ub23DrWEgKHJlbGFjacOzbiBlbnRyZSBpbmdyZXNvIHkgY29uc3VtbyksIHNhbHVkIChlZmVjdG8gZGUgdW5hIGRvc2lzIGRlIG1lZGljYW1lbnRvIHNvYnJlIHVuYSByZXNwdWVzdGEgY2zDrW5pY2EpLCBlZHVjYWNpw7NuIChpbXBhY3RvIGRlIGxhIGFzaXN0ZW5jaWEgYSBjbGFzZXMgZW4gZWwgcmVuZGltaWVudG8pLCBlIGluZ2VuaWVyw61hIChyZWxhY2nDs24gZW50cmUgbGEgdGVtcGVyYXR1cmEgeSBsYSByZXNpc3RlbmNpYSBkZSB1biBtYXRlcmlhbCkuIEVuIHRvZG9zIGVzdG9zIGNhc29zLCBsbyBpbXBvcnRhbnRlIGVzIHF1ZSBoYXlhIHVuYSByZWxhY2nDs24gcXVlIHNlIHB1ZWRhIGFwcm94aW1hciBkZSBtYW5lcmEgbGluZWFsLgoKRW4gbGEgcHLDoWN0aWNhLCBsYSByZWdyZXNpw7NuIGxpbmVhbCBzaW1wbGUgc2UgZXN0aW1hIG1lZGlhbnRlIGVsIG3DqXRvZG8gZGUgbcOtbmltb3MgY3VhZHJhZG9zIG9yZGluYXJpb3MgKE1DTyksIHF1ZSBjb25zaXN0ZSBlbiBlbmNvbnRyYXIgbG9zIHZhbG9yZXMgZGUgJFxoYXR7XGJldGF9XzAkIHkgJFxoYXR7XGJldGF9XzEkIHF1ZSBtaW5pbWljZW4gbGEgc3VtYSBkZSBsb3MgZXJyb3JlcyBjdWFkcsOhdGljb3M6CgokJFxtaW4gXHN1bV97aT0xfV5uICh5X2kgLSBcaGF0e3l9X2kpXjIgXDs9XDsgXG1pbiBcc3VtX3tpPTF9Xm4gXGxlZnQoeV9pIC0gKFxoYXR7XGJldGF9XzAgKyBcaGF0e1xiZXRhfV8xIHhfaSlccmlnaHQpXjIkJApFc3RlIHByb2NlZGltaWVudG8gZ2FyYW50aXphIGxhIG1lam9yIHJlY3RhIGRlIGFqdXN0ZSBwb3NpYmxlIGJham8gbG9zIHN1cHVlc3RvcyBkZWwgbW9kZWxvIChsaW5lYWxpZGFkLCBpbmRlcGVuZGVuY2lhLCBob21vY2VkYXN0aWNpZGFkIHkgbm9ybWFsaWRhZCBkZSBlcnJvcmVzKS4KCioqRWplbXBsbyBjb24gUioqCmBgYHtyfQoKCiMgU2ltdWxhY2nDs24gZGUgZGF0b3M6IEhvcmFzIGRlIGVzdHVkaW8gKFgpIHkgTm90YSBkZSBleGFtZW4gKFkpCnNldC5zZWVkKDEyMykKaG9yYXMgPC0gcnVuaWYoNTAsIDAsIDEwKSAgIyA1MCBvYnNlcnZhY2lvbmVzIGVudHJlIDAgeSAxMCBob3Jhcwpub3RhIDwtIDQwICsgNSAqIGhvcmFzICsgcm5vcm0oNTAsIDAsIDUpICAjIFJlbGFjacOzbiBsaW5lYWwgY29uIGFsZ28gZGUgcnVpZG8KZGF0b3MgPC0gZGF0YS5mcmFtZShob3Jhcywgbm90YSkKCiMgQWp1c3RlIGRlbCBtb2RlbG8gZGUgcmVncmVzacOzbiBsaW5lYWwgc2ltcGxlCm1vZGVsbyA8LSBsbShub3RhIH4gaG9yYXMsIGRhdGEgPSBkYXRvcykKCiMgUmVzdW1lbiAibGltcGlvIiBkZWwgbW9kZWxvCnRpZHkobW9kZWxvKQoKIyBHcsOhZmljbyBjb24gcmVjdGEgZGUgcmVncmVzacOzbgpwIDwtIGdncGxvdChkYXRvcywgYWVzKHggPSBob3JhcywgeSA9IG5vdGEpKSArCiAgZ2VvbV9wb2ludChjb2xvciA9ICJzdGVlbGJsdWUiLCBzaXplID0gMywgYWxwaGEgPSAwLjcpICsgICAjIHB1bnRvcwogIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIsIHNlID0gVFJVRSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDEpICsgIyByZWN0YQogIHN0YXRfcmVnbGluZV9lcXVhdGlvbiggICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgZWN1YWNpw7NuIGVuIGdyw6FmaWNvCiAgICBhZXMobGFiZWwgPSBwYXN0ZSguLmVxLmxhYmVsLi4sIC4ucnIubGFiZWwuLiwgc2VwID0gIn5+fiIpKSwKICAgIGZvcm11bGEgPSB5IH4geAogICkgKwogIGxhYnMoCiAgICB0aXRsZSA9ICJFamVtcGxvIGRlIFJlZ3Jlc2nDs24gTGluZWFsIFNpbXBsZSIsCiAgICB4ID0gIkhvcmFzIGRlIGVzdHVkaW8iLAogICAgeSA9ICJOb3RhIGVuIGVsIGV4YW1lbiIKICApICsKICB0aGVtZV9taW5pbWFsKGJhc2Vfc2l6ZSA9IDE0KQoKcHJpbnQocCkKCmBgYAoKIyMgMTAuMikgUmVncmVzacOzbiBsaW5lYWwgbcO6bHRpcGxlCgpMYSByZWdyZXNpw7NuIGxpbmVhbCBtw7psdGlwbGUgZXMgdW5hIGV4dGVuc2nDs24gZGUgbGEgcmVncmVzacOzbiBsaW5lYWwgc2ltcGxlIHF1ZSBwZXJtaXRlIGFuYWxpemFyIGxhIHJlbGFjacOzbiBlbnRyZSB1bmEgdmFyaWFibGUgZGVwZW5kaWVudGUgJFkkIHkgdmFyaWFzIHZhcmlhYmxlcyBpbmRlcGVuZGllbnRlcyAkWF8xLCBYXzIsIFxkb3RzLCBYX3AkLiBTdSBmb3JtYSBnZW5lcmFsIGVzOgoKJCRZID0gXGJldGFfMCArIFxiZXRhXzEgWF8xICsgXGJldGFfMiBYXzIgKyBcZG90cyArIFxiZXRhX3AgWF9wICsgXHZhcmVwc2lsb24KJCQKCmRvbmRlICRcYmV0YV8wJCBlcyBsYSBjb25zdGFudGUsICRcYmV0YV9qJCBsb3MgY29lZmljaWVudGVzIHF1ZSByZXByZXNlbnRhbiBlbCBlZmVjdG8gcGFyY2lhbCBkZSBjYWRhICRYX2okIHNvYnJlICRZJCwgeSAkXHZhcmVwc2lsb24kIGVsIGVycm9yIGFsZWF0b3Jpby4KCkVzdGUgbW9kZWxvIHNpcnZlIHBhcmEgZXhwbGljYXIsIGN1YW50aWZpY2FyIHkgcHJlZGVjaXIgZmVuw7NtZW5vcyBtw6FzIGNvbXBsZWpvcyBxdWUgbm8gZGVwZW5kZW4gZGUgdW5hIHNvbGEgdmFyaWFibGUuIENhZGEgY29lZmljaWVudGUgJFxiZXRhX2okIGluZGljYSBjdcOhbnRvIGNhbWJpYSBlbiBwcm9tZWRpbyAkWSQgYWwgdmFyaWFyICRYX2okIGVuIHVuYSB1bmlkYWQsIG1hbnRlbmllbmRvIGNvbnN0YW50ZXMgbGFzIGRlbcOhcyB2YXJpYWJsZXMuIERlIGVzdGEgZm9ybWEsIHNlIHB1ZWRlbiBpZGVudGlmaWNhciBsb3MgZmFjdG9yZXMgbcOhcyBpbmZsdXllbnRlcyB5IGPDs21vIGludGVyYWN0w7phbiBlbnRyZSBzw60uCgpMb3MgY2Fzb3MgZGUgdXNvIHNvbiBhbXBsaW9zOiBlbiBlY29ub23DrWEsIHByZWRlY2lyIGVsIGNvbnN1bW8gYSBwYXJ0aXIgZGUgaW5ncmVzbywgdGFzYSBkZSBpbnRlcsOpcyB5IGRlc2VtcGxlbzsgZW4gc2FsdWQsIGV4cGxpY2FyIGxhIHByZXNpw7NuIGFydGVyaWFsIHNlZ8O6biBlZGFkLCBwZXNvIHkgbml2ZWwgZGUgYWN0aXZpZGFkIGbDrXNpY2E7IGVuIG1hcmtldGluZywgZXN0aW1hciBsYXMgdmVudGFzIGEgcGFydGlyIGRlIGludmVyc2nDs24gZW4gcHVibGljaWRhZCwgcHJlY2lvIHkgY29tcGV0ZW5jaWE7IGVuIGluZ2VuaWVyw61hLCBtb2RlbGFyIGxhIHJlc2lzdGVuY2lhIGRlIHVuIG1hdGVyaWFsIGVuIGZ1bmNpw7NuIGRlIHRlbXBlcmF0dXJhLCBodW1lZGFkIHkgcHJlc2nDs24uCgpFbCBtb2RlbG8gc2UgZXN0aW1hIGdlbmVyYWxtZW50ZSBjb24gbcOtbmltb3MgY3VhZHJhZG9zIG9yZGluYXJpb3MgKE1DTyksIG1pbmltaXphbmRvIGxhIHN1bWEgZGUgbG9zIGVycm9yZXMgYWwgY3VhZHJhZG86CgokJFxtaW4gXHN1bV97aT0xfV5uIFxsZWZ0KCB5X2kgLSBcbGVmdCggXGJldGFfMCArIFxiZXRhXzEgeF97aTF9ICsgXGJldGFfMiB4X3tpMn0gKyBcZG90cyArIFxiZXRhX3AgeF97aXB9IFxyaWdodCkgXHJpZ2h0KV4yCiQkCkJham8gc3VwdWVzdG9zIGNvbW8gbGluZWFsaWRhZCwgaW5kZXBlbmRlbmNpYSwgaG9tb2NlZGFzdGljaWRhZCB5IG5vcm1hbGlkYWQgZGUgbG9zIGVycm9yZXMsIGVsIGVzdGltYWRvciBvYnRlbmlkbyBlcyBpbnNlc2dhZG8geSBlZmljaWVudGUsIGxvIHF1ZSBoYWNlIGRlIGxhIHJlZ3Jlc2nDs24gbcO6bHRpcGxlIHVuYSBoZXJyYW1pZW50YSBmdW5kYW1lbnRhbCBlbiBlc3RhZMOtc3RpY2EgYXBsaWNhZGEuCgoKKipFamVtcGxvIGNvbiBSKioKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CgoKIyBEYXRhc2V0CmRhdGEobXRjYXJzKQoKIyBBanVzdGUgZGVsIG1vZGVsbzogbXBnIGV4cGxpY2FkbyBwb3IgcGVzbyAod3QpIHkgcG90ZW5jaWEgKGhwKQptb2RlbG8gPC0gbG0obXBnIH4gd3QgKyBocCwgZGF0YSA9IG10Y2FycykKCiMgUmVzdW1lbiAibGltcGlvIiBkZWwgbW9kZWxvCnRpZHkobW9kZWxvKQoKIyBEaWFnbsOzc3RpY28gZGUgbXVsdGljb2xpbmVhbGlkYWQKdmlmKG1vZGVsbykKCiMgR3LDoWZpY286IGRpc3BlcnNpw7NuIDNEIHByb3llY3RhZGEgKG1wZyB+IHd0ICsgaHApCmdncGxvdChtdGNhcnMsIGFlcyh4ID0gd3QsIHkgPSBtcGcsIGNvbG9yID0gaHApKSArCiAgZ2VvbV9wb2ludChzaXplID0gMywgYWxwaGEgPSAwLjgpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSAibG0iLCBzZSA9IEZBTFNFLCBhZXMoZ3JvdXAgPSAxKSwgY29sb3IgPSAicmVkIikgKwogIHNjYWxlX2NvbG9yX2dyYWRpZW50KGxvdyA9ICJza3libHVlIiwgaGlnaCA9ICJkYXJrYmx1ZSIpICsKICBsYWJzKAogICAgdGl0bGUgPSAiUmVncmVzacOzbiBMaW5lYWwgTcO6bHRpcGxlOiBtcGcgfiB3dCArIGhwIiwKICAgIHggPSAiUGVzbyBkZWwgdmVow61jdWxvICgxMDAwIGxicykiLAogICAgeSA9ICJNaWxsYXMgcG9yIGdhbMOzbiAobXBnKSIsCiAgICBjb2xvciA9ICJQb3RlbmNpYSAoaHApIgogICkgKwogIHRoZW1lX21pbmltYWwoYmFzZV9zaXplID0gMTQpCgojIFBhcmVzIGRlIHJlbGFjaW9uZXMgZW50cmUgdmFyaWFibGVzICjDunRpbCBwYXJhIGV4cGxvcmFyIGVsIG1vZGVsbykKZ2dwYWlycyhtdGNhcnNbLCBjKCJtcGciLCAid3QiLCAiaHAiKV0pCgpgYGAKCgoqKk1lam9yIGZvcm1hIGRlIGVsZWdpciBsb3MgcHJlZGljdG9yZXMgcXVlIGFwb3J0YW4gbWFzIGEgZWwgbW9kZWxvKioKCkxhIG1lam9yIGZvcm1hIGRlIGVsZWdpciBwcmVkaWN0b3JlcyBxdWUgbcOhcyBhcG9ydGFuIGEgdW4gbW9kZWxvIHNlIGNvbm9jZSBjb21vIHNlbGVjY2nDs24gZGUgdmFyaWFibGVzIG8gc2VsZWNjacOzbiBkZSBjYXJhY3RlcsOtc3RpY2FzLiBFbCBvYmpldGl2byBlcyBpZGVudGlmaWNhciBxdcOpIHN1YmNvbmp1bnRvIGRlIHZhcmlhYmxlcyBleHBsaWNhdGl2YXMgJFhfMSwgWF8yLCBcZG90cywgWF9wJCB0aWVuZSBtYXlvciBwb2RlciBleHBsaWNhdGl2byBzb2JyZSBsYSB2YXJpYWJsZSByZXNwdWVzdGEgJFkkLiBFc3RvIGF5dWRhIGEgbWVqb3JhciBsYSBpbnRlcnByZXRhYmlsaWRhZCwgcmVkdWNpciBsYSBjb21wbGVqaWRhZCBkZWwgbW9kZWxvIHksIGVuIG11Y2hvcyBjYXNvcywgYXVtZW50YXIgc3UgY2FwYWNpZGFkIGRlIGdlbmVyYWxpemFjacOzbi4KCkV4aXN0ZW4gdmFyaWFzIGVzdHJhdGVnaWFzOiBsYSBzZWxlY2Npw7NuIGhhY2lhIGFkZWxhbnRlIChhw7FhZGllbmRvIHZhcmlhYmxlcyB1bmEgYSB1bmEpLCBsYSBzZWxlY2Npw7NuIGhhY2lhIGF0csOhcyAoZWxpbWluYW5kbyBwcmVkaWN0b3JlcyBpcnJlbGV2YW50ZXMpLCB5IGxhIHNlbGVjY2nDs24gZXhoYXVzdGl2YSwgcXVlIGV2YWzDumEgdG9kb3MgbG9zIHN1YmNvbmp1bnRvcyBwb3NpYmxlcyBkZSBwcmVkaWN0b3Jlcy4gTG9zIGNyaXRlcmlvcyBtw6FzIHVzYWRvcyBwYXJhIGVsZWdpciBlbCBtZWpvciBzdWJjb25qdW50byBzb24gbWVkaWRhcyBjb21vICRSXjIkLCAkUl4yJCBhanVzdGFkbywgJENfcCQgZGUgTWFsbG93cywgQUlDIHkgQklDLgoKRm9ybWFsbWVudGUsIHNlIGJ1c2NhIGVuY29udHJhciBlbCBzdWJjb25qdW50byBkZSB2YXJpYWJsZXMgJFMgXHN1YnNldGVxIHsxLFxkb3RzLHB9JCBxdWUgbWluaW1pemEgdW5hIG1lZGlkYSBkZSBlcnJvciBkZSBhanVzdGU6CiQkU157Kn0gPSBcYXJnXG1pbl97U30gXDsgXHRleHR7QUlDfShTKSQkIHkgJCRTXnsqfSA9IFxhcmdcbWluX3tTfSBcOyBcdGV4dHtCSUN9KFMpJCQKCmRvbmRlICRcdGV4dHtBSUN9JCB5ICRcdGV4dHtCSUN9JCBzb24gY3JpdGVyaW9zIGRlIGluZm9ybWFjacOzbiBxdWUgZXF1aWxpYnJhbiBlbCBhanVzdGUgeSBsYSBjb21wbGVqaWRhZCBkZWwgbW9kZWxvLgoKTG9zIGNhc29zIGRlIHVzbyBzb24gYW1wbGlvczogZW4gZWNvbm9tw61hLCBzZWxlY2Npb25hciBpbmRpY2Fkb3JlcyBjbGF2ZSBwYXJhIHVuIG1vZGVsbyBkZSBjcmVjaW1pZW50bzsgZW4gc2FsdWQsIGlkZW50aWZpY2FyIGZhY3RvcmVzIGRlIHJpZXNnbyBtw6FzIHJlbGV2YW50ZXMgcGFyYSB1bmEgZW5mZXJtZWRhZDsgZW4gbWFya2V0aW5nLCBlc2NvZ2VyIGxhcyB2YXJpYWJsZXMgZGUgY2FtcGHDsWFzIHF1ZSBtw6FzIGV4cGxpY2FuIGxhcyB2ZW50YXM7IHkgZW4gbWFjaGluZSBsZWFybmluZywgcmVkdWNpciBsYSBkaW1lbnNpb25hbGlkYWQgZGUgZGF0b3MgcGFyYSBldml0YXIgc29icmVhanVzdGUuCgoqKkVqZW1wbG8gY29uIFIqKgpgYGB7cn0KCgojIERhdGFzZXQ6IG10Y2FycywgcHJlZGVjaXIgbXBnIGEgcGFydGlyIGRlIHZhcmlhcyB2YXJpYWJsZXMKZGF0YShtdGNhcnMpCgojIFNlbGVjY2nDs24gZGUgc3ViY29uanVudG9zIGNvbiBsZWFwcwphanVzdGUgPC0gcmVnc3Vic2V0cyhtcGcgfiB3dCArIGhwICsgZGlzcCArIGN5bCArIGRyYXQgKyBxc2VjLCAKICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IG10Y2FycywgCiAgICAgICAgICAgICAgICAgICAgIG5iZXN0ID0gMSwgICAjIG1lam9yIHN1YmNvbmp1bnRvIHBvciB0YW1hw7FvCiAgICAgICAgICAgICAgICAgICAgIG52bWF4ID0gNikgICAjIG3DoXhpbW8gbsO6bWVybyBkZSB2YXJpYWJsZXMKCiMgUmVzdW1lbiBkZSByZXN1bHRhZG9zCnJlcyA8LSBzdW1tYXJ5KGFqdXN0ZSkKCiMgVmlzdWFsaXphY2nDs24gZGVsIFJeMiBhanVzdGFkbwpkZl9wbG90IDwtIGRhdGEuZnJhbWUoCiAgTnVtX1ZhcmlhYmxlcyA9IDE6bGVuZ3RoKHJlcyRhZGpyMiksCiAgQWRqX1IyID0gcmVzJGFkanIyCikKCmdncGxvdChkZl9wbG90LCBhZXMoeCA9IE51bV9WYXJpYWJsZXMsIHkgPSBBZGpfUjIpKSArCiAgZ2VvbV9saW5lKGNvbG9yID0gInN0ZWVsYmx1ZSIsIHNpemUgPSAxLjIpICsKICBnZW9tX3BvaW50KGNvbG9yID0gInJlZCIsIHNpemUgPSAzKSArCiAgbGFicygKICAgIHRpdGxlID0gIlNlbGVjY2nDs24gZGUgdmFyaWFibGVzIGNvbiBsZWFwcyIsCiAgICB4ID0gIk7Dum1lcm8gZGUgdmFyaWFibGVzIGVuIGVsIG1vZGVsbyIsCiAgICB5ID0gIlLCsiBBanVzdGFkbyIKICApICsKICB0aGVtZV9taW5pbWFsKGJhc2Vfc2l6ZSA9IDE0KQoKYGBgCgoKIyMgMTAuMykgUmVndWxhcml6YWNpw7NuCgpMYSByZWd1bGFyaXphY2nDs24gZXMgdW5hIHTDqWNuaWNhIHV0aWxpemFkYSBlbiBtb2RlbG9zIGVzdGFkw61zdGljb3MgeSBkZSBtYWNoaW5lIGxlYXJuaW5nLCBlc3BlY2lhbG1lbnRlIGVuIGxhIHJlZ3Jlc2nDs24gbGluZWFsIG3Dumx0aXBsZSwgcGFyYSBldml0YXIgZWwgc29icmVhanVzdGUgKG92ZXJmaXR0aW5nKS4gRWwgc29icmVhanVzdGUgb2N1cnJlIGN1YW5kbyB1biBtb2RlbG8gc2UgYWRhcHRhIGRlbWFzaWFkbyBhIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvLCBwZXJkaWVuZG8gY2FwYWNpZGFkIGRlIGdlbmVyYWxpemFjacOzbi4gTGEgaWRlYSBjZW50cmFsIGRlIGxhIHJlZ3VsYXJpemFjacOzbiBlcyBhw7FhZGlyIHVuIHTDqXJtaW5vIGRlIHBlbmFsaXphY2nDs24gYSBsYSBmdW5jacOzbiBkZSBlcnJvciwgZGUgbW9kbyBxdWUgbG9zIGNvZWZpY2llbnRlcyAkXGJldGFfaiQgbm8gY3JlemNhbiBkZW1hc2lhZG8uCgpFeGlzdGVuIGRvcyBtw6l0b2RvcyBjbMOhc2ljb3M6IFJpZGdlIChMMikgeSBMYXNzbyAoTDEpLiBFbiBSaWRnZSwgbGEgcGVuYWxpemFjacOzbiBlcyBwcm9wb3JjaW9uYWwgYSBsYSBzdW1hIGRlIGxvcyBjdWFkcmFkb3MgZGUgbG9zIGNvZWZpY2llbnRlczoKCiQkXG1pbiBcc3VtX3tpPTF9Xm4gXGxlZnQoeV9pIC0gXGhhdHt5fV9pXHJpZ2h0KV4yICsgXGxhbWJkYSBcc3VtX3tqPTF9XnAgXGJldGFfal4yJCQKCkVuIExhc3NvLCBsYSBwZW5hbGl6YWNpw7NuIGVzIHByb3BvcmNpb25hbCBhIGxhIHN1bWEgZGVsIHZhbG9yIGFic29sdXRvIGRlIGxvcyBjb2VmaWNpZW50ZXM6CgokJFxtaW4gXHN1bV97aT0xfV5uIFxsZWZ0KHlfaSAtIFxoYXR7eX1faVxyaWdodCleMiArIFxsYW1iZGEgXHN1bV97aj0xfV5wIFxsZWZ0fFxiZXRhX2pccmlnaHR8CiQkCmRvbmRlICRcbGFtYmRhIFxnZXEgMCQgZXMgdW4gaGlwZXJwYXLDoW1ldHJvIHF1ZSBjb250cm9sYSBsYSBmdWVyemEgZGUgbGEgcGVuYWxpemFjacOzbi4KCkxhIHJlZ3VsYXJpemFjacOzbiBzaXJ2ZSBwYXJhIG1lam9yYXIgbGEgZXN0YWJpbGlkYWQgZGVsIG1vZGVsbyB5IHNlbGVjY2lvbmFyIHZhcmlhYmxlcyByZWxldmFudGVzLiBFbiBSaWRnZSwgbG9zIGNvZWZpY2llbnRlcyBzZSByZWR1Y2VuIHBlcm8gbnVuY2Egc29uIGV4YWN0YW1lbnRlIGNlcm87IGVuIExhc3NvLCBhbGd1bm9zIGNvZWZpY2llbnRlcyBwdWVkZW4gdm9sdmVyc2UgZXhhY3RhbWVudGUgY2VybywgZnVuY2lvbmFuZG8gYWRlbcOhcyBjb21vIG3DqXRvZG8gZGUgc2VsZWNjacOzbiBkZSB2YXJpYWJsZXMuCgpMb3MgY2Fzb3MgZGUgdXNvIGluY2x1eWVuIG1vZGVsb3MgY29uIG11Y2hhcyB2YXJpYWJsZXMgKHAuZWouLCBlbiBnZW7Ds21pY2EsIHRleHRvLCBtYXJrZXRpbmcgZGlnaXRhbCksIHByb2JsZW1hcyBkZSBtdWx0aWNvbGluZWFsaWRhZCBkb25kZSB2YXJpYXMgJFhfaiQgZXN0w6FuIGNvcnJlbGFjaW9uYWRhcywgeSBzaXR1YWNpb25lcyBkb25kZSBpbnRlcmVzYSByZWR1Y2lyIGxhIGNvbXBsZWppZGFkIGRlbCBtb2RlbG8gc2luIHBlcmRlciBjYXBhY2lkYWQgcHJlZGljdGl2YS4gRW4gcHLDoWN0aWNhLCBsYSByZWd1bGFyaXphY2nDs24gYXl1ZGEgYSBjb25zdHJ1aXIgbW9kZWxvcyBtw6FzIHJvYnVzdG9zIHkgY29uIG1lam9yIGRlc2VtcGXDsW8gZW4gZGF0b3MgbnVldm9zLgoKCioqRWplbXBsbyBjb24gUioqCgpgYGB7cn0KCgojIERhdGFzZXQ6IG10Y2FycywgdmFyaWFibGVzIHByZWRpY3RvcmFzICh3dCwgaHAsIGRpc3AsIGN5bCkgcGFyYSBwcmVkZWNpciBtcGcKeCA8LSBhcy5tYXRyaXgobXRjYXJzWywgYygid3QiLCAiaHAiLCAiZGlzcCIsICJjeWwiKV0pCnkgPC0gbXRjYXJzJG1wZwoKIyBBanVzdGUgY29uIExhc3NvIChhbHBoYSA9IDEpCm1vZGVsb19sYXNzbyA8LSBnbG1uZXQoeCwgeSwgYWxwaGEgPSAxKQoKIyBBanVzdGUgY29uIFJpZGdlIChhbHBoYSA9IDApCm1vZGVsb19yaWRnZSA8LSBnbG1uZXQoeCwgeSwgYWxwaGEgPSAwKQoKIyBWaXN1YWxpemFjacOzbiBkZSB0cmF5ZWN0b3JpYXMgZGUgY29lZmljaWVudGVzIChwYXRoIHBsb3QpCnBhcihtZnJvdyA9IGMoMSwyKSkKcGxvdChtb2RlbG9fbGFzc28sIHh2YXIgPSAibGFtYmRhIiwgbGFiZWwgPSBUUlVFLCBtYWluID0gIkxhc3NvIChMMSkiKQpwbG90KG1vZGVsb19yaWRnZSwgeHZhciA9ICJsYW1iZGEiLCBsYWJlbCA9IFRSVUUsIG1haW4gPSAiUmlkZ2UgKEwyKSIpCgpgYGAKCgoKIyMgMTAuNCkgUmVndWxhcml6YWNpw7NuIFJpZGdlCgpMYSByZWd1bGFyaXphY2nDs24gUmlkZ2UsIHRhbWJpw6luIGxsYW1hZGEgcmVncmVzacOzbiBkZSBjcmVzdGEsIGVzIHVuYSB0w6ljbmljYSBkZSByZWdyZXNpw7NuIGxpbmVhbCByZWd1bGFyaXphZGEgcXVlIGJ1c2NhIG1lam9yYXIgbGEgZXN0YWJpbGlkYWQgeSBjYXBhY2lkYWQgZGUgcHJlZGljY2nDs24gZGVsIG1vZGVsbyBjdWFuZG8gZXhpc3RlbiBtdWNoYXMgdmFyaWFibGVzIG8gcHJvYmxlbWFzIGRlIG11bHRpY29saW5lYWxpZGFkLiBBIGRpZmVyZW5jaWEgZGUgbGEgcmVncmVzacOzbiBsaW5lYWwgdHJhZGljaW9uYWwsIFJpZGdlIGHDsWFkZSB1biB0w6lybWlubyBkZSBwZW5hbGl6YWNpw7NuIGN1YWRyw6F0aWNvIGEgbGEgZnVuY2nDs24gZGUgZXJyb3IsIGxvIHF1ZSBpbXBpZGUgcXVlIGxvcyBjb2VmaWNpZW50ZXMgJFxiZXRhX2okIGNyZXpjYW4gZGVtYXNpYWRvLgoKRm9ybWFsbWVudGUsIGVsIHByb2JsZW1hIGRlIFJpZGdlIHNlIHBsYW50ZWEgY29tbzoKCiQkXG1pbiBcc3VtX3tpPTF9Xm4gXGxlZnQoIHlfaSAtIFxoYXR7eX1faSBccmlnaHQpXjIgKyBcbGFtYmRhIFxzdW1fe2o9MX1ecCBcbGVmdHwgXGJldGFfaiBccmlnaHR8CiQkCmRvbmRlICRcbGFtYmRhIFxnZXEgMCQgZXMgZWwgaGlwZXJwYXLDoW1ldHJvIGRlIHJlZ3VsYXJpemFjacOzbi4gQSBtYXlvciAkXGxhbWJkYSQsIG3DoXMgZnVlcnRlIGVzIGxhIHBlbmFsaXphY2nDs24sIHkgbcOhcyBzZSByZWR1Y2VuIGxvcyBjb2VmaWNpZW50ZXMgaGFjaWEgY2VybywgYXVucXVlIG51bmNhIGxsZWdhbiBhIHNlciBleGFjdGFtZW50ZSBjZXJvLgoKUmlkZ2Ugc2lydmUgcGFyYSBtZWpvcmFyIGxhIGdlbmVyYWxpemFjacOzbiBkZWwgbW9kZWxvLCByZWR1Y2llbmRvIGxhIHZhcmlhbnphIGRlIGxvcyBlc3RpbWFkb3JlcyBhIGNvc3RhIGRlIGludHJvZHVjaXIgdW4gcGVxdWXDsW8gc2VzZ28uIEVzIMO6dGlsIGVuIGNvbnRleHRvcyBkb25kZSBoYXkgbXVjaGFzIHZhcmlhYmxlcyBwcmVkaWN0b3JhcyB5IGNvcnJlbGFjaW9uZXMgYWx0YXMgZW50cmUgZWxsYXMsIHlhIHF1ZSBjb250cm9sYSBsYSBpbmVzdGFiaWxpZGFkIGRlIGxvcyBjb2VmaWNpZW50ZXMuCgpMb3MgY2Fzb3MgZGUgdXNvIHNvbiBjb211bmVzIGVuIHByb2JsZW1hcyBkZSBwcmVkaWNjacOzbiBjb24gYWx0YSBkaW1lbnNpb25hbGlkYWQ6IGFuw6FsaXNpcyBkZSBkYXRvcyBnZW7Ds21pY29zLCBzZXJpZXMgZGUgdGllbXBvIGNvbiBtw7psdGlwbGVzIHByZWRpY3RvcmVzLCBtb2RlbG9zIGRlIHJpZXNnbyBlbiBmaW5hbnphcyB5IGFwbGljYWNpb25lcyBkZSBtYWNoaW5lIGxlYXJuaW5nIGRvbmRlIHNlIHJlcXVpZXJlIHVuIG1vZGVsbyByb2J1c3RvIHNpbiBzb2JyZWFqdXN0ZS4KCioqRWplbXBsbyBjb24gUioqCmBgYHtyfQoKCiMgRGF0YXNldDogbXRjYXJzLCB2YXJpYWJsZXMgcHJlZGljdG9yYXMgcGFyYSBwcmVkZWNpciBtcGcKeCA8LSBhcy5tYXRyaXgobXRjYXJzWywgYygid3QiLCAiaHAiLCAiZGlzcCIsICJjeWwiKV0pCnkgPC0gbXRjYXJzJG1wZwoKIyBBanVzdGUgY29uIFJpZGdlIChhbHBoYSA9IDApCm1vZGVsb19yaWRnZSA8LSBnbG1uZXQoeCwgeSwgYWxwaGEgPSAwKQoKIyBWYWxpZGFjacOzbiBjcnV6YWRhIHBhcmEgZWxlZ2lyIGVsIG1lam9yIGxhbWJkYQpjdl9yaWRnZSA8LSBjdi5nbG1uZXQoeCwgeSwgYWxwaGEgPSAwKQoKIyBNZWpvciB2YWxvciBkZSBsYW1iZGEKY3ZfcmlkZ2UkbGFtYmRhLm1pbgoKIyBHcsOhZmljbyBkZSB0cmF5ZWN0b3JpYXMgZGUgY29lZmljaWVudGVzCnBsb3QobW9kZWxvX3JpZGdlLCB4dmFyID0gImxhbWJkYSIsIGxhYmVsID0gVFJVRSwgbWFpbiA9ICJUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMgKFJpZGdlKSIpCgojIEdyw6FmaWNvIGRlIHZhbGlkYWNpw7NuIGNydXphZGEKcGxvdChjdl9yaWRnZSkKCmBgYAoKCiMjIDEwLjUpIFJlZ3VsYXJpemFjacOzbiBMYXpvCgpMYSByZWd1bGFyaXphY2nDs24gTGFzc28gKExlYXN0IEFic29sdXRlIFNocmlua2FnZSBhbmQgU2VsZWN0aW9uIE9wZXJhdG9yKSBlcyB1bmEgdmFyaWFudGUgZGUgbGEgcmVncmVzacOzbiBsaW5lYWwgcmVndWxhcml6YWRhIHF1ZSwgYWRlbcOhcyBkZSBjb250cm9sYXIgZWwgc29icmVhanVzdGUsIHBlcm1pdGUgcmVhbGl6YXIgc2VsZWNjacOzbiBhdXRvbcOhdGljYSBkZSB2YXJpYWJsZXMuIEFsIGlndWFsIHF1ZSBSaWRnZSwgYcOxYWRlIHVuIHTDqXJtaW5vIGRlIHBlbmFsaXphY2nDs24gYSBsYSBmdW5jacOzbiBkZSBlcnJvciwgcGVybyBlbiBlc3RlIGNhc28gc2UgdXRpbGl6YSBsYSBub3JtYSAkTDEkIGVuIGx1Z2FyIGRlIGxhICRMMiQuCgpGb3JtYWxtZW50ZSwgZWwgcHJvYmxlbWEgZGUgTGFzc28gc2UgZGVmaW5lIGNvbW86CiQkXG1pbiBcc3VtX3tpPTF9Xm4gXGxlZnQoIHlfaSAtIFxoYXR7eX1faSBccmlnaHQpXjIKJCQKCmRvbmRlICRcbGFtYmRhIFxnZXEgMCQgY29udHJvbGEgbGEgaW50ZW5zaWRhZCBkZSBsYSBwZW5hbGl6YWNpw7NuLiBBIG1lZGlkYSBxdWUgJFxsYW1iZGEkIGNyZWNlLCBhbGd1bm9zIGNvZWZpY2llbnRlcyAkXGJldGFfaiQgc2UgcmVkdWNlbiBleGFjdGFtZW50ZSBhIGNlcm8sIGxvIHF1ZSBzaWduaWZpY2EgcXVlIGVzYXMgdmFyaWFibGVzIHF1ZWRhbiBleGNsdWlkYXMgZGVsIG1vZGVsby4KCkxhc3NvIHNpcnZlIG5vIHNvbG8gcGFyYSBldml0YXIgc29icmVhanVzdGUsIHNpbm8gdGFtYmnDqW4gcGFyYSBzaW1wbGlmaWNhciBtb2RlbG9zIGNvbXBsZWpvcywgeWEgcXVlIHNlbGVjY2lvbmEgYXV0b23DoXRpY2FtZW50ZSBsYXMgdmFyaWFibGVzIG3DoXMgcmVsZXZhbnRlcy4gRXN0byBsbyBoYWNlIGVzcGVjaWFsbWVudGUgw7p0aWwgZW4gY29udGV4dG9zIGRvbmRlIGVsIG7Dum1lcm8gZGUgcHJlZGljdG9yZXMgJHAkIGVzIGdyYW5kZSB5IHNlIHNvc3BlY2hhIHF1ZSBzb2xvIHVuIHN1YmNvbmp1bnRvIGRlIGVsbG9zIHRpZW5lIHVuIGltcGFjdG8gcmVhbCBlbiBsYSB2YXJpYWJsZSByZXNwdWVzdGEuCgpMb3MgY2Fzb3MgZGUgdXNvIGluY2x1eWVuIMOhcmVhcyBjb21vIGxhIGJpb2luZm9ybcOhdGljYSAoc2VsZWNjacOzbiBkZSBnZW5lcyBhc29jaWFkb3MgYSB1bmEgZW5mZXJtZWRhZCksIGVsIG1hcmtldGluZyAoZGV0ZWN0YXIgcXXDqSB2YXJpYWJsZXMgZXhwbGljYW4gbWVqb3IgbGFzIHZlbnRhcyksIGxhIGVjb25vbcOtYSAoc2VsZWNjacOzbiBkZSBpbmRpY2Fkb3JlcyBjbGF2ZSBlbiBtb2RlbG9zIG1hY3JvZWNvbsOzbWljb3MpIHkgZWwgbWFjaGluZSBsZWFybmluZyBlbiBnZW5lcmFsIGN1YW5kbyBzZSBidXNjYSB1biBtb2RlbG8gcGFyc2ltb25pb3NvIHkgcm9idXN0by4KCioqRWplbXBsbyBjb24gUioqCmBgYHtyfQoKCiMgRGF0YXNldDogbXRjYXJzLCB2YXJpYWJsZXMgcHJlZGljdG9yYXMgcGFyYSBwcmVkZWNpciBtcGcKeCA8LSBhcy5tYXRyaXgobXRjYXJzWywgYygid3QiLCAiaHAiLCAiZGlzcCIsICJjeWwiKV0pCnkgPC0gbXRjYXJzJG1wZwoKIyBBanVzdGUgY29uIExhc3NvIChhbHBoYSA9IDEpCm1vZGVsb19sYXNzbyA8LSBnbG1uZXQoeCwgeSwgYWxwaGEgPSAxKQoKIyBWYWxpZGFjacOzbiBjcnV6YWRhIHBhcmEgZWxlZ2lyIGVsIG1lam9yIGxhbWJkYQpjdl9sYXNzbyA8LSBjdi5nbG1uZXQoeCwgeSwgYWxwaGEgPSAxKQoKIyBNZWpvciB2YWxvciBkZSBsYW1iZGEKY3ZfbGFzc28kbGFtYmRhLm1pbgoKIyBHcsOhZmljbyBkZSB0cmF5ZWN0b3JpYXMgZGUgY29lZmljaWVudGVzCnBsb3QobW9kZWxvX2xhc3NvLCB4dmFyID0gImxhbWJkYSIsIGxhYmVsID0gVFJVRSwgbWFpbiA9ICJUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMgKExhc3NvKSIpCgojIEdyw6FmaWNvIGRlIHZhbGlkYWNpw7NuIGNydXphZGEKcGxvdChjdl9sYXNzbykKCmBgYAoKCiMjIDEwLjYpIEVsYXN0aWMgTmV0CgpFbCBFbGFzdGljIE5ldCBlcyB1bmEgdMOpY25pY2EgZGUgcmVndWxhcml6YWNpw7NuIHF1ZSBjb21iaW5hIGxvIG1lam9yIGRlIFJpZGdlIChMMikgeSBMYXNzbyAoTDEpLiBTdSBvYmpldGl2byBlcyBjb250cm9sYXIgZWwgc29icmVhanVzdGUgeSBtZWpvcmFyIGxhIGNhcGFjaWRhZCBwcmVkaWN0aXZhIGRlIGxvcyBtb2RlbG9zIGRlIHJlZ3Jlc2nDs24gY3VhbmRvIGhheSBtdWNoYXMgdmFyaWFibGVzIG8gZnVlcnRlIGNvcnJlbGFjacOzbiBlbnRyZSBlbGxhcy4gTGEgZnVuY2nDs24gZGUgY29zdG8gaW5jbHV5ZSBhbWJvcyB0w6lybWlub3MgZGUgcGVuYWxpemFjacOzbjoKCiQkXG1pbiBcc3VtX3tpPTF9Xm4gXGxlZnQoIHlfaSAtIFxoYXR7eX1faSBccmlnaHQpXjIgCisgXGxhbWJkYSBcbGVmdCggXGFscGhhIFxzdW1fe2o9MX1ecCBcbGVmdHwgXGJldGFfaiBccmlnaHR8IAorICgxLVxhbHBoYSkgXHN1bV97aj0xfV5wIFxiZXRhX2peMiBccmlnaHQpJCQKCmRvbmRlICRcbGFtYmRhIFxnZXEgMCQgcmVndWxhIGxhIGZ1ZXJ6YSBkZSBsYSBwZW5hbGl6YWNpw7NuIHkgJDAgXGxlcSBcYWxwaGEgXGxlcSAxJCBjb250cm9sYSBsYSBtZXpjbGEgZW50cmUgTGFzc28geSBSaWRnZS4KClNpcnZlIHBhcmEgZXZpdGFyIGVsIHNvYnJlYWp1c3RlLCBtYW5lamFyIG11bHRpY29saW5lYWxpZGFkIHkgYSBsYSB2ZXogcmVhbGl6YXIgdW5hIHNlbGVjY2nDs24gcGFyY2lhbCBkZSB2YXJpYWJsZXMuIEN1YW5kbyAkXGFscGhhID0gMSQsIEVsYXN0aWMgTmV0IHNlIGNvbXBvcnRhIGNvbW8gTGFzc287IGN1YW5kbyAkXGFscGhhID0gMCQsIHNlIGNvbXBvcnRhIGNvbW8gUmlkZ2U7IHkgcGFyYSB2YWxvcmVzIGludGVybWVkaW9zIGNvbWJpbmEgc3VzIGVmZWN0b3MuCgpMb3MgY2Fzb3MgZGUgdXNvIG3DoXMgY29tdW5lcyBpbmNsdXllbiBwcm9ibGVtYXMgY29uIGRhdG9zIGRlIGFsdGEgZGltZW5zacOzbiAoJHAgXGdnIG4kKSwgY29tbyBlbiBiaW9pbmZvcm3DoXRpY2EgKHNlbGVjY2nDs24gZGUgZ2VuZXMpLCBhbsOhbGlzaXMgZGUgdGV4dG8gKG1pbGVzIGRlIHZhcmlhYmxlcyBkZSBwYWxhYnJhcyksIHByZWRpY2Npw7NuIGZpbmFuY2llcmEgY29uIG11Y2hvcyBpbmRpY2Fkb3JlcyBjb3JyZWxhY2lvbmFkb3MsIHkgbW9kZWxvcyBkZSBtYXJrZXRpbmcgZGlnaXRhbCBkb25kZSBoYXkgcXVlIGZpbHRyYXIgcHJlZGljdG9yZXMgcmVsZXZhbnRlcyBkZSBncmFuZGVzIGJhc2VzIGRlIGRhdG9zLgoKRW4gbGEgcHLDoWN0aWNhLCBFbGFzdGljIE5ldCBlcyBlc3BlY2lhbG1lbnRlIMO6dGlsIGN1YW5kbyBoYXkgZ3J1cG9zIGRlIHZhcmlhYmxlcyBhbHRhbWVudGUgY29ycmVsYWNpb25hZGFzOiBMYXNzbyB0ZW5kZXLDrWEgYSBlc2NvZ2VyIHNvbG8gdW5hIGRlIGVsbGFzLCBtaWVudHJhcyBxdWUgRWxhc3RpYyBOZXQgcHVlZGUgcmV0ZW5lciBlbCBncnVwbyBjb21wbGV0bywgbG9ncmFuZG8gdW4gbW9kZWxvIG3DoXMgZXN0YWJsZSB5IHJvYnVzdG8uCgoqKkVqZW1wbG8gZ3JhZmljbyBjb24gUioqCmBgYHtyfQoKCiMgRGF0YXNldDogbXRjYXJzLCB2YXJpYWJsZXMgcHJlZGljdG9yYXMgcGFyYSBwcmVkZWNpciBtcGcKeCA8LSBhcy5tYXRyaXgobXRjYXJzWywgYygid3QiLCAiaHAiLCAiZGlzcCIsICJjeWwiKV0pCnkgPC0gbXRjYXJzJG1wZwoKIyBBanVzdGUgY29uIEVsYXN0aWMgTmV0IChhbHBoYSA9IDAuNSBtZXpjbGEgZGUgTGFzc28geSBSaWRnZSkKbW9kZWxvX2VuIDwtIGdsbW5ldCh4LCB5LCBhbHBoYSA9IDAuNSkKCiMgVmFsaWRhY2nDs24gY3J1emFkYSBwYXJhIGVsZWdpciBlbCBtZWpvciBsYW1iZGEKY3ZfZW4gPC0gY3YuZ2xtbmV0KHgsIHksIGFscGhhID0gMC41KQoKIyBNZWpvciB2YWxvciBkZSBsYW1iZGEKY3ZfZW4kbGFtYmRhLm1pbgoKIyBHcsOhZmljbyBkZSB0cmF5ZWN0b3JpYXMgZGUgY29lZmljaWVudGVzCnBsb3QobW9kZWxvX2VuLCB4dmFyID0gImxhbWJkYSIsIGxhYmVsID0gVFJVRSwgbWFpbiA9ICJUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMgKEVsYXN0aWMgTmV0KSIpCgojIEdyw6FmaWNvIGRlIHZhbGlkYWNpw7NuIGNydXphZGEKcGxvdChjdl9lbikKCmBgYAoKCgojIyAxMC43KSBFamVtcGxvIGNvbiBQeXRob24KCgojIyMgMTAuNy4xKSBSZWdyZXNpw7NuIGxpbmVhbCBzaW1wbGUKYGBge3B5dGhvbn0KIyBTaW11bGFjacOzbiBkZSBkYXRvczogSG9yYXMgZGUgZXN0dWRpbyAoWCkgeSBOb3RhIGRlIGV4YW1lbiAoWSkKbnAucmFuZG9tLnNlZWQoMTIzKQpob3JhcyA9IG5wLnJhbmRvbS51bmlmb3JtKDAsIDEwLCA1MCkKbm90YSA9IDQwICsgNSAqIGhvcmFzICsgbnAucmFuZG9tLm5vcm1hbCgwLCA1LCA1MCkKCiMgRGF0YUZyYW1lCmRhdG9zID0gcGQuRGF0YUZyYW1lKHsiaG9yYXMiOiBob3JhcywgIm5vdGEiOiBub3RhfSkKCiMgQWp1c3RlIGNvbiBzY2lraXQtbGVhcm4KWCA9IGRhdG9zW1siaG9yYXMiXV0KeSA9IGRhdG9zWyJub3RhIl0KCm1vZGVsbyA9IExpbmVhclJlZ3Jlc3Npb24oKQptb2RlbG8uZml0KFgsIHkpCgpwcmludCgiSW50ZXJjZXB0bzoiLCBtb2RlbG8uaW50ZXJjZXB0XykKcHJpbnQoIkNvZWZpY2llbnRlOiIsIG1vZGVsby5jb2VmX1swXSkKcHJpbnQoIlLCsjoiLCBtb2RlbG8uc2NvcmUoWCwgeSkpCgojIFZpc3VhbGl6YWNpw7NuCnNucy5sbXBsb3QoeD0iaG9yYXMiLCB5PSJub3RhIiwgZGF0YT1kYXRvcywgY2k9OTUsIGxpbmVfa3dzPXsiY29sb3IiOiAicmVkIn0pCnBsdC50aXRsZSgiRWplbXBsbyBkZSBSZWdyZXNpw7NuIExpbmVhbCBTaW1wbGUiLCBmb250c2l6ZT0xNCkKcGx0LnNob3coKQoKYGBgCgoKIyMjIDEwLjcuMikgUmVncmVzacOzbiBsaW5lYWwgbcO6bHRpcGxlCgpgYGB7cHl0aG9ufQojIERhdGFzZXQgbXRjYXJzIGVuIFB5dGhvbiAoc2UgY2FyZ2EgZGVzZGUgc2VhYm9ybikKbXRjYXJzID0gc25zLmxvYWRfZGF0YXNldCgibXBnIikuZHJvcG5hKCkKCiMgVmFyaWFibGVzIHByZWRpY3RvcmFzOiBwZXNvICh3ZWlnaHQpIHkgcG90ZW5jaWEgKGhvcnNlcG93ZXIpClggPSBtdGNhcnNbWyJ3ZWlnaHQiLCAiaG9yc2Vwb3dlciJdXQp5ID0gbXRjYXJzWyJtcGciXQoKIyBBanVzdGUgZGVsIG1vZGVsbwptb2RlbG8gPSBMaW5lYXJSZWdyZXNzaW9uKCkKbW9kZWxvLmZpdChYLCB5KQoKcHJpbnQoIkludGVyY2VwdG86IiwgbW9kZWxvLmludGVyY2VwdF8pCnByaW50KCJDb2VmaWNpZW50ZXM6IiwgZGljdCh6aXAoWC5jb2x1bW5zLCBtb2RlbG8uY29lZl8pKSkKcHJpbnQoIlLCsjoiLCBtb2RlbG8uc2NvcmUoWCwgeSkpCgojIERpYWduw7NzdGljbyBjb24gc3RhdHNtb2RlbHMKWF9zbSA9IHNtLmFkZF9jb25zdGFudChYKQptb2RlbG9fc20gPSBzbS5PTFMoeSwgWF9zbSkuZml0KCkKcHJpbnQobW9kZWxvX3NtLnN1bW1hcnkoKSkKCiMgVmlzdWFsaXphY2nDs24KcGx0LmZpZ3VyZShmaWdzaXplPSg4LDYpKQpzbnMuc2NhdHRlcnBsb3QoeD1tdGNhcnNbIndlaWdodCJdLCB5PW10Y2Fyc1sibXBnIl0sIGh1ZT1tdGNhcnNbImhvcnNlcG93ZXIiXSwgcGFsZXR0ZT0iQmx1ZXMiLCBzPTgwKQpzbnMucmVncGxvdCh4PSJ3ZWlnaHQiLCB5PSJtcGciLCBkYXRhPW10Y2Fycywgc2NhdHRlcj1GYWxzZSwgY29sb3I9InJlZCIpCnBsdC50aXRsZSgiUmVncmVzacOzbiBMaW5lYWwgTcO6bHRpcGxlOiBtcGcgfiB3ZWlnaHQgKyBob3JzZXBvd2VyIiwgZm9udHNpemU9MTQpCnBsdC5zaG93KCkKCmBgYAoKCiMjIyAxMC43LjMpIFJlZ3VsYXJpemFjacOzbiAoUmlkZ2UgeSBMYXNzbykKYGBge3J9CiMgRGF0YXNldDogbXRjYXJzIGNvbiB2YXJpYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzClggPSBtdGNhcnNbWyJ3ZWlnaHQiLCAiaG9yc2Vwb3dlciIsICJkaXNwbGFjZW1lbnQiLCAiY3lsaW5kZXJzIl1dCnkgPSBtdGNhcnNbIm1wZyJdCgojIFJpZGdlCnJpZGdlID0gUmlkZ2UoYWxwaGE9MS4wKQpyaWRnZS5maXQoWCwgeSkKCiMgTGFzc28KbGFzc28gPSBMYXNzbyhhbHBoYT0wLjEpCmxhc3NvLmZpdChYLCB5KQoKcHJpbnQoIkNvZWZpY2llbnRlcyBSaWRnZToiLCBkaWN0KHppcChYLmNvbHVtbnMsIHJpZGdlLmNvZWZfKSkpCnByaW50KCJDb2VmaWNpZW50ZXMgTGFzc286IiwgZGljdCh6aXAoWC5jb2x1bW5zLCBsYXNzby5jb2VmXykpKQoKIyBUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMgcGFyYSBMYXNzbwphbHBoYXMgPSBucC5sb2dzcGFjZSgtMywgMiwgMTAwKQpjb2VmcyA9IFtdCgpmb3IgYSBpbiBhbHBoYXM6CiAgICBsYXNzbyA9IExhc3NvKGFscGhhPWEsIG1heF9pdGVyPTUwMDApCiAgICBsYXNzby5maXQoWCwgeSkKICAgIGNvZWZzLmFwcGVuZChsYXNzby5jb2VmXykKCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCw2KSkKYXggPSBwbHQuZ2NhKCkKYXgucGxvdChhbHBoYXMsIGNvZWZzKQpheC5zZXRfeHNjYWxlKCJsb2ciKQpwbHQueGxhYmVsKCJMYW1iZGEgKM6xKSIpCnBsdC55bGFiZWwoIkNvZWZpY2llbnRlcyIpCnBsdC50aXRsZSgiVHJheWVjdG9yaWEgZGUgY29lZmljaWVudGVzIChMYXNzbykiKQpwbHQuc2hvdygpCgpgYGAKCgojIyMgMTAuNy40KSBSZWd1bGFyaXphY2nDs24gUmlkZ2UgY29uIHZhbGlkYWNpw7NuIGNydXphZGEKCmBgYHtweXRob259CgojIFJpZGdlIGNvbiB2YWxpZGFjacOzbiBjcnV6YWRhCmFscGhhcyA9IG5wLmxvZ3NwYWNlKC0zLCAzLCAxMDApCnJpZGdlX2N2ID0gUmlkZ2VDVihhbHBoYXM9YWxwaGFzLCBzdG9yZV9jdl92YWx1ZXM9VHJ1ZSkKcmlkZ2VfY3YuZml0KFgsIHkpCgpwcmludCgiTWVqb3IgbGFtYmRhICjOsSk6IiwgcmlkZ2VfY3YuYWxwaGFfKQpwcmludCgiQ29lZmljaWVudGVzIFJpZGdlOiIsIGRpY3QoemlwKFguY29sdW1ucywgcmlkZ2VfY3YuY29lZl8pKSkKCiMgR3LDoWZpY28gZGUgY29lZmljaWVudGVzIHZzIGxhbWJkYQpjb2VmcyA9IFtdCmZvciBhIGluIGFscGhhczoKICAgIHJpZGdlID0gUmlkZ2UoYWxwaGE9YSkKICAgIHJpZGdlLmZpdChYLCB5KQogICAgY29lZnMuYXBwZW5kKHJpZGdlLmNvZWZfKQoKcGx0LmZpZ3VyZShmaWdzaXplPSg4LDYpKQpheCA9IHBsdC5nY2EoKQpheC5wbG90KGFscGhhcywgY29lZnMpCmF4LnNldF94c2NhbGUoImxvZyIpCnBsdC54bGFiZWwoIkxhbWJkYSAozrEpIikKcGx0LnlsYWJlbCgiQ29lZmljaWVudGVzIikKcGx0LnRpdGxlKCJUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMgKFJpZGdlKSIpCnBsdC5zaG93KCkKCmBgYAoKCgojIyMgMTAuNy41KSBSZWd1bGFyaXphY2nDs24gTGFzc28gY29uIHZhbGlkYWNpw7NuIGNydXphZGEKCmBgYHtweXRob259CgojIExhc3NvIGNvbiB2YWxpZGFjacOzbiBjcnV6YWRhCmxhc3NvX2N2ID0gTGFzc29DVihhbHBoYXM9YWxwaGFzLCBjdj01LCBtYXhfaXRlcj01MDAwKQpsYXNzb19jdi5maXQoWCwgeSkKCnByaW50KCJNZWpvciBsYW1iZGEgKM6xKToiLCBsYXNzb19jdi5hbHBoYV8pCnByaW50KCJDb2VmaWNpZW50ZXMgTGFzc286IiwgZGljdCh6aXAoWC5jb2x1bW5zLCBsYXNzb19jdi5jb2VmXykpKQoKIyBHcsOhZmljbyBkZSBjb2VmaWNpZW50ZXMgdnMgbGFtYmRhCnBsdC5maWd1cmUoZmlnc2l6ZT0oOCw2KSkKYXggPSBwbHQuZ2NhKCkKYXgucGxvdChsYXNzb19jdi5hbHBoYXNfLCBsYXNzb19jdi5wYXRoKFgsIHksIGFscGhhcz1hbHBoYXMsIG1heF9pdGVyPTUwMDApWzFdLlQpCmF4LnNldF94c2NhbGUoImxvZyIpCnBsdC54bGFiZWwoIkxhbWJkYSAozrEpIikKcGx0LnlsYWJlbCgiQ29lZmljaWVudGVzIikKcGx0LnRpdGxlKCJUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMgKExhc3NvKSIpCnBsdC5zaG93KCkKCmBgYAoKCgojIyMgMTAuNy42KSBFbGFzdGljIE5ldAoKCmBgYHtweXRob259CgojIEVsYXN0aWMgTmV0IGNvbiB2YWxpZGFjacOzbiBjcnV6YWRhCmVsYXN0aWNfY3YgPSBFbGFzdGljTmV0Q1YobDFfcmF0aW89MC41LCBhbHBoYXM9YWxwaGFzLCBjdj01LCBtYXhfaXRlcj01MDAwKQplbGFzdGljX2N2LmZpdChYLCB5KQoKcHJpbnQoIk1lam9yIGxhbWJkYSAozrEpOiIsIGVsYXN0aWNfY3YuYWxwaGFfKQpwcmludCgiQ29lZmljaWVudGVzIEVsYXN0aWMgTmV0OiIsIGRpY3QoemlwKFguY29sdW1ucywgZWxhc3RpY19jdi5jb2VmXykpKQoKIyBUcmF5ZWN0b3JpYSBkZSBjb2VmaWNpZW50ZXMKY29lZnMgPSBbXQpmb3IgYSBpbiBhbHBoYXM6CiAgICBlbiA9IEVsYXN0aWNOZXQoYWxwaGE9YSwgbDFfcmF0aW89MC41LCBtYXhfaXRlcj01MDAwKQogICAgZW4uZml0KFgsIHkpCiAgICBjb2Vmcy5hcHBlbmQoZW4uY29lZl8pCgpwbHQuZmlndXJlKGZpZ3NpemU9KDgsNikpCmF4ID0gcGx0LmdjYSgpCmF4LnBsb3QoYWxwaGFzLCBjb2VmcykKYXguc2V0X3hzY2FsZSgibG9nIikKcGx0LnhsYWJlbCgiTGFtYmRhICjOsSkiKQpwbHQueWxhYmVsKCJDb2VmaWNpZW50ZXMiKQpwbHQudGl0bGUoIlRyYXllY3RvcmlhIGRlIGNvZWZpY2llbnRlcyAoRWxhc3RpYyBOZXQpIikKcGx0LnNob3coKQoKYGBgCgo=