Introduction

WOE Binning là một kĩ thuật thuộc nhóm Data Transformation mà trong đó một biến liên lục được “rời rạc hóa” (thuật ngữ tiếng Anh là Discretization) thành các nhóm (gọi là binning). Việc chia biến liên tục thành các nhóm như vậy được thực hiện với mục đính là tính toán WoE (bằng chứng có trọng số WoE - Weight of Evidence) và giá trị thông tin IV (Information Value) của biến số. Một ví dụ minh họa được trình bày ở trang 80 cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring áp dụng cho biến tuổi (Age) như sau:

Và tính toán WoE/IV cũng như sử dụng tiêu chuẩn IV cho chọn biến số như sau (trang 81):

Lưu ý rằng kĩ thuật binning biến này còn áp dụng cho cả biến định tính chứ không riêng gì biến định lượng.

Chúng ta có thể viết một hàm binning biến số đồng thời tính toán luôn WoE/IV cũng như thực hiện cái gọi là WoE Transformation cho biến số và sẽ sử dụng hàm này cho bộ số liệu hmeq.csv của cuốn Credit Risk Analytics: Measurement Techniques, Applications, and Examples in SAS như sau:

# Load một số packages cho data manipulation: 
library(tidyverse)
library(magrittr)

library(SciViews)
library(Hmisc)

# Import data: 
hmeq <- read.csv("http://www.creditriskanalytics.net/uploads/1/9/5/1/19511601/hmeq.csv")

# Hàm thực hiện binning và tính toán WOE/IV + thực hiện luôn cái gọi là "WOE Transformation": 

manually_binning_WOE_trans <- function(your_df, y, x, break_points_selected) {

  # Chuẩn bị dữ liệu: 
  df_for_woe <- your_df[, c(x, y)] %>% dplyr::mutate_if(is.integer, as.numeric)
  names(df_for_woe) <- c("Var_selected", "Y")
  
  df_for_woe %<>% 
    mutate(Y = case_when(Y == 1 ~ "Bad", TRUE ~ "Good"), 
           Var_selected = case_when(Var_selected %in% c(-Inf, Inf) ~ NA %>% as.numeric(), 
                                    TRUE ~ Var_selected))
  
  # Tính toán WOE/IV table: 
  
  df_for_woe %>% 
    mutate(Cut_Point = cut2(Var_selected, break_points_selected, onlycuts = T) %>% as.character()) %>% 
    mutate(Cut_Point = case_when(is.na(Cut_Point) ~ "Missing", TRUE ~ Cut_Point)) %>% 
    group_by(Cut_Point, Y) %>% 
    count() %>% 
    ungroup() %>% 
    spread(Y, n) %>% 
    na.omit() %>% 
    mutate(Total = Bad + Good) %>% 
    mutate(BadRate = Bad / Total) %>% 
    mutate(Per_bin = Total / sum(Total)) %>% 
    mutate(DG = Good / sum(Good), DB = Bad / sum(Bad)) %>% 
    mutate(WOE = ln(DG / DB)) %>% 
    mutate(WOE = case_when(Bad == 0 ~ 2, Good == 0 ~ -2, TRUE ~ WOE)) %>% 
    mutate(IV = (DG - DB)*WOE, 
           IV_Total = sum(IV), Var_Name = x) -> df_woe_cal
  
  cut_point <- df_woe_cal %>% pull(Cut_Point)
  n_bins <- cut_point[!str_detect(cut_point, "Missing")] %>% length()
  df_woe_cal %<>% mutate(N_bins = n_bins)
  
  # Tính toán WOE Transformation: 
  
  df_for_woe %>% 
    mutate(Cut_Point = cut2(Var_selected, break_points_selected, onlycuts = T) %>% as.character()) %>% 
    mutate(Cut_Point = case_when(is.na(Cut_Point) ~ "Missing", TRUE ~ Cut_Point)) %>% 
    right_join(df_woe_cal, by = "Cut_Point") %>% 
    mutate(woe_trans = WOE) %>% 
    pull(woe_trans) -> woe_trans_data
  
  return(list(iv_table_final = df_woe_cal, woe_trans_data = woe_trans_data))
  
}

# Sử dụng hàm: 
manually_binning_WOE_trans(hmeq, 
                           y = "BAD", 
                           x = "LOAN", 
                           break_points_selected = c(6000, 8000, 16000, 38000)) -> binning_results

# Kết quả: 
binning_results$iv_table_final %>% 
  select(1:12) %>% 
  mutate_if(is.numeric, function(x) {round(x, 3)}) %>% 
  knitr::kable()
Cut_Point Bad Good Total BadRate Per_bin DG DB WOE IV IV_Total Var_Name
[ 1100, 6000) 142 162 304 0.467 0.051 0.034 0.119 -1.258 0.107 0.167 LOAN
[ 6000, 8000) 92 250 342 0.269 0.057 0.052 0.077 -0.390 0.010 0.167 LOAN
[ 8000,16000) 456 1772 2228 0.205 0.374 0.371 0.384 -0.032 0.000 0.167 LOAN
[16000,38000) 415 2323 2738 0.152 0.459 0.487 0.349 0.333 0.046 0.167 LOAN
[38000,89900] 84 264 348 0.241 0.058 0.055 0.071 -0.244 0.004 0.167 LOAN

Consequences of WOE Binning for Logistic Regression

Hệ quả trước tiên của việc sử dụng biến số được thực hiện WoE Transformation khi thực hiện hồi quy Logistic đơn biến như sau:

  1. Hệ số chặn của mô hình hồi quy luôn bằng log(B/G) với B và G lần lượt là số lượng hồ sơ Bad và Good.
  2. Hệ số góc của mô hình hồi quy luôn bằng -1.

Hai hệ quả trên có thể được kiểm tra lại với bộ dữ liệu hmeq như sau:

