Setting and plan for today

Managing expectations

  1. Causal ML methods do NOT provide more credible identification BUT more flexible estimation. Only because an estimation method is called “causal forest” does not make its output causal.
  2. We will focus on the unconfoundedness setting with binary treatments but extensions to multiple/continuous treatments and other research designs are available and build on the very same principles.
  3. We will not discuss why these methods work but focus on the “how”. However, I will give you pointers to understand the “why” as we proceed.

Controlling for confounders

Today we will focus on the classic exogeneity/selection-on-observables/unconfoundedness/no omitted variable bias/… setting where we are interested in the causal effect

  • of treatment \(D\)

  • on outcome \(Y\)

  • while controlling for all confounding variables \(X\)

I assume that you are all familiar with this setting. Usually OLS is introduced in such a setting. Historically new estimation methods are also introduce in this setting before extending the ideas to other research design like instrumental variables or difference-in-differences. Therefore it feels natural to introduce causal ML estimators in the “easy” unconfoundedness setting first.

Estimator journey for today

Once we convinced ourselves (and hopefully others) that unconfoundedness is justified in our setting, the big question is how to estimate the causal effect. These are the estimators that we will consider and hand-code today:

  1. OLS (the classic):
    • using lm_robust()

      • manually replicated using the Frisch-Waugh representation

      • replicated within DoubleML package

  2. Partially linear regression
    • Parametric version hand-coded with OLS and Logit

      • replicated using the DoubleML package
    • Double ML version with random forest via DoubleML package

      • replicated by hand
  3. Augmented inverse probability weighting
    • Parametric version hand-coded with OLS and Logit

      • replicated using the DoubleML package
    • Double ML version with random forest via DoubleML package

      • replicated by hand
  4. Causal Forest
    • Implemented using grf package

      • replicated by hand

401(k) data set

We use the data of the hdm package. The data was used in Chernozhukov and Hansen (2004). Their paper investigates the effect of participation in the employer-sponsored 401(k) retirement savings plan (our \(D\)) on net assets (our \(Y\)) while controlling for ten confounders (our \(X\)).

We use the data because it is publicly available and the programs run fast enough for our purposes. However, we do not really care about the application today (call ?pension to see the variable description).

library(ggplot2)
library(DoubleML)
library(mlr3)
library(grf)
library(hdm)
library(estimatr)

set.seed(1234) # for replicability

# Load data
data(pension)
# Outcome
Y = pension$net_tfa
# Treatment
D = pension$p401
# Create main effects matrix
X = model.matrix(~ 0 + age + db + educ + fsize + hown + inc + male + marr + pira + twoearn, data = pension)



Three ways to implement OLS

Let’s start with canonical OLS to estimate a linear model:

\[ Y = \tau D + X'\beta + \varepsilon \]

Standard implementation

Unfortunately the base R lm() function provides no robust standard errors, so I opt for the lm_robust() function of the estimatr package:

# OLS standard
ols_lm = lm_robust(Y ~ D + X)
summary(ols_lm)

Call:
lm_robust(formula = Y ~ D + X)

Standard error type:  HC2 

Coefficients:
              Estimate Std. Error t value  Pr(>|t|)   CI Lower   CI Upper   DF
(Intercept) -32701.099  4765.8379 -6.8616 7.219e-12 -4.204e+04 -23359.086 9903
D            11590.383  1812.4761  6.3948 1.680e-10  8.038e+03  15143.205 9903
Xage           630.113    55.6750 11.3177 1.628e-29  5.210e+02    739.247 9903
Xdb          -4879.000  1300.4171 -3.7519 1.765e-04 -7.428e+03  -2329.918 9903
Xeduc         -626.509   343.6506 -1.8231 6.832e-02 -1.300e+03     47.116 9903
Xfsize       -1056.891   406.3236 -2.6011 9.306e-03 -1.853e+03   -260.414 9903
Xhown          859.466   973.8877  0.8825 3.775e-01 -1.050e+03   2768.485 9903
Xinc             0.916     0.1119  8.1880 2.984e-16  6.967e-01      1.135 9903
Xmale         -985.766  1476.6786 -0.6676 5.044e-01 -3.880e+03   1908.825 9903
Xmarr          730.671  1681.1182  0.4346 6.638e-01 -2.565e+03   4026.005 9903
Xpira        28920.231  1799.5039 16.0712 2.153e-57  2.539e+04  32447.625 9903
Xtwoearn    -19404.112  2557.3323 -7.5876 3.551e-14 -2.442e+04 -14391.220 9903

Multiple R-squared:  0.2352 ,   Adjusted R-squared:  0.2344 
F-statistic: 101.3 on 11 and 9903 DF,  p-value: < 2.2e-16

This provides the familiar output including the target parameter and all the distracting coefficients that we should not interpret causally.

Hand-coded based on partialling out

The Frisch-Waugh Theorem tells us that a numerically equivalent procedure is to

  1. Run regression \(Y = X \beta_Y+ U_Y\), get fitted values \(\hat{Y}\) to estimate outcome residuals \(\hat{U}_Y = Y - \hat{Y}\)

  2. Run regression \(D = X \beta_D+ U_D\), get fitted values \(\hat{D}\) to estimate treatment residuals \(\hat{U}_D = D - \hat{D}\)

  3. Run a final residual-on-residual regression \(\hat{U}_Y = \tau \hat{U}_D + \epsilon\) even without a constant to obtain the target parameter

Let’s see this in code:

# OLS Frisch Waugh
Dhat_ols = lm(D ~ X)$fitted.values
Yhat_ols = lm(Y ~ X)$fitted.values
Dres_ols = D - Dhat_ols
Yres_ols = Y - Yhat_ols
ols_hand = lm_robust(Yres_ols ~ 0 + Dres_ols)
summary(ols_hand)

Call:
lm_robust(formula = Yres_ols ~ 0 + Dres_ols)

Standard error type:  HC2 

Coefficients:
         Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
Dres_ols    11590       1809   6.407 1.548e-10     8045    15136 9914

Multiple R-squared:  0.00744 ,  Adjusted R-squared:  0.00734 
F-statistic: 41.05 on 1 and 9914 DF,  p-value: 1.548e-10

We indeed obtain the identical point estimate as above and only a marginally different standard errors.

Crucial take away: We can think about OLS as running a residual-on-residual regression.

OLS is a special case of Double ML

For reasons that will be obvious later, we can run plain OLS also as special case of Double Machine Learning. To this end, we leverage the DoubleML package that requires a bit higher setup costs. However, I code everything for you in this notebook and you can take a deep dive with a lot of explanations and examples in the DoubleML documentation.

# Prepare the data
data_dml = dml_data = double_ml_data_from_matrix(X=X, y=Y, d=D)

# Specify the model using OLS as the learner for treatment and outcome
lrn_ols = lrn("regr.lm")
ols_dml = DoubleMLPLR$new(data_dml, 
                          ml_l = lrn_ols, 
                          ml_m = lrn_ols, 
                          n_folds = 1, 
                          apply_cross_fitting = FALSE)
ols_dml$fit()
INFO  [12:35:35.857] [mlr3] Applying learner 'regr.lm' on task 'nuis_l' (iter 1/1)
INFO  [12:35:36.080] [mlr3] Applying learner 'regr.lm' on task 'nuis_m' (iter 1/1)
print(ols_dml)
================= DoubleMLPLR Object ==================


------------------ Data summary      ------------------
Outcome variable: y
Treatment variable(s): d
Covariates: X1, X2, X3, X4, X5, X6, X7, X8, X9, X10
Instrument(s): 
No. Observations: 9915

------------------ Score & algorithm ------------------
Score function: partialling out
DML algorithm: dml1

------------------ Machine learner   ------------------
ml_l: regr.lm
ml_m: regr.lm

------------------ Resampling        ------------------
No. folds: 1
No. repeated sample splits: 1
Apply cross-fitting: FALSE

------------------ Fit summary       ------------------
 Estimates and significance testing of the effect of target variables
  Estimate. Std. Error t value Pr(>|t|)    
d     11590       1809   6.408 1.47e-10 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

We obtain exactly the same results as above. You do not believe me? Let’s formally test whether the point estimates of the implementations are identical:

cat("lm_robust() and DoubleML identical?", all.equal(as.numeric(ols_lm$coefficients[2]),as.numeric(ols_dml$coef)))
lm_robust() and DoubleML identical? TRUE

Not so serious take away: You are already using Double ML since the first time you ran OLS.



Double machine learning

Double ML is a very general framework. We introduce it using the canonical leading examples partially linear regression and augmented inverse probability weighting.

Partially linear regression

We introduce the partially linear outcome model \[ Y = \tau D + g(X) + \varepsilon \]

where the controls do not enter in a linear fashion but as a nonparametric function \(g(X)\) because why should the world be linear? The remaining restriction is that the causal effect is assumed to be homogeneous and not allowed to vary with \(X\).

This model underlies the partially linear regression (PLR) of Robinson (1988) and Chernozhukov et al. (2018) that is implemented using the following steps:

  1. Predict the outcome \(\hat{Y} = \hat{E}[Y|X]\) with a suitable method and obtain outcome residuals \(\hat{U}_Y = Y - \hat{Y}\)

  2. Predict the treatment \(\hat{D} = \hat{E}[D|X]\) with a suitable method and obtain treatment residuals \(\hat{U}_D = D - \hat{D}\)

  3. Run a final residual-on-residual regression \(\hat{U}_Y = \tau \hat{U}_D + \epsilon\) even without a constant to obtain the target parameter

We call the outcome and treatment predictions the “nuisance parameters” because we are not interested in them per se but they are needed to get our hands on the “target parameter” \(\tau\).

We now immediately see that OLS is just a special case of partially linear regression where outcome and treatment are predicted using OLS. This explains why we can run plain OLS in the DoubleML infrastructure.

Parametric estimation of PLR by hand

In our application, we have a binary treatment. We can therefore use, e.g. standard logistic regression to form the predictions and the residuals of the treatment.

# Parametric PLR hand-coded
logit_model = glm(D ~ X, family = binomial())
Dhat_logit = predict(logit_model, type = "response")
Dres_logit = D - Dhat_logit

We then run our first “official” partially linear regression where we recycle the OLS outcome residuals from above and use the newly obtained logit treatment residuals

plr_logit_hand = lm_robust(Yres_ols ~ 0 + Dres_logit)
summary(plr_logit_hand)

Call:
lm_robust(formula = Yres_ols ~ 0 + Dres_logit)

Standard error type:  HC2 

Coefficients:
           Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
Dres_logit    11289       1780   6.342 2.362e-10     7800    14778 9914

Multiple R-squared:  0.007096 , Adjusted R-squared:  0.006996 
F-statistic: 40.23 on 1 and 9914 DF,  p-value: 2.362e-10

We will compare the magnitudes of all estimators at the end below and for now focus on the implementation.

Parametric estimation of PLR by DoubleML

Double Machine Learning is doing exactly the same thing if we tell it to use OLS for outcome prediction and logit for treatment prediction:

# Parametric PLR DoubleML
lrn_logit = lrn("classif.log_reg", predict_type = "prob")
plr_logit_dml = DoubleMLPLR$new(data_dml, ml_l = lrn_ols, ml_m = lrn_logit, n_folds = 1, apply_cross_fitting = FALSE)
plr_logit_dml$fit()
INFO  [12:35:36.372] [mlr3] Applying learner 'regr.lm' on task 'nuis_l' (iter 1/1)
INFO  [12:35:36.475] [mlr3] Applying learner 'classif.log_reg' on task 'nuis_m' (iter 1/1)
print(plr_logit_dml)
================= DoubleMLPLR Object ==================


------------------ Data summary      ------------------
Outcome variable: y
Treatment variable(s): d
Covariates: X1, X2, X3, X4, X5, X6, X7, X8, X9, X10
Instrument(s): 
No. Observations: 9915

------------------ Score & algorithm ------------------
Score function: partialling out
DML algorithm: dml1

------------------ Machine learner   ------------------
ml_l: regr.lm
ml_m: classif.log_reg

------------------ Resampling        ------------------
No. folds: 1
No. repeated sample splits: 1
Apply cross-fitting: FALSE

------------------ Fit summary       ------------------
 Estimates and significance testing of the effect of target variables
  Estimate. Std. Error t value Pr(>|t|)    
d     11289       1780   6.343 2.25e-10 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Again let’s check whether the point estimates are actually identical:

all.equal(as.numeric(plr_logit_hand$coefficients),as.numeric(plr_logit_dml$coef))
[1] TRUE

PLR with ML nuisance parameters

Of course the cool recent development is that the Double ML framework shows that we can apply machine learning methods to form the predictions that are used in the residuals for the residual-on-residual regression. This helps us to avoid the (at least in my opinion) most annoying part of estimating effects, the model specification. Instead, we outsource this task to powerful ML methods.

For example, we can use random forests to form the predictions. I like random forests because they are build to find interactions and nonlinearities in a data-driven manner and do not require to commit to a dictionary like for Lasso. However, the recipe is generic and you can decide which prediction algorithm is most suitable for your application.

So let’s see how we can implement PLR with random forest (should run within one minute):

# PLR using Random Forest
lrn_ranger = lrn("regr.ranger")
lrn_ranger_prob = lrn("classif.ranger")    
plr_ranger_dml = DoubleMLPLR$new(dml_data, ml_l=lrn_ranger, ml_m=lrn_ranger)
plr_ranger_dml$fit(store_predictions=TRUE)
INFO  [12:35:36.833] [mlr3] Applying learner 'regr.ranger' on task 'nuis_l' (iter 1/5)
INFO  [12:35:39.334] [mlr3] Applying learner 'regr.ranger' on task 'nuis_l' (iter 2/5)
INFO  [12:35:42.397] [mlr3] Applying learner 'regr.ranger' on task 'nuis_l' (iter 3/5)
INFO  [12:35:44.844] [mlr3] Applying learner 'regr.ranger' on task 'nuis_l' (iter 4/5)
INFO  [12:35:47.312] [mlr3] Applying learner 'regr.ranger' on task 'nuis_l' (iter 5/5)
INFO  [12:35:49.860] [mlr3] Applying learner 'regr.ranger' on task 'nuis_m' (iter 1/5)
INFO  [12:35:51.796] [mlr3] Applying learner 'regr.ranger' on task 'nuis_m' (iter 2/5)
INFO  [12:35:53.733] [mlr3] Applying learner 'regr.ranger' on task 'nuis_m' (iter 3/5)
INFO  [12:35:55.662] [mlr3] Applying learner 'regr.ranger' on task 'nuis_m' (iter 4/5)
INFO  [12:35:57.563] [mlr3] Applying learner 'regr.ranger' on task 'nuis_m' (iter 5/5)
print(plr_ranger_dml)
================= DoubleMLPLR Object ==================


------------------ Data summary      ------------------
Outcome variable: y
Treatment variable(s): d
Covariates: X1, X2, X3, X4, X5, X6, X7, X8, X9, X10
Instrument(s): 
No. Observations: 9915

------------------ Score & algorithm ------------------
Score function: partialling out
DML algorithm: dml2

------------------ Machine learner   ------------------
ml_l: regr.ranger
ml_m: regr.ranger

------------------ Resampling        ------------------
No. folds: 5
No. repeated sample splits: 1
Apply cross-fitting: TRUE

------------------ Fit summary       ------------------
 Estimates and significance testing of the effect of target variables
  Estimate. Std. Error t value Pr(>|t|)    
d     13669       1450   9.427   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Manually replicate the results

Let’s replicate the DoubleML output manually to convince you that it is really as easy as I claim. We told the DoubleMLPLR() function to store the nuisance parameters by setting store_predictions=TRUE above.

This means we can extract the outcome and treatment predictions

Dhat_ranger = plr_ranger_dml$predictions$ml_m
Yhat_ranger = plr_ranger_dml$predictions$ml_l

and use them in a residual-on-residual regression

Dres_ranger = D - Dhat_ranger
Yres_ranger = Y - Yhat_ranger
plr_ranger_hand = lm_robust(Yres_ranger ~ 0 + Dres_ranger)
summary(plr_ranger_hand)

Call:
lm_robust(formula = Yres_ranger ~ 0 + Dres_ranger)

Standard error type:  HC2 

Coefficients:
            Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
Dres_ranger    13669       1450   9.426 5.217e-21    10826    16511 9914

Multiple R-squared:  0.01099 ,  Adjusted R-squared:  0.01089 
F-statistic: 88.85 on 1 and 9914 DF,  p-value: < 2.2e-16

Again the point estimates from package and hand-coded are identical:

all.equal(as.numeric(plr_ranger_dml$coef),as.numeric(plr_ranger_hand$coefficients))
[1] TRUE

Augmented inverse probability weighting

Double ML with PLR assumed a homogeneous treatment effect. This might not be innocent and can lead to estimates being representative for counterintuitive populations if effects are actually heterogeneous (see e.g. Słoczyński (2022)).

Now we relax this assumptions and consider estimators that operate under the flexible/interactive outcome model \[ Y = g(D,X) + \varepsilon \]

Under this model, there is not the one \(\tau\) capturing the effect for everybody but the effect is allowed to depend on \(X\)s. Now we can articulate different target parameters but we focus on the canonical Average Treatment Effect (ATE) \(\tau_{ATE} = E[Y(1) - Y(0)]\).

There are three common strategies to estimate the ATE:

  1. Regression adjustment
  2. Inverse probability weighting (IPW)
  3. Augmented IPW

and only the latter allows the integration of ML via the Double ML framework.

Parametric versions

Before moving too fast, let us first consider all three options in a standard parametric setting (we will focus on point estimates and ignore standard errors for the sake of simplicity).

Regression adjustment

Regression adjustment (RA) proceeds in the following way:

  1. Build an outcome prediction model only using control observations and use the model to form predicted outcomes under control \(\hat{Y}_0 = \hat{E}[Y|D=0,X]\) for all observations.
  2. Build an outcome prediction model only using treated observations and use the model to form predicted outcomes under treatment \(\hat{Y}_1 = \hat{E}[Y|D=1,X]\) for all observations.
  3. Take the mean of the differences in the predictions \(\hat{\tau}^{ra} = \frac{1}{n} \sum_i (\hat{Y}_1 - \hat{Y}_0)\)

To get started let’s use OLS for the prediction:

# Regression adjustment / outcome imputation
ols0 = lm(Y ~ X, subset = (D == 0))
Y0hat_ols = predict(ols0, newdata = as.data.frame(X))

ols1 = lm(Y ~ X, subset = (D == 1))
Y1hat_ols = predict(ols1, newdata = as.data.frame(X))

cat("RA with OLS:",
    mean(Y1hat_ols - Y0hat_ols))
RA with OLS: 9169.948

IPW

IPW follows this recipe

  1. Predict \(\hat{D}\) with a suitable method (can recycle predictions from PLR).
  2. Define inverse probability weights for treated \(w_1^{ipw} = D / \hat{D}\) and controls \((1-D) / (1-\hat{D})\)
  3. Take the mean of the pseudo-outcome \(\tilde{Y}^{ipw} = (w_1^{ipw} - w_0^{ipw}) Y \Rightarrow \hat{\tau}^{ipw} = \frac{1}{n} \sum_i \tilde{Y}^{ipw}\)
# IPW
ipw_weight1 = D / Dhat_logit
ipw_weight0 = (1 - D) / (1 - Dhat_logit)
Ytilde_ipw = (ipw_weight1 - ipw_weight0) * Y
cat("IPW with Logit:",
    mean(Ytilde_ipw))
IPW with Logit: 7067.326

AIPW

Augmented inverse probability weighting combines RA and IPW and creates the following pseudo-outcome

\[\begin{align*} \tilde{Y}^{aipw} & = \underbrace{\hat{Y}_1 - \hat{Y}_0}_{\text{RA}} + \underbrace{(w_1^{ipw} - w_0^{ipw}) Y}_{\text{IPW}} - \underbrace{(w_1^{ipw} \hat{Y}_1 - w_0^{ipw} \hat{Y}_0)}_{\text{debiasing term}} \\ &= \hat{Y}_1 - \hat{Y}_0 + \underbrace{w_1^{ipw} (Y - \hat{Y}_1) - w_0^{ipw} (Y - \hat{Y}_0)}_{\text{IPW weighted residuals}} \end{align*}\]

and takes its mean \(\hat{\tau}^{aipw} = \frac{1}{n} \sum_i \tilde{Y}^{aipw}\). One beautiful feature of AIPW is that the standard error of this mean is valid without adjusting for the estimation of the nuisance parameters.

This motivates the following recipe for AIPW:

  1. Predict the nuisance parameters \(\hat{Y}_0\), \(\hat{Y}_1\) and \(\hat{D}\)
  2. Create pseudo outcome \(\tilde{Y}^{aipw}\)
  3. Run OLS with only a constant \(\tilde{Y}^{aipw} = \tau + \varepsilon\) to get point estimate and standard error.

We first recycle the OLS outcome predictions from RA and the logit treatment predictions from IPW:

# AIPW
Ytilde_parametric = Y1hat_ols - Y0hat_ols + 
                      ipw_weight1 * (Y - Y1hat_ols) - 
                      ipw_weight0 * (Y - Y0hat_ols)
aipw_parametric_hand = lm(Ytilde_parametric ~ 1)
summary(aipw_parametric_hand)

Call:
lm(formula = Ytilde_parametric ~ 1)

Residuals:
      Min        1Q    Median        3Q       Max 
-19861481    -24574     -1241     26577   3653559 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)   
(Intercept)     6976       2627   2.656  0.00792 **
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 261500 on 9914 degrees of freedom

