Creative Commons License

library(ggplot2)
library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
## ✔ tibble  3.1.4     ✔ dplyr   1.0.7
## ✔ tidyr   1.1.3     ✔ stringr 1.4.0
## ✔ readr   1.4.0     ✔ forcats 0.5.1
## ✔ purrr   0.3.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()

1 Background

Histologic grade in breast cancer provides clinically important prognostic information. Researchers examined whether histologic grade was associated with gene expression profiles of breast cancers and whether such profiles could be used to improve histologic grading. In this tutorial we will assess the association between histologic grade and the expression of the KPNA2 gene that is known to be associated with poor BC prognosis. The patients, however, do not only differ in the histologic grade, but also on their lymph node status. The lymph nodes were not affected (0) or chirugically removed (1).

  • Redo data analysis (you can copy the results of the tutorial on multiple linear regression)
  • What is the power to pick up each of the contrasts when their real effect sizes would be equal to the effect sizes we observed in the study?
  • How does the power evolves if we have 2 upto 10 repeats for each factor combination of grade and node when their real effect sizes would be equal to the ones we observed in the study?
  • What is the power to pick up each of the contrasts when the FC for grade for patients with unaffected lymf nodes equals 1.5 (\(\beta_g = log2(1.5)\))?

2 Data analysis

2.1 Import KPNA2 data in R

kpna2 <- read.table("https://raw.githubusercontent.com/statOmics/SGA21/master/data/kpna2.txt",header=TRUE)
kpna2

Because histolic grade and lymph node status are both categorical variables, we model them both as factors.

kpna2$grade <- as.factor(kpna2$grade)
kpna2$node <- as.factor(kpna2$node)
kpna2 %>% 
  ggplot(aes(x=node:grade,y=gene,fill=node:grade)) +
  geom_boxplot(outlier.shape = NA) +
  geom_jitter()

As discussed in a previous exercise, it seems that there is both an effect of histologic grade and lymph node status on the gene expression. There also seems to be a different effect of lymph node status on the gene expression for the different histologic grades.

As we saw before, we can model this with a model that contains both histologic grade, lymph node status and the interaction term between both. When checking the linear model assumptions, we see that the variance is not equal. Therefore we model the gene expression with a log2-transformation, which makes that all the assumptions of the linear model are satisfied.

#Model with main effects for histological grade and node and grade x node interaction
fit <- lm(gene~grade*node,data=kpna2)
summary(fit)
## 
## Call:
## lm(formula = gene ~ grade * node, data = kpna2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -201.748  -53.294   -6.308   46.216  277.601 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    180.51      44.37   4.068   0.0006 ***
## grade3         401.33      62.75   6.396 3.07e-06 ***
## node1          103.98      62.75   1.657   0.1131    
## grade3:node1  -145.57      88.74  -1.640   0.1166    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 108.7 on 20 degrees of freedom
## Multiple R-squared:  0.7437, Adjusted R-squared:  0.7052 
## F-statistic: 19.34 on 3 and 20 DF,  p-value: 3.971e-06
plot(fit)

fit <- lm(gene %>% log2~grade*node,data=kpna2)
plot(fit)

When checking the significance of the interaction term, we see that it is significant on the 5% significance level. We therefore keep the full model.

library(car)
## Loading required package: carData
## 
## Attaching package: 'car'
## The following object is masked from 'package:dplyr':
## 
##     recode
## The following object is masked from 'package:purrr':
## 
##     some
Anova(fit,type="III")

As we are dealing with a factorial design, we can calculate the mean gene expression for each group by the following parameter summations.

ExploreModelMatrix::VisualizeDesign(kpna2,~grade*node)$plotlist
## [[1]]

The researchers want to know the power for testing following hypotheses (remark that we will have to adjust for multiple testing):

  • Log fold change between histologic grade 3 and histologic grade 1 for patients with unaffected lymph nodes (=0).

\[H_0: \log_2{FC}_{g3n0-g1n0} = \beta_{g3} = 0\]

  • Log fold change between histologic grade 3 and histologic grade 1 for patients with removed lymph nodes (=1).

\[H_0: \log_2{FC}_{g3n1-g1n1} = \beta_{g3} + \beta_{g3n1} = 0\]

  • Log fold change between unaffected and removed lymph nodes for patients of histologic grade 1.

\[H_0: \log_2{FC}_{g1n1-g1n0} = \beta_{n1} = 0\]

  • Log fold change between unaffected and removed lymph nodes for patients of histologic grade 3.

\[H_0: \log_2{FC}_{g3n1-g3n0} = \beta_{n1} + \beta_{g3n1} = 0\]

  • Difference in log fold change between patients of histological grade 3 and histological grade 1 with removed lymph nodes and log fold change between patients between patients of histological grade 3 and histological grade 1 with unaffected lymph nodes.

