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")
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)
- 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")

- 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")

- 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")

- 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.
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_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()
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()
arch_acf |
0.2294789 |
garch_acf |
0.2278266 |
arch_r2 |
0.1891338 |
garch_r2 |
0.1894199 |
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()
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()
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 |
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()
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()
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()
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()
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))

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=