From this site: https://shiring.github.io/machine_learning/2017/03/31/webinar_code

The dataset

The dataset I am using in these example analyses, is the Breast Cancer Wisconsin (Diagnostic) Dataset. The data was downloaded from the UC Irvine Machine Learning Repository(http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Diagnostic%29).

The first dataset looks at the predictor classes:

The features characterize cell nucleus properties and were generated from image analysis of fine needle aspirates (FNA) of breast masses:

Handling missing data

bc_data[bc_data == "?"] <- NA
# how many NAs are in the data
length(which(is.na(bc_data)))
[1] 16
# how many samples would we loose, if we removed them?
nrow(bc_data)
[1] 699
nrow(bc_data[is.na(bc_data), ])
[1] 16

Missing values are imputed with the mice package.

summary(bc_data$classes)
   benign malignant 
      458       241 

Data exploration

We can see that our data is unbalanced.

Why is unbalanced data a problem in machine learning?

Most machine learning classification algorithms are sensitive to unbalance in the predictor classes. Let’s consider an even more extreme example than our breast cancer dataset: assume we had 10 malignant vs 90 benign samples. A machine learning model that has been trained and tested on such a dataset could now predict “benign” for all samples and still gain a very high accuracy. An unbalanced dataset will bias the prediction model towards the more common class!

How to balance data for modeling

The basic theoretical concepts behind over- and under-sampling are very simple:

In reality though, we should not simply perform over- or under-sampling on our training data and then run the model. We need to account for cross-validation and perform over- or under-sampling on each fold independently to get an honest estimate of model performance!

Modeling the original unbalanced data (example)

I randomly divide the data into training and test sets (stratified by class) and perform Random Forest modeling with 10 x 10 repeated cross-validation. Final model performance is then measured on the test set.

library(caret)
Loading required package: lattice
Loading required package: ggplot2

Under-sampling

Luckily, caret makes it very easy to incorporate over- and under-sampling techniques with cross-validation resampling. We can simply add the sampling option to our trainControl and choose down for under- (also called down-) sampling. The rest stays the same as with our original model.

cm_under
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       132         0
  malignant      5        72
                                          
               Accuracy : 0.9761          
                 95% CI : (0.9451, 0.9922)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : < 2e-16         
                                          
                  Kappa : 0.9479          
 Mcnemar's Test P-Value : 0.07364         
                                          
            Sensitivity : 0.9635          
            Specificity : 1.0000          
         Pos Pred Value : 1.0000          
         Neg Pred Value : 0.9351          
             Prevalence : 0.6555          
         Detection Rate : 0.6316          
   Detection Prevalence : 0.6316          
      Balanced Accuracy : 0.9818          
                                          
       'Positive' Class : benign          
                                          

Oversampling

For over- (also called up-) sampling we simply specify sampling = "up".

ctrl <- trainControl(method = "repeatedcv", 
                     number = 10, 
                     repeats = 10, 
                     verboseIter = FALSE,
                     sampling = "up")
set.seed(42)
model_rf_over <- caret::train(classes ~ .,
                         data = train_data,
                         method = "rf",
                         preProcess = c("scale", "center"),
                         trControl = ctrl)
Loading required package: randomForest
randomForest 4.6-12
Type rfNews() to see new features/changes/bug fixes.

Attaching package: ‘randomForest’

The following object is masked from ‘package:ggplot2’:

    margin
final_over <- data.frame(actual = test_data$classes,
                          predict(model_rf_over, newdata = test_data, type = "prob"))
final_over$predict <- ifelse(final_over$benign > 0.5, "benign", "malignant")
cm_over <- confusionMatrix(final_over$predict, test_data$classes)
cm_over
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       133         1
  malignant      4        71
                                          
               Accuracy : 0.9761          
                 95% CI : (0.9451, 0.9922)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.9475          
 Mcnemar's Test P-Value : 0.3711          
                                          
            Sensitivity : 0.9708          
            Specificity : 0.9861          
         Pos Pred Value : 0.9925          
         Neg Pred Value : 0.9467          
             Prevalence : 0.6555          
         Detection Rate : 0.6364          
   Detection Prevalence : 0.6411          
      Balanced Accuracy : 0.9785          
                                          
       'Positive' Class : benign          
                                          

ROSE

Besides over- and under-sampling, there are hybrid methods that combine under-sampling with the generation of additional data. Two of the most popular are ROSE and SMOTE.

From Nicola Lunardon, Giovanna Menardi and Nicola Torelli’s “ROSE: A Package for Binary Imbalanced Learning” (R Journal, 2014, Vol. 6 Issue 1, p. 79): “The ROSE package provides functions to deal with binary classification problems in the presence of imbalanced classes. Artificial balanced samples are generated according to a smoothed bootstrap approach and allow for aiding both the phases of estimation and accuracy evaluation of a binary classifier in the presence of a rare class. Functions that implement more traditional remedies for the class imbalance and different metrics to evaluate accuracy are also provided. These are estimated by holdout, bootstrap, or cross-validation methods.”

You implement them the same way as before, this time choosing sampling = "rose"

# install.packages('ROSE')
ctrl <- trainControl(method = "repeatedcv", 
                     number = 10, 
                     repeats = 10, 
                     verboseIter = FALSE,
                     sampling = "rose")
set.seed(42)
model_rf_rose <- caret::train(classes ~ .,
                              data = train_data,
                              method = "rf",
                              preProcess = c("scale", "center"),
                              trControl = ctrl)
Loaded ROSE 0.0-3
final_rose <- data.frame(actual = test_data$classes,
                         predict(model_rf_rose, newdata = test_data, type = "prob"))
