Semi-Supervised Learning

RSSL provides implementations of several semi-supervised learning methods. The code can be found on Github. It is described in the following paper: arXiv. More R libraries are used in this notebook for visualization.

library(RSSL)
library(ggplot2)
library(grid)
library(gridExtra)
library(plot3D)
set.seed(1)

1 Classifiers

There are many classifiers available in the package. The main types are defined below: Least Squares Classifier, Linear Discriminant Analysis, Nearest Mean Classifier and Logistic Regression. They are used together with Self Learning and Expectation Maximization. classifiers is a list of two classifiers, with one supervised classifier and one self learning classifier. Each classifier is a function that accepts 4 arguments: a numeric design matrix of the labeled objects, a factor of labels, a numeric design matrix of unlabeled objects and a factor of labels for the unlabeled objects.

Classifiers available in RSSL

1.1 Self Learning

Self Learning

1.2 Expectation Maximization

Expectation Maximization

1.3 Least Squares Classifier

Least Squares Classifier

classifiers_LS <- list(
    "LS" = function(X, y, X_u, y_u) {
        LeastSquaresClassifier(X, y, lambda = 0)
    },
    "SL" = function(X, y, X_u, y_u) {
        SelfLearning(X, y, X_u, LeastSquaresClassifier)
    },
    "EM" = function(X, y, X_u, y_u) {
        EMLeastSquaresClassifier(X, y, X_u)
    }
)

1.4 Linear Discriminant Analysis

Linear Discriminant Analysis

classifiers_LD <- list(
    "LD" = function(X, y, X_u, y_u) {
        LinearDiscriminantClassifier(X, y)
    },
    "SL" = function(X, y, X_u, y_u) {
        SelfLearning(X, y, X_u, LinearDiscriminantClassifier)
    },
    "EM" = function(X, y, X_u, y_u) {
        EMLinearDiscriminantClassifier(X, y, X_u)
    }
)

1.5 Nearest Mean Classifier

Nearest Mean Classifier

classifiers_NM <- list(
    "NM" = function(X, y, X_u, y_u) {
        NearestMeanClassifier(X, y)
    },
    "SL" = function(X, y, X_u, y_u) {
        SelfLearning(X, y, X_u, NearestMeanClassifier)
    },
    "EM" = function(X, y, X_u, y_u) {
        EMNearestMeanClassifier(X, y, X_u)
    }
)

1.6 Logistic Regression

Logistic Regression

classifiers_LR <- list(
    "LR" = function(X, y, X_u, y_u) {
        LogisticRegression(X, y)
    },
    "SL" = function(X, y, X_u, y_u) {
        SelfLearning(X, y, X_u, LogisticRegression)
    }
)

2 Measures

There are five performance measures available: accuracy, error, test loss, labeled loss and train loss. measures is a list of performance measures that we want to select. Our aim is to improve the accuracy on the test set (or reduce the error). We can look at the losses of each split to know how the model is learning. The time is also an important measure when working with big datasets.

Measures

measures <- list(
    "Accuracy" = measure_accuracy,
    "Error" = measure_error,
    "Loss Test" = measure_losstest,
    "Loss Labeled" = measure_losslab,
    "Loss Train" = measure_losstrain
)

3 Artificial datasets

RSSL offers many artificial datasets. In the next sections I will generate and visualize them.

Simulated Datasets. Each can be generated using a function of the form generateDataset, where Dataset should be replaced by the name of the dataset. (alt) indicates non-default parameters were used when calling the function.

3.1 2ClassGaussian

data <- generate2ClassGaussian(2000, d = 2, var = 0.6, expected = TRUE)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

LearningCurveSSL evaluates semi-supervised classifiers for different amounts of unlabeled training examples or different fractions of unlabeled vs. labeled examples. This function allows for two different types of learning curves to be generated.

If type="unlabeled", the number of labeled objects remains fixed at the value of n_l, where sizes controls the number of unlabeled objects. n_test controls the number of objects used for the test set, while all remaining objects are used if with_replacement=FALSE in which case objects are drawn without replacement from the input dataset. We make sure each class is represented by at least n_min labeled objects of each class. For n_l, additional options include: “enough” which takes the max of the number of features and 20, max(ncol(X)+5,20), “d” which takes the number of features or “2d” which takes 2 times the number of features.

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "unlabeled", n_l = "enough", repeats = 3
)

plot(lc)

If type="fraction" the total number of objects remains fixed, while the fraction of labeled objects is changed. frac sets the fractions of labeled objects that should be considered, while test_fraction determines the fraction of the total number of objects left out to serve as the test set.

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LD, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_NM, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LR, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.2 2ClassGaussian (alt)

data <- generate2ClassGaussian(2000, d = 2, var = 0.6, expected = FALSE)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.3 ABA

data <- generateABA(2000, d = 2, var = 0.6)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.4 CrescentMoon

data <- generateCrescentMoon(150, 2, 1)
plot(data$X1, data$X2, col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 2:3]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.5 FourClusters

data <- generateFourClusters(1000, distance = 6, expected = TRUE)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.6 FourClusters (alt)

data <- generateFourClusters(1000, distance = 6, expected = FALSE)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.7 ParallelPlanes

classifiers_LS_SL <- list(
    "LS" = function(X, y, X_u, y_u) {
        LeastSquaresClassifier(X, y, lambda = 0)
    },
    "SL" = function(X, y, X_u, y_u) {
        SelfLearning(X, y, X_u, LeastSquaresClassifier)
    }
)
data <- generateParallelPlanes(100, 3)
plot(data[, 1], data[, 2], col = data$Class)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS_SL, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.8 SlicedCookie

