Support Vector Machines

Se utiliza la libreria e1071 en R para demostrar el clasificador de vectores de soporte y el SVM.

9.6.1 Support Vector Classifier

En particular la función svm() es un clasificador de vectores de soporte cuando se utilice el argumento kernel=“linear”. Un argumento de costos nos permite especificar el costo de violación al margen, cuando el argumento de costo es pequeño, entonces los margenes serán anchos.

set.seed (1)
x=matrix (rnorm (20*2) , ncol =2)
y=c(rep (-1,10) , rep (1 ,10) )
x[y==1 ,]= x[y==1,] + 1

Se verifica si las clases son linealmente separables por medio de la gráfica.

plot(x, col =(3-y))

Se pudo comprobar que no son linealmente separables, luego se ajusta el clasificador de vectores de soporte.

library (e1071)
package 㤼㸱e1071㤼㸲 was built under R version 3.5.3
dat=data.frame(x=x, y=as.factor (y))
svmfit =svm(y∼., data=dat , kernel ="linear", cost =10,scale =FALSE )

El argumento scale = FALSE significa que no escale cada caracteristica para que tenga una media de cero o una desviacion estandar de uno; Dependiendo de la aplicacion, uno podria preferir usar scale = TRUE

dat=data.frame(x=x, y=as.factor (y))
svmfit =svm(y∼., data=dat , kernel ="linear", cost =10,scale = TRUE )

Podemos trazar el clasificador de vectores de soporte obtenido.

plot(svmfit , dat)

La region del espacio de la caracteristica que se asignara a la clase −1 se muestra en azul claro, y la region que se asignara a la clase +1 se muestra en purpura. El límite de decision entre las dos clases es lineal (porque usamos el argumento kernel = “linear”), aunque debido a la forma en que se implementa la funcion de trazado en esta libreria, el limite de decision parece algo irregular en el trazado. Determinar las identidades de los vectores de la siguiente forma:

svmfit$index
[1]  1  2  5  7 14 16 17

Obtener información sobre el ajuste del clasificador de vectores de soporte usando summary()

summary (svmfit)

Call:
svm(formula = y ~ ., data = dat, kernel = "linear", cost = 10, scale = TRUE)


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  linear 
       cost:  10 

Number of Support Vectors:  7

 ( 4 3 )


Number of Classes:  2 

Levels: 
 -1 1

De lo anterior podemos decir que se uso un kernel lineal con el costo = 10, y que habia siete vectores de soporte, cuatro en una clase y tres en la otra.

svmfit =svm(y∼., data=dat , kernel ="linear", cost = 0.1,
scale =FALSE )
plot(svmfit , dat)

svmfit$index
 [1]  1  2  3  4  5  7  9 10 12 13 14 15 16 17 18 20

La funcion tune() permite realizar cruces de validacion, en forma predeterminada realiza una validacion cruzada de diez veces en un conjunto de modelos de interes. Para utilizar esta funcion se transfiere informacion relevante del conjunto de modelos que se esta considerando.

set.seed (1)
tune.out=tune(svm ,y∼.,data=dat ,kernel ="linear",
ranges =list(cost=c(0.001 , 0.01, 0.1, 1,5,10,100)))

Accedemos facilmente a los errores de validacion cruzada para cada uno de estos modelos usando summary():

summary (tune.out)

Parameter tuning of ‘svm’:

- sampling method: 10-fold cross validation 

- best parameters:

- best performance: 0.1 

- Detailed performance results:
NA

Ahora se puede ver que al utilizar cost = 0.1 se obtiene el error más pequeño de cross-validation. Por lo tanto la función tune() puede obtener el mejor modelo, al cual se puede acceder:

bestmod =tune.out$best.model
summary (bestmod)

Call:
best.tune(method = svm, train.x = y ~ ., data = dat, ranges = list(cost = c(0.001, 
    0.01, 0.1, 1, 5, 10, 100)), kernel = "linear")


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  linear 
       cost:  0.1 

Number of Support Vectors:  16

 ( 8 8 )


Number of Classes:  2 

Levels: 
 -1 1

La función predict() se puede usar para predecir la etiqueta de clase en un conjunto de observaciones de prueba, en cualquier valor dado del parametro de costo. Ahora generaremos un conjunto de datos de prueba.

xtest=matrix (rnorm (20*2) , ncol =2)
ytest=sample (c(-1,1) , 20, rep=TRUE)
xtest[ytest ==1 ,]= xtest[ytest ==1,] + 1
testdat =data.frame (x=xtest , y=as.factor (ytest))

Predecimos las etiquetas de clase de estas observaciones de prueba. Aquí utilizamos el mejor modelo obtenido mediante validación cruzada para hacer predicciones.

ypred=predict (bestmod ,testdat )
table(predict =ypred , truth= testdat$y )
       truth
predict -1  1
     -1 11  1
     1   0  8

Por lo tanto, con este valor de costo, hay 19 observaciones de prueba que estan correctamente clasificadas. ¿Qué pasaría si hubiésemos utilizado en su lugar el costo = 0.01?

svmfit =svm(y∼., data=dat , kernel ="linear", cost =.01,
scale =FALSE )
ypred=predict (svmfit ,testdat )
Error in eval(predvars, data, env) : object 'x.3' not found

Podemos encontrar un hiperplano de separación usando la función svm(). Primero separamos las dos clases en nuestros datos simulados para que sean linealmente separables:

x[y==1 ,]= x[y==1 ,]+0.5
plot(x, col =(y+5) /2, pch =19)

Ajustamos el clasificador de vectores de soporte y trazamos el hiperplano resultante, utilizando un valor de costo muy grande para que no se clasifiquen erróneamente las observaciones.

dat=data.frame(x=x,y=as.factor (y))
svmfit =svm(y∼., data=dat , kernel ="linear", cost =1e5)
summary (svmfit )

Call:
svm(formula = y ~ ., data = dat, kernel = "linear", cost = 1e+05)


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  linear 
       cost:  1e+05 

Number of Support Vectors:  3

 ( 1 2 )


Number of Classes:  2 

Levels: 
 -1 1

No se cometieron errores de entrenamiento y solo se usaron tres vectores de soporte. Sin embargo, podemos ver en la figura que el margen es muy estrecho (porque las observaciones que no son vectores de soporte, indicadas como círculos, están muy cerca del límite de decision). Parece probable que este modelo se desempeñe mal en los datos de prueba. Ahora intentamos un menor valor de costo:

svmfit =svm(y∼., data=dat , kernel ="linear", cost =1)
summary (svmfit )

Call:
svm(formula = y ~ ., data = dat, kernel = "linear", cost = 1)


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  linear 
       cost:  1 

