1 Giới thiệu

Cách đây khoảng 1 năm, Nhi đã viết một bài tổng quan về các tiêu chí để đánh giá hiệu năng của một mô hình phân loại: https://rpubs.com/lengockhanhi/347941 ; Xét rằng phân tích hồi quy cũng là một vấn đề phổ biến trong nghiên cứu y học, hôm nay Nhi cũng sẽ thực hiện một bài viết tương tự cho mô hình Hồi quy.

Như các bạn đã biết, trong phân tích hồi quy chúng ta đi tìm một quy luật (mô hình) cho phép tiên lượng giá trị của một biến kết quả từ dữ liệu đầu vào. Khi làm việc này, chính là ta đã xác định một hàm để rút gọn không gian dữ liệu rộng lớn và tất cả những quan hệ phức tạp giữa các biến số trong không gian này thành một con đường đơn giản, hay nói cách khác, ta đang giản lược hóa thế giới thực thành những quy luật đơn giản. Như vậy mô hình hồi quy có thể được dùng để giải quyết mục tiêu tiên lượng lẫn diễn dịch (suy diễn thống kê về mối tương quan, hiệu ứng, so sánh,… ).Trong cả 2 mục tiêu, điều kiện quan trọng nhất đảm bảo giá trị cho kết quả tiên lượng và suy diễn thống kê, đó là mô hình phải chính xác. Nhưng làm thế nào ta có thể đánh giá được độ chính xác của mô hình ? Hay so sánh phẩm chất giữa nhiều mô hình với nhau ?

Trong bài này, Nhi sẽ giới thiệu với các bạn tất cả những chỉ số cho phép đánh giá phẩm chất của một mô hình hồi quy.

Trước hết, Nhi sẽ dùng một thí dụ minh họa đơn giản với dữ liệu DLCO mà Nhi từng dùng trước kia. Mục tiêu là tiên lượng giá trị của DLCO, một đại lượng sinh lý hô hấp, dựa vào 3 biến là Giới tính, Tuổi và chiều cao.

library(tidyverse)
library(ggpubr)
library(rsample)

df = read.csv("https://raw.githubusercontent.com/kinokoberuji/R-Tutorials/master/DLCOkeras1.csv",sep=";")%>%
  dplyr::select(Sex,Age,Height,DLCO)

head(df)
##   Sex Age Height DLCO
## 1   F  20  173.0 29.2
## 2   F  22  165.0 26.3
## 3   F  22  168.6 27.9
## 4   F  23  164.0 25.4
## 5   F  24  170.0 29.3
## 6   F  25  170.5 24.8

Nhi chia dữ liệu thành 2 phần: Trainset (n=511) và Testset (n=126)

set.seed(123)
idx=caret::createDataPartition(y=df$Age, p=0.8,list=FALSE)
trainset=df[idx,]
testset=df[-idx,]

Sau đó, Nhi dựng một mô hình Polynomial đơn giản bằng hàm glm, có nội dung: DLCO ~ Sex + poly(Age, 2) + Height.

model = glm(data=trainset,
            formula=DLCO ~ Sex + poly(Age,2)+Height)

summary(model)
## 
## Call:
## glm(formula = DLCO ~ Sex + poly(Age, 2) + Height, data = trainset)
## 
## Deviance Residuals: 
##      Min        1Q    Median        3Q       Max  
## -30.8986   -2.8045   -0.4108    3.0483   19.9276  
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   -11.57376    5.40141  -2.143   0.0326 *  
## SexM            6.35204    0.59877  10.609  < 2e-16 ***
## poly(Age, 2)1 -67.11159    5.37940 -12.476  < 2e-16 ***
## poly(Age, 2)2 -21.83199    5.18266  -4.213 2.99e-05 ***
## Height          0.22215    0.03274   6.785 3.25e-11 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for gaussian family taken to be 25.83874)
## 
##     Null deviance: 33355  on 510  degrees of freedom
## Residual deviance: 13074  on 506  degrees of freedom
## AIC: 3118.8
## 
## Number of Fisher Scoring iterations: 2

Có rất nhiều algorithm có thể được dùng để tạo ra quy luật (mô hình hồi quy) mà ta cần, từ đơn giản như một mô hình tuyến tính, đến những công cụ kì lạ khác như gamlss, random forest, gaussian process hay thậm chí neural network; nhưng các bạn chỉ cần hình dung về chúng như một hàm F, cho phép ước tính kết quả y_hat từ dữ liệu đầu vào (X):

fi ~ F(X)

Dĩ nhiên, chúng ta mong muốn rằng kết quả (fi) phải gần với quan sát thực tế (yi) nhất có thể, càng gần càng tốt. Do đó, một cách tự nhiên, ta nhìn vào khác biệt giữa giá trị tiên lượng và giá trị thực tế và gọi đó là sai số :

err = fi - yi

Đây chính là nền tảng của mọi tiêu chí kiểm định mô hình mà ta sẽ thấy trong phần tiếp theo:

Nếu nghiên cứu có mục tiêu diễn dịch, ta thường kiểm định mô hình trên chính dữ liệu gốc (trainset) hoặc có sử dụng một hình thức tái chọn mẫu như bootstrap, cross-validation… , nhưng cho mục tiêu tiên lượng, ta bắt buộc phải kiểm định mô hình trên một quần thể độc lập khác (testset).

2 Kiểm định trực quan bằng biểu đồ

Trước hết, ta hoàn toàn có thể kiểm tra phẩm chất của mô hình một cách trực quan bằng hình ảnh mà không cần đến các chỉ số thống kê. Cách làm này sẽ giúp bạn gây ấn tượng với khán giả/độc giả khi thuyết trình, in poster hay công bố báo chí, và thực ra, chúng chuyển nhiều thông tin hơn bất cứ một con số đơn độc nào. Do đó, Nhi sẽ nói về phần này trước:

Nhi tạo ra 1 dataframe mới từ testset, với 3 cột truth = giá trị thực, predicted = giá trị từ mô hình, và error = sai biệt giữa 2 giá trị này.

pdf = data_frame(sex=testset$Sex,
                 age = testset$Age,
                 height = testset$Height,
                 truth = testset$DLCO,
                 predicted = predict(model,testset),
                 error= predicted - truth)

head(pdf)
## # A tibble: 6 x 6
##   sex     age height truth predicted  error
##   <fct> <int>  <dbl> <dbl>     <dbl>  <dbl>
## 1 F        22   165   26.3      27.8  1.53 
## 2 F        22   169.  27.9      28.6  0.732
## 3 F        26   149   19.2      24.2  4.97 
## 4 F        26   159   24.8      26.4  1.60 
## 5 F        28   171.  27.1      29.0  1.92 
## 6 F        30   173   30.2      29.3 -0.901

2.1 So sánh mật độ phân bố giữa thực tế và tiên lượng:

Tất cả các mô hình hồi quy đều hoạt động dựa trên một giả định về phân bố của biến kết quả ngẫu nhiên, nếu mô hình chính xác,hình ảnh phân bố kết quả của nó sẽ đồng dạng và trùng lắp với phân bố thực tế của đại lượng mà ta muốn ước tính: Tuy nhiên, hầu hết mô hình chỉ có khả năng giảm thiểu được sai biệt giữa fi và vị trí trung tâm của y, do đó kết quả tiên lượng thường tập trung quanh trung vị hay trung bình của y, nhưng không bao giờ chồng lắp một cách hoàn hảo và tái hiện được phân bố thực.

Trên biểu đồ, phẩm chất của mô hình được đánh giá bằng mức độ đồng dạng giữa 2 phân bố, vị trí trung tâm, trong khi sai sót của mô hình thể hiện qua phần diện tích không chồng lắp.

pdf%>%gather(truth,predicted,key="Y",value="DLCO")%>%
  ggplot(aes(x=DLCO,fill=Y))+
  geom_density(alpha=0.3)+
  scale_fill_manual(values=c("blue","red"))+
  facet_wrap(~sex,ncol=1)+
  theme_bw()

Một hình thức đơn giản hơn là biểu đồ boxplot, dạng biểu đồ này không thể hiện được tất cả hình ảnh của phân bố, nhưng nó trình bày rất tốt những mốc so sánh như: trung vị, min, max, tứ phân vị, và những giá trị outliers. Khi đặt song song 2 boxplot của thực tế và tiên lượng, ta có thể hình dung về giới hạn ứng dụng của mô hình, và khuynh hướng của nó (ước lượng quá cao, quá thấp, hay phù hợp):

pdf%>%gather(truth,predicted,key="Y",value="DLCO")%>%
  ggplot(aes(Y,DLCO,fill=Y,col=Y))+
  geom_jitter(alpha=0.5)+
  geom_boxplot(alpha=0.5)+
  coord_flip()+
  stat_compare_means(method="t.test",paired = TRUE,label.y = 45)+
  scale_color_manual(values=c("blue","red"))+
  scale_fill_manual(values=c("blue","red"))+
  facet_wrap(~sex,ncol=1)+
  theme_bw()

2.2 Tương quan tuyến tính giữa giá trị thực tế và tiên lượng:

Áp dụng một biểu đồ tán xạ và một đồ thị tuyến tính giữa 2 vectors: predicted và truth cho phép khảo sát tương quan tuyến tính giữa kết quả của mô hình và quan sát thực tế.Nếu bạn thích, có thể ghi chú thêm giá trị của hệ số tương quan lên biểu đồ.

pdf%>%ggplot(aes(truth,predicted,color=sex))+
  geom_jitter(alpha=0.5)+
  geom_smooth(aes(fill=sex),alpha=0.2,method="lm")+
  stat_cor(method="spearman",label.x = 15, label.y = 45)+
  scale_color_manual(values=c("blue","red"))+
  scale_fill_manual(values=c("blue","red"))+
  facet_wrap(~sex,ncol=2)+
  theme_bw()

2.3 Khảo sát trực tiếp sai biệt giữa thực tế và tiên lượng

