1 Mở đầu

Câu chuyện này bắt đầu với bệnh COPD (Bệnh phổi tắc nghẽn mạn tính), vào năm 2017 tổ chức GOLD đã cập nhật tài liệu khuyến cáo về chẩn đoán, tiên lượng và điều trị bệnh COPD, trong đó đặt ra hai hệ thống phân loại riêng biệt: Độ nặng dựa vào kết quả hô hấp ký, có 4 mức độ là 1,2,3,4; và phân loại định hướng điều trị gồm 4 loại là A,B,C,D dựa vào triệu chứng lâm sàng và các biến chứng.

Trong một nghiên cứu (giả định), chúng ta có 100 bệnh nhân COPD được theo dõi trong khoảng thời gian dài qua 10 lần tái khám, mỗi lần cách nhau 4 tháng. Ở mỗi lần khám, trạng thái phân loại độ nặng theo GOLD được đánh giá lại. Như vậy, ở mỗi bệnh nhân chúng ta có dữ liệu gồm 2 chuỗi trạng thái có độ dài = 10 (Severity và Grade), đều là biến định tính rời rạc (categorical).

Câu hỏi đặt ra là: Liệu chúng ta có thể rút ra được một quy luật diễn tiến của độ nặng và trạng thái lâm sàng theo thời gian hay không ?

Nhi sẽ giới thiệu với các bạn một phương pháp thống kê cho phép trả lời câu hỏi trên, đó là mô hình Markov ẩn (Hidden Markov model, HMM), thông qua package seqHMM của Satu Helske và Jouni Helske, mới được họ công bố vào đầu năm 2019.

Trước hết chúng ta tải dữ liệu COPD_HMM.csv vào R

library(tidyverse)
library(seqHMM)
library(igraph)

df = read.csv("COPD_HMM.csv",sep = ";")

head(df)
##   Visit Severity Grade
## 1     1       G2     B
## 2     2       G3     A
## 3     3       G2     C
## 4     4       G2     C
## 5     5       G4     B
## 6     6       G4     B

Dữ liệu có dạng bảng dọc (long format), mỗi hàng là 1 thời điểm (Visit), mỗi 10 hàng là 1 bệnh nhân, 2 cột là 2 chuỗi trạng thái Severity và Grade.

Qua thống kê mô tả với hàm group_by, ta thấy có 12 cặp tổ hợp giữa Severity và Grade:

df %>% mutate(ID = rep(c(1:100),each = 10))%>%
  group_by(Severity,Grade) %>%
  summarise(frequency = n())
## # A tibble: 12 x 3
## # Groups:   Severity [4]
##    Severity Grade frequency
##    <fct>    <fct>     <int>
##  1 G1       A            12
##  2 G1       B            51
##  3 G2       A            12
##  4 G2       B           130
##  5 G2       C           108
##  6 G3       A            11
##  7 G3       B           257
##  8 G3       C           100
##  9 G4       A             3
## 10 G4       B            26
## 11 G4       C           169
## 12 G4       D           121

2 Mô hình HMM

Sơ đồ trên trình bày về các thành phần trong một mô hình Markov ẩn, đầu ra của mô hình là 2 chuỗi trạng thái được quan sát (Y), ta giả định rằng chúng là kết quả của một quá trình Markov mang tính ngẫu nhiên (stochastic process) gồm các tham số trạng thái chưa xác định (trạng thái ẩn, X) với xác suất ban đầu (initial probability). Có sự chuyển tiếp giữa các tham số trạng thái ẩn, và xác suất đầu ra cho phép liên kết mỗi trạng thái ẩn với kết quả quan sát được.

Trong bài toán này, chúng ta có 2 chuỗi kết quả đầu ra là Severity và Grade, nên đây là một mô hình đa kênh (multichannels), ta ấn định một số lượng tham số trạng thái ẩn X (hidden states) = 12. Ta sẽ dùng mô hình HMM để xác định những xác suất chuyển tiếp.

Trước hết, ta chuyển dạng dữ liệu từ long thành wide format, là định dạng yêu cầu bởi package seqHMM, với 1 vòng lặp while :

# Setting
period = 10
start = 1

Severity = matrix(ncol = period)
Class = matrix(ncol = period )

while ((start + period -1)<= nrow(df)){
  end = (start + period) - 1
  temp_df = df[c(start:end),]
  Severity = rbind(Severity,as.vector(temp_df$Severity))%>%na.omit()
  Class = rbind(Class,as.vector(temp_df$Grade))%>%na.omit()
  start = end + 1
}

colnames(Severity) = c(paste('t',rep(1:period), sep=""))
colnames(Class) = c(paste('t',rep(1:period), sep=""))

Ta cũng tách rời 2 phần Severity và Grade trong 2 matrices riêng :