\[H_0: \log_2{FC}_{g3n1-g1n1} - \log_2{FC}_{g3n0-g1n0} = \beta_{g3n1} = 0\] which is an equivalent hypotheses with \[H_0: \log_2{FC}_{g3n1-g3n0} - \log_2{FC}_{g1n1-g1n0} = \beta_{g3n1} = 0\]

We can test this using multcomp, which controls for multiple testing.

library(multcomp)
## Loading required package: mvtnorm
## Loading required package: survival
## Loading required package: TH.data
## Loading required package: MASS
## 
## Attaching package: 'MASS'
## The following object is masked from 'package:dplyr':
## 
##     select
## 
## Attaching package: 'TH.data'
## The following object is masked from 'package:MASS':
## 
##     geyser
mcp <- glht(fit,linfct = c("grade3 = 0","grade3 + grade3:node1 = 0", "node1 = 0","node1 + grade3:node1 = 0", "grade3:node1 = 0"))
summary(mcp)
## 
##   Simultaneous Tests for General Linear Hypotheses
## 
## Fit: lm(formula = gene %>% log2 ~ grade * node, data = kpna2)
## 
## Linear Hypotheses:
##                            Estimate Std. Error t value Pr(>|t|)    
## grade3 == 0                  1.6675     0.1827   9.127  < 0.001 ***
## grade3 + grade3:node1 == 0   0.8928     0.1827   4.886  < 0.001 ***
## node1 == 0                   0.6577     0.1827   3.600  0.00711 ** 
## node1 + grade3:node1 == 0   -0.1170     0.1827  -0.640  0.89817    
## grade3:node1 == 0           -0.7748     0.2584  -2.998  0.02658 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## (Adjusted p values reported -- single-step method)

We get a significant p-value for the first, second, third and fifth hypothesis. The fourth hypothesis is not significant at the overall 5% significance level.

3 Power of the tests for each of the contrasts

3.1 Simulation function

Function to simulate data similar to that of our experiment under our model assumptions.

simFastMultipleContrasts <- function(form, data, betas, sd, contrasts, alpha = .05, nSim = 10000, adjust = "bonferroni")
{
    ySim <- rnorm(nrow(data)*nSim,sd=sd)
    dim(ySim) <-c(nrow(data),nSim)
    design <- model.matrix(form, data)
    ySim <- ySim + c(design %*%betas)
    ySim <- t(ySim)
  
    ### Fitting
    fitAll <- limma::lmFit(ySim,design)
  
    ### Inference
    varUnscaled <- t(contrasts)%*%fitAll$cov.coefficients%*%contrasts
    contrasts <- fitAll$coefficients %*%contrasts
    seContrasts <- matrix(diag(varUnscaled)^.5,nrow=nSim,ncol=5,byrow = TRUE)*fitAll$sigma
    tstats <- contrasts/seContrasts
    pvals <- pt(abs(tstats),fitAll$df.residual,lower.tail = FALSE)*2
    pvals <- t(apply(pvals, 1, p.adjust, method = adjust))
    
    return(colMeans(pvals < alpha))
}

3.2 power of current experiment

nSim <- 20000
betas <- fit$coefficients
sd <- sigma(fit)
contrasts <- matrix(0,nrow=4,ncol=5)
rownames(contrasts) <- names(fit$coefficients)
colnames(contrasts) <- c("graden0","graden1","nodeg1","nodeg3","interaction")
contrasts[2,1] <- 1
contrasts[c(2,4),2] <- 1
contrasts[3,3] <- 1
contrasts[c(3,4),4] <- 1
contrasts[4,5] <- 1
form <- ~ grade*node
power1 <- simFastMultipleContrasts(form, kpna2, betas, sd, contrasts, nSim = nSim)
power1
##     graden0     graden1      nodeg1      nodeg3 interaction 
##     1.00000     0.97005     0.76715     0.02380     0.57315

We observe large powers for all contrasts, except for contrast nodeg3, which has a small effect size.

3.3 Power for increasing sample size

nSim <- 20000
betas <- fit$coefficients
sd <- sigma(fit)
contrasts <- matrix(0,nrow=4,ncol=5)
rownames(contrasts) <- names(fit$coefficients)
colnames(contrasts) <- c("graden0","graden1","nodeg1","nodeg3","interaction")
contrasts[2,1] <- 1
contrasts[c(2,4),2] <- 1
contrasts[3,3] <- 1
contrasts[c(3,4),4] <- 1
contrasts[4,5] <- 1
form <- ~ grade*node

powers <- matrix(NA,nrow=9, ncol=6)
colnames(powers) <- c("n",colnames(contrasts))
powers[,1] <- 2:10

dataAllComb <- data.frame(grade = rep(c(1,3),each=2)%>% as.factor, 
                          node = rep(c(0,1),2)%>%as.factor) 
  
