1 Giới thiệu

Lựa chọn mô hình và chọn lọc biến số là một công đoạn quan trọng trong các nghiên cứu diễn dịch sử dụng hồi quy logistic. Hiện nay các bạn có nhiều giải pháp cho vấn đề này, bao gồm Bayesian model averaging (BMA), Lasso, Ridge, elastic net, … Vào tháng 9 vừa qua, một vũ khí mới vừa được bổ sung vào danh sách này, đó là package DMRnet của 2 tác giả người Ba Lan là Agnieszka Prochenka-Sołtys và Piotr Pokarowski.

https://cran.r-project.org/web/packages/DMRnet/index.html

Package DMRnet áp dụng một algorithm chọn lọc mô hình mới,được 2 tác giả công bố năm 2015, có tên là « Delete or merge regressors », viết tắt là DMR.Phương pháp DMR có thể xem như một sự kết hợp giữa Lasso, phân tích cụm và AIC based backward Step-wise. Cơ chế chọn lọc của nó là ép hệ số hồi quy về zero cho biến liên tục và san bằng hệ số hồi quy giữa các level trong factor, dựa vào agglomerative clustering analysis của bình phương của trị số t trong t-test. Mô hình phù hợp nhất được chọn trong một tập hợp nhiều mô hình dựa vào BIC.

https://projecteuclid.org/download/pdfview_1/euclid.ejs/1440507392

http://www.jmlr.org/papers/volume16/pokarowski15a/pokarowski15a.pdf

2 Bài toán minh họa: Hồi quy logistic

Trong thí dụ minh họa này, Nhi sẽ thử dùng DRMnet package cho một bài toán hồi quy logistic, trên bộ số liệu Heart disease. Đây là 1 tập hợp dữ liệu của hàng trăm bệnh nhân từ 4 bệnh viện (Cleveland,Budapest,Long Beach and Zurich), mục tiêu là xây dựng một mô hình tiên lượng cho bệnh Tim mạch dựa vào 14 biến số bao gồm Tuổi, Giới tính, Triệu chứng Đau ngực, cholesterol, fasting blood sugar test, và stress test gồm nhịp tim và đoạn ST của ECG.

Mục tiêu của bài này là thử nghiệm phương pháp DMR và so sánh nó với phương pháp BMA

library(tidyverse)

va=read.table("https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.va.data", sep =",",na.strings="?",strip.white=TRUE, fill = TRUE)%>%as_tibble()
hu=read.table("https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.hungarian.data", sep =",",na.strings="?",strip.white=TRUE, fill = TRUE)%>%as_tibble()
sw=read.table("https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.switzerland.data", sep =",",na.strings="?",strip.white=TRUE, fill = TRUE)%>%as_tibble()
cl=read.table("https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data", sep =",",na.strings="?",strip.white=TRUE, fill = TRUE)%>%as_tibble()

df=rbind(va,hu,sw,cl)
names(df)=c("Age","Sex","ChestPain","RestBP","Chol","FBS","RestECG","MaxHR","CPETAgina","Oldpeak","Slope","CA","Thal","Class")

data=df[,-c(11,12,13)]%>%filter(.,Chol!=0)

data=na.omit(data)

data$Sex%<>%as.factor()%>%recode_factor(.,`0` = "Female", `1` = "Male")

data$ChestPain%<>%as.factor()%>%recode_factor(.,`1` = "Typical", `2` = "Atypical",`3` = "Non_aginal", `4` = "asymptomatic" )

data$FBS%<>%as.factor()%>%recode_factor(.,`0` = "No", `1` = "Yes")

data$RestECG%<>%as.factor()%>%recode_factor(.,`0` = "Normal", `1` = "Abnormal_ST",`2` = "LVHypertrophy")

data$CPETAgina%<>%as.factor()%>%recode_factor(.,`0` = "No", `1` = "Yes")

rm(cl,df,hu,sw,va)

data$Class%<>%as.factor()%>%recode_factor(.,`0` = "Negative", `1` = "Positive",`2` = "Positive", `3` = "Positive",`4` = "Positive")

data$Class%>%as.integer()->data$Outcome
data$Outcome=(data$Outcome-1)%>%as.factor()

data%>%head()%>%knitr::kable()
Age Sex ChestPain RestBP Chol FBS RestECG MaxHR CPETAgina Oldpeak Class Outcome
63 Male asymptomatic 140 260 No Abnormal_ST 112 Yes 3.0 Positive 1
44 Male asymptomatic 130 209 No Abnormal_ST 127 No 0.0 Negative 0
60 Male asymptomatic 132 218 No Abnormal_ST 140 Yes 1.5 Positive 1
55 Male asymptomatic 142 228 No Abnormal_ST 149 Yes 2.5 Positive 1
66 Male Non_aginal 110 213 Yes LVHypertrophy 99 Yes 1.3 Negative 0
65 Male asymptomatic 150 236 Yes Abnormal_ST 105 Yes 0.0 Positive 1

Ta chia dữ liệu thành 2 phần, một dùng để dựng mô hình (Train), một để kiểm định mô hình (Test)

library(caret)
set.seed(123)
idx<- createDataPartition(data$Class,p=100/661,list=FALSE)

trainset<- data[-idx,]
testset<- data[idx,]

3 Bayesian Model averaging (BMA)

Đầu tiên, Nhi dùng BMA để chọn lọc biến số và tạo ra một mô hình logistic Bayes với tham số “trung bình”

library(BMA)

bmalog=bic.glm(f=Outcome~.,data=trainset[,-11],glm.family ="binomial",OR=20)

imageplot.bma(bmalog,color = c("#fc024d", "#0294fc", "white"))

Theo kết quả này, mô hình tối ưu bao gồm 7 biến số là Age, Sex=Male, cả 3 level của biến ChestPain,đau ngực khởi phát do stress test và Oldpeak.

Mô hình logistic trung bình có nội dung như sau:

bmalog$label[1]
## [1] "Age,Sex,ChestPain,CPETAgina,Oldpeak"
data.frame(Mean=bmalog$postmean, 
           Odds_Ratio=exp(bmalog$postmean),
           LL=exp(bmalog$postmean-1.645*bmalog$postsd),
           UL=exp(bmalog$postmean+1.645*bmalog$postsd)
           )%>%as.matrix()
##                                Mean Odds_Ratio           LL         UL
## (Intercept)           -4.5377934363 0.01069698 0.0003339521  0.3426404
## Age                    0.0305915709 1.03106430 0.9955446694  1.0678512
## SexMale                1.4118626754 4.10359195 2.5269761592  6.6638804
## ChestPainAtypical     -0.1139227996 0.89232684 0.3609812069  2.2057857
## ChestPainNon_aginal    0.2239545189 1.25101412 0.5346994653  2.9269458
## ChestPainasymptomatic  1.6047503837 4.97661720 2.1446131308 11.5483387
## RestBP                 0.0004471589 1.00044726 0.9962887051  1.0046232
## Chol                   0.0015983717 1.00159965 0.9975767629  1.0056388
## FBSYes                 0.1184140551 1.12571012 0.7000205408  1.8102658
## RestECGAbnormal_ST     0.0000000000 1.00000000 1.0000000000  1.0000000
## RestECGLVHypertrophy   0.0000000000 1.00000000 1.0000000000  1.0000000
## MaxHR                 -0.0041294169 0.99587910 0.9853032937  1.0065684
## CPETAginaYes           1.2299900971 3.42119566 2.2040510093  5.3104850
## Oldpeak                0.7082936002 2.03052342 1.6269597549  2.5341901
data.frame(Mean=bmalog$postmean, 
           Odds_Ratio=exp(bmalog$postmean),
           LL=exp(bmalog$postmean-1.645*bmalog$postsd),
           UL=exp(bmalog$postmean+1.645*bmalog$postsd),
           Features=row.names(as.matrix(bmalog$postmean))
           )%>%ggplot(aes(x=reorder(Features,Odds_Ratio),color=reorder(Features,Odds_Ratio)))+
  geom_errorbar(aes(ymin=LL, ymax=UL),width=0,size=1,show.legend = F)+
  geom_point(aes(y=Odds_Ratio,fill=reorder(Features,Odds_Ratio)),shape=21,size=4,stroke=1.5,show.legend = F)+
  theme_bw()+coord_flip()+
  geom_hline(yintercept = 1,size=1,color="blue",linetype=2)+
  scale_x_discrete("Predictors")+scale_y_continuous("Odds-Ratio")

