### Load standardpackages
library(tidyverse) # Collection of all the good stuff like dplyr, ggplot2 ect.
library(magrittr) # For extra-piping operators (eg. %<>%)
# Load specific packages
# install.packages("tidymodels") " Install if necessary
library(tidymodels)

Welcome all to this introduction to machine learning (ML). In this session we cover the following topics 1. Generalizating and valididating from ML models. 2. The Bias-Variance Trade-Off 3. Out-of-sample testing and cross-validation workflows 4. Implementing Ml workflows with the tidymodels ecosystem.

Introduction to ML workflows in R

Remeber, the steps in the ML workflow are:

  1. Obtaining data

  2. Cleaning and inspecting

  3. Visualizing and exploring data

  4. Preprocessing data

  5. Fiting and tuning models

  6. Validating models

  7. Communicating insights

While step 1-3 is mainly covered by the general tidyverse packages such as dplyr and ggplot2, step 7 can be done using for instance rmarkdown (like me here) or developing an interactive shiny application. We will touch upon that, but the main focus here lies in the steps 5-6, the core of ML work.

These steps are mainly covered by the packages to be found in the tidymodels ecosystem, which take care of sampling, fitting, tuning, and evaluating models and data.

tidymodels is an ecosystem of packages to implement efficient and consisting SML modelling workflows consistent with the tidy principles and neathly fitting into tidy workflows. It contains the following packages

  • rsample provides infrastructure for efficient data splitting and resampling.
  • parsnip is a tidy, unified interface to models independent of the particular package syntax.
  • recipes is a tidy interface to data pre-processing tools for feature engineering.
  • workflows bundle your pre-processing, modeling, and post-processing together.
  • tune optimizes the hyperparameters.
  • yardstick provides model performance metrics.
  • broom converts the information in common statistical R objects into user-friendly tidy formats.
  • dials creates and manages tuning parameters and parameter grids.

I will tap into most of them during this and later sessions, therefore it makes sense to upfront load th complete tidymodels ecosystem.

Lets get started.

The very basics:

Regression problems

Let’ do a brief example for a simple linear model. We generate some data, where \(y\) is a linear function of \(x\) plus some random error.

set.seed(1337)
beta0 = 15
beta1 = 0.3
data_reg <- tibble(x = runif(500, min = 0, max = 100),
               y = beta0+ (beta1*x) + rnorm(500, sd = 5))
data_reg %>% ggplot(aes(x = x, y = y)) + 
  geom_point() +
  geom_rug(size = 0.1, alpha = 0.75) 

We can now fit a linear regression model that aims at discovering the underlying relationship.

fit_lm <- data_reg %>% lm(formula = y ~ x)
fit_lm %>% summary()

Call:
lm(formula = y ~ x, data = .)

Residuals:
    Min      1Q  Median      3Q     Max 
-15.423  -3.317  -0.170   3.337  17.157 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 14.791865   0.452922   32.66   <2e-16 ***
x            0.303863   0.007865   38.63   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 5.123 on 498 degrees of freedom
Multiple R-squared:  0.7498,    Adjusted R-squared:  0.7493 
F-statistic:  1493 on 1 and 498 DF,  p-value: < 2.2e-16

We see it got the underlying relationship somewhat correct. Keep in mind, its ability to discover it is also limited by the small sample, where small random errors dan bias the result.

Note: This is exactly what geom_smooth() in ggplot does when giving it the method="lm" parameter. Lets take a look at it visually.

data_reg %>% ggplot(aes(x = x, y = y)) + 
  geom_point() +
  geom_smooth(method = "lm", formula = y ~ x, se = TRUE)

We can now use predict() to predict y values due to the fitted model.

data_reg %<>%
  mutate(predicted = fit_lm %>% predict())
data_reg %>% ggplot(aes(x = x, y = y)) +
  geom_segment(aes(xend = x, yend = predicted), alpha = .2) + 
  geom_point(alpha = 0.5) +
  geom_point(aes(y = predicted), col = 'red', shape = 21) 

It obviously predicts along th straight function line. Due to the random noise introduced, it is most of the time off a bit. Lets calculate the error term

error_reg <-  pull(data_reg, y) -  pull(data_reg, predicted)
error_reg %>% mean()
[1] 3.036836e-14

On average the error is very low. However, keep in mind positive and negative errors cancel each others out. Lets look at the RSME better.

sqrt(mean(error_reg ^ 2)) # Calculate RMSE
[1] 5.112672

Btw: Could also be piped…

error_reg^2 %>% mean() %>% sqrt()
[1] 5.112672

However, we predicted on the data the model was fitted on. How would it fair on new data?

set.seed(1338)
data_reg_new <- tibble(x = runif(500, min = 0, max = 100),
               y = beta0+ (beta1*x) + rnorm(500, sd = 5))
pred_reg_new <- fit_lm %>% predict(new_data = data_reg_new)
error_reg_new <- error <-  pull(data_reg_new, y) -  pred_reg_new
error_reg_new^2 %>% mean() %>% sqrt()
[1] 13.27436

Classification problems

Ok, lets try the same with a binary class prediction. Lets create a random x and an associated binary y.

set.seed(1337)
beta1 <- 5

data_clas <- tibble(
  x = rnorm(500),
  y = rbinom(500, size = 1, prob = 1/(1+exp(-(beta1*x))) ) %>% as.logical() %>% factor()
  )
data_clas %>% head()
data_clas %>%
  ggplot(aes(x = x, y = y)) +
  geom_point(alpha = 0.5)

lets fit a logistic regression on that

fit_log <- data_clas %>%
  glm(formula = y ~ x, family = 'binomial')
fit_log %>% summary()

Call:
glm(formula = y ~ x, family = "binomial", data = .)

Deviance Residuals: 
     Min        1Q    Median        3Q       Max  
-2.90735  -0.23058  -0.00276   0.22925   2.68607  

Coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept) -0.01544    0.17213  -0.090    0.929    
x            5.27883    0.52813   9.995   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 692.86  on 499  degrees of freedom
Residual deviance: 219.50  on 498  degrees of freedom
AIC: 223.5

Number of Fisher Scoring iterations: 7

We can again visualize it:

data_clas %>% 
  mutate(y = y %>% as.logical() %>% as.numeric()) %>%
  ggplot(aes(x = x, y = y)) + 
  geom_point(alpha = 0.5) +
  geom_smooth(method = "glm", method.args = list(family = "binomial"), se = FALSE) 

We again can use this fitted model to predict the datapoints y-class. Here, we have the choice to either report the predicted class or the predicted probability. We here do both.

data_clas %<>%
  mutate(predicted = fit_log %>% predict(type = 'response'),
         predicted_class = predicted %>% round(0) %>% as.logical() %>% factor())
data_clas %>% head()
cm_log <- data_clas %>% conf_mat(y, predicted_class)
cm_log %>% autoplot(type = "heatmap")

cm_log %>% summary() %>% mutate(.estimate = .estimate %>% round(3)) %>% select(-.estimator)
roc_log <- data_clas %>% 
  roc_curve(y, predicted, event_level = 'second') 

roc_log %>% head()
data_clas %>% roc_auc(y, predicted, event_level = 'second') 
roc_log %>% autoplot()

Again, lets create some new data to test

set.seed(1338)
beta1 <- 5

data_clas_new <- tibble(
  x = rnorm(500),
  y = rbinom(500, size = 1, prob = 1/(1+exp(-(beta1*x))) ) %>% as.logical() %>% factor()
  )
data_clas_new %<>%
  mutate(predicted = fit_log %>% predict(type = 'response', newdata = data_clas_new),
         predicted_class = predicted %>% round(0) %>% as.logical() %>% factor())
cm_log_new <- data_clas_new %>% conf_mat(y, predicted_class)
cm_log_new %>% summary() %>% mutate(.estimate = .estimate %>% round(3)) %>% select(-.estimator)
data_clas %>% roc_auc(y, predicted, event_level = 'second') 

SML workflows

Ok, that all now looked a bit cumbersome. Lets do it a bit more advanced and flexible introducing the tidymodel ML workflow. Here, we would apply the following standard workflow:

  1. Split dataset in training & test sample
    • This is done with the rsample function initial_split()
  2. Apply preprocessing steps if necessary
    • Can be don manually, but for convenience and reproducability better by defining a recipe
  3. Define the models to fit
    • Done by setting up a model structure with parsnip
  4. Define a resampling strategy.
    • We choose among diferent resampling options with the rsample package
  5. (Optimal): Tune Hyperparameters.
    • Here we use the tune package to tune hyperparameters and the dials package to manage the hyperparameter search
  6. Select the best performing hyperparameter setup.
  7. Fit the final model.
  8. Evaluate it on the test data.

ML case 1 (Regression, tabular data): Boston Housing Prices

Data Description

We will load a standard dataset from mlbench, the BostonHousing dataset. It comes as a dataframe with 506 observations on 14 features, the last one medv being the outcome:

  • crim per capita crime rate by town
  • zn proportion of residential land zoned for lots over 25,000 sq.ft
  • indus proportion of non-retail business acres per town
  • chas Charles River dummy variable (= 1 if tract bounds river; 0 otherwise) (deselected in this case)
  • nox nitric oxides concentration (parts per 110 million)
  • rm average number of rooms per dwelling
  • age proportion of owner-occupied units built prior to 1940
  • dis weighted distances to five Boston employment centres
  • rad index of accessibility to radial highways
  • tax full-value property-tax rate per USD 10,000
  • ptratio pupil-teacher ratio by town
  • b 1000(B - 0.63)^2 where B is the proportion of blacks by town
  • lstat lower status of the population
  • medv median value of owner-occupied homes in USD 1000’s (our outcome to predict)

Source: Harrison, D. and Rubinfeld, D.L. “Hedonic prices and the demand for clean air”, J. Environ. Economics & Management, vol.5, 81-102, 1978.

These data have been taken from the UCI Repository Of Machine Learning Databases

# install.packages('mlbench')# Install if necessary 
library(mlbench) # Library including many ML benchmark datasets
data(BostonHousing) 
data <- BostonHousing %>% as_tibble() %>% select(-chas)
rm(BostonHousing)
data %>% head()
data %>% glimpse()
Rows: 506
Columns: 13
$ crim    <dbl> 0.00632, 0.02731, 0.02729, 0.03237, 0.06905, 0.02985, 0.08829, 0.14455, 0.21124, 0.17004, 0.22489, 0.11747, 0.09378, 0.62976, 0.63796, 0.62739, 1.0539…
$ zn      <dbl> 18.0, 0.0, 0.0, 0.0, 0.0, 0.0, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.…
$ indus   <dbl> 2.31, 7.07, 7.07, 2.18, 2.18, 2.18, 7.87, 7.87, 7.87, 7.87, 7.87, 7.87, 7.87, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, 8.14, …
$ nox     <dbl> 0.538, 0.469, 0.469, 0.458, 0.458, 0.458, 0.524, 0.524, 0.524, 0.524, 0.524, 0.524, 0.524, 0.538, 0.538, 0.538, 0.538, 0.538, 0.538, 0.538, 0.538, 0.5…
$ rm      <dbl> 6.575, 6.421, 7.185, 6.998, 7.147, 6.430, 6.012, 6.172, 5.631, 6.004, 6.377, 6.009, 5.889, 5.949, 6.096, 5.834, 5.935, 5.990, 5.456, 5.727, 5.570, 5.9…
$ age     <dbl> 65.2, 78.9, 61.1, 45.8, 54.2, 58.7, 66.6, 96.1, 100.0, 85.9, 94.3, 82.9, 39.0, 61.8, 84.5, 56.5, 29.3, 81.7, 36.6, 69.5, 98.1, 89.2, 91.7, 100.0, 94.1…
$ dis     <dbl> 4.0900, 4.9671, 4.9671, 6.0622, 6.0622, 6.0622, 5.5605, 5.9505, 6.0821, 6.5921, 6.3467, 6.2267, 5.4509, 4.7075, 4.4619, 4.4986, 4.4986, 4.2579, 3.7965…
$ rad     <dbl> 1, 2, 2, 3, 3, 3, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, …
$ tax     <dbl> 296, 242, 242, 222, 222, 222, 311, 311, 311, 311, 311, 311, 311, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, 307, …
$ ptratio <dbl> 15.3, 17.8, 17.8, 18.7, 18.7, 18.7, 15.2, 15.2, 15.2, 15.2, 15.2, 15.2, 15.2, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, …
$ b       <dbl> 396.90, 396.90, 392.83, 394.63, 396.90, 394.12, 395.60, 396.90, 386.63, 386.71, 392.52, 396.90, 390.50, 396.90, 380.02, 395.62, 386.85, 386.75, 288.99…
$ lstat   <dbl> 4.98, 9.14, 4.03, 2.94, 5.33, 5.21, 12.43, 19.15, 29.93, 17.10, 20.45, 13.27, 15.71, 8.26, 10.26, 8.47, 6.58, 14.67, 11.69, 11.28, 21.02, 13.83, 18.72…
$ medv    <dbl> 24.0, 21.6, 34.7, 33.4, 36.2, 28.7, 22.9, 27.1, 16.5, 18.9, 15.0, 18.9, 21.7, 20.4, 18.2, 19.9, 23.1, 17.5, 20.2, 18.2, 13.6, 19.6, 15.2, 14.5, 15.6, …

In this exercise, we will predict medv (median value of owner-occupied homes in USD). Such a model would in the real world be used to predict developments in housing prices, eg. to inform policy makers or potential investors. In case I have only one target outcome, I prefer to name it as y. This simple naming convention helps to re-use code across datasets.

data %<>% 
  rename(y = medv) %>%
  relocate(y)

Data Inspecition and Visualization

Lets take a look at some descriptives.

data %>%
  summarise(across(everything(), list(min = min, mean = mean,max = max, sd = sd), .names = "{.col}_{.fn}")) %>%
  mutate(across(everything(), round, 2)) %>%
  pivot_longer(everything(), 
               names_sep = "_",
               names_to  = c("variable", ".value"))

Ok, time for some visual exploration. Here I will introduce the GGally package, a wrapper for ggplot2 which has some functions for very nice visual summaries in matrix form.

First, lets look at a classical correlation matrix.

# install.packages('GGally') # Install if necessary
data %>%
  GGally::ggcorr(label = TRUE, 
                 label_size = 3, 
                 label_round = 2, 
                 label_alpha = TRUE)

Even cooler, the ggpairs function creates you a scatterplot matrix plus all variable distributions and correlations.

data %>%
  GGally::ggpairs(aes(alpha = 0.3), 
          ggtheme = theme_gray())  

Data Preprocessing

Training & Test split

First, we split our data in training and test sample. We use the initial_split function of the rsample pckage.

data_split <- initial_split(data, prop = 0.75, strata = y)

data_train <- data_split  %>%  training()
data_test <- data_split %>% testing()

Preprocessing recipe

We use the recipe package to automatize and standardize all necessary pre-processing workflows.

