
Looking deeper into the liver
This notebook presents a demonstration canonical correlation analysis - a method for comparing inter-relationships between two sets of variables. In this case, we have three measures of liver health derived from MRI; liver iron, liver inflammation factor (LIF) and liver proton density fat fraction (PDFF). We also have a small set of biochemistry tests measured on the same participants.
Source the data and summarise the variables
source("R/0_setup.R")
library(CCA)
IP<- readRDS("DemoSet2.rds") # 502505 by 71
# Variable sets
biochem<- c("red_blood_cell_erythrocyte_count", "high_light_scatter_reticulocyte_count", "alanine_aminotransferase", "triglycerides", "urate", "c_reactive_protein")
liver<- c("liver_iron", "LIF", "PDFF")
# Make the set
sett<- IP %>% select(all_of(liver), all_of(biochem)) %>%
filter(complete.cases(.))
sk<- skim(sett) %>% select(variable= skim_variable, mean= numeric.mean, min= numeric.p0, median= numeric.p50,
max= numeric.p100, numeric.hist)
sk[, 2:4]<- apply(sk[, 2:4], 2, function(x) round(x, 2))
print(sk)
Pairwise Panels for X and Y
Let’s begin by visualising the variables in X and Y and how they relate to each other.
# Separate and standardise the X and Y matrices
X<- sett[, biochem] %>% scale()
Y<- sett[, liver] %>% scale()
# Pairwise panel for the X matrix (biochemistry)
psych::pairs.panels(X, method = "pearson", hist.col = "#b3d9ff", density = TRUE)

# Pairwise panel for the Y matrix (liver)
psych::pairs.panels(Y, method = "pearson", hist.col = "#b3d9ff", density = TRUE)

The pairwise panels show that for both X and Y matrices, there is some visible correlation between variables. In this situation, canonical correlation analysis is ideal for exploring the overall relationship between the two sets.
Cross-Correlation Panel
correl<-matcor(X, Y)
img.matcor(correl,type = 3)

The cross-correlation panel gives us a preview of the range in which the cross-correlations are likely to sit. Nothing is strongly standing out, so we can expect cross-correlations in the range of 0-0.4 or so.
Applying the CCA method
The main CCA method is given here via the {CCA} package.
can.model<- cc(X,Y)
can.model$cor
[1] 0.42335127 0.06836749 0.04168428
These are the “canonical correlations”, which indicate the overall relationship between X and Y in 3 dimensions. We have three since our smallest input matrix has 3 columns.
Which canonical variates are significant?
rho<- can.model$cor
n<- nrow(X)
p<- ncol(X)
q<- ncol(Y)
CCP::p.asym(rho, n, p, q, tstat="Wilks")
Wilks' Lambda, using F-approximation (Rao's F):
stat approx df1 df2 p.value
1 to 3: 0.8155178 47.522623 18 11441.47 0.000000000
2 to 3: 0.9935964 2.603395 10 8092.00 0.003733462
3 to 3: 0.9982624 1.761056 4 4047.00 0.133797814
An assymptotic test on the size of the correlations indicates that only the first two canonical correlations are significantly different from zero. Since the first canoncial correlation is so much larger than the second, the remainder of the analysis will focus on the first component.
In simple terms, the rest of the analysis is geared towards understanding how the matrices relate to each other, via this variate.
Standardised Canonical Weights
# Standardised Canonical Weights
sw1<-diag(sqrt(diag(cov(X)))) %*% can.model$xcoef %>%
data.frame() %>%
mutate(Var= biochem, Set= "Biochemistry") %>%
select(Var, X1, Set)
sw2<-diag(sqrt(diag(cov(Y)))) %*% can.model$ycoef %>%
data.frame() %>%
mutate(Var= liver, Set= "Liver") %>%
select(Var, X1, Set)
t0<- bind_rows(sw1, sw2) %>%
mutate(Direction= ifelse(X1 > 0, "Positive", "Negative"))
ggplot(t0, aes(x= X1, y= fct_reorder(Var, X1), fill= Direction)) +
geom_col(size= 0.2, color= "grey50") +
geom_text(aes(label= sprintf("%2.1f", X1*100)), hjust= ifelse(t0$X1 > 0, 1.2, -0.3))+
facet_wrap(~ Set, scales= "free") +
theme_bw() +
theme(legend.position = "None",
strip.background = element_rect(fill= "grey30"),
strip.text = element_text(color="white", face= "bold")) +
scale_fill_manual(values= c("#a3dca3", "#b3d9ff"))

sw1
sw2
Canonical loadings and cross-loadings
Canonical weights (standardised or not) have fallen out of use in favour of canonical loadings. These are the correlations between the original variables and the scores of the canonical variate.
# Canonical loadings and cross-loadings
can_loadings<- comput(X,Y,cancor)
can_loadings[3:6]
$corr.X.xscores
[,1] [,2] [,3]
red_blood_cell_erythrocyte_count -0.4108776 0.35522284 -0.163735110
high_light_scatter_reticulocyte_count -0.7215862 -0.09267242 -0.009252762
alanine_aminotransferase -0.6873395 0.20784334 -0.110979461
triglycerides -0.6427031 0.35672320 -0.236499349
urate -0.5187563 -0.47980607 -0.588432085
c_reactive_protein -0.4918877 -0.38984102 0.675329116
$corr.Y.xscores
[,1] [,2] [,3]
liver_iron -0.1825606 -0.050259395 -0.021804439
LIF -0.2537582 -0.034332977 0.025982655
PDFF -0.4214048 -0.005667556 -0.002000076
$corr.X.yscores
[,1] [,2] [,3]
red_blood_cell_erythrocyte_count -0.1739455 0.02428569 -0.0068251808
high_light_scatter_reticulocyte_count -0.3054844 -0.00633578 -0.0003856948
alanine_aminotransferase -0.2909861 0.01420973 -0.0046260994
triglycerides -0.2720892 0.02438827 -0.0098583060
urate -0.2196162 -0.03280314 -0.0245283701
c_reactive_protein -0.2082413 -0.02665245 0.0281506106
$corr.Y.yscores
[,1] [,2] [,3]
liver_iron -0.4312272 -0.7351359 -0.52308537
LIF -0.5994034 -0.5021828 0.62332017
PDFF -0.9954022 -0.0828984 -0.04798153
can_redunds<- candisc::redundancy(candisc::cancor(X,Y))
can_redunds
Redundancies for the X variables & total X canonical redundancy
Xcan1 Xcan2 Xcan3 total X|Y
0.0623131 0.0005355 0.0002599 0.0631086
Redundancies for the Y variables & total Y canonical redundancy
Ycan1 Ycan2 Ycan3 total Y|X
0.0917679 0.0012456 0.0003848 0.0933983
# X Plot
t0<- tibble(Var= c(biochem, biochem),
X1= c(can_loadings$corr.X.xscores[,1], can_loadings$corr.X.yscores[,1]),
Loading= c(rep("X-X Loading", 6), rep("X-Y Cross-loading", 6)))
ggplot(t0, aes(x= X1, y= fct_reorder(Var, -X1), fill= Loading)) +
geom_col(size= 0.2, color= "grey50") +
geom_text(aes(label= sprintf("%2.1f", X1*100)), hjust= -0.6)+
facet_wrap(~ Loading, scales= "free_x") +
theme_bw() +
theme(legend.position = "None",
strip.background = element_rect(fill= "grey30"),
strip.text = element_text(color="white", face= "bold")) +
scale_fill_manual(values= c("#a3dca3", "#b3d9ff")) +
labs(x="", y= "Variable", title= "Loadings and Cross-Loadings for X (Biochemistry)")

# Y Plot
t0<- tibble(Var= c(liver, liver),
X1= c(can_loadings$corr.Y.yscores[,1], can_loadings$corr.Y.xscores[,1]),
Loading= c(rep("Y-Y Loading", 3), rep("Y-X Cross-loading", 3))) %>%
mutate(Loading= ordered(Loading, levels=c("Y-Y Loading", "Y-X Cross-loading")))
ggplot(t0, aes(x= X1, y= fct_reorder(Var, -X1), fill= Loading)) +
geom_col(size= 0.2, color= "grey50") +
geom_text(aes(label= sprintf("%2.1f", X1*100)), hjust= -0.6)+
facet_wrap(~ Loading, scales= "free_x") +
theme_bw() +
theme(legend.position = "None",
strip.background = element_rect(fill= "grey30"),
strip.text = element_text(color="white", face= "bold")) +
scale_fill_manual(values= c("#a3dca3", "#b3d9ff")) +
labs(x="", y= "Variable", title= "Loadings and Cross-Loadings for Y (Liver)")