4 Sử dụng phương pháp DRMnet

Cú pháp của packae DMRnet tương tự như package glmnet, thậm chí nó còn giản dị hơn khi tiếp nhận thẳng factor mà không cần chuyển dạng chúng thành dummy variables, DMRnet cũng chấp nhận outcome Y là string factor và dataframe cho X. Theo mặc định, phương pháp clustering là “complete” (xem package hclust để biết thêm chi tiết)

library(DMRnet)

fit=DMRnet(X=trainset[,-c(11,12)],
           y=trainset$Class,
           clust.method = "complete",
           family="binomial")

g=gic.DMR(fit,c=2)

Kết quả của quy trình là 1 object thuộc class S3, ta có thể khai thác kết quả như sau:

Khi xuất nội dung, kết quả cũng cho ra 14 mô hình tương ứng với df giảm dần từ 14 đến 1, loglikelihood của từng mô hình

fit
## 
## Call:  DMRnet4glm(X = X, y = y, clust.method = clust.method, o = o,      nlambda = nlambda, lam = lam, maxp = maxp) 
## 
##       Df    loglik
##  [1,] 14 -233.3851
##  [2,] 13 -233.3919
##  [3,] 12 -233.4065
##  [4,] 11 -233.5668
##  [5,] 10 -234.0303
##  [6,]  9 -234.4586
##  [7,]  8 -235.7925
##  [8,]  7 -237.7911
##  [9,]  6 -240.4893
## [10,]  5 -246.3607
## [11,]  4 -258.9228
## [12,]  3 -273.8714
## [13,]  2 -310.6886
## [14,]  1 -387.4621

Hàm plot cho thấy giá trị Coefficients cho 14 biến số ở từng mô hình,

plot(fit)

Vì đây là một hình vẽ khá xấu, nên Nhi quyết định vẽ lại thủ công kết quả dưới dạng heatmap tương tự như biểu đồ của BMA. Hình vẽ được thực hiện bằng ggplot2 như sau:

Đầu tiên Nhi trích xuất matrix kết quả của object fit, sau đó tạo 1 variable danh sách features trong mode, và vẽ heatmap

matdf=as.tibble(fit$beta)
colnames(matdf)=c(1:14)
matdf$Features=row.names(fit$beta)
matdf$Features[1]<-"Intercept"

matdf%>%gather(`1`,`2`:`14`,key="model",value="Coef")%>%mutate(.,model=as.integer(.$model))%>%
  ggplot(aes(x=model,y=reorder(Features,abs(Coef)),fill=Coef))+
  geom_tile()+
  theme_bw()+
  scale_fill_gradient2(low="blue",high="red",mid="white",midpoint = 0)+
  theme_bw(8)+theme(axis.text.x = element_text(angle =45, hjust = 1))+
  scale_x_continuous(breaks = c(1:14))

Ta có thể xem hệ số hồi quy cho 14 mô hình như sau:

coef(fit)
##                               df14         df13         df12         df11
## (Intercept)           -5.011008311 -5.090108004 -5.086206095 -5.215491590
## SexMale                1.456151206  1.457672910  1.460231739  1.461331563
## Age                    0.025862633  0.026023784  0.026143227  0.027946650
## ChestPainAtypical     -0.079645820  0.000000000  0.000000000  0.000000000
## ChestPainNon_aginal    0.271738206  0.329790927  0.329311247  0.335330492
## ChestPainasymptomatic  1.617341052  1.677372391  1.678790513  1.681228958
## RestBP                 0.006702257  0.006751477  0.006934164  0.006663577
## Chol                   0.004265506  0.004249570  0.004239210  0.004351643
## FBSYes                 0.493958002  0.496821401  0.501798855  0.508675177
## RestECGAbnormal_ST     0.068147170  0.066377805  0.000000000  0.000000000
## RestECGLVHypertrophy   0.162958413  0.166375033  0.157176750  0.000000000
## MaxHR                 -0.010407071 -0.010377886 -0.010547515 -0.010016071
## CPETAginaYes           1.136248035  1.133180749  1.134493125  1.128106149
## Oldpeak                0.691767182  0.696434529  0.694629791  0.700835516
##                               df10          df9          df8          df7
## (Intercept)           -4.524948070 -4.414619538 -4.582719933 -6.511942492
## SexMale                1.458610034  1.459707579  1.495185505  1.520196417
## Age                    0.030495270  0.030979273  0.036359229  0.044358085
## ChestPainAtypical      0.000000000  0.000000000  0.000000000  0.000000000
## ChestPainNon_aginal    0.303270389  0.000000000  0.000000000  0.000000000
## ChestPainasymptomatic  1.647286313  1.500426760  1.470748053  1.563566734
## RestBP                 0.000000000  0.000000000  0.000000000  0.000000000
## Chol                   0.004547111  0.004538169  0.004578722  0.004437927
## FBSYes                 0.536888878  0.537600099  0.000000000  0.000000000
## RestECGAbnormal_ST     0.000000000  0.000000000  0.000000000  0.000000000
## RestECGLVHypertrophy   0.000000000  0.000000000  0.000000000  0.000000000
## MaxHR                 -0.009890514 -0.009837522 -0.010174754  0.000000000
## CPETAginaYes           1.152429086  1.161800645  1.146581723  1.247419674
## Oldpeak                0.707477296  0.709378533  0.706120474  0.705632380
##                               df6        df5        df4        df3
## (Intercept)           -5.29100131 -3.0025365 -1.9838445 -1.8716210
## SexMale                1.40601207  1.3358135  0.0000000  0.0000000
## Age                    0.04353632  0.0000000  0.0000000  0.0000000
## ChestPainAtypical      0.00000000  0.0000000  0.0000000  0.0000000
## ChestPainNon_aginal    0.00000000  0.0000000  0.0000000  0.0000000
## ChestPainasymptomatic  1.58133181  1.5491710  1.5897116  1.9633800
## RestBP                 0.00000000  0.0000000  0.0000000  0.0000000
## Chol                   0.00000000  0.0000000  0.0000000  0.0000000
## FBSYes                 0.00000000  0.0000000  0.0000000  0.0000000
## RestECGAbnormal_ST     0.00000000  0.0000000  0.0000000  0.0000000
## RestECGLVHypertrophy   0.00000000  0.0000000  0.0000000  0.0000000
## MaxHR                  0.00000000  0.0000000  0.0000000  0.0000000
## CPETAginaYes           1.25823706  1.3657882  1.3526843  0.0000000
## Oldpeak                0.70401742  0.7665069  0.7500592  0.9315353
##                             df2        df1
## (Intercept)           -1.261202 -0.1000835
## SexMale                0.000000  0.0000000
## Age                    0.000000  0.0000000
## ChestPainAtypical      0.000000  0.0000000
## ChestPainNon_aginal    0.000000  0.0000000
## ChestPainasymptomatic  2.261186  0.0000000
## RestBP                 0.000000  0.0000000
## Chol                   0.000000  0.0000000
## FBSYes                 0.000000  0.0000000
## RestECGAbnormal_ST     0.000000  0.0000000
## RestECGLVHypertrophy   0.000000  0.0000000
## MaxHR                  0.000000  0.0000000
## CPETAginaYes           0.000000  0.0000000
## Oldpeak                0.000000  0.0000000

