Introduction

In this tutorial we will learn some of the basics of Bayesian Networks by solving a simulated problem. Our agenda will include:

  • A quick brackground review on the packages we will be using (tidyverse and bnlearn)
  • A statistical approach to the problem at hand
  • Modeling with epert knowledge
  • Learning parameters from data with latent variables
  • Probability queries and simulation
  • Advanced use cases

Seed

set.seed(1020)

Aside: The tidyverse

You should know the tidyverse, it makes R easier and more fun.

You can get more information on https://www.tidyverse.org

I really suggest you to take Hadley’s Wickham Data Science course http://r4ds.had.co.nz

In this tutorial I will be using the following tidyverse utilities:

  • dplyr in general
  • ggplot2 for almost any plot
  • tidyr specifically the gather function
  • lubridate to extract components from Dates
  • read_r to read data from csv files

In order to make the tutorial work you should install and load the following packages

library(tidyverse)
library(lubridate)

The bnlearn package

The bnlearn package is the most complete and popular package for Bayesian Networks available to the date in R. We will start our tutorial by reviewing some of its capacities.

First we should install and load the package:

library(bnlearn)

*Please make sure that you either download or compile at least bnelarn 4.2.x as previous versions might have some bugs.

You should check out the documentation that is available online [Link]. It contains nice examples and additional use cases beyond this tutorial, if you are interested in this topic there are a couple of related books written by the author of the package, you can find the references in the mentioned website.

Data Structures

The usage of bnlearn revolves around the usage of two main data structures to represent a Bayesian Network in different stages (NOTE that these are S3 classes and the names might overlap with other functions):

  • bn [Docs]. Represents the structural information, variables, graph and learning algorithm if provided.
  • bn.fit [Docs]. Adds the parametric information on top of the previous structure. Contains the distribution of each node according to its type and parent configuration.

Creating the structure of Bayesian networks

There are different ways to manually initialize and modify the graph of a Bayesian Network.

We can create empty graphs from a set of variables:

vars <- LETTERS[1:6]
dag  <- empty.graph(vars)
dag

  Random/Generated Bayesian network

  model:
   [A][B][C][D][E][F] 
  nodes:                                 6 
  arcs:                                  0 
    undirected arcs:                     0 
    directed arcs:                       0 
  average markov blanket size:           0.00 
  average neighbourhood size:            0.00 
  average branching factor:              0.00 

  generation algorithm:                  Empty 

You can specify arcs sets in various ways. The most common are:

As a two column (from, to) matrix, setting them via arcs to an existing network:

e <- matrix(
      c("A", "C", "B", "F", "C", "F"),
      ncol = 2, byrow = TRUE,
      dimnames = list(NULL, c("from", "to"))
    )
arcs(dag) <- e
dag

  Random/Generated Bayesian network

  model:
   [A][B][D][E][C|A][F|B:C] 
  nodes:                                 6 
  arcs:                                  3 
    undirected arcs:                     0 
    directed arcs:                       3 
  average markov blanket size:           1.33 
  average neighbourhood size:            1.00 
  average branching factor:              0.50 

  generation algorithm:                  Empty 

We can also use an adjancecy matrix, and assign it to a dag with amat:

adj <- matrix(
        0L, 
        ncol = 6, 
        nrow = 6,
        dimnames = list(vars, vars)
       )
adj["A", "C"] = 1L
adj["B", "F"] = 1L
adj["C", "F"] = 1L
adj["D", "E"] = 1L
adj["A", "E"] = 1L
print(adj)
  A B C D E F
A 0 0 1 0 1 0
B 0 0 0 0 0 1
C 0 0 0 0 0 1
D 0 0 0 0 1 0
E 0 0 0 0 0 0
F 0 0 0 0 0 0
amat(dag) <- adj
dag

  Random/Generated Bayesian network

  model:
   [A][B][D][C|A][E|A:D][F|B:C] 
  nodes:                                 6 
  arcs:                                  5 
    undirected arcs:                     0 
    directed arcs:                       5 
  average markov blanket size:           2.33 
  average neighbourhood size:            1.67 
  average branching factor:              0.83 

  generation algorithm:                  Empty 

The last option is to create a formula for a given set of variables. Each node is specified by a pair of brackets [<var_name>]. If the node has a parentset we denote it by | and specify the list of parents separated by colons :. We can compute the formula into a bn object with model2network.

dag <- model2network("[A][C][B|A][D|C][F|A:B:C][E|F]")
dag

  Random/Generated Bayesian network

  model:
   [A][C][B|A][D|C][F|A:B:C][E|F] 
  nodes:                                 6 
  arcs:                                  6 
    undirected arcs:                     0 
    directed arcs:                       6 
  average markov blanket size:           2.67 
  average neighbourhood size:            2.00 
  average branching factor:              1.00 

  generation algorithm:                  Empty 

The package provide other capabilities such as random graphs and graph sampling. In addition there is a range of utilities to manipulate the graphs [Link] [Link].

Plotting graphs

We can ploting graphs using the built in R engine by using plot for the bn class:

plot(dag)

Minimal aspects of the plot can be customized as documented in the corresponding help page. Other packages can be used indrectly to plot graphs, bnlearn provides connections with some of them but be aware that some of them might be outdated.

Fortunatelly, graphs are a common data structure and we can find lots of utilities to work with them. One of the most common visualization tools for graphs, and for data in general, is the D3 library from the Javascript domain. Thanks to the integration of R shiny with web technologies we can find wonderful ports such as the networkD3 package.

The next snippet is just a custom function to transform a bn object to the required information to plot a D3 force graph.

Please install the networkD3, be carefull as this package is currently under beta development and future version could break this code.

plotD3bn <- function(bn) {
  varNames <- nodes(bn)
  # Nodes should be zero indexed!
  links <- data.frame(arcs(bn)) %>%
    mutate(from = match(from, varNames)-1, to = match(to, varNames)-1, value = 1)
  
  nodes <- data.frame(name = varNames) %>%
    mutate(group = 1, size = 30)
  
  networkD3::forceNetwork(
    Links = links,  
    Nodes = nodes,
    Source = "from",
    Target = "to",
    Value = "value",
    NodeID = "name",
    Group = "group",
    fontSize = 20,
    zoom = TRUE,
    arrows = TRUE,
    bounded = TRUE,
    opacityNoHover = 1
  )
}

We can now plot our last generated BN. If the result looks too small you can zoom in and out using your mouse wheel.

plotD3bn(dag)

Loading Bayesian networks from files

There are different file formats to represent a Bayesian network. They have originated over the years as an effort to create standards or as part of particular propietary systems. Despite the effort there is not a consensuated closed standard, perhaps the closest one is the xbif format [Link].

bnlearn provides several ways to load BNs from different formats [Docs]. You should use them when importing and exporting to other systems or to share them with other people, if you plan to use just bnlearn you could just save an rda file.

The Bayes net repository

The maintainers of bnlearn also provide a modern R-focused repository for a series of popular Bayesian networks that have been used extensivelly for benchmarking on the literature.

http://www.bnlearn.com/bnrepository/

In here you can find networks with different properties that can be use to test algorithms or explore this or other BN packages. We will now work with the so popular asia network: Asia is for BNs what “iris” is for statistics.

# This downloads the RData file from the repository and loads it.
# The bn is loaded into a bn.fit variable called bn
load(url("http://www.bnlearn.com/bnrepository/asia/asia.rda"))
asia <- bn

Discrete networks

We will know explore the contents of this variable. As you can see, this not only contains the structure but the parameters as well, but first we will take a look at the structure:

bn.net(asia)

  Random/Generated Bayesian network

  model:
   [asia][smoke][tub|asia][lung|smoke][bronc|smoke][either|tub:lung][xray|either]
   [dysp|bronc:either]
  nodes:                                 8 
  arcs:                                  8 
    undirected arcs:                     0 
    directed arcs:                       8 
  average markov blanket size:           2.50 
  average neighbourhood size:            2.00 
  average branching factor:              1.00 

  generation algorithm:                  Empty 
plotD3bn(asia)

Now is the time to review the parameters, this prints each node and the asociated probability table. In this case all variables are discrete so the tables would be conditional probability tables.

asia

  Bayesian network parameters

  Parameters of node asia (multinomial distribution)

Conditional probability table:
  yes   no 
0.01 0.99 

  Parameters of node tub (multinomial distribution)

Conditional probability table:
 
     asia
tub    yes   no
  yes 0.05 0.01
  no  0.95 0.99

  Parameters of node smoke (multinomial distribution)

Conditional probability table:
 yes  no 
0.5 0.5 

  Parameters of node lung (multinomial distribution)

Conditional probability table:
 
     smoke
lung   yes   no
  yes 0.10 0.01
  no  0.90 0.99

  Parameters of node bronc (multinomial distribution)

Conditional probability table:
 
     smoke
bronc yes  no
  yes 0.6 0.3
  no  0.4 0.7

  Parameters of node either (multinomial distribution)

Conditional probability table:
 
, , tub = yes

      lung
either yes no
   yes   1  1
   no    0  0

, , tub = no

      lung
either yes no
   yes   1  0
   no    0  1


  Parameters of node xray (multinomial distribution)

Conditional probability table:
 
     either
xray   yes   no
  yes 0.98 0.05
  no  0.02 0.95

  Parameters of node dysp (multinomial distribution)

Conditional probability table:
 
, , either = yes

     bronc
dysp  yes  no
  yes 0.9 0.7
  no  0.1 0.3

, , either = no

     bronc
dysp  yes  no
  yes 0.8 0.1
  no  0.2 0.9

We can access individual nodes of the net as in a data.frame:

asia$smoke

  Parameters of node smoke (multinomial distribution)

Conditional probability table:
 yes  no 
0.5 0.5 

There is also a function to plot the distributions of discrete networks:

bn.fit.barchart(asia$smoke)

bn.fit.barchart(asia$dysp)

Introducing expert knowledge

We can alter the probability tables of a BN, this is useful to override parameters learnt from data or not observed variables. This method allows us to include expert information from the domain of the problem modelled.

To modify a conditional probability table you can just directly replace the existing table in the model by extracting it with coef. Be careful to maintain the inherent restrictions of the probability distribution.

cpt <- coef(asia$smoke)
cpt[] <- c(0.2, 0.8)
asia$smoke <- cpt
asia$smoke

  Parameters of node smoke (multinomial distribution)

Conditional probability table:
 yes  no 
0.2 0.8 

Sampling data from a Bayesian network

bnlearn introduces an R like function to sample data from a given fitted model rbn. We will now sample a dataset from asia to test learning from data.

# Note that the order of the parameters is inverted from the R sample functions
sampleAsia <- rbn(x = asia, n = 10000)
head(sampleAsia)

Parametric learning from data

We can induce the parameters of a Bayesian Network from observed data. bnlearn provides different algorithms for that, while Maximum Likelihood Estimation (MLE) is the most common one.

We can invoke the learning algorithm by using the function bn.fit. For that we need a DAG and a compatible dataset:

net <- bn.net(asia)
asiaInduced <- bn.fit(x = net, data = sampleAsia)

We can now compare the two networks, there should be some discrepacies in the induced one. Notice that extremelly marginal probabilities will not be simulated and thus will not have a significant present in the sample.

asiaInduced

  Bayesian network parameters

  Parameters of node asia (multinomial distribution)

Conditional probability table:
    yes     no 
0.0103 0.9897 

  Parameters of node tub (multinomial distribution)

Conditional probability table:
 
     asia
tub          yes         no
  yes 0.04854369 0.01040719
  no  0.95145631 0.98959281

  Parameters of node smoke (multinomial distribution)

Conditional probability table:
    yes     no 
0.2008 0.7992 

  Parameters of node lung (multinomial distribution)

Conditional probability table:
 
     smoke
lung         yes         no
  yes 0.09711155 0.01051051
  no  0.90288845 0.98948949

  Parameters of node bronc (multinomial distribution)

Conditional probability table:
 
     smoke
bronc       yes        no
  yes 0.6000996 0.2936687
  no  0.3999004 0.7063313

  Parameters of node either (multinomial distribution)

Conditional probability table:
 
, , lung = yes

      tub
either yes no
   yes   1  1
   no    0  0

, , lung = no

      tub
either yes no
   yes   1  0
   no    0  1


  Parameters of node xray (multinomial distribution)

Conditional probability table:
 
     either
xray         yes         no
  yes 0.96850394 0.05291610
  no  0.03149606 0.94708390

  Parameters of node dysp (multinomial distribution)

Conditional probability table:
 
, , either = yes

     bronc
dysp         yes         no
  yes 0.91847826 0.68020305
  no  0.08152174 0.31979695

, , either = no

     bronc
dysp         yes         no
  yes 0.80938242 0.09854423
  no  0.19061758 0.90145577

Structural learning

In many ocasions the structure of the model is designed by hand. We usually know the variables of the problems and the different causal patterns from expert knowledge, this descriptive information can be tranfered to the model. This is what we call an open box model and provides a powerful framework for many problems, specially when compared to other models that do not provide a clear interpretation of their parameters.

However, there are many situations in which we would like to automatize the structural learning of a model such as causal patterns discovery or a lack of knowledge of the domain.

In other cases we just want to learn about particular dependency relationships between the variables or select the best structure among a particular set.

bnlearn specializes in structural learning, and for this reason it provides a range of algorithms to learn the models. There is a complex taxonomy of such algorithms related to the statistical tests, metrics and heuristics in which they are based. Exact learning is a NP-hard problem and thus several approaches have been proposed.

We will briefly study the hc algorithm to learn a full structure and the BIC score metric to measure the fit of a particular network with a dataset.

The hc algorithm can be simply run from a sample of data, although many options can be set to optimize it for a particular problem.

networkInduced <- hc(x = sampleAsia)
networkInduced

  Bayesian network learned via Score-based methods

  model:
   [asia][tub][smoke][lung|smoke][bronc|smoke][either|tub:lung][xray|either][dysp|bronc:either] 
  nodes:                                 8 
  arcs:                                  7 
    undirected arcs:                     0 
    directed arcs:                       7 
  average markov blanket size:           2.25 
  average neighbourhood size:            1.75 
  average branching factor:              0.88 

  learning algorithm:                    Hill-Climbing 
  score:                                 BIC (disc.) 
  penalization coefficient:              4.60517 
  tests used in the learning procedure:  77 
  optimized:                             TRUE 

Lets compare it with the original network golden model as the algorithm may have introduced some differences given that we used a small data sample.

modelstring(asia)
[1] "[asia][smoke][tub|asia][lung|smoke][bronc|smoke][either|lung:tub][xray|either][dysp|bronc:either]"

We can also compute some metrics to compare the network. The structural Hamming distance determines the amount of discrepancy between the two graphs.

shd(bn.net(asia), networkInduced)
[1] 1

The BIC metric measures the fit of the structure for a given sample, it also penalizes the number of parameters to avoid overfitting. In this case the result is almost the same, the lower the metric the better, so it seems that the induced model could be biased towards the sample and marginally outperforms the golden model.

print(BIC(object = asia, data = sampleAsia))
[1] -19437.54
print(BIC(object = networkInduced, data = sampleAsia))
[1] -19432.09

Gaussian networks

Gaussian networks differ in the class of probability tables that represent them. If all nodes are gaussian we will find Gaussian nodes and conditional Gaussian nodes. Gaussian nodes are encoded by the normal distribution parameters (mean and sd), conditional gaussian are represented as linear reggresion with a coef for each parent, an intercept term and standard deviation of the residuals.

bnlearn has some sample gaussian data to test these BNs:

data(gaussian.test)
dag = model2network("[A][B][E][G][C|A:B][D|B][F|A:D:E:G]")
model <- bn.fit(dag, gaussian.test)
model

  Bayesian network parameters

  Parameters of node A (Gaussian distribution)

Conditional density: A
Coefficients:
(Intercept)  
   1.007493  
Standard deviation of the residuals: 1.004233 

  Parameters of node B (Gaussian distribution)

Conditional density: B
Coefficients:
(Intercept)  
   2.039499  
Standard deviation of the residuals: 3.034111 

  Parameters of node C (Gaussian distribution)

Conditional density: C | A + B
Coefficients:
(Intercept)            A            B  
   2.001083     1.995901     1.999108  
Standard deviation of the residuals: 0.5089772 

  Parameters of node D (Gaussian distribution)

Conditional density: D | B
Coefficients:
(Intercept)            B  
   5.995036     1.498395  
Standard deviation of the residuals: 0.3286672 

  Parameters of node E (Gaussian distribution)

Conditional density: E
Coefficients:
(Intercept)  
   3.493906  
Standard deviation of the residuals: 1.98986 

  Parameters of node F (Gaussian distribution)

Conditional density: F | A + D + E + G
Coefficients:
 (Intercept)             A             D             E             G  
-0.006047321   1.994853041   1.005636909   1.002577002   1.494373265  
Standard deviation of the residuals: 0.9961475 

  Parameters of node G (Gaussian distribution)

Conditional density: G
Coefficients:
(Intercept)  
   5.028076  
Standard deviation of the residuals: 1.983631 

To modify a gaussian network node we proceed as in the discrete caser:

model$A <- list(coef = c("(Intercept)" = 10), sd = 0)
model$A

  Parameters of node A (Gaussian distribution)

Conditional density: A
Coefficients:
(Intercept)  
         10  
Standard deviation of the residuals: 0 

Hybrid networks

An hybrid network contains both discrete and continuous variables. There is a framework restriction in which a discrete variable cannot have any continuous parent. The parameters are modelled as in the previous cases.

A continuous variable with discrete parents is represented by a conditional Gaussian distribution, where there is a linear gaussian distribution (according to any continuous parents) for each configuration of the discrete parents.

In the next example we will use custom.fit to manually load the parameters into a graph:

net <- model2network("[A][B][C|A:B]")
cptA  <- matrix(c(0.4, 0.6), ncol = 2, dimnames = list(NULL, c("LOW", "HIGH")))
distB <- list(coef = c("(Intercept)" = 1), sd = 1.5)
distC <- list(
  coef = 
    matrix(
      c(1.2, 2.3, 3.4, 4.5), 
      ncol = 2,
      dimnames = list(c("(Intercept)", "B"), NULL)
    ),
    sd = c(0.3, 0.6)
)
model = custom.fit(net, dist = list(A = cptA, B = distB, C = distC))
model

  Bayesian network parameters

  Parameters of node A (multinomial distribution)

Conditional probability table:
 
 LOW HIGH 
 0.4  0.6 

  Parameters of node B (Gaussian distribution)

Conditional density: B
Coefficients:
(Intercept)  
          1  
Standard deviation of the residuals: 1.5 

  Parameters of node C (conditional Gaussian distribution)

Conditional density: C | A + B
Coefficients:
               0    1
(Intercept)  1.2  3.4
B            2.3  4.5
Standard deviation of the residuals:
  0    1  
0.3  0.6  
Discrete parents' configurations:
      A
0   LOW
1  HIGH

Inference and probability queries

The inference engine of bnlearn is limited, but it can be used to test the networks and to perform basic operations with the models.

By using cpquery we can ask for the probability of an event given a set of evidence, both of them are boolean expressions involving the variables in the model. We may ask for a particular combination of configurations in the BN and a set of observed statuses for the variables in the evidence.

For example we could ask, what is the posibility of a positive cancer diagnosis for a person that smokes?, in the asia network.

(For cpquery I recommend the most powerfull lw algorithm)

# First we should observe the prior probability to compare
# TRUE is for empty evidence
cpquery(asia, event = lung == "yes", evidence = TRUE)
[1] 0.0266
cpquery(asia, event = lung == "yes", evidence = list(smoke = "yes"), method = "lw", n = 100000)
[1] 0.10054

Notice that the method is not much stable, so it is better to aggregate a repeated sample of predictions:

query <- cpquery(asia, event = lung == "yes", evidence = TRUE)
print(mean(rep(query, 1000)))
[1] 0.0216
query <- cpquery(asia, event = lung == "yes", evidence = list(smoke = "yes"), method = "lw", n = 100000)
print(mean(rep(query, 1000)))
[1] 0.0997

The other option is to use cpdist to sample cases from the network for a set of nodes in the presence of some evidence, the usage is the same, and we can obtain more stability by increasing the size of the sample.

s <- cpdist(asia, nodes = c("lung"), evidence = TRUE, n=1000)
head(s)

We can even compute or plot of the distribution:

summary(s)
  lung    
 yes: 21  
 no :979  
prop.table(table(s))
s
  yes    no 
0.021 0.979 
ggplot(s, aes(x=lung)) + geom_bar()


Modeling with Bayesian Networks

Now that we have a nice overview on the operation with bnlearn we will switch to an applied problem in which we want to build a model from a real context.

The problem

We are part of a Data Science team from an IoT company that has developed new Temperature sensor to monitor data center rooms in order to detect and predict early refrigeration failures. The sensors were installed a year ago, and we need to create a model that would be able to detect specific heat outbreaks or global heating problems in the room.

