Below I provide code and step-by-step explanations to produce a heatmap for log odd-ratios from log-linear models. I exemplify the implementation in R using data on intergenerational class mobility for England, France and Sweden.

Steps:

  1. Load the following packages for data manipulation (tidyverse,modelr,reshape2), log-linear model estimation (vcdExtra,logmult) and ploting (cowplot). Install previously if you do not have them.
    library("tidyverse")
    library("modelr")
    library("reshape2")
    library("vcdExtra")
    library("logmult")
    library("cowplot")


  1. Input the contingency table an turn it into a data frame. I use the dataset erikson from the package gnm, a dependency of package logmult. This is a cross-classification of subject’s occupational status (destination) and his father’s occupational status (origin) across 3 countries.
    
    # Inpute data and create contingency table as data.frame()
    data(erikson)
    table <- ftable(erikson)
    mydata <- as.data.frame(table)
    levels(mydata$country) <- c("England-Wales","France","Sweden")
    
    # Save levels variables. To be used later.
    levels.origin      <- levels(mydata$origin)
    levels.destination <- levels(mydata$destination)
    levels.country     <- levels(mydata$country)

This is what the data looks like:


  1. Next, I set the values to be used as reference categories.
    # Set reference categories
    mydata$origin      <- relevel(mydata$origin, ref = "V/VI")
    mydata$destination <- relevel(mydata$destination, ref = "V/VI")
    mydata$country     <- relevel(mydata$country, ref = "England-Wales")
    mydata$Freq        <- mydata$Freq + 1 # add small constant to avoid problems with empty cells. 


  1. Fit different model specifications. Some of these modes are log-linear and other are log-multiplicative.
# Fit models 
# independence 
indep <- gnm(Freq ~ (origin + destination)*country, family = poisson, data = mydata)
# quasi-perfect mobility 
qpm  <- gnm(Freq ~ (origin + destination)*country + Diag(origin, destination)*country, family = poisson, data = mydata)
# row-column association 1
rc1 <- gnm(Freq ~ (origin + destination)*country + Mult(origin, destination) + Diag(origin, destination)*country, family = poisson, data = mydata)
Initialising
Running start-up iterations..
Running main iterations.........................................................................................................
Done
# quasi-symmetry
qsymm <- gnm(Freq ~ (origin + destination)*country + Symm(origin, destination)*country, family = poisson, data = mydata)
# unidiff or log-multiplicative layers
unidiff <- gnm(Freq ~ (origin + destination)*country + Mult(Exp(country), origin:destination), family = poisson, data = mydata)
Initialising
Running start-up iterations..
Running main iterations........
Done
# saturated
sat <- gnm(Freq ~ origin*destination*country, family = poisson, data = mydata)
# Compare models via godness of fit statistics
models <- glmlist(indep,qpm,rc1,qsymm,unidiff,sat)
LRstats(models)
Likelihood summary table:
           AIC    BIC LR Chisq  Df Pr(>Chisq)    
indep   6498.4 6676.5   5152.6 192  < 2.2e-16 ***
qpm     3130.9 3403.4   1731.1 165  < 2.2e-16 ***
rc1     1973.4 2298.3    543.7 150  < 2.2e-16 ***
qsymm   1689.1 2244.5    127.3  84   0.001605 ** 
unidiff 1649.0 2057.7    171.2 126   0.004580 ** 
sat     1729.8 2578.6      0.0   0   1.000000    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1


  1. Goodness of fit statistics suggest that the unidiff models is the one that better fits the data. At this point I compute prediction from this model and from them, log odd ratios.
