1 Giới thiệu

Trong bài thực hành ngắn này, Nhi muốn làm một thí nghiệm nhỏ để so sánh khả năng của 2 giải thuật khác nhau khi áp dụng cho cùng một bài toán hồi quy: một bên là Mô hình Deep neural network bằng Tensorflow (giao thức Keras), bên kia là mô hình Lambda-Mu-Sigma (LMS) thuộc phương pháp GAMLSS. Mỗi loại có thể xem như ứng cử viên mạnh nhất từ trường phái Whiteboxes và Blackboxes.

Tình huống đặt ra, đó là một nghiên cứu dịch tễ nhằm ước tính giá trị dung phổi bình thường (TLC) ở nam giới thuộc chủng tộc Caucasian.

2 Dữ liệu

library(tidyverse)

df=read.csv("https://raw.githubusercontent.com/kinokoberuji/R-Tutorials/master/LMSmodelGAMLSS.csv", sep=";")%>%as_tibble()

dfM=filter(df,Sex=="M")%>%dplyr::select(-Sex)

head(dfM)%>%knitr::kable()
TLC Age Height
7.017265 18 177.5
7.648100 18 181.0
6.737513 18 176.0
5.949827 22 171.0
7.389043 22 180.0
7.048908 23 185.0

3 Mô hình LMS (Gamlss)

Trước hết, ta sẽ dùng mô hình LMS từ package gamlss. Lý thuyết về mô hình LMS đã được Nhi giải thích cặn kẽ trong bài thực hành số 7 series Gamlss : http://rpubs.com/lengockhanhi/342110. Một cách ngắn gọn, LMS là một tập hợp 3 mô hình riêng biệt M,S,L cho 3 tham số : Mu (vị trị trung tâm), Sigma (Scale) và Lambda (Skewness), tương ứng với 3 tham số Mu (link function = log), Sigma (link=log) và Nu (link = identity) của quy luật phân phối Box-Cox-Cole-Green. Mỗi mô hình M,L,S là một mô hình GAM (generalized additive model), cho phép tích hợp thêm Smoothing splines (các hàm điều hòa/hiệu chỉnh) và hàm đa thức (Polynomial). Chính những hàm này cho phép đồ thị LMS model uốn lượn mềm mại theo những hình dạng phức tạp nhất có thể và tăng độ chính xác của mô hình.

Đầu tiên, ta sẽ dùng caret để chia dữ liệu thành 2 phần, 20% dành cho kiểm định và 80% để dựng mô hình:

library(caret)

set.seed(123)

idM=createDataPartition(y=dfM$TLC, p=0.8,list=FALSE)
trainM=dfM[idM,]
testM=dfM[-idM,]

Ta dựng mô hình LMS bằng quy trình Stepwise, trong đó ta sẽ xác định tổ hợp tối ưu cho 2 câu hỏi: Có cần mô hình cho Sigma/Nu hay không ? và: Nếu cần, thì nội dung mô hình là gì ?

Trong trường hợp này Nhi muốn chọn lọc mô hình L,M,S tối ưu dựa vào 3 giả định: hàm đa thức bậc 3 cho Height , bậc 2 cho Age, kèm hoặc không kèm Spline cho Age, công thức này sẽ lần lượt test cho Mu và Sigma, Nu được mặc định là hằng số.

library(gamlss)
nC<-detectCores()

m1=gamlss(data=trainM,
          TLC~1,
          sigma.formula = TLC~1,
          nu.formula = TLC~1,
          family=BCCG(mu.link="log"),
          trace=FALSE,
          parallel="multicore",
          ncpus = nC)

m2=stepGAICAll.A(m1, scope=list(lower=~1, 
                                upper=~poly(Age,2)+poly(Height,3)+pb(Age)),
                 sigma.scope = list(lower=~1, 
                                upper=~poly(Age,2)+poly(Height,3)+pb(Age)),
                 k=log(length(trainM)),
                 trace=FALSE,
                 parallel="multicore",
                 ncpus = nC
              )
## --------------------------------------------------- 
## Start:  AIC= 546.27 
##  TLC ~ 1 
## 
## --------------------------------------------------- 
## Start:  AIC= 437.74 
##  ~1 
## 
## --------------------------------------------------- 
## Start:  AIC= 435.34 
##  ~1 
## 
## --------------------------------------------------- 
## Start:  AIC= 435.34 
##  ~pb(Age) 
## 
## --------------------------------------------------- 
## Start:  AIC= 435.34 
##  TLC ~ poly(Height, 3) + pb(Age) 
## 
## ---------------------------------------------------

Kết quả Stepwise như sau:

Mô hình cho Mu có công thức: Mu ~ poly(Height,3)+Age+ MuSpline.

Mô hình cho Sigma chỉ gồm Sigma ~ Age + SigmSplin.

Nu = hằng số.

summary(m2)
## ******************************************************************
## Family:  c("BCCG", "Box-Cox-Cole-Green") 
## 
## Call:  
## gamlss(formula = TLC ~ poly(Height, 3) + pb(Age), sigma.formula = ~pb(Age),  
##     nu.formula = ~1, family = BCCG(mu.link = "log"),  
##     data = trainM, trace = FALSE, parallel = "multicore",      ncpus = nC) 
## 
## 
## Fitting method: RS() 
## 
## ------------------------------------------------------------------
## Mu link function:  log
## Mu Coefficients:
##                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       1.9041367  0.0222386  85.623  < 2e-16 ***
## poly(Height, 3)1  1.1785602  0.1076800  10.945  < 2e-16 ***
## poly(Height, 3)2 -0.3753507  0.1063796  -3.528 0.000527 ***
## poly(Height, 3)3 -0.1604175  0.1038782  -1.544 0.124224    
## pb(Age)           0.0009752  0.0005334   1.828 0.069093 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## ------------------------------------------------------------------
## Sigma link function:  log
## Sigma Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -2.568038   0.155515  -16.51   <2e-16 ***
## pb(Age)      0.006680   0.003356    1.99    0.048 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## ------------------------------------------------------------------
## Nu link function:  identity 
## Nu Coefficients:
##             Estimate Std. Error t value Pr(>|t|)
## (Intercept)  0.05722    0.59892   0.096    0.924
## 
## ------------------------------------------------------------------
## NOTE: Additive smoothing terms exist in the formulas: 
##  i) Std. Error for smoothers are for the linear effect only. 
## ii) Std. Error for the linear terms maybe are not accurate. 
## ------------------------------------------------------------------
## No. of observations in the fit:  195 
## Degrees of Freedom for the fit:  9.53894
##       Residual Deg. of Freedom:  185.4611 
##                       at cycle:  5 
##  
## Global Deviance:     424.8601 
##             AIC:     443.938 
##             SBC:     475.1589 
## ******************************************************************