Bạn thử so sánh kết quả trung bình, trung vị của 14 mô hình này với BMA nhé…

data.frame(Mean=apply(fit$beta,1,mean),
           Median=apply(fit$beta,1,median),
           BMA=bmalog$postmean)%>%as.matrix()%>%knitr::kable()
Mean Median BMA
(Intercept) -3.8533809 -4.5538340 -4.5377934
SexMale 1.0364938 1.4569121 0.0305916
Age 0.0208360 0.0260835 1.4118627
ChestPainAtypical -0.0056890 0.0000000 -0.1139228
ChestPainNon_aginal 0.1121029 0.0000000 0.2239545
ChestPainasymptomatic 1.5558244 1.6035263 1.6047504
RestBP 0.0019322 0.0000000 0.0004472
Chol 0.0025148 0.0042444 0.0015984
FBSYes 0.2196959 0.0000000 0.1184141
RestECGAbnormal_ST 0.0096089 0.0000000 0.0000000
RestECGLVHypertrophy 0.0347507 0.0000000 0.0000000
MaxHR -0.0050894 -0.0049188 -0.0041294
CPETAginaYes 0.9440692 1.1414149 1.2299901
Oldpeak 0.6260282 0.7048249 0.7082936

Bây giờ ta thử tính BIC cho từng mô hình (trị số này trong RRMnet gọi là GIC)

plot(g)

data.frame(Coef=coef(fit, df=g$df.min),
           OR=exp(coef(fit, df=g$df.min)))%>%as.matrix()
##                               Coef          OR
## (Intercept)           -6.511942492 0.001485591
## SexMale                1.520196417 4.573123344
## Age                    0.044358085 1.045356615
## ChestPainAtypical      0.000000000 1.000000000
## ChestPainNon_aginal    0.000000000 1.000000000
## ChestPainasymptomatic  1.563566734 4.775825003
## RestBP                 0.000000000 1.000000000
## Chol                   0.004437927 1.004447789
## FBSYes                 0.000000000 1.000000000
## RestECGAbnormal_ST     0.000000000 1.000000000
## RestECGLVHypertrophy   0.000000000 1.000000000
## MaxHR                  0.000000000 1.000000000
## CPETAginaYes           1.247419674 3.481348345
## Oldpeak                0.705632380 2.025126930

Nếu dựa vào BIC, mô hình được chọn có 7 biến (df=7), vì BIC của nó thấp nhất. Nội dung của model này gồm Sex=Male, Age,1 level của biến ChestPain, cholesterol,đau ngực do stress test và OldPeak. Như vậy tập hợp này hoàn toàn khác so vớimô hình tối ưu theo BMA. Nguyên nhân vì DMR có khuynh hướng loại bỏ (merging) các level của 1 factor.

Liệu đây có phải là mô hình tối ưu hay không ? Ta sẽ kiểm định lại dựa vào test set:

pred=predict(fit,new=testset[,-c(11,12)],  type = "response",df=g$df.min)

label=ifelse(pred>0.5,"Positive","Negative")

confusionMatrix(label,testset$Class,positive="Positive")
## Warning in confusionMatrix.default(label, testset$Class, positive =
## "Positive"): Levels are not in the same order for reference and data.
## Refactoring data to match.
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Negative Positive
##   Negative        0        0
##   Positive       53       48
##                                          
##                Accuracy : 0.4752         
##                  95% CI : (0.3749, 0.577)
##     No Information Rate : 0.5248         
##     P-Value [Acc > NIR] : 0.8634         
##                                          
##                   Kappa : 0              
##  Mcnemar's Test P-Value : 9.148e-13      
##                                          
##             Sensitivity : 1.0000         
##             Specificity : 0.0000         
##          Pos Pred Value : 0.4752         
##          Neg Pred Value :    NaN         
##              Prevalence : 0.4752         
##          Detection Rate : 0.4752         
##    Detection Prevalence : 1.0000         
##       Balanced Accuracy : 0.5000         
##                                          
##        'Positive' Class : Positive       
## 

Kết quả kiểm định cho thấy mô hình mà DRMnet chọn tự động rất tồi tệ, nó phân loại toàn bộ bệnh nhân là Positive. Do đó tỉ lệ False positive của mô hình rất cao…

Với độ chính xác chỉ có 50%, ta chỉ dùng một đồng xu cũng có thể chẩn đoán tốt hơn như thế này.

Ta thử chọn một mô hình khác, có df=4

pred=predict(fit,new=testset[,-c(11,12)],  type = "response",df=4)

label=ifelse(pred>0.5,"Positive","Negative")

confusionMatrix(label,testset$Class,positive="Positive")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Negative Positive
##   Negative       42        7
##   Positive       11       41
##                                          
##                Accuracy : 0.8218         
##                  95% CI : (0.733, 0.8908)
##     No Information Rate : 0.5248         
##     P-Value [Acc > NIR] : 4.239e-10      
##                                          
##                   Kappa : 0.6441         
##  Mcnemar's Test P-Value : 0.4795         
##                                          
##             Sensitivity : 0.8542         
##             Specificity : 0.7925         
##          Pos Pred Value : 0.7885         
##          Neg Pred Value : 0.8571         
##              Prevalence : 0.4752         
##          Detection Rate : 0.4059         
##    Detection Prevalence : 0.5149         
##       Balanced Accuracy : 0.8233         
##                                          
##        'Positive' Class : Positive       
## 

Phẩm chất mô hình này khá hơn nhiều. Vậy Mô hình có df=3 thì sao ?

pred=predict(fit,new=testset[,-c(11,12)],  type = "response",df=3)

label=ifelse(pred>0.5,"Positive","Negative")

confusionMatrix(label,testset$Class,positive="Positive")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Negative Positive
##   Negative       40        3
##   Positive       13       45
##                                           
##                Accuracy : 0.8416          
##                  95% CI : (0.7555, 0.9067)
##     No Information Rate : 0.5248          
##     P-Value [Acc > NIR] : 2.131e-11       
##                                           
##                   Kappa : 0.6855          
##  Mcnemar's Test P-Value : 0.02445         
##                                           
##             Sensitivity : 0.9375          
##             Specificity : 0.7547          
##          Pos Pred Value : 0.7759          
##          Neg Pred Value : 0.9302          
##              Prevalence : 0.4752          
##          Detection Rate : 0.4455          
##    Detection Prevalence : 0.5743          
##       Balanced Accuracy : 0.8461          
##                                           
##        'Positive' Class : Positive        
## 

