Gold putting accuracy: Fitting a nonlinear model using Stan. See Chapter 22 in Regression and Other Stories.


Load packages

library("rprojroot")
root<-has_file(".ROS-Examples-root")$make_fix_file()
library("rstan")
rstan_options(auto_write = TRUE)
library("rstanarm")
invlogit <- plogis

Set up the data

golf <- read.table(root("Golf/data","golf.txt"), header=TRUE, skip=2)
golf$se <- with(golf, sqrt((y/n)*(1-y/n)/n))
r <- (1.68/2)/12
R <- (4.25/2)/12
golf_data <- list(x=golf$x, y=golf$y, n=golf$n, J=nrow(golf), r=r, R=R)

Plot data

par(mar=c(3,3,2,1), mgp=c(1.7,.5,0), tck=-.02)
with(golf, {
    plot(x, y/n, xlim=c(0, 1.1*max(x)), ylim=c(0, 1.02),
         xaxs="i", yaxs="i", pch=20, bty="l",
         xlab="Distance from hole (feet)", ylab="Probability of success",
         main="Data on putts in pro golf")
    segments(x, y/n + se, x, y/n-se, lwd=.5)
    text(x + .4, y/n + se + .02, paste(y, "/", n,sep=""), cex=.6, col="gray40")
})

Logistic regression model with rstanarm

fit1 <- stan_glm(cbind(y, n-y) ~ x, family=binomial(link="logit"), data=golf,
                 refresh = 0)

Post-processing

a_hat <- fit1$coefficients[1]
b_hat <- fit1$coefficients[2]

Plot logistic regression result

par(mar=c(3,3,2,1), mgp=c(1.7,.5,0), tck=-.02)
with(golf, {
    plot(x, y/n, xlim=c(0, 1.1*max(x)), ylim=c(0, 1.02),
         xaxs="i", yaxs="i", pch=20, bty="l",
         xlab="Distance from hole (feet)", ylab="Probability of success",
         main="Fitted logistic regression")
    segments(x, y/n + se, x, y/n-se, lwd=.5)
    curve(invlogit(a_hat + b_hat*x), from=0, to=1.1*max(x), add=TRUE)
    text(10.6, .57, paste("Logistic regression,\n    a = ",
                          round(a_hat, 2), ", b = ", round(b_hat, 2), sep=""))
})

Logistic regression model with rstan

stanfile_golf_logistic <- root("Golf","golf_logistic.stan")
writeLines(readLines(stanfile_golf_logistic))
data {
  int J;
  int n[J];
  vector[J] x;
  int y[J];
}
parameters {
  real a;
  real b;
}
model {
  y ~ binomial_logit(n, a + b*x);
}
fit_logistic <- stan(file = stanfile_golf_logistic, data = golf_data,
                     refresh = 0)
print(fit_logistic)
Inference for Stan model: golf_logistic.
4 chains, each with iter=2000; warmup=1000; thin=1; 
post-warmup draws per chain=1000, total post-warmup draws=4000.

         mean se_mean   sd     2.5%      25%      50%      75%    97.5% n_eff
a        2.23    0.00 0.06     2.12     2.19     2.23     2.27     2.35  1340
b       -0.26    0.00 0.01    -0.27    -0.26    -0.26    -0.25    -0.24  1341
lp__ -3021.14    0.03 1.00 -3023.93 -3021.51 -3020.82 -3020.44 -3020.18  1400
     Rhat
a       1
b       1
lp__    1

Samples were drawn using NUTS(diag_e) at Sat Aug 22 12:03:46 2020.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at 
convergence, Rhat=1).

Post-processing

sims_logistic <- as.matrix(fit_logistic)
a_hat <- median(sims_logistic[,"a"])
b_hat <- median(sims_logistic[,"b"])

Plot logistic regression result

The result is indistinguishable from rstanarm logistic model.