Number of Support Vectors:  7

 ( 4 3 )


Number of Classes:  2 

Levels: 
 -1 1
plot(svmfit ,dat )

Usando cost = 1, clasificamos erroneamente una observacion de entrenamiento, pero tambien obtenemos un margen mucho mas amplio y utilizamos siete vectores de soporte. Parece probable que este modelo tenga un mejor desempeño en los datos de prueba que el modelo con cost = 1e5.

9.6.2 Support Vector Machine

Para ajustar un SVM usando un kernel no lineal, una vez mas usamos la funcion svm(). Sin embargo, ahora usamos un valor diferente del parámetro kernel. Para ajustar una SVM con un núcleo polinomial usamos kernel = “polinomial”, y para ajustar una SVM con un núcleo radial usamos kernel = “radial”.

set.seed (1)
x=matrix (rnorm (200*2) , ncol =2)
x[1:100 ,]=x[1:100 ,]+2
x[101:150 ,]= x[101:150 ,] -2
y=c(rep (1 ,150) ,rep (2 ,50) )
dat=data.frame(x=x,y=as.factor (y))

El trazado de los datos deja claro que el limite de la clase es no lineal:

plot(x, col=y)

Los datos se dividen aleatoriamente en grupos de train y test. Entonces encajamos los datos del train utilizando la función svm () con un núcleo radial y γ = 1:

train=sample (200 ,100)
svmfit =svm(y∼., data=dat [train ,], kernel ="radial", gamma =1,
cost =1)
plot(svmfit , dat[train ,])

La grafica muestra que el SVM resultante tiene un limite no lineal. La funcion de summary() se puede utilizar para obtener alguna informacion sobre el ajuste SVM:

summary (svmfit )

Call:
svm(formula = y ~ ., data = dat[train, ], kernel = "radial", gamma = 1, cost = 1)


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  radial 
       cost:  1 

Number of Support Vectors:  37

 ( 17 20 )


Number of Classes:  2 

Levels: 
 1 2

Podemos ver en la figura que hay un buen numero de errores de entrenamiento en este ajuste de SVM. Si aumentamos el valor del costo, podemos reducir el número de errores de capacitacion.

svmfit =svm(y∼., data=dat [train ,], kernel ="radial",gamma =1,
cost=1e5)
plot(svmfit ,dat [train ,])

Podemos realizar una validación cruzada utilizando tune() para seleccionar la mejor opción de γ y el costo de una SVM con un núcleo radial:

set.seed (1)
tune.out=tune(svm , y∼., data=dat[train ,], kernel ="radial",
              ranges =list(cost=c(0.1 ,1 ,10 ,100 ,1000),
                           gamma=c(0.5,1,2,3,4) ))
summary (tune.out)

Parameter tuning of ‘svm’:

- sampling method: 10-fold cross validation 

- best parameters:

- best performance: 0.12 

- Detailed performance results:
NA

Podemos ver las predicciones del conjunto de pruebas para este modelo aplicando la funcion predict() a los datos. Tener en cuenta que para hacer esto, subcontratamos los datos del marco de datos utilizando -train como un conjunto de indices.

table(true=dat[-train ,"y"], pred=predict (tune.out$best.model ,
newx=dat[-train ,]))
    pred
true  1  2
   1 56 21
   2 18  5

39% de las observaciones de prueba estan mal clasificadas por este SVM.

9.6.3 ROC Curves

El paquete ROCR se puede utilizar para producir curvas ROC.

library (ROCR)
package 㤼㸱ROCR㤼㸲 was built under R version 3.5.3Loading required package: gplots
package 㤼㸱gplots㤼㸲 was built under R version 3.5.3
Attaching package: 㤼㸱gplots㤼㸲

The following object is masked from 㤼㸱package:stats㤼㸲:

    lowess
rocplot =function (pred , truth , ...){
   predob = prediction (pred , truth )
   perf = performance (predob , "tpr", "fpr")
   plot(perf ,...)}

Para una SVM con un kernel no lineal. En esencia, el signo del valor ajustado determina en que lado del limite de decision se encuentra la observacion. Por lo tanto, la relacion entre el valor ajustado y la prediccion de clase para una observacion dada es simple: si el valor ajustado excede de cero, la observacion se asigna a una clase y si es menor que cero se asigna a la otra.

svmfit.opt=svm(y∼., data=dat[train ,], kernel ="radial",
gamma =2, cost=1, decision.values =T)
fitted =attributes (predict (svmfit.opt ,dat[train ,], decision.values =TRUE))$decision.values
par(mfrow =c(1,2))
rocplot (fitted ,dat [train ,"y"], main="Training Data")
Error in prediction(pred, truth) : 
  Number of cross-validation runs must be equal for predictions and labels.

Al aumentar γ podemos producir un ajuste mas flexible y generar mas mejoras en la precision.

svmfit.flex <- svm(y ~ ., data = dat[train,], kernel = "radial", gamma = 50, cost = 1, decision.values = T)
fitted <- attributes(predict(svmfit.flex, dat[train,], decision.values = T))$decision.values
rocplot(fitted, dat[train,"y"], col = "red")

Interesa mas el nivel de precision de prediccion en los datos de prueba. Cuando calculamos las curvas ROC en los datos de prueba, el modelo con γ = 2 parece proporcionar los resultados más precisos.

fitted =attributes (predict (svmfit.opt ,dat[-train ,], decision.values =T))$decision.values
rocplot (fitted ,dat [-train ,"y"], main ="Test Data")
fitted =attributes (predict (svmfit.flex ,dat[-train ,], decision.values =T))$decision.values
rocplot (fitted ,dat [-train ,"y"], add=T,col ="red ")

9.6.4 SVM with Multiple Classes

La función svm() realizara una clasificacion de multiples clases utilizando el enfoque de uno contra uno. Se genera una tercera clase de observaciones.

set.seed (1)
x=rbind(x, matrix (rnorm (50*2) , ncol =2))
y=c(y, rep (0 ,50) )
x[y==0 ,2]= x[y==0 ,2]+2
dat=data.frame(x=x, y=as.factor (y))
par(mfrow =c(1,1))
plot(x,col =(y+1))

Ajustamos un SVM a los datos:

svmfit =svm(y∼., data=dat , kernel ="radial", cost =10, gamma =1)
plot(svmfit , dat)

9.6.5 Application to Gene Expression Data

Ahora examinamos el conjunto de datos de Khan, que consiste en una serie de muestras de tejido que corresponden a cuatro tipos distintos de tumores pequeños redondos de células azules. Para cada muestra de tejido, las mediciones de expresion genica estan disponibles.

