Goals:

  • Handcode DR-learner for GATE estimation with OLS, Kernel and Series regression


Linear heterogeneity

DGP

Consider the following DGP with linear heterogeneous treatment effects:

  • \(p=10\) independent covariates \(X_1,...,X_k,...,X_{10}\) drawn from a uniform distribution: \(X_k \sim uniform(-\pi,\pi)\)

  • The treatment model is \(W \sim Bernoulli(\underbrace{\Phi(sin(X_1))}_{e(X)})\), where \(\Phi(\cdot)\) is the standard normal cumulative density function

  • The potential outcome model of the controls is \(Y(0) = \underbrace{sin(X_1)}_{m_0(X)} + \varepsilon\), with \(\varepsilon \sim N(0,1)\)

  • The CATE function is a linear function of the first three covariates \(\tau(X) = \underbrace{0.3}_{\rho_1} X_1 + \underbrace{0.2}_{\rho_2} X_2 + \underbrace{0.1}_{\rho_3} X_3\)

  • The potential outcome model of the treated is \(Y(1) = m_0(X) + \tau(X) + \varepsilon\), with \(\varepsilon \sim N(0,1)\)

This leads to substantial effect heterogeneity driven by the first three covariates:

if (!require("grf")) install.packages("grf", dependencies = TRUE); library(grf)
if (!require("tidyverse")) install.packages("tidyverse", dependencies = TRUE); library(tidyverse)
if (!require("patchwork")) install.packages("patchwork", dependencies = TRUE); library(patchwork)
if (!require("estimatr")) install.packages("estimatr", dependencies = TRUE); library(estimatr)
if (!require("np")) install.packages("np", dependencies = TRUE); library(np)
if (!require("crs")) install.packages("crs", dependencies = TRUE); library(crs)
if (!require("causalDML")) {
  if (!require("devtools")) install.packages("devtools", dependencies = TRUE); library(devtools)
  install_github(repo="MCKnaus/causalDML") 
}; library(causalDML)

set.seed(1234)

# Set parameters
n = 1000
p = 10

rho = c(0.3,0.2,0.1,rep(0,7))

# Draw sample
x = matrix(runif(n*p,-pi,pi),ncol=p)
e = function(x){pnorm(sin(x))}
m0 = function(x){sin(x)}
tau = x %*% rho
w = rbinom(n,1,e(x[,1]))
y = m0(x[,1]) + w*tau + rnorm(n,0,1)
hist(tau)


GATE estimation with OLS

Hand-coded 2-fold cross-fitting

Consider that we are interested in the heterogeneity with respect to the first five covariates, \(X_1\) to \(X_5\).

We draw a sample of \(N=1000\) and estimate the nuisance parameters \(e(X)=E[W|X]\), \(m(0,X)=E[Y|W=0,X]\) and \(m(1,X)=E[Y|W=1,X]\) using honest random forest with self-tuned tuning parameters and all ten covariates.

We first handcode 2-fold cross-validation and plug the resulting nuisance parameters into the pseudo outcome \[\tilde{Y}_{ATE} = \underbrace{\hat{m}(1,X) - \hat{m}(0,X)}_{\text{outcome predictions}} + \underbrace{\frac{W (Y - \hat{m}(1,X))}{\hat{e}(X)} - \frac{(1-W) (Y - \hat{m}(0,X))}{1-\hat{e}(X)}}_{\text{weighted residuals}}\]

# 2-fold cross-fitting
m0hat = m1hat = ehat = rep(NA,n)
# Draw random indices for sample 1
index_s1 = sample(1:n,n/2)
# Create S1
x1 = x[index_s1,]
w1 = w[index_s1]
y1 = y[index_s1]
# Create sample 2 with those not in S1
x2 = x[-index_s1,]
w2 = w[-index_s1]
y2 = y[-index_s1]
# Model in S1, predict in S2
rf = regression_forest(x1,w1,tune.parameters = "all")
ehat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1[w1==0,],y1[w1==0],tune.parameters = "all")
m0hat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1[w1==1,],y1[w1==1],tune.parameters = "all")
m1hat[-index_s1] = predict(rf,newdata=x2)$predictions
# Model in S2, predict in S1
rf = regression_forest(x2,w2,tune.parameters = "all")
ehat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2[w2==0,],y2[w2==0],tune.parameters = "all")
m0hat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2[w2==1,],y2[w2==1],tune.parameters = "all")
m1hat[index_s1] = predict(rf,newdata=x1)$predictions
# Generate pseudo-outcome
pseudo_y =  m1hat - m0hat +
  w*(y-m1hat) / ehat - (1-w)*(y-m0hat) / (1-ehat)

We know that the unconditional mean of the pseudo-outcome estimates the ATE. Now, we use it as pseudo-outcome in a multivariate regression using the five heterogeneity variables as covariates:

lm_fit2 = lm_robust(pseudo_y~x[,1:5])
summary(lm_fit2)

Call:
lm_robust(formula = pseudo_y ~ x[, 1:5])

Standard error type:  HC2 

Coefficients:
            Estimate Std. Error t value  Pr(>|t|)  CI Lower CI Upper  DF
(Intercept) 0.118936    0.06886  1.7271 8.445e-02 -0.016198  0.25407 994
x[, 1:5]1   0.234405    0.04041  5.8009 8.860e-09  0.155109  0.31370 994
x[, 1:5]2   0.213947    0.04082  5.2417 1.943e-07  0.133850  0.29404 994
x[, 1:5]3   0.077709    0.03862  2.0122 4.447e-02  0.001925  0.15349 994
x[, 1:5]4   0.007174    0.03726  0.1925 8.474e-01 -0.065948  0.08030 994
x[, 1:5]5   0.018603    0.03633  0.5121 6.087e-01 -0.052682  0.08989 994

Multiple R-squared:  0.06936 ,  Adjusted R-squared:  0.06468 
F-statistic: 13.15 on 5 and 994 DF,  p-value: 2.005e-12
se2 = lm_fit2$std.error
data.frame(Variable = c("Constant",paste0("X",1:5)),
           Coefficient = lm_fit2$coefficients,
           cil = lm_fit2$coefficients - 1.96*se2,
           ciu = lm_fit2$coefficients + 1.96*se2,
           truth = c(0,rho[1:5])) %>% 
  ggplot(aes(x=Variable,y=Coefficient,ymin=cil,ymax=ciu)) + geom_point(size=2.5,aes(colour="Estimate",shape="Estimate")) + geom_errorbar(width=0.15)  +
  geom_hline(yintercept=0) + geom_point(aes(x=Variable,y=truth,colour="Truth",shape="Truth"),size=2.5) +
  scale_colour_manual(name="Legend", values = c("black","blue")) + 
  scale_shape_manual(name="Legend",values = c(19,8))

The estimated coefficients are close to the true ones and the true coefficients are covered by the respective 95% confidence intervals.

Thus, it is also not surprising that the correlation of the resulting fitted values with the true CATE is obvious and strong:

plot(tau,lm_fit2$fitted.values)


5-fold cross-fitting with causalDML package

Next we consider 5-fold cross-fitting using the causalDML package. This requires to first estimate the standard average effects:

# 5-fold cross-fitting with causalDML package
# Create learner
forest = create_method("forest_grf",args=list(tune.parameters = "all"))
# Run
aipw = DML_aipw(y,w,x,ml_w=list(forest),ml_y=list(forest),cf=5)
summary(aipw$APO)
          APO         SE
0 -0.03810188 0.05205454
1  0.06029791 0.06423154
summary(aipw$ATE)
          ATE      SE      t      p