We are provided with a dataset consisting on a number of measures per day during the previous year for a total of 3 sensors that have been places in strategic places across room.

We will start to work on the first version of the problem heatAlarm-lvl1.csv.

dataRaw <- read_csv("./data/heatAlarm-lvl1.csv")
Parsed with column specification:
cols(
  Date = col_date(format = ""),
  Hour = col_character(),
  TS1 = col_double(),
  TS2 = col_double(),
  TS3 = col_double()
)
head(dataRaw)

Exploratory analisys

We will start by studying the properties of our problem at hand. First we should look into the distribution of the dataset.

summary(dataRaw)
      Date                Hour                TS1              TS2             TS3       
 Min.   :2016-01-01   Length:1830        Min.   : 4.627   Min.   :12.44   Min.   : 8.89  
 1st Qu.:2016-04-01   Class :character   1st Qu.:12.745   1st Qu.:21.41   1st Qu.:17.61  
 Median :2016-07-01   Mode  :character   Median :16.979   Median :26.09   Median :22.15  
 Mean   :2016-07-01                      Mean   :17.011   Mean   :26.12   Mean   :22.19  
 3rd Qu.:2016-10-01                      3rd Qu.:21.384   3rd Qu.:30.96   3rd Qu.:26.88  
 Max.   :2016-12-31                      Max.   :31.276   Max.   :41.89   Max.   :37.50  

We see that we have a complete year of measures and different ranges for each sensor. Now it is interesting to see the completeness of the data, we would also like to se its distribution along dates.

dataRaw %>%
  group_by(Date) %>%
  summarise(Measures = n()) %>%
  ggplot(aes(x=Measures)) + geom_histogram()

We confirm that the number of measures is stable during the day. We would now observe the distribution of the Temperature measures for each sensor.

dataRaw %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Date, y=Measure, group=Sensor, color=Sensor)) +
  geom_line()

dataRaw %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Date, y=Measure, group=Sensor, color=Sensor)) +
  geom_smooth()

dataRaw %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Date, y=Measure, group=Sensor, color=Sensor)) +
  geom_boxplot()

We observe three different distributions for each sensor. Visually they appear to be some linear variation, in fact, a simple additive relationship that could be modelled by adding a scalar intercept value. This dependency relationship can be modelled in a BN, but, what is the best way to do it?

Latent variables

The solution is elegant from the computational and the statistical point of view. We can think that the sensors are measuring some real and not observable temperature from the environment. This hidden value conditions the exact measures that the sensors capture, and thus can model its individual distributions.

In other words, our model requires an additional variable, more concretelly a Temp latent variable that cannot be observed but that is captured by each sensor by following a particular dependency, we will represent this by adding a new variable to our data (we will also remove unused variables).

dataLatent <- dataRaw %>% 
  mutate(Temp = NA) %>%
  select(Temp, TS1, TS2, TS3) %>%
  as.data.frame()
dataLatent$Temp <- as.numeric(dataLatent$Temp)

The next step is to estimate this variable to complete our model. We can learn the three network distributions from the data but not the Temp variable. For that we would use the EM algorithm. First, lets build the model:

heatAlarmDag1 <- model2network("[Temp][TS1|Temp][TS2|Temp][TS3|Temp]")
plotD3bn(heatAlarmDag1)

Now for the EM, unfortunatelly bnlearn does not provide a standalone implementation of this algorithm (it does for structural learning EM which is a superset), but it provides the tools to to so. Basically, the parametric EM algorithm can be implemented by:

  • E step: Impute the missing variables in the data by using Bayesian estimation from the actual model.
  • M step: Learning new parameters for the model by maximizing the likelihood from the imputten data (MLE).

As you can imagine these steps would be repetead though a number of iterations or convergence. To impute the latent variables we will use the function inpute, however, we would need an initial model to do so. This is another important element for the EM algorithm, as we would need a good guess to ensure a proper fit.

Lets first try with a random initialization of Temp

# Lets add uniform Temp 
dataImputed <- dataLatent %>% rowwise() %>% mutate(Temp = runif(1, 10, 25))
dataImputed %>%
  ggplot(aes(x=Temp)) + geom_density()

The EM algorithm is then plain simple

parametric.em <- function(dag, dataLatent, dataImputed, iter = 5) {
  fitted <- bn.fit(dag, dataImputed, method = "mle")
  for ( i in seq(iter)) {
   complete <- impute(fitted, data = dataLatent, method = "bayes-lw")
   fitted <- bn.fit(dag, complete, method = "mle")
  }
  fitted
}

Let’s try it

heatAlarmModel1 <- parametric.em(heatAlarmDag1, dataLatent, dataImputed, iter = 10)

Lets see the resulting model:

heatAlarmModel1

  Bayesian network parameters

  Parameters of node Temp (Gaussian distribution)

Conditional density: Temp
Coefficients:
(Intercept)  
   17.43173  
Standard deviation of the residuals: 0.3912403 

  Parameters of node TS1 (Gaussian distribution)

Conditional density: TS1 | Temp
Coefficients:
(Intercept)         Temp  
  247.33610    -13.21297  
Standard deviation of the residuals: 0.04914744 

  Parameters of node TS2 (Gaussian distribution)

Conditional density: TS2 | Temp
Coefficients:
(Intercept)         Temp  
  280.69264    -14.60381  
Standard deviation of the residuals: 0.05432086 

  Parameters of node TS3 (Gaussian distribution)

Conditional density: TS3 | Temp
Coefficients:
(Intercept)         Temp  
  269.48291    -14.18656  
Standard deviation of the residuals: 0.05276883 
# Sample some data and plot it
impute(heatAlarmModel1, data = dataLatent, method = "bayes-lw") %>%
gather("Sensor", "Measure") %>%
ggplot(aes(x=Measure, color=Sensor)) + geom_density()

Look at those intercept coefficient, they do not look right. As we can see in the plot it seems like a forced fit, where the Temp variable has a very different distribution. Lets try another guess at the prior for Temp, this time we will use a Gaussian distribution with the ststistics drawn from the sensors.

statistics <- dataLatent %>% 
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  summarise( 
    mu    = mean(Measure),
    sigma = sd(Measure)
  )
dataImputed <- dataLatent %>% 
  rowwise() %>% 
  mutate(Temp = rnorm(1, statistics$mu, statistics$sigma))
dataImputed %>%
  ggplot(aes(x=Temp)) + geom_density()

Let’s repeat the EM:

heatAlarmModel1 <- parametric.em(heatAlarmDag1, dataLatent, dataImputed, iter = 10)
heatAlarmModel1

  Bayesian network parameters

  Parameters of node Temp (Gaussian distribution)

Conditional density: Temp
Coefficients:
(Intercept)  
   21.75432  
Standard deviation of the residuals: 0.1244617 

  Parameters of node TS1 (Gaussian distribution)

Conditional density: TS1 | Temp
Coefficients:
(Intercept)         Temp  
 -886.55599     41.53506  
Standard deviation of the residuals: 0.04063577 

  Parameters of node TS2 (Gaussian distribution)

Conditional density: TS2 | Temp
Coefficients:
(Intercept)         Temp  
 -972.55651     45.90717  
Standard deviation of the residuals: 0.04491322 

  Parameters of node TS3 (Gaussian distribution)

Conditional density: TS3 | Temp
Coefficients:
(Intercept)         Temp  
 -947.95912     44.59554  
Standard deviation of the residuals: 0.04362999 
# Sample some data and plot it
impute(heatAlarmModel1, data = dataLatent, method = "bayes-lw") %>%
gather("Sensor", "Measure") %>%
ggplot(aes(x=Measure, color=Sensor)) + geom_density()

This time the fit is even worse, as we overfitted a bad prior. We will try to make a better guess, this time depedant on the actual value of the sensors.

dataImputed <- dataLatent %>% 
  rowwise() %>% 
  mutate(Temp = mean(c(TS1, TS2, TS3)))
dataImputed %>%
  ggplot(aes(x=Temp)) + geom_density()

heatAlarmModel1 <- parametric.em(heatAlarmDag1, dataLatent, dataImputed, iter = 10)
heatAlarmModel1

  Bayesian network parameters

  Parameters of node Temp (Gaussian distribution)

Conditional density: Temp
Coefficients:
(Intercept)  
   21.77576  
Standard deviation of the residuals: 5.46792 

  Parameters of node TS1 (Gaussian distribution)

Conditional density: TS1 | Temp
Coefficients:
(Intercept)         Temp  
 -3.5759540    0.9454084  
Standard deviation of the residuals: 0.05232406 

  Parameters of node TS2 (Gaussian distribution)

Conditional density: TS2 | Temp
Coefficients:
(Intercept)         Temp  
   3.368793     1.044925  
Standard deviation of the residuals: 0.05783186 

  Parameters of node TS3 (Gaussian distribution)

Conditional density: TS3 | Temp
Coefficients:
(Intercept)         Temp  
 0.08259935   1.01507008  
Standard deviation of the residuals: 0.05617952 
# Sample some data and plot it
impute(heatAlarmModel1, data = dataLatent, method = "bayes-lw") %>%
gather("Sensor", "Measure") %>%
ggplot(aes(x=Measure, color=Sensor)) + geom_density()

Now it seems that we got a nice fit, the density graph represents the sensors as scalar transformations of the latent Temp variable.

Modeling the domain

If we go back to the plot showing the evolution of the temperature along the year we can see that there could be different distributions for different periods of time. If we can obtain this discriminative information we could do better modeling the domain.

We will start by discriminating the distribution by month:

dataByMonth <- dataRaw %>%
  mutate(Month = as.factor(month(Date, label = TRUE, abbr = TRUE)))
dataByMonth %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Measure, color=Sensor, fill=Sensor)) + 
    geom_density(alpha=0.5) +
    facet_wrap(~Month)

It really seems that the month can capture additional information about the distribution. In this case, we would obtain a better fit to the data if we condition the Month as a discrete parent for the Temp variable. In this case we would need parameters for 12 different gaussian distributions, which may be a bit overkill.

Let’s try if we could summarise the month information by creating a lower dimensional variable. If we think about the weather domain we rapidly come up with the year’s seasons. Let’s try to discriminate the distributions by season.

# Lubridate cant extract seasons so we need a custom function
# We better encode the states as a dict to avoid magic strings
SeasonStates <- list(
  Winter = "winter",
  Spring = "spring",
  Summer = "summer",
  Fall = "fall"
)
season <- function(date){
  WS <- as.Date("2012-12-15", format = "%Y-%m-%d") # Winter Solstice
  SE <- as.Date("2012-3-15",  format = "%Y-%m-%d") # Spring Equinox
  SS <- as.Date("2012-6-15",  format = "%Y-%m-%d") # Summer Solstice
  FE <- as.Date("2012-9-15",  format = "%Y-%m-%d") # Fall Equinox
  
  # Convert dates from any year to 2012 dates
  d <- as.Date(strftime(date, format="2012-%m-%d"))
  
  factor(
    ifelse (d >= WS | d < SE, SeasonStates$Winter,
      ifelse (d >= SE & d < SS, SeasonStates$Spring,
        ifelse (d >= SS & d < FE, SeasonStates$Summer, SeasonStates$Fall))),
    levels = SeasonStates
  )
}
dataBySeason <- dataByMonth %>%
  mutate(Season = season(Date))
dataBySeason %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Measure, color=Sensor, fill=Sensor)) + 
    geom_density(alpha=0.5) +
    facet_wrap(~Season)

The distributions for the season discrimination seems less Gaussian, but they encode the same pattern. Lets try to model both networks and see how well they fit the parameters.

First we will model the Month based one :

dataLatentMonth <- dataByMonth %>%
  mutate(Temp = NA) %>%
  select(Month, Temp, TS1, TS2, TS3) %>%
  as.data.frame()
dataLatentMonth$Temp <- as.numeric(dataLatentMonth$Temp)

And now we model the network DAG

heatAlarmDagMonth <- model2network("[Month][Temp|Month][TS1|Temp][TS2|Temp][TS3|Temp]")
plotD3bn(heatAlarmDagMonth)

Lets repeat the EM example and induce the parameters of the hidden variable with the previous successful configuration.

dataImputedMonth <- dataLatentMonth %>% 
  rowwise() %>% 
  mutate(Temp = mean(c(TS1, TS2, TS3))) %>%
  as.data.frame()
heatAlarmModelMonth <- parametric.em(heatAlarmDagMonth, dataLatentMonth, dataImputedMonth, iter = 10)
dataImputeMonthPosterior <- impute(heatAlarmModelMonth, data = dataLatentMonth, method = "bayes-lw")
dataImputeMonthPosterior %>%
  gather("Sensor", "Measure", Temp, TS1, TS2, TS3) %>%
  ggplot(aes(x=Measure, color=Sensor, fill=Sensor)) + 
    geom_density(alpha=0.5) +
    facet_wrap(~Month)

This is a very good fit indeed. We can see that Temp is almost identical to TS3, and that the other two sensors are just transformations of this variable. Lets check it the season is a good simplification of the Month variable.

dataLatentSeason <- dataBySeason %>%
  mutate(Temp = NA) %>%
  select(Season, Temp, TS1, TS2, TS3) %>%
  as.data.frame()
dataLatentSeason$Temp <- as.numeric(dataLatentSeason$Temp)
heatAlarmDagSeason <- model2network("[Season][Temp|Season][TS1|Temp][TS2|Temp][TS3|Temp]")
dataImputedSeason <- dataLatentSeason %>% 
  rowwise() %>% 
  mutate(Temp = mean(c(TS1, TS2, TS3))) %>%
  as.data.frame()

With that configuration we run the EM

heatAlarmModelSeason <- parametric.em(heatAlarmDagSeason, dataLatentSeason, dataImputedSeason, iter = 10)
dataImputeSeasonPosterior <- impute(heatAlarmModelSeason, data = dataLatentSeason, method = "bayes-lw")
dataImputeSeasonPosterior %>%
  gather("Sensor", "Measure", Temp, TS1, TS2, TS3) %>%
  ggplot(aes(x=Measure, color=Sensor, fill=Sensor)) + 
    geom_density(alpha=0.5) +
    facet_wrap(~Season)

Another good fit. Which one of the two approaches is better?. The initial guess should be the Season one, as it has fewer parameters than using the Month. Let’s ask the BIC score.

print(BIC(heatAlarmModelSeason, dataImputeSeasonPosterior))
[1] -1004.384
print(BIC(heatAlarmModelMonth, dataImputeMonthPosterior))
[1] -4853.787

As expected, season is by far more superior at minimizing BIC than month and should be our pick.

Modeling anomalies

One of the most appealing aspects of Bayesian Networks is the ability to include expert knowledge in the model. While this is almost impossible in other frameworks it is a natural concepts in this case.

We call BNs open box models because we can understand and interpretate the parameters. This is really useful for modelling anomalies, because we can use latent variables and expert knowledge to represent events that cannot be observed.

If the events cannot be observed we would not have available data to learn from. In our example we would have to destroy machines and lit the room on fire to capture data about anomalies, and that would surely have a high cost. Imagine other scenarios such as rare diseases, autonomous driving or financial fraud.

We will start by modeling a global fault by adding a new variable Alarm, this variable will have two states yes and no. In this example we have been collecting data from the sensors for a year, we assume that the functioning has been correct and that there has not been recorder failures (perhaps because the expert technicians that maintain the room have assured us so).

Given this, the Alarm variable will be always observed to no.

# New Variables
AlarmStates <- list(
  No  = "no",
  Yes = "yes"
)
dataLatent <- dataLatentSeason %>%
  mutate(Alarm = factor(AlarmStates$No, levels = AlarmStates)) %>%
  select(Alarm, Season, Temp, TS1, TS2, TS3) %>%
  as.data.frame()

And now the new dag:

heatAlarmDag <- model2network("[Alarm][Season][Temp|Season:Alarm][TS1|Temp][TS2|Temp][TS3|Temp]")
plotD3bn(heatAlarmDag)

Lets run the EM algorithm one more time, ther should be no changes to the parameters but it will reinforce our logical pipeline.

dataImputed <- dataLatent %>% 
  rowwise() %>% 
  mutate(Temp = mean(c(TS1, TS2, TS3))) %>%
  as.data.frame()
heatAlarmModel <- parametric.em(heatAlarmDag, dataLatent, dataImputed, iter = 10)

Lets check the result on the Alarm variable:

heatAlarmModel$Alarm

  Parameters of node Alarm (multinomial distribution)

Conditional probability table:
  no yes 
  1   0 

Obviously the algorithm will fit all the cases to the observed population in which no alarms have occurred. Here is where the expert knowledge enters. We will contact our experts and design a proper parametrization of the variable.

“The system has a random failure rate of 1/1000, in such cases, the room temperature is quickly raised about 10”.

We have enough information to include this expert knowledge into de Alarm variable. Notice that this variable introduces additional parameters into the Temp variable that should be modelled to.

cptAlarm <- coef(heatAlarmModel$Alarm)
print(cptAlarm)
 no yes 
  1   0 
cptAlarm[] <- c(0.999, 0.001)
heatAlarmModel$Alarm <- cptAlarm
print(heatAlarmModel$Alarm)

  Parameters of node Alarm (multinomial distribution)

Conditional probability table:
    no   yes 
0.999 0.001 

Now for the Temp variable, which a little more complex

cgaussTemp <- coef(heatAlarmModel$Temp)
sdTemp     <- heatAlarmModel$Temp$sd
print(cgaussTemp)
                   0   1        2   3        4   5        6   7
(Intercept) 16.68242 NaN 21.16419 NaN 28.02095 NaN 21.01119 NaN
print(sdTemp)
       0        1        2        3        4        5        6        7 
3.428538      NaN 3.774607      NaN 2.444216      NaN 4.371588      NaN 
cgaussTemp[is.nan(cgaussTemp)] <- cgaussTemp[!is.nan(cgaussTemp)] + 10
sdTemp[is.nan(sdTemp)] <- sdTemp[!is.nan(sdTemp)]
heatAlarmModel$Temp <- list( coef = cgaussTemp, sd = sdTemp)
heatAlarmModel$Temp

  Parameters of node Temp (conditional Gaussian distribution)

Conditional density: Temp | Alarm + Season
Coefficients:
                    0         1         2         3         4         5         6         7
(Intercept)  16.68242  26.68242  21.16419  31.16419  28.02095  38.02095  21.01119  31.01119
Standard deviation of the residuals:
       0         1         2         3         4         5         6         7  
3.428538  3.428538  3.774607  3.774607  2.444216  2.444216  4.371588  4.371588  
Discrete parents' configurations:
   Alarm  Season
0     no  winter
1    yes  winter
2     no  spring
3    yes  spring
4     no  summer
5    yes  summer
6     no    fall
7    yes    fall

We have a complete model now, with a modelled anomaly detection schema. Lets test some queries.

e <- list( "Season" = SeasonStates$Winter, "TS1" = 23, "TS2" = 33, "TS3" = 29 )
query <- cpquery(heatAlarmModel, event = Alarm == "yes", evidence = e, method = "lw", n = 100000)
print(mean(rep(query, 10000)))
[1] 0.3232407

This temperature is too high for winter, so the alarm is likely to go up.

e <- list( "Season" = SeasonStates$Summer, "TS1" = 23, "TS2" = 33, "TS3" = 29 )
query <- cpquery(heatAlarmModel, event = Alarm == "yes", evidence = e, method = "lw", n = 100000)
print(mean(rep(query, 10000)))
[1] 0

However this is a good fit for the higher temperatures that are registered in summer.

More anomalies

We could model additional anomalies for the individual malfunctioning of each sensor. In that case we could observe either global temperature anomalies or individual malfunctions.

A common pattern is to add a new hidden discrete variable conditioning each sensor with a random distribution in the case of malfunction.

# New Variables
TS1FaultStates <- list(
  No  = "no",
  Yes = "yes"
)
dataLatent <- dataLatentSeason %>%
  mutate(
    Alarm = factor(AlarmStates$No, levels = AlarmStates),
    TS1Fault = factor(TS1FaultStates$No, levels = TS1FaultStates)
  ) %>%
  select(Alarm, Season, Temp, TS1Fault, TS1, TS2, TS3) %>%
  as.data.frame()
dataImputed <- dataLatent %>% 
  rowwise() %>% 
  mutate(Temp = mean(c(TS1, TS2, TS3))) %>%
  as.data.frame()
heatAlarmDag <- model2network("[Alarm][Season][TS1Fault][Temp|Season:Alarm][TS1|Temp:TS1Fault][TS2|Temp][TS3|Temp]")
plotD3bn(heatAlarmDag)

We launch EM and modify the new probability of the nodes (now Alarm and TS1 Fault). For that we need additional exprt knowledge from the domain. We ask our IoT team about the fault tolerance of our sensors. They tell us the following>

Our sensors are the best, they usually do not fail. The testing guys estimate just a 1/1000 failures.