Phẩm chất mô hình này khá hơn nữa, chỉ còn 3 cases false negative

Ta thử dùng 1 mô hình với tham số hồi quy trung bình được xuất ra từ kết quả BMA :

fit2=fit

fit2$beta[,14]<-as.vector(bmalog$postmean)

coef(fit2, df=1)%>%as.matrix()
##                                [,1]
## (Intercept)           -4.5377934363
## SexMale                0.0305915709
## Age                    1.4118626754
## ChestPainAtypical     -0.1139227996
## ChestPainNon_aginal    0.2239545189
## ChestPainasymptomatic  1.6047503837
## RestBP                 0.0004471589
## Chol                   0.0015983717
## FBSYes                 0.1184140551
## RestECGAbnormal_ST     0.0000000000
## RestECGLVHypertrophy   0.0000000000
## MaxHR                 -0.0041294169
## CPETAginaYes           1.2299900971
## Oldpeak                0.7082936002
pred=predict(fit2,new=testset[,-c(11,12)],  type = "response",df=1)

label=ifelse(pred>0.3,"Positive","Negative")

confusionMatrix(label,testset$Class,positive="Positive")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Negative Positive
##   Negative       38        1
##   Positive       15       47
##                                           
##                Accuracy : 0.8416          
##                  95% CI : (0.7555, 0.9067)
##     No Information Rate : 0.5248          
##     P-Value [Acc > NIR] : 2.131e-11       
##                                           
##                   Kappa : 0.6867          
##  Mcnemar's Test P-Value : 0.001154        
##                                           
##             Sensitivity : 0.9792          
##             Specificity : 0.7170          
##          Pos Pred Value : 0.7581          
##          Neg Pred Value : 0.9744          
##              Prevalence : 0.4752          
##          Detection Rate : 0.4653          
##    Detection Prevalence : 0.6139          
##       Balanced Accuracy : 0.8481          
##                                           
##        'Positive' Class : Positive        
## 

Mô hình này có phẩm chất tốt hơn cả, với BAC = 0.842, chỉ có 1 case false negative

5 Kết luận

Phương pháp DMRnet là một lựa chọn mới thay thế cho BMA và Lasso. Nó thân thiện và dễ sử dụng hơn Lasso trong glmnet package. Tuy nhiên việc san bằng hệ số hồi quy cho các factor level của MRMnet dựa vào clustering cũng như việc chọn mô hình dựa vào BIC còn phải bàn cãi. Mô hình có BIC tối ưu chưa hẳn là mô hình chính xác. Nếu xét về phẩm chất mô hình tiên lượng thì DMRnet vẫn còn kém xa so với BMA.