par(mar=c(3,3,2,1), mgp=c(1.7,.5,0), tck=-.02)
with(golf, {
    plot(x, y/n, xlim=c(0, 1.1*max(x)), ylim=c(0, 1.02),
         xaxs="i", yaxs="i", pch=20, bty="l",
         xlab="Distance from hole (feet)", ylab="Probability of success",
         main="Fitted logistic regression")
    segments(x, y/n + se, x, y/n-se, lwd=.5)
    curve(invlogit(a_hat + b_hat*x), from=0, to=1.1*max(x), add=TRUE)
    text(10.6, .57, paste("Logistic regression,\n    a = ",
                          round(a_hat, 2), ", b = ", round(b_hat, 2), sep=""))
})

Geometry based nonlinear model

stanfile_golf_trig <- root("Golf","golf_trig.stan")
writeLines(readLines(stanfile_golf_trig))
data {
  int J;
  int n[J];
  vector[J] x;
  int y[J];
  real r;
  real R;
}
parameters {
  real<lower=0> sigma;
}
model {
  vector[J] p = 2*Phi(asin((R-r) ./ x) / sigma) - 1;
  y ~ binomial(n, p);
}
generated quantities {
  real sigma_degrees;
  sigma_degrees = (180/pi())*sigma;
}
fit_trig <- stan(file = stanfile_golf_trig, data = golf_data, refresh = 0)
print(fit_trig)
Inference for Stan model: golf_trig.
4 chains, each with iter=2000; warmup=1000; thin=1; 
post-warmup draws per chain=1000, total post-warmup draws=4000.

                  mean se_mean   sd     2.5%      25%      50%      75%
sigma             0.03    0.00 0.00     0.03     0.03     0.03     0.03
sigma_degrees     1.53    0.00 0.02     1.48     1.51     1.53     1.54
lp__          -2926.77    0.02 0.72 -2928.77 -2926.92 -2926.50 -2926.32
                 97.5% n_eff Rhat
sigma             0.03  1522    1
sigma_degrees     1.57  1522    1
lp__          -2926.27  1844    1

Samples were drawn using NUTS(diag_e) at Sat Aug 22 12:04:32 2020.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at 
convergence, Rhat=1).

Post-processing

sims_trig <- as.matrix(fit_trig)
sigma_hat <- median(sims_trig[,"sigma"])

Plot geometry based model result

par(mar=c(3,3,2,1), mgp=c(1.7,.5,0), tck=-.02)
with(golf, {
    plot(x, y/n, xlim=c(0, 1.1*max(x)), ylim=c(0, 1.02),
         xaxs="i", yaxs="i", pch=20, bty="l",
         xlab="Distance from hole (feet)", ylab="Probability of success",
         main="Custom nonlinear model fit in Stan")
    segments(x, y/n + se, x, y/n-se, lwd=.5)
    x_grid <- seq(R-r, 1.1*max(x), .01)
    p_grid <- 2*pnorm(asin((R-r)/x_grid) / sigma_hat) - 1
    lines(c(0, R-r, x_grid), c(1, 1, p_grid))
    text(18.5, .26, paste("Geometry-based model,\n sigma = ",
                          round(sigma_hat*180/pi, 1), " degrees", sep=""))
})

Plot geometry based model posterior draws of sigma

par(mar=c(3,3,2,1), mgp=c(1.7,.5,0), tck=-.02)
with(golf, {
    plot(x, y/n, xlim=c(0, 1.1*max(x)), ylim=c(0, 1.02),
         xaxs="i", yaxs="i", pch=20, bty="l",
         xlab="Distance from hole (feet)", ylab="Probability of success",
         main="Custom nonlinear model fit in Stan")
    segments(x, y/n + se, x, y/n-se, lwd=.5)
    x_grid <- seq(R-r, 1.1*max(x), .01)
    n_sims <- length(sims_trig[,"sigma"])
    for (s in sample(n_sims, 20)){
        p_grid <- 2*pnorm(asin((R-r)/x_grid) / sims_trig[s,"sigma"]) - 1
        lines(c(0, R-r, x_grid), c(1, 1, p_grid), lwd=0.5)
    }
    text(18.5, .26, "Geometry-based model,\n post draws of sigma")
})

Plot two models in same figure

