1 Giới thiệu

Trong 2 bài trước, Nhi đã hướng dẫn các bạn cách tính thủ công tất cả các tiêu chí nhằm kiểm định mô hình phân loại nhị phân : https://rpubs.com/lengockhanhi/347941 và mô hình hồi quy : https://rpubs.com/lengockhanhi/445130 .Bài hôm nay Nhi sẽ tiếp tục giới thiệu một số tiêu chí đặc biệt dành cho mô hình phân loại multiclass.

Với bài toán Multiclass classification, mỗi trường hợp được dán 1 nhãn duy nhất trong tập hợp n nhãn giá trị được quy định từ trước. Như vậy các nhãn giá trị sẽ loại trừ lẫn nhau (khác với bài toán multilabel, khi một cá thể có thể nhận đồng thời nhiều nhãn).

2 Thí dụ minh họa

Trong thí dụ minh họa sau, ta có 400 bệnh nhân được xét nghiệm để đo nồng độ của 3 loại marker khác nhau là A,B,C. Mục tiêu của chúng ta là xây dựng một quy luật chẩn đoán để phân chia độ năng của một căn bệnh Y, với 4 mức độ gồm: Bình thường (Normal), bệnh nhẹ (Mild), bệnh trung bình (Moderate) và bệnh nặng (Severity), sử dụng chỉ số BMI và 3 biomarker A,B,C nói trên.

library(tidyverse)

df = read.csv("Multiclass.csv",sep = ',',dec = ".")
knitr::kable(head(df))
BMI Marker_A Marker_B Marker_C Severity
32.32 34.991 14.480 42.724 Moderate
31.31 86.754 89.415 102.693 Severe
27.40 54.385 74.213 63.430 Moderate
26.35 41.457 26.850 54.493 Moderate
23.24 16.252 12.590 33.832 Mild
27.70 45.826 27.428 55.275 Moderate
severity_col = c("#ffad2b","#ff2b55","#b1ef45","#a045ef")

df%>%gather(Marker_A,Marker_B,Marker_C,key="Marker",value="Score")%>%
  ggplot()+
  geom_density(aes(x=Score,fill=Severity),alpha=0.5)+
  theme_bw(8)+
  scale_fill_manual(values=severity_col)+
  scale_x_continuous(breaks=c(0,10,20,seq(from=30,to=300,by=20)))+
  facet_wrap(~Marker,
             ncol=1,scales = "free_y")

Trước kia, bài toán phân chia độ nặng của bệnh lý thường chỉ dựa vào 1 chỉ số lâm sàng hay cận lâm sàng duy nhất, thí dụ một marker X. Cách làm đơn giản nhất là phân chia thang đo của X thành nhiều phần dựa vào 1 quy luật phân bố nào đó, thí dụ tứ phân vị, trung vị, hoặc những ngưỡng cắt tùy tiện khác.

Nếu làm nghiêm túc hơn, người ta có thể dùng một mô hình multinomial logistic, hoặc dựa vào kết cục lâm sàng/tiên lượng sống còn … để xác định các ngưỡng cắt phù hợp cho từng độ nặng.Tuy nhiên, nếu quan sát phân bố của 3 marker trên biểu đồ, ta sẽ thấy rằng bất cứ biến số nào khi đứng một mình cũng không cho phép phân chia hiệu quả, ngay cả giữa 2 lớp cũng có sự chồng lắp đáng kể.

Ở thời đại mới, khi machine learning trở nên phổ biến, bài toán phân chia nhiều nhóm trở nên hết sức đơn giản, hơn nữa ta còn có thể đưa nhiều thông tin hơn vào mô hình. Một số algorithm xử lý bài toán multiclass một cách tự nhiên và chính xác, bao gồm mô hình cây, random forest hay neural network…

Để minh họa, Nhi sử dụng một mô hình RF, luyện trên 70% dữ liệu và kiểm định trên 30% còn lại. Nhi sẽ lần lượt giới thiệu các tiêu chí khác nhau, được dùng riêng cho loại mô hình multiclass này.

set.seed(1234)
idtest=caret::createDataPartition(y=df$Severity, p=0.7,list=FALSE)

trainset=df[idtest,]
testset=df[-idtest,]

table(trainset$Severity)
## 
##     Mild Moderate   Normal   Severe 
##       77       81       47       76
table(testset$Severity)
## 
##     Mild Moderate   Normal   Severe 
##       33       34       20       32
library(randomForest)

rfmod=randomForest(Severity~.,
                   data=trainset,
                   ntree=500,
                   mtry=2)

rfmod
## 
## Call:
##  randomForest(formula = Severity ~ ., data = trainset, ntree = 500,      mtry = 2) 
##                Type of random forest: classification
##                      Number of trees: 500
## No. of variables tried at each split: 2
## 
##         OOB estimate of  error rate: 30.96%
## Confusion matrix:
##          Mild Moderate Normal Severe class.error
## Mild       50       19      8      0   0.3506494
## Moderate   24       42      0     15   0.4814815
## Normal     10        0     37      0   0.2127660
## Severe      0       11      0     65   0.1447368

3 Confusion matrix multiclass

Đầu tiên, ta áp dụng mô hình cho testset, kết quả prediction gồm probability cho mỗi nhãn giá trị trong 4 nhãn, và kết quả phân loại (response).

predict(rfmod,newdata=testset,type="prob")%>%
  as.data.frame()%>%
  mutate(response=predict(rfmod,newdata=testset))->pred_df

knitr::kable(head(pred_df))
Mild Moderate Normal Severe response
0.502 0.480 0.018 0.000 Mild
0.048 0.514 0.000 0.438 Moderate
0.152 0.814 0.000 0.034 Moderate
0.600 0.398 0.000 0.002 Mild
0.076 0.360 0.000 0.564 Severe
0.026 0.786 0.002 0.186 Moderate

Cũng như bài toán nhị phân, cách kiểm định đơn giản nhất là dựng một confusion matrix. Cấu trúc của matrix này cũng tương tự, chỉ khác về kích thước với nhiều hàng và cột hơn. Độ chính xác của mô hình được thể hiện qua đường chéo của matrix:

truth = testset$Severity
response = pred_df$response
pred = pred_df[-5]

conf.mat = table(truth,response)

conf.mat%>%knitr::kable()
Mild Moderate Normal Severe
Mild 25 6 2 0
Moderate 5 23 2 4
Normal 3 0 17 0
Severe 0 8 0 24

Quan sát cho thấy mô hình tương đối chính xác, nó hoạt động tốt cho 2 nhãn Normal và Severe tuy nhiên mô hình nhầm lẫn giữa 2 nhóm Mild và Moderate (hiện tượng này cũng tương tự như khi ta dùng ROC curve để xác định ngưỡng cắt cho 1 biến số duy nhất, 2 đầu thang đo và những giá trị cực điểm lúc nào cũng dễ dàng được nhận ra hơn các vùng trung gian)

4 Độ chính xác sau cân bằng

Balanced accuracy : được định nghĩa là trung bình giữa tỉ lệ xác định đúng (TPR) và tỉ lệ loại trừ đúng (TNR). Như vậy, có thể tính BAC như sau:

# bac

diag(conf.mat) 
##     Mild Moderate   Normal   Severe 
##       25       23       17       24
table(truth, truth)
##           truth
## truth      Mild Moderate Normal Severe
##   Mild       33        0      0      0
##   Moderate    0       34      0      0
##   Normal      0        0     20      0
##   Severe      0        0      0     32
bac = mean(diag(table(truth, response) / table(truth, truth)))

bac
## [1] 0.7585116

Kết quả cho ra BAC = 0.758

Diễn giải: Giá trị tốt nhất của BAC là 1, xấu nhất =0.

5 Tỉ lệ nhầm lẫn sau cân bằng

Balanced error rate (BER) có ý nghĩa ngược lại với BAC, nó là tỉ lệ nhầm lẫn trung bình cho tất cả các nhãn (bài toán nhị phân có 2 nhãn, ở đây ta có 4 nhãn), BER có thể được tính thủ công từ 2 vector truth và response, nhưng cũng có thể tính nhanh bằng công thức BER = 1 - BAC.

Do BER nghịch với BAC, nên giá trị tối ưu của nó =0,giá trị xấu nhất =1

ber = mean(diag(1 - (table(truth, response) / table(truth, truth))))

rbind(bac,ber,1-bac)%>%knitr::kable()
bac 0.7585116
ber 0.2414884
0.2414884

6 Logloss

Logloss được định nghĩa bằng công thức: -mean(log(p_i)),với p_i là xác suất của nhãn giá trị thực sự ở trường hợp thứ i,

