1 Giới thiệu

Chuỗi thời gian (time series), là một cấu trúc dữ liệu gồm nhiều giá trị liên tiếp theo trình tự thời gian. Một cách phổ quát hơn, Nhi thích sử dụng thuật ngữ “chuỗi dữ liệu” (sequence, sequential data) cho bất cứ dữ liệu nào có cấu trúc liên tục và theo trình tự, không bắt buộc phải là những con số và phải có yếu tố thời gian biểu kiến (thí dụ chuỗi sự kiện/trạng thái, văn bản, gen, protein, … ).

Trong các bài toán Machine learning, dữ liệu chuỗi có thể được hoán chuyển rất đa dạng ở đầu vào, theo 2 trường phái khác nhau: hoặc trực tiếp sử dụng dữ liệu thô (arrays/tensor từ 1D cho đến 5D) - cách làm này thường gặp trong deep learning (CNN, LSTM, …) hoặc gián tiếp bằng cách trích xuất các features từ dữ liệu gốc rồi dùng các features này như input data cho những algorithm như SVM, XGBoost hay Random Forest. Bài thực hành hôm nay sẽ bàn về trường phái thứ 2, trích xuất features, với nội dung cụ thể là giới thiệu về R package tsfeatures. Công cụ này cho phép thực hiện rất nhiều quy trình trích xuất features - vốn trước kia chỉ có thể làm bằng Matlab.

Bạn có thể cài đặt bản chính thức từ CRAN như sau:

# Cài đặt từ CRAN
install.packages('tsfeatures', dependencies = TRUE)

Bạn cũng có thể cài bản thử nghiệm từ Github như sau:

# Cần package "devtools"
devtools::install_github("robjhyndman/tsfeatures")

2 Một vài thí dụ về time series

Sau đây là một số dataset kinh điển giúp các bạn hình dung về time series,

library(tsfeatures)
library(tidyverse)
  1. Data sunpost : timeseries có yếu tố thời gian biểu kiến (mốc thời gian là năm, tháng, ngày, giờ xác định)
annual_sunpost = read.csv("http://vincentarelbundock.github.io/Rdatasets/csv/datasets/sunspot.year.csv",
                          row.names = 1)%>%
  ts(data=.$value,
     start=min(.$time),
     end=max(.$time),
     frequency=1)

head(annual_sunpost,20)
## Time Series:
## Start = 1700 
## End = 1719 
## Frequency = 1 
##  [1]  5 11 16 23 36 58 29 20 10  8  3  0  0  2 11 27 47 63 60 39
plot(annual_sunpost,type="l",col="red")

  1. Dữ liệu AirPassenger: là một thí dụ về time series có yếu tố chu kỳ
airpass = read.csv("http://vincentarelbundock.github.io/Rdatasets/csv/datasets/AirPassengers.csv",
                   row.names = 1)%>%
  ts(data=.$value,
      class="ts",
      start=1949,end=1960,
      frequency=12)

airpass
##      Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
## 1949 112 118 132 129 121 135 148 148 136 119 104 118
## 1950 115 126 141 135 125 149 170 170 158 133 114 140
## 1951 145 150 178 163 172 178 199 199 184 162 146 166
## 1952 171 180 193 181 183 218 230 242 209 191 172 194
## 1953 196 196 236 235 229 243 264 272 237 211 180 201
## 1954 204 188 235 227 234 264 302 293 259 229 203 229
## 1955 242 233 267 269 270 315 364 347 312 274 237 278
## 1956 284 277 317 313 318 374 413 405 355 306 271 306
## 1957 315 301 356 348 355 422 465 467 404 347 305 336
## 1958 340 318 362 348 363 435 491 505 404 359 310 337
## 1959 360 342 406 396 420 472 548 559 463 407 362 405
## 1960 417
plot(airpass,type="l",col="blue")

  1. Một đoạn tín hiệu điện tim:

Đây là thí dụ về timeseries không có mốc thời gian biểu kiến (có thể chỉ đơn giản là 1 vector số)

ecg = read.csv("ecg0606_1.csv",header = F)%>%as.ts(.$V1)

plot(ecg,type="l",col="black")

  1. Dữ liệu chuỗi đa chiều:

8 băng Tín hiệu cảm biến gyro và gia tốc ghi nhận tư thế, chuyển động của cơ thể trong 5 trạng thái giấc ngủ khác nhau: có thể xem như một tensor 3D.

sleep_df = read.csv("origin_comp.csv")

Awk_df = sleep_df%>%filter(Stade == "Awk")%>%.[c(1:100),]
N1_df = sleep_df%>%filter(Stade == "N1")%>%.[c(1:100),]
N2_df = sleep_df%>%filter(Stade == "N2")%>%.[c(1:100),]
N3_df = sleep_df%>%filter(Stade == "N3")%>%.[c(1:100),]
Rem_df = sleep_df%>%filter(Stade == "REM")%>%.[c(1:100),]

df=rbind(Awk_df,N1_df,N2_df,N3_df,Rem_df)%>%select(-idx)

df$idx = rep(c(1:100),5)

df%>%gather(1:8,key="band",value="amplitude")%>%
  ggplot(aes(x=idx,y=amplitude,col=band))+
  geom_line(alpha=0.6,size=1)+
  theme_bw(5)+
  facet_wrap(~Stade, ncol=1, scales="free")

df%>%gather(1:8,key="band",value="amplitude")%>%
  ggplot(aes(x=idx,y=amplitude,col=band))+
  geom_line(alpha=0.6,size=0.8)+
  theme_bw(8)+
  facet_grid(band~Stade, scales="free_y")

Hình vẽ trên chỉ mới trình bày 1 phân đoạn ngắn (10 giây tín hiệu với tần số lấy mẫu 10 Hz),khi một chuỗi dài được chia đều thành nhiều phân đoạn, nó sẽ trở thành một tensor/array 4D, thậm chí 5D, bao gồm chiều thời gian. Giả định nếu muốn phân biệt 5 trạng thái giấc ngủ khác nhau (bài toán multiclass classification), ta khó lòng sử dụng trực tiếp dữ liệu gốc, ngay cả bằng mô tả trực quan.Đây là một không gian dữ liệu quá lớn, nhiều thông tin tiềm ẩn bên trong và khó phân tích. Kỹ thuật trích xuất features có thể rút gọn chiều dữ liệu xuống bằng cách sử dụng các chỉ số cho phép mô tả về đặc tính, khuynh hướng của chuỗi dữ liệu gốc, package tsfeatures cung cấp một số hàm tiện lợi để làm việc này.

3 Trích xuất feature cho 1 chuỗi bằng hàm chuyên dụng

về mặt kỹ thuật, mỗi chuỗi dữ liệu cơ bản là một vector (1D array) nên ta có thể áp dụng tất cả những hàm thông thường, bao gồm thống kê, tính toán số học, hoán chuyển, ..Nếu hàm này xuất ra kết quả là một con số, kết quả này có thể được xem như một feature,

Thí dụ đơn giản nhất, hàm median cho biết vị trí trung tâm của chuỗi:

median(annual_sunpost)
## [1] 39
ggplot(data=annual_sunpost)+
  geom_point(aes(y=annual_sunpost,x=c(1700:1988)),col="red",alpha=0.5)+
  geom_line(aes(y=annual_sunpost,x=c(1700:1988)),col="grey",alpha=0.5)+
  geom_hline(yintercept = median(annual_sunpost), color = "blue", linetype=2)+
  scale_x_continuous('Year')+
  theme_bw()

Package tsfeature cung cấp khoảng 30 hàm khác nhau tương ứng với nhiều thuộc tính chuyên biệt cho time series,

https://cran.r-project.org/web/packages/tsfeatures/vignettes/tsfeatures.html#stl_features

Thí dụ, hàm acf_features cung cấp hệ số tự tương quan và tổng bình phương của 10 autocorrelation coefficients đầu tiên

