Bagging, Random Forest, and Boosting
library(ISLR)
library(tidyverse)
library(randomForest)
library(gbm)
library(MASS)
library(rpart)

Bagging

Motivation:

Decision trees discussed previously have high variance. Imagine we split the data into two parts at random and fit a decision tree to both halves. The results could be extremely different. What we want to do is find a procedure with low variance that will give similar results when applied over and over again.

Reminder about Trees

data = read.csv("50_Startups.csv")
head(data)
set.seed(1)
split = sample (1:nrow(Boston), nrow(Boston)/2)
regressor2 = rpart(formula = Profit ~ R.D.Spend, data= data, subset = split, control = rpart.control(minsplit = 2))
xgrid = seq(min(data$R.D.Spend),max(data$R.D.Spend),1)
ggplot() +
  geom_point(aes(x = data$R.D.Spend, y = data$Profit), color = 'red')+
  geom_line(aes(x = xgrid, y = predict(regressor2, newdata = data.frame(R.D.Spend = xgrid))), color = 'blue')

set.seed(1)
split = sample (1:nrow(Boston), nrow(Boston)/2)
regressor2 = rpart(formula = Profit ~ R.D.Spend, data= data, subset = -split, control = rpart.control(minsplit = 2))
xgrid = seq(min(data$R.D.Spend),max(data$R.D.Spend),1)
ggplot() +
  geom_point(aes(x = data$R.D.Spend, y = data$Profit), color = 'red')+
  geom_line(aes(x = xgrid, y = predict(regressor2, newdata = data.frame(R.D.Spend = xgrid))), color = 'blue')

Bagging also known as bootstrap aggregation will reduce the variance of a statistical learning method.

Ignoring the math for a moment, consider the the following intuition; Averaging a set of observations reduces the variance, so we could reduce the variance and increase the accuracy of our decision tree by taking many training sets from the population, building distinct trees, and averaging the results to obtain a single low variance model.

Unfortunately, we don’t expect to have access to multiple training sets. We can, however, bootstrap and take repeated samples from the single training set. This is called Bagging and works for lots of different statistical learning methods, but is especially useful for decision trees. Our procedure to implement this method begins by constructing B regression trees using B bootstrapped training sets and then averaging the results.

This gives us the following equation

Note These trees should be grown deep and not be pruned, so an individual tree should have high variance and low bias.

Out of Bag Error Estimation (OOB)

We don’t need to cross validate OR use the validation set approach! Instead, we can use the Out of Bag Error Estimate

Recall that the key to bagging is that trees are repeatedly fit to bootstrapped subsets of the observations and that, as Dr. White mentioned in his bootstrap lecture, each bagged tree will only use about 2/3rds of the observations. The remaining one third of the observations for a given bagged tree are referred to as the out of bag observations.

We can predict the response for the ith observation using each of the trees in which that observation was OOB. This will yield around B/3 predictions for the ith observation. Then, we can find a single prediction by averaging these predictions or take a majority vote. This ultimately leads to an overall OOB MSE which is a valid estimate of the test error for the bagged model.

Application of Bagging

Now to a good example on the Boston dataset

Training our Model

set.seed(12)
train = sample (1:nrow(Boston), nrow(Boston)/2)
bag.boston = randomForest(medv~., data = Boston, subset = train, mtry = 13, importance = TRUE)
# mtry -    Number of variables randomly sampled as candidates at each split. Note that the default values are different for classification (sqrt(p) where p is number of variables in x) and regression (p/3)
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: 14.09328
                    % Var explained: 84.14

Testing our Model

The OOB estimate for MSE is 14.09

yhat.bag = predict (bag.boston , newdata=Boston[-train ,])
ggplot()+
  geom_point(aes(x = yhat.bag, y = Boston[-train, "medv"]))+
  geom_abline(color = "red")+
  xlab("Predicted Values")+
  ylab("True Values")

mean((yhat.bag -Boston[-train, "medv"])^2)
[1] 14.08248

The test set MSE associated with the bagged regression tree is 14.08, this is an improvement over the textbook example of a single optimally pruned tree.

set.seed(1)
bag.boston2 = randomForest(medv~., data = Boston, subset = train, mtry = 13, importance = TRUE, ntree = 1000)
yhat.bag2 = predict (bag.boston2 , newdata=Boston[-train ,])
mean((yhat.bag2 -Boston[-train, "medv"])^2)
[1] 14.06875

Interpretation

It’s not great. When we bag a large number of trees, it is no longer possible to represent the model with a single tree and it is no longer clear which variables are most important to the precedure.

Bagging improves the accuracy at the expense of interpretability

We do have some tools to identify importance though, we can use the RSS for regression trees or Gini index (Probability that a feature is classified incorrectly when selected randomly) for classification.

For regression - We can look at the total amount the RSS (Sum of Squared Residuals if you, like me, forgot) has decreased due to splits over a given predictor.

For classification - We can do the same with the Gini index measure.

Random Forest

This is an improvement over bagged trees by decorrelating the trees. We still build multiple decision trees on bootstrapped training samples, but instead of using all predictors, we use only a random sample from the full set of predictors. Typically we choose the number of predictors to be approximately equal to the square root of the total number of predictors.

Weird but works! Why wouldn’t we want the trees to be built on all available data? How does this decorrelate the trees?

From the textbook “Suppose that there is one very strong predictor in the data set, along with a number of other moderately strong predictors. Then in the collection of bagged trees, most or all of the trees will use this strong predictor in the top split. Consequently, all of the bagged trees will look quite similar to each other. Hence the predictions from the bagged trees will be highly correlated.”

We know that averaging over highly correlated values doesn’t reduce the variance as much as doing so with uncorrelated values. This means that bagging doesn’t reduce the variance as much as we would like.

Random forest deals with this by forcing each tree to only consider a subset of the predictors. In the textbook example, this leads to some trees not even considering the strong predictor.

Main difference between random forest and bagging is the choise of predictor subset size

This leads to a reduction in both test error and OOB error over bagging. Basically use randomforest, not bagging.

Note Sometimes we may want to use less than the suggested number of predictors when variables are highly correlated. The randomForest library in R defaults to p/3 for regression trees.

Application of Randomforest

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[-train, "medv"])^2)
[1] 12.94768

Wow! Look at how much better that is. Let’s see if we can understand what predictors are important.

importance(rf.boston)
          %IncMSE IncNodePurity