Parametric AIPW with Double ML

We have handcoded AIPW with parametric estimators for the nuisance parameters above. Now we establish that we could have done just the same within the DoubleML package by using the DoubleMLIRM() function (IRM stands for interactive regression model and is another label for AIPW, sometimes AIPW is also called the doubly robust estimators, all mean the same thing, so don’t get confused).

# With DoubleML parametric
aipw_parametric_dml = DoubleMLIRM$new(dml_data, ml_g = lrn_ols, ml_m = lrn_logit, 
                                      n_folds = 1, apply_cross_fitting = FALSE)
aipw_parametric_dml$fit()
INFO  [12:35:59.888] [mlr3] Applying learner 'classif.log_reg' on task 'nuis_m' (iter 1/1)
INFO  [12:36:00.001] [mlr3] Applying learner 'regr.lm' on task 'nuis_g0' (iter 1/1)
INFO  [12:36:00.062] [mlr3] Applying learner 'regr.lm' on task 'nuis_g1' (iter 1/1)
print(aipw_parametric_dml)
================= DoubleMLIRM Object ==================


------------------ Data summary      ------------------
Outcome variable: y
Treatment variable(s): d
Covariates: X1, X2, X3, X4, X5, X6, X7, X8, X9, X10
Instrument(s): 
No. Observations: 9915

------------------ Score & algorithm ------------------
Score function: ATE
DML algorithm: dml1

------------------ Machine learner   ------------------
ml_g: regr.lm
ml_m: classif.log_reg

------------------ Resampling        ------------------
No. folds: 1
No. repeated sample splits: 1
Apply cross-fitting: FALSE

------------------ Fit summary       ------------------
 Estimates and significance testing of the effect of target variables
  Estimate. Std. Error t value Pr(>|t|)   
d      6976       2626   2.656   0.0079 **
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Again the handcoded and the DoubleML point estimates are identical:

all.equal(as.numeric(aipw_parametric_hand$coefficients),as.numeric(aipw_parametric_dml$coef))
[1] TRUE

AIPW with Double ML

Double ML for ATE estimation simply uses ML methods to estimate the nuisance parameters instead of OLS and logit. Like for PLR, we use random forest today:

# DoubleML random forest
aipw_ranger_dml = DoubleMLIRM$new(dml_data, ml_g = lrn_ranger, ml_m = lrn_ranger_prob)
aipw_ranger_dml$fit(store_predictions=TRUE)
INFO  [12:36:00.188] [mlr3] Applying learner 'classif.ranger' on task 'nuis_m' (iter 1/5)
INFO  [12:36:02.314] [mlr3] Applying learner 'classif.ranger' on task 'nuis_m' (iter 2/5)
INFO  [12:36:04.328] [mlr3] Applying learner 'classif.ranger' on task 'nuis_m' (iter 3/5)
INFO  [12:36:06.923] [mlr3] Applying learner 'classif.ranger' on task 'nuis_m' (iter 4/5)
INFO  [12:36:09.093] [mlr3] Applying learner 'classif.ranger' on task 'nuis_m' (iter 5/5)
INFO  [12:36:15.160] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g0' (iter 1/5)
INFO  [12:36:16.974] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g0' (iter 2/5)
INFO  [12:36:18.750] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g0' (iter 3/5)
INFO  [12:36:20.536] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g0' (iter 4/5)
INFO  [12:36:22.301] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g0' (iter 5/5)
INFO  [12:36:24.223] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g1' (iter 1/5)
INFO  [12:36:24.863] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g1' (iter 2/5)
INFO  [12:36:25.532] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g1' (iter 3/5)
INFO  [12:36:26.175] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g1' (iter 4/5)
INFO  [12:36:26.823] [mlr3] Applying learner 'regr.ranger' on task 'nuis_g1' (iter 5/5)
print(aipw_ranger_dml)
================= DoubleMLIRM Object ==================


------------------ Data summary      ------------------
Outcome variable: y
Treatment variable(s): d
Covariates: X1, X2, X3, X4, X5, X6, X7, X8, X9, X10
Instrument(s): 
No. Observations: 9915

------------------ Score & algorithm ------------------
Score function: ATE
DML algorithm: dml2

------------------ Machine learner   ------------------
ml_g: regr.ranger
ml_m: classif.ranger

------------------ Resampling        ------------------
No. folds: 5
No. repeated sample splits: 1
Apply cross-fitting: TRUE

------------------ Fit summary       ------------------
 Estimates and significance testing of the effect of target variables
  Estimate. Std. Error t value Pr(>|t|)    
d     10900       1159   9.408   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

The rest is identical as we can see by exactly replicating the DoubleML output if we extract the nuisance parameters from the DoubleML object

# Extract nuisance parameters
Dhat_ranger = aipw_ranger_dml$predictions$ml_m
Y0hat_ranger = aipw_ranger_dml$predictions$ml_g0
Y1hat_ranger = aipw_ranger_dml$predictions$ml_g1

# Create pseudo outcome
Ytilde_ranger = Y1hat_ranger - Y0hat_ranger + 
                (D / Dhat_ranger) * (Y - Y1hat_ranger) - 
                (1 - D) / (1 - Dhat_ranger) * (Y - Y0hat_ranger)

# Run OLS with a constant
aipw_ranger_hand = lm(Ytilde_ranger ~ 1)
summary(aipw_ranger_hand)

Call:
lm(formula = Ytilde_ranger ~ 1)

Residuals:
     Min       1Q   Median       3Q      Max 
-2373530   -13163    -1835    13965  2920441 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)    10900       1159   9.407   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 115400 on 9914 degrees of freedom

The canonical check shows identical point estimates:

all.equal(as.numeric(aipw_ranger_hand$coefficients),as.numeric(aipw_ranger_dml$coef))
[1] TRUE

Consolidation

Double ML boils down to very simple recipes:

  1. Predict the relevant nuisance parameters
  2. Use them to define new variables like outcome/treatmetresiduals for PLR or the pseudo-outcome for AIPW
  3. Run a very simple OLS regression to obtain point estimate and (!) valid standard errors even if ML is applied in the first step

We implemented Double ML manually with standard parametric estimators and showed that calling DoubleML functions with the same parametric methods produces identical results.

We then used the DoubleML functions with random forests and showed that we could replicate the outputs manually.

This required few lines of code and hopefully convinced you that everything boils down to relatively simple recipes.

Let’s collect what we achieved.

estimator_names = c("OLS","DoubleML PLR Parametric","DoubleML PLR Forest",
                   "DoubleML AIPW parametric","DoubleML AIPW Forest")
point_estimates = c(ols_dml$coef,plr_logit_dml$coef,plr_ranger_dml$coef,
                   aipw_parametric_dml$coef,aipw_ranger_dml$coef)
standard_errors = c(ols_dml$se,plr_logit_dml$se,plr_ranger_dml$se,
                   aipw_parametric_dml$se,aipw_ranger_dml$se)

