Motivation

Post 1 cho thấy việc sử dụng AUC để lựa chọn biến số cho mô hình Logistic để đạt được AUC = 0.903 trên Test Data chỉ bằng sử dụng hồi quy Logistic là một kết quả tương đối tốt. Nếu sử dụng danh sách các biến được lựa chọn để huấn luyện Automated Machine Learning thì chúng ta có thể đạt kết quả còn tốt hơn với AUC = 0.9181.

Post 2 chỉ ra rằng việc lựa chọn biến số dựa trên Information Value có thể đạt được kết quả còn cao hơn nữa (AUC = 0.937021 trên Test Data). Kết quả này thua kém không nhiều so với AUC đạt được của Team xếp thứ nhất (Team KotaShimomura có AUC = 0.93798).

Với cố gắng vượt qua được kết quả của Team KotaShimomura, post này sẽ sử dụng Monotonic Optimal Binning - MOB như là một kĩ thuật cho Feature Selection. Cụ thể từ 94 biến số ban đầu sẽ chỉ lựa chọn ra 41 biến số tiềm năng (đã được “rời rạc hóa”) dựa trên IV từ MOB. Sau đó từ danh sách các biến tiềm năng này lại chọn ra sự kết hợp tối ưu nhất các biến bằng Stepwise Selection (bạn đọc quan tâm có thể tìm hiểu thêm về thuật toán này tại trang 229 cuốn An Introduction to Statistical Learning). Kết quả thực nghiệm cho thấy chúng ta có thể đạt AUC = 0.9383908 trên Test Data. Danh sách Team dẫn đầu ở Private Score là Team KotaShimomura với AUC = 0.93798.

Vẫn như ở hai post trước, post này vẫn sử dụng bộ dữ liệu đã được sử dụng cho cuộc thi Corporate Bankruptcy Prediction 2021 trên Kaggle (có thể download tại đây).

Short Introduction to Monotonic Optimal Binning

MOB là một kĩ thuật “rời rạc hóa” được sử dụng phổ biến khi mô hình hóa rủi ro và phân loại-xếp hạng tín dụng (như PD, LGD, EAD). Ưu điểm của nó là làm cho mô hình dễ giải thích và nhất quán - một yêu cầu thường giữ vị trí quan trọng tại các tổ chức tài chính như ngân hàng.

MOB có thể được thực hiện bằng nhiều R package khác nhau. Trong post này chúng ta sử dụng thư viện mob của Liu. Dưới đây là R codes thực hiện MOB cho biến OperatingGrossMargin:

# Clear our R environment: 
rm(list = ls())

# Load tidyverse package: 
library(tidyverse)

# Load data: 
read_csv("F:/data.csv/data.csv") -> data

# Rename for all columns: 

old_names <- names(data)

old_names %>% str_replace_all("[^a-z|^A-Z]", "") -> new_names

names(data) <- new_names

# MOB for OperatingProfitRate: 

library(mob)

bin_result <- iso_bin(data$OperatingGrossMargin, data$Bankrupt)

# Decreasing event rates (or Bankrupt rate): 

bin_result$tbl %>%
  select(-rule) %>%
  knitr::kable() 
bin freq miss bads rate woe iv ks
1 15 0 4 0.2667 2.3894 0.0395 1.65
2 9 0 2 0.2222 2.1483 0.0173 2.45
3 99 0 19 0.1919 1.9635 0.1458 9.88
4 90 0 15 0.1667 1.7916 0.1018 15.56
5 130 0 19 0.1462 1.6360 0.1138 22.51
6 54 0 6 0.1111 1.3216 0.0264 24.51
7 71 0 7 0.0986 1.1881 0.0263 26.73
8 13 0 1 0.0769 0.9161 0.0025 27.00
9 934 0 50 0.0535 0.5286 0.0493 36.33
10 277 0 12 0.0433 0.3062 0.0044 37.77
11 576 0 21 0.0365 0.1266 0.0014 38.90
12 55 0 2 0.0364 0.1239 0.0001 39.01
13 804 0 22 0.0274 -0.1698 0.0031 37.16
14 1863 0 21 0.0113 -1.0730 0.1971 18.79
15 1379 0 15 0.0109 -1.1091 0.1536 4.94
16 309 0 3 0.0097 -1.2239 0.0401 1.67
17 141 0 1 0.0071 -1.5406 0.0257 0.00

Biến OperatingGrossMargin sẽ được “rời rạc hóa” thành 17 khoảng khác nhau sao cho tỉ lệ Bankrupt là đơn điệu giảm. Ví dụ, ở khoảng OperatingGrossMargin <= 0.5408913 thì Bankrupt Rate = 0.2667 là cao nhất. Tại các khoảng tăng dần của OperatingGrossMargin thì Bankrupt Rate cũng giảm dần một cách đơn điệu.

IV from Monotonic Binning for Feature Selection

Monotonic Binning có thể được thực hiện bằng nhiều thuật toán. Từ đơn giản như K-mean Clustering cho đến việc sử dụng các thuật toán Machine Learning như XGBoost. Post này sử dụng cách tiếp cận isotonic regression để thực hiện MOB với hàm iso_bin(). Chúng ta khảo sát AUC trên Test Data nếu mô hình Logistic chỉ sử dụng chỉ một biến số sau khi đã được rời rạc hóa. Dưới đây là R codes:

# Remove NetIncomeFlag column: 
data %>% select(-NetIncomeFlag) -> df

# Set response and predictors: 

response <- "Bankrupt"

predictors <- names(df %>% select(-Bankrupt))

# Split our data: 

library(caret)

set.seed(1)

id <- createDataPartition(y = df %>% pull(response), p = 0.7, list = FALSE)

# 70% data for training và validation: 
train_valid <- df[id, ] 

# 30% data will be used for evaluating model performance: 

df_test <- df[-id, ] # Test data.  

set.seed(1)

id_new <- createDataPartition(y = train_valid %>% pull(response), p = nrow(df_test) / nrow(train_valid), list = FALSE)

df_train <- train_valid[-id_new, ] # Train data. 

df_valid <- train_valid[id_new, ] # Validation data. 

# Discretize selected variables: 

n_var <- length(predictors)

auc_space <- NULL

iv_space <- NULL

library(pROC)