acf_features(ecg)%>%knitr::kable()
x
x_acf1 0.9690683
x_acf10 2.9172409
diff1_acf1 0.9187728
diff1_acf10 2.5582842
diff2_acf1 0.7313610
diff2_acf10 1.6636852

Hàm stl_features trích xuất 8 thuộc tính về trend và seasonality của time series dựa vào phân tích STL decomposition.

stl_features(ecg)%>%knitr::kable()
x
nperiods 0.0000000
seasonal_period 1.0000000
trend 0.0043295
spike 0.0000000
linearity 0.4771984
curvature -0.3072355
e_acf1 0.9690477
e_acf10 2.9124859

Hàm heterogeneity trích xuất 4 thuộc tính nhằm khảo sát heterogeneity dựa vào tổng bình phương và R2 của mô hình GARCH và ARCH:

heterogeneity (airpass)%>%knitr::kable()
x
arch_acf 0.2294789
garch_acf 0.2278266
arch_r2 0.1891338
garch_r2 0.1894199

4 Trích xuất features cho hàng loạt chuỗi

Hàm tsfeatures cho phép áp dụng một hay nhiều phương pháp trích xuất features khác nhau cho 1 list gồm nhiều chuỗi khác nhau. Nó làm đơn giản code mà không cần đến vòng lặp, hàm apply hay hàm map.

Trước hết ta đưa các chuỗi cần xử lý vào 1 list (có thể dùng hàm split trên 1 dataframe), thí dụ ta muốn xử lý 3 chuỗi anorm_raw ở 3 trạng thái Awk, N2 và REM :

ts_list = list(Awk_df$anorm_raw,
               N2_df$anorm_raw,
               Rem_df$anorm_raw)

str(ts_list)
## List of 3
##  $ : num [1:100] 1.02 1.02 1.02 1.02 1.02 ...
##  $ : num [1:100] 1.01 1.01 1.01 1.01 1.01 ...
##  $ : num [1:100] 1.01 1.02 1.02 1.01 1.01 ...

Có thể áp dụng 1 hay hàng loạt phương pháp, tên của phương pháp là tên của hàm có trong Environment, có thể là 1 hàm có sẵn trong package hay 1 hàm đặc biệt do bạn tạo ra:

Thí dụ quy trình sau trích xuất các features: entropy, stability, nonlinearity, lumpiness, crossing_points cho list 3 series:

tsfeatures(ts_list, features = c("entropy","stability","nonlinearity", 
                                 "lumpiness", "crossing_points"))%>%knitr::kable()
entropy stability nonlinearity lumpiness crossing_points
0.9866066 0.3323816 1.1801256 0.4820703 53
0.9516680 0.0166262 0.3401689 0.3215552 62
0.8107511 0.0340071 0.1445946 0.2417979 78

Kết quả xuất ra là 1 data frame, mỗi hàng tương ứng 1 series trong list, mỗi cột là 1 features. Nếu hàm xuất ra nhiều features, chúng sẽ được ghép nối tiếp theo nhiều cột

Như đã nói, ta có thể viết hàm tùy thích, thí dụ Nhi tạo ra 1 hàm có tên là boxcox_stl, nó sẽ làm 1 quy trình phức tạp gồm: chuẩn hóa (scaling) chuỗi thành thang đo 0:1, sau đó hoán chuyển BoxCox, cuối cùng áp dụng phương pháp STL để xuất ra 8 features:

boxcox_stl <- function(x,...) {
  minmax = (x- min(x)) /(max(x)-min(x))
  lambda <- forecast::BoxCox.lambda(minmax)
  y <- forecast::BoxCox(minmax, lambda)
  c(stl_features(y, s.window='periodic', robust=TRUE), lambda=lambda)
}

tsfeatures(ts_list, features = 'boxcox_stl')%>%knitr::kable()
nperiods seasonal_period trend spike linearity curvature e_acf1 e_acf10 lambda
0 1 0.2707876 1e-07 0.4579602 0.3788559 -0.0796860 0.0908502 1.077253
0 1 0.0000000 2e-07 0.0668875 -0.0161874 -0.3999478 0.3523617 1.018802
0 1 0.0122354 1e-07 0.0182492 0.1391286 -0.6922963 1.7365545 1.421700

5 Trích xuất features cho dữ liệu chuỗi đa chiều

Ta sẽ làm 1 quy trình phức tạp hơn, thí dụ ta có dataframe sleep_df gồm 8 băng tín hiệu, ghi trong 5 trạng thái giấc ngủ khác nhau, mỗi trạng thái lại được chia thành 10 phân đoạn khác nhau có độ dài 100 mẫu (10 giây, 10Hz), cấu trúc này tương ứng với 1 tensor/array 4D. Ta muốn trích xuất entropy của từng phân đoạn

sleep_df$idx = rep(c(1:50),each = 100)

sleep_df%>%sample_n(10)%>%knitr::kable()
anorm_raw ax_raw ay_raw az_raw gnorm_raw gx_raw gy_raw gz_raw idx Stade
1.009713 -0.7873840 0.6317135 0.0219420 0.8806421 -0.7765406 0.2447864 0.3354096 28 N2
1.012446 -0.7616880 0.6274110 -0.2263485 0.3145053 -0.1477296 0.2104879 0.1810646 45 REM
1.014557 -0.7628377 0.6295167 -0.2260740 0.1365469 -0.0245079 0.0478859 0.0807093 43 REM
1.009033 -0.7902835 0.6268615 0.0253600 1.4076002 -0.8641931 0.4715389 0.9966136 24 N2
1.022717 0.0953163 0.6772870 -0.7603557 1.0109192 -0.4437156 -0.3700511 -0.3308760 10 Awk
1.014243 -0.7630310 0.6286010 -0.2265625 0.2718565 -0.1934616 -0.1610826 0.0190981 46 REM
1.013054 -0.7928260 0.6295577 0.0367433 0.8897557 0.7910407 -0.2252344 -0.0259980 26 N2
1.011761 -0.7847290 0.6381230 0.0256960 1.3894277 -0.9327906 0.6944809 0.7603336 25 N2
1.012252 -0.7820437 0.6424767 0.0168457 1.0432809 0.5661931 0.5255283 -0.6980000 34 N3
1.011412 -0.7983400 0.6207885 0.0150755 0.6220690 0.5096639 -0.0982016 -0.3315114 13 N1
sleep_df%>%group_by(idx,Stade)%>%tally()%>%knitr::kable()
idx Stade n
1 Awk 100
2 Awk 100
3 Awk 100
4 Awk 100
5 Awk 100
6 Awk 100
7 Awk 100
8 Awk 100
9 Awk 100
10 Awk 100
11 N1 100
12 N1 100
13 N1 100
14 N1 100
15 N1 100
16 N1 100
17 N1 100
18 N1 100
19 N1 100
20 N1 100
21 N2 100
22 N2 100
23 N2 100
24 N2 100
25 N2 100
26 N2 100
27 N2 100
28 N2 100
29 N2 100
30 N2 100
31 N3 100
32 N3 100
33 N3 100
34 N3 100
35 N3 100
36 N3 100
37 N3 100
38 N3 100
39 N3 100
40 N3 100
41 REM 100
42 REM 100
43 REM 100
44 REM 100
45 REM 100
46 REM 100
47 REM 100
48 REM 100
49 REM 100
50 REM 100

Để làm việc này, dự kiến ta phải dùng hàm map và 1 hàm cải biên để làm việc trên list.

Ta tạo ra hàm ent_func, nó sẽ chuyển dataframe nhỏ gồm 8 băng dữ liệu thành 1 list, sau đó áp dụng hàm ts_features (hay trực tiếp hàm entropy cũng được) để trích xuất kết quả entropy cho mỗi băng dữ liệu.

ent_func = function(.x){
  ts_list = as.list(.x[,-c(9,10)])
  out = tsfeatures(ts_list,
             features="entropy")%>%as.vector()
  return(out)
 }

