Cesar Tinoco - 13003387

8.3.1 Clasificando Arboles de Clasificacion

La libreria Tree se utiliza para construir arboles de clasificacion y regresion. Ahora cargaremos la libreria.

library(tree)

Primero usamos classification tree para analizar el conjunto de datos de Carseats. Sale es una variable continua, por lo que comenzamos por recodificarla como una variable binaria. Usamos la función ifelse() para crear una variable, llamada Alto, que toma un valor de Yes si la variable Sale supera 8, y toma un valor de No en caso contrario.

library (ISLR)
attach (Carseats )
High=ifelse (Sales <=8," No"," Yes ")

Finalmente, usamos la funcion data.frame() para fusionar High con el resto de los datos de Carseats.

Carseats =data.frame(Carseats ,High)

Ahora usamos la funcion tree() para ajustar un arbol de clasificacion con el fin de predecir High usando todas las variables excepto Sale. La sintaxis de la función tree() es bastante similar a la de la funcion lm().

tree.carseats =tree(High∼.-Sales ,Carseats )

La funcion de summary() enumera las variables que se utilizan como nodos internos en el arbol, el numero de nodos terminales y la tasa de error (entrenamiento).

summary (tree.carseats )

Classification tree:
tree(formula = High ~ . - Sales, data = Carseats)
Variables actually used in tree construction:
[1] "ShelveLoc"   "Price"       "Income"      "CompPrice"   "Population"  "Advertising" "Age"        
[8] "US"         
Number of terminal nodes:  27 
Residual mean deviance:  0.4575 = 170.7 / 373 
Misclassification error rate: 0.09 = 36 / 400 

Vemos que la tasa de error de entrenamiento es del 9%. Una de las propiedades mas atractivas de los arboles es que se ppueden mostrar graficamente. Usamos la funcion plot() para mostrar la estructura del arbol, y la funcion text() para mostrar las etiquetas de nodo. El argumento pretty = 0 indica a R que ha incluido los nombres de categoria para cualquier predictor cualitativo, en lugar de simplemente mostrar una letra para cada categoria.

plot(tree.carseats )
text(tree.carseats ,pretty =0)

Las ramas que conducen a los nodos terminales se indican mediante asteriscos.

tree.carseats
node), split, n, deviance, yval, (yprob)
      * denotes terminal node

  1) root 400 541.500  No ( 0.59000 0.41000 )  
    2) ShelveLoc: Bad,Medium 315 390.600  No ( 0.68889 0.31111 )  
      4) Price < 92.5 46  56.530  Yes  ( 0.30435 0.69565 )  
        8) Income < 57 10  12.220  No ( 0.70000 0.30000 )  
         16) CompPrice < 110.5 5   0.000  No ( 1.00000 0.00000 ) *
         17) CompPrice > 110.5 5   6.730  Yes  ( 0.40000 0.60000 ) *
        9) Income > 57 36  35.470  Yes  ( 0.19444 0.80556 )  
         18) Population < 207.5 16  21.170  Yes  ( 0.37500 0.62500 ) *
         19) Population > 207.5 20   7.941  Yes  ( 0.05000 0.95000 ) *
      5) Price > 92.5 269 299.800  No ( 0.75465 0.24535 )  
       10) Advertising < 13.5 224 213.200  No ( 0.81696 0.18304 )  
         20) CompPrice < 124.5 96  44.890  No ( 0.93750 0.06250 )  
           40) Price < 106.5 38  33.150  No ( 0.84211 0.15789 )  
             80) Population < 177 12  16.300  No ( 0.58333 0.41667 )  
              160) Income < 60.5 6   0.000  No ( 1.00000 0.00000 ) *
              161) Income > 60.5 6   5.407  Yes  ( 0.16667 0.83333 ) *
             81) Population > 177 26   8.477  No ( 0.96154 0.03846 ) *
           41) Price > 106.5 58   0.000  No ( 1.00000 0.00000 ) *
         21) CompPrice > 124.5 128 150.200  No ( 0.72656 0.27344 )  
           42) Price < 122.5 51  70.680  Yes  ( 0.49020 0.50980 )  
             84) ShelveLoc: Bad 11   6.702  No ( 0.90909 0.09091 ) *
             85) ShelveLoc: Medium 40  52.930  Yes  ( 0.37500 0.62500 )  
              170) Price < 109.5 16   7.481  Yes  ( 0.06250 0.93750 ) *
              171) Price > 109.5 24  32.600  No ( 0.58333 0.41667 )  
                342) Age < 49.5 13  16.050  Yes  ( 0.30769 0.69231 ) *
                343) Age > 49.5 11   6.702  No ( 0.90909 0.09091 ) *
           43) Price > 122.5 77  55.540  No ( 0.88312 0.11688 )  
             86) CompPrice < 147.5 58  17.400  No ( 0.96552 0.03448 ) *
             87) CompPrice > 147.5 19  25.010  No ( 0.63158 0.36842 )  
              174) Price < 147 12  16.300  Yes  ( 0.41667 0.58333 )  
                348) CompPrice < 152.5 7   5.742  Yes  ( 0.14286 0.85714 ) *
                349) CompPrice > 152.5 5   5.004  No ( 0.80000 0.20000 ) *
              175) Price > 147 7   0.000  No ( 1.00000 0.00000 ) *
       11) Advertising > 13.5 45  61.830  Yes  ( 0.44444 0.55556 )  
         22) Age < 54.5 25  25.020  Yes  ( 0.20000 0.80000 )  
           44) CompPrice < 130.5 14  18.250  Yes  ( 0.35714 0.64286 )  
             88) Income < 100 9  12.370  No ( 0.55556 0.44444 ) *
             89) Income > 100 5   0.000  Yes  ( 0.00000 1.00000 ) *
           45) CompPrice > 130.5 11   0.000  Yes  ( 0.00000 1.00000 ) *
         23) Age > 54.5 20  22.490  No ( 0.75000 0.25000 )  
           46) CompPrice < 122.5 10   0.000  No ( 1.00000 0.00000 ) *
           47) CompPrice > 122.5 10  13.860  No ( 0.50000 0.50000 )  
             94) Price < 125 5   0.000  Yes  ( 0.00000 1.00000 ) *
             95) Price > 125 5   0.000  No ( 1.00000 0.00000 ) *
    3) ShelveLoc: Good 85  90.330  Yes  ( 0.22353 0.77647 )  
      6) Price < 135 68  49.260  Yes  ( 0.11765 0.88235 )  
       12) US: No 17  22.070  Yes  ( 0.35294 0.64706 )  
         24) Price < 109 8   0.000  Yes  ( 0.00000 1.00000 ) *
         25) Price > 109 9  11.460  No ( 0.66667 0.33333 ) *
       13) US: Yes 51  16.880  Yes  ( 0.03922 0.96078 ) *
      7) Price > 135 17  22.070  No ( 0.64706 0.35294 )  
       14) Income < 46 6   0.000  No ( 1.00000 0.00000 ) *
       15) Income > 46 11  15.160  Yes  ( 0.45455 0.54545 ) *

