source("https://raw.githubusercontent.com/MatteoFasulo/Rare-Earth/main/R/util/coreFunctions.R")

loadPackages(c('sn','tidyverse','psych','RColorBrewer','stargazer','mclust','ContaminatedMixt',
               'plotly','ggplot2','ggdendro','teigen','tclust','HDMD','caTools','clustvarsel',
               'vscc','sparcl','pgmm','caret','glmnet','MLmetrics'))

load("Z:\\DesktopC\\LUMSA\\2\\Data Mining\\Finite Mixture\\FiniteMixtureL31.RData")
#load("H:\\smoxy\\Downloads\\FiniteMixtureL31.RData")
rm(CO2data)
rm(NOdata)
rm(tonedata)
type <- wine$Type
rm(wine)

1 Introduzione

Dati su 27 caratteristiche chimico/fisiche di tre diversi tipi di vino (Barolo, Grignolino, Barbera) dal Piemonte. Un set di dati con 178 osservazioni e 28 variabili (di cui la prima relativa alla tipologia di vino). Nell’ordine:

  • Barolo
  • Grignolino
  • Barbera
data(wines)
wines

1.1 Annata del vino: un fattore da considerare?

E’ stato possibile attraverso la ricerca originaria risalire all’anno di osservazione di ciascun vino. Di seguito vengono riportate le osservazioni dei tre diversi tipi di vino durante gli anni:

year <- as.numeric(substr(rownames(wines), 6, 7))
table(wines$wine, year)
            year
             70 71 72 73 74 75 76 78 79
  Barolo      0 19  0 20 20  0  0  0  0
  Grignolino  9  9  7  9 16  9 12  0  0
  Barbera     0  0  0  0  9  0  5 29  5
#wines[,'wine'] <- type

Notiamo subito che il Barbera è distribuito principalmente negli ultimi anni (76,78,79) mentre il Barolo nel 71, 73 e 74. Per quanto riguarda la percentuale delle singole classi:

wines %>%
  count(wine = factor(wine)) %>%
  mutate(pct = prop.table(n)) %>% 
  ggplot(aes(x = wine, y = pct, fill = wine, label = scales::percent(pct))) + 
  geom_col(position = 'dodge') + 
  geom_text(position = position_dodge(width = .9),
            vjust = -0.5, 
            size = 3) + 
  scale_y_continuous(name = "Percentage")+
  scale_x_discrete(name = "Wine Name")+
  scale_fill_hue(l=40, c=35)+
  theme(legend.position = "none")

E’ chiaro che il Grignolino sia il più numeroso (39.9%) seguito dal Barolo (33.1%) e dal Barbera (27.0%).

Per rappresentare la dispersione dei dati abbiamo usato uno scatterplot leggermente differente dal solito. Sulla diagonale superiore si vede la distribuzione dei dati mentre sulla diagonale inferiore vi è la correlazione di Pearson tra le variabili.

my_cols <- c("#00AFBB", "#E7B800", "#FC4E07") 
panel.cor <- function(x, y){
    usr <- par("usr"); on.exit(par(usr))
    par(usr = c(0, 1, 0, 1))
    r <- round(cor(x, y), digits=2)
    txt <- paste0("R = ", r)
    text(0.5, 0.5, txt, cex = 1)
}
upper.panel<-function(x, y){
  points(x,y, pch = 19, col = my_cols[wines$wine],cex=.5)
}
pairs(wines[,2:8], 
      lower.panel = panel.cor,
      upper.panel = upper.panel)

Dalle prime 7 variabili, è possibile notare una correlazione di 0.69 tra acidity e malic e ovviamente una relazione inversamente proporzionale tra pH e acidity.

2 Statistiche descrittive

Per visualizzare le statistiche descrittive (media e deviazione standard) ci è sembrato opportuno dividerle in base alla classe di appartenenza:

printMeanAndSdByGroup <- function(variables,groupvariable)
  {
     variablenames <- c(names(groupvariable),names(as.data.frame(variables)))
     groupvariable <- groupvariable[,1]
     means <- aggregate(as.matrix(variables) ~ groupvariable, FUN = mean)
     names(means) <- variablenames
     print(paste("Means:"))
     print(means)
     sds <- aggregate(as.matrix(variables) ~ groupvariable, FUN = sd)
     names(sds) <- variablenames
     print(paste("Standard deviations:"))
     print(sds)
}
printMeanAndSdByGroup(wines[2:28],wines[1])
[1] "Means:"
[1] "Standard deviations:"

Alcune considerazioni:

  • La media di sugar, potassium, magnesium, phosphate, chloride, flavanoids, proanthocyanins, colour nel Barolo è più alta.
  • La media di acidity, tartaric, malic, uronic, alcal_ash nel Barbera è più alta.
  • La deviazione standard di acidity è più alta nel Grignolino.

2.1 Varianza Within

Abbiamo calcolato la varianza within tra una feature e i tipi di vino:

calcWithinGroupsVariance <- function(variable,groupvariable)
  {
     groupvariable2 <- as.factor(groupvariable[[1]])
     levels <- levels(groupvariable2)
     numlevels <- length(levels)
     numtotal <- 0
     denomtotal <- 0
     for (i in 1:numlevels)
     {
        leveli <- levels[i]
        levelidata <- variable[groupvariable==leveli,]
        levelilength <- length(levelidata)
        sdi <- sd(levelidata)
        numi <- (levelilength - 1)*(sdi * sdi)
        denomi <- levelilength
        numtotal <- numtotal + numi
        denomtotal <- denomtotal + denomi
     }
     Vw <- numtotal / (denomtotal - numlevels)
     return(Vw)
}
calcWithinGroupsVariance(wines["flavanoids"],wines[1])
[1] 0.2747075

2.2 Varianza Between

Stesso discorso per la varianza between tra una feature e i vini:

calcBetweenGroupsVariance <- function(variable,groupvariable)
  {
     groupvariable2 <- as.factor(groupvariable[[1]])
     levels <- levels(groupvariable2)
     numlevels <- length(levels)
     grandmean <- sapply(variable,mean)
     numtotal <- 0
     denomtotal <- 0
     for (i in 1:numlevels)
     {
        leveli <- levels[i]
        levelidata <- variable[groupvariable==leveli,]
        levelilength <- length(levelidata)
        meani <- mean(levelidata)
        sdi <- sd(levelidata)
        numi <- levelilength * ((meani - grandmean)^2)
        denomi <- levelilength
        numtotal <- numtotal + numi
        denomtotal <- denomtotal + denomi
     }
     Vb <- numtotal / (numlevels - 1)
     Vb <- Vb[[1]]
     return(Vb)
}
calcBetweenGroupsVariance(wines["flavanoids"],wines[1])
[1] 64.2612

2.3 Separazione

Per vedere quali variabili hanno la maggiore separazione, (rapporto tra Varianza Between e Varianza Within) abbiamo scritto una funzione apposita per calcolarne il valore per ogni feature.

calcSeparations <- function(variables,groupvariable)
  {
     # find out how many variables we have
     variables <- as.data.frame(variables)
     numvariables <- length(variables)
     # find the variable names
     variablenames <- colnames(variables)
     # calculate the separation for each variable
     Vw <- NULL
     Vb <- NULL
     sep <- NULL
     for (i in 1:numvariables)
     {
        variablei <- variables[i]
        variablename <- variablenames[i]
        Vw[i] <- calcWithinGroupsVariance(variablei, groupvariable)
        Vb[i] <- calcBetweenGroupsVariance(variablei, groupvariable)
        sep[i] <- Vb[i]/Vw[i]
     }
     result <- data.frame('Within'=Vw,'Between'=Vb,'Sep'=sep, row.names = as.vector(colnames(wines[,-1])), stringsAsFactors = F)
     result[order(result$Sep,decreasing = T),]
}
calcSeparations(wines[2:28],wines[1])

3 Splitting in Train e Test

Per alcune analisi successive, abbiamo provato a cambiare il task della ricerca tentando di utilizzare un modello come classificatore e di misurarne le prestazioni di classificazione. A tale scopo, abbiamo suddiviso il dataset originario in due sottogruppi:

  • train
  • test
require(caTools)
sample = sample.split(wines[,1], SplitRatio = .50)

train = subset(wines, sample == TRUE)
trainTestNames <- train$wine
print(paste("Train Obs:",nrow(train)))
[1] "Train Obs: 90"
#train$wine <- as.numeric(train$wine)

test  = subset(wines, sample == FALSE)
wineTestNames <- test$wine
print(paste("Test Obs:",nrow(test)))
[1] "Test Obs: 88"
#test$wine <- as.numeric(test$wine)

4 Clustering

Per il Clustering abbiamo deciso di applicare:

  • Un approccio Distance-Based:
    • Gerarchico:
      • Euclidean, Minkowski, Manhattan, Mahalanobis
    • Di partizionamento:
      • KMeans
      • PAM
  • Un approccio Model-Based:
    • Gaussian Mixture (Mclust)
    • Contaminated Normal (CNmixt)
    • Multivariate t Distribution (teigen)
    • Parsimonious Gaussian Mixture Models (pgmm)

4.1 Distance-Based

dissMatrix    <- pairwise.mahalanobis(wines[,-1],
                                      grouping = c(1:nrow(wines)),
                                      cov = cov(wines[,-1]))$distance
dissMatrix <- sqrt(dissMatrix)
dissMatrix <- as.dist(dissMatrix)
combDist <- function(distance, methods, df, dt, dissMatrix) {
  c <- 0
  results <- list()
  for (i in 1:length(distance)){
    ifelse(distance[i] == "minkowski",
           dist <- dist(df, method = distance[i], p = 4),
           ifelse(distance[i] == "mahalanobis",
                  dist <- dissMatrix,
                  dist <- dist(df, method = distance[i])))
    for (j in 1:length(methods)){
      dendo = hclust(dist, method = methods[j])
      dendo.x = ggdendrogram(dendo, rotate = F, size = 2, leaf_labels = T, labels = F) + 
                               ggtitle(paste(distance[i],' ',methods[j],sep=''))
      for(elem in 2:4){
        cluster = cutree(dendo, k=elem)
        c <- c + 1
        results[[c]] <- list(distance = distance[i],
                             method = methods[j],
                             groups = elem,
                             table = table(dt,cluster),
                             dendo = dendo.x,
                             AdjustedRandIndex = adjustedRandIndex(dt,cluster),
                             cluster = cluster)
      }
    }
  }
  return(results)
}
results <- combDist(c("euclidean", "manhattan", "minkowski","mahalanobis"),
                    c("single", "complete", "average", "ward.D"), scale(wines[,-1]), wines[,1], dissMatrix)

optimal <- function(results){
  best_randIndex.eu = 0
  best_randIndex.ma = 0
  best_randIndex.mi = 0
  best_randIndex.maha = 0
  best_model.eu = integer()
  best_model.ma = integer()
  best_model.mi = integer()
  best_model.maha = integer()
  for (i in 1:length(results)){
    current_randIndex = results[[i]]$AdjustedRandIndex
    if (results[[i]]$distance == "euclidean"){
      if (current_randIndex > best_randIndex.eu) {
        best_randIndex.eu = current_randIndex
        best_model.eu = i
      }
    }
    else if (results[[i]]$distance == "manhattan"){
      if (current_randIndex > best_randIndex.ma) {
        best_randIndex.ma = current_randIndex
        best_model.ma = i
      }
    }
    else if (results[[i]]$distance == "minkowski"){
      if (current_randIndex > best_randIndex.mi) {
        best_randIndex.mi = current_randIndex
        best_model.mi = i
      }
    }
    else if (results[[i]]$distance == "mahalanobis"){
      if (current_randIndex > best_randIndex.maha) {
        best_randIndex.maha = current_randIndex
        best_model.maha = i
      }
    }
  }
  #print(list(euclidean = list(model.number = best_model.eu,
  #                            cluster = results[[best_model.eu]]$groups,
  #                            AdjustedRandIndex = best_randIndex.eu),
  #           manhattan = list(model.number = best_model.ma,
  #                            cluster = results[[best_model.ma]]$groups,
  #                            AdjustedRandIndex = best_randIndex.ma),
  #           minkowski = list(model.number = best_model.mi,
  #                            cluster = results[[best_model.mi]]$groups,
  #                            AdjustedRandIndex = best_randIndex.mi),
  #           mahalanobis=list(model.number = best_model.maha,
  #                            cluster = results[[best_model.maha]]$groups,
  #                            AdjustedRandIndex = best_randIndex.maha))
  #    )
  return(list(euclidean = results[[best_model.eu]],
              manhattan = results[[best_model.ma]],
              minkowski = results[[best_model.mi]],
              mahalanobis=results[[best_model.maha]]))
}
best_dist_model = optimal(results)

4.1.1 Dendrogrammi

4.1.1.1 Euclidean

ggplotly(best_dist_model$euclidean$dendo)
print(best_dist_model$euclidean$table)
            cluster
dt            1  2  3
  Barolo     55  4  0
  Grignolino  6 61  4
  Barbera     0  0 48
print(paste("AdjustedRandIndex:",round(best_dist_model$euclidean$AdjustedRandIndex,3)))
[1] "AdjustedRandIndex: 0.769"

4.1.1.2 Manhattan

ggplotly(best_dist_model$manhattan$dendo)
print(best_dist_model$manhattan$table)
            cluster
dt            1  2  3
  Barolo     59  0  0
  Grignolino  3 68  0
  Barbera     0  0 48
print(paste("AdjustedRandIndex:",round(best_dist_model$manhattan$AdjustedRandIndex,3)))
[1] "AdjustedRandIndex: 0.946"

4.1.1.3 Minkowski

ggplotly(best_dist_model$minkowski$dendo)
print(best_dist_model$minkowski$table)
            cluster
dt            1  2  3  4
  Barolo     55  2  2  0
  Grignolino  8  1 59  3
  Barbera     2  0  0 46
print(paste("AdjustedRandIndex:",round(best_dist_model$minkowski$AdjustedRandIndex,3)))
[1] "AdjustedRandIndex: 0.73"

4.1.1.4 Mahalanobis

ggplotly(best_dist_model$mahalanobis$dendo)
print(best_dist_model$mahalanobis$table)
            cluster
dt            1  2
  Barolo     52  7
  Grignolino 57 14
  Barbera     5 43
print(paste("AdjustedRandIndex:",round(best_dist_model$mahalanobis$AdjustedRandIndex,3)))
[1] "AdjustedRandIndex: 0.27"

4.1.2 K-Means e PAM

4.1.2.1 KMeans 3

require(cluster)
k.means.3 <- kmeans(scale(wines[,-1]),centers=3,nstart = 50, iter.max = 100)
table(wines[,1], k.means.3$cluster)
            
              1  2  3
  Barolo     58  1  0
  Grignolino  2 68  1
  Barbera     0  0 48
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(k.means.3$cluster, wines[,1]),3)))
[1] "AdjustedRandIndex: 0.93"

4.1.2.2 KMeans 4

require(cluster)
k.means.4 <- kmeans(scale(wines[,-1]),centers=4,nstart = 50, iter.max = 100)
table(wines[,1], k.means.4$cluster)
            
              1  2  3  4
  Barolo      0  0 59  0
  Grignolino  0 38  5 28
  Barbera    47  0  0  1
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(k.means.4$cluster, wines[,1]),3)))
[1] "AdjustedRandIndex: 0.736"

4.1.2.3 PAM 3

require(cluster)
PAM.3 <- pam(wines[,-1], k=3,
    metric = "euclidean", 
    nstart = 50,
    stand = TRUE)
table(wines[,1], PAM.3$clustering)
            
              1  2  3
  Barolo     50  9  0
  Grignolino  3 67  1
  Barbera     1  1 46
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(PAM.3$clustering, wines[,1]),3)))
[1] "AdjustedRandIndex: 0.754"

4.1.2.4 PAM 4

require(cluster)
PAM.4 <- pam(wines[,-1], k=4,
    metric = "euclidean", 
    nstart = 50,
    stand = TRUE)
table(wines[,1], PAM.4$clustering)
            
              1  2  3  4
  Barolo     28 27  4  0
  Grignolino  3  0 67  1
  Barbera     1  0  1 46
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(PAM.4$clustering, wines[,1]),3)))
[1] "AdjustedRandIndex: 0.728"

5 Variable Selection

5.1 Principio filosofico

Novacula Occami: frustra fit per plura quod potest fieri per pauciora (Il rasoio di Occam: è futile fare con più mezzi ciò che si può fare con meno). Tale principio metodologico è ritenuto alla base del pensiero scientifico moderno.

5.1.1 Headlong

            
              1  2  3  4
  Barolo     47 10  2  0
  Grignolino  6  3 61  1
  Barbera     0  0  1 47
[1] "AdjustedRandIndex: 0.734"

