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).
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))
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
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))
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 |
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)
Độ 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.
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 |
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
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()
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
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ệ.
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 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()
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
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 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
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
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
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