Introduzione

In questo notebook esploriamo la collinearità e multicollinearità nei modelli di regressione lineare, gli indicatori principali (correlogramma, VIF, numero di condizionamento) e le tecniche di riduzione dei modelli (stepwise) e regolarizzazione (Ridgee Lasso).

library(car)
library(ggcorrplot)

Dataset swiss

Consideriamo il dataset swiss che contiene indicatori di fertilità e socioeconomici delle province (di lingua francese) della svizzera nel 1888.

data(swiss)
?swiss
head(swiss)

Studiamo un modello di regressione lineare con la variabile Fertility (un indice di fertilità) come fattore di uscita, e tutte le altre come caratteristiche di ingresso.

# Modello iniziale
fit_swiss_full <- lm(Fertility ~ ., data = swiss)
summary(fit_swiss_full)

Call:
lm(formula = Fertility ~ ., data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-15.2743  -5.2617   0.5032   4.1198  15.3213 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      66.91518   10.70604   6.250 1.91e-07 ***
Agriculture      -0.17211    0.07030  -2.448  0.01873 *  
Examination      -0.25801    0.25388  -1.016  0.31546    
Education        -0.87094    0.18303  -4.758 2.43e-05 ***
Catholic          0.10412    0.03526   2.953  0.00519 ** 
Infant.Mortality  1.07705    0.38172   2.822  0.00734 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.165 on 41 degrees of freedom
Multiple R-squared:  0.7067,    Adjusted R-squared:  0.671 
F-statistic: 19.76 on 5 and 41 DF,  p-value: 5.594e-10

Osserviamo che alcuni \(p\)-value dei coefficienti risultano molto elevati, un primo segno di collinearità, mentre il coefficiente \(R^2\) è abbastanza alto e il \(p\)-value dell’\(F\)-test è basso, segno che i fattori di ingresso nel modello spiegano una buona parte dell’uscita.

Diagnosi di collinearità

Analizziamo le correlazioni tra i fattori di ingresso mediante la matrice delle correlazioni e un correlogramma.

# Matrice di correlazione (approssimata a due cifra decimali per semplicità)
round(cor(swiss[, -1]), 2)
                 Agriculture Examination Education Catholic Infant.Mortality
Agriculture             1.00       -0.69     -0.64     0.40            -0.06
Examination            -0.69        1.00      0.70    -0.57            -0.11
Education              -0.64        0.70      1.00    -0.15            -0.10
Catholic                0.40       -0.57     -0.15     1.00             0.18
Infant.Mortality       -0.06       -0.11     -0.10     0.18             1.00
# Diagramma a nuvola di punti (non efficace: troppe variabili, eventualmente selezionarne alcune)
#pairs(mtcars, main = "Scatterplot (mtcars)")

# heatmap con ggcorrplot
library(ggcorrplot)
corr_mat_mtcars <- cor(swiss[, -1])
ggcorrplot(corr_mat_mtcars, lab = TRUE, type = "lower",
           title = "Matrice di correlazione tra predittori (mtcars)")

Abbiamo alcune correlazioni tra coppie (Examination e Education), il caso più semplice di collinearità. Studiamo gli indicatori specifici, il VIF e il condizionamento della matrice di Gram. Per il primo usiamo il comando vif() dal pacchetto car.

# Variance Inflation Factor
library(car)

vif(fit_swiss_full)
     Agriculture      Examination        Education         Catholic Infant.Mortality 
        2.284129         3.675420         2.774943         1.937160         1.107542 

Ricordiamo che valori maggiori di \(5\) indicano variabili per cui potrebbe essere presente collinearità (in particolare se maggiore di \(10\)). Notiamo che nessun VIF supera la soglia \(5\) ma Examination presenta un valore comunque grande.

Vediamo infine la matrice di Gram e il numero di condizionamento.


# estrae la matrice di design del modello (dati con una colonna di 1 per l'intercetta)
swiss_features <- model.matrix(fit_swiss_full)

# calcola la matrice di Gram
G_swiss <- t(swiss_features) %*% swiss_features

G_swiss
                 (Intercept) Agriculture Examination Education  Catholic Infant.Mortality
(Intercept)            47.00     2381.00      775.00    516.00   1933.76           937.30
Agriculture          2381.00   144347.22    33539.10  19716.10 115439.06         47298.03
Examination           775.00    33539.10    15707.00  10973.00  23120.68         15333.60
Education             516.00    19716.10    10973.00   9918.00  18392.07         10162.40
Catholic             1933.76   115439.06    23120.68  18392.07 159569.84         39544.75
Infant.Mortality      937.30    47298.03    15333.60  10162.40  39544.75         19082.41

Per il numero di condizionamento usiamo la funzione norm() per calcolare (una) norma della matrice \(G\) e solve() per l’inversa.

kappa_G <- norm(G_swiss, type="2") * norm(solve(G_swiss, type="2"))

kappa_G
[1] 681041.3

Un valore maggiore di \(1000\) indica che \(G\) è mal condizionata (segno di collinearità). Vediamo per confronto un semplice esempio generato.

# genera una matrice quasi non-invertibile
eps = 10^{-3}

G = matrix( c(1, 1-eps, 1-eps, 1 ), byrow=TRUE, nrow=2, ncol=2)

G
      [,1]  [,2]
[1,] 1.000 0.999
[2,] 0.999 1.000
kappa_G = norm(G)*norm(solve(G))
print(paste("Numero di condizionamento:", round(kappa_G, 0)))
[1] "Numero di condizionamento: 1999"

Un indicatore alternativo è il determinante della matrice di correlazione dei fattori di ingresso.

round(cor(swiss), 2)
                 Fertility Agriculture Examination Education Catholic Infant.Mortality
Fertility             1.00        0.35       -0.65     -0.66     0.46             0.42
Agriculture           0.35        1.00       -0.69     -0.64     0.40            -0.06
Examination          -0.65       -0.69        1.00      0.70    -0.57            -0.11
Education            -0.66       -0.64        0.70      1.00    -0.15            -0.10
Catholic              0.46        0.40       -0.57     -0.15     1.00             0.18
Infant.Mortality      0.42       -0.06       -0.11     -0.10     0.18             1.00
det(cor(swiss))
[1] 0.03496976

Riduzione del modello

Per eliminare eventuali variabili collineari, consideriamo gli approcci basati sui test di significatività dei coefficienti e sugli indici AIC/BIC.

Riprendiamo i \(p\)-value del modello completo.

summary(fit_swiss_full)

Call:
lm(formula = Fertility ~ ., data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-15.2743  -5.2617   0.5032   4.1198  15.3213 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      66.91518   10.70604   6.250 1.91e-07 ***
Agriculture      -0.17211    0.07030  -2.448  0.01873 *  
Examination      -0.25801    0.25388  -1.016  0.31546    
Education        -0.87094    0.18303  -4.758 2.43e-05 ***
Catholic          0.10412    0.03526   2.953  0.00519 ** 
Infant.Mortality  1.07705    0.38172   2.822  0.00734 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.165 on 41 degrees of freedom
Multiple R-squared:  0.7067,    Adjusted R-squared:  0.671 
F-statistic: 19.76 on 5 and 41 DF,  p-value: 5.594e-10

Una soluzione rapida (ma potenzialmente rischiosa!) è di eliminare direttamente le variabili con \(p\)-value maggiore di una determinata soglia (ad esempio 0.1).

fit_swiss_reduced <- lm(Fertility ~ .- Agriculture - Examination, data = swiss)
summary(fit_swiss_reduced)

Call:
lm(formula = Fertility ~ . - Agriculture - Examination, data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-14.4781  -5.4403  -0.5143   4.1568  15.1187 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      48.67707    7.91908   6.147 2.24e-07 ***
Education        -0.75925    0.11680  -6.501 6.83e-08 ***
Catholic          0.09607    0.02722   3.530  0.00101 ** 
Infant.Mortality  1.29615    0.38699   3.349  0.00169 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.505 on 43 degrees of freedom
Multiple R-squared:  0.6625,    Adjusted R-squared:  0.639 
F-statistic: 28.14 on 3 and 43 DF,  p-value: 3.15e-10

Il coefficiente \(R^2\) (o meglio quello Adjusted che tiene conto del numero di variabili) è sceso leggermente ma non drasticamente. Confrontiamo con il modello in cui eliminiamo solo Examination.

fit_swiss_reduced_one_step <- lm(Fertility ~ .- Examination, data = swiss)
summary(fit_swiss_reduced_one_step)

Call:
lm(formula = Fertility ~ . - Examination, data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-14.6765  -6.0522   0.7514   3.1664  16.1422 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      62.10131    9.60489   6.466 8.49e-08 ***
Agriculture      -0.15462    0.06819  -2.267  0.02857 *  
Education        -0.98026    0.14814  -6.617 5.14e-08 ***
Catholic          0.12467    0.02889   4.315 9.50e-05 ***
Infant.Mortality  1.07844    0.38187   2.824  0.00722 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.168 on 42 degrees of freedom
Multiple R-squared:  0.6993,    Adjusted R-squared:  0.6707 
F-statistic: 24.42 on 4 and 42 DF,  p-value: 1.717e-10

In questo caso l’Adjusted \(R^2\) è rimasto invariato.

Problema: discutere cosa accade se invece si rimuove la variabile Education.

Ricordiamo che AIC e BIC cercano di indicare la performance del modello in termini del compromesso tra bias (numero di parametri) e varianza (valore della verosimiglianza). Per calcolarli è sufficiente usare la funzione AIC()

AIC(fit_swiss_full, fit_swiss_reduced, fit_swiss_reduced_one_step)

Vediamo quindi che il modello in cui abbiamo eliminato solo la variabile Examination è preferibile in termini di AIC. Similmente per il BIC:

BIC(fit_swiss_full, fit_swiss_reduced, fit_swiss_reduced_one_step)

Ovviamente non si può procedere a mano con la riduzione del modello, specialmente se ci sono molte caratteristiche. Per questo le procedure stepwise sono automatizzate. Usiamo il comando step (già caricato).

Selezione all’indietro.

# Backward
fit_back <- step(fit_swiss_full, direction = "backward", trace = 0) # mettere trace = 1 o TRUE per avere informazioni sui passi del'algoritmo

summary(fit_back)

Call:
lm(formula = Fertility ~ Agriculture + Education + Catholic + 
    Infant.Mortality, data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-14.6765  -6.0522   0.7514   3.1664  16.1422 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      62.10131    9.60489   6.466 8.49e-08 ***
Agriculture      -0.15462    0.06819  -2.267  0.02857 *  
Education        -0.98026    0.14814  -6.617 5.14e-08 ***
Catholic          0.12467    0.02889   4.315 9.50e-05 ***
Infant.Mortality  1.07844    0.38187   2.824  0.00722 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.168 on 42 degrees of freedom
Multiple R-squared:  0.6993,    Adjusted R-squared:  0.6707 
F-statistic: 24.42 on 4 and 42 DF,  p-value: 1.717e-10

Selezione in avanti.

# Forward
# iniziamo con il modello costante (banale)
fit_swiss_trivial <- lm(Fertility ~ 1, data = swiss)


fit_forw <- step(fit_swiss_trivial,
                 direction = "forward",
                 scope = formula(fit_swiss_full), #specifica un modello `massimo`
                 trace = FALSE)
summary(fit_forw)

Call:
lm(formula = Fertility ~ Education + Catholic + Infant.Mortality + 
    Agriculture, data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-14.6765  -6.0522   0.7514   3.1664  16.1422 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      62.10131    9.60489   6.466 8.49e-08 ***
Education        -0.98026    0.14814  -6.617 5.14e-08 ***
Catholic          0.12467    0.02889   4.315 9.50e-05 ***
Infant.Mortality  1.07844    0.38187   2.824  0.00722 ** 
Agriculture      -0.15462    0.06819  -2.267  0.02857 *  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.168 on 42 degrees of freedom
Multiple R-squared:  0.6993,    Adjusted R-squared:  0.6707 
F-statistic: 24.42 on 4 and 42 DF,  p-value: 1.717e-10

E infine una selezione in entrambe le direzioni.

# Both directions
fit_both <- step(fit_swiss_full, direction = "both", trace = FALSE, k=log(nrow(swiss))) #in questo caso impostiamo BIC

summary(fit_both)

Call:
lm(formula = Fertility ~ Agriculture + Education + Catholic + 
    Infant.Mortality, data = swiss)

Residuals:
     Min       1Q   Median       3Q      Max 
-14.6765  -6.0522   0.7514   3.1664  16.1422 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      62.10131    9.60489   6.466 8.49e-08 ***
Agriculture      -0.15462    0.06819  -2.267  0.02857 *  
Education        -0.98026    0.14814  -6.617 5.14e-08 ***
Catholic          0.12467    0.02889   4.315 9.50e-05 ***
Infant.Mortality  1.07844    0.38187   2.824  0.00722 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.168 on 42 degrees of freedom
Multiple R-squared:  0.6993,    Adjusted R-squared:  0.6707 
F-statistic: 24.42 on 4 and 42 DF,  p-value: 1.717e-10

Attenzione: Le procedure stepwise ottimizzano un criterio (AIC, BIC), ma non sempre portano a un modello interpretabile o stabile. È buona pratica consideriare gli indicatori (\(p\)-value, VIF) anche dopo la riduzione automatica.

vif(fit_both)
     Agriculture        Education         Catholic Infant.Mortality 
        2.147153         1.816361         1.299916         1.107528 
There were 50 or more warnings (use warnings() to see the first 50)

PCR

Un approccio differente consiste nell’applicare la PCA sulle caratteristiche in input. Dividiamo al solito in training e test set con il comando createDataPartition.

library(caret)

# Dividiamo in training e test set il dataset ciqual_reduced

train_index <- createDataPartition(swiss$Fertility,  p = 0.8, list = FALSE)

swiss_train <- swiss[train_index, ]
swiss_test <- swiss[-train_index, ]

Effetuiamo la PCA sulle variabili di ingresso (escludiamo la prima colonna, la variabile di uscita) con il comando preProcess() dal pacchetto caret. In questo modo sarà più semplice fare previsioni con il modello (altrimenti bisognerebbe trasformare ogni volta le caratteristiche di input di un punto di test)


# possiamo specificare quante componenti principali, in questo caso pcaComp=3. È anche possibile impostare una soglia sulla percentuale di varianza cumulata con l'opzione thresh=0.95
pca_model <- preProcess(swiss_train[, -1], method = c("center", "scale", "pca"), pcaComp=3)

Possiamo ora calcolare i fattori di ingresso proiettati sulle componenti principali con il comando predict (sia sul training che sul test set).

pca_train <- predict(pca_model, swiss_train[, -1])
head(pca_train)

pca_test <- predict(pca_model, swiss_test[, -1])
head(pca_test)

Effettuiamo quindi la regressione usando come fattori di ingresso le PC selezionate.

fit_pcr <- lm(swiss_train$Fertility~., data=pca_train)
summary(fit_pcr)

Call:
lm(formula = swiss_train$Fertility ~ ., data = pca_train)

Residuals:
     Min       1Q   Median       3Q      Max 
-16.8587  -5.9633   0.1315   6.2244  16.1768 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  69.7692     1.3886  50.243  < 2e-16 ***
PC1           5.2321     0.8567   6.107 5.58e-07 ***
PC2           4.1389     1.3819   2.995  0.00501 ** 
PC3          -2.3331     1.5193  -1.536  0.13361    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 8.672 on 35 degrees of freedom
Multiple R-squared:  0.5815,    Adjusted R-squared:  0.5456 
F-statistic: 16.21 on 3 and 35 DF,  p-value: 8.996e-07
vif(fit_pcr)
PC1 PC2 PC3 
  1   1   1 

Le prime 2 componenti spiegano già parte della variabilità e riducono (necessariamente) la collinearità.

Regolarizzazione: Ridge e LASSO

Usiamo il pacchetto caret per usare Ridge, Lasso (anche altri modelli regolarizzati sarebbero disponibili) e determinare i parametri usando cross-validation. Un pacchetto alternativo più specifico è glmnet.

Definiamo prima il metodo di cross-validation.

train_control <- trainControl(method = "cv", number = 10) #10-fold cv

Per la regressione Ridge, specifichiamo il metodo “glmnet” con il parametro \(\alpha=0\) (glmnet è un modello Elastic Net che permette di interpolare tra Ridge e LASSO).


ridge_model <- train(Fertility ~ ., data = swiss_train,  
                        method = "glmnet", 
                        trControl = train_control, 
                        tuneGrid = expand.grid(alpha = 0, lambda = seq(0, 1, length = 100)))

Plottiamo l’errore di CV al variare del parametro di regolarizzazione \(\lambda\).

plot(ridge_model, type='l')

Vediamo quindi che il parametro ottimale (scelto tramite cross-validation) è 10.7070707. Vediamo i parametri determinati.


coef(ridge_model$finalModel, ridge_model$bestTune$lambda)
6 x 1 sparse Matrix of class "dgCMatrix"
                 s=0.8585859
(Intercept)      63.07000721
Agriculture      -0.12330908
Examination      -0.25350028
Education        -0.74633617
Catholic          0.08684621
Infant.Mortality  1.10689452

Per la regressione LASSO, specifichiamo il metodo “glmnet” con il parametro \(\alpha=1\).


lasso_model <- train(Fertility ~ ., data = swiss_train,  
                        method = "glmnet", 
                        trControl = train_control, 
                        tuneGrid = expand.grid(alpha = 1, lambda = seq(0, 10, length = 100)))

Plottiamo l’errore di CV al variare del parametro di regolarizzazione \(\lambda\).

plot(lasso_model, type='l')

lasso_model$bestTune$lambda
[1] 0.1414141
coef(lasso_model$finalModel, lasso_model$bestTune$lambda)
6 x 1 sparse Matrix of class "dgCMatrix"
                 s=0.1414141
(Intercept)       63.2699451
Agriculture       -0.1469023
Examination       -0.1271760
Education         -0.8786019
Catholic           0.1050504
Infant.Mortality   1.0893602

In alternativa possiamo usare il comando cv.glm() dal pacchetto glmnet.

library(glmnet)
X <- as.matrix(swiss_train[, -1])
y <- swiss_train$Fertility
cv_ridge <- cv.glmnet(X, y, alpha = 0)
plot(cv_ridge)


# confronta i lambda trovati tramite i due metodi
print(paste("lambda_min glmnet:", cv_ridge$lambda.min))
[1] "lambda_min glmnet: 0.8545232653727"
print(paste("lambda_min_caret:", ridge_model$bestTune$lambda))
[1] "lambda_min_caret: 0.858585858585859"
# confronto dei coefficienti del modello trovati per i due metodi
coef(cv_ridge)
6 x 1 sparse Matrix of class "dgCMatrix"
                  lambda.1se
(Intercept)      60.63706601
Agriculture       0.01045061
Examination      -0.28175926
Education        -0.32409486
Catholic          0.04008528
Infant.Mortality  0.76982050
coef(ridge_model$finalModel, ridge_model$bestTune$lambda)
6 x 1 sparse Matrix of class "dgCMatrix"
                 s=0.8585859
(Intercept)      63.07000721
Agriculture      -0.12330908
Examination      -0.25350028
Education        -0.74633617
Catholic          0.08684621
Infant.Mortality  1.10689452

Operiamo similmente con LASSO.

cv_lasso <- cv.glmnet(X, y, alpha = 1)
plot(cv_lasso)


# confronta i lambda trovati tramite i due metodi
print(paste("lambda_min glmnet:", cv_lasso$lambda.min))
[1] "lambda_min glmnet: 0.226968880540014"
print(paste("lambda_min_caret:", lasso_model$bestTune$lambda))
[1] "lambda_min_caret: 0.141414141414141"
# confronto dei coefficienti del modello trovati per i due metodi
coef(cv_lasso)
6 x 1 sparse Matrix of class "dgCMatrix"
                  lambda.1se
(Intercept)      57.12305060
Agriculture       .         
Examination      -0.08853346
Education        -0.58404464
Catholic          0.05533704
Infant.Mortality  0.92894431
coef(lasso_model$finalModel, lasso_model$bestTune$lambda)
6 x 1 sparse Matrix of class "dgCMatrix"
                 s=0.1414141
(Intercept)       63.2699451
Agriculture       -0.1469023
Examination       -0.1271760
Education         -0.8786019
Catholic           0.1050504
Infant.Mortality   1.0893602

Dataset CIQUAL

Riprendiamo il dataset CIQUAL sulle caratteristiche nutritive degli alimenti. Carichiamo la versione .csv già ripulita nel corso della lezione sulla PCA.

ciqual_tidy <- read.csv('datasets/ciqual_tidy.csv', header = TRUE)

head(ciqual_tidy)

Eliminiamo le prime due colonne (con le classi di alimenti) e usiamo la terza come colonna di nomi (in modo da non includerla nella regressione).


ciqual_reduced <- ciqual_tidy[, -(1:3)]

row.names(ciqual_reduced) <- ciqual_tidy[,3]

head(ciqual_reduced)

Poniamoci come obiettivo di calcolare l’energia (in kcal) a partire dalle altre informazioni nutritive. Rimuoviamo quindi le colonne (praticamente duplicate) dell’energia (teniamo solo la prima).


ciqual_reduced <- ciqual_reduced[,-(2:4)]

head(ciqual_reduced)

Consideriamo il modello di regressione lineare completo.


ciqual_fit <- lm( ciqual_reduced$Energy..Regulation.EU.No.1169.2011..kJ.100g. ~., data=ciqual_reduced)

summary(ciqual_fit)

Visualizziamo le significatività e i VIF

# plot delle significatività (-log(p-value))
library(ggplot2)
ciqual_coefs <- summary(ciqual_fit)$coefficients

ciqual_coefs_df <- data.frame(
  Term = rownames(ciqual_coefs)[-1],  # escludi l'intercetta
  NegLogPValue = -log10(ciqual_coefs[-1, "Pr(>|t|)"]),  # escludi l'intercetta
  VIF = vif(ciqual_fit)
)
ggplot(ciqual_coefs_df, aes(x = reorder(Term, NegLogPValue), y = NegLogPValue)) +
  geom_bar(stat = "identity", fill = "steelblue") +
  coord_flip() +
  labs(title = "-log10(p-value) dei coefficienti del modello CIQUAL", x = "Termine", y = "-log10(p-value)") +
  theme_minimal()

ggplot(ciqual_coefs_df, aes(x = reorder(Term, VIF), y = VIF)) +
  geom_bar(stat = "identity", fill = "darkorange") +
  coord_flip() +
  labs(title = "Variance Inflation Factor (VIF) dei coefficienti del modello CIQUAL", x = "Termine", y = "VIF") +
  theme_minimal()

Molte caratteristiche hanno un \(p\)-value alto, ma potrebbe esserci collinearità (ad esempio tra gli zuccheri).

Riduzione stepwise

Procediamo con una riduzione all’indietro.

ciqual_reduced_stepwise <- step(ciqual_fit, direction = "backward", trace=0)

summary(ciqual_reduced_stepwise)

Plottiamo al solito significatività e VIF.

ciqual_coefs <- summary(ciqual_reduced_stepwise)$coefficients

ciqual_coefs_df <- data.frame(
  Term = rownames(ciqual_coefs)[-1],  # escludi l'intercetta
  NegLogPValue = -log10(ciqual_coefs[-1, "Pr(>|t|)"]),  # escludi l'intercetta
  VIF = vif(ciqual_reduced_stepwise)
)
ggplot(ciqual_coefs_df, aes(x = reorder(Term, NegLogPValue), y = NegLogPValue)) +
  geom_bar(stat = "identity", fill = "steelblue") +
  coord_flip() +
  labs(title = "-log10(p-value) dei coefficienti del modello CIQUAL", x = "Termine", y = "-log10(p-value)") +
  theme_minimal()

ggplot(ciqual_coefs_df, aes(x = reorder(Term, VIF), y = VIF)) +
  geom_bar(stat = "identity", fill = "darkorange") +
  coord_flip() +
  labs(title = "Variance Inflation Factor (VIF) dei coefficienti del modello CIQUAL", x = "Termine", y = "VIF") +
  theme_minimal()

Procediamo in alternativa con una selezione in avanti


ciqual_reduced_stepwise_forward <- step(lm(ciqual_reduced$Energy..Regulation.EU.No.1169.2011..kJ.100g.~ 1, data=ciqual_reduced), 
                                        scope=formula(ciqual_fit), direction="forward", trace=0)

summary(ciqual_reduced_stepwise_forward)
ciqual_coefs <- summary(ciqual_reduced_stepwise)$coefficients

ciqual_coefs_df <- data.frame(
  Term = rownames(ciqual_coefs)[-1],  # escludi l'intercetta
  NegLogPValue = -log10(ciqual_coefs[-1, "Pr(>|t|)"]),  # escludi l'intercetta
  VIF = vif(ciqual_reduced_stepwise)
)
ggplot(ciqual_coefs_df, aes(x = reorder(Term, NegLogPValue), y = NegLogPValue)) +
  geom_bar(stat = "identity", fill = "steelblue") +
  coord_flip() +
  labs(title = "-log10(p-value) CIQUAL ridotto", x = "Termine", y = "-log10(p-value)") +
  theme_minimal()

ggplot(ciqual_coefs_df, aes(x = reorder(Term, VIF), y = VIF)) +
  geom_bar(stat = "identity", fill = "darkorange") +
  coord_flip() +
  labs(title = "Variance Inflation Factor (VIF)  CIQUAL ridotto", x = "Termine", y = "VIF") +
  theme_minimal()

Vediamo che in entrambi i casi il modello ridotto ha comunque un gran numero di fattori di ingresso. Possiamo però forzare il numero massimo di passi in modo da ridurre le variabili di input.




ciqual_reduced_stepwise_forward <- step(lm(ciqual_reduced$Energy..Regulation.EU.No.1169.2011..kJ.100g.~ 1, data=ciqual_reduced), 
                                        scope=formula(ciqual_fit), direction="forward", steps=5, trace=0)

summary(ciqual_reduced_stepwise_forward)

Verifichiamone la collinearità.

vif(ciqual_reduced_stepwise_forward)

Vediamo che c’è una grande collinearità tra le due variabili relative alle proteine (abbastanza ovvio, sono due colonne duplicate). Quindi possiamo ridurre ulteriormente eliminandone una.


# rimuovi la variabile dalla formula usando update()
ciqual_minimal <- lm( update(ciqual_reduced_stepwise_forward, . ~ . - Protein..crude..N.x.6.25..g.100g.), data=ciqual_reduced)

summary(ciqual_minimal)
vif(ciqual_minimal)

Ovviamente gli indicatori come l’\(R^2\) sono diminuiti, ma abbiamo un modello decisamente semplificato, intepretabile e senza collinearità.

PCR

Dividiamo al solito in training e test set con il comando createDataPartition.

library(caret)

# Dividiamo in training e test set il dataset ciqual_reduced

train_index <- createDataPartition(ciqual_reduced$Energy..Regulation.EU.No.1169.2011..kJ.100g., p = 0.8, list = FALSE)

ciqual_reduced_train <- ciqual_reduced[train_index, ]
ciqual_reduced_test <- ciqual_reduced[-train_index, ]

Effetuiamo la PCA sulle variabili di ingresso (escludiamo la prima colonna, la variabile di uscita) con il comando preProcess() dal pacchetto caret. In questo modo sarà più semplice fare previsioni con il modello (altrimenti bisognerebbe trasformare ogni volta le caratteristiche di input di un punto di test)


# specifichiamo una soglia sulla percentuale di varianza cumulata con l'opzione thresh
pca_model <- preProcess(ciqual_reduced_train[, -1], method = c("center", "scale", "pca"), thresh =0.95)

Possiamo ora calcolare i fattori di ingresso proiettati sulle componenti principali con il comando predict (sia sul training che sul test set).

pca_ciqual_train <- predict(pca_model, ciqual_reduced_train[, -1])
head(pca_ciqual_train)

pca_ciqual_test <- predict(pca_model, ciqual_reduced_test[, -1])
head(pca_ciqual_test)

Effettuiamo quindi la regressione usando come fattori di ingresso le PC selezionate.

fit_pcr <- lm(ciqual_reduced_train$Energy..Regulation.EU.No.1169.2011..kJ.100g. ~., data=pca_ciqual_train)
summary(fit_pcr)
vif(fit_pcr)

Problema: come cambia la performance del modello se specifichiamo un numero basso di PC?

Regolarizzazione LASSO

Usiamo il pacchetto glmnet per la regolarizzazione con LASSO.


X = as.matrix(ciqual_reduced_train[,-1])
y = as.matrix(ciqual_reduced_train[, 1])
cv_lasso <- cv.glmnet(X, y, alpha = 1)
plot(cv_lasso)


print(paste("lambda_min glmnet:", cv_lasso$lambda.min))
[1] "lambda_min glmnet: 9.36755294235116"
# confronto dei coefficienti del modello trovati per i due metodi
coef(cv_lasso)
50 x 1 sparse Matrix of class "dgCMatrix"
                                          lambda.1se
(Intercept)                              149.9657571
Water..g.100g.                            -0.6032222
Protein..g.100g.                          13.6781669
Protein..crude..N.x.6.25..g.100g.          .        
Carbohydrate..g.100g.                      4.9960658
Fat..g.100g.                              21.1724823
Sugars..g.100g.                           -2.6449047
fructose..g.100g.                          .        
galactose..g.100g.                         .        
glucose..g.100g.                           1.6691924
lactose..g.100g.                           .        
maltose..g.100g.                           .        
sucrose..g.100g.                          -5.6103001
Starch..g.100g.                            7.0525990
Fibres..g.100g.                            .        
Polyols..g.100g.                           1.1199149
Ash..g.100g.                               .        
Alcohol..g.100g.                          15.8331160
Organic.acids..g.100g.                     .        
FA.saturated..g.100g.                      9.6775364
FA.mono..g.100g.                           2.3824688
FA.poly..g.100g.                           .        
Cholesterol..mg.100g.                      .        
Salt..g.100g.                              .        
Calcium..mg.100g.                          .        
Chloride..mg.100g.                         .        
Copper..mg.100g.                           .        
Iron..mg.100g.                             3.3243452
Iodine..µg.100g.                           .        
Magnesium..mg.100g.                        .        
Manganese..mg.100g.                        .        
Phosphorus..mg.100g.                       .        
Potassium..mg.100g.                        .        
Selenium..µg.100g.                         .        
Sodium..mg.100g.                           .        
Zinc..mg.100g.                             .        
Retinol..µg.100g.                          .        
Beta.carotene..µg.100g.                    .        
Vitamin.D..µg.100g.                        0.1470879
Vitamin.E..mg.100g.                        9.1144274
Vitamin.K1..µg.100g.                       .        
Vitamin.K2..µg.100g.                       .        
Vitamin.C..mg.100g.                        .        
Vitamin.B1.or.Thiamin..mg.100g.            .        
Vitamin.B2.or.Riboflavin..mg.100g.         .        
Vitamin.B3.or.Niacin..mg.100g.             .        
Vitamin.B5.or.Pantothenic.acid..mg.100g.   .        
Vitamin.B6..mg.100g.                       .        
Vitamin.B9.or.Folate..µg.100g.             .        
Vitamin.B12..µg.100g.                      .        
vif(cv_lasso)
Error in UseMethod("vcov") : 
  no applicable method for 'vcov' applied to an object of class "cv.glmnet"

Confrontiamo con il risultato di caret in cross-validation.

train_control <- trainControl(method = "cv", number = 20)


lasso_model <- train(Energy..Regulation.EU.No.1169.2011..kJ.100g. ~ ., data = ciqual_reduced_train,  
                        method = "glmnet", 
                        trControl = train_control, 
                        tuneGrid = expand.grid(alpha = 1, lambda = seq(0, 20, length = 100)))

plot(lasso_model)

Confrontiamo i coefficienti selezionati.

lasso_model$bestTune$lambda
[1] 10.70707
cbind( coef(lasso_model$finalModel, lasso_model$bestTune$lambda), coef(cv_lasso))
50 x 2 sparse Matrix of class "dgCMatrix"
                                            s=10.70707  lambda.1se
(Intercept)                               7.472305e+01 149.9657571
Water..g.100g.                           -4.185668e-01  -0.6032222
Protein..g.100g.                          1.536153e+01  13.6781669
Protein..crude..N.x.6.25..g.100g.         .              .        
Carbohydrate..g.100g.                     9.716083e+00   4.9960658
Fat..g.100g.                              2.087898e+01  21.1724823
Sugars..g.100g.                          -1.080517e+01  -2.6449047
fructose..g.100g.                         4.098578e+00   .        
galactose..g.100g.                        .              .        
glucose..g.100g.                          1.341604e+01   1.6691924
lactose..g.100g.                          1.700698e+01   .        
maltose..g.100g.                          8.277471e+00   .        
sucrose..g.100g.                         -6.037565e+00  -5.6103001
Starch..g.100g.                           4.672916e+00   7.0525990
Fibres..g.100g.                           .              .        
Polyols..g.100g.                          1.941517e+00   1.1199149
Ash..g.100g.                              4.099386e-02   .        
Alcohol..g.100g.                          2.319661e+01  15.8331160
Organic.acids..g.100g.                    4.130024e+01   .        
FA.saturated..g.100g.                     1.232813e+01   9.6775364
FA.mono..g.100g.                          4.030925e+00   2.3824688
FA.poly..g.100g.                          .              .        
Cholesterol..mg.100g.                     4.800914e-02   .        
Salt..g.100g.                             .              .        
Calcium..mg.100g.                         .              .        
Chloride..mg.100g.                        .              .        
Copper..mg.100g.                         -2.794927e+01   .        
Iron..mg.100g.                            6.204059e+00   3.3243452
Iodine..µg.100g.                          2.964907e-04   .        
Magnesium..mg.100g.                      -1.063540e-01   .        
Manganese..mg.100g.                       6.147647e-01   .        
Phosphorus..mg.100g.                     -1.356200e-02   .        
Potassium..mg.100g.                       .              .        
Selenium..µg.100g.                        .              .        
Sodium..mg.100g.                          .              .        
Zinc..mg.100g.                            .              .        
Retinol..µg.100g.                         .              .        
Beta.carotene..µg.100g.                   .              .        
Vitamin.D..µg.100g.                       2.044689e+00   0.1470879
Vitamin.E..mg.100g.                       1.186171e+01   9.1144274
Vitamin.K1..µg.100g.                      .              .        
Vitamin.K2..µg.100g.                      .              .        
Vitamin.C..mg.100g.                       .              .        
Vitamin.B1.or.Thiamin..mg.100g.          -1.176463e+02   .        
Vitamin.B2.or.Riboflavin..mg.100g.        .              .        
Vitamin.B3.or.Niacin..mg.100g.            8.112183e+00   .        
Vitamin.B5.or.Pantothenic.acid..mg.100g.  .              .        
Vitamin.B6..mg.100g.                      .              .        
Vitamin.B9.or.Folate..µg.100g.            .              .        
Vitamin.B12..µg.100g.                     .              .        

Esercizi suggeriti

  1. Studia la multicollinearità nel dataset mtcars usando VIF e numero di condizionamento della matrice di Gram. Applica una riduzione del modello usando stepwise selection (sia forward che backward) e confronta i modelli risultanti in termini di AIC, BIC, VIF e interpretabilità.

  2. Utilizza il dataset mtcars e applica Ridge e Lasso regression per prevedere il consumo di carburante (mpg) in base al resto delle variabili. Confronta le prestazioni dei due modelli utilizzando RMSE e visualizza i coefficienti stimati. Discuti quale modello offre una migliore interpretabilità.

  3. Discuti la multicollinearità in un modello di regressione polinomiale (anche su dati generati).

  4. Genera un dataset in cui la PCR (su un certo numero \(k\) di variabili ruotate) peggiora la performance rispetto a OLS.

  5. Utilizza il dataset diamonds per eseguire un’analisi delle componenti principali. Seleziona un numero ridotto di componenti che spiegano una certa percentuale della varianza totale e utilizza queste componenti in un modello di regressione lineare per prevedere il prezzo. Valuta le prestazioni rispetto a un modello di regressione lineare standard.

  6. Utilizza bootstrap per stimare gli intervalli di confidenza dei coefficienti di un modello di regressione lineare regolarizzato su un dataset a scelta (ad esempio mtcars o swiss).

LS0tCnRpdGxlOiAiUmVncmVzc2lvbmUgSUkgKG5vdGVib29rIDgpIgphdXRob3I6ICJEYXJpbyBUcmV2aXNhbiIKZGF0ZTogIjEyLzExLzIwMjUiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDMKICAgIHRvY19mbG9hdDogdHJ1ZQogICAgdGhlbWU6IHJlYWRhYmxlCiAgICBkZl9wcmludDogcGFnZWQKICAgIGRvd25sb2FkX2hhbmRsZXI6IHRydWUKICBodG1sX2RvY3VtZW50OgogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6ICczJwogICAgZGZfcHJpbnQ6IHBhZ2VkCnN1YnRpdGxlOiAiU3RhdGlzdGljYSBJSSAtIDc1MEFBIgotLS0KCgoKCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQoja25pdHI6Om9wdHNfY2h1bmskc2V0KHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBlY2hvPUZBTFNFKSAKYGBgCgoKIyBJbnRyb2R1emlvbmUKCkluIHF1ZXN0byBub3RlYm9vayBlc3Bsb3JpYW1vIGxhIGNvbGxpbmVhcml0w6AgZSBtdWx0aWNvbGxpbmVhcml0w6AgbmVpIG1vZGVsbGkgZGkgcmVncmVzc2lvbmUgbGluZWFyZSwgZ2xpIGluZGljYXRvcmkgcHJpbmNpcGFsaSAoY29ycmVsb2dyYW1tYSwgVklGLCBudW1lcm8gZGkgY29uZGl6aW9uYW1lbnRvKSBlIGxlIHRlY25pY2hlIGRpIHJpZHV6aW9uZSBkZWkgbW9kZWxsaSAoc3RlcHdpc2UpIGUgcmVnb2xhcml6emF6aW9uZSAoUmlkZ2VlIExhc3NvKS4KCmBgYHtyfQpsaWJyYXJ5KGNhcikKbGlicmFyeShnZ2NvcnJwbG90KQpsaWJyYXJ5KEdHYWxseSkKbGlicmFyeShjYXJldCkgIyBQZXIgbGEgY3Jvc3MtdmFsaWRhdGlvbgpsaWJyYXJ5KGdsbW5ldCkgIyBQZXIgaSBtb2RlbGxpIHJlZ29sYXJpenphdGkgUmlkZ2UvTEFTU08KYGBgCgoKCiMjIERhdGFzZXQgYHN3aXNzYAoKQ29uc2lkZXJpYW1vIGlsIGRhdGFzZXQgYHN3aXNzYCBjaGUgY29udGllbmUgaW5kaWNhdG9yaSBkaSBmZXJ0aWxpdMOgIGUgc29jaW9lY29ub21pY2kgZGVsbGUgcHJvdmluY2UgKGRpIGxpbmd1YSBmcmFuY2VzZSkgZGVsbGEgc3ZpenplcmEgbmVsIDE4ODguCgpgYGB7cn0KZGF0YShzd2lzcykKP3N3aXNzCmhlYWQoc3dpc3MpCmBgYAoKU3R1ZGlhbW8gdW4gbW9kZWxsbyBkaSByZWdyZXNzaW9uZSBsaW5lYXJlIGNvbiBsYSB2YXJpYWJpbGUgYEZlcnRpbGl0eWAgKHVuIGluZGljZSBkaSBmZXJ0aWxpdMOgKSBjb21lIGZhdHRvcmUgZGkgdXNjaXRhLCBlIHR1dHRlIGxlIGFsdHJlIGNvbWUgY2FyYXR0ZXJpc3RpY2hlIGRpIGluZ3Jlc3NvLgoKYGBge3J9CiMgTW9kZWxsbyBpbml6aWFsZQpmaXRfc3dpc3NfZnVsbCA8LSBsbShGZXJ0aWxpdHkgfiAuLCBkYXRhID0gc3dpc3MpCnN1bW1hcnkoZml0X3N3aXNzX2Z1bGwpCmBgYAoKT3NzZXJ2aWFtbyBjaGUgYWxjdW5pICRwJC12YWx1ZSBkZWkgY29lZmZpY2llbnRpIHJpc3VsdGFubyBtb2x0byBlbGV2YXRpLCB1biBwcmltbyBzZWdubyBkaSBjb2xsaW5lYXJpdMOgLCBtZW50cmUgaWwgY29lZmZpY2llbnRlICRSXjIkIMOoIGFiYmFzdGFuemEgYWx0byBlIGlsICRwJC12YWx1ZSBkZWxsJyRGJC10ZXN0IMOoIGJhc3NvLCBzZWdubyBjaGUgaSBmYXR0b3JpIGRpIGluZ3Jlc3NvIG5lbCBtb2RlbGxvIHNwaWVnYW5vIHVuYSBidW9uYSBwYXJ0ZSBkZWxsJ3VzY2l0YS4KCiMjIyBEaWFnbm9zaSBkaSBjb2xsaW5lYXJpdMOgCgpBbmFsaXp6aWFtbyBsZSBjb3JyZWxhemlvbmkgdHJhIGkgZmF0dG9yaSBkaSBpbmdyZXNzbyBtZWRpYW50ZSBsYSBtYXRyaWNlIGRlbGxlIGNvcnJlbGF6aW9uaSBlIHVuIGNvcnJlbG9ncmFtbWEuCgpgYGB7cn0KIyBNYXRyaWNlIGRpIGNvcnJlbGF6aW9uZSAoYXBwcm9zc2ltYXRhIGEgZHVlIGNpZnJhIGRlY2ltYWxpIHBlciBzZW1wbGljaXTDoCkKcm91bmQoY29yKHN3aXNzWywgLTFdKSwgMikKYGBgCgoKYGBge3J9CiMgRGlhZ3JhbW1hIGEgbnV2b2xhIGRpIHB1bnRpIChub24gZWZmaWNhY2U6IHRyb3BwZSB2YXJpYWJpbGksIGV2ZW50dWFsbWVudGUgc2VsZXppb25hcm5lIGFsY3VuZSkKI3BhaXJzKG10Y2FycywgbWFpbiA9ICJTY2F0dGVycGxvdCAobXRjYXJzKSIpCgojIGhlYXRtYXAgY29uIGdnY29ycnBsb3QKbGlicmFyeShnZ2NvcnJwbG90KQpjb3JyX21hdF9tdGNhcnMgPC0gY29yKHN3aXNzWywgLTFdKQpnZ2NvcnJwbG90KGNvcnJfbWF0X210Y2FycywgbGFiID0gVFJVRSwgdHlwZSA9ICJsb3dlciIsCiAgICAgICAgICAgdGl0bGUgPSAiTWF0cmljZSBkaSBjb3JyZWxhemlvbmUgdHJhIHByZWRpdHRvcmkgKG10Y2FycykiKQpgYGAKCkFiYmlhbW8gYWxjdW5lIGNvcnJlbGF6aW9uaSB0cmEgY29wcGllIChgRXhhbWluYXRpb25gIGUgYEVkdWNhdGlvbmApLCBpbCBjYXNvIHBpw7kgc2VtcGxpY2UgZGkgY29sbGluZWFyaXTDoC4gU3R1ZGlhbW8gZ2xpIGluZGljYXRvcmkgc3BlY2lmaWNpLCBpbCBWSUYgZSBpbCBjb25kaXppb25hbWVudG8gZGVsbGEgbWF0cmljZSBkaSBHcmFtLiBQZXIgaWwgcHJpbW8gdXNpYW1vIGlsIGNvbWFuZG8gYHZpZigpYCBkYWwgcGFjY2hldHRvIGBjYXJgLgoKCmBgYHtyfQojIFZhcmlhbmNlIEluZmxhdGlvbiBGYWN0b3IKbGlicmFyeShjYXIpCgp2aWYoZml0X3N3aXNzX2Z1bGwpCmBgYApSaWNvcmRpYW1vIGNoZSB2YWxvcmkgbWFnZ2lvcmkgZGkgJDUkIGluZGljYW5vIHZhcmlhYmlsaSBwZXIgY3VpIHBvdHJlYmJlIGVzc2VyZSBwcmVzZW50ZSBjb2xsaW5lYXJpdMOgIChpbiBwYXJ0aWNvbGFyZSBzZSBtYWdnaW9yZSBkaSAkMTAkKS4gTm90aWFtbyBjaGUgbmVzc3VuIFZJRiBzdXBlcmEgbGEgc29nbGlhICQ1JCBtYSBgRXhhbWluYXRpb25gIHByZXNlbnRhIHVuIHZhbG9yZSBjb211bnF1ZSBncmFuZGUuCgpWZWRpYW1vIGluZmluZSBsYSBtYXRyaWNlIGRpIEdyYW0gZSBpbCBudW1lcm8gZGkgY29uZGl6aW9uYW1lbnRvLgoKYGBge3J9CgojIGVzdHJhZSBsYSBtYXRyaWNlIGRpIGRlc2lnbiBkZWwgbW9kZWxsbyAoZGF0aSBjb24gdW5hIGNvbG9ubmEgZGkgMSBwZXIgbCdpbnRlcmNldHRhKQpzd2lzc19mZWF0dXJlcyA8LSBtb2RlbC5tYXRyaXgoZml0X3N3aXNzX2Z1bGwpCgojIGNhbGNvbGEgbGEgbWF0cmljZSBkaSBHcmFtCkdfc3dpc3MgPC0gdChzd2lzc19mZWF0dXJlcykgJSolIHN3aXNzX2ZlYXR1cmVzCgpHX3N3aXNzCmBgYAoKClBlciBpbCBudW1lcm8gZGkgY29uZGl6aW9uYW1lbnRvIHVzaWFtbyBsYSBmdW56aW9uZSBgYG5vcm0oKWBgIHBlciBjYWxjb2xhcmUgKHVuYSkgbm9ybWEgZGVsbGEgbWF0cmljZSAkRyQgZSBgYHNvbHZlKClgYCBwZXIgbCdpbnZlcnNhLgoKYGBge3J9CmthcHBhX0cgPC0gbm9ybShHX3N3aXNzLCB0eXBlPSIyIikgKiBub3JtKHNvbHZlKEdfc3dpc3MsIHR5cGU9IjIiKSkKCmthcHBhX0cKYGBgCgpVbiB2YWxvcmUgbWFnZ2lvcmUgZGkgJDEwMDAkIGluZGljYSBjaGUgJEckIMOoICptYWwgY29uZGl6aW9uYXRhKiAoc2Vnbm8gZGkgY29sbGluZWFyaXTDoCkuIFZlZGlhbW8gcGVyIGNvbmZyb250byB1biBzZW1wbGljZSBlc2VtcGlvIGdlbmVyYXRvLgoKCmBgYHtyfQojIGdlbmVyYSB1bmEgbWF0cmljZSBxdWFzaSBub24taW52ZXJ0aWJpbGUKZXBzID0gMTBeey0zfQoKRyA9IG1hdHJpeCggYygxLCAxLWVwcywgMS1lcHMsIDEgKSwgYnlyb3c9VFJVRSwgbnJvdz0yLCBuY29sPTIpCgpHCgprYXBwYV9HID0gbm9ybShHKSpub3JtKHNvbHZlKEcpKQpwcmludChwYXN0ZSgiTnVtZXJvIGRpIGNvbmRpemlvbmFtZW50bzoiLCByb3VuZChrYXBwYV9HLCAwKSkpCmBgYAoKClVuIGluZGljYXRvcmUgYWx0ZXJuYXRpdm8gw6ggaWwgZGV0ZXJtaW5hbnRlIGRlbGxhIG1hdHJpY2UgZGkgY29ycmVsYXppb25lIGRlaSBmYXR0b3JpIGRpIGluZ3Jlc3NvLgoKYGBge3J9CnJvdW5kKGNvcihzd2lzcyksIDIpCmRldChjb3Ioc3dpc3MpKQpgYGAKCgojIyMgUmlkdXppb25lIGRlbCBtb2RlbGxvCgpQZXIgZWxpbWluYXJlIGV2ZW50dWFsaSB2YXJpYWJpbGkgY29sbGluZWFyaSwgY29uc2lkZXJpYW1vIGdsaSBhcHByb2NjaSBiYXNhdGkgc3VpIHRlc3QgZGkgc2lnbmlmaWNhdGl2aXTDoCBkZWkgY29lZmZpY2llbnRpIGUgc3VnbGkgaW5kaWNpIEFJQy9CSUMuCgpSaXByZW5kaWFtbyBpICRwJC12YWx1ZSBkZWwgbW9kZWxsbyBjb21wbGV0by4KCmBgYHtyfQpzdW1tYXJ5KGZpdF9zd2lzc19mdWxsKQpgYGAKClVuYSBzb2x1emlvbmUgcmFwaWRhIChtYSBwb3RlbnppYWxtZW50ZSByaXNjaGlvc2EhKSDDqCBkaSBlbGltaW5hcmUgZGlyZXR0YW1lbnRlIGxlIHZhcmlhYmlsaSBjb24gJHAkLXZhbHVlIG1hZ2dpb3JlIGRpIHVuYSBkZXRlcm1pbmF0YSBzb2dsaWEgKGFkIGVzZW1waW8gMC4xKS4gCgpgYGB7cn0KZml0X3N3aXNzX3JlZHVjZWQgPC0gbG0oRmVydGlsaXR5IH4gLi0gQWdyaWN1bHR1cmUgLSBFeGFtaW5hdGlvbiwgZGF0YSA9IHN3aXNzKQpzdW1tYXJ5KGZpdF9zd2lzc19yZWR1Y2VkKQpgYGAKSWwgY29lZmZpY2llbnRlICRSXjIkIChvIG1lZ2xpbyBxdWVsbG8gKkFkanVzdGVkKiBjaGUgdGllbmUgY29udG8gZGVsIG51bWVybyBkaSB2YXJpYWJpbGkpIMOoIHNjZXNvIGxlZ2dlcm1lbnRlIG1hIG5vbiBkcmFzdGljYW1lbnRlLiBDb25mcm9udGlhbW8gY29uIGlsIG1vZGVsbG8gaW4gY3VpIGVsaW1pbmlhbW8gc29sbyBgRXhhbWluYXRpb25gLgoKYGBge3J9CmZpdF9zd2lzc19yZWR1Y2VkX29uZV9zdGVwIDwtIGxtKEZlcnRpbGl0eSB+IC4tIEV4YW1pbmF0aW9uLCBkYXRhID0gc3dpc3MpCnN1bW1hcnkoZml0X3N3aXNzX3JlZHVjZWRfb25lX3N0ZXApCmBgYApJbiBxdWVzdG8gY2FzbyBsJypBZGp1c3RlZCogJFJeMiQgw6ggcmltYXN0byBpbnZhcmlhdG8uCgoqKlByb2JsZW1hOioqIGRpc2N1dGVyZSBjb3NhIGFjY2FkZSBzZSBpbnZlY2Ugc2kgcmltdW92ZSBsYSB2YXJpYWJpbGUgYEVkdWNhdGlvbmAuCgoKUmljb3JkaWFtbyBjaGUgQUlDIGUgQklDIGNlcmNhbm8gZGkgaW5kaWNhcmUgbGEgcGVyZm9ybWFuY2UgZGVsIG1vZGVsbG8gaW4gdGVybWluaSBkZWwgY29tcHJvbWVzc28gdHJhIGJpYXMgKG51bWVybyBkaSBwYXJhbWV0cmkpIGUgdmFyaWFuemEgKHZhbG9yZSBkZWxsYSB2ZXJvc2ltaWdsaWFuemEpLiBQZXIgY2FsY29sYXJsaSDDqCBzdWZmaWNpZW50ZSB1c2FyZSBsYSBmdW56aW9uZSBgQUlDKClgIAoKYGBge3J9CkFJQyhmaXRfc3dpc3NfZnVsbCwgZml0X3N3aXNzX3JlZHVjZWQsIGZpdF9zd2lzc19yZWR1Y2VkX29uZV9zdGVwKQpgYGAKVmVkaWFtbyBxdWluZGkgY2hlIGlsIG1vZGVsbG8gaW4gY3VpIGFiYmlhbW8gZWxpbWluYXRvIHNvbG8gbGEgdmFyaWFiaWxlIGBFeGFtaW5hdGlvbmAgw6ggcHJlZmVyaWJpbGUgaW4gdGVybWluaSBkaSBBSUMuIFNpbWlsbWVudGUgcGVyIGlsIEJJQzoKCmBgYHtyfQpCSUMoZml0X3N3aXNzX2Z1bGwsIGZpdF9zd2lzc19yZWR1Y2VkLCBmaXRfc3dpc3NfcmVkdWNlZF9vbmVfc3RlcCkKYGBgCgpPdnZpYW1lbnRlIG5vbiBzaSBwdcOyIHByb2NlZGVyZSAqYSBtYW5vKiBjb24gbGEgcmlkdXppb25lIGRlbCBtb2RlbGxvLCBzcGVjaWFsbWVudGUgc2UgY2kgc29ubyBtb2x0ZSBjYXJhdHRlcmlzdGljaGUuIFBlciBxdWVzdG8gbGUgcHJvY2VkdXJlICpzdGVwd2lzZSogc29ubyBhdXRvbWF0aXp6YXRlLiBVc2lhbW8gaWwgY29tYW5kbyBgc3RlcGAgKGdpw6AgY2FyaWNhdG8pLgoKClNlbGV6aW9uZSBhbGwnaW5kaWV0cm8uCgpgYGB7cn0KIyBCYWNrd2FyZApmaXRfYmFjayA8LSBzdGVwKGZpdF9zd2lzc19mdWxsLCBkaXJlY3Rpb24gPSAiYmFja3dhcmQiLCB0cmFjZSA9IDApICMgbWV0dGVyZSB0cmFjZSA9IDEgbyBUUlVFIHBlciBhdmVyZSBpbmZvcm1hemlvbmkgc3VpIHBhc3NpIGRlbCdhbGdvcml0bW8KCnN1bW1hcnkoZml0X2JhY2spCmBgYAoKU2VsZXppb25lIGluIGF2YW50aS4KCmBgYHtyfQojIEZvcndhcmQKIyBpbml6aWFtbyBjb24gaWwgbW9kZWxsbyBjb3N0YW50ZSAoYmFuYWxlKQpmaXRfc3dpc3NfdHJpdmlhbCA8LSBsbShGZXJ0aWxpdHkgfiAxLCBkYXRhID0gc3dpc3MpCgoKZml0X2ZvcncgPC0gc3RlcChmaXRfc3dpc3NfdHJpdmlhbCwKICAgICAgICAgICAgICAgICBkaXJlY3Rpb24gPSAiZm9yd2FyZCIsCiAgICAgICAgICAgICAgICAgc2NvcGUgPSBmb3JtdWxhKGZpdF9zd2lzc19mdWxsKSwgI3NwZWNpZmljYSB1biBtb2RlbGxvIGBtYXNzaW1vYAogICAgICAgICAgICAgICAgIHRyYWNlID0gRkFMU0UpCnN1bW1hcnkoZml0X2ZvcncpCmBgYAoKRSBpbmZpbmUgdW5hIHNlbGV6aW9uZSBpbiBlbnRyYW1iZSBsZSBkaXJlemlvbmkuCgpgYGB7cn0KIyBCb3RoIGRpcmVjdGlvbnMKZml0X2JvdGggPC0gc3RlcChmaXRfc3dpc3NfZnVsbCwgZGlyZWN0aW9uID0gImJvdGgiLCB0cmFjZSA9IEZBTFNFLCBrPWxvZyhucm93KHN3aXNzKSkpICNpbiBxdWVzdG8gY2FzbyBpbXBvc3RpYW1vIEJJQwoKc3VtbWFyeShmaXRfYm90aCkKYGBgCgoqKkF0dGVuemlvbmU6KiogTGUgcHJvY2VkdXJlIHN0ZXB3aXNlIG90dGltaXp6YW5vIHVuIGNyaXRlcmlvIChBSUMsIEJJQyksIG1hIG5vbiBzZW1wcmUgcG9ydGFubyBhIHVuIG1vZGVsbG8gaW50ZXJwcmV0YWJpbGUgbyBzdGFiaWxlLiDDiCBidW9uYSBwcmF0aWNhIGNvbnNpZGVyaWFyZSBnbGkgaW5kaWNhdG9yaSAoJHAkLXZhbHVlLCBWSUYpIGFuY2hlIGRvcG8gbGEgcmlkdXppb25lIGF1dG9tYXRpY2EuCgpgYGB7cn0KdmlmKGZpdF9ib3RoKQpgYGAKCiMjIyBQQ1IKClVuIGFwcHJvY2NpbyBkaWZmZXJlbnRlIGNvbnNpc3RlIG5lbGwnYXBwbGljYXJlIGxhIFBDQSBzdWxsZSBjYXJhdHRlcmlzdGljaGUgaW4gaW5wdXQuIERpdmlkaWFtbyBhbCBzb2xpdG8gaW4gdHJhaW5pbmcgZSB0ZXN0IHNldCBjb24gaWwgY29tYW5kbyBgY3JlYXRlRGF0YVBhcnRpdGlvbmAuCgoKYGBge3J9CmxpYnJhcnkoY2FyZXQpCgojIERpdmlkaWFtbyBpbiB0cmFpbmluZyBlIHRlc3Qgc2V0IGlsIGRhdGFzZXQgY2lxdWFsX3JlZHVjZWQKCnRyYWluX2luZGV4IDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oc3dpc3MkRmVydGlsaXR5LCAgcCA9IDAuOCwgbGlzdCA9IEZBTFNFKQoKc3dpc3NfdHJhaW4gPC0gc3dpc3NbdHJhaW5faW5kZXgsIF0Kc3dpc3NfdGVzdCA8LSBzd2lzc1stdHJhaW5faW5kZXgsIF0KYGBgCgoKRWZmZXR1aWFtbyBsYSBQQ0Egc3VsbGUgdmFyaWFiaWxpIGRpIGluZ3Jlc3NvIChlc2NsdWRpYW1vIGxhIHByaW1hIGNvbG9ubmEsIGxhIHZhcmlhYmlsZSBkaSB1c2NpdGEpIGNvbiBpbCBjb21hbmRvICBgcHJlUHJvY2VzcygpYCBkYWwgcGFjY2hldHRvIGBjYXJldGAuIEluIHF1ZXN0byBtb2RvIHNhcsOgIHBpw7kgc2VtcGxpY2UgZmFyZSBwcmV2aXNpb25pIGNvbiBpbCBtb2RlbGxvIChhbHRyaW1lbnRpIGJpc29nbmVyZWJiZSB0cmFzZm9ybWFyZSBvZ25pIHZvbHRhIGxlIGNhcmF0dGVyaXN0aWNoZSBkaSBpbnB1dCBkaSB1biBwdW50byBkaSB0ZXN0KQoKYGBge3J9CgojIHBvc3NpYW1vIHNwZWNpZmljYXJlIHF1YW50ZSBjb21wb25lbnRpIHByaW5jaXBhbGksIGluIHF1ZXN0byBjYXNvIHBjYUNvbXA9My4gw4ggYW5jaGUgcG9zc2liaWxlIGltcG9zdGFyZSB1bmEgc29nbGlhIHN1bGxhIHBlcmNlbnR1YWxlIGRpIHZhcmlhbnphIGN1bXVsYXRhIGNvbiBsJ29wemlvbmUgdGhyZXNoPTAuOTUKcGNhX21vZGVsIDwtIHByZVByb2Nlc3Moc3dpc3NfdHJhaW5bLCAtMV0sIG1ldGhvZCA9IGMoImNlbnRlciIsICJzY2FsZSIsICJwY2EiKSwgcGNhQ29tcD0zKQpgYGAKClBvc3NpYW1vIG9yYSBjYWxjb2xhcmUgaSBmYXR0b3JpIGRpIGluZ3Jlc3NvIHByb2lldHRhdGkgc3VsbGUgY29tcG9uZW50aSBwcmluY2lwYWxpIGNvbiBpbCBjb21hbmRvIHByZWRpY3QgKHNpYSBzdWwgdHJhaW5pbmcgY2hlIHN1bCB0ZXN0IHNldCkuCgpgYGB7cn0KcGNhX3RyYWluIDwtIHByZWRpY3QocGNhX21vZGVsLCBzd2lzc190cmFpblssIC0xXSkKaGVhZChwY2FfdHJhaW4pCgpwY2FfdGVzdCA8LSBwcmVkaWN0KHBjYV9tb2RlbCwgc3dpc3NfdGVzdFssIC0xXSkKaGVhZChwY2FfdGVzdCkKYGBgCgpFZmZldHR1aWFtbyBxdWluZGkgbGEgcmVncmVzc2lvbmUgdXNhbmRvIGNvbWUgZmF0dG9yaSBkaSBpbmdyZXNzbyBsZSBQQyBzZWxlemlvbmF0ZS4KCmBgYHtyfQpmaXRfcGNyIDwtIGxtKHN3aXNzX3RyYWluJEZlcnRpbGl0eX4uLCBkYXRhPXBjYV90cmFpbikKc3VtbWFyeShmaXRfcGNyKQp2aWYoZml0X3BjcikKYGBgCgpMZSBwcmltZSAyIGNvbXBvbmVudGkgc3BpZWdhbm8gZ2nDoCBwYXJ0ZSBkZWxsYSB2YXJpYWJpbGl0w6AgZSByaWR1Y29ubyAobmVjZXNzYXJpYW1lbnRlKSBsYSBjb2xsaW5lYXJpdMOgLgoKCiMjIyBSZWdvbGFyaXp6YXppb25lOiBSaWRnZSBlIExBU1NPCgpVc2lhbW8gaWwgcGFjY2hldHRvIGBjYXJldGAgcGVyIHVzYXJlIFJpZGdlLCBMYXNzbyAoYW5jaGUgYWx0cmkgbW9kZWxsaSByZWdvbGFyaXp6YXRpIHNhcmViYmVybyBkaXNwb25pYmlsaSkgZSBkZXRlcm1pbmFyZSBpIHBhcmFtZXRyaSB1c2FuZG8gY3Jvc3MtdmFsaWRhdGlvbi4gVW4gcGFjY2hldHRvIGFsdGVybmF0aXZvIHBpw7kgc3BlY2lmaWNvIMOoIGBnbG1uZXRgLgoKRGVmaW5pYW1vIHByaW1hIGlsIG1ldG9kbyBkaSBjcm9zcy12YWxpZGF0aW9uLgoKYGBge3J9CnRyYWluX2NvbnRyb2wgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJjdiIsIG51bWJlciA9IDEwKSAjMTAtZm9sZCBjdgpgYGAKClBlciBsYSByZWdyZXNzaW9uZSBSaWRnZSwgc3BlY2lmaWNoaWFtbyBpbCBtZXRvZG8gImdsbW5ldCIgY29uIGlsIHBhcmFtZXRybyAkXGFscGhhPTAkIChnbG1uZXQgw6ggdW4gbW9kZWxsbyAqRWxhc3RpYyBOZXQqIGNoZSBwZXJtZXR0ZSBkaSBpbnRlcnBvbGFyZSB0cmEgUmlkZ2UgZSBMQVNTTykuCgpgYGB7cn0KCnJpZGdlX21vZGVsIDwtIHRyYWluKEZlcnRpbGl0eSB+IC4sIGRhdGEgPSBzd2lzc190cmFpbiwgIAogICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiZ2xtbmV0IiwgCiAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IHRyYWluX2NvbnRyb2wsIAogICAgICAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGV4cGFuZC5ncmlkKGFscGhhID0gMCwgbGFtYmRhID0gc2VxKDAsIDEsIGxlbmd0aCA9IDEwMCkpKQpgYGAKClBsb3R0aWFtbyBsJ2Vycm9yZSBkaSBDViBhbCB2YXJpYXJlIGRlbCBwYXJhbWV0cm8gZGkgcmVnb2xhcml6emF6aW9uZSAkXGxhbWJkYSQuCgpgYGB7cn0KcGxvdChyaWRnZV9tb2RlbCwgdHlwZT0nbCcpCmBgYApWZWRpYW1vIHF1aW5kaSBjaGUgaWwgcGFyYW1ldHJvIG90dGltYWxlIChzY2VsdG8gdHJhbWl0ZSBjcm9zcy12YWxpZGF0aW9uKSDDqCBgYHIgcmlkZ2VfbW9kZWwkYmVzdFR1bmUkbGFtYmRhCmBgLiBWZWRpYW1vIGkgcGFyYW1ldHJpIGRldGVybWluYXRpLgoKYGBge3J9Cgpjb2VmKHJpZGdlX21vZGVsJGZpbmFsTW9kZWwsIHJpZGdlX21vZGVsJGJlc3RUdW5lJGxhbWJkYSkKCmBgYAoKUGVyIGxhIHJlZ3Jlc3Npb25lIExBU1NPLCBzcGVjaWZpY2hpYW1vIGlsIG1ldG9kbyAiZ2xtbmV0IiBjb24gaWwgcGFyYW1ldHJvICRcYWxwaGE9MSQuCgpgYGB7cn0KCmxhc3NvX21vZGVsIDwtIHRyYWluKEZlcnRpbGl0eSB+IC4sIGRhdGEgPSBzd2lzc190cmFpbiwgIAogICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiZ2xtbmV0IiwgCiAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IHRyYWluX2NvbnRyb2wsIAogICAgICAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGV4cGFuZC5ncmlkKGFscGhhID0gMSwgbGFtYmRhID0gc2VxKDAsIDIsIGxlbmd0aCA9IDEwMCkpKQpgYGAKClBsb3R0aWFtbyBsJ2Vycm9yZSBkaSBDViBhbCB2YXJpYXJlIGRlbCBwYXJhbWV0cm8gZGkgcmVnb2xhcml6emF6aW9uZSAkXGxhbWJkYSQuCgpgYGB7cn0KcGxvdChsYXNzb19tb2RlbCwgdHlwZT0nbCcpCmBgYAoKYGBge3J9Cmxhc3NvX21vZGVsJGJlc3RUdW5lJGxhbWJkYQoKY29lZihsYXNzb19tb2RlbCRmaW5hbE1vZGVsLCBsYXNzb19tb2RlbCRiZXN0VHVuZSRsYW1iZGEpCgpgYGAKCkluIGFsdGVybmF0aXZhIHBvc3NpYW1vIHVzYXJlIGlsIGNvbWFuZG8gYGBjdi5nbG0oKWBgIGRhbCBwYWNjaGV0dG8gYGBnbG1uZXRgYC4KCmBgYHtyfQpsaWJyYXJ5KGdsbW5ldCkKWCA8LSBhcy5tYXRyaXgoc3dpc3NfdHJhaW5bLCAtMV0pCnkgPC0gc3dpc3NfdHJhaW4kRmVydGlsaXR5CmN2X3JpZGdlIDwtIGN2LmdsbW5ldChYLCB5LCBhbHBoYSA9IDApCnBsb3QoY3ZfcmlkZ2UpCgojIGNvbmZyb250YSBpIGxhbWJkYSB0cm92YXRpIHRyYW1pdGUgaSBkdWUgbWV0b2RpCnByaW50KHBhc3RlKCJsYW1iZGFfbWluIGdsbW5ldDoiLCBjdl9yaWRnZSRsYW1iZGEubWluKSkKcHJpbnQocGFzdGUoImxhbWJkYV9taW5fY2FyZXQ6IiwgcmlkZ2VfbW9kZWwkYmVzdFR1bmUkbGFtYmRhKSkKCiMgY29uZnJvbnRvIGRlaSBjb2VmZmljaWVudGkgZGVsIG1vZGVsbG8gdHJvdmF0aSBwZXIgaSBkdWUgbWV0b2RpCmNvZWYoY3ZfcmlkZ2UpCmNvZWYocmlkZ2VfbW9kZWwkZmluYWxNb2RlbCwgcmlkZ2VfbW9kZWwkYmVzdFR1bmUkbGFtYmRhKQoKCmBgYApPcGVyaWFtbyBzaW1pbG1lbnRlIGNvbiBMQVNTTy4KCmBgYHtyfQpjdl9sYXNzbyA8LSBjdi5nbG1uZXQoWCwgeSwgYWxwaGEgPSAxKQpwbG90KGN2X2xhc3NvKQoKIyBjb25mcm9udGEgaSBsYW1iZGEgdHJvdmF0aSB0cmFtaXRlIGkgZHVlIG1ldG9kaQpwcmludChwYXN0ZSgibGFtYmRhX21pbiBnbG1uZXQ6IiwgY3ZfbGFzc28kbGFtYmRhLm1pbikpCnByaW50KHBhc3RlKCJsYW1iZGFfbWluX2NhcmV0OiIsIGxhc3NvX21vZGVsJGJlc3RUdW5lJGxhbWJkYSkpCgojIGNvbmZyb250byBkZWkgY29lZmZpY2llbnRpIGRlbCBtb2RlbGxvIHRyb3ZhdGkgcGVyIGkgZHVlIG1ldG9kaQpjb2VmKGN2X2xhc3NvKQpjb2VmKGxhc3NvX21vZGVsJGZpbmFsTW9kZWwsIGxhc3NvX21vZGVsJGJlc3RUdW5lJGxhbWJkYSkKYGBgCgojIyBEYXRhc2V0IENJUVVBTAoKUmlwcmVuZGlhbW8gaWwgZGF0YXNldCBbQ0lRVUFMXShodHRwczovL2NpcXVhbC5hbnNlcy5mci8jL2Ntcy9kb3dubG9hZC9ub2RlLzIwKSAgc3VsbGUgY2FyYXR0ZXJpc3RpY2hlIG51dHJpdGl2ZSBkZWdsaSBhbGltZW50aS4gQ2FyaWNoaWFtbyBsYSB2ZXJzaW9uZSBgLmNzdmAgZ2nDoCAqcmlwdWxpdGEqIG5lbCBjb3JzbyBkZWxsYSBsZXppb25lIHN1bGxhIFBDQS4KCmBgYHtyfQpjaXF1YWxfdGlkeSA8LSByZWFkLmNzdignZGF0YXNldHMvY2lxdWFsX3RpZHkuY3N2JywgaGVhZGVyID0gVFJVRSkKCmhlYWQoY2lxdWFsX3RpZHkpCmBgYAoKRWxpbWluaWFtbyBsZSBwcmltZSBkdWUgY29sb25uZSAoY29uIGxlIGNsYXNzaSBkaSBhbGltZW50aSkgZSB1c2lhbW8gbGEgdGVyemEgY29tZSBjb2xvbm5hIGRpIG5vbWkgKGluIG1vZG8gZGEgbm9uIGluY2x1ZGVybGEgbmVsbGEgcmVncmVzc2lvbmUpLgoKYGBge3J9CgpjaXF1YWxfcmVkdWNlZCA8LSBjaXF1YWxfdGlkeVssIC0oMTozKV0KCnJvdy5uYW1lcyhjaXF1YWxfcmVkdWNlZCkgPC0gY2lxdWFsX3RpZHlbLDNdCgpoZWFkKGNpcXVhbF9yZWR1Y2VkKQoKYGBgCgpQb25pYW1vY2kgY29tZSBvYmlldHRpdm8gZGkgY2FsY29sYXJlIGwnZW5lcmdpYSAoaW4ga2NhbCkgYSBwYXJ0aXJlIGRhbGxlIGFsdHJlIGluZm9ybWF6aW9uaSBudXRyaXRpdmUuIFJpbXVvdmlhbW8gcXVpbmRpIGxlIGNvbG9ubmUgKHByYXRpY2FtZW50ZSBkdXBsaWNhdGUpIGRlbGwnZW5lcmdpYSAodGVuaWFtbyBzb2xvIGxhIHByaW1hKS4KCmBgYHtyfQoKY2lxdWFsX3JlZHVjZWQgPC0gY2lxdWFsX3JlZHVjZWRbLC0oMjo0KV0KCmhlYWQoY2lxdWFsX3JlZHVjZWQpCmBgYAoKQ29uc2lkZXJpYW1vIGlsIG1vZGVsbG8gZGkgcmVncmVzc2lvbmUgbGluZWFyZSBjb21wbGV0by4KCmBgYHtyfQoKY2lxdWFsX2ZpdCA8LSBsbSggY2lxdWFsX3JlZHVjZWQkRW5lcmd5Li5SZWd1bGF0aW9uLkVVLk5vLjExNjkuMjAxMS4ua0ouMTAwZy4gfi4sIGRhdGE9Y2lxdWFsX3JlZHVjZWQpCgpzdW1tYXJ5KGNpcXVhbF9maXQpCmBgYApWaXN1YWxpenppYW1vIGxlIHNpZ25pZmljYXRpdml0w6AgZSBpIFZJRgoKYGBge3J9CiMgcGxvdCBkZWxsZSBzaWduaWZpY2F0aXZpdMOgICgtbG9nKHAtdmFsdWUpKQpsaWJyYXJ5KGdncGxvdDIpCmNpcXVhbF9jb2VmcyA8LSBzdW1tYXJ5KGNpcXVhbF9maXQpJGNvZWZmaWNpZW50cwoKY2lxdWFsX2NvZWZzX2RmIDwtIGRhdGEuZnJhbWUoCiAgVGVybSA9IHJvd25hbWVzKGNpcXVhbF9jb2VmcylbLTFdLCAgIyBlc2NsdWRpIGwnaW50ZXJjZXR0YQogIE5lZ0xvZ1BWYWx1ZSA9IC1sb2cxMChjaXF1YWxfY29lZnNbLTEsICJQcig+fHR8KSJdKSwgICMgZXNjbHVkaSBsJ2ludGVyY2V0dGEKICBWSUYgPSB2aWYoY2lxdWFsX2ZpdCkKKQpnZ3Bsb3QoY2lxdWFsX2NvZWZzX2RmLCBhZXMoeCA9IHJlb3JkZXIoVGVybSwgTmVnTG9nUFZhbHVlKSwgeSA9IE5lZ0xvZ1BWYWx1ZSkpICsKICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJzdGVlbGJsdWUiKSArCiAgY29vcmRfZmxpcCgpICsKICBsYWJzKHRpdGxlID0gIi1sb2cxMChwLXZhbHVlKSBkZWkgY29lZmZpY2llbnRpIGRlbCBtb2RlbGxvIENJUVVBTCIsIHggPSAiVGVybWluZSIsIHkgPSAiLWxvZzEwKHAtdmFsdWUpIikgKwogIHRoZW1lX21pbmltYWwoKQoKZ2dwbG90KGNpcXVhbF9jb2Vmc19kZiwgYWVzKHggPSByZW9yZGVyKFRlcm0sIFZJRiksIHkgPSBWSUYpKSArCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIsIGZpbGwgPSAiZGFya29yYW5nZSIpICsKICBjb29yZF9mbGlwKCkgKwogIGxhYnModGl0bGUgPSAiVmFyaWFuY2UgSW5mbGF0aW9uIEZhY3RvciAoVklGKSBkZWkgY29lZmZpY2llbnRpIGRlbCBtb2RlbGxvIENJUVVBTCIsIHggPSAiVGVybWluZSIsIHkgPSAiVklGIikgKwogIHRoZW1lX21pbmltYWwoKQoKCgpgYGAKCk1vbHRlIGNhcmF0dGVyaXN0aWNoZSBoYW5ubyB1biAkcCQtdmFsdWUgYWx0bywgbWEgcG90cmViYmUgZXNzZXJjaSBjb2xsaW5lYXJpdMOgIChhZCBlc2VtcGlvIHRyYSBnbGkgenVjY2hlcmkpLiAKCgojIyMgUmlkdXppb25lIHN0ZXB3aXNlIAoKUHJvY2VkaWFtbyBjb24gdW5hIHJpZHV6aW9uZSBhbGwnaW5kaWV0cm8uIAoKYGBge3J9CmNpcXVhbF9yZWR1Y2VkX3N0ZXB3aXNlIDwtIHN0ZXAoY2lxdWFsX2ZpdCwgZGlyZWN0aW9uID0gImJhY2t3YXJkIiwgdHJhY2U9MCkKCnN1bW1hcnkoY2lxdWFsX3JlZHVjZWRfc3RlcHdpc2UpCmBgYAoKUGxvdHRpYW1vIGFsIHNvbGl0byBzaWduaWZpY2F0aXZpdMOgIGUgVklGLgoKYGBge3J9CmNpcXVhbF9jb2VmcyA8LSBzdW1tYXJ5KGNpcXVhbF9yZWR1Y2VkX3N0ZXB3aXNlKSRjb2VmZmljaWVudHMKCmNpcXVhbF9jb2Vmc19kZiA8LSBkYXRhLmZyYW1lKAogIFRlcm0gPSByb3duYW1lcyhjaXF1YWxfY29lZnMpWy0xXSwgICMgZXNjbHVkaSBsJ2ludGVyY2V0dGEKICBOZWdMb2dQVmFsdWUgPSAtbG9nMTAoY2lxdWFsX2NvZWZzWy0xLCAiUHIoPnx0fCkiXSksICAjIGVzY2x1ZGkgbCdpbnRlcmNldHRhCiAgVklGID0gdmlmKGNpcXVhbF9yZWR1Y2VkX3N0ZXB3aXNlKQopCmdncGxvdChjaXF1YWxfY29lZnNfZGYsIGFlcyh4ID0gcmVvcmRlcihUZXJtLCBOZWdMb2dQVmFsdWUpLCB5ID0gTmVnTG9nUFZhbHVlKSkgKwogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBmaWxsID0gInN0ZWVsYmx1ZSIpICsKICBjb29yZF9mbGlwKCkgKwogIGxhYnModGl0bGUgPSAiLWxvZzEwKHAtdmFsdWUpIGRlaSBjb2VmZmljaWVudGkgZGVsIG1vZGVsbG8gQ0lRVUFMIiwgeCA9ICJUZXJtaW5lIiwgeSA9ICItbG9nMTAocC12YWx1ZSkiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpnZ3Bsb3QoY2lxdWFsX2NvZWZzX2RmLCBhZXMoeCA9IHJlb3JkZXIoVGVybSwgVklGKSwgeSA9IFZJRikpICsKICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJkYXJrb3JhbmdlIikgKwogIGNvb3JkX2ZsaXAoKSArCiAgbGFicyh0aXRsZSA9ICJWYXJpYW5jZSBJbmZsYXRpb24gRmFjdG9yIChWSUYpIGRlaSBjb2VmZmljaWVudGkgZGVsIG1vZGVsbG8gQ0lRVUFMIiwgeCA9ICJUZXJtaW5lIiwgeSA9ICJWSUYiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpgYGAKClByb2NlZGlhbW8gaW4gYWx0ZXJuYXRpdmEgY29uIHVuYSBzZWxlemlvbmUgaW4gYXZhbnRpCgpgYGB7cn0KCmNpcXVhbF9yZWR1Y2VkX3N0ZXB3aXNlX2ZvcndhcmQgPC0gc3RlcChsbShjaXF1YWxfcmVkdWNlZCRFbmVyZ3kuLlJlZ3VsYXRpb24uRVUuTm8uMTE2OS4yMDExLi5rSi4xMDBnLn4gMSwgZGF0YT1jaXF1YWxfcmVkdWNlZCksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NvcGU9Zm9ybXVsYShjaXF1YWxfZml0KSwgZGlyZWN0aW9uPSJmb3J3YXJkIiwgdHJhY2U9MCkKCnN1bW1hcnkoY2lxdWFsX3JlZHVjZWRfc3RlcHdpc2VfZm9yd2FyZCkKYGBgCgpgYGB7cn0KY2lxdWFsX2NvZWZzIDwtIHN1bW1hcnkoY2lxdWFsX3JlZHVjZWRfc3RlcHdpc2UpJGNvZWZmaWNpZW50cwoKY2lxdWFsX2NvZWZzX2RmIDwtIGRhdGEuZnJhbWUoCiAgVGVybSA9IHJvd25hbWVzKGNpcXVhbF9jb2VmcylbLTFdLCAgIyBlc2NsdWRpIGwnaW50ZXJjZXR0YQogIE5lZ0xvZ1BWYWx1ZSA9IC1sb2cxMChjaXF1YWxfY29lZnNbLTEsICJQcig+fHR8KSJdKSwgICMgZXNjbHVkaSBsJ2ludGVyY2V0dGEKICBWSUYgPSB2aWYoY2lxdWFsX3JlZHVjZWRfc3RlcHdpc2UpCikKZ2dwbG90KGNpcXVhbF9jb2Vmc19kZiwgYWVzKHggPSByZW9yZGVyKFRlcm0sIE5lZ0xvZ1BWYWx1ZSksIHkgPSBOZWdMb2dQVmFsdWUpKSArCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIsIGZpbGwgPSAic3RlZWxibHVlIikgKwogIGNvb3JkX2ZsaXAoKSArCiAgbGFicyh0aXRsZSA9ICItbG9nMTAocC12YWx1ZSkgQ0lRVUFMIHJpZG90dG8iLCB4ID0gIlRlcm1pbmUiLCB5ID0gIi1sb2cxMChwLXZhbHVlKSIpICsKICB0aGVtZV9taW5pbWFsKCkKCmdncGxvdChjaXF1YWxfY29lZnNfZGYsIGFlcyh4ID0gcmVvcmRlcihUZXJtLCBWSUYpLCB5ID0gVklGKSkgKwogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBmaWxsID0gImRhcmtvcmFuZ2UiKSArCiAgY29vcmRfZmxpcCgpICsKICBsYWJzKHRpdGxlID0gIlZhcmlhbmNlIEluZmxhdGlvbiBGYWN0b3IgKFZJRikgIENJUVVBTCByaWRvdHRvIiwgeCA9ICJUZXJtaW5lIiwgeSA9ICJWSUYiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpgYGAKCgoKVmVkaWFtbyBjaGUgaW4gZW50cmFtYmkgaSBjYXNpIGlsIG1vZGVsbG8gcmlkb3R0byBoYSAgY29tdW5xdWUgdW4gZ3JhbiBudW1lcm8gZGkgZmF0dG9yaSBkaSBpbmdyZXNzby4gUG9zc2lhbW8gcGVyw7IgZm9yemFyZSBpbCBudW1lcm8gbWFzc2ltbyBkaSBwYXNzaSBpbiBtb2RvIGRhIHJpZHVycmUgbGUgdmFyaWFiaWxpIGRpIGlucHV0LgoKYGBge3J9CgoKCmNpcXVhbF9yZWR1Y2VkX3N0ZXB3aXNlX2ZvcndhcmQgPC0gc3RlcChsbShjaXF1YWxfcmVkdWNlZCRFbmVyZ3kuLlJlZ3VsYXRpb24uRVUuTm8uMTE2OS4yMDExLi5rSi4xMDBnLn4gMSwgZGF0YT1jaXF1YWxfcmVkdWNlZCksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NvcGU9Zm9ybXVsYShjaXF1YWxfZml0KSwgZGlyZWN0aW9uPSJmb3J3YXJkIiwgc3RlcHM9NSwgdHJhY2U9MCkKCnN1bW1hcnkoY2lxdWFsX3JlZHVjZWRfc3RlcHdpc2VfZm9yd2FyZCkKYGBgCgpWZXJpZmljaGlhbW9uZSBsYSBjb2xsaW5lYXJpdMOgLiAKCmBgYHtyfQp2aWYoY2lxdWFsX3JlZHVjZWRfc3RlcHdpc2VfZm9yd2FyZCkKYGBgCgpWZWRpYW1vIGNoZSBjJ8OoIHVuYSBncmFuZGUgY29sbGluZWFyaXTDoCB0cmEgbGUgZHVlIHZhcmlhYmlsaSByZWxhdGl2ZSBhbGxlIHByb3RlaW5lIChhYmJhc3RhbnphIG92dmlvLCBzb25vIGR1ZSBjb2xvbm5lIGR1cGxpY2F0ZSkuIFF1aW5kaSBwb3NzaWFtbyByaWR1cnJlIHVsdGVyaW9ybWVudGUgZWxpbWluYW5kb25lIHVuYS4KCmBgYHtyfQoKIyByaW11b3ZpIGxhIHZhcmlhYmlsZSBkYWxsYSBmb3JtdWxhIHVzYW5kbyB1cGRhdGUoKQpjaXF1YWxfbWluaW1hbCA8LSBsbSggdXBkYXRlKGNpcXVhbF9yZWR1Y2VkX3N0ZXB3aXNlX2ZvcndhcmQsIC4gfiAuIC0gUHJvdGVpbi4uY3J1ZGUuLk4ueC42LjI1Li5nLjEwMGcuKSwgZGF0YT1jaXF1YWxfcmVkdWNlZCkKCnN1bW1hcnkoY2lxdWFsX21pbmltYWwpCnZpZihjaXF1YWxfbWluaW1hbCkKYGBgCk92dmlhbWVudGUgZ2xpIGluZGljYXRvcmkgY29tZSBsJyRSXjIkIHNvbm8gZGltaW51aXRpLCBtYSBhYmJpYW1vIHVuIG1vZGVsbG8gZGVjaXNhbWVudGUgc2VtcGxpZmljYXRvLCBpbnRlcHJldGFiaWxlIGUgc2VuemEgY29sbGluZWFyaXTDoC4KCiMjIyBQQ1IgCgpEaXZpZGlhbW8gYWwgc29saXRvIGluIHRyYWluaW5nIGUgdGVzdCBzZXQgY29uIGlsIGNvbWFuZG8gYGNyZWF0ZURhdGFQYXJ0aXRpb25gLgoKCmBgYHtyfQpsaWJyYXJ5KGNhcmV0KQoKIyBEaXZpZGlhbW8gaW4gdHJhaW5pbmcgZSB0ZXN0IHNldCBpbCBkYXRhc2V0IGNpcXVhbF9yZWR1Y2VkCgp0cmFpbl9pbmRleCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKGNpcXVhbF9yZWR1Y2VkJEVuZXJneS4uUmVndWxhdGlvbi5FVS5Oby4xMTY5LjIwMTEuLmtKLjEwMGcuLCBwID0gMC44LCBsaXN0ID0gRkFMU0UpCgpjaXF1YWxfcmVkdWNlZF90cmFpbiA8LSBjaXF1YWxfcmVkdWNlZFt0cmFpbl9pbmRleCwgXQpjaXF1YWxfcmVkdWNlZF90ZXN0IDwtIGNpcXVhbF9yZWR1Y2VkWy10cmFpbl9pbmRleCwgXQpgYGAKCgpFZmZldHVpYW1vIGxhIFBDQSBzdWxsZSB2YXJpYWJpbGkgZGkgaW5ncmVzc28gKGVzY2x1ZGlhbW8gbGEgcHJpbWEgY29sb25uYSwgbGEgdmFyaWFiaWxlIGRpIHVzY2l0YSkgY29uIGlsIGNvbWFuZG8gIGBwcmVQcm9jZXNzKClgIGRhbCBwYWNjaGV0dG8gYGNhcmV0YC4gSW4gcXVlc3RvIG1vZG8gc2Fyw6AgcGnDuSBzZW1wbGljZSBmYXJlIHByZXZpc2lvbmkgY29uIGlsIG1vZGVsbG8gKGFsdHJpbWVudGkgYmlzb2duZXJlYmJlIHRyYXNmb3JtYXJlIG9nbmkgdm9sdGEgbGUgY2FyYXR0ZXJpc3RpY2hlIGRpIGlucHV0IGRpIHVuIHB1bnRvIGRpIHRlc3QpCgpgYGB7cn0KCiMgc3BlY2lmaWNoaWFtbyB1bmEgc29nbGlhIHN1bGxhIHBlcmNlbnR1YWxlIGRpIHZhcmlhbnphIGN1bXVsYXRhIGNvbiBsJ29wemlvbmUgdGhyZXNoCnBjYV9tb2RlbCA8LSBwcmVQcm9jZXNzKGNpcXVhbF9yZWR1Y2VkX3RyYWluWywgLTFdLCBtZXRob2QgPSBjKCJjZW50ZXIiLCAic2NhbGUiLCAicGNhIiksIHRocmVzaCA9MC45NSkKYGBgCgpQb3NzaWFtbyBvcmEgY2FsY29sYXJlIGkgZmF0dG9yaSBkaSBpbmdyZXNzbyBwcm9pZXR0YXRpIHN1bGxlIGNvbXBvbmVudGkgcHJpbmNpcGFsaSBjb24gaWwgY29tYW5kbyBwcmVkaWN0IChzaWEgc3VsIHRyYWluaW5nIGNoZSBzdWwgdGVzdCBzZXQpLgoKYGBge3J9CnBjYV9jaXF1YWxfdHJhaW4gPC0gcHJlZGljdChwY2FfbW9kZWwsIGNpcXVhbF9yZWR1Y2VkX3RyYWluWywgLTFdKQpoZWFkKHBjYV9jaXF1YWxfdHJhaW4pCgpwY2FfY2lxdWFsX3Rlc3QgPC0gcHJlZGljdChwY2FfbW9kZWwsIGNpcXVhbF9yZWR1Y2VkX3Rlc3RbLCAtMV0pCmhlYWQocGNhX2NpcXVhbF90ZXN0KQpgYGAKCkVmZmV0dHVpYW1vIHF1aW5kaSBsYSByZWdyZXNzaW9uZSB1c2FuZG8gY29tZSBmYXR0b3JpIGRpIGluZ3Jlc3NvIGxlIFBDIHNlbGV6aW9uYXRlLgoKYGBge3J9CmZpdF9wY3IgPC0gbG0oY2lxdWFsX3JlZHVjZWRfdHJhaW4kRW5lcmd5Li5SZWd1bGF0aW9uLkVVLk5vLjExNjkuMjAxMS4ua0ouMTAwZy4gfi4sIGRhdGE9cGNhX2NpcXVhbF90cmFpbikKc3VtbWFyeShmaXRfcGNyKQp2aWYoZml0X3BjcikKYGBgCgoqKlByb2JsZW1hOioqIGNvbWUgY2FtYmlhIGxhIHBlcmZvcm1hbmNlIGRlbCBtb2RlbGxvIHNlIHNwZWNpZmljaGlhbW8gdW4gbnVtZXJvIGJhc3NvIGRpIFBDPwoKCiMjIyBSZWdvbGFyaXp6YXppb25lIExBU1NPCgpVc2lhbW8gaWwgcGFjY2hldHRvIGBnbG1uZXRgIHBlciBsYSByZWdvbGFyaXp6YXppb25lIGNvbiBMQVNTTy4KCmBgYHtyfQoKWCA9IGFzLm1hdHJpeChjaXF1YWxfcmVkdWNlZF90cmFpblssLTFdKQp5ID0gYXMubWF0cml4KGNpcXVhbF9yZWR1Y2VkX3RyYWluWywgMV0pCmN2X2xhc3NvIDwtIGN2LmdsbW5ldChYLCB5LCBhbHBoYSA9IDEpCnBsb3QoY3ZfbGFzc28pCgpwcmludChwYXN0ZSgibGFtYmRhX21pbiBnbG1uZXQ6IiwgY3ZfbGFzc28kbGFtYmRhLm1pbikpCgpjb2VmKGN2X2xhc3NvKQpgYGAKCkNvbmZyb250aWFtbyBjb24gaWwgcmlzdWx0YXRvIGRpIGBjYXJldGAgaW4gY3Jvc3MtdmFsaWRhdGlvbi4KCmBgYHtyfQp0cmFpbl9jb250cm9sIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAiY3YiLCBudW1iZXIgPSAyMCkKCgpsYXNzb19tb2RlbCA8LSB0cmFpbihFbmVyZ3kuLlJlZ3VsYXRpb24uRVUuTm8uMTE2OS4yMDExLi5rSi4xMDBnLiB+IC4sIGRhdGEgPSBjaXF1YWxfcmVkdWNlZF90cmFpbiwgIAogICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiZ2xtbmV0IiwgCiAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IHRyYWluX2NvbnRyb2wsIAogICAgICAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGV4cGFuZC5ncmlkKGFscGhhID0gMSwgbGFtYmRhID0gc2VxKDAsIDIwLCBsZW5ndGggPSAxMDApKSkKCnBsb3QobGFzc29fbW9kZWwpCmBgYAoKQ29uZnJvbnRpYW1vIGkgY29lZmZpY2llbnRpIHNlbGV6aW9uYXRpLgoKYGBge3J9Cmxhc3NvX21vZGVsJGJlc3RUdW5lJGxhbWJkYQoKY2JpbmQoIGNvZWYobGFzc29fbW9kZWwkZmluYWxNb2RlbCwgbGFzc29fbW9kZWwkYmVzdFR1bmUkbGFtYmRhKSwgY29lZihjdl9sYXNzbykpCgoKYGBgCgoKCiMjIEVzZXJjaXppIHN1Z2dlcml0aSAKCjEuIFN0dWRpYSBsYSBtdWx0aWNvbGxpbmVhcml0w6AgbmVsIGRhdGFzZXQgYG10Y2Fyc2AgdXNhbmRvIFZJRiBlIG51bWVybyBkaSBjb25kaXppb25hbWVudG8gZGVsbGEgbWF0cmljZSBkaSBHcmFtLiBBcHBsaWNhIHVuYSByaWR1emlvbmUgZGVsIG1vZGVsbG8gdXNhbmRvIHN0ZXB3aXNlIHNlbGVjdGlvbiAoc2lhIGZvcndhcmQgY2hlIGJhY2t3YXJkKSBlIGNvbmZyb250YSBpIG1vZGVsbGkgcmlzdWx0YW50aSBpbiB0ZXJtaW5pIGRpIEFJQywgQklDLCBWSUYgZSBpbnRlcnByZXRhYmlsaXTDoC4KCjIuIFV0aWxpenphIGlsIGRhdGFzZXQgbXRjYXJzIGUgYXBwbGljYSBSaWRnZSBlIExhc3NvIHJlZ3Jlc3Npb24gcGVyIHByZXZlZGVyZSBpbCBjb25zdW1vIGRpIGNhcmJ1cmFudGUgKGBtcGdgKSBpbiBiYXNlIGFsIHJlc3RvIGRlbGxlIHZhcmlhYmlsaS4gIENvbmZyb250YSBsZSBwcmVzdGF6aW9uaSBkZWkgZHVlIG1vZGVsbGkgdXRpbGl6emFuZG8gUk1TRSBlIHZpc3VhbGl6emEgaSBjb2VmZmljaWVudGkgc3RpbWF0aS4gRGlzY3V0aSBxdWFsZSBtb2RlbGxvIG9mZnJlIHVuYSBtaWdsaW9yZSBpbnRlcnByZXRhYmlsaXTDoC4KCjMuIERpc2N1dGkgbGEgbXVsdGljb2xsaW5lYXJpdMOgIGluIHVuIG1vZGVsbG8gZGkgcmVncmVzc2lvbmUgcG9saW5vbWlhbGUgKGFuY2hlIHN1IGRhdGkgZ2VuZXJhdGkpLgoKNC4gR2VuZXJhIHVuIGRhdGFzZXQgaW4gY3VpIGxhIFBDUiAoc3UgdW4gY2VydG8gbnVtZXJvICRrJCBkaSB2YXJpYWJpbGkgcnVvdGF0ZSkgcGVnZ2lvcmEgbGEgcGVyZm9ybWFuY2UgcmlzcGV0dG8gYSBPTFMuCgo1LiBVdGlsaXp6YSBpbCBkYXRhc2V0IGBkaWFtb25kc2AgcGVyIGVzZWd1aXJlIHVuJ2FuYWxpc2kgZGVsbGUgY29tcG9uZW50aSBwcmluY2lwYWxpLiBTZWxlemlvbmEgdW4gbnVtZXJvIHJpZG90dG8gZGkgY29tcG9uZW50aSBjaGUgc3BpZWdhbm8gdW5hIGNlcnRhIHBlcmNlbnR1YWxlIGRlbGxhIHZhcmlhbnphIHRvdGFsZSBlIHV0aWxpenphIHF1ZXN0ZSBjb21wb25lbnRpIGluIHVuIG1vZGVsbG8gZGkgcmVncmVzc2lvbmUgbGluZWFyZSBwZXIgcHJldmVkZXJlIGlsIHByZXp6by4gVmFsdXRhIGxlIHByZXN0YXppb25pIHJpc3BldHRvIGEgdW4gbW9kZWxsbyBkaSByZWdyZXNzaW9uZSBsaW5lYXJlIHN0YW5kYXJkLgoKNi4gVXRpbGl6emEgYm9vdHN0cmFwIHBlciBzdGltYXJlIGdsaSBpbnRlcnZhbGxpIGRpIGNvbmZpZGVuemEgZGVpIGNvZWZmaWNpZW50aSBkaSB1biBtb2RlbGxvIGRpIHJlZ3Jlc3Npb25lIGxpbmVhcmUgcmVnb2xhcml6emF0byBzdSB1biBkYXRhc2V0IGEgc2NlbHRhIChhZCBlc2VtcGlvIGBtdGNhcnNgIG8gYHN3aXNzYCkuCgoKCg==