Lưu ý là logloss càng cao càng xấu, giá trị tối ưu của nó =0, nhưng không có giá trị tối đa, do đó khi tính logloss ta phải đặt ra 1 ngưỡng cho xác suất tối thiểu, thí dụ 1e-20, để nó không thể = 0 (không thể tính log của 0).

# logloss

pred_mat = pred

pred_mat[pred_mat > 1 - (1e-20)] = 1 - (1e-20)
pred_mat[pred_mat < (1e-20)] = 1e-20
  
c_truth = match(as.character(truth), colnames(pred_mat))
inds = cbind(BBmisc::seq_row(pred_mat), c_truth)
  
logloss = -1 * mean(log(pred[inds]))

logloss
## [1] 0.6772009

7 Kappa

Hệ số Kappa của Cohen là một trị số thống kê dùng để đánh giá mức độ tương hợp giữa 2 vector, được xác định theo công thức: kappa = 1 - (1 - p0) / (1 - pe).Với p0 = tần suất tương hợp trên thực tế và pe = tần suất tương hợp giả định (nếu 2 vector là độc lập với nhau)

Ta có thể tính thủ công kappa từ 2 vector truth và response như sau:

# conf mat
  
conf.mat = table(truth, response)
conf.mat = conf.mat / sum(conf.mat)
p0 = sum(diag(conf.mat))
rowsum = rowSums(conf.mat)
colsum = colSums(conf.mat)
pe = sum(rowsum * colsum) / sum(conf.mat)^2
# kappa
kappa = 1 - (1 - p0) / (1 - pe)

round(cbind(p0,pe,kappa),5) %>%knitr::kable()
p0 pe kappa
0.7479 0.25867 0.65994

8 Kappa hiệu chỉnh trọng số

Đặc biệt cho những trường hợp mà vector kết quả có tính thứ bậc (thí dụ độ nặng của bệnh lý), ta có hệ số kappa hiểu chỉnh bởi một matrix trọng số

Dưới đây là cách tính thủ công kappa với trọng số:

#Wkappa
conf.mat = table(truth, response)
conf.mat = conf.mat / sum(conf.mat)
# get expected probs under independence
rowsum = rowSums(conf.mat)
colsum = colSums(conf.mat)
expected.mat = rowsum %*% t(colsum)

# get weights
class.values = seq_along(levels(truth)) - 1L
k_weights = outer(class.values, class.values, FUN = function(x, y) (x - y)^2)

# calculate weighted kappa
wkappa = 1 - sum(k_weights * conf.mat) / sum(k_weights * expected.mat)


k_weights 
##      [,1] [,2] [,3] [,4]
## [1,]    0    1    4    9
## [2,]    1    0    1    4
## [3,]    4    1    0    1
## [4,]    9    4    1    0
round(rbind(kappa,wkappa),5) %>%knitr::kable()
kappa 0.65994
wkappa 0.73811

Cả 2 hệ số kappa này dao động từ -1 đến 1, giá trị tối ưu =1, xấu nhất =-1

9 Chỉ số Brier multiclass

Brier score là một tiêu chí đo lường nguy cơ sai lầm của mô hình, nó dao động từ 0 đến 2, giá trị tốt nhất =0 và xấu nhất =2. brier score được tính bằng công thức:

Brier score = (1/n) sum_i sum_j (y_ij - p_ij)^2, với y_ij = 1 nếu trường hợp i nhận nhãn j (trái lại, sẽ =0), còn p_ij là xác suất tiên lượng của mô hình cho nhãn j tại trường hợp i

  # multiclass brier
  
truth_f = factor(truth, levels = colnames(pred))
mat01 = mlr::createDummyFeatures(truth_f)
mc_brier = mean(rowSums((pred - mat01)^2))
  
mc_brier
## [1] 0.3735652

Brier score của mô hình = 0.37, một giá trị không tệ.

10 AU1P

Diện tích dưới đường cong ROC là một tiêu chí đo lường hiệu năng của một quy luật nhị phân, thí dụ sự phân biệt giữa 2 nhãn A và B. Một bài toán multiclass có C nhãn, do đó ta có thể dựng lên tất cả c(c-1) đường cong ROC để phân biệt từng cặp nhãn với nhau.

Tiêu chí AU1P là giá trị AUC trung bình của c(c - 1) đường cong ROC,nhưng có xét thêm tỉ lệ phân bố của mỗi class trong dữ liệu.

colAUC = function(samples, truth, maximum = TRUE) {
  y = as.factor(truth)
  X = as.matrix(samples)
  if (nrow(X) == 1)
    X = t(X)
  nr = nrow(X)
  nc = ncol(X)
  ny = table(y)
  ul = as.factor(rownames(ny))
  nl = length(ny)
  if (nl <= 1)
    stop("colAUC: List of labels 'y' have to contain at least 2 class labels.")
  if (!is.numeric(X))
    stop("colAUC: 'X' must be numeric")
  if (nr != length(y))
    stop("colAUC: length(y) and nrow(X) must be the same")
  per = t(utils::combn(1:nl, 2))
  np = nrow(per)
  auc = matrix(0.5, np, nc)
  rownames(auc) = paste(ul[per[, 1]], " vs. ", ul[per[, 2]], sep = "")
  colnames(auc) = colnames(X)
  # Wilcoxon AUC
  idxl = vector(mode = "list", length = nl)
  for (i in 1:nl) idxl[[i]] = which(y == ul[i])
  for (j in 1:nc) {
    for (i in 1:np) {
      c1 = per[i, 1]
      c2 = per[i, 2]
      n1 = as.numeric(ny[c1])
      n2 = as.numeric(ny[c2])
      if (n1 > 0 & n2 > 0) {
        r = rank(c(X[idxl[[c1]], j], X[idxl[[c2]], j]))
        auc[i, j] = (sum(r[1:n1]) - n1 * (n1 + 1) / 2) / (n1 * n2)
      }
    }
  }
  if (maximum == TRUE) {
    auc = pmax(auc, 1 - auc)
  }
  return(auc)
}
# au1p
m = colAUC(as.matrix(pred), truth)

weights = table(truth) / length(truth)

m = m * matrix(rep(weights, each = nrow(m)), ncol = length(weights))

m%>%knitr::kable()
Mild Moderate Normal Severe
Mild vs. Moderate 0.2449333 0.2291826 0.1398313 0.2224120
Mild vs. Normal 0.2296218 0.2666667 0.1591546 0.1861981
Mild vs. Severe 0.2723214 0.1904762 0.1567673 0.2647059
Moderate vs. Normal 0.1598616 0.2789916 0.1643599 0.2479486
Moderate vs. Severe 0.2346183 0.2351628 0.1190219 0.2429560
Normal vs. Severe 0.2214154 0.2174107 0.1680672 0.2686975
weights%>%knitr::kable()
truth Freq
Mild 0.2773109
Moderate 0.2857143
Normal 0.1680672
Severe 0.2689076

Tính AU1P:

c = c(combn(1:nlevels(truth), 2))

au1p = sum(m[cbind(rep(seq_len(nrow(m)), each = 2), c)]) / (nlevels(truth) - 1)

au1p
## [1] 0.9193849

11 AU1U

Tương tự, chỉ số AU1U cũng tính tring bình AUC của c(c - 1) đường cong ROC nhằm phân biệt bắt cặp tuần tự giữa các nhãn, nhưng khác với AU1P, AU1U giả định các nhãn có phân bố đồng dạng.

#au1u
  
m = colAUC(as.matrix(pred), truth)

m%>%knitr::kable()
Mild Moderate Normal Severe
Mild vs. Moderate 0.8832442 0.8021390 0.8319964 0.8270945
Mild vs. Normal 0.8280303 0.9333333 0.9469697 0.6924242
Mild vs. Severe 0.9820076 0.6666667 0.9327652 0.9843750
Moderate vs. Normal 0.5764706 0.9764706 0.9779412 0.9220588
Moderate vs. Severe 0.8460478 0.8230699 0.7081801 0.9034926
Normal vs. Severe 0.7984375 0.7609375 1.0000000 0.9992188
c = c(combn(1:nlevels(truth), 2))
au1u = mean(m[cbind(rep(seq_len(nrow(m)), each = 2), c)])

au1u
## [1] 0.9255799

12 AUNP

Tiêu chí AUNP cũng tính AUC trung bình, nhưng của 4 đường cong ROC tương ứng với 4 quy luật nhị phân, nhằm phân biệt 1 nhãn với 3 nhãn còn lại, có xét đến tỉ lệ phân bố thực tế của các nhãn.

aunp=sum(BBmisc::vnapply(1:nlevels(truth),function(i) mean(truth == levels(truth)[i]) * (colAUC(as.matrix(pred)[, i], truth == levels(truth)[i]))))