# Consequences of using WOE for Logistic Regression:  
df_woe <- data.frame(BAD = hmeq$BAD, LOAN_woe = binning_results$woe_trans_data)
glm(BAD ~ LOAN_woe, family = "binomial", data = df_woe) %>% summary()
## 
## Call:
## glm(formula = BAD ~ LOAN_woe, family = "binomial", data = df_woe)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.1220  -0.6767  -0.5734  -0.5734   1.9425  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -1.38944    0.03305  -42.05   <2e-16 ***
## LOAN_woe    -1.00000    0.07624  -13.12   <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: 5956.5  on 5959  degrees of freedom
## Residual deviance: 5791.0  on 5958  degrees of freedom
## AIC: 5795
## 
## Number of Fisher Scoring iterations: 4

Hệ quả thứ ba là chúng ta có thể tính tỉ lệ hồ sơ Bad cho từng nhóm (hay khoảng binning) từ mô hình Logistic. Ví dụ, với nhóm từ 1100 đến 6000 thì tỉ lệ Bad được tính theo công thức sau:

# Consequences of using WOE for Logistic Regression: 

ts <- exp(-1*-1.258 - 1.38944) 
ts / (ts + 1)
## [1] 0.4671872

Tương tự là cho nhóm từ 6000 đến 8000:

ts <- exp(-1*-0.390 - 1.38944) 
ts / (ts + 1)
## [1] 0.2690515

Hai kết quả 0.467 và 0.269 chính là tỉ lệ hồ sơ Bad (46.7% và 26.9%) tương ứng với các nhóm này mà chúng ta đã biết ở bảng IV Table khi sử dụng hàm binning mà chúng ta đã viết ở trên.

Some R Packages for WoE Binning

Hiện R có rất nhiều package sử dụng cho binning và với mục đích là Scorecard Modelling thì scorecard là thuận lợi hơn cả vì package được viết nhằm hiện thực hóa cách thức xây dựng Scorecard thường được thực hiện bởi các ngân hàng được mô tả trong cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring. Tuy nhiên tác giả của package này đã “nhầm lẫn” khi tính toán WoE như sau:

Điều này dẫn đến các giá trị WoE sẽ ngược dấu hoàn toàn với những tính toán được mô tả trong tài liệu của Naeem Siddiqi như chúng ta có thể thấy:

# Tính WoE cho biến LOAN bằng sử dụng các hàm của gói scorecard: 
library(scorecard)
bins <- woebin(hmeq, y = "BAD",  positive = "BAD|1")
## [INFO] creating woe binning ...
df2_woe <- woebin_ply(hmeq, bins = bins)
## [INFO] converting into woe values ...
bins$LOAN[, 1:10] %>% 
  mutate_if(is.numeric, function(x) {round(x, 3)}) %>% 
  knitr::kable()
variable bin count count_distr good bad badprob woe bin_iv total_iv
LOAN [-Inf,6000) 304 0.051 162 142 0.467 1.258 0.107 0.167
LOAN [6000,8000) 342 0.057 250 92 0.269 0.390 0.010 0.167
LOAN [8000,16000) 2228 0.374 1772 456 0.205 0.032 0.000 0.167
LOAN [16000,38000) 2738 0.459 2323 415 0.152 -0.333 0.046 0.167
LOAN [38000, Inf) 348 0.058 264 84 0.241 0.244 0.004 0.167

Do ngược dấu nên hệ số chặn của mô hình hồi quy Logistic lúc này là 1:

glm(BAD ~ LOAN_woe, family = "binomial", data = df2_woe) %>% summary()
## 
## Call:
## glm(formula = BAD ~ LOAN_woe, family = "binomial", data = df2_woe)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.1220  -0.6767  -0.5734  -0.5734   1.9425  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -1.38944    0.03305  -42.05   <2e-16 ***
## LOAN_woe     1.00000    0.07624   13.12   <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: 5956.5  on 5959  degrees of freedom
## Residual deviance: 5791.0  on 5958  degrees of freedom
## AIC: 5795
## 
## Number of Fisher Scoring iterations: 4

Chúng ta có thể kiểm tra với một biến số khác:

glm(BAD ~ VALUE_woe, family = "binomial", data = df2_woe) %>% summary()
## 
## Call:
## glm(formula = BAD ~ VALUE_woe, family = "binomial", data = df2_woe)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.3548  -0.6409  -0.6409  -0.5400   2.2322  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -1.38944    0.03392  -40.97   <2e-16 ***
## VALUE_woe    1.00000    0.06567   15.23   <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: 5956.5  on 5959  degrees of freedom
## Residual deviance: 5537.2  on 5958  degrees of freedom
## AIC: 5541.2
## 
## Number of Fisher Scoring iterations: 5

Ngoài scorecard thì còn có gói logiBin cũng có thể được sử dụng để binning biến số với mục đích là binning theo hướng monotonic (tức là đơn điệu tăng hoặc giảm cho nhóm hồ sơ Bad hoặc Good theo các khoảng):

detach(package:scorecard)

# Import package: 
library(logiBin)


# Binning data cho biến LOAN: 

my_bin <- getBins(hmeq, y = "BAD", xVars = c("LOAN"), 
                  minProp = 0.05, minCr = 0.2, nCores = 20)

# Monotonic trend or not:
my_bin$varSummary
##     var     iv         pVal     stat    ent trend monTrend flipRatio
## 16 LOAN 0.2607 6.741998e-09 33.60785 0.6906     I        N       0.5
##    numBins purNode varType
## 16      15       N integer
# IV and results: 
my_bin$bin %>% 
  select(-c(1, 9)) %>% 
  knitr::kable()