# Create a synthetic dataset with all possible combinations of values
dummy.model <- lm(Freq ~ origin + destination + country, data=mydata)
new_x <- mydata %>% data_grid(origin,destination,country,.model=dummy.model) 
# Compute predictions from different models. In this case: unidiff, quasi-symmetry and saturated model.
for ( m in c("unidiff","qsymm","sat")) {
  
  chosen_model <- eval(parse(text = m ))  
  # Predicted counts
  predictions <- cbind(mydata%>% data_grid(origin,destination,country,.model=dummy.model), pred = predict(chosen_model, newdata=new_x)) %>%
    as_tibble()  
  # Intercept
  intercept <- predictions %>% filter(origin=="V/VI", destination=="V/VI", country=="England-Wales") %>% dplyr::summarise(pred) %>% as.numeric()
  # Log odd ratios for marginal distributions
  predictions <- predictions %>% mutate(pred = pred - intercept) # remove intercept
  predictions_country     <- predictions %>% filter(origin=="V/VI", destination=="V/VI")  %>% rename(margin_country=pred) %>% select(country,margin_country)
  predictions_origin      <- predictions %>% filter(country=="England-Wales", destination=="V/VI") %>% rename(margin_origin=pred) %>% select(origin,margin_origin)
  predictions_destination <- predictions %>% filter(country=="England-Wales", origin=="V/VI") %>% rename(margin_destination=pred) %>% select(destination,margin_destination)
  # match
  predictions <- predictions %>% left_join(predictions_country, by="country")
  predictions <- predictions %>% left_join(predictions_origin, by="origin")
  predictions <- predictions %>% left_join(predictions_destination, by="destination")
  # Log odd ratios for marginal distributions origin and destination by country
  predictions_country_origin <- predictions %>% filter(origin!="V/VI",country!="England-Wales",destination=="V/VI") %>%
    rename(margin_country_origin=pred) %>% mutate(margin_country_origin = margin_country_origin - (margin_country + margin_origin )) %>%
    select(country,origin,margin_country_origin)
  predictions_country_destination <- predictions %>% filter(origin=="V/VI",country!="England-Wales",destination!="V/VI") %>%
    rename(margin_country_destination=pred) %>% mutate(margin_country_destination = margin_country_destination - (margin_country + margin_destination )) %>% 
    select(country,destination,margin_country_destination)
  predictions <- predictions %>% left_join(predictions_country_origin, by=c("country","origin")) %>%  replace_na(list(margin_country_origin = 0))
  predictions <- predictions %>% left_join(predictions_country_destination, by=c("country","destination")) %>%  replace_na(list(margin_country_destination = 0))
  # Margin-free log-odd ratios (LORs)
  predictions <- predictions %>% 
    mutate(`log-OR` = pred - (margin_country + margin_origin + margin_destination + margin_country_origin + margin_country_destination) ) 
  # Save predictions 
  assign(paste0("predictions_",m),predictions)
    
}


  1. Finally, for each model I visualize the estimated log odd ratios capturing margin-free association between origin and destination across countries . Of course, other quantities can also be visualized in the same way.
# Combine models
predictions_unidiff <- predictions_unidiff %>% mutate(model = "Unidiff")
predictions_qsymm <- predictions_qsymm %>% mutate(model = "Quasi-symmetry")
predictions_sat <- predictions_sat %>% mutate(model = "Saturated")
predictions <- bind_rows(predictions_unidiff,predictions_qsymm,predictions_sat) %>%
  mutate(model = factor(model, levels=c("Unidiff","Quasi-symmetry","Saturated")))
# Plot
plot <- predictions %>% 
  ggplot(aes(y=factor(origin, levels = rev(levels.origin)),
             x=factor(destination, levels = levels.destination))) + facet_grid(model ~ country) + geom_raster(aes(fill= `log-OR`)) +
  scale_fill_gradientn(limits=c(-6,6), colours=c("red","white","blue")) +
  labs(y="Father's occupation", x= "Children's occupation", colour="") +
  theme_bw() + theme(axis.text.x = element_text(size=9, angle=45, vjust=-1, hjust=0),
                     axis.text.y = element_text(size=9, angle=0),
                     plot.title= element_text(size=11)) +
  scale_x_discrete(position="top") 
# Add labels 
plot <- plot %>% add_sub(.,"I+II: Service class, III: Routine non-manual employees, IVa+b:Petty bourgeoisie, IVc: Farmers, V/VI: Skilled working class, VIIa: Semi and unskilled working class, VIIb: Agricultural workers", size= 9) %>%  ggdraw()
print(plot)