1 - 0 0.09840 0.07321 1.3441 0.1792

It stores the pseudo-outcome in the created object (object$ATE$delta) such that we can use it in the next step without rerunning the ML steps again:

lm_fit5 = lm_robust(aipw$ATE$delta~x[,1:5])
summary(lm_fit5)

Call:
lm_robust(formula = aipw$ATE$delta ~ x[, 1:5])

Standard error type:  HC2 

Coefficients:
            Estimate Std. Error t value  Pr(>|t|)  CI Lower CI Upper  DF
(Intercept) 0.100617    0.07027  1.4318 1.525e-01 -0.037284  0.23852 994
x[, 1:5]1   0.244793    0.04073  6.0102 2.599e-09  0.164867  0.32472 994
x[, 1:5]2   0.231625    0.04135  5.6014 2.751e-08  0.150479  0.31277 994
x[, 1:5]3   0.079803    0.03940  2.0256 4.308e-02  0.002491  0.15711 994
x[, 1:5]4   0.008015    0.03796  0.2112 8.328e-01 -0.066472  0.08250 994
x[, 1:5]5   0.023393    0.03736  0.6261 5.314e-01 -0.049922  0.09671 994

Multiple R-squared:  0.07435 ,  Adjusted R-squared:  0.0697 
F-statistic: 14.41 on 5 and 994 DF,  p-value: 1.215e-13
se5 = lm_fit5$std.error
data.frame(Variable = c("Constant",paste0("X",1:5)),
           Coefficient = lm_fit5$coefficients,
           cil = lm_fit5$coefficients - 1.96*se5,
           ciu = lm_fit5$coefficients + 1.96*se5,
           truth = c(0,rho[1:5])) %>% 
  ggplot(aes(x=Variable,y=Coefficient,ymin=cil,ymax=ciu)) + geom_point(linewidth=2.5,aes(colour="Estimate",shape="Estimate")) + geom_errorbar(width=0.15)  +
  geom_hline(yintercept=0) + geom_point(aes(x=Variable,y=truth,colour="Truth",shape="Truth"),linewidth=2.5) +
  scale_colour_manual(name="Legend", values = c("black","blue")) + 
  scale_shape_manual(name="Legend",values = c(19,8))
Warnung: Ignoring unknown parameters: `linewidth`Warnung: Ignoring unknown parameters: `linewidth`

Again the predicted CATEs and the true ones are highly correlated:

plot(tau,lm_fit5$fitted.values)

Remark: This procedure would estimate the Best Linear Predictor of the CATE with respect to the five variables in the (likely) case that the underlying CATE function is not really linear.



Non-parametric heterogeneity

Now we revisit the second DGP of notebook SNB_AIPW_DML with zero ATE but highly nonlinear effect heterogeneity:

  • \(p=10\) independent covariates \(X_1,...,X_k,...,X_{10}\) drawn from a uniform distribution: \(X_k \sim uniform(-\pi,\pi)\)

  • The treatment model is \(W \sim Bernoulli(\underbrace{\Phi(sin(X_1))}_{e(X)})\), where \(\Phi(\cdot)\) is the standard normal cumulative density function

  • The outcome model of the controls is \(Y(0) = \underbrace{cos(X_1+1/2\pi)}_{m_0(X)}+ \varepsilon\), with \(\varepsilon \sim N(0,1)\)

  • The outcome model of the treated is \(Y(1) = \underbrace{sin(X_1)}_{m_1(X)}+ \varepsilon\), with \(\varepsilon \sim N(0,1)\)

  • The treatment effect function is \(\tau(X) = sin(X_1) - cos(X_1+1/2\pi)\)

x = matrix(runif(n*p,-pi,pi),ncol=p)
e = function(x){pnorm(sin(x))}
m1 = function(x){sin(x)}
m0 = function(x){cos(x+1/2*pi)}
tau = function(x){m1(x) - m0(x)}
w = rbinom(n,1,e(x[,1]))
y = w*m1(x[,1]) + (1-w)*m0(x[,1]) + rnorm(n,0,1)

g1 = data.frame(x = c(-pi, pi)) %>% ggplot(aes(x)) + stat_function(fun=e,linewidth=1) + ylab("e") + xlab("X1")
g2 = data.frame(x = c(-pi, pi)) %>% ggplot(aes(x)) + stat_function(fun=m1,linewidth=1,aes(colour="Y1")) + 
  stat_function(fun=m0,linewidth=1,aes(colour="Y0")) + ylab("Y") + xlab("X1")
g3 = data.frame(x = c(-pi, pi)) %>% ggplot(aes(x)) + stat_function(fun=tau,linewidth=1) + ylab(expression(tau)) + xlab("X1")
g1 / g2 / g3


GATE estimation with kernel regression

Hand-coded 2-fold cross-fitting

Consider we are interested in the heterogeneity with respect to the first covariate \(X_1\).

We draw a sample of \(N=1000\) and estimate the nuisance parameters \(e(X)=E[W|X]\), \(m(0,X)=E[Y|W=0,X]\) and \(m(1,X)=E[Y|W=1,X]\) using honest random forest with self-tuned tuning parameters and all ten covariates.

We first handcode 2-fold cross-validation and plug the resulting nuisance parameters into the pseudo outcome formula:

m0hat = m1hat = ehat = rep(NA,n)
# Draw random indices for sample 1
index_s1 = sample(1:n,n/2)
# Create S1
x1 = x[index_s1,]
w1 = w[index_s1]
y1 = y[index_s1]
# Create sample 2 with those not in S1
x2 = x[-index_s1,]
w2 = w[-index_s1]
y2 = y[-index_s1]
# Model in S1, predict in S2
rf = regression_forest(x1,w1,tune.parameters = "all")
ehat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1[w1==0,],y1[w1==0],tune.parameters = "all")
m0hat[-index_s1] = predict(rf,newdata=x2)$predictions
rf = regression_forest(x1[w1==1,],y1[w1==1],tune.parameters = "all")
m1hat[-index_s1] = predict(rf,newdata=x2)$predictions
# Model in S2, predict in S1
rf = regression_forest(x2,w2,tune.parameters = "all")
ehat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2[w2==0,],y2[w2==0],tune.parameters = "all")
m0hat[index_s1] = predict(rf,newdata=x1)$predictions
rf = regression_forest(x2[w2==1,],y2[w2==1],tune.parameters = "all")
m1hat[index_s1] = predict(rf,newdata=x1)$predictions
# generate pseudo-outcome
pseudo_y =  m1hat - m0hat +
  w*(y-m1hat) / ehat - (1-w)*(y-m0hat) / (1-ehat)

Now we use the pseudo-outcome in a kernel regression with the only covariate being \(X_1\). We cross-validate the bandwidth of the kernel regression, run the estimation with the np package and plot the estimated curve:

z = as.data.frame(x[,1])
# Crossvalidate bandwidth
bwobj = npregbw(ydat = pseudo_y, xdat = z, ckertype = 'gaussian', ckerorder = 2, regtype = 'lc', bwmethod = 'cv.ls')
bws = bwobj$bw
# Undersmoothing, i.e. chose a slightly smaller bandwidth than was cross-validated
bw = bwobj$bw * 0.9
cate_model = npreg(tydat = pseudo_y, txdat = z, bws=bw, ckertype = 'gaussian', ckerorder = 2, regtype = 'lc')

plot(cate_model)

This looks very similar to the true function.


5-fold cross-fitting with causalDML package

