Cesar Tinoco - 13003387

Se utiliza la libreria e1071 en R para demostrar el clasificador de vectores de soporte y el SVM. Otra opción es la libreria LiblineaR, que es útil para problemas lineales muy grandes.

9.6.1 Clasificador de vectores de soporte

La libreria e1071 contiene implementaciones para varios metodos de aprendizaje estadistico, en particular la función svm() 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. Ahora usamos la funcion svm () para ajustar el clasificador de vectores de soporte para un Valor dado del parametro coste.Comenzamos generando las observaciones, que pertenecen a dos clases.

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.

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

Se pudo comprobar que no son, luego se ajusta el clasificador de vectores de soporte. Tenga en cuenta que para que la funcion svm() realice una clasificacion se debe codificar la respuesta como una variable de factor. A continuacion se crea un dataframe con la respuesta codificada como un factor.

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 le dice a la funcion svm() 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 )

Ahora podemos trazar el clasificador de vectores de soporte obtenido.

plot(svmfit , dat)

Hay que tener en cuenta que los argumentos de la funcion plot.svm() son la salida de la llamada a svm().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. Vemos que en este caso solo una observacion está mal clasificada.Los vectores de soporte se trazan como cruces y Las observaciones restantes se trazan como círculos; Vemos aquí que hay siete vectores de apoyo.

Podemos determinar las identidades de los vectores de la siguiente manera:

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

Podemos 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.

¿Qué pasaría si en cambio usáramos un valor más pequeño del parámetro de costo?

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

Como se esta utilizando un valor mas pequeño del parametro de costo se obtiene un numero mayor de vectores de soporte debido a que el ancho del margen es mas ancho. svm() no genera explicitamente los coeficientes del limite de decision lineal que se obtiene cuando el clasificador de vectores esta ajustado. 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. A continuacion realizaremos una comparacion entre SVM y un Kernel Lineal utilizando un rango de valores del parametro de costo.

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)))

Podemos acceder 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

Vemos que el costo = 0.1 es la tasa de error de validacion cruzada mas baja. La funcion tune() almacena el mejor modelo obtenido, al cual se puede acceder de la siguiente manera:

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))

Ahora 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 )
table(predict =ypred , truth= testdat$y )
       truth
predict -1  1
     -1 11  2
     1   0  7

En este caso, una observación adicional está mal clasificada. Ahora considere una situación en la que las dos clases son linealmente separables. Luego 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)

Ahora las observaciones son apenas linealmente separables. 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 Maquinas de vectores soporte

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”. En el primer caso, también usamos el argumento de grado para especificar un grado para el núcleo polinomial (esto es d en (9.22)), y en el último caso usamos gamma para especificar un valor de γ para el núcleo de base radial (9.24) . Primero generamos algunos datos con un límite de clase no lineal.

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:  35

 ( 15 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. Sin embargo, esto viene al precio de un limite de decision mas irregular que parece estar en riesgo de sobreajustar los datos.

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.09 

- Detailed performance results:
NA

Por lo tanto, la mejor eleccion de parametros implica cost = 1 y gamma = 2. 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 55 23
   2 16  6

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

9.6.3 Curvas ROC

El paquete ROCR se puede utilizar para producir curvas ROC. Primero escribimos una función corta para trazar una curva ROC dado un vector que contiene una puntuacion numerica para cada observacion, pred y un vector que contiene la etiqueta de la clase para cada observación, truth.

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 ,...)}

Los SVM y los clasificadores de vectores dan salida a las etiquetas de clase para cada observacion y tambien es posible obtener valores ajustados para cada observacion, que son los puntajes numericos utilizados para obtener las etiquetas de clase. 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. Para obtener los valores ajustados para un ajuste de modelo SVM dado, usamos decision.values = TRUE cuando ajustamos svm(). Entonces la función de predict() dará salida a los valores ajustados.

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

Ahora podemos producir ROC plot.

par(mfrow =c(1,2))
rocplot (fitted ,dat [train ,"y"], main="Training Data")

SVM parece estar produciendo predicciones precisas. 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")

Sin embargo, estas curvas ROC estan todas en los datos de entrenamiento. 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 con multiples clases

Si la respuesta es un factor que contiene mas de dos niveles, 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))

Ahora ajustamos un SVM a los datos:

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

La libreria e1071 tambien se puede usar para realizar una regresion de vectores de soporte, si el vector de respuesta que se pasa a svm() es numerico en lugar de un factor.

9.6.5 Aplicacion a los datos de expresion genica

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. El conjunto de datos consta de datos de entrenamiento, xtrain y ytrain, y datos de prueba, xtest y ytest. Examinamos la dimension de los datos:

library (ISLR)
package 㤼㸱ISLR㤼㸲 was built under R version 3.5.3
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 

Utilizaremos un enfoque de vectores de apoyo para predecir el subtipo de cancer mediante mediciones de expresion genica. 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

Vemos que 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. Estamos mas interesados no en el desempeño del clasificador de vectores de soporte en las observaciones de entrenamiento, sino en su desempeño en las observaciones de prueba.

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

Vemos que usar cost = 10 produce dos errores de conjunto de prueba en estos datos.