data <- generateSlicedCookie(1000, expected = TRUE)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.9 SlicedCookie (alt)

data <- generateSlicedCookie(1000, expected = FALSE)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.10 TwoCircles

data <- generateTwoCircles(n = 100, noise_var = 0.2)
plot(data[, 1], data[, 2], col = data$Class, asp = 1)

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

3.11 Spirals

data <- generateSpirals(100, sigma = 0.1)
plot3D::scatter3D(data$x, data$y, data$z, col = data$Class)
Warning in `[<-.factor`(`*tmp*`, is.na(Col), value = "white") :
  invalid factor level, NA generated

lc <- LearningCurveSSL(as.matrix(data[, 1:2]),
    data$Class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", test_fraction = 0.5, repeats = 3
)

plot(lc)

4 Real datasets

4.1 Iris

Iris

data("iris")
iris[sample(nrow(iris), 10), ]
pairs(iris[1:4], main = "Iris Scatter Plots", pch = 21, bg = c("red", "green3", "blue")[unclass(iris$Species)])

BpSl <- ggplot(iris, aes(Species, Sepal.Length, fill = Species)) +
    geom_boxplot() +
    scale_y_continuous("Sepal Length (cm)", breaks = seq(0, 30, by = .5)) +
    theme(legend.position = "none")

BpSw <- ggplot(iris, aes(Species, Sepal.Width, fill = Species)) +
    geom_boxplot() +
    scale_y_continuous("Sepal Width (cm)", breaks = seq(0, 30, by = .5)) +
    theme(legend.position = "none")

BpPl <- ggplot(iris, aes(Species, Petal.Length, fill = Species)) +
    geom_boxplot() +
    scale_y_continuous("Petal Length (cm)", breaks = seq(0, 30, by = .5)) +
    theme(legend.position = "none")

BpPw <- ggplot(iris, aes(Species, Petal.Width, fill = Species)) +
    geom_boxplot() +
    scale_y_continuous("Petal Width (cm)", breaks = seq(0, 30, by = .5)) +
    theme(legend.position = "none")

# Plot all visualizations
grid.arrange(BpSl + ggtitle(""),
    BpSw + ggtitle(""),
    BpPl + ggtitle(""),
    BpPw + ggtitle(""),
    nrow = 2,
    top = textGrob("Iris Box Plots",
        gp = gpar(fontsize = 15)
    )
)

classifiers_LS_SL <- list(
    "LS" = function(X, y, X_u, y_u) {
        LeastSquaresClassifier(X, y, lambda = 0)
    },
    "SL" = function(X, y, X_u, y_u) {
        SelfLearning(X, y, X_u, LeastSquaresClassifier)
    }
)
lc_iris <- LearningCurveSSL(as.matrix(iris[1:4]), iris$Species,
    classifiers = classifiers_LS_SL, measures = measures,
    type = "fraction", fracs = seq(0.1, 0.8, 0.1),
    test_fraction = 0.5, repeats = 3
)

plot(lc_iris)

4.2 Spambase

Spam email Non spam email

spambase <- read.csv("../data/spambase.csv", header = TRUE, sep = ",")
spambase$class <- as.factor(spambase$class)
spambase[sample(nrow(spambase), 10), ]
lc_spambase <- LearningCurveSSL(as.matrix(spambase[1:57]), spambase$class,
    classifiers = classifiers_LS, measures = measures,
    type = "fraction", fracs = seq(0.1, 0.8, 0.1),
    test_fraction = 0.5, repeats = 3
)

plot(lc_spambase)