head(Severity)
##      t1   t2   t3   t4   t5   t6   t7   t8   t9   t10 
## [1,] "G2" "G3" "G2" "G2" "G4" "G4" "G4" "G4" "G3" "G3"
## [2,] "G3" "G2" "G4" "G3" "G1" "G3" "G3" "G4" "G4" "G3"
## [3,] "G2" "G3" "G4" "G4" "G4" "G2" "G3" "G4" "G3" "G3"
## [4,] "G3" "G1" "G3" "G4" "G3" "G4" "G1" "G1" "G4" "G3"
## [5,] "G2" "G4" "G3" "G3" "G3" "G1" "G4" "G4" "G3" "G2"
## [6,] "G2" "G4" "G4" "G2" "G3" "G2" "G4" "G4" "G4" "G4"
head(Class)
##      t1  t2  t3  t4  t5  t6  t7  t8  t9  t10
## [1,] "B" "A" "C" "C" "B" "B" "B" "B" "A" "A"
## [2,] "A" "B" "B" "A" "A" "A" "A" "B" "B" "A"
## [3,] "B" "A" "A" "A" "A" "B" "A" "B" "A" "B"
## [4,] "B" "B" "B" "B" "B" "B" "B" "B" "B" "B"
## [5,] "C" "B" "B" "B" "B" "B" "B" "B" "B" "B"
## [6,] "C" "B" "B" "B" "B" "C" "B" "B" "B" "B"

Bước tiếp theo, ta dùng hàm seqdef (bạn còn nhớ package TraMineR) để thiết lập 2 chuỗi dữ liệu đầu ra cho mô hình là GOLD và ABCD_Class, từ 2 matrices Severity và Class ở trên

GOLD = seqdef(Severity,start=1)
ABCD_Class = seqdef(Class, start=1)

Chuỗi dữ liệu là 1 object chứa các thông tin về cấu trúc như độ dài =10, số quan sát = 100, tên các nhãn giá trị, và phổ màu để vẽ đồ thị.

summary(GOLD)
##  [>] sequence object created with TraMineR version 2.0-11.1 
##  [>] 100 sequences in the data set, 100 unique 
##  [>] min/max sequence length: 10/10
##  [>] alphabet (state labels):  
##      1=G1 (G1)
##      2=G2 (G2)
##      3=G3 (G3)
##      4=G4 (G4)
##  [>] dimensionality of the sequence space: 30 
##  [>] colors: 1=#7FC97F 2=#BEAED4 3=#FDC086 4=#FFFF99
summary(ABCD_Class)
##  [>] sequence object created with TraMineR version 2.0-11.1 
##  [>] 100 sequences in the data set, 99 unique 
##  [>] min/max sequence length: 10/10
##  [>] alphabet (state labels):  
##      1=A (A)
##      2=B (B)
##      3=C (C)
##      4=D (D)
##  [>] dimensionality of the sequence space: 30 
##  [>] colors: 1=#7FC97F 2=#BEAED4 3=#FDC086 4=#FFFF99

Ta có thể thay đổi phổ màu của mỗi object để đạt hiệu ứng mỹ thuật tùy ý:

attr(GOLD, "cpal") <- c('#ffc2c5',
                         '#fa757c',
                         '#fc303a',
                         '#8a0007')

attr(ABCD_Class, "cpal") <- c('#98c429',
                              '#ffce08',
                              '#ff8903',
                              '#ff0385')

Hàm ssplot cho phép vẽ các biểu đồ sau, nhằm tóm tắt về phân bố và quy luật chuyển tiếp giữa các trạng thái

ssplot(list("GOLD Severity" = GOLD, 
            "ABCD_Grade" = ABCD_Class),
       plots = 'obs',
       type = 'd')

ssplot(list("GOLD Severity" = GOLD, 
            "ABCD_Grade" = ABCD_Class),
       plots = 'obs',
       sort.channel = "from.start",
       type = 'I')

Ta có thể thấy rằng ở mỗi bệnh nhân (hàng), độ năng và trạng thái lâm sàng của bệnh COPD không diễn tiến một cách tuyến tính, nhưng có thể chuyển tiếp ngược từ một trạng thái nặng hơn về trạng thái nhẹ hơn:

Để dựng mô hình HMM multichannel (đa kênh), các chuỗi Y được đóng gói vào trong 1 list và đưa vào hàm build_hmm, sau đó ta xác định số hidden states mong muốn (12), rồi dùng tiếp hàm fit_model để dựng mô hình.

Tùy chỉnh threads cho phép chạy quy trình trên nhiều CPU để tăng tốc độ tính toán, trên máy của Nhi có 8 cores nên Nhi set threads = 8.

list(GOLD, ABCD_Class) %>%
  build_hmm(observations = ., 
            n_states = 12,
            channel_names = c("GOLD","Class")) %>% 
  fit_model(., 
            em_step = TRUE, 
            global_step = TRUE,
            local_step = TRUE,
            threads = 8) -> mc_fit

Sau khi mô hình dựng xong, bạn có thể dùng hàm plot để vẽ ra mạng lưới chuyển tiếp giữa các hidden states, là kết quả cần tìm.

plot(mc_fit$model,
     edge.arrow.size = 0.5, 
     interactive = TRUE, 
     loops  = TRUE, 
     vertex.size = 15,
     edge.curved = FALSE,
     layout =  layout.star, 
     legend.prop = 0.1,
     trim = 0.01,
     with.legend = "right")

Packages HMM vẽ mạng lưới này bằng package igraph, nên bạn có thể gọi bất cứ định dạng mỹ thuật nào cho mô hình mạng từ package igraph trong tùy chỉnh layout.

plot(mc_fit$model,
     edge.arrow.size = 0.5, 
     interactive = TRUE, 
     loops  = TRUE, 
     vertex.size = 15,
     edge.curved = TRUE,
     layout =  layout.fruchterman.reingold, 
     legend.prop = 0.1,
     trim = 0.01,
     with.legend = "right")