# Graph powered by ChatGPT
# Create a data frame
data <- data.frame(
  Estimator = factor(estimator_names, levels = rev(estimator_names)), # Reverse the order for correct plotting
  Estimate = point_estimates,
  SE = standard_errors
)

# Calculate confidence intervals
data$Lower <- data$Estimate - 1.96 * data$SE
data$Upper <- data$Estimate + 1.96 * data$SE

# Plot
ggplot(data, aes(x = Estimator, y = Estimate, color = Estimator)) +
  geom_errorbar(aes(ymin = Lower, ymax = Upper), width = 0.2, color = "gray40", linewidth = 0.5) + # Custom error bars
  geom_point(size = 4) + # Larger points
  geom_hline(yintercept = 0, linetype = "dashed", color = "red", linewidth = 0.5) + # More prominent horizontal line at zero
  coord_flip() + # Flip coordinates to put estimators on the y-axis
  labs(x = "Estimator", y = "Estimate", title = "Point Estimates with 95% Confidence Intervals") +
  theme_light(base_size = 14) + # Lighter theme with larger base text size
  theme(
    plot.title = element_text(size = 16, face = "bold", color = "darkblue"), # Custom plot title
    axis.title.x = element_text(size = 14, face = "bold"), # Custom x-axis title
    axis.title.y = element_text(size = 14, face = "bold"), # Custom y-axis title
    panel.grid.major = element_line(color = "gray80"), # Custom major grid lines
    panel.grid.minor = element_blank(), # Remove minor grid lines
    legend.position = "none" # Remove legend as color is mapped to Estimator but not needed
  )

The differences in the point estimates are not striking. However, AIPW with random forest is by far the most precise estimator. This makes sense because AIPW is the efficient estimator in the likely case that effects are heterogeneous and random forest is expected to approximate the nuisance parameters much better than simple parametric models.



Causal Forest

So far, we were interested in estimating an assumed homogeneous treatment effect \(\tau\) or the ATE. However, we might be interested in going beyond such summary measures and want to estimate conditional average treatment effects (CATEs)

\[ \tau(x) = E[Y(1) - Y(0)|X = x] \]

Causal forest as introduced by Athey et al. (2019) is arguably the most prominent estimator to estimate such CATEs. They are implemented in the grf package and can be considered as just running a weighted residual-on-residual regression (this might not be obvious in the original paper but for an accessible explanation see Athey and Wager (2019)).

It might feel counterintuitive why a method for homogeneous effects can suddenly be used to estimate heterogeneous effects. Unfortunately, we have not the time to discuss why it actually makes sense. Instead, I show you that this is indeed what is going on.

First, run the default implementation of the causal_forest() and plot the distribution of estimated CATEs:

cf = causal_forest(X,Y,D)
cate = predict(cf)$predictions
hist(cate)

We see that there is quite some heterogeneity but we will not take a closer look today. However, there are excellent tutorials what to do with these kinds of results on the grf homepage.

Today we are interested in understanding how these numbers come about. I told you that grf is actually running a weighted RoRR. To see this first extract the outcome and treatment predictions that are stored in the causal_forest() object and use them to produce residuals:

Dhat_cf = cf$W.hat 
Dres_cf = D - Dhat_cf 
Yhat_cf = cf$Y.hat 
Yres_cf = Y - Yhat_cf

They could be used to form the by now familiar PLR estimate:

plr_cf_hand = lm_robust(Yres_cf ~ 0 + Dres_cf) 
summary(plr_cf_hand)

Call:
lm_robust(formula = Yres_cf ~ 0 + Dres_cf)

Standard error type:  HC2 

Coefficients:
        Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
Dres_cf    13783       1505   9.157 6.399e-20    10832    16733 9914

Multiple R-squared:  0.01111 ,  Adjusted R-squared:  0.01101 
F-statistic: 83.85 on 1 and 9914 DF,  p-value: < 2.2e-16

However, this is not what causal forests actually do. Causal forests produce an individualized estimate for every individual. To this end, they run RoRR but give higher weights to observations that are similar to the individual for which the CATE is estimated.

This weights - usually called \(\alpha\) - can be extracted via the get_forest_weights() function:

alpha = get_forest_weights(cf)

Let’s illustrate this by replicating the CATE for the first observation in the sample as an example, which is

cate1 = cate[1] 
cate1
[1] 5716.159

We try to replicate this number in a weighted RoRR:

first_try = lm_robust(Yres_cf ~ 0 + Dres_cf, weight = alpha[1,]) 
summary(first_try)

Call:
lm_robust(formula = Yres_cf ~ 0 + Dres_cf, weights = alpha[1, 
    ])

Weighted, Standard error type:  HC2 

Coefficients:
        Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
Dres_cf     5715      986.1   5.795 7.041e-09     3782     7648 9914

Multiple R-squared:  0.04641 ,  Adjusted R-squared:  0.04631 
F-statistic: 33.58 on 1 and 9914 DF,  p-value: 7.041e-09