Một hướng đi khác đó là ta chỉ quan tâm đến sai số của mô hình, thí dụ biểu đồ mật độ phân bố có thể được dùng để kiểm tra các giả định về sai số : phân bố chuẩn, trung bình = 0, đối xứng…

pdf%>%
  ggplot(aes(x=error,fill=sex))+
  geom_density(alpha=0.5)+
  scale_fill_manual(values=c("pink","skyblue"))+
  theme_bw()

Một biểu đồ tán xạ trình bày giá trị tuyệt đối của sai số, và liên hệ với thang đo của kết quả cần tiên lượng: Hình vẽ cho biết khuynh hướng sai biệt của mô hình (over hay underestimation), và sai biệt này là ngẫu nhiên hay có hệ thống: (Dạng biểu đồ này tương tự như Bland-Altman plot)

pdf%>%mutate(est=if_else(.$error>0,"Over","Under"))%>%
  ggplot(aes(x=truth,y=error,col=est))+
  geom_jitter(alpha=0.5)+
  geom_hline(yintercept = 0,linetype=2,col="red")+
  scale_color_manual(values=c("red","blue"))+
  facet_wrap(~sex,ncol=2)+
  theme_bw()

2.4 Kiểm tra tính hợp lý của nội dung mô hình

Bạn có thể sử dụng dạng biểu đồ sau đây để kiểm tra đồng thời : liệu mô hình có chính xác không ? Quy luật bên trong của mô hình có phù hợp với quan sát thực tế hay không ? Thí dụ, mô hình hiện thời sử dụng một hàm đa thức bậc 2 để diễn tả sự biến thiên của DLCO tùy theo tuổi - ta muốn kiểm tra xem quy luật này có áp dụng được cho một quần thể độc lập hay không ?

pdf%>%gather(truth,predicted,key="Y",value="DLCO")%>%ggplot()+
  geom_jitter(aes(age,DLCO),alpha=0.5)+
  geom_smooth(aes(age,DLCO,col=Y,fill=Y),alpha=0.2)+
  scale_color_manual(values=c("blue","red"))+
  scale_fill_manual(values=c("blue","red"))+
  facet_wrap(~sex,ncol=2)+
  ggtitle("Test_set")+
  theme_bw()

Cho bài toán diễn dịch, bạn có thể áp dụng biểu đồ cho dữ liệu gốc (trainset):

trainset%>%mutate(truth=trainset$DLCO,predicted=predict(model,trainset))%>%
  gather(truth,predicted,key="Y",value="DLCO")%>%ggplot()+
  geom_jitter(aes(Age,DLCO),alpha=0.2)+
  geom_smooth(aes(Age,DLCO,col=Y,fill=Y),alpha=0.5)+
  scale_color_manual(values=c("blue","red"))+
  scale_fill_manual(values=c("blue","red"))+
  facet_wrap(~Sex,ncol=2)+
  ggtitle("Train_set")+
  theme_bw()
## Warning: attributes are not identical across measure variables;
## they will be dropped
## `geom_smooth()` using method = 'loess' and formula 'y ~ x'

Bạn cũng có thể thay biểu đồ tuyến kí bằng 2D density plot:

pdf%>%gather(truth,predicted,key="Y",value="DLCO")%>%
  ggplot(aes(age,DLCO))+
  stat_density_2d(geom = "polygon", aes(col = Y,fill=Y, alpha = ..level..))+
  geom_jitter(alpha=0.5)+
  scale_color_manual(values=c("blue4","red4"))+
  scale_fill_manual(values=c("blue","red"))+
  facet_wrap(~sex,ncol=2)+
  theme_bw()

3 Các tiêu chí kiểm định mô hình hồi quy

Trong phần tiếp theo, Nhi sẽ liệt kê hơn 14 tiêu chí có thể sử dụng để kiểm tra phẩm chất của mô hình hồi quy. Như đã nói ở trên, tất cả các tiêu chí này đều nhắm đến mục tiêu đo lường kích thước sai biệt giữa thực tế (truth, yi) và tiên lượng (predicted, fi).

3.1 Nhóm 1: Các tiêu chí dựa vào giá trị sai biệt tuyệt đối

Trong nhóm này, ta có:

  1. MAE : Trung bình của sai biệt tuyệt đối

\[MAE = \frac{\sum abs(f_{i}-y_{i})}{n}\]

  1. MEDAE: Trung vị của sai biệt tuyệt đối

\[MEDAE = Median(abs(f_{i}-y_{i}))\]

  1. SAE: Tổng sai biệt tuyệt đối

\[SAE = \sum_{i}^{n}abs(f_{i}-y_{i})\]

  1. MAPE Mean absolute percentage error

\[MAPE = \frac{\sum {(\frac{abs(y_{i}-f_{i})}{y_{i}})}}{n}\]

4 tiêu chí này rất dễ hiểu, cả 3 đều đo lường sai biệt giữa thực tế (yi) và tiên lượng của mô hình (fi),giá trị tuyệt đối được dùng để tránh sai lầm trong trường hợp mô hình đồng thờicó nguy cơ đánh giá quá cao và quá thấp, dẫn đến việc sai số > 0 và <0 triệt tiêu lẫn nhau.

Tiêu chí thứ 4: MAPE đo lường sai biệt theo tỉ lệ % , dùng cho những trường hợp mà biến kết quả có đơn vị quá thấp hoặc quá cao

3.2 Nhóm 2:Các tiêu chí dựa vào bình phương sai số

Các tiêu chí trong nhóm này không sử dụng hàm abs, nhưng dựa vào bình phương của sai số; ta lần lượt có:

  1. MSE : trung bình bình phương sai số

\[MSE = \frac{\sum (f_{i}-y_{i})^{2}}{N}\]

  1. MEDSE: Trung vị bình phương sai số

\[MEDSE = Median ((f_{i}-y_{i})^{2})\]

  1. SSE: tổng bình phương sai số

\[SSE = \sum (f_{i}-y_{i})^{2}\]

  1. RMSE : Căn bậc 2 của trung bình bình phương sai số

\[RMSE = \sqrt{\frac{\sum_{i}^{n} (f_{i}-y_{i})^{2}}{n}}\]

  1. MSLE Mean squared logarithmic error

\[MSLE = \frac{\sum (log(f_{i}+1)- log(y_{i}+1))^{2}}{n}\]

  1. RMSLE: Root mean squared logarithmic errors

\[RMSLE = \sqrt{\frac{\sum (log(f_{i}+1)- log(y_{i}+1))^{2}}{n}}\] ## Nhóm 3: Khảo sát sai biệt tương đối của mô hình

2 tiêu chí trong nhóm 3 này không đo lường sai biệt của mô hình một cách độc lập và tuyệt đối, nhưng đánh giá tương đối tỉ lệ giữa sai biệt của mô hình và sai biệt nội tại của biến kết quả bên trong quần thể

  1. RRSE Root relative squared error

\[RRSE = \sqrt{\frac{\sum(f_{i}-y_{i})^{2}}{\sum(y_{i} - \bar{y})^{2}}}\]

  1. RAE : Relative absolute error

\[RAE = \frac{\sum (abs(f_{i}-y_{i}))}{\sum (abs(y_{i}-\bar{y}))}\]

3.3 Nhóm 4 :

  1. Rsq: hệ số xác định : coefficient of determination (R2)

Bản thân hệ số này không cung cấp thông tin về độ chính xác của mô hình nhưng cho biết mô hình giải thích được bao nhiêu phần phương sai của biến kết quả trong mẫu. Thông tin này khá quan trọng cho mô hình diễn dịch.

Hệ số R2 có liên hệ với những thành phần phương sai mà ta đã biết trong ANOVA cổ điển, bao gồm tổng phương sai (SSE), phương sai của mô hình (mà mô hình có khả năng giải thích, MSE) và phương sai tồn lưu (residual sum of squares)

\[R^{2} = \frac{SSM}{SSE} = 1 - \frac{SSR}{SSE} = \frac{\sum (f_{i}-\bar{y})^{2}}{\sum (y_{i} - \bar{y})^{2}} = 1- \frac{\sum (y_{i}-f_{i})^{2}}{\sum (y_{i} - \bar{y})^{2}}\]

Hình: SSE = vùng màu đỏ, SSR = vùng màu xanh, f = mô hình

3.4 Nhóm 5 : Các tiêu chí đánh giá mối tương quan tuyến tính giữa giá trị thực và tiên lượng:

Bản chất của 3 tiêu chí này chính là 3 phương pháp tính hệ số tương quan trong thống kê cổ điển mà ta từng biết, gồm

  1. Kendall’s Tau (thứ hạng)

\[\tau = \frac{số cặp trùng hợp - số cặp không trùng hợp}{n(n-1)/2}\]

  1. Rho của Spearman (phi tham số)

\[\rho = \frac{cov(rankY,rankY_{hat})}{\sigma_{rankY} \sigma_{rankY_{hat}}}\]

  1. Pearson’r

\[r = \frac{cov(rankY,rankY_{hat})}{\sigma_{Y} \sigma_{Y_{hat}}}\]

4 Viết một hàm tính thủ công tất cả tiêu chí kiểm định mô hình hồi quy