crim    14.162597    1495.43316
zn       2.354257      57.80273
indus    7.026734     721.89977
chas     4.271671      84.36145
nox     13.310095     907.93589
rm      34.210395    7223.81670
age      7.855808     317.85702
dis     13.120051    1058.37980
rad      4.263160     159.48436
tax      8.696159     641.37864
ptratio 14.993781    1405.86289
black    5.980337     264.13753
lstat   31.040048    7704.31207

We see the %increase in MSE which is a measure of the decrease in accuracy in prediction on the OOB samples when a given variable is excluded.

We also see the total decrease in node impurity that results from splits over that variable (averaged over all trees.) For regression that is measured over RSS and for classification by deviance.

We can plot this!

varImpPlot(rf.boston)

Boosting

Boosting is another general approach we can use to improve statistical learning methods. Boosting works like bagging does with multiple trees, but doesn’t use boot strapping and instead each tree is fit on a modified version of the original data. The main difference is that in boosting trees are grown sequentially based on information learned in previous trees

What is interesting about this?

• Unlike fitting a single large decision tree to the data, which amounts to fitting the data hard and potentially overfitting, the boosting approach instead learns slowly.

• Given the current model, we fit a decision tree to the residuals from the model. We then add this new decision tree into the fitted function in order to update the residuals.

• Each of these trees can be rather small, with just a few terminal nodes, determined by the parameter d in the algorithm.

• By fitting small trees to the residuals, we slowly improve f in areas where it does not perform well. The shrinkage parameter λ slows the process down even further, allowing more and different shaped trees to attack the residuals.

Tuning Parameters for Boosting

  1. The number of trees B. Unlike bagging and random forests, boosting can overfit if B is too large, although this overfitting tends to occur slowly if at all. We use cross-validation to select B.

  2. The shrinkage parameter λ, a small positive number. This controls the rate at which boosting learns. Typical values are 0.01 or 0.001, and the right choice can depend on the problem. Very small λ can require using a very large value of B in order to achieve good performance.

  3. The number of splits d in each tree, which controls the complexity of the boosted ensemble. Often d = 1 works well, in which case each tree is a stump, consisting of a single split and resulting in an additive model. More generally d is the interaction depth, and controls the interaction order of the boosted model, since d splits can involve at most d variables.

Application

set.seed(12)
boost.boston=gbm(medv~.,data=Boston[train ,], distribution="gaussian",n.trees=5000, interaction.depth=4)
summary(boost.boston)

Clearly rm and lstat are the most influential, lets take a look at them individually

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

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

yhat.boost=predict (boost.boston ,newdata =Boston[-train ,])#, n.trees=??)
Using 5000 trees...
best.iter <- gbm.perf(boost.boston, method = "OOB", plot = FALSE)
OOB generally underestimates the optimal number of iterations although predictive performance is reasonably competitive. Using cv_folds>1 when calling gbm usually results in improved predictive performance.
print(best.iter)
[1] 102
attr(,"smoother")
Call:
loess(formula = object$oobag.improve ~ x, enp.target = min(max(4, 
    length(x)/10), 50))

Number of Observations: 5000 
Equivalent Number of Parameters: 39.99 
Residual Standard Error: 0.2303 
set.seed(1)
yhat.boost=predict (boost.boston ,newdata =Boston[-train ,], n.trees=103)
mean((yhat.boost-Boston[-train, "medv"])^2)
[1] 15.88817

Not a great MSE, but still better than bagging. Let’s try to tune it with lambda

set.seed(1)
boost.boston2=gbm(medv~.,data=Boston[train ,], distribution="gaussian",n.trees=5000, interaction.depth=5, shrinkage =.1)
best.iter <- gbm.perf(boost.boston2, method = "OOB",plot = FALSE)
OOB generally underestimates the optimal number of iterations although predictive performance is reasonably competitive. Using cv_folds>1 when calling gbm usually results in improved predictive performance.
yhat.boost2=predict (boost.boston2 ,newdata =Boston[-train ,], n.trees=best.iter)
mean((yhat.boost2-Boston[-train, "medv"])^2)
[1] 12.95806

An improvement!

##Classification

data = read.csv("Social_Network_Ads.csv")
head(data)
set.seed(1234)
data$Age=scale(data$Age)
data$EstimatedSalary=scale(data$EstimatedSalary)
train = sample (1:nrow(data), nrow(data)/2)
classifier = randomForest(factor(Purchased)~EstimatedSalary + Age, data = data, subset= train, ntree = 10)
yhat = predict(classifier, newdata=data[-train ,])
confusion_matrix = table(data[-train, 5],yhat)
confusion_matrix
   yhat
      0   1
  0 109  14
  1  11  66
test = data[-train, ]
X1 = seq(min(test$EstimatedSalary) - 1, max(test$EstimatedSalary) + 1, by = 0.1)
X2 = seq(min(test$Age) - 1, max(test$Age) + 1, by = 0.1)
grid = expand.grid(X1,X2)
colnames(grid) = c("EstimatedSalary","Age")
prob = predict(classifier, type = "response", newdata = grid)
ggplot()+
  geom_point(data = grid, aes(x=EstimatedSalary,y=Age, color = prob), size = .1)+
  geom_point(aes(x=EstimatedSalary, y = Age, color= factor(Purchased)), data = test)+
  geom_contour(aes(x=grid$EstimatedSalary, y = grid$Age, z=as.numeric(prob)))

##HW

  1. Repeat the previous classification example using bagging and boosting methods and comment on any differences you observe.