LS0tCnRpdGxlOiAiSGVhdG1hcCBmb3IgcGF0dGVybnMgb2YgYXNzb2NpYXRpb24gaW4gbG9nLWxpbmVhciBtb2RlbHMiCm91dHB1dDoKICBodG1sX25vdGVib29rOiBkZWZhdWx0Ci0tLQoKCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIGZpZy53aWR0aCA9IDEyLCBmaWcuaGVpZ2h0ID0gMTIpCmBgYAoKCkJlbG93IEkgcHJvdmlkZSBjb2RlIGFuZCBzdGVwLWJ5LXN0ZXAgZXhwbGFuYXRpb25zIHRvIHByb2R1Y2UgYSBoZWF0bWFwIGZvciBsb2cgb2RkLXJhdGlvcyBmcm9tIGxvZy1saW5lYXIgbW9kZWxzLiBJIGV4ZW1wbGlmeSB0aGUgaW1wbGVtZW50YXRpb24gaW4gYGBgUmBgYCB1c2luZyBkYXRhIG9uIGludGVyZ2VuZXJhdGlvbmFsIGNsYXNzIG1vYmlsaXR5IGZvciBFbmdsYW5kLCBGcmFuY2UgYW5kIFN3ZWRlbi4KCiMjIyMgU3RlcHM6CgoxLiAgTG9hZCB0aGUgZm9sbG93aW5nIHBhY2thZ2VzIGZvciBkYXRhIG1hbmlwdWxhdGlvbiAodGlkeXZlcnNlLG1vZGVscixyZXNoYXBlMiksIGxvZy1saW5lYXIgbW9kZWwgZXN0aW1hdGlvbiAodmNkRXh0cmEsbG9nbXVsdCkgYW5kIHBsb3RpbmcgKGNvd3Bsb3QpLiBJbnN0YWxsIHByZXZpb3VzbHkgaWYgeW91IGRvIG5vdCBoYXZlIHRoZW0uCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCiAgICBsaWJyYXJ5KCJ0aWR5dmVyc2UiKQogICAgbGlicmFyeSgibW9kZWxyIikKICAgIGxpYnJhcnkoInJlc2hhcGUyIikKICAgIGxpYnJhcnkoInZjZEV4dHJhIikKICAgIGxpYnJhcnkoImxvZ211bHQiKQogICAgbGlicmFyeSgiY293cGxvdCIpCgpgYGAKCjxicj4KCjIuIElucHV0IHRoZSBjb250aW5nZW5jeSB0YWJsZSBhbiB0dXJuIGl0IGludG8gYSBkYXRhIGZyYW1lLiBJIHVzZSB0aGUgZGF0YXNldCBgYGBlcmlrc29uYGBgIGZyb20gdGhlIHBhY2thZ2UgYGBgZ25tYGBgLCBhIGRlcGVuZGVuY3kgb2YgcGFja2FnZSBgYGBsb2dtdWx0YGBgLiBUaGlzIGlzIGEgY3Jvc3MtY2xhc3NpZmljYXRpb24gb2Ygc3ViamVjdCdzIG9jY3VwYXRpb25hbCBzdGF0dXMgKGRlc3RpbmF0aW9uKSBhbmQgaGlzIGZhdGhlcidzIG9jY3VwYXRpb25hbCBzdGF0dXMgKG9yaWdpbikgYWNyb3NzIDMgY291bnRyaWVzLiAKCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQogICAgCiAgICAjIElucHV0ZSBkYXRhIGFuZCBjcmVhdGUgY29udGluZ2VuY3kgdGFibGUgYXMgZGF0YS5mcmFtZSgpCiAgICBkYXRhKGVyaWtzb24pCiAgICB0YWJsZSA8LSBmdGFibGUoZXJpa3NvbikKICAgIG15ZGF0YSA8LSBhcy5kYXRhLmZyYW1lKHRhYmxlKQogICAgbGV2ZWxzKG15ZGF0YSRjb3VudHJ5KSA8LSBjKCJFbmdsYW5kLVdhbGVzIiwiRnJhbmNlIiwiU3dlZGVuIikKICAgIAogICAgIyBTYXZlIGxldmVscyB2YXJpYWJsZXMuIFRvIGJlIHVzZWQgbGF0ZXIuCiAgICBsZXZlbHMub3JpZ2luICAgICAgPC0gbGV2ZWxzKG15ZGF0YSRvcmlnaW4pCiAgICBsZXZlbHMuZGVzdGluYXRpb24gPC0gbGV2ZWxzKG15ZGF0YSRkZXN0aW5hdGlvbikKICAgIGxldmVscy5jb3VudHJ5ICAgICA8LSBsZXZlbHMobXlkYXRhJGNvdW50cnkpCgpgYGAKClRoaXMgaXMgd2hhdCB0aGUgZGF0YSBsb29rcyBsaWtlOgoKYGBge3IsIGVjaG89RkFMU0V9CiAgICBwcmludChteWRhdGEgJT4lIGFzX3RpYmJsZSgpKQpgYGAKCjxicj4KCjMuIE5leHQsIEkgc2V0IHRoZSB2YWx1ZXMgdG8gYmUgdXNlZCBhcyByZWZlcmVuY2UgY2F0ZWdvcmllcy4gCgpgYGB7cn0KCiAgICAjIFNldCByZWZlcmVuY2UgY2F0ZWdvcmllcwogICAgbXlkYXRhJG9yaWdpbiAgICAgIDwtIHJlbGV2ZWwobXlkYXRhJG9yaWdpbiwgcmVmID0gIlYvVkkiKQogICAgbXlkYXRhJGRlc3RpbmF0aW9uIDwtIHJlbGV2ZWwobXlkYXRhJGRlc3RpbmF0aW9uLCByZWYgPSAiVi9WSSIpCiAgICBteWRhdGEkY291bnRyeSAgICAgPC0gcmVsZXZlbChteWRhdGEkY291bnRyeSwgcmVmID0gIkVuZ2xhbmQtV2FsZXMiKQogICAgbXlkYXRhJEZyZXEgICAgICAgIDwtIG15ZGF0YSRGcmVxICsgMSAjIGFkZCBzbWFsbCBjb25zdGFudCB0byBhdm9pZCBwcm9ibGVtcyB3aXRoIGVtcHR5IGNlbGxzLiAKCmBgYAoKPGJyPgoKCjQuIEZpdCBkaWZmZXJlbnQgbW9kZWwgc3BlY2lmaWNhdGlvbnMuIFNvbWUgb2YgdGhlc2UgbW9kZXMgYXJlIGxvZy1saW5lYXIgYW5kIG90aGVyIGFyZSBsb2ctbXVsdGlwbGljYXRpdmUuCgogCmBgYHtyfQoKIyBGaXQgbW9kZWxzIAoKIyBpbmRlcGVuZGVuY2UgCmluZGVwIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5LCBmYW1pbHkgPSBwb2lzc29uLCBkYXRhID0gbXlkYXRhKQoKIyBxdWFzaS1wZXJmZWN0IG1vYmlsaXR5IApxcG0gIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5ICsgRGlhZyhvcmlnaW4sIGRlc3RpbmF0aW9uKSpjb3VudHJ5LCBmYW1pbHkgPSBwb2lzc29uLCBkYXRhID0gbXlkYXRhKQoKIyByb3ctY29sdW1uIGFzc29jaWF0aW9uIDEKcmMxIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5ICsgTXVsdChvcmlnaW4sIGRlc3RpbmF0aW9uKSArIERpYWcob3JpZ2luLCBkZXN0aW5hdGlvbikqY291bnRyeSwgZmFtaWx5ID0gcG9pc3NvbiwgZGF0YSA9IG15ZGF0YSkKCiMgcXVhc2ktc3ltbWV0cnkKcXN5bW0gPC0gZ25tKEZyZXEgfiAob3JpZ2luICsgZGVzdGluYXRpb24pKmNvdW50cnkgKyBTeW1tKG9yaWdpbiwgZGVzdGluYXRpb24pKmNvdW50cnksIGZhbWlseSA9IHBvaXNzb24sIGRhdGEgPSBteWRhdGEpCgojIHVuaWRpZmYgb3IgbG9nLW11bHRpcGxpY2F0aXZlIGxheWVycwp1bmlkaWZmIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5ICsgTXVsdChFeHAoY291bnRyeSksIG9yaWdpbjpkZXN0aW5hdGlvbiksIGZhbWlseSA9IHBvaXNzb24sIGRhdGEgPSBteWRhdGEpCgojIHNhdHVyYXRlZApzYXQgPC0gZ25tKEZyZXEgfiBvcmlnaW4qZGVzdGluYXRpb24qY291bnRyeSwgZmFtaWx5ID0gcG9pc3NvbiwgZGF0YSA9IG15ZGF0YSkKCgojIENvbXBhcmUgbW9kZWxzIHZpYSBnb2RuZXNzIG9mIGZpdCBzdGF0aXN0aWNzCgptb2RlbHMgPC0gZ2xtbGlzdChpbmRlcCxxcG0scmMxLHFzeW1tLHVuaWRpZmYsc2F0KQpMUnN0YXRzKG1vZGVscykKCmBgYAoKPGJyPgoKNS4gR29vZG5lc3Mgb2YgZml0IHN0YXRpc3RpY3Mgc3VnZ2VzdCB0aGF0IHRoZSBgYHVuaWRpZmZgYCBtb2RlbHMgaXMgdGhlIG9uZSB0aGF0IGJldHRlciBmaXRzIHRoZSBkYXRhLiBBdCB0aGlzIHBvaW50IEkgY29tcHV0ZSBwcmVkaWN0aW9uIGZyb20gdGhpcyBtb2RlbCBhbmQgZnJvbSB0aGVtLCBsb2cgb2RkIHJhdGlvcy4gCgoKYGBge3IsIGluY2x1ZGU9VFJVRSwgZWNobz1UUlVFfQoKIyBDcmVhdGUgYSBzeW50aGV0aWMgZGF0YXNldCB3aXRoIGFsbCBwb3NzaWJsZSBjb21iaW5hdGlvbnMgb2YgdmFsdWVzCgpkdW1teS5tb2RlbCA8LSBsbShGcmVxIH4gb3JpZ2luICsgZGVzdGluYXRpb24gKyBjb3VudHJ5LCBkYXRhPW15ZGF0YSkKbmV3X3ggPC0gbXlkYXRhICU+JSBkYXRhX2dyaWQob3JpZ2luLGRlc3RpbmF0aW9uLGNvdW50cnksLm1vZGVsPWR1bW15Lm1vZGVsKSAKCiMgQ29tcHV0ZSBwcmVkaWN0aW9ucyBmcm9tIGRpZmZlcmVudCBtb2RlbHMuIEluIHRoaXMgY2FzZTogdW5pZGlmZiwgcXVhc2ktc3ltbWV0cnkgYW5kIHNhdHVyYXRlZCBtb2RlbC4KCmZvciAoIG0gaW4gYygidW5pZGlmZiIsInFzeW1tIiwic2F0IikpIHsKICAKICBjaG9zZW5fbW9kZWwgPC0gZXZhbChwYXJzZSh0ZXh0ID0gbSApKSAgCgogICMgUHJlZGljdGVkIGNvdW50cwogIHByZWRpY3Rpb25zIDwtIGNiaW5kKG15ZGF0YSU+JSBkYXRhX2dyaWQob3JpZ2luLGRlc3RpbmF0aW9uLGNvdW50cnksLm1vZGVsPWR1bW15Lm1vZGVsKSwgcHJlZCA9IHByZWRpY3QoY2hvc2VuX21vZGVsLCBuZXdkYXRhPW5ld194KSkgJT4lCiAgICBhc190aWJibGUoKSAgCgoKICAjIEludGVyY2VwdAogIGludGVyY2VwdCA8LSBwcmVkaWN0aW9ucyAlPiUgZmlsdGVyKG9yaWdpbj09IlYvVkkiLCBkZXN0aW5hdGlvbj09IlYvVkkiLCBjb3VudHJ5PT0iRW5nbGFuZC1XYWxlcyIpICU+JSBkcGx5cjo6c3VtbWFyaXNlKHByZWQpICU+JSBhcy5udW1lcmljKCkKCgogICMgTG9nIG9kZCByYXRpb3MgZm9yIG1hcmdpbmFsIGRpc3RyaWJ1dGlvbnMKICBwcmVkaWN0aW9ucyA8LSBwcmVkaWN0aW9ucyAlPiUgbXV0YXRlKHByZWQgPSBwcmVkIC0gaW50ZXJjZXB0KSAjIHJlbW92ZSBpbnRlcmNlcHQKCiAgcHJlZGljdGlvbnNfY291bnRyeSAgICAgPC0gcHJlZGljdGlvbnMgJT4lIGZpbHRlcihvcmlnaW49PSJWL1ZJIiwgZGVzdGluYXRpb249PSJWL1ZJIikgICU+JSByZW5hbWUobWFyZ2luX2NvdW50cnk9cHJlZCkgJT4lIHNlbGVjdChjb3VudHJ5LG1hcmdpbl9jb3VudHJ5KQogIHByZWRpY3Rpb25zX29yaWdpbiAgICAgIDwtIHByZWRpY3Rpb25zICU+JSBmaWx0ZXIoY291bnRyeT09IkVuZ2xhbmQtV2FsZXMiLCBkZXN0aW5hdGlvbj09IlYvVkkiKSAlPiUgcmVuYW1lKG1hcmdpbl9vcmlnaW49cHJlZCkgJT4lIHNlbGVjdChvcmlnaW4sbWFyZ2luX29yaWdpbikKICBwcmVkaWN0aW9uc19kZXN0aW5hdGlvbiA8LSBwcmVkaWN0aW9ucyAlPiUgZmlsdGVyKGNvdW50cnk9PSJFbmdsYW5kLVdhbGVzIiwgb3JpZ2luPT0iVi9WSSIpICU+JSByZW5hbWUobWFyZ2luX2Rlc3RpbmF0aW9uPXByZWQpICU+JSBzZWxlY3QoZGVzdGluYXRpb24sbWFyZ2luX2Rlc3RpbmF0aW9uKQoKCiAgIyBtYXRjaAogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSBsZWZ0X2pvaW4ocHJlZGljdGlvbnNfY291bnRyeSwgYnk9ImNvdW50cnkiKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSBsZWZ0X2pvaW4ocHJlZGljdGlvbnNfb3JpZ2luLCBieT0ib3JpZ2luIikKICBwcmVkaWN0aW9ucyA8LSBwcmVkaWN0aW9ucyAlPiUgbGVmdF9qb2luKHByZWRpY3Rpb25zX2Rlc3RpbmF0aW9uLCBieT0iZGVzdGluYXRpb24iKQoKCiAgIyBMb2cgb2RkIHJhdGlvcyBmb3IgbWFyZ2luYWwgZGlzdHJpYnV0aW9ucyBvcmlnaW4gYW5kIGRlc3RpbmF0aW9uIGJ5IGNvdW50cnkKCiAgcHJlZGljdGlvbnNfY291bnRyeV9vcmlnaW4gPC0gcHJlZGljdGlvbnMgJT4lIGZpbHRlcihvcmlnaW4hPSJWL1ZJIixjb3VudHJ5IT0iRW5nbGFuZC1XYWxlcyIsZGVzdGluYXRpb249PSJWL1ZJIikgJT4lCiAgICByZW5hbWUobWFyZ2luX2NvdW50cnlfb3JpZ2luPXByZWQpICU+JSBtdXRhdGUobWFyZ2luX2NvdW50cnlfb3JpZ2luID0gbWFyZ2luX2NvdW50cnlfb3JpZ2luIC0gKG1hcmdpbl9jb3VudHJ5ICsgbWFyZ2luX29yaWdpbiApKSAlPiUKICAgIHNlbGVjdChjb3VudHJ5LG9yaWdpbixtYXJnaW5fY291bnRyeV9vcmlnaW4pCgogIHByZWRpY3Rpb25zX2NvdW50cnlfZGVzdGluYXRpb24gPC0gcHJlZGljdGlvbnMgJT4lIGZpbHRlcihvcmlnaW49PSJWL1ZJIixjb3VudHJ5IT0iRW5nbGFuZC1XYWxlcyIsZGVzdGluYXRpb24hPSJWL1ZJIikgJT4lCiAgICByZW5hbWUobWFyZ2luX2NvdW50cnlfZGVzdGluYXRpb249cHJlZCkgJT4lIG11dGF0ZShtYXJnaW5fY291bnRyeV9kZXN0aW5hdGlvbiA9IG1hcmdpbl9jb3VudHJ5X2Rlc3RpbmF0aW9uIC0gKG1hcmdpbl9jb3VudHJ5ICsgbWFyZ2luX2Rlc3RpbmF0aW9uICkpICU+JSAKICAgIHNlbGVjdChjb3VudHJ5LGRlc3RpbmF0aW9uLG1hcmdpbl9jb3VudHJ5X2Rlc3RpbmF0aW9uKQoKICBwcmVkaWN0aW9ucyA8LSBwcmVkaWN0aW9ucyAlPiUgbGVmdF9qb2luKHByZWRpY3Rpb25zX2NvdW50cnlfb3JpZ2luLCBieT1jKCJjb3VudHJ5Iiwib3JpZ2luIikpICU+JSAgcmVwbGFjZV9uYShsaXN0KG1hcmdpbl9jb3VudHJ5X29yaWdpbiA9IDApKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSBsZWZ0X2pvaW4ocHJlZGljdGlvbnNfY291bnRyeV9kZXN0aW5hdGlvbiwgYnk9YygiY291bnRyeSIsImRlc3RpbmF0aW9uIikpICU+JSAgcmVwbGFjZV9uYShsaXN0KG1hcmdpbl9jb3VudHJ5X2Rlc3RpbmF0aW9uID0gMCkpCgoKICAjIE1hcmdpbi1mcmVlIGxvZy1vZGQgcmF0aW9zIChMT1JzKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSAKICAgIG11dGF0ZShgbG9nLU9SYCA9IHByZWQgLSAobWFyZ2luX2NvdW50cnkgKyBtYXJnaW5fb3JpZ2luICsgbWFyZ2luX2Rlc3RpbmF0aW9uICsgbWFyZ2luX2NvdW50cnlfb3JpZ2luICsgbWFyZ2luX2NvdW50cnlfZGVzdGluYXRpb24pICkgCgoKICAjIFNhdmUgcHJlZGljdGlvbnMgCiAgYXNzaWduKHBhc3RlMCgicHJlZGljdGlvbnNfIixtKSxwcmVkaWN0aW9ucykKICAgIAp9CgoKYGBgCgo8YnI+CgoKNi4gRmluYWxseSwgZm9yIGVhY2ggbW9kZWwgSSB2aXN1YWxpemUgdGhlIGVzdGltYXRlZCBsb2cgb2RkIHJhdGlvcyBjYXB0dXJpbmcgbWFyZ2luLWZyZWUgYXNzb2NpYXRpb24gYmV0d2VlbiBvcmlnaW4gYW5kIGRlc3RpbmF0aW9uIGFjcm9zcyBjb3VudHJpZXMgLiBPZiBjb3Vyc2UsIG90aGVyIHF1YW50aXRpZXMgY2FuIGFsc28gYmUgdmlzdWFsaXplZCBpbiB0aGUgc2FtZSB3YXkuCgoKCmBgYHtyfQoKIyBDb21iaW5lIG1vZGVscwoKcHJlZGljdGlvbnNfdW5pZGlmZiA8LSBwcmVkaWN0aW9uc191bmlkaWZmICU+JSBtdXRhdGUobW9kZWwgPSAiVW5pZGlmZiIpCnByZWRpY3Rpb25zX3FzeW1tIDwtIHByZWRpY3Rpb25zX3FzeW1tICU+JSBtdXRhdGUobW9kZWwgPSAiUXVhc2ktc3ltbWV0cnkiKQpwcmVkaWN0aW9uc19zYXQgPC0gcHJlZGljdGlvbnNfc2F0ICU+JSBtdXRhdGUobW9kZWwgPSAiU2F0dXJhdGVkIikKCnByZWRpY3Rpb25zIDwtIGJpbmRfcm93cyhwcmVkaWN0aW9uc191bmlkaWZmLHByZWRpY3Rpb25zX3FzeW1tLHByZWRpY3Rpb25zX3NhdCkgJT4lCiAgbXV0YXRlKG1vZGVsID0gZmFjdG9yKG1vZGVsLCBsZXZlbHM9YygiVW5pZGlmZiIsIlF1YXNpLXN5bW1ldHJ5IiwiU2F0dXJhdGVkIikpKQoKCiMgUGxvdAoKcGxvdCA8LSBwcmVkaWN0aW9ucyAlPiUgCiAgZ2dwbG90KGFlcyh5PWZhY3RvcihvcmlnaW4sIGxldmVscyA9IHJldihsZXZlbHMub3JpZ2luKSksCiAgICAgICAgICAgICB4PWZhY3RvcihkZXN0aW5hdGlvbiwgbGV2ZWxzID0gbGV2ZWxzLmRlc3RpbmF0aW9uKSkpICsgZmFjZXRfZ3JpZChtb2RlbCB+IGNvdW50cnkpICsgZ2VvbV9yYXN0ZXIoYWVzKGZpbGw9IGBsb2ctT1JgKSkgKwogIHNjYWxlX2ZpbGxfZ3JhZGllbnRuKGxpbWl0cz1jKC02LDYpLCBjb2xvdXJzPWMoInJlZCIsIndoaXRlIiwiYmx1ZSIpKSArCiAgbGFicyh5PSJGYXRoZXIncyBvY2N1cGF0aW9uIiwgeD0gIkNoaWxkcmVuJ3Mgb2NjdXBhdGlvbiIsIGNvbG91cj0iIikgKwogIHRoZW1lX2J3KCkgKyB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChzaXplPTksIGFuZ2xlPTQ1LCB2anVzdD0tMSwgaGp1c3Q9MCksCiAgICAgICAgICAgICAgICAgICAgIGF4aXMudGV4dC55ID0gZWxlbWVudF90ZXh0KHNpemU9OSwgYW5nbGU9MCksCiAgICAgICAgICAgICAgICAgICAgIHBsb3QudGl0bGU9IGVsZW1lbnRfdGV4dChzaXplPTExKSkgKwogIHNjYWxlX3hfZGlzY3JldGUocG9zaXRpb249InRvcCIpIAoKCiMgQWRkIGxhYmVscyAKcGxvdCA8LSBwbG90ICU+JSBhZGRfc3ViKC4sIkkrSUk6IFNlcnZpY2UgY2xhc3MsIElJSTogUm91dGluZSBub24tbWFudWFsIGVtcGxveWVlcywgSVZhK2I6UGV0dHkgYm91cmdlb2lzaWUsIElWYzogRmFybWVycywgVi9WSTogU2tpbGxlZCB3b3JraW5nIGNsYXNzLCBWSUlhOiBTZW1pIGFuZCB1bnNraWxsZWQgd29ya2luZyBjbGFzcywgVklJYjogQWdyaWN1bHR1cmFsIHdvcmtlcnMiLCBzaXplPSA5KSAlPiUgIGdnZHJhdygpCgpwcmludChwbG90KQoKCmBgYAoKCgo=