Introduction to caret

Nếu Python có thư viện scikit-learn cho các thuật toán Machine Learning thì R có caret - viết tắt của Classification And REgression Training. Có chừng không ít hơn 40 đầu sách về Machine Learning/Data Science sử dụng caret được bán trên Amazon. Một trong số đó là Applied Predictive Modeling của Max Kuhn và Kjell Johnson. Trong hai tác giả này thì Max Kuhn là tác giả của caret.

Trong series 6.0.86 là phiên bản được sử dụng của caret:

# Load caret package: 
library(caret)

# Check version: 
packageVersion("caret")
## [1] '6.0.86'

Hiên tại có 238 thuật toán phân loại và hồi quy được gói này hỗ trợ. Danh sách các thuật toán này cũng như các tham số có thể tinh chỉnh được có thể đọc tại đây hoặc sử dụng R codes dưới đây:

# List all algorithms (or methods) supported by the caret: 

getModelInfo() -> model_info

names(model_info) -> all_ML_models

# Show some ML models: 

tail(all_ML_models)
## [1] "WM"        "wsrf"      "xgbDART"   "xgbLinear" "xgbTree"   "xyf"

Chẳng chạn chúng ta muốn có thêm thông tin và mô tả về thuật toán có “kí hiệu” (hay method) là xgbTree thì sử dụng R codes như sau:

# Extract description about xgbTree: 
getModelInfo("xgbTree") -> xgbTree_info

# Full name of algorithm: 
xgbTree_info$xgbTree$label
## [1] "eXtreme Gradient Boosting"

Như vậy thuật toán có kí hiệu là xgbTree tên đầy đủ là eXtreme Gradient Boosting.

Các thuật toán ML thường có nhiều hơn 1 tham số (Hyper-parameters hay Parameters) mà giá trị của chúng ảnh hưởng đến khả năng phân loại - dự báo của thuật toán và công việc trọng tâm của việc huấn luyện một thuật toán ML là tìm ra những giá trị tối ưu cho tham số - được hiểu là giá trị mà tại đó khả năng phân loại - dự báo của thuật toán ML là cao nhất. Chúng ta sẽ tìm hiểu kĩ quá trình tìm kiếm tham số tối ưu trong các mục sau. Trước hết với mỗi thuật toán ML đã chọn chúng ta cần biết những tham số nào của thuật toán có thể tinh chỉnh (bởi caret). R codes để list ra danh sách các tham số có thể tinh chỉnh của, ví dụ,

# Parameters can be turned by the caret: 
xgbTree_info$xgbTree$parameters
##          parameter   class                          label
## 1          nrounds numeric          # Boosting Iterations
## 2        max_depth numeric                 Max Tree Depth
## 3              eta numeric                      Shrinkage
## 4            gamma numeric         Minimum Loss Reduction
## 5 colsample_bytree numeric     Subsample Ratio of Columns
## 6 min_child_weight numeric Minimum Sum of Instance Weight
## 7        subsample numeric           Subsample Percentage

Như vậy xgbTree có 7 tham số có thể được tinh chỉnh là nrounds, max_depth, eta, gamma, colsample_bytree, in_child_weight và subsample.

Motivation and Approach

Số textbook viết về Machine Learning nói chung và sử dụng caret cho Machine Learning nói riêng là rất nhiều và hầu hết chúng được viết bằng tiếng Anh. Đây cũng là một rào cản đáng kể vì không phải ai cũng có vốn tiếng Anh đủ tốt để nghiên cứu hay đọc chúng. Mặt khác những người trước hết là muốn tìm hiểu, rồi sau đó là việc sử dụng/áp dụng Machine Learning không phải ai cũng có background về khoa học máy tính, khoa học dữ liệu, và background về thuật toán cho nên việc tự học/nghiên cứu những textbook này là một thách thức không nhỏ và cần nhiều thời gian. Đó là lí do series này về Machine Learning sử dụng caret được viết với cách tiếp cận như sau:

  • Giải thích trực quan quy trình thực hiện huấn luyện, đánh giá và tinh chỉnh các thuật toán Machine Learning.
  • ….
  • ….

Mặc định rằng người theo dõi series này đã nắm vững tương đối những thứ dưới đây:

  • Sử dụng ngôn ngữ R, R programming cũng như hai thư viện ggplot2dplyr. Đây lần lượt là hai thư viện cho hình ảnh hóa và biến đổi dữ liệu.

  • Toán - Thống Kê.

Train A Machine Learning: A Short Illutration

Để minh họa quá trình huấn luyện một thuật toán Machine Learning bằng thư viện caret chúng ta xét một thuật toán đơn giản nhất là hồi quy tuyến tính OLS để dự báo Volume theo Height với bộ dữ liệu trees có tất cả 31 quan sát. Đây là bộ dữ liệu luôn thường trực trong R Environment và chúng ta có thể xem qua bộ dữ liệu này:

# Use trees data set: 
head(trees)
##   Girth Height Volume
## 1   8.3     70   10.3
## 2   8.6     65   10.3
## 3   8.8     63   10.2
## 4  10.5     72   16.4
## 5  10.7     81   18.8
## 6  10.8     83   19.7

Để huấn luyện, tinh chỉnh tham số và đánh giá chất lượng của mô hình OLS cũng như bất kì mô hình ML nào thì chúng ta luôn chia bộ dữ liệu ban đầu (Original Data) thành hai phần. Phần thứ nhất màu xám và được gọi là Training + Validation Data. Phần thứ hai là Holdout Data màu da cam như Figure 1 dưới đây:

Giả sử 31 quan sát ban đầu này được chia thành 6 phần bằng (hoặc xấp xỉ nhau) trong đó training + validation chiếm 5/6, còn holdout chiếm 1/6. Cụ thể, các quan sát từ 1 đến 6 là holdout còn phần còn lại là training + validation data. Để làm việc này chúng ta có thể sử dụng hàm slice() của dplyr:

# Load tidyverse package: 
library(tidyverse)

# Remove Girth column: 

trees %>% select(-Girth) -> trees

# Holdout data: 
id <- 1:6 # Observations belong to holdout. 

trees %>% slice(id) -> holdout_data

# Testing + validation data: 
trees %>% slice(-id) -> train_valid_data

Ở đây phần dữ liệu train_valid_data được sử dụng để huấn luyện và tinh chỉnh các tham số của mô hình OLS còn phần dữ liệu holdout_data để kiểm tra ngược lại chất lượng dự báo của mô hình.

Để huấn luyện và đánh giá/tinh chỉnh mô hình OLS (hay bất kì mô hình ML nào) chúng ta sử dụng hàm train() theo cú pháp sau:

# Set conditions for train model: 
ols_controls <- trainControl(method = "repeatedcv", 
                             repeats = 1, 
                             number = 5)


# Train OLS: 
set.seed(1) # For reproduct results. 

train(Volume ~ Height, 
      data = train_valid_data, 
      method = "lm", # this option for training OLS 
      metric = "Rsquared", # indicator selected for training and turning model
      trControl = ols_controls) -> ols_model

Đến đây có thể xem qua kết quả:

ols_model
## Linear Regression 
## 
## 25 samples
##  1 predictor
## 
## No pre-processing
## Resampling: Cross-Validated (5 fold, repeated 1 times) 
## Summary of sample sizes: 19, 20, 21, 19, 21 
## Resampling results:
## 
##   RMSE     Rsquared   MAE     
##   12.4594  0.3965534  11.36471
## 
## Tuning parameter 'intercept' was held constant at a value of TRUE

Hoặc khai thác thêm thông tin về mô hình như sau:

# Extract more information about OLS trained: 
ols_model$resample -> more_info

# Print: 

library(kableExtra) # For presenting table. 

more_info %>% 
  kbl(caption = "Table 1: Model Performance, OLS", escape = TRUE) %>%
  kable_classic(full_width = FALSE, html_font = "Cambria")
Table 1: Model Performance, OLS
RMSE Rsquared MAE Resample
15.643662 0.0522900 15.017649 Fold1.Rep1
6.562818 0.6168795 4.539174 Fold2.Rep1
17.178930 0.4415682 15.731343 Fold3.Rep1
11.110220 0.3838955 9.920240 Fold4.Rep1
11.801394 0.4881338 11.615160 Fold5.Rep1

Những kết quả này, ví dụ, Rsquared = 0.3965534 đến từ đâu? Nếu để ý có thể kiểm tra rằng 0.3965534 chính là trung bình cộng của cột biến Rsquared ở Table 1:

more_info %>% 
  pull(Rsquared) %>% 
  mean()
## [1] 0.3965534

5 giá trị khác nhau của Rsquared trong Table 1 chính là tương ứng vơi các Performance1 cho đến Performance5 trong Figure 1. Performance có thể là bất kì tiêu chí nào được chọn để đánh giá chất lượng của mô hình/thuật toán. Với bài toán hồi quy thì đó có thể là Rsquared, RMSE hoặc MAE.

Giá trị Rsquared = 0.3965534 cũng như mối liên hệ của nó với các kết quả khác sẽ được giải thích chi tiết hơn ở mục dưới đây.

An Short Explanation of Training a Machine Learning Algorithm

Phần dữ liệu train_valid_data có 25 quan sát sẽ được phân chia tiếp thành hai phần nhỏ hơn nữa là training data (màu xám) và validation (màu xanh) mà ở đó training và validation data lần lượt chiếm 4/5 và 1/5. Như vậy train_valid_data được chia thành 5 phần bằng nhau (hoặc xấp xỉ bằng) và cách thức phân chia dữ liệu như vậy được gọi là 5-Fold Cross-Validation. Như vậy sẽ có 5 bộ training và 5 bộ validation data tương ứng. Lúc này OLS sẽ được huấn luyện trên bộ training thứ nhất còn bộ validation tương ứng sẽ được sử dụng để đánh giá ngược lại mô hình và thu được Rsquared = 0.05228999. Kế tiếp OLS lại được huấn luyện trên bộ training thứ hai còn bộ validation tương ứng được sử dụng để đánh giá ngược lại mô hình và thu được Rsquared = 0.61687945. Lặp lại quá trình này cho ba cặp training/validation còn lại chúng ta sẽ có kết quả là cột Rsquared ở Table 1.

Việc thiết lập 5-Fold Cross-Validation có mô tả như trên được thể hiện bằng number = 5 trong code sau:

ols_controls <- trainControl(method = "repeatedcv", 
                             repeats = 1, 
                             number = 5)

Đến đây phát sinh câu hỏi quan trọng sau: Nếu quá trình huấn luyện, đánh giá và tinh chỉnh bằng 5-Fold Cross-Validation như trên thì mô hình OLS cuối cùng được sử dụng cho dự báo sẽ là mô hình OLS nào?.

Câu trả lời là: Đó chính là mô hình OLS được huấn luyện trên toàn bộ 25 quan sát của bộ train_valid_data. Chúng ta có thể sử dụng mô hình OLS “cuối cùng” này để, ví dụ, thự hiện dự báo cho Volume nếu biết các giá trị của Height:

# Use model for predicting Volume: 
predicted_values <- predict(ols_model, holdout_data)

# Show predicted values: 
predicted_values
##        1        2        3        4        5        6 
## 22.78851 14.65225 11.39775 26.04301 40.68828 43.94278

Nếu muốn chúng ta có thể so sánh các giá trị dự báo này với giá trị thực tế:

holdout_data %>% 
  mutate(Volume_predicted = predicted_values)
##   Height Volume Volume_predicted
## 1     70   10.3         22.78851
## 2     65   10.3         14.65225
## 3     63   10.2         11.39775
## 4     72   16.4         26.04301
## 5     81   18.8         40.68828
## 6     83   19.7         43.94278

Quá trình huần luyện, đánh giá và tinh chỉnh có mô tả như ở trên được áp dụng cho mọi thuật toán ML chứ không chỉ riêng cho OLS và sẽ được mô tả ở phần kế tiếp ngay sau đây.

Sampling Techiques for Turning Hyper-parameter

Phần trên đã giới thiệu sơ bộ về Cross-Validation. Mục này chúng ta sẽ tìm hiểu kĩ hơn hai kĩ thuật lấy mẫu (Sampling Techniques) là Cross-Validation và Boostrap được minh họa dưới đây:

Cross-Validation

Theo phương pháp này thì từ bộ dữ liệu ban đầu (đây là bộ dữ liệu sử dụng cho huấn luyện và tinh chỉnh) có 12 quan sát sẽ lấy ra, ví dụ, ba mẫu con khác nhau đều có 8 quan sát gọi là Sampele 1, Sample 2 và Sample 3 (các mẫu con này chính là Training Data) khác nhau và tương ứng với ba mẫu con này là ba mẫu Validation Data đều có 4 quan sát. Thuật toán ML sẽ được huấn luyện, tinh chỉnh và đánh giá trên các bộ dữ liệu con này theo quá trình được mô tả ở trên mà chúng ta đã biết.

Quá trình huấn luyện, tinh chỉnh và đánh giá mô hinh bằng Cross-Validation (chính xác là K-Fold Cross-Validation, với K = 3) như trên được hiện thực hóa bằng R codes như sau với gói caret:

controls_cv3 <- trainControl(method = "repeatedcv", 
                             number = 3, 
                             repeats = 1)

Nếu quá trình này được lặp lại, ví dụ, 5 lần thì được gọi là Repeating K-Fold Cross-Validation và được thực hiện với R codes như sau:

controls_cv3_repeat5 <- trainControl(method = "repeatedcv", 
                                     number = 3, 
                                     repeats = 5)

Một trường hợp đặc biệt của K-Fold Cross-Validation là K bằng số lượng các quan sát của bộ dữ liệu được sử dụng. Lúc này dễ dàng suy ra rằng Validation Data chỉ còn lại đúng 1 quan sát. Tình huống đặc biệt này được gọi là Leave One Out Cross-Validation (thường viết tắt là LOOCV). K càng lớn chúng ta càng mất nhiều gánh nặng tính toán (more computationally burdensome) và do vậy trong thực tiễn thì K = 5 hoặc K = 10 thường được lựa chọn. Molinaro (2005) chỉ ra rằng Cross-Validation khi K = 10 tạo ra kết quả tương tự như LOOCV. Ngoài ra Molinaro (2005) và Kim (2009) cũng cho rằng Repeating K-Fold Cross-Validation có thể làm tăng hiệu quả dự báo của thuật toán trong khi vẫn duy trì được bias nhỏ.

The Boostrap

Chọn mẫu ngẫu nhiên theo phương pháp Boostrap là cách thức lấy mẫu có hoàn lại (Efron và Tibshirani, 1986). Điều này có nghĩa là: không giống như Cross-Validation, các quan sát của mẫu lấy theo phương pháp Boostrap có thể trùng lặp - tức là một quan sát có thể được chọn nhiều hơn 1 lần. Chẳng hạn như Sample 3 thì quan sát hình tròn màu đỏ và hình vuông màu xanh được đều xuất hiện hai lần trong mẫu.

Nhìn chung error rate (khác biệt giữa giá trị thực tế và dự báo) sẽ ít biến động hơn K-Fold Cross-Validation (Efron, 1983). Tuy nhiên, về trung bình thì xấp xỉ 62.3% các quan sát sẽ được chọn lặp lại ít nhất 1 lần nên phương pháp chọn mẫu này là tương tự như K-Fold Cross-Validation khi K = 2. Nếu dữ liệu ban đầu là quá bé thì bias có thể là lớn và do vậy phương pháp chọn mẫu này chỉ nên được sử dụng khi dữ liệu đủ lớn. Efron (1983), Efron và Tibshirani (1997) sau đó đã đề xuất cái gọi là 632 method632+ method nhằm giải quyết các nhược điểm của Boostrap trong một số trường hợp nhưng hai nhánh chọn mẫu Boostrap này không được giải thích và sử dụng trong series bài giảng này. Nếu muốn sử dụng bạn đọc có thể tham khảo thêm bằng cách thay đổi các lựa chọn của hàm trainControl() tại đây.

Bất kể kĩ thuật lấy mẫu là gì chúng ta cũng dễ thấy rằng, ví dụ, có hàng chục ngàn (thậm chí là hàng tỉ) khả năng khác nhau để lấy ra 8 trong số 12 quan sát. Mặt khác, quá trình huấn luyện và tinh chỉnh các thuật toán ML còn liên quan đến nhiều quá trình ngẫu nhiên khác nữa mà không đề cập được trong series này. Do vậy khi huấn luyện và tinh chỉnh các thuật toán ML, để đảm bảo việc tái tạo lại kết quả chúng ta sẽ sử dụng lựa chọn set.seed(1) khi huấn luyện OLS như đã biết dưới đây:

set.seed(1) # For reproduct results. 

train(Volume ~ Height, 
      data = train_valid_data, 
      method = "lm", # this option for training OLS 
      metric = "Rsquared", # indicator selected for training and turning model
      trControl = ols_controls) -> ols_model

Tất nhiên chúng ta có thể thay 1 bằng một số nguyên bất kì nào khác và đương nhiên kết quả sẽ khác. Bạn có thể tự kiểm nghiệm điều này. Số 29 được chọn vì đó là ngày sinh nhật của người viết series này.

Process of Training and Turning a ML Algorithm

Quá trình huấn luyện, tinh chỉnh và thậm chí là lựa chọn giữa các thuật toán ML khác nhau sẽ, nhìn chung, theo các bước có thứ tự dưới đây:

Stage 1: Exploratory Data Analysis (EDA). Bước EDA luôn được thực hiện đầu tiên bất nhằm xác định những đặc điểm của bộ dữ liệu, dạng dữ liệu (Integer, Categorical, Numeric…), dữ liệu thiếu (Missing Data) và mức độ nghiêm trọng của dữ liệu thiếu.

Stage 2: Data pre-processing/Feature Engineering. Đây thường là bước mất nhiều thời gian nhất của mộ dự án và tập trung vào xử lí một số (hoặc tất cả) vấn đề sau:

  • Xử lí Missing Data. Các thuật toán ML đòi hỏi rằng data đầu vào không được phép có dữ liệu thiếu (Missing Data). Vì vậy trước khi data được sử dụng cho một thuật toán ML nào đó thì cần phải xử lí missing data.

  • Nhiều thuật toán ML đòi hỏi rằng data đầu vào tất cả đều phải ở dạng numeric. Do vậy các biến định tính (Categorical Data/Feature) hoặc những kiểu dữ liệu tương tự cần phải được chuyển đổi về numeric. Quá trình này được gọi là Encoding Categorical Data.

  • Xử lí vấn đề các biến tương quan cao.

  • Các phương pháp biến đổi dữ liệu sao cho thuật toán ML có khả năng dự báo cao nhất trên Holdout Data. Gọi chung là Data Transformation.

  • Tạo ra các biến mới từ các biến thuộc bộ dữ liệu nguyên bản. Hoặc ngược lại, chỉ chọn ra một số biến số từ bộ dữ liệu nguyên bản sao cho thuật toán ML có khả năng dự báo cao nhất trên tập Holdout Data. Việc này thường được gọi chung là Feature Engineering/Feature Selection.

Stage 2 là bước quan trọng và thường là chiếm một tỉ lệ đáng kể về thời gian. Do vậy bước này sẽ được tách hẳn thành một mục riêng và sẽ được thảo luận chi tiết hơn trong series bài giảng này.

Stage 3: Spliting Data. Bước này bộ dữ liệu nguyên bản (có thể là đã được xử lí sau khi hoàn thành Stage 2) được phân chia thành hai phần riêng biệt mà chúng ta đã biết: phần để huấn luyện và tinh chỉnh tham số (Training + Validation Data), phần còn lại để đánh giá ngược lại thuật toán ML - thường gọi là Holdout Data (một số tài liệu - sách gọi là Test Data). Không có quy tắc cố định nào về tỉ lệ giữa hai phần này mà tùy vào quy mô của dữ liệu cũng như một số yếu tố khác. Thông thường thì phần training/validation là 70% (hoặc 80%), còn holdout là 30% (hoặc 20%).

Stage 4: Select sampling method for training and turning hyper-parameters. Bước này sẽ xác định/chỉ định phương pháp chọn mẫu để huấn luyện và tinh chỉnh tham số cũng như phương pháp tìm kiếm tham số tối ưu cho thuật toán ML.

Stage 5: Training/Turning ML Algorithm. Bước này được thực hiện để tìm kiếm tham số tối ưu cũng như huấn luyện thuật toán ML với bộ tham số tối ưu tìm được dựa trên phương pháp chọn mẫu và tìm kiếm được xác định ở Stage 4.

Stage 6: Evaluating/Comparing and Selecting. Với các tham số tối ưu tìm được ở Stage 5 thì chất lượng dự báo của thuật toán (gọi là Model Performance) phải được đánh giá và kiểm tra lại trên Holdout/Test Data.

Stage 7: Deployment. Giả sử chúng ta đã tinh chỉnh và huấn luyện được một thuật toán chấm điểm và xếp hạng tín dụng cho các khách hàng có nhu cầu vay tiêu dùng dựa trên các inputs đầu vào là: (1) thu nhập hàng tháng, (2) độ tuổi, và (3) nghề nghiệp. Thế thì chúng ta cần triển khai thuật toán này bằng một cách thức nào đó để, ví dụ, có thể triển khai thuật toán này trên iPhone hoặc các máy tính để bàn khác. Để nhân viên sale của một ngân hàng hay một công ti tài chính nào đó chỉ cần nhập ba thông tin của người muốn vay tiêu dùng là thu nhập, tuổi và nghề nghiệp vào thì sẽ phải nhận được câu trả lời là người đó có được vay hay không. Việc triển khai thuật toán đã được huấn luyện từ trước (Pre-trained Model/Algorithm) thành các App (gọi tắt của Application) trên các thiết bị như iPhone có thể thông qua các dịch vụ tính toán đám mây của một đối tác/công ti khác (Cloud Services) hoặc tự triển khai. Nội dung của Stage 7 này nằm ngoài phạm vi của series bài giảng này nên sẽ không được đề cập chi tiết.

Stage 1 và 2 trong thực tế thường là khâu mất nhiều thời gian nhất của một dự án dữ liệu trong thực tế. Và cũng vì tầm quan trọng của khâu này (đặc biệt là Stage 2) nên khâu này sẽ được tách riêng thành các mục riêng trong series bài giảng này. Các bước còn lại của quá trình huấn luyện - tinh chỉnh một thuật toán ML sẽ được minh họa bằng hai task điển hình của việc áp dụng các thuật toán ML là Regression và Classification dưới đây.

Regression Task: Case of Predicting Housing Value in Boston

Dự báo một đại lượng liên tục (numeric) như giá cổ phiếu, giá nhà được gọi chung là Regression Task. Với nhóm công việc này thì các tiêu chí (Metrics) thường được sử dụng để huấn luyện, tinh chỉnh tham số và lựa chọn các thuật toán khác nhau là MAE (Mean Absolute Error), MSE (Mean Squared Error), RMSE (Root Mean Squared Error) và Rsquared (Coefficient of determination). Các tiêu chí này có thể tìm trong bất kì giáo trình thống kê nhập môn nào và có công thức như sau:

Có nhiều gói của R (R packages) có sẵn những hàm để thực hiện tính toán những metrics này nếu biết đồng thời cả hai thứ: (1) giá trị được dự báo, và (2) giá trị thực tế. Tuy nhiên chúng ta nên tự viết, ví dụ, hàm tính MAE theo công thức mô tả ở trên và đặt tên cho hàm này là calculate_MAE:

# Function calculates MAE: 

calculate_MAE <- function(predicted_values, actuals) {
  
  errors <- actuals - predicted_values
  
  errors_abs <- abs(errors)
  
  n <- length(predicted_values)
  
  mae <- sum(errors_abs) / n
  
  return(mae)
  
}

Chúng ta có thể sử dụng hàm này để tính MAE của mô hình OLS khi sử dụng mô hình này để dự báo như sau:

# Define actual values: 
actual_volumes <- holdout_data %>% pull(Volume)

# Calculate MAE: 
calculate_MAE(predicted_values = predicted_values, actuals = actual_volumes)
## [1] 12.3021

Kết quả này đương nhiên là giống so với việc sử dụng hàm MAE() sẵn có của gói caret như chúng ta có thể thấy:

MAE(pred = predicted_values, obs = actual_volumes)
## [1] 12.3021

Việc tự viết hàm là kĩ năng cần thiết vì rằng có nhiều tình huống chúng ta sẽ không có sẵn hàm để làm một công việc cụ thể nào đó và do vậy cần phải viết hàm.

Để minh họa các bước huấn luyện và tinh chỉnh các thuật toán ML cho nhóm công việc Regression Task chúng ta sẽ sử dụng bộ số liệu Boston gồm 506 quan sát được tích hợp sẵn ở gói MASS (cài đặt bằng lệnh install.packages("MASS")):

# Clear our R environment: 
rm(list = ls()) # This command should be done at the beginning of any data project. 

# Load Boston data: 
data("Boston", package = "MASS")

Bộ dữ liệu này có 14 cột biến (features) sau:

  • 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).

  • nox nitrogen oxides concentration (parts per 10 million).

  • rm average number of rooms per dwelling.

  • age proportion of owner-occupied units built prior to 1940.

  • dis weighted mean of distances to five Boston employment centres.

  • rad index of accessibility to radial highways.

  • tax full-value property-tax rate per $10,000.

  • ptratio pupil-teacher ratio by town.

  • black the proportion of blacks by town.

  • lstat lower status of the population (percent).

  • medv median value of owner-occupied homes in $1000s.

Mục tiêu của chúng ta là dự báo medv (gọi là target variable hay outcome variable) nếu biết giá trị của 13 features còn lại. Có thể xem qua bộ dữ liệu này:

# Show some observations: 
head(Boston)
##      crim zn indus chas   nox    rm  age    dis rad tax ptratio  black lstat
## 1 0.00632 18  2.31    0 0.538 6.575 65.2 4.0900   1 296    15.3 396.90  4.98
## 2 0.02731  0  7.07    0 0.469 6.421 78.9 4.9671   2 242    17.8 396.90  9.14
## 3 0.02729  0  7.07    0 0.469 7.185 61.1 4.9671   2 242    17.8 392.83  4.03
## 4 0.03237  0  2.18    0 0.458 6.998 45.8 6.0622   3 222    18.7 394.63  2.94
## 5 0.06905  0  2.18    0 0.458 7.147 54.2 6.0622   3 222    18.7 396.90  5.33
## 6 0.02985  0  2.18    0 0.458 6.430 58.7 6.0622   3 222    18.7 394.12  5.21
##   medv
## 1 24.0
## 2 21.6
## 3 34.7
## 4 33.4
## 5 36.2
## 6 28.7

Boosted Tree đã biết ở mục trên được chọn để minh họa cho các bước huấn luyện và tinh chỉnh một thuật toán ML. Chú ý rằng chúng ta tạm bỏ qua Stage 1 và Stage 2 và bắt đầu luôn từ Stage 3 - bước phân chia dữ liệu. Cụ thể chúng sẽ lấy ra ngẫu nhiên 400 quan sát để huấn luyện và tinh chỉnh mô hình (phần này gọi chung là Training + Validation Data như ta đã biết). Phần còn lại 106 quan sát là Holdout Data (còn gọi là Test Data).

Ở ví dụ mở đầu chúng ta sử dụng hàm slice() để thực hiện phân chia dữ liệu. Tuy nhiên khi huấn luyện và tính chỉnh các thuật toán ML chúng ta nên sử dụng hàm createDataPartition() của gói caret để phân chia dữ liệu (tìm hiểu thêm về ham này tại đây). Hàm này sẽ bảo toàn tốt nhất có thể được rằng các bộ dữ liệu được phân chia có các đặc điểm thống kê giống như bộ dữ liệu ban đầu, đặc biệt là phân phối của outcome variable:

#==========================
#  Stage 3: Spliting Data
#==========================

# Observations for training + validation data: 

n_obs <- 400

# Total observations of original data: 

n <- nrow(Boston)

# Define the percentage of data that goes to training and validation: 

p <- n_obs / n

# Positions (by row) belong to training + validation data: 

id_for_train <- createDataPartition(y = Boston$medv, p = p, list = FALSE, times = 1)

# Training + validation data: 
data_train_valid <- Boston %>% slice(id_for_train)

# Holdout data: 
data_test <- Boston %>% slice(-id_for_train)

Bước kế tiếp chỉ định/lựa chọn kĩ thuật chọn mẫu để huấn luyện và tinh chỉnh thuật toán, cụ thể là Repeating K-Fold Cross-Validation, K = 4 và lặp lại 5 lần:

#============================================================================
#  Stage 4: Select sampling method for training and turning hyper-parameters
#============================================================================

my_sampling <- trainControl(method = "repeatedcv", 
                            number = 4, 
                            repeats = 5)

Sử dụng hàm train() để huấn luyện Boosted Tree ở chế độ mặc định. Chú ý rằng dấu chấm ở medv ~ . đại diện cho tất cả 13 features còn lại của bộ dữ liệu trừ medv còn metric = “Rsquared” nghĩa là sử dụng tiêu chuẩn Rsquared để tinh chỉnh và lựa chọn tham số tối ưu:

#========================================================
#  Stage 5: Train and turn Boosted Tree (using default)
#========================================================

# Train Boosted Tree: 

set.seed(29)
train(medv ~ ., 
      data = data_train_valid, 
      method = "bstTree", 
      metric = "Rsquared", 
      trControl = my_sampling) -> default_bstTree

# Show results: 

default_bstTree
## Boosted Tree 
## 
## 402 samples
##  13 predictor
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 301, 302, 301, 302, 302, 301, ... 
## Resampling results across tuning parameters:
## 
##   maxdepth  mstop  RMSE      Rsquared   MAE     
##   1          50    4.149816  0.8055344  2.876232
##   1         100    3.943440  0.8159765  2.666701
##   1         150    3.868146  0.8219291  2.601253
##   2          50    3.750711  0.8339411  2.558583
##   2         100    3.575665  0.8466390  2.406693
##   2         150    3.506693  0.8518664  2.344259
##   3          50    3.651287  0.8423931  2.450433
##   3         100    3.541706  0.8510007  2.342021
##   3         150    3.510699  0.8534981  2.310569
## 
## Tuning parameter 'nu' was held constant at a value of 0.1
## Rsquared was used to select the optimal model using the largest value.
## The final values used for the model were mstop = 150, maxdepth = 3 and nu = 0.1.

Một số thông báo đáng chú ý có thể thấy là:

  • Resampling: Cross-Validated (4 fold, repeated 5 times) –> Đây chính là thông tin về kĩ thuật chọn mẫu đã sử dụng.

  • Summary of sample sizes: 300, 302, 301, 303, 300, 302, … –> Dòng thông báo này có nghĩa là tham số tối ưu được tìm dựa trên huấn luyện Boosted Tree trên các mẫu training data có số lượng 300, 302, 301.. và được đánh giá ngược lại trên các mẫu validation data có chừng 100 quan sát. Có tất cả 4×5 = 20 cặp training/validation data với số lượng quan sát tương ứng xấp xỉ là 300 - 100 vì bộ dữ liệu data_train_valid có 402 quan sát.

  • Tuning parameter ‘nu’ was held constant at a value of 0.1 –> Boosted Tree có ba tham số có thể tinh chỉnh. Nhưng sử dụng chế độ mặc định cho huấn luyện và tìm kiếm tham số tối ưu thì tham số nu được cố định là 0.1. Với tham số maxdepth thì các giá trị ứng viên là 1, 2 và 3 còn mstop thì các giá trị ứng viên là 50, 100 và 150. Có thể hiểu là có tất cả 1×3×3×4×5 + 1 = 181 mô hình Boosted Tree đã được huấn luyện.

  • Rsquared was used to select the optimal model using the largest value.The final values used for the model were mstop = 150, maxdepth = 3 and nu = 0.1. –> Dòng thông báo này có nghĩa là Rsquared được sử dụng làm tiêu chí tìm các kiếm tham số tối ưu và bộ tham số tối ưu đó là mstop = 150, maxdepth = 3 và nu = 0.1. Với bộ tham số này thì giá trị bình quân của Rsquared trên 20 lần chọn mẫu, mỗi mẫu có số quan sát xấp xỉ 100 (là số lượng của validation data) sẽ là 0.8534981. Bộ tham số tối ưu này có thể được tìm bằng cách khai thác default_bstTree (gọi là caret object) như sau:

default_bstTree$bestTune
##   mstop maxdepth  nu
## 9   150        3 0.1

Chúng ta có thể khảo sát sự thay đổi khả năng dự báo của Boosted Tree theo tiêu chuẩn Rsquared khi các tham số của thuật toán này thay đổi (Figure 3):

ggplot(default_bstTree) + 
  labs(title = "Figure 3: Model Performance by Parameters", 
       subtitle = "Default Search, nu = 0.1") + 
  theme(legend.position = "top")

Figure 3 chỉ ra rằng có định nu = 0.1, với mọi giá trị của maxdepth thì khả năng dự báo của mô hình tăng khi mstop (Boosting Interations) tăng. Bộ giá trị tham số tối ưu tìm được theo chế độ mặc định này sẽ được sử dụng làm cơ sở để xây dựng Grid Search. Cụ thể, Grid Search - danh sách các ứng viên của tham số sẽ chứa bộ giá trị mstop = 150, maxdepth = 3 và nu = 0.1:

# Define Grid Search for search optimal hyper-parameters: 

grid_bstTree <- expand.grid(nu = seq(0.025, 0.15, by = 0.025), 
                            mstop = seq(25, 200, by = 25), 
                            maxdepth = seq(1, 6, by = 1))

Có thể kiểm tra rằng có tất cả 288 bộ tham số khác nhau:

nrow(grid_bstTree)
## [1] 288

Và chúng ta phải tìm ra trong số 288 này bộ tham số tối ưu cho Boosted Tree bằng việc huấn luyện lại. Vì quá trình này có thể mất khá nhiều thời gian nên chúng ta lồng vào hàm system.time() để ước lượng thời gian mà máy tính cần sử dụng. Vì quá trình tìm kiếm tham số tối ưu theo Full Grid Search có thể mất nhiều thời gian nên chúng ta sử dụng lựa chọn allowParallel = TRUE để tận dụng khả năng tính toán song song đối với những máy tính mà bộ vi xử lí có nhiều nhân (core):

# Reset coditions for searching optimal hyper-parameters: 

my_sampling_full_grid <- trainControl(method = "repeatedcv", 
                                      search = "grid", 
                                      allowParallel = TRUE, 
                                      repeats = 5, 
                                      number = 4)

# Search optimal parameter (and train) for Boosted Tree using Full Grid Search Method: 
set.seed(29)

system.time(
  
  train(medv ~ ., 
      data = data_train_valid, 
      method = "bstTree", 
      metric = "Rsquared", 
      tuneGrid = grid_bstTree, 
      trControl = my_sampling_full_grid) -> fullGrid_bstTree
)
##    user  system elapsed 
## 1087.13    2.58 1158.83

Mất chừng 18 phút để huấn luyện và tinh chỉnh Boosted Tree. Bộ tham số tối ưu tìm được theo Full Grid Search là mstop = 200, maxdepth = 3 và nu = 0.075:

# Show optimal parameters: 
fullGrid_bstTree$bestTune
##     mstop maxdepth    nu
## 120   200        3 0.075

Chúng ta có thể sử dụng Boosted Tree với bộ tham số tối ưu này cho dự báo khi biết 13 features đầu vào:

# Features used for predicting: 
features_input <- data_test %>% select(-medv)

# Actuals: 
actual_medv <- data_test %>% pull(medv)

# Use Boosted Tree for predicting: 
pred_medv <- predict(fullGrid_bstTree, features_input)

# Show some predicted values: 
head(pred_medv)
##        1        2        3        4        5        6 
## 22.65765 35.24177 35.89192 17.74258 18.01212 14.75015

Cho đến lúc này có thể coi là đang có hai mô hình cạnh tranh nhau: Boosted Tree mặc định và Boosted Tree với tham số tối ưu được tìm kiếm dựa trên phương pháp Full Grid Search. Lấy căn cứ gì để chọn mô hình này cho dự báo medv chứ không phải là mô hình còn lại?. Để đánh giá, so sánh hai (hay nhiều) mô hình, thuật toán/cách tiếp cận khác nhau thì trước hết cần lựa chọn một tiêu chí để đánh giá và so sánh. Chúng ta có thể chọn tiêu chí đó là Rsquared. Vì đã có cặp giá trị dự báo và giá trị thực tế nên chúng ta có thể tính chỉ số này bằng cách viết hàm có tên calculate_R2 theo mô tả của James et al. (2018, p70) như sau:

#=========================================================================
#  Stage 6: Evaluate/compare/select ML models/ or alternative approaches
#=========================================================================

# Function calculates Rsquared: 
calculate_R2 <- function(predicted_values, actuals) {
  
  my_cor <- cor(predicted_values, actuals)
  
  r_squared <- my_cor^2
  
  return(r_squared)
}

Sử dụng hàm này cho cặp thực tế - dự báo bởi Boosted Tree:

calculate_R2(pred_medv, actual_medv)
## [1] 0.8742157

Nghĩa là khi sử dụng 13 features đầu vào để dự báo medv bằng Boosted Tree thì 13 features này giải thích được 87.42% biến động của medv.

Khảo sát sự thay đổi khả năng dự báo của Boosted Tree theo tiêu chuẩn Rsquared khi các tham số của thuật toán này thay đổi (Figure 4):

ggplot(fullGrid_bstTree) + 
  labs(title = "Figure 4: Model Performance by Parameters", 
       subtitle = "Full Grid Search") + 
  theme(legend.position = "top", legend.box = "horizontal") + 
  guides(color = guide_legend(nrow = 1))

Từ Figure 4 có thể nhận định rằng nếu chúng ta nới rộng mstop lên nữa thì có thể khả năng dự báo của mô hình còn tăng nữa. Nhận định này là cơ sở cho việc, ví dụ, tập trung tinh chỉnh mstop trong khi cố định maxdepth = 3 và nu = 0.075. Chúng ta thiết lập Grid Search mới như sau:

new_grid <- expand.grid(maxdepth = 3, 
                        nu = 0.075, 
                        mstop = seq(175, 400, by = 25))

Huấn luyện và tinh chỉnh với Grid Search mới này:

# Train Boosted Tree again: 
set.seed(29)

system.time(
  
  train(medv ~ ., 
      data = data_train_valid, 
      method = "bstTree", 
      metric = "Rsquared", 
      tuneGrid = new_grid, 
      trControl = my_sampling_full_grid) -> newGrid_bstTree
)
##    user  system elapsed 
##   78.74    0.28   83.01
# Show new results: 

newGrid_bstTree
## Boosted Tree 
## 
## 402 samples
##  13 predictor
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 301, 302, 301, 302, 302, 301, ... 
## Resampling results across tuning parameters:
## 
##   mstop  RMSE      Rsquared   MAE     
##   175    3.501024  0.8544505  2.307081
##   200    3.493025  0.8551059  2.298754
##   225    3.486846  0.8556573  2.290906
##   250    3.486626  0.8556679  2.289137
##   275    3.487163  0.8556620  2.287614
##   300    3.486518  0.8557657  2.286558
##   325    3.488498  0.8556345  2.287563
##   350    3.489499  0.8555137  2.288650
##   375    3.490238  0.8554706  2.289494
##   400    3.493669  0.8551839  2.292134
## 
## Tuning parameter 'maxdepth' was held constant at a value of 3
## Tuning
##  parameter 'nu' was held constant at a value of 0.075
## Rsquared was used to select the optimal model using the largest value.
## The final values used for the model were mstop = 300, maxdepth = 3 and nu
##  = 0.075.

Khảo sát khả năng dự báo của Boosted Tree khi mstop (Boosting Iterations) thay đổi với chú ý rằng hai tham số còn lại được cố định:

ggplot(newGrid_bstTree) + 
  labs(title = "Figure 5: Model Performance by mstop", 
       subtitle = "New Grid Search") + 
  theme(legend.position = "top")

Figure 5 chỉ ra rằng nhìn chung, mstop tăng thì khả năng dự báo của thuật toán tăng nhưng đến một ngưỡng nào đó thì khả năng dự báo của thuật toán lại giảm. Như vậy tham số tối ưu mới sẽ là mstop = 300, maxdepth = 3 và nu = 0.075:

newGrid_bstTree$bestTune
##   mstop maxdepth    nu
## 6   300        3 0.075

Chúng ta có thể sử dụng Boosted Tree tối ưu hơn này cho dự báo và tính Rsquared:

# Predict with new Boosted Tree: 
pred_medv_better <- predict(newGrid_bstTree, features_input)

# Calculate Rsquared and compare: 
calculate_R2(pred_medv_better, actual_medv)
## [1] 0.8786574

Rsquared = 87.86% cao hơn 87.42% - nói cách khác Boosted Tree với bộ tham số mstop = 300, maxdepth = 3 và nu = 0.075 có khả năng dự báo tốt hơn so với Boosted Tree với bộ tham số mstop = 200, maxdepth = 3 và nu = 0.075. Câu hỏi là bằng cách nào chứng minh được rằng điều này không phải là sự ngẫu nhiên do “tình cờ” chọn được bộ dữ liệu features_input mà “ưu tiên” cho Boosted Tree có mstop = 300, maxdepth = 3 và nu = 0.075 dẫn đến kết quả dự báo sát hơn so với giá trị thực của medv? Cách đơn giản nhất để có thể trả lời câu hỏi này là chúng ta có thể so sánh giá trị trung bình của Rsquared trên 20 bộ Validation Data và những thông tin này có thể được khai thác từ các caret objects như chúng ta đã biết:

# Extract results from fullGrid_bstTree: 
fullGrid_bstTree$results %>% 
  slice(which.max(Rsquared)) %>% 
  select(-MAE, -RMSESD, -MAESD, -RMSE) %>% 
  mutate(Approach = "FullGridSearch") -> df_fullGrid

# Extract results from newGrid_bstTree: 
newGrid_bstTree$results %>% 
  slice(which.max(Rsquared)) %>% 
  select(-MAE, -RMSESD, -MAESD, -RMSE) %>% 
  mutate(Approach = "FocusOn_mstop") -> df_betterGrid

#  Combine the two data frames and show results: 
bind_rows(df_fullGrid, df_betterGrid)
##   maxdepth    nu mstop  Rsquared RsquaredSD       Approach
## 1        3 0.075   200 0.8550911 0.04184562 FullGridSearch
## 2        3 0.075   300 0.8557657 0.04118040  FocusOn_mstop

Giá trị trung bình của Rsquared từ 20 bộ Validation Data khi sử dụng Boosted Tree với bộ tham số mstop = 300, maxdepth = 3 và nu = 0.075 là 0.8557657. Còn giá trị trung bình này của Boosted Tree với bộ tham số mstop = 200, maxdepth = 3 và nu = 0.075 là 0.8550911. Nghĩa là, việc sử dụng Boosted Tree với bộ tham số mstop = 300, maxdepth = 3 và nu = 0.075 tạo ra kết quả dự báo chính xác hơn không phải là ngẫu nhiên. Cũng cần phải nói thêm rằng Boosted Tree với bộ tham số mstop = 300, maxdepth = 3 và nu = 0.075 không những dự báo chính xác hơn mà kết quả dự báo còn ít biến động hơn. Vì độ lệch chuẩn (cột RsquaredSD) thấp hơn.

Cũng có thể kiểm tra rằng nếu sử dụng Boosted Tree mặc định thì Rsquared trên Test Data là cao nhất, lên đến 88% như có thể thấy:

predict(default_bstTree, features_input) -> pred_by_default

calculate_R2(pred_by_default, actual_medv)
## [1] 0.8800521

Nhưng có thể thấy rằng kết quả 88% này trên Test Data không mang tính quy luật vì giá trị trung bình của Rsquared trên 20 bộ Validation là 0.8534981 - thấp nhất trong bộ ba Boosted Tree:

default_bstTree$results %>% 
  slice(which.max(Rsquared)) %>% 
  select(-MAE, -RMSESD, -MAESD, -RMSE) %>% 
  mutate(Approach = "Default") 
##   maxdepth  nu mstop  Rsquared RsquaredSD Approach
## 1        3 0.1   150 0.8534981 0.04435447  Default

Ngoài việc lựa chọn một tiêu chí để huấn luyện và tinh chỉnh thuật toán ML chúng ta cũng có thể lựa chọn một thuật toán/cách tiếp cận làm cơ sở so sánh (Base Line). Giả sử chúng ta chọn thuật toán hồi quy tuyến tính truyền thống Linear Regression (OLS) của thống kê truyền thống làm base line. Như đã biết chúng ta có thể huấn luyện Linear Regression bằng R codes dưới đây:

# Train Linear Regression - OLS: 
set.seed(29)
train(medv ~ ., 
      data = data_train_valid, 
      method = "lm", 
      metric = "Rsquared", 
      trControl = my_sampling_full_grid) -> lr_model

# Show results: 

lr_model
## Linear Regression 
## 
## 402 samples
##  13 predictor
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 301, 302, 301, 302, 302, 301, ... 
## Resampling results:
## 
##   RMSE      Rsquared   MAE     
##   4.713283  0.7377736  3.295485
## 
## Tuning parameter 'intercept' was held constant at a value of TRUE

Giá trị bình quân của Rsquared trên 20 bộ Validation Data là 73.77%. Tất nhiên chúng ta cũng có thể sử dụng cho dự báo medv và tính Rsquared trên Test Data:

# Use Linear Regression for predicting: 
pred_fromOLS <- predict(lr_model, features_input)

# Calculate Rsquared: 
calculate_R2(pred_fromOLS, actual_medv)
## [1] 0.6563089

Rsquared của Linear Regression trên Test Data chỉ là 65.63% - một kết quả thấp hơn đáng kể so với ngay cả Boosted Tree mặc định. Như vậy có thể kết luận là giữa các mô hình/cách tiếp cận cạnh tranh nhau chúng ta thấy rằng Boosted Tree với bộ tham số mstop = 300, maxdepth = 3 và nu = 0.075 nên được sử dụng cho mục đích dự báo medv.

Classification Task: Credit Classification and Scoring

Nếu Tagret Variable là biến kiểu nhị phân (Binomial Data hay Binary Response) - tức là kiểu biến số định tính mà giá trị của biến chỉ thuộc về một trong hai loại và chúng ta cần dự báo một quan sát thuộc loại này hay loại kia thì chúng ta đang có bài toán phân loại (Classification Task). Để minh họa chúng ta lấy ví dụ là bộ dữ liệu GermanCredit (được cung cấp bởi GS Hans Hofmann, Universit”at Hamburg) có 1000 quan sát với Target Variable là Class chỉ nhận một trong hai giá trị là Bad (với ý nghĩa là sẽ không được cấp tín dụng) và Good (với ý nghĩa là sẽ được cấp tín dụng) tại một ngân hàng ở Đức. Bộ dữ liệu này được lưu trữ bởi Center for Machine Learning and Intelligent Systems thuộc University of California Irvine. Bạn đọc quan tâm đến bộ dữ liệu gốc (cũng như download) + các thông tin mô tả về các features có thể tìm thấy ở đây. Tuy nhiên trong mục này chúng ta sẽ sử dụng một phiên bản của bộ dữ liệu này được tích hợp cùng gói caret:

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

# Load GermanCredit data set: 
data("GermanCredit")

# Show some columns + observations: 
GermanCredit %>% 
  select(8:11) %>% 
  head()
##   Telephone ForeignWorker Class CheckingAccountStatus.lt.0
## 1         0             1  Good                          1
## 2         1             1   Bad                          0
## 3         1             1  Good                          0
## 4         1             1  Good                          1
## 5         1             1   Bad                          1
## 6         0             1  Good                          0

Bộ dữ liệu này đã được sử dụng trong nhiều nghiên cứu về ML áp dụng cho bài toán phân loại và xếp hạng tín dụng bởi nhiều tác giả khác nhau. Mức độ chính xác (Accuracy) được lựa chọn làm tiêu chuẩn để so sánh - đánh giá các thuật toán ML khác nhau được cho ở bảng dưới đây:

Random Forest (RF) - một thuật toán thuộc nhóm Ensemble là một thuật toán ML mạnh cho cả bài toán hồi quy lẫn phân loại. Chúng ta sẽ sử dụng thuật toán này cho mục đích phân loại và xếp hạng tín dụng. Trước hết chúng ta cần RF có những tham số nào có thể tinh chỉnh bởi caret:

# Extract description about Random Forest: 
getModelInfo("rf") -> rf_info

# Parameters can be turned by the caret package: 
rf_info$rf$parameters
##   parameter   class                         label
## 1      mtry numeric #Randomly Selected Predictors

Như vậy Randomly Selected Predictors - kí hiệu mtry là tham số có thể được tinh chỉnh. Như đã nói, chúng ta tạm bỏ qua stage 1 và 3 và thực hiện luôn stage 3 là phân chia bộ dữ liệu thành hai phần theo tỉ lệ 80 - 20:

#==========================
#  Stage 3: Spliting Data
#==========================

set.seed(29)
id <- createDataPartition(y = GermanCredit$Class, p = 0.8, list = FALSE, times = 1)

# Training + validation data: 
train_validCreditData <- GermanCredit %>% slice(id)

# Holdout data: 
testCreditData <- GermanCredit %>% slice(-id)

Dưới đây là R codes cho các bước còn lại của quá trình tinh chỉnh - huấn luyện RF mặc định:

#============================================================================
#  Stage 4: Select sampling method for training and turning hyper-parameters
#============================================================================

sampling_status <- trainControl(method = "repeatedcv", 
                                number = 4, 
                                repeats = 5)


#========================================================
#  Stage 5: Train and turn Boosted Tree (using default)
#========================================================

# Train Random Forest with default status: 

set.seed(29)

train(Class ~ ., 
      data = train_validCreditData, 
      method = "rf", 
      metric = "Accuracy", 
      trControl = sampling_status) -> default_rf

# Show results: 

default_rf
## Random Forest 
## 
## 800 samples
##  61 predictor
##   2 classes: 'Bad', 'Good' 
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 600, 600, 600, 600, 600, 600, ... 
## Resampling results across tuning parameters:
## 
##   mtry  Accuracy  Kappa     
##    2    0.71425   0.08001213
##   31    0.75700   0.36127304
##   61    0.74950   0.35449671
## 
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was mtry = 31.

Như vậy khi tinh chỉnh tham số ở chế độ mặc định thì giá trị tối ưu của tham số là mtry = 31 và trung bình Accuracy trên 20 bộ Validation data là 74.95% - một kết quả cũng không quá kém so với kết quả của những tác giả đi trước được list trong bảng ở trên. Nhưng cần lưu ý rằng Accuracy phụ thuộc vào ngưỡng xác suất (Threshold) để phân loại. Kết quả 74.95% là tương ứng với ngưỡng mặc định 0.5. Chúng ta sẽ tìm hiểu kĩ thêm về ảnh hưởng của ngưỡng phân loại lên Accuracy ngay sau đây. Trước hết sử dụng thuật toán đã được tinh chỉnh - huấn luyện để tính xác suất vỡ nợ PF (Probability of Default) với bộ dữ liệu test:

# Actual labels from test data: 
actual_labels <- testCreditData %>% pull(Class)

# Inputs: 
inputs <- testCreditData %>% select(-Class)

# Calculate PD on test data: 

predict(default_rf, inputs, type = "prob") -> df_PD_predicted

# Vector of PD predicted by Random Forest: 
pd_predicted <- df_PD_predicted %>% pull(Bad)

Chúng ta có thể xem qua một số kết quả:

# Show some probability of default for some cases: 
head(df_PD_predicted)
##     Bad  Good
## 1 0.636 0.364
## 2 0.016 0.984
## 3 0.356 0.644
## 4 0.712 0.288
## 5 0.808 0.192
## 6 0.060 0.940

Kết qủa này có nghĩa là với case đầu tiên thì xác suất dự báo cho sự kiện “Bad” là 0.636 và Good là 0.364. Chú ý rằng tổng của hai con số luôn là 1 nhưng chúng ta đã biết từ các giáo trình xác suất thống kê. Dựa trên PD dự báo bởi RF chúng ta có thể, ví dụ, phân loại các hồ sơ xin cấp tín dụng với ngưỡng được chọn cho phân loại là 0.5 như sau:

# Label for cases from PD predicted: 
labels_when_0.5 <- case_when(pd_predicted >= 0.5 ~ "Bad", TRUE ~ "Good")

# Compare with actual labels: 

tibble(actual = actual_labels, predicted = labels_when_0.5, PD = pd_predicted) -> df_labels_0.5

head(df_labels_0.5)
## # A tibble: 6 x 3
##   actual predicted    PD
##   <fct>  <chr>     <dbl>
## 1 Good   Bad       0.636
## 2 Good   Good      0.016
## 3 Good   Good      0.356
## 4 Bad    Bad       0.712
## 5 Bad    Bad       0.808
## 6 Good   Good      0.06

Như vậy với ngưỡng xác suất được chọn cho phân loại là 0.5 thì case đầu tiên có PD = 0.636 sẽ được dán nhãn là Bad còn case thứ hai sẽ có nhãn là Good. Tương tự chúng ta có thể xem kết quả phân loại nếu ngưỡng được chọn là, ví dụ, 0.35 như sau:

labels_when_0.35 <- case_when(pd_predicted >= 0.35 ~ "Bad", TRUE ~ "Good")

tibble(actual = actual_labels, predicted = labels_when_0.35, PD = pd_predicted) -> df_labels_0.35

head(df_labels_0.35)
## # A tibble: 6 x 3
##   actual predicted    PD
##   <fct>  <chr>     <dbl>
## 1 Good   Bad       0.636
## 2 Good   Good      0.016
## 3 Good   Bad       0.356
## 4 Bad    Bad       0.712
## 5 Bad    Bad       0.808
## 6 Good   Good      0.06

Chúng ta cũng có thể tính Accuracy tương ứng với hai ngưỡng được chọn cho phân loại này:

# Accurary when threshold = 0.5: 
sum(df_labels_0.5$actual == df_labels_0.5$predicted) / nrow(df_labels_0.5)
## [1] 0.77
# Accuracy when threshold = 0.35: 
sum(df_labels_0.35$actual == df_labels_0.35$predicted) / nrow(df_labels_0.35)
## [1] 0.745

Như vậy tại các ngưỡng khác nhau thì Accuracy sẽ khác nhau. Tiêu chí Accuracy này cùng với các metrics khác cũng còn được báo cáo trong Ma Trận Nhầm Lẫn (kí hiệu CM) bằng cách sử dụng hàm confusionMatrix() như sau:

# Convert to factor: 
labels_when_0.5 <- factor(labels_when_0.5)

# CM when threshold = 0.5: 
confusionMatrix(labels_when_0.5, actual_labels, positive = "Bad")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Bad Good
##       Bad   28   14
##       Good  32  126
##                                           
##                Accuracy : 0.77            
##                  95% CI : (0.7054, 0.8264)
##     No Information Rate : 0.7             
##     P-Value [Acc > NIR] : 0.01687         
##                                           
##                   Kappa : 0.401           
##                                           
##  Mcnemar's Test P-Value : 0.01219         
##                                           
##             Sensitivity : 0.4667          
##             Specificity : 0.9000          
##          Pos Pred Value : 0.6667          
##          Neg Pred Value : 0.7975          
##              Prevalence : 0.3000          
##          Detection Rate : 0.1400          
##    Detection Prevalence : 0.2100          
##       Balanced Accuracy : 0.6833          
##                                           
##        'Positive' Class : Bad             
## 

Tương tự là CM khi ngưỡng được chọn cho phân loại là 0.35:

# Convert to factor: 
labels_when_0.35 <- factor(labels_when_0.35)

# CM when threshold = 0.5: 
confusionMatrix(labels_when_0.35, actual_labels, positive = "Bad")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Bad Good
##       Bad   43   34
##       Good  17  106
##                                           
##                Accuracy : 0.745           
##                  95% CI : (0.6787, 0.8039)
##     No Information Rate : 0.7             
##     P-Value [Acc > NIR] : 0.09344         
##                                           
##                   Kappa : 0.4383          
##                                           
##  Mcnemar's Test P-Value : 0.02506         
##                                           
##             Sensitivity : 0.7167          
##             Specificity : 0.7571          
##          Pos Pred Value : 0.5584          
##          Neg Pred Value : 0.8618          
##              Prevalence : 0.3000          
##          Detection Rate : 0.2150          
##    Detection Prevalence : 0.3850          
##       Balanced Accuracy : 0.7369          
##                                           
##        'Positive' Class : Bad             
## 

Chọn ngưỡng phân loại là 0.35 thì RF phân loại đúng 43 cases là Bad (trong tổng số 60 cases là Bad được kiểm tra). So với 28 Bad được phân loại đúng (khi Threshold = 0.5) thì số cases được phân loại đúng là tăng hơn 53%.

Ngoài Accuracy, để đọc hiểu một số kết quả quan trọng khác từ CM chúng ta cần hiểu một số tiêu chí đánh giá - so sánh chất lượng/khả năng phân loại của thuật toán ML dưới đây:

  • Dương Tính Giả (False Positive, FP). Đây là những cases là Good nhưng bị dán nhãn sai thành Bad. Bạn không bị nhiễm Covid 19 nhưng bác sĩ kết luận bạn bị nhiễm loại virus này bằng kết luận chính thức là bạn “có kết quả dương tính”. Dương tính giả là như vậy cho dễ nhớ. Trong tình huống của chúng ta thì FN = 14. Tức là có 14 cases thực tế là Good nhưng bị xếp hạng nhầm thành Bad.

  • Âm tính giả (False Negative, FN). Đây là những cases là Bad nhưng bị dán nhãn sai thành Good. Trong tình huống của chúng ta thì FN = 32. Cả FP và FN đều là phân loại sai nhưng hậu quả của FN là nghiêm trọng hơn FP rất nhiều. Bạn bị nhiễm Covid 19 nhưng bác sĩ lại phân loại bạn khỏe mạnh, không bị nhiễm Covid 19 và sẽ lại tiếp tục lây nhiễm thêm cho rất nhiều người. Với các tổ chức tài chính - ngân hàng thì việc phân loại sai một case thực sự là Good thành Bad chỉ làm mất đi của họ cơ hội kiếm lãi trên số vốn cho vay. Nhưng nếu một case là Bad mà bị phân loại sai thành Good thì tổ chức gần như mất hoàn toàn số tiền đã cho vay.

  • Accuracy. Là tổng số cases được phân loại đúng (bao gồm cả nhãn Bad và Good) chia cho tổng số cases được phân loại/xếp hạng. Trong tình huống của chúng ta thì RF phân loại đúng 28 cases có nhãn Bad và 126 cases có nhãn Good. Trong khi đó tổng số cases được xếp hạng là 200. Do vậy mà Accuracy = (28 + 126) / 200 = 0.77 mà chúng ta đã biết.

  • Sensitivity. Là tỉ lệ chính xác khi phân loại nhãn Bad. RF (với ngưỡng 0.5) xác định chính xác 28 cases là Bad. Trong khi thực tế có 28 + 32 = 60 cases là Bad. Do vậy Sensitivity = 28 / 60 = 0.4667. Con số này có thể thấy ngay trên CM.

  • Specificity. Là tỉ lệ chính xác khi phân loại nhãn Good. RF (với ngưỡng 0.5) xác định chính xác 126 cases là Good trong tổng số 140. Do vậy Specificity = 126 / 140 = 0.9. Như vậy thì thuật toán RF của chúng ta với Threshold = 0.5 thì mô hình có khả năng phân loại hồ sơ có nhãn Good với độ chính xác rất cao nhưng lại khá kém khi phân loại hồ sơ có nhãn Bad. Với một tổ chức hoạt động vì lợi nhuận và e ngại rủi ro như ngân hàng thì rõ ràng nó quan tâm hơn đến việc lựa chọn một mô hình có năng lực đủ tốt khi phân loại nhãn Bad - tức Sensitivity.

Ngoài 5 metrics thường được sử dụng khi đánh giá mô hình phân loại như trên thì cũng còn nhiều tiêu chí khác được mô tả chi tiết tại đây. Một lần nữa nhắc lại rằng cả 5 tiêu chí này, nếu các thứ khác không đổi, thì đều phụ thuộc vào ngưỡng được lựa chọn khi phân loại - dán nhãn.

Để khảo sát sự phụ thuộc của Accuracy, Sensitivity và Specificity chẳng hạn vào ngưỡng xác suất được chọn chúng ta viết hàm có tên metrics_with_threshold tính toán ba metrics này khi biết Threshold được chọn:

metrics_with_threshold <- function(threshold) {
  
  # Label for cases from PD: 
  case_when(pd_predicted >= threshold ~ "Bad", TRUE ~ "Good") -> labels_predicted
  
  # Create actual - predicted data frame for purpose of comparision: 
  tibble(actual = actual_labels, predicted = labels_predicted) -> df_compared
  
  # Calculate Accuracy metric: 
  acc_metric <- sum(labels_predicted == actual_labels) / length(labels_predicted)
  
  # Calculate Sensitiviy metric: 
  df_compared %>% filter(actual == "Bad") -> df_bad_sen
  
  df_bad_sen %>% 
    filter(predicted == "Bad") %>% 
    nrow() / nrow(df_bad_sen) -> sen_metric
  
  # Calculate Specificity metric: 
  
  df_compared %>% filter(actual == "Good") -> df_good_spec
  
  df_good_spec %>% 
    filter(predicted == "Good") %>% 
    nrow() / nrow(df_good_spec) -> spec_metric
  
  # Final results in DF form: 
  
  tibble(Accuracy = acc_metric, 
         Sensitiviy = sen_metric,
         Specificity = spec_metric, 
         Threshold = threshold) -> df_report
  
  # Return final outputs: 
  
  return(df_report)
}

Đương nhiên nếu Threshold = 0.5 thì chúng ta sẽ có những kết quả đã biết (Accuracy, Sensitivity, Specificity) như sau:

metrics_with_threshold(threshold = 0.5)
## # A tibble: 1 x 4
##   Accuracy Sensitiviy Specificity Threshold
##      <dbl>      <dbl>       <dbl>     <dbl>
## 1     0.77      0.467         0.9       0.5

Chúng ta sẽ khảo sát sự phụ thuộc của ba tiêu chí này vào Threshold như sau:

# Create some thresholds: 
some_thresholds <- seq(0.1, 0.9, by = 0.05)

# Classification metrics by threshold: 
lapply(some_thresholds, metrics_with_threshold) -> list_of_df

# Convert to DF from list: 
do.call("bind_rows", list_of_df) -> df_results

# Threshold that maximizes Accuracy: 
df_results %>% slice(which.max(Accuracy)) -> max_acc

# Threshold that maximizes Sensitivity: 
df_results %>% slice(which.max(Sensitiviy)) -> max_sen

# Threshold that maximizes Specificity: 
df_results %>% slice(which.max(Specificity)) -> max_spec

Nếu lựa chọn Accuracy làm tiêu chí đánh giá và so sánh các thuật toán ML khác nhau thì chúng ta có thể chỉ ra khi Threshold = 0.45 thì Accuracy là lớn nhất:

max_acc
## # A tibble: 1 x 4
##   Accuracy Sensitiviy Specificity Threshold
##      <dbl>      <dbl>       <dbl>     <dbl>
## 1    0.785      0.567       0.879      0.45

Kết quả 78.5% này, so với các tác giả đi trước ở trên, có thể được coi là cao hơn so với mô hình phân loại của Setiono, Baesens và Mues (2011) với Accuracy = 78.47%. Chúng ta có thể khảo sát sự phụ thuộc của ba metrics này khi ngưỡng phân loại được chọn thay đổi ở Figure 6 dưới đây:

# Convert to long form: 

df_results %>% 
  gather(key = "Metric", value = "Value", -Threshold) -> df_long

# Plot classification metrics vs threshold selected: 

ggplot() + 
  geom_line(data = df_long, aes(Threshold, Value, color = Metric), size = 1) +  
  geom_point(data = max_acc, aes(Threshold, Accuracy), color = "firebrick", size = 2) + 
  scale_x_continuous(breaks = some_thresholds) + 
  theme(legend.position = "top") + 
  labs(y = "Rate", 
       title = "Figure 6: Some Classification Metrics by Threshold, Default RF")

Từ Figure 6 có thể rút ra một số nhận định quan trọng sau:

  • Accuracy có dạng hình chữ U ngược. Nghĩa là ban đầu Threshold tăng thì Accuracy tăng nhưng đến một ngưỡng nào đó (điểm màu đỏ trên Figure 6) thì sẽ lại giảm dần. Pattern này cũng lặp lại nếu ngân hàng lựa chọn lợi nhuận (Profit) chứ không phải Accuracy. Vấn đề này sẽ được đề cập chi tiết trong phần sau.

  • Có sự đánh đổi giữa các metrics. Chúng ta có thể thấy Sensitivity và Specificity tuân theo những patterns ngược chiều nhau: khi threshold tăng thì Sensitivity giảm nhưng Specificity lại tăng (và ngược lại). Điều này hàm ý rõ ràng rằng nếu ngân hàng e ngại rủi ro (có thể là do nền kinh tế bị đình đốn do Covid 19) thì nó sẽ phải điều chỉnh Threshold sao cho mô hình phân loại đạt được mức chính xác cao hơn, ví dụ, trên 70% khi phân loại các cases thực sự là Bad với cái giá phải trả là sẽ bỏ lỡ một số cơ hội kiếm lời khi cấp tín dụng cho các cases Good (nhưng bị phân loại sai thành Bad) do FP tăng. Từ Figure 6 thì có thể thấy nếu đòi hỏi là phân loại đúng các cases là Bad với tỉ lệ chính xác tối thiểu 0.7 thì threshold sẽ khoảng chừng 0.35.

Chúng ta có thể thấy rằng các chỉ số đánh giá chất lượng phân loại của thuật toán được báo cáo trên CM sẽ phụ thuộc ngưỡng được lựa chọn cho phân loại. Điều này có thể tạo ra những khó khăn khi so sánh và lựa chọn giữa các thuật toán ML khác nhau. Chính vì vậy với bài toán phân loại Binary Response thì ROC-AUC là tiêu chí phù hợp hơn làm căn cứ để so sánh và lựa chọn giữa các mô hình ML khác nhau. Chi tiết hơn về tiêu chí này bạn đọc có thể tìm hiểu ở đây. Đây là tiêu chí chỉ phụ thuộc vào PD dự báo từ thuật toán mà thôi.

Với PD đã có chúng ta có thể sử dụng hàm roc() của gói pROC để tính toán chỉ số này:

# Calculate ROC-AUC metric: 

library(pROC)

my_roc <- roc(actual_labels, pd_predicted)

my_roc
## 
## Call:
## roc.default(response = actual_labels, predictor = pd_predicted)
## 
## Data: pd_predicted in 60 controls (actual_labels Bad) > 140 cases (actual_labels Good).
## Area under the curve: 0.8124

Thông báo Area under the curve: 0.8124 có nghĩa là ROC-AUC = 0.8124. Ngoài giá trị của ROC-AUC thì hình dạng của đường cong này cũng là một yếu tố được quan tâm. Chúng ta có thể vẽ đường cong ROC-AUC (Figure 7) như sau:

sen_spec_df <- tibble(TPR = my_roc$sensitivities, FPR = 1 - my_roc$specificities)

sen_spec_df %>% 
  ggplot(aes(x = FPR, ymin = 0, ymax = TPR))+
  geom_polygon(aes(y = TPR), fill = "red", alpha = 0.3)+
  geom_path(aes(y = TPR), col = "firebrick", size = 1.2) +
  geom_abline(intercept = 0, slope = 1, color = "gray37", size = 1, linetype = "dashed") + 
  theme_bw() +
  coord_equal() +
  labs(x = "FPR (1 - Specificity)", 
       y = "TPR (Sensitivity)", 
       title = "Figure 7: Model Performance based on Test Data", 
       subtitle = paste0("AUC Value: ", my_roc$auc %>% round(2)))

Một công cụ hình ảnh tương tự ROC được sử dụng để đánh giá chất lượng của mô hình phân loại là lift chart. Bạn đọc quan tâm có thể tham khảo thêm tại đây hoặc tại đây.

Chúng ta có thể huấn luyện và tinh chỉnh lại RF với tiêu chuẩn ROC-AUC ở chế độ mặc định như sau:

# New metric and sampling technique for searching optimal parameters:  

sampling_new <- trainControl(method = "repeatedcv", 
                             classProbs = TRUE,
                             summaryFunction = twoClassSummary,  # For Binary Response/Classification Task. 
                             number = 4, 
                             repeats = 5)

# Train and turn RF which ROC-AUC used for searching optimal parameters: 

set.seed(29)

train(Class ~ ., 
      data = train_validCreditData, 
      method = "rf", 
      metric = "ROC", # Metric selected for searching optimal parameters. 
      trControl = sampling_new) -> default_rf_auc

# Show results: 

default_rf_auc
## Random Forest 
## 
## 800 samples
##  61 predictor
##   2 classes: 'Bad', 'Good' 
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 600, 600, 600, 600, 600, 600, ... 
## Resampling results across tuning parameters:
## 
##   mtry  ROC        Sens        Spec     
##    2    0.7841667  0.06833333  0.9910714
##   31    0.7715625  0.43083333  0.8967857
##   61    0.7683958  0.44833333  0.8785714
## 
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was mtry = 2.

Với tiêu chuẩn mới là ROC-AUC thì tham số tối ưu tìm được là mtry = 2. Sử dụng RF với tham số tối ưu này để tính ROC-AUC trên tập dữ liệu test như sau:

# Use RF for predicting PD: 
pd_new <- predict(default_rf_auc, inputs, type = "prob")

pd_new %>% pull(Bad) -> pd_new

# ROC-AUC: 
my_roc_new <- roc(actual_labels, pd_new)

my_roc_new
## 
## Call:
## roc.default(response = actual_labels, predictor = pd_new)
## 
## Data: pd_new in 60 controls (actual_labels Bad) > 140 cases (actual_labels Good).
## Area under the curve: 0.8111

ROC-AUC = 0.8111 không có nghĩa là RF với tham số tối ưu theo tiêu chuẩn ROC-AUC (mtry = 2) có khả năng phân loại kém hơn RF với tham số tối ưu được tìm kiếm theo tiêu chuẩn Accuracy (mtry = 31). Vì chúng ta chưa thể bắc bỏ được tình huống rằng kết quả 0.8111 này chỉ là tình cờ do mẫu dữ liệu Test Data bất lợi cho ROC-AUC ứng với tham số tinh chỉnh theo tiêu chuẩn ROC-AUC.

Dựa trên tham số tối ưu tìm được ở chế độ mặc định chúng ta có thể sử dụng giá trị mtry = 2 này để tìm ra tham số tối ưu mới bằng phương pháp Full Grid Search với R codes như sau:

# List all potential paramter (mtry) for Random Forest: 
grid_for_RF <- expand.grid(mtry = seq(2, 11, by = 1))

# Search optimal parameter (and train) for RF using Full Grid Search Method: 

set.seed(29)

system.time(
  
  train(Class ~ ., 
        data = train_validCreditData, 
        method = "rf", 
        metric = "ROC", # Metric selected for search optimal parameters. 
        tuneGrid = grid_for_RF, 
        trControl = sampling_new) -> rf_fullGrid
  )
##    user  system elapsed 
##  174.84    3.02  182.74
# Show results: 
rf_fullGrid
## Random Forest 
## 
## 800 samples
##  61 predictor
##   2 classes: 'Bad', 'Good' 
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 600, 600, 600, 600, 600, 600, ... 
## Resampling results across tuning parameters:
## 
##   mtry  ROC        Sens        Spec     
##    2    0.7843839  0.06916667  0.9914286
##    3    0.7853810  0.25666667  0.9589286
##    4    0.7891845  0.32916667  0.9446429
##    5    0.7883839  0.36166667  0.9375000
##    6    0.7849613  0.37750000  0.9314286
##    7    0.7873810  0.39333333  0.9282143
##    8    0.7846845  0.40166667  0.9221429
##    9    0.7850476  0.40833333  0.9203571
##   10    0.7832798  0.40333333  0.9171429
##   11    0.7832083  0.41083333  0.9150000
## 
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was mtry = 4.

Tham số tối ưu tìm được là mtry = 4 và giá trị trung bình của ROC-AUC trên 20 bộ Validation Data là 0.7891845 - một kết quả cao hơn nếu so với RF được tinh chỉnh mặc định có mtry = 2 với trung bình ROC-AUC là 0.7841667. Sự thay đổi của ROC-AUC trung bình (trên 20 bộ Validation) theo mtry có thể thấy trên Figure 9 dưới đây:

ggplot(rf_fullGrid) + 
  scale_x_continuous(breaks = seq(2, 11, 1)) + 
  labs(title = "Figure 9: Model Performance by mtry", 
       subtitle = "Full Grid Search for Random Forest") + 
  theme(legend.position = "top")

Figure 9 chỉ ra rằng mtry = 4 là giá trị tối ưu của tham số cho RF với tiêu chuẩn tinh chỉnh và lựa chọn là ROC-AUC.

Với nhóm công việc Classification thì còn có một cách tiếp cận phổ biến (và lâu đời) là hồi quy Logistic (gọi là hồi quy nhưng đây là thuật toán phân loại dựa trên xác suất dự báo). Chúng ta sẽ sử dụng cách thuận toán/mô hình này làm Base Line để so sánh các thuật toán khác. R codes để “huấn luyện” mô hình Logistic như sau:

set.seed(29)

train(Class ~ ., 
      data = train_validCreditData, 
      method = "glm", # This option for Logistic Regression. 
      metric = "ROC", # Metric selected for searching optimal parameters. 
      trControl = sampling_new) -> logistic

# Show results: 

logistic
## Generalized Linear Model 
## 
## 800 samples
##  61 predictor
##   2 classes: 'Bad', 'Good' 
## 
## No pre-processing
## Resampling: Cross-Validated (4 fold, repeated 5 times) 
## Summary of sample sizes: 600, 600, 600, 600, 600, 600, ... 
## Resampling results:
## 
##   ROC        Sens   Spec     
##   0.7722143  0.485  0.8596429

Giá trị trung bình trên 20 bộ Validation Data của Logistic Regression là 0.772381 - thấp hơn so với 0.7841667 của RF được tinh chỉnh theo tiêu chuẩn ROC-ACU ở chế độ mặc định (mtry tối ưu là 2). Sử dụng Logistic Regression để tính toán PD trên bộ dữ liệu Test:

# Use Logistic Regrression for predicting PD: 
pd_from_logistic <- predict(logistic, inputs, type = "prob")

pd_from_logistic %>% pull(Bad) -> pd_from_logistic

# Some PD predicted by LR: 
head(pd_from_logistic)
## [1] 0.22283317 0.10531536 0.35150209 0.46525952 0.56023603 0.06383618

PD này có thể được sử dụng cho, ví dụ, so sánh và đánh giá ảnh hưởng của việc sử dụng mô hình LR lên lợi nhuận của ngân hàng so với cách cách tiếp/thuật toán khác để từ đó làm căn cứ cho việc lựa chọn mô hình/thuật toán nào để phân loại - xếp hạng tín dụng.

Select ML Model by Profit Metric

Các tiêu chuẩn (hay metric) được đề cập ở trên, nhất là ROC-AUC thường được sử dụng như là các tiêu chuẩn để đánh giá - so sánh các thuật toán ML khác nhau và chúng thường xuất hiện trên các papers. Nhưng với các tổ chức hoạt động vì lợi nhuận như ngân hàng thì thứ mà chúng quan tâm hơn là: khi mọi thứ như nhau (Ceteris paribus) thì, ví dụ, việc sử dụng phương án A có mang lại lợi ích kinh tế cao hơn so với dùng phương án B hay không?.

Nếu coi thuật toán hồi quy Logistic là phương án A còn RF là phương án B, và lựa chọn lợi nhuận (Profit) thì chúng ta cần đánh giá xem phương án A hay B sẽ mang lại lợi nhuận cao hơn cho ngân hàng. Chúng ta sẽ sử dụng cách tiếp cận thường được gọi là A/B Test để so sánh nhằm tìm ra câu trả lời cho câu hỏi này. Trước hết chúng ta định nghĩa tiêu chí lợi nhuận một cách đơn giản dựa trên hai giả thuyết sau:

  • Lãi là 30% trên số tiền cho vay đố với các cases là Good và được phân loại đúng là Good.

  • Khi cho các hồ sơ vốn là hồ sơ xấu (Bad) nhưng mô hình phân loại sai thành tốt (Good) thì ngân hàng sẽ mất vốn hoàn toàn.

Vì rằng kết quả phân loại sẽ thay đổi nếu ngưỡng được chọn cho dán nhãn (Bad hay Good) thay đổi nên chúng ta sẽ viết hàm tính toán lợi nhuận tương ứng với một ngưỡng cụ thể. Mặt khác để loại bỏ, hoặc ít nhất là giảm thiểu tối đa ảnh hưởng của yếu tố ngẫu nhiên, thì ứng với mỗi ngưỡng được lựa chọn chúng ta sẽ tính toán lợi nhuận này trên 1000 lần chọn mẫu ngẫu nhiên bằng cách lấy ra 100 quan sát bất kì trong số 200 quan sát của Test Data.

Để minh họa chúng ta xét một tình huống cụ thể với PD dự báo từ RF (với mtry = 31) và threshold được chọn cho dán nhãn, ví dụ, là 0.35. Trước hết tạo data frame chứa hai ba thông tin cơ bản là nhãn thực tế (actual), nhãn dự báo (predicted_class) và số tiền được duyệt vay do người xin cấp tín dụng đề xuất (Amount):

# Select threshold for classification from PD predicted: 
threshold <- 0.35

# Use threshold for labelling: 

labels_predicted <- case_when(pd_new >= threshold ~ "Bad", TRUE ~ "Good")

# Create data frame that contains actual and predicted lablels:   
tibble(actual = actual_labels, predicted_class = labels_predicted) -> df_for_com

# Add amount of loans and interest of rate 30%: 
  
df_for_com %>% 
  mutate(Amount = testCreditData$Amount) %>% 
  mutate(r = 0.3) -> df_for_calculating_profit

# Calculate profit: 
df_for_calculating_profit %>% 
  mutate(profit = case_when(actual == "Good" & predicted_class == "Good" ~ Amount*r, 
                            actual == "Bad" & predicted_class == "Good" ~ -1*Amount, 
                            TRUE ~ 0)) -> df_profit

# Some first observations: 
head(df_profit)
## # A tibble: 6 x 5
##   actual predicted_class Amount     r profit
##   <fct>  <chr>            <int> <dbl>  <dbl>
## 1 Good   Bad               7882   0.3     0 
## 2 Good   Good              2835   0.3   850.
## 3 Good   Good              6948   0.3  2084.
## 4 Bad    Good              1282   0.3 -1282 
## 5 Bad    Bad              12579   0.3     0 
## 6 Good   Good              3430   0.3  1029

Từ kết quả này có thể thấy, ví dụ, với case đầu tiên thì do mô hình phân loại là Bad (dù trong thực tế là Good) nên ngân hàng sẽ bỏ lỡ cô hội kiếm lãi 30%. Case thứ hai mô hình phân loại đúng là Good và ngân hàng sẽ kiếm được lãi là 850 = 2835×0.3. Case thứ 4 thì mô hình phân loại là Good nhưng thực tế lại là Bad nên ngân hàng sẽ mất toàn bộ số tiền cho vay là 1283 (hãy lãi là -1282). Như vậy lãi sẽ là tổng của cột profit:

# Profit for all cases classified as Good by model:  
df_profit %>% 
  pull(profit) %>% 
  sum() -> total_profit

print(total_profit)
## [1] 18531.4

Tổng tiền lãi thu được sẽ là 18531. Sử dụng kết quả này chúng ta có thể tính lợi nhuận tính trên mỗi một đồng vốn cho vay:

# Profit per 1$: 
df_profit %>% 
  filter(predicted_class == "Good") %>% 
  pull(Amount) %>% 
  sum() -> total_money_out

total_profit / total_money_out
## [1] 0.03821664

Như vậy cứ mỗi một đồng vốn cho vay thì ngân hàng sẽ thu về 0.038 đồng lãi. Nếu muốn chúng ta cũng có thể xem các thước đo đánh giá chất lượng phân loại khác của mô hình nếu ngưỡng được chọn là 0.35 như đã biết:

# Convert to factor: 
factor(labels_predicted ) -> labels_predicted

# Some metrics at threshold 0.35: 
confusionMatrix(labels_predicted, actual_labels, positive = "Bad")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Bad Good
##       Bad   20   13
##       Good  40  127
##                                           
##                Accuracy : 0.735           
##                  95% CI : (0.6681, 0.7948)
##     No Information Rate : 0.7             
##     P-Value [Acc > NIR] : 0.1578863       
##                                           
##                   Kappa : 0.276           
##                                           
##  Mcnemar's Test P-Value : 0.0003551       
##                                           
##             Sensitivity : 0.3333          
##             Specificity : 0.9071          
##          Pos Pred Value : 0.6061          
##          Neg Pred Value : 0.7605          
##              Prevalence : 0.3000          
##          Detection Rate : 0.1000          
##    Detection Prevalence : 0.1650          
##       Balanced Accuracy : 0.6202          
##                                           
##        'Positive' Class : Bad             
## 

Để khảo sát sự biến đổi của profit (và một số metrics khác) khi ngưỡng được chọn thay đổi chúng ta viết hàm có tên some_metrics_with_threshold nhận inputs đầu vào là: (1) mô hình/thuật toán được chọn, và (2) ngưỡng threshold cho phân loại được chọn và tính toán outputs là các metrics:

# Function calculates some metrics (including profit) at given threshold and model selected: 
some_metrics_with_threshold <- function(model_selected, threshold) {
  
  # Calculate PD by model selected: 
  
  df_pd <- predict(model_selected, inputs, type = "prob")
  
  df_pd %>% pull(Bad) -> pd
   
  # Create data frame that contains actual and predicted lablels: 
  labels_predicted <- case_when(pd >= threshold ~ "Bad", TRUE ~ "Good")
  
  # Create actual - predicted data frame for purpose of comparision: 
  tibble(actual = actual_labels, predicted = labels_predicted) -> df_compared
  
  # Calculate Accuracy metric: 
  acc_metric <- sum(labels_predicted == actual_labels) / length(labels_predicted)
  
  # Calculate Sensitiviy metric: 
  df_compared %>% filter(actual == "Bad") -> df_bad_sen
  
  df_bad_sen %>% 
    filter(predicted == "Bad") %>% 
    nrow() / nrow(df_bad_sen) -> sen_metric
  
  # Calculate Specificity metric: 
  
  df_compared %>% filter(actual == "Good") -> df_good_spec
  
  df_good_spec %>% 
    filter(predicted == "Good") %>% 
    nrow() / nrow(df_good_spec) -> spec_metric
  
  # Calculate profit and some metrics at given threshold: 
  
  df_compared %>% 
    mutate(Amount = testCreditData$Amount) %>% 
    mutate(r = 0.3) %>% 
    mutate(profit = case_when(actual == "Good" & predicted == "Good" ~ Amount*r, 
                              actual == "Bad" & predicted == "Good" ~ -1*Amount, 
                              TRUE ~ 0)) -> df_profit
  
  df_profit %>% 
    pull(profit) %>% 
    sum() -> prof
  
  # Final results in DF form: 
  
  tibble(Accuracy = acc_metric, 
         Sensitiviy = sen_metric,
         Specificity = spec_metric, 
         Profit = prof, 
         Threshold = threshold) -> df_report
  
  # Return final outputs: 
  
  return(df_report)
}

Chúng ta có thể test sự vận hành của hàm này với threshold = 0.35 và mô hình được chọn là RF với mtry = 31 (tinh chỉnh theo tiêu chuẩn ROC-ACU ở chế độ mặc định):

some_metrics_with_threshold(default_rf_auc, 0.35)
## # A tibble: 1 x 5
##   Accuracy Sensitiviy Specificity Profit Threshold
##      <dbl>      <dbl>       <dbl>  <dbl>     <dbl>
## 1    0.735      0.333       0.907 18531.      0.35

Chúng ta khảo sát sự biến đổi model performance (bao gồm cả profit) cho cả bốn mô hình/thuật toán/cách tiếp cận khi threshold thay đổi như sau:

# For LR: 
lapply(some_thresholds, function(x) {some_metrics_with_threshold(logistic, x)}) -> results_logistic_list

do.call("bind_rows", results_logistic_list) -> df_results_lr

df_results_lr %>% mutate(Model = "Logistic") -> df_results_lr

# For RF by accuracy metric: 

lapply(some_thresholds, function(x) {some_metrics_with_threshold(default_rf, x)}) -> results_rf_acc

do.call("bind_rows", results_rf_acc) -> df_results_rf_acc

df_results_rf_acc %>% mutate(Model = "RF-Accuracy") -> df_results_rf_acc

# For RF by ROC-ACU default search: 

lapply(some_thresholds, function(x) {some_metrics_with_threshold(default_rf_auc, x)}) -> results_rf_auc

do.call("bind_rows", results_rf_auc) -> df_results_rf_auc

df_results_rf_auc %>% mutate(Model = "RF-AUC") -> df_results_rf_auc

# For RF by ROC-AUC full grid search: 

lapply(some_thresholds, function(x) {some_metrics_with_threshold(rf_fullGrid, x)}) -> results_rf_aucFull

do.call("bind_rows", results_rf_aucFull) -> df_results_rf_aucFull

df_results_rf_aucFull %>% mutate(Model = "RF-Full") -> df_results_rf_aucFull

So sánh các phương án:

# Combine results: 
df_results_lr %>% 
  bind_rows(df_results_rf_acc) %>% 
  bind_rows(df_results_rf_auc) %>% 
  bind_rows(df_results_rf_aucFull) -> df_big

# Convert to long form: 
df_big %>% 
  gather(Metric, Value, -Model, -Threshold) -> df_big_long

# Threshold tha maximizes Accuracy by model/ML algorithm: 
df_big_long %>% 
  filter(Metric == "Accuracy") %>% 
  group_by(Model) %>% 
  ungroup() %>% 
  slice(which.max(Value)) -> df_max_acc

# Threshold tha maximizes profit by model/ML algorithm: 
df_big_long %>% 
  filter(Metric == "Profit") %>% 
  group_by(Model) %>% 
  slice(which.max(Value)) -> df_max_profit

Như vậy với kịch bản sử dụng LR làm mô hình xếp hạng và phân loại tín dụng thì ngưỡng tối ưu để đạt tối đa hóa profit là 0.4 và lúc này profit là 56730:

df_max_profit
## # A tibble: 4 x 4
## # Groups:   Model [4]
##   Threshold Model       Metric  Value
##       <dbl> <chr>       <chr>   <dbl>
## 1      0.4  Logistic    Profit 56730.
## 2      0.25 RF-Accuracy Profit 52787.
## 3      0.3  RF-AUC      Profit 48331.
## 4      0.25 RF-Full     Profit 51082.

Chúng ta có thể khảo sát sự thay đổi của profit khi threshold thay đổi cho cả 4 mô hình/thuật toán (Figure 9):

df_big_long %>% 
  ggplot(aes(Threshold, Value, color = Model)) + 
  geom_line(size = 1) + 
  geom_point(data = df_max_profit %>% ungroup() %>% slice(which.max(Value)), 
             aes(Threshold, Value), shape = 18, size = 2.5, color = "firebrick") + 
  facet_wrap(~ Metric, scales = "free") + 
  scale_x_continuous(breaks = some_thresholds) + 
  theme(panel.grid.minor = element_blank()) + 
  theme(legend.position = "top") + 
  labs(y = NULL, 
       x = NULL, 
       title = "Figure 9: Threshold that maximizes Profit by Model")

Figure 9 cho thấy:

  • Nếu chọn profit làm tiêu chuẩn so sánh và lựa chọn mô hình/thuật toán thì LR sẽ là mô hình mang lại lợi nhuận lớn nhất so với 3 mô hình/thuật toán còn lại. Ngưỡng phân loại để tối ưu hóa profit khi sử dụng LR là 0.4 và khi đó Max Profit = 56730.

  • RF có tham số tinh chỉnh theo tiêu chuẩn Accuracy, và tại ngưỡng tối ưu tương ứng là 0.25 là mô hình/thuật toán tốt thứ hai căn cứ theo tiêu chuẩn profit (đường RF-Accuracy).

  • RF có tham số tinh chỉnh theo tiêu chuẩn tối đa ROC-AUC ở chế độ mặc định (đường RF-AUC), và tại ngưỡng tối ưu tương ứng là 0.3 sẽ tạo ra profit lớn nhất là 48331 nhưng ngay sau ngưỡng tối ưu này thì profit giảm rất nhanh. Nói cách khác, profit có biến động rất lớn xung quanh ngưỡng tối ưu tương ứng của mô hình/thuật toán này.

  • Accuracy và Profit có dạng hình chữ U ngược bất kể mô hình/thuật toán là gì. Điều này hàm ý rằng: với mỗi một mô hình/thuật toán thì tồn tại một ngưỡng mà Accuracy/Profit là lớn nhất và vượt qua ngưỡng này thì hai tiêu chí này bắt đầu giảm. Điều đáng chú ý và giá trị hơn là với một mô hình/thuật toán thì ngưỡng mà tại đó Accurcy lớn nhất không phải là ngưỡng làm cho Profit lớn nhất.

  • Có sự đánh đổi giữa các tiêu chí, đặc biệt là Sensitivity và Specificity. Tùy bối cảnh và đòi hỏi người làm mô hình có thể chọn ngưỡng sao cho phù hợp với các mục tiêu. Chẳng hạn trong đại dịch Covid 19 vừa rồi, một hệ thống test nhằm xét nghiệm nhanh ai bị hay không bị nhiễm loại virus này có lẽ nên được thiết kế sao cho nó phát hiện ra TP (dương tính thật) - hay Sensitivity cao nhất có thể bất chấp cái giá phải trả là có thể phân loại sai những người không bị nhiễm Covid 19 thành bị nhiễm bằng cách điều chỉnh ngưỡng phân loại.

Từ kết quả thực nghiệm với bộ dữ liệu GermanCredit ở trên chúng ta rút ra một số kết quả quan trọng sau:

  • Các nhà thống kê hoặc người làm mô hình có thể quan tâm đến các chỉ tiêu thuần thống kê để đánh giá và lựa chọn các mô hình khác nhau. Nhưng với một tổ chức hoạt động vì lợi nhuận thì các tiêu chí đó (như Accuracy, thậm chí là ROC-AUC) chỉ có giá trị tham khảo mà thôi. Lợi nhuận hoặc là rủi ro (hoặc cả hai) mới là quan tâm chính của những tổ chức hoạt động vì lợi nhuận.

  • Một mô hình/thuật toán có ROC-AUC trung bình (trên 20 bộ Validation Data) cao hơn, thậm chí là ROC-AUC cao hơn trên Test Data chưa chưa phải là mô hình/thuật toán mang lại lợi nhuận cao nhất cho ngân hàng.

Sensitivity for Searching Optimal Parameters

Ngoài tinh chỉnh và tìm kiếm tham số tối ưu theo Accuracy hoặc ROC-AUC như đã biết thì caret cũng cho phép tinh chỉnh và tìm kiếm tham số tối ưu theo Sensitivity tại ngưỡng 0.5 (Kuhn và Johnson, 2013) với lựa chọn metric = "Sens" như sau:

# Search optimal parameters for RF using Sensitivity metric: 
set.seed(29)

system.time(
  
  train(Class ~ ., 
        data = train_validCreditData, 
        method = "rf", 
        metric = "Sens", # Sensitivity selected for searching optimal parameters. 
        tuneGrid = expand.grid(mtry = 30:61), 
        trControl = sampling_new) -> rf_fullGrid_sen
  )
##    user  system elapsed 
##  635.40    9.56  671.46
# Show best paramter by Sensitivity metric: 
rf_fullGrid_sen$bestTune
##    mtry
## 29   58

Như vậy tinh chỉnh theo tiêu chuẩn Sensitivity thì tham số tối ưu cho RF là mtry = 58. Chúng ta có thể khảo sát sự thay đổi trung bình của Sensitivity (trên 20 bộ Validation Data) khi mtry thay đổi ở Figure 10 dưới đây:

ggplot(rf_fullGrid_sen) + 
  labs(title = "Figure 10: Model Performance, Random Forest", 
       subtitle = "Sensitivity selected for searching optimal mtry") + 
  theme(legend.position = "top")

Chúng ta có thể sử dụng RF (với mtry = 58) này để chỉ ra ngưỡng phân loại mà tối đa hóa lợi nhuận theo cách thức đã biết như sau:

# Calculate profit at range of thresholds for RF with mtry = 58: 
lapply(some_thresholds, function(x) {some_metrics_with_threshold(rf_fullGrid_sen, x)}) -> results_rf_sen

do.call("bind_rows", results_rf_sen) -> df_results_rf_sen

# Threshold that maximizes profit: 

df_results_rf_sen %>% mutate(Model = "RF-Sen") -> df_results_rf_sen

df_results_rf_sen %>% 
  slice(which.max(Profit))
## # A tibble: 1 x 6
##   Accuracy Sensitiviy Specificity Profit Threshold Model 
##      <dbl>      <dbl>       <dbl>  <dbl>     <dbl> <chr> 
## 1     0.65      0.917       0.536 55577.       0.2 RF-Sen

Như vậy ngưỡng tối ưu là 0.2 nhưng profit tại ngưỡng tối ưu này là 55577 - một con số tuy có tăng so với 52787 nhưng vẫn thấp hơn nếu so với sử dụng LR với ngưỡng tối ưu 0.4.

Feature Engineering

  • Missing Data

  • Encoding Categorical Data

  • Numeric Data

  • Feature Selection

  • Correlation

To be continued…

Capstone Project: Financial Fraud Detection

To be continued…

Capstone Project: HomeCredit Contest

References

http://www.feat.engineering/greedy-simple-filters.html

Efron B (1983). Estimating the Error Rate of a Prediction Rule: Improvement on Cross–Validation. Journal of the American Statistical Association, pp. 316–331.

Efron B, Tibshirani R (1986). Bootstrap Methods for Standard Errors, Confidence Intervals, and Other Measures of Statistical Accuracy. Statistical Science, pp. 54–75.

Efron B, Tibshirani R (1997). Improvements on Cross–Validation: The 632+ Bootstrap Method. Journal of the American Statistical Association, 92(438), 548–560.

Molinaro A (2005). Prediction Error Estimation: A Comparison of Resampling Methods. Bioinformatics, 21(15), 3301–3307.

James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An introduction to statistical learning. New York: Springer.

Kim JH (2009). Estimating Classification Error Rate: Repeated Cross– Validation, Repeated Hold–Out and Bootstrap. Computational Statistics & Data Analysis, 53(11), 3735–3745.

LS0tDQp0aXRsZTogJ1JlYWwtV29ybGQgTWFjaGluZSBMZWFybmluZzogQSBIYW5kcy1vbiBBcHByb2FjaCB1c2luZyBSJw0KYXV0aG9yOiAnQXV0aG9yOiBOZ3V5ZW4gQ2hpIER1bmcnDQpzdWJ0aXRsZTogIlIgRGF0YSBTY2llbmNlIFNlcmllcyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICMgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiB6ZW5idXJuDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBjYWNoZSA9IFRSVUUpDQoNCmBgYA0KDQoNCiFbXShDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvYm9vay5qcGcpDQoNCg0KDQojIEludHJvZHVjdGlvbiB0byBjYXJldA0KDQpO4bq/dSBQeXRob24gY8OzIHRoxrAgdmnhu4duIHNjaWtpdC1sZWFybiBjaG8gY8OhYyB0aHXhuq10IHRvw6FuIE1hY2hpbmUgTGVhcm5pbmcgdGjDrCBSIGPDsyBbY2FyZXQgLSB2aeG6v3QgdOG6r3QgY+G7p2EgQ2xhc3NpZmljYXRpb24gQW5kIFJFZ3Jlc3Npb24gVHJhaW5pbmddKGh0dHBzOi8vdG9wZXBvLmdpdGh1Yi5pby9jYXJldC8pLiBDw7MgY2jhu6tuZyBraMO0bmcgw610IGjGoW4gNDAgxJHhuqd1IHPDoWNoIHbhu4EgTWFjaGluZSBMZWFybmluZy9EYXRhIFNjaWVuY2Ugc+G7rSBk4bulbmcgY2FyZXQgxJHGsOG7o2MgYsOhbiB0csOqbiBBbWF6b24uIE3hu5l0IHRyb25nIHPhu5EgxJHDsyBsw6AgW0FwcGxpZWQgUHJlZGljdGl2ZSBNb2RlbGluZ10oaHR0cHM6Ly93d3cuYW1hem9uLmNvbS9BcHBsaWVkLVByZWRpY3RpdmUtTW9kZWxpbmctTWF4LUt1aG4vZHAvMTQ2MTQ2ODQ4NS9yZWY9c3JfMV8xP2NyaWQ9U1E4RFJIVUxSVU5MJmRjaGlsZD0xJmtleXdvcmRzPWFwcGxpZWQrcHJlZGljdGl2ZSttb2RlbGluZyZxaWQ9MTYzMjE1NDQxOCZzcHJlZml4PWFwcGxpZWQrcHJlZGljJTJDYXBzJTJDNDA4JnNyPTgtMSkgY+G7p2EgTWF4IEt1aG4gdsOgIEtqZWxsIEpvaG5zb24uIFRyb25nIGhhaSB0w6FjIGdp4bqjIG7DoHkgdGjDrCBbTWF4IEt1aG5dKGh0dHBzOi8vd3d3LmxpbmtlZGluLmNvbS9pbi9tYXgta3Vobi04NjRhOTExMCkgbMOgIHTDoWMgZ2nhuqMgY+G7p2EgY2FyZXQuIA0KDQpUcm9uZyBzZXJpZXMgKio2LjAuODYqKiBsw6AgcGhpw6puIGLhuqNuIMSRxrDhu6NjIHPhu60gZOG7pW5nIGPhu6dhIGNhcmV0OiAgDQoNCmBgYHtyfQ0KIyBMb2FkIGNhcmV0IHBhY2thZ2U6IA0KbGlicmFyeShjYXJldCkNCg0KIyBDaGVjayB2ZXJzaW9uOiANCnBhY2thZ2VWZXJzaW9uKCJjYXJldCIpDQpgYGANCg0KSGnDqm4gdOG6oWkgY8OzIDIzOCB0aHXhuq10IHRvw6FuIHBow6JuIGxv4bqhaSB2w6AgaOG7k2kgcXV5IMSRxrDhu6NjIGfDs2kgbsOgeSBo4buXIHRy4bujLiBEYW5oIHPDoWNoIGPDoWMgdGh14bqtdCB0b8OhbiBuw6B5IGPFqW5nIG5oxrAgY8OhYyB0aGFtIHPhu5EgY8OzIHRo4buDIHRpbmggY2jhu4luaCDEkcaw4bujYyBjw7MgdGjhu4MgxJHhu41jIFt04bqhaSDEkcOieV0oaHR0cHM6Ly90b3BlcG8uZ2l0aHViLmlvL2NhcmV0L2F2YWlsYWJsZS1tb2RlbHMuaHRtbCkgaG/hurdjIHPhu60gZOG7pW5nIFIgY29kZXMgZMaw4bubaSDEkcOieTogDQoNCmBgYHtyfQ0KIyBMaXN0IGFsbCBhbGdvcml0aG1zIChvciBtZXRob2RzKSBzdXBwb3J0ZWQgYnkgdGhlIGNhcmV0OiANCg0KZ2V0TW9kZWxJbmZvKCkgLT4gbW9kZWxfaW5mbw0KDQpuYW1lcyhtb2RlbF9pbmZvKSAtPiBhbGxfTUxfbW9kZWxzDQoNCiMgU2hvdyBzb21lIE1MIG1vZGVsczogDQoNCnRhaWwoYWxsX01MX21vZGVscykNCg0KYGBgDQoNCkNo4bqzbmcgY2jhuqFuIGNow7puZyB0YSBtdeG7kW4gY8OzIHRow6ptIHRow7RuZyB0aW4gdsOgIG3DtCB04bqjIHbhu4EgdGh14bqtdCB0b8OhbiBjw7MgImvDrSBoaeG7h3UiIChoYXkgbWV0aG9kKSBsw6AgKip4Z2JUcmVlKiogdGjDrCBz4butIGThu6VuZyBSIGNvZGVzIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIEV4dHJhY3QgZGVzY3JpcHRpb24gYWJvdXQgeGdiVHJlZTogDQpnZXRNb2RlbEluZm8oInhnYlRyZWUiKSAtPiB4Z2JUcmVlX2luZm8NCg0KIyBGdWxsIG5hbWUgb2YgYWxnb3JpdGhtOiANCnhnYlRyZWVfaW5mbyR4Z2JUcmVlJGxhYmVsDQpgYGANCg0KTmjGsCB24bqteSB0aHXhuq10IHRvw6FuIGPDsyBrw60gaGnhu4d1IGzDoCB4Z2JUcmVlIHTDqm4gxJHhuqd5IMSR4bunIGzDoCBbZVh0cmVtZSBHcmFkaWVudCBCb29zdGluZ10oaHR0cHM6Ly9tYWNoaW5lbGVhcm5pbmdtYXN0ZXJ5LmNvbS9leHRyZW1lLWdyYWRpZW50LWJvb3N0aW5nLWVuc2VtYmxlLWluLXB5dGhvbi8pLiANCg0KQ8OhYyB0aHXhuq10IHRvw6FuIE1MIHRoxrDhu51uZyBjw7Mgbmhp4buBdSBoxqFuIDEgdGhhbSBz4buRIChIeXBlci1wYXJhbWV0ZXJzIGhheSBQYXJhbWV0ZXJzKSBtw6AgZ2nDoSB0cuG7iyBj4bunYSBjaMO6bmcg4bqjbmggaMaw4bufbmcgxJHhur9uIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSAtIGThu7EgYsOhbyBj4bunYSB0aHXhuq10IHRvw6FuIHbDoCBjw7RuZyB2aeG7h2MgdHLhu41uZyB0w6JtIGPhu6dhIHZp4buHYyBodeG6pW4gbHV54buHbiBt4buZdCB0aHXhuq10IHRvw6FuIE1MIGzDoCB0w6xtIHJhIG5o4buvbmcgZ2nDoSB0cuG7iyB04buRaSDGsHUgY2hvIHRoYW0gc+G7kSAtIMSRxrDhu6NjIGhp4buDdSBsw6AgZ2nDoSB0cuG7iyBtw6AgdOG6oWkgxJHDsyBraOG6oyBuxINuZyBwaMOibiBsb+G6oWkgLSBk4buxIGLDoW8gY+G7p2EgdGh14bqtdCB0b8OhbiBNTCBsw6AgY2FvIG5o4bqldC4gQ2jDum5nIHRhIHPhur0gdMOsbSBoaeG7g3Uga8SpIHF1w6EgdHLDrG5oIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgdHJvbmcgY8OhYyBt4bulYyBzYXUuIFRyxrDhu5tjIGjhur90IHbhu5tpIG3hu5dpIHRodeG6rXQgdG/DoW4gTUwgxJHDoyBjaOG7jW4gY2jDum5nIHRhIGPhuqduIGJp4bq/dCBuaOG7r25nIHRoYW0gc+G7kSBuw6BvIGPhu6dhIHRodeG6rXQgdG/DoW4gY8OzIHRo4buDIHRpbmggY2jhu4luaCAoYuG7n2kgY2FyZXQpLiBSIGNvZGVzIMSR4buDIGxpc3QgcmEgZGFuaCBzw6FjaCBjw6FjIHRoYW0gc+G7kSBjw7MgdGjhu4MgdGluaCBjaOG7iW5oIGPhu6dhLCB2w60gZOG7pSwgDQoNCmBgYHtyfQ0KIyBQYXJhbWV0ZXJzIGNhbiBiZSB0dXJuZWQgYnkgdGhlIGNhcmV0OiANCnhnYlRyZWVfaW5mbyR4Z2JUcmVlJHBhcmFtZXRlcnMNCg0KYGBgDQoNCk5oxrAgduG6rXkgeGdiVHJlZSBjw7MgNyB0aGFtIHPhu5EgY8OzIHRo4buDIMSRxrDhu6NjIHRpbmggY2jhu4luaCBsw6AgbnJvdW5kcywgbWF4X2RlcHRoLCBldGEsIGdhbW1hLCBjb2xzYW1wbGVfYnl0cmVlLCBpbl9jaGlsZF93ZWlnaHQgdsOgIHN1YnNhbXBsZS4gDQoNCg0KIyBNb3RpdmF0aW9uIGFuZCBBcHByb2FjaA0KDQpT4buRIHRleHRib29rIHZp4bq/dCB24buBIE1hY2hpbmUgTGVhcm5pbmcgbsOzaSBjaHVuZyB2w6Agc+G7rSBk4bulbmcgY2FyZXQgY2hvIE1hY2hpbmUgTGVhcm5pbmcgbsOzaSByacOqbmcgbMOgIHLhuqV0IG5oaeG7gXUgdsOgIGjhuqd1IGjhur90IGNow7puZyDEkcaw4bujYyB2aeG6v3QgYuG6sW5nIHRp4bq/bmcgQW5oLiDEkMOieSBjxaluZyBsw6AgbeG7mXQgcsOgbyBj4bqjbiDEkcOhbmcga+G7gyB2w6wga2jDtG5nIHBo4bqjaSBhaSBjxaluZyBjw7MgduG7kW4gdGnhur9uZyBBbmggxJHhu6cgdOG7kXQgxJHhu4MgbmdoacOqbiBj4bupdSBoYXkgxJHhu41jIGNow7puZy4gTeG6t3Qga2jDoWMgbmjhu69uZyBuZ8aw4budaSB0csaw4bubYyBo4bq/dCBsw6AgbXXhu5FuIHTDrG0gaGnhu4N1LCBy4buTaSBzYXUgxJHDsyBsw6Agdmnhu4djIHPhu60gZOG7pW5nL8OhcCBk4bulbmcgTWFjaGluZSBMZWFybmluZyBraMO0bmcgcGjhuqNpIGFpIGPFqW5nIGPDsyBiYWNrZ3JvdW5kIHbhu4Ega2hvYSBo4buNYyBtw6F5IHTDrW5oLCBraG9hIGjhu41jIGThu68gbGnhu4d1LCB2w6AgYmFja2dyb3VuZCB24buBIHRodeG6rXQgdG/DoW4gY2hvIG7Dqm4gdmnhu4djIHThu7EgaOG7jWMvbmdoacOqbiBj4bupdSBuaOG7r25nIHRleHRib29rIG7DoHkgbMOgIG3hu5l0IHRow6FjaCB0aOG7qWMga2jDtG5nIG5o4buPIHbDoCBj4bqnbiBuaGnhu4F1IHRo4budaSBnaWFuLiDEkMOzIGzDoCBsw60gZG8gc2VyaWVzIG7DoHkgduG7gSBNYWNoaW5lIExlYXJuaW5nIHPhu60gZOG7pW5nIGNhcmV0IMSRxrDhu6NjIHZp4bq/dCB24bubaSBjw6FjaCB0aeG6v3AgY+G6rW4gbmjGsCBzYXU6IA0KDQotIEdp4bqjaSB0aMOtY2ggdHLhu7FjIHF1YW4gcXV5IHRyw6xuaCB0aOG7sWMgaGnhu4duIGh14bqlbiBsdXnhu4duLCDEkcOhbmggZ2nDoSB2w6AgdGluaCBjaOG7iW5oIGPDoWMgdGh14bqtdCB0b8OhbiBNYWNoaW5lIExlYXJuaW5nLiANCi0gLi4uLg0KLSAuLi4uDQoNCg0KTeG6t2MgxJHhu4tuaCBy4bqxbmcgbmfGsOG7nWkgdGhlbyBkw7VpIHNlcmllcyBuw6B5IMSRw6MgbuG6r20gduG7r25nIHTGsMahbmcgxJHhu5FpIG5o4buvbmcgdGjhu6kgZMaw4bubaSDEkcOieTogDQoNCi0gU+G7rSBk4bulbmcgbmfDtG4gbmfhu68gUiwgW1IgcHJvZ3JhbW1pbmddKGh0dHBzOi8vd3d3LmNvdXJzZXJhLm9yZy9sZWFybi9yLXByb2dyYW1taW5nKSBjxaluZyBuaMawIGhhaSB0aMawIHZp4buHbiBbZ2dwbG90Ml0oaHR0cHM6Ly9nZ3Bsb3QyLnRpZHl2ZXJzZS5vcmcvKSB2w6AgW2RwbHlyXShodHRwczovL2RwbHlyLnRpZHl2ZXJzZS5vcmcvKS4gxJDDonkgbOG6p24gbMaw4bujdCBsw6AgaGFpIHRoxrAgdmnhu4duIGNobyBow6xuaCDhuqNuaCBow7NhIHbDoCBiaeG6v24gxJHhu5VpIGThu68gbGnhu4d1LiANCg0KLSBUb8OhbiAtIFRo4buRbmcgS8OqLiANCg0KIyBUcmFpbiBBIE1hY2hpbmUgTGVhcm5pbmc6IEEgU2hvcnQgSWxsdXRyYXRpb24NCg0KxJDhu4MgbWluaCBo4buNYSBxdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiBt4buZdCB0aHXhuq10IHRvw6FuIE1hY2hpbmUgTGVhcm5pbmcgYuG6sW5nIHRoxrAgdmnhu4duIGNhcmV0IGNow7puZyB0YSB4w6l0IG3hu5l0IHRodeG6rXQgdG/DoW4gxJHGoW4gZ2nhuqNuIG5o4bqldCBsw6AgaOG7k2kgcXV5IHR1eeG6v24gdMOtbmggT0xTIMSR4buDIGThu7EgYsOhbyBWb2x1bWUgdGhlbyBIZWlnaHQgduG7m2kgYuG7mSBk4buvIGxp4buHdSB0cmVlcyBjw7MgdOG6pXQgY+G6oyAzMSBxdWFuIHPDoXQuIMSQw6J5IGzDoCBi4buZIGThu68gbGnhu4d1IGx1w7RuIHRoxrDhu51uZyB0cuG7sWMgdHJvbmcgUiBFbnZpcm9ubWVudCB2w6AgY2jDum5nIHRhIGPDsyB0aOG7gyB4ZW0gcXVhIGLhu5kgZOG7ryBsaeG7h3UgbsOgeTogDQoNCg0KYGBge3J9DQojIFVzZSB0cmVlcyBkYXRhIHNldDogDQpoZWFkKHRyZWVzKQ0KDQpgYGANCg0KxJDhu4MgaHXhuqVuIGx1eeG7h24sIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgdsOgIMSRw6FuaCBnacOhIGNo4bqldCBsxrDhu6NuZyBj4bunYSBtw7QgaMOsbmggT0xTIGPFqW5nIG5oxrAgYuG6pXQga8OsIG3DtCBow6xuaCBNTCBuw6BvIHRow6wgY2jDum5nIHRhIGx1w7RuIGNoaWEgYuG7mSBk4buvIGxp4buHdSBiYW4gxJHhuqd1IChPcmlnaW5hbCBEYXRhKSB0aMOgbmggaGFpIHBo4bqnbi4gUGjhuqduIHRo4bupIG5o4bqldCBtw6B1IHjDoW0gdsOgIMSRxrDhu6NjIGfhu41pIGzDoCBUcmFpbmluZyArIFZhbGlkYXRpb24gRGF0YS4gUGjhuqduIHRo4bupIGhhaSBsw6AgSG9sZG91dCBEYXRhIG3DoHUgZGEgY2FtIG5oxrAgRmlndXJlIDEgZMaw4bubaSDEkcOieTogDQoNCg0KIVtdKEM6L1VzZXJzL0FkbWluL0RvY3VtZW50cy9waWMxXzEucG5nKQ0KDQpHaeG6oyBz4butIDMxIHF1YW4gc8OhdCBiYW4gxJHhuqd1IG7DoHkgxJHGsOG7o2MgY2hpYSB0aMOgbmggNiBwaOG6p24gYuG6sW5nIChob+G6t2MgeOG6pXAgeOG7iSBuaGF1KSB0cm9uZyDEkcOzIHRyYWluaW5nICsgdmFsaWRhdGlvbiBjaGnhur9tIDUvNiwgY8OybiBob2xkb3V0IGNoaeG6v20gMS82LiBD4bulIHRo4buDLCBjw6FjIHF1YW4gc8OhdCB04burIDEgxJHhur9uIDYgbMOgIGhvbGRvdXQgY8OybiBwaOG6p24gY8OybiBs4bqhaSBsw6AgdHJhaW5pbmcgKyB2YWxpZGF0aW9uIGRhdGEuIMSQ4buDIGzDoG0gdmnhu4djIG7DoHkgY2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBow6BtIGBzbGljZSgpYCBj4bunYSBkcGx5cjoNCg0KDQpgYGB7cn0NCiMgTG9hZCB0aWR5dmVyc2UgcGFja2FnZTogDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KIyBSZW1vdmUgR2lydGggY29sdW1uOiANCg0KdHJlZXMgJT4lIHNlbGVjdCgtR2lydGgpIC0+IHRyZWVzDQoNCiMgSG9sZG91dCBkYXRhOiANCmlkIDwtIDE6NiAjIE9ic2VydmF0aW9ucyBiZWxvbmcgdG8gaG9sZG91dC4gDQoNCnRyZWVzICU+JSBzbGljZShpZCkgLT4gaG9sZG91dF9kYXRhDQoNCiMgVGVzdGluZyArIHZhbGlkYXRpb24gZGF0YTogDQp0cmVlcyAlPiUgc2xpY2UoLWlkKSAtPiB0cmFpbl92YWxpZF9kYXRhDQpgYGANCg0K4bueIMSRw6J5IHBo4bqnbiBk4buvIGxp4buHdSB0cmFpbl92YWxpZF9kYXRhIMSRxrDhu6NjIHPhu60gZOG7pW5nIMSR4buDIGh14bqlbiBsdXnhu4duIHbDoCB0aW5oIGNo4buJbmggY8OhYyB0aGFtIHPhu5EgY+G7p2EgbcO0IGjDrG5oIE9MUyBjw7JuIHBo4bqnbiBk4buvIGxp4buHdSBob2xkb3V0X2RhdGEgxJHhu4Mga2nhu4NtIHRyYSBuZ8aw4bujYyBs4bqhaSBjaOG6pXQgbMaw4bujbmcgZOG7sSBiw6FvIGPhu6dhIG3DtCBow6xuaC4gDQoNCsSQ4buDIGh14bqlbiBsdXnhu4duIHbDoCDEkcOhbmggZ2nDoS90aW5oIGNo4buJbmggbcO0IGjDrG5oIE9MUyAoaGF5IGLhuqV0IGvDrCBtw7QgaMOsbmggTUwgbsOgbykgY2jDum5nIHRhIHPhu60gZOG7pW5nIGjDoG0gYHRyYWluKClgIHRoZW8gY8O6IHBow6FwIHNhdTogDQoNCmBgYHtyfQ0KIyBTZXQgY29uZGl0aW9ucyBmb3IgdHJhaW4gbW9kZWw6IA0Kb2xzX2NvbnRyb2xzIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gMSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDUpDQoNCg0KIyBUcmFpbiBPTFM6IA0Kc2V0LnNlZWQoMSkgIyBGb3IgcmVwcm9kdWN0IHJlc3VsdHMuIA0KDQp0cmFpbihWb2x1bWUgfiBIZWlnaHQsIA0KICAgICAgZGF0YSA9IHRyYWluX3ZhbGlkX2RhdGEsIA0KICAgICAgbWV0aG9kID0gImxtIiwgIyB0aGlzIG9wdGlvbiBmb3IgdHJhaW5pbmcgT0xTIA0KICAgICAgbWV0cmljID0gIlJzcXVhcmVkIiwgIyBpbmRpY2F0b3Igc2VsZWN0ZWQgZm9yIHRyYWluaW5nIGFuZCB0dXJuaW5nIG1vZGVsDQogICAgICB0ckNvbnRyb2wgPSBvbHNfY29udHJvbHMpIC0+IG9sc19tb2RlbA0KDQpgYGANCg0KxJDhur9uIMSRw6J5IGPDsyB0aOG7gyB4ZW0gcXVhIGvhur90IHF14bqjOiANCg0KYGBge3J9DQpvbHNfbW9kZWwNCmBgYA0KDQpIb+G6t2Mga2hhaSB0aMOhYyB0aMOqbSB0aMO0bmcgdGluIHbhu4EgbcO0IGjDrG5oIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIEV4dHJhY3QgbW9yZSBpbmZvcm1hdGlvbiBhYm91dCBPTFMgdHJhaW5lZDogDQpvbHNfbW9kZWwkcmVzYW1wbGUgLT4gbW9yZV9pbmZvDQoNCiMgUHJpbnQ6IA0KDQpsaWJyYXJ5KGthYmxlRXh0cmEpICMgRm9yIHByZXNlbnRpbmcgdGFibGUuIA0KDQptb3JlX2luZm8gJT4lIA0KICBrYmwoY2FwdGlvbiA9ICJUYWJsZSAxOiBNb2RlbCBQZXJmb3JtYW5jZSwgT0xTIiwgZXNjYXBlID0gVFJVRSkgJT4lDQogIGthYmxlX2NsYXNzaWMoZnVsbF93aWR0aCA9IEZBTFNFLCBodG1sX2ZvbnQgPSAiQ2FtYnJpYSIpDQoNCmBgYA0KDQpOaOG7r25nIGvhur90IHF14bqjIG7DoHksIHbDrSBk4bulLCBSc3F1YXJlZCA9IDAuMzk2NTUzNCDEkeG6v24gdOG7qyDEkcOidT8gTuG6v3UgxJHhu4Mgw70gY8OzIHRo4buDIGtp4buDbSB0cmEgcuG6sW5nIDAuMzk2NTUzNCBjaMOtbmggbMOgICoqdHJ1bmcgYsOsbmggY+G7mW5nKiogY+G7p2EgY+G7mXQgYmnhur9uIFJzcXVhcmVkIOG7nyBUYWJsZSAxOiANCg0KYGBge3J9DQptb3JlX2luZm8gJT4lIA0KICBwdWxsKFJzcXVhcmVkKSAlPiUgDQogIG1lYW4oKQ0KYGBgDQoNCg0KNSBnacOhIHRy4buLIGtow6FjIG5oYXUgY+G7p2EgUnNxdWFyZWQgdHJvbmcgVGFibGUgMSBjaMOtbmggbMOgIHTGsMahbmcg4bupbmcgdsahaSBjw6FjIFBlcmZvcm1hbmNlMSBjaG8gxJHhur9uIFBlcmZvcm1hbmNlNSB0cm9uZyBGaWd1cmUgMS4gUGVyZm9ybWFuY2UgY8OzIHRo4buDIGzDoCBi4bqldCBrw6wgdGnDqnUgY2jDrSBuw6BvIMSRxrDhu6NjIGNo4buNbiDEkeG7gyDEkcOhbmggZ2nDoSBjaOG6pXQgbMaw4bujbmcgY+G7p2EgbcO0IGjDrG5oL3RodeG6rXQgdG/DoW4uIFbhu5tpIGLDoGkgdG/DoW4gaOG7k2kgcXV5IHRow6wgxJHDsyBjw7MgdGjhu4MgbMOgIFJzcXVhcmVkLCBSTVNFIGhv4bq3YyBNQUUuIA0KDQpHacOhIHRy4buLIFJzcXVhcmVkID0gMC4zOTY1NTM0IGPFqW5nIG5oxrAgbeG7kWkgbGnDqm4gaOG7hyBj4bunYSBuw7MgduG7m2kgY8OhYyBr4bq/dCBxdeG6oyBraMOhYyBz4bq9IMSRxrDhu6NjIGdp4bqjaSB0aMOtY2ggY2hpIHRp4bq/dCBoxqFuIOG7nyBt4bulYyBkxrDhu5tpIMSRw6J5LiANCg0KIyBBbiBTaG9ydCBFeHBsYW5hdGlvbiBvZiBUcmFpbmluZyBhIE1hY2hpbmUgTGVhcm5pbmcgQWxnb3JpdGhtDQoNClBo4bqnbiBk4buvIGxp4buHdSB0cmFpbl92YWxpZF9kYXRhIGPDsyAyNSBxdWFuIHPDoXQgc+G6vSDEkcaw4bujYyBwaMOibiBjaGlhIHRp4bq/cCB0aMOgbmggaGFpIHBo4bqnbiBuaOG7jyBoxqFuIG7hu69hIGzDoCB0cmFpbmluZyBkYXRhIChtw6B1IHjDoW0pIHbDoCB2YWxpZGF0aW9uIChtw6B1IHhhbmgpIG3DoCDhu58gxJHDsyB0cmFpbmluZyB2w6AgdmFsaWRhdGlvbiBkYXRhIGzhuqduIGzGsOG7o3QgY2hp4bq/bSA0LzUgdsOgIDEvNS4gTmjGsCB24bqteSB0cmFpbl92YWxpZF9kYXRhIMSRxrDhu6NjIGNoaWEgdGjDoG5oIDUgcGjhuqduIGLhurFuZyBuaGF1IChob+G6t2MgeOG6pXAgeOG7iSBi4bqxbmcpIHbDoCBjw6FjaCB0aOG7qWMgcGjDom4gY2hpYSBk4buvIGxp4buHdSBuaMawIHbhuq15IMSRxrDhu6NjIGfhu41pIGzDoCBbNS1Gb2xkIENyb3NzLVZhbGlkYXRpb25dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0Nyb3NzLXZhbGlkYXRpb25fKHN0YXRpc3RpY3MpKS4gTmjGsCB24bqteSBz4bq9IGPDsyA1IGLhu5kgdHJhaW5pbmcgdsOgIDUgYuG7mSB2YWxpZGF0aW9uIGRhdGEgdMawxqFuZyDhu6luZy4gTMO6YyBuw6B5IE9MUyBz4bq9IMSRxrDhu6NjIGh14bqlbiBsdXnhu4duIHRyw6puIGLhu5kgdHJhaW5pbmcgdGjhu6kgbmjhuqV0IGPDsm4gYuG7mSB2YWxpZGF0aW9uIHTGsMahbmcg4bupbmcgc+G6vSDEkcaw4bujYyBz4butIGThu6VuZyDEkeG7gyDEkcOhbmggZ2nDoSBuZ8aw4bujYyBs4bqhaSBtw7QgaMOsbmggdsOgIHRodSDEkcaw4bujYyBSc3F1YXJlZCA9IDAuMDUyMjg5OTkuIEvhur8gdGnhur9wIE9MUyBs4bqhaSDEkcaw4bujYyBodeG6pW4gbHV54buHbiB0csOqbiBi4buZIHRyYWluaW5nIHRo4bupIGhhaSBjw7JuIGLhu5kgdmFsaWRhdGlvbiB0xrDGoW5nIOG7qW5nIMSRxrDhu6NjIHPhu60gZOG7pW5nIMSR4buDIMSRw6FuaCBnacOhIG5nxrDhu6NjIGzhuqFpIG3DtCBow6xuaCB2w6AgdGh1IMSRxrDhu6NjIFJzcXVhcmVkID0gMC42MTY4Nzk0NS4gTOG6t3AgbOG6oWkgcXXDoSB0csOsbmggbsOgeSBjaG8gYmEgY+G6t3AgdHJhaW5pbmcvdmFsaWRhdGlvbiBjw7JuIGzhuqFpIGNow7puZyB0YSBz4bq9IGPDsyBr4bq/dCBxdeG6oyBsw6AgY+G7mXQgUnNxdWFyZWQg4bufIFRhYmxlIDEuIA0KDQpWaeG7h2MgdGhp4bq/dCBs4bqtcCA1LUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbiBjw7MgbcO0IHThuqMgbmjGsCB0csOqbiDEkcaw4bujYyB0aOG7gyBoaeG7h24gYuG6sW5nIGBudW1iZXIgPSA1YCB0cm9uZyBjb2RlIHNhdTogDQoNCg0KYGBge3J9DQpvbHNfY29udHJvbHMgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gNSkNCmBgYA0KDQoNCsSQ4bq/biDEkcOieSBwaMOhdCBzaW5oIGPDonUgaOG7j2kgcXVhbiB0cuG7jW5nIHNhdTogKk7hur91IHF1w6EgdHLDrG5oIGh14bqlbiBsdXnhu4duLCDEkcOhbmggZ2nDoSB2w6AgdGluaCBjaOG7iW5oIGLhurFuZyA1LUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbiBuaMawIHRyw6puIHRow6wgbcO0IGjDrG5oIE9MUyBjdeG7kWkgY8O5bmcgxJHGsOG7o2Mgc+G7rSBk4bulbmcgY2hvIGThu7EgYsOhbyBz4bq9IGzDoCBtw7QgaMOsbmggT0xTIG7DoG8/Ki4gDQoNCkPDonUgdHLhuqMgbOG7nWkgbMOgOiAqxJDDsyBjaMOtbmggbMOgIG3DtCBow6xuaCBPTFMgxJHGsOG7o2MgaHXhuqVuIGx1eeG7h24gdHLDqm4gdG/DoG4gYuG7mSAyNSBxdWFuIHPDoXQgY+G7p2EgYuG7mSB0cmFpbl92YWxpZF9kYXRhKi4gQ2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBtw7QgaMOsbmggT0xTICJjdeG7kWkgY8O5bmciIG7DoHkgxJHhu4MsIHbDrSBk4bulLCB0aOG7sSBoaeG7h24gZOG7sSBiw6FvIGNobyBWb2x1bWUgbuG6v3UgYmnhur90IGPDoWMgZ2nDoSB0cuG7iyBj4bunYSBIZWlnaHQ6IA0KDQpgYGB7cn0NCiMgVXNlIG1vZGVsIGZvciBwcmVkaWN0aW5nIFZvbHVtZTogDQpwcmVkaWN0ZWRfdmFsdWVzIDwtIHByZWRpY3Qob2xzX21vZGVsLCBob2xkb3V0X2RhdGEpDQoNCiMgU2hvdyBwcmVkaWN0ZWQgdmFsdWVzOiANCnByZWRpY3RlZF92YWx1ZXMNCmBgYA0KDQpO4bq/dSBtdeG7kW4gY2jDum5nIHRhIGPDsyB0aOG7gyBzbyBzw6FuaCBjw6FjIGdpw6EgdHLhu4sgZOG7sSBiw6FvIG7DoHkgduG7m2kgZ2nDoSB0cuG7iyB0aOG7sWMgdOG6vzogDQoNCmBgYHtyfQ0KaG9sZG91dF9kYXRhICU+JSANCiAgbXV0YXRlKFZvbHVtZV9wcmVkaWN0ZWQgPSBwcmVkaWN0ZWRfdmFsdWVzKQ0KYGBgDQoNClF1w6EgdHLDrG5oIGh14bqnbiBsdXnhu4duLCDEkcOhbmggZ2nDoSB2w6AgdGluaCBjaOG7iW5oIGPDsyBtw7QgdOG6oyBuaMawIOG7nyB0csOqbiDEkcaw4bujYyDDoXAgZOG7pW5nIGNobyBt4buNaSB0aHXhuq10IHRvw6FuIE1MIGNo4bupIGtow7RuZyBjaOG7iSByacOqbmcgY2hvIE9MUyB2w6Agc+G6vSDEkcaw4bujYyBtw7QgdOG6oyDhu58gcGjhuqduIGvhur8gdGnhur9wIG5nYXkgc2F1IMSRw6J5LiANCg0KIyBTZWFyY2ggT3B0aW1hbCBIeXBlci1wYXJhbWV0ZXJzOiBGdWxsIEdyaWQgYW5kIFJhbmRvbSBTZWFyY2gNCg0KT0xTIGzDoCBt4buZdCB0aHXhuq10IHRvw6FuIGtow7RuZyBjw7MgdGhhbSBz4buRIChoYXkgbsOzaSBjaMOtbmggeMOhYyBoxqFuIGzDoCBt4buZdCB0aHXhuq10IHRvw6FuIGtow7RuZyBjw7MgdGhhbSBz4buRIGPDsyB0aOG7gyB0aW5oIGNo4buJbmgpLiBUdXkgbmhpw6puIGjhuqd1IGjhur90IGPDoWMgdGh14bqtdCB0b8OhbiBNTCBraMOhYyB0aMaw4budbmcgY8OzIG5oaeG7gXUgaMahbiBt4buZdCB0aGFtIHPhu5EgKHBhcmFtZXRlcnMgaGF5IFtoeXBlcnBhcmFtZXRlcnNdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0h5cGVycGFyYW1ldGVyXyhtYWNoaW5lX2xlYXJuaW5nKSkpLiBOaOG7r25nIHRoYW0gc+G7kSBuw6B5IGPDsyB2YWkgdHLDsiBxdXnhur90IMSR4buLbmggxJHhur9uIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSBt4buZdCB0aHXhuq10IHRvw6FuIE1MIG5oxrBuZyBraMO0bmcgY8OzIGPDtG5nIHRo4bupYyBj4bulIHRo4buDIG7DoG8gY2hvIGNow7puZyB2w6AgdGhhbSBz4buRIHThu5FpIMawdSwgxJHGsOG7o2MgaGnhu4N1IHRoZW8gbmdoxKlhIGzDoCBnacOhIHRy4buLIGPhu6dhIGLhu5kgdGhhbSBz4buRIHNhbyBjaG8ga2jhuqMgbsSDbmcgZOG7sSBiw6FvIGPhu6dhIHRodeG6rXQgdG/DoW4gKGPEg24gY+G7qSB2w6BvIG3hu5l0IHRpw6p1IGNow60gxJHGsOG7o2MgbOG7sWEgY2jhu41uIHRyxrDhu5tjKSBsw6AgdOG7kXQgbmjhuqV0IGNo4buJIGPDsyB0aOG7gyB0w6xtIGLhurFuZyB0aOG7sWMgbmdoaeG7h20gZOG7sWEgdHLDqm4gbeG7mXQgYuG7mSBkYXRhIGPhu6UgdGjhu4MgbcOgIHRow7RpLiBOaMawIHbhuq15ICp0cuG7jW5nIHTDom0gY+G7p2EgcXXDoSB0csOsbmggaHXhuqVuIGx1eeG7h24gY8OhYyB0aHXhuq10IHRvw6FuIE1MIGzDoCB0w6xtIMSRxrDhu6NjIHRoYW0gc+G7kSB04buRaSDGsHUgKG9wdGltYWwgcGFyYW1ldGVycykqIGtoaSBjw7MgdHLGsOG7m2MgYuG7mSBk4buvIGxp4buHdSBj4bulIHRo4buDLiANCg0KVHJvbmcgZ2nhu5tpIGjhuqFuIHNlcmllcyBiw6BpIGdp4bqjbmcgbsOgeSwgY8OzIGJhIGPDoWNoIMSRxrDhu6NjIHPhu60gZOG7pW5nIMSR4buDIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHU6ICgxKSBGdWxsIEdyaWQgU2VhcmNoLCAoMikgUmFuZG9tIFNlYXJjaCwgdsOgICgzKSBEZWZhdWx0IFNlYXJjaC4gDQoNCioqRnVsbCBHcmlkIFNlYXJjaCoqDQoNCg0KxJDhu4MgbWluaCBo4buNYSBjaMO6bmcgdGEgaMOjeSB4ZW0geMOpdCB0aHXhuq10IHRvw6FuIFtLLW5lYXJlc3QgTmVpZ2hib3JzIEFsZ29yaXRobV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvSy1uZWFyZXN0X25laWdoYm9yc19hbGdvcml0aG0jOn46dGV4dD1JbiUyMHN0YXRpc3RpY3MlMkMlMjB0aGUlMjBrJTJEbmVhcmVzdCx0cmFpbmluZyUyMGV4YW1wbGVzJTIwaW4lMjBkYXRhJTIwc2V0LiksIGfhu41pIHThuq90IGzDoCBLTk4uIFRodeG6rXQgdG/DoW4gbsOgeSBjaOG7iSBjw7MgZHV5IG5o4bqldCBt4buZdCB0aGFtIHPhu5EgbMOgIHPhu5EgbMOhbmcgZ2nhu4FuZyBn4bqnbiBuaOG6pXQgLSBrw60gaGnhu4d1IGzDoCBrOiANCg0KYGBge3J9DQojIFNob3cgcGFyYW1ldGVycyB0aGF0IGNhbiBiZSB0dXJuZWQ6IA0KZ2V0TW9kZWxJbmZvKCJrbm4iKSAtPiBrbm5faW5mbw0KDQprbm5faW5mbyRrbm4kcGFyYW1ldGVycw0KYGBgDQoNCsSQ4buDIHTDrG0gdGhhbSBz4buRIHThu5FpIMawdSBjaG8gS05OIGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgbeG7mXQgY8OhY2ggdGnhur9wIGPhuq1uIGPDsyB0w6puIGzDoCBmdWxsIGdyaWQgc2VhcmNoLiBUaGVvIGPDoWNoIHRp4bq/cCBj4bqtbiBuw6B5IGNow7puZyB0YSBz4bq9IGxp4buHdCBrw6ogbeG7mXQgc+G7kSBnacOhIHRy4buLIHRp4buBbSBuxINuZyBj4bunYSBrLCBjaOG6s25nIGjhuqFuLCBsw6AgMywgNSB2w6AgNy4gxJDhu4MgdMOsbSBrIHThu5FpIMawdSAodHJvbmcgc+G7kSBiYSDhu6luZyB2acOqbiB0aeG7gW0gbsSDbmcgbsOgeSkgc+G7rSBk4bulbmcgNS1Gb2xkIENyb3NzLVZhbGlkYXRpb246IA0KDQotICoqQsaw4bubYyAxKiouIEPhu5EgxJHhu4tuaCBrID0gMyBy4buTaSB0aOG7sWMgaGnhu4duIDUtRm9sZCBDcm9zcy1WYWxpZGF0aW9uIGNow7puZyB0YSBz4bq9IGPDsyA1IGdpw6EgdHLhu4sgY+G7p2EgUnNxdWFyZWQuIFTDrW5oIGLDrG5oIHF1w6JuIGPhu6dhIDUgZ2nDoSB0cuG7iyBSc3F1YXJlZCBuw6B5LiANCi0gKipCxrDhu5tjIDIqKi4gTOG6t3AgbOG6oWkgYsaw4bubYyAxIGNobyBjw6FjIGdpw6EgdHLhu4sgY8OybiBs4bqhaSBj4bunYSBrIGzDoCA1IHbDoCA3LiANCi0gKipCxrDhu5tjIDMqKi4gU28gc8OhbmggYmEgZ2nDoSB0cuG7iyBiw6xuaCBxdcOibiBj4bunYSBSc3F1YXJlZCB0xrDGoW5nIOG7qW5nIHbhu5tpIGJhIGdpw6EgdHLhu4sgY+G7p2EgayDEkeG7gyBjaOG7jW4gZ2nDoSB0cuG7iyBiw6xuaCBxdcOibiBs4bubbiBuaOG6pXQuIFTDrG0gayBtw6AgdMawxqFuZyDhu6luZyB24bubaSBnacOhIHRy4buLIGLDrG5oIHF1w6JuIGzhu5tuIG5o4bqldCBuw6B5LiDEkMOieSBjaMOtbmggbMOgIGsgdOG7kWkgxrB1LiANCi0gKipCxrDhu5tjIDQqKi4gSHXhuqVuIGx1eeG7h24gbOG6oWkgS05OIHbhu5tpIGsgdOG7kWkgxrB1IHTDrG0gcmEg4bufIGLGsOG7m2MgMyB0csOqbiB0b8OgbiBi4buZIDI1IHF1YW4gc8OhdCBj4bunYSBi4buZIGThu68gbGnhu4d1IHRyYWluX3ZhbGlkX2RhdGEuIA0KDQpRdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIHRoYW0gc+G7kSBuaMawIG3DtCB04bqjIOG7nyB0csOqbiDEkcaw4bujYyBLdWhuIHbDoCBKb2huc29uICgyMDEzLCBwNjYpIG3DtCB04bqjIGLhurFuZyBkaWFncmFtIHNhdTogDQoNCiFbXShDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvcGljMl8yLnBuZykNCg0KUiBjb2RlcyDEkeG7gyBodeG6pW4gbHV54buHbiwgxJHDoW5oIGdpw6EgdsOgIHTDrG0ga2nhur9tL3RpbmggY2jhu4luaCBLTk4gYuG6sW5nIGZ1bGwgZ3JpZCBzZWFyY2ggdGhlbyBkaWFncmFtIHRyw6puIHbhu5tpIGzhu7FhIGNo4buNbiBgc2VhcmNoID0gImdyaWQiYCAoY2jDuiDDvSBy4bqxbmcgaMOgbSBgdHJhaW5Db250cm9sKClgIHPhur0gbeG6t2MgxJHhu4tuaCBjaOG7jW4gRnVsbCBHcmlkIFNlYXJjaCBu4bq/dSBraMO0bmcgY2jhu4kgdGjhu4sgY+G7pSB0aOG7gykgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCiMgU2V0IGNvbmRpdGlvbnMgZm9yIGNyb3NzLXZhbGlkYXRpb246IA0KDQprbm5fY29udHJvbHNfZ3JpZCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWFyY2ggPSAiZ3JpZCIsICMgb3B0aW9uIGZvciBmdWxsIGdyaWQgc2VhcmNoIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSA1KQ0KDQojIFNldCBzb21lIHBvdGVudGlhbCB2YWx1ZXMgb2YgSyBmb3IgS05OIChvciBkZWZpbmUgR3JpZCBTZWFyY2gpOiANCmtubl9ncmlkIDwtIGV4cGFuZC5ncmlkKGsgPSBjKDMsIDUsIDcpKQ0KDQojIFRyYWluIGFuZCBzZWFyY2ggb3B0aW1hbCBrOiANCnNldC5zZWVkKDI5KQ0KdHJhaW4oVm9sdW1lIH4gSGVpZ2h0LCANCiAgICAgIGRhdGEgPSB0cmFpbl92YWxpZF9kYXRhLCANCiAgICAgIG1ldGhvZCA9ICJrbm4iLCAjIG9wdGlvbiBmb3IgdHJhaW5pbmcgS05ODQogICAgICBtZXRyaWMgPSAiUnNxdWFyZWQiLCAjIG1ldHJpYyBzZWxlY3RlZCBmb3IgdHJhaW5pbmcgYW5kIHNlYXJjaGluZyBvcHRpbWFsIHBhcmFtZXRlciBLIA0KICAgICAgdHVuZUdyaWQgPSBrbm5fZ3JpZCwgDQogICAgICB0ckNvbnRyb2wgPSBrbm5fY29udHJvbHNfZ3JpZCkgLT4ga25uX21vZGVsIA0KDQpgYGANCksgdOG7kWkgxrB1IGzDoCA3OiANCg0KYGBge3J9DQojIFNob3cgb3B0aW1hbCBwYXJhbWV0ZXJzOiANCmtubl9tb2RlbCRiZXN0VHVuZQ0KYGBgDQoNCkJhIGdpw6EgdHLhu4sgdHJ1bmcgYsOsbmggY+G7p2EgUnNxdWFyZWQgdMawxqFuZyDhu6luZyB24bubaSBiYSBnacOhIHRy4buLIGPhu6dhIGsgdsOgIMSRaSBrw6htIHbhu5tpIHRow7RuZyBiw6FvICpSc3F1YXJlZCB3YXMgdXNlZCB0byBzZWxlY3QgdGhlIG9wdGltYWwgbW9kZWwgdXNpbmcgdGhlIGxhcmdlc3QgdmFsdWUuIFRoZSBmaW5hbCB2YWx1ZSB1c2VkIGZvciB0aGUgbW9kZWwgd2FzIGsgPSA3KjogDQoNCmBgYHtyfQ0Ka25uX21vZGVsDQpgYGANCg0KTMawdSDDvSBy4bqxbmcgc+G6vSBjw7MgdOG7lW5nIGPhu5luZyA1w5czICsgMSA9IDE1ICsgMSA9IDE2IG3DtCBow6xuaCBLTk4gdHJvbmcgxJHDsyBjw7MgMTUgKktOTiBjb24qIHbDoCAxICpLTk4gY3Xhu5FpIGPDuW5nKiAtIHThu6ljIGzDoCBLTk4gxJHGsOG7o2MgaHXhuqVuIGx1eeG7h24ga2hpIGsgPSA3IHRyw6puIDI1IHF1YW4gc8OhdCBj4bunYSBi4buZIGThu68gbGnhu4d1IHRyYWluX3ZhbGlkX2RhdGEuIE7hur91IG3hu5l0IHRodeG6rXQgdG/DoW4gbsOgbyBraMOhYyBuaMawIGVYdHJlbWUgR3JhZGllbnQgQm9vc3RpbmcgY8OzIDcgdGhhbSBz4buRIGPDsyB0aOG7gyB0aW5oIGNo4buJbmggYuG7n2kgY2FyZXQsIG3DoCBt4buXaSB0aGFtIHPhu5EgY2jhu41uIDUg4bupbmcgdmnDqm4gdGjDrCB04buVbmcgc+G7kSBjw6FjIG3DtCBow6xuaCBYR0Jvb3N0IG3DoCBtw6F5IHTDrW5oIHBo4bqjaSB0cmFpbiBsw6AgNcOXNcOXNcOXNcOXNcOXNcOXNcOXNSArIDEgPSAzOTA2MjYuIE7hur91IG3hu5l0IG3DtCBow6xuaCBt4bqldCAxLzEwIHMgdGjDrCB04buVbmcgdGjhu51pIGdpYW4gbcOgIG3DoXkgdMOtbmggY+G6p24gc+G6vSB44bqlcCB44buJIDExIGdp4budIMSR4buDIHTDrG0gcmEgdGhhbSBz4buRIHThu5FpIMawdSB2w6AgaHXhuqVuIGx1eeG7h24gWEdCb29zdC4gDQoNCk5oxrAgduG6rXkgc+G7rSBk4bulbmcgY8OhY2ggdGnhur9wIGPhuq1uIGZ1bGwgZ3JpZCBzZWFyY2ggdGjDrCBz4bq9IGPhuqduIGh14bqlbiBsdXnhu4duIHRodeG6rXQgdG/DoW4gaOG7jWMgbcOheSAqY2hvIHThuqV0IGPhuqMgY8OhYyB0aGFtIHPhu5EgxJHGsOG7o2MgbGnhu4d0IGvDqiouIMSQ4buRaSB24bubaSBjw6FjIHRodeG6rXQgdG/DoW4gY+G6p24gbmhp4buBdSB0aOG7nWkgZ2lhbiBodeG6pW4gbHV54buHbiwgaG/hurdjIGLhu5kgZOG7ryBsaeG7h3UgbMOgIGzhu5tuIChob+G6t2MgY+G6oyBoYWkpIHRow6wgaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCB0aGVvIGZ1bGwgZ3JpZCBzZWFyY2ggY8OzIHRo4buDIG3huqV0IHLhuqV0IG5oaeG7gXUgdGjhu51pIGdpYW4gdsOgIGRvIHbhuq15IGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgUmFuZG9tIFNlYXJjaCB24bubaSBgc2VhcmNoID0gInJhbmRvbSJgIGtoaSB0aGnhur90IGzhuq1wIGPDoWMgxJFp4buBdSBraeG7h24gaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCBuaMawIHNhdToNCg0KYGBge3J9DQojIFNldCByYW5kb20gc2VhcmNoOiANCmNvbnRyb2xzX3JhbmQgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWFyY2ggPSAicmFuZG9tIiwgIyBmb3IgcmFuZG9tIHNlYXJjaCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDUpDQoNCiMgVHJhaW4gYW5kIHNlYXJjaCBvcHRpbWFsIHBhcmFtdGVyIHVzaW5nIHJhbmRvbSBzZWFyY2g6IA0KdHJhaW4oVm9sdW1lIH4gSGVpZ2h0LCANCiAgICAgIGRhdGEgPSB0cmFpbl92YWxpZF9kYXRhLCANCiAgICAgIG1ldGhvZCA9ICJrbm4iLCANCiAgICAgIG1ldHJpYyA9ICJSc3F1YXJlZCIsIA0KICAgICAgdHVuZUdyaWQgPSBrbm5fZ3JpZCwgDQogICAgICB0ckNvbnRyb2wgPSBjb250cm9sc19yYW5kKQ0KYGBgDQoNCioqUmFuZG9tIFNlYXJjaCoqDQoNCkPDoWkgdMOqbiBj4bunYSBwaMawxqFuZyBwaMOhcCBuw6B5IG5n4bulIMO9IHLhurFuZyAqa2hpIHPhu60gZOG7pW5nIHJhbmRvbSBzZWFyY2gsIHRodeG6rXQgdG/DoW4ga2jDtG5nIMSRxrDhu6NjIGh14bqlbiBsdXnhu4duIHbhu5tpIE3hu4xJIGdpw6EgdHLhu4sgdGnhu4FtIG7Eg25nIGPhu6dhIHRoYW0gc+G7kSBtw6AgY2jDum5nIHRhIGxp4buHdCBrw6ogdHLGsOG7m2MqLiDEkGnhu4F1IG7DoHkgY2hvIHRo4bqleSB0aW5oIGNo4buJbmggdsOgIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB0aGVvIGPDoWNoIHRp4bq/cCBj4bqtbiBuw6B5ICpjw7MgdGjhu4MgYuG7jyBzw7N0IHRoYW0gc+G7kSB04buRdCBuaOG6pXQgdHJvbmcgZGFuaCBzw6FjaCBjw6FjIOG7qW5nIHZpw6puKi4gxJDDonkgbMOgIGPDoWkgZ2nDoSBwaOG6o2kgdHLhuqMgY2hvIGPDoWNoIHRo4bupYyBodeG6pW4gbHV54buHbiB2w6AgdMOsbSBraeG6v20gdGhhbSBz4buRIHThu5FpIMawdSBuw6B5LiDEkOG7gyBraOG6r2MgcGjhu6VjIG5oxrDhu6NjIMSRaeG7g20gbsOgeSBj4bunYSByYW5kb20gc2VhcmNoIG5oxrBuZyDDrXQgbmjhuqV0IGzDoCB0aeG7h20gY+G6rW4gxJHhur9uIGdpw6EgdHLhu4sgdOG7kWkgxrB1IGNobyB0aGFtIHPhu5EgdGjDrCBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgKipCYXllc2lhbiBPcHRpbWl6YXRpb24qKi4gVHV5IG5oacOqbiB0aW5oIGNo4buJbmggdsOgIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgdGhlbyBCYXllc2lhbiBPcHRpbWl6YXRpb24gbuG6sW0gbmdvw6BpIHBo4bqhbSB2aSB0aOG6o28gbHXhuq1uIGPhu6dhIGLDoGkgZ2nhuqNuZyBuw6B5IHbDoCBz4bq9IGtow7RuZyDEkcaw4bujYyBnaeG7m2kgdGhp4buHdSDhu58gxJHDonkuIA0KDQoNCk5oxrDhu6NjIMSRaeG7g20gY+G7p2EgZnVsbCBncmlkIHNlYXJjaCBuZ2/DoGkgdmnhu4djIHThu5FuIHRo4budaSBnaWFuIHbDrCB0aHXhuq10IHRvw6FuIHPhur0gxJHGsOG7o2MgbOG6p24gbMaw4bujdCBodeG6pW4gbHV54buHbiB24bubaSB04bqldCBj4bqjIGdpw6EgdHLhu4sgY8OhYyBnacOhIHRy4buLIGPhu6dhIHRoYW0gc+G7kSB0aMOsIGdpw6EgdHLhu4sgdOG7kWkgxrB1IGPhu6dhIHRoYW0gc+G7kSB0w6xtIMSRxrDhu6NjIHBo4bulIHRodeG7mWMgcuG6pXQgbmhp4buBdSB2w6BvIGtpbmggbmdoaeG7h20gY8WpbmcgbmjGsCBoaeG7g3UgYmnhur90IHbhu4EgdGh14bqtdCB0b8OhbiBj4bunYSBuZ8aw4budaSBsw6BtIG3DtCBow6xuaC4gQ2jhurNuZyBo4bqhbiB24bubaSBLTk4g4bufIHRyw6puLCBkYW5oIHPDoWNoIGPDoWMg4bupbmcgdmnDqm4gY2hvIEsgbMOgIDMsIDUsIDcuIE5oxrBuZyBiaeG6v3QgxJHDonUgSyA9IDExIGzhuqFpIGzDoCBnacOhIHRy4buLIHThu5FpIMawdSBjaG8gSyB0aMOsIHNhbyBuaMawbmcgbmfGsOG7nWkgbMOgbSBtw7QgaMOsbmggbOG6oWkga2jDtG5nIMSRxrBhIOG7qW5nIHZpw6puIEsgPSAxMSB2w6AgZGFuaCBzw6FjaCBodeG6pW4gbHV54buHbiBraGkgdGhp4bq/dCBs4bqtcCBHcmlkIFNlYXJjaC4gDQoNCioqRGVmYXVsdCBTZWFyY2gqKg0KDQpHw7NpIGNhcmV0IGPDsm4gY2hvIHBow6lwIHRo4buxYyBoaeG7h24gdMOsbSBraeG6v20gdGhhbSBz4buRIHThu5FpIMawdSB0aGVvIHBoxrDGoW5nIHBow6FwIEZ1bGwgR3JpZCBTZWFyY2gg4bufIGNo4bq/IMSR4buZIG3hurdjIMSR4buLbmggKERlZmF1bHQpLiBUaGF5IHbDrCBjaMO6bmcgdGEgcGjhuqNpIHRoaeG6v3QgbOG6rXAgbMaw4bubaSB0aW5oIGNo4buJbmggYmFvIGfhu5NtIHThuqV0IGPhuqMgY8OhYyBz4buxIGvhur90IGjhu6NwIGtow6FjIG5oYXUgY+G7p2EgdGhhbSBz4buRLCBjYXJldCBz4bq9IHThu7EgbOG7sWEgY2jhu41uIGPDoWMgZ2nDoSB0cuG7iyDhu6luZyB2acOqbiB0aeG7gW0gbsSDbmcgY2hvIGPDoWMgdGhhbSBz4buRIGPhu6dhIHRodeG6rXQgdG/DoW4uIMSQ4buDIG1pbmggaOG7jWEgY2jDum5nIHRhIHjDqXQgdGh14bqtdCB0b8OhbiAqQm9vc3RlZCBUcmVlKiBjw7MgImvDrSBoaeG7h3UiIHhnYlRyZWUuIFRyxrDhu5tjIGjhur90IGxpc3QgcmEgY8OhYyB0aGFtIHPhu5EgY8OzIHRo4buDIHRpbmggY2jhu4luaCBi4bufaSBjYXJldDogDQoNCmBgYHtyfQ0KZ2V0TW9kZWxJbmZvKCJic3RUcmVlIikgLT4gYnN0VHJlZV9pbmZvDQoNCiMgUGFyYW1ldGVycyBjYW4gYmUgdHVybmVkIGJ5IHRoZSBjYXJldDogDQpic3RUcmVlX2luZm8kYnN0VHJlZSRwYXJhbWV0ZXJzDQpgYGANCg0KTmjGsCB24bqteSBjw6FjIHRoYW0gc+G7kSBjw7MgdGjhu4MgdGluaCBjaOG7iW5oIGPhu6dhIEJvb3N0ZWQgVHJlZSBsw6AgbXN0b3AsIG1heGRlcHRoIHbDoCBudS4gQ2jDum5nIHRhIGPDsyB0aOG7gyBodeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIGPDoWMgdGhhbSBz4buRIGPhu6dhIEJvb3N0ZWQgVHJlZSDhu58gY2jhur8gxJHhu5kgbeG6t2MgxJHhu4tuaCBuaMawIHNhdTogIA0KDQoNCmBgYHtyfQ0KIyBTZXQgY29uZGl0aW9ucyBmb3IgNS1Gb2xkIENyb3NzLVZhbGlkYXRpb246IA0KDQpkZWZhdWx0X2NvbnRyb2xzX2N2IDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gNSkNCg0KIyBUcmFpbiBhbmQgdHVybiBwYXJhbWV0ZXJzIGZvciBCb29zdGVkIFRyZWU6IA0Kc2V0LnNlZWQoMjkpDQoNCnRyYWluKFZvbHVtZSB+IEhlaWdodCwgDQogICAgICBkYXRhID0gdHJhaW5fdmFsaWRfZGF0YSwgDQogICAgICBtZXRob2QgPSAiYnN0VHJlZSIsICMgdGhpcyBvcHRpb24gZm9yIHRyYWluaW5nIEJvb3N0ZWQgVHJlZSANCiAgICAgIG1ldHJpYyA9ICJSc3F1YXJlZCIsIA0KICAgICAgdHJDb250cm9sID0gZGVmYXVsdF9jb250cm9sc19jdikgLT4gZGVmYXVsdF9ic3RUcmVlDQoNCg0KIyBTaG93IG9wdGltYWwgcGFyYW1ldGVycyBhbmQgb3RoZXIgcmVzdWx0czogDQpkZWZhdWx0X2JzdFRyZWUNCmBgYA0KDQpL4bq/dCBxdeG6oyBuw6B5IG5naMSpYSBsw6AgY8OhYyBnacOhIHRy4buLIHThu5FpIMawdSBz4bq9IGzDoCBtc3RvcCA9IDE1MCwgbWF4ZGVwdGggPSAxLCB2w6AgbnUgPSAwLjEuIEPDoWMgZ2nDoSB0cuG7iyB04buRaSDGsHUgY+G7p2EgdGhhbSBz4buRIMSRxrDhu6NjIHTDrG0gYuG6sW5nIG3DoCDhu58gxJHDsyBjw6FjIGto4bqjIG7Eg25nIGtow6FjIG5oYXUgY+G7p2EgdGhhbSBz4buRIGzDoCBz4buxIGvhur90IGjhu6NwIGPhu6dhOiANCg0KLSBD4buRIMSR4buLbmggbnUgPSAwLjEuIA0KLSBDw6FjIGdpw6EgdHLhu4sg4bupbmcgdmnDqm4gY+G7p2EgbWF4ZGVwdGggbMOgIDEsIDIgdsOgIDMuIA0KLSBDw6FjIGdpw6EgdHLhu4sg4bupbmcgdmnDqm4gY+G7p2EgbXN0b3AgbMOgIDUwLCAxMDAsIDE1MC4gDQoNCk5o4buvbmcgZ2nDoSB0cuG7iyB04buRaSDGsHUgY+G7p2EgdGhhbSBz4buRIG3hurdjIMSR4buLbmgg4bufIHRyw6puIGPDsyB0aOG7gyDEkcaw4bujYyBz4butIGThu6VuZyDEkeG7gyBsw6BtIGPGoSBz4bufIGNobyB2aeG7h2MgdGhp4bq/dCBs4bqtcCBncmlkIHNlYXJjaCAobMaw4bubaSB0aW5oIGNo4buJbmgpIMSR4bqneSDEkeG7pyBoxqFuLCB04bupYyBsw6AgbGnhu4d0IGvDqiB04bqldCBj4bqjIGPDoWMgc+G7sSBr4bq/dCBo4bujcCBjw7MgdGjhu4MgxJHGsOG7o2MgY+G7p2EgYuG7mSB0aGFtIHPhu5EuIE5oxrAgduG6rXksIGdyaWQgc2VhcmNoIHBo4bqjaSBjaOG7qWEgbXN0b3AgPSAxNTAsIG1heGRlcHRoID0gMSwgdsOgIG51ID0gMC4xIHbDoCBi4buZIGJhIHRoYW0gc+G7kSBuw6B5IHPhur0gxJHGsOG7o2MgY29pIGzDoCAidGnDqnUgY2h14bqpbiIgY2hvIHZp4buHYyB0aGnhur90IGzhuq1wIGdyaWQgc2VhcmNoLiBSIGNvZGVzIHRoaeG6v3QgbOG6rXAgZ3JpZCBzZWFyY2ggbmjGsCBzYXU6IA0KDQpgYGB7cn0NCiMgU2V0IGZ1bGwgZ3JpZCBzZWFyY2ggZm9yIEJvb3N0ZWQgVHJlZTogDQpncmlkX2JzdFRyZWUgPC0gZXhwYW5kLmdyaWQobnUgPSBjKDAuMDUsIDAuMSwgMC4xNSksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heGRlcHRoID0gMTo1LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc3RvcCA9IGMoNTAsIDEwMCwgMTUwLCAyMDApKQ0KDQojIFNob3cgc29tZSBjb21iaW5hdGlvbnM6IA0KaGVhZChncmlkX2JzdFRyZWUpDQpgYGANCg0KQ8OzIHRo4buDIHRo4bqleSBncmlkX2JzdFRyZWUgbMOgIG3hu5l0IERhdGEgRnJhbWUgY8OzIDPDlzXDlzQgPSA2MCBraOG6oyBuxINuZyAoc+G7sSBr4bq/dCBo4bujcCkga2jDoWMgbmhhdSBj4bunYSBjw6FjIHRoYW0gc+G7kS4gS2jhuqMgbsSDbmcgdGjhu6kgbmjhuqV0IGzDoCBi4buZIG51ID0gMC4wNSwgbWF4ZGVwdGggPSAxIHbDoCBtc3RvcCA9IDUwLiBUaGVvIGPDoWNoIHRp4bq/cCBj4bqtbiBmdWxsIGdyaWQgc2VhcmNoIHRow6wgYuG7mSB0aGFtIHPhu5EgdOG7kWkgxrB1IHPhur0gbMOgIG3hu5l0IGPhurdwIG7hurFtIHRyb25nIHPhu5EgNjAga2jhuqMgbsSDbmcga2jDoWMgbmhhdSBuw6B5IHbDoCDEkcaw4bujYyB0w6xtIGLhurFuZyBow6BtIGB0cmFpbigpYCBuaMawIGNow7puZyB0YSDEkcOjIGJp4bq/dDogDQoNCmBgYHtyfQ0KDQojIFNldCBjb25kaXRpb25zIGZvciA1LUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbjogDQoNCmdyaWRfY29udHJvbHNfY3YgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWFyY2ggPSAiZ3JpZCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gNSkNCg0KIyBUcmFpbiBhbmQgdHVybiBwYXJhbWV0ZXJzIGZvciBCb29zdGVkIFRyZWU6IA0Kc2V0LnNlZWQoMjkpDQoNCnRyYWluKFZvbHVtZSB+IEhlaWdodCwgDQogICAgICBkYXRhID0gdHJhaW5fdmFsaWRfZGF0YSwgDQogICAgICBtZXRob2QgPSAiYnN0VHJlZSIsIA0KICAgICAgbWV0cmljID0gIlJzcXVhcmVkIiwgDQogICAgICB0dW5lR3JpZCA9IGdyaWRfYnN0VHJlZSwgDQogICAgICB0ckNvbnRyb2wgPSBncmlkX2NvbnRyb2xzX2N2KSAtPiBmdWxsX2dyaWRfYnN0VHJlZQ0KDQojIFNob3cgb3B0aW1hbCBwYXJhbWV0ZXJzIGFuZCBvdGhlciByZXN1bHRzOiANCmZ1bGxfZ3JpZF9ic3RUcmVlDQpgYGANCg0KTMO6YyBuw6B5IGLhu5kgdGhhbSBz4buRIHThu5FpIMawdSAodMOsbSByYSB0cm9uZyBz4buRIDYwIGto4bqjIG7Eg25nIGvhur90IGjhu6NwIGtow6FjIG5oYXUgY+G7p2EgYuG7mSBiYSB0aGFtIHPhu5EpIGzDoCBtc3RvcCA9IDIwMCwgbWF4ZGVwdGggPSAxIHbDoCBudSA9IDAuMTUuIENow7puZyB0YSBjw7MgdGjhu4MgdMOsbSBs4bqhaSBi4buZIHRoYW0gc+G7kSB04buRaSDGsHUgbsOgeSBjw7luZyB24bubaSBSc3F1YXJlZCB0cnVuZyBiw6xuaCAodHLDqm4gNSBt4bqrdSB2YWxpZGF0aW9uIGRhdGEpIHTGsMahbmcg4bupbmcgbMOgIDAuNTAwNTA5MiBuaMawIHNhdTogDQoNCmBgYHtyfQ0KDQpmdWxsX2dyaWRfYnN0VHJlZSRyZXN1bHRzICU+JSANCiAgc2xpY2Uod2hpY2gubWF4KFJzcXVhcmVkKSkNCmBgYA0KDQpWw6Agc28gduG7m2kgYuG7mSB0aGFtIHPhu5EgdOG7kWkgxrB1IMSRxrDhu6NjIHTDrG0gdGhlbyBraeG7g3UgbeG6t2MgxJHhu4tuaCBjw7MgUnNxdWFyZWQgdHJ1bmcgYsOsbmggdMawxqFuZyDhu6luZyBsw6AgMC40OTQ0NTA2IG5oxrAgY2jDum5nIHRhIMSRw6MgYmnhur90OiANCg0KYGBge3J9DQpmdWxsX2dyaWRfYnN0VHJlZSRyZXN1bHRzICU+JSANCiAgZmlsdGVyKG51ID09IDAuMSwgbWF4ZGVwdGggPT0gMSwgbXN0b3AgPT0gMTUwKQ0KDQpgYGANCg0KTmjGsCB24bqteSBu4bq/dSBs4buxYSBjaOG7jW4gUnNxdWFyZWQgbMOgbSB0acOqdSBjaMOtIHRpbmggY2jhu4luaCB2w6AgbOG7sWEgY2jhu41uIHRow6wgYuG7mSBtc3RvcCA9IDIwMCwgbWF4ZGVwdGggPSAxIHbDoCBudSA9IDAuMTUgbMOgIGPDoWMgZ2nDoSB0cuG7iyB04buRaSDGsHUgdMOsbSDEkcaw4bujYyB0aGVvIHBoxrDGoW5nIHBow6FwIGZ1bGwgZ3JpZCBzZWFyY2guIA0KDQoNCk3hu5l0IHPhu5EgcXXDoSB0csOsbmggdMOtbmggdG/DoW4gbsOzaSBjaHVuZywgaG/hurdjIHF1w6EgdHLDrG5oIGh14bqlbiBsdXnhu4duIC0gdGluaCBjaOG7iW5oIHRodeG6rSB0b8OhbiBjw7MgdGjhu4MgbeG6pXQgbmhp4buBdSB0aOG7nWkgZ2lhbiwgdGjhuq1tIGNow60gbMOgIHLhuqV0IG5oaeG7gXUgdGjhu51pIGdpYW4gdsOgIMSR4buDIGJp4bq/dCB0aOG7nWkgZ2lhbiBj4bqnbiB0aGnhur90IMSR4buDIG3DoXkgdMOtbmggdMOtbmggdG/DoW4gbeG6pXQgYmFvIG5oacOqdSBjaMO6bmcgdGEgbMOgbSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KDQpzZXQuc2VlZCgyOSkNCnN5c3RlbS50aW1lKA0KDQogIHRyYWluKFZvbHVtZSB+IEhlaWdodCwgDQogICAgICBkYXRhID0gdHJhaW5fdmFsaWRfZGF0YSwgDQogICAgICBtZXRob2QgPSAiYnN0VHJlZSIsIA0KICAgICAgbWV0cmljID0gIlJzcXVhcmVkIiwgDQogICAgICB0dW5lR3JpZCA9IGdyaWRfYnN0VHJlZSwgDQogICAgICB0ckNvbnRyb2wgPSBncmlkX2NvbnRyb2xzX2N2KSAtPiBmdWxsX2dyaWRfYnN0VHJlZQ0KKQ0KYGBgDQoNClRo4budaSBnaWFuIGPhuqduIHRoaeG6v3QgbuG6sW0g4bufIGPhu5l0ICplbGFwc2VkKi4gTcOheSB0w61uaCBj4bunYSBi4bqhbiBjw7MgdGjhu4MgdOG6oW8gcmEgY29uIHPhu5Ega2jDoWMgdsOgIGNow61uaCBjw7luZyBt4buZdCBtw6F5IHTDrW5oLCB0aOG7sWMgaGnhu4duIGzhuqFpIG3hu5l0IGzhuqduIG7hu69hIHRow6wgY29uIHPhu5EgdGjhu51pIGdpYW4gY+G6p24gdGhp4bq/dCDEkcaw4bujYyBiw6FvIGPDoW8gY8Wpbmcgc+G6vSBoxqFpIGtow6FjIHNvIHbhu5tpIGJhbiDEkeG6p3UuIA0KDQoNCiMgU2FtcGxpbmcgVGVjaGlxdWVzIGZvciBUdXJuaW5nIEh5cGVyLXBhcmFtZXRlcg0KDQpQaOG6p24gdHLDqm4gxJHDoyBnaeG7m2kgdGhp4buHdSBzxqEgYuG7mSB24buBIENyb3NzLVZhbGlkYXRpb24uIE3hu6VjIG7DoHkgY2jDum5nIHRhIHPhur0gdMOsbSBoaeG7g3Uga8SpIGjGoW4gaGFpIGvEqSB0aHXhuq10IGzhuqV5IG3huqt1IChTYW1wbGluZyBUZWNobmlxdWVzKSBsw6AgQ3Jvc3MtVmFsaWRhdGlvbiB2w6AgQm9vc3RyYXAgxJHGsOG7o2MgbWluaCBo4buNYSBkxrDhu5tpIMSRw6J5OiANCg0KIVtdKEM6L1VzZXJzL0FkbWluL0RvY3VtZW50cy9waWMzXzIucG5nKQ0KDQoqKkNyb3NzLVZhbGlkYXRpb24qKg0KDQpUaGVvIHBoxrDGoW5nIHBow6FwIG7DoHkgdGjDrCB04burIGLhu5kgZOG7ryBsaeG7h3UgYmFuIMSR4bqndSAoxJHDonkgbMOgIGLhu5kgZOG7ryBsaeG7h3Ugc+G7rSBk4bulbmcgY2hvIGh14bqlbiBsdXnhu4duIHbDoCB0aW5oIGNo4buJbmgpIGPDsyAxMiBxdWFuIHPDoXQgc+G6vSBs4bqleSByYSwgdsOtIGThu6UsIGJhIG3huqt1IGNvbiBraMOhYyBuaGF1IMSR4buBdSBjw7MgOCBxdWFuIHPDoXQgZ+G7jWkgbMOgIFNhbXBlbGUgMSwgU2FtcGxlIDIgdsOgIFNhbXBsZSAzIChjw6FjIG3huqt1IGNvbiBuw6B5IGNow61uaCBsw6AgVHJhaW5pbmcgRGF0YSkga2jDoWMgbmhhdSB2w6AgdMawxqFuZyDhu6luZyB24bubaSBiYSBt4bqrdSBjb24gbsOgeSBsw6AgYmEgbeG6q3UgVmFsaWRhdGlvbiBEYXRhIMSR4buBdSBjw7MgNCBxdWFuIHPDoXQuIFRodeG6rXQgdG/DoW4gTUwgc+G6vSDEkcaw4bujYyBodeG6pW4gbHV54buHbiwgdGluaCBjaOG7iW5oIHbDoCDEkcOhbmggZ2nDoSB0csOqbiBjw6FjIGLhu5kgZOG7ryBsaeG7h3UgY29uIG7DoHkgdGhlbyBxdcOhIHRyw6xuaCDEkcaw4bujYyBtw7QgdOG6oyDhu58gdHLDqm4gbcOgIGNow7puZyB0YSDEkcOjIGJp4bq/dC4gDQoNCg0KUXXDoSB0csOsbmggaHXhuqVuIGx1eeG7h24sIHRpbmggY2jhu4luaCB2w6AgxJHDoW5oIGdpw6EgbcO0IGhpbmggYuG6sW5nIENyb3NzLVZhbGlkYXRpb24gKGNow61uaCB4w6FjIGzDoCBLLUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbiwgduG7m2kgSyA9IDMpIG5oxrAgdHLDqm4gxJHGsOG7o2MgaGnhu4duIHRo4buxYyBow7NhIGLhurFuZyBSIGNvZGVzIG5oxrAgc2F1IHbhu5tpIGfDs2kgY2FyZXQ6IA0KDQoNCmBgYHtyfQ0KY29udHJvbHNfY3YzIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSAzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDEpDQpgYGANCg0KDQpO4bq/dSBxdcOhIHRyw6xuaCBuw6B5IMSRxrDhu6NjIGzhurdwIGzhuqFpLCB2w60gZOG7pSwgNSBs4bqnbiB0aMOsIMSRxrDhu6NjIGfhu41pIGzDoCAqKlJlcGVhdGluZyBLLUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbioqIHbDoCDEkcaw4bujYyB0aOG7sWMgaGnhu4duIHbhu5tpIFIgY29kZXMgbmjGsCBzYXU6IA0KDQoNCmBgYHtyfQ0KY29udHJvbHNfY3YzX3JlcGVhdDUgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gMywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDUpDQpgYGANCg0KDQpN4buZdCB0csaw4budbmcgaOG7o3AgxJHhurdjIGJp4buHdCBj4bunYSBLLUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbiBsw6AgSyBi4bqxbmcgc+G7kSBsxrDhu6NuZyBjw6FjIHF1YW4gc8OhdCBj4bunYSBi4buZIGThu68gbGnhu4d1IMSRxrDhu6NjIHPhu60gZOG7pW5nLiBMw7pjIG7DoHkgZOG7hSBkw6BuZyBzdXkgcmEgcuG6sW5nIFZhbGlkYXRpb24gRGF0YSBjaOG7iSBjw7JuIGzhuqFpIMSRw7puZyAxIHF1YW4gc8OhdC4gVMOsbmggaHXhu5FuZyDEkeG6t2MgYmnhu4d0IG7DoHkgxJHGsOG7o2MgZ+G7jWkgbMOgICoqTGVhdmUgT25lIE91dCBDcm9zcy1WYWxpZGF0aW9uKiogKHRoxrDhu51uZyB2aeG6v3QgdOG6r3QgbMOgIExPT0NWKS4gSyBjw6BuZyBs4bubbiBjaMO6bmcgdGEgY8OgbmcgbeG6pXQgbmhp4buBdSBnw6FuaCBu4bq3bmcgdMOtbmggdG/DoW4gKG1vcmUgY29tcHV0YXRpb25hbGx5IGJ1cmRlbnNvbWUpIHbDoCBkbyB24bqteSB0cm9uZyB0aOG7sWMgdGnhu4VuIHRow6wgSyA9IDUgaG/hurdjIEsgPSAxMCB0aMaw4budbmcgxJHGsOG7o2MgbOG7sWEgY2jhu41uLiBNb2xpbmFybyAoMjAwNSkgY2jhu4kgcmEgcuG6sW5nDQpDcm9zcy1WYWxpZGF0aW9uIGtoaSBLID0gMTAgdOG6oW8gcmEga+G6v3QgcXXhuqMgdMawxqFuZyB04buxIG5oxrAgTE9PQ1YuIE5nb8OgaSByYSBNb2xpbmFybyAoMjAwNSkgdsOgIEtpbSAoMjAwOSkgY8WpbmcgY2hvIHLhurFuZyBSZXBlYXRpbmcgSy1Gb2xkIENyb3NzLVZhbGlkYXRpb24gY8OzIHRo4buDIGzDoG0gdMSDbmcgaGnhu4d1IHF14bqjIGThu7EgYsOhbyBj4bunYSB0aHXhuq10IHRvw6FuIHRyb25nIGtoaSB24bqrbiBkdXkgdHLDrCDEkcaw4bujYyBiaWFzIG5o4buPLiANCg0KKipUaGUgQm9vc3RyYXAqKg0KDQpDaOG7jW4gbeG6q3Ugbmfhuqt1IG5oacOqbiB0aGVvIHBoxrDGoW5nIHBow6FwIEJvb3N0cmFwIGzDoCBjw6FjaCB0aOG7qWMgbOG6pXkgbeG6q3UgY8OzIGhvw6BuIGzhuqFpIChFZnJvbiB2w6AgVGlic2hpcmFuaSwgMTk4NikuIMSQaeG7gXUgbsOgeSBjw7MgbmdoxKlhIGzDoDogKmtow7RuZyBnaeG7kW5nIG5oxrAgQ3Jvc3MtVmFsaWRhdGlvbiwgY8OhYyBxdWFuIHPDoXQgY+G7p2EgbeG6q3UgbOG6pXkgdGhlbyBwaMawxqFuZyBwaMOhcCBCb29zdHJhcCBjw7MgdGjhu4MgdHLDuW5nIGzhurdwIC0gdOG7qWMgbMOgIG3hu5l0IHF1YW4gc8OhdCBjw7MgdGjhu4MgxJHGsOG7o2MgY2jhu41uIG5oaeG7gXUgaMahbiAxIGzhuqduKi4gQ2jhurNuZyBo4bqhbiBuaMawIFNhbXBsZSAzIHRow6wgcXVhbiBzw6F0IGjDrG5oIHRyw7JuIG3DoHUgxJHhu48gdsOgIGjDrG5oIHZ1w7RuZyBtw6B1IHhhbmggxJHGsOG7o2MgxJHhu4F1IHh14bqldCBoaeG7h24gaGFpIGzhuqduIHRyb25nIG3huqt1LiAgDQoNCk5ow6xuIGNodW5nIGVycm9yIHJhdGUgKGtow6FjIGJp4buHdCBnaeG7r2EgZ2nDoSB0cuG7iyB0aOG7sWMgdOG6vyB2w6AgZOG7sSBiw6FvKSBz4bq9IMOtdCBiaeG6v24gxJHhu5luZyBoxqFuIEstRm9sZCBDcm9zcy1WYWxpZGF0aW9uIChFZnJvbiwgMTk4MykuIFR1eSBuaGnDqm4sIHbhu4EgdHJ1bmcgYsOsbmggdGjDrCAqeOG6pXAgeOG7iSA2Mi4zJSBjw6FjIHF1YW4gc8OhdCBz4bq9IMSRxrDhu6NjIGNo4buNbiBs4bq3cCBs4bqhaSDDrXQgbmjhuqV0IDEgbOG6p24qIG7Dqm4gcGjGsMahbmcgcGjDoXAgY2jhu41uIG3huqt1IG7DoHkgbMOgIHTGsMahbmcgdOG7sSBuaMawIEstRm9sZCBDcm9zcy1WYWxpZGF0aW9uIGtoaSBLID0gMi4gTuG6v3UgZOG7ryBsaeG7h3UgYmFuIMSR4bqndSBsw6AgcXXDoSBiw6kgdGjDrCBiaWFzIGPDsyB0aOG7gyBsw6AgbOG7m24gdsOgIGRvIHbhuq15IHBoxrDGoW5nIHBow6FwIGNo4buNbiBt4bqrdSBuw6B5IGNo4buJIG7Dqm4gxJHGsOG7o2Mgc+G7rSBk4bulbmcga2hpIGThu68gbGnhu4d1IMSR4bunIGzhu5tuLiBFZnJvbiAoMTk4MyksICBFZnJvbiB2w6AgVGlic2hpcmFuaSAoMTk5Nykgc2F1IMSRw7MgxJHDoyDEkeG7gSB4deG6pXQgY8OhaSBn4buNaSBsw6AgKjYzMiBtZXRob2QqIHbDoCAqNjMyKyBtZXRob2QqIG5o4bqxbSBnaeG6o2kgcXV54bq/dCBjw6FjIG5oxrDhu6NjIMSRaeG7g20gY+G7p2EgQm9vc3RyYXAgdHJvbmcgbeG7mXQgc+G7kSB0csaw4budbmcgaOG7o3AgbmjGsG5nIGhhaSBuaMOhbmggY2jhu41uIG3huqt1IEJvb3N0cmFwIG7DoHkga2jDtG5nIMSRxrDhu6NjIGdp4bqjaSB0aMOtY2ggdsOgIHPhu60gZOG7pW5nIHRyb25nIHNlcmllcyBiw6BpIGdp4bqjbmcgbsOgeS4gTuG6v3UgbXXhu5FuIHPhu60gZOG7pW5nIGLhuqFuIMSR4buNYyBjw7MgdGjhu4MgdGhhbSBraOG6o28gdGjDqm0gYuG6sW5nIGPDoWNoIHRoYXkgxJHhu5VpIGPDoWMgbOG7sWEgY2jhu41uIGPhu6dhIGjDoG0gYHRyYWluQ29udHJvbCgpYCBbdOG6oWkgxJHDonldKGh0dHBzOi8vcmRyci5pby9jcmFuL2NhcmV0L21hbi90cmFpbkNvbnRyb2wuaHRtbCkuIA0KDQpC4bqldCBr4buDIGvEqSB0aHXhuq10IGzhuqV5IG3huqt1IGzDoCBnw6wgY2jDum5nIHRhIGPFqW5nIGThu4UgdGjhuqV5IHLhurFuZywgdsOtIGThu6UsIGPDsyBow6BuZyBjaOG7pWMgbmfDoG4gKHRo4bqtbSBjaMOtIGzDoCBow6BuZyB04buJKSBraOG6oyBuxINuZyBraMOhYyBuaGF1IMSR4buDIGzhuqV5IHJhIDggdHJvbmcgc+G7kSAxMiBxdWFuIHPDoXQuIE3hurd0IGtow6FjLCBxdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIGPDoWMgdGh14bqtdCB0b8OhbiBNTCBjw7JuIGxpw6puIHF1YW4gxJHhur9uIG5oaeG7gXUgcXXDoSB0csOsbmggbmfhuqt1IG5oacOqbiBraMOhYyBu4buvYSBtw6Aga2jDtG5nIMSR4buBIGPhuq1wIMSRxrDhu6NjIHRyb25nIHNlcmllcyBuw6B5LiBEbyB24bqteSBraGkgaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCBjw6FjIHRodeG6rXQgdG/DoW4gTUwsIMSR4buDIMSR4bqjbSBi4bqjbyB2aeG7h2MgdMOhaSB04bqhbyBs4bqhaSBr4bq/dCBxdeG6oyBjaMO6bmcgdGEgc+G6vSBz4butIGThu6VuZyBs4buxYSBjaOG7jW4gYHNldC5zZWVkKDEpYCBraGkgaHXhuqVuIGx1eeG7h24gT0xTIG5oxrAgxJHDoyBiaeG6v3QgZMaw4bubaSDEkcOieTogDQoNCg0KYGBge3J9DQpzZXQuc2VlZCgxKSAjIEZvciByZXByb2R1Y3QgcmVzdWx0cy4gDQoNCnRyYWluKFZvbHVtZSB+IEhlaWdodCwgDQogICAgICBkYXRhID0gdHJhaW5fdmFsaWRfZGF0YSwgDQogICAgICBtZXRob2QgPSAibG0iLCAjIHRoaXMgb3B0aW9uIGZvciB0cmFpbmluZyBPTFMgDQogICAgICBtZXRyaWMgPSAiUnNxdWFyZWQiLCAjIGluZGljYXRvciBzZWxlY3RlZCBmb3IgdHJhaW5pbmcgYW5kIHR1cm5pbmcgbW9kZWwNCiAgICAgIHRyQ29udHJvbCA9IG9sc19jb250cm9scykgLT4gb2xzX21vZGVsDQoNCmBgYA0KDQoNClThuqV0IG5oacOqbiBjaMO6bmcgdGEgY8OzIHRo4buDIHRoYXkgMSBi4bqxbmcgbeG7mXQgc+G7kSBuZ3V5w6puIGLhuqV0IGvDrCBuw6BvIGtow6FjIHbDoCDEkcawxqFuZyBuaGnDqm4ga+G6v3QgcXXhuqMgc+G6vSBraMOhYy4gQuG6oW4gY8OzIHRo4buDIHThu7Ega2nhu4NtIG5naGnhu4dtIMSRaeG7gXUgbsOgeS4gU+G7kSAyOSDEkcaw4bujYyBjaOG7jW4gdsOsIMSRw7MgbMOgIG5nw6B5IHNpbmggbmjhuq10IGPhu6dhIG5nxrDhu51pIHZp4bq/dCBzZXJpZXMgbsOgeS4gDQoNCiMgUHJvY2VzcyBvZiBUcmFpbmluZyBhbmQgVHVybmluZyBhIE1MIEFsZ29yaXRobQ0KDQpRdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiwgdGluaCBjaOG7iW5oIHbDoCB0aOG6rW0gY2jDrSBsw6AgbOG7sWEgY2jhu41uIGdp4buvYSBjw6FjIHRodeG6rXQgdG/DoW4gTUwga2jDoWMgbmhhdSBz4bq9LCBuaMOsbiBjaHVuZywgdGhlbyBjw6FjIGLGsOG7m2MgY8OzIHRo4bupIHThu7EgZMaw4bubaSDEkcOieTogDQoNCioqU3RhZ2UgMTogRXhwbG9yYXRvcnkgRGF0YSBBbmFseXNpcyAoRURBKSoqLiBCxrDhu5tjIFtFREFdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0V4cGxvcmF0b3J5X2RhdGFfYW5hbHlzaXMpIGx1w7RuIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gxJHhuqd1IHRpw6puIGLhuqV0IG5o4bqxbSB4w6FjIMSR4buLbmggbmjhu69uZyDEkeG6t2MgxJFp4buDbSBj4bunYSBi4buZIGThu68gbGnhu4d1LCBk4bqhbmcgZOG7ryBsaeG7h3UgKEludGVnZXIsIENhdGVnb3JpY2FsLCBOdW1lcmljLi4uKSwgZOG7ryBsaeG7h3UgdGhp4bq/dSAoTWlzc2luZyBEYXRhKSB2w6AgbeG7qWMgxJHhu5kgbmdoacOqbSB0cuG7jW5nIGPhu6dhIGThu68gbGnhu4d1IHRoaeG6v3UuIA0KDQoqKlN0YWdlIDI6IERhdGEgcHJlLXByb2Nlc3NpbmcvRmVhdHVyZSBFbmdpbmVlcmluZyoqLiDEkMOieSB0aMaw4budbmcgbMOgIGLGsOG7m2MgbeG6pXQgbmhp4buBdSB0aOG7nWkgZ2lhbiBuaOG6pXQgY+G7p2EgbeG7mSBk4buxIMOhbiB2w6AgdOG6rXAgdHJ1bmcgdsOgbyB44butIGzDrSBt4buZdCBz4buRIChob+G6t2MgdOG6pXQgY+G6oykgduG6pW4gxJHhu4Egc2F1OiANCg0KLSBY4butIGzDrSBNaXNzaW5nIERhdGEuIEPDoWMgdGh14bqtdCB0b8OhbiBNTCDEkcOyaSBo4buPaSBy4bqxbmcgZGF0YSDEkeG6p3UgdsOgbyBraMO0bmcgxJHGsOG7o2MgcGjDqXAgY8OzIGThu68gbGnhu4d1IHRoaeG6v3UgKE1pc3NpbmcgRGF0YSkuIFbDrCB24bqteSB0csaw4bubYyBraGkgZGF0YSDEkcaw4bujYyBz4butIGThu6VuZyBjaG8gbeG7mXQgdGh14bqtdCB0b8OhbiBNTCBuw6BvIMSRw7MgdGjDrCBj4bqnbiBwaOG6o2kgeOG7rSBsw60gbWlzc2luZyBkYXRhLiANCi0gTmhp4buBdSB0aHXhuq10IHRvw6FuIE1MIMSRw7JpIGjhu49pIHLhurFuZyBkYXRhIMSR4bqndSB2w6BvIHThuqV0IGPhuqMgxJHhu4F1IHBo4bqjaSDhu58gZOG6oW5nIG51bWVyaWMuIERvIHbhuq15IGPDoWMgYmnhur9uIMSR4buLbmggdMOtbmggKENhdGVnb3JpY2FsIERhdGEvRmVhdHVyZSkgaG/hurdjIG5o4buvbmcga2nhu4N1IGThu68gbGnhu4d1IHTGsMahbmcgdOG7sSBj4bqnbiBwaOG6o2kgxJHGsOG7o2MgY2h1eeG7g24gxJHhu5VpIHbhu4EgbnVtZXJpYy4gUXXDoSB0csOsbmggbsOgeSDEkcaw4bujYyBn4buNaSBsw6AgKkVuY29kaW5nIENhdGVnb3JpY2FsIERhdGEqLiANCg0KLSBY4butIGzDrSB24bqlbiDEkeG7gSBjw6FjIGJp4bq/biB0xrDGoW5nIHF1YW4gY2FvLiANCi0gQ8OhYyBwaMawxqFuZyBwaMOhcCBiaeG6v24gxJHhu5VpIGThu68gbGnhu4d1IHNhbyBjaG8gdGh14bqtdCB0b8OhbiBNTCBjw7Mga2jhuqMgbsSDbmcgZOG7sSBiw6FvIGNhbyBuaOG6pXQgdHLDqm4gSG9sZG91dCBEYXRhLiBH4buNaSBjaHVuZyBsw6AgKkRhdGEgVHJhbnNmb3JtYXRpb24qLiANCi0gVOG6oW8gcmEgY8OhYyBiaeG6v24gbeG7m2kgdOG7qyBjw6FjIGJp4bq/biB0aHXhu5ljIGLhu5kgZOG7ryBsaeG7h3Ugbmd1ecOqbiBi4bqjbi4gSG/hurdjIG5nxrDhu6NjIGzhuqFpLCBjaOG7iSBjaOG7jW4gcmEgbeG7mXQgc+G7kSBiaeG6v24gc+G7kSB04burIGLhu5kgZOG7ryBsaeG7h3Ugbmd1ecOqbiBi4bqjbiBzYW8gY2hvIHRodeG6rXQgdG/DoW4gTUwgY8OzIGto4bqjIG7Eg25nIGThu7EgYsOhbyBjYW8gbmjhuqV0IHRyw6puIHThuq1wIEhvbGRvdXQgRGF0YS4gVmnhu4djIG7DoHkgdGjGsOG7nW5nIMSRxrDhu6NjIGfhu41pIGNodW5nIGzDoCAqRmVhdHVyZSBFbmdpbmVlcmluZy9GZWF0dXJlIFNlbGVjdGlvbiouIA0KDQoNClN0YWdlIDIgbMOgIGLGsOG7m2MgcXVhbiB0cuG7jW5nIHbDoCB0aMaw4budbmcgbMOgIGNoaeG6v20gbeG7mXQgdOG7iSBs4buHIMSRw6FuZyBr4buDIHbhu4EgdGjhu51pIGdpYW4uIERvIHbhuq15IGLGsOG7m2MgbsOgeSBz4bq9IMSRxrDhu6NjIHTDoWNoIGjhurNuIHRow6BuaCBt4buZdCBt4bulYyByacOqbmcgdsOgIHPhur0gxJHGsOG7o2MgdGjhuqNvIGx14bqtbiBjaGkgdGnhur90IGjGoW4gdHJvbmcgc2VyaWVzIGLDoGkgZ2nhuqNuZyBuw6B5LiANCg0KKipTdGFnZSAzOiBTcGxpdGluZyBEYXRhKiouIELGsOG7m2MgbsOgeSBi4buZIGThu68gbGnhu4d1IG5ndXnDqm4gYuG6o24gKGPDsyB0aOG7gyBsw6AgxJHDoyDEkcaw4bujYyB44butIGzDrSBzYXUga2hpIGhvw6BuIHRow6BuaCBTdGFnZSAyKSDEkcaw4bujYyBwaMOibiBjaGlhIHRow6BuaCBoYWkgcGjhuqduIHJpw6puZyBiaeG7h3QgbcOgIGNow7puZyB0YSDEkcOjIGJp4bq/dDogcGjhuqduIMSR4buDIGh14bqlbiBsdXnhu4duIHbDoCB0aW5oIGNo4buJbmggdGhhbSBz4buRIChUcmFpbmluZyArIFZhbGlkYXRpb24gRGF0YSksIHBo4bqnbiBjw7JuIGzhuqFpIMSR4buDIMSRw6FuaCBnacOhIG5nxrDhu6NjIGzhuqFpIHRodeG6rXQgdG/DoW4gTUwgLSB0aMaw4budbmcgZ+G7jWkgbMOgIEhvbGRvdXQgRGF0YSAobeG7mXQgc+G7kSB0w6BpIGxp4buHdSAtIHPDoWNoIGfhu41pIGzDoCBUZXN0IERhdGEpLiBLaMO0bmcgY8OzIHF1eSB04bqvYyBj4buRIMSR4buLbmggbsOgbyB24buBIHThu4kgbOG7hyBnaeG7r2EgaGFpIHBo4bqnbiBuw6B5IG3DoCB0w7l5IHbDoG8gcXV5IG3DtCBj4bunYSBk4buvIGxp4buHdSBjxaluZyBuaMawIG3hu5l0IHPhu5EgeeG6v3UgdOG7kSBraMOhYy4gVGjDtG5nIHRoxrDhu51uZyB0aMOsIHBo4bqnbiB0cmFpbmluZy92YWxpZGF0aW9uIGzDoCA3MCUgKGhv4bq3YyA4MCUpLCBjw7JuIGhvbGRvdXQgbMOgIDMwJSAoaG/hurdjIDIwJSkuICANCg0KKipTdGFnZSA0OiBTZWxlY3Qgc2FtcGxpbmcgbWV0aG9kIGZvciB0cmFpbmluZyBhbmQgdHVybmluZyBoeXBlci1wYXJhbWV0ZXJzKiouIELGsOG7m2MgbsOgeSBz4bq9IHjDoWMgxJHhu4tuaC9jaOG7iSDEkeG7i25oIHBoxrDGoW5nIHBow6FwIGNo4buNbiBt4bqrdSDEkeG7gyBodeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIHRoYW0gc+G7kSBjxaluZyBuaMawIHBoxrDGoW5nIHBow6FwIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgY2hvIHRodeG6rXQgdG/DoW4gTUwuIA0KDQoqKlN0YWdlIDU6IFRyYWluaW5nL1R1cm5pbmcgTUwgQWxnb3JpdGhtKiouIELGsOG7m2MgbsOgeSDEkcaw4bujYyB0aOG7sWMgaGnhu4duIMSR4buDIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgY8WpbmcgbmjGsCBodeG6pW4gbHV54buHbiB0aHXhuq10IHRvw6FuIE1MIHbhu5tpIGLhu5kgdGhhbSBz4buRIHThu5FpIMawdSB0w6xtIMSRxrDhu6NjIGThu7FhIHRyw6puIHBoxrDGoW5nIHBow6FwIGNo4buNbiBt4bqrdSB2w6AgdMOsbSBraeG6v20gxJHGsOG7o2MgeMOhYyDEkeG7i25oIOG7nyBTdGFnZSA0LiANCg0KKipTdGFnZSA2OiBFdmFsdWF0aW5nL0NvbXBhcmluZyBhbmQgU2VsZWN0aW5nKiouIFbhu5tpIGPDoWMgdGhhbSBz4buRIHThu5FpIMawdSB0w6xtIMSRxrDhu6NjIOG7nyBTdGFnZSA1IHRow6wgY2jhuqV0IGzGsOG7o25nIGThu7EgYsOhbyBj4bunYSB0aHXhuq10IHRvw6FuIChn4buNaSBsw6AgTW9kZWwgUGVyZm9ybWFuY2UpIHBo4bqjaSDEkcaw4bujYyDEkcOhbmggZ2nDoSB2w6Aga2nhu4NtIHRyYSBs4bqhaSB0csOqbiBIb2xkb3V0L1Rlc3QgRGF0YS4gDQoNCioqU3RhZ2UgNzogRGVwbG95bWVudCoqLiBHaeG6oyBz4butIGNow7puZyB0YSDEkcOjIHRpbmggY2jhu4luaCB2w6AgaHXhuqVuIGx1eeG7h24gxJHGsOG7o2MgbeG7mXQgdGh14bqtdCB0b8OhbiBjaOG6pW0gxJFp4buDbSB2w6AgeOG6v3AgaOG6oW5nIHTDrW4gZOG7pW5nIGNobyBjw6FjIGtow6FjaCBow6BuZyBjw7Mgbmh1IGPhuqd1IHZheSB0acOqdSBkw7luZyBk4buxYSB0csOqbiBjw6FjIGlucHV0cyDEkeG6p3UgdsOgbyBsw6A6ICgxKSB0aHUgbmjhuq1wIGjDoG5nIHRow6FuZywgKDIpIMSR4buZIHR14buVaSwgdsOgICgzKSBuZ2jhu4EgbmdoaeG7h3AuIFRo4bq/IHRow6wgY2jDum5nIHRhIGPhuqduIHRyaeG7g24ga2hhaSB0aHXhuq10IHRvw6FuIG7DoHkgYuG6sW5nIG3hu5l0IGPDoWNoIHRo4bupYyBuw6BvIMSRw7MgxJHhu4MsIHbDrSBk4bulLCBjw7MgdGjhu4MgdHJp4buDbiBraGFpIHRodeG6rXQgdG/DoW4gbsOgeSB0csOqbiBpUGhvbmUgaG/hurdjIGPDoWMgbcOheSB0w61uaCDEkeG7gyBiw6BuIGtow6FjLiDEkOG7gyBuaMOibiB2acOqbiBzYWxlIGPhu6dhIG3hu5l0IG5nw6JuIGjDoG5nIGhheSBt4buZdCBjw7RuZyB0aSB0w6BpIGNow61uaCBuw6BvIMSRw7MgY2jhu4kgY+G6p24gbmjhuq1wIGJhIHRow7RuZyB0aW4gY+G7p2EgbmfGsOG7nWkgbXXhu5FuIHZheSB0acOqdSBkw7luZyBsw6AgdGh1IG5o4bqtcCwgdHXhu5VpIHbDoCBuZ2jhu4EgbmdoaeG7h3AgdsOgbyB0aMOsIHPhur0gcGjhuqNpIG5o4bqtbiDEkcaw4bujYyBjw6J1IHRy4bqjIGzhu51pIGzDoCBuZ8aw4budaSDEkcOzIGPDsyDEkcaw4bujYyB2YXkgaGF5IGtow7RuZy4gVmnhu4djIHRyaeG7g24ga2hhaSB0aHXhuq10IHRvw6FuIMSRw6MgxJHGsOG7o2MgaHXhuqVuIGx1eeG7h24gdOG7qyB0csaw4bubYyAoUHJlLXRyYWluZWQgTW9kZWwvQWxnb3JpdGhtKSB0aMOgbmggY8OhYyBBcHAgKGfhu41pIHThuq90IGPhu6dhIEFwcGxpY2F0aW9uKSB0csOqbiBjw6FjIHRoaeG6v3QgYuG7iyBuaMawIGlQaG9uZSBjw7MgdGjhu4MgdGjDtG5nIHF1YSBjw6FjIGThu4tjaCB24bulIHTDrW5oIHRvw6FuIMSRw6FtIG3DonkgY+G7p2EgbeG7mXQgxJHhu5FpIHTDoWMvY8O0bmcgdGkga2jDoWMgKENsb3VkIFNlcnZpY2VzKSBob+G6t2MgdOG7sSB0cmnhu4NuIGtoYWkuIE7hu5lpIGR1bmcgY+G7p2EgU3RhZ2UgNyBuw6B5IG7hurFtIG5nb8OgaSBwaOG6oW0gdmkgY+G7p2Egc2VyaWVzIGLDoGkgZ2nhuqNuZyBuw6B5IG7Dqm4gc+G6vSBraMO0bmcgxJHGsOG7o2MgxJHhu4EgY+G6rXAgY2hpIHRp4bq/dC4gDQoNCg0KU3RhZ2UgMSB2w6AgMiB0cm9uZyB0aOG7sWMgdOG6vyB0aMaw4budbmcgbMOgIGtow6J1IG3huqV0IG5oaeG7gXUgdGjhu51pIGdpYW4gbmjhuqV0IGPhu6dhIG3hu5l0IGThu7Egw6FuIGThu68gbGnhu4d1IHRyb25nIHRo4buxYyB04bq/LiBWw6AgY8WpbmcgdsOsIHThuqdtIHF1YW4gdHLhu41uZyBj4bunYSBraMOidSBuw6B5ICjEkeG6t2MgYmnhu4d0IGzDoCBTdGFnZSAyKSBuw6puIGtow6J1IG7DoHkgc+G6vSDEkcaw4bujYyB0w6FjaCByacOqbmcgdGjDoG5oIGPDoWMgbeG7pWMgcmnDqm5nIHRyb25nIHNlcmllcyBiw6BpIGdp4bqjbmcgbsOgeS4gQ8OhYyBixrDhu5tjIGPDsm4gbOG6oWkgY+G7p2EgcXXDoSB0csOsbmggaHXhuqVuIGx1eeG7h24gLSB0aW5oIGNo4buJbmggbeG7mXQgdGh14bqtdCB0b8OhbiBNTCBz4bq9IMSRxrDhu6NjIG1pbmggaOG7jWEgYuG6sW5nIGhhaSB0YXNrIMSRaeG7g24gaMOsbmggY+G7p2Egdmnhu4djIMOhcCBk4bulbmcgY8OhYyB0aHXhuq10IHRvw6FuIE1MIGzDoCBSZWdyZXNzaW9uIHbDoCBDbGFzc2lmaWNhdGlvbiBkxrDhu5tpIMSRw6J5LiANCg0KDQojIFJlZ3Jlc3Npb24gVGFzazogQ2FzZSBvZiBQcmVkaWN0aW5nIEhvdXNpbmcgVmFsdWUgaW4gQm9zdG9uDQoNCkThu7EgYsOhbyBt4buZdCDEkeG6oWkgbMaw4bujbmcgbGnDqm4gdOG7pWMgKG51bWVyaWMpIG5oxrAgZ2nDoSBj4buVIHBoaeG6v3UsIGdpw6EgbmjDoCDEkcaw4bujYyBn4buNaSBjaHVuZyBsw6AgUmVncmVzc2lvbiBUYXNrLiBW4bubaSBuaMOzbSBjw7RuZyB2aeG7h2MgbsOgeSB0aMOsIGPDoWMgdGnDqnUgY2jDrSAoTWV0cmljcykgdGjGsOG7nW5nIMSRxrDhu6NjIHPhu60gZOG7pW5nIMSR4buDIGh14bqlbiBsdXnhu4duLCB0aW5oIGNo4buJbmggdGhhbSBz4buRIHbDoCBs4buxYSBjaOG7jW4gY8OhYyB0aHXhuq10IHRvw6FuIGtow6FjIG5oYXUgbMOgICoqTUFFKiogKE1lYW4gQWJzb2x1dGUgRXJyb3IpLCAqKk1TRSoqIChNZWFuIFNxdWFyZWQgRXJyb3IpLCAqKlJNU0UqKiAoUm9vdCBNZWFuIFNxdWFyZWQgRXJyb3IpIHbDoCAqKlJzcXVhcmVkKiogKENvZWZmaWNpZW50IG9mIGRldGVybWluYXRpb24pLiBDw6FjIHRpw6p1IGNow60gbsOgeSBjw7MgdGjhu4MgdMOsbSB0cm9uZyBi4bqldCBrw6wgZ2nDoW8gdHLDrG5oIHRo4buRbmcga8OqIG5o4bqtcCBtw7RuIG7DoG8gdsOgIGPDsyBjw7RuZyB0aOG7qWMgbmjGsCBzYXU6IA0KDQohW10oQzovVXNlcnMvQWRtaW4vRG9jdW1lbnRzL3BpYzQuSlBHKQ0KDQpDw7Mgbmhp4buBdSBnw7NpIGPhu6dhIFIgKFIgcGFja2FnZXMpIGPDsyBz4bq1biBuaOG7r25nIGjDoG0gxJHhu4MgdGjhu7FjIGhp4buHbiB0w61uaCB0b8OhbiBuaOG7r25nIG1ldHJpY3MgbsOgeSBu4bq/dSBiaeG6v3QgxJHhu5NuZyB0aOG7nWkgY+G6oyBoYWkgdGjhu6k6ICgxKSBnacOhIHRy4buLIMSRxrDhu6NjIGThu7EgYsOhbywgdsOgICgyKSBnacOhIHRy4buLIHRo4buxYyB04bq/LiBUdXkgbmhpw6puIGNow7puZyB0YSBuw6puIHThu7Egdmnhur90LCB2w60gZOG7pSwgaMOgbSB0w61uaCBNQUUgdGhlbyBjw7RuZyB0aOG7qWMgbcO0IHThuqMg4bufIHRyw6puIHbDoCDEkeG6t3QgdMOqbiBjaG8gaMOgbSBuw6B5IGzDoCAqY2FsY3VsYXRlX01BRSo6IA0KDQpgYGB7cn0NCiMgRnVuY3Rpb24gY2FsY3VsYXRlcyBNQUU6IA0KDQpjYWxjdWxhdGVfTUFFIDwtIGZ1bmN0aW9uKHByZWRpY3RlZF92YWx1ZXMsIGFjdHVhbHMpIHsNCiAgDQogIGVycm9ycyA8LSBhY3R1YWxzIC0gcHJlZGljdGVkX3ZhbHVlcw0KICANCiAgZXJyb3JzX2FicyA8LSBhYnMoZXJyb3JzKQ0KICANCiAgbiA8LSBsZW5ndGgocHJlZGljdGVkX3ZhbHVlcykNCiAgDQogIG1hZSA8LSBzdW0oZXJyb3JzX2FicykgLyBuDQogIA0KICByZXR1cm4obWFlKQ0KICANCn0NCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIHPhu60gZOG7pW5nIGjDoG0gbsOgeSDEkeG7gyB0w61uaCBNQUUgY+G7p2EgbcO0IGjDrG5oIE9MUyBraGkgc+G7rSBk4bulbmcgbcO0IGjDrG5oIG7DoHkgxJHhu4MgZOG7sSBiw6FvIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIERlZmluZSBhY3R1YWwgdmFsdWVzOiANCmFjdHVhbF92b2x1bWVzIDwtIGhvbGRvdXRfZGF0YSAlPiUgcHVsbChWb2x1bWUpDQoNCiMgQ2FsY3VsYXRlIE1BRTogDQpjYWxjdWxhdGVfTUFFKHByZWRpY3RlZF92YWx1ZXMgPSBwcmVkaWN0ZWRfdmFsdWVzLCBhY3R1YWxzID0gYWN0dWFsX3ZvbHVtZXMpDQpgYGANCkvhur90IHF14bqjIG7DoHkgxJHGsMahbmcgbmhpw6puIGzDoCBnaeG7kW5nIHNvIHbhu5tpIHZp4buHYyBz4butIGThu6VuZyBow6BtIGBNQUUoKWAgc+G6tW4gY8OzIGPhu6dhIGfDs2kgY2FyZXQgbmjGsCBjaMO6bmcgdGEgY8OzIHRo4buDIHRo4bqleTogIA0KDQpgYGB7cn0NCk1BRShwcmVkID0gcHJlZGljdGVkX3ZhbHVlcywgb2JzID0gYWN0dWFsX3ZvbHVtZXMpDQpgYGANCg0KVmnhu4djIHThu7Egdmnhur90IGjDoG0gbMOgIGvEqSBuxINuZyBj4bqnbiB0aGnhur90IHbDrCBy4bqxbmcgY8OzIG5oaeG7gXUgdMOsbmggaHXhu5FuZyBjaMO6bmcgdGEgc+G6vSBraMO0bmcgY8OzIHPhurVuIGjDoG0gxJHhu4MgbMOgbSBt4buZdCBjw7RuZyB2aeG7h2MgY+G7pSB0aOG7gyBuw6BvIMSRw7MgdsOgIGRvIHbhuq15IGPhuqduIHBo4bqjaSB2aeG6v3QgaMOgbS4gDQoNCsSQ4buDIG1pbmggaOG7jWEgY8OhYyBixrDhu5tjIGh14bqlbiBsdXnhu4duIHbDoCB0aW5oIGNo4buJbmggY8OhYyB0aHXhuq10IHRvw6FuIE1MIGNobyBuaMOzbSBjw7RuZyB2aeG7h2MgUmVncmVzc2lvbiBUYXNrIGNow7puZyB0YSBz4bq9IHPhu60gZOG7pW5nIGLhu5kgc+G7kSBsaeG7h3UgKkJvc3RvbiogZ+G7k20gNTA2IHF1YW4gc8OhdCDEkcaw4bujYyB0w61jaCBo4bujcCBz4bq1biDhu58gZ8OzaSBNQVNTIChjw6BpIMSR4bq3dCBi4bqxbmcgbOG7h25oIGBpbnN0YWxsLnBhY2thZ2VzKCJNQVNTIilgKTogDQoNCmBgYHtyfQ0KIyBDbGVhciBvdXIgUiBlbnZpcm9ubWVudDogDQpybShsaXN0ID0gbHMoKSkgIyBUaGlzIGNvbW1hbmQgc2hvdWxkIGJlIGRvbmUgYXQgdGhlIGJlZ2lubmluZyBvZiBhbnkgZGF0YSBwcm9qZWN0LiANCg0KIyBMb2FkIEJvc3RvbiBkYXRhOiANCmRhdGEoIkJvc3RvbiIsIHBhY2thZ2UgPSAiTUFTUyIpDQpgYGANCg0KQuG7mSBk4buvIGxp4buHdSBuw6B5IGPDsyAxNCBj4buZdCBiaeG6v24gKGZlYXR1cmVzKSBzYXU6IA0KDQotICpjcmltKjogcGVyIGNhcGl0YSBjcmltZSByYXRlIGJ5IHRvd24uDQoNCi0gKnpuKiBwcm9wb3J0aW9uIG9mIHJlc2lkZW50aWFsIGxhbmQgem9uZWQgZm9yIGxvdHMgb3ZlciAyNSwwMDAgc3EuZnQuDQoNCi0gKmluZHVzKiBwcm9wb3J0aW9uIG9mIG5vbi1yZXRhaWwgYnVzaW5lc3MgYWNyZXMgcGVyIHRvd24uDQoNCi0gKmNoYXMqIENoYXJsZXMgUml2ZXIgZHVtbXkgdmFyaWFibGUgKD0gMSBpZiB0cmFjdCBib3VuZHMgcml2ZXI7IDAgb3RoZXJ3aXNlKS4NCg0KLSAqbm94KiBuaXRyb2dlbiBveGlkZXMgY29uY2VudHJhdGlvbiAocGFydHMgcGVyIDEwIG1pbGxpb24pLg0KDQotICpybSogYXZlcmFnZSBudW1iZXIgb2Ygcm9vbXMgcGVyIGR3ZWxsaW5nLg0KDQotICphZ2UqIHByb3BvcnRpb24gb2Ygb3duZXItb2NjdXBpZWQgdW5pdHMgYnVpbHQgcHJpb3IgdG8gMTk0MC4NCg0KLSAqZGlzKiB3ZWlnaHRlZCBtZWFuIG9mIGRpc3RhbmNlcyB0byBmaXZlIEJvc3RvbiBlbXBsb3ltZW50IGNlbnRyZXMuDQoNCi0gKnJhZCogaW5kZXggb2YgYWNjZXNzaWJpbGl0eSB0byByYWRpYWwgaGlnaHdheXMuDQoNCi0gKnRheCogZnVsbC12YWx1ZSBwcm9wZXJ0eS10YXggcmF0ZSBwZXIgJDEwLDAwMC4NCg0KLSAqcHRyYXRpbyogcHVwaWwtdGVhY2hlciByYXRpbyBieSB0b3duLg0KDQotICpibGFjayogdGhlIHByb3BvcnRpb24gb2YgYmxhY2tzIGJ5IHRvd24uDQoNCi0gKmxzdGF0KiBsb3dlciBzdGF0dXMgb2YgdGhlIHBvcHVsYXRpb24gKHBlcmNlbnQpLg0KDQotICptZWR2KiBtZWRpYW4gdmFsdWUgb2Ygb3duZXItb2NjdXBpZWQgaG9tZXMgaW4gJDEwMDBzLg0KDQpN4bulYyB0acOqdSBj4bunYSBjaMO6bmcgdGEgbMOgIGThu7EgYsOhbyBtZWR2IChn4buNaSBsw6AgdGFyZ2V0IHZhcmlhYmxlIGhheSBvdXRjb21lIHZhcmlhYmxlKSBu4bq/dSBiaeG6v3QgZ2nDoSB0cuG7iyBj4bunYSAxMyBmZWF0dXJlcyBjw7JuIGzhuqFpLiBDw7MgdGjhu4MgeGVtIHF1YSBi4buZIGThu68gbGnhu4d1IG7DoHk6IA0KDQpgYGB7cn0NCiMgU2hvdyBzb21lIG9ic2VydmF0aW9uczogDQpoZWFkKEJvc3RvbikNCmBgYA0KDQpCb29zdGVkIFRyZWUgxJHDoyBiaeG6v3Qg4bufIG3hu6VjIHRyw6puIMSRxrDhu6NjIGNo4buNbiDEkeG7gyBtaW5oIGjhu41hIGNobyBjw6FjIGLGsOG7m2MgaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCBt4buZdCB0aHXhuq10IHRvw6FuIE1MLiBDaMO6IMO9IHLhurFuZyBjaMO6bmcgdGEgdOG6oW0gYuG7jyBxdWEgU3RhZ2UgMSB2w6AgU3RhZ2UgMiB2w6AgYuG6r3QgxJHhuqd1IGx1w7RuIHThu6sgU3RhZ2UgMyAtIGLGsOG7m2MgcGjDom4gY2hpYSBk4buvIGxp4buHdS4gQ+G7pSB0aOG7gyBjaMO6bmcgc+G6vSBs4bqleSByYSBuZ+G6q3Ugbmhpw6puIDQwMCBxdWFuIHPDoXQgxJHhu4MgaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCBtw7QgaMOsbmggKHBo4bqnbiBuw6B5IGfhu41pIGNodW5nIGzDoCBUcmFpbmluZyArIFZhbGlkYXRpb24gRGF0YSBuaMawIHRhIMSRw6MgYmnhur90KS4gUGjhuqduIGPDsm4gbOG6oWkgMTA2IHF1YW4gc8OhdCBsw6AgSG9sZG91dCBEYXRhIChjw7JuIGfhu41pIGzDoCBUZXN0IERhdGEpLiANCg0K4bueIHbDrSBk4bulIG3hu58gxJHhuqd1IGNow7puZyB0YSBz4butIGThu6VuZyBow6BtIGBzbGljZSgpYCDEkeG7gyB0aOG7sWMgaGnhu4duIHBow6JuIGNoaWEgZOG7ryBsaeG7h3UuIFR1eSBuaGnDqm4ga2hpIGh14bqlbiBsdXnhu4duIHbDoCB0w61uaCBjaOG7iW5oIGPDoWMgdGh14bqtdCB0b8OhbiBNTCBjaMO6bmcgdGEgbsOqbiBz4butIGThu6VuZyBow6BtIGBjcmVhdGVEYXRhUGFydGl0aW9uKClgIGPhu6dhIGfDs2kgY2FyZXQgxJHhu4MgcGjDom4gY2hpYSBk4buvIGxp4buHdSAodMOsbSBoaeG7g3UgdGjDqm0gduG7gSBoYW0gbsOgeSBbdOG6oWkgxJHDonldKGh0dHBzOi8vdG9wZXBvLmdpdGh1Yi5pby9jYXJldC9kYXRhLXNwbGl0dGluZy5odG1sKSkuIEjDoG0gbsOgeSBz4bq9IGLhuqNvIHRvw6BuIHThu5F0IG5o4bqldCBjw7MgdGjhu4MgxJHGsOG7o2MgcuG6sW5nIGPDoWMgYuG7mSBk4buvIGxp4buHdSDEkcaw4bujYyBwaMOibiBjaGlhIGPDsyBjw6FjIMSR4bq3YyDEkWnhu4NtIHRo4buRbmcga8OqIGdp4buRbmcgbmjGsCBi4buZIGThu68gbGnhu4d1IGJhbiDEkeG6p3UsIMSR4bq3YyBiaeG7h3QgbMOgIHBow6JuIHBo4buRaSBj4bunYSBvdXRjb21lIHZhcmlhYmxlOiANCg0KDQpgYGB7cn0NCiM9PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAgU3RhZ2UgMzogU3BsaXRpbmcgRGF0YQ0KIz09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgT2JzZXJ2YXRpb25zIGZvciB0cmFpbmluZyArIHZhbGlkYXRpb24gZGF0YTogDQoNCm5fb2JzIDwtIDQwMA0KDQojIFRvdGFsIG9ic2VydmF0aW9ucyBvZiBvcmlnaW5hbCBkYXRhOiANCg0KbiA8LSBucm93KEJvc3RvbikNCg0KIyBEZWZpbmUgdGhlIHBlcmNlbnRhZ2Ugb2YgZGF0YSB0aGF0IGdvZXMgdG8gdHJhaW5pbmcgYW5kIHZhbGlkYXRpb246IA0KDQpwIDwtIG5fb2JzIC8gbg0KDQojIFBvc2l0aW9ucyAoYnkgcm93KSBiZWxvbmcgdG8gdHJhaW5pbmcgKyB2YWxpZGF0aW9uIGRhdGE6IA0KDQppZF9mb3JfdHJhaW4gPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih5ID0gQm9zdG9uJG1lZHYsIHAgPSBwLCBsaXN0ID0gRkFMU0UsIHRpbWVzID0gMSkNCg0KIyBUcmFpbmluZyArIHZhbGlkYXRpb24gZGF0YTogDQpkYXRhX3RyYWluX3ZhbGlkIDwtIEJvc3RvbiAlPiUgc2xpY2UoaWRfZm9yX3RyYWluKQ0KDQojIEhvbGRvdXQgZGF0YTogDQpkYXRhX3Rlc3QgPC0gQm9zdG9uICU+JSBzbGljZSgtaWRfZm9yX3RyYWluKQ0KYGBgDQoNCkLGsOG7m2Mga+G6vyB0aeG6v3AgY2jhu4kgxJHhu4tuaC9s4buxYSBjaOG7jW4ga8SpIHRodeG6rXQgY2jhu41uIG3huqt1IMSR4buDIGh14bqlbiBsdXnhu4duIHbDoCB0aW5oIGNo4buJbmggdGh14bqtdCB0b8OhbiwgY+G7pSB0aOG7gyBsw6AgUmVwZWF0aW5nIEstRm9sZCBDcm9zcy1WYWxpZGF0aW9uLCBLID0gNCB2w6AgbOG6t3AgbOG6oWkgNSBs4bqnbjogDQoNCmBgYHtyfQ0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgIFN0YWdlIDQ6IFNlbGVjdCBzYW1wbGluZyBtZXRob2QgZm9yIHRyYWluaW5nIGFuZCB0dXJuaW5nIGh5cGVyLXBhcmFtZXRlcnMNCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCm15X3NhbXBsaW5nIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSA1KQ0KDQpgYGANCg0KU+G7rSBk4bulbmcgaMOgbSBgdHJhaW4oKWAgxJHhu4MgaHXhuqVuIGx1eeG7h24gQm9vc3RlZCBUcmVlIOG7nyBjaOG6vyDEkeG7mSBt4bq3YyDEkeG7i25oLiBDaMO6IMO9IHLhurFuZyBk4bqldSBjaOG6pW0g4bufICptZWR2IH4gLiogxJHhuqFpIGRp4buHbiBjaG8gdOG6pXQgY+G6oyAxMyBmZWF0dXJlcyBjw7JuIGzhuqFpIGPhu6dhIGLhu5kgZOG7ryBsaeG7h3UgdHLhu6sgbWVkdiBjw7JuICptZXRyaWMgPSAiUnNxdWFyZWQiKiBuZ2jEqWEgbMOgIHPhu60gZOG7pW5nIHRpw6p1IGNodeG6qW4gUnNxdWFyZWQgxJHhu4MgdGluaCBjaOG7iW5oIHbDoCBs4buxYSBjaOG7jW4gdGhhbSBz4buRIHThu5FpIMawdTogDQoNCmBgYHtyfQ0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBTdGFnZSA1OiBUcmFpbiBhbmQgdHVybiBCb29zdGVkIFRyZWUgKHVzaW5nIGRlZmF1bHQpDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0KIyBUcmFpbiBCb29zdGVkIFRyZWU6IA0KDQpzZXQuc2VlZCgyOSkNCnRyYWluKG1lZHYgfiAuLCANCiAgICAgIGRhdGEgPSBkYXRhX3RyYWluX3ZhbGlkLCANCiAgICAgIG1ldGhvZCA9ICJic3RUcmVlIiwgDQogICAgICBtZXRyaWMgPSAiUnNxdWFyZWQiLCANCiAgICAgIHRyQ29udHJvbCA9IG15X3NhbXBsaW5nKSAtPiBkZWZhdWx0X2JzdFRyZWUNCg0KIyBTaG93IHJlc3VsdHM6IA0KDQpkZWZhdWx0X2JzdFRyZWUNCg0KYGBgDQoNCk3hu5l0IHPhu5EgdGjDtG5nIGLDoW8gxJHDoW5nIGNow7ogw70gY8OzIHRo4buDIHRo4bqleSBsw6A6IA0KDQotICpSZXNhbXBsaW5nOiBDcm9zcy1WYWxpZGF0ZWQgKDQgZm9sZCwgcmVwZWF0ZWQgNSB0aW1lcykqIC0tPiDEkMOieSBjaMOtbmggbMOgIHRow7RuZyB0aW4gduG7gSBrxKkgdGh14bqtdCBjaOG7jW4gbeG6q3UgxJHDoyBz4butIGThu6VuZy4gIA0KDQotICpTdW1tYXJ5IG9mIHNhbXBsZSBzaXplczogMzAwLCAzMDIsIDMwMSwgMzAzLCAzMDAsIDMwMiwgLi4uKiAtLT4gRMOybmcgdGjDtG5nIGLDoW8gbsOgeSBjw7MgbmdoxKlhIGzDoCB0aGFtIHPhu5EgdOG7kWkgxrB1IMSRxrDhu6NjIHTDrG0gZOG7sWEgdHLDqm4gaHXhuqVuIGx1eeG7h24gQm9vc3RlZCBUcmVlIHRyw6puIGPDoWMgbeG6q3UgdHJhaW5pbmcgZGF0YSBjw7Mgc+G7kSBsxrDhu6NuZyAzMDAsIDMwMiwgMzAxLi4gdsOgIMSRxrDhu6NjIMSRw6FuaCBnacOhIG5nxrDhu6NjIGzhuqFpIHRyw6puIGPDoWMgbeG6q3UgdmFsaWRhdGlvbiBkYXRhIGPDsyBjaOG7q25nIDEwMCBxdWFuIHPDoXQuIEPDsyB04bqldCBj4bqjIDTDlzUgPSAyMCBj4bq3cCB0cmFpbmluZy92YWxpZGF0aW9uIGRhdGEgduG7m2kgc+G7kSBsxrDhu6NuZyBxdWFuIHPDoXQgdMawxqFuZyDhu6luZyB44bqlcCB44buJIGzDoCAzMDAgLSAxMDAgdsOsIGLhu5kgZOG7ryBsaeG7h3UgZGF0YV90cmFpbl92YWxpZCBjw7MgNDAyIHF1YW4gc8OhdC4NCg0KLSAqVHVuaW5nIHBhcmFtZXRlciAnbnUnIHdhcyBoZWxkIGNvbnN0YW50IGF0IGEgdmFsdWUgb2YgMC4xKiAtLT4gQm9vc3RlZCBUcmVlIGPDsyBiYSB0aGFtIHPhu5EgY8OzIHRo4buDIHRpbmggY2jhu4luaC4gTmjGsG5nIHPhu60gZOG7pW5nIGNo4bq/IMSR4buZIG3hurdjIMSR4buLbmggY2hvIGh14bqlbiBsdXnhu4duIHbDoCB0w6xtIGtp4bq/bSB0aGFtIHPhu5EgdOG7kWkgxrB1IHRow6wgdGhhbSBz4buRIG51IMSRxrDhu6NjIGPhu5EgxJHhu4tuaCBsw6AgMC4xLiBW4bubaSB0aGFtIHPhu5EgbWF4ZGVwdGggdGjDrCBjw6FjIGdpw6EgdHLhu4sg4bupbmcgdmnDqm4gbMOgIDEsIDIgdsOgIDMgY8OybiBtc3RvcCB0aMOsIGPDoWMgZ2nDoSB0cuG7iyDhu6luZyB2acOqbiBsw6AgNTAsIDEwMCB2w6AgMTUwLiBDw7MgdGjhu4MgaGnhu4N1IGzDoCBjw7MgdOG6pXQgY+G6oyAxw5czw5czw5c0w5c1ICsgMSA9IDE4MSBtw7QgaMOsbmggQm9vc3RlZCBUcmVlIMSRw6MgxJHGsOG7o2MgaHXhuqVuIGx1eeG7h24uIA0KDQotICpSc3F1YXJlZCB3YXMgdXNlZCB0byBzZWxlY3QgdGhlIG9wdGltYWwgbW9kZWwgdXNpbmcgdGhlIGxhcmdlc3QgdmFsdWUuKiB2w6AgKlRoZSBmaW5hbCB2YWx1ZXMgdXNlZCBmb3IgdGhlIG1vZGVsIHdlcmUgbXN0b3AgPSAxNTAsIG1heGRlcHRoID0gMyBhbmQgbnUgPSAwLjEuKiAtLT4gRMOybmcgdGjDtG5nIGLDoW8gbsOgeSBjw7MgbmdoxKlhIGzDoCBSc3F1YXJlZCDEkcaw4bujYyBz4butIGThu6VuZyBsw6BtIHRpw6p1IGNow60gdMOsbSBjw6FjIGtp4bq/bSB0aGFtIHPhu5EgdOG7kWkgxrB1IHbDoCBi4buZIHRoYW0gc+G7kSB04buRaSDGsHUgxJHDsyBsw6AgbXN0b3AgPSAxNTAsIG1heGRlcHRoID0gMyB2w6AgbnUgPSAwLjEuIFbhu5tpIGLhu5kgdGhhbSBz4buRIG7DoHkgdGjDrCBnacOhIHRy4buLIGLDrG5oIHF1w6JuIGPhu6dhIFJzcXVhcmVkIHRyw6puIDIwIGzhuqduIGNo4buNbiBt4bqrdSwgbeG7l2kgbeG6q3UgY8OzIHPhu5EgcXVhbiBzw6F0IHjhuqVwIHjhu4kgMTAwIChsw6Agc+G7kSBsxrDhu6NuZyBj4bunYSB2YWxpZGF0aW9uIGRhdGEpIHPhur0gbMOgIDAuODUzNDk4MS4gQuG7mSB0aGFtIHPhu5EgdOG7kWkgxrB1IG7DoHkgY8OzIHRo4buDIMSRxrDhu6NjIHTDrG0gYuG6sW5nIGPDoWNoIGtoYWkgdGjDoWMgZGVmYXVsdF9ic3RUcmVlIChn4buNaSBsw6AgY2FyZXQgb2JqZWN0KSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KZGVmYXVsdF9ic3RUcmVlJGJlc3RUdW5lDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyBraOG6o28gc8OhdCBz4buxIHRoYXkgxJHhu5VpIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSBCb29zdGVkIFRyZWUgdGhlbyB0acOqdSBjaHXhuqluIFJzcXVhcmVkIGtoaSBjw6FjIHRoYW0gc+G7kSBj4bunYSB0aHXhuq10IHRvw6FuIG7DoHkgdGhheSDEkeG7lWkgKEZpZ3VyZSAzKTogDQoNCmBgYHtyfQ0KDQpnZ3Bsb3QoZGVmYXVsdF9ic3RUcmVlKSArIA0KICBsYWJzKHRpdGxlID0gIkZpZ3VyZSAzOiBNb2RlbCBQZXJmb3JtYW5jZSBieSBQYXJhbWV0ZXJzIiwgDQogICAgICAgc3VidGl0bGUgPSAiRGVmYXVsdCBTZWFyY2gsIG51ID0gMC4xIikgKyANCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gInRvcCIpDQoNCmBgYA0KDQpGaWd1cmUgMyBjaOG7iSByYSBy4bqxbmcgY8OzIMSR4buLbmggbnUgPSAwLjEsIHbhu5tpIG3hu41pIGdpw6EgdHLhu4sgY+G7p2EgbWF4ZGVwdGggdGjDrCBraOG6oyBuxINuZyBk4buxIGLDoW8gY+G7p2EgbcO0IGjDrG5oIHTEg25nIGtoaSBtc3RvcCAoQm9vc3RpbmcgSW50ZXJhdGlvbnMpIHTEg25nLiBC4buZIGdpw6EgdHLhu4sgdGhhbSBz4buRIHThu5FpIMawdSB0w6xtIMSRxrDhu6NjIHRoZW8gY2jhur8gxJHhu5kgbeG6t2MgxJHhu4tuaCBuw6B5IHPhur0gxJHGsOG7o2Mgc+G7rSBk4bulbmcgbMOgbSBjxqEgc+G7nyDEkeG7gyB4w6J5IGThu7FuZyBHcmlkIFNlYXJjaC4gQ+G7pSB0aOG7gywgR3JpZCBTZWFyY2ggLSBkYW5oIHPDoWNoIGPDoWMg4bupbmcgdmnDqm4gY+G7p2EgdGhhbSBz4buRIHPhur0gY2jhu6lhIGLhu5kgZ2nDoSB0cuG7iyBtc3RvcCA9IDE1MCwgbWF4ZGVwdGggPSAzIHbDoCBudSA9IDAuMTogDQoNCmBgYHtyfQ0KIyBEZWZpbmUgR3JpZCBTZWFyY2ggZm9yIHNlYXJjaCBvcHRpbWFsIGh5cGVyLXBhcmFtZXRlcnM6IA0KDQpncmlkX2JzdFRyZWUgPC0gZXhwYW5kLmdyaWQobnUgPSBzZXEoMC4wMjUsIDAuMTUsIGJ5ID0gMC4wMjUpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc3RvcCA9IHNlcSgyNSwgMjAwLCBieSA9IDI1KSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4ZGVwdGggPSBzZXEoMSwgNiwgYnkgPSAxKSkNCmBgYA0KDQpDw7MgdGjhu4Mga2nhu4NtIHRyYSBy4bqxbmcgY8OzIHThuqV0IGPhuqMgMjg4IGLhu5kgdGhhbSBz4buRIGtow6FjIG5oYXU6IA0KDQpgYGB7cn0NCm5yb3coZ3JpZF9ic3RUcmVlKQ0KYGBgDQoNClbDoCBjaMO6bmcgdGEgcGjhuqNpIHTDrG0gcmEgdHJvbmcgc+G7kSAyODggbsOgeSBi4buZIHRoYW0gc+G7kSB04buRaSDGsHUgY2hvIEJvb3N0ZWQgVHJlZSBi4bqxbmcgdmnhu4djIGh14bqlbiBsdXnhu4duIGzhuqFpLiBWw6wgcXXDoSB0csOsbmggbsOgeSBjw7MgdGjhu4MgbeG6pXQga2jDoSBuaGnhu4F1IHRo4budaSBnaWFuIG7Dqm4gY2jDum5nIHRhIGzhu5NuZyB2w6BvIGjDoG0gYHN5c3RlbS50aW1lKClgIMSR4buDIMaw4bubYyBsxrDhu6NuZyB0aOG7nWkgZ2lhbiBtw6AgbcOheSB0w61uaCBj4bqnbiBz4butIGThu6VuZy4gVsOsIHF1w6EgdHLDrG5oIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgdGhlbyBGdWxsIEdyaWQgU2VhcmNoIGPDsyB0aOG7gyBt4bqldCBuaGnhu4F1IHRo4budaSBnaWFuIG7Dqm4gY2jDum5nIHRhIHPhu60gZOG7pW5nIGzhu7FhIGNo4buNbiBgYWxsb3dQYXJhbGxlbCA9IFRSVUVgIMSR4buDIHThuq1uIGThu6VuZyBraOG6oyBuxINuZyB0w61uaCB0b8OhbiBzb25nIHNvbmcgxJHhu5FpIHbhu5tpIG5o4buvbmcgbcOheSB0w61uaCBtw6AgYuG7mSB2aSB44butIGzDrSBjw7Mgbmhp4buBdSBuaMOibiAoY29yZSk6IA0KDQoNCmBgYHtyfQ0KIyBSZXNldCBjb2RpdGlvbnMgZm9yIHNlYXJjaGluZyBvcHRpbWFsIGh5cGVyLXBhcmFtZXRlcnM6IA0KDQpteV9zYW1wbGluZ19mdWxsX2dyaWQgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlYXJjaCA9ICJncmlkIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93UGFyYWxsZWwgPSBUUlVFLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSA0KQ0KDQojIFNlYXJjaCBvcHRpbWFsIHBhcmFtZXRlciAoYW5kIHRyYWluKSBmb3IgQm9vc3RlZCBUcmVlIHVzaW5nIEZ1bGwgR3JpZCBTZWFyY2ggTWV0aG9kOiANCnNldC5zZWVkKDI5KQ0KDQpzeXN0ZW0udGltZSgNCiAgDQogIHRyYWluKG1lZHYgfiAuLCANCiAgICAgIGRhdGEgPSBkYXRhX3RyYWluX3ZhbGlkLCANCiAgICAgIG1ldGhvZCA9ICJic3RUcmVlIiwgDQogICAgICBtZXRyaWMgPSAiUnNxdWFyZWQiLCANCiAgICAgIHR1bmVHcmlkID0gZ3JpZF9ic3RUcmVlLCANCiAgICAgIHRyQ29udHJvbCA9IG15X3NhbXBsaW5nX2Z1bGxfZ3JpZCkgLT4gZnVsbEdyaWRfYnN0VHJlZQ0KKQ0KYGBgDQoNCk3huqV0IGNo4burbmcgMTggcGjDunQgxJHhu4MgaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCBCb29zdGVkIFRyZWUuIELhu5kgdGhhbSBz4buRIHThu5FpIMawdSB0w6xtIMSRxrDhu6NjIHRoZW8gRnVsbCBHcmlkIFNlYXJjaCBsw6AgbXN0b3AgPSAyMDAsIG1heGRlcHRoID0gMyB2w6AgbnUgPSAwLjA3NTogDQoNCmBgYHtyfQ0KIyBTaG93IG9wdGltYWwgcGFyYW1ldGVyczogDQpmdWxsR3JpZF9ic3RUcmVlJGJlc3RUdW5lDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBCb29zdGVkIFRyZWUgduG7m2kgYuG7mSB0aGFtIHPhu5EgdOG7kWkgxrB1IG7DoHkgY2hvIGThu7EgYsOhbyBraGkgYmnhur90IDEzIGZlYXR1cmVzIMSR4bqndSB2w6BvOiANCg0KYGBge3J9DQojIEZlYXR1cmVzIHVzZWQgZm9yIHByZWRpY3Rpbmc6IA0KZmVhdHVyZXNfaW5wdXQgPC0gZGF0YV90ZXN0ICU+JSBzZWxlY3QoLW1lZHYpDQoNCiMgQWN0dWFsczogDQphY3R1YWxfbWVkdiA8LSBkYXRhX3Rlc3QgJT4lIHB1bGwobWVkdikNCg0KIyBVc2UgQm9vc3RlZCBUcmVlIGZvciBwcmVkaWN0aW5nOiANCnByZWRfbWVkdiA8LSBwcmVkaWN0KGZ1bGxHcmlkX2JzdFRyZWUsIGZlYXR1cmVzX2lucHV0KQ0KDQojIFNob3cgc29tZSBwcmVkaWN0ZWQgdmFsdWVzOiANCmhlYWQocHJlZF9tZWR2KQ0KYGBgDQoNCkNobyDEkeG6v24gbMO6YyBuw6B5IGPDsyB0aOG7gyBjb2kgbMOgIMSRYW5nIGPDsyBoYWkgbcO0IGjDrG5oIGPhuqFuaCB0cmFuaCBuaGF1OiBCb29zdGVkIFRyZWUgbeG6t2MgxJHhu4tuaCB2w6AgQm9vc3RlZCBUcmVlIHbhu5tpIHRoYW0gc+G7kSB04buRaSDGsHUgxJHGsOG7o2MgdMOsbSBraeG6v20gZOG7sWEgdHLDqm4gcGjGsMahbmcgcGjDoXAgRnVsbCBHcmlkIFNlYXJjaC4gTOG6pXkgY8SDbiBj4bupIGfDrCDEkeG7gyBjaOG7jW4gbcO0IGjDrG5oIG7DoHkgY2hvIGThu7EgYsOhbyBtZWR2IGNo4bupIGtow7RuZyBwaOG6o2kgbMOgIG3DtCBow6xuaCBjw7JuIGzhuqFpPy4gxJDhu4MgxJHDoW5oIGdpw6EsIHNvIHPDoW5oIGhhaSAoaGF5IG5oaeG7gXUpIG3DtCBow6xuaCwgdGh14bqtdCB0b8Ohbi9jw6FjaCB0aeG6v3AgY+G6rW4ga2jDoWMgbmhhdSB0aMOsIHRyxrDhu5tjIGjhur90IGPhuqduIGzhu7FhIGNo4buNbiBt4buZdCB0acOqdSBjaMOtIMSR4buDIMSRw6FuaCBnacOhIHbDoCBzbyBzw6FuaC4gQ2jDum5nIHRhIGPDsyB0aOG7gyBjaOG7jW4gdGnDqnUgY2jDrSDEkcOzIGzDoCBSc3F1YXJlZC4gVsOsIMSRw6MgY8OzIGPhurdwIGdpw6EgdHLhu4sgZOG7sSBiw6FvIHbDoCBnacOhIHRy4buLIHRo4buxYyB04bq/IG7Dqm4gY2jDum5nIHRhIGPDsyB0aOG7gyB0w61uaCBjaOG7iSBz4buRIG7DoHkgYuG6sW5nIGPDoWNoIHZp4bq/dCBow6BtIGPDsyB0w6puICoqY2FsY3VsYXRlX1IyKiogdGhlbyBtw7QgdOG6oyBj4bunYSBKYW1lcyBldCBhbC4gKDIwMTgsIHA3MCkgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBTdGFnZSA2OiBFdmFsdWF0ZS9jb21wYXJlL3NlbGVjdCBNTCBtb2RlbHMvIG9yIGFsdGVybmF0aXZlIGFwcHJvYWNoZXMNCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgRnVuY3Rpb24gY2FsY3VsYXRlcyBSc3F1YXJlZDogDQpjYWxjdWxhdGVfUjIgPC0gZnVuY3Rpb24ocHJlZGljdGVkX3ZhbHVlcywgYWN0dWFscykgew0KICANCiAgbXlfY29yIDwtIGNvcihwcmVkaWN0ZWRfdmFsdWVzLCBhY3R1YWxzKQ0KICANCiAgcl9zcXVhcmVkIDwtIG15X2Nvcl4yDQogIA0KICByZXR1cm4ocl9zcXVhcmVkKQ0KfQ0KYGBgDQoNClPhu60gZOG7pW5nIGjDoG0gbsOgeSBjaG8gY+G6t3AgdGjhu7FjIHThur8gLSBk4buxIGLDoW8gYuG7n2kgQm9vc3RlZCBUcmVlOiANCg0KYGBge3J9DQpjYWxjdWxhdGVfUjIocHJlZF9tZWR2LCBhY3R1YWxfbWVkdikNCmBgYA0KDQpOZ2jEqWEgbMOgIGtoaSBz4butIGThu6VuZyAxMyBmZWF0dXJlcyDEkeG6p3UgdsOgbyDEkeG7gyBk4buxIGLDoW8gbWVkdiBi4bqxbmcgQm9vc3RlZCBUcmVlIHRow6wgMTMgZmVhdHVyZXMgbsOgeSBnaeG6o2kgdGjDrWNoIMSRxrDhu6NjIDg3LjQyJSBiaeG6v24gxJHhu5luZyBj4bunYSBtZWR2LiANCg0KDQpLaOG6o28gc8OhdCBz4buxIHRoYXkgxJHhu5VpIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSBCb29zdGVkIFRyZWUgdGhlbyB0acOqdSBjaHXhuqluIFJzcXVhcmVkIGtoaSBjw6FjIHRoYW0gc+G7kSBj4bunYSB0aHXhuq10IHRvw6FuIG7DoHkgdGhheSDEkeG7lWkgKEZpZ3VyZSA0KTogDQoNCmBgYHtyfQ0KZ2dwbG90KGZ1bGxHcmlkX2JzdFRyZWUpICsgDQogIGxhYnModGl0bGUgPSAiRmlndXJlIDQ6IE1vZGVsIFBlcmZvcm1hbmNlIGJ5IFBhcmFtZXRlcnMiLCANCiAgICAgICBzdWJ0aXRsZSA9ICJGdWxsIEdyaWQgU2VhcmNoIikgKyANCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gInRvcCIsIGxlZ2VuZC5ib3ggPSAiaG9yaXpvbnRhbCIpICsgDQogIGd1aWRlcyhjb2xvciA9IGd1aWRlX2xlZ2VuZChucm93ID0gMSkpDQpgYGANCg0KDQpU4burIEZpZ3VyZSA0IGPDsyB0aOG7gyBuaOG6rW4gxJHhu4tuaCBy4bqxbmcgbuG6v3UgY2jDum5nIHRhIG7hu5tpIHLhu5luZyBtc3RvcCBsw6puIG7hu69hIHRow6wgY8OzIHRo4buDIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSBtw7QgaMOsbmggY8OybiB0xINuZyBu4buvYS4gTmjhuq1uIMSR4buLbmggbsOgeSBsw6AgY8ahIHPhu58gY2hvIHZp4buHYywgdsOtIGThu6UsIHThuq1wIHRydW5nIHRpbmggY2jhu4luaCBtc3RvcCB0cm9uZyBraGkgY+G7kSDEkeG7i25oIG1heGRlcHRoID0gMyB2w6AgbnUgPSAwLjA3NS4gQ2jDum5nIHRhIHRoaeG6v3QgbOG6rXAgR3JpZCBTZWFyY2ggbeG7m2kgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCm5ld19ncmlkIDwtIGV4cGFuZC5ncmlkKG1heGRlcHRoID0gMywgDQogICAgICAgICAgICAgICAgICAgICAgICBudSA9IDAuMDc1LCANCiAgICAgICAgICAgICAgICAgICAgICAgIG1zdG9wID0gc2VxKDE3NSwgNDAwLCBieSA9IDI1KSkNCmBgYA0KDQpIdeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIHbhu5tpIEdyaWQgU2VhcmNoIG3hu5tpIG7DoHk6IA0KDQpgYGB7cn0NCg0KIyBUcmFpbiBCb29zdGVkIFRyZWUgYWdhaW46IA0Kc2V0LnNlZWQoMjkpDQoNCnN5c3RlbS50aW1lKA0KICANCiAgdHJhaW4obWVkdiB+IC4sIA0KICAgICAgZGF0YSA9IGRhdGFfdHJhaW5fdmFsaWQsIA0KICAgICAgbWV0aG9kID0gImJzdFRyZWUiLCANCiAgICAgIG1ldHJpYyA9ICJSc3F1YXJlZCIsIA0KICAgICAgdHVuZUdyaWQgPSBuZXdfZ3JpZCwgDQogICAgICB0ckNvbnRyb2wgPSBteV9zYW1wbGluZ19mdWxsX2dyaWQpIC0+IG5ld0dyaWRfYnN0VHJlZQ0KKQ0KDQojIFNob3cgbmV3IHJlc3VsdHM6IA0KDQpuZXdHcmlkX2JzdFRyZWUNCmBgYA0KDQpLaOG6o28gc8OhdCBraOG6oyBuxINuZyBk4buxIGLDoW8gY+G7p2EgQm9vc3RlZCBUcmVlIGtoaSBtc3RvcCAoQm9vc3RpbmcgSXRlcmF0aW9ucykgdGhheSDEkeG7lWkgduG7m2kgY2jDuiDDvSBy4bqxbmcgaGFpIHRoYW0gc+G7kSBjw7JuIGzhuqFpIMSRxrDhu6NjIGPhu5EgxJHhu4tuaDogDQpgYGB7cn0NCmdncGxvdChuZXdHcmlkX2JzdFRyZWUpICsgDQogIGxhYnModGl0bGUgPSAiRmlndXJlIDU6IE1vZGVsIFBlcmZvcm1hbmNlIGJ5IG1zdG9wIiwgDQogICAgICAgc3VidGl0bGUgPSAiTmV3IEdyaWQgU2VhcmNoIikgKyANCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gInRvcCIpDQpgYGANCg0KRmlndXJlIDUgY2jhu4kgcmEgcuG6sW5nIG5ow6xuIGNodW5nLCBtc3RvcCB0xINuZyB0aMOsIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSB0aHXhuq10IHRvw6FuIHTEg25nIG5oxrBuZyDEkeG6v24gbeG7mXQgbmfGsOG7oW5nIG7DoG8gxJHDsyB0aMOsIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSB0aHXhuq10IHRvw6FuIGzhuqFpIGdp4bqjbS4gTmjGsCB24bqteSB0aGFtIHPhu5EgdOG7kWkgxrB1IG3hu5tpIHPhur0gbMOgIG1zdG9wID0gMzAwLCBtYXhkZXB0aCA9IDMgdsOgIG51ID0gMC4wNzU6IA0KDQpgYGB7cn0NCm5ld0dyaWRfYnN0VHJlZSRiZXN0VHVuZQ0KYGBgDQoNCkNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgQm9vc3RlZCBUcmVlIHThu5FpIMawdSBoxqFuIG7DoHkgY2hvIGThu7EgYsOhbyB2w6AgdMOtbmggUnNxdWFyZWQ6IA0KDQpgYGB7cn0NCiMgUHJlZGljdCB3aXRoIG5ldyBCb29zdGVkIFRyZWU6IA0KcHJlZF9tZWR2X2JldHRlciA8LSBwcmVkaWN0KG5ld0dyaWRfYnN0VHJlZSwgZmVhdHVyZXNfaW5wdXQpDQoNCiMgQ2FsY3VsYXRlIFJzcXVhcmVkIGFuZCBjb21wYXJlOiANCmNhbGN1bGF0ZV9SMihwcmVkX21lZHZfYmV0dGVyLCBhY3R1YWxfbWVkdikNCmBgYA0KDQpSc3F1YXJlZCA9IDg3Ljg2JSBjYW8gaMahbiA4Ny40MiUgLSBuw7NpIGPDoWNoIGtow6FjIEJvb3N0ZWQgVHJlZSB24bubaSBi4buZIHRoYW0gc+G7kSBtc3RvcCA9IDMwMCwgbWF4ZGVwdGggPSAzIHbDoCBudSA9IDAuMDc1IGPDsyBraOG6oyBuxINuZyBk4buxIGLDoW8gdOG7kXQgaMahbiBzbyB24bubaSBCb29zdGVkIFRyZWUgduG7m2kgYuG7mSB0aGFtIHPhu5EgbXN0b3AgPSAyMDAsIG1heGRlcHRoID0gMyB2w6AgbnUgPSAwLjA3NS4gQ8OidSBo4buPaSBsw6AgYuG6sW5nIGPDoWNoIG7DoG8gY2jhu6luZyBtaW5oIMSRxrDhu6NjIHLhurFuZyDEkWnhu4F1IG7DoHkgKmtow7RuZyBwaOG6o2kgbMOgIHPhu7Egbmfhuqt1IG5oacOqbiBkbyAidMOsbmggY+G7nSIgY2jhu41uIMSRxrDhu6NjIGLhu5kgZOG7ryBsaeG7h3UgZmVhdHVyZXNfaW5wdXQgbcOgICLGsHUgdGnDqm4iIGNobyBCb29zdGVkIFRyZWUgY8OzIG1zdG9wID0gMzAwLCBtYXhkZXB0aCA9IDMgdsOgIG51ID0gMC4wNzUgZOG6q24gxJHhur9uIGvhur90IHF14bqjIGThu7EgYsOhbyBzw6F0IGjGoW4gc28gduG7m2kgZ2nDoSB0cuG7iyB0aOG7sWMgY+G7p2EgbWVkdj8qIEPDoWNoIMSRxqFuIGdp4bqjbiBuaOG6pXQgxJHhu4MgY8OzIHRo4buDIHRy4bqjIGzhu51pIGPDonUgaOG7j2kgbsOgeSBsw6AgY2jDum5nIHRhIGPDsyB0aOG7gyBzbyBzw6FuaCBnacOhIHRy4buLIHRydW5nIGLDrG5oIGPhu6dhIFJzcXVhcmVkIHRyw6puIDIwIGLhu5kgVmFsaWRhdGlvbiBEYXRhIHbDoCBuaOG7r25nIHRow7RuZyB0aW4gbsOgeSBjw7MgdGjhu4MgxJHGsOG7o2Mga2hhaSB0aMOhYyB04burIGPDoWMgY2FyZXQgb2JqZWN0cyBuaMawIGNow7puZyB0YSDEkcOjIGJp4bq/dDogDQoNCg0KYGBge3J9DQojIEV4dHJhY3QgcmVzdWx0cyBmcm9tIGZ1bGxHcmlkX2JzdFRyZWU6IA0KZnVsbEdyaWRfYnN0VHJlZSRyZXN1bHRzICU+JSANCiAgc2xpY2Uod2hpY2gubWF4KFJzcXVhcmVkKSkgJT4lIA0KICBzZWxlY3QoLU1BRSwgLVJNU0VTRCwgLU1BRVNELCAtUk1TRSkgJT4lIA0KICBtdXRhdGUoQXBwcm9hY2ggPSAiRnVsbEdyaWRTZWFyY2giKSAtPiBkZl9mdWxsR3JpZA0KDQojIEV4dHJhY3QgcmVzdWx0cyBmcm9tIG5ld0dyaWRfYnN0VHJlZTogDQpuZXdHcmlkX2JzdFRyZWUkcmVzdWx0cyAlPiUgDQogIHNsaWNlKHdoaWNoLm1heChSc3F1YXJlZCkpICU+JSANCiAgc2VsZWN0KC1NQUUsIC1STVNFU0QsIC1NQUVTRCwgLVJNU0UpICU+JSANCiAgbXV0YXRlKEFwcHJvYWNoID0gIkZvY3VzT25fbXN0b3AiKSAtPiBkZl9iZXR0ZXJHcmlkDQoNCiMgIENvbWJpbmUgdGhlIHR3byBkYXRhIGZyYW1lcyBhbmQgc2hvdyByZXN1bHRzOiANCmJpbmRfcm93cyhkZl9mdWxsR3JpZCwgZGZfYmV0dGVyR3JpZCkNCmBgYA0KDQpHacOhIHRy4buLIHRydW5nIGLDrG5oIGPhu6dhIFJzcXVhcmVkIHThu6sgMjAgYuG7mSBWYWxpZGF0aW9uIERhdGEga2hpIHPhu60gZOG7pW5nIEJvb3N0ZWQgVHJlZSB24bubaSBi4buZIHRoYW0gc+G7kSBtc3RvcCA9IDMwMCwgbWF4ZGVwdGggPSAzIHbDoCBudSA9IDAuMDc1IGzDoCAwLjg1NTc2NTcuIEPDsm4gZ2nDoSB0cuG7iyB0cnVuZyBiw6xuaCBuw6B5IGPhu6dhIEJvb3N0ZWQgVHJlZSB24bubaSBi4buZIHRoYW0gc+G7kSBtc3RvcCA9IDIwMCwgbWF4ZGVwdGggPSAzIHbDoCBudSA9IDAuMDc1IGzDoCAwLjg1NTA5MTEuIE5naMSpYSBsw6AsIHZp4buHYyBz4butIGThu6VuZyBCb29zdGVkIFRyZWUgduG7m2kgYuG7mSB0aGFtIHPhu5EgbXN0b3AgPSAzMDAsIG1heGRlcHRoID0gMyB2w6AgbnUgPSAwLjA3NSB04bqhbyByYSBr4bq/dCBxdeG6oyBk4buxIGLDoW8gY2jDrW5oIHjDoWMgaMahbiBraMO0bmcgcGjhuqNpIGzDoCBuZ+G6q3Ugbmhpw6puLiBDxaluZyBj4bqnbiBwaOG6o2kgbsOzaSB0aMOqbSBy4bqxbmcgQm9vc3RlZCBUcmVlIHbhu5tpIGLhu5kgdGhhbSBz4buRIG1zdG9wID0gMzAwLCBtYXhkZXB0aCA9IDMgdsOgIG51ID0gMC4wNzUga2jDtG5nIG5o4buvbmcgZOG7sSBiw6FvIGNow61uaCB4w6FjIGjGoW4gbcOgIGvhur90IHF14bqjIGThu7EgYsOhbyBjw7JuIMOtdCBiaeG6v24gxJHhu5luZyBoxqFuLiBWw6wgxJHhu5kgbOG7h2NoIGNodeG6qW4gKGPhu5l0IFJzcXVhcmVkU0QpIHRo4bqlcCBoxqFuLiANCg0KQ8WpbmcgY8OzIHRo4buDIGtp4buDbSB0cmEgcuG6sW5nIG7hur91IHPhu60gZOG7pW5nIEJvb3N0ZWQgVHJlZSBt4bq3YyDEkeG7i25oIHRow6wgUnNxdWFyZWQgdHLDqm4gVGVzdCBEYXRhIGzDoCBjYW8gbmjhuqV0LCBsw6puIMSR4bq/biA4OCUgbmjGsCBjw7MgdGjhu4MgdGjhuqV5OiANCg0KYGBge3J9DQpwcmVkaWN0KGRlZmF1bHRfYnN0VHJlZSwgZmVhdHVyZXNfaW5wdXQpIC0+IHByZWRfYnlfZGVmYXVsdA0KDQpjYWxjdWxhdGVfUjIocHJlZF9ieV9kZWZhdWx0LCBhY3R1YWxfbWVkdikNCg0KYGBgDQoNCk5oxrBuZyBjw7MgdGjhu4MgdGjhuqV5IHLhurFuZyBr4bq/dCBxdeG6oyA4OCUgbsOgeSB0csOqbiBUZXN0IERhdGEga2jDtG5nIG1hbmcgdMOtbmggcXV5IGx14bqtdCB2w6wgZ2nDoSB0cuG7iyB0cnVuZyBiw6xuaCBj4bunYSBSc3F1YXJlZCB0csOqbiAyMCBi4buZIFZhbGlkYXRpb24gbMOgIDAuODUzNDk4MSAtIHRo4bqlcCBuaOG6pXQgdHJvbmcgYuG7mSBiYSBCb29zdGVkIFRyZWU6DQoNCmBgYHtyfQ0KZGVmYXVsdF9ic3RUcmVlJHJlc3VsdHMgJT4lIA0KICBzbGljZSh3aGljaC5tYXgoUnNxdWFyZWQpKSAlPiUgDQogIHNlbGVjdCgtTUFFLCAtUk1TRVNELCAtTUFFU0QsIC1STVNFKSAlPiUgDQogIG11dGF0ZShBcHByb2FjaCA9ICJEZWZhdWx0IikgDQpgYGANCg0KTmdvw6BpIHZp4buHYyBs4buxYSBjaOG7jW4gbeG7mXQgdGnDqnUgY2jDrSDEkeG7gyBodeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIHRodeG6rXQgdG/DoW4gTUwgY2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyBs4buxYSBjaOG7jW4gbeG7mXQgdGh14bqtdCB0b8Ohbi9jw6FjaCB0aeG6v3AgY+G6rW4gbMOgbSBjxqEgc+G7nyBzbyBzw6FuaCAoQmFzZSBMaW5lKS4gR2nhuqMgc+G7rSBjaMO6bmcgdGEgY2jhu41uIHRodeG6rXQgdG/DoW4gaOG7k2kgcXV5IHR1eeG6v24gdMOtbmggdHJ1eeG7gW4gdGjhu5FuZyBMaW5lYXIgUmVncmVzc2lvbiAoT0xTKSBj4bunYSB0aOG7kW5nIGvDqiB0cnV54buBbiB0aOG7kW5nIGzDoG0gYmFzZSBsaW5lLiBOaMawIMSRw6MgYmnhur90IGNow7puZyB0YSBjw7MgdGjhu4MgaHXhuqVuIGx1eeG7h24gTGluZWFyIFJlZ3Jlc3Npb24gYuG6sW5nIFIgY29kZXMgZMaw4bubaSDEkcOieTogIA0KDQpgYGB7cn0NCiMgVHJhaW4gTGluZWFyIFJlZ3Jlc3Npb24gLSBPTFM6IA0Kc2V0LnNlZWQoMjkpDQp0cmFpbihtZWR2IH4gLiwgDQogICAgICBkYXRhID0gZGF0YV90cmFpbl92YWxpZCwgDQogICAgICBtZXRob2QgPSAibG0iLCANCiAgICAgIG1ldHJpYyA9ICJSc3F1YXJlZCIsIA0KICAgICAgdHJDb250cm9sID0gbXlfc2FtcGxpbmdfZnVsbF9ncmlkKSAtPiBscl9tb2RlbA0KDQojIFNob3cgcmVzdWx0czogDQoNCmxyX21vZGVsDQpgYGANCg0KR2nDoSB0cuG7iyBiw6xuaCBxdcOibiBj4bunYSBSc3F1YXJlZCB0csOqbiAyMCBi4buZIFZhbGlkYXRpb24gRGF0YSBsw6AgNzMuNzclLiBU4bqldCBuaGnDqm4gY2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyBz4butIGThu6VuZyBjaG8gZOG7sSBiw6FvIG1lZHYgdsOgIHTDrW5oIFJzcXVhcmVkIHRyw6puIFRlc3QgRGF0YTogDQoNCmBgYHtyfQ0KIyBVc2UgTGluZWFyIFJlZ3Jlc3Npb24gZm9yIHByZWRpY3Rpbmc6IA0KcHJlZF9mcm9tT0xTIDwtIHByZWRpY3QobHJfbW9kZWwsIGZlYXR1cmVzX2lucHV0KQ0KDQojIENhbGN1bGF0ZSBSc3F1YXJlZDogDQpjYWxjdWxhdGVfUjIocHJlZF9mcm9tT0xTLCBhY3R1YWxfbWVkdikNCmBgYA0KDQpSc3F1YXJlZCBj4bunYSBMaW5lYXIgUmVncmVzc2lvbiB0csOqbiBUZXN0IERhdGEgY2jhu4kgbMOgIDY1LjYzJSAtIG3hu5l0IGvhur90IHF14bqjIHRo4bqlcCBoxqFuIMSRw6FuZyBr4buDIHNvIHbhu5tpIG5nYXkgY+G6oyBCb29zdGVkIFRyZWUgbeG6t2MgxJHhu4tuaC4gTmjGsCB24bqteSBjw7MgdGjhu4Mga+G6v3QgbHXhuq1uIGzDoCBnaeG7r2EgY8OhYyBtw7QgaMOsbmgvY8OhY2ggdGnhur9wIGPhuq1uIGPhuqFuaCB0cmFuaCBuaGF1IGNow7puZyB0YSB0aOG6pXkgcuG6sW5nIEJvb3N0ZWQgVHJlZSB24bubaSBi4buZIHRoYW0gc+G7kSBtc3RvcCA9IDMwMCwgbWF4ZGVwdGggPSAzIHbDoCBudSA9IDAuMDc1IG7Dqm4gxJHGsOG7o2Mgc+G7rSBk4bulbmcgY2hvIG3hu6VjIMSRw61jaCBk4buxIGLDoW8gbWVkdi4gDQoNCg0KIyBDbGFzc2lmaWNhdGlvbiBUYXNrOiBDcmVkaXQgQ2xhc3NpZmljYXRpb24gYW5kIFNjb3JpbmcgIA0KDQpO4bq/dSBUYWdyZXQgVmFyaWFibGUgbMOgIGJp4bq/biBraeG7g3Ugbmjhu4sgcGjDom4gKEJpbm9taWFsIERhdGEgaGF5IEJpbmFyeSBSZXNwb25zZSkgLSB04bupYyBsw6Aga2nhu4N1IGJp4bq/biBz4buRIMSR4buLbmggdMOtbmggbcOgIGdpw6EgdHLhu4sgY+G7p2EgYmnhur9uIGNo4buJIHRodeG7mWMgduG7gSBt4buZdCB0cm9uZyBoYWkgbG/huqFpIHbDoCBjaMO6bmcgdGEgY+G6p24gZOG7sSBiw6FvIG3hu5l0IHF1YW4gc8OhdCB0aHXhu5ljIGxv4bqhaSBuw6B5IGhheSBsb+G6oWkga2lhIHRow6wgY2jDum5nIHRhIMSRYW5nIGPDsyBiw6BpIHRvw6FuIHBow6JuIGxv4bqhaSAoQ2xhc3NpZmljYXRpb24gVGFzaykuIMSQ4buDIG1pbmggaOG7jWEgY2jDum5nIHRhIGzhuqV5IHbDrSBk4bulIGzDoCBi4buZIGThu68gbGnhu4d1ICoqR2VybWFuQ3JlZGl0KiogKMSRxrDhu6NjIGN1bmcgY+G6pXAgYuG7n2kgR1MgSGFucyBIb2ZtYW5uLCBVbml2ZXJzaXQiYXQgSGFtYnVyZykgY8OzIDEwMDAgcXVhbiBzw6F0IHbhu5tpIFRhcmdldCBWYXJpYWJsZSBsw6AgQ2xhc3MgY2jhu4kgbmjhuq1uIG3hu5l0IHRyb25nIGhhaSBnacOhIHRy4buLIGzDoCBCYWQgKHbhu5tpIMO9IG5naMSpYSBsw6Agc+G6vSBraMO0bmcgxJHGsOG7o2MgY+G6pXAgdMOtbiBk4bulbmcpIHbDoCBHb29kICh24bubaSDDvSBuZ2jEqWEgbMOgIHPhur0gxJHGsOG7o2MgY+G6pXAgdMOtbiBk4bulbmcpIHThuqFpIG3hu5l0IG5nw6JuIGjDoG5nIOG7nyDEkOG7qWMuIELhu5kgZOG7ryBsaeG7h3UgbsOgeSDEkcaw4bujYyBsxrB1IHRy4buvIGLhu59pIENlbnRlciBmb3IgTWFjaGluZSBMZWFybmluZyBhbmQgSW50ZWxsaWdlbnQgU3lzdGVtcyB0aHXhu5ljIFVuaXZlcnNpdHkgb2YgQ2FsaWZvcm5pYSBJcnZpbmUuIELhuqFuIMSR4buNYyBxdWFuIHTDom0gxJHhur9uIGLhu5kgZOG7ryBsaeG7h3UgZ+G7kWMgKGPFqW5nIG5oxrAgZG93bmxvYWQpICsgY8OhYyB0aMO0bmcgdGluIG3DtCB04bqjIHbhu4EgY8OhYyBmZWF0dXJlcyBjw7MgdGjhu4MgdMOsbSB0aOG6pXkgW+G7nyDEkcOieV0oaHR0cHM6Ly9hcmNoaXZlLmljcy51Y2kuZWR1L21sL2RhdGFzZXRzL3N0YXRsb2crKGdlcm1hbitjcmVkaXQrZGF0YSkpLiBUdXkgbmhpw6puIHRyb25nIG3hu6VjIG7DoHkgY2jDum5nIHRhIHPhur0gc+G7rSBk4bulbmcgbeG7mXQgcGhpw6puIGLhuqNuIGPhu6dhIGLhu5kgZOG7ryBsaeG7h3UgbsOgeSDEkcaw4bujYyB0w61jaCBo4bujcCBjw7luZyBnw7NpIGNhcmV0OiANCg0KYGBge3J9DQojIENsZWFyIG91ciBSIGVudmlyb25tZW50OiANCnJtKGxpc3QgPSBscygpKQ0KDQojIExvYWQgR2VybWFuQ3JlZGl0IGRhdGEgc2V0OiANCmRhdGEoIkdlcm1hbkNyZWRpdCIpDQoNCiMgU2hvdyBzb21lIGNvbHVtbnMgKyBvYnNlcnZhdGlvbnM6IA0KR2VybWFuQ3JlZGl0ICU+JSANCiAgc2VsZWN0KDg6MTEpICU+JSANCiAgaGVhZCgpDQpgYGANCg0KQuG7mSBk4buvIGxp4buHdSBuw6B5IMSRw6MgxJHGsOG7o2Mgc+G7rSBk4bulbmcgdHJvbmcgbmhp4buBdSBuZ2hpw6puIGPhu6l1IHbhu4EgTUwgw6FwIGThu6VuZyBjaG8gYsOgaSB0b8OhbiBwaMOibiBsb+G6oWkgdsOgIHjhur9wIGjhuqFuZyB0w61uIGThu6VuZyBi4bufaSBuaGnhu4F1IHTDoWMgZ2nhuqMga2jDoWMgbmhhdS4gTeG7qWMgxJHhu5kgY2jDrW5oIHjDoWMgKEFjY3VyYWN5KSDEkcaw4bujYyBs4buxYSBjaOG7jW4gbMOgbSB0acOqdSBjaHXhuqluIMSR4buDIHNvIHPDoW5oIC0gxJHDoW5oIGdpw6EgY8OhYyB0aHXhuq10IHRvw6FuIE1MIGtow6FjIG5oYXUgxJHGsOG7o2MgY2hvIOG7nyBi4bqjbmcgZMaw4bubaSDEkcOieTogDQoNCiFbXShDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvY3JlZGl0LnBuZykNCg0KUmFuZG9tIEZvcmVzdCAoUkYpIC0gbeG7mXQgdGh14bqtdCB0b8OhbiB0aHXhu5ljIG5ow7NtIEVuc2VtYmxlIGzDoCBt4buZdCB0aHXhuq10IHRvw6FuIE1MIG3huqFuaCBjaG8gY+G6oyBiw6BpIHRvw6FuIGjhu5NpIHF1eSBs4bqrbiBwaMOibiBsb+G6oWkuIENow7puZyB0YSBz4bq9IHPhu60gZOG7pW5nIHRodeG6rXQgdG/DoW4gbsOgeSBjaG8gbeG7pWMgxJHDrWNoIHBow6JuIGxv4bqhaSB2w6AgeOG6v3AgaOG6oW5nIHTDrW4gZOG7pW5nLiBUcsaw4bubYyBo4bq/dCBjaMO6bmcgdGEgY+G6p24gUkYgY8OzIG5o4buvbmcgdGhhbSBz4buRIG7DoG8gY8OzIHRo4buDIHRpbmggY2jhu4luaCBi4bufaSBjYXJldDogDQoNCmBgYHtyfQ0KIyBFeHRyYWN0IGRlc2NyaXB0aW9uIGFib3V0IFJhbmRvbSBGb3Jlc3Q6IA0KZ2V0TW9kZWxJbmZvKCJyZiIpIC0+IHJmX2luZm8NCg0KIyBQYXJhbWV0ZXJzIGNhbiBiZSB0dXJuZWQgYnkgdGhlIGNhcmV0IHBhY2thZ2U6IA0KcmZfaW5mbyRyZiRwYXJhbWV0ZXJzDQoNCmBgYA0KDQpOaMawIHbhuq15ICpSYW5kb21seSBTZWxlY3RlZCBQcmVkaWN0b3JzKiAtIGvDrSBoaeG7h3UgbXRyeSBsw6AgdGhhbSBz4buRIGPDsyB0aOG7gyDEkcaw4bujYyB0aW5oIGNo4buJbmguIE5oxrAgxJHDoyBuw7NpLCBjaMO6bmcgdGEgdOG6oW0gYuG7jyBxdWEgc3RhZ2UgMSB2w6AgMyB2w6AgdGjhu7FjIGhp4buHbiBsdcO0biBzdGFnZSAzIGzDoCBwaMOibiBjaGlhIGLhu5kgZOG7ryBsaeG7h3UgdGjDoG5oIGhhaSBwaOG6p24gdGhlbyB04buJIGzhu4cgODAgLSAyMDogDQoNCmBgYHtyfQ0KIz09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBTdGFnZSAzOiBTcGxpdGluZyBEYXRhDQojPT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0Kc2V0LnNlZWQoMjkpDQppZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBHZXJtYW5DcmVkaXQkQ2xhc3MsIHAgPSAwLjgsIGxpc3QgPSBGQUxTRSwgdGltZXMgPSAxKQ0KDQojIFRyYWluaW5nICsgdmFsaWRhdGlvbiBkYXRhOiANCnRyYWluX3ZhbGlkQ3JlZGl0RGF0YSA8LSBHZXJtYW5DcmVkaXQgJT4lIHNsaWNlKGlkKQ0KDQojIEhvbGRvdXQgZGF0YTogDQp0ZXN0Q3JlZGl0RGF0YSA8LSBHZXJtYW5DcmVkaXQgJT4lIHNsaWNlKC1pZCkNCmBgYA0KDQpExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzIGNobyBjw6FjIGLGsOG7m2MgY8OybiBs4bqhaSBj4bunYSBxdcOhIHRyw6xuaCB0aW5oIGNo4buJbmggLSBodeG6pW4gbHV54buHbiBSRiBt4bq3YyDEkeG7i25oOiANCg0KDQpgYGB7cn0NCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBTdGFnZSA0OiBTZWxlY3Qgc2FtcGxpbmcgbWV0aG9kIGZvciB0cmFpbmluZyBhbmQgdHVybmluZyBoeXBlci1wYXJhbWV0ZXJzDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KDQpzYW1wbGluZ19zdGF0dXMgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gNSkNCg0KDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgIFN0YWdlIDU6IFRyYWluIGFuZCB0dXJuIEJvb3N0ZWQgVHJlZSAodXNpbmcgZGVmYXVsdCkNCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KDQojIFRyYWluIFJhbmRvbSBGb3Jlc3Qgd2l0aCBkZWZhdWx0IHN0YXR1czogDQoNCnNldC5zZWVkKDI5KQ0KDQp0cmFpbihDbGFzcyB+IC4sIA0KICAgICAgZGF0YSA9IHRyYWluX3ZhbGlkQ3JlZGl0RGF0YSwgDQogICAgICBtZXRob2QgPSAicmYiLCANCiAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsIA0KICAgICAgdHJDb250cm9sID0gc2FtcGxpbmdfc3RhdHVzKSAtPiBkZWZhdWx0X3JmDQoNCiMgU2hvdyByZXN1bHRzOiANCg0KZGVmYXVsdF9yZg0KYGBgDQoNCk5oxrAgduG6rXkga2hpIHRpbmggY2jhu4luaCB0aGFtIHPhu5Eg4bufIGNo4bq/IMSR4buZIG3hurdjIMSR4buLbmggdGjDrCBnacOhIHRy4buLIHThu5FpIMawdSBj4bunYSB0aGFtIHPhu5EgbMOgIG10cnkgPSAzMSB2w6AgdHJ1bmcgYsOsbmggQWNjdXJhY3kgdHLDqm4gMjAgYuG7mSBWYWxpZGF0aW9uIGRhdGEgbMOgIDc0Ljk1JSAtIG3hu5l0IGvhur90IHF14bqjIGPFqW5nIGtow7RuZyBxdcOhIGvDqW0gc28gduG7m2kga+G6v3QgcXXhuqMgY+G7p2Egbmjhu69uZyB0w6FjIGdp4bqjIMSRaSB0csaw4bubYyDEkcaw4bujYyBsaXN0IHRyb25nIGLhuqNuZyDhu58gdHLDqm4uIE5oxrBuZyBj4bqnbiBsxrB1IMO9IHLhurFuZyBBY2N1cmFjeSBwaOG7pSB0aHXhu5ljIHbDoG8gbmfGsOG7oW5nIHjDoWMgc3XhuqV0IChUaHJlc2hvbGQpIMSR4buDIHBow6JuIGxv4bqhaS4gS+G6v3QgcXXhuqMgNzQuOTUlIGzDoCB0xrDGoW5nIOG7qW5nIHbhu5tpIG5nxrDhu6FuZyBt4bq3YyDEkeG7i25oIDAuNS4gQ2jDum5nIHRhIHPhur0gdMOsbSBoaeG7g3Uga8SpIHRow6ptIHbhu4Eg4bqjbmggaMaw4bufbmcgY+G7p2EgbmfGsOG7oW5nIHBow6JuIGxv4bqhaSBsw6puIEFjY3VyYWN5IG5nYXkgc2F1IMSRw6J5LiBUcsaw4bubYyBo4bq/dCBz4butIGThu6VuZyB0aHXhuq10IHRvw6FuIMSRw6MgxJHGsOG7o2MgdGluaCBjaOG7iW5oIC0gaHXhuqVuIGx1eeG7h24gxJHhu4MgdMOtbmggeMOhYyBzdeG6pXQgduG7oSBu4bujIFBGIChQcm9iYWJpbGl0eSBvZiBEZWZhdWx0KSB24bubaSBi4buZIGThu68gbGnhu4d1IHRlc3Q6IA0KDQpgYGB7cn0NCiMgQWN0dWFsIGxhYmVscyBmcm9tIHRlc3QgZGF0YTogDQphY3R1YWxfbGFiZWxzIDwtIHRlc3RDcmVkaXREYXRhICU+JSBwdWxsKENsYXNzKQ0KDQojIElucHV0czogDQppbnB1dHMgPC0gdGVzdENyZWRpdERhdGEgJT4lIHNlbGVjdCgtQ2xhc3MpDQoNCiMgQ2FsY3VsYXRlIFBEIG9uIHRlc3QgZGF0YTogDQoNCnByZWRpY3QoZGVmYXVsdF9yZiwgaW5wdXRzLCB0eXBlID0gInByb2IiKSAtPiBkZl9QRF9wcmVkaWN0ZWQNCg0KIyBWZWN0b3Igb2YgUEQgcHJlZGljdGVkIGJ5IFJhbmRvbSBGb3Jlc3Q6IA0KcGRfcHJlZGljdGVkIDwtIGRmX1BEX3ByZWRpY3RlZCAlPiUgcHVsbChCYWQpDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyB4ZW0gcXVhIG3hu5l0IHPhu5Ega+G6v3QgcXXhuqM6IA0KDQpgYGB7cn0NCiMgU2hvdyBzb21lIHByb2JhYmlsaXR5IG9mIGRlZmF1bHQgZm9yIHNvbWUgY2FzZXM6IA0KaGVhZChkZl9QRF9wcmVkaWN0ZWQpDQpgYGANCg0KS+G6v3QgceG7p2EgbsOgeSBjw7MgbmdoxKlhIGzDoCB24bubaSBjYXNlIMSR4bqndSB0acOqbiB0aMOsIHjDoWMgc3XhuqV0IGThu7EgYsOhbyBjaG8gc+G7sSBraeG7h24gKiJCYWQiKiBsw6AgMC42MzYgdsOgICpHb29kKiBsw6AgMC4zNjQuIENow7ogw70gcuG6sW5nIHThu5VuZyBj4bunYSBoYWkgY29uIHPhu5EgbHXDtG4gbMOgIDEgbmjGsG5nIGNow7puZyB0YSDEkcOjIGJp4bq/dCB04burIGPDoWMgZ2nDoW8gdHLDrG5oIHjDoWMgc3XhuqV0IHRo4buRbmcga8OqLiBE4buxYSB0csOqbiBQRCBk4buxIGLDoW8gYuG7n2kgUkYgY2jDum5nIHRhIGPDsyB0aOG7gywgdsOtIGThu6UsIHBow6JuIGxv4bqhaSBjw6FjIGjhu5Mgc8ahIHhpbiBj4bqlcCB0w61uIGThu6VuZyB24bubaSBuZ8aw4buhbmcgxJHGsOG7o2MgY2jhu41uIGNobyBwaMOibiBsb+G6oWkgbMOgIDAuNSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBMYWJlbCBmb3IgY2FzZXMgZnJvbSBQRCBwcmVkaWN0ZWQ6IA0KbGFiZWxzX3doZW5fMC41IDwtIGNhc2Vfd2hlbihwZF9wcmVkaWN0ZWQgPj0gMC41IH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpDQoNCiMgQ29tcGFyZSB3aXRoIGFjdHVhbCBsYWJlbHM6IA0KDQp0aWJibGUoYWN0dWFsID0gYWN0dWFsX2xhYmVscywgcHJlZGljdGVkID0gbGFiZWxzX3doZW5fMC41LCBQRCA9IHBkX3ByZWRpY3RlZCkgLT4gZGZfbGFiZWxzXzAuNQ0KDQpoZWFkKGRmX2xhYmVsc18wLjUpDQoNCmBgYA0KDQoNCk5oxrAgduG6rXkgduG7m2kgbmfGsOG7oW5nIHjDoWMgc3XhuqV0IMSRxrDhu6NjIGNo4buNbiBjaG8gcGjDom4gbG/huqFpIGzDoCAwLjUgdGjDrCBjYXNlIMSR4bqndSB0acOqbiBjw7MgUEQgPSAwLjYzNiBz4bq9IMSRxrDhu6NjIGTDoW4gbmjDo24gbMOgIEJhZCBjw7JuIGNhc2UgdGjhu6kgaGFpIHPhur0gY8OzIG5ow6NuIGzDoCBHb29kLiBUxrDGoW5nIHThu7EgY2jDum5nIHRhIGPDsyB0aOG7gyB4ZW0ga+G6v3QgcXXhuqMgcGjDom4gbG/huqFpIG7hur91IG5nxrDhu6FuZyDEkcaw4bujYyBjaOG7jW4gbMOgLCB2w60gZOG7pSwgMC4zNSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KbGFiZWxzX3doZW5fMC4zNSA8LSBjYXNlX3doZW4ocGRfcHJlZGljdGVkID49IDAuMzUgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikNCg0KdGliYmxlKGFjdHVhbCA9IGFjdHVhbF9sYWJlbHMsIHByZWRpY3RlZCA9IGxhYmVsc193aGVuXzAuMzUsIFBEID0gcGRfcHJlZGljdGVkKSAtPiBkZl9sYWJlbHNfMC4zNQ0KDQpoZWFkKGRmX2xhYmVsc18wLjM1KQ0KYGBgDQoNCg0KQ2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyB0w61uaCBBY2N1cmFjeSB0xrDGoW5nIOG7qW5nIHbhu5tpIGhhaSBuZ8aw4buhbmcgxJHGsOG7o2MgY2jhu41uIGNobyBwaMOibiBsb+G6oWkgbsOgeTogDQoNCmBgYHtyfQ0KIyBBY2N1cmFyeSB3aGVuIHRocmVzaG9sZCA9IDAuNTogDQpzdW0oZGZfbGFiZWxzXzAuNSRhY3R1YWwgPT0gZGZfbGFiZWxzXzAuNSRwcmVkaWN0ZWQpIC8gbnJvdyhkZl9sYWJlbHNfMC41KQ0KDQojIEFjY3VyYWN5IHdoZW4gdGhyZXNob2xkID0gMC4zNTogDQpzdW0oZGZfbGFiZWxzXzAuMzUkYWN0dWFsID09IGRmX2xhYmVsc18wLjM1JHByZWRpY3RlZCkgLyBucm93KGRmX2xhYmVsc18wLjM1KQ0KYGBgDQoNCk5oxrAgduG6rXkgdOG6oWkgY8OhYyBuZ8aw4buhbmcga2jDoWMgbmhhdSB0aMOsIEFjY3VyYWN5IHPhur0ga2jDoWMgbmhhdS4gVGnDqnUgY2jDrSBBY2N1cmFjeSBuw6B5IGPDuW5nIHbhu5tpIGPDoWMgbWV0cmljcyBraMOhYyBjxaluZyBjw7JuIMSRxrDhu6NjIGLDoW8gY8OhbyB0cm9uZyBNYSBUcuG6rW4gTmjhuqdtIEzhuqtuIChrw60gaGnhu4d1IENNKSBi4bqxbmcgY8OhY2ggc+G7rSBk4bulbmcgaMOgbSBgY29uZnVzaW9uTWF0cml4KClgIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIENvbnZlcnQgdG8gZmFjdG9yOiANCmxhYmVsc193aGVuXzAuNSA8LSBmYWN0b3IobGFiZWxzX3doZW5fMC41KQ0KDQojIENNIHdoZW4gdGhyZXNob2xkID0gMC41OiANCmNvbmZ1c2lvbk1hdHJpeChsYWJlbHNfd2hlbl8wLjUsIGFjdHVhbF9sYWJlbHMsIHBvc2l0aXZlID0gIkJhZCIpDQpgYGANCg0KVMawxqFuZyB04buxIGzDoCBDTSBraGkgbmfGsOG7oW5nIMSRxrDhu6NjIGNo4buNbiBjaG8gcGjDom4gbG/huqFpIGzDoCAwLjM1OiANCg0KYGBge3J9DQojIENvbnZlcnQgdG8gZmFjdG9yOiANCmxhYmVsc193aGVuXzAuMzUgPC0gZmFjdG9yKGxhYmVsc193aGVuXzAuMzUpDQoNCiMgQ00gd2hlbiB0aHJlc2hvbGQgPSAwLjU6IA0KY29uZnVzaW9uTWF0cml4KGxhYmVsc193aGVuXzAuMzUsIGFjdHVhbF9sYWJlbHMsIHBvc2l0aXZlID0gIkJhZCIpDQpgYGANCg0KQ2jhu41uIG5nxrDhu6FuZyBwaMOibiBsb+G6oWkgbMOgIDAuMzUgIHRow6wgUkYgcGjDom4gbG/huqFpIMSRw7puZyA0MyBjYXNlcyBsw6AgQmFkICh0cm9uZyB04buVbmcgc+G7kSA2MCBjYXNlcyBsw6AgQmFkIMSRxrDhu6NjIGtp4buDbSB0cmEpLiBTbyB24bubaSAyOCBCYWQgxJHGsOG7o2MgcGjDom4gbG/huqFpIMSRw7puZyAoa2hpIFRocmVzaG9sZCA9IDAuNSkgdGjDrCBz4buRIGNhc2VzIMSRxrDhu6NjIHBow6JuIGxv4bqhaSDEkcO6bmcgbMOgIHTEg25nIGjGoW4gNTMlLiANCg0KTmdvw6BpIEFjY3VyYWN5LCDEkeG7gyDEkeG7jWMgaGnhu4N1IG3hu5l0IHPhu5Ega+G6v3QgcXXhuqMgcXVhbiB0cuG7jW5nIGtow6FjIHThu6sgQ00gY2jDum5nIHRhIGPhuqduIGhp4buDdSBt4buZdCBz4buRIHRpw6p1IGNow60gxJHDoW5oIGdpw6EgLSBzbyBzw6FuaCBjaOG6pXQgbMaw4bujbmcva2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGPhu6dhIHRodeG6rXQgdG/DoW4gTUwgZMaw4bubaSDEkcOieTogDQoNCi0gKipExrDGoW5nIFTDrW5oIEdp4bqjIChGYWxzZSBQb3NpdGl2ZSwgRlApKiouIMSQw6J5IGzDoCBuaOG7r25nIGNhc2VzIGzDoCBHb29kIG5oxrBuZyBi4buLIGTDoW4gbmjDo24gc2FpIHRow6BuaCBCYWQuIELhuqFuIGtow7RuZyBi4buLIG5oaeG7hW0gQ292aWQgMTkgbmjGsG5nIGLDoWMgc8SpIGvhur90IGx14bqtbiBi4bqhbiBi4buLIG5oaeG7hW0gbG/huqFpIHZpcnVzIG7DoHkgYuG6sW5nIGvhur90IGx14bqtbiBjaMOtbmggdGjhu6ljIGzDoCBi4bqhbiAiY8OzIGvhur90IHF14bqjIGTGsMahbmcgdMOtbmgiLiBExrDGoW5nIHTDrW5oIGdp4bqjIGzDoCBuaMawIHbhuq15IGNobyBk4buFIG5o4bubLiBUcm9uZyB0w6xuaCBodeG7kW5nIGPhu6dhIGNow7puZyB0YSB0aMOsIEZOID0gMTQuIFThu6ljIGzDoCBjw7MgMTQgY2FzZXMgdGjhu7FjIHThur8gbMOgIEdvb2QgbmjGsG5nIGLhu4sgeOG6v3AgaOG6oW5nIG5o4bqnbSB0aMOgbmggQmFkLiANCg0KLSAqKsOCbSB0w61uaCBnaeG6oyAoRmFsc2UgTmVnYXRpdmUsIEZOKSoqLiDEkMOieSBsw6Agbmjhu69uZyBjYXNlcyBsw6AgQmFkIG5oxrBuZyBi4buLIGTDoW4gbmjDo24gc2FpIHRow6BuaCBHb29kLiBUcm9uZyB0w6xuaCBodeG7kW5nIGPhu6dhIGNow7puZyB0YSB0aMOsIEZOID0gMzIuIEPhuqMgRlAgdsOgIEZOIMSR4buBdSBsw6AgcGjDom4gbG/huqFpIHNhaSBuaMawbmcgaOG6rXUgcXXhuqMgY+G7p2EgRk4gbMOgIG5naGnDqm0gdHLhu41uZyBoxqFuIEZQIHLhuqV0IG5oaeG7gXUuIELhuqFuIGLhu4sgbmhp4buFbSBDb3ZpZCAxOSBuaMawbmcgYsOhYyBzxKkgbOG6oWkgcGjDom4gbG/huqFpIGLhuqFuIGto4buPZSBt4bqhbmgsIGtow7RuZyBi4buLIG5oaeG7hW0gQ292aWQgMTkgdsOgIHPhur0gbOG6oWkgdGnhur9wIHThu6VjIGzDonkgbmhp4buFbSB0aMOqbSBjaG8gcuG6pXQgbmhp4buBdSBuZ8aw4budaS4gVuG7m2kgY8OhYyB04buVIGNo4bupYyB0w6BpIGNow61uaCAtIG5nw6JuIGjDoG5nIHRow6wgdmnhu4djIHBow6JuIGxv4bqhaSBzYWkgbeG7mXQgY2FzZSB0aOG7sWMgc+G7sSBsw6AgR29vZCB0aMOgbmggQmFkIGNo4buJIGzDoG0gbeG6pXQgxJFpIGPhu6dhIGjhu40gY8ahIGjhu5lpIGtp4bq/bSBsw6NpIHRyw6puIHPhu5EgduG7kW4gY2hvIHZheS4gTmjGsG5nIG7hur91IG3hu5l0IGNhc2UgbMOgIEJhZCBtw6AgYuG7iyBwaMOibiBsb+G6oWkgc2FpIHRow6BuaCBHb29kIHRow6wgdOG7lSBjaOG7qWMgZ+G6p24gbmjGsCBt4bqldCBob8OgbiB0b8OgbiBz4buRIHRp4buBbiDEkcOjIGNobyB2YXkuIA0KDQotICoqQWNjdXJhY3kqKi4gTMOgIHThu5VuZyBz4buRIGNhc2VzIMSRxrDhu6NjIHBow6JuIGxv4bqhaSDEkcO6bmcgKGJhbyBn4buTbSBj4bqjIG5ow6NuIEJhZCB2w6AgR29vZCkgY2hpYSBjaG8gdOG7lW5nIHPhu5EgY2FzZXMgxJHGsOG7o2MgcGjDom4gbG/huqFpL3jhur9wIGjhuqFuZy4gVHJvbmcgdMOsbmggaHXhu5FuZyBj4bunYSBjaMO6bmcgdGEgdGjDrCBSRiBwaMOibiBsb+G6oWkgxJHDum5nIDI4IGNhc2VzIGPDsyBuaMOjbiBCYWQgdsOgIDEyNiBjYXNlcyBjw7MgbmjDo24gR29vZC4gVHJvbmcga2hpIMSRw7MgdOG7lW5nIHPhu5EgY2FzZXMgxJHGsOG7o2MgeOG6v3AgaOG6oW5nIGzDoCAyMDAuIERvIHbhuq15IG3DoCBBY2N1cmFjeSA9ICgyOCArIDEyNikgLyAyMDAgPSAwLjc3IG3DoCBjaMO6bmcgdGEgxJHDoyBiaeG6v3QuIA0KDQotICoqU2Vuc2l0aXZpdHkqKi4gTMOgIHThu4kgbOG7hyBjaMOtbmggeMOhYyBraGkgcGjDom4gbG/huqFpIG5ow6NuIEJhZC4gUkYgKHbhu5tpIG5nxrDhu6FuZyAwLjUpIHjDoWMgxJHhu4tuaCBjaMOtbmggeMOhYyAyOCBjYXNlcyBsw6AgQmFkLiBUcm9uZyBraGkgdGjhu7FjIHThur8gY8OzIDI4ICsgMzIgPSA2MCBjYXNlcyBsw6AgQmFkLiBEbyB24bqteSBTZW5zaXRpdml0eSA9IDI4IC8gNjAgPSAwLjQ2NjcuIENvbiBz4buRIG7DoHkgY8OzIHRo4buDIHRo4bqleSBuZ2F5IHRyw6puIENNLiANCg0KLSAqKlNwZWNpZmljaXR5KiouIEzDoCB04buJIGzhu4cgY2jDrW5oIHjDoWMga2hpIHBow6JuIGxv4bqhaSBuaMOjbiBHb29kLiBSRiAoduG7m2kgbmfGsOG7oW5nIDAuNSkgeMOhYyDEkeG7i25oIGNow61uaCB4w6FjIDEyNiBjYXNlcyBsw6AgR29vZCB0cm9uZyB04buVbmcgc+G7kSAxNDAuIERvIHbhuq15IFNwZWNpZmljaXR5ID0gMTI2IC8gMTQwID0gMC45LiBOaMawIHbhuq15IHRow6wgdGh14bqtdCB0b8OhbiBSRiBj4bunYSBjaMO6bmcgdGEgduG7m2kgVGhyZXNob2xkID0gMC41IHRow6wgbcO0IGjDrG5oIGPDsyBraOG6oyBuxINuZyBwaMOibiBsb+G6oWkgaOG7kyBzxqEgY8OzIG5ow6NuIEdvb2QgduG7m2kgxJHhu5kgY2jDrW5oIHjDoWMgcuG6pXQgY2FvIG5oxrBuZyBs4bqhaSBraMOhIGvDqW0ga2hpIHBow6JuIGxv4bqhaSBo4buTIHPGoSBjw7MgbmjDo24gQmFkLiBW4bubaSBt4buZdCB04buVIGNo4bupYyBob+G6oXQgxJHhu5luZyB2w6wgbOG7o2kgbmh14bqtbiB2w6AgZSBuZ+G6oWkgcuG7p2kgcm8gbmjGsCBuZ8OibiBow6BuZyB0aMOsIHLDtSByw6BuZyBuw7MgcXVhbiB0w6JtIGjGoW4gxJHhur9uIHZp4buHYyBs4buxYSBjaOG7jW4gbeG7mXQgbcO0IGjDrG5oIGPDsyBuxINuZyBs4buxYyDEkeG7pyB04buRdCBraGkgcGjDom4gbG/huqFpIG5ow6NuIEJhZCAtIHThu6ljIFNlbnNpdGl2aXR5LiANCg0KTmdvw6BpIDUgbWV0cmljcyB0aMaw4budbmcgxJHGsOG7o2Mgc+G7rSBk4bulbmcga2hpIMSRw6FuaCBnacOhIG3DtCBow6xuaCBwaMOibiBsb+G6oWkgbmjGsCB0csOqbiB0aMOsIGPFqW5nIGPDsm4gbmhp4buBdSB0acOqdSBjaMOtIGtow6FjIMSRxrDhu6NjIG3DtCB04bqjIGNoaSB0aeG6v3QgW3ThuqFpIMSRw6J5XShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9TZW5zaXRpdml0eV9hbmRfc3BlY2lmaWNpdHkpLiBN4buZdCBs4bqnbiBu4buvYSBuaOG6r2MgbOG6oWkgcuG6sW5nIGPhuqMgNSB0acOqdSBjaMOtIG7DoHksIG7hur91IGPDoWMgdGjhu6kga2jDoWMga2jDtG5nIMSR4buVaSwgdGjDrCDEkeG7gXUgcGjhu6UgdGh14buZYyB2w6BvIG5nxrDhu6FuZyDEkcaw4bujYyBs4buxYSBjaOG7jW4ga2hpIHBow6JuIGxv4bqhaSAtIGTDoW4gbmjDo24uIA0KDQrEkOG7gyBraOG6o28gc8OhdCBz4buxIHBo4bulIHRodeG7mWMgY+G7p2EgQWNjdXJhY3ksIFNlbnNpdGl2aXR5IHbDoCBTcGVjaWZpY2l0eSBjaOG6s25nIGjhuqFuIHbDoG8gbmfGsOG7oW5nIHjDoWMgc3XhuqV0IMSRxrDhu6NjIGNo4buNbiBjaMO6bmcgdGEgdmnhur90IGjDoG0gY8OzIHTDqm4gKiptZXRyaWNzX3dpdGhfdGhyZXNob2xkKiogdMOtbmggdG/DoW4gYmEgbWV0cmljcyBuw6B5IGtoaSBiaeG6v3QgVGhyZXNob2xkIMSRxrDhu6NjIGNo4buNbjogDQoNCmBgYHtyfQ0KDQptZXRyaWNzX3dpdGhfdGhyZXNob2xkIDwtIGZ1bmN0aW9uKHRocmVzaG9sZCkgew0KICANCiAgIyBMYWJlbCBmb3IgY2FzZXMgZnJvbSBQRDogDQogIGNhc2Vfd2hlbihwZF9wcmVkaWN0ZWQgPj0gdGhyZXNob2xkIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpIC0+IGxhYmVsc19wcmVkaWN0ZWQNCiAgDQogICMgQ3JlYXRlIGFjdHVhbCAtIHByZWRpY3RlZCBkYXRhIGZyYW1lIGZvciBwdXJwb3NlIG9mIGNvbXBhcmlzaW9uOiANCiAgdGliYmxlKGFjdHVhbCA9IGFjdHVhbF9sYWJlbHMsIHByZWRpY3RlZCA9IGxhYmVsc19wcmVkaWN0ZWQpIC0+IGRmX2NvbXBhcmVkDQogIA0KICAjIENhbGN1bGF0ZSBBY2N1cmFjeSBtZXRyaWM6IA0KICBhY2NfbWV0cmljIDwtIHN1bShsYWJlbHNfcHJlZGljdGVkID09IGFjdHVhbF9sYWJlbHMpIC8gbGVuZ3RoKGxhYmVsc19wcmVkaWN0ZWQpDQogIA0KICAjIENhbGN1bGF0ZSBTZW5zaXRpdml5IG1ldHJpYzogDQogIGRmX2NvbXBhcmVkICU+JSBmaWx0ZXIoYWN0dWFsID09ICJCYWQiKSAtPiBkZl9iYWRfc2VuDQogIA0KICBkZl9iYWRfc2VuICU+JSANCiAgICBmaWx0ZXIocHJlZGljdGVkID09ICJCYWQiKSAlPiUgDQogICAgbnJvdygpIC8gbnJvdyhkZl9iYWRfc2VuKSAtPiBzZW5fbWV0cmljDQogIA0KICAjIENhbGN1bGF0ZSBTcGVjaWZpY2l0eSBtZXRyaWM6IA0KICANCiAgZGZfY29tcGFyZWQgJT4lIGZpbHRlcihhY3R1YWwgPT0gIkdvb2QiKSAtPiBkZl9nb29kX3NwZWMNCiAgDQogIGRmX2dvb2Rfc3BlYyAlPiUgDQogICAgZmlsdGVyKHByZWRpY3RlZCA9PSAiR29vZCIpICU+JSANCiAgICBucm93KCkgLyBucm93KGRmX2dvb2Rfc3BlYykgLT4gc3BlY19tZXRyaWMNCiAgDQogICMgRmluYWwgcmVzdWx0cyBpbiBERiBmb3JtOiANCiAgDQogIHRpYmJsZShBY2N1cmFjeSA9IGFjY19tZXRyaWMsIA0KICAgICAgICAgU2Vuc2l0aXZpeSA9IHNlbl9tZXRyaWMsDQogICAgICAgICBTcGVjaWZpY2l0eSA9IHNwZWNfbWV0cmljLCANCiAgICAgICAgIFRocmVzaG9sZCA9IHRocmVzaG9sZCkgLT4gZGZfcmVwb3J0DQogIA0KICAjIFJldHVybiBmaW5hbCBvdXRwdXRzOiANCiAgDQogIHJldHVybihkZl9yZXBvcnQpDQp9DQoNCmBgYA0KDQrEkMawxqFuZyBuaGnDqm4gbuG6v3UgVGhyZXNob2xkID0gMC41IHRow6wgY2jDum5nIHRhIHPhur0gY8OzIG5o4buvbmcga+G6v3QgcXXhuqMgxJHDoyBiaeG6v3QgKEFjY3VyYWN5LCBTZW5zaXRpdml0eSwgU3BlY2lmaWNpdHkpIG5oxrAgc2F1OiANCg0KYGBge3J9DQptZXRyaWNzX3dpdGhfdGhyZXNob2xkKHRocmVzaG9sZCA9IDAuNSkNCmBgYA0KDQpDaMO6bmcgdGEgc+G6vSBraOG6o28gc8OhdCBz4buxIHBo4bulIHRodeG7mWMgY+G7p2EgYmEgdGnDqnUgY2jDrSBuw6B5IHbDoG8gVGhyZXNob2xkIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIENyZWF0ZSBzb21lIHRocmVzaG9sZHM6IA0Kc29tZV90aHJlc2hvbGRzIDwtIHNlcSgwLjEsIDAuOSwgYnkgPSAwLjA1KQ0KDQojIENsYXNzaWZpY2F0aW9uIG1ldHJpY3MgYnkgdGhyZXNob2xkOiANCmxhcHBseShzb21lX3RocmVzaG9sZHMsIG1ldHJpY3Nfd2l0aF90aHJlc2hvbGQpIC0+IGxpc3Rfb2ZfZGYNCg0KIyBDb252ZXJ0IHRvIERGIGZyb20gbGlzdDogDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCBsaXN0X29mX2RmKSAtPiBkZl9yZXN1bHRzDQoNCiMgVGhyZXNob2xkIHRoYXQgbWF4aW1pemVzIEFjY3VyYWN5OiANCmRmX3Jlc3VsdHMgJT4lIHNsaWNlKHdoaWNoLm1heChBY2N1cmFjeSkpIC0+IG1heF9hY2MNCg0KIyBUaHJlc2hvbGQgdGhhdCBtYXhpbWl6ZXMgU2Vuc2l0aXZpdHk6IA0KZGZfcmVzdWx0cyAlPiUgc2xpY2Uod2hpY2gubWF4KFNlbnNpdGl2aXkpKSAtPiBtYXhfc2VuDQoNCiMgVGhyZXNob2xkIHRoYXQgbWF4aW1pemVzIFNwZWNpZmljaXR5OiANCmRmX3Jlc3VsdHMgJT4lIHNsaWNlKHdoaWNoLm1heChTcGVjaWZpY2l0eSkpIC0+IG1heF9zcGVjDQpgYGANCg0KTuG6v3UgbOG7sWEgY2jhu41uIEFjY3VyYWN5IGzDoG0gdGnDqnUgY2jDrSDEkcOhbmggZ2nDoSB2w6Agc28gc8OhbmggY8OhYyB0aHXhuq10IHRvw6FuIE1MIGtow6FjIG5oYXUgdGjDrCBjaMO6bmcgdGEgY8OzIHRo4buDIGNo4buJIHJhIGtoaSBUaHJlc2hvbGQgPSAwLjQ1IHRow6wgQWNjdXJhY3kgbMOgIGzhu5tuIG5o4bqldDogDQoNCmBgYHtyfQ0KbWF4X2FjYw0KYGBgDQoNCkvhur90IHF14bqjIDc4LjUlIG7DoHksIHNvIHbhu5tpIGPDoWMgdMOhYyBnaeG6oyDEkWkgdHLGsOG7m2Mg4bufIHRyw6puLCBjw7MgdGjhu4MgxJHGsOG7o2MgY29pIGzDoCBjYW8gaMahbiBzbyB24bubaSBtw7QgaMOsbmggcGjDom4gbG/huqFpIGPhu6dhIFNldGlvbm8sIEJhZXNlbnMgdsOgIE11ZXMgKDIwMTEpIHbhu5tpIEFjY3VyYWN5ID0gNzguNDclLiBDaMO6bmcgdGEgY8OzIHRo4buDIGto4bqjbyBzw6F0IHPhu7EgcGjhu6UgdGh14buZYyBj4bunYSBiYSBtZXRyaWNzIG7DoHkga2hpIG5nxrDhu6FuZyBwaMOibiBsb+G6oWkgxJHGsOG7o2MgY2jhu41uIHRoYXkgxJHhu5VpIOG7nyBGaWd1cmUgNiBkxrDhu5tpIMSRw6J5OiANCg0KYGBge3J9DQojIENvbnZlcnQgdG8gbG9uZyBmb3JtOiANCg0KZGZfcmVzdWx0cyAlPiUgDQogIGdhdGhlcihrZXkgPSAiTWV0cmljIiwgdmFsdWUgPSAiVmFsdWUiLCAtVGhyZXNob2xkKSAtPiBkZl9sb25nDQoNCiMgUGxvdCBjbGFzc2lmaWNhdGlvbiBtZXRyaWNzIHZzIHRocmVzaG9sZCBzZWxlY3RlZDogDQoNCmdncGxvdCgpICsgDQogIGdlb21fbGluZShkYXRhID0gZGZfbG9uZywgYWVzKFRocmVzaG9sZCwgVmFsdWUsIGNvbG9yID0gTWV0cmljKSwgc2l6ZSA9IDEpICsgIA0KICBnZW9tX3BvaW50KGRhdGEgPSBtYXhfYWNjLCBhZXMoVGhyZXNob2xkLCBBY2N1cmFjeSksIGNvbG9yID0gImZpcmVicmljayIsIHNpemUgPSAyKSArIA0KICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gc29tZV90aHJlc2hvbGRzKSArIA0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAidG9wIikgKyANCiAgbGFicyh5ID0gIlJhdGUiLCANCiAgICAgICB0aXRsZSA9ICJGaWd1cmUgNjogU29tZSBDbGFzc2lmaWNhdGlvbiBNZXRyaWNzIGJ5IFRocmVzaG9sZCwgRGVmYXVsdCBSRiIpDQogIA0KYGBgDQoNClThu6sgRmlndXJlIDYgY8OzIHRo4buDIHLDunQgcmEgbeG7mXQgc+G7kSBuaOG6rW4gxJHhu4tuaCBxdWFuIHRy4buNbmcgc2F1OiANCg0KLSBBY2N1cmFjeSBjw7MgZOG6oW5nIGjDrG5oIGNo4buvIFUgbmfGsOG7o2MuIE5naMSpYSBsw6AgYmFuIMSR4bqndSBUaHJlc2hvbGQgdMSDbmcgdGjDrCBBY2N1cmFjeSB0xINuZyBuaMawbmcgxJHhur9uIG3hu5l0IG5nxrDhu6FuZyBuw6BvIMSRw7MgKMSRaeG7g20gbcOgdSDEkeG7jyB0csOqbiBGaWd1cmUgNikgdGjDrCBz4bq9IGzhuqFpIGdp4bqjbSBk4bqnbi4gUGF0dGVybiBuw6B5IGPFqW5nIGzhurdwIGzhuqFpIG7hur91IG5nw6JuIGjDoG5nIGzhu7FhIGNo4buNbiBs4bujaSBuaHXhuq1uIChQcm9maXQpIGNo4bupIGtow7RuZyBwaOG6o2kgQWNjdXJhY3kuIFbhuqVuIMSR4buBIG7DoHkgc+G6vSDEkcaw4bujYyDEkeG7gSBj4bqtcCBjaGkgdGnhur90IHRyb25nIHBo4bqnbiBzYXUuIA0KDQotIEPDsyBz4buxIMSRw6FuaCDEkeG7lWkgZ2nhu69hIGPDoWMgbWV0cmljcy4gQ2jDum5nIHRhIGPDsyB0aOG7gyB0aOG6pXkgU2Vuc2l0aXZpdHkgdsOgIFNwZWNpZmljaXR5IHR1w6JuIHRoZW8gbmjhu69uZyBwYXR0ZXJucyBuZ8aw4bujYyBjaGnhu4F1IG5oYXU6IGtoaSB0aHJlc2hvbGQgdMSDbmcgdGjDrCBTZW5zaXRpdml0eSBnaeG6o20gbmjGsG5nIFNwZWNpZmljaXR5IGzhuqFpIHTEg25nICh2w6AgbmfGsOG7o2MgbOG6oWkpLiDEkGnhu4F1IG7DoHkgaMOgbSDDvSByw7UgcsOgbmcgcuG6sW5nIG7hur91IG5nw6JuIGjDoG5nIGUgbmfhuqFpIHLhu6dpIHJvIChjw7MgdGjhu4MgbMOgIGRvIG7hu4FuIGtpbmggdOG6vyBi4buLIMSRw6xuaCDEkeG7kW4gZG8gQ292aWQgMTkpIHRow6wgbsOzIHPhur0gcGjhuqNpIMSRaeG7gXUgY2jhu4luaCBUaHJlc2hvbGQgc2FvIGNobyBtw7QgaMOsbmggcGjDom4gbG/huqFpIMSR4bqhdCDEkcaw4bujYyBt4bupYyBjaMOtbmggeMOhYyBjYW8gaMahbiwgdsOtIGThu6UsIHRyw6puIDcwJSBraGkgcGjDom4gbG/huqFpIGPDoWMgY2FzZXMgdGjhu7FjIHPhu7EgbMOgIEJhZCB24bubaSBjw6FpIGdpw6EgcGjhuqNpIHRy4bqjIGzDoCBz4bq9IGLhu48gbOG7oSBt4buZdCBz4buRIGPGoSBo4buZaSBraeG6v20gbOG7nWkga2hpIGPhuqVwIHTDrW4gZOG7pW5nIGNobyBjw6FjIGNhc2VzIEdvb2QgKG5oxrBuZyBi4buLIHBow6JuIGxv4bqhaSBzYWkgdGjDoG5oIEJhZCkgZG8gRlAgdMSDbmcuIFThu6sgRmlndXJlIDYgdGjDrCBjw7MgdGjhu4MgdGjhuqV5IG7hur91IMSRw7JpIGjhu49pIGzDoCBwaMOibiBsb+G6oWkgxJHDum5nIGPDoWMgY2FzZXMgbMOgIEJhZCB24bubaSB04buJIGzhu4cgY2jDrW5oIHjDoWMgdOG7kWkgdGhp4buDdSAwLjcgdGjDrCB0aHJlc2hvbGQgc+G6vSBraG/huqNuZyBjaOG7q25nIDAuMzUuIA0KDQoNCkNow7puZyB0YSBjw7MgdGjhu4MgdGjhuqV5IHLhurFuZyBjw6FjIGNo4buJIHPhu5EgxJHDoW5oIGdpw6EgY2jhuqV0IGzGsOG7o25nIHBow6JuIGxv4bqhaSBj4bunYSB0aHXhuq10IHRvw6FuIMSRxrDhu6NjIGLDoW8gY8OhbyB0csOqbiBDTSBz4bq9IHBo4bulIHRodeG7mWMgbmfGsOG7oW5nIMSRxrDhu6NjIGzhu7FhIGNo4buNbiBjaG8gcGjDom4gbG/huqFpLiDEkGnhu4F1IG7DoHkgY8OzIHRo4buDIHThuqFvIHJhIG5o4buvbmcga2jDsyBraMSDbiBraGkgc28gc8OhbmggdsOgIGzhu7FhIGNo4buNbiBnaeG7r2EgY8OhYyB0aHXhuq10IHRvw6FuIE1MIGtow6FjIG5oYXUuIENow61uaCB2w6wgduG6rXkgduG7m2kgYsOgaSB0b8OhbiBwaMOibiBsb+G6oWkgQmluYXJ5IFJlc3BvbnNlIHRow6wgUk9DLUFVQyBsw6AgdGnDqnUgY2jDrSBwaMO5IGjhu6NwIGjGoW4gbMOgbSBjxINuIGPhu6kgxJHhu4Mgc28gc8OhbmggdsOgIGzhu7FhIGNo4buNbiBnaeG7r2EgY8OhYyBtw7QgaMOsbmggTUwga2jDoWMgbmhhdS4gQ2hpIHRp4bq/dCBoxqFuIHbhu4EgdGnDqnUgY2jDrSBuw6B5IGLhuqFuIMSR4buNYyBjw7MgdGjhu4MgdMOsbSBoaeG7g3UgW+G7nyDEkcOieV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvUmVjZWl2ZXJfb3BlcmF0aW5nX2NoYXJhY3RlcmlzdGljKS4gxJDDonkgbMOgIHRpw6p1IGNow60gY2jhu4kgcGjhu6UgdGh14buZYyB2w6BvIFBEIGThu7EgYsOhbyB04burIHRodeG6rXQgdG/DoW4gbcOgIHRow7RpLiANCg0KVuG7m2kgUEQgxJHDoyBjw7MgY2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBow6BtIGByb2MoKWAgY+G7p2EgZ8OzaSAqcFJPQyogxJHhu4MgdMOtbmggdG/DoW4gY2jhu4kgc+G7kSBuw6B5OiANCg0KYGBge3J9DQojIENhbGN1bGF0ZSBST0MtQVVDIG1ldHJpYzogDQoNCmxpYnJhcnkocFJPQykNCg0KbXlfcm9jIDwtIHJvYyhhY3R1YWxfbGFiZWxzLCBwZF9wcmVkaWN0ZWQpDQoNCm15X3JvYw0KYGBgDQoNClRow7RuZyBiw6FvICpBcmVhIHVuZGVyIHRoZSBjdXJ2ZTogMC44MTI0KiBjw7MgbmdoxKlhIGzDoCBST0MtQVVDID0gMC44MTI0LiBOZ2/DoGkgZ2nDoSB0cuG7iyBj4bunYSBST0MtQVVDIHRow6wgaMOsbmggZOG6oW5nIGPhu6dhIMSRxrDhu51uZyBjb25nIG7DoHkgY8WpbmcgbMOgIG3hu5l0IHnhur91IHThu5EgxJHGsOG7o2MgcXVhbiB0w6JtLiBDaMO6bmcgdGEgY8OzIHRo4buDIHbhur0gxJHGsOG7nW5nIGNvbmcgUk9DLUFVQyAoRmlndXJlIDcpIG5oxrAgc2F1OiANCg0KYGBge3J9DQpzZW5fc3BlY19kZiA8LSB0aWJibGUoVFBSID0gbXlfcm9jJHNlbnNpdGl2aXRpZXMsIEZQUiA9IDEgLSBteV9yb2Mkc3BlY2lmaWNpdGllcykNCg0Kc2VuX3NwZWNfZGYgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBGUFIsIHltaW4gPSAwLCB5bWF4ID0gVFBSKSkrDQogIGdlb21fcG9seWdvbihhZXMoeSA9IFRQUiksIGZpbGwgPSAicmVkIiwgYWxwaGEgPSAwLjMpKw0KICBnZW9tX3BhdGgoYWVzKHkgPSBUUFIpLCBjb2wgPSAiZmlyZWJyaWNrIiwgc2l6ZSA9IDEuMikgKw0KICBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yID0gImdyYXkzNyIsIHNpemUgPSAxLCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArIA0KICB0aGVtZV9idygpICsNCiAgY29vcmRfZXF1YWwoKSArDQogIGxhYnMoeCA9ICJGUFIgKDEgLSBTcGVjaWZpY2l0eSkiLCANCiAgICAgICB5ID0gIlRQUiAoU2Vuc2l0aXZpdHkpIiwgDQogICAgICAgdGl0bGUgPSAiRmlndXJlIDc6IE1vZGVsIFBlcmZvcm1hbmNlIGJhc2VkIG9uIFRlc3QgRGF0YSIsIA0KICAgICAgIHN1YnRpdGxlID0gcGFzdGUwKCJBVUMgVmFsdWU6ICIsIG15X3JvYyRhdWMgJT4lIHJvdW5kKDIpKSkNCmBgYA0KDQpN4buZdCBjw7RuZyBj4bulIGjDrG5oIOG6o25oIHTGsMahbmcgdOG7sSBST0MgxJHGsOG7o2Mgc+G7rSBk4bulbmcgxJHhu4MgxJHDoW5oIGdpw6EgY2jhuqV0IGzGsOG7o25nIGPhu6dhIG3DtCBow6xuaCBwaMOibiBsb+G6oWkgbMOgICpsaWZ0IGNoYXJ0Ki4gQuG6oW4gxJHhu41jIHF1YW4gdMOibSBjw7MgdGjhu4MgdGhhbSBraOG6o28gdGjDqm0gW3ThuqFpIMSRw6J5XShodHRwczovL3d3dy5zYWVkc2F5YWQuY29tL21vZGVsX2V2YWx1YXRpb25fYy5odG0pIGhv4bq3YyBbdOG6oWkgxJHDonldKGh0dHBzOi8vd3d3Lm1kcGkuY29tLzIwNzgtMjQ4OS8xMS85LzQyOS9wZGYpLiANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyBodeG6pW4gbHV54buHbiB2w6AgdGluaCBjaOG7iW5oIGzhuqFpIFJGIHbhu5tpIHRpw6p1IGNodeG6qW4gUk9DLUFVQyDhu58gY2jhur8gxJHhu5kgbeG6t2MgxJHhu4tuaCBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBOZXcgbWV0cmljIGFuZCBzYW1wbGluZyB0ZWNobmlxdWUgZm9yIHNlYXJjaGluZyBvcHRpbWFsIHBhcmFtZXRlcnM6ICANCg0Kc2FtcGxpbmdfbmV3IDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyeUZ1bmN0aW9uID0gdHdvQ2xhc3NTdW1tYXJ5LCAgIyBGb3IgQmluYXJ5IFJlc3BvbnNlL0NsYXNzaWZpY2F0aW9uIFRhc2suIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSA0LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDUpDQoNCiMgVHJhaW4gYW5kIHR1cm4gUkYgd2hpY2ggUk9DLUFVQyB1c2VkIGZvciBzZWFyY2hpbmcgb3B0aW1hbCBwYXJhbWV0ZXJzOiANCg0Kc2V0LnNlZWQoMjkpDQoNCnRyYWluKENsYXNzIH4gLiwgDQogICAgICBkYXRhID0gdHJhaW5fdmFsaWRDcmVkaXREYXRhLCANCiAgICAgIG1ldGhvZCA9ICJyZiIsIA0KICAgICAgbWV0cmljID0gIlJPQyIsICMgTWV0cmljIHNlbGVjdGVkIGZvciBzZWFyY2hpbmcgb3B0aW1hbCBwYXJhbWV0ZXJzLiANCiAgICAgIHRyQ29udHJvbCA9IHNhbXBsaW5nX25ldykgLT4gZGVmYXVsdF9yZl9hdWMNCg0KIyBTaG93IHJlc3VsdHM6IA0KDQpkZWZhdWx0X3JmX2F1Yw0KDQpgYGANCg0KVuG7m2kgdGnDqnUgY2h14bqpbiBt4bubaSBsw6AgUk9DLUFVQyB0aMOsIHRoYW0gc+G7kSB04buRaSDGsHUgdMOsbSDEkcaw4bujYyBsw6AgbXRyeSA9IDIuIFPhu60gZOG7pW5nIFJGIHbhu5tpIHRoYW0gc+G7kSB04buRaSDGsHUgbsOgeSDEkeG7gyB0w61uaCBST0MtQVVDIHRyw6puIHThuq1wIGThu68gbGnhu4d1IHRlc3QgbmjGsCBzYXU6IA0KDQoNCmBgYHtyfQ0KIyBVc2UgUkYgZm9yIHByZWRpY3RpbmcgUEQ6IA0KcGRfbmV3IDwtIHByZWRpY3QoZGVmYXVsdF9yZl9hdWMsIGlucHV0cywgdHlwZSA9ICJwcm9iIikNCg0KcGRfbmV3ICU+JSBwdWxsKEJhZCkgLT4gcGRfbmV3DQoNCiMgUk9DLUFVQzogDQpteV9yb2NfbmV3IDwtIHJvYyhhY3R1YWxfbGFiZWxzLCBwZF9uZXcpDQoNCm15X3JvY19uZXcNCmBgYA0KDQpST0MtQVVDID0gMC44MTExIGtow7RuZyBjw7MgbmdoxKlhIGzDoCBSRiB24bubaSB0aGFtIHPhu5EgdOG7kWkgxrB1IHRoZW8gdGnDqnUgY2h14bqpbiBST0MtQVVDIChtdHJ5ID0gMikgY8OzIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSBrw6ltIGjGoW4gUkYgduG7m2kgdGhhbSBz4buRIHThu5FpIMawdSDEkcaw4bujYyB0w6xtIGtp4bq/bSB0aGVvIHRpw6p1IGNodeG6qW4gQWNjdXJhY3kgKG10cnkgPSAzMSkuIFbDrCBjaMO6bmcgdGEgY2jGsGEgdGjhu4MgYuG6r2MgYuG7jyDEkcaw4bujYyB0w6xuaCBodeG7kW5nIHLhurFuZyBr4bq/dCBxdeG6oyAwLjgxMTEgbsOgeSBjaOG7iSBsw6AgdMOsbmggY+G7nSBkbyBt4bqrdSBk4buvIGxp4buHdSBUZXN0IERhdGEgYuG6pXQgbOG7o2kgY2hvIFJPQy1BVUMg4bupbmcgduG7m2kgdGhhbSBz4buRIHRpbmggY2jhu4luaCB0aGVvIHRpw6p1IGNodeG6qW4gUk9DLUFVQy4gDQoNCkThu7FhIHRyw6puIHRoYW0gc+G7kSB04buRaSDGsHUgdMOsbSDEkcaw4bujYyDhu58gY2jhur8gxJHhu5kgbeG6t2MgxJHhu4tuaCBjaMO6bmcgdGEgY8OzIHRo4buDIHPhu60gZOG7pW5nIGdpw6EgdHLhu4sgbXRyeSA9IDIgbsOgeSDEkeG7gyB0w6xtIHJhIHRoYW0gc+G7kSB04buRaSDGsHUgbeG7m2kgYuG6sW5nIHBoxrDGoW5nIHBow6FwIEZ1bGwgR3JpZCBTZWFyY2ggduG7m2kgUiBjb2RlcyBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBMaXN0IGFsbCBwb3RlbnRpYWwgcGFyYW10ZXIgKG10cnkpIGZvciBSYW5kb20gRm9yZXN0OiANCmdyaWRfZm9yX1JGIDwtIGV4cGFuZC5ncmlkKG10cnkgPSBzZXEoMiwgMTEsIGJ5ID0gMSkpDQoNCiMgU2VhcmNoIG9wdGltYWwgcGFyYW1ldGVyIChhbmQgdHJhaW4pIGZvciBSRiB1c2luZyBGdWxsIEdyaWQgU2VhcmNoIE1ldGhvZDogDQoNCnNldC5zZWVkKDI5KQ0KDQpzeXN0ZW0udGltZSgNCiAgDQogIHRyYWluKENsYXNzIH4gLiwgDQogICAgICAgIGRhdGEgPSB0cmFpbl92YWxpZENyZWRpdERhdGEsIA0KICAgICAgICBtZXRob2QgPSAicmYiLCANCiAgICAgICAgbWV0cmljID0gIlJPQyIsICMgTWV0cmljIHNlbGVjdGVkIGZvciBzZWFyY2ggb3B0aW1hbCBwYXJhbWV0ZXJzLiANCiAgICAgICAgdHVuZUdyaWQgPSBncmlkX2Zvcl9SRiwgDQogICAgICAgIHRyQ29udHJvbCA9IHNhbXBsaW5nX25ldykgLT4gcmZfZnVsbEdyaWQNCiAgKQ0KDQojIFNob3cgcmVzdWx0czogDQpyZl9mdWxsR3JpZA0KDQpgYGANCg0KVGhhbSBz4buRIHThu5FpIMawdSB0w6xtIMSRxrDhu6NjIGzDoCBtdHJ5ID0gNCB2w6AgZ2nDoSB0cuG7iyB0cnVuZyBiw6xuaCBj4bunYSBST0MtQVVDIHRyw6puIDIwIGLhu5kgVmFsaWRhdGlvbiBEYXRhIGzDoCAwLjc4OTE4NDUgLSBt4buZdCBr4bq/dCBxdeG6oyBjYW8gaMahbiBu4bq/dSBzbyB24bubaSBSRiDEkcaw4bujYyB0aW5oIGNo4buJbmggbeG6t2MgxJHhu4tuaCBjw7MgbXRyeSA9IDIgduG7m2kgdHJ1bmcgYsOsbmggUk9DLUFVQyBsw6AgMC43ODQxNjY3LiBT4buxIHRoYXkgxJHhu5VpIGPhu6dhIFJPQy1BVUMgdHJ1bmcgYsOsbmggKHRyw6puIDIwIGLhu5kgVmFsaWRhdGlvbikgdGhlbyBtdHJ5IGPDsyB0aOG7gyB0aOG6pXkgdHLDqm4gRmlndXJlIDkgZMaw4bubaSDEkcOieTogDQoNCmBgYHtyfQ0KZ2dwbG90KHJmX2Z1bGxHcmlkKSArIA0KICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDIsIDExLCAxKSkgKyANCiAgbGFicyh0aXRsZSA9ICJGaWd1cmUgOTogTW9kZWwgUGVyZm9ybWFuY2UgYnkgbXRyeSIsIA0KICAgICAgIHN1YnRpdGxlID0gIkZ1bGwgR3JpZCBTZWFyY2ggZm9yIFJhbmRvbSBGb3Jlc3QiKSArIA0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAidG9wIikNCmBgYA0KDQpGaWd1cmUgOSBjaOG7iSByYSBy4bqxbmcgbXRyeSA9IDQgbMOgIGdpw6EgdHLhu4sgdOG7kWkgxrB1IGPhu6dhIHRoYW0gc+G7kSBjaG8gUkYgduG7m2kgdGnDqnUgY2h14bqpbiB0aW5oIGNo4buJbmggdsOgIGzhu7FhIGNo4buNbiBsw6AgUk9DLUFVQy4gDQoNClbhu5tpIG5ow7NtIGPDtG5nIHZp4buHYyBDbGFzc2lmaWNhdGlvbiB0aMOsIGPDsm4gY8OzIG3hu5l0IGPDoWNoIHRp4bq/cCBj4bqtbiBwaOG7lSBiaeG6v24gKHbDoCBsw6J1IMSR4budaSkgbMOgIGjhu5NpIHF1eSBMb2dpc3RpYyAoZ+G7jWkgbMOgIGjhu5NpIHF1eSBuaMawbmcgxJHDonkgbMOgIHRodeG6rXQgdG/DoW4gcGjDom4gbG/huqFpIGThu7FhIHRyw6puIHjDoWMgc3XhuqV0IGThu7EgYsOhbykuIENow7puZyB0YSBz4bq9IHPhu60gZOG7pW5nIGPDoWNoIHRodeG6rW4gdG/DoW4vbcO0IGjDrG5oIG7DoHkgbMOgbSBCYXNlIExpbmUgxJHhu4Mgc28gc8OhbmggY8OhYyB0aHXhuq10IHRvw6FuIGtow6FjLiBSIGNvZGVzIMSR4buDICJodeG6pW4gbHV54buHbiIgbcO0IGjDrG5oIExvZ2lzdGljIG5oxrAgc2F1OiANCg0KYGBge3J9DQpzZXQuc2VlZCgyOSkNCg0KdHJhaW4oQ2xhc3MgfiAuLCANCiAgICAgIGRhdGEgPSB0cmFpbl92YWxpZENyZWRpdERhdGEsIA0KICAgICAgbWV0aG9kID0gImdsbSIsICMgVGhpcyBvcHRpb24gZm9yIExvZ2lzdGljIFJlZ3Jlc3Npb24uIA0KICAgICAgbWV0cmljID0gIlJPQyIsICMgTWV0cmljIHNlbGVjdGVkIGZvciBzZWFyY2hpbmcgb3B0aW1hbCBwYXJhbWV0ZXJzLiANCiAgICAgIHRyQ29udHJvbCA9IHNhbXBsaW5nX25ldykgLT4gbG9naXN0aWMNCg0KIyBTaG93IHJlc3VsdHM6IA0KDQpsb2dpc3RpYw0KYGBgDQoNCkdpw6EgdHLhu4sgdHJ1bmcgYsOsbmggdHLDqm4gMjAgYuG7mSBWYWxpZGF0aW9uIERhdGEgY+G7p2EgTG9naXN0aWMgUmVncmVzc2lvbiBsw6AgMC43NzIzODEgLSB0aOG6pXAgaMahbiBzbyB24bubaSAwLjc4NDE2NjcgY+G7p2EgUkYgxJHGsOG7o2MgdGluaCBjaOG7iW5oIHRoZW8gdGnDqnUgY2h14bqpbiBST0MtQUNVIOG7nyBjaOG6vyDEkeG7mSBt4bq3YyDEkeG7i25oIChtdHJ5IHThu5FpIMawdSBsw6AgMikuIFPhu60gZOG7pW5nIExvZ2lzdGljIFJlZ3Jlc3Npb24gxJHhu4MgdMOtbmggdG/DoW4gUEQgdHLDqm4gYuG7mSBk4buvIGxp4buHdSBUZXN0OiANCg0KYGBge3J9DQojIFVzZSBMb2dpc3RpYyBSZWdycmVzc2lvbiBmb3IgcHJlZGljdGluZyBQRDogDQpwZF9mcm9tX2xvZ2lzdGljIDwtIHByZWRpY3QobG9naXN0aWMsIGlucHV0cywgdHlwZSA9ICJwcm9iIikNCg0KcGRfZnJvbV9sb2dpc3RpYyAlPiUgcHVsbChCYWQpIC0+IHBkX2Zyb21fbG9naXN0aWMNCg0KIyBTb21lIFBEIHByZWRpY3RlZCBieSBMUjogDQpoZWFkKHBkX2Zyb21fbG9naXN0aWMpDQpgYGANCg0KUEQgbsOgeSBjw7MgdGjhu4MgxJHGsOG7o2Mgc+G7rSBk4bulbmcgY2hvLCB2w60gZOG7pSwgc28gc8OhbmggdsOgIMSRw6FuaCBnacOhIOG6o25oIGjGsOG7n25nIGPhu6dhIHZp4buHYyBz4butIGThu6VuZyBtw7QgaMOsbmggTFIgbMOqbiBs4bujaSBuaHXhuq1uIGPhu6dhIG5nw6JuIGjDoG5nIHNvIHbhu5tpIGPDoWNoIGPDoWNoIHRp4bq/cC90aHXhuq10IHRvw6FuIGtow6FjIMSR4buDIHThu6sgxJHDsyBsw6BtIGPEg24gY+G7qSBjaG8gdmnhu4djIGzhu7FhIGNo4buNbiBtw7QgaMOsbmgvdGh14bqtdCB0b8OhbiBuw6BvIMSR4buDIHBow6JuIGxv4bqhaSAtIHjhur9wIGjhuqFuZyB0w61uIGThu6VuZy4gDQoNCiMgU2VsZWN0IE1MIE1vZGVsIGJ5IFByb2ZpdCBNZXRyaWMgDQoNCkPDoWMgdGnDqnUgY2h14bqpbiAoaGF5IG1ldHJpYykgxJHGsOG7o2MgxJHhu4EgY+G6rXAg4bufIHRyw6puLCBuaOG6pXQgbMOgIFJPQy1BVUMgdGjGsOG7nW5nIMSRxrDhu6NjIHPhu60gZOG7pW5nIG5oxrAgbMOgIGPDoWMgdGnDqnUgY2h14bqpbiDEkeG7gyDEkcOhbmggZ2nDoSAtIHNvIHPDoW5oIGPDoWMgdGh14bqtdCB0b8OhbiBNTCBraMOhYyBuaGF1IHbDoCBjaMO6bmcgdGjGsOG7nW5nIHh14bqldCBoaeG7h24gdHLDqm4gY8OhYyBwYXBlcnMuIE5oxrBuZyB24bubaSBjw6FjIHThu5UgY2jhu6ljIGhv4bqhdCDEkeG7mW5nIHbDrCBs4bujaSBuaHXhuq1uIG5oxrAgbmfDom4gaMOgbmcgdGjDrCB0aOG7qSBtw6AgY2jDum5nIHF1YW4gdMOibSBoxqFuIGzDoDogKmtoaSBt4buNaSB0aOG7qSBuaMawIG5oYXUgKENldGVyaXMgcGFyaWJ1cykgdGjDrCwgdsOtIGThu6UsIHZp4buHYyBz4butIGThu6VuZyBwaMawxqFuZyDDoW4gQSBjw7MgbWFuZyBs4bqhaSBs4bujaSDDrWNoIGtpbmggdOG6vyBjYW8gaMahbiBzbyB24bubaSBkw7luZyBwaMawxqFuZyDDoW4gQiBoYXkga2jDtG5nPyouIA0KDQpO4bq/dSBjb2kgdGh14bqtdCB0b8OhbiBo4buTaSBxdXkgTG9naXN0aWMgbMOgIHBoxrDGoW5nIMOhbiBBIGPDsm4gUkYgbMOgIHBoxrDGoW5nIMOhbiBCLCB2w6AgbOG7sWEgY2jhu41uIGzhu6NpIG5odeG6rW4gKFByb2ZpdCkgdGjDrCBjaMO6bmcgdGEgY+G6p24gxJHDoW5oIGdpw6EgeGVtIHBoxrDGoW5nIMOhbiBBIGhheSBCIHPhur0gbWFuZyBs4bqhaSBs4bujaSBuaHXhuq1uIGNhbyBoxqFuIGNobyBuZ8OibiBow6BuZy4gQ2jDum5nIHRhIHPhur0gc+G7rSBk4bulbmcgY8OhY2ggdGnhur9wIGPhuq1uIHRoxrDhu51uZyDEkcaw4bujYyBn4buNaSBsw6AgW0EvQiBUZXN0XShodHRwczovL29ubGluZS5oYnMuZWR1L2Jsb2cvcG9zdC93aGF0LWlzLWFiLXRlc3RpbmcpIMSR4buDIHNvIHPDoW5oIG5o4bqxbSB0w6xtIHJhIGPDonUgdHLhuqMgbOG7nWkgY2hvIGPDonUgaOG7j2kgbsOgeS4gVHLGsOG7m2MgaOG6v3QgY2jDum5nIHRhIMSR4buLbmggbmdoxKlhIHRpw6p1IGNow60gKmzhu6NpIG5odeG6rW4qIG3hu5l0IGPDoWNoIMSRxqFuIGdp4bqjbiBk4buxYSB0csOqbiBoYWkgZ2nhuqMgdGh1eeG6v3Qgc2F1OiANCg0KLSBMw6NpIGzDoCAzMCUgdHLDqm4gc+G7kSB0aeG7gW4gY2hvIHZheSDEkeG7kSB24bubaSBjw6FjIGNhc2VzIGzDoCBHb29kIHbDoCDEkcaw4bujYyBwaMOibiBsb+G6oWkgxJHDum5nIGzDoCBHb29kLiANCg0KLSBLaGkgY2hvIGPDoWMgaOG7kyBzxqEgduG7kW4gbMOgIGjhu5Mgc8ahIHjhuqV1IChCYWQpIG5oxrBuZyBtw7QgaMOsbmggcGjDom4gbG/huqFpIHNhaSB0aMOgbmggdOG7kXQgKEdvb2QpIHRow6wgbmfDom4gaMOgbmcgc+G6vSBt4bqldCB24buRbiBob8OgbiB0b8Ogbi4NCg0KVsOsIHLhurFuZyBr4bq/dCBxdeG6oyBwaMOibiBsb+G6oWkgc+G6vSB0aGF5IMSR4buVaSBu4bq/dSBuZ8aw4buhbmcgxJHGsOG7o2MgY2jhu41uIGNobyBkw6FuIG5ow6NuIChCYWQgaGF5IEdvb2QpIHRoYXkgxJHhu5VpIG7Dqm4gY2jDum5nIHRhIHPhur0gdmnhur90IGjDoG0gdMOtbmggdG/DoW4gbOG7o2kgbmh14bqtbiB0xrDGoW5nIOG7qW5nIHbhu5tpIG3hu5l0IG5nxrDhu6FuZyBj4bulIHRo4buDLiBN4bq3dCBraMOhYyDEkeG7gyBsb+G6oWkgYuG7jywgaG/hurdjIMOtdCBuaOG6pXQgbMOgIGdp4bqjbSB0aGnhu4N1IHThu5FpIMSRYSDhuqNuaCBoxrDhu59uZyBj4bunYSB54bq/dSB04buRIG5n4bqrdSBuaGnDqm4sIHRow6wg4bupbmcgduG7m2kgbeG7l2kgbmfGsOG7oW5nIMSRxrDhu6NjIGzhu7FhIGNo4buNbiBjaMO6bmcgdGEgc+G6vSB0w61uaCB0b8OhbiBs4bujaSBuaHXhuq1uIG7DoHkgdHLDqm4gMTAwMCBs4bqnbiBjaOG7jW4gbeG6q3Ugbmfhuqt1IG5oacOqbiBi4bqxbmcgY8OhY2ggbOG6pXkgcmEgMTAwIHF1YW4gc8OhdCBi4bqldCBrw6wgdHJvbmcgc+G7kSAyMDAgcXVhbiBzw6F0IGPhu6dhIFRlc3QgRGF0YS4gDQoNCsSQ4buDIG1pbmggaOG7jWEgY2jDum5nIHRhIHjDqXQgbeG7mXQgdMOsbmggaHXhu5FuZyBj4bulIHRo4buDIHbhu5tpIFBEIGThu7EgYsOhbyB04burIFJGICh24bubaSBtdHJ5ID0gMzEpIHbDoCB0aHJlc2hvbGQgxJHGsOG7o2MgY2jhu41uIGNobyBkw6FuIG5ow6NuLCB2w60gZOG7pSwgbMOgIDAuMzUuIFRyxrDhu5tjIGjhur90IHThuqFvIGRhdGEgZnJhbWUgY2jhu6lhIGhhaSBiYSB0aMO0bmcgdGluIGPGoSBi4bqjbiBsw6AgbmjDo24gdGjhu7FjIHThur8gKGFjdHVhbCksIG5ow6NuIGThu7EgYsOhbyAocHJlZGljdGVkX2NsYXNzKSB2w6Agc+G7kSB0aeG7gW4gxJHGsOG7o2MgZHV54buHdCB2YXkgZG8gbmfGsOG7nWkgeGluIGPhuqVwIHTDrW4gZOG7pW5nIMSR4buBIHh14bqldCAoQW1vdW50KTogDQoNCmBgYHtyfQ0KIyBTZWxlY3QgdGhyZXNob2xkIGZvciBjbGFzc2lmaWNhdGlvbiBmcm9tIFBEIHByZWRpY3RlZDogDQp0aHJlc2hvbGQgPC0gMC4zNQ0KDQojIFVzZSB0aHJlc2hvbGQgZm9yIGxhYmVsbGluZzogDQoNCmxhYmVsc19wcmVkaWN0ZWQgPC0gY2FzZV93aGVuKHBkX25ldyA+PSB0aHJlc2hvbGQgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikNCg0KIyBDcmVhdGUgZGF0YSBmcmFtZSB0aGF0IGNvbnRhaW5zIGFjdHVhbCBhbmQgcHJlZGljdGVkIGxhYmxlbHM6ICAgDQp0aWJibGUoYWN0dWFsID0gYWN0dWFsX2xhYmVscywgcHJlZGljdGVkX2NsYXNzID0gbGFiZWxzX3ByZWRpY3RlZCkgLT4gZGZfZm9yX2NvbQ0KDQojIEFkZCBhbW91bnQgb2YgbG9hbnMgYW5kIGludGVyZXN0IG9mIHJhdGUgMzAlOiANCiAgDQpkZl9mb3JfY29tICU+JSANCiAgbXV0YXRlKEFtb3VudCA9IHRlc3RDcmVkaXREYXRhJEFtb3VudCkgJT4lIA0KICBtdXRhdGUociA9IDAuMykgLT4gZGZfZm9yX2NhbGN1bGF0aW5nX3Byb2ZpdA0KDQojIENhbGN1bGF0ZSBwcm9maXQ6IA0KZGZfZm9yX2NhbGN1bGF0aW5nX3Byb2ZpdCAlPiUgDQogIG11dGF0ZShwcm9maXQgPSBjYXNlX3doZW4oYWN0dWFsID09ICJHb29kIiAmIHByZWRpY3RlZF9jbGFzcyA9PSAiR29vZCIgfiBBbW91bnQqciwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgYWN0dWFsID09ICJCYWQiICYgcHJlZGljdGVkX2NsYXNzID09ICJHb29kIiB+IC0xKkFtb3VudCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgVFJVRSB+IDApKSAtPiBkZl9wcm9maXQNCg0KIyBTb21lIGZpcnN0IG9ic2VydmF0aW9uczogDQpoZWFkKGRmX3Byb2ZpdCkNCg0KYGBgDQoNClThu6sga+G6v3QgcXXhuqMgbsOgeSBjw7MgdGjhu4MgdGjhuqV5LCB2w60gZOG7pSwgduG7m2kgY2FzZSDEkeG6p3UgdGnDqm4gdGjDrCBkbyBtw7QgaMOsbmggcGjDom4gbG/huqFpIGzDoCBCYWQgKGTDuSB0cm9uZyB0aOG7sWMgdOG6vyBsw6AgR29vZCkgbsOqbiBuZ8OibiBow6BuZyBz4bq9IGLhu48gbOG7oSBjw7QgaOG7mWkga2nhur9tIGzDo2kgMzAlLiBDYXNlIHRo4bupIGhhaSBtw7QgaMOsbmggcGjDom4gbG/huqFpIMSRw7puZyBsw6AgR29vZCB2w6AgbmfDom4gaMOgbmcgc+G6vSBraeG6v20gxJHGsOG7o2MgbMOjaSBsw6AgODUwID0gMjgzNcOXMC4zLiBDYXNlIHRo4bupIDQgdGjDrCBtw7QgaMOsbmggcGjDom4gbG/huqFpIGzDoCBHb29kIG5oxrBuZyB0aOG7sWMgdOG6vyBs4bqhaSBsw6AgQmFkIG7Dqm4gbmfDom4gaMOgbmcgc+G6vSBt4bqldCB0b8OgbiBi4buZIHPhu5EgdGnhu4FuIGNobyB2YXkgbMOgIDEyODMgKGjDo3kgbMOjaSBsw6AgLTEyODIpLiBOaMawIHbhuq15IGzDo2kgc+G6vSBsw6AgdOG7lW5nIGPhu6dhIGPhu5l0IHByb2ZpdDogDQoNCmBgYHtyfQ0KIyBQcm9maXQgZm9yIGFsbCBjYXNlcyBjbGFzc2lmaWVkIGFzIEdvb2QgYnkgbW9kZWw6ICANCmRmX3Byb2ZpdCAlPiUgDQogIHB1bGwocHJvZml0KSAlPiUgDQogIHN1bSgpIC0+IHRvdGFsX3Byb2ZpdA0KDQpwcmludCh0b3RhbF9wcm9maXQpDQpgYGANCg0KVOG7lW5nIHRp4buBbiBsw6NpIHRodSDEkcaw4bujYyBz4bq9IGzDoCAxODUzMS4gU+G7rSBk4bulbmcga+G6v3QgcXXhuqMgbsOgeSBjaMO6bmcgdGEgY8OzIHRo4buDIHTDrW5oIGzhu6NpIG5odeG6rW4gdMOtbmggdHLDqm4gbeG7l2kgbeG7mXQgxJHhu5NuZyB24buRbiBjaG8gdmF5OiANCg0KYGBge3J9DQojIFByb2ZpdCBwZXIgMSQ6IA0KZGZfcHJvZml0ICU+JSANCiAgZmlsdGVyKHByZWRpY3RlZF9jbGFzcyA9PSAiR29vZCIpICU+JSANCiAgcHVsbChBbW91bnQpICU+JSANCiAgc3VtKCkgLT4gdG90YWxfbW9uZXlfb3V0DQoNCnRvdGFsX3Byb2ZpdCAvIHRvdGFsX21vbmV5X291dA0KYGBgDQoNCk5oxrAgduG6rXkgY+G7qSBt4buXaSBt4buZdCDEkeG7k25nIHbhu5FuIGNobyB2YXkgdGjDrCBuZ8OibiBow6BuZyBz4bq9IHRodSB24buBIDAuMDM4IMSR4buTbmcgbMOjaS4gTuG6v3UgbXXhu5FuIGNow7puZyB0YSBjxaluZyBjw7MgdGjhu4MgeGVtIGPDoWMgdGjGsOG7m2MgxJFvIMSRw6FuaCBnacOhIGNo4bqldCBsxrDhu6NuZyBwaMOibiBsb+G6oWkga2jDoWMgY+G7p2EgbcO0IGjDrG5oIG7hur91IG5nxrDhu6FuZyDEkcaw4bujYyBjaOG7jW4gbMOgIDAuMzUgbmjGsCDEkcOjIGJp4bq/dDogDQoNCmBgYHtyfQ0KIyBDb252ZXJ0IHRvIGZhY3RvcjogDQpmYWN0b3IobGFiZWxzX3ByZWRpY3RlZCApIC0+IGxhYmVsc19wcmVkaWN0ZWQNCg0KIyBTb21lIG1ldHJpY3MgYXQgdGhyZXNob2xkIDAuMzU6IA0KY29uZnVzaW9uTWF0cml4KGxhYmVsc19wcmVkaWN0ZWQsIGFjdHVhbF9sYWJlbHMsIHBvc2l0aXZlID0gIkJhZCIpDQpgYGANCg0KxJDhu4Mga2jhuqNvIHPDoXQgc+G7sSBiaeG6v24gxJHhu5VpIGPhu6dhIHByb2ZpdCAodsOgIG3hu5l0IHPhu5EgbWV0cmljcyBraMOhYykga2hpIG5nxrDhu6FuZyDEkcaw4bujYyBjaOG7jW4gdGhheSDEkeG7lWkgY2jDum5nIHRhIHZp4bq/dCBow6BtIGPDsyB0w6puICoqc29tZV9tZXRyaWNzX3dpdGhfdGhyZXNob2xkKiogbmjhuq1uIGlucHV0cyDEkeG6p3UgdsOgbyBsw6A6ICgxKSBtw7QgaMOsbmgvdGh14bqtdCB0b8OhbiDEkcaw4bujYyBjaOG7jW4sIHbDoCAoMikgbmfGsOG7oW5nIHRocmVzaG9sZCBjaG8gcGjDom4gbG/huqFpIMSRxrDhu6NjIGNo4buNbiB2w6AgdMOtbmggdG/DoW4gb3V0cHV0cyBsw6AgY8OhYyBtZXRyaWNzOiANCg0KDQpgYGB7cn0NCiMgRnVuY3Rpb24gY2FsY3VsYXRlcyBzb21lIG1ldHJpY3MgKGluY2x1ZGluZyBwcm9maXQpIGF0IGdpdmVuIHRocmVzaG9sZCBhbmQgbW9kZWwgc2VsZWN0ZWQ6IA0Kc29tZV9tZXRyaWNzX3dpdGhfdGhyZXNob2xkIDwtIGZ1bmN0aW9uKG1vZGVsX3NlbGVjdGVkLCB0aHJlc2hvbGQpIHsNCiAgDQogICMgQ2FsY3VsYXRlIFBEIGJ5IG1vZGVsIHNlbGVjdGVkOiANCiAgDQogIGRmX3BkIDwtIHByZWRpY3QobW9kZWxfc2VsZWN0ZWQsIGlucHV0cywgdHlwZSA9ICJwcm9iIikNCiAgDQogIGRmX3BkICU+JSBwdWxsKEJhZCkgLT4gcGQNCiAgIA0KICAjIENyZWF0ZSBkYXRhIGZyYW1lIHRoYXQgY29udGFpbnMgYWN0dWFsIGFuZCBwcmVkaWN0ZWQgbGFibGVsczogDQogIGxhYmVsc19wcmVkaWN0ZWQgPC0gY2FzZV93aGVuKHBkID49IHRocmVzaG9sZCB+ICJCYWQiLCBUUlVFIH4gIkdvb2QiKQ0KICANCiAgIyBDcmVhdGUgYWN0dWFsIC0gcHJlZGljdGVkIGRhdGEgZnJhbWUgZm9yIHB1cnBvc2Ugb2YgY29tcGFyaXNpb246IA0KICB0aWJibGUoYWN0dWFsID0gYWN0dWFsX2xhYmVscywgcHJlZGljdGVkID0gbGFiZWxzX3ByZWRpY3RlZCkgLT4gZGZfY29tcGFyZWQNCiAgDQogICMgQ2FsY3VsYXRlIEFjY3VyYWN5IG1ldHJpYzogDQogIGFjY19tZXRyaWMgPC0gc3VtKGxhYmVsc19wcmVkaWN0ZWQgPT0gYWN0dWFsX2xhYmVscykgLyBsZW5ndGgobGFiZWxzX3ByZWRpY3RlZCkNCiAgDQogICMgQ2FsY3VsYXRlIFNlbnNpdGl2aXkgbWV0cmljOiANCiAgZGZfY29tcGFyZWQgJT4lIGZpbHRlcihhY3R1YWwgPT0gIkJhZCIpIC0+IGRmX2JhZF9zZW4NCiAgDQogIGRmX2JhZF9zZW4gJT4lIA0KICAgIGZpbHRlcihwcmVkaWN0ZWQgPT0gIkJhZCIpICU+JSANCiAgICBucm93KCkgLyBucm93KGRmX2JhZF9zZW4pIC0+IHNlbl9tZXRyaWMNCiAgDQogICMgQ2FsY3VsYXRlIFNwZWNpZmljaXR5IG1ldHJpYzogDQogIA0KICBkZl9jb21wYXJlZCAlPiUgZmlsdGVyKGFjdHVhbCA9PSAiR29vZCIpIC0+IGRmX2dvb2Rfc3BlYw0KICANCiAgZGZfZ29vZF9zcGVjICU+JSANCiAgICBmaWx0ZXIocHJlZGljdGVkID09ICJHb29kIikgJT4lIA0KICAgIG5yb3coKSAvIG5yb3coZGZfZ29vZF9zcGVjKSAtPiBzcGVjX21ldHJpYw0KICANCiAgIyBDYWxjdWxhdGUgcHJvZml0IGFuZCBzb21lIG1ldHJpY3MgYXQgZ2l2ZW4gdGhyZXNob2xkOiANCiAgDQogIGRmX2NvbXBhcmVkICU+JSANCiAgICBtdXRhdGUoQW1vdW50ID0gdGVzdENyZWRpdERhdGEkQW1vdW50KSAlPiUgDQogICAgbXV0YXRlKHIgPSAwLjMpICU+JSANCiAgICBtdXRhdGUocHJvZml0ID0gY2FzZV93aGVuKGFjdHVhbCA9PSAiR29vZCIgJiBwcmVkaWN0ZWQgPT0gIkdvb2QiIH4gQW1vdW50KnIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWN0dWFsID09ICJCYWQiICYgcHJlZGljdGVkID09ICJHb29kIiB+IC0xKkFtb3VudCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBUUlVFIH4gMCkpIC0+IGRmX3Byb2ZpdA0KICANCiAgZGZfcHJvZml0ICU+JSANCiAgICBwdWxsKHByb2ZpdCkgJT4lIA0KICAgIHN1bSgpIC0+IHByb2YNCiAgDQogICMgRmluYWwgcmVzdWx0cyBpbiBERiBmb3JtOiANCiAgDQogIHRpYmJsZShBY2N1cmFjeSA9IGFjY19tZXRyaWMsIA0KICAgICAgICAgU2Vuc2l0aXZpeSA9IHNlbl9tZXRyaWMsDQogICAgICAgICBTcGVjaWZpY2l0eSA9IHNwZWNfbWV0cmljLCANCiAgICAgICAgIFByb2ZpdCA9IHByb2YsIA0KICAgICAgICAgVGhyZXNob2xkID0gdGhyZXNob2xkKSAtPiBkZl9yZXBvcnQNCiAgDQogICMgUmV0dXJuIGZpbmFsIG91dHB1dHM6IA0KICANCiAgcmV0dXJuKGRmX3JlcG9ydCkNCn0NCmBgYA0KDQoNCkNow7puZyB0YSBjw7MgdGjhu4MgdGVzdCBz4buxIHbhuq1uIGjDoG5oIGPhu6dhIGjDoG0gbsOgeSB24bubaSB0aHJlc2hvbGQgPSAwLjM1IHbDoCBtw7QgaMOsbmggxJHGsOG7o2MgY2jhu41uIGzDoCBSRiB24bubaSBtdHJ5ID0gMzEgKHRpbmggY2jhu4luaCB0aGVvIHRpw6p1IGNodeG6qW4gUk9DLUFDVSDhu58gY2jhur8gxJHhu5kgbeG6t2MgxJHhu4tuaCk6IA0KDQpgYGB7cn0NCnNvbWVfbWV0cmljc193aXRoX3RocmVzaG9sZChkZWZhdWx0X3JmX2F1YywgMC4zNSkNCmBgYA0KDQpDaMO6bmcgdGEga2jhuqNvIHPDoXQgc+G7sSBiaeG6v24gxJHhu5VpIG1vZGVsIHBlcmZvcm1hbmNlIChiYW8gZ+G7k20gY+G6oyBwcm9maXQpIGNobyBj4bqjIGLhu5FuIG3DtCBow6xuaC90aHXhuq10IHRvw6FuL2PDoWNoIHRp4bq/cCBj4bqtbiBraGkgdGhyZXNob2xkIHRoYXkgxJHhu5VpIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIEZvciBMUjogDQpsYXBwbHkoc29tZV90aHJlc2hvbGRzLCBmdW5jdGlvbih4KSB7c29tZV9tZXRyaWNzX3dpdGhfdGhyZXNob2xkKGxvZ2lzdGljLCB4KX0pIC0+IHJlc3VsdHNfbG9naXN0aWNfbGlzdA0KDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCByZXN1bHRzX2xvZ2lzdGljX2xpc3QpIC0+IGRmX3Jlc3VsdHNfbHINCg0KZGZfcmVzdWx0c19sciAlPiUgbXV0YXRlKE1vZGVsID0gIkxvZ2lzdGljIikgLT4gZGZfcmVzdWx0c19scg0KDQojIEZvciBSRiBieSBhY2N1cmFjeSBtZXRyaWM6IA0KDQpsYXBwbHkoc29tZV90aHJlc2hvbGRzLCBmdW5jdGlvbih4KSB7c29tZV9tZXRyaWNzX3dpdGhfdGhyZXNob2xkKGRlZmF1bHRfcmYsIHgpfSkgLT4gcmVzdWx0c19yZl9hY2MNCg0KZG8uY2FsbCgiYmluZF9yb3dzIiwgcmVzdWx0c19yZl9hY2MpIC0+IGRmX3Jlc3VsdHNfcmZfYWNjDQoNCmRmX3Jlc3VsdHNfcmZfYWNjICU+JSBtdXRhdGUoTW9kZWwgPSAiUkYtQWNjdXJhY3kiKSAtPiBkZl9yZXN1bHRzX3JmX2FjYw0KDQojIEZvciBSRiBieSBST0MtQUNVIGRlZmF1bHQgc2VhcmNoOiANCg0KbGFwcGx5KHNvbWVfdGhyZXNob2xkcywgZnVuY3Rpb24oeCkge3NvbWVfbWV0cmljc193aXRoX3RocmVzaG9sZChkZWZhdWx0X3JmX2F1YywgeCl9KSAtPiByZXN1bHRzX3JmX2F1Yw0KDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCByZXN1bHRzX3JmX2F1YykgLT4gZGZfcmVzdWx0c19yZl9hdWMNCg0KZGZfcmVzdWx0c19yZl9hdWMgJT4lIG11dGF0ZShNb2RlbCA9ICJSRi1BVUMiKSAtPiBkZl9yZXN1bHRzX3JmX2F1Yw0KDQojIEZvciBSRiBieSBST0MtQVVDIGZ1bGwgZ3JpZCBzZWFyY2g6IA0KDQpsYXBwbHkoc29tZV90aHJlc2hvbGRzLCBmdW5jdGlvbih4KSB7c29tZV9tZXRyaWNzX3dpdGhfdGhyZXNob2xkKHJmX2Z1bGxHcmlkLCB4KX0pIC0+IHJlc3VsdHNfcmZfYXVjRnVsbA0KDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCByZXN1bHRzX3JmX2F1Y0Z1bGwpIC0+IGRmX3Jlc3VsdHNfcmZfYXVjRnVsbA0KDQpkZl9yZXN1bHRzX3JmX2F1Y0Z1bGwgJT4lIG11dGF0ZShNb2RlbCA9ICJSRi1GdWxsIikgLT4gZGZfcmVzdWx0c19yZl9hdWNGdWxsDQoNCmBgYA0KDQpTbyBzw6FuaCBjw6FjIHBoxrDGoW5nIMOhbjogDQoNCmBgYHtyfQ0KIyBDb21iaW5lIHJlc3VsdHM6IA0KZGZfcmVzdWx0c19sciAlPiUgDQogIGJpbmRfcm93cyhkZl9yZXN1bHRzX3JmX2FjYykgJT4lIA0KICBiaW5kX3Jvd3MoZGZfcmVzdWx0c19yZl9hdWMpICU+JSANCiAgYmluZF9yb3dzKGRmX3Jlc3VsdHNfcmZfYXVjRnVsbCkgLT4gZGZfYmlnDQoNCiMgQ29udmVydCB0byBsb25nIGZvcm06IA0KZGZfYmlnICU+JSANCiAgZ2F0aGVyKE1ldHJpYywgVmFsdWUsIC1Nb2RlbCwgLVRocmVzaG9sZCkgLT4gZGZfYmlnX2xvbmcNCg0KIyBUaHJlc2hvbGQgdGhhIG1heGltaXplcyBBY2N1cmFjeSBieSBtb2RlbC9NTCBhbGdvcml0aG06IA0KZGZfYmlnX2xvbmcgJT4lIA0KICBmaWx0ZXIoTWV0cmljID09ICJBY2N1cmFjeSIpICU+JSANCiAgZ3JvdXBfYnkoTW9kZWwpICU+JSANCiAgdW5ncm91cCgpICU+JSANCiAgc2xpY2Uod2hpY2gubWF4KFZhbHVlKSkgLT4gZGZfbWF4X2FjYw0KDQojIFRocmVzaG9sZCB0aGEgbWF4aW1pemVzIHByb2ZpdCBieSBtb2RlbC9NTCBhbGdvcml0aG06IA0KZGZfYmlnX2xvbmcgJT4lIA0KICBmaWx0ZXIoTWV0cmljID09ICJQcm9maXQiKSAlPiUgDQogIGdyb3VwX2J5KE1vZGVsKSAlPiUgDQogIHNsaWNlKHdoaWNoLm1heChWYWx1ZSkpIC0+IGRmX21heF9wcm9maXQNCg0KYGBgDQoNCg0KTmjGsCB24bqteSB24bubaSBr4buLY2ggYuG6o24gc+G7rSBk4bulbmcgTFIgbMOgbSBtw7QgaMOsbmggeOG6v3AgaOG6oW5nIHbDoCBwaMOibiBsb+G6oWkgdMOtbiBk4bulbmcgdGjDrCBuZ8aw4buhbmcgdOG7kWkgxrB1IMSR4buDIMSR4bqhdCB04buRaSDEkWEgaMOzYSBwcm9maXQgbMOgIDAuNCB2w6AgbMO6YyBuw6B5IHByb2ZpdCBsw6AgNTY3MzA6IA0KDQpgYGB7cn0NCmRmX21heF9wcm9maXQNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIGto4bqjbyBzw6F0IHPhu7EgdGhheSDEkeG7lWkgY+G7p2EgcHJvZml0IGtoaSB0aHJlc2hvbGQgdGhheSDEkeG7lWkgY2hvIGPhuqMgNCBtw7QgaMOsbmgvdGh14bqtdCB0b8OhbiAoRmlndXJlIDkpOg0KDQpgYGB7cn0NCmRmX2JpZ19sb25nICU+JSANCiAgZ2dwbG90KGFlcyhUaHJlc2hvbGQsIFZhbHVlLCBjb2xvciA9IE1vZGVsKSkgKyANCiAgZ2VvbV9saW5lKHNpemUgPSAxKSArIA0KICBnZW9tX3BvaW50KGRhdGEgPSBkZl9tYXhfcHJvZml0ICU+JSB1bmdyb3VwKCkgJT4lIHNsaWNlKHdoaWNoLm1heChWYWx1ZSkpLCANCiAgICAgICAgICAgICBhZXMoVGhyZXNob2xkLCBWYWx1ZSksIHNoYXBlID0gMTgsIHNpemUgPSAyLjUsIGNvbG9yID0gImZpcmVicmljayIpICsgDQogIGZhY2V0X3dyYXAofiBNZXRyaWMsIHNjYWxlcyA9ICJmcmVlIikgKyANCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNvbWVfdGhyZXNob2xkcykgKyANCiAgdGhlbWUocGFuZWwuZ3JpZC5taW5vciA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gInRvcCIpICsgDQogIGxhYnMoeSA9IE5VTEwsIA0KICAgICAgIHggPSBOVUxMLCANCiAgICAgICB0aXRsZSA9ICJGaWd1cmUgOTogVGhyZXNob2xkIHRoYXQgbWF4aW1pemVzIFByb2ZpdCBieSBNb2RlbCIpDQpgYGANCg0KDQpGaWd1cmUgOSBjaG8gdGjhuqV5OiANCg0KLSBO4bq/dSBjaOG7jW4gcHJvZml0IGzDoG0gdGnDqnUgY2h14bqpbiBzbyBzw6FuaCB2w6AgbOG7sWEgY2jhu41uIG3DtCBow6xuaC90aHXhuq10IHRvw6FuIHRow6wgTFIgc+G6vSBsw6AgbcO0IGjDrG5oIG1hbmcgbOG6oWkgbOG7o2kgbmh14bqtbiBs4bubbiBuaOG6pXQgc28gduG7m2kgMyBtw7QgaMOsbmgvdGh14bqtdCB0b8OhbiBjw7JuIGzhuqFpLiBOZ8aw4buhbmcgcGjDom4gbG/huqFpIMSR4buDIHThu5FpIMawdSBow7NhIHByb2ZpdCBraGkgc+G7rSBk4bulbmcgTFIgbMOgIDAuNCB2w6Aga2hpIMSRw7MgTWF4IFByb2ZpdCA9IDU2NzMwLiANCg0KLSBSRiBjw7MgdGhhbSBz4buRIHRpbmggY2jhu4luaCB0aGVvIHRpw6p1IGNodeG6qW4gQWNjdXJhY3ksIHbDoCB04bqhaSBuZ8aw4buhbmcgdOG7kWkgxrB1IHTGsMahbmcg4bupbmcgbMOgIDAuMjUgbMOgIG3DtCBow6xuaC90aHXhuq10IHRvw6FuIHThu5F0IHRo4bupIGhhaSBjxINuIGPhu6kgdGhlbyB0acOqdSBjaHXhuqluIHByb2ZpdCAoxJHGsOG7nW5nIFJGLUFjY3VyYWN5KS4gDQoNCi0gUkYgY8OzIHRoYW0gc+G7kSB0aW5oIGNo4buJbmggdGhlbyB0acOqdSBjaHXhuqluIHThu5FpIMSRYSBST0MtQVVDIOG7nyBjaOG6vyDEkeG7mSBt4bq3YyDEkeG7i25oICjEkcaw4budbmcgUkYtQVVDKSwgdsOgIHThuqFpIG5nxrDhu6FuZyB04buRaSDGsHUgdMawxqFuZyDhu6luZyBsw6AgMC4zIHPhur0gdOG6oW8gcmEgcHJvZml0IGzhu5tuIG5o4bqldCBsw6AgNDgzMzEgbmjGsG5nIG5nYXkgc2F1IG5nxrDhu6FuZyB04buRaSDGsHUgbsOgeSB0aMOsIHByb2ZpdCBnaeG6o20gcuG6pXQgbmhhbmguIE7Ds2kgY8OhY2gga2jDoWMsIHByb2ZpdCBjw7MgYmnhur9uIMSR4buZbmcgcuG6pXQgbOG7m24geHVuZyBxdWFuaCBuZ8aw4buhbmcgdOG7kWkgxrB1IHTGsMahbmcg4bupbmcgY+G7p2EgbcO0IGjDrG5oL3RodeG6rXQgdG/DoW4gbsOgeS4NCg0KLSBBY2N1cmFjeSB2w6AgUHJvZml0IGPDsyBk4bqhbmcgaMOsbmggY2jhu68gVSBuZ8aw4bujYyBi4bqldCBr4buDIG3DtCBow6xuaC90aHXhuq10IHRvw6FuIGzDoCBnw6wuIMSQaeG7gXUgbsOgeSBow6BtIMO9IHLhurFuZzogduG7m2kgbeG7l2kgbeG7mXQgbcO0IGjDrG5oL3RodeG6rXQgdG/DoW4gdGjDrCB04buTbiB04bqhaSBt4buZdCBuZ8aw4buhbmcgbcOgIEFjY3VyYWN5L1Byb2ZpdCBsw6AgbOG7m24gbmjhuqV0IHbDoCB2xrDhu6N0IHF1YSBuZ8aw4buhbmcgbsOgeSB0aMOsIGhhaSB0acOqdSBjaMOtIG7DoHkgYuG6r3QgxJHhuqd1IGdp4bqjbS4gxJBp4buBdSDEkcOhbmcgY2jDuiDDvSB2w6AgZ2nDoSB0cuG7iyBoxqFuIGzDoCAqduG7m2kgbeG7mXQgbcO0IGjDrG5oL3RodeG6rXQgdG/DoW4gdGjDrCBuZ8aw4buhbmcgbcOgIHThuqFpIMSRw7MgQWNjdXJjeSBs4bubbiBuaOG6pXQga2jDtG5nIHBo4bqjaSBsw6AgbmfGsOG7oW5nIGzDoG0gY2hvIFByb2ZpdCBs4bubbiBuaOG6pXQqLiANCg0KLSBDw7Mgc+G7sSDEkcOhbmggxJHhu5VpIGdp4buvYSBjw6FjIHRpw6p1IGNow60sIMSR4bq3YyBiaeG7h3QgbMOgIFNlbnNpdGl2aXR5IHbDoCBTcGVjaWZpY2l0eS4gVMO5eSBi4buRaSBj4bqjbmggdsOgIMSRw7JpIGjhu49pIG5nxrDhu51pIGzDoG0gbcO0IGjDrG5oIGPDsyB0aOG7gyBjaOG7jW4gbmfGsOG7oW5nIHNhbyBjaG8gcGjDuSBo4bujcCB24bubaSBjw6FjIG3hu6VjIHRpw6p1LiBDaOG6s25nIGjhuqFuIHRyb25nIMSR4bqhaSBk4buLY2ggQ292aWQgMTkgduG7q2EgcuG7k2ksIG3hu5l0IGjhu4cgdGjhu5FuZyB0ZXN0IG5o4bqxbSB4w6l0IG5naGnhu4dtIG5oYW5oIGFpIGLhu4sgaGF5IGtow7RuZyBi4buLIG5oaeG7hW0gbG/huqFpIHZpcnVzIG7DoHkgY8OzIGzhur0gbsOqbiDEkcaw4bujYyB0aGnhur90IGvhur8gc2FvIGNobyBuw7MgcGjDoXQgaGnhu4duIHJhIFRQIChkxrDGoW5nIHTDrW5oIHRo4bqtdCkgLSBoYXkgU2Vuc2l0aXZpdHkgY2FvIG5o4bqldCBjw7MgdGjhu4MgYuG6pXQgY2jhuqVwIGPDoWkgZ2nDoSBwaOG6o2kgdHLhuqMgbMOgIGPDsyB0aOG7gyBwaMOibiBsb+G6oWkgc2FpIG5o4buvbmcgbmfGsOG7nWkga2jDtG5nIGLhu4sgbmhp4buFbSBDb3ZpZCAxOSB0aMOgbmggYuG7iyBuaGnhu4VtIGLhurFuZyBjw6FjaCDEkWnhu4F1IGNo4buJbmggbmfGsOG7oW5nIHBow6JuIGxv4bqhaS4gDQoNCg0KVOG7qyBr4bq/dCBxdeG6oyB0aOG7sWMgbmdoaeG7h20gduG7m2kgYuG7mSBk4buvIGxp4buHdSBHZXJtYW5DcmVkaXQg4bufIHRyw6puIGNow7puZyB0YSByw7p0IHJhIG3hu5l0IHPhu5Ega+G6v3QgcXXhuqMgcXVhbiB0cuG7jW5nIHNhdTogDQoNCi0gQ8OhYyBuaMOgIHRo4buRbmcga8OqIGhv4bq3YyBuZ8aw4budaSBsw6BtIG3DtCBow6xuaCBjw7MgdGjhu4MgcXVhbiB0w6JtIMSR4bq/biBjw6FjIGNo4buJIHRpw6p1IHRodeG6p24gdGjhu5FuZyBrw6ogxJHhu4MgxJHDoW5oIGdpw6EgdsOgIGzhu7FhIGNo4buNbiBjw6FjIG3DtCBow6xuaCBraMOhYyBuaGF1LiBOaMawbmcgduG7m2kgbeG7mXQgdOG7lSBjaOG7qWMgaG/huqF0IMSR4buZbmcgdsOsIGzhu6NpIG5odeG6rW4gdGjDrCAqY8OhYyB0acOqdSBjaMOtIMSRw7MgKG5oxrAgQWNjdXJhY3ksIHRo4bqtbSBjaMOtIGzDoCBST0MtQVVDKSBjaOG7iSBjw7MgZ2nDoSB0cuG7iyB0aGFtIGto4bqjbyBtw6AgdGjDtGkqLiBM4bujaSBuaHXhuq1uIGhv4bq3YyBsw6AgcuG7p2kgcm8gKGhv4bq3YyBj4bqjIGhhaSkgbeG7m2kgbMOgIHF1YW4gdMOibSBjaMOtbmggY+G7p2Egbmjhu69uZyB04buVIGNo4bupYyBob+G6oXQgxJHhu5luZyB2w6wgbOG7o2kgbmh14bqtbi4gDQoNCi0gTeG7mXQgbcO0IGjDrG5oL3RodeG6rXQgdG/DoW4gY8OzIFJPQy1BVUMgdHJ1bmcgYsOsbmggKHRyw6puIDIwIGLhu5kgVmFsaWRhdGlvbiBEYXRhKSBjYW8gaMahbiwgdGjhuq1tIGNow60gbMOgIFJPQy1BVUMgY2FvIGjGoW4gdHLDqm4gVGVzdCBEYXRhIGNoxrBhIGNoxrBhIHBo4bqjaSBsw6AgbcO0IGjDrG5oL3RodeG6rXQgdG/DoW4gbWFuZyBs4bqhaSBs4bujaSBuaHXhuq1uIGNhbyBuaOG6pXQgY2hvIG5nw6JuIGjDoG5nLiANCg0KIyBTZW5zaXRpdml0eSBmb3IgU2VhcmNoaW5nIE9wdGltYWwgUGFyYW1ldGVycw0KDQpOZ2/DoGkgdGluaCBjaOG7iW5oIHbDoCB0w6xtIGtp4bq/bSB0aGFtIHPhu5EgdOG7kWkgxrB1IHRoZW8gQWNjdXJhY3kgaG/hurdjIFJPQy1BVUMgbmjGsCDEkcOjIGJp4bq/dCB0aMOsIGNhcmV0IGPFqW5nIGNobyBwaMOpcCB0aW5oIGNo4buJbmggdsOgIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgdGhlbyBTZW5zaXRpdml0eSB04bqhaSBuZ8aw4buhbmcgMC41IChLdWhuIHbDoCBKb2huc29uLCAyMDEzKSB24bubaSBs4buxYSBjaOG7jW4gYG1ldHJpYyA9ICJTZW5zImAgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCiMgU2VhcmNoIG9wdGltYWwgcGFyYW1ldGVycyBmb3IgUkYgdXNpbmcgU2Vuc2l0aXZpdHkgbWV0cmljOiANCnNldC5zZWVkKDI5KQ0KDQpzeXN0ZW0udGltZSgNCiAgDQogIHRyYWluKENsYXNzIH4gLiwgDQogICAgICAgIGRhdGEgPSB0cmFpbl92YWxpZENyZWRpdERhdGEsIA0KICAgICAgICBtZXRob2QgPSAicmYiLCANCiAgICAgICAgbWV0cmljID0gIlNlbnMiLCAjIFNlbnNpdGl2aXR5IHNlbGVjdGVkIGZvciBzZWFyY2hpbmcgb3B0aW1hbCBwYXJhbWV0ZXJzLiANCiAgICAgICAgdHVuZUdyaWQgPSBleHBhbmQuZ3JpZChtdHJ5ID0gMzA6NjEpLCANCiAgICAgICAgdHJDb250cm9sID0gc2FtcGxpbmdfbmV3KSAtPiByZl9mdWxsR3JpZF9zZW4NCiAgKQ0KDQojIFNob3cgYmVzdCBwYXJhbXRlciBieSBTZW5zaXRpdml0eSBtZXRyaWM6IA0KcmZfZnVsbEdyaWRfc2VuJGJlc3RUdW5lDQpgYGANCg0KTmjGsCB24bqteSB0aW5oIGNo4buJbmggdGhlbyB0acOqdSBjaHXhuqluIFNlbnNpdGl2aXR5IHRow6wgdGhhbSBz4buRIHThu5FpIMawdSBjaG8gUkYgbMOgIG10cnkgPSA1OC4gQ2jDum5nIHRhIGPDsyB0aOG7gyBraOG6o28gc8OhdCBz4buxIHRoYXkgxJHhu5VpIHRydW5nIGLDrG5oIGPhu6dhIFNlbnNpdGl2aXR5ICh0csOqbiAyMCBi4buZIFZhbGlkYXRpb24gRGF0YSkga2hpIG10cnkgdGhheSDEkeG7lWkg4bufIEZpZ3VyZSAxMCBkxrDhu5tpIMSRw6J5OiANCg0KYGBge3J9DQpnZ3Bsb3QocmZfZnVsbEdyaWRfc2VuKSArIA0KICBsYWJzKHRpdGxlID0gIkZpZ3VyZSAxMDogTW9kZWwgUGVyZm9ybWFuY2UsIFJhbmRvbSBGb3Jlc3QiLCANCiAgICAgICBzdWJ0aXRsZSA9ICJTZW5zaXRpdml0eSBzZWxlY3RlZCBmb3Igc2VhcmNoaW5nIG9wdGltYWwgbXRyeSIpICsgDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJ0b3AiKQ0KYGBgDQoNCkNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgUkYgKHbhu5tpIG10cnkgPSA1OCkgbsOgeSDEkeG7gyBjaOG7iSByYSBuZ8aw4buhbmcgcGjDom4gbG/huqFpIG3DoCB04buRaSDEkWEgaMOzYSBs4bujaSBuaHXhuq1uIHRoZW8gY8OhY2ggdGjhu6ljIMSRw6MgYmnhur90IG5oxrAgc2F1OiANCg0KYGBge3J9DQojIENhbGN1bGF0ZSBwcm9maXQgYXQgcmFuZ2Ugb2YgdGhyZXNob2xkcyBmb3IgUkYgd2l0aCBtdHJ5ID0gNTg6IA0KbGFwcGx5KHNvbWVfdGhyZXNob2xkcywgZnVuY3Rpb24oeCkge3NvbWVfbWV0cmljc193aXRoX3RocmVzaG9sZChyZl9mdWxsR3JpZF9zZW4sIHgpfSkgLT4gcmVzdWx0c19yZl9zZW4NCg0KZG8uY2FsbCgiYmluZF9yb3dzIiwgcmVzdWx0c19yZl9zZW4pIC0+IGRmX3Jlc3VsdHNfcmZfc2VuDQoNCiMgVGhyZXNob2xkIHRoYXQgbWF4aW1pemVzIHByb2ZpdDogDQoNCmRmX3Jlc3VsdHNfcmZfc2VuICU+JSBtdXRhdGUoTW9kZWwgPSAiUkYtU2VuIikgLT4gZGZfcmVzdWx0c19yZl9zZW4NCg0KZGZfcmVzdWx0c19yZl9zZW4gJT4lIA0KICBzbGljZSh3aGljaC5tYXgoUHJvZml0KSkNCg0KYGBgDQoNCk5oxrAgduG6rXkgbmfGsOG7oW5nIHThu5FpIMawdSBsw6AgMC4yIG5oxrBuZyBwcm9maXQgdOG6oWkgbmfGsOG7oW5nIHThu5FpIMawdSBuw6B5IGzDoCA1NTU3NyAtIG3hu5l0IGNvbiBz4buRIHR1eSBjw7MgdMSDbmcgc28gduG7m2kgNTI3ODcgbmjGsG5nIHbhuqtuIHRo4bqlcCBoxqFuIG7hur91IHNvIHbhu5tpIHPhu60gZOG7pW5nIExSIHbhu5tpIG5nxrDhu6FuZyB04buRaSDGsHUgMC40LiANCg0KIyBGZWF0dXJlIEVuZ2luZWVyaW5nDQoNCi0gTWlzc2luZyBEYXRhDQoNCi0gRW5jb2RpbmcgQ2F0ZWdvcmljYWwgRGF0YQ0KDQotIE51bWVyaWMgRGF0YQ0KDQotIEZlYXR1cmUgU2VsZWN0aW9uDQoNCi0gQ29ycmVsYXRpb24NCg0KVG8gYmUgY29udGludWVkLi4uDQoNCg0KIyBDYXBzdG9uZSBQcm9qZWN0OiBGaW5hbmNpYWwgRnJhdWQgRGV0ZWN0aW9uIA0KDQpUbyBiZSBjb250aW51ZWQuLi4NCg0KIyBDYXBzdG9uZSBQcm9qZWN0OiBIb21lQ3JlZGl0IENvbnRlc3QNCg0KDQojIFJlZmVyZW5jZXMNCg0KaHR0cDovL3d3dy5mZWF0LmVuZ2luZWVyaW5nL2dyZWVkeS1zaW1wbGUtZmlsdGVycy5odG1sDQoNCkVmcm9uIEIgKDE5ODMpLiBFc3RpbWF0aW5nIHRoZSBFcnJvciBSYXRlIG9mIGEgUHJlZGljdGlvbiBSdWxlOiBJbXByb3ZlbWVudCBvbiBDcm9zc+KAk1ZhbGlkYXRpb24uICpKb3VybmFsIG9mIHRoZSBBbWVyaWNhbiBTdGF0aXN0aWNhbCBBc3NvY2lhdGlvbiosIHBwLiAzMTbigJMzMzEuDQoNCkVmcm9uIEIsIFRpYnNoaXJhbmkgUiAoMTk4NikuIEJvb3RzdHJhcCBNZXRob2RzIGZvciBTdGFuZGFyZCBFcnJvcnMsIENvbmZpZGVuY2UgSW50ZXJ2YWxzLCBhbmQgT3RoZXIgTWVhc3VyZXMgb2YgU3RhdGlzdGljYWwgQWNjdXJhY3kuICpTdGF0aXN0aWNhbCBTY2llbmNlKiwgcHAuIDU04oCTNzUuIA0KDQpFZnJvbiBCLCBUaWJzaGlyYW5pIFIgKDE5OTcpLiBJbXByb3ZlbWVudHMgb24gQ3Jvc3PigJNWYWxpZGF0aW9uOiBUaGUgNjMyKyBCb290c3RyYXAgTWV0aG9kLiAqSm91cm5hbCBvZiB0aGUgQW1lcmljYW4gU3RhdGlzdGljYWwgQXNzb2NpYXRpb24qLCA5Mig0MzgpLCA1NDjigJM1NjAuIA0KDQoNCk1vbGluYXJvIEEgKDIwMDUpLiBQcmVkaWN0aW9uIEVycm9yIEVzdGltYXRpb246IEEgQ29tcGFyaXNvbiBvZiBSZXNhbXBsaW5nIE1ldGhvZHMuICpCaW9pbmZvcm1hdGljcyosIDIxKDE1KSwgMzMwMeKAkzMzMDcuDQoNCkphbWVzLCBHLiwgV2l0dGVuLCBELiwgSGFzdGllLCBULiwgJiBUaWJzaGlyYW5pLCBSLiAoMjAxMykuIEFuIGludHJvZHVjdGlvbiB0byBzdGF0aXN0aWNhbCBsZWFybmluZy4gTmV3IFlvcms6IFNwcmluZ2VyLg0KDQpLaW0gSkggKDIwMDkpLiBFc3RpbWF0aW5nIENsYXNzaWZpY2F0aW9uIEVycm9yIFJhdGU6IFJlcGVhdGVkIENyb3Nz4oCTIFZhbGlkYXRpb24sIFJlcGVhdGVkIEhvbGTigJNPdXQgYW5kIEJvb3RzdHJhcC4gKkNvbXB1dGF0aW9uYWwgU3RhdGlzdGljcyAmIERhdGEgQW5hbHlzaXMqLCA1MygxMSksIDM3MzXigJMzNzQ1Lg0KDQoNCg0KDQoNCg0KDQoNCg==