En este proyecto se desea ver si el “WIFI Fingerprinting” es un buen método de localización en interiores. Y se desea comprobar que algoritmo de machine learning puede dar una mejor precisión para la localización.

Para ello, hemos usado una base de datos perteneciente a un estudio que se hizo en la Facultad Jaume I de Castellón. Esta facultad consiste en 3 edificios. El data set contiene las medidas de intensidad de señal tomadas por 520 WAPs que hay dispuestos en distintos puntos de los 3 edificios. Además se proprcionan otras variables de interés: Latitud, Longitud, Piso, Edificio, Espacio, Usuario, Teléfono, Posición relativa y timestamp.

Los valores de intensidad de los WAPs están medidos en dBm, midiendo de 0 a -104 dBm, siendo 0 el caso de máxima señal. Para los casos en que no se detecta señal se indica con un 100.

Primero, leemos el data set y realizamos un pre-proceso. En éste, modificamos los valores de los WAPS que no detectan de 100 a -105.

#Cargamos librerías
library(caret)
library(dplyr)
library(tidyr)
library(ggplot2)
library(lubridate)
library(anytime)
library(matlab)

#Leemos training data
setwd("C:/Users/Pau A/Documents/Data Analysis Course/3- Deep Analytics and Visualization/Task 3/UJIndoorLoc")
trainingData <- read.csv("trainingData.csv")
validation_set <- read.csv("validationData.csv")

#Modificamos a -105 el valor de los Waps que no detectan
trainingData[trainingData==100] <- -105
validation_set[validation_set==100] <- -105

waps_train <- trainingData[,1:520]
#Borramos registros que contengan algun WAP con valor 0

vlength <- vector(mode="numeric", length=19937)

for (i in 1:19937){
  
  vlength[i] <-length(which(waps_train[i,] == 0))
  
}

waps_nozero <- which(vlength==0)

#el data set siguiente no contiene waps q marquen 0
trainingData <- trainingData[which(vlength==0),]

#Separamos por edificios
build0 <- subset(trainingData, BUILDINGID==0)
build1 <- subset(trainingData, BUILDINGID==1)
build2 <- subset(trainingData, BUILDINGID==2)

A continuación, para hacernos una idea de la forma de los edificios hacemos una representación de las latitudes y longitudes de los puntos donde se han tomado los datos. También hacemos visualizaciones separando edificio y planta para hacernos una idea de si se han tomado datos en todas las regiones de los edificios.

#Creamos data set con las ultimas columnas, y de los waps
last_col <- trainingData[,c(521:529)]
waps_col <- trainingData[,c(1:520)]
#Separamos por edificios
build0 <- subset(trainingData, BUILDINGID==0)
build1 <- subset(trainingData, BUILDINGID==1)
build2 <- subset(trainingData, BUILDINGID==2)
building_0 <- subset(last_col, BUILDINGID==0)
building_1 <- subset(last_col, BUILDINGID==1)
building_2 <- subset(last_col, BUILDINGID==2)
##Separamos por pisos cada edificio
#Edificio 0
f0_b0 <- subset(building_0, FLOOR==0)
f1_b0 <- subset(building_0, FLOOR==1)
f2_b0 <- subset(building_0, FLOOR==2)
f3_b0 <- subset(building_0, FLOOR==3)
#Edificio 1
f0_b1 <- subset(building_1, FLOOR==0)
f1_b1 <- subset(building_1, FLOOR==1)
f2_b1 <- subset(building_1, FLOOR==2)
f3_b1 <- subset(building_1, FLOOR==3)
#Edificio 2
f0_b2 <- subset(building_2, FLOOR==0)
f1_b2 <- subset(building_2, FLOOR==1)
f2_b2 <- subset(building_2, FLOOR==2)
f3_b2 <- subset(building_2, FLOOR==3)
f4_b2 <- subset(building_2, FLOOR==4)
f0_b0_reduced <- distinct(f0_b0, SPACEID, LONGITUDE, LATITUDE)
build_draw <- ggplot(last_col, aes(x=LONGITUDE)) + 
      geom_point(aes(y=LATITUDE))
            
build_draw