and are slightly off :-(

This is because causal_forest() uses a constant in the RORR. Once we figured that out, we can perfectly replicate the output:

cate1_hand = lm_robust(Yres_cf ~ Dres_cf, weight = alpha[1,])$coefficients[2]
cate1_hand 
 Dres_cf 
5716.159 
cat("\nEqual to grf output?",all.equal(as.numeric(cate1),as.numeric(cate1_hand)))

Equal to grf output? TRUE

You could repeat this for every observation by using the appropriate row of the alpha weight matrix.

Some remarks:

  • The results with and without a constant are usually very similar because the constant is zero asymptotically.

  • Unlike with Double ML, we can NOT use the standard errors of this output. Inference for causal forest CATEs is much more complicated and beyond the scope of this notebook.

  • My students in Tübingen programmed the Causal Forest Fun Shiny App to illustrate the different weighting in toy examples (have a look).



Further free material

You see that things can be quite easy. If you want to take a deep dive, I collect here a variety of resources that can help you to understand the theoretical underpinning beyond the papers linked throughout the notebook above:

LS0tDQp0aXRsZTogIkludHJvZHVjdGlvbiB0byBDYXVzYWwgTUwgZXN0aW1hdG9ycyBpbiBSICINCmF1dGhvcjogIk1pY2hhZWwgS25hdXMiDQpkYXRlOiAiV29ya3Nob3AgZm9yIFVrcmFpbmUgMTEuMDQuMjAyNCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQotLS0NCg0KIyBTZXR0aW5nIGFuZCBwbGFuIGZvciB0b2RheQ0KDQojIyBNYW5hZ2luZyBleHBlY3RhdGlvbnMNCg0KMS4gIENhdXNhbCBNTCBtZXRob2RzIGRvIE5PVCBwcm92aWRlIG1vcmUgY3JlZGlibGUgaWRlbnRpZmljYXRpb24gQlVUIG1vcmUgZmxleGlibGUgZXN0aW1hdGlvbi4gT25seSBiZWNhdXNlIGFuDQogICAgZXN0aW1hdGlvbiBtZXRob2QgaXMgY2FsbGVkICJjYXVzYWwgZm9yZXN0IiBkb2VzIG5vdCBtYWtlIGl0cyBvdXRwdXQgY2F1c2FsLg0KMi4gIFdlIHdpbGwgZm9jdXMgb24gdGhlIHVuY29uZm91bmRlZG5lc3Mgc2V0dGluZyB3aXRoIGJpbmFyeSB0cmVhdG1lbnRzIGJ1dCBleHRlbnNpb25zIHRvIG11bHRpcGxlL2NvbnRpbnVvdXMNCiAgICB0cmVhdG1lbnRzIGFuZCBvdGhlciByZXNlYXJjaCBkZXNpZ25zIGFyZSBhdmFpbGFibGUgYW5kIGJ1aWxkIG9uIHRoZSB2ZXJ5IHNhbWUgcHJpbmNpcGxlcy4NCjMuICBXZSB3aWxsIG5vdCBkaXNjdXNzIHdoeSB0aGVzZSBtZXRob2RzIHdvcmsgYnV0IGZvY3VzIG9uIHRoZSAiaG93Ii4gSG93ZXZlciwgSSB3aWxsIGdpdmUgeW91IHBvaW50ZXJzIHRvIHVuZGVyc3RhbmQgdGhlDQogICAgIndoeSIgYXMgd2UgcHJvY2VlZC4NCg0KIyMgQ29udHJvbGxpbmcgZm9yIGNvbmZvdW5kZXJzDQoNClRvZGF5IHdlIHdpbGwgZm9jdXMgb24gdGhlIGNsYXNzaWMgZXhvZ2VuZWl0eS9zZWxlY3Rpb24tb24tb2JzZXJ2YWJsZXMvdW5jb25mb3VuZGVkbmVzcy9ubyBvbWl0dGVkIHZhcmlhYmxlIGJpYXMvLi4uDQpzZXR0aW5nIHdoZXJlIHdlIGFyZSBpbnRlcmVzdGVkIGluIHRoZSBjYXVzYWwgZWZmZWN0DQoNCi0gICBvZiB0cmVhdG1lbnQgJEQkDQoNCi0gICBvbiBvdXRjb21lICRZJA0KDQotICAgd2hpbGUgY29udHJvbGxpbmcgZm9yIGFsbCBjb25mb3VuZGluZyB2YXJpYWJsZXMgJFgkDQoNCkkgYXNzdW1lIHRoYXQgeW91IGFyZSBhbGwgZmFtaWxpYXIgd2l0aCB0aGlzIHNldHRpbmcuIFVzdWFsbHkgT0xTIGlzIGludHJvZHVjZWQgaW4gc3VjaCBhIHNldHRpbmcuIEhpc3RvcmljYWxseSBuZXcNCmVzdGltYXRpb24gbWV0aG9kcyBhcmUgYWxzbyBpbnRyb2R1Y2UgaW4gdGhpcyBzZXR0aW5nIGJlZm9yZSBleHRlbmRpbmcgdGhlIGlkZWFzIHRvIG90aGVyIHJlc2VhcmNoIGRlc2lnbiBsaWtlDQppbnN0cnVtZW50YWwgdmFyaWFibGVzIG9yIGRpZmZlcmVuY2UtaW4tZGlmZmVyZW5jZXMuIFRoZXJlZm9yZSBpdCBmZWVscyBuYXR1cmFsIHRvIGludHJvZHVjZSBjYXVzYWwgTUwgZXN0aW1hdG9ycyBpbiB0aGUNCiJlYXN5IiB1bmNvbmZvdW5kZWRuZXNzIHNldHRpbmcgZmlyc3QuDQoNCiMjIEVzdGltYXRvciBqb3VybmV5IGZvciB0b2RheQ0KDQpPbmNlIHdlIGNvbnZpbmNlZCBvdXJzZWx2ZXMgKGFuZCBob3BlZnVsbHkgb3RoZXJzKSB0aGF0IHVuY29uZm91bmRlZG5lc3MgaXMganVzdGlmaWVkIGluIG91ciBzZXR0aW5nLCB0aGUgYmlnIHF1ZXN0aW9uDQppcyBob3cgdG8gZXN0aW1hdGUgdGhlIGNhdXNhbCBlZmZlY3QuIFRoZXNlIGFyZSB0aGUgZXN0aW1hdG9ycyB0aGF0IHdlIHdpbGwgY29uc2lkZXIgYW5kIGhhbmQtY29kZSB0b2RheToNCg0KMS4gIE9MUyAodGhlIGNsYXNzaWMpOg0KICAgIC0gICB1c2luZyBgbG1fcm9idXN0KClgDQoNCiAgICAgICAgLSAgIG1hbnVhbGx5IHJlcGxpY2F0ZWQgdXNpbmcgdGhlIEZyaXNjaC1XYXVnaCByZXByZXNlbnRhdGlvbg0KDQogICAgICAgIC0gICByZXBsaWNhdGVkIHdpdGhpbiBgRG91YmxlTUxgIHBhY2thZ2UNCjIuICBQYXJ0aWFsbHkgbGluZWFyIHJlZ3Jlc3Npb24NCiAgICAtICAgUGFyYW1ldHJpYyB2ZXJzaW9uIGhhbmQtY29kZWQgd2l0aCBPTFMgYW5kIExvZ2l0DQoNCiAgICAgICAgLSAgIHJlcGxpY2F0ZWQgdXNpbmcgdGhlIGBEb3VibGVNTGAgcGFja2FnZQ0KDQogICAgLSAgIERvdWJsZSBNTCB2ZXJzaW9uIHdpdGggcmFuZG9tIGZvcmVzdCB2aWEgYERvdWJsZU1MYCBwYWNrYWdlDQoNCiAgICAgICAgLSAgIHJlcGxpY2F0ZWQgYnkgaGFuZA0KMy4gIEF1Z21lbnRlZCBpbnZlcnNlIHByb2JhYmlsaXR5IHdlaWdodGluZw0KICAgIC0gICBQYXJhbWV0cmljIHZlcnNpb24gaGFuZC1jb2RlZCB3aXRoIE9MUyBhbmQgTG9naXQNCg0KICAgICAgICAtICAgcmVwbGljYXRlZCB1c2luZyB0aGUgYERvdWJsZU1MYCBwYWNrYWdlDQoNCiAgICAtICAgRG91YmxlIE1MIHZlcnNpb24gd2l0aCByYW5kb20gZm9yZXN0IHZpYSBgRG91YmxlTUxgIHBhY2thZ2UNCg0KICAgICAgICAtICAgcmVwbGljYXRlZCBieSBoYW5kDQo0LiAgQ2F1c2FsIEZvcmVzdA0KICAgIC0gICBJbXBsZW1lbnRlZCB1c2luZyBgZ3JmYCBwYWNrYWdlDQoNCiAgICAgICAgLSAgIHJlcGxpY2F0ZWQgYnkgaGFuZA0KDQojIyA0MDEoaykgZGF0YSBzZXQNCg0KV2UgdXNlIHRoZSBkYXRhIG9mIHRoZSBgaGRtYCBwYWNrYWdlLiBUaGUgZGF0YSB3YXMgdXNlZCBpbiBbQ2hlcm5vemh1a292IGFuZCBIYW5zZW4NCigyMDA0KV0oaHR0cHM6Ly9kaXJlY3QubWl0LmVkdS9yZXN0L2FydGljbGUvODYvMy83MzUvNTc1ODYvVGhlLUVmZmVjdHMtb2YtNDAxLUstUGFydGljaXBhdGlvbi1vbi10aGUtV2VhbHRoKS4gVGhlaXINCnBhcGVyIGludmVzdGlnYXRlcyB0aGUgZWZmZWN0IG9mIHBhcnRpY2lwYXRpb24gaW4gdGhlIGVtcGxveWVyLXNwb25zb3JlZCA0MDEoaykgcmV0aXJlbWVudCBzYXZpbmdzIHBsYW4gKG91ciAkRCQpIG9uIG5ldA0KYXNzZXRzIChvdXIgJFkkKSB3aGlsZSBjb250cm9sbGluZyBmb3IgdGVuIGNvbmZvdW5kZXJzIChvdXIgJFgkKS4NCg0KV2UgdXNlIHRoZSBkYXRhIGJlY2F1c2UgaXQgaXMgcHVibGljbHkgYXZhaWxhYmxlIGFuZCB0aGUgcHJvZ3JhbXMgcnVuIGZhc3QgZW5vdWdoIGZvciBvdXIgcHVycG9zZXMuIEhvd2V2ZXIsIHdlIGRvIG5vdA0KcmVhbGx5IGNhcmUgYWJvdXQgdGhlIGFwcGxpY2F0aW9uIHRvZGF5IChjYWxsIGA/cGVuc2lvbmAgdG8gc2VlIHRoZSB2YXJpYWJsZSBkZXNjcmlwdGlvbikuDQoNCmBgYHtyLCB3YXJuaW5nPUYsbWVzc2FnZT1GfQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShEb3VibGVNTCkNCmxpYnJhcnkobWxyMykNCmxpYnJhcnkoZ3JmKQ0KbGlicmFyeShoZG0pDQpsaWJyYXJ5KGVzdGltYXRyKQ0KDQpzZXQuc2VlZCgxMjM0KSAjIGZvciByZXBsaWNhYmlsaXR5DQoNCiMgTG9hZCBkYXRhDQpkYXRhKHBlbnNpb24pDQojIE91dGNvbWUNClkgPSBwZW5zaW9uJG5ldF90ZmENCiMgVHJlYXRtZW50DQpEID0gcGVuc2lvbiRwNDAxDQojIENyZWF0ZSBtYWluIGVmZmVjdHMgbWF0cml4DQpYID0gbW9kZWwubWF0cml4KH4gMCArIGFnZSArIGRiICsgZWR1YyArIGZzaXplICsgaG93biArIGluYyArIG1hbGUgKyBtYXJyICsgcGlyYSArIHR3b2Vhcm4sIGRhdGEgPSBwZW5zaW9uKQ0KYGBgDQoNCjxicj4gPGJyPg0KDQojIFRocmVlIHdheXMgdG8gaW1wbGVtZW50IE9MUw0KDQpMZXQncyBzdGFydCB3aXRoIGNhbm9uaWNhbCBPTFMgdG8gZXN0aW1hdGUgYSBsaW5lYXIgbW9kZWw6DQoNCiQkDQpZID0gXHRhdSBEICsgWCdcYmV0YSArIFx2YXJlcHNpbG9uDQokJA0KDQojIyBTdGFuZGFyZCBpbXBsZW1lbnRhdGlvbg0KDQpVbmZvcnR1bmF0ZWx5IHRoZSBiYXNlIFIgYGxtKClgIGZ1bmN0aW9uIHByb3ZpZGVzIG5vIHJvYnVzdCBzdGFuZGFyZCBlcnJvcnMsIHNvIEkgb3B0IGZvciB0aGUgYGxtX3JvYnVzdCgpYCBmdW5jdGlvbiBvZg0KdGhlIGBlc3RpbWF0cmAgcGFja2FnZToNCg0KYGBge3J9DQojIE9MUyBzdGFuZGFyZA0Kb2xzX2xtID0gbG1fcm9idXN0KFkgfiBEICsgWCkNCnN1bW1hcnkob2xzX2xtKQ0KYGBgDQoNClRoaXMgcHJvdmlkZXMgdGhlIGZhbWlsaWFyIG91dHB1dCBpbmNsdWRpbmcgdGhlIHRhcmdldCBwYXJhbWV0ZXIgYW5kIGFsbCB0aGUgZGlzdHJhY3RpbmcgY29lZmZpY2llbnRzIHRoYXQgd2Ugc2hvdWxkIG5vdA0KaW50ZXJwcmV0IGNhdXNhbGx5Lg0KDQojIyBIYW5kLWNvZGVkIGJhc2VkIG9uIHBhcnRpYWxsaW5nIG91dA0KDQpUaGUgW0ZyaXNjaC1XYXVnaCBUaGVvcmVtXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9GcmlzY2glRTIlODAlOTNXYXVnaCVFMiU4MCU5M0xvdmVsbF90aGVvcmVtKSB0ZWxscyB1cyB0aGF0IGENCm51bWVyaWNhbGx5IGVxdWl2YWxlbnQgcHJvY2VkdXJlIGlzIHRvDQoNCjEuICBSdW4gcmVncmVzc2lvbiAkWSA9IFggXGJldGFfWSsgVV9ZJCwgZ2V0IGZpdHRlZCB2YWx1ZXMgJFxoYXR7WX0kIHRvIGVzdGltYXRlIG91dGNvbWUgcmVzaWR1YWxzDQogICAgJFxoYXR7VX1fWSA9IFkgLSBcaGF0e1l9JA0KDQoyLiAgUnVuIHJlZ3Jlc3Npb24gJEQgPSBYIFxiZXRhX0QrIFVfRCQsIGdldCBmaXR0ZWQgdmFsdWVzICRcaGF0e0R9JCB0byBlc3RpbWF0ZSB0cmVhdG1lbnQgcmVzaWR1YWxzDQogICAgJFxoYXR7VX1fRCA9IEQgLSBcaGF0e0R9JA0KDQozLiAgUnVuIGEgZmluYWwgcmVzaWR1YWwtb24tcmVzaWR1YWwgcmVncmVzc2lvbiAkXGhhdHtVfV9ZID0gXHRhdSBcaGF0e1V9X0QgKyBcZXBzaWxvbiQgZXZlbiB3aXRob3V0IGEgY29uc3RhbnQgdG8NCiAgICBvYnRhaW4gdGhlIHRhcmdldCBwYXJhbWV0ZXINCg0KTGV0J3Mgc2VlIHRoaXMgaW4gY29kZToNCg0KYGBge3J9DQojIE9MUyBGcmlzY2ggV2F1Z2gNCkRoYXRfb2xzID0gbG0oRCB+IFgpJGZpdHRlZC52YWx1ZXMNClloYXRfb2xzID0gbG0oWSB+IFgpJGZpdHRlZC52YWx1ZXMNCkRyZXNfb2xzID0gRCAtIERoYXRfb2xzDQpZcmVzX29scyA9IFkgLSBZaGF0X29scw0Kb2xzX2hhbmQgPSBsbV9yb2J1c3QoWXJlc19vbHMgfiAwICsgRHJlc19vbHMpDQpzdW1tYXJ5KG9sc19oYW5kKQ0KYGBgDQoNCldlIGluZGVlZCBvYnRhaW4gdGhlIGlkZW50aWNhbCBwb2ludCBlc3RpbWF0ZSBhcyBhYm92ZSBhbmQgb25seSBhIG1hcmdpbmFsbHkgZGlmZmVyZW50IHN0YW5kYXJkIGVycm9ycy4NCg0KKkNydWNpYWwgdGFrZSBhd2F5OiogV2UgY2FuIHRoaW5rIGFib3V0IE9MUyBhcyBydW5uaW5nIGEgcmVzaWR1YWwtb24tcmVzaWR1YWwgcmVncmVzc2lvbi4NCg0KIyMgT0xTIGlzIGEgc3BlY2lhbCBjYXNlIG9mIERvdWJsZSBNTA0KDQpGb3IgcmVhc29ucyB0aGF0IHdpbGwgYmUgb2J2aW91cyBsYXRlciwgd2UgY2FuIHJ1biBwbGFpbiBPTFMgYWxzbyBhcyBzcGVjaWFsIGNhc2Ugb2YgRG91YmxlIE1hY2hpbmUgTGVhcm5pbmcuIFRvIHRoaXMNCmVuZCwgd2UgbGV2ZXJhZ2UgdGhlIGBEb3VibGVNTGAgcGFja2FnZSB0aGF0IHJlcXVpcmVzIGEgYml0IGhpZ2hlciBzZXR1cCBjb3N0cy4gSG93ZXZlciwgSSBjb2RlIGV2ZXJ5dGhpbmcgZm9yIHlvdSBpbg0KdGhpcyBub3RlYm9vayBhbmQgeW91IGNhbiB0YWtlIGEgZGVlcCBkaXZlIHdpdGggYSBsb3Qgb2YgZXhwbGFuYXRpb25zIGFuZCBleGFtcGxlcyBpbiB0aGUgW0RvdWJsZU1MDQpkb2N1bWVudGF0aW9uXShodHRwczovL2RvY3MuZG91YmxlbWwub3JnL3N0YWJsZS9pbmRleC5odG1sIykuDQoNCmBgYHtyfQ0KIyBQcmVwYXJlIHRoZSBkYXRhDQpkYXRhX2RtbCA9IGRtbF9kYXRhID0gZG91YmxlX21sX2RhdGFfZnJvbV9tYXRyaXgoWD1YLCB5PVksIGQ9RCkNCg0KIyBTcGVjaWZ5IHRoZSBtb2RlbCB1c2luZyBPTFMgYXMgdGhlIGxlYXJuZXIgZm9yIHRyZWF0bWVudCBhbmQgb3V0Y29tZQ0KbHJuX29scyA9IGxybigicmVnci5sbSIpDQpvbHNfZG1sID0gRG91YmxlTUxQTFIkbmV3KGRhdGFfZG1sLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgbWxfbCA9IGxybl9vbHMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBtbF9tID0gbHJuX29scywgDQogICAgICAgICAgICAgICAgICAgICAgICAgIG5fZm9sZHMgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgYXBwbHlfY3Jvc3NfZml0dGluZyA9IEZBTFNFKQ0Kb2xzX2RtbCRmaXQoKQ0KcHJpbnQob2xzX2RtbCkNCmBgYA0KDQpXZSBvYnRhaW4gZXhhY3RseSB0aGUgc2FtZSByZXN1bHRzIGFzIGFib3ZlLiBZb3UgZG8gbm90IGJlbGlldmUgbWU/IExldCdzIGZvcm1hbGx5IHRlc3Qgd2hldGhlciB0aGUgcG9pbnQgZXN0aW1hdGVzIG9mDQp0aGUgaW1wbGVtZW50YXRpb25zIGFyZSBpZGVudGljYWw6DQoNCmBgYHtyfQ0KY2F0KCJsbV9yb2J1c3QoKSBhbmQgRG91YmxlTUwgaWRlbnRpY2FsPyIsIGFsbC5lcXVhbChhcy5udW1lcmljKG9sc19sbSRjb2VmZmljaWVudHNbMl0pLGFzLm51bWVyaWMob2xzX2RtbCRjb2VmKSkpDQpgYGANCg0KKk5vdCBzbyBzZXJpb3VzIHRha2UgYXdheToqIFlvdSBhcmUgYWxyZWFkeSB1c2luZyBEb3VibGUgTUwgc2luY2UgdGhlIGZpcnN0IHRpbWUgeW91IHJhbiBPTFMuDQoNCjxicj4gPGJyPg0KDQojIERvdWJsZSBtYWNoaW5lIGxlYXJuaW5nDQoNCkRvdWJsZSBNTCBpcyBhIHZlcnkgZ2VuZXJhbCBmcmFtZXdvcmsuIFdlIGludHJvZHVjZSBpdCB1c2luZyB0aGUgY2Fub25pY2FsIGxlYWRpbmcgZXhhbXBsZXMgcGFydGlhbGx5IGxpbmVhciByZWdyZXNzaW9uDQphbmQgYXVnbWVudGVkIGludmVyc2UgcHJvYmFiaWxpdHkgd2VpZ2h0aW5nLg0KDQojIyBQYXJ0aWFsbHkgbGluZWFyIHJlZ3Jlc3Npb24NCg0KV2UgaW50cm9kdWNlIHRoZSBwYXJ0aWFsbHkgbGluZWFyIG91dGNvbWUgbW9kZWwgDQokJA0KWSA9IFx0YXUgRCArIGcoWCkgKyBcdmFyZXBzaWxvbg0KJCQNCg0Kd2hlcmUgdGhlIGNvbnRyb2xzIGRvIG5vdCBlbnRlciBpbiBhIGxpbmVhciBmYXNoaW9uIGJ1dCBhcyBhIG5vbnBhcmFtZXRyaWMgZnVuY3Rpb24gJGcoWCkkIGJlY2F1c2Ugd2h5IHNob3VsZCB0aGUgd29ybGQNCmJlIGxpbmVhcj8gVGhlIHJlbWFpbmluZyByZXN0cmljdGlvbiBpcyB0aGF0IHRoZSBjYXVzYWwgZWZmZWN0IGlzIGFzc3VtZWQgdG8gYmUgaG9tb2dlbmVvdXMgYW5kIG5vdCBhbGxvd2VkIHRvIHZhcnkgd2l0aA0KJFgkLg0KDQpUaGlzIG1vZGVsIHVuZGVybGllcyB0aGUgcGFydGlhbGx5IGxpbmVhciByZWdyZXNzaW9uIChQTFIpIG9mIFtSb2JpbnNvbiAoMTk4OCldKGh0dHBzOi8vZG9pLm9yZy8xMC4yMzA3LzE5MTI3MDUpIGFuZA0KW0NoZXJub3podWtvdiBldCBhbC4gKDIwMTgpXShodHRwczovL2RvaS5vcmcvMTAuMTExMS9lY3RqLjEyMDk3KSB0aGF0IGlzIGltcGxlbWVudGVkIHVzaW5nIHRoZSBmb2xsb3dpbmcgc3RlcHM6DQoNCjEuICBQcmVkaWN0IHRoZSBvdXRjb21lICRcaGF0e1l9ID0gXGhhdHtFfVtZfFhdJCB3aXRoIGEgc3VpdGFibGUgbWV0aG9kIGFuZCBvYnRhaW4gb3V0Y29tZSByZXNpZHVhbHMNCiAgICAkXGhhdHtVfV9ZID0gWSAtIFxoYXR7WX0kDQoNCjIuICBQcmVkaWN0IHRoZSB0cmVhdG1lbnQgJFxoYXR7RH0gPSBcaGF0e0V9W0R8WF0kIHdpdGggYSBzdWl0YWJsZSBtZXRob2QgYW5kIG9idGFpbiB0cmVhdG1lbnQgcmVzaWR1YWxzDQogICAgJFxoYXR7VX1fRCA9IEQgLSBcaGF0e0R9JA0KDQozLiAgUnVuIGEgZmluYWwgcmVzaWR1YWwtb24tcmVzaWR1YWwgcmVncmVzc2lvbiAkXGhhdHtVfV9ZID0gXHRhdSBcaGF0e1V9X0QgKyBcZXBzaWxvbiQgZXZlbiB3aXRob3V0IGEgY29uc3RhbnQgdG8NCiAgICBvYnRhaW4gdGhlIHRhcmdldCBwYXJhbWV0ZXINCg0KV2UgY2FsbCB0aGUgb3V0Y29tZSBhbmQgdHJlYXRtZW50IHByZWRpY3Rpb25zIHRoZSAibnVpc2FuY2UgcGFyYW1ldGVycyIgYmVjYXVzZSB3ZSBhcmUgbm90IGludGVyZXN0ZWQgaW4gdGhlbSAqcGVyIHNlKg0KYnV0IHRoZXkgYXJlIG5lZWRlZCB0byBnZXQgb3VyIGhhbmRzIG9uIHRoZSAidGFyZ2V0IHBhcmFtZXRlciIgJFx0YXUkLg0KDQpXZSBub3cgaW1tZWRpYXRlbHkgc2VlIHRoYXQgT0xTIGlzIGp1c3QgYSBzcGVjaWFsIGNhc2Ugb2YgcGFydGlhbGx5IGxpbmVhciByZWdyZXNzaW9uIHdoZXJlIG91dGNvbWUgYW5kIHRyZWF0bWVudCBhcmUNCnByZWRpY3RlZCB1c2luZyBPTFMuIFRoaXMgZXhwbGFpbnMgd2h5IHdlIGNhbiBydW4gcGxhaW4gT0xTIGluIHRoZSBEb3VibGVNTCBpbmZyYXN0cnVjdHVyZS4NCg0KIyMjIFBhcmFtZXRyaWMgZXN0aW1hdGlvbiBvZiBQTFIgYnkgaGFuZA0KDQpJbiBvdXIgYXBwbGljYXRpb24sIHdlIGhhdmUgYSBiaW5hcnkgdHJlYXRtZW50LiBXZSBjYW4gdGhlcmVmb3JlIHVzZSwgZS5nLiBzdGFuZGFyZCBsb2dpc3RpYyByZWdyZXNzaW9uIHRvIGZvcm0gdGhlDQpwcmVkaWN0aW9ucyBhbmQgdGhlIHJlc2lkdWFscyBvZiB0aGUgdHJlYXRtZW50Lg0KDQpgYGB7cn0NCiMgUGFyYW1ldHJpYyBQTFIgaGFuZC1jb2RlZA0KbG9naXRfbW9kZWwgPSBnbG0oRCB+IFgsIGZhbWlseSA9IGJpbm9taWFsKCkpDQpEaGF0X2xvZ2l0ID0gcHJlZGljdChsb2dpdF9tb2RlbCwgdHlwZSA9ICJyZXNwb25zZSIpDQpEcmVzX2xvZ2l0ID0gRCAtIERoYXRfbG9naXQNCmBgYA0KDQpXZSB0aGVuIHJ1biBvdXIgZmlyc3QgIm9mZmljaWFsIiBwYXJ0aWFsbHkgbGluZWFyIHJlZ3Jlc3Npb24gd2hlcmUgd2UgcmVjeWNsZSB0aGUgT0xTIG91dGNvbWUgcmVzaWR1YWxzIGZyb20gYWJvdmUgYW5kDQp1c2UgdGhlIG5ld2x5IG9idGFpbmVkIGxvZ2l0IHRyZWF0bWVudCByZXNpZHVhbHMNCg0KYGBge3J9DQpwbHJfbG9naXRfaGFuZCA9IGxtX3JvYnVzdChZcmVzX29scyB+IDAgKyBEcmVzX2xvZ2l0KQ0Kc3VtbWFyeShwbHJfbG9naXRfaGFuZCkNCmBgYA0KDQpXZSB3aWxsIGNvbXBhcmUgdGhlIG1hZ25pdHVkZXMgb2YgYWxsIGVzdGltYXRvcnMgYXQgdGhlIGVuZCBiZWxvdyBhbmQgZm9yIG5vdyBmb2N1cyBvbiB0aGUgaW1wbGVtZW50YXRpb24uDQoNCiMjIyBQYXJhbWV0cmljIGVzdGltYXRpb24gb2YgUExSIGJ5IERvdWJsZU1MDQoNCkRvdWJsZSBNYWNoaW5lIExlYXJuaW5nIGlzIGRvaW5nIGV4YWN0bHkgdGhlIHNhbWUgdGhpbmcgaWYgd2UgdGVsbCBpdCB0byB1c2UgT0xTIGZvciBvdXRjb21lIHByZWRpY3Rpb24gYW5kIGxvZ2l0DQpmb3IgdHJlYXRtZW50IHByZWRpY3Rpb246DQoNCmBgYHtyfQ0KIyBQYXJhbWV0cmljIFBMUiBEb3VibGVNTA0KbHJuX2xvZ2l0ID0gbHJuKCJjbGFzc2lmLmxvZ19yZWciLCBwcmVkaWN0X3R5cGUgPSAicHJvYiIpDQpwbHJfbG9naXRfZG1sID0gRG91YmxlTUxQTFIkbmV3KGRhdGFfZG1sLCBtbF9sID0gbHJuX29scywgbWxfbSA9IGxybl9sb2dpdCwgbl9mb2xkcyA9IDEsIGFwcGx5X2Nyb3NzX2ZpdHRpbmcgPSBGQUxTRSkNCnBscl9sb2dpdF9kbWwkZml0KCkNCnByaW50KHBscl9sb2dpdF9kbWwpDQpgYGANCg0KQWdhaW4gbGV0J3MgY2hlY2sgd2hldGhlciB0aGUgcG9pbnQgZXN0aW1hdGVzIGFyZSBhY3R1YWxseSBpZGVudGljYWw6DQoNCmBgYHtyfQ0KYWxsLmVxdWFsKGFzLm51bWVyaWMocGxyX2xvZ2l0X2hhbmQkY29lZmZpY2llbnRzKSxhcy5udW1lcmljKHBscl9sb2dpdF9kbWwkY29lZikpDQpgYGANCg0KIyMjIFBMUiB3aXRoIE1MIG51aXNhbmNlIHBhcmFtZXRlcnMNCg0KT2YgY291cnNlIHRoZSBjb29sIHJlY2VudCBkZXZlbG9wbWVudCBpcyB0aGF0IHRoZSBEb3VibGUgTUwgZnJhbWV3b3JrIHNob3dzIHRoYXQgd2UgY2FuIGFwcGx5IG1hY2hpbmUgbGVhcm5pbmcgbWV0aG9kcw0KdG8gZm9ybSB0aGUgcHJlZGljdGlvbnMgdGhhdCBhcmUgdXNlZCBpbiB0aGUgcmVzaWR1YWxzIGZvciB0aGUgcmVzaWR1YWwtb24tcmVzaWR1YWwgcmVncmVzc2lvbi4gVGhpcyBoZWxwcyB1cyB0byBhdm9pZA0KdGhlIChhdCBsZWFzdCBpbiBteSBvcGluaW9uKSBtb3N0IGFubm95aW5nIHBhcnQgb2YgZXN0aW1hdGluZyBlZmZlY3RzLCB0aGUgbW9kZWwgc3BlY2lmaWNhdGlvbi4gSW5zdGVhZCwgd2Ugb3V0c291cmNlDQp0aGlzIHRhc2sgdG8gcG93ZXJmdWwgTUwgbWV0aG9kcy4NCg0KRm9yIGV4YW1wbGUsIHdlIGNhbiB1c2UgcmFuZG9tIGZvcmVzdHMgdG8gZm9ybSB0aGUgcHJlZGljdGlvbnMuIEkgbGlrZSByYW5kb20gZm9yZXN0cyBiZWNhdXNlIHRoZXkgYXJlIGJ1aWxkIHRvIGZpbmQNCmludGVyYWN0aW9ucyBhbmQgbm9ubGluZWFyaXRpZXMgaW4gYSBkYXRhLWRyaXZlbiBtYW5uZXIgYW5kIGRvIG5vdCByZXF1aXJlIHRvIGNvbW1pdCB0byBhIGRpY3Rpb25hcnkgbGlrZSBmb3IgTGFzc28uDQpIb3dldmVyLCB0aGUgcmVjaXBlIGlzIGdlbmVyaWMgYW5kIHlvdSBjYW4gZGVjaWRlIHdoaWNoIHByZWRpY3Rpb24gYWxnb3JpdGhtIGlzIG1vc3Qgc3VpdGFibGUgZm9yIHlvdXIgYXBwbGljYXRpb24uDQoNClNvIGxldCdzIHNlZSBob3cgd2UgY2FuIGltcGxlbWVudCBQTFIgd2l0aCByYW5kb20gZm9yZXN0IChzaG91bGQgcnVuIHdpdGhpbiBvbmUgbWludXRlKToNCg0KYGBge3J9DQojIFBMUiB1c2luZyBSYW5kb20gRm9yZXN0DQpscm5fcmFuZ2VyID0gbHJuKCJyZWdyLnJhbmdlciIpDQpscm5fcmFuZ2VyX3Byb2IgPSBscm4oImNsYXNzaWYucmFuZ2VyIikgICAgDQpwbHJfcmFuZ2VyX2RtbCA9IERvdWJsZU1MUExSJG5ldyhkbWxfZGF0YSwgbWxfbD1scm5fcmFuZ2VyLCBtbF9tPWxybl9yYW5nZXIpDQpwbHJfcmFuZ2VyX2RtbCRmaXQoc3RvcmVfcHJlZGljdGlvbnM9VFJVRSkNCnByaW50KHBscl9yYW5nZXJfZG1sKQ0KYGBgDQoNCiMjIyMgTWFudWFsbHkgcmVwbGljYXRlIHRoZSByZXN1bHRzDQoNCkxldCdzIHJlcGxpY2F0ZSB0aGUgRG91YmxlTUwgb3V0cHV0IG1hbnVhbGx5IHRvIGNvbnZpbmNlIHlvdSB0aGF0IGl0IGlzIHJlYWxseSBhcyBlYXN5IGFzIEkgY2xhaW0uIFdlIHRvbGQgdGhlDQpgRG91YmxlTUxQTFIoKWAgZnVuY3Rpb24gdG8gc3RvcmUgdGhlIG51aXNhbmNlIHBhcmFtZXRlcnMgYnkgc2V0dGluZyBgc3RvcmVfcHJlZGljdGlvbnM9VFJVRWAgYWJvdmUuDQoNClRoaXMgbWVhbnMgd2UgY2FuIGV4dHJhY3QgdGhlIG91dGNvbWUgYW5kIHRyZWF0bWVudCBwcmVkaWN0aW9ucw0KDQpgYGB7cn0NCkRoYXRfcmFuZ2VyID0gcGxyX3Jhbmdlcl9kbWwkcHJlZGljdGlvbnMkbWxfbQ0KWWhhdF9yYW5nZXIgPSBwbHJfcmFuZ2VyX2RtbCRwcmVkaWN0aW9ucyRtbF9sDQpgYGANCg0KYW5kIHVzZSB0aGVtIGluIGEgcmVzaWR1YWwtb24tcmVzaWR1YWwgcmVncmVzc2lvbg0KDQpgYGB7cn0NCkRyZXNfcmFuZ2VyID0gRCAtIERoYXRfcmFuZ2VyDQpZcmVzX3JhbmdlciA9IFkgLSBZaGF0X3Jhbmdlcg0KcGxyX3Jhbmdlcl9oYW5kID0gbG1fcm9idXN0KFlyZXNfcmFuZ2VyIH4gMCArIERyZXNfcmFuZ2VyKQ0Kc3VtbWFyeShwbHJfcmFuZ2VyX2hhbmQpDQpgYGANCg0KQWdhaW4gdGhlIHBvaW50IGVzdGltYXRlcyBmcm9tIHBhY2thZ2UgYW5kIGhhbmQtY29kZWQgYXJlIGlkZW50aWNhbDoNCg0KYGBge3J9DQphbGwuZXF1YWwoYXMubnVtZXJpYyhwbHJfcmFuZ2VyX2RtbCRjb2VmKSxhcy5udW1lcmljKHBscl9yYW5nZXJfaGFuZCRjb2VmZmljaWVudHMpKQ0KYGBgDQoNCiMgQXVnbWVudGVkIGludmVyc2UgcHJvYmFiaWxpdHkgd2VpZ2h0aW5nDQoNCkRvdWJsZSBNTCB3aXRoIFBMUiBhc3N1bWVkIGEgaG9tb2dlbmVvdXMgdHJlYXRtZW50IGVmZmVjdC4gVGhpcyBtaWdodCBub3QgYmUgaW5ub2NlbnQgYW5kIGNhbiBsZWFkIHRvIGVzdGltYXRlcyBiZWluZw0KcmVwcmVzZW50YXRpdmUgZm9yIGNvdW50ZXJpbnR1aXRpdmUgcG9wdWxhdGlvbnMgaWYgZWZmZWN0cyBhcmUgYWN0dWFsbHkgaGV0ZXJvZ2VuZW91cyAoc2VlIGUuZy4gW1PFgm9jennFhHNraQ0KKDIwMjIpXShodHRwczovL2RvaS5vcmcvMTAuMTE2Mi9yZXN0X2FfMDA5NTMpKS4NCg0KTm93IHdlIHJlbGF4IHRoaXMgYXNzdW1wdGlvbnMgYW5kIGNvbnNpZGVyIGVzdGltYXRvcnMgdGhhdCBvcGVyYXRlIHVuZGVyIHRoZSBmbGV4aWJsZS9pbnRlcmFjdGl2ZSBvdXRjb21lIG1vZGVsIA0KJCQNClkgPSBnKEQsWCkgKyBcdmFyZXBzaWxvbg0KJCQNCg0KVW5kZXIgdGhpcyBtb2RlbCwgdGhlcmUgaXMgbm90IHRoZSBvbmUgJFx0YXUkIGNhcHR1cmluZyB0aGUgZWZmZWN0IGZvciBldmVyeWJvZHkgYnV0IHRoZSBlZmZlY3QgaXMgYWxsb3dlZCB0byBkZXBlbmQgb24NCiRYJHMuIE5vdyB3ZSBjYW4gYXJ0aWN1bGF0ZSBkaWZmZXJlbnQgdGFyZ2V0IHBhcmFtZXRlcnMgYnV0IHdlIGZvY3VzIG9uIHRoZSBjYW5vbmljYWwgQXZlcmFnZSBUcmVhdG1lbnQgRWZmZWN0IChBVEUpDQokXHRhdV97QVRFfSA9IEVbWSgxKSAtIFkoMCldJC4NCg0KVGhlcmUgYXJlIHRocmVlIGNvbW1vbiBzdHJhdGVnaWVzIHRvIGVzdGltYXRlIHRoZSBBVEU6DQoNCjEuICBSZWdyZXNzaW9uIGFkanVzdG1lbnQNCjIuICBJbnZlcnNlIHByb2JhYmlsaXR5IHdlaWdodGluZyAoSVBXKQ0KMy4gIEF1Z21lbnRlZCBJUFcNCg0KYW5kIG9ubHkgdGhlIGxhdHRlciBhbGxvd3MgdGhlIGludGVncmF0aW9uIG9mIE1MIHZpYSB0aGUgRG91YmxlIE1MIGZyYW1ld29yay4NCg0KIyMgUGFyYW1ldHJpYyB2ZXJzaW9ucw0KDQpCZWZvcmUgbW92aW5nIHRvbyBmYXN0LCBsZXQgdXMgZmlyc3QgY29uc2lkZXIgYWxsIHRocmVlIG9wdGlvbnMgaW4gYSBzdGFuZGFyZCBwYXJhbWV0cmljIHNldHRpbmcgKHdlIHdpbGwgZm9jdXMgb24gcG9pbnQNCmVzdGltYXRlcyBhbmQgaWdub3JlIHN0YW5kYXJkIGVycm9ycyBmb3IgdGhlIHNha2Ugb2Ygc2ltcGxpY2l0eSkuDQoNCiMjIyBSZWdyZXNzaW9uIGFkanVzdG1lbnQNCg0KUmVncmVzc2lvbiBhZGp1c3RtZW50IChSQSkgcHJvY2VlZHMgaW4gdGhlIGZvbGxvd2luZyB3YXk6DQoNCjEuICBCdWlsZCBhbiAqKm91dGNvbWUgcHJlZGljdGlvbiBtb2RlbCBvbmx5IHVzaW5nIGNvbnRyb2wgb2JzZXJ2YXRpb25zKiogYW5kIHVzZSB0aGUgbW9kZWwgdG8gZm9ybSBwcmVkaWN0ZWQgb3V0Y29tZXMNCiAgICB1bmRlciBjb250cm9sICRcaGF0e1l9XzAgPSBcaGF0e0V9W1l8RD0wLFhdJCBmb3IgYWxsIG9ic2VydmF0aW9ucy4NCjIuICBCdWlsZCBhbiAqKm91dGNvbWUgcHJlZGljdGlvbiBtb2RlbCBvbmx5IHVzaW5nIHRyZWF0ZWQgb2JzZXJ2YXRpb25zKiogYW5kIHVzZSB0aGUgbW9kZWwgdG8gZm9ybSBwcmVkaWN0ZWQgb3V0Y29tZXMNCiAgICB1bmRlciB0cmVhdG1lbnQgJFxoYXR7WX1fMSA9IFxoYXR7RX1bWXxEPTEsWF0kIGZvciBhbGwgb2JzZXJ2YXRpb25zLg0KMy4gIFRha2UgdGhlIG1lYW4gb2YgdGhlIGRpZmZlcmVuY2VzIGluIHRoZSBwcmVkaWN0aW9ucyAkXGhhdHtcdGF1fV57cmF9ID0gXGZyYWN7MX17bn0gXHN1bV9pIChcaGF0e1l9XzEgLSBcaGF0e1l9XzApJA0KDQpUbyBnZXQgc3RhcnRlZCBsZXQncyB1c2UgT0xTIGZvciB0aGUgcHJlZGljdGlvbjoNCg0KYGBge3J9DQojIFJlZ3Jlc3Npb24gYWRqdXN0bWVudCAvIG91dGNvbWUgaW1wdXRhdGlvbg0Kb2xzMCA9IGxtKFkgfiBYLCBzdWJzZXQgPSAoRCA9PSAwKSkNClkwaGF0X29scyA9IHByZWRpY3Qob2xzMCwgbmV3ZGF0YSA9IGFzLmRhdGEuZnJhbWUoWCkpDQoNCm9sczEgPSBsbShZIH4gWCwgc3Vic2V0ID0gKEQgPT0gMSkpDQpZMWhhdF9vbHMgPSBwcmVkaWN0KG9sczEsIG5ld2RhdGEgPSBhcy5kYXRhLmZyYW1lKFgpKQ0KDQpjYXQoIlJBIHdpdGggT0xTOiIsDQogICAgbWVhbihZMWhhdF9vbHMgLSBZMGhhdF9vbHMpKQ0KYGBgDQoNCiMjIyBJUFcNCg0KSVBXIGZvbGxvd3MgdGhpcyByZWNpcGUNCg0KMS4gIFByZWRpY3QgJFxoYXR7RH0kIHdpdGggYSBzdWl0YWJsZSBtZXRob2QgKGNhbiByZWN5Y2xlIHByZWRpY3Rpb25zIGZyb20gUExSKS4NCjIuICBEZWZpbmUgaW52ZXJzZSBwcm9iYWJpbGl0eSB3ZWlnaHRzIGZvciB0cmVhdGVkICR3XzFee2lwd30gPSBEIC8gXGhhdHtEfSQgYW5kIGNvbnRyb2xzICQoMS1EKSAvICgxLVxoYXR7RH0pJA0KMy4gIFRha2UgdGhlIG1lYW4gb2YgdGhlIHBzZXVkby1vdXRjb21lDQogICAgJFx0aWxkZXtZfV57aXB3fSA9ICh3XzFee2lwd30gLSB3XzBee2lwd30pIFkgXFJpZ2h0YXJyb3cgXGhhdHtcdGF1fV57aXB3fSA9IFxmcmFjezF9e259IFxzdW1faSBcdGlsZGV7WX1ee2lwd30kDQoNCmBgYHtyfQ0KIyBJUFcNCmlwd193ZWlnaHQxID0gRCAvIERoYXRfbG9naXQNCmlwd193ZWlnaHQwID0gKDEgLSBEKSAvICgxIC0gRGhhdF9sb2dpdCkNCll0aWxkZV9pcHcgPSAoaXB3X3dlaWdodDEgLSBpcHdfd2VpZ2h0MCkgKiBZDQpjYXQoIklQVyB3aXRoIExvZ2l0OiIsDQogICAgbWVhbihZdGlsZGVfaXB3KSkNCmBgYA0KDQojIyMgQUlQVw0KDQpBdWdtZW50ZWQgaW52ZXJzZSBwcm9iYWJpbGl0eSB3ZWlnaHRpbmcgY29tYmluZXMgUkEgYW5kIElQVyBhbmQgY3JlYXRlcyB0aGUgZm9sbG93aW5nIHBzZXVkby1vdXRjb21lDQoNCiQkXGJlZ2lue2FsaWduKn0NCiAgICAgXHRpbGRle1l9XnthaXB3fSAmID0gDQogICAgIFx1bmRlcmJyYWNle1xoYXR7WX1fMSAtIFxoYXR7WX1fMH1fe1x0ZXh0e1JBfX0gKyBcdW5kZXJicmFjZXsod18xXntpcHd9IC0gd18wXntpcHd9KSBZfV97XHRleHR7SVBXfX0gLSBcdW5kZXJicmFjZXsod18xXntpcHd9IFxoYXR7WX1fMSAgLSB3XzBee2lwd30gXGhhdHtZfV8wKX1fe1x0ZXh0e2RlYmlhc2luZyB0ZXJtfX0gXFwNCiAgICAgJj0gXGhhdHtZfV8xIC0gXGhhdHtZfV8wICsgXHVuZGVyYnJhY2V7d18xXntpcHd9IChZIC0gXGhhdHtZfV8xKSAtIHdfMF57aXB3fSAoWSAtIFxoYXR7WX1fMCl9X3tcdGV4dHtJUFcgd2VpZ2h0ZWQgcmVzaWR1YWxzfX0NClxlbmR7YWxpZ24qfSQkDQoNCmFuZCB0YWtlcyBpdHMgbWVhbiAkXGhhdHtcdGF1fV57YWlwd30gPSBcZnJhY3sxfXtufSBcc3VtX2kgXHRpbGRle1l9XnthaXB3fSQuIE9uZSBiZWF1dGlmdWwgZmVhdHVyZSBvZiBBSVBXIGlzIHRoYXQgdGhlDQpzdGFuZGFyZCBlcnJvciBvZiB0aGlzIG1lYW4gaXMgdmFsaWQgd2l0aG91dCBhZGp1c3RpbmcgZm9yIHRoZSBlc3RpbWF0aW9uIG9mIHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzLg0KDQpUaGlzIG1vdGl2YXRlcyB0aGUgZm9sbG93aW5nIHJlY2lwZSBmb3IgQUlQVzoNCg0KMS4gIFByZWRpY3QgdGhlIG51aXNhbmNlIHBhcmFtZXRlcnMgJFxoYXR7WX1fMCQsICRcaGF0e1l9XzEkIGFuZCAkXGhhdHtEfSQNCjIuICBDcmVhdGUgcHNldWRvIG91dGNvbWUgJFx0aWxkZXtZfV57YWlwd30kDQozLiAgUnVuIE9MUyB3aXRoIG9ubHkgYSBjb25zdGFudCAkXHRpbGRle1l9XnthaXB3fSA9IFx0YXUgKyBcdmFyZXBzaWxvbiQgdG8gZ2V0IHBvaW50IGVzdGltYXRlIGFuZCBzdGFuZGFyZCBlcnJvci4NCg0KV2UgZmlyc3QgcmVjeWNsZSB0aGUgT0xTIG91dGNvbWUgcHJlZGljdGlvbnMgZnJvbSBSQSBhbmQgdGhlIGxvZ2l0IHRyZWF0bWVudCBwcmVkaWN0aW9ucyBmcm9tIElQVzoNCg0KYGBge3J9DQojIEFJUFcNCll0aWxkZV9wYXJhbWV0cmljID0gWTFoYXRfb2xzIC0gWTBoYXRfb2xzICsgDQogICAgICAgICAgICAgICAgICAgICAgaXB3X3dlaWdodDEgKiAoWSAtIFkxaGF0X29scykgLSANCiAgICAgICAgICAgICAgICAgICAgICBpcHdfd2VpZ2h0MCAqIChZIC0gWTBoYXRfb2xzKQ0KYWlwd19wYXJhbWV0cmljX2hhbmQgPSBsbShZdGlsZGVfcGFyYW1ldHJpYyB+IDEpDQpzdW1tYXJ5KGFpcHdfcGFyYW1ldHJpY19oYW5kKQ0KYGBgDQoNCiMjIyBQYXJhbWV0cmljIEFJUFcgd2l0aCBEb3VibGUgTUwNCg0KV2UgaGF2ZSBoYW5kY29kZWQgQUlQVyB3aXRoIHBhcmFtZXRyaWMgZXN0aW1hdG9ycyBmb3IgdGhlIG51aXNhbmNlIHBhcmFtZXRlcnMgYWJvdmUuIE5vdyB3ZSBlc3RhYmxpc2ggdGhhdCB3ZSBjb3VsZCBoYXZlIGRvbmUNCmp1c3QgdGhlIHNhbWUgd2l0aGluIHRoZSBgRG91YmxlTUxgIHBhY2thZ2UgYnkgdXNpbmcgdGhlIGBEb3VibGVNTElSTSgpYCBmdW5jdGlvbiAoSVJNIHN0YW5kcyBmb3IgaW50ZXJhY3RpdmUgcmVncmVzc2lvbg0KbW9kZWwgYW5kIGlzIGFub3RoZXIgbGFiZWwgZm9yIEFJUFcsIHNvbWV0aW1lcyBBSVBXIGlzIGFsc28gY2FsbGVkIHRoZSBkb3VibHkgcm9idXN0IGVzdGltYXRvcnMsIGFsbCBtZWFuIHRoZSBzYW1lDQp0aGluZywgc28gZG9uJ3QgZ2V0IGNvbmZ1c2VkKS4NCg0KYGBge3J9DQojIFdpdGggRG91YmxlTUwgcGFyYW1ldHJpYw0KYWlwd19wYXJhbWV0cmljX2RtbCA9IERvdWJsZU1MSVJNJG5ldyhkbWxfZGF0YSwgbWxfZyA9IGxybl9vbHMsIG1sX20gPSBscm5fbG9naXQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2ZvbGRzID0gMSwgYXBwbHlfY3Jvc3NfZml0dGluZyA9IEZBTFNFKQ0KYWlwd19wYXJhbWV0cmljX2RtbCRmaXQoKQ0KcHJpbnQoYWlwd19wYXJhbWV0cmljX2RtbCkNCmBgYA0KDQpBZ2FpbiB0aGUgaGFuZGNvZGVkIGFuZCB0aGUgRG91YmxlTUwgcG9pbnQgZXN0aW1hdGVzIGFyZSBpZGVudGljYWw6DQoNCmBgYHtyfQ0KYWxsLmVxdWFsKGFzLm51bWVyaWMoYWlwd19wYXJhbWV0cmljX2hhbmQkY29lZmZpY2llbnRzKSxhcy5udW1lcmljKGFpcHdfcGFyYW1ldHJpY19kbWwkY29lZikpDQpgYGANCg0KIyMjIEFJUFcgd2l0aCBEb3VibGUgTUwNCg0KRG91YmxlIE1MIGZvciBBVEUgZXN0aW1hdGlvbiBzaW1wbHkgdXNlcyBNTCBtZXRob2RzIHRvIGVzdGltYXRlIHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzIGluc3RlYWQgb2YgT0xTIGFuZCBsb2dpdC4gTGlrZQ0KZm9yIFBMUiwgd2UgdXNlIHJhbmRvbSBmb3Jlc3QgdG9kYXk6DQoNCmBgYHtyfQ0KIyBEb3VibGVNTCByYW5kb20gZm9yZXN0DQphaXB3X3Jhbmdlcl9kbWwgPSBEb3VibGVNTElSTSRuZXcoZG1sX2RhdGEsIG1sX2cgPSBscm5fcmFuZ2VyLCBtbF9tID0gbHJuX3Jhbmdlcl9wcm9iKQ0KYWlwd19yYW5nZXJfZG1sJGZpdChzdG9yZV9wcmVkaWN0aW9ucz1UUlVFKQ0KcHJpbnQoYWlwd19yYW5nZXJfZG1sKQ0KYGBgDQoNClRoZSByZXN0IGlzIGlkZW50aWNhbCBhcyB3ZSBjYW4gc2VlIGJ5IGV4YWN0bHkgcmVwbGljYXRpbmcgdGhlIERvdWJsZU1MIG91dHB1dCBpZiB3ZSBleHRyYWN0IHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzDQpmcm9tIHRoZSBEb3VibGVNTCBvYmplY3QNCg0KYGBge3J9DQojIEV4dHJhY3QgbnVpc2FuY2UgcGFyYW1ldGVycw0KRGhhdF9yYW5nZXIgPSBhaXB3X3Jhbmdlcl9kbWwkcHJlZGljdGlvbnMkbWxfbQ0KWTBoYXRfcmFuZ2VyID0gYWlwd19yYW5nZXJfZG1sJHByZWRpY3Rpb25zJG1sX2cwDQpZMWhhdF9yYW5nZXIgPSBhaXB3X3Jhbmdlcl9kbWwkcHJlZGljdGlvbnMkbWxfZzENCg0KIyBDcmVhdGUgcHNldWRvIG91dGNvbWUNCll0aWxkZV9yYW5nZXIgPSBZMWhhdF9yYW5nZXIgLSBZMGhhdF9yYW5nZXIgKyANCiAgICAgICAgICAgICAgICAoRCAvIERoYXRfcmFuZ2VyKSAqIChZIC0gWTFoYXRfcmFuZ2VyKSAtIA0KICAgICAgICAgICAgICAgICgxIC0gRCkgLyAoMSAtIERoYXRfcmFuZ2VyKSAqIChZIC0gWTBoYXRfcmFuZ2VyKQ0KDQojIFJ1biBPTFMgd2l0aCBhIGNvbnN0YW50DQphaXB3X3Jhbmdlcl9oYW5kID0gbG0oWXRpbGRlX3JhbmdlciB+IDEpDQpzdW1tYXJ5KGFpcHdfcmFuZ2VyX2hhbmQpDQpgYGANCg0KVGhlIGNhbm9uaWNhbCBjaGVjayBzaG93cyBpZGVudGljYWwgcG9pbnQgZXN0aW1hdGVzOg0KDQpgYGB7cn0NCmFsbC5lcXVhbChhcy5udW1lcmljKGFpcHdfcmFuZ2VyX2hhbmQkY29lZmZpY2llbnRzKSxhcy5udW1lcmljKGFpcHdfcmFuZ2VyX2RtbCRjb2VmKSkNCmBgYA0KDQojIyBDb25zb2xpZGF0aW9uDQoNCkRvdWJsZSBNTCBib2lscyBkb3duIHRvIHZlcnkgc2ltcGxlIHJlY2lwZXM6DQoNCjEuICBQcmVkaWN0IHRoZSByZWxldmFudCBudWlzYW5jZSBwYXJhbWV0ZXJzDQoyLiAgVXNlIHRoZW0gdG8gZGVmaW5lIG5ldyB2YXJpYWJsZXMgbGlrZSBvdXRjb21lL3RyZWF0bWV0cmVzaWR1YWxzIGZvciBQTFIgb3IgdGhlIHBzZXVkby1vdXRjb21lIGZvciBBSVBXDQozLiAgUnVuIGEgdmVyeSBzaW1wbGUgT0xTIHJlZ3Jlc3Npb24gdG8gb2J0YWluIHBvaW50IGVzdGltYXRlIGFuZCAoISkgdmFsaWQgc3RhbmRhcmQgZXJyb3JzIGV2ZW4gaWYgTUwgaXMgYXBwbGllZCBpbiB0aGUNCiAgICBmaXJzdCBzdGVwDQoNCldlIGltcGxlbWVudGVkIERvdWJsZSBNTCBtYW51YWxseSB3aXRoIHN0YW5kYXJkIHBhcmFtZXRyaWMgZXN0aW1hdG9ycyBhbmQgc2hvd2VkIHRoYXQgY2FsbGluZyBEb3VibGVNTCBmdW5jdGlvbnMgd2l0aA0KdGhlIHNhbWUgcGFyYW1ldHJpYyBtZXRob2RzIHByb2R1Y2VzIGlkZW50aWNhbCByZXN1bHRzLg0KDQpXZSB0aGVuIHVzZWQgdGhlIERvdWJsZU1MIGZ1bmN0aW9ucyB3aXRoIHJhbmRvbSBmb3Jlc3RzIGFuZCBzaG93ZWQgdGhhdCB3ZSBjb3VsZCByZXBsaWNhdGUgdGhlIG91dHB1dHMgbWFudWFsbHkuDQoNClRoaXMgcmVxdWlyZWQgZmV3IGxpbmVzIG9mIGNvZGUgYW5kIGhvcGVmdWxseSBjb252aW5jZWQgeW91IHRoYXQgZXZlcnl0aGluZyBib2lscyBkb3duIHRvIHJlbGF0aXZlbHkgc2ltcGxlIHJlY2lwZXMuDQoNCkxldCdzIGNvbGxlY3Qgd2hhdCB3ZSBhY2hpZXZlZC4NCg0KYGBge3J9DQplc3RpbWF0b3JfbmFtZXMgPSBjKCJPTFMiLCJEb3VibGVNTCBQTFIgUGFyYW1ldHJpYyIsIkRvdWJsZU1MIFBMUiBGb3Jlc3QiLA0KICAgICAgICAgICAgICAgICAgICJEb3VibGVNTCBBSVBXIHBhcmFtZXRyaWMiLCJEb3VibGVNTCBBSVBXIEZvcmVzdCIpDQpwb2ludF9lc3RpbWF0ZXMgPSBjKG9sc19kbWwkY29lZixwbHJfbG9naXRfZG1sJGNvZWYscGxyX3Jhbmdlcl9kbWwkY29lZiwNCiAgICAgICAgICAgICAgICAgICBhaXB3X3BhcmFtZXRyaWNfZG1sJGNvZWYsYWlwd19yYW5nZXJfZG1sJGNvZWYpDQpzdGFuZGFyZF9lcnJvcnMgPSBjKG9sc19kbWwkc2UscGxyX2xvZ2l0X2RtbCRzZSxwbHJfcmFuZ2VyX2RtbCRzZSwNCiAgICAgICAgICAgICAgICAgICBhaXB3X3BhcmFtZXRyaWNfZG1sJHNlLGFpcHdfcmFuZ2VyX2RtbCRzZSkNCg0KIyBHcmFwaCBwb3dlcmVkIGJ5IENoYXRHUFQNCiMgQ3JlYXRlIGEgZGF0YSBmcmFtZQ0KZGF0YSA8LSBkYXRhLmZyYW1lKA0KICBFc3RpbWF0b3IgPSBmYWN0b3IoZXN0aW1hdG9yX25hbWVzLCBsZXZlbHMgPSByZXYoZXN0aW1hdG9yX25hbWVzKSksICMgUmV2ZXJzZSB0aGUgb3JkZXIgZm9yIGNvcnJlY3QgcGxvdHRpbmcNCiAgRXN0aW1hdGUgPSBwb2ludF9lc3RpbWF0ZXMsDQogIFNFID0gc3RhbmRhcmRfZXJyb3JzDQopDQoNCiMgQ2FsY3VsYXRlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzDQpkYXRhJExvd2VyIDwtIGRhdGEkRXN0aW1hdGUgLSAxLjk2ICogZGF0YSRTRQ0KZGF0YSRVcHBlciA8LSBkYXRhJEVzdGltYXRlICsgMS45NiAqIGRhdGEkU0UNCg0KIyBQbG90DQpnZ3Bsb3QoZGF0YSwgYWVzKHggPSBFc3RpbWF0b3IsIHkgPSBFc3RpbWF0ZSwgY29sb3IgPSBFc3RpbWF0b3IpKSArDQogIGdlb21fZXJyb3JiYXIoYWVzKHltaW4gPSBMb3dlciwgeW1heCA9IFVwcGVyKSwgd2lkdGggPSAwLjIsIGNvbG9yID0gImdyYXk0MCIsIGxpbmV3aWR0aCA9IDAuNSkgKyAjIEN1c3RvbSBlcnJvciBiYXJzDQogIGdlb21fcG9pbnQoc2l6ZSA9IDQpICsgIyBMYXJnZXIgcG9pbnRzDQogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDAsIGxpbmV0eXBlID0gImRhc2hlZCIsIGNvbG9yID0gInJlZCIsIGxpbmV3aWR0aCA9IDAuNSkgKyAjIE1vcmUgcHJvbWluZW50IGhvcml6b250YWwgbGluZSBhdCB6ZXJvDQogIGNvb3JkX2ZsaXAoKSArICMgRmxpcCBjb29yZGluYXRlcyB0byBwdXQgZXN0aW1hdG9ycyBvbiB0aGUgeS1heGlzDQogIGxhYnMoeCA9ICJFc3RpbWF0b3IiLCB5ID0gIkVzdGltYXRlIiwgdGl0bGUgPSAiUG9pbnQgRXN0aW1hdGVzIHdpdGggOTUlIENvbmZpZGVuY2UgSW50ZXJ2YWxzIikgKw0KICB0aGVtZV9saWdodChiYXNlX3NpemUgPSAxNCkgKyAjIExpZ2h0ZXIgdGhlbWUgd2l0aCBsYXJnZXIgYmFzZSB0ZXh0IHNpemUNCiAgdGhlbWUoDQogICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gMTYsIGZhY2UgPSAiYm9sZCIsIGNvbG9yID0gImRhcmtibHVlIiksICMgQ3VzdG9tIHBsb3QgdGl0bGUNCiAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDE0LCBmYWNlID0gImJvbGQiKSwgIyBDdXN0b20geC1heGlzIHRpdGxlDQogICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KHNpemUgPSAxNCwgZmFjZSA9ICJib2xkIiksICMgQ3VzdG9tIHktYXhpcyB0aXRsZQ0KICAgIHBhbmVsLmdyaWQubWFqb3IgPSBlbGVtZW50X2xpbmUoY29sb3IgPSAiZ3JheTgwIiksICMgQ3VzdG9tIG1ham9yIGdyaWQgbGluZXMNCiAgICBwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpLCAjIFJlbW92ZSBtaW5vciBncmlkIGxpbmVzDQogICAgbGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiICMgUmVtb3ZlIGxlZ2VuZCBhcyBjb2xvciBpcyBtYXBwZWQgdG8gRXN0aW1hdG9yIGJ1dCBub3QgbmVlZGVkDQogICkNCg0KYGBgDQoNClRoZSBkaWZmZXJlbmNlcyBpbiB0aGUgcG9pbnQgZXN0aW1hdGVzIGFyZSBub3Qgc3RyaWtpbmcuIEhvd2V2ZXIsIEFJUFcgd2l0aCByYW5kb20gZm9yZXN0IGlzIGJ5IGZhciB0aGUgbW9zdCBwcmVjaXNlDQplc3RpbWF0b3IuIFRoaXMgbWFrZXMgc2Vuc2UgYmVjYXVzZSBBSVBXIGlzIHRoZSBlZmZpY2llbnQgZXN0aW1hdG9yIGluIHRoZSBsaWtlbHkgY2FzZSB0aGF0IGVmZmVjdHMgYXJlIGhldGVyb2dlbmVvdXMNCmFuZCByYW5kb20gZm9yZXN0IGlzIGV4cGVjdGVkIHRvIGFwcHJveGltYXRlIHRoZSBudWlzYW5jZSBwYXJhbWV0ZXJzIG11Y2ggYmV0dGVyIHRoYW4gc2ltcGxlIHBhcmFtZXRyaWMgbW9kZWxzLg0KDQo8YnI+PGJyPg0KDQojIENhdXNhbCBGb3Jlc3QNCg0KU28gZmFyLCB3ZSB3ZXJlIGludGVyZXN0ZWQgaW4gZXN0aW1hdGluZyBhbiBhc3N1bWVkIGhvbW9nZW5lb3VzIHRyZWF0bWVudCBlZmZlY3QgJFx0YXUkIG9yIHRoZSBBVEUuIEhvd2V2ZXIsIHdlIG1pZ2h0IGJlDQppbnRlcmVzdGVkIGluIGdvaW5nIGJleW9uZCBzdWNoIHN1bW1hcnkgbWVhc3VyZXMgYW5kIHdhbnQgdG8gZXN0aW1hdGUgY29uZGl0aW9uYWwgYXZlcmFnZSB0cmVhdG1lbnQgZWZmZWN0cyAoQ0FURXMpDQoNCiQkIFx0YXUoeCkgPSBFW1koMSkgLSBZKDApfFggPSB4XSAkJA0KDQpDYXVzYWwgZm9yZXN0IGFzIGludHJvZHVjZWQgYnkgW0F0aGV5IGV0IGFsLiAoMjAxOSldKGh0dHBzOi8vZG9pLm9yZy8xMC4xMjE0LzE4LUFPUzE3MDkpIGlzIGFyZ3VhYmx5IHRoZSBtb3N0IHByb21pbmVudA0KZXN0aW1hdG9yIHRvIGVzdGltYXRlIHN1Y2ggQ0FURXMuIFRoZXkgYXJlIGltcGxlbWVudGVkIGluIHRoZSBgZ3JmYCBwYWNrYWdlIGFuZCBjYW4gYmUgY29uc2lkZXJlZCBhcyBqdXN0IHJ1bm5pbmcgYQ0KKip3ZWlnaHRlZCByZXNpZHVhbC1vbi1yZXNpZHVhbCByZWdyZXNzaW9uKiogKHRoaXMgbWlnaHQgbm90IGJlIG9idmlvdXMgaW4gdGhlIG9yaWdpbmFsIHBhcGVyIGJ1dCBmb3IgYW4gYWNjZXNzaWJsZQ0KZXhwbGFuYXRpb24gc2VlIFtBdGhleSBhbmQgV2FnZXIgKDIwMTkpXShodHRwczovL211c2Uuamh1LmVkdS9wdWIvNTYvYXJ0aWNsZS83OTMzNTYvc3VtbWFyeSkpLg0KDQpJdCBtaWdodCBmZWVsIGNvdW50ZXJpbnR1aXRpdmUgd2h5IGEgbWV0aG9kIGZvciBob21vZ2VuZW91cyBlZmZlY3RzIGNhbiBzdWRkZW5seSBiZSB1c2VkIHRvIGVzdGltYXRlIGhldGVyb2dlbmVvdXMNCmVmZmVjdHMuIFVuZm9ydHVuYXRlbHksIHdlIGhhdmUgbm90IHRoZSB0aW1lIHRvIGRpc2N1c3Mgd2h5IGl0IGFjdHVhbGx5IG1ha2VzIHNlbnNlLiBJbnN0ZWFkLCBJIHNob3cgeW91IHRoYXQgdGhpcyBpcyBpbmRlZWQgd2hhdCBpcw0KZ29pbmcgb24uDQoNCkZpcnN0LCBydW4gdGhlIGRlZmF1bHQgaW1wbGVtZW50YXRpb24gb2YgdGhlIGBjYXVzYWxfZm9yZXN0KClgIGFuZCBwbG90IHRoZSBkaXN0cmlidXRpb24gb2YgZXN0aW1hdGVkIENBVEVzOg0KDQpgYGB7cn0NCmNmID0gY2F1c2FsX2ZvcmVzdChYLFksRCkNCmNhdGUgPSBwcmVkaWN0KGNmKSRwcmVkaWN0aW9ucw0KaGlzdChjYXRlKQ0KYGBgDQoNCldlIHNlZSB0aGF0IHRoZXJlIGlzIHF1aXRlIHNvbWUgaGV0ZXJvZ2VuZWl0eSBidXQgd2Ugd2lsbCBub3QgdGFrZSBhIGNsb3NlciBsb29rIHRvZGF5LiBIb3dldmVyLCB0aGVyZSBhcmUgZXhjZWxsZW50DQp0dXRvcmlhbHMgd2hhdCB0byBkbyB3aXRoIHRoZXNlIGtpbmRzIG9mIHJlc3VsdHMgb24gdGhlIFtncmYgaG9tZXBhZ2VdKGh0dHBzOi8vZ3JmLWxhYnMuZ2l0aHViLmlvL2dyZi9pbmRleC5odG1sKS4NCg0KVG9kYXkgd2UgYXJlIGludGVyZXN0ZWQgaW4gdW5kZXJzdGFuZGluZyBob3cgdGhlc2UgbnVtYmVycyBjb21lIGFib3V0LiBJIHRvbGQgeW91IHRoYXQgZ3JmIGlzIGFjdHVhbGx5IHJ1bm5pbmcgYQ0Kd2VpZ2h0ZWQgUm9SUi4gVG8gc2VlIHRoaXMgZmlyc3QgZXh0cmFjdCB0aGUgb3V0Y29tZSBhbmQgdHJlYXRtZW50IHByZWRpY3Rpb25zIHRoYXQgYXJlIHN0b3JlZCBpbiB0aGUgYGNhdXNhbF9mb3Jlc3QoKWANCm9iamVjdCBhbmQgdXNlIHRoZW0gdG8gcHJvZHVjZSByZXNpZHVhbHM6DQoNCmBgYHtyfQ0KRGhhdF9jZiA9IGNmJFcuaGF0IA0KRHJlc19jZiA9IEQgLSBEaGF0X2NmIA0KWWhhdF9jZiA9IGNmJFkuaGF0IA0KWXJlc19jZiA9IFkgLSBZaGF0X2NmDQpgYGANCg0KVGhleSBjb3VsZCBiZSB1c2VkIHRvIGZvcm0gdGhlIGJ5IG5vdyBmYW1pbGlhciBQTFIgZXN0aW1hdGU6DQoNCmBgYHtyfQ0KcGxyX2NmX2hhbmQgPSBsbV9yb2J1c3QoWXJlc19jZiB+IDAgKyBEcmVzX2NmKSANCnN1bW1hcnkocGxyX2NmX2hhbmQpDQpgYGANCg0KSG93ZXZlciwgdGhpcyBpcyBub3Qgd2hhdCBjYXVzYWwgZm9yZXN0cyBhY3R1YWxseSBkby4gQ2F1c2FsIGZvcmVzdHMgcHJvZHVjZSBhbiBpbmRpdmlkdWFsaXplZCBlc3RpbWF0ZSBmb3IgZXZlcnkNCmluZGl2aWR1YWwuIFRvIHRoaXMgZW5kLCB0aGV5IHJ1biBSb1JSIGJ1dCBnaXZlIGhpZ2hlciB3ZWlnaHRzIHRvIG9ic2VydmF0aW9ucyB0aGF0IGFyZSBzaW1pbGFyIHRvIHRoZSBpbmRpdmlkdWFsIGZvcg0Kd2hpY2ggdGhlIENBVEUgaXMgZXN0aW1hdGVkLg0KDQpUaGlzIHdlaWdodHMgLSB1c3VhbGx5IGNhbGxlZCAkXGFscGhhJCAtIGNhbiBiZSBleHRyYWN0ZWQgdmlhIHRoZSBgZ2V0X2ZvcmVzdF93ZWlnaHRzKClgIGZ1bmN0aW9uOg0KDQpgYGB7cn0NCmFscGhhID0gZ2V0X2ZvcmVzdF93ZWlnaHRzKGNmKQ0KYGBgDQoNCkxldCdzIGlsbHVzdHJhdGUgdGhpcyBieSByZXBsaWNhdGluZyB0aGUgQ0FURSBmb3IgdGhlIGZpcnN0IG9ic2VydmF0aW9uIGluIHRoZSBzYW1wbGUgYXMgYW4gZXhhbXBsZSwgd2hpY2ggaXMNCg0KYGBge3J9DQpjYXRlMSA9IGNhdGVbMV0gDQpjYXRlMQ0KYGBgDQoNCldlIHRyeSB0byByZXBsaWNhdGUgdGhpcyBudW1iZXIgaW4gYSB3ZWlnaHRlZCBSb1JSOg0KDQpgYGB7cn0NCmZpcnN0X3RyeSA9IGxtX3JvYnVzdChZcmVzX2NmIH4gMCArIERyZXNfY2YsIHdlaWdodCA9IGFscGhhWzEsXSkgDQpzdW1tYXJ5KGZpcnN0X3RyeSkNCmBgYA0KDQphbmQgYXJlIHNsaWdodGx5IG9mZiA6LSgNCg0KVGhpcyBpcyBiZWNhdXNlIGBjYXVzYWxfZm9yZXN0KClgIHVzZXMgYSBjb25zdGFudCBpbiB0aGUgUk9SUi4gT25jZSB3ZSBmaWd1cmVkIHRoYXQgb3V0LCB3ZSBjYW4gcGVyZmVjdGx5IHJlcGxpY2F0ZSB0aGUNCm91dHB1dDoNCg0KYGBge3J9DQpjYXRlMV9oYW5kID0gbG1fcm9idXN0KFlyZXNfY2YgfiBEcmVzX2NmLCB3ZWlnaHQgPSBhbHBoYVsxLF0pJGNvZWZmaWNpZW50c1syXQ0KY2F0ZTFfaGFuZCANCmNhdCgiXG5FcXVhbCB0byBncmYgb3V0cHV0PyIsYWxsLmVxdWFsKGFzLm51bWVyaWMoY2F0ZTEpLGFzLm51bWVyaWMoY2F0ZTFfaGFuZCkpKQ0KYGBgDQoNCllvdSBjb3VsZCByZXBlYXQgdGhpcyBmb3IgZXZlcnkgb2JzZXJ2YXRpb24gYnkgdXNpbmcgdGhlIGFwcHJvcHJpYXRlIHJvdyBvZiB0aGUgYWxwaGEgd2VpZ2h0IG1hdHJpeC4NCg0KKlNvbWUgcmVtYXJrczoqDQoNCi0gICBUaGUgcmVzdWx0cyB3aXRoIGFuZCB3aXRob3V0IGEgY29uc3RhbnQgYXJlIHVzdWFsbHkgdmVyeSBzaW1pbGFyIGJlY2F1c2UgdGhlIGNvbnN0YW50IGlzIHplcm8gYXN5bXB0b3RpY2FsbHkuDQoNCi0gICBVbmxpa2Ugd2l0aCBEb3VibGUgTUwsIHdlIGNhbiBOT1QgdXNlIHRoZSBzdGFuZGFyZCBlcnJvcnMgb2YgdGhpcyBvdXRwdXQuIEluZmVyZW5jZSBmb3IgY2F1c2FsIGZvcmVzdCBDQVRFcyBpcyBtdWNoDQogICAgbW9yZSBjb21wbGljYXRlZCBhbmQgYmV5b25kIHRoZSBzY29wZSBvZiB0aGlzIG5vdGVib29rLg0KDQotICAgTXkgc3R1ZGVudHMgaW4gVMO8YmluZ2VuIHByb2dyYW1tZWQgdGhlIFtDYXVzYWwgRm9yZXN0IEZ1biBTaGlueSBBcHBdKGh0dHBzOi8vbWFyZW5ibWcuc2hpbnlhcHBzLmlvL2NhdXNhbEZvcmVzdC8pIHRvDQogICAgaWxsdXN0cmF0ZSB0aGUgZGlmZmVyZW50IHdlaWdodGluZyBpbiB0b3kgZXhhbXBsZXMgKGhhdmUgYSBsb29rKS4NCg0KPGJyPiA8YnI+DQoNCiMgRnVydGhlciBmcmVlIG1hdGVyaWFsDQoNCllvdSBzZWUgdGhhdCB0aGluZ3MgY2FuIGJlIHF1aXRlIGVhc3kuIElmIHlvdSB3YW50IHRvIHRha2UgYSBkZWVwIGRpdmUsIEkgY29sbGVjdCBoZXJlIGEgdmFyaWV0eSBvZiByZXNvdXJjZXMgdGhhdCBjYW4NCmhlbHAgeW91IHRvIHVuZGVyc3RhbmQgdGhlIHRoZW9yZXRpY2FsIHVuZGVycGlubmluZyBiZXlvbmQgdGhlIHBhcGVycyBsaW5rZWQgdGhyb3VnaG91dCB0aGUgbm90ZWJvb2sgYWJvdmU6DQoNCi0gICBbQ2F1c2FsIE1MIGJvb2tdKGh0dHBzOi8vY2F1c2FsbWwtYm9vay5vcmcvKTogQnJhbmQgbmV3IHRleHRib29rIGNvdmVyaW5nIERvdWJsZSBNTCBhbmQgbXVjaCBtb3JlDQoNCi0gICBbQ2F1c2FsIEluZmVyZW5jZSBmb3IgVGhlIEJyYXZlIGFuZA0KICAgIFRydWVdKGh0dHBzOi8vbWF0aGV1c2ZhY3VyZS5naXRodWIuaW8vcHl0aG9uLWNhdXNhbGl0eS1oYW5kYm9vay9sYW5kaW5nLXBhZ2UuaHRtbCksIGVzcGVjaWFsbHkgUGFydCBJSTogQWNjZXNzaWJsZQ0KICAgIGludHJvZHVjdGlvbg0KDQotICAgW1N0YXRzIDM2MSBsZWN0dXJlIG5vdGVzIG9mIFN0ZWZhbiBXYWdlcl0oaHR0cHM6Ly93ZWIuc3RhbmZvcmQuZWR1L35zd2FnZXIvc3RhdHMzNjEucGRmKTogTW9yZSB0ZWNobmljYWwgbWF0ZXJpYWwNCg0KLSAgIFRoZSBbRG91YmxlTUxdKGh0dHBzOi8vZG9jcy5kb3VibGVtbC5vcmcvc3RhYmxlL2luZGV4Lmh0bWwpIGFuZCBbZ3JmXShodHRwczovL2RvY3MuZG91YmxlbWwub3JnL3N0YWJsZS9pbmRleC5odG1sKQ0KICAgIGV4cGxhaW4gdGhlIHRoZW9yeSBiZWhpbmQgdGhlIGFwcHJvYWNoZXMgYW5kIHByb3ZpZGUgZ3JlYXQgaW1wbGVtZW50YXRpb24gZXhhbXBsZXMNCg0KLSAgIFtNYWNoaW5lIExlYXJuaW5nIGZvciBFY29ub21pc3RzXShodHRwczovL3NpdGVzLmdvb2dsZS5jb20vdmlldy9kYXJpb3NhbnNvbmUvcmVzb3VyY2VzL21hY2hpbmUtbGVhcm5pbmcpIGEgbGlzdA0KICAgIG1haW50YWluZWQgYnkgRGFyaW8gU2Fuc29uZSBwcm92aWRlcyBhIG5pY2Ugb3ZlcnZpZXcgb2YgbWV0aG9kb2xvZ2ljYWwgd29yayBhbmQgYXBwbGljYXRpb24NCg0KLSAgIFtNeSBDYXVzYWwgTUwgdGVhY2hpbmcgc2xpZGVzXShodHRwczovL2dpdGh1Yi5jb20vTUNLbmF1cy9jYXVzYWxNTC10ZWFjaGluZyk6IElmIHlvdSBsaWtlIG15IHN0eWxlIG9mIHRlYWNoaW5nIGFuZA0KICAgIG5vdGVib29rcyBsaWtlIHRoaXMsIGhhdmUgYSBjbG9zZXIgbG9vaw0K