library (ISLR)
names(Khan)
[1] "xtrain" "xtest"  "ytrain" "ytest" 
dim( Khan$xtrain )
[1]   63 2308
dim( Khan$xtest )
[1]   20 2308
length (Khan$ytrain )
[1] 63
length (Khan$ytest )
[1] 20

Este conjunto de datos consiste en mediciones de expresion para 2,308 genes. Los conjuntos de entrenamiento y prueba constan de 63 y 20 observaciones de forma espectativa.

table(Khan$ytrain )

 1  2  3  4 
 8 23 12 20 
table(Khan$ytest )

1 2 3 4 
3 6 6 5 

En este conjunto de datos, hay una gran cantidad de caracteristicas en relacion con la cantidad de observaciones. Esto sugiere que deberiamos usar un nucleo lineal, porque la flexibilidad adicional que resultara del uso de un nucleo polinomial o radial es innecesaria.

dat=data.frame(x=Khan$xtrain , y=as.factor ( Khan$ytrain ))
out=svm(y∼., data=dat , kernel ="linear",cost =10)
summary (out)

Call:
svm(formula = y ~ ., data = dat, kernel = "linear", cost = 10)


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  linear 
       cost:  10 

Number of Support Vectors:  58

 ( 20 20 11 7 )


Number of Classes:  4 

Levels: 
 1 2 3 4
table(out$fitted , dat$y)
   
     1  2  3  4
  1  8  0  0  0
  2  0 23  0  0
  3  0  0 12  0
  4  0  0  0 20

No hay errores de entrenamiento. De hecho, esto no es sorprendente, debido a que la gran cantidad de variables en relacion con la cantidad de observaciones implica que es facil encontrar hiperplanos que separan las clases por completo.

dat.te=data.frame(x=Khan$xtest , y=as.factor (Khan$ytest ))
pred.te=predict (out , newdata =dat.te)
table(pred.te , dat.te$y)
       
pred.te 1 2 3 4
      1 3 0 0 0
      2 0 6 2 0
      3 0 0 4 0
      4 0 0 0 5

Usar cost = 10 produce dos errores de conjunto de prueba en estos datos.