scoring = function(methods,truth,predicted){
  
  SSE = function(truth, predicted) {
    sum((predicted - truth)^2)
  }
  
  # MSE
  
  MSE = function(truth, predicted) {
    mean((predicted - truth)^2)
  }
  
  # RMSE
  
  RMSE = function(truth, predicted) {
    sqrt(MSE(truth, predicted))
  }
  
  # MEDSE
  
  MEDSE = function(truth, predicted) {
    median((predicted - truth)^2)
  }
  
  # SAE
  
  SAE = function(truth, predicted) {
    sum(abs(predicted - truth))
  }
  
  # MAE
  
  MAE = function(truth, predicted) {
    mean(abs(predicted - truth))
  }
  
  # MEDAE
  
  MEDAE = function(truth, predicted) {
    median(abs(predicted - truth))
  }
  
  # RSQ
  
  RSQ = function(truth,  predicted) {
    rss = SSE(truth,  predicted)
    ess = sum((truth - mean(truth))^2L)
    if (ess == 0){
      warning("Error: all truth values are equal")
      return(NA_real_)
    }
    1 - rss / ess
  }

  # RRSE Root relative squared error
  
  RRSE = function(truth, predicted){
    tss = sum((truth - mean(truth))^2L)
    if (tss == 0){
      warning("Error: all truth values are equal.")
      return(NA_real_)
    }
    sqrt(SSE(truth, predicted) / tss)
  }
  
  # RAE : Relative absolute error
  
  RAE = function(truth, predicted){
    meanad = sum(abs(truth - mean(truth)))
    if (meanad == 0){
      warning("Error:  all truth values are equal.")
      return(NA_real_)
    }
    return(SAE(truth, predicted) / meanad)
  }
  
  # MAPE Mean absolute percentage error
  
  MAPE = function(truth, predicted){
    if (any(truth == 0)){
      warning("Error: truth value is equal to 0.")
      return(NA_real_)
    }
    return(mean(abs((truth - predicted) / truth)))
  }
  
  # Mean squared logarithmic error
  
  MSLE = function(truth, predicted) {
    if (any(truth < -1))
      stop("All truth values must be greater or equal -1")
    if (any(predicted < -1))
      stop("All predicted values must be greater or equal -1")
    
    mean((log(predicted + 1) - log(truth + 1))^2)
  }
  
  # Root mean squared logarithmic error
  
  RMSLE = function(truth, predicted) {
    sqrt(MSLE(truth, predicted))
  }
  
  # Kendall Tau
  
  KendallTau = function(truth, predicted) {
    cor(truth, predicted, use = "na.or.complete", method = "kendall")
  }
  
  # rho
  
  SpearmanRho = function(truth, predicted) {
    cor(truth, predicted, use = "na.or.complete", method = "spearman")
  }
  
  # Pearson r
  
  PearsonR = function(truth, predicted) {
    cor(truth, predicted, use = "na.or.complete", method = "pearson")
  }
  
  scores = data_frame(Score=methods,Value=rep(NA,length(methods)))
  
  for (i in 1:length(methods)){
    scoring_func = get(methods[i])
    scores$Value[i]= scoring_func(truth,predicted)
  }
  
  return(scores)
}

Áp dụng hàm này cho kết quả trên testset: Chú ý danh pháp viết tắt của các tiêu chí:

scores = scoring(methods=c('MAE','MAPE','MEDAE','MEDSE','MSE','MSLE',
                  'RAE','RMSE','RMSLE','RRSE','RSQ','SAE','SSE',
                  'PearsonR','KendallTau','SpearmanRho'),
        truth=pdf$truth,
        predicted=pdf$predicted)
knitr::kable(scores)
Score Value
MAE 4.0078789
MAPE 0.1316461
MEDAE 3.4934013
MEDSE 12.2039131
MSE 25.9979765
MSLE 0.0234722
RAE 0.6218403
RMSE 5.0988211
RMSLE 0.1532062
RRSE 0.6241336
RSQ 0.6104573
SAE 504.9927456
SSE 3275.7450392
PearsonR 0.7832422
KendallTau 0.6043231
SpearmanRho 0.7972955

Bài thực hành đến đây là hết. Tạm biệt các bạn và hẹn gặp lại :)