final_rose$predict <- ifelse(final_rose$benign > 0.5, "benign", "malignant")
cm_rose <- confusionMatrix(final_rose$predict, test_data$classes)
cm_rose
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       132         2
  malignant      5        70
                                          
               Accuracy : 0.9665          
                 95% CI : (0.9322, 0.9864)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.9266          
 Mcnemar's Test P-Value : 0.4497          
                                          
            Sensitivity : 0.9635          
            Specificity : 0.9722          
         Pos Pred Value : 0.9851          
         Neg Pred Value : 0.9333          
             Prevalence : 0.6555          
         Detection Rate : 0.6316          
   Detection Prevalence : 0.6411          
      Balanced Accuracy : 0.9679          
                                          
       'Positive' Class : benign          
                                          

SMOTE

… or by choosing sampling = "smote" in the trainControl settings.

From Nitesh V. Chawla, Kevin W. Bowyer, Lawrence O. Hall and W. Philip Kegelmeyer’s “SMOTE: Synthetic Minority Over-sampling Technique” (Journal of Artificial Intelligence Research, 2002, Vol. 16, pp. 321–357): “This paper shows that a combination of our method of over-sampling the minority (abnormal) class and under-sampling the majority (normal) class can achieve better classifier performance (in ROC space) than only under-sampling the majority class. This paper also shows that a combination of our method of over-sampling the minority class and under-sampling the majority class can achieve better classifier performance (in ROC space) than varying the loss ratios in Ripper or class priors in Naive Bayes. Our method of over-sampling the minority class involves creating synthetic minority class examples.”

model_rf_smote <- caret::train(classes ~ .,
                              data = train_data,
                              method = "rf",
                              preProcess = c("scale", "center"),
                              trControl = ctrl)
final_smote <- data.frame(actual = test_data$classes,
                         predict(model_rf_smote, newdata = test_data, type = "prob"))
final_smote$predict <- ifelse(final_smote$benign > 0.5, "benign", "malignant")
cm_smote <- confusionMatrix(final_smote$predict, test_data$classes)
cm_smote
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       133         2
  malignant      4        70
                                          
               Accuracy : 0.9713          
                 95% CI : (0.9386, 0.9894)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.9369          
 Mcnemar's Test P-Value : 0.6831          
                                          
            Sensitivity : 0.9708          
            Specificity : 0.9722          
         Pos Pred Value : 0.9852          
         Neg Pred Value : 0.9459          
             Prevalence : 0.6555          
         Detection Rate : 0.6364          
   Detection Prevalence : 0.6459          
      Balanced Accuracy : 0.9715          
                                          
       'Positive' Class : benign          
                                          

Predictions

Now let’s compare the predictions of all these models:

library(dplyr)

Attaching package: ‘dplyr’

The following object is masked from ‘package:randomForest’:

    combine

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union
comparison <- data.frame(model = names(models),
                         Sensitivity = rep(NA, length(models)),
                         Specificity = rep(NA, length(models)),
                         Precision = rep(NA, length(models)),
                         Recall = rep(NA, length(models)),
                         F1 = rep(NA, length(models)))
for (name in names(models)) {
  model <- get(paste0("cm_", name))
  
  comparison[comparison$model == name, ] <- filter(comparison, model == name) %>%
    mutate(Sensitivity = model$byClass["Sensitivity"],
           Specificity = model$byClass["Specificity"],
           Precision = model$byClass["Precision"],
           Recall = model$byClass["Recall"],
           F1 = model$byClass["F1"])
}
library(tidyr)

Attaching package: ‘tidyr’

The following object is masked from ‘package:mice’:

    complete
comparison %>%
  gather(x, y, Sensitivity:F1) %>%
  ggplot(aes(x = x, y = y, color = model)) +
    geom_jitter(width = 0.2, alpha = 0.5, size = 3)

With this small dataset, we can already see how the different techniques can influence model performance. Sensitivity (or recall) describes the proportion of benign cases that have been predicted correctly, while specificity describes the proportion of malignant cases that have been predicted correctly. Precision describes the true positives, i.e. the proportion of benign predictions that were actual from benign samples. F1 is the weighted average of precision and sensitivity/ recall.

Here, all four methods improved specificity and precision compared to the original model. Under-sampling, over-sampling and ROSE additionally improved precision and the F1 score.