par(mar=c(3,3,2,1), mgp=c(1.7,.5,0), tck=-.02)
with(golf, {
    plot(x, y/n, xlim=c(0, 1.1*max(x)), ylim=c(0, 1.02),
         xaxs="i", yaxs="i", pch=20, bty="l",
         xlab="Distance from hole (feet)", ylab="Probability of success",
         main="Two models fit to the golf putting data")
    segments(x, y/n + se, x, y/n-se, lwd=.5)
    curve(invlogit(a_hat + b_hat*x), from=0, to=1.1*max(x), add=TRUE)
    x_grid <- seq(R-r, 1.1*max(x), .01)
    p_grid <- 2*pnorm(asin((R-r)/x_grid) / sigma_hat) - 1
    lines(c(0, R-r, x_grid), c(1, 1, p_grid))
    text(10.3, .58, "Logistic regression")
    text(18.5, .24, "Geometry-based model")
})

LS0tCnRpdGxlOiAiUmVncmVzc2lvbiBhbmQgT3RoZXIgU3RvcmllczogR29sZiIKYXV0aG9yOiAiQW5kcmV3IEdlbG1hbiwgSmVubmlmZXIgSGlsbCwgQWtpIFZlaHRhcmkiCmRhdGU6ICJgciBmb3JtYXQoU3lzLkRhdGUoKSlgIgpvdXRwdXQ6CiAgaHRtbF9kb2N1bWVudDoKICAgIHRoZW1lOiByZWFkYWJsZQogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDogdHJ1ZQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQotLS0KR29sZCBwdXR0aW5nIGFjY3VyYWN5OiBGaXR0aW5nIGEgbm9ubGluZWFyIG1vZGVsIHVzaW5nIFN0YW4uIFNlZQpDaGFwdGVyIDIyIGluIFJlZ3Jlc3Npb24gYW5kIE90aGVyIFN0b3JpZXMuCgotLS0tLS0tLS0tLS0tCgoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChtZXNzYWdlPUZBTFNFLCBlcnJvcj1GQUxTRSwgd2FybmluZz1GQUxTRSwgY29tbWVudD1OQSkKIyBzd2l0Y2ggdGhpcyB0byBUUlVFIHRvIHNhdmUgZmlndXJlcyBpbiBzZXBhcmF0ZSBmaWxlcwpzYXZlZmlncyA8LSBGQUxTRQpgYGAKCiMjIyMgTG9hZCBwYWNrYWdlcwoKYGBge3IgfQpsaWJyYXJ5KCJycHJvanJvb3QiKQpyb290PC1oYXNfZmlsZSgiLlJPUy1FeGFtcGxlcy1yb290IikkbWFrZV9maXhfZmlsZSgpCmxpYnJhcnkoInJzdGFuIikKcnN0YW5fb3B0aW9ucyhhdXRvX3dyaXRlID0gVFJVRSkKbGlicmFyeSgicnN0YW5hcm0iKQppbnZsb2dpdCA8LSBwbG9naXMKYGBgCgojIyMjIFNldCB1cCB0aGUgZGF0YQoKYGBge3IgfQpnb2xmIDwtIHJlYWQudGFibGUocm9vdCgiR29sZi9kYXRhIiwiZ29sZi50eHQiKSwgaGVhZGVyPVRSVUUsIHNraXA9MikKZ29sZiRzZSA8LSB3aXRoKGdvbGYsIHNxcnQoKHkvbikqKDEteS9uKS9uKSkKciA8LSAoMS42OC8yKS8xMgpSIDwtICg0LjI1LzIpLzEyCmdvbGZfZGF0YSA8LSBsaXN0KHg9Z29sZiR4LCB5PWdvbGYkeSwgbj1nb2xmJG4sIEo9bnJvdyhnb2xmKSwgcj1yLCBSPVIpCmBgYAoKIyMjIyBQbG90IGRhdGEKCmBgYHtyIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmlmIChzYXZlZmlncykgcGRmKHJvb3QoIkdvbGYvZmlncyIsImdvbGYwLnBkZiIpLCBoZWlnaHQ9NSwgd2lkdGg9NykKYGBgCgoKCmBgYHtyIH0KcGFyKG1hcj1jKDMsMywyLDEpLCBtZ3A9YygxLjcsLjUsMCksIHRjaz0tLjAyKQp3aXRoKGdvbGYsIHsKICAgIHBsb3QoeCwgeS9uLCB4bGltPWMoMCwgMS4xKm1heCh4KSksIHlsaW09YygwLCAxLjAyKSwKICAgICAgICAgeGF4cz0iaSIsIHlheHM9ImkiLCBwY2g9MjAsIGJ0eT0ibCIsCiAgICAgICAgIHhsYWI9IkRpc3RhbmNlIGZyb20gaG9sZSAoZmVldCkiLCB5bGFiPSJQcm9iYWJpbGl0eSBvZiBzdWNjZXNzIiwKICAgICAgICAgbWFpbj0iRGF0YSBvbiBwdXR0cyBpbiBwcm8gZ29sZiIpCiAgICBzZWdtZW50cyh4LCB5L24gKyBzZSwgeCwgeS9uLXNlLCBsd2Q9LjUpCiAgICB0ZXh0KHggKyAuNCwgeS9uICsgc2UgKyAuMDIsIHBhc3RlKHksICIvIiwgbixzZXA9IiIpLCBjZXg9LjYsIGNvbD0iZ3JheTQwIikKfSkKYGBgCmBgYHtyIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmlmIChzYXZlZmlncykgZGV2Lm9mZigpCmBgYAoKIyMgTG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbCB3aXRoIHJzdGFuYXJtCgpgYGB7ciB9CmZpdDEgPC0gc3Rhbl9nbG0oY2JpbmQoeSwgbi15KSB+IHgsIGZhbWlseT1iaW5vbWlhbChsaW5rPSJsb2dpdCIpLCBkYXRhPWdvbGYsCiAgICAgICAgICAgICAgICAgcmVmcmVzaCA9IDApCmBgYAoKIyMjIyBQb3N0LXByb2Nlc3NpbmcKCmBgYHtyIH0KYV9oYXQgPC0gZml0MSRjb2VmZmljaWVudHNbMV0KYl9oYXQgPC0gZml0MSRjb2VmZmljaWVudHNbMl0KYGBgCgojIyMjIFBsb3QgbG9naXN0aWMgcmVncmVzc2lvbiByZXN1bHQKCmBgYHtyIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmlmIChzYXZlZmlncykgcGRmKHJvb3QoIkdvbGYvZmlncyIsImdvbGYxLnBkZiIpLCBoZWlnaHQ9NSwgd2lkdGg9NykKYGBgCmBgYHtyIH0KcGFyKG1hcj1jKDMsMywyLDEpLCBtZ3A9YygxLjcsLjUsMCksIHRjaz0tLjAyKQp3aXRoKGdvbGYsIHsKICAgIHBsb3QoeCwgeS9uLCB4bGltPWMoMCwgMS4xKm1heCh4KSksIHlsaW09YygwLCAxLjAyKSwKICAgICAgICAgeGF4cz0iaSIsIHlheHM9ImkiLCBwY2g9MjAsIGJ0eT0ibCIsCiAgICAgICAgIHhsYWI9IkRpc3RhbmNlIGZyb20gaG9sZSAoZmVldCkiLCB5bGFiPSJQcm9iYWJpbGl0eSBvZiBzdWNjZXNzIiwKICAgICAgICAgbWFpbj0iRml0dGVkIGxvZ2lzdGljIHJlZ3Jlc3Npb24iKQogICAgc2VnbWVudHMoeCwgeS9uICsgc2UsIHgsIHkvbi1zZSwgbHdkPS41KQogICAgY3VydmUoaW52bG9naXQoYV9oYXQgKyBiX2hhdCp4KSwgZnJvbT0wLCB0bz0xLjEqbWF4KHgpLCBhZGQ9VFJVRSkKICAgIHRleHQoMTAuNiwgLjU3LCBwYXN0ZSgiTG9naXN0aWMgcmVncmVzc2lvbixcbiAgICBhID0gIiwKICAgICAgICAgICAgICAgICAgICAgICAgICByb3VuZChhX2hhdCwgMiksICIsIGIgPSAiLCByb3VuZChiX2hhdCwgMiksIHNlcD0iIikpCn0pCmBgYApgYGB7ciBldmFsPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQppZiAoc2F2ZWZpZ3MpIGRldi5vZmYoKQpgYGAKCiMjIExvZ2lzdGljIHJlZ3Jlc3Npb24gbW9kZWwgd2l0aCByc3RhbgoKYGBge3IgfQpzdGFuZmlsZV9nb2xmX2xvZ2lzdGljIDwtIHJvb3QoIkdvbGYiLCJnb2xmX2xvZ2lzdGljLnN0YW4iKQp3cml0ZUxpbmVzKHJlYWRMaW5lcyhzdGFuZmlsZV9nb2xmX2xvZ2lzdGljKSkKZml0X2xvZ2lzdGljIDwtIHN0YW4oZmlsZSA9IHN0YW5maWxlX2dvbGZfbG9naXN0aWMsIGRhdGEgPSBnb2xmX2RhdGEsCiAgICAgICAgICAgICAgICAgICAgIHJlZnJlc2ggPSAwKQpwcmludChmaXRfbG9naXN0aWMpCmBgYAoKIyMjIyBQb3N0LXByb2Nlc3NpbmcKCmBgYHtyIH0Kc2ltc19sb2dpc3RpYyA8LSBhcy5tYXRyaXgoZml0X2xvZ2lzdGljKQphX2hhdCA8LSBtZWRpYW4oc2ltc19sb2dpc3RpY1ssImEiXSkKYl9oYXQgPC0gbWVkaWFuKHNpbXNfbG9naXN0aWNbLCJiIl0pCmBgYAoKIyMjIyBQbG90IGxvZ2lzdGljIHJlZ3Jlc3Npb24gcmVzdWx0PGJyPgpUaGUgcmVzdWx0IGlzIGluZGlzdGluZ3Vpc2hhYmxlIGZyb20gcnN0YW5hcm0gbG9naXN0aWMgbW9kZWwuCgpgYGB7ciB9CnBhcihtYXI9YygzLDMsMiwxKSwgbWdwPWMoMS43LC41LDApLCB0Y2s9LS4wMikKd2l0aChnb2xmLCB7CiAgICBwbG90KHgsIHkvbiwgeGxpbT1jKDAsIDEuMSptYXgoeCkpLCB5bGltPWMoMCwgMS4wMiksCiAgICAgICAgIHhheHM9ImkiLCB5YXhzPSJpIiwgcGNoPTIwLCBidHk9ImwiLAogICAgICAgICB4bGFiPSJEaXN0YW5jZSBmcm9tIGhvbGUgKGZlZXQpIiwgeWxhYj0iUHJvYmFiaWxpdHkgb2Ygc3VjY2VzcyIsCiAgICAgICAgIG1haW49IkZpdHRlZCBsb2dpc3RpYyByZWdyZXNzaW9uIikKICAgIHNlZ21lbnRzKHgsIHkvbiArIHNlLCB4LCB5L24tc2UsIGx3ZD0uNSkKICAgIGN1cnZlKGludmxvZ2l0KGFfaGF0ICsgYl9oYXQqeCksIGZyb209MCwgdG89MS4xKm1heCh4KSwgYWRkPVRSVUUpCiAgICB0ZXh0KDEwLjYsIC41NywgcGFzdGUoIkxvZ2lzdGljIHJlZ3Jlc3Npb24sXG4gICAgYSA9ICIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgcm91bmQoYV9oYXQsIDIpLCAiLCBiID0gIiwgcm91bmQoYl9oYXQsIDIpLCBzZXA9IiIpKQp9KQpgYGAKCiMjIEdlb21ldHJ5IGJhc2VkIG5vbmxpbmVhciBtb2RlbAoKYGBge3IgfQpzdGFuZmlsZV9nb2xmX3RyaWcgPC0gcm9vdCgiR29sZiIsImdvbGZfdHJpZy5zdGFuIikKd3JpdGVMaW5lcyhyZWFkTGluZXMoc3RhbmZpbGVfZ29sZl90cmlnKSkKZml0X3RyaWcgPC0gc3RhbihmaWxlID0gc3RhbmZpbGVfZ29sZl90cmlnLCBkYXRhID0gZ29sZl9kYXRhLCByZWZyZXNoID0gMCkKcHJpbnQoZml0X3RyaWcpCmBgYAoKIyMjIyBQb3N0LXByb2Nlc3NpbmcKCmBgYHtyIH0Kc2ltc190cmlnIDwtIGFzLm1hdHJpeChmaXRfdHJpZykKc2lnbWFfaGF0IDwtIG1lZGlhbihzaW1zX3RyaWdbLCJzaWdtYSJdKQpgYGAKCiMjIyMgUGxvdCBnZW9tZXRyeSBiYXNlZCBtb2RlbCByZXN1bHQKCmBgYHtyIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmlmIChzYXZlZmlncykgcGRmKHJvb3QoIkdvbGYvZmlncyIsImdvbGYyLnBkZiIpLCBoZWlnaHQ9NSwgd2lkdGg9NykKYGBgCmBgYHtyIH0KcGFyKG1hcj1jKDMsMywyLDEpLCBtZ3A9YygxLjcsLjUsMCksIHRjaz0tLjAyKQp3aXRoKGdvbGYsIHsKICAgIHBsb3QoeCwgeS9uLCB4bGltPWMoMCwgMS4xKm1heCh4KSksIHlsaW09YygwLCAxLjAyKSwKICAgICAgICAgeGF4cz0iaSIsIHlheHM9ImkiLCBwY2g9MjAsIGJ0eT0ibCIsCiAgICAgICAgIHhsYWI9IkRpc3RhbmNlIGZyb20gaG9sZSAoZmVldCkiLCB5bGFiPSJQcm9iYWJpbGl0eSBvZiBzdWNjZXNzIiwKICAgICAgICAgbWFpbj0iQ3VzdG9tIG5vbmxpbmVhciBtb2RlbCBmaXQgaW4gU3RhbiIpCiAgICBzZWdtZW50cyh4LCB5L24gKyBzZSwgeCwgeS9uLXNlLCBsd2Q9LjUpCiAgICB4X2dyaWQgPC0gc2VxKFItciwgMS4xKm1heCh4KSwgLjAxKQogICAgcF9ncmlkIDwtIDIqcG5vcm0oYXNpbigoUi1yKS94X2dyaWQpIC8gc2lnbWFfaGF0KSAtIDEKICAgIGxpbmVzKGMoMCwgUi1yLCB4X2dyaWQpLCBjKDEsIDEsIHBfZ3JpZCkpCiAgICB0ZXh0KDE4LjUsIC4yNiwgcGFzdGUoIkdlb21ldHJ5LWJhc2VkIG1vZGVsLFxuIHNpZ21hID0gIiwKICAgICAgICAgICAgICAgICAgICAgICAgICByb3VuZChzaWdtYV9oYXQqMTgwL3BpLCAxKSwgIiBkZWdyZWVzIiwgc2VwPSIiKSkKfSkKYGBgCmBgYHtyIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmlmIChzYXZlZmlncykgZGV2Lm9mZigpCmBgYAoKIyMjIyBQbG90IGdlb21ldHJ5IGJhc2VkIG1vZGVsIHBvc3RlcmlvciBkcmF3cyBvZiBzaWdtYQoKYGBge3IgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0KaWYgKHNhdmVmaWdzKSBwZGYocm9vdCgiR29sZi9maWdzIiwiZ29sZjJhLnBkZiIpLCBoZWlnaHQ9NSwgd2lkdGg9NykKYGBgCmBgYHtyIH0KcGFyKG1hcj1jKDMsMywyLDEpLCBtZ3A9YygxLjcsLjUsMCksIHRjaz0tLjAyKQp3aXRoKGdvbGYsIHsKICAgIHBsb3QoeCwgeS9uLCB4bGltPWMoMCwgMS4xKm1heCh4KSksIHlsaW09YygwLCAxLjAyKSwKICAgICAgICAgeGF4cz0iaSIsIHlheHM9ImkiLCBwY2g9MjAsIGJ0eT0ibCIsCiAgICAgICAgIHhsYWI9IkRpc3RhbmNlIGZyb20gaG9sZSAoZmVldCkiLCB5bGFiPSJQcm9iYWJpbGl0eSBvZiBzdWNjZXNzIiwKICAgICAgICAgbWFpbj0iQ3VzdG9tIG5vbmxpbmVhciBtb2RlbCBmaXQgaW4gU3RhbiIpCiAgICBzZWdtZW50cyh4LCB5L24gKyBzZSwgeCwgeS9uLXNlLCBsd2Q9LjUpCiAgICB4X2dyaWQgPC0gc2VxKFItciwgMS4xKm1heCh4KSwgLjAxKQogICAgbl9zaW1zIDwtIGxlbmd0aChzaW1zX3RyaWdbLCJzaWdtYSJdKQogICAgZm9yIChzIGluIHNhbXBsZShuX3NpbXMsIDIwKSl7CiAgICAgICAgcF9ncmlkIDwtIDIqcG5vcm0oYXNpbigoUi1yKS94X2dyaWQpIC8gc2ltc190cmlnW3MsInNpZ21hIl0pIC0gMQogICAgICAgIGxpbmVzKGMoMCwgUi1yLCB4X2dyaWQpLCBjKDEsIDEsIHBfZ3JpZCksIGx3ZD0wLjUpCiAgICB9CiAgICB0ZXh0KDE4LjUsIC4yNiwgIkdlb21ldHJ5LWJhc2VkIG1vZGVsLFxuIHBvc3QgZHJhd3Mgb2Ygc2lnbWEiKQp9KQpgYGAKYGBge3IgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0KaWYgKHNhdmVmaWdzKSBkZXYub2ZmKCkKYGBgCgojIyMjIFBsb3QgdHdvIG1vZGVscyBpbiBzYW1lIGZpZ3VyZQoKYGBge3IgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0KaWYgKHNhdmVmaWdzKSBwZGYocm9vdCgiR29sZi9maWdzIiwiZ29sZjMucGRmIiksIGhlaWdodD01LCB3aWR0aD03KQpgYGAKYGBge3IgfQpwYXIobWFyPWMoMywzLDIsMSksIG1ncD1jKDEuNywuNSwwKSwgdGNrPS0uMDIpCndpdGgoZ29sZiwgewogICAgcGxvdCh4LCB5L24sIHhsaW09YygwLCAxLjEqbWF4KHgpKSwgeWxpbT1jKDAsIDEuMDIpLAogICAgICAgICB4YXhzPSJpIiwgeWF4cz0iaSIsIHBjaD0yMCwgYnR5PSJsIiwKICAgICAgICAgeGxhYj0iRGlzdGFuY2UgZnJvbSBob2xlIChmZWV0KSIsIHlsYWI9IlByb2JhYmlsaXR5IG9mIHN1Y2Nlc3MiLAogICAgICAgICBtYWluPSJUd28gbW9kZWxzIGZpdCB0byB0aGUgZ29sZiBwdXR0aW5nIGRhdGEiKQogICAgc2VnbWVudHMoeCwgeS9uICsgc2UsIHgsIHkvbi1zZSwgbHdkPS41KQogICAgY3VydmUoaW52bG9naXQoYV9oYXQgKyBiX2hhdCp4KSwgZnJvbT0wLCB0bz0xLjEqbWF4KHgpLCBhZGQ9VFJVRSkKICAgIHhfZ3JpZCA8LSBzZXEoUi1yLCAxLjEqbWF4KHgpLCAuMDEpCiAgICBwX2dyaWQgPC0gMipwbm9ybShhc2luKChSLXIpL3hfZ3JpZCkgLyBzaWdtYV9oYXQpIC0gMQogICAgbGluZXMoYygwLCBSLXIsIHhfZ3JpZCksIGMoMSwgMSwgcF9ncmlkKSkKICAgIHRleHQoMTAuMywgLjU4LCAiTG9naXN0aWMgcmVncmVzc2lvbiIpCiAgICB0ZXh0KDE4LjUsIC4yNCwgIkdlb21ldHJ5LWJhc2VkIG1vZGVsIikKfSkKYGBgCmBgYHtyIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmlmIChzYXZlZmlncykgZGV2Lm9mZigpCmBgYAoK