Next we consider 5-fold cross-fitting using the causalDML package. This requires to first estimate the standard average effects:

# 5-fold cross-fitting with causalDML package
# Create learner
forest = create_method("forest_grf",args=list(tune.parameters = "all"))
# Run
aipw = DML_aipw(y,w,x,ml_w=list(forest),ml_y=list(forest),cf=5)
summary(aipw$APO)
          APO         SE
0 -0.01052922 0.05749346
1  0.08131356 0.05128989
summary(aipw$ATE)
           ATE       SE     t      p
1 - 0 0.091843 0.082444 1.114 0.2655

It stores the pseudo-outcome in the created object (object$ATE$delta) such that we can use it in the next step without rerunning the ML steps again. The full procedure is implemented in the kr_cate function:

kernel_reg_x1 = kr_cate(aipw$ATE$delta,x[, 1])
plot(kernel_reg_x1)

We observe that the estimated curve fits quite nicely and the x-axis is included in the 95% confidence intervals where it should be \(\Rightarrow\) We find what we should find.

Finally, lets check whether we find something that is not there by checking heterogeneity with respect to \(X_2\).

kernel_reg_x2 = kr_cate(aipw$ATE$delta,x[, 2])
plot(kernel_reg_x2)

We find no evidence of heterogeneous effects along \(X_2\), as we should.



GATE estimation with series regression

Hand-coded 2-fold cross-fitting

Consider again that we are interested in the heterogeneity with respect to the first covariate \(X_1\). We reuse first the handcoded 2-fold cross-fitted pseudo outcome in a series regression with B-splines using the crs package

spline_gate = crs(pseudo_y ~ as.matrix(z))
plot(spline_gate,mean=T)

Also the spline function nicely approximates the true function.


5-fold cross-fitting with causalDML package

Again, we can reuse the pseudo-outcome (object\$ATE\$delta) and use the spline_cate function of the causalDML package:

spline_reg_x1 = spline_cate(aipw$ATE$delta,x[, 1])
plot(spline_reg_x1)

Again this looks like the true function. Most importantly, the x-axis is included in the 95% confidence intervals where it should be \(\Rightarrow\) We find what we should find.

Finally, lets check whether we find something that is not there by checking heterogeneity with respect to \(X_2\).

spline_reg_x2 = spline_cate(aipw$ATE$delta,x[, 2])
plot(spline_reg_x2)

We find no evidence of heterogeneous effects along \(X_2\), as we should.



Take-away

  • Estimating heterogeneous effects with pre-specified heterogeneity variables is just a few lines of additional code reusing the pseudo-outcome from the average effect estimation

  • We can model effect sizes as we are used to modeling outcome levels by using the right pseudo-outcome



Suggestions to play with the toy model

Some suggestions:

  • Increase/decrease the number of observations

  • Create a non-linear CATE in the first part

  • Change the treatment shares