aunp
## [1] 0.9156638

13 AUNU

Tương tự, AUNU cũng xét 4 quy luật nhị phân như AUNP, nhưng với giả định các phân nhóm có phân bố đồng dạng

aunu=mean(BBmisc::vnapply(1:nlevels(truth),function(i) colAUC(as.matrix(pred)[, i],truth == levels(truth)[i])))

aunu
## [1] 0.9222792

14 Tổng kết

Bây giờ, ta tạo ra 1 hàm để tính đồng thời 10 tiêu chí kể trên từ matrix xác suất của C nhãn, vector nhãn thực tế và vector nhãn predict.

multiclass_perfomance = function(pred,truth,response){
  if (length(unique(truth)) != nlevels(truth)){
    warning("Error: Missing label(s) detected")
    return(NA_real_)
  }
  #aunu
  aunu=mean(BBmisc::vnapply(1:nlevels(truth),
                       function(i) colAUC(as.matrix(pred)[, i],
                                                   truth == levels(truth)[i])))
  # aunp
  aunp=sum(BBmisc::vnapply(1:nlevels(truth), 
                           function(i) mean(truth == levels(truth)[i]) * (colAUC(as.matrix(pred)[, i], 
                                                                                          truth == levels(truth)[i]))))
  # au1p
  m = colAUC(as.matrix(pred), truth)
  weights = table(truth) / length(truth)
  m = m * matrix(rep(weights, each = nrow(m)), ncol = length(weights))
  c = c(combn(1:nlevels(truth), 2))
  au1p = sum(m[cbind(rep(seq_len(nrow(m)), each = 2), c)]) / (nlevels(truth) - 1)
  
  #au1u
  
  m = colAUC(as.matrix(pred), truth)
  c = c(combn(1:nlevels(truth), 2))
  mean(m[cbind(rep(seq_len(nrow(m)), each = 2), c)])
  au1u = mean(m[cbind(rep(seq_len(nrow(m)), each = 2), c)])
  
  # conf mat
  
  conf.mat = table(truth, response)
  conf.mat = conf.mat / sum(conf.mat)
  p0 = sum(diag(conf.mat))
  rowsum = rowSums(conf.mat)
  colsum = colSums(conf.mat)
  pe = sum(rowsum * colsum) / sum(conf.mat)^2
  # kappa
  kappa = 1 - (1 - p0) / (1 - pe)
  
  #Wkappa
  
  conf.mat = table(truth, response)
  conf.mat = conf.mat / sum(conf.mat)
  # get expected probs under independence
  rowsum = rowSums(conf.mat)
  colsum = colSums(conf.mat)
  expected.mat = rowsum %*% t(colsum)
  
  # get weights
  class.values = seq_along(levels(truth)) - 1L
  k_weights = outer(class.values, class.values, FUN = function(x, y) (x - y)^2)
  
  # calculate weighted kappa
  wkappa = 1 - sum(k_weights * conf.mat) / sum(k_weights * expected.mat)
  
  # multiclass brier
  
  truth_f = factor(truth, levels = colnames(pred))
  mat01 = mlr::createDummyFeatures(truth_f)
  mc_brier = mean(rowSums((pred - mat01)^2))
  
  # bac
  bac = mean(diag(table(truth, response) / table(truth, truth)))
  
  # ber
  ber = mean(diag(1 - (table(truth, response) / table(truth, truth))))
  
  # logloss
  
  pred[pred > 1 - (1e-20)] = 1 - (1e-20)
  pred[pred < (1e-20)] = 1e-20
  
  c_truth = match(as.character(truth), colnames(pred))
  inds = cbind(BBmisc::seq_row(pred), c_truth)
  
  logloss = -1 * mean(log(pred[inds]))
  
  result = rbind(bac,ber,logloss,mc_brier,
                 aunu,aunp,au1u,au1p,
                 kappa,wkappa)
  
  return(result)
  }

Áp dụng hàm này để lấy kết quả của 10 tiêu chí.

multiclass_perfomance(truth=truth,
                      pred=pred,
                      response=response)%>%knitr::kable()
bac 0.7585116
ber 0.2414884
logloss 0.6772009
mc_brier 0.3735652
aunu 0.9222792
aunp 0.9156638
au1u 0.9255799
au1p 0.9193849
kappa 0.6599352
wkappa 0.7381062

Bài thực hành đến đây là kết thúc; hẹn gặp lại các bạn lần tới.

