In multilevel analyses, it is often necessary to estimate a standardised mean difference (SMD) such that the numerator mean effect comes from a different model than the denominator SD. The numerator could be a covariate-adjusted mean difference and the denominator the SD from the sum of residual and random effect variances in a model without covariate adjustment. For example, statistical analysis guidance for EEF evaluations requires this approach, alongside confidence intervals.

This note simulates data with known sample effect size, covariate-outcome association, and random effect variances, and estimates the SMD using the two packages in R I know that directly do the sums:

  1. {eefAnalytics} has a function crtFREQ that wraps up calls to lme4::lmer and calculates SMDs using total and residual variance, with and without covariate adjustment.
  2. {lmeInfo} has a function g_mlm that takes two nlme::lme models and allows you to select which fixed and random effect variance estimates go into the ratio.

TL;DR summary: surprisingly, {eefAnalytics} does not follow EEF guidance. I can, however, replicate what it does using {lmeInfo}. Also I can get {lmeInfo} to provide the answer I expect.

(I’m aware of arguments against SMDs, e.g., Tukey, 1969, and arguments for using an SD from a survey of the population of interest.)

Include required libraries

quiet_library <- \(...) library(...) |> suppressPackageStartupMessages()

quiet_library(tidyverse)
quiet_library(lme4)
quiet_library(tictoc)
quiet_library(beepr)
quiet_library(lmeInfo)
quiet_library(eefAnalytics)
quiet_library(nlme)
quiet_library(lmeInfo)

Parameters for the simulation

n_schools  <- 1000
n_pupils   <- 100
n_total    <- n_schools * n_pupils
n_treat    <- n_total / 2
cov_var    <- 0.6  # proportion of variance explained by the pupil covariate
school_var <- 0.3  # proportion of variance explained by school
es         <- 1.23 # the effect size we want to simulate

stopifnot(n_total %% 2 == 0)
stopifnot(n_schools %% 2 == 0)

The SMD we’re looking for is 1.23 – keep an eye out for that number!

Simulate the data

Note that I’m trying to get the sample to have exactly the properties I want, hence all the scaling to get sample SD = 1 and use of residuals to remove sample correlations.

set.seed(42)
my_scale <- function(...) scale(...) |> as.numeric()
school_sim <- tibble(school       = 1:n_schools,
                     noise_school = rnorm(n_schools))

sim_dat <- tibble(
  id     = 1:n_total,
  school = rep(1:n_schools, n_pupils) |> sort(),
  treat  = (school <= n_schools / 2) + 0
) |>
  left_join(school_sim, by = "school") |>
  mutate(
    ranef_school = my_scale(lm(noise_school ~ treat)$resid) * sqrt(school_var),
    noise1       = lm(rnorm(n_total) ~ ranef_school + factor(school))$resid |>
                     my_scale(),
    noise2       = lm(rnorm(n_total) ~ noise1 + ranef_school + factor(school))$resid |>
                     my_scale(),
    resid        = noise1 * sqrt(1 - cov_var - school_var),
    covar        = noise2 * sqrt(cov_var),
    y            = resid + ranef_school + covar + es * treat
  )

Descriptives

sim_dat |>
  select(-starts_with("noise")) |>
  pivot_longer(cols = everything()) |>
  filter(!name %in% c("id", "school")) |>
  group_by(name) |>
  summarise(
    mean = mean(value),
    sd = sd(value),
    var = sd^2,
    min = min(value),
    max = max(value)
  ) |>
  mutate(across(where(is.numeric), \(x) round(x, 2)))

The SD for y is greater than 1 since y is a mixture of normals. We can get (nearly) 1 by calculating the SD by group and pooling:

by_group_summary <- sim_dat |>
  group_by(treat) |>
  summarise(mean_y = mean(y), sd_y = sd(y), n = n())
by_group_summary |> round(3)
by_group_summary$sd_y^2 |> mean() |> sqrt()
[1] 1.000005

Here’s a picture:

sim_dat |>
  ggplot(aes(x = y, fill = factor(treat, labels = c("Control", "Treat")))) +
  geom_histogram(alpha = 0.5,
                 position = "identity",
                 bins = 100) +
  labs(fill = "Group", x = "Outcome", y = "Count") +
  theme_minimal()

Here are the covariances:

sim_dat |>
  select(-starts_with("noise")) |>
  select(-c("id", "school")) |>
  cov() |>
  round(2) |>
  as.data.frame()

{eefAnalytics}

(In case you too try this approach, crtFREQ wants the random effect ID to be a numeric and doesn’t like factors.)

This first example includes only treatment group and random effect, with no covariate. It should provide the correct answer, though this isn’t generally a good idea as the confidence interval will be too wide if your covariates explain variation in the outcome:

eefa_uncond <- crtFREQ(
  y ~ treat,
  random = "school",
  intervention = "treat",
  data = sim_dat
)
Computing profile confidence intervals ...
eefa_uncond

Model Info:
 Method:     MLM
 Design:     CRT
 Approach:   Frequentist
 Function:   crtFREQ
---------

Result for: Conditional effect size
$treat1
       Estimate 95% LB 95% UB
Within     1.46   1.38   1.54
Total      1.23   1.16   1.30


Result for: Unconditional effect size
$treat1
       Estimate 95% LB 95% UB
Within     1.46   1.34   1.58
Total      1.05   0.96   1.14


Please use summary to get more results 
should_be_correct <- eefa_uncond$ES$treat1["Total",]$Estimate
stopifnot(round(should_be_correct, 2) == round(es, 2))

As expected, the conditional effect size using the total variance is 1.23, matching the target.

The following call with the covariate included yields too large an answer for the conditional total and too small an answer for the unconditional total effect:

eef_wrong <- crtFREQ(
  y ~ treat + covar,
  random = "school",
  intervention = "treat",
  data = sim_dat
)
Computing profile confidence intervals ...
eef_wrong

Model Info:
 Method:     MLM
 Design:     CRT
 Approach:   Frequentist
 Function:   crtFREQ
---------

Result for: Conditional effect size
$treat1
       Estimate 95% LB 95% UB
Within     3.87   3.66   4.08
Total      1.94   1.82   2.07