#Distribuci?n zonas edificio 0
f0_b0_map <- ggplot(f0_b0, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 0, planta 0") + xlab("Longitud") + ylab("Latitud")
f0_b0_map

f1_b0_map <- ggplot(f1_b0, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 0, planta 1") + xlab("Longitud") + ylab("Latitud")
f1_b0_map

f2_b0_map <- ggplot(f2_b0, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 0, planta 2") + xlab("Longitud") + ylab("Latitud")
f2_b0_map

f3_b0_map <- ggplot(f3_b0, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 0, planta 3") + xlab("Longitud") + ylab("Latitud")
f3_b0_map

#Distribuci?n zonas edificio 1
f0_b1_map <- ggplot(f0_b1, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 1, planta 0") + xlab("Longitud") + ylab("Latitud")
f0_b1_map

f1_b1_map <- ggplot(f1_b1, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 1, planta 1") + xlab("Longitud") + ylab("Latitud")
f1_b1_map

f2_b1_map <- ggplot(f2_b1, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 1, planta 2") + xlab("Longitud") + ylab("Latitud")
f2_b1_map

f3_b1_map <- ggplot(f3_b1, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 1, planta 3") + xlab("Longitud") + ylab("Latitud")
f3_b1_map

#Distribuci?n zonas edificio 2
f0_b2_map <- ggplot(f0_b2, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 2, planta 0") + xlab("Longitud") + ylab("Latitud")
f0_b2_map

f1_b2_map <- ggplot(f1_b2, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 2, planta 1") + xlab("Longitud") + ylab("Latitud")
f1_b2_map

f2_b2_map <- ggplot(f2_b2, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 2, planta 2") + xlab("Longitud") + ylab("Latitud")
f2_b2_map

f3_b2_map <- ggplot(f3_b2, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 2, planta 3") + xlab("Longitud") + ylab("Latitud")
f3_b2_map

f4_b3_map <- ggplot(f4_b2, aes(x=LONGITUDE)) + geom_point(aes(y=LATITUDE, color=factor(SPACEID))) + ggtitle("Edificio 2, planta 4") + xlab("Longitud") + ylab("Latitud")
f4_b3_map

De los gráficos anteriores se puede ver por ejemplo que en la última planta del edificio 2 no se tomaron datos para una de las zonas (la esquina inferior derecha), asi como tampoco en una zona de las plantas 0 y 1 del edificio 1.

Nuestro objetivo ahora es ver si podemos predecir la posición de una persona una vez tenemos los datos de la señal de los Waps recibida. Para ello hemos probado con distintos algoritmos de machine learning (KNN, Support Vector Machine, Random Forest, C5.0). Hemos entrenado esos algoritmos para predecir las distintas variables que nos pueden indicar la posición.

En nuestro caso hemos determinado que para predecir las 3 coordenadas de posición usaremos latitud, longitud y planta del edificio. También hemos considerado predecir el edificio porque pensamos que se puede predecir con una accuracy de prácticamente 1, y que podemos incorporar la predicción del edificio para predecir las otras variables, separando en tres grupos.

Para comenzar veamos como han sido entrenados los modelos.

Entrenamiento para predecir el Buliding. Usamos el árbol de decisión C5.0. Para la predicción usamos tan solo los 3 WAPS mas relevantes para cada registro, esto es el nombre de los 3 WAPS mas detectados:

### Predicción de Building ###

build_data <- trainingData[,c(1:520)]

build_data[build_data==0] <- -105

build_data <- cbind(build_data, trainingData$BUILDINGID)

waps.relevantes <- data.frame()
for (i in 1:nrow(build_data)){
  vec <- build_data[i,1:520] 
  #posicion del max en el vector build data va a ser mi primera celda de cada registro. 
  row.relevante <- order(vec, decreasing = T)[1:3]
#  row.relevante <- order(vec, decreasing = T)[1]
  waps.relevantes <- rbind(waps.relevantes, row.relevante)
      #mi data set va a ser registro x registro los 3 valores maximos relacionados con el wap que los detecta
      #voy a pedir el "nombre del wap" de forma numerica    
      
    }

waps.relevantes$BUILDINGID <- build_data$`trainingData$BUILDINGID`
waps.relevantes$FLOOR <- trainingData$FLOOR

reg.irrelevantes <- c() 
for (i in 1:nrow(build_data)){
  if (sum(build_data[i,1:ncol(build_data)-1])/(ncol(build_data)-1) == -105)
  reg.irrelevantes <- c(reg.irrelevantes, i)
}
  
waps.relevantes.entreno <- waps.relevantes[-reg.irrelevantes,]
waps.relevantes.BUILD <- waps.relevantes.entreno[,c(1:4)]


## Entrenamos modelo c5.0 ##
tic()
#Cambiamos valores numericos de building por letras
waps.relevantes.BUILD$BUILDINGID[waps.relevantes.BUILD$BUILDINGID==0] <- "a"
waps.relevantes.BUILD$BUILDINGID[waps.relevantes.BUILD$BUILDINGID==1] <- "b"
waps.relevantes.BUILD$BUILDINGID[waps.relevantes.BUILD$BUILDINGID==2] <- "c"

#Factorizamos BUILDINGID
waps.relevantes.BUILD$BUILDINGID <- factor(waps.relevantes.BUILD$BUILDINGID)

colnames(waps.relevantes.BUILD) <- c("Max1","Max2","Max3","BUILDINGID")

#Creamos 10 fold cross validation
fitControl <- trainControl(## 10-fold CV
  method = "cv",
  classProbs = TRUE
)

#Entrenamos los datos
set.seed(15)
C5_build <- train(BUILDINGID ~ ., data = waps.relevantes.BUILD, 
                 method = "C5.0", 
                 trControl = fitControl,
                 metric = "ROC",
                 preProc = c("center", "scale")
)

A continuación, entrenamos los modelos de latitud, longitud y floor por cada edificio por separado. Para ello hemos usado el Random Forest, que dió mejor RMSE en el caso de latitud y longitud y mejor Accuracy para el piso.

library(randomForest)
library(hydroGOF)


### LATITUDE ###

lat_b0 <- build0[,c(1:520,522,524)]
lat_b1 <- build1[,c(1:520,522,524)]
lat_b2 <- build2[,c(1:520,522,524)]

#Realizamos partición de train y test
tic()

RF_lat_b0 <- randomForest(LATITUDE ~ ., data = lat_b0)
RF_lat_b1 <- randomForest(LATITUDE ~ ., data = lat_b1)
RF_lat_b2 <- randomForest(LATITUDE ~ ., data = lat_b2)

toc()


### LONGITUDE ###

long_b0 <- build0[,c(1:520,521,524)]
long_b1 <- build1[,c(1:520,521,524)]
long_b2 <- build2[,c(1:520,521,524)]

#Realizamos partición de train y test
tic()

RF_long_b0 <- randomForest(LONGITUDE ~ ., data = long_b0)
RF_long_b1 <- randomForest(LONGITUDE ~ ., data = long_b1)
RF_long_b2 <- randomForest(LONGITUDE ~ ., data = long_b2)

toc()

### FLOOR ###
#Usaremos solo los 5 Waps mas relevantes para el entreno y la predicción

floor_b0 <- build0[,c(1:520,523,524)]
floor_b1 <- build1[,c(1:520,523,524)]
floor_b2 <- build2[,c(1:520,523,524)]

#Cambiamos valores numC)ricos de Floor por palabras
floor_b0$FLOOR[floor_b0$FLOOR==0] <- "a"
floor_b0$FLOOR[floor_b0$FLOOR==1] <- "b"
floor_b0$FLOOR[floor_b0$FLOOR==2] <- "c"
floor_b0$FLOOR[floor_b0$FLOOR==3] <- "d"

floor_b1$FLOOR[floor_b1$FLOOR==0] <- "a"
floor_b1$FLOOR[floor_b1$FLOOR==1] <- "b"
floor_b1$FLOOR[floor_b1$FLOOR==2] <- "c"
floor_b1$FLOOR[floor_b1$FLOOR==3] <- "d"

floor_b2$FLOOR[floor_b2$FLOOR==0] <- "a"
floor_b2$FLOOR[floor_b2$FLOOR==1] <- "b"
floor_b2$FLOOR[floor_b2$FLOOR==2] <- "c"
floor_b2$FLOOR[floor_b2$FLOOR==3] <- "d"
floor_b2$FLOOR[floor_b2$FLOOR==4] <- "e"

#Factorizamos Floor
floor_b0$FLOOR <- factor(floor_b0$FLOOR)
floor_b1$FLOOR <- factor(floor_b1$FLOOR)
floor_b2$FLOOR <- factor(floor_b2$FLOOR)

#Entrenamos modelos
RF_floor_b0 <- randomForest(FLOOR ~ ., data = floor_b0)
RF_floor_b1 <- randomForest(FLOOR ~ ., data = floor_b1)
RF_floor_b2 <- randomForest(FLOOR ~ ., data = floor_b2)

A continuación creamos una función que haga predicciones de forma escalonada, de latitud, longitud y floor con la informacion del building, usando los modelos entrenados préviamente:

#FUNCTION´
wifiloc <- function(wap){
  
  if (length(wap) != 520){
    stop("The vector needs 520 waps")
  }
  
  if (colnames(wap)[1] != "WAP001"){
    stop("Introduce a Vector containing WAP001 to WAP520")
  }
  #Building
  label <- order(wap, decreasing = T)[1:3]
  bd <- as.data.frame(label)
  bd <- as.data.frame(t(bd))
  colnames(bd) <- c("Max1","Max2","Max3")
  bd$BUILDINGID <- vector(mode="character", length = 1)
  bd$BUILDINGID <- predict(C5_build, bd)
  
  bd$BUILDINGID <- as.numeric(bd$BUILDINGID) - 1      
  
    #Latitude build 0
  lt <- wap
  lt$BUILDINGID <- bd$BUILDINGID
  lt_b0 <- subset(lt, BUILDINGID==0)
  lt_b1 <- subset(lt, BUILDINGID==1)
  lt_b2 <- subset(lt, BUILDINGID==2)
  lt$LATITUDE <- rbind(predict(RF_lat_b0, lt_b0), predict(RF_lat_b1, lt_b1), predict(RF_lat_b2, lt_b2))
  
   #Longitude
  lg <- wap
  lg$BUILDINGID <- bd$BUILDINGID
  lg_b0 <- subset(lg, BUILDINGID==0)
  lg_b1 <- subset(lg, BUILDINGID==1)
  lg_b2 <- subset(lg, BUILDINGID==2)
  lg$LONGITUDE <- rbind(predict(RF_long_b0, lg_b0), predict(RF_long_b1, lg_b1), predict(RF_long_b2, lg_b2))
  
   #Floor
  label_2 <- order(wap, decreasing = T)[1:5]
  fl <- as.data.frame(label_2)
  fl <- as.data.frame(t(fl))
  colnames(fl) <- c("Max1","Max2","Max3","Max4","Max5")
  fl$FLOOR <- vector(mode="character", length = 1)
  fl$BUILDINGID <- bd$BUILDINGID
  fl_b0 <- subset(fl, BUILDINGID==0)
  fl_b1 <- subset(fl, BUILDINGID==1)
  fl_b2 <- subset(fl, BUILDINGID==2)
  if(nrow(fl_b0) != 0){
     fl_b0$FLOOR <- predict(C5_floor_b0, fl_b0)
  }
  
  
  if(nrow(fl_b1) != 0){
     fl_b1$FLOOR <- predict(C5_floor_b1, fl_b1)
  }
  
  if(nrow(fl_b2) != 0){
     fl_b2$FLOOR <- predict(C5_floor_b2, fl_b2)
  }
  fl <- rbind(fl_b0,fl_b1,fl_b2)
  # 
  wap$LATITUDE <- lt$LATITUDE
  wap$LONGITUDE <- lg$LONGITUDE
  wap$BUILDINGID <- bd$BUILDINGID
 
  wap$FLOOR <- fl$FLOOR
  wap$FLOOR <- as.numeric(wap$FLOOR) - 1
  
  return(wap[,521:ncol(wap)])
  
}

Llamamos la función dándole los datos del Validation Set y hace una predicción por cada registro del data set:

#Creamos data frame
pred <- data.frame(LATITUDE = as.numeric(), LONGITUDE = as.numeric(),BUILDINGID = as.numeric(),FLOOR= as.numeric())
#Hacemos loop para todos los datos y llamamos la funci?n a cada fila
for(i in 1:1111){
pred[i,] <- wifiloc(validation_set [i,1:520])
}

Calculamos los erroresm asociados a la predicción de cada variable:

#Building
pred$BUILDREAL <- validation_set$BUILDINGID[1:1111]
pred$ERRORBID <- pred$BUILDINGID - pred$BUILDREAL
mean(pred$ERRORBID, na.rm=T)
[1] -0.00540054
buildMAE <- mean(abs(pred$ERRORBID), na.rm=T)
build_confusion <- confusionMatrix(data=pred$BUILDINGID, validation_set$BUILDINGID)
build_confusion
Confusion Matrix and Statistics

          Reference
Prediction   0   1   2
         0 535   6   0
         1   1 301   1
         2   0   0 267

Overall Statistics
                                          
               Accuracy : 0.9928          
                 95% CI : (0.9859, 0.9969)
    No Information Rate : 0.4824          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9886          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: 0 Class: 1 Class: 2
Sensitivity            0.9981   0.9805   0.9963
Specificity            0.9896   0.9975   1.0000
Pos Pred Value         0.9889   0.9934   1.0000
Neg Pred Value         0.9982   0.9926   0.9988
Prevalence             0.4824   0.2763   0.2412
Detection Rate         0.4815   0.2709   0.2403
Detection Prevalence   0.4869   0.2727   0.2403
Balanced Accuracy      0.9938   0.9890   0.9981
#Latitud
pred$LATREAL <- validation_set$LATITUDE[1:1111]
pred$ERRORLAT <- pred$LATITUDE - pred$LATREAL
mean(pred$ERRORLAT, na.rm=T)
[1] 1.755398
mean(abs(pred$ERRORLAT), na.rm=T)
[1] 6.257309
rmse_lat <- rmse(pred$LATITUDE, pred$LATREAL)
latitudeRSQ <- 1 - (sum((pred$LATREAL-pred$LATITUDE)^2)/sum((pred$LATREAL-mean(pred$LATREAL))^2))
#Longitud
pred$LONGREAL <- validation_set$LONGITUDE[1:1111]
pred$ERRORLONG <- pred$LONGITUDE - pred$LONGREAL
mean(pred$ERRORLONG, na.rm=T)
[1] -1.371383
mean(abs(pred$ERRORLONG), na.rm=T)
[1] 7.057186
rmse_long <- rmse(pred$LONGITUDE, pred$LONGREAL)
longitudeRSQ <- 1 - (sum((pred$LONGREAL-pred$LONGITUDE)^2)/sum((pred$LONGREAL-mean(pred$LONGREAL))^2))
#Floor
pred$FLOORREAL <- validation_set$FLOOR[1:1111]
pred$ERRORFLOOR <- pred$FLOOR - pred$FLOORREAL
mean(pred$ERRORFLOOR, na.rm=T)
[1] -0.01890189
floor_confusion <- confusionMatrix(data=pred$FLOOR, validation_set$FLOOR)
floor_confusion
Confusion Matrix and Statistics

          Reference
Prediction   0   1   2   3   4
         0 124  21   2   0   1
         1   6 420  20   1   2
         2   2  16 276   6   0
         3   0   5   8 164   3
         4   0   0   0   1  33

Overall Statistics
                                          
               Accuracy : 0.9154          
                 95% CI : (0.8975, 0.9311)
    No Information Rate : 0.4158          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.8818          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: 0 Class: 1 Class: 2 Class: 3 Class: 4
Sensitivity            0.9394   0.9091   0.9020   0.9535   0.8462
Specificity            0.9755   0.9553   0.9702   0.9830   0.9991
Pos Pred Value         0.8378   0.9354   0.9200   0.9111   0.9706
Neg Pred Value         0.9917   0.9366   0.9630   0.9914   0.9944
Prevalence             0.1188   0.4158   0.2754   0.1548   0.0351
Detection Rate         0.1116   0.3780   0.2484   0.1476   0.0297
Detection Prevalence   0.1332   0.4041   0.2700   0.1620   0.0306
Balanced Accuracy      0.9574   0.9322   0.9361   0.9682   0.9226

Como podemos ver anteriormente, para la predicción del building obtenemos una Accuracy de prácticamente 1, errando solo en 8 casos de 1111. Para la predicción del floor la Accuracy ya es mas baja, de 0,9154. Este error puede ser debido a la confusión que se puede producir en puntos cercanos a huecos de escalera o ascensor, donde la señal de los WAPS de pisos siguientes puede ser bien recibida.

Representamos gráficamente las distribuciones de error para la latitud y la longitud. Observamos también el RMSE de ambas coordenadas.

library(ggplot2)
#Histogramas
hist(pred$ERRORLAT, 
     breaks=60, 
     freq=TRUE,
     xlim = c(-60,60),
     col="#0b87a1",
     main="Distribución de error en la latitud",
     xlab="Error latitud",
     ylab="Frecuencia"
)

hist(pred$ERRORLONG, 
     breaks=120, 
     freq=TRUE,
     xlim = c(-60,60),
     col="#0b87a1",
     main="Distribución de error en la longitud",
     xlab="Error longitud",
     ylab="Frecuencia"
)

rmse_long
[1] 17.02018
rmse_lat
[1] 11.74968

El RMSE de la latitud es 11,75 y el de la longitud es 17,02. Tiene sentido que el de la longitud sea mayor puesto que el conjunto de los edificios es mas extenso en el sentido de coordenadas de la longitud. Una de las fuentes del error puede ser que en el Training Set se han tomado los datos de forma controlada, en puntos concretos, y muchas veces en los mismos puntos, mientras que en el Validation Set las medidas son tomadas de forma aleatoria. Además el Training Set se deja zonas de algun edificio sin cubrir con lo cuál en esas zonas el error es mayor. También, como podemos ver a continuación se observa que es en las esquinas donde se producen mas errores, probablemente debido a la dispersión que provocan las paredes de éstas, haciendo que la senyal de los Waps de una dirección quede atenuada y la trinagulación no pueda ser tan precisa.

outliers_LAT <- subset(pred, ERRORLAT > 15)
p_LAT <- ggplot() + 
  geom_point(data=pred, aes(x=LONGREAL, y=LATREAL), color="grey", size=1) +
  geom_point(data=outliers_LAT,aes(x=LONGREAL, y=LATREAL), color="#003b55", size=2) +
  ggtitle("Outliers latitud") + xlab("Longitud") + ylab("Latitud")
p_LAT

outliers_LONG <- subset(pred, ERRORLONG > 20)
p_LONG <- ggplot() + 
  geom_point(data=pred, aes(x=LONGREAL, y=LATREAL), color="grey", size=1) +
  geom_point(data=outliers_LONG,aes(x=LONGREAL, y=LATREAL), color="#003b55", size=2) +
  ggtitle("Outliers longitud") + xlab("Longitud") + ylab("Latitud")
p_LONG

p <-  ggplot() + 
  geom_point(data=pred, aes(x=LONGREAL, y=LATREAL), color="grey", size=1) +
  geom_point(data=outliers_LONG,aes(x=LONGREAL, y=LATREAL), color="#003b55", size=2) +
  ggtitle("Outliers longitud") + xlab("Longitud") + ylab("Latitud") +
  geom_point(data=pred, aes(x=LONGREAL, y=LATREAL), color="grey", size=1) +
  geom_point(data=outliers_LAT,aes(x=LONGREAL, y=LATREAL), color="#003b55", size=2)

Finalmente quiero comentar algunas opciones para mejorar el modelo. La primera es normalizar por filas la señal de los Waps para predecir la latitud y la longitud, puesto que así se pierde variabilidad y se equiparan los valores máximos recibidos en cada registro si son recibidos por el mismo WAP. Otra opción es incorporar datos del Validation Set para entrenar los modelos, cubriendo con éste las zonas que no se cubren con el Training Set. Otra opción sería probar otros algoritmos como el KNN, que si bien nos dan un RMSE mayor porqué los outliers son mayores, también predice mas cantidad de puntos correctamente, siendo la dsitribución de error antes expuesta mas estrecha y alta en el medio. Se podrían combinar ambos algoritmos, usando primero el KNN y despues usando el Random Forest para la predicción de esos puntos que sean muy mal predichos con el KNN.

LS0tDQogIHRpdGxlOiAiRXN0dWRpbyBkZSB0w6ljbmljYXMgZGUgbWFjaGluZSBsZWFybmluZyBwYXJhIGxhIGxvY2FsaXphY2nDs24gcG9yIFdJRkkiDQogIG91dHB1dDogaHRtbF9ub3RlYm9vaw0KDQotLS0NCg0KRW4gZXN0ZSBwcm95ZWN0byBzZSBkZXNlYSB2ZXIgc2kgZWwgIldJRkkgRmluZ2VycHJpbnRpbmciIGVzIHVuIGJ1ZW4gbcOpdG9kbyBkZSBsb2NhbGl6YWNpw7NuIGVuIGludGVyaW9yZXMuIFkgc2UgZGVzZWEgY29tcHJvYmFyIHF1ZSBhbGdvcml0bW8gZGUgbWFjaGluZSBsZWFybmluZyBwdWVkZSBkYXIgdW5hIG1lam9yIHByZWNpc2nDs24gcGFyYSBsYSBsb2NhbGl6YWNpw7NuLg0KDQpQYXJhIGVsbG8sIGhlbW9zIHVzYWRvIHVuYSBiYXNlIGRlIGRhdG9zIHBlcnRlbmVjaWVudGUgYSB1biBlc3R1ZGlvIHF1ZSBzZSBoaXpvIGVuIGxhIEZhY3VsdGFkIEphdW1lIEkgZGUgQ2FzdGVsbMOzbi4gRXN0YSBmYWN1bHRhZCBjb25zaXN0ZSBlbiAzIGVkaWZpY2lvcy4NCkVsIGRhdGEgc2V0IGNvbnRpZW5lIGxhcyBtZWRpZGFzIGRlIGludGVuc2lkYWQgZGUgc2XDsWFsIHRvbWFkYXMgcG9yIDUyMCBXQVBzIHF1ZSBoYXkgZGlzcHVlc3RvcyBlbiBkaXN0aW50b3MgcHVudG9zIGRlIGxvcyAzIGVkaWZpY2lvcy4gQWRlbcOhcyBzZSBwcm9wcmNpb25hbiBvdHJhcyB2YXJpYWJsZXMgZGUgaW50ZXLDqXM6IExhdGl0dWQsIExvbmdpdHVkLCBQaXNvLCBFZGlmaWNpbywgRXNwYWNpbywgVXN1YXJpbywNClRlbMOpZm9ubywgIFBvc2ljacOzbiByZWxhdGl2YSB5IHRpbWVzdGFtcC4NCg0KTG9zIHZhbG9yZXMgZGUgaW50ZW5zaWRhZCBkZSBsb3MgV0FQcyBlc3TDoW4gbWVkaWRvcyBlbiBkQm0sIG1pZGllbmRvIGRlIDAgYSAtMTA0IGRCbSwgc2llbmRvIDAgZWwgY2FzbyBkZSBtw6F4aW1hIHNlw7FhbC4gUGFyYSBsb3MgY2Fzb3MgZW4gcXVlIG5vIHNlIGRldGVjdGEgc2XDsWFsIHNlIGluZGljYSBjb24gdW4gMTAwLg0KDQpQcmltZXJvLCBsZWVtb3MgZWwgZGF0YSBzZXQgeSByZWFsaXphbW9zIHVuIHByZS1wcm9jZXNvLiBFbiDDqXN0ZSwgbW9kaWZpY2Ftb3MgbG9zIHZhbG9yZXMgZGUgbG9zIFdBUFMgcXVlIG5vIGRldGVjdGFuIGRlIDEwMCBhIC0xMDUuDQpgYGB7cn0NCiNDYXJnYW1vcyBsaWJyZXLDrWFzDQpsaWJyYXJ5KGNhcmV0KQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkodGlkeXIpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGx1YnJpZGF0ZSkNCmxpYnJhcnkoYW55dGltZSkNCmxpYnJhcnkobWF0bGFiKQ0KDQojTGVlbW9zIHRyYWluaW5nIGRhdGENCnNldHdkKCJDOi9Vc2Vycy9QYXUgQS9Eb2N1bWVudHMvRGF0YSBBbmFseXNpcyBDb3Vyc2UvMy0gRGVlcCBBbmFseXRpY3MgYW5kIFZpc3VhbGl6YXRpb24vVGFzayAzL1VKSW5kb29yTG9jIikNCnRyYWluaW5nRGF0YSA8LSByZWFkLmNzdigidHJhaW5pbmdEYXRhLmNzdiIpDQp2YWxpZGF0aW9uX3NldCA8LSByZWFkLmNzdigidmFsaWRhdGlvbkRhdGEuY3N2IikNCg0KI01vZGlmaWNhbW9zIGEgLTEwNSBlbCB2YWxvciBkZSBsb3MgV2FwcyBxdWUgbm8gZGV0ZWN0YW4NCnRyYWluaW5nRGF0YVt0cmFpbmluZ0RhdGE9PTEwMF0gPC0gLTEwNQ0KdmFsaWRhdGlvbl9zZXRbdmFsaWRhdGlvbl9zZXQ9PTEwMF0gPC0gLTEwNQ0KDQp3YXBzX3RyYWluIDwtIHRyYWluaW5nRGF0YVssMTo1MjBdDQojQm9ycmFtb3MgcmVnaXN0cm9zIHF1ZSBjb250ZW5nYW4gYWxndW4gV0FQIGNvbiB2YWxvciAwDQoNCnZsZW5ndGggPC0gdmVjdG9yKG1vZGU9Im51bWVyaWMiLCBsZW5ndGg9MTk5MzcpDQoNCmZvciAoaSBpbiAxOjE5OTM3KXsNCiAgDQogIHZsZW5ndGhbaV0gPC1sZW5ndGgod2hpY2god2Fwc190cmFpbltpLF0gPT0gMCkpDQogIA0KfQ0KDQp3YXBzX25vemVybyA8LSB3aGljaCh2bGVuZ3RoPT0wKQ0KDQojZWwgZGF0YSBzZXQgc2lndWllbnRlIG5vIGNvbnRpZW5lIHdhcHMgcSBtYXJxdWVuIDANCnRyYWluaW5nRGF0YSA8LSB0cmFpbmluZ0RhdGFbd2hpY2godmxlbmd0aD09MCksXQ0KDQojU2VwYXJhbW9zIHBvciBlZGlmaWNpb3MNCmJ1aWxkMCA8LSBzdWJzZXQodHJhaW5pbmdEYXRhLCBCVUlMRElOR0lEPT0wKQ0KYnVpbGQxIDwtIHN1YnNldCh0cmFpbmluZ0RhdGEsIEJVSUxESU5HSUQ9PTEpDQpidWlsZDIgPC0gc3Vic2V0KHRyYWluaW5nRGF0YSwgQlVJTERJTkdJRD09MikNCg0KYGBgDQoNCkEgY29udGludWFjacOzbiwgcGFyYSBoYWNlcm5vcyB1bmEgaWRlYSBkZSBsYSBmb3JtYSBkZSBsb3MgZWRpZmljaW9zIGhhY2Vtb3MgdW5hIHJlcHJlc2VudGFjacOzbiBkZSBsYXMgbGF0aXR1ZGVzIHkgbG9uZ2l0dWRlcyBkZSBsb3MgcHVudG9zIGRvbmRlIHNlIGhhbiB0b21hZG8gbG9zIGRhdG9zLiBUYW1iacOpbiBoYWNlbW9zIHZpc3VhbGl6YWNpb25lcyBzZXBhcmFuZG8gZWRpZmljaW8geSBwbGFudGEgcGFyYSBoYWNlcm5vcyB1bmEgaWRlYSBkZSBzaSBzZSBoYW4gdG9tYWRvIGRhdG9zIGVuIHRvZGFzIGxhcyByZWdpb25lcyBkZSBsb3MgZWRpZmljaW9zLg0KDQpgYGB7cn0NCiNDcmVhbW9zIGRhdGEgc2V0IGNvbiBsYXMgdWx0aW1hcyBjb2x1bW5hcywgeSBkZSBsb3Mgd2Fwcw0KbGFzdF9jb2wgPC0gdHJhaW5pbmdEYXRhWyxjKDUyMTo1MjkpXQ0Kd2Fwc19jb2wgPC0gdHJhaW5pbmdEYXRhWyxjKDE6NTIwKV0NCg0KI1NlcGFyYW1vcyBwb3IgZWRpZmljaW9zDQpidWlsZDAgPC0gc3Vic2V0KHRyYWluaW5nRGF0YSwgQlVJTERJTkdJRD09MCkNCmJ1aWxkMSA8LSBzdWJzZXQodHJhaW5pbmdEYXRhLCBCVUlMRElOR0lEPT0xKQ0KYnVpbGQyIDwtIHN1YnNldCh0cmFpbmluZ0RhdGEsIEJVSUxESU5HSUQ9PTIpDQoNCmJ1aWxkaW5nXzAgPC0gc3Vic2V0KGxhc3RfY29sLCBCVUlMRElOR0lEPT0wKQ0KYnVpbGRpbmdfMSA8LSBzdWJzZXQobGFzdF9jb2wsIEJVSUxESU5HSUQ9PTEpDQpidWlsZGluZ18yIDwtIHN1YnNldChsYXN0X2NvbCwgQlVJTERJTkdJRD09MikNCg0KIyNTZXBhcmFtb3MgcG9yIHBpc29zIGNhZGEgZWRpZmljaW8NCg0KI0VkaWZpY2lvIDANCmYwX2IwIDwtIHN1YnNldChidWlsZGluZ18wLCBGTE9PUj09MCkNCmYxX2IwIDwtIHN1YnNldChidWlsZGluZ18wLCBGTE9PUj09MSkNCmYyX2IwIDwtIHN1YnNldChidWlsZGluZ18wLCBGTE9PUj09MikNCmYzX2IwIDwtIHN1YnNldChidWlsZGluZ18wLCBGTE9PUj09MykNCg0KI0VkaWZpY2lvIDENCmYwX2IxIDwtIHN1YnNldChidWlsZGluZ18xLCBGTE9PUj09MCkNCmYxX2IxIDwtIHN1YnNldChidWlsZGluZ18xLCBGTE9PUj09MSkNCmYyX2IxIDwtIHN1YnNldChidWlsZGluZ18xLCBGTE9PUj09MikNCmYzX2IxIDwtIHN1YnNldChidWlsZGluZ18xLCBGTE9PUj09MykNCg0KI0VkaWZpY2lvIDINCmYwX2IyIDwtIHN1YnNldChidWlsZGluZ18yLCBGTE9PUj09MCkNCmYxX2IyIDwtIHN1YnNldChidWlsZGluZ18yLCBGTE9PUj09MSkNCmYyX2IyIDwtIHN1YnNldChidWlsZGluZ18yLCBGTE9PUj09MikNCmYzX2IyIDwtIHN1YnNldChidWlsZGluZ18yLCBGTE9PUj09MykNCmY0X2IyIDwtIHN1YnNldChidWlsZGluZ18yLCBGTE9PUj09NCkNCg0KDQpmMF9iMF9yZWR1Y2VkIDwtIGRpc3RpbmN0KGYwX2IwLCBTUEFDRUlELCBMT05HSVRVREUsIExBVElUVURFKQ0KDQpidWlsZF9kcmF3IDwtIGdncGxvdChsYXN0X2NvbCwgYWVzKHg9TE9OR0lUVURFKSkgKyANCiAgICAgIGdlb21fcG9pbnQoYWVzKHk9TEFUSVRVREUpKQ0KICAgICAgICAgICAgDQpidWlsZF9kcmF3DQoNCiNEaXN0cmlidWNpP24gem9uYXMgZWRpZmljaW8gMA0KDQpmMF9iMF9tYXAgPC0gZ2dwbG90KGYwX2IwLCBhZXMoeD1MT05HSVRVREUpKSArIGdlb21fcG9pbnQoYWVzKHk9TEFUSVRVREUsIGNvbG9yPWZhY3RvcihTUEFDRUlEKSkpICsgZ2d0aXRsZSgiRWRpZmljaW8gMCwgcGxhbnRhIDAiKSArIHhsYWIoIkxvbmdpdHVkIikgKyB5bGFiKCJMYXRpdHVkIikNCmYwX2IwX21hcA0KDQpmMV9iMF9tYXAgPC0gZ2dwbG90KGYxX2IwLCBhZXMoeD1MT05HSVRVREUpKSArIGdlb21fcG9pbnQoYWVzKHk9TEFUSVRVREUsIGNvbG9yPWZhY3RvcihTUEFDRUlEKSkpICsgZ2d0aXRsZSgiRWRpZmljaW8gMCwgcGxhbnRhIDEiKSArIHhsYWIoIkxvbmdpdHVkIikgKyB5bGFiKCJMYXRpdHVkIikNCmYxX2IwX21hcA0KDQpmMl9iMF9tYXAgPC0gZ2dwbG90KGYyX2IwLCBhZXMoeD1MT05HSVRVREUpKSArIGdlb21fcG9pbnQoYWVzKHk9TEFUSVRVREUsIGNvbG9yPWZhY3RvcihTUEFDRUlEKSkpICsgZ2d0aXRsZSgiRWRpZmljaW8gMCwgcGxhbnRhIDIiKSArIHhsYWIoIkxvbmdpdHVkIikgKyB5bGFiKCJMYXRpdHVkIikNCmYyX2IwX21hcA0KDQpmM19iMF9tYXAgPC0gZ2dwbG90KGYzX2IwLCBhZXMoeD1MT05HSVRVREUpKSArIGdlb21fcG9pbnQoYWVzKHk9TEFUSVRVREUsIGNvbG9yPWZhY3RvcihTUEFDRUlEKSkpICsgZ2d0aXRsZSgiRWRpZmljaW8gMCwgcGxhbnRhIDMiKSArIHhsYWIoIkxvbmdpdHVkIikgKyB5bGFiKCJMYXRpdHVkIikNCmYzX2IwX21hcA0KDQojRGlzdHJpYnVjaT9uIHpvbmFzIGVkaWZpY2lvIDENCmYwX2IxX21hcCA8LSBnZ3Bsb3QoZjBfYjEsIGFlcyh4PUxPTkdJVFVERSkpICsgZ2VvbV9wb2ludChhZXMoeT1MQVRJVFVERSwgY29sb3I9ZmFjdG9yKFNQQUNFSUQpKSkgKyBnZ3RpdGxlKCJFZGlmaWNpbyAxLCBwbGFudGEgMCIpICsgeGxhYigiTG9uZ2l0dWQiKSArIHlsYWIoIkxhdGl0dWQiKQ0KZjBfYjFfbWFwDQoNCmYxX2IxX21hcCA8LSBnZ3Bsb3QoZjFfYjEsIGFlcyh4PUxPTkdJVFVERSkpICsgZ2VvbV9wb2ludChhZXMoeT1MQVRJVFVERSwgY29sb3I9ZmFjdG9yKFNQQUNFSUQpKSkgKyBnZ3RpdGxlKCJFZGlmaWNpbyAxLCBwbGFudGEgMSIpICsgeGxhYigiTG9uZ2l0dWQiKSArIHlsYWIoIkxhdGl0dWQiKQ0KZjFfYjFfbWFwDQoNCmYyX2IxX21hcCA8LSBnZ3Bsb3QoZjJfYjEsIGFlcyh4PUxPTkdJVFVERSkpICsgZ2VvbV9wb2ludChhZXMoeT1MQVRJVFVERSwgY29sb3I9ZmFjdG9yKFNQQUNFSUQpKSkgKyBnZ3RpdGxlKCJFZGlmaWNpbyAxLCBwbGFudGEgMiIpICsgeGxhYigiTG9uZ2l0dWQiKSArIHlsYWIoIkxhdGl0dWQiKQ0KZjJfYjFfbWFwDQoNCmYzX2IxX21hcCA8LSBnZ3Bsb3QoZjNfYjEsIGFlcyh4PUxPTkdJVFVERSkpICsgZ2VvbV9wb2ludChhZXMoeT1MQVRJVFVERSwgY29sb3I9ZmFjdG9yKFNQQUNFSUQpKSkgKyBnZ3RpdGxlKCJFZGlmaWNpbyAxLCBwbGFudGEgMyIpICsgeGxhYigiTG9uZ2l0dWQiKSArIHlsYWIoIkxhdGl0dWQiKQ0KZjNfYjFfbWFwDQoNCiNEaXN0cmlidWNpP24gem9uYXMgZWRpZmljaW8gMg0KZjBfYjJfbWFwIDwtIGdncGxvdChmMF9iMiwgYWVzKHg9TE9OR0lUVURFKSkgKyBnZW9tX3BvaW50KGFlcyh5PUxBVElUVURFLCBjb2xvcj1mYWN0b3IoU1BBQ0VJRCkpKSArIGdndGl0bGUoIkVkaWZpY2lvIDIsIHBsYW50YSAwIikgKyB4bGFiKCJMb25naXR1ZCIpICsgeWxhYigiTGF0aXR1ZCIpDQpmMF9iMl9tYXANCg0KZjFfYjJfbWFwIDwtIGdncGxvdChmMV9iMiwgYWVzKHg9TE9OR0lUVURFKSkgKyBnZW9tX3BvaW50KGFlcyh5PUxBVElUVURFLCBjb2xvcj1mYWN0b3IoU1BBQ0VJRCkpKSArIGdndGl0bGUoIkVkaWZpY2lvIDIsIHBsYW50YSAxIikgKyB4bGFiKCJMb25naXR1ZCIpICsgeWxhYigiTGF0aXR1ZCIpDQpmMV9iMl9tYXANCg0KZjJfYjJfbWFwIDwtIGdncGxvdChmMl9iMiwgYWVzKHg9TE9OR0lUVURFKSkgKyBnZW9tX3BvaW50KGFlcyh5PUxBVElUVURFLCBjb2xvcj1mYWN0b3IoU1BBQ0VJRCkpKSArIGdndGl0bGUoIkVkaWZpY2lvIDIsIHBsYW50YSAyIikgKyB4bGFiKCJMb25naXR1ZCIpICsgeWxhYigiTGF0aXR1ZCIpDQpmMl9iMl9tYXANCg0KZjNfYjJfbWFwIDwtIGdncGxvdChmM19iMiwgYWVzKHg9TE9OR0lUVURFKSkgKyBnZW9tX3BvaW50KGFlcyh5PUxBVElUVURFLCBjb2xvcj1mYWN0b3IoU1BBQ0VJRCkpKSArIGdndGl0bGUoIkVkaWZpY2lvIDIsIHBsYW50YSAzIikgKyB4bGFiKCJMb25naXR1ZCIpICsgeWxhYigiTGF0aXR1ZCIpDQpmM19iMl9tYXANCg0KZjRfYjNfbWFwIDwtIGdncGxvdChmNF9iMiwgYWVzKHg9TE9OR0lUVURFKSkgKyBnZW9tX3BvaW50KGFlcyh5PUxBVElUVURFLCBjb2xvcj1mYWN0b3IoU1BBQ0VJRCkpKSArIGdndGl0bGUoIkVkaWZpY2lvIDIsIHBsYW50YSA0IikgKyB4bGFiKCJMb25naXR1ZCIpICsgeWxhYigiTGF0aXR1ZCIpDQpmNF9iM19tYXANCg0KYGBgDQoNCg0KRGUgbG9zIGdyw6FmaWNvcyBhbnRlcmlvcmVzIHNlIHB1ZWRlIHZlciBwb3IgZWplbXBsbyBxdWUgZW4gbGEgw7psdGltYSBwbGFudGEgZGVsIGVkaWZpY2lvIDIgbm8gc2UgdG9tYXJvbiBkYXRvcyBwYXJhIHVuYSBkZSBsYXMgem9uYXMgKGxhIGVzcXVpbmEgaW5mZXJpb3IgZGVyZWNoYSksIGFzaSBjb21vIHRhbXBvY28gZW4gdW5hIHpvbmEgZGUgbGFzIHBsYW50YXMgMCB5IDEgZGVsIGVkaWZpY2lvIDEuDQoNCk51ZXN0cm8gb2JqZXRpdm8gYWhvcmEgZXMgdmVyIHNpIHBvZGVtb3MgcHJlZGVjaXIgbGEgcG9zaWNpw7NuIGRlIHVuYSBwZXJzb25hIHVuYSB2ZXogdGVuZW1vcyBsb3MgZGF0b3MgZGUgbGEgc2XDsWFsIGRlIGxvcyBXYXBzIHJlY2liaWRhLiBQYXJhIGVsbG8gaGVtb3MgcHJvYmFkbyBjb24gZGlzdGludG9zIGFsZ29yaXRtb3MgZGUgbWFjaGluZSBsZWFybmluZyAoS05OLCBTdXBwb3J0IFZlY3RvciBNYWNoaW5lLCBSYW5kb20gRm9yZXN0LCBDNS4wKS4NCkhlbW9zIGVudHJlbmFkbyBlc29zIGFsZ29yaXRtb3MgcGFyYSBwcmVkZWNpciBsYXMgZGlzdGludGFzIHZhcmlhYmxlcyBxdWUgbm9zIHB1ZWRlbiBpbmRpY2FyIGxhIHBvc2ljacOzbi4NCg0KRW4gbnVlc3RybyBjYXNvIGhlbW9zIGRldGVybWluYWRvIHF1ZSBwYXJhIHByZWRlY2lyIGxhcyAzIGNvb3JkZW5hZGFzIGRlIHBvc2ljacOzbiB1c2FyZW1vcyBsYXRpdHVkLCBsb25naXR1ZCB5IHBsYW50YSBkZWwgZWRpZmljaW8uIFRhbWJpw6luIGhlbW9zIGNvbnNpZGVyYWRvIHByZWRlY2lyIGVsIGVkaWZpY2lvIHBvcnF1ZSBwZW5zYW1vcyBxdWUgc2UgcHVlZGUgcHJlZGVjaXIgY29uIHVuYSBhY2N1cmFjeSBkZSBwcsOhY3RpY2FtZW50ZSAxLCB5IHF1ZSBwb2RlbW9zIGluY29ycG9yYXIgbGEgcHJlZGljY2nDs24gZGVsIGVkaWZpY2lvIHBhcmEgcHJlZGVjaXIgbGFzIG90cmFzIHZhcmlhYmxlcywgc2VwYXJhbmRvIGVuIHRyZXMgZ3J1cG9zLiANCg0KDQpQYXJhIGNvbWVuemFyIHZlYW1vcyBjb21vIGhhbiBzaWRvIGVudHJlbmFkb3MgbG9zIG1vZGVsb3MuDQoNCkVudHJlbmFtaWVudG8gcGFyYSBwcmVkZWNpciBlbCBCdWxpZGluZy4gVXNhbW9zIGVsIMOhcmJvbCBkZSBkZWNpc2nDs24gQzUuMC4gUGFyYSBsYSBwcmVkaWNjacOzbiB1c2Ftb3MgdGFuIHNvbG8gbG9zIDMgV0FQUyBtYXMgcmVsZXZhbnRlcyBwYXJhIGNhZGEgcmVnaXN0cm8sIGVzdG8gZXMgZWwgbm9tYnJlIGRlIGxvcyAzIFdBUFMgbWFzIGRldGVjdGFkb3M6DQoNCmBgYHtyfQ0KIyMjIFByZWRpY2Npw7NuIGRlIEJ1aWxkaW5nICMjIw0KDQpidWlsZF9kYXRhIDwtIHRyYWluaW5nRGF0YVssYygxOjUyMCldDQoNCmJ1aWxkX2RhdGFbYnVpbGRfZGF0YT09MF0gPC0gLTEwNQ0KDQpidWlsZF9kYXRhIDwtIGNiaW5kKGJ1aWxkX2RhdGEsIHRyYWluaW5nRGF0YSRCVUlMRElOR0lEKQ0KDQp3YXBzLnJlbGV2YW50ZXMgPC0gZGF0YS5mcmFtZSgpDQpmb3IgKGkgaW4gMTpucm93KGJ1aWxkX2RhdGEpKXsNCiAgdmVjIDwtIGJ1aWxkX2RhdGFbaSwxOjUyMF0gDQogICNwb3NpY2lvbiBkZWwgbWF4IGVuIGVsIHZlY3RvciBidWlsZCBkYXRhIHZhIGEgc2VyIG1pIHByaW1lcmEgY2VsZGEgZGUgY2FkYSByZWdpc3Ryby4gDQogIHJvdy5yZWxldmFudGUgPC0gb3JkZXIodmVjLCBkZWNyZWFzaW5nID0gVClbMTozXQ0KIyAgcm93LnJlbGV2YW50ZSA8LSBvcmRlcih2ZWMsIGRlY3JlYXNpbmcgPSBUKVsxXQ0KICB3YXBzLnJlbGV2YW50ZXMgPC0gcmJpbmQod2Fwcy5yZWxldmFudGVzLCByb3cucmVsZXZhbnRlKQ0KICAgICAgI21pIGRhdGEgc2V0IHZhIGEgc2VyIHJlZ2lzdHJvIHggcmVnaXN0cm8gbG9zIDMgdmFsb3JlcyBtYXhpbW9zIHJlbGFjaW9uYWRvcyBjb24gZWwgd2FwIHF1ZSBsb3MgZGV0ZWN0YQ0KICAgICAgI3ZveSBhIHBlZGlyIGVsICJub21icmUgZGVsIHdhcCIgZGUgZm9ybWEgbnVtZXJpY2EgICAgDQogICAgICANCiAgICB9DQoNCndhcHMucmVsZXZhbnRlcyRCVUlMRElOR0lEIDwtIGJ1aWxkX2RhdGEkYHRyYWluaW5nRGF0YSRCVUlMRElOR0lEYA0Kd2Fwcy5yZWxldmFudGVzJEZMT09SIDwtIHRyYWluaW5nRGF0YSRGTE9PUg0KDQpyZWcuaXJyZWxldmFudGVzIDwtIGMoKSANCmZvciAoaSBpbiAxOm5yb3coYnVpbGRfZGF0YSkpew0KICBpZiAoc3VtKGJ1aWxkX2RhdGFbaSwxOm5jb2woYnVpbGRfZGF0YSktMV0pLyhuY29sKGJ1aWxkX2RhdGEpLTEpID09IC0xMDUpDQogIHJlZy5pcnJlbGV2YW50ZXMgPC0gYyhyZWcuaXJyZWxldmFudGVzLCBpKQ0KfQ0KICANCndhcHMucmVsZXZhbnRlcy5lbnRyZW5vIDwtIHdhcHMucmVsZXZhbnRlc1stcmVnLmlycmVsZXZhbnRlcyxdDQp3YXBzLnJlbGV2YW50ZXMuQlVJTEQgPC0gd2Fwcy5yZWxldmFudGVzLmVudHJlbm9bLGMoMTo0KV0NCg0KDQojIyBFbnRyZW5hbW9zIG1vZGVsbyBjNS4wICMjDQp0aWMoKQ0KI0NhbWJpYW1vcyB2YWxvcmVzIG51bWVyaWNvcyBkZSBidWlsZGluZyBwb3IgbGV0cmFzDQp3YXBzLnJlbGV2YW50ZXMuQlVJTEQkQlVJTERJTkdJRFt3YXBzLnJlbGV2YW50ZXMuQlVJTEQkQlVJTERJTkdJRD09MF0gPC0gImEiDQp3YXBzLnJlbGV2YW50ZXMuQlVJTEQkQlVJTERJTkdJRFt3YXBzLnJlbGV2YW50ZXMuQlVJTEQkQlVJTERJTkdJRD09MV0gPC0gImIiDQp3YXBzLnJlbGV2YW50ZXMuQlVJTEQkQlVJTERJTkdJRFt3YXBzLnJlbGV2YW50ZXMuQlVJTEQkQlVJTERJTkdJRD09Ml0gPC0gImMiDQoNCiNGYWN0b3JpemFtb3MgQlVJTERJTkdJRA0Kd2Fwcy5yZWxldmFudGVzLkJVSUxEJEJVSUxESU5HSUQgPC0gZmFjdG9yKHdhcHMucmVsZXZhbnRlcy5CVUlMRCRCVUlMRElOR0lEKQ0KDQpjb2xuYW1lcyh3YXBzLnJlbGV2YW50ZXMuQlVJTEQpIDwtIGMoIk1heDEiLCJNYXgyIiwiTWF4MyIsIkJVSUxESU5HSUQiKQ0KDQojQ3JlYW1vcyAxMCBmb2xkIGNyb3NzIHZhbGlkYXRpb24NCmZpdENvbnRyb2wgPC0gdHJhaW5Db250cm9sKCMjIDEwLWZvbGQgQ1YNCiAgbWV0aG9kID0gImN2IiwNCiAgY2xhc3NQcm9icyA9IFRSVUUNCikNCg0KI0VudHJlbmFtb3MgbG9zIGRhdG9zDQpzZXQuc2VlZCgxNSkNCkM1X2J1aWxkIDwtIHRyYWluKEJVSUxESU5HSUQgfiAuLCBkYXRhID0gd2Fwcy5yZWxldmFudGVzLkJVSUxELCANCiAgICAgICAgICAgICAgICAgbWV0aG9kID0gIkM1LjAiLCANCiAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gZml0Q29udHJvbCwNCiAgICAgICAgICAgICAgICAgbWV0cmljID0gIlJPQyIsDQogICAgICAgICAgICAgICAgIHByZVByb2MgPSBjKCJjZW50ZXIiLCAic2NhbGUiKQ0KKQ0KDQpgYGANCg0KQSBjb250aW51YWNpw7NuLCBlbnRyZW5hbW9zIGxvcyBtb2RlbG9zIGRlIGxhdGl0dWQsIGxvbmdpdHVkIHkgZmxvb3IgcG9yIGNhZGEgZWRpZmljaW8gcG9yIHNlcGFyYWRvLiBQYXJhIGVsbG8gaGVtb3MgdXNhZG8gZWwgUmFuZG9tIEZvcmVzdCwgcXVlIGRpw7MgbWVqb3IgUk1TRSBlbiBlbCBjYXNvIGRlIGxhdGl0dWQgeSBsb25naXR1ZCB5IG1lam9yIEFjY3VyYWN5IHBhcmEgZWwgcGlzby4NCg0KYGBge3J9DQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkNCmxpYnJhcnkoaHlkcm9HT0YpDQoNCg0KIyMjIExBVElUVURFICMjIw0KDQpsYXRfYjAgPC0gYnVpbGQwWyxjKDE6NTIwLDUyMiw1MjQpXQ0KbGF0X2IxIDwtIGJ1aWxkMVssYygxOjUyMCw1MjIsNTI0KV0NCmxhdF9iMiA8LSBidWlsZDJbLGMoMTo1MjAsNTIyLDUyNCldDQoNCiNSZWFsaXphbW9zIHBhcnRpY2nDs24gZGUgdHJhaW4geSB0ZXN0DQp0aWMoKQ0KDQpSRl9sYXRfYjAgPC0gcmFuZG9tRm9yZXN0KExBVElUVURFIH4gLiwgZGF0YSA9IGxhdF9iMCkNClJGX2xhdF9iMSA8LSByYW5kb21Gb3Jlc3QoTEFUSVRVREUgfiAuLCBkYXRhID0gbGF0X2IxKQ0KUkZfbGF0X2IyIDwtIHJhbmRvbUZvcmVzdChMQVRJVFVERSB+IC4sIGRhdGEgPSBsYXRfYjIpDQoNCnRvYygpDQoNCg0KIyMjIExPTkdJVFVERSAjIyMNCg0KbG9uZ19iMCA8LSBidWlsZDBbLGMoMTo1MjAsNTIxLDUyNCldDQpsb25nX2IxIDwtIGJ1aWxkMVssYygxOjUyMCw1MjEsNTI0KV0NCmxvbmdfYjIgPC0gYnVpbGQyWyxjKDE6NTIwLDUyMSw1MjQpXQ0KDQojUmVhbGl6YW1vcyBwYXJ0aWNpw7NuIGRlIHRyYWluIHkgdGVzdA0KdGljKCkNCg0KUkZfbG9uZ19iMCA8LSByYW5kb21Gb3Jlc3QoTE9OR0lUVURFIH4gLiwgZGF0YSA9IGxvbmdfYjApDQpSRl9sb25nX2IxIDwtIHJhbmRvbUZvcmVzdChMT05HSVRVREUgfiAuLCBkYXRhID0gbG9uZ19iMSkNClJGX2xvbmdfYjIgPC0gcmFuZG9tRm9yZXN0KExPTkdJVFVERSB+IC4sIGRhdGEgPSBsb25nX2IyKQ0KDQp0b2MoKQ0KDQojIyMgRkxPT1IgIyMjDQojVXNhcmVtb3Mgc29sbyBsb3MgNSBXYXBzIG1hcyByZWxldmFudGVzIHBhcmEgZWwgZW50cmVubyB5IGxhIHByZWRpY2Npw7NuDQoNCmZsb29yX2IwIDwtIGJ1aWxkMFssYygxOjUyMCw1MjMsNTI0KV0NCmZsb29yX2IxIDwtIGJ1aWxkMVssYygxOjUyMCw1MjMsNTI0KV0NCmZsb29yX2IyIDwtIGJ1aWxkMlssYygxOjUyMCw1MjMsNTI0KV0NCg0KI0NhbWJpYW1vcyB2YWxvcmVzIG51bUMpcmljb3MgZGUgRmxvb3IgcG9yIHBhbGFicmFzDQpmbG9vcl9iMCRGTE9PUltmbG9vcl9iMCRGTE9PUj09MF0gPC0gImEiDQpmbG9vcl9iMCRGTE9PUltmbG9vcl9iMCRGTE9PUj09MV0gPC0gImIiDQpmbG9vcl9iMCRGTE9PUltmbG9vcl9iMCRGTE9PUj09Ml0gPC0gImMiDQpmbG9vcl9iMCRGTE9PUltmbG9vcl9iMCRGTE9PUj09M10gPC0gImQiDQoNCmZsb29yX2IxJEZMT09SW2Zsb29yX2IxJEZMT09SPT0wXSA8LSAiYSINCmZsb29yX2IxJEZMT09SW2Zsb29yX2IxJEZMT09SPT0xXSA8LSAiYiINCmZsb29yX2IxJEZMT09SW2Zsb29yX2IxJEZMT09SPT0yXSA8LSAiYyINCmZsb29yX2IxJEZMT09SW2Zsb29yX2IxJEZMT09SPT0zXSA8LSAiZCINCg0KZmxvb3JfYjIkRkxPT1JbZmxvb3JfYjIkRkxPT1I9PTBdIDwtICJhIg0KZmxvb3JfYjIkRkxPT1JbZmxvb3JfYjIkRkxPT1I9PTFdIDwtICJiIg0KZmxvb3JfYjIkRkxPT1JbZmxvb3JfYjIkRkxPT1I9PTJdIDwtICJjIg0KZmxvb3JfYjIkRkxPT1JbZmxvb3JfYjIkRkxPT1I9PTNdIDwtICJkIg0KZmxvb3JfYjIkRkxPT1JbZmxvb3JfYjIkRkxPT1I9PTRdIDwtICJlIg0KDQojRmFjdG9yaXphbW9zIEZsb29yDQpmbG9vcl9iMCRGTE9PUiA8LSBmYWN0b3IoZmxvb3JfYjAkRkxPT1IpDQpmbG9vcl9iMSRGTE9PUiA8LSBmYWN0b3IoZmxvb3JfYjEkRkxPT1IpDQpmbG9vcl9iMiRGTE9PUiA8LSBmYWN0b3IoZmxvb3JfYjIkRkxPT1IpDQoNCiNFbnRyZW5hbW9zIG1vZGVsb3MNClJGX2Zsb29yX2IwIDwtIHJhbmRvbUZvcmVzdChGTE9PUiB+IC4sIGRhdGEgPSBmbG9vcl9iMCkNClJGX2Zsb29yX2IxIDwtIHJhbmRvbUZvcmVzdChGTE9PUiB+IC4sIGRhdGEgPSBmbG9vcl9iMSkNClJGX2Zsb29yX2IyIDwtIHJhbmRvbUZvcmVzdChGTE9PUiB+IC4sIGRhdGEgPSBmbG9vcl9iMikNCg0KDQpgYGANCg0KQSBjb250aW51YWNpw7NuIGNyZWFtb3MgdW5hIGZ1bmNpw7NuIHF1ZSBoYWdhIHByZWRpY2Npb25lcyBkZSBmb3JtYSBlc2NhbG9uYWRhLCBkZSBsYXRpdHVkLCBsb25naXR1ZCB5IGZsb29yIGNvbiBsYSBpbmZvcm1hY2lvbiBkZWwgYnVpbGRpbmcsIHVzYW5kbyBsb3MgbW9kZWxvcyBlbnRyZW5hZG9zIHByw6l2aWFtZW50ZToNCg0KYGBge3J9DQojRlVOQ1RJT07CtA0Kd2lmaWxvYyA8LSBmdW5jdGlvbih3YXApew0KICANCiAgaWYgKGxlbmd0aCh3YXApICE9IDUyMCl7DQogICAgc3RvcCgiVGhlIHZlY3RvciBuZWVkcyA1MjAgd2FwcyIpDQogIH0NCiAgDQogIGlmIChjb2xuYW1lcyh3YXApWzFdICE9ICJXQVAwMDEiKXsNCiAgICBzdG9wKCJJbnRyb2R1Y2UgYSBWZWN0b3IgY29udGFpbmluZyBXQVAwMDEgdG8gV0FQNTIwIikNCiAgfQ0KICAjQnVpbGRpbmcNCiAgbGFiZWwgPC0gb3JkZXIod2FwLCBkZWNyZWFzaW5nID0gVClbMTozXQ0KICBiZCA8LSBhcy5kYXRhLmZyYW1lKGxhYmVsKQ0KICBiZCA8LSBhcy5kYXRhLmZyYW1lKHQoYmQpKQ0KICBjb2xuYW1lcyhiZCkgPC0gYygiTWF4MSIsIk1heDIiLCJNYXgzIikNCiAgYmQkQlVJTERJTkdJRCA8LSB2ZWN0b3IobW9kZT0iY2hhcmFjdGVyIiwgbGVuZ3RoID0gMSkNCiAgYmQkQlVJTERJTkdJRCA8LSBwcmVkaWN0KEM1X2J1aWxkLCBiZCkNCiAgDQogIGJkJEJVSUxESU5HSUQgPC0gYXMubnVtZXJpYyhiZCRCVUlMRElOR0lEKSAtIDEgICAgICANCiAgDQogICAgI0xhdGl0dWRlIGJ1aWxkIDANCiAgbHQgPC0gd2FwDQogIGx0JEJVSUxESU5HSUQgPC0gYmQkQlVJTERJTkdJRA0KICBsdF9iMCA8LSBzdWJzZXQobHQsIEJVSUxESU5HSUQ9PTApDQogIGx0X2IxIDwtIHN1YnNldChsdCwgQlVJTERJTkdJRD09MSkNCiAgbHRfYjIgPC0gc3Vic2V0KGx0LCBCVUlMRElOR0lEPT0yKQ0KICBsdCRMQVRJVFVERSA8LSByYmluZChwcmVkaWN0KFJGX2xhdF9iMCwgbHRfYjApLCBwcmVkaWN0KFJGX2xhdF9iMSwgbHRfYjEpLCBwcmVkaWN0KFJGX2xhdF9iMiwgbHRfYjIpKQ0KICANCiAgICNMb25naXR1ZGUNCiAgbGcgPC0gd2FwDQogIGxnJEJVSUxESU5HSUQgPC0gYmQkQlVJTERJTkdJRA0KICBsZ19iMCA8LSBzdWJzZXQobGcsIEJVSUxESU5HSUQ9PTApDQogIGxnX2IxIDwtIHN1YnNldChsZywgQlVJTERJTkdJRD09MSkNCiAgbGdfYjIgPC0gc3Vic2V0KGxnLCBCVUlMRElOR0lEPT0yKQ0KICBsZyRMT05HSVRVREUgPC0gcmJpbmQocHJlZGljdChSRl9sb25nX2IwLCBsZ19iMCksIHByZWRpY3QoUkZfbG9uZ19iMSwgbGdfYjEpLCBwcmVkaWN0KFJGX2xvbmdfYjIsIGxnX2IyKSkNCiAgDQogICAjRmxvb3INCiAgbGFiZWxfMiA8LSBvcmRlcih3YXAsIGRlY3JlYXNpbmcgPSBUKVsxOjVdDQogIGZsIDwtIGFzLmRhdGEuZnJhbWUobGFiZWxfMikNCiAgZmwgPC0gYXMuZGF0YS5mcmFtZSh0KGZsKSkNCiAgY29sbmFtZXMoZmwpIDwtIGMoIk1heDEiLCJNYXgyIiwiTWF4MyIsIk1heDQiLCJNYXg1IikNCiAgZmwkRkxPT1IgPC0gdmVjdG9yKG1vZGU9ImNoYXJhY3RlciIsIGxlbmd0aCA9IDEpDQogIGZsJEJVSUxESU5HSUQgPC0gYmQkQlVJTERJTkdJRA0KICBmbF9iMCA8LSBzdWJzZXQoZmwsIEJVSUxESU5HSUQ9PTApDQogIGZsX2IxIDwtIHN1YnNldChmbCwgQlVJTERJTkdJRD09MSkNCiAgZmxfYjIgPC0gc3Vic2V0KGZsLCBCVUlMRElOR0lEPT0yKQ0KICBpZihucm93KGZsX2IwKSAhPSAwKXsNCiAgICAgZmxfYjAkRkxPT1IgPC0gcHJlZGljdChDNV9mbG9vcl9iMCwgZmxfYjApDQogIH0NCiAgDQogIA0KICBpZihucm93KGZsX2IxKSAhPSAwKXsNCiAgICAgZmxfYjEkRkxPT1IgPC0gcHJlZGljdChDNV9mbG9vcl9iMSwgZmxfYjEpDQogIH0NCiAgDQogIGlmKG5yb3coZmxfYjIpICE9IDApew0KICAgICBmbF9iMiRGTE9PUiA8LSBwcmVkaWN0KEM1X2Zsb29yX2IyLCBmbF9iMikNCiAgfQ0KICBmbCA8LSByYmluZChmbF9iMCxmbF9iMSxmbF9iMikNCiAgIyANCiAgd2FwJExBVElUVURFIDwtIGx0JExBVElUVURFDQogIHdhcCRMT05HSVRVREUgPC0gbGckTE9OR0lUVURFDQogIHdhcCRCVUlMRElOR0lEIDwtIGJkJEJVSUxESU5HSUQNCiANCiAgd2FwJEZMT09SIDwtIGZsJEZMT09SDQogIHdhcCRGTE9PUiA8LSBhcy5udW1lcmljKHdhcCRGTE9PUikgLSAxDQogIA0KICByZXR1cm4od2FwWyw1MjE6bmNvbCh3YXApXSkNCiAgDQp9DQoNCmBgYA0KDQoNCkxsYW1hbW9zIGxhIGZ1bmNpw7NuIGTDoW5kb2xlIGxvcyBkYXRvcyBkZWwgVmFsaWRhdGlvbiBTZXQgeSBoYWNlIHVuYSBwcmVkaWNjacOzbiBwb3IgY2FkYSByZWdpc3RybyBkZWwgZGF0YSBzZXQ6DQpgYGB7cn0NCiNDcmVhbW9zIGRhdGEgZnJhbWUNCnByZWQgPC0gZGF0YS5mcmFtZShMQVRJVFVERSA9IGFzLm51bWVyaWMoKSwgTE9OR0lUVURFID0gYXMubnVtZXJpYygpLEJVSUxESU5HSUQgPSBhcy5udW1lcmljKCksRkxPT1I9IGFzLm51bWVyaWMoKSkNCg0KI0hhY2Vtb3MgbG9vcCBwYXJhIHRvZG9zIGxvcyBkYXRvcyB5IGxsYW1hbW9zIGxhIGZ1bmNpw7NuIGEgY2FkYSBmaWxhDQpmb3IoaSBpbiAxOjExMTEpew0KcHJlZFtpLF0gPC0gd2lmaWxvYyh2YWxpZGF0aW9uX3NldCBbaSwxOjUyMF0pDQp9DQpgYGANCg0KQ2FsY3VsYW1vcyBsb3MgZXJyb3Jlc20gYXNvY2lhZG9zIGEgbGEgcHJlZGljY2nDs24gZGUgY2FkYSB2YXJpYWJsZToNCmBgYHtyfQ0KI0J1aWxkaW5nDQpwcmVkJEJVSUxEUkVBTCA8LSB2YWxpZGF0aW9uX3NldCRCVUlMRElOR0lEWzE6MTExMV0NCnByZWQkRVJST1JCSUQgPC0gcHJlZCRCVUlMRElOR0lEIC0gcHJlZCRCVUlMRFJFQUwNCm1lYW4ocHJlZCRFUlJPUkJJRCwgbmEucm09VCkNCmJ1aWxkTUFFIDwtIG1lYW4oYWJzKHByZWQkRVJST1JCSUQpLCBuYS5ybT1UKQ0KYnVpbGRfY29uZnVzaW9uIDwtIGNvbmZ1c2lvbk1hdHJpeChkYXRhPXByZWQkQlVJTERJTkdJRCwgdmFsaWRhdGlvbl9zZXQkQlVJTERJTkdJRCkNCmJ1aWxkX2NvbmZ1c2lvbg0KDQojTGF0aXR1ZA0KcHJlZCRMQVRSRUFMIDwtIHZhbGlkYXRpb25fc2V0JExBVElUVURFWzE6MTExMV0NCnByZWQkRVJST1JMQVQgPC0gcHJlZCRMQVRJVFVERSAtIHByZWQkTEFUUkVBTA0KbWVhbihwcmVkJEVSUk9STEFULCBuYS5ybT1UKQ0KbWVhbihhYnMocHJlZCRFUlJPUkxBVCksIG5hLnJtPVQpDQpybXNlX2xhdCA8LSBybXNlKHByZWQkTEFUSVRVREUsIHByZWQkTEFUUkVBTCkNCmxhdGl0dWRlUlNRIDwtIDEgLSAoc3VtKChwcmVkJExBVFJFQUwtcHJlZCRMQVRJVFVERSleMikvc3VtKChwcmVkJExBVFJFQUwtbWVhbihwcmVkJExBVFJFQUwpKV4yKSkNCg0KDQojTG9uZ2l0dWQNCnByZWQkTE9OR1JFQUwgPC0gdmFsaWRhdGlvbl9zZXQkTE9OR0lUVURFWzE6MTExMV0NCnByZWQkRVJST1JMT05HIDwtIHByZWQkTE9OR0lUVURFIC0gcHJlZCRMT05HUkVBTA0KbWVhbihwcmVkJEVSUk9STE9ORywgbmEucm09VCkNCm1lYW4oYWJzKHByZWQkRVJST1JMT05HKSwgbmEucm09VCkNCnJtc2VfbG9uZyA8LSBybXNlKHByZWQkTE9OR0lUVURFLCBwcmVkJExPTkdSRUFMKQ0KbG9uZ2l0dWRlUlNRIDwtIDEgLSAoc3VtKChwcmVkJExPTkdSRUFMLXByZWQkTE9OR0lUVURFKV4yKS9zdW0oKHByZWQkTE9OR1JFQUwtbWVhbihwcmVkJExPTkdSRUFMKSleMikpDQoNCiNGbG9vcg0KcHJlZCRGTE9PUlJFQUwgPC0gdmFsaWRhdGlvbl9zZXQkRkxPT1JbMToxMTExXQ0KcHJlZCRFUlJPUkZMT09SIDwtIHByZWQkRkxPT1IgLSBwcmVkJEZMT09SUkVBTA0KbWVhbihwcmVkJEVSUk9SRkxPT1IsIG5hLnJtPVQpDQpmbG9vcl9jb25mdXNpb24gPC0gY29uZnVzaW9uTWF0cml4KGRhdGE9cHJlZCRGTE9PUiwgdmFsaWRhdGlvbl9zZXQkRkxPT1IpDQpmbG9vcl9jb25mdXNpb24NCmBgYA0KDQpDb21vIHBvZGVtb3MgdmVyIGFudGVyaW9ybWVudGUsIHBhcmEgbGEgcHJlZGljY2nDs24gZGVsIGJ1aWxkaW5nIG9idGVuZW1vcyB1bmEgQWNjdXJhY3kgZGUgcHLDoWN0aWNhbWVudGUgMSwgZXJyYW5kbyBzb2xvIGVuIDggY2Fzb3MgZGUgMTExMS4gUGFyYSBsYSBwcmVkaWNjacOzbiBkZWwgZmxvb3IgbGEgQWNjdXJhY3kgeWEgZXMgbWFzIGJhamEsIGRlIDAsOTE1NC4gRXN0ZSBlcnJvciBwdWVkZSBzZXIgZGViaWRvIGEgbGEgY29uZnVzacOzbiBxdWUgc2UgcHVlZGUgcHJvZHVjaXIgZW4gcHVudG9zIGNlcmNhbm9zIGEgaHVlY29zIGRlIGVzY2FsZXJhIG8gYXNjZW5zb3IsIGRvbmRlIGxhIHNlw7FhbCBkZSBsb3MgV0FQUyBkZSBwaXNvcyBzaWd1aWVudGVzIHB1ZWRlIHNlciBiaWVuIHJlY2liaWRhLg0KDQpSZXByZXNlbnRhbW9zIGdyw6FmaWNhbWVudGUgbGFzIGRpc3RyaWJ1Y2lvbmVzIGRlIGVycm9yIHBhcmEgbGEgbGF0aXR1ZCB5IGxhIGxvbmdpdHVkLiBPYnNlcnZhbW9zIHRhbWJpw6luIGVsIFJNU0UgZGUgYW1iYXMgY29vcmRlbmFkYXMuICANCg0KYGBge3J9DQpsaWJyYXJ5KGdncGxvdDIpDQojSGlzdG9ncmFtYXMNCmhpc3QocHJlZCRFUlJPUkxBVCwgDQogICAgIGJyZWFrcz02MCwgDQogICAgIGZyZXE9VFJVRSwNCiAgICAgeGxpbSA9IGMoLTYwLDYwKSwNCiAgICAgY29sPSIjMGI4N2ExIiwNCiAgICAgbWFpbj0iRGlzdHJpYnVjacOzbiBkZSBlcnJvciBlbiBsYSBsYXRpdHVkIiwNCiAgICAgeGxhYj0iRXJyb3IgbGF0aXR1ZCIsDQogICAgIHlsYWI9IkZyZWN1ZW5jaWEiDQopDQoNCmhpc3QocHJlZCRFUlJPUkxPTkcsIA0KICAgICBicmVha3M9MTIwLCANCiAgICAgZnJlcT1UUlVFLA0KICAgICB4bGltID0gYygtNjAsNjApLA0KICAgICBjb2w9IiMwYjg3YTEiLA0KICAgICBtYWluPSJEaXN0cmlidWNpw7NuIGRlIGVycm9yIGVuIGxhIGxvbmdpdHVkIiwNCiAgICAgeGxhYj0iRXJyb3IgbG9uZ2l0dWQiLA0KICAgICB5bGFiPSJGcmVjdWVuY2lhIg0KKQ0KDQoNCnJtc2VfbG9uZw0Kcm1zZV9sYXQNCmBgYA0KDQpFbCBSTVNFIGRlIGxhIGxhdGl0dWQgZXMgMTEsNzUgeSBlbCBkZSBsYSBsb25naXR1ZCBlcyAxNywwMi4gVGllbmUgc2VudGlkbyBxdWUgZWwgZGUgbGEgbG9uZ2l0dWQgc2VhIG1heW9yIHB1ZXN0byBxdWUgZWwgY29uanVudG8gZGUgbG9zIGVkaWZpY2lvcyBlcyBtYXMgZXh0ZW5zbyBlbiBlbCBzZW50aWRvIGRlIGNvb3JkZW5hZGFzIGRlIGxhIGxvbmdpdHVkLiBVbmEgZGUgbGFzIGZ1ZW50ZXMgZGVsIGVycm9yIHB1ZWRlIHNlciBxdWUgZW4gZWwgVHJhaW5pbmcgU2V0IHNlIGhhbiB0b21hZG8gbG9zIGRhdG9zIGRlIGZvcm1hIGNvbnRyb2xhZGEsIGVuIHB1bnRvcyBjb25jcmV0b3MsIHkgbXVjaGFzIHZlY2VzIGVuIGxvcyBtaXNtb3MgcHVudG9zLCBtaWVudHJhcyBxdWUgZW4gZWwgVmFsaWRhdGlvbiBTZXQgbGFzIG1lZGlkYXMgc29uIHRvbWFkYXMgZGUgZm9ybWEgYWxlYXRvcmlhLiBBZGVtw6FzIGVsIFRyYWluaW5nIFNldCBzZSBkZWphIHpvbmFzIGRlIGFsZ3VuIGVkaWZpY2lvIHNpbiBjdWJyaXIgY29uIGxvIGN1w6FsIGVuIGVzYXMgem9uYXMgZWwgZXJyb3IgZXMgbWF5b3IuIFRhbWJpw6luLCBjb21vIHBvZGVtb3MgdmVyIGEgY29udGludWFjacOzbiBzZSBvYnNlcnZhIHF1ZSBlcyBlbiBsYXMgZXNxdWluYXMgZG9uZGUgc2UgcHJvZHVjZW4gbWFzIGVycm9yZXMsIHByb2JhYmxlbWVudGUgZGViaWRvIGEgbGEgZGlzcGVyc2nDs24gcXVlIHByb3ZvY2FuIGxhcyBwYXJlZGVzIGRlIMOpc3RhcywgaGFjaWVuZG8gcXVlIGxhIHNlbnlhbCBkZSBsb3MgV2FwcyBkZSB1bmEgZGlyZWNjacOzbiBxdWVkZSBhdGVudWFkYSB5IGxhIHRyaW5hZ3VsYWNpw7NuIG5vIHB1ZWRhIHNlciB0YW4gcHJlY2lzYS4gDQoNCmBgYHtyfQ0Kb3V0bGllcnNfTEFUIDwtIHN1YnNldChwcmVkLCBFUlJPUkxBVCA+IDE1KQ0KDQpwX0xBVCA8LSBnZ3Bsb3QoKSArIA0KICBnZW9tX3BvaW50KGRhdGE9cHJlZCwgYWVzKHg9TE9OR1JFQUwsIHk9TEFUUkVBTCksIGNvbG9yPSJncmV5Iiwgc2l6ZT0xKSArDQogIGdlb21fcG9pbnQoZGF0YT1vdXRsaWVyc19MQVQsYWVzKHg9TE9OR1JFQUwsIHk9TEFUUkVBTCksIGNvbG9yPSIjMDAzYjU1Iiwgc2l6ZT0yKSArDQogIGdndGl0bGUoIk91dGxpZXJzIGxhdGl0dWQiKSArIHhsYWIoIkxvbmdpdHVkIikgKyB5bGFiKCJMYXRpdHVkIikNCnBfTEFUDQoNCm91dGxpZXJzX0xPTkcgPC0gc3Vic2V0KHByZWQsIEVSUk9STE9ORyA+IDIwKQ0KDQpwX0xPTkcgPC0gZ2dwbG90KCkgKyANCiAgZ2VvbV9wb2ludChkYXRhPXByZWQsIGFlcyh4PUxPTkdSRUFMLCB5PUxBVFJFQUwpLCBjb2xvcj0iZ3JleSIsIHNpemU9MSkgKw0KICBnZW9tX3BvaW50KGRhdGE9b3V0bGllcnNfTE9ORyxhZXMoeD1MT05HUkVBTCwgeT1MQVRSRUFMKSwgY29sb3I9IiMwMDNiNTUiLCBzaXplPTIpICsNCiAgZ2d0aXRsZSgiT3V0bGllcnMgbG9uZ2l0dWQiKSArIHhsYWIoIkxvbmdpdHVkIikgKyB5bGFiKCJMYXRpdHVkIikNCnBfTE9ORw0KDQpwIDwtICBnZ3Bsb3QoKSArIA0KICBnZW9tX3BvaW50KGRhdGE9cHJlZCwgYWVzKHg9TE9OR1JFQUwsIHk9TEFUUkVBTCksIGNvbG9yPSJncmV5Iiwgc2l6ZT0xKSArDQogIGdlb21fcG9pbnQoZGF0YT1vdXRsaWVyc19MT05HLGFlcyh4PUxPTkdSRUFMLCB5PUxBVFJFQUwpLCBjb2xvcj0iIzAwM2I1NSIsIHNpemU9MikgKw0KICBnZ3RpdGxlKCJPdXRsaWVycyBsb25naXR1ZCIpICsgeGxhYigiTG9uZ2l0dWQiKSArIHlsYWIoIkxhdGl0dWQiKSArDQogIGdlb21fcG9pbnQoZGF0YT1wcmVkLCBhZXMoeD1MT05HUkVBTCwgeT1MQVRSRUFMKSwgY29sb3I9ImdyZXkiLCBzaXplPTEpICsNCiAgZ2VvbV9wb2ludChkYXRhPW91dGxpZXJzX0xBVCxhZXMoeD1MT05HUkVBTCwgeT1MQVRSRUFMKSwgY29sb3I9IiMwMDNiNTUiLCBzaXplPTIpDQpgYGANCg0KDQpGaW5hbG1lbnRlIHF1aWVybyBjb21lbnRhciBhbGd1bmFzIG9wY2lvbmVzIHBhcmEgbWVqb3JhciBlbCBtb2RlbG8uIExhIHByaW1lcmEgZXMgbm9ybWFsaXphciBwb3IgZmlsYXMgbGEgc2XDsWFsIGRlIGxvcyBXYXBzIHBhcmEgcHJlZGVjaXIgbGEgbGF0aXR1ZCB5IGxhIGxvbmdpdHVkLCBwdWVzdG8gcXVlIGFzw60gc2UgcGllcmRlIHZhcmlhYmlsaWRhZCB5IHNlIGVxdWlwYXJhbiBsb3MgdmFsb3JlcyBtw6F4aW1vcyByZWNpYmlkb3MgZW4gY2FkYSByZWdpc3RybyBzaSBzb24gcmVjaWJpZG9zIHBvciBlbCBtaXNtbyBXQVAuIE90cmEgb3BjacOzbiBlcyBpbmNvcnBvcmFyIGRhdG9zIGRlbCBWYWxpZGF0aW9uIFNldCBwYXJhIGVudHJlbmFyIGxvcyBtb2RlbG9zLCBjdWJyaWVuZG8gY29uIMOpc3RlIGxhcyB6b25hcyBxdWUgbm8gc2UgY3VicmVuIGNvbiBlbCBUcmFpbmluZyBTZXQuIE90cmEgb3BjacOzbiBzZXLDrWEgcHJvYmFyIG90cm9zIGFsZ29yaXRtb3MgY29tbyBlbCBLTk4sIHF1ZSBzaSBiaWVuIG5vcyBkYW4gdW4gUk1TRSBtYXlvciBwb3JxdcOpIGxvcyBvdXRsaWVycyBzb24gbWF5b3JlcywgdGFtYmnDqW4gcHJlZGljZSBtYXMgY2FudGlkYWQgZGUgcHVudG9zIGNvcnJlY3RhbWVudGUsIHNpZW5kbyBsYSBkc2l0cmlidWNpw7NuIGRlIGVycm9yIGFudGVzIGV4cHVlc3RhIG1hcyBlc3RyZWNoYSB5IGFsdGEgZW4gZWwgbWVkaW8uIFNlIHBvZHLDrWFuIGNvbWJpbmFyIGFtYm9zIGFsZ29yaXRtb3MsIHVzYW5kbyBwcmltZXJvIGVsIEtOTiB5IGRlc3B1ZXMgdXNhbmRvIGVsIFJhbmRvbSBGb3Jlc3QgcGFyYSBsYSBwcmVkaWNjacOzbiBkZSBlc29zIHB1bnRvcyBxdWUgc2VhbiBtdXkgbWFsIHByZWRpY2hvcyBjb24gZWwgS05OLg0KDQoNCg==