LS0tDQp0aXRsZTogJ1JTU0w6IFNlbWktU3VwZXJ2aXNlZCBMZWFybmluZyBpbiBSJw0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOiANCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCi0tLQ0KDQohW1NlbWktU3VwZXJ2aXNlZCBMZWFybmluZ10oLi4vaW1hZ2VzL3NlbWlfc3VwZXJ2aXNlZF9sZWFybmluZy5wbmcpDQoNClJTU0wgcHJvdmlkZXMgaW1wbGVtZW50YXRpb25zIG9mIHNldmVyYWwgc2VtaS1zdXBlcnZpc2VkIGxlYXJuaW5nIG1ldGhvZHMuIFRoZSBjb2RlIGNhbiBiZSBmb3VuZCBvbiBbR2l0aHViXShodHRwczovL2dpdGh1Yi5jb20vamtyaWp0aGUvUlNTTCkuIEl0IGlzIGRlc2NyaWJlZCBpbiB0aGUgZm9sbG93aW5nIHBhcGVyOiBbYXJYaXZdKGh0dHBzOi8vYXJ4aXYub3JnL3BkZi8xNjEyLjA3OTkzLnBkZikuIE1vcmUgUiBsaWJyYXJpZXMgYXJlIHVzZWQgaW4gdGhpcyBub3RlYm9vayBmb3IgdmlzdWFsaXphdGlvbi4NCg0KYGBge3J9DQpsaWJyYXJ5KFJTU0wpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGdyaWQpDQpsaWJyYXJ5KGdyaWRFeHRyYSkNCmxpYnJhcnkocGxvdDNEKQ0Kc2V0LnNlZWQoMSkNCmBgYA0KDQojIENsYXNzaWZpZXJzDQoNClRoZXJlIGFyZSBtYW55IGNsYXNzaWZpZXJzIGF2YWlsYWJsZSBpbiB0aGUgcGFja2FnZS4gVGhlIG1haW4gdHlwZXMgYXJlIGRlZmluZWQgYmVsb3c6IExlYXN0IFNxdWFyZXMgQ2xhc3NpZmllciwgTGluZWFyIERpc2NyaW1pbmFudCBBbmFseXNpcywgTmVhcmVzdCBNZWFuIENsYXNzaWZpZXIgYW5kIExvZ2lzdGljIFJlZ3Jlc3Npb24uIFRoZXkgYXJlIHVzZWQgdG9nZXRoZXIgd2l0aCBTZWxmIExlYXJuaW5nIGFuZCBFeHBlY3RhdGlvbiBNYXhpbWl6YXRpb24uIGBjbGFzc2lmaWVyc2AgaXMgYSBsaXN0IG9mIHR3byBjbGFzc2lmaWVycywgd2l0aCBvbmUgc3VwZXJ2aXNlZCBjbGFzc2lmaWVyIGFuZCBvbmUgc2VsZiBsZWFybmluZyBjbGFzc2lmaWVyLiBFYWNoDQpjbGFzc2lmaWVyIGlzIGEgZnVuY3Rpb24gdGhhdCBhY2NlcHRzIDQgYXJndW1lbnRzOiBhDQpudW1lcmljIGRlc2lnbiBtYXRyaXggb2YgdGhlIGxhYmVsZWQgb2JqZWN0cywgYSBmYWN0b3Igb2YgbGFiZWxzLA0KYSBudW1lcmljIGRlc2lnbiBtYXRyaXggb2YgdW5sYWJlbGVkIG9iamVjdHMgYW5kIGEgZmFjdG9yIG9mDQpsYWJlbHMgZm9yIHRoZSB1bmxhYmVsZWQgb2JqZWN0cy4NCg0KIVtDbGFzc2lmaWVycyBhdmFpbGFibGUgaW4gUlNTTF0oLi4vaW1hZ2VzL2NsYXNzaWZpZXJzLnBuZykNCg0KIyMgU2VsZiBMZWFybmluZw0KDQohW1NlbGYgTGVhcm5pbmddKC4uL2ltYWdlcy9TTC5wbmcpDQoNCiMjIEV4cGVjdGF0aW9uIE1heGltaXphdGlvbg0KDQohW0V4cGVjdGF0aW9uIE1heGltaXphdGlvbl0oLi4vaW1hZ2VzL0VNLnBuZykNCg0KIyMgTGVhc3QgU3F1YXJlcyBDbGFzc2lmaWVyDQoNCiFbTGVhc3QgU3F1YXJlcyBDbGFzc2lmaWVyXSguLi9pbWFnZXMvTFMucG5nKQ0KDQpgYGB7cn0NCmNsYXNzaWZpZXJzX0xTIDwtIGxpc3QoDQogICAgIkxTIiA9IGZ1bmN0aW9uKFgsIHksIFhfdSwgeV91KSB7DQogICAgICAgIExlYXN0U3F1YXJlc0NsYXNzaWZpZXIoWCwgeSwgbGFtYmRhID0gMCkNCiAgICB9LA0KICAgICJTTCIgPSBmdW5jdGlvbihYLCB5LCBYX3UsIHlfdSkgew0KICAgICAgICBTZWxmTGVhcm5pbmcoWCwgeSwgWF91LCBMZWFzdFNxdWFyZXNDbGFzc2lmaWVyKQ0KICAgIH0sDQogICAgIkVNIiA9IGZ1bmN0aW9uKFgsIHksIFhfdSwgeV91KSB7DQogICAgICAgIEVNTGVhc3RTcXVhcmVzQ2xhc3NpZmllcihYLCB5LCBYX3UpDQogICAgfQ0KKQ0KYGBgDQoNCiMjIExpbmVhciBEaXNjcmltaW5hbnQgQW5hbHlzaXMNCg0KIVtMaW5lYXIgRGlzY3JpbWluYW50IEFuYWx5c2lzXSguLi9pbWFnZXMvTEQuanBnKQ0KDQpgYGB7cn0NCmNsYXNzaWZpZXJzX0xEIDwtIGxpc3QoDQogICAgIkxEIiA9IGZ1bmN0aW9uKFgsIHksIFhfdSwgeV91KSB7DQogICAgICAgIExpbmVhckRpc2NyaW1pbmFudENsYXNzaWZpZXIoWCwgeSkNCiAgICB9LA0KICAgICJTTCIgPSBmdW5jdGlvbihYLCB5LCBYX3UsIHlfdSkgew0KICAgICAgICBTZWxmTGVhcm5pbmcoWCwgeSwgWF91LCBMaW5lYXJEaXNjcmltaW5hbnRDbGFzc2lmaWVyKQ0KICAgIH0sDQogICAgIkVNIiA9IGZ1bmN0aW9uKFgsIHksIFhfdSwgeV91KSB7DQogICAgICAgIEVNTGluZWFyRGlzY3JpbWluYW50Q2xhc3NpZmllcihYLCB5LCBYX3UpDQogICAgfQ0KKQ0KYGBgDQoNCg0KIyMgTmVhcmVzdCBNZWFuIENsYXNzaWZpZXINCg0KIVtOZWFyZXN0IE1lYW4gQ2xhc3NpZmllcl0oLi4vaW1hZ2VzL05NLnBuZykNCg0KYGBge3J9DQpjbGFzc2lmaWVyc19OTSA8LSBsaXN0KA0KICAgICJOTSIgPSBmdW5jdGlvbihYLCB5LCBYX3UsIHlfdSkgew0KICAgICAgICBOZWFyZXN0TWVhbkNsYXNzaWZpZXIoWCwgeSkNCiAgICB9LA0KICAgICJTTCIgPSBmdW5jdGlvbihYLCB5LCBYX3UsIHlfdSkgew0KICAgICAgICBTZWxmTGVhcm5pbmcoWCwgeSwgWF91LCBOZWFyZXN0TWVhbkNsYXNzaWZpZXIpDQogICAgfSwNCiAgICAiRU0iID0gZnVuY3Rpb24oWCwgeSwgWF91LCB5X3UpIHsNCiAgICAgICAgRU1OZWFyZXN0TWVhbkNsYXNzaWZpZXIoWCwgeSwgWF91KQ0KICAgIH0NCikNCmBgYA0KDQojIyBMb2dpc3RpYyBSZWdyZXNzaW9uDQoNCiFbTG9naXN0aWMgUmVncmVzc2lvbl0oLi4vaW1hZ2VzL0xSLmpwZykNCg0KYGBge3J9DQpjbGFzc2lmaWVyc19MUiA8LSBsaXN0KA0KICAgICJMUiIgPSBmdW5jdGlvbihYLCB5LCBYX3UsIHlfdSkgew0KICAgICAgICBMb2dpc3RpY1JlZ3Jlc3Npb24oWCwgeSkNCiAgICB9LA0KICAgICJTTCIgPSBmdW5jdGlvbihYLCB5LCBYX3UsIHlfdSkgew0KICAgICAgICBTZWxmTGVhcm5pbmcoWCwgeSwgWF91LCBMb2dpc3RpY1JlZ3Jlc3Npb24pDQogICAgfQ0KKQ0KYGBgDQoNCiMgTWVhc3VyZXMNCg0KVGhlcmUgYXJlIGZpdmUgcGVyZm9ybWFuY2UgbWVhc3VyZXMgYXZhaWxhYmxlOiBhY2N1cmFjeSwgZXJyb3IsIHRlc3QgbG9zcywgbGFiZWxlZCBsb3NzIGFuZCB0cmFpbiBsb3NzLiBgbWVhc3VyZXNgIGlzIGEgbGlzdCBvZiBwZXJmb3JtYW5jZSBtZWFzdXJlcyB0aGF0IHdlIHdhbnQgdG8gc2VsZWN0LiBPdXIgYWltIGlzIHRvIGltcHJvdmUgdGhlIGFjY3VyYWN5IG9uIHRoZSB0ZXN0IHNldCAob3IgcmVkdWNlIHRoZSBlcnJvcikuIFdlIGNhbiBsb29rIGF0IHRoZSBsb3NzZXMgb2YgZWFjaCBzcGxpdCB0byBrbm93IGhvdyB0aGUgbW9kZWwgaXMgbGVhcm5pbmcuIFRoZSB0aW1lIGlzIGFsc28gYW4gaW1wb3J0YW50IG1lYXN1cmUgd2hlbiB3b3JraW5nIHdpdGggYmlnIGRhdGFzZXRzLg0KDQohW01lYXN1cmVzXSguLi9pbWFnZXMvbWVhc3VyZXMucG5nKQ0KDQpgYGB7cn0NCm1lYXN1cmVzIDwtIGxpc3QoDQogICAgIkFjY3VyYWN5IiA9IG1lYXN1cmVfYWNjdXJhY3ksDQogICAgIkVycm9yIiA9IG1lYXN1cmVfZXJyb3IsDQogICAgIkxvc3MgVGVzdCIgPSBtZWFzdXJlX2xvc3N0ZXN0LA0KICAgICJMb3NzIExhYmVsZWQiID0gbWVhc3VyZV9sb3NzbGFiLA0KICAgICJMb3NzIFRyYWluIiA9IG1lYXN1cmVfbG9zc3RyYWluDQopDQpgYGANCg0KIyBBcnRpZmljaWFsIGRhdGFzZXRzDQoNClJTU0wgb2ZmZXJzIG1hbnkgYXJ0aWZpY2lhbCBkYXRhc2V0cy4gSW4gdGhlIG5leHQgc2VjdGlvbnMgSSB3aWxsIGdlbmVyYXRlIGFuZCB2aXN1YWxpemUgdGhlbS4NCg0KIVtTaW11bGF0ZWQgRGF0YXNldHMuIEVhY2ggY2FuIGJlIGdlbmVyYXRlZCB1c2luZyBhIGZ1bmN0aW9uIG9mIHRoZSBmb3JtDQpnZW5lcmF0ZURhdGFzZXQsIHdoZXJlIERhdGFzZXQgc2hvdWxkIGJlIHJlcGxhY2VkIGJ5IHRoZSBuYW1lIG9mIHRoZSBkYXRhc2V0LiAoYWx0KSBpbmRpY2F0ZXMNCm5vbi1kZWZhdWx0IHBhcmFtZXRlcnMgd2VyZSB1c2VkIHdoZW4gY2FsbGluZyB0aGUgZnVuY3Rpb24uXSguLi9pbWFnZXMvYXJ0aWZpY2lhbF9kYXRhc2V0cy5wbmcpDQoNCiMjIDJDbGFzc0dhdXNzaWFuDQoNCmBgYHtyfQ0KZGF0YSA8LSBnZW5lcmF0ZTJDbGFzc0dhdXNzaWFuKDIwMDAsIGQgPSAyLCB2YXIgPSAwLjYsIGV4cGVjdGVkID0gVFJVRSkNCnBsb3QoZGF0YVssIDFdLCBkYXRhWywgMl0sIGNvbCA9IGRhdGEkQ2xhc3MsIGFzcCA9IDEpDQpgYGANCg0KYExlYXJuaW5nQ3VydmVTU0xgIGV2YWx1YXRlcyBzZW1pLXN1cGVydmlzZWQgY2xhc3NpZmllcnMgZm9yIGRpZmZlcmVudCBhbW91bnRzIG9mDQp1bmxhYmVsZWQgdHJhaW5pbmcgZXhhbXBsZXMgb3IgZGlmZmVyZW50IGZyYWN0aW9ucyBvZiB1bmxhYmVsZWQNCnZzLiBsYWJlbGVkIGV4YW1wbGVzLiBUaGlzIGZ1bmN0aW9uIGFsbG93cyBmb3IgdHdvIGRpZmZlcmVudCB0eXBlcyBvZiBsZWFybmluZyBjdXJ2ZXMgdG8NCmJlIGdlbmVyYXRlZC4gDQoNCklmIGB0eXBlPSJ1bmxhYmVsZWQiYCwgdGhlIG51bWJlciBvZiBsYWJlbGVkIG9iamVjdHMNCnJlbWFpbnMgZml4ZWQgYXQgdGhlIHZhbHVlIG9mIGBuX2xgLCB3aGVyZSBgc2l6ZXNgIGNvbnRyb2xzIHRoZQ0KbnVtYmVyIG9mIHVubGFiZWxlZCBvYmplY3RzLiBgbl90ZXN0YCBjb250cm9scyB0aGUgbnVtYmVyIG9mDQpvYmplY3RzIHVzZWQgZm9yIHRoZSB0ZXN0IHNldCwgd2hpbGUgYWxsIHJlbWFpbmluZyBvYmplY3RzIGFyZQ0KdXNlZCBpZiBgd2l0aF9yZXBsYWNlbWVudD1GQUxTRWAgaW4gd2hpY2ggY2FzZSBvYmplY3RzIGFyZSBkcmF3bg0Kd2l0aG91dCByZXBsYWNlbWVudCBmcm9tIHRoZSBpbnB1dCBkYXRhc2V0LiBXZSBtYWtlIHN1cmUgZWFjaA0KY2xhc3MgaXMgcmVwcmVzZW50ZWQgYnkgYXQgbGVhc3QgYG5fbWluYCBsYWJlbGVkIG9iamVjdHMgb2YgZWFjaA0KY2xhc3MuIEZvciBgbl9sYCwgYWRkaXRpb25hbCBvcHRpb25zIGluY2x1ZGU6ICJlbm91Z2giIHdoaWNoIHRha2VzDQp0aGUgbWF4IG9mIHRoZSBudW1iZXIgb2YgZmVhdHVyZXMgYW5kIDIwLCBtYXgobmNvbChYKSs1LDIwKSwgImQiDQp3aGljaCB0YWtlcyB0aGUgbnVtYmVyIG9mIGZlYXR1cmVzIG9yICIyZCIgd2hpY2ggdGFrZXMgMiB0aW1lcyB0aGUNCm51bWJlciBvZiBmZWF0dXJlcy4NCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMToyXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xTLCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAidW5sYWJlbGVkIiwgbl9sID0gImVub3VnaCIsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KSWYgYHR5cGU9ImZyYWN0aW9uImAgdGhlIHRvdGFsIG51bWJlciBvZiBvYmplY3RzIHJlbWFpbnMgZml4ZWQsDQp3aGlsZSB0aGUgZnJhY3Rpb24gb2YgbGFiZWxlZCBvYmplY3RzIGlzIGNoYW5nZWQuIGBmcmFjYCBzZXRzIHRoZQ0KZnJhY3Rpb25zIG9mIGxhYmVsZWQgb2JqZWN0cyB0aGF0IHNob3VsZCBiZSBjb25zaWRlcmVkLCB3aGlsZQ0KYHRlc3RfZnJhY3Rpb25gIGRldGVybWluZXMgdGhlIGZyYWN0aW9uIG9mIHRoZSB0b3RhbCBudW1iZXIgb2YNCm9iamVjdHMgbGVmdCBvdXQgdG8gc2VydmUgYXMgdGhlIHRlc3Qgc2V0Lg0KDQpgYGB7cn0NCmxjIDwtIExlYXJuaW5nQ3VydmVTU0woYXMubWF0cml4KGRhdGFbLCAxOjJdKSwNCiAgICBkYXRhJENsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFMsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMToyXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xELCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAiZnJhY3Rpb24iLCB0ZXN0X2ZyYWN0aW9uID0gMC41LCByZXBlYXRzID0gMw0KKQ0KDQpwbG90KGxjKQ0KYGBgDQoNCmBgYHtyfQ0KbGMgPC0gTGVhcm5pbmdDdXJ2ZVNTTChhcy5tYXRyaXgoZGF0YVssIDE6Ml0pLA0KICAgIGRhdGEkQ2xhc3MsDQogICAgY2xhc3NpZmllcnMgPSBjbGFzc2lmaWVyc19OTSwgbWVhc3VyZXMgPSBtZWFzdXJlcywNCiAgICB0eXBlID0gImZyYWN0aW9uIiwgdGVzdF9mcmFjdGlvbiA9IDAuNSwgcmVwZWF0cyA9IDMNCikNCg0KcGxvdChsYykNCmBgYA0KDQpgYGB7cn0NCmxjIDwtIExlYXJuaW5nQ3VydmVTU0woYXMubWF0cml4KGRhdGFbLCAxOjJdKSwNCiAgICBkYXRhJENsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFIsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KIyMgMkNsYXNzR2F1c3NpYW4gKGFsdCkNCg0KYGBge3J9DQpkYXRhIDwtIGdlbmVyYXRlMkNsYXNzR2F1c3NpYW4oMjAwMCwgZCA9IDIsIHZhciA9IDAuNiwgZXhwZWN0ZWQgPSBGQUxTRSkNCnBsb3QoZGF0YVssIDFdLCBkYXRhWywgMl0sIGNvbCA9IGRhdGEkQ2xhc3MsIGFzcCA9IDEpDQpgYGANCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMToyXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xTLCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAiZnJhY3Rpb24iLCB0ZXN0X2ZyYWN0aW9uID0gMC41LCByZXBlYXRzID0gMw0KKQ0KDQpwbG90KGxjKQ0KYGBgDQoNCg0KIyMgQUJBDQoNCmBgYHtyfQ0KZGF0YSA8LSBnZW5lcmF0ZUFCQSgyMDAwLCBkID0gMiwgdmFyID0gMC42KQ0KcGxvdChkYXRhWywgMV0sIGRhdGFbLCAyXSwgY29sID0gZGF0YSRDbGFzcywgYXNwID0gMSkNCmBgYA0KDQpgYGB7cn0NCmxjIDwtIExlYXJuaW5nQ3VydmVTU0woYXMubWF0cml4KGRhdGFbLCAxOjJdKSwNCiAgICBkYXRhJENsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFMsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KIyMgQ3Jlc2NlbnRNb29uDQoNCmBgYHtyfQ0KZGF0YSA8LSBnZW5lcmF0ZUNyZXNjZW50TW9vbigxNTAsIDIsIDEpDQpwbG90KGRhdGEkWDEsIGRhdGEkWDIsIGNvbCA9IGRhdGEkQ2xhc3MsIGFzcCA9IDEpDQpgYGANCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMjozXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xTLCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAiZnJhY3Rpb24iLCB0ZXN0X2ZyYWN0aW9uID0gMC41LCByZXBlYXRzID0gMw0KKQ0KDQpwbG90KGxjKQ0KYGBgDQoNCiMjIEZvdXJDbHVzdGVycw0KDQpgYGB7cn0NCmRhdGEgPC0gZ2VuZXJhdGVGb3VyQ2x1c3RlcnMoMTAwMCwgZGlzdGFuY2UgPSA2LCBleHBlY3RlZCA9IFRSVUUpDQpwbG90KGRhdGFbLCAxXSwgZGF0YVssIDJdLCBjb2wgPSBkYXRhJENsYXNzLCBhc3AgPSAxKQ0KYGBgDQoNCmBgYHtyfQ0KbGMgPC0gTGVhcm5pbmdDdXJ2ZVNTTChhcy5tYXRyaXgoZGF0YVssIDE6Ml0pLA0KICAgIGRhdGEkQ2xhc3MsDQogICAgY2xhc3NpZmllcnMgPSBjbGFzc2lmaWVyc19MUywgbWVhc3VyZXMgPSBtZWFzdXJlcywNCiAgICB0eXBlID0gImZyYWN0aW9uIiwgdGVzdF9mcmFjdGlvbiA9IDAuNSwgcmVwZWF0cyA9IDMNCikNCg0KcGxvdChsYykNCmBgYA0KDQojIyBGb3VyQ2x1c3RlcnMgKGFsdCkNCg0KYGBge3J9DQpkYXRhIDwtIGdlbmVyYXRlRm91ckNsdXN0ZXJzKDEwMDAsIGRpc3RhbmNlID0gNiwgZXhwZWN0ZWQgPSBGQUxTRSkNCnBsb3QoZGF0YVssIDFdLCBkYXRhWywgMl0sIGNvbCA9IGRhdGEkQ2xhc3MsIGFzcCA9IDEpDQpgYGANCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMToyXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xTLCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAiZnJhY3Rpb24iLCB0ZXN0X2ZyYWN0aW9uID0gMC41LCByZXBlYXRzID0gMw0KKQ0KDQpwbG90KGxjKQ0KYGBgDQoNCiMjIFBhcmFsbGVsUGxhbmVzDQoNCmBgYHtyfQ0KY2xhc3NpZmllcnNfTFNfU0wgPC0gbGlzdCgNCiAgICAiTFMiID0gZnVuY3Rpb24oWCwgeSwgWF91LCB5X3UpIHsNCiAgICAgICAgTGVhc3RTcXVhcmVzQ2xhc3NpZmllcihYLCB5LCBsYW1iZGEgPSAwKQ0KICAgIH0sDQogICAgIlNMIiA9IGZ1bmN0aW9uKFgsIHksIFhfdSwgeV91KSB7DQogICAgICAgIFNlbGZMZWFybmluZyhYLCB5LCBYX3UsIExlYXN0U3F1YXJlc0NsYXNzaWZpZXIpDQogICAgfQ0KKQ0KYGBgDQoNCmBgYHtyfQ0KZGF0YSA8LSBnZW5lcmF0ZVBhcmFsbGVsUGxhbmVzKDEwMCwgMykNCnBsb3QoZGF0YVssIDFdLCBkYXRhWywgMl0sIGNvbCA9IGRhdGEkQ2xhc3MpDQpgYGANCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMToyXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xTX1NMLCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAiZnJhY3Rpb24iLCB0ZXN0X2ZyYWN0aW9uID0gMC41LCByZXBlYXRzID0gMw0KKQ0KDQpwbG90KGxjKQ0KYGBgDQoNCiMjIFNsaWNlZENvb2tpZQ0KDQpgYGB7cn0NCmRhdGEgPC0gZ2VuZXJhdGVTbGljZWRDb29raWUoMTAwMCwgZXhwZWN0ZWQgPSBUUlVFKQ0KcGxvdChkYXRhWywgMV0sIGRhdGFbLCAyXSwgY29sID0gZGF0YSRDbGFzcywgYXNwID0gMSkNCmBgYA0KDQpgYGB7cn0NCmxjIDwtIExlYXJuaW5nQ3VydmVTU0woYXMubWF0cml4KGRhdGFbLCAxOjJdKSwNCiAgICBkYXRhJENsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFMsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KIyMgU2xpY2VkQ29va2llIChhbHQpDQoNCmBgYHtyfQ0KZGF0YSA8LSBnZW5lcmF0ZVNsaWNlZENvb2tpZSgxMDAwLCBleHBlY3RlZCA9IEZBTFNFKQ0KcGxvdChkYXRhWywgMV0sIGRhdGFbLCAyXSwgY29sID0gZGF0YSRDbGFzcywgYXNwID0gMSkNCmBgYA0KDQpgYGB7cn0NCmxjIDwtIExlYXJuaW5nQ3VydmVTU0woYXMubWF0cml4KGRhdGFbLCAxOjJdKSwNCiAgICBkYXRhJENsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFMsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KIyMgVHdvQ2lyY2xlcw0KDQpgYGB7cn0NCmRhdGEgPC0gZ2VuZXJhdGVUd29DaXJjbGVzKG4gPSAxMDAsIG5vaXNlX3ZhciA9IDAuMikNCnBsb3QoZGF0YVssIDFdLCBkYXRhWywgMl0sIGNvbCA9IGRhdGEkQ2xhc3MsIGFzcCA9IDEpDQpgYGANCg0KYGBge3J9DQpsYyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChkYXRhWywgMToyXSksDQogICAgZGF0YSRDbGFzcywNCiAgICBjbGFzc2lmaWVycyA9IGNsYXNzaWZpZXJzX0xTLCBtZWFzdXJlcyA9IG1lYXN1cmVzLA0KICAgIHR5cGUgPSAiZnJhY3Rpb24iLCB0ZXN0X2ZyYWN0aW9uID0gMC41LCByZXBlYXRzID0gMw0KKQ0KDQpwbG90KGxjKQ0KYGBgDQoNCiMjIFNwaXJhbHMNCg0KYGBge3J9DQpkYXRhIDwtIGdlbmVyYXRlU3BpcmFscygxMDAsIHNpZ21hID0gMC4xKQ0KcGxvdDNEOjpzY2F0dGVyM0QoZGF0YSR4LCBkYXRhJHksIGRhdGEkeiwgY29sID0gZGF0YSRDbGFzcykNCmBgYA0KDQpgYGB7cn0NCmxjIDwtIExlYXJuaW5nQ3VydmVTU0woYXMubWF0cml4KGRhdGFbLCAxOjJdKSwNCiAgICBkYXRhJENsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFMsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGMpDQpgYGANCg0KIyBSZWFsIGRhdGFzZXRzDQoNCiMjIElyaXMNCg0KIVtJcmlzXSguLi9pbWFnZXMvaXJpcy5wbmcpDQoNCmBgYHtyfQ0KZGF0YSgiaXJpcyIpDQppcmlzW3NhbXBsZShucm93KGlyaXMpLCAxMCksIF0NCmBgYA0KDQpgYGB7cn0NCnBhaXJzKGlyaXNbMTo0XSwgbWFpbiA9ICJJcmlzIFNjYXR0ZXIgUGxvdHMiLCBwY2ggPSAyMSwgYmcgPSBjKCJyZWQiLCAiZ3JlZW4zIiwgImJsdWUiKVt1bmNsYXNzKGlyaXMkU3BlY2llcyldKQ0KYGBgDQoNCmBgYHtyfQ0KQnBTbCA8LSBnZ3Bsb3QoaXJpcywgYWVzKFNwZWNpZXMsIFNlcGFsLkxlbmd0aCwgZmlsbCA9IFNwZWNpZXMpKSArDQogICAgZ2VvbV9ib3hwbG90KCkgKw0KICAgIHNjYWxlX3lfY29udGludW91cygiU2VwYWwgTGVuZ3RoIChjbSkiLCBicmVha3MgPSBzZXEoMCwgMzAsIGJ5ID0gLjUpKSArDQogICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KDQpCcFN3IDwtIGdncGxvdChpcmlzLCBhZXMoU3BlY2llcywgU2VwYWwuV2lkdGgsIGZpbGwgPSBTcGVjaWVzKSkgKw0KICAgIGdlb21fYm94cGxvdCgpICsNCiAgICBzY2FsZV95X2NvbnRpbnVvdXMoIlNlcGFsIFdpZHRoIChjbSkiLCBicmVha3MgPSBzZXEoMCwgMzAsIGJ5ID0gLjUpKSArDQogICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KDQpCcFBsIDwtIGdncGxvdChpcmlzLCBhZXMoU3BlY2llcywgUGV0YWwuTGVuZ3RoLCBmaWxsID0gU3BlY2llcykpICsNCiAgICBnZW9tX2JveHBsb3QoKSArDQogICAgc2NhbGVfeV9jb250aW51b3VzKCJQZXRhbCBMZW5ndGggKGNtKSIsIGJyZWFrcyA9IHNlcSgwLCAzMCwgYnkgPSAuNSkpICsNCiAgICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpDQoNCkJwUHcgPC0gZ2dwbG90KGlyaXMsIGFlcyhTcGVjaWVzLCBQZXRhbC5XaWR0aCwgZmlsbCA9IFNwZWNpZXMpKSArDQogICAgZ2VvbV9ib3hwbG90KCkgKw0KICAgIHNjYWxlX3lfY29udGludW91cygiUGV0YWwgV2lkdGggKGNtKSIsIGJyZWFrcyA9IHNlcSgwLCAzMCwgYnkgPSAuNSkpICsNCiAgICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpDQoNCiMgUGxvdCBhbGwgdmlzdWFsaXphdGlvbnMNCmdyaWQuYXJyYW5nZShCcFNsICsgZ2d0aXRsZSgiIiksDQogICAgQnBTdyArIGdndGl0bGUoIiIpLA0KICAgIEJwUGwgKyBnZ3RpdGxlKCIiKSwNCiAgICBCcFB3ICsgZ2d0aXRsZSgiIiksDQogICAgbnJvdyA9IDIsDQogICAgdG9wID0gdGV4dEdyb2IoIklyaXMgQm94IFBsb3RzIiwNCiAgICAgICAgZ3AgPSBncGFyKGZvbnRzaXplID0gMTUpDQogICAgKQ0KKQ0KYGBgDQoNCmBgYHtyfQ0KY2xhc3NpZmllcnNfTFNfU0wgPC0gbGlzdCgNCiAgICAiTFMiID0gZnVuY3Rpb24oWCwgeSwgWF91LCB5X3UpIHsNCiAgICAgICAgTGVhc3RTcXVhcmVzQ2xhc3NpZmllcihYLCB5LCBsYW1iZGEgPSAwKQ0KICAgIH0sDQogICAgIlNMIiA9IGZ1bmN0aW9uKFgsIHksIFhfdSwgeV91KSB7DQogICAgICAgIFNlbGZMZWFybmluZyhYLCB5LCBYX3UsIExlYXN0U3F1YXJlc0NsYXNzaWZpZXIpDQogICAgfQ0KKQ0KYGBgDQoNCmBgYHtyfQ0KbGNfaXJpcyA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChpcmlzWzE6NF0pLCBpcmlzJFNwZWNpZXMsDQogICAgY2xhc3NpZmllcnMgPSBjbGFzc2lmaWVyc19MU19TTCwgbWVhc3VyZXMgPSBtZWFzdXJlcywNCiAgICB0eXBlID0gImZyYWN0aW9uIiwgZnJhY3MgPSBzZXEoMC4xLCAwLjgsIDAuMSksDQogICAgdGVzdF9mcmFjdGlvbiA9IDAuNSwgcmVwZWF0cyA9IDMNCikNCg0KcGxvdChsY19pcmlzKQ0KYGBgDQoNCiMjIFNwYW1iYXNlDQoNCiFbU3BhbSBlbWFpbF0oLi4vaW1hZ2VzL3NwYW0ucG5nKQ0KIVtOb24gc3BhbSBlbWFpbF0oLi4vaW1hZ2VzL25vbnNwYW0ucG5nKQ0KDQpgYGB7cn0NCnNwYW1iYXNlIDwtIHJlYWQuY3N2KCIuLi9kYXRhL3NwYW1iYXNlLmNzdiIsIGhlYWRlciA9IFRSVUUsIHNlcCA9ICIsIikNCnNwYW1iYXNlJGNsYXNzIDwtIGFzLmZhY3RvcihzcGFtYmFzZSRjbGFzcykNCnNwYW1iYXNlW3NhbXBsZShucm93KHNwYW1iYXNlKSwgMTApLCBdDQpgYGANCg0KYGBge3J9DQpsY19zcGFtYmFzZSA8LSBMZWFybmluZ0N1cnZlU1NMKGFzLm1hdHJpeChzcGFtYmFzZVsxOjU3XSksIHNwYW1iYXNlJGNsYXNzLA0KICAgIGNsYXNzaWZpZXJzID0gY2xhc3NpZmllcnNfTFMsIG1lYXN1cmVzID0gbWVhc3VyZXMsDQogICAgdHlwZSA9ICJmcmFjdGlvbiIsIGZyYWNzID0gc2VxKDAuMSwgMC44LCAwLjEpLA0KICAgIHRlc3RfZnJhY3Rpb24gPSAwLjUsIHJlcGVhdHMgPSAzDQopDQoNCnBsb3QobGNfc3BhbWJhc2UpDQpgYGANCg==