Result for: Unconditional effect size
$treat1
       Estimate 95% LB 95% UB
Within     1.46   1.34   1.58
Total      1.05   0.96   1.14


Please use summary to get more results 
too_big_es   <- eef_wrong$ES$treat1["Total",]$Estimate
too_small_es <- eef_wrong$Unconditional$ES$treat1["Total",]$Estimate
stopifnot(too_big_es > es)
stopifnot(too_small_es < es)

We’re aiming for 1.23. The total conditional effect size is 1.94 and the unconditional is 1.05.

The model behind the conditional effect size shrinks the residuals, as the covariate explains variance in the outcome. The denominator is smaller than we want, so the SMD is too large.

The model behind the unconditional effect size denominator doesn’t take account of the intervention increasing the between-school variance as it pushes the means apart. We can see that if we examine the school random effects in the unconditional model:

uncond_lmer <- lmer(y ~ (1|school), data = sim_dat)
summary(uncond_lmer)
Linear mixed model fit by REML ['lmerMod']
Formula: y ~ (1 | school)
   Data: sim_dat

REML criterion at convergence: 253693.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.1978 -0.6704 -0.0001  0.6682  4.7211 

Random effects:
 Groups   Name        Variance Std.Dev.
 school   (Intercept) 0.6718   0.8197  
 Residual             0.7071   0.8409  
Number of obs: 100000, groups:  school, 1000

Fixed effects:
            Estimate Std. Error t value
(Intercept)  0.61500    0.02606    23.6

Here’s a picture of the school random effects showing what’s going on:

plot_ranefs <- function(mod) {
school_ranefs <- ranef(mod)$school |>
  rename("intercept" = "(Intercept)") |>
  rownames_to_column("school") |>
  mutate(school = as.numeric(school),
         treat  = (school <= n_schools / 2) + 0)

school_ranefs |>
  ggplot(aes(x = intercept, fill = factor(treat, labels = c("Control", "Treat")))) +
  geom_histogram(alpha = 0.5,
                 position = "identity",
                 bins = 21) +
  labs(fill = "Group", x = "School random effect", y = "Count") +
  theme_minimal()
}
plot_ranefs(uncond_lmer)

Let’s look again, this time using a model that includes the treatment predictor:

treat_lmer <- lmer(y ~ treat + (1|school), data = sim_dat)
summary(treat_lmer)
Linear mixed model fit by REML ['lmerMod']
Formula: y ~ treat + (1 | school)
   Data: sim_dat

REML criterion at convergence: 252883.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.1922 -0.6704 -0.0005  0.6683  4.7100 

Random effects:
 Groups   Name        Variance Std.Dev.
 school   (Intercept) 0.2935   0.5418  
 Residual             0.7071   0.8409  
Number of obs: 100000, groups:  school, 1000

Fixed effects:
             Estimate Std. Error t value
(Intercept) 3.093e-13  2.452e-02    0.00
treat       1.230e+00  3.468e-02   35.47

Correlation of Fixed Effects:
      (Intr)
treat -0.707

Note how much smaller the school random effect variance is now (almost the 0.3 I was originally aiming for – see the simulation parameters). The picture:

plot_ranefs(treat_lmer)

This is the model we want for the denominator.

{lmeInfo}

Let’s see if {lmeInfo} can produce the SMD we want. First, fit a model for the numerator (effect estimate) and denominator (SD). I’ve also fitted an intercept-only model, for use later when replicating {eefAnalytics}’s results.

lme_num <- lme(
  fixed = y ~ treat + covar,
  random = ~ 1 | school,
  data = sim_dat,
  method = "REML",
  control = lmeControl(opt = "optim")
)

lme_den <- lme(
  fixed = y ~ treat,
  random = ~ 1 | school,
  data = sim_dat,
  method = "REML",
  control = lmeControl(opt = "optim")
)

lme_empty <- lme(
  fixed = y ~ 1,
  random = ~ 1 | school,
  data = sim_dat,
  method = "REML",
  control = lmeControl(opt = "optim")
)

Estimate the SMD:

correct_es_est <- g_mlm(
  lme_num,
  p_const = c(0,1,0),
  mod_denom = lme_den,
  r_const = c(1,1),
  infotype = "expected",
  separate_variances = FALSE
)
correct_es_est
                            est    se
unadjusted effect size     1.23 0.036
adjusted effect size       1.23 0.036
degree of freedom      10484.71      
stopifnot(round(correct_es_est$g_AB, 3) == es)

The estimate is as desired, to three decimal places. In full:

correct_es_est$g_AB
[1] 1.229549

We can also get a CI:

CI_g(correct_es_est) |> round(2)
[1] 1.16 1.30

Replicating {eefAnalytics} using {lmeInfo}

Let’s see if we can replicate {eefAnalytics}. Here’s the conditional effect size:

conditional_eef_style <- g_mlm(
  lme_num,
  p_const = c(0,1,0),
  mod_denom = lme_num,
  r_const = c(1,1),
  infotype = "expected",
  separate_variances = FALSE
)
conditional_eef_style
                            est    se
unadjusted effect size    1.943 0.064
adjusted effect size      1.943 0.064
degree of freedom      1770.482      
stopifnot(round(conditional_eef_style$delta_AB, 2) == round(too_big_es, 2))

1.9433481 matches {eefAnalytics}’s 1.94 to two decimal places.

And the unconditional effect size:

unconditional_eef_style <- g_mlm(
  lme_num,
  p_const = c(0,1,0),
  mod_denom = lme_empty,
  r_const = c(1,1),
  infotype = "expected",
  separate_variances = FALSE
)
unconditional_eef_style
                            est    se
unadjusted effect size    1.047 0.032
adjusted effect size      1.047 0.032
degree of freedom      4077.371      
stopifnot(round(unconditional_eef_style$g_AB, 2) == round(too_small_es, 2))

1.0472727 again matches {eefAnalytics}’s 1.05 to two decimal places.

The CIs:

CI_g(conditional_eef_style) |> round(2)
[1] 1.82 2.07
CI_g(unconditional_eef_style) |> round(2)
[1] 0.99 1.11

The CI matches for the conditional total but not the unconditional total. Here are the corresponding estimates again from {eefAnalytics}:

eef_wrong$ES$treat1["Total", c("95% LB", "95% UB")]
eef_wrong$Unconditional$ES$treat1["Total", c("95% LB", "95% UB")]
beep(2) # Wakey wakey

Session info

sessionInfo()
R version 4.4.1 (2024-06-14 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22631)

Matrix products: default


locale:
[1] LC_COLLATE=English_United Kingdom.utf8  LC_CTYPE=English_United Kingdom.utf8   
[3] LC_MONETARY=English_United Kingdom.utf8 LC_NUMERIC=C                           
[5] LC_TIME=English_United Kingdom.utf8    

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] nlme_3.1-164       eefAnalytics_1.1.1 lmeInfo_0.3.2      beepr_2.0         
 [5] tictoc_1.2.1       lme4_1.1-35.5      Matrix_1.7-0       lubridate_1.9.3   
 [9] forcats_1.0.0      stringr_1.5.1      dplyr_1.1.4        purrr_1.0.2       
[13] readr_2.1.5        tidyr_1.3.1        tibble_3.2.1       ggplot2_3.5.1     
[17] tidyverse_2.0.0   

loaded via a namespace (and not attached):
 [1] gridExtra_2.3        inline_0.3.19        rlang_1.1.4          magrittr_2.0.3      
 [5] matrixStats_1.3.0    compiler_4.4.1       loo_2.8.0            vctrs_0.6.5         
 [9] reshape2_1.4.4       pkgconfig_2.0.3      fastmap_1.2.0        backports_1.5.0     