4 Mô hình Deep neural network với Keras

Keras là một nền tảng chuyên dụng cho Deep learning. Bản thể của Keras dùng ngôn ngữ Python, nhưng có giao thức để sử dụng trong R (package keras), bạn cần install trước một gói Python thí dụ Anaconda để có thể sử dụng keras trong R.

Giao thức Keras tiếp nhận một cấu trúc dữ liệu đặc biệt gọi là các tensor. Ta có thể hình dung về tensors như khái niệm tổng quát để gọi tên những đối tượng dữ liệu mà ta từng biết trong R theo số chiều thông tin (dimension). Trong thí dụ này, ta có thể gọi vector biến kết quả là là 1D tensor, còn matrix hay dataframe gồm các features được gọi là 2D tensor.

Trong R, tensor được lưu dưới dạng array, do đó trước hết ta cần hoán chuyển những dataframe hiện có thành array. Điểm khác biệt thứ hai đó là Keras tiếp nhận riêng tensors cho tập features và cho biến kết quả nên ta phải cắt 2 phần này riêng lẻ từ tập train và test

library(keras)

train_data<-trainM%>%dplyr::select(-1)%>%as.matrix()%>%as.array.default(dimnames=NULL)  # Convert dataframe to array
dimnames(train_data) <- NULL

train_targets<-trainM%>%.$TLC%>%as.array() 

test_data<-testM%>%dplyr::select(-1)%>%as.matrix()%>%as.array.default(dimnames=NULL)  # Convert dataframe to array
dimnames(train_data) <- NULL

test_targets<-testM%>%.$TLC%>%as.array() 
str(train_data)
##  num [1:195, 1:2] 18 18 18 22 23 23 24 25 25 26 ...
str(test_data)
##  num [1:48, 1:2] 22 27 32 33 34 34 35 47 51 61 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : NULL
##   ..$ : chr [1:2] "Age" "Height"
str(train_targets)
##  num [1:195(1d)] 7.02 7.65 6.74 5.95 7.05 ...
str(test_targets)
##  num [1:48(1d)] 7.39 7.78 8.69 8.04 6.75 ...

Một điểm cần lưu ý tiếp theo đó là Neural network chỉ hoạt động tối ưu khi dữ liệu đầu vào được chuẩn hóa bằng hàm scale, sử dụng trung bình của tập train:

mu <- apply(train_data, 2, mean)
sigma <- apply(train_data, 2, sd)
train_data <- scale(train_data, center = mu, scale = sigma)

test_data <- scale(test_data, center = mu, scale = sigma)

head(train_data)%>%knitr::kable()
-1.616520 0.0889939
-1.616520 0.5362570
-1.616520 -0.1026903
-1.365097 -0.7416377
-1.302241 1.0474149
-1.302241 -1.7000587

Trong thí dụ này, do bài toán rất đơn giản nên ta chỉ cần dùng một mạng neuron có cấu trúc 2 lớp hidden, mỗi lớp gồm 64 đơn vị neurons. Đầu ra của mạng lưới là 1 neuron duy nhất (và không kèm activation function như trong bài toán tiên lượng xác suất) – đủ để xuất kết quả là một con số (scalar) và tuyến tính – chính là giá trị cần ước lượng.

Mạng lưới được đóng gói với loss function là mean squared error (sai số bình phương trung bình) vì đây là bài toán hồi quy. Trong quá trình huấn luyện, tiêu chí được sử dụng để cải thiện hiệu năng của mô hình là MSE (sai số tuyệt đối trung bình).

build_model <- function() {
  model <- keras_model_sequential() %>% 
    layer_dense(units = 64, activation = "relu", 
                input_shape = dim(train_data)[[2]]) %>% 
    layer_dense(units = 64, activation = "relu") %>% 
    layer_dense(units = 1) 
  
  model %>% compile(
    optimizer = "rmsprop", 
    loss = "mse", 
    metrics = c("mae")
  )
}

Tiếp theo, ta thực hiện một quy trình kiểm chứng chéo 5 blocks lặp lại 100 lần

set.seed(123)

k <- 5
indices <- sample(1:nrow(train_data))
folds <- cut(1:length(indices), breaks = k, labels = FALSE) 

num_epochs <- 100

all_mae_histories <- NULL
for (i in 1:k) {
  cat("processing fold #", i, "\n")
  
  # Prepare the validation data: data from partition # k
  val_indices <- which(folds == i, arr.ind = TRUE)
  val_data <- train_data[val_indices,]
  val_targets <- train_targets[val_indices]
  
  # Prepare the training data: data from all other partitions
  partial_train_data <- train_data[-val_indices,]
  partial_train_targets <- train_targets[-val_indices]
  
  # Build the Keras model (already compiled)
  model <- build_model()
  
  # Train the model (in silent mode, verbose=0)
  history <- model %>% fit(
    partial_train_data, partial_train_targets,
    validation_data = list(val_data, val_targets),
    epochs = num_epochs, batch_size = 1, verbose = 0
  )
  mae_history <- history$metrics$val_mean_absolute_error
  all_mae_histories <- rbind(all_mae_histories, mae_history)
}
## processing fold # 1 
## processing fold # 2 
## processing fold # 3 
## processing fold # 4 
## processing fold # 5
median_mae_history <- data.frame(
  epoch = seq(1:ncol(all_mae_histories)),
  validation_mae = apply(all_mae_histories, 2, median)
)

median_mae_history%>%ggplot(aes(x = epoch, y = validation_mae))+geom_line(col="red",size=1)+
  theme_bw()+
  geom_hline(yintercept = median(median_mae_history$validation_mae),linetype=2)

ggplot(median_mae_history, aes(x = epoch, y = validation_mae))+geom_smooth(col="red",fill="red")+
  theme_bw()+
  geom_hline(yintercept = median(median_mae_history$validation_mae),linetype=2)

Cuối cùng, ta dựng một mô hình chính thức với số epochs = 50 và batch_size=16

set.seed(123)

model <- build_model()

# Train it on the entirety of the data.
model %>% fit(train_data, train_targets,
              epochs = 50, batch_size = 16, verbose = 0)