This is good, with this marginal error probbility out new node will only detect anomalies without introducing noise in the remaining propagation within the model.

heatAlarmModel <- parametric.em(heatAlarmDag, dataLatent, dataImputed, iter = 10)
# TODO: Please dont do this, this is not DRY...
cptAlarm[] <- c(0.999, 0.001)
heatAlarmModel$Alarm <- cptAlarm
cgaussTemp <- coef(heatAlarmModel$Temp)
sdTemp     <- heatAlarmModel$Temp$sd
cgaussTemp[is.nan(cgaussTemp)] <- cgaussTemp[!is.nan(cgaussTemp)] + 10
sdTemp[is.nan(sdTemp)] <- sdTemp[!is.nan(sdTemp)]
heatAlarmModel$Temp <- list( coef = cgaussTemp, sd = sdTemp)
cptTS1Fault <- coef(heatAlarmModel$TS1Fault)
print(cptTS1Fault)
 no yes 
  1   0 
cptTS1Fault[] <- c(0.9999, 0.0001)
heatAlarmModel$TS1Fault <- cptTS1Fault
print(heatAlarmModel$TS1Fault)

  Parameters of node TS1Fault (multinomial distribution)

Conditional probability table:
     no    yes 
0.9999 0.0001 

And now for the TS1 node, in which we have to add a random distribution. We can use a Gaussian with lots of variance.

cgaussTS1 <- coef(heatAlarmModel$TS1)
sdTS1     <- heatAlarmModel$TS1$sd
print(cgaussTS1)
                     0   1
(Intercept) -3.7579690 NaN
Temp         0.9554187 NaN
print(sdTS1)
        0         1 
0.1100198       NaN 
cgaussTS1[is.nan(cgaussTS1)] <- 0
sdTS1[is.nan(sdTS1)] <- 1000000
heatAlarmModel$TS1 <- list( coef = cgaussTS1, sd = sdTS1)
heatAlarmModel$TS1

  Parameters of node TS1 (conditional Gaussian distribution)

Conditional density: TS1 | Temp + TS1Fault
Coefficients:
                      0           1
(Intercept)  -3.7579690   0.0000000
Temp          0.9554187   0.0000000
Standard deviation of the residuals:
           0             1  
1.100198e-01  1.000000e+06  
Discrete parents' configurations:
   TS1Fault
0        no
1       yes

Lets test some inference cases in which the TS1 sensor is malfunctioning or reporting some kind of anomalous measures.

If TS1 is only one capturing a very high temperature the fault node will detect the anomaly

e <- list( "Season" = SeasonStates$Winter, "TS1" = 35, "TS2" = 24, "TS3" = 20 )
query <- cpquery(heatAlarmModel, event = Alarm == "yes", evidence = e, method = "lw", n = 100000)
print(mean(rep(query, 10000)))
[1] 0.1508417
e <- list( "Season" = SeasonStates$Winter, "TS1" = 35, "TS2" = 24, "TS3" = 20 )
query <- cpquery(heatAlarmModel, event = TS1Fault == "yes", evidence = e, method = "lw", n = 100000)
print(mean(rep(query, 10000)))
[1] 0.4746715

If the three sensors are capturing the same anomaly, then the TS1Fault node will assume that the problem is outside the model.

e <- list( "Season" = SeasonStates$Winter, "TS1" = 35, "TS2" = 37, "TS3" = 29 )
query <- cpquery(heatAlarmModel, event = Alarm == "yes", evidence = e, method = "lw", n = 100000)
print(mean(rep(query, 10000)))
[1] 0.8
e <- list( "Season" = SeasonStates$Winter, "TS1" = 35, "TS2" = 37, "TS3" = 29 )
query <- cpquery(heatAlarmModel, event = TS1Fault == "yes", evidence = e, method = "lw", n = 100000)
print(mean(rep(query, 10000)))
[1] 0

A more complex problem

This is a variation on the previous problem. Can you spot the differences introduced by this new version?

dataRaw <- read_csv("./data/heatAlarm-lvl2.csv")
Parsed with column specification:
cols(
  Date = col_date(format = ""),
  Hour = col_character(),
  TSO = col_double(),
  TS1 = col_double(),
  TS2 = col_double(),
  TS3 = col_double()
)

It seems that the measures are somehow dependent, but the relationship is not that clear that in the previous phase. An ex

dataRaw %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Date, y=Measure, group=Sensor, color=Sensor)) +
  geom_line()

dataRaw %>%
  gather("Sensor", "Measure", TS1, TS2, TS3) %>%
  ggplot(aes(x=Date, y=Measure, group=Sensor, color=Sensor)) +
  geom_smooth()

Looking at the context

Our experts tell us that the indoor temperature of the room is affected by the outdoor weather, specially near the windows and doors. The temperature in the interior part of the room is more stable.

Given this new knowledge we decide to place a new sensor outdoors TSO. Now we can have an additional component to add to our model, hopefully it will allow to mixture the indoor and outdoor temperatures.

dataRaw %>%
  gather("Sensor", "Measure", TSO, TS1, TS2, TS3) %>%
  ggplot(aes(x=Date, y=Measure, group=Sensor, color=Sensor)) +
  geom_smooth()

dataByMonth <- dataRaw %>%
  mutate(Month = as.factor(month(Date, label = TRUE, abbr = TRUE)))
dataByMonth %>%
  gather("Sensor", "Measure", TSO, TS1, TS2, TS3) %>%
  ggplot(aes(x=Measure, color=Sensor, fill=Sensor)) + 
    geom_density(alpha=0.5) +
    facet_wrap(~Month)

dataBySeason <- dataByMonth %>%
  mutate(Season = season(Date))
dataBySeason %>%
  gather("Sensor", "Measure", TSO, TS1, TS2, TS3) %>%
  ggplot(aes(x=Measure, color=Sensor, fill=Sensor)) + 
    geom_density(alpha=0.4) +
    facet_wrap(~Season)

Adding new components to the model

To add the new sensor to the model we are going to need an additional latent variable. Our model will now have two latent variables to model the temperature TInd and TOut (indoor and outdoor). The Indoor temperature will depend on the outdoor temperature. Lets build it:

heatAlarmDag2 <- model2network("[Alarm][Season][TOut|Season][TS0|TOut][TInd|TOut:Alarm][TS1|TInd][TS2|TInd][TS3|TInd]")
plotD3bn(heatAlarmDag2)

This model is the base example of the new domain. However we can have some doubts regarding the relationship of the variable TOut and the models, look closely at the distributions and you will see that not all distributions are affected equally by the outdoor temperature. We ask again…

IoT team: Uh, yes. We instaled TS1 in a corner of the room, near the entrance. TS2 is on the ceiling and might be near a ventilation shaft, so it will probably capture a bit more of heat from the machines. TS3 should be more stable, it is located in the middle of the room.

This almost make sense visually. However, at this point we will need to test the different structures, either by learning all of them and comparing with BIC, or running a structural learning process.



License

This tutorial is licensed under GPL3 license as is originally published and maintained in this github repository by Jacinto Arias