[13] labeling_0.4.3       utf8_1.2.4           threejs_0.3.3        promises_1.3.0      
[17] markdown_1.13        tzdb_0.4.0           nloptr_2.1.1         xfun_0.46           
[21] jsonlite_1.8.8       later_1.3.2          parallel_4.4.1       R6_2.5.1            
[25] dygraphs_1.1.1.6     stringi_1.8.4        StanHeaders_2.32.10  boot_1.3-30         
[29] Rcpp_1.0.13          rstan_2.32.6         knitr_1.48           zoo_1.8-12          
[33] audio_0.1-11         base64enc_0.1-3      bayesplot_1.11.1     httpuv_1.6.15       
[37] splines_4.4.1        igraph_2.0.3         timechange_0.3.0     tidyselect_1.2.1    
[41] rstudioapi_0.16.0    abind_1.4-5          codetools_0.2-20     miniUI_0.1.1.1      
[45] curl_5.2.1           pkgbuild_1.4.4       lattice_0.22-6       plyr_1.8.9          
[49] shiny_1.9.1          withr_3.0.1          posterior_1.6.0      survival_3.7-0      
[53] RcppParallel_5.1.8   xts_0.14.0           pillar_1.9.0         tensorA_0.36.2.1    
[57] checkmate_2.3.2      DT_0.33              stats4_4.4.1         shinyjs_2.1.0       
[61] distributional_0.4.0 generics_0.1.3       hms_1.1.3            rstantools_2.4.0    
[65] munsell_0.5.1        scales_1.3.0         minqa_1.2.8          gtools_3.9.5        
[69] xtable_1.8-4         glue_1.7.0           tools_4.4.1          shinystan_2.6.0     
[73] colourpicker_1.3.0   mvtnorm_1.2-6        grid_4.4.1           QuickJSR_1.3.1      
[77] crosstalk_1.2.1      colorspace_2.1-1     cli_3.6.3            fansi_1.0.6         
[81] V8_5.0.0             gtable_0.3.5         digest_0.6.36        farver_2.1.2        
[85] htmlwidgets_1.6.4    htmltools_0.5.8.1    lifecycle_1.0.4      mime_0.12           
[89] rstanarm_2.32.1      shinythemes_1.2.0    MASS_7.3-60.2       
LS0tDQp0aXRsZTogIkNvbXBhcmluZyB7X2VlZkFuYWx5dGljc199IGFuZCB7X2xtZUluZm9ffSINCmF1dGhvcjogIkFuZGkgRnVnYXJkIChbQGFuZGlAc2NpZW5jZXMuc29jaWFsXShodHRwczovL3NjaWVuY2VzLnNvY2lhbC9AYW5kaSkpIg0KZGF0ZTogIkxhc3Qga25pdHRlZCBgciBmb3JtYXQoU3lzLkRhdGUoKSwgJyVkICVCICVZJylgIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIGNvZGVfZm9sZGluZzogbm9uZQ0KICBodG1sX2RvY3VtZW50Og0KICAgIGRmX3ByaW50OiBwYWdlZA0KLS0tDQoNCg0KSW4gbXVsdGlsZXZlbCBhbmFseXNlcywgaXQgaXMgb2Z0ZW4gbmVjZXNzYXJ5IHRvIGVzdGltYXRlIGEgc3RhbmRhcmRpc2VkIG1lYW4gZGlmZmVyZW5jZSAoU01EKSBzdWNoIHRoYXQgdGhlIG51bWVyYXRvciBtZWFuIGVmZmVjdCBjb21lcyBmcm9tIGEgZGlmZmVyZW50IG1vZGVsIHRoYW4gdGhlIGRlbm9taW5hdG9yIFNELiBUaGUgbnVtZXJhdG9yIGNvdWxkIGJlIGEgY292YXJpYXRlLWFkanVzdGVkIG1lYW4gZGlmZmVyZW5jZSBhbmQgdGhlIGRlbm9taW5hdG9yIHRoZSBTRCBmcm9tIHRoZSBzdW0gb2YgcmVzaWR1YWwgYW5kIHJhbmRvbSBlZmZlY3QgdmFyaWFuY2VzIGluIGEgbW9kZWwgd2l0aG91dCBjb3ZhcmlhdGUgYWRqdXN0bWVudC4gRm9yIGV4YW1wbGUsICBbc3RhdGlzdGljYWwgYW5hbHlzaXMgZ3VpZGFuY2UgZm9yIEVFRiBldmFsdWF0aW9uc10oaHR0cHM6Ly9lZHVjYXRpb25lbmRvd21lbnRmb3VuZGF0aW9uLm9yZy51ay9wcm9qZWN0cy1hbmQtZXZhbHVhdGlvbi9ldmFsdWF0aW9uL2V2YWx1YXRpb24tZ3VpZGFuY2UtYW5kLXJlc291cmNlcy9ldmFsdWF0aW9uLWRlc2lnbikgcmVxdWlyZXMgdGhpcyBhcHByb2FjaCwgYWxvbmdzaWRlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzLg0KDQpUaGlzIG5vdGUgc2ltdWxhdGVzIGRhdGEgd2l0aCBrbm93biBzYW1wbGUgZWZmZWN0IHNpemUsIGNvdmFyaWF0ZS1vdXRjb21lIGFzc29jaWF0aW9uLCBhbmQgcmFuZG9tIGVmZmVjdCB2YXJpYW5jZXMsIGFuZCBlc3RpbWF0ZXMgdGhlIFNNRCB1c2luZyB0aGUgdHdvIHBhY2thZ2VzIGluIFIgSSBrbm93IHRoYXQgZGlyZWN0bHkgZG8gdGhlIHN1bXM6DQoNCjEuIHtfZWVmQW5hbHl0aWNzX30gaGFzIGEgZnVuY3Rpb24gX2NydEZSRVFfIHRoYXQgd3JhcHMgdXAgY2FsbHMgdG8gX2xtZTQ6OmxtZXJfIGFuZCBjYWxjdWxhdGVzIFNNRHMgdXNpbmcgdG90YWwgYW5kIHJlc2lkdWFsIHZhcmlhbmNlLCB3aXRoIGFuZCB3aXRob3V0IGNvdmFyaWF0ZSBhZGp1c3RtZW50Lg0KMi4ge19sbWVJbmZvX30gaGFzIGEgZnVuY3Rpb24gX2dfbWxtXyB0aGF0IHRha2VzIHR3byBfbmxtZTo6bG1lXyBtb2RlbHMgYW5kIGFsbG93cyB5b3UgdG8gc2VsZWN0IHdoaWNoIGZpeGVkIGFuZCByYW5kb20gZWZmZWN0IHZhcmlhbmNlIGVzdGltYXRlcyBnbyBpbnRvIHRoZSByYXRpby4NCg0KVEw7RFIgc3VtbWFyeTogc3VycHJpc2luZ2x5LCB7X2VlZkFuYWx5dGljc199IGRvZXMgbm90IGZvbGxvdyBFRUYgZ3VpZGFuY2UuIEkgY2FuLCBob3dldmVyLCByZXBsaWNhdGUgd2hhdCBpdCBkb2VzIHVzaW5nIHtfbG1lSW5mb199LiBBbHNvIEkgY2FuIGdldCB7X2xtZUluZm9ffSB0byBwcm92aWRlIHRoZSBhbnN3ZXIgSSBleHBlY3QuDQoNCihJJ20gYXdhcmUgb2YgYXJndW1lbnRzIGFnYWluc3QgU01EcywgZS5nLiwgVHVrZXksIDE5NjksIGFuZCBhcmd1bWVudHMgZm9yIHVzaW5nIGFuIFNEIGZyb20gYSBzdXJ2ZXkgb2YgdGhlIHBvcHVsYXRpb24gb2YgaW50ZXJlc3QuKQ0KDQoNCiMjIEluY2x1ZGUgcmVxdWlyZWQgbGlicmFyaWVzDQoNCmBgYHtyfQ0KcXVpZXRfbGlicmFyeSA8LSBcKC4uLikgbGlicmFyeSguLi4pIHw+IHN1cHByZXNzUGFja2FnZVN0YXJ0dXBNZXNzYWdlcygpDQoNCnF1aWV0X2xpYnJhcnkodGlkeXZlcnNlKQ0KcXVpZXRfbGlicmFyeShsbWU0KQ0KcXVpZXRfbGlicmFyeSh0aWN0b2MpDQpxdWlldF9saWJyYXJ5KGJlZXByKQ0KcXVpZXRfbGlicmFyeShsbWVJbmZvKQ0KcXVpZXRfbGlicmFyeShlZWZBbmFseXRpY3MpDQpxdWlldF9saWJyYXJ5KG5sbWUpDQpxdWlldF9saWJyYXJ5KGxtZUluZm8pDQpgYGANCg0KDQojIyBQYXJhbWV0ZXJzIGZvciB0aGUgc2ltdWxhdGlvbg0KDQpgYGB7cn0NCm5fc2Nob29scyAgPC0gMTAwMA0Kbl9wdXBpbHMgICA8LSAxMDANCm5fdG90YWwgICAgPC0gbl9zY2hvb2xzICogbl9wdXBpbHMNCm5fdHJlYXQgICAgPC0gbl90b3RhbCAvIDINCmNvdl92YXIgICAgPC0gMC42ICAjIHByb3BvcnRpb24gb2YgdmFyaWFuY2UgZXhwbGFpbmVkIGJ5IHRoZSBwdXBpbCBjb3ZhcmlhdGUNCnNjaG9vbF92YXIgPC0gMC4zICAjIHByb3BvcnRpb24gb2YgdmFyaWFuY2UgZXhwbGFpbmVkIGJ5IHNjaG9vbA0KZXMgICAgICAgICA8LSAxLjIzICMgdGhlIGVmZmVjdCBzaXplIHdlIHdhbnQgdG8gc2ltdWxhdGUNCg0Kc3RvcGlmbm90KG5fdG90YWwgJSUgMiA9PSAwKQ0Kc3RvcGlmbm90KG5fc2Nob29scyAlJSAyID09IDApDQpgYGANCg0KDQpUaGUgU01EIHdlJ3JlIGxvb2tpbmcgZm9yIGlzIGByIGVzYCAtLSBrZWVwIGFuIGV5ZSBvdXQgZm9yIHRoYXQgbnVtYmVyIQ0KDQoNCiMjIFNpbXVsYXRlIHRoZSBkYXRhDQoNCk5vdGUgdGhhdCBJJ20gdHJ5aW5nIHRvIGdldCB0aGUgc2FtcGxlIHRvIGhhdmUgZXhhY3RseSB0aGUgcHJvcGVydGllcyBJIHdhbnQsIGhlbmNlIGFsbCB0aGUgc2NhbGluZyB0byBnZXQgc2FtcGxlIFNEID0gMSBhbmQgdXNlIG9mIHJlc2lkdWFscyB0byByZW1vdmUgc2FtcGxlIGNvcnJlbGF0aW9ucy4NCg0KYGBge3J9DQpzZXQuc2VlZCg0MikNCm15X3NjYWxlIDwtIGZ1bmN0aW9uKC4uLikgc2NhbGUoLi4uKSB8PiBhcy5udW1lcmljKCkNCnNjaG9vbF9zaW0gPC0gdGliYmxlKHNjaG9vbCAgICAgICA9IDE6bl9zY2hvb2xzLA0KICAgICAgICAgICAgICAgICAgICAgbm9pc2Vfc2Nob29sID0gcm5vcm0obl9zY2hvb2xzKSkNCg0Kc2ltX2RhdCA8LSB0aWJibGUoDQogIGlkICAgICA9IDE6bl90b3RhbCwNCiAgc2Nob29sID0gcmVwKDE6bl9zY2hvb2xzLCBuX3B1cGlscykgfD4gc29ydCgpLA0KICB0cmVhdCAgPSAoc2Nob29sIDw9IG5fc2Nob29scyAvIDIpICsgMA0KKSB8Pg0KICBsZWZ0X2pvaW4oc2Nob29sX3NpbSwgYnkgPSAic2Nob29sIikgfD4NCiAgbXV0YXRlKA0KICAgIHJhbmVmX3NjaG9vbCA9IG15X3NjYWxlKGxtKG5vaXNlX3NjaG9vbCB+IHRyZWF0KSRyZXNpZCkgKiBzcXJ0KHNjaG9vbF92YXIpLA0KICAgIG5vaXNlMSAgICAgICA9IGxtKHJub3JtKG5fdG90YWwpIH4gcmFuZWZfc2Nob29sICsgZmFjdG9yKHNjaG9vbCkpJHJlc2lkIHw+DQogICAgICAgICAgICAgICAgICAgICBteV9zY2FsZSgpLA0KICAgIG5vaXNlMiAgICAgICA9IGxtKHJub3JtKG5fdG90YWwpIH4gbm9pc2UxICsgcmFuZWZfc2Nob29sICsgZmFjdG9yKHNjaG9vbCkpJHJlc2lkIHw+DQogICAgICAgICAgICAgICAgICAgICBteV9zY2FsZSgpLA0KICAgIHJlc2lkICAgICAgICA9IG5vaXNlMSAqIHNxcnQoMSAtIGNvdl92YXIgLSBzY2hvb2xfdmFyKSwNCiAgICBjb3ZhciAgICAgICAgPSBub2lzZTIgKiBzcXJ0KGNvdl92YXIpLA0KICAgIHkgICAgICAgICAgICA9IHJlc2lkICsgcmFuZWZfc2Nob29sICsgY292YXIgKyBlcyAqIHRyZWF0DQogICkNCmBgYA0KDQoNCiMjIERlc2NyaXB0aXZlcw0KDQpgYGB7cn0NCnNpbV9kYXQgfD4NCiAgc2VsZWN0KC1zdGFydHNfd2l0aCgibm9pc2UiKSkgfD4NCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSBldmVyeXRoaW5nKCkpIHw+DQogIGZpbHRlcighbmFtZSAlaW4lIGMoImlkIiwgInNjaG9vbCIpKSB8Pg0KICBncm91cF9ieShuYW1lKSB8Pg0KICBzdW1tYXJpc2UoDQogICAgbWVhbiA9IG1lYW4odmFsdWUpLA0KICAgIHNkID0gc2QodmFsdWUpLA0KICAgIHZhciA9IHNkXjIsDQogICAgbWluID0gbWluKHZhbHVlKSwNCiAgICBtYXggPSBtYXgodmFsdWUpDQogICkgfD4NCiAgbXV0YXRlKGFjcm9zcyh3aGVyZShpcy5udW1lcmljKSwgXCh4KSByb3VuZCh4LCAyKSkpDQpgYGANCg0KDQpUaGUgU0QgZm9yIF95XyBpcyBncmVhdGVyIHRoYW4gMSBzaW5jZSBfeV8gaXMgYSBtaXh0dXJlIG9mIG5vcm1hbHMuIFdlIGNhbiBnZXQgKG5lYXJseSkgMSBieSBjYWxjdWxhdGluZyB0aGUgU0QgYnkgZ3JvdXAgYW5kIHBvb2xpbmc6DQoNCg0KYGBge3J9DQpieV9ncm91cF9zdW1tYXJ5IDwtIHNpbV9kYXQgfD4NCiAgZ3JvdXBfYnkodHJlYXQpIHw+DQogIHN1bW1hcmlzZShtZWFuX3kgPSBtZWFuKHkpLCBzZF95ID0gc2QoeSksIG4gPSBuKCkpDQpieV9ncm91cF9zdW1tYXJ5IHw+IHJvdW5kKDMpDQpgYGANCg0KDQpgYGB7cn0NCmJ5X2dyb3VwX3N1bW1hcnkkc2RfeV4yIHw+IG1lYW4oKSB8PiBzcXJ0KCkNCmBgYA0KDQpIZXJlJ3MgYSBwaWN0dXJlOg0KDQpgYGB7cn0NCnNpbV9kYXQgfD4NCiAgZ2dwbG90KGFlcyh4ID0geSwgZmlsbCA9IGZhY3Rvcih0cmVhdCwgbGFiZWxzID0gYygiQ29udHJvbCIsICJUcmVhdCIpKSkpICsNCiAgZ2VvbV9oaXN0b2dyYW0oYWxwaGEgPSAwLjUsDQogICAgICAgICAgICAgICAgIHBvc2l0aW9uID0gImlkZW50aXR5IiwNCiAgICAgICAgICAgICAgICAgYmlucyA9IDEwMCkgKw0KICBsYWJzKGZpbGwgPSAiR3JvdXAiLCB4ID0gIk91dGNvbWUiLCB5ID0gIkNvdW50IikgKw0KICB0aGVtZV9taW5pbWFsKCkNCmBgYA0KDQoNCkhlcmUgYXJlIHRoZSBjb3ZhcmlhbmNlczoNCg0KYGBge3J9DQpzaW1fZGF0IHw+DQogIHNlbGVjdCgtc3RhcnRzX3dpdGgoIm5vaXNlIikpIHw+DQogIHNlbGVjdCgtYygiaWQiLCAic2Nob29sIikpIHw+DQogIGNvdigpIHw+DQogIHJvdW5kKDIpIHw+DQogIGFzLmRhdGEuZnJhbWUoKQ0KYGBgDQoNCiMjIHtfZWVmQW5hbHl0aWNzX30NCg0KKEluIGNhc2UgeW91IHRvbyB0cnkgdGhpcyBhcHByb2FjaCwgX2NydEZSRVFfIHdhbnRzIHRoZSByYW5kb20gZWZmZWN0IElEIHRvIGJlIGEgbnVtZXJpYyBhbmQgZG9lc24ndCBsaWtlIGZhY3RvcnMuKQ0KDQpUaGlzIGZpcnN0IGV4YW1wbGUgaW5jbHVkZXMgb25seSB0cmVhdG1lbnQgZ3JvdXAgYW5kIHJhbmRvbSBlZmZlY3QsIHdpdGggbm8gY292YXJpYXRlLiBJdCBzaG91bGQgcHJvdmlkZSB0aGUgY29ycmVjdCBhbnN3ZXIsIHRob3VnaCB0aGlzIGlzbid0IGdlbmVyYWxseSBhIGdvb2QgaWRlYSBhcyB0aGUgY29uZmlkZW5jZSBpbnRlcnZhbCB3aWxsIGJlIHRvbyB3aWRlIGlmIHlvdXIgY292YXJpYXRlcyBleHBsYWluIHZhcmlhdGlvbiBpbiB0aGUgb3V0Y29tZToNCg0KYGBge3J9DQplZWZhX3VuY29uZCA8LSBjcnRGUkVRKA0KICB5IH4gdHJlYXQsDQogIHJhbmRvbSA9ICJzY2hvb2wiLA0KICBpbnRlcnZlbnRpb24gPSAidHJlYXQiLA0KICBkYXRhID0gc2ltX2RhdA0KKQ0KZWVmYV91bmNvbmQNCmBgYA0KDQpgYGB7cn0NCnNob3VsZF9iZV9jb3JyZWN0IDwtIGVlZmFfdW5jb25kJEVTJHRyZWF0MVsiVG90YWwiLF0kRXN0aW1hdGUNCnN0b3BpZm5vdChyb3VuZChzaG91bGRfYmVfY29ycmVjdCwgMikgPT0gcm91bmQoZXMsIDIpKQ0KYGBgDQoNCkFzIGV4cGVjdGVkLCB0aGUgY29uZGl0aW9uYWwgZWZmZWN0IHNpemUgdXNpbmcgdGhlIHRvdGFsIHZhcmlhbmNlIGlzIGByIHNob3VsZF9iZV9jb3JyZWN0YCwgbWF0Y2hpbmcgdGhlIHRhcmdldC4NCg0KVGhlIGZvbGxvd2luZyBjYWxsIHdpdGggdGhlIGNvdmFyaWF0ZSBpbmNsdWRlZCB5aWVsZHMgdG9vIGxhcmdlIGFuIGFuc3dlciBmb3IgdGhlIGNvbmRpdGlvbmFsIHRvdGFsIGFuZCB0b28gc21hbGwgYW4gYW5zd2VyIGZvciB0aGUgdW5jb25kaXRpb25hbCB0b3RhbCBlZmZlY3Q6DQoNCmBgYHtyfQ0KZWVmX3dyb25nIDwtIGNydEZSRVEoDQogIHkgfiB0cmVhdCArIGNvdmFyLA0KICByYW5kb20gPSAic2Nob29sIiwNCiAgaW50ZXJ2ZW50aW9uID0gInRyZWF0IiwNCiAgZGF0YSA9IHNpbV9kYXQNCikNCmVlZl93cm9uZw0KYGBgDQoNCmBgYHtyfQ0KdG9vX2JpZ19lcyAgIDwtIGVlZl93cm9uZyRFUyR0cmVhdDFbIlRvdGFsIixdJEVzdGltYXRlDQp0b29fc21hbGxfZXMgPC0gZWVmX3dyb25nJFVuY29uZGl0aW9uYWwkRVMkdHJlYXQxWyJUb3RhbCIsXSRFc3RpbWF0ZQ0Kc3RvcGlmbm90KHRvb19iaWdfZXMgPiBlcykNCnN0b3BpZm5vdCh0b29fc21hbGxfZXMgPCBlcykNCmBgYA0KDQoNCldlJ3JlIGFpbWluZyBmb3IgYHIgZXNgLiBUaGUgdG90YWwgY29uZGl0aW9uYWwgZWZmZWN0IHNpemUgaXMgYHIgdG9vX2JpZ19lc2AgYW5kIHRoZSB1bmNvbmRpdGlvbmFsIGlzIGByIHRvb19zbWFsbF9lc2AuDQoNClRoZSBtb2RlbCBiZWhpbmQgdGhlIGNvbmRpdGlvbmFsIGVmZmVjdCBzaXplIHNocmlua3MgdGhlIHJlc2lkdWFscywgYXMgdGhlIGNvdmFyaWF0ZSBleHBsYWlucyB2YXJpYW5jZSBpbiB0aGUgb3V0Y29tZS4gVGhlIGRlbm9taW5hdG9yIGlzIHNtYWxsZXIgdGhhbiB3ZSB3YW50LCBzbyB0aGUgU01EIGlzIHRvbyBsYXJnZS4NCg0KVGhlIG1vZGVsIGJlaGluZCB0aGUgdW5jb25kaXRpb25hbCBlZmZlY3Qgc2l6ZSBkZW5vbWluYXRvciBkb2Vzbid0IHRha2UgYWNjb3VudCBvZiB0aGUgaW50ZXJ2ZW50aW9uIGluY3JlYXNpbmcgdGhlIGJldHdlZW4tc2Nob29sIHZhcmlhbmNlIGFzIGl0IHB1c2hlcyB0aGUgbWVhbnMgYXBhcnQuIFdlIGNhbiBzZWUgdGhhdCBpZiB3ZSBleGFtaW5lIHRoZSBzY2hvb2wgcmFuZG9tIGVmZmVjdHMgaW4gdGhlIHVuY29uZGl0aW9uYWwgbW9kZWw6DQoNCmBgYHtyfQ0KdW5jb25kX2xtZXIgPC0gbG1lcih5IH4gKDF8c2Nob29sKSwgZGF0YSA9IHNpbV9kYXQpDQpzdW1tYXJ5KHVuY29uZF9sbWVyKQ0KYGBgDQoNCkhlcmUncyBhIHBpY3R1cmUgb2YgdGhlIHNjaG9vbCByYW5kb20gZWZmZWN0cyBzaG93aW5nIHdoYXQncyBnb2luZyBvbjoNCg0KYGBge3J9DQpwbG90X3JhbmVmcyA8LSBmdW5jdGlvbihtb2QpIHsNCnNjaG9vbF9yYW5lZnMgPC0gcmFuZWYobW9kKSRzY2hvb2wgfD4NCiAgcmVuYW1lKCJpbnRlcmNlcHQiID0gIihJbnRlcmNlcHQpIikgfD4NCiAgcm93bmFtZXNfdG9fY29sdW1uKCJzY2hvb2wiKSB8Pg0KICBtdXRhdGUoc2Nob29sID0gYXMubnVtZXJpYyhzY2hvb2wpLA0KICAgICAgICAgdHJlYXQgID0gKHNjaG9vbCA8PSBuX3NjaG9vbHMgLyAyKSArIDApDQoNCnNjaG9vbF9yYW5lZnMgfD4NCiAgZ2dwbG90KGFlcyh4ID0gaW50ZXJjZXB0LCBmaWxsID0gZmFjdG9yKHRyZWF0LCBsYWJlbHMgPSBjKCJDb250cm9sIiwgIlRyZWF0IikpKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShhbHBoYSA9IDAuNSwNCiAgICAgICAgICAgICAgICAgcG9zaXRpb24gPSAiaWRlbnRpdHkiLA0KICAgICAgICAgICAgICAgICBiaW5zID0gMjEpICsNCiAgbGFicyhmaWxsID0gIkdyb3VwIiwgeCA9ICJTY2hvb2wgcmFuZG9tIGVmZmVjdCIsIHkgPSAiQ291bnQiKSArDQogIHRoZW1lX21pbmltYWwoKQ0KfQ0KcGxvdF9yYW5lZnModW5jb25kX2xtZXIpDQpgYGANCg0KTGV0J3MgbG9vayBhZ2FpbiwgdGhpcyB0aW1lIHVzaW5nIGEgbW9kZWwgdGhhdCBpbmNsdWRlcyB0aGUgdHJlYXRtZW50IHByZWRpY3RvcjoNCg0KYGBge3J9DQp0cmVhdF9sbWVyIDwtIGxtZXIoeSB+IHRyZWF0ICsgKDF8c2Nob29sKSwgZGF0YSA9IHNpbV9kYXQpDQpzdW1tYXJ5KHRyZWF0X2xtZXIpDQpgYGANCg0KTm90ZSBob3cgbXVjaCBzbWFsbGVyIHRoZSBzY2hvb2wgcmFuZG9tIGVmZmVjdCB2YXJpYW5jZSBpcyBub3cgKGFsbW9zdCB0aGUgYHIgc2Nob29sX3ZhcmAgSSB3YXMgb3JpZ2luYWxseSBhaW1pbmcgZm9yIC0tIHNlZSB0aGUgc2ltdWxhdGlvbiBwYXJhbWV0ZXJzKS4gVGhlIHBpY3R1cmU6DQoNCmBgYHtyfQ0KcGxvdF9yYW5lZnModHJlYXRfbG1lcikNCmBgYA0KDQpUaGlzIGlzIHRoZSBtb2RlbCB3ZSB3YW50IGZvciB0aGUgZGVub21pbmF0b3IuDQoNCg0KIyMge19sbWVJbmZvX30NCg0KTGV0J3Mgc2VlIGlmIHtfbG1lSW5mb199IGNhbiBwcm9kdWNlIHRoZSBTTUQgd2Ugd2FudC4gRmlyc3QsIGZpdCBhIG1vZGVsIGZvciB0aGUgbnVtZXJhdG9yIChlZmZlY3QgZXN0aW1hdGUpIGFuZCBkZW5vbWluYXRvciAoU0QpLiBJJ3ZlIGFsc28gZml0dGVkIGFuIGludGVyY2VwdC1vbmx5IG1vZGVsLCBmb3IgdXNlIGxhdGVyIHdoZW4gcmVwbGljYXRpbmcge19lZWZBbmFseXRpY3NffSdzIHJlc3VsdHMuDQoNCmBgYHtyfQ0KbG1lX251bSA8LSBsbWUoDQogIGZpeGVkID0geSB+IHRyZWF0ICsgY292YXIsDQogIHJhbmRvbSA9IH4gMSB8IHNjaG9vbCwNCiAgZGF0YSA9IHNpbV9kYXQsDQogIG1ldGhvZCA9ICJSRU1MIiwNCiAgY29udHJvbCA9IGxtZUNvbnRyb2wob3B0ID0gIm9wdGltIikNCikNCg0KbG1lX2RlbiA8LSBsbWUoDQogIGZpeGVkID0geSB+IHRyZWF0LA0KICByYW5kb20gPSB+IDEgfCBzY2hvb2wsDQogIGRhdGEgPSBzaW1fZGF0LA0KICBtZXRob2QgPSAiUkVNTCIsDQogIGNvbnRyb2wgPSBsbWVDb250cm9sKG9wdCA9ICJvcHRpbSIpDQopDQoNCmxtZV9lbXB0eSA8LSBsbWUoDQogIGZpeGVkID0geSB+IDEsDQogIHJhbmRvbSA9IH4gMSB8IHNjaG9vbCwNCiAgZGF0YSA9IHNpbV9kYXQsDQogIG1ldGhvZCA9ICJSRU1MIiwNCiAgY29udHJvbCA9IGxtZUNvbnRyb2wob3B0ID0gIm9wdGltIikNCikNCmBgYA0KDQoNCkVzdGltYXRlIHRoZSBTTUQ6DQoNCmBgYHtyfQ0KY29ycmVjdF9lc19lc3QgPC0gZ19tbG0oDQogIGxtZV9udW0sDQogIHBfY29uc3QgPSBjKDAsMSwwKSwNCiAgbW9kX2Rlbm9tID0gbG1lX2RlbiwNCiAgcl9jb25zdCA9IGMoMSwxKSwNCiAgaW5mb3R5cGUgPSAiZXhwZWN0ZWQiLA0KICBzZXBhcmF0ZV92YXJpYW5jZXMgPSBGQUxTRQ0KKQ0KY29ycmVjdF9lc19lc3QNCmBgYA0KDQpgYGB7cn0NCnN0b3BpZm5vdChyb3VuZChjb3JyZWN0X2VzX2VzdCRnX0FCLCAzKSA9PSBlcykNCmBgYA0KDQpUaGUgZXN0aW1hdGUgaXMgYXMgZGVzaXJlZCwgdG8gdGhyZWUgZGVjaW1hbCBwbGFjZXMuIEluIGZ1bGw6IA0KDQpgYGB7cn0NCmNvcnJlY3RfZXNfZXN0JGdfQUINCmBgYA0KDQpXZSBjYW4gYWxzbyBnZXQgYSBDSToNCg0KYGBge3J9DQpDSV9nKGNvcnJlY3RfZXNfZXN0KSB8PiByb3VuZCgyKQ0KYGBgDQoNCg0KIyMgUmVwbGljYXRpbmcge19lZWZBbmFseXRpY3NffSB1c2luZyB7X2xtZUluZm9ffQ0KDQpMZXQncyBzZWUgaWYgd2UgY2FuIHJlcGxpY2F0ZSB7X2VlZkFuYWx5dGljc199LiBIZXJlJ3MgdGhlIGNvbmRpdGlvbmFsIGVmZmVjdCBzaXplOg0KDQpgYGB7cn0NCmNvbmRpdGlvbmFsX2VlZl9zdHlsZSA8LSBnX21sbSgNCiAgbG1lX251bSwNCiAgcF9jb25zdCA9IGMoMCwxLDApLA0KICBtb2RfZGVub20gPSBsbWVfbnVtLA0KICByX2NvbnN0ID0gYygxLDEpLA0KICBpbmZvdHlwZSA9ICJleHBlY3RlZCIsDQogIHNlcGFyYXRlX3ZhcmlhbmNlcyA9IEZBTFNFDQopDQpjb25kaXRpb25hbF9lZWZfc3R5bGUNCmBgYA0KDQpgYGB7cn0NCnN0b3BpZm5vdChyb3VuZChjb25kaXRpb25hbF9lZWZfc3R5bGUkZGVsdGFfQUIsIDIpID09IHJvdW5kKHRvb19iaWdfZXMsIDIpKQ0KYGBgDQoNCg0KYHIgY29uZGl0aW9uYWxfZWVmX3N0eWxlJGRlbHRhX0FCYCBtYXRjaGVzIHtfZWVmQW5hbHl0aWNzX30ncyBgciB0b29fYmlnX2VzYCB0byB0d28gZGVjaW1hbCBwbGFjZXMuDQoNCkFuZCB0aGUgdW5jb25kaXRpb25hbCBlZmZlY3Qgc2l6ZToNCg0KYGBge3J9DQp1bmNvbmRpdGlvbmFsX2VlZl9zdHlsZSA8LSBnX21sbSgNCiAgbG1lX251bSwNCiAgcF9jb25zdCA9IGMoMCwxLDApLA0KICBtb2RfZGVub20gPSBsbWVfZW1wdHksDQogIHJfY29uc3QgPSBjKDEsMSksDQogIGluZm90eXBlID0gImV4cGVjdGVkIiwNCiAgc2VwYXJhdGVfdmFyaWFuY2VzID0gRkFMU0UNCikNCnVuY29uZGl0aW9uYWxfZWVmX3N0eWxlDQpgYGANCg0KYGBge3J9DQpzdG9waWZub3Qocm91bmQodW5jb25kaXRpb25hbF9lZWZfc3R5bGUkZ19BQiwgMikgPT0gcm91bmQodG9vX3NtYWxsX2VzLCAyKSkNCmBgYA0KDQoNCmByIHVuY29uZGl0aW9uYWxfZWVmX3N0eWxlJGdfQUJgIGFnYWluIG1hdGNoZXMge19lZWZBbmFseXRpY3NffSdzIGByIHRvb19zbWFsbF9lc2AgdG8gdHdvIGRlY2ltYWwgcGxhY2VzLg0KDQoNClRoZSBDSXM6DQoNCmBgYHtyfQ0KQ0lfZyhjb25kaXRpb25hbF9lZWZfc3R5bGUpIHw+IHJvdW5kKDIpDQpDSV9nKHVuY29uZGl0aW9uYWxfZWVmX3N0eWxlKSB8PiByb3VuZCgyKQ0KYGBgDQoNClRoZSBDSSBtYXRjaGVzIGZvciB0aGUgY29uZGl0aW9uYWwgdG90YWwgYnV0IG5vdCB0aGUgdW5jb25kaXRpb25hbCB0b3RhbC4gSGVyZSBhcmUgdGhlIGNvcnJlc3BvbmRpbmcgZXN0aW1hdGVzIGFnYWluIGZyb20ge19lZWZBbmFseXRpY3NffToNCg0KYGBge3J9DQplZWZfd3JvbmckRVMkdHJlYXQxWyJUb3RhbCIsIGMoIjk1JSBMQiIsICI5NSUgVUIiKV0NCmBgYA0KDQpgYGB7cn0NCmVlZl93cm9uZyRVbmNvbmRpdGlvbmFsJEVTJHRyZWF0MVsiVG90YWwiLCBjKCI5NSUgTEIiLCAiOTUlIFVCIildDQpgYGANCg0KYGBge3J9DQpiZWVwKDIpICMgV2FrZXkgd2FrZXkNCmBgYA0KDQoNCg0KIyMgU2Vzc2lvbiBpbmZvDQoNCmBgYHtyfQ0Kc2Vzc2lvbkluZm8oKQ0KYGBgDQoNCg==