5.1.2 Greedy

            
              1  2  3  4
  Barolo     47 10  2  0
  Grignolino  6  3 61  1
  Barbera     0  0  1 47
[1] "AdjustedRandIndex: 0.734"

5.1.3 VSCC

table(wines[,1], vscc.mclust$initialrun$classification) #Clustering results on full data set
            
              1  2  3
  Barolo     58  1  0
  Grignolino  1 70  0
  Barbera     0  2 46
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(vscc.mclust$initialrun$classification, wines[,1]),3)))
[1] "AdjustedRandIndex: 0.931"
table(wines[,1], vscc.mclust$bestmodel$classification) #Clustering results on reduced data set
            
              1  2  3
  Barolo     59  0  0
  Grignolino  3 66  2
  Barbera     0  0 48
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(vscc.mclust$bestmodel$classification, wines[,1]),3)))
[1] "AdjustedRandIndex: 0.913"

5.1.4 KMeansSparse 3

            
              1  2  3
  Barolo     46 13  0
  Grignolino  1 20 50
  Barbera     0 29 19
[1] "AdjustedRandIndex: 0.371"

5.1.5 KMeansSparse 4

            
              1  2  3  4
  Barolo     30 23  6  0
  Grignolino  4  0 22 45
  Barbera     5  0 31 12
[1] "AdjustedRandIndex: 0.303"

Le variabili selezionate dalla funzione clustervarsel sono:

  • flavanoids, OD_dw, proline, colour, uronic, malic, chloride

Le variabili selezionate dalla funzione vscc sono:

  • flavanoids, OD_dw, proline, colour, alcohol, OD_fl, hue, phenols, uronic, tartaric

5.2 Model-Based

5.2.1 Mclust

table(wines[,1],mixt.selected.wines$classification)
            
              1  2  3  4
  Barolo     59  0  0  0
  Grignolino  8 50 11  2
  Barbera     0  0  0 48
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],mixt.selected.wines$classification),3)))
[1] "AdjustedRandIndex: 0.745"

5.2.2 CNmixt

table(wines[,1],getBestModel(cn.wines.mixt, criterion = 'ICL')$models[[1]]$group)
            
              1  2  3
  Barolo      0  0 59
  Grignolino  3 63  5
  Barbera    48  0  0
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],getBestModel(cn.wines.mixt, criterion = 'ICL')$models[[1]]$group),3)))
[1] "AdjustedRandIndex: 0.864"

5.2.3 CNmixt kmeans

table(wines[,1],getBestModel(cn.wines.kmeans, criterion = 'ICL')$models[[1]]$group)
            
              1  2  3
  Barolo      0  0 59
  Grignolino 63  3  5
  Barbera     0 48  0
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],getBestModel(cn.wines.kmeans, criterion = 'ICL')$models[[1]]$group),3)))
[1] "AdjustedRandIndex: 0.864"

5.2.4 CNmixt rpost

table(wines[,1],getBestModel(cn.wines.rpost, criterion = 'ICL')$models[[1]]$group)
            
              1  2  3
  Barolo     59  0  0
  Grignolino  5  3 63
  Barbera     0 48  0
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],getBestModel(cn.wines.rpost, criterion = 'ICL')$models[[1]]$group),3)))
[1] "AdjustedRandIndex: 0.864"

5.2.5 CNmixt rclass

table(wines[,1],getBestModel(cn.wines.rclass, criterion = 'ICL')$models[[1]]$group)
            
              1  2  3
  Barolo      0  0 59
  Grignolino 62  3  6
  Barbera     0 48  0
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],getBestModel(cn.wines.rclass, criterion = 'ICL')$models[[1]]$group),3)))
[1] "AdjustedRandIndex: 0.847"

5.3 Multivariate t Distribution

5.3.1 teigen kmeans

table(wines[,1],teigen.kmeans$classification)
            
              1  2  3  4
  Barolo     58  0  1  0
  Grignolino  1  1 69  0
  Barbera     0 25  0 23
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],teigen.kmeans$classification),3)))
[1] "AdjustedRandIndex: 0.865"

5.3.2 teigen hard

table(wines[,1],teigen.hard$classification)
            
              1  2  3  4
  Barolo      1  0  0 58
  Grignolino 69  1  0  1
  Barbera     0 25 23  0
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],teigen.hard$classification),3)))
[1] "AdjustedRandIndex: 0.865"

5.3.3 teigen soft

table(wines[,1],teigen.soft$classification)
            
              1  2  3  4
  Barolo     58  0  1  0
  Grignolino  1  1 69  0
  Barbera     0 25  0 23
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(wines[,1],teigen.soft$classification),3)))
[1] "AdjustedRandIndex: 0.865"

5.3.4 t-classifier kmeans

table(test[,1],teigen.pre.kmeans$classification)
            
              1  2  3
  Barolo     28  1  0
  Grignolino  3 31  1
  Barbera     0  0 24
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(test[,1],teigen.pre.kmeans$classification),3)))
[1] "AdjustedRandIndex: 0.827"

5.3.5 t-classifier uniform

teigen.classifier.uniform <- teigen(train[,-1], Gs=3:4, init = 'uniform', scale = T, known = train[,1], verbose = F)
teigen.pre.uniform = predict(teigen.classifier.uniform,test[,-1])

table(test[,1],teigen.pre.uniform$classification)
            
              1  2  3
  Barolo     28  1  0
  Grignolino  3 31  1
  Barbera     0  0 24
print(paste("AdjustedRandIndex:",round(adjustedRandIndex(test[,1],teigen.pre.uniform$classification),3)))
[1] "AdjustedRandIndex: 0.827"

6 Tecniche di regolarizzazione

Per la nostra analisi abbiamo voluto verificare l’efficienza di tre noti modelli di regolarizzazione attraverso il pacchetto caret:

  • Ridge
  • Lasso
  • Elastic Net
lambda <- 10^seq(0, -2, length = 250)

6.1 Ridge

Il modello Ridge riduce i coefficienti, in modo che le variabili, con un contributo minore al risultato, abbiano i loro coefficienti vicini allo zero. Invece di forzarli a essere esattamente zero (come nel Lasso), li penalizziamo con un termine chiamato norma L2 costringendoli così a essere piccoli. In questo modo diminuiamo la complessità del modello senza eliminare nessuna variabile attraverso una costante chiamata lambda (\(\lambda\)) di penalizzazione: \[ L_{ridge}(\hat{\beta}) = \sum_{i = 1}^{n}{(y_i - x_i\hat{\beta})^2} + \lambda\sum_{k = 1}^{K}{\hat{\beta}_k^2} \]

# Build the model
set.seed(123)
ridge <- caret::train(
  x = train[,-1],
  y = factor(train[,1]),
  method = "glmnet",
  trControl = trainControl("cv", number = 10, classProbs = TRUE, summaryFunction = multiClassSummary),
  tuneGrid = expand.grid(alpha = 0, lambda=lambda),
  metric="Accuracy")
# Model coefficients
coef(ridge$finalModel, ridge$bestTune$lambda)
$Barolo
28 x 1 sparse Matrix of class "dgCMatrix"
                            1
(Intercept)     -5.773974e+00
alcohol          1.601221e-01
sugar            2.847696e-02
acidity         -2.776960e-03
tartaric        -1.009434e-01
malic           -1.734279e-02
uronic          -1.523725e-01
pH               6.403133e-02
ash              1.990303e-01
alcal_ash       -3.323135e-02
potassium        2.699287e-04
calcium         -1.600088e-03
magnesium        5.805483e-03
phosphate        9.690038e-04
chloride         1.129685e-04
phenols          1.263709e-01
flavanoids       1.177827e-01
nonflavanoids   -5.550151e-01
proanthocyanins  1.462032e-01
colour           1.013349e-02
hue              2.626132e-01
OD_dw            1.388700e-01
OD_fl            7.422181e-02
glycerol         5.042171e-02
butanediol      -2.132007e-05
nitrogen         8.420899e-04
proline          4.925977e-04
methanol         4.285249e-04

$Grignolino
28 x 1 sparse Matrix of class "dgCMatrix"
                            1
(Intercept)      6.787280e+00
alcohol         -1.890888e-01
sugar           -3.224553e-02
acidity         -1.772993e-03
tartaric        -5.621554e-02
malic           -7.762842e-02
uronic          -2.237885e-01
pH               1.351516e-01
ash             -3.611891e-01
alcal_ash        8.905963e-03
potassium       -4.815379e-04
calcium          3.291712e-03
magnesium       -7.495180e-03
phosphate       -8.798840e-04
chloride         4.059303e-04
phenols          3.362762e-03
flavanoids       1.053689e-03
nonflavanoids    6.151024e-02
proanthocyanins -4.543428e-02
colour          -5.651463e-02
hue              2.889793e-01
OD_dw            2.124811e-02
OD_fl            5.477169e-02
glycerol        -5.802764e-02
butanediol      -3.782010e-04
nitrogen         9.400765e-07
proline         -4.114483e-04
methanol        -1.608830e-03

$Barbera
28 x 1 sparse Matrix of class "dgCMatrix"
                            1
(Intercept)     -1.013307e+00
alcohol          2.896670e-02
sugar            3.768566e-03
acidity          4.549953e-03
tartaric         1.571589e-01
malic            9.497122e-02
uronic           3.761610e-01
pH              -1.991830e-01
ash              1.621588e-01
alcal_ash        2.432538e-02
potassium        2.116092e-04
calcium         -1.691623e-03
magnesium        1.689697e-03
phosphate       -8.911989e-05
chloride        -5.188988e-04
phenols         -1.297336e-01
flavanoids      -1.188364e-01
nonflavanoids    4.935049e-01
proanthocyanins -1.007690e-01
colour           4.638115e-02
hue             -5.515925e-01
OD_dw           -1.601181e-01
OD_fl           -1.289935e-01
glycerol         7.605930e-03
butanediol       3.995211e-04
nitrogen        -8.430300e-04
proline         -8.114934e-05
methanol         1.180305e-03
# Make predictions
predictions.ridge <- ridge %>% predict(test)
# Model prediction performance
tibble(
  trueValue = wineTestNames,
  predictedValue = predictions.ridge)

Il ridge è composto dalla somma dei residui quadrati più una penalità, definita dalla lettera Lambda, che è moltiplicata per la somma dei coefficienti quadrati \(\beta\). Quando \(\lambda = 0\), il termine di penalità non ha alcun effetto e il ridge produrrà i coefficienti minimi quadrati classici. Tuttavia, quando \(\lambda\) aumenta all’infinito, l’impatto della penalità aumenta e i coefficienti si avvicinano allo zero. Il ridge è particolarmente indicato quando si hanno molti dati multivariati con numero di feature maggiore del numero di osservazioni. Lo svantaggio, però, è che includerà tutti le feature nel modello finale, a differenza dei metodi di feature selection, che generalmente selezioneranno un insieme ridotto di variabili tra quelle disponibili.

caret::confusionMatrix(predictions.ridge, test$wine)
Confusion Matrix and Statistics

            Reference
Prediction   Barolo Grignolino Barbera
  Barolo         29          2       0
  Grignolino      0         33       2
  Barbera         0          0      22

Overall Statistics
                                          
               Accuracy : 0.9545          
                 95% CI : (0.8877, 0.9875)
    No Information Rate : 0.3977          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9309          
                                          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: Barolo Class: Grignolino Class: Barbera
Sensitivity                 1.0000            0.9429         0.9167
Specificity                 0.9661            0.9623         1.0000
Pos Pred Value              0.9355            0.9429         1.0000
Neg Pred Value              1.0000            0.9623         0.9697
Prevalence                  0.3295            0.3977         0.2727
Detection Rate              0.3295            0.3750         0.2500
Detection Prevalence        0.3523            0.3977         0.2500
Balanced Accuracy           0.9831            0.9526         0.9583

6.2 Lasso

Il Least Absolute Shrinkage and Selection Operator (LASSO) riduce i coefficienti verso lo zero penalizzando il modello con un termine di penalità chiamato norma L1, che è la somma dei coefficienti in valore assoluto: \[ L_{lasso}(\hat{\beta}) = \sum_{i = 1}^{n}{(y_i - x_i\hat{\beta})^2} + \lambda\sum_{k = 1}^{K}{|\hat{\beta}_k|} \]

# Build the model
set.seed(123)
lasso <- caret::train(
  x = train[,-1],
  y = factor(train[,1]),
  method = "glmnet",
  trControl = trainControl("cv", number = 10, classProbs = TRUE, summaryFunction = multiClassSummary),
  tuneGrid = expand.grid(alpha = 1, lambda=lambda),
  metric="Accuracy")
# Model coefficients
coef(lasso$finalModel, lasso$bestTune$lambda)
$Barolo
28 x 1 sparse Matrix of class "dgCMatrix"
                            1
(Intercept)     -11.849841488
alcohol           .          
sugar             .          
acidity           .          
tartaric          .          
malic             .          
uronic            .          
pH                .          
ash               .          
alcal_ash         .          
potassium         .          
calcium           .          
magnesium         .          
phosphate         .          
chloride          .          
phenols           .          
flavanoids        0.174755104
nonflavanoids     .          
proanthocyanins   .          
colour            .          
hue               .          
OD_dw             0.729463141
OD_fl             .          
glycerol          .          
butanediol        .          
nitrogen          .          
proline           0.004795062
methanol          .          

$Grignolino
28 x 1 sparse Matrix of class "dgCMatrix"
                            1
(Intercept)      1.461681e+01
alcohol         -9.204137e-01
sugar            .           
acidity          .           
tartaric         .           
malic           -6.597026e-02
uronic           .           
pH               .           
ash             -1.578711e+00
alcal_ash        .           
potassium       -1.607104e-06
calcium          .           
magnesium        .           
phosphate        .           
chloride         .           
phenols          .           
flavanoids       .           
nonflavanoids    .           
proanthocyanins  .           
colour          -4.840949e-01
hue              .           
OD_dw            .           
OD_fl            .           
glycerol        -2.330578e-01
butanediol       .           
nitrogen         .           
proline          .           
methanol         .           

$Barbera
28 x 1 sparse Matrix of class "dgCMatrix"
                          1
(Intercept)     -2.76696471
alcohol          .         
sugar            .         
acidity          .         
tartaric         .         
malic            0.19468148
uronic           0.97246611
pH               .         
ash              .         
alcal_ash        .         
potassium        .         
calcium          .         
magnesium        .         
phosphate        .         
chloride         .         
phenols          .         
flavanoids      -0.80529767
nonflavanoids    .         
proanthocyanins  .         
colour           0.02919194
hue             -3.08136966
OD_dw            .         
OD_fl           -0.25692597
glycerol         .         
butanediol       .         
nitrogen         .         
proline          .         
methanol         .         
# Make predictions
predictions.lasso <- lasso %>% predict(test)
# Model prediction performance
tibble(
  trueValue = wineTestNames,
  predictedValue = predictions.lasso)

In questo caso la penalità ha l’effetto di forzare alcune delle stime dei coefficienti, con un contributo minore al modello, a essere esattamente uguale a zero. Il lasso, quindi, può anche essere visto come un’alternativa ai metodi di feature selection per eseguire la selezione delle variabili al fine di ridurre la complessità del modello.

Come nel ridge, è fondamentale selezionare un buon valore di \(\lambda\).

Quando lambda è piccolo, il risultato è molto vicino alla stima dei minimi quadrati. All’aumentare di lambda, si verifica una contrazione in modo da poter eliminare le variabili che sono a zero.

caret::confusionMatrix(predictions.lasso, test$wine)
Confusion Matrix and Statistics

            Reference
Prediction   Barolo Grignolino Barbera
  Barolo         28          1       0
  Grignolino      1         33       1
  Barbera         0          1      23

Overall Statistics
                                          
               Accuracy : 0.9545          
                 95% CI : (0.8877, 0.9875)
    No Information Rate : 0.3977          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.931           
                                          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: Barolo Class: Grignolino Class: Barbera
Sensitivity                 0.9655            0.9429         0.9583
Specificity                 0.9831            0.9623         0.9844
Pos Pred Value              0.9655            0.9429         0.9583
Neg Pred Value              0.9831            0.9623         0.9844
Prevalence                  0.3295            0.3977         0.2727
Detection Rate              0.3182            0.3750         0.2614
Detection Prevalence        0.3295            0.3977         0.2727
Balanced Accuracy           0.9743            0.9526         0.9714

6.3 Elastic Net

Elastic Net combina le proprietà di Ridge e Lasso penalizzando il modello usando sia la norma L2 che la norma L1: \[ L_{ElasticNet}(\hat{\beta}) = \frac{\sum_{i = 1}^{n}{(y_i - x_i\hat{\beta})^2}}{2n} + \lambda(\frac{1-\alpha}{2}\sum_{k = 1}^{K}{\hat{\beta}_k^2} + \alpha\sum_{k = 1}^{K}{|\hat{\beta}_k}|) \]

