Introduzione

In questo notebook R iniziamo a studiare la classificazione statistica su dataset generati e un dataset reale (pre-caricato). Il notebook si concentra sull’utilizzo dell’algoritmo \(k\)-NN, ma allo stesso tempo discute concetti generali come errori di training/test, cross-validation, matrici di confusione e curve ROC, che saranno poi utili anche per altri metodi di classificazione.

Iniziamo caricando le librerie necessarie – usare install.packages("nome pacchetto") per installare quelle non già presenti.

# ggplot2 per grafiche migliorate
library(ggplot2)

# libreria con comandi per classificazione di base
library(class)

# libreria con comandi avanzati per classificazione e regressione
library(caret)


# libreria per il dataset medicale
library(MedDataSets)

Dataset Generati

In questa sezione creiamo un dataset fittizio, usando comandi di campionamento come rnorm ma anche rbinom, rpois per avere caratteristiche discrete.


# Creazione di un dataset fittizio 
# impostare il seed per riproducibilità
#set.seed(42)

# numero di osservazioni
n_neg <- 200 # numero di negativi
n_pos <- 100 # numero di positivi

# n è la taglia del campione
n <- n_neg+n_pos

# generiamo le features
x1 <- rnorm(n_neg, mean=-1, sd=1)
x2 <- rnorm(n_neg, mean=-1, sd=1)
x3 <- rnorm(n_pos, mean=1, sd=1)
x4 <- rnorm(n_pos, mean=1, sd=1)

# generiamo le classi
classe <- factor(c(rep(-1, n_neg), rep(1, n_pos) ))

data_generated <- data.frame(c(x1, x3), c(x2, x4), classe)

colnames(data_generated) <- c("feature_1", "feature_2", "label")

# Visualizzazione del dataset
ggplot(data_generated, aes(x =feature_1, y = feature_2, color = label)) +
  geom_point() +
  labs(title = "Dataset Fittizio", x = "feature 1", y = "feature 2")

Come discusso a lezione, procediamo inizialmente dividendo il dataset in training e test sets. Scegliamo una percentuale significativa per il training (\(80\%\) ad esempio, ma comunque anche da valutare in base a quanti dati disponiamo).

Usiamo la funzione sample() per campionare in modo casuale, per evitare di prendere ad esempio dati da una stessa classe se già ordinati per classe (come nel nostro caso).

Visualizziamo i dati distinguendo train e test usando una geometria diversa.


# creiamo un vettore che indica se il dato è test o train

is_train <- rep(FALSE, n)
is_train[train_index] <- TRUE

# Visualizzazione del dataset distinguendo tra train e test
ggplot(train_generated, aes(x =feature_1, y = feature_2, color = label) )+ #shape=is_train )) +
  geom_point() +
  labs(title = "Dataset Fittizio", x = "feature 1", y = "feature 2") # shape="è training point?")

Ora alleniamo il modello \(k\)-NN sui dati del training set e lo valutiamo sui dati del test set generato. Usiamo la funzione knn() dal pacchetto class. In alternativa si può usare knn3() dal pacchetto caret.

# Allenamento del modello KNN

# Scegliamo il valore di k che preferiamo, meglio dispari per classificazione binaria altrimenti deve risolvere le parità a caso
k <- 1

punto_test <- c(0,0)
# salviamo le previsioni
knn_pred <- knn(train_generated[, 1:2], test_generated[, 1:2], train_generated$label, k = k)

table(knn_pred)
knn_pred
-1  1 
43 17 

Visualizziamo le etichette attribuite al test set (usiamo delle forme) confrontandole con quelle originali (usiamo i colori originali).


test_generated$knn <- knn_pred

ggplot(test_generated, aes(x =feature_1, y = feature_2, color = label, shape=knn )) +
  geom_point() +
  labs(title = "Dataset Fittizio", x = "feature 1", y = "feature 2", shape="knn")

Indicatori di performance

Una metrica generale è l’accuratezza sul test set, calcoliamola confrontando le previsioni con le vere etichette.

# accuratezza

accuracy <- mean(test_generated$knn== test_generated$label )

round(accuracy, 6)
[1] 0.9

Il test error è quindi 0.23.

Trattandosi di una classificazione binaria valutiamo la performance in una matrice di confusione (questo si può fare anche per classificazioni non binarie, ma in tal caso la taglia della matrice aumenta ed è meno riassuntiva).

# Matrice di confusione
conf_matrix <- table("Reale" = test_generated$label, "Classificato" = test_generated$knn)
print(conf_matrix)
     Classificato
Reale -1  1
   -1 39  2
   1   4 15

Convenientemente abbiamo chiamato le classi \(-1\) per i Negativi, \(1\) per i Positivi, ma possiamo anche cambiare i nomi alle righe e colonne della tabella.

colnames(conf_matrix) <- c("CN", "CP")
row.names(conf_matrix) <-  c("N", "P")

print(conf_matrix)
     Classificato
Reale CN CP
    N 39  2
    P  4 15

Estraiamo a mano le componenti della matrice e calcoliamo le metriche principali


reali <- test_generated$label
previsti <- test_generated$knn

# positivi reali
P <- sum( reali == 1)
# negativi reali
N <- sum( reali == -1)

# prevalenza
prevalence <- P/(P+N) 

# veri negativi
TN <- sum( reali == -1 & previsti == -1)
# veri positivi
TP <-sum( reali == 1 & previsti == 1)
# falsi negativi, mancato allarme
FN <- sum( reali == 1 &  previsti == -1)
# falsi positivi, falso allarme
FP <- sum( reali == -1 & previsti == 1)

# precisione (probabilità che sia vero positivo sapendo che il test indica positivo)
precision <- TP/(TP+FP)

# TPR, recall/richiamo, sensibilità (probabilità che il test indichi positivo sapendo che il soggetto è realmente positivo)
TPR <- TP/P
recall <- TPR

# TNR, specificità  (probabilità che il test indichi negativo sapendo che è realmente negativo)

TNR <- TN/N

# FPR, errore di tipo I (probabilità che il test indichi positivo sapendo che è realmente negativo)

FPR <- FP/N

# FNR, errore di tipo II (probabilità che il test indichi negativo sapendo che è realmente positivo)

FNR <- FN/P

Possiamo rappresentare il risultato del classificatore sul test set come un punto nel grafico FPR/TPR. Più il punto è vicino all’angolo in alto a sinistra \((0,1)\) migliore è la performance del classificatore (sul test set).


ROC <- data.frame( "FPR" = FPR, "TPR" = TPR, "k" = k)

ggplot(data = ROC, aes(x=FPR, y=TPR, colour = k))+geom_point()+xlim(0,1)+ylim(0,1)+geom_abline(slope=1, intercept = 0, col="red")


# togliamo il commento per aggiungere la bisettrice per indicare la performance dei classificatori puramente casuali

Ci sono altri indicatori che cercano di riassumere la performance di un classificatore binario (o meglio, di un test) con un singolo numero. Uno è anche l’accuratezza, ma potrebbe essere influenzata dalla bassa prevalenza. Indicatori migliori in questo senso sono la misura \(F_1\) (\(F_1\) score) che media tra precisione e richiamo. Idealmente vorremmo sia recisione che recall vicine ad \(1\), quindi mediandole avremo un indicatore che più si avvicina ad \(1\) meglio è. La media che viene utilizzata è la media armonica: \[ F_1 =\left( \frac{precision^{-1} + recall^{-1}}{2} \right)^{-1} \]

F1 <- 2/(1/precision+1/recall)

La misura \(F_1\) in questo caso vale 0.1363636. Più in generale è possibile introdurre un peso relativo tra precisione e richiamo con la misura \(F_\beta\), dove \(\beta>0\) indica un fattore che pesa il richiamo \(\beta\) volte più importante della precisione.

\[ F_\beta =\left( \frac{\beta^{-1} \cdot precision^{-1} + \beta \cdot recall^{-1}}{\beta^{-1}+\beta} \right)^{-1} \] Ad esempio, si usa \(F_2\) che pone più peso al richiamo, oppure il caso \(F_{1/2}\) che pone più peso alla precisione.

F_beta <- function(precision, recall, beta=1){
  1/((1/(beta *precision) + beta/recall)/(1/beta+beta))
}

F1 <- round(F_beta(precision, recall), 2)
F2 <- round(F_beta(precision, recall, beta=2), 2)
F0.5 <- round(F_beta(precision, recall, beta=0.5), 2)

Troviamo in questo caso \(F_1=\) 0.1363636, \(F_2=\) 0.81, \(F_{1/2}=\) 0.86 (in questo caso precision = 0.88, recall=0.79).

Un ulteriore indicatore è il coefficiente \(\phi\), noto anche come Matthews correlation coefficient (MCC) in machine learning. Per definizione, si tratta di calcolare il coefficiente di correlazione di Pearson tra le variabili aleatorie (a valori in \(-1\), \(1\)) con densità discreta di probabilità congiunta data dalle entrate della matrice di confusione divise per la taglia del test set. Si trova la formula \[ \varphi = \frac{ TP \cdot TN - FN \cdot FN}{\sqrt{ P \cdot N \cdot (TP+FP) \cdot (TN+FN)}}. \]

Trattandosi di un coefficiente di correlazione, valori estremi sono vicini a \(1\) o \(-1\) (e indicano una forte associazone tra previsione ed etichette reali), mentre valori vicini a \(0\) indicano una debole associazione. Tipicamente si osservano valori positivi comunque (un classificatore con valore \(\phi\) negativo indica che sbaglia sistematicamente).


phi = (TP*TN - FP*FN)/(sqrt( P*N* (TP+FP)*(TN+FN) ) )

In questo caso il coefficiente vale \(\varphi =\) 0.76.

Non è necessario scrivere il codice per calcolarsi tutti questi indicatori ogni volta ex novo: possiamo usare la funzione confusionMatrix() dalla libreria caret.

#carichiamo la libreria caret se non già fatto sopra
library(caret)

conf_matrix <- confusionMatrix(test_generated$knn, test_generated$label,positive="1")

# visualizziamo la matrice di confusione
print(conf_matrix$table)
          Reference
Prediction -1  1
        -1 39  4
        1   2 15
# gli indicatori principali sono pure calcolati (e accessibili singolarmente)

print(conf_matrix)
Confusion Matrix and Statistics

          Reference
Prediction -1  1
        -1 39  4
        1   2 15
                                          
               Accuracy : 0.9             
                 95% CI : (0.7949, 0.9624)
    No Information Rate : 0.6833          
    P-Value [Acc > NIR] : 7.645e-05       
                                          
                  Kappa : 0.7622          
                                          
 Mcnemar's Test P-Value : 0.6831          
                                          
            Sensitivity : 0.7895          
            Specificity : 0.9512          
         Pos Pred Value : 0.8824          
         Neg Pred Value : 0.9070          
             Prevalence : 0.3167          
         Detection Rate : 0.2500          
   Detection Prevalence : 0.2833          
      Balanced Accuracy : 0.8703          
                                          
       'Positive' Class : 1               
                                          

Nel pacchetto caret sono disponiblii anche ulteriori funzioni, come recall(), precision() e F_meas() dal nome autoesplicativo (leggerne l’help per informazioni più precise).