LS0tDQp0aXRsZTogIkJhZ2dpbmcsIFJhbmRvbSBGb3Jlc3QsIGFuZCBCb29zdGluZyINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCmBgYHtyfQ0KbGlicmFyeShJU0xSKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkNCmxpYnJhcnkoZ2JtKQ0KbGlicmFyeShNQVNTKQ0KbGlicmFyeShycGFydCkNCmBgYA0KIyMgQmFnZ2luZw0KDQojIyMgTW90aXZhdGlvbjoNCkRlY2lzaW9uIHRyZWVzIGRpc2N1c3NlZCBwcmV2aW91c2x5IGhhdmUgKmhpZ2ggdmFyaWFuY2UqLiBJbWFnaW5lIHdlIHNwbGl0IHRoZSBkYXRhIGludG8gdHdvIHBhcnRzIGF0IHJhbmRvbSBhbmQgZml0IGEgZGVjaXNpb24gdHJlZSB0byBib3RoIGhhbHZlcy4gVGhlIHJlc3VsdHMgY291bGQgYmUgZXh0cmVtZWx5IGRpZmZlcmVudC4gV2hhdCB3ZSB3YW50IHRvIGRvIGlzIGZpbmQgYSBwcm9jZWR1cmUgd2l0aCAqbG93IHZhcmlhbmNlKiB0aGF0IHdpbGwgZ2l2ZSBzaW1pbGFyIHJlc3VsdHMgd2hlbiBhcHBsaWVkIG92ZXIgYW5kIG92ZXIgYWdhaW4uDQoNCioqUmVtaW5kZXIgYWJvdXQgVHJlZXMqKg0KDQpgYGB7cn0NCmRhdGEgPSByZWFkLmNzdigiNTBfU3RhcnR1cHMuY3N2IikNCmhlYWQoZGF0YSkNCmBgYA0KDQpgYGB7cn0NCnNldC5zZWVkKDEpDQpzcGxpdCA9IHNhbXBsZSAoMTpucm93KEJvc3RvbiksIG5yb3coQm9zdG9uKS8yKQ0KcmVncmVzc29yMiA9IHJwYXJ0KGZvcm11bGEgPSBQcm9maXQgfiBSLkQuU3BlbmQsIGRhdGE9IGRhdGEsIHN1YnNldCA9IHNwbGl0LCBjb250cm9sID0gcnBhcnQuY29udHJvbChtaW5zcGxpdCA9IDIpKQ0KeGdyaWQgPSBzZXEobWluKGRhdGEkUi5ELlNwZW5kKSxtYXgoZGF0YSRSLkQuU3BlbmQpLDEpDQpnZ3Bsb3QoKSArDQogIGdlb21fcG9pbnQoYWVzKHggPSBkYXRhJFIuRC5TcGVuZCwgeSA9IGRhdGEkUHJvZml0KSwgY29sb3IgPSAncmVkJykrDQogIGdlb21fbGluZShhZXMoeCA9IHhncmlkLCB5ID0gcHJlZGljdChyZWdyZXNzb3IyLCBuZXdkYXRhID0gZGF0YS5mcmFtZShSLkQuU3BlbmQgPSB4Z3JpZCkpKSwgY29sb3IgPSAnYmx1ZScpDQpgYGANCg0KYGBge3J9DQpzZXQuc2VlZCgxKQ0Kc3BsaXQgPSBzYW1wbGUgKDE6bnJvdyhCb3N0b24pLCBucm93KEJvc3RvbikvMikNCnJlZ3Jlc3NvcjIgPSBycGFydChmb3JtdWxhID0gUHJvZml0IH4gUi5ELlNwZW5kLCBkYXRhPSBkYXRhLCBzdWJzZXQgPSAtc3BsaXQsIGNvbnRyb2wgPSBycGFydC5jb250cm9sKG1pbnNwbGl0ID0gMikpDQp4Z3JpZCA9IHNlcShtaW4oZGF0YSRSLkQuU3BlbmQpLG1heChkYXRhJFIuRC5TcGVuZCksMSkNCmdncGxvdCgpICsNCiAgZ2VvbV9wb2ludChhZXMoeCA9IGRhdGEkUi5ELlNwZW5kLCB5ID0gZGF0YSRQcm9maXQpLCBjb2xvciA9ICdyZWQnKSsNCiAgZ2VvbV9saW5lKGFlcyh4ID0geGdyaWQsIHkgPSBwcmVkaWN0KHJlZ3Jlc3NvcjIsIG5ld2RhdGEgPSBkYXRhLmZyYW1lKFIuRC5TcGVuZCA9IHhncmlkKSkpLCBjb2xvciA9ICdibHVlJykNCmBgYA0KDQoqKkJhZ2dpbmcqKiBhbHNvIGtub3duIGFzICoqYm9vdHN0cmFwIGFnZ3JlZ2F0aW9uKiogd2lsbCByZWR1Y2UgdGhlIHZhcmlhbmNlIG9mIGEgc3RhdGlzdGljYWwgbGVhcm5pbmcgbWV0aG9kLg0KDQpJZ25vcmluZyB0aGUgbWF0aCBmb3IgYSBtb21lbnQsIGNvbnNpZGVyIHRoZSB0aGUgZm9sbG93aW5nIGludHVpdGlvbjsgQXZlcmFnaW5nIGEgc2V0IG9mIG9ic2VydmF0aW9ucyByZWR1Y2VzIHRoZSB2YXJpYW5jZSwgc28gd2UgY291bGQgcmVkdWNlIHRoZSB2YXJpYW5jZSBhbmQgaW5jcmVhc2UgdGhlIGFjY3VyYWN5IG9mIG91ciBkZWNpc2lvbiB0cmVlIGJ5IHRha2luZyBtYW55IHRyYWluaW5nIHNldHMgZnJvbSB0aGUgcG9wdWxhdGlvbiwgYnVpbGRpbmcgZGlzdGluY3QgdHJlZXMsIGFuZCBhdmVyYWdpbmcgdGhlIHJlc3VsdHMgdG8gb2J0YWluIGEgc2luZ2xlIGxvdyB2YXJpYW5jZSBtb2RlbC4NCg0KVW5mb3J0dW5hdGVseSwgd2UgZG9uJ3QgZXhwZWN0IHRvIGhhdmUgYWNjZXNzIHRvIG11bHRpcGxlIHRyYWluaW5nIHNldHMuIFdlIGNhbiwgaG93ZXZlciwgYm9vdHN0cmFwIGFuZCB0YWtlIHJlcGVhdGVkIHNhbXBsZXMgZnJvbSB0aGUgc2luZ2xlIHRyYWluaW5nIHNldC4gVGhpcyBpcyBjYWxsZWQgKipCYWdnaW5nKiogYW5kIHdvcmtzIGZvciBsb3RzIG9mIGRpZmZlcmVudCBzdGF0aXN0aWNhbCBsZWFybmluZyBtZXRob2RzLCBidXQgaXMgZXNwZWNpYWxseSB1c2VmdWwgZm9yIGRlY2lzaW9uIHRyZWVzLiBPdXIgcHJvY2VkdXJlIHRvIGltcGxlbWVudCB0aGlzIG1ldGhvZCBiZWdpbnMgYnkgY29uc3RydWN0aW5nICpCKiByZWdyZXNzaW9uIHRyZWVzIHVzaW5nICpCKiBib290c3RyYXBwZWQgdHJhaW5pbmcgc2V0cyBhbmQgdGhlbiBhdmVyYWdpbmcgdGhlIHJlc3VsdHMuDQoNClRoaXMgZ2l2ZXMgdXMgdGhlIGZvbGxvd2luZyBlcXVhdGlvbg0KDQohW10oYmFnLnBuZykNCg0KKipOb3RlKiogVGhlc2UgdHJlZXMgc2hvdWxkIGJlIGdyb3duIGRlZXAgYW5kIG5vdCBiZSBwcnVuZWQsIHNvIGFuIGluZGl2aWR1YWwgdHJlZSBzaG91bGQgaGF2ZSBoaWdoIHZhcmlhbmNlIGFuZCBsb3cgYmlhcy4gDQoNCiMjIyBPdXQgb2YgQmFnIEVycm9yIEVzdGltYXRpb24gKE9PQikNCg0KV2UgZG9uJ3QgbmVlZCB0byBjcm9zcyB2YWxpZGF0ZSBPUiB1c2UgdGhlIHZhbGlkYXRpb24gc2V0IGFwcHJvYWNoISBJbnN0ZWFkLCB3ZSBjYW4gdXNlIHRoZSBPdXQgb2YgQmFnIEVycm9yIEVzdGltYXRlDQoNClJlY2FsbCB0aGF0IHRoZSBrZXkgdG8gYmFnZ2luZyBpcyB0aGF0IHRyZWVzIGFyZSByZXBlYXRlZGx5IGZpdCB0byBib290c3RyYXBwZWQgc3Vic2V0cyBvZiB0aGUgb2JzZXJ2YXRpb25zIGFuZCB0aGF0LCBhcyBEci4gV2hpdGUgbWVudGlvbmVkIGluIGhpcyBib290c3RyYXAgbGVjdHVyZSwgZWFjaCBiYWdnZWQgdHJlZSB3aWxsIG9ubHkgdXNlIGFib3V0IDIvM3JkcyBvZiB0aGUgb2JzZXJ2YXRpb25zLiBUaGUgcmVtYWluaW5nIG9uZSB0aGlyZCBvZiB0aGUgb2JzZXJ2YXRpb25zIGZvciBhIGdpdmVuIGJhZ2dlZCB0cmVlIGFyZSByZWZlcnJlZCB0byBhcyB0aGUgb3V0IG9mIGJhZyBvYnNlcnZhdGlvbnMuICANCg0KV2UgY2FuIHByZWRpY3QgdGhlIHJlc3BvbnNlIGZvciB0aGUgaXRoIG9ic2VydmF0aW9uIHVzaW5nIGVhY2ggb2YgdGhlIHRyZWVzIGluIHdoaWNoIHRoYXQgb2JzZXJ2YXRpb24gd2FzIE9PQi4gVGhpcyB3aWxsIHlpZWxkIGFyb3VuZCBCLzMgcHJlZGljdGlvbnMgZm9yIHRoZSBpdGggb2JzZXJ2YXRpb24uIFRoZW4sIHdlIGNhbiBmaW5kIGEgc2luZ2xlIHByZWRpY3Rpb24gYnkgYXZlcmFnaW5nIHRoZXNlIHByZWRpY3Rpb25zIG9yIHRha2UgYSBtYWpvcml0eSB2b3RlLiBUaGlzIHVsdGltYXRlbHkgbGVhZHMgdG8gYW4gb3ZlcmFsbCBPT0IgTVNFIHdoaWNoIGlzIGEgdmFsaWQgZXN0aW1hdGUgb2YgdGhlIHRlc3QgZXJyb3IgZm9yIHRoZSBiYWdnZWQgbW9kZWwuDQoNCg0KDQojIyMgQXBwbGljYXRpb24gb2YgQmFnZ2luZw0KDQoNCg0KTm93IHRvIGEgZ29vZCBleGFtcGxlIG9uIHRoZSBCb3N0b24gZGF0YXNldA0KDQoNCioqVHJhaW5pbmcgb3VyIE1vZGVsKioNCg0KYGBge3J9DQpzZXQuc2VlZCgxMikNCnRyYWluID0gc2FtcGxlICgxOm5yb3coQm9zdG9uKSwgbnJvdyhCb3N0b24pLzIpDQpiYWcuYm9zdG9uID0gcmFuZG9tRm9yZXN0KG1lZHZ+LiwgZGF0YSA9IEJvc3Rvbiwgc3Vic2V0ID0gdHJhaW4sIG10cnkgPSAxMywgaW1wb3J0YW5jZSA9IFRSVUUpDQojIG10cnkgLQlOdW1iZXIgb2YgdmFyaWFibGVzIHJhbmRvbWx5IHNhbXBsZWQgYXMgY2FuZGlkYXRlcyBhdCBlYWNoIHNwbGl0LiBOb3RlIHRoYXQgdGhlIGRlZmF1bHQgdmFsdWVzIGFyZSBkaWZmZXJlbnQgZm9yIGNsYXNzaWZpY2F0aW9uIChzcXJ0KHApIHdoZXJlIHAgaXMgbnVtYmVyIG9mIHZhcmlhYmxlcyBpbiB4KSBhbmQgcmVncmVzc2lvbiAocC8zKQ0KYmFnLmJvc3Rvbg0KYGBgDQoqKlRlc3Rpbmcgb3VyIE1vZGVsKioNCg0KVGhlIE9PQiBlc3RpbWF0ZSBmb3IgTVNFIGlzIDE0LjA5DQoNCg0KYGBge3J9DQp5aGF0LmJhZyA9IHByZWRpY3QgKGJhZy5ib3N0b24gLCBuZXdkYXRhPUJvc3RvblstdHJhaW4gLF0pDQpnZ3Bsb3QoKSsNCiAgZ2VvbV9wb2ludChhZXMoeCA9IHloYXQuYmFnLCB5ID0gQm9zdG9uWy10cmFpbiwgIm1lZHYiXSkpKw0KICBnZW9tX2FibGluZShjb2xvciA9ICJyZWQiKSsNCiAgeGxhYigiUHJlZGljdGVkIFZhbHVlcyIpKw0KICB5bGFiKCJUcnVlIFZhbHVlcyIpDQpgYGANCg0KDQpgYGB7cn0NCm1lYW4oKHloYXQuYmFnIC1Cb3N0b25bLXRyYWluLCAibWVkdiJdKV4yKQ0KYGBgDQpUaGUgdGVzdCBzZXQgTVNFIGFzc29jaWF0ZWQgd2l0aCB0aGUgYmFnZ2VkIHJlZ3Jlc3Npb24gdHJlZSBpcyAxNC4wOCwgdGhpcyBpcyBhbiBpbXByb3ZlbWVudCBvdmVyIHRoZSB0ZXh0Ym9vayBleGFtcGxlIG9mIGEgc2luZ2xlIG9wdGltYWxseSBwcnVuZWQgdHJlZS4NCg0KYGBge3J9DQpzZXQuc2VlZCgxKQ0KYmFnLmJvc3RvbjIgPSByYW5kb21Gb3Jlc3QobWVkdn4uLCBkYXRhID0gQm9zdG9uLCBzdWJzZXQgPSB0cmFpbiwgbXRyeSA9IDEzLCBpbXBvcnRhbmNlID0gVFJVRSwgbnRyZWUgPSAxMDAwKQ0KeWhhdC5iYWcyID0gcHJlZGljdCAoYmFnLmJvc3RvbjIgLCBuZXdkYXRhPUJvc3RvblstdHJhaW4gLF0pDQptZWFuKCh5aGF0LmJhZzIgLUJvc3RvblstdHJhaW4sICJtZWR2Il0pXjIpDQpgYGANCg0KIyMjIEludGVycHJldGF0aW9uDQoNCioqSXQncyBub3QgZ3JlYXQuKiogV2hlbiB3ZSBiYWcgYSBsYXJnZSBudW1iZXIgb2YgdHJlZXMsIGl0IGlzIG5vIGxvbmdlciBwb3NzaWJsZSB0byByZXByZXNlbnQgdGhlIG1vZGVsIHdpdGggYSBzaW5nbGUgdHJlZSBhbmQgaXQgaXMgbm8gbG9uZ2VyIGNsZWFyIHdoaWNoIHZhcmlhYmxlcyBhcmUgbW9zdCBpbXBvcnRhbnQgdG8gdGhlIHByZWNlZHVyZS4NCg0KKipCYWdnaW5nIGltcHJvdmVzIHRoZSBhY2N1cmFjeSBhdCB0aGUgZXhwZW5zZSBvZiBpbnRlcnByZXRhYmlsaXR5KioNCg0KV2UgZG8gaGF2ZSBzb21lIHRvb2xzIHRvIGlkZW50aWZ5IGltcG9ydGFuY2UgdGhvdWdoLCB3ZSBjYW4gdXNlIHRoZSBSU1MgZm9yIHJlZ3Jlc3Npb24gdHJlZXMgb3IgR2luaSBpbmRleCAoUHJvYmFiaWxpdHkgdGhhdCBhIGZlYXR1cmUgaXMgY2xhc3NpZmllZCBpbmNvcnJlY3RseSB3aGVuIHNlbGVjdGVkIHJhbmRvbWx5KSBmb3IgY2xhc3NpZmljYXRpb24uDQoNCkZvciByZWdyZXNzaW9uIC0gV2UgY2FuIGxvb2sgYXQgdGhlIHRvdGFsIGFtb3VudCB0aGUgUlNTIChTdW0gb2YgU3F1YXJlZCBSZXNpZHVhbHMgaWYgeW91LCBsaWtlIG1lLCBmb3Jnb3QpIGhhcyBkZWNyZWFzZWQgZHVlIHRvIHNwbGl0cyBvdmVyIGEgZ2l2ZW4gcHJlZGljdG9yLiANCg0KRm9yIGNsYXNzaWZpY2F0aW9uIC0gV2UgY2FuIGRvIHRoZSBzYW1lIHdpdGggdGhlIEdpbmkgaW5kZXggbWVhc3VyZS4NCg0KIyMgUmFuZG9tIEZvcmVzdA0KDQpUaGlzIGlzIGFuIGltcHJvdmVtZW50IG92ZXIgYmFnZ2VkIHRyZWVzIGJ5IGRlY29ycmVsYXRpbmcgdGhlIHRyZWVzLiBXZSBzdGlsbCBidWlsZCBtdWx0aXBsZSBkZWNpc2lvbiB0cmVlcyBvbiBib290c3RyYXBwZWQgdHJhaW5pbmcgc2FtcGxlcywgYnV0IGluc3RlYWQgb2YgdXNpbmcgYWxsIHByZWRpY3RvcnMsIHdlIHVzZSBvbmx5IGEgcmFuZG9tIHNhbXBsZSBmcm9tIHRoZSBmdWxsIHNldCBvZiBwcmVkaWN0b3JzLiBUeXBpY2FsbHkgd2UgY2hvb3NlIHRoZSBudW1iZXIgb2YgcHJlZGljdG9ycyB0byBiZSBhcHByb3hpbWF0ZWx5IGVxdWFsIHRvIHRoZSBzcXVhcmUgcm9vdCBvZiB0aGUgdG90YWwgbnVtYmVyIG9mIHByZWRpY3RvcnMuDQoNCldlaXJkIGJ1dCB3b3JrcyEgV2h5IHdvdWxkbid0IHdlIHdhbnQgdGhlIHRyZWVzIHRvIGJlIGJ1aWx0IG9uIGFsbCBhdmFpbGFibGUgZGF0YT8gSG93IGRvZXMgdGhpcyAqZGVjb3JyZWxhdGUqIHRoZSB0cmVlcz8NCg0KRnJvbSB0aGUgdGV4dGJvb2sNCiJTdXBwb3NlIHRoYXQgdGhlcmUgaXMgb25lIHZlcnkgc3Ryb25nIHByZWRpY3RvciBpbiB0aGUgZGF0YSBzZXQsIGFsb25nIHdpdGggYSBudW1iZXIgb2Ygb3RoZXIgbW9kZXJhdGVseSBzdHJvbmcgcHJlZGljdG9ycy4gVGhlbiBpbiB0aGUgY29sbGVjdGlvbiBvZiBiYWdnZWQgdHJlZXMsIG1vc3Qgb3IgYWxsIG9mIHRoZSB0cmVlcyB3aWxsIHVzZSB0aGlzIHN0cm9uZyBwcmVkaWN0b3IgaW4gdGhlIHRvcCBzcGxpdC4gQ29uc2VxdWVudGx5LCBhbGwgb2YgdGhlIGJhZ2dlZCB0cmVlcyB3aWxsIGxvb2sgcXVpdGUgc2ltaWxhciB0byBlYWNoIG90aGVyLiBIZW5jZSB0aGUgcHJlZGljdGlvbnMgZnJvbSB0aGUgYmFnZ2VkIHRyZWVzIHdpbGwgYmUgaGlnaGx5IGNvcnJlbGF0ZWQuIg0KDQpXZSBrbm93IHRoYXQgYXZlcmFnaW5nIG92ZXIgaGlnaGx5IGNvcnJlbGF0ZWQgdmFsdWVzIGRvZXNuJ3QgcmVkdWNlIHRoZSB2YXJpYW5jZSBhcyBtdWNoIGFzIGRvaW5nIHNvIHdpdGggdW5jb3JyZWxhdGVkIHZhbHVlcy4gVGhpcyBtZWFucyB0aGF0IGJhZ2dpbmcgZG9lc24ndCByZWR1Y2UgdGhlIHZhcmlhbmNlIGFzIG11Y2ggYXMgd2Ugd291bGQgbGlrZS4NCg0KUmFuZG9tIGZvcmVzdCBkZWFscyB3aXRoIHRoaXMgYnkgZm9yY2luZyBlYWNoIHRyZWUgdG8gb25seSBjb25zaWRlciBhIHN1YnNldCBvZiB0aGUgcHJlZGljdG9ycy4gSW4gdGhlIHRleHRib29rIGV4YW1wbGUsIHRoaXMgbGVhZHMgdG8gc29tZSB0cmVlcyBub3QgZXZlbiBjb25zaWRlcmluZyB0aGUgc3Ryb25nIHByZWRpY3Rvci4NCg0KKipNYWluIGRpZmZlcmVuY2UgYmV0d2VlbiByYW5kb20gZm9yZXN0IGFuZCBiYWdnaW5nIGlzIHRoZSBjaG9pc2Ugb2YgcHJlZGljdG9yIHN1YnNldCBzaXplKioNCg0KVGhpcyBsZWFkcyB0byBhIHJlZHVjdGlvbiBpbiBib3RoIHRlc3QgZXJyb3IgYW5kIE9PQiBlcnJvciBvdmVyIGJhZ2dpbmcuIEJhc2ljYWxseSB1c2UgcmFuZG9tZm9yZXN0LCBub3QgYmFnZ2luZy4NCg0KKipOb3RlKiogU29tZXRpbWVzIHdlIG1heSB3YW50IHRvIHVzZSBsZXNzIHRoYW4gdGhlIHN1Z2dlc3RlZCBudW1iZXIgb2YgcHJlZGljdG9ycyB3aGVuIHZhcmlhYmxlcyBhcmUgaGlnaGx5IGNvcnJlbGF0ZWQuIFRoZSByYW5kb21Gb3Jlc3QgbGlicmFyeSBpbiBSIGRlZmF1bHRzIHRvIHAvMyBmb3IgcmVncmVzc2lvbiB0cmVlcy4NCg0KIyMjIEFwcGxpY2F0aW9uIG9mIFJhbmRvbWZvcmVzdA0KDQpgYGB7cn0NCnNldC5zZWVkKDEpDQpyZi5ib3N0b249IHJhbmRvbUZvcmVzdChtZWR2IH4gLixkYXRhPUJvc3Rvbiwgc3Vic2V0PXRyYWluLCBtdHJ5PTYsIGltcG9ydGFuY2UgPVRSVUUpDQp5aGF0LnJmID0gcHJlZGljdChyZi5ib3N0b24gLG5ld2RhdGE9Qm9zdG9uWy0gdHJhaW4gLF0pDQptZWFuKCh5aGF0LnJmLUJvc3RvblstdHJhaW4sICJtZWR2Il0pXjIpDQpgYGANCg0KV293ISBMb29rIGF0IGhvdyBtdWNoIGJldHRlciB0aGF0IGlzLiBMZXQncyBzZWUgaWYgd2UgY2FuIHVuZGVyc3RhbmQgd2hhdCBwcmVkaWN0b3JzIGFyZSBpbXBvcnRhbnQuDQoNCmBgYHtyfQ0KaW1wb3J0YW5jZShyZi5ib3N0b24pDQpgYGANCldlIHNlZSB0aGUgJWluY3JlYXNlIGluIE1TRSB3aGljaCBpcyBhIG1lYXN1cmUgb2YgdGhlIGRlY3JlYXNlIGluIGFjY3VyYWN5IGluIHByZWRpY3Rpb24gb24gdGhlIE9PQiBzYW1wbGVzIHdoZW4gYSBnaXZlbiB2YXJpYWJsZSBpcyBleGNsdWRlZC4gDQoNCldlIGFsc28gc2VlIHRoZSB0b3RhbCBkZWNyZWFzZSBpbiBub2RlIGltcHVyaXR5IHRoYXQgcmVzdWx0cyBmcm9tIHNwbGl0cyBvdmVyIHRoYXQgdmFyaWFibGUgKGF2ZXJhZ2VkIG92ZXIgYWxsIHRyZWVzLikgRm9yIHJlZ3Jlc3Npb24gdGhhdCBpcyBtZWFzdXJlZCBvdmVyIFJTUyBhbmQgZm9yIGNsYXNzaWZpY2F0aW9uIGJ5IGRldmlhbmNlLg0KDQpXZSBjYW4gcGxvdCB0aGlzIQ0KDQpgYGB7cn0NCnZhckltcFBsb3QocmYuYm9zdG9uKQ0KYGBgDQoNCiMjIEJvb3N0aW5nDQoNCkJvb3N0aW5nIGlzIGFub3RoZXIgZ2VuZXJhbCBhcHByb2FjaCB3ZSBjYW4gdXNlIHRvIGltcHJvdmUgc3RhdGlzdGljYWwgbGVhcm5pbmcgbWV0aG9kcy4gQm9vc3Rpbmcgd29ya3MgbGlrZSBiYWdnaW5nIGRvZXMgd2l0aCBtdWx0aXBsZSB0cmVlcywgYnV0IGRvZXNuJ3QgdXNlIGJvb3Qgc3RyYXBwaW5nIGFuZCBpbnN0ZWFkIGVhY2ggdHJlZSBpcyBmaXQgb24gYSBtb2RpZmllZCB2ZXJzaW9uIG9mIHRoZSBvcmlnaW5hbCBkYXRhLiBUaGUgbWFpbiBkaWZmZXJlbmNlIGlzIHRoYXQgaW4gYm9vc3RpbmcgKip0cmVlcyBhcmUgZ3Jvd24gc2VxdWVudGlhbGx5IGJhc2VkIG9uIGluZm9ybWF0aW9uIGxlYXJuZWQgaW4gcHJldmlvdXMgdHJlZXMqKg0KDQohW10oYm9vc3RpbmdhbGdvLnBuZykNCg0KIyMjIFdoYXQgaXMgaW50ZXJlc3RpbmcgYWJvdXQgdGhpcz8NCg0K4oCiIFVubGlrZSBmaXR0aW5nIGEgc2luZ2xlIGxhcmdlIGRlY2lzaW9uIHRyZWUgdG8gdGhlIGRhdGEsIHdoaWNoIGFtb3VudHMgdG8gZml0dGluZyB0aGUgZGF0YSBoYXJkIGFuZCBwb3RlbnRpYWxseSBvdmVyZml0dGluZywgdGhlIGJvb3N0aW5nIGFwcHJvYWNoIGluc3RlYWQgbGVhcm5zIHNsb3dseS4gDQoNCuKAoiBHaXZlbiB0aGUgY3VycmVudCBtb2RlbCwgd2UgZml0IGEgZGVjaXNpb24gdHJlZSB0byB0aGUgcmVzaWR1YWxzIGZyb20gdGhlIG1vZGVsLiBXZSB0aGVuIGFkZCB0aGlzIG5ldyBkZWNpc2lvbiB0cmVlIGludG8gdGhlIGZpdHRlZCBmdW5jdGlvbiBpbiBvcmRlciB0byB1cGRhdGUgdGhlIHJlc2lkdWFscy4gDQoNCuKAoiBFYWNoIG9mIHRoZXNlIHRyZWVzIGNhbiBiZSByYXRoZXIgc21hbGwsIHdpdGgganVzdCBhIGZldyB0ZXJtaW5hbCBub2RlcywgZGV0ZXJtaW5lZCBieSB0aGUgcGFyYW1ldGVyIGQgaW4gdGhlIGFsZ29yaXRobS4gDQoNCuKAoiBCeSBmaXR0aW5nIHNtYWxsIHRyZWVzIHRvIHRoZSByZXNpZHVhbHMsIHdlIHNsb3dseSBpbXByb3ZlIGYgaW4gYXJlYXMgd2hlcmUgaXQgZG9lcyBub3QgcGVyZm9ybSB3ZWxsLiBUaGUgc2hyaW5rYWdlIHBhcmFtZXRlciDOuyBzbG93cyB0aGUgcHJvY2VzcyBkb3duIGV2ZW4gZnVydGhlciwgYWxsb3dpbmcgbW9yZSBhbmQgZGlmZmVyZW50IHNoYXBlZCB0cmVlcyB0byBhdHRhY2sgdGhlIHJlc2lkdWFscy4NCg0KIyMjIFR1bmluZyBQYXJhbWV0ZXJzIGZvciBCb29zdGluZw0KDQoxLiBUaGUgbnVtYmVyIG9mIHRyZWVzIEIuIFVubGlrZSBiYWdnaW5nIGFuZCByYW5kb20gZm9yZXN0cywgYm9vc3RpbmcgY2FuIG92ZXJmaXQgaWYgQiBpcyB0b28gbGFyZ2UsIGFsdGhvdWdoIHRoaXMgb3ZlcmZpdHRpbmcgdGVuZHMgdG8gb2NjdXIgc2xvd2x5IGlmIGF0IGFsbC4gV2UgdXNlIGNyb3NzLXZhbGlkYXRpb24gdG8gc2VsZWN0IEIuIA0KDQoyLiBUaGUgc2hyaW5rYWdlIHBhcmFtZXRlciDOuywgYSBzbWFsbCBwb3NpdGl2ZSBudW1iZXIuIFRoaXMgY29udHJvbHMgdGhlIHJhdGUgYXQgd2hpY2ggYm9vc3RpbmcgbGVhcm5zLiBUeXBpY2FsIHZhbHVlcyBhcmUgMC4wMSBvciAwLjAwMSwgYW5kIHRoZSByaWdodCBjaG9pY2UgY2FuIGRlcGVuZCBvbiB0aGUgcHJvYmxlbS4gVmVyeSBzbWFsbCDOuyBjYW4gcmVxdWlyZSB1c2luZyBhIHZlcnkgbGFyZ2UgdmFsdWUgb2YgQiBpbiBvcmRlciB0byBhY2hpZXZlIGdvb2QgcGVyZm9ybWFuY2UuIA0KDQozLiBUaGUgbnVtYmVyIG9mIHNwbGl0cyBkIGluIGVhY2ggdHJlZSwgd2hpY2ggY29udHJvbHMgdGhlIGNvbXBsZXhpdHkgb2YgdGhlIGJvb3N0ZWQgZW5zZW1ibGUuIE9mdGVuIGQgPSAxIHdvcmtzIHdlbGwsIGluIHdoaWNoIGNhc2UgZWFjaCB0cmVlIGlzIGEgc3R1bXAsIGNvbnNpc3Rpbmcgb2YgYSBzaW5nbGUgc3BsaXQgYW5kIHJlc3VsdGluZyBpbiBhbiBhZGRpdGl2ZSBtb2RlbC4gTW9yZSBnZW5lcmFsbHkgZCBpcyB0aGUgaW50ZXJhY3Rpb24gZGVwdGgsIGFuZCBjb250cm9scyB0aGUgaW50ZXJhY3Rpb24gb3JkZXIgb2YgdGhlIGJvb3N0ZWQgbW9kZWwsIHNpbmNlIGQgc3BsaXRzIGNhbiBpbnZvbHZlIGF0IG1vc3QgZCB2YXJpYWJsZXMuDQoNCg0KIyMjIEFwcGxpY2F0aW9uDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIpDQpib29zdC5ib3N0b249Z2JtKG1lZHZ+LixkYXRhPUJvc3Rvblt0cmFpbiAsXSwgZGlzdHJpYnV0aW9uPSJnYXVzc2lhbiIsbi50cmVlcz01MDAwLCBpbnRlcmFjdGlvbi5kZXB0aD00KQ0Kc3VtbWFyeShib29zdC5ib3N0b24pDQpgYGANCg0KQ2xlYXJseSBybSBhbmQgbHN0YXQgYXJlIHRoZSBtb3N0IGluZmx1ZW50aWFsLCBsZXRzIHRha2UgYSBsb29rIGF0IHRoZW0gaW5kaXZpZHVhbGx5DQoNCmBgYHtyfQ0KcGxvdChib29zdC5ib3N0b24gLGk9InJtIikNCmBgYA0KYGBge3J9DQpwbG90KGJvb3N0LmJvc3RvbiAsaT0ibHN0YXQiKQ0KYGBgDQoNCmBgYHtyfQ0KeWhhdC5ib29zdD1wcmVkaWN0IChib29zdC5ib3N0b24gLG5ld2RhdGEgPUJvc3RvblstdHJhaW4gLF0pIywgbi50cmVlcz0/PykNCmBgYA0KDQoNCmBgYHtyfQ0KYmVzdC5pdGVyIDwtIGdibS5wZXJmKGJvb3N0LmJvc3RvbiwgbWV0aG9kID0gIk9PQiIsIHBsb3QgPSBGQUxTRSkNCnByaW50KGJlc3QuaXRlcikNCmBgYA0KDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMSkNCnloYXQuYm9vc3Q9cHJlZGljdCAoYm9vc3QuYm9zdG9uICxuZXdkYXRhID1Cb3N0b25bLXRyYWluICxdLCBuLnRyZWVzPTEwMykNCm1lYW4oKHloYXQuYm9vc3QtQm9zdG9uWy10cmFpbiwgIm1lZHYiXSleMikNCmBgYA0KTm90IGEgZ3JlYXQgTVNFLCBidXQgc3RpbGwgYmV0dGVyIHRoYW4gYmFnZ2luZy4gTGV0J3MgdHJ5IHRvIHR1bmUgaXQgd2l0aCBsYW1iZGENCg0KDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMSkNCmJvb3N0LmJvc3RvbjI9Z2JtKG1lZHZ+LixkYXRhPUJvc3Rvblt0cmFpbiAsXSwgZGlzdHJpYnV0aW9uPSJnYXVzc2lhbiIsbi50cmVlcz01MDAwLCBpbnRlcmFjdGlvbi5kZXB0aD01LCBzaHJpbmthZ2UgPS4xKQ0KYmVzdC5pdGVyIDwtIGdibS5wZXJmKGJvb3N0LmJvc3RvbjIsIG1ldGhvZCA9ICJPT0IiLHBsb3QgPSBGQUxTRSkNCnloYXQuYm9vc3QyPXByZWRpY3QgKGJvb3N0LmJvc3RvbjIgLG5ld2RhdGEgPUJvc3RvblstdHJhaW4gLF0sIG4udHJlZXM9YmVzdC5pdGVyKQ0KbWVhbigoeWhhdC5ib29zdDItQm9zdG9uWy10cmFpbiwgIm1lZHYiXSleMikNCmBgYA0KDQoNCg0KQW4gaW1wcm92ZW1lbnQhDQoNCiMjQ2xhc3NpZmljYXRpb24NCg0KYGBge3J9DQpkYXRhID0gcmVhZC5jc3YoIlNvY2lhbF9OZXR3b3JrX0Fkcy5jc3YiKQ0KaGVhZChkYXRhKQ0KYGBgDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIzNCkNCmRhdGEkQWdlPXNjYWxlKGRhdGEkQWdlKQ0KZGF0YSRFc3RpbWF0ZWRTYWxhcnk9c2NhbGUoZGF0YSRFc3RpbWF0ZWRTYWxhcnkpDQp0cmFpbiA9IHNhbXBsZSAoMTpucm93KGRhdGEpLCBucm93KGRhdGEpLzIpDQpjbGFzc2lmaWVyID0gcmFuZG9tRm9yZXN0KGZhY3RvcihQdXJjaGFzZWQpfkVzdGltYXRlZFNhbGFyeSArIEFnZSwgZGF0YSA9IGRhdGEsIHN1YnNldD0gdHJhaW4sIG50cmVlID0gMTApDQp5aGF0ID0gcHJlZGljdChjbGFzc2lmaWVyLCBuZXdkYXRhPWRhdGFbLXRyYWluICxdKQ0KYGBgDQoNCmBgYHtyfQ0KY29uZnVzaW9uX21hdHJpeCA9IHRhYmxlKGRhdGFbLXRyYWluLCA1XSx5aGF0KQ0KY29uZnVzaW9uX21hdHJpeA0KYGBgDQoNCmBgYHtyfQ0KdGVzdCA9IGRhdGFbLXRyYWluLCBdDQpYMSA9IHNlcShtaW4odGVzdCRFc3RpbWF0ZWRTYWxhcnkpIC0gMSwgbWF4KHRlc3QkRXN0aW1hdGVkU2FsYXJ5KSArIDEsIGJ5ID0gMC4xKQ0KWDIgPSBzZXEobWluKHRlc3QkQWdlKSAtIDEsIG1heCh0ZXN0JEFnZSkgKyAxLCBieSA9IDAuMSkNCmdyaWQgPSBleHBhbmQuZ3JpZChYMSxYMikNCmNvbG5hbWVzKGdyaWQpID0gYygiRXN0aW1hdGVkU2FsYXJ5IiwiQWdlIikNCnByb2IgPSBwcmVkaWN0KGNsYXNzaWZpZXIsIHR5cGUgPSAicmVzcG9uc2UiLCBuZXdkYXRhID0gZ3JpZCkNCmdncGxvdCgpKw0KICBnZW9tX3BvaW50KGRhdGEgPSBncmlkLCBhZXMoeD1Fc3RpbWF0ZWRTYWxhcnkseT1BZ2UsIGNvbG9yID0gcHJvYiksIHNpemUgPSAuMSkrDQogIGdlb21fcG9pbnQoYWVzKHg9RXN0aW1hdGVkU2FsYXJ5LCB5ID0gQWdlLCBjb2xvcj0gZmFjdG9yKFB1cmNoYXNlZCkpLCBkYXRhID0gdGVzdCkrDQogIGdlb21fY29udG91cihhZXMoeD1ncmlkJEVzdGltYXRlZFNhbGFyeSwgeSA9IGdyaWQkQWdlLCB6PWFzLm51bWVyaWMocHJvYikpKQ0KYGBgDQoNCg0KIyNIVw0KDQoxKSBSZXBlYXQgdGhlIHByZXZpb3VzIGNsYXNzaWZpY2F0aW9uIGV4YW1wbGUgdXNpbmcgYmFnZ2luZyBhbmQgYm9vc3RpbmcgbWV0aG9kcyBhbmQgY29tbWVudCBvbiBhbnkgZGlmZmVyZW5jZXMgeW91IG9ic2VydmUuDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0K