LS0tDQp0aXRsZTogIkxhYm9yYXRvcmlvIDkuNiAtIE1hcXVpbmFzIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIyBDZXNhciBUaW5vY28gLSAxMzAwMzM4Nw0KDQpTZSB1dGlsaXphIGxhIGxpYnJlcmlhIGUxMDcxIGVuIFIgcGFyYSBkZW1vc3RyYXIgZWwgY2xhc2lmaWNhZG9yIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUgeSBlbCBTVk0uDQpPdHJhIG9wY2nDs24gZXMgbGEgbGlicmVyaWEgTGlibGluZWFSLCBxdWUgZXMgw7p0aWwgcGFyYSBwcm9ibGVtYXMgbGluZWFsZXMgbXV5IGdyYW5kZXMuDQoNCiMjIDkuNi4xIENsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlIA0KDQpMYSBsaWJyZXJpYSBlMTA3MSBjb250aWVuZSBpbXBsZW1lbnRhY2lvbmVzIHBhcmEgdmFyaW9zIG1ldG9kb3MgZGUgYXByZW5kaXphamUgZXN0YWRpc3RpY28sIGVuIHBhcnRpY3VsYXIgbGEgZnVuY2nDs24gc3ZtKCkgdW4gY2xhc2lmaWNhZG9yIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUgY3VhbmRvIHNlIHV0aWxpY2UgZWwgYXJndW1lbnRvIGtlcm5lbD0ibGluZWFyIi4gVW4gYXJndW1lbnRvIGRlIGNvc3RvcyBub3MgcGVybWl0ZSBlc3BlY2lmaWNhciBlbCBjb3N0byBkZSB2aW9sYWNpw7NuIGFsIG1hcmdlbiwgY3VhbmRvIGVsIGFyZ3VtZW50byBkZSBjb3N0byBlcyBwZXF1ZcOxbywgZW50b25jZXMgbG9zIG1hcmdlbmVzIHNlcsOhbiBhbmNob3MuDQpBaG9yYSB1c2Ftb3MgbGEgZnVuY2lvbiBzdm0gKCkgcGFyYSBhanVzdGFyIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlIHBhcmEgdW4gVmFsb3IgZGFkbyBkZWwgcGFyYW1ldHJvIGNvc3RlLkNvbWVuemFtb3MgZ2VuZXJhbmRvIGxhcyBvYnNlcnZhY2lvbmVzLCBxdWUgcGVydGVuZWNlbiBhIGRvcyBjbGFzZXMuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQgKDEpDQp4PW1hdHJpeCAocm5vcm0gKDIwKjIpICwgbmNvbCA9MikNCnk9YyhyZXAgKC0xLDEwKSAsIHJlcCAoMSAsMTApICkNCnhbeT09MSAsXT0geFt5PT0xLF0gKyAxDQpgYGANCg0KU2UgdmVyaWZpY2Egc2kgbGFzIGNsYXNlcyBzb24gbGluZWFsbWVudGUgc2VwYXJhYmxlcy4NCg0KYGBge3J9DQpwbG90KHgsIGNvbCA9KDMteSkpDQpgYGANCg0KU2UgcHVkbyBjb21wcm9iYXIgcXVlIG5vIHNvbiwgbHVlZ28gc2UgYWp1c3RhIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlLiBUZW5nYSBlbiBjdWVudGEgcXVlIHBhcmEgcXVlIGxhIGZ1bmNpb24gc3ZtKCkgcmVhbGljZSB1bmEgY2xhc2lmaWNhY2lvbiBzZSBkZWJlIGNvZGlmaWNhciBsYSByZXNwdWVzdGEgY29tbyB1bmEgdmFyaWFibGUgZGUgZmFjdG9yLiBBIGNvbnRpbnVhY2lvbiBzZSBjcmVhIHVuIGRhdGFmcmFtZSBjb24gbGEgcmVzcHVlc3RhIGNvZGlmaWNhZGEgY29tbyB1biBmYWN0b3IuDQoNCmBgYHtyfQ0KbGlicmFyeSAoZTEwNzEpDQpkYXQ9ZGF0YS5mcmFtZSh4PXgsIHk9YXMuZmFjdG9yICh5KSkNCnN2bWZpdCA9c3ZtKHniiLwuLCBkYXRhPWRhdCAsIGtlcm5lbCA9ImxpbmVhciIsIGNvc3QgPTEwLHNjYWxlID1GQUxTRSApDQpgYGANCg0KRWwgYXJndW1lbnRvIHNjYWxlID0gRkFMU0UgbGUgZGljZSBhIGxhIGZ1bmNpb24gc3ZtKCkgcXVlIG5vIGVzY2FsZSBjYWRhIGNhcmFjdGVyaXN0aWNhIHBhcmEgcXVlIHRlbmdhIHVuYSBtZWRpYSBkZSBjZXJvIG8gdW5hIGRlc3ZpYWNpb24gZXN0YW5kYXIgZGUgdW5vOyBEZXBlbmRpZW5kbyBkZSBsYSBhcGxpY2FjaW9uLCB1bm8gcG9kcmlhIHByZWZlcmlyIHVzYXIgc2NhbGUgPSBUUlVFDQoNCmBgYHtyfQ0KZGF0PWRhdGEuZnJhbWUoeD14LCB5PWFzLmZhY3RvciAoeSkpDQpzdm1maXQgPXN2bSh54oi8LiwgZGF0YT1kYXQgLCBrZXJuZWwgPSJsaW5lYXIiLCBjb3N0ID0xMCxzY2FsZSA9IFRSVUUgKQ0KYGBgDQoNCkFob3JhIHBvZGVtb3MgdHJhemFyIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlIG9idGVuaWRvLg0KDQpgYGB7cn0NCnBsb3Qoc3ZtZml0ICwgZGF0KQ0KDQpgYGANCg0KSGF5IHF1ZSB0ZW5lciBlbiBjdWVudGEgcXVlIGxvcyBhcmd1bWVudG9zIGRlIGxhIGZ1bmNpb24gcGxvdC5zdm0oKSBzb24gbGEgc2FsaWRhIGRlIGxhIGxsYW1hZGEgYSBzdm0oKS5MYSByZWdpb24gZGVsIGVzcGFjaW8gZGUgbGEgY2FyYWN0ZXJpc3RpY2EgcXVlIHNlIGFzaWduYXJhIGEgbGEgY2xhc2Ug4oiSMSBzZSBtdWVzdHJhIGVuIGF6dWwgY2xhcm8sIHkgbGEgcmVnaW9uIHF1ZSBzZSBhc2lnbmFyYSBhIGxhIGNsYXNlICsxIHNlIG11ZXN0cmEgZW4gcHVycHVyYS4NCkVsIGzDrW1pdGUgZGUgZGVjaXNpb24gZW50cmUgbGFzIGRvcyBjbGFzZXMgZXMgbGluZWFsIChwb3JxdWUgdXNhbW9zIGVsIGFyZ3VtZW50byBrZXJuZWwgPSAibGluZWFyIiksIGF1bnF1ZSBkZWJpZG8gYSBsYSBmb3JtYSBlbiBxdWUgc2UgaW1wbGVtZW50YSBsYSBmdW5jaW9uIGRlIHRyYXphZG8gZW4gZXN0YSBsaWJyZXJpYSwgZWwgbGltaXRlIGRlIGRlY2lzaW9uIHBhcmVjZSBhbGdvIGlycmVndWxhciBlbiBlbCB0cmF6YWRvLiBWZW1vcyBxdWUgZW4gZXN0ZSBjYXNvIHNvbG8gdW5hIG9ic2VydmFjaW9uIGVzdMOhIG1hbCBjbGFzaWZpY2FkYS5Mb3MgdmVjdG9yZXMgZGUgc29wb3J0ZSBzZSB0cmF6YW4gY29tbyBjcnVjZXMgeSBMYXMgb2JzZXJ2YWNpb25lcyByZXN0YW50ZXMgc2UgdHJhemFuIGNvbW8gY8OtcmN1bG9zOyBWZW1vcyBhcXXDrSBxdWUgaGF5IHNpZXRlIHZlY3RvcmVzIGRlIGFwb3lvLg0KDQpQb2RlbW9zIGRldGVybWluYXIgbGFzIGlkZW50aWRhZGVzIGRlIGxvcyB2ZWN0b3JlcyBkZSBsYSBzaWd1aWVudGUgbWFuZXJhOiANCg0KYGBge3J9DQpzdm1maXQkaW5kZXgNCmBgYA0KDQpQb2RlbW9zIG9idGVuZXIgaW5mb3JtYWNpw7NuIHNvYnJlIGVsIGFqdXN0ZSBkZWwgY2xhc2lmaWNhZG9yIGRlIHZlY3RvcmVzIGRlIHNvcG9ydGUgdXNhbmRvIHN1bW1hcnkoKQ0KDQpgYGB7cn0NCnN1bW1hcnkgKHN2bWZpdCkNCg0KYGBgDQoNCkRlIGxvIGFudGVyaW9yIHBvZGVtb3MgZGVjaXIgcXVlIHNlIHVzbyB1biBrZXJuZWwgbGluZWFsIGNvbiBlbCBjb3N0byA9IDEwLCB5IHF1ZSBoYWJpYSBzaWV0ZSB2ZWN0b3JlcyBkZSBzb3BvcnRlLCBjdWF0cm8gZW4gdW5hIGNsYXNlIHkgdHJlcyBlbiBsYSBvdHJhLg0KDQrCv1F1w6kgcGFzYXLDrWEgc2kgZW4gY2FtYmlvIHVzw6FyYW1vcyB1biB2YWxvciBtw6FzIHBlcXVlw7FvIGRlbCBwYXLDoW1ldHJvIGRlIGNvc3RvPw0KDQpgYGB7cn0NCnN2bWZpdCA9c3ZtKHniiLwuLCBkYXRhPWRhdCAsIGtlcm5lbCA9ImxpbmVhciIsIGNvc3QgPSAwLjEsDQpzY2FsZSA9RkFMU0UgKQ0KcGxvdChzdm1maXQgLCBkYXQpDQpzdm1maXQkaW5kZXgNCmBgYA0KDQpDb21vIHNlIGVzdGEgdXRpbGl6YW5kbyB1biB2YWxvciBtYXMgcGVxdWXDsW8gZGVsIHBhcmFtZXRybyBkZSBjb3N0byBzZSBvYnRpZW5lIHVuIG51bWVybyBtYXlvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlIGRlYmlkbyBhIHF1ZSBlbCBhbmNobyBkZWwgbWFyZ2VuIGVzIG1hcyBhbmNoby4gc3ZtKCkgbm8gZ2VuZXJhIGV4cGxpY2l0YW1lbnRlIGxvcyBjb2VmaWNpZW50ZXMgZGVsIGxpbWl0ZSBkZSBkZWNpc2lvbiBsaW5lYWwgcXVlIHNlIG9idGllbmUgY3VhbmRvIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBlc3RhIGFqdXN0YWRvLg0KTGEgZnVuY2lvbiB0dW5lKCkgcGVybWl0ZSByZWFsaXphciBjcnVjZXMgZGUgdmFsaWRhY2lvbiwgZW4gZm9ybWEgcHJlZGV0ZXJtaW5hZGEgcmVhbGl6YSB1bmEgdmFsaWRhY2lvbiBjcnV6YWRhIA0KZGUgZGlleiB2ZWNlcyBlbiB1biBjb25qdW50byBkZSBtb2RlbG9zIGRlIGludGVyZXMuIFBhcmEgdXRpbGl6YXIgZXN0YSBmdW5jaW9uIHNlIHRyYW5zZmllcmUgaW5mb3JtYWNpb24gcmVsZXZhbnRlIGRlbCBjb25qdW50byBkZSBtb2RlbG9zIHF1ZSBzZSBlc3RhIGNvbnNpZGVyYW5kby4NCkEgY29udGludWFjaW9uIHJlYWxpemFyZW1vcyB1bmEgY29tcGFyYWNpb24gZW50cmUgU1ZNIHkgdW4gS2VybmVsIExpbmVhbCB1dGlsaXphbmRvIHVuIHJhbmdvIGRlIHZhbG9yZXMgZGVsIHBhcmFtZXRybyBkZSBjb3N0by4NCg0KYGBge3J9DQpzZXQuc2VlZCAoMSkNCnR1bmUub3V0PXR1bmUoc3ZtICx54oi8LixkYXRhPWRhdCAsa2VybmVsID0ibGluZWFyIiwNCnJhbmdlcyA9bGlzdChjb3N0PWMoMC4wMDEgLCAwLjAxLCAwLjEsIDEsNSwxMCwxMDApKSkNCmBgYA0KDQpQb2RlbW9zIGFjY2VkZXIgZmFjaWxtZW50ZSBhIGxvcyBlcnJvcmVzIGRlIHZhbGlkYWNpb24gY3J1emFkYSBwYXJhIGNhZGEgdW5vIGRlIGVzdG9zIG1vZGVsb3MNCnVzYW5kbyBzdW1tYXJ5KCk6DQoNCmBgYHtyfQ0Kc3VtbWFyeSAodHVuZS5vdXQpDQpgYGANCg0KVmVtb3MgcXVlIGVsIGNvc3RvID0gMC4xIGVzIGxhIHRhc2EgZGUgZXJyb3IgZGUgdmFsaWRhY2lvbiBjcnV6YWRhIG1hcyBiYWphLiBMYSBmdW5jaW9uIHR1bmUoKSBhbG1hY2VuYSBlbCBtZWpvciBtb2RlbG8gb2J0ZW5pZG8sIGFsIGN1YWwgc2UgcHVlZGUgYWNjZWRlciBkZSBsYSBzaWd1aWVudGUgbWFuZXJhOg0KDQpgYGB7cn0NCmJlc3Rtb2QgPXR1bmUub3V0JGJlc3QubW9kZWwNCnN1bW1hcnkgKGJlc3Rtb2QpDQpgYGANCg0KTGEgZnVuY2nDs24gcHJlZGljdCgpIHNlIHB1ZWRlIHVzYXIgcGFyYSBwcmVkZWNpciBsYSBldGlxdWV0YSBkZSBjbGFzZSBlbiB1biBjb25qdW50byBkZSBvYnNlcnZhY2lvbmVzIGRlIHBydWViYSwgZW4gY3VhbHF1aWVyIHZhbG9yIGRhZG8gZGVsIHBhcmFtZXRybyBkZSBjb3N0by4gQWhvcmEgZ2VuZXJhcmVtb3MgdW4gY29uanVudG8gZGUgZGF0b3MgZGUgcHJ1ZWJhLg0KDQpgYGB7cn0NCnh0ZXN0PW1hdHJpeCAocm5vcm0gKDIwKjIpICwgbmNvbCA9MikNCnl0ZXN0PXNhbXBsZSAoYygtMSwxKSAsIDIwLCByZXA9VFJVRSkNCnh0ZXN0W3l0ZXN0ID09MSAsXT0geHRlc3RbeXRlc3QgPT0xLF0gKyAxDQp0ZXN0ZGF0ID1kYXRhLmZyYW1lICh4PXh0ZXN0ICwgeT1hcy5mYWN0b3IgKHl0ZXN0KSkNCmBgYA0KICANCkFob3JhIHByZWRlY2ltb3MgbGFzIGV0aXF1ZXRhcyBkZSBjbGFzZSBkZSBlc3RhcyBvYnNlcnZhY2lvbmVzIGRlIHBydWViYS4NCkFxdcOtIHV0aWxpemFtb3MgZWwgbWVqb3IgbW9kZWxvIG9idGVuaWRvIG1lZGlhbnRlIHZhbGlkYWNpw7NuIGNydXphZGEgcGFyYSBoYWNlciBwcmVkaWNjaW9uZXMuDQoNCmBgYHtyfQ0KeXByZWQ9cHJlZGljdCAoYmVzdG1vZCAsdGVzdGRhdCApDQp0YWJsZShwcmVkaWN0ID15cHJlZCAsIHRydXRoPSB0ZXN0ZGF0JHkgKQ0KYGBgDQoNClBvciBsbyB0YW50bywgY29uIGVzdGUgdmFsb3IgZGUgY29zdG8sIGhheSAxOSBvYnNlcnZhY2lvbmVzIGRlIHBydWViYSBxdWUgZXN0YW4gY29ycmVjdGFtZW50ZQ0KY2xhc2lmaWNhZGFzLiDCv1F1w6kgcGFzYXLDrWEgc2kgaHViacOpc2Vtb3MgdXRpbGl6YWRvIGVuIHN1IGx1Z2FyIGVsIGNvc3RvID0gMC4wMT8NCg0KYGBge3J9DQpzdm1maXQgPXN2bSh54oi8LiwgZGF0YT1kYXQgLCBrZXJuZWwgPSJsaW5lYXIiLCBjb3N0ID0uMDEsDQpzY2FsZSA9RkFMU0UgKQ0KeXByZWQ9cHJlZGljdCAoc3ZtZml0ICx0ZXN0ZGF0ICkNCnRhYmxlKHByZWRpY3QgPXlwcmVkICwgdHJ1dGg9IHRlc3RkYXQkeSApDQpgYGANCg0KRW4gZXN0ZSBjYXNvLCB1bmEgb2JzZXJ2YWNpw7NuIGFkaWNpb25hbCBlc3TDoSBtYWwgY2xhc2lmaWNhZGEuDQpBaG9yYSBjb25zaWRlcmUgdW5hIHNpdHVhY2nDs24gZW4gbGEgcXVlIGxhcyBkb3MgY2xhc2VzIHNvbiBsaW5lYWxtZW50ZSBzZXBhcmFibGVzLiBMdWVnbyBwb2RlbW9zIGVuY29udHJhciB1bg0KaGlwZXJwbGFubyBkZSBzZXBhcmFjacOzbiB1c2FuZG8gbGEgZnVuY2nDs24gc3ZtKCkuIFByaW1lcm8gc2VwYXJhbW9zIGxhcyBkb3MgY2xhc2VzIGVuIG51ZXN0cm9zIGRhdG9zIHNpbXVsYWRvcw0KcGFyYSBxdWUgc2VhbiBsaW5lYWxtZW50ZSBzZXBhcmFibGVzOg0KDQpgYGB7cn0NCnhbeT09MSAsXT0geFt5PT0xICxdKzAuNQ0KcGxvdCh4LCBjb2wgPSh5KzUpIC8yLCBwY2ggPTE5KQ0KYGBgDQoNCkFob3JhIGxhcyBvYnNlcnZhY2lvbmVzIHNvbiBhcGVuYXMgbGluZWFsbWVudGUgc2VwYXJhYmxlcy4gQWp1c3RhbW9zIGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlDQp5IHRyYXphbW9zIGVsIGhpcGVycGxhbm8gcmVzdWx0YW50ZSwgdXRpbGl6YW5kbyB1biB2YWxvciBkZSBjb3N0byBtdXkgZ3JhbmRlIHBhcmEgcXVlIG5vIHNlIGNsYXNpZmlxdWVuDQplcnLDs25lYW1lbnRlIGxhcyBvYnNlcnZhY2lvbmVzDQoNCmBgYHtyfQ0KZGF0PWRhdGEuZnJhbWUoeD14LHk9YXMuZmFjdG9yICh5KSkNCnN2bWZpdCA9c3ZtKHniiLwuLCBkYXRhPWRhdCAsIGtlcm5lbCA9ImxpbmVhciIsIGNvc3QgPTFlNSkNCnN1bW1hcnkgKHN2bWZpdCApDQpgYGANCg0KTm8gc2UgY29tZXRpZXJvbiBlcnJvcmVzIGRlIGVudHJlbmFtaWVudG8geSBzb2xvIHNlIHVzYXJvbiB0cmVzIHZlY3RvcmVzIGRlIHNvcG9ydGUuIFNpbiBlbWJhcmdvLCBwb2RlbW9zIHZlcg0KZW4gbGEgZmlndXJhIHF1ZSBlbCBtYXJnZW4gZXMgbXV5IGVzdHJlY2hvIChwb3JxdWUgbGFzIG9ic2VydmFjaW9uZXMgcXVlIG5vIHNvbiB2ZWN0b3JlcyBkZSBzb3BvcnRlLCBpbmRpY2FkYXMNCmNvbW8gY8OtcmN1bG9zLCBlc3TDoW4gbXV5IGNlcmNhIGRlbCBsw61taXRlIGRlIGRlY2lzaW9uKS4gUGFyZWNlIHByb2JhYmxlIHF1ZSBlc3RlIG1vZGVsbyBzZSBkZXNlbXBlw7FlIG1hbCBlbiBsb3MNCmRhdG9zIGRlIHBydWViYS4gQWhvcmEgaW50ZW50YW1vcyB1biBtZW5vciB2YWxvciBkZSBjb3N0bzoNCg0KYGBge3J9DQpzdm1maXQgPXN2bSh54oi8LiwgZGF0YT1kYXQgLCBrZXJuZWwgPSJsaW5lYXIiLCBjb3N0ID0xKQ0Kc3VtbWFyeSAoc3ZtZml0ICkNCnBsb3Qoc3ZtZml0ICxkYXQgKQ0KYGBgDQoNClVzYW5kbyBjb3N0ID0gMSwgY2xhc2lmaWNhbW9zIGVycm9uZWFtZW50ZSB1bmEgb2JzZXJ2YWNpb24gZGUgZW50cmVuYW1pZW50bywgcGVybyB0YW1iaWVuIG9idGVuZW1vcyB1biBtYXJnZW4gbXVjaG8gbWFzIGFtcGxpbyB5IHV0aWxpemFtb3Mgc2lldGUgdmVjdG9yZXMgZGUgc29wb3J0ZS4gUGFyZWNlIHByb2JhYmxlIHF1ZSBlc3RlIG1vZGVsbyB0ZW5nYSB1biBtZWpvciBkZXNlbXBlw7FvIGVuIGxvcyBkYXRvcyBkZSBwcnVlYmEgcXVlIGVsIG1vZGVsbyBjb24gY29zdCA9IDFlNS4NCg0KDQojIyA5LjYuMiBNYXF1aW5hcyBkZSB2ZWN0b3JlcyBzb3BvcnRlDQoNClBhcmEgYWp1c3RhciB1biBTVk0gdXNhbmRvIHVuIGtlcm5lbCBubyBsaW5lYWwsIHVuYSB2ZXogbWFzIHVzYW1vcyBsYSBmdW5jaW9uIHN2bSgpLiBTaW4gZW1iYXJnbywgYWhvcmEgdXNhbW9zDQp1biB2YWxvciBkaWZlcmVudGUgZGVsIHBhcsOhbWV0cm8ga2VybmVsLiBQYXJhIGFqdXN0YXIgdW5hIFNWTSBjb24gdW4gbsO6Y2xlbyBwb2xpbm9taWFsIHVzYW1vcw0Ka2VybmVsID0gInBvbGlub21pYWwiLCB5IHBhcmEgYWp1c3RhciB1bmEgU1ZNIGNvbiB1biBuw7pjbGVvIHJhZGlhbCB1c2Ftb3Mga2VybmVsID0gInJhZGlhbCIuDQpFbiBlbCBwcmltZXIgY2FzbywgdGFtYmnDqW4gdXNhbW9zIGVsIGFyZ3VtZW50byBkZSBncmFkbyBwYXJhIGVzcGVjaWZpY2FyIHVuIGdyYWRvIHBhcmEgZWwgbsO6Y2xlbyBwb2xpbm9taWFsDQooZXN0byBlcyBkIGVuICg5LjIyKSksIHkgZW4gZWwgw7psdGltbyBjYXNvIHVzYW1vcyBnYW1tYSBwYXJhIGVzcGVjaWZpY2FyIHVuIHZhbG9yIGRlIM6zIHBhcmEgZWwgbsO6Y2xlbyBkZSBiYXNlIHJhZGlhbCAoOS4yNCkgLg0KUHJpbWVybyBnZW5lcmFtb3MgYWxndW5vcyBkYXRvcyBjb24gdW4gbMOtbWl0ZSBkZSBjbGFzZSBubyBsaW5lYWwuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQgKDEpDQp4PW1hdHJpeCAocm5vcm0gKDIwMCoyKSAsIG5jb2wgPTIpDQp4WzE6MTAwICxdPXhbMToxMDAgLF0rMg0KeFsxMDE6MTUwICxdPSB4WzEwMToxNTAgLF0gLTINCnk9YyhyZXAgKDEgLDE1MCkgLHJlcCAoMiAsNTApICkNCmRhdD1kYXRhLmZyYW1lKHg9eCx5PWFzLmZhY3RvciAoeSkpDQpgYGANCg0KRWwgdHJhemFkbyBkZSBsb3MgZGF0b3MgZGVqYSBjbGFybyBxdWUgZWwgbGltaXRlIGRlIGxhIGNsYXNlIGVzIG5vIGxpbmVhbDoNCg0KYGBge3J9DQpwbG90KHgsIGNvbD15KQ0KYGBgDQoNCkxvcyBkYXRvcyBzZSBkaXZpZGVuIGFsZWF0b3JpYW1lbnRlIGVuIGdydXBvcyBkZSB0cmFpbiB5IHRlc3QuIEVudG9uY2VzIGVuY2FqYW1vcyBsb3MgZGF0b3MgZGVsIHRyYWluIHV0aWxpemFuZG8gbGEgZnVuY2nDs24gc3ZtICgpIGNvbiB1biBuw7pjbGVvIHJhZGlhbCB5IM6zID0gMToNCg0KYGBge3J9DQp0cmFpbj1zYW1wbGUgKDIwMCAsMTAwKQ0Kc3ZtZml0ID1zdm0oeeKIvC4sIGRhdGE9ZGF0IFt0cmFpbiAsXSwga2VybmVsID0icmFkaWFsIiwgZ2FtbWEgPTEsDQpjb3N0ID0xKQ0KcGxvdChzdm1maXQgLCBkYXRbdHJhaW4gLF0pDQpgYGANCg0KTGEgZ3JhZmljYSBtdWVzdHJhIHF1ZSBlbCBTVk0gcmVzdWx0YW50ZSB0aWVuZSB1biBsaW1pdGUgbm8gbGluZWFsLiBMYSBmdW5jaW9uIGRlIHN1bW1hcnkoKSBzZSBwdWVkZSB1dGlsaXphciBwYXJhIG9idGVuZXIgYWxndW5hIGluZm9ybWFjaW9uIHNvYnJlIGVsIGFqdXN0ZSBTVk06DQoNCmBgYHtyfQ0Kc3VtbWFyeSAoc3ZtZml0ICkNCmBgYA0KDQpQb2RlbW9zIHZlciBlbiBsYSBmaWd1cmEgcXVlIGhheSB1biBidWVuIG51bWVybyBkZSBlcnJvcmVzIGRlIGVudHJlbmFtaWVudG8gZW4gZXN0ZSBhanVzdGUgZGUgU1ZNLiBTaSBhdW1lbnRhbW9zDQplbCB2YWxvciBkZWwgY29zdG8sIHBvZGVtb3MgcmVkdWNpciBlbCBuw7ptZXJvIGRlIGVycm9yZXMgZGUgY2FwYWNpdGFjaW9uLiBTaW4gZW1iYXJnbywgZXN0byB2aWVuZSBhbCBwcmVjaW8gZGUNCnVuIGxpbWl0ZSBkZSBkZWNpc2lvbiBtYXMgaXJyZWd1bGFyIHF1ZSBwYXJlY2UgZXN0YXIgZW4gcmllc2dvIGRlIHNvYnJlYWp1c3RhciBsb3MgZGF0b3MuDQoNCmBgYHtyfQ0Kc3ZtZml0ID1zdm0oeeKIvC4sIGRhdGE9ZGF0IFt0cmFpbiAsXSwga2VybmVsID0icmFkaWFsIixnYW1tYSA9MSwNCmNvc3Q9MWU1KQ0KcGxvdChzdm1maXQgLGRhdCBbdHJhaW4gLF0pDQpgYGANCg0KUG9kZW1vcyByZWFsaXphciB1bmEgdmFsaWRhY2nDs24gY3J1emFkYSB1dGlsaXphbmRvIHR1bmUoKSBwYXJhIHNlbGVjY2lvbmFyIGxhIG1lam9yIG9wY2nDs24gZGUgzrMgeSBlbCBjb3N0byANCmRlIHVuYSBTVk0gY29uIHVuIG7DumNsZW8gcmFkaWFsOg0KDQpgYGB7cn0NCnNldC5zZWVkICgxKQ0KdHVuZS5vdXQ9dHVuZShzdm0gLCB54oi8LiwgZGF0YT1kYXRbdHJhaW4gLF0sIGtlcm5lbCA9InJhZGlhbCIsDQogICAgICAgICAgICAgIHJhbmdlcyA9bGlzdChjb3N0PWMoMC4xICwxICwxMCAsMTAwICwxMDAwKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGdhbW1hPWMoMC41LDEsMiwzLDQpICkpDQpzdW1tYXJ5ICh0dW5lLm91dCkNCmBgYA0KDQpQb3IgbG8gdGFudG8sIGxhIG1lam9yIGVsZWNjaW9uIGRlIHBhcmFtZXRyb3MgaW1wbGljYSBjb3N0ID0gMSB5IGdhbW1hID0gMi4gUG9kZW1vcyB2ZXIgbGFzIHByZWRpY2Npb25lcyBkZWwgY29uanVudG8gZGUgcHJ1ZWJhcyBwYXJhIGVzdGUgbW9kZWxvIGFwbGljYW5kbyBsYSBmdW5jaW9uIHByZWRpY3QoKSBhIGxvcyBkYXRvcy4gVGVuZXIgZW4gY3VlbnRhIHF1ZSBwYXJhIGhhY2VyIGVzdG8sIHN1YmNvbnRyYXRhbW9zIGxvcyBkYXRvcyBkZWwgbWFyY28gZGUgZGF0b3MgdXRpbGl6YW5kbyAtdHJhaW4gY29tbyB1biBjb25qdW50byBkZSBpbmRpY2VzLg0KDQpgYGB7cn0NCnRhYmxlKHRydWU9ZGF0Wy10cmFpbiAsInkiXSwgcHJlZD1wcmVkaWN0ICh0dW5lLm91dCRiZXN0Lm1vZGVsICwNCm5ld3g9ZGF0Wy10cmFpbiAsXSkpDQpgYGANCg0KMzklIGRlIGxhcyBvYnNlcnZhY2lvbmVzIGRlIHBydWViYSBlc3RhbiBtYWwgY2xhc2lmaWNhZGFzIHBvciBlc3RlIFNWTS4NCg0KIyMgOS42LjMgQ3VydmFzIFJPQw0KDQpFbCBwYXF1ZXRlIFJPQ1Igc2UgcHVlZGUgdXRpbGl6YXIgcGFyYSBwcm9kdWNpciBjdXJ2YXMgUk9DLiBQcmltZXJvIGVzY3JpYmltb3MgdW5hIGZ1bmNpw7NuIGNvcnRhIHBhcmEgdHJhemFyIHVuYQ0KY3VydmEgUk9DIGRhZG8gdW4gdmVjdG9yIHF1ZSBjb250aWVuZSB1bmEgcHVudHVhY2lvbiBudW1lcmljYSBwYXJhIGNhZGEgb2JzZXJ2YWNpb24sIHByZWQgeSB1biB2ZWN0b3IgcXVlDQpjb250aWVuZSBsYSBldGlxdWV0YSBkZSBsYSBjbGFzZSBwYXJhIGNhZGEgb2JzZXJ2YWNpw7NuLCB0cnV0aC4NCg0KYGBge3J9DQpsaWJyYXJ5IChST0NSKQ0Kcm9jcGxvdCA9ZnVuY3Rpb24gKHByZWQgLCB0cnV0aCAsIC4uLil7DQogICBwcmVkb2IgPSBwcmVkaWN0aW9uIChwcmVkICwgdHJ1dGggKQ0KICAgcGVyZiA9IHBlcmZvcm1hbmNlIChwcmVkb2IgLCAidHByIiwgImZwciIpDQogICBwbG90KHBlcmYgLC4uLil9DQpgYGANCg0KTG9zIFNWTSB5IGxvcyBjbGFzaWZpY2Fkb3JlcyBkZSB2ZWN0b3JlcyBkYW4gc2FsaWRhIGEgbGFzIGV0aXF1ZXRhcyBkZSBjbGFzZSBwYXJhIGNhZGEgb2JzZXJ2YWNpb24geSANCnRhbWJpZW4gZXMgcG9zaWJsZSBvYnRlbmVyIHZhbG9yZXMgYWp1c3RhZG9zIHBhcmEgY2FkYSBvYnNlcnZhY2lvbiwgcXVlIHNvbiBsb3MgcHVudGFqZXMgbnVtZXJpY29zIHV0aWxpemFkb3MNCnBhcmEgb2J0ZW5lciBsYXMgZXRpcXVldGFzIGRlIGNsYXNlLiANClBhcmEgdW5hIFNWTSBjb24gdW4ga2VybmVsIG5vIGxpbmVhbC4gRW4gZXNlbmNpYSwgZWwgc2lnbm8gZGVsIHZhbG9yIGFqdXN0YWRvIGRldGVybWluYSBlbiBxdWUgbGFkbyBkZWwgbGltaXRlDQpkZSBkZWNpc2lvbiBzZSBlbmN1ZW50cmEgbGEgb2JzZXJ2YWNpb24uIFBvciBsbyB0YW50bywgbGEgcmVsYWNpb24gZW50cmUgZWwgdmFsb3IgYWp1c3RhZG8geSBsYSBwcmVkaWNjaW9uIGRlDQpjbGFzZSBwYXJhIHVuYSBvYnNlcnZhY2lvbiBkYWRhIGVzIHNpbXBsZTogc2kgZWwgdmFsb3IgYWp1c3RhZG8gZXhjZWRlIGRlIGNlcm8sIGxhIG9ic2VydmFjaW9uIHNlIGFzaWduYSBhIHVuYQ0KY2xhc2UgeSBzaSBlcyBtZW5vciBxdWUgY2VybyBzZSBhc2lnbmEgYSBsYSBvdHJhLiBQYXJhIG9idGVuZXIgbG9zIHZhbG9yZXMgYWp1c3RhZG9zIHBhcmEgdW4gYWp1c3RlIGRlIG1vZGVsbyBTVk0gZGFkbywgdXNhbW9zIGRlY2lzaW9uLnZhbHVlcyA9IFRSVUUgY3VhbmRvIGFqdXN0YW1vcyBzdm0oKS4gRW50b25jZXMgbGEgZnVuY2nDs24gZGUgcHJlZGljdCgpIGRhcsOhIHNhbGlkYSBhIGxvcyB2YWxvcmVzIGFqdXN0YWRvcy4NCg0KYGBge3J9DQpzdm1maXQub3B0PXN2bSh54oi8LiwgZGF0YT1kYXRbdHJhaW4gLF0sIGtlcm5lbCA9InJhZGlhbCIsDQpnYW1tYSA9MiwgY29zdD0xLCBkZWNpc2lvbi52YWx1ZXMgPVQpDQpmaXR0ZWQgPWF0dHJpYnV0ZXMgKHByZWRpY3QgKHN2bWZpdC5vcHQgLGRhdFt0cmFpbiAsXSwgZGVjaXNpb24udmFsdWVzID1UUlVFKSkkZGVjaXNpb24udmFsdWVzDQpgYGANCg0KQWhvcmEgcG9kZW1vcyBwcm9kdWNpciBST0MgcGxvdC4NCg0KYGBge3J9DQpwYXIobWZyb3cgPWMoMSwyKSkNCnJvY3Bsb3QgKGZpdHRlZCAsZGF0IFt0cmFpbiAsInkiXSwgbWFpbj0iVHJhaW5pbmcgRGF0YSIpDQpgYGANCg0KU1ZNIHBhcmVjZSBlc3RhciBwcm9kdWNpZW5kbyBwcmVkaWNjaW9uZXMgcHJlY2lzYXMuIEFsIGF1bWVudGFyIM6zIHBvZGVtb3MgcHJvZHVjaXIgdW4gYWp1c3RlIG1hcyBmbGV4aWJsZSB5DQpnZW5lcmFyIG1hcyBtZWpvcmFzIGVuIGxhIHByZWNpc2lvbi4NCg0KYGBge3J9DQpzdm1maXQuZmxleCA8LSBzdm0oeSB+IC4sIGRhdGEgPSBkYXRbdHJhaW4sXSwga2VybmVsID0gInJhZGlhbCIsIGdhbW1hID0gNTAsIGNvc3QgPSAxLCBkZWNpc2lvbi52YWx1ZXMgPSBUKQ0KZml0dGVkIDwtIGF0dHJpYnV0ZXMocHJlZGljdChzdm1maXQuZmxleCwgZGF0W3RyYWluLF0sIGRlY2lzaW9uLnZhbHVlcyA9IFQpKSRkZWNpc2lvbi52YWx1ZXMNCnJvY3Bsb3QoZml0dGVkLCBkYXRbdHJhaW4sInkiXSwgY29sID0gInJlZCIpDQoNCmBgYA0KDQpTaW4gZW1iYXJnbywgZXN0YXMgY3VydmFzIFJPQyBlc3RhbiB0b2RhcyBlbiBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50by4gSW50ZXJlc2EgbWFzIGVsIG5pdmVsIGRlIHByZWNpc2lvbiBkZQ0KcHJlZGljY2lvbiBlbiBsb3MgZGF0b3MgZGUgcHJ1ZWJhLiBDdWFuZG8gY2FsY3VsYW1vcyBsYXMgY3VydmFzIFJPQyBlbiBsb3MgZGF0b3MgZGUgcHJ1ZWJhLCBlbCBtb2RlbG8gY29uIM6zID0gMg0KcGFyZWNlIHByb3BvcmNpb25hciBsb3MgcmVzdWx0YWRvcyBtw6FzIHByZWNpc29zLg0KDQpgYGB7cn0NCmZpdHRlZCA9YXR0cmlidXRlcyAocHJlZGljdCAoc3ZtZml0Lm9wdCAsZGF0Wy10cmFpbiAsXSwgZGVjaXNpb24udmFsdWVzID1UKSkkZGVjaXNpb24udmFsdWVzDQpyb2NwbG90IChmaXR0ZWQgLGRhdCBbLXRyYWluICwieSJdLCBtYWluID0iVGVzdCBEYXRhIikNCmZpdHRlZCA9YXR0cmlidXRlcyAocHJlZGljdCAoc3ZtZml0LmZsZXggLGRhdFstdHJhaW4gLF0sIGRlY2lzaW9uLnZhbHVlcyA9VCkpJGRlY2lzaW9uLnZhbHVlcw0Kcm9jcGxvdCAoZml0dGVkICxkYXQgWy10cmFpbiAsInkiXSwgYWRkPVQsY29sID0icmVkICIpDQpgYGANCg0KDQojIyA5LjYuNCBTVk0gY29uIG11bHRpcGxlcyBjbGFzZXMNCg0KU2kgbGEgcmVzcHVlc3RhIGVzIHVuIGZhY3RvciBxdWUgY29udGllbmUgbWFzIGRlIGRvcyBuaXZlbGVzLCBsYSBmdW5jacOzbiBzdm0oKSByZWFsaXphcmEgdW5hIGNsYXNpZmljYWNpb24gZGUNCm11bHRpcGxlcyBjbGFzZXMgdXRpbGl6YW5kbyBlbCBlbmZvcXVlIGRlIHVubyBjb250cmEgdW5vLiBTZSBnZW5lcmEgdW5hIHRlcmNlcmEgY2xhc2UgZGUgb2JzZXJ2YWNpb25lcy4NCg0KYGBge3J9DQpzZXQuc2VlZCAoMSkNCng9cmJpbmQoeCwgbWF0cml4IChybm9ybSAoNTAqMikgLCBuY29sID0yKSkNCnk9Yyh5LCByZXAgKDAgLDUwKSApDQp4W3k9PTAgLDJdPSB4W3k9PTAgLDJdKzINCmRhdD1kYXRhLmZyYW1lKHg9eCwgeT1hcy5mYWN0b3IgKHkpKQ0KcGFyKG1mcm93ID1jKDEsMSkpDQpwbG90KHgsY29sID0oeSsxKSkNCmBgYA0KDQpBaG9yYSBhanVzdGFtb3MgdW4gU1ZNIGEgbG9zIGRhdG9zOg0KDQpgYGB7cn0NCnN2bWZpdCA9c3ZtKHniiLwuLCBkYXRhPWRhdCAsIGtlcm5lbCA9InJhZGlhbCIsIGNvc3QgPTEwLCBnYW1tYSA9MSkNCnBsb3Qoc3ZtZml0ICwgZGF0KQ0KYGBgDQoNCkxhIGxpYnJlcmlhIGUxMDcxIHRhbWJpZW4gc2UgcHVlZGUgdXNhciBwYXJhIHJlYWxpemFyIHVuYSByZWdyZXNpb24gZGUgdmVjdG9yZXMgZGUgc29wb3J0ZSwgc2kgZWwgdmVjdG9yIGRlDQpyZXNwdWVzdGEgcXVlIHNlIHBhc2EgYSBzdm0oKSBlcyBudW1lcmljbyBlbiBsdWdhciBkZSB1biBmYWN0b3IuDQoNCg0KIyMgOS42LjUgQXBsaWNhY2lvbiBhIGxvcyBkYXRvcyBkZSBleHByZXNpb24gZ2VuaWNhDQoNCkFob3JhIGV4YW1pbmFtb3MgZWwgY29uanVudG8gZGUgZGF0b3MgZGUgS2hhbiwgcXVlIGNvbnNpc3RlIGVuIHVuYSBzZXJpZSBkZSBtdWVzdHJhcyBkZSB0ZWppZG8gcXVlIGNvcnJlc3BvbmRlbg0KYSBjdWF0cm8gdGlwb3MgZGlzdGludG9zIGRlIHR1bW9yZXMgcGVxdWXDsW9zIHJlZG9uZG9zIGRlIGPDqWx1bGFzIGF6dWxlcy4gUGFyYSBjYWRhIG11ZXN0cmEgZGUgdGVqaWRvLCBsYXMNCm1lZGljaW9uZXMgZGUgZXhwcmVzaW9uIGdlbmljYSBlc3RhbiBkaXNwb25pYmxlcy4gRWwgY29uanVudG8gZGUgZGF0b3MgY29uc3RhIGRlIGRhdG9zIGRlIGVudHJlbmFtaWVudG8sIHh0cmFpbg0KeSB5dHJhaW4sIHkgZGF0b3MgZGUgcHJ1ZWJhLCB4dGVzdCB5IHl0ZXN0LiBFeGFtaW5hbW9zIGxhIGRpbWVuc2lvbiBkZSBsb3MgZGF0b3M6DQoNCmBgYHtyfQ0KbGlicmFyeSAoSVNMUikNCm5hbWVzKEtoYW4pDQpkaW0oIEtoYW4keHRyYWluICkNCmRpbSggS2hhbiR4dGVzdCApDQpsZW5ndGggKEtoYW4keXRyYWluICkNCmxlbmd0aCAoS2hhbiR5dGVzdCApDQpgYGANCg0KRXN0ZSBjb25qdW50byBkZSBkYXRvcyBjb25zaXN0ZSBlbiBtZWRpY2lvbmVzIGRlIGV4cHJlc2lvbiBwYXJhIDIsMzA4IGdlbmVzLg0KTG9zIGNvbmp1bnRvcyBkZSBlbnRyZW5hbWllbnRvIHkgcHJ1ZWJhIGNvbnN0YW4gZGUgNjMgeSAyMCBvYnNlcnZhY2lvbmVzIGRlIGZvcm1hIGVzcGVjdGF0aXZhLg0KDQpgYGB7cn0NCnRhYmxlKEtoYW4keXRyYWluICkNCnRhYmxlKEtoYW4keXRlc3QgKQ0KYGBgDQoNClV0aWxpemFyZW1vcyB1biBlbmZvcXVlIGRlIHZlY3RvcmVzIGRlIGFwb3lvIHBhcmEgcHJlZGVjaXIgZWwgc3VidGlwbyBkZSBjYW5jZXIgbWVkaWFudGUgbWVkaWNpb25lcyBkZSBleHByZXNpb24NCmdlbmljYS4gRW4gZXN0ZSBjb25qdW50byBkZSBkYXRvcywgaGF5IHVuYSBncmFuIGNhbnRpZGFkIGRlIGNhcmFjdGVyaXN0aWNhcyBlbiByZWxhY2lvbiBjb24gbGEgY2FudGlkYWQgZGUNCm9ic2VydmFjaW9uZXMuIEVzdG8gc3VnaWVyZSBxdWUgZGViZXJpYW1vcyB1c2FyIHVuIG51Y2xlbyBsaW5lYWwsIHBvcnF1ZSBsYSBmbGV4aWJpbGlkYWQgYWRpY2lvbmFsIHF1ZSByZXN1bHRhcmENCmRlbCB1c28gZGUgdW4gbnVjbGVvIHBvbGlub21pYWwgbyByYWRpYWwgZXMgaW5uZWNlc2FyaWEuDQoNCmBgYHtyfQ0KZGF0PWRhdGEuZnJhbWUoeD1LaGFuJHh0cmFpbiAsIHk9YXMuZmFjdG9yICggS2hhbiR5dHJhaW4gKSkNCm91dD1zdm0oeeKIvC4sIGRhdGE9ZGF0ICwga2VybmVsID0ibGluZWFyIixjb3N0ID0xMCkNCnN1bW1hcnkgKG91dCkNCnRhYmxlKG91dCRmaXR0ZWQgLCBkYXQkeSkNCmBgYA0KDQpWZW1vcyBxdWUgbm8gaGF5IGVycm9yZXMgZGUgZW50cmVuYW1pZW50by4gRGUgaGVjaG8sIGVzdG8gbm8gZXMgc29ycHJlbmRlbnRlLCBkZWJpZG8gYSBxdWUgbGEgZ3JhbiBjYW50aWRhZCBkZQ0KdmFyaWFibGVzIGVuIHJlbGFjaW9uIGNvbiBsYSBjYW50aWRhZCBkZSBvYnNlcnZhY2lvbmVzIGltcGxpY2EgcXVlIGVzIGZhY2lsIGVuY29udHJhciBoaXBlcnBsYW5vcyBxdWUgc2VwYXJhbg0KbGFzIGNsYXNlcyBwb3IgY29tcGxldG8uIEVzdGFtb3MgbWFzIGludGVyZXNhZG9zIG5vIGVuIGVsIGRlc2VtcGXDsW8gZGVsIGNsYXNpZmljYWRvciBkZSB2ZWN0b3JlcyBkZSBzb3BvcnRlIGVuDQpsYXMgb2JzZXJ2YWNpb25lcyBkZSBlbnRyZW5hbWllbnRvLCBzaW5vIGVuIHN1IGRlc2VtcGXDsW8gZW4gbGFzIG9ic2VydmFjaW9uZXMgZGUgcHJ1ZWJhLg0KDQpgYGB7cn0NCmRhdC50ZT1kYXRhLmZyYW1lKHg9S2hhbiR4dGVzdCAsIHk9YXMuZmFjdG9yIChLaGFuJHl0ZXN0ICkpDQpwcmVkLnRlPXByZWRpY3QgKG91dCAsIG5ld2RhdGEgPWRhdC50ZSkNCnRhYmxlKHByZWQudGUgLCBkYXQudGUkeSkNCmBgYA0KDQpWZW1vcyBxdWUgdXNhciBjb3N0ID0gMTAgcHJvZHVjZSBkb3MgZXJyb3JlcyBkZSBjb25qdW50byBkZSBwcnVlYmEgZW4gZXN0b3MgZGF0b3MuDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQo=