cite: “P. Gc, R.-J. Singh, and R. Grewal, “Explainable AI for Stroke
Prediction: A Hybrid Bayesian Network Approach,” Jul.09, 2026,
Preprints: 2026070625. doi: 10.20944/preprints202607.0625.v1.”
updated on: July 28, 2026
00 Setup & reproducibility
library(tidyverse); library(bnlearn); library(caret); library(pROC)
library(mice); library(recipes); library(themis)
library(lme4); library(lmerTest) # factorial mixed model
library(xgboost) # opaque benchmark
library(vcd) # training-only association screen
GLOBAL_SEED <- 42
set.seed(GLOBAL_SEED)
# Inference is LIKELIHOOD WEIGHTING for every arm.
INFERENCE_METHOD <- "Monte Carlo likelihood weighting"
N_PARTICLES_PATIENT <- 1000 # per-patient posterior (matches Methods)
N_PARTICLES_QUERY <- 1e6 # population-level conditional-risk queries
01 Load & clean data
stroke_data <- read.csv("healthcare-dataset-stroke-data.csv")
stroke_data <- stroke_data %>%
filter(gender != "Other") %>% droplevels() %>%
mutate(bmi = as.numeric(as.character(bmi)),
across(c(stroke,hypertension,heart_disease,gender,ever_married,
work_type,Residence_type,smoking_status), as.factor))
PI_POP <- mean(stroke_data$stroke == "1")
N_TOTAL <- nrow(stroke_data)
cat(sprintf("N = %d | stroke PREVALENCE = %.2f%% | missing BMI = %d (%.1f%%)\n",
N_TOTAL, 100*PI_POP, sum(is.na(stroke_data$bmi)),
100*mean(is.na(stroke_data$bmi))))
N = 5109 | stroke PREVALENCE = 4.87% | missing BMI = 201 (3.9%)
03 Stratified split, absolute sizes, level harmonisation
split_stroke <- function(data, seed = GLOBAL_SEED, p_train = .6, p_val = .2) {
set.seed(seed)
s0 <- data %>% filter(stroke=="0"); s1 <- data %>% filter(stroke=="1")
n0 <- nrow(s0); n1 <- nrow(s1)
tr0 <- sample(n0, round(n0*p_train)); tr1 <- sample(n1, round(n1*p_train))
va0 <- sample(setdiff(seq_len(n0), tr0), round(n0*p_val))
va1 <- sample(setdiff(seq_len(n1), tr1), round(n1*p_val))
list(train = bind_rows(s0[tr0,], s1[tr1,]),
val = bind_rows(s0[va0,], s1[va1,]),
test = bind_rows(s0[setdiff(seq_len(n0), c(tr0,va0)),],
s1[setdiff(seq_len(n1), c(tr1,va1)),]))
}
# Guarantees every resample is analysable.
harmonise_levels <- function(tr, va, te) {
for (v in names(Filter(is.factor, tr))) {
lv <- Reduce(union, list(levels(factor(tr[[v]])), levels(factor(va[[v]])),
levels(factor(te[[v]]))))
tr[[v]] <- factor(as.character(tr[[v]]), levels = lv)
va[[v]] <- factor(as.character(va[[v]]), levels = lv)
te[[v]] <- factor(as.character(te[[v]]), levels = lv)
}
list(train = tr, val = va, test = te)
}
sp <- split_stroke(stroke_data); sp <- harmonise_levels(sp$train, sp$val, sp$test)
trainset <- sp$train; valset <- sp$val; testset <- sp$test
# Partition data
split_tbl <- tibble(
Partition = c("Training","Validation","Test"),
n = c(nrow(trainset), nrow(valset), nrow(testset)),
Events = c(sum(trainset$stroke=="1"), sum(valset$stroke=="1"), sum(testset$stroke=="1"))) %>%
mutate(Prevalence = sprintf("%.2f%%", 100*Events/n))
print(split_tbl); write.csv(split_tbl, "v3_split_sizes.csv", row.names=FALSE)
# A tibble: 3 × 4
Partition n Events Prevalence
<chr> <int> <int> <chr>
1 Training 3065 149 4.86%
2 Validation 1022 50 4.89%
3 Test 1022 50 4.89%
04 Preprocessing helpers (incl. Borderline-SMOTE)
# --- (a) Discretising imputer: median BMI from TRAIN only, then WHO/CDC/ADA bins
impute_baseline <- function(data, train_median_bmi) {
data %>%
mutate(bmi = as.numeric(as.character(bmi)),
bmi = ifelse(is.na(bmi), train_median_bmi, bmi)) %>%
mutate(age = cut(age, breaks=c(seq(0,85,by=5),Inf), right=FALSE,
labels=c("0-4","5-9","10-14","15-19","20-24","25-29","30-34","35-39",
"40-44","45-49","50-54","55-59","60-64","65-69","70-74",
"75-79","80-84","85+")),
bmi = cut(bmi, breaks=c(0,18.5,25,30,Inf), right=FALSE,
labels=c("Underweight","Normal","Overweight","Obese")),
avg_glucose_level = cut(avg_glucose_level, breaks=c(0,100,126,Inf),
right=FALSE, labels=c("Normal","Prediabetes","Diabetes"))) %>%
mutate(across(c(gender,hypertension,heart_disease,ever_married,work_type,
Residence_type,smoking_status,stroke), as.factor)) %>%
droplevels() %>%
select(-any_of("id"))
}
# --- (b) Continuous/hybrid prep: keep age/bmi/glucose numeric
prep_continuous <- function(df, train_median_bmi) {
df %>%
mutate(bmi = as.numeric(as.character(bmi)),
bmi = ifelse(is.na(bmi), train_median_bmi, bmi),
avg_glucose_level = as.numeric(avg_glucose_level),
age = as.numeric(age)) %>%
mutate(across(c(gender,hypertension,heart_disease,ever_married,work_type,
Residence_type,smoking_status,stroke), as.factor)) %>%
select(-any_of("id"))
}
# --- (c) SMOTENC synthetic oversampling for mixed (categorical+continuous) data
apply_smotenc <- function(df, target="stroke") {
df <- droplevels(as.data.frame(df))
rec <- recipe(formula(paste(target,"~ .")), data=df) %>%
step_smotenc(all_outcomes(), over_ratio=1, seed=GLOBAL_SEED) %>% prep()
droplevels(as.data.frame(juice(rec) %>%
mutate(across(where(is.numeric), as.numeric),
across(where(is.character), as.factor))))
}
05 Build the 8 ablation study arms (M1-M8)
# outcome retained for TRAINING imputation, excluded for VAL/TEST
# single imputation by chained equations (SICE)
M_IMP <- 1
impute_sice <- function(trainset, valset, testset, seed = GLOBAL_SEED) {
trainset$.split <- "train"; valset$.split <- "val"; testset$.split <- "test"
comb <- bind_rows(trainset, valset, testset) %>% select(-any_of("id"))
dat <- comb %>% select(-.split); ig <- comb$.split != "train"
pm_tr <- make.predictorMatrix(dat); pm_te <- pm_tr; pm_te[,"stroke"] <- 0
a <- complete(suppressWarnings(mice(dat, m=M_IMP, maxit=5, predictorMatrix=pm_tr,
ignore=ig, seed=seed, printFlag=FALSE)),1)
b <- complete(suppressWarnings(mice(dat, m=M_IMP, maxit=5, predictorMatrix=pm_te,
ignore=ig, seed=seed+1, printFlag=FALSE)),1)
list(train=a[comb$.split=="train",,drop=FALSE],
val =b[comb$.split=="val", ,drop=FALSE],
test =b[comb$.split=="test", ,drop=FALSE])
}
sice <- impute_sice(trainset, valset, testset)
med <- median(as.numeric(as.character(trainset$bmi)), na.rm=TRUE)
train_m1 <- impute_baseline(trainset, med); val_m1 <- impute_baseline(valset, med); test_m1 <- impute_baseline(testset, med)
train_m2 <- impute_baseline(sice$train, med); val_m2 <- impute_baseline(sice$val, med); test_m2 <- impute_baseline(sice$test, med)
train_m3 <- prep_continuous(trainset, med); val_m3 <- prep_continuous(valset, med); test_m3 <- prep_continuous(testset, med)
train_m4 <- apply_smotenc(train_m1); val_m4 <- val_m1; test_m4 <- test_m1
train_m5_base <- prep_continuous(sice$train, med)
train_m5 <- apply_smotenc(train_m5_base)
val_m5 <- prep_continuous(sice$val, med); test_m5 <- prep_continuous(sice$test, med)
train_m6 <- train_m5_base; val_m6 <- val_m5; test_m6 <- test_m5
train_m7 <- apply_smotenc(train_m3); val_m7 <- val_m3; test_m7 <- test_m3
# [R1.2c] M8 now uses SICE val/test (v1 wrongly used median-imputed sets)
train_m8 <- apply_smotenc(train_m2); val_m8 <- val_m2; test_m8 <- test_m2
cat("Eight arms (2x2x2) built with leakage-safe imputation.\n")
Eight arms (2x2x2) built with leakage-safe imputation.
06 Association screen on training data, supplementary use only
assoc_tbl <- bind_rows(
map_dfr(c("gender","hypertension","heart_disease","ever_married","work_type",
"Residence_type","smoking_status"),
~data.frame(Variable=.x, Type="categorical",
Assoc=round(assocstats(table(train_m3[[.x]], train_m3$stroke))$cramer,4))),
map_dfr(c("age","avg_glucose_level","bmi"),
~data.frame(Variable=.x, Type="continuous",
Assoc=round(cor(as.numeric(train_m3[[.x]]),
as.numeric(as.character(train_m3$stroke)), use="complete.obs"),4)))
) %>% arrange(desc(abs(Assoc)))
print(assoc_tbl); write.csv(assoc_tbl,"v3_supp_assoc_TRAIN_ONLY.csv",row.names=FALSE)
Variable Type Assoc
1 age continuous 0.2410
2 avg_glucose_level continuous 0.1318
3 hypertension categorical 0.1132
4 heart_disease categorical 0.1071
5 ever_married categorical 0.0964
6 work_type categorical 0.0926
7 smoking_status categorical 0.0750
8 bmi continuous 0.0356
9 Residence_type categorical 0.0250
10 gender categorical 0.0008
07 Structural constraints — specified a priori from the
literature
all_nodes <- c("age","gender","hypertension","heart_disease","ever_married",
"work_type","Residence_type","avg_glucose_level","bmi",
"smoking_status","stroke")
discrete_nodes <- setdiff(all_nodes, c("age","avg_glucose_level","bmi","stroke"))
wl_discrete <- matrix(c("age","stroke","heart_disease","stroke",
"hypertension","stroke","avg_glucose_level","stroke"),
ncol=2, byrow=TRUE, dimnames=list(NULL,c("from","to")))
bl_discrete <- bind_rows(
expand.grid(from=all_nodes, to=c("age","gender"), stringsAsFactors=FALSE) %>% filter(from!=to),
expand.grid(from="stroke", to=all_nodes, stringsAsFactors=FALSE) %>% filter(to!="stroke")
) %>% as.matrix(); colnames(bl_discrete) <- c("from","to")
# continuous nodes cannot parent discrete nodes -> arcs reverse
wl_hybrid <- matrix(c("stroke","age","heart_disease","stroke",
"hypertension","stroke","stroke","avg_glucose_level"),
ncol=2, byrow=TRUE, dimnames=list(NULL,c("from","to")))
bl_hybrid <- bind_rows(
expand.grid(from=all_nodes, to="gender", stringsAsFactors=FALSE) %>% filter(from!=to),
expand.grid(from="stroke", to=discrete_nodes, stringsAsFactors=FALSE) %>% filter(to!="stroke")
) %>% as.matrix(); colnames(bl_hybrid) <- c("from","to")
cat("Whitelist and blacklist create based on literature. \n")
Whitelist and blacklist create based on literature.
08 Core evaluation function
evaluate_variant <- function(train_df, val_df, test_df, model_name, boot_R = 200) {
is_disc <- all(sapply(train_df, is.factor))
wl <- if (is_disc) wl_discrete else wl_hybrid
bl <- if (is_disc) bl_discrete else bl_hybrid
dag <- hc(train_df, whitelist=wl, blacklist=bl)
arc_str <- if (boot_R>0) boot.strength(train_df, R=boot_R, algorithm="hc",
algorithm.args=list(whitelist=wl, blacklist=bl)) else NULL
fitted <- if (is_disc) bn.fit(dag, train_df, method="bayes", iss=10) else bn.fit(dag, train_df)
# [R1.5b] likelihood weighting for BOTH arm types
get_probs <- function(td, fit=fitted, disc=is_disc) {
if (disc) { pp <- predict(fit, node="stroke", data=td, method="bayes-lw", prob=TRUE)
return(attr(pp,"prob")["1",]) }
ec <- setdiff(names(td),"stroke")
vapply(seq_len(nrow(td)), function(i){
p <- suppressWarnings(cpquery(fit, event=(stroke=="1"),
evidence=as.list(td[i,ec]), method="lw", n=N_PARTICLES_PATIENT))
if (is.na(p)) 0 else p}, numeric(1))
}
vp <- get_probs(val_df); roc_v <- roc(val_df$stroke, vp, levels=c("0","1"), quiet=TRUE)
th <- coords(roc_v,"best",ret="threshold",best.method="youden")$threshold[1]
if (is.na(th)) th <- 0.5
tp <- get_probs(test_df); roc_t <- roc(test_df$stroke, tp, levels=c("0","1"), quiet=TRUE)
cm <- confusionMatrix(factor(ifelse(tp>=th,"1","0"),levels=c("0","1")),
test_df$stroke, positive="1")
list(results=data.frame(Model=model_name, Val_Threshold=round(th,4),
Val_AUC=round(as.numeric(auc(roc_v)),4),
Test_AUC=round(as.numeric(auc(roc_t)),4),
Test_Sensitivity=round(cm$byClass["Sensitivity"],4),
Test_Specificity=round(cm$byClass["Specificity"],4),
Test_Youden_J=round(cm$byClass["Sensitivity"]+cm$byClass["Specificity"]-1,4),
Test_F1=round(as.numeric(cm$byClass["F1"]),4)),
roc=roc_t, roc_val=roc_v, fit=fitted, structure=dag, arc_strength=arc_str,
confusion_matrix=cm, is_disc=is_disc, wl=wl, bl=bl, train=train_df, test=test_df)
}
09 Main result — train arms, then select on VALIDATION
experiments <- list(
list(train_m1,val_m1,test_m1,"M1: Baseline"),
list(train_m2,val_m2,test_m2,"M2: SICE + Discretize"),
list(train_m3,val_m3,test_m3,"M3: Continuous Nodes"),
list(train_m4,val_m4,test_m4,"M4: SMOTE on Baseline"),
list(train_m5,val_m5,test_m5,"M5: ALL (SICE + Cont + SMOTE)"),
list(train_m6,val_m6,test_m6,"M6: SICE + Continuous"),
list(train_m7,val_m7,test_m7,"M7: Continuous + SMOTE"),
list(train_m8,val_m8,test_m8,"M8: SICE + SMOTE"))
ablation <- data.frame(); roc_list <- list(); dags <- list()
arcs_l <- list(); cms <- list(); fits <- list(); runs <- list()
for (ex in experiments) {
tryCatch({
r <- evaluate_variant(ex[[1]],ex[[2]],ex[[3]],ex[[4]],boot_R=200)
ablation <- rbind(ablation, r$results); roc_list[[ex[[4]]]] <- r$roc
dags[[ex[[4]]]] <- r$structure; arcs_l[[ex[[4]]]] <- r$arc_strength
cms[[ex[[4]]]] <- r$confusion_matrix; fits[[ex[[4]]]] <- r$fit; runs[[ex[[4]]]] <- r
}, error=function(e) cat(sprintf("Error %s: %s\n", ex[[4]], e$message)))
}
leaderboard <- ablation
leaderboard$Test_AUC_95_CI <- vapply(leaderboard$Model, function(m){
ci <- ci.auc(roc_list[[m]], method="bootstrap", boot.n=2000, quiet=TRUE)
sprintf("[%.4f - %.4f]", ci[1], ci[3])}, character(1))
# selection on VALIDATION only, with the 0.005 parsimony rule
vr <- leaderboard %>% arrange(desc(Val_AUC))
near <- vr %>% filter(Val_AUC >= max(Val_AUC) - 0.005)
simple_rank <- c("M1","M3","M2","M6","M4","M7","M8","M5")
near$simp <- match(sub(":.*","",near$Model), simple_rank)
PRIMARY <- near$Model[which.min(near$simp)]
cat("\nBest validation AUC:", vr$Model[1], sprintf("(%.4f)", vr$Val_AUC[1]),
"\nPRIMARY after 0.005 parsimony rule:", PRIMARY,
"\nIts unbiased TEST AUC:", leaderboard$Test_AUC[leaderboard$Model==PRIMARY], "\n")
Best validation AUC: M6: SICE + Continuous (0.8683)
PRIMARY after 0.005 parsimony rule: M3: Continuous Nodes
Its unbiased TEST AUC: 0.8109
leaderboard <- leaderboard %>% relocate(Test_AUC_95_CI,.after=Test_AUC) %>%
arrange(desc(Test_AUC), desc(Test_Youden_J))
print(leaderboard); write.csv(leaderboard,"v3_leaderboard.csv",row.names=FALSE)
Model Val_Threshold Val_AUC Test_AUC Test_AUC_95_CI Test_Sensitivity
Sensitivity2 M3: Continuous Nodes 0.0857 0.8642 0.8109 [0.7612 - 0.8533] 0.62
Sensitivity5 M6: SICE + Continuous 0.0951 0.8683 0.8094 [0.7615 - 0.8522] 0.58
Sensitivity6 M7: Continuous + SMOTE 0.4303 0.7602 0.7456 [0.6672 - 0.8126] 0.72
Sensitivity4 M5: ALL (SICE + Cont + SMOTE) 0.3022 0.7500 0.7381 [0.6688 - 0.8048] 0.76
Sensitivity M1: Baseline 0.0350 0.7710 0.7235 [0.6542 - 0.7907] 0.68
Sensitivity1 M2: SICE + Discretize 0.0330 0.7774 0.7108 [0.6376 - 0.7805] 0.68
Sensitivity3 M4: SMOTE on Baseline 0.5530 0.7509 0.6903 [0.6128 - 0.7630] 0.50
Sensitivity7 M8: SICE + SMOTE 0.5350 0.7392 0.6855 [0.6084 - 0.7617] 0.52
Test_Specificity Test_Youden_J Test_F1
Sensitivity2 0.7912 0.4112 0.2183
Sensitivity5 0.8035 0.3835 0.2148
Sensitivity6 0.7109 0.4309 0.1962
Sensitivity4 0.6626 0.4226 0.1827
Sensitivity 0.6584 0.3384 0.1635
Sensitivity1 0.6348 0.3148 0.1549
Sensitivity3 0.7747 0.2747 0.1701
Sensitivity7 0.7623 0.2823 0.1694
09b Figure 4 — DAG of the primary model, with M3-vs-M6 structural
check
library(bnlearn)
library(igraph)
library(ggraph)
library(ggplot2)
library(dplyr)
# ==============================================================================
# 1. Primary Model Setup & Structural Check
# ==============================================================================
dag_primary <- dags[[PRIMARY]]
astr_primary <- arcs_l[[PRIMARY]]
cat("Plotting DAG for PRIMARY =", PRIMARY, "\n")
Plotting DAG for PRIMARY = M3: Continuous Nodes
cat("Parents of stroke:", paste(parents(dag_primary, "stroke"), collapse = ", "), "\n")
Parents of stroke: hypertension, heart_disease, ever_married
cat("Children of stroke:", paste(children(dag_primary, "stroke"), collapse = ", "), "\n")
Children of stroke: age, avg_glucose_level
# Structural check: Compare PRIMARY against M6
M6_NAME <- "M6: SICE + Continuous"
if (PRIMARY != M6_NAME && M6_NAME %in% names(dags)) {
a_primary <- apply(arcs(dags[[PRIMARY]]), 1, paste, collapse = "->")
a_m6 <- apply(arcs(dags[[M6_NAME]]), 1, paste, collapse = "->")
only_primary <- setdiff(a_primary, a_m6)
only_m6 <- setdiff(a_m6, a_primary)
identical_structures <- length(only_primary) == 0 && length(only_m6) == 0
cat("\n[Figure 4 check] Is", PRIMARY, "structurally identical to", M6_NAME, "?\n")
cat(" Identical:", identical_structures, "\n")
if (!identical_structures) {
cat(" Arcs in", PRIMARY, "but not", M6_NAME, ":\n"); print(only_primary)
cat(" Arcs in", M6_NAME, "but not", PRIMARY, ":\n"); print(only_m6)
}
write.csv(data.frame(only_in_primary = paste(only_primary, collapse = "; "),
only_in_m6 = paste(only_m6, collapse = "; "),
identical = identical_structures),
"v3_dag_M3_vs_M6_check.csv", row.names = FALSE)
}
[Figure 4 check] Is M3: Continuous Nodes structurally identical to M6: SICE + Continuous ?
Identical: TRUE
# ==============================================================================
# 2. Publication-Quality DAG Rendering via ggraph
# ==============================================================================
# Convert bnlearn DAG to igraph object
ig <- as.igraph(dag_primary)
# Map bootstrap arc strengths onto igraph edges
if (!is.null(astr_primary)) {
str_df <- as.data.frame(astr_primary)
e_mat <- as_edgelist(ig)
edge_strengths <- numeric(nrow(e_mat))
for (i in seq_len(nrow(e_mat))) {
u <- e_mat[i, 1]
v <- e_mat[i, 2]
match_idx <- which(str_df$from == u & str_df$to == v)
if (length(match_idx) > 0) {
edge_strengths[i] <- str_df$strength[match_idx[1]]
} else {
edge_strengths[i] <- 0.5
}
}
E(ig)$strength <- edge_strengths
} else {
E(ig)$strength <- 1.0
}
# Clean variable display labels
clean_labels <- c(
"stroke" = "Stroke\n(Target)",
"age" = "Age",
"bmi" = "BMI",
"avg_glucose_level" = "Avg. Glucose",
"hypertension" = "Hypertension",
"heart_disease" = "Heart Disease",
"ever_married" = "Ever Married",
"work_type" = "Work Type",
"smoking_status" = "Smoking Status",
"gender" = "Gender",
"Residence_type" = "Residence Type"
)
# Define visual node roles
node_types <- c(
"stroke" = "Target Outcome",
"age" = "Continuous Predictor",
"bmi" = "Continuous Predictor",
"avg_glucose_level" = "Continuous Predictor",
"hypertension" = "Categorical Predictor",
"heart_disease" = "Categorical Predictor",
"ever_married" = "Categorical Predictor",
"work_type" = "Categorical Predictor",
"smoking_status" = "Categorical Predictor",
"gender" = "Categorical Predictor",
"Residence_type" = "Categorical Predictor"
)
v_names <- V(ig)$name
V(ig)$label <- ifelse(v_names %in% names(clean_labels), clean_labels[v_names], v_names)
V(ig)$type <- ifelse(v_names %in% names(node_types), node_types[v_names], "Categorical Predictor")
V(ig)$type <- factor(V(ig)$type, levels = c("Target Outcome", "Continuous Predictor", "Categorical Predictor"))
# Plot using hierarchical Sugiyama layout
set.seed(if (exists("GLOBAL_SEED")) GLOBAL_SEED else 42)
p_dag <- ggraph(ig, layout = "sugiyama") +
# Directed edges with strength-scaled thickness
geom_edge_link(
aes(edge_width = strength),
arrow = arrow(length = unit(3.5, "mm"), type = "closed"),
end_cap = circle(10, "mm"),
start_cap = circle(10, "mm"),
color = "#4A5568",
alpha = 0.75
) +
scale_edge_width_continuous(
range = c(0.6, 2.2),
name = "Bootstrap Arc Strength"
) +
# Node styling
geom_node_point(aes(color = type), size = 20) +
geom_node_text(aes(label = label), fontface = "bold", size = 2.8, color = "#1A202C") +
scale_color_manual(
values = c(
"Target Outcome" = "#E53E3E", # Red/Coral fill
"Continuous Predictor" = "#3182CE", # Soft Blue fill
"Categorical Predictor" = "#E2E8F0" # Light Slate Gray fill
),
name = "Node Category"
) +
theme_void() +
theme(
legend.position = "bottom",
legend.box = "horizontal",
legend.title = element_text(face = "bold", size = 10),
legend.text = element_text(size = 9),
plot.margin = margin(15, 15, 15, 15)
)
# Export clean 300 DPI image
ggsave("v3_DAG_primary.png", plot = p_dag, width = 10, height = 7.5, dpi = 300)
cat("Saved publication-ready v3_DAG_primary.png\n")
Saved publication-ready v3_DAG_primary.png
# ==============================================================================
# 3. Model Diagnostics & Logs
# ==============================================================================
cat("\nModel string:\n", modelstring(dag_primary), "\n")
Model string:
[gender][Residence_type][heart_disease|gender][ever_married|heart_disease][work_type|ever_married][hypertension|work_type][smoking_status|work_type][stroke|hypertension:heart_disease:ever_married][age|ever_married:work_type:stroke][bmi|age:work_type][avg_glucose_level|age:ever_married:bmi:stroke]
cat("Full arc list (from -> to):\n"); print(arcs(dag_primary))
Full arc list (from -> to):
from to
[1,] "stroke" "age"
[2,] "heart_disease" "stroke"
[3,] "hypertension" "stroke"
[4,] "stroke" "avg_glucose_level"
[5,] "work_type" "age"
[6,] "ever_married" "work_type"
[7,] "work_type" "bmi"
[8,] "work_type" "smoking_status"
[9,] "ever_married" "age"
[10,] "ever_married" "avg_glucose_level"
[11,] "work_type" "hypertension"
[12,] "age" "avg_glucose_level"
[13,] "bmi" "avg_glucose_level"
[14,] "heart_disease" "ever_married"
[15,] "age" "bmi"
[16,] "gender" "heart_disease"
[17,] "ever_married" "stroke"
cat("\nMarkov blanket of stroke:", paste(mb(dag_primary, "stroke"), collapse = ", "), "\n")
Markov blanket of stroke: age, hypertension, heart_disease, ever_married, work_type, avg_glucose_level, bmi
11 Calibration — reference model, weak calibration, prior
correction
#A prevalence-only model attains Brier ~= p(1-p) ~= 0.047 here, so raw
# Brier alone cannot demonstrate calibration. BSS, intercept and slope are added.
calib_metrics <- function(y,p,eps=1e-6){
y <- as.numeric(as.character(y)); p <- pmin(pmax(p,eps),1-eps)
brier <- mean((p-y)^2); ref <- mean((mean(y)-y)^2); lp <- qlogis(p)
data.frame(Brier=round(brier,4), Brier_ref=round(ref,4),
BSS=round(1-brier/ref,4),
Calib_intercept=round(unname(coef(glm(y~offset(lp),family=binomial))[1]),3),
Calib_slope=round(unname(coef(glm(y~lp,family=binomial))[2]),3))
}
prior_correct <- function(p, pi_train=0.5, pi_pop=PI_POP, eps=1e-6){
p <- pmin(pmax(p,eps),1-eps)
plogis(qlogis(p) - log((pi_train/(1-pi_train))/(pi_pop/(1-pi_pop))))
}
SMOTE_ARMS <- c("M4","M5","M7","M8")
calib <- map_dfr(names(roc_list), function(m){
y <- roc_list[[m]]$response; p <- roc_list[[m]]$predictor
out <- cbind(Model=m, Correction="none", calib_metrics(y,p))
if (any(sapply(SMOTE_ARMS, function(a) startsWith(m,a))))
out <- bind_rows(out, cbind(Model=m, Correction="prior-corrected",
calib_metrics(y, prior_correct(p))))
out })
print(calib); write.csv(calib,"v3_calibration.csv",row.names=FALSE)
Model Correction Brier Brier_ref BSS Calib_intercept Calib_slope
1 M1: Baseline none 0.0504 0.0465 -0.0824 -0.206 0.257
2 M2: SICE + Discretize none 0.0509 0.0465 -0.0941 -0.217 0.240
3 M3: Continuous Nodes none 0.0561 0.0465 -0.2049 -0.643 0.344
4 M4: SMOTE on Baseline none 0.1735 0.0465 -2.7279 -3.012 0.153
5 M4: SMOTE on Baseline prior-corrected 0.0489 0.0465 -0.0519 -0.041 0.174
6 M5: ALL (SICE + Cont + SMOTE) none 0.1756 0.0465 -2.7739 -3.282 0.174
7 M5: ALL (SICE + Cont + SMOTE) prior-corrected 0.0542 0.0465 -0.1651 -0.311 0.193
8 M6: SICE + Continuous none 0.0564 0.0465 -0.2132 -0.651 0.345
9 M7: Continuous + SMOTE none 0.1768 0.0465 -2.8007 -3.401 0.165
10 M7: Continuous + SMOTE prior-corrected 0.0552 0.0465 -0.1859 -0.430 0.188
11 M8: SICE + SMOTE none 0.1750 0.0465 -2.7611 -3.049 0.139
12 M8: SICE + SMOTE prior-corrected 0.0493 0.0465 -0.0593 -0.077 0.169
calib_curve <- function(m,bins=10){
y <- as.numeric(as.character(roc_list[[m]]$response)); p <- roc_list[[m]]$predictor
br <- unique(quantile(p, probs=seq(0,1,length.out=bins+1), na.rm=TRUE))
gg <- if (length(br)<3) cut(p, breaks=unique(c(-Inf,sort(unique(p)))), include.lowest=TRUE)
else cut(p, breaks=br, include.lowest=TRUE)
na.omit(data.frame(model=m, pred=tapply(p,gg,mean), obs=tapply(y,gg,mean)))
}
focus <- intersect(c("M1: Baseline","M3: Continuous Nodes","M6: SICE + Continuous",
"M5: ALL (SICE + Cont + SMOTE)"), names(roc_list))
cp <- ggplot(bind_rows(lapply(focus,calib_curve)), aes(pred,obs,colour=model)) +
geom_abline(slope=1,intercept=0,linetype="dashed",colour="grey50") +
geom_hline(yintercept=PI_POP, linetype="dotted", colour="red") +
geom_line()+geom_point() +
labs(x="Mean predicted probability", y="Observed stroke fraction",
caption="Dotted red line: prevalence-only reference") + theme_minimal()
ggsave("v3_calibration.png", cp, width=7, height=6, dpi=300); print(cp)