Este enfoque conduce a predicciones correctas para alrededor del 71.5% de las ubicaciones en el conjunto de datos de prueba.

set.seed (2)
train=sample (1: nrow(Carseats ), 200)
Carseats.test=Carseats [-train ,]
High.test=High[-train ]
tree.carseats =tree(High∼.-Sales ,Carseats ,subset =train )
tree.pred=predict (tree.carseats ,Carseats.test ,type ="class")
table(tree.pred ,High.test)
         High.test
tree.pred  No  Yes 
     No    86    27
     Yes   30    57
(86+57) /200
[1] 0.715

A continuacion, consideramos si la poda del arbol puede llevar a mejores resultados. La función cv.tree() realiza una validacion cruzada para determinar el nivel optimo de complejidad del arbol; la poda de complejidad de costos se utiliza para seleccionar una secuencia de arboles para su consideracion. Utilizamos el argumento FUN=prune.misclass para indicar que queremos que la tasa de error de clasificacion guie el proceso de validacion cruzada y poda, en lugar del valor predeterminado para la función cv.tree(), que es la desviacion. La funcion cv.tree() informa el numero de nodos terminales de cada arbol considerado(tamaño), asi como la tasa de error correspondiente y el valor del parametro de costo-complejidad utilizado (k, que corresponde a α en (8.4)).

set.seed (3)
cv.carseats =cv.tree(tree.carseats ,FUN=prune.misclass )
names(cv.carseats )
[1] "size"   "dev"    "k"      "method"
cv.carseats
$size
[1] 19 17 14 13  9  7  3  2  1

$dev
[1] 55 55 53 52 50 56 69 65 80

$k
[1]       -Inf  0.0000000  0.6666667  1.0000000  1.7500000  2.0000000  4.2500000  5.0000000 23.0000000

$method
[1] "misclass"

attr(,"class")
[1] "prune"         "tree.sequence"

Tenga en cuenta que, a pesar del nombre, dev corresponde a la tasa de error de validacion cruzada en esta instancia. El arbol con 9 nodos terminales produce la tasa de error de validacion cruzada mas baja, con 50 errores de validacion cruzada. Trazamos la tasa de error como una funcion de tamaño y k.

par(mfrow =c(1,2))
plot(cv.carseats$size ,cv.carseats$dev ,type="b")
plot(cv.carseats$k ,cv.carseats$dev ,type="b")

Ahora aplicamos la funcion prune.misclass() para podar el arbol y obtener el arbol de nueve nodos.

prune.carseats =prune.misclass (tree.carseats ,best =9)
plot(prune.carseats )
text(prune.carseats ,pretty =0)

¿Que tan bien se desempeña este arbol podado en el conjunto de datos de prueba? Una vez mas, aplicamos la funcion predict().

tree.pred=predict (prune.carseats , Carseats.test ,type="class")
table(tree.pred ,High.test)
         High.test
tree.pred  No  Yes 
     No    94    24
     Yes   22    60
(94+60) /200
[1] 0.77

Ahora, el 77% de las observaciones de prueba se clasifican correctamente, por lo que el proceso de poda no solo produjo un arbol mas interpretable, sino que tambien mejoro la precision de la clasificacion. Si aumentamos el valor de best, obtenemos un arbol podado mas grande con una menor precision de clasificacion.

prune.carseats =prune.misclass (tree.carseats ,best =15)
plot(prune.carseats )
text(prune.carseats ,pretty =0)

tree.pred=predict (prune.carseats , Carseats.test ,type="class")
table(tree.pred ,High.test)
         High.test
tree.pred  No  Yes 
     No    86    22
     Yes   30    62
(86+62) /200
[1] 0.74

8.3.2 Ajuste de Arboles de Regresion

Aqui ajustamos un arbol de regresion al conjunto de datos de Boston. Primero, creamos un conjunto de entrenamiento, y ajustamos el arbol a los datos de entrenamiento.

library (MASS)
set.seed (1)
train = sample (1: nrow(Boston ), nrow(Boston )/2)
tree.boston =tree(medv∼.,Boston ,subset =train)
summary (tree.boston )

Regression tree:
tree(formula = medv ~ ., data = Boston, subset = train)
Variables actually used in tree construction:
[1] "lstat" "rm"    "dis"  
Number of terminal nodes:  8 
Residual mean deviance:  12.65 = 3099 / 245 
Distribution of residuals:
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
-14.10000  -2.04200  -0.05357   0.00000   1.96000  12.60000 

Observe que la salida de summary() indica que solo tres de las variables se han utilizado para construir el arbol. En el contexto de un arbol de regresion, la desviacion es simplemente la suma de los errores cuadrados para el arbol. Ahora trazamos el árbol.

plot(tree.boston )
text(tree.boston ,pretty =0)

Ahora usamos la funcion cv.tree() para ver si la poda del arbol mejorar el rendimiento.

cv.boston =cv.tree(tree.boston )
plot(cv.boston$size ,cv.boston$dev ,type='b')

En este caso, el arbol mas complejo se selecciona mediante validacion cruzada. Sin embargo, si deseamos podar el arbol, podriamos hacerlo de la siguiente manera, utilizando la funcion prune.tree()

prune.boston =prune.tree(tree.boston ,best =5)
plot(prune.boston )
text(prune.boston ,pretty =0)

De acuerdo con los resultados de la validacion cruzada, usamos el arbol no afinado para hacer predicciones en el conjunto de pruebas

yhat=predict (tree.boston ,newdata =Boston [-train ,])
boston.test=Boston [-train ,"medv"]
plot(yhat ,boston.test)
abline (0,1)

mean((yhat -boston.test)^2)
[1] 25.04559

La raiz cuadrada del MSE es por lo tanto alrededor de 5.005, indicando que este modelo conduce a predicciones de prueba que estan dentro de aproximadamente $ 5, 005 del valor medio real de la vivienda para el suburbio.

8.3.3 Sacos y Bosques Aleatorios

Aqui aplicamos empaquetamiento y bosques aleatorios a los datos de Boston, usando el paquete randomForest en R. Los resultados exactos obtenidos en esta seccion pueden depender de la version de R y la version del paquete randomForest instalada en la computadora, Recordemos que el embolsado es simplemente un caso especial de un bosque al azar con m = p. Por lo tanto, la funcion randomForest() se puede utilizar para realizar tanto bosques aleatorios como empaquetamiento. Realizamos embolsado como sigue.

library (randomForest)
randomForest 4.6-14
Type rfNews() to see new features/changes/bug fixes.
set.seed (1)
bag.boston =randomForest(medv∼.,data=Boston ,subset =train ,
mtry=13, importance =TRUE)
bag.boston