for (i in 1:nrow(powers))
{  
predData <- data.frame(grade = rep(dataAllComb$grade, powers[i,1]), 
                       node = rep(dataAllComb$node, powers[i,1]))
powers[i,-1] <- simFastMultipleContrasts(form, predData, betas, sd, contrasts, nSim = nSim)
}
powers
##        n graden0 graden1  nodeg1  nodeg3 interaction
##  [1,]  2 0.69955 0.20985 0.10980 0.01210     0.07100
##  [2,]  3 0.99265 0.56885 0.29765 0.01505     0.19005
##  [3,]  4 0.99990 0.80425 0.48150 0.01825     0.32385
##  [4,]  5 1.00000 0.92205 0.64395 0.02090     0.45290
##  [5,]  6 1.00000 0.96985 0.76520 0.02460     0.56785
##  [6,]  7 1.00000 0.99105 0.85340 0.02775     0.66360
##  [7,]  8 1.00000 0.99640 0.90625 0.03030     0.75270
##  [8,]  9 1.00000 0.99905 0.94405 0.03495     0.81640
##  [9,] 10 1.00000 0.99960 0.96950 0.03805     0.87120
powers %>% 
  as.data.frame %>% 
  gather(contrast, power, -n) %>% 
  ggplot(aes(n,power,color=contrast)) +
  geom_line()

3.4 Power when FC for grade in patients with unaffected lymph nodes equals 1.5 (\(\beta_g\) = 1.5)

nSim <- 20000
betas2 <- fit$coefficients
betas2["grade3"] <- log2(1.5)
sd <- sigma(fit)
contrasts <- matrix(0,nrow=4,ncol=5)
rownames(contrasts) <- names(fit$coefficients)
colnames(contrasts) <- c("graden0","graden1","nodeg1","nodeg3","interaction")
contrasts[2,1] <- 1
contrasts[c(2,4),2] <- 1
contrasts[3,3] <- 1
contrasts[c(3,4),4] <- 1
contrasts[4,5] <- 1
form <- ~ grade*node
power3 <- simFastMultipleContrasts(form, kpna2, betas2, sd, contrasts,nSim = nSim)
power3
##     graden0     graden1      nodeg1      nodeg3 interaction 
##     0.64100     0.05465     0.76170     0.02465     0.56510

It is clear that only the power of the contrasts containing \(\beta_g\) change when the effect size of \(\beta_g\) changes.