summary(model)
## ___________________________________________________________________________
## Layer (type)                     Output Shape                  Param #     
## ===========================================================================
## dense_16 (Dense)                 (None, 64)                    192         
## ___________________________________________________________________________
## dense_17 (Dense)                 (None, 64)                    4160        
## ___________________________________________________________________________
## dense_18 (Dense)                 (None, 1)                     65          
## ===========================================================================
## Total params: 4,417
## Trainable params: 4,417
## Non-trainable params: 0
## ___________________________________________________________________________
result <- model %>% evaluate(test_data, test_targets)

result
## $loss
## [1] 0.6035802
## 
## $mean_absolute_error
## [1] 0.6450254

5 So sánh hiệu năng 2 mô hình

Truth=testM$TLC
DNN=predict(model,test_data)
LMS=predict(m2,newdata=testM,type="response")
## new prediction 
## New way of prediction in pb()  (starting from GAMLSS version 5.0-3)
pdf=cbind(Truth,DNN,LMS)%>%as_data_frame()

colnames(pdf)=c("Truth","DeepNN","LMS")

pdf$Age=testM$Age

library(mlr)

regr.task= mlr::makeRegrTask(id = "dfM", data=testM, target = "TLC")
regr.lrn = makeLearner("regr.glm")

dummy=mlr::train(regr.lrn,regr.task)

dumpredDNN=predict(dummy,regr.task)
dumpredLMS=predict(dummy,regr.task)

dumpredDNN$data$response<-DNN
dumpredLMS$data$response<-LMS

Đây là hiệu năng mô hình Deep neural net với Tensorflo (Keras)

mets=list(mse,mae,medae,rmse)
performance(dumpredDNN,measures =mets)
##       mse       mae     medae      rmse 
## 0.6035802 0.6450254 0.5570394 0.7769043

Còn đây là hiệu năng mô hình LMS của gamlss

performance(dumpredLMS,measures =mets)
##       mse       mae     medae      rmse 
## 0.5183984 0.5819141 0.5102973 0.7199989

Màu xanh là mô hình LMS, màu đỏ là mô hình Deep neural network

pdf%>%ggplot()+geom_point(aes(x=Age,y=Truth),col="black",alpha=0.5)+
  geom_smooth(aes(x=Age,y=DNN),col="red",fill="red",alpha=0.2)+
  geom_smooth(aes(x=Age,y=LMS),col="blue",fill="blue",alpha=0.2)+theme_bw()+scale_y_continuous("TLC")
## `geom_smooth()` using method = 'loess'
## `geom_smooth()` using method = 'loess'

Trung bình, mô hình Deep neural network chỉ tiên lượng dung tích phổi sai khoảng 50 mL

mean(pdf$DeepNN-pdf$Truth)
## [1] -0.02100793

6 Nhận xét

Deep neural network (còn gọi là Deep learning) là một giải pháp vô cùng mạnh mẽ cho bài toán hồi quy, thậm chí nó tương đương với mô hình phi tuyến tính với hàm Spline bù trừ vốn là công cụ tối ưu nhất hiện nay cho các mô hình tăng trưởng trong y học lâm sàng.

Tuy nhiên, mô hình LMS có 2 ưu điểm mà Deep NN không thể thay thế, thứ nhất là tính tường minh: nội dung mô hình có thể được tính thủ công từ lookup table, thứ hai là khả năng ước tính được các ngưỡng giới hạn trên và dưới của giá trị bình thường trong quần thể từ 3 tham số L,M và S, cho phép diễn giải kết quả về mặt lâm sàng.Trong khi đó mô hình Deep NN không thể diễn giải được và chỉ có thể được áp dụng dưới dạng hard code trong firmware của thiết bị xét nghiệm.

Tuy nhiên, có thể dự báo trong tương lai giải pháp Deep learning sẽ cho phép những thiết bị xét nghiệm có khả năng tự tạo ra mô hình ước lượng giá trị tham chiếu cho chính nó mà không cần sự can thiệp của con người, và đây là một ứng dụng không thể xem thường.