bin count bads goods propn bad_rate iv
LOAN <= 6000 332 157 175 5.57 47.29 0.1222
LOAN > 6000 & LOAN <= 8700 535 114 421 8.98 21.31 0.0006
LOAN > 8700 & LOAN <= 10000 344 95 249 5.77 27.62 0.0118
LOAN > 10000 & LOAN <= 11400 365 57 308 6.12 15.62 0.0049
LOAN > 11400 & LOAN <= 12600 347 76 271 5.82 21.90 0.0008
LOAN > 12600 & LOAN <= 13900 377 62 315 6.33 16.45 0.0033
LOAN > 13900 & LOAN <= 15000 322 94 228 5.40 29.19 0.0157
LOAN > 15000 & LOAN <= 17000 556 91 465 9.33 16.37 0.0051
LOAN > 17000 & LOAN <= 18700 392 45 347 6.58 11.48 0.0228
LOAN > 18700 & LOAN <= 21000 464 90 374 7.79 19.40 0.0001
LOAN > 21000 & LOAN <= 23400 455 50 405 7.63 10.99 0.0301
LOAN > 23400 & LOAN <= 25000 300 69 231 5.03 23.00 0.0017
LOAN > 25000 & LOAN <= 27600 360 37 323 6.04 10.28 0.0284
LOAN > 27600 & LOAN <= 37900 463 68 395 7.77 14.69 0.0095
LOAN > 37900 348 84 264 5.84 24.14 0.0037
Total 5960 1189 4771 1.00 19.95 0.2607

Chúng ta có thể binning biến LOAN này sao cho monotonic (tăng hoặc giảm):

# Check increasing trend: 
forceIncrTrend(my_bin, xVars = c("LOAN")) %>% .[[3]]
##    var           bin count bads goods propn bad_rate     iv    ent
## 1 LOAN LOAN <= 37900  5612 1105  4507 94.16    19.69 0.0003 0.7157
## 2 LOAN  LOAN > 37900   348   84   264  5.84    24.14 0.0037 0.7973
## 3 LOAN         Total  5960 1189  4771  1.00    19.95 0.0040 0.7205
# Check decreasing trend: 
my_bin_de <- forceDecrTrend(my_bin, xVars = c("LOAN"))

# IV Table: 
iv_table <- my_bin_de$bin %>% 
  filter(bin != "Total") %>% 
  mutate(bin = str_replace_all(bin, "LOAN", "")) %>% 
  mutate(DG = goods / sum(goods), DB = bads / sum(bads)) %>% 
  mutate(woe = log(DG / DB)) %>% 
  mutate(iv_total = sum(iv))

Có thể thấy tỉ lệ hồ sơ Bad giảm dần theo từng khoảng tăng dần của LOAN:

iv_table %>% 
  select(-c(9, 10, 11)) %>% 
  knitr::kable()
var bin count bads goods propn bad_rate iv woe iv_total
LOAN <= 6000 332 157 175 5.57 47.29 0.1222 -1.2809031 0.1678
LOAN > 6000 & <= 10000 879 209 670 14.75 23.78 0.0079 -0.2244998 0.1678
LOAN > 10000 & <= 15000 1411 289 1122 23.67 20.48 0.0003 -0.0330019 0.1678
LOAN > 15000 & <= 17000 556 91 465 9.33 16.37 0.0051 0.2417346 0.1678
LOAN > 17000 2782 443 2339 46.68 15.92 0.0323 0.2744657 0.1678

Tất nhiên nếu sử dụng biến sau khi thực hiện WoE Transformation thì hệ số góc của mô hình hồi quy đơn biến vẫn là -1:

# WOE Transformation: 
createBins(my_bin_de, hmeq, xVars = "LOAN") %>% 
  select(LOAN, b_LOAN) %>% 
  right_join(iv_table, by = c("b_LOAN" = "bin")) %>% 
  pull(woe) -> LOAN_binned

# Logistic Regresion: 
hmeq %>% 
  mutate(LOAN_binned = LOAN_binned) %>% 
  glm(BAD ~ LOAN_binned, family = "binomial", data = .) %>% 
  summary()
## 
## Call:
## glm(formula = BAD ~ LOAN_binned, family = "binomial", data = .)
## 
## Deviance Residuals: 
##    Min      1Q  Median      3Q     Max  
## -1.132  -0.677  -0.589  -0.589   1.917  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -1.38944    0.03305  -42.05   <2e-16 ***
## LOAN_binned -1.00000    0.07545  -13.25   <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: 5956.5  on 5959  degrees of freedom
## Residual deviance: 5789.2  on 5958  degrees of freedom
## AIC: 5793.2
## 
## Number of Fisher Scoring iterations: 4

Some Recommendations

Do cách tính WoE của gói scorecard ngược hoàn toàn nên cần lưu ý để hiệu chỉnh. Tuy nhiên giá trị IV thì vẫn đúng. Nguyên nhân mà IV vẫn đúng trong khi WoE “sai” các bạn có thể giải thích được từ công thức mô tả của gói scorecard. Ngoại trừ “thiếu sót” này thì chúng ta vẫn có thể sử dụng gói này để thực hiện Scorecard Modelling như đã mô tả trong cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring hoặc Credit Risk Analytics: Measurement Techniques, Applications, and Examples in SAS.

Trong tình huống mà cần binning biến để tìm kiếm monotonic trend (đơn điệu tăng hoặc giảm của Bad Rate) thì chúng ta nên sử dụng gói logiBin như đã trình bày ở trên.

