Abstract

This is a demonstration of Dirk Schumacher’s ompr package (https://dirkschumacher.github.io/ompr/). We will exercise it on the MIP discriminant model from an old paper of mine [1], applied to the Anderson’s iris data. Specifically, we’ll try to discriminate between each individual species (setosa, versicolor or virginica) and the other two, using a linear discriminant function that minimizes the number of misclassifications on the overall sample.

For general amusement, we’ll also look at how a support vector machine does on the same three classification problems. This is not intended to be a test of MIP classification v. other methods, though, and we won’t get into separate training and testing sample or k-fold cross-validation. The focus of this notebook is to exercise the ompr package (using CPLEX 12.6.3 as the solver) on a (somewhat) real-world statistical optimization problem.

Before starting, I’ll just note one minor glitch. As of this writing, you cannot name a variable “c” in your optimization model. Dirk has been very responsive in checking into issues with ompr, but this one issue traces back to a library used by ompr, so while Dirk has reported it, he cannot fix it himself. So far, “c” seems to be the only variable name that causes any trouble.

About this document

This document is an the HTML output from an R notebook. The control in the upper right will let you show or hide code (although, in this case, the code is pretty much the point of the notebook). It will also allow you to extract and save the notebook (R source file) itself.

Solver

As mentioned, the solver used here is CPLEX 12.6.3. One of the virtues of ompr (and the ROI package it depends on) is that, armed with appropriate plug-ins, you can switch to alternative solvers. The connection to CPLEX is handled by the ROI.plugin.cplex package, loaded below. What is not immediately visible is that, in addition to CPLEX itself, this package also depends on the Rcplex library.

Preparation

We will use the 1-norm of the observations to set some model parameters, so we start by defining that.

norm.1 <- function(x) sum(abs(x))

We will arbitrarily set a minimum absolute discriminant value (delta) of 0.01 for a correct classification (so that discriminant values between -0.01 and +0.01 will be considered inconclusive).

delta <- 0.01

Before creating any models, we need to load the required libraries.

library(ROI)               # general solver interface
ROI.plugin.cplex: R Optimization Infrastructure
Registered solver plugins: nlminb, cplex.
Default solver: auto.
library(ROI.plugin.cplex)  # connects the ROI package to CPLEX
library(ompr)              # MIP modeling package
library(ompr.roi)          # connects OMPR to ROI
library(magrittr)          # for the pipe operator
library(kernlab)           # for SVMs

Function definitions

Partitioning the data into positive/negative samples

We will be partitioning the original data set into positive and negative samples more than once, so we create a function to do that. The function takes as its sole argument the name (“pos”) of the species in the positive sample and returns a named list of samples. The (now redundant) “Species” column is removed from each subsample.

partition <- function(pos) {
  list(positive = subset(iris, subset = (Species == pos), select = -Species),
       negative = subset(iris, subset = (Species != pos), select = -Species)
  )
}

Building and running MIP discriminant models

Since we want to generate several different discriminant functions, we will create a function that takes our list of subsamples, uses it to generate a MIP model, solves the model and returns the result in a list.

Variable names generally conform to the 1990 paper. The variables in the model have “.var” appended to distinguish them from the R variables containing their values (e.g., w.var is the discriminant coefficient vector in the model, while w is the R variable receiving the values of w.var in the solution).

mipDiscriminant <- function(data) {
  positive <- data$positive
  negative <- data$negative
  #
  # We need the maximum 1-norm of any observation in each sample.
  #
  Delta1 <- max(apply(negative, 1, norm.1))
  Delta2 <- max(apply(positive, 1, norm.1))
  Deltabar <- (Delta1 + Delta2) / 2
  #
  # Those values are used to compute a "big M" coefficient for each sample.
  #
  M1 <- 2 * Delta1 + Deltabar  # big M for negative observations
  M2 <- 2 * Delta2 + Deltabar  # big M for positive observations
  #
  # We also need a coefficient for a term in the objective function
  # that rewards greater separation between the two groups.
  #
  epsilon <- 1 / (2 * Deltabar)
  #
  # Next, we compute the dimensions of the data.
  #
  npos <- nrow(positive)  # size of positive sample
  nneg <- nrow(negative)  # size of negative sample
  nvar <- ncol(positive)  # number of features
  #
  # Now we can build the model.
  #
  model <-
    MIPModel() %>% 
    add_variable(y.var[i], i = 1:nneg, type = "binary") %>%   # indicators for negative misclassifications
    add_variable(z.var[i], i = 1:npos, type = "binary") %>%   # indicators for positive misclassifications
    add_variable(w.var[i], i = 1:nvar, type = "continuous", lb = -1, ub = 1) %>%  # coefficients of discriminant function
    add_variable(c.var, type = "continuous") %>%   # constant term of discriminant function (unbounded)
    add_variable(d.var, type = "continuous", lb = delta) %>%   # minimum absolute score of any successfully classified observation (bounded away from zero)
    set_objective(sum_expr(y.var[i], i = 1:nneg) + sum_expr(z.var[i], i = 1:npos) - epsilon * d.var, sense = "min") %>%   # minimize number of misclassifications with credit for higher spacing from the classification boundary
    add_constraint(sum_expr(negative[i, j] * w.var[j], j = 1:nvar) + c.var + d.var - M1 * y.var[i] <= 0, i = 1:nneg) %>%   # determine negative misclassifications
    add_constraint(sum_expr(positive[i, j] * w.var[j], j = 1:nvar) + c.var - d.var + M2 * z.var[i] >= 0, i = 1:npos)  # determine positive misclassifications
  #
  # Finally, we solve the model and return the result.
  #
  modelObject <<- as_ROI_model(model)
  model %>% solve_model(with_ROI(solver = "cplex"))
}

Creating discriminant functions

We will also want a function that takes the solution to our MIP model and turns it into a linear discriminant function, to facilitate testing the function.

extractFunction <- function(mipResult) {
  a <- (mipResult %>% get_solution(w.var[j]))[, "value"]
  a0 <- mipResult %>% get_solution(c.var)
  f <- function(x) as.numeric(a %*% as.numeric(x) + a0)
  f
}

Testing the MIP discriminant model

Setosa v. Versicolor/Virginica

Let’s start by trying to discriminate between setosa (“positive”) and versicolor/virginica (“negative”). The sink() function is used to avoid cluttering the page with a rather lengthy progress report from the solver.

samples <- partition("setosa")
sink("/dev/null")
result <- mipDiscriminant(samples)
CPLEX environment opened
Closed CPLEX environment
sink()

The first thing to do is check whether the model solved successfully.

cat("Status = ", result$status)
Status =  optimal

Next, we can take a look at the objective value.

cat("Objective value = ", result$objective_value)
Objective value =  -0.04166667

Values for scalar model variables are returned as numeric vectors of length 1.

c <- result %>% get_solution(c.var)
c
c.var 
 3.65 
d <- result %>% get_solution(d.var)
d
d.var 
 1.35 

Values for indexed model variables are returned in data frames.

w <- result %>% get_solution(w.var[j])
y <- result %>% get_solution(y.var[i])
z <- result %>% get_solution(z.var[i])
w

Let’s see, based on the y and z indicators, how many observations the model thinks it misclassified.

cat(sprintf("Negative misclassifications: %d\n", nrow(y[y$value >= 0.1, ])))
Negative misclassifications: 0
cat(sprintf("Positive misclassifications: %d\n", nrow(z[z$value >= 0.1, ])))
Positive misclassifications: 0

We allegedly have no errors (100% correct classification for this split). Just to be sure, let’s extract the discriminant function and apply it directly to the data, finding its smallest score on the positive sample (hopefully > 0) and its largest score on the negative sample (hopefully < 0).

dFunc <- extractFunction(result)
cat("Mininum clearance for positive sample: ", min(apply(samples$positive, 1, dFunc)))
Mininum clearance for positive sample:  1.35
cat("\nMinimum clearance for negative sample: ", max(apply(samples$negative, 1, dFunc)))

Minimum clearance for negative sample:  -1.35

So all scores are correct (as expected), and the closest any observation comes to the “boundary” score of 0 is 1.35 (which is the value of the d variable in the model).

For completeness, since we will be doing it below, we create a “confusion matrix” comparing predictions to actual values.

predicted <- as.factor(ifelse(apply(iris[, 1:4], 1, dFunc) > 0, "setosa", "other"))  # predicted class
table(predicted, iris$Species)
         
predicted setosa versicolor virginica
   other       0         50        50
   setosa     50          0         0

Vericolor v. Setosa/Virginica

Next, we repeat the experiment, trying to distinguish versicolor from setosa and virginica.

samples <- partition("versicolor")
sink("/dev/null")
result2 <- mipDiscriminant(samples)
CPLEX environment opened
Closed CPLEX environment
sink()
cat("Status = ", result2$status)
Status =  optimal
cat("\nObjective value = ", result2$objective_value)

Objective value =  25.99967

This takes considerably longer to solve, because 100% accuracy is not attainable (so the MIP solver has to do a fair bit of branching). The good news is that we have an optimal solution; the bad news is that we apparently misclassified 26 observations. We can break that down by group.

dFunc <- extractFunction(result2)
predicted <- as.factor(ifelse(apply(iris[, 1:4], 1, dFunc) > 0, "versicolor", "other"))  # predicted class
table(predicted, iris$Species)
            
predicted    setosa versicolor virginica
  other          50          8        32
  versicolor      0         42        18

The best solution misclassifies 8 versicolor (positive) specimens and 18 specimens from the other two species (negative). We can easily check which they are.

y <- result2 %>% get_solution(y.var[i])
z <- result2 %>% get_solution(z.var[i])
e1 <- which(y$value > 0.5)
e2 <- which(z$value > 0.5)
cat("Negative:\n")
Negative:
e1
 [1] 53 54 56 58 59 62 67 68 69 70 73 76 80 81 82 84 85
[18] 88
cat("Positive:\n")
Positive:
e2
[1]  2  7 10 12 15 21 36 49

We can take a look at the discriminant values for those observations, just to confirm what we are seeing.

cat("Negative:\n")
Negative:
apply(samples$negative[e1, ], 1, dFunc)
       103        104        106        108        109 
0.03900046 0.24529895 0.35739388 0.55365130 0.45506618 
       112        117        118        119        120 
0.06451392 0.16337289 0.05438156 0.42010497 0.48674121 
       123        126        130        131        132 
0.56428571 0.32193063 0.49007303 0.38879507 0.10960749 
       134        135        138 
0.34169329 0.74228663 0.13439069 
cat("\nPositive:\n")

Positive:
apply(samples$positive[e2, ], 1, dFunc)
         52          57          60          62 
-0.06579188 -0.10093565 -0.06825650 -0.13452761 
         65          71          86          99 
-0.17902784 -0.21499315 -0.21973984 -0.12654039 

Virginica v. Setosa/Versicolor

Finally, let’s see if we can discriminate between virginica and setosa/versicolor.

samples <- partition("virginica")
sink("/dev/null")
result3 <- mipDiscriminant(samples)
CPLEX environment opened
Closed CPLEX environment
sink()
cat("Status = ", result3$status)
Status =  optimal
cat("\nObjective value = ", result3$objective_value)

Objective value =  0.9991571

It looks as though we got away with just one misclassification.

dFunc <- extractFunction(result3)
predicted <- as.factor(ifelse(apply(iris[, 1:4], 1, dFunc) > 0, "virginica", "other"))  # predicted class
table(predicted, iris$Species)
           
predicted   setosa versicolor virginica
  other         50         49         0
  virginica      0          1        50
# one of the negative samples was classified positive - which?
y <- result3 %>% get_solution(y.var[i])
e1 <- which(y$value > 0.5)
cat("Error in observation ", e1)
Error in observation  84

So observation 84 from the negative sample, which happens to be a versicolor specimen, seems to be the only one misclassified. Let’s check its discriminant score.

dFunc <- extractFunction(result3)
apply(samples$negative[e1, ], 1, dFunc)
       84 
0.2365741 

It is indeed a positive (incorrect) score.

Classification using an SVM

Just for fun, let’s try using a classification support vector machine (SVMs), via the kernlab package. To keep things simple, we will leave all options at default settings.

Setosa v. Versicolor/Virginica

First, let’s try to discriminate setosa from the others.

class <- factor(iris$Species == "setosa", labels = c("negative", "positive"))
svm <- ksvm(class ~ . - Species, data = iris)
table(predict(svm, iris), iris$Species)
          
           setosa versicolor virginica
  negative      0         50        50
  positive     50          0         0

The SVM matches the 100% accuracy of the MIP model on setosa.

Versicolor v. Setosa/Virginica

class <- factor(iris$Species == "versicolor", labels = c("negative", "positive"))
svm <- ksvm(class ~ . - Species, data = iris)
table(predict(svm, iris), iris$Species)
          
           setosa versicolor virginica
  negative     50          1        48
  positive      0         49         2

Attempting to distinguish versicolor from the other two species, the SVM model misclassifies one or two veriscolor instances and one or two virginica instances, much better than the MIP model did. (This is due to the SVM not being restricted to a linear discriminant function.) The reason I say “one or two” and not specifically what you see in the confusion matrix is that the SVM fitting algorithm appears to use a somewhat randomized algorithm. If you keep refitting the model, the results periodically change. You can stabilize the results by setting a random seed before calling ksvm(), but which results you get will depend on how lucky you are picking the seed.

Virginica v. Setosa/Versicolor

class <- factor(iris$Species == "virginica", labels = c("negative", "positive"))
svm <- ksvm(class ~ . - Species, data = iris)
table(predict(svm, iris), iris$Species)
          
           setosa versicolor virginica
  negative     50         48         2
  positive      0          2        48

The SVM has a total of three or four errors when trying to discriminate between virginica and the other two species, worse than the single error the MIP model achieved. Again, the total error count varies depending on the random seed used.

Reference

[1] Rubin, P. A. Heuristic solution procedures for a mixed-integer programming discriminant model. Managerial and Decision Economics, Vol. 11, No. 4, October 1990.

LS0tCnRpdGxlOiAiT01QUiBEZW1vIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKYXV0aG9yOiBQYXVsIEEuIFJ1YmluCmRhdGU6IE5vdmVtYmVyIDksIDIwMTYKLS0tCgojIEFic3RyYWN0CgpUaGlzIGlzIGEgZGVtb25zdHJhdGlvbiBvZiBEaXJrIFNjaHVtYWNoZXIncyAqKm9tcHIqKiBwYWNrYWdlIChodHRwczovL2RpcmtzY2h1bWFjaGVyLmdpdGh1Yi5pby9vbXByLykuIFdlIHdpbGwgZXhlcmNpc2UgaXQgb24gdGhlIE1JUCBkaXNjcmltaW5hbnQgbW9kZWwgZnJvbSBhbiBvbGQgcGFwZXIgb2YgbWluZSBbMV0sIGFwcGxpZWQgdG8gdGhlIEFuZGVyc29uJ3MgaXJpcyBkYXRhLiBTcGVjaWZpY2FsbHksIHdlJ2xsIHRyeSB0byBkaXNjcmltaW5hdGUgYmV0d2VlbiBlYWNoIGluZGl2aWR1YWwgc3BlY2llcyAoc2V0b3NhLCB2ZXJzaWNvbG9yIG9yIHZpcmdpbmljYSkgYW5kIHRoZSBvdGhlciB0d28sIHVzaW5nIGEgbGluZWFyIGRpc2NyaW1pbmFudCBmdW5jdGlvbiB0aGF0IG1pbmltaXplcyB0aGUgbnVtYmVyIG9mIG1pc2NsYXNzaWZpY2F0aW9ucyBvbiB0aGUgb3ZlcmFsbCBzYW1wbGUuCgpGb3IgZ2VuZXJhbCBhbXVzZW1lbnQsIHdlJ2xsIGFsc28gbG9vayBhdCBob3cgYSBzdXBwb3J0IHZlY3RvciBtYWNoaW5lIGRvZXMgb24gdGhlIHNhbWUgdGhyZWUgY2xhc3NpZmljYXRpb24gcHJvYmxlbXMuIFRoaXMgaXMgbm90IGludGVuZGVkIHRvIGJlIGEgdGVzdCBvZiBNSVAgY2xhc3NpZmljYXRpb24gdi4gb3RoZXIgbWV0aG9kcywgdGhvdWdoLCBhbmQgd2Ugd29uJ3QgZ2V0IGludG8gc2VwYXJhdGUgdHJhaW5pbmcgYW5kIHRlc3Rpbmcgc2FtcGxlIG9yIGstZm9sZCBjcm9zcy12YWxpZGF0aW9uLiBUaGUgZm9jdXMgb2YgdGhpcyBub3RlYm9vayBpcyB0byBleGVyY2lzZSB0aGUgb21wciBwYWNrYWdlICh1c2luZyBDUExFWCAxMi42LjMgYXMgdGhlIHNvbHZlcikgb24gYSAoc29tZXdoYXQpIHJlYWwtd29ybGQgc3RhdGlzdGljYWwgb3B0aW1pemF0aW9uIHByb2JsZW0uCgpCZWZvcmUgc3RhcnRpbmcsIEknbGwganVzdCBub3RlIG9uZSBtaW5vciBnbGl0Y2guIEFzIG9mIHRoaXMgd3JpdGluZywgeW91IGNhbm5vdCBuYW1lIGEgdmFyaWFibGUgImMiIGluIHlvdXIgb3B0aW1pemF0aW9uIG1vZGVsLiBEaXJrIGhhcyBiZWVuIHZlcnkgcmVzcG9uc2l2ZSBpbiBjaGVja2luZyBpbnRvIGlzc3VlcyB3aXRoIGBvbXByYCwgYnV0IHRoaXMgb25lIGlzc3VlIHRyYWNlcyBiYWNrIHRvIGEgbGlicmFyeSB1c2VkIGJ5IGBvbXByYCwgc28gd2hpbGUgRGlyayBoYXMgcmVwb3J0ZWQgaXQsIGhlIGNhbm5vdCBmaXggaXQgaGltc2VsZi4gU28gZmFyLCAiYyIgc2VlbXMgdG8gYmUgdGhlIG9ubHkgdmFyaWFibGUgbmFtZSB0aGF0IGNhdXNlcyBhbnkgdHJvdWJsZS4KCiMgQWJvdXQgdGhpcyBkb2N1bWVudAoKVGhpcyBkb2N1bWVudCBpcyBhbiB0aGUgSFRNTCBvdXRwdXQgZnJvbSBhbiBbUiBub3RlYm9va10oaHR0cDovL3JtYXJrZG93bi5yc3R1ZGlvLmNvbS9yX25vdGVib29rcy5odG1sKS4gVGhlIGNvbnRyb2wgaW4gdGhlIHVwcGVyIHJpZ2h0IHdpbGwgbGV0IHlvdSBzaG93IG9yIGhpZGUgY29kZSAoYWx0aG91Z2gsIGluIHRoaXMgY2FzZSwgdGhlIGNvZGUgaXMgcHJldHR5IG11Y2ggdGhlIHBvaW50IG9mIHRoZSBub3RlYm9vaykuIEl0IHdpbGwgYWxzbyBhbGxvdyB5b3UgdG8gZXh0cmFjdCBhbmQgc2F2ZSB0aGUgbm90ZWJvb2sgKFIgc291cmNlIGZpbGUpIGl0c2VsZi4KCiMgU29sdmVyCgpBcyBtZW50aW9uZWQsIHRoZSBzb2x2ZXIgdXNlZCBoZXJlIGlzIENQTEVYIDEyLjYuMy4gT25lIG9mIHRoZSB2aXJ0dWVzIG9mIG9tcHIgKGFuZCB0aGUgYFJPSWAgcGFja2FnZSBpdCBkZXBlbmRzIG9uKSBpcyB0aGF0LCBhcm1lZCB3aXRoIGFwcHJvcHJpYXRlIHBsdWctaW5zLCB5b3UgY2FuIHN3aXRjaCB0byBhbHRlcm5hdGl2ZSBzb2x2ZXJzLiBUaGUgY29ubmVjdGlvbiB0byBDUExFWCBpcyBoYW5kbGVkIGJ5IHRoZSBgUk9JLnBsdWdpbi5jcGxleGAgcGFja2FnZSwgbG9hZGVkIGJlbG93LiBXaGF0IGlzIG5vdCBpbW1lZGlhdGVseSB2aXNpYmxlIGlzIHRoYXQsIGluIGFkZGl0aW9uIHRvIENQTEVYIGl0c2VsZiwgdGhpcyBwYWNrYWdlIGFsc28gZGVwZW5kcyBvbiB0aGUgYFJjcGxleGAgbGlicmFyeS4KCiMgUHJlcGFyYXRpb24KCldlIHdpbGwgdXNlIHRoZSAxLW5vcm0gb2YgdGhlIG9ic2VydmF0aW9ucyB0byBzZXQgc29tZSBtb2RlbCBwYXJhbWV0ZXJzLCBzbyB3ZSBzdGFydCBieSBkZWZpbmluZyB0aGF0LgoKYGBge3J9Cm5vcm0uMSA8LSBmdW5jdGlvbih4KSBzdW0oYWJzKHgpKQpgYGAKCldlIHdpbGwgYXJiaXRyYXJpbHkgc2V0IGEgbWluaW11bSBhYnNvbHV0ZSBkaXNjcmltaW5hbnQgdmFsdWUgKGRlbHRhKSBvZiAwLjAxIGZvciBhIGNvcnJlY3QgY2xhc3NpZmljYXRpb24gKHNvIHRoYXQgZGlzY3JpbWluYW50IHZhbHVlcyBiZXR3ZWVuIC0wLjAxIGFuZCArMC4wMSB3aWxsIGJlIGNvbnNpZGVyZWQgaW5jb25jbHVzaXZlKS4KCmBgYHtyfQpkZWx0YSA8LSAwLjAxCmBgYAoKQmVmb3JlIGNyZWF0aW5nIGFueSBtb2RlbHMsIHdlIG5lZWQgdG8gbG9hZCB0aGUgcmVxdWlyZWQgbGlicmFyaWVzLgoKYGBge3J9CmxpYnJhcnkoUk9JKSAgICAgICAgICAgICAgICMgZ2VuZXJhbCBzb2x2ZXIgaW50ZXJmYWNlCmxpYnJhcnkoUk9JLnBsdWdpbi5jcGxleCkgICMgY29ubmVjdHMgdGhlIFJPSSBwYWNrYWdlIHRvIENQTEVYCmxpYnJhcnkob21wcikgICAgICAgICAgICAgICMgTUlQIG1vZGVsaW5nIHBhY2thZ2UKbGlicmFyeShvbXByLnJvaSkgICAgICAgICAgIyBjb25uZWN0cyBPTVBSIHRvIFJPSQpsaWJyYXJ5KG1hZ3JpdHRyKSAgICAgICAgICAjIGZvciB0aGUgcGlwZSBvcGVyYXRvcgpsaWJyYXJ5KGtlcm5sYWIpICAgICAgICAgICAjIGZvciBTVk1zCmBgYAoKIyBGdW5jdGlvbiBkZWZpbml0aW9ucwoKIyMjIFBhcnRpdGlvbmluZyB0aGUgZGF0YSBpbnRvIHBvc2l0aXZlL25lZ2F0aXZlIHNhbXBsZXMKCldlIHdpbGwgYmUgcGFydGl0aW9uaW5nIHRoZSBvcmlnaW5hbCBkYXRhIHNldCBpbnRvIHBvc2l0aXZlIGFuZCBuZWdhdGl2ZSBzYW1wbGVzIG1vcmUgdGhhbiBvbmNlLCBzbyB3ZSBjcmVhdGUgYSBmdW5jdGlvbiB0byBkbyB0aGF0LiBUaGUgZnVuY3Rpb24gdGFrZXMgYXMgaXRzIHNvbGUgYXJndW1lbnQgdGhlIG5hbWUgKCJwb3MiKSBvZiB0aGUgc3BlY2llcyBpbiB0aGUgcG9zaXRpdmUgc2FtcGxlIGFuZCByZXR1cm5zIGEgbmFtZWQgbGlzdCBvZiBzYW1wbGVzLiBUaGUgKG5vdyByZWR1bmRhbnQpICJTcGVjaWVzIiBjb2x1bW4gaXMgcmVtb3ZlZCBmcm9tIGVhY2ggc3Vic2FtcGxlLgoKYGBge3J9CnBhcnRpdGlvbiA8LSBmdW5jdGlvbihwb3MpIHsKICBsaXN0KHBvc2l0aXZlID0gc3Vic2V0KGlyaXMsIHN1YnNldCA9IChTcGVjaWVzID09IHBvcyksIHNlbGVjdCA9IC1TcGVjaWVzKSwKICAgICAgIG5lZ2F0aXZlID0gc3Vic2V0KGlyaXMsIHN1YnNldCA9IChTcGVjaWVzICE9IHBvcyksIHNlbGVjdCA9IC1TcGVjaWVzKQogICkKfQpgYGAKCiMjIyBCdWlsZGluZyBhbmQgcnVubmluZyBNSVAgZGlzY3JpbWluYW50IG1vZGVscwoKU2luY2Ugd2Ugd2FudCB0byBnZW5lcmF0ZSBzZXZlcmFsIGRpZmZlcmVudCBkaXNjcmltaW5hbnQgZnVuY3Rpb25zLCB3ZSB3aWxsIGNyZWF0ZSBhIGZ1bmN0aW9uIHRoYXQgdGFrZXMgb3VyIGxpc3Qgb2Ygc3Vic2FtcGxlcywgdXNlcyBpdCB0byBnZW5lcmF0ZSBhIE1JUCBtb2RlbCwgc29sdmVzIHRoZSBtb2RlbCBhbmQgcmV0dXJucyB0aGUgcmVzdWx0IGluIGEgbGlzdC4KClZhcmlhYmxlIG5hbWVzIGdlbmVyYWxseSBjb25mb3JtIHRvIHRoZSAxOTkwIHBhcGVyLiBUaGUgdmFyaWFibGVzIGluIHRoZSBtb2RlbCBoYXZlICIudmFyIiBhcHBlbmRlZCB0byBkaXN0aW5ndWlzaCB0aGVtIGZyb20gdGhlIFIgdmFyaWFibGVzIGNvbnRhaW5pbmcgdGhlaXIgdmFsdWVzIChlLmcuLCBgdy52YXJgIGlzIHRoZSBkaXNjcmltaW5hbnQgY29lZmZpY2llbnQgdmVjdG9yIGluIHRoZSBtb2RlbCwgd2hpbGUgYHdgIGlzIHRoZSBSIHZhcmlhYmxlIHJlY2VpdmluZyB0aGUgdmFsdWVzIG9mIGB3LnZhcmAgaW4gdGhlIHNvbHV0aW9uKS4KCmBgYHtyfQptaXBEaXNjcmltaW5hbnQgPC0gZnVuY3Rpb24oZGF0YSkgewogIHBvc2l0aXZlIDwtIGRhdGEkcG9zaXRpdmUKICBuZWdhdGl2ZSA8LSBkYXRhJG5lZ2F0aXZlCiAgIwogICMgV2UgbmVlZCB0aGUgbWF4aW11bSAxLW5vcm0gb2YgYW55IG9ic2VydmF0aW9uIGluIGVhY2ggc2FtcGxlLgogICMKICBEZWx0YTEgPC0gbWF4KGFwcGx5KG5lZ2F0aXZlLCAxLCBub3JtLjEpKQogIERlbHRhMiA8LSBtYXgoYXBwbHkocG9zaXRpdmUsIDEsIG5vcm0uMSkpCiAgRGVsdGFiYXIgPC0gKERlbHRhMSArIERlbHRhMikgLyAyCiAgIwogICMgVGhvc2UgdmFsdWVzIGFyZSB1c2VkIHRvIGNvbXB1dGUgYSAiYmlnIE0iIGNvZWZmaWNpZW50IGZvciBlYWNoIHNhbXBsZS4KICAjCiAgTTEgPC0gMiAqIERlbHRhMSArIERlbHRhYmFyICAjIGJpZyBNIGZvciBuZWdhdGl2ZSBvYnNlcnZhdGlvbnMKICBNMiA8LSAyICogRGVsdGEyICsgRGVsdGFiYXIgICMgYmlnIE0gZm9yIHBvc2l0aXZlIG9ic2VydmF0aW9ucwogICMKICAjIFdlIGFsc28gbmVlZCBhIGNvZWZmaWNpZW50IGZvciBhIHRlcm0gaW4gdGhlIG9iamVjdGl2ZSBmdW5jdGlvbgogICMgdGhhdCByZXdhcmRzIGdyZWF0ZXIgc2VwYXJhdGlvbiBiZXR3ZWVuIHRoZSB0d28gZ3JvdXBzLgogICMKICBlcHNpbG9uIDwtIDEgLyAoMiAqIERlbHRhYmFyKQogICMKICAjIE5leHQsIHdlIGNvbXB1dGUgdGhlIGRpbWVuc2lvbnMgb2YgdGhlIGRhdGEuCiAgIwogIG5wb3MgPC0gbnJvdyhwb3NpdGl2ZSkgICMgc2l6ZSBvZiBwb3NpdGl2ZSBzYW1wbGUKICBubmVnIDwtIG5yb3cobmVnYXRpdmUpICAjIHNpemUgb2YgbmVnYXRpdmUgc2FtcGxlCiAgbnZhciA8LSBuY29sKHBvc2l0aXZlKSAgIyBudW1iZXIgb2YgZmVhdHVyZXMKICAjCiAgIyBOb3cgd2UgY2FuIGJ1aWxkIHRoZSBtb2RlbC4KICAjCiAgbW9kZWwgPC0KICAgIE1JUE1vZGVsKCkgJT4lIAogICAgYWRkX3ZhcmlhYmxlKHkudmFyW2ldLCBpID0gMTpubmVnLCB0eXBlID0gImJpbmFyeSIpICU+JSAgICMgaW5kaWNhdG9ycyBmb3IgbmVnYXRpdmUgbWlzY2xhc3NpZmljYXRpb25zCiAgICBhZGRfdmFyaWFibGUoei52YXJbaV0sIGkgPSAxOm5wb3MsIHR5cGUgPSAiYmluYXJ5IikgJT4lICAgIyBpbmRpY2F0b3JzIGZvciBwb3NpdGl2ZSBtaXNjbGFzc2lmaWNhdGlvbnMKICAgIGFkZF92YXJpYWJsZSh3LnZhcltpXSwgaSA9IDE6bnZhciwgdHlwZSA9ICJjb250aW51b3VzIiwgbGIgPSAtMSwgdWIgPSAxKSAlPiUgICMgY29lZmZpY2llbnRzIG9mIGRpc2NyaW1pbmFudCBmdW5jdGlvbgogICAgYWRkX3ZhcmlhYmxlKGMudmFyLCB0eXBlID0gImNvbnRpbnVvdXMiKSAlPiUgICAjIGNvbnN0YW50IHRlcm0gb2YgZGlzY3JpbWluYW50IGZ1bmN0aW9uICh1bmJvdW5kZWQpCiAgICBhZGRfdmFyaWFibGUoZC52YXIsIHR5cGUgPSAiY29udGludW91cyIsIGxiID0gZGVsdGEpICU+JSAgICMgbWluaW11bSBhYnNvbHV0ZSBzY29yZSBvZiBhbnkgc3VjY2Vzc2Z1bGx5IGNsYXNzaWZpZWQgb2JzZXJ2YXRpb24gKGJvdW5kZWQgYXdheSBmcm9tIHplcm8pCiAgICBzZXRfb2JqZWN0aXZlKHN1bV9leHByKHkudmFyW2ldLCBpID0gMTpubmVnKSArIHN1bV9leHByKHoudmFyW2ldLCBpID0gMTpucG9zKSAtIGVwc2lsb24gKiBkLnZhciwgc2Vuc2UgPSAibWluIikgJT4lICAgIyBtaW5pbWl6ZSBudW1iZXIgb2YgbWlzY2xhc3NpZmljYXRpb25zIHdpdGggY3JlZGl0IGZvciBoaWdoZXIgc3BhY2luZyBmcm9tIHRoZSBjbGFzc2lmaWNhdGlvbiBib3VuZGFyeQogICAgYWRkX2NvbnN0cmFpbnQoc3VtX2V4cHIobmVnYXRpdmVbaSwgal0gKiB3LnZhcltqXSwgaiA9IDE6bnZhcikgKyBjLnZhciArIGQudmFyIC0gTTEgKiB5LnZhcltpXSA8PSAwLCBpID0gMTpubmVnKSAlPiUgICAjIGRldGVybWluZSBuZWdhdGl2ZSBtaXNjbGFzc2lmaWNhdGlvbnMKICAgIGFkZF9jb25zdHJhaW50KHN1bV9leHByKHBvc2l0aXZlW2ksIGpdICogdy52YXJbal0sIGogPSAxOm52YXIpICsgYy52YXIgLSBkLnZhciArIE0yICogei52YXJbaV0gPj0gMCwgaSA9IDE6bnBvcykgICMgZGV0ZXJtaW5lIHBvc2l0aXZlIG1pc2NsYXNzaWZpY2F0aW9ucwogICMKICAjIEZpbmFsbHksIHdlIHNvbHZlIHRoZSBtb2RlbCBhbmQgcmV0dXJuIHRoZSByZXN1bHQuCiAgIwogIG1vZGVsICU+JSBzb2x2ZV9tb2RlbCh3aXRoX1JPSShzb2x2ZXIgPSAiY3BsZXgiKSkKfQpgYGAKCiMjIyBDcmVhdGluZyBkaXNjcmltaW5hbnQgZnVuY3Rpb25zCgpXZSB3aWxsIGFsc28gd2FudCBhIGZ1bmN0aW9uIHRoYXQgdGFrZXMgdGhlIHNvbHV0aW9uIHRvIG91ciBNSVAgbW9kZWwgYW5kIHR1cm5zIGl0IGludG8gYSBsaW5lYXIgZGlzY3JpbWluYW50IGZ1bmN0aW9uLCB0byBmYWNpbGl0YXRlIHRlc3RpbmcgdGhlIGZ1bmN0aW9uLgoKYGBge3J9CmV4dHJhY3RGdW5jdGlvbiA8LSBmdW5jdGlvbihtaXBSZXN1bHQpIHsKICBhIDwtIChtaXBSZXN1bHQgJT4lIGdldF9zb2x1dGlvbih3LnZhcltqXSkpWywgInZhbHVlIl0KICBhMCA8LSBtaXBSZXN1bHQgJT4lIGdldF9zb2x1dGlvbihjLnZhcikKICBmIDwtIGZ1bmN0aW9uKHgpIGFzLm51bWVyaWMoYSAlKiUgYXMubnVtZXJpYyh4KSArIGEwKQogIGYKfQpgYGAKCiMgVGVzdGluZyB0aGUgTUlQIGRpc2NyaW1pbmFudCBtb2RlbAoKIyMjIFNldG9zYSB2LiBWZXJzaWNvbG9yL1ZpcmdpbmljYQoKTGV0J3Mgc3RhcnQgYnkgdHJ5aW5nIHRvIGRpc2NyaW1pbmF0ZSBiZXR3ZWVuIHNldG9zYSAoInBvc2l0aXZlIikgYW5kIHZlcnNpY29sb3IvdmlyZ2luaWNhICgibmVnYXRpdmUiKS4gVGhlIGBzaW5rKClgIGZ1bmN0aW9uIGlzIHVzZWQgdG8gYXZvaWQgY2x1dHRlcmluZyB0aGUgcGFnZSB3aXRoIGEgcmF0aGVyIGxlbmd0aHkgcHJvZ3Jlc3MgcmVwb3J0IGZyb20gdGhlIHNvbHZlci4KCmBgYHtyfQpzYW1wbGVzIDwtIHBhcnRpdGlvbigic2V0b3NhIikKc2luaygiL2Rldi9udWxsIikKcmVzdWx0IDwtIG1pcERpc2NyaW1pbmFudChzYW1wbGVzKQpzaW5rKCkKYGBgCgoKVGhlIGZpcnN0IHRoaW5nIHRvIGRvIGlzIGNoZWNrIHdoZXRoZXIgdGhlIG1vZGVsIHNvbHZlZCBzdWNjZXNzZnVsbHkuCgpgYGB7cn0KY2F0KCJTdGF0dXMgPSAiLCByZXN1bHQkc3RhdHVzKQpgYGAKCk5leHQsIHdlIGNhbiB0YWtlIGEgbG9vayBhdCB0aGUgb2JqZWN0aXZlIHZhbHVlLgoKYGBge3J9CmNhdCgiT2JqZWN0aXZlIHZhbHVlID0gIiwgcmVzdWx0JG9iamVjdGl2ZV92YWx1ZSkKYGBgCgpWYWx1ZXMgZm9yIHNjYWxhciBtb2RlbCB2YXJpYWJsZXMgYXJlIHJldHVybmVkIGFzIG51bWVyaWMgdmVjdG9ycyBvZiBsZW5ndGggMS4KCmBgYHtyfQpjIDwtIHJlc3VsdCAlPiUgZ2V0X3NvbHV0aW9uKGMudmFyKQpjCmQgPC0gcmVzdWx0ICU+JSBnZXRfc29sdXRpb24oZC52YXIpCmQKYGBgCgpWYWx1ZXMgZm9yIGluZGV4ZWQgbW9kZWwgdmFyaWFibGVzIGFyZSByZXR1cm5lZCBpbiBkYXRhIGZyYW1lcy4KCmBgYHtyfQp3IDwtIHJlc3VsdCAlPiUgZ2V0X3NvbHV0aW9uKHcudmFyW2pdKQp5IDwtIHJlc3VsdCAlPiUgZ2V0X3NvbHV0aW9uKHkudmFyW2ldKQp6IDwtIHJlc3VsdCAlPiUgZ2V0X3NvbHV0aW9uKHoudmFyW2ldKQp3CmBgYAoKTGV0J3Mgc2VlLCBiYXNlZCBvbiB0aGUgeSBhbmQgeiBpbmRpY2F0b3JzLCBob3cgbWFueSBvYnNlcnZhdGlvbnMgdGhlIG1vZGVsIHRoaW5rcyBpdCBtaXNjbGFzc2lmaWVkLgoKYGBge3J9CmNhdChzcHJpbnRmKCJOZWdhdGl2ZSBtaXNjbGFzc2lmaWNhdGlvbnM6ICVkXG4iLCBucm93KHlbeSR2YWx1ZSA+PSAwLjEsIF0pKSkKY2F0KHNwcmludGYoIlBvc2l0aXZlIG1pc2NsYXNzaWZpY2F0aW9uczogJWRcbiIsIG5yb3coelt6JHZhbHVlID49IDAuMSwgXSkpKQpgYGAKCldlIGFsbGVnZWRseSBoYXZlIG5vIGVycm9ycyAoMTAwJSBjb3JyZWN0IGNsYXNzaWZpY2F0aW9uIGZvciB0aGlzIHNwbGl0KS4gSnVzdCB0byBiZSBzdXJlLCBsZXQncyBleHRyYWN0IHRoZSBkaXNjcmltaW5hbnQgZnVuY3Rpb24gYW5kIGFwcGx5IGl0IGRpcmVjdGx5IHRvIHRoZSBkYXRhLCBmaW5kaW5nIGl0cyBzbWFsbGVzdCBzY29yZSBvbiB0aGUgcG9zaXRpdmUgc2FtcGxlIChob3BlZnVsbHkgPiAwKSBhbmQgaXRzIGxhcmdlc3Qgc2NvcmUgb24gdGhlIG5lZ2F0aXZlIHNhbXBsZSAoaG9wZWZ1bGx5IDwgMCkuCgpgYGB7cn0KZEZ1bmMgPC0gZXh0cmFjdEZ1bmN0aW9uKHJlc3VsdCkKY2F0KCJNaW5pbnVtIGNsZWFyYW5jZSBmb3IgcG9zaXRpdmUgc2FtcGxlOiAiLCBtaW4oYXBwbHkoc2FtcGxlcyRwb3NpdGl2ZSwgMSwgZEZ1bmMpKSkKY2F0KCJcbk1pbmltdW0gY2xlYXJhbmNlIGZvciBuZWdhdGl2ZSBzYW1wbGU6ICIsIG1heChhcHBseShzYW1wbGVzJG5lZ2F0aXZlLCAxLCBkRnVuYykpKQpgYGAKClNvIGFsbCBzY29yZXMgYXJlIGNvcnJlY3QgKGFzIGV4cGVjdGVkKSwgYW5kIHRoZSBjbG9zZXN0IGFueSBvYnNlcnZhdGlvbiBjb21lcyB0byB0aGUgImJvdW5kYXJ5IiBzY29yZSBvZiAwIGlzIDEuMzUgKHdoaWNoIGlzIHRoZSB2YWx1ZSBvZiB0aGUgZCB2YXJpYWJsZSBpbiB0aGUgbW9kZWwpLgoKRm9yIGNvbXBsZXRlbmVzcywgc2luY2Ugd2Ugd2lsbCBiZSBkb2luZyBpdCBiZWxvdywgd2UgY3JlYXRlIGEgImNvbmZ1c2lvbiBtYXRyaXgiIGNvbXBhcmluZyBwcmVkaWN0aW9ucyB0byBhY3R1YWwgdmFsdWVzLgoKYGBge3J9CnByZWRpY3RlZCA8LSBhcy5mYWN0b3IoaWZlbHNlKGFwcGx5KGlyaXNbLCAxOjRdLCAxLCBkRnVuYykgPiAwLCAic2V0b3NhIiwgIm90aGVyIikpICAjIHByZWRpY3RlZCBjbGFzcwp0YWJsZShwcmVkaWN0ZWQsIGlyaXMkU3BlY2llcykKYGBgCgojIyMgVmVyaWNvbG9yIHYuIFNldG9zYS9WaXJnaW5pY2EKCk5leHQsIHdlIHJlcGVhdCB0aGUgZXhwZXJpbWVudCwgdHJ5aW5nIHRvIGRpc3Rpbmd1aXNoIHZlcnNpY29sb3IgZnJvbSBzZXRvc2EgYW5kIHZpcmdpbmljYS4KCmBgYHtyfQpzYW1wbGVzIDwtIHBhcnRpdGlvbigidmVyc2ljb2xvciIpCnNpbmsoIi9kZXYvbnVsbCIpCnJlc3VsdDIgPC0gbWlwRGlzY3JpbWluYW50KHNhbXBsZXMpCnNpbmsoKQpjYXQoIlN0YXR1cyA9ICIsIHJlc3VsdDIkc3RhdHVzKQpjYXQoIlxuT2JqZWN0aXZlIHZhbHVlID0gIiwgcmVzdWx0MiRvYmplY3RpdmVfdmFsdWUpCmBgYAoKVGhpcyB0YWtlcyBjb25zaWRlcmFibHkgbG9uZ2VyIHRvIHNvbHZlLCBiZWNhdXNlIDEwMCUgYWNjdXJhY3kgaXMgbm90IGF0dGFpbmFibGUgKHNvIHRoZSBNSVAgc29sdmVyIGhhcyB0byBkbyBhIGZhaXIgYml0IG9mIGJyYW5jaGluZykuIFRoZSBnb29kIG5ld3MgaXMgdGhhdCB3ZSBoYXZlIGFuIG9wdGltYWwgc29sdXRpb247IHRoZSBiYWQgbmV3cyBpcyB0aGF0IHdlIGFwcGFyZW50bHkgbWlzY2xhc3NpZmllZCAyNiBvYnNlcnZhdGlvbnMuIFdlIGNhbiBicmVhayB0aGF0IGRvd24gYnkgZ3JvdXAuCgpgYGB7cn0KZEZ1bmMgPC0gZXh0cmFjdEZ1bmN0aW9uKHJlc3VsdDIpCnByZWRpY3RlZCA8LSBhcy5mYWN0b3IoaWZlbHNlKGFwcGx5KGlyaXNbLCAxOjRdLCAxLCBkRnVuYykgPiAwLCAidmVyc2ljb2xvciIsICJvdGhlciIpKSAgIyBwcmVkaWN0ZWQgY2xhc3MKdGFibGUocHJlZGljdGVkLCBpcmlzJFNwZWNpZXMpCmBgYAoKVGhlIGJlc3Qgc29sdXRpb24gbWlzY2xhc3NpZmllcyA4IHZlcnNpY29sb3IgKHBvc2l0aXZlKSBzcGVjaW1lbnMgYW5kIDE4IHNwZWNpbWVucyBmcm9tIHRoZSBvdGhlciB0d28gc3BlY2llcyAobmVnYXRpdmUpLiBXZSBjYW4gZWFzaWx5IGNoZWNrIHdoaWNoIHRoZXkgYXJlLgoKYGBge3J9CnkgPC0gcmVzdWx0MiAlPiUgZ2V0X3NvbHV0aW9uKHkudmFyW2ldKQp6IDwtIHJlc3VsdDIgJT4lIGdldF9zb2x1dGlvbih6LnZhcltpXSkKZTEgPC0gd2hpY2goeSR2YWx1ZSA+IDAuNSkKZTIgPC0gd2hpY2goeiR2YWx1ZSA+IDAuNSkKY2F0KCJOZWdhdGl2ZTpcbiIpCmUxCmNhdCgiUG9zaXRpdmU6XG4iKQplMgpgYGAKCldlIGNhbiB0YWtlIGEgbG9vayBhdCB0aGUgZGlzY3JpbWluYW50IHZhbHVlcyBmb3IgdGhvc2Ugb2JzZXJ2YXRpb25zLCBqdXN0IHRvIGNvbmZpcm0gd2hhdCB3ZSBhcmUgc2VlaW5nLgoKYGBge3J9CmNhdCgiTmVnYXRpdmU6XG4iKQphcHBseShzYW1wbGVzJG5lZ2F0aXZlW2UxLCBdLCAxLCBkRnVuYykKY2F0KCJcblBvc2l0aXZlOlxuIikKYXBwbHkoc2FtcGxlcyRwb3NpdGl2ZVtlMiwgXSwgMSwgZEZ1bmMpCmBgYAoKIyMjIFZpcmdpbmljYSB2LiBTZXRvc2EvVmVyc2ljb2xvcgoKRmluYWxseSwgbGV0J3Mgc2VlIGlmIHdlIGNhbiBkaXNjcmltaW5hdGUgYmV0d2VlbiB2aXJnaW5pY2EgYW5kIHNldG9zYS92ZXJzaWNvbG9yLgoKYGBge3J9CnNhbXBsZXMgPC0gcGFydGl0aW9uKCJ2aXJnaW5pY2EiKQpzaW5rKCIvZGV2L251bGwiKQpyZXN1bHQzIDwtIG1pcERpc2NyaW1pbmFudChzYW1wbGVzKQpzaW5rKCkKY2F0KCJTdGF0dXMgPSAiLCByZXN1bHQzJHN0YXR1cykKY2F0KCJcbk9iamVjdGl2ZSB2YWx1ZSA9ICIsIHJlc3VsdDMkb2JqZWN0aXZlX3ZhbHVlKQpgYGAKCkl0IGxvb2tzIGFzIHRob3VnaCB3ZSBnb3QgYXdheSB3aXRoIGp1c3Qgb25lIG1pc2NsYXNzaWZpY2F0aW9uLgoKYGBge3J9CmRGdW5jIDwtIGV4dHJhY3RGdW5jdGlvbihyZXN1bHQzKQpwcmVkaWN0ZWQgPC0gYXMuZmFjdG9yKGlmZWxzZShhcHBseShpcmlzWywgMTo0XSwgMSwgZEZ1bmMpID4gMCwgInZpcmdpbmljYSIsICJvdGhlciIpKSAgIyBwcmVkaWN0ZWQgY2xhc3MKdGFibGUocHJlZGljdGVkLCBpcmlzJFNwZWNpZXMpCiMgb25lIG9mIHRoZSBuZWdhdGl2ZSBzYW1wbGVzIHdhcyBjbGFzc2lmaWVkIHBvc2l0aXZlIC0gd2hpY2g/CnkgPC0gcmVzdWx0MyAlPiUgZ2V0X3NvbHV0aW9uKHkudmFyW2ldKQplMSA8LSB3aGljaCh5JHZhbHVlID4gMC41KQpjYXQoIkVycm9yIGluIG9ic2VydmF0aW9uICIsIGUxKQpgYGAKClNvIG9ic2VydmF0aW9uIDg0IGZyb20gdGhlIG5lZ2F0aXZlIHNhbXBsZSwgd2hpY2ggaGFwcGVucyB0byBiZSBhIHZlcnNpY29sb3Igc3BlY2ltZW4sIHNlZW1zIHRvIGJlIHRoZSBvbmx5IG9uZSBtaXNjbGFzc2lmaWVkLiBMZXQncyBjaGVjayBpdHMgZGlzY3JpbWluYW50IHNjb3JlLgoKYGBge3J9CmRGdW5jIDwtIGV4dHJhY3RGdW5jdGlvbihyZXN1bHQzKQphcHBseShzYW1wbGVzJG5lZ2F0aXZlW2UxLCBdLCAxLCBkRnVuYykKYGBgCgpJdCBpcyBpbmRlZWQgYSBwb3NpdGl2ZSAoaW5jb3JyZWN0KSBzY29yZS4KCiMgQ2xhc3NpZmljYXRpb24gdXNpbmcgYW4gU1ZNCgpKdXN0IGZvciBmdW4sIGxldCdzIHRyeSB1c2luZyBhIGNsYXNzaWZpY2F0aW9uIHN1cHBvcnQgdmVjdG9yIG1hY2hpbmUgKFNWTXMpLCB2aWEgdGhlIGtlcm5sYWIgcGFja2FnZS4gVG8ga2VlcCB0aGluZ3Mgc2ltcGxlLCB3ZSB3aWxsIGxlYXZlIGFsbCBvcHRpb25zIGF0IGRlZmF1bHQgc2V0dGluZ3MuCgojIyMgU2V0b3NhIHYuIFZlcnNpY29sb3IvVmlyZ2luaWNhCgpGaXJzdCwgbGV0J3MgdHJ5IHRvIGRpc2NyaW1pbmF0ZSBzZXRvc2EgZnJvbSB0aGUgb3RoZXJzLgoKYGBge3J9CmNsYXNzIDwtIGZhY3RvcihpcmlzJFNwZWNpZXMgPT0gInNldG9zYSIsIGxhYmVscyA9IGMoIm5lZ2F0aXZlIiwgInBvc2l0aXZlIikpCnN2bSA8LSBrc3ZtKGNsYXNzIH4gLiAtIFNwZWNpZXMsIGRhdGEgPSBpcmlzKQp0YWJsZShwcmVkaWN0KHN2bSwgaXJpcyksIGlyaXMkU3BlY2llcykKYGBgCgpUaGUgU1ZNIG1hdGNoZXMgdGhlIDEwMCUgYWNjdXJhY3kgb2YgdGhlIE1JUCBtb2RlbCBvbiBzZXRvc2EuCgojIyMgVmVyc2ljb2xvciB2LiBTZXRvc2EvVmlyZ2luaWNhCgpgYGB7cn0KY2xhc3MgPC0gZmFjdG9yKGlyaXMkU3BlY2llcyA9PSAidmVyc2ljb2xvciIsIGxhYmVscyA9IGMoIm5lZ2F0aXZlIiwgInBvc2l0aXZlIikpCnN2bSA8LSBrc3ZtKGNsYXNzIH4gLiAtIFNwZWNpZXMsIGRhdGEgPSBpcmlzKQp0YWJsZShwcmVkaWN0KHN2bSwgaXJpcyksIGlyaXMkU3BlY2llcykKYGBgCgpBdHRlbXB0aW5nIHRvIGRpc3Rpbmd1aXNoIHZlcnNpY29sb3IgZnJvbSB0aGUgb3RoZXIgdHdvIHNwZWNpZXMsIHRoZSBTVk0gbW9kZWwgbWlzY2xhc3NpZmllcyBvbmUgb3IgdHdvIHZlcmlzY29sb3IgaW5zdGFuY2VzIGFuZCBvbmUgb3IgdHdvIHZpcmdpbmljYSBpbnN0YW5jZXMsIG11Y2ggYmV0dGVyIHRoYW4gdGhlIE1JUCBtb2RlbCBkaWQuIChUaGlzIGlzIGR1ZSB0byB0aGUgU1ZNIG5vdCBiZWluZyByZXN0cmljdGVkIHRvIGEgbGluZWFyIGRpc2NyaW1pbmFudCBmdW5jdGlvbi4pIFRoZSByZWFzb24gSSBzYXkgIm9uZSBvciB0d28iIGFuZCBub3Qgc3BlY2lmaWNhbGx5IHdoYXQgeW91IHNlZSBpbiB0aGUgY29uZnVzaW9uIG1hdHJpeCBpcyB0aGF0IHRoZSBTVk0gZml0dGluZyBhbGdvcml0aG0gYXBwZWFycyB0byB1c2UgYSBzb21ld2hhdCByYW5kb21pemVkIGFsZ29yaXRobS4gSWYgeW91IGtlZXAgcmVmaXR0aW5nIHRoZSBtb2RlbCwgdGhlIHJlc3VsdHMgcGVyaW9kaWNhbGx5IGNoYW5nZS4gWW91IGNhbiBzdGFiaWxpemUgdGhlIHJlc3VsdHMgYnkgc2V0dGluZyBhIHJhbmRvbSBzZWVkIGJlZm9yZSBjYWxsaW5nIGBrc3ZtKClgLCBidXQgd2hpY2ggcmVzdWx0cyB5b3UgZ2V0IHdpbGwgZGVwZW5kIG9uIGhvdyBsdWNreSB5b3UgYXJlIHBpY2tpbmcgdGhlIHNlZWQuCgojIyMgVmlyZ2luaWNhIHYuIFNldG9zYS9WZXJzaWNvbG9yCgpgYGB7cn0KY2xhc3MgPC0gZmFjdG9yKGlyaXMkU3BlY2llcyA9PSAidmlyZ2luaWNhIiwgbGFiZWxzID0gYygibmVnYXRpdmUiLCAicG9zaXRpdmUiKSkKc3ZtIDwtIGtzdm0oY2xhc3MgfiAuIC0gU3BlY2llcywgZGF0YSA9IGlyaXMpCnRhYmxlKHByZWRpY3Qoc3ZtLCBpcmlzKSwgaXJpcyRTcGVjaWVzKQpgYGAKClRoZSBTVk0gaGFzIGEgdG90YWwgb2YgdGhyZWUgb3IgZm91ciBlcnJvcnMgd2hlbiB0cnlpbmcgdG8gZGlzY3JpbWluYXRlIGJldHdlZW4gdmlyZ2luaWNhIGFuZCB0aGUgb3RoZXIgdHdvIHNwZWNpZXMsIHdvcnNlIHRoYW4gdGhlIHNpbmdsZSBlcnJvciB0aGUgTUlQIG1vZGVsIGFjaGlldmVkLiBBZ2FpbiwgdGhlIHRvdGFsIGVycm9yIGNvdW50IHZhcmllcyBkZXBlbmRpbmcgb24gdGhlIHJhbmRvbSBzZWVkIHVzZWQuCgojIFJlZmVyZW5jZQoKWzFdIFJ1YmluLCBQLiBBLiBIZXVyaXN0aWMgc29sdXRpb24gcHJvY2VkdXJlcyBmb3IgYSBtaXhlZC1pbnRlZ2VyIHByb2dyYW1taW5nIGRpc2NyaW1pbmFudCBtb2RlbC4gKk1hbmFnZXJpYWwgYW5kIERlY2lzaW9uIEVjb25vbWljcyosIFZvbC4gMTEsIE5vLiA0LCBPY3RvYmVyIDE5OTAu