precision(conf_matrix$table, relevant="1")
[1] 0.8823529
recall(conf_matrix$table, relevant="1")
[1] 0.7894737
F_meas(conf_matrix$table, relevant="1", beta=1)
[1] 0.8333333

Cross-Validation

La cross-validation divide il training set in più sottogruppi, utilizzando alcuni di essi per addestrare il modello e altri per testarlo. Questo processo viene ripetuto più volte, cambiando ad ogni iterazione i dati utilizzati per l’addestramento e il test, in modo da avere uno stimatore dell’errore di generalizzazione, il rischio atteso.

Per la funzione knn() del pacchetto class è già implementata la leave-one-out CV, con la funzione knn.cv().


k <- 3
generated_knn_cv <- knn.cv(train_generated[, 1:2], train_generated$label, k)

# aggiungiamo il risultato della cv come colonna

train_generated$knn_cv <- generated_knn_cv

# calcoliamo l'errore di cross validation

error_cv_generated = mean(train_generated$knn_cv != train_generated$label)

# confrontiamolo con l'errore di training e di test

error_train_generated = mean( knn(train_generated[, 1:2], train_generated[, 1:2], train_generated$label, k) != train_generated$label)

error_test_generated = mean( knn(train_generated[, 1:2], test_generated[, 1:2], train_generated$label, k) != test_generated$label)

print(paste("errore di cv (LoO):", round(error_cv_generated, 2)))
[1] "errore di cv (LoO): 0.09"
print(paste("errore di training:", round(error_train_generated, 2)))
[1] "errore di training: 0.08"
print(paste("errore di test:", round(error_test_generated, 2)))
[1] "errore di test: 0.1"

Un approccio comune è la \(k\)-fold cross-validation, dove il dataset viene suddiviso in \(k\) parti, e il modello viene addestrato \(k\) volte, ogni volta utilizzando un diverso fold come set di test.


# impostiamo il k della k-fold CV.

k_cv <- 10

n_train <- nrow(train_generated)

# prendiamo una permutazione casuale degli indici

permutation_train_index <- sample(1:n_train)

folds <- round(seq(1, n_train, length.out=k_cv), 0)


#carichiamo gli errori in un vettore

error_cv_kfold <- c()

for( i in 1:(length(folds)-1)){
  # seleziona gli indici della fold i
  fold_index <- permutation_train_index[folds[i]:(folds[i+1]-1)]
  # definisce il test/train set per la fold corrente
  test_fold_generated <- train_generated[fold_index, ]
  train_fold_generated <- train_generated[-fold_index, ]
  
  #calcola l'errore
  error_fold <-  mean( knn(train_fold_generated[, 1:2], test_fold_generated[, 1:2], train_fold_generated$label, k) != test_fold_generated$label)
  
  # aggiunge l'errore alla sequenza
  error_cv_kfold <- c(error_cv_kfold, error_fold)
}

# calcola la media degli errori
mean(error_cv_kfold)
[1] 0.09275087

Anche per la CV, tuttavia, è possibile e fortemente consigliato utilizzare delle funzioni già predisposte in R. Nel pacchetto caret possiamo usare la funzione train() e trainControl() specificando il metodo cv (altri metodi sono pure già implementati)

# Esecuzione della cross-validation
#set.seed(42)

#specifichiamo i valori per cui calcolare l'errore di CV

control <- trainControl(method = "cv", number = 10)

# usare method = "LOOCV" per leave-one-out CV

knn_cv <- train(train_generated[,1:2], train_generated$label, method = "knn", trControl = control, tuneLength = 20)

# usare l'opzione tuneGrid = data.frame("k"=1:10) per specificare i valori di k da esplorare

# Visualizzazione dei risultati della cross-validation
print(knn_cv)
k-Nearest Neighbors 

240 samples
  2 predictor
  2 classes: '-1', '1' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 215, 217, 216, 216, 216, 216, ... 
Resampling results across tuning parameters:

  k   Accuracy   Kappa    
   5  0.9168043  0.8130049
   7  0.9168043  0.8118685
   9  0.9251377  0.8306002
  11  0.9044710  0.7823798
  13  0.9128043  0.8036702
  15  0.9086377  0.7927024
  17  0.9086377  0.7927024
  19  0.9086377  0.7927024
  21  0.9086377  0.7927024
  23  0.9086377  0.7927024
  25  0.9128043  0.8036702
  27  0.9086377  0.7952024
  29  0.9086377  0.7927024
  31  0.9086377  0.7927024
  33  0.9126377  0.8003913
  35  0.9126377  0.8003913
  37  0.9126377  0.8003913
  39  0.9086377  0.7927024
  41  0.9044710  0.7824202
  43  0.9044710  0.7824202

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

# oppure ggplot(knn_cv)

Dataset Precaricato

Applichiamo il clustering con il metodo \(k\)-NN al dataset Melanoma_df del pacchetto MedDataSets.

Un obiettivo dell’analisi di classificazione potrebbe essere di determinare se il paziente sopravvive oltre \(3\) anni dalla diagnosi (che possiamo ottenere dalla prima colonna) in base alle caratteristiche come il sesso, l’età, lo spessore e la presenza di ulcerazioni.

table("sesso" = mel_data_tidy$sex, "sopravvivenza" = mel_data_tidy$label)
     sopravvivenza
sesso FALSE TRUE
    0    16  110
    1    22   57

Passiamo alla classificazione con \(k\)-NN. Poiché il metodo si basa sulle distanze (euclidee) tra le caratteristiche, è buona pratica standardizzare i dati, quantomeno le variabili numeriche. Per esercizio: ripetere l’analisi senza standardizzare i dati, oppure standardizzando tutte le variabili.


mel_data_tidy[2:3] <- scale(mel_data_tidy[2:3])

head(mel_data_tidy)

Dividiamo in training e test set.


n <- nrow(mel_data_tidy)

train_index <- sample(1:n, size = 0.8 * n)


train_mel <- mel_data_tidy[train_index, ]
test_mel <- mel_data_tidy[-train_index, ]

Alleniamo intanto il modello con un \(k\) scelto da noi.

# k scelto da noi
k <- 5

# salviamo le previsioni
knn_pred <- knn(train_mel[, -5], test_mel[, -5], train_mel$label, k = k)

Valutiamo la performance con la matrice di confusione. In questo caso impostiamo come positivo un paziente che non sopravvive a \(5\) anni dalla diagnosi (vogliamo che il nostro classificatore fornisca una valutazione del rischio di recivida/complicazioni ecc.).



conf_matrix <- confusionMatrix(as.factor(knn_pred), as.factor(test_mel$label),positive="FALSE")

# visualizziamo la matrice di confusione
print(conf_matrix$table)
          Reference
Prediction FALSE TRUE
     FALSE     1    1
     TRUE      4   35
# e gli indicatori principali sono pure calcolati (e accessibili singolarmente)

print(conf_matrix)
Confusion Matrix and Statistics

          Reference
Prediction FALSE TRUE
     FALSE     1    1
     TRUE      4   35
                                         
               Accuracy : 0.878          
                 95% CI : (0.738, 0.9592)
    No Information Rate : 0.878          
    P-Value [Acc > NIR] : 0.6162         
                                         
                  Kappa : 0.2322         
                                         
 Mcnemar's Test P-Value : 0.3711         
                                         
            Sensitivity : 0.20000        
            Specificity : 0.97222        
         Pos Pred Value : 0.50000        
         Neg Pred Value : 0.89744        
             Prevalence : 0.12195        
         Detection Rate : 0.02439        
   Detection Prevalence : 0.04878        
      Balanced Accuracy : 0.58611        
                                         
       'Positive' Class : FALSE          
                                         

Vediamo l’\(F_1\) score.

 F1 <- F_meas(conf_matrix$table, relevant="FALSE", beta=1)
F1
[1] 0.2857143

Plottiamo il risultato nel piano FPR/TPR.


# TPR è la sensitivity
TPR = conf_matrix$byClass[1]
# FPR è 1-specificity
FPR = 1-conf_matrix$byClass[2]

ROC <- data.frame( "FPR" = FPR, "TPR" = TPR, "k" = k)

ggplot(data = ROC, aes(x=FPR, y=TPR, colour = k))+geom_point()+xlim(0,1)+ylim(0,1) +geom_abline(slope=1, intercept = 0, col="red")

NA
NA

Per il valore di \(k\) scelto il test performa male per la TPR (sensibilità). Esploriamo altri valori di \(k\) usando però la cross validation.


k_max = 10

performance <- data.frame( "FPR" = c(), "TPR" = c(), "accuracy"=c(), "F1" = c(), "k" = c())

for( k in 1:k_max){
  knn_cv <- knn.cv(train_mel[, 1:4], train_mel$label, k = k)
  
  accuracy <- mean( knn_cv == train_mel$label)
  
  conf_matrix <- confusionMatrix(as.factor(knn_cv), as.factor(train_mel$label),positive="FALSE")
  # TPR è la sensitivity
  TPR <- conf_matrix$byClass[1]
  # FPR è 1-specificity
  FPR <- 1-conf_matrix$byClass[2]
  
  F1 <- F_meas(conf_matrix$table, relevant="FALSE", beta=1)


  performance = rbind(performance, data.frame("FPR" = FPR, "TPR" = TPR, "accuracy"=accuracy, "F1"=F1, "k" = k) )
}

ggplot(performance, aes(x=k, y=accuracy))+geom_line()+ggtitle("validation error (LoOCV) al variare di k, dataset melanoma_df")

Eseguiamo una cross validation con \(k\)-fold, \(k=10\).


control <- trainControl(method = "cv", number = 10)

# usare method = "LOOCV" per leave-one-out CV

knn_cv <- train(train_mel[, -5], factor(train_mel$label), method = "knn", trControl = control,  tuneGrid = data.frame("k"=1:10))

# usare l'opzione tuneGrid = data.frame("k"=1:10) per specificare i valori di k da esplorare

# Visualizzazione dei risultati della cross-validation
plot(knn_cv)

Sembra che \(k=3\) dia una accuratezza leggermente preferibile, stando ad entrambe le CV. Plottiamo gli \(F\)-score al variare di \(k\).

ggplot(performance, aes(x=k, y=F1))+geom_line()+ggtitle("F1 score al variare di k, dataset melanoma_df")

Plottiamo infine i punti sul piano FPR/TPR al variare di k.


ggplot(data = performance, aes(x=FPR, y=TPR, colour = k, label=k))+geom_text(hjust=0, vjust=0)+xlim(0,1)+ylim(0,1) +geom_abline(slope=1, intercept = 0, col="red")

NA
NA

La cross-validation e gli altri indicatori ci suggeriscono di passare a \(k=2\). Scegliamo quindi questo modello e valutiamo infine il test error.

# k individuato tramite CV

k <- 2

# salviamo le previsioni
knn_pred <- knn(train_mel[, -5], test_mel[, -5], train_mel$label, k = k)


conf_matrix <- confusionMatrix(as.factor(knn_pred), as.factor(test_mel$label),positive="FALSE")

# visualizziamo la matrice di confusione
print(conf_matrix$table)
          Reference
Prediction FALSE TRUE
     FALSE     0    5
     TRUE      5   31
# e gli indicatori principali sono pure calcolati (e accessibili singolarmente)

print(conf_matrix)
Confusion Matrix and Statistics

          Reference