Here, we do only some simple transformations. * We normalize all numeric data by centering (subtracting the mean) and scaling (divide by standard deviation). * We remove features with near-zero-variance, which would not help the model a lot. * We here also add a simple way to already in the preprocessing deal with missing data. recipes has inbuild missing value inputation algorithms, such as ‘k-nearest-neighbors’.

data_recipe <- data_train %>%
  recipe(y ~.) %>%
  step_center(all_numeric(), -all_outcomes()) %>% # Centers all numeric variables to mean = 0
  step_scale(all_numeric(), -all_outcomes()) %>% # scales all numeric variables to sd = 1
  step_nzv(all_predictors())  %>% # Removed predictors with zero variance
  step_knnimpute(all_predictors()) %>% #  knn inputation of missing values
  prep()
data_recipe
Data Recipe

Inputs:

Training data contained 381 data points and no missing data.

Operations:

Centering for crim, zn, indus, nox, rm, age, dis, rad, tax, ptratio, b, lstat [trained]
Scaling for crim, zn, indus, nox, rm, age, dis, rad, tax, ptratio, b, lstat [trained]
Sparse, unbalanced variable filter removed no terms [trained]
K-nearest neighbor imputation for zn, indus, nox, rm, age, dis, rad, tax, ptratio, b, lstat, crim [trained]

Defining the models

First of all, we will define the models we will run here. In detail, we will run a:

  1. OLS model (Baseline)
  2. Elastic net (still parametric, but maybe advantage in feature selection)
  3. Random forest (tree-based ensemble model)

There is no particular reason other than to demonstrate different models with increasing complexity and hyperparameter tuning options.

To set up a model with parsnip, the following syntax applies:

model_XX <- model_family(mode = 'regression/classification',
                         parameter_1 = 123,
                         parameter_2 = tune()) %>%
  set_engine('packagename')

Linear Model (OLS)

model_lm <- linear_reg(mode = 'regression') %>%
  set_engine('lm') 

Elastic Net (Penalized Regression)

model_el <-linear_reg(mode = 'regression', 
                      penalty = tune(), 
                      mixture = tune()) %>%
  set_engine("glmnet")

Random Forest

model_rf <- rand_forest(mode = 'regression',
                        trees = 25,
                        mtry = tune(),
                        min_n = tune()
                        ) %>%
  set_engine('ranger', importance = 'impurity') 

Define workflow

We now define workflows by putting the preprocessing recipe together with the corresponding models. Not a necessary step, but I find it neath.

workflow_general <- workflow() %>%
  add_recipe(data_recipe) 

workflow_lm <- workflow_general %>%
  add_model(model_lm)

workflow_el <- workflow_general %>%
  add_model(model_el)

workflow_rf <- workflow_general %>%
  add_model(model_rf)

Hyperparameter Tuning

Validation Sampling (Bootstrapping)

  • Now it is time to define a sampling strategy. Instead of the k-fold crossvalidation strategy I already introduced earlier, we will here use a bootstrap sampling strategy.
  • We will draw a number of n randomly selected observations from the sample, and repeat this process 5 times.
  • That means that our bootstrapped samples have the same size as the original one. This is a good resampling strategy in case the initial number of observations is low.
data_resample <- bootstraps(data_train, 
                            strata = y,
                            times = 5)