LS0tDQp0aXRsZTogIlBhY2thZ2UgRE1SbmV0Ig0Kc3VidGl0bGU6ICJN4buZdCBnaeG6o2kgcGjDoXAgbeG7m2kgxJHhu4MgY2jhu41uIGzhu41jIG3DtCBow6xuaCINCmF1dGhvcjogIkzDqiBOZ+G7jWMgS2jhuqMgTmhpIg0KZGF0ZTogIjI4IFRow6FuZyAxMSAyMDE3Ig0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImRlZmF1bHQiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpgYGANCg0KIVtdKERSTW5ldDIucG5nKQ0KDQojIEdp4bubaSB0aGnhu4d1DQoNCkzhu7FhIGNo4buNbiBtw7QgaMOsbmggdsOgIGNo4buNbiBs4buNYyBiaeG6v24gc+G7kSBsw6AgbeG7mXQgY8O0bmcgxJFv4bqhbiBxdWFuIHRy4buNbmcgdHJvbmcgY8OhYyBuZ2hpw6puIGPhu6l1IGRp4buFbiBk4buLY2ggc+G7rSBk4bulbmcgaOG7k2kgcXV5IGxvZ2lzdGljLiBIaeG7h24gbmF5IGPDoWMgYuG6oW4gY8OzIG5oaeG7gXUgZ2nhuqNpIHBow6FwIGNobyB24bqlbiDEkeG7gSBuw6B5LCBiYW8gZ+G7k20gQmF5ZXNpYW4gbW9kZWwgYXZlcmFnaW5nIChCTUEpLCBMYXNzbywgUmlkZ2UsIGVsYXN0aWMgbmV0LCDigKYgVsOgbyB0aMOhbmcgOSB24burYSBxdWEsIG3hu5l0IHbFqSBraMOtIG3hu5tpIHbhu6thIMSRxrDhu6NjIGLhu5Ugc3VuZyB2w6BvIGRhbmggc8OhY2ggbsOgeSwgxJHDsyBsw6AgcGFja2FnZSBETVJuZXQgY+G7p2EgMiB0w6FjIGdp4bqjIG5nxrDhu51pIEJhIExhbiBsw6AgQWduaWVzemthIFByb2NoZW5rYS1Tb8WCdHlzIHbDoCBQaW90ciBQb2thcm93c2tpLg0KDQpodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvRE1SbmV0L2luZGV4Lmh0bWwNCg0KUGFja2FnZSBETVJuZXQgw6FwIGThu6VuZyBt4buZdCBhbGdvcml0aG0gY2jhu41uIGzhu41jIG3DtCBow6xuaCBt4bubaSzEkcaw4bujYyAyIHTDoWMgZ2nhuqMgY8O0bmcgYuG7kSBuxINtIDIwMTUsIGPDsyB0w6puIGzDoCDCqyBEZWxldGUgb3IgbWVyZ2UgcmVncmVzc29ycyDCuywgdmnhur90IHThuq90IGzDoCBETVIuUGjGsMahbmcgcGjDoXAgRE1SIGPDsyB0aOG7gyB4ZW0gbmjGsCBt4buZdCBz4buxIGvhur90IGjhu6NwIGdp4buvYSBMYXNzbywgcGjDom4gdMOtY2ggY+G7pW0gdsOgIEFJQyBiYXNlZCBiYWNrd2FyZCBTdGVwLXdpc2UuIEPGoSBjaOG6vyBjaOG7jW4gbOG7jWMgY+G7p2EgbsOzIGzDoCDDqXAgaOG7hyBz4buRIGjhu5NpIHF1eSB24buBIHplcm8gY2hvIGJp4bq/biBsacOqbiB04bulYyB2w6Agc2FuIGLhurFuZyBo4buHIHPhu5EgaOG7k2kgcXV5IGdp4buvYSBjw6FjIGxldmVsIHRyb25nIGZhY3RvciwgZOG7sWEgdsOgbyBhZ2dsb21lcmF0aXZlIGNsdXN0ZXJpbmcgYW5hbHlzaXMgY+G7p2EgYsOsbmggcGjGsMahbmcgY+G7p2EgdHLhu4sgc+G7kSB0IHRyb25nIHQtdGVzdC4gTcO0IGjDrG5oIHBow7kgaOG7o3AgbmjhuqV0IMSRxrDhu6NjIGNo4buNbiB0cm9uZyBt4buZdCB04bqtcCBo4bujcCBuaGnhu4F1IG3DtCBow6xuaCBk4buxYSB2w6BvIEJJQy4NCg0KaHR0cHM6Ly9wcm9qZWN0ZXVjbGlkLm9yZy9kb3dubG9hZC9wZGZ2aWV3XzEvZXVjbGlkLmVqcy8xNDQwNTA3MzkyDQoNCmh0dHA6Ly93d3cuam1sci5vcmcvcGFwZXJzL3ZvbHVtZTE2L3Bva2Fyb3dza2kxNWEvcG9rYXJvd3NraTE1YS5wZGYNCg0KDQojIELDoGkgdG/DoW4gbWluaCBo4buNYTogSOG7k2kgcXV5IGxvZ2lzdGljDQoNClRyb25nIHRow60gZOG7pSAgbWluaCBo4buNYSBuw6B5LCBOaGkgc+G6vSB0aOG7rSBkw7luZyBEUk1uZXQgcGFja2FnZSBjaG8gbeG7mXQgYsOgaSB0b8OhbiBo4buTaSBxdXkgbG9naXN0aWMsIHRyw6puIGLhu5kgc+G7kSBsaeG7h3UgSGVhcnQgZGlzZWFzZS4gxJDDonkgbMOgIDEgdOG6rXAgaOG7o3AgZOG7ryBsaeG7h3UgY+G7p2EgaMOgbmcgdHLEg20gYuG7h25oIG5ow6JuIHThu6sgNCBi4buHbmggdmnhu4duIChDbGV2ZWxhbmQsQnVkYXBlc3QsTG9uZyBCZWFjaCBhbmQgWnVyaWNoKSwgbeG7pWMgdGnDqnUgbMOgIHjDonkgZOG7sW5nIG3hu5l0IG3DtCBow6xuaCB0acOqbiBsxrDhu6NuZyBjaG8gYuG7h25oIFRpbSBt4bqhY2ggZOG7sWEgdsOgbyAxNCBiaeG6v24gc+G7kSBiYW8gZ+G7k20gVHXhu5VpLCBHaeG7m2kgdMOtbmgsIFRyaeG7h3UgY2jhu6luZyDEkGF1IG5n4buxYywgY2hvbGVzdGVyb2wsIGZhc3RpbmcgYmxvb2Qgc3VnYXIgdGVzdCwgdsOgIHN0cmVzcyB0ZXN0IGfhu5NtIG5o4buLcCB0aW0gdsOgIMSRb+G6oW4gU1QgY+G7p2EgRUNHLg0KDQpN4bulYyB0acOqdSBj4bunYSBiw6BpIG7DoHkgbMOgIHRo4butIG5naGnhu4dtIHBoxrDGoW5nIHBow6FwIERNUiB2w6Agc28gc8OhbmggbsOzIHbhu5tpIHBoxrDGoW5nIHBow6FwIEJNQQ0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQp2YT1yZWFkLnRhYmxlKCJodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvbWFjaGluZS1sZWFybmluZy1kYXRhYmFzZXMvaGVhcnQtZGlzZWFzZS9wcm9jZXNzZWQudmEuZGF0YSIsIHNlcCA9IiwiLG5hLnN0cmluZ3M9Ij8iLHN0cmlwLndoaXRlPVRSVUUsIGZpbGwgPSBUUlVFKSU+JWFzX3RpYmJsZSgpDQpodT1yZWFkLnRhYmxlKCJodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvbWFjaGluZS1sZWFybmluZy1kYXRhYmFzZXMvaGVhcnQtZGlzZWFzZS9wcm9jZXNzZWQuaHVuZ2FyaWFuLmRhdGEiLCBzZXAgPSIsIixuYS5zdHJpbmdzPSI/IixzdHJpcC53aGl0ZT1UUlVFLCBmaWxsID0gVFJVRSklPiVhc190aWJibGUoKQ0Kc3c9cmVhZC50YWJsZSgiaHR0cHM6Ly9hcmNoaXZlLmljcy51Y2kuZWR1L21sL21hY2hpbmUtbGVhcm5pbmctZGF0YWJhc2VzL2hlYXJ0LWRpc2Vhc2UvcHJvY2Vzc2VkLnN3aXR6ZXJsYW5kLmRhdGEiLCBzZXAgPSIsIixuYS5zdHJpbmdzPSI/IixzdHJpcC53aGl0ZT1UUlVFLCBmaWxsID0gVFJVRSklPiVhc190aWJibGUoKQ0KY2w9cmVhZC50YWJsZSgiaHR0cHM6Ly9hcmNoaXZlLmljcy51Y2kuZWR1L21sL21hY2hpbmUtbGVhcm5pbmctZGF0YWJhc2VzL2hlYXJ0LWRpc2Vhc2UvcHJvY2Vzc2VkLmNsZXZlbGFuZC5kYXRhIiwgc2VwID0iLCIsbmEuc3RyaW5ncz0iPyIsc3RyaXAud2hpdGU9VFJVRSwgZmlsbCA9IFRSVUUpJT4lYXNfdGliYmxlKCkNCg0KZGY9cmJpbmQodmEsaHUsc3csY2wpDQpuYW1lcyhkZik9YygiQWdlIiwiU2V4IiwiQ2hlc3RQYWluIiwiUmVzdEJQIiwiQ2hvbCIsIkZCUyIsIlJlc3RFQ0ciLCJNYXhIUiIsIkNQRVRBZ2luYSIsIk9sZHBlYWsiLCJTbG9wZSIsIkNBIiwiVGhhbCIsIkNsYXNzIikNCg0KZGF0YT1kZlssLWMoMTEsMTIsMTMpXSU+JWZpbHRlciguLENob2whPTApDQoNCmRhdGE9bmEub21pdChkYXRhKQ0KDQpkYXRhJFNleCU8PiVhcy5mYWN0b3IoKSU+JXJlY29kZV9mYWN0b3IoLixgMGAgPSAiRmVtYWxlIiwgYDFgID0gIk1hbGUiKQ0KDQpkYXRhJENoZXN0UGFpbiU8PiVhcy5mYWN0b3IoKSU+JXJlY29kZV9mYWN0b3IoLixgMWAgPSAiVHlwaWNhbCIsIGAyYCA9ICJBdHlwaWNhbCIsYDNgID0gIk5vbl9hZ2luYWwiLCBgNGAgPSAiYXN5bXB0b21hdGljIiApDQoNCmRhdGEkRkJTJTw+JWFzLmZhY3RvcigpJT4lcmVjb2RlX2ZhY3RvciguLGAwYCA9ICJObyIsIGAxYCA9ICJZZXMiKQ0KDQpkYXRhJFJlc3RFQ0clPD4lYXMuZmFjdG9yKCklPiVyZWNvZGVfZmFjdG9yKC4sYDBgID0gIk5vcm1hbCIsIGAxYCA9ICJBYm5vcm1hbF9TVCIsYDJgID0gIkxWSHlwZXJ0cm9waHkiKQ0KDQpkYXRhJENQRVRBZ2luYSU8PiVhcy5mYWN0b3IoKSU+JXJlY29kZV9mYWN0b3IoLixgMGAgPSAiTm8iLCBgMWAgPSAiWWVzIikNCg0Kcm0oY2wsZGYsaHUsc3csdmEpDQoNCmRhdGEkQ2xhc3MlPD4lYXMuZmFjdG9yKCklPiVyZWNvZGVfZmFjdG9yKC4sYDBgID0gIk5lZ2F0aXZlIiwgYDFgID0gIlBvc2l0aXZlIixgMmAgPSAiUG9zaXRpdmUiLCBgM2AgPSAiUG9zaXRpdmUiLGA0YCA9ICJQb3NpdGl2ZSIpDQoNCmRhdGEkQ2xhc3MlPiVhcy5pbnRlZ2VyKCktPmRhdGEkT3V0Y29tZQ0KZGF0YSRPdXRjb21lPShkYXRhJE91dGNvbWUtMSklPiVhcy5mYWN0b3IoKQ0KDQpkYXRhJT4laGVhZCgpJT4la25pdHI6OmthYmxlKCkNCmBgYA0KDQpUYSBjaGlhIGThu68gbGnhu4d1IHRow6BuaCAyIHBo4bqnbiwgbeG7mXQgZMO5bmcgxJHhu4MgZOG7sW5nIG3DtCBow6xuaCAoVHJhaW4pLCBt4buZdCDEkeG7gyBraeG7g20gxJHhu4tuaCBtw7QgaMOsbmggKFRlc3QpDQoNCmBgYHtyLCxtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoY2FyZXQpDQpzZXQuc2VlZCgxMjMpDQppZHg8LSBjcmVhdGVEYXRhUGFydGl0aW9uKGRhdGEkQ2xhc3MscD0xMDAvNjYxLGxpc3Q9RkFMU0UpDQoNCnRyYWluc2V0PC0gZGF0YVstaWR4LF0NCnRlc3RzZXQ8LSBkYXRhW2lkeCxdDQpgYGANCg0KIyBCYXllc2lhbiBNb2RlbCBhdmVyYWdpbmcgKEJNQSkNCg0KxJDhuqd1IHRpw6puLCBOaGkgZMO5bmcgQk1BIMSR4buDIGNo4buNbiBs4buNYyBiaeG6v24gc+G7kSB2w6AgdOG6oW8gcmEgbeG7mXQgbcO0IGjDrG5oIGxvZ2lzdGljIEJheWVzIHbhu5tpIHRoYW0gc+G7kSAidHJ1bmcgYsOsbmgiDQoNCmBgYHtyLCxtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoQk1BKQ0KDQpibWFsb2c9YmljLmdsbShmPU91dGNvbWV+LixkYXRhPXRyYWluc2V0WywtMTFdLGdsbS5mYW1pbHkgPSJiaW5vbWlhbCIsT1I9MjApDQoNCmltYWdlcGxvdC5ibWEoYm1hbG9nLGNvbG9yID0gYygiI2ZjMDI0ZCIsICIjMDI5NGZjIiwgIndoaXRlIikpDQpgYGANCg0KVGhlbyBr4bq/dCBxdeG6oyBuw6B5LCBtw7QgaMOsbmggdOG7kWkgxrB1IGJhbyBn4buTbSA3IGJp4bq/biBz4buRIGzDoCBBZ2UsIFNleD1NYWxlLCBj4bqjIDMgbGV2ZWwgY+G7p2EgYmnhur9uIENoZXN0UGFpbizEkWF1IG5n4buxYyBraOG7n2kgcGjDoXQgZG8gc3RyZXNzIHRlc3QgdsOgIE9sZHBlYWsuIA0KDQpNw7QgaMOsbmggbG9naXN0aWMgdHJ1bmcgYsOsbmggY8OzIG7hu5lpIGR1bmcgbmjGsCBzYXU6DQoNCmBgYHtyfQ0KDQpibWFsb2ckbGFiZWxbMV0NCg0KZGF0YS5mcmFtZShNZWFuPWJtYWxvZyRwb3N0bWVhbiwgDQogICAgICAgICAgIE9kZHNfUmF0aW89ZXhwKGJtYWxvZyRwb3N0bWVhbiksDQogICAgICAgICAgIExMPWV4cChibWFsb2ckcG9zdG1lYW4tMS42NDUqYm1hbG9nJHBvc3RzZCksDQogICAgICAgICAgIFVMPWV4cChibWFsb2ckcG9zdG1lYW4rMS42NDUqYm1hbG9nJHBvc3RzZCkNCiAgICAgICAgICAgKSU+JWFzLm1hdHJpeCgpDQoNCmBgYA0KDQpgYGB7cn0NCmRhdGEuZnJhbWUoTWVhbj1ibWFsb2ckcG9zdG1lYW4sIA0KICAgICAgICAgICBPZGRzX1JhdGlvPWV4cChibWFsb2ckcG9zdG1lYW4pLA0KICAgICAgICAgICBMTD1leHAoYm1hbG9nJHBvc3RtZWFuLTEuNjQ1KmJtYWxvZyRwb3N0c2QpLA0KICAgICAgICAgICBVTD1leHAoYm1hbG9nJHBvc3RtZWFuKzEuNjQ1KmJtYWxvZyRwb3N0c2QpLA0KICAgICAgICAgICBGZWF0dXJlcz1yb3cubmFtZXMoYXMubWF0cml4KGJtYWxvZyRwb3N0bWVhbikpDQogICAgICAgICAgICklPiVnZ3Bsb3QoYWVzKHg9cmVvcmRlcihGZWF0dXJlcyxPZGRzX1JhdGlvKSxjb2xvcj1yZW9yZGVyKEZlYXR1cmVzLE9kZHNfUmF0aW8pKSkrDQogIGdlb21fZXJyb3JiYXIoYWVzKHltaW49TEwsIHltYXg9VUwpLHdpZHRoPTAsc2l6ZT0xLHNob3cubGVnZW5kID0gRikrDQogIGdlb21fcG9pbnQoYWVzKHk9T2Rkc19SYXRpbyxmaWxsPXJlb3JkZXIoRmVhdHVyZXMsT2Rkc19SYXRpbykpLHNoYXBlPTIxLHNpemU9NCxzdHJva2U9MS41LHNob3cubGVnZW5kID0gRikrDQogIHRoZW1lX2J3KCkrY29vcmRfZmxpcCgpKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAxLHNpemU9MSxjb2xvcj0iYmx1ZSIsbGluZXR5cGU9MikrDQogIHNjYWxlX3hfZGlzY3JldGUoIlByZWRpY3RvcnMiKStzY2FsZV95X2NvbnRpbnVvdXMoIk9kZHMtUmF0aW8iKQ0KYGBgDQoNCiMgU+G7rSBk4bulbmcgcGjGsMahbmcgcGjDoXAgRFJNbmV0DQoNCkPDuiBwaMOhcCBj4bunYSBwYWNrYWUgRE1SbmV0IHTGsMahbmcgdOG7sSBuaMawIHBhY2thZ2UgZ2xtbmV0LCB0aOG6rW0gY2jDrSBuw7MgY8OybiBnaeG6o24gZOG7iyBoxqFuIGtoaSB0aeG6v3Agbmjhuq1uIHRo4bqzbmcgZmFjdG9yIG3DoCBraMO0bmcgY+G6p24gY2h1eeG7g24gZOG6oW5nIGNow7puZyB0aMOgbmggZHVtbXkgdmFyaWFibGVzLCBETVJuZXQgY8WpbmcgY2jhuqVwIG5o4bqtbiBvdXRjb21lIFkgbMOgIHN0cmluZyBmYWN0b3IgdsOgIGRhdGFmcmFtZSBjaG8gWC4gVGhlbyBt4bq3YyDEkeG7i25oLCBwaMawxqFuZyBwaMOhcCBjbHVzdGVyaW5nIGzDoCAiY29tcGxldGUiICh4ZW0gcGFja2FnZSBoY2x1c3QgxJHhu4MgYmnhur90IHRow6ptIGNoaSB0aeG6v3QpDQoNCmBgYHtyLCxtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCg0KbGlicmFyeShETVJuZXQpDQoNCmZpdD1ETVJuZXQoWD10cmFpbnNldFssLWMoMTEsMTIpXSwNCiAgICAgICAgICAgeT10cmFpbnNldCRDbGFzcywNCiAgICAgICAgICAgY2x1c3QubWV0aG9kID0gImNvbXBsZXRlIiwNCiAgICAgICAgICAgZmFtaWx5PSJiaW5vbWlhbCIpDQoNCmc9Z2ljLkRNUihmaXQsYz0yKQ0KYGBgDQoNCkvhur90IHF14bqjIGPhu6dhIHF1eSB0csOsbmggbMOgIDEgb2JqZWN0IHRodeG7mWMgY2xhc3MgUzMsIHRhIGPDsyB0aOG7gyBraGFpIHRow6FjIGvhur90IHF14bqjIG5oxrAgc2F1Og0KDQpLaGkgeHXhuqV0IG7hu5lpIGR1bmcsIGvhur90IHF14bqjIGPFqW5nIGNobyByYSAxNCBtw7QgaMOsbmggdMawxqFuZyDhu6luZyB24bubaSBkZiBnaeG6o20gZOG6p24gdOG7qyAxNCDEkeG6v24gMSwgbG9nbGlrZWxpaG9vZCBj4bunYSB04burbmcgbcO0IGjDrG5oDQoNCmBgYHtyLCxtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmZpdA0KYGBgDQoNCkjDoG0gcGxvdCBjaG8gdGjhuqV5IGdpw6EgdHLhu4sgQ29lZmZpY2llbnRzIGNobyAxNCBiaeG6v24gc+G7kSDhu58gdOG7q25nIG3DtCBow6xuaCwNCg0KYGBge3J9DQpwbG90KGZpdCkNCg0KYGBgDQoNClbDrCDEkcOieSBsw6AgbeG7mXQgaMOsbmggduG6vSBraMOhIHjhuqV1LCBuw6puIE5oaSBxdXnhur90IMSR4buLbmggduG6vSBs4bqhaSB0aOG7pyBjw7RuZyBr4bq/dCBxdeG6oyBkxrDhu5tpIGThuqFuZyBoZWF0bWFwIHTGsMahbmcgdOG7sSBuaMawIGJp4buDdSDEkeG7kyBj4bunYSBCTUEuIEjDrG5oIHbhur0gxJHGsOG7o2MgdGjhu7FjIGhp4buHbiBi4bqxbmcgZ2dwbG90MiBuaMawIHNhdTogDQoNCsSQ4bqndSB0acOqbiBOaGkgdHLDrWNoIHh14bqldCBtYXRyaXgga+G6v3QgcXXhuqMgY+G7p2Egb2JqZWN0IGZpdCwgc2F1IMSRw7MgdOG6oW8gMSB2YXJpYWJsZSBkYW5oIHPDoWNoIGZlYXR1cmVzIHRyb25nIG1vZGUsIHbDoCB24bq9IGhlYXRtYXANCg0KYGBge3J9DQptYXRkZj1hcy50aWJibGUoZml0JGJldGEpDQpjb2xuYW1lcyhtYXRkZik9YygxOjE0KQ0KbWF0ZGYkRmVhdHVyZXM9cm93Lm5hbWVzKGZpdCRiZXRhKQ0KbWF0ZGYkRmVhdHVyZXNbMV08LSJJbnRlcmNlcHQiDQoNCm1hdGRmJT4lZ2F0aGVyKGAxYCxgMmA6YDE0YCxrZXk9Im1vZGVsIix2YWx1ZT0iQ29lZiIpJT4lbXV0YXRlKC4sbW9kZWw9YXMuaW50ZWdlciguJG1vZGVsKSklPiUNCiAgZ2dwbG90KGFlcyh4PW1vZGVsLHk9cmVvcmRlcihGZWF0dXJlcyxhYnMoQ29lZikpLGZpbGw9Q29lZikpKw0KICBnZW9tX3RpbGUoKSsNCiAgdGhlbWVfYncoKSsNCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIobG93PSJibHVlIixoaWdoPSJyZWQiLG1pZD0id2hpdGUiLG1pZHBvaW50ID0gMCkrDQogIHRoZW1lX2J3KDgpK3RoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID00NSwgaGp1c3QgPSAxKSkrDQogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSBjKDE6MTQpKQ0KYGBgDQoNClRhIGPDsyB0aOG7gyB4ZW0gaOG7hyBz4buRIGjhu5NpIHF1eSBjaG8gMTQgbcO0IGjDrG5oIG5oxrAgc2F1Og0KDQpgYGB7cn0NCmNvZWYoZml0KQ0KYGBgDQoNCkLhuqFuIHRo4butIHNvIHPDoW5oIGvhur90IHF14bqjIHRydW5nIGLDrG5oLCB0cnVuZyB24buLIGPhu6dhIDE0IG3DtCBow6xuaCBuw6B5IHbhu5tpIEJNQSBuaMOpLi4uDQoNCmBgYHtyfQ0KZGF0YS5mcmFtZShNZWFuPWFwcGx5KGZpdCRiZXRhLDEsbWVhbiksDQogICAgICAgICAgIE1lZGlhbj1hcHBseShmaXQkYmV0YSwxLG1lZGlhbiksDQogICAgICAgICAgIEJNQT1ibWFsb2ckcG9zdG1lYW4pJT4lYXMubWF0cml4KCklPiVrbml0cjo6a2FibGUoKQ0KDQpgYGANCg0KQsOieSBnaeG7nSB0YSB0aOG7rSB0w61uaCBCSUMgY2hvIHThu6tuZyBtw7QgaMOsbmggKHRy4buLIHPhu5EgbsOgeSB0cm9uZyBSUk1uZXQgZ+G7jWkgbMOgIEdJQykNCg0KYGBge3J9DQoNCnBsb3QoZykNCg0KZGF0YS5mcmFtZShDb2VmPWNvZWYoZml0LCBkZj1nJGRmLm1pbiksDQogICAgICAgICAgIE9SPWV4cChjb2VmKGZpdCwgZGY9ZyRkZi5taW4pKSklPiVhcy5tYXRyaXgoKQ0KYGBgDQoNCk7hur91IGThu7FhIHbDoG8gQklDLCBtw7QgaMOsbmggxJHGsOG7o2MgY2jhu41uIGPDsyA3IGJp4bq/biAoZGY9NyksIHbDrCBCSUMgY+G7p2EgbsOzIHRo4bqlcCBuaOG6pXQuIE7hu5lpIGR1bmcgY+G7p2EgbW9kZWwgbsOgeSBn4buTbSBTZXg9TWFsZSwgQWdlLDEgbGV2ZWwgY+G7p2EgYmnhur9uIENoZXN0UGFpbiwgY2hvbGVzdGVyb2wsxJFhdSBuZ+G7sWMgZG8gc3RyZXNzIHRlc3QgdsOgIE9sZFBlYWsuIE5oxrAgduG6rXkgdOG6rXAgaOG7o3AgbsOgeSBob8OgbiB0b8OgbiBraMOhYyBzbyB24bubaW3DtCBow6xuaCB04buRaSDGsHUgdGhlbyBCTUEuIE5ndXnDqm4gbmjDom4gdsOsIERNUiBjw7Mga2h1eW5oIGjGsOG7m25nIGxv4bqhaSBi4buPIChtZXJnaW5nKSBjw6FjIGxldmVsIGPhu6dhIDEgZmFjdG9yLiANCg0KTGnhu4d1IMSRw6J5IGPDsyBwaOG6o2kgbMOgIG3DtCBow6xuaCB04buRaSDGsHUgaGF5IGtow7RuZyA/IFRhIHPhur0ga2nhu4NtIMSR4buLbmggbOG6oWkgZOG7sWEgdsOgbyB0ZXN0IHNldDoNCg0KYGBge3J9DQpwcmVkPXByZWRpY3QoZml0LG5ldz10ZXN0c2V0WywtYygxMSwxMildLCAgdHlwZSA9ICJyZXNwb25zZSIsZGY9ZyRkZi5taW4pDQoNCmxhYmVsPWlmZWxzZShwcmVkPjAuNSwiUG9zaXRpdmUiLCJOZWdhdGl2ZSIpDQoNCmNvbmZ1c2lvbk1hdHJpeChsYWJlbCx0ZXN0c2V0JENsYXNzLHBvc2l0aXZlPSJQb3NpdGl2ZSIpDQpgYGANCg0KS+G6v3QgcXXhuqMga2nhu4NtIMSR4buLbmggY2hvIHRo4bqleSBtw7QgaMOsbmggbcOgIERSTW5ldCBjaOG7jW4gdOG7sSDEkeG7mW5nIHLhuqV0IHThu5NpIHThu4csIG7DsyBwaMOibiBsb+G6oWkgdG/DoG4gYuG7mSBi4buHbmggbmjDom4gbMOgIFBvc2l0aXZlLiBEbyDEkcOzIHThu4kgbOG7hyBGYWxzZSBwb3NpdGl2ZSBj4bunYSBtw7QgaMOsbmggcuG6pXQgY2FvLi4uDQoNClbhu5tpIMSR4buZIGNow61uaCB4w6FjIGNo4buJIGPDsyA1MCUsIHRhIGNo4buJIGTDuW5nIG3hu5l0IMSR4buTbmcgeHUgY8WpbmcgY8OzIHRo4buDIGNo4bqpbiDEkW/DoW4gdOG7kXQgaMahbiBuaMawIHRo4bq/IG7DoHkuDQoNClRhIHRo4butIGNo4buNbiBt4buZdCBtw7QgaMOsbmgga2jDoWMsIGPDsyBkZj00DQoNCmBgYHtyfQ0KcHJlZD1wcmVkaWN0KGZpdCxuZXc9dGVzdHNldFssLWMoMTEsMTIpXSwgIHR5cGUgPSAicmVzcG9uc2UiLGRmPTQpDQoNCmxhYmVsPWlmZWxzZShwcmVkPjAuNSwiUG9zaXRpdmUiLCJOZWdhdGl2ZSIpDQoNCmNvbmZ1c2lvbk1hdHJpeChsYWJlbCx0ZXN0c2V0JENsYXNzLHBvc2l0aXZlPSJQb3NpdGl2ZSIpDQpgYGANCg0KUGjhuqltIGNo4bqldCBtw7QgaMOsbmggbsOgeSBraMOhIGjGoW4gbmhp4buBdS4gVuG6rXkgTcO0IGjDrG5oIGPDsyBkZj0zIHRow6wgc2FvID8NCg0KYGBge3J9DQpwcmVkPXByZWRpY3QoZml0LG5ldz10ZXN0c2V0WywtYygxMSwxMildLCAgdHlwZSA9ICJyZXNwb25zZSIsZGY9MykNCg0KbGFiZWw9aWZlbHNlKHByZWQ+MC41LCJQb3NpdGl2ZSIsIk5lZ2F0aXZlIikNCg0KY29uZnVzaW9uTWF0cml4KGxhYmVsLHRlc3RzZXQkQ2xhc3MscG9zaXRpdmU9IlBvc2l0aXZlIikNCmBgYA0KDQpQaOG6qW0gY2jhuqV0IG3DtCBow6xuaCBuw6B5IGtow6EgaMahbiBu4buvYSwgY2jhu4kgY8OybiAzIGNhc2VzIGZhbHNlIG5lZ2F0aXZlDQoNClRhIHRo4butIGTDuW5nIDEgbcO0IGjDrG5oIHbhu5tpIHRoYW0gc+G7kSBo4buTaSBxdXkgdHJ1bmcgYsOsbmggxJHGsOG7o2MgeHXhuqV0IHJhIHThu6sga+G6v3QgcXXhuqMgQk1BIDoNCg0KYGBge3J9DQpmaXQyPWZpdA0KDQpmaXQyJGJldGFbLDE0XTwtYXMudmVjdG9yKGJtYWxvZyRwb3N0bWVhbikNCg0KY29lZihmaXQyLCBkZj0xKSU+JWFzLm1hdHJpeCgpDQoNCnByZWQ9cHJlZGljdChmaXQyLG5ldz10ZXN0c2V0WywtYygxMSwxMildLCAgdHlwZSA9ICJyZXNwb25zZSIsZGY9MSkNCg0KbGFiZWw9aWZlbHNlKHByZWQ+MC4zLCJQb3NpdGl2ZSIsIk5lZ2F0aXZlIikNCg0KY29uZnVzaW9uTWF0cml4KGxhYmVsLHRlc3RzZXQkQ2xhc3MscG9zaXRpdmU9IlBvc2l0aXZlIikNCmBgYA0KDQpNw7QgaMOsbmggbsOgeSBjw7MgcGjhuqltIGNo4bqldCB04buRdCBoxqFuIGPhuqMsIHbhu5tpIEJBQyA9IDAuODQyLCBjaOG7iSBjw7MgMSBjYXNlIGZhbHNlIG5lZ2F0aXZlDQoNCiMgS+G6v3QgbHXhuq1uDQoNClBoxrDGoW5nIHBow6FwIERNUm5ldCBsw6AgbeG7mXQgbOG7sWEgY2jhu41uIG3hu5tpIHRoYXkgdGjhur8gY2hvIEJNQSB2w6AgTGFzc28uIE7DsyB0aMOibiB0aGnhu4duIHbDoCBk4buFIHPhu60gZOG7pW5nIGjGoW4gTGFzc28gdHJvbmcgZ2xtbmV0IHBhY2thZ2UuIFR1eSBuaGnDqm4gdmnhu4djIHNhbiBi4bqxbmcgaOG7hyBz4buRIGjhu5NpIHF1eSBjaG8gY8OhYyBmYWN0b3IgbGV2ZWwgY+G7p2EgTVJNbmV0IGThu7FhIHbDoG8gY2x1c3RlcmluZyBjxaluZyBuaMawIHZp4buHYyBjaOG7jW4gbcO0IGjDrG5oIGThu7FhIHbDoG8gQklDIGPDsm4gcGjhuqNpIGLDoG4gY8OjaS4gTcO0IGjDrG5oIGPDsyBCSUMgdOG7kWkgxrB1IGNoxrBhIGjhurNuIGzDoCBtw7QgaMOsbmggY2jDrW5oIHjDoWMuIE7hur91IHjDqXQgduG7gSBwaOG6qW0gY2jhuqV0IG3DtCBow6xuaCB0acOqbiBsxrDhu6NuZyB0aMOsIERNUm5ldCB24bqrbiBjw7JuIGvDqW0geGEgc28gduG7m2kgQk1BLg0K