for (j in 1:n_var) {
  
  var_j <- predictors[j]
  
  var_j_new <- str_c(var_j, "_woe")
  
  x <- df_train %>% pull(var_j)
  
  y <- df_train$Bankrupt
  
  bin_result_j <- iso_bin(x, y)
  
  # Calculate IV: 
  
  bin_result_j$tbl %>% pull(iv) %>% sum() -> iv_j
  
  iv_space <- c(iv_space, iv_j)
  
  # Discretize variable: 
  
  cal_woe(x, bin_result_j) -> var_woe_j
  
  #------------------------------------
  # Train Logistic on Discretized Data
  #------------------------------------
  
  df_train %>% 
    mutate(Bankrupt = case_when(Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt") %>% as.factor()) %>% 
    pull(Bankrupt) -> label_train
  
  df_j <- data.frame(Bankrupt = label_train, var_woe = var_woe_j)
  
  logit <- glm(Bankrupt ~., family = "binomial", data = df_j)
  
  #-----------------------
  # PD on Validation data
  #-----------------------
  
  df_valid %>% 
    mutate(Bankrupt = case_when(Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt") %>% as.factor()) %>% 
    pull(Bankrupt) -> label_valid
  
  df_valid_j <- data.frame(var_woe =  cal_woe(df_valid %>% pull(var_j), bin_result_j))
  
  prob_pred <- predict(logit, df_valid_j, type = "response")
  
  # AUC on Validation Data: 
  
  my_auc <- roc(label_valid, prob_pred)$auc %>% as.numeric()
  
  auc_space <- c(auc_space, my_auc)
  
}


df_iv_single_var <- data.frame(variable = predictors, iv = iv_space, auc = auc_space)

# Some Results: 

df_iv_single_var %>% 
  arrange(-iv) %>% 
  head() %>% 
  knitr::kable()
variable iv auc
PersistentEPSintheLastFourSeasons 3.2601 0.8932600
NetprofitbeforetaxPaidincapital 3.1445 0.8844458
PerShareNetprofitbeforetaxYuan 3.0799 0.8849536
NetIncometoTotalAssets 3.0300 0.8577858
RetainedEarningstoTotalAssets 2.9773 0.8638168
ROACbeforeinterestanddepreciationbeforeinterest 2.8790 0.8646053

Ở đây IV dựa trên MOB được xếp theo thứ tự giảm dần. Biến PersistentEPSintheLastFourSeasons có IV cao nhất là 3.2601 và nếu ta chỉ sử dụng biến số này cho mô hình Logistic thì AUC đạt được trên Test Data sẽ là 0.8932600.

Figure 1 dưới đây chỉ ra ngưỡng IV được chọn để có AUC tối ưu trên Test Data (điểm màu đỏ):

# Function discretizes variable (for Train, Validation and Test Data): 

discretize_fun <- function(variable_selected) {
  
  x <- df_train %>% pull(variable_selected)
  
  y <- df_train$Bankrupt
  
  bin_result <- iso_bin(x, y)
  
  cal_woe(x, bin_result) -> var_woe_train
  
  cal_woe(df_valid %>% pull(variable_selected), bin_result) -> var_woe_valid
  
  cal_woe(df_test %>% pull(variable_selected), bin_result) -> var_woe_test
  
  var_woe <- c(var_woe_train, var_woe_valid, var_woe_test)
  
  data_set <- c(rep("train", nrow(df_train)), rep("validation", nrow(df_valid)), rep("test", nrow(df_test)))
  
  data.frame(var_woe = var_woe, data_set = data_set) -> df_result
  
  return(df_result %>% mutate(original_var = variable_selected))
  
}


do.call("bind_rows", lapply(predictors, discretize_fun)) -> df_woe

df_woe %>% 
  filter(data_set == "train") %>% 
  pull(var_woe) %>% 
  matrix(ncol = length(predictors), byrow = FALSE) %>% 
  as.data.frame() %>% 
  set_names(predictors) %>% 
  mutate(Bankrupt = case_when(df_train$Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt") %>% as.factor()) -> df_train_woe


df_woe %>% 
  filter(data_set == "validation") %>% 
  pull(var_woe) %>% 
  matrix(ncol = length(predictors), byrow = FALSE) %>% 
  as.data.frame() %>% 
  set_names(predictors) %>% 
  mutate(Bankrupt = case_when(df_valid$Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt") %>% as.factor()) -> df_validation_woe


df_woe %>% 
  filter(data_set == "test") %>% 
  pull(var_woe) %>% 
  matrix(ncol = length(predictors), byrow = FALSE) %>% 
  as.data.frame() %>% 
  set_names(predictors) %>% 
  mutate(Bankrupt = case_when(df_test$Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt") %>% as.factor()) -> df_test_woe


returnROC_AUC_ValidData <- function(predictor_selected) {
  
  f <- as.formula(paste0(response, " ~ ", paste(predictor_selected, collapse = " + ")))
  
  logit <- glm(f, family = "binomial", data = df_train_woe)
  
  prob_pred <- predict(logit, df_validation_woe, type = "response")
  
  my_auc <- roc(df_validation_woe$Bankrupt, prob_pred)$auc %>% as.numeric()
  
  return(my_auc)
  
}

# Set a sequence of thresholds: 

iv_thresholds <- seq(min(df_iv_single_var$iv), max(df_iv_single_var$iv), 0.05)

auc_space <- NULL

# AUC by threshold: 

for (j in iv_thresholds) {
  
  df_iv_single_var %>% 
    filter(iv >= j) %>% 
    pull(variable) -> predictors_for_modelling
  
  returnROC_AUC_ValidData(predictors_for_modelling) -> my_auc
  
  auc_space <- c(auc_space, my_auc)
  
}

tibble(iv_thresholds = iv_thresholds, auc = auc_space) -> df_auc_threshold

# Features create max AUC on validation data: 

auc_max <- df_auc_threshold %>% slice(which.max(auc))

df_auc_threshold %>% 
  ggplot(aes(iv_thresholds, auc)) + 
  geom_line(size = 1, color = "blue") + 
  geom_point(data = auc_max, aes(iv_thresholds, auc), color = "red", size = 2) + 
  labs(x = "IV Threshold", 
       y = "AUC on Valid Data", 
       title = "Figure 1: AUC on Validation Data by Information Value Threshold")

Như vậy nếu chọn các biến số mà có IV >= 1.14 thì AUC trên Validation Data sẽ là 0.938:

auc_max
## # A tibble: 1 x 2
##   iv_thresholds   auc
##           <dbl> <dbl>
## 1          1.14 0.938

Dưới đây là danh sách các biến thỏa mãn IV >= 1.14:

df_iv_single_var %>% 
  filter(iv >= auc_max$iv_thresholds) %>% 
  pull(variable) -> var_auc_938

df_iv_single_var %>% 
  filter(iv >= auc_max$iv_thresholds) %>% 
  select(-auc) %>% 
  knitr::kable()
variable iv
ROACbeforeinterestanddepreciationbeforeinterest 2.8790
ROAAbeforeinterestandaftertax 2.4156
ROABbeforeinterestanddepreciationaftertax 2.7952
OperatingProfitRate 1.4413
PretaxnetInterestRate 2.1303
AftertaxnetInterestRate 1.9496
Nonindustryincomeandexpenditurerevenue 2.0579
Continuousinterestrateaftertax 2.4234
TaxrateA 1.3241
NetValuePerShareB 2.2337
NetValuePerShareA 2.2157
NetValuePerShareC 2.1619
PersistentEPSintheLastFourSeasons 3.2601
OperatingProfitPerShareYuan 1.8248
PerShareNetprofitbeforetaxYuan 3.0799
NetValueGrowthRate 2.3407
CurrentRatio 1.4081
QuickRatio 1.8383
InterestExpenseRatio 1.3197
TotaldebtTotalnetworth 2.0893
Debtratio 2.0640
NetworthAssets 2.0640
Borrowingdependency 2.3064
OperatingprofitPaidincapital 1.8207
NetprofitbeforetaxPaidincapital 3.1445
Operatingprofitperperson 1.5079
WorkingCapitaltoTotalAssets 1.2208
CashTotalAssets 1.1772
QuickAssetsCurrentLiability 1.4090
OperatingFundstoLiability 1.2125
CurrentLiabilitiesEquity 1.5337
RetainedEarningstoTotalAssets 2.9773
TotalincomeTotalexpense 2.4484
CurrentLiabilitytoEquity 1.5337
CurrentLiabilitytoCurrentAssets 1.4016
NetIncometoTotalAssets 3.0300
NetIncometoStockholdersEquity 2.8223
LiabilitytoEquity 1.8260
DegreeofFinancialLeverageDFL 1.4161
InterestCoverageRatioInterestexpensetoEBIT 1.3253
EquitytoLiability 2.0658

Nếu sử dụng 41 các biến số này cho mô hình Logistic thì chúng ta sẽ đạt AUC = 0.9365559 trên Test Data:

f938 <- as.formula(paste0(response, " ~ ", paste(var_auc_938, collapse = " + ")))

logit938 <- glm(f938, family = "binomial", data = df_train_woe)

prob_pred938 <- predict(logit938, df_test_woe, type = "response")

my_auc <- roc(df_test_woe$Bankrupt, prob_pred938)$auc %>% as.numeric()

my_auc
## [1] 0.9365559

Chúng ta có thể cải thiện kết quả này với Stepwise Selection.

Stepwise Selection

R codes dưới đây thực hiện Stepwise Selection cho mô hình Logistic và tính AUC trên Test Data:

# Conduct Stepwise Selection: 
logit_back <- step(logit938, steps = 1000, direction = "backward", trace = 0) 

# AUC on Test Data
roc(df_test_woe$Bankrupt, predict(logit_back, df_test_woe, type = "response"))$auc %>% as.numeric() 
## [1] 0.9383908

Kết quả 0.9383908 chỉ ra rằng bằng cách sử dụng MOB kết hợp với Stepwise Selection chúng ta có thể đạt được kết quả tốt hơn KotaShimomura là Team đang xếp vị trí thứ nhất với AUC = 0.93798.

Summary

  1. Sử dụng MOB kết hợp với Stepwise Selection cho mô hình Logistic chúng ta có thể đạt được thứ hạng cao hơn vị trí của Team đang xếp vị trí thứ nhất là KotaShimomura.

  2. Đây là dữ liệu bất cân bằng rất cao (chỉ có 3.2% các quan sát là Bankrupt) nên có thể cần xem xét đến khả năng sử dụng các giải pháp resampling dữ liệu như SMOTE, upsampling - downsampling để đạt kết quả tốt hơn nữa.

R Environment and OS

sessionInfo()
## R version 4.1.2 (2021-11-01)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19043)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] pROC_1.18.0     caret_6.0-90    lattice_0.20-45 mob_0.4.2      
##  [5] forcats_0.5.1   stringr_1.4.0   dplyr_1.0.7     purrr_0.3.4    
##  [9] readr_2.1.0     tidyr_1.1.4     tibble_3.1.6    ggplot2_3.3.5  
## [13] tidyverse_1.3.1
## 
## loaded via a namespace (and not attached):
##  [1] nlme_3.1-153         fs_1.5.0             lubridate_1.8.0     
##  [4] httr_1.4.2           tools_4.1.2          backports_1.3.0     
##  [7] bslib_0.3.1          utf8_1.2.2           R6_2.5.1            
## [10] rpart_4.1-15         DBI_1.1.1            colorspace_2.0-2    
## [13] nnet_7.3-16          withr_2.4.3          gbm_2.1.8           
## [16] tidyselect_1.1.1     compiler_4.1.2       cli_3.1.0           
## [19] rvest_1.0.2          xml2_1.3.2           sass_0.4.0          
## [22] scales_1.1.1         digest_0.6.28        rmarkdown_2.11      
## [25] pkgconfig_2.0.3      htmltools_0.5.2      parallelly_1.30.0   
## [28] highr_0.9            dbplyr_2.1.1         fastmap_1.1.0       
## [31] rlang_0.4.12         readxl_1.3.1         rstudioapi_0.13     
## [34] jquerylib_0.1.4      generics_0.1.1       jsonlite_1.7.3      
## [37] ModelMetrics_1.2.2.2 magrittr_2.0.1       Matrix_1.3-4        
## [40] Rcpp_1.0.7           munsell_0.5.0        fansi_0.5.0         
## [43] lifecycle_1.0.1      stringi_1.7.6        yaml_2.2.1          
## [46] MASS_7.3-54          plyr_1.8.6           recipes_0.1.17      
## [49] grid_4.1.2           parallel_4.1.2       listenv_0.8.0       
## [52] crayon_1.4.2         haven_2.4.3          splines_4.1.2       
## [55] hms_1.1.1            knitr_1.36           pillar_1.6.4        
## [58] stats4_4.1.2         reshape2_1.4.4       future.apply_1.8.1  
## [61] codetools_0.2-18     reprex_2.0.1         glue_1.5.0          
## [64] evaluate_0.14        data.table_1.14.2    modelr_0.1.8        
## [67] vctrs_0.3.8          tzdb_0.2.0           foreach_1.5.1       
## [70] cellranger_1.1.0     Rborist_0.2-3        gtable_0.3.0        
## [73] future_1.23.0        assertthat_0.2.1     xfun_0.28           
## [76] gower_0.2.2          prodlim_2019.11.13   broom_0.7.10        
## [79] class_7.3-19         survival_3.2-13      timeDate_3043.102   
## [82] iterators_1.0.13     lava_1.6.10          globals_0.14.0      
## [85] ellipsis_0.3.2       ipred_0.9-12
LS0tDQp0aXRsZTogIk1vbm90b25pYyBPcHRpbWFsIEJpbm5pbmcgZm9yIEZlYXR1cmUgU2VsZWN0aW9uOiBFbXBpcmljYWwgRXZpZGVuY2UgZnJvbSBDb3Jwb3JhdGUgQmFua3J1cHRjeSBQcmVkaWN0aW9uLCBLYWdnbGUgMjAyMSBDb21wZXRpdGlvbiINCmF1dGhvcjogJ0F1dGhvcjogTmd1eWVuIENoaSBEdW5nJw0Kc3VidGl0bGU6ICJSIERhdGEgU2NpZW5jZSBTZXJpZXMiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICAjIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogemVuYnVybg0KICAgICMgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImZsYXRseSINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgY2FjaGUgPSBUUlVFKQ0KDQpgYGANCg0KIVtdKEM6XFVzZXJzXFxBZG1pblxcRG9jdW1lbnRzXFxwaWNfYmFua3J1cHQuanBnKQ0KDQojIE1vdGl2YXRpb24gDQoNCltQb3N0IDFdKGh0dHBzOi8vcnB1YnMuY29tL2NoaWR1bmdrdC84NjcyNzIpIGNobyB0aOG6pXkgdmnhu4djIHPhu60gZOG7pW5nIEFVQyDEkeG7gyBs4buxYSBjaOG7jW4gYmnhur9uIHPhu5EgY2hvIG3DtCBow6xuaCBMb2dpc3RpYyDEkeG7gyDEkeG6oXQgxJHGsOG7o2MgQVVDID0gMC45MDMgdHLDqm4gVGVzdCBEYXRhIGNo4buJIGLhurFuZyBz4butIGThu6VuZyBo4buTaSBxdXkgTG9naXN0aWMgbMOgIG3hu5l0IGvhur90IHF14bqjIHTGsMahbmcgxJHhu5FpIHThu5F0LiBO4bq/dSBz4butIGThu6VuZyBkYW5oIHPDoWNoIGPDoWMgYmnhur9uIMSRxrDhu6NjIGzhu7FhIGNo4buNbiDEkeG7gyBodeG6pW4gbHV54buHbiBBdXRvbWF0ZWQgTWFjaGluZSBMZWFybmluZyB0aMOsIGNow7puZyB0YSBjw7MgdGjhu4MgxJHhuqF0IGvhur90IHF14bqjIGPDsm4gdOG7kXQgaMahbiB24bubaSBBVUMgPSAwLjkxODEuIA0KDQpbUG9zdCAyXShodHRwczovL3JwdWJzLmNvbS9jaGlkdW5na3QvODY4MTA0KSBjaOG7iSByYSBy4bqxbmcgdmnhu4djIGzhu7FhIGNo4buNbiBiaeG6v24gc+G7kSBk4buxYSB0csOqbiBJbmZvcm1hdGlvbiBWYWx1ZSBjw7MgdGjhu4MgxJHhuqF0IMSRxrDhu6NjIGvhur90IHF14bqjIGPDsm4gY2FvIGjGoW4gbuG7r2EgKEFVQyA9IDAuOTM3MDIxIHRyw6puIFRlc3QgRGF0YSkuIEvhur90IHF14bqjIG7DoHkgdGh1YSBrw6ltIGtow7RuZyBuaGnhu4F1IHNvIHbhu5tpIEFVQyDEkeG6oXQgxJHGsOG7o2MgY+G7p2EgVGVhbSB44bq/cCB0aOG7qSBuaOG6pXQgKFRlYW0gS290YVNoaW1vbXVyYSBjw7MgQVVDID0gMC45Mzc5OCkuIA0KDQpW4bubaSBj4buRIGfhuq9uZyB2xrDhu6N0IHF1YSDEkcaw4bujYyBr4bq/dCBxdeG6oyBj4bunYSBUZWFtIEtvdGFTaGltb211cmEsIHBvc3QgbsOgeSBz4bq9IHPhu60gZOG7pW5nICoqTW9ub3RvbmljIE9wdGltYWwgQmlubmluZyAtIE1PQioqIG5oxrAgbMOgIG3hu5l0IGvEqSB0aHXhuq10IGNobyBGZWF0dXJlIFNlbGVjdGlvbi4gQ+G7pSB0aOG7gyB04burIDk0IGJp4bq/biBz4buRIGJhbiDEkeG6p3Ugc+G6vSBjaOG7iSBs4buxYSBjaOG7jW4gcmEgNDEgYmnhur9uIHPhu5EgdGnhu4FtIG7Eg25nICjEkcOjIMSRxrDhu6NjICJy4budaSBy4bqhYyBow7NhIikgZOG7sWEgdHLDqm4gSVYgdOG7qyBNT0IuIFNhdSDEkcOzIHThu6sgZGFuaCBzw6FjaCBjw6FjIGJp4bq/biB0aeG7gW0gbsSDbmcgbsOgeSBs4bqhaSBjaOG7jW4gcmEgc+G7sSBr4bq/dCBo4bujcCB04buRaSDGsHUgbmjhuqV0IGPDoWMgYmnhur9uIGLhurFuZyAqU3RlcHdpc2UgU2VsZWN0aW9uKiAoYuG6oW4gxJHhu41jIHF1YW4gdMOibSBjw7MgdGjhu4MgdMOsbSBoaeG7g3UgdGjDqm0gduG7gSB0aHXhuq10IHRvw6FuIG7DoHkgdOG6oWkgdHJhbmcgMjI5IGN14buRbiBbQW4gSW50cm9kdWN0aW9uIHRvIFN0YXRpc3RpY2FsIExlYXJuaW5nXShodHRwczovL3d3dy5zdGF0bGVhcm5pbmcuY29tLykpLiBL4bq/dCBxdeG6oyB0aOG7sWMgbmdoaeG7h20gY2hvIHRo4bqleSBjaMO6bmcgdGEgY8OzIHRo4buDIMSR4bqhdCBBVUMgPSAwLjkzODM5MDggdHLDqm4gVGVzdCBEYXRhLiBEYW5oIHPDoWNoIFRlYW0gZOG6q24gxJHhuqd1IOG7nyBQcml2YXRlIFNjb3JlIGzDoCBUZWFtIEtvdGFTaGltb211cmEgduG7m2kgQVVDID0gMC45Mzc5OC4gDQoNClbhuqtuIG5oxrAg4bufIGhhaSBwb3N0IHRyxrDhu5tjLCBwb3N0IG7DoHkgduG6q24gc+G7rSBk4bulbmcgYuG7mSBk4buvIGxp4buHdSDEkcOjIMSRxrDhu6NjIHPhu60gZOG7pW5nIGNobyBjdeG7mWMgdGhpIFtDb3Jwb3JhdGUgQmFua3J1cHRjeSBQcmVkaWN0aW9uIDIwMjFdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYy8xMDU2bGFiLWNvcnBvcmF0ZS1iYW5rcnVwdGN5LXByZWRpY3Rpb24tMjAyMS9sZWFkZXJib2FyZD90YWI9cHVibGljKSB0csOqbiBLYWdnbGUgKGPDsyB0aOG7gyBkb3dubG9hZCBbdOG6oWkgxJHDonldKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vZmVkZXNvcmlhbm8vY29tcGFueS1iYW5rcnVwdGN5LXByZWRpY3Rpb24pKS4gDQoNCg0KIyBTaG9ydCBJbnRyb2R1Y3Rpb24gdG8gTW9ub3RvbmljIE9wdGltYWwgQmlubmluZw0KDQpNT0IgbMOgIG3hu5l0IGvEqSB0aHXhuq10ICJy4budaSBy4bqhYyBow7NhIiDEkcaw4bujYyBz4butIGThu6VuZyBwaOG7lSBiaeG6v24ga2hpIG3DtCBow6xuaCBow7NhIHLhu6dpIHJvIHbDoCBwaMOibiBsb+G6oWkteOG6v3AgaOG6oW5nIHTDrW4gZOG7pW5nIChuaMawIFBELCBMR0QsIEVBRCkuIMavdSDEkWnhu4NtIGPhu6dhIG7DsyBsw6AgbMOgbSBjaG8gbcO0IGjDrG5oIGThu4UgZ2nhuqNpIHRow61jaCB2w6AgbmjhuqV0IHF1w6FuIC0gbeG7mXQgecOqdSBj4bqndSB0aMaw4budbmcgZ2nhu68gduG7iyB0csOtIHF1YW4gdHLhu41uZyB04bqhaSBjw6FjIHThu5UgY2jhu6ljIHTDoGkgY2jDrW5oIG5oxrAgbmfDom4gaMOgbmcuIA0KDQpNT0IgY8OzIHRo4buDIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gYuG6sW5nIG5oaeG7gXUgUiBwYWNrYWdlIGtow6FjIG5oYXUuIFRyb25nIHBvc3QgbsOgeSBjaMO6bmcgdGEgc+G7rSBk4bulbmcgdGjGsCB2aeG7h24gbW9iIGPhu6dhIFtMaXVdKGh0dHBzOi8vd3d3LmxpbmtlZGluLmNvbS9pbi9saXV3ZW5zdWkvKS4gRMaw4bubaSDEkcOieSBsw6AgUiBjb2RlcyB0aOG7sWMgaGnhu4duIE1PQiBjaG8gYmnhur9uIE9wZXJhdGluZ0dyb3NzTWFyZ2luOiANCg0KYGBge3J9DQojIENsZWFyIG91ciBSIGVudmlyb25tZW50OiANCnJtKGxpc3QgPSBscygpKQ0KDQojIExvYWQgdGlkeXZlcnNlIHBhY2thZ2U6IA0KbGlicmFyeSh0aWR5dmVyc2UpDQoNCiMgTG9hZCBkYXRhOiANCnJlYWRfY3N2KCJGOi9kYXRhLmNzdi9kYXRhLmNzdiIpIC0+IGRhdGENCg0KIyBSZW5hbWUgZm9yIGFsbCBjb2x1bW5zOiANCg0Kb2xkX25hbWVzIDwtIG5hbWVzKGRhdGEpDQoNCm9sZF9uYW1lcyAlPiUgc3RyX3JlcGxhY2VfYWxsKCJbXmEtenxeQS1aXSIsICIiKSAtPiBuZXdfbmFtZXMNCg0KbmFtZXMoZGF0YSkgPC0gbmV3X25hbWVzDQoNCiMgTU9CIGZvciBPcGVyYXRpbmdQcm9maXRSYXRlOiANCg0KbGlicmFyeShtb2IpDQoNCmJpbl9yZXN1bHQgPC0gaXNvX2JpbihkYXRhJE9wZXJhdGluZ0dyb3NzTWFyZ2luLCBkYXRhJEJhbmtydXB0KQ0KDQojIERlY3JlYXNpbmcgZXZlbnQgcmF0ZXMgKG9yIEJhbmtydXB0IHJhdGUpOiANCg0KYmluX3Jlc3VsdCR0YmwgJT4lDQogIHNlbGVjdCgtcnVsZSkgJT4lDQogIGtuaXRyOjprYWJsZSgpIA0KDQpgYGANCg0KQmnhur9uIE9wZXJhdGluZ0dyb3NzTWFyZ2luIHPhur0gxJHGsOG7o2MgInLhu51pIHLhuqFjIGjDs2EiIHRow6BuaCAxNyBraG/huqNuZyBraMOhYyBuaGF1IHNhbyBjaG8gdOG7iSBs4buHIEJhbmtydXB0IGzDoCDEkcahbiDEkWnhu4d1IGdp4bqjbS4gVsOtIGThu6UsIOG7nyBraG/huqNuZyBPcGVyYXRpbmdHcm9zc01hcmdpbiA8PSAwLjU0MDg5MTMgdGjDrCBCYW5rcnVwdCBSYXRlID0gMC4yNjY3IGzDoCBjYW8gbmjhuqV0LiBU4bqhaSBjw6FjIGtob+G6o25nIHTEg25nIGThuqduIGPhu6dhIE9wZXJhdGluZ0dyb3NzTWFyZ2luIHRow6wgQmFua3J1cHQgUmF0ZSBjxaluZyBnaeG6o20gZOG6p24gbeG7mXQgY8OhY2ggxJHGoW4gxJFp4buHdS4gDQoNCiMgSVYgZnJvbSBNb25vdG9uaWMgQmlubmluZyBmb3IgRmVhdHVyZSBTZWxlY3Rpb24NCg0KTW9ub3RvbmljIEJpbm5pbmcgY8OzIHRo4buDIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gYuG6sW5nIG5oaeG7gXUgdGh14bqtdCB0b8Ohbi4gVOG7qyDEkcahbiBnaeG6o24gbmjGsCBLLW1lYW4gQ2x1c3RlcmluZyBjaG8gxJHhur9uIHZp4buHYyBz4butIGThu6VuZyBjw6FjIHRodeG6rXQgdG/DoW4gTWFjaGluZSBMZWFybmluZyBuaMawIFtYR0Jvb3N0XShodHRwczovL3d3dy5yLWJsb2dnZXJzLmNvbS8yMDIxLzA5L21vbm90b25pYy1iaW5uaW5nLXVzaW5nLXhnYm9vc3QvKS4gUG9zdCBuw6B5IHPhu60gZOG7pW5nIGPDoWNoIHRp4bq/cCBj4bqtbiBpc290b25pYyByZWdyZXNzaW9uIMSR4buDIHRo4buxYyBoaeG7h24gTU9CIHbhu5tpIGjDoG0gKippc29fYmluKCkqKi4gQ2jDum5nIHRhIGto4bqjbyBzw6F0IEFVQyB0csOqbiBUZXN0IERhdGEgbuG6v3UgbcO0IGjDrG5oIExvZ2lzdGljIGNo4buJIHPhu60gZOG7pW5nIGNo4buJIG3hu5l0IGJp4bq/biBz4buRIHNhdSBraGkgxJHDoyDEkcaw4bujYyBy4budaSBy4bqhYyBow7NhLiBExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzOiANCg0KDQpgYGB7cn0NCg0KIyBSZW1vdmUgTmV0SW5jb21lRmxhZyBjb2x1bW46IA0KZGF0YSAlPiUgc2VsZWN0KC1OZXRJbmNvbWVGbGFnKSAtPiBkZg0KDQojIFNldCByZXNwb25zZSBhbmQgcHJlZGljdG9yczogDQoNCnJlc3BvbnNlIDwtICJCYW5rcnVwdCINCg0KcHJlZGljdG9ycyA8LSBuYW1lcyhkZiAlPiUgc2VsZWN0KC1CYW5rcnVwdCkpDQoNCiMgU3BsaXQgb3VyIGRhdGE6IA0KDQpsaWJyYXJ5KGNhcmV0KQ0KDQpzZXQuc2VlZCgxKQ0KDQppZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBkZiAlPiUgcHVsbChyZXNwb25zZSksIHAgPSAwLjcsIGxpc3QgPSBGQUxTRSkNCg0KIyA3MCUgZGF0YSBmb3IgdHJhaW5pbmcgdsOgIHZhbGlkYXRpb246IA0KdHJhaW5fdmFsaWQgPC0gZGZbaWQsIF0gDQoNCiMgMzAlIGRhdGEgd2lsbCBiZSB1c2VkIGZvciBldmFsdWF0aW5nIG1vZGVsIHBlcmZvcm1hbmNlOiANCg0KZGZfdGVzdCA8LSBkZlstaWQsIF0gIyBUZXN0IGRhdGEuICANCg0Kc2V0LnNlZWQoMSkNCg0KaWRfbmV3IDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oeSA9IHRyYWluX3ZhbGlkICU+JSBwdWxsKHJlc3BvbnNlKSwgcCA9IG5yb3coZGZfdGVzdCkgLyBucm93KHRyYWluX3ZhbGlkKSwgbGlzdCA9IEZBTFNFKQ0KDQpkZl90cmFpbiA8LSB0cmFpbl92YWxpZFstaWRfbmV3LCBdICMgVHJhaW4gZGF0YS4gDQoNCmRmX3ZhbGlkIDwtIHRyYWluX3ZhbGlkW2lkX25ldywgXSAjIFZhbGlkYXRpb24gZGF0YS4gDQoNCiMgRGlzY3JldGl6ZSBzZWxlY3RlZCB2YXJpYWJsZXM6IA0KDQpuX3ZhciA8LSBsZW5ndGgocHJlZGljdG9ycykNCg0KYXVjX3NwYWNlIDwtIE5VTEwNCg0KaXZfc3BhY2UgPC0gTlVMTA0KDQpsaWJyYXJ5KHBST0MpDQoNCmZvciAoaiBpbiAxOm5fdmFyKSB7DQogIA0KICB2YXJfaiA8LSBwcmVkaWN0b3JzW2pdDQogIA0KICB2YXJfal9uZXcgPC0gc3RyX2ModmFyX2osICJfd29lIikNCiAgDQogIHggPC0gZGZfdHJhaW4gJT4lIHB1bGwodmFyX2opDQogIA0KICB5IDwtIGRmX3RyYWluJEJhbmtydXB0DQogIA0KICBiaW5fcmVzdWx0X2ogPC0gaXNvX2Jpbih4LCB5KQ0KICANCiAgIyBDYWxjdWxhdGUgSVY6IA0KICANCiAgYmluX3Jlc3VsdF9qJHRibCAlPiUgcHVsbChpdikgJT4lIHN1bSgpIC0+IGl2X2oNCiAgDQogIGl2X3NwYWNlIDwtIGMoaXZfc3BhY2UsIGl2X2opDQogIA0KICAjIERpc2NyZXRpemUgdmFyaWFibGU6IA0KICANCiAgY2FsX3dvZSh4LCBiaW5fcmVzdWx0X2opIC0+IHZhcl93b2Vfag0KICANCiAgIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KICAjIFRyYWluIExvZ2lzdGljIG9uIERpc2NyZXRpemVkIERhdGENCiAgIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KICANCiAgZGZfdHJhaW4gJT4lIA0KICAgIG11dGF0ZShCYW5rcnVwdCA9IGNhc2Vfd2hlbihCYW5rcnVwdCA9PSAxIH4gIkJhbmtydXB0IiwgVFJVRSB+ICJOb25CYW5rcnVwdCIpICU+JSBhcy5mYWN0b3IoKSkgJT4lIA0KICAgIHB1bGwoQmFua3J1cHQpIC0+IGxhYmVsX3RyYWluDQogIA0KICBkZl9qIDwtIGRhdGEuZnJhbWUoQmFua3J1cHQgPSBsYWJlbF90cmFpbiwgdmFyX3dvZSA9IHZhcl93b2VfaikNCiAgDQogIGxvZ2l0IDwtIGdsbShCYW5rcnVwdCB+LiwgZmFtaWx5ID0gImJpbm9taWFsIiwgZGF0YSA9IGRmX2opDQogIA0KICAjLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiAgIyBQRCBvbiBWYWxpZGF0aW9uIGRhdGENCiAgIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQogIA0KICBkZl92YWxpZCAlPiUgDQogICAgbXV0YXRlKEJhbmtydXB0ID0gY2FzZV93aGVuKEJhbmtydXB0ID09IDEgfiAiQmFua3J1cHQiLCBUUlVFIH4gIk5vbkJhbmtydXB0IikgJT4lIGFzLmZhY3RvcigpKSAlPiUgDQogICAgcHVsbChCYW5rcnVwdCkgLT4gbGFiZWxfdmFsaWQNCiAgDQogIGRmX3ZhbGlkX2ogPC0gZGF0YS5mcmFtZSh2YXJfd29lID0gIGNhbF93b2UoZGZfdmFsaWQgJT4lIHB1bGwodmFyX2opLCBiaW5fcmVzdWx0X2opKQ0KICANCiAgcHJvYl9wcmVkIDwtIHByZWRpY3QobG9naXQsIGRmX3ZhbGlkX2osIHR5cGUgPSAicmVzcG9uc2UiKQ0KICANCiAgIyBBVUMgb24gVmFsaWRhdGlvbiBEYXRhOiANCiAgDQogIG15X2F1YyA8LSByb2MobGFiZWxfdmFsaWQsIHByb2JfcHJlZCkkYXVjICU+JSBhcy5udW1lcmljKCkNCiAgDQogIGF1Y19zcGFjZSA8LSBjKGF1Y19zcGFjZSwgbXlfYXVjKQ0KICANCn0NCg0KDQpkZl9pdl9zaW5nbGVfdmFyIDwtIGRhdGEuZnJhbWUodmFyaWFibGUgPSBwcmVkaWN0b3JzLCBpdiA9IGl2X3NwYWNlLCBhdWMgPSBhdWNfc3BhY2UpDQoNCiMgU29tZSBSZXN1bHRzOiANCg0KZGZfaXZfc2luZ2xlX3ZhciAlPiUgDQogIGFycmFuZ2UoLWl2KSAlPiUgDQogIGhlYWQoKSAlPiUgDQogIGtuaXRyOjprYWJsZSgpDQpgYGANCg0K4bueIMSRw6J5IElWIGThu7FhIHRyw6puIE1PQiDEkcaw4bujYyB44bq/cCB0aGVvIHRo4bupIHThu7EgZ2nhuqNtIGThuqduLiBCaeG6v24gUGVyc2lzdGVudEVQU2ludGhlTGFzdEZvdXJTZWFzb25zIGPDsyBJViBjYW8gbmjhuqV0IGzDoCAzLjI2MDEgdsOgIG7hur91IHRhICpjaOG7iSBz4butIGThu6VuZyogYmnhur9uIHPhu5EgbsOgeSBjaG8gbcO0IGjDrG5oIExvZ2lzdGljIHRow6wgQVVDIMSR4bqhdCDEkcaw4bujYyB0csOqbiBUZXN0IERhdGEgc+G6vSBsw6AgMC44OTMyNjAwLiANCg0KDQpGaWd1cmUgMSBkxrDhu5tpIMSRw6J5IGNo4buJIHJhIG5nxrDhu6FuZyBJViDEkcaw4bujYyBjaOG7jW4gxJHhu4MgY8OzIEFVQyB04buRaSDGsHUgdHLDqm4gVGVzdCBEYXRhICjEkWnhu4NtIG3DoHUgxJHhu48pOiANCg0KDQpgYGB7cn0NCiMgRnVuY3Rpb24gZGlzY3JldGl6ZXMgdmFyaWFibGUgKGZvciBUcmFpbiwgVmFsaWRhdGlvbiBhbmQgVGVzdCBEYXRhKTogDQoNCmRpc2NyZXRpemVfZnVuIDwtIGZ1bmN0aW9uKHZhcmlhYmxlX3NlbGVjdGVkKSB7DQogIA0KICB4IDwtIGRmX3RyYWluICU+JSBwdWxsKHZhcmlhYmxlX3NlbGVjdGVkKQ0KICANCiAgeSA8LSBkZl90cmFpbiRCYW5rcnVwdA0KICANCiAgYmluX3Jlc3VsdCA8LSBpc29fYmluKHgsIHkpDQogIA0KICBjYWxfd29lKHgsIGJpbl9yZXN1bHQpIC0+IHZhcl93b2VfdHJhaW4NCiAgDQogIGNhbF93b2UoZGZfdmFsaWQgJT4lIHB1bGwodmFyaWFibGVfc2VsZWN0ZWQpLCBiaW5fcmVzdWx0KSAtPiB2YXJfd29lX3ZhbGlkDQogIA0KICBjYWxfd29lKGRmX3Rlc3QgJT4lIHB1bGwodmFyaWFibGVfc2VsZWN0ZWQpLCBiaW5fcmVzdWx0KSAtPiB2YXJfd29lX3Rlc3QNCiAgDQogIHZhcl93b2UgPC0gYyh2YXJfd29lX3RyYWluLCB2YXJfd29lX3ZhbGlkLCB2YXJfd29lX3Rlc3QpDQogIA0KICBkYXRhX3NldCA8LSBjKHJlcCgidHJhaW4iLCBucm93KGRmX3RyYWluKSksIHJlcCgidmFsaWRhdGlvbiIsIG5yb3coZGZfdmFsaWQpKSwgcmVwKCJ0ZXN0IiwgbnJvdyhkZl90ZXN0KSkpDQogIA0KICBkYXRhLmZyYW1lKHZhcl93b2UgPSB2YXJfd29lLCBkYXRhX3NldCA9IGRhdGFfc2V0KSAtPiBkZl9yZXN1bHQNCiAgDQogIHJldHVybihkZl9yZXN1bHQgJT4lIG11dGF0ZShvcmlnaW5hbF92YXIgPSB2YXJpYWJsZV9zZWxlY3RlZCkpDQogIA0KfQ0KDQoNCmRvLmNhbGwoImJpbmRfcm93cyIsIGxhcHBseShwcmVkaWN0b3JzLCBkaXNjcmV0aXplX2Z1bikpIC0+IGRmX3dvZQ0KDQpkZl93b2UgJT4lIA0KICBmaWx0ZXIoZGF0YV9zZXQgPT0gInRyYWluIikgJT4lIA0KICBwdWxsKHZhcl93b2UpICU+JSANCiAgbWF0cml4KG5jb2wgPSBsZW5ndGgocHJlZGljdG9ycyksIGJ5cm93ID0gRkFMU0UpICU+JSANCiAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgc2V0X25hbWVzKHByZWRpY3RvcnMpICU+JSANCiAgbXV0YXRlKEJhbmtydXB0ID0gY2FzZV93aGVuKGRmX3RyYWluJEJhbmtydXB0ID09IDEgfiAiQmFua3J1cHQiLCBUUlVFIH4gIk5vbkJhbmtydXB0IikgJT4lIGFzLmZhY3RvcigpKSAtPiBkZl90cmFpbl93b2UNCg0KDQpkZl93b2UgJT4lIA0KICBmaWx0ZXIoZGF0YV9zZXQgPT0gInZhbGlkYXRpb24iKSAlPiUgDQogIHB1bGwodmFyX3dvZSkgJT4lIA0KICBtYXRyaXgobmNvbCA9IGxlbmd0aChwcmVkaWN0b3JzKSwgYnlyb3cgPSBGQUxTRSkgJT4lIA0KICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICBzZXRfbmFtZXMocHJlZGljdG9ycykgJT4lIA0KICBtdXRhdGUoQmFua3J1cHQgPSBjYXNlX3doZW4oZGZfdmFsaWQkQmFua3J1cHQgPT0gMSB+ICJCYW5rcnVwdCIsIFRSVUUgfiAiTm9uQmFua3J1cHQiKSAlPiUgYXMuZmFjdG9yKCkpIC0+IGRmX3ZhbGlkYXRpb25fd29lDQoNCg0KZGZfd29lICU+JSANCiAgZmlsdGVyKGRhdGFfc2V0ID09ICJ0ZXN0IikgJT4lIA0KICBwdWxsKHZhcl93b2UpICU+JSANCiAgbWF0cml4KG5jb2wgPSBsZW5ndGgocHJlZGljdG9ycyksIGJ5cm93ID0gRkFMU0UpICU+JSANCiAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgc2V0X25hbWVzKHByZWRpY3RvcnMpICU+JSANCiAgbXV0YXRlKEJhbmtydXB0ID0gY2FzZV93aGVuKGRmX3Rlc3QkQmFua3J1cHQgPT0gMSB+ICJCYW5rcnVwdCIsIFRSVUUgfiAiTm9uQmFua3J1cHQiKSAlPiUgYXMuZmFjdG9yKCkpIC0+IGRmX3Rlc3Rfd29lDQoNCg0KcmV0dXJuUk9DX0FVQ19WYWxpZERhdGEgPC0gZnVuY3Rpb24ocHJlZGljdG9yX3NlbGVjdGVkKSB7DQogIA0KICBmIDwtIGFzLmZvcm11bGEocGFzdGUwKHJlc3BvbnNlLCAiIH4gIiwgcGFzdGUocHJlZGljdG9yX3NlbGVjdGVkLCBjb2xsYXBzZSA9ICIgKyAiKSkpDQogIA0KICBsb2dpdCA8LSBnbG0oZiwgZmFtaWx5ID0gImJpbm9taWFsIiwgZGF0YSA9IGRmX3RyYWluX3dvZSkNCiAgDQogIHByb2JfcHJlZCA8LSBwcmVkaWN0KGxvZ2l0LCBkZl92YWxpZGF0aW9uX3dvZSwgdHlwZSA9ICJyZXNwb25zZSIpDQogIA0KICBteV9hdWMgPC0gcm9jKGRmX3ZhbGlkYXRpb25fd29lJEJhbmtydXB0LCBwcm9iX3ByZWQpJGF1YyAlPiUgYXMubnVtZXJpYygpDQogIA0KICByZXR1cm4obXlfYXVjKQ0KICANCn0NCg0KIyBTZXQgYSBzZXF1ZW5jZSBvZiB0aHJlc2hvbGRzOiANCg0KaXZfdGhyZXNob2xkcyA8LSBzZXEobWluKGRmX2l2X3NpbmdsZV92YXIkaXYpLCBtYXgoZGZfaXZfc2luZ2xlX3ZhciRpdiksIDAuMDUpDQoNCmF1Y19zcGFjZSA8LSBOVUxMDQoNCiMgQVVDIGJ5IHRocmVzaG9sZDogDQoNCmZvciAoaiBpbiBpdl90aHJlc2hvbGRzKSB7DQogIA0KICBkZl9pdl9zaW5nbGVfdmFyICU+JSANCiAgICBmaWx0ZXIoaXYgPj0gaikgJT4lIA0KICAgIHB1bGwodmFyaWFibGUpIC0+IHByZWRpY3RvcnNfZm9yX21vZGVsbGluZw0KICANCiAgcmV0dXJuUk9DX0FVQ19WYWxpZERhdGEocHJlZGljdG9yc19mb3JfbW9kZWxsaW5nKSAtPiBteV9hdWMNCiAgDQogIGF1Y19zcGFjZSA8LSBjKGF1Y19zcGFjZSwgbXlfYXVjKQ0KICANCn0NCg0KdGliYmxlKGl2X3RocmVzaG9sZHMgPSBpdl90aHJlc2hvbGRzLCBhdWMgPSBhdWNfc3BhY2UpIC0+IGRmX2F1Y190aHJlc2hvbGQNCg0KIyBGZWF0dXJlcyBjcmVhdGUgbWF4IEFVQyBvbiB2YWxpZGF0aW9uIGRhdGE6IA0KDQphdWNfbWF4IDwtIGRmX2F1Y190aHJlc2hvbGQgJT4lIHNsaWNlKHdoaWNoLm1heChhdWMpKQ0KDQpkZl9hdWNfdGhyZXNob2xkICU+JSANCiAgZ2dwbG90KGFlcyhpdl90aHJlc2hvbGRzLCBhdWMpKSArIA0KICBnZW9tX2xpbmUoc2l6ZSA9IDEsIGNvbG9yID0gImJsdWUiKSArIA0KICBnZW9tX3BvaW50KGRhdGEgPSBhdWNfbWF4LCBhZXMoaXZfdGhyZXNob2xkcywgYXVjKSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDIpICsgDQogIGxhYnMoeCA9ICJJViBUaHJlc2hvbGQiLCANCiAgICAgICB5ID0gIkFVQyBvbiBWYWxpZCBEYXRhIiwgDQogICAgICAgdGl0bGUgPSAiRmlndXJlIDE6IEFVQyBvbiBWYWxpZGF0aW9uIERhdGEgYnkgSW5mb3JtYXRpb24gVmFsdWUgVGhyZXNob2xkIikNCmBgYA0KDQoNCk5oxrAgduG6rXkgbuG6v3UgY2jhu41uIGPDoWMgYmnhur9uIHPhu5EgbcOgIGPDsyBJViA+PSAxLjE0IHRow6wgQVVDIHRyw6puIFZhbGlkYXRpb24gRGF0YSBz4bq9IGzDoCAwLjkzODogDQoNCmBgYHtyLCBldmFsPVRSVUV9DQphdWNfbWF4DQpgYGANCg0KRMaw4bubaSDEkcOieSBsw6AgZGFuaCBzw6FjaCBjw6FjIGJp4bq/biB0aOG7j2EgbcOjbiBJViA+PSAxLjE0OiANCg0KYGBge3IsIGV2YWw9VFJVRX0NCmRmX2l2X3NpbmdsZV92YXIgJT4lIA0KICBmaWx0ZXIoaXYgPj0gYXVjX21heCRpdl90aHJlc2hvbGRzKSAlPiUgDQogIHB1bGwodmFyaWFibGUpIC0+IHZhcl9hdWNfOTM4DQoNCmRmX2l2X3NpbmdsZV92YXIgJT4lIA0KICBmaWx0ZXIoaXYgPj0gYXVjX21heCRpdl90aHJlc2hvbGRzKSAlPiUgDQogIHNlbGVjdCgtYXVjKSAlPiUgDQogIGtuaXRyOjprYWJsZSgpDQoNCmBgYA0KDQpO4bq/dSBz4butIGThu6VuZyA0MSBjw6FjIGJp4bq/biBz4buRIG7DoHkgY2hvIG3DtCBow6xuaCBMb2dpc3RpYyB0aMOsIGNow7puZyB0YSBz4bq9IMSR4bqhdCBBVUMgPSAwLjkzNjU1NTkgdHLDqm4gVGVzdCBEYXRhOiANCg0KYGBge3IsIGV2YWw9VFJVRX0NCmY5MzggPC0gYXMuZm9ybXVsYShwYXN0ZTAocmVzcG9uc2UsICIgfiAiLCBwYXN0ZSh2YXJfYXVjXzkzOCwgY29sbGFwc2UgPSAiICsgIikpKQ0KDQpsb2dpdDkzOCA8LSBnbG0oZjkzOCwgZmFtaWx5ID0gImJpbm9taWFsIiwgZGF0YSA9IGRmX3RyYWluX3dvZSkNCg0KcHJvYl9wcmVkOTM4IDwtIHByZWRpY3QobG9naXQ5MzgsIGRmX3Rlc3Rfd29lLCB0eXBlID0gInJlc3BvbnNlIikNCg0KbXlfYXVjIDwtIHJvYyhkZl90ZXN0X3dvZSRCYW5rcnVwdCwgcHJvYl9wcmVkOTM4KSRhdWMgJT4lIGFzLm51bWVyaWMoKQ0KDQpteV9hdWMNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIGPhuqNpIHRoaeG7h24ga+G6v3QgcXXhuqMgbsOgeSB24bubaSBTdGVwd2lzZSBTZWxlY3Rpb24uIA0KDQojIFN0ZXB3aXNlIFNlbGVjdGlvbg0KDQpSIGNvZGVzIGTGsOG7m2kgxJHDonkgdGjhu7FjIGhp4buHbiBTdGVwd2lzZSBTZWxlY3Rpb24gY2hvIG3DtCBow6xuaCBMb2dpc3RpYyB2w6AgdMOtbmggQVVDIHRyw6puIFRlc3QgRGF0YTogDQoNCmBgYHtyLCBldmFsPVRSVUV9DQojIENvbmR1Y3QgU3RlcHdpc2UgU2VsZWN0aW9uOiANCmxvZ2l0X2JhY2sgPC0gc3RlcChsb2dpdDkzOCwgc3RlcHMgPSAxMDAwLCBkaXJlY3Rpb24gPSAiYmFja3dhcmQiLCB0cmFjZSA9IDApIA0KDQojIEFVQyBvbiBUZXN0IERhdGENCnJvYyhkZl90ZXN0X3dvZSRCYW5rcnVwdCwgcHJlZGljdChsb2dpdF9iYWNrLCBkZl90ZXN0X3dvZSwgdHlwZSA9ICJyZXNwb25zZSIpKSRhdWMgJT4lIGFzLm51bWVyaWMoKSANCmBgYA0KDQpL4bq/dCBxdeG6oyAwLjkzODM5MDggY2jhu4kgcmEgcuG6sW5nIGLhurFuZyBjw6FjaCBz4butIGThu6VuZyBNT0Iga+G6v3QgaOG7o3AgduG7m2kgU3RlcHdpc2UgU2VsZWN0aW9uIGNow7puZyB0YSBjw7MgdGjhu4MgxJHhuqF0IMSRxrDhu6NjIGvhur90IHF14bqjIHThu5F0IGjGoW4gS290YVNoaW1vbXVyYSBsw6AgVGVhbSDEkWFuZyB44bq/cCB24buLIHRyw60gdGjhu6kgbmjhuqV0IHbhu5tpIEFVQyA9IDAuOTM3OTguIA0KDQojIFN1bW1hcnkNCg0KMS4gU+G7rSBk4bulbmcgTU9CIGvhur90IGjhu6NwIHbhu5tpIFN0ZXB3aXNlIFNlbGVjdGlvbiBjaG8gbcO0IGjDrG5oIExvZ2lzdGljIGNow7puZyB0YSBjw7MgdGjhu4MgxJHhuqF0IMSRxrDhu6NjIHRo4bupIGjhuqFuZyBjYW8gaMahbiB24buLIHRyw60gY+G7p2EgVGVhbSDEkWFuZyB44bq/cCB24buLIHRyw60gdGjhu6kgbmjhuqV0IGzDoCBLb3RhU2hpbW9tdXJhLiANCg0KMi4gxJDDonkgbMOgIGThu68gbGnhu4d1IGLhuqV0IGPDom4gYuG6sW5nIHLhuqV0IGNhbyAoY2jhu4kgY8OzIDMuMiUgY8OhYyBxdWFuIHPDoXQgbMOgIEJhbmtydXB0KSBuw6puIGPDsyB0aOG7gyBj4bqnbiB4ZW0geMOpdCDEkeG6v24ga2jhuqMgbsSDbmcgc+G7rSBk4bulbmcgY8OhYyBnaeG6o2kgcGjDoXAgcmVzYW1wbGluZyBk4buvIGxp4buHdSBuaMawIFNNT1RFLCB1cHNhbXBsaW5nIC0gZG93bnNhbXBsaW5nIMSR4buDIMSR4bqhdCBr4bq/dCBxdeG6oyB04buRdCBoxqFuIG7hu69hLg0KDQojIFJlZmVyZW5jZXMNCg0KMS4gW01vbm90b25lIG9wdGltYWwgYmlubmluZyBhbGdvcml0aG0gZm9yIGNyZWRpdCByaXNrIG1vZGVsaW5nXShodHRwczovL3d3dy5yZXNlYXJjaGdhdGUubmV0L3B1YmxpY2F0aW9uLzMyMjUyMDEzNV9Nb25vdG9uZV9vcHRpbWFsX2Jpbm5pbmdfYWxnb3JpdGhtX2Zvcl9jcmVkaXRfcmlza19tb2RlbGluZykuIA0KMi4gW1B1cnBvc2VmdWwgc2VsZWN0aW9uIG9mIHZhcmlhYmxlcyBpbiBsb2dpc3RpYyByZWdyZXNzaW9uXShodHRwczovL3NjZmJtLmJpb21lZGNlbnRyYWwuY29tL2FydGljbGVzLzEwLjExODYvMTc1MS0wNDczLTMtMTcpLiANCjMuIFtNb25vdG9uaWMgYmlubmluZyB1c2luZyBYR0JPT1NUXShodHRwczovL3d3dy5yLWJsb2dnZXJzLmNvbS8yMDIxLzA5L21vbm90b25pYy1iaW5uaW5nLXVzaW5nLXhnYm9vc3QvKS4gDQo0LiBbQW4gSW50cm9kdWN0aW9uIHRvIFN0YXRpc3RpY2FsIExlYXJuaW5nXShodHRwczovL3d3dy5zdGF0bGVhcm5pbmcuY29tLykuIA0KNS4gW09wdGltYWwgYmlubmluZzogbWF0aGVtYXRpY2FsIHByb2dyYW1taW5nIGZvcm11bGF0aW9uXShodHRwczovL2FyeGl2Lm9yZy9wZGYvMjAwMS4wODAyNS5wZGYpLiANCjYuIFtTdGVwIGF3YXkgZnJvbSBzdGVwd2lzZV0oaHR0cHM6Ly9qb3VybmFsb2ZiaWdkYXRhLnNwcmluZ2Vyb3Blbi5jb20vYXJ0aWNsZXMvMTAuMTE4Ni9zNDA1MzctMDE4LTAxNDMtNikuIA0KDQoNCiMgUiBFbnZpcm9ubWVudCBhbmQgT1MNCg0KYGBge3J9DQpzZXNzaW9uSW5mbygpDQpgYGANCg0KDQoNCg==