Hàm BIC cho phép đánh giá phẩm chất mô hình:

BIC(mc_fit$model)
## [1] 4748.835

Nội dung bên trong mô hình HMM gồm những ma trận xac suất, như: matrix Initial probabilities là xác suất ban đầu của mỗi hidden state (tại thời điểm t=1), matrix Transition probabilities chứa xác suất chuyển tiếp giữa các hidden states (từ đó biểu đồ mạng được vẽ ra trong igraph, và bạn cũng có thể vẽ thủ công bằng ggraph), Sau cùng là Emission probabilities cho mỗi chuỗi đầu ra

mc_fit$model
## Initial probabilities :
##  State 1  State 2  State 3  State 4  State 5  State 6  State 7  State 8 
##   0.0430   0.1200   0.1772   0.2644   0.0174   0.0000   0.1738   0.1700 
##  State 9 State 10 State 11 State 12 
##   0.0000   0.0343   0.0000   0.0000 
## 
## 
## Transition probabilities :
##           to
## from       State 1 State 2 State 3 State 4 State 5 State 6 State 7 State 8
##   State 1   0.0637   0.000 0.00000   0.456 0.00000  0.0000  0.0000  0.4800
##   State 2   0.0000   0.000 0.33709   0.000 0.00000  0.0000  0.0000  0.0000
##   State 3   0.0000   0.114 0.49461   0.000 0.00000  0.0000  0.0000  0.0000
##   State 4   0.0343   0.000 0.00438   0.375 0.00465  0.0999  0.1123  0.3692
##   State 5   0.0889   0.000 0.30491   0.567 0.00000  0.0387  0.0000  0.0000
##   State 6   0.0000   0.000 0.00000   0.803 0.04448  0.0297  0.1226  0.0000
##   State 7   0.0440   0.000 0.00000   0.305 0.03072  0.1506  0.0930  0.1750
##   State 8   0.0333   0.000 0.00000   0.473 0.00000  0.1078  0.0981  0.2881
##   State 9   0.0171   0.214 0.51619   0.000 0.03399  0.0000  0.0283  0.0000
##   State 10  0.0000   0.000 0.00000   0.000 0.00000  0.0292  0.0000  0.0204
##   State 11  0.0000   0.000 0.54627   0.000 0.00000  0.0000  0.0000  0.0000
##   State 12  0.0000   0.575 0.00000   0.000 0.00000  0.0000  0.0000  0.0000
##           to
## from        State 9 State 10 State 11 State 12
##   State 1  0.00e+00   0.0000    0.000    0.000
##   State 2  1.43e-01   0.0000    0.232    0.288
##   State 3  2.26e-01   0.0000    0.165    0.000
##   State 4  0.00e+00   0.0000    0.000    0.000
##   State 5  0.00e+00   0.0000    0.000    0.000
##   State 6  0.00e+00   0.0000    0.000    0.000
##   State 7  0.00e+00   0.0952    0.000    0.107
##   State 8  0.00e+00   0.0000    0.000    0.000
##   State 9  1.90e-01   0.0000    0.000    0.000
##   State 10 0.00e+00   0.9504    0.000    0.000
##   State 11 3.50e-01   0.0000    0.103    0.000
##   State 12 9.53e-05   0.0000    0.000    0.425
## 
## 
## Emission probabilities :
## GOLD :
##            symbol_names
## state_names     G1     G2    G3    G4
##    State 1  1.0000 0.0000 0.000 0.000
##    State 2  0.0000 0.0000 0.000 1.000
##    State 3  0.0000 0.3342 0.666 0.000
##    State 4  0.0445 0.1215 0.834 0.000
##    State 5  1.0000 0.0000 0.000 0.000
##    State 6  0.0000 0.8715 0.128 0.000
##    State 7  0.0000 1.0000 0.000 0.000
##    State 8  0.0000 0.0000 0.000 1.000
##    State 9  0.0000 0.0000 0.000 1.000
##    State 10 0.0836 0.0678 0.390 0.459
##    State 11 0.1546 0.8454 0.000 0.000
##    State 12 0.2169 0.3229 0.460 0.000
## 
## Class :
##            symbol_names
## state_names      A     B     C      D
##    State 1  0.0758 0.924 0.000 0.0000
##    State 2  0.0000 0.000 0.000 1.0000
##    State 3  0.0000 0.000 1.000 0.0000
##    State 4  0.0000 1.000 0.000 0.0000
##    State 5  1.0000 0.000 0.000 0.0000
##    State 6  0.1661 0.834 0.000 0.0000
##    State 7  0.0000 0.208 0.792 0.0000
##    State 8  0.0000 0.000 0.975 0.0247
##    State 9  0.0000 0.000 0.000 1.0000
##    State 10 0.2231 0.777 0.000 0.0000
##    State 11 0.1147 0.885 0.000 0.0000
##    State 12 0.0000 1.000 0.000 0.0000

3 Kết luận:

Qua thí dụ mô phỏng này, các bạn đã làm quen với một số tính năng cơ bản của package seqHMM để dựng mô hình Markov ẩn đa kênh. Đây là một phương pháp có tiềm năng ứng dụng rộng lớn, chứ không chỉ dành cho bài toán phân tích chuỗi giá trị categorical. Package seqHMM còn cho phép bạn đưa vào mô hình các hiệp biến (covariates) thí dụ tuổi, giới tính, … của bệnh nhân để thực hiện một phân tích Clustering kết hợp mô hình Markov.Như vậy bạn đã có trong tay một công cụ độc đáo trong nghiên cứu trường diễn hoặc các bài toán liên quan đến dữ liệu chuỗi.