Tiếp theo, ta dùng hàm map_df để chạy hàm ent_func cho từng phân đoạn từ 1 đến 50 (có 50 phân đoạn, mỗi nhóm label có 10 phân đoạn và mỗi phân đoạn dài 100 mẫu) và ghi kết quả trong dataframe out_df

sleep_df%>%split(.$idx)%>%
  map_df(.,~data_frame(ent=ent_func(.x)%>%.$entropy,
                       para = colnames(df[,-c(9,10)]),
                       stade=.x$Stade[[1]]))->out_df
out_df$idx = factor(c(1:50))

out_df%>%sample_n(10)%>%knitr::kable()
ent para stade idx
0.9112369 gx_raw N1 42
0.8880870 gy_raw N2 49
0.9242151 gx_raw N2 6
0.8515206 gz_raw N2 18
0.9102416 az_raw REM 48
0.6979388 ax_raw N2 18
0.9169486 ax_raw N1 22
0.8572580 gx_raw N1 10
0.8757794 ax_raw REM 44
0.8719307 gy_raw N2 15

Lúc này ta có thể dùng entropy như 1 feature để phân biệt 5 label sleep stades:

out_df%>%ggplot()+
  geom_density(aes(x=ent,fill=stade),alpha=0.7)+
  theme_bw(8)+
  facet_wrap(~para,ncol=3,scales = "free")

out_df%>%ggplot()+
  geom_boxplot(aes(x=stade,y=ent,fill=stade),alpha=0.7)+
  theme_bw(8)+
  coord_flip()+
  facet_wrap(~para,ncol=2,scales = "free")

Thay vì xuất 1 features duy nhất là entropy, ta có thể xuất nhiều features hơn, như sau đây - Nhi dùng quy trình stl_features để xuất ra 8 features, sau đó chọn ra 4 là trend,spike,linearity và curvature. Lưu ý, hàm stl_func xuất kết quả là 1 dataframe thay vì vector như hàm ent_func

stl_func = function(.x){
  ts_list = as.list(.x[,-c(9,10)])
  out = tsfeatures(ts_list,
             features="stl_features")
  return(out)
 }

Kết hợp hàm split và map_df, ta có thể chạy áp dụng hàm stl_func vừa tạo cho hàng loạt phân đoạn, và lưu kết quả lại thành dataframe out_df2

sleep_df%>%split(.$idx)%>%
  map_df(.,~stl_func(.x)%>%
           select(trend,spike,linearity,curvature))->out_df2
out_df2%>%sample_n(10)%>%knitr::kable()
trend spike linearity curvature
0.2821990 0.0000906 -1.0794391 1.8113425
0.0584019 0.0001293 1.1788674 -0.1435161
0.4926281 0.0001104 -0.3078516 -1.1142797
0.3645284 0.0000748 2.3945986 -1.9783793
0.8308025 0.0000071 -0.7858780 0.7821673
0.6655960 0.0000375 5.5228314 -3.0745624
0.7475569 0.0000328 1.9326001 3.3317111
0.0084289 0.0002730 -0.0255262 -0.6787094
0.3947238 0.0008709 0.0167956 0.8797239
0.4312240 0.0000725 0.4181652 0.5867820

Ta ghép 2 dataframe out_df và out_df2 lại thành 1, lúc này ta có 5 features trong 1 dataframe

stl_out_df = bind_cols(out_df2,out_df)

dataframe này có thể được dùng như input data cho bài toán classfication, hoặc unsupervised learning.

Ta có thể thăm dò trực quan đặc tính của dữ liệu features bằng heatmap:

library(viridis)

stl_out_df%>%
  dplyr::select(-c(para,stade,idx))%>%
  scale(.)%>%
  as_tibble()%>%
  mutate(Stade=out_df$stade,
         Id=factor(rep(c(1:5),80)),
         para=out_df$para)%>%
  gather(trend:ent,
         key="Feature",value="Value")%>%
  ggplot(aes(x=reorder(Id,Value),
             y=reorder(Feature,Value),
             fill=Value))+
  geom_tile(show.legend=T)+
  theme_bw(7)+
  theme(axis.text.y=element_blank(),
        axis.text.x = element_text(angle =45,hjust=1,vjust=1))+
  coord_flip()+
  scale_y_discrete("Features")+
  scale_x_discrete("Fragments",breaks=NULL)+
  facet_grid(Stade~para,shrink = T)+
  scale_fill_viridis(option="A",begin=1,end=0)

Hoặc bằng violin plot

stl_out_df%>%
  dplyr::select(-c(para,stade,idx))%>%
  scale(.)%>%
  as_tibble()%>%
  mutate(stade=out_df$stade,
         para=out_df$para)%>%
  gather(1:5,key="Feature",value="Value")%>%
  ggplot()+
  geom_violin(aes(x=stade,y=Value,fill=stade),alpha=0.5)+
  facet_grid(Feature~para)+
  theme_bw(7)+
  theme(axis.text.y=element_blank(),
        axis.text.x = element_text(angle =45,hjust=1,vjust=1))

6 Tổng kết

Bài thực hành đến đây là hết, Nhi hy vọng package tsfeature sẽ mang lại nhiều tiên ích cho các bạn nghiên cứu sinh khi làm việc với dữ liệu chuỗi. Chúc các bạn thực hành vui và hẹn gặp lại.