LS0tCnRpdGxlOiAiQnVpbGRpbmcgbWVhbmluZ2Z1bCBtYWNoaW5lIGxlYXJuaW5nIG1vZGVscyBmb3IgZGlzZWFzZSBwcmVkaWN0aW9uIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpGcm9tIHRoaXMgc2l0ZTogaHR0cHM6Ly9zaGlyaW5nLmdpdGh1Yi5pby9tYWNoaW5lX2xlYXJuaW5nLzIwMTcvMDMvMzEvd2ViaW5hcl9jb2RlCgojI1RoZSBkYXRhc2V0ClRoZSBkYXRhc2V0IEkgYW0gdXNpbmcgaW4gdGhlc2UgZXhhbXBsZSBhbmFseXNlcywgaXMgdGhlIEJyZWFzdCBDYW5jZXIgV2lzY29uc2luIChEaWFnbm9zdGljKSBEYXRhc2V0LiBUaGUgZGF0YSB3YXMgZG93bmxvYWRlZCBmcm9tIHRoZSBVQyBJcnZpbmUgTWFjaGluZSBMZWFybmluZyBSZXBvc2l0b3J5KGh0dHA6Ly9hcmNoaXZlLmljcy51Y2kuZWR1L21sL2RhdGFzZXRzL0JyZWFzdCtDYW5jZXIrV2lzY29uc2luKyUyOERpYWdub3N0aWMlMjkpLgoKVGhlIGZpcnN0IGRhdGFzZXQgbG9va3MgYXQgdGhlIHByZWRpY3RvciBjbGFzc2VzOgoKKiBtYWxpZ25hbnQgb3IKKiBiZW5pZ24gYnJlYXN0IG1hc3MuCgpUaGUgZmVhdHVyZXMgY2hhcmFjdGVyaXplIGNlbGwgbnVjbGV1cyBwcm9wZXJ0aWVzIGFuZCB3ZXJlIGdlbmVyYXRlZCBmcm9tIGltYWdlIGFuYWx5c2lzIG9mIGZpbmUgbmVlZGxlIGFzcGlyYXRlcyAoRk5BKSBvZiBicmVhc3QgbWFzc2VzOgoKKiBTYW1wbGUgSUQgKGNvZGUgbnVtYmVyKQoqIENsdW1wIHRoaWNrbmVzcwoqIFVuaWZvcm1pdHkgb2YgY2VsbCBzaXplCiogVW5pZm9ybWl0eSBvZiBjZWxsIHNoYXBlCiogTWFyZ2luYWwgYWRoZXNpb24KKiBTaW5nbGUgZXBpdGhlbGlhbCBjZWxsIHNpemUKKiBOdW1iZXIgb2YgYmFyZSBudWNsZWkKKiBCbGFuZCBjaHJvbWF0aW4KKiBOdW1iZXIgb2Ygbm9ybWFsIG51Y2xlaQoqIE1pdG9zaXMKKiBDbGFzc2VzLCBpLmUuIGRpYWdub3NpcwoKYGBge3J9CmJjX2RhdGEgPC0gcmVhZC50YWJsZSgiZGF0YS9icmVhc3QtY2FuY2VyLXdpc2NvbnNpbi5kYXRhIiwgCiAgICAgICAgICAgICAgICAgICAgICBoZWFkZXIgPSBGQUxTRSwgCiAgICAgICAgICAgICAgICAgICAgICBzZXAgPSAiLCIpCmNvbG5hbWVzKGJjX2RhdGEpIDwtIGMoInNhbXBsZV9jb2RlX251bWJlciIsIAogICAgICAgICAgICAgICAgICAgICAgICJjbHVtcF90aGlja25lc3MiLCAKICAgICAgICAgICAgICAgICAgICAgICAidW5pZm9ybWl0eV9vZl9jZWxsX3NpemUiLCAKICAgICAgICAgICAgICAgICAgICAgICAidW5pZm9ybWl0eV9vZl9jZWxsX3NoYXBlIiwgCiAgICAgICAgICAgICAgICAgICAgICAgIm1hcmdpbmFsX2FkaGVzaW9uIiwgCiAgICAgICAgICAgICAgICAgICAgICAgInNpbmdsZV9lcGl0aGVsaWFsX2NlbGxfc2l6ZSIsIAogICAgICAgICAgICAgICAgICAgICAgICJiYXJlX251Y2xlaSIsIAogICAgICAgICAgICAgICAgICAgICAgICJibGFuZF9jaHJvbWF0aW4iLCAKICAgICAgICAgICAgICAgICAgICAgICAibm9ybWFsX251Y2xlb2xpIiwgCiAgICAgICAgICAgICAgICAgICAgICAgIm1pdG9zaXMiLCAKICAgICAgICAgICAgICAgICAgICAgICAiY2xhc3NlcyIpCgpiY19kYXRhJGNsYXNzZXMgPC0gaWZlbHNlKGJjX2RhdGEkY2xhc3NlcyA9PSAiMiIsICJiZW5pZ24iLAogICAgICAgICAgICAgICAgICAgICAgICAgIGlmZWxzZShiY19kYXRhJGNsYXNzZXMgPT0gIjQiLCAibWFsaWduYW50IiwgTkEpKQoKaGVhZChiY19kYXRhKQpgYGAKCiMjIEhhbmRsaW5nIG1pc3NpbmcgZGF0YQoKYGBge3J9CmJjX2RhdGFbYmNfZGF0YSA9PSAiPyJdIDwtIE5BCgojIGhvdyBtYW55IE5BcyBhcmUgaW4gdGhlIGRhdGEKbGVuZ3RoKHdoaWNoKGlzLm5hKGJjX2RhdGEpKSkKIyBob3cgbWFueSBzYW1wbGVzIHdvdWxkIHdlIGxvb3NlLCBpZiB3ZSByZW1vdmVkIHRoZW0/Cm5yb3coYmNfZGF0YSkKbnJvdyhiY19kYXRhW2lzLm5hKGJjX2RhdGEpLCBdKQpgYGAKCk1pc3NpbmcgdmFsdWVzIGFyZSBpbXB1dGVkIHdpdGggdGhlIG1pY2UgcGFja2FnZS4KYGBge3J9CiMgaW1wdXRlIG1pc3NpbmcgZGF0YQpsaWJyYXJ5KG1pY2UpCgpiY19kYXRhWywyOjEwXSA8LSBhcHBseShiY19kYXRhWywgMjoxMF0sIDIsIGZ1bmN0aW9uKHgpIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHgpKSkKZGF0YXNldF9pbXB1dGUgPC0gbWljZShiY19kYXRhWywgMjoxMF0sICBwcmludCA9IEZBTFNFKQpiY19kYXRhIDwtIGNiaW5kKGJjX2RhdGFbLCAxMSwgZHJvcCA9IEZBTFNFXSwgbWljZTo6Y29tcGxldGUoZGF0YXNldF9pbXB1dGUsIDEpKQoKYmNfZGF0YSRjbGFzc2VzIDwtIGFzLmZhY3RvcihiY19kYXRhJGNsYXNzZXMpCgojIGhvdyBtYW55IGJlbmlnbiBhbmQgbWFsaWduYW50IGNhc2VzIGFyZSB0aGVyZT8Kc3VtbWFyeShiY19kYXRhJGNsYXNzZXMpCmBgYAoKIyMgRGF0YSBleHBsb3JhdGlvbgpgYGB7cn0KIyBpbXB1dGUgbWlzc2luZyBkYXRhCmxpYnJhcnkoZ2dwbG90MikKCmdncGxvdChiY19kYXRhLCBhZXMoeCA9IGNsYXNzZXMsIGZpbGwgPSBjbGFzc2VzKSkgKwogIGdlb21fYmFyKCkKCgpgYGAKCldlIGNhbiBzZWUgdGhhdCBvdXIgZGF0YSBpcyB1bmJhbGFuY2VkLiAKCiMjIFdoeSBpcyB1bmJhbGFuY2VkIGRhdGEgYSBwcm9ibGVtIGluIG1hY2hpbmUgbGVhcm5pbmc/Ck1vc3QgbWFjaGluZSBsZWFybmluZyBjbGFzc2lmaWNhdGlvbiBhbGdvcml0aG1zIGFyZSBzZW5zaXRpdmUgdG8gdW5iYWxhbmNlIGluIHRoZSBwcmVkaWN0b3IgY2xhc3Nlcy4gTGV04oCZcyBjb25zaWRlciBhbiBldmVuIG1vcmUgZXh0cmVtZSBleGFtcGxlIHRoYW4gb3VyIGJyZWFzdCBjYW5jZXIgZGF0YXNldDogYXNzdW1lIHdlIGhhZCAxMCBtYWxpZ25hbnQgdnMgOTAgYmVuaWduIHNhbXBsZXMuIEEgbWFjaGluZSBsZWFybmluZyBtb2RlbCB0aGF0IGhhcyBiZWVuIHRyYWluZWQgYW5kIHRlc3RlZCBvbiBzdWNoIGEgZGF0YXNldCBjb3VsZCBub3cgcHJlZGljdCDigJxiZW5pZ27igJ0gZm9yIGFsbCBzYW1wbGVzIGFuZCBzdGlsbCBnYWluIGEgdmVyeSBoaWdoIGFjY3VyYWN5LiBBbiB1bmJhbGFuY2VkIGRhdGFzZXQgd2lsbCBiaWFzIHRoZSBwcmVkaWN0aW9uIG1vZGVsIHRvd2FyZHMgdGhlIG1vcmUgY29tbW9uIGNsYXNzIQoKIyMgSG93IHRvIGJhbGFuY2UgZGF0YSBmb3IgbW9kZWxpbmcKVGhlIGJhc2ljIHRoZW9yZXRpY2FsIGNvbmNlcHRzIGJlaGluZCBvdmVyLSBhbmQgdW5kZXItc2FtcGxpbmcgYXJlIHZlcnkgc2ltcGxlOgoKKiBXaXRoIHVuZGVyLXNhbXBsaW5nLCB3ZSByYW5kb21seSBzZWxlY3QgYSBzdWJzZXQgb2Ygc2FtcGxlcyBmcm9tIHRoZSBjbGFzcyB3aXRoIG1vcmUgaW5zdGFuY2VzIHRvIG1hdGNoIHRoZSBudW1iZXIgb2Ygc2FtcGxlcyBjb21pbmcgZnJvbSBlYWNoIGNsYXNzLiBJbiBvdXIgZXhhbXBsZSwgd2Ugd291bGQgcmFuZG9tbHkgcGljayAyNDEgb3V0IG9mIHRoZSA0NTggYmVuaWduIGNhc2VzLiBUaGUgbWFpbiBkaXNhZHZhbnRhZ2Ugb2YgdW5kZXItc2FtcGxpbmcgaXMgdGhhdCB3ZSBsb29zZSBwb3RlbnRpYWxseSByZWxldmFudCBpbmZvcm1hdGlvbiBmcm9tIHRoZSBsZWZ0LW91dCBzYW1wbGVzLgoKKiBXaXRoIG92ZXJzYW1wbGluZywgd2UgcmFuZG9tbHkgZHVwbGljYXRlIHNhbXBsZXMgZnJvbSB0aGUgY2xhc3Mgd2l0aCBmZXdlciBpbnN0YW5jZXMgb3Igd2UgZ2VuZXJhdGUgYWRkaXRpb25hbCBpbnN0YW5jZXMgYmFzZWQgb24gdGhlIGRhdGEgdGhhdCB3ZSBoYXZlLCBzbyBhcyB0byBtYXRjaCB0aGUgbnVtYmVyIG9mIHNhbXBsZXMgaW4gZWFjaCBjbGFzcy4gV2hpbGUgd2UgYXZvaWQgbG9vc2luZyBpbmZvcm1hdGlvbiB3aXRoIHRoaXMgYXBwcm9hY2gsIHdlIGFsc28gcnVuIHRoZSByaXNrIG9mIG92ZXJmaXR0aW5nIG91ciBtb2RlbCBhcyB3ZSBhcmUgbW9yZSBsaWtlbHkgdG8gZ2V0IHRoZSBzYW1lIHNhbXBsZXMgaW4gdGhlIHRyYWluaW5nIGFuZCBpbiB0aGUgdGVzdCBkYXRhLCBpLmUuIHRoZSB0ZXN0IGRhdGEgaXMgbm8gbG9uZ2VyIGluZGVwZW5kZW50IGZyb20gdHJhaW5pbmcgZGF0YS4gVGhpcyB3b3VsZCBsZWFkIHRvIGFuIG92ZXJlc3RpbWF0aW9uIG9mIG91ciBtb2RlbOKAmXMgcGVyZm9ybWFuY2UgYW5kIGdlbmVyYWxpemFiaWxpdHkuCgpJbiByZWFsaXR5IHRob3VnaCwgd2Ugc2hvdWxkIG5vdCBzaW1wbHkgcGVyZm9ybSBvdmVyLSBvciB1bmRlci1zYW1wbGluZyBvbiBvdXIgdHJhaW5pbmcgZGF0YSBhbmQgdGhlbiBydW4gdGhlIG1vZGVsLiBXZSBuZWVkIHRvIGFjY291bnQgZm9yIGNyb3NzLXZhbGlkYXRpb24gYW5kIHBlcmZvcm0gb3Zlci0gb3IgdW5kZXItc2FtcGxpbmcgb24gZWFjaCBmb2xkIGluZGVwZW5kZW50bHkgdG8gZ2V0IGFuIGhvbmVzdCBlc3RpbWF0ZSBvZiBtb2RlbCBwZXJmb3JtYW5jZSEKCiMjIyBNb2RlbGluZyB0aGUgb3JpZ2luYWwgdW5iYWxhbmNlZCBkYXRhIChleGFtcGxlKQpJIHJhbmRvbWx5IGRpdmlkZSB0aGUgZGF0YSBpbnRvIHRyYWluaW5nIGFuZCB0ZXN0IHNldHMgKHN0cmF0aWZpZWQgYnkgY2xhc3MpIGFuZCBwZXJmb3JtIFJhbmRvbSBGb3Jlc3QgbW9kZWxpbmcgd2l0aCAxMCB4IDEwIHJlcGVhdGVkIGNyb3NzLXZhbGlkYXRpb24uIEZpbmFsIG1vZGVsIHBlcmZvcm1hbmNlIGlzIHRoZW4gbWVhc3VyZWQgb24gdGhlIHRlc3Qgc2V0LgpgYGB7cn0KbGlicmFyeShjYXJldCkKc2V0LnNlZWQoNDIpCmluZGV4IDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oYmNfZGF0YSRjbGFzc2VzLCBwID0gMC43LCBsaXN0ID0gRkFMU0UpCnRyYWluX2RhdGEgPC0gYmNfZGF0YVtpbmRleCwgXQp0ZXN0X2RhdGEgIDwtIGJjX2RhdGFbLWluZGV4LCBdCgojIyMjIyMjIyMjIyMjIyMjIyMjIyMgVHJhaW5pbmcgbW9kZWwKbW9kZWxfcmYgPC0gY2FyZXQ6OnRyYWluKGNsYXNzZXMgfiAuLAogICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluX2RhdGEsCiAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAicmYiLAogICAgICAgICAgICAgICAgICAgICAgICAgcHJlUHJvY2VzcyA9IGMoInNjYWxlIiwgImNlbnRlciIpLAogICAgICAgICAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gMTAsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSAxMCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZUl0ZXIgPSBGQUxTRSkpCgpwcmludChtb2RlbF9yZikKCiMjIyMjIyMjIyMjIyMjIyMjIyMjIwpmaW5hbCA8LSBkYXRhLmZyYW1lKGFjdHVhbCA9IHRlc3RfZGF0YSRjbGFzc2VzLAogICAgICAgICAgICAgICAgICAgIHByZWRpY3QobW9kZWxfcmYsIG5ld2RhdGEgPSB0ZXN0X2RhdGEsIHR5cGUgPSAicHJvYiIpKQpmaW5hbCRwcmVkaWN0IDwtIGlmZWxzZShmaW5hbCRiZW5pZ24gPiAwLjUsICJiZW5pZ24iLCAibWFsaWduYW50IikKY21fb3JpZ2luYWwgPC0gY29uZnVzaW9uTWF0cml4KGZpbmFsJHByZWRpY3QsIHRlc3RfZGF0YSRjbGFzc2VzKQpjbV9vcmlnaW5hbApgYGAKCiMjIyBVbmRlci1zYW1wbGluZwpMdWNraWx5LCBgY2FyZXRgIG1ha2VzIGl0IHZlcnkgZWFzeSB0byBpbmNvcnBvcmF0ZSBvdmVyLSBhbmQgdW5kZXItc2FtcGxpbmcgdGVjaG5pcXVlcyB3aXRoIGNyb3NzLXZhbGlkYXRpb24gcmVzYW1wbGluZy4gV2UgY2FuIHNpbXBseSBhZGQgdGhlIGBzYW1wbGluZ2Agb3B0aW9uIHRvIG91ciBgdHJhaW5Db250cm9sYCBhbmQgY2hvb3NlIGBkb3duYCBmb3IgdW5kZXItIChhbHNvIGNhbGxlZCBkb3duLSkgc2FtcGxpbmcuIFRoZSByZXN0IHN0YXlzIHRoZSBzYW1lIGFzIHdpdGggb3VyIG9yaWdpbmFsIG1vZGVsLgpgYGB7cn0KY3RybCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLCAKICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gMTAsIAogICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gMTAsIAogICAgICAgICAgICAgICAgICAgICB2ZXJib3NlSXRlciA9IEZBTFNFLAogICAgICAgICAgICAgICAgICAgICBzYW1wbGluZyA9ICJkb3duIikKCnNldC5zZWVkKDQyKQptb2RlbF9yZl91bmRlciA8LSBjYXJldDo6dHJhaW4oY2xhc3NlcyB+IC4sCiAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW5fZGF0YSwKICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJyZiIsCiAgICAgICAgICAgICAgICAgICAgICAgICBwcmVQcm9jZXNzID0gYygic2NhbGUiLCAiY2VudGVyIiksCiAgICAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjdHJsKQoKZmluYWxfdW5kZXIgPC0gZGF0YS5mcmFtZShhY3R1YWwgPSB0ZXN0X2RhdGEkY2xhc3NlcywKICAgICAgICAgICAgICAgICAgICBwcmVkaWN0KG1vZGVsX3JmX3VuZGVyLCBuZXdkYXRhID0gdGVzdF9kYXRhLCB0eXBlID0gInByb2IiKSkKZmluYWxfdW5kZXIkcHJlZGljdCA8LSBpZmVsc2UoZmluYWxfdW5kZXIkYmVuaWduID4gMC41LCAiYmVuaWduIiwgIm1hbGlnbmFudCIpCgpjbV91bmRlciA8LSBjb25mdXNpb25NYXRyaXgoZmluYWxfdW5kZXIkcHJlZGljdCwgdGVzdF9kYXRhJGNsYXNzZXMpCgpjbV91bmRlcgpgYGAKIyMjIE92ZXJzYW1wbGluZwpGb3Igb3Zlci0gKGFsc28gY2FsbGVkIHVwLSkgc2FtcGxpbmcgd2Ugc2ltcGx5IHNwZWNpZnkgYHNhbXBsaW5nID0gInVwImAuCmBgYHtyfQpjdHJsIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIAogICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSAxMCwgCiAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSAxMCwgCiAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2VJdGVyID0gRkFMU0UsCiAgICAgICAgICAgICAgICAgICAgIHNhbXBsaW5nID0gInVwIikKCnNldC5zZWVkKDQyKQptb2RlbF9yZl9vdmVyIDwtIGNhcmV0Ojp0cmFpbihjbGFzc2VzIH4gLiwKICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSB0cmFpbl9kYXRhLAogICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJmIiwKICAgICAgICAgICAgICAgICAgICAgICAgIHByZVByb2Nlc3MgPSBjKCJzY2FsZSIsICJjZW50ZXIiKSwKICAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGN0cmwpCgpmaW5hbF9vdmVyIDwtIGRhdGEuZnJhbWUoYWN0dWFsID0gdGVzdF9kYXRhJGNsYXNzZXMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgcHJlZGljdChtb2RlbF9yZl9vdmVyLCBuZXdkYXRhID0gdGVzdF9kYXRhLCB0eXBlID0gInByb2IiKSkKZmluYWxfb3ZlciRwcmVkaWN0IDwtIGlmZWxzZShmaW5hbF9vdmVyJGJlbmlnbiA+IDAuNSwgImJlbmlnbiIsICJtYWxpZ25hbnQiKQoKY21fb3ZlciA8LSBjb25mdXNpb25NYXRyaXgoZmluYWxfb3ZlciRwcmVkaWN0LCB0ZXN0X2RhdGEkY2xhc3NlcykKCmNtX292ZXIKYGBgCgojIyMgUk9TRQpCZXNpZGVzIG92ZXItIGFuZCB1bmRlci1zYW1wbGluZywgdGhlcmUgYXJlIGh5YnJpZCBtZXRob2RzIHRoYXQgY29tYmluZSB1bmRlci1zYW1wbGluZyB3aXRoIHRoZSBnZW5lcmF0aW9uIG9mIGFkZGl0aW9uYWwgZGF0YS4gVHdvIG9mIHRoZSBtb3N0IHBvcHVsYXIgYXJlIFJPU0UgYW5kIFNNT1RFLgoKRnJvbSBOaWNvbGEgTHVuYXJkb24sIEdpb3Zhbm5hIE1lbmFyZGkgYW5kIE5pY29sYSBUb3JlbGxp4oCZcyDigJxST1NFOiBBIFBhY2thZ2UgZm9yIEJpbmFyeSBJbWJhbGFuY2VkIExlYXJuaW5n4oCdIChSIEpvdXJuYWwsIDIwMTQsIFZvbC4gNiBJc3N1ZSAxLCBwLiA3OSk6IOKAnFRoZSBST1NFIHBhY2thZ2UgcHJvdmlkZXMgZnVuY3Rpb25zIHRvIGRlYWwgd2l0aCBiaW5hcnkgY2xhc3NpZmljYXRpb24gcHJvYmxlbXMgaW4gdGhlIHByZXNlbmNlIG9mIGltYmFsYW5jZWQgY2xhc3Nlcy4gQXJ0aWZpY2lhbCBiYWxhbmNlZCBzYW1wbGVzIGFyZSBnZW5lcmF0ZWQgYWNjb3JkaW5nIHRvIGEgc21vb3RoZWQgYm9vdHN0cmFwIGFwcHJvYWNoIGFuZCBhbGxvdyBmb3IgYWlkaW5nIGJvdGggdGhlIHBoYXNlcyBvZiBlc3RpbWF0aW9uIGFuZCBhY2N1cmFjeSBldmFsdWF0aW9uIG9mIGEgYmluYXJ5IGNsYXNzaWZpZXIgaW4gdGhlIHByZXNlbmNlIG9mIGEgcmFyZSBjbGFzcy4gRnVuY3Rpb25zIHRoYXQgaW1wbGVtZW50IG1vcmUgdHJhZGl0aW9uYWwgcmVtZWRpZXMgZm9yIHRoZSBjbGFzcyBpbWJhbGFuY2UgYW5kIGRpZmZlcmVudCBtZXRyaWNzIHRvIGV2YWx1YXRlIGFjY3VyYWN5IGFyZSBhbHNvIHByb3ZpZGVkLiBUaGVzZSBhcmUgZXN0aW1hdGVkIGJ5IGhvbGRvdXQsIGJvb3RzdHJhcCwgb3IgY3Jvc3MtdmFsaWRhdGlvbiBtZXRob2RzLuKAnQoKWW91IGltcGxlbWVudCB0aGVtIHRoZSBzYW1lIHdheSBhcyBiZWZvcmUsIHRoaXMgdGltZSBjaG9vc2luZyBgc2FtcGxpbmcgPSAicm9zZSJg4oCmCmBgYHtyfQojIGluc3RhbGwucGFja2FnZXMoJ1JPU0UnKQpjdHJsIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIAogICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSAxMCwgCiAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSAxMCwgCiAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2VJdGVyID0gRkFMU0UsCiAgICAgICAgICAgICAgICAgICAgIHNhbXBsaW5nID0gInJvc2UiKQoKc2V0LnNlZWQoNDIpCm1vZGVsX3JmX3Jvc2UgPC0gY2FyZXQ6OnRyYWluKGNsYXNzZXMgfiAuLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW5fZGF0YSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJmIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJlUHJvY2VzcyA9IGMoInNjYWxlIiwgImNlbnRlciIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjdHJsKQoKZmluYWxfcm9zZSA8LSBkYXRhLmZyYW1lKGFjdHVhbCA9IHRlc3RfZGF0YSRjbGFzc2VzLAogICAgICAgICAgICAgICAgICAgICAgICAgcHJlZGljdChtb2RlbF9yZl9yb3NlLCBuZXdkYXRhID0gdGVzdF9kYXRhLCB0eXBlID0gInByb2IiKSkKZmluYWxfcm9zZSRwcmVkaWN0IDwtIGlmZWxzZShmaW5hbF9yb3NlJGJlbmlnbiA+IDAuNSwgImJlbmlnbiIsICJtYWxpZ25hbnQiKQoKY21fcm9zZSA8LSBjb25mdXNpb25NYXRyaXgoZmluYWxfcm9zZSRwcmVkaWN0LCB0ZXN0X2RhdGEkY2xhc3NlcykKCmNtX3Jvc2UKYGBgCgojIyMgU01PVEUKCuKApiBvciBieSBjaG9vc2luZyBgc2FtcGxpbmcgPSAic21vdGUiYCBpbiB0aGUgYHRyYWluQ29udHJvbGAgc2V0dGluZ3MuCgpGcm9tIE5pdGVzaCBWLiBDaGF3bGEsIEtldmluIFcuIEJvd3llciwgTGF3cmVuY2UgTy4gSGFsbCBhbmQgVy4gUGhpbGlwIEtlZ2VsbWV5ZXLigJlzIOKAnFNNT1RFOiBTeW50aGV0aWMgTWlub3JpdHkgT3Zlci1zYW1wbGluZyBUZWNobmlxdWXigJ0gKEpvdXJuYWwgb2YgQXJ0aWZpY2lhbCBJbnRlbGxpZ2VuY2UgUmVzZWFyY2gsIDIwMDIsIFZvbC4gMTYsIHBwLiAzMjHigJMzNTcpOiDigJxUaGlzIHBhcGVyIHNob3dzIHRoYXQgYSBjb21iaW5hdGlvbiBvZiBvdXIgbWV0aG9kIG9mIG92ZXItc2FtcGxpbmcgdGhlIG1pbm9yaXR5IChhYm5vcm1hbCkgY2xhc3MgYW5kIHVuZGVyLXNhbXBsaW5nIHRoZSBtYWpvcml0eSAobm9ybWFsKSBjbGFzcyBjYW4gYWNoaWV2ZSBiZXR0ZXIgY2xhc3NpZmllciBwZXJmb3JtYW5jZSAoaW4gUk9DIHNwYWNlKSB0aGFuIG9ubHkgdW5kZXItc2FtcGxpbmcgdGhlIG1ham9yaXR5IGNsYXNzLiBUaGlzIHBhcGVyIGFsc28gc2hvd3MgdGhhdCBhIGNvbWJpbmF0aW9uIG9mIG91ciBtZXRob2Qgb2Ygb3Zlci1zYW1wbGluZyB0aGUgbWlub3JpdHkgY2xhc3MgYW5kIHVuZGVyLXNhbXBsaW5nIHRoZSBtYWpvcml0eSBjbGFzcyBjYW4gYWNoaWV2ZSBiZXR0ZXIgY2xhc3NpZmllciBwZXJmb3JtYW5jZSAoaW4gUk9DIHNwYWNlKSB0aGFuIHZhcnlpbmcgdGhlIGxvc3MgcmF0aW9zIGluIFJpcHBlciBvciBjbGFzcyBwcmlvcnMgaW4gTmFpdmUgQmF5ZXMuIE91ciBtZXRob2Qgb2Ygb3Zlci1zYW1wbGluZyB0aGUgbWlub3JpdHkgY2xhc3MgaW52b2x2ZXMgY3JlYXRpbmcgc3ludGhldGljIG1pbm9yaXR5IGNsYXNzIGV4YW1wbGVzLuKAnQpgYGB7cn0KIyBpbnN0YWxsLnBhY2thZ2VzKCJETXdSIikKY3RybCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLCAKICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gMTAsIAogICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gMTAsIAogICAgICAgICAgICAgICAgICAgICB2ZXJib3NlSXRlciA9IEZBTFNFLAogICAgICAgICAgICAgICAgICAgICBzYW1wbGluZyA9ICJzbW90ZSIpCgpzZXQuc2VlZCg0MikKbW9kZWxfcmZfc21vdGUgPC0gY2FyZXQ6OnRyYWluKGNsYXNzZXMgfiAuLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW5fZGF0YSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJmIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJlUHJvY2VzcyA9IGMoInNjYWxlIiwgImNlbnRlciIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjdHJsKQoKZmluYWxfc21vdGUgPC0gZGF0YS5mcmFtZShhY3R1YWwgPSB0ZXN0X2RhdGEkY2xhc3NlcywKICAgICAgICAgICAgICAgICAgICAgICAgIHByZWRpY3QobW9kZWxfcmZfc21vdGUsIG5ld2RhdGEgPSB0ZXN0X2RhdGEsIHR5cGUgPSAicHJvYiIpKQpmaW5hbF9zbW90ZSRwcmVkaWN0IDwtIGlmZWxzZShmaW5hbF9zbW90ZSRiZW5pZ24gPiAwLjUsICJiZW5pZ24iLCAibWFsaWduYW50IikKCmNtX3Ntb3RlIDwtIGNvbmZ1c2lvbk1hdHJpeChmaW5hbF9zbW90ZSRwcmVkaWN0LCB0ZXN0X2RhdGEkY2xhc3NlcykKY21fc21vdGUKYGBgCgojIyMgUHJlZGljdGlvbnMKTm93IGxldOKAmXMgY29tcGFyZSB0aGUgcHJlZGljdGlvbnMgb2YgYWxsIHRoZXNlIG1vZGVsczoKYGBge3J9Cm1vZGVscyA8LSBsaXN0KG9yaWdpbmFsID0gbW9kZWxfcmYsCiAgICAgICAgICAgICAgICAgICAgICAgdW5kZXIgPSBtb2RlbF9yZl91bmRlciwKICAgICAgICAgICAgICAgICAgICAgICBvdmVyID0gbW9kZWxfcmZfb3ZlciwKICAgICAgICAgICAgICAgICAgICAgICBzbW90ZSA9IG1vZGVsX3JmX3Ntb3RlLAogICAgICAgICAgICAgICAgICAgICAgIHJvc2UgPSBtb2RlbF9yZl9yb3NlKQoKcmVzYW1wbGluZyA8LSByZXNhbXBsZXMobW9kZWxzKQpid3Bsb3QocmVzYW1wbGluZykKYGBgCgpgYGB7cn0KbGlicmFyeShkcGx5cikKY29tcGFyaXNvbiA8LSBkYXRhLmZyYW1lKG1vZGVsID0gbmFtZXMobW9kZWxzKSwKICAgICAgICAgICAgICAgICAgICAgICAgIFNlbnNpdGl2aXR5ID0gcmVwKE5BLCBsZW5ndGgobW9kZWxzKSksCiAgICAgICAgICAgICAgICAgICAgICAgICBTcGVjaWZpY2l0eSA9IHJlcChOQSwgbGVuZ3RoKG1vZGVscykpLAogICAgICAgICAgICAgICAgICAgICAgICAgUHJlY2lzaW9uID0gcmVwKE5BLCBsZW5ndGgobW9kZWxzKSksCiAgICAgICAgICAgICAgICAgICAgICAgICBSZWNhbGwgPSByZXAoTkEsIGxlbmd0aChtb2RlbHMpKSwKICAgICAgICAgICAgICAgICAgICAgICAgIEYxID0gcmVwKE5BLCBsZW5ndGgobW9kZWxzKSkpCgpmb3IgKG5hbWUgaW4gbmFtZXMobW9kZWxzKSkgewogIG1vZGVsIDwtIGdldChwYXN0ZTAoImNtXyIsIG5hbWUpKQogIAogIGNvbXBhcmlzb25bY29tcGFyaXNvbiRtb2RlbCA9PSBuYW1lLCBdIDwtIGZpbHRlcihjb21wYXJpc29uLCBtb2RlbCA9PSBuYW1lKSAlPiUKICAgIG11dGF0ZShTZW5zaXRpdml0eSA9IG1vZGVsJGJ5Q2xhc3NbIlNlbnNpdGl2aXR5Il0sCiAgICAgICAgICAgU3BlY2lmaWNpdHkgPSBtb2RlbCRieUNsYXNzWyJTcGVjaWZpY2l0eSJdLAogICAgICAgICAgIFByZWNpc2lvbiA9IG1vZGVsJGJ5Q2xhc3NbIlByZWNpc2lvbiJdLAogICAgICAgICAgIFJlY2FsbCA9IG1vZGVsJGJ5Q2xhc3NbIlJlY2FsbCJdLAogICAgICAgICAgIEYxID0gbW9kZWwkYnlDbGFzc1siRjEiXSkKfQoKbGlicmFyeSh0aWR5cikKY29tcGFyaXNvbiAlPiUKICBnYXRoZXIoeCwgeSwgU2Vuc2l0aXZpdHk6RjEpICU+JQogIGdncGxvdChhZXMoeCA9IHgsIHkgPSB5LCBjb2xvciA9IG1vZGVsKSkgKwogICAgZ2VvbV9qaXR0ZXIod2lkdGggPSAwLjIsIGFscGhhID0gMC41LCBzaXplID0gMykKYGBgCldpdGggdGhpcyBzbWFsbCBkYXRhc2V0LCB3ZSBjYW4gYWxyZWFkeSBzZWUgaG93IHRoZSBkaWZmZXJlbnQgdGVjaG5pcXVlcyBjYW4gaW5mbHVlbmNlIG1vZGVsIHBlcmZvcm1hbmNlLiBTZW5zaXRpdml0eSAob3IgcmVjYWxsKSBkZXNjcmliZXMgdGhlIHByb3BvcnRpb24gb2YgYmVuaWduIGNhc2VzIHRoYXQgaGF2ZSBiZWVuIHByZWRpY3RlZCBjb3JyZWN0bHksIHdoaWxlIHNwZWNpZmljaXR5IGRlc2NyaWJlcyB0aGUgcHJvcG9ydGlvbiBvZiBtYWxpZ25hbnQgY2FzZXMgdGhhdCBoYXZlIGJlZW4gcHJlZGljdGVkIGNvcnJlY3RseS4gUHJlY2lzaW9uIGRlc2NyaWJlcyB0aGUgdHJ1ZSBwb3NpdGl2ZXMsIGkuZS4gdGhlIHByb3BvcnRpb24gb2YgYmVuaWduIHByZWRpY3Rpb25zIHRoYXQgd2VyZSBhY3R1YWwgZnJvbSBiZW5pZ24gc2FtcGxlcy4gRjEgaXMgdGhlIHdlaWdodGVkIGF2ZXJhZ2Ugb2YgcHJlY2lzaW9uIGFuZCBzZW5zaXRpdml0eS8gcmVjYWxsLgoKSGVyZSwgYWxsIGZvdXIgbWV0aG9kcyBpbXByb3ZlZCBzcGVjaWZpY2l0eSBhbmQgcHJlY2lzaW9uIGNvbXBhcmVkIHRvIHRoZSBvcmlnaW5hbCBtb2RlbC4gVW5kZXItc2FtcGxpbmcsIG92ZXItc2FtcGxpbmcgYW5kIFJPU0UgYWRkaXRpb25hbGx5IGltcHJvdmVkIHByZWNpc2lvbiBhbmQgdGhlIEYxIHNjb3JlLgoK