LS0tDQp0aXRsZTogIsSQw6FuaCBnacOhIG3DtCBow6xuaCBwaMOibiBsb+G6oWkgTXVsdGljbGFzcyIgDQphdXRob3I6ICJMw6ogTmfhu41jIEto4bqjIE5oaSINCmRhdGU6ICIxOCBUaMOhbmcgMSBuxINtIDIwMTgiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZGVmYXVsdCINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCiAgICBkZXY6ICdzdmcnDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmBgYA0KDQohW10obXVsdGljbGFzc21ldHJpYy5wbmcpDQoNCiMgR2nhu5tpIHRoaeG7h3UNCg0KVHJvbmcgMiBiw6BpIHRyxrDhu5tjLCBOaGkgxJHDoyBoxrDhu5tuZyBk4bqrbiBjw6FjIGLhuqFuIGPDoWNoIHTDrW5oIHRo4bunIGPDtG5nIHThuqV0IGPhuqMgY8OhYyB0acOqdSBjaMOtIG5o4bqxbSBraeG7g20gxJHhu4tuaCBtw7QgaMOsbmggcGjDom4gbG/huqFpIG5o4buLIHBow6JuIDogaHR0cHM6Ly9ycHVicy5jb20vbGVuZ29ja2hhbmhpLzM0Nzk0MSB2w6AgbcO0IGjDrG5oIGjhu5NpIHF1eSA6IGh0dHBzOi8vcnB1YnMuY29tL2xlbmdvY2toYW5oaS80NDUxMzAgLkLDoGkgaMO0bSBuYXkgTmhpIHPhur0gdGnhur9wIHThu6VjIGdp4bubaSB0aGnhu4d1IG3hu5l0IHPhu5EgdGnDqnUgY2jDrSDEkeG6t2MgYmnhu4d0IGTDoG5oIGNobyBtw7QgaMOsbmggcGjDom4gbG/huqFpIG11bHRpY2xhc3MuDQoNClbhu5tpIGLDoGkgdG/DoW4gTXVsdGljbGFzcyBjbGFzc2lmaWNhdGlvbiwgbeG7l2kgdHLGsOG7nW5nIGjhu6NwIMSRxrDhu6NjIGTDoW4gMSBuaMOjbiBkdXkgbmjhuqV0IHRyb25nIHThuq1wIGjhu6NwIG4gbmjDo24gZ2nDoSB0cuG7iyDEkcaw4bujYyBxdXkgxJHhu4tuaCB04burIHRyxrDhu5tjLiBOaMawIHbhuq15IGPDoWMgbmjDo24gZ2nDoSB0cuG7iyBz4bq9IGxv4bqhaSB0cuG7qyBs4bqrbiBuaGF1IChraMOhYyB24bubaSBiw6BpIHRvw6FuIG11bHRpbGFiZWwsIGtoaSBt4buZdCBjw6EgdGjhu4MgY8OzIHRo4buDIG5o4bqtbiDEkeG7k25nIHRo4budaSBuaGnhu4F1IG5ow6NuKS4NCg0KIyBUaMOtIGThu6UgbWluaCBo4buNYQ0KDQpUcm9uZyB0aMOtIGThu6UgbWluaCBo4buNYSBzYXUsIHRhIGPDsyA0MDAgYuG7h25oIG5ow6JuIMSRxrDhu6NjIHjDqXQgbmdoaeG7h20gxJHhu4MgxJFvIG7hu5NuZyDEkeG7mSBj4bunYSAzIGxv4bqhaSBtYXJrZXIga2jDoWMgbmhhdSBsw6AgQSxCLEMuIE3hu6VjIHRpw6p1IGPhu6dhIGNow7puZyB0YSBsw6AgeMOieSBk4buxbmcgbeG7mXQgcXV5IGx14bqtdCBjaOG6qW4gxJFvw6FuIMSR4buDIHBow6JuIGNoaWEgxJHhu5kgbsSDbmcgY+G7p2EgbeG7mXQgY8SDbiBi4buHbmggWSwgduG7m2kgNCBt4bupYyDEkeG7mSBn4buTbTogQsOsbmggdGjGsOG7nW5nIChOb3JtYWwpLCBi4buHbmggbmjhurkgKE1pbGQpLCBi4buHbmggdHJ1bmcgYsOsbmggKE1vZGVyYXRlKSB2w6AgYuG7h25oIG7hurduZyAoU2V2ZXJpdHkpLCBz4butIGThu6VuZyBjaOG7iSBz4buRIEJNSSB2w6AgMyBiaW9tYXJrZXIgQSxCLEMgbsOzaSB0csOqbi4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KZGYgPSByZWFkLmNzdigiTXVsdGljbGFzcy5jc3YiLHNlcCA9ICcsJyxkZWMgPSAiLiIpDQprbml0cjo6a2FibGUoaGVhZChkZikpDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzZXZlcml0eV9jb2wgPSBjKCIjZmZhZDJiIiwiI2ZmMmI1NSIsIiNiMWVmNDUiLCIjYTA0NWVmIikNCg0KZGYlPiVnYXRoZXIoTWFya2VyX0EsTWFya2VyX0IsTWFya2VyX0Msa2V5PSJNYXJrZXIiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdCgpKw0KICBnZW9tX2RlbnNpdHkoYWVzKHg9U2NvcmUsZmlsbD1TZXZlcml0eSksYWxwaGE9MC41KSsNCiAgdGhlbWVfYncoOCkrDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1zZXZlcml0eV9jb2wpKw0KICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzPWMoMCwxMCwyMCxzZXEoZnJvbT0zMCx0bz0zMDAsYnk9MjApKSkrDQogIGZhY2V0X3dyYXAofk1hcmtlciwNCiAgICAgICAgICAgICBuY29sPTEsc2NhbGVzID0gImZyZWVfeSIpDQpgYGANCg0KVHLGsOG7m2Mga2lhLCBiw6BpIHRvw6FuIHBow6JuIGNoaWEgxJHhu5kgbuG6t25nIGPhu6dhIGLhu4duaCBsw70gdGjGsOG7nW5nIGNo4buJIGThu7FhIHbDoG8gMSBjaOG7iSBz4buRIGzDom0gc8OgbmcgaGF5IGPhuq1uIGzDom0gc8OgbmcgZHV5IG5o4bqldCwgdGjDrSBk4bulIG3hu5l0IG1hcmtlciBYLiBDw6FjaCBsw6BtIMSRxqFuIGdp4bqjbiBuaOG6pXQgbMOgIHBow6JuIGNoaWEgdGhhbmcgxJFvIGPhu6dhIFggdGjDoG5oIG5oaeG7gXUgcGjhuqduIGThu7FhIHbDoG8gMSBxdXkgbHXhuq10IHBow6JuIGLhu5EgbsOgbyDEkcOzLCB0aMOtIGThu6UgdOG7qSBwaMOibiB24buLLCB0cnVuZyB24buLLCBob+G6t2Mgbmjhu69uZyBuZ8aw4buhbmcgY+G6r3QgdMO5eSB0aeG7h24ga2jDoWMuDQoNCk7hur91IGzDoG0gbmdoacOqbSB0w7pjIGjGoW4sIG5nxrDhu51pIHRhIGPDsyB0aOG7gyBkw7luZyBt4buZdCBtw7QgaMOsbmggbXVsdGlub21pYWwgbG9naXN0aWMsIGhv4bq3YyBk4buxYSB2w6BvIGvhur90IGPhu6VjIGzDom0gc8OgbmcvdGnDqm4gbMaw4bujbmcgc+G7kW5nIGPDsm4gLi4uIMSR4buDIHjDoWMgxJHhu4tuaCBjw6FjIG5nxrDhu6FuZyBj4bqvdCBwaMO5IGjhu6NwIGNobyB04burbmcgxJHhu5kgbuG6t25nLlR1eSBuaGnDqm4sIG7hur91IHF1YW4gc8OhdCBwaMOibiBi4buRIGPhu6dhIDMgbWFya2VyIHRyw6puIGJp4buDdSDEkeG7kywgdGEgc+G6vSB0aOG6pXkgcuG6sW5nIGLhuqV0IGPhu6kgYmnhur9uIHPhu5EgbsOgbyBraGkgxJHhu6luZyBt4buZdCBtw6xuaCBjxaluZyBraMO0bmcgY2hvIHBow6lwIHBow6JuIGNoaWEgaGnhu4d1IHF14bqjLCBuZ2F5IGPhuqMgZ2nhu69hIDIgbOG7m3AgY8WpbmcgY8OzIHPhu7EgY2jhu5NuZyBs4bqvcCDEkcOhbmcga+G7gy4NCg0K4bueIHRo4budaSDEkeG6oWkgbeG7m2ksIGtoaSBtYWNoaW5lIGxlYXJuaW5nIHRy4bufIG7Dqm4gcGjhu5UgYmnhur9uLCBiw6BpIHRvw6FuIHBow6JuIGNoaWEgbmhp4buBdSBuaMOzbSB0cuG7nyBuw6puIGjhur90IHPhu6ljIMSRxqFuIGdp4bqjbiwgaMahbiBu4buvYSB0YSBjw7JuIGPDsyB0aOG7gyDEkcawYSBuaGnhu4F1IHRow7RuZyB0aW4gaMahbiB2w6BvIG3DtCBow6xuaC4gTeG7mXQgc+G7kSBhbGdvcml0aG0geOG7rSBsw70gYsOgaSB0b8OhbiBtdWx0aWNsYXNzIG3hu5l0IGPDoWNoIHThu7Egbmhpw6puIHbDoCBjaMOtbmggeMOhYywgYmFvIGfhu5NtIG3DtCBow6xuaCBjw6J5LCByYW5kb20gZm9yZXN0IGhheSBuZXVyYWwgbmV0d29yay4uLiANCg0KxJDhu4MgbWluaCBo4buNYSwgTmhpIHPhu60gZOG7pW5nIG3hu5l0IG3DtCBow6xuaCBSRiwgbHV54buHbiB0csOqbiA3MCUgZOG7ryBsaeG7h3UgdsOgIGtp4buDbSDEkeG7i25oIHRyw6puIDMwJSBjw7JuIGzhuqFpLiBOaGkgc+G6vSBs4bqnbiBsxrDhu6N0IGdp4bubaSB0aGnhu4d1IGPDoWMgdGnDqnUgY2jDrSBraMOhYyBuaGF1LCDEkcaw4bujYyBkw7luZyByacOqbmcgY2hvIGxv4bqhaSBtw7QgaMOsbmggbXVsdGljbGFzcyBuw6B5Lg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnNldC5zZWVkKDEyMzQpDQppZHRlc3Q9Y2FyZXQ6OmNyZWF0ZURhdGFQYXJ0aXRpb24oeT1kZiRTZXZlcml0eSwgcD0wLjcsbGlzdD1GQUxTRSkNCg0KdHJhaW5zZXQ9ZGZbaWR0ZXN0LF0NCnRlc3RzZXQ9ZGZbLWlkdGVzdCxdDQoNCnRhYmxlKHRyYWluc2V0JFNldmVyaXR5KQ0KdGFibGUodGVzdHNldCRTZXZlcml0eSkNCmBgYA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkocmFuZG9tRm9yZXN0KQ0KDQpyZm1vZD1yYW5kb21Gb3Jlc3QoU2V2ZXJpdHl+LiwNCiAgICAgICAgICAgICAgICAgICBkYXRhPXRyYWluc2V0LA0KICAgICAgICAgICAgICAgICAgIG50cmVlPTUwMCwNCiAgICAgICAgICAgICAgICAgICBtdHJ5PTIpDQoNCnJmbW9kDQpgYGANCg0KIyBDb25mdXNpb24gbWF0cml4IG11bHRpY2xhc3MNCg0KxJDhuqd1IHRpw6puLCB0YSDDoXAgZOG7pW5nIG3DtCBow6xuaCBjaG8gdGVzdHNldCwga+G6v3QgcXXhuqMgcHJlZGljdGlvbiBn4buTbSBwcm9iYWJpbGl0eSBjaG8gbeG7l2kgbmjDo24gZ2nDoSB0cuG7iyB0cm9uZyA0IG5ow6NuLCB2w6Aga+G6v3QgcXXhuqMgcGjDom4gbG/huqFpIChyZXNwb25zZSkuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KcHJlZGljdChyZm1vZCxuZXdkYXRhPXRlc3RzZXQsdHlwZT0icHJvYiIpJT4lDQogIGFzLmRhdGEuZnJhbWUoKSU+JQ0KICBtdXRhdGUocmVzcG9uc2U9cHJlZGljdChyZm1vZCxuZXdkYXRhPXRlc3RzZXQpKS0+cHJlZF9kZg0KDQprbml0cjo6a2FibGUoaGVhZChwcmVkX2RmKSkNCmBgYA0KDQpDxaluZyBuaMawIGLDoGkgdG/DoW4gbmjhu4sgcGjDom4sIGPDoWNoIGtp4buDbSDEkeG7i25oIMSRxqFuIGdp4bqjbiBuaOG6pXQgbMOgIGThu7FuZyBt4buZdCBjb25mdXNpb24gbWF0cml4LiBD4bqldSB0csO6YyBj4bunYSBtYXRyaXggbsOgeSBjxaluZyB0xrDGoW5nIHThu7EsIGNo4buJIGtow6FjIHbhu4Ega8OtY2ggdGjGsOG7m2MgduG7m2kgbmhp4buBdSBow6BuZyB2w6AgY+G7mXQgaMahbi4gxJDhu5kgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oIMSRxrDhu6NjIHRo4buDIGhp4buHbiBxdWEgxJHGsOG7nW5nIGNow6lvIGPhu6dhIG1hdHJpeDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQp0cnV0aCA9IHRlc3RzZXQkU2V2ZXJpdHkNCnJlc3BvbnNlID0gcHJlZF9kZiRyZXNwb25zZQ0KcHJlZCA9IHByZWRfZGZbLTVdDQoNCmNvbmYubWF0ID0gdGFibGUodHJ1dGgscmVzcG9uc2UpDQoNCmNvbmYubWF0JT4la25pdHI6OmthYmxlKCkNCmBgYA0KDQpRdWFuIHPDoXQgY2hvIHRo4bqleSBtw7QgaMOsbmggdMawxqFuZyDEkeG7kWkgY2jDrW5oIHjDoWMsIG7DsyBob+G6oXQgxJHhu5luZyB04buRdCBjaG8gMiBuaMOjbiBOb3JtYWwgdsOgIFNldmVyZSB0dXkgbmhpw6puIG3DtCBow6xuaCBuaOG6p20gbOG6q24gZ2nhu69hIDIgbmjDs20gTWlsZCB2w6AgTW9kZXJhdGUgKGhp4buHbiB0xrDhu6NuZyBuw6B5IGPFqW5nIHTGsMahbmcgdOG7sSBuaMawIGtoaSB0YSBkw7luZyBST0MgY3VydmUgxJHhu4MgeMOhYyDEkeG7i25oIG5nxrDhu6FuZyBj4bqvdCBjaG8gMSBiaeG6v24gc+G7kSBkdXkgbmjhuqV0LCAyIMSR4bqndSB0aGFuZyDEkW8gdsOgIG5o4buvbmcgZ2nDoSB0cuG7iyBj4buxYyDEkWnhu4NtIGzDumMgbsOgbyBjxaluZyBk4buFIGTDoG5nIMSRxrDhu6NjIG5o4bqtbiByYSBoxqFuIGPDoWMgdsO5bmcgdHJ1bmcgZ2lhbikNCg0KIyDEkOG7mSBjaMOtbmggeMOhYyBzYXUgY8OibiBi4bqxbmcNCg0KQmFsYW5jZWQgYWNjdXJhY3kgOiDEkcaw4bujYyDEkeG7i25oIG5naMSpYSBsw6AgdHJ1bmcgYsOsbmggZ2nhu69hIHThu4kgbOG7hyB4w6FjIMSR4buLbmggxJHDum5nIChUUFIpIHbDoCB04buJIGzhu4cgbG/huqFpIHRy4burIMSRw7puZyAoVE5SKS4gTmjGsCB24bqteSwgY8OzIHRo4buDIHTDrW5oIEJBQyBuaMawIHNhdTogDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KIyBiYWMNCg0KZGlhZyhjb25mLm1hdCkgDQoNCnRhYmxlKHRydXRoLCB0cnV0aCkNCg0KYmFjID0gbWVhbihkaWFnKHRhYmxlKHRydXRoLCByZXNwb25zZSkgLyB0YWJsZSh0cnV0aCwgdHJ1dGgpKSkNCg0KYmFjDQpgYGANCg0KS+G6v3QgcXXhuqMgY2hvIHJhIEJBQyA9IDAuNzU4DQoNCkRp4buFbiBnaeG6o2k6IEdpw6EgdHLhu4sgdOG7kXQgbmjhuqV0IGPhu6dhIEJBQyBsw6AgMSwgeOG6pXUgbmjhuqV0ID0wLg0KDQojIFThu4kgbOG7hyBuaOG6p20gbOG6q24gc2F1IGPDom4gYuG6sW5nDQoNCkJhbGFuY2VkIGVycm9yIHJhdGUgKEJFUikgY8OzIMO9IG5naMSpYSBuZ8aw4bujYyBs4bqhaSB24bubaSBCQUMsIG7DsyBsw6AgdOG7iSBs4buHIG5o4bqnbSBs4bqrbiB0cnVuZyBiw6xuaCBjaG8gdOG6pXQgY+G6oyBjw6FjIG5ow6NuIChiw6BpIHRvw6FuIG5o4buLIHBow6JuIGPDsyAyIG5ow6NuLCDhu58gxJHDonkgdGEgY8OzIDQgbmjDo24pLCBCRVIgY8OzIHRo4buDIMSRxrDhu6NjIHTDrW5oIHRo4bunIGPDtG5nIHThu6sgMiB2ZWN0b3IgdHJ1dGggdsOgIHJlc3BvbnNlLCBuaMawbmcgY8WpbmcgY8OzIHRo4buDIHTDrW5oIG5oYW5oIGLhurFuZyBjw7RuZyB0aOG7qWMgQkVSID0gMSAtIEJBQy4NCg0KRG8gQkVSIG5naOG7i2NoIHbhu5tpIEJBQywgbsOqbiBnacOhIHRy4buLIHThu5FpIMawdSBj4bunYSBuw7MgPTAsZ2nDoSB0cuG7iyB44bqldSBuaOG6pXQgPTENCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpiZXIgPSBtZWFuKGRpYWcoMSAtICh0YWJsZSh0cnV0aCwgcmVzcG9uc2UpIC8gdGFibGUodHJ1dGgsIHRydXRoKSkpKQ0KDQpyYmluZChiYWMsYmVyLDEtYmFjKSU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KIyBMb2dsb3NzDQoNCkxvZ2xvc3MgxJHGsOG7o2MgxJHhu4tuaCBuZ2jEqWEgYuG6sW5nIGPDtG5nIHRo4bupYzogLW1lYW4obG9nKHBfaSkpLHbhu5tpIHBfaSBsw6AgeMOhYyBzdeG6pXQgY+G7p2EgbmjDo24gZ2nDoSB0cuG7iyB0aOG7sWMgc+G7sSDhu58gdHLGsOG7nW5nIGjhu6NwIHRo4bupIGksIA0KDQpMxrB1IMO9IGzDoCBsb2dsb3NzIGPDoG5nIGNhbyBjw6BuZyB44bqldSwgZ2nDoSB0cuG7iyB04buRaSDGsHUgY+G7p2EgbsOzID0wLCBuaMawbmcga2jDtG5nIGPDsyBnacOhIHRy4buLIHThu5FpIMSRYSwgZG8gxJHDsyBraGkgdMOtbmggbG9nbG9zcyB0YSBwaOG6o2kgxJHhurd0IHJhIDEgbmfGsOG7oW5nIGNobyB4w6FjIHN14bqldCB04buRaSB0aGnhu4N1LCB0aMOtIGThu6UgMWUtMjAsIMSR4buDIG7DsyBraMO0bmcgdGjhu4MgPSAwIChraMO0bmcgdGjhu4MgdMOtbmggbG9nIGPhu6dhIDApLg0KDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KIyBsb2dsb3NzDQoNCnByZWRfbWF0ID0gcHJlZA0KDQpwcmVkX21hdFtwcmVkX21hdCA+IDEgLSAoMWUtMjApXSA9IDEgLSAoMWUtMjApDQpwcmVkX21hdFtwcmVkX21hdCA8ICgxZS0yMCldID0gMWUtMjANCiAgDQpjX3RydXRoID0gbWF0Y2goYXMuY2hhcmFjdGVyKHRydXRoKSwgY29sbmFtZXMocHJlZF9tYXQpKQ0KaW5kcyA9IGNiaW5kKEJCbWlzYzo6c2VxX3JvdyhwcmVkX21hdCksIGNfdHJ1dGgpDQogIA0KbG9nbG9zcyA9IC0xICogbWVhbihsb2cocHJlZFtpbmRzXSkpDQoNCmxvZ2xvc3MNCmBgYA0KDQojIEthcHBhDQoNCkjhu4cgc+G7kSBLYXBwYSBj4bunYSBDb2hlbiBsw6AgbeG7mXQgdHLhu4sgc+G7kSB0aOG7kW5nIGvDqiBkw7luZyDEkeG7gyDEkcOhbmggZ2nDoSBt4bupYyDEkeG7mSB0xrDGoW5nIGjhu6NwIGdp4buvYSAyIHZlY3RvciwgxJHGsOG7o2MgeMOhYyDEkeG7i25oIHRoZW8gY8O0bmcgdGjhu6ljOiAga2FwcGEgPSAxIC0gKDEgLSBwMCkgLyAoMSAtIHBlKS5W4bubaSBwMCA9IHThuqduIHN14bqldCB0xrDGoW5nIGjhu6NwICB0csOqbiB0aOG7sWMgdOG6vyB2w6AgcGUgPSB04bqnbiBzdeG6pXQgdMawxqFuZyBo4bujcCBnaeG6oyDEkeG7i25oIChu4bq/dSAyIHZlY3RvciBsw6AgxJHhu5ljIGzhuq1wIHbhu5tpIG5oYXUpDQoNClRhIGPDsyB0aOG7gyB0w61uaCB0aOG7pyBjw7RuZyBrYXBwYSB04burIDIgdmVjdG9yIHRydXRoIHbDoCByZXNwb25zZSBuaMawIHNhdToNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQojIGNvbmYgbWF0DQogIA0KY29uZi5tYXQgPSB0YWJsZSh0cnV0aCwgcmVzcG9uc2UpDQpjb25mLm1hdCA9IGNvbmYubWF0IC8gc3VtKGNvbmYubWF0KQ0KcDAgPSBzdW0oZGlhZyhjb25mLm1hdCkpDQpyb3dzdW0gPSByb3dTdW1zKGNvbmYubWF0KQ0KY29sc3VtID0gY29sU3Vtcyhjb25mLm1hdCkNCnBlID0gc3VtKHJvd3N1bSAqIGNvbHN1bSkgLyBzdW0oY29uZi5tYXQpXjINCiMga2FwcGENCmthcHBhID0gMSAtICgxIC0gcDApIC8gKDEgLSBwZSkNCg0Kcm91bmQoY2JpbmQocDAscGUsa2FwcGEpLDUpICU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KIyBLYXBwYSBoaeG7h3UgY2jhu4luaCB0cuG7jW5nIHPhu5ENCg0KxJDhurdjIGJp4buHdCBjaG8gbmjhu69uZyB0csaw4budbmcgaOG7o3AgbcOgIHZlY3RvciBr4bq/dCBxdeG6oyBjw7MgdMOtbmggdGjhu6kgYuG6rWMgKHRow60gZOG7pSDEkeG7mSBu4bq3bmcgY+G7p2EgYuG7h25oIGzDvSksIHRhIGPDsyBo4buHIHPhu5Ega2FwcGEgaGnhu4N1IGNo4buJbmggYuG7n2kgbeG7mXQgbWF0cml4IHRy4buNbmcgc+G7kQ0KDQpExrDhu5tpIMSRw6J5IGzDoCBjw6FjaCB0w61uaCB0aOG7pyBjw7RuZyBrYXBwYSB24bubaSB0cuG7jW5nIHPhu5E6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KI1drYXBwYQ0KY29uZi5tYXQgPSB0YWJsZSh0cnV0aCwgcmVzcG9uc2UpDQpjb25mLm1hdCA9IGNvbmYubWF0IC8gc3VtKGNvbmYubWF0KQ0KIyBnZXQgZXhwZWN0ZWQgcHJvYnMgdW5kZXIgaW5kZXBlbmRlbmNlDQpyb3dzdW0gPSByb3dTdW1zKGNvbmYubWF0KQ0KY29sc3VtID0gY29sU3Vtcyhjb25mLm1hdCkNCmV4cGVjdGVkLm1hdCA9IHJvd3N1bSAlKiUgdChjb2xzdW0pDQoNCiMgZ2V0IHdlaWdodHMNCmNsYXNzLnZhbHVlcyA9IHNlcV9hbG9uZyhsZXZlbHModHJ1dGgpKSAtIDFMDQprX3dlaWdodHMgPSBvdXRlcihjbGFzcy52YWx1ZXMsIGNsYXNzLnZhbHVlcywgRlVOID0gZnVuY3Rpb24oeCwgeSkgKHggLSB5KV4yKQ0KDQojIGNhbGN1bGF0ZSB3ZWlnaHRlZCBrYXBwYQ0Kd2thcHBhID0gMSAtIHN1bShrX3dlaWdodHMgKiBjb25mLm1hdCkgLyBzdW0oa193ZWlnaHRzICogZXhwZWN0ZWQubWF0KQ0KDQoNCmtfd2VpZ2h0cyANCg0Kcm91bmQocmJpbmQoa2FwcGEsd2thcHBhKSw1KSAlPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCkPhuqMgMiBo4buHIHPhu5Ega2FwcGEgbsOgeSBkYW8gxJHhu5luZyB04burIC0xIMSR4bq/biAxLCBnacOhIHRy4buLIHThu5FpIMawdSA9MSwgeOG6pXUgbmjhuqV0ID0tMQ0KDQojIENo4buJIHPhu5EgQnJpZXIgbXVsdGljbGFzcw0KDQpCcmllciBzY29yZSBsw6AgbeG7mXQgdGnDqnUgY2jDrSDEkW8gbMaw4budbmcgbmd1eSBjxqEgc2FpIGzhuqdtIGPhu6dhIG3DtCBow6xuaCwgbsOzIGRhbyDEkeG7mW5nIHThu6sgMCDEkeG6v24gMiwgZ2nDoSB0cuG7iyB04buRdCBuaOG6pXQgPTAgdsOgIHjhuqV1IG5o4bqldCA9Mi4gYnJpZXIgc2NvcmUgxJHGsOG7o2MgdMOtbmggYuG6sW5nIGPDtG5nIHRo4bupYzogDQoNCkJyaWVyIHNjb3JlID0gICgxL24pIHN1bV9pIHN1bV9qICh5X2lqIC0gcF9paileMiwgduG7m2kgeV9paiA9IDEgbuG6v3UgdHLGsOG7nW5nIGjhu6NwIGkgbmjhuq1uIG5ow6NuIGogKHRyw6FpIGzhuqFpLCBz4bq9ID0wKSwgY8OybiAgcF9paiBsw6AgeMOhYyBzdeG6pXQgdGnDqm4gbMaw4bujbmcgY+G7p2EgbcO0IGjDrG5oIGNobyBuaMOjbiBqIHThuqFpIHRyxrDhu51uZyBo4bujcCBpIA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCiAgIyBtdWx0aWNsYXNzIGJyaWVyDQogIA0KdHJ1dGhfZiA9IGZhY3Rvcih0cnV0aCwgbGV2ZWxzID0gY29sbmFtZXMocHJlZCkpDQptYXQwMSA9IG1scjo6Y3JlYXRlRHVtbXlGZWF0dXJlcyh0cnV0aF9mKQ0KbWNfYnJpZXIgPSBtZWFuKHJvd1N1bXMoKHByZWQgLSBtYXQwMSleMikpDQogIA0KbWNfYnJpZXINCmBgYA0KDQpCcmllciBzY29yZSBj4bunYSBtw7QgaMOsbmggPSAwLjM3LCBt4buZdCBnacOhIHRy4buLIGtow7RuZyB04buHLg0KDQojIEFVMVANCg0KRGnhu4duIHTDrWNoIGTGsOG7m2kgxJHGsOG7nW5nIGNvbmcgUk9DIGzDoCBt4buZdCB0acOqdSBjaMOtIMSRbyBsxrDhu51uZyBoaeG7h3UgbsSDbmcgY+G7p2EgbeG7mXQgcXV5IGx14bqtdCBuaOG7iyBwaMOibiwgdGjDrSBk4bulIHPhu7EgcGjDom4gYmnhu4d0IGdp4buvYSAyIG5ow6NuIEEgdsOgIEIuIE3hu5l0IGLDoGkgdG/DoW4gbXVsdGljbGFzcyBjw7MgQyBuaMOjbiwgZG8gxJHDsyB0YSBjw7MgdGjhu4MgZOG7sW5nIGzDqm4gdOG6pXQgY+G6oyBjKGMtMSkgxJHGsOG7nW5nIGNvbmcgUk9DIMSR4buDIHBow6JuIGJp4buHdCB04burbmcgY+G6t3AgbmjDo24gduG7m2kgbmhhdS4NCg0KVGnDqnUgY2jDrSBBVTFQIGzDoCBnacOhIHRy4buLIEFVQyB0cnVuZyBiw6xuaCBj4bunYSBjKGMgLSAxKSDEkcaw4budbmcgY29uZyBST0MsbmjGsG5nIGPDsyB4w6l0IHRow6ptIHThu4kgbOG7hyBwaMOibiBi4buRIGPhu6dhIG3hu5dpIGNsYXNzIHRyb25nIGThu68gbGnhu4d1Lg0KDQpgYGB7cn0NCmNvbEFVQyA9IGZ1bmN0aW9uKHNhbXBsZXMsIHRydXRoLCBtYXhpbXVtID0gVFJVRSkgew0KICB5ID0gYXMuZmFjdG9yKHRydXRoKQ0KICBYID0gYXMubWF0cml4KHNhbXBsZXMpDQogIGlmIChucm93KFgpID09IDEpDQogICAgWCA9IHQoWCkNCiAgbnIgPSBucm93KFgpDQogIG5jID0gbmNvbChYKQ0KICBueSA9IHRhYmxlKHkpDQogIHVsID0gYXMuZmFjdG9yKHJvd25hbWVzKG55KSkNCiAgbmwgPSBsZW5ndGgobnkpDQogIGlmIChubCA8PSAxKQ0KICAgIHN0b3AoImNvbEFVQzogTGlzdCBvZiBsYWJlbHMgJ3knIGhhdmUgdG8gY29udGFpbiBhdCBsZWFzdCAyIGNsYXNzIGxhYmVscy4iKQ0KICBpZiAoIWlzLm51bWVyaWMoWCkpDQogICAgc3RvcCgiY29sQVVDOiAnWCcgbXVzdCBiZSBudW1lcmljIikNCiAgaWYgKG5yICE9IGxlbmd0aCh5KSkNCiAgICBzdG9wKCJjb2xBVUM6IGxlbmd0aCh5KSBhbmQgbnJvdyhYKSBtdXN0IGJlIHRoZSBzYW1lIikNCiAgcGVyID0gdCh1dGlsczo6Y29tYm4oMTpubCwgMikpDQogIG5wID0gbnJvdyhwZXIpDQogIGF1YyA9IG1hdHJpeCgwLjUsIG5wLCBuYykNCiAgcm93bmFtZXMoYXVjKSA9IHBhc3RlKHVsW3BlclssIDFdXSwgIiB2cy4gIiwgdWxbcGVyWywgMl1dLCBzZXAgPSAiIikNCiAgY29sbmFtZXMoYXVjKSA9IGNvbG5hbWVzKFgpDQogICMgV2lsY294b24gQVVDDQogIGlkeGwgPSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gbmwpDQogIGZvciAoaSBpbiAxOm5sKSBpZHhsW1tpXV0gPSB3aGljaCh5ID09IHVsW2ldKQ0KICBmb3IgKGogaW4gMTpuYykgew0KICAgIGZvciAoaSBpbiAxOm5wKSB7DQogICAgICBjMSA9IHBlcltpLCAxXQ0KICAgICAgYzIgPSBwZXJbaSwgMl0NCiAgICAgIG4xID0gYXMubnVtZXJpYyhueVtjMV0pDQogICAgICBuMiA9IGFzLm51bWVyaWMobnlbYzJdKQ0KICAgICAgaWYgKG4xID4gMCAmIG4yID4gMCkgew0KICAgICAgICByID0gcmFuayhjKFhbaWR4bFtbYzFdXSwgal0sIFhbaWR4bFtbYzJdXSwgal0pKQ0KICAgICAgICBhdWNbaSwgal0gPSAoc3VtKHJbMTpuMV0pIC0gbjEgKiAobjEgKyAxKSAvIDIpIC8gKG4xICogbjIpDQogICAgICB9DQogICAgfQ0KICB9DQogIGlmIChtYXhpbXVtID09IFRSVUUpIHsNCiAgICBhdWMgPSBwbWF4KGF1YywgMSAtIGF1YykNCiAgfQ0KICByZXR1cm4oYXVjKQ0KfQ0KYGBgDQoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQojIGF1MXANCm0gPSBjb2xBVUMoYXMubWF0cml4KHByZWQpLCB0cnV0aCkNCg0Kd2VpZ2h0cyA9IHRhYmxlKHRydXRoKSAvIGxlbmd0aCh0cnV0aCkNCg0KbSA9IG0gKiBtYXRyaXgocmVwKHdlaWdodHMsIGVhY2ggPSBucm93KG0pKSwgbmNvbCA9IGxlbmd0aCh3ZWlnaHRzKSkNCg0KbSU+JWtuaXRyOjprYWJsZSgpDQp3ZWlnaHRzJT4la25pdHI6OmthYmxlKCkNCmBgYA0KDQpUw61uaCBBVTFQOg0KDQpgYGB7cn0NCmMgPSBjKGNvbWJuKDE6bmxldmVscyh0cnV0aCksIDIpKQ0KDQphdTFwID0gc3VtKG1bY2JpbmQocmVwKHNlcV9sZW4obnJvdyhtKSksIGVhY2ggPSAyKSwgYyldKSAvIChubGV2ZWxzKHRydXRoKSAtIDEpDQoNCmF1MXANCmBgYA0KDQojIEFVMVUNCg0KVMawxqFuZyB04buxLCBjaOG7iSBz4buRIEFVMVUgY8WpbmcgdMOtbmggdHJpbmcgYsOsbmggQVVDIGPhu6dhIGMoYyAtIDEpIMSRxrDhu51uZyBjb25nIFJPQyBuaOG6sW0gcGjDom4gYmnhu4d0IGLhuq90IGPhurdwIHR14bqnbiB04buxIGdp4buvYSBjw6FjIG5ow6NuLCBuaMawbmcga2jDoWMgduG7m2kgQVUxUCwgQVUxVSBnaeG6oyDEkeG7i25oIGPDoWMgbmjDo24gY8OzIHBow6JuIGLhu5EgxJHhu5NuZyBk4bqhbmcuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KI2F1MXUNCiAgDQptID0gY29sQVVDKGFzLm1hdHJpeChwcmVkKSwgdHJ1dGgpDQoNCm0lPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCmBgYHtyfQ0KYyA9IGMoY29tYm4oMTpubGV2ZWxzKHRydXRoKSwgMikpDQphdTF1ID0gbWVhbihtW2NiaW5kKHJlcChzZXFfbGVuKG5yb3cobSkpLCBlYWNoID0gMiksIGMpXSkNCg0KYXUxdQ0KYGBgDQoNCiMgQVVOUA0KDQpUacOqdSBjaMOtIEFVTlAgY8WpbmcgdMOtbmggQVVDIHRydW5nIGLDrG5oLCBuaMawbmcgY+G7p2EgNCDEkcaw4budbmcgY29uZyBST0MgdMawxqFuZyDhu6luZyB24bubaSA0IHF1eSBsdeG6rXQgbmjhu4sgcGjDom4sIG5o4bqxbSBwaMOibiBiaeG7h3QgMSBuaMOjbiB24bubaSAzIG5ow6NuIGPDsm4gbOG6oWksIGPDsyB4w6l0IMSR4bq/biB04buJIGzhu4cgcGjDom4gYuG7kSB0aOG7sWMgdOG6vyBj4bunYSBjw6FjIG5ow6NuLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmF1bnA9c3VtKEJCbWlzYzo6dm5hcHBseSgxOm5sZXZlbHModHJ1dGgpLGZ1bmN0aW9uKGkpIG1lYW4odHJ1dGggPT0gbGV2ZWxzKHRydXRoKVtpXSkgKiAoY29sQVVDKGFzLm1hdHJpeChwcmVkKVssIGldLCB0cnV0aCA9PSBsZXZlbHModHJ1dGgpW2ldKSkpKQ0KDQphdW5wDQpgYGANCg0KIyBBVU5VDQoNClTGsMahbmcgdOG7sSwgQVVOVSBjxaluZyB4w6l0IDQgcXV5IGx14bqtdCBuaOG7iyBwaMOibiBuaMawIEFVTlAsIG5oxrBuZyB24bubaSBnaeG6oyDEkeG7i25oIGPDoWMgcGjDom4gbmjDs20gY8OzIHBow6JuIGLhu5EgxJHhu5NuZyBk4bqhbmcNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQphdW51PW1lYW4oQkJtaXNjOjp2bmFwcGx5KDE6bmxldmVscyh0cnV0aCksZnVuY3Rpb24oaSkgY29sQVVDKGFzLm1hdHJpeChwcmVkKVssIGldLHRydXRoID09IGxldmVscyh0cnV0aClbaV0pKSkNCg0KYXVudQ0KYGBgDQoNCiMgVOG7lW5nIGvhur90DQoNCkLDonkgZ2nhu50sIHRhIHThuqFvIHJhIDEgaMOgbSDEkeG7gyB0w61uaCDEkeG7k25nIHRo4budaSAxMCB0acOqdSBjaMOtIGvhu4MgdHLDqm4gdOG7qyBtYXRyaXggeMOhYyBzdeG6pXQgY+G7p2EgQyBuaMOjbiwgdmVjdG9yIG5ow6NuIHRo4buxYyB04bq/IHbDoCB2ZWN0b3IgbmjDo24gcHJlZGljdC4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptdWx0aWNsYXNzX3BlcmZvbWFuY2UgPSBmdW5jdGlvbihwcmVkLHRydXRoLHJlc3BvbnNlKXsNCiAgaWYgKGxlbmd0aCh1bmlxdWUodHJ1dGgpKSAhPSBubGV2ZWxzKHRydXRoKSl7DQogICAgd2FybmluZygiRXJyb3I6IE1pc3NpbmcgbGFiZWwocykgZGV0ZWN0ZWQiKQ0KICAgIHJldHVybihOQV9yZWFsXykNCiAgfQ0KICAjYXVudQ0KICBhdW51PW1lYW4oQkJtaXNjOjp2bmFwcGx5KDE6bmxldmVscyh0cnV0aCksDQogICAgICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKGkpIGNvbEFVQyhhcy5tYXRyaXgocHJlZClbLCBpXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRydXRoID09IGxldmVscyh0cnV0aClbaV0pKSkNCiAgIyBhdW5wDQogIGF1bnA9c3VtKEJCbWlzYzo6dm5hcHBseSgxOm5sZXZlbHModHJ1dGgpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKGkpIG1lYW4odHJ1dGggPT0gbGV2ZWxzKHRydXRoKVtpXSkgKiAoY29sQVVDKGFzLm1hdHJpeChwcmVkKVssIGldLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRydXRoID09IGxldmVscyh0cnV0aClbaV0pKSkpDQogICMgYXUxcA0KICBtID0gY29sQVVDKGFzLm1hdHJpeChwcmVkKSwgdHJ1dGgpDQogIHdlaWdodHMgPSB0YWJsZSh0cnV0aCkgLyBsZW5ndGgodHJ1dGgpDQogIG0gPSBtICogbWF0cml4KHJlcCh3ZWlnaHRzLCBlYWNoID0gbnJvdyhtKSksIG5jb2wgPSBsZW5ndGgod2VpZ2h0cykpDQogIGMgPSBjKGNvbWJuKDE6bmxldmVscyh0cnV0aCksIDIpKQ0KICBhdTFwID0gc3VtKG1bY2JpbmQocmVwKHNlcV9sZW4obnJvdyhtKSksIGVhY2ggPSAyKSwgYyldKSAvIChubGV2ZWxzKHRydXRoKSAtIDEpDQogIA0KICAjYXUxdQ0KICANCiAgbSA9IGNvbEFVQyhhcy5tYXRyaXgocHJlZCksIHRydXRoKQ0KICBjID0gYyhjb21ibigxOm5sZXZlbHModHJ1dGgpLCAyKSkNCiAgbWVhbihtW2NiaW5kKHJlcChzZXFfbGVuKG5yb3cobSkpLCBlYWNoID0gMiksIGMpXSkNCiAgYXUxdSA9IG1lYW4obVtjYmluZChyZXAoc2VxX2xlbihucm93KG0pKSwgZWFjaCA9IDIpLCBjKV0pDQogIA0KICAjIGNvbmYgbWF0DQogIA0KICBjb25mLm1hdCA9IHRhYmxlKHRydXRoLCByZXNwb25zZSkNCiAgY29uZi5tYXQgPSBjb25mLm1hdCAvIHN1bShjb25mLm1hdCkNCiAgcDAgPSBzdW0oZGlhZyhjb25mLm1hdCkpDQogIHJvd3N1bSA9IHJvd1N1bXMoY29uZi5tYXQpDQogIGNvbHN1bSA9IGNvbFN1bXMoY29uZi5tYXQpDQogIHBlID0gc3VtKHJvd3N1bSAqIGNvbHN1bSkgLyBzdW0oY29uZi5tYXQpXjINCiAgIyBrYXBwYQ0KICBrYXBwYSA9IDEgLSAoMSAtIHAwKSAvICgxIC0gcGUpDQogIA0KICAjV2thcHBhDQogIA0KICBjb25mLm1hdCA9IHRhYmxlKHRydXRoLCByZXNwb25zZSkNCiAgY29uZi5tYXQgPSBjb25mLm1hdCAvIHN1bShjb25mLm1hdCkNCiAgIyBnZXQgZXhwZWN0ZWQgcHJvYnMgdW5kZXIgaW5kZXBlbmRlbmNlDQogIHJvd3N1bSA9IHJvd1N1bXMoY29uZi5tYXQpDQogIGNvbHN1bSA9IGNvbFN1bXMoY29uZi5tYXQpDQogIGV4cGVjdGVkLm1hdCA9IHJvd3N1bSAlKiUgdChjb2xzdW0pDQogIA0KICAjIGdldCB3ZWlnaHRzDQogIGNsYXNzLnZhbHVlcyA9IHNlcV9hbG9uZyhsZXZlbHModHJ1dGgpKSAtIDFMDQogIGtfd2VpZ2h0cyA9IG91dGVyKGNsYXNzLnZhbHVlcywgY2xhc3MudmFsdWVzLCBGVU4gPSBmdW5jdGlvbih4LCB5KSAoeCAtIHkpXjIpDQogIA0KICAjIGNhbGN1bGF0ZSB3ZWlnaHRlZCBrYXBwYQ0KICB3a2FwcGEgPSAxIC0gc3VtKGtfd2VpZ2h0cyAqIGNvbmYubWF0KSAvIHN1bShrX3dlaWdodHMgKiBleHBlY3RlZC5tYXQpDQogIA0KICAjIG11bHRpY2xhc3MgYnJpZXINCiAgDQogIHRydXRoX2YgPSBmYWN0b3IodHJ1dGgsIGxldmVscyA9IGNvbG5hbWVzKHByZWQpKQ0KICBtYXQwMSA9IG1scjo6Y3JlYXRlRHVtbXlGZWF0dXJlcyh0cnV0aF9mKQ0KICBtY19icmllciA9IG1lYW4ocm93U3VtcygocHJlZCAtIG1hdDAxKV4yKSkNCiAgDQogICMgYmFjDQogIGJhYyA9IG1lYW4oZGlhZyh0YWJsZSh0cnV0aCwgcmVzcG9uc2UpIC8gdGFibGUodHJ1dGgsIHRydXRoKSkpDQogIA0KICAjIGJlcg0KICBiZXIgPSBtZWFuKGRpYWcoMSAtICh0YWJsZSh0cnV0aCwgcmVzcG9uc2UpIC8gdGFibGUodHJ1dGgsIHRydXRoKSkpKQ0KICANCiAgIyBsb2dsb3NzDQogIA0KICBwcmVkW3ByZWQgPiAxIC0gKDFlLTIwKV0gPSAxIC0gKDFlLTIwKQ0KICBwcmVkW3ByZWQgPCAoMWUtMjApXSA9IDFlLTIwDQogIA0KICBjX3RydXRoID0gbWF0Y2goYXMuY2hhcmFjdGVyKHRydXRoKSwgY29sbmFtZXMocHJlZCkpDQogIGluZHMgPSBjYmluZChCQm1pc2M6OnNlcV9yb3cocHJlZCksIGNfdHJ1dGgpDQogIA0KICBsb2dsb3NzID0gLTEgKiBtZWFuKGxvZyhwcmVkW2luZHNdKSkNCiAgDQogIHJlc3VsdCA9IHJiaW5kKGJhYyxiZXIsbG9nbG9zcyxtY19icmllciwNCiAgICAgICAgICAgICAgICAgYXVudSxhdW5wLGF1MXUsYXUxcCwNCiAgICAgICAgICAgICAgICAga2FwcGEsd2thcHBhKQ0KICANCiAgcmV0dXJuKHJlc3VsdCkNCiAgfQ0KYGBgDQoNCsOBcCBk4bulbmcgaMOgbSBuw6B5IMSR4buDIGzhuqV5IGvhur90IHF14bqjIGPhu6dhIDEwIHRpw6p1IGNow60uDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbXVsdGljbGFzc19wZXJmb21hbmNlKHRydXRoPXRydXRoLA0KICAgICAgICAgICAgICAgICAgICAgIHByZWQ9cHJlZCwNCiAgICAgICAgICAgICAgICAgICAgICByZXNwb25zZT1yZXNwb25zZSklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCkLDoGkgdGjhu7FjIGjDoG5oIMSR4bq/biDEkcOieSBsw6Aga+G6v3QgdGjDumM7IGjhurluIGfhurdwIGzhuqFpIGPDoWMgYuG6oW4gbOG6p24gdOG7m2ku