data_resample %>% glimpse() 
Rows: 5
Columns: 2
$ splits <list> [<rsplit[381 x 156 x 381 x 13]>, <rsplit[381 x 140 x 381 x 13]>, <rsplit[381 x 134 x 381 x 13]>, <rsplit[381 x 140 x 381 x 13]>, <rsplit[381 x 136 x 3…
$ id     <chr> "Bootstrap1", "Bootstrap2", "Bootstrap3", "Bootstrap4", "Bootstrap5"

Hyperparameter Tuning: Elastic Net

tune_el <-
  tune_grid(
    workflow_el,
    resamples = data_resample,
    grid = 10
  )
tune_el %>% autoplot()

best_param_el <- tune_el %>% select_best(metric = 'rmse')
best_param_el
tune_el %>% show_best(metric = 'rmse', n = 1)

Hyperparameter Tuning: Random Forest

tune_rf <-
  tune_grid(
    workflow_rf,
    resamples = data_resample,
    grid = 10
  )
tune_rf %>% autoplot()

best_param_rf <- tune_rf %>% select_best(metric = 'rmse')
best_param_rf
tune_rf %>% show_best(metric = 'rmse', n = 1)

Fit models with tuned hyperparameters

Alright, now we can fit the final models. Therefore, we have to first upate the formerly created workflows, where we fill the tune() placeholders with the by now determined best performing hyperparameter setup.

workflow_final_el <- workflow_el %>%
  finalize_workflow(parameters = best_param_el)

workflow_final_rf <- workflow_rf %>%
  finalize_workflow(parameters = best_param_rf)
fit_lm <- workflow_lm %>%
  fit(data_train)

fit_el <- workflow_final_el %>%
  fit(data_train)

fit_rf <- workflow_final_rf %>%
  fit(data_train)

Compare performance

pred_collected <- tibble(
  truth = data_train %>% pull(y),
  base = mean(truth),
  lm = fit_lm %>% predict(new_data = data_train) %>% pull(.pred),
  el = fit_el %>% predict(new_data = data_train) %>% pull(.pred),
  rf = fit_rf %>% predict(new_data = data_train) %>% pull(.pred),
  ) %>% 
  pivot_longer(cols = -truth,
               names_to = 'model',
               values_to = '.pred')
pred_collected %>% head()
pred_collected %>%
  group_by(model) %>%
  rmse(truth = truth, estimate = .pred) %>%
  select(model, .estimate) %>%
  arrange(.estimate)
pred_collected %>%
  ggplot(aes(x = truth, y = .pred, color = model)) +
  geom_abline(lty = 2, color = "gray80", size = 1.5) +
  geom_point(alpha = 0.5) +
  labs(
    x = "Truth",
    y = "Predicted price",
    color = "Type of model"
  )

Final prediction

So, now we are almost there. Since we know we will use the random forest, we only have to predict on our test sample and see how we fair…

fit_last_rf <- workflow_final_rf %>% last_fit(split = data_split)
fit_last_rf %>% collect_metrics()

Variable importance

fit_last_rf %>% 
  pluck(".workflow", 1) %>%   
  pull_workflow_fit() %>% 
  vip::vip(num_features = 10)

fit_el %>%
  pull_workflow_fit() %>%
  vip::vip(num_features = 10)

ML case 2 (Classification, tabular data): Telco Customer Churn

Data Description

Customer churn refers to the situation when a customer ends their relationship with a company, and it’s a costly problem. Customers are the fuel that powers a business. Loss of customers impacts sales. Further, it’s much more difficult and costly to gain new customers than it is to retain existing customers. As a result, organizations need to focus on reducing customer churn.

The good news is that machine learning can help. For many businesses that offer subscription based services, it’s critical to both predict customer churn and explain what features relate to customer churn.

Data: IBM Watson Dataset

We now dive into the IBM Watson Telco Dataset. According to IBM, the business challenge is.

A telecommunications company [Telco] is concerned about the number of customers leaving their landline business for cable competitors. They need to understand who is leaving. Imagine that you’re an analyst at this company and you have to find out who is leaving and why.

The dataset includes information about:

  • Customers who left within the last month: Churn
  • Services that each customer has signed up for: phone, multiple lines, internet, online security, online backup, device protection, tech support, and streaming TV and movies
  • Customer account information: how long they’ve been a customer, contract, payment method, paperless billing, monthly charges, and total charges
  • Demographic info about customers: gender, age range, and if they have partners and dependents
data <- readRDS(url("https://github.com/SDS-AAU/SDS-master/raw/master/00_data/telco_churn.rds")) # notice that for readRDS i have to wrap the adress in url()
data %>% head()
data %>% glimpse()
Rows: 7,043
Columns: 21
$ customerID       <chr> "7590-VHVEG", "5575-GNVDE", "3668-QPYBK", "7795-CFOCW", "9237-HQITU", "9305-CDSKC", "1452-KIOVK", "6713-OKOMC", "7892-POOKP", "6388-TABGU", "…
$ gender           <chr> "Female", "Male", "Male", "Male", "Female", "Female", "Male", "Female", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "…
$ SeniorCitizen    <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Partner          <chr> "Yes", "No", "No", "No", "No", "No", "No", "No", "Yes", "No", "Yes", "No", "Yes", "No", "No", "Yes", "No", "No", "Yes", "No", "No", "Yes", "N…
$ Dependents       <chr> "No", "No", "No", "No", "No", "No", "Yes", "No", "No", "Yes", "Yes", "No", "No", "No", "No", "Yes", "No", "Yes", "Yes", "No", "No", "No", "No…
$ tenure           <int> 1, 34, 2, 45, 2, 8, 22, 10, 28, 62, 13, 16, 58, 49, 25, 69, 52, 71, 10, 21, 1, 12, 1, 58, 49, 30, 47, 1, 72, 17, 71, 2, 27, 1, 1, 72, 5, 46, …
$ PhoneService     <chr> "No", "Yes", "Yes", "No", "Yes", "Yes", "Yes", "No", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "No"…
$ MultipleLines    <chr> "No phone service", "No", "No", "No phone service", "No", "Yes", "Yes", "No phone service", "Yes", "No", "No", "No", "Yes", "Yes", "No", "Yes…
$ InternetService  <chr> "DSL", "DSL", "DSL", "DSL", "Fiber optic", "Fiber optic", "Fiber optic", "DSL", "Fiber optic", "DSL", "DSL", "No", "Fiber optic", "Fiber opti…
$ OnlineSecurity   <chr> "No", "Yes", "Yes", "Yes", "No", "No", "No", "Yes", "No", "Yes", "Yes", "No internet service", "No", "No", "Yes", "Yes", "No internet service…
$ OnlineBackup     <chr> "Yes", "No", "Yes", "No", "No", "No", "Yes", "No", "No", "Yes", "No", "No internet service", "No", "Yes", "No", "Yes", "No internet service",…
$ DeviceProtection <chr> "No", "Yes", "No", "Yes", "No", "Yes", "No", "No", "Yes", "No", "No", "No internet service", "Yes", "Yes", "Yes", "Yes", "No internet service…
$ TechSupport      <chr> "No", "No", "No", "Yes", "No", "No", "No", "No", "Yes", "No", "No", "No internet service", "No", "No", "Yes", "Yes", "No internet service", "…
$ StreamingTV      <chr> "No", "No", "No", "No", "No", "Yes", "Yes", "No", "Yes", "No", "No", "No internet service", "Yes", "Yes", "Yes", "Yes", "No internet service"…
$ StreamingMovies  <chr> "No", "No", "No", "No", "No", "Yes", "No", "No", "Yes", "No", "No", "No internet service", "Yes", "Yes", "Yes", "Yes", "No internet service",…
$ Contract         <chr> "Month-to-month", "One year", "Month-to-month", "One year", "Month-to-month", "Month-to-month", "Month-to-month", "Month-to-month", "Month-to…
$ PaperlessBilling <chr> "Yes", "No", "Yes", "No", "Yes", "Yes", "Yes", "No", "Yes", "No", "Yes", "No", "No", "Yes", "Yes", "No", "No", "No", "No", "Yes", "Yes", "No"…
$ PaymentMethod    <chr> "Electronic check", "Mailed check", "Mailed check", "Bank transfer (automatic)", "Electronic check", "Electronic check", "Credit card (automa…
$ MonthlyCharges   <dbl> 29.85, 56.95, 53.85, 42.30, 70.70, 99.65, 89.10, 29.75, 104.80, 56.15, 49.95, 18.95, 100.35, 103.70, 105.50, 113.25, 20.65, 106.70, 55.20, 90…
$ TotalCharges     <dbl> 29.85, 1889.50, 108.15, 1840.75, 151.65, 820.50, 1949.40, 301.90, 3046.05, 3487.95, 587.45, 326.80, 5681.10, 5036.30, 2686.05, 7895.15, 1022.…
$ Churn            <chr> "No", "No", "Yes", "No", "Yes", "Yes", "No", "No", "Yes", "No", "No", "No", "No", "Yes", "No", "No", "No", "No", "Yes", "No", "Yes", "No", "Y…
data %<>%
  rename(y = Churn) %>%
  select(y, everything(), -customerID) 

Data Inspecition and Visualization

data %>% summary()
      y                gender          SeniorCitizen      Partner           Dependents            tenure      PhoneService       MultipleLines      InternetService   
 Length:7043        Length:7043        Min.   :0.0000   Length:7043        Length:7043        Min.   : 0.00   Length:7043        Length:7043        Length:7043       
 Class :character   Class :character   1st Qu.:0.0000   Class :character   Class :character   1st Qu.: 9.00   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Median :0.0000   Mode  :character   Mode  :character   Median :29.00   Mode  :character   Mode  :character   Mode  :character  
                                       Mean   :0.1621                                         Mean   :32.37                                                           
                                       3rd Qu.:0.0000                                         3rd Qu.:55.00                                                           
                                       Max.   :1.0000                                         Max.   :72.00                                                           
                                                                                                                                                                      
 OnlineSecurity     OnlineBackup       DeviceProtection   TechSupport        StreamingTV        StreamingMovies      Contract         PaperlessBilling  
 Length:7043        Length:7043        Length:7043        Length:7043        Length:7043        Length:7043        Length:7043        Length:7043       
 Class :character   Class :character   Class :character   Class :character   Class :character   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character  
                                                                                                                                                        
                                                                                                                                                        
                                                                                                                                                        
                                                                                                                                                        
 PaymentMethod      MonthlyCharges    TotalCharges   
 Length:7043        Min.   : 18.25   Min.   :  18.8  
 Class :character   1st Qu.: 35.50   1st Qu.: 401.4  
 Mode  :character   Median : 70.35   Median :1397.5  
                    Mean   : 64.76   Mean   :2283.3  
                    3rd Qu.: 89.85   3rd Qu.:3794.7  
                    Max.   :118.75   Max.   :8684.8  
                                     NA's   :11      

Next, lets have a first visual inspections. Many models in our prediction exercise to follow require the conditional distribution of the features to be different for the outcomes states to be predicted. So, lets take a look. Here, ggplot2 plus the ggridges package is my favorite. It is particularly helpfull when dealing with many variables, where you want to see differences in their conditional distribution with respect to an outcome of interest.

install.packages('ggridges')
Error in install.packages : Updating loaded packages

Data Preprocessing

Training & Test split

data_split <- initial_split(data, prop = 0.75, strata = y)

data_train <- data_split  %>%  training()
data_test <- data_split %>% testing()

Preprocessing recipe

Here, I do the following preprocessing:

  • discretize the tenure variable in 3 bins rather than using the contineous value.
  • apply a logarithmic transformation to TotalCharges
  • center and scale all numerical values
  • transform categoricl vriables to dummies
  • KNN inpute missing valus
data_recipe <- data_train %>%
  recipe(y ~.) %>%
  #step_discretize(tenure, options = list(cuts = 3)) %>%
  step_log(TotalCharges) %>%
  step_center(all_numeric(), -all_outcomes()) %>%
  step_scale(all_numeric(), -all_outcomes()) %>%
  step_dummy(all_nominal(), -all_outcomes()) %>%
  step_knnimpute(all_predictors()) %>% #  knn inputation of missing values
  prep()

Defining the models

Logistic Regression

model_lg <- logistic_reg(mode = 'classification') %>%
  set_engine('glm', family = binomial) 

Decision tree

model_dt <- decision_tree(mode = 'classification',
                          cost_complexity = tune(),
                          tree_depth = tune(), 
                          min_n = tune()
                          ) %>%
  set_engine('rpart') 

Extreme Gradient Boosted Tree (XGBoost)

model_xg <- boost_tree(mode = 'classification', 
                       trees = 100,
                       mtry = tune(), 
                       min_n = tune(), 
                       tree_depth = tune(), 
                       learn_rate = tune()
                       ) %>%
  set_engine("xgboost") 

Define workflow

workflow_general <- workflow() %>%
  add_recipe(data_recipe) 

workflow_lg <- workflow_general %>%
  add_model(model_lg)

workflow_dt <- workflow_general %>%
  add_model(model_dt)

workflow_xg <- workflow_general %>%
  add_model(model_xg)

Hyperparameter Tuning

Validation Sampling (N-fold crossvlidation)

data_resample <- data_train %>% 
  vfold_cv(strata = y,
           v = 3,
           repeats = 3)

Hyperparameter Tuning: Decision Tree

tune_dt <-
  tune_grid(
    workflow_dt,
    resamples = data_resample,
    grid = 10
  )
tune_dt %>% autoplot()

best_param_dt <- tune_dt %>% select_best(metric = 'roc_auc')
best_param_dt
tune_dt %>% show_best(metric = 'roc_auc', n = 1)

Hyperparameter Tuning: Random Forest

tune_xg <-
  tune_grid(
    workflow_xg,
    resamples = data_resample,
    grid = 10
  )
tune_xg %>% autoplot()

best_param_xg <- tune_xg %>% select_best(metric = 'roc_auc')
best_param_xg
tune_xg %>% show_best(metric = 'roc_auc', n = 1)

Fit models with tuned hyperparameters

workflow_final_dt <- workflow_dt %>%
  finalize_workflow(parameters = best_param_dt)

workflow_final_xg <- workflow_xg %>%
  finalize_workflow(parameters = best_param_xg)
fit_lg <- workflow_lg %>%
  fit(data_train)

fit_dt <- workflow_final_dt %>%
  fit(data_train)

fit_xg <- workflow_final_xg %>%
  fit(data_train)

Compare performance

pred_collected <- tibble(
  truth = data_train %>% pull(y) %>% as.factor(),
  #base = mean(truth),
  lg = fit_lg %>% predict(new_data = data_train) %>% pull(.pred_class),
  dt = fit_dt %>% predict(new_data = data_train) %>% pull(.pred_class),
  xg = fit_xg %>% predict(new_data = data_train) %>% pull(.pred_class),
  ) %>% 
  pivot_longer(cols = -truth,
               names_to = 'model',
               values_to = '.pred')
pred_collected %>% head()
pred_collected %>%
  group_by(model) %>%
  accuracy(truth = truth, estimate = .pred) %>%
  select(model, .estimate) %>%
  arrange(desc(.estimate))
pred_collected %>%
  group_by(model) %>%
  bal_accuracy(truth = truth, estimate = .pred) %>%
  select(model, .estimate) %>%
  arrange(desc(.estimate))

Surprisingly, here the less complex model seems to hve the edge!

Final prediction

So, now we are almost there. Since we know we will use the random forest, we only have to predict on our test sample and see how we fair…

fit_last_dt <- workflow_final_dt %>% last_fit(split = data_split)
fit_last_dt %>% collect_metrics()

Variable importance

fit_last_dt %>% 
  pluck(".workflow", 1) %>%   
  pull_workflow_fit() %>% 
  vip::vip(num_features = 10)

fit_xg %>%
  pull_workflow_fit() %>%
  vip::vip(num_features = 10)

Summing up

Endnotes

Packages and Ecosystem

  • tidymodels: Tidy statistical and predictive modeling ecosystem. Full of introductions, examples, and further material

Further Readings

Session info

sessionInfo()
LS0tCnRpdGxlOiAnTWFjaGluZSBMZWFybmluZzogV29ya2Zsb3cgYW5kIEFwcGxpY2F0aW9ucycKYXV0aG9yOiAiRGFuaWVsIFMuIEhhaW4gKGRzaEBidXNpbmVzcy5hYXUuZGspIgpkYXRlOiAiVXBkYXRlZCBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVCICVkLCAlWScpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiBmYWxzZQogICAgdGhlbWU6IGZsYXRseQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQojIyMgR2VuZXJpYyBwcmVhbWJsZQpybShsaXN0PWxzKCkpClN5cy5zZXRlbnYoTEFORyA9ICJlbiIpICMgRm9yIGVuZ2xpc2ggbGFuZ3VhZ2UKb3B0aW9ucyhzY2lwZW4gPSA1KSAjIFRvIGRlYWN0aXZhdGUgYW5ub3lpbmcgc2NpZW50aWZpYyBudW1iZXIgbm90YXRpb24KCiMjIyBLbml0ciBvcHRpb25zCmxpYnJhcnkoa25pdHIpICMgRm9yIGRpc3BsYXkgb2YgdGhlIG1hcmtkb3duCmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBtZXNzYWdlPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBjb21tZW50PUZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgZmlnLmFsaWduPSJjZW50ZXIiCiAgICAgICAgICAgICAgICAgICAgICkKYGBgCgpgYGB7cn0KIyMjIExvYWQgc3RhbmRhcmRwYWNrYWdlcwpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBDb2xsZWN0aW9uIG9mIGFsbCB0aGUgZ29vZCBzdHVmZiBsaWtlIGRwbHlyLCBnZ3Bsb3QyIGVjdC4KbGlicmFyeShtYWdyaXR0cikgIyBGb3IgZXh0cmEtcGlwaW5nIG9wZXJhdG9ycyAoZWcuICU8PiUpCmBgYAoKYGBge3J9CiMgTG9hZCBzcGVjaWZpYyBwYWNrYWdlcwojIGluc3RhbGwucGFja2FnZXMoInRpZHltb2RlbHMiKSAiIEluc3RhbGwgaWYgbmVjZXNzYXJ5CmxpYnJhcnkodGlkeW1vZGVscykKYGBgCgoKV2VsY29tZSBhbGwgdG8gdGhpcyBpbnRyb2R1Y3Rpb24gdG8gbWFjaGluZSBsZWFybmluZyAoTUwpLiBJbiB0aGlzIHNlc3Npb24gd2UgY292ZXIgdGhlIGZvbGxvd2luZyB0b3BpY3MKMS4gR2VuZXJhbGl6YXRpbmcgYW5kIHZhbGlkaWRhdGluZyBmcm9tIE1MIG1vZGVscy4KMi4gVGhlIEJpYXMtVmFyaWFuY2UgVHJhZGUtT2ZmCjMuIE91dC1vZi1zYW1wbGUgdGVzdGluZyBhbmQgY3Jvc3MtdmFsaWRhdGlvbiB3b3JrZmxvd3MKNC4gSW1wbGVtZW50aW5nIE1sIHdvcmtmbG93cyB3aXRoIHRoZSBgdGlkeW1vZGVsc2AgZWNvc3lzdGVtLgoKIyBJbnRyb2R1Y3Rpb24gdG8gTUwgd29ya2Zsb3dzIGluIFIKCiFbXShodHRwczovL3Nkcy1hYXUuZ2l0aHViLmlvL1NEUy1tYXN0ZXIvMDBfbWVkaWEvbWxfdGlkeW1vZGVsc193b3JrZmxvd19sYXJnZS5wbmcpCgpSZW1lYmVyLCB0aGUgc3RlcHMgaW4gdGhlIE1MIHdvcmtmbG93IGFyZToKCjEuIE9idGFpbmluZyBkYXRhCjIuIENsZWFuaW5nIGFuZCBpbnNwZWN0aW5nIAozLiBWaXN1YWxpemluZyBhbmQgZXhwbG9yaW5nIGRhdGEKCjQuIFByZXByb2Nlc3NpbmcgZGF0YQo1LiBGaXRpbmcgYW5kIHR1bmluZyBtb2RlbHMKNi4gVmFsaWRhdGluZyBtb2RlbHMKCjcuIENvbW11bmljYXRpbmcgaW5zaWdodHMKCldoaWxlIHN0ZXAgMS0zIGlzIG1haW5seSBjb3ZlcmVkIGJ5IHRoZSBnZW5lcmFsIGB0aWR5dmVyc2VgIHBhY2thZ2VzIHN1Y2ggYXMgYGRwbHlyYCBhbmQgYGdncGxvdDJgLCBzdGVwIDcgY2FuIGJlIGRvbmUgdXNpbmcgZm9yIGluc3RhbmNlIGBybWFya2Rvd25gIChsaWtlIG1lIGhlcmUpIG9yIGRldmVsb3BpbmcgYW4gaW50ZXJhY3RpdmUgYHNoaW55YCBhcHBsaWNhdGlvbi4gV2Ugd2lsbCB0b3VjaCB1cG9uIHRoYXQsIGJ1dCB0aGUgbWFpbiBmb2N1cyBoZXJlIGxpZXMgaW4gdGhlIHN0ZXBzIDUtNiwgdGhlIGNvcmUgb2YgTUwgd29yay4KClRoZXNlIHN0ZXBzIGFyZSBtYWlubHkgY292ZXJlZCBieSB0aGUgcGFja2FnZXMgdG8gYmUgZm91bmQgaW4gdGhlIFtgdGlkeW1vZGVsc2BdKGh0dHBzOi8vd3d3LnRpZHltb2RlbHMub3JnLykgZWNvc3lzdGVtLCB3aGljaCB0YWtlIGNhcmUgb2Ygc2FtcGxpbmcsIGZpdHRpbmcsIHR1bmluZywgYW5kIGV2YWx1YXRpbmcgbW9kZWxzIGFuZCBkYXRhLgoKIVtdKGh0dHBzOi8vc2RzLWFhdS5naXRodWIuaW8vU0RTLW1hc3Rlci8wMF9tZWRpYS9tbF90aWR5bW9kZWxzX2Zsb3cucG5nKQoKYHRpZHltb2RlbHNgIGlzIGFuIGVjb3N5c3RlbSBvZiBwYWNrYWdlcyB0byBpbXBsZW1lbnQgZWZmaWNpZW50IGFuZCBjb25zaXN0aW5nIFNNTCBtb2RlbGxpbmcgd29ya2Zsb3dzIGNvbnNpc3RlbnQgd2l0aCB0aGUgdGlkeSBwcmluY2lwbGVzIGFuZCBuZWF0aGx5IGZpdHRpbmcgaW50byB0aWR5IHdvcmtmbG93cy4gSXQgY29udGFpbnMgdGhlIGZvbGxvd2luZyBwYWNrYWdlcwoKKiBgcnNhbXBsZWAgcHJvdmlkZXMgaW5mcmFzdHJ1Y3R1cmUgZm9yIGVmZmljaWVudCBkYXRhIHNwbGl0dGluZyBhbmQgcmVzYW1wbGluZy4KKiBgcGFyc25pcGAgaXMgYSB0aWR5LCB1bmlmaWVkIGludGVyZmFjZSB0byBtb2RlbHMgaW5kZXBlbmRlbnQgb2YgdGhlIHBhcnRpY3VsYXIgcGFja2FnZSBzeW50YXguCiogYHJlY2lwZXNgIGlzIGEgdGlkeSBpbnRlcmZhY2UgdG8gZGF0YSBwcmUtcHJvY2Vzc2luZyB0b29scyBmb3IgZmVhdHVyZSBlbmdpbmVlcmluZy4KKiBgd29ya2Zsb3dzYCBidW5kbGUgeW91ciBwcmUtcHJvY2Vzc2luZywgbW9kZWxpbmcsIGFuZCBwb3N0LXByb2Nlc3NpbmcgdG9nZXRoZXIuCiogYHR1bmVgIG9wdGltaXplcyB0aGUgaHlwZXJwYXJhbWV0ZXJzLgoqIGB5YXJkc3RpY2tgIHByb3ZpZGVzIG1vZGVsICBwZXJmb3JtYW5jZSBtZXRyaWNzLgoqIGBicm9vbWAgY29udmVydHMgdGhlIGluZm9ybWF0aW9uIGluIGNvbW1vbiBzdGF0aXN0aWNhbCBSIG9iamVjdHMgaW50byB1c2VyLWZyaWVuZGx5IHRpZHkgZm9ybWF0cy4KKiBgZGlhbHNgIGNyZWF0ZXMgYW5kIG1hbmFnZXMgdHVuaW5nIHBhcmFtZXRlcnMgYW5kIHBhcmFtZXRlciBncmlkcy4KCkkgd2lsbCB0YXAgaW50byBtb3N0IG9mIHRoZW0gZHVyaW5nIHRoaXMgYW5kIGxhdGVyIHNlc3Npb25zLCB0aGVyZWZvcmUgaXQgbWFrZXMgc2Vuc2UgdG8gdXBmcm9udCBsb2FkIHRoIGNvbXBsZXRlIGB0aWR5bW9kZWxzYCBlY29zeXN0ZW0uCgpMZXRzIGdldCBzdGFydGVkLgoKIyBUaGUgdmVyeSBiYXNpY3M6CgojIyBSZWdyZXNzaW9uIHByb2JsZW1zCgpMZXQnIGRvIGEgYnJpZWYgZXhhbXBsZSBmb3IgYSBzaW1wbGUgbGluZWFyIG1vZGVsLiBXZSBnZW5lcmF0ZSBzb21lIGRhdGEsIHdoZXJlICR5JCBpcyBhIGxpbmVhciBmdW5jdGlvbiBvZiAkeCQgcGx1cyBzb21lIHJhbmRvbSBlcnJvci4KCmBgYHtyfQpzZXQuc2VlZCgxMzM3KQpiZXRhMCA9IDE1CmJldGExID0gMC4zCmRhdGFfcmVnIDwtIHRpYmJsZSh4ID0gcnVuaWYoNTAwLCBtaW4gPSAwLCBtYXggPSAxMDApLAogICAgICAgICAgICAgICB5ID0gYmV0YTArIChiZXRhMSp4KSArIHJub3JtKDUwMCwgc2QgPSA1KSkKYGBgCgpgYGB7cn0KZGF0YV9yZWcgJT4lIGdncGxvdChhZXMoeCA9IHgsIHkgPSB5KSkgKyAKICBnZW9tX3BvaW50KCkgKwogIGdlb21fcnVnKHNpemUgPSAwLjEsIGFscGhhID0gMC43NSkgCmBgYApXZSBjYW4gbm93IGZpdCBhIGxpbmVhciByZWdyZXNzaW9uIG1vZGVsIHRoYXQgYWltcyBhdCBkaXNjb3ZlcmluZyB0aGUgdW5kZXJseWluZyByZWxhdGlvbnNoaXAuCgpgYGB7cn0KZml0X2xtIDwtIGRhdGFfcmVnICU+JSBsbShmb3JtdWxhID0geSB+IHgpCmZpdF9sbSAlPiUgc3VtbWFyeSgpCmBgYAoKV2Ugc2VlIGl0IGdvdCB0aGUgdW5kZXJseWluZyByZWxhdGlvbnNoaXAgc29tZXdoYXQgY29ycmVjdC4gS2VlcCBpbiBtaW5kLCBpdHMgYWJpbGl0eSB0byBkaXNjb3ZlciBpdCBpcyBhbHNvIGxpbWl0ZWQgYnkgdGhlIHNtYWxsIHNhbXBsZSwgd2hlcmUgc21hbGwgcmFuZG9tIGVycm9ycyBkYW4gYmlhcyB0aGUgcmVzdWx0LgoKTm90ZTogVGhpcyBpcyBleGFjdGx5IHdoYXQgYGdlb21fc21vb3RoKClgIGluIGBnZ3Bsb3RgIGRvZXMgd2hlbiBnaXZpbmcgaXQgdGhlIGBtZXRob2Q9ImxtImAgcGFyYW1ldGVyLiBMZXRzIHRha2UgYSBsb29rIGF0IGl0IHZpc3VhbGx5LgoKYGBge3J9CmRhdGFfcmVnICU+JSBnZ3Bsb3QoYWVzKHggPSB4LCB5ID0geSkpICsgCiAgZ2VvbV9wb2ludCgpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSAibG0iLCBmb3JtdWxhID0geSB+IHgsIHNlID0gVFJVRSkKYGBgCldlIGNhbiBub3cgdXNlIGBwcmVkaWN0KClgIHRvIHByZWRpY3QgeSB2YWx1ZXMgZHVlIHRvIHRoZSBmaXR0ZWQgbW9kZWwuIAoKYGBge3J9CmRhdGFfcmVnICU8PiUKICBtdXRhdGUocHJlZGljdGVkID0gZml0X2xtICU+JSBwcmVkaWN0KCkpCmBgYAoKCmBgYHtyfQpkYXRhX3JlZyAlPiUgZ2dwbG90KGFlcyh4ID0geCwgeSA9IHkpKSArCiAgZ2VvbV9zZWdtZW50KGFlcyh4ZW5kID0geCwgeWVuZCA9IHByZWRpY3RlZCksIGFscGhhID0gLjIpICsgCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuNSkgKwogIGdlb21fcG9pbnQoYWVzKHkgPSBwcmVkaWN0ZWQpLCBjb2wgPSAncmVkJywgc2hhcGUgPSAyMSkgCmBgYApJdCBvYnZpb3VzbHkgcHJlZGljdHMgYWxvbmcgdGggc3RyYWlnaHQgZnVuY3Rpb24gbGluZS4gRHVlIHRvIHRoZSByYW5kb20gbm9pc2UgaW50cm9kdWNlZCwgaXQgaXMgbW9zdCBvZiB0aGUgdGltZSBvZmYgYSBiaXQuIExldHMgY2FsY3VsYXRlIHRoZSBlcnJvciB0ZXJtCgpgYGB7cn0KZXJyb3JfcmVnIDwtICBwdWxsKGRhdGFfcmVnLCB5KSAtICBwdWxsKGRhdGFfcmVnLCBwcmVkaWN0ZWQpCmBgYAoKYGBge3J9CmVycm9yX3JlZyAlPiUgbWVhbigpCmBgYAoKT24gYXZlcmFnZSB0aGUgZXJyb3IgaXMgdmVyeSBsb3cuIEhvd2V2ZXIsIGtlZXAgaW4gbWluZCBwb3NpdGl2ZSBhbmQgbmVnYXRpdmUgZXJyb3JzIGNhbmNlbCBlYWNoIG90aGVycyBvdXQuIExldHMgbG9vayBhdCB0aGUgUlNNRSBiZXR0ZXIuCgpgYGB7cn0Kc3FydChtZWFuKGVycm9yX3JlZyBeIDIpKSAjIENhbGN1bGF0ZSBSTVNFCmBgYAoKQnR3OiBDb3VsZCBhbHNvIGJlIHBpcGVkLi4uCgpgYGB7cn0KZXJyb3JfcmVnXjIgJT4lIG1lYW4oKSAlPiUgc3FydCgpCmBgYAoKSG93ZXZlciwgd2UgcHJlZGljdGVkIG9uIHRoZSBkYXRhIHRoZSBtb2RlbCB3YXMgZml0dGVkIG9uLiBIb3cgd291bGQgaXQgZmFpciBvbiBuZXcgZGF0YT8KCmBgYHtyfQpzZXQuc2VlZCgxMzM4KQpkYXRhX3JlZ19uZXcgPC0gdGliYmxlKHggPSBydW5pZig1MDAsIG1pbiA9IDAsIG1heCA9IDEwMCksCiAgICAgICAgICAgICAgIHkgPSBiZXRhMCsgKGJldGExKngpICsgcm5vcm0oNTAwLCBzZCA9IDUpKQpgYGAKCmBgYHtyfQpwcmVkX3JlZ19uZXcgPC0gZml0X2xtICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV9yZWdfbmV3KQpgYGAKCmBgYHtyfQplcnJvcl9yZWdfbmV3IDwtIGVycm9yIDwtICBwdWxsKGRhdGFfcmVnX25ldywgeSkgLSAgcHJlZF9yZWdfbmV3CmBgYAoKYGBge3J9CmVycm9yX3JlZ19uZXdeMiAlPiUgbWVhbigpICU+JSBzcXJ0KCkKYGBgCgojIyBDbGFzc2lmaWNhdGlvbiBwcm9ibGVtcwoKT2ssIGxldHMgdHJ5IHRoZSBzYW1lIHdpdGggYSBiaW5hcnkgY2xhc3MgcHJlZGljdGlvbi4gTGV0cyBjcmVhdGUgYSByYW5kb20geCBhbmQgYW4gYXNzb2NpYXRlZCBiaW5hcnkgeS4KCmBgYHtyfQpzZXQuc2VlZCgxMzM3KQpiZXRhMSA8LSA1CgpkYXRhX2NsYXMgPC0gdGliYmxlKAogIHggPSBybm9ybSg1MDApLAogIHkgPSByYmlub20oNTAwLCBzaXplID0gMSwgcHJvYiA9IDEvKDErZXhwKC0oYmV0YTEqeCkpKSApICU+JSBhcy5sb2dpY2FsKCkgJT4lIGZhY3RvcigpCiAgKQpgYGAKCmBgYHtyfQpkYXRhX2NsYXMgJT4lIGhlYWQoKQpgYGAKCmBgYHtyfQpkYXRhX2NsYXMgJT4lCiAgZ2dwbG90KGFlcyh4ID0geCwgeSA9IHkpKSArCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuNSkKYGBgCgpsZXRzIGZpdCBhIGxvZ2lzdGljIHJlZ3Jlc3Npb24gb24gdGhhdAoKYGBge3J9CmZpdF9sb2cgPC0gZGF0YV9jbGFzICU+JQogIGdsbShmb3JtdWxhID0geSB+IHgsIGZhbWlseSA9ICdiaW5vbWlhbCcpCmBgYAoKYGBge3J9CmZpdF9sb2cgJT4lIHN1bW1hcnkoKQpgYGAKCgpXZSBjYW4gYWdhaW4gdmlzdWFsaXplIGl0OgoKYGBge3J9CmRhdGFfY2xhcyAlPiUgCiAgbXV0YXRlKHkgPSB5ICU+JSBhcy5sb2dpY2FsKCkgJT4lIGFzLm51bWVyaWMoKSkgJT4lCiAgZ2dwbG90KGFlcyh4ID0geCwgeSA9IHkpKSArIAogIGdlb21fcG9pbnQoYWxwaGEgPSAwLjUpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSAiZ2xtIiwgbWV0aG9kLmFyZ3MgPSBsaXN0KGZhbWlseSA9ICJiaW5vbWlhbCIpLCBzZSA9IEZBTFNFKSAKYGBgCgpXZSBhZ2FpbiBjYW4gdXNlIHRoaXMgZml0dGVkIG1vZGVsIHRvIHByZWRpY3QgdGhlIGRhdGFwb2ludHMgeS1jbGFzcy4gSGVyZSwgd2UgaGF2ZSB0aGUgY2hvaWNlIHRvIGVpdGhlciByZXBvcnQgdGhlICoqcHJlZGljdGVkIGNsYXNzKiogb3IgdGhlICoqcHJlZGljdGVkIHByb2JhYmlsaXR5KiouIFdlIGhlcmUgZG8gYm90aC4KCmBgYHtyfQpkYXRhX2NsYXMgJTw+JQogIG11dGF0ZShwcmVkaWN0ZWQgPSBmaXRfbG9nICU+JSBwcmVkaWN0KHR5cGUgPSAncmVzcG9uc2UnKSwKICAgICAgICAgcHJlZGljdGVkX2NsYXNzID0gcHJlZGljdGVkICU+JSByb3VuZCgwKSAlPiUgYXMubG9naWNhbCgpICU+JSBmYWN0b3IoKSkKYGBgCgpgYGB7cn0KZGF0YV9jbGFzICU+JSBoZWFkKCkKYGBgCgoKCmBgYHtyfQpjbV9sb2cgPC0gZGF0YV9jbGFzICU+JSBjb25mX21hdCh5LCBwcmVkaWN0ZWRfY2xhc3MpCmBgYAoKYGBge3J9CmNtX2xvZyAlPiUgYXV0b3Bsb3QodHlwZSA9ICJoZWF0bWFwIikKYGBgCmBgYHtyfQpjbV9sb2cgJT4lIHN1bW1hcnkoKSAlPiUgbXV0YXRlKC5lc3RpbWF0ZSA9IC5lc3RpbWF0ZSAlPiUgcm91bmQoMykpICU+JSBzZWxlY3QoLS5lc3RpbWF0b3IpCmBgYAoKYGBge3J9CnJvY19sb2cgPC0gZGF0YV9jbGFzICU+JSAKICByb2NfY3VydmUoeSwgcHJlZGljdGVkLCBldmVudF9sZXZlbCA9ICdzZWNvbmQnKSAKCnJvY19sb2cgJT4lIGhlYWQoKQpgYGAKCmBgYHtyfQpkYXRhX2NsYXMgJT4lIHJvY19hdWMoeSwgcHJlZGljdGVkLCBldmVudF9sZXZlbCA9ICdzZWNvbmQnKSAKYGBgCgpgYGB7cn0Kcm9jX2xvZyAlPiUgYXV0b3Bsb3QoKQpgYGAKQWdhaW4sIGxldHMgY3JlYXRlIHNvbWUgbmV3IGRhdGEgdG8gdGVzdAoKYGBge3J9CnNldC5zZWVkKDEzMzgpCmJldGExIDwtIDUKCmRhdGFfY2xhc19uZXcgPC0gdGliYmxlKAogIHggPSBybm9ybSg1MDApLAogIHkgPSByYmlub20oNTAwLCBzaXplID0gMSwgcHJvYiA9IDEvKDErZXhwKC0oYmV0YTEqeCkpKSApICU+JSBhcy5sb2dpY2FsKCkgJT4lIGZhY3RvcigpCiAgKQpgYGAKCmBgYHtyfQpkYXRhX2NsYXNfbmV3ICU8PiUKICBtdXRhdGUocHJlZGljdGVkID0gZml0X2xvZyAlPiUgcHJlZGljdCh0eXBlID0gJ3Jlc3BvbnNlJywgbmV3ZGF0YSA9IGRhdGFfY2xhc19uZXcpLAogICAgICAgICBwcmVkaWN0ZWRfY2xhc3MgPSBwcmVkaWN0ZWQgJT4lIHJvdW5kKDApICU+JSBhcy5sb2dpY2FsKCkgJT4lIGZhY3RvcigpKQpgYGAKCmBgYHtyfQpjbV9sb2dfbmV3IDwtIGRhdGFfY2xhc19uZXcgJT4lIGNvbmZfbWF0KHksIHByZWRpY3RlZF9jbGFzcykKY21fbG9nX25ldyAlPiUgc3VtbWFyeSgpICU+JSBtdXRhdGUoLmVzdGltYXRlID0gLmVzdGltYXRlICU+JSByb3VuZCgzKSkgJT4lIHNlbGVjdCgtLmVzdGltYXRvcikKZGF0YV9jbGFzICU+JSByb2NfYXVjKHksIHByZWRpY3RlZCwgZXZlbnRfbGV2ZWwgPSAnc2Vjb25kJykgCmBgYAoKCgojIFNNTCB3b3JrZmxvd3MKCk9rLCB0aGF0IGFsbCBub3cgbG9va2VkIGEgYml0IGN1bWJlcnNvbWUuIExldHMgZG8gaXQgYSBiaXQgbW9yZSBhZHZhbmNlZCBhbmQgZmxleGlibGUgaW50cm9kdWNpbmcgdGhlIGB0aWR5bW9kZWxgIE1MIHdvcmtmbG93LiBIZXJlLCB3ZSB3b3VsZCBhcHBseSB0aGUgZm9sbG93aW5nIHN0YW5kYXJkIHdvcmtmbG93OgoKMS4gU3BsaXQgZGF0YXNldCBpbiB0cmFpbmluZyAmIHRlc3Qgc2FtcGxlCiAgICogVGhpcyBpcyBkb25lIHdpdGggdGhlIGByc2FtcGxlYCBmdW5jdGlvbiBgaW5pdGlhbF9zcGxpdCgpYCAKMi4gQXBwbHkgcHJlcHJvY2Vzc2luZyBzdGVwcyBpZiBuZWNlc3NhcnkKICAgKiBDYW4gYmUgZG9uIG1hbnVhbGx5LCBidXQgZm9yIGNvbnZlbmllbmNlIGFuZCByZXByb2R1Y2FiaWxpdHkgYmV0dGVyIGJ5IGRlZmluaW5nIGEgYHJlY2lwZWAKMy4gRGVmaW5lIHRoZSBtb2RlbHMgdG8gZml0CiAgICogRG9uZSBieSBzZXR0aW5nIHVwIGEgbW9kZWwgc3RydWN0dXJlIHdpdGggYHBhcnNuaXBgCjQuIERlZmluZSBhIHJlc2FtcGxpbmcgc3RyYXRlZ3kuCiAgICogV2UgY2hvb3NlIGFtb25nIGRpZmVyZW50IHJlc2FtcGxpbmcgb3B0aW9ucyB3aXRoIHRoZSBgcnNhbXBsZWAgcGFja2FnZQo1LiAoT3B0aW1hbCk6IFR1bmUgSHlwZXJwYXJhbWV0ZXJzLgogICAqIEhlcmUgd2UgdXNlIHRoZSBgdHVuZWAgcGFja2FnZSB0byB0dW5lIGh5cGVycGFyYW1ldGVycyBhbmQgdGhlIGBkaWFsc2AgcGFja2FnZSB0byBtYW5hZ2UgdGhlIGh5cGVycGFyYW1ldGVyIHNlYXJjaAo2LiBTZWxlY3QgdGhlIGJlc3QgcGVyZm9ybWluZyBoeXBlcnBhcmFtZXRlciBzZXR1cC4KNy4gRml0IHRoZSBmaW5hbCBtb2RlbC4KOC4gRXZhbHVhdGUgaXQgb24gdGhlIHRlc3QgZGF0YS4KCgojIE1MIGNhc2UgMSAoUmVncmVzc2lvbiwgdGFidWxhciBkYXRhKTogQm9zdG9uIEhvdXNpbmcgUHJpY2VzCgojIyBEYXRhIERlc2NyaXB0aW9uCgpXZSB3aWxsIGxvYWQgYSBzdGFuZGFyZCBkYXRhc2V0IGZyb20gYG1sYmVuY2hgLCB0aGUgQm9zdG9uSG91c2luZyBkYXRhc2V0LiBJdCBjb21lcyBhcyBhIGRhdGFmcmFtZSB3aXRoIDUwNiBvYnNlcnZhdGlvbnMgb24gMTQgZmVhdHVyZXMsIHRoZSBsYXN0IG9uZSBgbWVkdmAgYmVpbmcgdGhlIG91dGNvbWU6CgoqIGBjcmltYAlwZXIgY2FwaXRhIGNyaW1lIHJhdGUgYnkgdG93bgoqIGB6bmAJcHJvcG9ydGlvbiBvZiByZXNpZGVudGlhbCBsYW5kIHpvbmVkIGZvciBsb3RzIG92ZXIgMjUsMDAwIHNxLmZ0CiogYGluZHVzYAlwcm9wb3J0aW9uIG9mIG5vbi1yZXRhaWwgYnVzaW5lc3MgYWNyZXMgcGVyIHRvd24KKiBgY2hhc2AJQ2hhcmxlcyBSaXZlciBkdW1teSB2YXJpYWJsZSAoPSAxIGlmIHRyYWN0IGJvdW5kcyByaXZlcjsgMCBvdGhlcndpc2UpIChkZXNlbGVjdGVkIGluIHRoaXMgY2FzZSkKKiBgbm94YAluaXRyaWMgb3hpZGVzIGNvbmNlbnRyYXRpb24gKHBhcnRzIHBlciAxMTAgbWlsbGlvbikKKiBgcm1gCWF2ZXJhZ2UgbnVtYmVyIG9mIHJvb21zIHBlciBkd2VsbGluZwoqIGBhZ2VgCXByb3BvcnRpb24gb2Ygb3duZXItb2NjdXBpZWQgdW5pdHMgYnVpbHQgcHJpb3IgdG8gMTk0MAoqIGBkaXNgCXdlaWdodGVkIGRpc3RhbmNlcyB0byBmaXZlIEJvc3RvbiBlbXBsb3ltZW50IGNlbnRyZXMKKiBgcmFkYAlpbmRleCBvZiBhY2Nlc3NpYmlsaXR5IHRvIHJhZGlhbCBoaWdod2F5cwoqIGB0YXhgCWZ1bGwtdmFsdWUgcHJvcGVydHktdGF4IHJhdGUgcGVyIFVTRCAxMCwwMDAKKiBgcHRyYXRpb2AJcHVwaWwtdGVhY2hlciByYXRpbyBieSB0b3duCiogYGJgCTEwMDAoQiAtIDAuNjMpXjIgd2hlcmUgQiBpcyB0aGUgcHJvcG9ydGlvbiBvZiBibGFja3MgYnkgdG93bgoqIGBsc3RhdGAJbG93ZXIgc3RhdHVzIG9mIHRoZSBwb3B1bGF0aW9uCiogYG1lZHZgCW1lZGlhbiB2YWx1ZSBvZiBvd25lci1vY2N1cGllZCBob21lcyBpbiBVU0QgMTAwMCdzIChvdXIgb3V0Y29tZSB0byBwcmVkaWN0KQoKU291cmNlOiBIYXJyaXNvbiwgRC4gYW5kIFJ1YmluZmVsZCwgRC5MLiAiSGVkb25pYyBwcmljZXMgYW5kIHRoZSBkZW1hbmQgZm9yIGNsZWFuIGFpciIsIEouIEVudmlyb24uIEVjb25vbWljcyAmIE1hbmFnZW1lbnQsIHZvbC41LCA4MS0xMDIsIDE5NzguCgpUaGVzZSBkYXRhIGhhdmUgYmVlbiB0YWtlbiBmcm9tIHRoZSBbVUNJIFJlcG9zaXRvcnkgT2YgTWFjaGluZSBMZWFybmluZyBEYXRhYmFzZXNdKGZ0cDovL2Z0cC5pY3MudWNpLmVkdS9wdWIvbWFjaGluZS1sZWFybmluZy1kYXRhYmFzZXMpCgpgYGB7cn0KIyBpbnN0YWxsLnBhY2thZ2VzKCdtbGJlbmNoJykjIEluc3RhbGwgaWYgbmVjZXNzYXJ5IApsaWJyYXJ5KG1sYmVuY2gpICMgTGlicmFyeSBpbmNsdWRpbmcgbWFueSBNTCBiZW5jaG1hcmsgZGF0YXNldHMKZGF0YShCb3N0b25Ib3VzaW5nKSAKZGF0YSA8LSBCb3N0b25Ib3VzaW5nICU+JSBhc190aWJibGUoKSAlPiUgc2VsZWN0KC1jaGFzKQpybShCb3N0b25Ib3VzaW5nKQpgYGAKCmBgYHtyfQpkYXRhICU+JSBoZWFkKCkKYGBgCgpgYGB7cn0KZGF0YSAlPiUgZ2xpbXBzZSgpCmBgYAoKSW4gdGhpcyBleGVyY2lzZSwgd2Ugd2lsbCBwcmVkaWN0IGBtZWR2YCAobWVkaWFuIHZhbHVlIG9mIG93bmVyLW9jY3VwaWVkIGhvbWVzIGluIFVTRCkuIFN1Y2ggYSBtb2RlbCB3b3VsZCBpbiB0aGUgcmVhbCB3b3JsZCBiZSB1c2VkIHRvIHByZWRpY3QgZGV2ZWxvcG1lbnRzIGluIGhvdXNpbmcgcHJpY2VzLCBlZy4gdG8gaW5mb3JtIHBvbGljeSBtYWtlcnMgIG9yIHBvdGVudGlhbCBpbnZlc3RvcnMuIEluIGNhc2UgSSBoYXZlIG9ubHkgb25lIHRhcmdldCBvdXRjb21lLCBJIHByZWZlciB0byBuYW1lIGl0IGFzIGB5YC4gVGhpcyBzaW1wbGUgbmFtaW5nIGNvbnZlbnRpb24gaGVscHMgdG8gcmUtdXNlIGNvZGUgYWNyb3NzIGRhdGFzZXRzLgoKYGBge3J9CmRhdGEgJTw+JSAKICByZW5hbWUoeSA9IG1lZHYpICU+JQogIHJlbG9jYXRlKHkpCmBgYAoKIyMgRGF0YSBJbnNwZWNpdGlvbiBhbmQgVmlzdWFsaXphdGlvbgoKTGV0cyB0YWtlIGEgbG9vayBhdCBzb21lIGRlc2NyaXB0aXZlcy4gCgpgYGB7cn0KZGF0YSAlPiUKICBzdW1tYXJpc2UoYWNyb3NzKGV2ZXJ5dGhpbmcoKSwgbGlzdChtaW4gPSBtaW4sIG1lYW4gPSBtZWFuLG1heCA9IG1heCwgc2QgPSBzZCksIC5uYW1lcyA9ICJ7LmNvbH1fey5mbn0iKSkgJT4lCiAgbXV0YXRlKGFjcm9zcyhldmVyeXRoaW5nKCksIHJvdW5kLCAyKSkgJT4lCiAgcGl2b3RfbG9uZ2VyKGV2ZXJ5dGhpbmcoKSwgCiAgICAgICAgICAgICAgIG5hbWVzX3NlcCA9ICJfIiwKICAgICAgICAgICAgICAgbmFtZXNfdG8gID0gYygidmFyaWFibGUiLCAiLnZhbHVlIikpCmBgYAoKT2ssIHRpbWUgZm9yIHNvbWUgdmlzdWFsIGV4cGxvcmF0aW9uLiBIZXJlIEkgd2lsbCBpbnRyb2R1Y2UgdGhlIGBHR2FsbHlgIHBhY2thZ2UsIGEgd3JhcHBlciBmb3IgYGdncGxvdDJgIHdoaWNoIGhhcyBzb21lIGZ1bmN0aW9ucyBmb3IgdmVyeSBuaWNlIHZpc3VhbCBzdW1tYXJpZXMgaW4gbWF0cml4IGZvcm0uCgpGaXJzdCwgbGV0cyBsb29rIGF0IGEgY2xhc3NpY2FsIGNvcnJlbGF0aW9uIG1hdHJpeC4KCmBgYHtyLGZpZy53aWR0aD03LjUsZmlnLmhlaWdodD03LjUsZmlnLmFsaWduPSdjZW50ZXInfQojIGluc3RhbGwucGFja2FnZXMoJ0dHYWxseScpICMgSW5zdGFsbCBpZiBuZWNlc3NhcnkKZGF0YSAlPiUKICBHR2FsbHk6OmdnY29ycihsYWJlbCA9IFRSVUUsIAogICAgICAgICAgICAgICAgIGxhYmVsX3NpemUgPSAzLCAKICAgICAgICAgICAgICAgICBsYWJlbF9yb3VuZCA9IDIsIAogICAgICAgICAgICAgICAgIGxhYmVsX2FscGhhID0gVFJVRSkKYGBgCgpFdmVuIGNvb2xlciwgdGhlIGBnZ3BhaXJzYCBmdW5jdGlvbiBjcmVhdGVzIHlvdSBhIHNjYXR0ZXJwbG90IG1hdHJpeCBwbHVzIGFsbCB2YXJpYWJsZSBkaXN0cmlidXRpb25zIGFuZCBjb3JyZWxhdGlvbnMuIAoKYGBge3IsZmlnLndpZHRoPTEwLGZpZy5oZWlnaHQ9MTAsZmlnLmFsaWduPSdjZW50ZXInfQpkYXRhICU+JQogIEdHYWxseTo6Z2dwYWlycyhhZXMoYWxwaGEgPSAwLjMpLCAKICAgICAgICAgIGdndGhlbWUgPSB0aGVtZV9ncmF5KCkpICAKYGBgCgoKIyMgRGF0YSBQcmVwcm9jZXNzaW5nCgojIyMgVHJhaW5pbmcgJiBUZXN0IHNwbGl0CgpGaXJzdCwgd2Ugc3BsaXQgb3VyIGRhdGEgaW4gdHJhaW5pbmcgYW5kIHRlc3Qgc2FtcGxlLiBXZSB1c2UgdGhlIGBpbml0aWFsX3NwbGl0YCBmdW5jdGlvbiBvZiB0aGUgYHJzYW1wbGVgIHBja2FnZS4KCmBgYHtyfQpkYXRhX3NwbGl0IDwtIGluaXRpYWxfc3BsaXQoZGF0YSwgcHJvcCA9IDAuNzUsIHN0cmF0YSA9IHkpCgpkYXRhX3RyYWluIDwtIGRhdGFfc3BsaXQgICU+JSAgdHJhaW5pbmcoKQpkYXRhX3Rlc3QgPC0gZGF0YV9zcGxpdCAlPiUgdGVzdGluZygpCmBgYAoKIyMjIFByZXByb2Nlc3NpbmcgcmVjaXBlCgpXZSB1c2UgdGhlIGByZWNpcGVgIHBhY2thZ2UgdG8gYXV0b21hdGl6ZSBhbmQgc3RhbmRhcmRpemUgYWxsIG5lY2Vzc2FyeSBwcmUtcHJvY2Vzc2luZyB3b3JrZmxvd3MuCgpIZXJlLCB3ZSBkbyBvbmx5IHNvbWUgc2ltcGxlIHRyYW5zZm9ybWF0aW9ucy4gCiogV2Ugbm9ybWFsaXplIGFsbCBudW1lcmljIGRhdGEgYnkgY2VudGVyaW5nIChzdWJ0cmFjdGluZyB0aGUgbWVhbikgYW5kIHNjYWxpbmcgKGRpdmlkZSBieSBzdGFuZGFyZCBkZXZpYXRpb24pLiAKKiBXZSByZW1vdmUgZmVhdHVyZXMgd2l0aCBuZWFyLXplcm8tdmFyaWFuY2UsIHdoaWNoIHdvdWxkIG5vdCBoZWxwIHRoZSBtb2RlbCBhIGxvdC4gCiogV2UgaGVyZSBhbHNvIGFkZCBhIHNpbXBsZSB3YXkgdG8gYWxyZWFkeSBpbiB0aGUgcHJlcHJvY2Vzc2luZyBkZWFsIHdpdGggbWlzc2luZyBkYXRhLiBgcmVjaXBlc2AgaGFzIGluYnVpbGQgbWlzc2luZyB2YWx1ZSBpbnB1dGF0aW9uIGFsZ29yaXRobXMsIHN1Y2ggYXMgJ2stbmVhcmVzdC1uZWlnaGJvcnMnLgoKYGBge3J9CmRhdGFfcmVjaXBlIDwtIGRhdGFfdHJhaW4gJT4lCiAgcmVjaXBlKHkgfi4pICU+JQogIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lICMgQ2VudGVycyBhbGwgbnVtZXJpYyB2YXJpYWJsZXMgdG8gbWVhbiA9IDAKICBzdGVwX3NjYWxlKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lICMgc2NhbGVzIGFsbCBudW1lcmljIHZhcmlhYmxlcyB0byBzZCA9IDEKICBzdGVwX256dihhbGxfcHJlZGljdG9ycygpKSAgJT4lICMgUmVtb3ZlZCBwcmVkaWN0b3JzIHdpdGggemVybyB2YXJpYW5jZQogIHN0ZXBfa25uaW1wdXRlKGFsbF9wcmVkaWN0b3JzKCkpICU+JSAjICBrbm4gaW5wdXRhdGlvbiBvZiBtaXNzaW5nIHZhbHVlcwogIHByZXAoKQpgYGAKCmBgYHtyfQpkYXRhX3JlY2lwZQpgYGAKCiMjIyBEZWZpbmluZyB0aGUgbW9kZWxzCgpGaXJzdCBvZiBhbGwsIHdlIHdpbGwgZGVmaW5lIHRoZSBtb2RlbHMgd2Ugd2lsbCBydW4gaGVyZS4gSW4gZGV0YWlsLCB3ZSB3aWxsIHJ1biBhOgoKMS4gT0xTIG1vZGVsIChCYXNlbGluZSkKMi4gRWxhc3RpYyBuZXQgKHN0aWxsIHBhcmFtZXRyaWMsIGJ1dCBtYXliZSBhZHZhbnRhZ2UgaW4gZmVhdHVyZSBzZWxlY3Rpb24pCjMuIFJhbmRvbSBmb3Jlc3QgKHRyZWUtYmFzZWQgZW5zZW1ibGUgbW9kZWwpCgpUaGVyZSBpcyBubyBwYXJ0aWN1bGFyIHJlYXNvbiBvdGhlciB0aGFuIHRvIGRlbW9uc3RyYXRlIGRpZmZlcmVudCBtb2RlbHMgd2l0aCBpbmNyZWFzaW5nIGNvbXBsZXhpdHkgYW5kIGh5cGVycGFyYW1ldGVyIHR1bmluZyBvcHRpb25zLgoKVG8gc2V0IHVwIGEgbW9kZWwgd2l0aCBgcGFyc25pcGAsIHRoZSBmb2xsb3dpbmcgc3ludGF4IGFwcGxpZXM6CgpgYGB7ciwgZXZhbD1GQUxTRX0KbW9kZWxfWFggPC0gbW9kZWxfZmFtaWx5KG1vZGUgPSAncmVncmVzc2lvbi9jbGFzc2lmaWNhdGlvbicsCiAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbWV0ZXJfMSA9IDEyMywKICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFtZXRlcl8yID0gdHVuZSgpKSAlPiUKICBzZXRfZW5naW5lKCdwYWNrYWdlbmFtZScpCmBgYAoKCiMjIyMgTGluZWFyIE1vZGVsIChPTFMpCgpgYGB7cn0KbW9kZWxfbG0gPC0gbGluZWFyX3JlZyhtb2RlID0gJ3JlZ3Jlc3Npb24nKSAlPiUKICBzZXRfZW5naW5lKCdsbScpIApgYGAKCiMjIyMgRWxhc3RpYyBOZXQgKFBlbmFsaXplZCBSZWdyZXNzaW9uKQoKYGBge3J9Cm1vZGVsX2VsIDwtbGluZWFyX3JlZyhtb2RlID0gJ3JlZ3Jlc3Npb24nLCAKICAgICAgICAgICAgICAgICAgICAgIHBlbmFsdHkgPSB0dW5lKCksIAogICAgICAgICAgICAgICAgICAgICAgbWl4dHVyZSA9IHR1bmUoKSkgJT4lCiAgc2V0X2VuZ2luZSgiZ2xtbmV0IikKYGBgCgojIyMjIFJhbmRvbSBGb3Jlc3QKCmBgYHtyfQptb2RlbF9yZiA8LSByYW5kX2ZvcmVzdChtb2RlID0gJ3JlZ3Jlc3Npb24nLAogICAgICAgICAgICAgICAgICAgICAgICB0cmVlcyA9IDI1LAogICAgICAgICAgICAgICAgICAgICAgICBtdHJ5ID0gdHVuZSgpLAogICAgICAgICAgICAgICAgICAgICAgICBtaW5fbiA9IHR1bmUoKQogICAgICAgICAgICAgICAgICAgICAgICApICU+JQogIHNldF9lbmdpbmUoJ3JhbmdlcicsIGltcG9ydGFuY2UgPSAnaW1wdXJpdHknKSAKYGBgCgojIyMjIERlZmluZSB3b3JrZmxvdwoKV2Ugbm93IGRlZmluZSBgd29ya2Zsb3dzYCBieSBwdXR0aW5nIHRoZSBwcmVwcm9jZXNzaW5nIHJlY2lwZSB0b2dldGhlciB3aXRoIHRoZSBjb3JyZXNwb25kaW5nIG1vZGVscy4gTm90IGEgbmVjZXNzYXJ5IHN0ZXAsIGJ1dCBJIGZpbmQgaXQgbmVhdGguCgpgYGB7cn0Kd29ya2Zsb3dfZ2VuZXJhbCA8LSB3b3JrZmxvdygpICU+JQogIGFkZF9yZWNpcGUoZGF0YV9yZWNpcGUpIAoKd29ya2Zsb3dfbG0gPC0gd29ya2Zsb3dfZ2VuZXJhbCAlPiUKICBhZGRfbW9kZWwobW9kZWxfbG0pCgp3b3JrZmxvd19lbCA8LSB3b3JrZmxvd19nZW5lcmFsICU+JQogIGFkZF9tb2RlbChtb2RlbF9lbCkKCndvcmtmbG93X3JmIDwtIHdvcmtmbG93X2dlbmVyYWwgJT4lCiAgYWRkX21vZGVsKG1vZGVsX3JmKQpgYGAKCiMjIyBIeXBlcnBhcmFtZXRlciBUdW5pbmcKCiMjIyMgVmFsaWRhdGlvbiBTYW1wbGluZyAoQm9vdHN0cmFwcGluZykKCiogTm93IGl0IGlzIHRpbWUgdG8gZGVmaW5lIGEgc2FtcGxpbmcgc3RyYXRlZ3kuIEluc3RlYWQgb2YgdGhlICoqay1mb2xkIGNyb3NzdmFsaWRhdGlvbioqIHN0cmF0ZWd5IEkgYWxyZWFkeSBpbnRyb2R1Y2VkIGVhcmxpZXIsIHdlIHdpbGwgaGVyZSB1c2UgYSBib290c3RyYXAgc2FtcGxpbmcgc3RyYXRlZ3kuCiogV2Ugd2lsbCBkcmF3IGEgbnVtYmVyIG9mIG4gcmFuZG9tbHkgc2VsZWN0ZWQgb2JzZXJ2YXRpb25zIGZyb20gdGhlIHNhbXBsZSwgYW5kIHJlcGVhdCB0aGlzIHByb2Nlc3MgNSB0aW1lcy4gCiogVGhhdCBtZWFucyB0aGF0IG91ciBib290c3RyYXBwZWQgc2FtcGxlcyBoYXZlIHRoZSBzYW1lIHNpemUgYXMgdGhlIG9yaWdpbmFsIG9uZS4gVGhpcyBpcyBhIGdvb2QgcmVzYW1wbGluZyBzdHJhdGVneSBpbiBjYXNlIHRoZSBpbml0aWFsIG51bWJlciBvZiBvYnNlcnZhdGlvbnMgaXMgbG93LgoKYGBge3J9CmRhdGFfcmVzYW1wbGUgPC0gYm9vdHN0cmFwcyhkYXRhX3RyYWluLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmF0YSA9IHksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aW1lcyA9IDUpCmBgYAoKYGBge3J9CmRhdGFfcmVzYW1wbGUgJT4lIGdsaW1wc2UoKSAKYGBgCgojIyMjIEh5cGVycGFyYW1ldGVyIFR1bmluZzogRWxhc3RpYyBOZXQKCmBgYHtyfQp0dW5lX2VsIDwtCiAgdHVuZV9ncmlkKAogICAgd29ya2Zsb3dfZWwsCiAgICByZXNhbXBsZXMgPSBkYXRhX3Jlc2FtcGxlLAogICAgZ3JpZCA9IDEwCiAgKQpgYGAKCmBgYHtyfQp0dW5lX2VsICU+JSBhdXRvcGxvdCgpCmBgYAoKYGBge3J9CmJlc3RfcGFyYW1fZWwgPC0gdHVuZV9lbCAlPiUgc2VsZWN0X2Jlc3QobWV0cmljID0gJ3Jtc2UnKQpiZXN0X3BhcmFtX2VsCmBgYAoKYGBge3J9CnR1bmVfZWwgJT4lIHNob3dfYmVzdChtZXRyaWMgPSAncm1zZScsIG4gPSAxKQpgYGAKCiMjIyMgSHlwZXJwYXJhbWV0ZXIgVHVuaW5nOiBSYW5kb20gRm9yZXN0CgpgYGB7cn0KdHVuZV9yZiA8LQogIHR1bmVfZ3JpZCgKICAgIHdvcmtmbG93X3JmLAogICAgcmVzYW1wbGVzID0gZGF0YV9yZXNhbXBsZSwKICAgIGdyaWQgPSAxMAogICkKYGBgCgpgYGB7cn0KdHVuZV9yZiAlPiUgYXV0b3Bsb3QoKQpgYGAKCmBgYHtyfQpiZXN0X3BhcmFtX3JmIDwtIHR1bmVfcmYgJT4lIHNlbGVjdF9iZXN0KG1ldHJpYyA9ICdybXNlJykKYmVzdF9wYXJhbV9yZgpgYGAKCmBgYHtyfQp0dW5lX3JmICU+JSBzaG93X2Jlc3QobWV0cmljID0gJ3Jtc2UnLCBuID0gMSkKYGBgCgoKCiMjIyMgRml0IG1vZGVscyB3aXRoIHR1bmVkIGh5cGVycGFyYW1ldGVycwoKQWxyaWdodCwgbm93IHdlIGNhbiBmaXQgdGhlIGZpbmFsIG1vZGVscy4gVGhlcmVmb3JlLCB3ZSBoYXZlIHRvIGZpcnN0IHVwYXRlIHRoZSBmb3JtZXJseSBjcmVhdGVkIHdvcmtmbG93cywgd2hlcmUgd2UgZmlsbCB0aGUgYHR1bmUoKWAgcGxhY2Vob2xkZXJzIHdpdGggdGhlIGJ5IG5vdyBkZXRlcm1pbmVkIGJlc3QgcGVyZm9ybWluZyBoeXBlcnBhcmFtZXRlciBzZXR1cC4KCmBgYHtyfQp3b3JrZmxvd19maW5hbF9lbCA8LSB3b3JrZmxvd19lbCAlPiUKICBmaW5hbGl6ZV93b3JrZmxvdyhwYXJhbWV0ZXJzID0gYmVzdF9wYXJhbV9lbCkKCndvcmtmbG93X2ZpbmFsX3JmIDwtIHdvcmtmbG93X3JmICU+JQogIGZpbmFsaXplX3dvcmtmbG93KHBhcmFtZXRlcnMgPSBiZXN0X3BhcmFtX3JmKQpgYGAKCmBgYHtyfQpmaXRfbG0gPC0gd29ya2Zsb3dfbG0gJT4lCiAgZml0KGRhdGFfdHJhaW4pCgpmaXRfZWwgPC0gd29ya2Zsb3dfZmluYWxfZWwgJT4lCiAgZml0KGRhdGFfdHJhaW4pCgpmaXRfcmYgPC0gd29ya2Zsb3dfZmluYWxfcmYgJT4lCiAgZml0KGRhdGFfdHJhaW4pCmBgYAoKIyMjIyBDb21wYXJlIHBlcmZvcm1hbmNlCgpgYGB7cn0KcHJlZF9jb2xsZWN0ZWQgPC0gdGliYmxlKAogIHRydXRoID0gZGF0YV90cmFpbiAlPiUgcHVsbCh5KSwKICBiYXNlID0gbWVhbih0cnV0aCksCiAgbG0gPSBmaXRfbG0gJT4lIHByZWRpY3QobmV3X2RhdGEgPSBkYXRhX3RyYWluKSAlPiUgcHVsbCgucHJlZCksCiAgZWwgPSBmaXRfZWwgJT4lIHByZWRpY3QobmV3X2RhdGEgPSBkYXRhX3RyYWluKSAlPiUgcHVsbCgucHJlZCksCiAgcmYgPSBmaXRfcmYgJT4lIHByZWRpY3QobmV3X2RhdGEgPSBkYXRhX3RyYWluKSAlPiUgcHVsbCgucHJlZCksCiAgKSAlPiUgCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSAtdHJ1dGgsCiAgICAgICAgICAgICAgIG5hbWVzX3RvID0gJ21vZGVsJywKICAgICAgICAgICAgICAgdmFsdWVzX3RvID0gJy5wcmVkJykKYGBgCgpgYGB7cn0KcHJlZF9jb2xsZWN0ZWQgJT4lIGhlYWQoKQpgYGAKCgpgYGB7cn0KcHJlZF9jb2xsZWN0ZWQgJT4lCiAgZ3JvdXBfYnkobW9kZWwpICU+JQogIHJtc2UodHJ1dGggPSB0cnV0aCwgZXN0aW1hdGUgPSAucHJlZCkgJT4lCiAgc2VsZWN0KG1vZGVsLCAuZXN0aW1hdGUpICU+JQogIGFycmFuZ2UoLmVzdGltYXRlKQpgYGAKCmBgYHtyfQpwcmVkX2NvbGxlY3RlZCAlPiUKICBnZ3Bsb3QoYWVzKHggPSB0cnV0aCwgeSA9IC5wcmVkLCBjb2xvciA9IG1vZGVsKSkgKwogIGdlb21fYWJsaW5lKGx0eSA9IDIsIGNvbG9yID0gImdyYXk4MCIsIHNpemUgPSAxLjUpICsKICBnZW9tX3BvaW50KGFscGhhID0gMC41KSArCiAgbGFicygKICAgIHggPSAiVHJ1dGgiLAogICAgeSA9ICJQcmVkaWN0ZWQgcHJpY2UiLAogICAgY29sb3IgPSAiVHlwZSBvZiBtb2RlbCIKICApCmBgYAoKIyMjIyBGaW5hbCBwcmVkaWN0aW9uCgpTbywgbm93IHdlIGFyZSBhbG1vc3QgdGhlcmUuIFNpbmNlIHdlIGtub3cgd2Ugd2lsbCB1c2UgdGhlIHJhbmRvbSBmb3Jlc3QsIHdlIG9ubHkgaGF2ZSB0byBwcmVkaWN0IG9uIG91ciB0ZXN0IHNhbXBsZSBhbmQgc2VlIGhvdyB3ZSBmYWlyLi4uCgpgYGB7cn0KZml0X2xhc3RfcmYgPC0gd29ya2Zsb3dfZmluYWxfcmYgJT4lIGxhc3RfZml0KHNwbGl0ID0gZGF0YV9zcGxpdCkKYGBgCgpgYGB7cn0KZml0X2xhc3RfcmYgJT4lIGNvbGxlY3RfbWV0cmljcygpCmBgYAoKIyMjIyBWYXJpYWJsZSBpbXBvcnRhbmNlCgpgYGB7cn0KZml0X2xhc3RfcmYgJT4lIAogIHBsdWNrKCIud29ya2Zsb3ciLCAxKSAlPiUgICAKICBwdWxsX3dvcmtmbG93X2ZpdCgpICU+JSAKICB2aXA6OnZpcChudW1fZmVhdHVyZXMgPSAxMCkKYGBgCgoKYGBge3J9CmZpdF9lbCAlPiUKICBwdWxsX3dvcmtmbG93X2ZpdCgpICU+JQogIHZpcDo6dmlwKG51bV9mZWF0dXJlcyA9IDEwKQpgYGAKCgojIE1MIGNhc2UgMiAoQ2xhc3NpZmljYXRpb24sIHRhYnVsYXIgZGF0YSk6IFRlbGNvIEN1c3RvbWVyIENodXJuCgpgYGB7ciwgaW5jbHVkZT1GQUxTRX0Kcm0obGlzdD1scygpKTsgZ3JhcGhpY3Mub2ZmKCkgIyBnZXQgcmlkIG9mIGV2ZXJ5dGhpbmcgaW4gdGhlIHdvcmtzcGFjZQpgYGAKCiMjIERhdGEgRGVzY3JpcHRpb24KCkN1c3RvbWVyIGNodXJuIHJlZmVycyB0byB0aGUgc2l0dWF0aW9uIHdoZW4gYSBjdXN0b21lciBlbmRzIHRoZWlyIHJlbGF0aW9uc2hpcCB3aXRoIGEgY29tcGFueSwgYW5kIGl0J3MgYSBjb3N0bHkgcHJvYmxlbS4gQ3VzdG9tZXJzIGFyZSB0aGUgZnVlbCB0aGF0IHBvd2VycyBhIGJ1c2luZXNzLiBMb3NzIG9mIGN1c3RvbWVycyBpbXBhY3RzIHNhbGVzLiBGdXJ0aGVyLCBpdCdzIG11Y2ggbW9yZSBkaWZmaWN1bHQgYW5kIGNvc3RseSB0byBnYWluIG5ldyBjdXN0b21lcnMgdGhhbiBpdCBpcyB0byByZXRhaW4gZXhpc3RpbmcgY3VzdG9tZXJzLiBBcyBhIHJlc3VsdCwgb3JnYW5pemF0aW9ucyBuZWVkIHRvIGZvY3VzIG9uIHJlZHVjaW5nIGN1c3RvbWVyIGNodXJuLgoKVGhlIGdvb2QgbmV3cyBpcyB0aGF0IG1hY2hpbmUgbGVhcm5pbmcgY2FuIGhlbHAuIEZvciBtYW55IGJ1c2luZXNzZXMgdGhhdCBvZmZlciBzdWJzY3JpcHRpb24gYmFzZWQgc2VydmljZXMsIGl0J3MgY3JpdGljYWwgdG8gYm90aCBwcmVkaWN0IGN1c3RvbWVyIGNodXJuIGFuZCBleHBsYWluIHdoYXQgZmVhdHVyZXMgcmVsYXRlIHRvIGN1c3RvbWVyIGNodXJuLiAKCiMjIERhdGE6IElCTSBXYXRzb24gRGF0YXNldCAKV2Ugbm93IGRpdmUgaW50byB0aGUgSUJNIFdhdHNvbiBUZWxjbyBEYXRhc2V0LiBBY2NvcmRpbmcgdG8gSUJNLCB0aGUgYnVzaW5lc3MgY2hhbGxlbmdlIGlzLgoKPiBBIHRlbGVjb21tdW5pY2F0aW9ucyBjb21wYW55IFtUZWxjb10gaXMgY29uY2VybmVkIGFib3V0IHRoZSBudW1iZXIgb2YgY3VzdG9tZXJzIGxlYXZpbmcgdGhlaXIgbGFuZGxpbmUgYnVzaW5lc3MgZm9yIGNhYmxlIGNvbXBldGl0b3JzLiBUaGV5IG5lZWQgdG8gdW5kZXJzdGFuZCB3aG8gaXMgbGVhdmluZy4gSW1hZ2luZSB0aGF0IHlvdSdyZSBhbiBhbmFseXN0IGF0IHRoaXMgY29tcGFueSBhbmQgeW91IGhhdmUgdG8gZmluZCBvdXQgd2hvIGlzIGxlYXZpbmcgYW5kIHdoeS4KClRoZSBkYXRhc2V0IGluY2x1ZGVzIGluZm9ybWF0aW9uIGFib3V0OgoKKiBDdXN0b21lcnMgd2hvIGxlZnQgd2l0aGluIHRoZSBsYXN0IG1vbnRoOiBgQ2h1cm5gCiogU2VydmljZXMgdGhhdCBlYWNoIGN1c3RvbWVyIGhhcyBzaWduZWQgdXAgZm9yOiBwaG9uZSwgbXVsdGlwbGUgbGluZXMsIGludGVybmV0LCBvbmxpbmUgc2VjdXJpdHksIG9ubGluZSBiYWNrdXAsIGRldmljZSBwcm90ZWN0aW9uLCB0ZWNoIHN1cHBvcnQsIGFuZCBzdHJlYW1pbmcgVFYgYW5kIG1vdmllcwoqIEN1c3RvbWVyIGFjY291bnQgaW5mb3JtYXRpb246IGhvdyBsb25nIHRoZXkndmUgYmVlbiBhIGN1c3RvbWVyLCBjb250cmFjdCwgcGF5bWVudCBtZXRob2QsIHBhcGVybGVzcyBiaWxsaW5nLCBtb250aGx5IGNoYXJnZXMsIGFuZCB0b3RhbCBjaGFyZ2VzCiogRGVtb2dyYXBoaWMgaW5mbyBhYm91dCBjdXN0b21lcnM6IGdlbmRlciwgYWdlIHJhbmdlLCBhbmQgaWYgdGhleSBoYXZlIHBhcnRuZXJzIGFuZCBkZXBlbmRlbnRzCgoKYGBge3J9CmRhdGEgPC0gcmVhZFJEUyh1cmwoImh0dHBzOi8vZ2l0aHViLmNvbS9TRFMtQUFVL1NEUy1tYXN0ZXIvcmF3L21hc3Rlci8wMF9kYXRhL3RlbGNvX2NodXJuLnJkcyIpKSAjIG5vdGljZSB0aGF0IGZvciByZWFkUkRTIGkgaGF2ZSB0byB3cmFwIHRoZSBhZHJlc3MgaW4gdXJsKCkKYGBgCgpgYGB7cn0KZGF0YSAlPiUgaGVhZCgpCmBgYAoKYGBge3J9CmRhdGEgJT4lIGdsaW1wc2UoKQpgYGAKCgpgYGB7cn0KZGF0YSAlPD4lCiAgcmVuYW1lKHkgPSBDaHVybikgJT4lCiAgc2VsZWN0KHksIGV2ZXJ5dGhpbmcoKSwgLWN1c3RvbWVySUQpIApgYGAKCiMjIERhdGEgSW5zcGVjaXRpb24gYW5kIFZpc3VhbGl6YXRpb24KCmBgYHtyfQpkYXRhICU+JSBzdW1tYXJ5KCkKYGBgCgpOZXh0LCBsZXRzIGhhdmUgYSBmaXJzdCB2aXN1YWwgaW5zcGVjdGlvbnMuIE1hbnkgbW9kZWxzIGluIG91ciBwcmVkaWN0aW9uIGV4ZXJjaXNlIHRvIGZvbGxvdyByZXF1aXJlIHRoZSBjb25kaXRpb25hbCBkaXN0cmlidXRpb24gb2YgdGhlIGZlYXR1cmVzIHRvIGJlIGRpZmZlcmVudCBmb3IgdGhlIG91dGNvbWVzIHN0YXRlcyB0byBiZSBwcmVkaWN0ZWQuIFNvLCBsZXRzIHRha2UgYSBsb29rLiBIZXJlLCBgZ2dwbG90MmAgcGx1cyB0aGUgYGdncmlkZ2VzYCBwYWNrYWdlIGlzIG15IGZhdm9yaXRlLiBJdCBpcyBwYXJ0aWN1bGFybHkgaGVscGZ1bGwgd2hlbiBkZWFsaW5nIHdpdGggbWFueSB2YXJpYWJsZXMsIHdoZXJlIHlvdSB3YW50IHRvIHNlZSBkaWZmZXJlbmNlcyBpbiB0aGVpciBjb25kaXRpb25hbCBkaXN0cmlidXRpb24gd2l0aCByZXNwZWN0IHRvIGFuIG91dGNvbWUgb2YgaW50ZXJlc3QuCgpgYGB7cixmaWcuaGVpZ2h0PTUsZmlnLndpZHRoPTEyLjV9CiMgaW5zdGFsbC5wYWNrYWdlcygnZ2dyaWRnZXMnKSAjIGluc3RhbGwgaWYgbmVjZXNzYXJ5CmRhdGEgJT4lCiAgZ2F0aGVyKHZhcmlhYmxlLCB2YWx1ZSwgLXkpICU+JSAjIE5vdGU6IEF0IG9uZSBwb2ludCBkbyBwaXZvdF9sb25nZXIgaW5zdGVhZAogIGdncGxvdChhZXMoeSA9IGFzLmZhY3Rvcih2YXJpYWJsZSksIAogICAgICAgICAgICAgZmlsbCA9ICBhcy5mYWN0b3IoeSksIAogICAgICAgICAgICAgeCA9IHBlcmNlbnRfcmFuayh2YWx1ZSkpICkgKwogIGdncmlkZ2VzOjpnZW9tX2RlbnNpdHlfcmlkZ2VzKGFscGhhID0gMC43NSkKYGBgCgojIyBEYXRhIFByZXByb2Nlc3NpbmcKCiMjIyBUcmFpbmluZyAmIFRlc3Qgc3BsaXQKCmBgYHtyfQpkYXRhX3NwbGl0IDwtIGluaXRpYWxfc3BsaXQoZGF0YSwgcHJvcCA9IDAuNzUsIHN0cmF0YSA9IHkpCgpkYXRhX3RyYWluIDwtIGRhdGFfc3BsaXQgICU+JSAgdHJhaW5pbmcoKQpkYXRhX3Rlc3QgPC0gZGF0YV9zcGxpdCAlPiUgdGVzdGluZygpCmBgYAoKIyMjIFByZXByb2Nlc3NpbmcgcmVjaXBlCgpIZXJlLCBJIGRvIHRoZSBmb2xsb3dpbmcgcHJlcHJvY2Vzc2luZzoKCiogZGlzY3JldGl6ZSB0aGUgdGVudXJlIHZhcmlhYmxlIGluIDMgYmlucyByYXRoZXIgdGhhbiB1c2luZyB0aGUgY29udGluZW91cyB2YWx1ZS4KKiBhcHBseSBhIGxvZ2FyaXRobWljIHRyYW5zZm9ybWF0aW9uIHRvIGBUb3RhbENoYXJnZXNgCiogY2VudGVyIGFuZCBzY2FsZSBhbGwgbnVtZXJpY2FsIHZhbHVlcwoqIHRyYW5zZm9ybSBjYXRlZ29yaWNsIHZyaWFibGVzIHRvIGR1bW1pZXMKKiBLTk4gaW5wdXRlIG1pc3NpbmcgdmFsdXMKCmBgYHtyfQpkYXRhX3JlY2lwZSA8LSBkYXRhX3RyYWluICU+JQogIHJlY2lwZSh5IH4uKSAlPiUKICBzdGVwX2xvZyhUb3RhbENoYXJnZXMpICU+JQogIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgc3RlcF9zY2FsZShhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICBzdGVwX2tubmltcHV0ZShhbGxfcHJlZGljdG9ycygpKSAlPiUgIyAga25uIGlucHV0YXRpb24gb2YgbWlzc2luZyB2YWx1ZXMKICBwcmVwKCkKYGBgCgoKIyMjIERlZmluaW5nIHRoZSBtb2RlbHMKCiMjIyMgTG9naXN0aWMgUmVncmVzc2lvbgoKYGBge3J9Cm1vZGVsX2xnIDwtIGxvZ2lzdGljX3JlZyhtb2RlID0gJ2NsYXNzaWZpY2F0aW9uJykgJT4lCiAgc2V0X2VuZ2luZSgnZ2xtJywgZmFtaWx5ID0gYmlub21pYWwpIApgYGAKCiMjIyMgRGVjaXNpb24gdHJlZQoKYGBge3J9Cm1vZGVsX2R0IDwtIGRlY2lzaW9uX3RyZWUobW9kZSA9ICdjbGFzc2lmaWNhdGlvbicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgY29zdF9jb21wbGV4aXR5ID0gdHVuZSgpLAogICAgICAgICAgICAgICAgICAgICAgICAgIHRyZWVfZGVwdGggPSB0dW5lKCksIAogICAgICAgICAgICAgICAgICAgICAgICAgIG1pbl9uID0gdHVuZSgpCiAgICAgICAgICAgICAgICAgICAgICAgICAgKSAlPiUKICBzZXRfZW5naW5lKCdycGFydCcpIApgYGAKCiMjIyMgRXh0cmVtZSBHcmFkaWVudCBCb29zdGVkIFRyZWUgKFhHQm9vc3QpCgpgYGB7cn0KbW9kZWxfeGcgPC0gYm9vc3RfdHJlZShtb2RlID0gJ2NsYXNzaWZpY2F0aW9uJywgCiAgICAgICAgICAgICAgICAgICAgICAgdHJlZXMgPSAxMDAsCiAgICAgICAgICAgICAgICAgICAgICAgbXRyeSA9IHR1bmUoKSwgCiAgICAgICAgICAgICAgICAgICAgICAgbWluX24gPSB0dW5lKCksIAogICAgICAgICAgICAgICAgICAgICAgIHRyZWVfZGVwdGggPSB0dW5lKCksIAogICAgICAgICAgICAgICAgICAgICAgIGxlYXJuX3JhdGUgPSB0dW5lKCkKICAgICAgICAgICAgICAgICAgICAgICApICU+JQogIHNldF9lbmdpbmUoInhnYm9vc3QiKSAKYGBgCgojIyMjIERlZmluZSB3b3JrZmxvdwoKYGBge3J9CndvcmtmbG93X2dlbmVyYWwgPC0gd29ya2Zsb3coKSAlPiUKICBhZGRfcmVjaXBlKGRhdGFfcmVjaXBlKSAKCndvcmtmbG93X2xnIDwtIHdvcmtmbG93X2dlbmVyYWwgJT4lCiAgYWRkX21vZGVsKG1vZGVsX2xnKQoKd29ya2Zsb3dfZHQgPC0gd29ya2Zsb3dfZ2VuZXJhbCAlPiUKICBhZGRfbW9kZWwobW9kZWxfZHQpCgp3b3JrZmxvd194ZyA8LSB3b3JrZmxvd19nZW5lcmFsICU+JQogIGFkZF9tb2RlbChtb2RlbF94ZykKYGBgCgojIyMgSHlwZXJwYXJhbWV0ZXIgVHVuaW5nCgojIyMjIFZhbGlkYXRpb24gU2FtcGxpbmcgKE4tZm9sZCBjcm9zc3ZsaWRhdGlvbikKCmBgYHtyfQpkYXRhX3Jlc2FtcGxlIDwtIGRhdGFfdHJhaW4gJT4lIAogIHZmb2xkX2N2KHN0cmF0YSA9IHksCiAgICAgICAgICAgdiA9IDMsCiAgICAgICAgICAgcmVwZWF0cyA9IDMpCmBgYAoKIyMjIyBIeXBlcnBhcmFtZXRlciBUdW5pbmc6IERlY2lzaW9uIFRyZWUKCmBgYHtyfQp0dW5lX2R0IDwtCiAgdHVuZV9ncmlkKAogICAgd29ya2Zsb3dfZHQsCiAgICByZXNhbXBsZXMgPSBkYXRhX3Jlc2FtcGxlLAogICAgZ3JpZCA9IDEwCiAgKQpgYGAKCmBgYHtyfQp0dW5lX2R0ICU+JSBhdXRvcGxvdCgpCmBgYAoKYGBge3J9CmJlc3RfcGFyYW1fZHQgPC0gdHVuZV9kdCAlPiUgc2VsZWN0X2Jlc3QobWV0cmljID0gJ3JvY19hdWMnKQpiZXN0X3BhcmFtX2R0CmBgYAoKYGBge3J9CnR1bmVfZHQgJT4lIHNob3dfYmVzdChtZXRyaWMgPSAncm9jX2F1YycsIG4gPSAxKQpgYGAKCiMjIyMgSHlwZXJwYXJhbWV0ZXIgVHVuaW5nOiBSYW5kb20gRm9yZXN0CgpgYGB7cn0KdHVuZV94ZyA8LQogIHR1bmVfZ3JpZCgKICAgIHdvcmtmbG93X3hnLAogICAgcmVzYW1wbGVzID0gZGF0YV9yZXNhbXBsZSwKICAgIGdyaWQgPSAxMAogICkKYGBgCgpgYGB7cn0KdHVuZV94ZyAlPiUgYXV0b3Bsb3QoKQpgYGAKCmBgYHtyfQpiZXN0X3BhcmFtX3hnIDwtIHR1bmVfeGcgJT4lIHNlbGVjdF9iZXN0KG1ldHJpYyA9ICdyb2NfYXVjJykKYmVzdF9wYXJhbV94ZwpgYGAKCmBgYHtyfQp0dW5lX3hnICU+JSBzaG93X2Jlc3QobWV0cmljID0gJ3JvY19hdWMnLCBuID0gMSkKYGBgCgoKIyMjIyBGaXQgbW9kZWxzIHdpdGggdHVuZWQgaHlwZXJwYXJhbWV0ZXJzCgoKYGBge3J9CndvcmtmbG93X2ZpbmFsX2R0IDwtIHdvcmtmbG93X2R0ICU+JQogIGZpbmFsaXplX3dvcmtmbG93KHBhcmFtZXRlcnMgPSBiZXN0X3BhcmFtX2R0KQoKd29ya2Zsb3dfZmluYWxfeGcgPC0gd29ya2Zsb3dfeGcgJT4lCiAgZmluYWxpemVfd29ya2Zsb3cocGFyYW1ldGVycyA9IGJlc3RfcGFyYW1feGcpCmBgYAoKYGBge3J9CmZpdF9sZyA8LSB3b3JrZmxvd19sZyAlPiUKICBmaXQoZGF0YV90cmFpbikKCmZpdF9kdCA8LSB3b3JrZmxvd19maW5hbF9kdCAlPiUKICBmaXQoZGF0YV90cmFpbikKCmZpdF94ZyA8LSB3b3JrZmxvd19maW5hbF94ZyAlPiUKICBmaXQoZGF0YV90cmFpbikKYGBgCgojIyMjIENvbXBhcmUgcGVyZm9ybWFuY2UKCmBgYHtyfQpwcmVkX2NvbGxlY3RlZCA8LSB0aWJibGUoCiAgdHJ1dGggPSBkYXRhX3RyYWluICU+JSBwdWxsKHkpICU+JSBhcy5mYWN0b3IoKSwKICAjYmFzZSA9IG1lYW4odHJ1dGgpLAogIGxnID0gZml0X2xnICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV90cmFpbikgJT4lIHB1bGwoLnByZWRfY2xhc3MpLAogIGR0ID0gZml0X2R0ICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV90cmFpbikgJT4lIHB1bGwoLnByZWRfY2xhc3MpLAogIHhnID0gZml0X3hnICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV90cmFpbikgJT4lIHB1bGwoLnByZWRfY2xhc3MpLAogICkgJT4lIAogIHBpdm90X2xvbmdlcihjb2xzID0gLXRydXRoLAogICAgICAgICAgICAgICBuYW1lc190byA9ICdtb2RlbCcsCiAgICAgICAgICAgICAgIHZhbHVlc190byA9ICcucHJlZCcpCmBgYAoKYGBge3J9CnByZWRfY29sbGVjdGVkICU+JSBoZWFkKCkKYGBgCgoKYGBge3J9CnByZWRfY29sbGVjdGVkICU+JQogIGdyb3VwX2J5KG1vZGVsKSAlPiUKICBhY2N1cmFjeSh0cnV0aCA9IHRydXRoLCBlc3RpbWF0ZSA9IC5wcmVkKSAlPiUKICBzZWxlY3QobW9kZWwsIC5lc3RpbWF0ZSkgJT4lCiAgYXJyYW5nZShkZXNjKC5lc3RpbWF0ZSkpCmBgYAoKYGBge3J9CnByZWRfY29sbGVjdGVkICU+JQogIGdyb3VwX2J5KG1vZGVsKSAlPiUKICBiYWxfYWNjdXJhY3kodHJ1dGggPSB0cnV0aCwgZXN0aW1hdGUgPSAucHJlZCkgJT4lCiAgc2VsZWN0KG1vZGVsLCAuZXN0aW1hdGUpICU+JQogIGFycmFuZ2UoZGVzYyguZXN0aW1hdGUpKQpgYGAKClN1cnByaXNpbmdseSwgaGVyZSB0aGUgbGVzcyBjb21wbGV4IG1vZGVsIHNlZW1zIHRvIGh2ZSB0aGUgZWRnZSEKCiMjIyMgRmluYWwgcHJlZGljdGlvbgoKU28sIG5vdyB3ZSBhcmUgYWxtb3N0IHRoZXJlLiBTaW5jZSB3ZSBrbm93IHdlIHdpbGwgdXNlIHRoZSByYW5kb20gZm9yZXN0LCB3ZSBvbmx5IGhhdmUgdG8gcHJlZGljdCBvbiBvdXIgdGVzdCBzYW1wbGUgYW5kIHNlZSBob3cgd2UgZmFpci4uLgoKYGBge3J9CmZpdF9sYXN0X2R0IDwtIHdvcmtmbG93X2ZpbmFsX2R0ICU+JSBsYXN0X2ZpdChzcGxpdCA9IGRhdGFfc3BsaXQpCmBgYAoKYGBge3J9CmZpdF9sYXN0X2R0ICU+JSBjb2xsZWN0X21ldHJpY3MoKQpgYGAKCiMjIyMgVmFyaWFibGUgaW1wb3J0YW5jZQoKYGBge3J9CmZpdF9sYXN0X2R0ICU+JSAKICBwbHVjaygiLndvcmtmbG93IiwgMSkgJT4lICAgCiAgcHVsbF93b3JrZmxvd19maXQoKSAlPiUgCiAgdmlwOjp2aXAobnVtX2ZlYXR1cmVzID0gMTApCmBgYAoKCmBgYHtyfQpmaXRfeGcgJT4lCiAgcHVsbF93b3JrZmxvd19maXQoKSAlPiUKICB2aXA6OnZpcChudW1fZmVhdHVyZXMgPSAxMCkKYGBgCgojIFN1bW1pbmcgdXAKCiMgRW5kbm90ZXMKCiMjIyBSZWZlcmVuY2VzCgoqIFtIYWluLCBELiwgJiBKdXJvd2V0emtpLCBSLiAoMjAyMCkuIEludHJvZHVjdGlvbiB0byBSYXJlLUV2ZW50IFByZWRpY3RpdmUgTW9kZWxpbmcgZm9yIEluZmVyZW50aWFsIFN0YXRpc3RpY2lhbnMtLUEgSGFuZHMtT24gQXBwbGljYXRpb24gaW4gdGhlIFByZWRpY3Rpb24gb2YgQnJlYWt0aHJvdWdoIFBhdGVudHMuIGFyWGl2IHByZXByaW50IGFyWGl2OjIwMDMuMTM0NDEuXShodHRwczovL2FyeGl2Lm9yZy9hYnMvMjAwMy4xMzQ0MSk6IFNvbWUgb2Ygb3VyIGludHJvZHVjdG9yeSBwYXBlcnMuIEFuIGEgYml0IG1vcmUgZWxhYm9yYXRlIHZlcnNpb24gb2Ygd2hhdCB3ZSBkaWQgc28gZmFyIG9uIGEgbW9yZSBleGNpdGluZyBkYXRhc2V0LgoKIyMjIFBhY2thZ2VzIGFuZCBFY29zeXN0ZW0KCiogW2B0aWR5bW9kZWxzYF0oaHR0cHM6Ly93d3cudGlkeW1vZGVscy5vcmcvKTogVGlkeSBzdGF0aXN0aWNhbCBhbmQgcHJlZGljdGl2ZSBtb2RlbGluZyBlY29zeXN0ZW0uIEZ1bGwgb2YgaW50cm9kdWN0aW9ucywgZXhhbXBsZXMsIGFuZCBmdXJ0aGVyIG1hdGVyaWFsCgojIyMgRnVydGhlciBSZWFkaW5ncwoKKiBbSnVsaWEgU2lsZ2VzIFNNTCBjYXNlIHN0dWR5IG9ubGluZSBjb3Vyc2VdKGh0dHBzOi8vc3VwZXJ2aXNlZC1tbC1jb3Vyc2UubmV0bGlmeS5hcHAvKTogR1JlYXQgY291cnNlIEp1bGlhIHRvb2sgb3V0IG9mIERhdGFDYW1wIHRvIG9mZmVyIGl0IGZvciBmcmVlIGluc3RlYWQuIEZ1bGx5IHVwZGF0ZWQgdG8gdGhlIHRpZHltb2RlbHMgd29ya2Zsb3cuIFlPVSBBTEwgU0hPVUxEIERPIElUIQoqIERhdGFjYW1wOiAhISEgV2FybmluZzogR29vZCB0byBnZXQgdGhlIGNvbmNlcHRzLCBidXQgb2Z0ZW4gdXNpbmcgYGNhcmV0YCBhbmQgb3RoZXIgc2xvd2x5IGRlY2xpbmluZyBNTCBwYWNrYWdlIGVjb3N5c3RlbXMuCiAgICogW01hY2hpbmUgTGVhcm5pbmcgaW4gdGhlIFRpZHl2ZXJzZV0oaHR0cHM6Ly9sZWFybi5kYXRhY2FtcC5jb20vY291cnNlcy9tYWNoaW5lLWxlYXJuaW5nLWluLXRoZS10aWR5dmVyc2UpOiBHb29kIGNvdXJzZSB0byBnZXQgc3RhcnRlZCB3aXRoIHRpZHkgTUwuIAogICAqIFtTdXBlcnZpc2VkIExlYXJuaW5nIGluIFI6IFJlZ3Jlc3Npb25dKGh0dHBzOi8vbGVhcm4uZGF0YWNhbXAuY29tL2NvdXJzZXMvc3VwZXJ2aXNlZC1sZWFybmluZy1pbi1yLXJlZ3Jlc3Npb24pOiBEcmlsbHMgZGVlcGVyIGluIHRvIHJlZ3Jlc3Npb24gbW9kZWxzLgogICAqIFtTdXBlcnZpc2VkIExlYXJuaW5nIGluIFI6IENsYXNzaWZpY2F0aW9uXShodHRwczovL2xlYXJuLmRhdGFjYW1wLmNvbS9jb3Vyc2VzL3N1cGVydmlzZWQtbGVhcm5pbmctaW4tci1jbGFzc2lmaWNhdGlvbik6IERyaWxscyBkZWVwZXIgaW4gdG8gcmVncmVzc2lvbiBtb2RlbHMuIAogICAqIFtIeXBlcnBhcmFtZXRlciBUdW5pbmcgaW4gUl0oaHR0cHM6Ly9sZWFybi5kYXRhY2FtcC5jb20vY291cnNlcy9oeXBlcnBhcmFtZXRlci10dW5pbmctaW4tcik6IEFkdmFuY2VkIHR1bmluZyBzZXR1cHMuCiAgICogW0ZlYXR1cmUgRW5naW5lZXJpbmcgaW4gUl0oaHR0cHM6Ly9sZWFybi5kYXRhY2FtcC5jb20vY291cnNlcy9mZWF0dXJlLWVuZ2luZWVyaW5nLWluLXIpOiBBZHZhbmNlZCBmZWF0dXJlIGVuZ2luZWVyaW5nLgogICAqIFtDYXJlZXIgVHJhY2s6IG1hY2hpbiBMZWFybmluZyBTY2llbnRpc3QgaW4gUl0oaHR0cHM6Ly9sZWFybi5kYXRhY2FtcC5jb20vY2FyZWVyLXRyYWNrcy9tYWNoaW5lLWxlYXJuaW5nLXNjaWVudGlzdC13aXRoLXIpOiBGb3IgdGhvc2Ugd2hvIHdhbnQgaXQgYWxsIQoqIGVib29rcyBldGMKICAgKiBJc21heSAmIEtpbSAoMjAyMCksIFtTdGF0aXN0aWNhbCBJbmZlcmVuY2UgdmlhIERhdGEgU2NpZW5jZTogQSBNb2Rlcm5EaXZlIGludG8gUiBhbmQgdGhlIFRpZHl2ZXJzZV0oaHR0cHM6Ly9tb2Rlcm5kaXZlLmNvbS8pLCBDUkMgUHJlc3MuIEZvciB0aG9zZSB3aG8gd2FudCB0byBmaXJzdCB1cGRhdGUgdGhlaXIga25vd2xlZGdlIGluIGJhc2ljIGFuZCBpbmZlcmVudGlhbCBzdGF0aXN0aWNzIGluIGEgbW9kZXJuIFIgc2V0dXAuCiAgICogS3VobiAmIEpvaG5zb24gKDIwMTkpLCBbRmVhdHVyZSBFbmdpbmVlcmluZyBhbmQgU2VsZWN0aW9uOiBBIFByYWN0aWNhbCBBcHByb2FjaCBmb3IgUHJlZGljdGl2ZSBNb2RlbHNdKGh0dHBzOi8vYm9va2Rvd24ub3JnL21heC9GRVMvKSwgVGF5bG9yICYgRnJhbmNpcy4gTGVzcyBjb2RlIGJ1dCBtdWNoIGRlZXAgaW5zaWdodHMgaW4gbW9kZXJuIE1MIGRldGFpbHMsIGJ5IFRob21hcyBLdWhuLCB0aGUgbWFrZXIgb2YgbXVjaCBvZiBgdGlkeW1vZGVsc2AgYW5kIGBjYXJldGAKCiMjIyBTZXNzaW9uIGluZm8KYGBge3J9CnNlc3Npb25JbmZvKCkKYGBgCg==