LS0tDQp0aXRsZTogIk3DtCBow6xuaCBNYXJrb3Yg4bqpbiIgDQphdXRob3I6ICJMw6ogTmfhu41jIEto4bqjIE5oaSINCmRhdGU6ICIyNCBUaMOhbmcgOCBuxINtIDIwMTkiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZGVmYXVsdCINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCiAgICBkZXY6ICdzdmcnDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmBgYA0KDQohW10oSE1NMS5wbmcpDQoNCiMgTeG7nyDEkeG6p3UNCg0KQ8OidSBjaHV54buHbiBuw6B5IGLhuq90IMSR4bqndSB24bubaSBi4buHbmggQ09QRCAoQuG7h25oIHBo4buVaSB04bqvYyBuZ2jhur1uIG3huqFuIHTDrW5oKSwgdsOgbyBuxINtIDIwMTcgdOG7lSBjaOG7qWMgR09MRCDEkcOjIGPhuq1wIG5o4bqtdCB0w6BpIGxp4buHdSBraHV54bq/biBjw6FvIHbhu4EgY2jhuqluIMSRb8OhbiwgdGnDqm4gbMaw4bujbmcgdsOgIMSRaeG7gXUgdHLhu4sgYuG7h25oIENPUEQsIHRyb25nIMSRw7MgxJHhurd0IHJhIGhhaSBo4buHIHRo4buRbmcgcGjDom4gbG/huqFpIHJpw6puZyBiaeG7h3Q6IMSQ4buZIG7hurduZyBk4buxYSB2w6BvIGvhur90IHF14bqjIGjDtCBo4bqlcCBrw70sIGPDsyA0IG3hu6ljIMSR4buZIGzDoCAxLDIsMyw0OyB2w6AgcGjDom4gbG/huqFpIMSR4buLbmggaMaw4bubbmcgxJFp4buBdSB0cuG7iyBn4buTbSA0IGxv4bqhaSBsw6AgQSxCLEMsRCBk4buxYSB2w6BvIHRyaeG7h3UgY2jhu6luZyBsw6JtIHPDoG5nIHbDoCBjw6FjIGJp4bq/biBjaOG7qW5nLiANCg0KVHJvbmcgbeG7mXQgbmdoacOqbiBj4bupdSAoZ2nhuqMgxJHhu4tuaCksIGNow7puZyB0YSBjw7MgMTAwIGLhu4duaCBuaMOibiBDT1BEIMSRxrDhu6NjIHRoZW8gZMO1aSB0cm9uZyBraG/huqNuZyB0aOG7nWkgZ2lhbiBkw6BpIHF1YSAxMCBs4bqnbiB0w6FpIGtow6FtLCBt4buXaSBs4bqnbiBjw6FjaCBuaGF1IDQgdGjDoW5nLiDhu54gbeG7l2kgbOG6p24ga2jDoW0sIHRy4bqhbmcgdGjDoWkgcGjDom4gbG/huqFpIMSR4buZIG7hurduZyB0aGVvIEdPTEQgxJHGsOG7o2MgxJHDoW5oIGdpw6EgbOG6oWkuIE5oxrAgduG6rXksIOG7nyBt4buXaSBi4buHbmggbmjDom4gY2jDum5nIHRhIGPDsyBk4buvIGxp4buHdSBn4buTbSAyIGNodeG7l2kgdHLhuqFuZyB0aMOhaSBjw7MgxJHhu5kgZMOgaSA9IDEwIChTZXZlcml0eSB2w6AgR3JhZGUpLCDEkeG7gXUgbMOgIGJp4bq/biDEkeG7i25oIHTDrW5oIHLhu51pIHLhuqFjIChjYXRlZ29yaWNhbCkuDQoNCkPDonUgaOG7j2kgxJHhurd0IHJhIGzDoDogTGnhu4d1IGNow7puZyB0YSBjw7MgdGjhu4MgcsO6dCByYSDEkcaw4bujYyBt4buZdCBxdXkgbHXhuq10IGRp4buFbiB0aeG6v24gY+G7p2EgxJHhu5kgbuG6t25nIHbDoCB0cuG6oW5nIHRow6FpIGzDom0gc8OgbmcgdGhlbyB0aOG7nWkgZ2lhbiBoYXkga2jDtG5nID8NCg0KTmhpIHPhur0gZ2nhu5tpIHRoaeG7h3UgduG7m2kgY8OhYyBi4bqhbiBt4buZdCBwaMawxqFuZyBwaMOhcCB0aOG7kW5nIGvDqiBjaG8gcGjDqXAgdHLhuqMgbOG7nWkgY8OidSBo4buPaSB0csOqbiwgxJHDsyBsw6AgbcO0IGjDrG5oIE1hcmtvdiDhuqluIChIaWRkZW4gTWFya292IG1vZGVsLCBITU0pLCB0aMO0bmcgcXVhIHBhY2thZ2Ugc2VxSE1NIGPhu6dhIFNhdHUgSGVsc2tlIHbDoCBKb3VuaSBIZWxza2UsIG3hu5tpIMSRxrDhu6NjIGjhu40gY8O0bmcgYuG7kSB2w6BvIMSR4bqndSBuxINtIDIwMTkuIA0KDQpUcsaw4bubYyBo4bq/dCBjaMO6bmcgdGEgdOG6o2kgZOG7ryBsaeG7h3UgQ09QRF9ITU0uY3N2IHbDoG8gUg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShzZXFITU0pDQpsaWJyYXJ5KGlncmFwaCkNCg0KZGYgPSByZWFkLmNzdigiQ09QRF9ITU0uY3N2IixzZXAgPSAiOyIpDQoNCmhlYWQoZGYpDQoNCmBgYA0KDQpE4buvIGxp4buHdSBjw7MgZOG6oW5nIGLhuqNuZyBk4buNYyAobG9uZyBmb3JtYXQpLCBt4buXaSBow6BuZyBsw6AgMSB0aOG7nWkgxJFp4buDbSAoVmlzaXQpLCBt4buXaSAxMCBow6BuZyBsw6AgMSBi4buHbmggbmjDom4sIDIgY+G7mXQgbMOgIDIgY2h14buXaSB0cuG6oW5nIHRow6FpIFNldmVyaXR5IHbDoCBHcmFkZS4NCg0KDQpRdWEgdGjhu5FuZyBrw6ogbcO0IHThuqMgduG7m2kgaMOgbSBncm91cF9ieSwgdGEgdGjhuqV5IGPDsyAxMiBj4bq3cCB04buVIGjhu6NwIGdp4buvYSBTZXZlcml0eSB2w6AgR3JhZGU6IA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmRmICU+JSBtdXRhdGUoSUQgPSByZXAoYygxOjEwMCksZWFjaCA9IDEwKSklPiUNCiAgZ3JvdXBfYnkoU2V2ZXJpdHksR3JhZGUpICU+JQ0KICBzdW1tYXJpc2UoZnJlcXVlbmN5ID0gbigpKQ0KYGBgDQoNCiMgTcO0IGjDrG5oIEhNTQ0KDQohW10oSE1NMi5wbmcpDQoNClPGoSDEkeG7kyB0csOqbiB0csOsbmggYsOgeSB24buBIGPDoWMgdGjDoG5oIHBo4bqnbiB0cm9uZyBt4buZdCBtw7QgaMOsbmggTWFya292IOG6qW4sIMSR4bqndSByYSBj4bunYSBtw7QgaMOsbmggbMOgIDIgY2h14buXaSB0cuG6oW5nIHRow6FpIMSRxrDhu6NjIHF1YW4gc8OhdCAoWSksIHRhIGdp4bqjIMSR4buLbmggcuG6sW5nIGNow7puZyBsw6Aga+G6v3QgcXXhuqMgY+G7p2EgbeG7mXQgcXXDoSB0csOsbmggTWFya292IG1hbmcgdMOtbmggbmfhuqt1IG5oacOqbiAoc3RvY2hhc3RpYyBwcm9jZXNzKSBn4buTbSBjw6FjIHRoYW0gc+G7kSB0cuG6oW5nIHRow6FpIGNoxrBhIHjDoWMgxJHhu4tuaCAodHLhuqFuZyB0aMOhaSDhuqluLCBYKSB24bubaSB4w6FjIHN14bqldCBiYW4gxJHhuqd1IChpbml0aWFsIHByb2JhYmlsaXR5KS4gQ8OzIHPhu7EgY2h1eeG7g24gdGnhur9wIGdp4buvYSBjw6FjIHRoYW0gc+G7kSB0cuG6oW5nIHRow6FpIOG6qW4sIHbDoCB4w6FjIHN14bqldCDEkeG6p3UgcmEgY2hvIHBow6lwIGxpw6puIGvhur90IG3hu5dpIHRy4bqhbmcgdGjDoWkg4bqpbiB24bubaSBr4bq/dCBxdeG6oyBxdWFuIHPDoXQgxJHGsOG7o2MuDQoNClRyb25nIGLDoGkgdG/DoW4gbsOgeSwgY2jDum5nIHRhIGPDsyAyIGNodeG7l2kga+G6v3QgcXXhuqMgxJHhuqd1IHJhIGzDoCBTZXZlcml0eSB2w6AgR3JhZGUsIG7Dqm4gxJHDonkgbMOgIG3hu5l0IG3DtCBow6xuaCDEkWEga8OqbmggKG11bHRpY2hhbm5lbHMpLCB0YSDhuqVuIMSR4buLbmggbeG7mXQgc+G7kSBsxrDhu6NuZyB0aGFtIHPhu5EgdHLhuqFuZyB0aMOhaSDhuqluIFggKGhpZGRlbiBzdGF0ZXMpID0gMTIuIFRhIHPhur0gZMO5bmcgbcO0IGjDrG5oIEhNTSDEkeG7gyB4w6FjIMSR4buLbmggbmjhu69uZyB4w6FjIHN14bqldCBjaHV54buDbiB0aeG6v3AuDQoNClRyxrDhu5tjIGjhur90LCB0YSBjaHV54buDbiBk4bqhbmcgZOG7ryBsaeG7h3UgdOG7qyBsb25nIHRow6BuaCB3aWRlIGZvcm1hdCwgbMOgIMSR4buLbmggZOG6oW5nIHnDqnUgY+G6p3UgYuG7n2kgcGFja2FnZSBzZXFITU0sIHbhu5tpIDEgdsOybmcgbOG6t3Agd2hpbGUgOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCiMgU2V0dGluZw0KcGVyaW9kID0gMTANCnN0YXJ0ID0gMQ0KDQpTZXZlcml0eSA9IG1hdHJpeChuY29sID0gcGVyaW9kKQ0KQ2xhc3MgPSBtYXRyaXgobmNvbCA9IHBlcmlvZCApDQoNCndoaWxlICgoc3RhcnQgKyBwZXJpb2QgLTEpPD0gbnJvdyhkZikpew0KICBlbmQgPSAoc3RhcnQgKyBwZXJpb2QpIC0gMQ0KICB0ZW1wX2RmID0gZGZbYyhzdGFydDplbmQpLF0NCiAgU2V2ZXJpdHkgPSByYmluZChTZXZlcml0eSxhcy52ZWN0b3IodGVtcF9kZiRTZXZlcml0eSkpJT4lbmEub21pdCgpDQogIENsYXNzID0gcmJpbmQoQ2xhc3MsYXMudmVjdG9yKHRlbXBfZGYkR3JhZGUpKSU+JW5hLm9taXQoKQ0KICBzdGFydCA9IGVuZCArIDENCn0NCg0KY29sbmFtZXMoU2V2ZXJpdHkpID0gYyhwYXN0ZSgndCcscmVwKDE6cGVyaW9kKSwgc2VwPSIiKSkNCmNvbG5hbWVzKENsYXNzKSA9IGMocGFzdGUoJ3QnLHJlcCgxOnBlcmlvZCksIHNlcD0iIikpDQpgYGANCg0KVGEgY8WpbmcgdMOhY2ggcuG7nWkgMiBwaOG6p24gU2V2ZXJpdHkgdsOgIEdyYWRlIHRyb25nIDIgbWF0cmljZXMgcmnDqm5nIDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpoZWFkKFNldmVyaXR5KQ0KYGBgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KaGVhZChDbGFzcykNCmBgYA0KDQpCxrDhu5tjIHRp4bq/cCB0aGVvLCB0YSBkw7luZyBow6BtIHNlcWRlZiAoYuG6oW4gY8OybiBuaOG7myBwYWNrYWdlIFRyYU1pbmVSKSDEkeG7gyB0aGnhur90IGzhuq1wIDIgY2h14buXaSBk4buvIGxp4buHdSDEkeG6p3UgcmEgY2hvIG3DtCBow6xuaCBsw6AgR09MRCB2w6AgQUJDRF9DbGFzcywgdOG7qyAyIG1hdHJpY2VzICBTZXZlcml0eSB2w6AgQ2xhc3Mg4bufIHRyw6puDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpHT0xEID0gc2VxZGVmKFNldmVyaXR5LHN0YXJ0PTEpDQpBQkNEX0NsYXNzID0gc2VxZGVmKENsYXNzLCBzdGFydD0xKQ0KYGBgDQoNCkNodeG7l2kgZOG7ryBsaeG7h3UgbMOgIDEgb2JqZWN0IGNo4bupYSBjw6FjIHRow7RuZyB0aW4gduG7gSBj4bqldSB0csO6YyBuaMawIMSR4buZIGTDoGkgPTEwLCBz4buRIHF1YW4gc8OhdCA9IDEwMCwgdMOqbiBjw6FjIG5ow6NuIGdpw6EgdHLhu4ssIHbDoCBwaOG7lSBtw6B1IMSR4buDIHbhur0gxJHhu5MgdGjhu4suDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc3VtbWFyeShHT0xEKQ0KYGBgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc3VtbWFyeShBQkNEX0NsYXNzKQ0KYGBgDQoNClRhIGPDsyB0aOG7gyB0aGF5IMSR4buVaSBwaOG7lSBtw6B1IGPhu6dhIG3hu5dpIG9iamVjdCDEkeG7gyDEkeG6oXQgaGnhu4d1IOG7qW5nIG3hu7kgdGh14bqtdCB0w7l5IMO9Og0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmF0dHIoR09MRCwgImNwYWwiKSA8LSBjKCcjZmZjMmM1JywNCiAgICAgICAgICAgICAgICAgICAgICAgICAnI2ZhNzU3YycsDQogICAgICAgICAgICAgICAgICAgICAgICAgJyNmYzMwM2EnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICcjOGEwMDA3JykNCg0KYXR0cihBQkNEX0NsYXNzLCAiY3BhbCIpIDwtIGMoJyM5OGM0MjknLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJyNmZmNlMDgnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJyNmZjg5MDMnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJyNmZjAzODUnKQ0KYGBgDQoNCkjDoG0gc3NwbG90IGNobyBwaMOpcCB24bq9IGPDoWMgYmnhu4N1IMSR4buTIHNhdSwgbmjhurFtIHTDs20gdOG6r3QgduG7gSBwaMOibiBi4buRIHbDoCBxdXkgbHXhuq10IGNodXnhu4NuIHRp4bq/cCBnaeG7r2EgY8OhYyB0cuG6oW5nIHRow6FpDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc3NwbG90KGxpc3QoIkdPTEQgU2V2ZXJpdHkiID0gR09MRCwgDQogICAgICAgICAgICAiQUJDRF9HcmFkZSIgPSBBQkNEX0NsYXNzKSwNCiAgICAgICBwbG90cyA9ICdvYnMnLA0KICAgICAgIHR5cGUgPSAnZCcpDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzc3Bsb3QobGlzdCgiR09MRCBTZXZlcml0eSIgPSBHT0xELCANCiAgICAgICAgICAgICJBQkNEX0dyYWRlIiA9IEFCQ0RfQ2xhc3MpLA0KICAgICAgIHBsb3RzID0gJ29icycsDQogICAgICAgc29ydC5jaGFubmVsID0gImZyb20uc3RhcnQiLA0KICAgICAgIHR5cGUgPSAnSScpDQpgYGANCg0KVGEgY8OzIHRo4buDIHRo4bqleSBy4bqxbmcg4bufIG3hu5dpIGLhu4duaCBuaMOibiAoaMOgbmcpLCDEkeG7mSBuxINuZyB2w6AgdHLhuqFuZyB0aMOhaSBsw6JtIHPDoG5nIGPhu6dhIGLhu4duaCBDT1BEIGtow7RuZyBkaeG7hW4gdGnhur9uIG3hu5l0IGPDoWNoIHR1eeG6v24gdMOtbmgsIG5oxrBuZyBjw7MgdGjhu4MgY2h1eeG7g24gdGnhur9wIG5nxrDhu6NjIHThu6sgbeG7mXQgdHLhuqFuZyB0aMOhaSBu4bq3bmcgaMahbiB24buBIHRy4bqhbmcgdGjDoWkgbmjhurkgaMahbjoNCg0KxJDhu4MgZOG7sW5nIG3DtCBow6xuaCBITU0gbXVsdGljaGFubmVsICjEkWEga8OqbmgpLCBjw6FjIGNodeG7l2kgWSDEkcaw4bujYyDEkcOzbmcgZ8OzaSB2w6BvIHRyb25nIDEgbGlzdCB2w6AgxJHGsGEgdsOgbyBow6BtIGJ1aWxkX2htbSwgc2F1IMSRw7MgdGEgeMOhYyDEkeG7i25oIHPhu5EgaGlkZGVuIHN0YXRlcyBtb25nIG114buRbiAoMTIpLCBy4buTaSBkw7luZyB0aeG6v3AgaMOgbSBmaXRfbW9kZWwgxJHhu4MgZOG7sW5nIG3DtCBow6xuaC4NCg0KVMO5eSBjaOG7iW5oIHRocmVhZHMgY2hvIHBow6lwIGNo4bqheSBxdXkgdHLDrG5oIHRyw6puIG5oaeG7gXUgQ1BVIMSR4buDIHTEg25nIHThu5FjIMSR4buZIHTDrW5oIHRvw6FuLCB0csOqbiBtw6F5IGPhu6dhIE5oaSBjw7MgOCBjb3JlcyBuw6puIE5oaSBzZXQgdGhyZWFkcyA9IDguDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpsaXN0KEdPTEQsIEFCQ0RfQ2xhc3MpICU+JQ0KICBidWlsZF9obW0ob2JzZXJ2YXRpb25zID0gLiwgDQogICAgICAgICAgICBuX3N0YXRlcyA9IDEyLA0KICAgICAgICAgICAgY2hhbm5lbF9uYW1lcyA9IGMoIkdPTEQiLCJDbGFzcyIpKSAlPiUgDQogIGZpdF9tb2RlbCguLCANCiAgICAgICAgICAgIGVtX3N0ZXAgPSBUUlVFLCANCiAgICAgICAgICAgIGdsb2JhbF9zdGVwID0gVFJVRSwNCiAgICAgICAgICAgIGxvY2FsX3N0ZXAgPSBUUlVFLA0KICAgICAgICAgICAgdGhyZWFkcyA9IDgpIC0+IG1jX2ZpdA0KYGBgDQoNClNhdSBraGkgbcO0IGjDrG5oIGThu7FuZyB4b25nLCBi4bqhbiBjw7MgdGjhu4MgZMO5bmcgaMOgbSBwbG90IMSR4buDIHbhur0gcmEgbeG6oW5nIGzGsOG7m2kgY2h1eeG7g24gdGnhur9wIGdp4buvYSBjw6FjIGhpZGRlbiBzdGF0ZXMsIGzDoCBr4bq/dCBxdeG6oyBj4bqnbiB0w6xtLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnBsb3QobWNfZml0JG1vZGVsLA0KICAgICBlZGdlLmFycm93LnNpemUgPSAwLjUsIA0KICAgICBpbnRlcmFjdGl2ZSA9IFRSVUUsIA0KICAgICBsb29wcyAgPSBUUlVFLCANCiAgICAgdmVydGV4LnNpemUgPSAxNSwNCiAgICAgZWRnZS5jdXJ2ZWQgPSBGQUxTRSwNCiAgICAgbGF5b3V0ID0gIGxheW91dC5zdGFyLCANCiAgICAgbGVnZW5kLnByb3AgPSAwLjEsDQogICAgIHRyaW0gPSAwLjAxLA0KICAgICB3aXRoLmxlZ2VuZCA9ICJyaWdodCIpDQpgYGANCg0KUGFja2FnZXMgSE1NIHbhur0gbeG6oW5nIGzGsOG7m2kgbsOgeSBi4bqxbmcgcGFja2FnZSBpZ3JhcGgsIG7Dqm4gYuG6oW4gY8OzIHRo4buDIGfhu41pIGLhuqV0IGPhu6kgxJHhu4tuaCBk4bqhbmcgbeG7uSB0aHXhuq10IG7DoG8gY2hvIG3DtCBow6xuaCBt4bqhbmcgdOG7qyBwYWNrYWdlIGlncmFwaCB0cm9uZyB0w7l5IGNo4buJbmggbGF5b3V0LiANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpwbG90KG1jX2ZpdCRtb2RlbCwNCiAgICAgZWRnZS5hcnJvdy5zaXplID0gMC41LCANCiAgICAgaW50ZXJhY3RpdmUgPSBUUlVFLCANCiAgICAgbG9vcHMgID0gVFJVRSwgDQogICAgIHZlcnRleC5zaXplID0gMTUsDQogICAgIGVkZ2UuY3VydmVkID0gVFJVRSwNCiAgICAgbGF5b3V0ID0gIGxheW91dC5mcnVjaHRlcm1hbi5yZWluZ29sZCwgDQogICAgIGxlZ2VuZC5wcm9wID0gMC4xLA0KICAgICB0cmltID0gMC4wMSwNCiAgICAgd2l0aC5sZWdlbmQgPSAicmlnaHQiKQ0KYGBgDQoNCkjDoG0gQklDIGNobyBwaMOpcCDEkcOhbmggZ2nDoSBwaOG6qW0gY2jhuqV0IG3DtCBow6xuaDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpCSUMobWNfZml0JG1vZGVsKQ0KYGBgDQoNCk7hu5lpIGR1bmcgYsOqbiB0cm9uZyBtw7QgaMOsbmggSE1NIGfhu5NtIG5o4buvbmcgbWEgdHLhuq1uIHhhYyBzdeG6pXQsIG5oxrA6IG1hdHJpeCBJbml0aWFsIHByb2JhYmlsaXRpZXMgbMOgIHjDoWMgc3XhuqV0IGJhbiDEkeG6p3UgY+G7p2EgbeG7l2kgaGlkZGVuIHN0YXRlICh04bqhaSB0aOG7nWkgxJFp4buDbSB0PTEpLCBtYXRyaXggVHJhbnNpdGlvbiBwcm9iYWJpbGl0aWVzIGNo4bupYSB4w6FjIHN14bqldCBjaHV54buDbiB0aeG6v3AgZ2nhu69hIGPDoWMgaGlkZGVuIHN0YXRlcyAodOG7qyDEkcOzIGJp4buDdSDEkeG7kyBt4bqhbmcgxJHGsOG7o2MgduG6vSByYSB0cm9uZyBpZ3JhcGgsIHbDoCBi4bqhbiBjxaluZyBjw7MgdGjhu4MgduG6vSB0aOG7pyBjw7RuZyBi4bqxbmcgZ2dyYXBoKSwgU2F1IGPDuW5nIGzDoCBFbWlzc2lvbiBwcm9iYWJpbGl0aWVzIGNobyBt4buXaSBjaHXhu5dpIMSR4bqndSByYQ0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCm1jX2ZpdCRtb2RlbA0KYGBgDQoNCiMgS+G6v3QgbHXhuq1uOiANCg0KUXVhIHRow60gZOG7pSBtw7QgcGjhu49uZyBuw6B5LCBjw6FjIGLhuqFuIMSRw6MgbMOgbSBxdWVuIHbhu5tpIG3hu5l0IHPhu5EgdMOtbmggbsSDbmcgY8ahIGLhuqNuIGPhu6dhIHBhY2thZ2Ugc2VxSE1NIMSR4buDIGThu7FuZyBtw7QgaMOsbmggTWFya292IOG6qW4gxJFhIGvDqm5oLiDEkMOieSBsw6AgbeG7mXQgcGjGsMahbmcgcGjDoXAgY8OzIHRp4buBbSBuxINuZyDhu6luZyBk4bulbmcgcuG7mW5nIGzhu5tuLCBjaOG7qSBraMO0bmcgY2jhu4kgZMOgbmggY2hvIGLDoGkgdG/DoW4gcGjDom4gdMOtY2ggY2h14buXaSBnacOhIHRy4buLIGNhdGVnb3JpY2FsLiBQYWNrYWdlIHNlcUhNTSBjw7JuIGNobyBwaMOpcCBi4bqhbiDEkcawYSB2w6BvIG3DtCBow6xuaCBjw6FjIGhp4buHcCBiaeG6v24gKGNvdmFyaWF0ZXMpIHRow60gZOG7pSB0deG7lWksIGdp4bubaSB0w61uaCwgLi4uIGPhu6dhIGLhu4duaCBuaMOibiDEkeG7gyB0aOG7sWMgaGnhu4duIG3hu5l0IHBow6JuIHTDrWNoIENsdXN0ZXJpbmcga+G6v3QgaOG7o3AgbcO0IGjDrG5oIE1hcmtvdi5OaMawIHbhuq15IGLhuqFuIMSRw6MgY8OzIHRyb25nIHRheSBt4buZdCBjw7RuZyBj4bulIMSR4buZYyDEkcOhbyB0cm9uZyBuZ2hpw6puIGPhu6l1IHRyxrDhu51uZyBkaeG7hW4gaG/hurdjIGPDoWMgYsOgaSB0b8OhbiBsacOqbiBxdWFuIMSR4bq/biBk4buvIGxp4buHdSBjaHXhu5dpLg==