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.
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.
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)
}
)
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)
}
)
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)
}
)
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)
}
)
Artificial datasets
RSSL offers many artificial datasets. In the next sections I will generate and visualize them.
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
Real datasets
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)
Spambase
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==