LS0tDQp0aXRsZTogIsSQw6FuaCBnacOhIG3DtCBow6xuaCBo4buTaSBxdXkiIA0KYXV0aG9yOiAiTMOqIE5n4buNYyBLaOG6oyBOaGkiDQpkYXRlOiAiMDIgVGjDoW5nIDEyIDIwMTgiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZGVmYXVsdCINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KYGBgDQoNCiFbXShSZWdzY29yZS5wbmcpDQoNCiMgR2nhu5tpIHRoaeG7h3UNCg0KQ8OhY2ggxJHDonkga2hv4bqjbmcgMSBuxINtLCBOaGkgxJHDoyB2aeG6v3QgbeG7mXQgYsOgaSB04buVbmcgcXVhbiB24buBIGPDoWMgdGnDqnUgY2jDrSDEkeG7gyDEkcOhbmggZ2nDoSBoaeG7h3UgbsSDbmcgY+G7p2EgbeG7mXQgbcO0IGjDrG5oIHBow6JuIGxv4bqhaTogaHR0cHM6Ly9ycHVicy5jb20vbGVuZ29ja2hhbmhpLzM0Nzk0MSA7IFjDqXQgcuG6sW5nIHBow6JuIHTDrWNoIGjhu5NpIHF1eSBjxaluZyBsw6AgbeG7mXQgduG6pW4gxJHhu4EgcGjhu5UgYmnhur9uIHRyb25nIG5naGnDqm4gY+G7qXUgeSBo4buNYywgaMO0bSBuYXkgIE5oaSBjxaluZyBz4bq9IHRo4buxYyBoaeG7h24gbeG7mXQgYsOgaSB2aeG6v3QgIHTGsMahbmcgdOG7sSBjaG8gbcO0IGjDrG5oIEjhu5NpIHF1eS4NCg0KTmjGsCBjw6FjIGLhuqFuIMSRw6MgYmnhur90LCAgdHJvbmcgcGjDom4gdMOtY2ggaOG7k2kgcXV5ICBjaMO6bmcgdGEgxJFpIHTDrG0gbeG7mXQgcXV5IGx14bqtdCAobcO0IGjDrG5oKSBjaG8gcGjDqXAgdGnDqm4gbMaw4bujbmcgZ2nDoSB0cuG7iyBj4bunYSBt4buZdCBiaeG6v24ga+G6v3QgcXXhuqMgdOG7qyBk4buvIGxp4buHdSDEkeG6p3UgdsOgby4gS2hpIGzDoG0gdmnhu4djIG7DoHksIGNow61uaCBsw6AgdGEgxJHDoyB4w6FjIMSR4buLbmggbeG7mXQgaMOgbSDEkeG7gyByw7p0IGfhu41uIGtow7RuZyBnaWFuIGThu68gbGnhu4d1IHLhu5luZyBs4bubbiB2w6AgdOG6pXQgY+G6oyBuaOG7r25nIHF1YW4gaOG7hyBwaOG7qWMgdOG6oXAgZ2nhu69hIGPDoWMgYmnhur9uIHPhu5EgdHJvbmcga2jDtG5nIGdpYW4gbsOgeSB0aMOgbmggbeG7mXQgY29uIMSRxrDhu51uZyDEkcahbiBnaeG6o24sIGhheSBuw7NpIGPDoWNoIGtow6FjLCB0YSDEkWFuZyBnaeG6o24gbMaw4bujYyBow7NhIHRo4bq/IGdp4bubaSB0aOG7sWMgdGjDoG5oIG5o4buvbmcgcXV5IGx14bqtdCDEkcahbiBnaeG6o24uIE5oxrAgduG6rXkgIG3DtCBow6xuaCBo4buTaSBxdXkgY8OzIHRo4buDIMSRxrDhu6NjIGTDuW5nIMSR4buDIGdp4bqjaSBxdXnhur90IG3hu6VjIHRpw6p1IHRpw6puIGzGsOG7o25nIGzhuqtuIGRp4buFbiBk4buLY2ggKHN1eSBkaeG7hW4gdGjhu5FuZyBrw6ogduG7gSBt4buRaSB0xrDGoW5nIHF1YW4sIGhp4buHdSDhu6luZywgc28gc8OhbmgsLi4uICkuVHJvbmcgY+G6oyAyIG3hu6VjIHRpw6p1LCDEkWnhu4F1IGtp4buHbiBxdWFuIHRy4buNbmcgbmjhuqV0IMSR4bqjbSBi4bqjbyBnacOhIHRy4buLIGNobyBr4bq/dCBxdeG6oyB0acOqbiBsxrDhu6NuZyB2w6Agc3V5IGRp4buFbiB0aOG7kW5nIGvDqiwgxJHDsyBsw6AgbcO0IGjDrG5oIHBo4bqjaSBjaMOtbmggeMOhYy4gTmjGsG5nIGzDoG0gdGjhur8gbsOgbyB0YSBjw7MgdGjhu4MgxJHDoW5oIGdpw6EgxJHGsOG7o2MgxJHhu5kgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oID8gSGF5IHNvIHPDoW5oIHBo4bqpbSBjaOG6pXQgZ2nhu69hIG5oaeG7gXUgbcO0IGjDrG5oIHbhu5tpIG5oYXUgPyANCg0KVHJvbmcgYsOgaSBuw6B5LCBOaGkgc+G6vSBnaeG7m2kgdGhp4buHdSB24bubaSBjw6FjIGLhuqFuIHThuqV0IGPhuqMgbmjhu69uZyBjaOG7iSBz4buRIGNobyBwaMOpcCDEkcOhbmggZ2nDoSBwaOG6qW0gY2jhuqV0IGPhu6dhIG3hu5l0IG3DtCBow6xuaCBo4buTaSBxdXkuDQoNClRyxrDhu5tjIGjhur90LCBOaGkgc+G6vSBkw7luZyBt4buZdCB0aMOtIGThu6UgbWluaCBo4buNYSDEkcahbiBnaeG6o24gduG7m2kgZOG7ryBsaeG7h3UgRExDTyBtw6AgTmhpIHThu6tuZyBkw7luZyB0csaw4bubYyBraWEuIE3hu6VjIHRpw6p1IGzDoCB0acOqbiBsxrDhu6NuZyBnacOhIHRy4buLIGPhu6dhIERMQ08sIG3hu5l0IMSR4bqhaSBsxrDhu6NuZyBzaW5oIGzDvSBow7QgaOG6pXAsIGThu7FhIHbDoG8gMyBiaeG6v24gbMOgIEdp4bubaSB0w61uaCwgVHXhu5VpIHbDoCBjaGnhu4F1IGNhby4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoZ2dwdWJyKQ0KbGlicmFyeShyc2FtcGxlKQ0KDQpkZiA9IHJlYWQuY3N2KCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20va2lub2tvYmVydWppL1ItVHV0b3JpYWxzL21hc3Rlci9ETENPa2VyYXMxLmNzdiIsc2VwPSI7IiklPiUNCiAgZHBseXI6OnNlbGVjdChTZXgsQWdlLEhlaWdodCxETENPKQ0KDQpoZWFkKGRmKQ0KYGBgDQoNCk5oaSBjaGlhIGThu68gbGnhu4d1IHRow6BuaCAyIHBo4bqnbjogVHJhaW5zZXQgKG49NTExKSB2w6AgVGVzdHNldCAobj0xMjYpDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc2V0LnNlZWQoMTIzKQ0KaWR4PWNhcmV0OjpjcmVhdGVEYXRhUGFydGl0aW9uKHk9ZGYkQWdlLCBwPTAuOCxsaXN0PUZBTFNFKQ0KdHJhaW5zZXQ9ZGZbaWR4LF0NCnRlc3RzZXQ9ZGZbLWlkeCxdDQpgYGANCg0KU2F1IMSRw7MsIE5oaSBk4buxbmcgbeG7mXQgbcO0IGjDrG5oIFBvbHlub21pYWwgxJHGoW4gZ2nhuqNuIGLhurFuZyBow6BtIGdsbSwgY8OzIG7hu5lpIGR1bmc6IERMQ08gfiBTZXggKyBwb2x5KEFnZSwgMikgKyBIZWlnaHQuDQoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptb2RlbCA9IGdsbShkYXRhPXRyYWluc2V0LA0KICAgICAgICAgICAgZm9ybXVsYT1ETENPIH4gU2V4ICsgcG9seShBZ2UsMikrSGVpZ2h0KQ0KDQpzdW1tYXJ5KG1vZGVsKQ0KYGBgDQoNCkPDsyBy4bqldCBuaGnhu4F1IGFsZ29yaXRobSBjw7MgdGjhu4MgxJHGsOG7o2MgZMO5bmcgxJHhu4MgdOG6oW8gcmEgcXV5IGx14bqtdCAobcO0IGjDrG5oIGjhu5NpIHF1eSkgbcOgIHRhIGPhuqduLCB04burIMSRxqFuIGdp4bqjbiBuaMawIG3hu5l0IG3DtCBow6xuaCB0dXnhur9uIHTDrW5oLCDEkeG6v24gbmjhu69uZyBjw7RuZyBj4bulIGvDrCBs4bqhIGtow6FjIG5oxrAgZ2FtbHNzLCByYW5kb20gZm9yZXN0LCBnYXVzc2lhbiBwcm9jZXNzIGhheSB0aOG6rW0gY2jDrSBuZXVyYWwgbmV0d29yazsgbmjGsG5nIGPDoWMgYuG6oW4gY2jhu4kgY+G6p24gaMOsbmggZHVuZyB24buBIGNow7puZyBuaMawIG3hu5l0IGjDoG0gRiwgY2hvIHBow6lwIMaw4bubYyB0w61uaCBr4bq/dCBxdeG6oyB5X2hhdCB04burIGThu68gbGnhu4d1IMSR4bqndSB2w6BvIChYKTogDQoNCmZpIH4gRihYKQ0KDQpExKkgbmhpw6puLCBjaMO6bmcgdGEgbW9uZyBtdeG7kW4gcuG6sW5nIGvhur90IHF14bqjIChmaSkgcGjhuqNpIGfhuqduIHbhu5tpIHF1YW4gc8OhdCB0aOG7sWMgdOG6vyAoeWkpIG5o4bqldCBjw7MgdGjhu4MsIGPDoG5nIGfhuqduIGPDoG5nIHThu5F0LiBEbyDEkcOzLCBt4buZdCBjw6FjaCB04buxIG5oacOqbiwgdGEgbmjDrG4gdsOgbyBraMOhYyBiaeG7h3QgZ2nhu69hIGdpw6EgdHLhu4sgdGnDqm4gbMaw4bujbmcgdsOgIGdpw6EgdHLhu4sgdGjhu7FjIHThur8gdsOgIGfhu41pIMSRw7MgbMOgIHNhaSBz4buRIDoNCg0KZXJyID0gZmkgLSB5aQ0KDQrEkMOieSBjaMOtbmggbMOgIG7hu4FuIHThuqNuZyBj4bunYSBt4buNaSB0acOqdSBjaMOtIGtp4buDbSDEkeG7i25oIG3DtCBow6xuaCBtw6AgdGEgc+G6vSB0aOG6pXkgdHJvbmcgcGjhuqduIHRp4bq/cCB0aGVvOg0KDQpO4bq/dSBuZ2hpw6puIGPhu6l1IGPDsyBt4bulYyB0acOqdSBkaeG7hW4gZOG7i2NoLCB0YSB0aMaw4budbmcga2nhu4NtIMSR4buLbmggbcO0IGjDrG5oIHRyw6puIGNow61uaCBk4buvIGxp4buHdSBn4buRYyAodHJhaW5zZXQpIGhv4bq3YyBjw7Mgc+G7rSBk4bulbmcgbeG7mXQgaMOsbmggdGjhu6ljIHTDoWkgY2jhu41uIG3huqt1IG5oxrAgYm9vdHN0cmFwLCBjcm9zcy12YWxpZGF0aW9uLi4uICwgbmjGsG5nIGNobyBt4bulYyB0acOqdSB0acOqbiBsxrDhu6NuZywgdGEgYuG6r3QgYnXhu5ljIHBo4bqjaSBraeG7g20gxJHhu4tuaCBtw7QgaMOsbmggdHLDqm4gbeG7mXQgcXXhuqduIHRo4buDIMSR4buZYyBs4bqtcCBraMOhYyAodGVzdHNldCkuDQoNCiMgS2nhu4NtIMSR4buLbmggdHLhu7FjIHF1YW4gYuG6sW5nIGJp4buDdSDEkeG7kw0KDQpUcsaw4bubYyBo4bq/dCwgdGEgaG/DoG4gdG/DoG4gY8OzIHRo4buDIGtp4buDbSB0cmEgcGjhuqltIGNo4bqldCBj4bunYSBtw7QgaMOsbmggbeG7mXQgY8OhY2ggdHLhu7FjIHF1YW4gYuG6sW5nIGjDrG5oIOG6o25oIG3DoCBraMO0bmcgY+G6p24gxJHhur9uIGPDoWMgY2jhu4kgc+G7kSB0aOG7kW5nIGvDqi4gQ8OhY2ggbMOgbSBuw6B5IHPhur0gZ2nDunAgYuG6oW4gZ8OieSDhuqVuIHTGsOG7o25nIHbhu5tpIGtow6FuIGdp4bqjL8SR4buZYyBnaeG6oyBraGkgdGh1eeG6v3QgdHLDrG5oLCBpbiBwb3N0ZXIgaGF5IGPDtG5nIGLhu5EgYsOhbyBjaMOtLCB2w6AgdGjhu7FjIHJhLCBjaMO6bmcgY2h1eeG7g24gbmhp4buBdSB0aMO0bmcgdGluIGjGoW4gYuG6pXQgY+G7qSBt4buZdCBjb24gc+G7kSDEkcahbiDEkeG7mWMgbsOgby4gRG8gxJHDsywgTmhpIHPhur0gbsOzaSB24buBIHBo4bqnbiBuw6B5IHRyxrDhu5tjOiANCg0KTmhpIHThuqFvIHJhIDEgZGF0YWZyYW1lIG3hu5tpIHThu6sgdGVzdHNldCwgduG7m2kgMyBj4buZdCB0cnV0aCA9IGdpw6EgdHLhu4sgdGjhu7FjLCBwcmVkaWN0ZWQgPSBnacOhIHRy4buLIHThu6sgbcO0IGjDrG5oLCB2w6AgZXJyb3IgPSBzYWkgYmnhu4d0IGdp4buvYSAyIGdpw6EgdHLhu4sgbsOgeS4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpwZGYgPSBkYXRhX2ZyYW1lKHNleD10ZXN0c2V0JFNleCwNCiAgICAgICAgICAgICAgICAgYWdlID0gdGVzdHNldCRBZ2UsDQogICAgICAgICAgICAgICAgIGhlaWdodCA9IHRlc3RzZXQkSGVpZ2h0LA0KICAgICAgICAgICAgICAgICB0cnV0aCA9IHRlc3RzZXQkRExDTywNCiAgICAgICAgICAgICAgICAgcHJlZGljdGVkID0gcHJlZGljdChtb2RlbCx0ZXN0c2V0KSwNCiAgICAgICAgICAgICAgICAgZXJyb3I9IHByZWRpY3RlZCAtIHRydXRoKQ0KDQpoZWFkKHBkZikNCmBgYA0KDQojIyBTbyBzw6FuaCBt4bqtdCDEkeG7mSBwaMOibiBi4buRIGdp4buvYSB0aOG7sWMgdOG6vyB2w6AgdGnDqm4gbMaw4bujbmc6DQoNClThuqV0IGPhuqMgY8OhYyBtw7QgaMOsbmggIGjhu5NpIHF1eSDEkeG7gXUgaG/huqF0IMSR4buZbmcgZOG7sWEgdHLDqm4gbeG7mXQgZ2nhuqMgxJHhu4tuaCB24buBIHBow6JuIGLhu5EgY+G7p2EgYmnhur9uIGvhur90IHF14bqjIG5n4bqrdSBuaGnDqm4sIG7hur91IG3DtCBow6xuaCBjaMOtbmggeMOhYyxow6xuaCDhuqNuaCBwaMOibiBi4buRIGvhur90IHF14bqjIGPhu6dhIG7DsyBz4bq9IMSR4buTbmcgZOG6oW5nIHbDoCB0csO5bmcgbOG6r3AgduG7m2kgcGjDom4gYuG7kSB0aOG7sWMgdOG6vyBj4bunYSDEkeG6oWkgbMaw4bujbmcgbcOgIHRhIG114buRbiDGsOG7m2MgdMOtbmg6IFR1eSBuaGnDqm4sIGjhuqd1IGjhur90IG3DtCBow6xuaCBjaOG7iSBjw7Mga2jhuqMgbsSDbmcgZ2nhuqNtIHRoaeG7g3UgxJHGsOG7o2Mgc2FpIGJp4buHdCBnaeG7r2EgZmkgdsOgIHbhu4sgdHLDrSB0cnVuZyB0w6JtIGPhu6dhIHksIGRvIMSRw7Mga+G6v3QgcXXhuqMgdGnDqm4gbMaw4bujbmcgdGjGsOG7nW5nIHThuq1wIHRydW5nIHF1YW5oIHRydW5nIHbhu4sgaGF5IHRydW5nIGLDrG5oIGPhu6dhIHksIG5oxrBuZyBraMO0bmcgYmFvIGdp4budIGNo4buTbmcgbOG6r3AgbeG7mXQgY8OhY2ggaG/DoG4gaOG6o28gdsOgIHTDoWkgaGnhu4duIMSRxrDhu6NjIHBow6JuIGLhu5EgdGjhu7FjLiANCg0KVHLDqm4gYmnhu4N1IMSR4buTLCBwaOG6qW0gY2jhuqV0IGPhu6dhIG3DtCBow6xuaCDEkcaw4bujYyDEkcOhbmggZ2nDoSBi4bqxbmcgbeG7qWMgxJHhu5kgxJHhu5NuZyBk4bqhbmcgZ2nhu69hIDIgcGjDom4gYuG7kSwgduG7iyB0csOtIHRydW5nIHTDom0sIHRyb25nIGtoaSBzYWkgc8OzdCBj4bunYSBtw7QgaMOsbmggdGjhu4MgaGnhu4duIHF1YSBwaOG6p24gZGnhu4duIHTDrWNoIGtow7RuZyBjaOG7k25nIGzhuq9wLg0KDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KcGRmJT4lZ2F0aGVyKHRydXRoLHByZWRpY3RlZCxrZXk9IlkiLHZhbHVlPSJETENPIiklPiUNCiAgZ2dwbG90KGFlcyh4PURMQ08sZmlsbD1ZKSkrDQogIGdlb21fZGVuc2l0eShhbHBoYT0wLjMpKw0KICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKSsNCiAgZmFjZXRfd3JhcCh+c2V4LG5jb2w9MSkrDQogIHRoZW1lX2J3KCkNCmBgYA0KDQpN4buZdCBow6xuaCB0aOG7qWMgxJHGoW4gZ2nhuqNuIGjGoW4gbMOgIGJp4buDdSDEkeG7kyBib3hwbG90LCBk4bqhbmcgYmnhu4N1IMSR4buTIG7DoHkga2jDtG5nIHRo4buDIGhp4buHbiDEkcaw4bujYyB04bqldCBj4bqjIGjDrG5oIOG6o25oIGPhu6dhIHBow6JuIGLhu5EsIG5oxrBuZyBuw7MgdHLDrG5oIGLDoHkgcuG6pXQgdOG7kXQgbmjhu69uZyBt4buRYyBzbyBzw6FuaCBuaMawOiB0cnVuZyB24buLLCBtaW4sIG1heCwgdOG7qSBwaMOibiB24buLLCB2w6Agbmjhu69uZyBnacOhIHRy4buLIG91dGxpZXJzLiBLaGkgxJHhurd0IHNvbmcgc29uZyAyIGJveHBsb3QgY+G7p2EgdGjhu7FjIHThur8gdsOgIHRpw6puIGzGsOG7o25nLCB0YSBjw7MgdGjhu4MgaMOsbmggZHVuZyB24buBIGdp4bubaSBo4bqhbiDhu6luZyBk4bulbmcgY+G7p2EgbcO0IGjDrG5oLCB2w6Aga2h1eW5oIGjGsOG7m25nIGPhu6dhIG7DsyAoxrDhu5tjIGzGsOG7o25nIHF1w6EgY2FvLCBxdcOhIHRo4bqlcCwgaGF5IHBow7kgaOG7o3ApOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnBkZiU+JWdhdGhlcih0cnV0aCxwcmVkaWN0ZWQsa2V5PSJZIix2YWx1ZT0iRExDTyIpJT4lDQogIGdncGxvdChhZXMoWSxETENPLGZpbGw9WSxjb2w9WSkpKw0KICBnZW9tX2ppdHRlcihhbHBoYT0wLjUpKw0KICBnZW9tX2JveHBsb3QoYWxwaGE9MC41KSsNCiAgY29vcmRfZmxpcCgpKw0KICBzdGF0X2NvbXBhcmVfbWVhbnMobWV0aG9kPSJ0LnRlc3QiLHBhaXJlZCA9IFRSVUUsbGFiZWwueSA9IDQ1KSsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwicmVkIikpKw0KICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKSsNCiAgZmFjZXRfd3JhcCh+c2V4LG5jb2w9MSkrDQogIHRoZW1lX2J3KCkNCmBgYA0KDQojIyBUxrDGoW5nIHF1YW4gdHV54bq/biB0w61uaCBnaeG7r2EgZ2nDoSB0cuG7iyB0aOG7sWMgdOG6vyB2w6AgdGnDqm4gbMaw4bujbmc6DQoNCsOBcCBk4bulbmcgbeG7mXQgYmnhu4N1IMSR4buTIHTDoW4geOG6oSB2w6AgbeG7mXQgxJHhu5MgdGjhu4sgdHV54bq/biB0w61uaCBnaeG7r2EgMiB2ZWN0b3JzOiBwcmVkaWN0ZWQgdsOgIHRydXRoIGNobyBwaMOpcCBraOG6o28gc8OhdCB0xrDGoW5nIHF1YW4gdHV54bq/biB0w61uaCBnaeG7r2Ega+G6v3QgcXXhuqMgY+G7p2EgbcO0IGjDrG5oIHbDoCBxdWFuIHPDoXQgdGjhu7FjIHThur8uTuG6v3UgYuG6oW4gdGjDrWNoLCBjw7MgdGjhu4MgZ2hpIGNow7ogdGjDqm0gZ2nDoSB0cuG7iyBj4bunYSBo4buHIHPhu5EgdMawxqFuZyBxdWFuIGzDqm4gYmnhu4N1IMSR4buTLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnBkZiU+JWdncGxvdChhZXModHJ1dGgscHJlZGljdGVkLGNvbG9yPXNleCkpKw0KICBnZW9tX2ppdHRlcihhbHBoYT0wLjUpKw0KICBnZW9tX3Ntb290aChhZXMoZmlsbD1zZXgpLGFscGhhPTAuMixtZXRob2Q9ImxtIikrDQogIHN0YXRfY29yKG1ldGhvZD0ic3BlYXJtYW4iLGxhYmVsLnggPSAxNSwgbGFiZWwueSA9IDQ1KSsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwicmVkIikpKw0KICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKSsNCiAgZmFjZXRfd3JhcCh+c2V4LG5jb2w9MikrDQogIHRoZW1lX2J3KCkNCmBgYA0KDQojIyBLaOG6o28gc8OhdCB0cuG7sWMgdGnhur9wIHNhaSBiaeG7h3QgZ2nhu69hIHRo4buxYyB04bq/IHbDoCB0acOqbiBsxrDhu6NuZw0KDQpN4buZdCBoxrDhu5tuZyDEkWkga2jDoWMgxJHDsyBsw6AgdGEgY2jhu4kgcXVhbiB0w6JtIMSR4bq/biBzYWkgc+G7kSBj4bunYSBtw7QgaMOsbmgsIHRow60gZOG7pSBiaeG7g3UgxJHhu5MgbeG6rXQgxJHhu5kgcGjDom4gYuG7kSBjw7MgdGjhu4MgxJHGsOG7o2MgZMO5bmcgxJHhu4Mga2nhu4NtIHRyYSBjw6FjIGdp4bqjIMSR4buLbmggduG7gSBzYWkgc+G7kSA6IHBow6JuIGLhu5EgY2h14bqpbiwgdHJ1bmcgYsOsbmggPSAwLCDEkeG7kWkgeOG7qW5nLi4uDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KcGRmJT4lDQogIGdncGxvdChhZXMoeD1lcnJvcixmaWxsPXNleCkpKw0KICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41KSsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoInBpbmsiLCJza3libHVlIikpKw0KICB0aGVtZV9idygpDQpgYGANCg0KTeG7mXQgYmnhu4N1IMSR4buTIHTDoW4geOG6oSB0csOsbmggYsOgeSBnacOhIHRy4buLIHR1eeG7h3QgxJHhu5FpIGPhu6dhIHNhaSBz4buRLCB2w6AgbGnDqm4gaOG7hyB24bubaSB0aGFuZyDEkW8gY+G7p2Ega+G6v3QgcXXhuqMgY+G6p24gdGnDqm4gbMaw4bujbmc6IEjDrG5oIHbhur0gY2hvIGJp4bq/dCBraHV5bmggaMaw4bubbmcgc2FpIGJp4buHdCBj4bunYSBtw7QgaMOsbmggKG92ZXIgaGF5IHVuZGVyZXN0aW1hdGlvbiksIHbDoCBzYWkgYmnhu4d0IG7DoHkgbMOgIG5n4bqrdSBuaGnDqm4gaGF5IGPDsyBo4buHIHRo4buRbmc6IA0KKEThuqFuZyBiaeG7g3UgxJHhu5MgbsOgeSB0xrDGoW5nIHThu7EgbmjGsCBCbGFuZC1BbHRtYW4gcGxvdCkNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpwZGYlPiVtdXRhdGUoZXN0PWlmX2Vsc2UoLiRlcnJvcj4wLCJPdmVyIiwiVW5kZXIiKSklPiUNCiAgZ2dwbG90KGFlcyh4PXRydXRoLHk9ZXJyb3IsY29sPWVzdCkpKw0KICBnZW9tX2ppdHRlcihhbHBoYT0wLjUpKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLGxpbmV0eXBlPTIsY29sPSJyZWQiKSsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jKCJyZWQiLCJibHVlIikpKw0KICBmYWNldF93cmFwKH5zZXgsbmNvbD0yKSsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNCiMjIEtp4buDbSB0cmEgdMOtbmggaOG7o3AgbMO9IGPhu6dhIG7hu5lpIGR1bmcgbcO0IGjDrG5oIA0KDQpC4bqhbiBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgZOG6oW5nIGJp4buDdSDEkeG7kyBzYXUgxJHDonkgxJHhu4Mga2nhu4NtIHRyYSDEkeG7k25nIHRo4budaSA6IGxp4buHdSBtw7QgaMOsbmggY8OzIGNow61uaCB4w6FjIGtow7RuZyA/IFF1eSBsdeG6rXQgYsOqbiB0cm9uZyBj4bunYSBtw7QgaMOsbmggY8OzIHBow7kgaOG7o3AgduG7m2kgcXVhbiBzw6F0IHRo4buxYyB04bq/IGhheSBraMO0bmcgPyBUaMOtIGThu6UsIG3DtCBow6xuaCBoaeG7h24gdGjhu51pIHPhu60gZOG7pW5nIG3hu5l0IGjDoG0gxJFhIHRo4bupYyBi4bqtYyAyIMSR4buDIGRp4buFbiB04bqjIHPhu7EgYmnhur9uIHRoacOqbiBj4bunYSBETENPIHTDuXkgdGhlbyB0deG7lWkgLSB0YSBtdeG7kW4ga2nhu4NtIHRyYSB4ZW0gcXV5IGx14bqtdCBuw6B5IGPDsyDDoXAgZOG7pW5nIMSRxrDhu6NjIGNobyBt4buZdCBxdeG6p24gdGjhu4MgxJHhu5ljIGzhuq1wIGhheSBraMO0bmcgPyANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpwZGYlPiVnYXRoZXIodHJ1dGgscHJlZGljdGVkLGtleT0iWSIsdmFsdWU9IkRMQ08iKSU+JWdncGxvdCgpKw0KICBnZW9tX2ppdHRlcihhZXMoYWdlLERMQ08pLGFscGhhPTAuNSkrDQogIGdlb21fc21vb3RoKGFlcyhhZ2UsRExDTyxjb2w9WSxmaWxsPVkpLGFscGhhPTAuMikrDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKSsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkrDQogIGZhY2V0X3dyYXAofnNleCxuY29sPTIpKw0KICBnZ3RpdGxlKCJUZXN0X3NldCIpKw0KICB0aGVtZV9idygpDQpgYGANCg0KQ2hvIGLDoGkgdG/DoW4gZGnhu4VuIGThu4tjaCwgYuG6oW4gY8OzIHRo4buDIMOhcCBk4bulbmcgYmnhu4N1IMSR4buTIGNobyBk4buvIGxp4buHdSBn4buRYyAodHJhaW5zZXQpOg0KDQpgYGB7cn0NCnRyYWluc2V0JT4lbXV0YXRlKHRydXRoPXRyYWluc2V0JERMQ08scHJlZGljdGVkPXByZWRpY3QobW9kZWwsdHJhaW5zZXQpKSU+JQ0KICBnYXRoZXIodHJ1dGgscHJlZGljdGVkLGtleT0iWSIsdmFsdWU9IkRMQ08iKSU+JWdncGxvdCgpKw0KICBnZW9tX2ppdHRlcihhZXMoQWdlLERMQ08pLGFscGhhPTAuMikrDQogIGdlb21fc21vb3RoKGFlcyhBZ2UsRExDTyxjb2w9WSxmaWxsPVkpLGFscGhhPTAuNSkrDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKSsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkrDQogIGZhY2V0X3dyYXAoflNleCxuY29sPTIpKw0KICBnZ3RpdGxlKCJUcmFpbl9zZXQiKSsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNCkLhuqFuIGPFqW5nIGPDsyB0aOG7gyB0aGF5IGJp4buDdSDEkeG7kyB0dXnhur9uIGvDrSBi4bqxbmcgMkQgZGVuc2l0eSBwbG90Og0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnBkZiU+JWdhdGhlcih0cnV0aCxwcmVkaWN0ZWQsa2V5PSJZIix2YWx1ZT0iRExDTyIpJT4lDQogIGdncGxvdChhZXMoYWdlLERMQ08pKSsNCiAgc3RhdF9kZW5zaXR5XzJkKGdlb20gPSAicG9seWdvbiIsIGFlcyhjb2wgPSBZLGZpbGw9WSwgYWxwaGEgPSAuLmxldmVsLi4pKSsNCiAgZ2VvbV9qaXR0ZXIoYWxwaGE9MC41KSsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jKCJibHVlNCIsInJlZDQiKSkrDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwicmVkIikpKw0KICBmYWNldF93cmFwKH5zZXgsbmNvbD0yKSsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNCiMgQ8OhYyB0acOqdSBjaMOtIGtp4buDbSDEkeG7i25oIG3DtCBow6xuaCBo4buTaSBxdXkNCg0KVHJvbmcgcGjhuqduIHRp4bq/cCB0aGVvLCBOaGkgc+G6vSBsaeG7h3Qga8OqIGjGoW4gMTQgdGnDqnUgY2jDrSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgxJHhu4Mga2nhu4NtIHRyYSBwaOG6qW0gY2jhuqV0IGPhu6dhIG3DtCBow6xuaCBo4buTaSBxdXkuIE5oxrAgxJHDoyBuw7NpIOG7nyB0csOqbiwgdOG6pXQgY+G6oyBjw6FjIHRpw6p1IGNow60gbsOgeSDEkeG7gXUgbmjhuq9tIMSR4bq/biBt4bulYyB0acOqdSDEkW8gbMaw4budbmcga8OtY2ggdGjGsOG7m2Mgc2FpIGJp4buHdCBnaeG7r2EgdGjhu7FjIHThur8gKHRydXRoLCB5aSkgdsOgIHRpw6puIGzGsOG7o25nIChwcmVkaWN0ZWQsIGZpKS4NCg0KIyMgTmjDs20gMTogQ8OhYyB0acOqdSBjaMOtIGThu7FhIHbDoG8gZ2nDoSB0cuG7iyBzYWkgYmnhu4d0IHR1eeG7h3QgxJHhu5FpDQoNClRyb25nIG5ow7NtIG7DoHksIHRhIGPDszoNCg0KMSkgTUFFIDogVHJ1bmcgYsOsbmggY+G7p2Egc2FpIGJp4buHdCB0dXnhu4d0IMSR4buRaQ0KDQokJE1BRSA9IFxmcmFje1xzdW0gYWJzKGZfe2l9LXlfe2l9KX17bn0kJA0KDQoyKSBNRURBRTogVHJ1bmcgduG7iyBj4bunYSBzYWkgYmnhu4d0IHR1eeG7h3QgxJHhu5FpDQoNCiQkTUVEQUUgPSBNZWRpYW4oYWJzKGZfe2l9LXlfe2l9KSkkJA0KDQozKSBTQUU6IFThu5VuZyBzYWkgYmnhu4d0IHR1eeG7h3QgxJHhu5FpDQoNCiQkU0FFID0gXHN1bV97aX1ee259YWJzKGZfe2l9LXlfe2l9KSQkDQoNCjQpIE1BUEUgTWVhbiBhYnNvbHV0ZSBwZXJjZW50YWdlIGVycm9yDQoNCiQkTUFQRSA9IFxmcmFje1xzdW0geyhcZnJhY3thYnMoeV97aX0tZl97aX0pfXt5X3tpfX0pfX17bn0kJA0KDQo0IHRpw6p1IGNow60gbsOgeSBy4bqldCBk4buFIGhp4buDdSwgY+G6oyAzIMSR4buBdSDEkW8gbMaw4budbmcgc2FpIGJp4buHdCBnaeG7r2EgdGjhu7FjIHThur8gKHlpKSB2w6AgdGnDqm4gbMaw4bujbmcgY+G7p2EgbcO0IGjDrG5oIChmaSksZ2nDoSB0cuG7iyB0dXnhu4d0IMSR4buRaSDEkcaw4bujYyBkw7luZyDEkeG7gyB0csOhbmggc2FpIGzhuqdtIHRyb25nIHRyxrDhu51uZyBo4bujcCBtw7QgaMOsbmggxJHhu5NuZyB0aOG7nWljw7Mgbmd1eSBjxqEgxJHDoW5oIGdpw6EgcXXDoSBjYW8gdsOgIHF1w6EgdGjhuqVwLCBk4bqrbiDEkeG6v24gdmnhu4djIHNhaSBz4buRID4gMCB2w6AgPDAgdHJp4buHdCB0acOqdSBs4bqrbiBuaGF1LiANCg0KVGnDqnUgY2jDrSB0aOG7qSA0OiBNQVBFIMSRbyBsxrDhu51uZyBzYWkgYmnhu4d0IHRoZW8gdOG7iSBs4buHICUgLCBkw7luZyBjaG8gbmjhu69uZyB0csaw4budbmcgaOG7o3AgbcOgIGJp4bq/biBr4bq/dCBxdeG6oyBjw7MgxJHGoW4gduG7iyBxdcOhIHRo4bqlcCBob+G6t2MgcXXDoSBjYW8NCg0KIyMgTmjDs20gMjpDw6FjIHRpw6p1IGNow60gZOG7sWEgdsOgbyBiw6xuaCBwaMawxqFuZyBzYWkgc+G7kQ0KDQpDw6FjIHRpw6p1IGNow60gdHJvbmcgbmjDs20gbsOgeSBraMO0bmcgc+G7rSBk4bulbmcgaMOgbSBhYnMsIG5oxrBuZyBk4buxYSB2w6BvIGLDrG5oIHBoxrDGoW5nIGPhu6dhIHNhaSBz4buROyB0YSBs4bqnbiBsxrDhu6N0IGPDszoNCg0KNSkgTVNFIDogdHJ1bmcgYsOsbmggYsOsbmggcGjGsMahbmcgc2FpIHPhu5ENCg0KJCRNU0UgPSBcZnJhY3tcc3VtIChmX3tpfS15X3tpfSleezJ9fXtOfSQkDQoNCjYpIE1FRFNFOiBUcnVuZyB24buLIGLDrG5oIHBoxrDGoW5nIHNhaSBz4buRDQoNCiQkTUVEU0UgPSBNZWRpYW4gKChmX3tpfS15X3tpfSleezJ9KSQkDQoNCjcpIFNTRTogdOG7lW5nIGLDrG5oIHBoxrDGoW5nIHNhaSBz4buRDQoNCiQkU1NFID0gXHN1bSAoZl97aX0teV97aX0pXnsyfSQkDQoNCjgpIFJNU0UgOiBDxINuIGLhuq1jIDIgY+G7p2EgdHJ1bmcgYsOsbmggYsOsbmggcGjGsMahbmcgc2FpIHPhu5ENCg0KJCRSTVNFID0gXHNxcnR7XGZyYWN7XHN1bV97aX1ee259IChmX3tpfS15X3tpfSleezJ9fXtufX0kJA0KDQo5KSBNU0xFIE1lYW4gc3F1YXJlZCBsb2dhcml0aG1pYyBlcnJvcg0KDQokJE1TTEUgPSBcZnJhY3tcc3VtIChsb2coZl97aX0rMSktIGxvZyh5X3tpfSsxKSleezJ9fXtufSQkDQoNCjEwKSBSTVNMRTogUm9vdCBtZWFuIHNxdWFyZWQgbG9nYXJpdGhtaWMgZXJyb3JzDQoNCiQkUk1TTEUgPSBcc3FydHtcZnJhY3tcc3VtIChsb2coZl97aX0rMSktIGxvZyh5X3tpfSsxKSleezJ9fXtufX0kJA0KIyMgTmjDs20gMzogS2jhuqNvIHPDoXQgc2FpIGJp4buHdCB0xrDGoW5nIMSR4buRaSBj4bunYSBtw7QgaMOsbmgNCg0KMiB0acOqdSBjaMOtIHRyb25nIG5ow7NtIDMgbsOgeSBraMO0bmcgxJFvIGzGsOG7nW5nIHNhaSBiaeG7h3QgY+G7p2EgbcO0IGjDrG5oIG3hu5l0IGPDoWNoIMSR4buZYyBs4bqtcCB2w6AgdHV54buHdCDEkeG7kWksIG5oxrBuZyDEkcOhbmggZ2nDoSB0xrDGoW5nIMSR4buRaSB04buJIGzhu4cgZ2nhu69hIHNhaSBiaeG7h3QgY+G7p2EgbcO0IGjDrG5oIHbDoCBzYWkgYmnhu4d0IG7hu5lpIHThuqFpIGPhu6dhIGJp4bq/biBr4bq/dCBxdeG6oyBiw6puIHRyb25nIHF14bqnbiB0aOG7gw0KDQoxMSkgUlJTRSBSb290IHJlbGF0aXZlIHNxdWFyZWQgZXJyb3INCg0KJCRSUlNFID0gXHNxcnR7XGZyYWN7XHN1bShmX3tpfS15X3tpfSleezJ9fXtcc3VtKHlfe2l9IC0gXGJhcnt5fSleezJ9fX0kJA0KDQoxMikgUkFFIDogUmVsYXRpdmUgYWJzb2x1dGUgZXJyb3INCg0KJCRSQUUgPSBcZnJhY3tcc3VtIChhYnMoZl97aX0teV97aX0pKX17XHN1bSAoYWJzKHlfe2l9LVxiYXJ7eX0pKX0kJA0KDQojIyBOaMOzbSA0IDogDQoNCjEzKSBSc3E6IGjhu4cgc+G7kSB4w6FjIMSR4buLbmggOiBjb2VmZmljaWVudCBvZiBkZXRlcm1pbmF0aW9uIChSMikNCg0KQuG6o24gdGjDom4gaOG7hyBz4buRIG7DoHkga2jDtG5nIGN1bmcgY+G6pXAgdGjDtG5nIHRpbiB24buBIMSR4buZIGNow61uaCB4w6FjIGPhu6dhIG3DtCBow6xuaCBuaMawbmcgY2hvIGJp4bq/dCBtw7QgaMOsbmggZ2nhuqNpIHRow61jaCDEkcaw4bujYyBiYW8gbmhpw6p1IHBo4bqnbiBwaMawxqFuZyBzYWkgY+G7p2EgYmnhur9uIGvhur90IHF14bqjIHRyb25nIG3huqt1LiBUaMO0bmcgdGluIG7DoHkga2jDoSBxdWFuIHRy4buNbmcgY2hvIG3DtCBow6xuaCBkaeG7hW4gZOG7i2NoLg0KDQpI4buHIHPhu5EgUjIgY8OzIGxpw6puIGjhu4cgduG7m2kgbmjhu69uZyB0aMOgbmggcGjhuqduIHBoxrDGoW5nIHNhaSBtw6AgdGEgxJHDoyBiaeG6v3QgdHJvbmcgQU5PVkEgY+G7lSDEkWnhu4NuLCBiYW8gZ+G7k20gdOG7lW5nIHBoxrDGoW5nIHNhaSAoU1NFKSwgcGjGsMahbmcgc2FpIGPhu6dhIG3DtCBow6xuaCAobcOgIG3DtCBow6xuaCBjw7Mga2jhuqMgbsSDbmcgZ2nhuqNpIHRow61jaCwgTVNFKSB2w6AgcGjGsMahbmcgc2FpIHThu5NuIGzGsHUgKHJlc2lkdWFsIHN1bSBvZiBzcXVhcmVzKQ0KDQokJFJeezJ9ID0gXGZyYWN7U1NNfXtTU0V9ID0gIDEgLSBcZnJhY3tTU1J9e1NTRX0gPSBcZnJhY3tcc3VtIChmX3tpfS1cYmFye3l9KV57Mn19e1xzdW0gKHlfe2l9IC0gXGJhcnt5fSleezJ9fSA9IDEtIFxmcmFje1xzdW0gKHlfe2l9LWZfe2l9KV57Mn19e1xzdW0gKHlfe2l9IC0gXGJhcnt5fSleezJ9fSQkDQoNCiFbXShodHRwczovL3VwbG9hZC53aWtpbWVkaWEub3JnL3dpa2lwZWRpYS9jb21tb25zL3RodW1iLzgvODYvQ29lZmZpY2llbnRfb2ZfRGV0ZXJtaW5hdGlvbi5zdmcvNjAwcHgtQ29lZmZpY2llbnRfb2ZfRGV0ZXJtaW5hdGlvbi5zdmcucG5nKQ0KDQpIw6xuaDogU1NFID0gdsO5bmcgbcOgdSDEkeG7jywgU1NSID0gdsO5bmcgbcOgdSB4YW5oLCBmID0gbcO0IGjDrG5oDQoNCiMjIE5ow7NtIDUgOiBDw6FjIHRpw6p1IGNow60gxJHDoW5oIGdpw6EgbeG7kWkgdMawxqFuZyBxdWFuIHR1eeG6v24gdMOtbmggZ2nhu69hIGdpw6EgdHLhu4sgdGjhu7FjIHbDoCB0acOqbiBsxrDhu6NuZzoNCg0KQuG6o24gY2jhuqV0IGPhu6dhIDMgdGnDqnUgY2jDrSBuw6B5IGNow61uaCBsw6AgMyBwaMawxqFuZyBwaMOhcCB0w61uaCBo4buHIHPhu5EgdMawxqFuZyBxdWFuIHRyb25nIHRo4buRbmcga8OqIGPhu5UgxJFp4buDbiBtw6AgdGEgdOG7q25nIGJp4bq/dCwgZ+G7k20NCg0KMTQpIEtlbmRhbGwncyBUYXUgKHRo4bupIGjhuqFuZykNCg0KJCRcdGF1ID0gXGZyYWN7c+G7kSBj4bq3cCB0csO5bmcgaOG7o3AgLSBz4buRIGPhurdwIGtow7RuZyB0csO5bmcgaOG7o3B9e24obi0xKS8yfSQkDQoNCjE1KSBSaG8gY+G7p2EgU3BlYXJtYW4gKHBoaSB0aGFtIHPhu5EpDQoNCiQkXHJobyA9IFxmcmFje2NvdihyYW5rWSxyYW5rWV97aGF0fSl9e1xzaWdtYV97cmFua1l9IFxzaWdtYV97cmFua1lfe2hhdH19fSQkDQoNCjE2KSBQZWFyc29uJ3IgDQoNCiQkciA9IFxmcmFje2NvdihyYW5rWSxyYW5rWV97aGF0fSl9e1xzaWdtYV97WX0gXHNpZ21hX3tZX3toYXR9fX0kJA0KDQojIFZp4bq/dCBt4buZdCBow6BtIHTDrW5oIHRo4bunIGPDtG5nIHThuqV0IGPhuqMgdGnDqnUgY2jDrSBraeG7g20gxJHhu4tuaCBtw7QgaMOsbmggaOG7k2kgcXV5DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc2NvcmluZyA9IGZ1bmN0aW9uKG1ldGhvZHMsdHJ1dGgscHJlZGljdGVkKXsNCiAgDQogIFNTRSA9IGZ1bmN0aW9uKHRydXRoLCBwcmVkaWN0ZWQpIHsNCiAgICBzdW0oKHByZWRpY3RlZCAtIHRydXRoKV4yKQ0KICB9DQogIA0KICAjIE1TRQ0KICANCiAgTVNFID0gZnVuY3Rpb24odHJ1dGgsIHByZWRpY3RlZCkgew0KICAgIG1lYW4oKHByZWRpY3RlZCAtIHRydXRoKV4yKQ0KICB9DQogIA0KICAjIFJNU0UNCiAgDQogIFJNU0UgPSBmdW5jdGlvbih0cnV0aCwgcHJlZGljdGVkKSB7DQogICAgc3FydChNU0UodHJ1dGgsIHByZWRpY3RlZCkpDQogIH0NCiAgDQogICMgTUVEU0UNCiAgDQogIE1FRFNFID0gZnVuY3Rpb24odHJ1dGgsIHByZWRpY3RlZCkgew0KICAgIG1lZGlhbigocHJlZGljdGVkIC0gdHJ1dGgpXjIpDQogIH0NCiAgDQogICMgU0FFDQogIA0KICBTQUUgPSBmdW5jdGlvbih0cnV0aCwgcHJlZGljdGVkKSB7DQogICAgc3VtKGFicyhwcmVkaWN0ZWQgLSB0cnV0aCkpDQogIH0NCiAgDQogICMgTUFFDQogIA0KICBNQUUgPSBmdW5jdGlvbih0cnV0aCwgcHJlZGljdGVkKSB7DQogICAgbWVhbihhYnMocHJlZGljdGVkIC0gdHJ1dGgpKQ0KICB9DQogIA0KICAjIE1FREFFDQogIA0KICBNRURBRSA9IGZ1bmN0aW9uKHRydXRoLCBwcmVkaWN0ZWQpIHsNCiAgICBtZWRpYW4oYWJzKHByZWRpY3RlZCAtIHRydXRoKSkNCiAgfQ0KICANCiAgIyBSU1ENCiAgDQogIFJTUSA9IGZ1bmN0aW9uKHRydXRoLCAgcHJlZGljdGVkKSB7DQogICAgcnNzID0gU1NFKHRydXRoLCAgcHJlZGljdGVkKQ0KICAgIGVzcyA9IHN1bSgodHJ1dGggLSBtZWFuKHRydXRoKSleMkwpDQogICAgaWYgKGVzcyA9PSAwKXsNCiAgICAgIHdhcm5pbmcoIkVycm9yOiBhbGwgdHJ1dGggdmFsdWVzIGFyZSBlcXVhbCIpDQogICAgICByZXR1cm4oTkFfcmVhbF8pDQogICAgfQ0KICAgIDEgLSByc3MgLyBlc3MNCiAgfQ0KDQogICMgUlJTRSBSb290IHJlbGF0aXZlIHNxdWFyZWQgZXJyb3INCiAgDQogIFJSU0UgPSBmdW5jdGlvbih0cnV0aCwgcHJlZGljdGVkKXsNCiAgICB0c3MgPSBzdW0oKHRydXRoIC0gbWVhbih0cnV0aCkpXjJMKQ0KICAgIGlmICh0c3MgPT0gMCl7DQogICAgICB3YXJuaW5nKCJFcnJvcjogYWxsIHRydXRoIHZhbHVlcyBhcmUgZXF1YWwuIikNCiAgICAgIHJldHVybihOQV9yZWFsXykNCiAgICB9DQogICAgc3FydChTU0UodHJ1dGgsIHByZWRpY3RlZCkgLyB0c3MpDQogIH0NCiAgDQogICMgUkFFIDogUmVsYXRpdmUgYWJzb2x1dGUgZXJyb3INCiAgDQogIFJBRSA9IGZ1bmN0aW9uKHRydXRoLCBwcmVkaWN0ZWQpew0KICAgIG1lYW5hZCA9IHN1bShhYnModHJ1dGggLSBtZWFuKHRydXRoKSkpDQogICAgaWYgKG1lYW5hZCA9PSAwKXsNCiAgICAgIHdhcm5pbmcoIkVycm9yOiAgYWxsIHRydXRoIHZhbHVlcyBhcmUgZXF1YWwuIikNCiAgICAgIHJldHVybihOQV9yZWFsXykNCiAgICB9DQogICAgcmV0dXJuKFNBRSh0cnV0aCwgcHJlZGljdGVkKSAvIG1lYW5hZCkNCiAgfQ0KICANCiAgIyBNQVBFIE1lYW4gYWJzb2x1dGUgcGVyY2VudGFnZSBlcnJvcg0KICANCiAgTUFQRSA9IGZ1bmN0aW9uKHRydXRoLCBwcmVkaWN0ZWQpew0KICAgIGlmIChhbnkodHJ1dGggPT0gMCkpew0KICAgICAgd2FybmluZygiRXJyb3I6IHRydXRoIHZhbHVlIGlzIGVxdWFsIHRvIDAuIikNCiAgICAgIHJldHVybihOQV9yZWFsXykNCiAgICB9DQogICAgcmV0dXJuKG1lYW4oYWJzKCh0cnV0aCAtIHByZWRpY3RlZCkgLyB0cnV0aCkpKQ0KICB9DQogIA0KICAjIE1lYW4gc3F1YXJlZCBsb2dhcml0aG1pYyBlcnJvcg0KICANCiAgTVNMRSA9IGZ1bmN0aW9uKHRydXRoLCBwcmVkaWN0ZWQpIHsNCiAgICBpZiAoYW55KHRydXRoIDwgLTEpKQ0KICAgICAgc3RvcCgiQWxsIHRydXRoIHZhbHVlcyBtdXN0IGJlIGdyZWF0ZXIgb3IgZXF1YWwgLTEiKQ0KICAgIGlmIChhbnkocHJlZGljdGVkIDwgLTEpKQ0KICAgICAgc3RvcCgiQWxsIHByZWRpY3RlZCB2YWx1ZXMgbXVzdCBiZSBncmVhdGVyIG9yIGVxdWFsIC0xIikNCiAgICANCiAgICBtZWFuKChsb2cocHJlZGljdGVkICsgMSkgLSBsb2codHJ1dGggKyAxKSleMikNCiAgfQ0KICANCiAgIyBSb290IG1lYW4gc3F1YXJlZCBsb2dhcml0aG1pYyBlcnJvcg0KICANCiAgUk1TTEUgPSBmdW5jdGlvbih0cnV0aCwgcHJlZGljdGVkKSB7DQogICAgc3FydChNU0xFKHRydXRoLCBwcmVkaWN0ZWQpKQ0KICB9DQogIA0KICAjIEtlbmRhbGwgVGF1DQogIA0KICBLZW5kYWxsVGF1ID0gZnVuY3Rpb24odHJ1dGgsIHByZWRpY3RlZCkgew0KICAgIGNvcih0cnV0aCwgcHJlZGljdGVkLCB1c2UgPSAibmEub3IuY29tcGxldGUiLCBtZXRob2QgPSAia2VuZGFsbCIpDQogIH0NCiAgDQogICMgcmhvDQogIA0KICBTcGVhcm1hblJobyA9IGZ1bmN0aW9uKHRydXRoLCBwcmVkaWN0ZWQpIHsNCiAgICBjb3IodHJ1dGgsIHByZWRpY3RlZCwgdXNlID0gIm5hLm9yLmNvbXBsZXRlIiwgbWV0aG9kID0gInNwZWFybWFuIikNCiAgfQ0KICANCiAgIyBQZWFyc29uIHINCiAgDQogIFBlYXJzb25SID0gZnVuY3Rpb24odHJ1dGgsIHByZWRpY3RlZCkgew0KICAgIGNvcih0cnV0aCwgcHJlZGljdGVkLCB1c2UgPSAibmEub3IuY29tcGxldGUiLCBtZXRob2QgPSAicGVhcnNvbiIpDQogIH0NCiAgDQogIHNjb3JlcyA9IGRhdGFfZnJhbWUoU2NvcmU9bWV0aG9kcyxWYWx1ZT1yZXAoTkEsbGVuZ3RoKG1ldGhvZHMpKSkNCiAgDQogIGZvciAoaSBpbiAxOmxlbmd0aChtZXRob2RzKSl7DQogICAgc2NvcmluZ19mdW5jID0gZ2V0KG1ldGhvZHNbaV0pDQogICAgc2NvcmVzJFZhbHVlW2ldPSBzY29yaW5nX2Z1bmModHJ1dGgscHJlZGljdGVkKQ0KICB9DQogIA0KICByZXR1cm4oc2NvcmVzKQ0KfQ0KDQpgYGANCg0Kw4FwIGThu6VuZyBow6BtIG7DoHkgY2hvIGvhur90IHF14bqjIHRyw6puIHRlc3RzZXQ6IENow7ogw70gZGFuaCBwaMOhcCB2aeG6v3QgdOG6r3QgY+G7p2EgY8OhYyB0acOqdSBjaMOtOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnNjb3JlcyA9IHNjb3JpbmcobWV0aG9kcz1jKCdNQUUnLCdNQVBFJywnTUVEQUUnLCdNRURTRScsJ01TRScsJ01TTEUnLA0KICAgICAgICAgICAgICAgICAgJ1JBRScsJ1JNU0UnLCdSTVNMRScsJ1JSU0UnLCdSU1EnLCdTQUUnLCdTU0UnLA0KICAgICAgICAgICAgICAgICAgJ1BlYXJzb25SJywnS2VuZGFsbFRhdScsJ1NwZWFybWFuUmhvJyksDQogICAgICAgIHRydXRoPXBkZiR0cnV0aCwNCiAgICAgICAgcHJlZGljdGVkPXBkZiRwcmVkaWN0ZWQpDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQprbml0cjo6a2FibGUoc2NvcmVzKQ0KYGBgDQoNCkLDoGkgdGjhu7FjIGjDoG5oIMSR4bq/biDEkcOieSBsw6AgaOG6v3QuIFThuqFtIGJp4buHdCBjw6FjIGLhuqFuIHbDoCBo4bq5biBn4bq3cCBs4bqhaSA6KQ0K