LS0tDQp0aXRsZTogIkNhcCAjIDksIEZJQUJJTElEQUQgLSBPc2NhciBQYWRpbGxhIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KI1N1cHBvcnQgVmVjdG9yIE1hY2hpbmVzDQoNClNlIHV0aWxpemEgbGEgbGlicmVyaWEgZTEwNzEgZW4gUiBwYXJhIGRlbW9zdHJhciBlbCBjbGFzaWZpY2Fkb3IgZGUgdmVjdG9yZXMgZGUgc29wb3J0ZSB5IGVsIFNWTS4NCg0KDQojIyA5LjYuMSBTdXBwb3J0IFZlY3RvciBDbGFzc2lmaWVyDQoNCkVuIHBhcnRpY3VsYXIgbGEgZnVuY2nDs24gc3ZtKCkgZXMgdW4gY2xhc2lmaWNhZG9yIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUgY3VhbmRvIHNlIHV0aWxpY2UgZWwgYXJndW1lbnRvIGtlcm5lbD0ibGluZWFyIi4gVW4gYXJndW1lbnRvIGRlIGNvc3RvcyBub3MgcGVybWl0ZSBlc3BlY2lmaWNhciBlbCBjb3N0byBkZSB2aW9sYWNpw7NuIGFsIG1hcmdlbiwgY3VhbmRvIGVsIGFyZ3VtZW50byBkZSBjb3N0byBlcyBwZXF1ZcOxbywgZW50b25jZXMgbG9zIG1hcmdlbmVzIHNlcsOhbiBhbmNob3MuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQgKDEpDQp4PW1hdHJpeCAocm5vcm0gKDIwKjIpICwgbmNvbCA9MikNCnk9YyhyZXAgKC0xLDEwKSAsIHJlcCAoMSAsMTApICkNCnhbeT09MSAsXT0geFt5PT0xLF0gKyAxDQpgYGANCg0KU2UgdmVyaWZpY2Egc2kgbGFzIGNsYXNlcyBzb24gbGluZWFsbWVudGUgc2VwYXJhYmxlcyBwb3IgbWVkaW8gZGUgbGEgZ3LDoWZpY2EuDQoNCmBgYHtyfQ0KcGxvdCh4LCBjb2wgPSgzLXkpKQ0KYGBgDQoNClNlIHB1ZG8gY29tcHJvYmFyIHF1ZSBubyBzb24gbGluZWFsbWVudGUgc2VwYXJhYmxlcywgbHVlZ28gc2UgYWp1c3RhIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlLiANCg0KYGBge3J9DQpsaWJyYXJ5IChlMTA3MSkNCmRhdD1kYXRhLmZyYW1lKHg9eCwgeT1hcy5mYWN0b3IgKHkpKQ0Kc3ZtZml0ID1zdm0oeeKIvC4sIGRhdGE9ZGF0ICwga2VybmVsID0ibGluZWFyIiwgY29zdCA9MTAsc2NhbGUgPUZBTFNFICkNCmBgYA0KDQpFbCBhcmd1bWVudG8gc2NhbGUgPSBGQUxTRSBzaWduaWZpY2EgcXVlIG5vIGVzY2FsZSBjYWRhIGNhcmFjdGVyaXN0aWNhIHBhcmEgcXVlIHRlbmdhIHVuYSBtZWRpYSBkZSBjZXJvIG8gdW5hIGRlc3ZpYWNpb24gZXN0YW5kYXIgZGUgdW5vOyBEZXBlbmRpZW5kbyBkZSBsYSBhcGxpY2FjaW9uLCB1bm8gcG9kcmlhIHByZWZlcmlyIHVzYXIgc2NhbGUgPSBUUlVFDQoNCmBgYHtyfQ0KZGF0PWRhdGEuZnJhbWUoeD14LCB5PWFzLmZhY3RvciAoeSkpDQpzdm1maXQgPXN2bSh54oi8LiwgZGF0YT1kYXQgLCBrZXJuZWwgPSJsaW5lYXIiLCBjb3N0ID0xMCxzY2FsZSA9IFRSVUUgKQ0KYGBgDQoNClBvZGVtb3MgdHJhemFyIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlIG9idGVuaWRvLg0KDQpgYGB7cn0NCnBsb3Qoc3ZtZml0ICwgZGF0KQ0KDQpgYGANCg0KTGEgcmVnaW9uIGRlbCBlc3BhY2lvIGRlIGxhIGNhcmFjdGVyaXN0aWNhIHF1ZSBzZSBhc2lnbmFyYSBhIGxhIGNsYXNlIOKIkjEgc2UgbXVlc3RyYSBlbiBhenVsIGNsYXJvLCB5IGxhIHJlZ2lvbiBxdWUgc2UgYXNpZ25hcmEgYSBsYSBjbGFzZSArMSBzZSBtdWVzdHJhIGVuIHB1cnB1cmEuDQpFbCBsw61taXRlIGRlIGRlY2lzaW9uIGVudHJlIGxhcyBkb3MgY2xhc2VzIGVzIGxpbmVhbCAocG9ycXVlIHVzYW1vcyBlbCBhcmd1bWVudG8ga2VybmVsID0gImxpbmVhciIpLCBhdW5xdWUgZGViaWRvIGEgbGEgZm9ybWEgZW4gcXVlIHNlIGltcGxlbWVudGEgbGEgZnVuY2lvbiBkZSB0cmF6YWRvIGVuIGVzdGEgbGlicmVyaWEsIGVsIGxpbWl0ZSBkZSBkZWNpc2lvbiBwYXJlY2UgYWxnbyBpcnJlZ3VsYXIgZW4gZWwgdHJhemFkby4NCkRldGVybWluYXIgbGFzIGlkZW50aWRhZGVzIGRlIGxvcyB2ZWN0b3JlcyBkZSBsYSBzaWd1aWVudGUgZm9ybWE6IA0KDQpgYGB7cn0NCnN2bWZpdCRpbmRleA0KYGBgDQoNCk9idGVuZXIgaW5mb3JtYWNpw7NuIHNvYnJlIGVsIGFqdXN0ZSBkZWwgY2xhc2lmaWNhZG9yIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUgdXNhbmRvIHN1bW1hcnkoKQ0KDQpgYGB7cn0NCnN1bW1hcnkgKHN2bWZpdCkNCg0KYGBgDQoNCkRlIGxvIGFudGVyaW9yIHBvZGVtb3MgZGVjaXIgcXVlIHNlIHVzbyB1biBrZXJuZWwgbGluZWFsIGNvbiBlbCBjb3N0byA9IDEwLCB5IHF1ZSBoYWJpYSBzaWV0ZSB2ZWN0b3JlcyBkZSBzb3BvcnRlLCBjdWF0cm8gZW4gdW5hIGNsYXNlIHkgdHJlcyBlbiBsYSBvdHJhLg0KDQoNCmBgYHtyfQ0Kc3ZtZml0ID1zdm0oeeKIvC4sIGRhdGE9ZGF0ICwga2VybmVsID0ibGluZWFyIiwgY29zdCA9IDAuMSwNCnNjYWxlID1GQUxTRSApDQpwbG90KHN2bWZpdCAsIGRhdCkNCnN2bWZpdCRpbmRleA0KYGBgDQoNCg0KTGEgZnVuY2lvbiB0dW5lKCkgcGVybWl0ZSByZWFsaXphciBjcnVjZXMgZGUgdmFsaWRhY2lvbiwgZW4gZm9ybWEgcHJlZGV0ZXJtaW5hZGEgcmVhbGl6YSB1bmEgdmFsaWRhY2lvbiBjcnV6YWRhIA0KZGUgZGlleiB2ZWNlcyBlbiB1biBjb25qdW50byBkZSBtb2RlbG9zIGRlIGludGVyZXMuIFBhcmEgdXRpbGl6YXIgZXN0YSBmdW5jaW9uIHNlIHRyYW5zZmllcmUgaW5mb3JtYWNpb24gcmVsZXZhbnRlIGRlbCBjb25qdW50byBkZSBtb2RlbG9zIHF1ZSBzZSBlc3RhIGNvbnNpZGVyYW5kby4NCg0KDQpgYGB7cn0NCnNldC5zZWVkICgxKQ0KdHVuZS5vdXQ9dHVuZShzdm0gLHniiLwuLGRhdGE9ZGF0ICxrZXJuZWwgPSJsaW5lYXIiLA0KcmFuZ2VzID1saXN0KGNvc3Q9YygwLjAwMSAsIDAuMDEsIDAuMSwgMSw1LDEwLDEwMCkpKQ0KYGBgDQoNCkFjY2VkZW1vcyBmYWNpbG1lbnRlIGEgbG9zIGVycm9yZXMgZGUgdmFsaWRhY2lvbiBjcnV6YWRhIHBhcmEgY2FkYSB1bm8gZGUgZXN0b3MgbW9kZWxvcw0KdXNhbmRvIHN1bW1hcnkoKToNCg0KYGBge3J9DQpzdW1tYXJ5ICh0dW5lLm91dCkNCmBgYA0KDQpBaG9yYSBzZSBwdWVkZSB2ZXIgcXVlIGFsIHV0aWxpemFyIGNvc3QgPSAwLjEgc2Ugb2J0aWVuZSBlbCBlcnJvciBtw6FzIHBlcXVlw7FvIGRlIGNyb3NzLXZhbGlkYXRpb24uIFBvciBsbyB0YW50byBsYSBmdW5jacOzbiB0dW5lKCkgcHVlZGUgb2J0ZW5lciBlbCBtZWpvciBtb2RlbG8sIGFsIGN1YWwgc2UgcHVlZGUgYWNjZWRlcjoNCg0KYGBge3J9DQpiZXN0bW9kID10dW5lLm91dCRiZXN0Lm1vZGVsDQpzdW1tYXJ5IChiZXN0bW9kKQ0KYGBgDQoNCkxhIGZ1bmNpw7NuIHByZWRpY3QoKSBzZSBwdWVkZSB1c2FyIHBhcmEgcHJlZGVjaXIgbGEgZXRpcXVldGEgZGUgY2xhc2UgZW4gdW4gY29uanVudG8gZGUgb2JzZXJ2YWNpb25lcyBkZSBwcnVlYmEsIGVuIGN1YWxxdWllciB2YWxvciBkYWRvIGRlbCBwYXJhbWV0cm8gZGUgY29zdG8uIEFob3JhIGdlbmVyYXJlbW9zIHVuIGNvbmp1bnRvIGRlIGRhdG9zIGRlIHBydWViYS4NCg0KYGBge3J9DQp4dGVzdD1tYXRyaXggKHJub3JtICgyMCoyKSAsIG5jb2wgPTIpDQp5dGVzdD1zYW1wbGUgKGMoLTEsMSkgLCAyMCwgcmVwPVRSVUUpDQp4dGVzdFt5dGVzdCA9PTEgLF09IHh0ZXN0W3l0ZXN0ID09MSxdICsgMQ0KdGVzdGRhdCA9ZGF0YS5mcmFtZSAoeD14dGVzdCAsIHk9YXMuZmFjdG9yICh5dGVzdCkpDQpgYGANCiAgDQpQcmVkZWNpbW9zIGxhcyBldGlxdWV0YXMgZGUgY2xhc2UgZGUgZXN0YXMgb2JzZXJ2YWNpb25lcyBkZSBwcnVlYmEuDQpBcXXDrSB1dGlsaXphbW9zIGVsIG1lam9yIG1vZGVsbyBvYnRlbmlkbyBtZWRpYW50ZSB2YWxpZGFjacOzbiBjcnV6YWRhIHBhcmEgaGFjZXIgcHJlZGljY2lvbmVzLg0KDQpgYGB7cn0NCnlwcmVkPXByZWRpY3QgKGJlc3Rtb2QgLHRlc3RkYXQgKQ0KdGFibGUocHJlZGljdCA9eXByZWQgLCB0cnV0aD0gdGVzdGRhdCR5ICkNCmBgYA0KDQpQb3IgbG8gdGFudG8sIGNvbiBlc3RlIHZhbG9yIGRlIGNvc3RvLCBoYXkgMTkgb2JzZXJ2YWNpb25lcyBkZSBwcnVlYmEgcXVlIGVzdGFuIGNvcnJlY3RhbWVudGUNCmNsYXNpZmljYWRhcy4gwr9RdcOpIHBhc2Fyw61hIHNpIGh1YmnDqXNlbW9zIHV0aWxpemFkbyBlbiBzdSBsdWdhciBlbCBjb3N0byA9IDAuMDE/DQoNCmBgYHtyfQ0Kc3ZtZml0ID1zdm0oeeKIvC4sIGRhdGE9ZGF0ICwga2VybmVsID0ibGluZWFyIiwgY29zdCA9MC4wMSwNCnNjYWxlID1GQUxTRSApDQp5cHJlZD1wcmVkaWN0IChzdm1maXQgLHRlc3RkYXQgKQ0KdGFibGUocHJlZGljdCA9eXByZWQgLCB0cnV0aD0gdGVzdGRhdCR5ICkNCmBgYA0KDQpQb2RlbW9zIGVuY29udHJhciB1biBoaXBlcnBsYW5vIGRlIHNlcGFyYWNpw7NuIHVzYW5kbyBsYSBmdW5jacOzbiBzdm0oKS4gUHJpbWVybyBzZXBhcmFtb3MgbGFzIGRvcyBjbGFzZXMgZW4gbnVlc3Ryb3MgZGF0b3Mgc2ltdWxhZG9zIHBhcmEgcXVlIHNlYW4gbGluZWFsbWVudGUgc2VwYXJhYmxlczoNCg0KYGBge3J9DQp4W3k9PTEgLF09IHhbeT09MSAsXSswLjUNCnBsb3QoeCwgY29sID0oeSs1KSAvMiwgcGNoID0xOSkNCmBgYA0KDQpBanVzdGFtb3MgZWwgY2xhc2lmaWNhZG9yIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUgeSB0cmF6YW1vcyBlbCBoaXBlcnBsYW5vIHJlc3VsdGFudGUsIHV0aWxpemFuZG8gdW4gdmFsb3IgZGUgY29zdG8gbXV5IGdyYW5kZSBwYXJhIHF1ZSBubyBzZSBjbGFzaWZpcXVlbiBlcnLDs25lYW1lbnRlIGxhcyBvYnNlcnZhY2lvbmVzLg0KDQpgYGB7cn0NCmRhdD1kYXRhLmZyYW1lKHg9eCx5PWFzLmZhY3RvciAoeSkpDQpzdm1maXQgPXN2bSh54oi8LiwgZGF0YT1kYXQgLCBrZXJuZWwgPSJsaW5lYXIiLCBjb3N0ID0xZTUpDQpzdW1tYXJ5IChzdm1maXQgKQ0KYGBgDQoNCk5vIHNlIGNvbWV0aWVyb24gZXJyb3JlcyBkZSBlbnRyZW5hbWllbnRvIHkgc29sbyBzZSB1c2Fyb24gdHJlcyB2ZWN0b3JlcyBkZSBzb3BvcnRlLiBTaW4gZW1iYXJnbywgcG9kZW1vcyB2ZXIgZW4gbGEgZmlndXJhIHF1ZSBlbCBtYXJnZW4gZXMgbXV5IGVzdHJlY2hvIChwb3JxdWUgbGFzIG9ic2VydmFjaW9uZXMgcXVlIG5vIHNvbiB2ZWN0b3JlcyBkZSBzb3BvcnRlLCBpbmRpY2FkYXMgY29tbyBjw61yY3Vsb3MsIGVzdMOhbiBtdXkgY2VyY2EgZGVsIGzDrW1pdGUgZGUgZGVjaXNpb24pLiBQYXJlY2UgcHJvYmFibGUgcXVlIGVzdGUgbW9kZWxvIHNlIGRlc2VtcGXDsWUgbWFsIGVuIGxvcyBkYXRvcyBkZSBwcnVlYmEuIEFob3JhIGludGVudGFtb3MgdW4gbWVub3IgdmFsb3IgZGUgY29zdG86DQoNCmBgYHtyfQ0Kc3ZtZml0ID1zdm0oeeKIvC4sIGRhdGE9ZGF0ICwga2VybmVsID0ibGluZWFyIiwgY29zdCA9MSkNCnN1bW1hcnkgKHN2bWZpdCApDQpwbG90KHN2bWZpdCAsZGF0ICkNCmBgYA0KDQpVc2FuZG8gY29zdCA9IDEsIGNsYXNpZmljYW1vcyBlcnJvbmVhbWVudGUgdW5hIG9ic2VydmFjaW9uIGRlIGVudHJlbmFtaWVudG8sIHBlcm8gdGFtYmllbiBvYnRlbmVtb3MgdW4gbWFyZ2VuIG11Y2hvIG1hcyBhbXBsaW8geSB1dGlsaXphbW9zIHNpZXRlIHZlY3RvcmVzIGRlIHNvcG9ydGUuIFBhcmVjZSBwcm9iYWJsZSBxdWUgZXN0ZSBtb2RlbG8gdGVuZ2EgdW4gbWVqb3IgZGVzZW1wZcOxbyBlbiBsb3MgZGF0b3MgZGUgcHJ1ZWJhIHF1ZSBlbCBtb2RlbG8gY29uIGNvc3QgPSAxZTUuDQoNCg0KIyMgOS42LjIgU3VwcG9ydCBWZWN0b3IgTWFjaGluZQ0KDQpQYXJhIGFqdXN0YXIgdW4gU1ZNIHVzYW5kbyB1biBrZXJuZWwgbm8gbGluZWFsLCB1bmEgdmV6IG1hcyB1c2Ftb3MgbGEgZnVuY2lvbiBzdm0oKS4gU2luIGVtYmFyZ28sIGFob3JhIHVzYW1vcyB1biB2YWxvciBkaWZlcmVudGUgZGVsIHBhcsOhbWV0cm8ga2VybmVsLiBQYXJhIGFqdXN0YXIgdW5hIFNWTSBjb24gdW4gbsO6Y2xlbyBwb2xpbm9taWFsIHVzYW1vcyBrZXJuZWwgPSAicG9saW5vbWlhbCIsIHkgcGFyYSBhanVzdGFyIHVuYSBTVk0gY29uIHVuIG7DumNsZW8gcmFkaWFsIHVzYW1vcyBrZXJuZWwgPSAicmFkaWFsIi4NCg0KDQpgYGB7cn0NCnNldC5zZWVkICgxKQ0KeD1tYXRyaXggKHJub3JtICgyMDAqMikgLCBuY29sID0yKQ0KeFsxOjEwMCAsXT14WzE6MTAwICxdKzINCnhbMTAxOjE1MCAsXT0geFsxMDE6MTUwICxdIC0yDQp5PWMocmVwICgxICwxNTApICxyZXAgKDIgLDUwKSApDQpkYXQ9ZGF0YS5mcmFtZSh4PXgseT1hcy5mYWN0b3IgKHkpKQ0KYGBgDQoNCkVsIHRyYXphZG8gZGUgbG9zIGRhdG9zIGRlamEgY2xhcm8gcXVlIGVsIGxpbWl0ZSBkZSBsYSBjbGFzZSBlcyBubyBsaW5lYWw6DQoNCmBgYHtyfQ0KcGxvdCh4LCBjb2w9eSkNCmBgYA0KDQpMb3MgZGF0b3Mgc2UgZGl2aWRlbiBhbGVhdG9yaWFtZW50ZSBlbiBncnVwb3MgZGUgdHJhaW4geSB0ZXN0LiBFbnRvbmNlcyBlbmNhamFtb3MgbG9zIGRhdG9zIGRlbCB0cmFpbiB1dGlsaXphbmRvIGxhIGZ1bmNpw7NuIHN2bSAoKSBjb24gdW4gbsO6Y2xlbyByYWRpYWwgeSDOsyA9IDE6DQoNCmBgYHtyfQ0KdHJhaW49c2FtcGxlICgyMDAgLDEwMCkNCnN2bWZpdCA9c3ZtKHniiLwuLCBkYXRhPWRhdCBbdHJhaW4gLF0sIGtlcm5lbCA9InJhZGlhbCIsIGdhbW1hID0xLA0KY29zdCA9MSkNCnBsb3Qoc3ZtZml0ICwgZGF0W3RyYWluICxdKQ0KYGBgDQoNCkxhIGdyYWZpY2EgbXVlc3RyYSBxdWUgZWwgU1ZNIHJlc3VsdGFudGUgdGllbmUgdW4gbGltaXRlIG5vIGxpbmVhbC4gTGEgZnVuY2lvbiBkZSBzdW1tYXJ5KCkgc2UgcHVlZGUgdXRpbGl6YXIgcGFyYSBvYnRlbmVyIGFsZ3VuYSBpbmZvcm1hY2lvbiBzb2JyZSBlbCBhanVzdGUgU1ZNOg0KDQpgYGB7cn0NCnN1bW1hcnkgKHN2bWZpdCApDQpgYGANCg0KUG9kZW1vcyB2ZXIgZW4gbGEgZmlndXJhIHF1ZSBoYXkgdW4gYnVlbiBudW1lcm8gZGUgZXJyb3JlcyBkZSBlbnRyZW5hbWllbnRvIGVuIGVzdGUgYWp1c3RlIGRlIFNWTS4gU2kgYXVtZW50YW1vcyBlbCB2YWxvciBkZWwgY29zdG8sIHBvZGVtb3MgcmVkdWNpciBlbCBuw7ptZXJvIGRlIGVycm9yZXMgZGUgY2FwYWNpdGFjaW9uLiANCg0KYGBge3J9DQpzdm1maXQgPXN2bSh54oi8LiwgZGF0YT1kYXQgW3RyYWluICxdLCBrZXJuZWwgPSJyYWRpYWwiLGdhbW1hID0xLA0KY29zdD0xZTUpDQpwbG90KHN2bWZpdCAsZGF0IFt0cmFpbiAsXSkNCmBgYA0KDQpQb2RlbW9zIHJlYWxpemFyIHVuYSB2YWxpZGFjacOzbiBjcnV6YWRhIHV0aWxpemFuZG8gdHVuZSgpIHBhcmEgc2VsZWNjaW9uYXIgbGEgbWVqb3Igb3BjacOzbiBkZSDOsyB5IGVsIGNvc3RvIGRlIHVuYSBTVk0gY29uIHVuIG7DumNsZW8gcmFkaWFsOg0KDQpgYGB7cn0NCnNldC5zZWVkICgxKQ0KdHVuZS5vdXQ9dHVuZShzdm0gLCB54oi8LiwgZGF0YT1kYXRbdHJhaW4gLF0sIGtlcm5lbCA9InJhZGlhbCIsDQogICAgICAgICAgICAgIHJhbmdlcyA9bGlzdChjb3N0PWMoMC4xICwxICwxMCAsMTAwICwxMDAwKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGdhbW1hPWMoMC41LDEsMiwzLDQpICkpDQpzdW1tYXJ5ICh0dW5lLm91dCkNCmBgYA0KDQpQb2RlbW9zIHZlciBsYXMgcHJlZGljY2lvbmVzIGRlbCBjb25qdW50byBkZSBwcnVlYmFzIHBhcmEgZXN0ZSBtb2RlbG8gYXBsaWNhbmRvIGxhIGZ1bmNpb24gcHJlZGljdCgpIGEgbG9zIGRhdG9zLiBUZW5lciBlbiBjdWVudGEgcXVlIHBhcmEgaGFjZXIgZXN0bywgc3ViY29udHJhdGFtb3MgbG9zIGRhdG9zIGRlbCBtYXJjbyBkZSBkYXRvcyB1dGlsaXphbmRvIC10cmFpbiBjb21vIHVuIGNvbmp1bnRvIGRlIGluZGljZXMuDQoNCmBgYHtyfQ0KdGFibGUodHJ1ZT1kYXRbLXRyYWluICwieSJdLCBwcmVkPXByZWRpY3QgKHR1bmUub3V0JGJlc3QubW9kZWwgLA0KbmV3eD1kYXRbLXRyYWluICxdKSkNCmBgYA0KDQozOSUgZGUgbGFzIG9ic2VydmFjaW9uZXMgZGUgcHJ1ZWJhIGVzdGFuIG1hbCBjbGFzaWZpY2FkYXMgcG9yIGVzdGUgU1ZNLg0KDQojIyA5LjYuMyBST0MgQ3VydmVzDQoNCkVsIHBhcXVldGUgUk9DUiBzZSBwdWVkZSB1dGlsaXphciBwYXJhIHByb2R1Y2lyIGN1cnZhcyBST0MuIA0KDQpgYGB7cn0NCmxpYnJhcnkgKFJPQ1IpDQpyb2NwbG90ID1mdW5jdGlvbiAocHJlZCAsIHRydXRoICwgLi4uKXsNCiAgIHByZWRvYiA9IHByZWRpY3Rpb24gKHByZWQgLCB0cnV0aCApDQogICBwZXJmID0gcGVyZm9ybWFuY2UgKHByZWRvYiAsICJ0cHIiLCAiZnByIikNCiAgIHBsb3QocGVyZiAsLi4uKX0NCmBgYA0KDQoNClBhcmEgdW5hIFNWTSBjb24gdW4ga2VybmVsIG5vIGxpbmVhbC4gRW4gZXNlbmNpYSwgZWwgc2lnbm8gZGVsIHZhbG9yIGFqdXN0YWRvIGRldGVybWluYSBlbiBxdWUgbGFkbyBkZWwgbGltaXRlIGRlIGRlY2lzaW9uIHNlIGVuY3VlbnRyYSBsYSBvYnNlcnZhY2lvbi4gUG9yIGxvIHRhbnRvLCBsYSByZWxhY2lvbiBlbnRyZSBlbCB2YWxvciBhanVzdGFkbyB5IGxhIHByZWRpY2Npb24gZGUgY2xhc2UgcGFyYSB1bmEgb2JzZXJ2YWNpb24gZGFkYSBlcyBzaW1wbGU6IHNpIGVsIHZhbG9yIGFqdXN0YWRvIGV4Y2VkZSBkZSBjZXJvLCBsYSBvYnNlcnZhY2lvbiBzZSBhc2lnbmEgYSB1bmEgY2xhc2UgeSBzaSBlcyBtZW5vciBxdWUgY2VybyBzZSBhc2lnbmEgYSBsYSBvdHJhLg0KDQpgYGB7cn0NCnN2bWZpdC5vcHQ9c3ZtKHniiLwuLCBkYXRhPWRhdFt0cmFpbiAsXSwga2VybmVsID0icmFkaWFsIiwNCmdhbW1hID0yLCBjb3N0PTEsIGRlY2lzaW9uLnZhbHVlcyA9VCkNCmZpdHRlZCA9YXR0cmlidXRlcyAocHJlZGljdCAoc3ZtZml0Lm9wdCAsZGF0W3RyYWluICxdLCBkZWNpc2lvbi52YWx1ZXMgPVRSVUUpKSRkZWNpc2lvbi52YWx1ZXMNCnBhcihtZnJvdyA9YygxLDIpKQ0Kcm9jcGxvdCAoZml0dGVkICxkYXQgW3RyYWluICwieSJdLCBtYWluPSJUcmFpbmluZyBEYXRhIikNCmBgYA0KDQoNCkFsIGF1bWVudGFyIM6zIHBvZGVtb3MgcHJvZHVjaXIgdW4gYWp1c3RlIG1hcyBmbGV4aWJsZSB5IGdlbmVyYXIgbWFzIG1lam9yYXMgZW4gbGEgcHJlY2lzaW9uLg0KDQpgYGB7cn0NCnN2bWZpdC5mbGV4IDwtIHN2bSh5IH4gLiwgZGF0YSA9IGRhdFt0cmFpbixdLCBrZXJuZWwgPSAicmFkaWFsIiwgZ2FtbWEgPSA1MCwgY29zdCA9IDEsIGRlY2lzaW9uLnZhbHVlcyA9IFQpDQpmaXR0ZWQgPC0gYXR0cmlidXRlcyhwcmVkaWN0KHN2bWZpdC5mbGV4LCBkYXRbdHJhaW4sXSwgZGVjaXNpb24udmFsdWVzID0gVCkpJGRlY2lzaW9uLnZhbHVlcw0Kcm9jcGxvdChmaXR0ZWQsIGRhdFt0cmFpbiwieSJdLCBjb2wgPSAicmVkIikNCg0KYGBgDQoNCkludGVyZXNhIG1hcyBlbCBuaXZlbCBkZSBwcmVjaXNpb24gZGUgcHJlZGljY2lvbiBlbiBsb3MgZGF0b3MgZGUgcHJ1ZWJhLiBDdWFuZG8gY2FsY3VsYW1vcyBsYXMgY3VydmFzIFJPQyBlbiBsb3MgZGF0b3MgZGUgcHJ1ZWJhLCBlbCBtb2RlbG8gY29uIM6zID0gMiBwYXJlY2UgcHJvcG9yY2lvbmFyIGxvcyByZXN1bHRhZG9zIG3DoXMgcHJlY2lzb3MuDQoNCmBgYHtyfQ0KZml0dGVkID1hdHRyaWJ1dGVzIChwcmVkaWN0IChzdm1maXQub3B0ICxkYXRbLXRyYWluICxdLCBkZWNpc2lvbi52YWx1ZXMgPVQpKSRkZWNpc2lvbi52YWx1ZXMNCnJvY3Bsb3QgKGZpdHRlZCAsZGF0IFstdHJhaW4gLCJ5Il0sIG1haW4gPSJUZXN0IERhdGEiKQ0KZml0dGVkID1hdHRyaWJ1dGVzIChwcmVkaWN0IChzdm1maXQuZmxleCAsZGF0Wy10cmFpbiAsXSwgZGVjaXNpb24udmFsdWVzID1UKSkkZGVjaXNpb24udmFsdWVzDQpyb2NwbG90IChmaXR0ZWQgLGRhdCBbLXRyYWluICwieSJdLCBhZGQ9VCxjb2wgPSJyZWQgIikNCmBgYA0KDQoNCiMjIDkuNi40IFNWTSB3aXRoIE11bHRpcGxlIENsYXNzZXMNCg0KTGEgZnVuY2nDs24gc3ZtKCkgcmVhbGl6YXJhIHVuYSBjbGFzaWZpY2FjaW9uIGRlIG11bHRpcGxlcyBjbGFzZXMgdXRpbGl6YW5kbyBlbCBlbmZvcXVlIGRlIHVubyBjb250cmEgdW5vLiBTZSBnZW5lcmEgdW5hIHRlcmNlcmEgY2xhc2UgZGUgb2JzZXJ2YWNpb25lcy4NCg0KYGBge3J9DQpzZXQuc2VlZCAoMSkNCng9cmJpbmQoeCwgbWF0cml4IChybm9ybSAoNTAqMikgLCBuY29sID0yKSkNCnk9Yyh5LCByZXAgKDAgLDUwKSApDQp4W3k9PTAgLDJdPSB4W3k9PTAgLDJdKzINCmRhdD1kYXRhLmZyYW1lKHg9eCwgeT1hcy5mYWN0b3IgKHkpKQ0KcGFyKG1mcm93ID1jKDEsMSkpDQpwbG90KHgsY29sID0oeSsxKSkNCmBgYA0KDQpBanVzdGFtb3MgdW4gU1ZNIGEgbG9zIGRhdG9zOg0KDQpgYGB7cn0NCnN2bWZpdCA9c3ZtKHniiLwuLCBkYXRhPWRhdCAsIGtlcm5lbCA9InJhZGlhbCIsIGNvc3QgPTEwLCBnYW1tYSA9MSkNCnBsb3Qoc3ZtZml0ICwgZGF0KQ0KYGBgDQoNCg0KIyMgOS42LjUgQXBwbGljYXRpb24gdG8gR2VuZSBFeHByZXNzaW9uIERhdGENCg0KQWhvcmEgZXhhbWluYW1vcyBlbCBjb25qdW50byBkZSBkYXRvcyBkZSBLaGFuLCBxdWUgY29uc2lzdGUgZW4gdW5hIHNlcmllIGRlIG11ZXN0cmFzIGRlIHRlamlkbyBxdWUgY29ycmVzcG9uZGVuIGEgY3VhdHJvIHRpcG9zIGRpc3RpbnRvcyBkZSB0dW1vcmVzIHBlcXVlw7FvcyByZWRvbmRvcyBkZSBjw6lsdWxhcyBhenVsZXMuIFBhcmEgY2FkYSBtdWVzdHJhIGRlIHRlamlkbywgbGFzIG1lZGljaW9uZXMgZGUgZXhwcmVzaW9uIGdlbmljYSBlc3RhbiBkaXNwb25pYmxlcy4gDQpgYGB7cn0NCmxpYnJhcnkgKElTTFIpDQpuYW1lcyhLaGFuKQ0KZGltKCBLaGFuJHh0cmFpbiApDQpkaW0oIEtoYW4keHRlc3QgKQ0KbGVuZ3RoIChLaGFuJHl0cmFpbiApDQpsZW5ndGggKEtoYW4keXRlc3QgKQ0KYGBgDQoNCkVzdGUgY29uanVudG8gZGUgZGF0b3MgY29uc2lzdGUgZW4gbWVkaWNpb25lcyBkZSBleHByZXNpb24gcGFyYSAyLDMwOCBnZW5lcy4NCkxvcyBjb25qdW50b3MgZGUgZW50cmVuYW1pZW50byB5IHBydWViYSBjb25zdGFuIGRlIDYzIHkgMjAgb2JzZXJ2YWNpb25lcyBkZSBmb3JtYSBlc3BlY3RhdGl2YS4NCg0KYGBge3J9DQp0YWJsZShLaGFuJHl0cmFpbiApDQp0YWJsZShLaGFuJHl0ZXN0ICkNCmBgYA0KDQpFbiBlc3RlIGNvbmp1bnRvIGRlIGRhdG9zLCBoYXkgdW5hIGdyYW4gY2FudGlkYWQgZGUgY2FyYWN0ZXJpc3RpY2FzIGVuIHJlbGFjaW9uIGNvbiBsYSBjYW50aWRhZCBkZQ0Kb2JzZXJ2YWNpb25lcy4gRXN0byBzdWdpZXJlIHF1ZSBkZWJlcmlhbW9zIHVzYXIgdW4gbnVjbGVvIGxpbmVhbCwgcG9ycXVlIGxhIGZsZXhpYmlsaWRhZCBhZGljaW9uYWwgcXVlIHJlc3VsdGFyYSBkZWwgdXNvIGRlIHVuIG51Y2xlbyBwb2xpbm9taWFsIG8gcmFkaWFsIGVzIGlubmVjZXNhcmlhLg0KDQpgYGB7cn0NCmRhdD1kYXRhLmZyYW1lKHg9S2hhbiR4dHJhaW4gLCB5PWFzLmZhY3RvciAoIEtoYW4keXRyYWluICkpDQpvdXQ9c3ZtKHniiLwuLCBkYXRhPWRhdCAsIGtlcm5lbCA9ImxpbmVhciIsY29zdCA9MTApDQpzdW1tYXJ5IChvdXQpDQp0YWJsZShvdXQkZml0dGVkICwgZGF0JHkpDQpgYGANCg0KTm8gaGF5IGVycm9yZXMgZGUgZW50cmVuYW1pZW50by4gRGUgaGVjaG8sIGVzdG8gbm8gZXMgc29ycHJlbmRlbnRlLCBkZWJpZG8gYSBxdWUgbGEgZ3JhbiBjYW50aWRhZCBkZQ0KdmFyaWFibGVzIGVuIHJlbGFjaW9uIGNvbiBsYSBjYW50aWRhZCBkZSBvYnNlcnZhY2lvbmVzIGltcGxpY2EgcXVlIGVzIGZhY2lsIGVuY29udHJhciBoaXBlcnBsYW5vcyBxdWUgc2VwYXJhbiBsYXMgY2xhc2VzIHBvciBjb21wbGV0by4NCg0KYGBge3J9DQpkYXQudGU9ZGF0YS5mcmFtZSh4PUtoYW4keHRlc3QgLCB5PWFzLmZhY3RvciAoS2hhbiR5dGVzdCApKQ0KcHJlZC50ZT1wcmVkaWN0IChvdXQgLCBuZXdkYXRhID1kYXQudGUpDQp0YWJsZShwcmVkLnRlICwgZGF0LnRlJHkpDQpgYGANCg0KVXNhciBjb3N0ID0gMTAgcHJvZHVjZSBkb3MgZXJyb3JlcyBkZSBjb25qdW50byBkZSBwcnVlYmEgZW4gZXN0b3MgZGF0b3MuDQoNCg==