Examination of the loadings to the first canonical variate show that all variables included in our analysis have a part to play in explaining the relation between biochemistry and liver health. From the liver side, PDFF is the strongest variable, while from the biochemistry side the three most influential variables are light scatter reticulocyte count, alanine aminotransferase and triglycerides.
plt.cc(cc(X,Y),var.label = TRUE)

…
…
…
LS0tCnRpdGxlOiAiQ2Fub25pY2FsIENvcnJlbGF0aW9uIEFuYWx5c2lzOiBMaXZlciB2cyBCaW9jaGVtaXN0cnkiCm91dHB1dDogaHRtbF9ub3RlYm9vawpkYXRlOiAiMjAyMC0xMC0xMiIKYXV0aG9yOiAiQ2VsIE1jQ3JhY2tlbiIKLS0tCgoKIVtdKC9Vc2Vycy9DZWxlc3RlL0Rlc2t0b3AvTElWRVIucG5nKQoKIyMjIExvb2tpbmcgZGVlcGVyIGludG8gdGhlIGxpdmVyCgpUaGlzIG5vdGVib29rIHByZXNlbnRzIGEgZGVtb25zdHJhdGlvbiBjYW5vbmljYWwgY29ycmVsYXRpb24gYW5hbHlzaXMgLSBhIG1ldGhvZCBmb3IgY29tcGFyaW5nIGludGVyLXJlbGF0aW9uc2hpcHMgYmV0d2VlbiB0d28gc2V0cyBvZiB2YXJpYWJsZXMuICBJbiB0aGlzIGNhc2UsIHdlIGhhdmUgdGhyZWUgbWVhc3VyZXMgb2YgbGl2ZXIgaGVhbHRoIGRlcml2ZWQgZnJvbSBNUkk7IGxpdmVyIGlyb24sIGxpdmVyIGluZmxhbW1hdGlvbiBmYWN0b3IgKExJRikgYW5kIGxpdmVyIHByb3RvbiBkZW5zaXR5IGZhdCBmcmFjdGlvbiAoUERGRikuIFdlIGFsc28gaGF2ZSBhIHNtYWxsIHNldCBvZiBiaW9jaGVtaXN0cnkgdGVzdHMgbWVhc3VyZWQgb24gdGhlIHNhbWUgcGFydGljaXBhbnRzLgoKIyMjIyMgU291cmNlIHRoZSBkYXRhIGFuZCBzdW1tYXJpc2UgdGhlIHZhcmlhYmxlcwpgYGB7ciwgbWVzc2FnZT0gRkFMU0UsIHdhcm5pbmc9IEZBTFNFfQpzb3VyY2UoIlIvMF9zZXR1cC5SIikKCmxpYnJhcnkoQ0NBKQoKSVA8LSByZWFkUkRTKCJEZW1vU2V0Mi5yZHMiKSAjIDUwMjUwNSBieSA3MQoKIyBWYXJpYWJsZSBzZXRzCmJpb2NoZW08LSBjKCJyZWRfYmxvb2RfY2VsbF9lcnl0aHJvY3l0ZV9jb3VudCIsICJoaWdoX2xpZ2h0X3NjYXR0ZXJfcmV0aWN1bG9jeXRlX2NvdW50IiwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFsYW5pbmVfYW1pbm90cmFuc2ZlcmFzZSIsICJ0cmlnbHljZXJpZGVzIiwgInVyYXRlIiwgImNfcmVhY3RpdmVfcHJvdGVpbiIpCgpsaXZlcjwtIGMoImxpdmVyX2lyb24iLCAiTElGIiwgIlBERkYiKQoKIyBNYWtlIHRoZSBzZXQKCnNldHQ8LSBJUCAlPiUgc2VsZWN0KGFsbF9vZihsaXZlciksIGFsbF9vZihiaW9jaGVtKSkgJT4lIAogICAgICAgICAgICAgIGZpbHRlcihjb21wbGV0ZS5jYXNlcyguKSkgCgpzazwtIHNraW0oc2V0dCkgJT4lIHNlbGVjdCh2YXJpYWJsZT0gc2tpbV92YXJpYWJsZSwgbWVhbj0gbnVtZXJpYy5tZWFuLCBtaW49IG51bWVyaWMucDAsIG1lZGlhbj0gbnVtZXJpYy5wNTAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heD0gbnVtZXJpYy5wMTAwLCBudW1lcmljLmhpc3QpCnNrWywgMjo0XTwtIGFwcGx5KHNrWywgMjo0XSwgMiwgZnVuY3Rpb24oeCkgcm91bmQoeCwgMikpCnByaW50KHNrKQpgYGAKIyMjIFBhaXJ3aXNlIFBhbmVscyBmb3IgWCBhbmQgWQpMZXQncyBiZWdpbiBieSB2aXN1YWxpc2luZyB0aGUgdmFyaWFibGVzIGluIFggYW5kIFkgYW5kIGhvdyB0aGV5IHJlbGF0ZSB0byBlYWNoIG90aGVyLgpgYGB7cn0KIyBTZXBhcmF0ZSBhbmQgc3RhbmRhcmRpc2UgdGhlIFggYW5kIFkgbWF0cmljZXMKWDwtIHNldHRbLCBiaW9jaGVtXSAlPiUgc2NhbGUoKQpZPC0gc2V0dFssIGxpdmVyXSAlPiUgc2NhbGUoKQoKIyBQYWlyd2lzZSBwYW5lbCBmb3IgdGhlIFggbWF0cml4IChiaW9jaGVtaXN0cnkpCnBzeWNoOjpwYWlycy5wYW5lbHMoWCwgbWV0aG9kID0gInBlYXJzb24iLCBoaXN0LmNvbCA9ICIjYjNkOWZmIiwgZGVuc2l0eSA9IFRSVUUpCmBgYApgYGB7cn0KIyBQYWlyd2lzZSBwYW5lbCBmb3IgdGhlIFkgbWF0cml4IChsaXZlcikKcHN5Y2g6OnBhaXJzLnBhbmVscyhZLCBtZXRob2QgPSAicGVhcnNvbiIsIGhpc3QuY29sID0gIiNiM2Q5ZmYiLCBkZW5zaXR5ID0gVFJVRSkKYGBgClRoZSBwYWlyd2lzZSBwYW5lbHMgc2hvdyB0aGF0IGZvciBib3RoIFggYW5kIFkgbWF0cmljZXMsIHRoZXJlIGlzIHNvbWUgdmlzaWJsZSBjb3JyZWxhdGlvbiBiZXR3ZWVuIHZhcmlhYmxlcy4gSW4gdGhpcyBzaXR1YXRpb24sIGNhbm9uaWNhbCBjb3JyZWxhdGlvbiBhbmFseXNpcyBpcyBpZGVhbCBmb3IgZXhwbG9yaW5nIHRoZSBvdmVyYWxsIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIHRoZSB0d28gc2V0cy4KCiMjIyBDcm9zcy1Db3JyZWxhdGlvbiBQYW5lbApgYGB7cn0KY29ycmVsPC1tYXRjb3IoWCwgWSkKaW1nLm1hdGNvcihjb3JyZWwsdHlwZSA9IDMpCmBgYApUaGUgY3Jvc3MtY29ycmVsYXRpb24gcGFuZWwgZ2l2ZXMgdXMgYSBwcmV2aWV3IG9mIHRoZSByYW5nZSBpbiB3aGljaCB0aGUgY3Jvc3MtY29ycmVsYXRpb25zIGFyZSBsaWtlbHkgdG8gc2l0LiBOb3RoaW5nIGlzIHN0cm9uZ2x5IHN0YW5kaW5nIG91dCwgc28gd2UgY2FuIGV4cGVjdCBjcm9zcy1jb3JyZWxhdGlvbnMgaW4gdGhlIHJhbmdlIG9mIDAtMC40IG9yIHNvLgoKIyMjIEFwcGx5aW5nIHRoZSBDQ0EgbWV0aG9kClRoZSBtYWluIENDQSBtZXRob2QgaXMgZ2l2ZW4gaGVyZSB2aWEgdGhlIHtDQ0F9IHBhY2thZ2UuCmBgYHtyfQpjYW4ubW9kZWw8LSBjYyhYLFkpCmNhbi5tb2RlbCRjb3IKYGBgClRoZXNlIGFyZSB0aGUgImNhbm9uaWNhbCBjb3JyZWxhdGlvbnMiLCB3aGljaCBpbmRpY2F0ZSB0aGUgb3ZlcmFsbCByZWxhdGlvbnNoaXAgYmV0d2VlbiBYIGFuZCBZIGluIDMgZGltZW5zaW9ucy4gV2UgaGF2ZSB0aHJlZSBzaW5jZSBvdXIgc21hbGxlc3QgaW5wdXQgbWF0cml4IGhhcyAzIGNvbHVtbnMuIAoKIyMjIFdoaWNoIGNhbm9uaWNhbCB2YXJpYXRlcyBhcmUgc2lnbmlmaWNhbnQ/CmBgYHtyfQpyaG88LSBjYW4ubW9kZWwkY29yCm48LSAgIG5yb3coWCkKcDwtICAgbmNvbChYKQpxPC0gICBuY29sKFkpCkNDUDo6cC5hc3ltKHJobywgbiwgcCwgcSwgdHN0YXQ9IldpbGtzIikKYGBgCkFuIGFzc3ltcHRvdGljIHRlc3Qgb24gdGhlIHNpemUgb2YgdGhlIGNvcnJlbGF0aW9ucyBpbmRpY2F0ZXMgdGhhdCBvbmx5IHRoZSBmaXJzdCB0d28gY2Fub25pY2FsIGNvcnJlbGF0aW9ucyBhcmUgc2lnbmlmaWNhbnRseSBkaWZmZXJlbnQgZnJvbSB6ZXJvLiAgU2luY2UgdGhlIGZpcnN0IGNhbm9uY2lhbCBjb3JyZWxhdGlvbiBpcyBzbyBtdWNoIGxhcmdlciB0aGFuIHRoZSBzZWNvbmQsIHRoZSByZW1haW5kZXIgb2YgdGhlIGFuYWx5c2lzIHdpbGwgZm9jdXMgb24gdGhlIGZpcnN0IGNvbXBvbmVudC4gIApJbiBzaW1wbGUgdGVybXMsIHRoZSByZXN0IG9mIHRoZSBhbmFseXNpcyBpcyBnZWFyZWQgdG93YXJkcyB1bmRlcnN0YW5kaW5nIGhvdyB0aGUgbWF0cmljZXMgcmVsYXRlIHRvIGVhY2ggb3RoZXIsIHZpYSB0aGlzIHZhcmlhdGUuCgojIyMgU3RhbmRhcmRpc2VkIENhbm9uaWNhbCBXZWlnaHRzCmBgYHtyfQojIFN0YW5kYXJkaXNlZCBDYW5vbmljYWwgV2VpZ2h0cwoKc3cxPC1kaWFnKHNxcnQoZGlhZyhjb3YoWCkpKSkgJSolIGNhbi5tb2RlbCR4Y29lZiAlPiUgCiAgICAgICBkYXRhLmZyYW1lKCkgJT4lIAogICAgICAgbXV0YXRlKFZhcj0gYmlvY2hlbSwgU2V0PSAiQmlvY2hlbWlzdHJ5IikgJT4lIAogICAgICAgc2VsZWN0KFZhciwgWDEsIFNldCkgCgpzdzI8LWRpYWcoc3FydChkaWFnKGNvdihZKSkpKSAlKiUgY2FuLm1vZGVsJHljb2VmICU+JSAKICAgICAgZGF0YS5mcmFtZSgpICU+JSAKICAgICAgbXV0YXRlKFZhcj0gbGl2ZXIsIFNldD0gIkxpdmVyIikgJT4lIAogICAgICBzZWxlY3QoVmFyLCBYMSwgU2V0KSAKCnQwPC0gYmluZF9yb3dzKHN3MSwgc3cyKSAlPiUKICAgICAgIG11dGF0ZShEaXJlY3Rpb249IGlmZWxzZShYMSA+IDAsICJQb3NpdGl2ZSIsICJOZWdhdGl2ZSIpKQoKZ2dwbG90KHQwLCBhZXMoeD0gWDEsIHk9IGZjdF9yZW9yZGVyKFZhciwgWDEpLCBmaWxsPSBEaXJlY3Rpb24pKSArCiAgZ2VvbV9jb2woc2l6ZT0gMC4yLCBjb2xvcj0gImdyZXk1MCIpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsPSBzcHJpbnRmKCIlMi4xZiIsIFgxKjEwMCkpLCBoanVzdD0gaWZlbHNlKHQwJFgxID4gMCwgMS4yLCAtMC4zKSkrCiAgZmFjZXRfd3JhcCh+IFNldCwgc2NhbGVzPSAiZnJlZSIpICsKICB0aGVtZV9idygpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiTm9uZSIsCiAgICAgICAgIHN0cmlwLmJhY2tncm91bmQgPSBlbGVtZW50X3JlY3QoZmlsbD0gImdyZXkzMCIpLAogICAgICAgICBzdHJpcC50ZXh0ID0gZWxlbWVudF90ZXh0KGNvbG9yPSJ3aGl0ZSIsIGZhY2U9ICJib2xkIikpICArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPSBjKCIjYTNkY2EzIiwgIiNiM2Q5ZmYiKSkKCnN3MQpzdzIKYGBgCiMjIyBDYW5vbmljYWwgbG9hZGluZ3MgYW5kIGNyb3NzLWxvYWRpbmdzCkNhbm9uaWNhbCB3ZWlnaHRzIChzdGFuZGFyZGlzZWQgb3Igbm90KSBoYXZlIGZhbGxlbiBvdXQgb2YgdXNlIGluIGZhdm91ciBvZiBjYW5vbmljYWwgbG9hZGluZ3MuICBUaGVzZSBhcmUgdGhlIGNvcnJlbGF0aW9ucyBiZXR3ZWVuIHRoZSBvcmlnaW5hbCB2YXJpYWJsZXMgYW5kIHRoZSBzY29yZXMgb2YgdGhlIGNhbm9uaWNhbCB2YXJpYXRlLgpgYGB7cn0KIyBDYW5vbmljYWwgbG9hZGluZ3MgYW5kIGNyb3NzLWxvYWRpbmdzCgpjYW5fbG9hZGluZ3M8LSBjb21wdXQoWCxZLGNhbmNvcikKY2FuX2xvYWRpbmdzWzM6Nl0KCmNhbl9yZWR1bmRzPC0gIGNhbmRpc2M6OnJlZHVuZGFuY3koY2FuZGlzYzo6Y2FuY29yKFgsWSkpCmNhbl9yZWR1bmRzCmBgYApgYGB7cn0KIyBYIFBsb3QKdDA8LSB0aWJibGUoVmFyPSBjKGJpb2NoZW0sIGJpb2NoZW0pLAogICAgICAgICAgICBYMT0gYyhjYW5fbG9hZGluZ3MkY29yci5YLnhzY29yZXNbLDFdLCBjYW5fbG9hZGluZ3MkY29yci5YLnlzY29yZXNbLDFdKSwKICAgICAgICAgICAgTG9hZGluZz0gYyhyZXAoIlgtWCBMb2FkaW5nIiwgNiksIHJlcCgiWC1ZIENyb3NzLWxvYWRpbmciLCA2KSkpCmdncGxvdCh0MCwgYWVzKHg9IFgxLCB5PSBmY3RfcmVvcmRlcihWYXIsIC1YMSksIGZpbGw9IExvYWRpbmcpKSArCiAgZ2VvbV9jb2woc2l6ZT0gMC4yLCBjb2xvcj0gImdyZXk1MCIpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsPSBzcHJpbnRmKCIlMi4xZiIsIFgxKjEwMCkpLCBoanVzdD0gLTAuNikrCiAgZmFjZXRfd3JhcCh+IExvYWRpbmcsIHNjYWxlcz0gImZyZWVfeCIpICsKICB0aGVtZV9idygpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiTm9uZSIsCiAgICAgICAgIHN0cmlwLmJhY2tncm91bmQgPSBlbGVtZW50X3JlY3QoZmlsbD0gImdyZXkzMCIpLAogICAgICAgICBzdHJpcC50ZXh0ID0gZWxlbWVudF90ZXh0KGNvbG9yPSJ3aGl0ZSIsIGZhY2U9ICJib2xkIikpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9IGMoIiNhM2RjYTMiLCAiI2IzZDlmZiIpKSArCiAgbGFicyh4PSIiLCB5PSAiVmFyaWFibGUiLCB0aXRsZT0gIkxvYWRpbmdzIGFuZCBDcm9zcy1Mb2FkaW5ncyBmb3IgWCAoQmlvY2hlbWlzdHJ5KSIpCmBgYApgYGB7cn0KIyBZIFBsb3QKdDA8LSB0aWJibGUoVmFyPSBjKGxpdmVyLCBsaXZlciksCiAgICAgICAgICAgIFgxPSBjKGNhbl9sb2FkaW5ncyRjb3JyLlkueXNjb3Jlc1ssMV0sIGNhbl9sb2FkaW5ncyRjb3JyLlkueHNjb3Jlc1ssMV0pLAogICAgICAgICAgICBMb2FkaW5nPSBjKHJlcCgiWS1ZIExvYWRpbmciLCAzKSwgcmVwKCJZLVggQ3Jvc3MtbG9hZGluZyIsIDMpKSkgJT4lCiAgICAgbXV0YXRlKExvYWRpbmc9IG9yZGVyZWQoTG9hZGluZywgbGV2ZWxzPWMoIlktWSBMb2FkaW5nIiwgIlktWCBDcm9zcy1sb2FkaW5nIikpKQpnZ3Bsb3QodDAsIGFlcyh4PSBYMSwgeT0gZmN0X3Jlb3JkZXIoVmFyLCAtWDEpLCBmaWxsPSBMb2FkaW5nKSkgKwogIGdlb21fY29sKHNpemU9IDAuMiwgY29sb3I9ICJncmV5NTAiKSArCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbD0gc3ByaW50ZigiJTIuMWYiLCBYMSoxMDApKSwgaGp1c3Q9IC0wLjYpKwogIGZhY2V0X3dyYXAofiBMb2FkaW5nLCBzY2FsZXM9ICJmcmVlX3giKSArCiAgdGhlbWVfYncoKSArCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIk5vbmUiLAogICAgICAgICBzdHJpcC5iYWNrZ3JvdW5kID0gZWxlbWVudF9yZWN0KGZpbGw9ICJncmV5MzAiKSwKICAgICAgICAgc3RyaXAudGV4dCA9IGVsZW1lbnRfdGV4dChjb2xvcj0id2hpdGUiLCBmYWNlPSAiYm9sZCIpKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPSBjKCIjYTNkY2EzIiwgIiNiM2Q5ZmYiKSkgKwogIGxhYnMoeD0iIiwgeT0gIlZhcmlhYmxlIiwgdGl0bGU9ICJMb2FkaW5ncyBhbmQgQ3Jvc3MtTG9hZGluZ3MgZm9yIFkgKExpdmVyKSIpCmBgYApFeGFtaW5hdGlvbiBvZiB0aGUgbG9hZGluZ3MgdG8gdGhlIGZpcnN0IGNhbm9uaWNhbCB2YXJpYXRlIHNob3cgdGhhdCBhbGwgdmFyaWFibGVzIGluY2x1ZGVkIGluIG91ciBhbmFseXNpcyBoYXZlIGEgcGFydCB0byBwbGF5IGluIGV4cGxhaW5pbmcgdGhlIHJlbGF0aW9uIGJldHdlZW4gYmlvY2hlbWlzdHJ5IGFuZCBsaXZlciBoZWFsdGguIEZyb20gdGhlIGxpdmVyIHNpZGUsIFBERkYgaXMgdGhlIHN0cm9uZ2VzdCB2YXJpYWJsZSwgd2hpbGUgZnJvbSB0aGUgYmlvY2hlbWlzdHJ5IHNpZGUgdGhlIHRocmVlIG1vc3QgaW5mbHVlbnRpYWwgdmFyaWFibGVzIGFyZSBsaWdodCBzY2F0dGVyIHJldGljdWxvY3l0ZSBjb3VudCwgYWxhbmluZSBhbWlub3RyYW5zZmVyYXNlIGFuZCB0cmlnbHljZXJpZGVzLgpgYGB7cn0KcGx0LmNjKGNjKFgsWSksdmFyLmxhYmVsID0gVFJVRSkKYGBgCi4uLgoKCi4uLgoKCi4uLgo=