Call:
 randomForest(formula = medv ~ ., data = Boston, mtry = 13, importance = TRUE,      subset = train) 
               Type of random forest: regression
                     Number of trees: 500
No. of variables tried at each split: 13

          Mean of squared residuals: 11.15723
                    % Var explained: 86.49

El argumento mtry = 13 indica que se deben considerar los 13 predictores para cada division del arbol; en otras palabras, se debe realizar el empaquetamiento. ¿Que tan bien se desempeña este modelo embolsado en el conjunto de prueba?

yhat.bag = predict (bag.boston ,newdata =Boston [-train ,])
plot(yhat.bag , boston.test)
abline (0,1)

mean(( yhat.bag -boston.test)^2)
[1] 13.50808

El MSE del conjunto de pruebas asociado con el arbol de regresion empaquetado es 13.16, casi la mitad que se obtiene utilizando un arbol unico optimizado. Podriamos cambiar el numero de arboles cultivados por randomForest() usando el argumento ntree.

bag.boston =randomForest(medv∼.,data=Boston ,subset =train ,
mtry=13, ntree =25)
yhat.bag = predict (bag.boston ,newdata =Boston [-train ,])
mean(( yhat.bag -boston.test)^2)
[1] 13.94835

El crecimiento de un bosque aleatorio procede exactamente de la misma manera, excepto que usamos un valor mas pequeño del argumento mtry. De forma predeterminada, randomForest() u√s p / 3 variables cuando se crea un bosque aleatorio de arboles de regresion, y p variables cuando se crea un bosque aleatorio de arboles de clasificacion. Aqui usamos mtry = 6.

set.seed (1)
rf.boston =randomForest(medv∼.,data=Boston ,subset =train ,
mtry=6, importance =TRUE)
yhat.rf = predict (rf.boston ,newdata =Boston [-train ,])
mean(( yhat.rf -boston.test)^2)
[1] 11.66454

El conjunto de prueba MSE es 11.31; esto indica que los bosques aleatorios produjeron una mejora sobre el empaquetamiento en este caso. Usando la funcion importance(), podemos ver la importancia de cada variable.

importance (rf.boston )
          %IncMSE IncNodePurity
crim    12.132320     986.50338
zn       1.955579      57.96945
indus    9.069302     882.78261
chas     2.210835      45.22941
nox     11.104823    1044.33776
rm      31.784033    6359.31971
age     10.962684     516.82969
dis     15.015236    1224.11605
rad      4.118011      95.94586
tax      8.587932     502.96719
ptratio 12.503896     830.77523
black    6.702609     341.30361
lstat   30.695224    7505.73936

Se pueden producir graficos de estas medidas de importancia utilizando la funcion varImpPlot().

varImpPlot (rf.boston )

Los resultados indican que en todos los arboles considerados en el bosque aleatorio, el nivel de riqueza de la comunidad (lstat) y el tamaño de la casa (rm) son, con mucho, las dos variables mas importantes.

8.3.4 Boosting

Aquí usamos el paquete gbm, y dentro de el la funcion gbm(), para ajustar los arboles de regresion aumentados al conjunto de datos de Boston. Ejecutamos gbm() con la opcion distribution = “gaussian” ya que este es un problema de regresion; Si se tratara de un problema de clasificacion binaria, utilizaríamos distribution = “bernoulli”.

library (gbm)
Loaded gbm 2.1.5
set.seed (1)
boost.boston =gbm(medv∼.,data=Boston [train ,], distribution= "gaussian",n.trees =5000 , interaction.depth =4)

La funcion summary() produce un grafico de influencia relativa y tambien genera las estadisticas de influencia relativa.

summary (boost.boston )

Vemos que lstat y rm son las variables más importantes. Podemos Tambien se producir parcelas de dependencia parcial para estas dos variables. En este caso, como cabria esperar, la mediana. Los precios de las casas aumentan con rm y disminuyen con lstat.

par(mfrow =c(1,2))
plot(boost.boston ,i="rm")

plot(boost.boston ,i="lstat")

Ahora usamos el modelo reforzado para predecir medv en el conjunto de prueba:

yhat.boost=predict (boost.boston ,newdata =Boston [-train ,],
n.trees =5000)
mean(( yhat.boost -boston.test)^2)
[1] 10.81479

La prueba MSE obtenida es 11.8; Similar al MSE de prueba para bosques aleatorios y superior al de ensacado. Si queremos, podemos realizar un aumento con un valor diferente del parametro de contraccion λ en (8.10). El valor predeterminado es 0.001, pero esto se modifica facilmente. Aquí tomamos λ = 0.2.