set.seed(123)
elastic <- caret::train(
  x = train[,-1],
  y = factor(train[,1]),
  method = "glmnet",
  trControl = trainControl("cv", number = 10, classProbs = TRUE, summaryFunction = multiClassSummary),
  metric="Accuracy")
# Model coefficients
coef(elastic$finalModel, elastic$bestTune$lambda)
$Barolo
28 x 1 sparse Matrix of class "dgCMatrix"
                           1
(Intercept)     -9.730569698
alcohol          0.274590456
sugar            .          
acidity          .          
tartaric         .          
malic            .          
uronic           .          
pH               .          
ash              .          
alcal_ash       -0.039923198
potassium        .          
calcium          .          
magnesium        .          
phosphate        0.001283703
chloride         .          
phenols          .          
flavanoids       0.299534487
nonflavanoids    .          
proanthocyanins  .          
colour           .          
hue              .          
OD_dw            0.303221659
OD_fl            .          
glycerol         0.025831137
butanediol       .          
nitrogen         .          
proline          0.002009706
methanol         .          

$Grignolino
28 x 1 sparse Matrix of class "dgCMatrix"
                            1
(Intercept)     11.2568155702
alcohol         -0.5859864115
sugar            .           
acidity          .           
tartaric         .           
malic           -0.1291909425
uronic           .           
pH               .           
ash             -1.0082315724
alcal_ash        .           
potassium       -0.0003227710
calcium          .           
magnesium       -0.0062108242
phosphate        .           
chloride         .           
phenols          .           
flavanoids       .           
nonflavanoids    .           
proanthocyanins  .           
colour          -0.1938906837
hue              .           
OD_dw            .           
OD_fl            .           
glycerol        -0.1620487622
butanediol       .           
nitrogen         .           
proline         -0.0009097107
methanol         .           

$Barbera
28 x 1 sparse Matrix of class "dgCMatrix"
                          1
(Intercept)     -1.52624587
alcohol          .         
sugar            .         
acidity          .         
tartaric         0.21065074
malic            0.20195569
uronic           0.75211696
pH               .         
ash              .         
alcal_ash        .         
potassium        .         
calcium          .         
magnesium        .         
phosphate        .         
chloride         .         
phenols          .         
flavanoids      -0.39354493
nonflavanoids    .         
proanthocyanins  .         
colour           0.08931841
hue             -1.73977955
OD_dw           -0.36134581
OD_fl           -0.28785551
glycerol         .         
butanediol       .         
nitrogen         .         
proline          .         
methanol         .         
# Make predictions
predictions.enet <- elastic %>% predict(test)
# Model prediction performance
tibble(
  trueValue = wineTestNames,
  predictedValue = predictions.enet)

Oltre a impostare e scegliere un valore lambda, l’elastic net ci consente anche di ottimizzare il parametro alfa dove \(\alpha = 0\) corrisponde a ridge e \(\alpha = 1\) al lasso.

Pertanto possiamo scegliere un valore \(\alpha\) compreso tra 0 e 1 per ottimizzare l’elastic net. Se tale valore è incluso in questo intervallo, si avrà una riduzione con alcuni portati a \(0\).

caret::confusionMatrix(predictions.enet, test$wine)
Confusion Matrix and Statistics

            Reference
Prediction   Barolo Grignolino Barbera
  Barolo         29          1       0
  Grignolino      0         33       1
  Barbera         0          1      23

Overall Statistics
                                          
               Accuracy : 0.9659          
                 95% CI : (0.9036, 0.9929)
    No Information Rate : 0.3977          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9483          
                                          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: Barolo Class: Grignolino Class: Barbera
Sensitivity                 1.0000            0.9429         0.9583
Specificity                 0.9831            0.9811         0.9844
Pos Pred Value              0.9667            0.9706         0.9583
Neg Pred Value              1.0000            0.9630         0.9844
Prevalence                  0.3295            0.3977         0.2727
Detection Rate              0.3295            0.3750         0.2614
Detection Prevalence        0.3409            0.3864         0.2727
Balanced Accuracy           0.9915            0.9620         0.9714

6.4 Summary delle prestazioni

models <- list(ridge = caret::confusionMatrix(predictions.ridge, test$wine)$overall[1], 
               lasso = caret::confusionMatrix(predictions.lasso,test$wine)$overall[1],
               elastic = caret::confusionMatrix(predictions.enet,test$wine)$overall[1])
for(j in 1:length(models)){
  print(paste("Accuracy of",names(models[j]),"=",round(models[[j]],3)))
}
[1] "Accuracy of ridge = 0.955"
[1] "Accuracy of lasso = 0.955"
[1] "Accuracy of elastic = 0.966"

7 Parsimonious Gaussian Mixture Models

dist <- dist(wines[,-1], method = "manhattan")
dendo <- hclust(dist, method = "ward.D")
custom <- list()
for (i in 3:4){
  custom[[i]] = cutree(dendo, k=i)
}

models_to_run <- c("UCU", "UCC", "CUU", "CUC", "CCU", "CCC")

# Il rilassamento dato da (p-q)^2 > p+q è verificato per  1 <= q <= 20
pg.kmeans <- pgmmEM(scale(wines[,-1]), rG=3:4, rq=1:7, icl=T, zstart=2,seed=1234, modelSubset=models_to_run)
Based on k-means starting values, the best model (ICL) for the range of factors and components used is a CUU model with q = 6 and G = 3.
The ICL for this model is -11480.51.
table(wines[,1],pg.kmeans$map)
            
              1  2  3
  Barolo      0 59  0
  Grignolino 67  3  1
  Barbera     0  0 48
adjustedRandIndex(wines[,1],pg.kmeans$map)
[1] 0.9294895
pg.manhattan <- pgmmEM(scale(wines[,-1]), rG=3:4, rq=1:7, icl=T, zstart = 3, seed = 1234, zlist = custom, modelSubset=models_to_run)
Based on custom starting values, the best model (ICL) for the range of factors and components used is a CUU model with q = 6 and G = 3.
The ICL for this model is -11480.51.
table(wines[,1],pg.manhattan$map)
            
              1  2  3
  Barolo     59  0  0
  Grignolino  3  1 67
  Barbera     0 48  0
adjustedRandIndex(wines[,1],pg.manhattan$map)
[1] 0.9294895

8 Confronto finale

summaryOfModels <- function(df, models){
  nModel <- length(models)
  ari <- NULL
  bic <- NULL
  icl <- NULL
  G <- NULL
  for(i in 1:nModel){
    if (class(models[[i]])=='kmeans'){
      ari[i] <- adjustedRandIndex(df,models[[i]]$cluster)
      bic[i] <- NA
      icl[i] <- NA
      G[i] <- length(unique(models[[i]]$cluster))
    } else if (class(models[[i]])=='pam') {
      ari[i] <- adjustedRandIndex(df,models[[i]]$clustering)
      bic[i] <- NA
      icl[i] <- NA
      G[i] <- length(models[[i]]$id.med)
    } else if (class(models[[i]])=='KMeansSparseCluster'){
      modelName <- models[[i]]
      ari[i] <- adjustedRandIndex(df,modelName[[1]]$Cs)
      bic[i] <- NA
      icl[i] <- NA
      G[i] <- modelName[[1]]$wbound
    } else if (class(models[[i]])=='Mclust'){
      ari[i] <- adjustedRandIndex(df,models[[i]]$classification)
      bic[i] <- models[[i]]$bic
      icl[i] <- models[[i]]$icl
      G[i] <- models[[i]]$G
    } else if (class(models[[i]])=='ContaminatedMixt'){
      bestModel <- getBestModel(models[[i]], criterion = 'ICL')$models[[1]]
      ari[i] <- adjustedRandIndex(df,bestModel$group)
      bic[i] <- bestModel$IC$BIC
      icl[i] <- bestModel$IC$ICL
      G[i] <- bestModel$G
    } else if (class(models[[i]])=='teigen'){
      ari[i] <- adjustedRandIndex(df,models[[i]]$iclresults$classification)
      bic[i] <- models[[i]]$bic
      icl[i] <- models[[i]]$iclresults$icl
      G[i] <- models[[i]]$G
    } else if (class(models[[i]])=='pgmm'){
      ari[i] <- adjustedRandIndex(df,models[[i]]$map)
      G[i] <- models[[i]]$g
      ifelse(is.null(models[[i]]$bic[1])==TRUE,
             bic[i] <- NA,
             bic[i] <- as.double(models[[i]]$bic[1]))
      ifelse(is.null(models[[i]]$icl[1])==TRUE,
             icl[i] <- NA,
             icl[i] <- as.double(models[[i]]$icl[1]))
    } else if (class(models[[i]])=='tkmeans'){
      ari[i] <- adjustedRandIndex(df,models[[i]]$cluster)
      G[i] <- models[[i]]$k
      bic[i] <- NA
      icl[i] <- NA
    }
  }
  outputDF <- data.frame('AdjustedRandIndex' = ari,
                         'BIC' = bic,
                         'ICL' = icl,
                         'G' = as.integer(G),
                         row.names = c('KMeans3',
                                       'KMeans4',
                                       'PAM3',
                                       'PAM4',
                                       'VS-Greedy',
                                       'VS-Headlong',
                                       'VSCC',
                                       'KMeansSparse3',
                                       'KMeansSparse4',
                                       'VS-MVN',
                                       'VS-Contaminated Mixt',
                                       'VS-VS-Contaminated KMeans',
                                       'VS-Contaminated RPost',
                                       'VS-Contaminated RClass',
                                       'VS-TEigen KMeans',
                                       'VS-TEigen Hard',
                                       'VS-TEigen Soft'),
                         stringsAsFactors = F)
  return(outputDF)
}
test <- summaryOfModels(wines[,1],list(
                             k.means.3,
                             k.means.4,
                             PAM.3,
                             PAM.4,
                             subset.greedy$model,
                             subset.headlong$model,
                             vscc.mclust$bestmodel,
                             km.sparse.3,
                             km.sparse.4,
                             mixt.selected.wines,
                             cn.wines.mixt,
                             cn.wines.kmeans,
                             cn.wines.rpost,
                             cn.wines.rclass,
                             teigen.kmeans,
                             teigen.hard,
                             teigen.soft))
print(test, n=20)

9 Modellazione 3D

# best 3 for separation
plot3d(wines$flavanoids, wines$OD_dw, wines$proline, type='s', size=2, col=as.numeric(wines$wine))
# worst 3 for separation
plot3d(wines$methanol, wines$potassium, wines$pH, type='s', size=2, col=as.numeric(wines$wine))
LS0tDQp0aXRsZTogJ011bHRpdmFyaWF0ZSBkYXRhIGFuYWx5c2lzOiBhIGRpc2NyaW1pbmF0aW5nIG1ldGhvZCBvZiB0aGUgb3JpZ2luIG9mIHdpbmVzJw0KYXV0aG9yOiAiTWF0dGVvIEZhc3VsbywgU2ltb25lIEZsYXZpbyBQYXJpcywgTWF0dGVvIFNpdm9jY2lhIg0KZGF0ZTogIjIzLzExLzIwMjEiDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6IDMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogICAgdGhlbWU6IHVuaXRlZA0KICAgIGhpZ2hsaWdodDogdGFuZ28NCiAgICBkZl9wcmludDogcGFnZWQNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogJzMnDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0b2NfZmxvYXQ6IHllcw0KICAgIHRoZW1lOiB1bml0ZWQNCiAgICBoaWdobGlnaHQ6IHRhbmdvDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQotLS0NCmBgYHtyIGxvYWRFbnYsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpzb3VyY2UoImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9NYXR0ZW9GYXN1bG8vUmFyZS1FYXJ0aC9tYWluL1IvdXRpbC9jb3JlRnVuY3Rpb25zLlIiKQ0KDQpsb2FkUGFja2FnZXMoYygnc24nLCd0aWR5dmVyc2UnLCdwc3ljaCcsJ1JDb2xvckJyZXdlcicsJ3N0YXJnYXplcicsJ21jbHVzdCcsJ0NvbnRhbWluYXRlZE1peHQnLA0KICAgICAgICAgICAgICAgJ3Bsb3RseScsJ2dncGxvdDInLCdnZ2RlbmRybycsJ3RlaWdlbicsJ3RjbHVzdCcsJ0hETUQnLCdjYVRvb2xzJywnY2x1c3R2YXJzZWwnLA0KICAgICAgICAgICAgICAgJ3ZzY2MnLCdzcGFyY2wnLCdwZ21tJywnY2FyZXQnLCdnbG1uZXQnLCdNTG1ldHJpY3MnLCdyZ2wnKSkNCg0KbG9hZCgiWjpcXERlc2t0b3BDXFxMVU1TQVxcMlxcRGF0YSBNaW5pbmdcXEZpbml0ZSBNaXh0dXJlXFxGaW5pdGVNaXh0dXJlTDMxLlJEYXRhIikNCiNsb2FkKCJIOlxcc21veHlcXERvd25sb2Fkc1xcRmluaXRlTWl4dHVyZUwzMS5SRGF0YSIpDQpybShDTzJkYXRhKQ0Kcm0oTk9kYXRhKQ0Kcm0odG9uZWRhdGEpDQp0eXBlIDwtIHdpbmUkVHlwZQ0Kcm0od2luZSkNCmBgYA0KIyBJbnRyb2R1emlvbmUNCkRhdGkgc3UgMjcgY2FyYXR0ZXJpc3RpY2hlIGNoaW1pY28vZmlzaWNoZSBkaSB0cmUgZGl2ZXJzaSB0aXBpIGRpIHZpbm8gKEJhcm9sbywgR3JpZ25vbGlubywgQmFyYmVyYSkNCmRhbCBQaWVtb250ZS4gVW4gc2V0IGRpIGRhdGkgY29uIDE3OCBvc3NlcnZhemlvbmkgZSAyOCB2YXJpYWJpbGkgKGRpIGN1aSBsYSBwcmltYSByZWxhdGl2YSBhbGxhIHRpcG9sb2dpYSBkaSB2aW5vKS4gTmVsbCdvcmRpbmU6IA0KDQotIEJhcm9sbyANCi0gR3JpZ25vbGlubw0KLSBCYXJiZXJhDQoNCmBgYHtyIGR0VGFibGV9DQpkYXRhKHdpbmVzKQ0Kd2luZXMNCmBgYA0KDQojIyBBbm5hdGEgZGVsIHZpbm86IHVuIGZhdHRvcmUgZGEgY29uc2lkZXJhcmU/DQpFJyBzdGF0byBwb3NzaWJpbGUgYXR0cmF2ZXJzbyBsYSByaWNlcmNhIG9yaWdpbmFyaWEgcmlzYWxpcmUgYWxsJ2Fubm8gZGkgb3NzZXJ2YXppb25lIGRpIGNpYXNjdW4gdmluby4gRGkgc2VndWl0byB2ZW5nb25vIHJpcG9ydGF0ZSBsZSBvc3NlcnZhemlvbmkgZGVpIHRyZSBkaXZlcnNpIHRpcGkgZGkgdmlubyBkdXJhbnRlIGdsaSBhbm5pOg0KDQpgYGB7ciB2aW5vQW5uaSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnllYXIgPC0gYXMubnVtZXJpYyhzdWJzdHIocm93bmFtZXMod2luZXMpLCA2LCA3KSkNCnRhYmxlKHdpbmVzJHdpbmUsIHllYXIpDQojd2luZXNbLCd3aW5lJ10gPC0gdHlwZQ0KYGBgDQpOb3RpYW1vIHN1Yml0byBjaGUgaWwgQmFyYmVyYSDDqCBkaXN0cmlidWl0byBwcmluY2lwYWxtZW50ZSBuZWdsaSB1bHRpbWkgYW5uaSAoNzYsNzgsNzkpIG1lbnRyZSBpbCBCYXJvbG8gbmVsIDcxLCA3MyBlIDc0Lg0KUGVyIHF1YW50byByaWd1YXJkYSBsYSBwZXJjZW50dWFsZSBkZWxsZSBzaW5nb2xlIGNsYXNzaToNCg0KYGBge3IgbkNsYXNzaSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCndpbmVzICU+JQ0KICBjb3VudCh3aW5lID0gZmFjdG9yKHdpbmUpKSAlPiUNCiAgbXV0YXRlKHBjdCA9IHByb3AudGFibGUobikpICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gd2luZSwgeSA9IHBjdCwgZmlsbCA9IHdpbmUsIGxhYmVsID0gc2NhbGVzOjpwZXJjZW50KHBjdCkpKSArIA0KICBnZW9tX2NvbChwb3NpdGlvbiA9ICdkb2RnZScpICsgDQogIGdlb21fdGV4dChwb3NpdGlvbiA9IHBvc2l0aW9uX2RvZGdlKHdpZHRoID0gLjkpLA0KICAgICAgICAgICAgdmp1c3QgPSAtMC41LCANCiAgICAgICAgICAgIHNpemUgPSAzKSArIA0KICBzY2FsZV95X2NvbnRpbnVvdXMobmFtZSA9ICJQZXJjZW50YWdlIikrDQogIHNjYWxlX3hfZGlzY3JldGUobmFtZSA9ICJXaW5lIE5hbWUiKSsNCiAgc2NhbGVfZmlsbF9odWUobD00MCwgYz0zNSkrDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikNCmBgYA0KRScgY2hpYXJvIGNoZSBpbCBHcmlnbm9saW5vIHNpYSBpbCBwacO5IG51bWVyb3NvICgzOS45JSkgc2VndWl0byBkYWwgQmFyb2xvICgzMy4xJSkgZSBkYWwgQmFyYmVyYSAoMjcuMCUpLg0KDQpQZXIgcmFwcHJlc2VudGFyZSBsYSBkaXNwZXJzaW9uZSBkZWkgZGF0aSBhYmJpYW1vIHVzYXRvIHVubyBzY2F0dGVycGxvdCBsZWdnZXJtZW50ZSBkaWZmZXJlbnRlIGRhbCBzb2xpdG8uIFN1bGxhIGRpYWdvbmFsZSBzdXBlcmlvcmUgc2kgdmVkZSBsYSBkaXN0cmlidXppb25lIGRlaSBkYXRpIG1lbnRyZSBzdWxsYSBkaWFnb25hbGUgaW5mZXJpb3JlIHZpIMOoIGxhIF9jb3JyZWxhemlvbmUgZGkgUGVhcnNvbl8gdHJhIGxlIHZhcmlhYmlsaS4NCg0KYGBge3Igc2NhdHRlclBsb3QsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpteV9jb2xzIDwtIGMoIiMwMEFGQkIiLCAiI0U3QjgwMCIsICIjRkM0RTA3IikgDQpwYW5lbC5jb3IgPC0gZnVuY3Rpb24oeCwgeSl7DQogICAgdXNyIDwtIHBhcigidXNyIik7IG9uLmV4aXQocGFyKHVzcikpDQogICAgcGFyKHVzciA9IGMoMCwgMSwgMCwgMSkpDQogICAgciA8LSByb3VuZChjb3IoeCwgeSksIGRpZ2l0cz0yKQ0KICAgIHR4dCA8LSBwYXN0ZTAoIlIgPSAiLCByKQ0KICAgIHRleHQoMC41LCAwLjUsIHR4dCwgY2V4ID0gMSkNCn0NCnVwcGVyLnBhbmVsPC1mdW5jdGlvbih4LCB5KXsNCiAgcG9pbnRzKHgseSwgcGNoID0gMTksIGNvbCA9IG15X2NvbHNbd2luZXMkd2luZV0sY2V4PS41KQ0KfQ0KcGFpcnMod2luZXNbLDI6OF0sIA0KICAgICAgbG93ZXIucGFuZWwgPSBwYW5lbC5jb3IsDQogICAgICB1cHBlci5wYW5lbCA9IHVwcGVyLnBhbmVsKQ0KYGBgDQpEYWxsZSBwcmltZSA3IHZhcmlhYmlsaSwgw6ggcG9zc2liaWxlIG5vdGFyZSB1bmEgY29ycmVsYXppb25lIGRpIDAuNjkgdHJhICphY2lkaXR5KiBlICptYWxpYyogZSBvdnZpYW1lbnRlIHVuYSByZWxhemlvbmUgaW52ZXJzYW1lbnRlIHByb3Bvcnppb25hbGUgdHJhICpwSCogZSAqYWNpZGl0eSouDQoNCiMgU3RhdGlzdGljaGUgZGVzY3JpdHRpdmUNClBlciB2aXN1YWxpenphcmUgbGUgc3RhdGlzdGljaGUgZGVzY3JpdHRpdmUgKG1lZGlhIGUgZGV2aWF6aW9uZSBzdGFuZGFyZCkgY2kgw6ggc2VtYnJhdG8gb3Bwb3J0dW5vIGRpdmlkZXJsZSBpbiBiYXNlIGFsbGEgY2xhc3NlIGRpIGFwcGFydGVuZW56YToNCmBgYHtyIGRlc2NyaXB0aXZlLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KcHJpbnRNZWFuQW5kU2RCeUdyb3VwIDwtIGZ1bmN0aW9uKHZhcmlhYmxlcyxncm91cHZhcmlhYmxlKQ0KICB7DQogICAgIHZhcmlhYmxlbmFtZXMgPC0gYyhuYW1lcyhncm91cHZhcmlhYmxlKSxuYW1lcyhhcy5kYXRhLmZyYW1lKHZhcmlhYmxlcykpKQ0KICAgICBncm91cHZhcmlhYmxlIDwtIGdyb3VwdmFyaWFibGVbLDFdDQogICAgIG1lYW5zIDwtIGFnZ3JlZ2F0ZShhcy5tYXRyaXgodmFyaWFibGVzKSB+IGdyb3VwdmFyaWFibGUsIEZVTiA9IG1lYW4pDQogICAgIG5hbWVzKG1lYW5zKSA8LSB2YXJpYWJsZW5hbWVzDQogICAgIHByaW50KHBhc3RlKCJNZWFuczoiKSkNCiAgICAgcHJpbnQobWVhbnMpDQogICAgIHNkcyA8LSBhZ2dyZWdhdGUoYXMubWF0cml4KHZhcmlhYmxlcykgfiBncm91cHZhcmlhYmxlLCBGVU4gPSBzZCkNCiAgICAgbmFtZXMoc2RzKSA8LSB2YXJpYWJsZW5hbWVzDQogICAgIHByaW50KHBhc3RlKCJTdGFuZGFyZCBkZXZpYXRpb25zOiIpKQ0KICAgICBwcmludChzZHMpDQp9DQpwcmludE1lYW5BbmRTZEJ5R3JvdXAod2luZXNbMjoyOF0sd2luZXNbMV0pDQpgYGANCkFsY3VuZSBjb25zaWRlcmF6aW9uaToNCg0KLSBMYSBtZWRpYSBkaSBzdWdhciwgcG90YXNzaXVtLCBtYWduZXNpdW0sIHBob3NwaGF0ZSwgY2hsb3JpZGUsIGZsYXZhbm9pZHMsIHByb2FudGhvY3lhbmlucywgY29sb3VyIG5lbCBCYXJvbG8gw6ggcGnDuSBhbHRhLg0KLSBMYSBtZWRpYSBkaSBhY2lkaXR5LCB0YXJ0YXJpYywgbWFsaWMsIHVyb25pYywgYWxjYWxfYXNoIG5lbCBCYXJiZXJhIMOoIHBpw7kgYWx0YS4NCi0gTGEgZGV2aWF6aW9uZSBzdGFuZGFyZCBkaSBhY2lkaXR5IMOoIHBpw7kgYWx0YSBuZWwgR3JpZ25vbGluby4gDQoNCiMjIFZhcmlhbnphIFdpdGhpbg0KQWJiaWFtbyBjYWxjb2xhdG8gbGEgdmFyaWFuemEgd2l0aGluIHRyYSB1bmEgZmVhdHVyZSBlIGkgdGlwaSBkaSB2aW5vOg0KYGBge3Igd2l0aGluVmFyaWFuY2UsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpjYWxjV2l0aGluR3JvdXBzVmFyaWFuY2UgPC0gZnVuY3Rpb24odmFyaWFibGUsZ3JvdXB2YXJpYWJsZSkNCiAgew0KICAgICBncm91cHZhcmlhYmxlMiA8LSBhcy5mYWN0b3IoZ3JvdXB2YXJpYWJsZVtbMV1dKQ0KICAgICBsZXZlbHMgPC0gbGV2ZWxzKGdyb3VwdmFyaWFibGUyKQ0KICAgICBudW1sZXZlbHMgPC0gbGVuZ3RoKGxldmVscykNCiAgICAgbnVtdG90YWwgPC0gMA0KICAgICBkZW5vbXRvdGFsIDwtIDANCiAgICAgZm9yIChpIGluIDE6bnVtbGV2ZWxzKQ0KICAgICB7DQogICAgICAgIGxldmVsaSA8LSBsZXZlbHNbaV0NCiAgICAgICAgbGV2ZWxpZGF0YSA8LSB2YXJpYWJsZVtncm91cHZhcmlhYmxlPT1sZXZlbGksXQ0KICAgICAgICBsZXZlbGlsZW5ndGggPC0gbGVuZ3RoKGxldmVsaWRhdGEpDQogICAgICAgIHNkaSA8LSBzZChsZXZlbGlkYXRhKQ0KICAgICAgICBudW1pIDwtIChsZXZlbGlsZW5ndGggLSAxKSooc2RpICogc2RpKQ0KICAgICAgICBkZW5vbWkgPC0gbGV2ZWxpbGVuZ3RoDQogICAgICAgIG51bXRvdGFsIDwtIG51bXRvdGFsICsgbnVtaQ0KICAgICAgICBkZW5vbXRvdGFsIDwtIGRlbm9tdG90YWwgKyBkZW5vbWkNCiAgICAgfQ0KICAgICBWdyA8LSBudW10b3RhbCAvIChkZW5vbXRvdGFsIC0gbnVtbGV2ZWxzKQ0KICAgICByZXR1cm4oVncpDQp9DQpjYWxjV2l0aGluR3JvdXBzVmFyaWFuY2Uod2luZXNbImZsYXZhbm9pZHMiXSx3aW5lc1sxXSkNCmBgYA0KIyMgVmFyaWFuemEgQmV0d2Vlbg0KU3Rlc3NvIGRpc2NvcnNvIHBlciBsYSB2YXJpYW56YSBiZXR3ZWVuIHRyYSB1bmEgZmVhdHVyZSBlIGkgdmluaTogDQpgYGB7ciBiZXR3ZWVuVmFyaWFuY2UsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpjYWxjQmV0d2Vlbkdyb3Vwc1ZhcmlhbmNlIDwtIGZ1bmN0aW9uKHZhcmlhYmxlLGdyb3VwdmFyaWFibGUpDQogIHsNCiAgICAgZ3JvdXB2YXJpYWJsZTIgPC0gYXMuZmFjdG9yKGdyb3VwdmFyaWFibGVbWzFdXSkNCiAgICAgbGV2ZWxzIDwtIGxldmVscyhncm91cHZhcmlhYmxlMikNCiAgICAgbnVtbGV2ZWxzIDwtIGxlbmd0aChsZXZlbHMpDQogICAgIGdyYW5kbWVhbiA8LSBzYXBwbHkodmFyaWFibGUsbWVhbikNCiAgICAgbnVtdG90YWwgPC0gMA0KICAgICBkZW5vbXRvdGFsIDwtIDANCiAgICAgZm9yIChpIGluIDE6bnVtbGV2ZWxzKQ0KICAgICB7DQogICAgICAgIGxldmVsaSA8LSBsZXZlbHNbaV0NCiAgICAgICAgbGV2ZWxpZGF0YSA8LSB2YXJpYWJsZVtncm91cHZhcmlhYmxlPT1sZXZlbGksXQ0KICAgICAgICBsZXZlbGlsZW5ndGggPC0gbGVuZ3RoKGxldmVsaWRhdGEpDQogICAgICAgIG1lYW5pIDwtIG1lYW4obGV2ZWxpZGF0YSkNCiAgICAgICAgc2RpIDwtIHNkKGxldmVsaWRhdGEpDQogICAgICAgIG51bWkgPC0gbGV2ZWxpbGVuZ3RoICogKChtZWFuaSAtIGdyYW5kbWVhbileMikNCiAgICAgICAgZGVub21pIDwtIGxldmVsaWxlbmd0aA0KICAgICAgICBudW10b3RhbCA8LSBudW10b3RhbCArIG51bWkNCiAgICAgICAgZGVub210b3RhbCA8LSBkZW5vbXRvdGFsICsgZGVub21pDQogICAgIH0NCiAgICAgVmIgPC0gbnVtdG90YWwgLyAobnVtbGV2ZWxzIC0gMSkNCiAgICAgVmIgPC0gVmJbWzFdXQ0KICAgICByZXR1cm4oVmIpDQp9DQpjYWxjQmV0d2Vlbkdyb3Vwc1ZhcmlhbmNlKHdpbmVzWyJmbGF2YW5vaWRzIl0sd2luZXNbMV0pDQpgYGANCiMjIFNlcGFyYXppb25lDQpQZXIgdmVkZXJlIHF1YWxpIHZhcmlhYmlsaSBoYW5ubyBsYSBtYWdnaW9yZSBzZXBhcmF6aW9uZSwgKHJhcHBvcnRvIHRyYSBWYXJpYW56YSBCZXR3ZWVuIGUgVmFyaWFuemEgV2l0aGluKSBhYmJpYW1vIHNjcml0dG8gdW5hIGZ1bnppb25lIGFwcG9zaXRhIHBlciBjYWxjb2xhcm5lIGlsIHZhbG9yZSBwZXIgb2duaSBmZWF0dXJlLg0KYGBge3Igc2VwYXJhdGlvbiwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmNhbGNTZXBhcmF0aW9ucyA8LSBmdW5jdGlvbih2YXJpYWJsZXMsZ3JvdXB2YXJpYWJsZSkNCiAgew0KICAgICB2YXJpYWJsZXMgPC0gYXMuZGF0YS5mcmFtZSh2YXJpYWJsZXMpDQogICAgIG51bXZhcmlhYmxlcyA8LSBsZW5ndGgodmFyaWFibGVzKQ0KICAgICB2YXJpYWJsZW5hbWVzIDwtIGNvbG5hbWVzKHZhcmlhYmxlcykNCiAgICAgVncgPC0gTlVMTA0KICAgICBWYiA8LSBOVUxMDQogICAgIHNlcCA8LSBOVUxMDQogICAgIGZvciAoaSBpbiAxOm51bXZhcmlhYmxlcykNCiAgICAgew0KICAgICAgICB2YXJpYWJsZWkgPC0gdmFyaWFibGVzW2ldDQogICAgICAgIHZhcmlhYmxlbmFtZSA8LSB2YXJpYWJsZW5hbWVzW2ldDQogICAgICAgIFZ3W2ldIDwtIGNhbGNXaXRoaW5Hcm91cHNWYXJpYW5jZSh2YXJpYWJsZWksIGdyb3VwdmFyaWFibGUpDQogICAgICAgIFZiW2ldIDwtIGNhbGNCZXR3ZWVuR3JvdXBzVmFyaWFuY2UodmFyaWFibGVpLCBncm91cHZhcmlhYmxlKQ0KICAgICAgICBzZXBbaV0gPC0gVmJbaV0vVndbaV0NCiAgICAgfQ0KICAgICByZXN1bHQgPC0gZGF0YS5mcmFtZSgnV2l0aGluJz1WdywnQmV0d2Vlbic9VmIsJ1NlcCc9c2VwLCByb3cubmFtZXMgPSBhcy52ZWN0b3IoY29sbmFtZXMod2luZXNbLC0xXSkpLCBzdHJpbmdzQXNGYWN0b3JzID0gRikNCiAgICAgcmVzdWx0W29yZGVyKHJlc3VsdCRTZXAsZGVjcmVhc2luZyA9IFQpLF0NCn0NCmNhbGNTZXBhcmF0aW9ucyh3aW5lc1syOjI4XSx3aW5lc1sxXSkNCmBgYA0KIyBTcGxpdHRpbmcgaW4gVHJhaW4gZSBUZXN0DQpQZXIgYWxjdW5lIGFuYWxpc2kgc3VjY2Vzc2l2ZSwgYWJiaWFtbyBwcm92YXRvIGEgY2FtYmlhcmUgaWwgdGFzayBkZWxsYSByaWNlcmNhIHRlbnRhbmRvIGRpIHV0aWxpenphcmUgdW4gbW9kZWxsbyBjb21lIGNsYXNzaWZpY2F0b3JlIGUgZGkgbWlzdXJhcm5lIGxlIHByZXN0YXppb25pIGRpIGNsYXNzaWZpY2F6aW9uZS4gQSB0YWxlIHNjb3BvLCBhYmJpYW1vIHN1ZGRpdmlzbyBpbCBkYXRhc2V0IG9yaWdpbmFyaW8gaW4gZHVlIHNvdHRvZ3J1cHBpOiANCg0KLSB0cmFpbg0KLSB0ZXN0DQpgYGB7ciBzcGxpdFRyYWluVGVzdCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJlcXVpcmUoY2FUb29scykNCnNhbXBsZSA9IHNhbXBsZS5zcGxpdCh3aW5lc1ssMV0sIFNwbGl0UmF0aW8gPSAuNTApDQoNCnRyYWluID0gc3Vic2V0KHdpbmVzLCBzYW1wbGUgPT0gVFJVRSkNCnRyYWluVGVzdE5hbWVzIDwtIHRyYWluJHdpbmUNCnByaW50KHBhc3RlKCJUcmFpbiBPYnM6Iixucm93KHRyYWluKSkpDQojdHJhaW4kd2luZSA8LSBhcy5udW1lcmljKHRyYWluJHdpbmUpDQoNCnRlc3QgID0gc3Vic2V0KHdpbmVzLCBzYW1wbGUgPT0gRkFMU0UpDQp3aW5lVGVzdE5hbWVzIDwtIHRlc3Qkd2luZQ0KcHJpbnQocGFzdGUoIlRlc3QgT2JzOiIsbnJvdyh0ZXN0KSkpDQojdGVzdCR3aW5lIDwtIGFzLm51bWVyaWModGVzdCR3aW5lKQ0KYGBgDQojIENsdXN0ZXJpbmcNClBlciBpbCBDbHVzdGVyaW5nIGFiYmlhbW8gZGVjaXNvIGRpIGFwcGxpY2FyZTogDQoNCi0gVW4gYXBwcm9jY2lvICoqRGlzdGFuY2UtQmFzZWQqKjoNCiAgLSBHZXJhcmNoaWNvOg0KICAgIC0gRXVjbGlkZWFuLCBNaW5rb3dza2ksIE1hbmhhdHRhbiwgTWFoYWxhbm9iaXMNCiAgLSBEaSBwYXJ0aXppb25hbWVudG86DQogICAgLSBLTWVhbnMNCiAgICAtIFBBTQ0KLSBVbiBhcHByb2NjaW8gKipNb2RlbC1CYXNlZCoqOg0KICAtIEdhdXNzaWFuIE1peHR1cmUgKE1jbHVzdCkNCiAgLSBDb250YW1pbmF0ZWQgTm9ybWFsIChDTm1peHQpDQogIC0gTXVsdGl2YXJpYXRlIHQgRGlzdHJpYnV0aW9uICh0ZWlnZW4pDQogIC0gUGFyc2ltb25pb3VzIEdhdXNzaWFuIE1peHR1cmUgTW9kZWxzIChwZ21tKQ0KDQoNCiMjIERpc3RhbmNlLUJhc2VkDQpgYGB7ciBkaXN0YW5jZSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmRpc3NNYXRyaXggPC0gcGFpcndpc2UubWFoYWxhbm9iaXMod2luZXNbLC0xXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXBpbmcgPSBjKDE6bnJvdyh3aW5lcykpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb3YgPSBjb3Yod2luZXNbLC0xXSkpJGRpc3RhbmNlDQpkaXNzTWF0cml4IDwtIHNxcnQoZGlzc01hdHJpeCkNCmRpc3NNYXRyaXggPC0gYXMuZGlzdChkaXNzTWF0cml4KQ0KY29tYkRpc3QgPC0gZnVuY3Rpb24oZGlzdGFuY2UsIG1ldGhvZHMsIGRmLCBkdCwgZGlzc01hdHJpeCkgew0KICBjIDwtIDANCiAgcmVzdWx0cyA8LSBsaXN0KCkNCiAgZm9yIChpIGluIDE6bGVuZ3RoKGRpc3RhbmNlKSl7DQogICAgaWZlbHNlKGRpc3RhbmNlW2ldID09ICJtaW5rb3dza2kiLA0KICAgICAgICAgICBkaXN0IDwtIGRpc3QoZGYsIG1ldGhvZCA9IGRpc3RhbmNlW2ldLCBwID0gNCksDQogICAgICAgICAgIGlmZWxzZShkaXN0YW5jZVtpXSA9PSAibWFoYWxhbm9iaXMiLA0KICAgICAgICAgICAgICAgICAgZGlzdCA8LSBkaXNzTWF0cml4LA0KICAgICAgICAgICAgICAgICAgZGlzdCA8LSBkaXN0KGRmLCBtZXRob2QgPSBkaXN0YW5jZVtpXSkpKQ0KICAgIGZvciAoaiBpbiAxOmxlbmd0aChtZXRob2RzKSl7DQogICAgICBkZW5kbyA9IGhjbHVzdChkaXN0LCBtZXRob2QgPSBtZXRob2RzW2pdKQ0KICAgICAgZGVuZG8ueCA9IGdnZGVuZHJvZ3JhbShkZW5kbywgcm90YXRlID0gRiwgc2l6ZSA9IDIsIGxlYWZfbGFiZWxzID0gVCwgbGFiZWxzID0gRikgKyANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBnZ3RpdGxlKHBhc3RlKGRpc3RhbmNlW2ldLCcgJyxtZXRob2RzW2pdLHNlcD0nJykpDQogICAgICBmb3IoZWxlbSBpbiAyOjQpew0KICAgICAgICBjbHVzdGVyID0gY3V0cmVlKGRlbmRvLCBrPWVsZW0pDQogICAgICAgIGMgPC0gYyArIDENCiAgICAgICAgcmVzdWx0c1tbY11dIDwtIGxpc3QoZGlzdGFuY2UgPSBkaXN0YW5jZVtpXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gbWV0aG9kc1tqXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXBzID0gZWxlbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGFibGUgPSB0YWJsZShkdCxjbHVzdGVyKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGVuZG8gPSBkZW5kby54LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBBZGp1c3RlZFJhbmRJbmRleCA9IGFkanVzdGVkUmFuZEluZGV4KGR0LGNsdXN0ZXIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbHVzdGVyID0gY2x1c3RlcikNCiAgICAgIH0NCiAgICB9DQogIH0NCiAgcmV0dXJuKHJlc3VsdHMpDQp9DQpyZXN1bHRzIDwtIGNvbWJEaXN0KGMoImV1Y2xpZGVhbiIsICJtYW5oYXR0YW4iLCAibWlua293c2tpIiwibWFoYWxhbm9iaXMiKSwNCiAgICAgICAgICAgICAgICAgICAgYygic2luZ2xlIiwgImNvbXBsZXRlIiwgImF2ZXJhZ2UiLCAid2FyZC5EIiksIHNjYWxlKHdpbmVzWywtMV0pLCB3aW5lc1ssMV0sIGRpc3NNYXRyaXgpDQoNCm9wdGltYWwgPC0gZnVuY3Rpb24ocmVzdWx0cyl7DQogIGJlc3RfcmFuZEluZGV4LmV1ID0gMA0KICBiZXN0X3JhbmRJbmRleC5tYSA9IDANCiAgYmVzdF9yYW5kSW5kZXgubWkgPSAwDQogIGJlc3RfcmFuZEluZGV4Lm1haGEgPSAwDQogIGJlc3RfbW9kZWwuZXUgPSBpbnRlZ2VyKCkNCiAgYmVzdF9tb2RlbC5tYSA9IGludGVnZXIoKQ0KICBiZXN0X21vZGVsLm1pID0gaW50ZWdlcigpDQogIGJlc3RfbW9kZWwubWFoYSA9IGludGVnZXIoKQ0KICBmb3IgKGkgaW4gMTpsZW5ndGgocmVzdWx0cykpew0KICAgIGN1cnJlbnRfcmFuZEluZGV4ID0gcmVzdWx0c1tbaV1dJEFkanVzdGVkUmFuZEluZGV4DQogICAgaWYgKHJlc3VsdHNbW2ldXSRkaXN0YW5jZSA9PSAiZXVjbGlkZWFuIil7DQogICAgICBpZiAoY3VycmVudF9yYW5kSW5kZXggPiBiZXN0X3JhbmRJbmRleC5ldSkgew0KICAgICAgICBiZXN0X3JhbmRJbmRleC5ldSA9IGN1cnJlbnRfcmFuZEluZGV4DQogICAgICAgIGJlc3RfbW9kZWwuZXUgPSBpDQogICAgICB9DQogICAgfQ0KICAgIGVsc2UgaWYgKHJlc3VsdHNbW2ldXSRkaXN0YW5jZSA9PSAibWFuaGF0dGFuIil7DQogICAgICBpZiAoY3VycmVudF9yYW5kSW5kZXggPiBiZXN0X3JhbmRJbmRleC5tYSkgew0KICAgICAgICBiZXN0X3JhbmRJbmRleC5tYSA9IGN1cnJlbnRfcmFuZEluZGV4DQogICAgICAgIGJlc3RfbW9kZWwubWEgPSBpDQogICAgICB9DQogICAgfQ0KICAgIGVsc2UgaWYgKHJlc3VsdHNbW2ldXSRkaXN0YW5jZSA9PSAibWlua293c2tpIil7DQogICAgICBpZiAoY3VycmVudF9yYW5kSW5kZXggPiBiZXN0X3JhbmRJbmRleC5taSkgew0KICAgICAgICBiZXN0X3JhbmRJbmRleC5taSA9IGN1cnJlbnRfcmFuZEluZGV4DQogICAgICAgIGJlc3RfbW9kZWwubWkgPSBpDQogICAgICB9DQogICAgfQ0KICAgIGVsc2UgaWYgKHJlc3VsdHNbW2ldXSRkaXN0YW5jZSA9PSAibWFoYWxhbm9iaXMiKXsNCiAgICAgIGlmIChjdXJyZW50X3JhbmRJbmRleCA+IGJlc3RfcmFuZEluZGV4Lm1haGEpIHsNCiAgICAgICAgYmVzdF9yYW5kSW5kZXgubWFoYSA9IGN1cnJlbnRfcmFuZEluZGV4DQogICAgICAgIGJlc3RfbW9kZWwubWFoYSA9IGkNCiAgICAgIH0NCiAgICB9DQogIH0NCiAgI3ByaW50KGxpc3QoZXVjbGlkZWFuID0gbGlzdChtb2RlbC5udW1iZXIgPSBiZXN0X21vZGVsLmV1LA0KICAjICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsdXN0ZXIgPSByZXN1bHRzW1tiZXN0X21vZGVsLmV1XV0kZ3JvdXBzLA0KICAjICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFkanVzdGVkUmFuZEluZGV4ID0gYmVzdF9yYW5kSW5kZXguZXUpLA0KICAjICAgICAgICAgICBtYW5oYXR0YW4gPSBsaXN0KG1vZGVsLm51bWJlciA9IGJlc3RfbW9kZWwubWEsDQogICMgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2x1c3RlciA9IHJlc3VsdHNbW2Jlc3RfbW9kZWwubWFdXSRncm91cHMsDQogICMgICAgICAgICAgICAgICAgICAgICAgICAgICAgQWRqdXN0ZWRSYW5kSW5kZXggPSBiZXN0X3JhbmRJbmRleC5tYSksDQogICMgICAgICAgICAgIG1pbmtvd3NraSA9IGxpc3QobW9kZWwubnVtYmVyID0gYmVzdF9tb2RlbC5taSwNCiAgIyAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbHVzdGVyID0gcmVzdWx0c1tbYmVzdF9tb2RlbC5taV1dJGdyb3VwcywNCiAgIyAgICAgICAgICAgICAgICAgICAgICAgICAgICBBZGp1c3RlZFJhbmRJbmRleCA9IGJlc3RfcmFuZEluZGV4Lm1pKSwNCiAgIyAgICAgICAgICAgbWFoYWxhbm9iaXM9bGlzdChtb2RlbC5udW1iZXIgPSBiZXN0X21vZGVsLm1haGEsDQogICMgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2x1c3RlciA9IHJlc3VsdHNbW2Jlc3RfbW9kZWwubWFoYV1dJGdyb3VwcywNCiAgIyAgICAgICAgICAgICAgICAgICAgICAgICAgICBBZGp1c3RlZFJhbmRJbmRleCA9IGJlc3RfcmFuZEluZGV4Lm1haGEpKQ0KICAjICAgICkNCiAgcmV0dXJuKGxpc3QoZXVjbGlkZWFuID0gcmVzdWx0c1tbYmVzdF9tb2RlbC5ldV1dLA0KICAgICAgICAgICAgICBtYW5oYXR0YW4gPSByZXN1bHRzW1tiZXN0X21vZGVsLm1hXV0sDQogICAgICAgICAgICAgIG1pbmtvd3NraSA9IHJlc3VsdHNbW2Jlc3RfbW9kZWwubWldXSwNCiAgICAgICAgICAgICAgbWFoYWxhbm9iaXM9cmVzdWx0c1tbYmVzdF9tb2RlbC5tYWhhXV0pKQ0KfQ0KYmVzdF9kaXN0X21vZGVsID0gb3B0aW1hbChyZXN1bHRzKQ0KYGBgDQojIyMgRGVuZHJvZ3JhbW1pIHsudGFic2V0fQ0KDQojIyMjIEV1Y2xpZGVhbg0KYGBge3IgZXVjbGlkZWFuLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KZ2dwbG90bHkoYmVzdF9kaXN0X21vZGVsJGV1Y2xpZGVhbiRkZW5kbykNCnByaW50KGJlc3RfZGlzdF9tb2RlbCRldWNsaWRlYW4kdGFibGUpDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChiZXN0X2Rpc3RfbW9kZWwkZXVjbGlkZWFuJEFkanVzdGVkUmFuZEluZGV4LDMpKSkNCmBgYA0KIyMjIyBNYW5oYXR0YW4NCmBgYHtyIG1hbmhhdHRhbiwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmdncGxvdGx5KGJlc3RfZGlzdF9tb2RlbCRtYW5oYXR0YW4kZGVuZG8pDQpwcmludChiZXN0X2Rpc3RfbW9kZWwkbWFuaGF0dGFuJHRhYmxlKQ0KcHJpbnQocGFzdGUoIkFkanVzdGVkUmFuZEluZGV4OiIscm91bmQoYmVzdF9kaXN0X21vZGVsJG1hbmhhdHRhbiRBZGp1c3RlZFJhbmRJbmRleCwzKSkpDQpgYGANCiMjIyMgTWlua293c2tpDQpgYGB7ciBtaW5rb3dza2ksIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpnZ3Bsb3RseShiZXN0X2Rpc3RfbW9kZWwkbWlua293c2tpJGRlbmRvKQ0KcHJpbnQoYmVzdF9kaXN0X21vZGVsJG1pbmtvd3NraSR0YWJsZSkNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGJlc3RfZGlzdF9tb2RlbCRtaW5rb3dza2kkQWRqdXN0ZWRSYW5kSW5kZXgsMykpKQ0KYGBgDQojIyMjIE1haGFsYW5vYmlzDQpgYGB7ciBtYWhhbGFub2JpcywgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmdncGxvdGx5KGJlc3RfZGlzdF9tb2RlbCRtYWhhbGFub2JpcyRkZW5kbykNCnByaW50KGJlc3RfZGlzdF9tb2RlbCRtYWhhbGFub2JpcyR0YWJsZSkNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGJlc3RfZGlzdF9tb2RlbCRtYWhhbGFub2JpcyRBZGp1c3RlZFJhbmRJbmRleCwzKSkpDQpgYGANCg0KIyMjIEstTWVhbnMgZSBQQU0gey50YWJzZXR9DQojIyMjIEtNZWFucyAzDQpgYGB7ciBrbWVhbnMzLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KcmVxdWlyZShjbHVzdGVyKQ0Kay5tZWFucy4zIDwtIGttZWFucyhzY2FsZSh3aW5lc1ssLTFdKSxjZW50ZXJzPTMsbnN0YXJ0ID0gNTAsIGl0ZXIubWF4ID0gMTAwKQ0KdGFibGUod2luZXNbLDFdLCBrLm1lYW5zLjMkY2x1c3RlcikNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KGsubWVhbnMuMyRjbHVzdGVyLCB3aW5lc1ssMV0pLDMpKSkNCmBgYA0KIyMjIyBLTWVhbnMgNA0KYGBge3Iga21lYW5zNCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJlcXVpcmUoY2x1c3RlcikNCmsubWVhbnMuNCA8LSBrbWVhbnMoc2NhbGUod2luZXNbLC0xXSksY2VudGVycz00LG5zdGFydCA9IDUwLCBpdGVyLm1heCA9IDEwMCkNCnRhYmxlKHdpbmVzWywxXSwgay5tZWFucy40JGNsdXN0ZXIpDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleChrLm1lYW5zLjQkY2x1c3Rlciwgd2luZXNbLDFdKSwzKSkpDQpgYGANCiMjIyMgUEFNIDMNCmBgYHtyIHBhbTMsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpyZXF1aXJlKGNsdXN0ZXIpDQpQQU0uMyA8LSBwYW0od2luZXNbLC0xXSwgaz0zLA0KICAgIG1ldHJpYyA9ICJldWNsaWRlYW4iLCANCiAgICBuc3RhcnQgPSA1MCwNCiAgICBzdGFuZCA9IFRSVUUpDQp0YWJsZSh3aW5lc1ssMV0sIFBBTS4zJGNsdXN0ZXJpbmcpDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleChQQU0uMyRjbHVzdGVyaW5nLCB3aW5lc1ssMV0pLDMpKSkNCmBgYA0KIyMjIyBQQU0gNA0KYGBge3IgcGFtNCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJlcXVpcmUoY2x1c3RlcikNClBBTS40IDwtIHBhbSh3aW5lc1ssLTFdLCBrPTQsDQogICAgbWV0cmljID0gImV1Y2xpZGVhbiIsIA0KICAgIG5zdGFydCA9IDUwLA0KICAgIHN0YW5kID0gVFJVRSkNCnRhYmxlKHdpbmVzWywxXSwgUEFNLjQkY2x1c3RlcmluZykNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KFBBTS40JGNsdXN0ZXJpbmcsIHdpbmVzWywxXSksMykpKQ0KYGBgDQoNCiMgVmFyaWFibGUgU2VsZWN0aW9uIA0KDQojIyBQcmluY2lwaW8gZmlsb3NvZmljbyB7LnRhYnNldH0NCl9Ob3ZhY3VsYSBPY2NhbWk6IGZydXN0cmEgZml0IHBlciBwbHVyYSBxdW9kIHBvdGVzdCBmaWVyaSBwZXIgcGF1Y2lvcmFfIChJbCByYXNvaW8gZGkgT2NjYW06IMOoIGZ1dGlsZSBmYXJlIGNvbiBwacO5IG1lenppIGNpw7IgY2hlIHNpIHB1w7IgZmFyZSBjb24gbWVubykuIFRhbGUgcHJpbmNpcGlvIG1ldG9kb2xvZ2ljbyDDqCByaXRlbnV0byBhbGxhIGJhc2UgZGVsIHBlbnNpZXJvIHNjaWVudGlmaWNvIG1vZGVybm8uDQoNCiMjIyBIZWFkbG9uZw0KYGBge3IgaGVhZGxvbmcsIGVjaG89RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpzdWJzZXQuaGVhZGxvbmcgPC0gY2x1c3R2YXJzZWwod2luZXNbLC0xXSxHPTM6NCwgc2VhcmNoID0gJ2hlYWRsb25nJywgZGlyZWN0aW9uID0gJ2ZvcndhcmQnLCBwYXJhbGxlbCA9IFQsIHZlcmJvc2UgPSBGKQ0KDQpoZWFkbG9uZy5zZWxlY3RlZCA8LSBzdWJzZXQuaGVhZGxvbmckbW9kZWwNCnRhYmxlKHdpbmVzWywxXSxoZWFkbG9uZy5zZWxlY3RlZCRjbGFzc2lmaWNhdGlvbikNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KGhlYWRsb25nLnNlbGVjdGVkJGNsYXNzaWZpY2F0aW9uLCB3aW5lc1ssMV0pLDMpKSkNCmBgYA0KIyMjIEdyZWVkeQ0KYGBge3IgR3JlZWR5LCBlY2hvPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kc3Vic2V0LmdyZWVkeSA8LSBjbHVzdHZhcnNlbCh3aW5lc1ssLTFdLEc9Mzo0LCBzZWFyY2ggPSAnZ3JlZWR5JywgZGlyZWN0aW9uID0gJ2ZvcndhcmQnLCBwYXJhbGxlbCA9IFQsIHZlcmJvc2UgPSBGKQ0KDQpncmVlZHkuc2VsZWN0ZWQgPC0gc3Vic2V0LmdyZWVkeSRtb2RlbA0KdGFibGUod2luZXNbLDFdLGdyZWVkeS5zZWxlY3RlZCRjbGFzc2lmaWNhdGlvbikNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KGdyZWVkeS5zZWxlY3RlZCRjbGFzc2lmaWNhdGlvbiwgd2luZXNbLDFdKSwzKSkpDQpgYGANCiMjIyBWU0NDDQpgYGB7ciBWU0NDLCBlY2hvPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0KdnNjYy5tY2x1c3QgPC0gdnNjYyh3aW5lc1ssLTFdLCBHPTM6NCwgYXV0b21hdGUgPSAibWNsdXN0IiwgaW5pdGlhbCA9IE5VTEwsIHRyYWluID0gTlVMTCwgZm9yY2VyZWR1Y3Rpb24gPSBUKQ0KYGBgDQoNCmBgYHtyIFZTQ0NSRVNVTFQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQp0YWJsZSh3aW5lc1ssMV0sIHZzY2MubWNsdXN0JGluaXRpYWxydW4kY2xhc3NpZmljYXRpb24pICNDbHVzdGVyaW5nIHJlc3VsdHMgb24gZnVsbCBkYXRhIHNldA0KcHJpbnQocGFzdGUoIkFkanVzdGVkUmFuZEluZGV4OiIscm91bmQoYWRqdXN0ZWRSYW5kSW5kZXgodnNjYy5tY2x1c3QkaW5pdGlhbHJ1biRjbGFzc2lmaWNhdGlvbiwgd2luZXNbLDFdKSwzKSkpDQp0YWJsZSh3aW5lc1ssMV0sIHZzY2MubWNsdXN0JGJlc3Rtb2RlbCRjbGFzc2lmaWNhdGlvbikgI0NsdXN0ZXJpbmcgcmVzdWx0cyBvbiByZWR1Y2VkIGRhdGEgc2V0DQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleCh2c2NjLm1jbHVzdCRiZXN0bW9kZWwkY2xhc3NpZmljYXRpb24sIHdpbmVzWywxXSksMykpKQ0KYGBgDQojIyMgS01lYW5zU3BhcnNlIDMNCmBgYHtyIGttc3BhcnNlMywgZWNobz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmttLnBlcm0uMyA8LSBLTWVhbnNTcGFyc2VDbHVzdGVyLnBlcm11dGUod2luZXNbLC0xXSxLPTMsd2JvdW5kcz1zZXEoMyw3LGxlbj0xNSksbnBlcm1zPTUwLHNpbGVudCA9IFQpDQoNCmttLnNwYXJzZS4zIDwtIEtNZWFuc1NwYXJzZUNsdXN0ZXIod2luZXNbLC0xXSxLPTMsd2JvdW5kcz1rbS5wZXJtLjMkYmVzdHcsbnN0YXJ0ID0gNTAsIHNpbGVudCA9IFQsIG1heGl0ZXI9MTAwKQ0KdGFibGUod2luZXNbLDFdLGttLnNwYXJzZS4zW1sxXV0kQ3MpDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleChrbS5zcGFyc2UuM1tbMV1dJENzLCB3aW5lc1ssMV0pLDMpKSkNCmBgYA0KIyMjIEtNZWFuc1NwYXJzZSA0DQpgYGB7ciBrbXNwYXJzZTQsIGVjaG89RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQprbS5wZXJtLjQgPC0gS01lYW5zU3BhcnNlQ2x1c3Rlci5wZXJtdXRlKHdpbmVzWywtMV0sSz00LHdib3VuZHM9c2VxKDMsNyxsZW49MTUpLG5wZXJtcz01MCxzaWxlbnQgPSBUKQ0KDQprbS5zcGFyc2UuNCA8LSBLTWVhbnNTcGFyc2VDbHVzdGVyKHdpbmVzWywtMV0sSz00LHdib3VuZHM9a20ucGVybS40JGJlc3R3LG5zdGFydCA9IDUwLCBzaWxlbnQgPSBULCBtYXhpdGVyPTEwMCkNCnRhYmxlKHdpbmVzWywxXSxrbS5zcGFyc2UuNFtbMV1dJENzKQ0KcHJpbnQocGFzdGUoIkFkanVzdGVkUmFuZEluZGV4OiIscm91bmQoYWRqdXN0ZWRSYW5kSW5kZXgoa20uc3BhcnNlLjRbWzFdXSRDcywgd2luZXNbLDFdKSwzKSkpDQpgYGANCkxlIHZhcmlhYmlsaSBzZWxlemlvbmF0ZSBkYWxsYSBmdW56aW9uZSAqY2x1c3RlcnZhcnNlbCogc29ubzoNCg0KLSBmbGF2YW5vaWRzLCBPRF9kdywgcHJvbGluZSwgY29sb3VyLCB1cm9uaWMsIG1hbGljLCBjaGxvcmlkZQ0KDQpMZSB2YXJpYWJpbGkgc2VsZXppb25hdGUgZGFsbGEgZnVuemlvbmUgKnZzY2MqIHNvbm86DQoNCi0gZmxhdmFub2lkcywgT0RfZHcsIHByb2xpbmUsIGNvbG91ciwgYWxjb2hvbCwgT0RfZmwsIGh1ZSwgcGhlbm9scywgdXJvbmljLCB0YXJ0YXJpYw0KDQoNCiMjIE1vZGVsLUJhc2VkIHsudGFic2V0fQ0KDQojIyMgTWNsdXN0DQpgYGB7ciBOb3JtYWxfTWl4dHVyZSwgbWVzc2FnZT1GQUxTRSwgd2FyT0RfZHduaW5nPUZBTFNFfQ0Kc2VsZWN0ZWRXaW5lcyA8LSB3aW5lc1ssYygnZmxhdmFub2lkcycsJ09EX2R3JywncHJvbGluZScsJ2NvbG91cicsJ2FsY29ob2wnLCdPRF9mbCcsJ2h1ZScsJ3BoZW5vbHMnLCd1cm9uaWMnLCd0YXJ0YXJpYycpXQ0KDQptaXh0LnNlbGVjdGVkLndpbmVzIDwtIE1jbHVzdChzZWxlY3RlZFdpbmVzLEc9Mzo4LCB2ZXJib3NlID0gRikNCg0KdGFibGUod2luZXNbLDFdLG1peHQuc2VsZWN0ZWQud2luZXMkY2xhc3NpZmljYXRpb24pDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleCh3aW5lc1ssMV0sbWl4dC5zZWxlY3RlZC53aW5lcyRjbGFzc2lmaWNhdGlvbiksMykpKQ0KDQojc3VtbWFyeShtaXh0LnNlbGVjdGVkLndpbmVzKQ0KI3Bsb3QuTWNsdXN0KG1peHQuc2VsZWN0ZWQud2luZXMsIHdoYXQgPSAnY2xhc3NpZmljYXRpb24nLCBhZGRFbGxpcHNlcyA9IFRSVUUpDQoNCiNtaXh0LndpbmVzIDwtIE1jbHVzdCh3aW5lc1ssLTFdLEc9Mzo4LCB2ZXJib3NlID0gRikNCiN0YWJsZSh3aW5lc1ssMV0sbWl4dC53aW5lcyRjbGFzc2lmaWNhdGlvbikNCiNwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleCh3aW5lc1ssMV0sbWl4dC53aW5lcyRjbGFzc2lmaWNhdGlvbiksMykpKQ0KYGBgDQoNCiMjIyBDTm1peHQNCmBgYHtyIGNvbnRhbWluYXRlZDEsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpjbi53aW5lcy5taXh0IDwtIENObWl4dChzZWxlY3RlZFdpbmVzLCBHID0gMywgaW5pdGlhbGl6YXRpb24gPSAibWl4dCIsIHNlZWQgPSAxMjM0LCBwYXJhbGxlbCA9IEYsIHZlcmJvc2UgPSBGKQ0KdGFibGUod2luZXNbLDFdLGdldEJlc3RNb2RlbChjbi53aW5lcy5taXh0LCBjcml0ZXJpb24gPSAnSUNMJykkbW9kZWxzW1sxXV0kZ3JvdXApDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleCh3aW5lc1ssMV0sZ2V0QmVzdE1vZGVsKGNuLndpbmVzLm1peHQsIGNyaXRlcmlvbiA9ICdJQ0wnKSRtb2RlbHNbWzFdXSRncm91cCksMykpKQ0KYGBgDQoNCiMjIyBDTm1peHQga21lYW5zDQpgYGB7ciBjb250YW1pbmF0ZWQyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KY24ud2luZXMua21lYW5zIDwtIENObWl4dChzZWxlY3RlZFdpbmVzLCBHID0gMywgaW5pdGlhbGl6YXRpb24gPSAia21lYW5zIiwgc2VlZCA9IDEyMzQsIHBhcmFsbGVsID0gRiwgdmVyYm9zZSA9IEYpDQp0YWJsZSh3aW5lc1ssMV0sZ2V0QmVzdE1vZGVsKGNuLndpbmVzLmttZWFucywgY3JpdGVyaW9uID0gJ0lDTCcpJG1vZGVsc1tbMV1dJGdyb3VwKQ0KcHJpbnQocGFzdGUoIkFkanVzdGVkUmFuZEluZGV4OiIscm91bmQoYWRqdXN0ZWRSYW5kSW5kZXgod2luZXNbLDFdLGdldEJlc3RNb2RlbChjbi53aW5lcy5rbWVhbnMsIGNyaXRlcmlvbiA9ICdJQ0wnKSRtb2RlbHNbWzFdXSRncm91cCksMykpKQ0KYGBgDQojIyMgQ05taXh0IHJwb3N0DQpgYGB7ciBjb250YW1pbmF0ZWQzLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KY24ud2luZXMucnBvc3QgPC0gQ05taXh0KHNlbGVjdGVkV2luZXMsIEcgPSAzLCBpbml0aWFsaXphdGlvbiA9ICJyYW5kb20ucG9zdCIsIHNlZWQgPSAxMjM0LCBwYXJhbGxlbCA9IEYsIHZlcmJvc2UgPSBGKQ0KdGFibGUod2luZXNbLDFdLGdldEJlc3RNb2RlbChjbi53aW5lcy5ycG9zdCwgY3JpdGVyaW9uID0gJ0lDTCcpJG1vZGVsc1tbMV1dJGdyb3VwKQ0KcHJpbnQocGFzdGUoIkFkanVzdGVkUmFuZEluZGV4OiIscm91bmQoYWRqdXN0ZWRSYW5kSW5kZXgod2luZXNbLDFdLGdldEJlc3RNb2RlbChjbi53aW5lcy5ycG9zdCwgY3JpdGVyaW9uID0gJ0lDTCcpJG1vZGVsc1tbMV1dJGdyb3VwKSwzKSkpDQpgYGANCg0KIyMjIENObWl4dCByY2xhc3MNCmBgYHtyIGNvbnRhbWluYXRlZDQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpjbi53aW5lcy5yY2xhc3MgPC0gQ05taXh0KHNlbGVjdGVkV2luZXMsIEcgPSAzLCBpbml0aWFsaXphdGlvbiA9ICJyYW5kb20uY2xhcyIsIHNlZWQgPSAxMjM0LCBwYXJhbGxlbCA9IEYsIHZlcmJvc2UgPSBGKQ0KdGFibGUod2luZXNbLDFdLGdldEJlc3RNb2RlbChjbi53aW5lcy5yY2xhc3MsIGNyaXRlcmlvbiA9ICdJQ0wnKSRtb2RlbHNbWzFdXSRncm91cCkNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KHdpbmVzWywxXSxnZXRCZXN0TW9kZWwoY24ud2luZXMucmNsYXNzLCBjcml0ZXJpb24gPSAnSUNMJykkbW9kZWxzW1sxXV0kZ3JvdXApLDMpKSkNCmBgYA0KDQojIyBNdWx0aXZhcmlhdGUgdCBEaXN0cmlidXRpb24gey50YWJzZXR9DQoNCiMjIyB0ZWlnZW4ga21lYW5zDQpgYGB7ciB0ZWlnZW4sIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQp0ZWlnZW4ua21lYW5zIDwtIHRlaWdlbihzZWxlY3RlZFdpbmVzLCBHcz0zOjQsIGluaXQgPSAna21lYW5zJywgc2NhbGUgPSBULCB2ZXJib3NlID0gRikNCnRhYmxlKHdpbmVzWywxXSx0ZWlnZW4ua21lYW5zJGNsYXNzaWZpY2F0aW9uKQ0KcHJpbnQocGFzdGUoIkFkanVzdGVkUmFuZEluZGV4OiIscm91bmQoYWRqdXN0ZWRSYW5kSW5kZXgod2luZXNbLDFdLHRlaWdlbi5rbWVhbnMkY2xhc3NpZmljYXRpb24pLDMpKSkNCmBgYA0KIyMjIHRlaWdlbiBoYXJkDQpgYGB7ciB0ZWlnZW4gaGFyZCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnRlaWdlbi5oYXJkIDwtIHRlaWdlbihzZWxlY3RlZFdpbmVzLCBHcz0zOjQsIGluaXQgPSAnaGFyZCcsIHNjYWxlID0gVCwgdmVyYm9zZSA9IEYpDQp0YWJsZSh3aW5lc1ssMV0sdGVpZ2VuLmhhcmQkY2xhc3NpZmljYXRpb24pDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleCh3aW5lc1ssMV0sdGVpZ2VuLmhhcmQkY2xhc3NpZmljYXRpb24pLDMpKSkNCmBgYA0KDQojIyMgdGVpZ2VuIHNvZnQNCmBgYHtyIHRlaWdlbiBzb2Z0LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KdGVpZ2VuLnNvZnQgPC0gdGVpZ2VuKHNlbGVjdGVkV2luZXMsIEdzPTM6NCwgaW5pdCA9ICdzb2Z0Jywgc2NhbGUgPSBULCB2ZXJib3NlID0gRikNCnRhYmxlKHdpbmVzWywxXSx0ZWlnZW4uc29mdCRjbGFzc2lmaWNhdGlvbikNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KHdpbmVzWywxXSx0ZWlnZW4uc29mdCRjbGFzc2lmaWNhdGlvbiksMykpKQ0KYGBgDQoNCiMjIyB0LWNsYXNzaWZpZXIga21lYW5zDQpgYGB7ciB0ZWlnZW4ga21lYW5zLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KdGVpZ2VuLmNsYXNzaWZpZXIua21lYW5zIDwtIHRlaWdlbih0cmFpblssLTFdLCBHcz0zOjQsIGluaXQgPSAna21lYW5zJywgc2NhbGUgPSBULCBrbm93biA9IHRyYWluWywxXSwgdmVyYm9zZSA9IEYpDQp0ZWlnZW4ucHJlLmttZWFucyA9IHByZWRpY3QodGVpZ2VuLmNsYXNzaWZpZXIua21lYW5zLG5ld2RhdGE9dGVzdFssLTFdKQ0KDQp0YWJsZSh0ZXN0WywxXSx0ZWlnZW4ucHJlLmttZWFucyRjbGFzc2lmaWNhdGlvbikNCnByaW50KHBhc3RlKCJBZGp1c3RlZFJhbmRJbmRleDoiLHJvdW5kKGFkanVzdGVkUmFuZEluZGV4KHRlc3RbLDFdLHRlaWdlbi5wcmUua21lYW5zJGNsYXNzaWZpY2F0aW9uKSwzKSkpDQpgYGANCg0KIyMjIHQtY2xhc3NpZmllciB1bmlmb3JtDQpgYGB7ciB0ZWlnZW4gdW5pZm9ybSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnRlaWdlbi5jbGFzc2lmaWVyLnVuaWZvcm0gPC0gdGVpZ2VuKHRyYWluWywtMV0sIEdzPTM6NCwgaW5pdCA9ICd1bmlmb3JtJywgc2NhbGUgPSBULCBrbm93biA9IHRyYWluWywxXSwgdmVyYm9zZSA9IEYpDQp0ZWlnZW4ucHJlLnVuaWZvcm0gPSBwcmVkaWN0KHRlaWdlbi5jbGFzc2lmaWVyLnVuaWZvcm0sdGVzdFssLTFdKQ0KDQp0YWJsZSh0ZXN0WywxXSx0ZWlnZW4ucHJlLnVuaWZvcm0kY2xhc3NpZmljYXRpb24pDQpwcmludChwYXN0ZSgiQWRqdXN0ZWRSYW5kSW5kZXg6Iixyb3VuZChhZGp1c3RlZFJhbmRJbmRleCh0ZXN0WywxXSx0ZWlnZW4ucHJlLnVuaWZvcm0kY2xhc3NpZmljYXRpb24pLDMpKSkNCmBgYA0KDQojIFRlY25pY2hlIGRpIHJlZ29sYXJpenphemlvbmUNClBlciBsYSBub3N0cmEgYW5hbGlzaSBhYmJpYW1vIHZvbHV0byB2ZXJpZmljYXJlIGwnZWZmaWNpZW56YSBkaSB0cmUgbm90aSBtb2RlbGxpIGRpIHJlZ29sYXJpenphemlvbmUgYXR0cmF2ZXJzbyBpbCBwYWNjaGV0dG8gKmNhcmV0KjoNCg0KLSBSaWRnZQ0KLSBMYXNzbw0KLSBFbGFzdGljIE5ldA0KDQpgYGB7ciBsYW1iZGFUdW5pbmcsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsYW1iZGEgPC0gMTBec2VxKDAsIC0yLCBsZW5ndGggPSAyNTApDQpgYGANCg0KIyMgUmlkZ2UNCklsIG1vZGVsbG8gKlJpZGdlKiByaWR1Y2UgaSBjb2VmZmljaWVudGksIGluIG1vZG8gY2hlIGxlIHZhcmlhYmlsaSwgY29uIHVuIGNvbnRyaWJ1dG8gbWlub3JlIGFsIHJpc3VsdGF0bywgYWJiaWFubyBpIGxvcm8gY29lZmZpY2llbnRpIHZpY2luaSBhbGxvIHplcm8uIEludmVjZSBkaSBmb3J6YXJsaSBhIGVzc2VyZSBlc2F0dGFtZW50ZSB6ZXJvIChjb21lIG5lbCAqTGFzc28qKSwgbGkgcGVuYWxpenppYW1vIGNvbiB1biB0ZXJtaW5lIGNoaWFtYXRvICpub3JtYSBMMiogY29zdHJpbmdlbmRvbGkgY29zw6wgYSBlc3NlcmUgcGljY29saS4gSW4gcXVlc3RvIG1vZG8gZGltaW51aWFtbyBsYSBjb21wbGVzc2l0w6AgZGVsIG1vZGVsbG8gc2VuemEgZWxpbWluYXJlIG5lc3N1bmEgdmFyaWFiaWxlIGF0dHJhdmVyc28gdW5hIGNvc3RhbnRlIGNoaWFtYXRhIGxhbWJkYSAoJFxsYW1iZGEkKSBkaSBwZW5hbGl6emF6aW9uZToNCiQkDQpMX3tyaWRnZX0oXGhhdHtcYmV0YX0pID0gXHN1bV97aSA9IDF9XntufXsoeV9pIC0geF9pXGhhdHtcYmV0YX0pXjJ9ICsgXGxhbWJkYVxzdW1fe2sgPSAxfV57S317XGhhdHtcYmV0YX1fa14yfQ0KJCQNCg0KYGBge3IgY2FyZXRSaWRnZSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgQnVpbGQgdGhlIG1vZGVsDQpzZXQuc2VlZCgxMjMpDQpyaWRnZSA8LSBjYXJldDo6dHJhaW4oDQogIHggPSB0cmFpblssLTFdLA0KICB5ID0gZmFjdG9yKHRyYWluWywxXSksDQogIG1ldGhvZCA9ICJnbG1uZXQiLA0KICB0ckNvbnRyb2wgPSB0cmFpbkNvbnRyb2woImN2IiwgbnVtYmVyID0gMTAsIGNsYXNzUHJvYnMgPSBUUlVFLCBzdW1tYXJ5RnVuY3Rpb24gPSBtdWx0aUNsYXNzU3VtbWFyeSksDQogIHR1bmVHcmlkID0gZXhwYW5kLmdyaWQoYWxwaGEgPSAwLCBsYW1iZGE9bGFtYmRhKSwNCiAgbWV0cmljPSJBY2N1cmFjeSIpDQojIE1vZGVsIGNvZWZmaWNpZW50cw0KY29lZihyaWRnZSRmaW5hbE1vZGVsLCByaWRnZSRiZXN0VHVuZSRsYW1iZGEpDQojIE1ha2UgcHJlZGljdGlvbnMNCnByZWRpY3Rpb25zLnJpZGdlIDwtIHJpZGdlICU+JSBwcmVkaWN0KHRlc3QpDQojIE1vZGVsIHByZWRpY3Rpb24gcGVyZm9ybWFuY2UNCnRpYmJsZSgNCiAgdHJ1ZVZhbHVlID0gd2luZVRlc3ROYW1lcywNCiAgcHJlZGljdGVkVmFsdWUgPSBwcmVkaWN0aW9ucy5yaWRnZSkNCmBgYA0KSWwgcmlkZ2Ugw6ggY29tcG9zdG8gZGFsbGEgc29tbWEgZGVpIHJlc2lkdWkgcXVhZHJhdGkgcGnDuSB1bmEgcGVuYWxpdMOgLCBkZWZpbml0YSBkYWxsYSBsZXR0ZXJhIExhbWJkYSwgY2hlIMOoIG1vbHRpcGxpY2F0YSBwZXIgbGEgc29tbWEgZGVpIGNvZWZmaWNpZW50aSBxdWFkcmF0aSAkXGJldGEkLiBRdWFuZG8gJFxsYW1iZGEgPSAwJCwgaWwgdGVybWluZSBkaSBwZW5hbGl0w6Agbm9uIGhhIGFsY3VuIGVmZmV0dG8gZSBpbCByaWRnZSBwcm9kdXJyw6AgaSBjb2VmZmljaWVudGkgbWluaW1pIHF1YWRyYXRpIGNsYXNzaWNpLiBUdXR0YXZpYSwgcXVhbmRvICRcbGFtYmRhJCBhdW1lbnRhIGFsbOKAmWluZmluaXRvLCBs4oCZaW1wYXR0byBkZWxsYSBwZW5hbGl0w6AgYXVtZW50YSBlIGkgY29lZmZpY2llbnRpIHNpIGF2dmljaW5hbm8gYWxsbyB6ZXJvLiBJbCByaWRnZSDDqCBwYXJ0aWNvbGFybWVudGUgaW5kaWNhdG8gcXVhbmRvIHNpIGhhbm5vIG1vbHRpIGRhdGkgbXVsdGl2YXJpYXRpIGNvbiBudW1lcm8gZGkgZmVhdHVyZSBtYWdnaW9yZSBkZWwgbnVtZXJvIGRpIG9zc2VydmF6aW9uaS4gTG8gc3ZhbnRhZ2dpbywgcGVyw7IsIMOoIGNoZSBpbmNsdWRlcsOgIHR1dHRpIGxlIGZlYXR1cmUgbmVsIG1vZGVsbG8gZmluYWxlLCBhIGRpZmZlcmVuemEgZGVpIG1ldG9kaSBkaSBmZWF0dXJlIHNlbGVjdGlvbiwgY2hlIGdlbmVyYWxtZW50ZSBzZWxlemlvbmVyYW5ubyB1biBpbnNpZW1lIHJpZG90dG8gZGkgdmFyaWFiaWxpIHRyYSBxdWVsbGUgZGlzcG9uaWJpbGkuDQpgYGB7ciByaWRnZVJlc3VsdH0NCmNhcmV0Ojpjb25mdXNpb25NYXRyaXgocHJlZGljdGlvbnMucmlkZ2UsIHRlc3Qkd2luZSkNCmBgYA0KDQojIyBMYXNzbw0KSWwgKkxlYXN0IEFic29sdXRlIFNocmlua2FnZSBhbmQgU2VsZWN0aW9uIE9wZXJhdG9yKiAoTEFTU08pIHJpZHVjZSBpIGNvZWZmaWNpZW50aSB2ZXJzbyBsbyB6ZXJvIHBlbmFsaXp6YW5kbyBpbCBtb2RlbGxvIGNvbiB1biB0ZXJtaW5lIGRpIHBlbmFsaXTDoCBjaGlhbWF0byAqbm9ybWEgTDEqLCBjaGUgw6ggbGEgc29tbWEgZGVpIGNvZWZmaWNpZW50aSBpbiB2YWxvcmUgYXNzb2x1dG86DQokJA0KTF97bGFzc299KFxoYXR7XGJldGF9KSA9IFxzdW1fe2kgPSAxfV57bn17KHlfaSAtIHhfaVxoYXR7XGJldGF9KV4yfSArIFxsYW1iZGFcc3VtX3trID0gMX1ee0t9e3xcaGF0e1xiZXRhfV9rfH0NCiQkDQpgYGB7ciBjYXJldExhc3NvLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KIyBCdWlsZCB0aGUgbW9kZWwNCnNldC5zZWVkKDEyMykNCmxhc3NvIDwtIGNhcmV0Ojp0cmFpbigNCiAgeCA9IHRyYWluWywtMV0sDQogIHkgPSBmYWN0b3IodHJhaW5bLDFdKSwNCiAgbWV0aG9kID0gImdsbW5ldCIsDQogIHRyQ29udHJvbCA9IHRyYWluQ29udHJvbCgiY3YiLCBudW1iZXIgPSAxMCwgY2xhc3NQcm9icyA9IFRSVUUsIHN1bW1hcnlGdW5jdGlvbiA9IG11bHRpQ2xhc3NTdW1tYXJ5KSwNCiAgdHVuZUdyaWQgPSBleHBhbmQuZ3JpZChhbHBoYSA9IDEsIGxhbWJkYT1sYW1iZGEpLA0KICBtZXRyaWM9IkFjY3VyYWN5IikNCiMgTW9kZWwgY29lZmZpY2llbnRzDQpjb2VmKGxhc3NvJGZpbmFsTW9kZWwsIGxhc3NvJGJlc3RUdW5lJGxhbWJkYSkNCiMgTWFrZSBwcmVkaWN0aW9ucw0KcHJlZGljdGlvbnMubGFzc28gPC0gbGFzc28gJT4lIHByZWRpY3QodGVzdCkNCiMgTW9kZWwgcHJlZGljdGlvbiBwZXJmb3JtYW5jZQ0KdGliYmxlKA0KICB0cnVlVmFsdWUgPSB3aW5lVGVzdE5hbWVzLA0KICBwcmVkaWN0ZWRWYWx1ZSA9IHByZWRpY3Rpb25zLmxhc3NvKQ0KYGBgDQpJbiBxdWVzdG8gY2FzbyBsYSBwZW5hbGl0w6AgaGEgbOKAmWVmZmV0dG8gZGkgZm9yemFyZSBhbGN1bmUgZGVsbGUgc3RpbWUgZGVpIGNvZWZmaWNpZW50aSwgY29uIHVuIGNvbnRyaWJ1dG8gbWlub3JlIGFsIG1vZGVsbG8sIGEgZXNzZXJlIGVzYXR0YW1lbnRlIHVndWFsZSBhIHplcm8uIElsIGxhc3NvLCBxdWluZGksIHB1w7IgYW5jaGUgZXNzZXJlIHZpc3RvIGNvbWUgdW7igJlhbHRlcm5hdGl2YSBhaSBtZXRvZGkgZGkgZmVhdHVyZSBzZWxlY3Rpb24gcGVyIGVzZWd1aXJlIGxhIHNlbGV6aW9uZSBkZWxsZSB2YXJpYWJpbGkgYWwgZmluZSBkaSByaWR1cnJlIGxhIGNvbXBsZXNzaXTDoCBkZWwgbW9kZWxsby4NCg0KQ29tZSBuZWwgcmlkZ2UsIMOoIGZvbmRhbWVudGFsZSBzZWxlemlvbmFyZSB1biBidW9uIHZhbG9yZSBkaSAkXGxhbWJkYSQuDQoNClF1YW5kbyBsYW1iZGEgw6ggcGljY29sbywgaWwgcmlzdWx0YXRvIMOoIG1vbHRvIHZpY2lubyBhbGxhIHN0aW1hIGRlaSBtaW5pbWkgcXVhZHJhdGkuIEFsbOKAmWF1bWVudGFyZSBkaSBsYW1iZGEsIHNpIHZlcmlmaWNhIHVuYSBjb250cmF6aW9uZSBpbiBtb2RvIGRhIHBvdGVyIGVsaW1pbmFyZSBsZSB2YXJpYWJpbGkgY2hlIHNvbm8gYSB6ZXJvLg0KYGBge3IgbGFzc29SZXN1bHQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpjYXJldDo6Y29uZnVzaW9uTWF0cml4KHByZWRpY3Rpb25zLmxhc3NvLCB0ZXN0JHdpbmUpDQpgYGANCg0KIyMgRWxhc3RpYyBOZXQNCkVsYXN0aWMgTmV0IGNvbWJpbmEgbGUgcHJvcHJpZXTDoCBkaSBSaWRnZSBlIExhc3NvIHBlbmFsaXp6YW5kbyBpbCBtb2RlbGxvIHVzYW5kbyBzaWEgbGEgbm9ybWEgTDIgY2hlIGxhIG5vcm1hIEwxOg0KJCQNCkxfe0VsYXN0aWNOZXR9KFxoYXR7XGJldGF9KSA9IFxmcmFje1xzdW1fe2kgPSAxfV57bn17KHlfaSAtIHhfaVxoYXR7XGJldGF9KV4yfX17Mm59ICsgXGxhbWJkYShcZnJhY3sxLVxhbHBoYX17Mn1cc3VtX3trID0gMX1ee0t9e1xoYXR7XGJldGF9X2teMn0gKyBcYWxwaGFcc3VtX3trID0gMX1ee0t9e3xcaGF0e1xiZXRhfV9rfXwpDQokJA0KYGBge3IgY2FyZXRFbGFzdGljLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kc2V0LnNlZWQoMTIzKQ0KZWxhc3RpYyA8LSBjYXJldDo6dHJhaW4oDQogIHggPSB0cmFpblssLTFdLA0KICB5ID0gZmFjdG9yKHRyYWluWywxXSksDQogIG1ldGhvZCA9ICJnbG1uZXQiLA0KICB0ckNvbnRyb2wgPSB0cmFpbkNvbnRyb2woImN2IiwgbnVtYmVyID0gMTAsIGNsYXNzUHJvYnMgPSBUUlVFLCBzdW1tYXJ5RnVuY3Rpb24gPSBtdWx0aUNsYXNzU3VtbWFyeSksDQogIG1ldHJpYz0iQWNjdXJhY3kiKQ0KIyBNb2RlbCBjb2VmZmljaWVudHMNCmNvZWYoZWxhc3RpYyRmaW5hbE1vZGVsLCBlbGFzdGljJGJlc3RUdW5lJGxhbWJkYSkNCiMgTWFrZSBwcmVkaWN0aW9ucw0KcHJlZGljdGlvbnMuZW5ldCA8LSBlbGFzdGljICU+JSBwcmVkaWN0KHRlc3QpDQojIE1vZGVsIHByZWRpY3Rpb24gcGVyZm9ybWFuY2UNCnRpYmJsZSgNCiAgdHJ1ZVZhbHVlID0gd2luZVRlc3ROYW1lcywNCiAgcHJlZGljdGVkVmFsdWUgPSBwcmVkaWN0aW9ucy5lbmV0KQ0KYGBgDQpPbHRyZSBhIGltcG9zdGFyZSBlIHNjZWdsaWVyZSB1biB2YWxvcmUgbGFtYmRhLCBs4oCZKmVsYXN0aWMgbmV0KiBjaSBjb25zZW50ZSBhbmNoZSBkaSBvdHRpbWl6emFyZSBpbCBwYXJhbWV0cm8gYWxmYSBkb3ZlICRcYWxwaGEgPSAwJCBjb3JyaXNwb25kZSBhICpyaWRnZSogZSAkXGFscGhhID0gMSQgYWwgKmxhc3NvKi4NCg0KUGVydGFudG8gcG9zc2lhbW8gc2NlZ2xpZXJlIHVuIHZhbG9yZSAkXGFscGhhJCBjb21wcmVzbyB0cmEgMCBlIDEgcGVyIG90dGltaXp6YXJlIGzigJllbGFzdGljIG5ldC4gU2UgdGFsZSB2YWxvcmUgw6ggaW5jbHVzbyBpbiBxdWVzdG8gaW50ZXJ2YWxsbywgc2kgYXZyw6AgdW5hIHJpZHV6aW9uZSBjb24gYWxjdW5pIHBvcnRhdGkgYSAkMCQuDQoNCmBgYHtyIGVuZXRSZXN1bHQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpjYXJldDo6Y29uZnVzaW9uTWF0cml4KHByZWRpY3Rpb25zLmVuZXQsIHRlc3Qkd2luZSkNCmBgYA0KDQojIyBTdW1tYXJ5IGRlbGxlIHByZXN0YXppb25pDQpgYGB7ciBjb25mTWF0cml4LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbW9kZWxzIDwtIGxpc3QocmlkZ2UgPSBjYXJldDo6Y29uZnVzaW9uTWF0cml4KHByZWRpY3Rpb25zLnJpZGdlLCB0ZXN0JHdpbmUpJG92ZXJhbGxbMV0sIA0KICAgICAgICAgICAgICAgbGFzc28gPSBjYXJldDo6Y29uZnVzaW9uTWF0cml4KHByZWRpY3Rpb25zLmxhc3NvLHRlc3Qkd2luZSkkb3ZlcmFsbFsxXSwNCiAgICAgICAgICAgICAgIGVsYXN0aWMgPSBjYXJldDo6Y29uZnVzaW9uTWF0cml4KHByZWRpY3Rpb25zLmVuZXQsdGVzdCR3aW5lKSRvdmVyYWxsWzFdKQ0KZm9yKGogaW4gMTpsZW5ndGgobW9kZWxzKSl7DQogIHByaW50KHBhc3RlKCJBY2N1cmFjeSBvZiIsbmFtZXMobW9kZWxzW2pdKSwiPSIscm91bmQobW9kZWxzW1tqXV0sMykpKQ0KfQ0KYGBgDQojIFBhcnNpbW9uaW91cyBHYXVzc2lhbiBNaXh0dXJlIE1vZGVscw0KYGBge3IgYWR2YW5jZWQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpkaXN0IDwtIGRpc3Qod2luZXNbLC0xXSwgbWV0aG9kID0gIm1hbmhhdHRhbiIpDQpkZW5kbyA8LSBoY2x1c3QoZGlzdCwgbWV0aG9kID0gIndhcmQuRCIpDQpjdXN0b20gPC0gbGlzdCgpDQpmb3IgKGkgaW4gMzo0KXsNCiAgY3VzdG9tW1tpXV0gPSBjdXRyZWUoZGVuZG8sIGs9aSkNCn0NCg0KbW9kZWxzX3RvX3J1biA8LSBjKCJVQ1UiLCAiVUNDIiwgIkNVVSIsICJDVUMiLCAiQ0NVIiwgIkNDQyIpDQoNCiMgSWwgcmlsYXNzYW1lbnRvIGRhdG8gZGEgKHAtcSleMiA+IHArcSDDqCB2ZXJpZmljYXRvIHBlciAgMSA8PSBxIDw9IDIwDQpwZy5rbWVhbnMgPC0gcGdtbUVNKHNjYWxlKHdpbmVzWywtMV0pLCByRz0zOjQsIHJxPTE6NywgaWNsPVQsIHpzdGFydD0yLHNlZWQ9MTIzNCwgbW9kZWxTdWJzZXQ9bW9kZWxzX3RvX3J1bikNCnRhYmxlKHdpbmVzWywxXSxwZy5rbWVhbnMkbWFwKQ0KYWRqdXN0ZWRSYW5kSW5kZXgod2luZXNbLDFdLHBnLmttZWFucyRtYXApDQoNCnBnLm1hbmhhdHRhbiA8LSBwZ21tRU0oc2NhbGUod2luZXNbLC0xXSksIHJHPTM6NCwgcnE9MTo3LCBpY2w9VCwgenN0YXJ0ID0gMywgc2VlZCA9IDEyMzQsIHpsaXN0ID0gY3VzdG9tLCBtb2RlbFN1YnNldD1tb2RlbHNfdG9fcnVuKQ0KdGFibGUod2luZXNbLDFdLHBnLm1hbmhhdHRhbiRtYXApDQphZGp1c3RlZFJhbmRJbmRleCh3aW5lc1ssMV0scGcubWFuaGF0dGFuJG1hcCkNCmBgYA0KDQojIENvbmZyb250byBmaW5hbGUNCmBgYHtyIHN1bW1hcnlPZkFsbE1vZGVscywgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgcm93cy5wcmludD0xN30NCnN1bW1hcnlPZk1vZGVscyA8LSBmdW5jdGlvbihkZiwgbW9kZWxzKXsNCiAgbk1vZGVsIDwtIGxlbmd0aChtb2RlbHMpDQogIGFyaSA8LSBOVUxMDQogIGJpYyA8LSBOVUxMDQogIGljbCA8LSBOVUxMDQogIEcgPC0gTlVMTA0KICBmb3IoaSBpbiAxOm5Nb2RlbCl7DQogICAgaWYgKGNsYXNzKG1vZGVsc1tbaV1dKT09J2ttZWFucycpew0KICAgICAgYXJpW2ldIDwtIGFkanVzdGVkUmFuZEluZGV4KGRmLG1vZGVsc1tbaV1dJGNsdXN0ZXIpDQogICAgICBiaWNbaV0gPC0gTkENCiAgICAgIGljbFtpXSA8LSBOQQ0KICAgICAgR1tpXSA8LSBsZW5ndGgodW5pcXVlKG1vZGVsc1tbaV1dJGNsdXN0ZXIpKQ0KICAgIH0gZWxzZSBpZiAoY2xhc3MobW9kZWxzW1tpXV0pPT0ncGFtJykgew0KICAgICAgYXJpW2ldIDwtIGFkanVzdGVkUmFuZEluZGV4KGRmLG1vZGVsc1tbaV1dJGNsdXN0ZXJpbmcpDQogICAgICBiaWNbaV0gPC0gTkENCiAgICAgIGljbFtpXSA8LSBOQQ0KICAgICAgR1tpXSA8LSBsZW5ndGgobW9kZWxzW1tpXV0kaWQubWVkKQ0KICAgIH0gZWxzZSBpZiAoY2xhc3MobW9kZWxzW1tpXV0pPT0nS01lYW5zU3BhcnNlQ2x1c3Rlcicpew0KICAgICAgbW9kZWxOYW1lIDwtIG1vZGVsc1tbaV1dDQogICAgICBhcmlbaV0gPC0gYWRqdXN0ZWRSYW5kSW5kZXgoZGYsbW9kZWxOYW1lW1sxXV0kQ3MpDQogICAgICBiaWNbaV0gPC0gTkENCiAgICAgIGljbFtpXSA8LSBOQQ0KICAgICAgR1tpXSA8LSBtb2RlbE5hbWVbWzFdXSR3Ym91bmQNCiAgICB9IGVsc2UgaWYgKGNsYXNzKG1vZGVsc1tbaV1dKT09J01jbHVzdCcpew0KICAgICAgYXJpW2ldIDwtIGFkanVzdGVkUmFuZEluZGV4KGRmLG1vZGVsc1tbaV1dJGNsYXNzaWZpY2F0aW9uKQ0KICAgICAgYmljW2ldIDwtIG1vZGVsc1tbaV1dJGJpYw0KICAgICAgaWNsW2ldIDwtIG1vZGVsc1tbaV1dJGljbA0KICAgICAgR1tpXSA8LSBtb2RlbHNbW2ldXSRHDQogICAgfSBlbHNlIGlmIChjbGFzcyhtb2RlbHNbW2ldXSk9PSdDb250YW1pbmF0ZWRNaXh0Jyl7DQogICAgICBiZXN0TW9kZWwgPC0gZ2V0QmVzdE1vZGVsKG1vZGVsc1tbaV1dLCBjcml0ZXJpb24gPSAnSUNMJykkbW9kZWxzW1sxXV0NCiAgICAgIGFyaVtpXSA8LSBhZGp1c3RlZFJhbmRJbmRleChkZixiZXN0TW9kZWwkZ3JvdXApDQogICAgICBiaWNbaV0gPC0gYmVzdE1vZGVsJElDJEJJQw0KICAgICAgaWNsW2ldIDwtIGJlc3RNb2RlbCRJQyRJQ0wNCiAgICAgIEdbaV0gPC0gYmVzdE1vZGVsJEcNCiAgICB9IGVsc2UgaWYgKGNsYXNzKG1vZGVsc1tbaV1dKT09J3RlaWdlbicpew0KICAgICAgYXJpW2ldIDwtIGFkanVzdGVkUmFuZEluZGV4KGRmLG1vZGVsc1tbaV1dJGljbHJlc3VsdHMkY2xhc3NpZmljYXRpb24pDQogICAgICBiaWNbaV0gPC0gbW9kZWxzW1tpXV0kYmljDQogICAgICBpY2xbaV0gPC0gbW9kZWxzW1tpXV0kaWNscmVzdWx0cyRpY2wNCiAgICAgIEdbaV0gPC0gbW9kZWxzW1tpXV0kRw0KICAgIH0gZWxzZSBpZiAoY2xhc3MobW9kZWxzW1tpXV0pPT0ncGdtbScpew0KICAgICAgYXJpW2ldIDwtIGFkanVzdGVkUmFuZEluZGV4KGRmLG1vZGVsc1tbaV1dJG1hcCkNCiAgICAgIEdbaV0gPC0gbW9kZWxzW1tpXV0kZw0KICAgICAgaWZlbHNlKGlzLm51bGwobW9kZWxzW1tpXV0kYmljWzFdKT09VFJVRSwNCiAgICAgICAgICAgICBiaWNbaV0gPC0gTkEsDQogICAgICAgICAgICAgYmljW2ldIDwtIGFzLmRvdWJsZShtb2RlbHNbW2ldXSRiaWNbMV0pKQ0KICAgICAgaWZlbHNlKGlzLm51bGwobW9kZWxzW1tpXV0kaWNsWzFdKT09VFJVRSwNCiAgICAgICAgICAgICBpY2xbaV0gPC0gTkEsDQogICAgICAgICAgICAgaWNsW2ldIDwtIGFzLmRvdWJsZShtb2RlbHNbW2ldXSRpY2xbMV0pKQ0KICAgIH0gZWxzZSBpZiAoY2xhc3MobW9kZWxzW1tpXV0pPT0ndGttZWFucycpew0KICAgICAgYXJpW2ldIDwtIGFkanVzdGVkUmFuZEluZGV4KGRmLG1vZGVsc1tbaV1dJGNsdXN0ZXIpDQogICAgICBHW2ldIDwtIG1vZGVsc1tbaV1dJGsNCiAgICAgIGJpY1tpXSA8LSBOQQ0KICAgICAgaWNsW2ldIDwtIE5BDQogICAgfQ0KICB9DQogIG91dHB1dERGIDwtIGRhdGEuZnJhbWUoJ0FkanVzdGVkUmFuZEluZGV4JyA9IGFyaSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAnQklDJyA9IGJpYywNCiAgICAgICAgICAgICAgICAgICAgICAgICAnSUNMJyA9IGljbCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAnRycgPSBhcy5pbnRlZ2VyKEcpLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHJvdy5uYW1lcyA9IGMoJ0tNZWFuczMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ0tNZWFuczQnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1BBTTMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1BBTTQnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1ZTLUdyZWVkeScsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnVlMtSGVhZGxvbmcnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1ZTQ0MnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ0tNZWFuc1NwYXJzZTMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ0tNZWFuc1NwYXJzZTQnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1ZTLU1WTicsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnVlMtQ29udGFtaW5hdGVkIE1peHQnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1ZTLVZTLUNvbnRhbWluYXRlZCBLTWVhbnMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1ZTLUNvbnRhbWluYXRlZCBSUG9zdCcsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnVlMtQ29udGFtaW5hdGVkIFJDbGFzcycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnVlMtVEVpZ2VuIEtNZWFucycsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnVlMtVEVpZ2VuIEhhcmQnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1ZTLVRFaWdlbiBTb2Z0JyksDQogICAgICAgICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpDQogIHJldHVybihvdXRwdXRERikNCn0NCnRlc3QgPC0gc3VtbWFyeU9mTW9kZWxzKHdpbmVzWywxXSxsaXN0KA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrLm1lYW5zLjMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGsubWVhbnMuNCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUEFNLjMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIFBBTS40LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJzZXQuZ3JlZWR5JG1vZGVsLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJzZXQuaGVhZGxvbmckbW9kZWwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZzY2MubWNsdXN0JGJlc3Rtb2RlbCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAga20uc3BhcnNlLjMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGttLnNwYXJzZS40LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaXh0LnNlbGVjdGVkLndpbmVzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbi53aW5lcy5taXh0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbi53aW5lcy5rbWVhbnMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNuLndpbmVzLnJwb3N0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbi53aW5lcy5yY2xhc3MsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlaWdlbi5rbWVhbnMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlaWdlbi5oYXJkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZWlnZW4uc29mdCkpDQpwcmludCh0ZXN0LCBuPTIwKQ0KYGBgDQojIE1vZGVsbGF6aW9uZSAzRA0KYGBge3IgM2RzLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KIyBiZXN0IDMgZm9yIHNlcGFyYXRpb24NCnBsb3QzZCh3aW5lcyRmbGF2YW5vaWRzLCB3aW5lcyRPRF9kdywgd2luZXMkcHJvbGluZSwgdHlwZT0ncycsIHNpemU9MiwgY29sPWFzLm51bWVyaWMod2luZXMkd2luZSkpDQojIHdvcnN0IDMgZm9yIHNlcGFyYXRpb24NCnBsb3QzZCh3aW5lcyRtZXRoYW5vbCwgd2luZXMkcG90YXNzaXVtLCB3aW5lcyRwSCwgdHlwZT0ncycsIHNpemU9MiwgY29sPWFzLm51bWVyaWMod2luZXMkd2luZSkpDQpgYGANCg0K