LS0tCnRpdGxlOiAiRXhwZXJpbWVudGVlbCBEZXNpZ24gSUk6IHJlcGxpY2F0aWUgZW4gcG93ZXIgLSBLUE5BMiIKYXV0aG9yOiAiTGlldmVuIENsZW1lbnQgJiBBbGV4YW5kcmUgU2VnZXJzIgpkYXRlOiAic3RhdE9taWNzLCBHaGVudCBVbml2ZXJzaXR5IChodHRwczovL3N0YXRvbWljcy5naXRodWIuaW8pIgpvdXRwdXQ6CiAgaHRtbF9kb2N1bWVudDoKICAgIGNvZGVfZG93bmxvYWQ6IHllcwogICAgdGhlbWU6IGNvc21vCiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKICAgIGhpZ2hsaWdodDogdGFuZ28KICAgIG51bWJlcl9zZWN0aW9uczogeWVzCiAgcGRmX2RvY3VtZW50OgogICAgdG9jOiB0cnVlCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUKICAgIGxhdGV4X2VuZ2luZTogeGVsYXRleAotLS0KCgo8YSByZWw9ImxpY2Vuc2UiIGhyZWY9Imh0dHBzOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy1zYS80LjAiPjxpbWcgYWx0PSJDcmVhdGl2ZSBDb21tb25zIExpY2Vuc2UiIHN0eWxlPSJib3JkZXItd2lkdGg6MCIgc3JjPSJodHRwczovL2kuY3JlYXRpdmVjb21tb25zLm9yZy9sL2J5LW5jLXNhLzQuMC84OHgzMS5wbmciIC8+PC9hPgoKCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpCmBgYAoKCmBgYHtyfQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkodGlkeXZlcnNlKQpgYGAKCiMgQmFja2dyb3VuZAoKSGlzdG9sb2dpYyBncmFkZSBpbiBicmVhc3QgY2FuY2VyIHByb3ZpZGVzIGNsaW5pY2FsbHkgaW1wb3J0YW50IHByb2dub3N0aWMgaW5mb3JtYXRpb24uIFJlc2VhcmNoZXJzIGV4YW1pbmVkIHdoZXRoZXIgaGlzdG9sb2dpYyBncmFkZSB3YXMgYXNzb2NpYXRlZCB3aXRoIGdlbmUgZXhwcmVzc2lvbiBwcm9maWxlcyBvZiBicmVhc3QgY2FuY2VycyBhbmQgd2hldGhlciBzdWNoIHByb2ZpbGVzIGNvdWxkIGJlIHVzZWQgdG8gaW1wcm92ZSBoaXN0b2xvZ2ljIGdyYWRpbmcuIEluIHRoaXMgdHV0b3JpYWwgd2Ugd2lsbCBhc3Nlc3MgdGhlIGFzc29jaWF0aW9uIGJldHdlZW4gaGlzdG9sb2dpYyBncmFkZSBhbmQgdGhlIGV4cHJlc3Npb24gb2YgdGhlIEtQTkEyIGdlbmUgdGhhdCBpcyBrbm93biB0byBiZSBhc3NvY2lhdGVkIHdpdGggcG9vciBCQyBwcm9nbm9zaXMuClRoZSBwYXRpZW50cywgaG93ZXZlciwgZG8gbm90IG9ubHkgZGlmZmVyIGluIHRoZSBoaXN0b2xvZ2ljIGdyYWRlLCBidXQgYWxzbyBvbiB0aGVpciBseW1waCBub2RlIHN0YXR1cy4gClRoZSBseW1waCBub2RlcyB3ZXJlIG5vdCBhZmZlY3RlZCAoMCkgb3IgY2hpcnVnaWNhbGx5IHJlbW92ZWQgKDEpLgoKLSBSZWRvIGRhdGEgYW5hbHlzaXMgKHlvdSBjYW4gY29weSB0aGUgcmVzdWx0cyBvZiB0aGUgdHV0b3JpYWwgb24gbXVsdGlwbGUgbGluZWFyIHJlZ3Jlc3Npb24pCi0gV2hhdCBpcyB0aGUgcG93ZXIgdG8gcGljayB1cCBlYWNoIG9mIHRoZSBjb250cmFzdHMgd2hlbiB0aGVpciByZWFsIGVmZmVjdCBzaXplcyB3b3VsZCBiZSBlcXVhbCB0byB0aGUgZWZmZWN0IHNpemVzIHdlIG9ic2VydmVkIGluIHRoZSBzdHVkeT8gCi0gSG93IGRvZXMgdGhlIHBvd2VyIGV2b2x2ZXMgaWYgd2UgaGF2ZSAyIHVwdG8gMTAgcmVwZWF0cyBmb3IgZWFjaCBmYWN0b3IgY29tYmluYXRpb24gb2YgZ3JhZGUgYW5kIG5vZGUgd2hlbiB0aGVpciByZWFsIGVmZmVjdCBzaXplcyB3b3VsZCBiZSBlcXVhbCB0byB0aGUgb25lcyB3ZSBvYnNlcnZlZCBpbiB0aGUgc3R1ZHk/IAotIFdoYXQgaXMgdGhlIHBvd2VyIHRvIHBpY2sgdXAgZWFjaCBvZiB0aGUgY29udHJhc3RzIHdoZW4gdGhlIEZDIGZvciBncmFkZSBmb3IgcGF0aWVudHMgd2l0aCB1bmFmZmVjdGVkIGx5bWYgbm9kZXMgZXF1YWxzIDEuNSAoJFxiZXRhX2cgPSBsb2cyKDEuNSkkKT8gCgoKIyBEYXRhIGFuYWx5c2lzCiMjIEltcG9ydCBLUE5BMiBkYXRhIGluIFIKYGBge3J9CmtwbmEyIDwtIHJlYWQudGFibGUoImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9zdGF0T21pY3MvU0dBMjEvbWFzdGVyL2RhdGEva3BuYTIudHh0IixoZWFkZXI9VFJVRSkKa3BuYTIKYGBgCgpCZWNhdXNlIGhpc3RvbGljIGdyYWRlIGFuZCBseW1waCBub2RlIHN0YXR1cyBhcmUgYm90aCBjYXRlZ29yaWNhbCB2YXJpYWJsZXMsIHdlIG1vZGVsIHRoZW0gYm90aCBhcyBmYWN0b3JzLgoKYGBge3J9CmtwbmEyJGdyYWRlIDwtIGFzLmZhY3RvcihrcG5hMiRncmFkZSkKa3BuYTIkbm9kZSA8LSBhcy5mYWN0b3Ioa3BuYTIkbm9kZSkKYGBgCgpgYGB7cn0Ka3BuYTIgJT4lIAogIGdncGxvdChhZXMoeD1ub2RlOmdyYWRlLHk9Z2VuZSxmaWxsPW5vZGU6Z3JhZGUpKSArCiAgZ2VvbV9ib3hwbG90KG91dGxpZXIuc2hhcGUgPSBOQSkgKwogIGdlb21faml0dGVyKCkKYGBgCgpBcyBkaXNjdXNzZWQgaW4gYSBwcmV2aW91cyBleGVyY2lzZSwgaXQgc2VlbXMgdGhhdCB0aGVyZSBpcyBib3RoIGFuIGVmZmVjdCBvZiBoaXN0b2xvZ2ljIGdyYWRlIGFuZCBseW1waCBub2RlIHN0YXR1cyBvbiB0aGUgZ2VuZSBleHByZXNzaW9uLiBUaGVyZSBhbHNvIHNlZW1zIHRvIGJlIGEgZGlmZmVyZW50IGVmZmVjdCBvZiBseW1waCBub2RlIHN0YXR1cyBvbiB0aGUgZ2VuZSBleHByZXNzaW9uIGZvciB0aGUgZGlmZmVyZW50IGhpc3RvbG9naWMgZ3JhZGVzLgoKQXMgd2Ugc2F3IGJlZm9yZSwgd2UgY2FuIG1vZGVsIHRoaXMgd2l0aCBhIG1vZGVsIHRoYXQgY29udGFpbnMgYm90aCBoaXN0b2xvZ2ljIGdyYWRlLCBseW1waCBub2RlIHN0YXR1cyBhbmQgdGhlIGludGVyYWN0aW9uIHRlcm0gYmV0d2VlbiBib3RoLiBXaGVuIGNoZWNraW5nIHRoZSBsaW5lYXIgbW9kZWwgYXNzdW1wdGlvbnMsIHdlIHNlZSB0aGF0IHRoZSB2YXJpYW5jZSBpcyBub3QgZXF1YWwuIFRoZXJlZm9yZSB3ZSBtb2RlbCB0aGUgZ2VuZSBleHByZXNzaW9uIHdpdGggYSBsb2cyLXRyYW5zZm9ybWF0aW9uLCB3aGljaCBtYWtlcyB0aGF0IGFsbCB0aGUgYXNzdW1wdGlvbnMgb2YgdGhlIGxpbmVhciBtb2RlbCBhcmUgc2F0aXNmaWVkLgoKCmBgYHtyfQojTW9kZWwgd2l0aCBtYWluIGVmZmVjdHMgZm9yIGhpc3RvbG9naWNhbCBncmFkZSBhbmQgbm9kZSBhbmQgZ3JhZGUgeCBub2RlIGludGVyYWN0aW9uCmZpdCA8LSBsbShnZW5lfmdyYWRlKm5vZGUsZGF0YT1rcG5hMikKc3VtbWFyeShmaXQpCnBsb3QoZml0KQpgYGAKCmBgYHtyfQpmaXQgPC0gbG0oZ2VuZSAlPiUgbG9nMn5ncmFkZSpub2RlLGRhdGE9a3BuYTIpCnBsb3QoZml0KQpgYGAKCldoZW4gY2hlY2tpbmcgdGhlIHNpZ25pZmljYW5jZSBvZiB0aGUgaW50ZXJhY3Rpb24gdGVybSwgd2Ugc2VlIHRoYXQgaXQgaXMgc2lnbmlmaWNhbnQgb24gdGhlIDUlIHNpZ25pZmljYW5jZSBsZXZlbC4gV2UgdGhlcmVmb3JlIGtlZXAgdGhlIGZ1bGwgbW9kZWwuCgpgYGB7cn0KbGlicmFyeShjYXIpCkFub3ZhKGZpdCx0eXBlPSJJSUkiKQpgYGAKCkFzIHdlIGFyZSBkZWFsaW5nIHdpdGggYSBmYWN0b3JpYWwgZGVzaWduLCB3ZSBjYW4gY2FsY3VsYXRlIHRoZSBtZWFuIGdlbmUgZXhwcmVzc2lvbiBmb3IgZWFjaCBncm91cCBieSB0aGUgZm9sbG93aW5nIHBhcmFtZXRlciBzdW1tYXRpb25zLgoKYGBge3J9CkV4cGxvcmVNb2RlbE1hdHJpeDo6VmlzdWFsaXplRGVzaWduKGtwbmEyLH5ncmFkZSpub2RlKSRwbG90bGlzdApgYGAKClRoZSByZXNlYXJjaGVycyB3YW50IHRvIGtub3cgdGhlIHBvd2VyIGZvciB0ZXN0aW5nIGZvbGxvd2luZyBoeXBvdGhlc2VzIChyZW1hcmsgdGhhdCB3ZSB3aWxsIGhhdmUgdG8gYWRqdXN0IGZvciBtdWx0aXBsZSB0ZXN0aW5nKToKCi0gTG9nIGZvbGQgY2hhbmdlIGJldHdlZW4gaGlzdG9sb2dpYyBncmFkZSAzIGFuZCBoaXN0b2xvZ2ljIGdyYWRlIDEgZm9yIHBhdGllbnRzIHdpdGggdW5hZmZlY3RlZCBseW1waCBub2RlcyAoPTApLgoKJCRIXzA6IFxsb2dfMntGQ31fe2czbjAtZzFuMH0gPSBcYmV0YV97ZzN9ID0gMCQkCgotIExvZyBmb2xkIGNoYW5nZSBiZXR3ZWVuIGhpc3RvbG9naWMgZ3JhZGUgMyBhbmQgaGlzdG9sb2dpYyBncmFkZSAxIGZvciBwYXRpZW50cyB3aXRoIHJlbW92ZWQgbHltcGggbm9kZXMgKD0xKS4gCgokJEhfMDogXGxvZ18ye0ZDfV97ZzNuMS1nMW4xfSA9IFxiZXRhX3tnM30gKyBcYmV0YV97ZzNuMX0gPSAwJCQKCi0gTG9nIGZvbGQgY2hhbmdlIGJldHdlZW4gdW5hZmZlY3RlZCBhbmQgcmVtb3ZlZCBseW1waCBub2RlcyBmb3IgcGF0aWVudHMgb2YgaGlzdG9sb2dpYyBncmFkZSAxLgoKJCRIXzA6IFxsb2dfMntGQ31fe2cxbjEtZzFuMH0gPSBcYmV0YV97bjF9ID0gMCQkCgotIExvZyBmb2xkIGNoYW5nZSBiZXR3ZWVuIHVuYWZmZWN0ZWQgYW5kIHJlbW92ZWQgbHltcGggbm9kZXMgZm9yIHBhdGllbnRzIG9mIGhpc3RvbG9naWMgZ3JhZGUgMy4KCiQkSF8wOiBcbG9nXzJ7RkN9X3tnM24xLWczbjB9ID0gXGJldGFfe24xfSArIFxiZXRhX3tnM24xfSA9IDAkJAoKLSBEaWZmZXJlbmNlIGluIGxvZyBmb2xkIGNoYW5nZSBiZXR3ZWVuIHBhdGllbnRzIG9mIGhpc3RvbG9naWNhbCBncmFkZSAzIGFuZCBoaXN0b2xvZ2ljYWwgZ3JhZGUgMSB3aXRoIHJlbW92ZWQgbHltcGggbm9kZXMgYW5kIGxvZyBmb2xkIGNoYW5nZSBiZXR3ZWVuIHBhdGllbnRzIGJldHdlZW4gcGF0aWVudHMgb2YgaGlzdG9sb2dpY2FsIGdyYWRlIDMgYW5kIGhpc3RvbG9naWNhbCBncmFkZSAxIHdpdGggdW5hZmZlY3RlZCBseW1waCBub2Rlcy4KCiQkSF8wOiBcbG9nXzJ7RkN9X3tnM24xLWcxbjF9IC0gXGxvZ18ye0ZDfV97ZzNuMC1nMW4wfSA9IFxiZXRhX3tnM24xfSA9IDAkJCB3aGljaCBpcyBhbiBlcXVpdmFsZW50IGh5cG90aGVzZXMgd2l0aCAkJEhfMDogXGxvZ18ye0ZDfV97ZzNuMS1nM24wfSAtIFxsb2dfMntGQ31fe2cxbjEtZzFuMH0gPSBcYmV0YV97ZzNuMX0gPSAwJCQKCldlIGNhbiB0ZXN0IHRoaXMgdXNpbmcgbXVsdGNvbXAsIHdoaWNoIGNvbnRyb2xzIGZvciBtdWx0aXBsZSB0ZXN0aW5nLgoKYGBge3J9CmxpYnJhcnkobXVsdGNvbXApCm1jcCA8LSBnbGh0KGZpdCxsaW5mY3QgPSBjKCJncmFkZTMgPSAwIiwiZ3JhZGUzICsgZ3JhZGUzOm5vZGUxID0gMCIsICJub2RlMSA9IDAiLCJub2RlMSArIGdyYWRlMzpub2RlMSA9IDAiLCAiZ3JhZGUzOm5vZGUxID0gMCIpKQpzdW1tYXJ5KG1jcCkKYGBgCgpXZSBnZXQgYSBzaWduaWZpY2FudCBwLXZhbHVlIGZvciB0aGUgZmlyc3QsIHNlY29uZCwgdGhpcmQgYW5kIGZpZnRoIGh5cG90aGVzaXMuIFRoZSBmb3VydGggaHlwb3RoZXNpcyBpcyBub3Qgc2lnbmlmaWNhbnQgYXQgdGhlIG92ZXJhbGwgNSUgc2lnbmlmaWNhbmNlIGxldmVsLgoKIyBQb3dlciBvZiB0aGUgdGVzdHMgZm9yIGVhY2ggb2YgdGhlIGNvbnRyYXN0cwoKIyMgU2ltdWxhdGlvbiBmdW5jdGlvbgoKRnVuY3Rpb24gdG8gc2ltdWxhdGUgZGF0YSBzaW1pbGFyIHRvIHRoYXQgb2Ygb3VyIGV4cGVyaW1lbnQgdW5kZXIgb3VyIG1vZGVsIGFzc3VtcHRpb25zLiAKCmBgYHtyfQpzaW1GYXN0TXVsdGlwbGVDb250cmFzdHMgPC0gZnVuY3Rpb24oZm9ybSwgZGF0YSwgYmV0YXMsIHNkLCBjb250cmFzdHMsIGFscGhhID0gLjA1LCBuU2ltID0gMTAwMDAsIGFkanVzdCA9ICJib25mZXJyb25pIikKewogICAgeVNpbSA8LSBybm9ybShucm93KGRhdGEpKm5TaW0sc2Q9c2QpCiAgICBkaW0oeVNpbSkgPC1jKG5yb3coZGF0YSksblNpbSkKICAgIGRlc2lnbiA8LSBtb2RlbC5tYXRyaXgoZm9ybSwgZGF0YSkKICAgIHlTaW0gPC0geVNpbSArIGMoZGVzaWduICUqJWJldGFzKQogICAgeVNpbSA8LSB0KHlTaW0pCiAgCiAgICAjIyMgRml0dGluZwogICAgZml0QWxsIDwtIGxpbW1hOjpsbUZpdCh5U2ltLGRlc2lnbikKICAKICAgICMjIyBJbmZlcmVuY2UKICAgIHZhclVuc2NhbGVkIDwtIHQoY29udHJhc3RzKSUqJWZpdEFsbCRjb3YuY29lZmZpY2llbnRzJSolY29udHJhc3RzCiAgICBjb250cmFzdHMgPC0gZml0QWxsJGNvZWZmaWNpZW50cyAlKiVjb250cmFzdHMKICAgIHNlQ29udHJhc3RzIDwtIG1hdHJpeChkaWFnKHZhclVuc2NhbGVkKV4uNSxucm93PW5TaW0sbmNvbD01LGJ5cm93ID0gVFJVRSkqZml0QWxsJHNpZ21hCiAgICB0c3RhdHMgPC0gY29udHJhc3RzL3NlQ29udHJhc3RzCiAgICBwdmFscyA8LSBwdChhYnModHN0YXRzKSxmaXRBbGwkZGYucmVzaWR1YWwsbG93ZXIudGFpbCA9IEZBTFNFKSoyCiAgICBwdmFscyA8LSB0KGFwcGx5KHB2YWxzLCAxLCBwLmFkanVzdCwgbWV0aG9kID0gYWRqdXN0KSkKICAgIAogICAgcmV0dXJuKGNvbE1lYW5zKHB2YWxzIDwgYWxwaGEpKQp9CmBgYAoKIyMgcG93ZXIgb2YgY3VycmVudCBleHBlcmltZW50CgpgYGB7cn0KblNpbSA8LSAyMDAwMApiZXRhcyA8LSBmaXQkY29lZmZpY2llbnRzCnNkIDwtIHNpZ21hKGZpdCkKY29udHJhc3RzIDwtIG1hdHJpeCgwLG5yb3c9NCxuY29sPTUpCnJvd25hbWVzKGNvbnRyYXN0cykgPC0gbmFtZXMoZml0JGNvZWZmaWNpZW50cykKY29sbmFtZXMoY29udHJhc3RzKSA8LSBjKCJncmFkZW4wIiwiZ3JhZGVuMSIsIm5vZGVnMSIsIm5vZGVnMyIsImludGVyYWN0aW9uIikKY29udHJhc3RzWzIsMV0gPC0gMQpjb250cmFzdHNbYygyLDQpLDJdIDwtIDEKY29udHJhc3RzWzMsM10gPC0gMQpjb250cmFzdHNbYygzLDQpLDRdIDwtIDEKY29udHJhc3RzWzQsNV0gPC0gMQpmb3JtIDwtIH4gZ3JhZGUqbm9kZQpwb3dlcjEgPC0gc2ltRmFzdE11bHRpcGxlQ29udHJhc3RzKGZvcm0sIGtwbmEyLCBiZXRhcywgc2QsIGNvbnRyYXN0cywgblNpbSA9IG5TaW0pCnBvd2VyMQpgYGAKCldlIG9ic2VydmUgbGFyZ2UgcG93ZXJzIGZvciBhbGwgY29udHJhc3RzLCBleGNlcHQgZm9yIGNvbnRyYXN0IG5vZGVnMywgd2hpY2ggaGFzIGEgc21hbGwgZWZmZWN0IHNpemUuIAoKIyMgUG93ZXIgZm9yIGluY3JlYXNpbmcgc2FtcGxlIHNpemUKCmBgYHtyfQpuU2ltIDwtIDIwMDAwCmJldGFzIDwtIGZpdCRjb2VmZmljaWVudHMKc2QgPC0gc2lnbWEoZml0KQpjb250cmFzdHMgPC0gbWF0cml4KDAsbnJvdz00LG5jb2w9NSkKcm93bmFtZXMoY29udHJhc3RzKSA8LSBuYW1lcyhmaXQkY29lZmZpY2llbnRzKQpjb2xuYW1lcyhjb250cmFzdHMpIDwtIGMoImdyYWRlbjAiLCJncmFkZW4xIiwibm9kZWcxIiwibm9kZWczIiwiaW50ZXJhY3Rpb24iKQpjb250cmFzdHNbMiwxXSA8LSAxCmNvbnRyYXN0c1tjKDIsNCksMl0gPC0gMQpjb250cmFzdHNbMywzXSA8LSAxCmNvbnRyYXN0c1tjKDMsNCksNF0gPC0gMQpjb250cmFzdHNbNCw1XSA8LSAxCmZvcm0gPC0gfiBncmFkZSpub2RlCgpwb3dlcnMgPC0gbWF0cml4KE5BLG5yb3c9OSwgbmNvbD02KQpjb2xuYW1lcyhwb3dlcnMpIDwtIGMoIm4iLGNvbG5hbWVzKGNvbnRyYXN0cykpCnBvd2Vyc1ssMV0gPC0gMjoxMAoKZGF0YUFsbENvbWIgPC0gZGF0YS5mcmFtZShncmFkZSA9IHJlcChjKDEsMyksZWFjaD0yKSU+JSBhcy5mYWN0b3IsIAogICAgICAgICAgICAgICAgICAgICAgICAgIG5vZGUgPSByZXAoYygwLDEpLDIpJT4lYXMuZmFjdG9yKSAKICAKZm9yIChpIGluIDE6bnJvdyhwb3dlcnMpKQp7ICAKcHJlZERhdGEgPC0gZGF0YS5mcmFtZShncmFkZSA9IHJlcChkYXRhQWxsQ29tYiRncmFkZSwgcG93ZXJzW2ksMV0pLCAKICAgICAgICAgICAgICAgICAgICAgICBub2RlID0gcmVwKGRhdGFBbGxDb21iJG5vZGUsIHBvd2Vyc1tpLDFdKSkKcG93ZXJzW2ksLTFdIDwtIHNpbUZhc3RNdWx0aXBsZUNvbnRyYXN0cyhmb3JtLCBwcmVkRGF0YSwgYmV0YXMsIHNkLCBjb250cmFzdHMsIG5TaW0gPSBuU2ltKQp9CnBvd2VycwpgYGAKCmBgYHtyfQpwb3dlcnMgJT4lIAogIGFzLmRhdGEuZnJhbWUgJT4lIAogIGdhdGhlcihjb250cmFzdCwgcG93ZXIsIC1uKSAlPiUgCiAgZ2dwbG90KGFlcyhuLHBvd2VyLGNvbG9yPWNvbnRyYXN0KSkgKwogIGdlb21fbGluZSgpCmBgYAoKIyMgUG93ZXIgd2hlbiBGQyBmb3IgZ3JhZGUgaW4gcGF0aWVudHMgd2l0aCB1bmFmZmVjdGVkIGx5bXBoIG5vZGVzIGVxdWFscyAxLjUgKCRcYmV0YV9nJCA9IDEuNSkKCmBgYHtyfQpuU2ltIDwtIDIwMDAwCmJldGFzMiA8LSBmaXQkY29lZmZpY2llbnRzCmJldGFzMlsiZ3JhZGUzIl0gPC0gbG9nMigxLjUpCnNkIDwtIHNpZ21hKGZpdCkKY29udHJhc3RzIDwtIG1hdHJpeCgwLG5yb3c9NCxuY29sPTUpCnJvd25hbWVzKGNvbnRyYXN0cykgPC0gbmFtZXMoZml0JGNvZWZmaWNpZW50cykKY29sbmFtZXMoY29udHJhc3RzKSA8LSBjKCJncmFkZW4wIiwiZ3JhZGVuMSIsIm5vZGVnMSIsIm5vZGVnMyIsImludGVyYWN0aW9uIikKY29udHJhc3RzWzIsMV0gPC0gMQpjb250cmFzdHNbYygyLDQpLDJdIDwtIDEKY29udHJhc3RzWzMsM10gPC0gMQpjb250cmFzdHNbYygzLDQpLDRdIDwtIDEKY29udHJhc3RzWzQsNV0gPC0gMQpmb3JtIDwtIH4gZ3JhZGUqbm9kZQpwb3dlcjMgPC0gc2ltRmFzdE11bHRpcGxlQ29udHJhc3RzKGZvcm0sIGtwbmEyLCBiZXRhczIsIHNkLCBjb250cmFzdHMsblNpbSA9IG5TaW0pCnBvd2VyMwpgYGAKCkl0IGlzIGNsZWFyIHRoYXQgb25seSB0aGUgcG93ZXIgb2YgdGhlIGNvbnRyYXN0cyBjb250YWluaW5nICRcYmV0YV9nJCBjaGFuZ2Ugd2hlbiB0aGUgZWZmZWN0IHNpemUgb2YgJFxiZXRhX2ckIGNoYW5nZXMu