LS0tCnRpdGxlOiAiSW50cm9kdWN0b3J5IHR1dG9yaWFsIG9uIEJheWVzaWFuIG5ldHdvcmtzIGluIFIiCmF1dGhvcjogIkphY2ludG8gQXJpYXMiCmRhdGU6ICJMYXN0IFVwZGF0ZSBPY3RvYmVyIDIwLCAyMDE3IgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIHRvYzogeWVzCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDogeWVzCi0tLQoKLS0tCgojIEludHJvZHVjdGlvbgoKSW4gdGhpcyB0dXRvcmlhbCB3ZSB3aWxsIGxlYXJuIHNvbWUgb2YgdGhlIGJhc2ljcyBvZiBCYXllc2lhbiBOZXR3b3JrcyBieSBzb2x2aW5nIGEgc2ltdWxhdGVkIHByb2JsZW0uIE91ciBhZ2VuZGEgd2lsbCBpbmNsdWRlOgoKKiBBIHF1aWNrIGJyYWNrZ3JvdW5kIHJldmlldyBvbiB0aGUgcGFja2FnZXMgd2Ugd2lsbCBiZSB1c2luZyAoYHRpZHl2ZXJzZWAgYW5kIGBibmxlYXJuYCkKKiBBIHN0YXRpc3RpY2FsIGFwcHJvYWNoIHRvIHRoZSBwcm9ibGVtIGF0IGhhbmQKKiBNb2RlbGluZyB3aXRoIGVwZXJ0IGtub3dsZWRnZQoqIExlYXJuaW5nIHBhcmFtZXRlcnMgZnJvbSBkYXRhIHdpdGggbGF0ZW50IHZhcmlhYmxlcwoqIFByb2JhYmlsaXR5IHF1ZXJpZXMgYW5kIHNpbXVsYXRpb24KKiBBZHZhbmNlZCB1c2UgY2FzZXMKCiMjIyMgU2VlZAoKYGBge3J9CnNldC5zZWVkKDEwMjApCmBgYAoKLS0tCgojIEFzaWRlOiBUaGUgdGlkeXZlcnNlCgpZb3Ugc2hvdWxkIGtub3cgdGhlIHRpZHl2ZXJzZSwgaXQgbWFrZXMgUiBlYXNpZXIgYW5kIG1vcmUgZnVuLgoKWW91IGNhbiBnZXQgbW9yZSBpbmZvcm1hdGlvbiBvbiBodHRwczovL3d3dy50aWR5dmVyc2Uub3JnCgpJIHJlYWxseSBzdWdnZXN0IHlvdSB0byB0YWtlIEhhZGxleSdzIFdpY2toYW0gRGF0YSBTY2llbmNlIGNvdXJzZSBodHRwOi8vcjRkcy5oYWQuY28ubnoKCkluIHRoaXMgdHV0b3JpYWwgSSB3aWxsIGJlIHVzaW5nIHRoZSBmb2xsb3dpbmcgdGlkeXZlcnNlIHV0aWxpdGllczoKCiogYGRwbHlyYCBpbiBnZW5lcmFsCiogYGdncGxvdDJgIGZvciBhbG1vc3QgYW55IHBsb3QKKiBgdGlkeXJgIHNwZWNpZmljYWxseSB0aGUgYGdhdGhlcmAgZnVuY3Rpb24KKiBgbHVicmlkYXRlYCB0byBleHRyYWN0IGNvbXBvbmVudHMgZnJvbSBEYXRlcwoqIGByZWFkX3JgIHRvIHJlYWQgZGF0YSBmcm9tIGNzdiBmaWxlcwoKSW4gb3JkZXIgdG8gbWFrZSB0aGUgdHV0b3JpYWwgd29yayB5b3Ugc2hvdWxkIGluc3RhbGwgYW5kIGxvYWQgdGhlIGZvbGxvd2luZyBwYWNrYWdlcwoKYGBge3IgZWNobz1UUlVFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBwYWdlZC5wcmludD1GQUxTRX0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkobHVicmlkYXRlKQpgYGAKCi0tLQoKIyBUaGUgYm5sZWFybiBwYWNrYWdlCgpUaGUgYGJubGVhcm5gIHBhY2thZ2UgaXMgdGhlIG1vc3QgY29tcGxldGUgYW5kIHBvcHVsYXIgcGFja2FnZSBmb3IgQmF5ZXNpYW4gTmV0d29ya3MgYXZhaWxhYmxlIHRvIHRoZSBkYXRlIGluIFIuIFdlIHdpbGwgc3RhcnQgb3VyIHR1dG9yaWFsIGJ5IHJldmlld2luZyBzb21lIG9mIGl0cyBjYXBhY2l0aWVzLgoKRmlyc3Qgd2Ugc2hvdWxkIGluc3RhbGwgYW5kICoqbG9hZCoqIHRoZSBwYWNrYWdlOgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgcGFnZWQucHJpbnQ9RkFMU0V9CmxpYnJhcnkoYm5sZWFybikKYGBgCgoqUGxlYXNlIG1ha2Ugc3VyZSB0aGF0IHlvdSBlaXRoZXIgZG93bmxvYWQgb3IgY29tcGlsZSBhdCBsZWFzdCBgYm5lbGFybiA0LjIueGAgYXMgcHJldmlvdXMgdmVyc2lvbnMgbWlnaHQgaGF2ZSBzb21lIGJ1Z3MuCgpZb3Ugc2hvdWxkIGNoZWNrIG91dCB0aGUgZG9jdW1lbnRhdGlvbiB0aGF0IGlzIGF2YWlsYWJsZSBvbmxpbmUgW1tMaW5rXV0oaHR0cDovL2JubGVhcm4uY29tKS4gSXQgY29udGFpbnMgbmljZSBleGFtcGxlcyBhbmQgYWRkaXRpb25hbCB1c2UgY2FzZXMgYmV5b25kIHRoaXMgdHV0b3JpYWwsIGlmIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiB0aGlzIHRvcGljIHRoZXJlIGFyZSBhIGNvdXBsZSBvZiByZWxhdGVkIGJvb2tzIHdyaXR0ZW4gYnkgdGhlIGF1dGhvciBvZiB0aGUgcGFja2FnZSwgeW91IGNhbiBmaW5kIHRoZSByZWZlcmVuY2VzIGluIHRoZSBtZW50aW9uZWQgd2Vic2l0ZS4KCgojIyBEYXRhIFN0cnVjdHVyZXMKClRoZSB1c2FnZSBvZiBibmxlYXJuIHJldm9sdmVzIGFyb3VuZCB0aGUgdXNhZ2Ugb2YgdHdvIG1haW4gKipkYXRhIHN0cnVjdHVyZXMqKiB0byByZXByZXNlbnQgYSBCYXllc2lhbiBOZXR3b3JrIGluIGRpZmZlcmVudCBzdGFnZXMgKE5PVEUgdGhhdCB0aGVzZSBhcmUgKlMzKiBjbGFzc2VzIGFuZCB0aGUgbmFtZXMgbWlnaHQgb3ZlcmxhcCB3aXRoIG90aGVyIGZ1bmN0aW9ucyk6CgoqIGBibmAgW1tEb2NzXV0oaHR0cDovL3d3dy5ibmxlYXJuLmNvbS9kb2N1bWVudGF0aW9uL21hbi9ibi5jbGFzcy5odG1sKS4gUmVwcmVzZW50cyB0aGUgc3RydWN0dXJhbCBpbmZvcm1hdGlvbiwgdmFyaWFibGVzLCBncmFwaCBhbmQgbGVhcm5pbmcgYWxnb3JpdGhtIGlmIHByb3ZpZGVkLgoqIGBibi5maXRgIFtbRG9jc11dKGh0dHA6Ly93d3cuYm5sZWFybi5jb20vZG9jdW1lbnRhdGlvbi9tYW4vYm4uZml0LmNsYXNzLmh0bWwpLiBBZGRzIHRoZSBwYXJhbWV0cmljIGluZm9ybWF0aW9uIG9uIHRvcCBvZiB0aGUgcHJldmlvdXMgc3RydWN0dXJlLiBDb250YWlucyB0aGUgZGlzdHJpYnV0aW9uIG9mIGVhY2ggbm9kZSBhY2NvcmRpbmcgdG8gaXRzIHR5cGUgYW5kIHBhcmVudCBjb25maWd1cmF0aW9uLgoKCiMjIENyZWF0aW5nIHRoZSBzdHJ1Y3R1cmUgb2YgQmF5ZXNpYW4gbmV0d29ya3MKClRoZXJlIGFyZSBkaWZmZXJlbnQgd2F5cyB0byBtYW51YWxseSBpbml0aWFsaXplIGFuZCBtb2RpZnkgdGhlIGdyYXBoIG9mIGEgQmF5ZXNpYW4gTmV0d29yay4KCldlIGNhbiBjcmVhdGUgZW1wdHkgZ3JhcGhzIGZyb20gYSBzZXQgb2YgdmFyaWFibGVzOgoKYGBge3J9CnZhcnMgPC0gTEVUVEVSU1sxOjZdCmRhZyAgPC0gZW1wdHkuZ3JhcGgodmFycykKCmRhZwpgYGAKCllvdSBjYW4gc3BlY2lmeSBhcmNzIHNldHMgaW4gdmFyaW91cyB3YXlzLiBUaGUgbW9zdCBjb21tb24gYXJlOgoKQXMgYSB0d28gY29sdW1uIChgZnJvbWAsIGB0b2ApIG1hdHJpeCwgc2V0dGluZyB0aGVtIHZpYSBgYXJjc2AgdG8gYW4gZXhpc3RpbmcgbmV0d29yazoKCmBgYHtyfQplIDwtIG1hdHJpeCgKICAgICAgYygiQSIsICJDIiwgIkIiLCAiRiIsICJDIiwgIkYiKSwKICAgICAgbmNvbCA9IDIsIGJ5cm93ID0gVFJVRSwKICAgICAgZGltbmFtZXMgPSBsaXN0KE5VTEwsIGMoImZyb20iLCAidG8iKSkKICAgICkKCmFyY3MoZGFnKSA8LSBlCgpkYWcKYGBgCgpXZSBjYW4gYWxzbyB1c2UgYW4gYWRqYW5jZWN5IG1hdHJpeCwgYW5kIGFzc2lnbiBpdCB0byBhIGRhZyB3aXRoIGBhbWF0YDoKCmBgYHtyfQphZGogPC0gbWF0cml4KAogICAgICAgIDBMLCAKICAgICAgICBuY29sID0gNiwgCiAgICAgICAgbnJvdyA9IDYsCiAgICAgICAgZGltbmFtZXMgPSBsaXN0KHZhcnMsIHZhcnMpCiAgICAgICApCgphZGpbIkEiLCAiQyJdID0gMUwKYWRqWyJCIiwgIkYiXSA9IDFMCmFkalsiQyIsICJGIl0gPSAxTAphZGpbIkQiLCAiRSJdID0gMUwKYWRqWyJBIiwgIkUiXSA9IDFMCnByaW50KGFkaikKCmFtYXQoZGFnKSA8LSBhZGoKCmRhZwpgYGAKClRoZSBsYXN0IG9wdGlvbiBpcyB0byBjcmVhdGUgYSAqZm9ybXVsYSogZm9yIGEgZ2l2ZW4gc2V0IG9mIHZhcmlhYmxlcy4gRWFjaCBub2RlIGlzIHNwZWNpZmllZCBieSBhIHBhaXIgb2YgYnJhY2tldHMgYFs8dmFyX25hbWU+XWAuIElmIHRoZSBub2RlIGhhcyBhIHBhcmVudHNldCB3ZSBkZW5vdGUgaXQgYnkgYHxgIGFuZCBzcGVjaWZ5IHRoZSBsaXN0IG9mIHBhcmVudHMgc2VwYXJhdGVkIGJ5IGNvbG9ucyBgOmAuIFdlIGNhbiBjb21wdXRlIHRoZSBmb3JtdWxhIGludG8gYSBgYm5gIG9iamVjdCB3aXRoIGBtb2RlbDJuZXR3b3JrYC4KCmBgYHtyfQpkYWcgPC0gbW9kZWwybmV0d29yaygiW0FdW0NdW0J8QV1bRHxDXVtGfEE6QjpDXVtFfEZdIikKZGFnCmBgYAoKVGhlIHBhY2thZ2UgcHJvdmlkZSBvdGhlciBjYXBhYmlsaXRpZXMgc3VjaCBhcyAqKnJhbmRvbSBncmFwaHMqKiBhbmQgKipncmFwaCBzYW1wbGluZyoqLiBJbiBhZGRpdGlvbiB0aGVyZSBpcyBhIHJhbmdlIG9mIHV0aWxpdGllcyB0byBtYW5pcHVsYXRlIHRoZSBncmFwaHMgW1tMaW5rXV0oaHR0cDovL3d3dy5ibmxlYXJuLmNvbS9kb2N1bWVudGF0aW9uL21hbi9tYi5odG1sKSBbW0xpbmtdXShodHRwOi8vd3d3LmJubGVhcm4uY29tL2RvY3VtZW50YXRpb24vbWFuL2dyYXBoLmh0bWwpLgoKCiMjIFBsb3R0aW5nIGdyYXBocwoKV2UgY2FuIHBsb3RpbmcgZ3JhcGhzIHVzaW5nIHRoZSBidWlsdCBpbiBSIGVuZ2luZSBieSB1c2luZyBgcGxvdGAgZm9yIHRoZSBgYm5gIGNsYXNzOgoKYGBge3J9CnBsb3QoZGFnKQpgYGAKCk1pbmltYWwgYXNwZWN0cyBvZiB0aGUgcGxvdCBjYW4gYmUgY3VzdG9taXplZCBhcyBkb2N1bWVudGVkIGluIHRoZSBjb3JyZXNwb25kaW5nIGhlbHAgcGFnZS4gT3RoZXIgcGFja2FnZXMgY2FuIGJlIHVzZWQgaW5kcmVjdGx5IHRvIHBsb3QgZ3JhcGhzLCBgYm5sZWFybmAgcHJvdmlkZXMgY29ubmVjdGlvbnMgd2l0aCBzb21lIG9mIHRoZW0gYnV0IGJlIGF3YXJlIHRoYXQgc29tZSBvZiB0aGVtIG1pZ2h0IGJlIG91dGRhdGVkLgoKRm9ydHVuYXRlbGx5LCBncmFwaHMgYXJlIGEgY29tbW9uIGRhdGEgc3RydWN0dXJlIGFuZCB3ZSBjYW4gZmluZCBsb3RzIG9mIHV0aWxpdGllcyB0byB3b3JrIHdpdGggdGhlbS4gT25lIG9mIHRoZSBtb3N0IGNvbW1vbiB2aXN1YWxpemF0aW9uIHRvb2xzIGZvciBncmFwaHMsIGFuZCBmb3IgZGF0YSBpbiBnZW5lcmFsLCBpcyB0aGUgRDMgbGlicmFyeSBmcm9tIHRoZSAqSmF2YXNjcmlwdCogZG9tYWluLiBUaGFua3MgdG8gdGhlIGludGVncmF0aW9uIG9mICoqUiBzaGlueSoqIHdpdGggd2ViIHRlY2hub2xvZ2llcyB3ZSBjYW4gZmluZCB3b25kZXJmdWwgcG9ydHMgc3VjaCBhcyB0aGUgYG5ldHdvcmtEM2AgcGFja2FnZS4KClRoZSBuZXh0IHNuaXBwZXQgaXMganVzdCBhIGN1c3RvbSBmdW5jdGlvbiB0byB0cmFuc2Zvcm0gYSBgYm5gIG9iamVjdCB0byB0aGUgcmVxdWlyZWQgaW5mb3JtYXRpb24gdG8gcGxvdCBhICpEMyBmb3JjZSBncmFwaCouCgoqUGxlYXNlIGluc3RhbGwqIHRoZSBgbmV0d29ya0QzYCwgYmUgY2FyZWZ1bGwgYXMgdGhpcyBwYWNrYWdlIGlzIGN1cnJlbnRseSB1bmRlciBiZXRhIGRldmVsb3BtZW50IGFuZCBmdXR1cmUgdmVyc2lvbiBjb3VsZCBicmVhayB0aGlzIGNvZGUuCgpgYGB7cn0KcGxvdEQzYm4gPC0gZnVuY3Rpb24oYm4pIHsKICB2YXJOYW1lcyA8LSBub2RlcyhibikKCiAgIyBOb2RlcyBzaG91bGQgYmUgemVybyBpbmRleGVkIQogIGxpbmtzIDwtIGRhdGEuZnJhbWUoYXJjcyhibikpICU+JQogICAgbXV0YXRlKGZyb20gPSBtYXRjaChmcm9tLCB2YXJOYW1lcyktMSwgdG8gPSBtYXRjaCh0bywgdmFyTmFtZXMpLTEsIHZhbHVlID0gMSkKICAKICBub2RlcyA8LSBkYXRhLmZyYW1lKG5hbWUgPSB2YXJOYW1lcykgJT4lCiAgICBtdXRhdGUoZ3JvdXAgPSAxLCBzaXplID0gMzApCiAgCiAgbmV0d29ya0QzOjpmb3JjZU5ldHdvcmsoCiAgICBMaW5rcyA9IGxpbmtzLCAgCiAgICBOb2RlcyA9IG5vZGVzLAogICAgU291cmNlID0gImZyb20iLAogICAgVGFyZ2V0ID0gInRvIiwKICAgIFZhbHVlID0gInZhbHVlIiwKICAgIE5vZGVJRCA9ICJuYW1lIiwKICAgIEdyb3VwID0gImdyb3VwIiwKICAgIGZvbnRTaXplID0gMjAsCiAgICB6b29tID0gVFJVRSwKICAgIGFycm93cyA9IFRSVUUsCiAgICBib3VuZGVkID0gVFJVRSwKICAgIG9wYWNpdHlOb0hvdmVyID0gMQogICkKfQpgYGAKCgpXZSBjYW4gbm93IHBsb3Qgb3VyIGxhc3QgZ2VuZXJhdGVkIEJOLiBJZiB0aGUgcmVzdWx0IGxvb2tzIHRvbyBzbWFsbCB5b3UgY2FuIHpvb20gaW4gYW5kIG91dCB1c2luZyB5b3VyICptb3VzZSB3aGVlbCouCgoKYGBge3J9CnBsb3REM2JuKGRhZykKYGBgCgoKIyMgTG9hZGluZyBCYXllc2lhbiBuZXR3b3JrcyBmcm9tIGZpbGVzCgpUaGVyZSBhcmUgZGlmZmVyZW50IGZpbGUgZm9ybWF0cyB0byByZXByZXNlbnQgYSBCYXllc2lhbiBuZXR3b3JrLiBUaGV5IGhhdmUgb3JpZ2luYXRlZCBvdmVyIHRoZSB5ZWFycyBhcyBhbiBlZmZvcnQgdG8gY3JlYXRlIHN0YW5kYXJkcyBvciBhcyBwYXJ0IG9mIHBhcnRpY3VsYXIgcHJvcGlldGFyeSBzeXN0ZW1zLiBEZXNwaXRlIHRoZSBlZmZvcnQgdGhlcmUgaXMgbm90IGEgY29uc2Vuc3VhdGVkIGNsb3NlZCBzdGFuZGFyZCwgcGVyaGFwcyB0aGUgY2xvc2VzdCBvbmUgaXMgdGhlIGB4YmlmYCBmb3JtYXQgW1tMaW5rXV0oaHR0cDovL3d3dy5jcy5jbXUuZWR1L35mZ2Nvem1hbi9SZXNlYXJjaC9JbnRlcmNoYW5nZUZvcm1hdC8pLgoKYGJubGVhcm5gIHByb3ZpZGVzIHNldmVyYWwgd2F5cyB0byBsb2FkIEJOcyBmcm9tIGRpZmZlcmVudCBmb3JtYXRzIFtbRG9jc11dKGh0dHA6Ly93d3cuYm5sZWFybi5jb20vZG9jdW1lbnRhdGlvbi9tYW4vZm9yZWlnbi5odG1sKS4gWW91IHNob3VsZCB1c2UgdGhlbSB3aGVuIGltcG9ydGluZyBhbmQgZXhwb3J0aW5nIHRvIG90aGVyIHN5c3RlbXMgb3IgdG8gc2hhcmUgdGhlbSB3aXRoIG90aGVyIHBlb3BsZSwgaWYgeW91IHBsYW4gdG8gdXNlIGp1c3QgYm5sZWFybiB5b3UgY291bGQganVzdCBzYXZlIGFuIGByZGFgIGZpbGUuCgoKIyMgVGhlIEJheWVzIG5ldCByZXBvc2l0b3J5CgpUaGUgbWFpbnRhaW5lcnMgb2YgYGJubGVhcm5gIGFsc28gcHJvdmlkZSBhIG1vZGVybiBSLWZvY3VzZWQgcmVwb3NpdG9yeSBmb3IgYSBzZXJpZXMgb2YgcG9wdWxhciBCYXllc2lhbiBuZXR3b3JrcyB0aGF0IGhhdmUgYmVlbiB1c2VkIGV4dGVuc2l2ZWxseSBmb3IgYmVuY2htYXJraW5nIG9uIHRoZSBsaXRlcmF0dXJlLgoKaHR0cDovL3d3dy5ibmxlYXJuLmNvbS9ibnJlcG9zaXRvcnkvCgpJbiBoZXJlIHlvdSBjYW4gZmluZCBuZXR3b3JrcyB3aXRoIGRpZmZlcmVudCBwcm9wZXJ0aWVzIHRoYXQgY2FuIGJlIHVzZSB0byB0ZXN0IGFsZ29yaXRobXMgb3IgZXhwbG9yZSB0aGlzIG9yIG90aGVyIEJOIHBhY2thZ2VzLiBXZSB3aWxsIG5vdyB3b3JrIHdpdGggdGhlIHNvIHBvcHVsYXIgYGFzaWFgIG5ldHdvcms6ICpBc2lhKiBpcyBmb3IgQk5zIHdoYXQgKiJpcmlzIiogaXMgZm9yIHN0YXRpc3RpY3MuCgpgYGB7cn0KIyBUaGlzIGRvd25sb2FkcyB0aGUgUkRhdGEgZmlsZSBmcm9tIHRoZSByZXBvc2l0b3J5IGFuZCBsb2FkcyBpdC4KIyBUaGUgYm4gaXMgbG9hZGVkIGludG8gYSBibi5maXQgdmFyaWFibGUgY2FsbGVkIGJuCmxvYWQodXJsKCJodHRwOi8vd3d3LmJubGVhcm4uY29tL2JucmVwb3NpdG9yeS9hc2lhL2FzaWEucmRhIikpCmFzaWEgPC0gYm4KYGBgCgoKIyMgRGlzY3JldGUgbmV0d29ya3MKCldlIHdpbGwga25vdyBleHBsb3JlIHRoZSBjb250ZW50cyBvZiB0aGlzIHZhcmlhYmxlLiBBcyB5b3UgY2FuIHNlZSwgdGhpcyBub3Qgb25seSBjb250YWlucyB0aGUgc3RydWN0dXJlIGJ1dCB0aGUgcGFyYW1ldGVycyBhcyB3ZWxsLCBidXQgZmlyc3Qgd2Ugd2lsbCB0YWtlIGEgbG9vayBhdCB0aGUgc3RydWN0dXJlOgoKYGBge3J9CmJuLm5ldChhc2lhKQpgYGAKCmBgYHtyfQpwbG90RDNibihhc2lhKQpgYGAKCk5vdyBpcyB0aGUgdGltZSB0byByZXZpZXcgdGhlIHBhcmFtZXRlcnMsIHRoaXMgcHJpbnRzIGVhY2ggbm9kZSBhbmQgdGhlIGFzb2NpYXRlZCBwcm9iYWJpbGl0eSB0YWJsZS4gSW4gdGhpcyBjYXNlIGFsbCB2YXJpYWJsZXMgYXJlICoqZGlzY3JldGUqKiBzbyB0aGUgdGFibGVzIHdvdWxkIGJlIGNvbmRpdGlvbmFsIHByb2JhYmlsaXR5IHRhYmxlcy4KCmBgYHtyfQphc2lhCmBgYAoKV2UgY2FuIGFjY2VzcyBpbmRpdmlkdWFsIG5vZGVzIG9mIHRoZSBuZXQgYXMgaW4gYSBkYXRhLmZyYW1lOgoKYGBge3J9CmFzaWEkc21va2UKYGBgCgpUaGVyZSBpcyBhbHNvIGEgZnVuY3Rpb24gdG8gcGxvdCB0aGUgZGlzdHJpYnV0aW9ucyBvZiBkaXNjcmV0ZSBuZXR3b3JrczoKCmBgYHtyfQpibi5maXQuYmFyY2hhcnQoYXNpYSRzbW9rZSkKYGBgCgpgYGB7cn0KYm4uZml0LmJhcmNoYXJ0KGFzaWEkZHlzcCkKYGBgCgoKIyMgSW50cm9kdWNpbmcgZXhwZXJ0IGtub3dsZWRnZQoKV2UgY2FuIGFsdGVyIHRoZSBwcm9iYWJpbGl0eSB0YWJsZXMgb2YgYSBCTiwgdGhpcyBpcyB1c2VmdWwgdG8gb3ZlcnJpZGUgcGFyYW1ldGVycyBsZWFybnQgZnJvbSBkYXRhIG9yIG5vdCBvYnNlcnZlZCB2YXJpYWJsZXMuIFRoaXMgbWV0aG9kIGFsbG93cyB1cyB0byBpbmNsdWRlIGV4cGVydCBpbmZvcm1hdGlvbiBmcm9tIHRoZSBkb21haW4gb2YgdGhlIHByb2JsZW0gbW9kZWxsZWQuCgpUbyBtb2RpZnkgYSAqKmNvbmRpdGlvbmFsIHByb2JhYmlsaXR5IHRhYmxlKiogeW91IGNhbiBqdXN0IGRpcmVjdGx5IHJlcGxhY2UgdGhlIGV4aXN0aW5nIHRhYmxlIGluIHRoZSBtb2RlbCBieSBleHRyYWN0aW5nIGl0IHdpdGggYGNvZWZgLiBCZSBjYXJlZnVsIHRvIG1haW50YWluIHRoZSBpbmhlcmVudCByZXN0cmljdGlvbnMgb2YgdGhlIHByb2JhYmlsaXR5IGRpc3RyaWJ1dGlvbi4KCmBgYHtyfQpjcHQgPC0gY29lZihhc2lhJHNtb2tlKQpjcHRbXSA8LSBjKDAuMiwgMC44KQphc2lhJHNtb2tlIDwtIGNwdAphc2lhJHNtb2tlCmBgYAoKCiMjIFNhbXBsaW5nIGRhdGEgZnJvbSBhIEJheWVzaWFuIG5ldHdvcmsKCmBibmxlYXJuYCBpbnRyb2R1Y2VzIGFuICpSIGxpa2UqIGZ1bmN0aW9uIHRvIHNhbXBsZSBkYXRhIGZyb20gYSBnaXZlbiBmaXR0ZWQgbW9kZWwgYHJibmAuIFdlIHdpbGwgbm93IHNhbXBsZSBhIGRhdGFzZXQgZnJvbSAqYXNpYSogdG8gdGVzdCBsZWFybmluZyBmcm9tIGRhdGEuCgpgYGB7cn0KIyBOb3RlIHRoYXQgdGhlIG9yZGVyIG9mIHRoZSBwYXJhbWV0ZXJzIGlzIGludmVydGVkIGZyb20gdGhlIFIgc2FtcGxlIGZ1bmN0aW9ucwpzYW1wbGVBc2lhIDwtIHJibih4ID0gYXNpYSwgbiA9IDEwMDAwKQpoZWFkKHNhbXBsZUFzaWEpCmBgYAoKCiMjIFBhcmFtZXRyaWMgbGVhcm5pbmcgZnJvbSBkYXRhCgpXZSBjYW4gaW5kdWNlIHRoZSBwYXJhbWV0ZXJzIG9mIGEgQmF5ZXNpYW4gTmV0d29yayBmcm9tIG9ic2VydmVkIGRhdGEuIGBibmxlYXJuYCBwcm92aWRlcyBkaWZmZXJlbnQgYWxnb3JpdGhtcyBmb3IgdGhhdCwgd2hpbGUgTWF4aW11bSBMaWtlbGlob29kIEVzdGltYXRpb24gKE1MRSkgaXMgdGhlIG1vc3QgY29tbW9uIG9uZS4KCldlIGNhbiBpbnZva2UgdGhlIGxlYXJuaW5nIGFsZ29yaXRobSBieSB1c2luZyB0aGUgZnVuY3Rpb24gYGJuLmZpdGAuIEZvciB0aGF0IHdlIG5lZWQgYSBEQUcgYW5kIGEgY29tcGF0aWJsZSBkYXRhc2V0OgoKYGBge3J9Cm5ldCA8LSBibi5uZXQoYXNpYSkKYXNpYUluZHVjZWQgPC0gYm4uZml0KHggPSBuZXQsIGRhdGEgPSBzYW1wbGVBc2lhKQpgYGAKCldlIGNhbiBub3cgY29tcGFyZSB0aGUgdHdvIG5ldHdvcmtzLCB0aGVyZSBzaG91bGQgYmUgc29tZSBkaXNjcmVwYWNpZXMgaW4gdGhlIGluZHVjZWQgb25lLiBOb3RpY2UgdGhhdCBleHRyZW1lbGx5IG1hcmdpbmFsIHByb2JhYmlsaXRpZXMgd2lsbCBub3QgYmUgc2ltdWxhdGVkIGFuZCB0aHVzIHdpbGwgbm90IGhhdmUgYSBzaWduaWZpY2FudCBwcmVzZW50IGluIHRoZSBzYW1wbGUuCgpgYGB7cn0KYXNpYUluZHVjZWQKYGBgCgojIyBTdHJ1Y3R1cmFsIGxlYXJuaW5nCgpJbiBtYW55IG9jYXNpb25zIHRoZSBzdHJ1Y3R1cmUgb2YgdGhlIG1vZGVsIGlzIGRlc2lnbmVkIGJ5IGhhbmQuIFdlIHVzdWFsbHkga25vdyB0aGUgdmFyaWFibGVzIG9mIHRoZSBwcm9ibGVtcyBhbmQgdGhlIGRpZmZlcmVudCBjYXVzYWwgcGF0dGVybnMgZnJvbSBleHBlcnQga25vd2xlZGdlLCB0aGlzIGRlc2NyaXB0aXZlIGluZm9ybWF0aW9uIGNhbiBiZSB0cmFuZmVyZWQgdG8gdGhlIG1vZGVsLiBUaGlzIGlzIHdoYXQgd2UgY2FsbCBhbiBvcGVuIGJveCBtb2RlbCBhbmQgcHJvdmlkZXMgYSBwb3dlcmZ1bCBmcmFtZXdvcmsgZm9yIG1hbnkgcHJvYmxlbXMsIHNwZWNpYWxseSB3aGVuIGNvbXBhcmVkIHRvIG90aGVyIG1vZGVscyB0aGF0IGRvIG5vdCBwcm92aWRlIGEgY2xlYXIgaW50ZXJwcmV0YXRpb24gb2YgdGhlaXIgcGFyYW1ldGVycy4KCkhvd2V2ZXIsIHRoZXJlIGFyZSBtYW55IHNpdHVhdGlvbnMgaW4gd2hpY2ggd2Ugd291bGQgbGlrZSB0byBhdXRvbWF0aXplIHRoZSBzdHJ1Y3R1cmFsIGxlYXJuaW5nIG9mIGEgbW9kZWwgc3VjaCBhcyBjYXVzYWwgcGF0dGVybnMgZGlzY292ZXJ5IG9yIGEgbGFjayBvZiBrbm93bGVkZ2Ugb2YgdGhlIGRvbWFpbi4KCkluIG90aGVyIGNhc2VzIHdlIGp1c3Qgd2FudCB0byBsZWFybiBhYm91dCBwYXJ0aWN1bGFyIGRlcGVuZGVuY3kgcmVsYXRpb25zaGlwcyBiZXR3ZWVuIHRoZSB2YXJpYWJsZXMgb3Igc2VsZWN0IHRoZSBiZXN0IHN0cnVjdHVyZSBhbW9uZyBhIHBhcnRpY3VsYXIgc2V0LgoKYGJubGVhcm5gIHNwZWNpYWxpemVzIGluIHN0cnVjdHVyYWwgbGVhcm5pbmcsIGFuZCBmb3IgdGhpcyByZWFzb24gaXQgcHJvdmlkZXMgYSByYW5nZSBvZiBhbGdvcml0aG1zIHRvIGxlYXJuIHRoZSBtb2RlbHMuIFRoZXJlIGlzIGEgY29tcGxleCB0YXhvbm9teSBvZiBzdWNoIGFsZ29yaXRobXMgcmVsYXRlZCB0byB0aGUgc3RhdGlzdGljYWwgdGVzdHMsIG1ldHJpY3MgYW5kIGhldXJpc3RpY3MgaW4gd2hpY2ggdGhleSBhcmUgYmFzZWQuIEV4YWN0IGxlYXJuaW5nIGlzIGEgTlAtaGFyZCBwcm9ibGVtIGFuZCB0aHVzIHNldmVyYWwgYXBwcm9hY2hlcyBoYXZlIGJlZW4gcHJvcG9zZWQuCgpXZSB3aWxsIGJyaWVmbHkgc3R1ZHkgdGhlIGBoY2AgYWxnb3JpdGhtIHRvIGxlYXJuIGEgZnVsbCBzdHJ1Y3R1cmUgYW5kIHRoZSBgQklDYCBzY29yZSBtZXRyaWMgdG8gbWVhc3VyZSB0aGUgZml0IG9mIGEgcGFydGljdWxhciBuZXR3b3JrIHdpdGggYSBkYXRhc2V0LgoKVGhlIGBoY2AgYWxnb3JpdGhtIGNhbiBiZSBzaW1wbHkgcnVuIGZyb20gYSBzYW1wbGUgb2YgZGF0YSwgYWx0aG91Z2ggbWFueSBvcHRpb25zIGNhbiBiZSBzZXQgdG8gb3B0aW1pemUgaXQgZm9yIGEgcGFydGljdWxhciBwcm9ibGVtLgoKYGBge3J9Cm5ldHdvcmtJbmR1Y2VkIDwtIGhjKHggPSBzYW1wbGVBc2lhKQpuZXR3b3JrSW5kdWNlZApgYGAKCkxldHMgY29tcGFyZSBpdCB3aXRoIHRoZSBvcmlnaW5hbCBuZXR3b3JrICoqZ29sZGVuIG1vZGVsKiogYXMgdGhlIGFsZ29yaXRobSBtYXkgaGF2ZSBpbnRyb2R1Y2VkIHNvbWUgZGlmZmVyZW5jZXMgZ2l2ZW4gdGhhdCB3ZSB1c2VkIGEgc21hbGwgZGF0YSBzYW1wbGUuCgpgYGB7cn0KbW9kZWxzdHJpbmcoYXNpYSkKYGBgCgpXZSBjYW4gYWxzbyBjb21wdXRlIHNvbWUgbWV0cmljcyB0byBjb21wYXJlIHRoZSBuZXR3b3JrLiBUaGUgc3RydWN0dXJhbCBIYW1taW5nIGRpc3RhbmNlIGRldGVybWluZXMgdGhlIGFtb3VudCBvZiBkaXNjcmVwYW5jeSBiZXR3ZWVuIHRoZSB0d28gZ3JhcGhzLgoKYGBge3J9CnNoZChibi5uZXQoYXNpYSksIG5ldHdvcmtJbmR1Y2VkKQpgYGAKClRoZSBCSUMgbWV0cmljIG1lYXN1cmVzIHRoZSBmaXQgb2YgdGhlIHN0cnVjdHVyZSBmb3IgYSBnaXZlbiBzYW1wbGUsIGl0IGFsc28gcGVuYWxpemVzIHRoZSBudW1iZXIgb2YgcGFyYW1ldGVycyB0byBhdm9pZCBvdmVyZml0dGluZy4gSW4gdGhpcyBjYXNlIHRoZSByZXN1bHQgaXMgYWxtb3N0IHRoZSBzYW1lLCB0aGUgbG93ZXIgdGhlIG1ldHJpYyB0aGUgYmV0dGVyLCBzbyBpdCBzZWVtcyB0aGF0IHRoZSBpbmR1Y2VkIG1vZGVsIGNvdWxkIGJlIGJpYXNlZCB0b3dhcmRzIHRoZSBzYW1wbGUgYW5kIG1hcmdpbmFsbHkgb3V0cGVyZm9ybXMgdGhlIGdvbGRlbiBtb2RlbC4KCmBgYHtyfQpwcmludChCSUMob2JqZWN0ID0gYXNpYSwgZGF0YSA9IHNhbXBsZUFzaWEpKQpwcmludChCSUMob2JqZWN0ID0gbmV0d29ya0luZHVjZWQsIGRhdGEgPSBzYW1wbGVBc2lhKSkKYGBgCgoKIyMgR2F1c3NpYW4gbmV0d29ya3MKCkdhdXNzaWFuIG5ldHdvcmtzIGRpZmZlciBpbiB0aGUgY2xhc3Mgb2YgcHJvYmFiaWxpdHkgdGFibGVzIHRoYXQgcmVwcmVzZW50IHRoZW0uIElmIGFsbCBub2RlcyBhcmUgZ2F1c3NpYW4gd2Ugd2lsbCBmaW5kIEdhdXNzaWFuIG5vZGVzIGFuZCBjb25kaXRpb25hbCBHYXVzc2lhbiBub2Rlcy4gR2F1c3NpYW4gbm9kZXMgYXJlIGVuY29kZWQgYnkgdGhlIG5vcm1hbCBkaXN0cmlidXRpb24gcGFyYW1ldGVycyAobWVhbiBhbmQgc2QpLCBjb25kaXRpb25hbCBnYXVzc2lhbiBhcmUgcmVwcmVzZW50ZWQgYXMgbGluZWFyIHJlZ2dyZXNpb24gd2l0aCBhIGNvZWYgZm9yIGVhY2ggcGFyZW50LCBhbiBpbnRlcmNlcHQgdGVybSBhbmQgc3RhbmRhcmQgZGV2aWF0aW9uIG9mIHRoZSByZXNpZHVhbHMuCgpgYm5sZWFybmAgaGFzIHNvbWUgc2FtcGxlIGdhdXNzaWFuIGRhdGEgdG8gdGVzdCB0aGVzZSBCTnM6CgpgYGB7cn0KZGF0YShnYXVzc2lhbi50ZXN0KQpkYWcgPSBtb2RlbDJuZXR3b3JrKCJbQV1bQl1bRV1bR11bQ3xBOkJdW0R8Ql1bRnxBOkQ6RTpHXSIpCm1vZGVsIDwtIGJuLmZpdChkYWcsIGdhdXNzaWFuLnRlc3QpCm1vZGVsCmBgYAoKVG8gbW9kaWZ5IGEgZ2F1c3NpYW4gbmV0d29yayBub2RlIHdlIHByb2NlZWQgYXMgaW4gdGhlIGRpc2NyZXRlIGNhc2VyOgoKYGBge3J9Cm1vZGVsJEEgPC0gbGlzdChjb2VmID0gYygiKEludGVyY2VwdCkiID0gMTApLCBzZCA9IDApCm1vZGVsJEEKCmBgYAoKCiMjIEh5YnJpZCBuZXR3b3JrcwoKQW4gaHlicmlkIG5ldHdvcmsgY29udGFpbnMgYm90aCBkaXNjcmV0ZSBhbmQgY29udGludW91cyB2YXJpYWJsZXMuICoqVGhlcmUgaXMgYSBmcmFtZXdvcmsgcmVzdHJpY3Rpb24gaW4gd2hpY2ggYSBkaXNjcmV0ZSB2YXJpYWJsZSBjYW5ub3QgaGF2ZSBhbnkgY29udGludW91cyBwYXJlbnQqKi4gVGhlIHBhcmFtZXRlcnMgYXJlIG1vZGVsbGVkIGFzIGluIHRoZSBwcmV2aW91cyBjYXNlcy4KCkEgY29udGludW91cyB2YXJpYWJsZSB3aXRoIGRpc2NyZXRlIHBhcmVudHMgaXMgcmVwcmVzZW50ZWQgYnkgYSBjb25kaXRpb25hbCBHYXVzc2lhbiBkaXN0cmlidXRpb24sIHdoZXJlIHRoZXJlIGlzIGEgbGluZWFyIGdhdXNzaWFuIGRpc3RyaWJ1dGlvbiAoYWNjb3JkaW5nIHRvIGFueSBjb250aW51b3VzIHBhcmVudHMpIGZvciBlYWNoIGNvbmZpZ3VyYXRpb24gb2YgdGhlIGRpc2NyZXRlIHBhcmVudHMuCgpJbiB0aGUgbmV4dCBleGFtcGxlIHdlIHdpbGwgdXNlIGBjdXN0b20uZml0YCB0byBtYW51YWxseSBsb2FkIHRoZSBwYXJhbWV0ZXJzIGludG8gYSBncmFwaDoKCmBgYHtyfQpuZXQgPC0gbW9kZWwybmV0d29yaygiW0FdW0JdW0N8QTpCXSIpCgpjcHRBICA8LSBtYXRyaXgoYygwLjQsIDAuNiksIG5jb2wgPSAyLCBkaW1uYW1lcyA9IGxpc3QoTlVMTCwgYygiTE9XIiwgIkhJR0giKSkpCmRpc3RCIDwtIGxpc3QoY29lZiA9IGMoIihJbnRlcmNlcHQpIiA9IDEpLCBzZCA9IDEuNSkKZGlzdEMgPC0gbGlzdCgKICBjb2VmID0gCiAgICBtYXRyaXgoCiAgICAgIGMoMS4yLCAyLjMsIDMuNCwgNC41KSwgCiAgICAgIG5jb2wgPSAyLAogICAgICBkaW1uYW1lcyA9IGxpc3QoYygiKEludGVyY2VwdCkiLCAiQiIpLCBOVUxMKQogICAgKSwKICAgIHNkID0gYygwLjMsIDAuNikKKQoKbW9kZWwgPSBjdXN0b20uZml0KG5ldCwgZGlzdCA9IGxpc3QoQSA9IGNwdEEsIEIgPSBkaXN0QiwgQyA9IGRpc3RDKSkKbW9kZWwKYGBgCgojIyBJbmZlcmVuY2UgYW5kIHByb2JhYmlsaXR5IHF1ZXJpZXMKClRoZSBpbmZlcmVuY2UgZW5naW5lIG9mIGBibmxlYXJuYCBpcyBsaW1pdGVkLCBidXQgaXQgY2FuIGJlIHVzZWQgdG8gdGVzdCB0aGUgbmV0d29ya3MgYW5kIHRvIHBlcmZvcm0gYmFzaWMgb3BlcmF0aW9ucyB3aXRoIHRoZSBtb2RlbHMuCgpCeSB1c2luZyBgY3BxdWVyeWAgd2UgY2FuIGFzayBmb3IgdGhlIHByb2JhYmlsaXR5IG9mIGFuICoqZXZlbnQqKiBnaXZlbiBhIHNldCBvZiBldmlkZW5jZSwgYm90aCBvZiB0aGVtIGFyZSBib29sZWFuIGV4cHJlc3Npb25zIGludm9sdmluZyB0aGUgdmFyaWFibGVzIGluIHRoZSBtb2RlbC4gV2UgbWF5IGFzayBmb3IgYSBwYXJ0aWN1bGFyIGNvbWJpbmF0aW9uIG9mIGNvbmZpZ3VyYXRpb25zIGluIHRoZSBCTiBhbmQgYSBzZXQgb2Ygb2JzZXJ2ZWQgc3RhdHVzZXMgZm9yIHRoZSB2YXJpYWJsZXMgaW4gdGhlIGV2aWRlbmNlLgoKRm9yIGV4YW1wbGUgd2UgY291bGQgYXNrLCAgKndoYXQgaXMgdGhlIHBvc2liaWxpdHkgb2YgYSBwb3NpdGl2ZSBjYW5jZXIgZGlhZ25vc2lzIGZvciBhIHBlcnNvbiB0aGF0IHNtb2tlcz8qLCBpbiB0aGUgYXNpYSBuZXR3b3JrLgoKKEZvciBjcHF1ZXJ5IEkgcmVjb21tZW5kIHRoZSBtb3N0IHBvd2VyZnVsbCBgbHdgIGFsZ29yaXRobSkKCmBgYHtyfQojIEZpcnN0IHdlIHNob3VsZCBvYnNlcnZlIHRoZSBwcmlvciBwcm9iYWJpbGl0eSB0byBjb21wYXJlCiMgVFJVRSBpcyBmb3IgZW1wdHkgZXZpZGVuY2UKY3BxdWVyeShhc2lhLCBldmVudCA9IGx1bmcgPT0gInllcyIsIGV2aWRlbmNlID0gVFJVRSkKYGBgCmBgYHtyfQpjcHF1ZXJ5KGFzaWEsIGV2ZW50ID0gbHVuZyA9PSAieWVzIiwgZXZpZGVuY2UgPSBsaXN0KHNtb2tlID0gInllcyIpLCBtZXRob2QgPSAibHciLCBuID0gMTAwMDAwKQpgYGAKCk5vdGljZSB0aGF0IHRoZSBtZXRob2QgaXMgbm90IG11Y2ggc3RhYmxlLCBzbyBpdCBpcyBiZXR0ZXIgdG8gYWdncmVnYXRlIGEgcmVwZWF0ZWQgc2FtcGxlIG9mIHByZWRpY3Rpb25zOgoKYGBge3J9CnF1ZXJ5IDwtIGNwcXVlcnkoYXNpYSwgZXZlbnQgPSBsdW5nID09ICJ5ZXMiLCBldmlkZW5jZSA9IFRSVUUpCnByaW50KG1lYW4ocmVwKHF1ZXJ5LCAxMDAwKSkpCgpxdWVyeSA8LSBjcHF1ZXJ5KGFzaWEsIGV2ZW50ID0gbHVuZyA9PSAieWVzIiwgZXZpZGVuY2UgPSBsaXN0KHNtb2tlID0gInllcyIpLCBtZXRob2QgPSAibHciLCBuID0gMTAwMDAwKQpwcmludChtZWFuKHJlcChxdWVyeSwgMTAwMCkpKQpgYGAKClRoZSBvdGhlciBvcHRpb24gaXMgdG8gdXNlIGBjcGRpc3RgIHRvIHNhbXBsZSBjYXNlcyBmcm9tIHRoZSBuZXR3b3JrIGZvciBhIHNldCBvZiBub2RlcyBpbiB0aGUgcHJlc2VuY2Ugb2Ygc29tZSBldmlkZW5jZSwgdGhlIHVzYWdlIGlzIHRoZSBzYW1lLCBhbmQgd2UgY2FuIG9idGFpbiBtb3JlIHN0YWJpbGl0eSBieSBpbmNyZWFzaW5nIHRoZSBzaXplIG9mIHRoZSBzYW1wbGUuCgpgYGB7cn0KcyA8LSBjcGRpc3QoYXNpYSwgbm9kZXMgPSBjKCJsdW5nIiksIGV2aWRlbmNlID0gVFJVRSwgbj0xMDAwKQpoZWFkKHMpCmBgYAoKV2UgY2FuIGV2ZW4gY29tcHV0ZSBvciBwbG90IG9mIHRoZSBkaXN0cmlidXRpb246CgpgYGB7cn0Kc3VtbWFyeShzKQpgYGAKCmBgYHtyfQpwcm9wLnRhYmxlKHRhYmxlKHMpKQpgYGAKCgpgYGB7cn0KZ2dwbG90KHMsIGFlcyh4PWx1bmcpKSArIGdlb21fYmFyKCkKYGBgCgotLS0KCiMgTW9kZWxpbmcgd2l0aCBCYXllc2lhbiBOZXR3b3JrcwoKTm93IHRoYXQgd2UgaGF2ZSBhIG5pY2Ugb3ZlcnZpZXcgb24gdGhlIG9wZXJhdGlvbiB3aXRoIGBibmxlYXJuYCB3ZSB3aWxsIHN3aXRjaCB0byBhbiBhcHBsaWVkIHByb2JsZW0gaW4gd2hpY2ggd2Ugd2FudCB0byBidWlsZCBhIG1vZGVsIGZyb20gYSByZWFsIGNvbnRleHQuCgojIyBUaGUgcHJvYmxlbQoKV2UgYXJlIHBhcnQgb2YgYSBEYXRhIFNjaWVuY2UgdGVhbSBmcm9tIGFuIElvVCBjb21wYW55IHRoYXQgaGFzIGRldmVsb3BlZCBuZXcgKlRlbXBlcmF0dXJlKiBzZW5zb3IgdG8gbW9uaXRvciBkYXRhIGNlbnRlciByb29tcyBpbiBvcmRlciB0byBkZXRlY3QgYW5kIHByZWRpY3QgZWFybHkgcmVmcmlnZXJhdGlvbiBmYWlsdXJlcy4gVGhlIHNlbnNvcnMgd2VyZSBpbnN0YWxsZWQgYSB5ZWFyIGFnbywgYW5kIHdlIG5lZWQgdG8gY3JlYXRlIGEgbW9kZWwgdGhhdCB3b3VsZCBiZSBhYmxlIHRvIGRldGVjdCBzcGVjaWZpYyBoZWF0IG91dGJyZWFrcyBvciBnbG9iYWwgaGVhdGluZyBwcm9ibGVtcyBpbiB0aGUgcm9vbS4KCldlIGFyZSBwcm92aWRlZCB3aXRoIGEgZGF0YXNldCBjb25zaXN0aW5nIG9uIGEgbnVtYmVyIG9mIG1lYXN1cmVzIHBlciBkYXkgZHVyaW5nIHRoZSBwcmV2aW91cyB5ZWFyIGZvciBhIHRvdGFsIG9mICozIHNlbnNvcnMqIHRoYXQgaGF2ZSBiZWVuIHBsYWNlcyBpbiBzdHJhdGVnaWMgcGxhY2VzIGFjcm9zcyByb29tLgoKV2Ugd2lsbCBzdGFydCB0byB3b3JrIG9uIHRoZSBmaXJzdCB2ZXJzaW9uIG9mIHRoZSBwcm9ibGVtIGBoZWF0QWxhcm0tbHZsMS5jc3ZgLgoKYGBge3J9CmRhdGFSYXcgPC0gcmVhZF9jc3YoIi4vZGF0YS9oZWF0QWxhcm0tbHZsMS5jc3YiKQpoZWFkKGRhdGFSYXcpCmBgYAoKIyMgRXhwbG9yYXRvcnkgYW5hbGlzeXMKCldlIHdpbGwgc3RhcnQgYnkgc3R1ZHlpbmcgdGhlIHByb3BlcnRpZXMgb2Ygb3VyIHByb2JsZW0gYXQgaGFuZC4gRmlyc3Qgd2Ugc2hvdWxkIGxvb2sgaW50byB0aGUgZGlzdHJpYnV0aW9uIG9mIHRoZSBkYXRhc2V0LgoKYGBge3J9CnN1bW1hcnkoZGF0YVJhdykKYGBgCgpXZSBzZWUgdGhhdCB3ZSBoYXZlIGEgY29tcGxldGUgeWVhciBvZiBtZWFzdXJlcyBhbmQgZGlmZmVyZW50IHJhbmdlcyBmb3IgZWFjaCBzZW5zb3IuIE5vdyBpdCBpcyBpbnRlcmVzdGluZyB0byBzZWUgdGhlIGNvbXBsZXRlbmVzcyBvZiB0aGUgZGF0YSwgd2Ugd291bGQgYWxzbyBsaWtlIHRvIHNlIGl0cyBkaXN0cmlidXRpb24gYWxvbmcgZGF0ZXMuCgpgYGB7cn0KZGF0YVJhdyAlPiUKICBncm91cF9ieShEYXRlKSAlPiUKICBzdW1tYXJpc2UoTWVhc3VyZXMgPSBuKCkpICU+JQogIGdncGxvdChhZXMoeD1NZWFzdXJlcykpICsgZ2VvbV9oaXN0b2dyYW0oKQpgYGAKCldlIGNvbmZpcm0gdGhhdCB0aGUgbnVtYmVyIG9mIG1lYXN1cmVzIGlzIHN0YWJsZSBkdXJpbmcgdGhlIGRheS4gV2Ugd291bGQgbm93IG9ic2VydmUgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGUgVGVtcGVyYXR1cmUgbWVhc3VyZXMgZm9yIGVhY2ggc2Vuc29yLgoKYGBge3J9CmRhdGFSYXcgJT4lCiAgZ2F0aGVyKCJTZW5zb3IiLCAiTWVhc3VyZSIsIFRTMSwgVFMyLCBUUzMpICU+JQogIGdncGxvdChhZXMoeD1EYXRlLCB5PU1lYXN1cmUsIGdyb3VwPVNlbnNvciwgY29sb3I9U2Vuc29yKSkgKwogIGdlb21fbGluZSgpCgpkYXRhUmF3ICU+JQogIGdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9RGF0ZSwgeT1NZWFzdXJlLCBncm91cD1TZW5zb3IsIGNvbG9yPVNlbnNvcikpICsKICBnZW9tX3Ntb290aCgpCgpkYXRhUmF3ICU+JQogIGdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9RGF0ZSwgeT1NZWFzdXJlLCBncm91cD1TZW5zb3IsIGNvbG9yPVNlbnNvcikpICsKICBnZW9tX2JveHBsb3QoKQpgYGAKCldlIG9ic2VydmUgdGhyZWUgZGlmZmVyZW50IGRpc3RyaWJ1dGlvbnMgZm9yIGVhY2ggc2Vuc29yLiBWaXN1YWxseSB0aGV5IGFwcGVhciB0byBiZSBzb21lIGxpbmVhciB2YXJpYXRpb24sIGluIGZhY3QsIGEgc2ltcGxlIGFkZGl0aXZlIHJlbGF0aW9uc2hpcCB0aGF0IGNvdWxkIGJlIG1vZGVsbGVkIGJ5IGFkZGluZyBhIHNjYWxhciBpbnRlcmNlcHQgdmFsdWUuIFRoaXMgKipkZXBlbmRlbmN5KiogcmVsYXRpb25zaGlwIGNhbiBiZSBtb2RlbGxlZCBpbiBhIEJOLCBidXQsIHdoYXQgaXMgdGhlIGJlc3Qgd2F5IHRvIGRvIGl0PwoKIyMgTGF0ZW50IHZhcmlhYmxlcwoKVGhlIHNvbHV0aW9uIGlzIGVsZWdhbnQgZnJvbSB0aGUgY29tcHV0YXRpb25hbCBhbmQgdGhlIHN0YXRpc3RpY2FsIHBvaW50IG9mIHZpZXcuIFdlIGNhbiB0aGluayB0aGF0IHRoZSBzZW5zb3JzIGFyZSAqKm1lYXN1cmluZyoqIHNvbWUgcmVhbCBhbmQgbm90IG9ic2VydmFibGUgdGVtcGVyYXR1cmUgZnJvbSB0aGUgZW52aXJvbm1lbnQuIFRoaXMgaGlkZGVuIHZhbHVlIGNvbmRpdGlvbnMgdGhlIGV4YWN0IG1lYXN1cmVzIHRoYXQgdGhlIHNlbnNvcnMgY2FwdHVyZSwgYW5kIHRodXMgY2FuIG1vZGVsIGl0cyBpbmRpdmlkdWFsIGRpc3RyaWJ1dGlvbnMuCgpJbiBvdGhlciB3b3Jkcywgb3VyIG1vZGVsIHJlcXVpcmVzIGFuIGFkZGl0aW9uYWwgdmFyaWFibGUsIG1vcmUgY29uY3JldGVsbHkgYSAqKlRlbXAqKiBsYXRlbnQgdmFyaWFibGUgdGhhdCBjYW5ub3QgYmUgb2JzZXJ2ZWQgYnV0IHRoYXQgaXMgY2FwdHVyZWQgYnkgZWFjaCBzZW5zb3IgYnkgZm9sbG93aW5nIGEgcGFydGljdWxhciBkZXBlbmRlbmN5LCB3ZSB3aWxsIHJlcHJlc2VudCB0aGlzIGJ5IGFkZGluZyBhIG5ldyB2YXJpYWJsZSB0byBvdXIgZGF0YSAod2Ugd2lsbCBhbHNvIHJlbW92ZSB1bnVzZWQgdmFyaWFibGVzKS4KCmBgYHtyfQpkYXRhTGF0ZW50IDwtIGRhdGFSYXcgJT4lIAogIG11dGF0ZShUZW1wID0gTkEpICU+JQogIHNlbGVjdChUZW1wLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBhcy5kYXRhLmZyYW1lKCkKCmRhdGFMYXRlbnQkVGVtcCA8LSBhcy5udW1lcmljKGRhdGFMYXRlbnQkVGVtcCkKYGBgCgpUaGUgbmV4dCBzdGVwIGlzIHRvIGVzdGltYXRlIHRoaXMgdmFyaWFibGUgdG8gY29tcGxldGUgb3VyIG1vZGVsLiBXZSBjYW4gbGVhcm4gdGhlIHRocmVlIG5ldHdvcmsgZGlzdHJpYnV0aW9ucyBmcm9tIHRoZSBkYXRhIGJ1dCBub3QgdGhlIFRlbXAgdmFyaWFibGUuIEZvciB0aGF0IHdlIHdvdWxkIHVzZSB0aGUgYEVNYCBhbGdvcml0aG0uIEZpcnN0LCBsZXRzIGJ1aWxkIHRoZSBtb2RlbDoKCmBgYHtyfQpoZWF0QWxhcm1EYWcxIDwtIG1vZGVsMm5ldHdvcmsoIltUZW1wXVtUUzF8VGVtcF1bVFMyfFRlbXBdW1RTM3xUZW1wXSIpCnBsb3REM2JuKGhlYXRBbGFybURhZzEpCmBgYAoKCk5vdyBmb3IgdGhlIEVNLCB1bmZvcnR1bmF0ZWxseSBgYm5sZWFybmAgZG9lcyBub3QgcHJvdmlkZSBhIHN0YW5kYWxvbmUgaW1wbGVtZW50YXRpb24gb2YgdGhpcyBhbGdvcml0aG0gKGl0IGRvZXMgZm9yIHN0cnVjdHVyYWwgbGVhcm5pbmcgRU0gd2hpY2ggaXMgYSBzdXBlcnNldCksIGJ1dCBpdCBwcm92aWRlcyB0aGUgdG9vbHMgdG8gdG8gc28uIEJhc2ljYWxseSwgdGhlIHBhcmFtZXRyaWMgRU0gYWxnb3JpdGhtIGNhbiBiZSBpbXBsZW1lbnRlZCBieToKCiogKipFIHN0ZXA6KiogSW1wdXRlIHRoZSBtaXNzaW5nIHZhcmlhYmxlcyBpbiB0aGUgZGF0YSBieSB1c2luZyBCYXllc2lhbiBlc3RpbWF0aW9uIGZyb20gdGhlIGFjdHVhbCBtb2RlbC4KKiAqKk0gc3RlcDoqKiBMZWFybmluZyBuZXcgcGFyYW1ldGVycyBmb3IgdGhlIG1vZGVsIGJ5IG1heGltaXppbmcgdGhlIGxpa2VsaWhvb2QgZnJvbSB0aGUgaW1wdXR0ZW4gZGF0YSAoTUxFKS4KCkFzIHlvdSBjYW4gaW1hZ2luZSB0aGVzZSBzdGVwcyB3b3VsZCBiZSByZXBldGVhZCB0aG91Z2ggYSBudW1iZXIgb2YgaXRlcmF0aW9ucyBvciBjb252ZXJnZW5jZS4gVG8gaW1wdXRlIHRoZSBsYXRlbnQgdmFyaWFibGVzIHdlIHdpbGwgdXNlIHRoZSBmdW5jdGlvbiBgaW5wdXRlYCwgKipob3dldmVyKiosIHdlIHdvdWxkIG5lZWQgYW4gaW5pdGlhbCBtb2RlbCB0byBkbyBzby4gVGhpcyBpcyBhbm90aGVyIGltcG9ydGFudCBlbGVtZW50IGZvciB0aGUgRU0gYWxnb3JpdGhtLCBhcyB3ZSB3b3VsZCBuZWVkIGEgZ29vZCBndWVzcyB0byBlbnN1cmUgYSBwcm9wZXIgZml0LgoKTGV0cyBmaXJzdCB0cnkgd2l0aCBhIHJhbmRvbSBpbml0aWFsaXphdGlvbiBvZiBUZW1wCgpgYGB7cn0KIyBMZXRzIGFkZCB1bmlmb3JtIFRlbXAgCmRhdGFJbXB1dGVkIDwtIGRhdGFMYXRlbnQgJT4lIHJvd3dpc2UoKSAlPiUgbXV0YXRlKFRlbXAgPSBydW5pZigxLCAxMCwgMjUpKQoKZGF0YUltcHV0ZWQgJT4lCiAgZ2dwbG90KGFlcyh4PVRlbXApKSArIGdlb21fZGVuc2l0eSgpCmBgYAoKVGhlIEVNIGFsZ29yaXRobSBpcyB0aGVuIHBsYWluIHNpbXBsZQoKYGBge3J9CnBhcmFtZXRyaWMuZW0gPC0gZnVuY3Rpb24oZGFnLCBkYXRhTGF0ZW50LCBkYXRhSW1wdXRlZCwgaXRlciA9IDUpIHsKICBmaXR0ZWQgPC0gYm4uZml0KGRhZywgZGF0YUltcHV0ZWQsIG1ldGhvZCA9ICJtbGUiKQogIGZvciAoIGkgaW4gc2VxKGl0ZXIpKSB7CiAgIGNvbXBsZXRlIDwtIGltcHV0ZShmaXR0ZWQsIGRhdGEgPSBkYXRhTGF0ZW50LCBtZXRob2QgPSAiYmF5ZXMtbHciKQogICBmaXR0ZWQgPC0gYm4uZml0KGRhZywgY29tcGxldGUsIG1ldGhvZCA9ICJtbGUiKQogIH0KICBmaXR0ZWQKfQpgYGAKCkxldCdzIHRyeSBpdAoKYGBge3J9CmhlYXRBbGFybU1vZGVsMSA8LSBwYXJhbWV0cmljLmVtKGhlYXRBbGFybURhZzEsIGRhdGFMYXRlbnQsIGRhdGFJbXB1dGVkLCBpdGVyID0gMTApCmBgYAoKTGV0cyBzZWUgdGhlIHJlc3VsdGluZyBtb2RlbDoKCmBgYHtyfQpoZWF0QWxhcm1Nb2RlbDEKCiMgU2FtcGxlIHNvbWUgZGF0YSBhbmQgcGxvdCBpdAppbXB1dGUoaGVhdEFsYXJtTW9kZWwxLCBkYXRhID0gZGF0YUxhdGVudCwgbWV0aG9kID0gImJheWVzLWx3IikgJT4lCmdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiKSAlPiUKZ2dwbG90KGFlcyh4PU1lYXN1cmUsIGNvbG9yPVNlbnNvcikpICsgZ2VvbV9kZW5zaXR5KCkKYGBgCgpMb29rIGF0IHRob3NlIGludGVyY2VwdCBjb2VmZmljaWVudCwgdGhleSBkbyBub3QgbG9vayByaWdodC4gQXMgd2UgY2FuIHNlZSBpbiB0aGUgcGxvdCBpdCBzZWVtcyBsaWtlIGEgZm9yY2VkIGZpdCwgd2hlcmUgdGhlIFRlbXAgdmFyaWFibGUgaGFzIGEgdmVyeSBkaWZmZXJlbnQgZGlzdHJpYnV0aW9uLiBMZXRzIHRyeSBhbm90aGVyIGd1ZXNzIGF0IHRoZSBwcmlvciBmb3IgVGVtcCwgdGhpcyB0aW1lIHdlIHdpbGwgdXNlIGEgR2F1c3NpYW4gZGlzdHJpYnV0aW9uIHdpdGggdGhlIHN0c3Rpc3RpY3MgZHJhd24gZnJvbSB0aGUgc2Vuc29ycy4KCgpgYGB7cn0KCnN0YXRpc3RpY3MgPC0gZGF0YUxhdGVudCAlPiUgCiAgZ2F0aGVyKCJTZW5zb3IiLCAiTWVhc3VyZSIsIFRTMSwgVFMyLCBUUzMpICU+JQogIHN1bW1hcmlzZSggCiAgICBtdSAgICA9IG1lYW4oTWVhc3VyZSksCiAgICBzaWdtYSA9IHNkKE1lYXN1cmUpCiAgKQoKZGF0YUltcHV0ZWQgPC0gZGF0YUxhdGVudCAlPiUgCiAgcm93d2lzZSgpICU+JSAKICBtdXRhdGUoVGVtcCA9IHJub3JtKDEsIHN0YXRpc3RpY3MkbXUsIHN0YXRpc3RpY3Mkc2lnbWEpKQoKZGF0YUltcHV0ZWQgJT4lCiAgZ2dwbG90KGFlcyh4PVRlbXApKSArIGdlb21fZGVuc2l0eSgpCmBgYAoKTGV0J3MgcmVwZWF0IHRoZSBFTToKCmBgYHtyfQpoZWF0QWxhcm1Nb2RlbDEgPC0gcGFyYW1ldHJpYy5lbShoZWF0QWxhcm1EYWcxLCBkYXRhTGF0ZW50LCBkYXRhSW1wdXRlZCwgaXRlciA9IDEwKQpgYGAKCmBgYHtyfQpoZWF0QWxhcm1Nb2RlbDEKCiMgU2FtcGxlIHNvbWUgZGF0YSBhbmQgcGxvdCBpdAppbXB1dGUoaGVhdEFsYXJtTW9kZWwxLCBkYXRhID0gZGF0YUxhdGVudCwgbWV0aG9kID0gImJheWVzLWx3IikgJT4lCmdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiKSAlPiUKZ2dwbG90KGFlcyh4PU1lYXN1cmUsIGNvbG9yPVNlbnNvcikpICsgZ2VvbV9kZW5zaXR5KCkKYGBgCgpUaGlzIHRpbWUgdGhlIGZpdCBpcyBldmVuIHdvcnNlLCBhcyB3ZSBvdmVyZml0dGVkIGEgYmFkIHByaW9yLiBXZSB3aWxsIHRyeSB0byBtYWtlIGEgYmV0dGVyIGd1ZXNzLCB0aGlzIHRpbWUgZGVwZWRhbnQgb24gdGhlIGFjdHVhbCB2YWx1ZSBvZiB0aGUgc2Vuc29ycy4KCgoKYGBge3J9CmRhdGFJbXB1dGVkIDwtIGRhdGFMYXRlbnQgJT4lIAogIHJvd3dpc2UoKSAlPiUgCiAgbXV0YXRlKFRlbXAgPSBtZWFuKGMoVFMxLCBUUzIsIFRTMykpKQoKZGF0YUltcHV0ZWQgJT4lCiAgZ2dwbG90KGFlcyh4PVRlbXApKSArIGdlb21fZGVuc2l0eSgpCmBgYAoKYGBge3J9CmhlYXRBbGFybU1vZGVsMSA8LSBwYXJhbWV0cmljLmVtKGhlYXRBbGFybURhZzEsIGRhdGFMYXRlbnQsIGRhdGFJbXB1dGVkLCBpdGVyID0gMTApCmBgYAoKYGBge3J9CmhlYXRBbGFybU1vZGVsMQoKIyBTYW1wbGUgc29tZSBkYXRhIGFuZCBwbG90IGl0CmltcHV0ZShoZWF0QWxhcm1Nb2RlbDEsIGRhdGEgPSBkYXRhTGF0ZW50LCBtZXRob2QgPSAiYmF5ZXMtbHciKSAlPiUKZ2F0aGVyKCJTZW5zb3IiLCAiTWVhc3VyZSIpICU+JQpnZ3Bsb3QoYWVzKHg9TWVhc3VyZSwgY29sb3I9U2Vuc29yKSkgKyBnZW9tX2RlbnNpdHkoKQpgYGAKCk5vdyBpdCBzZWVtcyB0aGF0IHdlIGdvdCBhIG5pY2UgZml0LCB0aGUgZGVuc2l0eSBncmFwaCByZXByZXNlbnRzIHRoZSBzZW5zb3JzIGFzIHNjYWxhciB0cmFuc2Zvcm1hdGlvbnMgb2YgdGhlIGxhdGVudCBUZW1wIHZhcmlhYmxlLgoKCiMjIE1vZGVsaW5nIHRoZSBkb21haW4KCklmIHdlIGdvIGJhY2sgdG8gdGhlIHBsb3Qgc2hvd2luZyB0aGUgZXZvbHV0aW9uIG9mIHRoZSB0ZW1wZXJhdHVyZSBhbG9uZyB0aGUgeWVhciB3ZSBjYW4gc2VlIHRoYXQgdGhlcmUgY291bGQgYmUgZGlmZmVyZW50IGRpc3RyaWJ1dGlvbnMgZm9yIGRpZmZlcmVudCBwZXJpb2RzIG9mIHRpbWUuIElmIHdlIGNhbiBvYnRhaW4gdGhpcyBkaXNjcmltaW5hdGl2ZSBpbmZvcm1hdGlvbiB3ZSBjb3VsZCBkbyBiZXR0ZXIgbW9kZWxpbmcgdGhlIGRvbWFpbi4KCldlIHdpbGwgc3RhcnQgYnkgZGlzY3JpbWluYXRpbmcgdGhlIGRpc3RyaWJ1dGlvbiBieSBtb250aDoKCmBgYHtyfQpkYXRhQnlNb250aCA8LSBkYXRhUmF3ICU+JQogIG11dGF0ZShNb250aCA9IGFzLmZhY3Rvcihtb250aChEYXRlLCBsYWJlbCA9IFRSVUUsIGFiYnIgPSBUUlVFKSkpCgpkYXRhQnlNb250aCAlPiUKICBnYXRoZXIoIlNlbnNvciIsICJNZWFzdXJlIiwgVFMxLCBUUzIsIFRTMykgJT4lCiAgZ2dwbG90KGFlcyh4PU1lYXN1cmUsIGNvbG9yPVNlbnNvciwgZmlsbD1TZW5zb3IpKSArIAogICAgZ2VvbV9kZW5zaXR5KGFscGhhPTAuNSkgKwogICAgZmFjZXRfd3JhcCh+TW9udGgpCmBgYAoKSXQgcmVhbGx5IHNlZW1zIHRoYXQgdGhlIG1vbnRoIGNhbiBjYXB0dXJlIGFkZGl0aW9uYWwgaW5mb3JtYXRpb24gYWJvdXQgdGhlIGRpc3RyaWJ1dGlvbi4gSW4gdGhpcyBjYXNlLCB3ZSB3b3VsZCBvYnRhaW4gYSBiZXR0ZXIgZml0IHRvIHRoZSBkYXRhIGlmIHdlIGNvbmRpdGlvbiB0aGUgTW9udGggYXMgYSBkaXNjcmV0ZSBwYXJlbnQgZm9yIHRoZSBUZW1wIHZhcmlhYmxlLiBJbiB0aGlzIGNhc2Ugd2Ugd291bGQgbmVlZCBwYXJhbWV0ZXJzIGZvciAxMiBkaWZmZXJlbnQgZ2F1c3NpYW4gZGlzdHJpYnV0aW9ucywgd2hpY2ggbWF5IGJlIGEgYml0IG92ZXJraWxsLgoKTGV0J3MgdHJ5IGlmIHdlIGNvdWxkIHN1bW1hcmlzZSB0aGUgbW9udGggaW5mb3JtYXRpb24gYnkgY3JlYXRpbmcgYSBsb3dlciBkaW1lbnNpb25hbCB2YXJpYWJsZS4gSWYgd2UgdGhpbmsgYWJvdXQgdGhlIHdlYXRoZXIgZG9tYWluIHdlIHJhcGlkbHkgY29tZSB1cCB3aXRoIHRoZSB5ZWFyJ3Mgc2Vhc29ucy4gTGV0J3MgdHJ5IHRvIGRpc2NyaW1pbmF0ZSB0aGUgZGlzdHJpYnV0aW9ucyBieSBzZWFzb24uCgpgYGB7cn0KIyBMdWJyaWRhdGUgY2FudCBleHRyYWN0IHNlYXNvbnMgc28gd2UgbmVlZCBhIGN1c3RvbSBmdW5jdGlvbgoKIyBXZSBiZXR0ZXIgZW5jb2RlIHRoZSBzdGF0ZXMgYXMgYSBkaWN0IHRvIGF2b2lkIG1hZ2ljIHN0cmluZ3MKU2Vhc29uU3RhdGVzIDwtIGxpc3QoCiAgV2ludGVyID0gIndpbnRlciIsCiAgU3ByaW5nID0gInNwcmluZyIsCiAgU3VtbWVyID0gInN1bW1lciIsCiAgRmFsbCA9ICJmYWxsIgopCgpzZWFzb24gPC0gZnVuY3Rpb24oZGF0ZSl7CiAgV1MgPC0gYXMuRGF0ZSgiMjAxMi0xMi0xNSIsIGZvcm1hdCA9ICIlWS0lbS0lZCIpICMgV2ludGVyIFNvbHN0aWNlCiAgU0UgPC0gYXMuRGF0ZSgiMjAxMi0zLTE1IiwgIGZvcm1hdCA9ICIlWS0lbS0lZCIpICMgU3ByaW5nIEVxdWlub3gKICBTUyA8LSBhcy5EYXRlKCIyMDEyLTYtMTUiLCAgZm9ybWF0ID0gIiVZLSVtLSVkIikgIyBTdW1tZXIgU29sc3RpY2UKICBGRSA8LSBhcy5EYXRlKCIyMDEyLTktMTUiLCAgZm9ybWF0ID0gIiVZLSVtLSVkIikgIyBGYWxsIEVxdWlub3gKICAKICAjIENvbnZlcnQgZGF0ZXMgZnJvbSBhbnkgeWVhciB0byAyMDEyIGRhdGVzCiAgZCA8LSBhcy5EYXRlKHN0cmZ0aW1lKGRhdGUsIGZvcm1hdD0iMjAxMi0lbS0lZCIpKQogIAogIGZhY3RvcigKICAgIGlmZWxzZSAoZCA+PSBXUyB8IGQgPCBTRSwgU2Vhc29uU3RhdGVzJFdpbnRlciwKICAgICAgaWZlbHNlIChkID49IFNFICYgZCA8IFNTLCBTZWFzb25TdGF0ZXMkU3ByaW5nLAogICAgICAgIGlmZWxzZSAoZCA+PSBTUyAmIGQgPCBGRSwgU2Vhc29uU3RhdGVzJFN1bW1lciwgU2Vhc29uU3RhdGVzJEZhbGwpKSksCiAgICBsZXZlbHMgPSBTZWFzb25TdGF0ZXMKICApCn0KCmRhdGFCeVNlYXNvbiA8LSBkYXRhQnlNb250aCAlPiUKICBtdXRhdGUoU2Vhc29uID0gc2Vhc29uKERhdGUpKQoKZGF0YUJ5U2Vhc29uICU+JQogIGdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9TWVhc3VyZSwgY29sb3I9U2Vuc29yLCBmaWxsPVNlbnNvcikpICsgCiAgICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41KSArCiAgICBmYWNldF93cmFwKH5TZWFzb24pCmBgYAoKVGhlIGRpc3RyaWJ1dGlvbnMgZm9yIHRoZSBzZWFzb24gZGlzY3JpbWluYXRpb24gc2VlbXMgbGVzcyBHYXVzc2lhbiwgYnV0IHRoZXkgZW5jb2RlIHRoZSBzYW1lIHBhdHRlcm4uIExldHMgdHJ5IHRvIG1vZGVsIGJvdGggbmV0d29ya3MgYW5kIHNlZSBob3cgd2VsbCB0aGV5IGZpdCB0aGUgcGFyYW1ldGVycy4KCkZpcnN0IHdlIHdpbGwgbW9kZWwgdGhlIE1vbnRoIGJhc2VkIG9uZSA6CgpgYGB7cn0KZGF0YUxhdGVudE1vbnRoIDwtIGRhdGFCeU1vbnRoICU+JQogIG11dGF0ZShUZW1wID0gTkEpICU+JQogIHNlbGVjdChNb250aCwgVGVtcCwgVFMxLCBUUzIsIFRTMykgJT4lCiAgYXMuZGF0YS5mcmFtZSgpCgpkYXRhTGF0ZW50TW9udGgkVGVtcCA8LSBhcy5udW1lcmljKGRhdGFMYXRlbnRNb250aCRUZW1wKQpgYGAKCkFuZCBub3cgd2UgbW9kZWwgdGhlIG5ldHdvcmsgREFHCgpgYGB7cn0KaGVhdEFsYXJtRGFnTW9udGggPC0gbW9kZWwybmV0d29yaygiW01vbnRoXVtUZW1wfE1vbnRoXVtUUzF8VGVtcF1bVFMyfFRlbXBdW1RTM3xUZW1wXSIpCnBsb3REM2JuKGhlYXRBbGFybURhZ01vbnRoKQpgYGAKCkxldHMgcmVwZWF0IHRoZSBFTSBleGFtcGxlIGFuZCBpbmR1Y2UgdGhlIHBhcmFtZXRlcnMgb2YgdGhlIGhpZGRlbiB2YXJpYWJsZSB3aXRoIHRoZSBwcmV2aW91cyBzdWNjZXNzZnVsIGNvbmZpZ3VyYXRpb24uCgpgYGB7cn0KZGF0YUltcHV0ZWRNb250aCA8LSBkYXRhTGF0ZW50TW9udGggJT4lIAogIHJvd3dpc2UoKSAlPiUgCiAgbXV0YXRlKFRlbXAgPSBtZWFuKGMoVFMxLCBUUzIsIFRTMykpKSAlPiUKICBhcy5kYXRhLmZyYW1lKCkKYGBgCgpgYGB7cn0KaGVhdEFsYXJtTW9kZWxNb250aCA8LSBwYXJhbWV0cmljLmVtKGhlYXRBbGFybURhZ01vbnRoLCBkYXRhTGF0ZW50TW9udGgsIGRhdGFJbXB1dGVkTW9udGgsIGl0ZXIgPSAxMCkKYGBgCgpgYGB7cn0KZGF0YUltcHV0ZU1vbnRoUG9zdGVyaW9yIDwtIGltcHV0ZShoZWF0QWxhcm1Nb2RlbE1vbnRoLCBkYXRhID0gZGF0YUxhdGVudE1vbnRoLCBtZXRob2QgPSAiYmF5ZXMtbHciKQoKZGF0YUltcHV0ZU1vbnRoUG9zdGVyaW9yICU+JQogIGdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiLCBUZW1wLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9TWVhc3VyZSwgY29sb3I9U2Vuc29yLCBmaWxsPVNlbnNvcikpICsgCiAgICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41KSArCiAgICBmYWNldF93cmFwKH5Nb250aCkKYGBgCgpUaGlzIGlzIGEgdmVyeSBnb29kIGZpdCBpbmRlZWQuIFdlIGNhbiBzZWUgdGhhdCBUZW1wIGlzIGFsbW9zdCBpZGVudGljYWwgdG8gVFMzLCBhbmQgdGhhdCB0aGUgb3RoZXIgdHdvIHNlbnNvcnMgYXJlIGp1c3QgdHJhbnNmb3JtYXRpb25zIG9mIHRoaXMgdmFyaWFibGUuIExldHMgY2hlY2sgaXQgdGhlIHNlYXNvbiBpcyBhIGdvb2Qgc2ltcGxpZmljYXRpb24gb2YgdGhlIE1vbnRoIHZhcmlhYmxlLgoKYGBge3J9CmRhdGFMYXRlbnRTZWFzb24gPC0gZGF0YUJ5U2Vhc29uICU+JQogIG11dGF0ZShUZW1wID0gTkEpICU+JQogIHNlbGVjdChTZWFzb24sIFRlbXAsIFRTMSwgVFMyLCBUUzMpICU+JQogIGFzLmRhdGEuZnJhbWUoKQoKZGF0YUxhdGVudFNlYXNvbiRUZW1wIDwtIGFzLm51bWVyaWMoZGF0YUxhdGVudFNlYXNvbiRUZW1wKQoKaGVhdEFsYXJtRGFnU2Vhc29uIDwtIG1vZGVsMm5ldHdvcmsoIltTZWFzb25dW1RlbXB8U2Vhc29uXVtUUzF8VGVtcF1bVFMyfFRlbXBdW1RTM3xUZW1wXSIpCgpkYXRhSW1wdXRlZFNlYXNvbiA8LSBkYXRhTGF0ZW50U2Vhc29uICU+JSAKICByb3d3aXNlKCkgJT4lIAogIG11dGF0ZShUZW1wID0gbWVhbihjKFRTMSwgVFMyLCBUUzMpKSkgJT4lCiAgYXMuZGF0YS5mcmFtZSgpCmBgYAoKV2l0aCB0aGF0IGNvbmZpZ3VyYXRpb24gd2UgcnVuIHRoZSBFTQoKYGBge3J9CmhlYXRBbGFybU1vZGVsU2Vhc29uIDwtIHBhcmFtZXRyaWMuZW0oaGVhdEFsYXJtRGFnU2Vhc29uLCBkYXRhTGF0ZW50U2Vhc29uLCBkYXRhSW1wdXRlZFNlYXNvbiwgaXRlciA9IDEwKQpgYGAKCmBgYHtyfQpkYXRhSW1wdXRlU2Vhc29uUG9zdGVyaW9yIDwtIGltcHV0ZShoZWF0QWxhcm1Nb2RlbFNlYXNvbiwgZGF0YSA9IGRhdGFMYXRlbnRTZWFzb24sIG1ldGhvZCA9ICJiYXllcy1sdyIpCgpkYXRhSW1wdXRlU2Vhc29uUG9zdGVyaW9yICU+JQogIGdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiLCBUZW1wLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9TWVhc3VyZSwgY29sb3I9U2Vuc29yLCBmaWxsPVNlbnNvcikpICsgCiAgICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41KSArCiAgICBmYWNldF93cmFwKH5TZWFzb24pCmBgYAoKQW5vdGhlciBnb29kIGZpdC4gV2hpY2ggb25lIG9mIHRoZSB0d28gYXBwcm9hY2hlcyBpcyBiZXR0ZXI/LiBUaGUgaW5pdGlhbCBndWVzcyBzaG91bGQgYmUgdGhlIFNlYXNvbiBvbmUsIGFzIGl0IGhhcyBmZXdlciBwYXJhbWV0ZXJzIHRoYW4gdXNpbmcgdGhlIE1vbnRoLiBMZXQncyBhc2sgdGhlIEJJQyBzY29yZS4KCmBgYHtyfQpwcmludChCSUMoaGVhdEFsYXJtTW9kZWxTZWFzb24sIGRhdGFJbXB1dGVTZWFzb25Qb3N0ZXJpb3IpKQpwcmludChCSUMoaGVhdEFsYXJtTW9kZWxNb250aCwgZGF0YUltcHV0ZU1vbnRoUG9zdGVyaW9yKSkKYGBgCkFzIGV4cGVjdGVkLCBzZWFzb24gaXMgYnkgZmFyIG1vcmUgc3VwZXJpb3IgYXQgbWluaW1pemluZyBCSUMgdGhhbiBtb250aCBhbmQgc2hvdWxkIGJlIG91ciBwaWNrLgoKCiMjIE1vZGVsaW5nIGFub21hbGllcwoKT25lIG9mIHRoZSBtb3N0IGFwcGVhbGluZyBhc3BlY3RzIG9mIEJheWVzaWFuIE5ldHdvcmtzIGlzIHRoZSBhYmlsaXR5IHRvIGluY2x1ZGUgZXhwZXJ0IGtub3dsZWRnZSBpbiB0aGUgbW9kZWwuIFdoaWxlIHRoaXMgaXMgYWxtb3N0IGltcG9zc2libGUgaW4gb3RoZXIgZnJhbWV3b3JrcyBpdCBpcyBhIG5hdHVyYWwgY29uY2VwdHMgaW4gdGhpcyBjYXNlLgoKV2UgY2FsbCBCTnMgb3BlbiBib3ggbW9kZWxzIGJlY2F1c2Ugd2UgY2FuIHVuZGVyc3RhbmQgYW5kIGludGVycHJldGF0ZSB0aGUgcGFyYW1ldGVycy4gVGhpcyBpcyByZWFsbHkgdXNlZnVsIGZvciBtb2RlbGxpbmcgYW5vbWFsaWVzLCBiZWNhdXNlIHdlIGNhbiB1c2UgbGF0ZW50IHZhcmlhYmxlcyBhbmQgZXhwZXJ0IGtub3dsZWRnZSB0byByZXByZXNlbnQgZXZlbnRzIHRoYXQgY2Fubm90IGJlIG9ic2VydmVkLgoKSWYgdGhlIGV2ZW50cyBjYW5ub3QgYmUgb2JzZXJ2ZWQgd2Ugd291bGQgbm90IGhhdmUgYXZhaWxhYmxlIGRhdGEgdG8gbGVhcm4gZnJvbS4gSW4gb3VyIGV4YW1wbGUgd2Ugd291bGQgaGF2ZSB0byBkZXN0cm95IG1hY2hpbmVzIGFuZCBsaXQgdGhlIHJvb20gb24gZmlyZSB0byBjYXB0dXJlIGRhdGEgYWJvdXQgYW5vbWFsaWVzLCBhbmQgdGhhdCB3b3VsZCBzdXJlbHkgaGF2ZSBhIGhpZ2ggY29zdC4gSW1hZ2luZSBvdGhlciBzY2VuYXJpb3Mgc3VjaCBhcyByYXJlIGRpc2Vhc2VzLCBhdXRvbm9tb3VzIGRyaXZpbmcgb3IgZmluYW5jaWFsIGZyYXVkLgoKV2Ugd2lsbCBzdGFydCBieSBtb2RlbGluZyBhIGdsb2JhbCBmYXVsdCBieSBhZGRpbmcgYSBuZXcgdmFyaWFibGUgYEFsYXJtYCwgdGhpcyB2YXJpYWJsZSB3aWxsIGhhdmUgdHdvIHN0YXRlcyBgeWVzYCBhbmQgYG5vYC4gSW4gdGhpcyBleGFtcGxlIHdlIGhhdmUgYmVlbiBjb2xsZWN0aW5nIGRhdGEgZnJvbSB0aGUgc2Vuc29ycyBmb3IgYSB5ZWFyLCB3ZSBhc3N1bWUgdGhhdCB0aGUgZnVuY3Rpb25pbmcgaGFzIGJlZW4gY29ycmVjdCBhbmQgdGhhdCB0aGVyZSBoYXMgbm90IGJlZW4gcmVjb3JkZXIgZmFpbHVyZXMgKHBlcmhhcHMgYmVjYXVzZSB0aGUgZXhwZXJ0IHRlY2huaWNpYW5zIHRoYXQgbWFpbnRhaW4gdGhlIHJvb20gaGF2ZSBhc3N1cmVkIHVzIHNvKS4KCkdpdmVuIHRoaXMsIHRoZSBBbGFybSB2YXJpYWJsZSB3aWxsIGJlIGFsd2F5cyBvYnNlcnZlZCB0byBgbm9gLgoKYGBge3J9CiMgTmV3IFZhcmlhYmxlcwpBbGFybVN0YXRlcyA8LSBsaXN0KAogIE5vICA9ICJubyIsCiAgWWVzID0gInllcyIKKQoKZGF0YUxhdGVudCA8LSBkYXRhTGF0ZW50U2Vhc29uICU+JQogIG11dGF0ZShBbGFybSA9IGZhY3RvcihBbGFybVN0YXRlcyRObywgbGV2ZWxzID0gQWxhcm1TdGF0ZXMpKSAlPiUKICBzZWxlY3QoQWxhcm0sIFNlYXNvbiwgVGVtcCwgVFMxLCBUUzIsIFRTMykgJT4lCiAgYXMuZGF0YS5mcmFtZSgpCmBgYAoKQW5kIG5vdyB0aGUgbmV3IGRhZzoKCmBgYHtyfQpoZWF0QWxhcm1EYWcgPC0gbW9kZWwybmV0d29yaygiW0FsYXJtXVtTZWFzb25dW1RlbXB8U2Vhc29uOkFsYXJtXVtUUzF8VGVtcF1bVFMyfFRlbXBdW1RTM3xUZW1wXSIpCnBsb3REM2JuKGhlYXRBbGFybURhZykKYGBgCgpMZXRzIHJ1biB0aGUgRU0gYWxnb3JpdGhtIG9uZSBtb3JlIHRpbWUsIHRoZXIgc2hvdWxkIGJlIG5vIGNoYW5nZXMgdG8gdGhlIHBhcmFtZXRlcnMgYnV0IGl0IHdpbGwgcmVpbmZvcmNlIG91ciBsb2dpY2FsIHBpcGVsaW5lLgoKYGBge3J9CmRhdGFJbXB1dGVkIDwtIGRhdGFMYXRlbnQgJT4lIAogIHJvd3dpc2UoKSAlPiUgCiAgbXV0YXRlKFRlbXAgPSBtZWFuKGMoVFMxLCBUUzIsIFRTMykpKSAlPiUKICBhcy5kYXRhLmZyYW1lKCkKYGBgCgpgYGB7cn0KaGVhdEFsYXJtTW9kZWwgPC0gcGFyYW1ldHJpYy5lbShoZWF0QWxhcm1EYWcsIGRhdGFMYXRlbnQsIGRhdGFJbXB1dGVkLCBpdGVyID0gMTApCmBgYAoKTGV0cyBjaGVjayB0aGUgcmVzdWx0IG9uIHRoZSBBbGFybSB2YXJpYWJsZToKCmBgYHtyfQpoZWF0QWxhcm1Nb2RlbCRBbGFybQpgYGAKCk9idmlvdXNseSB0aGUgYWxnb3JpdGhtIHdpbGwgZml0IGFsbCB0aGUgY2FzZXMgdG8gdGhlIG9ic2VydmVkIHBvcHVsYXRpb24gaW4gd2hpY2ggbm8gYWxhcm1zIGhhdmUgb2NjdXJyZWQuIEhlcmUgaXMgd2hlcmUgdGhlICoqZXhwZXJ0IGtub3dsZWRnZSoqIGVudGVycy4gV2Ugd2lsbCBjb250YWN0IG91ciBleHBlcnRzIGFuZCBkZXNpZ24gYSBwcm9wZXIgcGFyYW1ldHJpemF0aW9uIG9mIHRoZSB2YXJpYWJsZS4KCj4gIlRoZSBzeXN0ZW0gaGFzIGEgcmFuZG9tIGZhaWx1cmUgcmF0ZSBvZiAxLzEwMDAsIGluIHN1Y2ggY2FzZXMsIHRoZSByb29tIHRlbXBlcmF0dXJlIGlzIHF1aWNrbHkgcmFpc2VkIGFib3V0IDEwIi4KCldlIGhhdmUgZW5vdWdoIGluZm9ybWF0aW9uIHRvIGluY2x1ZGUgdGhpcyBleHBlcnQga25vd2xlZGdlIGludG8gZGUgQWxhcm0gdmFyaWFibGUuIE5vdGljZSB0aGF0IHRoaXMgdmFyaWFibGUgaW50cm9kdWNlcyBhZGRpdGlvbmFsIHBhcmFtZXRlcnMgaW50byB0aGUgVGVtcCB2YXJpYWJsZSB0aGF0IHNob3VsZCBiZSBtb2RlbGxlZCB0by4KCmBgYHtyfQpjcHRBbGFybSA8LSBjb2VmKGhlYXRBbGFybU1vZGVsJEFsYXJtKQpwcmludChjcHRBbGFybSkKYGBgCgpgYGB7cn0KY3B0QWxhcm1bXSA8LSBjKDAuOTk5LCAwLjAwMSkKaGVhdEFsYXJtTW9kZWwkQWxhcm0gPC0gY3B0QWxhcm0KcHJpbnQoaGVhdEFsYXJtTW9kZWwkQWxhcm0pCmBgYAoKTm93IGZvciB0aGUgVGVtcCB2YXJpYWJsZSwgd2hpY2ggYSBsaXR0bGUgbW9yZSBjb21wbGV4CgpgYGB7cn0KY2dhdXNzVGVtcCA8LSBjb2VmKGhlYXRBbGFybU1vZGVsJFRlbXApCnNkVGVtcCAgICAgPC0gaGVhdEFsYXJtTW9kZWwkVGVtcCRzZApwcmludChjZ2F1c3NUZW1wKQpwcmludChzZFRlbXApCmBgYAoKYGBge3J9CmNnYXVzc1RlbXBbaXMubmFuKGNnYXVzc1RlbXApXSA8LSBjZ2F1c3NUZW1wWyFpcy5uYW4oY2dhdXNzVGVtcCldICsgMTAKc2RUZW1wW2lzLm5hbihzZFRlbXApXSA8LSBzZFRlbXBbIWlzLm5hbihzZFRlbXApXQoKaGVhdEFsYXJtTW9kZWwkVGVtcCA8LSBsaXN0KCBjb2VmID0gY2dhdXNzVGVtcCwgc2QgPSBzZFRlbXApCmhlYXRBbGFybU1vZGVsJFRlbXAKYGBgCgpXZSBoYXZlIGEgY29tcGxldGUgbW9kZWwgbm93LCB3aXRoIGEgbW9kZWxsZWQgYW5vbWFseSBkZXRlY3Rpb24gc2NoZW1hLiBMZXRzIHRlc3Qgc29tZSBxdWVyaWVzLgoKCmBgYHtyfQplIDwtIGxpc3QoICJTZWFzb24iID0gU2Vhc29uU3RhdGVzJFdpbnRlciwgIlRTMSIgPSAyMywgIlRTMiIgPSAzMywgIlRTMyIgPSAyOSApCnF1ZXJ5IDwtIGNwcXVlcnkoaGVhdEFsYXJtTW9kZWwsIGV2ZW50ID0gQWxhcm0gPT0gInllcyIsIGV2aWRlbmNlID0gZSwgbWV0aG9kID0gImx3IiwgbiA9IDEwMDAwMCkKcHJpbnQobWVhbihyZXAocXVlcnksIDEwMDAwKSkpCmBgYAoKVGhpcyB0ZW1wZXJhdHVyZSBpcyB0b28gaGlnaCBmb3Igd2ludGVyLCBzbyB0aGUgYWxhcm0gaXMgbGlrZWx5IHRvIGdvIHVwLgoKYGBge3J9CmUgPC0gbGlzdCggIlNlYXNvbiIgPSBTZWFzb25TdGF0ZXMkU3VtbWVyLCAiVFMxIiA9IDIzLCAiVFMyIiA9IDMzLCAiVFMzIiA9IDI5ICkKcXVlcnkgPC0gY3BxdWVyeShoZWF0QWxhcm1Nb2RlbCwgZXZlbnQgPSBBbGFybSA9PSAieWVzIiwgZXZpZGVuY2UgPSBlLCBtZXRob2QgPSAibHciLCBuID0gMTAwMDAwKQpwcmludChtZWFuKHJlcChxdWVyeSwgMTAwMDApKSkKYGBgCgpIb3dldmVyIHRoaXMgaXMgYSBnb29kIGZpdCBmb3IgdGhlIGhpZ2hlciB0ZW1wZXJhdHVyZXMgdGhhdCBhcmUgcmVnaXN0ZXJlZCBpbiBzdW1tZXIuCgoKIyMgTW9yZSBhbm9tYWxpZXMKCldlIGNvdWxkIG1vZGVsIGFkZGl0aW9uYWwgYW5vbWFsaWVzIGZvciB0aGUgaW5kaXZpZHVhbCBtYWxmdW5jdGlvbmluZyBvZiBlYWNoIHNlbnNvci4gSW4gdGhhdCBjYXNlIHdlIGNvdWxkIG9ic2VydmUgZWl0aGVyIGdsb2JhbCB0ZW1wZXJhdHVyZSBhbm9tYWxpZXMgb3IgaW5kaXZpZHVhbCBtYWxmdW5jdGlvbnMuCgpBIGNvbW1vbiBwYXR0ZXJuIGlzIHRvIGFkZCBhIG5ldyBoaWRkZW4gZGlzY3JldGUgdmFyaWFibGUgY29uZGl0aW9uaW5nIGVhY2ggc2Vuc29yIHdpdGggYSByYW5kb20gZGlzdHJpYnV0aW9uIGluIHRoZSBjYXNlIG9mIG1hbGZ1bmN0aW9uLgoKYGBge3J9CiMgTmV3IFZhcmlhYmxlcwpUUzFGYXVsdFN0YXRlcyA8LSBsaXN0KAogIE5vICA9ICJubyIsCiAgWWVzID0gInllcyIKKQoKZGF0YUxhdGVudCA8LSBkYXRhTGF0ZW50U2Vhc29uICU+JQogIG11dGF0ZSgKICAgIEFsYXJtID0gZmFjdG9yKEFsYXJtU3RhdGVzJE5vLCBsZXZlbHMgPSBBbGFybVN0YXRlcyksCiAgICBUUzFGYXVsdCA9IGZhY3RvcihUUzFGYXVsdFN0YXRlcyRObywgbGV2ZWxzID0gVFMxRmF1bHRTdGF0ZXMpCiAgKSAlPiUKICBzZWxlY3QoQWxhcm0sIFNlYXNvbiwgVGVtcCwgVFMxRmF1bHQsIFRTMSwgVFMyLCBUUzMpICU+JQogIGFzLmRhdGEuZnJhbWUoKQoKZGF0YUltcHV0ZWQgPC0gZGF0YUxhdGVudCAlPiUgCiAgcm93d2lzZSgpICU+JSAKICBtdXRhdGUoVGVtcCA9IG1lYW4oYyhUUzEsIFRTMiwgVFMzKSkpICU+JQogIGFzLmRhdGEuZnJhbWUoKQoKaGVhdEFsYXJtRGFnIDwtIG1vZGVsMm5ldHdvcmsoIltBbGFybV1bU2Vhc29uXVtUUzFGYXVsdF1bVGVtcHxTZWFzb246QWxhcm1dW1RTMXxUZW1wOlRTMUZhdWx0XVtUUzJ8VGVtcF1bVFMzfFRlbXBdIikKcGxvdEQzYm4oaGVhdEFsYXJtRGFnKQpgYGAKCldlIGxhdW5jaCBFTSBhbmQgbW9kaWZ5IHRoZSBuZXcgcHJvYmFiaWxpdHkgb2YgdGhlIG5vZGVzIChub3cgQWxhcm0gYW5kIFRTMSBGYXVsdCkuIEZvciB0aGF0IHdlIG5lZWQgYWRkaXRpb25hbCBleHBydCBrbm93bGVkZ2UgZnJvbSB0aGUgZG9tYWluLiBXZSBhc2sgb3VyIElvVCB0ZWFtIGFib3V0IHRoZSBmYXVsdCB0b2xlcmFuY2Ugb2Ygb3VyIHNlbnNvcnMuIFRoZXkgdGVsbCB1cyB0aGUgZm9sbG93aW5nPgoKPiBPdXIgc2Vuc29ycyBhcmUgdGhlIGJlc3QsIHRoZXkgdXN1YWxseSBkbyBub3QgZmFpbC4gVGhlIHRlc3RpbmcgZ3V5cyBlc3RpbWF0ZSBqdXN0IGEgMS8xMDAwIGZhaWx1cmVzLgoKVGhpcyBpcyBnb29kLCB3aXRoIHRoaXMgbWFyZ2luYWwgZXJyb3IgcHJvYmJpbGl0eSBvdXQgbmV3IG5vZGUgd2lsbCBvbmx5IGRldGVjdCBhbm9tYWxpZXMgd2l0aG91dCBpbnRyb2R1Y2luZyBub2lzZSBpbiB0aGUgcmVtYWluaW5nIHByb3BhZ2F0aW9uIHdpdGhpbiB0aGUgbW9kZWwuCgpgYGB7cn0KaGVhdEFsYXJtTW9kZWwgPC0gcGFyYW1ldHJpYy5lbShoZWF0QWxhcm1EYWcsIGRhdGFMYXRlbnQsIGRhdGFJbXB1dGVkLCBpdGVyID0gMTApCmBgYAoKYGBge3J9CiMgVE9ETzogUGxlYXNlIGRvbnQgZG8gdGhpcywgdGhpcyBpcyBub3QgRFJZLi4uCmNwdEFsYXJtW10gPC0gYygwLjk5OSwgMC4wMDEpCmhlYXRBbGFybU1vZGVsJEFsYXJtIDwtIGNwdEFsYXJtCmNnYXVzc1RlbXAgPC0gY29lZihoZWF0QWxhcm1Nb2RlbCRUZW1wKQpzZFRlbXAgICAgIDwtIGhlYXRBbGFybU1vZGVsJFRlbXAkc2QKY2dhdXNzVGVtcFtpcy5uYW4oY2dhdXNzVGVtcCldIDwtIGNnYXVzc1RlbXBbIWlzLm5hbihjZ2F1c3NUZW1wKV0gKyAxMApzZFRlbXBbaXMubmFuKHNkVGVtcCldIDwtIHNkVGVtcFshaXMubmFuKHNkVGVtcCldCmhlYXRBbGFybU1vZGVsJFRlbXAgPC0gbGlzdCggY29lZiA9IGNnYXVzc1RlbXAsIHNkID0gc2RUZW1wKQpgYGAKCgpgYGB7cn0KY3B0VFMxRmF1bHQgPC0gY29lZihoZWF0QWxhcm1Nb2RlbCRUUzFGYXVsdCkKcHJpbnQoY3B0VFMxRmF1bHQpCmBgYAoKYGBge3J9CmNwdFRTMUZhdWx0W10gPC0gYygwLjk5OTksIDAuMDAwMSkKaGVhdEFsYXJtTW9kZWwkVFMxRmF1bHQgPC0gY3B0VFMxRmF1bHQKcHJpbnQoaGVhdEFsYXJtTW9kZWwkVFMxRmF1bHQpCmBgYAoKQW5kIG5vdyBmb3IgdGhlIFRTMSBub2RlLCBpbiB3aGljaCB3ZSBoYXZlIHRvIGFkZCBhIHJhbmRvbSBkaXN0cmlidXRpb24uIFdlIGNhbiB1c2UgYSBHYXVzc2lhbiB3aXRoIGxvdHMgb2YgdmFyaWFuY2UuCgpgYGB7cn0KY2dhdXNzVFMxIDwtIGNvZWYoaGVhdEFsYXJtTW9kZWwkVFMxKQpzZFRTMSAgICAgPC0gaGVhdEFsYXJtTW9kZWwkVFMxJHNkCnByaW50KGNnYXVzc1RTMSkKcHJpbnQoc2RUUzEpCmBgYAoKYGBge3J9CmNnYXVzc1RTMVtpcy5uYW4oY2dhdXNzVFMxKV0gPC0gMApzZFRTMVtpcy5uYW4oc2RUUzEpXSA8LSAxMDAwMDAwCgpoZWF0QWxhcm1Nb2RlbCRUUzEgPC0gbGlzdCggY29lZiA9IGNnYXVzc1RTMSwgc2QgPSBzZFRTMSkKaGVhdEFsYXJtTW9kZWwkVFMxCmBgYAoKTGV0cyB0ZXN0IHNvbWUgaW5mZXJlbmNlIGNhc2VzIGluIHdoaWNoIHRoZSBUUzEgc2Vuc29yIGlzIG1hbGZ1bmN0aW9uaW5nIG9yIHJlcG9ydGluZyBzb21lIGtpbmQgb2YgYW5vbWFsb3VzIG1lYXN1cmVzLgoKSWYgVFMxIGlzIG9ubHkgb25lIGNhcHR1cmluZyBhIHZlcnkgaGlnaCB0ZW1wZXJhdHVyZSB0aGUgZmF1bHQgbm9kZSB3aWxsIGRldGVjdCB0aGUgYW5vbWFseQoKYGBge3J9CmUgPC0gbGlzdCggIlNlYXNvbiIgPSBTZWFzb25TdGF0ZXMkV2ludGVyLCAiVFMxIiA9IDM1LCAiVFMyIiA9IDI0LCAiVFMzIiA9IDIwICkKcXVlcnkgPC0gY3BxdWVyeShoZWF0QWxhcm1Nb2RlbCwgZXZlbnQgPSBBbGFybSA9PSAieWVzIiwgZXZpZGVuY2UgPSBlLCBtZXRob2QgPSAibHciLCBuID0gMTAwMDAwKQpwcmludChtZWFuKHJlcChxdWVyeSwgMTAwMDApKSkKYGBgCgpgYGB7cn0KZSA8LSBsaXN0KCAiU2Vhc29uIiA9IFNlYXNvblN0YXRlcyRXaW50ZXIsICJUUzEiID0gMzUsICJUUzIiID0gMjQsICJUUzMiID0gMjAgKQpxdWVyeSA8LSBjcHF1ZXJ5KGhlYXRBbGFybU1vZGVsLCBldmVudCA9IFRTMUZhdWx0ID09ICJ5ZXMiLCBldmlkZW5jZSA9IGUsIG1ldGhvZCA9ICJsdyIsIG4gPSAxMDAwMDApCnByaW50KG1lYW4ocmVwKHF1ZXJ5LCAxMDAwMCkpKQpgYGAKCklmIHRoZSB0aHJlZSBzZW5zb3JzIGFyZSBjYXB0dXJpbmcgdGhlIHNhbWUgYW5vbWFseSwgdGhlbiB0aGUgVFMxRmF1bHQgbm9kZSB3aWxsIGFzc3VtZSB0aGF0IHRoZSBwcm9ibGVtIGlzIG91dHNpZGUgdGhlIG1vZGVsLgoKYGBge3J9CmUgPC0gbGlzdCggIlNlYXNvbiIgPSBTZWFzb25TdGF0ZXMkV2ludGVyLCAiVFMxIiA9IDM1LCAiVFMyIiA9IDM3LCAiVFMzIiA9IDI5ICkKcXVlcnkgPC0gY3BxdWVyeShoZWF0QWxhcm1Nb2RlbCwgZXZlbnQgPSBBbGFybSA9PSAieWVzIiwgZXZpZGVuY2UgPSBlLCBtZXRob2QgPSAibHciLCBuID0gMTAwMDAwKQpwcmludChtZWFuKHJlcChxdWVyeSwgMTAwMDApKSkKYGBgCgpgYGB7cn0KZSA8LSBsaXN0KCAiU2Vhc29uIiA9IFNlYXNvblN0YXRlcyRXaW50ZXIsICJUUzEiID0gMzUsICJUUzIiID0gMzcsICJUUzMiID0gMjkgKQpxdWVyeSA8LSBjcHF1ZXJ5KGhlYXRBbGFybU1vZGVsLCBldmVudCA9IFRTMUZhdWx0ID09ICJ5ZXMiLCBldmlkZW5jZSA9IGUsIG1ldGhvZCA9ICJsdyIsIG4gPSAxMDAwMDApCnByaW50KG1lYW4ocmVwKHF1ZXJ5LCAxMDAwMCkpKQpgYGAKCgoKIyMgQSBtb3JlIGNvbXBsZXggcHJvYmxlbQoKVGhpcyBpcyBhIHZhcmlhdGlvbiBvbiB0aGUgcHJldmlvdXMgcHJvYmxlbS4gQ2FuIHlvdSBzcG90IHRoZSBkaWZmZXJlbmNlcyBpbnRyb2R1Y2VkIGJ5IHRoaXMgbmV3IHZlcnNpb24/CgpgYGB7cn0KZGF0YVJhdyA8LSByZWFkX2NzdigiLi9kYXRhL2hlYXRBbGFybS1sdmwyLmNzdiIpCmBgYAoKSXQgc2VlbXMgdGhhdCB0aGUgbWVhc3VyZXMgYXJlIHNvbWVob3cgZGVwZW5kZW50LCBidXQgdGhlIHJlbGF0aW9uc2hpcCBpcyBub3QgdGhhdCBjbGVhciB0aGF0IGluIHRoZSBwcmV2aW91cyBwaGFzZS4gQW4gZXgKCmBgYHtyfQpkYXRhUmF3ICU+JQogIGdhdGhlcigiU2Vuc29yIiwgIk1lYXN1cmUiLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9RGF0ZSwgeT1NZWFzdXJlLCBncm91cD1TZW5zb3IsIGNvbG9yPVNlbnNvcikpICsKICBnZW9tX2xpbmUoKQoKZGF0YVJhdyAlPiUKICBnYXRoZXIoIlNlbnNvciIsICJNZWFzdXJlIiwgVFMxLCBUUzIsIFRTMykgJT4lCiAgZ2dwbG90KGFlcyh4PURhdGUsIHk9TWVhc3VyZSwgZ3JvdXA9U2Vuc29yLCBjb2xvcj1TZW5zb3IpKSArCiAgZ2VvbV9zbW9vdGgoKQpgYGAKCiMjIyBMb29raW5nIGF0IHRoZSBjb250ZXh0Cgo+IE91ciBleHBlcnRzIHRlbGwgdXMgdGhhdCB0aGUgaW5kb29yIHRlbXBlcmF0dXJlIG9mIHRoZSByb29tIGlzIGFmZmVjdGVkIGJ5IHRoZSBvdXRkb29yIHdlYXRoZXIsIHNwZWNpYWxseSBuZWFyIHRoZSB3aW5kb3dzIGFuZCBkb29ycy4gVGhlIHRlbXBlcmF0dXJlIGluIHRoZSBpbnRlcmlvciBwYXJ0IG9mIHRoZSByb29tIGlzIG1vcmUgc3RhYmxlLgoKR2l2ZW4gdGhpcyBuZXcga25vd2xlZGdlIHdlIGRlY2lkZSB0byBwbGFjZSBhIG5ldyBzZW5zb3Igb3V0ZG9vcnMgYFRTT2AuIE5vdyB3ZSBjYW4gaGF2ZSBhbiBhZGRpdGlvbmFsIGNvbXBvbmVudCB0byBhZGQgdG8gb3VyIG1vZGVsLCBob3BlZnVsbHkgaXQgd2lsbCBhbGxvdyB0byBtaXh0dXJlIHRoZSBpbmRvb3IgYW5kIG91dGRvb3IgdGVtcGVyYXR1cmVzLgoKYGBge3J9CmRhdGFSYXcgJT4lCiAgZ2F0aGVyKCJTZW5zb3IiLCAiTWVhc3VyZSIsIFRTTywgVFMxLCBUUzIsIFRTMykgJT4lCiAgZ2dwbG90KGFlcyh4PURhdGUsIHk9TWVhc3VyZSwgZ3JvdXA9U2Vuc29yLCBjb2xvcj1TZW5zb3IpKSArCiAgZ2VvbV9zbW9vdGgoKQpgYGAKCmBgYHtyfQpkYXRhQnlNb250aCA8LSBkYXRhUmF3ICU+JQogIG11dGF0ZShNb250aCA9IGFzLmZhY3Rvcihtb250aChEYXRlLCBsYWJlbCA9IFRSVUUsIGFiYnIgPSBUUlVFKSkpCgpkYXRhQnlNb250aCAlPiUKICBnYXRoZXIoIlNlbnNvciIsICJNZWFzdXJlIiwgVFNPLCBUUzEsIFRTMiwgVFMzKSAlPiUKICBnZ3Bsb3QoYWVzKHg9TWVhc3VyZSwgY29sb3I9U2Vuc29yLCBmaWxsPVNlbnNvcikpICsgCiAgICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41KSArCiAgICBmYWNldF93cmFwKH5Nb250aCkKYGBgCgpgYGB7cn0KZGF0YUJ5U2Vhc29uIDwtIGRhdGFCeU1vbnRoICU+JQogIG11dGF0ZShTZWFzb24gPSBzZWFzb24oRGF0ZSkpCgpkYXRhQnlTZWFzb24gJT4lCiAgZ2F0aGVyKCJTZW5zb3IiLCAiTWVhc3VyZSIsIFRTTywgVFMxLCBUUzIsIFRTMykgJT4lCiAgZ2dwbG90KGFlcyh4PU1lYXN1cmUsIGNvbG9yPVNlbnNvciwgZmlsbD1TZW5zb3IpKSArIAogICAgZ2VvbV9kZW5zaXR5KGFscGhhPTAuNCkgKwogICAgZmFjZXRfd3JhcCh+U2Vhc29uKQpgYGAKCiMjIyBBZGRpbmcgbmV3IGNvbXBvbmVudHMgdG8gdGhlIG1vZGVsCgpUbyBhZGQgdGhlIG5ldyBzZW5zb3IgdG8gdGhlIG1vZGVsIHdlIGFyZSBnb2luZyB0byBuZWVkIGFuIGFkZGl0aW9uYWwgbGF0ZW50IHZhcmlhYmxlLiBPdXIgbW9kZWwgd2lsbCBub3cgaGF2ZSB0d28gbGF0ZW50IHZhcmlhYmxlcyB0byBtb2RlbCB0aGUgdGVtcGVyYXR1cmUgYFRJbmRgIGFuZCBgVE91dGAgKGluZG9vciBhbmQgb3V0ZG9vcikuIFRoZSBJbmRvb3IgdGVtcGVyYXR1cmUgd2lsbCBkZXBlbmQgb24gdGhlIG91dGRvb3IgdGVtcGVyYXR1cmUuIExldHMgYnVpbGQgaXQ6CgpgYGB7cn0KaGVhdEFsYXJtRGFnMiA8LSBtb2RlbDJuZXR3b3JrKCJbQWxhcm1dW1NlYXNvbl1bVE91dHxTZWFzb25dW1RTMHxUT3V0XVtUSW5kfFRPdXQ6QWxhcm1dW1RTMXxUSW5kXVtUUzJ8VEluZF1bVFMzfFRJbmRdIikKCnBsb3REM2JuKGhlYXRBbGFybURhZzIpCmBgYAoKVGhpcyBtb2RlbCBpcyB0aGUgYmFzZSBleGFtcGxlIG9mIHRoZSBuZXcgZG9tYWluLiBIb3dldmVyIHdlIGNhbiBoYXZlIHNvbWUgZG91YnRzIHJlZ2FyZGluZyB0aGUgcmVsYXRpb25zaGlwIG9mIHRoZSB2YXJpYWJsZSBUT3V0IGFuZCB0aGUgbW9kZWxzLCBsb29rIGNsb3NlbHkgYXQgdGhlIGRpc3RyaWJ1dGlvbnMgYW5kIHlvdSB3aWxsIHNlZSB0aGF0IG5vdCBhbGwgZGlzdHJpYnV0aW9ucyBhcmUgYWZmZWN0ZWQgZXF1YWxseSBieSB0aGUgb3V0ZG9vciB0ZW1wZXJhdHVyZS4gV2UgYXNrIGFnYWluLi4uCgo+IElvVCB0ZWFtOiBVaCwgeWVzLiBXZSBpbnN0YWxlZCBUUzEgaW4gYSBjb3JuZXIgb2YgdGhlIHJvb20sIG5lYXIgdGhlIGVudHJhbmNlLiBUUzIgaXMgb24gdGhlIGNlaWxpbmcgYW5kIG1pZ2h0IGJlIG5lYXIgYSB2ZW50aWxhdGlvbiBzaGFmdCwgc28gaXQgd2lsbCBwcm9iYWJseSBjYXB0dXJlIGEgYml0IG1vcmUgb2YgaGVhdCBmcm9tIHRoZSBtYWNoaW5lcy4gVFMzIHNob3VsZCBiZSBtb3JlIHN0YWJsZSwgaXQgaXMgbG9jYXRlZCBpbiB0aGUgbWlkZGxlIG9mIHRoZSByb29tLgoKVGhpcyBhbG1vc3QgbWFrZSBzZW5zZSB2aXN1YWxseS4gSG93ZXZlciwgYXQgdGhpcyBwb2ludCB3ZSB3aWxsIG5lZWQgdG8gdGVzdCB0aGUgZGlmZmVyZW50IHN0cnVjdHVyZXMsIGVpdGhlciBieSBsZWFybmluZyBhbGwgb2YgdGhlbSBhbmQgY29tcGFyaW5nIHdpdGggQklDLCBvciBydW5uaW5nIGEgc3RydWN0dXJhbCBsZWFybmluZyBwcm9jZXNzLgoKLS0tCgotLS0KCiMgTGljZW5zZQoKVGhpcyB0dXRvcmlhbCBpcyBsaWNlbnNlZCB1bmRlciBbR1BMMyBsaWNlbnNlXShodHRwczovL3d3dy5nbnUub3JnL2xpY2Vuc2VzL2xncGwtMy4wLmVuLmh0bWwpIGFzIGlzIG9yaWdpbmFsbHkgcHVibGlzaGVkIGFuZCBtYWludGFpbmVkIGluIHRoaXMgW2dpdGh1YiByZXBvc2l0b3J5XShodHRwczovL2dpdGh1Yi5jb20vamFjaW50b0FyaWFzL2JheWVzbmV0UnR1dG9yaWFsKSBieSBbSmFjaW50byBBcmlhc10oaHR0cDovL2phcmlhcy5lcykK