12 Comparison with alternative models (transparent + opaque) and
SHAP
bt <- train_m3; bv <- val_m3; be <- test_m3
# 1. Logistic Regression
lr <- glm(stroke ~ ., data = bt, family = binomial)
# FIX: Save the actual ROC object to 'roc_lr'
roc_lr <- roc(be$stroke, predict(lr, be, type = "response"), levels = c("0", "1"), quiet = TRUE)
lr_auc <- as.numeric(auc(roc_lr))
# 2. XGBoost
mm <- function(d) model.matrix(stroke ~ . -1, data = d)
ft <- Reduce(intersect, list(colnames(mm(bt)), colnames(mm(bv)), colnames(mm(be))))
dtr <- xgb.DMatrix(mm(bt)[, ft, drop = FALSE], label = as.numeric(as.character(bt$stroke)))
dva <- xgb.DMatrix(mm(bv)[, ft, drop = FALSE], label = as.numeric(as.character(bv$stroke)))
dte <- xgb.DMatrix(mm(be)[, ft, drop = FALSE], label = as.numeric(as.character(be$stroke)))
SPW <- sum(bt$stroke == "0") / sum(bt$stroke == "1")
XGB_PARAMS <- list(
objective = "binary:logistic",
eval_metric = "auc",
max_depth = 3,
learning_rate = 0.05,
subsample = 0.8,
colsample_bytree = 0.8,
scale_pos_weight = SPW
)
set.seed(GLOBAL_SEED)
xgb <- xgb.train(
params = XGB_PARAMS,
data = dtr,
nrounds = 1000,
watchlist = list(val = dva),
early_stopping_rounds = 25,
verbose = 0
)
# FIX: Save the actual ROC object to 'roc_xgb'
roc_xgb <- roc(be$stroke, predict(xgb, dte), levels = c("0", "1"), quiet = TRUE)
xgb_auc <- as.numeric(auc(roc_xgb))
# Hyperparameter output for Methods
cat("\n===== XGBoost specification (paste into Methods) =====\n")
===== XGBoost specification (paste into Methods) =====
cat(sprintf("objective=%s; eval_metric=%s; max_depth=%d; learning_rate=%.2f;\n",
XGB_PARAMS$objective, XGB_PARAMS$eval_metric, XGB_PARAMS$max_depth, XGB_PARAMS$learning_rate))
objective=binary:logistic; eval_metric=auc; max_depth=3; learning_rate=0.05;
cat(sprintf("subsample=%.1f; colsample_bytree=%.1f; scale_pos_weight=%.3f;\n",
XGB_PARAMS$subsample, XGB_PARAMS$colsample_bytree, SPW))
subsample=0.8; colsample_bytree=0.8; scale_pos_weight=19.570;
cat(sprintf("early_stopping_rounds=25; n_rounds_selected=%d; seed=%d\n",
xgb$best_iteration, GLOBAL_SEED))
bench <- data.frame(
Model = c(paste0("Bayesian network (", PRIMARY, ")"),
"Logistic regression (transparent)",
"XGBoost (opaque)"),
Test_AUC = round(c(leaderboard$Test_AUC[leaderboard$Model == PRIMARY], lr_auc, xgb_auc), 4)
)
print(bench)
Model Test_AUC
1 Bayesian network (M3: Continuous Nodes) 0.8109
2 Logistic regression (transparent) 0.8188
3 XGBoost (opaque) 0.8222
write.csv(bench, "v3_benchmark.csv", row.names = FALSE)
# [R3.15] SHAP for the opaque benchmark
sv <- predict(xgb, dte, predcontrib = TRUE)
imp <- sort(colMeans(abs(sv[, setdiff(colnames(sv), "BIAS"), drop = FALSE])), decreasing = TRUE)
print(round(head(imp, 10), 5))
age bmi avg_glucose_level genderFemale
0.85756 0.15479 0.10572 0.02464
hypertension1 work_typeGovt_job smoking_statussmokes (Intercept)
0.02349 0.01622 0.01375 0.01166
smoking_statusnever smoked work_typeSelf-employed
0.01138 0.01059
write.csv(data.frame(feature = names(imp), mean_abs_shap = round(imp, 5)),
"v3_shap.csv", row.names = FALSE)
13 Hypothesis testing — DeLong and McNemar (complete set)
thr <- function(m) leaderboard$Val_Threshold[leaderboard$Model==m]
mcn <- function(A,B){
y <- roc_list[[A]]$response
pa <- ifelse(roc_list[[A]]$predictor>=thr(A),1,0)
pb <- ifelse(roc_list[[B]]$predictor>=thr(B),1,0); i <- which(y==1)
p <- suppressWarnings(mcnemar.test(table(factor(pa[i],0:1),factor(pb[i],0:1)))$p.value)
cat(sprintf("McNemar %-30s vs %-30s p=%.4f\n",A,B,p)); p
}
dl <- function(A,B){
t <- roc.test(roc_list[[A]], roc_list[[B]], method="delong")
cat(sprintf("DeLong %-30s vs %-30s Z=%6.3f p=%.4f\n",A,B,
as.numeric(t$statistic),t$p.value)); c(Z=as.numeric(t$statistic),p=t$p.value)
}
M5 <- "M5: ALL (SICE + Cont + SMOTE)"
cat("\n== H1 topology ==\n"); dl("M3: Continuous Nodes","M1: Baseline"); dl("M6: SICE + Continuous","M2: SICE + Discretize")
== H1 topology ==
DeLong M3: Continuous Nodes vs M1: Baseline Z= 2.402 p=0.0163
Z p
2.40204355 0.01630377
DeLong M6: SICE + Continuous vs M2: SICE + Discretize Z= 2.630 p=0.0085
Z p
2.629591796 0.008548745
mcn("M3: Continuous Nodes","M1: Baseline"); mcn("M6: SICE + Continuous","M2: SICE + Discretize")
McNemar M3: Continuous Nodes vs M1: Baseline p=0.6276
[1] 0.6276258
McNemar M6: SICE + Continuous vs M2: SICE + Discretize p=0.3827
[1] 0.3827331
cat("\n== H2 imputation ==\n"); dl("M2: SICE + Discretize","M1: Baseline"); dl("M6: SICE + Continuous","M3: Continuous Nodes")
== H2 imputation ==
DeLong M2: SICE + Discretize vs M1: Baseline Z=-1.399 p=0.1618
Z p
-1.3989748 0.1618205
DeLong M6: SICE + Continuous vs M3: Continuous Nodes Z=-0.297 p=0.7667
Z p
-0.2967460 0.7666604
cat("\n== H3 balancing ==\n"); dl("M4: SMOTE on Baseline","M1: Baseline"); dl(M5,"M6: SICE + Continuous")
== H3 balancing ==
DeLong M4: SMOTE on Baseline vs M1: Baseline Z=-1.893 p=0.0584
Z p
-1.89266368 0.05840261
DeLong M5: ALL (SICE + Cont + SMOTE) vs M6: SICE + Continuous Z=-2.060 p=0.0394
Z p
-2.05954502 0.03944206
mcn("M4: SMOTE on Baseline","M1: Baseline"); mcn("M8: SICE + SMOTE","M2: SICE + Discretize"); mcn(M5,"M6: SICE + Continuous")
McNemar M4: SMOTE on Baseline vs M1: Baseline p=0.0077
[1] 0.007660761
McNemar M8: SICE + SMOTE vs M2: SICE + Discretize p=0.0133
[1] 0.01332833
McNemar M5: ALL (SICE + Cont + SMOTE) vs M6: SICE + Continuous p=0.0809
[1] 0.0808556
cat("\n== H4 combinations ==\n"); dl(M5,"M1: Baseline"); dl("M6: SICE + Continuous","M1: Baseline")
== H4 combinations ==
DeLong M5: ALL (SICE + Cont + SMOTE) vs M1: Baseline Z= 0.338 p=0.7352
Z p
0.3382692 0.7351604
DeLong M6: SICE + Continuous vs M1: Baseline Z= 2.338 p=0.0194
Z p
2.33845878 0.01936346
# ==============================================================================
# Benchmark Model Comparisons (BN vs Logistic Regression & XGBoost)
# ==============================================================================
cat("\n== Benchmark Comparisons ==\n")
== Benchmark Comparisons ==
# roc_bn MUST be the validation-selected PRIMARY model (set in Segment 09), not M5.
# M5 is the fully-combined SICE+continuous+SMOTE arm and is a much weaker
# configuration (test AUC 0.7381) than the model actually reported in the
# manuscript's leaderboard (PRIMARY = M3, test AUC 0.8109). Comparing benchmarks
# against M5 silently answers a different question -- it does not error, it just
# produces the wrong p-values -- which is what happened in the previous run.
roc_bn <- roc_list[[PRIMARY]]
t_lr <- roc.test(roc_bn, roc_lr, method = "delong")
t_xgb <- roc.test(roc_bn, roc_xgb, method = "delong")
cat(sprintf("DeLong %-30s vs %-30s Z=%6.3f p=%.4f\n",
PRIMARY, "Logistic regression", as.numeric(t_lr$statistic), t_lr$p.value))
DeLong M3: Continuous Nodes vs Logistic regression Z=-0.735 p=0.4622
cat(sprintf("DeLong %-30s vs %-30s Z=%6.3f p=%.4f\n",
PRIMARY, "XGBoost", as.numeric(t_xgb$statistic), t_xgb$p.value))
DeLong M3: Continuous Nodes vs XGBoost Z=-1.010 p=0.3126
14 Robustness — all arms, repeated stratified resampling
eval_light <- function(tr, te, disc){
# 1. Dynamically synchronize factor levels between training (tr) and test (te) sets
for (col in names(tr)) {
if (is.factor(tr[[col]])) {
# Remove unobserved levels from tr (fixes check.data warning)
tr[[col]] <- droplevels(tr[[col]])
# Force te to match tr's exact factor levels (fixes level mismatch skip error)
te[[col]] <- factor(te[[col]], levels = levels(tr[[col]]))
}
}
wl <- if (disc) wl_discrete else wl_hybrid
bl <- if (disc) bl_discrete else bl_hybrid
dag <- hc(tr, whitelist = wl, blacklist = bl)
fit <- if (disc) bn.fit(dag, tr, method = "bayes", iss = 10) else bn.fit(dag, tr)
gp <- function(d){
if (disc){
pp <- predict(fit, node = "stroke", data = d, method = "bayes-lw", prob = TRUE)
return(attr(pp, "prob")["1", ])
}
ec <- setdiff(names(d), "stroke")
vapply(seq_len(nrow(d)), function(i){
p <- suppressWarnings(cpquery(fit, event = (stroke == "1"),
evidence = as.list(d[i, ec]), method = "lw", n = 500))
if(is.na(p)) 0 else p
}, numeric(1))
}
as.numeric(auc(roc(te$stroke, gp(te), levels = c("0", "1"), quiet = TRUE)))
}
safe <- function(e) tryCatch(e, error = function(x){ cat(" skip:", x$message, "\n"); NA_real_ })
rob <- data.frame()
for (s in 1:N_REPEATS) {
q <- split_stroke(stroke_data, seed = 1000 + s)
q <- harmonise_levels(q$train, q$val, q$test)
m <- median(as.numeric(as.character(q$train$bmi)), na.rm = TRUE)
ce <- impute_sice(q$train, q$val, q$test, seed = 1000 + s)
d1 <- impute_baseline(q$train, m); d1t <- impute_baseline(q$test, m)
d2 <- impute_baseline(ce$train, m); d2t <- impute_baseline(ce$test, m)
c3 <- prep_continuous(q$train, m); c3t <- prep_continuous(q$test, m)
c6 <- prep_continuous(ce$train, m); c6t <- prep_continuous(ce$test, m)
rob <- rbind(rob, data.frame(seed = s,
M1 = safe(eval_light(d1, d1t, TRUE)),
M2 = safe(eval_light(d2, d2t, TRUE)),
M3 = safe(eval_light(c3, c3t, FALSE)),
M4 = safe(eval_light(apply_smotenc(d1), d1t, TRUE)),
M5 = safe(eval_light(apply_smotenc(c6), c6t, FALSE)),
M6 = safe(eval_light(c6, c6t, FALSE)),
M7 = safe(eval_light(apply_smotenc(c3), c3t, FALSE)),
M8 = safe(eval_light(apply_smotenc(d2), d2t, TRUE))
))
cat(sprintf("seed %d/%d\n", s, N_REPEATS))
}
seed 1/30
seed 2/30
seed 3/30
seed 4/30
seed 5/30
seed 6/30
seed 7/30
seed 8/30
seed 9/30
seed 10/30
seed 11/30
seed 12/30
seed 13/30
seed 14/30
seed 15/30
seed 16/30
seed 17/30
seed 18/30
seed 19/30
seed 20/30
seed 21/30
seed 22/30
seed 23/30
seed 24/30
seed 25/30
seed 26/30
seed 27/30
seed 28/30
seed 29/30
seed 30/30
cat("\n ANALYSABLE RESAMPLES PER ARM:\n")
ANALYSABLE RESAMPLES PER ARM:
print(colSums(!is.na(rob[-1])))
M1 M2 M3 M4 M5 M6 M7 M8
30 30 30 30 30 30 30 30
write.csv(rob, "v3_robustness_raw.csv", row.names = FALSE)
rob_summary <- rob %>%
pivot_longer(-seed, names_to = "arm", values_to = "auc") %>%
group_by(arm) %>%
summarise(
n_analysable = sum(!is.na(auc)),
AUC_mean = round(mean(auc, na.rm = TRUE), 4),
AUC_sd = round(sd(auc,na.rm = TRUE), 4),
.groups = "drop"
) %>%
arrange(desc(AUC_mean))
print(rob_summary)
# A tibble: 8 × 4
arm n_analysable AUC_mean AUC_sd
<chr> <int> <dbl> <dbl>
1 M3 30 0.820 0.0304
2 M6 30 0.817 0.0297
3 M1 30 0.772 0.037
4 M2 30 0.772 0.0392
5 M4 30 0.758 0.0371
6 M8 30 0.756 0.0306
7 M5 30 0.746 0.0406
8 M7 30 0.746 0.0409
write.csv(rob_summary, "v3_robustness_summary.csv", row.names = FALSE)
15 Factorial analysis of the 2×2×2 design
# Main effects AND interactions. If no interaction is significant, the
# word "synergy" must be removed from Hypothesis 4 and the Conclusion.
d <- rob %>% pivot_longer(-seed,names_to="arm",values_to="auc") %>% filter(!is.na(auc)) %>%
mutate(topology =factor(if_else(arm %in% c("M3","M5","M6","M7"),"continuous","discrete"),
levels=c("discrete","continuous")),
imputation=factor(if_else(arm %in% c("M2","M5","M6","M8"),"sice","median"),
levels=c("median","sice")),
balancing =factor(if_else(arm %in% c("M4","M5","M7","M8"),"smote","none"),
levels=c("none","smote")), seed=factor(seed))
fm <- lmer(auc ~ topology*imputation*balancing + (1|seed), data=d, REML=TRUE)
cat("\n--- Type III fixed effects [R1.3a] ---\n"); print(anova(fm, type=3))
--- Type III fixed effects [R1.3a] ---
Type III Analysis of Variance Table with Satterthwaite's method
Sum Sq Mean Sq NumDF DenDF F value Pr(>F)
topology 0.018368 0.018368 1 203 29.1913 1.827e-07 ***
imputation 0.000068 0.000068 1 203 0.1078 0.7430
balancing 0.112374 0.112374 1 203 178.5938 < 2.2e-16 ***
topology:imputation 0.000000 0.000000 1 203 0.0000 0.9946
topology:balancing 0.050988 0.050988 1 203 81.0342 < 2.2e-16 ***
imputation:balancing 0.000005 0.000005 1 203 0.0077 0.9303
topology:imputation:balancing 0.000097 0.000097 1 203 0.1539 0.6952
---
Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
cat("\n--- Estimates with 95% CI ---\n")
--- Estimates with 95% CI ---
print(round(cbind(Estimate=fixef(fm), confint(fm,method="Wald")[names(fixef(fm)),]),4))
Estimate 2.5 % 97.5 %
(Intercept) 0.7716 0.7588 0.7845
topologycontinuous 0.0479 0.0352 0.0606
imputationsice -0.0001 -0.0127 0.0126
balancingsmote -0.0131 -0.0258 -0.0004
topologycontinuous:imputationsice -0.0026 -0.0205 0.0154
topologycontinuous:balancingsmote -0.0608 -0.0788 -0.0429
imputationsice:balancingsmote -0.0020 -0.0199 0.0160
topologycontinuous:imputationsice:balancingsmote 0.0051 -0.0203 0.0305
for (v in c("topology","imputation","balancing"))
print(d %>% group_by(.data[[v]]) %>% summarise(mean_auc=round(mean(auc),4), sd=round(sd(auc),4)))
# A tibble: 2 × 3
topology mean_auc sd
<fct> <dbl> <dbl>
1 discrete 0.765 0.0364
2 continuous 0.782 0.0507
# A tibble: 2 × 3
imputation mean_auc sd
<fct> <dbl> <dbl>
1 median 0.774 0.0457
2 sice 0.773 0.0443
# A tibble: 2 × 3
balancing mean_auc sd
<fct> <dbl> <dbl>
1 none 0.795 0.0412
2 smote 0.752 0.0375
w <- rob %>% filter(!is.na(M3),!is.na(M1))
cat("\nPaired confirmatory (M3 vs M1):\n"); print(t.test(w$M3,w$M1,paired=TRUE)); print(wilcox.test(w$M3,w$M1,paired=TRUE))
Paired confirmatory (M3 vs M1):
Paired t-test
data: w$M3 and w$M1
t = 6.8021, df = 29, p-value = 1.812e-07
alternative hypothesis: true mean difference is not equal to 0
95 percent confidence interval:
0.03352577 0.06235489
sample estimates:
mean difference
0.04794033
Wilcoxon signed rank exact test
data: w$M3 and w$M1
V = 441, p-value = 1.419e-06
alternative hypothesis: true location shift is not equal to 0
16 Sparse CPT reliability and consensus network
# ---- Fit the bootstrap-averaged CONSENSUS network and evaluate it ----
pr <- runs[[PRIMARY]]
astr <- pr$arc_strength
cons <- averaged.network(astr)
cons_directed <- TRUE
cons <- tryCatch(cextend(cons), error = function(e) {
cons_directed <<- FALSE
averaged.network(astr, threshold = 0.85)
})
cat("\n[R1.5a] Consensus network arcs:\n"); print(arcs(cons))
[R1.5a] Consensus network arcs:
from to
[1,] "age" "avg_glucose_level"
[2,] "age" "bmi"
[3,] "hypertension" "stroke"
[4,] "heart_disease" "stroke"
[5,] "ever_married" "age"
[6,] "ever_married" "heart_disease"
[7,] "ever_married" "avg_glucose_level"
[8,] "ever_married" "stroke"
[9,] "work_type" "age"
[10,] "work_type" "hypertension"
[11,] "work_type" "ever_married"
[12,] "work_type" "bmi"
[13,] "work_type" "smoking_status"
[14,] "bmi" "avg_glucose_level"
[15,] "stroke" "age"
[16,] "stroke" "avg_glucose_level"
cat("\nArcs in single HC fit but not consensus:\n")
Arcs in single HC fit but not consensus:
h <- apply(arcs(pr$structure),1,paste,collapse="->"); c2 <- apply(arcs(cons),1,paste,collapse="->")
print(setdiff(h,c2)); cat("Arcs in consensus but not HC fit:\n"); print(setdiff(c2,h))
[1] "ever_married->work_type" "heart_disease->ever_married" "gender->heart_disease"
Arcs in consensus but not HC fit:
[1] "ever_married->heart_disease" "work_type->ever_married"
cons_auc <- tryCatch({
cf <- if (pr$is_disc) bn.fit(cons, pr$train, method="bayes", iss=10) else bn.fit(cons, pr$train)
gp <- if (pr$is_disc) {
attr(predict(cf,node="stroke",data=pr$test,method="bayes-lw",prob=TRUE),"prob")["1",]
} else {
ec <- setdiff(names(pr$test),"stroke")
vapply(seq_len(nrow(pr$test)),function(i){
p<-suppressWarnings(cpquery(cf,event=(stroke=="1"),evidence=as.list(pr$test[i,ec]),
method="lw",n=N_PARTICLES_PATIENT)); if(is.na(p)) 0 else p},numeric(1)) }
round(as.numeric(auc(roc(pr$test$stroke, gp, levels=c("0","1"), quiet=TRUE))),4)
}, error=function(e){cat("consensus fit failed:",e$message,"\n"); NA})
cat(sprintf("\n[R1.5a] Consensus-network test AUC = %s (single HC fit = %.4f)\n",
cons_auc, leaderboard$Test_AUC[leaderboard$Model==PRIMARY]))
[R1.5a] Consensus-network test AUC = 0.8115 (single HC fit = 0.8109)
write.csv(astr %>% filter(strength>0.5, direction>=0.5) %>% arrange(desc(strength)),
"v3_arc_strengths.csv", row.names=FALSE)
# ---- CPT counts + Jeffreys intervals + shrinkage ----
cpt_tbl <- train_m6 %>% count(hypertension, heart_disease, ever_married, stroke) %>%
pivot_wider(names_from=stroke, values_from=n, values_fill=0) %>%
rename(n0=`0`, n1=`1`) %>%
mutate(n=n0+n1, p_hat=round(100*n1/n,2),
CrI_low =round(100*qbeta(.025,n1+.5,n0+.5),2),
CrI_high=round(100*qbeta(.975,n1+.5,n0+.5),2)) %>% arrange(desc(p_hat))
print(cpt_tbl); write.csv(cpt_tbl,"v3_cpt_intervals.csv",row.names=FALSE)
# A tibble: 8 × 9
hypertension heart_disease ever_married n0 n1 n p_hat CrI_low CrI_high
<fct> <fct> <fct> <int> <int> <int> <dbl> <dbl> <dbl>
1 1 1 No 1 3 4 75 28.4 97.2
2 0 1 No 11 2 13 15.4 3.34 40.9
3 1 0 No 26 4 30 13.3 4.67 28.6
4 1 1 Yes 26 4 30 13.3 4.67 28.6
5 0 1 Yes 94 14 108 13.0 7.63 20.2
6 1 0 Yes 213 26 239 10.9 7.4 15.3
7 0 0 Yes 1551 84 1635 5.14 4.15 6.29
8 0 0 No 994 12 1006 1.19 0.65 2.01
p_marg <- mean(train_m6$stroke=="1")
iss_tbl <- map_dfr(c(1,5,10,25,50,100), function(k)
cpt_tbl %>% transmute(hypertension,heart_disease,ever_married,n,iss=k,
p_smoothed=round(100*(n1+k*p_marg)/(n+k),2)))
write.csv(iss_tbl,"v3_cpt_iss_sensitivity.csv",row.names=FALSE)
print(iss_tbl %>% filter(n<=10))
# A tibble: 6 × 6
hypertension heart_disease ever_married n iss p_smoothed
<fct> <fct> <fct> <int> <dbl> <dbl>
1 1 1 No 4 1 61.0
2 1 1 No 4 5 36.0
3 1 1 No 4 10 24.9
4 1 1 No 4 25 14.5
5 1 1 No 4 50 10.1
6 1 1 No 4 100 7.56
# ---- [R3.8] Does the DISCRETE arm recover age -> stroke? ----
a1 <- arcs(dags[["M1: Baseline"]])
cat("\n[R3.8] Discrete-arm arcs involving age:\n"); print(a1[apply(a1,1,function(r) any(r=="age")),,drop=FALSE])
[R3.8] Discrete-arm arcs involving age:
from to
[1,] "age" "stroke"
[2,] "age" "work_type"
[3,] "age" "ever_married"
[4,] "age" "hypertension"
[5,] "age" "heart_disease"
17 Conditional-risk scenarios (NOT counterfactual
interventions)
# [R1.1] Observational conditioning, not do(). No treatment-effect language.
net <- fits[[PRIMARY]]; set.seed(2026)
pA <- cpquery(net, event=(stroke=="1"),
evidence=(age<=50 & hypertension=="0" & heart_disease=="0"), n=N_PARTICLES_QUERY)
pB <- cpquery(net, event=(stroke=="1"),
evidence=(age>=65 & hypertension=="1" & heart_disease=="1" & avg_glucose_level>=150), n=N_PARTICLES_QUERY)
pC <- cpquery(net, event=(stroke=="1"),
evidence=(age>=65 & hypertension=="1" & heart_disease=="1" & avg_glucose_level<=100), n=N_PARTICLES_QUERY)
scen <- data.frame(Profile=c("A: age<=50, no HTN, no HD",
"B: age>=65, HTN, HD, glucose>=150",
"C: age>=65, HTN, HD, glucose<=100"),
Conditional_probability=sprintf("%.2f%%",100*c(pA,pB,pC)))
print(scen); write.csv(scen,"v3_conditional_risk.csv",row.names=FALSE)
Profile Conditional_probability
1 A: age<=50, no HTN, no HD 0.74%
2 B: age>=65, HTN, HD, glucose>=150 46.13%
3 C: age>=65, HTN, HD, glucose<=100 29.89%
19 RESULTS PACK — every number the Results section needs
# Run last. Produces a single console block ordered to match the manuscript's
# Results subsections 3.1 - 3.9, so numbers can be transcribed directly.
line <- function(x) cat(strrep("=",72),"\n",x,"\n",strrep("=",72),"\n",sep="")
line("3.0 COHORT AND PARTITIONS")
========================================================================
3.0 COHORT AND PARTITIONS
========================================================================
cat(sprintf("N = %d; prevalence = %.2f%%; missing BMI = %.1f%%\n",
N_TOTAL, 100*PI_POP, 100*mean(is.na(stroke_data$bmi))))
N = 5109; prevalence = 4.87%; missing BMI = 3.9%
print(split_tbl)
# A tibble: 3 × 4
Partition n Events Prevalence
<chr> <int> <int> <chr>
1 Training 3065 149 4.86%
2 Validation 1022 50 4.89%
3 Test 1022 50 4.89%
line("3.1 TRAINING-ONLY ASSOCIATION SCREEN (Supplementary)")
========================================================================
3.1 TRAINING-ONLY ASSOCIATION SCREEN (Supplementary)
========================================================================
print(assoc_tbl)
Variable Type Assoc
1 age continuous 0.2410
2 avg_glucose_level continuous 0.1318
3 hypertension categorical 0.1132
4 heart_disease categorical 0.1071
5 ever_married categorical 0.0964
6 work_type categorical 0.0926
7 smoking_status categorical 0.0750
8 bmi continuous 0.0356
9 Residence_type categorical 0.0250
10 gender categorical 0.0008
line("3.2 STRUCTURE — primary model, consensus comparison, arc direction")
========================================================================
3.2 STRUCTURE — primary model, consensus comparison, arc direction
========================================================================
cat("PRIMARY MODEL (validation-selected):", PRIMARY, "\n")
PRIMARY MODEL (validation-selected): M3: Continuous Nodes
cat("Parents of stroke:", paste(parents(fits[[PRIMARY]],"stroke"), collapse=", "), "\n")
Parents of stroke: hypertension, heart_disease, ever_married
cat("Children of stroke:", paste(children(fits[[PRIMARY]],"stroke"), collapse=", "), "\n")
Children of stroke: age, avg_glucose_level
cat(sprintf("Consensus-network test AUC = %s vs single HC = %.4f\n",
cons_auc, leaderboard$Test_AUC[leaderboard$Model==PRIMARY]))
Consensus-network test AUC = 0.8115 vs single HC = 0.8109
line("3.3 CPT RELIABILITY")
========================================================================
3.3 CPT RELIABILITY
========================================================================
print(cpt_tbl); cat("\nShrinkage for sparse cells:\n"); print(iss_tbl %>% filter(n<=10))
# A tibble: 8 × 9
hypertension heart_disease ever_married n0 n1 n p_hat CrI_low CrI_high
<fct> <fct> <fct> <int> <int> <int> <dbl> <dbl> <dbl>
1 1 1 No 1 3 4 75 28.4 97.2
2 0 1 No 11 2 13 15.4 3.34 40.9
3 1 0 No 26 4 30 13.3 4.67 28.6
4 1 1 Yes 26 4 30 13.3 4.67 28.6
5 0 1 Yes 94 14 108 13.0 7.63 20.2
6 1 0 Yes 213 26 239 10.9 7.4 15.3
7 0 0 Yes 1551 84 1635 5.14 4.15 6.29
8 0 0 No 994 12 1006 1.19 0.65 2.01
Shrinkage for sparse cells:
# A tibble: 6 × 6
hypertension heart_disease ever_married n iss p_smoothed
<fct> <fct> <fct> <int> <dbl> <dbl>
1 1 1 No 4 1 61.0
2 1 1 No 4 5 36.0
3 1 1 No 4 10 24.9
4 1 1 No 4 25 14.5
5 1 1 No 4 50 10.1
6 1 1 No 4 100 7.56
line("3.4 CONDITIONAL-RISK SCENARIOS")
========================================================================
3.4 CONDITIONAL-RISK SCENARIOS
========================================================================
print(scen)
Profile Conditional_probability
1 A: age<=50, no HTN, no HD 0.74%
2 B: age>=65, HTN, HD, glucose>=150 46.13%
3 C: age>=65, HTN, HD, glucose<=100 29.89%
line("3.5 PERFORMANCE LEADERBOARD")
========================================================================
3.5 PERFORMANCE LEADERBOARD
========================================================================
print(leaderboard)
Model Val_Threshold Val_AUC Test_AUC Test_AUC_95_CI Test_Sensitivity
Sensitivity2 M3: Continuous Nodes 0.0857 0.8642 0.8109 [0.7612 - 0.8533] 0.62
Sensitivity5 M6: SICE + Continuous 0.0951 0.8683 0.8094 [0.7615 - 0.8522] 0.58
Sensitivity6 M7: Continuous + SMOTE 0.4303 0.7602 0.7456 [0.6672 - 0.8126] 0.72
Sensitivity4 M5: ALL (SICE + Cont + SMOTE) 0.3022 0.7500 0.7381 [0.6688 - 0.8048] 0.76
Sensitivity M1: Baseline 0.0350 0.7710 0.7235 [0.6542 - 0.7907] 0.68
Sensitivity1 M2: SICE + Discretize 0.0330 0.7774 0.7108 [0.6376 - 0.7805] 0.68
Sensitivity3 M4: SMOTE on Baseline 0.5530 0.7509 0.6903 [0.6128 - 0.7630] 0.50
Sensitivity7 M8: SICE + SMOTE 0.5350 0.7392 0.6855 [0.6084 - 0.7617] 0.52
Test_Specificity Test_Youden_J Test_F1
Sensitivity2 0.7912 0.4112 0.2183
Sensitivity5 0.8035 0.3835 0.2148
Sensitivity6 0.7109 0.4309 0.1962
Sensitivity4 0.6626 0.4226 0.1827
Sensitivity 0.6584 0.3384 0.1635
Sensitivity1 0.6348 0.3148 0.1549
Sensitivity3 0.7747 0.2747 0.1701
Sensitivity7 0.7623 0.2823 0.1694
line("3.6 HYPOTHESIS TESTS")
========================================================================
3.6 HYPOTHESIS TESTS
========================================================================
cat("(see Segment 13 console output; transcribe Z and p for H1-H4)\n")
(see Segment 13 console output; transcribe Z and p for H1-H4)
line("3.7 ROBUSTNESS")
========================================================================
3.7 ROBUSTNESS
========================================================================
print(rob_summary)
# A tibble: 8 × 4
arm n_analysable AUC_mean AUC_sd
<chr> <int> <dbl> <dbl>
1 M3 30 0.820 0.0304
2 M6 30 0.817 0.0297
3 M1 30 0.772 0.037
4 M2 30 0.772 0.0392
5 M4 30 0.758 0.0371
6 M8 30 0.756 0.0306
7 M5 30 0.746 0.0406
8 M7 30 0.746 0.0409
cat("\nAnalysable resamples per arm:\n"); print(colSums(!is.na(rob[-1])))
Analysable resamples per arm:
M1 M2 M3 M4 M5 M6 M7 M8
30 30 30 30 30 30 30 30
line("3.7b FACTORIAL MODEL")
========================================================================
3.7b FACTORIAL MODEL
========================================================================
print(anova(fm, type=3))
Type III Analysis of Variance Table with Satterthwaite's method
Sum Sq Mean Sq NumDF DenDF F value Pr(>F)
topology 0.018368 0.018368 1 203 29.1913 1.827e-07 ***
imputation 0.000068 0.000068 1 203 0.1078 0.7430
balancing 0.112374 0.112374 1 203 178.5938 < 2.2e-16 ***
topology:imputation 0.000000 0.000000 1 203 0.0000 0.9946
topology:balancing 0.050988 0.050988 1 203 81.0342 < 2.2e-16 ***
imputation:balancing 0.000005 0.000005 1 203 0.0077 0.9303
topology:imputation:balancing 0.000097 0.000097 1 203 0.1539 0.6952
---
Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
line("3.8 CALIBRATION")
========================================================================
3.8 CALIBRATION
========================================================================
print(calib)
Model Correction Brier Brier_ref BSS Calib_intercept Calib_slope
1 M1: Baseline none 0.0504 0.0465 -0.0824 -0.206 0.257
2 M2: SICE + Discretize none 0.0509 0.0465 -0.0941 -0.217 0.240
3 M3: Continuous Nodes none 0.0561 0.0465 -0.2049 -0.643 0.344
4 M4: SMOTE on Baseline none 0.1735 0.0465 -2.7279 -3.012 0.153
5 M4: SMOTE on Baseline prior-corrected 0.0489 0.0465 -0.0519 -0.041 0.174
6 M5: ALL (SICE + Cont + SMOTE) none 0.1756 0.0465 -2.7739 -3.282 0.174
7 M5: ALL (SICE + Cont + SMOTE) prior-corrected 0.0542 0.0465 -0.1651 -0.311 0.193
8 M6: SICE + Continuous none 0.0564 0.0465 -0.2132 -0.651 0.345
9 M7: Continuous + SMOTE none 0.1768 0.0465 -2.8007 -3.401 0.165
10 M7: Continuous + SMOTE prior-corrected 0.0552 0.0465 -0.1859 -0.430 0.188
11 M8: SICE + SMOTE none 0.1750 0.0465 -2.7611 -3.049 0.139
12 M8: SICE + SMOTE prior-corrected 0.0493 0.0465 -0.0593 -0.077 0.169
cat(sprintf("\nPrevalence-only reference Brier = %.4f\n", mean((PI_POP-as.numeric(as.character(testset$stroke)))^2)))
Prevalence-only reference Brier = 0.0465
line("3.9 BENCHMARK")
========================================================================
3.9 BENCHMARK
========================================================================
print(bench)
Model Test_AUC
1 Bayesian network (M3: Continuous Nodes) 0.8109
2 Logistic regression (transparent) 0.8188
3 XGBoost (opaque) 0.8222
cat(sprintf("XGBoost best_iteration = %d; scale_pos_weight = %.3f\n", xgb$best_iteration, SPW))
cat("\nTop SHAP features:\n"); print(round(head(imp,8),5))
Top SHAP features:
age bmi avg_glucose_level genderFemale hypertension1
0.85756 0.15479 0.10572 0.02464 0.02349
work_typeGovt_job smoking_statussmokes (Intercept)
0.01622 0.01375 0.01166
line("END OF RESULTS PACK")
========================================================================
END OF RESULTS PACK
========================================================================
LS0tDQp0aXRsZTogIkh5YnJpZCBCYXllc2lhbiBOZXR3b3JrIGZvciBJbnRlcnByZXRhYmxlIFN0cm9rZSBQcmVkaWN0aW9uIg0KYXV0aG9yOiAiUHJhbmlsIEdDLCBSYXZpbmRlci1KZWV0IFNpbmdoLCBSYXR2aW5kZXIgR3Jld2FsIg0KZG9pOiAxMC4yMDk0NC9wcmVwcmludHMyMDI2MDcuMDYyNS52MQ0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19kZXB0aDogMg0KICBodG1sX2RvY3VtZW50Og0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19kZXB0aDogJzInDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQp2ZXJzaW9uOiAnMicNCnVwZGF0ZWQgb246IDIwMjYgSnVseSAyOA0KY2l0ZTogIlAuIEdjLCBSLi1KLiBTaW5naCwgYW5kIFIuIEdyZXdhbCwg4oCcRXhwbGFpbmFibGUgQUkgZm9yIFN0cm9rZSBQcmVkaWN0aW9uOiBBDQogIEh5YnJpZCBCYXllc2lhbiBOZXR3b3JrIEFwcHJvYWNoLOKAnSBKdWwuIDA5LCAyMDI2LCBQcmVwcmludHM6IDIwMjYwNzA2MjUuIGRvaTogMTAuMjA5NDQvcHJlcHJpbnRzMjAyNjA3LjA2MjUudjEuIg0KLS0tDQoNCmNpdGU6ICJQLiBHYywgUi4tSi4gU2luZ2gsIGFuZCBSLiBHcmV3YWwsIOKAnEV4cGxhaW5hYmxlIEFJIGZvciBTdHJva2UgUHJlZGljdGlvbjogQSBIeWJyaWQgQmF5ZXNpYW4gTmV0d29yayBBcHByb2FjaCzigJ0gSnVsLjA5LCAyMDI2LCBQcmVwcmludHM6IDIwMjYwNzA2MjUuIGRvaTogMTAuMjA5NDQvcHJlcHJpbnRzMjAyNjA3LjA2MjUudjEuIg0KDQp1cGRhdGVkIG9uOiBKdWx5IDI4LCAyMDI2DQoNCg0KIyAwMCAgU2V0dXAgJiByZXByb2R1Y2liaWxpdHkNCmBgYHtyIHNldHVwfQ0KbGlicmFyeSh0aWR5dmVyc2UpOyBsaWJyYXJ5KGJubGVhcm4pOyBsaWJyYXJ5KGNhcmV0KTsgbGlicmFyeShwUk9DKQ0KbGlicmFyeShtaWNlKTsgbGlicmFyeShyZWNpcGVzKTsgbGlicmFyeSh0aGVtaXMpDQpsaWJyYXJ5KGxtZTQpOyBsaWJyYXJ5KGxtZXJUZXN0KSAgICMgZmFjdG9yaWFsIG1peGVkIG1vZGVsDQpsaWJyYXJ5KHhnYm9vc3QpICAgICAgICAgICAgICAgICAgICMgb3BhcXVlIGJlbmNobWFyaw0KbGlicmFyeSh2Y2QpICAgICAgICAgICAgICAgICAgICAgICAjIHRyYWluaW5nLW9ubHkgYXNzb2NpYXRpb24gc2NyZWVuDQoNCkdMT0JBTF9TRUVEIDwtIDQyDQpzZXQuc2VlZChHTE9CQUxfU0VFRCkNCg0KIyBJbmZlcmVuY2UgaXMgTElLRUxJSE9PRCBXRUlHSFRJTkcgZm9yIGV2ZXJ5IGFybS4NCg0KSU5GRVJFTkNFX01FVEhPRCA8LSAiTW9udGUgQ2FybG8gbGlrZWxpaG9vZCB3ZWlnaHRpbmciDQpOX1BBUlRJQ0xFU19QQVRJRU5UIDwtIDEwMDAgICAgICAjIHBlci1wYXRpZW50IHBvc3RlcmlvciAgKG1hdGNoZXMgTWV0aG9kcykNCk5fUEFSVElDTEVTX1FVRVJZICAgPC0gMWU2ICAgICAgICMgcG9wdWxhdGlvbi1sZXZlbCBjb25kaXRpb25hbC1yaXNrIHF1ZXJpZXMNCg0KYGBgDQoNCiMgMDEgIExvYWQgJiBjbGVhbiBkYXRhDQpgYGB7ciBsb2FkLWNsZWFufQ0Kc3Ryb2tlX2RhdGEgPC0gcmVhZC5jc3YoImhlYWx0aGNhcmUtZGF0YXNldC1zdHJva2UtZGF0YS5jc3YiKQ0Kc3Ryb2tlX2RhdGEgPC0gc3Ryb2tlX2RhdGEgJT4lDQogIGZpbHRlcihnZW5kZXIgIT0gIk90aGVyIikgJT4lIGRyb3BsZXZlbHMoKSAlPiUNCiAgbXV0YXRlKGJtaSA9IGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKGJtaSkpLA0KICAgICAgICAgYWNyb3NzKGMoc3Ryb2tlLGh5cGVydGVuc2lvbixoZWFydF9kaXNlYXNlLGdlbmRlcixldmVyX21hcnJpZWQsDQogICAgICAgICAgICAgICAgICB3b3JrX3R5cGUsUmVzaWRlbmNlX3R5cGUsc21va2luZ19zdGF0dXMpLCBhcy5mYWN0b3IpKQ0KDQpQSV9QT1AgPC0gbWVhbihzdHJva2VfZGF0YSRzdHJva2UgPT0gIjEiKQ0KTl9UT1RBTCA8LSBucm93KHN0cm9rZV9kYXRhKQ0KY2F0KHNwcmludGYoIk4gPSAlZCB8IHN0cm9rZSBQUkVWQUxFTkNFID0gJS4yZiUlIHwgbWlzc2luZyBCTUkgPSAlZCAoJS4xZiUlKVxuIiwNCiAgICAgICAgICAgIE5fVE9UQUwsIDEwMCpQSV9QT1AsIHN1bShpcy5uYShzdHJva2VfZGF0YSRibWkpKSwNCiAgICAgICAgICAgIDEwMCptZWFuKGlzLm5hKHN0cm9rZV9kYXRhJGJtaSkpKSkNCg0KYGBgDQoNCiMgMDIgIEV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMgKGZpZ3VyZXMpDQpgYGB7ciBlZGEsIGV2YWw9RkFMU0V9DQojIFNldCBldmFsPVRSVUUgdG8gcmVnZW5lcmF0ZSB0aGUgZmVhdHVyZS1kaXN0cmlidXRpb24gZmlndXJlcy4NCmBgYA0KDQojIDAzICBTdHJhdGlmaWVkIHNwbGl0LCBhYnNvbHV0ZSBzaXplcywgbGV2ZWwgaGFybW9uaXNhdGlvbg0KYGBge3Igc3BsaXR9DQpzcGxpdF9zdHJva2UgPC0gZnVuY3Rpb24oZGF0YSwgc2VlZCA9IEdMT0JBTF9TRUVELCBwX3RyYWluID0gLjYsIHBfdmFsID0gLjIpIHsNCiAgc2V0LnNlZWQoc2VlZCkNCiAgczAgPC0gZGF0YSAlPiUgZmlsdGVyKHN0cm9rZT09IjAiKTsgczEgPC0gZGF0YSAlPiUgZmlsdGVyKHN0cm9rZT09IjEiKQ0KICBuMCA8LSBucm93KHMwKTsgbjEgPC0gbnJvdyhzMSkNCiAgdHIwIDwtIHNhbXBsZShuMCwgcm91bmQobjAqcF90cmFpbikpOyB0cjEgPC0gc2FtcGxlKG4xLCByb3VuZChuMSpwX3RyYWluKSkNCiAgdmEwIDwtIHNhbXBsZShzZXRkaWZmKHNlcV9sZW4objApLCB0cjApLCByb3VuZChuMCpwX3ZhbCkpDQogIHZhMSA8LSBzYW1wbGUoc2V0ZGlmZihzZXFfbGVuKG4xKSwgdHIxKSwgcm91bmQobjEqcF92YWwpKQ0KICBsaXN0KHRyYWluID0gYmluZF9yb3dzKHMwW3RyMCxdLCBzMVt0cjEsXSksDQogICAgICAgdmFsICAgPSBiaW5kX3Jvd3MoczBbdmEwLF0sIHMxW3ZhMSxdKSwNCiAgICAgICB0ZXN0ICA9IGJpbmRfcm93cyhzMFtzZXRkaWZmKHNlcV9sZW4objApLCBjKHRyMCx2YTApKSxdLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHMxW3NldGRpZmYoc2VxX2xlbihuMSksIGModHIxLHZhMSkpLF0pKQ0KfQ0KDQojIEd1YXJhbnRlZXMgZXZlcnkgcmVzYW1wbGUgaXMgYW5hbHlzYWJsZS4gDQpoYXJtb25pc2VfbGV2ZWxzIDwtIGZ1bmN0aW9uKHRyLCB2YSwgdGUpIHsNCiAgZm9yICh2IGluIG5hbWVzKEZpbHRlcihpcy5mYWN0b3IsIHRyKSkpIHsNCiAgICBsdiA8LSBSZWR1Y2UodW5pb24sIGxpc3QobGV2ZWxzKGZhY3Rvcih0cltbdl1dKSksIGxldmVscyhmYWN0b3IodmFbW3ZdXSkpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHMoZmFjdG9yKHRlW1t2XV0pKSkpDQogICAgdHJbW3ZdXSA8LSBmYWN0b3IoYXMuY2hhcmFjdGVyKHRyW1t2XV0pLCBsZXZlbHMgPSBsdikNCiAgICB2YVtbdl1dIDwtIGZhY3Rvcihhcy5jaGFyYWN0ZXIodmFbW3ZdXSksIGxldmVscyA9IGx2KQ0KICAgIHRlW1t2XV0gPC0gZmFjdG9yKGFzLmNoYXJhY3Rlcih0ZVtbdl1dKSwgbGV2ZWxzID0gbHYpDQogIH0NCiAgbGlzdCh0cmFpbiA9IHRyLCB2YWwgPSB2YSwgdGVzdCA9IHRlKQ0KfQ0KDQpzcCA8LSBzcGxpdF9zdHJva2Uoc3Ryb2tlX2RhdGEpOyBzcCA8LSBoYXJtb25pc2VfbGV2ZWxzKHNwJHRyYWluLCBzcCR2YWwsIHNwJHRlc3QpDQp0cmFpbnNldCA8LSBzcCR0cmFpbjsgdmFsc2V0IDwtIHNwJHZhbDsgdGVzdHNldCA8LSBzcCR0ZXN0DQoNCiMgUGFydGl0aW9uIGRhdGENCnNwbGl0X3RibCA8LSB0aWJibGUoDQogIFBhcnRpdGlvbiA9IGMoIlRyYWluaW5nIiwiVmFsaWRhdGlvbiIsIlRlc3QiKSwNCiAgbiAgICAgID0gYyhucm93KHRyYWluc2V0KSwgbnJvdyh2YWxzZXQpLCBucm93KHRlc3RzZXQpKSwNCiAgRXZlbnRzID0gYyhzdW0odHJhaW5zZXQkc3Ryb2tlPT0iMSIpLCBzdW0odmFsc2V0JHN0cm9rZT09IjEiKSwgc3VtKHRlc3RzZXQkc3Ryb2tlPT0iMSIpKSkgJT4lDQogIG11dGF0ZShQcmV2YWxlbmNlID0gc3ByaW50ZigiJS4yZiUlIiwgMTAwKkV2ZW50cy9uKSkNCnByaW50KHNwbGl0X3RibCk7IHdyaXRlLmNzdihzcGxpdF90YmwsICJ2M19zcGxpdF9zaXplcy5jc3YiLCByb3cubmFtZXM9RkFMU0UpDQpgYGANCg0KIyAwNCAgUHJlcHJvY2Vzc2luZyBoZWxwZXJzIChpbmNsLiBCb3JkZXJsaW5lLVNNT1RFKQ0KYGBge3IgaGVscGVyc30NCiMgLS0tIChhKSBEaXNjcmV0aXNpbmcgaW1wdXRlcjogbWVkaWFuIEJNSSBmcm9tIFRSQUlOIG9ubHksIHRoZW4gV0hPL0NEQy9BREEgYmlucw0KaW1wdXRlX2Jhc2VsaW5lIDwtIGZ1bmN0aW9uKGRhdGEsIHRyYWluX21lZGlhbl9ibWkpIHsNCiAgZGF0YSAlPiUNCiAgICBtdXRhdGUoYm1pID0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoYm1pKSksDQogICAgICAgICAgIGJtaSA9IGlmZWxzZShpcy5uYShibWkpLCB0cmFpbl9tZWRpYW5fYm1pLCBibWkpKSAlPiUNCiAgICBtdXRhdGUoYWdlID0gY3V0KGFnZSwgYnJlYWtzPWMoc2VxKDAsODUsYnk9NSksSW5mKSwgcmlnaHQ9RkFMU0UsDQogICAgICAgICAgICAgbGFiZWxzPWMoIjAtNCIsIjUtOSIsIjEwLTE0IiwiMTUtMTkiLCIyMC0yNCIsIjI1LTI5IiwiMzAtMzQiLCIzNS0zOSIsDQogICAgICAgICAgICAgICAgICAgICAgIjQwLTQ0IiwiNDUtNDkiLCI1MC01NCIsIjU1LTU5IiwiNjAtNjQiLCI2NS02OSIsIjcwLTc0IiwNCiAgICAgICAgICAgICAgICAgICAgICAiNzUtNzkiLCI4MC04NCIsIjg1KyIpKSwNCiAgICAgICAgICAgYm1pID0gY3V0KGJtaSwgYnJlYWtzPWMoMCwxOC41LDI1LDMwLEluZiksIHJpZ2h0PUZBTFNFLA0KICAgICAgICAgICAgIGxhYmVscz1jKCJVbmRlcndlaWdodCIsIk5vcm1hbCIsIk92ZXJ3ZWlnaHQiLCJPYmVzZSIpKSwNCiAgICAgICAgICAgYXZnX2dsdWNvc2VfbGV2ZWwgPSBjdXQoYXZnX2dsdWNvc2VfbGV2ZWwsIGJyZWFrcz1jKDAsMTAwLDEyNixJbmYpLA0KICAgICAgICAgICAgIHJpZ2h0PUZBTFNFLCBsYWJlbHM9YygiTm9ybWFsIiwiUHJlZGlhYmV0ZXMiLCJEaWFiZXRlcyIpKSkgJT4lDQogICAgbXV0YXRlKGFjcm9zcyhjKGdlbmRlcixoeXBlcnRlbnNpb24saGVhcnRfZGlzZWFzZSxldmVyX21hcnJpZWQsd29ya190eXBlLA0KICAgICAgICAgICAgICAgICAgICBSZXNpZGVuY2VfdHlwZSxzbW9raW5nX3N0YXR1cyxzdHJva2UpLCBhcy5mYWN0b3IpKSAlPiUNCiAgICBkcm9wbGV2ZWxzKCkgJT4lIA0KICAgIHNlbGVjdCgtYW55X29mKCJpZCIpKQ0KfQ0KDQoNCiMgLS0tIChiKSBDb250aW51b3VzL2h5YnJpZCBwcmVwOiBrZWVwIGFnZS9ibWkvZ2x1Y29zZSBudW1lcmljDQpwcmVwX2NvbnRpbnVvdXMgPC0gZnVuY3Rpb24oZGYsIHRyYWluX21lZGlhbl9ibWkpIHsNCiAgZGYgJT4lDQogICAgbXV0YXRlKGJtaSA9IGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKGJtaSkpLA0KICAgICAgICAgICBibWkgPSBpZmVsc2UoaXMubmEoYm1pKSwgdHJhaW5fbWVkaWFuX2JtaSwgYm1pKSwNCiAgICAgICAgICAgYXZnX2dsdWNvc2VfbGV2ZWwgPSBhcy5udW1lcmljKGF2Z19nbHVjb3NlX2xldmVsKSwNCiAgICAgICAgICAgYWdlID0gYXMubnVtZXJpYyhhZ2UpKSAlPiUNCiAgICBtdXRhdGUoYWNyb3NzKGMoZ2VuZGVyLGh5cGVydGVuc2lvbixoZWFydF9kaXNlYXNlLGV2ZXJfbWFycmllZCx3b3JrX3R5cGUsDQogICAgICAgICAgICAgICAgICAgIFJlc2lkZW5jZV90eXBlLHNtb2tpbmdfc3RhdHVzLHN0cm9rZSksIGFzLmZhY3RvcikpICU+JQ0KICAgIHNlbGVjdCgtYW55X29mKCJpZCIpKQ0KDQp9DQoNCiMgLS0tIChjKSBTTU9URU5DIHN5bnRoZXRpYyBvdmVyc2FtcGxpbmcgZm9yIG1peGVkIChjYXRlZ29yaWNhbCtjb250aW51b3VzKSBkYXRhDQphcHBseV9zbW90ZW5jIDwtIGZ1bmN0aW9uKGRmLCB0YXJnZXQ9InN0cm9rZSIpIHsNCiAgZGYgPC0gZHJvcGxldmVscyhhcy5kYXRhLmZyYW1lKGRmKSkNCiAgcmVjIDwtIHJlY2lwZShmb3JtdWxhKHBhc3RlKHRhcmdldCwifiAuIikpLCBkYXRhPWRmKSAlPiUNCiAgICBzdGVwX3Ntb3RlbmMoYWxsX291dGNvbWVzKCksIG92ZXJfcmF0aW89MSwgc2VlZD1HTE9CQUxfU0VFRCkgJT4lIHByZXAoKQ0KICBkcm9wbGV2ZWxzKGFzLmRhdGEuZnJhbWUoanVpY2UocmVjKSAlPiUNCiAgICBtdXRhdGUoYWNyb3NzKHdoZXJlKGlzLm51bWVyaWMpLCBhcy5udW1lcmljKSwNCiAgICAgICAgICAgYWNyb3NzKHdoZXJlKGlzLmNoYXJhY3RlciksIGFzLmZhY3RvcikpKSkNCn0NCg0KYGBgDQoNCiMgMDUgIEJ1aWxkIHRoZSA4IGFibGF0aW9uIHN0dWR5IGFybXMgKE0xLU04KQ0KYGBge3IgdmFyaWFudHN9DQoNCiMgb3V0Y29tZSByZXRhaW5lZCBmb3IgVFJBSU5JTkcgaW1wdXRhdGlvbiwgZXhjbHVkZWQgZm9yIFZBTC9URVNUDQojIHNpbmdsZSBpbXB1dGF0aW9uIGJ5IGNoYWluZWQgZXF1YXRpb25zIChTSUNFKQ0KTV9JTVAgPC0gMQ0KaW1wdXRlX3NpY2UgPC0gZnVuY3Rpb24odHJhaW5zZXQsIHZhbHNldCwgdGVzdHNldCwgc2VlZCA9IEdMT0JBTF9TRUVEKSB7DQogIHRyYWluc2V0JC5zcGxpdCA8LSAidHJhaW4iOyB2YWxzZXQkLnNwbGl0IDwtICJ2YWwiOyB0ZXN0c2V0JC5zcGxpdCA8LSAidGVzdCINCiAgY29tYiA8LSBiaW5kX3Jvd3ModHJhaW5zZXQsIHZhbHNldCwgdGVzdHNldCkgJT4lIHNlbGVjdCgtYW55X29mKCJpZCIpKQ0KICBkYXQgIDwtIGNvbWIgJT4lIHNlbGVjdCgtLnNwbGl0KTsgaWcgPC0gY29tYiQuc3BsaXQgIT0gInRyYWluIg0KICBwbV90ciA8LSBtYWtlLnByZWRpY3Rvck1hdHJpeChkYXQpOyBwbV90ZSA8LSBwbV90cjsgcG1fdGVbLCJzdHJva2UiXSA8LSAwDQogIGEgPC0gY29tcGxldGUoc3VwcHJlc3NXYXJuaW5ncyhtaWNlKGRhdCwgbT1NX0lNUCwgbWF4aXQ9NSwgcHJlZGljdG9yTWF0cml4PXBtX3RyLA0KICAgICAgICAgICAgICAgICBpZ25vcmU9aWcsIHNlZWQ9c2VlZCwgICBwcmludEZsYWc9RkFMU0UpKSwxKQ0KICBiIDwtIGNvbXBsZXRlKHN1cHByZXNzV2FybmluZ3MobWljZShkYXQsIG09TV9JTVAsIG1heGl0PTUsIHByZWRpY3Rvck1hdHJpeD1wbV90ZSwNCiAgICAgICAgICAgICAgICAgaWdub3JlPWlnLCBzZWVkPXNlZWQrMSwgcHJpbnRGbGFnPUZBTFNFKSksMSkNCiAgbGlzdCh0cmFpbj1hW2NvbWIkLnNwbGl0PT0idHJhaW4iLCxkcm9wPUZBTFNFXSwNCiAgICAgICB2YWwgID1iW2NvbWIkLnNwbGl0PT0idmFsIiwgICxkcm9wPUZBTFNFXSwNCiAgICAgICB0ZXN0ID1iW2NvbWIkLnNwbGl0PT0idGVzdCIsICxkcm9wPUZBTFNFXSkNCn0NCnNpY2UgPC0gaW1wdXRlX3NpY2UodHJhaW5zZXQsIHZhbHNldCwgdGVzdHNldCkNCm1lZCAgPC0gbWVkaWFuKGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHRyYWluc2V0JGJtaSkpLCBuYS5ybT1UUlVFKQ0KDQp0cmFpbl9tMSA8LSBpbXB1dGVfYmFzZWxpbmUodHJhaW5zZXQsIG1lZCk7IHZhbF9tMSA8LSBpbXB1dGVfYmFzZWxpbmUodmFsc2V0LCBtZWQpOyB0ZXN0X20xIDwtIGltcHV0ZV9iYXNlbGluZSh0ZXN0c2V0LCBtZWQpDQp0cmFpbl9tMiA8LSBpbXB1dGVfYmFzZWxpbmUoc2ljZSR0cmFpbiwgbWVkKTsgdmFsX20yIDwtIGltcHV0ZV9iYXNlbGluZShzaWNlJHZhbCwgbWVkKTsgdGVzdF9tMiA8LSBpbXB1dGVfYmFzZWxpbmUoc2ljZSR0ZXN0LCBtZWQpDQp0cmFpbl9tMyA8LSBwcmVwX2NvbnRpbnVvdXModHJhaW5zZXQsIG1lZCk7IHZhbF9tMyA8LSBwcmVwX2NvbnRpbnVvdXModmFsc2V0LCBtZWQpOyB0ZXN0X20zIDwtIHByZXBfY29udGludW91cyh0ZXN0c2V0LCBtZWQpDQp0cmFpbl9tNCA8LSBhcHBseV9zbW90ZW5jKHRyYWluX20xKTsgdmFsX200IDwtIHZhbF9tMTsgdGVzdF9tNCA8LSB0ZXN0X20xDQp0cmFpbl9tNV9iYXNlIDwtIHByZXBfY29udGludW91cyhzaWNlJHRyYWluLCBtZWQpDQp0cmFpbl9tNSA8LSBhcHBseV9zbW90ZW5jKHRyYWluX201X2Jhc2UpDQp2YWxfbTUgPC0gcHJlcF9jb250aW51b3VzKHNpY2UkdmFsLCBtZWQpOyB0ZXN0X201IDwtIHByZXBfY29udGludW91cyhzaWNlJHRlc3QsIG1lZCkNCnRyYWluX202IDwtIHRyYWluX201X2Jhc2U7IHZhbF9tNiA8LSB2YWxfbTU7IHRlc3RfbTYgPC0gdGVzdF9tNQ0KdHJhaW5fbTcgPC0gYXBwbHlfc21vdGVuYyh0cmFpbl9tMyk7IHZhbF9tNyA8LSB2YWxfbTM7IHRlc3RfbTcgPC0gdGVzdF9tMw0KIyBbUjEuMmNdIE04IG5vdyB1c2VzIFNJQ0UgdmFsL3Rlc3QgKHYxIHdyb25nbHkgdXNlZCBtZWRpYW4taW1wdXRlZCBzZXRzKQ0KdHJhaW5fbTggPC0gYXBwbHlfc21vdGVuYyh0cmFpbl9tMik7IHZhbF9tOCA8LSB2YWxfbTI7IHRlc3RfbTggPC0gdGVzdF9tMg0KY2F0KCJFaWdodCBhcm1zICgyeDJ4MikgYnVpbHQgd2l0aCBsZWFrYWdlLXNhZmUgaW1wdXRhdGlvbi5cbiIpDQpgYGANCg0KIyAwNiAgQXNzb2NpYXRpb24gc2NyZWVuIG9uIHRyYWluaW5nIGRhdGEsIHN1cHBsZW1lbnRhcnkgdXNlIG9ubHkNCmBgYHtyIGFzc29jfQ0KDQphc3NvY190YmwgPC0gYmluZF9yb3dzKA0KICBtYXBfZGZyKGMoImdlbmRlciIsImh5cGVydGVuc2lvbiIsImhlYXJ0X2Rpc2Vhc2UiLCJldmVyX21hcnJpZWQiLCJ3b3JrX3R5cGUiLA0KICAgICAgICAgICAgIlJlc2lkZW5jZV90eXBlIiwic21va2luZ19zdGF0dXMiKSwNCiAgICB+ZGF0YS5mcmFtZShWYXJpYWJsZT0ueCwgVHlwZT0iY2F0ZWdvcmljYWwiLA0KICAgICAgQXNzb2M9cm91bmQoYXNzb2NzdGF0cyh0YWJsZSh0cmFpbl9tM1tbLnhdXSwgdHJhaW5fbTMkc3Ryb2tlKSkkY3JhbWVyLDQpKSksDQogIG1hcF9kZnIoYygiYWdlIiwiYXZnX2dsdWNvc2VfbGV2ZWwiLCJibWkiKSwNCiAgICB+ZGF0YS5mcmFtZShWYXJpYWJsZT0ueCwgVHlwZT0iY29udGludW91cyIsDQogICAgICBBc3NvYz1yb3VuZChjb3IoYXMubnVtZXJpYyh0cmFpbl9tM1tbLnhdXSksDQogICAgICAgIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHRyYWluX20zJHN0cm9rZSkpLCB1c2U9ImNvbXBsZXRlLm9icyIpLDQpKSkNCikgJT4lIGFycmFuZ2UoZGVzYyhhYnMoQXNzb2MpKSkNCnByaW50KGFzc29jX3RibCk7IHdyaXRlLmNzdihhc3NvY190YmwsInYzX3N1cHBfYXNzb2NfVFJBSU5fT05MWS5jc3YiLHJvdy5uYW1lcz1GQUxTRSkNCmBgYA0KDQojIDA3ICBTdHJ1Y3R1cmFsIGNvbnN0cmFpbnRzIOKAlCBzcGVjaWZpZWQgYSBwcmlvcmkgZnJvbSB0aGUgbGl0ZXJhdHVyZQ0KYGBge3IgY29uc3RyYWludHN9DQphbGxfbm9kZXMgPC0gYygiYWdlIiwiZ2VuZGVyIiwiaHlwZXJ0ZW5zaW9uIiwiaGVhcnRfZGlzZWFzZSIsImV2ZXJfbWFycmllZCIsDQogICAgICAgICAgICAgICAid29ya190eXBlIiwiUmVzaWRlbmNlX3R5cGUiLCJhdmdfZ2x1Y29zZV9sZXZlbCIsImJtaSIsDQogICAgICAgICAgICAgICAic21va2luZ19zdGF0dXMiLCJzdHJva2UiKQ0KZGlzY3JldGVfbm9kZXMgPC0gc2V0ZGlmZihhbGxfbm9kZXMsIGMoImFnZSIsImF2Z19nbHVjb3NlX2xldmVsIiwiYm1pIiwic3Ryb2tlIikpDQoNCndsX2Rpc2NyZXRlIDwtIG1hdHJpeChjKCJhZ2UiLCJzdHJva2UiLCJoZWFydF9kaXNlYXNlIiwic3Ryb2tlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJoeXBlcnRlbnNpb24iLCJzdHJva2UiLCJhdmdfZ2x1Y29zZV9sZXZlbCIsInN0cm9rZSIpLA0KICAgICAgICAgICAgICAgICAgICAgIG5jb2w9MiwgYnlyb3c9VFJVRSwgZGltbmFtZXM9bGlzdChOVUxMLGMoImZyb20iLCJ0byIpKSkNCmJsX2Rpc2NyZXRlIDwtIGJpbmRfcm93cygNCiAgZXhwYW5kLmdyaWQoZnJvbT1hbGxfbm9kZXMsIHRvPWMoImFnZSIsImdlbmRlciIpLCBzdHJpbmdzQXNGYWN0b3JzPUZBTFNFKSAlPiUgZmlsdGVyKGZyb20hPXRvKSwNCiAgZXhwYW5kLmdyaWQoZnJvbT0ic3Ryb2tlIiwgIHRvPWFsbF9ub2RlcywgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnM9RkFMU0UpICU+JSBmaWx0ZXIodG8hPSJzdHJva2UiKQ0KKSAlPiUgYXMubWF0cml4KCk7IGNvbG5hbWVzKGJsX2Rpc2NyZXRlKSA8LSBjKCJmcm9tIiwidG8iKQ0KDQojIGNvbnRpbnVvdXMgbm9kZXMgY2Fubm90IHBhcmVudCBkaXNjcmV0ZSBub2RlcyAtPiBhcmNzIHJldmVyc2UNCndsX2h5YnJpZCA8LSBtYXRyaXgoYygic3Ryb2tlIiwiYWdlIiwiaGVhcnRfZGlzZWFzZSIsInN0cm9rZSIsDQogICAgICAgICAgICAgICAgICAgICAgImh5cGVydGVuc2lvbiIsInN0cm9rZSIsInN0cm9rZSIsImF2Z19nbHVjb3NlX2xldmVsIiksDQogICAgICAgICAgICAgICAgICAgIG5jb2w9MiwgYnlyb3c9VFJVRSwgZGltbmFtZXM9bGlzdChOVUxMLGMoImZyb20iLCJ0byIpKSkNCmJsX2h5YnJpZCA8LSBiaW5kX3Jvd3MoDQogIGV4cGFuZC5ncmlkKGZyb209YWxsX25vZGVzLCB0bz0iZ2VuZGVyIiwgICAgICAgc3RyaW5nc0FzRmFjdG9ycz1GQUxTRSkgJT4lIGZpbHRlcihmcm9tIT10byksDQogIGV4cGFuZC5ncmlkKGZyb209InN0cm9rZSIsICB0bz1kaXNjcmV0ZV9ub2Rlcywgc3RyaW5nc0FzRmFjdG9ycz1GQUxTRSkgJT4lIGZpbHRlcih0byE9InN0cm9rZSIpDQopICU+JSBhcy5tYXRyaXgoKTsgY29sbmFtZXMoYmxfaHlicmlkKSA8LSBjKCJmcm9tIiwidG8iKQ0KY2F0KCJXaGl0ZWxpc3QgYW5kIGJsYWNrbGlzdCBjcmVhdGUgYmFzZWQgb24gbGl0ZXJhdHVyZS4gXG4iKQ0KYGBgDQoNCiMgMDggIENvcmUgZXZhbHVhdGlvbiBmdW5jdGlvbg0KYGBge3IgZXZhbC1mbn0NCg0KZXZhbHVhdGVfdmFyaWFudCA8LSBmdW5jdGlvbih0cmFpbl9kZiwgdmFsX2RmLCB0ZXN0X2RmLCBtb2RlbF9uYW1lLCBib290X1IgPSAyMDApIHsNCiAgaXNfZGlzYyA8LSBhbGwoc2FwcGx5KHRyYWluX2RmLCBpcy5mYWN0b3IpKQ0KICB3bCA8LSBpZiAoaXNfZGlzYykgd2xfZGlzY3JldGUgZWxzZSB3bF9oeWJyaWQNCiAgYmwgPC0gaWYgKGlzX2Rpc2MpIGJsX2Rpc2NyZXRlIGVsc2UgYmxfaHlicmlkDQoNCiAgZGFnIDwtIGhjKHRyYWluX2RmLCB3aGl0ZWxpc3Q9d2wsIGJsYWNrbGlzdD1ibCkNCiAgYXJjX3N0ciA8LSBpZiAoYm9vdF9SPjApIGJvb3Quc3RyZW5ndGgodHJhaW5fZGYsIFI9Ym9vdF9SLCBhbGdvcml0aG09ImhjIiwNCiAgICAgICAgICAgICAgICAgYWxnb3JpdGhtLmFyZ3M9bGlzdCh3aGl0ZWxpc3Q9d2wsIGJsYWNrbGlzdD1ibCkpIGVsc2UgTlVMTA0KICBmaXR0ZWQgPC0gaWYgKGlzX2Rpc2MpIGJuLmZpdChkYWcsIHRyYWluX2RmLCBtZXRob2Q9ImJheWVzIiwgaXNzPTEwKSBlbHNlIGJuLmZpdChkYWcsIHRyYWluX2RmKQ0KDQogICMgW1IxLjViXSBsaWtlbGlob29kIHdlaWdodGluZyBmb3IgQk9USCBhcm0gdHlwZXMNCiAgZ2V0X3Byb2JzIDwtIGZ1bmN0aW9uKHRkLCBmaXQ9Zml0dGVkLCBkaXNjPWlzX2Rpc2MpIHsNCiAgICBpZiAoZGlzYykgeyBwcCA8LSBwcmVkaWN0KGZpdCwgbm9kZT0ic3Ryb2tlIiwgZGF0YT10ZCwgbWV0aG9kPSJiYXllcy1sdyIsIHByb2I9VFJVRSkNCiAgICAgICAgICAgICAgICByZXR1cm4oYXR0cihwcCwicHJvYiIpWyIxIixdKSB9DQogICAgZWMgPC0gc2V0ZGlmZihuYW1lcyh0ZCksInN0cm9rZSIpDQogICAgdmFwcGx5KHNlcV9sZW4obnJvdyh0ZCkpLCBmdW5jdGlvbihpKXsNCiAgICAgIHAgPC0gc3VwcHJlc3NXYXJuaW5ncyhjcHF1ZXJ5KGZpdCwgZXZlbnQ9KHN0cm9rZT09IjEiKSwNCiAgICAgICAgICAgICBldmlkZW5jZT1hcy5saXN0KHRkW2ksZWNdKSwgbWV0aG9kPSJsdyIsIG49Tl9QQVJUSUNMRVNfUEFUSUVOVCkpDQogICAgICBpZiAoaXMubmEocCkpIDAgZWxzZSBwfSwgbnVtZXJpYygxKSkNCiAgfQ0KDQogIHZwIDwtIGdldF9wcm9icyh2YWxfZGYpOyByb2NfdiA8LSByb2ModmFsX2RmJHN0cm9rZSwgdnAsIGxldmVscz1jKCIwIiwiMSIpLCBxdWlldD1UUlVFKQ0KICB0aCA8LSBjb29yZHMocm9jX3YsImJlc3QiLHJldD0idGhyZXNob2xkIixiZXN0Lm1ldGhvZD0ieW91ZGVuIikkdGhyZXNob2xkWzFdDQogIGlmIChpcy5uYSh0aCkpIHRoIDwtIDAuNQ0KICB0cCA8LSBnZXRfcHJvYnModGVzdF9kZik7IHJvY190IDwtIHJvYyh0ZXN0X2RmJHN0cm9rZSwgdHAsIGxldmVscz1jKCIwIiwiMSIpLCBxdWlldD1UUlVFKQ0KICBjbSA8LSBjb25mdXNpb25NYXRyaXgoZmFjdG9yKGlmZWxzZSh0cD49dGgsIjEiLCIwIiksbGV2ZWxzPWMoIjAiLCIxIikpLA0KICAgICAgICAgICAgICAgICAgICAgICAgdGVzdF9kZiRzdHJva2UsIHBvc2l0aXZlPSIxIikNCg0KICBsaXN0KHJlc3VsdHM9ZGF0YS5mcmFtZShNb2RlbD1tb2RlbF9uYW1lLCBWYWxfVGhyZXNob2xkPXJvdW5kKHRoLDQpLA0KICAgICAgICAgVmFsX0FVQz1yb3VuZChhcy5udW1lcmljKGF1Yyhyb2NfdikpLDQpLA0KICAgICAgICAgVGVzdF9BVUM9cm91bmQoYXMubnVtZXJpYyhhdWMocm9jX3QpKSw0KSwNCiAgICAgICAgIFRlc3RfU2Vuc2l0aXZpdHk9cm91bmQoY20kYnlDbGFzc1siU2Vuc2l0aXZpdHkiXSw0KSwNCiAgICAgICAgIFRlc3RfU3BlY2lmaWNpdHk9cm91bmQoY20kYnlDbGFzc1siU3BlY2lmaWNpdHkiXSw0KSwNCiAgICAgICAgIFRlc3RfWW91ZGVuX0o9cm91bmQoY20kYnlDbGFzc1siU2Vuc2l0aXZpdHkiXStjbSRieUNsYXNzWyJTcGVjaWZpY2l0eSJdLTEsNCksDQogICAgICAgICBUZXN0X0YxPXJvdW5kKGFzLm51bWVyaWMoY20kYnlDbGFzc1siRjEiXSksNCkpLA0KICAgICAgIHJvYz1yb2NfdCwgcm9jX3ZhbD1yb2NfdiwgZml0PWZpdHRlZCwgc3RydWN0dXJlPWRhZywgYXJjX3N0cmVuZ3RoPWFyY19zdHIsDQogICAgICAgY29uZnVzaW9uX21hdHJpeD1jbSwgaXNfZGlzYz1pc19kaXNjLCB3bD13bCwgYmw9YmwsIHRyYWluPXRyYWluX2RmLCB0ZXN0PXRlc3RfZGYpDQp9DQpgYGANCg0KIyAwOSAgTWFpbiByZXN1bHQg4oCUIHRyYWluIGFybXMsIHRoZW4gc2VsZWN0IG9uIFZBTElEQVRJT04NCmBgYHtyIG1haW4tbG9vcH0NCmV4cGVyaW1lbnRzIDwtIGxpc3QoDQogIGxpc3QodHJhaW5fbTEsdmFsX20xLHRlc3RfbTEsIk0xOiBCYXNlbGluZSIpLA0KICBsaXN0KHRyYWluX20yLHZhbF9tMix0ZXN0X20yLCJNMjogU0lDRSArIERpc2NyZXRpemUiKSwNCiAgbGlzdCh0cmFpbl9tMyx2YWxfbTMsdGVzdF9tMywiTTM6IENvbnRpbnVvdXMgTm9kZXMiKSwNCiAgbGlzdCh0cmFpbl9tNCx2YWxfbTQsdGVzdF9tNCwiTTQ6IFNNT1RFIG9uIEJhc2VsaW5lIiksDQogIGxpc3QodHJhaW5fbTUsdmFsX201LHRlc3RfbTUsIk01OiBBTEwgKFNJQ0UgKyBDb250ICsgU01PVEUpIiksDQogIGxpc3QodHJhaW5fbTYsdmFsX202LHRlc3RfbTYsIk02OiBTSUNFICsgQ29udGludW91cyIpLA0KICBsaXN0KHRyYWluX203LHZhbF9tNyx0ZXN0X203LCJNNzogQ29udGludW91cyArIFNNT1RFIiksDQogIGxpc3QodHJhaW5fbTgsdmFsX204LHRlc3RfbTgsIk04OiBTSUNFICsgU01PVEUiKSkNCg0KYWJsYXRpb24gPC0gZGF0YS5mcmFtZSgpOyByb2NfbGlzdCA8LSBsaXN0KCk7IGRhZ3MgPC0gbGlzdCgpDQphcmNzX2wgPC0gbGlzdCgpOyBjbXMgPC0gbGlzdCgpOyBmaXRzIDwtIGxpc3QoKTsgcnVucyA8LSBsaXN0KCkNCmZvciAoZXggaW4gZXhwZXJpbWVudHMpIHsNCiAgdHJ5Q2F0Y2goew0KICAgIHIgPC0gZXZhbHVhdGVfdmFyaWFudChleFtbMV1dLGV4W1syXV0sZXhbWzNdXSxleFtbNF1dLGJvb3RfUj0yMDApDQogICAgYWJsYXRpb24gPC0gcmJpbmQoYWJsYXRpb24sIHIkcmVzdWx0cyk7IHJvY19saXN0W1tleFtbNF1dXV0gPC0gciRyb2MNCiAgICBkYWdzW1tleFtbNF1dXV0gPC0gciRzdHJ1Y3R1cmU7IGFyY3NfbFtbZXhbWzRdXV1dIDwtIHIkYXJjX3N0cmVuZ3RoDQogICAgY21zW1tleFtbNF1dXV0gPC0gciRjb25mdXNpb25fbWF0cml4OyBmaXRzW1tleFtbNF1dXV0gPC0gciRmaXQ7IHJ1bnNbW2V4W1s0XV1dXSA8LSByDQogIH0sIGVycm9yPWZ1bmN0aW9uKGUpIGNhdChzcHJpbnRmKCJFcnJvciAlczogJXNcbiIsIGV4W1s0XV0sIGUkbWVzc2FnZSkpKQ0KfQ0KDQpsZWFkZXJib2FyZCA8LSBhYmxhdGlvbg0KbGVhZGVyYm9hcmQkVGVzdF9BVUNfOTVfQ0kgPC0gdmFwcGx5KGxlYWRlcmJvYXJkJE1vZGVsLCBmdW5jdGlvbihtKXsNCiAgY2kgPC0gY2kuYXVjKHJvY19saXN0W1ttXV0sIG1ldGhvZD0iYm9vdHN0cmFwIiwgYm9vdC5uPTIwMDAsIHF1aWV0PVRSVUUpDQogIHNwcmludGYoIlslLjRmIC0gJS40Zl0iLCBjaVsxXSwgY2lbM10pfSwgY2hhcmFjdGVyKDEpKQ0KDQojIHNlbGVjdGlvbiBvbiBWQUxJREFUSU9OIG9ubHksIHdpdGggdGhlIDAuMDA1IHBhcnNpbW9ueSBydWxlDQp2ciA8LSBsZWFkZXJib2FyZCAlPiUgYXJyYW5nZShkZXNjKFZhbF9BVUMpKQ0KbmVhciA8LSB2ciAlPiUgZmlsdGVyKFZhbF9BVUMgPj0gbWF4KFZhbF9BVUMpIC0gMC4wMDUpDQpzaW1wbGVfcmFuayA8LSBjKCJNMSIsIk0zIiwiTTIiLCJNNiIsIk00IiwiTTciLCJNOCIsIk01IikNCm5lYXIkc2ltcCA8LSBtYXRjaChzdWIoIjouKiIsIiIsbmVhciRNb2RlbCksIHNpbXBsZV9yYW5rKQ0KUFJJTUFSWSA8LSBuZWFyJE1vZGVsW3doaWNoLm1pbihuZWFyJHNpbXApXQ0KY2F0KCJcbkJlc3QgdmFsaWRhdGlvbiBBVUM6IiwgdnIkTW9kZWxbMV0sIHNwcmludGYoIiglLjRmKSIsIHZyJFZhbF9BVUNbMV0pLA0KICAgICJcblBSSU1BUlkgYWZ0ZXIgMC4wMDUgcGFyc2ltb255IHJ1bGU6IiwgUFJJTUFSWSwNCiAgICAiXG5JdHMgdW5iaWFzZWQgVEVTVCBBVUM6IiwgbGVhZGVyYm9hcmQkVGVzdF9BVUNbbGVhZGVyYm9hcmQkTW9kZWw9PVBSSU1BUlldLCAiXG4iKQ0KDQpsZWFkZXJib2FyZCA8LSBsZWFkZXJib2FyZCAlPiUgcmVsb2NhdGUoVGVzdF9BVUNfOTVfQ0ksLmFmdGVyPVRlc3RfQVVDKSAlPiUNCiAgYXJyYW5nZShkZXNjKFRlc3RfQVVDKSwgZGVzYyhUZXN0X1lvdWRlbl9KKSkNCnByaW50KGxlYWRlcmJvYXJkKTsgd3JpdGUuY3N2KGxlYWRlcmJvYXJkLCJ2M19sZWFkZXJib2FyZC5jc3YiLHJvdy5uYW1lcz1GQUxTRSkNCmBgYA0KDQojIDA5YiAgRmlndXJlIDQg4oCUIERBRyBvZiB0aGUgcHJpbWFyeSBtb2RlbCwgd2l0aCBNMy12cy1NNiBzdHJ1Y3R1cmFsIGNoZWNrDQpgYGB7ciBkYWctcHJpbWFyeX0NCmxpYnJhcnkoYm5sZWFybikNCmxpYnJhcnkoaWdyYXBoKQ0KbGlicmFyeShnZ3JhcGgpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGRwbHlyKQ0KDQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAxLiBQcmltYXJ5IE1vZGVsIFNldHVwICYgU3RydWN0dXJhbCBDaGVjaw0KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCmRhZ19wcmltYXJ5ICA8LSBkYWdzW1tQUklNQVJZXV0NCmFzdHJfcHJpbWFyeSA8LSBhcmNzX2xbW1BSSU1BUlldXQ0KDQpjYXQoIlBsb3R0aW5nIERBRyBmb3IgUFJJTUFSWSA9IiwgUFJJTUFSWSwgIlxuIikNCmNhdCgiUGFyZW50cyBvZiBzdHJva2U6IiwgcGFzdGUocGFyZW50cyhkYWdfcHJpbWFyeSwgInN0cm9rZSIpLCBjb2xsYXBzZSA9ICIsICIpLCAiXG4iKQ0KY2F0KCJDaGlsZHJlbiBvZiBzdHJva2U6IiwgcGFzdGUoY2hpbGRyZW4oZGFnX3ByaW1hcnksICJzdHJva2UiKSwgY29sbGFwc2UgPSAiLCAiKSwgIlxuIikNCg0KIyBTdHJ1Y3R1cmFsIGNoZWNrOiBDb21wYXJlIFBSSU1BUlkgYWdhaW5zdCBNNg0KTTZfTkFNRSA8LSAiTTY6IFNJQ0UgKyBDb250aW51b3VzIg0KaWYgKFBSSU1BUlkgIT0gTTZfTkFNRSAmJiBNNl9OQU1FICVpbiUgbmFtZXMoZGFncykpIHsNCiAgYV9wcmltYXJ5IDwtIGFwcGx5KGFyY3MoZGFnc1tbUFJJTUFSWV1dKSwgMSwgcGFzdGUsIGNvbGxhcHNlID0gIi0+IikNCiAgYV9tNiAgICAgIDwtIGFwcGx5KGFyY3MoZGFnc1tbTTZfTkFNRV1dKSwgMSwgcGFzdGUsIGNvbGxhcHNlID0gIi0+IikNCiAgb25seV9wcmltYXJ5IDwtIHNldGRpZmYoYV9wcmltYXJ5LCBhX202KQ0KICBvbmx5X202ICAgICAgPC0gc2V0ZGlmZihhX202LCBhX3ByaW1hcnkpDQogIGlkZW50aWNhbF9zdHJ1Y3R1cmVzIDwtIGxlbmd0aChvbmx5X3ByaW1hcnkpID09IDAgJiYgbGVuZ3RoKG9ubHlfbTYpID09IDANCg0KICBjYXQoIlxuW0ZpZ3VyZSA0IGNoZWNrXSBJcyIsIFBSSU1BUlksICJzdHJ1Y3R1cmFsbHkgaWRlbnRpY2FsIHRvIiwgTTZfTkFNRSwgIj9cbiIpDQogIGNhdCgiICBJZGVudGljYWw6IiwgaWRlbnRpY2FsX3N0cnVjdHVyZXMsICJcbiIpDQogIGlmICghaWRlbnRpY2FsX3N0cnVjdHVyZXMpIHsNCiAgICBjYXQoIiAgQXJjcyBpbiIsIFBSSU1BUlksICJidXQgbm90IiwgTTZfTkFNRSwgIjpcbiIpOyBwcmludChvbmx5X3ByaW1hcnkpDQogICAgY2F0KCIgIEFyY3MgaW4iLCBNNl9OQU1FLCAiYnV0IG5vdCIsIFBSSU1BUlksICI6XG4iKTsgcHJpbnQob25seV9tNikNCiAgfQ0KICB3cml0ZS5jc3YoZGF0YS5mcmFtZShvbmx5X2luX3ByaW1hcnkgPSBwYXN0ZShvbmx5X3ByaW1hcnksIGNvbGxhcHNlID0gIjsgIiksDQogICAgICAgICAgICAgICAgICAgICAgICBvbmx5X2luX202ICAgICAgPSBwYXN0ZShvbmx5X202LCBjb2xsYXBzZSA9ICI7ICIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgaWRlbnRpY2FsICAgICAgID0gaWRlbnRpY2FsX3N0cnVjdHVyZXMpLA0KICAgICAgICAgICAgInYzX2RhZ19NM192c19NNl9jaGVjay5jc3YiLCByb3cubmFtZXMgPSBGQUxTRSkNCn0NCg0KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgMi4gUHVibGljYXRpb24tUXVhbGl0eSBEQUcgUmVuZGVyaW5nIHZpYSBnZ3JhcGgNCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojIENvbnZlcnQgYm5sZWFybiBEQUcgdG8gaWdyYXBoIG9iamVjdA0KaWcgPC0gYXMuaWdyYXBoKGRhZ19wcmltYXJ5KQ0KDQojIE1hcCBib290c3RyYXAgYXJjIHN0cmVuZ3RocyBvbnRvIGlncmFwaCBlZGdlcw0KaWYgKCFpcy5udWxsKGFzdHJfcHJpbWFyeSkpIHsNCiAgc3RyX2RmIDwtIGFzLmRhdGEuZnJhbWUoYXN0cl9wcmltYXJ5KQ0KICBlX21hdCAgPC0gYXNfZWRnZWxpc3QoaWcpDQogIGVkZ2Vfc3RyZW5ndGhzIDwtIG51bWVyaWMobnJvdyhlX21hdCkpDQogIA0KICBmb3IgKGkgaW4gc2VxX2xlbihucm93KGVfbWF0KSkpIHsNCiAgICB1IDwtIGVfbWF0W2ksIDFdDQogICAgdiA8LSBlX21hdFtpLCAyXQ0KICAgIG1hdGNoX2lkeCA8LSB3aGljaChzdHJfZGYkZnJvbSA9PSB1ICYgc3RyX2RmJHRvID09IHYpDQogICAgaWYgKGxlbmd0aChtYXRjaF9pZHgpID4gMCkgew0KICAgICAgZWRnZV9zdHJlbmd0aHNbaV0gPC0gc3RyX2RmJHN0cmVuZ3RoW21hdGNoX2lkeFsxXV0NCiAgICB9IGVsc2Ugew0KICAgICAgZWRnZV9zdHJlbmd0aHNbaV0gPC0gMC41DQogICAgfQ0KICB9DQogIEUoaWcpJHN0cmVuZ3RoIDwtIGVkZ2Vfc3RyZW5ndGhzDQp9IGVsc2Ugew0KICBFKGlnKSRzdHJlbmd0aCA8LSAxLjANCn0NCg0KIyBDbGVhbiB2YXJpYWJsZSBkaXNwbGF5IGxhYmVscw0KY2xlYW5fbGFiZWxzIDwtIGMoDQogICJzdHJva2UiICAgICAgICAgICAgPSAiU3Ryb2tlXG4oVGFyZ2V0KSIsDQogICJhZ2UiICAgICAgICAgICAgICAgPSAiQWdlIiwNCiAgImJtaSIgICAgICAgICAgICAgICA9ICJCTUkiLA0KICAiYXZnX2dsdWNvc2VfbGV2ZWwiID0gIkF2Zy4gR2x1Y29zZSIsDQogICJoeXBlcnRlbnNpb24iICAgICAgPSAiSHlwZXJ0ZW5zaW9uIiwNCiAgImhlYXJ0X2Rpc2Vhc2UiICAgICA9ICJIZWFydCBEaXNlYXNlIiwNCiAgImV2ZXJfbWFycmllZCIgICAgICA9ICJFdmVyIE1hcnJpZWQiLA0KICAid29ya190eXBlIiAgICAgICAgID0gIldvcmsgVHlwZSIsDQogICJzbW9raW5nX3N0YXR1cyIgICAgPSAiU21va2luZyBTdGF0dXMiLA0KICAiZ2VuZGVyIiAgICAgICAgICAgID0gIkdlbmRlciIsDQogICJSZXNpZGVuY2VfdHlwZSIgICAgPSAiUmVzaWRlbmNlIFR5cGUiDQopDQoNCiMgRGVmaW5lIHZpc3VhbCBub2RlIHJvbGVzDQpub2RlX3R5cGVzIDwtIGMoDQogICJzdHJva2UiICAgICAgICAgICAgPSAiVGFyZ2V0IE91dGNvbWUiLA0KICAiYWdlIiAgICAgICAgICAgICAgID0gIkNvbnRpbnVvdXMgUHJlZGljdG9yIiwNCiAgImJtaSIgICAgICAgICAgICAgICA9ICJDb250aW51b3VzIFByZWRpY3RvciIsDQogICJhdmdfZ2x1Y29zZV9sZXZlbCIgPSAiQ29udGludW91cyBQcmVkaWN0b3IiLA0KICAiaHlwZXJ0ZW5zaW9uIiAgICAgID0gIkNhdGVnb3JpY2FsIFByZWRpY3RvciIsDQogICJoZWFydF9kaXNlYXNlIiAgICAgPSAiQ2F0ZWdvcmljYWwgUHJlZGljdG9yIiwNCiAgImV2ZXJfbWFycmllZCIgICAgICA9ICJDYXRlZ29yaWNhbCBQcmVkaWN0b3IiLA0KICAid29ya190eXBlIiAgICAgICAgID0gIkNhdGVnb3JpY2FsIFByZWRpY3RvciIsDQogICJzbW9raW5nX3N0YXR1cyIgICAgPSAiQ2F0ZWdvcmljYWwgUHJlZGljdG9yIiwNCiAgImdlbmRlciIgICAgICAgICAgICA9ICJDYXRlZ29yaWNhbCBQcmVkaWN0b3IiLA0KICAiUmVzaWRlbmNlX3R5cGUiICAgID0gIkNhdGVnb3JpY2FsIFByZWRpY3RvciINCikNCg0Kdl9uYW1lcyA8LSBWKGlnKSRuYW1lDQpWKGlnKSRsYWJlbCA8LSBpZmVsc2Uodl9uYW1lcyAlaW4lIG5hbWVzKGNsZWFuX2xhYmVscyksIGNsZWFuX2xhYmVsc1t2X25hbWVzXSwgdl9uYW1lcykNClYoaWcpJHR5cGUgIDwtIGlmZWxzZSh2X25hbWVzICVpbiUgbmFtZXMobm9kZV90eXBlcyksIG5vZGVfdHlwZXNbdl9uYW1lc10sICJDYXRlZ29yaWNhbCBQcmVkaWN0b3IiKQ0KVihpZykkdHlwZSAgPC0gZmFjdG9yKFYoaWcpJHR5cGUsIGxldmVscyA9IGMoIlRhcmdldCBPdXRjb21lIiwgIkNvbnRpbnVvdXMgUHJlZGljdG9yIiwgIkNhdGVnb3JpY2FsIFByZWRpY3RvciIpKQ0KDQojIFBsb3QgdXNpbmcgaGllcmFyY2hpY2FsIFN1Z2l5YW1hIGxheW91dA0Kc2V0LnNlZWQoaWYgKGV4aXN0cygiR0xPQkFMX1NFRUQiKSkgR0xPQkFMX1NFRUQgZWxzZSA0MikNCg0KcF9kYWcgPC0gZ2dyYXBoKGlnLCBsYXlvdXQgPSAic3VnaXlhbWEiKSArDQogICMgRGlyZWN0ZWQgZWRnZXMgd2l0aCBzdHJlbmd0aC1zY2FsZWQgdGhpY2tuZXNzDQogIGdlb21fZWRnZV9saW5rKA0KICAgIGFlcyhlZGdlX3dpZHRoID0gc3RyZW5ndGgpLA0KICAgIGFycm93ID0gYXJyb3cobGVuZ3RoID0gdW5pdCgzLjUsICJtbSIpLCB0eXBlID0gImNsb3NlZCIpLA0KICAgIGVuZF9jYXAgPSBjaXJjbGUoMTAsICJtbSIpLA0KICAgIHN0YXJ0X2NhcCA9IGNpcmNsZSgxMCwgIm1tIiksDQogICAgY29sb3IgPSAiIzRBNTU2OCIsDQogICAgYWxwaGEgPSAwLjc1DQogICkgKw0KICBzY2FsZV9lZGdlX3dpZHRoX2NvbnRpbnVvdXMoDQogICAgcmFuZ2UgPSBjKDAuNiwgMi4yKSwNCiAgICBuYW1lID0gIkJvb3RzdHJhcCBBcmMgU3RyZW5ndGgiDQogICkgKw0KICAjIE5vZGUgc3R5bGluZw0KICBnZW9tX25vZGVfcG9pbnQoYWVzKGNvbG9yID0gdHlwZSksIHNpemUgPSAyMCkgKw0KICBnZW9tX25vZGVfdGV4dChhZXMobGFiZWwgPSBsYWJlbCksIGZvbnRmYWNlID0gImJvbGQiLCBzaXplID0gMi44LCBjb2xvciA9ICIjMUEyMDJDIikgKw0KICBzY2FsZV9jb2xvcl9tYW51YWwoDQogICAgdmFsdWVzID0gYygNCiAgICAgICJUYXJnZXQgT3V0Y29tZSIgICAgICAgID0gIiNFNTNFM0UiLCAjIFJlZC9Db3JhbCBmaWxsDQogICAgICAiQ29udGludW91cyBQcmVkaWN0b3IiICA9ICIjMzE4MkNFIiwgIyBTb2Z0IEJsdWUgZmlsbA0KICAgICAgIkNhdGVnb3JpY2FsIFByZWRpY3RvciIgPSAiI0UyRThGMCIgICMgTGlnaHQgU2xhdGUgR3JheSBmaWxsDQogICAgKSwNCiAgICBuYW1lID0gIk5vZGUgQ2F0ZWdvcnkiDQogICkgKw0KICB0aGVtZV92b2lkKCkgKw0KICB0aGVtZSgNCiAgICBsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwNCiAgICBsZWdlbmQuYm94ID0gImhvcml6b250YWwiLA0KICAgIGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfdGV4dChmYWNlID0gImJvbGQiLCBzaXplID0gMTApLA0KICAgIGxlZ2VuZC50ZXh0ID0gZWxlbWVudF90ZXh0KHNpemUgPSA5KSwNCiAgICBwbG90Lm1hcmdpbiA9IG1hcmdpbigxNSwgMTUsIDE1LCAxNSkNCiAgKQ0KDQojIEV4cG9ydCBjbGVhbiAzMDAgRFBJIGltYWdlDQpnZ3NhdmUoInYzX0RBR19wcmltYXJ5LnBuZyIsIHBsb3QgPSBwX2RhZywgd2lkdGggPSAxMCwgaGVpZ2h0ID0gNy41LCBkcGkgPSAzMDApDQpjYXQoIlNhdmVkIHB1YmxpY2F0aW9uLXJlYWR5IHYzX0RBR19wcmltYXJ5LnBuZ1xuIikNCg0KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgMy4gTW9kZWwgRGlhZ25vc3RpY3MgJiBMb2dzDQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KY2F0KCJcbk1vZGVsIHN0cmluZzpcbiIsIG1vZGVsc3RyaW5nKGRhZ19wcmltYXJ5KSwgIlxuIikNCmNhdCgiRnVsbCBhcmMgbGlzdCAoZnJvbSAtPiB0byk6XG4iKTsgcHJpbnQoYXJjcyhkYWdfcHJpbWFyeSkpDQpjYXQoIlxuTWFya292IGJsYW5rZXQgb2Ygc3Ryb2tlOiIsIHBhc3RlKG1iKGRhZ19wcmltYXJ5LCAic3Ryb2tlIiksIGNvbGxhcHNlID0gIiwgIiksICJcbiIpDQpgYGANCg0KDQoNCg0KIyAxMCAgUk9DIGZpZ3VyZQ0KYGBge3Igcm9jLWZpZ30NCmNiIDwtIGMoIiM5OTk5OTkiLCIjRTY5RjAwIiwiIzU2QjRFOSIsIiMwMDlFNzMiLCIjRjBFNDQyIiwiIzAwNzJCMiIsIiNENTVFMDAiLCIjQ0M3OUE3IikNCnJwIDwtIHJvY19saXN0OyBuYW1lcyhycCkgPC0gZ3N1YigiIFxcKC4qXFwpIiwiIixuYW1lcyhycCkpDQpnIDwtIHBST0M6Omdncm9jKHJwLCBsZWdhY3kuYXhlcz1UUlVFLCBzaXplPTEpICsNCiAgZ2VvbV9hYmxpbmUoaW50ZXJjZXB0PTAsIHNsb3BlPTEsIGNvbG91cj0iZ3JleTUwIiwgbGluZXR5cGU9ImRhc2hlZCIpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcz1jYikgKw0KICBsYWJzKHg9IkZhbHNlIFBvc2l0aXZlIFJhdGUgKDEgLSBTcGVjaWZpY2l0eSkiLCB5PSJUcnVlIFBvc2l0aXZlIFJhdGUgKFNlbnNpdGl2aXR5KSIpICsNCiAgdGhlbWVfbWluaW1hbCgpICsgdGhlbWUobGVnZW5kLnRpdGxlPWVsZW1lbnRfYmxhbmsoKSkNCmdnc2F2ZSgidjNfUk9DLnBuZyIsIGcsIHdpZHRoPTgsIGhlaWdodD02LCBkcGk9MzAwKTsgcHJpbnQoZykNCmBgYA0KDQojIDExICBDYWxpYnJhdGlvbiDigJQgcmVmZXJlbmNlIG1vZGVsLCB3ZWFrIGNhbGlicmF0aW9uLCBwcmlvciBjb3JyZWN0aW9uDQpgYGB7ciBjYWxpYnJhdGlvbn0NCiNBIHByZXZhbGVuY2Utb25seSBtb2RlbCBhdHRhaW5zIEJyaWVyIH49IHAoMS1wKSB+PSAwLjA0NyBoZXJlLCBzbyByYXcNCiMgQnJpZXIgYWxvbmUgY2Fubm90IGRlbW9uc3RyYXRlIGNhbGlicmF0aW9uLiBCU1MsIGludGVyY2VwdCBhbmQgc2xvcGUgYXJlIGFkZGVkLg0KY2FsaWJfbWV0cmljcyA8LSBmdW5jdGlvbih5LHAsZXBzPTFlLTYpew0KICB5IDwtIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHkpKTsgcCA8LSBwbWluKHBtYXgocCxlcHMpLDEtZXBzKQ0KICBicmllciA8LSBtZWFuKChwLXkpXjIpOyByZWYgPC0gbWVhbigobWVhbih5KS15KV4yKTsgbHAgPC0gcWxvZ2lzKHApDQogIGRhdGEuZnJhbWUoQnJpZXI9cm91bmQoYnJpZXIsNCksIEJyaWVyX3JlZj1yb3VuZChyZWYsNCksDQogICAgICAgICAgICAgQlNTPXJvdW5kKDEtYnJpZXIvcmVmLDQpLA0KICAgICAgICAgICAgIENhbGliX2ludGVyY2VwdD1yb3VuZCh1bm5hbWUoY29lZihnbG0oeX5vZmZzZXQobHApLGZhbWlseT1iaW5vbWlhbCkpWzFdKSwzKSwNCiAgICAgICAgICAgICBDYWxpYl9zbG9wZT1yb3VuZCh1bm5hbWUoY29lZihnbG0oeX5scCxmYW1pbHk9Ymlub21pYWwpKVsyXSksMykpDQp9DQpwcmlvcl9jb3JyZWN0IDwtIGZ1bmN0aW9uKHAsIHBpX3RyYWluPTAuNSwgcGlfcG9wPVBJX1BPUCwgZXBzPTFlLTYpew0KICBwIDwtIHBtaW4ocG1heChwLGVwcyksMS1lcHMpDQogIHBsb2dpcyhxbG9naXMocCkgLSBsb2coKHBpX3RyYWluLygxLXBpX3RyYWluKSkvKHBpX3BvcC8oMS1waV9wb3ApKSkpDQp9DQpTTU9URV9BUk1TIDwtIGMoIk00IiwiTTUiLCJNNyIsIk04IikNCmNhbGliIDwtIG1hcF9kZnIobmFtZXMocm9jX2xpc3QpLCBmdW5jdGlvbihtKXsNCiAgeSA8LSByb2NfbGlzdFtbbV1dJHJlc3BvbnNlOyBwIDwtIHJvY19saXN0W1ttXV0kcHJlZGljdG9yDQogIG91dCA8LSBjYmluZChNb2RlbD1tLCBDb3JyZWN0aW9uPSJub25lIiwgY2FsaWJfbWV0cmljcyh5LHApKQ0KICBpZiAoYW55KHNhcHBseShTTU9URV9BUk1TLCBmdW5jdGlvbihhKSBzdGFydHNXaXRoKG0sYSkpKSkNCiAgICBvdXQgPC0gYmluZF9yb3dzKG91dCwgY2JpbmQoTW9kZWw9bSwgQ29ycmVjdGlvbj0icHJpb3ItY29ycmVjdGVkIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2FsaWJfbWV0cmljcyh5LCBwcmlvcl9jb3JyZWN0KHApKSkpDQogIG91dCB9KQ0KcHJpbnQoY2FsaWIpOyB3cml0ZS5jc3YoY2FsaWIsInYzX2NhbGlicmF0aW9uLmNzdiIscm93Lm5hbWVzPUZBTFNFKQ0KDQpjYWxpYl9jdXJ2ZSA8LSBmdW5jdGlvbihtLGJpbnM9MTApew0KICB5IDwtIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHJvY19saXN0W1ttXV0kcmVzcG9uc2UpKTsgcCA8LSByb2NfbGlzdFtbbV1dJHByZWRpY3Rvcg0KICBiciA8LSB1bmlxdWUocXVhbnRpbGUocCwgcHJvYnM9c2VxKDAsMSxsZW5ndGgub3V0PWJpbnMrMSksIG5hLnJtPVRSVUUpKQ0KICBnZyA8LSBpZiAobGVuZ3RoKGJyKTwzKSBjdXQocCwgYnJlYWtzPXVuaXF1ZShjKC1JbmYsc29ydCh1bmlxdWUocCkpKSksIGluY2x1ZGUubG93ZXN0PVRSVUUpDQogICAgICAgIGVsc2UgY3V0KHAsIGJyZWFrcz1iciwgaW5jbHVkZS5sb3dlc3Q9VFJVRSkNCiAgbmEub21pdChkYXRhLmZyYW1lKG1vZGVsPW0sIHByZWQ9dGFwcGx5KHAsZ2csbWVhbiksIG9icz10YXBwbHkoeSxnZyxtZWFuKSkpDQp9DQpmb2N1cyA8LSBpbnRlcnNlY3QoYygiTTE6IEJhc2VsaW5lIiwiTTM6IENvbnRpbnVvdXMgTm9kZXMiLCJNNjogU0lDRSArIENvbnRpbnVvdXMiLA0KICAgICAgICAgICAgICAgICAgICAgIk01OiBBTEwgKFNJQ0UgKyBDb250ICsgU01PVEUpIiksIG5hbWVzKHJvY19saXN0KSkNCmNwIDwtIGdncGxvdChiaW5kX3Jvd3MobGFwcGx5KGZvY3VzLGNhbGliX2N1cnZlKSksIGFlcyhwcmVkLG9icyxjb2xvdXI9bW9kZWwpKSArDQogIGdlb21fYWJsaW5lKHNsb3BlPTEsaW50ZXJjZXB0PTAsbGluZXR5cGU9ImRhc2hlZCIsY29sb3VyPSJncmV5NTAiKSArDQogIGdlb21faGxpbmUoeWludGVyY2VwdD1QSV9QT1AsIGxpbmV0eXBlPSJkb3R0ZWQiLCBjb2xvdXI9InJlZCIpICsNCiAgZ2VvbV9saW5lKCkrZ2VvbV9wb2ludCgpICsNCiAgbGFicyh4PSJNZWFuIHByZWRpY3RlZCBwcm9iYWJpbGl0eSIsIHk9Ik9ic2VydmVkIHN0cm9rZSBmcmFjdGlvbiIsDQogICAgICAgY2FwdGlvbj0iRG90dGVkIHJlZCBsaW5lOiBwcmV2YWxlbmNlLW9ubHkgcmVmZXJlbmNlIikgKyB0aGVtZV9taW5pbWFsKCkNCmdnc2F2ZSgidjNfY2FsaWJyYXRpb24ucG5nIiwgY3AsIHdpZHRoPTcsIGhlaWdodD02LCBkcGk9MzAwKTsgcHJpbnQoY3ApDQpgYGANCg0KIyAxMiAgQ29tcGFyaXNvbiB3aXRoIGFsdGVybmF0aXZlIG1vZGVscyAodHJhbnNwYXJlbnQgKyBvcGFxdWUpIGFuZCBTSEFQDQpgYGB7ciBiZW5jaG1hcmt9DQpidCA8LSB0cmFpbl9tMzsgYnYgPC0gdmFsX20zOyBiZSA8LSB0ZXN0X20zDQoNCiMgMS4gTG9naXN0aWMgUmVncmVzc2lvbg0KbHIgPC0gZ2xtKHN0cm9rZSB+IC4sIGRhdGEgPSBidCwgZmFtaWx5ID0gYmlub21pYWwpDQoNCiMgRklYOiBTYXZlIHRoZSBhY3R1YWwgUk9DIG9iamVjdCB0byAncm9jX2xyJw0Kcm9jX2xyIDwtIHJvYyhiZSRzdHJva2UsIHByZWRpY3QobHIsIGJlLCB0eXBlID0gInJlc3BvbnNlIiksIGxldmVscyA9IGMoIjAiLCAiMSIpLCBxdWlldCA9IFRSVUUpDQpscl9hdWMgPC0gYXMubnVtZXJpYyhhdWMocm9jX2xyKSkNCg0KIyAyLiBYR0Jvb3N0DQptbSA8LSBmdW5jdGlvbihkKSBtb2RlbC5tYXRyaXgoc3Ryb2tlIH4gLiAtMSwgZGF0YSA9IGQpDQpmdCA8LSBSZWR1Y2UoaW50ZXJzZWN0LCBsaXN0KGNvbG5hbWVzKG1tKGJ0KSksIGNvbG5hbWVzKG1tKGJ2KSksIGNvbG5hbWVzKG1tKGJlKSkpKQ0KDQpkdHIgPC0geGdiLkRNYXRyaXgobW0oYnQpWywgZnQsIGRyb3AgPSBGQUxTRV0sIGxhYmVsID0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoYnQkc3Ryb2tlKSkpDQpkdmEgPC0geGdiLkRNYXRyaXgobW0oYnYpWywgZnQsIGRyb3AgPSBGQUxTRV0sIGxhYmVsID0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoYnYkc3Ryb2tlKSkpDQpkdGUgPC0geGdiLkRNYXRyaXgobW0oYmUpWywgZnQsIGRyb3AgPSBGQUxTRV0sIGxhYmVsID0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoYmUkc3Ryb2tlKSkpDQoNClNQVyA8LSBzdW0oYnQkc3Ryb2tlID09ICIwIikgLyBzdW0oYnQkc3Ryb2tlID09ICIxIikNCg0KWEdCX1BBUkFNUyA8LSBsaXN0KA0KICBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIiwgDQogIGV2YWxfbWV0cmljID0gImF1YyIsIA0KICBtYXhfZGVwdGggPSAzLA0KICBsZWFybmluZ19yYXRlID0gMC4wNSwgDQogIHN1YnNhbXBsZSA9IDAuOCwgDQogIGNvbHNhbXBsZV9ieXRyZWUgPSAwLjgsDQogIHNjYWxlX3Bvc193ZWlnaHQgPSBTUFcNCikNCg0Kc2V0LnNlZWQoR0xPQkFMX1NFRUQpDQp4Z2IgPC0geGdiLnRyYWluKA0KICBwYXJhbXMgPSBYR0JfUEFSQU1TLCANCiAgZGF0YSA9IGR0ciwgDQogIG5yb3VuZHMgPSAxMDAwLA0KICB3YXRjaGxpc3QgPSBsaXN0KHZhbCA9IGR2YSksIA0KICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSAyNSwgDQogIHZlcmJvc2UgPSAwDQopDQoNCiMgRklYOiBTYXZlIHRoZSBhY3R1YWwgUk9DIG9iamVjdCB0byAncm9jX3hnYicNCnJvY194Z2IgPC0gcm9jKGJlJHN0cm9rZSwgcHJlZGljdCh4Z2IsIGR0ZSksIGxldmVscyA9IGMoIjAiLCAiMSIpLCBxdWlldCA9IFRSVUUpDQp4Z2JfYXVjIDwtIGFzLm51bWVyaWMoYXVjKHJvY194Z2IpKQ0KDQojIEh5cGVycGFyYW1ldGVyIG91dHB1dCBmb3IgTWV0aG9kcw0KY2F0KCJcbj09PT09IFhHQm9vc3Qgc3BlY2lmaWNhdGlvbiAocGFzdGUgaW50byBNZXRob2RzKSA9PT09PVxuIikNCmNhdChzcHJpbnRmKCJvYmplY3RpdmU9JXM7IGV2YWxfbWV0cmljPSVzOyBtYXhfZGVwdGg9JWQ7IGxlYXJuaW5nX3JhdGU9JS4yZjtcbiIsDQogICAgWEdCX1BBUkFNUyRvYmplY3RpdmUsIFhHQl9QQVJBTVMkZXZhbF9tZXRyaWMsIFhHQl9QQVJBTVMkbWF4X2RlcHRoLCBYR0JfUEFSQU1TJGxlYXJuaW5nX3JhdGUpKQ0KY2F0KHNwcmludGYoInN1YnNhbXBsZT0lLjFmOyBjb2xzYW1wbGVfYnl0cmVlPSUuMWY7IHNjYWxlX3Bvc193ZWlnaHQ9JS4zZjtcbiIsDQogICAgWEdCX1BBUkFNUyRzdWJzYW1wbGUsIFhHQl9QQVJBTVMkY29sc2FtcGxlX2J5dHJlZSwgU1BXKSkNCmNhdChzcHJpbnRmKCJlYXJseV9zdG9wcGluZ19yb3VuZHM9MjU7IG5fcm91bmRzX3NlbGVjdGVkPSVkOyBzZWVkPSVkXG4iLA0KICAgIHhnYiRiZXN0X2l0ZXJhdGlvbiwgR0xPQkFMX1NFRUQpKQ0KDQpiZW5jaCA8LSBkYXRhLmZyYW1lKA0KICBNb2RlbCA9IGMocGFzdGUwKCJCYXllc2lhbiBuZXR3b3JrICgiLCBQUklNQVJZLCAiKSIpLA0KICAgICAgICAgICAgIkxvZ2lzdGljIHJlZ3Jlc3Npb24gKHRyYW5zcGFyZW50KSIsDQogICAgICAgICAgICAiWEdCb29zdCAob3BhcXVlKSIpLA0KICBUZXN0X0FVQyA9IHJvdW5kKGMobGVhZGVyYm9hcmQkVGVzdF9BVUNbbGVhZGVyYm9hcmQkTW9kZWwgPT0gUFJJTUFSWV0sIGxyX2F1YywgeGdiX2F1YyksIDQpDQopDQpwcmludChiZW5jaCkNCndyaXRlLmNzdihiZW5jaCwgInYzX2JlbmNobWFyay5jc3YiLCByb3cubmFtZXMgPSBGQUxTRSkNCg0KIyBbUjMuMTVdIFNIQVAgZm9yIHRoZSBvcGFxdWUgYmVuY2htYXJrDQpzdiAgPC0gcHJlZGljdCh4Z2IsIGR0ZSwgcHJlZGNvbnRyaWIgPSBUUlVFKQ0KaW1wIDwtIHNvcnQoY29sTWVhbnMoYWJzKHN2Wywgc2V0ZGlmZihjb2xuYW1lcyhzdiksICJCSUFTIiksIGRyb3AgPSBGQUxTRV0pKSwgZGVjcmVhc2luZyA9IFRSVUUpDQpwcmludChyb3VuZChoZWFkKGltcCwgMTApLCA1KSkNCndyaXRlLmNzdihkYXRhLmZyYW1lKGZlYXR1cmUgPSBuYW1lcyhpbXApLCBtZWFuX2Fic19zaGFwID0gcm91bmQoaW1wLCA1KSksDQogICAgICAgICAgInYzX3NoYXAuY3N2Iiwgcm93Lm5hbWVzID0gRkFMU0UpDQpgYGANCg0KIyAxMyAgSHlwb3RoZXNpcyB0ZXN0aW5nIOKAlCBEZUxvbmcgYW5kIE1jTmVtYXIgKGNvbXBsZXRlIHNldCkNCmBgYHtyIGh5cC10ZXN0c30NCnRociA8LSBmdW5jdGlvbihtKSBsZWFkZXJib2FyZCRWYWxfVGhyZXNob2xkW2xlYWRlcmJvYXJkJE1vZGVsPT1tXQ0KDQptY24gPC0gZnVuY3Rpb24oQSxCKXsNCiAgeSA8LSByb2NfbGlzdFtbQV1dJHJlc3BvbnNlDQogIHBhIDwtIGlmZWxzZShyb2NfbGlzdFtbQV1dJHByZWRpY3Rvcj49dGhyKEEpLDEsMCkNCiAgcGIgPC0gaWZlbHNlKHJvY19saXN0W1tCXV0kcHJlZGljdG9yPj10aHIoQiksMSwwKTsgaSA8LSB3aGljaCh5PT0xKQ0KICBwIDwtIHN1cHByZXNzV2FybmluZ3MobWNuZW1hci50ZXN0KHRhYmxlKGZhY3RvcihwYVtpXSwwOjEpLGZhY3RvcihwYltpXSwwOjEpKSkkcC52YWx1ZSkNCiAgY2F0KHNwcmludGYoIk1jTmVtYXIgICUtMzBzIHZzICUtMzBzIHA9JS40ZlxuIixBLEIscCkpOyBwIA0KfQ0KDQpkbCA8LSBmdW5jdGlvbihBLEIpew0KICB0IDwtIHJvYy50ZXN0KHJvY19saXN0W1tBXV0sIHJvY19saXN0W1tCXV0sIG1ldGhvZD0iZGVsb25nIikNCiAgY2F0KHNwcmludGYoIkRlTG9uZyAgICUtMzBzIHZzICUtMzBzIFo9JTYuM2YgcD0lLjRmXG4iLEEsQiwNCiAgICAgIGFzLm51bWVyaWModCRzdGF0aXN0aWMpLHQkcC52YWx1ZSkpOyBjKFo9YXMubnVtZXJpYyh0JHN0YXRpc3RpYykscD10JHAudmFsdWUpIA0KfQ0KDQpNNSA8LSAiTTU6IEFMTCAoU0lDRSArIENvbnQgKyBTTU9URSkiDQoNCmNhdCgiXG49PSBIMSB0b3BvbG9neSA9PVxuIik7IGRsKCJNMzogQ29udGludW91cyBOb2RlcyIsIk0xOiBCYXNlbGluZSIpOyBkbCgiTTY6IFNJQ0UgKyBDb250aW51b3VzIiwiTTI6IFNJQ0UgKyBEaXNjcmV0aXplIikNCm1jbigiTTM6IENvbnRpbnVvdXMgTm9kZXMiLCJNMTogQmFzZWxpbmUiKTsgbWNuKCJNNjogU0lDRSArIENvbnRpbnVvdXMiLCJNMjogU0lDRSArIERpc2NyZXRpemUiKQ0KDQpjYXQoIlxuPT0gSDIgaW1wdXRhdGlvbiA9PVxuIik7IGRsKCJNMjogU0lDRSArIERpc2NyZXRpemUiLCJNMTogQmFzZWxpbmUiKTsgZGwoIk02OiBTSUNFICsgQ29udGludW91cyIsIk0zOiBDb250aW51b3VzIE5vZGVzIikNCg0KY2F0KCJcbj09IEgzIGJhbGFuY2luZyA9PVxuIik7IGRsKCJNNDogU01PVEUgb24gQmFzZWxpbmUiLCJNMTogQmFzZWxpbmUiKTsgZGwoTTUsIk02OiBTSUNFICsgQ29udGludW91cyIpDQptY24oIk00OiBTTU9URSBvbiBCYXNlbGluZSIsIk0xOiBCYXNlbGluZSIpOyBtY24oIk04OiBTSUNFICsgU01PVEUiLCJNMjogU0lDRSArIERpc2NyZXRpemUiKTsgbWNuKE01LCJNNjogU0lDRSArIENvbnRpbnVvdXMiKQ0KDQpjYXQoIlxuPT0gSDQgY29tYmluYXRpb25zID09XG4iKTsgZGwoTTUsIk0xOiBCYXNlbGluZSIpOyBkbCgiTTY6IFNJQ0UgKyBDb250aW51b3VzIiwiTTE6IEJhc2VsaW5lIikNCg0KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgQmVuY2htYXJrIE1vZGVsIENvbXBhcmlzb25zIChCTiB2cyBMb2dpc3RpYyBSZWdyZXNzaW9uICYgWEdCb29zdCkNCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQpjYXQoIlxuPT0gQmVuY2htYXJrIENvbXBhcmlzb25zID09XG4iKQ0KDQojIHJvY19ibiBNVVNUIGJlIHRoZSB2YWxpZGF0aW9uLXNlbGVjdGVkIFBSSU1BUlkgbW9kZWwgKHNldCBpbiBTZWdtZW50IDA5KSwgbm90IE01Lg0KIyBNNSBpcyB0aGUgZnVsbHktY29tYmluZWQgU0lDRStjb250aW51b3VzK1NNT1RFIGFybSBhbmQgaXMgYSBtdWNoIHdlYWtlcg0KIyBjb25maWd1cmF0aW9uICh0ZXN0IEFVQyAwLjczODEpIHRoYW4gdGhlIG1vZGVsIGFjdHVhbGx5IHJlcG9ydGVkIGluIHRoZQ0KIyBtYW51c2NyaXB0J3MgbGVhZGVyYm9hcmQgKFBSSU1BUlkgPSBNMywgdGVzdCBBVUMgMC44MTA5KS4gQ29tcGFyaW5nIGJlbmNobWFya3MNCiMgYWdhaW5zdCBNNSBzaWxlbnRseSBhbnN3ZXJzIGEgZGlmZmVyZW50IHF1ZXN0aW9uIC0tIGl0IGRvZXMgbm90IGVycm9yLCBpdCBqdXN0DQojIHByb2R1Y2VzIHRoZSB3cm9uZyBwLXZhbHVlcyAtLSB3aGljaCBpcyB3aGF0IGhhcHBlbmVkIGluIHRoZSBwcmV2aW91cyBydW4uDQpyb2NfYm4gPC0gcm9jX2xpc3RbW1BSSU1BUlldXQ0KDQp0X2xyICA8LSByb2MudGVzdChyb2NfYm4sIHJvY19sciwgIG1ldGhvZCA9ICJkZWxvbmciKQ0KdF94Z2IgPC0gcm9jLnRlc3Qocm9jX2JuLCByb2NfeGdiLCBtZXRob2QgPSAiZGVsb25nIikNCg0KY2F0KHNwcmludGYoIkRlTG9uZyAgICUtMzBzIHZzICUtMzBzIFo9JTYuM2YgcD0lLjRmXG4iLA0KICAgICAgICAgICAgUFJJTUFSWSwgIkxvZ2lzdGljIHJlZ3Jlc3Npb24iLCBhcy5udW1lcmljKHRfbHIkc3RhdGlzdGljKSwgIHRfbHIkcC52YWx1ZSkpDQpjYXQoc3ByaW50ZigiRGVMb25nICAgJS0zMHMgdnMgJS0zMHMgWj0lNi4zZiBwPSUuNGZcbiIsDQogICAgICAgICAgICBQUklNQVJZLCAiWEdCb29zdCIsICAgICAgICAgICAgIGFzLm51bWVyaWModF94Z2Ikc3RhdGlzdGljKSwgdF94Z2IkcC52YWx1ZSkpDQpgYGANCg0KIyAxNCAgUm9idXN0bmVzcyDigJQgYWxsIGFybXMsIHJlcGVhdGVkIHN0cmF0aWZpZWQgcmVzYW1wbGluZw0KYGBge3Igcm9idXN0bmVzc30NCmV2YWxfbGlnaHQgPC0gZnVuY3Rpb24odHIsIHRlLCBkaXNjKXsNCiAgIyAxLiBEeW5hbWljYWxseSBzeW5jaHJvbml6ZSBmYWN0b3IgbGV2ZWxzIGJldHdlZW4gdHJhaW5pbmcgKHRyKSBhbmQgdGVzdCAodGUpIHNldHMNCiAgZm9yIChjb2wgaW4gbmFtZXModHIpKSB7DQogICAgaWYgKGlzLmZhY3Rvcih0cltbY29sXV0pKSB7DQogICAgICAjIFJlbW92ZSB1bm9ic2VydmVkIGxldmVscyBmcm9tIHRyIChmaXhlcyBjaGVjay5kYXRhIHdhcm5pbmcpDQogICAgICB0cltbY29sXV0gPC0gZHJvcGxldmVscyh0cltbY29sXV0pIA0KICAgICAgIyBGb3JjZSB0ZSB0byBtYXRjaCB0cidzIGV4YWN0IGZhY3RvciBsZXZlbHMgKGZpeGVzIGxldmVsIG1pc21hdGNoIHNraXAgZXJyb3IpDQogICAgICB0ZVtbY29sXV0gPC0gZmFjdG9yKHRlW1tjb2xdXSwgbGV2ZWxzID0gbGV2ZWxzKHRyW1tjb2xdXSkpIA0KICAgIH0NCiAgfQ0KICANCiAgd2wgPC0gaWYgKGRpc2MpIHdsX2Rpc2NyZXRlIGVsc2Ugd2xfaHlicmlkDQogIGJsIDwtIGlmIChkaXNjKSBibF9kaXNjcmV0ZSBlbHNlIGJsX2h5YnJpZA0KICANCiAgZGFnIDwtIGhjKHRyLCB3aGl0ZWxpc3QgPSB3bCwgYmxhY2tsaXN0ID0gYmwpDQogIGZpdCA8LSBpZiAoZGlzYykgYm4uZml0KGRhZywgdHIsIG1ldGhvZCA9ICJiYXllcyIsIGlzcyA9IDEwKSBlbHNlIGJuLmZpdChkYWcsIHRyKQ0KICANCiAgZ3AgPC0gZnVuY3Rpb24oZCl7DQogICAgaWYgKGRpc2MpeyANCiAgICAgIHBwIDwtIHByZWRpY3QoZml0LCBub2RlID0gInN0cm9rZSIsIGRhdGEgPSBkLCBtZXRob2QgPSAiYmF5ZXMtbHciLCBwcm9iID0gVFJVRSkNCiAgICAgIHJldHVybihhdHRyKHBwLCAicHJvYiIpWyIxIiwgXSkgDQogICAgfQ0KICAgIGVjIDwtIHNldGRpZmYobmFtZXMoZCksICJzdHJva2UiKQ0KICAgIHZhcHBseShzZXFfbGVuKG5yb3coZCkpLCBmdW5jdGlvbihpKXsNCiAgICAgIHAgPC0gc3VwcHJlc3NXYXJuaW5ncyhjcHF1ZXJ5KGZpdCwgZXZlbnQgPSAoc3Ryb2tlID09ICIxIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBldmlkZW5jZSA9IGFzLmxpc3QoZFtpLCBlY10pLCBtZXRob2QgPSAibHciLCBuID0gNTAwKSkNCiAgICAgIGlmKGlzLm5hKHApKSAwIGVsc2UgcA0KICAgIH0sIG51bWVyaWMoMSkpDQogIH0NCiAgDQogIGFzLm51bWVyaWMoYXVjKHJvYyh0ZSRzdHJva2UsIGdwKHRlKSwgbGV2ZWxzID0gYygiMCIsICIxIiksIHF1aWV0ID0gVFJVRSkpKQ0KfQ0KDQpzYWZlIDwtIGZ1bmN0aW9uKGUpIHRyeUNhdGNoKGUsIGVycm9yID0gZnVuY3Rpb24oeCl7IGNhdCgiICBza2lwOiIsIHgkbWVzc2FnZSwgIlxuIik7IE5BX3JlYWxfIH0pDQoNCnJvYiA8LSBkYXRhLmZyYW1lKCkNCmZvciAocyBpbiAxOk5fUkVQRUFUUykgew0KICBxIDwtIHNwbGl0X3N0cm9rZShzdHJva2VfZGF0YSwgc2VlZCA9IDEwMDAgKyBzKQ0KICBxIDwtIGhhcm1vbmlzZV9sZXZlbHMocSR0cmFpbiwgcSR2YWwsIHEkdGVzdCkNCiAgDQogIG0gPC0gbWVkaWFuKGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHEkdHJhaW4kYm1pKSksIG5hLnJtID0gVFJVRSkNCiAgY2UgPC0gaW1wdXRlX3NpY2UocSR0cmFpbiwgcSR2YWwsIHEkdGVzdCwgc2VlZCA9IDEwMDAgKyBzKQ0KICANCiAgZDEgPC0gaW1wdXRlX2Jhc2VsaW5lKHEkdHJhaW4sIG0pOyBkMXQgPC0gaW1wdXRlX2Jhc2VsaW5lKHEkdGVzdCwgbSkNCiAgZDIgPC0gaW1wdXRlX2Jhc2VsaW5lKGNlJHRyYWluLCBtKTsgZDJ0IDwtIGltcHV0ZV9iYXNlbGluZShjZSR0ZXN0LCBtKQ0KICBjMyA8LSBwcmVwX2NvbnRpbnVvdXMocSR0cmFpbiwgbSk7IGMzdCA8LSBwcmVwX2NvbnRpbnVvdXMocSR0ZXN0LCBtKQ0KICBjNiA8LSBwcmVwX2NvbnRpbnVvdXMoY2UkdHJhaW4sIG0pOyBjNnQgPC0gcHJlcF9jb250aW51b3VzKGNlJHRlc3QsIG0pDQogIA0KICByb2IgPC0gcmJpbmQocm9iLCBkYXRhLmZyYW1lKHNlZWQgPSBzLA0KICAgIE0xID0gc2FmZShldmFsX2xpZ2h0KGQxLCBkMXQsIFRSVUUpKSwNCiAgICBNMiA9IHNhZmUoZXZhbF9saWdodChkMiwgZDJ0LCBUUlVFKSksDQogICAgTTMgPSBzYWZlKGV2YWxfbGlnaHQoYzMsIGMzdCwgRkFMU0UpKSwNCiAgICBNNCA9IHNhZmUoZXZhbF9saWdodChhcHBseV9zbW90ZW5jKGQxKSwgZDF0LCBUUlVFKSksDQogICAgTTUgPSBzYWZlKGV2YWxfbGlnaHQoYXBwbHlfc21vdGVuYyhjNiksIGM2dCwgRkFMU0UpKSwNCiAgICBNNiA9IHNhZmUoZXZhbF9saWdodChjNiwgYzZ0LCBGQUxTRSkpLA0KICAgIE03ID0gc2FmZShldmFsX2xpZ2h0KGFwcGx5X3Ntb3RlbmMoYzMpLCBjM3QsIEZBTFNFKSksDQogICAgTTggPSBzYWZlKGV2YWxfbGlnaHQoYXBwbHlfc21vdGVuYyhkMiksIGQydCwgVFJVRSkpDQogICkpDQogIGNhdChzcHJpbnRmKCJzZWVkICVkLyVkXG4iLCBzLCBOX1JFUEVBVFMpKQ0KfQ0KDQpjYXQoIlxuIEFOQUxZU0FCTEUgUkVTQU1QTEVTIFBFUiBBUk06XG4iKQ0KcHJpbnQoY29sU3VtcyghaXMubmEocm9iWy0xXSkpKQ0Kd3JpdGUuY3N2KHJvYiwgInYzX3JvYnVzdG5lc3NfcmF3LmNzdiIsIHJvdy5uYW1lcyA9IEZBTFNFKQ0KDQpyb2Jfc3VtbWFyeSA8LSByb2IgJT4lIA0KICBwaXZvdF9sb25nZXIoLXNlZWQsIG5hbWVzX3RvID0gImFybSIsIHZhbHVlc190byA9ICJhdWMiKSAlPiUNCiAgZ3JvdXBfYnkoYXJtKSAlPiUgDQogIHN1bW1hcmlzZSgNCiAgICBuX2FuYWx5c2FibGUgPSBzdW0oIWlzLm5hKGF1YykpLA0KICAgIEFVQ19tZWFuID0gcm91bmQobWVhbihhdWMsIG5hLnJtID0gVFJVRSksIDQpLA0KICAgIEFVQ19zZCA9IHJvdW5kKHNkKGF1YyxuYS5ybSA9IFRSVUUpLCA0KSwNCiAgICAuZ3JvdXBzID0gImRyb3AiDQogICkgJT4lIA0KICBhcnJhbmdlKGRlc2MoQVVDX21lYW4pKQ0KDQpwcmludChyb2Jfc3VtbWFyeSkNCndyaXRlLmNzdihyb2Jfc3VtbWFyeSwgInYzX3JvYnVzdG5lc3Nfc3VtbWFyeS5jc3YiLCByb3cubmFtZXMgPSBGQUxTRSkNCg0KYGBgDQoNCiMgMTUgIEZhY3RvcmlhbCBhbmFseXNpcyBvZiB0aGUgMsOXMsOXMiBkZXNpZ24NCmBgYHtyIGZhY3RvcmlhbH0NCiMgTWFpbiBlZmZlY3RzIEFORCBpbnRlcmFjdGlvbnMuIElmIG5vIGludGVyYWN0aW9uIGlzIHNpZ25pZmljYW50LCB0aGUNCiMgd29yZCAic3luZXJneSIgbXVzdCBiZSByZW1vdmVkIGZyb20gSHlwb3RoZXNpcyA0IGFuZCB0aGUgQ29uY2x1c2lvbi4NCmQgPC0gcm9iICU+JSBwaXZvdF9sb25nZXIoLXNlZWQsbmFtZXNfdG89ImFybSIsdmFsdWVzX3RvPSJhdWMiKSAlPiUgZmlsdGVyKCFpcy5uYShhdWMpKSAlPiUNCiAgbXV0YXRlKHRvcG9sb2d5ICA9ZmFjdG9yKGlmX2Vsc2UoYXJtICVpbiUgYygiTTMiLCJNNSIsIk02IiwiTTciKSwiY29udGludW91cyIsImRpc2NyZXRlIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHM9YygiZGlzY3JldGUiLCJjb250aW51b3VzIikpLA0KICAgICAgICAgaW1wdXRhdGlvbj1mYWN0b3IoaWZfZWxzZShhcm0gJWluJSBjKCJNMiIsIk01IiwiTTYiLCJNOCIpLCJzaWNlIiwibWVkaWFuIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHM9YygibWVkaWFuIiwic2ljZSIpKSwNCiAgICAgICAgIGJhbGFuY2luZyA9ZmFjdG9yKGlmX2Vsc2UoYXJtICVpbiUgYygiTTQiLCJNNSIsIk03IiwiTTgiKSwic21vdGUiLCJub25lIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHM9Yygibm9uZSIsInNtb3RlIikpLCBzZWVkPWZhY3RvcihzZWVkKSkNCmZtIDwtIGxtZXIoYXVjIH4gdG9wb2xvZ3kqaW1wdXRhdGlvbipiYWxhbmNpbmcgKyAoMXxzZWVkKSwgZGF0YT1kLCBSRU1MPVRSVUUpDQpjYXQoIlxuLS0tIFR5cGUgSUlJIGZpeGVkIGVmZmVjdHMgW1IxLjNhXSAtLS1cbiIpOyBwcmludChhbm92YShmbSwgdHlwZT0zKSkNCmNhdCgiXG4tLS0gRXN0aW1hdGVzIHdpdGggOTUlIENJIC0tLVxuIikNCnByaW50KHJvdW5kKGNiaW5kKEVzdGltYXRlPWZpeGVmKGZtKSwgY29uZmludChmbSxtZXRob2Q9IldhbGQiKVtuYW1lcyhmaXhlZihmbSkpLF0pLDQpKQ0KZm9yICh2IGluIGMoInRvcG9sb2d5IiwiaW1wdXRhdGlvbiIsImJhbGFuY2luZyIpKQ0KICBwcmludChkICU+JSBncm91cF9ieSguZGF0YVtbdl1dKSAlPiUgc3VtbWFyaXNlKG1lYW5fYXVjPXJvdW5kKG1lYW4oYXVjKSw0KSwgc2Q9cm91bmQoc2QoYXVjKSw0KSkpDQp3IDwtIHJvYiAlPiUgZmlsdGVyKCFpcy5uYShNMyksIWlzLm5hKE0xKSkNCmNhdCgiXG5QYWlyZWQgY29uZmlybWF0b3J5IChNMyB2cyBNMSk6XG4iKTsgcHJpbnQodC50ZXN0KHckTTMsdyRNMSxwYWlyZWQ9VFJVRSkpOyBwcmludCh3aWxjb3gudGVzdCh3JE0zLHckTTEscGFpcmVkPVRSVUUpKQ0KYGBgDQoNCiMgMTYgIFNwYXJzZSBDUFQgcmVsaWFiaWxpdHkgYW5kIGNvbnNlbnN1cyBuZXR3b3JrDQpgYGB7ciBjcHQtcmVsaWFiaWxpdHl9DQojIC0tLS0gRml0IHRoZSBib290c3RyYXAtYXZlcmFnZWQgQ09OU0VOU1VTIG5ldHdvcmsgYW5kIGV2YWx1YXRlIGl0IC0tLS0NCnByIDwtIHJ1bnNbW1BSSU1BUlldXQ0KYXN0ciA8LSBwciRhcmNfc3RyZW5ndGgNCmNvbnMgPC0gYXZlcmFnZWQubmV0d29yayhhc3RyKQ0KY29uc19kaXJlY3RlZCA8LSBUUlVFDQpjb25zIDwtIHRyeUNhdGNoKGNleHRlbmQoY29ucyksIGVycm9yID0gZnVuY3Rpb24oZSkgew0KICBjb25zX2RpcmVjdGVkIDw8LSBGQUxTRQ0KICBhdmVyYWdlZC5uZXR3b3JrKGFzdHIsIHRocmVzaG9sZCA9IDAuODUpDQp9KQ0KDQpjYXQoIlxuW1IxLjVhXSBDb25zZW5zdXMgbmV0d29yayBhcmNzOlxuIik7IHByaW50KGFyY3MoY29ucykpDQpjYXQoIlxuQXJjcyBpbiBzaW5nbGUgSEMgZml0IGJ1dCBub3QgY29uc2Vuc3VzOlxuIikNCmggPC0gYXBwbHkoYXJjcyhwciRzdHJ1Y3R1cmUpLDEscGFzdGUsY29sbGFwc2U9Ii0+Iik7IGMyIDwtIGFwcGx5KGFyY3MoY29ucyksMSxwYXN0ZSxjb2xsYXBzZT0iLT4iKQ0KcHJpbnQoc2V0ZGlmZihoLGMyKSk7IGNhdCgiQXJjcyBpbiBjb25zZW5zdXMgYnV0IG5vdCBIQyBmaXQ6XG4iKTsgcHJpbnQoc2V0ZGlmZihjMixoKSkNCmNvbnNfYXVjIDwtIHRyeUNhdGNoKHsNCiAgY2YgPC0gaWYgKHByJGlzX2Rpc2MpIGJuLmZpdChjb25zLCBwciR0cmFpbiwgbWV0aG9kPSJiYXllcyIsIGlzcz0xMCkgZWxzZSBibi5maXQoY29ucywgcHIkdHJhaW4pDQogIGdwIDwtIGlmIChwciRpc19kaXNjKSB7DQogICAgYXR0cihwcmVkaWN0KGNmLG5vZGU9InN0cm9rZSIsZGF0YT1wciR0ZXN0LG1ldGhvZD0iYmF5ZXMtbHciLHByb2I9VFJVRSksInByb2IiKVsiMSIsXQ0KICB9IGVsc2Ugew0KICAgIGVjIDwtIHNldGRpZmYobmFtZXMocHIkdGVzdCksInN0cm9rZSIpDQogICAgdmFwcGx5KHNlcV9sZW4obnJvdyhwciR0ZXN0KSksZnVuY3Rpb24oaSl7DQogICAgICBwPC1zdXBwcmVzc1dhcm5pbmdzKGNwcXVlcnkoY2YsZXZlbnQ9KHN0cm9rZT09IjEiKSxldmlkZW5jZT1hcy5saXN0KHByJHRlc3RbaSxlY10pLA0KICAgICAgICAgbWV0aG9kPSJsdyIsbj1OX1BBUlRJQ0xFU19QQVRJRU5UKSk7IGlmKGlzLm5hKHApKSAwIGVsc2UgcH0sbnVtZXJpYygxKSkgfQ0KICByb3VuZChhcy5udW1lcmljKGF1Yyhyb2MocHIkdGVzdCRzdHJva2UsIGdwLCBsZXZlbHM9YygiMCIsIjEiKSwgcXVpZXQ9VFJVRSkpKSw0KQ0KfSwgZXJyb3I9ZnVuY3Rpb24oZSl7Y2F0KCJjb25zZW5zdXMgZml0IGZhaWxlZDoiLGUkbWVzc2FnZSwiXG4iKTsgTkF9KQ0KY2F0KHNwcmludGYoIlxuW1IxLjVhXSBDb25zZW5zdXMtbmV0d29yayB0ZXN0IEFVQyA9ICVzIChzaW5nbGUgSEMgZml0ID0gJS40ZilcbiIsDQogICAgICAgICAgICBjb25zX2F1YywgbGVhZGVyYm9hcmQkVGVzdF9BVUNbbGVhZGVyYm9hcmQkTW9kZWw9PVBSSU1BUlldKSkNCndyaXRlLmNzdihhc3RyICU+JSBmaWx0ZXIoc3RyZW5ndGg+MC41LCBkaXJlY3Rpb24+PTAuNSkgJT4lIGFycmFuZ2UoZGVzYyhzdHJlbmd0aCkpLA0KICAgICAgICAgICJ2M19hcmNfc3RyZW5ndGhzLmNzdiIsIHJvdy5uYW1lcz1GQUxTRSkNCg0KIyAtLS0tIENQVCBjb3VudHMgKyBKZWZmcmV5cyBpbnRlcnZhbHMgKyBzaHJpbmthZ2UgLS0tLQ0KY3B0X3RibCA8LSB0cmFpbl9tNiAlPiUgY291bnQoaHlwZXJ0ZW5zaW9uLCBoZWFydF9kaXNlYXNlLCBldmVyX21hcnJpZWQsIHN0cm9rZSkgJT4lDQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb209c3Ryb2tlLCB2YWx1ZXNfZnJvbT1uLCB2YWx1ZXNfZmlsbD0wKSAlPiUNCiAgcmVuYW1lKG4wPWAwYCwgbjE9YDFgKSAlPiUNCiAgbXV0YXRlKG49bjArbjEsIHBfaGF0PXJvdW5kKDEwMCpuMS9uLDIpLA0KICAgICAgICAgQ3JJX2xvdyA9cm91bmQoMTAwKnFiZXRhKC4wMjUsbjErLjUsbjArLjUpLDIpLA0KICAgICAgICAgQ3JJX2hpZ2g9cm91bmQoMTAwKnFiZXRhKC45NzUsbjErLjUsbjArLjUpLDIpKSAlPiUgYXJyYW5nZShkZXNjKHBfaGF0KSkNCnByaW50KGNwdF90YmwpOyB3cml0ZS5jc3YoY3B0X3RibCwidjNfY3B0X2ludGVydmFscy5jc3YiLHJvdy5uYW1lcz1GQUxTRSkNCnBfbWFyZyA8LSBtZWFuKHRyYWluX202JHN0cm9rZT09IjEiKQ0KaXNzX3RibCA8LSBtYXBfZGZyKGMoMSw1LDEwLDI1LDUwLDEwMCksIGZ1bmN0aW9uKGspDQogIGNwdF90YmwgJT4lIHRyYW5zbXV0ZShoeXBlcnRlbnNpb24saGVhcnRfZGlzZWFzZSxldmVyX21hcnJpZWQsbixpc3M9aywNCiAgICAgICAgICAgICAgICAgICAgICAgIHBfc21vb3RoZWQ9cm91bmQoMTAwKihuMStrKnBfbWFyZykvKG4rayksMikpKQ0Kd3JpdGUuY3N2KGlzc190YmwsInYzX2NwdF9pc3Nfc2Vuc2l0aXZpdHkuY3N2Iixyb3cubmFtZXM9RkFMU0UpDQpwcmludChpc3NfdGJsICU+JSBmaWx0ZXIobjw9MTApKQ0KDQojIC0tLS0gW1IzLjhdIERvZXMgdGhlIERJU0NSRVRFIGFybSByZWNvdmVyIGFnZSAtPiBzdHJva2U/IC0tLS0NCmExIDwtIGFyY3MoZGFnc1tbIk0xOiBCYXNlbGluZSJdXSkNCmNhdCgiXG5bUjMuOF0gRGlzY3JldGUtYXJtIGFyY3MgaW52b2x2aW5nIGFnZTpcbiIpOyBwcmludChhMVthcHBseShhMSwxLGZ1bmN0aW9uKHIpIGFueShyPT0iYWdlIikpLCxkcm9wPUZBTFNFXSkNCmBgYA0KDQojIDE3ICBDb25kaXRpb25hbC1yaXNrIHNjZW5hcmlvcyAoTk9UIGNvdW50ZXJmYWN0dWFsIGludGVydmVudGlvbnMpDQpgYGB7ciBjb25kaXRpb25hbC1yaXNrfQ0KIyBbUjEuMV0gT2JzZXJ2YXRpb25hbCBjb25kaXRpb25pbmcsIG5vdCBkbygpLiBObyB0cmVhdG1lbnQtZWZmZWN0IGxhbmd1YWdlLg0KbmV0IDwtIGZpdHNbW1BSSU1BUlldXTsgc2V0LnNlZWQoMjAyNikNCnBBIDwtIGNwcXVlcnkobmV0LCBldmVudD0oc3Ryb2tlPT0iMSIpLA0KICAgICAgICAgICAgICBldmlkZW5jZT0oYWdlPD01MCAmIGh5cGVydGVuc2lvbj09IjAiICYgaGVhcnRfZGlzZWFzZT09IjAiKSwgbj1OX1BBUlRJQ0xFU19RVUVSWSkNCnBCIDwtIGNwcXVlcnkobmV0LCBldmVudD0oc3Ryb2tlPT0iMSIpLA0KICAgICAgICAgICAgICBldmlkZW5jZT0oYWdlPj02NSAmIGh5cGVydGVuc2lvbj09IjEiICYgaGVhcnRfZGlzZWFzZT09IjEiICYgYXZnX2dsdWNvc2VfbGV2ZWw+PTE1MCksIG49Tl9QQVJUSUNMRVNfUVVFUlkpDQpwQyA8LSBjcHF1ZXJ5KG5ldCwgZXZlbnQ9KHN0cm9rZT09IjEiKSwNCiAgICAgICAgICAgICAgZXZpZGVuY2U9KGFnZT49NjUgJiBoeXBlcnRlbnNpb249PSIxIiAmIGhlYXJ0X2Rpc2Vhc2U9PSIxIiAmIGF2Z19nbHVjb3NlX2xldmVsPD0xMDApLCBuPU5fUEFSVElDTEVTX1FVRVJZKQ0Kc2NlbiA8LSBkYXRhLmZyYW1lKFByb2ZpbGU9YygiQTogYWdlPD01MCwgbm8gSFROLCBubyBIRCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICJCOiBhZ2U+PTY1LCBIVE4sIEhELCBnbHVjb3NlPj0xNTAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiQzogYWdlPj02NSwgSFROLCBIRCwgZ2x1Y29zZTw9MTAwIiksDQogICAgICAgICAgICAgICAgICAgQ29uZGl0aW9uYWxfcHJvYmFiaWxpdHk9c3ByaW50ZigiJS4yZiUlIiwxMDAqYyhwQSxwQixwQykpKQ0KcHJpbnQoc2Nlbik7IHdyaXRlLmNzdihzY2VuLCJ2M19jb25kaXRpb25hbF9yaXNrLmNzdiIscm93Lm5hbWVzPUZBTFNFKQ0KYGBgDQoNCiMgMTggIFNlc3Npb24gaW5mb3JtYXRpb24NCmBgYHtyIHNlc3Npb259DQp3cml0ZUxpbmVzKGNhcHR1cmUub3V0cHV0KHNlc3Npb25JbmZvKCkpLCAidjJfc2Vzc2lvbkluZm8udHh0IikNCnNlc3Npb25JbmZvKCkNCmBgYA0KDQojIDE5ICBSRVNVTFRTIFBBQ0sg4oCUIGV2ZXJ5IG51bWJlciB0aGUgUmVzdWx0cyBzZWN0aW9uIG5lZWRzDQpgYGB7ciByZXN1bHRzLXBhY2t9DQojIFJ1biBsYXN0LiBQcm9kdWNlcyBhIHNpbmdsZSBjb25zb2xlIGJsb2NrIG9yZGVyZWQgdG8gbWF0Y2ggdGhlIG1hbnVzY3JpcHQncw0KIyBSZXN1bHRzIHN1YnNlY3Rpb25zIDMuMSAtIDMuOSwgc28gbnVtYmVycyBjYW4gYmUgdHJhbnNjcmliZWQgZGlyZWN0bHkuDQpsaW5lIDwtIGZ1bmN0aW9uKHgpIGNhdChzdHJyZXAoIj0iLDcyKSwiXG4iLHgsIlxuIixzdHJyZXAoIj0iLDcyKSwiXG4iLHNlcD0iIikNCg0KbGluZSgiMy4wICBDT0hPUlQgQU5EIFBBUlRJVElPTlMiKQ0KY2F0KHNwcmludGYoIk4gPSAlZDsgcHJldmFsZW5jZSA9ICUuMmYlJTsgbWlzc2luZyBCTUkgPSAlLjFmJSVcbiIsDQogICAgICAgICAgICBOX1RPVEFMLCAxMDAqUElfUE9QLCAxMDAqbWVhbihpcy5uYShzdHJva2VfZGF0YSRibWkpKSkpDQpwcmludChzcGxpdF90YmwpDQoNCmxpbmUoIjMuMSAgVFJBSU5JTkctT05MWSBBU1NPQ0lBVElPTiBTQ1JFRU4gKFN1cHBsZW1lbnRhcnkpIikNCnByaW50KGFzc29jX3RibCkNCg0KbGluZSgiMy4yICBTVFJVQ1RVUkUg4oCUIHByaW1hcnkgbW9kZWwsIGNvbnNlbnN1cyBjb21wYXJpc29uLCBhcmMgZGlyZWN0aW9uIikNCmNhdCgiUFJJTUFSWSBNT0RFTCAodmFsaWRhdGlvbi1zZWxlY3RlZCk6IiwgUFJJTUFSWSwgIlxuIikNCmNhdCgiUGFyZW50cyBvZiBzdHJva2U6IiwgcGFzdGUocGFyZW50cyhmaXRzW1tQUklNQVJZXV0sInN0cm9rZSIpLCBjb2xsYXBzZT0iLCAiKSwgIlxuIikNCmNhdCgiQ2hpbGRyZW4gb2Ygc3Ryb2tlOiIsIHBhc3RlKGNoaWxkcmVuKGZpdHNbW1BSSU1BUlldXSwic3Ryb2tlIiksIGNvbGxhcHNlPSIsICIpLCAiXG4iKQ0KY2F0KHNwcmludGYoIkNvbnNlbnN1cy1uZXR3b3JrIHRlc3QgQVVDID0gJXMgdnMgc2luZ2xlIEhDID0gJS40ZlxuIiwNCiAgICAgICAgICAgIGNvbnNfYXVjLCBsZWFkZXJib2FyZCRUZXN0X0FVQ1tsZWFkZXJib2FyZCRNb2RlbD09UFJJTUFSWV0pKQ0KDQpsaW5lKCIzLjMgIENQVCBSRUxJQUJJTElUWSIpDQpwcmludChjcHRfdGJsKTsgY2F0KCJcblNocmlua2FnZSBmb3Igc3BhcnNlIGNlbGxzOlxuIik7IHByaW50KGlzc190YmwgJT4lIGZpbHRlcihuPD0xMCkpDQoNCmxpbmUoIjMuNCAgQ09ORElUSU9OQUwtUklTSyBTQ0VOQVJJT1MiKQ0KcHJpbnQoc2NlbikNCg0KbGluZSgiMy41ICBQRVJGT1JNQU5DRSBMRUFERVJCT0FSRCIpDQpwcmludChsZWFkZXJib2FyZCkNCg0KbGluZSgiMy42ICBIWVBPVEhFU0lTIFRFU1RTIikNCmNhdCgiKHNlZSBTZWdtZW50IDEzIGNvbnNvbGUgb3V0cHV0OyB0cmFuc2NyaWJlIFogYW5kIHAgZm9yIEgxLUg0KVxuIikNCg0KbGluZSgiMy43ICBST0JVU1RORVNTIikNCnByaW50KHJvYl9zdW1tYXJ5KQ0KY2F0KCJcbkFuYWx5c2FibGUgcmVzYW1wbGVzIHBlciBhcm06XG4iKTsgcHJpbnQoY29sU3VtcyghaXMubmEocm9iWy0xXSkpKQ0KDQpsaW5lKCIzLjdiIEZBQ1RPUklBTCBNT0RFTCIpDQpwcmludChhbm92YShmbSwgdHlwZT0zKSkNCg0KbGluZSgiMy44ICBDQUxJQlJBVElPTiIpDQpwcmludChjYWxpYikNCmNhdChzcHJpbnRmKCJcblByZXZhbGVuY2Utb25seSByZWZlcmVuY2UgQnJpZXIgPSAlLjRmXG4iLCBtZWFuKChQSV9QT1AtYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIodGVzdHNldCRzdHJva2UpKSleMikpKQ0KDQpsaW5lKCIzLjkgIEJFTkNITUFSSyIpDQpwcmludChiZW5jaCkNCmNhdChzcHJpbnRmKCJYR0Jvb3N0IGJlc3RfaXRlcmF0aW9uID0gJWQ7IHNjYWxlX3Bvc193ZWlnaHQgPSAlLjNmXG4iLCB4Z2IkYmVzdF9pdGVyYXRpb24sIFNQVykpDQpjYXQoIlxuVG9wIFNIQVAgZmVhdHVyZXM6XG4iKTsgcHJpbnQocm91bmQoaGVhZChpbXAsOCksNSkpDQoNCmxpbmUoIkVORCBPRiBSRVNVTFRTIFBBQ0siKQ0KYGBgDQo=