LS0tDQp0aXRsZTogIkNvbnNlcXVlbmNlcyBvZiBXT0UgQmlubmluZyBmb3IgTG9naXN0aWMgUmVncmVzc2lvbiIgDQpzdWJ0aXRsZTogIlIgZm9yIEZ1biINCmF1dGhvcjogIk5ndXllbiBDaGkgRHVuZyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogcHlnbWVudHMNCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQpgYGANCg0KIyBJbnRyb2R1Y3Rpb24NCg0KV09FIEJpbm5pbmcgbMOgIG3hu5l0IGvEqSB0aHXhuq10IHRodeG7mWMgbmjDs20gRGF0YSBUcmFuc2Zvcm1hdGlvbiBtw6AgdHJvbmcgxJHDsyBt4buZdCBiaeG6v24gbGnDqm4gbOG7pWMgxJHGsOG7o2MgInLhu51pIHLhuqFjIGjDs2EiICh0aHXhuq10IG5n4buvIHRp4bq/bmcgQW5oIGzDoCBEaXNjcmV0aXphdGlvbikgdGjDoG5oIGPDoWMgbmjDs20gKGfhu41pIGzDoCBiaW5uaW5nKS4gVmnhu4djIGNoaWEgYmnhur9uIGxpw6puIHThu6VjIHRow6BuaCBjw6FjIG5ow7NtIG5oxrAgduG6rXkgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiB24bubaSBt4bulYyDEkcOtbmggbMOgIHTDrW5oIHRvw6FuIFdvRSAoYuG6sW5nIGNo4bupbmcgY8OzIHRy4buNbmcgc+G7kSBXb0UgLSBXZWlnaHQgb2YgRXZpZGVuY2UpIHbDoCBnacOhIHRy4buLIHRow7RuZyB0aW4gSVYgKEluZm9ybWF0aW9uIFZhbHVlKSBj4bunYSBiaeG6v24gc+G7kS4gTeG7mXQgdsOtIGThu6UgbWluaCBo4buNYSDEkcaw4bujYyB0csOsbmggYsOgeSDhu58gdHJhbmcgODAgY3Xhu5FuIFtDcmVkaXQgUmlzayBTY29yZWNhcmRzOiBEZXZlbG9waW5nIGFuZCBJbXBsZW1lbnRpbmcgSW50ZWxsaWdlbnQgQ3JlZGl0IFNjb3JpbmddKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vQ3JlZGl0LVJpc2stU2NvcmVjYXJkcy1JbXBsZW1lbnRpbmctSW50ZWxsaWdlbnQvZHAvMDQ3MTc1NDUxWCkgw6FwIGThu6VuZyBjaG8gYmnhur9uIHR14buVaSAoQWdlKSBuaMawIHNhdTogDQoNCg0KIVtdKEM6XFxVc2Vyc1xcWmJvb2tcXERlc2t0b3BcXHBpY1xccGljMS5wbmcpDQoNClbDoCB0w61uaCB0b8OhbiBXb0UvSVYgY8WpbmcgbmjGsCBz4butIGThu6VuZyB0acOqdSBjaHXhuqluIElWIGNobyBjaOG7jW4gYmnhur9uIHPhu5EgbmjGsCBzYXUgKHRyYW5nIDgxKTogDQoNCiFbXShDOlxcVXNlcnNcXFpib29rXFxEZXNrdG9wXFxwaWNcXHBpYzIucG5nKQ0KDQpMxrB1IMO9IHLhurFuZyBrxKkgdGh14bqtdCBiaW5uaW5nIGJp4bq/biBuw6B5IGPDsm4gw6FwIGThu6VuZyBjaG8gY+G6oyBiaeG6v24gxJHhu4tuaCB0w61uaCBjaOG7qSBraMO0bmcgcmnDqm5nIGfDrCBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZy4gDQoNCkNow7puZyB0YSBjw7MgdGjhu4Mgdmnhur90IG3hu5l0IGjDoG0gYmlubmluZyBiaeG6v24gc+G7kSDEkeG7k25nIHRo4budaSB0w61uaCB0b8OhbiBsdcO0biBXb0UvSVYgY8WpbmcgbmjGsCB0aOG7sWMgaGnhu4duIGPDoWkgZ+G7jWkgbMOgIFdvRSBUcmFuc2Zvcm1hdGlvbiBjaG8gYmnhur9uIHPhu5EgdsOgIHPhur0gc+G7rSBk4bulbmcgaMOgbSBuw6B5IGNobyBi4buZIHPhu5EgbGnhu4d1IGhtZXEuY3N2IGPhu6dhIGN14buRbiBbQ3JlZGl0IFJpc2sgQW5hbHl0aWNzOiBNZWFzdXJlbWVudCBUZWNobmlxdWVzLCBBcHBsaWNhdGlvbnMsIGFuZCBFeGFtcGxlcyBpbiBTQVNdKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vQ3JlZGl0LVJpc2stQW5hbHl0aWNzLU1lYXN1cmVtZW50LUFwcGxpY2F0aW9ucy9kcC8xMTE5MTQzOTg1KSBuaMawIHNhdTogDQoNCg0KYGBge3J9DQojIExvYWQgbeG7mXQgc+G7kSBwYWNrYWdlcyBjaG8gZGF0YSBtYW5pcHVsYXRpb246IA0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KG1hZ3JpdHRyKQ0KDQpsaWJyYXJ5KFNjaVZpZXdzKQ0KbGlicmFyeShIbWlzYykNCg0KIyBJbXBvcnQgZGF0YTogDQpobWVxIDwtIHJlYWQuY3N2KCJodHRwOi8vd3d3LmNyZWRpdHJpc2thbmFseXRpY3MubmV0L3VwbG9hZHMvMS85LzUvMS8xOTUxMTYwMS9obWVxLmNzdiIpDQoNCiMgSMOgbSB0aOG7sWMgaGnhu4duIGJpbm5pbmcgdsOgIHTDrW5oIHRvw6FuIFdPRS9JViArIHRo4buxYyBoaeG7h24gbHXDtG4gY8OhaSBn4buNaSBsw6AgIldPRSBUcmFuc2Zvcm1hdGlvbiI6IA0KDQptYW51YWxseV9iaW5uaW5nX1dPRV90cmFucyA8LSBmdW5jdGlvbih5b3VyX2RmLCB5LCB4LCBicmVha19wb2ludHNfc2VsZWN0ZWQpIHsNCg0KICAjIENodeG6qW4gYuG7iyBk4buvIGxp4buHdTogDQogIGRmX2Zvcl93b2UgPC0geW91cl9kZlssIGMoeCwgeSldICU+JSBkcGx5cjo6bXV0YXRlX2lmKGlzLmludGVnZXIsIGFzLm51bWVyaWMpDQogIG5hbWVzKGRmX2Zvcl93b2UpIDwtIGMoIlZhcl9zZWxlY3RlZCIsICJZIikNCiAgDQogIGRmX2Zvcl93b2UgJTw+JSANCiAgICBtdXRhdGUoWSA9IGNhc2Vfd2hlbihZID09IDEgfiAiQmFkIiwgVFJVRSB+ICJHb29kIiksIA0KICAgICAgICAgICBWYXJfc2VsZWN0ZWQgPSBjYXNlX3doZW4oVmFyX3NlbGVjdGVkICVpbiUgYygtSW5mLCBJbmYpIH4gTkEgJT4lIGFzLm51bWVyaWMoKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBUUlVFIH4gVmFyX3NlbGVjdGVkKSkNCiAgDQogICMgVMOtbmggdG/DoW4gV09FL0lWIHRhYmxlOiANCiAgDQogIGRmX2Zvcl93b2UgJT4lIA0KICAgIG11dGF0ZShDdXRfUG9pbnQgPSBjdXQyKFZhcl9zZWxlY3RlZCwgYnJlYWtfcG9pbnRzX3NlbGVjdGVkLCBvbmx5Y3V0cyA9IFQpICU+JSBhcy5jaGFyYWN0ZXIoKSkgJT4lIA0KICAgIG11dGF0ZShDdXRfUG9pbnQgPSBjYXNlX3doZW4oaXMubmEoQ3V0X1BvaW50KSB+ICJNaXNzaW5nIiwgVFJVRSB+IEN1dF9Qb2ludCkpICU+JSANCiAgICBncm91cF9ieShDdXRfUG9pbnQsIFkpICU+JSANCiAgICBjb3VudCgpICU+JSANCiAgICB1bmdyb3VwKCkgJT4lIA0KICAgIHNwcmVhZChZLCBuKSAlPiUgDQogICAgbmEub21pdCgpICU+JSANCiAgICBtdXRhdGUoVG90YWwgPSBCYWQgKyBHb29kKSAlPiUgDQogICAgbXV0YXRlKEJhZFJhdGUgPSBCYWQgLyBUb3RhbCkgJT4lIA0KICAgIG11dGF0ZShQZXJfYmluID0gVG90YWwgLyBzdW0oVG90YWwpKSAlPiUgDQogICAgbXV0YXRlKERHID0gR29vZCAvIHN1bShHb29kKSwgREIgPSBCYWQgLyBzdW0oQmFkKSkgJT4lIA0KICAgIG11dGF0ZShXT0UgPSBsbihERyAvIERCKSkgJT4lIA0KICAgIG11dGF0ZShXT0UgPSBjYXNlX3doZW4oQmFkID09IDAgfiAyLCBHb29kID09IDAgfiAtMiwgVFJVRSB+IFdPRSkpICU+JSANCiAgICBtdXRhdGUoSVYgPSAoREcgLSBEQikqV09FLCANCiAgICAgICAgICAgSVZfVG90YWwgPSBzdW0oSVYpLCBWYXJfTmFtZSA9IHgpIC0+IGRmX3dvZV9jYWwNCiAgDQogIGN1dF9wb2ludCA8LSBkZl93b2VfY2FsICU+JSBwdWxsKEN1dF9Qb2ludCkNCiAgbl9iaW5zIDwtIGN1dF9wb2ludFshc3RyX2RldGVjdChjdXRfcG9pbnQsICJNaXNzaW5nIildICU+JSBsZW5ndGgoKQ0KICBkZl93b2VfY2FsICU8PiUgbXV0YXRlKE5fYmlucyA9IG5fYmlucykNCiAgDQogICMgVMOtbmggdG/DoW4gV09FIFRyYW5zZm9ybWF0aW9uOiANCiAgDQogIGRmX2Zvcl93b2UgJT4lIA0KICAgIG11dGF0ZShDdXRfUG9pbnQgPSBjdXQyKFZhcl9zZWxlY3RlZCwgYnJlYWtfcG9pbnRzX3NlbGVjdGVkLCBvbmx5Y3V0cyA9IFQpICU+JSBhcy5jaGFyYWN0ZXIoKSkgJT4lIA0KICAgIG11dGF0ZShDdXRfUG9pbnQgPSBjYXNlX3doZW4oaXMubmEoQ3V0X1BvaW50KSB+ICJNaXNzaW5nIiwgVFJVRSB+IEN1dF9Qb2ludCkpICU+JSANCiAgICByaWdodF9qb2luKGRmX3dvZV9jYWwsIGJ5ID0gIkN1dF9Qb2ludCIpICU+JSANCiAgICBtdXRhdGUod29lX3RyYW5zID0gV09FKSAlPiUgDQogICAgcHVsbCh3b2VfdHJhbnMpIC0+IHdvZV90cmFuc19kYXRhDQogIA0KICByZXR1cm4obGlzdChpdl90YWJsZV9maW5hbCA9IGRmX3dvZV9jYWwsIHdvZV90cmFuc19kYXRhID0gd29lX3RyYW5zX2RhdGEpKQ0KICANCn0NCg0KIyBT4butIGThu6VuZyBow6BtOiANCm1hbnVhbGx5X2Jpbm5pbmdfV09FX3RyYW5zKGhtZXEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgeSA9ICJCQUQiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHggPSAiTE9BTiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtfcG9pbnRzX3NlbGVjdGVkID0gYyg2MDAwLCA4MDAwLCAxNjAwMCwgMzgwMDApKSAtPiBiaW5uaW5nX3Jlc3VsdHMNCg0KIyBL4bq/dCBxdeG6ozogDQpiaW5uaW5nX3Jlc3VsdHMkaXZfdGFibGVfZmluYWwgJT4lIA0KICBzZWxlY3QoMToxMikgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkge3JvdW5kKHgsIDMpfSkgJT4lIA0KICBrbml0cjo6a2FibGUoKQ0KDQpgYGANCg0KIyBDb25zZXF1ZW5jZXMgb2YgV09FIEJpbm5pbmcgZm9yIExvZ2lzdGljIFJlZ3Jlc3Npb24NCg0KSOG7hyBxdeG6oyB0csaw4bubYyB0acOqbiBj4bunYSB2aeG7h2Mgc+G7rSBk4bulbmcgYmnhur9uIHPhu5EgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiBXb0UgVHJhbnNmb3JtYXRpb24ga2hpIHRo4buxYyBoaeG7h24gaOG7k2kgcXV5IExvZ2lzdGljIMSRxqFuIGJp4bq/biBuaMawIHNhdTogDQoNCjEuIEjhu4cgc+G7kSBjaOG6t24gY+G7p2EgbcO0IGjDrG5oIGjhu5NpIHF1eSBsdcO0biBi4bqxbmcgbG9nKEIvRykgduG7m2kgQiB2w6AgRyBs4bqnbiBsxrDhu6N0IGzDoCBz4buRIGzGsOG7o25nIGjhu5Mgc8ahIEJhZCB2w6AgR29vZC4gDQoyLiBI4buHIHPhu5EgZ8OzYyBj4bunYSBtw7QgaMOsbmggaOG7k2kgcXV5IGx1w7RuIGLhurFuZyAtMS4gDQoNCkhhaSBo4buHIHF14bqjIHRyw6puIGPDsyB0aOG7gyDEkcaw4bujYyBraeG7g20gdHJhIGzhuqFpIHbhu5tpIGLhu5kgZOG7ryBsaeG7h3UgaG1lcSBuaMawIHNhdTogDQoNCg0KYGBge3J9DQojIENvbnNlcXVlbmNlcyBvZiB1c2luZyBXT0UgZm9yIExvZ2lzdGljIFJlZ3Jlc3Npb246ICANCmRmX3dvZSA8LSBkYXRhLmZyYW1lKEJBRCA9IGhtZXEkQkFELCBMT0FOX3dvZSA9IGJpbm5pbmdfcmVzdWx0cyR3b2VfdHJhbnNfZGF0YSkNCmdsbShCQUQgfiBMT0FOX3dvZSwgZmFtaWx5ID0gImJpbm9taWFsIiwgZGF0YSA9IGRmX3dvZSkgJT4lIHN1bW1hcnkoKQ0KYGBgDQoNCkjhu4cgcXXhuqMgdGjhu6kgYmEgbMOgIGNow7puZyB0YSBjw7MgdGjhu4MgdMOtbmggdOG7iSBs4buHIGjhu5Mgc8ahIEJhZCBjaG8gdOG7q25nIG5ow7NtIChoYXkga2hv4bqjbmcgYmlubmluZykgdOG7qyBtw7QgaMOsbmggTG9naXN0aWMuIFbDrSBk4bulLCB24bubaSBuaMOzbSB04burIDExMDAgxJHhur9uIDYwMDAgdGjDrCB04buJIGzhu4cgQmFkIMSRxrDhu6NjIHTDrW5oIHRoZW8gY8O0bmcgdGjhu6ljIHNhdTogDQoNCg0KYGBge3J9DQojIENvbnNlcXVlbmNlcyBvZiB1c2luZyBXT0UgZm9yIExvZ2lzdGljIFJlZ3Jlc3Npb246IA0KDQp0cyA8LSBleHAoLTEqLTEuMjU4IC0gMS4zODk0NCkgDQp0cyAvICh0cyArIDEpDQpgYGANCg0KVMawxqFuZyB04buxIGzDoCBjaG8gbmjDs20gdOG7qyA2MDAwIMSR4bq/biA4MDAwOiANCg0KYGBge3J9DQp0cyA8LSBleHAoLTEqLTAuMzkwIC0gMS4zODk0NCkgDQp0cyAvICh0cyArIDEpDQpgYGANCg0KSGFpIGvhur90IHF14bqjIDAuNDY3IHbDoCAwLjI2OSBjaMOtbmggbMOgIHThu4kgbOG7hyBo4buTIHPGoSBCYWQgKDQ2LjclIHbDoCAyNi45JSkgdMawxqFuZyDhu6luZyB24bubaSBjw6FjIG5ow7NtIG7DoHkgbcOgICBjaMO6bmcgdGEgxJHDoyBiaeG6v3Qg4bufIGLhuqNuZyBJViBUYWJsZSBraGkgc+G7rSBk4bulbmcgaMOgbSBiaW5uaW5nIG3DoCBjaMO6bmcgdGEgxJHDoyB2aeG6v3Qg4bufIHRyw6puLiANCg0KDQojIFNvbWUgUiBQYWNrYWdlcyBmb3IgV29FIEJpbm5pbmcNCg0KSGnhu4duIFIgY8OzIHLhuqV0IG5oaeG7gXUgcGFja2FnZSBz4butIGThu6VuZyBjaG8gYmlubmluZyB2w6AgduG7m2kgbeG7pWMgxJHDrWNoIGzDoCBTY29yZWNhcmQgTW9kZWxsaW5nIHRow6wgW3Njb3JlY2FyZF0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3Njb3JlY2FyZC9pbmRleC5odG1sKSBsw6AgdGh14bqtbiBs4bujaSBoxqFuIGPhuqMgdsOsIHBhY2thZ2UgxJHGsOG7o2Mgdmnhur90IG5o4bqxbSBoaeG7h24gdGjhu7FjIGjDs2EgY8OhY2ggdGjhu6ljIHjDonkgZOG7sW5nIFNjb3JlY2FyZCB0aMaw4budbmcgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiBi4bufaSBjw6FjIG5nw6JuIGjDoG5nIMSRxrDhu6NjIG3DtCB04bqjIHRyb25nIGN14buRbiAqQ3JlZGl0IFJpc2sgU2NvcmVjYXJkczogRGV2ZWxvcGluZyBhbmQgSW1wbGVtZW50aW5nIEludGVsbGlnZW50IENyZWRpdCBTY29yaW5nKi4gVHV5IG5oacOqbiB0w6FjIGdp4bqjIGPhu6dhIHBhY2thZ2UgbsOgeSDEkcOjICJuaOG6p20gbOG6q24iIGtoaSB0w61uaCB0b8OhbiBXb0UgbmjGsCBzYXU6IA0KDQohW10oQzpcXFVzZXJzXFxaYm9va1xcRGVza3RvcFxccGljXFxwaWMzLnBuZykNCg0KxJBp4buBdSBuw6B5IGThuqtuIMSR4bq/biBjw6FjIGdpw6EgdHLhu4sgV29FIHPhur0gbmfGsOG7o2MgZOG6pXUgaG/DoG4gdG/DoG4gduG7m2kgbmjhu69uZyB0w61uaCB0b8OhbiDEkcaw4bujYyBtw7QgdOG6oyB0cm9uZyB0w6BpIGxp4buHdSBj4bunYSBbTmFlZW0gU2lkZGlxaV0oaHR0cHM6Ly93d3cuYW1hem9uLmNvbS9OYWVlbS1TaWRkaXFpL2UvQjAwMUpTSEc2OC9yZWY9ZHBfYnlsaW5lX2NvbnRfYm9va18xKSBuaMawIGNow7puZyB0YSBjw7MgdGjhu4MgdGjhuqV5OiANCg0KYGBge3J9DQojIFTDrW5oIFdvRSBjaG8gYmnhur9uIExPQU4gYuG6sW5nIHPhu60gZOG7pW5nIGPDoWMgaMOgbSBj4bunYSBnw7NpIHNjb3JlY2FyZDogDQpsaWJyYXJ5KHNjb3JlY2FyZCkNCmJpbnMgPC0gd29lYmluKGhtZXEsIHkgPSAiQkFEIiwgIHBvc2l0aXZlID0gIkJBRHwxIikNCmRmMl93b2UgPC0gd29lYmluX3BseShobWVxLCBiaW5zID0gYmlucykNCg0KYmlucyRMT0FOWywgMToxMF0gJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkge3JvdW5kKHgsIDMpfSkgJT4lIA0KICBrbml0cjo6a2FibGUoKQ0KDQpgYGANCg0KRG8gbmfGsOG7o2MgZOG6pXUgbsOqbiBo4buHIHPhu5EgY2jhurduIGPhu6dhIG3DtCBow6xuaCBo4buTaSBxdXkgTG9naXN0aWMgbMO6YyBuw6B5IGzDoCAxOiANCg0KYGBge3J9DQpnbG0oQkFEIH4gTE9BTl93b2UsIGZhbWlseSA9ICJiaW5vbWlhbCIsIGRhdGEgPSBkZjJfd29lKSAlPiUgc3VtbWFyeSgpDQoNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIGtp4buDbSB0cmEgduG7m2kgbeG7mXQgYmnhur9uIHPhu5Ega2jDoWM6IA0KDQpgYGB7cn0NCmdsbShCQUQgfiBWQUxVRV93b2UsIGZhbWlseSA9ICJiaW5vbWlhbCIsIGRhdGEgPSBkZjJfd29lKSAlPiUgc3VtbWFyeSgpDQpgYGANCg0KTmdvw6BpIHNjb3JlY2FyZCB0aMOsIGPDsm4gY8OzIGfDs2kgW2xvZ2lCaW5dKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9sb2dpQmluL2xvZ2lCaW4ucGRmP2ZiY2xpZD1Jd0FSMy1kV21ZOFlicl9SSEw4WGYyLTI1VTJTcXQtcmRiaW1CXzVISmZIRUg3T3pfVGp5VXlwSVJlNUtjKSBjxaluZyBjw7MgdGjhu4MgxJHGsOG7o2Mgc+G7rSBk4bulbmcgxJHhu4MgYmlubmluZyBiaeG6v24gc+G7kSB24bubaSBt4bulYyDEkcOtY2ggbMOgIGJpbm5pbmcgdGhlbyBoxrDhu5tuZyBtb25vdG9uaWMgKHThu6ljIGzDoCDEkcahbiDEkWnhu4d1IHTEg25nIGhv4bq3YyBnaeG6o20gY2hvIG5ow7NtIGjhu5Mgc8ahIEJhZCBob+G6t2MgR29vZCB0aGVvIGPDoWMga2hv4bqjbmcpOiANCg0KDQpgYGB7cn0NCmRldGFjaChwYWNrYWdlOnNjb3JlY2FyZCkNCg0KIyBJbXBvcnQgcGFja2FnZTogDQpsaWJyYXJ5KGxvZ2lCaW4pDQoNCg0KIyBCaW5uaW5nIGRhdGEgY2hvIGJp4bq/biBMT0FOOiANCg0KbXlfYmluIDwtIGdldEJpbnMoaG1lcSwgeSA9ICJCQUQiLCB4VmFycyA9IGMoIkxPQU4iKSwgDQogICAgICAgICAgICAgICAgICBtaW5Qcm9wID0gMC4wNSwgbWluQ3IgPSAwLjIsIG5Db3JlcyA9IDIwKQ0KDQojIE1vbm90b25pYyB0cmVuZCBvciBub3Q6DQpteV9iaW4kdmFyU3VtbWFyeQ0KDQojIElWIGFuZCByZXN1bHRzOiANCm15X2JpbiRiaW4gJT4lIA0KICBzZWxlY3QoLWMoMSwgOSkpICU+JSANCiAga25pdHI6OmthYmxlKCkNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIGJpbm5pbmcgYmnhur9uIExPQU4gbsOgeSBzYW8gY2hvIG1vbm90b25pYyAodMSDbmcgaG/hurdjIGdp4bqjbSk6IA0KDQpgYGB7cn0NCiMgQ2hlY2sgaW5jcmVhc2luZyB0cmVuZDogDQpmb3JjZUluY3JUcmVuZChteV9iaW4sIHhWYXJzID0gYygiTE9BTiIpKSAlPiUgLltbM11dDQoNCiMgQ2hlY2sgZGVjcmVhc2luZyB0cmVuZDogDQpteV9iaW5fZGUgPC0gZm9yY2VEZWNyVHJlbmQobXlfYmluLCB4VmFycyA9IGMoIkxPQU4iKSkNCg0KIyBJViBUYWJsZTogDQppdl90YWJsZSA8LSBteV9iaW5fZGUkYmluICU+JSANCiAgZmlsdGVyKGJpbiAhPSAiVG90YWwiKSAlPiUgDQogIG11dGF0ZShiaW4gPSBzdHJfcmVwbGFjZV9hbGwoYmluLCAiTE9BTiIsICIiKSkgJT4lIA0KICBtdXRhdGUoREcgPSBnb29kcyAvIHN1bShnb29kcyksIERCID0gYmFkcyAvIHN1bShiYWRzKSkgJT4lIA0KICBtdXRhdGUod29lID0gbG9nKERHIC8gREIpKSAlPiUgDQogIG11dGF0ZShpdl90b3RhbCA9IHN1bShpdikpDQoNCmBgYA0KDQpDw7MgdGjhu4MgdGjhuqV5IHThu4kgbOG7hyBo4buTIHPGoSBCYWQgZ2nhuqNtIGThuqduIHRoZW8gdOG7q25nIGtob+G6o25nIHTEg25nIGThuqduIGPhu6dhIExPQU46IA0KDQpgYGB7cn0NCml2X3RhYmxlICU+JSANCiAgc2VsZWN0KC1jKDksIDEwLCAxMSkpICU+JSANCiAga25pdHI6OmthYmxlKCkNCmBgYA0KDQoNClThuqV0IG5oacOqbiBu4bq/dSBz4butIGThu6VuZyBiaeG6v24gc2F1IGtoaSB0aOG7sWMgaGnhu4duIFdvRSBUcmFuc2Zvcm1hdGlvbiB0aMOsIGjhu4cgc+G7kSBnw7NjIGPhu6dhIG3DtCBow6xuaCBo4buTaSBxdXkgxJHGoW4gYmnhur9uIHbhuqtuIGzDoCAtMTogDQoNCmBgYHtyfQ0KIyBXT0UgVHJhbnNmb3JtYXRpb246IA0KY3JlYXRlQmlucyhteV9iaW5fZGUsIGhtZXEsIHhWYXJzID0gIkxPQU4iKSAlPiUgDQogIHNlbGVjdChMT0FOLCBiX0xPQU4pICU+JSANCiAgcmlnaHRfam9pbihpdl90YWJsZSwgYnkgPSBjKCJiX0xPQU4iID0gImJpbiIpKSAlPiUgDQogIHB1bGwod29lKSAtPiBMT0FOX2Jpbm5lZA0KDQojIExvZ2lzdGljIFJlZ3Jlc2lvbjogDQpobWVxICU+JSANCiAgbXV0YXRlKExPQU5fYmlubmVkID0gTE9BTl9iaW5uZWQpICU+JSANCiAgZ2xtKEJBRCB+IExPQU5fYmlubmVkLCBmYW1pbHkgPSAiYmlub21pYWwiLCBkYXRhID0gLikgJT4lIA0KICBzdW1tYXJ5KCkNCg0KYGBgDQoNCg0KIyBTb21lIFJlY29tbWVuZGF0aW9ucw0KDQpEbyBjw6FjaCB0w61uaCBXb0UgY+G7p2EgZ8OzaSBzY29yZWNhcmQgbmfGsOG7o2MgaG/DoG4gdG/DoG4gbsOqbiBj4bqnbiBsxrB1IMO9IMSR4buDIGhp4buHdSBjaOG7iW5oLiBUdXkgbmhpw6puIGdpw6EgdHLhu4sgSVYgdGjDrCB24bqrbiDEkcO6bmcuIE5ndXnDqm4gbmjDom4gbcOgIElWIHbhuqtuIMSRw7puZyB0cm9uZyBraGkgV29FICJzYWkiIGPDoWMgYuG6oW4gY8OzIHRo4buDIGdp4bqjaSB0aMOtY2ggxJHGsOG7o2MgdOG7qyBjw7RuZyB0aOG7qWMgbcO0IHThuqMgY+G7p2EgZ8OzaSBzY29yZWNhcmQuIE5nb+G6oWkgdHLhu6sgInRoaeG6v3Ugc8OzdCIgbsOgeSB0aMOsIGNow7puZyB0YSB24bqrbiBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgZ8OzaSBuw6B5IMSR4buDIHRo4buxYyBoaeG7h24gU2NvcmVjYXJkIE1vZGVsbGluZyBuaMawIMSRw6MgbcO0IHThuqMgdHJvbmcgY3Xhu5FuICBDcmVkaXQgUmlzayBTY29yZWNhcmRzOiBEZXZlbG9waW5nIGFuZCBJbXBsZW1lbnRpbmcgSW50ZWxsaWdlbnQgQ3JlZGl0IFNjb3JpbmcgaG/hurdjICBDcmVkaXQgUmlzayBBbmFseXRpY3M6IE1lYXN1cmVtZW50IFRlY2huaXF1ZXMsIEFwcGxpY2F0aW9ucywgYW5kIEV4YW1wbGVzIGluIFNBUy4gDQoNCg0KVHJvbmcgdMOsbmggaHXhu5FuZyBtw6AgY+G6p24gYmlubmluZyBiaeG6v24gxJHhu4MgdMOsbSBraeG6v20gbW9ub3RvbmljIHRyZW5kICjEkcahbiDEkWnhu4d1IHTEg25nIGhv4bq3YyBnaeG6o20gY+G7p2EgQmFkIFJhdGUpIHRow6wgY2jDum5nIHRhIG7Dqm4gc+G7rSBk4bulbmcgZ8OzaSBsb2dpQmluIG5oxrAgxJHDoyB0csOsbmggYsOgeSDhu58gdHLDqm4uIA0KDQoNCg0KDQo=