LS0tDQp0aXRsZTogIkNhdXNhbCBNTDogR3JvdXAgQXZlcmFnZSBUcmVhdG1lbnQgRWZmZWN0cyINCnN1YnRpdGxlOiAiU2ltdWxhdGlvbiBub3RlYm9vayINCmF1dGhvcjogIk1pY2hhZWwgS25hdXMiDQpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclbS8leScpYCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQotLS0NCg0KDQpHb2FsczoNCg0KLSBIYW5kY29kZSBEUi1sZWFybmVyIGZvciBHQVRFIGVzdGltYXRpb24gd2l0aCBPTFMsIEtlcm5lbCBhbmQgU2VyaWVzIHJlZ3Jlc3Npb24NCg0KPGJyPg0KDQoNCg0KIyBMaW5lYXIgaGV0ZXJvZ2VuZWl0eQ0KDQojIyBER1ANCg0KQ29uc2lkZXIgdGhlIGZvbGxvd2luZyBER1Agd2l0aCBsaW5lYXIgaGV0ZXJvZ2VuZW91cyB0cmVhdG1lbnQgZWZmZWN0czoNCg0KLSAkcD0xMCQgaW5kZXBlbmRlbnQgY292YXJpYXRlcyAkWF8xLC4uLixYX2ssLi4uLFhfezEwfSQgZHJhd24gZnJvbSBhIHVuaWZvcm0gZGlzdHJpYnV0aW9uOiAkWF9rIFxzaW0gdW5pZm9ybSgtXHBpLFxwaSkkDQoNCi0gVGhlIHRyZWF0bWVudCBtb2RlbCBpcyAkVyBcc2ltIEJlcm5vdWxsaShcdW5kZXJicmFjZXtcUGhpKHNpbihYXzEpKX1fe2UoWCl9KSQsIHdoZXJlICRcUGhpKFxjZG90KSQgaXMgdGhlIHN0YW5kYXJkIG5vcm1hbCBjdW11bGF0aXZlIGRlbnNpdHkgZnVuY3Rpb24NCg0KLSBUaGUgcG90ZW50aWFsIG91dGNvbWUgbW9kZWwgb2YgdGhlIGNvbnRyb2xzIGlzICRZKDApID0gXHVuZGVyYnJhY2V7c2luKFhfMSl9X3ttXzAoWCl9ICsgXHZhcmVwc2lsb24kLCB3aXRoICRcdmFyZXBzaWxvbiBcc2ltIE4oMCwxKSQNCg0KLSBUaGUgQ0FURSBmdW5jdGlvbiBpcyBhIGxpbmVhciBmdW5jdGlvbiBvZiB0aGUgZmlyc3QgdGhyZWUgY292YXJpYXRlcyAkXHRhdShYKSA9IFx1bmRlcmJyYWNlezAuM31fe1xyaG9fMX0gWF8xICsgXHVuZGVyYnJhY2V7MC4yfV97XHJob18yfSBYXzIgKyBcdW5kZXJicmFjZXswLjF9X3tccmhvXzN9IFhfMyQNCg0KLSBUaGUgcG90ZW50aWFsIG91dGNvbWUgbW9kZWwgb2YgdGhlIHRyZWF0ZWQgaXMgJFkoMSkgPSBtXzAoWCkgKyBcdGF1KFgpICsgXHZhcmVwc2lsb24kLCB3aXRoICRcdmFyZXBzaWxvbiBcc2ltIE4oMCwxKSQNCg0KVGhpcyBsZWFkcyB0byBzdWJzdGFudGlhbCBlZmZlY3QgaGV0ZXJvZ2VuZWl0eSBkcml2ZW4gYnkgdGhlIGZpcnN0IHRocmVlIGNvdmFyaWF0ZXM6IA0KDQoNCmBgYHtyLCB3YXJuaW5nID0gRiwgbWVzc2FnZSA9IEZ9DQppZiAoIXJlcXVpcmUoImdyZiIpKSBpbnN0YWxsLnBhY2thZ2VzKCJncmYiLCBkZXBlbmRlbmNpZXMgPSBUUlVFKTsgbGlicmFyeShncmYpDQppZiAoIXJlcXVpcmUoInRpZHl2ZXJzZSIpKSBpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5dmVyc2UiLCBkZXBlbmRlbmNpZXMgPSBUUlVFKTsgbGlicmFyeSh0aWR5dmVyc2UpDQppZiAoIXJlcXVpcmUoInBhdGNod29yayIpKSBpbnN0YWxsLnBhY2thZ2VzKCJwYXRjaHdvcmsiLCBkZXBlbmRlbmNpZXMgPSBUUlVFKTsgbGlicmFyeShwYXRjaHdvcmspDQppZiAoIXJlcXVpcmUoImVzdGltYXRyIikpIGluc3RhbGwucGFja2FnZXMoImVzdGltYXRyIiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkoZXN0aW1hdHIpDQppZiAoIXJlcXVpcmUoIm5wIikpIGluc3RhbGwucGFja2FnZXMoIm5wIiwgZGVwZW5kZW5jaWVzID0gVFJVRSk7IGxpYnJhcnkobnApDQppZiAoIXJlcXVpcmUoImNycyIpKSBpbnN0YWxsLnBhY2thZ2VzKCJjcnMiLCBkZXBlbmRlbmNpZXMgPSBUUlVFKTsgbGlicmFyeShjcnMpDQppZiAoIXJlcXVpcmUoImNhdXNhbERNTCIpKSB7DQogIGlmICghcmVxdWlyZSgiZGV2dG9vbHMiKSkgaW5zdGFsbC5wYWNrYWdlcygiZGV2dG9vbHMiLCBkZXBlbmRlbmNpZXMgPSBUUlVFKTsgbGlicmFyeShkZXZ0b29scykNCiAgaW5zdGFsbF9naXRodWIocmVwbz0iTUNLbmF1cy9jYXVzYWxETUwiKSANCn07IGxpYnJhcnkoY2F1c2FsRE1MKQ0KDQpzZXQuc2VlZCgxMjM0KQ0KDQojIFNldCBwYXJhbWV0ZXJzDQpuID0gMTAwMA0KcCA9IDEwDQoNCnJobyA9IGMoMC4zLDAuMiwwLjEscmVwKDAsNykpDQoNCiMgRHJhdyBzYW1wbGUNCnggPSBtYXRyaXgocnVuaWYobipwLC1waSxwaSksbmNvbD1wKQ0KZSA9IGZ1bmN0aW9uKHgpe3Bub3JtKHNpbih4KSl9DQptMCA9IGZ1bmN0aW9uKHgpe3Npbih4KX0NCnRhdSA9IHggJSolIHJobw0KdyA9IHJiaW5vbShuLDEsZSh4WywxXSkpDQp5ID0gbTAoeFssMV0pICsgdyp0YXUgKyBybm9ybShuLDAsMSkNCmhpc3QodGF1KQ0KYGBgDQoNCjxicj4gDQoNCiMjIEdBVEUgZXN0aW1hdGlvbiB3aXRoIE9MUw0KDQojIyMgSGFuZC1jb2RlZCAyLWZvbGQgY3Jvc3MtZml0dGluZw0KDQpDb25zaWRlciB0aGF0IHdlIGFyZSBpbnRlcmVzdGVkIGluIHRoZSBoZXRlcm9nZW5laXR5IHdpdGggcmVzcGVjdCB0byB0aGUgZmlyc3QgZml2ZSBjb3ZhcmlhdGVzLCAkWF8xJCB0byAkWF81JC4NCg0KV2UgZHJhdyBhIHNhbXBsZSBvZiAkTj0xMDAwJCBhbmQgZXN0aW1hdGUgdGhlIG51aXNhbmNlIHBhcmFtZXRlcnMgJGUoWCk9RVtXfFhdJCwgJG0oMCxYKT1FW1l8Vz0wLFhdJCBhbmQgJG0oMSxYKT1FW1l8Vz0xLFhdJCB1c2luZyBob25lc3QgcmFuZG9tIGZvcmVzdCB3aXRoIHNlbGYtdHVuZWQgdHVuaW5nIHBhcmFtZXRlcnMgYW5kIGFsbCB0ZW4gY292YXJpYXRlcy4gDQoNCldlIGZpcnN0IGhhbmRjb2RlIDItZm9sZCBjcm9zcy12YWxpZGF0aW9uIGFuZCBwbHVnIHRoZSByZXN1bHRpbmcgbnVpc2FuY2UgcGFyYW1ldGVycyBpbnRvIHRoZSBwc2V1ZG8gb3V0Y29tZQ0KJCRcdGlsZGV7WX1fe0FURX0gPSBcdW5kZXJicmFjZXtcaGF0e219KDEsWCkgLSBcaGF0e219KDAsWCl9X3tcdGV4dHtvdXRjb21lIHByZWRpY3Rpb25zfX0gKyBcdW5kZXJicmFjZXtcZnJhY3tXIChZIC0gXGhhdHttfSgxLFgpKX17XGhhdHtlfShYKX0gLSBcZnJhY3soMS1XKSAoWSAtIFxoYXR7bX0oMCxYKSl9ezEtXGhhdHtlfShYKX19X3tcdGV4dHt3ZWlnaHRlZCByZXNpZHVhbHN9fSQkDQoNCmBgYHtyfQ0KIyAyLWZvbGQgY3Jvc3MtZml0dGluZw0KbTBoYXQgPSBtMWhhdCA9IGVoYXQgPSByZXAoTkEsbikNCiMgRHJhdyByYW5kb20gaW5kaWNlcyBmb3Igc2FtcGxlIDENCmluZGV4X3MxID0gc2FtcGxlKDE6bixuLzIpDQojIENyZWF0ZSBTMQ0KeDEgPSB4W2luZGV4X3MxLF0NCncxID0gd1tpbmRleF9zMV0NCnkxID0geVtpbmRleF9zMV0NCiMgQ3JlYXRlIHNhbXBsZSAyIHdpdGggdGhvc2Ugbm90IGluIFMxDQp4MiA9IHhbLWluZGV4X3MxLF0NCncyID0gd1staW5kZXhfczFdDQp5MiA9IHlbLWluZGV4X3MxXQ0KIyBNb2RlbCBpbiBTMSwgcHJlZGljdCBpbiBTMg0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4MSx3MSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCmVoYXRbLWluZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MikkcHJlZGljdGlvbnMNCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDFbdzE9PTAsXSx5MVt3MT09MF0sdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQptMGhhdFstaW5kZXhfczFdID0gcHJlZGljdChyZixuZXdkYXRhPXgyKSRwcmVkaWN0aW9ucw0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4MVt3MT09MSxdLHkxW3cxPT0xXSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCm0xaGF0Wy1pbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDIpJHByZWRpY3Rpb25zDQojIE1vZGVsIGluIFMyLCBwcmVkaWN0IGluIFMxDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyLHcyLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KZWhhdFtpbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDEpJHByZWRpY3Rpb25zDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyW3cyPT0wLF0seTJbdzI9PTBdLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KbTBoYXRbaW5kZXhfczFdID0gcHJlZGljdChyZixuZXdkYXRhPXgxKSRwcmVkaWN0aW9ucw0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4Mlt3Mj09MSxdLHkyW3cyPT0xXSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCm0xaGF0W2luZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MSkkcHJlZGljdGlvbnMNCiMgR2VuZXJhdGUgcHNldWRvLW91dGNvbWUNCnBzZXVkb195ID0gIG0xaGF0IC0gbTBoYXQgKw0KICB3Kih5LW0xaGF0KSAvIGVoYXQgLSAoMS13KSooeS1tMGhhdCkgLyAoMS1laGF0KQ0KYGBgDQoNCldlIGtub3cgdGhhdCB0aGUgdW5jb25kaXRpb25hbCBtZWFuIG9mIHRoZSBwc2V1ZG8tb3V0Y29tZSBlc3RpbWF0ZXMgdGhlIEFURS4gTm93LCB3ZSB1c2UgaXQgYXMgcHNldWRvLW91dGNvbWUgaW4gYSBtdWx0aXZhcmlhdGUgcmVncmVzc2lvbiB1c2luZyB0aGUgZml2ZSBoZXRlcm9nZW5laXR5IHZhcmlhYmxlcyBhcyBjb3ZhcmlhdGVzOg0KDQoNCmBgYHtyfQ0KbG1fZml0MiA9IGxtX3JvYnVzdChwc2V1ZG9feX54WywxOjVdKQ0Kc3VtbWFyeShsbV9maXQyKQ0Kc2UyID0gbG1fZml0MiRzdGQuZXJyb3INCmRhdGEuZnJhbWUoVmFyaWFibGUgPSBjKCJDb25zdGFudCIscGFzdGUwKCJYIiwxOjUpKSwNCiAgICAgICAgICAgQ29lZmZpY2llbnQgPSBsbV9maXQyJGNvZWZmaWNpZW50cywNCiAgICAgICAgICAgY2lsID0gbG1fZml0MiRjb2VmZmljaWVudHMgLSAxLjk2KnNlMiwNCiAgICAgICAgICAgY2l1ID0gbG1fZml0MiRjb2VmZmljaWVudHMgKyAxLjk2KnNlMiwNCiAgICAgICAgICAgdHJ1dGggPSBjKDAscmhvWzE6NV0pKSAlPiUgDQogIGdncGxvdChhZXMoeD1WYXJpYWJsZSx5PUNvZWZmaWNpZW50LHltaW49Y2lsLHltYXg9Y2l1KSkgKyBnZW9tX3BvaW50KHNpemU9Mi41LGFlcyhjb2xvdXI9IkVzdGltYXRlIixzaGFwZT0iRXN0aW1hdGUiKSkgKyBnZW9tX2Vycm9yYmFyKHdpZHRoPTAuMTUpICArDQogIGdlb21faGxpbmUoeWludGVyY2VwdD0wKSArIGdlb21fcG9pbnQoYWVzKHg9VmFyaWFibGUseT10cnV0aCxjb2xvdXI9IlRydXRoIixzaGFwZT0iVHJ1dGgiKSxzaXplPTIuNSkgKw0KICBzY2FsZV9jb2xvdXJfbWFudWFsKG5hbWU9IkxlZ2VuZCIsIHZhbHVlcyA9IGMoImJsYWNrIiwiYmx1ZSIpKSArIA0KICBzY2FsZV9zaGFwZV9tYW51YWwobmFtZT0iTGVnZW5kIix2YWx1ZXMgPSBjKDE5LDgpKQ0KYGBgDQoNClRoZSBlc3RpbWF0ZWQgY29lZmZpY2llbnRzIGFyZSBjbG9zZSB0byB0aGUgdHJ1ZSBvbmVzIGFuZCB0aGUgdHJ1ZSBjb2VmZmljaWVudHMgYXJlIGNvdmVyZWQgYnkgdGhlIHJlc3BlY3RpdmUgOTUlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzLg0KDQpUaHVzLCBpdCBpcyBhbHNvIG5vdCBzdXJwcmlzaW5nIHRoYXQgdGhlIGNvcnJlbGF0aW9uIG9mIHRoZSByZXN1bHRpbmcgZml0dGVkIHZhbHVlcyB3aXRoIHRoZSB0cnVlIENBVEUgaXMgb2J2aW91cyBhbmQgc3Ryb25nOg0KDQpgYGB7cn0NCnBsb3QodGF1LGxtX2ZpdDIkZml0dGVkLnZhbHVlcykNCmBgYA0KDQoNCjxicj4NCg0KIyMjIDUtZm9sZCBjcm9zcy1maXR0aW5nIHdpdGggYGNhdXNhbERNTGAgcGFja2FnZQ0KDQpOZXh0IHdlIGNvbnNpZGVyIDUtZm9sZCBjcm9zcy1maXR0aW5nIHVzaW5nIHRoZSBgY2F1c2FsRE1MYCBwYWNrYWdlLiBUaGlzIHJlcXVpcmVzIHRvIGZpcnN0IGVzdGltYXRlIHRoZSBzdGFuZGFyZCBhdmVyYWdlIGVmZmVjdHM6DQoNCg0KYGBge3J9DQojIDUtZm9sZCBjcm9zcy1maXR0aW5nIHdpdGggY2F1c2FsRE1MIHBhY2thZ2UNCiMgQ3JlYXRlIGxlYXJuZXINCmZvcmVzdCA9IGNyZWF0ZV9tZXRob2QoImZvcmVzdF9ncmYiLGFyZ3M9bGlzdCh0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikpDQojIFJ1bg0KYWlwdyA9IERNTF9haXB3KHksdyx4LG1sX3c9bGlzdChmb3Jlc3QpLG1sX3k9bGlzdChmb3Jlc3QpLGNmPTUpDQpzdW1tYXJ5KGFpcHckQVBPKQ0Kc3VtbWFyeShhaXB3JEFURSkNCmBgYA0KDQpJdCBzdG9yZXMgdGhlIHBzZXVkby1vdXRjb21lIGluIHRoZSBjcmVhdGVkIG9iamVjdCAoYG9iamVjdCRBVEUkZGVsdGFgKSBzdWNoIHRoYXQgd2UgY2FuIHVzZSBpdCBpbiB0aGUgbmV4dCBzdGVwIHdpdGhvdXQgcmVydW5uaW5nIHRoZSBNTCBzdGVwcyBhZ2FpbjoNCg0KDQpgYGB7cn0NCmxtX2ZpdDUgPSBsbV9yb2J1c3QoYWlwdyRBVEUkZGVsdGF+eFssMTo1XSkNCnN1bW1hcnkobG1fZml0NSkNCnNlNSA9IGxtX2ZpdDUkc3RkLmVycm9yDQpkYXRhLmZyYW1lKFZhcmlhYmxlID0gYygiQ29uc3RhbnQiLHBhc3RlMCgiWCIsMTo1KSksDQogICAgICAgICAgIENvZWZmaWNpZW50ID0gbG1fZml0NSRjb2VmZmljaWVudHMsDQogICAgICAgICAgIGNpbCA9IGxtX2ZpdDUkY29lZmZpY2llbnRzIC0gMS45NipzZTUsDQogICAgICAgICAgIGNpdSA9IGxtX2ZpdDUkY29lZmZpY2llbnRzICsgMS45NipzZTUsDQogICAgICAgICAgIHRydXRoID0gYygwLHJob1sxOjVdKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHg9VmFyaWFibGUseT1Db2VmZmljaWVudCx5bWluPWNpbCx5bWF4PWNpdSkpICsgZ2VvbV9wb2ludChsaW5ld2lkdGg9Mi41LGFlcyhjb2xvdXI9IkVzdGltYXRlIixzaGFwZT0iRXN0aW1hdGUiKSkgKyBnZW9tX2Vycm9yYmFyKHdpZHRoPTAuMTUpICArDQogIGdlb21faGxpbmUoeWludGVyY2VwdD0wKSArIGdlb21fcG9pbnQoYWVzKHg9VmFyaWFibGUseT10cnV0aCxjb2xvdXI9IlRydXRoIixzaGFwZT0iVHJ1dGgiKSxsaW5ld2lkdGg9Mi41KSArDQogIHNjYWxlX2NvbG91cl9tYW51YWwobmFtZT0iTGVnZW5kIiwgdmFsdWVzID0gYygiYmxhY2siLCJibHVlIikpICsgDQogIHNjYWxlX3NoYXBlX21hbnVhbChuYW1lPSJMZWdlbmQiLHZhbHVlcyA9IGMoMTksOCkpDQpgYGANCg0KDQpBZ2FpbiB0aGUgcHJlZGljdGVkIENBVEVzIGFuZCB0aGUgdHJ1ZSBvbmVzIGFyZSBoaWdobHkgY29ycmVsYXRlZDoNCg0KYGBge3J9DQpwbG90KHRhdSxsbV9maXQ1JGZpdHRlZC52YWx1ZXMpDQpgYGANCg0KDQoqUmVtYXJrOiogVGhpcyBwcm9jZWR1cmUgd291bGQgZXN0aW1hdGUgdGhlIEJlc3QgTGluZWFyIFByZWRpY3RvciBvZiB0aGUgQ0FURSB3aXRoIHJlc3BlY3QgdG8gdGhlIGZpdmUgdmFyaWFibGVzIGluIHRoZSAobGlrZWx5KSBjYXNlIHRoYXQgdGhlIHVuZGVybHlpbmcgQ0FURSBmdW5jdGlvbiBpcyBub3QgcmVhbGx5IGxpbmVhci4NCjxicj4NCjxicj4NCjxicj4NCjxicj4NCg0KIyBOb24tcGFyYW1ldHJpYyBoZXRlcm9nZW5laXR5DQoNCk5vdyB3ZSByZXZpc2l0IHRoZSBzZWNvbmQgREdQIG9mIG5vdGVib29rIFtTTkJfQUlQV19ETUxdKGh0dHBzOi8vbWNrbmF1cy5naXRodWIuaW8vYXNzZXRzL25vdGVib29rcy9TTkIvU05CX0FJUFdfRE1MLm5iLmh0bWwpIHdpdGggemVybyBBVEUgYnV0IGhpZ2hseSBub25saW5lYXIgZWZmZWN0IGhldGVyb2dlbmVpdHk6DQoNCi0gJHA9MTAkIGluZGVwZW5kZW50IGNvdmFyaWF0ZXMgJFhfMSwuLi4sWF9rLC4uLixYX3sxMH0kIGRyYXduIGZyb20gYSB1bmlmb3JtIGRpc3RyaWJ1dGlvbjogJFhfayBcc2ltIHVuaWZvcm0oLVxwaSxccGkpJA0KDQotIFRoZSB0cmVhdG1lbnQgbW9kZWwgaXMgJFcgXHNpbSBCZXJub3VsbGkoXHVuZGVyYnJhY2V7XFBoaShzaW4oWF8xKSl9X3tlKFgpfSkkLCB3aGVyZSAkXFBoaShcY2RvdCkkIGlzIHRoZSBzdGFuZGFyZCBub3JtYWwgY3VtdWxhdGl2ZSBkZW5zaXR5IGZ1bmN0aW9uDQoNCi0gVGhlIG91dGNvbWUgbW9kZWwgb2YgdGhlIGNvbnRyb2xzIGlzICRZKDApID0gXHVuZGVyYnJhY2V7Y29zKFhfMSsxLzJccGkpfV97bV8wKFgpfSsgXHZhcmVwc2lsb24kLCB3aXRoICRcdmFyZXBzaWxvbiBcc2ltIE4oMCwxKSQNCg0KLSBUaGUgb3V0Y29tZSBtb2RlbCBvZiB0aGUgdHJlYXRlZCBpcyAkWSgxKSA9IFx1bmRlcmJyYWNle3NpbihYXzEpfV97bV8xKFgpfSsgXHZhcmVwc2lsb24kLCB3aXRoICRcdmFyZXBzaWxvbiBcc2ltIE4oMCwxKSQNCg0KLSBUaGUgdHJlYXRtZW50IGVmZmVjdCBmdW5jdGlvbiBpcyAkXHRhdShYKSA9IHNpbihYXzEpIC0gY29zKFhfMSsxLzJccGkpJA0KDQoNCmBgYHtyfQ0KeCA9IG1hdHJpeChydW5pZihuKnAsLXBpLHBpKSxuY29sPXApDQplID0gZnVuY3Rpb24oeCl7cG5vcm0oc2luKHgpKX0NCm0xID0gZnVuY3Rpb24oeCl7c2luKHgpfQ0KbTAgPSBmdW5jdGlvbih4KXtjb3MoeCsxLzIqcGkpfQ0KdGF1ID0gZnVuY3Rpb24oeCl7bTEoeCkgLSBtMCh4KX0NCncgPSByYmlub20obiwxLGUoeFssMV0pKQ0KeSA9IHcqbTEoeFssMV0pICsgKDEtdykqbTAoeFssMV0pICsgcm5vcm0obiwwLDEpDQoNCmcxID0gZGF0YS5mcmFtZSh4ID0gYygtcGksIHBpKSkgJT4lIGdncGxvdChhZXMoeCkpICsgc3RhdF9mdW5jdGlvbihmdW49ZSxsaW5ld2lkdGg9MSkgKyB5bGFiKCJlIikgKyB4bGFiKCJYMSIpDQpnMiA9IGRhdGEuZnJhbWUoeCA9IGMoLXBpLCBwaSkpICU+JSBnZ3Bsb3QoYWVzKHgpKSArIHN0YXRfZnVuY3Rpb24oZnVuPW0xLGxpbmV3aWR0aD0xLGFlcyhjb2xvdXI9IlkxIikpICsgDQogIHN0YXRfZnVuY3Rpb24oZnVuPW0wLGxpbmV3aWR0aD0xLGFlcyhjb2xvdXI9IlkwIikpICsgeWxhYigiWSIpICsgeGxhYigiWDEiKQ0KZzMgPSBkYXRhLmZyYW1lKHggPSBjKC1waSwgcGkpKSAlPiUgZ2dwbG90KGFlcyh4KSkgKyBzdGF0X2Z1bmN0aW9uKGZ1bj10YXUsbGluZXdpZHRoPTEpICsgeWxhYihleHByZXNzaW9uKHRhdSkpICsgeGxhYigiWDEiKQ0KZzEgLyBnMiAvIGczDQpgYGANCg0KPGJyPiANCg0KIyMgR0FURSBlc3RpbWF0aW9uIHdpdGgga2VybmVsIHJlZ3Jlc3Npb24NCg0KIyMjIEhhbmQtY29kZWQgMi1mb2xkIGNyb3NzLWZpdHRpbmcNCg0KQ29uc2lkZXIgd2UgYXJlIGludGVyZXN0ZWQgaW4gdGhlIGhldGVyb2dlbmVpdHkgd2l0aCByZXNwZWN0IHRvIHRoZSBmaXJzdCBjb3ZhcmlhdGUgJFhfMSQuDQoNCldlIGRyYXcgYSBzYW1wbGUgb2YgJE49MTAwMCQgYW5kIGVzdGltYXRlIHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzICRlKFgpPUVbV3xYXSQsICRtKDAsWCk9RVtZfFc9MCxYXSQgYW5kICRtKDEsWCk9RVtZfFc9MSxYXSQgdXNpbmcgaG9uZXN0IHJhbmRvbSBmb3Jlc3Qgd2l0aCBzZWxmLXR1bmVkIHR1bmluZyBwYXJhbWV0ZXJzIGFuZCBhbGwgdGVuIGNvdmFyaWF0ZXMuIA0KDQpXZSBmaXJzdCBoYW5kY29kZSAyLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbiBhbmQgcGx1ZyB0aGUgcmVzdWx0aW5nIG51aXNhbmNlIHBhcmFtZXRlcnMgaW50byB0aGUgcHNldWRvIG91dGNvbWUgZm9ybXVsYToNCg0KYGBge3J9DQptMGhhdCA9IG0xaGF0ID0gZWhhdCA9IHJlcChOQSxuKQ0KIyBEcmF3IHJhbmRvbSBpbmRpY2VzIGZvciBzYW1wbGUgMQ0KaW5kZXhfczEgPSBzYW1wbGUoMTpuLG4vMikNCiMgQ3JlYXRlIFMxDQp4MSA9IHhbaW5kZXhfczEsXQ0KdzEgPSB3W2luZGV4X3MxXQ0KeTEgPSB5W2luZGV4X3MxXQ0KIyBDcmVhdGUgc2FtcGxlIDIgd2l0aCB0aG9zZSBub3QgaW4gUzENCngyID0geFstaW5kZXhfczEsXQ0KdzIgPSB3Wy1pbmRleF9zMV0NCnkyID0geVstaW5kZXhfczFdDQojIE1vZGVsIGluIFMxLCBwcmVkaWN0IGluIFMyDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgxLHcxLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KZWhhdFstaW5kZXhfczFdID0gcHJlZGljdChyZixuZXdkYXRhPXgyKSRwcmVkaWN0aW9ucw0KcmYgPSByZWdyZXNzaW9uX2ZvcmVzdCh4MVt3MT09MCxdLHkxW3cxPT0wXSx0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikNCm0waGF0Wy1pbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDIpJHByZWRpY3Rpb25zDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgxW3cxPT0xLF0seTFbdzE9PTFdLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KbTFoYXRbLWluZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MikkcHJlZGljdGlvbnMNCiMgTW9kZWwgaW4gUzIsIHByZWRpY3QgaW4gUzENCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDIsdzIsdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQplaGF0W2luZGV4X3MxXSA9IHByZWRpY3QocmYsbmV3ZGF0YT14MSkkcHJlZGljdGlvbnMNCnJmID0gcmVncmVzc2lvbl9mb3Jlc3QoeDJbdzI9PTAsXSx5Mlt3Mj09MF0sdHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpDQptMGhhdFtpbmRleF9zMV0gPSBwcmVkaWN0KHJmLG5ld2RhdGE9eDEpJHByZWRpY3Rpb25zDQpyZiA9IHJlZ3Jlc3Npb25fZm9yZXN0KHgyW3cyPT0xLF0seTJbdzI9PTFdLHR1bmUucGFyYW1ldGVycyA9ICJhbGwiKQ0KbTFoYXRbaW5kZXhfczFdID0gcHJlZGljdChyZixuZXdkYXRhPXgxKSRwcmVkaWN0aW9ucw0KIyBnZW5lcmF0ZSBwc2V1ZG8tb3V0Y29tZQ0KcHNldWRvX3kgPSAgbTFoYXQgLSBtMGhhdCArDQogIHcqKHktbTFoYXQpIC8gZWhhdCAtICgxLXcpKih5LW0waGF0KSAvICgxLWVoYXQpDQpgYGANCg0KTm93IHdlIHVzZSB0aGUgcHNldWRvLW91dGNvbWUgaW4gYSBrZXJuZWwgcmVncmVzc2lvbiB3aXRoIHRoZSBvbmx5IGNvdmFyaWF0ZSBiZWluZyAkWF8xJC4gV2UgY3Jvc3MtdmFsaWRhdGUgdGhlIGJhbmR3aWR0aCBvZiB0aGUga2VybmVsIHJlZ3Jlc3Npb24sIHJ1biB0aGUgZXN0aW1hdGlvbiB3aXRoIHRoZSBgbnBgIHBhY2thZ2UgYW5kIHBsb3QgdGhlIGVzdGltYXRlZCBjdXJ2ZToNCg0KYGBge3IsIHJlc3VsdHM9J2hpZGUnfQ0KeiA9IGFzLmRhdGEuZnJhbWUoeFssMV0pDQojIENyb3NzdmFsaWRhdGUgYmFuZHdpZHRoDQpid29iaiA9IG5wcmVnYncoeWRhdCA9IHBzZXVkb195LCB4ZGF0ID0geiwgY2tlcnR5cGUgPSAnZ2F1c3NpYW4nLCBja2Vyb3JkZXIgPSAyLCByZWd0eXBlID0gJ2xjJywgYndtZXRob2QgPSAnY3YubHMnKQ0KYndzID0gYndvYmokYncNCiMgVW5kZXJzbW9vdGhpbmcsIGkuZS4gY2hvc2UgYSBzbGlnaHRseSBzbWFsbGVyIGJhbmR3aWR0aCB0aGFuIHdhcyBjcm9zcy12YWxpZGF0ZWQNCmJ3ID0gYndvYmokYncgKiAwLjkNCmNhdGVfbW9kZWwgPSBucHJlZyh0eWRhdCA9IHBzZXVkb195LCB0eGRhdCA9IHosIGJ3cz1idywgY2tlcnR5cGUgPSAnZ2F1c3NpYW4nLCBja2Vyb3JkZXIgPSAyLCByZWd0eXBlID0gJ2xjJykNCg0KcGxvdChjYXRlX21vZGVsKQ0KYGBgDQoNClRoaXMgbG9va3MgdmVyeSBzaW1pbGFyIHRvIHRoZSB0cnVlIGZ1bmN0aW9uLg0KDQoNCjxicj4NCg0KIyMjIDUtZm9sZCBjcm9zcy1maXR0aW5nIHdpdGggYGNhdXNhbERNTGAgcGFja2FnZQ0KDQpOZXh0IHdlIGNvbnNpZGVyIDUtZm9sZCBjcm9zcy1maXR0aW5nIHVzaW5nIHRoZSBgY2F1c2FsRE1MYCBwYWNrYWdlLiBUaGlzIHJlcXVpcmVzIHRvIGZpcnN0IGVzdGltYXRlIHRoZSBzdGFuZGFyZCBhdmVyYWdlIGVmZmVjdHM6DQoNCg0KYGBge3J9DQojIDUtZm9sZCBjcm9zcy1maXR0aW5nIHdpdGggY2F1c2FsRE1MIHBhY2thZ2UNCiMgQ3JlYXRlIGxlYXJuZXINCmZvcmVzdCA9IGNyZWF0ZV9tZXRob2QoImZvcmVzdF9ncmYiLGFyZ3M9bGlzdCh0dW5lLnBhcmFtZXRlcnMgPSAiYWxsIikpDQojIFJ1bg0KYWlwdyA9IERNTF9haXB3KHksdyx4LG1sX3c9bGlzdChmb3Jlc3QpLG1sX3k9bGlzdChmb3Jlc3QpLGNmPTUpDQpzdW1tYXJ5KGFpcHckQVBPKQ0Kc3VtbWFyeShhaXB3JEFURSkNCmBgYA0KDQpJdCBzdG9yZXMgdGhlIHBzZXVkby1vdXRjb21lIGluIHRoZSBjcmVhdGVkIG9iamVjdCAoYG9iamVjdCRBVEUkZGVsdGFgKSBzdWNoIHRoYXQgd2UgY2FuIHVzZSBpdCBpbiB0aGUgbmV4dCBzdGVwIHdpdGhvdXQgcmVydW5uaW5nIHRoZSBNTCBzdGVwcyBhZ2Fpbi4gVGhlIGZ1bGwgcHJvY2VkdXJlIGlzIGltcGxlbWVudGVkIGluIHRoZSBga3JfY2F0ZWAgZnVuY3Rpb246DQoNCmBgYHtyLCByZXN1bHRzPSdoaWRlJ30NCmtlcm5lbF9yZWdfeDEgPSBrcl9jYXRlKGFpcHckQVRFJGRlbHRhLHhbLCAxXSkNCg0KcGxvdChrZXJuZWxfcmVnX3gxKQ0KYGBgDQoNCldlIG9ic2VydmUgdGhhdCB0aGUgZXN0aW1hdGVkIGN1cnZlIGZpdHMgcXVpdGUgbmljZWx5IGFuZCB0aGUgeC1heGlzIGlzIGluY2x1ZGVkIGluIHRoZSA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMgd2hlcmUgaXQgc2hvdWxkIGJlICRcUmlnaHRhcnJvdyQgV2UgZmluZCB3aGF0IHdlIHNob3VsZCBmaW5kLg0KDQpGaW5hbGx5LCBsZXRzIGNoZWNrIHdoZXRoZXIgd2UgZmluZCBzb21ldGhpbmcgdGhhdCBpcyBub3QgdGhlcmUgYnkgY2hlY2tpbmcgaGV0ZXJvZ2VuZWl0eSB3aXRoIHJlc3BlY3QgdG8gJFhfMiQuDQoNCmBgYHtyLCByZXN1bHRzPSdoaWRlJ30NCmtlcm5lbF9yZWdfeDIgPSBrcl9jYXRlKGFpcHckQVRFJGRlbHRhLHhbLCAyXSkNCnBsb3Qoa2VybmVsX3JlZ194MikNCmBgYA0KDQpXZSBmaW5kIG5vIGV2aWRlbmNlIG9mIGhldGVyb2dlbmVvdXMgZWZmZWN0cyBhbG9uZyAkWF8yJCwgYXMgd2Ugc2hvdWxkLg0KDQo8YnI+DQo8YnI+DQoNCg0KIyMgR0FURSBlc3RpbWF0aW9uIHdpdGggc2VyaWVzIHJlZ3Jlc3Npb24NCg0KIyMjIEhhbmQtY29kZWQgMi1mb2xkIGNyb3NzLWZpdHRpbmcNCg0KQ29uc2lkZXIgYWdhaW4gdGhhdCB3ZSBhcmUgaW50ZXJlc3RlZCBpbiB0aGUgaGV0ZXJvZ2VuZWl0eSB3aXRoIHJlc3BlY3QgdG8gdGhlIGZpcnN0IGNvdmFyaWF0ZSAkWF8xJC4gV2UgcmV1c2UgZmlyc3QgdGhlIGhhbmRjb2RlZCAyLWZvbGQgY3Jvc3MtZml0dGVkIHBzZXVkbyBvdXRjb21lIGluIGEgc2VyaWVzIHJlZ3Jlc3Npb24gd2l0aCBCLXNwbGluZXMgdXNpbmcgdGhlIGBjcnNgIHBhY2thZ2UNCg0KYGBge3IsIHJlc3VsdHM9J2hpZGUnfQ0Kc3BsaW5lX2dhdGUgPSBjcnMocHNldWRvX3kgfiBhcy5tYXRyaXgoeikpDQoNCnBsb3Qoc3BsaW5lX2dhdGUsbWVhbj1UKQ0KYGBgDQoNCkFsc28gdGhlIHNwbGluZSBmdW5jdGlvbiBuaWNlbHkgYXBwcm94aW1hdGVzIHRoZSB0cnVlIGZ1bmN0aW9uLg0KDQoNCjxicj4NCg0KIyMjIDUtZm9sZCBjcm9zcy1maXR0aW5nIHdpdGggKmNhdXNhbERNTCogcGFja2FnZQ0KDQpBZ2Fpbiwgd2UgY2FuIHJldXNlIHRoZSBwc2V1ZG8tb3V0Y29tZSAoYG9iamVjdFwkQVRFXCRkZWx0YWApIGFuZCB1c2UgdGhlIGBzcGxpbmVfY2F0ZWAgZnVuY3Rpb24gb2YgdGhlIGBjYXVzYWxETUxgIHBhY2thZ2U6DQoNCmBgYHtyLCByZXN1bHRzPSdoaWRlJ30NCnNwbGluZV9yZWdfeDEgPSBzcGxpbmVfY2F0ZShhaXB3JEFURSRkZWx0YSx4WywgMV0pDQpwbG90KHNwbGluZV9yZWdfeDEpDQpgYGANCg0KQWdhaW4gdGhpcyBsb29rcyBsaWtlIHRoZSB0cnVlIGZ1bmN0aW9uLiBNb3N0IGltcG9ydGFudGx5LCB0aGUgeC1heGlzIGlzIGluY2x1ZGVkIGluIHRoZSA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMgd2hlcmUgaXQgc2hvdWxkIGJlICRcUmlnaHRhcnJvdyQgV2UgZmluZCB3aGF0IHdlIHNob3VsZCBmaW5kLg0KDQpGaW5hbGx5LCBsZXRzIGNoZWNrIHdoZXRoZXIgd2UgZmluZCBzb21ldGhpbmcgdGhhdCBpcyBub3QgdGhlcmUgYnkgY2hlY2tpbmcgaGV0ZXJvZ2VuZWl0eSB3aXRoIHJlc3BlY3QgdG8gJFhfMiQuDQoNCmBgYHtyLCByZXN1bHRzPSdoaWRlJ30NCnNwbGluZV9yZWdfeDIgPSBzcGxpbmVfY2F0ZShhaXB3JEFURSRkZWx0YSx4WywgMl0pDQpwbG90KHNwbGluZV9yZWdfeDIpDQpgYGANCg0KV2UgZmluZCBubyBldmlkZW5jZSBvZiBoZXRlcm9nZW5lb3VzIGVmZmVjdHMgYWxvbmcgJFhfMiQsIGFzIHdlIHNob3VsZC4NCg0KPGJyPg0KPGJyPg0KDQoNCg0KIyMgVGFrZS1hd2F5DQogDQogLSBFc3RpbWF0aW5nIGhldGVyb2dlbmVvdXMgZWZmZWN0cyB3aXRoIHByZS1zcGVjaWZpZWQgaGV0ZXJvZ2VuZWl0eSB2YXJpYWJsZXMgaXMganVzdCBhIGZldyBsaW5lcyBvZiBhZGRpdGlvbmFsIGNvZGUgcmV1c2luZyB0aGUgcHNldWRvLW91dGNvbWUgZnJvbSB0aGUgYXZlcmFnZSBlZmZlY3QgZXN0aW1hdGlvbg0KIA0KIC0gV2UgY2FuIG1vZGVsIGVmZmVjdCBzaXplcyBhcyB3ZSBhcmUgdXNlZCB0byBtb2RlbGluZyBvdXRjb21lIGxldmVscyBieSB1c2luZyB0aGUgcmlnaHQgcHNldWRvLW91dGNvbWUNCiANCjxicj4NCjxicj4NCiANCiANCiMjIFN1Z2dlc3Rpb25zIHRvIHBsYXkgd2l0aCB0aGUgdG95IG1vZGVsDQoNClNvbWUgc3VnZ2VzdGlvbnM6DQogDQotIEluY3JlYXNlL2RlY3JlYXNlIHRoZSBudW1iZXIgb2Ygb2JzZXJ2YXRpb25zDQoNCi0gQ3JlYXRlIGEgbm9uLWxpbmVhciBDQVRFIGluIHRoZSBmaXJzdCBwYXJ0DQoNCi0gQ2hhbmdlIHRoZSB0cmVhdG1lbnQgc2hhcmVzDQoNCiA=