boost.boston =gbm(medv∼.,data=Boston [train ,], distribution="gaussian",n.trees =5000 , interaction.depth =4, shrinkage =0.2,
verbose =F)
yhat.boost=predict (boost.boston ,newdata =Boston [-train ,],
n.trees =5000)
mean(( yhat.boost -boston.test)^2)
[1] 11.51109
LS0tDQp0aXRsZTogIkxhYm9yYXRvcmlvIDguMyAtIEFyYm9sZXMgZGUgZGVjaXNpb24iDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIyMgQ2VzYXIgVGlub2NvIC0gMTMwMDMzODcNCg0KIyMgOC4zLjEgQ2xhc2lmaWNhbmRvIEFyYm9sZXMgZGUgQ2xhc2lmaWNhY2lvbg0KDQpMYSBsaWJyZXJpYSBUcmVlIHNlIHV0aWxpemEgcGFyYSBjb25zdHJ1aXIgYXJib2xlcyBkZSBjbGFzaWZpY2FjaW9uIHkgcmVncmVzaW9uLg0KQWhvcmEgY2FyZ2FyZW1vcyBsYSBsaWJyZXJpYS4NCg0KYGBge3J9DQpsaWJyYXJ5KHRyZWUpDQpgYGANCg0KUHJpbWVybyB1c2Ftb3MgY2xhc3NpZmljYXRpb24gdHJlZSBwYXJhIGFuYWxpemFyIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIENhcnNlYXRzLiBTYWxlIGVzIHVuYSB2YXJpYWJsZQ0KY29udGludWEsIHBvciBsbyBxdWUgY29tZW56YW1vcyBwb3IgcmVjb2RpZmljYXJsYSBjb21vIHVuYSB2YXJpYWJsZSBiaW5hcmlhLiBVc2Ftb3MgbGEgZnVuY2nDs24gaWZlbHNlKCkgcGFyYQ0KY3JlYXIgdW5hIHZhcmlhYmxlLCBsbGFtYWRhIEFsdG8sIHF1ZSB0b21hIHVuIHZhbG9yIGRlIFllcyBzaSBsYSB2YXJpYWJsZSBTYWxlIHN1cGVyYSA4LCB5IHRvbWEgdW4gdmFsb3IgZGUgTm8gDQplbiBjYXNvIGNvbnRyYXJpby4NCg0KYGBge3J9DQpsaWJyYXJ5IChJU0xSKQ0KYXR0YWNoIChDYXJzZWF0cyApDQpIaWdoPWlmZWxzZSAoU2FsZXMgPD04LCIgTm8iLCIgWWVzICIpDQpgYGANCg0KRmluYWxtZW50ZSwgdXNhbW9zIGxhIGZ1bmNpb24gZGF0YS5mcmFtZSgpIHBhcmEgZnVzaW9uYXIgSGlnaCBjb24gZWwgcmVzdG8gZGUgbG9zIGRhdG9zIGRlIENhcnNlYXRzLg0KDQpgYGB7cn0NCkNhcnNlYXRzID1kYXRhLmZyYW1lKENhcnNlYXRzICxIaWdoKQ0KYGBgDQoNCkFob3JhIHVzYW1vcyBsYSBmdW5jaW9uIHRyZWUoKSBwYXJhIGFqdXN0YXIgdW4gYXJib2wgZGUgY2xhc2lmaWNhY2lvbiBjb24gZWwgZmluIGRlIHByZWRlY2lyIEhpZ2ggdXNhbmRvIHRvZGFzDQpsYXMgdmFyaWFibGVzIGV4Y2VwdG8gU2FsZS4gTGEgc2ludGF4aXMgZGUgbGEgZnVuY2nDs24gdHJlZSgpIGVzIGJhc3RhbnRlIHNpbWlsYXIgYSBsYSBkZSBsYSBmdW5jaW9uIGxtKCkuDQoNCmBgYHtyfQ0KdHJlZS5jYXJzZWF0cyA9dHJlZShIaWdo4oi8Li1TYWxlcyAsQ2Fyc2VhdHMgKQ0KYGBgDQoNCkxhIGZ1bmNpb24gZGUgc3VtbWFyeSgpIGVudW1lcmEgbGFzIHZhcmlhYmxlcyBxdWUgc2UgdXRpbGl6YW4gY29tbyBub2RvcyBpbnRlcm5vcyBlbiBlbCBhcmJvbCwgZWwgbnVtZXJvIA0KZGUgbm9kb3MgdGVybWluYWxlcyB5IGxhIHRhc2EgZGUgZXJyb3IgKGVudHJlbmFtaWVudG8pLg0KDQpgYGB7cn0NCnN1bW1hcnkgKHRyZWUuY2Fyc2VhdHMgKQ0KYGBgDQoNClZlbW9zIHF1ZSBsYSB0YXNhIGRlIGVycm9yIGRlIGVudHJlbmFtaWVudG8gZXMgZGVsIDklLiBVbmEgZGUgbGFzIHByb3BpZWRhZGVzIG1hcyBhdHJhY3RpdmFzIGRlIGxvcyBhcmJvbGVzIGVzDQpxdWUgc2UgcHB1ZWRlbiBtb3N0cmFyIGdyYWZpY2FtZW50ZS4gVXNhbW9zIGxhIGZ1bmNpb24gcGxvdCgpIHBhcmEgbW9zdHJhciBsYSBlc3RydWN0dXJhIGRlbCBhcmJvbCwgeSBsYSBmdW5jaW9uDQp0ZXh0KCkgcGFyYSBtb3N0cmFyIGxhcyBldGlxdWV0YXMgZGUgbm9kby4gRWwgYXJndW1lbnRvIHByZXR0eSA9IDAgaW5kaWNhIGEgUiBxdWUgaGEgaW5jbHVpZG8gbG9zIG5vbWJyZXMgZGUNCmNhdGVnb3JpYSBwYXJhIGN1YWxxdWllciBwcmVkaWN0b3IgY3VhbGl0YXRpdm8sIGVuIGx1Z2FyIGRlIHNpbXBsZW1lbnRlIG1vc3RyYXIgdW5hIGxldHJhIHBhcmEgY2FkYSBjYXRlZ29yaWEuDQoNCmBgYHtyfQ0KcGxvdCh0cmVlLmNhcnNlYXRzICkNCnRleHQodHJlZS5jYXJzZWF0cyAscHJldHR5ID0wKQ0KYGBgDQoNCkxhcyByYW1hcyBxdWUgY29uZHVjZW4gYSBsb3Mgbm9kb3MgdGVybWluYWxlcyBzZSBpbmRpY2FuIG1lZGlhbnRlIGFzdGVyaXNjb3MuDQoNCmBgYHtyfQ0KdHJlZS5jYXJzZWF0cw0KYGBgDQoNCkVzdGUgZW5mb3F1ZSBjb25kdWNlIGEgcHJlZGljY2lvbmVzIGNvcnJlY3RhcyBwYXJhIGFscmVkZWRvciBkZWwgNzEuNSUgZGUgbGFzIHViaWNhY2lvbmVzIGVuIGVsIGNvbmp1bnRvIGRlIGRhdG9zDQpkZSBwcnVlYmEuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQgKDIpDQp0cmFpbj1zYW1wbGUgKDE6IG5yb3coQ2Fyc2VhdHMgKSwgMjAwKQ0KQ2Fyc2VhdHMudGVzdD1DYXJzZWF0cyBbLXRyYWluICxdDQpIaWdoLnRlc3Q9SGlnaFstdHJhaW4gXQ0KdHJlZS5jYXJzZWF0cyA9dHJlZShIaWdo4oi8Li1TYWxlcyAsQ2Fyc2VhdHMgLHN1YnNldCA9dHJhaW4gKQ0KdHJlZS5wcmVkPXByZWRpY3QgKHRyZWUuY2Fyc2VhdHMgLENhcnNlYXRzLnRlc3QgLHR5cGUgPSJjbGFzcyIpDQp0YWJsZSh0cmVlLnByZWQgLEhpZ2gudGVzdCkNCig4Nis1NykgLzIwMA0KYGBgDQoNCkEgY29udGludWFjaW9uLCBjb25zaWRlcmFtb3Mgc2kgbGEgcG9kYSBkZWwgYXJib2wgcHVlZGUgbGxldmFyIGEgbWVqb3JlcyByZXN1bHRhZG9zLiBMYSBmdW5jacOzbiBjdi50cmVlKCkgcmVhbGl6YQ0KdW5hIHZhbGlkYWNpb24gY3J1emFkYSBwYXJhIGRldGVybWluYXIgZWwgbml2ZWwgb3B0aW1vIGRlIGNvbXBsZWppZGFkIGRlbCBhcmJvbDsgbGEgcG9kYSBkZSBjb21wbGVqaWRhZCBkZSBjb3N0b3MNCnNlIHV0aWxpemEgcGFyYSBzZWxlY2Npb25hciB1bmEgc2VjdWVuY2lhIGRlIGFyYm9sZXMgcGFyYSBzdSBjb25zaWRlcmFjaW9uLiBVdGlsaXphbW9zIGVsIGFyZ3VtZW50bw0KRlVOPXBydW5lLm1pc2NsYXNzIHBhcmEgaW5kaWNhciBxdWUgcXVlcmVtb3MgcXVlIGxhIHRhc2EgZGUgZXJyb3IgZGUgY2xhc2lmaWNhY2lvbiBndWllIGVsIHByb2Nlc28gZGUgdmFsaWRhY2lvbg0KY3J1emFkYSB5IHBvZGEsIGVuIGx1Z2FyIGRlbCB2YWxvciBwcmVkZXRlcm1pbmFkbyBwYXJhIGxhIGZ1bmNpw7NuIGN2LnRyZWUoKSwgcXVlIGVzIGxhIGRlc3ZpYWNpb24uIExhIGZ1bmNpb24NCmN2LnRyZWUoKSBpbmZvcm1hIGVsIG51bWVybyBkZSBub2RvcyB0ZXJtaW5hbGVzIGRlIGNhZGEgYXJib2wgY29uc2lkZXJhZG8odGFtYcOxbyksIGFzaSBjb21vIGxhIHRhc2EgZGUgZXJyb3INCmNvcnJlc3BvbmRpZW50ZSB5IGVsIHZhbG9yIGRlbCBwYXJhbWV0cm8gZGUgY29zdG8tY29tcGxlamlkYWQgdXRpbGl6YWRvIChrLCBxdWUgY29ycmVzcG9uZGUgYSDOsSBlbiAoOC40KSkuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQgKDMpDQpjdi5jYXJzZWF0cyA9Y3YudHJlZSh0cmVlLmNhcnNlYXRzICxGVU49cHJ1bmUubWlzY2xhc3MgKQ0KbmFtZXMoY3YuY2Fyc2VhdHMgKQ0KY3YuY2Fyc2VhdHMNCmBgYA0KDQpUZW5nYSBlbiBjdWVudGEgcXVlLCBhIHBlc2FyIGRlbCBub21icmUsIGRldiBjb3JyZXNwb25kZSBhIGxhIHRhc2EgZGUgZXJyb3IgZGUgdmFsaWRhY2lvbiBjcnV6YWRhIGVuIGVzdGENCmluc3RhbmNpYS4gRWwgYXJib2wgY29uIDkgbm9kb3MgdGVybWluYWxlcyBwcm9kdWNlIGxhIHRhc2EgZGUgZXJyb3IgZGUgdmFsaWRhY2lvbiBjcnV6YWRhIG1hcyBiYWphLCBjb24gNTANCmVycm9yZXMgZGUgdmFsaWRhY2lvbiBjcnV6YWRhLiBUcmF6YW1vcyBsYSB0YXNhIGRlIGVycm9yIGNvbW8gdW5hIGZ1bmNpb24gZGUgdGFtYcOxbyB5IGsuDQoNCmBgYHtyfQ0KcGFyKG1mcm93ID1jKDEsMikpDQpwbG90KGN2LmNhcnNlYXRzJHNpemUgLGN2LmNhcnNlYXRzJGRldiAsdHlwZT0iYiIpDQpwbG90KGN2LmNhcnNlYXRzJGsgLGN2LmNhcnNlYXRzJGRldiAsdHlwZT0iYiIpDQpgYGANCg0KQWhvcmEgYXBsaWNhbW9zIGxhIGZ1bmNpb24gcHJ1bmUubWlzY2xhc3MoKSBwYXJhIHBvZGFyIGVsIGFyYm9sIHkgb2J0ZW5lciBlbCBhcmJvbCBkZSBudWV2ZSBub2Rvcy4NCg0KYGBge3J9DQpwcnVuZS5jYXJzZWF0cyA9cHJ1bmUubWlzY2xhc3MgKHRyZWUuY2Fyc2VhdHMgLGJlc3QgPTkpDQpwbG90KHBydW5lLmNhcnNlYXRzICkNCnRleHQocHJ1bmUuY2Fyc2VhdHMgLHByZXR0eSA9MCkNCmBgYA0KDQrCv1F1ZSB0YW4gYmllbiBzZSBkZXNlbXBlw7FhIGVzdGUgYXJib2wgcG9kYWRvIGVuIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIHBydWViYT8gVW5hIHZleiBtYXMsIGFwbGljYW1vcyBsYQ0KZnVuY2lvbiBwcmVkaWN0KCkuDQoNCmBgYHtyfQ0KdHJlZS5wcmVkPXByZWRpY3QgKHBydW5lLmNhcnNlYXRzICwgQ2Fyc2VhdHMudGVzdCAsdHlwZT0iY2xhc3MiKQ0KdGFibGUodHJlZS5wcmVkICxIaWdoLnRlc3QpDQooOTQrNjApIC8yMDANCmBgYA0KDQpBaG9yYSwgZWwgNzclIGRlIGxhcyBvYnNlcnZhY2lvbmVzIGRlIHBydWViYSBzZSBjbGFzaWZpY2FuIGNvcnJlY3RhbWVudGUsIHBvciBsbyBxdWUgZWwgcHJvY2VzbyBkZSBwb2RhIG5vIHNvbG8NCnByb2R1am8gdW4gYXJib2wgbWFzIGludGVycHJldGFibGUsIHNpbm8gcXVlIHRhbWJpZW4gbWVqb3JvIGxhIHByZWNpc2lvbiBkZSBsYSBjbGFzaWZpY2FjaW9uLg0KU2kgYXVtZW50YW1vcyBlbCB2YWxvciBkZSBiZXN0LCBvYnRlbmVtb3MgdW4gYXJib2wgcG9kYWRvIG1hcyBncmFuZGUgY29uIHVuYSBtZW5vciBwcmVjaXNpb24gZGUgY2xhc2lmaWNhY2lvbi4NCg0KYGBge3J9DQpwcnVuZS5jYXJzZWF0cyA9cHJ1bmUubWlzY2xhc3MgKHRyZWUuY2Fyc2VhdHMgLGJlc3QgPTE1KQ0KcGxvdChwcnVuZS5jYXJzZWF0cyApDQp0ZXh0KHBydW5lLmNhcnNlYXRzICxwcmV0dHkgPTApDQp0cmVlLnByZWQ9cHJlZGljdCAocHJ1bmUuY2Fyc2VhdHMgLCBDYXJzZWF0cy50ZXN0ICx0eXBlPSJjbGFzcyIpDQp0YWJsZSh0cmVlLnByZWQgLEhpZ2gudGVzdCkNCig4Nis2MikgLzIwMA0KYGBgDQoNCiMjIDguMy4yIEFqdXN0ZSBkZSBBcmJvbGVzIGRlIFJlZ3Jlc2lvbg0KDQpBcXVpIGFqdXN0YW1vcyB1biBhcmJvbCBkZSByZWdyZXNpb24gYWwgY29uanVudG8gZGUgZGF0b3MgZGUgQm9zdG9uLiBQcmltZXJvLCBjcmVhbW9zIHVuIGNvbmp1bnRvIGRlDQplbnRyZW5hbWllbnRvLCB5IGFqdXN0YW1vcyBlbCBhcmJvbCBhIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvLg0KDQpgYGB7cn0NCmxpYnJhcnkgKE1BU1MpDQpzZXQuc2VlZCAoMSkNCnRyYWluID0gc2FtcGxlICgxOiBucm93KEJvc3RvbiApLCBucm93KEJvc3RvbiApLzIpDQp0cmVlLmJvc3RvbiA9dHJlZShtZWR24oi8LixCb3N0b24gLHN1YnNldCA9dHJhaW4pDQpzdW1tYXJ5ICh0cmVlLmJvc3RvbiApDQoNCmBgYA0KDQpPYnNlcnZlIHF1ZSBsYSBzYWxpZGEgZGUgc3VtbWFyeSgpIGluZGljYSBxdWUgc29sbyB0cmVzIGRlIGxhcyB2YXJpYWJsZXMgc2UgaGFuIHV0aWxpemFkbyBwYXJhIGNvbnN0cnVpciBlbA0KYXJib2wuIEVuIGVsIGNvbnRleHRvIGRlIHVuIGFyYm9sIGRlIHJlZ3Jlc2lvbiwgbGEgZGVzdmlhY2lvbiBlcyBzaW1wbGVtZW50ZSBsYSBzdW1hIGRlIGxvcyBlcnJvcmVzIGN1YWRyYWRvcyBwYXJhIGVsIGFyYm9sLiBBaG9yYSB0cmF6YW1vcyBlbCDDoXJib2wuDQoNCmBgYHtyfQ0KcGxvdCh0cmVlLmJvc3RvbiApDQp0ZXh0KHRyZWUuYm9zdG9uICxwcmV0dHkgPTApDQpgYGANCg0KQWhvcmEgdXNhbW9zIGxhIGZ1bmNpb24gY3YudHJlZSgpIHBhcmEgdmVyIHNpIGxhIHBvZGEgZGVsIGFyYm9sIG1lam9yYXIgZWwgcmVuZGltaWVudG8uDQoNCmBgYHtyfQ0KY3YuYm9zdG9uID1jdi50cmVlKHRyZWUuYm9zdG9uICkNCnBsb3QoY3YuYm9zdG9uJHNpemUgLGN2LmJvc3RvbiRkZXYgLHR5cGU9J2InKQ0KYGBgDQoNCkVuIGVzdGUgY2FzbywgZWwgYXJib2wgbWFzIGNvbXBsZWpvIHNlIHNlbGVjY2lvbmEgbWVkaWFudGUgdmFsaWRhY2lvbiBjcnV6YWRhLiBTaW4gZW1iYXJnbywgc2kgZGVzZWFtb3MgcG9kYXINCmVsIGFyYm9sLCBwb2RyaWFtb3MgaGFjZXJsbyBkZSBsYSBzaWd1aWVudGUgbWFuZXJhLCB1dGlsaXphbmRvIGxhIGZ1bmNpb24gcHJ1bmUudHJlZSgpDQoNCmBgYHtyfQ0KcHJ1bmUuYm9zdG9uID1wcnVuZS50cmVlKHRyZWUuYm9zdG9uICxiZXN0ID01KQ0KcGxvdChwcnVuZS5ib3N0b24gKQ0KdGV4dChwcnVuZS5ib3N0b24gLHByZXR0eSA9MCkNCmBgYA0KDQpEZSBhY3VlcmRvIGNvbiBsb3MgcmVzdWx0YWRvcyBkZSBsYSB2YWxpZGFjaW9uIGNydXphZGEsIHVzYW1vcyBlbCBhcmJvbCBubyBhZmluYWRvIHBhcmEgaGFjZXIgcHJlZGljY2lvbmVzIA0KZW4gZWwgY29uanVudG8gZGUgcHJ1ZWJhcw0KDQpgYGB7cn0NCg0KeWhhdD1wcmVkaWN0ICh0cmVlLmJvc3RvbiAsbmV3ZGF0YSA9Qm9zdG9uIFstdHJhaW4gLF0pDQpib3N0b24udGVzdD1Cb3N0b24gWy10cmFpbiAsIm1lZHYiXQ0KcGxvdCh5aGF0ICxib3N0b24udGVzdCkNCmFibGluZSAoMCwxKQ0KbWVhbigoeWhhdCAtYm9zdG9uLnRlc3QpXjIpDQpgYGANCg0KTGEgcmFpeiBjdWFkcmFkYSBkZWwgTVNFIGVzIHBvciBsbyB0YW50byBhbHJlZGVkb3IgZGUgNS4wMDUsIGluZGljYW5kbyBxdWUgZXN0ZSBtb2RlbG8gY29uZHVjZSBhIHByZWRpY2Npb25lcw0KZGUgcHJ1ZWJhIHF1ZSBlc3RhbiBkZW50cm8gZGUgYXByb3hpbWFkYW1lbnRlICQgNSwgMDA1IGRlbCB2YWxvciBtZWRpbyByZWFsIGRlIGxhIHZpdmllbmRhIHBhcmEgZWwgc3VidXJiaW8uDQoNCg0KIyMgOC4zLjMgU2Fjb3MgeSBCb3NxdWVzIEFsZWF0b3Jpb3MgDQoNCkFxdWkgYXBsaWNhbW9zIGVtcGFxdWV0YW1pZW50byB5IGJvc3F1ZXMgYWxlYXRvcmlvcyBhIGxvcyBkYXRvcyBkZSBCb3N0b24sIHVzYW5kbyBlbCBwYXF1ZXRlIHJhbmRvbUZvcmVzdCBlbiBSLg0KTG9zIHJlc3VsdGFkb3MgZXhhY3RvcyBvYnRlbmlkb3MgZW4gZXN0YSBzZWNjaW9uIHB1ZWRlbiBkZXBlbmRlciBkZSBsYSB2ZXJzaW9uIGRlIFIgeSBsYSB2ZXJzaW9uIGRlbCBwYXF1ZXRlDQpyYW5kb21Gb3Jlc3QgaW5zdGFsYWRhIGVuIGxhIGNvbXB1dGFkb3JhLCBSZWNvcmRlbW9zIHF1ZSBlbCBlbWJvbHNhZG8gZXMgc2ltcGxlbWVudGUgdW4gY2FzbyBlc3BlY2lhbCBkZQ0KdW4gYm9zcXVlIGFsIGF6YXIgY29uIG0gPSBwLiBQb3IgbG8gdGFudG8sIGxhIGZ1bmNpb24gcmFuZG9tRm9yZXN0KCkgc2UgcHVlZGUgdXRpbGl6YXIgcGFyYSByZWFsaXphciB0YW50byBib3NxdWVzIGFsZWF0b3Jpb3MgY29tbyBlbXBhcXVldGFtaWVudG8uIFJlYWxpemFtb3MgZW1ib2xzYWRvIGNvbW8gc2lndWUuDQoNCg0KYGBge3J9DQpsaWJyYXJ5IChyYW5kb21Gb3Jlc3QpDQpzZXQuc2VlZCAoMSkNCmJhZy5ib3N0b24gPXJhbmRvbUZvcmVzdChtZWR24oi8LixkYXRhPUJvc3RvbiAsc3Vic2V0ID10cmFpbiAsDQptdHJ5PTEzLCBpbXBvcnRhbmNlID1UUlVFKQ0KYmFnLmJvc3Rvbg0KYGBgDQoNCkVsIGFyZ3VtZW50byBtdHJ5ID0gMTMgaW5kaWNhIHF1ZSBzZSBkZWJlbiBjb25zaWRlcmFyIGxvcyAxMyBwcmVkaWN0b3JlcyBwYXJhIGNhZGEgZGl2aXNpb24gZGVsIGFyYm9sOw0KZW4gb3RyYXMgcGFsYWJyYXMsIHNlIGRlYmUgcmVhbGl6YXIgZWwgZW1wYXF1ZXRhbWllbnRvLiDCv1F1ZSB0YW4gYmllbiBzZSBkZXNlbXBlw7FhIGVzdGUgbW9kZWxvIGVtYm9sc2FkbyANCmVuIGVsIGNvbmp1bnRvIGRlIHBydWViYT8NCg0KYGBge3J9DQp5aGF0LmJhZyA9IHByZWRpY3QgKGJhZy5ib3N0b24gLG5ld2RhdGEgPUJvc3RvbiBbLXRyYWluICxdKQ0KcGxvdCh5aGF0LmJhZyAsIGJvc3Rvbi50ZXN0KQ0KYWJsaW5lICgwLDEpDQptZWFuKCggeWhhdC5iYWcgLWJvc3Rvbi50ZXN0KV4yKQ0KYGBgDQoNCkVsIE1TRSBkZWwgY29uanVudG8gZGUgcHJ1ZWJhcyBhc29jaWFkbyBjb24gZWwgYXJib2wgZGUgcmVncmVzaW9uIGVtcGFxdWV0YWRvIGVzIDEzLjE2LCBjYXNpIGxhIG1pdGFkIHF1ZSBzZQ0Kb2J0aWVuZSB1dGlsaXphbmRvIHVuIGFyYm9sIHVuaWNvIG9wdGltaXphZG8uIFBvZHJpYW1vcyBjYW1iaWFyIGVsIG51bWVybyBkZSBhcmJvbGVzIGN1bHRpdmFkb3MgcG9yIA0KcmFuZG9tRm9yZXN0KCkgdXNhbmRvIGVsIGFyZ3VtZW50byBudHJlZS4NCg0KYGBge3J9DQpiYWcuYm9zdG9uID1yYW5kb21Gb3Jlc3QobWVkduKIvC4sZGF0YT1Cb3N0b24gLHN1YnNldCA9dHJhaW4gLA0KbXRyeT0xMywgbnRyZWUgPTI1KQ0KeWhhdC5iYWcgPSBwcmVkaWN0IChiYWcuYm9zdG9uICxuZXdkYXRhID1Cb3N0b24gWy10cmFpbiAsXSkNCm1lYW4oKCB5aGF0LmJhZyAtYm9zdG9uLnRlc3QpXjIpDQpgYGANCg0KRWwgY3JlY2ltaWVudG8gZGUgdW4gYm9zcXVlIGFsZWF0b3JpbyBwcm9jZWRlIGV4YWN0YW1lbnRlIGRlIGxhIG1pc21hIG1hbmVyYSwgZXhjZXB0byBxdWUgdXNhbW9zIHVuIHZhbG9yIG1hcw0KcGVxdWXDsW8gZGVsIGFyZ3VtZW50byBtdHJ5LiBEZSBmb3JtYSBwcmVkZXRlcm1pbmFkYSwgcmFuZG9tRm9yZXN0KCkgdeKImnMgcCAvIDMgdmFyaWFibGVzIGN1YW5kbyBzZSBjcmVhIHVuIGJvc3F1ZQ0KYWxlYXRvcmlvIGRlIGFyYm9sZXMgZGUgcmVncmVzaW9uLCB5IHAgdmFyaWFibGVzIGN1YW5kbyBzZSBjcmVhIHVuIGJvc3F1ZSBhbGVhdG9yaW8gZGUgYXJib2xlcyBkZSBjbGFzaWZpY2FjaW9uLg0KQXF1aSB1c2Ftb3MgbXRyeSA9IDYuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQgKDEpDQpyZi5ib3N0b24gPXJhbmRvbUZvcmVzdChtZWR24oi8LixkYXRhPUJvc3RvbiAsc3Vic2V0ID10cmFpbiAsDQptdHJ5PTYsIGltcG9ydGFuY2UgPVRSVUUpDQp5aGF0LnJmID0gcHJlZGljdCAocmYuYm9zdG9uICxuZXdkYXRhID1Cb3N0b24gWy10cmFpbiAsXSkNCm1lYW4oKCB5aGF0LnJmIC1ib3N0b24udGVzdCleMikNCmBgYA0KDQpFbCBjb25qdW50byBkZSBwcnVlYmEgTVNFIGVzIDExLjMxOyBlc3RvIGluZGljYSBxdWUgbG9zIGJvc3F1ZXMgYWxlYXRvcmlvcyBwcm9kdWplcm9uIHVuYSBtZWpvcmEgc29icmUgZWwNCmVtcGFxdWV0YW1pZW50byBlbiBlc3RlIGNhc28uDQpVc2FuZG8gbGEgZnVuY2lvbiBpbXBvcnRhbmNlKCksIHBvZGVtb3MgdmVyIGxhIGltcG9ydGFuY2lhIGRlIGNhZGEgdmFyaWFibGUuDQoNCmBgYHtyfQ0KaW1wb3J0YW5jZSAocmYuYm9zdG9uICkNCg0KYGBgDQoNClNlIHB1ZWRlbiBwcm9kdWNpciBncmFmaWNvcyBkZSBlc3RhcyBtZWRpZGFzIGRlIGltcG9ydGFuY2lhIHV0aWxpemFuZG8gbGEgZnVuY2lvbiB2YXJJbXBQbG90KCkuDQoNCmBgYHtyfQ0KdmFySW1wUGxvdCAocmYuYm9zdG9uICkNCmBgYA0KDQpMb3MgcmVzdWx0YWRvcyBpbmRpY2FuIHF1ZSBlbiB0b2RvcyBsb3MgYXJib2xlcyBjb25zaWRlcmFkb3MgZW4gZWwgYm9zcXVlIGFsZWF0b3JpbywgZWwgbml2ZWwgZGUgcmlxdWV6YSBkZSBsYSBjb211bmlkYWQgKGxzdGF0KSB5IGVsIHRhbWHDsW8gZGUgbGEgY2FzYSAocm0pIHNvbiwgY29uIG11Y2hvLCBsYXMgZG9zIHZhcmlhYmxlcyBtYXMgaW1wb3J0YW50ZXMuDQoNCg0KDQojIyA4LjMuNCBCb29zdGluZw0KDQpBcXXDrSB1c2Ftb3MgZWwgcGFxdWV0ZSBnYm0sIHkgZGVudHJvIGRlIGVsIGxhIGZ1bmNpb24gZ2JtKCksIHBhcmEgYWp1c3RhciBsb3MgYXJib2xlcyBkZSByZWdyZXNpb24gYXVtZW50YWRvcyBhbCBjb25qdW50byBkZSBkYXRvcyBkZSBCb3N0b24uIEVqZWN1dGFtb3MgZ2JtKCkgY29uIGxhIG9wY2lvbiBkaXN0cmlidXRpb24gPSAiZ2F1c3NpYW4iIHlhIHF1ZSBlc3RlIGVzIHVuIHByb2JsZW1hIGRlIHJlZ3Jlc2lvbjsgU2kgc2UgdHJhdGFyYSBkZSB1biBwcm9ibGVtYSBkZSBjbGFzaWZpY2FjaW9uIGJpbmFyaWEsIHV0aWxpemFyw61hbW9zIGRpc3RyaWJ1dGlvbiA9ICJiZXJub3VsbGkiLg0KDQpgYGB7cn0NCmxpYnJhcnkgKGdibSkNCnNldC5zZWVkICgxKQ0KYm9vc3QuYm9zdG9uID1nYm0obWVkduKIvC4sZGF0YT1Cb3N0b24gW3RyYWluICxdLCBkaXN0cmlidXRpb249ICJnYXVzc2lhbiIsbi50cmVlcyA9NTAwMCAsIGludGVyYWN0aW9uLmRlcHRoID00KQ0KYGBgDQoNCkxhIGZ1bmNpb24gc3VtbWFyeSgpIHByb2R1Y2UgdW4gZ3JhZmljbyBkZSBpbmZsdWVuY2lhIHJlbGF0aXZhIHkgdGFtYmllbiBnZW5lcmEgbGFzIGVzdGFkaXN0aWNhcyBkZSBpbmZsdWVuY2lhIHJlbGF0aXZhLg0KDQpgYGB7cn0NCnN1bW1hcnkgKGJvb3N0LmJvc3RvbiApDQpgYGANCg0KVmVtb3MgcXVlIGxzdGF0IHkgcm0gc29uIGxhcyB2YXJpYWJsZXMgbcOhcyBpbXBvcnRhbnRlcy4gUG9kZW1vcyBUYW1iaWVuIHNlIHByb2R1Y2lyIHBhcmNlbGFzIGRlIGRlcGVuZGVuY2lhDQpwYXJjaWFsIHBhcmEgZXN0YXMgZG9zIHZhcmlhYmxlcy4gRW4gZXN0ZSBjYXNvLCBjb21vIGNhYnJpYSBlc3BlcmFyLCBsYSBtZWRpYW5hLiBMb3MgcHJlY2lvcyBkZSBsYXMgY2FzYXMNCmF1bWVudGFuIGNvbiBybSB5IGRpc21pbnV5ZW4gY29uIGxzdGF0Lg0KDQpgYGB7cn0NCnBhcihtZnJvdyA9YygxLDIpKQ0KcGxvdChib29zdC5ib3N0b24gLGk9InJtIikNCnBsb3QoYm9vc3QuYm9zdG9uICxpPSJsc3RhdCIpDQpgYGANCg0KQWhvcmEgdXNhbW9zIGVsIG1vZGVsbyByZWZvcnphZG8gcGFyYSBwcmVkZWNpciBtZWR2IGVuIGVsIGNvbmp1bnRvIGRlIHBydWViYToNCg0KYGBge3J9DQp5aGF0LmJvb3N0PXByZWRpY3QgKGJvb3N0LmJvc3RvbiAsbmV3ZGF0YSA9Qm9zdG9uIFstdHJhaW4gLF0sDQpuLnRyZWVzID01MDAwKQ0KbWVhbigoIHloYXQuYm9vc3QgLWJvc3Rvbi50ZXN0KV4yKQ0KYGBgDQoNCkxhIHBydWViYSBNU0Ugb2J0ZW5pZGEgZXMgMTEuODsgU2ltaWxhciBhbCBNU0UgZGUgcHJ1ZWJhIHBhcmEgYm9zcXVlcyBhbGVhdG9yaW9zIHkgc3VwZXJpb3IgYWwgZGUgZW5zYWNhZG8uIFNpIHF1ZXJlbW9zLCBwb2RlbW9zIHJlYWxpemFyIHVuIGF1bWVudG8gY29uIHVuIHZhbG9yIGRpZmVyZW50ZSBkZWwgcGFyYW1ldHJvIGRlIGNvbnRyYWNjaW9uIM67IGVuICg4LjEwKS4gRWwgdmFsb3IgcHJlZGV0ZXJtaW5hZG8gZXMgMC4wMDEsIHBlcm8gZXN0byBzZSBtb2RpZmljYSBmYWNpbG1lbnRlLiBBcXXDrSB0b21hbW9zIM67ID0gMC4yLg0KDQpgYGB7cn0NCmJvb3N0LmJvc3RvbiA9Z2JtKG1lZHbiiLwuLGRhdGE9Qm9zdG9uIFt0cmFpbiAsXSwgZGlzdHJpYnV0aW9uPSJnYXVzc2lhbiIsbi50cmVlcyA9NTAwMCAsIGludGVyYWN0aW9uLmRlcHRoID00LCBzaHJpbmthZ2UgPTAuMiwNCnZlcmJvc2UgPUYpDQp5aGF0LmJvb3N0PXByZWRpY3QgKGJvb3N0LmJvc3RvbiAsbmV3ZGF0YSA9Qm9zdG9uIFstdHJhaW4gLF0sDQpuLnRyZWVzID01MDAwKQ0KbWVhbigoIHloYXQuYm9vc3QgLWJvc3Rvbi50ZXN0KV4yKQ0KYGBgDQoNCg0KDQo=