LS0tDQp0aXRsZTogIlRyw61jaCB4deG6pXQgZmVhdHVyZSBk4buvIGxp4buHdSBjaHXhu5dpIiANCmF1dGhvcjogIkzDqiBOZ+G7jWMgS2jhuqMgTmhpIg0KZGF0ZTogImByIFN5cy5EYXRlKClgIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImRlZmF1bHQiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQogICAgZGV2OiAnc3ZnJw0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQoNCmtuaXRyOjpvcHRzX2NodW5rJHNldCgNCiAgZWNobz1UUlVFLA0KICB3YXJuaW5nID0gRkFMU0UsDQogIGZpZy5hbGlnbiA9ICJjZW50ZXIiDQopDQoNCmBgYA0KDQohW10oY2hyb25vcy5wbmcpDQoNCiMgR2nhu5tpIHRoaeG7h3UgDQoNCkNodeG7l2kgdGjhu51pIGdpYW4gKHRpbWUgc2VyaWVzKSwgbMOgIG3hu5l0IGPhuqV1IHRyw7pjIGThu68gbGnhu4d1IGfhu5NtIG5oaeG7gXUgZ2nDoSB0cuG7iyBsacOqbiB0aeG6v3AgdGhlbyB0csOsbmggdOG7sSB0aOG7nWkgZ2lhbi4gTeG7mXQgY8OhY2ggcGjhu5UgcXXDoXQgaMahbiwgTmhpIHRow61jaCBz4butIGThu6VuZyB0aHXhuq10IG5n4buvICJjaHXhu5dpIGThu68gbGnhu4d1IiAoc2VxdWVuY2UsIHNlcXVlbnRpYWwgZGF0YSkgY2hvIGLhuqV0IGPhu6kgZOG7ryBsaeG7h3UgbsOgbyBjw7MgY+G6pXUgdHLDumMgbGnDqm4gdOG7pWMgdsOgIHRoZW8gdHLDrG5oIHThu7EsIGtow7RuZyBi4bqvdCBideG7mWMgcGjhuqNpIGzDoCBuaOG7r25nIGNvbiBz4buRIHbDoCBwaOG6o2kgY8OzIHnhur91IHThu5EgdGjhu51pIGdpYW4gYmnhu4N1IGtp4bq/biAodGjDrSBk4bulIGNodeG7l2kgc+G7sSBraeG7h24vdHLhuqFuZyB0aMOhaSwgdsSDbiBi4bqjbiwgZ2VuLCBwcm90ZWluLCAuLi4gKS4gDQoNClRyb25nIGPDoWMgYsOgaSB0b8OhbiBNYWNoaW5lIGxlYXJuaW5nLCBk4buvIGxp4buHdSBjaHXhu5dpIGPDsyB0aOG7gyDEkcaw4bujYyBob8OhbiBjaHV54buDbiBy4bqldCDEkWEgZOG6oW5nIOG7nyDEkeG6p3UgdsOgbywgdGhlbyAyIHRyxrDhu51uZyBwaMOhaSBraMOhYyBuaGF1OiBob+G6t2MgdHLhu7FjIHRp4bq/cCBz4butIGThu6VuZyBk4buvIGxp4buHdSB0aMO0IChhcnJheXMvdGVuc29yIHThu6sgMUQgY2hvIMSR4bq/biA1RCkgLSBjw6FjaCBsw6BtIG7DoHkgdGjGsOG7nW5nIGfhurdwIHRyb25nIGRlZXAgbGVhcm5pbmcgKENOTiwgTFNUTSwgLi4uKSBob+G6t2MgZ2nDoW4gdGnhur9wIGLhurFuZyBjw6FjaCB0csOtY2ggeHXhuqV0IGPDoWMgZmVhdHVyZXMgdOG7qyBk4buvIGxp4buHdSBn4buRYyBy4buTaSBkw7luZyBjw6FjIGZlYXR1cmVzIG7DoHkgbmjGsCBpbnB1dCBkYXRhIGNobyBuaOG7r25nIGFsZ29yaXRobSBuaMawIFNWTSwgWEdCb29zdCBoYXkgUmFuZG9tIEZvcmVzdC4gQsOgaSB0aOG7sWMgaMOgbmggaMO0bSBuYXkgc+G6vSBiw6BuIHbhu4EgdHLGsOG7nW5nIHBow6FpIHRo4bupIDIsIHRyw61jaCB4deG6pXQgZmVhdHVyZXMsIHbhu5tpIG7hu5lpIGR1bmcgY+G7pSB0aOG7gyBsw6AgZ2nhu5tpIHRoaeG7h3UgduG7gSBSIHBhY2thZ2UgdHNmZWF0dXJlcy4gQ8O0bmcgY+G7pSBuw6B5IGNobyBwaMOpcCB0aOG7sWMgaGnhu4duIHLhuqV0IG5oaeG7gXUgcXV5IHRyw6xuaCB0csOtY2ggeHXhuqV0IGZlYXR1cmVzIC0gduG7kW4gdHLGsOG7m2Mga2lhIGNo4buJIGPDsyB0aOG7gyBsw6BtIGLhurFuZyBNYXRsYWIuDQoNCkLhuqFuIGPDsyB0aOG7gyBjw6BpIMSR4bq3dCBi4bqjbiAqKmNow61uaCB0aOG7qWMqKiB04burIFtDUkFOXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvdHNmZWF0dXJlcykgbmjGsCBzYXU6DQoNCg0KYGBge3IsIGV2YWwgPSBGQUxTRX0NCiMgQ8OgaSDEkeG6t3QgdOG7qyBDUkFODQppbnN0YWxsLnBhY2thZ2VzKCd0c2ZlYXR1cmVzJywgZGVwZW5kZW5jaWVzID0gVFJVRSkNCmBgYA0KDQpC4bqhbiBjxaluZyBjw7MgdGjhu4MgY8OgaSBi4bqjbiAqKnRo4butIG5naGnhu4dtKiogdOG7qyBbR2l0aHViXShodHRwczovL2dpdGh1Yi5jb20vcm9iamh5bmRtYW4vdHNmZWF0dXJlcykgbmjGsCBzYXU6DQoNCmBgYHtyLCBldmFsID0gRkFMU0V9DQojIEPhuqduIHBhY2thZ2UgImRldnRvb2xzIg0KZGV2dG9vbHM6Omluc3RhbGxfZ2l0aHViKCJyb2JqaHluZG1hbi90c2ZlYXR1cmVzIikNCmBgYA0KDQojIE3hu5l0IHbDoGkgdGjDrSBk4bulIHbhu4EgdGltZSBzZXJpZXMNCg0KU2F1IMSRw6J5IGzDoCBt4buZdCBz4buRIGRhdGFzZXQga2luaCDEkWnhu4NuIGdpw7pwIGPDoWMgYuG6oW4gaMOsbmggZHVuZyB24buBIHRpbWUgc2VyaWVzLCANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRzZmVhdHVyZXMpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmBgYA0KDQoxKSBEYXRhIHN1bnBvc3QgOiB0aW1lc2VyaWVzIGPDsyB54bq/dSB04buRIHRo4budaSBnaWFuIGJp4buDdSBraeG6v24gKG3hu5FjIHRo4budaSBnaWFuIGzDoCBuxINtLCB0aMOhbmcsIG5nw6B5LCBnaeG7nSB4w6FjIMSR4buLbmgpDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KYW5udWFsX3N1bnBvc3QgPSByZWFkLmNzdigiaHR0cDovL3ZpbmNlbnRhcmVsYnVuZG9jay5naXRodWIuaW8vUmRhdGFzZXRzL2Nzdi9kYXRhc2V0cy9zdW5zcG90LnllYXIuY3N2IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgcm93Lm5hbWVzID0gMSklPiUNCiAgdHMoZGF0YT0uJHZhbHVlLA0KICAgICBzdGFydD1taW4oLiR0aW1lKSwNCiAgICAgZW5kPW1heCguJHRpbWUpLA0KICAgICBmcmVxdWVuY3k9MSkNCg0KaGVhZChhbm51YWxfc3VucG9zdCwyMCkNCg0KcGxvdChhbm51YWxfc3VucG9zdCx0eXBlPSJsIixjb2w9InJlZCIpDQpgYGANCg0KMikgROG7ryBsaeG7h3UgQWlyUGFzc2VuZ2VyOiBsw6AgbeG7mXQgdGjDrSBk4bulIHbhu4EgdGltZSBzZXJpZXMgY8OzIHnhur91IHThu5EgY2h1IGvhu7MNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQphaXJwYXNzID0gcmVhZC5jc3YoImh0dHA6Ly92aW5jZW50YXJlbGJ1bmRvY2suZ2l0aHViLmlvL1JkYXRhc2V0cy9jc3YvZGF0YXNldHMvQWlyUGFzc2VuZ2Vycy5jc3YiLA0KICAgICAgICAgICAgICAgICAgIHJvdy5uYW1lcyA9IDEpJT4lDQogIHRzKGRhdGE9LiR2YWx1ZSwNCiAgICAgIGNsYXNzPSJ0cyIsDQogICAgICBzdGFydD0xOTQ5LGVuZD0xOTYwLA0KICAgICAgZnJlcXVlbmN5PTEyKQ0KDQphaXJwYXNzDQogIA0KcGxvdChhaXJwYXNzLHR5cGU9ImwiLGNvbD0iYmx1ZSIpDQpgYGANCg0KMykgTeG7mXQgxJFv4bqhbiB0w61uIGhp4buHdSDEkWnhu4duIHRpbToNCg0KxJDDonkgbMOgIHRow60gZOG7pSB24buBIHRpbWVzZXJpZXMga2jDtG5nIGPDsyBt4buRYyB0aOG7nWkgZ2lhbiBiaeG7g3Uga2nhur9uIChjw7MgdGjhu4MgY2jhu4kgxJHGoW4gZ2nhuqNuIGzDoCAxIHZlY3RvciBz4buRKQ0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmVjZyA9IHJlYWQuY3N2KCJlY2cwNjA2XzEuY3N2IixoZWFkZXIgPSBGKSU+JWFzLnRzKC4kVjEpDQoNCnBsb3QoZWNnLHR5cGU9ImwiLGNvbD0iYmxhY2siKQ0KYGBgDQoNCjQpIEThu68gbGnhu4d1IGNodeG7l2kgxJFhIGNoaeG7gXU6DQoNCjggYsSDbmcgVMOtbiBoaeG7h3UgY+G6o20gYmnhur9uIGd5cm8gdsOgIGdpYSB04buRYyBnaGkgbmjhuq1uIHTGsCB0aOG6vywgY2h1eeG7g24gxJHhu5luZyBj4bunYSBjxqEgdGjhu4MgdHJvbmcgNSB0cuG6oW5nIHRow6FpIGdp4bqlYyBuZ+G7pyBraMOhYyBuaGF1OiBjw7MgdGjhu4MgeGVtIG5oxrAgbeG7mXQgdGVuc29yIDNELiANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzbGVlcF9kZiA9IHJlYWQuY3N2KCJvcmlnaW5fY29tcC5jc3YiKQ0KDQpBd2tfZGYgPSBzbGVlcF9kZiU+JWZpbHRlcihTdGFkZSA9PSAiQXdrIiklPiUuW2MoMToxMDApLF0NCk4xX2RmID0gc2xlZXBfZGYlPiVmaWx0ZXIoU3RhZGUgPT0gIk4xIiklPiUuW2MoMToxMDApLF0NCk4yX2RmID0gc2xlZXBfZGYlPiVmaWx0ZXIoU3RhZGUgPT0gIk4yIiklPiUuW2MoMToxMDApLF0NCk4zX2RmID0gc2xlZXBfZGYlPiVmaWx0ZXIoU3RhZGUgPT0gIk4zIiklPiUuW2MoMToxMDApLF0NClJlbV9kZiA9IHNsZWVwX2RmJT4lZmlsdGVyKFN0YWRlID09ICJSRU0iKSU+JS5bYygxOjEwMCksXQ0KDQpkZj1yYmluZChBd2tfZGYsTjFfZGYsTjJfZGYsTjNfZGYsUmVtX2RmKSU+JXNlbGVjdCgtaWR4KQ0KDQpkZiRpZHggPSByZXAoYygxOjEwMCksNSkNCg0KZGYlPiVnYXRoZXIoMTo4LGtleT0iYmFuZCIsdmFsdWU9ImFtcGxpdHVkZSIpJT4lDQogIGdncGxvdChhZXMoeD1pZHgseT1hbXBsaXR1ZGUsY29sPWJhbmQpKSsNCiAgZ2VvbV9saW5lKGFscGhhPTAuNixzaXplPTEpKw0KICB0aGVtZV9idyg1KSsNCiAgZmFjZXRfd3JhcCh+U3RhZGUsIG5jb2w9MSwgc2NhbGVzPSJmcmVlIikNCiAgDQpkZiU+JWdhdGhlcigxOjgsa2V5PSJiYW5kIix2YWx1ZT0iYW1wbGl0dWRlIiklPiUNCiAgZ2dwbG90KGFlcyh4PWlkeCx5PWFtcGxpdHVkZSxjb2w9YmFuZCkpKw0KICBnZW9tX2xpbmUoYWxwaGE9MC42LHNpemU9MC44KSsNCiAgdGhlbWVfYncoOCkrDQogIGZhY2V0X2dyaWQoYmFuZH5TdGFkZSwgc2NhbGVzPSJmcmVlX3kiKQ0KYGBgDQoNCkjDrG5oIHbhur0gdHLDqm4gY2jhu4kgbeG7m2kgdHLDrG5oIGLDoHkgMSBwaMOibiDEkW/huqFuIG5n4bqvbiAoMTAgZ2nDonkgdMOtbiBoaeG7h3UgduG7m2kgdOG6p24gc+G7kSBs4bqleSBt4bqrdSAxMCBIeiksa2hpIG3hu5l0IGNodeG7l2kgZMOgaSDEkcaw4bujYyBjaGlhIMSR4buBdSB0aMOgbmggbmhp4buBdSBwaMOibiDEkW/huqFuLCBuw7Mgc+G6vSB0cuG7nyB0aMOgbmggbeG7mXQgdGVuc29yL2FycmF5IDRELCB0aOG6rW0gY2jDrSA1RCwgYmFvIGfhu5NtIGNoaeG7gXUgdGjhu51pIGdpYW4uIEdp4bqjIMSR4buLbmggbuG6v3UgbXXhu5FuIHBow6JuIGJp4buHdCA1IHRy4bqhbmcgdGjDoWkgZ2nhuqVjIG5n4bunIGtow6FjIG5oYXUgKGLDoGkgdG/DoW4gbXVsdGljbGFzcyBjbGFzc2lmaWNhdGlvbiksIHRhIGtow7MgbMOybmcgc+G7rSBk4bulbmcgdHLhu7FjIHRp4bq/cCBk4buvIGxp4buHdSBn4buRYywgbmdheSBj4bqjIGLhurFuZyBtw7QgdOG6oyB0cuG7sWMgcXVhbi7EkMOieSBsw6AgbeG7mXQga2jDtG5nIGdpYW4gZOG7ryBsaeG7h3UgcXXDoSBs4bubbiwgbmhp4buBdSB0aMO0bmcgdGluIHRp4buBbSDhuqluIGLDqm4gdHJvbmcgdsOgIGtow7MgcGjDom4gdMOtY2guIEvhu7kgdGh14bqtdCB0csOtY2ggeHXhuqV0IGZlYXR1cmVzIGPDsyB0aOG7gyByw7p0IGfhu41uIGNoaeG7gXUgZOG7ryBsaeG7h3UgeHXhu5FuZyBi4bqxbmcgY8OhY2ggc+G7rSBk4bulbmcgY8OhYyBjaOG7iSBz4buRIGNobyBwaMOpcCBtw7QgdOG6oyB24buBIMSR4bq3YyB0w61uaCwga2h1eW5oIGjGsOG7m25nIGPhu6dhIGNodeG7l2kgZOG7ryBsaeG7h3UgZ+G7kWMsIHBhY2thZ2UgdHNmZWF0dXJlcyBjdW5nIGPhuqVwIG3hu5l0IHPhu5EgaMOgbSB0aeG7h24gbOG7o2kgxJHhu4MgbMOgbSB2aeG7h2MgbsOgeS4NCg0KIyBUcsOtY2ggeHXhuqV0IGZlYXR1cmUgY2hvIDEgY2h14buXaSBi4bqxbmcgaMOgbSBjaHV5w6puIGThu6VuZw0KDQp24buBIG3hurd0IGvhu7kgdGh14bqtdCwgbeG7l2kgY2h14buXaSBk4buvIGxp4buHdSBjxqEgYuG6o24gbMOgIG3hu5l0IHZlY3RvciAoMUQgYXJyYXkpIG7Dqm4gdGEgY8OzIHRo4buDIMOhcCBk4bulbmcgdOG6pXQgY+G6oyBuaOG7r25nIGjDoG0gdGjDtG5nIHRoxrDhu51uZywgYmFvIGfhu5NtIHRo4buRbmcga8OqLCB0w61uaCB0b8OhbiBz4buRIGjhu41jLCBob8OhbiBjaHV54buDbiwgLi5O4bq/dSBow6BtIG7DoHkgeHXhuqV0IHJhIGvhur90IHF14bqjIGzDoCBt4buZdCBjb24gc+G7kSwga+G6v3QgcXXhuqMgbsOgeSBjw7MgdGjhu4MgxJHGsOG7o2MgeGVtIG5oxrAgbeG7mXQgZmVhdHVyZSwNCg0KVGjDrSBk4bulIMSRxqFuIGdp4bqjbiBuaOG6pXQsIGjDoG0gbWVkaWFuIGNobyBiaeG6v3QgduG7iyB0csOtIHRydW5nIHTDom0gY+G7p2EgY2h14buXaToNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptZWRpYW4oYW5udWFsX3N1bnBvc3QpDQoNCmdncGxvdChkYXRhPWFubnVhbF9zdW5wb3N0KSsNCiAgZ2VvbV9wb2ludChhZXMoeT1hbm51YWxfc3VucG9zdCx4PWMoMTcwMDoxOTg4KSksY29sPSJyZWQiLGFscGhhPTAuNSkrDQogIGdlb21fbGluZShhZXMoeT1hbm51YWxfc3VucG9zdCx4PWMoMTcwMDoxOTg4KSksY29sPSJncmV5IixhbHBoYT0wLjUpKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSBtZWRpYW4oYW5udWFsX3N1bnBvc3QpLCBjb2xvciA9ICJibHVlIiwgbGluZXR5cGU9MikrDQogIHNjYWxlX3hfY29udGludW91cygnWWVhcicpKw0KICB0aGVtZV9idygpDQpgYGANCg0KUGFja2FnZSB0c2ZlYXR1cmUgY3VuZyBj4bqlcCBraG/huqNuZyAzMCBow6BtIGtow6FjIG5oYXUgdMawxqFuZyDhu6luZyB24bubaSBuaGnhu4F1IHRodeG7mWMgdMOtbmggY2h1ecOqbiBiaeG7h3QgY2hvIHRpbWUgc2VyaWVzLCANCg0KaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3RzZmVhdHVyZXMvdmlnbmV0dGVzL3RzZmVhdHVyZXMuaHRtbCNzdGxfZmVhdHVyZXMNCg0KVGjDrSBk4bulLCBow6BtIGFjZl9mZWF0dXJlcyBjdW5nIGPhuqVwIGjhu4cgc+G7kSB04buxIHTGsMahbmcgcXVhbiB2w6AgdOG7lW5nIGLDrG5oIHBoxrDGoW5nIGPhu6dhIDEwIGF1dG9jb3JyZWxhdGlvbiBjb2VmZmljaWVudHMgxJHhuqd1IHRpw6puDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KYWNmX2ZlYXR1cmVzKGVjZyklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCkjDoG0gc3RsX2ZlYXR1cmVzIHRyw61jaCB4deG6pXQgOCB0aHXhu5ljIHTDrW5oIHbhu4EgdHJlbmQgdsOgIHNlYXNvbmFsaXR5IGPhu6dhIHRpbWUgc2VyaWVzIGThu7FhIHbDoG8gcGjDom4gdMOtY2ggU1RMIGRlY29tcG9zaXRpb24uDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc3RsX2ZlYXR1cmVzKGVjZyklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCkjDoG0gaGV0ZXJvZ2VuZWl0eSB0csOtY2ggeHXhuqV0IDQgdGh14buZYyB0w61uaCBuaOG6sW0ga2jhuqNvIHPDoXQgaGV0ZXJvZ2VuZWl0eSBk4buxYSB2w6BvIHThu5VuZyBiw6xuaCBwaMawxqFuZyB2w6AgUjIgY+G7p2EgbcO0IGjDrG5oIEdBUkNIIHbDoCBBUkNIOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmhldGVyb2dlbmVpdHkgKGFpcnBhc3MpJT4la25pdHI6OmthYmxlKCkNCmBgYA0KDQojIFRyw61jaCB4deG6pXQgZmVhdHVyZXMgY2hvIGjDoG5nIGxv4bqhdCBjaHXhu5dpDQoNCkjDoG0gdHNmZWF0dXJlcyBjaG8gcGjDqXAgw6FwIGThu6VuZyBt4buZdCBoYXkgbmhp4buBdSBwaMawxqFuZyBwaMOhcCB0csOtY2ggeHXhuqV0IGZlYXR1cmVzIGtow6FjIG5oYXUgY2hvIDEgbGlzdCBn4buTbSBuaGnhu4F1IGNodeG7l2kga2jDoWMgbmhhdS4gTsOzIGzDoG0gxJHGoW4gZ2nhuqNuIGNvZGUgbcOgIGtow7RuZyBj4bqnbiDEkeG6v24gdsOybmcgbOG6t3AsIGjDoG0gYXBwbHkgaGF5IGjDoG0gbWFwLiANCg0KVHLGsOG7m2MgaOG6v3QgdGEgxJHGsGEgY8OhYyBjaHXhu5dpIGPhuqduIHjhu60gbMO9IHbDoG8gMSBsaXN0IChjw7MgdGjhu4MgZMO5bmcgaMOgbSBzcGxpdCB0csOqbiAxIGRhdGFmcmFtZSksIHRow60gZOG7pSB0YSBtdeG7kW4geOG7rSBsw70gMyBjaHXhu5dpIGFub3JtX3JhdyDhu58gMyB0cuG6oW5nIHRow6FpIEF3aywgTjIgdsOgIFJFTSA6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KdHNfbGlzdCA9IGxpc3QoQXdrX2RmJGFub3JtX3JhdywNCiAgICAgICAgICAgICAgIE4yX2RmJGFub3JtX3JhdywNCiAgICAgICAgICAgICAgIFJlbV9kZiRhbm9ybV9yYXcpDQoNCnN0cih0c19saXN0KQ0KYGBgDQoNCkPDsyB0aOG7gyDDoXAgZOG7pW5nIDEgaGF5IGjDoG5nIGxv4bqhdCBwaMawxqFuZyBwaMOhcCwgdMOqbiBj4bunYSBwaMawxqFuZyBwaMOhcCBsw6AgdMOqbiBj4bunYSBow6BtIGPDsyB0cm9uZyBFbnZpcm9ubWVudCwgY8OzIHRo4buDIGzDoCAxIGjDoG0gIGPDsyBz4bq1biB0cm9uZyBwYWNrYWdlIGhheSAxIGjDoG0gxJHhurdjIGJp4buHdCBkbyBi4bqhbiB04bqhbyByYToNCg0KVGjDrSBk4bulIHF1eSB0csOsbmggc2F1IHRyw61jaCB4deG6pXQgY8OhYyBmZWF0dXJlczogZW50cm9weSwgc3RhYmlsaXR5LCBub25saW5lYXJpdHksIGx1bXBpbmVzcywgY3Jvc3NpbmdfcG9pbnRzIGNobyBsaXN0IDMgc2VyaWVzOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnRzZmVhdHVyZXModHNfbGlzdCwgZmVhdHVyZXMgPSBjKCJlbnRyb3B5Iiwic3RhYmlsaXR5Iiwibm9ubGluZWFyaXR5IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibHVtcGluZXNzIiwgImNyb3NzaW5nX3BvaW50cyIpKSU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KS+G6v3QgcXXhuqMgeHXhuqV0IHJhIGzDoCAxIGRhdGEgZnJhbWUsIG3hu5dpIGjDoG5nIHTGsMahbmcg4bupbmcgMSBzZXJpZXMgdHJvbmcgbGlzdCwgbeG7l2kgY+G7mXQgbMOgIDEgZmVhdHVyZXMuIE7hur91IGjDoG0geHXhuqV0IHJhIG5oaeG7gXUgZmVhdHVyZXMsIGNow7puZyBz4bq9IMSRxrDhu6NjIGdow6lwIG7hu5FpIHRp4bq/cCB0aGVvIG5oaeG7gXUgY+G7mXQNCg0KTmjGsCDEkcOjIG7Ds2ksIHRhIGPDsyB0aOG7gyB2aeG6v3QgaMOgbSB0w7l5IHRow61jaCwgdGjDrSBk4bulIE5oaSB04bqhbyByYSAxIGjDoG0gY8OzIHTDqm4gbMOgIGJveGNveF9zdGwsIG7DsyBz4bq9IGzDoG0gMSBxdXkgdHLDrG5oIHBo4bupYyB04bqhcCBn4buTbTogY2h14bqpbiBow7NhIChzY2FsaW5nKSBjaHXhu5dpIHRow6BuaCB0aGFuZyDEkW8gMDoxLCBzYXUgxJHDsyBob8OhbiBjaHV54buDbiBCb3hDb3gsIGN14buRaSBjw7luZyDDoXAgZOG7pW5nIHBoxrDGoW5nIHBow6FwIFNUTCDEkeG7gyB4deG6pXQgcmEgOCBmZWF0dXJlczoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpib3hjb3hfc3RsIDwtIGZ1bmN0aW9uKHgsLi4uKSB7DQogIG1pbm1heCA9ICh4LSBtaW4oeCkpIC8obWF4KHgpLW1pbih4KSkNCiAgbGFtYmRhIDwtIGZvcmVjYXN0OjpCb3hDb3gubGFtYmRhKG1pbm1heCkNCiAgeSA8LSBmb3JlY2FzdDo6Qm94Q294KG1pbm1heCwgbGFtYmRhKQ0KICBjKHN0bF9mZWF0dXJlcyh5LCBzLndpbmRvdz0ncGVyaW9kaWMnLCByb2J1c3Q9VFJVRSksIGxhbWJkYT1sYW1iZGEpDQp9DQoNCnRzZmVhdHVyZXModHNfbGlzdCwgZmVhdHVyZXMgPSAnYm94Y294X3N0bCcpJT4la25pdHI6OmthYmxlKCkNCg0KYGBgDQoNCiMgVHLDrWNoIHh14bqldCBmZWF0dXJlcyBjaG8gZOG7ryBsaeG7h3UgY2h14buXaSDEkWEgY2hp4buBdQ0KDQpUYSBz4bq9IGzDoG0gMSBxdXkgdHLDrG5oIHBo4bupYyB04bqhcCBoxqFuLCB0aMOtIGThu6UgdGEgY8OzIGRhdGFmcmFtZSBzbGVlcF9kZiBn4buTbSA4IGLEg25nIHTDrW4gaGnhu4d1LCBnaGkgdHJvbmcgNSB0cuG6oW5nIHRow6FpIGdp4bqlYyBuZ+G7pyBraMOhYyBuaGF1LCBt4buXaSB0cuG6oW5nIHRow6FpIGzhuqFpIMSRxrDhu6NjIGNoaWEgdGjDoG5oIDEwIHBow6JuIMSRb+G6oW4ga2jDoWMgbmhhdSBjw7MgxJHhu5kgZMOgaSAxMDAgbeG6q3UgKDEwIGdpw6J5LCAxMEh6KSwgY+G6pXUgdHLDumMgbsOgeSB0xrDGoW5nIOG7qW5nIHbhu5tpIDEgdGVuc29yL2FycmF5IDRELiBUYSBtdeG7kW4gdHLDrWNoIHh14bqldCBlbnRyb3B5IGPhu6dhIHThu6tuZyBwaMOibiDEkW/huqFuDQoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzbGVlcF9kZiRpZHggPSByZXAoYygxOjUwKSxlYWNoID0gMTAwKQ0KDQpzbGVlcF9kZiU+JXNhbXBsZV9uKDEwKSU+JWtuaXRyOjprYWJsZSgpDQoNCnNsZWVwX2RmJT4lZ3JvdXBfYnkoaWR4LFN0YWRlKSU+JXRhbGx5KCklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCsSQ4buDIGzDoG0gdmnhu4djIG7DoHksIGThu7Ega2nhur9uIHRhIHBo4bqjaSBkw7luZyBow6BtIG1hcCB2w6AgMSBow6BtIGPhuqNpIGJpw6puIMSR4buDIGzDoG0gdmnhu4djIHRyw6puIGxpc3QuDQoNClRhIHThuqFvIHJhIGjDoG0gZW50X2Z1bmMsIG7DsyBz4bq9IGNodXnhu4NuIGRhdGFmcmFtZSBuaOG7jyBn4buTbSA4IGLEg25nIGThu68gbGnhu4d1IHRow6BuaCAxIGxpc3QsIHNhdSDEkcOzIMOhcCBk4bulbmcgaMOgbSB0c19mZWF0dXJlcyAoaGF5IHRy4buxYyB0aeG6v3AgaMOgbSBlbnRyb3B5IGPFqW5nIMSRxrDhu6NjKSDEkeG7gyB0csOtY2ggeHXhuqV0IGvhur90IHF14bqjIGVudHJvcHkgY2hvIG3hu5dpIGLEg25nIGThu68gbGnhu4d1LiANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQplbnRfZnVuYyA9IGZ1bmN0aW9uKC54KXsNCiAgdHNfbGlzdCA9IGFzLmxpc3QoLnhbLC1jKDksMTApXSkNCiAgb3V0ID0gdHNmZWF0dXJlcyh0c19saXN0LA0KICAgICAgICAgICAgIGZlYXR1cmVzPSJlbnRyb3B5IiklPiVhcy52ZWN0b3IoKQ0KICByZXR1cm4ob3V0KQ0KIH0NCmBgYA0KDQpUaeG6v3AgdGhlbywgdGEgZMO5bmcgaMOgbSBtYXBfZGYgxJHhu4MgY2jhuqF5IGjDoG0gZW50X2Z1bmMgY2hvIHThu6tuZyBwaMOibiDEkW/huqFuIHThu6sgMSDEkeG6v24gNTAgKGPDsyA1MCBwaMOibiDEkW/huqFuLCBt4buXaSBuaMOzbSBsYWJlbCBjw7MgMTAgcGjDom4gxJFv4bqhbiB2w6AgbeG7l2kgcGjDom4gxJFv4bqhbiBkw6BpIDEwMCBt4bqrdSkgdsOgIGdoaSBr4bq/dCBxdeG6oyB0cm9uZyBkYXRhZnJhbWUgb3V0X2RmDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc2xlZXBfZGYlPiVzcGxpdCguJGlkeCklPiUNCiAgbWFwX2RmKC4sfmRhdGFfZnJhbWUoZW50PWVudF9mdW5jKC54KSU+JS4kZW50cm9weSwNCiAgICAgICAgICAgICAgICAgICAgICAgcGFyYSA9IGNvbG5hbWVzKGRmWywtYyg5LDEwKV0pLA0KICAgICAgICAgICAgICAgICAgICAgICBzdGFkZT0ueCRTdGFkZVtbMV1dKSktPm91dF9kZg0KYGBgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kb3V0X2RmJGlkeCA9IGZhY3RvcihjKDE6NTApKQ0KDQpvdXRfZGYlPiVzYW1wbGVfbigxMCklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCkzDumMgbsOgeSB0YSBjw7MgdGjhu4MgZMO5bmcgZW50cm9weSBuaMawIDEgZmVhdHVyZSDEkeG7gyBwaMOibiBiaeG7h3QgNSBsYWJlbCBzbGVlcCBzdGFkZXM6IA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCm91dF9kZiU+JWdncGxvdCgpKw0KICBnZW9tX2RlbnNpdHkoYWVzKHg9ZW50LGZpbGw9c3RhZGUpLGFscGhhPTAuNykrDQogIHRoZW1lX2J3KDgpKw0KICBmYWNldF93cmFwKH5wYXJhLG5jb2w9MyxzY2FsZXMgPSAiZnJlZSIpDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpvdXRfZGYlPiVnZ3Bsb3QoKSsNCiAgZ2VvbV9ib3hwbG90KGFlcyh4PXN0YWRlLHk9ZW50LGZpbGw9c3RhZGUpLGFscGhhPTAuNykrDQogIHRoZW1lX2J3KDgpKw0KICBjb29yZF9mbGlwKCkrDQogIGZhY2V0X3dyYXAofnBhcmEsbmNvbD0yLHNjYWxlcyA9ICJmcmVlIikNCmBgYA0KDQpUaGF5IHbDrCB4deG6pXQgMSBmZWF0dXJlcyBkdXkgbmjhuqV0IGzDoCBlbnRyb3B5LCB0YSBjw7MgdGjhu4MgeHXhuqV0IG5oaeG7gXUgZmVhdHVyZXMgaMahbiwgbmjGsCBzYXUgxJHDonkgLSBOaGkgZMO5bmcgcXV5IHRyw6xuaCBzdGxfZmVhdHVyZXMgxJHhu4MgeHXhuqV0IHJhIDggZmVhdHVyZXMsIHNhdSDEkcOzIGNo4buNbiByYSA0IGzDoCB0cmVuZCxzcGlrZSxsaW5lYXJpdHkgdsOgIGN1cnZhdHVyZS4gTMawdSDDvSwgaMOgbSBzdGxfZnVuYyB4deG6pXQga+G6v3QgcXXhuqMgbMOgIDEgZGF0YWZyYW1lIHRoYXkgdsOsIHZlY3RvciBuaMawIGjDoG0gZW50X2Z1bmMNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzdGxfZnVuYyA9IGZ1bmN0aW9uKC54KXsNCiAgdHNfbGlzdCA9IGFzLmxpc3QoLnhbLC1jKDksMTApXSkNCiAgb3V0ID0gdHNmZWF0dXJlcyh0c19saXN0LA0KICAgICAgICAgICAgIGZlYXR1cmVzPSJzdGxfZmVhdHVyZXMiKQ0KICByZXR1cm4ob3V0KQ0KIH0NCmBgYA0KDQpL4bq/dCBo4bujcCBow6BtIHNwbGl0IHbDoCBtYXBfZGYsIHRhIGPDsyB0aOG7gyBjaOG6oXkgw6FwIGThu6VuZyBow6BtIHN0bF9mdW5jIHbhu6thIHThuqFvIGNobyBow6BuZyBsb+G6oXQgcGjDom4gxJFv4bqhbiwgdsOgIGzGsHUga+G6v3QgcXXhuqMgbOG6oWkgdGjDoG5oIGRhdGFmcmFtZSBvdXRfZGYyDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kc2xlZXBfZGYlPiVzcGxpdCguJGlkeCklPiUNCiAgbWFwX2RmKC4sfnN0bF9mdW5jKC54KSU+JQ0KICAgICAgICAgICBzZWxlY3QodHJlbmQsc3Bpa2UsbGluZWFyaXR5LGN1cnZhdHVyZSkpLT5vdXRfZGYyDQpgYGANCg0KYGBge3J9DQpvdXRfZGYyJT4lc2FtcGxlX24oMTApJT4la25pdHI6OmthYmxlKCkNCmBgYA0KDQpUYSBnaMOpcCAyIGRhdGFmcmFtZSBvdXRfZGYgdsOgIG91dF9kZjIgbOG6oWkgdGjDoG5oIDEsIGzDumMgbsOgeSB0YSBjw7MgNSBmZWF0dXJlcyB0cm9uZyAxIGRhdGFmcmFtZQ0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnN0bF9vdXRfZGYgPSBiaW5kX2NvbHMob3V0X2RmMixvdXRfZGYpDQpgYGANCg0KZGF0YWZyYW1lIG7DoHkgY8OzIHRo4buDIMSRxrDhu6NjIGTDuW5nIG5oxrAgaW5wdXQgZGF0YSBjaG8gYsOgaSB0b8OhbiBjbGFzc2ZpY2F0aW9uLCBob+G6t2MgdW5zdXBlcnZpc2VkIGxlYXJuaW5nLiANCg0KVGEgY8OzIHRo4buDIHRoxINtIGTDsiB0cuG7sWMgcXVhbiDEkeG6t2MgdMOtbmggY+G7p2EgZOG7ryBsaeG7h3UgZmVhdHVyZXMgYuG6sW5nIGhlYXRtYXA6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh2aXJpZGlzKQ0KDQpzdGxfb3V0X2RmJT4lDQogIGRwbHlyOjpzZWxlY3QoLWMocGFyYSxzdGFkZSxpZHgpKSU+JQ0KICBzY2FsZSguKSU+JQ0KICBhc190aWJibGUoKSU+JQ0KICBtdXRhdGUoU3RhZGU9b3V0X2RmJHN0YWRlLA0KICAgICAgICAgSWQ9ZmFjdG9yKHJlcChjKDE6NSksODApKSwNCiAgICAgICAgIHBhcmE9b3V0X2RmJHBhcmEpJT4lDQogIGdhdGhlcih0cmVuZDplbnQsDQogICAgICAgICBrZXk9IkZlYXR1cmUiLHZhbHVlPSJWYWx1ZSIpJT4lDQogIGdncGxvdChhZXMoeD1yZW9yZGVyKElkLFZhbHVlKSwNCiAgICAgICAgICAgICB5PXJlb3JkZXIoRmVhdHVyZSxWYWx1ZSksDQogICAgICAgICAgICAgZmlsbD1WYWx1ZSkpKw0KICBnZW9tX3RpbGUoc2hvdy5sZWdlbmQ9VCkrDQogIHRoZW1lX2J3KDcpKw0KICB0aGVtZShheGlzLnRleHQueT1lbGVtZW50X2JsYW5rKCksDQogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID00NSxoanVzdD0xLHZqdXN0PTEpKSsNCiAgY29vcmRfZmxpcCgpKw0KICBzY2FsZV95X2Rpc2NyZXRlKCJGZWF0dXJlcyIpKw0KICBzY2FsZV94X2Rpc2NyZXRlKCJGcmFnbWVudHMiLGJyZWFrcz1OVUxMKSsNCiAgZmFjZXRfZ3JpZChTdGFkZX5wYXJhLHNocmluayA9IFQpKw0KICBzY2FsZV9maWxsX3ZpcmlkaXMob3B0aW9uPSJBIixiZWdpbj0xLGVuZD0wKQ0KYGBgDQoNCkhv4bq3YyBi4bqxbmcgdmlvbGluIHBsb3QNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzdGxfb3V0X2RmJT4lDQogIGRwbHlyOjpzZWxlY3QoLWMocGFyYSxzdGFkZSxpZHgpKSU+JQ0KICBzY2FsZSguKSU+JQ0KICBhc190aWJibGUoKSU+JQ0KICBtdXRhdGUoc3RhZGU9b3V0X2RmJHN0YWRlLA0KICAgICAgICAgcGFyYT1vdXRfZGYkcGFyYSklPiUNCiAgZ2F0aGVyKDE6NSxrZXk9IkZlYXR1cmUiLHZhbHVlPSJWYWx1ZSIpJT4lDQogIGdncGxvdCgpKw0KICBnZW9tX3Zpb2xpbihhZXMoeD1zdGFkZSx5PVZhbHVlLGZpbGw9c3RhZGUpLGFscGhhPTAuNSkrDQogIGZhY2V0X2dyaWQoRmVhdHVyZX5wYXJhKSsNCiAgdGhlbWVfYncoNykrDQogIHRoZW1lKGF4aXMudGV4dC55PWVsZW1lbnRfYmxhbmsoKSwNCiAgICAgICAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPTQ1LGhqdXN0PTEsdmp1c3Q9MSkpDQpgYGANCg0KIyBU4buVbmcga+G6v3QNCg0KQsOgaSB0aOG7sWMgaMOgbmggxJHhur9uIMSRw6J5IGzDoCBo4bq/dCwgTmhpIGh5IHbhu41uZyBwYWNrYWdlIHRzZmVhdHVyZSBz4bq9IG1hbmcgbOG6oWkgbmhp4buBdSB0acOqbiDDrWNoIGNobyBjw6FjIGLhuqFuIG5naGnDqm4gY+G7qXUgc2luaCBraGkgbMOgbSB2aeG7h2MgduG7m2kgZOG7ryBsaeG7h3UgY2h14buXaS4gQ2jDumMgY8OhYyBi4bqhbiB0aOG7sWMgaMOgbmggdnVpIHbDoCBo4bq5biBn4bq3cCBs4bqhaS4=