Data
Notation and time
index
Each series is y_1,\dots,y_n\ge0
with n=16{,}992 half-hourly records
from 1 January 2018 00:30 to 21 December 2018 00:00, that is, 354 daily
blocks running from 00:30 to 24:00. For each time t,
h(t)=\big((t-1)\bmod 48\big)+1,\quad k(t)=\Big\lceil
\tfrac{t}{48}\Big\rceil,\quad
d(t)=\big((k(t)-1)\bmod 7\big)+1,\quad \ell(t)=\Big\lceil
\tfrac{t}{336}\Big\rceil
are the half-hour of the day, the day, the weekday (1 = Monday) and
the week. The daily period is s_1=48
and the weekly period is s_2=336. \mathcal O denotes the observed indices and
\mathcal M=\{1,\dots,n\}\setminus\mathcal
O the missing ones. The code checks that the columns
J and Hari of the file equal h(t) and d(t).
if (!file.exists(P$data_path))
stop("Data file not found: ", P$data_path, ". Put aqms_surabaya_2018.csv in the data/ folder next to this notebook ",
"or set data_path in the YAML header.", call. = FALSE)
raw <- read.csv(P$data_path, check.names = FALSE)
names(raw) <- gsub("_", " ", names(raw))
N <- nrow(raw); S1 <- 48L; S2 <- 336L
TS <- seq(as.POSIXct("2018-01-01 00:30", tz = "UTC"), by = "30 min", length.out = N)
POLS <- c("PM10", "NO2", "SO2", "CO", "O3")
STATIONS <- c("SUF 1", "SUF 6", "SUF 7")
UNIT <- c(PM10 = "\u00b5g/m\u00b3", NO2 = "\u00b5g/m\u00b3", SO2 = "\u00b5g/m\u00b3", CO = "mg/m\u00b3", O3 = "\u00b5g/m\u00b3")
h_of <- function(t) ((t - 1) %% S1) + 1 # half-hour of the day (1 = 00:30)
d_of <- function(t) (((t - 1) %/% S1) %% 7) + 1 # weekday (1 = Monday)
SERIES <- paste(rep(STATIONS, each = 5), POLS)
if (!all(c("J", "Hari", SERIES) %in% names(raw)))
stop("The data file does not have the expected columns (J, Hari and 'SUF 1 CO' ... 'SUF 7 SO2').", call. = FALSE)
stopifnot(N == 16992, all(raw$J == h_of(seq_len(N))), all(raw$Hari == d_of(seq_len(N))))
get_series <- function(st, p) as.numeric(raw[[paste(st, p)]])
# check against the reference data, from which all stored results were computed
X <- sapply(SERIES, function(v) as.numeric(raw[[v]]))
data_check <- data.frame(item = c("rows", "missing values", "sum of the observed values"),
found = c(N, sum(is.na(X)), sum(X, na.rm = TRUE)), reference = c(16992, 33250, 5022646.3757342435))
data_check$identical <- abs(data_check$found - data_check$reference) <= 1e-6 * pmax(1, abs(data_check$reference))
if (!all(data_check$identical)) cat("NOTE: the data differ from the reference data; set use_cache: false so that",
"every result is recomputed from these data.\n")
knitr::kable(transform(data_check, found = sprintf(c("%.0f", "%.0f", "%.4f"), found),
reference = sprintf(c("%.0f", "%.0f", "%.4f"), reference)),
caption = "Data check against the reference data set")
Data check against the reference data set
| item |
found |
reference |
identical |
| rows |
16992 |
16992 |
TRUE |
| missing values |
33250 |
33250 |
TRUE |
| sum of the observed values |
5022646.3757 |
5022646.3757 |
TRUE |
TEST_DATE <- as.POSIXct("2018-12-01 00:30", tz = "UTC")
cat("n =", N, "| period:", format(TS[1]), "to", format(TS[N]), "| columns J and Hari verified\n")
n = 16992 | period: 2018-01-01 00:30:00 to 2018-12-21 | columns J and Hari verified
Missingness and
descriptive statistics
gap_lengths <- function(x) { r <- rle(is.na(x)); r$lengths[r$values] }
tab1 <- bind_rows(lapply(POLS, function(p) {
x <- get_series("SUF 1", p); g <- gap_lengths(x); o <- x[!is.na(x)]
data.frame(pollutant = p, unit = UNIT[[p]], missing = 100 * mean(is.na(x)), gaps = length(g),
median_gap = floor(median(g)), max_gap = max(g), in_long = 100 * sum(g[g > 48]) / sum(g),
mean = mean(o), median = median(o), sd = sd_pop(o), max = max(o),
suf6 = 100 * mean(is.na(get_series("SUF 6", p))), suf7 = 100 * mean(is.na(get_series("SUF 7", p))))
}))
write.csv(tab1, out_file("table1_data.csv"), row.names = FALSE)
write_tex(c("\\begin{table*}[!t]", "\\centering",
"\\caption{Missingness and descriptive statistics of the half-hourly pollutant series (1~Jan--20~Dec 2018, $n=16{,}992$ per series). Gap statistics refer to SUF~1; the last two columns give the missing rate at the two development stations.}",
"\\label{tab:data}", "\\setlength{\\tabcolsep}{4pt}", "\\begin{tabular}{llrrrrrrrrrrr}", "\\toprule",
" & & \\multicolumn{5}{c}{SUF~1 missingness} & \\multicolumn{4}{c}{SUF~1 observed values} & \\multicolumn{2}{c}{Missing (\\%)}\\\\",
"\\cmidrule(lr){3-7}\\cmidrule(lr){8-11}\\cmidrule(lr){12-13}",
"Pollutant & Unit & Missing (\\%) & Gaps & Median gap & Max. gap & In gaps $>$1\\,d (\\%) & Mean & Median & SD & Max. & SUF~6 & SUF~7\\\\",
"\\midrule",
with(tab1, sprintf("%s & %s & %.1f & %d & %d & %s & %.1f & %.2f & %.2f & %.2f & %.1f & %.1f & %.1f\\\\",
POL_TEX[pollutant], UNIT_TEX[pollutant], missing, gaps, as.integer(median_gap),
formatC(max_gap, format = "d", big.mark = ","), in_long, mean, median, sd, max, suf6, suf7)),
"\\bottomrule", "\\end{tabular}", "\\end{table*}"), "tab_data")
knitr::kable(tab1, digits = c(0, 0, 1, 0, 0, 0, 1, 2, 2, 2, 1, 1, 1),
col.names = c("Pollutant", "Unit", "Missing (%)", "Gaps", "Median gap", "Max. gap", "In gaps > 1 d (%)",
"Mean", "Median", "SD", "Max.", "SUF 6 missing (%)", "SUF 7 missing (%)"),
caption = cap("Table 2", "Missingness and descriptive statistics (gap statistics: SUF 1)"))
Missingness and descriptive statistics (gap statistics: SUF
1)
| Pollutant |
Unit |
Missing (%) |
Gaps |
Median gap |
Max. gap |
In gaps > 1 d (%) |
Mean |
Median |
SD |
Max. |
SUF 6 missing (%) |
SUF 7 missing (%) |
| PM10 |
µg/m³ |
44.9 |
225 |
3 |
5623 |
80.1 |
25.26 |
12.00 |
35.41 |
342.6 |
9.3 |
12.9 |
| NO2 |
µg/m³ |
14.3 |
305 |
1 |
1263 |
83.0 |
22.57 |
18.25 |
16.89 |
135.6 |
15.0 |
5.1 |
| SO2 |
µg/m³ |
7.8 |
449 |
2 |
76 |
5.8 |
28.61 |
23.91 |
25.25 |
374.2 |
6.7 |
9.6 |
| CO |
mg/m³ |
11.4 |
420 |
1 |
845 |
54.6 |
0.63 |
0.46 |
0.51 |
6.1 |
42.4 |
3.7 |
| O3 |
µg/m³ |
2.8 |
340 |
1 |
11 |
0.0 |
36.55 |
26.99 |
30.13 |
176.5 |
5.9 |
3.9 |
Missing values on the
time axis
Panel (a) shows the five half-hourly series of SUF 1 on their real
timestamps. Each run of missing values is shaded red, as in
imputeTS::ggplot_na_distribution(), and the label gives the
share of missing values; the blue band is the test period. Panel (b)
splits the missing values by gap length. The data frame
gaps_df, with the start and end of every gap, is reused for
the overview diagram at the end.
# fig01_missingness: (a) the half-hourly series of SUF 1 on their real timestamps. Every run of missing values is
# marked by a red tick at the bottom, gaps longer than two hours are also shaded red (the view of
# imputeTS::ggplot_na_distribution), and the test period is shaded blue. (b) Share of the
# missing values by gap length.
na_runs <- function(st, pols = POLS) bind_rows(lapply(pols, function(p) {
x <- get_series(st, p); r <- rle(is.na(x)); e <- cumsum(r$lengths); s <- e - r$lengths + 1
data.frame(station = st, pollutant = p, start = TS[s[r$values]], end = TS[e[r$values]] + 1800,
len = r$lengths[r$values])
}))
POL_LAB <- c(PM10 = "atop(PM[10], '\u00b5g/m\u00b3')", NO2 = "atop(NO[2], '\u00b5g/m\u00b3')",
SO2 = "atop(SO[2], '\u00b5g/m\u00b3')", CO = "atop(CO, 'mg/m\u00b3')", O3 = "atop(O[3], '\u00b5g/m\u00b3')")
plot_na_series <- function(stations, title) {
d <- bind_rows(lapply(stations, function(st) bind_rows(lapply(POLS, function(p)
data.frame(station = st, pollutant = p, t = TS, y = get_series(st, p)))))) |>
mutate(pollutant = factor(pollutant, POLS))
g <- bind_rows(lapply(stations, na_runs)) |> mutate(pollutant = factor(pollutant, POLS))
r <- d |> group_by(station, pollutant) |> summarise(rate = mean(is.na(y)), .groups = "drop")
ggplot(d, aes(t, y)) +
annotate("rect", xmin = TEST_DATE, xmax = max(TS), ymin = -Inf, ymax = Inf, fill = "#2471a3", alpha = 0.18) +
geom_rect(data = g[g$len > 4, ], aes(xmin = start, xmax = end, ymin = -Inf, ymax = Inf), inherit.aes = FALSE,
fill = "#f2b8b0") + # gaps longer than two hours are shaded
geom_rug(data = g, aes(x = start), inherit.aes = FALSE, sides = "b", colour = "#c0392b",
linewidth = 0.15, length = unit(0.12, "npc")) + # every gap, including single readings
geom_line(linewidth = 0.12, colour = "grey15", na.rm = TRUE) +
geom_label(data = r, aes(x = min(TS) + 3 * 86400, y = Inf, label = sprintf("%.1f%% missing", 100 * rate)),
inherit.aes = FALSE, hjust = 0, vjust = 1.1, size = 2.5, family = FONT_TEXT, colour = "#922b21",
fill = alpha("white", 0.85), label.size = 0, label.padding = unit(0.1, "lines")) +
facet_grid(if (length(stations) > 1) pollutant ~ station else pollutant ~ ., scales = "free_y", switch = "y",
labeller = labeller(pollutant = as_labeller(POL_LAB, label_parsed))) +
scale_x_datetime(date_breaks = if (length(stations) > 1) "2 months" else "1 month", date_labels = "%b",
expand = expansion(mult = 0.004)) +
scale_y_continuous(n.breaks = 3, expand = expansion(mult = c(0.03, 0.38))) +
labs(x = NULL, y = NULL, title = title) +
theme(strip.placement = "outside", strip.text.y.left = element_text(angle = 0, size = 7.8), axis.text = element_text(size = 7.2),
panel.grid.minor = element_blank(), panel.grid.major.y = element_blank(),
panel.grid.major.x = element_line(linewidth = 0.2, colour = "grey88"),
plot.title = element_text(size = 8.6), panel.spacing.y = unit(3, "pt"))
}
cls <- data.frame(lo = c(1, 5, 49, 337), hi = c(4, 48, 336, Inf), lab = c("1-4", "5-48", "49-336", ">336"))
share_df <- bind_rows(lapply(POLS, function(p) {
g <- gap_lengths(get_series("SUF 1", p))
data.frame(pollutant = factor(p, levels = POLS), class = factor(cls$lab, levels = cls$lab),
share = sapply(1:4, function(i) 100 * sum(g[g >= cls$lo[i] & g <= cls$hi[i]]) / sum(g)))
}))
p1a <- plot_na_series("SUF 1", "(a) Half-hourly series at SUF 1: gaps (red ticks; shaded if longer than 2 h) and test period (blue)")
p1b <- ggplot(share_df, aes(share, 1, fill = class)) +
geom_col(position = position_stack(reverse = TRUE), width = 0.55, orientation = "y") +
facet_grid(pollutant ~ .) +
scale_x_continuous(breaks = c(0, 50, 100), labels = function(v) paste0(v, "%"), expand = c(0, 0)) +
scale_y_continuous(expand = expansion(add = 0.6)) +
scale_fill_manual(values = c("#f5b7b1", "#ec7063", "#c0392b", "#641e16"), guide = guide_legend(nrow = 1)) +
labs(x = NULL, y = NULL, fill = "Gap length (half-hour steps):", title = "(b) Share by gap length") +
theme(strip.text = element_blank(), axis.text.y = element_blank(), panel.grid = element_blank(),
plot.title = element_text(size = 8.6), panel.spacing.y = unit(3, "pt"), plot.margin = margin(5.5, 10, 5.5, 5.5), axis.text.x = element_text(size = 7.2),
legend.key.size = unit(0.28, "cm"), legend.title = element_text(size = 7.8), legend.text = element_text(size = 7.8))
p1 <- if (HAS_PATCHWORK) patchwork::wrap_plots(p1a, p1b, widths = c(3.5, 1), guides = "collect") &
theme(legend.position = "bottom", legend.margin = margin(0, 0, 0, 0)) else p1a
print(p1); save_fig(p1, "fig01_missingness", 7.16, 3.55)

if (!HAS_PATCHWORK) { print(p1b); save_fig(p1b, "fig01b_gap_lengths", 3.5, 2.1) }
gaps_df <- na_runs("SUF 1") |> mutate(y = match(pollutant, rev(POLS))) # reused by the overview diagram (fig03)
All 15 series
The same view for the three stations. It shows where each series is
missing before any imputation.
# figS1_series_all_stations: the same view for the 15 series of the three stations.
pS1 <- plot_na_series(c("SUF 1", "SUF 6", "SUF 7"),
"Half-hourly series of the three stations: gaps (red ticks; shaded if longer than 2 h) and test period (blue)") +
theme(strip.text.x = element_text(size = 7.5, face = "bold"))
print(pS1); save_fig(pS1, "figS1_series_all_stations", 7.16, 5.6)

Double
seasonality
For day of the week d and half-hour
h the mean profile is \bar y_{d,h}=\frac{1}{|\mathcal
O_{d,h}|}\sum_{t\in\mathcal O_{d,h}}y_t with \mathcal O_{d,h}=\{t\in\mathcal O:d(t)=d,\
h(t)=h\}, and its average over the seven days, \bar y_{\cdot,h}=\frac17\sum_{d=1}^{7}\bar
y_{d,h}, is the mean daily profile. Panel (a) draws \bar y_{d,h} along the 336 half-hours of the
week together with \bar y_{\cdot,h}
repeated on every day: the repetition is the daily seasonality, and the
shaded difference \bar y_{d,h}-\bar
y_{\cdot,h}, the weekly deviation, is the weekly seasonality.
Panel (b) examines the weekly part where it is largest, in the morning:
for every date the mean of 06:00–10:00 is computed, the dates are
grouped by day of the week, and each group mean is expressed relative to
the working-day mean, with a 95% confidence interval of \pm1.96 standard errors across dates.
Intervals wider than \pm45\% (PM_{10}, with many missing mornings) are cut at
the edge.
# fig02_seasonality. (a) Mean concentration at each of the 336 half-hours of the week (solid) and the mean
# daily profile repeated on every day (dashed): the repetition is the daily seasonality and the shaded
# difference is the weekly seasonality. (b) Mean of 06:00-10:00 for each day of the week relative to the
# working-day mean, with 95% confidence intervals computed from the daily morning means.
wk <- bind_rows(lapply(POLS, function(p) data.frame(pollutant = p, d = d_of(seq_len(N)), h = h_of(seq_len(N)),
y = get_series("SUF 1", p)))) |>
filter(!is.na(y)) |> group_by(pollutant, d, h) |> summarise(m = mean(y), .groups = "drop") |>
group_by(pollutant, h) |> mutate(dp = mean(m)) |> ungroup() |> # dp: mean daily profile
mutate(how = (d - 1) * 48 + h, pollutant = factor(pollutant, POLS)) # how: half-hour of the week
morning <- bind_rows(lapply(POLS, function(p) {
y <- get_series("SUF 1", p); dd <- (seq_len(N) - 1) %/% 48; mo <- h_of(seq_len(N)) %in% 12:19 # 06:00-10:00
dm <- tapply(y[mo], dd[mo], function(v) if (sum(!is.na(v)) >= 4) mean(v, na.rm = TRUE) else NA) # one mean per date
wd <- tapply(d_of(seq_len(N))[mo], dd[mo], `[`, 1); ok <- !is.na(dm); ref <- mean(dm[ok & wd <= 5])
bind_rows(lapply(1:7, function(k) { v <- dm[ok & wd == k]
data.frame(pollutant = p, d = k, est = 100 * (mean(v) / ref - 1), se = 100 * sd(v) / sqrt(length(v)) / ref) }))
})) |> mutate(pollutant = factor(pollutant, POLS), lo = est - 1.96 * se, hi = est + 1.96 * se)
for (p in POLS) { su <- morning[morning$pollutant == p & morning$d == 7, ]; sa <- morning[morning$pollutant == p & morning$d == 6, ]
key(paste0("sun_morning_", p), su$est); key(paste0("sun_morning_lo_", p), su$lo); key(paste0("sun_morning_hi_", p), su$hi)
key(paste0("sat_morning_", p), sa$est) }
LINE2 <- c("mean at each half-hour of the week" = "#1f4e79", "mean daily profile, repeated" = "#5d6d7e")
p2a <- ggplot(wk, aes(how)) +
annotate("rect", xmin = 239.5, xmax = 335.5, ymin = -Inf, ymax = Inf, fill = "grey93") +
geom_vline(xintercept = 48 * (1:6) - 0.5, colour = "grey70", linewidth = 0.25, linetype = "22") +
geom_ribbon(aes(ymin = pmin(m, dp), ymax = pmax(m, dp)), fill = "#f5cba7") +
geom_line(aes(y = dp, colour = names(LINE2)[2], linetype = names(LINE2)[2]), linewidth = 0.35) +
geom_line(aes(y = m, colour = names(LINE2)[1], linetype = names(LINE2)[1]), linewidth = 0.45) +
facet_grid(pollutant ~ ., scales = "free_y", switch = "y", labeller = labeller(pollutant = as_labeller(POL_LAB, label_parsed))) +
scale_colour_manual(NULL, values = LINE2, breaks = names(LINE2)) +
scale_linetype_manual(NULL, values = setNames(c("solid", "22"), names(LINE2)), breaks = names(LINE2)) +
scale_x_continuous(breaks = 48 * (0:6) + 23.5, labels = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"), expand = c(0, 0)) +
scale_y_continuous(n.breaks = 3, expand = expansion(mult = c(0.04, 0.1))) +
labs(x = NULL, y = NULL, title = "(a) Mean concentration over the week at SUF 1; shaded: weekly deviation, grey band: weekend") +
theme(strip.placement = "outside", strip.text.y.left = element_text(angle = 0, size = 7.8), axis.text = element_text(size = 7.2),
panel.grid.minor = element_blank(), panel.grid.major.x = element_blank(),
panel.grid.major.y = element_line(linewidth = 0.15, colour = "grey88"), plot.title = element_text(size = 8.6),
panel.spacing.y = unit(3, "pt"))
lim <- 45 # % (wider intervals are cut at the edge)
p2b <- ggplot(morning, aes(d, est)) +
annotate("rect", xmin = 5.5, xmax = 7.5, ymin = -Inf, ymax = Inf, fill = "grey93") +
geom_hline(yintercept = 0, colour = "grey45", linewidth = 0.3) +
geom_errorbar(aes(ymin = lo, ymax = hi, colour = d >= 6), width = 0, linewidth = 0.55) +
geom_point(aes(colour = d >= 6), size = 1.4) +
facet_grid(pollutant ~ .) +
scale_colour_manual(values = c(`FALSE` = "#1f4e79", `TRUE` = "#d35400"), guide = "none") +
scale_x_continuous(breaks = 1:7, labels = c("M", "T", "W", "T", "F", "S", "S"), expand = expansion(add = 0.5)) +
scale_y_continuous(limits = c(-lim, lim), breaks = c(-30, 30), labels = c("\u221230%", "+30%"), oob = scales::oob_squish) +
labs(x = NULL, y = NULL, title = "(b) Mornings (06\u201310 h)") +
theme(strip.text = element_blank(), panel.grid.minor = element_blank(), panel.grid.major.x = element_blank(),
panel.grid.major.y = element_line(linewidth = 0.15, colour = "grey88"), axis.text.x = element_text(size = 7.2),
axis.text.y = element_text(size = 6.6), plot.title = element_text(size = 8.6), panel.spacing.y = unit(3, "pt"),
plot.margin = margin(5.5, 8, 5.5, 5.5))
p2 <- if (HAS_PATCHWORK) patchwork::wrap_plots(p2a, p2b, widths = c(3.3, 1.1), guides = "collect") &
theme(legend.position = "bottom", legend.text = element_text(size = 7.8), legend.key.width = unit(0.8, "cm"),
legend.margin = margin(0, 0, 0, 0)) else p2a
print(p2); save_fig(p2, "fig02_seasonality", 7.16, 3.6)

knitr::kable(tidyr::pivot_wider(morning |> mutate(v = sprintf("%+.1f [%+.1f, %+.1f]", est, lo, hi)) |> select(pollutant, d, v),
names_from = d, values_from = v) |> setNames(c("Pollutant", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")),
caption = "06:00-10:00 mean by day of the week relative to working days (%), with 95% confidence intervals")
06:00-10:00 mean by day of the week relative to working days
(%), with 95% confidence intervals
| Pollutant |
Mon |
Tue |
Wed |
Thu |
Fri |
Sat |
Sun |
| PM10 |
+4.3 [-41.4, +50.0] |
+4.4 [-62.2, +71.0] |
-5.6 [-51.7, +40.5] |
+16.9 [-29.0, +62.7] |
-21.4 [-54.3, +11.4] |
-0.4 [-46.1, +45.4] |
-4.3 [-44.1, +35.5] |
| NO2 |
-0.4 [-16.5, +15.7] |
-6.3 [-21.7, +9.1] |
+1.2 [-16.0, +18.4] |
+5.4 [-12.2, +23.1] |
-0.1 [-17.1, +16.9] |
+6.0 [-13.8, +25.8] |
-19.8 [-33.3, -6.3] |
| SO2 |
-5.2 [-22.9, +12.4] |
-6.5 [-23.4, +10.4] |
+8.0 [-9.5, +25.6] |
+2.2 [-16.6, +21.1] |
+1.3 [-16.8, +19.3] |
+23.0 [-1.2, +47.2] |
-10.6 [-28.0, +6.7] |
| CO |
+0.7 [-12.3, +13.8] |
-3.3 [-18.0, +11.5] |
-2.4 [-17.7, +12.9] |
+7.4 [-10.7, +25.4] |
-2.7 [-17.8, +12.4] |
-4.0 [-18.8, +10.7] |
-16.4 [-29.6, -3.2] |
| O3 |
-3.0 [-25.7, +19.7] |
-0.2 [-22.6, +22.1] |
+1.6 [-21.0, +24.2] |
+3.0 [-21.4, +27.4] |
-1.4 [-22.8, +20.0] |
+2.0 [-21.3, +25.3] |
+2.0 [-20.7, +24.8] |
Why imputation cannot
be avoided
Window-based forecasters (MLP, LSTM, TCN) need complete input
windows. For the 334 days before the test period, the code computes the
share of days with at least one missing value and the share of complete
two-day (96-step) windows.
pre <- seq_len(334 * S1)
mot <- bind_rows(lapply(POLS, function(p) {
x <- get_series("SUF 1", p)[pre]; miss <- is.na(x)
M <- matrix(miss, ncol = S1, byrow = TRUE) # days x half-hours
cs <- c(0, cumsum(miss)); L <- 96
w <- cs[(L + 1):length(cs)] - cs[1:(length(cs) - L)] # missing values per two-day window
data.frame(pollutant = p, days = 100 * mean(rowSums(M) > 0), complete = 100 * mean(w == 0))
}))
key("days_with_missing_min_pct", min(mot$days[mot$pollutant != "PM10"]))
key("complete_2day_windows_max_pct", max(mot$complete[mot$pollutant != "PM10"]))
knitr::kable(mot, digits = 1, col.names = c("Pollutant", "Days with >= 1 missing (%)", "Complete two-day windows (%)"),
caption = "Days with missing values and complete two-day windows (training period, 334 days)")
Days with missing values and complete two-day windows (training
period, 334 days)
| Pollutant |
Days with >= 1 missing (%) |
Complete two-day windows (%) |
| PM10 |
67.1 |
26.6 |
| NO2 |
93.4 |
0.0 |
| SO2 |
99.1 |
0.3 |
| CO |
93.1 |
0.7 |
| O3 |
91.6 |
0.6 |
The proposed method
Seas-AR
Seas-AR decomposes the transformed series as
z_t=T_t+D_t+W_t+R_t, \tag{2}
with a smooth trend T_t, a daily
profile D_t, a weekday-specific
deviation W_t and a remainder R_t. All components are estimated
from observed values only; \delta_t=1 if t\in\mathcal O and \delta_t=0 otherwise. The method consists of
the seven steps below.
Step 2 —
Missing-aware trend (Eq. 3)
For a partial residual u_t
(initially u_t=z_t), the trend is the
centred 2\times336 moving average of
the observed values:
T_t=\frac{\sum_{j=-168}^{168}w_j\,\delta_{t+j}\,u_{t+j}}{\sum_{j=-168}^{168}w_j\,\delta_{t+j}},\qquad
w_j=\begin{cases}1/336,&|j|<168,\\ 1/672,&|j|=168.\end{cases}
\tag{3}
The weights sum to one and span exactly one week, so for complete
data any component with period 48 or 336 that sums to zero over its
cycle disappears from T_t. Where less
than one quarter of the full weight is observed (inside long gaps),
T_t is filled by linear interpolation,
\hat x_t=x_a+\frac{t-a}{b-a}(x_b-x_a)
between the nearest observed points a<t<b, and kept constant beyond the
first and last observations. When only one value is observed, it is used
everywhere (with stats::approx this case would stop with
“need at least two non-NA values”).
Implementation. Eq. (3) is evaluated in O(n) operations with cumulative sums: the
inner sum (|j|<168) is a difference
of two cumulative sums and the two end points are added with half
weight. The observed weight equals (2c_{\rm
in}+c_{\rm end})/672, where c_{\rm
in} and c_{\rm end} count the
observed points; the rule “at least one quarter observed” is therefore
checked exactly with integers, 2c_{\rm in}+c_{\rm end}\ge168. Check 2
confirms that this fast version equals the literal convolution of Eq.
(3).
interp_linear <- function(x) { # linear interpolation, constant at the ends
obs <- !is.na(x); n_obs <- sum(obs)
if (n_obs == length(x)) return(x)
if (n_obs == 0) return(rep(0, length(x)))
if (n_obs == 1) { x[!obs] <- x[obs]; return(x) } # a single observation: constant
t <- seq_along(x)
x[!obs] <- stats::approx(t[obs], x[obs], xout = t[!obs], rule = 2)$y
x
}
nan_ma <- function(x, period = S2, min_frac = 0.25) { # Eq. (3) in O(n) with cumulative sums
n <- length(x); h <- period %/% 2; obs <- !is.na(x)
xp <- c(rep(0, h), ifelse(obs, x, 0), rep(0, h)) # zero padding = outside values missing
op <- c(rep(0, h), as.numeric(obs), rep(0, h))
cx <- c(0, cumsum(xp)); co <- c(0, cumsum(op))
i <- seq_len(n) + h # position of t in the padded series
in_x <- cx[i + h] - cx[i - h + 1]; in_o <- co[i + h] - co[i - h + 1] # |j| < h (weight 1/period)
en_x <- xp[i - h] + xp[i + h]; en_o <- op[i - h] + op[i + h] # |j| = h (weight 1/(2 period))
cnt2 <- 2 * in_o + en_o # = 2 * period * (observed weight), an integer
ma <- ifelse(cnt2 >= 2 * period * min_frac, (in_x + 0.5 * en_x) / (in_o + 0.5 * en_o), NA_real_)
interp_linear(ma)
}
nan_ma_direct <- function(x, period = S2, min_frac = 0.25) { # literal Eq. (3) by convolution (reference)
w <- rep(1, period + 1); w[c(1, period + 1)] <- 0.5; w <- w / period; h <- period %/% 2
conv <- function(v) as.numeric(stats::filter(c(rep(0, h), v, rep(0, h)), w, sides = 2))[(h + 1):(h + length(v))]
num <- conv(ifelse(is.na(x), 0, x)); den <- conv(as.numeric(!is.na(x)))
interp_linear(ifelse(den >= min_frac - 1e-12, num / den, NA_real_))
}
tt_demo <- seq_len(10 * S2)
x_demo <- 3 + sin(2 * pi * tt_demo / S1) + 0.5 * cos(2 * pi * tt_demo / S2)
inner <- (S2 / 2 + 1):(length(tt_demo) - S2 / 2)
cat("Check 1: max |T - 3| for a pure double-seasonal signal =", signif(max(abs(nan_ma(x_demo)[inner] - 3)), 3), "\n")
Check 1: max |T - 3| for a pure double-seasonal signal = 2.93e-14
set.seed(2); xr <- rnorm(5000); xr[sample(5000, 1500)] <- NA; xr[1000:1500] <- NA
cat("Check 2: max |fast cumulative-sum version - direct convolution| =", signif(max(abs(nan_ma(xr) - nan_ma_direct(xr))), 3), "\n")
Check 2: max |fast cumulative-sum version - direct convolution| = 1.74e-15
cat("Check 3: interp_linear(c(NA, 5, NA, NA)) =", interp_linear(c(NA, 5, NA, NA)), "\n")
Check 3: interp_linear(c(NA, 5, NA, NA)) = 5 5 5 5
Step 3 — Daily
profile by kernel smoothing (Eq. 4)
The partial residual r_t=z_t-T_t-W_t
is arranged as a matrix \mathbf V with
one row per day k and one column per
half-hour h. Each column (a
cycle-subseries) is smoothed across days with a Nadaraya–Watson
estimator with Gaussian kernel K_h(u)=\exp\{-u^2/(2h^2)\}:
\tilde
D_{k,h}=\frac{\sum_{k'}K_{h_D}(k-k')\,\delta_{k',h}\,r_{k',h}}{\sum_{k'}K_{h_D}(k-k')\,\delta_{k',h}},
\qquad D_{k,h}=\tilde D_{k,h}-\frac{1}{48}\sum_{h'=1}^{48}\tilde
D_{k,h'}. \tag{4}
In matrix form \tilde{\mathbf D}=(\mathbf
K\mathbf V_0)\oslash(\mathbf K\boldsymbol\Delta), where \mathbf K_{kk'}=K_{h_D}(k-k'), \mathbf V_0 is \mathbf V with missing entries set to 0,
\boldsymbol\Delta is the indicator
matrix of observed entries and \oslash
is element-wise division. Centring each row makes D sum to zero within every day. The bandwidth
h_D (days) controls how fast the daily
profile may change over the year; h_D\to\infty gives a static profile.
gauss_kernel <- function(n, bw) {
if (!is.finite(bw) || bw > 1e5) return(matrix(1, n, n)) # bw = Inf: equal weights (static profile)
idx <- 0:(n - 1); exp(-0.5 * (outer(idx, idx, "-") / bw)^2)
}
cycle_smooth <- function(r, period, bw) { # Eq. (4)/(5) before centring; rows = cycles
n <- length(r); nc <- ceiling(n / period)
v <- rep(NA_real_, nc * period); v[seq_len(n)] <- r
V <- matrix(v, nrow = nc, ncol = period, byrow = TRUE)
Delta <- (!is.na(V)) * 1; V0 <- V; V0[is.na(V0)] <- 0
K <- gauss_kernel(nc, bw)
num <- K %*% V0; den <- K %*% Delta
S <- num / den; S[!(den > 1e-300)] <- 0; S[is.na(S)] <- 0 # columns without data -> 0
S
}
Step 4 — Weekly
deviation (Eq. 5)
The partial residual r'_t=z_t-T_t-D_t is arranged by weeks
(rows) and the 336 half-hours of the week (columns), smoothed across
weeks with bandwidth h_W (weeks), and
centred over weekdays:
W_{\ell,(d,h)}=\tilde W_{\ell,(d,h)}-\frac17\sum_{d'=1}^{7}\tilde
W_{\ell,(d',h)}. \tag{5}
The centring makes D and W identifiable: W only carries the part of the weekly pattern
that is not common to all days. In the code, each row of 336 values is
viewed as a 48\times7 array and the
mean over the seven weekdays is subtracted at every half-hour, in one
vectorised operation.
center_weekly <- function(Sw) { # Eq. (5): subtract the mean over weekdays
nw <- nrow(Sw)
A <- array(Sw, dim = c(nw, S1, 7)) # A[week, half-hour, weekday] (columns are day-major)
M <- rowMeans(A, dims = 2) # mean over the 7 weekdays: weeks x 48
matrix(A - array(M, dim = c(nw, S1, 7)), nrow = nw, ncol = S2)
}
Step 5 —
Backfitting
Starting from T=\mathrm{MA}(z) and
D=W=0, the components are updated K=3 times: (i) D\leftarrow Eq. (4) on z-T-W; (ii) W\leftarrow Eq. (5) on z-T-D; (iii) T\leftarrow Eq. (3) on z-D-W. The remainder is R_t=z_t-T_t-D_t-W_t for t\in\mathcal O and missing on \mathcal M. Because all kernel sums use
observed values only, no pre-imputation is needed; inside a long gap the
profiles are borrowed from neighbouring days and weeks with Gaussian
weights.
dsd_decompose <- function(z, bw_day = 14, bw_week = 16, weekly = TRUE, n_iter = 3) {
n <- length(z)
Tr <- nan_ma(z, S2); D <- numeric(n); W <- numeric(n)
for (it in seq_len(n_iter)) {
Sd <- cycle_smooth(z - Tr - W, S1, bw_day) # Eq. (4), days x 48
Sd <- Sd - rowMeans(Sd) # zero mean within each day
D <- as.vector(t(Sd))[seq_len(n)]
if (weekly) W <- as.vector(t(center_weekly(cycle_smooth(z - Tr - D, S2, bw_week))))[seq_len(n)]
Tr <- nan_ma(z - D - W, S2) # Eq. (3)
}
list(trend = Tr, daily = D, weekly = W, remainder = z - Tr - D - W)
}
Step 6 — Remainder
imputation by the AR(1) bridge (Eqs. 6–7, Proposition 1)
The remainder is modelled as R_t=\phi
R_{t-1}+\varepsilon_t, with \phi
estimated from the pairs of consecutive observations \mathcal P=\{t:t\in\mathcal O,\ t-1\in\mathcal
O\}:
\tilde\phi=\frac{\sum_{t\in\mathcal P}R_tR_{t-1}}{\sum_{t\in\mathcal
P}R_{t-1}^2},\qquad
\hat\phi=\min\{0.999,\max\{0,\tilde\phi\}\}. \tag{6}
For a missing t with nearest
observed neighbours a<t<b,
\hat
R_t=\frac{\phi^{\,t-a}\big(1-\phi^{2(b-t)}\big)R_a+\phi^{\,b-t}\big(1-\phi^{2(t-a)}\big)R_b}{1-\phi^{2(b-a)}},
\tag{7}
while \hat R_t=\phi^{b-t}R_b before
the first and \hat R_t=\phi^{t-a}R_a
after the last observation (the remainder is centred by its observed
mean first).
Proposition 1. For a stationary Gaussian AR(1), (7)
equals the Kalman smoother estimate \mathrm
E[R_t\mid R_s,s\in\mathcal O], with error variance \gamma_0\big[1-\frac{\phi^{2(t-a)}+\phi^{2(b-t)}-2\phi^{2(b-a)}}{1-\phi^{2(b-a)}}\big],
\gamma_0=\mathrm{Var}(R_t). By the
Markov property, R_t depends on the
other observations only through (R_a,R_b); Gaussian conditioning with \mathrm{Cov}(R_s,R_u)=\gamma_0\phi^{|s-u|}
gives the weights \mathbf
c^\top\boldsymbol\Sigma^{-1} with \mathbf
c=\gamma_0(\phi^{t-a},\phi^{b-t})^\top and \boldsymbol\Sigma=\gamma_0\left[\begin{smallmatrix}1&\phi^{b-a}\\\phi^{b-a}&1\end{smallmatrix}\right].
For short gaps with \phi\approx1, (7)
is close to linear interpolation of the remainder; for long gaps \hat R_t\to0 and the imputation reverts to
the profile T_t+D_t+W_t. The code
verifies the mean and the variance of Proposition 1
against stats::KalmanSmooth.
estimate_ar1 <- function(R) { # Eq. (6)
a <- R[-length(R)]; b <- R[-1]; m <- !is.na(a) & !is.na(b)
if (sum(m) < 10) return(0)
min(max(sum(a[m] * b[m]) / sum(a[m]^2), 0), 0.999)
}
ar1_bridge <- function(R, phi = NULL) { # Eq. (7)
obs <- !is.na(R)
if (all(obs)) return(R)
if (!any(obs)) return(rep(0, length(R)))
if (is.null(phi)) phi <- estimate_ar1(R)
mu <- mean(R, na.rm = TRUE); Rc <- R - mu
io <- which(obs); im <- which(!obs)
k <- findInterval(im, io) # number of observed indices before t
hp <- k > 0; hn <- k < length(io) # has a previous / next observation
a <- ifelse(hp, io[pmax(k, 1)], NA_integer_)
b <- ifelse(hn, io[pmin(k + 1, length(io))], NA_integer_)
est <- numeric(length(im)); both <- hp & hn
if (any(both)) {
tt <- im[both]; aa <- a[both]; bb <- b[both]
pa <- phi^(tt - aa); pb <- phi^(bb - tt); den <- 1 - phi^(2 * (bb - aa))
est[both] <- (pa * (1 - pb^2) * Rc[aa] + pb * (1 - pa^2) * Rc[bb]) / den
}
oa <- hp & !hn; if (any(oa)) est[oa] <- phi^(im[oa] - a[oa]) * Rc[a[oa]]
ob <- !hp & hn; if (any(ob)) est[ob] <- phi^(b[ob] - im[ob]) * Rc[b[ob]]
R[im] <- est + mu
R
}
ar1_bridge_var <- function(R, phi, gamma0) { # variance in Proposition 1 (inner gaps)
io <- which(!is.na(R)); im <- which(is.na(R)); k <- findInterval(im, io)
a <- io[k]; b <- io[k + 1]; m <- b - a
gamma0 * (1 - (phi^(2 * (im - a)) + phi^(2 * (b - im)) - 2 * phi^(2 * m)) / (1 - phi^(2 * m)))
}
set.seed(1); phi0 <- 0.9
r <- as.numeric(arima.sim(list(ar = phi0), 2000)); r[c(100:180, 500:503)] <- NA
ks <- stats::KalmanSmooth(r - mean(r, na.rm = TRUE), stats::makeARIMA(phi = phi0, theta = numeric(), Delta = numeric()), nit = -1L)
cat("Proposition 1, mean: max |bridge - Kalman smoother| =",
signif(max(abs(ar1_bridge(r, phi0) - (ks$smooth[, 1] + mean(r, na.rm = TRUE)))[is.na(r)]), 3), "\n")
Proposition 1, mean: max |bridge - Kalman smoother| = 8.88e-16
cat("Proposition 1, variance: max |formula - Kalman smoother| =",
signif(max(abs(ar1_bridge_var(r, phi0, 1 / (1 - phi0^2)) - ks$var[is.na(r), 1, 1])), 3), "\n")
Proposition 1, variance: max |formula - Kalman smoother| = 2.44e-15
Step 7 —
Reconstruction (Eq. 8) and the complete function
\hat y_t=\max\Big\{0,\ m\big[\exp\big(T_t+D_t+W_t+\hat
R_t\big)-1\big]\Big\},\qquad t\in\mathcal M, \tag{8}
and the observed values are left unchanged. The arguments
transform, weekly and remainder
define the variants of the ablation study in Experiment 1. The last line
measures the computing time for one full series.
impute_seasar <- function(x, bw_day = 14, bw_week = 16, transform = "log", weekly = TRUE,
remainder = "ar1", n_iter = 3, return_components = FALSE) {
obs <- !is.na(x)
if (all(obs)) return(x)
m <- seasar_scale(x)
z <- if (transform == "log") log1p(pmax(x, 0) / m) else x / m # Eq. (1)
dc <- dsd_decompose(z, bw_day, bw_week, weekly, n_iter) # Eqs. (3)-(5)
Rh <- switch(remainder, ar1 = ar1_bridge(dc$remainder), # Eqs. (6)-(7)
linear = interp_linear(dc$remainder),
zero = ifelse(is.na(dc$remainder), 0, dc$remainder))
zh <- dc$trend + dc$daily + dc$weekly + Rh
yh <- pmax(if (transform == "log") m * expm1(zh) else m * zh, 0) # Eq. (8)
out <- x; out[!obs] <- yh[!obs]
if (return_components) return(list(imputed = out, z = z, comp = dc, remainder_imp = Rh))
out
}
x_no2 <- get_series("SUF 1", "NO2")
t_seasar <- system.time(for (i in 1:5) impute_seasar(x_no2))[["elapsed"]] / 5
cat("Seas-AR imputes the full NO2 series of SUF 1 in", round(t_seasar, 3), "s\n")
Seas-AR imputes the full NO2 series of SUF 1 in 0.03 s
Competing methods
All outputs are truncated at zero, \hat
y_t\leftarrow\max\{0,\hat y_t\}.
- MEAN, MEDIAN: \hat
y_t=\bar y_{\mathcal O} or \operatorname{median}(y_{\mathcal O})
(
imputeTS::na_mean).
- LI: linear interpolation of Step 2
(
na_interpolation).
- SS-MEAN, SS-MEDIAN, SS-LI (seasonal split, period
336): for every position p of the week,
the subseries \{y_t:(t-1)\bmod336=p-1\}
is filled with its own mean, median or linear interpolation
(
na_seasplit). Positions without any observation use the
same global statistic.
- SEADEC: classical additive decomposition with
period 48 of the interpolated series, S_t=\bar
e_{h(t)}-\frac1{48}\sum_h\bar e_h with \bar e_h the mean of y-\mathrm{MA}_{2\times48}(y) at half-hour
h; then \hat
y_t=\mathrm{LI}(y-S)_t+S_t. This is the scheme of
na_seadec, which itself uses a robust STL (evaluated
separately in Section 6.8).
- MSTL-LI: as SEADEC, with S_t=S^{(48)}_t+S^{(336)}_t from
forecast::mstl.
- KALMAN: an ARIMA(2,0,1) model for y_t-\bar y fitted by maximum likelihood (the
Kalman filter handles the missing values exactly), and \hat y_t=\mathbf
Z^\top\hat{\boldsymbol\alpha}_{t\mid n}+\bar y from the Kalman
smoother; an AR(1) is used if the optimisation fails.
impute_mean <- function(x) { x[is.na(x)] <- mean(x, na.rm = TRUE); x }
impute_median <- function(x) { x[is.na(x)] <- median(x, na.rm = TRUE); x }
impute_seasplit <- function(x, algorithm = "median", period = S2) {
x0 <- x
for (k in seq_len(period)) {
idx <- seq(k, length(x), by = period); sub <- x[idx]
if (anyNA(sub)) x[idx] <- switch(algorithm, mean = impute_mean(sub),
median = impute_median(sub), interpolation = interp_linear(sub))
}
if (anyNA(x)) { # positions without any observation: global fallback
fb <- switch(algorithm, mean = impute_mean, median = impute_median, interpolation = interp_linear)
x[is.na(x)] <- fb(x0)[is.na(x)]
}
x
}
impute_seadec <- function(x, period = S1) { # classical additive decomposition
seas <- as.numeric(stats::decompose(ts(interp_linear(x), frequency = period))$seasonal)
xd <- interp_linear(x - seas); x[is.na(x)] <- (xd + seas)[is.na(x)]; x
}
impute_seadec_stl <- function(x, period = S1) { # replica of imputeTS::na_seadec (robust STL)
seas <- as.numeric(stats::stl(ts(interp_linear(x), frequency = period), robust = TRUE,
s.window = 11)$time.series[, "seasonal"])
xd <- interp_linear(x - seas); x[is.na(x)] <- (xd + seas)[is.na(x)]; x
}
impute_mstl_li <- function(x) {
fit <- forecast::mstl(forecast::msts(interp_linear(x), seasonal.periods = c(S1, S2)))
seas <- rowSums(fit[, grep("Seasonal", colnames(fit)), drop = FALSE])
xd <- interp_linear(x - seas); x[is.na(x)] <- (xd + seas)[is.na(x)]; x
}
impute_kalman <- function(x, order = c(2, 0, 1)) {
mu <- mean(x, na.rm = TRUE); x0 <- x - mu
fit <- tryCatch(stats::arima(x0, order = order, include.mean = FALSE, method = "ML"),
error = function(e) stats::arima(x0, order = c(1, 0, 0), include.mean = FALSE, method = "ML"))
sm <- stats::KalmanSmooth(x0, fit$model, nit = -1L)$smooth
yhat <- as.numeric(sm %*% fit$model$Z) + mu
x[is.na(x)] <- yhat[is.na(x)]; x
}
nonneg <- function(f) function(x) pmax(f(x), 0)
METHOD_ORDER <- c("MEAN", "MEDIAN", "LI", "SS-MEAN", "SS-MEDIAN", "SS-LI", "SEADEC", "MSTL-LI", "KALMAN", "Seas-AR")
METHODS <- lapply(list(MEAN = impute_mean, MEDIAN = impute_median, LI = interp_linear,
`SS-MEAN` = function(x) impute_seasplit(x, "mean"), `SS-MEDIAN` = function(x) impute_seasplit(x, "median"),
`SS-LI` = function(x) impute_seasplit(x, "interpolation"), SEADEC = impute_seadec,
`MSTL-LI` = impute_mstl_li, KALMAN = impute_kalman, `Seas-AR` = impute_seasar), nonneg)
Validation against imputeTS. In
imputeTS the seasonal-split median is requested with
algorithm = "mean", option = "median". The last row is
non-zero by design (classical decomposition versus robust STL).
if (HAS_IMPUTETS) {
xs1 <- ts(x_no2, frequency = S1); xs2 <- ts(x_no2, frequency = S2)
chk <- function(a, b) signif(max(abs(a - as.numeric(b))), 3)
knitr::kable(data.frame(
comparison = c("LI vs na_interpolation", "SS-MEAN vs na_seasplit(algorithm = 'mean')",
"SS-MEDIAN vs na_seasplit(algorithm = 'mean', option = 'median')",
"SS-LI vs na_seasplit(algorithm = 'interpolation')",
"robust-STL replica vs na_seadec", "SEADEC (classical decomposition) vs na_seadec"),
max_abs_diff = c(
chk(interp_linear(x_no2), imputeTS::na_interpolation(x_no2)),
chk(impute_seasplit(x_no2, "mean"), imputeTS::na_seasplit(xs2, algorithm = "mean")),
chk(impute_seasplit(x_no2, "median"), imputeTS::na_seasplit(xs2, algorithm = "mean", option = "median")),
chk(impute_seasplit(x_no2, "interpolation"), imputeTS::na_seasplit(xs2, algorithm = "interpolation")),
chk(impute_seadec_stl(x_no2), imputeTS::na_seadec(xs1, algorithm = "interpolation")),
chk(impute_seadec(x_no2), imputeTS::na_seadec(xs1, algorithm = "interpolation")))),
caption = "Agreement with imputeTS (0 = identical; the last row differs by design)")
} else cat("imputeTS is not installed; check skipped.\n")
Agreement with imputeTS (0 = identical; the last row differs by
design)
| comparison |
max_abs_diff |
| LI vs na_interpolation |
0 |
| SS-MEAN vs na_seasplit(algorithm = ‘mean’) |
0 |
| SS-MEDIAN vs na_seasplit(algorithm = ‘mean’, option =
‘median’) |
0 |
| SS-LI vs na_seasplit(algorithm = ‘interpolation’) |
0 |
| robust-STL replica vs na_seadec |
0 |
| SEADEC (classical decomposition) vs na_seadec |
21 |
Experiment 0: bandwidth
selection
For a target station s^* the
bandwidths are chosen on the other two stations:
(\hat h_D,\hat h_W)_{s^*}=\arg\min_{h_D\in\{3.5,7,14,28\},\
h_W\in\{2,4,8,16\}}\
\frac{1}{|\mathcal S_{-s^*}|}\sum_{(s,p,r,i)\in\mathcal
S_{-s^*}}\mathrm{NRMSE}_{s,p,r,i}(h_D,h_W),
where \mathcal S_{-s^*} contains the
scenarios of the two other stations (5 pollutants × 3 regimes × 2
replicates, \rho=20\%). SUF 1 never
influences its own bandwidths.
BW_DAY <- if (QUICK) c(7, 14) else c(3.5, 7, 14, 28)
BW_WEEK <- if (QUICK) c(4, 16) else c(2, 4, 8, 16)
dir0 <- out_file(paste0("exp0_", TAG0, if (QUICK) "_quick" else "")); dir.create(dir0, showWarnings = FALSE)
for (si in seq_along(STATIONS)) for (pk in seq_along(POLS)) {
st <- STATIONS[si]; p <- POLS[pk]; f <- file.path(dir0, sprintf("%s_%s.csv", gsub(" ", "", st), p))
if (isTRUE(P$use_cache) && file.exists(f)) next
x <- get_series(st, p); sdx <- sd_pop(x); rows <- list()
for (ri in seq_along(REG_NAMES)) for (rep in seq_len(if (QUICK) 1 else 2)) {
m <- get_mask("exp0", x, st, p, REG_NAMES[ri], 0.20, rep, seed = 10000 + 100 * si + 10 * pk + 3 * ri + rep)
xm <- x; xm[m] <- NA
for (bd in BW_DAY) for (bw in BW_WEEK) {
y <- impute_seasar(xm, bw_day = bd, bw_week = bw)
rows[[length(rows) + 1]] <- data.frame(station = st, pollutant = p, regime = REG_NAMES[ri], rep = rep,
bw_day = bd, bw_week = bw, nrmse = rmse(y[m], x[m]) / sdx)
}
}
write.csv(bind_rows(rows), f, row.names = FALSE)
}
grid <- bind_rows(lapply(list.files(dir0, pattern = "\\.csv$", full.names = TRUE), read.csv))
BW <- lapply(setNames(STATIONS, STATIONS), function(st) {
g <- grid |> filter(station != st) |> group_by(bw_day, bw_week) |>
summarise(nrmse = mean(nrmse), .groups = "drop") |> arrange(bw_day, bw_week) |>
slice_min(nrmse, n = 1, with_ties = FALSE)
list(bw_day = g$bw_day, bw_week = g$bw_week, nrmse = g$nrmse)
})
cat("Experiment 0:", nrow(grid), "grid evaluations\n")
Experiment 0: 1440 grid evaluations
for (st in STATIONS) { key(paste0("bw_day_", gsub(" ", "", st)), BW[[st]]$bw_day); key(paste0("bw_week_", gsub(" ", "", st)), BW[[st]]$bw_week) }
g4 <- grid |> filter(station != "SUF 1") |> group_by(bw_day, bw_week) |>
summarise(nrmse = mean(nrmse), .groups = "drop") |> mutate(best = nrmse == min(nrmse))
key("grid_best_nrmse", min(g4$nrmse)); key("grid_worst_nrmse", max(g4$nrmse))
key("grid_worst_vs_best_pct", 100 * (max(g4$nrmse) / min(g4$nrmse) - 1))
key("grid_worst_pair", paste(g4$bw_day[which.max(g4$nrmse)], g4$bw_week[which.max(g4$nrmse)]))
p4 <- ggplot(g4, aes(factor(bw_week), factor(bw_day), fill = nrmse)) + geom_tile(colour = "white") +
geom_text(aes(label = sprintf("%.4f", nrmse), fontface = ifelse(best, "bold", "plain"),
colour = nrmse < mean(range(nrmse))), size = 2.65, family = FONT_TEXT, show.legend = FALSE) +
scale_colour_manual(values = c(`TRUE` = "white", `FALSE` = "black")) +
scale_fill_distiller(palette = "Blues", direction = -1) + scale_y_discrete(limits = rev) +
labs(x = expression(h[W] ~ "(weeks)"), y = expression(h[D] ~ "(days)"), fill = "Mean\nNRMSE") +
theme(panel.grid = element_blank(), legend.key.height = unit(0.5, "cm"), axis.text = element_text(size = 7.5),
axis.title = element_text(size = 9), legend.text = element_text(size = 7), legend.title = element_text(size = 8))
print(p4); save_fig(p4, "fig04_bandwidth", 3.5, 2.45)

knitr::kable(data.frame(station = STATIONS, h_D = sapply(BW, `[[`, "bw_day"), h_W = sapply(BW, `[[`, "bw_week"),
nrmse = sapply(BW, `[[`, "nrmse")), digits = 4,
col.names = c("Target station", "h_D (days)", "h_W (weeks)", "Mean NRMSE on the other stations"),
caption = "Selected bandwidths (leave-one-station-out)")
Selected bandwidths (leave-one-station-out)
|
Target station |
h_D (days) |
h_W (weeks) |
Mean NRMSE on the other stations |
| SUF 1 |
SUF 1 |
14 |
16 |
0.5128 |
| SUF 6 |
SUF 6 |
14 |
16 |
0.5371 |
| SUF 7 |
SUF 7 |
14 |
16 |
0.5268 |
Experiment 1:
imputation accuracy
Running the
experiment
For every scenario the ten methods, the five ablation variants of
Seas-AR and, when imputeTS is installed, the original
na_seadec are applied to the same masked series, and the
computing time is recorded. Seas-AR uses the bandwidths of Experiment 0
for the station concerned. Results are stored per series, so an
interrupted run resumes where it stopped.
RATES <- if (QUICK) 0.30 else c(0.10, 0.30)
N_REPS <- if (QUICK) 1 else 3
st_list <- if (QUICK) "SUF 1" else STATIONS
dir1 <- out_file(paste0("exp1_", TAG1, if (QUICK) "_quick" else "")); dir.create(dir1, showWarnings = FALSE)
for (st in st_list) for (p in POLS) {
f <- file.path(dir1, sprintf("%s_%s.csv", gsub(" ", "", st), p))
if (isTRUE(P$use_cache) && file.exists(f)) next
si <- match(st, STATIONS); pk <- match(p, POLS)
x <- get_series(st, p); sdx <- sd_pop(x); bw <- BW[[st]]
meths <- METHODS
meths$`Seas-AR` <- function(v) impute_seasar(v, bw_day = bw$bw_day, bw_week = bw$bw_week)
abl <- list(
`w/o log transform` = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, transform = "none"),
`w/o weekly comp.` = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, weekly = FALSE),
`static profiles` = function(v) impute_seasar(v, Inf, Inf),
`remainder = 0` = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, remainder = "zero"),
`remainder LI` = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, remainder = "linear"))
extra <- if (HAS_IMPUTETS) list(`NA_SEADEC (imputeTS)` = function(v)
as.numeric(imputeTS::na_seadec(ts(v, frequency = S1), algorithm = "interpolation"))) else list()
allf <- c(meths, abl, extra)
kinds <- c(rep("main", length(meths)), rep("ablation", length(abl)), rep("extra", length(extra)))
rows <- list()
for (ri in seq_along(REG_NAMES)) for (rate in RATES) for (rep in seq_len(N_REPS)) {
seed <- 20000 + 1000 * si + 100 * pk + 10 * ri + 3 * match(rate, c(0.10, 0.30)) + rep
m <- get_mask("exp1", x, st, p, REG_NAMES[ri], rate, rep, seed); xm <- x; xm[m] <- NA
for (j in seq_along(allf)) {
t1 <- proc.time()[["elapsed"]]; y <- pmax(allf[[j]](xm), 0); dt <- proc.time()[["elapsed"]] - t1
rows[[length(rows) + 1]] <- data.frame(station = st, pollutant = p, regime = REG_NAMES[ri], rate = rate,
rep = rep, method = names(allf)[j], kind = kinds[j], n_masked = sum(m),
rmse = rmse(y[m], x[m]), mae = mae(y[m], x[m]), nrmse = rmse(y[m], x[m]) / sdx, sec = dt)
}
}
write.csv(bind_rows(rows), f, row.names = FALSE)
}
res1 <- bind_rows(lapply(list.files(dir1, pattern = "\\.csv$", full.names = TRUE), read.csv))
main <- res1 |> filter(kind == "main") |>
mutate(scenario = paste(station, pollutant, regime, rate, rep, sep = "|"), method = factor(method, levels = METHOD_ORDER))
cat("Experiment 1:", n_distinct(main$scenario), "scenarios,", nrow(res1), "result rows\n")
Experiment 1: 270 scenarios, 4320 result rows
Accuracy by
method
Left: RMSE at SUF 1 in original units (mean of 18 scenarios per
pollutant). Middle: NRMSE averaged over the 15 series for each gap
regime. Right: average Friedman rank and mean computing time per
series.
Pw <- main |> select(scenario, method, nrmse) |> pivot_wider(names_from = method, values_from = nrmse)
Pm <- as.matrix(Pw[, METHOD_ORDER])
avg_rank <- colMeans(t(apply(Pm, 1, rank))) # rank 1 = most accurate
t4 <- data.frame(method = factor(METHOD_ORDER, levels = METHOD_ORDER)) |>
left_join(main |> filter(station == "SUF 1") |> group_by(method, pollutant) |> summarise(v = mean(rmse), .groups = "drop") |>
pivot_wider(names_from = pollutant, values_from = v), by = "method") |>
left_join(main |> group_by(method, regime) |> summarise(v = mean(nrmse), .groups = "drop") |>
pivot_wider(names_from = regime, values_from = v), by = "method") |>
left_join(main |> group_by(method) |> summarise(all = mean(nrmse), sec = mean(sec), .groups = "drop"), by = "method")
t4$rank <- avg_rank[as.character(t4$method)]
write.csv(t4, out_file("exp1_table4.csv"), row.names = FALSE)
pc <- intersect(POLS, names(t4)); rc <- intersect(REG_NAMES, names(t4))
md4 <- data.frame(Method = as.character(t4$method), check.names = FALSE)
for (p in pc) md4[[p]] <- fmt_md(t4[[p]], if (p == "CO") 3 else 2)
for (r in rc) md4[[paste("NRMSE", r)]] <- fmt_md(t4[[r]], 3)
md4[["Avg. rank"]] <- fmt_md(t4$rank, 2); md4[["Time (s)"]] <- fmt_num(t4$sec, 3)
tx <- lapply(pc, function(p) fmt_tex(t4[[p]], if (p == "CO") 3 else 2)); tr <- lapply(rc, function(r) fmt_tex(t4[[r]], 3))
rows4 <- sapply(seq_len(nrow(t4)), function(i) paste0(paste(c(
if (t4$method[i] == "Seas-AR") "\\textbf{Seas-AR (proposed)}" else as.character(t4$method[i]),
sapply(tx, `[`, i), sapply(tr, `[`, i), fmt_tex(t4$rank, 2)[i], fmt_num(t4$sec[i], 3)), collapse = " & "), " \\\\"))
write_tex(c("\\begin{table*}[!t]", "\\centering",
sprintf("\\caption{Imputation accuracy under artificial block missingness. Left: RMSE at SUF~1 in original units, averaged over the three gap regimes, two missing rates and three replicates. Middle: NRMSE (RMSE divided by the standard deviation of the series) averaged over all 15 series (three stations $\\times$ five pollutants) for each gap regime. Right: average Friedman rank over the %d scenarios and mean computing time per imputation of a full series (R, one CPU core). Best values in bold.}", nrow(Pm)),
"\\label{tab:imp}", "\\setlength{\\tabcolsep}{4.2pt}", "\\begin{tabular}{lrrrrrrrrrr}", "\\toprule",
" & \\multicolumn{5}{c}{RMSE at SUF~1} & \\multicolumn{3}{c}{NRMSE, all stations} & & \\\\",
"\\cmidrule(lr){2-6}\\cmidrule(lr){7-9}",
"Method & PM$_{10}$ & NO$_2$ & SO$_2$ & CO & O$_3$ & Short & Medium & Long & Avg. rank & Time (s)\\\\", "\\midrule",
rows4, "\\bottomrule", "\\end{tabular}", "\\end{table*}"), "tab_imputation")
knitr::kable(md4, align = "r", caption = cap("Table 5", "RMSE at SUF 1, NRMSE of all stations by gap regime, average rank and time"))
RMSE at SUF 1, NRMSE of all stations by gap regime, average
rank and time
| Method |
PM10 |
NO2 |
SO2 |
CO |
O3 |
NRMSE short |
NRMSE medium |
NRMSE long |
Avg. rank |
Time (s) |
| MEAN |
35.24 |
17.02 |
26.75 |
0.501 |
30.43 |
0.990 |
0.966 |
1.021 |
8.08 |
0.000 |
| MEDIAN |
37.16 |
17.89 |
26.89 |
0.531 |
31.28 |
1.027 |
0.993 |
1.059 |
9.16 |
0.000 |
| LI |
21.00 |
12.16 |
27.07 |
0.346 |
13.36 |
0.300 |
0.737 |
1.034 |
5.10 |
0.000 |
| SS-MEAN |
36.36 |
16.27 |
26.12 |
0.473 |
28.52 |
0.899 |
0.877 |
0.935 |
6.38 |
0.002 |
| SS-MEDIAN |
37.17 |
16.95 |
26.21 |
0.502 |
28.51 |
0.923 |
0.898 |
0.960 |
7.33 |
0.004 |
| SS-LI |
33.36 |
15.20 |
26.28 |
0.425 |
17.41 |
0.837 |
0.827 |
0.982 |
6.63 |
0.005 |
| SEADEC |
21.02 |
11.06 |
27.16 |
0.306 |
10.42 |
0.292 |
0.626 |
0.895 |
2.95 |
0.004 |
| MSTL-LI |
21.36 |
10.68 |
27.17 |
0.310 |
9.36 |
0.305 |
0.622 |
0.879 |
3.44 |
0.021 |
| KALMAN |
22.06 |
11.52 |
21.17 |
0.337 |
13.03 |
0.293 |
0.673 |
0.924 |
3.63 |
0.080 |
| Seas-AR |
20.68 |
9.17 |
20.36 |
0.265 |
8.41 |
0.312 |
0.529 |
0.698 |
2.30 |
0.023 |
NRMSE by gap regime
and missing rate
Each row is a method (ordered by overall NRMSE, best at the top),
each panel a gap regime; open and filled circles are the 10% and 30%
missing rates, and Seas-AR is drawn in red.
ord5 <- main |> group_by(method) |> summarise(v = mean(nrmse), .groups = "drop") |> arrange(desc(v)) |> pull(method)
d5 <- main |> group_by(method, regime, rate) |> summarise(nrmse = mean(nrmse), .groups = "drop") |>
mutate(regime = factor(regime, levels = REG_NAMES, labels = c("Short gaps (1-4 steps)", "Medium gaps (5-48 steps)", "Long gaps (49-336 steps)")),
rate = factor(sprintf("%d%%", round(100 * rate))), method = factor(method, levels = as.character(ord5)),
seasar = method == "Seas-AR")
p5 <- ggplot(d5, aes(nrmse, method)) +
geom_line(aes(group = method), colour = "grey75", linewidth = 0.5) +
geom_point(aes(shape = rate, colour = seasar), size = 1.7, stroke = 0.6) +
scale_colour_manual(values = c(`FALSE` = "grey20", `TRUE` = "#c0392b"), guide = "none") +
scale_shape_manual(values = c(1, 16), name = "Additional missing rate") +
facet_wrap(~regime, nrow = 1) + labs(x = "NRMSE (mean over series)", y = NULL) +
theme(legend.position = "bottom", panel.grid.major.y = element_line(colour = "grey92"))
print(p5); save_fig(p5, "fig05_accuracy_by_regime", 7.16, 2.6)

Ranks, Friedman test
and Nemenyi critical difference
Within each scenario j the methods
receive ranks r_{ij} (1 = smallest
NRMSE) and \bar r_i=\frac1N\sum_j
r_{ij}. The Friedman test checks H_0: all methods are equivalent. Two average
ranks differ significantly (Nemenyi) when their difference exceeds
\mathrm{CD}=q_{\alpha}\sqrt{\frac{k(k+1)}{6N}},\qquad k=10,\ N=270,\
q_{0.05}=3.164 .
fr <- friedman.test(Pm)
k_ <- ncol(Pm); N_ <- nrow(Pm); CD <- 3.164 * sqrt(k_ * (k_ + 1) / (6 * N_)) # q_0.05 = 3.164 for k = 10
for (mm in names(avg_rank)) key(paste0("rank_", mm), avg_rank[[mm]])
key("friedman_chi2", unname(fr$statistic)); key("friedman_p", fr$p.value); key("nemenyi_cd", CD); key("n_scenarios", N_)
cat(sprintf("Friedman chi^2(%d) = %.1f, p = %.2g | Nemenyi CD = %.3f (k = %d, N = %d)\n", k_ - 1, fr$statistic, fr$p.value, CD, k_, N_))
Friedman chi^2(9) = 1474.0, p = 8e-312 | Nemenyi CD = 0.824 (k = 10, N = 270)
d6 <- data.frame(method = names(avg_rank), rank = avg_rank) |> arrange(rank) |> mutate(y = rev(seq_along(rank)))
p6 <- ggplot(d6, aes(rank, y)) +
annotate("rect", xmin = min(d6$rank), xmax = min(d6$rank) + CD, ymin = -Inf, ymax = Inf, fill = "#c0392b", alpha = .08) +
annotate("segment", x = min(d6$rank), xend = min(d6$rank) + CD, y = nrow(d6) + .8, yend = nrow(d6) + .8, linewidth = 0.8) +
annotate("text", x = min(d6$rank), y = nrow(d6) + 1.35, label = sprintf("CD = %.2f", CD), hjust = 0, size = 2.4, family = FONT_TEXT) +
geom_point(aes(colour = method), size = 1.8, show.legend = FALSE) +
geom_text(aes(label = sprintf("%s (%.2f)", method, rank)), hjust = -0.15, size = 2.4, family = FONT_TEXT) +
scale_colour_manual(values = COL) + scale_x_continuous(limits = c(1, 12), breaks = seq(2, 10, 2)) +
scale_y_continuous(limits = c(0.5, nrow(d6) + 1.6)) +
labs(x = "Average rank over the imputation scenarios (1 = best)", y = NULL) +
theme(axis.text.y = element_blank(), panel.grid.major.y = element_blank())
print(p6); save_fig(p6, "fig06_ranks", 3.5, 2.4)

Paired Wilcoxon tests
with Holm correction
For each competitor c the
differences \Delta_j=\mathrm{NRMSE}_j(\mathrm{Seas-AR})-\mathrm{NRMSE}_j(c)
are tested with the two-sided Wilcoxon signed-rank test (normal
approximation without continuity correction), and the nine p-values are adjusted with Holm’s method.
Negative \Delta favours Seas-AR.
t5 <- bind_rows(lapply(setdiff(METHOD_ORDER, "Seas-AR"), function(cc) {
d <- Pm[, "Seas-AR"] - Pm[, cc]
data.frame(competitor = cc, median_diff = median(d), wins = 100 * mean(d < 0),
p = wilcox.test(Pm[, "Seas-AR"], Pm[, cc], paired = TRUE, exact = FALSE, correct = FALSE)$p.value)
}))
t5$p_holm <- p.adjust(t5$p, method = "holm")
write.csv(t5, out_file("exp1_tests.csv"), row.names = FALSE)
ptxt <- function(p) ifelse(p < 1e-4, "$<10^{-4}$", formatC(p, format = "f", digits = 4))
write_tex(c("\\begin{table}[!t]", "\\centering",
sprintf("\\caption{Paired comparison of Seas-AR with each competing imputation method over the %d masking scenarios: median NRMSE difference (negative favours Seas-AR), percentage of scenarios in which Seas-AR is more accurate, and Holm-adjusted Wilcoxon signed-rank $p$-values. Friedman test: $\\chi^2_{%d}=%.1f$, $p<10^{-4}$.}", N_, k_ - 1, fr$statistic),
"\\label{tab:tests}", "\\setlength{\\tabcolsep}{5pt}", "\\begin{tabular}{lrrr}", "\\toprule",
"Competitor & Median $\\Delta$NRMSE & Seas-AR wins (\\%) & $p_{\\mathrm{Holm}}$\\\\", "\\midrule",
sprintf("%s & %+.4f & %.1f & %s \\\\", t5$competitor, t5$median_diff, t5$wins, ptxt(t5$p_holm)),
"\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_tests")
for (i in seq_len(nrow(t5))) { key(paste0("wil_meddiff_", t5$competitor[i]), t5$median_diff[i]); key(paste0("wil_wins_", t5$competitor[i]), t5$wins[i]) }
knitr::kable(t5 |> transmute(Competitor = competitor, `Median ΔNRMSE` = median_diff, `Seas-AR wins (%)` = wins,
`p (Holm)` = ifelse(p_holm < 1e-4, "< 1e-4", formatC(p_holm, format = "g", digits = 3))),
digits = 4, caption = cap("Table 6", "Seas-AR versus each competitor (Wilcoxon signed-rank test, Holm correction)"))
Seas-AR versus each competitor (Wilcoxon signed-rank test, Holm
correction)
| Competitor |
Median ΔNRMSE |
Seas-AR wins (%) |
p (Holm) |
| MEAN |
-0.4537 |
97.4074 |
< 1e-4 |
| MEDIAN |
-0.4797 |
98.1481 |
< 1e-4 |
| LI |
-0.1262 |
73.7037 |
< 1e-4 |
| SS-MEAN |
-0.3794 |
97.4074 |
< 1e-4 |
| SS-MEDIAN |
-0.4021 |
97.4074 |
< 1e-4 |
| SS-LI |
-0.3466 |
99.2593 |
< 1e-4 |
| SEADEC |
-0.0487 |
66.6667 |
< 1e-4 |
| MSTL-LI |
-0.0503 |
71.4815 |
< 1e-4 |
| KALMAN |
-0.0733 |
68.1481 |
< 1e-4 |
Gap length, missing
rate and station
reg_tab <- main |> group_by(regime, method) |> summarise(nrmse = mean(nrmse), .groups = "drop")
rel <- bind_rows(lapply(REG_NAMES, function(r) {
g <- reg_tab |> filter(regime == r); comp <- g |> filter(method != "Seas-AR") |> slice_min(nrmse, n = 1)
seasar <- g$nrmse[g$method == "Seas-AR"]; sel <- grepl(paste0("\\|", r, "\\|"), Pw$scenario)
data.frame(regime = r, best = as.character(comp$method), nrmse_best = comp$nrmse, nrmse_seasar = seasar,
rel_pct = 100 * (seasar / comp$nrmse - 1),
beats_kalman = 100 * mean(Pm[sel, "Seas-AR"] < Pm[sel, "KALMAN"]), beats_li = 100 * mean(Pm[sel, "Seas-AR"] < Pm[sel, "LI"]),
beats_seadec = 100 * mean(Pm[sel, "Seas-AR"] < Pm[sel, "SEADEC"]))
}))
write.csv(rel, out_file("exp1_regime.csv"), row.names = FALSE)
for (i in seq_len(nrow(rel))) for (cc in names(rel)[-1]) key(paste0("regime_", rel$regime[i], "_", cc), rel[[cc]][i])
by_rate <- main |> filter(method == "Seas-AR") |> group_by(rate) |> summarise(nrmse = mean(nrmse))
key("seasar_nrmse_rate10", by_rate$nrmse[by_rate$rate == 0.1]); key("seasar_nrmse_rate30", by_rate$nrmse[by_rate$rate == 0.3])
st_rank <- sapply(STATIONS, function(st) { sel <- startsWith(Pw$scenario, paste0(st, "|"))
if (!any(sel)) return(rep(NA, length(METHOD_ORDER))); colMeans(t(apply(Pm[sel, , drop = FALSE], 1, rank))) })
rownames(st_rank) <- METHOD_ORDER
for (st in STATIONS) key(paste0("seasar_rank_", gsub(" ", "", st)), st_rank["Seas-AR", st])
suf <- t4 |> filter(method != "Seas-AR")
for (p in pc) {
b <- suf$method[which.min(suf[[p]])]; key(paste0("suf1_best_", p), as.character(b)); key(paste0("suf1_bestval_", p), min(suf[[p]]))
key(paste0("suf1_seasar_", p), t4[[p]][t4$method == "Seas-AR"]); key(paste0("suf1_rel_", p), 100 * (1 - t4[[p]][t4$method == "Seas-AR"] / min(suf[[p]])))
key(paste0("suf1_vs_ssmedian_", p), 100 * (1 - t4[[p]][t4$method == "Seas-AR"] / t4[[p]][t4$method == "SS-MEDIAN"]))
}
knitr::kable(rel, digits = c(0, 0, 4, 4, 1, 1, 1, 1),
col.names = c("Gap regime", "Best competitor", "NRMSE best competitor", "NRMSE Seas-AR", "Seas-AR vs best (%)",
"Seas-AR beats KALMAN (%)", "Seas-AR beats LI (%)", "Seas-AR beats SEADEC (%)"),
caption = "Seas-AR versus the best competitor in each gap regime")
Seas-AR versus the best competitor in each gap regime
| Gap regime |
Best competitor |
NRMSE best competitor |
NRMSE Seas-AR |
Seas-AR vs best (%) |
Seas-AR beats KALMAN (%) |
Seas-AR beats LI (%) |
Seas-AR beats SEADEC (%) |
| short |
SEADEC |
0.2918 |
0.3121 |
7.0 |
25.6 |
40.0 |
18.9 |
| medium |
MSTL-LI |
0.6221 |
0.5293 |
-14.9 |
93.3 |
93.3 |
92.2 |
| long |
MSTL-LI |
0.8787 |
0.6983 |
-20.5 |
85.6 |
87.8 |
88.9 |
knitr::kable(data.frame(method = METHOD_ORDER, st_rank, check.names = FALSE), digits = 2, caption = "Average rank per station")
Average rank per station
|
method |
SUF 1 |
SUF 6 |
SUF 7 |
| MEAN |
MEAN |
8.06 |
7.96 |
8.23 |
| MEDIAN |
MEDIAN |
9.30 |
9.18 |
8.99 |
| LI |
LI |
4.48 |
5.34 |
5.47 |
| SS-MEAN |
SS-MEAN |
7.10 |
6.03 |
6.00 |
| SS-MEDIAN |
SS-MEDIAN |
8.10 |
7.02 |
6.87 |
| SS-LI |
SS-LI |
6.07 |
7.13 |
6.70 |
| SEADEC |
SEADEC |
3.07 |
2.84 |
2.94 |
| MSTL-LI |
MSTL-LI |
3.39 |
3.42 |
3.50 |
| KALMAN |
KALMAN |
3.33 |
3.60 |
3.97 |
| Seas-AR |
Seas-AR |
2.11 |
2.47 |
2.33 |
Ablation study
Each variant removes one component of Seas-AR: no log transform
(z=y/m), daily component only (W_t\equiv0), static profiles (h_D=h_W=\infty), no remainder model (\hat R_t=0), and linear interpolation of the
remainder instead of the AR(1) bridge.
ABL_ORDER <- c("Seas-AR", "w/o log transform", "w/o weekly comp.", "static profiles", "remainder = 0", "remainder LI")
ABL_TEX <- c(`Seas-AR` = "Full Seas-AR", `w/o log transform` = "no log transform ($z=y/m$)",
`w/o weekly comp.` = "daily component only ($W_t\\equiv0$)", `static profiles` = "static profiles ($h_D=h_W=\\infty$)",
`remainder = 0` = "no remainder model ($\\hat R_t=0$)", `remainder LI` = "remainder by linear interpolation")
ab <- res1 |> filter(kind == "ablation" | method == "Seas-AR")
t6 <- ab |> group_by(method, regime) |> summarise(v = mean(nrmse), .groups = "drop") |>
pivot_wider(names_from = regime, values_from = v) |>
left_join(ab |> group_by(method) |> summarise(All = mean(nrmse)), by = "method") |>
mutate(method = factor(method, levels = ABL_ORDER)) |> arrange(method)
write.csv(t6, out_file("exp1_ablation.csv"), row.names = FALSE)
cc6 <- c(intersect(REG_NAMES, names(t6)), "All")
for (i in seq_len(nrow(t6))) for (cc in cc6) key(paste0("abl_", gsub("[^A-Za-z0-9]", "", as.character(t6$method[i])), "_", cc), t6[[cc]][i])
tx6 <- lapply(cc6, function(cc) fmt_tex(t6[[cc]], 4))
write_tex(c("\\begin{table}[!t]", "\\centering",
"\\caption{Ablation study: mean NRMSE over the 15 series (three stations $\\times$ five pollutants) by gap regime. Best values in bold.}",
"\\label{tab:ablation}", "\\setlength{\\tabcolsep}{3.5pt}", "\\begin{tabular}{lrrrr}", "\\toprule",
"Variant & Short & Medium & Long & All\\\\", "\\midrule",
sapply(seq_len(nrow(t6)), function(i) paste0(paste(c(ABL_TEX[[as.character(t6$method[i])]], sapply(tx6, `[`, i)), collapse = " & "), " \\\\")),
"\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_ablation")
md6 <- data.frame(Variant = as.character(t6$method), check.names = FALSE)
for (cc in cc6) md6[[cc]] <- fmt_md(t6[[cc]], 4)
knitr::kable(md6, align = "r", caption = cap("Table 7", "Ablation study (mean NRMSE)"))
Ablation study (mean NRMSE)
| Variant |
short |
medium |
long |
All |
| Seas-AR |
0.3121 |
0.5293 |
0.6983 |
0.5132 |
| w/o log transform |
0.3162 |
0.5364 |
0.7017 |
0.5181 |
| w/o weekly comp. |
0.2955 |
0.5213 |
0.6908 |
0.5025 |
| static profiles |
0.2974 |
0.5564 |
0.7401 |
0.5313 |
| remainder = 0 |
0.6337 |
0.6261 |
0.7215 |
0.6604 |
| remainder LI |
0.3088 |
0.5569 |
0.8070 |
0.5575 |
Additional check: the
original imputeTS::na_seadec
if (any(res1$kind == "extra")) {
ex <- res1 |> filter(kind == "extra") |> mutate(scenario = paste(station, pollutant, regime, rate, rep, sep = "|"))
jj <- ex |> select(scenario, ex = nrmse) |>
inner_join(main |> filter(method == "Seas-AR") |> select(scenario, seasar = nrmse), by = "scenario") |>
inner_join(main |> filter(method == "SEADEC") |> select(scenario, seadec = nrmse), by = "scenario")
wx <- wilcox.test(jj$seasar, jj$ex, paired = TRUE, exact = FALSE, correct = FALSE)
key("naseadec_nrmse", mean(jj$ex)); key("seadec_nrmse", mean(jj$seadec)); key("seasar_nrmse", mean(jj$seasar))
key("naseadec_seasar_wins", 100 * mean(jj$seasar < jj$ex)); key("naseadec_p", wx$p.value)
cat(sprintf("Mean NRMSE: na_seadec (robust STL) = %.4f | SEADEC (classical) = %.4f | Seas-AR = %.4f\n", mean(jj$ex), mean(jj$seadec), mean(jj$seasar)))
cat(sprintf("Seas-AR is more accurate than na_seadec in %.1f%% of the scenarios (Wilcoxon p = %.2g)\n", 100 * mean(jj$seasar < jj$ex), wx$p.value))
} else cat("imputeTS is not available; check skipped.\n")
Mean NRMSE: na_seadec (robust STL) = 0.6126 | SEADEC (classical) = 0.6041 | Seas-AR = 0.5132
Seas-AR is more accurate than na_seadec in 83.7% of the scenarios (Wilcoxon p = 2.1e-28)
Illustration of a
removed block
A four-day NO_2 block is chosen as
the block starting at 00:30, between 7 May and the end of October, with
the fewest missing values. It is removed and imputed by five methods.
Each panel shows the true series (grey) and one imputation (colour)
inside the shaded gap, with the RMSE over the block.
x7 <- get_series("SUF 1", "NO2"); L7 <- 48 * 4; ctx <- 48 * 2
i0 <- which(TS >= as.POSIXct("2018-05-07 00:30", tz = "UTC"))[1] - 1 # 0-based start
cand <- seq(i0, 304 * 48 - L7 - ctx - 1, by = 48)
i0 <- cand[which.min(sapply(cand, function(k) sum(is.na(x7[(k + 1):(k + L7)]))))]
blk <- (i0 + 1):(i0 + L7); xm7 <- x7; xm7[blk] <- NA; okb <- !is.na(x7[blk]); sl <- (i0 + 1 - ctx):(i0 + L7 + ctx)
M7 <- c("LI", "KALMAN", "SEADEC", "MSTL-LI", "Seas-AR")
f7 <- c(METHODS[c("LI", "KALMAN", "SEADEC", "MSTL-LI")],
list(`Seas-AR` = function(v) impute_seasar(v, BW[["SUF 1"]]$bw_day, BW[["SUF 1"]]$bw_week)))
imp7 <- lapply(f7, function(f) f(xm7)); rm7 <- sapply(imp7, function(y) rmse(y[blk][okb], x7[blk][okb]))
for (mm in M7) key(paste0("example_rmse_", mm), rm7[[mm]])
lab7 <- setNames(sprintf("%s (RMSE %.1f)", M7, rm7[M7]), M7)
d7 <- bind_rows(lapply(M7, function(mm) data.frame(method = mm, t = TS[sl], y = ifelse(sl %in% blk, imp7[[mm]][sl], NA)))) |>
mutate(panel = factor(lab7[method], levels = lab7))
truth7 <- tidyr::crossing(panel = factor(lab7, levels = lab7), data.frame(t = TS[sl], y = x7[sl]))
p7 <- ggplot() + annotate("rect", xmin = TS[min(blk)], xmax = TS[max(blk)], ymin = -Inf, ymax = Inf, fill = "grey93") +
geom_line(data = truth7, aes(t, y), colour = "grey30", linewidth = 0.3, na.rm = TRUE) +
geom_line(data = d7, aes(t, y, colour = method), linewidth = 0.6, na.rm = TRUE) +
facet_wrap(~panel, ncol = 1) + scale_colour_manual(values = COL, guide = "none") +
scale_x_datetime(date_breaks = "1 day", date_labels = "%d %b") +
labs(x = NULL, y = expression(NO[2] ~ "[\u00b5g/m"^3 * "]"))
print(p7); save_fig(p7, "fig07_example_imputation", 7.16, 4.2)

key("example_block", paste(format(TS[min(blk)], "%d %b"), "-", format(TS[max(blk)], "%d %b")))
Seas-AR
decomposition
Four weeks of log-scaled NO_2 at SUF
1 from 6 August 2018, split into its four components. Gaps longer than
two hours are shaded red; inside them the imputed z_t and the AR(1) bridge for R_t are drawn in red with a pointwise 95%
band from the closed-form conditional variance of the bridge. Weekends
are shaded grey, which shows where the weekly deviation W_t departs from zero.
# Seas-AR decomposition of the log-scaled NO2 series of SUF 1 over four weeks. Gaps longer than two hours
# are shaded red; the imputed z_t and the AR(1) bridge for R_t are drawn in red with a pointwise 95% band from the
# closed-form conditional variance of the bridge; weekends are shaded grey.
dec <- impute_seasar(get_series("SUF 1", "NO2"), BW[["SUF 1"]]$bw_day, BW[["SUF 1"]]$bw_week, return_components = TRUE)
win <- which(TS >= as.POSIXct("2018-08-06 00:30", tz = "UTC"))[1] + 0:(48 * 28 - 1)
miss <- is.na(dec$z[win]); zfill <- (dec$comp$trend + dec$comp$daily + dec$comp$weekly + dec$remainder_imp)[win]
# conditional standard deviation of the bridge: gamma0 (1 - phi^2k)(1 - phi^2(n-k)) / (1 - phi^2n)
r <- dec$comp$remainder; n <- length(r); pr <- !is.na(r[-1]) & !is.na(r[-n])
phi <- min(0.999, max(0, sum(r[-1][pr] * r[-n][pr]) / sum(r[-n][pr]^2)))
g0 <- mean((r[-1][pr] - phi * r[-n][pr])^2) / (1 - phi^2)
sd_b <- rep(NA_real_, n); rr <- rle(is.na(r)); e <- cumsum(rr$lengths); s <- e - rr$lengths + 1
for (k in which(rr$values)) {
a <- s[k] - 1; b <- e[k] + 1; i <- s[k]:e[k]
sd_b[i] <- if (a >= 1 && b <= n) sqrt(g0 * (1 - phi^(2 * (i - a))) * (1 - phi^(2 * (b - i))) / (1 - phi^(2 * (b - a)))) else
if (a >= 1) sqrt(g0 * (1 - phi^(2 * (i - a)))) else sqrt(g0 * (1 - phi^(2 * (b - i))))
}
LAB8 <- c(z = "atop(z[t], 'log scale')", T = "atop(T[t], 'trend')", D = "atop(D[t], 'daily')",
W = "atop(W[t], 'weekly')", R = "atop(R[t], 'remainder')")
COL8 <- setNames(c("#2c3e50", "#1f618d", "#148f77", "#ca6f1e", "#7d3c98"), LAB8)
pf <- function(k) factor(unname(LAB8[k]), levels = LAB8)
d_obs <- bind_rows(data.frame(panel = pf("z"), t = TS[win], v = dec$z[win]),
data.frame(panel = pf("T"), t = TS[win], v = dec$comp$trend[win]),
data.frame(panel = pf("D"), t = TS[win], v = dec$comp$daily[win]),
data.frame(panel = pf("W"), t = TS[win], v = dec$comp$weekly[win]),
data.frame(panel = pf("R"), t = TS[win], v = dec$comp$remainder[win]))
d_imp <- bind_rows(data.frame(panel = pf("z"), t = TS[win], v = ifelse(miss, zfill, NA), sd = sd_b[win]),
data.frame(panel = pf("R"), t = TS[win], v = ifelse(miss, dec$remainder_imp[win], NA), sd = sd_b[win])) |>
mutate(lo = v - 1.96 * sd, hi = v + 1.96 * sd)
rw <- rle(miss); ew <- cumsum(rw$lengths); sw <- ew - rw$lengths + 1; kw <- which(rw$values)
gaps8 <- data.frame(xmin = TS[win][sw[kw]] - 900, xmax = TS[win][ew[kw]] + 900, len = rw$lengths[kw])
gl <- gaps8[which.max(gaps8$len), ]
gl_lab <- data.frame(panel = pf("z"), t = gl$xmin + (gl$xmax - gl$xmin) / 2,
lab = if (gl$len >= 48) sprintf("%.1f-day gap", gl$len / 48) else sprintf("%g-h gap", gl$len / 2))
dd <- seq(as.Date(min(TS[win])), as.Date(max(TS[win])), by = "day"); sat <- dd[format(dd, "%u") == "6"]
wkend <- data.frame(xmin = pmax(as.POSIXct(paste(sat, "00:00"), tz = "UTC"), min(TS[win])),
xmax = pmin(as.POSIXct(paste(sat + 2, "00:00"), tz = "UTC"), max(TS[win])))
p8 <- ggplot() +
geom_rect(data = wkend, aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf), fill = "grey93") +
geom_rect(data = gaps8[gaps8$len > 4, ], aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf), fill = "#fadbd8") +
geom_hline(data = data.frame(panel = pf(c("D", "W", "R")), y = 0), aes(yintercept = y), colour = "grey50",
linewidth = 0.2, linetype = "22") +
geom_ribbon(data = filter(d_obs, panel %in% pf(c("D", "W"))), aes(t, ymin = pmin(v, 0), ymax = pmax(v, 0), fill = panel),
alpha = 0.25, na.rm = TRUE) +
geom_ribbon(data = d_imp, aes(t, ymin = lo, ymax = hi), fill = "#e74c3c", alpha = 0.25) + # NA rows split the band
geom_line(data = d_obs, aes(t, v, colour = panel, linewidth = panel == LAB8[["T"]]), na.rm = TRUE) +
geom_line(data = d_imp, aes(t, v), colour = "#c0392b", linewidth = 0.5, na.rm = TRUE) +
geom_text(data = gl_lab, aes(t, Inf, label = lab), vjust = 1.5, size = 2.3, family = FONT_TEXT, colour = "#922b21") +
facet_grid(panel ~ ., scales = "free_y", switch = "y", labeller = label_parsed) +
scale_colour_manual(values = COL8, guide = "none") + scale_fill_manual(values = COL8, guide = "none") +
scale_linewidth_manual(values = c(`FALSE` = 0.3, `TRUE` = 0.7), guide = "none") +
scale_x_datetime(breaks = as.POSIXct(c("2018-08-13", "2018-08-20", "2018-08-27"), tz = "UTC"), date_labels = "%d %b",
expand = c(0, 0)) +
scale_y_continuous(n.breaks = 3, expand = expansion(mult = c(0.06, 0.14))) +
labs(x = NULL, y = NULL, title = expression(z[t] == T[t] + D[t] + W[t] + R[t]),
subtitle = "red: gaps imputed by Seas-AR, with 95% band; grey: weekends") +
theme(strip.placement = "outside", strip.text.y.left = element_text(angle = 0, size = 7.5),
panel.grid.minor = element_blank(), panel.grid.major.y = element_line(linewidth = 0.15, colour = "grey88"),
panel.grid.major.x = element_line(linewidth = 0.2, colour = "grey82"), panel.spacing.y = unit(5, "pt"),
plot.title = element_text(size = 9, hjust = 0.5), plot.subtitle = element_text(size = 7, hjust = 0.5, colour = "grey30"),
axis.text = element_text(size = 6.8), plot.margin = margin(4, 8, 4, 2))
print(p8); save_fig(p8, "fig08_seasar_components", 3.5, 4.6)

Computing time
for (mm in METHOD_ORDER) key(paste0("sec_", mm), t4$sec[t4$method == mm])
knitr::kable(t4 |> transmute(method, seconds = sec), digits = 3, caption = "Mean computing time per imputation of a full series (R, one CPU core)")
Mean computing time per imputation of a full series (R, one CPU
core)
| method |
seconds |
| MEAN |
0.000 |
| MEDIAN |
0.000 |
| LI |
0.000 |
| SS-MEAN |
0.002 |
| SS-MEDIAN |
0.004 |
| SS-LI |
0.005 |
| SEADEC |
0.004 |
| MSTL-LI |
0.021 |
| KALMAN |
0.080 |
| Seas-AR |
0.023 |
Forecasting models and
protocol
Data split, origins
and causal imputation
An origin \tau is the number of
observations available when the forecast is issued (at midnight); the
targets are y_{\tau+1},\dots,y_{\tau+48} (day-ahead,
horizons h=1,\dots,48).
- Test: 1–20 December 2018, \tau_j=16{,}032+48(j-1), j=1,\dots,20 (960 target values).
- Validation: origins in November (14{,}592\le\tau\le15{,}984, every 2 steps)
for early stopping; the loss uses observed targets
only.
- Training: origins from 15 January to 31 October
(672\le\tau\le14{,}544, every 6 steps,
2313 origins).
Three rules prevent leakage: (1) the history used for training is
imputed with the data before 1 December only, \tilde y=\mathrm{Imp}(y_{1:16032}); (2) at
every test origin the imputation is re-run causally, \tilde y^{(\tau)}=\mathrm{Imp}(y_{1:\tau});
(3) validation and test errors are computed on genuinely observed values
only, while the training targets are the imputed series.
H <- 48L; L_SEQ <- 48L; TEST_START <- 334L * 48L; VAL_START <- 304L * 48L
MIN_ORIGIN <- 2L * S2; N_TEST_DAYS <- 20L
test_origins <- TEST_START + 48L * (0:(N_TEST_DAYS - 1))
tr_orig <- seq(MIN_ORIGIN, VAL_START - H, by = 6)
va_orig <- seq(VAL_START, TEST_START - H, by = 2)
hod <- function(j) j %% 48L # j = 0-based index; 0 = 00:30
dow <- function(j) (j %/% 48L) %% 7L # 0 = Monday
cat("training origins:", length(tr_orig), "| validation origins:", length(va_orig), "| test origins:", length(test_origins),
"(", format(TS[TEST_START + 1]), "to", format(TS[TEST_START + 960]), ")\n")
training origins: 2313 | validation origins: 697 | test origins: 20 ( 2018-12-01 00:30:00 to 2018-12-21 )
Seasonal naive and
TSR (Eq. 10)
Seasonal naive: \hat
y_{\tau+h}=\tilde y^{(\tau)}_{\tau+h-336}. TSR
(additive double-seasonal time series regression):
y_t=\beta_0+\beta_1t+\sum_{i=2}^{48}\alpha_iH_{i,t}+\sum_{j=2}^{7}\gamma_jE_{j,t}+e_t,
\tag{10}
with half-hour dummies H_{i,t} and
weekday dummies E_{j,t} (base
categories 00:30 and Monday), fitted by least squares on the whole
imputed history. Time is scaled as t/10^4 for numerical stability; the scaling
does not change the forecasts.
tsr_design <- function(j) {
n <- length(j); hh <- hod(j); dd <- dow(j)
Xh <- matrix(0, n, 47); Xh[cbind(which(hh > 0), hh[hh > 0])] <- 1
Xd <- matrix(0, n, 6); Xd[cbind(which(dd > 0), dd[dd > 0])] <- 1
cbind(1, j / 1e4, Xh, Xd)
}
tsr_fit <- function(y, j) qr.coef(qr(tsr_design(j)), y)
tsr_pred <- function(beta, j) { b <- beta; b[is.na(b)] <- 0; as.numeric(tsr_design(j) %*% b) }
LightGBM (direct
multi-horizon model)
Each (origin \tau, horizon h) pair is one row with 14 features: h, half-hour and weekday of the target, the
lags 1, 2, 3, 6 and 12 at the origin, the values 48, 96, 336 and 672
steps before the target, and the means of the last day and week.
Settings: learning rate 0.03, 31 leaves, at least 50 samples per leaf,
bagging 0.8, feature fraction 0.9, L_2
penalty 1, early stopping after 100 rounds (at most 2000 trees) on the
observed validation targets.
gbm_rows <- function(y, tau) {
h <- 0:(H - 1); tgt <- tau + h # 0-based target indices
cs <- c(0, cumsum(y)); m48 <- (cs[tau + 1] - cs[tau + 1 - 48]) / 48; m336 <- (cs[tau + 1] - cs[tau + 1 - 336]) / 336
lag <- function(k) rep(y[tau - k + 1], H); sl <- function(k) y[tgt - k + 1]
cbind(h = h, hod = hod(tgt), dow = dow(tgt), lag1 = lag(1), lag2 = lag(2), lag3 = lag(3), lag6 = lag(6),
lag12 = lag(12), slag48 = sl(48), slag96 = sl(96), slag336 = sl(336), slag672 = sl(672),
mean48 = m48, mean336 = m336)
}
build_gbm <- function(y, ytarget, origins) list(
X = do.call(rbind, lapply(origins, function(t) gbm_rows(y, t))),
Y = unlist(lapply(origins, function(t) ytarget[(t + 1):(t + H)])))
fit_predict_gbm <- function(y_hist, x_hist_raw, hist_at) {
tr <- build_gbm(y_hist, y_hist, tr_orig); va <- build_gbm(y_hist, x_hist_raw, va_orig); ok <- !is.na(va$Y)
dtr <- lightgbm::lgb.Dataset(tr$X, label = tr$Y)
dva <- lightgbm::lgb.Dataset.create.valid(dtr, va$X[ok, , drop = FALSE], label = va$Y[ok])
prm <- list(objective = "regression", learning_rate = 0.03, num_leaves = 31L, min_data_in_leaf = 50L,
bagging_fraction = 0.8, bagging_freq = 1L, feature_fraction = 0.9, lambda_l2 = 1,
num_threads = 1L, verbose = -1L, seed = 1L)
gb <- lightgbm::lgb.train(params = prm, data = dtr, nrounds = 2000L, valids = list(valid = dva),
early_stopping_rounds = 100L, verbose = -1L)
Xte <- do.call(rbind, lapply(seq_along(test_origins), function(k) gbm_rows(hist_at[[k]], test_origins[k])))
matrix(predict(gb, Xte), nrow = N_TEST_DAYS, byrow = TRUE)
}
Neural networks: MLP,
LSTM, TCN and the TSR–LSTM hybrid
All networks use standardised data and the same inputs: the sequence
\mathbf s_\tau=(\tilde
y_{\tau-47},\dots,\tilde y_\tau) and the side vector \mathbf v_\tau=(\tilde y_{\tau-335},\dots,\tilde
y_{\tau-288},\sin\tfrac{2\pi h_\tau}{48},\cos\tfrac{2\pi
h_\tau}{48},\sin\tfrac{2\pi d_\tau}{7},\cos\tfrac{2\pi
d_\tau}{7}) (the same period one week earlier and calendar codes
of the origin).
- MLP: \hat{\mathbf
y}=\mathbf W_3\,\mathrm{ReLU}(\mathbf W_2\,\mathrm{ReLU}(\mathbf
W_1[\mathbf s_\tau;\mathbf v_\tau]+\mathbf b_1)+\mathbf b_2)+\mathbf
b_3 (128 and 64 units).
- LSTM: 64 units, \mathbf
c_t=\mathbf f_t\odot\mathbf c_{t-1}+\mathbf i_t\odot\tilde{\mathbf
c}_t and \mathbf h_t=\mathbf
o_t\odot\tanh(\mathbf c_t) with sigmoid gates \mathbf i_t,\mathbf f_t,\mathbf o_t; \mathbf h_{48} is concatenated with \mathbf v_\tau, followed by Dense 64 (ReLU)
and Dense 48.
- TCN: five causal dilated convolutions (F*_dx)(t)=\sum_{i=0}^{2}f(i)\,x_{t-d\,i},
d\in\{1,2,4,8,16\}, 32 filters
(receptive field 63), then as above.
- TSR–LSTM: the LSTM forecasts the TSR residuals,
\hat y_{\tau+h}=\hat L_{\tau+h}+\hat
N_{\tau+h}.
Loss: masked mean squared error over observed targets, \mathcal L=\sum_{i,h}m_{ih}(y_{ih}-\hat
y_{ih})^2/\sum_{i,h}m_{ih}. Adam (learning rate 2\times10^{-3}), batch 256, at most 20
epochs, early stopping with patience 3 and best weights restored; two
seeds, forecasts averaged. Without keras3 only an MLP from
nnet is available.
origin_cal <- function(tau) { t <- tau - 1; a <- 2 * pi * hod(t) / 48; b <- 2 * pi * dow(t) / 7
c(sin(a), cos(a), sin(b), cos(b)) }
nn_inputs <- function(y, tau) list(seq = y[(tau - L_SEQ + 1):tau],
side = c(y[(tau - S2 + 1):(tau - S2 + H)], origin_cal(tau)))
build_nn_samples <- function(y, ytarget, origins) {
n <- length(origins); SEQ <- array(0, c(n, L_SEQ, 1)); SIDE <- matrix(0, n, H + 4); Y <- matrix(0, n, H)
for (i in seq_len(n)) { tau <- origins[i]; inp <- nn_inputs(y, tau)
SEQ[i, , 1] <- inp$seq; SIDE[i, ] <- inp$side; Y[i, ] <- ytarget[(tau + 1):(tau + H)] }
list(seq = SEQ, side = SIDE, y = Y)
}
stack_test <- function(lst) list(seq = array(t(sapply(lst, `[[`, "seq")), c(length(lst), L_SEQ, 1)),
side = t(sapply(lst, `[[`, "side")))
if (HAS_KERAS) {
library(keras3)
masked_mse <- function(y_true, y_pred) { # mean squared error over observed targets only
mask <- op_logical_not(op_isnan(y_true))
diff <- op_where(mask, y_true - y_pred, op_zeros_like(y_pred))
op_sum(op_square(diff)) / op_maximum(op_sum(op_cast(mask, "float32")), 1)
}
build_nn <- function(kind, units = 64, lr = 2e-3) {
a <- keras_input(shape = c(L_SEQ, 1L)); b <- keras_input(shape = c(H + 4L))
if (kind == "MLP") {
z <- layer_concatenate(list(layer_flatten(a), b)) |>
layer_dense(128, activation = "relu") |> layer_dense(64, activation = "relu")
} else {
if (kind %in% c("LSTM", "TSR-LSTM")) h <- a |> layer_lstm(units)
if (kind == "TCN") {
h <- a
for (d in c(1, 2, 4, 8, 16)) h <- h |> layer_conv_1d(32, 3, dilation_rate = d, padding = "causal", activation = "relu")
h <- h |> layer_cropping_1d(cropping = c(L_SEQ - 1L, 0L)) |> layer_flatten() # last time step
}
z <- layer_concatenate(list(h, b)) |> layer_dense(64, activation = "relu")
}
m <- keras_model(list(a, b), z |> layer_dense(H))
m |> compile(optimizer = optimizer_adam(lr), loss = masked_mse)
m
}
fit_predict_nn <- function(kind, tr, va, te, seed, epochs = 20, patience = 3) {
clear_session(); set_random_seed(seed)
m <- build_nn(kind)
m |> fit(list(tr$seq, tr$side), tr$y, validation_data = list(list(va$seq, va$side), va$y),
epochs = epochs, batch_size = 256, verbose = 0,
callbacks = list(callback_early_stopping(patience = patience, restore_best_weights = TRUE)))
m |> predict(list(te$seq, te$side), verbose = 0)
}
NN_MODELS <- c("MLP", "LSTM", "TCN", "TSR-LSTM")
} else {
fit_predict_nn <- function(kind, tr, va, te, seed, epochs = NA, patience = NA) { # fallback without keras3
set.seed(seed)
Xtr <- cbind(matrix(tr$seq, nrow(tr$y)), tr$side); Xte <- cbind(matrix(te$seq, nrow(te$side)), te$side)
fit <- nnet::nnet(Xtr, tr$y, size = 12, linout = TRUE, decay = 1e-3, maxit = 300, MaxNWts = 10000, trace = FALSE)
predict(fit, Xte)
}
NN_MODELS <- "MLP"
}
One complete run:
from imputation to forecasts
run_forecast() applies the protocol for one pollutant
and one imputation method and returns the forecasts of all models
(truncated at zero) and the RMSE of every seed.
run_forecast <- function(x_raw, impute_fn, seeds = SEEDS) {
y_hist <- impute_fn(x_raw[1:TEST_START]) # rule 1
hist_at <- lapply(test_origins, function(tau) impute_fn(x_raw[1:tau])) # rule 2 (causal)
y_true <- t(sapply(test_origins, function(tau) x_raw[(tau + 1):(tau + H)])) # NA = not observed (rule 3)
x_hist_raw <- x_raw[1:TEST_START]
out <- list(); seed_rows <- list()
out$SNAIVE <- t(sapply(seq_along(test_origins), function(k) {
tau <- test_origins[k]; hist_at[[k]][(tau - S2 + 1):(tau - S2 + H)] }))
j_hist <- 0:(TEST_START - 1); beta <- tsr_fit(y_hist, j_hist)
out$TSR <- t(sapply(test_origins, function(tau) tsr_pred(beta, tau + 0:(H - 1))))
if (HAS_LGB) out$LightGBM <- fit_predict_gbm(y_hist, x_hist_raw, hist_at)
mu <- mean(y_hist[1:VAL_START]); sdv <- sd(y_hist[1:VAL_START]) # standardisation
trs <- build_nn_samples((y_hist - mu) / sdv, (y_hist - mu) / sdv, tr_orig)
vas <- build_nn_samples((y_hist - mu) / sdv, (x_hist_raw - mu) / sdv, va_orig)
tes <- stack_test(lapply(seq_along(test_origins), function(k) nn_inputs((hist_at[[k]] - mu) / sdv, test_origins[k])))
r_hist <- y_hist - tsr_pred(beta, j_hist); r_sd <- sd(r_hist[1:VAL_START]) # TSR residuals
for (kind in NN_MODELS) {
preds <- lapply(seeds, function(s) {
if (kind == "TSR-LSTM") {
trr <- build_nn_samples(r_hist / r_sd, r_hist / r_sd, tr_orig)
var <- build_nn_samples(r_hist / r_sd, (x_hist_raw - tsr_pred(beta, j_hist)) / r_sd, va_orig)
ter <- stack_test(lapply(seq_along(test_origins), function(k) {
tau <- test_origins[k]; nn_inputs((hist_at[[k]] - tsr_pred(beta, 0:(tau - 1))) / r_sd, tau) }))
fit_predict_nn(kind, trr, var, ter, s) * r_sd + out$TSR
} else fit_predict_nn(kind, trs, vas, tes, s) * sdv + mu
})
for (i in seq_along(seeds)) seed_rows[[length(seed_rows) + 1]] <- data.frame(
model = kind, seed = seeds[i], rmse = sqrt(mean((pmax(preds[[i]], 0) - y_true)^2, na.rm = TRUE)))
out[[kind]] <- Reduce(`+`, preds) / length(preds)
}
fc <- bind_rows(lapply(names(out), function(mn) data.frame(model = mn,
origin = rep(test_origins, each = H), h = rep(1:H, N_TEST_DAYS),
y = as.vector(t(y_true)), yhat = pmax(as.vector(t(out[[mn]])), 0))))
list(fc = fc, seeds = bind_rows(seed_rows))
}
Diebold–Mariano test
(Eq. 11)
With d_t=e_{1t}^2-e_{2t}^2 (Seas-AR
history minus alternative history),
\mathrm{DM}=\sqrt{\frac{N+1-2h+h(h-1)/N}{N}}\;\frac{\bar
d}{\sqrt{\widehat{\mathrm{Var}}(\bar d)}},\qquad
\widehat{\mathrm{Var}}(\bar
d)=\frac1N\Big(\hat\gamma_0+2\sum_{k=1}^{h-1}\hat\gamma_k\Big), \tag{11}
with sample autocovariances \hat\gamma_k of d_t, h=48,
the Harvey–Leybourne–Newbold factor and a t_{N-1} reference distribution; negative
values favour Seas-AR. If the variance estimate is not positive, \hat\gamma_0/N is used. This equals
forecast::dm.test(varestimator = "acf") (checked in Section
8.4).
dm_test <- function(e1, e2, h = 48, power = 2) { # Eq. (11)
ok <- !(is.na(e1) | is.na(e2)); d <- abs(e1[ok])^power - abs(e2[ok])^power
n <- length(d); dc <- d - mean(d)
gam <- sapply(0:(h - 1), function(k) sum(dc[(k + 1):n] * dc[1:(n - k)]) / n)
v <- (gam[1] + 2 * sum(gam[-1])) / n
if (v <= 0) v <- gam[1] / n
stat <- mean(d) / sqrt(v) * sqrt((n + 1 - 2 * h + h * (h - 1) / n) / n)
c(stat = stat, p = 2 * pt(-abs(stat), df = n - 1))
}
Experiment 2: impact of
imputation on forecasting
Running the
experiment
Five imputations are compared for seven models at SUF 1: MEDIAN (a
common practical default), LI (the generic default), SEADEC (the
strongest competitor in Experiment 1), KALMAN (state-space smoothing)
and Seas-AR (bandwidths of SUF 1 from Experiment 0). Forecasts are
stored per pollutant and imputation, so an interrupted run resumes where
it stopped.
IMP_FC <- c("MEDIAN", "LI", "SEADEC", "KALMAN", "Seas-AR")
MODELS <- c("SNAIVE", "TSR", "LightGBM", "MLP", "LSTM", "TCN", "TSR-LSTM")
MODEL_TEX <- c(SNAIVE = "Seasonal na\\\"ive", TSR = "TSR", LightGBM = "LightGBM", MLP = "MLP", LSTM = "LSTM",
TCN = "TCN", `TSR-LSTM` = "TSR--LSTM")
imputer_for <- function(imp) switch(imp, MEDIAN = METHODS$MEDIAN, LI = METHODS$LI, SEADEC = METHODS$SEADEC,
KALMAN = METHODS$KALMAN, `Seas-AR` = function(v) impute_seasar(v, BW[["SUF 1"]]$bw_day, BW[["SUF 1"]]$bw_week))
dir2 <- out_file(if (QUICK) "exp2_quick" else "exp2"); dir.create(dir2, showWarnings = FALSE)
pols_fc <- if (QUICK) "NO2" else POLS; imps_fc <- if (QUICK) c("MEDIAN", "Seas-AR") else IMP_FC
if (!(HAS_KERAS && HAS_LGB)) cat("Note: the full model set needs lightgbm and keras3; missing models are skipped.\n")
Note: the full model set needs lightgbm and keras3; missing models are skipped.
for (p in pols_fc) for (imp in imps_fc) {
ff <- file.path(dir2, sprintf("fc_%s_%s.csv", p, imp))
if (isTRUE(P$use_cache) && file.exists(ff)) next
t0 <- Sys.time()
res <- run_forecast(get_series("SUF 1", p), imputer_for(imp), seeds = if (QUICK) SEEDS[1] else SEEDS)
write.csv(res$fc |> mutate(pollutant = p, imputation = imp), ff, row.names = FALSE)
write.csv(res$seeds |> mutate(pollutant = p, imputation = imp), sub("fc_", "seeds_", ff), row.names = FALSE)
cat(p, imp, "finished in", round(as.numeric(Sys.time() - t0, units = "secs")), "s\n")
}
PM10 MEDIAN finished in 15 s
PM10 LI finished in 16 s
PM10 SEADEC finished in 16 s
PM10 KALMAN finished in 17 s
PM10 Seas-AR finished in 16 s
NO2 MEDIAN finished in 16 s
NO2 LI finished in 17 s
NO2 SEADEC finished in 17 s
NO2 KALMAN finished in 20 s
NO2 Seas-AR finished in 16 s
SO2 MEDIAN finished in 16 s
SO2 LI finished in 16 s
SO2 SEADEC finished in 16 s
SO2 KALMAN finished in 20 s
SO2 Seas-AR finished in 16 s
CO MEDIAN finished in 16 s
CO LI finished in 16 s
CO SEADEC finished in 17 s
CO KALMAN finished in 18 s
CO Seas-AR finished in 17 s
O3 MEDIAN finished in 16 s
O3 LI finished in 16 s
O3 SEADEC finished in 16 s
O3 KALMAN finished in 17 s
O3 Seas-AR finished in 16 s
fc_all <- bind_rows(lapply(list.files(dir2, pattern = "^fc_.*\\.csv$", full.names = TRUE), read.csv)) |>
select(pollutant, imputation, model, origin, h, y, yhat)
seed_files <- list.files(dir2, pattern = "^seeds_.*\\.csv$", full.names = TRUE)
seeds_all <- if (length(seed_files)) bind_rows(lapply(seed_files, read.csv)) else NULL
cat("Experiment 2:", nrow(fc_all), "forecast rows | models:", paste(intersect(MODELS, unique(fc_all$model)), collapse = ", "), "\n")
Experiment 2: 96000 forecast rows | models: SNAIVE, TSR, LightGBM, MLP
RMSE and MAE on the
test days
\mathrm{RMSE}=\big(\frac{1}{|\mathcal
T|}\sum_{(\tau,h)\in\mathcal T}(\hat
y_{\tau+h}-y_{\tau+h})^2\big)^{1/2} over the pairs \mathcal T whose target is observed. Bold
marks the best imputation for each model.
sc <- fc_all |> filter(!is.na(y)) |> group_by(pollutant, model, imputation) |>
summarise(RMSE = sqrt(mean((yhat - y)^2)), MAE = mean(abs(yhat - y)), n = n(), .groups = "drop")
write.csv(sc, out_file("exp2_scores.csv"), row.names = FALSE)
wide7 <- function(metric) sc |> select(pollutant, model, imputation, v = all_of(metric)) |>
pivot_wider(names_from = imputation, values_from = v) |>
mutate(pollutant = factor(pollutant, levels = POLS), model = factor(model, levels = MODELS)) |> arrange(pollutant, model)
md7 <- function(metric) {
w <- wide7(metric); ic <- intersect(IMP_FC, names(w)); M <- as.matrix(w[, ic])
Mf <- do.call(rbind, lapply(seq_len(nrow(M)), function(i) fmt_md(M[i, ], if (w$pollutant[i] == "CO") 3 else 2)))
colnames(Mf) <- ic; data.frame(Pollutant = as.character(w$pollutant), Model = as.character(w$model), Mf, check.names = FALSE)
}
w7 <- wide7("RMSE"); ic7 <- intersect(IMP_FC, names(w7)); rows7 <- c()
for (p in intersect(POLS, as.character(w7$pollutant))) {
w <- w7 |> filter(pollutant == p); M <- as.matrix(w[, ic7]); best_all <- min(M, na.rm = TRUE); dg <- if (p == "CO") 3 else 2
for (i in seq_len(nrow(w))) {
cells <- sapply(seq_along(ic7), function(j) { v <- M[i, j]; s <- fmt_num(v, dg)
if (abs(v - min(M[i, ], na.rm = TRUE)) < 1e-12) s <- paste0("\\textbf{", s, "}")
if (abs(v - best_all) < 1e-12) s <- paste0("\\underline{", s, "}"); s })
lead <- if (i == 1) sprintf("\\multirow{%d}{*}{%s}", nrow(w), POL_TEX[[p]]) else ""
rows7 <- c(rows7, paste0(paste(c(lead, MODEL_TEX[[as.character(w$model[i])]], cells), collapse = " & "), " \\\\"))
}
rows7 <- c(rows7, "\\midrule")
}
write_tex(c("\\begin{table}[!t]", "\\centering",
sprintf("\\caption{Day-ahead forecast RMSE on the 20 test days (1--20 Dec 2018, observed values only) for models trained on histories imputed by %d methods. Bold: best imputation for each model; underlined: best model--imputation pair for each pollutant.}", length(ic7)),
"\\label{tab:forecast}", "\\setlength{\\tabcolsep}{2.6pt}", sprintf("\\begin{tabular}{ll%s}", strrep("r", length(ic7))), "\\toprule",
paste0(" & Model & ", paste(ic7, collapse = " & "), "\\\\"), "\\midrule", head(rows7, -1),
"\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_forecast")
knitr::kable(md7("RMSE"), align = "r", caption = cap("Table 8", "Day-ahead RMSE on the 20 test days (observed values)"))
Day-ahead RMSE on the 20 test days (observed values)
| Pollutant |
Model |
MEDIAN |
LI |
SEADEC |
KALMAN |
Seas-AR |
| PM10 |
SNAIVE |
47.44 |
47.44 |
47.44 |
47.44 |
47.44 |
| PM10 |
TSR |
50.92 |
52.57 |
52.54 |
49.39 |
51.68 |
| PM10 |
LightGBM |
35.15 |
35.49 |
36.90 |
37.77 |
37.92 |
| PM10 |
MLP |
34.65 |
42.87 |
36.91 |
38.10 |
34.30 |
| NO2 |
SNAIVE |
10.70 |
8.24 |
11.71 |
12.41 |
6.35 |
| NO2 |
TSR |
12.33 |
9.72 |
10.95 |
13.30 |
9.61 |
| NO2 |
LightGBM |
5.99 |
5.24 |
6.02 |
6.44 |
4.95 |
| NO2 |
MLP |
7.46 |
7.49 |
6.55 |
7.27 |
6.08 |
| SO2 |
SNAIVE |
23.94 |
23.82 |
23.84 |
23.75 |
23.59 |
| SO2 |
TSR |
33.84 |
35.91 |
36.03 |
35.44 |
35.31 |
| SO2 |
LightGBM |
20.04 |
20.21 |
20.15 |
20.20 |
20.07 |
| SO2 |
MLP |
25.79 |
24.67 |
24.02 |
24.80 |
25.32 |
| CO |
SNAIVE |
0.899 |
0.891 |
0.892 |
0.892 |
0.889 |
| CO |
TSR |
0.938 |
0.931 |
0.922 |
0.919 |
0.932 |
| CO |
LightGBM |
0.742 |
0.732 |
0.671 |
0.721 |
0.684 |
| CO |
MLP |
0.890 |
0.891 |
0.923 |
0.902 |
0.892 |
| O3 |
SNAIVE |
25.52 |
25.42 |
25.42 |
25.42 |
25.40 |
| O3 |
TSR |
29.36 |
29.54 |
29.53 |
29.53 |
29.52 |
| O3 |
LightGBM |
25.30 |
25.20 |
25.53 |
25.21 |
25.23 |
| O3 |
MLP |
32.43 |
26.61 |
27.16 |
27.52 |
28.10 |
knitr::kable(md7("MAE"), align = "r", caption = "Day-ahead MAE on the 20 test days (observed values)")
Day-ahead MAE on the 20 test days (observed values)
| Pollutant |
Model |
MEDIAN |
LI |
SEADEC |
KALMAN |
Seas-AR |
| PM10 |
SNAIVE |
26.93 |
26.93 |
26.93 |
26.93 |
26.93 |
| PM10 |
TSR |
30.39 |
33.16 |
33.09 |
28.43 |
31.46 |
| PM10 |
LightGBM |
19.75 |
20.00 |
20.31 |
20.76 |
20.24 |
| PM10 |
MLP |
20.26 |
23.86 |
20.64 |
20.24 |
18.68 |
| NO2 |
SNAIVE |
8.13 |
6.39 |
8.50 |
9.08 |
4.70 |
| NO2 |
TSR |
10.89 |
8.23 |
9.30 |
11.86 |
8.06 |
| NO2 |
LightGBM |
5.06 |
4.46 |
5.09 |
5.52 |
4.12 |
| NO2 |
MLP |
6.47 |
6.80 |
5.50 |
5.88 |
5.32 |
| SO2 |
SNAIVE |
8.57 |
8.18 |
8.19 |
8.12 |
7.97 |
| SO2 |
TSR |
30.62 |
33.00 |
33.10 |
32.46 |
32.24 |
| SO2 |
LightGBM |
9.03 |
9.82 |
9.37 |
8.78 |
9.12 |
| SO2 |
MLP |
20.47 |
18.99 |
17.91 |
19.29 |
20.02 |
| CO |
SNAIVE |
0.654 |
0.647 |
0.647 |
0.647 |
0.645 |
| CO |
TSR |
0.700 |
0.692 |
0.684 |
0.678 |
0.696 |
| CO |
LightGBM |
0.507 |
0.492 |
0.453 |
0.483 |
0.468 |
| CO |
MLP |
0.639 |
0.642 |
0.669 |
0.647 |
0.647 |
| O3 |
SNAIVE |
17.86 |
17.75 |
17.74 |
17.74 |
17.71 |
| O3 |
TSR |
26.08 |
26.28 |
26.27 |
26.27 |
26.26 |
| O3 |
LightGBM |
18.80 |
18.57 |
18.83 |
18.62 |
18.65 |
| O3 |
MLP |
26.68 |
20.76 |
21.18 |
22.10 |
21.15 |
bestp <- sc |> group_by(pollutant) |> slice_min(RMSE, n = 1, with_ties = FALSE) |> ungroup() |>
mutate(pollutant = factor(pollutant, levels = POLS)) |> arrange(pollutant)
for (i in seq_len(nrow(bestp))) { p <- as.character(bestp$pollutant[i])
key(paste0("bestpair_", p), paste(bestp$model[i], bestp$imputation[i])); key(paste0("bestpair_rmse_", p), bestp$RMSE[i]) }
knitr::kable(bestp, digits = 3, caption = "Best model-imputation pair for each pollutant")
Best model-imputation pair for each pollutant
| pollutant |
model |
imputation |
RMSE |
MAE |
n |
| PM10 |
MLP |
Seas-AR |
34.299 |
18.681 |
959 |
| NO2 |
LightGBM |
Seas-AR |
4.951 |
4.117 |
937 |
| SO2 |
LightGBM |
MEDIAN |
20.038 |
9.028 |
925 |
| CO |
LightGBM |
SEADEC |
0.671 |
0.453 |
942 |
| O3 |
LightGBM |
LI |
25.197 |
18.568 |
935 |
Change relative to
median imputation
\Delta_{p,m,i}=100\big(\mathrm{RMSE}_{p,m,i}/\mathrm{RMSE}_{p,m,\mathrm{MEDIAN}}-1\big)
for pollutant p, model m and imputation i; blue cells mean a lower error than with a
median-imputed history.
ABBR <- c(MEDIAN = "MED", LI = "LI", SEADEC = "SEA", KALMAN = "KAL", `Seas-AR` = "S-AR")
d9 <- sc |> group_by(pollutant, model) |> mutate(change = 100 * (RMSE / RMSE[imputation == "MEDIAN"] - 1)) |> ungroup() |>
mutate(pollutant = factor(pollutant, levels = POLS, labels = c("PM[10]", "NO[2]", "SO[2]", "CO", "O[3]")),
model = factor(model, levels = rev(MODELS), labels = rev(c("Seasonal naive", MODELS[-1]))),
imputation = factor(ABBR[imputation], levels = ABBR))
p9 <- ggplot(d9, aes(imputation, model, fill = pmax(pmin(change, 30), -30))) + geom_tile(colour = "white") +
geom_text(aes(label = sprintf("%+.1f", change)), size = 1.9, family = FONT_TEXT) +
scale_fill_gradient2(low = "#2166ac", high = "#b2182b", limits = c(-30, 30), name = "RMSE change\nvs MEDIAN (%)") +
facet_wrap(~pollutant, nrow = 1, labeller = label_parsed) + labs(x = NULL, y = NULL) +
theme(panel.grid = element_blank(), axis.text.x = element_text(size = 6), legend.key.height = unit(0.5, "cm"))
print(p9); save_fig(p9, "fig09_forecast_heatmap", 7.16, 2.3)

Diebold–Mariano
tests
Each cell counts the seven models as W/T/L: Seas-AR significantly
better / no significant difference / significantly worse at the 5%
level. The first line checks dm_test() against
forecast::dm.test().
ex_ref <- fc_all |> filter(pollutant == "NO2", model == "MLP", imputation == "Seas-AR") |> arrange(origin, h)
ex_oth <- fc_all |> filter(pollutant == "NO2", model == "MLP", imputation == "MEDIAN") |> arrange(origin, h)
if (nrow(ex_ref) && nrow(ex_oth)) {
ok <- !is.na(ex_ref$y); e1 <- (ex_ref$yhat - ex_ref$y)[ok]; e2 <- (ex_oth$yhat - ex_oth$y)[ok]
mine <- dm_test(e1, e2, h = 48); fdm <- suppressWarnings(forecast::dm.test(e1, e2, h = 48, power = 2, varestimator = "acf"))
cat(sprintf("Check (NO2, MLP, Seas-AR vs MEDIAN): dm_test DM = %.4f (p = %.3g) | forecast::dm.test DM = %.4f (p = %.3g)\n",
mine[["stat"]], mine[["p"]], fdm$statistic, fdm$p.value))
}
Check (NO2, MLP, Seas-AR vs MEDIAN): dm_test DM = -2.9493 (p = 0.00326) | forecast::dm.test DM = -2.9493 (p = 0.00326)
dm <- bind_rows(lapply(split(fc_all, list(fc_all$pollutant, fc_all$model), drop = TRUE), function(g) {
ref <- g |> filter(imputation == "Seas-AR") |> arrange(origin, h)
if (!nrow(ref)) return(NULL)
bind_rows(lapply(setdiff(unique(g$imputation), "Seas-AR"), function(imp) {
oth <- g |> filter(imputation == imp) |> arrange(origin, h)
r <- dm_test(ref$yhat - ref$y, oth$yhat - oth$y, h = 48)
data.frame(pollutant = ref$pollutant[1], model = ref$model[1], competitor = imp, DM = r[["stat"]], p = r[["p"]])
}))
}))
write.csv(dm, out_file("exp2_dm.csv"), row.names = FALSE)
wtl_f <- function(d) d |> summarise(W = sum(p < .05 & DM < 0), L = sum(p < .05 & DM > 0), n = n(), .groups = "drop") |>
mutate(T = n - W - L, cell = sprintf("%d/%d/%d", W, T, L))
wtl <- bind_rows(dm |> group_by(pollutant, competitor) |> wtl_f(), dm |> group_by(competitor) |> wtl_f() |> mutate(pollutant = "Total"))
write.csv(wtl, out_file("exp2_wtl.csv"), row.names = FALSE)
for (i in which(wtl$pollutant == "Total")) key(paste0("dm_total_", wtl$competitor[i]), wtl$cell[i])
comp_order <- intersect(setdiff(IMP_FC, "Seas-AR"), unique(dm$competitor))
t8 <- wtl |> select(pollutant, competitor, cell) |> pivot_wider(names_from = competitor, values_from = cell) |>
mutate(pollutant = factor(pollutant, levels = c(POLS, "Total"))) |> arrange(pollutant)
rows8 <- sapply(seq_len(nrow(t8)), function(i) { p <- as.character(t8$pollutant[i])
paste0(paste(c(if (p == "Total") "\\textit{Total}" else POL_TEX[[p]], unlist(t8[i, comp_order])), collapse = " & "), " \\\\") })
write_tex(c("\\begin{table}[!t]", "\\centering",
"\\caption{Diebold--Mariano tests (squared loss, $h=48$, Harvey--Leybourne--Newbold correction) comparing forecasts obtained with Seas-AR-imputed histories against each alternative imputation. Each cell counts the seven forecasting models as W/T/L: Seas-AR significantly better / no significant difference / significantly worse at the 5\\% level.}",
"\\label{tab:dm}", "\\setlength{\\tabcolsep}{5pt}", sprintf("\\begin{tabular}{l%s}", strrep("r", length(comp_order))), "\\toprule",
paste0("Pollutant & ", paste(paste("vs", comp_order), collapse = " & "), "\\\\"), "\\midrule",
head(rows8, -1), "\\midrule", tail(rows8, 1), "\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_dm")
knitr::kable(t8[, c("pollutant", comp_order)], col.names = c("Pollutant", paste("vs", comp_order)),
caption = cap("Table 9", "Diebold-Mariano tests: Seas-AR vs each alternative (W/T/L over the seven models)"))
Diebold-Mariano tests: Seas-AR vs each alternative (W/T/L over
the seven models)
| Pollutant |
vs MEDIAN |
vs LI |
vs SEADEC |
vs KALMAN |
| PM10 |
0/3/1 |
1/3/0 |
1/3/0 |
1/2/1 |
| NO2 |
4/0/0 |
3/1/0 |
2/2/0 |
2/2/0 |
| SO2 |
1/2/1 |
1/3/0 |
1/2/1 |
1/3/0 |
| CO |
3/1/0 |
1/2/1 |
0/3/1 |
1/2/1 |
| O3 |
0/3/1 |
1/3/0 |
2/2/0 |
1/3/0 |
| Total |
8/9/3 |
7/12/1 |
6/12/2 |
6/12/2 |
Summary over
pollutants and models
M7 <- as.matrix(w7[, ic7]); best_imp <- ic7[apply(M7, 1, which.min)]
rk7 <- colMeans(t(apply(M7, 1, rank)))
chg <- colMeans(100 * (M7 / M7[, "MEDIAN"] - 1))
summ <- data.frame(imputation = ic7, best_for = as.integer(table(factor(best_imp, levels = ic7))), avg_rank = rk7, mean_change_vs_median = chg)
write.csv(summ, out_file("exp2_summary.csv"), row.names = FALSE)
for (i in seq_len(nrow(summ))) { key(paste0("fc_best_", summ$imputation[i]), summ$best_for[i]); key(paste0("fc_rank_", summ$imputation[i]), summ$avg_rank[i])
key(paste0("fc_chg_", summ$imputation[i]), summ$mean_change_vs_median[i]) }
rel7 <- w7; for (cc in ic7) rel7[[cc]] <- 100 * (w7[[cc]] / w7$MEDIAN - 1)
chg_p <- rel7 |> group_by(pollutant) |> summarise(across(all_of(ic7), mean), .groups = "drop")
bestc <- w7 |> mutate(best = best_imp) |> count(pollutant, best) |> pivot_wider(names_from = best, values_from = n, values_fill = 0)
write.csv(chg_p, out_file("exp2_change_by_pollutant.csv"), row.names = FALSE); write.csv(bestc, out_file("exp2_bestcount_by_pollutant.csv"), row.names = FALSE)
for (i in seq_len(nrow(chg_p))) for (cc in ic7) key(paste0("chg_", chg_p$pollutant[i], "_", cc), chg_p[[cc]][i])
knitr::kable(summ, digits = 2, col.names = c("Imputation", "Best for (pairs)", "Average rank", "Mean RMSE change vs MEDIAN (%)"),
caption = sprintf("Summary over the %d pollutant-model pairs", nrow(M7)))
Summary over the 20 pollutant-model pairs
|
Imputation |
Best for (pairs) |
Average rank |
Mean RMSE change vs MEDIAN (%) |
| MEDIAN |
MEDIAN |
6 |
3.15 |
0.00 |
| LI |
LI |
2 |
3.15 |
-2.35 |
| SEADEC |
SEADEC |
2 |
3.15 |
-1.12 |
| KALMAN |
KALMAN |
2 |
3.20 |
1.23 |
| Seas-AR |
Seas-AR |
8 |
2.35 |
-5.60 |
knitr::kable(chg_p, digits = 2, caption = "Mean RMSE change vs MEDIAN (%) by pollutant")
Mean RMSE change vs MEDIAN (%) by pollutant
| pollutant |
MEDIAN |
LI |
SEADEC |
KALMAN |
Seas-AR |
| PM10 |
0 |
6.98 |
3.67 |
3.60 |
2.08 |
| NO2 |
0 |
-14.07 |
-3.35 |
7.20 |
-24.66 |
| SO2 |
0 |
0.54 |
-0.07 |
0.22 |
0.31 |
| CO |
0 |
-0.69 |
-2.07 |
-1.04 |
-2.33 |
| O3 |
0 |
-4.53 |
-3.79 |
-3.83 |
-3.39 |
knitr::kable(bestc, caption = "Number of models for which each imputation is best, by pollutant")
Number of models for which each imputation is best, by
pollutant
| pollutant |
KALMAN |
MEDIAN |
Seas-AR |
SEADEC |
LI |
| PM10 |
1 |
2 |
1 |
0 |
0 |
| NO2 |
0 |
0 |
4 |
0 |
0 |
| SO2 |
0 |
2 |
1 |
1 |
0 |
| CO |
1 |
1 |
1 |
1 |
0 |
| O3 |
0 |
1 |
1 |
0 |
2 |
Example forecasts for
NO_2
The best model with a Seas-AR history is shown for the first four
test days, one panel per imputation: observed values in grey, forecasts
in colour, and the test RMSE of that model–imputation pair in the panel
title.
best_m <- sc |> filter(pollutant == "NO2", imputation == "Seas-AR") |> slice_min(RMSE, n = 1, with_ties = FALSE) |> pull(model)
rm10 <- sc |> filter(pollutant == "NO2", model == best_m) |> mutate(imputation = factor(imputation, levels = IMP_FC)) |> arrange(imputation)
lab10 <- setNames(sprintf("%s trained on a %s-imputed history (test RMSE %.2f)", best_m, rm10$imputation, rm10$RMSE), as.character(rm10$imputation))
key("fig10_model", best_m)
d10 <- fc_all |> filter(pollutant == "NO2", model == best_m, origin < min(origin) + 4 * 48) |>
mutate(time = TS[origin + h], panel = factor(lab10[imputation], levels = lab10))
p10 <- ggplot(d10, aes(time)) + geom_line(aes(y = y), colour = "grey30", linewidth = 0.35, na.rm = TRUE) +
geom_line(aes(y = yhat, colour = imputation), linewidth = 0.6) + facet_wrap(~panel, ncol = 1) +
scale_colour_manual(values = COL[IMP_FC], guide = "none") +
scale_x_datetime(date_breaks = "1 day", date_labels = "%d %b") +
labs(x = NULL, y = expression(NO[2] ~ "[" * mu * "g/m"^3 * "]"))
print(p10); save_fig(p10, "fig10_forecast_example", 7.16, 4.2)

LS0tCnRpdGxlOiAiRG91YmxlLVNlYXNvbmFsIERlY29tcG9zaXRpb24gSW1wdXRhdGlvbiBXaXRoIGFuIEF1dG9yZWdyZXNzaXZlIEJyaWRnZSBmb3IgSGFsZi1Ib3VybHkgQWlyIFBvbGx1dGFudCBTZXJpZXMsIGFuZCBJdHMgSW1wYWN0IG9uIE1hY2hpbmUgTGVhcm5pbmcgYW5kIERlZXAgTGVhcm5pbmcgRm9yZWNhc3RpbmciCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiB0cnVlCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUKICAgIG1hdGhfbWV0aG9kOiBrYXRleApwYXJhbXM6CiAgZGF0YV9wYXRoOgogICAgbGFiZWw6ICJEYXRhIGZpbGUgKENTVikiCiAgICB2YWx1ZTogImFxbXNfc3VyYWJheWFfMjAxOC5jc3YiCiAgICBpbnB1dDogZmlsZQogIG1hc2tfZGlyOiAibWFza3MiCiAgb3V0X2RpcjogInJlc3VsdHNfUiIKICBmaWdfZGlyOiAiZmlndXJlc19SIgogIHRhYl9kaXI6ICJ0YWJsZXNfUiIKICBtYXNrX3NvdXJjZTogImZpbGUiCiAgdXNlX2NhY2hlOiB0cnVlCiAgcXVpY2s6IGZhbHNlCiAgc2VlZHM6IFsxLCAyXQogIHBhcGVyX3JlZnM6IGZhbHNlCi0tLQoKYGBge2NzcywgZWNobyA9IEZBTFNFfQp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpIHsKICB3aWR0aDogYXV0byAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IG1hcmdpbjogMC42ZW0gMCAxLjZlbSAwICFpbXBvcnRhbnQ7CiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTsgYm9yZGVyLXRvcDogMnB4IHNvbGlkICMyYzNlNTA7IGJvcmRlci1ib3R0b206IDJweCBzb2xpZCAjMmMzZTUwOwogIGZvbnQtc2l6ZTogMC45ZW07IGZvbnQtdmFyaWFudC1udW1lcmljOiB0YWJ1bGFyLW51bXM7Cn0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSBjYXB0aW9uIHsKICBjYXB0aW9uLXNpZGU6IHRvcDsgdGV4dC1hbGlnbjogbGVmdDsgY29sb3I6ICMyYzNlNTA7IGZvbnQtd2VpZ2h0OiA2MDA7IHBhZGRpbmc6IDAgMCA2cHggMDsKfQp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpID4gdGhlYWQgPiB0ciA+IHRoIHsKICBiYWNrZ3JvdW5kOiAjZjJmNWY4OyBib3JkZXItdG9wOiBub25lICFpbXBvcnRhbnQ7IGJvcmRlci1ib3R0b206IDFweCBzb2xpZCAjMmMzZTUwICFpbXBvcnRhbnQ7CiAgZm9udC13ZWlnaHQ6IDYwMDsgdmVydGljYWwtYWxpZ246IGJvdHRvbSAhaW1wb3J0YW50Owp9CnRhYmxlOm5vdCgudWktZGF0ZXBpY2tlci1jYWxlbmRhcikgPiB0aGVhZCA+IHRyID4gdGgsCnRhYmxlOm5vdCgudWktZGF0ZXBpY2tlci1jYWxlbmRhcikgPiB0Ym9keSA+IHRyID4gdGQgeyBwYWRkaW5nOiA0cHggMTBweCAhaW1wb3J0YW50OyBib3JkZXItdG9wOiBub25lICFpbXBvcnRhbnQ7IH0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSA+IHRoZWFkID4gdHIgPiB0aFthbGlnbj0icmlnaHQiXSB7IHRleHQtYWxpZ246IHJpZ2h0ICFpbXBvcnRhbnQ7IH0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSA+IHRoZWFkID4gdHIgPiB0aFthbGlnbj0iY2VudGVyIl0geyB0ZXh0LWFsaWduOiBjZW50ZXIgIWltcG9ydGFudDsgfQp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpID4gdGhlYWQgPiB0ciA+IHRoOmZpcnN0LWNoaWxkLAp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpID4gdGJvZHkgPiB0ciA+IHRkOmZpcnN0LWNoaWxkIHsgdGV4dC1hbGlnbjogbGVmdCAhaW1wb3J0YW50OyB3aGl0ZS1zcGFjZTogbm93cmFwOyB9CnRhYmxlOm5vdCgudWktZGF0ZXBpY2tlci1jYWxlbmRhcikgPiB0Ym9keSA+IHRyOm50aC1jaGlsZChldmVuKSB7IGJhY2tncm91bmQ6ICNmOGZhZmM7IH0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSA+IHRib2R5ID4gdHI6aG92ZXIgeyBiYWNrZ3JvdW5kOiAjZTlmMWZhOyB9CmBgYAoKIyBEYXRhCgojIyBOb3RhdGlvbiBhbmQgdGltZSBpbmRleAoKRWFjaCBzZXJpZXMgaXMgJHlfMSxcZG90cyx5X25cZ2UwJCB3aXRoICRuPTE2eyx9OTkyJCBoYWxmLWhvdXJseSByZWNvcmRzIGZyb20gMSBKYW51YXJ5IDIwMTggMDA6MzAKdG8gMjEgRGVjZW1iZXIgMjAxOCAwMDowMCwgdGhhdCBpcywgMzU0IGRhaWx5IGJsb2NrcyBydW5uaW5nIGZyb20gMDA6MzAgdG8gMjQ6MDAuIEZvciBlYWNoIHRpbWUgJHQkLAoKJCQKaCh0KT1cYmlnKCh0LTEpXGJtb2QgNDhcYmlnKSsxLFxxdWFkIGsodCk9XEJpZ1xsY2VpbCBcdGZyYWN7dH17NDh9XEJpZ1xyY2VpbCxccXVhZApkKHQpPVxiaWcoKGsodCktMSlcYm1vZCA3XGJpZykrMSxccXVhZCBcZWxsKHQpPVxCaWdcbGNlaWwgXHRmcmFje3R9ezMzNn1cQmlnXHJjZWlsCiQkCgphcmUgdGhlIGhhbGYtaG91ciBvZiB0aGUgZGF5LCB0aGUgZGF5LCB0aGUgd2Vla2RheSAoMSA9IE1vbmRheSkgYW5kIHRoZSB3ZWVrLiBUaGUgZGFpbHkgcGVyaW9kIGlzCiRzXzE9NDgkIGFuZCB0aGUgd2Vla2x5IHBlcmlvZCBpcyAkc18yPTMzNiQuICRcbWF0aGNhbCBPJCBkZW5vdGVzIHRoZSBvYnNlcnZlZCBpbmRpY2VzIGFuZAokXG1hdGhjYWwgTT1cezEsXGRvdHMsblx9XHNldG1pbnVzXG1hdGhjYWwgTyQgdGhlIG1pc3Npbmcgb25lcy4gVGhlIGNvZGUgY2hlY2tzIHRoYXQgdGhlIGNvbHVtbnMgYEpgCmFuZCBgSGFyaWAgb2YgdGhlIGZpbGUgZXF1YWwgJGgodCkkIGFuZCAkZCh0KSQuCgpgYGB7ciBkYXRhfQppZiAoIWZpbGUuZXhpc3RzKFAkZGF0YV9wYXRoKSkKICBzdG9wKCJEYXRhIGZpbGUgbm90IGZvdW5kOiAiLCBQJGRhdGFfcGF0aCwgIi4gUHV0IGFxbXNfc3VyYWJheWFfMjAxOC5jc3YgaW4gdGhlIGRhdGEvIGZvbGRlciBuZXh0IHRvIHRoaXMgbm90ZWJvb2sgIiwKICAgICAgICJvciBzZXQgZGF0YV9wYXRoIGluIHRoZSBZQU1MIGhlYWRlci4iLCBjYWxsLiA9IEZBTFNFKQpyYXcgPC0gcmVhZC5jc3YoUCRkYXRhX3BhdGgsIGNoZWNrLm5hbWVzID0gRkFMU0UpCm5hbWVzKHJhdykgPC0gZ3N1YigiXyIsICIgIiwgbmFtZXMocmF3KSkKTiAgPC0gbnJvdyhyYXcpOyBTMSA8LSA0OEw7IFMyIDwtIDMzNkwKVFMgPC0gc2VxKGFzLlBPU0lYY3QoIjIwMTgtMDEtMDEgMDA6MzAiLCB0eiA9ICJVVEMiKSwgYnkgPSAiMzAgbWluIiwgbGVuZ3RoLm91dCA9IE4pClBPTFMgICAgIDwtIGMoIlBNMTAiLCAiTk8yIiwgIlNPMiIsICJDTyIsICJPMyIpClNUQVRJT05TIDwtIGMoIlNVRiAxIiwgIlNVRiA2IiwgIlNVRiA3IikKVU5JVCA8LSBjKFBNMTAgPSAiXHUwMGI1Zy9tXHUwMGIzIiwgTk8yID0gIlx1MDBiNWcvbVx1MDBiMyIsIFNPMiA9ICJcdTAwYjVnL21cdTAwYjMiLCBDTyA9ICJtZy9tXHUwMGIzIiwgTzMgPSAiXHUwMGI1Zy9tXHUwMGIzIikKaF9vZiA8LSBmdW5jdGlvbih0KSAoKHQgLSAxKSAlJSBTMSkgKyAxICAgICAgICAgICAgICAgICAjIGhhbGYtaG91ciBvZiB0aGUgZGF5ICgxID0gMDA6MzApCmRfb2YgPC0gZnVuY3Rpb24odCkgKCgodCAtIDEpICUvJSBTMSkgJSUgNykgKyAxICAgICAgICAgIyB3ZWVrZGF5ICgxID0gTW9uZGF5KQpTRVJJRVMgPC0gcGFzdGUocmVwKFNUQVRJT05TLCBlYWNoID0gNSksIFBPTFMpCmlmICghYWxsKGMoIkoiLCAiSGFyaSIsIFNFUklFUykgJWluJSBuYW1lcyhyYXcpKSkKICBzdG9wKCJUaGUgZGF0YSBmaWxlIGRvZXMgbm90IGhhdmUgdGhlIGV4cGVjdGVkIGNvbHVtbnMgKEosIEhhcmkgYW5kICdTVUYgMSBDTycgLi4uICdTVUYgNyBTTzInKS4iLCBjYWxsLiA9IEZBTFNFKQpzdG9waWZub3QoTiA9PSAxNjk5MiwgYWxsKHJhdyRKID09IGhfb2Yoc2VxX2xlbihOKSkpLCBhbGwocmF3JEhhcmkgPT0gZF9vZihzZXFfbGVuKE4pKSkpCmdldF9zZXJpZXMgPC0gZnVuY3Rpb24oc3QsIHApIGFzLm51bWVyaWMocmF3W1twYXN0ZShzdCwgcCldXSkKIyBjaGVjayBhZ2FpbnN0IHRoZSByZWZlcmVuY2UgZGF0YSwgZnJvbSB3aGljaCBhbGwgc3RvcmVkIHJlc3VsdHMgd2VyZSBjb21wdXRlZApYIDwtIHNhcHBseShTRVJJRVMsIGZ1bmN0aW9uKHYpIGFzLm51bWVyaWMocmF3W1t2XV0pKQpkYXRhX2NoZWNrIDwtIGRhdGEuZnJhbWUoaXRlbSA9IGMoInJvd3MiLCAibWlzc2luZyB2YWx1ZXMiLCAic3VtIG9mIHRoZSBvYnNlcnZlZCB2YWx1ZXMiKSwKICAgICAgICAgICAgICAgICAgICAgICAgIGZvdW5kID0gYyhOLCBzdW0oaXMubmEoWCkpLCBzdW0oWCwgbmEucm0gPSBUUlVFKSksIHJlZmVyZW5jZSA9IGMoMTY5OTIsIDMzMjUwLCA1MDIyNjQ2LjM3NTczNDI0MzUpKQpkYXRhX2NoZWNrJGlkZW50aWNhbCA8LSBhYnMoZGF0YV9jaGVjayRmb3VuZCAtIGRhdGFfY2hlY2skcmVmZXJlbmNlKSA8PSAxZS02ICogcG1heCgxLCBhYnMoZGF0YV9jaGVjayRyZWZlcmVuY2UpKQppZiAoIWFsbChkYXRhX2NoZWNrJGlkZW50aWNhbCkpIGNhdCgiTk9URTogdGhlIGRhdGEgZGlmZmVyIGZyb20gdGhlIHJlZmVyZW5jZSBkYXRhOyBzZXQgdXNlX2NhY2hlOiBmYWxzZSBzbyB0aGF0IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImV2ZXJ5IHJlc3VsdCBpcyByZWNvbXB1dGVkIGZyb20gdGhlc2UgZGF0YS5cbiIpCmtuaXRyOjprYWJsZSh0cmFuc2Zvcm0oZGF0YV9jaGVjaywgZm91bmQgPSBzcHJpbnRmKGMoIiUuMGYiLCAiJS4wZiIsICIlLjRmIiksIGZvdW5kKSwKICAgICAgICAgICAgICAgICAgICAgICByZWZlcmVuY2UgPSBzcHJpbnRmKGMoIiUuMGYiLCAiJS4wZiIsICIlLjRmIiksIHJlZmVyZW5jZSkpLAogICAgICAgICAgICAgY2FwdGlvbiA9ICJEYXRhIGNoZWNrIGFnYWluc3QgdGhlIHJlZmVyZW5jZSBkYXRhIHNldCIpClRFU1RfREFURSA8LSBhcy5QT1NJWGN0KCIyMDE4LTEyLTAxIDAwOjMwIiwgdHogPSAiVVRDIikKY2F0KCJuID0iLCBOLCAifCBwZXJpb2Q6IiwgZm9ybWF0KFRTWzFdKSwgInRvIiwgZm9ybWF0KFRTW05dKSwgInwgY29sdW1ucyBKIGFuZCBIYXJpIHZlcmlmaWVkXG4iKQpgYGAKCiMjIE1pc3NpbmduZXNzIGFuZCBkZXNjcmlwdGl2ZSBzdGF0aXN0aWNzCgoKYGBge3IgdGFibGUxfQpnYXBfbGVuZ3RocyA8LSBmdW5jdGlvbih4KSB7IHIgPC0gcmxlKGlzLm5hKHgpKTsgciRsZW5ndGhzW3IkdmFsdWVzXSB9CnRhYjEgPC0gYmluZF9yb3dzKGxhcHBseShQT0xTLCBmdW5jdGlvbihwKSB7CiAgeCA8LSBnZXRfc2VyaWVzKCJTVUYgMSIsIHApOyBnIDwtIGdhcF9sZW5ndGhzKHgpOyBvIDwtIHhbIWlzLm5hKHgpXQogIGRhdGEuZnJhbWUocG9sbHV0YW50ID0gcCwgdW5pdCA9IFVOSVRbW3BdXSwgbWlzc2luZyA9IDEwMCAqIG1lYW4oaXMubmEoeCkpLCBnYXBzID0gbGVuZ3RoKGcpLAogICAgICAgICAgICAgbWVkaWFuX2dhcCA9IGZsb29yKG1lZGlhbihnKSksIG1heF9nYXAgPSBtYXgoZyksIGluX2xvbmcgPSAxMDAgKiBzdW0oZ1tnID4gNDhdKSAvIHN1bShnKSwKICAgICAgICAgICAgIG1lYW4gPSBtZWFuKG8pLCBtZWRpYW4gPSBtZWRpYW4obyksIHNkID0gc2RfcG9wKG8pLCBtYXggPSBtYXgobyksCiAgICAgICAgICAgICBzdWY2ID0gMTAwICogbWVhbihpcy5uYShnZXRfc2VyaWVzKCJTVUYgNiIsIHApKSksIHN1ZjcgPSAxMDAgKiBtZWFuKGlzLm5hKGdldF9zZXJpZXMoIlNVRiA3IiwgcCkpKSkKfSkpCndyaXRlLmNzdih0YWIxLCBvdXRfZmlsZSgidGFibGUxX2RhdGEuY3N2IiksIHJvdy5uYW1lcyA9IEZBTFNFKQp3cml0ZV90ZXgoYygiXFxiZWdpbnt0YWJsZSp9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgIlxcY2FwdGlvbntNaXNzaW5nbmVzcyBhbmQgZGVzY3JpcHRpdmUgc3RhdGlzdGljcyBvZiB0aGUgaGFsZi1ob3VybHkgcG9sbHV0YW50IHNlcmllcyAoMX5KYW4tLTIwfkRlYyAyMDE4LCAkbj0xNnssfTk5MiQgcGVyIHNlcmllcykuIEdhcCBzdGF0aXN0aWNzIHJlZmVyIHRvIFNVRn4xOyB0aGUgbGFzdCB0d28gY29sdW1ucyBnaXZlIHRoZSBtaXNzaW5nIHJhdGUgYXQgdGhlIHR3byBkZXZlbG9wbWVudCBzdGF0aW9ucy59IiwKICAiXFxsYWJlbHt0YWI6ZGF0YX0iLCAiXFxzZXRsZW5ndGh7XFx0YWJjb2xzZXB9ezRwdH0iLCAiXFxiZWdpbnt0YWJ1bGFyfXtsbHJycnJycnJycnJyfSIsICJcXHRvcHJ1bGUiLAogICIgJiAmIFxcbXVsdGljb2x1bW57NX17Y317U1VGfjEgbWlzc2luZ25lc3N9ICYgXFxtdWx0aWNvbHVtbns0fXtjfXtTVUZ+MSBvYnNlcnZlZCB2YWx1ZXN9ICYgXFxtdWx0aWNvbHVtbnsyfXtjfXtNaXNzaW5nIChcXCUpfVxcXFwiLAogICJcXGNtaWRydWxlKGxyKXszLTd9XFxjbWlkcnVsZShscil7OC0xMX1cXGNtaWRydWxlKGxyKXsxMi0xM30iLAogICJQb2xsdXRhbnQgJiBVbml0ICYgTWlzc2luZyAoXFwlKSAmIEdhcHMgJiBNZWRpYW4gZ2FwICYgTWF4LiBnYXAgJiBJbiBnYXBzICQ+JDFcXCxkIChcXCUpICYgTWVhbiAmIE1lZGlhbiAmIFNEICYgTWF4LiAmIFNVRn42ICYgU1VGfjdcXFxcIiwKICAiXFxtaWRydWxlIiwKICB3aXRoKHRhYjEsIHNwcmludGYoIiVzICYgJXMgJiAlLjFmICYgJWQgJiAlZCAmICVzICYgJS4xZiAmICUuMmYgJiAlLjJmICYgJS4yZiAmICUuMWYgJiAlLjFmICYgJS4xZlxcXFwiLAogICAgUE9MX1RFWFtwb2xsdXRhbnRdLCBVTklUX1RFWFtwb2xsdXRhbnRdLCBtaXNzaW5nLCBnYXBzLCBhcy5pbnRlZ2VyKG1lZGlhbl9nYXApLAogICAgZm9ybWF0QyhtYXhfZ2FwLCBmb3JtYXQgPSAiZCIsIGJpZy5tYXJrID0gIiwiKSwgaW5fbG9uZywgbWVhbiwgbWVkaWFuLCBzZCwgbWF4LCBzdWY2LCBzdWY3KSksCiAgIlxcYm90dG9tcnVsZSIsICJcXGVuZHt0YWJ1bGFyfSIsICJcXGVuZHt0YWJsZSp9IiksICJ0YWJfZGF0YSIpCmtuaXRyOjprYWJsZSh0YWIxLCBkaWdpdHMgPSBjKDAsIDAsIDEsIDAsIDAsIDAsIDEsIDIsIDIsIDIsIDEsIDEsIDEpLAogIGNvbC5uYW1lcyA9IGMoIlBvbGx1dGFudCIsICJVbml0IiwgIk1pc3NpbmcgKCUpIiwgIkdhcHMiLCAiTWVkaWFuIGdhcCIsICJNYXguIGdhcCIsICJJbiBnYXBzID4gMSBkICglKSIsCiAgICAgICAgICAgICAgICAiTWVhbiIsICJNZWRpYW4iLCAiU0QiLCAiTWF4LiIsICJTVUYgNiBtaXNzaW5nICglKSIsICJTVUYgNyBtaXNzaW5nICglKSIpLAogIGNhcHRpb24gPSBjYXAoIlRhYmxlIDIiLCAiTWlzc2luZ25lc3MgYW5kIGRlc2NyaXB0aXZlIHN0YXRpc3RpY3MgKGdhcCBzdGF0aXN0aWNzOiBTVUYgMSkiKSkKYGBgCgojIyBNaXNzaW5nIHZhbHVlcyBvbiB0aGUgdGltZSBheGlzCgpQYW5lbCAoYSkgc2hvd3MgdGhlIGZpdmUgaGFsZi1ob3VybHkgc2VyaWVzIG9mIFNVRiAxIG9uIHRoZWlyIHJlYWwgdGltZXN0YW1wcy4gRWFjaCBydW4gb2YgbWlzc2luZyB2YWx1ZXMKaXMgc2hhZGVkIHJlZCwgYXMgaW4gYGltcHV0ZVRTOjpnZ3Bsb3RfbmFfZGlzdHJpYnV0aW9uKClgLCBhbmQgdGhlIGxhYmVsIGdpdmVzIHRoZSBzaGFyZSBvZiBtaXNzaW5nIHZhbHVlczsKdGhlIGJsdWUgYmFuZCBpcyB0aGUgdGVzdCBwZXJpb2QuIFBhbmVsIChiKSBzcGxpdHMgdGhlIG1pc3NpbmcgdmFsdWVzIGJ5IGdhcCBsZW5ndGguIFRoZSBkYXRhIGZyYW1lCmBnYXBzX2RmYCwgd2l0aCB0aGUgc3RhcnQgYW5kIGVuZCBvZiBldmVyeSBnYXAsIGlzIHJldXNlZCBmb3IgdGhlIG92ZXJ2aWV3IGRpYWdyYW0gYXQgdGhlIGVuZC4KCmBgYHtyIGZpZzEsIGZpZy53aWR0aD03LjE2LCBmaWcuaGVpZ2h0PTMuNTV9CiMgZmlnMDFfbWlzc2luZ25lc3M6IChhKSB0aGUgaGFsZi1ob3VybHkgc2VyaWVzIG9mIFNVRiAxIG9uIHRoZWlyIHJlYWwgdGltZXN0YW1wcy4gRXZlcnkgcnVuIG9mIG1pc3NpbmcgdmFsdWVzIGlzCiMgbWFya2VkIGJ5IGEgcmVkIHRpY2sgYXQgdGhlIGJvdHRvbSwgZ2FwcyBsb25nZXIgdGhhbiB0d28gaG91cnMgYXJlIGFsc28gc2hhZGVkIHJlZCAodGhlIHZpZXcgb2YKIyBpbXB1dGVUUzo6Z2dwbG90X25hX2Rpc3RyaWJ1dGlvbiksIGFuZCB0aGUgdGVzdCBwZXJpb2QgaXMgc2hhZGVkIGJsdWUuIChiKSBTaGFyZSBvZiB0aGUKIyBtaXNzaW5nIHZhbHVlcyBieSBnYXAgbGVuZ3RoLgpuYV9ydW5zIDwtIGZ1bmN0aW9uKHN0LCBwb2xzID0gUE9MUykgYmluZF9yb3dzKGxhcHBseShwb2xzLCBmdW5jdGlvbihwKSB7CiAgeCA8LSBnZXRfc2VyaWVzKHN0LCBwKTsgciA8LSBybGUoaXMubmEoeCkpOyBlIDwtIGN1bXN1bShyJGxlbmd0aHMpOyBzIDwtIGUgLSByJGxlbmd0aHMgKyAxCiAgZGF0YS5mcmFtZShzdGF0aW9uID0gc3QsIHBvbGx1dGFudCA9IHAsIHN0YXJ0ID0gVFNbc1tyJHZhbHVlc11dLCBlbmQgPSBUU1tlW3IkdmFsdWVzXV0gKyAxODAwLAogICAgICAgICAgICAgbGVuID0gciRsZW5ndGhzW3IkdmFsdWVzXSkKfSkpClBPTF9MQUIgPC0gYyhQTTEwID0gImF0b3AoUE1bMTBdLCAnXHUwMGI1Zy9tXHUwMGIzJykiLCBOTzIgPSAiYXRvcChOT1syXSwgJ1x1MDBiNWcvbVx1MDBiMycpIiwKICAgICAgICAgICAgIFNPMiA9ICJhdG9wKFNPWzJdLCAnXHUwMGI1Zy9tXHUwMGIzJykiLCBDTyA9ICJhdG9wKENPLCAnbWcvbVx1MDBiMycpIiwgTzMgPSAiYXRvcChPWzNdLCAnXHUwMGI1Zy9tXHUwMGIzJykiKQpwbG90X25hX3NlcmllcyA8LSBmdW5jdGlvbihzdGF0aW9ucywgdGl0bGUpIHsKICBkIDwtIGJpbmRfcm93cyhsYXBwbHkoc3RhdGlvbnMsIGZ1bmN0aW9uKHN0KSBiaW5kX3Jvd3MobGFwcGx5KFBPTFMsIGZ1bmN0aW9uKHApCiAgICAgICAgIGRhdGEuZnJhbWUoc3RhdGlvbiA9IHN0LCBwb2xsdXRhbnQgPSBwLCB0ID0gVFMsIHkgPSBnZXRfc2VyaWVzKHN0LCBwKSkpKSkpIHw+CiAgICAgICBtdXRhdGUocG9sbHV0YW50ID0gZmFjdG9yKHBvbGx1dGFudCwgUE9MUykpCiAgZyA8LSBiaW5kX3Jvd3MobGFwcGx5KHN0YXRpb25zLCBuYV9ydW5zKSkgfD4gbXV0YXRlKHBvbGx1dGFudCA9IGZhY3Rvcihwb2xsdXRhbnQsIFBPTFMpKQogIHIgPC0gZCB8PiBncm91cF9ieShzdGF0aW9uLCBwb2xsdXRhbnQpIHw+IHN1bW1hcmlzZShyYXRlID0gbWVhbihpcy5uYSh5KSksIC5ncm91cHMgPSAiZHJvcCIpCiAgZ2dwbG90KGQsIGFlcyh0LCB5KSkgKwogICAgYW5ub3RhdGUoInJlY3QiLCB4bWluID0gVEVTVF9EQVRFLCB4bWF4ID0gbWF4KFRTKSwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYsIGZpbGwgPSAiIzI0NzFhMyIsIGFscGhhID0gMC4xOCkgKwogICAgZ2VvbV9yZWN0KGRhdGEgPSBnW2ckbGVuID4gNCwgXSwgYWVzKHhtaW4gPSBzdGFydCwgeG1heCA9IGVuZCwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYpLCBpbmhlcml0LmFlcyA9IEZBTFNFLAogICAgICAgICAgICAgIGZpbGwgPSAiI2YyYjhiMCIpICsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBnYXBzIGxvbmdlciB0aGFuIHR3byBob3VycyBhcmUgc2hhZGVkCiAgICBnZW9tX3J1ZyhkYXRhID0gZywgYWVzKHggPSBzdGFydCksIGluaGVyaXQuYWVzID0gRkFMU0UsIHNpZGVzID0gImIiLCBjb2xvdXIgPSAiI2MwMzkyYiIsCiAgICAgICAgICAgICBsaW5ld2lkdGggPSAwLjE1LCBsZW5ndGggPSB1bml0KDAuMTIsICJucGMiKSkgKyAgICAgICMgZXZlcnkgZ2FwLCBpbmNsdWRpbmcgc2luZ2xlIHJlYWRpbmdzCiAgICBnZW9tX2xpbmUobGluZXdpZHRoID0gMC4xMiwgY29sb3VyID0gImdyZXkxNSIsIG5hLnJtID0gVFJVRSkgKwogICAgZ2VvbV9sYWJlbChkYXRhID0gciwgYWVzKHggPSBtaW4oVFMpICsgMyAqIDg2NDAwLCB5ID0gSW5mLCBsYWJlbCA9IHNwcmludGYoIiUuMWYlJSBtaXNzaW5nIiwgMTAwICogcmF0ZSkpLAogICAgICAgICAgICAgICBpbmhlcml0LmFlcyA9IEZBTFNFLCBoanVzdCA9IDAsIHZqdXN0ID0gMS4xLCBzaXplID0gMi41LCBmYW1pbHkgPSBGT05UX1RFWFQsIGNvbG91ciA9ICIjOTIyYjIxIiwKICAgICAgICAgICAgICAgZmlsbCA9IGFscGhhKCJ3aGl0ZSIsIDAuODUpLCBsYWJlbC5zaXplID0gMCwgbGFiZWwucGFkZGluZyA9IHVuaXQoMC4xLCAibGluZXMiKSkgKwogICAgZmFjZXRfZ3JpZChpZiAobGVuZ3RoKHN0YXRpb25zKSA+IDEpIHBvbGx1dGFudCB+IHN0YXRpb24gZWxzZSBwb2xsdXRhbnQgfiAuLCBzY2FsZXMgPSAiZnJlZV95Iiwgc3dpdGNoID0gInkiLAogICAgICAgICAgICAgICBsYWJlbGxlciA9IGxhYmVsbGVyKHBvbGx1dGFudCA9IGFzX2xhYmVsbGVyKFBPTF9MQUIsIGxhYmVsX3BhcnNlZCkpKSArCiAgICBzY2FsZV94X2RhdGV0aW1lKGRhdGVfYnJlYWtzID0gaWYgKGxlbmd0aChzdGF0aW9ucykgPiAxKSAiMiBtb250aHMiIGVsc2UgIjEgbW9udGgiLCBkYXRlX2xhYmVscyA9ICIlYiIsCiAgICAgICAgICAgICAgICAgICAgIGV4cGFuZCA9IGV4cGFuc2lvbihtdWx0ID0gMC4wMDQpKSArCiAgICBzY2FsZV95X2NvbnRpbnVvdXMobi5icmVha3MgPSAzLCBleHBhbmQgPSBleHBhbnNpb24obXVsdCA9IGMoMC4wMywgMC4zOCkpKSArCiAgICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgdGl0bGUgPSB0aXRsZSkgKwogICAgdGhlbWUoc3RyaXAucGxhY2VtZW50ID0gIm91dHNpZGUiLCBzdHJpcC50ZXh0LnkubGVmdCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsIHNpemUgPSA3LjgpLCBheGlzLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuMiksCiAgICAgICAgICBwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkLm1ham9yLnkgPSBlbGVtZW50X2JsYW5rKCksCiAgICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2xpbmUobGluZXdpZHRoID0gMC4yLCBjb2xvdXIgPSAiZ3JleTg4IiksCiAgICAgICAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA4LjYpLCBwYW5lbC5zcGFjaW5nLnkgPSB1bml0KDMsICJwdCIpKQp9CmNscyA8LSBkYXRhLmZyYW1lKGxvID0gYygxLCA1LCA0OSwgMzM3KSwgaGkgPSBjKDQsIDQ4LCAzMzYsIEluZiksIGxhYiA9IGMoIjEtNCIsICI1LTQ4IiwgIjQ5LTMzNiIsICI+MzM2IikpCnNoYXJlX2RmIDwtIGJpbmRfcm93cyhsYXBwbHkoUE9MUywgZnVuY3Rpb24ocCkgewogIGcgPC0gZ2FwX2xlbmd0aHMoZ2V0X3NlcmllcygiU1VGIDEiLCBwKSkKICBkYXRhLmZyYW1lKHBvbGx1dGFudCA9IGZhY3RvcihwLCBsZXZlbHMgPSBQT0xTKSwgY2xhc3MgPSBmYWN0b3IoY2xzJGxhYiwgbGV2ZWxzID0gY2xzJGxhYiksCiAgICAgICAgICAgICBzaGFyZSA9IHNhcHBseSgxOjQsIGZ1bmN0aW9uKGkpIDEwMCAqIHN1bShnW2cgPj0gY2xzJGxvW2ldICYgZyA8PSBjbHMkaGlbaV1dKSAvIHN1bShnKSkpCn0pKQpwMWEgPC0gcGxvdF9uYV9zZXJpZXMoIlNVRiAxIiwgIihhKSBIYWxmLWhvdXJseSBzZXJpZXMgYXQgU1VGIDE6IGdhcHMgKHJlZCB0aWNrczsgc2hhZGVkIGlmIGxvbmdlciB0aGFuIDIgaCkgYW5kIHRlc3QgcGVyaW9kIChibHVlKSIpCnAxYiA8LSBnZ3Bsb3Qoc2hhcmVfZGYsIGFlcyhzaGFyZSwgMSwgZmlsbCA9IGNsYXNzKSkgKwogIGdlb21fY29sKHBvc2l0aW9uID0gcG9zaXRpb25fc3RhY2socmV2ZXJzZSA9IFRSVUUpLCB3aWR0aCA9IDAuNTUsIG9yaWVudGF0aW9uID0gInkiKSArCiAgZmFjZXRfZ3JpZChwb2xsdXRhbnQgfiAuKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IGMoMCwgNTAsIDEwMCksIGxhYmVscyA9IGZ1bmN0aW9uKHYpIHBhc3RlMCh2LCAiJSIpLCBleHBhbmQgPSBjKDAsIDApKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGV4cGFuZCA9IGV4cGFuc2lvbihhZGQgPSAwLjYpKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygiI2Y1YjdiMSIsICIjZWM3MDYzIiwgIiNjMDM5MmIiLCAiIzY0MWUxNiIpLCBndWlkZSA9IGd1aWRlX2xlZ2VuZChucm93ID0gMSkpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgZmlsbCA9ICJHYXAgbGVuZ3RoIChoYWxmLWhvdXIgc3RlcHMpOiIsIHRpdGxlID0gIihiKSBTaGFyZSBieSBnYXAgbGVuZ3RoIikgKwogIHRoZW1lKHN0cmlwLnRleHQgPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGV4dC55ID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkID0gZWxlbWVudF9ibGFuaygpLAogICAgICAgIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDguNiksIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoMywgInB0IiksIHBsb3QubWFyZ2luID0gbWFyZ2luKDUuNSwgMTAsIDUuNSwgNS41KSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuMiksCiAgICAgICAgbGVnZW5kLmtleS5zaXplID0gdW5pdCgwLjI4LCAiY20iKSwgbGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LjgpLCBsZWdlbmQudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gNy44KSkKcDEgPC0gaWYgKEhBU19QQVRDSFdPUkspIHBhdGNod29yazo6d3JhcF9wbG90cyhwMWEsIHAxYiwgd2lkdGhzID0gYygzLjUsIDEpLCBndWlkZXMgPSAiY29sbGVjdCIpICYKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgbGVnZW5kLm1hcmdpbiA9IG1hcmdpbigwLCAwLCAwLCAwKSkgZWxzZSBwMWEKcHJpbnQocDEpOyBzYXZlX2ZpZyhwMSwgImZpZzAxX21pc3NpbmduZXNzIiwgNy4xNiwgMy41NSkKaWYgKCFIQVNfUEFUQ0hXT1JLKSB7IHByaW50KHAxYik7IHNhdmVfZmlnKHAxYiwgImZpZzAxYl9nYXBfbGVuZ3RocyIsIDMuNSwgMi4xKSB9CmdhcHNfZGYgPC0gbmFfcnVucygiU1VGIDEiKSB8PiBtdXRhdGUoeSA9IG1hdGNoKHBvbGx1dGFudCwgcmV2KFBPTFMpKSkgICAgICAgIyByZXVzZWQgYnkgdGhlIG92ZXJ2aWV3IGRpYWdyYW0gKGZpZzAzKQpgYGAKCiMjIEFsbCAxNSBzZXJpZXMKClRoZSBzYW1lIHZpZXcgZm9yIHRoZSB0aHJlZSBzdGF0aW9ucy4gSXQgc2hvd3Mgd2hlcmUgZWFjaCBzZXJpZXMgaXMKbWlzc2luZyBiZWZvcmUgYW55IGltcHV0YXRpb24uCgpgYGB7ciBmaWdTMSwgZmlnLndpZHRoPTcuMTYsIGZpZy5oZWlnaHQ9NS42fQojIGZpZ1MxX3Nlcmllc19hbGxfc3RhdGlvbnM6IHRoZSBzYW1lIHZpZXcgZm9yIHRoZSAxNSBzZXJpZXMgb2YgdGhlIHRocmVlIHN0YXRpb25zLgpwUzEgPC0gcGxvdF9uYV9zZXJpZXMoYygiU1VGIDEiLCAiU1VGIDYiLCAiU1VGIDciKSwKICAgICAgICAgICAgICAgICAgICAgICJIYWxmLWhvdXJseSBzZXJpZXMgb2YgdGhlIHRocmVlIHN0YXRpb25zOiBnYXBzIChyZWQgdGlja3M7IHNoYWRlZCBpZiBsb25nZXIgdGhhbiAyIGgpIGFuZCB0ZXN0IHBlcmlvZCAoYmx1ZSkiKSArCiAgdGhlbWUoc3RyaXAudGV4dC54ID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LjUsIGZhY2UgPSAiYm9sZCIpKQpwcmludChwUzEpOyBzYXZlX2ZpZyhwUzEsICJmaWdTMV9zZXJpZXNfYWxsX3N0YXRpb25zIiwgNy4xNiwgNS42KQpgYGAKCiMjIERvdWJsZSBzZWFzb25hbGl0eQoKRm9yIGRheSBvZiB0aGUgd2VlayAkZCQgYW5kIGhhbGYtaG91ciAkaCQgdGhlIG1lYW4gcHJvZmlsZSBpcwokXGJhciB5X3tkLGh9PVxmcmFjezF9e3xcbWF0aGNhbCBPX3tkLGh9fH1cc3VtX3t0XGluXG1hdGhjYWwgT197ZCxofX15X3QkIHdpdGgKJFxtYXRoY2FsIE9fe2QsaH09XHt0XGluXG1hdGhjYWwgTzpkKHQpPWQsXCBoKHQpPWhcfSQsIGFuZCBpdHMgYXZlcmFnZSBvdmVyIHRoZSBzZXZlbiBkYXlzLAokXGJhciB5X3tcY2RvdCxofT1cZnJhYzE3XHN1bV97ZD0xfV57N31cYmFyIHlfe2QsaH0kLCBpcyB0aGUgbWVhbiBkYWlseSBwcm9maWxlLiBQYW5lbCAoYSkgZHJhd3MKJFxiYXIgeV97ZCxofSQgYWxvbmcgdGhlIDMzNiBoYWxmLWhvdXJzIG9mIHRoZSB3ZWVrIHRvZ2V0aGVyIHdpdGggJFxiYXIgeV97XGNkb3QsaH0kIHJlcGVhdGVkIG9uIGV2ZXJ5IGRheToKdGhlIHJlcGV0aXRpb24gaXMgdGhlIGRhaWx5IHNlYXNvbmFsaXR5LCBhbmQgdGhlIHNoYWRlZCBkaWZmZXJlbmNlICRcYmFyIHlfe2QsaH0tXGJhciB5X3tcY2RvdCxofSQsIHRoZSB3ZWVrbHkKZGV2aWF0aW9uLCBpcyB0aGUgd2Vla2x5IHNlYXNvbmFsaXR5LiBQYW5lbCAoYikgZXhhbWluZXMgdGhlIHdlZWtseSBwYXJ0IHdoZXJlIGl0IGlzIGxhcmdlc3QsIGluIHRoZSBtb3JuaW5nOgpmb3IgZXZlcnkgZGF0ZSB0aGUgbWVhbiBvZiAwNjowMOKAkzEwOjAwIGlzIGNvbXB1dGVkLCB0aGUgZGF0ZXMgYXJlIGdyb3VwZWQgYnkgZGF5IG9mIHRoZSB3ZWVrLCBhbmQgZWFjaCBncm91cAptZWFuIGlzIGV4cHJlc3NlZCByZWxhdGl2ZSB0byB0aGUgd29ya2luZy1kYXkgbWVhbiwgd2l0aCBhIDk1JSBjb25maWRlbmNlIGludGVydmFsIG9mICRccG0xLjk2JCBzdGFuZGFyZCBlcnJvcnMKYWNyb3NzIGRhdGVzLiBJbnRlcnZhbHMgd2lkZXIgdGhhbiAkXHBtNDVcJSQgKFBNJF97MTB9JCwgd2l0aCBtYW55IG1pc3NpbmcgbW9ybmluZ3MpIGFyZSBjdXQgYXQgdGhlIGVkZ2UuCgpgYGB7ciBmaWcyLCBmaWcud2lkdGg9Ny4xNiwgZmlnLmhlaWdodD0zLjZ9CiMgZmlnMDJfc2Vhc29uYWxpdHkuIChhKSBNZWFuIGNvbmNlbnRyYXRpb24gYXQgZWFjaCBvZiB0aGUgMzM2IGhhbGYtaG91cnMgb2YgdGhlIHdlZWsgKHNvbGlkKSBhbmQgdGhlIG1lYW4KIyBkYWlseSBwcm9maWxlIHJlcGVhdGVkIG9uIGV2ZXJ5IGRheSAoZGFzaGVkKTogdGhlIHJlcGV0aXRpb24gaXMgdGhlIGRhaWx5IHNlYXNvbmFsaXR5IGFuZCB0aGUgc2hhZGVkCiMgZGlmZmVyZW5jZSBpcyB0aGUgd2Vla2x5IHNlYXNvbmFsaXR5LiAoYikgTWVhbiBvZiAwNjowMC0xMDowMCBmb3IgZWFjaCBkYXkgb2YgdGhlIHdlZWsgcmVsYXRpdmUgdG8gdGhlCiMgd29ya2luZy1kYXkgbWVhbiwgd2l0aCA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMgY29tcHV0ZWQgZnJvbSB0aGUgZGFpbHkgbW9ybmluZyBtZWFucy4Kd2sgPC0gYmluZF9yb3dzKGxhcHBseShQT0xTLCBmdW5jdGlvbihwKSBkYXRhLmZyYW1lKHBvbGx1dGFudCA9IHAsIGQgPSBkX29mKHNlcV9sZW4oTikpLCBoID0gaF9vZihzZXFfbGVuKE4pKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSBnZXRfc2VyaWVzKCJTVUYgMSIsIHApKSkpIHw+CiAgZmlsdGVyKCFpcy5uYSh5KSkgfD4gZ3JvdXBfYnkocG9sbHV0YW50LCBkLCBoKSB8PiBzdW1tYXJpc2UobSA9IG1lYW4oeSksIC5ncm91cHMgPSAiZHJvcCIpIHw+CiAgZ3JvdXBfYnkocG9sbHV0YW50LCBoKSB8PiBtdXRhdGUoZHAgPSBtZWFuKG0pKSB8PiB1bmdyb3VwKCkgfD4gICAgICAgICAgICAgICAgIyBkcDogbWVhbiBkYWlseSBwcm9maWxlCiAgbXV0YXRlKGhvdyA9IChkIC0gMSkgKiA0OCArIGgsIHBvbGx1dGFudCA9IGZhY3Rvcihwb2xsdXRhbnQsIFBPTFMpKSAgICAgICAgICAgIyBob3c6IGhhbGYtaG91ciBvZiB0aGUgd2Vlawptb3JuaW5nIDwtIGJpbmRfcm93cyhsYXBwbHkoUE9MUywgZnVuY3Rpb24ocCkgewogIHkgPC0gZ2V0X3NlcmllcygiU1VGIDEiLCBwKTsgZGQgPC0gKHNlcV9sZW4oTikgLSAxKSAlLyUgNDg7IG1vIDwtIGhfb2Yoc2VxX2xlbihOKSkgJWluJSAxMjoxOSAgICAgIyAwNjowMC0xMDowMAogIGRtIDwtIHRhcHBseSh5W21vXSwgZGRbbW9dLCBmdW5jdGlvbih2KSBpZiAoc3VtKCFpcy5uYSh2KSkgPj0gNCkgbWVhbih2LCBuYS5ybSA9IFRSVUUpIGVsc2UgTkEpICAgIyBvbmUgbWVhbiBwZXIgZGF0ZQogIHdkIDwtIHRhcHBseShkX29mKHNlcV9sZW4oTikpW21vXSwgZGRbbW9dLCBgW2AsIDEpOyBvayA8LSAhaXMubmEoZG0pOyByZWYgPC0gbWVhbihkbVtvayAmIHdkIDw9IDVdKQogIGJpbmRfcm93cyhsYXBwbHkoMTo3LCBmdW5jdGlvbihrKSB7IHYgPC0gZG1bb2sgJiB3ZCA9PSBrXQogICAgZGF0YS5mcmFtZShwb2xsdXRhbnQgPSBwLCBkID0gaywgZXN0ID0gMTAwICogKG1lYW4odikgLyByZWYgLSAxKSwgc2UgPSAxMDAgKiBzZCh2KSAvIHNxcnQobGVuZ3RoKHYpKSAvIHJlZikgfSkpCn0pKSB8PiBtdXRhdGUocG9sbHV0YW50ID0gZmFjdG9yKHBvbGx1dGFudCwgUE9MUyksIGxvID0gZXN0IC0gMS45NiAqIHNlLCBoaSA9IGVzdCArIDEuOTYgKiBzZSkKZm9yIChwIGluIFBPTFMpIHsgc3UgPC0gbW9ybmluZ1ttb3JuaW5nJHBvbGx1dGFudCA9PSBwICYgbW9ybmluZyRkID09IDcsIF07IHNhIDwtIG1vcm5pbmdbbW9ybmluZyRwb2xsdXRhbnQgPT0gcCAmIG1vcm5pbmckZCA9PSA2LCBdCiAga2V5KHBhc3RlMCgic3VuX21vcm5pbmdfIiwgcCksIHN1JGVzdCk7IGtleShwYXN0ZTAoInN1bl9tb3JuaW5nX2xvXyIsIHApLCBzdSRsbyk7IGtleShwYXN0ZTAoInN1bl9tb3JuaW5nX2hpXyIsIHApLCBzdSRoaSkKICBrZXkocGFzdGUwKCJzYXRfbW9ybmluZ18iLCBwKSwgc2EkZXN0KSB9CkxJTkUyIDwtIGMoIm1lYW4gYXQgZWFjaCBoYWxmLWhvdXIgb2YgdGhlIHdlZWsiID0gIiMxZjRlNzkiLCAibWVhbiBkYWlseSBwcm9maWxlLCByZXBlYXRlZCIgPSAiIzVkNmQ3ZSIpCnAyYSA8LSBnZ3Bsb3Qod2ssIGFlcyhob3cpKSArCiAgYW5ub3RhdGUoInJlY3QiLCB4bWluID0gMjM5LjUsIHhtYXggPSAzMzUuNSwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYsIGZpbGwgPSAiZ3JleTkzIikgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDQ4ICogKDE6NikgLSAwLjUsIGNvbG91ciA9ICJncmV5NzAiLCBsaW5ld2lkdGggPSAwLjI1LCBsaW5ldHlwZSA9ICIyMiIpICsKICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHBtaW4obSwgZHApLCB5bWF4ID0gcG1heChtLCBkcCkpLCBmaWxsID0gIiNmNWNiYTciKSArCiAgZ2VvbV9saW5lKGFlcyh5ID0gZHAsIGNvbG91ciA9IG5hbWVzKExJTkUyKVsyXSwgbGluZXR5cGUgPSBuYW1lcyhMSU5FMilbMl0pLCBsaW5ld2lkdGggPSAwLjM1KSArCiAgZ2VvbV9saW5lKGFlcyh5ID0gbSwgY29sb3VyID0gbmFtZXMoTElORTIpWzFdLCBsaW5ldHlwZSA9IG5hbWVzKExJTkUyKVsxXSksIGxpbmV3aWR0aCA9IDAuNDUpICsKICBmYWNldF9ncmlkKHBvbGx1dGFudCB+IC4sIHNjYWxlcyA9ICJmcmVlX3kiLCBzd2l0Y2ggPSAieSIsIGxhYmVsbGVyID0gbGFiZWxsZXIocG9sbHV0YW50ID0gYXNfbGFiZWxsZXIoUE9MX0xBQiwgbGFiZWxfcGFyc2VkKSkpICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKE5VTEwsIHZhbHVlcyA9IExJTkUyLCBicmVha3MgPSBuYW1lcyhMSU5FMikpICsKICBzY2FsZV9saW5ldHlwZV9tYW51YWwoTlVMTCwgdmFsdWVzID0gc2V0TmFtZXMoYygic29saWQiLCAiMjIiKSwgbmFtZXMoTElORTIpKSwgYnJlYWtzID0gbmFtZXMoTElORTIpKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IDQ4ICogKDA6NikgKyAyMy41LCBsYWJlbHMgPSBjKCJNb24iLCAiVHVlIiwgIldlZCIsICJUaHUiLCAiRnJpIiwgIlNhdCIsICJTdW4iKSwgZXhwYW5kID0gYygwLCAwKSkgKwogIHNjYWxlX3lfY29udGludW91cyhuLmJyZWFrcyA9IDMsIGV4cGFuZCA9IGV4cGFuc2lvbihtdWx0ID0gYygwLjA0LCAwLjEpKSkgKwogIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMLCB0aXRsZSA9ICIoYSkgTWVhbiBjb25jZW50cmF0aW9uIG92ZXIgdGhlIHdlZWsgYXQgU1VGIDE7IHNoYWRlZDogd2Vla2x5IGRldmlhdGlvbiwgZ3JleSBiYW5kOiB3ZWVrZW5kIikgKwogIHRoZW1lKHN0cmlwLnBsYWNlbWVudCA9ICJvdXRzaWRlIiwgc3RyaXAudGV4dC55LmxlZnQgPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBzaXplID0gNy44KSwgYXhpcy50ZXh0ID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LjIpLAogICAgICAgIHBhbmVsLmdyaWQubWlub3IgPSBlbGVtZW50X2JsYW5rKCksIHBhbmVsLmdyaWQubWFqb3IueCA9IGVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnkgPSBlbGVtZW50X2xpbmUobGluZXdpZHRoID0gMC4xNSwgY29sb3VyID0gImdyZXk4OCIpLCBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA4LjYpLAogICAgICAgIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoMywgInB0IikpCmxpbSA8LSA0NSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgJSAod2lkZXIgaW50ZXJ2YWxzIGFyZSBjdXQgYXQgdGhlIGVkZ2UpCnAyYiA8LSBnZ3Bsb3QobW9ybmluZywgYWVzKGQsIGVzdCkpICsKICBhbm5vdGF0ZSgicmVjdCIsIHhtaW4gPSA1LjUsIHhtYXggPSA3LjUsIHltaW4gPSAtSW5mLCB5bWF4ID0gSW5mLCBmaWxsID0gImdyZXk5MyIpICsKICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLCBjb2xvdXIgPSAiZ3JleTQ1IiwgbGluZXdpZHRoID0gMC4zKSArCiAgZ2VvbV9lcnJvcmJhcihhZXMoeW1pbiA9IGxvLCB5bWF4ID0gaGksIGNvbG91ciA9IGQgPj0gNiksIHdpZHRoID0gMCwgbGluZXdpZHRoID0gMC41NSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91ciA9IGQgPj0gNiksIHNpemUgPSAxLjQpICsKICBmYWNldF9ncmlkKHBvbGx1dGFudCB+IC4pICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoYEZBTFNFYCA9ICIjMWY0ZTc5IiwgYFRSVUVgID0gIiNkMzU0MDAiKSwgZ3VpZGUgPSAibm9uZSIpICsKICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gMTo3LCBsYWJlbHMgPSBjKCJNIiwgIlQiLCAiVyIsICJUIiwgIkYiLCAiUyIsICJTIiksIGV4cGFuZCA9IGV4cGFuc2lvbihhZGQgPSAwLjUpKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGxpbWl0cyA9IGMoLWxpbSwgbGltKSwgYnJlYWtzID0gYygtMzAsIDMwKSwgbGFiZWxzID0gYygiXHUyMjEyMzAlIiwgIiszMCUiKSwgb29iID0gc2NhbGVzOjpvb2Jfc3F1aXNoKSArCiAgbGFicyh4ID0gTlVMTCwgeSA9IE5VTEwsIHRpdGxlID0gIihiKSBNb3JuaW5ncyAoMDZcdTIwMTMxMCBoKSIpICsKICB0aGVtZShzdHJpcC50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2JsYW5rKCksCiAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKGxpbmV3aWR0aCA9IDAuMTUsIGNvbG91ciA9ICJncmV5ODgiKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuMiksCiAgICAgICAgYXhpcy50ZXh0LnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDYuNiksIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDguNiksIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoMywgInB0IiksCiAgICAgICAgcGxvdC5tYXJnaW4gPSBtYXJnaW4oNS41LCA4LCA1LjUsIDUuNSkpCnAyIDwtIGlmIChIQVNfUEFUQ0hXT1JLKSBwYXRjaHdvcms6OndyYXBfcGxvdHMocDJhLCBwMmIsIHdpZHRocyA9IGMoMy4zLCAxLjEpLCBndWlkZXMgPSAiY29sbGVjdCIpICYKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgbGVnZW5kLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuOCksIGxlZ2VuZC5rZXkud2lkdGggPSB1bml0KDAuOCwgImNtIiksCiAgICAgICAgbGVnZW5kLm1hcmdpbiA9IG1hcmdpbigwLCAwLCAwLCAwKSkgZWxzZSBwMmEKcHJpbnQocDIpOyBzYXZlX2ZpZyhwMiwgImZpZzAyX3NlYXNvbmFsaXR5IiwgNy4xNiwgMy42KQprbml0cjo6a2FibGUodGlkeXI6OnBpdm90X3dpZGVyKG1vcm5pbmcgfD4gbXV0YXRlKHYgPSBzcHJpbnRmKCIlKy4xZiBbJSsuMWYsICUrLjFmXSIsIGVzdCwgbG8sIGhpKSkgfD4gc2VsZWN0KHBvbGx1dGFudCwgZCwgdiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmFtZXNfZnJvbSA9IGQsIHZhbHVlc19mcm9tID0gdikgfD4gc2V0TmFtZXMoYygiUG9sbHV0YW50IiwgIk1vbiIsICJUdWUiLCAiV2VkIiwgIlRodSIsICJGcmkiLCAiU2F0IiwgIlN1biIpKSwKICAgICAgICAgICAgIGNhcHRpb24gPSAiMDY6MDAtMTA6MDAgbWVhbiBieSBkYXkgb2YgdGhlIHdlZWsgcmVsYXRpdmUgdG8gd29ya2luZyBkYXlzICglKSwgd2l0aCA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMiKQpgYGAKCiMjIFdoeSBpbXB1dGF0aW9uIGNhbm5vdCBiZSBhdm9pZGVkCgpXaW5kb3ctYmFzZWQgZm9yZWNhc3RlcnMgKE1MUCwgTFNUTSwgVENOKSBuZWVkIGNvbXBsZXRlIGlucHV0IHdpbmRvd3MuIEZvciB0aGUgMzM0IGRheXMgYmVmb3JlIHRoZQp0ZXN0IHBlcmlvZCwgdGhlIGNvZGUgY29tcHV0ZXMgdGhlIHNoYXJlIG9mIGRheXMgd2l0aCBhdCBsZWFzdCBvbmUgbWlzc2luZyB2YWx1ZSBhbmQgdGhlIHNoYXJlIG9mCmNvbXBsZXRlIHR3by1kYXkgKDk2LXN0ZXApIHdpbmRvd3MuCgpgYGB7ciBtb3RpdmF0aW9ufQpwcmUgPC0gc2VxX2xlbigzMzQgKiBTMSkKbW90IDwtIGJpbmRfcm93cyhsYXBwbHkoUE9MUywgZnVuY3Rpb24ocCkgewogIHggPC0gZ2V0X3NlcmllcygiU1VGIDEiLCBwKVtwcmVdOyBtaXNzIDwtIGlzLm5hKHgpCiAgTSA8LSBtYXRyaXgobWlzcywgbmNvbCA9IFMxLCBieXJvdyA9IFRSVUUpICAgICAgICAgICAgICAgICAgICAgICAjIGRheXMgeCBoYWxmLWhvdXJzCiAgY3MgPC0gYygwLCBjdW1zdW0obWlzcykpOyBMIDwtIDk2CiAgdyA8LSBjc1soTCArIDEpOmxlbmd0aChjcyldIC0gY3NbMToobGVuZ3RoKGNzKSAtIEwpXSAgICAgICAgICAgICAjIG1pc3NpbmcgdmFsdWVzIHBlciB0d28tZGF5IHdpbmRvdwogIGRhdGEuZnJhbWUocG9sbHV0YW50ID0gcCwgZGF5cyA9IDEwMCAqIG1lYW4ocm93U3VtcyhNKSA+IDApLCBjb21wbGV0ZSA9IDEwMCAqIG1lYW4odyA9PSAwKSkKfSkpCmtleSgiZGF5c193aXRoX21pc3NpbmdfbWluX3BjdCIsIG1pbihtb3QkZGF5c1ttb3QkcG9sbHV0YW50ICE9ICJQTTEwIl0pKQprZXkoImNvbXBsZXRlXzJkYXlfd2luZG93c19tYXhfcGN0IiwgbWF4KG1vdCRjb21wbGV0ZVttb3QkcG9sbHV0YW50ICE9ICJQTTEwIl0pKQprbml0cjo6a2FibGUobW90LCBkaWdpdHMgPSAxLCBjb2wubmFtZXMgPSBjKCJQb2xsdXRhbnQiLCAiRGF5cyB3aXRoID49IDEgbWlzc2luZyAoJSkiLCAiQ29tcGxldGUgdHdvLWRheSB3aW5kb3dzICglKSIpLAogICAgICAgICAgICAgY2FwdGlvbiA9ICJEYXlzIHdpdGggbWlzc2luZyB2YWx1ZXMgYW5kIGNvbXBsZXRlIHR3by1kYXkgd2luZG93cyAodHJhaW5pbmcgcGVyaW9kLCAzMzQgZGF5cykiKQpgYGAKCiMgVGhlIHByb3Bvc2VkIG1ldGhvZCBTZWFzLUFSCgpTZWFzLUFSIGRlY29tcG9zZXMgdGhlIHRyYW5zZm9ybWVkIHNlcmllcyBhcwoKJCQKel90PVRfdCtEX3QrV190K1JfdCwgXHRhZ3syfQokJAoKd2l0aCBhIHNtb290aCB0cmVuZCAkVF90JCwgYSBkYWlseSBwcm9maWxlICREX3QkLCBhIHdlZWtkYXktc3BlY2lmaWMgZGV2aWF0aW9uICRXX3QkIGFuZCBhIHJlbWFpbmRlcgokUl90JC4gQWxsIGNvbXBvbmVudHMgYXJlIGVzdGltYXRlZCAqKmZyb20gb2JzZXJ2ZWQgdmFsdWVzIG9ubHkqKjsgJFxkZWx0YV90PTEkIGlmICR0XGluXG1hdGhjYWwgTyQKYW5kICRcZGVsdGFfdD0wJCBvdGhlcndpc2UuIFRoZSBtZXRob2QgY29uc2lzdHMgb2YgdGhlIHNldmVuIHN0ZXBzIGJlbG93LgoKIyMgU3RlcCAxIOKAlCBWYXJpYW5jZS1zdGFiaWxpc2luZyB0cmFuc2Zvcm0gKEVxLiAxKQoKJCQKel90PVxsb2dcQmlnKDErXGZyYWN7eV90fXttfVxCaWcpLFxxcXVhZCBtPVxvcGVyYXRvcm5hbWV7bWVkaWFufVx7eV9zOnNcaW5cbWF0aGNhbCBPXH0uIFx0YWd7MX0KJCQKClRoZSB0cmFuc2Zvcm0gaXMgZmluaXRlIGF0ICR5X3Q9MCQsIGRvZXMgbm90IGRlcGVuZCBvbiB0aGUgdW5pdCwgYW5kIHR1cm5zIG11bHRpcGxpY2F0aXZlIHNlYXNvbmFsCmVmZmVjdHMgaW50byBhZGRpdGl2ZSBvbmVzLiBJZiB0aGUgbWVkaWFuIGlzIDAsIHRoZSBtZWFuIGlzIHVzZWQgYXMgc2NhbGUsIGFuZCAkbT0xJCBpZiBib3RoIGFyZSAwLgoKYGBge3Igc3RlcDF9CnNlYXNhcl9zY2FsZSA8LSBmdW5jdGlvbih4KSB7ICAgICAgICAgICAgICAgICAgICAgICAjIG0gaW4gRXEuICgxKQogIG0gPC0gbWVkaWFuKHgsIG5hLnJtID0gVFJVRSkKICBpZiAoIShtID4gMCkpIHsgbW0gPC0gbWVhbih4LCBuYS5ybSA9IFRSVUUpOyBtIDwtIGlmIChtbSA+IDApIG1tIGVsc2UgMSB9CiAgbQp9CmBgYAoKIyMgU3RlcCAyIOKAlCBNaXNzaW5nLWF3YXJlIHRyZW5kIChFcS4gMykKCkZvciBhIHBhcnRpYWwgcmVzaWR1YWwgJHVfdCQgKGluaXRpYWxseSAkdV90PXpfdCQpLCB0aGUgdHJlbmQgaXMgdGhlIGNlbnRyZWQgJDJcdGltZXMzMzYkIG1vdmluZwphdmVyYWdlIG9mIHRoZSBvYnNlcnZlZCB2YWx1ZXM6CgokJApUX3Q9XGZyYWN7XHN1bV97aj0tMTY4fV57MTY4fXdfalwsXGRlbHRhX3t0K2p9XCx1X3t0K2p9fXtcc3VtX3tqPS0xNjh9XnsxNjh9d19qXCxcZGVsdGFfe3Qran19LFxxcXVhZAp3X2o9XGJlZ2lue2Nhc2VzfTEvMzM2LCZ8anw8MTY4LFxcIDEvNjcyLCZ8anw9MTY4LlxlbmR7Y2FzZXN9IFx0YWd7M30KJCQKClRoZSB3ZWlnaHRzIHN1bSB0byBvbmUgYW5kIHNwYW4gZXhhY3RseSBvbmUgd2Vlaywgc28gZm9yIGNvbXBsZXRlIGRhdGEgYW55IGNvbXBvbmVudCB3aXRoIHBlcmlvZCA0OApvciAzMzYgdGhhdCBzdW1zIHRvIHplcm8gb3ZlciBpdHMgY3ljbGUgZGlzYXBwZWFycyBmcm9tICRUX3QkLiBXaGVyZSBsZXNzIHRoYW4gb25lIHF1YXJ0ZXIgb2YgdGhlIGZ1bGwKd2VpZ2h0IGlzIG9ic2VydmVkIChpbnNpZGUgbG9uZyBnYXBzKSwgJFRfdCQgaXMgZmlsbGVkIGJ5IGxpbmVhciBpbnRlcnBvbGF0aW9uLAokXGhhdCB4X3Q9eF9hK1xmcmFje3QtYX17Yi1hfSh4X2IteF9hKSQgYmV0d2VlbiB0aGUgbmVhcmVzdCBvYnNlcnZlZCBwb2ludHMgJGE8dDxiJCwgYW5kIGtlcHQgY29uc3RhbnQKYmV5b25kIHRoZSBmaXJzdCBhbmQgbGFzdCBvYnNlcnZhdGlvbnMuIFdoZW4gb25seSBvbmUgdmFsdWUgaXMgb2JzZXJ2ZWQsIGl0IGlzIHVzZWQgZXZlcnl3aGVyZSAod2l0aApgc3RhdHM6OmFwcHJveGAgdGhpcyBjYXNlIHdvdWxkIHN0b3Agd2l0aCAqIm5lZWQgYXQgbGVhc3QgdHdvIG5vbi1OQSB2YWx1ZXMiKikuCgoqKkltcGxlbWVudGF0aW9uLioqIEVxLiAoMykgaXMgZXZhbHVhdGVkIGluICRPKG4pJCBvcGVyYXRpb25zIHdpdGggY3VtdWxhdGl2ZSBzdW1zOiB0aGUgaW5uZXIgc3VtCigkfGp8PDE2OCQpIGlzIGEgZGlmZmVyZW5jZSBvZiB0d28gY3VtdWxhdGl2ZSBzdW1zIGFuZCB0aGUgdHdvIGVuZCBwb2ludHMgYXJlIGFkZGVkIHdpdGggaGFsZiB3ZWlnaHQuClRoZSBvYnNlcnZlZCB3ZWlnaHQgZXF1YWxzICQoMmNfe1xybSBpbn0rY197XHJtIGVuZH0pLzY3MiQsIHdoZXJlICRjX3tccm0gaW59JCBhbmQgJGNfe1xybSBlbmR9JCBjb3VudAp0aGUgb2JzZXJ2ZWQgcG9pbnRzOyB0aGUgcnVsZSAiYXQgbGVhc3Qgb25lIHF1YXJ0ZXIgb2JzZXJ2ZWQiIGlzIHRoZXJlZm9yZSBjaGVja2VkICoqZXhhY3RseSoqIHdpdGgKaW50ZWdlcnMsICQyY197XHJtIGlufStjX3tccm0gZW5kfVxnZTE2OCQuIENoZWNrIDIgY29uZmlybXMgdGhhdCB0aGlzIGZhc3QgdmVyc2lvbiBlcXVhbHMgdGhlIGxpdGVyYWwKY29udm9sdXRpb24gb2YgRXEuICgzKS4KCmBgYHtyIHN0ZXAyfQppbnRlcnBfbGluZWFyIDwtIGZ1bmN0aW9uKHgpIHsgICAgICAgICAgICAgICAgICAgICMgbGluZWFyIGludGVycG9sYXRpb24sIGNvbnN0YW50IGF0IHRoZSBlbmRzCiAgb2JzIDwtICFpcy5uYSh4KTsgbl9vYnMgPC0gc3VtKG9icykKICBpZiAobl9vYnMgPT0gbGVuZ3RoKHgpKSByZXR1cm4oeCkKICBpZiAobl9vYnMgPT0gMCkgcmV0dXJuKHJlcCgwLCBsZW5ndGgoeCkpKQogIGlmIChuX29icyA9PSAxKSB7IHhbIW9ic10gPC0geFtvYnNdOyByZXR1cm4oeCkgfSAgICAgICAgICAjIGEgc2luZ2xlIG9ic2VydmF0aW9uOiBjb25zdGFudAogIHQgPC0gc2VxX2Fsb25nKHgpCiAgeFshb2JzXSA8LSBzdGF0czo6YXBwcm94KHRbb2JzXSwgeFtvYnNdLCB4b3V0ID0gdFshb2JzXSwgcnVsZSA9IDIpJHkKICB4Cn0KbmFuX21hIDwtIGZ1bmN0aW9uKHgsIHBlcmlvZCA9IFMyLCBtaW5fZnJhYyA9IDAuMjUpIHsgICAgICAgICMgRXEuICgzKSBpbiBPKG4pIHdpdGggY3VtdWxhdGl2ZSBzdW1zCiAgbiA8LSBsZW5ndGgoeCk7IGggPC0gcGVyaW9kICUvJSAyOyBvYnMgPC0gIWlzLm5hKHgpCiAgeHAgPC0gYyhyZXAoMCwgaCksIGlmZWxzZShvYnMsIHgsIDApLCByZXAoMCwgaCkpICAgICAgICAgICAjIHplcm8gcGFkZGluZyA9IG91dHNpZGUgdmFsdWVzIG1pc3NpbmcKICBvcCA8LSBjKHJlcCgwLCBoKSwgYXMubnVtZXJpYyhvYnMpLCByZXAoMCwgaCkpCiAgY3ggPC0gYygwLCBjdW1zdW0oeHApKTsgY28gPC0gYygwLCBjdW1zdW0ob3ApKQogIGkgIDwtIHNlcV9sZW4obikgKyBoICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBwb3NpdGlvbiBvZiB0IGluIHRoZSBwYWRkZWQgc2VyaWVzCiAgaW5feCA8LSBjeFtpICsgaF0gLSBjeFtpIC0gaCArIDFdOyBpbl9vIDwtIGNvW2kgKyBoXSAtIGNvW2kgLSBoICsgMV0gICAgICAjIHxqfCA8IGggICh3ZWlnaHQgMS9wZXJpb2QpCiAgZW5feCA8LSB4cFtpIC0gaF0gKyB4cFtpICsgaF07ICAgICBlbl9vIDwtIG9wW2kgLSBoXSArIG9wW2kgKyBoXSAgICAgICAgICAjIHxqfCA9IGggICh3ZWlnaHQgMS8oMiBwZXJpb2QpKQogIGNudDIgPC0gMiAqIGluX28gKyBlbl9vICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyA9IDIgKiBwZXJpb2QgKiAob2JzZXJ2ZWQgd2VpZ2h0KSwgYW4gaW50ZWdlcgogIG1hIDwtIGlmZWxzZShjbnQyID49IDIgKiBwZXJpb2QgKiBtaW5fZnJhYywgKGluX3ggKyAwLjUgKiBlbl94KSAvIChpbl9vICsgMC41ICogZW5fbyksIE5BX3JlYWxfKQogIGludGVycF9saW5lYXIobWEpCn0KbmFuX21hX2RpcmVjdCA8LSBmdW5jdGlvbih4LCBwZXJpb2QgPSBTMiwgbWluX2ZyYWMgPSAwLjI1KSB7ICAjIGxpdGVyYWwgRXEuICgzKSBieSBjb252b2x1dGlvbiAocmVmZXJlbmNlKQogIHcgPC0gcmVwKDEsIHBlcmlvZCArIDEpOyB3W2MoMSwgcGVyaW9kICsgMSldIDwtIDAuNTsgdyA8LSB3IC8gcGVyaW9kOyBoIDwtIHBlcmlvZCAlLyUgMgogIGNvbnYgPC0gZnVuY3Rpb24odikgYXMubnVtZXJpYyhzdGF0czo6ZmlsdGVyKGMocmVwKDAsIGgpLCB2LCByZXAoMCwgaCkpLCB3LCBzaWRlcyA9IDIpKVsoaCArIDEpOihoICsgbGVuZ3RoKHYpKV0KICBudW0gPC0gY29udihpZmVsc2UoaXMubmEoeCksIDAsIHgpKTsgZGVuIDwtIGNvbnYoYXMubnVtZXJpYyghaXMubmEoeCkpKQogIGludGVycF9saW5lYXIoaWZlbHNlKGRlbiA+PSBtaW5fZnJhYyAtIDFlLTEyLCBudW0gLyBkZW4sIE5BX3JlYWxfKSkKfQp0dF9kZW1vIDwtIHNlcV9sZW4oMTAgKiBTMikKeF9kZW1vICA8LSAzICsgc2luKDIgKiBwaSAqIHR0X2RlbW8gLyBTMSkgKyAwLjUgKiBjb3MoMiAqIHBpICogdHRfZGVtbyAvIFMyKQppbm5lciAgIDwtIChTMiAvIDIgKyAxKToobGVuZ3RoKHR0X2RlbW8pIC0gUzIgLyAyKQpjYXQoIkNoZWNrIDE6IG1heCB8VCAtIDN8IGZvciBhIHB1cmUgZG91YmxlLXNlYXNvbmFsIHNpZ25hbCA9Iiwgc2lnbmlmKG1heChhYnMobmFuX21hKHhfZGVtbylbaW5uZXJdIC0gMykpLCAzKSwgIlxuIikKc2V0LnNlZWQoMik7IHhyIDwtIHJub3JtKDUwMDApOyB4cltzYW1wbGUoNTAwMCwgMTUwMCldIDwtIE5BOyB4clsxMDAwOjE1MDBdIDwtIE5BCmNhdCgiQ2hlY2sgMjogbWF4IHxmYXN0IGN1bXVsYXRpdmUtc3VtIHZlcnNpb24gLSBkaXJlY3QgY29udm9sdXRpb258ID0iLCBzaWduaWYobWF4KGFicyhuYW5fbWEoeHIpIC0gbmFuX21hX2RpcmVjdCh4cikpKSwgMyksICJcbiIpCmNhdCgiQ2hlY2sgMzogaW50ZXJwX2xpbmVhcihjKE5BLCA1LCBOQSwgTkEpKSA9IiwgaW50ZXJwX2xpbmVhcihjKE5BLCA1LCBOQSwgTkEpKSwgIlxuIikKYGBgCgojIyBTdGVwIDMg4oCUIERhaWx5IHByb2ZpbGUgYnkga2VybmVsIHNtb290aGluZyAoRXEuIDQpCgpUaGUgcGFydGlhbCByZXNpZHVhbCAkcl90PXpfdC1UX3QtV190JCBpcyBhcnJhbmdlZCBhcyBhIG1hdHJpeCAkXG1hdGhiZiBWJCB3aXRoIG9uZSByb3cgcGVyIGRheSAkayQKYW5kIG9uZSBjb2x1bW4gcGVyIGhhbGYtaG91ciAkaCQuIEVhY2ggY29sdW1uIChhICpjeWNsZS1zdWJzZXJpZXMqKSBpcyBzbW9vdGhlZCBhY3Jvc3MgZGF5cyB3aXRoIGEKTmFkYXJheWHigJNXYXRzb24gZXN0aW1hdG9yIHdpdGggR2F1c3NpYW4ga2VybmVsICRLX2godSk9XGV4cFx7LXVeMi8oMmheMilcfSQ6CgokJApcdGlsZGUgRF97ayxofT1cZnJhY3tcc3VtX3trJ31LX3toX0R9KGstaycpXCxcZGVsdGFfe2snLGh9XCxyX3trJyxofX17XHN1bV97ayd9S197aF9EfShrLWsnKVwsXGRlbHRhX3trJyxofX0sClxxcXVhZCBEX3trLGh9PVx0aWxkZSBEX3trLGh9LVxmcmFjezF9ezQ4fVxzdW1fe2gnPTF9Xns0OH1cdGlsZGUgRF97ayxoJ30uIFx0YWd7NH0KJCQKCkluIG1hdHJpeCBmb3JtICRcdGlsZGV7XG1hdGhiZiBEfT0oXG1hdGhiZiBLXG1hdGhiZiBWXzApXG9zbGFzaChcbWF0aGJmIEtcYm9sZHN5bWJvbFxEZWx0YSkkLCB3aGVyZQokXG1hdGhiZiBLX3trayd9PUtfe2hfRH0oay1rJykkLCAkXG1hdGhiZiBWXzAkIGlzICRcbWF0aGJmIFYkIHdpdGggbWlzc2luZyBlbnRyaWVzIHNldCB0byAwLAokXGJvbGRzeW1ib2xcRGVsdGEkIGlzIHRoZSBpbmRpY2F0b3IgbWF0cml4IG9mIG9ic2VydmVkIGVudHJpZXMgYW5kICRcb3NsYXNoJCBpcyBlbGVtZW50LXdpc2UKZGl2aXNpb24uIENlbnRyaW5nIGVhY2ggcm93IG1ha2VzICREJCBzdW0gdG8gemVybyB3aXRoaW4gZXZlcnkgZGF5LiBUaGUgYmFuZHdpZHRoICRoX0QkIChkYXlzKQpjb250cm9scyBob3cgZmFzdCB0aGUgZGFpbHkgcHJvZmlsZSBtYXkgY2hhbmdlIG92ZXIgdGhlIHllYXI7ICRoX0RcdG9caW5mdHkkIGdpdmVzIGEgc3RhdGljIHByb2ZpbGUuCgpgYGB7ciBzdGVwM30KZ2F1c3Nfa2VybmVsIDwtIGZ1bmN0aW9uKG4sIGJ3KSB7CiAgaWYgKCFpcy5maW5pdGUoYncpIHx8IGJ3ID4gMWU1KSByZXR1cm4obWF0cml4KDEsIG4sIG4pKSAgIyBidyA9IEluZjogZXF1YWwgd2VpZ2h0cyAoc3RhdGljIHByb2ZpbGUpCiAgaWR4IDwtIDA6KG4gLSAxKTsgZXhwKC0wLjUgKiAob3V0ZXIoaWR4LCBpZHgsICItIikgLyBidyleMikKfQpjeWNsZV9zbW9vdGggPC0gZnVuY3Rpb24ociwgcGVyaW9kLCBidykgeyAgICAgICAgICMgRXEuICg0KS8oNSkgYmVmb3JlIGNlbnRyaW5nOyByb3dzID0gY3ljbGVzCiAgbiA8LSBsZW5ndGgocik7IG5jIDwtIGNlaWxpbmcobiAvIHBlcmlvZCkKICB2IDwtIHJlcChOQV9yZWFsXywgbmMgKiBwZXJpb2QpOyB2W3NlcV9sZW4obildIDwtIHIKICBWIDwtIG1hdHJpeCh2LCBucm93ID0gbmMsIG5jb2wgPSBwZXJpb2QsIGJ5cm93ID0gVFJVRSkKICBEZWx0YSA8LSAoIWlzLm5hKFYpKSAqIDE7IFYwIDwtIFY7IFYwW2lzLm5hKFYwKV0gPC0gMAogIEsgPC0gZ2F1c3Nfa2VybmVsKG5jLCBidykKICBudW0gPC0gSyAlKiUgVjA7IGRlbiA8LSBLICUqJSBEZWx0YQogIFMgPC0gbnVtIC8gZGVuOyBTWyEoZGVuID4gMWUtMzAwKV0gPC0gMDsgU1tpcy5uYShTKV0gPC0gMCAgIyBjb2x1bW5zIHdpdGhvdXQgZGF0YSAtPiAwCiAgUwp9CmBgYAoKIyMgU3RlcCA0IOKAlCBXZWVrbHkgZGV2aWF0aW9uIChFcS4gNSkKClRoZSBwYXJ0aWFsIHJlc2lkdWFsICRyJ190PXpfdC1UX3QtRF90JCBpcyBhcnJhbmdlZCBieSB3ZWVrcyAocm93cykgYW5kIHRoZSAzMzYgaGFsZi1ob3VycyBvZiB0aGUKd2VlayAoY29sdW1ucyksIHNtb290aGVkIGFjcm9zcyB3ZWVrcyB3aXRoIGJhbmR3aWR0aCAkaF9XJCAod2Vla3MpLCBhbmQgY2VudHJlZCBvdmVyIHdlZWtkYXlzOgoKJCQKV197XGVsbCwoZCxoKX09XHRpbGRlIFdfe1xlbGwsKGQsaCl9LVxmcmFjMTdcc3VtX3tkJz0xfV57N31cdGlsZGUgV197XGVsbCwoZCcsaCl9LiBcdGFnezV9CiQkCgpUaGUgY2VudHJpbmcgbWFrZXMgJEQkIGFuZCAkVyQgaWRlbnRpZmlhYmxlOiAkVyQgb25seSBjYXJyaWVzIHRoZSBwYXJ0IG9mIHRoZSB3ZWVrbHkgcGF0dGVybiB0aGF0IGlzCm5vdCBjb21tb24gdG8gYWxsIGRheXMuIEluIHRoZSBjb2RlLCBlYWNoIHJvdyBvZiAzMzYgdmFsdWVzIGlzIHZpZXdlZCBhcyBhICQ0OFx0aW1lczckIGFycmF5IGFuZCB0aGUKbWVhbiBvdmVyIHRoZSBzZXZlbiB3ZWVrZGF5cyBpcyBzdWJ0cmFjdGVkIGF0IGV2ZXJ5IGhhbGYtaG91ciwgaW4gb25lIHZlY3RvcmlzZWQgb3BlcmF0aW9uLgoKYGBge3Igc3RlcDR9CmNlbnRlcl93ZWVrbHkgPC0gZnVuY3Rpb24oU3cpIHsgICAgICAgICAgICAgICAgICAgIyBFcS4gKDUpOiBzdWJ0cmFjdCB0aGUgbWVhbiBvdmVyIHdlZWtkYXlzCiAgbncgPC0gbnJvdyhTdykKICBBICA8LSBhcnJheShTdywgZGltID0gYyhudywgUzEsIDcpKSAgICAgICAgICAgICAjIEFbd2VlaywgaGFsZi1ob3VyLCB3ZWVrZGF5XSAoY29sdW1ucyBhcmUgZGF5LW1ham9yKQogIE0gIDwtIHJvd01lYW5zKEEsIGRpbXMgPSAyKSAgICAgICAgICAgICAgICAgICAgICMgbWVhbiBvdmVyIHRoZSA3IHdlZWtkYXlzOiB3ZWVrcyB4IDQ4CiAgbWF0cml4KEEgLSBhcnJheShNLCBkaW0gPSBjKG53LCBTMSwgNykpLCBucm93ID0gbncsIG5jb2wgPSBTMikKfQpgYGAKCiMjIFN0ZXAgNSDigJQgQmFja2ZpdHRpbmcKClN0YXJ0aW5nIGZyb20gJFQ9XG1hdGhybXtNQX0oeikkIGFuZCAkRD1XPTAkLCB0aGUgY29tcG9uZW50cyBhcmUgdXBkYXRlZCAkSz0zJCB0aW1lczoKKGkpICREXGxlZnRhcnJvdyQgRXEuICg0KSBvbiAkei1ULVckOyAoaWkpICRXXGxlZnRhcnJvdyQgRXEuICg1KSBvbiAkei1ULUQkOyAoaWlpKSAkVFxsZWZ0YXJyb3ckIEVxLiAoMykKb24gJHotRC1XJC4gVGhlIHJlbWFpbmRlciBpcyAkUl90PXpfdC1UX3QtRF90LVdfdCQgZm9yICR0XGluXG1hdGhjYWwgTyQgYW5kIG1pc3Npbmcgb24gJFxtYXRoY2FsIE0kLgpCZWNhdXNlIGFsbCBrZXJuZWwgc3VtcyB1c2Ugb2JzZXJ2ZWQgdmFsdWVzIG9ubHksIG5vIHByZS1pbXB1dGF0aW9uIGlzIG5lZWRlZDsgaW5zaWRlIGEgbG9uZyBnYXAgdGhlCnByb2ZpbGVzIGFyZSBib3Jyb3dlZCBmcm9tIG5laWdoYm91cmluZyBkYXlzIGFuZCB3ZWVrcyB3aXRoIEdhdXNzaWFuIHdlaWdodHMuCgpgYGB7ciBzdGVwNX0KZHNkX2RlY29tcG9zZSA8LSBmdW5jdGlvbih6LCBid19kYXkgPSAxNCwgYndfd2VlayA9IDE2LCB3ZWVrbHkgPSBUUlVFLCBuX2l0ZXIgPSAzKSB7CiAgbiA8LSBsZW5ndGgoeikKICBUciA8LSBuYW5fbWEoeiwgUzIpOyBEIDwtIG51bWVyaWMobik7IFcgPC0gbnVtZXJpYyhuKQogIGZvciAoaXQgaW4gc2VxX2xlbihuX2l0ZXIpKSB7CiAgICBTZCA8LSBjeWNsZV9zbW9vdGgoeiAtIFRyIC0gVywgUzEsIGJ3X2RheSkgICAgIyBFcS4gKDQpLCBkYXlzIHggNDgKICAgIFNkIDwtIFNkIC0gcm93TWVhbnMoU2QpICAgICAgICAgICAgICAgICAgICAgICAjIHplcm8gbWVhbiB3aXRoaW4gZWFjaCBkYXkKICAgIEQgIDwtIGFzLnZlY3Rvcih0KFNkKSlbc2VxX2xlbihuKV0KICAgIGlmICh3ZWVrbHkpIFcgPC0gYXMudmVjdG9yKHQoY2VudGVyX3dlZWtseShjeWNsZV9zbW9vdGgoeiAtIFRyIC0gRCwgUzIsIGJ3X3dlZWspKSkpW3NlcV9sZW4obildCiAgICBUciA8LSBuYW5fbWEoeiAtIEQgLSBXLCBTMikgICAgICAgICAgICAgICAgICAgIyBFcS4gKDMpCiAgfQogIGxpc3QodHJlbmQgPSBUciwgZGFpbHkgPSBELCB3ZWVrbHkgPSBXLCByZW1haW5kZXIgPSB6IC0gVHIgLSBEIC0gVykKfQpgYGAKCiMjIFN0ZXAgNiDigJQgUmVtYWluZGVyIGltcHV0YXRpb24gYnkgdGhlIEFSKDEpIGJyaWRnZSAoRXFzLiA24oCTNywgUHJvcG9zaXRpb24gMSkKClRoZSByZW1haW5kZXIgaXMgbW9kZWxsZWQgYXMgJFJfdD1ccGhpIFJfe3QtMX0rXHZhcmVwc2lsb25fdCQsIHdpdGggJFxwaGkkIGVzdGltYXRlZCBmcm9tIHRoZSBwYWlycyBvZgpjb25zZWN1dGl2ZSBvYnNlcnZhdGlvbnMgJFxtYXRoY2FsIFA9XHt0OnRcaW5cbWF0aGNhbCBPLFwgdC0xXGluXG1hdGhjYWwgT1x9JDoKCiQkClx0aWxkZVxwaGk9XGZyYWN7XHN1bV97dFxpblxtYXRoY2FsIFB9Ul90Ul97dC0xfX17XHN1bV97dFxpblxtYXRoY2FsIFB9Ul97dC0xfV4yfSxccXF1YWQKXGhhdFxwaGk9XG1pblx7MC45OTksXG1heFx7MCxcdGlsZGVccGhpXH1cfS4gXHRhZ3s2fQokJAoKRm9yIGEgbWlzc2luZyAkdCQgd2l0aCBuZWFyZXN0IG9ic2VydmVkIG5laWdoYm91cnMgJGE8dDxiJCwKCiQkClxoYXQgUl90PVxmcmFje1xwaGlee1wsdC1hfVxiaWcoMS1ccGhpXnsyKGItdCl9XGJpZylSX2ErXHBoaV57XCxiLXR9XGJpZygxLVxwaGleezIodC1hKX1cYmlnKVJfYn17MS1ccGhpXnsyKGItYSl9fSwgXHRhZ3s3fQokJAoKd2hpbGUgJFxoYXQgUl90PVxwaGlee2ItdH1SX2IkIGJlZm9yZSB0aGUgZmlyc3QgYW5kICRcaGF0IFJfdD1ccGhpXnt0LWF9Ul9hJCBhZnRlciB0aGUgbGFzdCBvYnNlcnZhdGlvbgoodGhlIHJlbWFpbmRlciBpcyBjZW50cmVkIGJ5IGl0cyBvYnNlcnZlZCBtZWFuIGZpcnN0KS4KCioqUHJvcG9zaXRpb24gMS4qKiBGb3IgYSBzdGF0aW9uYXJ5IEdhdXNzaWFuIEFSKDEpLCAoNykgZXF1YWxzIHRoZSBLYWxtYW4gc21vb3RoZXIgZXN0aW1hdGUKJFxtYXRocm0gRVtSX3RcbWlkIFJfcyxzXGluXG1hdGhjYWwgT10kLCB3aXRoIGVycm9yIHZhcmlhbmNlCiRcZ2FtbWFfMFxiaWdbMS1cZnJhY3tccGhpXnsyKHQtYSl9K1xwaGleezIoYi10KX0tMlxwaGleezIoYi1hKX19ezEtXHBoaV57MihiLWEpfX1cYmlnXSQsICRcZ2FtbWFfMD1cbWF0aHJte1Zhcn0oUl90KSQuCkJ5IHRoZSBNYXJrb3YgcHJvcGVydHksICRSX3QkIGRlcGVuZHMgb24gdGhlIG90aGVyIG9ic2VydmF0aW9ucyBvbmx5IHRocm91Z2ggJChSX2EsUl9iKSQ7IEdhdXNzaWFuCmNvbmRpdGlvbmluZyB3aXRoICRcbWF0aHJte0Nvdn0oUl9zLFJfdSk9XGdhbW1hXzBccGhpXnt8cy11fH0kIGdpdmVzIHRoZSB3ZWlnaHRzCiRcbWF0aGJmIGNeXHRvcFxib2xkc3ltYm9sXFNpZ21hXnstMX0kIHdpdGggJFxtYXRoYmYgYz1cZ2FtbWFfMChccGhpXnt0LWF9LFxwaGlee2ItdH0pXlx0b3AkIGFuZAokXGJvbGRzeW1ib2xcU2lnbWE9XGdhbW1hXzBcbGVmdFtcYmVnaW57c21hbGxtYXRyaXh9MSZccGhpXntiLWF9XFxccGhpXntiLWF9JjFcZW5ke3NtYWxsbWF0cml4fVxyaWdodF0kLgpGb3Igc2hvcnQgZ2FwcyB3aXRoICRccGhpXGFwcHJveDEkLCAoNykgaXMgY2xvc2UgdG8gbGluZWFyIGludGVycG9sYXRpb24gb2YgdGhlIHJlbWFpbmRlcjsgZm9yIGxvbmcgZ2FwcwokXGhhdCBSX3RcdG8wJCBhbmQgdGhlIGltcHV0YXRpb24gcmV2ZXJ0cyB0byB0aGUgcHJvZmlsZSAkVF90K0RfdCtXX3QkLiBUaGUgY29kZSB2ZXJpZmllcyB0aGUgbWVhbgoqKmFuZCoqIHRoZSB2YXJpYW5jZSBvZiBQcm9wb3NpdGlvbiAxIGFnYWluc3QgYHN0YXRzOjpLYWxtYW5TbW9vdGhgLgoKYGBge3Igc3RlcDZ9CmVzdGltYXRlX2FyMSA8LSBmdW5jdGlvbihSKSB7ICAgICAgICAgICAgICAgICAgICAgIyBFcS4gKDYpCiAgYSA8LSBSWy1sZW5ndGgoUildOyBiIDwtIFJbLTFdOyBtIDwtICFpcy5uYShhKSAmICFpcy5uYShiKQogIGlmIChzdW0obSkgPCAxMCkgcmV0dXJuKDApCiAgbWluKG1heChzdW0oYVttXSAqIGJbbV0pIC8gc3VtKGFbbV1eMiksIDApLCAwLjk5OSkKfQphcjFfYnJpZGdlIDwtIGZ1bmN0aW9uKFIsIHBoaSA9IE5VTEwpIHsgICAgICAgICAgICMgRXEuICg3KQogIG9icyA8LSAhaXMubmEoUikKICBpZiAoYWxsKG9icykpIHJldHVybihSKQogIGlmICghYW55KG9icykpIHJldHVybihyZXAoMCwgbGVuZ3RoKFIpKSkKICBpZiAoaXMubnVsbChwaGkpKSBwaGkgPC0gZXN0aW1hdGVfYXIxKFIpCiAgbXUgPC0gbWVhbihSLCBuYS5ybSA9IFRSVUUpOyBSYyA8LSBSIC0gbXUKICBpbyA8LSB3aGljaChvYnMpOyBpbSA8LSB3aGljaCghb2JzKQogIGsgIDwtIGZpbmRJbnRlcnZhbChpbSwgaW8pICAgICAgICAgICAgICAgICAgICAgICMgbnVtYmVyIG9mIG9ic2VydmVkIGluZGljZXMgYmVmb3JlIHQKICBocCA8LSBrID4gMDsgaG4gPC0gayA8IGxlbmd0aChpbykgICAgICAgICAgICAgICAjIGhhcyBhIHByZXZpb3VzIC8gbmV4dCBvYnNlcnZhdGlvbgogIGEgIDwtIGlmZWxzZShocCwgaW9bcG1heChrLCAxKV0sIE5BX2ludGVnZXJfKQogIGIgIDwtIGlmZWxzZShobiwgaW9bcG1pbihrICsgMSwgbGVuZ3RoKGlvKSldLCBOQV9pbnRlZ2VyXykKICBlc3QgPC0gbnVtZXJpYyhsZW5ndGgoaW0pKTsgYm90aCA8LSBocCAmIGhuCiAgaWYgKGFueShib3RoKSkgewogICAgdHQgPC0gaW1bYm90aF07IGFhIDwtIGFbYm90aF07IGJiIDwtIGJbYm90aF0KICAgIHBhIDwtIHBoaV4odHQgLSBhYSk7IHBiIDwtIHBoaV4oYmIgLSB0dCk7IGRlbiA8LSAxIC0gcGhpXigyICogKGJiIC0gYWEpKQogICAgZXN0W2JvdGhdIDwtIChwYSAqICgxIC0gcGJeMikgKiBSY1thYV0gKyBwYiAqICgxIC0gcGFeMikgKiBSY1tiYl0pIC8gZGVuCiAgfQogIG9hIDwtIGhwICYgIWhuOyBpZiAoYW55KG9hKSkgZXN0W29hXSA8LSBwaGleKGltW29hXSAtIGFbb2FdKSAqIFJjW2Fbb2FdXQogIG9iIDwtICFocCAmIGhuOyBpZiAoYW55KG9iKSkgZXN0W29iXSA8LSBwaGleKGJbb2JdIC0gaW1bb2JdKSAqIFJjW2Jbb2JdXQogIFJbaW1dIDwtIGVzdCArIG11CiAgUgp9CmFyMV9icmlkZ2VfdmFyIDwtIGZ1bmN0aW9uKFIsIHBoaSwgZ2FtbWEwKSB7ICAgICAgIyB2YXJpYW5jZSBpbiBQcm9wb3NpdGlvbiAxIChpbm5lciBnYXBzKQogIGlvIDwtIHdoaWNoKCFpcy5uYShSKSk7IGltIDwtIHdoaWNoKGlzLm5hKFIpKTsgayA8LSBmaW5kSW50ZXJ2YWwoaW0sIGlvKQogIGEgPC0gaW9ba107IGIgPC0gaW9bayArIDFdOyBtIDwtIGIgLSBhCiAgZ2FtbWEwICogKDEgLSAocGhpXigyICogKGltIC0gYSkpICsgcGhpXigyICogKGIgLSBpbSkpIC0gMiAqIHBoaV4oMiAqIG0pKSAvICgxIC0gcGhpXigyICogbSkpKQp9CnNldC5zZWVkKDEpOyBwaGkwIDwtIDAuOQpyICA8LSBhcy5udW1lcmljKGFyaW1hLnNpbShsaXN0KGFyID0gcGhpMCksIDIwMDApKTsgcltjKDEwMDoxODAsIDUwMDo1MDMpXSA8LSBOQQprcyA8LSBzdGF0czo6S2FsbWFuU21vb3RoKHIgLSBtZWFuKHIsIG5hLnJtID0gVFJVRSksIHN0YXRzOjptYWtlQVJJTUEocGhpID0gcGhpMCwgdGhldGEgPSBudW1lcmljKCksIERlbHRhID0gbnVtZXJpYygpKSwgbml0ID0gLTFMKQpjYXQoIlByb3Bvc2l0aW9uIDEsIG1lYW46ICAgICBtYXggfGJyaWRnZSAtIEthbG1hbiBzbW9vdGhlcnwgPSIsCiAgICBzaWduaWYobWF4KGFicyhhcjFfYnJpZGdlKHIsIHBoaTApIC0gKGtzJHNtb290aFssIDFdICsgbWVhbihyLCBuYS5ybSA9IFRSVUUpKSlbaXMubmEocildKSwgMyksICJcbiIpCmNhdCgiUHJvcG9zaXRpb24gMSwgdmFyaWFuY2U6IG1heCB8Zm9ybXVsYSAtIEthbG1hbiBzbW9vdGhlcnwgPSIsCiAgICBzaWduaWYobWF4KGFicyhhcjFfYnJpZGdlX3ZhcihyLCBwaGkwLCAxIC8gKDEgLSBwaGkwXjIpKSAtIGtzJHZhcltpcy5uYShyKSwgMSwgMV0pKSwgMyksICJcbiIpCmBgYAoKIyMgU3RlcCA3IOKAlCBSZWNvbnN0cnVjdGlvbiAoRXEuIDgpIGFuZCB0aGUgY29tcGxldGUgZnVuY3Rpb24KCiQkClxoYXQgeV90PVxtYXhcQmlnXHswLFwgbVxiaWdbXGV4cFxiaWcoVF90K0RfdCtXX3QrXGhhdCBSX3RcYmlnKS0xXGJpZ11cQmlnXH0sXHFxdWFkIHRcaW5cbWF0aGNhbCBNLCBcdGFnezh9CiQkCgphbmQgdGhlIG9ic2VydmVkIHZhbHVlcyBhcmUgbGVmdCB1bmNoYW5nZWQuIFRoZSBhcmd1bWVudHMgYHRyYW5zZm9ybWAsIGB3ZWVrbHlgIGFuZCBgcmVtYWluZGVyYCBkZWZpbmUKdGhlIHZhcmlhbnRzIG9mIHRoZSBhYmxhdGlvbiBzdHVkeSBpbiBFeHBlcmltZW50IDEuIFRoZSBsYXN0IGxpbmUgbWVhc3VyZXMgdGhlIGNvbXB1dGluZyB0aW1lIGZvciBvbmUgZnVsbCBzZXJpZXMuCgpgYGB7ciBzdGVwN30KaW1wdXRlX3NlYXNhciA8LSBmdW5jdGlvbih4LCBid19kYXkgPSAxNCwgYndfd2VlayA9IDE2LCB0cmFuc2Zvcm0gPSAibG9nIiwgd2Vla2x5ID0gVFJVRSwKICAgICAgICAgICAgICAgICAgICAgICAgcmVtYWluZGVyID0gImFyMSIsIG5faXRlciA9IDMsIHJldHVybl9jb21wb25lbnRzID0gRkFMU0UpIHsKICBvYnMgPC0gIWlzLm5hKHgpCiAgaWYgKGFsbChvYnMpKSByZXR1cm4oeCkKICBtIDwtIHNlYXNhcl9zY2FsZSh4KQogIHogPC0gaWYgKHRyYW5zZm9ybSA9PSAibG9nIikgbG9nMXAocG1heCh4LCAwKSAvIG0pIGVsc2UgeCAvIG0gICAgICAgICAgIyBFcS4gKDEpCiAgZGMgPC0gZHNkX2RlY29tcG9zZSh6LCBid19kYXksIGJ3X3dlZWssIHdlZWtseSwgbl9pdGVyKSAgICAgICAgICAgICAgICAjIEVxcy4gKDMpLSg1KQogIFJoIDwtIHN3aXRjaChyZW1haW5kZXIsIGFyMSA9IGFyMV9icmlkZ2UoZGMkcmVtYWluZGVyKSwgICAgICAgICAgICAgICAgIyBFcXMuICg2KS0oNykKICAgICAgICAgICAgICAgbGluZWFyID0gaW50ZXJwX2xpbmVhcihkYyRyZW1haW5kZXIpLAogICAgICAgICAgICAgICB6ZXJvID0gaWZlbHNlKGlzLm5hKGRjJHJlbWFpbmRlciksIDAsIGRjJHJlbWFpbmRlcikpCiAgemggPC0gZGMkdHJlbmQgKyBkYyRkYWlseSArIGRjJHdlZWtseSArIFJoCiAgeWggPC0gcG1heChpZiAodHJhbnNmb3JtID09ICJsb2ciKSBtICogZXhwbTEoemgpIGVsc2UgbSAqIHpoLCAwKSAgICAgICAgIyBFcS4gKDgpCiAgb3V0IDwtIHg7IG91dFshb2JzXSA8LSB5aFshb2JzXQogIGlmIChyZXR1cm5fY29tcG9uZW50cykgcmV0dXJuKGxpc3QoaW1wdXRlZCA9IG91dCwgeiA9IHosIGNvbXAgPSBkYywgcmVtYWluZGVyX2ltcCA9IFJoKSkKICBvdXQKfQp4X25vMiA8LSBnZXRfc2VyaWVzKCJTVUYgMSIsICJOTzIiKQp0X3NlYXNhciA8LSBzeXN0ZW0udGltZShmb3IgKGkgaW4gMTo1KSBpbXB1dGVfc2Vhc2FyKHhfbm8yKSlbWyJlbGFwc2VkIl1dIC8gNQpjYXQoIlNlYXMtQVIgaW1wdXRlcyB0aGUgZnVsbCBOTzIgc2VyaWVzIG9mIFNVRiAxIGluIiwgcm91bmQodF9zZWFzYXIsIDMpLCAic1xuIikKYGBgCgojIENvbXBldGluZyBtZXRob2RzCgpBbGwgb3V0cHV0cyBhcmUgdHJ1bmNhdGVkIGF0IHplcm8sICRcaGF0IHlfdFxsZWZ0YXJyb3dcbWF4XHswLFxoYXQgeV90XH0kLgoKKiAqKk1FQU4sIE1FRElBTjoqKiAkXGhhdCB5X3Q9XGJhciB5X3tcbWF0aGNhbCBPfSQgb3IgJFxvcGVyYXRvcm5hbWV7bWVkaWFufSh5X3tcbWF0aGNhbCBPfSkkIChgaW1wdXRlVFM6Om5hX21lYW5gKS4KKiAqKkxJOioqIGxpbmVhciBpbnRlcnBvbGF0aW9uIG9mIFN0ZXAgMiAoYG5hX2ludGVycG9sYXRpb25gKS4KKiAqKlNTLU1FQU4sIFNTLU1FRElBTiwgU1MtTEkqKiAoc2Vhc29uYWwgc3BsaXQsIHBlcmlvZCAzMzYpOiBmb3IgZXZlcnkgcG9zaXRpb24gJHAkIG9mIHRoZSB3ZWVrLCB0aGUKICBzdWJzZXJpZXMgJFx7eV90Oih0LTEpXGJtb2QzMzY9cC0xXH0kIGlzIGZpbGxlZCB3aXRoIGl0cyBvd24gbWVhbiwgbWVkaWFuIG9yIGxpbmVhciBpbnRlcnBvbGF0aW9uCiAgKGBuYV9zZWFzcGxpdGApLiBQb3NpdGlvbnMgd2l0aG91dCBhbnkgb2JzZXJ2YXRpb24gdXNlIHRoZSBzYW1lIGdsb2JhbCBzdGF0aXN0aWMuCiogKipTRUFERUM6KiogY2xhc3NpY2FsIGFkZGl0aXZlIGRlY29tcG9zaXRpb24gd2l0aCBwZXJpb2QgNDggb2YgdGhlIGludGVycG9sYXRlZCBzZXJpZXMsCiAgJFNfdD1cYmFyIGVfe2godCl9LVxmcmFjMXs0OH1cc3VtX2hcYmFyIGVfaCQgd2l0aCAkXGJhciBlX2gkIHRoZSBtZWFuIG9mICR5LVxtYXRocm17TUF9X3syXHRpbWVzNDh9KHkpJAogIGF0IGhhbGYtaG91ciAkaCQ7IHRoZW4gJFxoYXQgeV90PVxtYXRocm17TEl9KHktUylfdCtTX3QkLiBUaGlzIGlzIHRoZSBzY2hlbWUgb2YgYG5hX3NlYWRlY2AsIHdoaWNoCiAgaXRzZWxmIHVzZXMgYSByb2J1c3QgU1RMIChldmFsdWF0ZWQgc2VwYXJhdGVseSBpbiBTZWN0aW9uIDYuOCkuCiogKipNU1RMLUxJOioqIGFzIFNFQURFQywgd2l0aCAkU190PVNeeyg0OCl9X3QrU157KDMzNil9X3QkIGZyb20gYGZvcmVjYXN0Ojptc3RsYC4KKiAqKktBTE1BTjoqKiBhbiBBUklNQSgyLDAsMSkgbW9kZWwgZm9yICR5X3QtXGJhciB5JCBmaXR0ZWQgYnkgbWF4aW11bSBsaWtlbGlob29kICh0aGUgS2FsbWFuIGZpbHRlcgogIGhhbmRsZXMgdGhlIG1pc3NpbmcgdmFsdWVzIGV4YWN0bHkpLCBhbmQgJFxoYXQgeV90PVxtYXRoYmYgWl5cdG9wXGhhdHtcYm9sZHN5bWJvbFxhbHBoYX1fe3RcbWlkIG59K1xiYXIgeSQKICBmcm9tIHRoZSBLYWxtYW4gc21vb3RoZXI7IGFuIEFSKDEpIGlzIHVzZWQgaWYgdGhlIG9wdGltaXNhdGlvbiBmYWlscy4KCmBgYHtyIGJhc2VsaW5lc30KaW1wdXRlX21lYW4gICA8LSBmdW5jdGlvbih4KSB7IHhbaXMubmEoeCldIDwtIG1lYW4oeCwgbmEucm0gPSBUUlVFKTsgeCB9CmltcHV0ZV9tZWRpYW4gPC0gZnVuY3Rpb24oeCkgeyB4W2lzLm5hKHgpXSA8LSBtZWRpYW4oeCwgbmEucm0gPSBUUlVFKTsgeCB9CmltcHV0ZV9zZWFzcGxpdCA8LSBmdW5jdGlvbih4LCBhbGdvcml0aG0gPSAibWVkaWFuIiwgcGVyaW9kID0gUzIpIHsKICB4MCA8LSB4CiAgZm9yIChrIGluIHNlcV9sZW4ocGVyaW9kKSkgewogICAgaWR4IDwtIHNlcShrLCBsZW5ndGgoeCksIGJ5ID0gcGVyaW9kKTsgc3ViIDwtIHhbaWR4XQogICAgaWYgKGFueU5BKHN1YikpIHhbaWR4XSA8LSBzd2l0Y2goYWxnb3JpdGhtLCBtZWFuID0gaW1wdXRlX21lYW4oc3ViKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1lZGlhbiA9IGltcHV0ZV9tZWRpYW4oc3ViKSwgaW50ZXJwb2xhdGlvbiA9IGludGVycF9saW5lYXIoc3ViKSkKICB9CiAgaWYgKGFueU5BKHgpKSB7ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBwb3NpdGlvbnMgd2l0aG91dCBhbnkgb2JzZXJ2YXRpb246IGdsb2JhbCBmYWxsYmFjawogICAgZmIgPC0gc3dpdGNoKGFsZ29yaXRobSwgbWVhbiA9IGltcHV0ZV9tZWFuLCBtZWRpYW4gPSBpbXB1dGVfbWVkaWFuLCBpbnRlcnBvbGF0aW9uID0gaW50ZXJwX2xpbmVhcikKICAgIHhbaXMubmEoeCldIDwtIGZiKHgwKVtpcy5uYSh4KV0KICB9CiAgeAp9CmltcHV0ZV9zZWFkZWMgPC0gZnVuY3Rpb24oeCwgcGVyaW9kID0gUzEpIHsgICAgICAgIyBjbGFzc2ljYWwgYWRkaXRpdmUgZGVjb21wb3NpdGlvbgogIHNlYXMgPC0gYXMubnVtZXJpYyhzdGF0czo6ZGVjb21wb3NlKHRzKGludGVycF9saW5lYXIoeCksIGZyZXF1ZW5jeSA9IHBlcmlvZCkpJHNlYXNvbmFsKQogIHhkIDwtIGludGVycF9saW5lYXIoeCAtIHNlYXMpOyB4W2lzLm5hKHgpXSA8LSAoeGQgKyBzZWFzKVtpcy5uYSh4KV07IHgKfQppbXB1dGVfc2VhZGVjX3N0bCA8LSBmdW5jdGlvbih4LCBwZXJpb2QgPSBTMSkgeyAgICMgcmVwbGljYSBvZiBpbXB1dGVUUzo6bmFfc2VhZGVjIChyb2J1c3QgU1RMKQogIHNlYXMgPC0gYXMubnVtZXJpYyhzdGF0czo6c3RsKHRzKGludGVycF9saW5lYXIoeCksIGZyZXF1ZW5jeSA9IHBlcmlvZCksIHJvYnVzdCA9IFRSVUUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcy53aW5kb3cgPSAxMSkkdGltZS5zZXJpZXNbLCAic2Vhc29uYWwiXSkKICB4ZCA8LSBpbnRlcnBfbGluZWFyKHggLSBzZWFzKTsgeFtpcy5uYSh4KV0gPC0gKHhkICsgc2VhcylbaXMubmEoeCldOyB4Cn0KaW1wdXRlX21zdGxfbGkgPC0gZnVuY3Rpb24oeCkgewogIGZpdCAgPC0gZm9yZWNhc3Q6Om1zdGwoZm9yZWNhc3Q6Om1zdHMoaW50ZXJwX2xpbmVhcih4KSwgc2Vhc29uYWwucGVyaW9kcyA9IGMoUzEsIFMyKSkpCiAgc2VhcyA8LSByb3dTdW1zKGZpdFssIGdyZXAoIlNlYXNvbmFsIiwgY29sbmFtZXMoZml0KSksIGRyb3AgPSBGQUxTRV0pCiAgeGQgPC0gaW50ZXJwX2xpbmVhcih4IC0gc2Vhcyk7IHhbaXMubmEoeCldIDwtICh4ZCArIHNlYXMpW2lzLm5hKHgpXTsgeAp9CmltcHV0ZV9rYWxtYW4gPC0gZnVuY3Rpb24oeCwgb3JkZXIgPSBjKDIsIDAsIDEpKSB7CiAgbXUgPC0gbWVhbih4LCBuYS5ybSA9IFRSVUUpOyB4MCA8LSB4IC0gbXUKICBmaXQgPC0gdHJ5Q2F0Y2goc3RhdHM6OmFyaW1hKHgwLCBvcmRlciA9IG9yZGVyLCBpbmNsdWRlLm1lYW4gPSBGQUxTRSwgbWV0aG9kID0gIk1MIiksCiAgICAgICAgICAgICAgICAgIGVycm9yID0gZnVuY3Rpb24oZSkgc3RhdHM6OmFyaW1hKHgwLCBvcmRlciA9IGMoMSwgMCwgMCksIGluY2x1ZGUubWVhbiA9IEZBTFNFLCBtZXRob2QgPSAiTUwiKSkKICBzbSA8LSBzdGF0czo6S2FsbWFuU21vb3RoKHgwLCBmaXQkbW9kZWwsIG5pdCA9IC0xTCkkc21vb3RoCiAgeWhhdCA8LSBhcy5udW1lcmljKHNtICUqJSBmaXQkbW9kZWwkWikgKyBtdQogIHhbaXMubmEoeCldIDwtIHloYXRbaXMubmEoeCldOyB4Cn0Kbm9ubmVnIDwtIGZ1bmN0aW9uKGYpIGZ1bmN0aW9uKHgpIHBtYXgoZih4KSwgMCkKTUVUSE9EX09SREVSIDwtIGMoIk1FQU4iLCAiTUVESUFOIiwgIkxJIiwgIlNTLU1FQU4iLCAiU1MtTUVESUFOIiwgIlNTLUxJIiwgIlNFQURFQyIsICJNU1RMLUxJIiwgIktBTE1BTiIsICJTZWFzLUFSIikKTUVUSE9EUyA8LSBsYXBwbHkobGlzdChNRUFOID0gaW1wdXRlX21lYW4sIE1FRElBTiA9IGltcHV0ZV9tZWRpYW4sIExJID0gaW50ZXJwX2xpbmVhciwKICBgU1MtTUVBTmAgPSBmdW5jdGlvbih4KSBpbXB1dGVfc2Vhc3BsaXQoeCwgIm1lYW4iKSwgYFNTLU1FRElBTmAgPSBmdW5jdGlvbih4KSBpbXB1dGVfc2Vhc3BsaXQoeCwgIm1lZGlhbiIpLAogIGBTUy1MSWAgPSBmdW5jdGlvbih4KSBpbXB1dGVfc2Vhc3BsaXQoeCwgImludGVycG9sYXRpb24iKSwgU0VBREVDID0gaW1wdXRlX3NlYWRlYywKICBgTVNUTC1MSWAgPSBpbXB1dGVfbXN0bF9saSwgS0FMTUFOID0gaW1wdXRlX2thbG1hbiwgYFNlYXMtQVJgID0gaW1wdXRlX3NlYXNhciksIG5vbm5lZykKYGBgCgoqKlZhbGlkYXRpb24gYWdhaW5zdCBgaW1wdXRlVFNgLioqIEluIGBpbXB1dGVUU2AgdGhlIHNlYXNvbmFsLXNwbGl0IG1lZGlhbiBpcyByZXF1ZXN0ZWQgd2l0aApgYWxnb3JpdGhtID0gIm1lYW4iLCBvcHRpb24gPSAibWVkaWFuImAuIFRoZSBsYXN0IHJvdyBpcyBub24temVybyBieSBkZXNpZ24gKGNsYXNzaWNhbCBkZWNvbXBvc2l0aW9uCnZlcnN1cyByb2J1c3QgU1RMKS4KCmBgYHtyIGNyb3NzY2hlY2staW1wdXRldHN9CmlmIChIQVNfSU1QVVRFVFMpIHsKICB4czEgPC0gdHMoeF9ubzIsIGZyZXF1ZW5jeSA9IFMxKTsgeHMyIDwtIHRzKHhfbm8yLCBmcmVxdWVuY3kgPSBTMikKICBjaGsgPC0gZnVuY3Rpb24oYSwgYikgc2lnbmlmKG1heChhYnMoYSAtIGFzLm51bWVyaWMoYikpKSwgMykKICBrbml0cjo6a2FibGUoZGF0YS5mcmFtZSgKICAgIGNvbXBhcmlzb24gPSBjKCJMSSB2cyBuYV9pbnRlcnBvbGF0aW9uIiwgIlNTLU1FQU4gdnMgbmFfc2Vhc3BsaXQoYWxnb3JpdGhtID0gJ21lYW4nKSIsCiAgICAgICAgICAgICAgICAgICAiU1MtTUVESUFOIHZzIG5hX3NlYXNwbGl0KGFsZ29yaXRobSA9ICdtZWFuJywgb3B0aW9uID0gJ21lZGlhbicpIiwKICAgICAgICAgICAgICAgICAgICJTUy1MSSB2cyBuYV9zZWFzcGxpdChhbGdvcml0aG0gPSAnaW50ZXJwb2xhdGlvbicpIiwKICAgICAgICAgICAgICAgICAgICJyb2J1c3QtU1RMIHJlcGxpY2EgdnMgbmFfc2VhZGVjIiwgIlNFQURFQyAoY2xhc3NpY2FsIGRlY29tcG9zaXRpb24pIHZzIG5hX3NlYWRlYyIpLAogICAgbWF4X2Fic19kaWZmID0gYygKICAgICAgY2hrKGludGVycF9saW5lYXIoeF9ubzIpLCBpbXB1dGVUUzo6bmFfaW50ZXJwb2xhdGlvbih4X25vMikpLAogICAgICBjaGsoaW1wdXRlX3NlYXNwbGl0KHhfbm8yLCAibWVhbiIpLCBpbXB1dGVUUzo6bmFfc2Vhc3BsaXQoeHMyLCBhbGdvcml0aG0gPSAibWVhbiIpKSwKICAgICAgY2hrKGltcHV0ZV9zZWFzcGxpdCh4X25vMiwgIm1lZGlhbiIpLCBpbXB1dGVUUzo6bmFfc2Vhc3BsaXQoeHMyLCBhbGdvcml0aG0gPSAibWVhbiIsIG9wdGlvbiA9ICJtZWRpYW4iKSksCiAgICAgIGNoayhpbXB1dGVfc2Vhc3BsaXQoeF9ubzIsICJpbnRlcnBvbGF0aW9uIiksIGltcHV0ZVRTOjpuYV9zZWFzcGxpdCh4czIsIGFsZ29yaXRobSA9ICJpbnRlcnBvbGF0aW9uIikpLAogICAgICBjaGsoaW1wdXRlX3NlYWRlY19zdGwoeF9ubzIpLCBpbXB1dGVUUzo6bmFfc2VhZGVjKHhzMSwgYWxnb3JpdGhtID0gImludGVycG9sYXRpb24iKSksCiAgICAgIGNoayhpbXB1dGVfc2VhZGVjKHhfbm8yKSwgaW1wdXRlVFM6Om5hX3NlYWRlYyh4czEsIGFsZ29yaXRobSA9ICJpbnRlcnBvbGF0aW9uIikpKSksCiAgICBjYXB0aW9uID0gIkFncmVlbWVudCB3aXRoIGltcHV0ZVRTICgwID0gaWRlbnRpY2FsOyB0aGUgbGFzdCByb3cgZGlmZmVycyBieSBkZXNpZ24pIikKfSBlbHNlIGNhdCgiaW1wdXRlVFMgaXMgbm90IGluc3RhbGxlZDsgY2hlY2sgc2tpcHBlZC5cbiIpCmBgYAoKIyBFeHBlcmltZW50YWwgZGVzaWduCgojIyBBcnRpZmljaWFsIGdhcHMKClRoZSB0cnVlIHZhbHVlcyBiZWhpbmQgcmVhbCBnYXBzIGFyZSB1bmtub3duLCBzbyBhY2N1cmFjeSBpcyBtZWFzdXJlZCBvbiBvYnNlcnZhdGlvbnMgcmVtb3ZlZCBvbgpwdXJwb3NlLiBGb3IgYSBnYXAgcmVnaW1lIHdpdGggbGVuZ3RocyBpbiAkW0xfe1xtaW59LExfe1xtYXh9XSQgYW5kIGEgcmF0ZSAkXHJobyQ6CgoxLiBkcmF3IGEgbGVuZ3RoICRMXHNpbVxtYXRocm0gVVx7TF97XG1pbn0sXGRvdHMsTF97XG1heH1cfSQgYW5kIGEgc3RhcnQgJHMkIGF0IHJhbmRvbTsKMi4gYWNjZXB0IHRoZSBibG9jayAkXHtzLFxkb3RzLHMrTC0xXH0kIG9ubHkgaWYgdGhlIGJsb2NrICoqYW5kKiogb25lIGd1YXJkIHBvaW50IG9uIGVhY2ggc2lkZQogICAoJHMtMSQgYW5kICRzK0wkKSBhcmUgb2JzZXJ2ZWQgYW5kIHVudXNlZCwgc28gdGhhdCB0aGUgcmVhbGlzZWQgZ2FwIGhhcyBsZW5ndGggZXhhY3RseSAkTCQ7CjMuIHJlcGVhdCB1bnRpbCBhIGZyYWN0aW9uICRccmhvJCBvZiB0aGUgb2JzZXJ2ZWQgdmFsdWVzIGlzIHJlbW92ZWQuCgpUaGUgcmVnaW1lcyBhcmUgKnNob3J0KiAkXG1hdGhybSBVXHsxLi40XH0kICh1cCB0byAyIGgpLCAqbWVkaXVtKiAkXG1hdGhybSBVXHs1Li40OFx9JCAodXAgdG8gb25lIGRheSkKYW5kICpsb25nKiAkXG1hdGhybSBVXHs0OS4uMzM2XH0kICh1cCB0byBvbmUgd2VlayksIHdpdGggJFxyaG9caW5cezEwXCUsMzBcJVx9JCBhbmQgdGhyZWUgcmVwbGljYXRlczoKMTggc2NlbmFyaW9zIMOXIDE1IHNlcmllcyA9ICoqMjcwIHNjZW5hcmlvcyoqLiBXaXRoIGBtYXNrX3NvdXJjZSA9ICJmaWxlImAgdGhlIGJsb2NrcyBhcmUgcmVhZCBmcm9tCmBtYXNrcy9gLCBnZW5lcmF0ZWQgb25jZSB3aXRoIHRoaXMgYWxnb3JpdGhtIGFuZCBmaXhlZCBzZWVkcywgc28gZXZlcnkgcnVuIHVzZXMgdGhlIHNhbWUgc2NlbmFyaW9zLgoKIyMgQWNjdXJhY3kgbWVhc3VyZXMgKEVxLiA5KQoKJCQKXG1hdGhybXtSTVNFfT1cQmlnKFx0ZnJhY3sxfXt8XG1hdGhjYWwgQXx9XHN1bV97dFxpblxtYXRoY2FsIEF9KFxoYXQgeV90LXlfdCleMlxCaWcpXnsxLzJ9LFxxcXVhZApcbWF0aHJte01BRX09XHRmcmFjezF9e3xcbWF0aGNhbCBBfH1cc3VtX3t0XGluXG1hdGhjYWwgQX18XGhhdCB5X3QteV90fCxccXF1YWQKXG1hdGhybXtOUk1TRX09XGZyYWN7XG1hdGhybXtSTVNFfX17XHNpZ21hX3l9LCBcdGFnezl9CiQkCgp3aGVyZSAkXG1hdGhjYWwgQSQgaXMgdGhlIHNldCBvZiByZW1vdmVkIHBvaW50cy4gTlJNU0UgbWFrZXMgZXJyb3JzIGNvbXBhcmFibGUgYWNyb3NzIHBvbGx1dGFudHMuCgpgYGB7ciBtYXNraW5nfQpSRUdJTUVTIDwtIGxpc3Qoc2hvcnQgPSBjKDEsIDQpLCBtZWRpdW0gPSBjKDUsIDQ4KSwgbG9uZyA9IGMoNDksIDMzNikpOyBSRUdfTkFNRVMgPC0gbmFtZXMoUkVHSU1FUykKbWFrZV9ibG9ja19tYXNrIDwtIGZ1bmN0aW9uKHgsIHJhdGUsIHJlZ2ltZSwgc2VlZCkgeyAgICAgICMgdXNlZCB3aGVuIG1hc2tfc291cmNlID0gIlIiCiAgc2V0LnNlZWQoc2VlZCkKICBuIDwtIGxlbmd0aCh4KTsgb2JzIDwtICFpcy5uYSh4KTsgdGFyZ2V0IDwtIHJvdW5kKHJhdGUgKiBzdW0ob2JzKSkKICBsciA8LSBSRUdJTUVTW1tyZWdpbWVdXTsgYXZhaWwgPC0gb2JzOyBtYXNrIDwtIGxvZ2ljYWwobik7IGNudCA8LSAwOyB0cmllcyA8LSAwCiAgd2hpbGUgKGNudCA8IHRhcmdldCAmJiB0cmllcyA8IDIwMDAwMCkgewogICAgdHJpZXMgPC0gdHJpZXMgKyAxCiAgICBMIDwtIHNhbXBsZShsclsxXTpsclsyXSwgMSk7IHMgPC0gc2FtcGxlLmludChuIC0gTCAtIDIsIDEpICsgMQogICAgaWYgKGFsbChhdmFpbFsocyAtIDEpOihzICsgTCldKSkgeyAgICAgICAgICAgICAgICAgICAgICMgYmxvY2sgYW5kIGJvdGggZ3VhcmQgcG9pbnRzIGF2YWlsYWJsZQogICAgICBtYXNrW3M6KHMgKyBMIC0gMSldIDwtIFRSVUU7IGF2YWlsWyhzIC0gMSk6KHMgKyBMKV0gPC0gRkFMU0U7IGNudCA8LSBjbnQgKyBMCiAgICB9CiAgfQogIG1hc2sKfQpsb2FkX21hc2tzIDwtIGZ1bmN0aW9uKGV4cCkgewogIGZfc2MgPC0gZmlsZS5wYXRoKFAkbWFza19kaXIsIHNwcmludGYoIm1hc2tzXyVzX3NjZW5hcmlvcy5jc3YiLCBleHApKQogIGZfYmwgPC0gZmlsZS5wYXRoKFAkbWFza19kaXIsIHNwcmludGYoIm1hc2tzXyVzX2Jsb2Nrcy5jc3YiLCBleHApKQogIGlmICghZmlsZS5leGlzdHMoZl9zYykgfHwgIWZpbGUuZXhpc3RzKGZfYmwpKSByZXR1cm4oTlVMTCkKICBibCA8LSByZWFkLmNzdihmX2JsKQogIGxpc3Qoc2MgPSByZWFkLmNzdihmX3NjKSwgYmxvY2tzID0gc3BsaXQoYmxbLCBjKCJzdGFydCIsICJsZW4iKV0sIGJsJHNpZCkpCn0KTUsgPC0gbGlzdChleHAwID0gTlVMTCwgZXhwMSA9IE5VTEwpCm1hc2tfZmlsZXMgPC0gZmlsZS5wYXRoKFAkbWFza19kaXIsIHNwcmludGYoIm1hc2tzXyVzXyVzLmNzdiIsIHJlcChjKCJleHAwIiwgImV4cDEiKSwgZWFjaCA9IDIpLCBjKCJzY2VuYXJpb3MiLCAiYmxvY2tzIikpKQppZiAoUCRtYXNrX3NvdXJjZSA9PSAiZmlsZSIgJiYgIWFsbChmaWxlLmV4aXN0cyhtYXNrX2ZpbGVzKSkpIHsKICBjYXQoIk5PVEU6IHRoZSBzdG9yZWQgbWFza3Mgd2VyZSBub3QgZm91bmQgaW4iLCBQJG1hc2tfZGlyLCAiLSBuZXcgbWFza3MgYXJlIGdlbmVyYXRlZCBpbiBSIGFuZCBldmVyeSByZXN1bHQgaXMiLAogICAgICAicmVjb21wdXRlZCwgc28gdGhlIG51bWJlcnMgZGlmZmVyIHNsaWdodGx5IGZyb20gdGhlIHN0b3JlZCByZXN1bHRzLlxuIikKICBQJG1hc2tfc291cmNlIDwtICJSIjsgUCR1c2VfY2FjaGUgPC0gRkFMU0UKfQppZiAoUCRtYXNrX3NvdXJjZSA9PSAiZmlsZSIpIHsgTUskZXhwMCA8LSBsb2FkX21hc2tzKCJleHAwIik7IE1LJGV4cDEgPC0gbG9hZF9tYXNrcygiZXhwMSIpIH0KVEFHMCA8LSBpZiAoaXMubnVsbChNSyRleHAwKSkgIlItbWFza3MiIGVsc2UgImZpbGUtbWFza3MiClRBRzEgPC0gaWYgKGlzLm51bGwoTUskZXhwMSkpICJSLW1hc2tzIiBlbHNlICJmaWxlLW1hc2tzIgpnZXRfbWFzayA8LSBmdW5jdGlvbihleHAsIHgsIHN0LCBwLCByZWcsIHJhdGUsIHJlcCwgc2VlZCkgewogIG1rIDwtIE1LW1tleHBdXQogIGlmIChpcy5udWxsKG1rKSkgcmV0dXJuKG1ha2VfYmxvY2tfbWFzayh4LCByYXRlLCByZWcsIHNlZWQpKQogIHNpZCA8LSBtayRzYyRzaWRbbWskc2Mkc3RhdGlvbiA9PSBzdCAmIG1rJHNjJHBvbGx1dGFudCA9PSBwICYgbWskc2MkcmVnaW1lID09IHJlZyAmCiAgICAgICAgICAgICAgICAgICBhYnMobWskc2MkcmF0ZSAtIHJhdGUpIDwgMWUtOSAmIG1rJHNjJHJlcCA9PSByZXBdCiAgYiA8LSBtayRibG9ja3NbW2FzLmNoYXJhY3RlcihzaWQpXV0KICBtIDwtIGxvZ2ljYWwobGVuZ3RoKHgpKTsgbVt1bmxpc3QobWFwcGx5KGZ1bmN0aW9uKHMsIGwpIHM6KHMgKyBsIC0gMSksIGIkc3RhcnQsIGIkbGVuKSldIDwtIFRSVUUKICBtCn0KY2F0KCJNYXNrczogRXhwZXJpbWVudCAwID0iLCBUQUcwLCAifCBFeHBlcmltZW50IDEgPSIsIFRBRzEsICJcbiIpCmBgYAoKIyBFeHBlcmltZW50IDA6IGJhbmR3aWR0aCBzZWxlY3Rpb24KCkZvciBhIHRhcmdldCBzdGF0aW9uICRzXiokIHRoZSBiYW5kd2lkdGhzIGFyZSBjaG9zZW4gb24gdGhlICoqb3RoZXIgdHdvIHN0YXRpb25zKio6CgokJAooXGhhdCBoX0QsXGhhdCBoX1cpX3tzXip9PVxhcmdcbWluX3toX0RcaW5cezMuNSw3LDE0LDI4XH0sXCBoX1dcaW5cezIsNCw4LDE2XH19XApcZnJhY3sxfXt8XG1hdGhjYWwgU197LXNeKn18fVxzdW1feyhzLHAscixpKVxpblxtYXRoY2FsIFNfey1zXip9fVxtYXRocm17TlJNU0V9X3tzLHAscixpfShoX0QsaF9XKSwKJCQKCndoZXJlICRcbWF0aGNhbCBTX3stc14qfSQgY29udGFpbnMgdGhlIHNjZW5hcmlvcyBvZiB0aGUgdHdvIG90aGVyIHN0YXRpb25zICg1IHBvbGx1dGFudHMgw5cgMyByZWdpbWVzIMOXCjIgcmVwbGljYXRlcywgJFxyaG89MjBcJSQpLiBTVUYgMSBuZXZlciBpbmZsdWVuY2VzIGl0cyBvd24gYmFuZHdpZHRocy4KCmBgYHtyIGV4cDAtcnVufQpCV19EQVkgIDwtIGlmIChRVUlDSykgYyg3LCAxNCkgZWxzZSBjKDMuNSwgNywgMTQsIDI4KQpCV19XRUVLIDwtIGlmIChRVUlDSykgYyg0LCAxNikgZWxzZSBjKDIsIDQsIDgsIDE2KQpkaXIwIDwtIG91dF9maWxlKHBhc3RlMCgiZXhwMF8iLCBUQUcwLCBpZiAoUVVJQ0spICJfcXVpY2siIGVsc2UgIiIpKTsgZGlyLmNyZWF0ZShkaXIwLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKZm9yIChzaSBpbiBzZXFfYWxvbmcoU1RBVElPTlMpKSBmb3IgKHBrIGluIHNlcV9hbG9uZyhQT0xTKSkgewogIHN0IDwtIFNUQVRJT05TW3NpXTsgcCA8LSBQT0xTW3BrXTsgZiA8LSBmaWxlLnBhdGgoZGlyMCwgc3ByaW50ZigiJXNfJXMuY3N2IiwgZ3N1YigiICIsICIiLCBzdCksIHApKQogIGlmIChpc1RSVUUoUCR1c2VfY2FjaGUpICYmIGZpbGUuZXhpc3RzKGYpKSBuZXh0CiAgeCA8LSBnZXRfc2VyaWVzKHN0LCBwKTsgc2R4IDwtIHNkX3BvcCh4KTsgcm93cyA8LSBsaXN0KCkKICBmb3IgKHJpIGluIHNlcV9hbG9uZyhSRUdfTkFNRVMpKSBmb3IgKHJlcCBpbiBzZXFfbGVuKGlmIChRVUlDSykgMSBlbHNlIDIpKSB7CiAgICBtIDwtIGdldF9tYXNrKCJleHAwIiwgeCwgc3QsIHAsIFJFR19OQU1FU1tyaV0sIDAuMjAsIHJlcCwgc2VlZCA9IDEwMDAwICsgMTAwICogc2kgKyAxMCAqIHBrICsgMyAqIHJpICsgcmVwKQogICAgeG0gPC0geDsgeG1bbV0gPC0gTkEKICAgIGZvciAoYmQgaW4gQldfREFZKSBmb3IgKGJ3IGluIEJXX1dFRUspIHsKICAgICAgeSA8LSBpbXB1dGVfc2Vhc2FyKHhtLCBid19kYXkgPSBiZCwgYndfd2VlayA9IGJ3KQogICAgICByb3dzW1tsZW5ndGgocm93cykgKyAxXV0gPC0gZGF0YS5mcmFtZShzdGF0aW9uID0gc3QsIHBvbGx1dGFudCA9IHAsIHJlZ2ltZSA9IFJFR19OQU1FU1tyaV0sIHJlcCA9IHJlcCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYndfZGF5ID0gYmQsIGJ3X3dlZWsgPSBidywgbnJtc2UgPSBybXNlKHlbbV0sIHhbbV0pIC8gc2R4KQogICAgfQogIH0KICB3cml0ZS5jc3YoYmluZF9yb3dzKHJvd3MpLCBmLCByb3cubmFtZXMgPSBGQUxTRSkKfQpncmlkIDwtIGJpbmRfcm93cyhsYXBwbHkobGlzdC5maWxlcyhkaXIwLCBwYXR0ZXJuID0gIlxcLmNzdiQiLCBmdWxsLm5hbWVzID0gVFJVRSksIHJlYWQuY3N2KSkKQlcgPC0gbGFwcGx5KHNldE5hbWVzKFNUQVRJT05TLCBTVEFUSU9OUyksIGZ1bmN0aW9uKHN0KSB7CiAgZyA8LSBncmlkIHw+IGZpbHRlcihzdGF0aW9uICE9IHN0KSB8PiBncm91cF9ieShid19kYXksIGJ3X3dlZWspIHw+CiAgICBzdW1tYXJpc2UobnJtc2UgPSBtZWFuKG5ybXNlKSwgLmdyb3VwcyA9ICJkcm9wIikgfD4gYXJyYW5nZShid19kYXksIGJ3X3dlZWspIHw+CiAgICBzbGljZV9taW4obnJtc2UsIG4gPSAxLCB3aXRoX3RpZXMgPSBGQUxTRSkKICBsaXN0KGJ3X2RheSA9IGckYndfZGF5LCBid193ZWVrID0gZyRid193ZWVrLCBucm1zZSA9IGckbnJtc2UpCn0pCmNhdCgiRXhwZXJpbWVudCAwOiIsIG5yb3coZ3JpZCksICJncmlkIGV2YWx1YXRpb25zXG4iKQpgYGAKCmBgYHtyIGV4cDAtcmVzdWx0cywgZmlnLndpZHRoPTMuNSwgZmlnLmhlaWdodD0yLjQ1LCBvdXQud2lkdGg9JzU1JSd9CmZvciAoc3QgaW4gU1RBVElPTlMpIHsga2V5KHBhc3RlMCgiYndfZGF5XyIsIGdzdWIoIiAiLCAiIiwgc3QpKSwgQldbW3N0XV0kYndfZGF5KTsga2V5KHBhc3RlMCgiYndfd2Vla18iLCBnc3ViKCIgIiwgIiIsIHN0KSksIEJXW1tzdF1dJGJ3X3dlZWspIH0KZzQgPC0gZ3JpZCB8PiBmaWx0ZXIoc3RhdGlvbiAhPSAiU1VGIDEiKSB8PiBncm91cF9ieShid19kYXksIGJ3X3dlZWspIHw+CiAgc3VtbWFyaXNlKG5ybXNlID0gbWVhbihucm1zZSksIC5ncm91cHMgPSAiZHJvcCIpIHw+IG11dGF0ZShiZXN0ID0gbnJtc2UgPT0gbWluKG5ybXNlKSkKa2V5KCJncmlkX2Jlc3RfbnJtc2UiLCBtaW4oZzQkbnJtc2UpKTsga2V5KCJncmlkX3dvcnN0X25ybXNlIiwgbWF4KGc0JG5ybXNlKSkKa2V5KCJncmlkX3dvcnN0X3ZzX2Jlc3RfcGN0IiwgMTAwICogKG1heChnNCRucm1zZSkgLyBtaW4oZzQkbnJtc2UpIC0gMSkpCmtleSgiZ3JpZF93b3JzdF9wYWlyIiwgcGFzdGUoZzQkYndfZGF5W3doaWNoLm1heChnNCRucm1zZSldLCBnNCRid193ZWVrW3doaWNoLm1heChnNCRucm1zZSldKSkKcDQgPC0gZ2dwbG90KGc0LCBhZXMoZmFjdG9yKGJ3X3dlZWspLCBmYWN0b3IoYndfZGF5KSwgZmlsbCA9IG5ybXNlKSkgKyBnZW9tX3RpbGUoY29sb3VyID0gIndoaXRlIikgKwogIGdlb21fdGV4dChhZXMobGFiZWwgPSBzcHJpbnRmKCIlLjRmIiwgbnJtc2UpLCBmb250ZmFjZSA9IGlmZWxzZShiZXN0LCAiYm9sZCIsICJwbGFpbiIpLAogICAgICAgICAgICAgICAgY29sb3VyID0gbnJtc2UgPCBtZWFuKHJhbmdlKG5ybXNlKSkpLCBzaXplID0gMi42NSwgZmFtaWx5ID0gRk9OVF9URVhULCBzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKGBUUlVFYCA9ICJ3aGl0ZSIsIGBGQUxTRWAgPSAiYmxhY2siKSkgKwogIHNjYWxlX2ZpbGxfZGlzdGlsbGVyKHBhbGV0dGUgPSAiQmx1ZXMiLCBkaXJlY3Rpb24gPSAtMSkgKyBzY2FsZV95X2Rpc2NyZXRlKGxpbWl0cyA9IHJldikgKwogIGxhYnMoeCA9IGV4cHJlc3Npb24oaFtXXSB+ICIod2Vla3MpIiksIHkgPSBleHByZXNzaW9uKGhbRF0gfiAiKGRheXMpIiksIGZpbGwgPSAiTWVhblxuTlJNU0UiKSArCiAgdGhlbWUocGFuZWwuZ3JpZCA9IGVsZW1lbnRfYmxhbmsoKSwgbGVnZW5kLmtleS5oZWlnaHQgPSB1bml0KDAuNSwgImNtIiksIGF4aXMudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gNy41KSwKICAgICAgICBheGlzLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA5KSwgbGVnZW5kLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcpLCBsZWdlbmQudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDgpKQpwcmludChwNCk7IHNhdmVfZmlnKHA0LCAiZmlnMDRfYmFuZHdpZHRoIiwgMy41LCAyLjQ1KQprbml0cjo6a2FibGUoZGF0YS5mcmFtZShzdGF0aW9uID0gU1RBVElPTlMsIGhfRCA9IHNhcHBseShCVywgYFtbYCwgImJ3X2RheSIpLCBoX1cgPSBzYXBwbHkoQlcsIGBbW2AsICJid193ZWVrIiksCiAgICAgICAgICAgICAgICAgICAgICAgIG5ybXNlID0gc2FwcGx5KEJXLCBgW1tgLCAibnJtc2UiKSksIGRpZ2l0cyA9IDQsCiAgICAgICAgICAgICBjb2wubmFtZXMgPSBjKCJUYXJnZXQgc3RhdGlvbiIsICJoX0QgKGRheXMpIiwgImhfVyAod2Vla3MpIiwgIk1lYW4gTlJNU0Ugb24gdGhlIG90aGVyIHN0YXRpb25zIiksCiAgICAgICAgICAgICBjYXB0aW9uID0gIlNlbGVjdGVkIGJhbmR3aWR0aHMgKGxlYXZlLW9uZS1zdGF0aW9uLW91dCkiKQpgYGAKCiMgRXhwZXJpbWVudCAxOiBpbXB1dGF0aW9uIGFjY3VyYWN5CgojIyBSdW5uaW5nIHRoZSBleHBlcmltZW50CgpGb3IgZXZlcnkgc2NlbmFyaW8gdGhlIHRlbiBtZXRob2RzLCB0aGUgZml2ZSBhYmxhdGlvbiB2YXJpYW50cyBvZiBTZWFzLUFSIGFuZCwgd2hlbiBgaW1wdXRlVFNgIGlzCmluc3RhbGxlZCwgdGhlIG9yaWdpbmFsIGBuYV9zZWFkZWNgIGFyZSBhcHBsaWVkIHRvIHRoZSBzYW1lIG1hc2tlZCBzZXJpZXMsIGFuZCB0aGUgY29tcHV0aW5nIHRpbWUgaXMKcmVjb3JkZWQuIFNlYXMtQVIgdXNlcyB0aGUgYmFuZHdpZHRocyBvZiBFeHBlcmltZW50IDAgZm9yIHRoZSBzdGF0aW9uIGNvbmNlcm5lZC4gUmVzdWx0cyBhcmUgc3RvcmVkIHBlcgpzZXJpZXMsIHNvIGFuIGludGVycnVwdGVkIHJ1biByZXN1bWVzIHdoZXJlIGl0IHN0b3BwZWQuCgpgYGB7ciBleHAxLXJ1bn0KUkFURVMgIDwtIGlmIChRVUlDSykgMC4zMCBlbHNlIGMoMC4xMCwgMC4zMCkKTl9SRVBTIDwtIGlmIChRVUlDSykgMSBlbHNlIDMKc3RfbGlzdCA8LSBpZiAoUVVJQ0spICJTVUYgMSIgZWxzZSBTVEFUSU9OUwpkaXIxIDwtIG91dF9maWxlKHBhc3RlMCgiZXhwMV8iLCBUQUcxLCBpZiAoUVVJQ0spICJfcXVpY2siIGVsc2UgIiIpKTsgZGlyLmNyZWF0ZShkaXIxLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKZm9yIChzdCBpbiBzdF9saXN0KSBmb3IgKHAgaW4gUE9MUykgewogIGYgPC0gZmlsZS5wYXRoKGRpcjEsIHNwcmludGYoIiVzXyVzLmNzdiIsIGdzdWIoIiAiLCAiIiwgc3QpLCBwKSkKICBpZiAoaXNUUlVFKFAkdXNlX2NhY2hlKSAmJiBmaWxlLmV4aXN0cyhmKSkgbmV4dAogIHNpIDwtIG1hdGNoKHN0LCBTVEFUSU9OUyk7IHBrIDwtIG1hdGNoKHAsIFBPTFMpCiAgeCA8LSBnZXRfc2VyaWVzKHN0LCBwKTsgc2R4IDwtIHNkX3BvcCh4KTsgYncgPC0gQldbW3N0XV0KICBtZXRocyA8LSBNRVRIT0RTCiAgbWV0aHMkYFNlYXMtQVJgIDwtIGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgYndfZGF5ID0gYnckYndfZGF5LCBid193ZWVrID0gYnckYndfd2VlaykKICBhYmwgPC0gbGlzdCgKICAgIGB3L28gbG9nIHRyYW5zZm9ybWAgPSBmdW5jdGlvbih2KSBpbXB1dGVfc2Vhc2FyKHYsIGJ3JGJ3X2RheSwgYnckYndfd2VlaywgdHJhbnNmb3JtID0gIm5vbmUiKSwKICAgIGB3L28gd2Vla2x5IGNvbXAuYCAgPSBmdW5jdGlvbih2KSBpbXB1dGVfc2Vhc2FyKHYsIGJ3JGJ3X2RheSwgYnckYndfd2Vlaywgd2Vla2x5ID0gRkFMU0UpLAogICAgYHN0YXRpYyBwcm9maWxlc2AgICA9IGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgSW5mLCBJbmYpLAogICAgYHJlbWFpbmRlciA9IDBgICAgICA9IGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgYnckYndfZGF5LCBidyRid193ZWVrLCByZW1haW5kZXIgPSAiemVybyIpLAogICAgYHJlbWFpbmRlciBMSWAgICAgICA9IGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgYnckYndfZGF5LCBidyRid193ZWVrLCByZW1haW5kZXIgPSAibGluZWFyIikpCiAgZXh0cmEgPC0gaWYgKEhBU19JTVBVVEVUUykgbGlzdChgTkFfU0VBREVDIChpbXB1dGVUUylgID0gZnVuY3Rpb24odikKICAgIGFzLm51bWVyaWMoaW1wdXRlVFM6Om5hX3NlYWRlYyh0cyh2LCBmcmVxdWVuY3kgPSBTMSksIGFsZ29yaXRobSA9ICJpbnRlcnBvbGF0aW9uIikpKSBlbHNlIGxpc3QoKQogIGFsbGYgIDwtIGMobWV0aHMsIGFibCwgZXh0cmEpCiAga2luZHMgPC0gYyhyZXAoIm1haW4iLCBsZW5ndGgobWV0aHMpKSwgcmVwKCJhYmxhdGlvbiIsIGxlbmd0aChhYmwpKSwgcmVwKCJleHRyYSIsIGxlbmd0aChleHRyYSkpKQogIHJvd3MgPC0gbGlzdCgpCiAgZm9yIChyaSBpbiBzZXFfYWxvbmcoUkVHX05BTUVTKSkgZm9yIChyYXRlIGluIFJBVEVTKSBmb3IgKHJlcCBpbiBzZXFfbGVuKE5fUkVQUykpIHsKICAgIHNlZWQgPC0gMjAwMDAgKyAxMDAwICogc2kgKyAxMDAgKiBwayArIDEwICogcmkgKyAzICogbWF0Y2gocmF0ZSwgYygwLjEwLCAwLjMwKSkgKyByZXAKICAgIG0gPC0gZ2V0X21hc2soImV4cDEiLCB4LCBzdCwgcCwgUkVHX05BTUVTW3JpXSwgcmF0ZSwgcmVwLCBzZWVkKTsgeG0gPC0geDsgeG1bbV0gPC0gTkEKICAgIGZvciAoaiBpbiBzZXFfYWxvbmcoYWxsZikpIHsKICAgICAgdDEgPC0gcHJvYy50aW1lKClbWyJlbGFwc2VkIl1dOyB5IDwtIHBtYXgoYWxsZltbal1dKHhtKSwgMCk7IGR0IDwtIHByb2MudGltZSgpW1siZWxhcHNlZCJdXSAtIHQxCiAgICAgIHJvd3NbW2xlbmd0aChyb3dzKSArIDFdXSA8LSBkYXRhLmZyYW1lKHN0YXRpb24gPSBzdCwgcG9sbHV0YW50ID0gcCwgcmVnaW1lID0gUkVHX05BTUVTW3JpXSwgcmF0ZSA9IHJhdGUsCiAgICAgICAgcmVwID0gcmVwLCBtZXRob2QgPSBuYW1lcyhhbGxmKVtqXSwga2luZCA9IGtpbmRzW2pdLCBuX21hc2tlZCA9IHN1bShtKSwKICAgICAgICBybXNlID0gcm1zZSh5W21dLCB4W21dKSwgbWFlID0gbWFlKHlbbV0sIHhbbV0pLCBucm1zZSA9IHJtc2UoeVttXSwgeFttXSkgLyBzZHgsIHNlYyA9IGR0KQogICAgfQogIH0KICB3cml0ZS5jc3YoYmluZF9yb3dzKHJvd3MpLCBmLCByb3cubmFtZXMgPSBGQUxTRSkKfQpyZXMxIDwtIGJpbmRfcm93cyhsYXBwbHkobGlzdC5maWxlcyhkaXIxLCBwYXR0ZXJuID0gIlxcLmNzdiQiLCBmdWxsLm5hbWVzID0gVFJVRSksIHJlYWQuY3N2KSkKbWFpbiA8LSByZXMxIHw+IGZpbHRlcihraW5kID09ICJtYWluIikgfD4KICBtdXRhdGUoc2NlbmFyaW8gPSBwYXN0ZShzdGF0aW9uLCBwb2xsdXRhbnQsIHJlZ2ltZSwgcmF0ZSwgcmVwLCBzZXAgPSAifCIpLCBtZXRob2QgPSBmYWN0b3IobWV0aG9kLCBsZXZlbHMgPSBNRVRIT0RfT1JERVIpKQpjYXQoIkV4cGVyaW1lbnQgMToiLCBuX2Rpc3RpbmN0KG1haW4kc2NlbmFyaW8pLCAic2NlbmFyaW9zLCIsIG5yb3cocmVzMSksICJyZXN1bHQgcm93c1xuIikKYGBgCgojIyBBY2N1cmFjeSBieSBtZXRob2QKCkxlZnQ6IFJNU0UgYXQgU1VGIDEgaW4gb3JpZ2luYWwgdW5pdHMgKG1lYW4gb2YgMTggc2NlbmFyaW9zIHBlciBwb2xsdXRhbnQpLiBNaWRkbGU6IE5STVNFIGF2ZXJhZ2VkIG92ZXIKdGhlIDE1IHNlcmllcyBmb3IgZWFjaCBnYXAgcmVnaW1lLiBSaWdodDogYXZlcmFnZSBGcmllZG1hbiByYW5rIGFuZCBtZWFuIGNvbXB1dGluZyB0aW1lIHBlciBzZXJpZXMuCgpgYGB7ciB0YWJsZTR9ClB3IDwtIG1haW4gfD4gc2VsZWN0KHNjZW5hcmlvLCBtZXRob2QsIG5ybXNlKSB8PiBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gbWV0aG9kLCB2YWx1ZXNfZnJvbSA9IG5ybXNlKQpQbSA8LSBhcy5tYXRyaXgoUHdbLCBNRVRIT0RfT1JERVJdKQphdmdfcmFuayA8LSBjb2xNZWFucyh0KGFwcGx5KFBtLCAxLCByYW5rKSkpICAgICAgICAgICAgICAgICAjIHJhbmsgMSA9IG1vc3QgYWNjdXJhdGUKdDQgPC0gZGF0YS5mcmFtZShtZXRob2QgPSBmYWN0b3IoTUVUSE9EX09SREVSLCBsZXZlbHMgPSBNRVRIT0RfT1JERVIpKSB8PgogIGxlZnRfam9pbihtYWluIHw+IGZpbHRlcihzdGF0aW9uID09ICJTVUYgMSIpIHw+IGdyb3VwX2J5KG1ldGhvZCwgcG9sbHV0YW50KSB8PiBzdW1tYXJpc2UodiA9IG1lYW4ocm1zZSksIC5ncm91cHMgPSAiZHJvcCIpIHw+CiAgICAgICAgICAgICAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHBvbGx1dGFudCwgdmFsdWVzX2Zyb20gPSB2KSwgYnkgPSAibWV0aG9kIikgfD4KICBsZWZ0X2pvaW4obWFpbiB8PiBncm91cF9ieShtZXRob2QsIHJlZ2ltZSkgfD4gc3VtbWFyaXNlKHYgPSBtZWFuKG5ybXNlKSwgLmdyb3VwcyA9ICJkcm9wIikgfD4KICAgICAgICAgICAgICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gcmVnaW1lLCB2YWx1ZXNfZnJvbSA9IHYpLCBieSA9ICJtZXRob2QiKSB8PgogIGxlZnRfam9pbihtYWluIHw+IGdyb3VwX2J5KG1ldGhvZCkgfD4gc3VtbWFyaXNlKGFsbCA9IG1lYW4obnJtc2UpLCBzZWMgPSBtZWFuKHNlYyksIC5ncm91cHMgPSAiZHJvcCIpLCBieSA9ICJtZXRob2QiKQp0NCRyYW5rIDwtIGF2Z19yYW5rW2FzLmNoYXJhY3Rlcih0NCRtZXRob2QpXQp3cml0ZS5jc3YodDQsIG91dF9maWxlKCJleHAxX3RhYmxlNC5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCnBjIDwtIGludGVyc2VjdChQT0xTLCBuYW1lcyh0NCkpOyByYyA8LSBpbnRlcnNlY3QoUkVHX05BTUVTLCBuYW1lcyh0NCkpCm1kNCA8LSBkYXRhLmZyYW1lKE1ldGhvZCA9IGFzLmNoYXJhY3Rlcih0NCRtZXRob2QpLCBjaGVjay5uYW1lcyA9IEZBTFNFKQpmb3IgKHAgaW4gcGMpIG1kNFtbcF1dIDwtIGZtdF9tZCh0NFtbcF1dLCBpZiAocCA9PSAiQ08iKSAzIGVsc2UgMikKZm9yIChyIGluIHJjKSBtZDRbW3Bhc3RlKCJOUk1TRSIsIHIpXV0gPC0gZm10X21kKHQ0W1tyXV0sIDMpCm1kNFtbIkF2Zy4gcmFuayJdXSA8LSBmbXRfbWQodDQkcmFuaywgMik7IG1kNFtbIlRpbWUgKHMpIl1dIDwtIGZtdF9udW0odDQkc2VjLCAzKQp0eCA8LSBsYXBwbHkocGMsIGZ1bmN0aW9uKHApIGZtdF90ZXgodDRbW3BdXSwgaWYgKHAgPT0gIkNPIikgMyBlbHNlIDIpKTsgdHIgPC0gbGFwcGx5KHJjLCBmdW5jdGlvbihyKSBmbXRfdGV4KHQ0W1tyXV0sIDMpKQpyb3dzNCA8LSBzYXBwbHkoc2VxX2xlbihucm93KHQ0KSksIGZ1bmN0aW9uKGkpIHBhc3RlMChwYXN0ZShjKAogIGlmICh0NCRtZXRob2RbaV0gPT0gIlNlYXMtQVIiKSAiXFx0ZXh0YmZ7U2Vhcy1BUiAocHJvcG9zZWQpfSIgZWxzZSBhcy5jaGFyYWN0ZXIodDQkbWV0aG9kW2ldKSwKICBzYXBwbHkodHgsIGBbYCwgaSksIHNhcHBseSh0ciwgYFtgLCBpKSwgZm10X3RleCh0NCRyYW5rLCAyKVtpXSwgZm10X251bSh0NCRzZWNbaV0sIDMpKSwgY29sbGFwc2UgPSAiICYgIiksICIgXFxcXCIpKQp3cml0ZV90ZXgoYygiXFxiZWdpbnt0YWJsZSp9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgc3ByaW50ZigiXFxjYXB0aW9ue0ltcHV0YXRpb24gYWNjdXJhY3kgdW5kZXIgYXJ0aWZpY2lhbCBibG9jayBtaXNzaW5nbmVzcy4gTGVmdDogUk1TRSBhdCBTVUZ+MSBpbiBvcmlnaW5hbCB1bml0cywgYXZlcmFnZWQgb3ZlciB0aGUgdGhyZWUgZ2FwIHJlZ2ltZXMsIHR3byBtaXNzaW5nIHJhdGVzIGFuZCB0aHJlZSByZXBsaWNhdGVzLiBNaWRkbGU6IE5STVNFIChSTVNFIGRpdmlkZWQgYnkgdGhlIHN0YW5kYXJkIGRldmlhdGlvbiBvZiB0aGUgc2VyaWVzKSBhdmVyYWdlZCBvdmVyIGFsbCAxNSBzZXJpZXMgKHRocmVlIHN0YXRpb25zICRcXHRpbWVzJCBmaXZlIHBvbGx1dGFudHMpIGZvciBlYWNoIGdhcCByZWdpbWUuIFJpZ2h0OiBhdmVyYWdlIEZyaWVkbWFuIHJhbmsgb3ZlciB0aGUgJWQgc2NlbmFyaW9zIGFuZCBtZWFuIGNvbXB1dGluZyB0aW1lIHBlciBpbXB1dGF0aW9uIG9mIGEgZnVsbCBzZXJpZXMgKFIsIG9uZSBDUFUgY29yZSkuIEJlc3QgdmFsdWVzIGluIGJvbGQufSIsIG5yb3coUG0pKSwKICAiXFxsYWJlbHt0YWI6aW1wfSIsICJcXHNldGxlbmd0aHtcXHRhYmNvbHNlcH17NC4ycHR9IiwgIlxcYmVnaW57dGFidWxhcn17bHJycnJycnJycnJ9IiwgIlxcdG9wcnVsZSIsCiAgIiAmIFxcbXVsdGljb2x1bW57NX17Y317Uk1TRSBhdCBTVUZ+MX0gJiBcXG11bHRpY29sdW1uezN9e2N9e05STVNFLCBhbGwgc3RhdGlvbnN9ICYgJiBcXFxcIiwKICAiXFxjbWlkcnVsZShscil7Mi02fVxcY21pZHJ1bGUobHIpezctOX0iLAogICJNZXRob2QgJiBQTSRfezEwfSQgJiBOTyRfMiQgJiBTTyRfMiQgJiBDTyAmIE8kXzMkICYgU2hvcnQgJiBNZWRpdW0gJiBMb25nICYgQXZnLiByYW5rICYgVGltZSAocylcXFxcIiwgIlxcbWlkcnVsZSIsCiAgcm93czQsICJcXGJvdHRvbXJ1bGUiLCAiXFxlbmR7dGFidWxhcn0iLCAiXFxlbmR7dGFibGUqfSIpLCAidGFiX2ltcHV0YXRpb24iKQprbml0cjo6a2FibGUobWQ0LCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9IGNhcCgiVGFibGUgNSIsICJSTVNFIGF0IFNVRiAxLCBOUk1TRSBvZiBhbGwgc3RhdGlvbnMgYnkgZ2FwIHJlZ2ltZSwgYXZlcmFnZSByYW5rIGFuZCB0aW1lIikpCmBgYAoKIyMgTlJNU0UgYnkgZ2FwIHJlZ2ltZSBhbmQgbWlzc2luZyByYXRlCgpFYWNoIHJvdyBpcyBhIG1ldGhvZCAob3JkZXJlZCBieSBvdmVyYWxsIE5STVNFLCBiZXN0IGF0IHRoZSB0b3ApLCBlYWNoIHBhbmVsIGEgZ2FwIHJlZ2ltZTsgb3BlbiBhbmQKZmlsbGVkIGNpcmNsZXMgYXJlIHRoZSAxMCUgYW5kIDMwJSBtaXNzaW5nIHJhdGVzLCBhbmQgU2Vhcy1BUiBpcyBkcmF3biBpbiByZWQuCgpgYGB7ciBmaWc1LCBmaWcud2lkdGg9Ny4xNiwgZmlnLmhlaWdodD0yLjZ9Cm9yZDUgPC0gbWFpbiB8PiBncm91cF9ieShtZXRob2QpIHw+IHN1bW1hcmlzZSh2ID0gbWVhbihucm1zZSksIC5ncm91cHMgPSAiZHJvcCIpIHw+IGFycmFuZ2UoZGVzYyh2KSkgfD4gcHVsbChtZXRob2QpCmQ1IDwtIG1haW4gfD4gZ3JvdXBfYnkobWV0aG9kLCByZWdpbWUsIHJhdGUpIHw+IHN1bW1hcmlzZShucm1zZSA9IG1lYW4obnJtc2UpLCAuZ3JvdXBzID0gImRyb3AiKSB8PgogIG11dGF0ZShyZWdpbWUgPSBmYWN0b3IocmVnaW1lLCBsZXZlbHMgPSBSRUdfTkFNRVMsIGxhYmVscyA9IGMoIlNob3J0IGdhcHMgKDEtNCBzdGVwcykiLCAiTWVkaXVtIGdhcHMgKDUtNDggc3RlcHMpIiwgIkxvbmcgZ2FwcyAoNDktMzM2IHN0ZXBzKSIpKSwKICAgICAgICAgcmF0ZSA9IGZhY3RvcihzcHJpbnRmKCIlZCUlIiwgcm91bmQoMTAwICogcmF0ZSkpKSwgbWV0aG9kID0gZmFjdG9yKG1ldGhvZCwgbGV2ZWxzID0gYXMuY2hhcmFjdGVyKG9yZDUpKSwKICAgICAgICAgc2Vhc2FyID0gbWV0aG9kID09ICJTZWFzLUFSIikKcDUgPC0gZ2dwbG90KGQ1LCBhZXMobnJtc2UsIG1ldGhvZCkpICsKICBnZW9tX2xpbmUoYWVzKGdyb3VwID0gbWV0aG9kKSwgY29sb3VyID0gImdyZXk3NSIsIGxpbmV3aWR0aCA9IDAuNSkgKwogIGdlb21fcG9pbnQoYWVzKHNoYXBlID0gcmF0ZSwgY29sb3VyID0gc2Vhc2FyKSwgc2l6ZSA9IDEuNywgc3Ryb2tlID0gMC42KSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKGBGQUxTRWAgPSAiZ3JleTIwIiwgYFRSVUVgID0gIiNjMDM5MmIiKSwgZ3VpZGUgPSAibm9uZSIpICsKICBzY2FsZV9zaGFwZV9tYW51YWwodmFsdWVzID0gYygxLCAxNiksIG5hbWUgPSAiQWRkaXRpb25hbCBtaXNzaW5nIHJhdGUiKSArCiAgZmFjZXRfd3JhcCh+cmVnaW1lLCBucm93ID0gMSkgKyBsYWJzKHggPSAiTlJNU0UgKG1lYW4gb3ZlciBzZXJpZXMpIiwgeSA9IE5VTEwpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKGNvbG91ciA9ICJncmV5OTIiKSkKcHJpbnQocDUpOyBzYXZlX2ZpZyhwNSwgImZpZzA1X2FjY3VyYWN5X2J5X3JlZ2ltZSIsIDcuMTYsIDIuNikKYGBgCgojIyBSYW5rcywgRnJpZWRtYW4gdGVzdCBhbmQgTmVtZW55aSBjcml0aWNhbCBkaWZmZXJlbmNlCgpXaXRoaW4gZWFjaCBzY2VuYXJpbyAkaiQgdGhlIG1ldGhvZHMgcmVjZWl2ZSByYW5rcyAkcl97aWp9JCAoMSA9IHNtYWxsZXN0IE5STVNFKSBhbmQKJFxiYXIgcl9pPVxmcmFjMU5cc3VtX2ogcl97aWp9JC4gVGhlIEZyaWVkbWFuIHRlc3QgY2hlY2tzICRIXzAkOiBhbGwgbWV0aG9kcyBhcmUgZXF1aXZhbGVudC4gVHdvCmF2ZXJhZ2UgcmFua3MgZGlmZmVyIHNpZ25pZmljYW50bHkgKE5lbWVueWkpIHdoZW4gdGhlaXIgZGlmZmVyZW5jZSBleGNlZWRzCgokJApcbWF0aHJte0NEfT1xX3tcYWxwaGF9XHNxcnR7XGZyYWN7ayhrKzEpfXs2Tn19LFxxcXVhZCBrPTEwLFwgTj0yNzAsXCBxX3swLjA1fT0zLjE2NCAuCiQkCgpgYGB7ciBmaWc2LCBmaWcud2lkdGg9My41LCBmaWcuaGVpZ2h0PTIuNCwgb3V0LndpZHRoPSc2MCUnfQpmciA8LSBmcmllZG1hbi50ZXN0KFBtKQprXyA8LSBuY29sKFBtKTsgTl8gPC0gbnJvdyhQbSk7IENEIDwtIDMuMTY0ICogc3FydChrXyAqIChrXyArIDEpIC8gKDYgKiBOXykpICAgICAjIHFfMC4wNSA9IDMuMTY0IGZvciBrID0gMTAKZm9yIChtbSBpbiBuYW1lcyhhdmdfcmFuaykpIGtleShwYXN0ZTAoInJhbmtfIiwgbW0pLCBhdmdfcmFua1tbbW1dXSkKa2V5KCJmcmllZG1hbl9jaGkyIiwgdW5uYW1lKGZyJHN0YXRpc3RpYykpOyBrZXkoImZyaWVkbWFuX3AiLCBmciRwLnZhbHVlKTsga2V5KCJuZW1lbnlpX2NkIiwgQ0QpOyBrZXkoIm5fc2NlbmFyaW9zIiwgTl8pCmNhdChzcHJpbnRmKCJGcmllZG1hbiBjaGleMiglZCkgPSAlLjFmLCBwID0gJS4yZyB8IE5lbWVueWkgQ0QgPSAlLjNmIChrID0gJWQsIE4gPSAlZClcbiIsIGtfIC0gMSwgZnIkc3RhdGlzdGljLCBmciRwLnZhbHVlLCBDRCwga18sIE5fKSkKZDYgPC0gZGF0YS5mcmFtZShtZXRob2QgPSBuYW1lcyhhdmdfcmFuayksIHJhbmsgPSBhdmdfcmFuaykgfD4gYXJyYW5nZShyYW5rKSB8PiBtdXRhdGUoeSA9IHJldihzZXFfYWxvbmcocmFuaykpKQpwNiA8LSBnZ3Bsb3QoZDYsIGFlcyhyYW5rLCB5KSkgKwogIGFubm90YXRlKCJyZWN0IiwgeG1pbiA9IG1pbihkNiRyYW5rKSwgeG1heCA9IG1pbihkNiRyYW5rKSArIENELCB5bWluID0gLUluZiwgeW1heCA9IEluZiwgZmlsbCA9ICIjYzAzOTJiIiwgYWxwaGEgPSAuMDgpICsKICBhbm5vdGF0ZSgic2VnbWVudCIsIHggPSBtaW4oZDYkcmFuayksIHhlbmQgPSBtaW4oZDYkcmFuaykgKyBDRCwgeSA9IG5yb3coZDYpICsgLjgsIHllbmQgPSBucm93KGQ2KSArIC44LCBsaW5ld2lkdGggPSAwLjgpICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSBtaW4oZDYkcmFuayksIHkgPSBucm93KGQ2KSArIDEuMzUsIGxhYmVsID0gc3ByaW50ZigiQ0QgPSAlLjJmIiwgQ0QpLCBoanVzdCA9IDAsIHNpemUgPSAyLjQsIGZhbWlseSA9IEZPTlRfVEVYVCkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91ciA9IG1ldGhvZCksIHNpemUgPSAxLjgsIHNob3cubGVnZW5kID0gRkFMU0UpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsID0gc3ByaW50ZigiJXMgKCUuMmYpIiwgbWV0aG9kLCByYW5rKSksIGhqdXN0ID0gLTAuMTUsIHNpemUgPSAyLjQsIGZhbWlseSA9IEZPTlRfVEVYVCkgKwogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gQ09MKSArIHNjYWxlX3hfY29udGludW91cyhsaW1pdHMgPSBjKDEsIDEyKSwgYnJlYWtzID0gc2VxKDIsIDEwLCAyKSkgKwogIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDAuNSwgbnJvdyhkNikgKyAxLjYpKSArCiAgbGFicyh4ID0gIkF2ZXJhZ2UgcmFuayBvdmVyIHRoZSBpbXB1dGF0aW9uIHNjZW5hcmlvcyAoMSA9IGJlc3QpIiwgeSA9IE5VTEwpICsKICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfYmxhbmsoKSwgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9ibGFuaygpKQpwcmludChwNik7IHNhdmVfZmlnKHA2LCAiZmlnMDZfcmFua3MiLCAzLjUsIDIuNCkKYGBgCgojIyBQYWlyZWQgV2lsY294b24gdGVzdHMgd2l0aCBIb2xtIGNvcnJlY3Rpb24KCkZvciBlYWNoIGNvbXBldGl0b3IgJGMkIHRoZSBkaWZmZXJlbmNlcyAkXERlbHRhX2o9XG1hdGhybXtOUk1TRX1faihcbWF0aHJte1NlYXMtQVJ9KS1cbWF0aHJte05STVNFfV9qKGMpJAphcmUgdGVzdGVkIHdpdGggdGhlIHR3by1zaWRlZCBXaWxjb3hvbiBzaWduZWQtcmFuayB0ZXN0IChub3JtYWwgYXBwcm94aW1hdGlvbiB3aXRob3V0IGNvbnRpbnVpdHkKY29ycmVjdGlvbiksIGFuZCB0aGUgbmluZSAkcCQtdmFsdWVzIGFyZSBhZGp1c3RlZCB3aXRoIEhvbG0ncyBtZXRob2QuIE5lZ2F0aXZlICRcRGVsdGEkIGZhdm91cnMgU2Vhcy1BUi4KCmBgYHtyIHRhYmxlNX0KdDUgPC0gYmluZF9yb3dzKGxhcHBseShzZXRkaWZmKE1FVEhPRF9PUkRFUiwgIlNlYXMtQVIiKSwgZnVuY3Rpb24oY2MpIHsKICBkIDwtIFBtWywgIlNlYXMtQVIiXSAtIFBtWywgY2NdCiAgZGF0YS5mcmFtZShjb21wZXRpdG9yID0gY2MsIG1lZGlhbl9kaWZmID0gbWVkaWFuKGQpLCB3aW5zID0gMTAwICogbWVhbihkIDwgMCksCiAgICAgICAgICAgICBwID0gd2lsY294LnRlc3QoUG1bLCAiU2Vhcy1BUiJdLCBQbVssIGNjXSwgcGFpcmVkID0gVFJVRSwgZXhhY3QgPSBGQUxTRSwgY29ycmVjdCA9IEZBTFNFKSRwLnZhbHVlKQp9KSkKdDUkcF9ob2xtIDwtIHAuYWRqdXN0KHQ1JHAsIG1ldGhvZCA9ICJob2xtIikKd3JpdGUuY3N2KHQ1LCBvdXRfZmlsZSgiZXhwMV90ZXN0cy5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCnB0eHQgPC0gZnVuY3Rpb24ocCkgaWZlbHNlKHAgPCAxZS00LCAiJDwxMF57LTR9JCIsIGZvcm1hdEMocCwgZm9ybWF0ID0gImYiLCBkaWdpdHMgPSA0KSkKd3JpdGVfdGV4KGMoIlxcYmVnaW57dGFibGV9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgc3ByaW50ZigiXFxjYXB0aW9ue1BhaXJlZCBjb21wYXJpc29uIG9mIFNlYXMtQVIgd2l0aCBlYWNoIGNvbXBldGluZyBpbXB1dGF0aW9uIG1ldGhvZCBvdmVyIHRoZSAlZCBtYXNraW5nIHNjZW5hcmlvczogbWVkaWFuIE5STVNFIGRpZmZlcmVuY2UgKG5lZ2F0aXZlIGZhdm91cnMgU2Vhcy1BUiksIHBlcmNlbnRhZ2Ugb2Ygc2NlbmFyaW9zIGluIHdoaWNoIFNlYXMtQVIgaXMgbW9yZSBhY2N1cmF0ZSwgYW5kIEhvbG0tYWRqdXN0ZWQgV2lsY294b24gc2lnbmVkLXJhbmsgJHAkLXZhbHVlcy4gRnJpZWRtYW4gdGVzdDogJFxcY2hpXjJfeyVkfT0lLjFmJCwgJHA8MTBeey00fSQufSIsIE5fLCBrXyAtIDEsIGZyJHN0YXRpc3RpYyksCiAgIlxcbGFiZWx7dGFiOnRlc3RzfSIsICJcXHNldGxlbmd0aHtcXHRhYmNvbHNlcH17NXB0fSIsICJcXGJlZ2lue3RhYnVsYXJ9e2xycnJ9IiwgIlxcdG9wcnVsZSIsCiAgIkNvbXBldGl0b3IgJiBNZWRpYW4gJFxcRGVsdGEkTlJNU0UgJiBTZWFzLUFSIHdpbnMgKFxcJSkgJiAkcF97XFxtYXRocm17SG9sbX19JFxcXFwiLCAiXFxtaWRydWxlIiwKICBzcHJpbnRmKCIlcyAmICUrLjRmICYgJS4xZiAmICVzIFxcXFwiLCB0NSRjb21wZXRpdG9yLCB0NSRtZWRpYW5fZGlmZiwgdDUkd2lucywgcHR4dCh0NSRwX2hvbG0pKSwKICAiXFxib3R0b21ydWxlIiwgIlxcZW5ke3RhYnVsYXJ9IiwgIlxcZW5ke3RhYmxlfSIpLCAidGFiX3Rlc3RzIikKZm9yIChpIGluIHNlcV9sZW4obnJvdyh0NSkpKSB7IGtleShwYXN0ZTAoIndpbF9tZWRkaWZmXyIsIHQ1JGNvbXBldGl0b3JbaV0pLCB0NSRtZWRpYW5fZGlmZltpXSk7IGtleShwYXN0ZTAoIndpbF93aW5zXyIsIHQ1JGNvbXBldGl0b3JbaV0pLCB0NSR3aW5zW2ldKSB9CmtuaXRyOjprYWJsZSh0NSB8PiB0cmFuc211dGUoQ29tcGV0aXRvciA9IGNvbXBldGl0b3IsIGBNZWRpYW4gzpROUk1TRWAgPSBtZWRpYW5fZGlmZiwgYFNlYXMtQVIgd2lucyAoJSlgID0gd2lucywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBgcCAoSG9sbSlgID0gaWZlbHNlKHBfaG9sbSA8IDFlLTQsICI8IDFlLTQiLCBmb3JtYXRDKHBfaG9sbSwgZm9ybWF0ID0gImciLCBkaWdpdHMgPSAzKSkpLAogICAgICAgICAgICAgZGlnaXRzID0gNCwgY2FwdGlvbiA9IGNhcCgiVGFibGUgNiIsICJTZWFzLUFSIHZlcnN1cyBlYWNoIGNvbXBldGl0b3IgKFdpbGNveG9uIHNpZ25lZC1yYW5rIHRlc3QsIEhvbG0gY29ycmVjdGlvbikiKSkKYGBgCgojIyBHYXAgbGVuZ3RoLCBtaXNzaW5nIHJhdGUgYW5kIHN0YXRpb24KCmBgYHtyIHJlZ2ltZS1hbmFseXNpc30KcmVnX3RhYiA8LSBtYWluIHw+IGdyb3VwX2J5KHJlZ2ltZSwgbWV0aG9kKSB8PiBzdW1tYXJpc2UobnJtc2UgPSBtZWFuKG5ybXNlKSwgLmdyb3VwcyA9ICJkcm9wIikKcmVsIDwtIGJpbmRfcm93cyhsYXBwbHkoUkVHX05BTUVTLCBmdW5jdGlvbihyKSB7CiAgZyA8LSByZWdfdGFiIHw+IGZpbHRlcihyZWdpbWUgPT0gcik7IGNvbXAgPC0gZyB8PiBmaWx0ZXIobWV0aG9kICE9ICJTZWFzLUFSIikgfD4gc2xpY2VfbWluKG5ybXNlLCBuID0gMSkKICBzZWFzYXIgPC0gZyRucm1zZVtnJG1ldGhvZCA9PSAiU2Vhcy1BUiJdOyBzZWwgPC0gZ3JlcGwocGFzdGUwKCJcXHwiLCByLCAiXFx8IiksIFB3JHNjZW5hcmlvKQogIGRhdGEuZnJhbWUocmVnaW1lID0gciwgYmVzdCA9IGFzLmNoYXJhY3Rlcihjb21wJG1ldGhvZCksIG5ybXNlX2Jlc3QgPSBjb21wJG5ybXNlLCBucm1zZV9zZWFzYXIgPSBzZWFzYXIsCiAgICAgICAgICAgICByZWxfcGN0ID0gMTAwICogKHNlYXNhciAvIGNvbXAkbnJtc2UgLSAxKSwKICAgICAgICAgICAgIGJlYXRzX2thbG1hbiA9IDEwMCAqIG1lYW4oUG1bc2VsLCAiU2Vhcy1BUiJdIDwgUG1bc2VsLCAiS0FMTUFOIl0pLCBiZWF0c19saSA9IDEwMCAqIG1lYW4oUG1bc2VsLCAiU2Vhcy1BUiJdIDwgUG1bc2VsLCAiTEkiXSksCiAgICAgICAgICAgICBiZWF0c19zZWFkZWMgPSAxMDAgKiBtZWFuKFBtW3NlbCwgIlNlYXMtQVIiXSA8IFBtW3NlbCwgIlNFQURFQyJdKSkKfSkpCndyaXRlLmNzdihyZWwsIG91dF9maWxlKCJleHAxX3JlZ2ltZS5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCmZvciAoaSBpbiBzZXFfbGVuKG5yb3cocmVsKSkpIGZvciAoY2MgaW4gbmFtZXMocmVsKVstMV0pIGtleShwYXN0ZTAoInJlZ2ltZV8iLCByZWwkcmVnaW1lW2ldLCAiXyIsIGNjKSwgcmVsW1tjY11dW2ldKQpieV9yYXRlIDwtIG1haW4gfD4gZmlsdGVyKG1ldGhvZCA9PSAiU2Vhcy1BUiIpIHw+IGdyb3VwX2J5KHJhdGUpIHw+IHN1bW1hcmlzZShucm1zZSA9IG1lYW4obnJtc2UpKQprZXkoInNlYXNhcl9ucm1zZV9yYXRlMTAiLCBieV9yYXRlJG5ybXNlW2J5X3JhdGUkcmF0ZSA9PSAwLjFdKTsga2V5KCJzZWFzYXJfbnJtc2VfcmF0ZTMwIiwgYnlfcmF0ZSRucm1zZVtieV9yYXRlJHJhdGUgPT0gMC4zXSkKc3RfcmFuayA8LSBzYXBwbHkoU1RBVElPTlMsIGZ1bmN0aW9uKHN0KSB7IHNlbCA8LSBzdGFydHNXaXRoKFB3JHNjZW5hcmlvLCBwYXN0ZTAoc3QsICJ8IikpCiAgaWYgKCFhbnkoc2VsKSkgcmV0dXJuKHJlcChOQSwgbGVuZ3RoKE1FVEhPRF9PUkRFUikpKTsgY29sTWVhbnModChhcHBseShQbVtzZWwsICwgZHJvcCA9IEZBTFNFXSwgMSwgcmFuaykpKSB9KQpyb3duYW1lcyhzdF9yYW5rKSA8LSBNRVRIT0RfT1JERVIKZm9yIChzdCBpbiBTVEFUSU9OUykga2V5KHBhc3RlMCgic2Vhc2FyX3JhbmtfIiwgZ3N1YigiICIsICIiLCBzdCkpLCBzdF9yYW5rWyJTZWFzLUFSIiwgc3RdKQpzdWYgPC0gdDQgfD4gZmlsdGVyKG1ldGhvZCAhPSAiU2Vhcy1BUiIpCmZvciAocCBpbiBwYykgewogIGIgPC0gc3VmJG1ldGhvZFt3aGljaC5taW4oc3VmW1twXV0pXTsga2V5KHBhc3RlMCgic3VmMV9iZXN0XyIsIHApLCBhcy5jaGFyYWN0ZXIoYikpOyBrZXkocGFzdGUwKCJzdWYxX2Jlc3R2YWxfIiwgcCksIG1pbihzdWZbW3BdXSkpCiAga2V5KHBhc3RlMCgic3VmMV9zZWFzYXJfIiwgcCksIHQ0W1twXV1bdDQkbWV0aG9kID09ICJTZWFzLUFSIl0pOyBrZXkocGFzdGUwKCJzdWYxX3JlbF8iLCBwKSwgMTAwICogKDEgLSB0NFtbcF1dW3Q0JG1ldGhvZCA9PSAiU2Vhcy1BUiJdIC8gbWluKHN1ZltbcF1dKSkpCiAga2V5KHBhc3RlMCgic3VmMV92c19zc21lZGlhbl8iLCBwKSwgMTAwICogKDEgLSB0NFtbcF1dW3Q0JG1ldGhvZCA9PSAiU2Vhcy1BUiJdIC8gdDRbW3BdXVt0NCRtZXRob2QgPT0gIlNTLU1FRElBTiJdKSkKfQprbml0cjo6a2FibGUocmVsLCBkaWdpdHMgPSBjKDAsIDAsIDQsIDQsIDEsIDEsIDEsIDEpLAogIGNvbC5uYW1lcyA9IGMoIkdhcCByZWdpbWUiLCAiQmVzdCBjb21wZXRpdG9yIiwgIk5STVNFIGJlc3QgY29tcGV0aXRvciIsICJOUk1TRSBTZWFzLUFSIiwgIlNlYXMtQVIgdnMgYmVzdCAoJSkiLAogICAgICAgICAgICAgICAgIlNlYXMtQVIgYmVhdHMgS0FMTUFOICglKSIsICJTZWFzLUFSIGJlYXRzIExJICglKSIsICJTZWFzLUFSIGJlYXRzIFNFQURFQyAoJSkiKSwKICBjYXB0aW9uID0gIlNlYXMtQVIgdmVyc3VzIHRoZSBiZXN0IGNvbXBldGl0b3IgaW4gZWFjaCBnYXAgcmVnaW1lIikKa25pdHI6OmthYmxlKGRhdGEuZnJhbWUobWV0aG9kID0gTUVUSE9EX09SREVSLCBzdF9yYW5rLCBjaGVjay5uYW1lcyA9IEZBTFNFKSwgZGlnaXRzID0gMiwgY2FwdGlvbiA9ICJBdmVyYWdlIHJhbmsgcGVyIHN0YXRpb24iKQpgYGAKCiMjIEFibGF0aW9uIHN0dWR5CgpFYWNoIHZhcmlhbnQgcmVtb3ZlcyBvbmUgY29tcG9uZW50IG9mIFNlYXMtQVI6IG5vIGxvZyB0cmFuc2Zvcm0gKCR6PXkvbSQpLCBkYWlseSBjb21wb25lbnQgb25seQooJFdfdFxlcXVpdjAkKSwgc3RhdGljIHByb2ZpbGVzICgkaF9EPWhfVz1caW5mdHkkKSwgbm8gcmVtYWluZGVyIG1vZGVsICgkXGhhdCBSX3Q9MCQpLCBhbmQgbGluZWFyCmludGVycG9sYXRpb24gb2YgdGhlIHJlbWFpbmRlciBpbnN0ZWFkIG9mIHRoZSBBUigxKSBicmlkZ2UuCgpgYGB7ciB0YWJsZTZ9CkFCTF9PUkRFUiA8LSBjKCJTZWFzLUFSIiwgIncvbyBsb2cgdHJhbnNmb3JtIiwgIncvbyB3ZWVrbHkgY29tcC4iLCAic3RhdGljIHByb2ZpbGVzIiwgInJlbWFpbmRlciA9IDAiLCAicmVtYWluZGVyIExJIikKQUJMX1RFWCA8LSBjKGBTZWFzLUFSYCA9ICJGdWxsIFNlYXMtQVIiLCBgdy9vIGxvZyB0cmFuc2Zvcm1gID0gIm5vIGxvZyB0cmFuc2Zvcm0gKCR6PXkvbSQpIiwKICAgICAgICAgICAgIGB3L28gd2Vla2x5IGNvbXAuYCA9ICJkYWlseSBjb21wb25lbnQgb25seSAoJFdfdFxcZXF1aXYwJCkiLCBgc3RhdGljIHByb2ZpbGVzYCA9ICJzdGF0aWMgcHJvZmlsZXMgKCRoX0Q9aF9XPVxcaW5mdHkkKSIsCiAgICAgICAgICAgICBgcmVtYWluZGVyID0gMGAgPSAibm8gcmVtYWluZGVyIG1vZGVsICgkXFxoYXQgUl90PTAkKSIsIGByZW1haW5kZXIgTElgID0gInJlbWFpbmRlciBieSBsaW5lYXIgaW50ZXJwb2xhdGlvbiIpCmFiIDwtIHJlczEgfD4gZmlsdGVyKGtpbmQgPT0gImFibGF0aW9uIiB8IG1ldGhvZCA9PSAiU2Vhcy1BUiIpCnQ2IDwtIGFiIHw+IGdyb3VwX2J5KG1ldGhvZCwgcmVnaW1lKSB8PiBzdW1tYXJpc2UodiA9IG1lYW4obnJtc2UpLCAuZ3JvdXBzID0gImRyb3AiKSB8PgogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSByZWdpbWUsIHZhbHVlc19mcm9tID0gdikgfD4KICBsZWZ0X2pvaW4oYWIgfD4gZ3JvdXBfYnkobWV0aG9kKSB8PiBzdW1tYXJpc2UoQWxsID0gbWVhbihucm1zZSkpLCBieSA9ICJtZXRob2QiKSB8PgogIG11dGF0ZShtZXRob2QgPSBmYWN0b3IobWV0aG9kLCBsZXZlbHMgPSBBQkxfT1JERVIpKSB8PiBhcnJhbmdlKG1ldGhvZCkKd3JpdGUuY3N2KHQ2LCBvdXRfZmlsZSgiZXhwMV9hYmxhdGlvbi5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCmNjNiA8LSBjKGludGVyc2VjdChSRUdfTkFNRVMsIG5hbWVzKHQ2KSksICJBbGwiKQpmb3IgKGkgaW4gc2VxX2xlbihucm93KHQ2KSkpIGZvciAoY2MgaW4gY2M2KSBrZXkocGFzdGUwKCJhYmxfIiwgZ3N1YigiW15BLVphLXowLTldIiwgIiIsIGFzLmNoYXJhY3Rlcih0NiRtZXRob2RbaV0pKSwgIl8iLCBjYyksIHQ2W1tjY11dW2ldKQp0eDYgPC0gbGFwcGx5KGNjNiwgZnVuY3Rpb24oY2MpIGZtdF90ZXgodDZbW2NjXV0sIDQpKQp3cml0ZV90ZXgoYygiXFxiZWdpbnt0YWJsZX1bIXRdIiwgIlxcY2VudGVyaW5nIiwKICAiXFxjYXB0aW9ue0FibGF0aW9uIHN0dWR5OiBtZWFuIE5STVNFIG92ZXIgdGhlIDE1IHNlcmllcyAodGhyZWUgc3RhdGlvbnMgJFxcdGltZXMkIGZpdmUgcG9sbHV0YW50cykgYnkgZ2FwIHJlZ2ltZS4gQmVzdCB2YWx1ZXMgaW4gYm9sZC59IiwKICAiXFxsYWJlbHt0YWI6YWJsYXRpb259IiwgIlxcc2V0bGVuZ3Roe1xcdGFiY29sc2VwfXszLjVwdH0iLCAiXFxiZWdpbnt0YWJ1bGFyfXtscnJycn0iLCAiXFx0b3BydWxlIiwKICAiVmFyaWFudCAmIFNob3J0ICYgTWVkaXVtICYgTG9uZyAmIEFsbFxcXFwiLCAiXFxtaWRydWxlIiwKICBzYXBwbHkoc2VxX2xlbihucm93KHQ2KSksIGZ1bmN0aW9uKGkpIHBhc3RlMChwYXN0ZShjKEFCTF9URVhbW2FzLmNoYXJhY3Rlcih0NiRtZXRob2RbaV0pXV0sIHNhcHBseSh0eDYsIGBbYCwgaSkpLCBjb2xsYXBzZSA9ICIgJiAiKSwgIiBcXFxcIikpLAogICJcXGJvdHRvbXJ1bGUiLCAiXFxlbmR7dGFidWxhcn0iLCAiXFxlbmR7dGFibGV9IiksICJ0YWJfYWJsYXRpb24iKQptZDYgPC0gZGF0YS5mcmFtZShWYXJpYW50ID0gYXMuY2hhcmFjdGVyKHQ2JG1ldGhvZCksIGNoZWNrLm5hbWVzID0gRkFMU0UpCmZvciAoY2MgaW4gY2M2KSBtZDZbW2NjXV0gPC0gZm10X21kKHQ2W1tjY11dLCA0KQprbml0cjo6a2FibGUobWQ2LCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9IGNhcCgiVGFibGUgNyIsICJBYmxhdGlvbiBzdHVkeSAobWVhbiBOUk1TRSkiKSkKYGBgCgojIyBBZGRpdGlvbmFsIGNoZWNrOiB0aGUgb3JpZ2luYWwgYGltcHV0ZVRTOjpuYV9zZWFkZWNgCgpgYGB7ciBuYS1zZWFkZWN9CmlmIChhbnkocmVzMSRraW5kID09ICJleHRyYSIpKSB7CiAgZXggPC0gcmVzMSB8PiBmaWx0ZXIoa2luZCA9PSAiZXh0cmEiKSB8PiBtdXRhdGUoc2NlbmFyaW8gPSBwYXN0ZShzdGF0aW9uLCBwb2xsdXRhbnQsIHJlZ2ltZSwgcmF0ZSwgcmVwLCBzZXAgPSAifCIpKQogIGpqIDwtIGV4IHw+IHNlbGVjdChzY2VuYXJpbywgZXggPSBucm1zZSkgfD4KICAgIGlubmVyX2pvaW4obWFpbiB8PiBmaWx0ZXIobWV0aG9kID09ICJTZWFzLUFSIikgfD4gc2VsZWN0KHNjZW5hcmlvLCBzZWFzYXIgPSBucm1zZSksIGJ5ID0gInNjZW5hcmlvIikgfD4KICAgIGlubmVyX2pvaW4obWFpbiB8PiBmaWx0ZXIobWV0aG9kID09ICJTRUFERUMiKSB8PiBzZWxlY3Qoc2NlbmFyaW8sIHNlYWRlYyA9IG5ybXNlKSwgYnkgPSAic2NlbmFyaW8iKQogIHd4IDwtIHdpbGNveC50ZXN0KGpqJHNlYXNhciwgamokZXgsIHBhaXJlZCA9IFRSVUUsIGV4YWN0ID0gRkFMU0UsIGNvcnJlY3QgPSBGQUxTRSkKICBrZXkoIm5hc2VhZGVjX25ybXNlIiwgbWVhbihqaiRleCkpOyBrZXkoInNlYWRlY19ucm1zZSIsIG1lYW4oamokc2VhZGVjKSk7IGtleSgic2Vhc2FyX25ybXNlIiwgbWVhbihqaiRzZWFzYXIpKQogIGtleSgibmFzZWFkZWNfc2Vhc2FyX3dpbnMiLCAxMDAgKiBtZWFuKGpqJHNlYXNhciA8IGpqJGV4KSk7IGtleSgibmFzZWFkZWNfcCIsIHd4JHAudmFsdWUpCiAgY2F0KHNwcmludGYoIk1lYW4gTlJNU0U6IG5hX3NlYWRlYyAocm9idXN0IFNUTCkgPSAlLjRmIHwgU0VBREVDIChjbGFzc2ljYWwpID0gJS40ZiB8IFNlYXMtQVIgPSAlLjRmXG4iLCBtZWFuKGpqJGV4KSwgbWVhbihqaiRzZWFkZWMpLCBtZWFuKGpqJHNlYXNhcikpKQogIGNhdChzcHJpbnRmKCJTZWFzLUFSIGlzIG1vcmUgYWNjdXJhdGUgdGhhbiBuYV9zZWFkZWMgaW4gJS4xZiUlIG9mIHRoZSBzY2VuYXJpb3MgKFdpbGNveG9uIHAgPSAlLjJnKVxuIiwgMTAwICogbWVhbihqaiRzZWFzYXIgPCBqaiRleCksIHd4JHAudmFsdWUpKQp9IGVsc2UgY2F0KCJpbXB1dGVUUyBpcyBub3QgYXZhaWxhYmxlOyBjaGVjayBza2lwcGVkLlxuIikKYGBgCgojIyBJbGx1c3RyYXRpb24gb2YgYSByZW1vdmVkIGJsb2NrCgpBIGZvdXItZGF5IE5PJF8yJCBibG9jayBpcyBjaG9zZW4gYXMgdGhlIGJsb2NrIHN0YXJ0aW5nIGF0IDAwOjMwLCBiZXR3ZWVuIDcgTWF5IGFuZCB0aGUgZW5kIG9mIE9jdG9iZXIsCndpdGggdGhlIGZld2VzdCBtaXNzaW5nIHZhbHVlcy4gSXQgaXMgcmVtb3ZlZCBhbmQgaW1wdXRlZCBieSBmaXZlIG1ldGhvZHMuIEVhY2ggcGFuZWwgc2hvd3MgdGhlIHRydWUKc2VyaWVzIChncmV5KSBhbmQgb25lIGltcHV0YXRpb24gKGNvbG91cikgaW5zaWRlIHRoZSBzaGFkZWQgZ2FwLCB3aXRoIHRoZSBSTVNFIG92ZXIgdGhlIGJsb2NrLgoKYGBge3IgZmlnNywgZmlnLndpZHRoPTcuMTYsIGZpZy5oZWlnaHQ9NC4yfQp4NyA8LSBnZXRfc2VyaWVzKCJTVUYgMSIsICJOTzIiKTsgTDcgPC0gNDggKiA0OyBjdHggPC0gNDggKiAyCmkwIDwtIHdoaWNoKFRTID49IGFzLlBPU0lYY3QoIjIwMTgtMDUtMDcgMDA6MzAiLCB0eiA9ICJVVEMiKSlbMV0gLSAxICAgICAgICAgICAgICMgMC1iYXNlZCBzdGFydApjYW5kIDwtIHNlcShpMCwgMzA0ICogNDggLSBMNyAtIGN0eCAtIDEsIGJ5ID0gNDgpCmkwIDwtIGNhbmRbd2hpY2gubWluKHNhcHBseShjYW5kLCBmdW5jdGlvbihrKSBzdW0oaXMubmEoeDdbKGsgKyAxKTooayArIEw3KV0pKSkpXQpibGsgPC0gKGkwICsgMSk6KGkwICsgTDcpOyB4bTcgPC0geDc7IHhtN1tibGtdIDwtIE5BOyBva2IgPC0gIWlzLm5hKHg3W2Jsa10pOyBzbCA8LSAoaTAgKyAxIC0gY3R4KTooaTAgKyBMNyArIGN0eCkKTTcgPC0gYygiTEkiLCAiS0FMTUFOIiwgIlNFQURFQyIsICJNU1RMLUxJIiwgIlNlYXMtQVIiKQpmNyA8LSBjKE1FVEhPRFNbYygiTEkiLCAiS0FMTUFOIiwgIlNFQURFQyIsICJNU1RMLUxJIildLAogICAgICAgIGxpc3QoYFNlYXMtQVJgID0gZnVuY3Rpb24odikgaW1wdXRlX3NlYXNhcih2LCBCV1tbIlNVRiAxIl1dJGJ3X2RheSwgQldbWyJTVUYgMSJdXSRid193ZWVrKSkpCmltcDcgPC0gbGFwcGx5KGY3LCBmdW5jdGlvbihmKSBmKHhtNykpOyBybTcgPC0gc2FwcGx5KGltcDcsIGZ1bmN0aW9uKHkpIHJtc2UoeVtibGtdW29rYl0sIHg3W2Jsa11bb2tiXSkpCmZvciAobW0gaW4gTTcpIGtleShwYXN0ZTAoImV4YW1wbGVfcm1zZV8iLCBtbSksIHJtN1tbbW1dXSkKbGFiNyA8LSBzZXROYW1lcyhzcHJpbnRmKCIlcyAoUk1TRSAlLjFmKSIsIE03LCBybTdbTTddKSwgTTcpCmQ3IDwtIGJpbmRfcm93cyhsYXBwbHkoTTcsIGZ1bmN0aW9uKG1tKSBkYXRhLmZyYW1lKG1ldGhvZCA9IG1tLCB0ID0gVFNbc2xdLCB5ID0gaWZlbHNlKHNsICVpbiUgYmxrLCBpbXA3W1ttbV1dW3NsXSwgTkEpKSkpIHw+CiAgbXV0YXRlKHBhbmVsID0gZmFjdG9yKGxhYjdbbWV0aG9kXSwgbGV2ZWxzID0gbGFiNykpCnRydXRoNyA8LSB0aWR5cjo6Y3Jvc3NpbmcocGFuZWwgPSBmYWN0b3IobGFiNywgbGV2ZWxzID0gbGFiNyksIGRhdGEuZnJhbWUodCA9IFRTW3NsXSwgeSA9IHg3W3NsXSkpCnA3IDwtIGdncGxvdCgpICsgYW5ub3RhdGUoInJlY3QiLCB4bWluID0gVFNbbWluKGJsayldLCB4bWF4ID0gVFNbbWF4KGJsayldLCB5bWluID0gLUluZiwgeW1heCA9IEluZiwgZmlsbCA9ICJncmV5OTMiKSArCiAgZ2VvbV9saW5lKGRhdGEgPSB0cnV0aDcsIGFlcyh0LCB5KSwgY29sb3VyID0gImdyZXkzMCIsIGxpbmV3aWR0aCA9IDAuMywgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV9saW5lKGRhdGEgPSBkNywgYWVzKHQsIHksIGNvbG91ciA9IG1ldGhvZCksIGxpbmV3aWR0aCA9IDAuNiwgbmEucm0gPSBUUlVFKSArCiAgZmFjZXRfd3JhcCh+cGFuZWwsIG5jb2wgPSAxKSArIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gQ09MLCBndWlkZSA9ICJub25lIikgKwogIHNjYWxlX3hfZGF0ZXRpbWUoZGF0ZV9icmVha3MgPSAiMSBkYXkiLCBkYXRlX2xhYmVscyA9ICIlZCAlYiIpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gZXhwcmVzc2lvbihOT1syXSB+ICJbXHUwMGI1Zy9tIl4zICogIl0iKSkKcHJpbnQocDcpOyBzYXZlX2ZpZyhwNywgImZpZzA3X2V4YW1wbGVfaW1wdXRhdGlvbiIsIDcuMTYsIDQuMikKa2V5KCJleGFtcGxlX2Jsb2NrIiwgcGFzdGUoZm9ybWF0KFRTW21pbihibGspXSwgIiVkICViIiksICItIiwgZm9ybWF0KFRTW21heChibGspXSwgIiVkICViIikpKQpgYGAKCiMjIFNlYXMtQVIgZGVjb21wb3NpdGlvbgoKRm91ciB3ZWVrcyBvZiBsb2ctc2NhbGVkIE5PJF8yJCBhdCBTVUYgMSBmcm9tIDYgQXVndXN0IDIwMTgsIHNwbGl0IGludG8gaXRzIGZvdXIgY29tcG9uZW50cy4gR2FwcyBsb25nZXIgdGhhbgp0d28gaG91cnMgYXJlIHNoYWRlZCByZWQ7IGluc2lkZSB0aGVtIHRoZSBpbXB1dGVkICR6X3QkIGFuZCB0aGUgQVIoMSkgYnJpZGdlIGZvciAkUl90JCBhcmUgZHJhd24gaW4gcmVkIHdpdGggYQpwb2ludHdpc2UgOTUlIGJhbmQgZnJvbSB0aGUgY2xvc2VkLWZvcm0gY29uZGl0aW9uYWwgdmFyaWFuY2Ugb2YgdGhlIGJyaWRnZS4gV2Vla2VuZHMgYXJlIHNoYWRlZCBncmV5LCB3aGljaApzaG93cyB3aGVyZSB0aGUgd2Vla2x5IGRldmlhdGlvbiAkV190JCBkZXBhcnRzIGZyb20gemVyby4KCmBgYHtyIGZpZzgsIGZpZy53aWR0aD0zLjUsIGZpZy5oZWlnaHQ9NC42LCBvdXQud2lkdGg9JzYwJSd9CiMgU2Vhcy1BUiBkZWNvbXBvc2l0aW9uIG9mIHRoZSBsb2ctc2NhbGVkIE5PMiBzZXJpZXMgb2YgU1VGIDEgb3ZlciBmb3VyIHdlZWtzLiBHYXBzIGxvbmdlciB0aGFuIHR3byBob3VycwojIGFyZSBzaGFkZWQgcmVkOyB0aGUgaW1wdXRlZCB6X3QgYW5kIHRoZSBBUigxKSBicmlkZ2UgZm9yIFJfdCBhcmUgZHJhd24gaW4gcmVkIHdpdGggYSBwb2ludHdpc2UgOTUlIGJhbmQgZnJvbSB0aGUKIyBjbG9zZWQtZm9ybSBjb25kaXRpb25hbCB2YXJpYW5jZSBvZiB0aGUgYnJpZGdlOyB3ZWVrZW5kcyBhcmUgc2hhZGVkIGdyZXkuCmRlYyA8LSBpbXB1dGVfc2Vhc2FyKGdldF9zZXJpZXMoIlNVRiAxIiwgIk5PMiIpLCBCV1tbIlNVRiAxIl1dJGJ3X2RheSwgQldbWyJTVUYgMSJdXSRid193ZWVrLCByZXR1cm5fY29tcG9uZW50cyA9IFRSVUUpCndpbiA8LSB3aGljaChUUyA+PSBhcy5QT1NJWGN0KCIyMDE4LTA4LTA2IDAwOjMwIiwgdHogPSAiVVRDIikpWzFdICsgMDooNDggKiAyOCAtIDEpCm1pc3MgPC0gaXMubmEoZGVjJHpbd2luXSk7IHpmaWxsIDwtIChkZWMkY29tcCR0cmVuZCArIGRlYyRjb21wJGRhaWx5ICsgZGVjJGNvbXAkd2Vla2x5ICsgZGVjJHJlbWFpbmRlcl9pbXApW3dpbl0KIyBjb25kaXRpb25hbCBzdGFuZGFyZCBkZXZpYXRpb24gb2YgdGhlIGJyaWRnZTogZ2FtbWEwICgxIC0gcGhpXjJrKSgxIC0gcGhpXjIobi1rKSkgLyAoMSAtIHBoaV4ybikKciA8LSBkZWMkY29tcCRyZW1haW5kZXI7IG4gPC0gbGVuZ3RoKHIpOyBwciA8LSAhaXMubmEoclstMV0pICYgIWlzLm5hKHJbLW5dKQpwaGkgPC0gbWluKDAuOTk5LCBtYXgoMCwgc3VtKHJbLTFdW3ByXSAqIHJbLW5dW3ByXSkgLyBzdW0oclstbl1bcHJdXjIpKSkKZzAgPC0gbWVhbigoclstMV1bcHJdIC0gcGhpICogclstbl1bcHJdKV4yKSAvICgxIC0gcGhpXjIpCnNkX2IgPC0gcmVwKE5BX3JlYWxfLCBuKTsgcnIgPC0gcmxlKGlzLm5hKHIpKTsgZSA8LSBjdW1zdW0ocnIkbGVuZ3Rocyk7IHMgPC0gZSAtIHJyJGxlbmd0aHMgKyAxCmZvciAoayBpbiB3aGljaChyciR2YWx1ZXMpKSB7CiAgYSA8LSBzW2tdIC0gMTsgYiA8LSBlW2tdICsgMTsgaSA8LSBzW2tdOmVba10KICBzZF9iW2ldIDwtIGlmIChhID49IDEgJiYgYiA8PSBuKSBzcXJ0KGcwICogKDEgLSBwaGleKDIgKiAoaSAtIGEpKSkgKiAoMSAtIHBoaV4oMiAqIChiIC0gaSkpKSAvICgxIC0gcGhpXigyICogKGIgLSBhKSkpKSBlbHNlCiAgICAgICAgICAgICBpZiAoYSA+PSAxKSBzcXJ0KGcwICogKDEgLSBwaGleKDIgKiAoaSAtIGEpKSkpIGVsc2Ugc3FydChnMCAqICgxIC0gcGhpXigyICogKGIgLSBpKSkpKQp9CkxBQjggPC0gYyh6ID0gImF0b3Aoelt0XSwgJ2xvZyBzY2FsZScpIiwgVCA9ICJhdG9wKFRbdF0sICd0cmVuZCcpIiwgRCA9ICJhdG9wKERbdF0sICdkYWlseScpIiwKICAgICAgICAgIFcgPSAiYXRvcChXW3RdLCAnd2Vla2x5JykiLCBSID0gImF0b3AoUlt0XSwgJ3JlbWFpbmRlcicpIikKQ09MOCA8LSBzZXROYW1lcyhjKCIjMmMzZTUwIiwgIiMxZjYxOGQiLCAiIzE0OGY3NyIsICIjY2E2ZjFlIiwgIiM3ZDNjOTgiKSwgTEFCOCkKcGYgPC0gZnVuY3Rpb24oaykgZmFjdG9yKHVubmFtZShMQUI4W2tdKSwgbGV2ZWxzID0gTEFCOCkKZF9vYnMgPC0gYmluZF9yb3dzKGRhdGEuZnJhbWUocGFuZWwgPSBwZigieiIpLCB0ID0gVFNbd2luXSwgdiA9IGRlYyR6W3dpbl0pLAogICAgICAgICAgICAgICAgICAgZGF0YS5mcmFtZShwYW5lbCA9IHBmKCJUIiksIHQgPSBUU1t3aW5dLCB2ID0gZGVjJGNvbXAkdHJlbmRbd2luXSksCiAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKHBhbmVsID0gcGYoIkQiKSwgdCA9IFRTW3dpbl0sIHYgPSBkZWMkY29tcCRkYWlseVt3aW5dKSwKICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUocGFuZWwgPSBwZigiVyIpLCB0ID0gVFNbd2luXSwgdiA9IGRlYyRjb21wJHdlZWtseVt3aW5dKSwKICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUocGFuZWwgPSBwZigiUiIpLCB0ID0gVFNbd2luXSwgdiA9IGRlYyRjb21wJHJlbWFpbmRlclt3aW5dKSkKZF9pbXAgPC0gYmluZF9yb3dzKGRhdGEuZnJhbWUocGFuZWwgPSBwZigieiIpLCB0ID0gVFNbd2luXSwgdiA9IGlmZWxzZShtaXNzLCB6ZmlsbCwgTkEpLCBzZCA9IHNkX2Jbd2luXSksCiAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKHBhbmVsID0gcGYoIlIiKSwgdCA9IFRTW3dpbl0sIHYgPSBpZmVsc2UobWlzcywgZGVjJHJlbWFpbmRlcl9pbXBbd2luXSwgTkEpLCBzZCA9IHNkX2Jbd2luXSkpIHw+CiAgbXV0YXRlKGxvID0gdiAtIDEuOTYgKiBzZCwgaGkgPSB2ICsgMS45NiAqIHNkKQpydyA8LSBybGUobWlzcyk7IGV3IDwtIGN1bXN1bShydyRsZW5ndGhzKTsgc3cgPC0gZXcgLSBydyRsZW5ndGhzICsgMTsga3cgPC0gd2hpY2gocnckdmFsdWVzKQpnYXBzOCA8LSBkYXRhLmZyYW1lKHhtaW4gPSBUU1t3aW5dW3N3W2t3XV0gLSA5MDAsIHhtYXggPSBUU1t3aW5dW2V3W2t3XV0gKyA5MDAsIGxlbiA9IHJ3JGxlbmd0aHNba3ddKQpnbCA8LSBnYXBzOFt3aGljaC5tYXgoZ2FwczgkbGVuKSwgXQpnbF9sYWIgPC0gZGF0YS5mcmFtZShwYW5lbCA9IHBmKCJ6IiksIHQgPSBnbCR4bWluICsgKGdsJHhtYXggLSBnbCR4bWluKSAvIDIsCiAgICAgICAgICAgICAgICAgICAgIGxhYiA9IGlmIChnbCRsZW4gPj0gNDgpIHNwcmludGYoIiUuMWYtZGF5IGdhcCIsIGdsJGxlbiAvIDQ4KSBlbHNlIHNwcmludGYoIiVnLWggZ2FwIiwgZ2wkbGVuIC8gMikpCmRkIDwtIHNlcShhcy5EYXRlKG1pbihUU1t3aW5dKSksIGFzLkRhdGUobWF4KFRTW3dpbl0pKSwgYnkgPSAiZGF5Iik7IHNhdCA8LSBkZFtmb3JtYXQoZGQsICIldSIpID09ICI2Il0Kd2tlbmQgPC0gZGF0YS5mcmFtZSh4bWluID0gcG1heChhcy5QT1NJWGN0KHBhc3RlKHNhdCwgIjAwOjAwIiksIHR6ID0gIlVUQyIpLCBtaW4oVFNbd2luXSkpLAogICAgICAgICAgICAgICAgICAgIHhtYXggPSBwbWluKGFzLlBPU0lYY3QocGFzdGUoc2F0ICsgMiwgIjAwOjAwIiksIHR6ID0gIlVUQyIpLCBtYXgoVFNbd2luXSkpKQpwOCA8LSBnZ3Bsb3QoKSArCiAgZ2VvbV9yZWN0KGRhdGEgPSB3a2VuZCwgYWVzKHhtaW4gPSB4bWluLCB4bWF4ID0geG1heCwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYpLCBmaWxsID0gImdyZXk5MyIpICsKICBnZW9tX3JlY3QoZGF0YSA9IGdhcHM4W2dhcHM4JGxlbiA+IDQsIF0sIGFlcyh4bWluID0geG1pbiwgeG1heCA9IHhtYXgsIHltaW4gPSAtSW5mLCB5bWF4ID0gSW5mKSwgZmlsbCA9ICIjZmFkYmQ4IikgKwogIGdlb21faGxpbmUoZGF0YSA9IGRhdGEuZnJhbWUocGFuZWwgPSBwZihjKCJEIiwgIlciLCAiUiIpKSwgeSA9IDApLCBhZXMoeWludGVyY2VwdCA9IHkpLCBjb2xvdXIgPSAiZ3JleTUwIiwKICAgICAgICAgICAgIGxpbmV3aWR0aCA9IDAuMiwgbGluZXR5cGUgPSAiMjIiKSArCiAgZ2VvbV9yaWJib24oZGF0YSA9IGZpbHRlcihkX29icywgcGFuZWwgJWluJSBwZihjKCJEIiwgIlciKSkpLCBhZXModCwgeW1pbiA9IHBtaW4odiwgMCksIHltYXggPSBwbWF4KHYsIDApLCBmaWxsID0gcGFuZWwpLAogICAgICAgICAgICAgIGFscGhhID0gMC4yNSwgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV9yaWJib24oZGF0YSA9IGRfaW1wLCBhZXModCwgeW1pbiA9IGxvLCB5bWF4ID0gaGkpLCBmaWxsID0gIiNlNzRjM2MiLCBhbHBoYSA9IDAuMjUpICsgICAjIE5BIHJvd3Mgc3BsaXQgdGhlIGJhbmQKICBnZW9tX2xpbmUoZGF0YSA9IGRfb2JzLCBhZXModCwgdiwgY29sb3VyID0gcGFuZWwsIGxpbmV3aWR0aCA9IHBhbmVsID09IExBQjhbWyJUIl1dKSwgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV9saW5lKGRhdGEgPSBkX2ltcCwgYWVzKHQsIHYpLCBjb2xvdXIgPSAiI2MwMzkyYiIsIGxpbmV3aWR0aCA9IDAuNSwgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV90ZXh0KGRhdGEgPSBnbF9sYWIsIGFlcyh0LCBJbmYsIGxhYmVsID0gbGFiKSwgdmp1c3QgPSAxLjUsIHNpemUgPSAyLjMsIGZhbWlseSA9IEZPTlRfVEVYVCwgY29sb3VyID0gIiM5MjJiMjEiKSArCiAgZmFjZXRfZ3JpZChwYW5lbCB+IC4sIHNjYWxlcyA9ICJmcmVlX3kiLCBzd2l0Y2ggPSAieSIsIGxhYmVsbGVyID0gbGFiZWxfcGFyc2VkKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBDT0w4LCBndWlkZSA9ICJub25lIikgKyBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBDT0w4LCBndWlkZSA9ICJub25lIikgKwogIHNjYWxlX2xpbmV3aWR0aF9tYW51YWwodmFsdWVzID0gYyhgRkFMU0VgID0gMC4zLCBgVFJVRWAgPSAwLjcpLCBndWlkZSA9ICJub25lIikgKwogIHNjYWxlX3hfZGF0ZXRpbWUoYnJlYWtzID0gYXMuUE9TSVhjdChjKCIyMDE4LTA4LTEzIiwgIjIwMTgtMDgtMjAiLCAiMjAxOC0wOC0yNyIpLCB0eiA9ICJVVEMiKSwgZGF0ZV9sYWJlbHMgPSAiJWQgJWIiLAogICAgICAgICAgICAgICAgICAgZXhwYW5kID0gYygwLCAwKSkgKwogIHNjYWxlX3lfY29udGludW91cyhuLmJyZWFrcyA9IDMsIGV4cGFuZCA9IGV4cGFuc2lvbihtdWx0ID0gYygwLjA2LCAwLjE0KSkpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgdGl0bGUgPSBleHByZXNzaW9uKHpbdF0gPT0gVFt0XSArIERbdF0gKyBXW3RdICsgUlt0XSksCiAgICAgICBzdWJ0aXRsZSA9ICJyZWQ6IGdhcHMgaW1wdXRlZCBieSBTZWFzLUFSLCB3aXRoIDk1JSBiYW5kOyBncmV5OiB3ZWVrZW5kcyIpICsKICB0aGVtZShzdHJpcC5wbGFjZW1lbnQgPSAib3V0c2lkZSIsIHN0cmlwLnRleHQueS5sZWZ0ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCwgc2l6ZSA9IDcuNSksCiAgICAgICAgcGFuZWwuZ3JpZC5taW5vciA9IGVsZW1lbnRfYmxhbmsoKSwgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKGxpbmV3aWR0aCA9IDAuMTUsIGNvbG91ciA9ICJncmV5ODgiKSwKICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2xpbmUobGluZXdpZHRoID0gMC4yLCBjb2xvdXIgPSAiZ3JleTgyIiksIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoNSwgInB0IiksCiAgICAgICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gOSwgaGp1c3QgPSAwLjUpLCBwbG90LnN1YnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LCBoanVzdCA9IDAuNSwgY29sb3VyID0gImdyZXkzMCIpLAogICAgICAgIGF4aXMudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gNi44KSwgcGxvdC5tYXJnaW4gPSBtYXJnaW4oNCwgOCwgNCwgMikpCnByaW50KHA4KTsgc2F2ZV9maWcocDgsICJmaWcwOF9zZWFzYXJfY29tcG9uZW50cyIsIDMuNSwgNC42KQpgYGAKCiMjIENvbXB1dGluZyB0aW1lCgpgYGB7ciB0aW1pbmd9CmZvciAobW0gaW4gTUVUSE9EX09SREVSKSBrZXkocGFzdGUwKCJzZWNfIiwgbW0pLCB0NCRzZWNbdDQkbWV0aG9kID09IG1tXSkKa25pdHI6OmthYmxlKHQ0IHw+IHRyYW5zbXV0ZShtZXRob2QsIHNlY29uZHMgPSBzZWMpLCBkaWdpdHMgPSAzLCBjYXB0aW9uID0gIk1lYW4gY29tcHV0aW5nIHRpbWUgcGVyIGltcHV0YXRpb24gb2YgYSBmdWxsIHNlcmllcyAoUiwgb25lIENQVSBjb3JlKSIpCmBgYAoKIyBGb3JlY2FzdGluZyBtb2RlbHMgYW5kIHByb3RvY29sCgojIyBEYXRhIHNwbGl0LCBvcmlnaW5zIGFuZCBjYXVzYWwgaW1wdXRhdGlvbgoKQW4gb3JpZ2luICRcdGF1JCBpcyB0aGUgbnVtYmVyIG9mIG9ic2VydmF0aW9ucyBhdmFpbGFibGUgd2hlbiB0aGUgZm9yZWNhc3QgaXMgaXNzdWVkIChhdCBtaWRuaWdodCk7CnRoZSB0YXJnZXRzIGFyZSAkeV97XHRhdSsxfSxcZG90cyx5X3tcdGF1KzQ4fSQgKGRheS1haGVhZCwgaG9yaXpvbnMgJGg9MSxcZG90cyw0OCQpLgoKKiAqKlRlc3Q6KiogMeKAkzIwIERlY2VtYmVyIDIwMTgsICRcdGF1X2o9MTZ7LH0wMzIrNDgoai0xKSQsICRqPTEsXGRvdHMsMjAkICg5NjAgdGFyZ2V0IHZhbHVlcykuCiogKipWYWxpZGF0aW9uOioqIG9yaWdpbnMgaW4gTm92ZW1iZXIgKCQxNHssfTU5MlxsZVx0YXVcbGUxNXssfTk4NCQsIGV2ZXJ5IDIgc3RlcHMpIGZvciBlYXJseSBzdG9wcGluZzsKICB0aGUgbG9zcyB1c2VzICoqb2JzZXJ2ZWQgdGFyZ2V0cyBvbmx5KiouCiogKipUcmFpbmluZzoqKiBvcmlnaW5zIGZyb20gMTUgSmFudWFyeSB0byAzMSBPY3RvYmVyICgkNjcyXGxlXHRhdVxsZTE0eyx9NTQ0JCwgZXZlcnkgNiBzdGVwcywgMjMxMyBvcmlnaW5zKS4KClRocmVlIHJ1bGVzIHByZXZlbnQgbGVha2FnZTogKDEpIHRoZSBoaXN0b3J5IHVzZWQgZm9yIHRyYWluaW5nIGlzIGltcHV0ZWQgd2l0aCB0aGUgZGF0YSBiZWZvcmUKMSBEZWNlbWJlciBvbmx5LCAkXHRpbGRlIHk9XG1hdGhybXtJbXB9KHlfezE6MTYwMzJ9KSQ7ICgyKSBhdCBldmVyeSB0ZXN0IG9yaWdpbiB0aGUgaW1wdXRhdGlvbiBpcyByZS1ydW4KY2F1c2FsbHksICRcdGlsZGUgeV57KFx0YXUpfT1cbWF0aHJte0ltcH0oeV97MTpcdGF1fSkkOyAoMykgdmFsaWRhdGlvbiBhbmQgdGVzdCBlcnJvcnMgYXJlIGNvbXB1dGVkIG9uCmdlbnVpbmVseSBvYnNlcnZlZCB2YWx1ZXMgb25seSwgd2hpbGUgdGhlIHRyYWluaW5nIHRhcmdldHMgYXJlIHRoZSBpbXB1dGVkIHNlcmllcy4KCmBgYHtyIGZvcmVjYXN0LXNldHVwfQpIIDwtIDQ4TDsgTF9TRVEgPC0gNDhMOyBURVNUX1NUQVJUIDwtIDMzNEwgKiA0OEw7IFZBTF9TVEFSVCA8LSAzMDRMICogNDhMCk1JTl9PUklHSU4gPC0gMkwgKiBTMjsgTl9URVNUX0RBWVMgPC0gMjBMCnRlc3Rfb3JpZ2lucyA8LSBURVNUX1NUQVJUICsgNDhMICogKDA6KE5fVEVTVF9EQVlTIC0gMSkpCnRyX29yaWcgPC0gc2VxKE1JTl9PUklHSU4sIFZBTF9TVEFSVCAtIEgsIGJ5ID0gNikKdmFfb3JpZyA8LSBzZXEoVkFMX1NUQVJULCBURVNUX1NUQVJUIC0gSCwgYnkgPSAyKQpob2QgPC0gZnVuY3Rpb24oaikgaiAlJSA0OEwgICAgICAgICAgICAgICAgICAgICAgIyBqID0gMC1iYXNlZCBpbmRleDsgMCA9IDAwOjMwCmRvdyA8LSBmdW5jdGlvbihqKSAoaiAlLyUgNDhMKSAlJSA3TCAgICAgICAgICAgICAjIDAgPSBNb25kYXkKY2F0KCJ0cmFpbmluZyBvcmlnaW5zOiIsIGxlbmd0aCh0cl9vcmlnKSwgInwgdmFsaWRhdGlvbiBvcmlnaW5zOiIsIGxlbmd0aCh2YV9vcmlnKSwgInwgdGVzdCBvcmlnaW5zOiIsIGxlbmd0aCh0ZXN0X29yaWdpbnMpLAogICAgIigiLCBmb3JtYXQoVFNbVEVTVF9TVEFSVCArIDFdKSwgInRvIiwgZm9ybWF0KFRTW1RFU1RfU1RBUlQgKyA5NjBdKSwgIilcbiIpCmBgYAoKIyMgU2Vhc29uYWwgbmFpdmUgYW5kIFRTUiAoRXEuIDEwKQoKKipTZWFzb25hbCBuYWl2ZToqKiAkXGhhdCB5X3tcdGF1K2h9PVx0aWxkZSB5XnsoXHRhdSl9X3tcdGF1K2gtMzM2fSQuICoqVFNSKiogKGFkZGl0aXZlIGRvdWJsZS1zZWFzb25hbAp0aW1lIHNlcmllcyByZWdyZXNzaW9uKToKCiQkCnlfdD1cYmV0YV8wK1xiZXRhXzF0K1xzdW1fe2k9Mn1eezQ4fVxhbHBoYV9pSF97aSx0fStcc3VtX3tqPTJ9Xns3fVxnYW1tYV9qRV97aix0fStlX3QsIFx0YWd7MTB9CiQkCgp3aXRoIGhhbGYtaG91ciBkdW1taWVzICRIX3tpLHR9JCBhbmQgd2Vla2RheSBkdW1taWVzICRFX3tqLHR9JCAoYmFzZSBjYXRlZ29yaWVzIDAwOjMwIGFuZCBNb25kYXkpLApmaXR0ZWQgYnkgbGVhc3Qgc3F1YXJlcyBvbiB0aGUgd2hvbGUgaW1wdXRlZCBoaXN0b3J5LiBUaW1lIGlzIHNjYWxlZCBhcyAkdC8xMF40JCBmb3IgbnVtZXJpY2FsCnN0YWJpbGl0eTsgdGhlIHNjYWxpbmcgZG9lcyBub3QgY2hhbmdlIHRoZSBmb3JlY2FzdHMuCgpgYGB7ciBtb2RlbHMtc3RhdH0KdHNyX2Rlc2lnbiA8LSBmdW5jdGlvbihqKSB7CiAgbiA8LSBsZW5ndGgoaik7IGhoIDwtIGhvZChqKTsgZGQgPC0gZG93KGopCiAgWGggPC0gbWF0cml4KDAsIG4sIDQ3KTsgWGhbY2JpbmQod2hpY2goaGggPiAwKSwgaGhbaGggPiAwXSldIDwtIDEKICBYZCA8LSBtYXRyaXgoMCwgbiwgNik7ICBYZFtjYmluZCh3aGljaChkZCA+IDApLCBkZFtkZCA+IDBdKV0gPC0gMQogIGNiaW5kKDEsIGogLyAxZTQsIFhoLCBYZCkKfQp0c3JfZml0ICA8LSBmdW5jdGlvbih5LCBqKSBxci5jb2VmKHFyKHRzcl9kZXNpZ24oaikpLCB5KQp0c3JfcHJlZCA8LSBmdW5jdGlvbihiZXRhLCBqKSB7IGIgPC0gYmV0YTsgYltpcy5uYShiKV0gPC0gMDsgYXMubnVtZXJpYyh0c3JfZGVzaWduKGopICUqJSBiKSB9CmBgYAoKIyMgTGlnaHRHQk0gKGRpcmVjdCBtdWx0aS1ob3Jpem9uIG1vZGVsKQoKRWFjaCAob3JpZ2luICRcdGF1JCwgaG9yaXpvbiAkaCQpIHBhaXIgaXMgb25lIHJvdyB3aXRoIDE0IGZlYXR1cmVzOiAkaCQsIGhhbGYtaG91ciBhbmQgd2Vla2RheSBvZiB0aGUKdGFyZ2V0LCB0aGUgbGFncyAxLCAyLCAzLCA2IGFuZCAxMiBhdCB0aGUgb3JpZ2luLCB0aGUgdmFsdWVzIDQ4LCA5NiwgMzM2IGFuZCA2NzIgc3RlcHMgYmVmb3JlIHRoZQp0YXJnZXQsIGFuZCB0aGUgbWVhbnMgb2YgdGhlIGxhc3QgZGF5IGFuZCB3ZWVrLiBTZXR0aW5nczogbGVhcm5pbmcgcmF0ZSAwLjAzLCAzMSBsZWF2ZXMsIGF0IGxlYXN0IDUwCnNhbXBsZXMgcGVyIGxlYWYsIGJhZ2dpbmcgMC44LCBmZWF0dXJlIGZyYWN0aW9uIDAuOSwgJExfMiQgcGVuYWx0eSAxLCBlYXJseSBzdG9wcGluZyBhZnRlciAxMDAgcm91bmRzCihhdCBtb3N0IDIwMDAgdHJlZXMpIG9uIHRoZSBvYnNlcnZlZCB2YWxpZGF0aW9uIHRhcmdldHMuCgpgYGB7ciBtb2RlbHMtZ2JtfQpnYm1fcm93cyA8LSBmdW5jdGlvbih5LCB0YXUpIHsKICBoIDwtIDA6KEggLSAxKTsgdGd0IDwtIHRhdSArIGggICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgMC1iYXNlZCB0YXJnZXQgaW5kaWNlcwogIGNzIDwtIGMoMCwgY3Vtc3VtKHkpKTsgbTQ4IDwtIChjc1t0YXUgKyAxXSAtIGNzW3RhdSArIDEgLSA0OF0pIC8gNDg7IG0zMzYgPC0gKGNzW3RhdSArIDFdIC0gY3NbdGF1ICsgMSAtIDMzNl0pIC8gMzM2CiAgbGFnIDwtIGZ1bmN0aW9uKGspIHJlcCh5W3RhdSAtIGsgKyAxXSwgSCk7IHNsIDwtIGZ1bmN0aW9uKGspIHlbdGd0IC0gayArIDFdCiAgY2JpbmQoaCA9IGgsIGhvZCA9IGhvZCh0Z3QpLCBkb3cgPSBkb3codGd0KSwgbGFnMSA9IGxhZygxKSwgbGFnMiA9IGxhZygyKSwgbGFnMyA9IGxhZygzKSwgbGFnNiA9IGxhZyg2KSwKICAgICAgICBsYWcxMiA9IGxhZygxMiksIHNsYWc0OCA9IHNsKDQ4KSwgc2xhZzk2ID0gc2woOTYpLCBzbGFnMzM2ID0gc2woMzM2KSwgc2xhZzY3MiA9IHNsKDY3MiksCiAgICAgICAgbWVhbjQ4ID0gbTQ4LCBtZWFuMzM2ID0gbTMzNikKfQpidWlsZF9nYm0gPC0gZnVuY3Rpb24oeSwgeXRhcmdldCwgb3JpZ2lucykgbGlzdCgKICBYID0gZG8uY2FsbChyYmluZCwgbGFwcGx5KG9yaWdpbnMsIGZ1bmN0aW9uKHQpIGdibV9yb3dzKHksIHQpKSksCiAgWSA9IHVubGlzdChsYXBwbHkob3JpZ2lucywgZnVuY3Rpb24odCkgeXRhcmdldFsodCArIDEpOih0ICsgSCldKSkpCmZpdF9wcmVkaWN0X2dibSA8LSBmdW5jdGlvbih5X2hpc3QsIHhfaGlzdF9yYXcsIGhpc3RfYXQpIHsKICB0ciA8LSBidWlsZF9nYm0oeV9oaXN0LCB5X2hpc3QsIHRyX29yaWcpOyB2YSA8LSBidWlsZF9nYm0oeV9oaXN0LCB4X2hpc3RfcmF3LCB2YV9vcmlnKTsgb2sgPC0gIWlzLm5hKHZhJFkpCiAgZHRyIDwtIGxpZ2h0Z2JtOjpsZ2IuRGF0YXNldCh0ciRYLCBsYWJlbCA9IHRyJFkpCiAgZHZhIDwtIGxpZ2h0Z2JtOjpsZ2IuRGF0YXNldC5jcmVhdGUudmFsaWQoZHRyLCB2YSRYW29rLCAsIGRyb3AgPSBGQUxTRV0sIGxhYmVsID0gdmEkWVtva10pCiAgcHJtIDwtIGxpc3Qob2JqZWN0aXZlID0gInJlZ3Jlc3Npb24iLCBsZWFybmluZ19yYXRlID0gMC4wMywgbnVtX2xlYXZlcyA9IDMxTCwgbWluX2RhdGFfaW5fbGVhZiA9IDUwTCwKICAgICAgICAgICAgICBiYWdnaW5nX2ZyYWN0aW9uID0gMC44LCBiYWdnaW5nX2ZyZXEgPSAxTCwgZmVhdHVyZV9mcmFjdGlvbiA9IDAuOSwgbGFtYmRhX2wyID0gMSwKICAgICAgICAgICAgICBudW1fdGhyZWFkcyA9IDFMLCB2ZXJib3NlID0gLTFMLCBzZWVkID0gMUwpCiAgZ2IgPC0gbGlnaHRnYm06OmxnYi50cmFpbihwYXJhbXMgPSBwcm0sIGRhdGEgPSBkdHIsIG5yb3VuZHMgPSAyMDAwTCwgdmFsaWRzID0gbGlzdCh2YWxpZCA9IGR2YSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSAxMDBMLCB2ZXJib3NlID0gLTFMKQogIFh0ZSA8LSBkby5jYWxsKHJiaW5kLCBsYXBwbHkoc2VxX2Fsb25nKHRlc3Rfb3JpZ2lucyksIGZ1bmN0aW9uKGspIGdibV9yb3dzKGhpc3RfYXRbW2tdXSwgdGVzdF9vcmlnaW5zW2tdKSkpCiAgbWF0cml4KHByZWRpY3QoZ2IsIFh0ZSksIG5yb3cgPSBOX1RFU1RfREFZUywgYnlyb3cgPSBUUlVFKQp9CmBgYAoKIyMgTmV1cmFsIG5ldHdvcmtzOiBNTFAsIExTVE0sIFRDTiBhbmQgdGhlIFRTUuKAk0xTVE0gaHlicmlkCgpBbGwgbmV0d29ya3MgdXNlIHN0YW5kYXJkaXNlZCBkYXRhIGFuZCB0aGUgc2FtZSBpbnB1dHM6IHRoZSBzZXF1ZW5jZQokXG1hdGhiZiBzX1x0YXU9KFx0aWxkZSB5X3tcdGF1LTQ3fSxcZG90cyxcdGlsZGUgeV9cdGF1KSQgYW5kIHRoZSBzaWRlIHZlY3RvcgokXG1hdGhiZiB2X1x0YXU9KFx0aWxkZSB5X3tcdGF1LTMzNX0sXGRvdHMsXHRpbGRlIHlfe1x0YXUtMjg4fSxcc2luXHRmcmFjezJccGkgaF9cdGF1fXs0OH0sXGNvc1x0ZnJhY3syXHBpIGhfXHRhdX17NDh9LFxzaW5cdGZyYWN7MlxwaSBkX1x0YXV9ezd9LFxjb3NcdGZyYWN7MlxwaSBkX1x0YXV9ezd9KSQKKHRoZSBzYW1lIHBlcmlvZCBvbmUgd2VlayBlYXJsaWVyIGFuZCBjYWxlbmRhciBjb2RlcyBvZiB0aGUgb3JpZ2luKS4KCiogKipNTFA6KiogJFxoYXR7XG1hdGhiZiB5fT1cbWF0aGJmIFdfM1wsXG1hdGhybXtSZUxVfShcbWF0aGJmIFdfMlwsXG1hdGhybXtSZUxVfShcbWF0aGJmIFdfMVtcbWF0aGJmIHNfXHRhdTtcbWF0aGJmIHZfXHRhdV0rXG1hdGhiZiBiXzEpK1xtYXRoYmYgYl8yKStcbWF0aGJmIGJfMyQgKDEyOCBhbmQgNjQgdW5pdHMpLgoqICoqTFNUTToqKiA2NCB1bml0cywgJFxtYXRoYmYgY190PVxtYXRoYmYgZl90XG9kb3RcbWF0aGJmIGNfe3QtMX0rXG1hdGhiZiBpX3Rcb2RvdFx0aWxkZXtcbWF0aGJmIGN9X3QkIGFuZAogICRcbWF0aGJmIGhfdD1cbWF0aGJmIG9fdFxvZG90XHRhbmgoXG1hdGhiZiBjX3QpJCB3aXRoIHNpZ21vaWQgZ2F0ZXMgJFxtYXRoYmYgaV90LFxtYXRoYmYgZl90LFxtYXRoYmYgb190JDsKICAkXG1hdGhiZiBoX3s0OH0kIGlzIGNvbmNhdGVuYXRlZCB3aXRoICRcbWF0aGJmIHZfXHRhdSQsIGZvbGxvd2VkIGJ5IERlbnNlIDY0IChSZUxVKSBhbmQgRGVuc2UgNDguCiogKipUQ046KiogZml2ZSBjYXVzYWwgZGlsYXRlZCBjb252b2x1dGlvbnMgJChGKl9keCkodCk9XHN1bV97aT0wfV57Mn1mKGkpXCx4X3t0LWRcLGl9JCwKICAkZFxpblx7MSwyLDQsOCwxNlx9JCwgMzIgZmlsdGVycyAocmVjZXB0aXZlIGZpZWxkIDYzKSwgdGhlbiBhcyBhYm92ZS4KKiAqKlRTUuKAk0xTVE06KiogdGhlIExTVE0gZm9yZWNhc3RzIHRoZSBUU1IgcmVzaWR1YWxzLCAkXGhhdCB5X3tcdGF1K2h9PVxoYXQgTF97XHRhdStofStcaGF0IE5fe1x0YXUraH0kLgoKTG9zczogbWFza2VkIG1lYW4gc3F1YXJlZCBlcnJvciBvdmVyIG9ic2VydmVkIHRhcmdldHMsCiRcbWF0aGNhbCBMPVxzdW1fe2ksaH1tX3tpaH0oeV97aWh9LVxoYXQgeV97aWh9KV4yL1xzdW1fe2ksaH1tX3tpaH0kLiBBZGFtIChsZWFybmluZyByYXRlCiQyXHRpbWVzMTBeey0zfSQpLCBiYXRjaCAyNTYsIGF0IG1vc3QgMjAgZXBvY2hzLCBlYXJseSBzdG9wcGluZyB3aXRoIHBhdGllbmNlIDMgYW5kIGJlc3Qgd2VpZ2h0cwpyZXN0b3JlZDsgdHdvIHNlZWRzLCBmb3JlY2FzdHMgYXZlcmFnZWQuIFdpdGhvdXQgYGtlcmFzM2Agb25seSBhbiBNTFAgZnJvbSBgbm5ldGAgaXMgYXZhaWxhYmxlLgoKYGBge3IgbW9kZWxzLW5ufQpvcmlnaW5fY2FsIDwtIGZ1bmN0aW9uKHRhdSkgeyB0IDwtIHRhdSAtIDE7IGEgPC0gMiAqIHBpICogaG9kKHQpIC8gNDg7IGIgPC0gMiAqIHBpICogZG93KHQpIC8gNwogIGMoc2luKGEpLCBjb3MoYSksIHNpbihiKSwgY29zKGIpKSB9Cm5uX2lucHV0cyA8LSBmdW5jdGlvbih5LCB0YXUpIGxpc3Qoc2VxID0geVsodGF1IC0gTF9TRVEgKyAxKTp0YXVdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNpZGUgPSBjKHlbKHRhdSAtIFMyICsgMSk6KHRhdSAtIFMyICsgSCldLCBvcmlnaW5fY2FsKHRhdSkpKQpidWlsZF9ubl9zYW1wbGVzIDwtIGZ1bmN0aW9uKHksIHl0YXJnZXQsIG9yaWdpbnMpIHsKICBuIDwtIGxlbmd0aChvcmlnaW5zKTsgU0VRIDwtIGFycmF5KDAsIGMobiwgTF9TRVEsIDEpKTsgU0lERSA8LSBtYXRyaXgoMCwgbiwgSCArIDQpOyBZIDwtIG1hdHJpeCgwLCBuLCBIKQogIGZvciAoaSBpbiBzZXFfbGVuKG4pKSB7IHRhdSA8LSBvcmlnaW5zW2ldOyBpbnAgPC0gbm5faW5wdXRzKHksIHRhdSkKICAgIFNFUVtpLCAsIDFdIDwtIGlucCRzZXE7IFNJREVbaSwgXSA8LSBpbnAkc2lkZTsgWVtpLCBdIDwtIHl0YXJnZXRbKHRhdSArIDEpOih0YXUgKyBIKV0gfQogIGxpc3Qoc2VxID0gU0VRLCBzaWRlID0gU0lERSwgeSA9IFkpCn0Kc3RhY2tfdGVzdCA8LSBmdW5jdGlvbihsc3QpIGxpc3Qoc2VxID0gYXJyYXkodChzYXBwbHkobHN0LCBgW1tgLCAic2VxIikpLCBjKGxlbmd0aChsc3QpLCBMX1NFUSwgMSkpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWRlID0gdChzYXBwbHkobHN0LCBgW1tgLCAic2lkZSIpKSkKaWYgKEhBU19LRVJBUykgewogIGxpYnJhcnkoa2VyYXMzKQogIG1hc2tlZF9tc2UgPC0gZnVuY3Rpb24oeV90cnVlLCB5X3ByZWQpIHsgICAgICAgICMgbWVhbiBzcXVhcmVkIGVycm9yIG92ZXIgb2JzZXJ2ZWQgdGFyZ2V0cyBvbmx5CiAgICBtYXNrIDwtIG9wX2xvZ2ljYWxfbm90KG9wX2lzbmFuKHlfdHJ1ZSkpCiAgICBkaWZmIDwtIG9wX3doZXJlKG1hc2ssIHlfdHJ1ZSAtIHlfcHJlZCwgb3BfemVyb3NfbGlrZSh5X3ByZWQpKQogICAgb3Bfc3VtKG9wX3NxdWFyZShkaWZmKSkgLyBvcF9tYXhpbXVtKG9wX3N1bShvcF9jYXN0KG1hc2ssICJmbG9hdDMyIikpLCAxKQogIH0KICBidWlsZF9ubiA8LSBmdW5jdGlvbihraW5kLCB1bml0cyA9IDY0LCBsciA9IDJlLTMpIHsKICAgIGEgPC0ga2VyYXNfaW5wdXQoc2hhcGUgPSBjKExfU0VRLCAxTCkpOyBiIDwtIGtlcmFzX2lucHV0KHNoYXBlID0gYyhIICsgNEwpKQogICAgaWYgKGtpbmQgPT0gIk1MUCIpIHsKICAgICAgeiA8LSBsYXllcl9jb25jYXRlbmF0ZShsaXN0KGxheWVyX2ZsYXR0ZW4oYSksIGIpKSB8PgogICAgICAgIGxheWVyX2RlbnNlKDEyOCwgYWN0aXZhdGlvbiA9ICJyZWx1IikgfD4gbGF5ZXJfZGVuc2UoNjQsIGFjdGl2YXRpb24gPSAicmVsdSIpCiAgICB9IGVsc2UgewogICAgICBpZiAoa2luZCAlaW4lIGMoIkxTVE0iLCAiVFNSLUxTVE0iKSkgaCA8LSBhIHw+IGxheWVyX2xzdG0odW5pdHMpCiAgICAgIGlmIChraW5kID09ICJUQ04iKSB7CiAgICAgICAgaCA8LSBhCiAgICAgICAgZm9yIChkIGluIGMoMSwgMiwgNCwgOCwgMTYpKSBoIDwtIGggfD4gbGF5ZXJfY29udl8xZCgzMiwgMywgZGlsYXRpb25fcmF0ZSA9IGQsIHBhZGRpbmcgPSAiY2F1c2FsIiwgYWN0aXZhdGlvbiA9ICJyZWx1IikKICAgICAgICBoIDwtIGggfD4gbGF5ZXJfY3JvcHBpbmdfMWQoY3JvcHBpbmcgPSBjKExfU0VRIC0gMUwsIDBMKSkgfD4gbGF5ZXJfZmxhdHRlbigpICAgIyBsYXN0IHRpbWUgc3RlcAogICAgICB9CiAgICAgIHogPC0gbGF5ZXJfY29uY2F0ZW5hdGUobGlzdChoLCBiKSkgfD4gbGF5ZXJfZGVuc2UoNjQsIGFjdGl2YXRpb24gPSAicmVsdSIpCiAgICB9CiAgICBtIDwtIGtlcmFzX21vZGVsKGxpc3QoYSwgYiksIHogfD4gbGF5ZXJfZGVuc2UoSCkpCiAgICBtIHw+IGNvbXBpbGUob3B0aW1pemVyID0gb3B0aW1pemVyX2FkYW0obHIpLCBsb3NzID0gbWFza2VkX21zZSkKICAgIG0KICB9CiAgZml0X3ByZWRpY3Rfbm4gPC0gZnVuY3Rpb24oa2luZCwgdHIsIHZhLCB0ZSwgc2VlZCwgZXBvY2hzID0gMjAsIHBhdGllbmNlID0gMykgewogICAgY2xlYXJfc2Vzc2lvbigpOyBzZXRfcmFuZG9tX3NlZWQoc2VlZCkKICAgIG0gPC0gYnVpbGRfbm4oa2luZCkKICAgIG0gfD4gZml0KGxpc3QodHIkc2VxLCB0ciRzaWRlKSwgdHIkeSwgdmFsaWRhdGlvbl9kYXRhID0gbGlzdChsaXN0KHZhJHNlcSwgdmEkc2lkZSksIHZhJHkpLAogICAgICAgICAgICAgZXBvY2hzID0gZXBvY2hzLCBiYXRjaF9zaXplID0gMjU2LCB2ZXJib3NlID0gMCwKICAgICAgICAgICAgIGNhbGxiYWNrcyA9IGxpc3QoY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSBwYXRpZW5jZSwgcmVzdG9yZV9iZXN0X3dlaWdodHMgPSBUUlVFKSkpCiAgICBtIHw+IHByZWRpY3QobGlzdCh0ZSRzZXEsIHRlJHNpZGUpLCB2ZXJib3NlID0gMCkKICB9CiAgTk5fTU9ERUxTIDwtIGMoIk1MUCIsICJMU1RNIiwgIlRDTiIsICJUU1ItTFNUTSIpCn0gZWxzZSB7CiAgZml0X3ByZWRpY3Rfbm4gPC0gZnVuY3Rpb24oa2luZCwgdHIsIHZhLCB0ZSwgc2VlZCwgZXBvY2hzID0gTkEsIHBhdGllbmNlID0gTkEpIHsgICAjIGZhbGxiYWNrIHdpdGhvdXQga2VyYXMzCiAgICBzZXQuc2VlZChzZWVkKQogICAgWHRyIDwtIGNiaW5kKG1hdHJpeCh0ciRzZXEsIG5yb3codHIkeSkpLCB0ciRzaWRlKTsgWHRlIDwtIGNiaW5kKG1hdHJpeCh0ZSRzZXEsIG5yb3codGUkc2lkZSkpLCB0ZSRzaWRlKQogICAgZml0IDwtIG5uZXQ6Om5uZXQoWHRyLCB0ciR5LCBzaXplID0gMTIsIGxpbm91dCA9IFRSVUUsIGRlY2F5ID0gMWUtMywgbWF4aXQgPSAzMDAsIE1heE5XdHMgPSAxMDAwMCwgdHJhY2UgPSBGQUxTRSkKICAgIHByZWRpY3QoZml0LCBYdGUpCiAgfQogIE5OX01PREVMUyA8LSAiTUxQIgp9CmBgYAoKIyMgT25lIGNvbXBsZXRlIHJ1bjogZnJvbSBpbXB1dGF0aW9uIHRvIGZvcmVjYXN0cwoKYHJ1bl9mb3JlY2FzdCgpYCBhcHBsaWVzIHRoZSBwcm90b2NvbCBmb3Igb25lIHBvbGx1dGFudCBhbmQgb25lIGltcHV0YXRpb24gbWV0aG9kIGFuZCByZXR1cm5zIHRoZQpmb3JlY2FzdHMgb2YgYWxsIG1vZGVscyAodHJ1bmNhdGVkIGF0IHplcm8pIGFuZCB0aGUgUk1TRSBvZiBldmVyeSBzZWVkLgoKYGBge3IgcnVuLWZvcmVjYXN0fQpydW5fZm9yZWNhc3QgPC0gZnVuY3Rpb24oeF9yYXcsIGltcHV0ZV9mbiwgc2VlZHMgPSBTRUVEUykgewogIHlfaGlzdCAgPC0gaW1wdXRlX2ZuKHhfcmF3WzE6VEVTVF9TVEFSVF0pICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBydWxlIDEKICBoaXN0X2F0IDwtIGxhcHBseSh0ZXN0X29yaWdpbnMsIGZ1bmN0aW9uKHRhdSkgaW1wdXRlX2ZuKHhfcmF3WzE6dGF1XSkpICAgICAgICAjIHJ1bGUgMiAoY2F1c2FsKQogIHlfdHJ1ZSAgPC0gdChzYXBwbHkodGVzdF9vcmlnaW5zLCBmdW5jdGlvbih0YXUpIHhfcmF3Wyh0YXUgKyAxKToodGF1ICsgSCldKSkgICMgTkEgPSBub3Qgb2JzZXJ2ZWQgKHJ1bGUgMykKICB4X2hpc3RfcmF3IDwtIHhfcmF3WzE6VEVTVF9TVEFSVF0KICBvdXQgPC0gbGlzdCgpOyBzZWVkX3Jvd3MgPC0gbGlzdCgpCiAgb3V0JFNOQUlWRSA8LSB0KHNhcHBseShzZXFfYWxvbmcodGVzdF9vcmlnaW5zKSwgZnVuY3Rpb24oaykgewogICAgdGF1IDwtIHRlc3Rfb3JpZ2luc1trXTsgaGlzdF9hdFtba11dWyh0YXUgLSBTMiArIDEpOih0YXUgLSBTMiArIEgpXSB9KSkKICBqX2hpc3QgPC0gMDooVEVTVF9TVEFSVCAtIDEpOyBiZXRhIDwtIHRzcl9maXQoeV9oaXN0LCBqX2hpc3QpCiAgb3V0JFRTUiA8LSB0KHNhcHBseSh0ZXN0X29yaWdpbnMsIGZ1bmN0aW9uKHRhdSkgdHNyX3ByZWQoYmV0YSwgdGF1ICsgMDooSCAtIDEpKSkpCiAgaWYgKEhBU19MR0IpIG91dCRMaWdodEdCTSA8LSBmaXRfcHJlZGljdF9nYm0oeV9oaXN0LCB4X2hpc3RfcmF3LCBoaXN0X2F0KQogIG11IDwtIG1lYW4oeV9oaXN0WzE6VkFMX1NUQVJUXSk7IHNkdiA8LSBzZCh5X2hpc3RbMTpWQUxfU1RBUlRdKSAgICAgICAgICAgICAgICMgc3RhbmRhcmRpc2F0aW9uCiAgdHJzIDwtIGJ1aWxkX25uX3NhbXBsZXMoKHlfaGlzdCAtIG11KSAvIHNkdiwgKHlfaGlzdCAtIG11KSAvIHNkdiwgdHJfb3JpZykKICB2YXMgPC0gYnVpbGRfbm5fc2FtcGxlcygoeV9oaXN0IC0gbXUpIC8gc2R2LCAoeF9oaXN0X3JhdyAtIG11KSAvIHNkdiwgdmFfb3JpZykKICB0ZXMgPC0gc3RhY2tfdGVzdChsYXBwbHkoc2VxX2Fsb25nKHRlc3Rfb3JpZ2lucyksIGZ1bmN0aW9uKGspIG5uX2lucHV0cygoaGlzdF9hdFtba11dIC0gbXUpIC8gc2R2LCB0ZXN0X29yaWdpbnNba10pKSkKICByX2hpc3QgPC0geV9oaXN0IC0gdHNyX3ByZWQoYmV0YSwgal9oaXN0KTsgcl9zZCA8LSBzZChyX2hpc3RbMTpWQUxfU1RBUlRdKSAgICAjIFRTUiByZXNpZHVhbHMKICBmb3IgKGtpbmQgaW4gTk5fTU9ERUxTKSB7CiAgICBwcmVkcyA8LSBsYXBwbHkoc2VlZHMsIGZ1bmN0aW9uKHMpIHsKICAgICAgaWYgKGtpbmQgPT0gIlRTUi1MU1RNIikgewogICAgICAgIHRyciA8LSBidWlsZF9ubl9zYW1wbGVzKHJfaGlzdCAvIHJfc2QsIHJfaGlzdCAvIHJfc2QsIHRyX29yaWcpCiAgICAgICAgdmFyIDwtIGJ1aWxkX25uX3NhbXBsZXMocl9oaXN0IC8gcl9zZCwgKHhfaGlzdF9yYXcgLSB0c3JfcHJlZChiZXRhLCBqX2hpc3QpKSAvIHJfc2QsIHZhX29yaWcpCiAgICAgICAgdGVyIDwtIHN0YWNrX3Rlc3QobGFwcGx5KHNlcV9hbG9uZyh0ZXN0X29yaWdpbnMpLCBmdW5jdGlvbihrKSB7CiAgICAgICAgICB0YXUgPC0gdGVzdF9vcmlnaW5zW2tdOyBubl9pbnB1dHMoKGhpc3RfYXRbW2tdXSAtIHRzcl9wcmVkKGJldGEsIDA6KHRhdSAtIDEpKSkgLyByX3NkLCB0YXUpIH0pKQogICAgICAgIGZpdF9wcmVkaWN0X25uKGtpbmQsIHRyciwgdmFyLCB0ZXIsIHMpICogcl9zZCArIG91dCRUU1IKICAgICAgfSBlbHNlIGZpdF9wcmVkaWN0X25uKGtpbmQsIHRycywgdmFzLCB0ZXMsIHMpICogc2R2ICsgbXUKICAgIH0pCiAgICBmb3IgKGkgaW4gc2VxX2Fsb25nKHNlZWRzKSkgc2VlZF9yb3dzW1tsZW5ndGgoc2VlZF9yb3dzKSArIDFdXSA8LSBkYXRhLmZyYW1lKAogICAgICBtb2RlbCA9IGtpbmQsIHNlZWQgPSBzZWVkc1tpXSwgcm1zZSA9IHNxcnQobWVhbigocG1heChwcmVkc1tbaV1dLCAwKSAtIHlfdHJ1ZSleMiwgbmEucm0gPSBUUlVFKSkpCiAgICBvdXRbW2tpbmRdXSA8LSBSZWR1Y2UoYCtgLCBwcmVkcykgLyBsZW5ndGgocHJlZHMpCiAgfQogIGZjIDwtIGJpbmRfcm93cyhsYXBwbHkobmFtZXMob3V0KSwgZnVuY3Rpb24obW4pIGRhdGEuZnJhbWUobW9kZWwgPSBtbiwKICAgIG9yaWdpbiA9IHJlcCh0ZXN0X29yaWdpbnMsIGVhY2ggPSBIKSwgaCA9IHJlcCgxOkgsIE5fVEVTVF9EQVlTKSwKICAgIHkgPSBhcy52ZWN0b3IodCh5X3RydWUpKSwgeWhhdCA9IHBtYXgoYXMudmVjdG9yKHQob3V0W1ttbl1dKSksIDApKSkpCiAgbGlzdChmYyA9IGZjLCBzZWVkcyA9IGJpbmRfcm93cyhzZWVkX3Jvd3MpKQp9CmBgYAoKIyMgRGllYm9sZOKAk01hcmlhbm8gdGVzdCAoRXEuIDExKQoKV2l0aCAkZF90PWVfezF0fV4yLWVfezJ0fV4yJCAoU2Vhcy1BUiBoaXN0b3J5IG1pbnVzIGFsdGVybmF0aXZlIGhpc3RvcnkpLAoKJCQKXG1hdGhybXtETX09XHNxcnR7XGZyYWN7TisxLTJoK2goaC0xKS9OfXtOfX1cO1xmcmFje1xiYXIgZH17XHNxcnR7XHdpZGVoYXR7XG1hdGhybXtWYXJ9fShcYmFyIGQpfX0sXHFxdWFkClx3aWRlaGF0e1xtYXRocm17VmFyfX0oXGJhciBkKT1cZnJhYzFOXEJpZyhcaGF0XGdhbW1hXzArMlxzdW1fe2s9MX1ee2gtMX1caGF0XGdhbW1hX2tcQmlnKSwgXHRhZ3sxMX0KJCQKCndpdGggc2FtcGxlIGF1dG9jb3ZhcmlhbmNlcyAkXGhhdFxnYW1tYV9rJCBvZiAkZF90JCwgJGg9NDgkLCB0aGUgSGFydmV54oCTTGV5Ym91cm5l4oCTTmV3Ym9sZCBmYWN0b3IgYW5kIGEKJHRfe04tMX0kIHJlZmVyZW5jZSBkaXN0cmlidXRpb247IG5lZ2F0aXZlIHZhbHVlcyBmYXZvdXIgU2Vhcy1BUi4gSWYgdGhlIHZhcmlhbmNlIGVzdGltYXRlIGlzIG5vdCBwb3NpdGl2ZSwKJFxoYXRcZ2FtbWFfMC9OJCBpcyB1c2VkLiBUaGlzIGVxdWFscyBgZm9yZWNhc3Q6OmRtLnRlc3QodmFyZXN0aW1hdG9yID0gImFjZiIpYCAoY2hlY2tlZCBpbiBTZWN0aW9uIDguNCkuCgpgYGB7ciBkbS10ZXN0fQpkbV90ZXN0IDwtIGZ1bmN0aW9uKGUxLCBlMiwgaCA9IDQ4LCBwb3dlciA9IDIpIHsgICAjIEVxLiAoMTEpCiAgb2sgPC0gIShpcy5uYShlMSkgfCBpcy5uYShlMikpOyBkIDwtIGFicyhlMVtva10pXnBvd2VyIC0gYWJzKGUyW29rXSlecG93ZXIKICBuIDwtIGxlbmd0aChkKTsgZGMgPC0gZCAtIG1lYW4oZCkKICBnYW0gPC0gc2FwcGx5KDA6KGggLSAxKSwgZnVuY3Rpb24oaykgc3VtKGRjWyhrICsgMSk6bl0gKiBkY1sxOihuIC0gayldKSAvIG4pCiAgdiA8LSAoZ2FtWzFdICsgMiAqIHN1bShnYW1bLTFdKSkgLyBuCiAgaWYgKHYgPD0gMCkgdiA8LSBnYW1bMV0gLyBuCiAgc3RhdCA8LSBtZWFuKGQpIC8gc3FydCh2KSAqIHNxcnQoKG4gKyAxIC0gMiAqIGggKyBoICogKGggLSAxKSAvIG4pIC8gbikKICBjKHN0YXQgPSBzdGF0LCBwID0gMiAqIHB0KC1hYnMoc3RhdCksIGRmID0gbiAtIDEpKQp9CmBgYAoKIyBFeHBlcmltZW50IDI6IGltcGFjdCBvZiBpbXB1dGF0aW9uIG9uIGZvcmVjYXN0aW5nCgojIyBSdW5uaW5nIHRoZSBleHBlcmltZW50CgpGaXZlIGltcHV0YXRpb25zIGFyZSBjb21wYXJlZCBmb3Igc2V2ZW4gbW9kZWxzIGF0IFNVRiAxOiBNRURJQU4gKGEgY29tbW9uIHByYWN0aWNhbCBkZWZhdWx0KSwgTEkKKHRoZSBnZW5lcmljIGRlZmF1bHQpLCBTRUFERUMgKHRoZSBzdHJvbmdlc3QgY29tcGV0aXRvciBpbiBFeHBlcmltZW50IDEpLCBLQUxNQU4gKHN0YXRlLXNwYWNlCnNtb290aGluZykgYW5kIFNlYXMtQVIgKGJhbmR3aWR0aHMgb2YgU1VGIDEgZnJvbSBFeHBlcmltZW50IDApLiBGb3JlY2FzdHMgYXJlIHN0b3JlZCBwZXIgcG9sbHV0YW50IGFuZAppbXB1dGF0aW9uLCBzbyBhbiBpbnRlcnJ1cHRlZCBydW4gcmVzdW1lcyB3aGVyZSBpdCBzdG9wcGVkLgoKYGBge3IgZXhwMi1ydW59CklNUF9GQyA8LSBjKCJNRURJQU4iLCAiTEkiLCAiU0VBREVDIiwgIktBTE1BTiIsICJTZWFzLUFSIikKTU9ERUxTIDwtIGMoIlNOQUlWRSIsICJUU1IiLCAiTGlnaHRHQk0iLCAiTUxQIiwgIkxTVE0iLCAiVENOIiwgIlRTUi1MU1RNIikKTU9ERUxfVEVYIDwtIGMoU05BSVZFID0gIlNlYXNvbmFsIG5hXFxcIml2ZSIsIFRTUiA9ICJUU1IiLCBMaWdodEdCTSA9ICJMaWdodEdCTSIsIE1MUCA9ICJNTFAiLCBMU1RNID0gIkxTVE0iLAogICAgICAgICAgICAgICBUQ04gPSAiVENOIiwgYFRTUi1MU1RNYCA9ICJUU1ItLUxTVE0iKQppbXB1dGVyX2ZvciA8LSBmdW5jdGlvbihpbXApIHN3aXRjaChpbXAsIE1FRElBTiA9IE1FVEhPRFMkTUVESUFOLCBMSSA9IE1FVEhPRFMkTEksIFNFQURFQyA9IE1FVEhPRFMkU0VBREVDLAogIEtBTE1BTiA9IE1FVEhPRFMkS0FMTUFOLCBgU2Vhcy1BUmAgPSBmdW5jdGlvbih2KSBpbXB1dGVfc2Vhc2FyKHYsIEJXW1siU1VGIDEiXV0kYndfZGF5LCBCV1tbIlNVRiAxIl1dJGJ3X3dlZWspKQpkaXIyIDwtIG91dF9maWxlKGlmIChRVUlDSykgImV4cDJfcXVpY2siIGVsc2UgImV4cDIiKTsgZGlyLmNyZWF0ZShkaXIyLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKcG9sc19mYyA8LSBpZiAoUVVJQ0spICJOTzIiIGVsc2UgUE9MUzsgaW1wc19mYyA8LSBpZiAoUVVJQ0spIGMoIk1FRElBTiIsICJTZWFzLUFSIikgZWxzZSBJTVBfRkMKaWYgKCEoSEFTX0tFUkFTICYmIEhBU19MR0IpKSBjYXQoIk5vdGU6IHRoZSBmdWxsIG1vZGVsIHNldCBuZWVkcyBsaWdodGdibSBhbmQga2VyYXMzOyBtaXNzaW5nIG1vZGVscyBhcmUgc2tpcHBlZC5cbiIpCmZvciAocCBpbiBwb2xzX2ZjKSBmb3IgKGltcCBpbiBpbXBzX2ZjKSB7CiAgZmYgPC0gZmlsZS5wYXRoKGRpcjIsIHNwcmludGYoImZjXyVzXyVzLmNzdiIsIHAsIGltcCkpCiAgaWYgKGlzVFJVRShQJHVzZV9jYWNoZSkgJiYgZmlsZS5leGlzdHMoZmYpKSBuZXh0CiAgdDAgPC0gU3lzLnRpbWUoKQogIHJlcyA8LSBydW5fZm9yZWNhc3QoZ2V0X3NlcmllcygiU1VGIDEiLCBwKSwgaW1wdXRlcl9mb3IoaW1wKSwgc2VlZHMgPSBpZiAoUVVJQ0spIFNFRURTWzFdIGVsc2UgU0VFRFMpCiAgd3JpdGUuY3N2KHJlcyRmYyB8PiBtdXRhdGUocG9sbHV0YW50ID0gcCwgaW1wdXRhdGlvbiA9IGltcCksIGZmLCByb3cubmFtZXMgPSBGQUxTRSkKICB3cml0ZS5jc3YocmVzJHNlZWRzIHw+IG11dGF0ZShwb2xsdXRhbnQgPSBwLCBpbXB1dGF0aW9uID0gaW1wKSwgc3ViKCJmY18iLCAic2VlZHNfIiwgZmYpLCByb3cubmFtZXMgPSBGQUxTRSkKICBjYXQocCwgaW1wLCAiZmluaXNoZWQgaW4iLCByb3VuZChhcy5udW1lcmljKFN5cy50aW1lKCkgLSB0MCwgdW5pdHMgPSAic2VjcyIpKSwgInNcbiIpCn0KZmNfYWxsIDwtIGJpbmRfcm93cyhsYXBwbHkobGlzdC5maWxlcyhkaXIyLCBwYXR0ZXJuID0gIl5mY18uKlxcLmNzdiQiLCBmdWxsLm5hbWVzID0gVFJVRSksIHJlYWQuY3N2KSkgfD4KICBzZWxlY3QocG9sbHV0YW50LCBpbXB1dGF0aW9uLCBtb2RlbCwgb3JpZ2luLCBoLCB5LCB5aGF0KQpzZWVkX2ZpbGVzIDwtIGxpc3QuZmlsZXMoZGlyMiwgcGF0dGVybiA9ICJec2VlZHNfLipcXC5jc3YkIiwgZnVsbC5uYW1lcyA9IFRSVUUpCnNlZWRzX2FsbCA8LSBpZiAobGVuZ3RoKHNlZWRfZmlsZXMpKSBiaW5kX3Jvd3MobGFwcGx5KHNlZWRfZmlsZXMsIHJlYWQuY3N2KSkgZWxzZSBOVUxMCmNhdCgiRXhwZXJpbWVudCAyOiIsIG5yb3coZmNfYWxsKSwgImZvcmVjYXN0IHJvd3MgfCBtb2RlbHM6IiwgcGFzdGUoaW50ZXJzZWN0KE1PREVMUywgdW5pcXVlKGZjX2FsbCRtb2RlbCkpLCBjb2xsYXBzZSA9ICIsICIpLCAiXG4iKQpgYGAKCiMjIFJNU0UgYW5kIE1BRSBvbiB0aGUgdGVzdCBkYXlzCgokXG1hdGhybXtSTVNFfT1cYmlnKFxmcmFjezF9e3xcbWF0aGNhbCBUfH1cc3VtX3soXHRhdSxoKVxpblxtYXRoY2FsIFR9KFxoYXQgeV97XHRhdStofS15X3tcdGF1K2h9KV4yXGJpZyleezEvMn0kCm92ZXIgdGhlIHBhaXJzICRcbWF0aGNhbCBUJCB3aG9zZSB0YXJnZXQgaXMgb2JzZXJ2ZWQuIEJvbGQgbWFya3MgdGhlIGJlc3QgaW1wdXRhdGlvbiBmb3IgZWFjaCBtb2RlbC4KCmBgYHtyIHRhYmxlN30Kc2MgPC0gZmNfYWxsIHw+IGZpbHRlcighaXMubmEoeSkpIHw+IGdyb3VwX2J5KHBvbGx1dGFudCwgbW9kZWwsIGltcHV0YXRpb24pIHw+CiAgc3VtbWFyaXNlKFJNU0UgPSBzcXJ0KG1lYW4oKHloYXQgLSB5KV4yKSksIE1BRSA9IG1lYW4oYWJzKHloYXQgLSB5KSksIG4gPSBuKCksIC5ncm91cHMgPSAiZHJvcCIpCndyaXRlLmNzdihzYywgb3V0X2ZpbGUoImV4cDJfc2NvcmVzLmNzdiIpLCByb3cubmFtZXMgPSBGQUxTRSkKd2lkZTcgPC0gZnVuY3Rpb24obWV0cmljKSBzYyB8PiBzZWxlY3QocG9sbHV0YW50LCBtb2RlbCwgaW1wdXRhdGlvbiwgdiA9IGFsbF9vZihtZXRyaWMpKSB8PgogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSBpbXB1dGF0aW9uLCB2YWx1ZXNfZnJvbSA9IHYpIHw+CiAgbXV0YXRlKHBvbGx1dGFudCA9IGZhY3Rvcihwb2xsdXRhbnQsIGxldmVscyA9IFBPTFMpLCBtb2RlbCA9IGZhY3Rvcihtb2RlbCwgbGV2ZWxzID0gTU9ERUxTKSkgfD4gYXJyYW5nZShwb2xsdXRhbnQsIG1vZGVsKQptZDcgPC0gZnVuY3Rpb24obWV0cmljKSB7CiAgdyA8LSB3aWRlNyhtZXRyaWMpOyBpYyA8LSBpbnRlcnNlY3QoSU1QX0ZDLCBuYW1lcyh3KSk7IE0gPC0gYXMubWF0cml4KHdbLCBpY10pCiAgTWYgPC0gZG8uY2FsbChyYmluZCwgbGFwcGx5KHNlcV9sZW4obnJvdyhNKSksIGZ1bmN0aW9uKGkpIGZtdF9tZChNW2ksIF0sIGlmICh3JHBvbGx1dGFudFtpXSA9PSAiQ08iKSAzIGVsc2UgMikpKQogIGNvbG5hbWVzKE1mKSA8LSBpYzsgZGF0YS5mcmFtZShQb2xsdXRhbnQgPSBhcy5jaGFyYWN0ZXIodyRwb2xsdXRhbnQpLCBNb2RlbCA9IGFzLmNoYXJhY3Rlcih3JG1vZGVsKSwgTWYsIGNoZWNrLm5hbWVzID0gRkFMU0UpCn0KdzcgPC0gd2lkZTcoIlJNU0UiKTsgaWM3IDwtIGludGVyc2VjdChJTVBfRkMsIG5hbWVzKHc3KSk7IHJvd3M3IDwtIGMoKQpmb3IgKHAgaW4gaW50ZXJzZWN0KFBPTFMsIGFzLmNoYXJhY3Rlcih3NyRwb2xsdXRhbnQpKSkgewogIHcgPC0gdzcgfD4gZmlsdGVyKHBvbGx1dGFudCA9PSBwKTsgTSA8LSBhcy5tYXRyaXgod1ssIGljN10pOyBiZXN0X2FsbCA8LSBtaW4oTSwgbmEucm0gPSBUUlVFKTsgZGcgPC0gaWYgKHAgPT0gIkNPIikgMyBlbHNlIDIKICBmb3IgKGkgaW4gc2VxX2xlbihucm93KHcpKSkgewogICAgY2VsbHMgPC0gc2FwcGx5KHNlcV9hbG9uZyhpYzcpLCBmdW5jdGlvbihqKSB7IHYgPC0gTVtpLCBqXTsgcyA8LSBmbXRfbnVtKHYsIGRnKQogICAgICBpZiAoYWJzKHYgLSBtaW4oTVtpLCBdLCBuYS5ybSA9IFRSVUUpKSA8IDFlLTEyKSBzIDwtIHBhc3RlMCgiXFx0ZXh0YmZ7IiwgcywgIn0iKQogICAgICBpZiAoYWJzKHYgLSBiZXN0X2FsbCkgPCAxZS0xMikgcyA8LSBwYXN0ZTAoIlxcdW5kZXJsaW5leyIsIHMsICJ9Iik7IHMgfSkKICAgIGxlYWQgPC0gaWYgKGkgPT0gMSkgc3ByaW50ZigiXFxtdWx0aXJvd3slZH17Kn17JXN9IiwgbnJvdyh3KSwgUE9MX1RFWFtbcF1dKSBlbHNlICIiCiAgICByb3dzNyA8LSBjKHJvd3M3LCBwYXN0ZTAocGFzdGUoYyhsZWFkLCBNT0RFTF9URVhbW2FzLmNoYXJhY3Rlcih3JG1vZGVsW2ldKV1dLCBjZWxscyksIGNvbGxhcHNlID0gIiAmICIpLCAiIFxcXFwiKSkKICB9CiAgcm93czcgPC0gYyhyb3dzNywgIlxcbWlkcnVsZSIpCn0Kd3JpdGVfdGV4KGMoIlxcYmVnaW57dGFibGV9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgc3ByaW50ZigiXFxjYXB0aW9ue0RheS1haGVhZCBmb3JlY2FzdCBSTVNFIG9uIHRoZSAyMCB0ZXN0IGRheXMgKDEtLTIwIERlYyAyMDE4LCBvYnNlcnZlZCB2YWx1ZXMgb25seSkgZm9yIG1vZGVscyB0cmFpbmVkIG9uIGhpc3RvcmllcyBpbXB1dGVkIGJ5ICVkIG1ldGhvZHMuIEJvbGQ6IGJlc3QgaW1wdXRhdGlvbiBmb3IgZWFjaCBtb2RlbDsgdW5kZXJsaW5lZDogYmVzdCBtb2RlbC0taW1wdXRhdGlvbiBwYWlyIGZvciBlYWNoIHBvbGx1dGFudC59IiwgbGVuZ3RoKGljNykpLAogICJcXGxhYmVse3RhYjpmb3JlY2FzdH0iLCAiXFxzZXRsZW5ndGh7XFx0YWJjb2xzZXB9ezIuNnB0fSIsIHNwcmludGYoIlxcYmVnaW57dGFidWxhcn17bGwlc30iLCBzdHJyZXAoInIiLCBsZW5ndGgoaWM3KSkpLCAiXFx0b3BydWxlIiwKICBwYXN0ZTAoIiAmIE1vZGVsICYgIiwgcGFzdGUoaWM3LCBjb2xsYXBzZSA9ICIgJiAiKSwgIlxcXFwiKSwgIlxcbWlkcnVsZSIsIGhlYWQocm93czcsIC0xKSwKICAiXFxib3R0b21ydWxlIiwgIlxcZW5ke3RhYnVsYXJ9IiwgIlxcZW5ke3RhYmxlfSIpLCAidGFiX2ZvcmVjYXN0IikKa25pdHI6OmthYmxlKG1kNygiUk1TRSIpLCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9IGNhcCgiVGFibGUgOCIsICJEYXktYWhlYWQgUk1TRSBvbiB0aGUgMjAgdGVzdCBkYXlzIChvYnNlcnZlZCB2YWx1ZXMpIikpCmtuaXRyOjprYWJsZShtZDcoIk1BRSIpLCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9ICJEYXktYWhlYWQgTUFFIG9uIHRoZSAyMCB0ZXN0IGRheXMgKG9ic2VydmVkIHZhbHVlcykiKQpiZXN0cCA8LSBzYyB8PiBncm91cF9ieShwb2xsdXRhbnQpIHw+IHNsaWNlX21pbihSTVNFLCBuID0gMSwgd2l0aF90aWVzID0gRkFMU0UpIHw+IHVuZ3JvdXAoKSB8PgogIG11dGF0ZShwb2xsdXRhbnQgPSBmYWN0b3IocG9sbHV0YW50LCBsZXZlbHMgPSBQT0xTKSkgfD4gYXJyYW5nZShwb2xsdXRhbnQpCmZvciAoaSBpbiBzZXFfbGVuKG5yb3coYmVzdHApKSkgeyBwIDwtIGFzLmNoYXJhY3RlcihiZXN0cCRwb2xsdXRhbnRbaV0pCiAga2V5KHBhc3RlMCgiYmVzdHBhaXJfIiwgcCksIHBhc3RlKGJlc3RwJG1vZGVsW2ldLCBiZXN0cCRpbXB1dGF0aW9uW2ldKSk7IGtleShwYXN0ZTAoImJlc3RwYWlyX3Jtc2VfIiwgcCksIGJlc3RwJFJNU0VbaV0pIH0Ka25pdHI6OmthYmxlKGJlc3RwLCBkaWdpdHMgPSAzLCBjYXB0aW9uID0gIkJlc3QgbW9kZWwtaW1wdXRhdGlvbiBwYWlyIGZvciBlYWNoIHBvbGx1dGFudCIpCmBgYAoKIyMgQ2hhbmdlIHJlbGF0aXZlIHRvIG1lZGlhbiBpbXB1dGF0aW9uCgokXERlbHRhX3twLG0saX09MTAwXGJpZyhcbWF0aHJte1JNU0V9X3twLG0saX0vXG1hdGhybXtSTVNFfV97cCxtLFxtYXRocm17TUVESUFOfX0tMVxiaWcpJCBmb3IgcG9sbHV0YW50ICRwJCwKbW9kZWwgJG0kIGFuZCBpbXB1dGF0aW9uICRpJDsgYmx1ZSBjZWxscyBtZWFuIGEgbG93ZXIgZXJyb3IgdGhhbiB3aXRoIGEgbWVkaWFuLWltcHV0ZWQgaGlzdG9yeS4KCmBgYHtyIGZpZzksIGZpZy53aWR0aD03LjE2LCBmaWcuaGVpZ2h0PTIuM30KQUJCUiA8LSBjKE1FRElBTiA9ICJNRUQiLCBMSSA9ICJMSSIsIFNFQURFQyA9ICJTRUEiLCBLQUxNQU4gPSAiS0FMIiwgYFNlYXMtQVJgID0gIlMtQVIiKQpkOSA8LSBzYyB8PiBncm91cF9ieShwb2xsdXRhbnQsIG1vZGVsKSB8PiBtdXRhdGUoY2hhbmdlID0gMTAwICogKFJNU0UgLyBSTVNFW2ltcHV0YXRpb24gPT0gIk1FRElBTiJdIC0gMSkpIHw+IHVuZ3JvdXAoKSB8PgogIG11dGF0ZShwb2xsdXRhbnQgPSBmYWN0b3IocG9sbHV0YW50LCBsZXZlbHMgPSBQT0xTLCBsYWJlbHMgPSBjKCJQTVsxMF0iLCAiTk9bMl0iLCAiU09bMl0iLCAiQ08iLCAiT1szXSIpKSwKICAgICAgICAgbW9kZWwgPSBmYWN0b3IobW9kZWwsIGxldmVscyA9IHJldihNT0RFTFMpLCBsYWJlbHMgPSByZXYoYygiU2Vhc29uYWwgbmFpdmUiLCBNT0RFTFNbLTFdKSkpLAogICAgICAgICBpbXB1dGF0aW9uID0gZmFjdG9yKEFCQlJbaW1wdXRhdGlvbl0sIGxldmVscyA9IEFCQlIpKQpwOSA8LSBnZ3Bsb3QoZDksIGFlcyhpbXB1dGF0aW9uLCBtb2RlbCwgZmlsbCA9IHBtYXgocG1pbihjaGFuZ2UsIDMwKSwgLTMwKSkpICsgZ2VvbV90aWxlKGNvbG91ciA9ICJ3aGl0ZSIpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsID0gc3ByaW50ZigiJSsuMWYiLCBjaGFuZ2UpKSwgc2l6ZSA9IDEuOSwgZmFtaWx5ID0gRk9OVF9URVhUKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIobG93ID0gIiMyMTY2YWMiLCBoaWdoID0gIiNiMjE4MmIiLCBsaW1pdHMgPSBjKC0zMCwgMzApLCBuYW1lID0gIlJNU0UgY2hhbmdlXG52cyBNRURJQU4gKCUpIikgKwogIGZhY2V0X3dyYXAofnBvbGx1dGFudCwgbnJvdyA9IDEsIGxhYmVsbGVyID0gbGFiZWxfcGFyc2VkKSArIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMKSArCiAgdGhlbWUocGFuZWwuZ3JpZCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDYpLCBsZWdlbmQua2V5LmhlaWdodCA9IHVuaXQoMC41LCAiY20iKSkKcHJpbnQocDkpOyBzYXZlX2ZpZyhwOSwgImZpZzA5X2ZvcmVjYXN0X2hlYXRtYXAiLCA3LjE2LCAyLjMpCmBgYAoKIyMgRGllYm9sZOKAk01hcmlhbm8gdGVzdHMKCkVhY2ggY2VsbCBjb3VudHMgdGhlIHNldmVuIG1vZGVscyBhcyBXL1QvTDogU2Vhcy1BUiBzaWduaWZpY2FudGx5IGJldHRlciAvIG5vIHNpZ25pZmljYW50IGRpZmZlcmVuY2UgLwpzaWduaWZpY2FudGx5IHdvcnNlIGF0IHRoZSA1JSBsZXZlbC4gVGhlIGZpcnN0IGxpbmUgY2hlY2tzIGBkbV90ZXN0KClgIGFnYWluc3QgYGZvcmVjYXN0OjpkbS50ZXN0KClgLgoKYGBge3IgdGFibGU4fQpleF9yZWYgPC0gZmNfYWxsIHw+IGZpbHRlcihwb2xsdXRhbnQgPT0gIk5PMiIsIG1vZGVsID09ICJNTFAiLCBpbXB1dGF0aW9uID09ICJTZWFzLUFSIikgfD4gYXJyYW5nZShvcmlnaW4sIGgpCmV4X290aCA8LSBmY19hbGwgfD4gZmlsdGVyKHBvbGx1dGFudCA9PSAiTk8yIiwgbW9kZWwgPT0gIk1MUCIsIGltcHV0YXRpb24gPT0gIk1FRElBTiIpIHw+IGFycmFuZ2Uob3JpZ2luLCBoKQppZiAobnJvdyhleF9yZWYpICYmIG5yb3coZXhfb3RoKSkgewogIG9rIDwtICFpcy5uYShleF9yZWYkeSk7IGUxIDwtIChleF9yZWYkeWhhdCAtIGV4X3JlZiR5KVtva107IGUyIDwtIChleF9vdGgkeWhhdCAtIGV4X290aCR5KVtva10KICBtaW5lIDwtIGRtX3Rlc3QoZTEsIGUyLCBoID0gNDgpOyBmZG0gPC0gc3VwcHJlc3NXYXJuaW5ncyhmb3JlY2FzdDo6ZG0udGVzdChlMSwgZTIsIGggPSA0OCwgcG93ZXIgPSAyLCB2YXJlc3RpbWF0b3IgPSAiYWNmIikpCiAgY2F0KHNwcmludGYoIkNoZWNrIChOTzIsIE1MUCwgU2Vhcy1BUiB2cyBNRURJQU4pOiBkbV90ZXN0IERNID0gJS40ZiAocCA9ICUuM2cpIHwgZm9yZWNhc3Q6OmRtLnRlc3QgRE0gPSAlLjRmIChwID0gJS4zZylcbiIsCiAgICAgICAgICAgICAgbWluZVtbInN0YXQiXV0sIG1pbmVbWyJwIl1dLCBmZG0kc3RhdGlzdGljLCBmZG0kcC52YWx1ZSkpCn0KZG0gPC0gYmluZF9yb3dzKGxhcHBseShzcGxpdChmY19hbGwsIGxpc3QoZmNfYWxsJHBvbGx1dGFudCwgZmNfYWxsJG1vZGVsKSwgZHJvcCA9IFRSVUUpLCBmdW5jdGlvbihnKSB7CiAgcmVmIDwtIGcgfD4gZmlsdGVyKGltcHV0YXRpb24gPT0gIlNlYXMtQVIiKSB8PiBhcnJhbmdlKG9yaWdpbiwgaCkKICBpZiAoIW5yb3cocmVmKSkgcmV0dXJuKE5VTEwpCiAgYmluZF9yb3dzKGxhcHBseShzZXRkaWZmKHVuaXF1ZShnJGltcHV0YXRpb24pLCAiU2Vhcy1BUiIpLCBmdW5jdGlvbihpbXApIHsKICAgIG90aCA8LSBnIHw+IGZpbHRlcihpbXB1dGF0aW9uID09IGltcCkgfD4gYXJyYW5nZShvcmlnaW4sIGgpCiAgICByIDwtIGRtX3Rlc3QocmVmJHloYXQgLSByZWYkeSwgb3RoJHloYXQgLSBvdGgkeSwgaCA9IDQ4KQogICAgZGF0YS5mcmFtZShwb2xsdXRhbnQgPSByZWYkcG9sbHV0YW50WzFdLCBtb2RlbCA9IHJlZiRtb2RlbFsxXSwgY29tcGV0aXRvciA9IGltcCwgRE0gPSByW1sic3RhdCJdXSwgcCA9IHJbWyJwIl1dKQogIH0pKQp9KSkKd3JpdGUuY3N2KGRtLCBvdXRfZmlsZSgiZXhwMl9kbS5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCnd0bF9mIDwtIGZ1bmN0aW9uKGQpIGQgfD4gc3VtbWFyaXNlKFcgPSBzdW0ocCA8IC4wNSAmIERNIDwgMCksIEwgPSBzdW0ocCA8IC4wNSAmIERNID4gMCksIG4gPSBuKCksIC5ncm91cHMgPSAiZHJvcCIpIHw+CiAgbXV0YXRlKFQgPSBuIC0gVyAtIEwsIGNlbGwgPSBzcHJpbnRmKCIlZC8lZC8lZCIsIFcsIFQsIEwpKQp3dGwgPC0gYmluZF9yb3dzKGRtIHw+IGdyb3VwX2J5KHBvbGx1dGFudCwgY29tcGV0aXRvcikgfD4gd3RsX2YoKSwgZG0gfD4gZ3JvdXBfYnkoY29tcGV0aXRvcikgfD4gd3RsX2YoKSB8PiBtdXRhdGUocG9sbHV0YW50ID0gIlRvdGFsIikpCndyaXRlLmNzdih3dGwsIG91dF9maWxlKCJleHAyX3d0bC5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCmZvciAoaSBpbiB3aGljaCh3dGwkcG9sbHV0YW50ID09ICJUb3RhbCIpKSBrZXkocGFzdGUwKCJkbV90b3RhbF8iLCB3dGwkY29tcGV0aXRvcltpXSksIHd0bCRjZWxsW2ldKQpjb21wX29yZGVyIDwtIGludGVyc2VjdChzZXRkaWZmKElNUF9GQywgIlNlYXMtQVIiKSwgdW5pcXVlKGRtJGNvbXBldGl0b3IpKQp0OCA8LSB3dGwgfD4gc2VsZWN0KHBvbGx1dGFudCwgY29tcGV0aXRvciwgY2VsbCkgfD4gcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IGNvbXBldGl0b3IsIHZhbHVlc19mcm9tID0gY2VsbCkgfD4KICBtdXRhdGUocG9sbHV0YW50ID0gZmFjdG9yKHBvbGx1dGFudCwgbGV2ZWxzID0gYyhQT0xTLCAiVG90YWwiKSkpIHw+IGFycmFuZ2UocG9sbHV0YW50KQpyb3dzOCA8LSBzYXBwbHkoc2VxX2xlbihucm93KHQ4KSksIGZ1bmN0aW9uKGkpIHsgcCA8LSBhcy5jaGFyYWN0ZXIodDgkcG9sbHV0YW50W2ldKQogIHBhc3RlMChwYXN0ZShjKGlmIChwID09ICJUb3RhbCIpICJcXHRleHRpdHtUb3RhbH0iIGVsc2UgUE9MX1RFWFtbcF1dLCB1bmxpc3QodDhbaSwgY29tcF9vcmRlcl0pKSwgY29sbGFwc2UgPSAiICYgIiksICIgXFxcXCIpIH0pCndyaXRlX3RleChjKCJcXGJlZ2lue3RhYmxlfVshdF0iLCAiXFxjZW50ZXJpbmciLAogICJcXGNhcHRpb257RGllYm9sZC0tTWFyaWFubyB0ZXN0cyAoc3F1YXJlZCBsb3NzLCAkaD00OCQsIEhhcnZleS0tTGV5Ym91cm5lLS1OZXdib2xkIGNvcnJlY3Rpb24pIGNvbXBhcmluZyBmb3JlY2FzdHMgb2J0YWluZWQgd2l0aCBTZWFzLUFSLWltcHV0ZWQgaGlzdG9yaWVzIGFnYWluc3QgZWFjaCBhbHRlcm5hdGl2ZSBpbXB1dGF0aW9uLiBFYWNoIGNlbGwgY291bnRzIHRoZSBzZXZlbiBmb3JlY2FzdGluZyBtb2RlbHMgYXMgVy9UL0w6IFNlYXMtQVIgc2lnbmlmaWNhbnRseSBiZXR0ZXIgLyBubyBzaWduaWZpY2FudCBkaWZmZXJlbmNlIC8gc2lnbmlmaWNhbnRseSB3b3JzZSBhdCB0aGUgNVxcJSBsZXZlbC59IiwKICAiXFxsYWJlbHt0YWI6ZG19IiwgIlxcc2V0bGVuZ3Roe1xcdGFiY29sc2VwfXs1cHR9Iiwgc3ByaW50ZigiXFxiZWdpbnt0YWJ1bGFyfXtsJXN9Iiwgc3RycmVwKCJyIiwgbGVuZ3RoKGNvbXBfb3JkZXIpKSksICJcXHRvcHJ1bGUiLAogIHBhc3RlMCgiUG9sbHV0YW50ICYgIiwgcGFzdGUocGFzdGUoInZzIiwgY29tcF9vcmRlciksIGNvbGxhcHNlID0gIiAmICIpLCAiXFxcXCIpLCAiXFxtaWRydWxlIiwKICBoZWFkKHJvd3M4LCAtMSksICJcXG1pZHJ1bGUiLCB0YWlsKHJvd3M4LCAxKSwgIlxcYm90dG9tcnVsZSIsICJcXGVuZHt0YWJ1bGFyfSIsICJcXGVuZHt0YWJsZX0iKSwgInRhYl9kbSIpCmtuaXRyOjprYWJsZSh0OFssIGMoInBvbGx1dGFudCIsIGNvbXBfb3JkZXIpXSwgY29sLm5hbWVzID0gYygiUG9sbHV0YW50IiwgcGFzdGUoInZzIiwgY29tcF9vcmRlcikpLAogICAgICAgICAgICAgY2FwdGlvbiA9IGNhcCgiVGFibGUgOSIsICJEaWVib2xkLU1hcmlhbm8gdGVzdHM6IFNlYXMtQVIgdnMgZWFjaCBhbHRlcm5hdGl2ZSAoVy9UL0wgb3ZlciB0aGUgc2V2ZW4gbW9kZWxzKSIpKQpgYGAKCiMjIFN1bW1hcnkgb3ZlciBwb2xsdXRhbnRzIGFuZCBtb2RlbHMKCmBgYHtyIGV4cDItc3VtbWFyeX0KTTcgPC0gYXMubWF0cml4KHc3WywgaWM3XSk7IGJlc3RfaW1wIDwtIGljN1thcHBseShNNywgMSwgd2hpY2gubWluKV0Kcms3IDwtIGNvbE1lYW5zKHQoYXBwbHkoTTcsIDEsIHJhbmspKSkKY2hnIDwtIGNvbE1lYW5zKDEwMCAqIChNNyAvIE03WywgIk1FRElBTiJdIC0gMSkpCnN1bW0gPC0gZGF0YS5mcmFtZShpbXB1dGF0aW9uID0gaWM3LCBiZXN0X2ZvciA9IGFzLmludGVnZXIodGFibGUoZmFjdG9yKGJlc3RfaW1wLCBsZXZlbHMgPSBpYzcpKSksIGF2Z19yYW5rID0gcms3LCBtZWFuX2NoYW5nZV92c19tZWRpYW4gPSBjaGcpCndyaXRlLmNzdihzdW1tLCBvdXRfZmlsZSgiZXhwMl9zdW1tYXJ5LmNzdiIpLCByb3cubmFtZXMgPSBGQUxTRSkKZm9yIChpIGluIHNlcV9sZW4obnJvdyhzdW1tKSkpIHsga2V5KHBhc3RlMCgiZmNfYmVzdF8iLCBzdW1tJGltcHV0YXRpb25baV0pLCBzdW1tJGJlc3RfZm9yW2ldKTsga2V5KHBhc3RlMCgiZmNfcmFua18iLCBzdW1tJGltcHV0YXRpb25baV0pLCBzdW1tJGF2Z19yYW5rW2ldKQogIGtleShwYXN0ZTAoImZjX2NoZ18iLCBzdW1tJGltcHV0YXRpb25baV0pLCBzdW1tJG1lYW5fY2hhbmdlX3ZzX21lZGlhbltpXSkgfQpyZWw3IDwtIHc3OyBmb3IgKGNjIGluIGljNykgcmVsN1tbY2NdXSA8LSAxMDAgKiAodzdbW2NjXV0gLyB3NyRNRURJQU4gLSAxKQpjaGdfcCA8LSByZWw3IHw+IGdyb3VwX2J5KHBvbGx1dGFudCkgfD4gc3VtbWFyaXNlKGFjcm9zcyhhbGxfb2YoaWM3KSwgbWVhbiksIC5ncm91cHMgPSAiZHJvcCIpCmJlc3RjIDwtIHc3IHw+IG11dGF0ZShiZXN0ID0gYmVzdF9pbXApIHw+IGNvdW50KHBvbGx1dGFudCwgYmVzdCkgfD4gcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IGJlc3QsIHZhbHVlc19mcm9tID0gbiwgdmFsdWVzX2ZpbGwgPSAwKQp3cml0ZS5jc3YoY2hnX3AsIG91dF9maWxlKCJleHAyX2NoYW5nZV9ieV9wb2xsdXRhbnQuY3N2IiksIHJvdy5uYW1lcyA9IEZBTFNFKTsgd3JpdGUuY3N2KGJlc3RjLCBvdXRfZmlsZSgiZXhwMl9iZXN0Y291bnRfYnlfcG9sbHV0YW50LmNzdiIpLCByb3cubmFtZXMgPSBGQUxTRSkKZm9yIChpIGluIHNlcV9sZW4obnJvdyhjaGdfcCkpKSBmb3IgKGNjIGluIGljNykga2V5KHBhc3RlMCgiY2hnXyIsIGNoZ19wJHBvbGx1dGFudFtpXSwgIl8iLCBjYyksIGNoZ19wW1tjY11dW2ldKQprbml0cjo6a2FibGUoc3VtbSwgZGlnaXRzID0gMiwgY29sLm5hbWVzID0gYygiSW1wdXRhdGlvbiIsICJCZXN0IGZvciAocGFpcnMpIiwgIkF2ZXJhZ2UgcmFuayIsICJNZWFuIFJNU0UgY2hhbmdlIHZzIE1FRElBTiAoJSkiKSwKICAgICAgICAgICAgIGNhcHRpb24gPSBzcHJpbnRmKCJTdW1tYXJ5IG92ZXIgdGhlICVkIHBvbGx1dGFudC1tb2RlbCBwYWlycyIsIG5yb3coTTcpKSkKa25pdHI6OmthYmxlKGNoZ19wLCBkaWdpdHMgPSAyLCBjYXB0aW9uID0gIk1lYW4gUk1TRSBjaGFuZ2UgdnMgTUVESUFOICglKSBieSBwb2xsdXRhbnQiKQprbml0cjo6a2FibGUoYmVzdGMsIGNhcHRpb24gPSAiTnVtYmVyIG9mIG1vZGVscyBmb3Igd2hpY2ggZWFjaCBpbXB1dGF0aW9uIGlzIGJlc3QsIGJ5IHBvbGx1dGFudCIpCmBgYAoKIyMgRXhhbXBsZSBmb3JlY2FzdHMgZm9yIE5PJF8yJAoKVGhlIGJlc3QgbW9kZWwgd2l0aCBhIFNlYXMtQVIgaGlzdG9yeSBpcyBzaG93biBmb3IgdGhlIGZpcnN0IGZvdXIgdGVzdCBkYXlzLCBvbmUgcGFuZWwgcGVyIGltcHV0YXRpb246Cm9ic2VydmVkIHZhbHVlcyBpbiBncmV5LCBmb3JlY2FzdHMgaW4gY29sb3VyLCBhbmQgdGhlIHRlc3QgUk1TRSBvZiB0aGF0IG1vZGVs4oCTaW1wdXRhdGlvbiBwYWlyIGluIHRoZQpwYW5lbCB0aXRsZS4KCmBgYHtyIGZpZzEwLCBmaWcud2lkdGg9Ny4xNiwgZmlnLmhlaWdodD00LjJ9CmJlc3RfbSA8LSBzYyB8PiBmaWx0ZXIocG9sbHV0YW50ID09ICJOTzIiLCBpbXB1dGF0aW9uID09ICJTZWFzLUFSIikgfD4gc2xpY2VfbWluKFJNU0UsIG4gPSAxLCB3aXRoX3RpZXMgPSBGQUxTRSkgfD4gcHVsbChtb2RlbCkKcm0xMCA8LSBzYyB8PiBmaWx0ZXIocG9sbHV0YW50ID09ICJOTzIiLCBtb2RlbCA9PSBiZXN0X20pIHw+IG11dGF0ZShpbXB1dGF0aW9uID0gZmFjdG9yKGltcHV0YXRpb24sIGxldmVscyA9IElNUF9GQykpIHw+IGFycmFuZ2UoaW1wdXRhdGlvbikKbGFiMTAgPC0gc2V0TmFtZXMoc3ByaW50ZigiJXMgdHJhaW5lZCBvbiBhICVzLWltcHV0ZWQgaGlzdG9yeSAodGVzdCBSTVNFICUuMmYpIiwgYmVzdF9tLCBybTEwJGltcHV0YXRpb24sIHJtMTAkUk1TRSksIGFzLmNoYXJhY3RlcihybTEwJGltcHV0YXRpb24pKQprZXkoImZpZzEwX21vZGVsIiwgYmVzdF9tKQpkMTAgPC0gZmNfYWxsIHw+IGZpbHRlcihwb2xsdXRhbnQgPT0gIk5PMiIsIG1vZGVsID09IGJlc3RfbSwgb3JpZ2luIDwgbWluKG9yaWdpbikgKyA0ICogNDgpIHw+CiAgbXV0YXRlKHRpbWUgPSBUU1tvcmlnaW4gKyBoXSwgcGFuZWwgPSBmYWN0b3IobGFiMTBbaW1wdXRhdGlvbl0sIGxldmVscyA9IGxhYjEwKSkKcDEwIDwtIGdncGxvdChkMTAsIGFlcyh0aW1lKSkgKyBnZW9tX2xpbmUoYWVzKHkgPSB5KSwgY29sb3VyID0gImdyZXkzMCIsIGxpbmV3aWR0aCA9IDAuMzUsIG5hLnJtID0gVFJVRSkgKwogIGdlb21fbGluZShhZXMoeSA9IHloYXQsIGNvbG91ciA9IGltcHV0YXRpb24pLCBsaW5ld2lkdGggPSAwLjYpICsgZmFjZXRfd3JhcCh+cGFuZWwsIG5jb2wgPSAxKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBDT0xbSU1QX0ZDXSwgZ3VpZGUgPSAibm9uZSIpICsKICBzY2FsZV94X2RhdGV0aW1lKGRhdGVfYnJlYWtzID0gIjEgZGF5IiwgZGF0ZV9sYWJlbHMgPSAiJWQgJWIiKSArCiAgbGFicyh4ID0gTlVMTCwgeSA9IGV4cHJlc3Npb24oTk9bMl0gfiAiWyIgKiBtdSAqICJnL20iXjMgKiAiXSIpKQpwcmludChwMTApOyBzYXZlX2ZpZyhwMTAsICJmaWcxMF9mb3JlY2FzdF9leGFtcGxlIiwgNy4xNiwgNC4yKQpgYGAKCgoKCgoKCg==