LS0tDQp0aXRsZTogIkLDoGkgdG/DoW4gaOG7k2kgcXV5Ig0Kc3VidGl0bGU6ICJLZXJhcyB2LnMgR2FtbHNzIg0KYXV0aG9yOiAiTMOqIE5n4buNYyBLaOG6oyBOaGkiDQpkYXRlOiAiMDIgVGjDoW5nIDA1IDIwMTgiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZGVmYXVsdCINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHBhbmRlcikNCmBgYA0KDQohW10oS2VyYXN2c2dhbWxzcy5wbmcpDQoNCiMgR2nhu5tpIHRoaeG7h3UNCg0KVHJvbmcgYsOgaSB0aOG7sWMgaMOgbmggbmfhuq9uIG7DoHksIE5oaSBtdeG7kW4gbMOgbSBt4buZdCB0aMOtIG5naGnhu4dtIG5o4buPIMSR4buDIHNvIHPDoW5oIGto4bqjIG7Eg25nIGPhu6dhIDIgZ2nhuqNpIHRodeG6rXQga2jDoWMgbmhhdSBraGkgw6FwIGThu6VuZyBjaG8gY8O5bmcgbeG7mXQgYsOgaSB0b8OhbiBo4buTaSBxdXk6IG3hu5l0IGLDqm4gbMOgIE3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXR3b3JrIGLhurFuZyBUZW5zb3JmbG93IChnaWFvIHRo4bupYyBLZXJhcyksIGLDqm4ga2lhIGzDoCBtw7QgaMOsbmggTGFtYmRhLU11LVNpZ21hIChMTVMpIHRodeG7mWMgcGjGsMahbmcgcGjDoXAgR0FNTFNTLiBN4buXaSBsb+G6oWkgY8OzIHRo4buDIHhlbSBuaMawIOG7qW5nIGPhu60gdmnDqm4gbeG6oW5oIG5o4bqldCB04burIHRyxrDhu51uZyBwaMOhaSBXaGl0ZWJveGVzIHbDoCBCbGFja2JveGVzLg0KDQpUw6xuaCBodeG7kW5nIMSR4bq3dCByYSwgxJHDsyBsw6AgbeG7mXQgbmdoacOqbiBj4bupdSBk4buLY2ggdOG7hSBuaOG6sW0gxrDhu5tjIHTDrW5oIGdpw6EgdHLhu4sgZHVuZyBwaOG7lWkgYsOsbmggdGjGsOG7nW5nIChUTEMpIOG7nyBuYW0gZ2nhu5tpIHRodeG7mWMgY2jhu6duZyB04buZYyBDYXVjYXNpYW4uIA0KDQojIEThu68gbGnhu4d1DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQoNCmRmPXJlYWQuY3N2KCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20va2lub2tvYmVydWppL1ItVHV0b3JpYWxzL21hc3Rlci9MTVNtb2RlbEdBTUxTUy5jc3YiLCBzZXA9IjsiKSU+JWFzX3RpYmJsZSgpDQoNCmRmTT1maWx0ZXIoZGYsU2V4PT0iTSIpJT4lZHBseXI6OnNlbGVjdCgtU2V4KQ0KDQpoZWFkKGRmTSklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCiMgTcO0IGjDrG5oIExNUyAoR2FtbHNzKQ0KDQpUcsaw4bubYyBo4bq/dCwgdGEgc+G6vSBkw7luZyBtw7QgaMOsbmggTE1TIHThu6sgcGFja2FnZSBnYW1sc3MuIEzDvSB0aHV54bq/dCB24buBIG3DtCBow6xuaCBMTVMgxJHDoyDEkcaw4bujYyBOaGkgZ2nhuqNpIHRow61jaCBj4bq3biBr4bq9IHRyb25nIGLDoGkgdGjhu7FjIGjDoG5oIHPhu5EgNyBzZXJpZXMgR2FtbHNzIDogPGh0dHA6Ly9ycHVicy5jb20vbGVuZ29ja2hhbmhpLzM0MjExMD4uIE3hu5l0IGPDoWNoIG5n4bqvbiBn4buNbiwgTE1TIGzDoCBt4buZdCB04bqtcCBo4bujcCAzIG3DtCBow6xuaCByacOqbmcgYmnhu4d0IE0sUyxMIGNobyAzIHRoYW0gc+G7kSA6IE11ICh24buLIHRy4buLIHRydW5nIHTDom0pLCBTaWdtYSAoU2NhbGUpIHbDoCBMYW1iZGEgKFNrZXduZXNzKSwgdMawxqFuZyDhu6luZyB24bubaSAzIHRoYW0gc+G7kSBNdSAobGluayBmdW5jdGlvbiA9IGxvZyksIFNpZ21hIChsaW5rPWxvZykgdsOgIE51IChsaW5rID0gaWRlbnRpdHkpIGPhu6dhIHF1eSBsdeG6rXQgcGjDom4gcGjhu5FpIEJveC1Db3gtQ29sZS1HcmVlbi4gTeG7l2kgbcO0IGjDrG5oIE0sTCxTIGzDoCBt4buZdCBtw7QgaMOsbmggR0FNIChnZW5lcmFsaXplZCBhZGRpdGl2ZSBtb2RlbCksIGNobyBwaMOpcCB0w61jaCBo4bujcCB0aMOqbSBTbW9vdGhpbmcgc3BsaW5lcyAoY8OhYyBow6BtIMSRaeG7gXUgaMOyYS9oaeG7h3UgY2jhu4luaCkgdsOgIGjDoG0gxJFhIHRo4bupYyAoUG9seW5vbWlhbCkuIENow61uaCBuaOG7r25nIGjDoG0gbsOgeSBjaG8gcGjDqXAgxJHhu5MgdGjhu4sgTE1TIG1vZGVsIHXhu5FuIGzGsOG7o24gbeG7gW0gbeG6oWkgdGhlbyBuaOG7r25nIGjDrG5oIGThuqFuZyBwaOG7qWMgdOG6oXAgbmjhuqV0IGPDsyB0aOG7gyB2w6AgdMSDbmcgxJHhu5kgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oLg0KDQrEkOG6p3UgdGnDqm4sIHRhIHPhur0gZMO5bmcgY2FyZXQgxJHhu4MgY2hpYSBk4buvIGxp4buHdSB0aMOgbmggMiBwaOG6p24sIDIwJSBkw6BuaCBjaG8ga2nhu4NtIMSR4buLbmggdsOgIDgwJSDEkeG7gyBk4buxbmcgbcO0IGjDrG5oOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoY2FyZXQpDQoNCnNldC5zZWVkKDEyMykNCg0KaWRNPWNyZWF0ZURhdGFQYXJ0aXRpb24oeT1kZk0kVExDLCBwPTAuOCxsaXN0PUZBTFNFKQ0KdHJhaW5NPWRmTVtpZE0sXQ0KdGVzdE09ZGZNWy1pZE0sXQ0KYGBgDQoNClRhIGThu7FuZyBtw7QgaMOsbmggTE1TIGLhurFuZyBxdXkgdHLDrG5oIFN0ZXB3aXNlLCB0cm9uZyDEkcOzIHRhIHPhur0geMOhYyDEkeG7i25oIHThu5UgaOG7o3AgdOG7kWkgxrB1IGNobyAyIGPDonUgaOG7j2k6IEPDsyBj4bqnbiBtw7QgaMOsbmggY2hvIFNpZ21hL051IGhheSBraMO0bmcgPyB2w6A6IE7hur91IGPhuqduLCB0aMOsIG7hu5lpIGR1bmcgbcO0IGjDrG5oIGzDoCBnw6wgPw0KDQpUcm9uZyB0csaw4budbmcgaOG7o3AgbsOgeSBOaGkgbXXhu5FuIGNo4buNbiBs4buNYyBtw7QgaMOsbmggTCxNLFMgdOG7kWkgxrB1IGThu7FhIHbDoG8gMyBnaeG6oyDEkeG7i25oOiBow6BtIMSRYSB0aOG7qWMgYuG6rWMgMyBjaG8gSGVpZ2h0ICwgYuG6rWMgMiBjaG8gQWdlLCBrw6htIGhv4bq3YyBraMO0bmcga8OobSBTcGxpbmUgY2hvIEFnZSwgY8O0bmcgdGjhu6ljIG7DoHkgc+G6vSBs4bqnbiBsxrDhu6N0IHRlc3QgY2hvIE11IHbDoCBTaWdtYSwgTnUgxJHGsOG7o2MgbeG6t2MgxJHhu4tuaCBsw6AgaOG6sW5nIHPhu5EuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpsaWJyYXJ5KGdhbWxzcykNCm5DPC1kZXRlY3RDb3JlcygpDQoNCm0xPWdhbWxzcyhkYXRhPXRyYWluTSwNCiAgICAgICAgICBUTEN+MSwNCiAgICAgICAgICBzaWdtYS5mb3JtdWxhID0gVExDfjEsDQogICAgICAgICAgbnUuZm9ybXVsYSA9IFRMQ34xLA0KICAgICAgICAgIGZhbWlseT1CQ0NHKG11Lmxpbms9ImxvZyIpLA0KICAgICAgICAgIHRyYWNlPUZBTFNFLA0KICAgICAgICAgIHBhcmFsbGVsPSJtdWx0aWNvcmUiLA0KICAgICAgICAgIG5jcHVzID0gbkMpDQoNCm0yPXN0ZXBHQUlDQWxsLkEobTEsIHNjb3BlPWxpc3QobG93ZXI9fjEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB1cHBlcj1+cG9seShBZ2UsMikrcG9seShIZWlnaHQsMykrcGIoQWdlKSksDQogICAgICAgICAgICAgICAgIHNpZ21hLnNjb3BlID0gbGlzdChsb3dlcj1+MSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHVwcGVyPX5wb2x5KEFnZSwyKStwb2x5KEhlaWdodCwzKStwYihBZ2UpKSwNCiAgICAgICAgICAgICAgICAgaz1sb2cobGVuZ3RoKHRyYWluTSkpLA0KICAgICAgICAgICAgICAgICB0cmFjZT1GQUxTRSwNCiAgICAgICAgICAgICAgICAgcGFyYWxsZWw9Im11bHRpY29yZSIsDQogICAgICAgICAgICAgICAgIG5jcHVzID0gbkMNCiAgICAgICAgICAgICAgKQ0KYGBgDQoNCkvhur90IHF14bqjIFN0ZXB3aXNlIG5oxrAgc2F1Og0KDQpNw7QgaMOsbmggY2hvIE11IGPDsyBjw7RuZyB0aOG7qWM6IE11IH4gcG9seShIZWlnaHQsMykrQWdlKyBNdVNwbGluZS4NCg0KTcO0IGjDrG5oIGNobyBTaWdtYSBjaOG7iSBn4buTbSBTaWdtYSB+IEFnZSArIFNpZ21TcGxpbi4NCg0KTnUgPSBo4bqxbmcgc+G7kS4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzdW1tYXJ5KG0yKQ0KYGBgDQoNCiMgTcO0IGjDrG5oIERlZXAgbmV1cmFsIG5ldHdvcmsgduG7m2kgS2VyYXMNCg0KS2VyYXMgbMOgIG3hu5l0IG7hu4FuIHThuqNuZyBjaHV5w6puIGThu6VuZyBjaG8gRGVlcCBsZWFybmluZy4gQuG6o24gdGjhu4MgY+G7p2EgS2VyYXMgZMO5bmcgbmfDtG4gbmfhu68gUHl0aG9uLCBuaMawbmcgY8OzIGdpYW8gdGjhu6ljIMSR4buDIHPhu60gZOG7pW5nIHRyb25nIFIgKHBhY2thZ2Uga2VyYXMpLCBi4bqhbiBj4bqnbiBpbnN0YWxsIHRyxrDhu5tjIG3hu5l0IGfDs2kgUHl0aG9uIHRow60gZOG7pSBBbmFjb25kYSDEkeG7gyBjw7MgdGjhu4Mgc+G7rSBk4bulbmcga2VyYXMgdHJvbmcgUi4gICANCg0KR2lhbyB0aOG7qWMgS2VyYXMgdGnhur9wIG5o4bqtbiBt4buZdCBj4bqldSB0csO6YyBk4buvIGxp4buHdSDEkeG6t2MgYmnhu4d0IGfhu41pIGzDoCBjw6FjIHRlbnNvci4gVGEgY8OzIHRo4buDIGjDrG5oIGR1bmcgduG7gSB0ZW5zb3JzIG5oxrAga2jDoWkgbmnhu4dtIHThu5VuZyBxdcOhdCDEkeG7gyBn4buNaSB0w6puIG5o4buvbmcgxJHhu5FpIHTGsOG7o25nIGThu68gbGnhu4d1IG3DoCB0YSB04burbmcgYmnhur90IHRyb25nIFIgdGhlbyBz4buRIGNoaeG7gXUgdGjDtG5nIHRpbiAoZGltZW5zaW9uKS4gVHJvbmcgdGjDrSBk4bulIG7DoHksIHRhIGPDsyB0aOG7gyBn4buNaSB2ZWN0b3IgYmnhur9uIGvhur90IHF14bqjIGzDoCBsw6AgMUQgdGVuc29yLCBjw7JuIG1hdHJpeCBoYXkgZGF0YWZyYW1lIGfhu5NtIGPDoWMgZmVhdHVyZXMgxJHGsOG7o2MgZ+G7jWkgbMOgIDJEIHRlbnNvci4gDQoNClRyb25nIFIsIHRlbnNvciDEkcaw4bujYyBsxrB1IGTGsOG7m2kgZOG6oW5nIGFycmF5LCBkbyDEkcOzIHRyxrDhu5tjIGjhur90IHRhIGPhuqduIGhvw6FuIGNodXnhu4NuIG5o4buvbmcgZGF0YWZyYW1lIGhp4buHbiBjw7MgdGjDoG5oIGFycmF5LiDEkGnhu4NtIGtow6FjIGJp4buHdCB0aOG7qSBoYWkgxJHDsyBsw6AgS2VyYXMgdGnhur9wIG5o4bqtbiByacOqbmcgdGVuc29ycyBjaG8gdOG6rXAgZmVhdHVyZXMgdsOgIGNobyBiaeG6v24ga+G6v3QgcXXhuqMgbsOqbiB0YSBwaOG6o2kgY+G6r3QgMiBwaOG6p24gbsOgeSByacOqbmcgbOG6uyB04burIHThuq1wIHRyYWluIHbDoCB0ZXN0DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpsaWJyYXJ5KGtlcmFzKQ0KDQp0cmFpbl9kYXRhPC10cmFpbk0lPiVkcGx5cjo6c2VsZWN0KC0xKSU+JWFzLm1hdHJpeCgpJT4lYXMuYXJyYXkuZGVmYXVsdChkaW1uYW1lcz1OVUxMKSAgIyBDb252ZXJ0IGRhdGFmcmFtZSB0byBhcnJheQ0KZGltbmFtZXModHJhaW5fZGF0YSkgPC0gTlVMTA0KDQp0cmFpbl90YXJnZXRzPC10cmFpbk0lPiUuJFRMQyU+JWFzLmFycmF5KCkgDQoNCnRlc3RfZGF0YTwtdGVzdE0lPiVkcGx5cjo6c2VsZWN0KC0xKSU+JWFzLm1hdHJpeCgpJT4lYXMuYXJyYXkuZGVmYXVsdChkaW1uYW1lcz1OVUxMKSAgIyBDb252ZXJ0IGRhdGFmcmFtZSB0byBhcnJheQ0KZGltbmFtZXModHJhaW5fZGF0YSkgPC0gTlVMTA0KDQp0ZXN0X3RhcmdldHM8LXRlc3RNJT4lLiRUTEMlPiVhcy5hcnJheSgpIA0KDQpgYGANCg0KYGBge3J9DQpzdHIodHJhaW5fZGF0YSkNCnN0cih0ZXN0X2RhdGEpDQpzdHIodHJhaW5fdGFyZ2V0cykNCnN0cih0ZXN0X3RhcmdldHMpDQpgYGANCg0KTeG7mXQgxJFp4buDbSBj4bqnbiBsxrB1IMO9IHRp4bq/cCB0aGVvIMSRw7MgbMOgIE5ldXJhbCBuZXR3b3JrIGNo4buJIGhv4bqhdCDEkeG7mW5nIHThu5FpIMawdSBraGkgZOG7ryBsaeG7h3UgxJHhuqd1IHbDoG8gxJHGsOG7o2MgY2h14bqpbiBow7NhIGLhurFuZyBow6BtIHNjYWxlLCBz4butIGThu6VuZyB0cnVuZyBiw6xuaCBj4bunYSB04bqtcCB0cmFpbjoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptdSA8LSBhcHBseSh0cmFpbl9kYXRhLCAyLCBtZWFuKQ0Kc2lnbWEgPC0gYXBwbHkodHJhaW5fZGF0YSwgMiwgc2QpDQp0cmFpbl9kYXRhIDwtIHNjYWxlKHRyYWluX2RhdGEsIGNlbnRlciA9IG11LCBzY2FsZSA9IHNpZ21hKQ0KDQp0ZXN0X2RhdGEgPC0gc2NhbGUodGVzdF9kYXRhLCBjZW50ZXIgPSBtdSwgc2NhbGUgPSBzaWdtYSkNCg0KaGVhZCh0cmFpbl9kYXRhKSU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KVHJvbmcgdGjDrSBk4bulIG7DoHksIGRvIGLDoGkgdG/DoW4gcuG6pXQgxJHGoW4gZ2nhuqNuIG7Dqm4gdGEgY2jhu4kgY+G6p24gZMO5bmcgbeG7mXQgbeG6oW5nIG5ldXJvbiBjw7MgY+G6pXUgdHLDumMgMiBs4bubcCBoaWRkZW4sIG3hu5dpIGzhu5twIGfhu5NtIDY0IMSRxqFuIHbhu4sgbmV1cm9ucy4gxJDhuqd1IHJhIGPhu6dhIG3huqFuZyBsxrDhu5tpIGzDoCAxIG5ldXJvbiBkdXkgbmjhuqV0ICh2w6Aga2jDtG5nIGvDqG0gYWN0aXZhdGlvbiBmdW5jdGlvbiBuaMawIHRyb25nIGLDoGkgdG/DoW4gdGnDqm4gbMaw4bujbmcgeMOhYyBzdeG6pXQpIOKAkyDEkeG7pyDEkeG7gyB4deG6pXQga+G6v3QgcXXhuqMgbMOgIG3hu5l0IGNvbiBz4buRIChzY2FsYXIpIHbDoCB0dXnhur9uIHTDrW5oIOKAkyBjaMOtbmggbMOgIGdpw6EgdHLhu4sgY+G6p24gxrDhu5tjIGzGsOG7o25nLg0KDQpN4bqhbmcgbMaw4bubaSDEkcaw4bujYyDEkcOzbmcgZ8OzaSB24bubaSBsb3NzIGZ1bmN0aW9uIGzDoCBtZWFuIHNxdWFyZWQgZXJyb3IgKHNhaSBz4buRIGLDrG5oIHBoxrDGoW5nIHRydW5nIGLDrG5oKSB2w6wgxJHDonkgbMOgIGLDoGkgdG/DoW4gaOG7k2kgcXV5LiBUcm9uZyBxdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiwgdGnDqnUgY2jDrSDEkcaw4bujYyBz4butIGThu6VuZyDEkeG7gyBj4bqjaSB0aGnhu4duIGhp4buHdSBuxINuZyBj4bunYSBtw7QgaMOsbmggbMOgIE1TRSAoc2FpIHPhu5EgdHV54buHdCDEkeG7kWkgdHJ1bmcgYsOsbmgpLiANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpidWlsZF9tb2RlbCA8LSBmdW5jdGlvbigpIHsNCiAgbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSANCiAgICBsYXllcl9kZW5zZSh1bml0cyA9IDY0LCBhY3RpdmF0aW9uID0gInJlbHUiLCANCiAgICAgICAgICAgICAgICBpbnB1dF9zaGFwZSA9IGRpbSh0cmFpbl9kYXRhKVtbMl1dKSAlPiUgDQogICAgbGF5ZXJfZGVuc2UodW5pdHMgPSA2NCwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIA0KICAgIGxheWVyX2RlbnNlKHVuaXRzID0gMSkgDQogIA0KICBtb2RlbCAlPiUgY29tcGlsZSgNCiAgICBvcHRpbWl6ZXIgPSAicm1zcHJvcCIsIA0KICAgIGxvc3MgPSAibXNlIiwgDQogICAgbWV0cmljcyA9IGMoIm1hZSIpDQogICkNCn0NCmBgYA0KDQpUaeG6v3AgdGhlbywgdGEgdGjhu7FjIGhp4buHbiBt4buZdCBxdXkgdHLDrG5oIGtp4buDbSBjaOG7qW5nIGNow6lvIDUgYmxvY2tzIGzhurdwIGzhuqFpIDEwMCBs4bqnbg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCg0Kc2V0LnNlZWQoMTIzKQ0KDQprIDwtIDUNCmluZGljZXMgPC0gc2FtcGxlKDE6bnJvdyh0cmFpbl9kYXRhKSkNCmZvbGRzIDwtIGN1dCgxOmxlbmd0aChpbmRpY2VzKSwgYnJlYWtzID0gaywgbGFiZWxzID0gRkFMU0UpIA0KDQpudW1fZXBvY2hzIDwtIDEwMA0KDQphbGxfbWFlX2hpc3RvcmllcyA8LSBOVUxMDQpmb3IgKGkgaW4gMTprKSB7DQogIGNhdCgicHJvY2Vzc2luZyBmb2xkICMiLCBpLCAiXG4iKQ0KICANCiAgIyBQcmVwYXJlIHRoZSB2YWxpZGF0aW9uIGRhdGE6IGRhdGEgZnJvbSBwYXJ0aXRpb24gIyBrDQogIHZhbF9pbmRpY2VzIDwtIHdoaWNoKGZvbGRzID09IGksIGFyci5pbmQgPSBUUlVFKQ0KICB2YWxfZGF0YSA8LSB0cmFpbl9kYXRhW3ZhbF9pbmRpY2VzLF0NCiAgdmFsX3RhcmdldHMgPC0gdHJhaW5fdGFyZ2V0c1t2YWxfaW5kaWNlc10NCiAgDQogICMgUHJlcGFyZSB0aGUgdHJhaW5pbmcgZGF0YTogZGF0YSBmcm9tIGFsbCBvdGhlciBwYXJ0aXRpb25zDQogIHBhcnRpYWxfdHJhaW5fZGF0YSA8LSB0cmFpbl9kYXRhWy12YWxfaW5kaWNlcyxdDQogIHBhcnRpYWxfdHJhaW5fdGFyZ2V0cyA8LSB0cmFpbl90YXJnZXRzWy12YWxfaW5kaWNlc10NCiAgDQogICMgQnVpbGQgdGhlIEtlcmFzIG1vZGVsIChhbHJlYWR5IGNvbXBpbGVkKQ0KICBtb2RlbCA8LSBidWlsZF9tb2RlbCgpDQogIA0KICAjIFRyYWluIHRoZSBtb2RlbCAoaW4gc2lsZW50IG1vZGUsIHZlcmJvc2U9MCkNCiAgaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KA0KICAgIHBhcnRpYWxfdHJhaW5fZGF0YSwgcGFydGlhbF90cmFpbl90YXJnZXRzLA0KICAgIHZhbGlkYXRpb25fZGF0YSA9IGxpc3QodmFsX2RhdGEsIHZhbF90YXJnZXRzKSwNCiAgICBlcG9jaHMgPSBudW1fZXBvY2hzLCBiYXRjaF9zaXplID0gMSwgdmVyYm9zZSA9IDANCiAgKQ0KICBtYWVfaGlzdG9yeSA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX21lYW5fYWJzb2x1dGVfZXJyb3INCiAgYWxsX21hZV9oaXN0b3JpZXMgPC0gcmJpbmQoYWxsX21hZV9oaXN0b3JpZXMsIG1hZV9oaXN0b3J5KQ0KfQ0KDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptZWRpYW5fbWFlX2hpc3RvcnkgPC0gZGF0YS5mcmFtZSgNCiAgZXBvY2ggPSBzZXEoMTpuY29sKGFsbF9tYWVfaGlzdG9yaWVzKSksDQogIHZhbGlkYXRpb25fbWFlID0gYXBwbHkoYWxsX21hZV9oaXN0b3JpZXMsIDIsIG1lZGlhbikNCikNCg0KbWVkaWFuX21hZV9oaXN0b3J5JT4lZ2dwbG90KGFlcyh4ID0gZXBvY2gsIHkgPSB2YWxpZGF0aW9uX21hZSkpK2dlb21fbGluZShjb2w9InJlZCIsc2l6ZT0xKSsNCiAgdGhlbWVfYncoKSsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gbWVkaWFuKG1lZGlhbl9tYWVfaGlzdG9yeSR2YWxpZGF0aW9uX21hZSksbGluZXR5cGU9MikNCg0KZ2dwbG90KG1lZGlhbl9tYWVfaGlzdG9yeSwgYWVzKHggPSBlcG9jaCwgeSA9IHZhbGlkYXRpb25fbWFlKSkrZ2VvbV9zbW9vdGgoY29sPSJyZWQiLGZpbGw9InJlZCIpKw0KICB0aGVtZV9idygpKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSBtZWRpYW4obWVkaWFuX21hZV9oaXN0b3J5JHZhbGlkYXRpb25fbWFlKSxsaW5ldHlwZT0yKQ0KYGBgDQoNCkN14buRaSBjw7luZywgdGEgZOG7sW5nIG3hu5l0IG3DtCBow6xuaCBjaMOtbmggdGjhu6ljIHbhu5tpIHPhu5EgZXBvY2hzID0gNTAgdsOgIGJhdGNoX3NpemU9MTYNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQoNCnNldC5zZWVkKDEyMykNCg0KbW9kZWwgPC0gYnVpbGRfbW9kZWwoKQ0KDQojIFRyYWluIGl0IG9uIHRoZSBlbnRpcmV0eSBvZiB0aGUgZGF0YS4NCm1vZGVsICU+JSBmaXQodHJhaW5fZGF0YSwgdHJhaW5fdGFyZ2V0cywNCiAgICAgICAgICAgICAgZXBvY2hzID0gNTAsIGJhdGNoX3NpemUgPSAxNiwgdmVyYm9zZSA9IDApDQoNCnN1bW1hcnkobW9kZWwpDQoNCmBgYA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnJlc3VsdCA8LSBtb2RlbCAlPiUgZXZhbHVhdGUodGVzdF9kYXRhLCB0ZXN0X3RhcmdldHMpDQoNCnJlc3VsdA0KYGBgDQoNCiMgU28gc8OhbmggaGnhu4d1IG7Eg25nIDIgbcO0IGjDrG5oDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KVHJ1dGg9dGVzdE0kVExDDQpETk49cHJlZGljdChtb2RlbCx0ZXN0X2RhdGEpDQpMTVM9cHJlZGljdChtMixuZXdkYXRhPXRlc3RNLHR5cGU9InJlc3BvbnNlIikNCg0KcGRmPWNiaW5kKFRydXRoLEROTixMTVMpJT4lYXNfZGF0YV9mcmFtZSgpDQoNCmNvbG5hbWVzKHBkZik9YygiVHJ1dGgiLCJEZWVwTk4iLCJMTVMiKQ0KDQpwZGYkQWdlPXRlc3RNJEFnZQ0KDQpsaWJyYXJ5KG1scikNCg0KcmVnci50YXNrPSBtbHI6Om1ha2VSZWdyVGFzayhpZCA9ICJkZk0iLCBkYXRhPXRlc3RNLCB0YXJnZXQgPSAiVExDIikNCnJlZ3IubHJuID0gbWFrZUxlYXJuZXIoInJlZ3IuZ2xtIikNCg0KZHVtbXk9bWxyOjp0cmFpbihyZWdyLmxybixyZWdyLnRhc2spDQoNCmR1bXByZWRETk49cHJlZGljdChkdW1teSxyZWdyLnRhc2spDQpkdW1wcmVkTE1TPXByZWRpY3QoZHVtbXkscmVnci50YXNrKQ0KDQpkdW1wcmVkRE5OJGRhdGEkcmVzcG9uc2U8LUROTg0KZHVtcHJlZExNUyRkYXRhJHJlc3BvbnNlPC1MTVMNCmBgYA0KDQrEkMOieSBsw6AgaGnhu4d1IG7Eg25nIG3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXQgduG7m2kgVGVuc29yZmxvIChLZXJhcykNCg0KYGBge3J9DQptZXRzPWxpc3QobXNlLG1hZSxtZWRhZSxybXNlKQ0KcGVyZm9ybWFuY2UoZHVtcHJlZEROTixtZWFzdXJlcyA9bWV0cykNCmBgYA0KDQpDw7JuIMSRw6J5IGzDoCBoaeG7h3UgbsSDbmcgbcO0IGjDrG5oIExNUyBj4bunYSBnYW1sc3MNCg0KYGBge3J9DQpwZXJmb3JtYW5jZShkdW1wcmVkTE1TLG1lYXN1cmVzID1tZXRzKQ0KDQpgYGANCg0KTcOgdSB4YW5oIGzDoCBtw7QgaMOsbmggTE1TLCBtw6B1IMSR4buPIGzDoCBtw7QgaMOsbmggRGVlcCBuZXVyYWwgbmV0d29yaw0KDQpgYGB7cn0NCnBkZiU+JWdncGxvdCgpK2dlb21fcG9pbnQoYWVzKHg9QWdlLHk9VHJ1dGgpLGNvbD0iYmxhY2siLGFscGhhPTAuNSkrDQogIGdlb21fc21vb3RoKGFlcyh4PUFnZSx5PUROTiksY29sPSJyZWQiLGZpbGw9InJlZCIsYWxwaGE9MC4yKSsNCiAgZ2VvbV9zbW9vdGgoYWVzKHg9QWdlLHk9TE1TKSxjb2w9ImJsdWUiLGZpbGw9ImJsdWUiLGFscGhhPTAuMikrdGhlbWVfYncoKStzY2FsZV95X2NvbnRpbnVvdXMoIlRMQyIpDQpgYGANCg0KVHJ1bmcgYsOsbmgsIG3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXR3b3JrIGNo4buJIHRpw6puIGzGsOG7o25nIGR1bmcgdMOtY2ggcGjhu5VpIHNhaSBraG/huqNuZyA1MCBtTCANCg0KYGBge3J9DQptZWFuKHBkZiREZWVwTk4tcGRmJFRydXRoKQ0KYGBgDQoNCiMgTmjhuq1uIHjDqXQNCg0KRGVlcCBuZXVyYWwgbmV0d29yayAoY8OybiBn4buNaSBsw6AgRGVlcCBsZWFybmluZykgbMOgIG3hu5l0IGdp4bqjaSBwaMOhcCB2w7QgY8O5bmcgbeG6oW5oIG3hur0gY2hvIGLDoGkgdG/DoW4gaOG7k2kgcXV5LCB0aOG6rW0gY2jDrSBuw7MgdMawxqFuZyDEkcawxqFuZyB24bubaSBtw7QgaMOsbmggcGhpIHR1eeG6v24gdMOtbmggduG7m2kgaMOgbSBTcGxpbmUgYsO5IHRy4burIHbhu5FuIGzDoCBjw7RuZyBj4bulIHThu5FpIMawdSBuaOG6pXQgaGnhu4duIG5heSBjaG8gY8OhYyBtw7QgaMOsbmggdMSDbmcgdHLGsOG7n25nIHRyb25nIHkgaOG7jWMgbMOibSBzw6BuZy4NCg0KVHV5IG5oacOqbiwgbcO0IGjDrG5oIExNUyBjw7MgMiDGsHUgxJFp4buDbSBtw6AgRGVlcCBOTiBraMO0bmcgdGjhu4MgdGhheSB0aOG6vywgdGjhu6kgbmjhuqV0IGzDoCB0w61uaCB0xrDhu51uZyBtaW5oOiBu4buZaSBkdW5nIG3DtCBow6xuaCBjw7MgdGjhu4MgxJHGsOG7o2MgdMOtbmggdGjhu6cgY8O0bmcgdOG7qyBsb29rdXAgdGFibGUsIHRo4bupIGhhaSBsw6Aga2jhuqMgbsSDbmcgxrDhu5tjIHTDrW5oIMSRxrDhu6NjIGPDoWMgbmfGsOG7oW5nIGdp4bubaSBo4bqhbiB0csOqbiB2w6AgZMaw4bubaSBj4bunYSBnacOhIHRy4buLIGLDrG5oIHRoxrDhu51uZyB0cm9uZyBxdeG6p24gdGjhu4MgdOG7qyAzIHRoYW0gc+G7kSBMLE0gdsOgIFMsIGNobyBwaMOpcCBkaeG7hW4gZ2nhuqNpIGvhur90IHF14bqjIHbhu4EgbeG6t3QgbMOibSBzw6BuZy5Ucm9uZyBraGkgxJHDsyBtw7QgaMOsbmggRGVlcCBOTiBraMO0bmcgdGjhu4MgZGnhu4VuIGdp4bqjaSDEkcaw4bujYyB2w6AgY2jhu4kgY8OzIHRo4buDIMSRxrDhu6NjIMOhcCBk4bulbmcgZMaw4bubaSBk4bqhbmcgaGFyZCBjb2RlIHRyb25nIGZpcm13YXJlIGPhu6dhIHRoaeG6v3QgYuG7iyB4w6l0IG5naGnhu4dtLg0KDQpUdXkgbmhpw6puLCBjw7MgdGjhu4MgZOG7sSBiw6FvIHRyb25nIHTGsMahbmcgbGFpIGdp4bqjaSBwaMOhcCBEZWVwIGxlYXJuaW5nIHPhur0gY2hvIHBow6lwIG5o4buvbmcgdGhp4bq/dCBi4buLIHjDqXQgbmdoaeG7h20gY8OzIGto4bqjIG7Eg25nIHThu7EgdOG6oW8gcmEgbcO0IGjDrG5oIMaw4bubYyBsxrDhu6NuZyBnacOhIHRy4buLIHRoYW0gY2hp4bq/dSBjaG8gY2jDrW5oIG7DsyBtw6Aga2jDtG5nIGPhuqduIHPhu7EgY2FuIHRoaeG7h3AgY+G7p2EgY29uIG5nxrDhu51pLCB2w6AgxJHDonkgbMOgIG3hu5l0IOG7qW5nIGThu6VuZyBraMO0bmcgdGjhu4MgeGVtIHRoxrDhu51uZy4NCg==