Prediction FALSE TRUE
     FALSE     0    5
     TRUE      5   31
                                         
               Accuracy : 0.7561         
                 95% CI : (0.597, 0.8764)
    No Information Rate : 0.878          
    P-Value [Acc > NIR] : 0.9915         
                                         
                  Kappa : -0.1389        
                                         
 Mcnemar's Test P-Value : 1.0000         
                                         
            Sensitivity : 0.0000         
            Specificity : 0.8611         
         Pos Pred Value : 0.0000         
         Neg Pred Value : 0.8611         
             Prevalence : 0.1220         
         Detection Rate : 0.0000         
   Detection Prevalence : 0.1220         
      Balanced Accuracy : 0.4306         
                                         
       'Positive' Class : FALSE          
                                         

Esercizi

1.Genera un dataset fittizio con due classi utilizzando distribuzioni normali con deviazioni standard diverse (e medie diverse). Ogni classe deve avere una dimensione di 100 campioni. Usa ggplot2 per visualizzare il dataset. Calcola l’accuratezza del modello KNN e seleziona un k ottimale basandoti sugli indicatori.

  1. Utilizza il dataset Iris per applicare il KNN (label è la colonna delle specie). Suddividi il dataset in un set di training (70%) e un set di test (30%). Calcola la matrice di confusione e valuta l’accuratezza del modello.

  2. Utilizza il dataset mtcars per applicare il KNN e ottimizzare il valore di k attraverso la k-fold cross-validation (ad esempio 10-fold). Traccia il grafico dell’accuratezza media in funzione dei diversi valori di \(k\) e identifica il valore ottimale.

  3. Usa il dataset wine (disponibile online https://www.kaggle.com/datasets/yasserh/wine-quality-dataset o nel pacchetto rattle) per applicare il KNN. Suddividi il dataset in un set di training e uno di test. Calcola la precisione, il richiamo e il punteggio F1 per ciascuna classe. Visualizza il risultato con una heatmap.

  4. Utilizza il dataset mtcars e confronta le prestazioni del modello KNN su dati non standardizzati e standardizzati. Calcola e confronta l’accuratezza del modello su entrambi i set di dati. Discutere come la standardizzazione influisce sulle prestazioni del KNN.

  5. Utilizza il dataset iris per applicare la PCA e ridurre la dimensionalità a due componenti principali. Successivamente, utilizza KNN per classificare le specie. Confronta la performance con il risultato senza PCA.

  6. Usa il dataset mtcars per applicare un algoritmo di clustering (ad esempio K-means) per identificare gruppi di automobili. Successivamente, utilizza un modello di classificazione per prevedere il gruppo a cui appartiene ogni automobile.

LS0tCnRpdGxlOiAiQ2xhc3NpZmljYXppb25lIEkgKG5vdGVib29rIDQpIgphdXRob3I6ICJEYXJpbyBUcmV2aXNhbiIKZGF0ZTogIjE1LzEwLzIwMjUiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDMKICAgIHRvY19mbG9hdDogdHJ1ZQogICAgdGhlbWU6IHJlYWRhYmxlCiAgICBkZl9wcmludDogcGFnZWQKICAgIGRvd25sb2FkX2hhbmRsZXI6IHRydWUKICBodG1sX2RvY3VtZW50OgogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6ICczJwogICAgZGZfcHJpbnQ6IHBhZ2VkCnN1YnRpdGxlOiAiU3RhdGlzdGljYSBJSSAtIDc1MEFBIgotLS0KCgoKCiMgSW50cm9kdXppb25lCgpJbiBxdWVzdG8gbm90ZWJvb2sgUiBpbml6aWFtbyBhIHN0dWRpYXJlIGxhIGNsYXNzaWZpY2F6aW9uZSBzdGF0aXN0aWNhIHN1IGRhdGFzZXQgZ2VuZXJhdGkgZSB1biBkYXRhc2V0IHJlYWxlIChwcmUtY2FyaWNhdG8pLiBJbCBub3RlYm9vayBzaSBjb25jZW50cmEgc3VsbCd1dGlsaXp6byBkZWxsJ2FsZ29yaXRtbyAkayQtTk4sIG1hIGFsbG8gc3Rlc3NvIHRlbXBvIGRpc2N1dGUgY29uY2V0dGkgZ2VuZXJhbGkgY29tZSBlcnJvcmkgZGkgdHJhaW5pbmcvdGVzdCwgY3Jvc3MtdmFsaWRhdGlvbiwgbWF0cmljaSBkaSBjb25mdXNpb25lIGUgY3VydmUgUk9DLCBjaGUgc2FyYW5ubyBwb2kgdXRpbGkgYW5jaGUgcGVyIGFsdHJpIG1ldG9kaSBkaSBjbGFzc2lmaWNhemlvbmUuCgpJbml6aWFtbyBjYXJpY2FuZG8gbGUgbGlicmVyaWUgbmVjZXNzYXJpZSAtLSB1c2FyZSBgYGluc3RhbGwucGFja2FnZXMoIm5vbWUgcGFjY2hldHRvIilgYCBwZXIgaW5zdGFsbGFyZSBxdWVsbGUgbm9uIGdpw6AgcHJlc2VudGkuCgpgYGB7cn0KIyBnZ3Bsb3QyIHBlciBncmFmaWNoZSBtaWdsaW9yYXRlCmxpYnJhcnkoZ2dwbG90MikKCiMgbGlicmVyaWEgY29uIGNvbWFuZGkgcGVyIGNsYXNzaWZpY2F6aW9uZSBkaSBiYXNlCmxpYnJhcnkoY2xhc3MpCgojIGxpYnJlcmlhIGNvbiBjb21hbmRpIGF2YW56YXRpIHBlciBjbGFzc2lmaWNhemlvbmUgZSByZWdyZXNzaW9uZQpsaWJyYXJ5KGNhcmV0KQoKCiMgbGlicmVyaWEgcGVyIGlsIGRhdGFzZXQgbWVkaWNhbGUKbGlicmFyeShNZWREYXRhU2V0cykKYGBgCgojIyBEYXRhc2V0IEdlbmVyYXRpCgpJbiBxdWVzdGEgc2V6aW9uZSBjcmVpYW1vIHVuIGRhdGFzZXQgZml0dGl6aW8sIHVzYW5kbyBjb21hbmRpIGRpIGNhbXBpb25hbWVudG8gY29tZSBgYHJub3JtYGAgbWEgYW5jaGUgYGByYmlub21gYCwgYGBycG9pc2BgIHBlciBhdmVyZSBjYXJhdHRlcmlzdGljaGUgZGlzY3JldGUuCgpgYGB7cn0KCiMgQ3JlYXppb25lIGRpIHVuIGRhdGFzZXQgZml0dGl6aW8gCiMgaW1wb3N0YXJlIGlsIHNlZWQgcGVyIHJpcHJvZHVjaWJpbGl0w6AKI3NldC5zZWVkKDQyKQoKIyBudW1lcm8gZGkgb3NzZXJ2YXppb25pCm5fbmVnIDwtIDIwMCAjIG51bWVybyBkaSBuZWdhdGl2aQpuX3BvcyA8LSAxMDAgIyBudW1lcm8gZGkgcG9zaXRpdmkKCiMgbiDDqCBsYSB0YWdsaWEgZGVsIGNhbXBpb25lCm4gPC0gbl9uZWcrbl9wb3MKCiMgZ2VuZXJpYW1vIGxlIGZlYXR1cmVzCngxIDwtIHJub3JtKG5fbmVnLCBtZWFuPS0xLCBzZD0xKQp4MiA8LSBybm9ybShuX25lZywgbWVhbj0tMSwgc2Q9MSkKeDMgPC0gcm5vcm0obl9wb3MsIG1lYW49MSwgc2Q9MSkKeDQgPC0gcm5vcm0obl9wb3MsIG1lYW49MSwgc2Q9MSkKCiMgZ2VuZXJpYW1vIGxlIGNsYXNzaQpjbGFzc2UgPC0gZmFjdG9yKGMocmVwKC0xLCBuX25lZyksIHJlcCgxLCBuX3BvcykgKSkKCmRhdGFfZ2VuZXJhdGVkIDwtIGRhdGEuZnJhbWUoYyh4MSwgeDMpLCBjKHgyLCB4NCksIGNsYXNzZSkKCmNvbG5hbWVzKGRhdGFfZ2VuZXJhdGVkKSA8LSBjKCJmZWF0dXJlXzEiLCAiZmVhdHVyZV8yIiwgImxhYmVsIikKCiMgVmlzdWFsaXp6YXppb25lIGRlbCBkYXRhc2V0CmdncGxvdChkYXRhX2dlbmVyYXRlZCwgYWVzKHggPWZlYXR1cmVfMSwgeSA9IGZlYXR1cmVfMiwgY29sb3IgPSBsYWJlbCkpICsKICBnZW9tX3BvaW50KCkgKwogIGxhYnModGl0bGUgPSAiRGF0YXNldCBGaXR0aXppbyIsIHggPSAiZmVhdHVyZSAxIiwgeSA9ICJmZWF0dXJlIDIiKQpgYGAKCkNvbWUgZGlzY3Vzc28gYSBsZXppb25lLCBwcm9jZWRpYW1vIGluaXppYWxtZW50ZSBkaXZpZGVuZG8gaWwgZGF0YXNldCBpbiB0cmFpbmluZyBlIHRlc3Qgc2V0cy4gU2NlZ2xpYW1vIHVuYSBwZXJjZW50dWFsZSBzaWduaWZpY2F0aXZhIHBlciBpbCB0cmFpbmluZyAoJDgwXCUkIGFkIGVzZW1waW8sIG1hIGNvbXVucXVlIGFuY2hlIGRhIHZhbHV0YXJlIGluIGJhc2UgYSBxdWFudGkgZGF0aSBkaXNwb25pYW1vKS4gCgpVc2lhbW8gbGEgZnVuemlvbmUgYGBzYW1wbGUoKWBgIHBlciBjYW1waW9uYXJlIGluIG1vZG8gY2FzdWFsZSwgcGVyIGV2aXRhcmUgZGkgcHJlbmRlcmUgYWQgZXNlbXBpbyBkYXRpIGRhIHVuYSBzdGVzc2EgY2xhc3NlIHNlIGdpw6Agb3JkaW5hdGkgcGVyIGNsYXNzZSAoY29tZSBuZWwgbm9zdHJvIGNhc28pLiAKCgpgYGB7cn0KIyBTdWRkaXZpc2lvbmUgZGVsIGRhdGFzZXQgaW4gdHJhaW5pbmcgZSB0ZXN0IHNldAojc2V0LnNlZWQoNDIpCgp0cmFpbl9pbmRleCA8LSBzYW1wbGUoMTpuLCBzaXplID0gMC44ICogbikKCgp0cmFpbl9nZW5lcmF0ZWQgPC0gZGF0YV9nZW5lcmF0ZWRbdHJhaW5faW5kZXgsIF0KdGVzdF9nZW5lcmF0ZWQgPC0gZGF0YV9nZW5lcmF0ZWRbLXRyYWluX2luZGV4LCBdCgpoZWFkKHRyYWluX2dlbmVyYXRlZCkKYGBgCgpWaXN1YWxpenppYW1vIGkgZGF0aSBkaXN0aW5ndWVuZG8gdHJhaW4gZSB0ZXN0IHVzYW5kbyB1bmEgZ2VvbWV0cmlhIGRpdmVyc2EuCgpgYGB7cn0KCiMgY3JlaWFtbyB1biB2ZXR0b3JlIGNoZSBpbmRpY2Egc2UgaWwgZGF0byDDqCB0ZXN0IG8gdHJhaW4KCmlzX3RyYWluIDwtIHJlcChGQUxTRSwgbikKaXNfdHJhaW5bdHJhaW5faW5kZXhdIDwtIFRSVUUKCiMgVmlzdWFsaXp6YXppb25lIGRlbCBkYXRhc2V0IGRpc3Rpbmd1ZW5kbyB0cmEgdHJhaW4gZSB0ZXN0CmdncGxvdCh0ZXN0X2dlbmVyYXRlZCwgYWVzKHggPWZlYXR1cmVfMSwgeSA9IGZlYXR1cmVfMiwgY29sb3IgPSBsYWJlbCkgKSsgI3NoYXBlPWlzX3RyYWluICkpICsKICBnZW9tX3BvaW50KCkgKwogIGxhYnModGl0bGUgPSAiRGF0YXNldCBGaXR0aXppbyIsIHggPSAiZmVhdHVyZSAxIiwgeSA9ICJmZWF0dXJlIDIiKSAjIHNoYXBlPSLDqCB0cmFpbmluZyBwb2ludD8iKQoKYGBgCgpPcmEgKmFsbGVuaWFtbyogaWwgbW9kZWxsbyAkayQtTk4gc3VpIGRhdGkgZGVsIHRyYWluaW5nIHNldCBlIGxvIHZhbHV0aWFtbyBzdWkgZGF0aSBkZWwgdGVzdCBzZXQgZ2VuZXJhdG8uIFVzaWFtbyBsYSBmdW56aW9uZSBgYGtubigpYGAgZGFsIHBhY2NoZXR0byBgYGNsYXNzYGAuIEluIGFsdGVybmF0aXZhIHNpIHB1w7IgdXNhcmUgYGBrbm4zKClgYCBkYWwgcGFjY2hldHRvIGBgY2FyZXRgYC4KCgoKYGBge3J9CiMgQWxsZW5hbWVudG8gZGVsIG1vZGVsbG8gS05OCgojIFNjZWdsaWFtbyBpbCB2YWxvcmUgZGkgayBjaGUgcHJlZmVyaWFtbywgbWVnbGlvIGRpc3BhcmkgcGVyIGNsYXNzaWZpY2F6aW9uZSBiaW5hcmlhIGFsdHJpbWVudGkgZGV2ZSByaXNvbHZlcmUgbGUgcGFyaXTDoCBhIGNhc28KayA8LSAxCgpwdW50b190ZXN0IDwtIGMoMCwwKQojIHNhbHZpYW1vIGxlIHByZXZpc2lvbmkKa25uX3ByZWQgPC0ga25uKHRyYWluX2dlbmVyYXRlZFssIDE6Ml0sIHRlc3RfZ2VuZXJhdGVkWywgMToyXSwgdHJhaW5fZ2VuZXJhdGVkJGxhYmVsLCBrID0gaykKCnRhYmxlKGtubl9wcmVkKQpgYGAKClZpc3VhbGl6emlhbW8gbGUgZXRpY2hldHRlIGF0dHJpYnVpdGUgYWwgdGVzdCBzZXQgKHVzaWFtbyBkZWxsZSBmb3JtZSkgY29uZnJvbnRhbmRvbGUgY29uIHF1ZWxsZSBvcmlnaW5hbGkgKHVzaWFtbyBpIGNvbG9yaSBvcmlnaW5hbGkpLgoKYGBge3J9Cgp0ZXN0X2dlbmVyYXRlZCRrbm4gPC0ga25uX3ByZWQKCmdncGxvdCh0ZXN0X2dlbmVyYXRlZCwgYWVzKHggPWZlYXR1cmVfMSwgeSA9IGZlYXR1cmVfMiwgY29sb3IgPSBsYWJlbCwgc2hhcGU9a25uICkpICsKICBnZW9tX3BvaW50KCkgKwogIGxhYnModGl0bGUgPSAiRGF0YXNldCBGaXR0aXppbyIsIHggPSAiZmVhdHVyZSAxIiwgeSA9ICJmZWF0dXJlIDIiLCBzaGFwZT0ia25uIikKYGBgCgojIyMgSW5kaWNhdG9yaSBkaSBwZXJmb3JtYW5jZQoKVW5hIG1ldHJpY2EgZ2VuZXJhbGUgw6ggbCdhY2N1cmF0ZXp6YSBzdWwgdGVzdCBzZXQsIGNhbGNvbGlhbW9sYSBjb25mcm9udGFuZG8gbGUgcHJldmlzaW9uaSBjb24gbGUgdmVyZSBldGljaGV0dGUuCgpgYGB7cn0KIyBhY2N1cmF0ZXp6YQoKYWNjdXJhY3kgPC0gbWVhbih0ZXN0X2dlbmVyYXRlZCRrbm49PSB0ZXN0X2dlbmVyYXRlZCRsYWJlbCApCgpyb3VuZChhY2N1cmFjeSwgNikKCmBgYAoKSWwgKnRlc3QgZXJyb3IqIMOoIHF1aW5kaSBgciByb3VuZCgxLWFjY3VyYWN5LDIpYC4KClRyYXR0YW5kb3NpIGRpIHVuYSBjbGFzc2lmaWNhemlvbmUgYmluYXJpYSB2YWx1dGlhbW8gbGEgcGVyZm9ybWFuY2UgaW4gdW5hICptYXRyaWNlIGRpIGNvbmZ1c2lvbmUqIChxdWVzdG8gc2kgcHXDsiBmYXJlIGFuY2hlIHBlciBjbGFzc2lmaWNhemlvbmkgbm9uIGJpbmFyaWUsIG1hIGluIHRhbCBjYXNvIGxhIHRhZ2xpYSBkZWxsYSBtYXRyaWNlIGF1bWVudGEgZWQgw6ggbWVubyByaWFzc3VudGl2YSkuCgoKYGBge3J9CiMgTWF0cmljZSBkaSBjb25mdXNpb25lCmNvbmZfbWF0cml4IDwtIHRhYmxlKCJSZWFsZSIgPSB0ZXN0X2dlbmVyYXRlZCRsYWJlbCwgIkNsYXNzaWZpY2F0byIgPSB0ZXN0X2dlbmVyYXRlZCRrbm4pCnByaW50KGNvbmZfbWF0cml4KQpgYGAKCkNvbnZlbmllbnRlbWVudGUgYWJiaWFtbyBjaGlhbWF0byBsZSBjbGFzc2kgJC0xJCBwZXIgaSBOZWdhdGl2aSwgJDEkIHBlciBpIFBvc2l0aXZpLCBtYSBwb3NzaWFtbyBhbmNoZSBjYW1iaWFyZSBpIG5vbWkgYWxsZSByaWdoZSBlIGNvbG9ubmUgZGVsbGEgdGFiZWxsYS4KCmBgYHtyfQpjb2xuYW1lcyhjb25mX21hdHJpeCkgPC0gYygiQ04iLCAiQ1AiKQpyb3cubmFtZXMoY29uZl9tYXRyaXgpIDwtICBjKCJOIiwgIlAiKQoKcHJpbnQoY29uZl9tYXRyaXgpCgpgYGAKCkVzdHJhaWFtbyAqYSBtYW5vKiBsZSBjb21wb25lbnRpIGRlbGxhIG1hdHJpY2UgZSBjYWxjb2xpYW1vIGxlIG1ldHJpY2hlIHByaW5jaXBhbGkKCmBgYHtyfQoKcmVhbGkgPC0gdGVzdF9nZW5lcmF0ZWQkbGFiZWwKcHJldmlzdGkgPC0gdGVzdF9nZW5lcmF0ZWQka25uCgojIHBvc2l0aXZpIHJlYWxpClAgPC0gc3VtKCByZWFsaSA9PSAxKQojIG5lZ2F0aXZpIHJlYWxpCk4gPC0gc3VtKCByZWFsaSA9PSAtMSkKCiMgcHJldmFsZW56YQpwcmV2YWxlbmNlIDwtIFAvKFArTikgCgojIHZlcmkgbmVnYXRpdmkKVE4gPC0gc3VtKCByZWFsaSA9PSAtMSAmIHByZXZpc3RpID09IC0xKQojIHZlcmkgcG9zaXRpdmkKVFAgPC1zdW0oIHJlYWxpID09IDEgJiBwcmV2aXN0aSA9PSAxKQojIGZhbHNpIG5lZ2F0aXZpLCBtYW5jYXRvIGFsbGFybWUKRk4gPC0gc3VtKCByZWFsaSA9PSAxICYgIHByZXZpc3RpID09IC0xKQojIGZhbHNpIHBvc2l0aXZpLCBmYWxzbyBhbGxhcm1lCkZQIDwtIHN1bSggcmVhbGkgPT0gLTEgJiBwcmV2aXN0aSA9PSAxKQoKIyBwcmVjaXNpb25lIChwcm9iYWJpbGl0w6AgY2hlIHNpYSB2ZXJvIHBvc2l0aXZvIHNhcGVuZG8gY2hlIGlsIHRlc3QgaW5kaWNhIHBvc2l0aXZvKQpwcmVjaXNpb24gPC0gVFAvKFRQK0ZQKQoKIyBUUFIsIHJlY2FsbC9yaWNoaWFtbywgc2Vuc2liaWxpdMOgIChwcm9iYWJpbGl0w6AgY2hlIGlsIHRlc3QgaW5kaWNoaSBwb3NpdGl2byBzYXBlbmRvIGNoZSBpbCBzb2dnZXR0byDDqCByZWFsbWVudGUgcG9zaXRpdm8pClRQUiA8LSBUUC9QCnJlY2FsbCA8LSBUUFIKCiMgVE5SLCBzcGVjaWZpY2l0w6AgIChwcm9iYWJpbGl0w6AgY2hlIGlsIHRlc3QgaW5kaWNoaSBuZWdhdGl2byBzYXBlbmRvIGNoZSDDqCByZWFsbWVudGUgbmVnYXRpdm8pCgpUTlIgPC0gVE4vTgoKIyBGUFIsIGVycm9yZSBkaSB0aXBvIEkgKHByb2JhYmlsaXTDoCBjaGUgaWwgdGVzdCBpbmRpY2hpIHBvc2l0aXZvIHNhcGVuZG8gY2hlIMOoIHJlYWxtZW50ZSBuZWdhdGl2bykKCkZQUiA8LSBGUC9OCgojIEZOUiwgZXJyb3JlIGRpIHRpcG8gSUkgKHByb2JhYmlsaXTDoCBjaGUgaWwgdGVzdCBpbmRpY2hpIG5lZ2F0aXZvIHNhcGVuZG8gY2hlIMOoIHJlYWxtZW50ZSBwb3NpdGl2bykKCkZOUiA8LSBGTi9QCgpgYGAKCgpQb3NzaWFtbyByYXBwcmVzZW50YXJlIGlsIHJpc3VsdGF0byBkZWwgY2xhc3NpZmljYXRvcmUgc3VsIHRlc3Qgc2V0IGNvbWUgdW4gcHVudG8gbmVsIGdyYWZpY28gRlBSL1RQUi4gUGnDuSBpbCBwdW50byDDqCB2aWNpbm8gYWxsJ2FuZ29sbyBpbiBhbHRvIGEgc2luaXN0cmEgJCgwLDEpJCBtaWdsaW9yZSDDqCBsYSBwZXJmb3JtYW5jZSBkZWwgY2xhc3NpZmljYXRvcmUgKHN1bCB0ZXN0IHNldCkuCgpgYGB7cn0KClJPQyA8LSBkYXRhLmZyYW1lKCAiRlBSIiA9IEZQUiwgIlRQUiIgPSBUUFIsICJrIiA9IGspCgpnZ3Bsb3QoZGF0YSA9IFJPQywgYWVzKHg9RlBSLCB5PVRQUiwgY29sb3VyID0gaykpK2dlb21fcG9pbnQoKSt4bGltKDAsMSkreWxpbSgwLDEpK2dlb21fYWJsaW5lKHNsb3BlPTEsIGludGVyY2VwdCA9IDAsIGNvbD0icmVkIikKCiMgdG9nbGlhbW8gaWwgY29tbWVudG8gcGVyIGFnZ2l1bmdlcmUgbGEgYmlzZXR0cmljZSBwZXIgaW5kaWNhcmUgbGEgcGVyZm9ybWFuY2UgZGVpIGNsYXNzaWZpY2F0b3JpIHB1cmFtZW50ZSBjYXN1YWxpCgpgYGAKCkNpIHNvbm8gYWx0cmkgaW5kaWNhdG9yaSBjaGUgY2VyY2FubyBkaSAqcmlhc3N1bWVyZSogbGEgcGVyZm9ybWFuY2UgZGkgdW4gY2xhc3NpZmljYXRvcmUgYmluYXJpbyAobyBtZWdsaW8sIGRpIHVuIHRlc3QpIGNvbiB1biBzaW5nb2xvIG51bWVyby4gVW5vIMOoIGFuY2hlIGwnYWNjdXJhdGV6emEsIG1hIHBvdHJlYmJlIGVzc2VyZSBpbmZsdWVuemF0YSBkYWxsYSBiYXNzYSBwcmV2YWxlbnphLiBJbmRpY2F0b3JpIG1pZ2xpb3JpIGluIHF1ZXN0byBzZW5zbyBzb25vIGxhICptaXN1cmEgJEZfMSQqICgkRl8xJCBzY29yZSkgY2hlIG1lZGlhICB0cmEgcHJlY2lzaW9uZSBlIHJpY2hpYW1vLiBJZGVhbG1lbnRlIHZvcnJlbW1vIHNpYSByZWNpc2lvbmUgY2hlIHJlY2FsbCB2aWNpbmUgYWQgJDEkLCBxdWluZGkgbWVkaWFuZG9sZSBhdnJlbW8gdW4gaW5kaWNhdG9yZSBjaGUgcGnDuSBzaSBhdnZpY2luYSBhZCAkMSQgbWVnbGlvIMOoLiBMYSBtZWRpYSBjaGUgdmllbmUgdXRpbGl6emF0YSDDqCBsYSBtZWRpYSBhcm1vbmljYToKJCQgRl8xID1cbGVmdCggXGZyYWN7cHJlY2lzaW9uXnstMX0gKyByZWNhbGxeey0xfX17Mn0gXHJpZ2h0KV57LTF9ICQkCgpgYGB7cn0KRjEgPC0gMi8oMS9wcmVjaXNpb24rMS9yZWNhbGwpCmBgYAoKTGEgbWlzdXJhICRGXzEkIGluIHF1ZXN0byBjYXNvIHZhbGUgYHIgRjFgLiBQacO5IGluIGdlbmVyYWxlIMOoIHBvc3NpYmlsZSBpbnRyb2R1cnJlIHVuICpwZXNvKiByZWxhdGl2byB0cmEgcHJlY2lzaW9uZSBlIHJpY2hpYW1vIGNvbiBsYSBtaXN1cmEgJEZfXGJldGEkLCBkb3ZlICRcYmV0YT4wJCBpbmRpY2EgdW4gZmF0dG9yZSBjaGUgcGVzYSBpbCByaWNoaWFtbyAkXGJldGEkIHZvbHRlIHBpw7kgaW1wb3J0YW50ZSBkZWxsYSBwcmVjaXNpb25lLgoKJCQgRl9cYmV0YSA9XGxlZnQoIFxmcmFje1xiZXRhXnstMX0gXGNkb3QgcHJlY2lzaW9uXnstMX0gKyBcYmV0YSBcY2RvdCByZWNhbGxeey0xfX17XGJldGFeey0xfStcYmV0YX0gXHJpZ2h0KV57LTF9ICQkCkFkIGVzZW1waW8sIHNpIHVzYSAgJEZfMiQgY2hlIHBvbmUgcGnDuSBwZXNvIGFsIHJpY2hpYW1vLCBvcHB1cmUgaWwgY2FzbyAkRl97MS8yfSQgY2hlIHBvbmUgcGnDuSBwZXNvIGFsbGEgcHJlY2lzaW9uZS4KCgpgYGB7cn0KRl9iZXRhIDwtIGZ1bmN0aW9uKHByZWNpc2lvbiwgcmVjYWxsLCBiZXRhPTEpewogIDEvKCgxLyhiZXRhICpwcmVjaXNpb24pICsgYmV0YS9yZWNhbGwpLygxL2JldGErYmV0YSkpCn0KCkYxIDwtIHJvdW5kKEZfYmV0YShwcmVjaXNpb24sIHJlY2FsbCksIDIpCkYyIDwtIHJvdW5kKEZfYmV0YShwcmVjaXNpb24sIHJlY2FsbCwgYmV0YT0yKSwgMikKRjAuNSA8LSByb3VuZChGX2JldGEocHJlY2lzaW9uLCByZWNhbGwsIGJldGE9MC41KSwgMikKCmBgYAoKVHJvdmlhbW8gaW4gcXVlc3RvIGNhc28gJEZfMT0kIGByIEYxYCwgJEZfMj0kIGByIEYyYCwgJEZfezEvMn09JCBgciBGMC41YCAoaW4gcXVlc3RvIGNhc28gcHJlY2lzaW9uID0gYHIgcm91bmQocHJlY2lzaW9uLCAyKWAsIHJlY2FsbD1gciByb3VuZChyZWNhbGwsIDIpYCkuCgpVbiB1bHRlcmlvcmUgaW5kaWNhdG9yZSDDqCBpbCBjb2VmZmljaWVudGUgJFxwaGkkLCBub3RvIGFuY2hlIGNvbWUgKk1hdHRoZXdzIGNvcnJlbGF0aW9uIGNvZWZmaWNpZW50IChNQ0MpKiBpbiBtYWNoaW5lIGxlYXJuaW5nLiBQZXIgZGVmaW5pemlvbmUsIHNpIHRyYXR0YSBkaSBjYWxjb2xhcmUgaWwgY29lZmZpY2llbnRlIGRpIGNvcnJlbGF6aW9uZSBkaSBQZWFyc29uIHRyYSBsZSB2YXJpYWJpbGkgYWxlYXRvcmllIChhIHZhbG9yaSBpbiAkLTEkLCAkMSQpIGNvbiBkZW5zaXTDoCBkaXNjcmV0YSBkaSBwcm9iYWJpbGl0w6AgY29uZ2l1bnRhIGRhdGEgZGFsbGUgZW50cmF0ZSBkZWxsYSBtYXRyaWNlIGRpIGNvbmZ1c2lvbmUgZGl2aXNlIHBlciBsYSB0YWdsaWEgZGVsIHRlc3Qgc2V0LiBTaSB0cm92YSBsYSBmb3JtdWxhCiQkIFx2YXJwaGkgPSBcZnJhY3sgVFAgXGNkb3QgVE4gLSBGTiBcY2RvdCBGTn17XHNxcnR7IFAgXGNkb3QgTiBcY2RvdCAoVFArRlApIFxjZG90IChUTitGTil9fS4gJCQKClRyYXR0YW5kb3NpIGRpIHVuIGNvZWZmaWNpZW50ZSBkaSBjb3JyZWxhemlvbmUsIHZhbG9yaSBlc3RyZW1pIHNvbm8gdmljaW5pIGEgJDEkIG8gJC0xJCAoZSBpbmRpY2FubyB1bmEgZm9ydGUgYXNzb2NpYXpvbmUgdHJhIHByZXZpc2lvbmUgZWQgZXRpY2hldHRlIHJlYWxpKSwgbWVudHJlIHZhbG9yaSB2aWNpbmkgYSAkMCQgaW5kaWNhbm8gdW5hIGRlYm9sZSBhc3NvY2lhemlvbmUuIFRpcGljYW1lbnRlIHNpIG9zc2VydmFubyB2YWxvcmkgcG9zaXRpdmkgY29tdW5xdWUgKHVuIGNsYXNzaWZpY2F0b3JlIGNvbiB2YWxvcmUgJFxwaGkkIG5lZ2F0aXZvIGluZGljYSBjaGUgc2JhZ2xpYSBzaXN0ZW1hdGljYW1lbnRlKS4KCgpgYGB7cn0KCnBoaSA9IChUUCpUTiAtIEZQKkZOKS8oc3FydCggUCpOKiAoVFArRlApKihUTitGTikgKSApCgpgYGAKCkluIHF1ZXN0byBjYXNvIGlsIGNvZWZmaWNpZW50ZSB2YWxlICRcdmFycGhpID0kIGByIHJvdW5kKHBoaSwgMilgLgoKTm9uIMOoIG5lY2Vzc2FyaW8gc2NyaXZlcmUgaWwgY29kaWNlIHBlciBjYWxjb2xhcnNpIHR1dHRpIHF1ZXN0aSBpbmRpY2F0b3JpIG9nbmkgdm9sdGEgKmV4IG5vdm8qOiBwb3NzaWFtbyB1c2FyZSBsYSBmdW56aW9uZSBgYGNvbmZ1c2lvbk1hdHJpeCgpYGAgZGFsbGEgbGlicmVyaWEgYGBjYXJldGBgLgoKYGBge3J9CiNjYXJpY2hpYW1vIGxhIGxpYnJlcmlhIGNhcmV0IHNlIG5vbiBnacOgIGZhdHRvIHNvcHJhCmxpYnJhcnkoY2FyZXQpCgpjb25mX21hdHJpeCA8LSBjb25mdXNpb25NYXRyaXgodGVzdF9nZW5lcmF0ZWQka25uLCB0ZXN0X2dlbmVyYXRlZCRsYWJlbCxwb3NpdGl2ZT0iMSIpCgojIHZpc3VhbGl6emlhbW8gbGEgbWF0cmljZSBkaSBjb25mdXNpb25lCnByaW50KGNvbmZfbWF0cml4JHRhYmxlKQoKIyBnbGkgaW5kaWNhdG9yaSBwcmluY2lwYWxpIHNvbm8gcHVyZSBjYWxjb2xhdGkgKGUgYWNjZXNzaWJpbGkgc2luZ29sYXJtZW50ZSkKCnByaW50KGNvbmZfbWF0cml4KQoKYGBgCgpOZWwgcGFjY2hldHRvIGBgY2FyZXRgYCBzb25vIGRpc3BvbmlibGlpIGFuY2hlIHVsdGVyaW9yaSBmdW56aW9uaSwgY29tZSBgYHJlY2FsbCgpYGAsIGBgcHJlY2lzaW9uKClgYCBlIGBgRl9tZWFzKClgYCBkYWwgbm9tZSBhdXRvZXNwbGljYXRpdm8gKGxlZ2dlcm5lIGwnaGVscCBwZXIgaW5mb3JtYXppb25pIHBpw7kgcHJlY2lzZSkuCgoKYGBge3J9CgpwcmVjaXNpb24oY29uZl9tYXRyaXgkdGFibGUsIHJlbGV2YW50PSIxIikKCnJlY2FsbChjb25mX21hdHJpeCR0YWJsZSwgcmVsZXZhbnQ9IjEiKQoKRl9tZWFzKGNvbmZfbWF0cml4JHRhYmxlLCByZWxldmFudD0iMSIsIGJldGE9MSkKYGBgCgoKCiMjIyBDcm9zcy1WYWxpZGF0aW9uCgpMYSBjcm9zcy12YWxpZGF0aW9uIGRpdmlkZSBpbCB0cmFpbmluZyBzZXQgaW4gcGnDuSBzb3R0b2dydXBwaSwgdXRpbGl6emFuZG8gYWxjdW5pIGRpIGVzc2kgcGVyIGFkZGVzdHJhcmUgaWwgbW9kZWxsbyBlIGFsdHJpIHBlciB0ZXN0YXJsby4gUXVlc3RvIHByb2Nlc3NvIHZpZW5lIHJpcGV0dXRvIHBpw7kgdm9sdGUsIGNhbWJpYW5kbyBhZCBvZ25pIGl0ZXJhemlvbmUgaSBkYXRpIHV0aWxpenphdGkgcGVyIGwnYWRkZXN0cmFtZW50byBlIGlsIHRlc3QsIGluIG1vZG8gZGEgYXZlcmUgdW5vIHN0aW1hdG9yZSBkZWxsJ2Vycm9yZSBkaSBnZW5lcmFsaXp6YXppb25lLCBpbCByaXNjaGlvIGF0dGVzby4KClBlciBsYSBmdW56aW9uZSBgYGtubigpYGAgZGVsIHBhY2NoZXR0byBgYGNsYXNzYGAgw6ggZ2nDoCBpbXBsZW1lbnRhdGEgbGEgKmxlYXZlLW9uZS1vdXQqIENWLCBjb24gbGEgZnVuemlvbmUgYGBrbm4uY3YoKWBgLgoKYGBge3J9CgprIDwtIDMKZ2VuZXJhdGVkX2tubl9jdiA8LSBrbm4uY3YodHJhaW5fZ2VuZXJhdGVkWywgMToyXSwgdHJhaW5fZ2VuZXJhdGVkJGxhYmVsLCBrKQoKIyBhZ2dpdW5naWFtbyBpbCByaXN1bHRhdG8gZGVsbGEgY3YgY29tZSBjb2xvbm5hCgp0cmFpbl9nZW5lcmF0ZWQka25uX2N2IDwtIGdlbmVyYXRlZF9rbm5fY3YKCiMgY2FsY29saWFtbyBsJ2Vycm9yZSBkaSBjcm9zcyB2YWxpZGF0aW9uCgplcnJvcl9jdl9nZW5lcmF0ZWQgPSBtZWFuKHRyYWluX2dlbmVyYXRlZCRrbm5fY3YgIT0gdHJhaW5fZ2VuZXJhdGVkJGxhYmVsKQoKIyBjb25mcm9udGlhbW9sbyBjb24gbCdlcnJvcmUgZGkgdHJhaW5pbmcgZSBkaSB0ZXN0CgplcnJvcl90cmFpbl9nZW5lcmF0ZWQgPSBtZWFuKCBrbm4odHJhaW5fZ2VuZXJhdGVkWywgMToyXSwgdHJhaW5fZ2VuZXJhdGVkWywgMToyXSwgdHJhaW5fZ2VuZXJhdGVkJGxhYmVsLCBrKSAhPSB0cmFpbl9nZW5lcmF0ZWQkbGFiZWwpCgplcnJvcl90ZXN0X2dlbmVyYXRlZCA9IG1lYW4oIGtubih0cmFpbl9nZW5lcmF0ZWRbLCAxOjJdLCB0ZXN0X2dlbmVyYXRlZFssIDE6Ml0sIHRyYWluX2dlbmVyYXRlZCRsYWJlbCwgaykgIT0gdGVzdF9nZW5lcmF0ZWQkbGFiZWwpCgpwcmludChwYXN0ZSgiZXJyb3JlIGRpIGN2IChMb08pOiIsIHJvdW5kKGVycm9yX2N2X2dlbmVyYXRlZCwgMikpKQpwcmludChwYXN0ZSgiZXJyb3JlIGRpIHRyYWluaW5nOiIsIHJvdW5kKGVycm9yX3RyYWluX2dlbmVyYXRlZCwgMikpKQpwcmludChwYXN0ZSgiZXJyb3JlIGRpIHRlc3Q6Iiwgcm91bmQoZXJyb3JfdGVzdF9nZW5lcmF0ZWQsIDIpKSkKCgpgYGAKCgpVbiBhcHByb2NjaW8gY29tdW5lIMOoIGxhICRrJC1mb2xkIGNyb3NzLXZhbGlkYXRpb24sIGRvdmUgaWwgZGF0YXNldCB2aWVuZSBzdWRkaXZpc28gaW4gJGskIHBhcnRpLCBlIGlsIG1vZGVsbG8gdmllbmUgYWRkZXN0cmF0byAkayQgdm9sdGUsIG9nbmkgdm9sdGEgdXRpbGl6emFuZG8gdW4gZGl2ZXJzbyBmb2xkIGNvbWUgc2V0IGRpIHRlc3QuCgoKYGBge3J9CgojIGltcG9zdGlhbW8gaWwgayBkZWxsYSBrLWZvbGQgQ1YuCgprX2N2IDwtIDEwCgpuX3RyYWluIDwtIG5yb3codHJhaW5fZ2VuZXJhdGVkKQoKIyBwcmVuZGlhbW8gdW5hIHBlcm11dGF6aW9uZSBjYXN1YWxlIGRlZ2xpIGluZGljaQoKcGVybXV0YXRpb25fdHJhaW5faW5kZXggPC0gc2FtcGxlKDE6bl90cmFpbikKCmZvbGRzIDwtIHJvdW5kKHNlcSgxLCBuX3RyYWluLCBsZW5ndGgub3V0PWtfY3YpLCAwKQoKCiNjYXJpY2hpYW1vIGdsaSBlcnJvcmkgaW4gdW4gdmV0dG9yZQoKZXJyb3JfY3Zfa2ZvbGQgPC0gYygpCgpmb3IoIGkgaW4gMToobGVuZ3RoKGZvbGRzKS0xKSl7CiAgIyBzZWxlemlvbmEgZ2xpIGluZGljaSBkZWxsYSBmb2xkIGkKICBmb2xkX2luZGV4IDwtIHBlcm11dGF0aW9uX3RyYWluX2luZGV4W2ZvbGRzW2ldOihmb2xkc1tpKzFdLTEpXQogICMgZGVmaW5pc2NlIGlsIHRlc3QvdHJhaW4gc2V0IHBlciBsYSBmb2xkIGNvcnJlbnRlCiAgdGVzdF9mb2xkX2dlbmVyYXRlZCA8LSB0cmFpbl9nZW5lcmF0ZWRbZm9sZF9pbmRleCwgXQogIHRyYWluX2ZvbGRfZ2VuZXJhdGVkIDwtIHRyYWluX2dlbmVyYXRlZFstZm9sZF9pbmRleCwgXQogIAogICNjYWxjb2xhIGwnZXJyb3JlCiAgZXJyb3JfZm9sZCA8LSAgbWVhbigga25uKHRyYWluX2ZvbGRfZ2VuZXJhdGVkWywgMToyXSwgdGVzdF9mb2xkX2dlbmVyYXRlZFssIDE6Ml0sIHRyYWluX2ZvbGRfZ2VuZXJhdGVkJGxhYmVsLCBrKSAhPSB0ZXN0X2ZvbGRfZ2VuZXJhdGVkJGxhYmVsKQogIAogICMgYWdnaXVuZ2UgbCdlcnJvcmUgYWxsYSBzZXF1ZW56YQogIGVycm9yX2N2X2tmb2xkIDwtIGMoZXJyb3JfY3Zfa2ZvbGQsIGVycm9yX2ZvbGQpCn0KCiMgY2FsY29sYSBsYSBtZWRpYSBkZWdsaSBlcnJvcmkKbWVhbihlcnJvcl9jdl9rZm9sZCkKCmBgYAoKCkFuY2hlIHBlciBsYSBDViwgdHV0dGF2aWEsIMOoIHBvc3NpYmlsZSBlICpmb3J0ZW1lbnRlIGNvbnNpZ2xpYXRvKiB1dGlsaXp6YXJlIGRlbGxlIGZ1bnppb25pIGdpw6AgcHJlZGlzcG9zdGUgaW4gUi4gTmVsIHBhY2NoZXR0byBgYGNhcmV0YGAgcG9zc2lhbW8gdXNhcmUgbGEgZnVuemlvbmUgYGB0cmFpbigpYGAgZSBgYHRyYWluQ29udHJvbCgpYGAgc3BlY2lmaWNhbmRvIGlsIG1ldG9kbyBgYGN2YGAgKGFsdHJpIG1ldG9kaSBzb25vIHB1cmUgZ2nDoCBpbXBsZW1lbnRhdGkpCgoKYGBge3J9CiMgRXNlY3V6aW9uZSBkZWxsYSBjcm9zcy12YWxpZGF0aW9uCiNzZXQuc2VlZCg0MikKCiNzcGVjaWZpY2hpYW1vIGkgdmFsb3JpIHBlciBjdWkgY2FsY29sYXJlIGwnZXJyb3JlIGRpIENWCgpjb250cm9sIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAiY3YiLCBudW1iZXIgPSAxMCkKCiMgdXNhcmUgbWV0aG9kID0gIkxPT0NWIiBwZXIgbGVhdmUtb25lLW91dCBDVgoKa25uX2N2IDwtIHRyYWluKHRyYWluX2dlbmVyYXRlZFssMToyXSwgdHJhaW5fZ2VuZXJhdGVkJGxhYmVsLCBtZXRob2QgPSAia25uIiwgdHJDb250cm9sID0gY29udHJvbCwgdHVuZUxlbmd0aCA9IDIwKQoKIyB1c2FyZSBsJ29wemlvbmUgdHVuZUdyaWQgPSBkYXRhLmZyYW1lKCJrIj0xOjEwKSBwZXIgc3BlY2lmaWNhcmUgaSB2YWxvcmkgZGkgayBkYSBlc3Bsb3JhcmUKCiMgVmlzdWFsaXp6YXppb25lIGRlaSByaXN1bHRhdGkgZGVsbGEgY3Jvc3MtdmFsaWRhdGlvbgpwcmludChrbm5fY3YpCnBsb3Qoa25uX2N2KQojIG9wcHVyZSBnZ3Bsb3Qoa25uX2N2KQpgYGAKCgojIyBEYXRhc2V0IFByZWNhcmljYXRvCgpBcHBsaWNoaWFtbyBpbCBjbHVzdGVyaW5nIGNvbiBpbCBtZXRvZG8gJGskLU5OIGFsIGRhdGFzZXQgYGBNZWxhbm9tYV9kZmBgIGRlbCBwYWNjaGV0dG8gYGBNZWREYXRhU2V0c2BgLgoKYGBge3J9CiNpbnN0YWxsYXJlIGlsIHBhY2NoZXR0byBpbnN0YWxsLnBhY2thZ2VzKCJNZWREYXRhU2V0cyIpCgptZWxfZGF0YSA8LSBNZWREYXRhU2V0czo6TWVsYW5vbWFfZGYKCmhlYWQobWVsX2RhdGEpCmBgYApVbiBvYmlldHRpdm8gZGVsbCdhbmFsaXNpIGRpIGNsYXNzaWZpY2F6aW9uZSBwb3RyZWJiZSBlc3NlcmUgZGkgZGV0ZXJtaW5hcmUgc2UgaWwgcGF6aWVudGUgc29wcmF2dml2ZSBvbHRyZSAkMyQgYW5uaSBkYWxsYSBkaWFnbm9zaSAoY2hlIHBvc3NpYW1vIG90dGVuZXJlIGRhbGxhIHByaW1hIGNvbG9ubmEpIGluIGJhc2UgYWxsZSBjYXJhdHRlcmlzdGljaGUgY29tZSBpbCBzZXNzbywgbCdldMOgLCBsbyBzcGVzc29yZSBlIGxhIHByZXNlbnphIGRpIHVsY2VyYXppb25pLgoKCmBgYHtyfQojaW50cm9kdWNpYW1vIHVuYSBjb2xvbm5hIHBlciBpbmRpY2FyZSBzZSBpbCBwYXppZW50ZSDDqCBzb3ByYXZ2aXZlIGEgMyBhbm5pIGRhbGxhIGRpYWdub3NpCgptZWxfZGF0YSRsYWJlbCA8LSBtZWxfZGF0YSR0aW1lID4gKDMqMzY1KQoKbWVsX2RhdGFfdGlkeSA8LSBtZWxfZGF0YVtjKDMsIDQsIDYsIDcsIDgpXQoKaGVhZChtZWxfZGF0YV90aWR5KQoKIyByYXBwcmVzZW50aWFtbyBjb24gdW4gYm94cGxvdCBsJ2Fzc29jaWF6aW9uZSB0cmEgc3Blc3NvcmUgZSBsYWJlbApnZ3Bsb3QobWVsX2RhdGFfdGlkeSwgYWVzKHkgPSB0aGlja25lc3MsIGdyb3VwPSBsYWJlbCwgZmlsbD1sYWJlbCkpK2dlb21fYm94cGxvdCgpCgojIHJhcHByZXNlbnRpYW1vIGNvbiB1bmEgdGFiZWxsYSBkaSBjb250aW5nZW56YSBsJ2Fzc29jaWF6b25lIHRyYSBzZXNzbyBlIGxhYmVsCgp0YWJsZSgic2Vzc28iID0gbWVsX2RhdGFfdGlkeSRzZXgsICJzb3ByYXZ2aXZlbnphIiA9IG1lbF9kYXRhX3RpZHkkbGFiZWwpCmBgYAoKUGFzc2lhbW8gYWxsYSBjbGFzc2lmaWNhemlvbmUgY29uICRrJC1OTi4gUG9pY2jDqSBpbCBtZXRvZG8gc2kgYmFzYSBzdWxsZSBkaXN0YW56ZSAoZXVjbGlkZWUpIHRyYSBsZSBjYXJhdHRlcmlzdGljaGUsIMOoIGJ1b25hIHByYXRpY2EgKnN0YW5kYXJkaXp6YXJlKiBpIGRhdGksIHF1YW50b21lbm8gbGUgdmFyaWFiaWxpIG51bWVyaWNoZS4gKipQZXIgZXNlcmNpemlvOioqIHJpcGV0ZXJlIGwnYW5hbGlzaSBzZW56YSBzdGFuZGFyZGl6emFyZSBpIGRhdGksIG9wcHVyZSBzdGFuZGFyZGl6emFuZG8gdHV0dGUgbGUgdmFyaWFiaWxpLgoKYGBge3J9CgptZWxfZGF0YV90aWR5WzI6M10gPC0gc2NhbGUobWVsX2RhdGFfdGlkeVsyOjNdKQoKaGVhZChtZWxfZGF0YV90aWR5KQpgYGAKRGl2aWRpYW1vIGluIHRyYWluaW5nIGUgdGVzdCBzZXQuCgpgYGB7cn0KCm4gPC0gbnJvdyhtZWxfZGF0YV90aWR5KQoKdHJhaW5faW5kZXggPC0gc2FtcGxlKDE6biwgc2l6ZSA9IDAuOCAqIG4pCgoKdHJhaW5fbWVsIDwtIG1lbF9kYXRhX3RpZHlbdHJhaW5faW5kZXgsIF0KdGVzdF9tZWwgPC0gbWVsX2RhdGFfdGlkeVstdHJhaW5faW5kZXgsIF0KCmBgYAoKQWxsZW5pYW1vIGludGFudG8gaWwgbW9kZWxsbyBjb24gdW4gJGskIHNjZWx0byBkYSBub2kuCgoKYGBge3J9CiMgayBzY2VsdG8gZGEgbm9pCmsgPC0gNQoKIyBzYWx2aWFtbyBsZSBwcmV2aXNpb25pCmtubl9wcmVkIDwtIGtubih0cmFpbl9tZWxbLCAtNV0sIHRlc3RfbWVsWywgLTVdLCB0cmFpbl9tZWwkbGFiZWwsIGsgPSBrKQoKYGBgCgpWYWx1dGlhbW8gbGEgcGVyZm9ybWFuY2UgY29uIGxhIG1hdHJpY2UgZGkgY29uZnVzaW9uZS4gSW4gcXVlc3RvIGNhc28gaW1wb3N0aWFtbyBjb21lICpwb3NpdGl2byogdW4gcGF6aWVudGUgY2hlICpub24qIHNvcHJhdnZpdmUgYSAkNSQgYW5uaSBkYWxsYSBkaWFnbm9zaSAodm9nbGlhbW8gY2hlIGlsIG5vc3RybyBjbGFzc2lmaWNhdG9yZSBmb3JuaXNjYSB1bmEgdmFsdXRhemlvbmUgZGVsIHJpc2NoaW8gZGkgcmVjaXZpZGEvY29tcGxpY2F6aW9uaSBlY2MuKS4KCmBgYHtyfQoKCmNvbmZfbWF0cml4IDwtIGNvbmZ1c2lvbk1hdHJpeChhcy5mYWN0b3Ioa25uX3ByZWQpLCBhcy5mYWN0b3IodGVzdF9tZWwkbGFiZWwpLHBvc2l0aXZlPSJGQUxTRSIpCgojIHZpc3VhbGl6emlhbW8gbGEgbWF0cmljZSBkaSBjb25mdXNpb25lCnByaW50KGNvbmZfbWF0cml4JHRhYmxlKQoKIyBlIGdsaSBpbmRpY2F0b3JpIHByaW5jaXBhbGkgc29ubyBwdXJlIGNhbGNvbGF0aSAoZSBhY2Nlc3NpYmlsaSBzaW5nb2xhcm1lbnRlKQoKcHJpbnQoY29uZl9tYXRyaXgpCmBgYAoKVmVkaWFtbyBsJyRGXzEkIHNjb3JlLgpgYGB7cn0KIEYxIDwtIEZfbWVhcyhjb25mX21hdHJpeCR0YWJsZSwgcmVsZXZhbnQ9IkZBTFNFIiwgYmV0YT0xKQpGMQpgYGAKClBsb3R0aWFtbyBpbCByaXN1bHRhdG8gbmVsIHBpYW5vIEZQUi9UUFIuCgpgYGB7cn0KCiMgVFBSIMOoIGxhIHNlbnNpdGl2aXR5ClRQUiA9IGNvbmZfbWF0cml4JGJ5Q2xhc3NbMV0KIyBGUFIgw6ggMS1zcGVjaWZpY2l0eQpGUFIgPSAxLWNvbmZfbWF0cml4JGJ5Q2xhc3NbMl0KClJPQyA8LSBkYXRhLmZyYW1lKCAiRlBSIiA9IEZQUiwgIlRQUiIgPSBUUFIsICJrIiA9IGspCgpnZ3Bsb3QoZGF0YSA9IFJPQywgYWVzKHg9RlBSLCB5PVRQUiwgY29sb3VyID0gaykpK2dlb21fcG9pbnQoKSt4bGltKDAsMSkreWxpbSgwLDEpICtnZW9tX2FibGluZShzbG9wZT0xLCBpbnRlcmNlcHQgPSAwLCBjb2w9InJlZCIpCgoKYGBgClBlciBpbCB2YWxvcmUgZGkgJGskIHNjZWx0byBpbCB0ZXN0IHBlcmZvcm1hIG1hbGUgcGVyIGxhIFRQUiAoc2Vuc2liaWxpdMOgKS4gRXNwbG9yaWFtbyBhbHRyaSB2YWxvcmkgZGkgJGskIHVzYW5kbyBwZXLDsiBsYSBjcm9zcyB2YWxpZGF0aW9uLgoKYGBge3J9CgprX21heCA9IDEwCgpwZXJmb3JtYW5jZSA8LSBkYXRhLmZyYW1lKCAiRlBSIiA9IGMoKSwgIlRQUiIgPSBjKCksICJhY2N1cmFjeSI9YygpLCAiRjEiID0gYygpLCAiayIgPSBjKCkpCgpmb3IoIGsgaW4gMTprX21heCl7CiAga25uX2N2IDwtIGtubi5jdih0cmFpbl9tZWxbLCAxOjRdLCB0cmFpbl9tZWwkbGFiZWwsIGsgPSBrKQogIAogIGFjY3VyYWN5IDwtIG1lYW4oIGtubl9jdiA9PSB0cmFpbl9tZWwkbGFiZWwpCiAgCiAgY29uZl9tYXRyaXggPC0gY29uZnVzaW9uTWF0cml4KGFzLmZhY3Rvcihrbm5fY3YpLCBhcy5mYWN0b3IodHJhaW5fbWVsJGxhYmVsKSxwb3NpdGl2ZT0iRkFMU0UiKQogICMgVFBSIMOoIGxhIHNlbnNpdGl2aXR5CiAgVFBSIDwtIGNvbmZfbWF0cml4JGJ5Q2xhc3NbMV0KICAjIEZQUiDDqCAxLXNwZWNpZmljaXR5CiAgRlBSIDwtIDEtY29uZl9tYXRyaXgkYnlDbGFzc1syXQogIAogIEYxIDwtIEZfbWVhcyhjb25mX21hdHJpeCR0YWJsZSwgcmVsZXZhbnQ9IkZBTFNFIiwgYmV0YT0xKQoKCiAgcGVyZm9ybWFuY2UgPSByYmluZChwZXJmb3JtYW5jZSwgZGF0YS5mcmFtZSgiRlBSIiA9IEZQUiwgIlRQUiIgPSBUUFIsICJhY2N1cmFjeSI9YWNjdXJhY3ksICJGMSI9RjEsICJrIiA9IGspICkKfQoKZ2dwbG90KHBlcmZvcm1hbmNlLCBhZXMoeD1rLCB5PWFjY3VyYWN5KSkrZ2VvbV9saW5lKCkrZ2d0aXRsZSgidmFsaWRhdGlvbiBlcnJvciAoTG9PQ1YpIGFsIHZhcmlhcmUgZGkgaywgZGF0YXNldCBtZWxhbm9tYV9kZiIpCgpgYGAKRXNlZ3VpYW1vIHVuYSBjcm9zcyB2YWxpZGF0aW9uIGNvbiAkayQtZm9sZCwgJGs9MTAkLgoKYGBge3J9Cgpjb250cm9sIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAiY3YiLCBudW1iZXIgPSAxMCkKCiMgdXNhcmUgbWV0aG9kID0gIkxPT0NWIiBwZXIgbGVhdmUtb25lLW91dCBDVgoKa25uX2N2IDwtIHRyYWluKHRyYWluX21lbFssIC01XSwgZmFjdG9yKHRyYWluX21lbCRsYWJlbCksIG1ldGhvZCA9ICJrbm4iLCB0ckNvbnRyb2wgPSBjb250cm9sLCAgdHVuZUdyaWQgPSBkYXRhLmZyYW1lKCJrIj0xOjEwKSkKCiMgdXNhcmUgbCdvcHppb25lIHR1bmVHcmlkID0gZGF0YS5mcmFtZSgiayI9MToxMCkgcGVyIHNwZWNpZmljYXJlIGkgdmFsb3JpIGRpIGsgZGEgZXNwbG9yYXJlCgojIFZpc3VhbGl6emF6aW9uZSBkZWkgcmlzdWx0YXRpIGRlbGxhIGNyb3NzLXZhbGlkYXRpb24KcGxvdChrbm5fY3YpCgpgYGAKCgpTZW1icmEgY2hlICRrPTMkIGRpYSB1bmEgYWNjdXJhdGV6emEgbGVnZ2VybWVudGUgcHJlZmVyaWJpbGUsIHN0YW5kbyBhZCBlbnRyYW1iZSBsZSBDVi4gUGxvdHRpYW1vIGdsaSAkRiQtc2NvcmUgYWwgdmFyaWFyZSBkaSAkayQuCgpgYGB7cn0KZ2dwbG90KHBlcmZvcm1hbmNlLCBhZXMoeD1rLCB5PUYxKSkrZ2VvbV9saW5lKCkrZ2d0aXRsZSgiRjEgc2NvcmUgYWwgdmFyaWFyZSBkaSBrLCBkYXRhc2V0IG1lbGFub21hX2RmIikKYGBgCgpQbG90dGlhbW8gaW5maW5lIGkgcHVudGkgc3VsIHBpYW5vIEZQUi9UUFIgYWwgdmFyaWFyZSBkaSBrLgoKYGBge3J9CgpnZ3Bsb3QoZGF0YSA9IHBlcmZvcm1hbmNlLCBhZXMoeD1GUFIsIHk9VFBSLCBjb2xvdXIgPSBrLCBsYWJlbD1rKSkrZ2VvbV90ZXh0KGhqdXN0PTAsIHZqdXN0PTApK3hsaW0oMCwxKSt5bGltKDAsMSkgK2dlb21fYWJsaW5lKHNsb3BlPTEsIGludGVyY2VwdCA9IDAsIGNvbD0icmVkIikKCgpgYGAKCkxhIGNyb3NzLXZhbGlkYXRpb24gZSBnbGkgYWx0cmkgaW5kaWNhdG9yaSBjaSBzdWdnZXJpc2Nvbm8gZGkgcGFzc2FyZSBhICRrPTIkLiBTY2VnbGlhbW8gcXVpbmRpIHF1ZXN0byBtb2RlbGxvIGUgdmFsdXRpYW1vIGluZmluZSBpbCB0ZXN0IGVycm9yLgoKCmBgYHtyfQojIGsgaW5kaXZpZHVhdG8gdHJhbWl0ZSBDVgoKayA8LSAyCgojIHNhbHZpYW1vIGxlIHByZXZpc2lvbmkKa25uX3ByZWQgPC0ga25uKHRyYWluX21lbFssIC01XSwgdGVzdF9tZWxbLCAtNV0sIHRyYWluX21lbCRsYWJlbCwgayA9IGspCgoKY29uZl9tYXRyaXggPC0gY29uZnVzaW9uTWF0cml4KGFzLmZhY3Rvcihrbm5fcHJlZCksIGFzLmZhY3Rvcih0ZXN0X21lbCRsYWJlbCkscG9zaXRpdmU9IkZBTFNFIikKCiMgdmlzdWFsaXp6aWFtbyBsYSBtYXRyaWNlIGRpIGNvbmZ1c2lvbmUKcHJpbnQoY29uZl9tYXRyaXgkdGFibGUpCgojIGUgZ2xpIGluZGljYXRvcmkgcHJpbmNpcGFsaSBzb25vIHB1cmUgY2FsY29sYXRpIChlIGFjY2Vzc2liaWxpIHNpbmdvbGFybWVudGUpCgpwcmludChjb25mX21hdHJpeCkKCmBgYAoKCiMjIEVzZXJjaXppCgoKMS5HZW5lcmEgdW4gZGF0YXNldCBmaXR0aXppbyBjb24gZHVlIGNsYXNzaSB1dGlsaXp6YW5kbyBkaXN0cmlidXppb25pIG5vcm1hbGkgY29uIGRldmlhemlvbmkgc3RhbmRhcmQgZGl2ZXJzZSAoZSBtZWRpZSBkaXZlcnNlKS4gT2duaSBjbGFzc2UgZGV2ZSBhdmVyZSB1bmEgZGltZW5zaW9uZSBkaSAxMDAgY2FtcGlvbmkuIFVzYSBnZ3Bsb3QyIHBlciB2aXN1YWxpenphcmUgaWwgZGF0YXNldC4gQ2FsY29sYSBsJ2FjY3VyYXRlenphIGRlbCBtb2RlbGxvIEtOTiBlIHNlbGV6aW9uYSB1biBrIG90dGltYWxlIGJhc2FuZG90aSBzdWdsaSBpbmRpY2F0b3JpLgoKMi4gVXRpbGl6emEgaWwgZGF0YXNldCBJcmlzIHBlciBhcHBsaWNhcmUgaWwgS05OIChsYWJlbCDDqCBsYSBjb2xvbm5hIGRlbGxlIHNwZWNpZSkuIFN1ZGRpdmlkaSBpbCBkYXRhc2V0IGluIHVuIHNldCBkaSB0cmFpbmluZyAoNzAlKSBlIHVuIHNldCBkaSB0ZXN0ICgzMCUpLiBDYWxjb2xhIGxhIG1hdHJpY2UgZGkgY29uZnVzaW9uZSBlIHZhbHV0YSBsJ2FjY3VyYXRlenphIGRlbCBtb2RlbGxvLiAKCgozLiBVdGlsaXp6YSBpbCBkYXRhc2V0IG10Y2FycyBwZXIgYXBwbGljYXJlIGlsIEtOTiBlIG90dGltaXp6YXJlIGlsIHZhbG9yZSBkaSBrIGF0dHJhdmVyc28gbGEgay1mb2xkIGNyb3NzLXZhbGlkYXRpb24gKGFkIGVzZW1waW8gMTAtZm9sZCkuIFRyYWNjaWEgaWwgZ3JhZmljbyBkZWxsJ2FjY3VyYXRlenphIG1lZGlhIGluIGZ1bnppb25lIGRlaSBkaXZlcnNpIHZhbG9yaSBkaSAkayQgZSBpZGVudGlmaWNhIGlsIHZhbG9yZSBvdHRpbWFsZS4KCjQuIFVzYSBpbCBkYXRhc2V0IGBgd2luZWBgIChkaXNwb25pYmlsZSBvbmxpbmUgPGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vZGF0YXNldHMveWFzc2VyaC93aW5lLXF1YWxpdHktZGF0YXNldD4gbyBuZWwgcGFjY2hldHRvIGBgcmF0dGxlYGApIHBlciBhcHBsaWNhcmUgaWwgS05OLiBTdWRkaXZpZGkgaWwgZGF0YXNldCBpbiB1biBzZXQgZGkgdHJhaW5pbmcgZSB1bm8gZGkgdGVzdC4gQ2FsY29sYSBsYSBwcmVjaXNpb25lLCBpbCByaWNoaWFtbyBlIGlsIHB1bnRlZ2dpbyBGMSBwZXIgY2lhc2N1bmEgY2xhc3NlLiBWaXN1YWxpenphIGlsIHJpc3VsdGF0byBjb24gdW5hIGhlYXRtYXAuCgo1LiAgVXRpbGl6emEgaWwgZGF0YXNldCBgYG10Y2Fyc2BgIGUgY29uZnJvbnRhIGxlIHByZXN0YXppb25pIGRlbCBtb2RlbGxvIEtOTiBzdSBkYXRpIG5vbiBzdGFuZGFyZGl6emF0aSBlIHN0YW5kYXJkaXp6YXRpLiBDYWxjb2xhIGUgY29uZnJvbnRhIGwnYWNjdXJhdGV6emEgZGVsIG1vZGVsbG8gc3UgZW50cmFtYmkgaSBzZXQgZGkgZGF0aS4gRGlzY3V0ZXJlIGNvbWUgbGEgc3RhbmRhcmRpenphemlvbmUgaW5mbHVpc2NlIHN1bGxlIHByZXN0YXppb25pIGRlbCBLTk4uCgoKNi4gIFV0aWxpenphIGlsIGRhdGFzZXQgYGBpcmlzYGAgcGVyIGFwcGxpY2FyZSBsYSBQQ0EgZSByaWR1cnJlIGxhIGRpbWVuc2lvbmFsaXTDoCBhIGR1ZSBjb21wb25lbnRpIHByaW5jaXBhbGkuIFN1Y2Nlc3NpdmFtZW50ZSwgdXRpbGl6emEgS05OIHBlciBjbGFzc2lmaWNhcmUgbGUgc3BlY2llLiBDb25mcm9udGEgbGEgcGVyZm9ybWFuY2UgY29uIGlsIHJpc3VsdGF0byBzZW56YSBQQ0EuCgo3LiBVc2EgaWwgZGF0YXNldCBgYG10Y2Fyc2BgIHBlciBhcHBsaWNhcmUgdW4gYWxnb3JpdG1vIGRpIGNsdXN0ZXJpbmcgKGFkIGVzZW1waW8gSy1tZWFucykgcGVyIGlkZW50aWZpY2FyZSBncnVwcGkgZGkgYXV0b21vYmlsaS4gU3VjY2Vzc2l2YW1lbnRlLCB1dGlsaXp6YSB1biBtb2RlbGxvIGRpIGNsYXNzaWZpY2F6aW9uZSBwZXIgcHJldmVkZXJlIGlsIGdydXBwbyBhIGN1aSBhcHBhcnRpZW5lIG9nbmkgYXV0b21vYmlsZS4=