Notebook 08: Visualizations

Feedback should be send to goran.milovanovic@datakolektiv.com. These notebooks accompany the MilanoR talk 2019/06/25.


1. Setup

Note. The following chunks load packages, define the project directory tree and some constants.

### --- libraries
library(plotly)
library(data.table)
library(tidyverse)
library(visNetwork)
library(stringr)
library(tm)
library(BBmisc)
library(text2vec)
library(parallelDist)

### --- directories
dataDir <- 'data/'
analyticsDir <- 'analytics/'
funDir <- 'functions/'

2. Load the topical distributions: document_topic_matrix and word_topic_matrix

word_topic_matrix <- read.csv(
  paste0(analyticsDir, "analysis_word_topic_matrix.csv"), 
  header = T, 
  check.names = F, 
  row.names = 1,
  stringsAsFactors = F)

2. Concepts Distance Matrix

concepts <- rownames(word_topic_matrix)
# - Hellinger distances
conceptDist <- parDist(as.matrix(word_topic_matrix),
                       method = "hellinger",
                       diag = T,
                       upper = T,
                       threads = 7)
rm(word_topic_matrix); gc()
            used   (Mb) gc trigger   (Mb)  max used   (Mb)
Ncells   2130230  113.8    3968292  212.0   3019832  161.3
Vcells 153158176 1168.6  213581747 1629.5 171223122 1306.4
conceptDistMat <- as.matrix(conceptDist)
rm(conceptDist); gc()
            used  (Mb) gc trigger   (Mb)  max used   (Mb)
Ncells   2139946 114.3    3968292  212.0   3019832  161.3
Vcells 103343176 788.5  408129829 3113.8 502043035 3830.3
rownames(conceptDistMat) <- concepts
colnames(conceptDistMat) <- concepts
saveRDS(conceptDistMat, 
        paste0(analyticsDir, "conceptDistMat.Rds")
        )
rm(concepts)

3 Visualize!

3.1 Visualize local concept neighbourhoods: Twitter

# - locate "twitter"
term <- "twitter"
# - size of the neighbourhood
n_size <- 20
wTerm <- which(grepl(term, rownames(conceptDistMat)))
# - fetch neighbourhood
ng1 <- vector(mode = "list", length = length(wTerm))
for (i in 1:length(wTerm)) {
  ng1[[i]] <- names(
    sort(conceptDistMat[wTerm[i], ], decreasing = F)[1:n_size+1]
  )
}
names(ng1) <- rownames(conceptDistMat)[wTerm]
for (i in 1:length(ng1)) {
  ng1[[i]] <- setdiff(ng1[[i]], names(ng1)[i])
}
ng2 <- vector(mode = "list", length = sum(sapply(ng1, length)))
c <- 0
for (i in 1:length(ng1)) {
  for (j in 1:length(ng1[[i]])) {
    c <- c + 1
    ng2[[c]] <- names(
    sort(
      conceptDistMat[which(rownames(conceptDistMat) == ng1[[i]][j]), ], 
      decreasing = F)[1:n_size+1]
    )
  }
}
names(ng2) <- unname(unlist(ng1))
for (i in 1:length(ng2)) {
  ng2[[i]] <- setdiff(ng2[[i]], names(ng2)[i])
}
graphData <- rbind(stack(ng1), stack(ng2))
graphData$ind <- as.character(graphData$ind)
graphData <- graphData[, c(2, 1)]
colnames(graphData) <- c('outgoing', 'incoming')
graphData$incoming[grepl("^wd_", graphData$incoming)] <- 
  toupper(graphData$incoming[grepl("^wd_", graphData$incoming)])
graphData$outgoing[grepl("^wd_", graphData$outgoing)] <- 
  toupper(graphData$outgoing[grepl("^wd_", graphData$outgoing)])

# - visualize w. {visNetwork}
nodes <- unique(c(graphData$outgoing, graphData$incoming))
nodes <- data.frame(id = 1:length(nodes),
                    label = nodes,
                    stringsAsFactors = F)
edges <- graphData
colnames(edges) <- c("from", "to")
edges$from <- sapply(edges$from, function(x) {
  nodes$id[which(nodes$label == x)]
})
edges$to <- sapply(edges$to, function(x) {
  nodes$id[which(nodes$label == x)]
})

# - visualize
visNetwork(nodes, edges, width = "100%") %>%
  visIgraphLayout() %>%
  visNodes(
    shape = "dot",
    color = list(
      background = "#0085AF",
      border = "#013848",
      highlight = "#FF8000"
    ),
    shadow = list(enabled = TRUE, size = 10)
  ) %>%
  visEdges(
    shadow = FALSE,
    color = list(color = "#0085AF", highlight = "#C62F4B")
  ) %>%
  visOptions(highlightNearest = TRUE, selectedBy = "label") %>%
  visLayout(randomSeed = 11)

3.2 Visualize local concept neighbourhoods: Google

# - locate "google"
term <- "google"
# - size of the neighbourhood
n_size <- 5
wTerm <- which(grepl(term, rownames(conceptDistMat)))
# - fetch neighbourhood
ng1 <- vector(mode = "list", length = length(wTerm))
for (i in 1:length(wTerm)) {
  ng1[[i]] <- names(
    sort(conceptDistMat[wTerm[i], ], decreasing = F)[1:n_size+1]
  )
}
names(ng1) <- rownames(conceptDistMat)[wTerm]
for (i in 1:length(ng1)) {
  ng1[[i]] <- setdiff(ng1[[i]], names(ng1)[i])
}
ng2 <- vector(mode = "list", length = sum(sapply(ng1, length)))
c <- 0
for (i in 1:length(ng1)) {
  for (j in 1:length(ng1[[i]])) {
    c <- c + 1
    ng2[[c]] <- names(
    sort(
      conceptDistMat[which(rownames(conceptDistMat) == ng1[[i]][j]), ], 
      decreasing = F)[1:n_size+1]
    )
  }
}
names(ng2) <- unname(unlist(ng1))
for (i in 1:length(ng2)) {
  ng2[[i]] <- setdiff(ng2[[i]], names(ng2)[i])
}
graphData <- rbind(stack(ng1), stack(ng2))
graphData$ind <- as.character(graphData$ind)
graphData <- graphData[, c(2, 1)]
colnames(graphData) <- c('outgoing', 'incoming')
graphData$incoming[grepl("^wd_", graphData$incoming)] <- 
  toupper(graphData$incoming[grepl("^wd_", graphData$incoming)])
graphData$outgoing[grepl("^wd_", graphData$outgoing)] <- 
  toupper(graphData$outgoing[grepl("^wd_", graphData$outgoing)])

# - visualize w. {visNetwork}
nodes <- unique(c(graphData$outgoing, graphData$incoming))
nodes <- data.frame(id = 1:length(nodes),
                    label = nodes,
                    stringsAsFactors = F)
edges <- graphData
colnames(edges) <- c("from", "to")
edges$from <- sapply(edges$from, function(x) {
  nodes$id[which(nodes$label == x)]
})
edges$to <- sapply(edges$to, function(x) {
  nodes$id[which(nodes$label == x)]
})

# - visualize
visNetwork(nodes, edges, width = "100%") %>%
  visIgraphLayout() %>%
  visNodes(
    shape = "dot",
    color = list(
      background = "#0085AF",
      border = "#013848",
      highlight = "#FF8000"
    ),
    shadow = list(enabled = TRUE, size = 10)
  ) %>%
  visEdges(
    shadow = FALSE,
    color = list(color = "#0085AF", highlight = "#C62F4B")
  ) %>%
  visOptions(highlightNearest = TRUE, selectedBy = "label") %>%
  visLayout(randomSeed = 11)

3.3 Visualize local concept neighbourhoods: Apple

# - locate "Apple"
term <- "appl"
# - size of the neighbourhood
n_size <- 5
wTerm <- which(grepl(term, rownames(conceptDistMat)))
# - fetch neighbourhood
ng1 <- vector(mode = "list", length = length(wTerm))
for (i in 1:length(wTerm)) {
  ng1[[i]] <- names(
    sort(conceptDistMat[wTerm[i], ], decreasing = F)[1:n_size+1]
  )
}
names(ng1) <- rownames(conceptDistMat)[wTerm]
for (i in 1:length(ng1)) {
  ng1[[i]] <- setdiff(ng1[[i]], names(ng1)[i])
}
ng2 <- vector(mode = "list", length = sum(sapply(ng1, length)))
c <- 0
for (i in 1:length(ng1)) {
  for (j in 1:length(ng1[[i]])) {
    c <- c + 1
    ng2[[c]] <- names(
    sort(
      conceptDistMat[which(rownames(conceptDistMat) == ng1[[i]][j]), ], 
      decreasing = F)[1:n_size+1]
    )
  }
}
names(ng2) <- unname(unlist(ng1))
for (i in 1:length(ng2)) {
  ng2[[i]] <- setdiff(ng2[[i]], names(ng2)[i])
}
graphData <- rbind(stack(ng1), stack(ng2))
graphData$ind <- as.character(graphData$ind)
graphData <- graphData[, c(2, 1)]
colnames(graphData) <- c('outgoing', 'incoming')
graphData$incoming[grepl("^wd_", graphData$incoming)] <- 
  toupper(graphData$incoming[grepl("^wd_", graphData$incoming)])
graphData$outgoing[grepl("^wd_", graphData$outgoing)] <- 
  toupper(graphData$outgoing[grepl("^wd_", graphData$outgoing)])

# - visualize w. {visNetwork}
nodes <- unique(c(graphData$outgoing, graphData$incoming))
nodes <- data.frame(id = 1:length(nodes),
                    label = nodes,
                    stringsAsFactors = F)
edges <- graphData
colnames(edges) <- c("from", "to")
edges$from <- sapply(edges$from, function(x) {
  nodes$id[which(nodes$label == x)]
})
edges$to <- sapply(edges$to, function(x) {
  nodes$id[which(nodes$label == x)]
})

# - visualize
visNetwork(nodes, edges, width = "100%") %>%
  visIgraphLayout() %>%
  visNodes(
    shape = "dot",
    color = list(
      background = "#0085AF",
      border = "#013848",
      highlight = "#FF8000"
    ),
    shadow = list(enabled = TRUE, size = 10)
  ) %>%
  visEdges(
    shadow = FALSE,
    color = list(color = "#0085AF", highlight = "#C62F4B")
  ) %>%
  visOptions(highlightNearest = TRUE, selectedBy = "label") %>%
  visLayout(randomSeed = 11)

3.4 Visualize local concept neighbourhoods: Amazon

# - locate "Amazon"
term <- "amazon"
# - size of the neighbourhood
n_size <- 5
wTerm <- which(grepl(term, rownames(conceptDistMat)))
# - fetch neighbourhood
ng1 <- vector(mode = "list", length = length(wTerm))
for (i in 1:length(wTerm)) {
  ng1[[i]] <- names(
    sort(conceptDistMat[wTerm[i], ], decreasing = F)[1:n_size+1]
  )
}
names(ng1) <- rownames(conceptDistMat)[wTerm]
for (i in 1:length(ng1)) {
  ng1[[i]] <- setdiff(ng1[[i]], names(ng1)[i])
}
ng2 <- vector(mode = "list", length = sum(sapply(ng1, length)))
c <- 0
for (i in 1:length(ng1)) {
  for (j in 1:length(ng1[[i]])) {
    c <- c + 1
    ng2[[c]] <- names(
    sort(
      conceptDistMat[which(rownames(conceptDistMat) == ng1[[i]][j]), ], 
      decreasing = F)[1:n_size+1]
    )
  }
}
names(ng2) <- unname(unlist(ng1))
for (i in 1:length(ng2)) {
  ng2[[i]] <- setdiff(ng2[[i]], names(ng2)[i])
}
graphData <- rbind(stack(ng1), stack(ng2))
graphData$ind <- as.character(graphData$ind)
graphData <- graphData[, c(2, 1)]
colnames(graphData) <- c('outgoing', 'incoming')
graphData$incoming[grepl("^wd_", graphData$incoming)] <- 
  toupper(graphData$incoming[grepl("^wd_", graphData$incoming)])
graphData$outgoing[grepl("^wd_", graphData$outgoing)] <- 
  toupper(graphData$outgoing[grepl("^wd_", graphData$outgoing)])

# - visualize w. {visNetwork}
nodes <- unique(c(graphData$outgoing, graphData$incoming))
nodes <- data.frame(id = 1:length(nodes),
                    label = nodes,
                    stringsAsFactors = F)
edges <- graphData
colnames(edges) <- c("from", "to")
edges$from <- sapply(edges$from, function(x) {
  nodes$id[which(nodes$label == x)]
})
edges$to <- sapply(edges$to, function(x) {
  nodes$id[which(nodes$label == x)]
})

# - visualize
visNetwork(nodes, edges, width = "100%") %>%
  visIgraphLayout() %>%
  visNodes(
    shape = "dot",
    color = list(
      background = "#0085AF",
      border = "#013848",
      highlight = "#FF8000"
    ),
    shadow = list(enabled = TRUE, size = 10)
  ) %>%
  visEdges(
    shadow = FALSE,
    color = list(color = "#0085AF", highlight = "#C62F4B")
  ) %>%
  visOptions(highlightNearest = TRUE, selectedBy = "label") %>%
  visLayout(randomSeed = 11)

3.5 Visualize local concept neighbourhoods: Microsoft

# - locate "Microsoft"
term <- "microsoft"
# - size of the neighbourhood
n_size <- 10
wTerm <- which(grepl(term, rownames(conceptDistMat)))
# - fetch neighbourhood
ng1 <- vector(mode = "list", length = length(wTerm))
for (i in 1:length(wTerm)) {
  ng1[[i]] <- names(
    sort(conceptDistMat[wTerm[i], ], decreasing = F)[1:n_size+1]
  )
}
names(ng1) <- rownames(conceptDistMat)[wTerm]
for (i in 1:length(ng1)) {
  ng1[[i]] <- setdiff(ng1[[i]], names(ng1)[i])
}
ng2 <- vector(mode = "list", length = sum(sapply(ng1, length)))
c <- 0
for (i in 1:length(ng1)) {
  for (j in 1:length(ng1[[i]])) {
    c <- c + 1
    ng2[[c]] <- names(
    sort(
      conceptDistMat[which(rownames(conceptDistMat) == ng1[[i]][j]), ], 
      decreasing = F)[1:n_size+1]
    )
  }
}
names(ng2) <- unname(unlist(ng1))
for (i in 1:length(ng2)) {
  ng2[[i]] <- setdiff(ng2[[i]], names(ng2)[i])
}
graphData <- rbind(stack(ng1), stack(ng2))
graphData$ind <- as.character(graphData$ind)
graphData <- graphData[, c(2, 1)]
colnames(graphData) <- c('outgoing', 'incoming')
graphData$incoming[grepl("^wd_", graphData$incoming)] <- 
  toupper(graphData$incoming[grepl("^wd_", graphData$incoming)])
graphData$outgoing[grepl("^wd_", graphData$outgoing)] <- 
  toupper(graphData$outgoing[grepl("^wd_", graphData$outgoing)])

# - visualize w. {visNetwork}
nodes <- unique(c(graphData$outgoing, graphData$incoming))
nodes <- data.frame(id = 1:length(nodes),
                    label = nodes,
                    stringsAsFactors = F)
edges <- graphData
colnames(edges) <- c("from", "to")
edges$from <- sapply(edges$from, function(x) {
  nodes$id[which(nodes$label == x)]
})
edges$to <- sapply(edges$to, function(x) {
  nodes$id[which(nodes$label == x)]
})

# - visualize
visNetwork(nodes, edges, width = "100%") %>%
  visIgraphLayout() %>%
  visNodes(
    shape = "dot",
    color = list(
      background = "#0085AF",
      border = "#013848",
      highlight = "#FF8000"
    ),
    shadow = list(enabled = TRUE, size = 10)
  ) %>%
  visEdges(
    shadow = FALSE,
    color = list(color = "#0085AF", highlight = "#C62F4B")
  ) %>%
  visOptions(highlightNearest = TRUE, selectedBy = "label") %>%
  visLayout(randomSeed = 11)

3.6 Visualize local concept neighbourhoods: Facebook

# - locate "Facebook"
term <- "facebook"
# - size of the neighbourhood
n_size <- 3
wTerm <- which(grepl(term, rownames(conceptDistMat)))
# - fetch neighbourhood
ng1 <- vector(mode = "list", length = length(wTerm))
for (i in 1:length(wTerm)) {
  ng1[[i]] <- names(
    sort(conceptDistMat[wTerm[i], ], decreasing = F)[1:n_size+1]
  )
}
names(ng1) <- rownames(conceptDistMat)[wTerm]
for (i in 1:length(ng1)) {
  ng1[[i]] <- setdiff(ng1[[i]], names(ng1)[i])
}
ng2 <- vector(mode = "list", length = sum(sapply(ng1, length)))
c <- 0
for (i in 1:length(ng1)) {
  for (j in 1:length(ng1[[i]])) {
    c <- c + 1
    ng2[[c]] <- names(
    sort(
      conceptDistMat[which(rownames(conceptDistMat) == ng1[[i]][j]), ], 
      decreasing = F)[1:n_size+1]
    )
  }
}
names(ng2) <- unname(unlist(ng1))
for (i in 1:length(ng2)) {
  ng2[[i]] <- setdiff(ng2[[i]], names(ng2)[i])
}
graphData <- rbind(stack(ng1), stack(ng2))
graphData$ind <- as.character(graphData$ind)
graphData <- graphData[, c(2, 1)]
colnames(graphData) <- c('outgoing', 'incoming')
graphData$incoming[grepl("^wd_", graphData$incoming)] <- 
  toupper(graphData$incoming[grepl("^wd_", graphData$incoming)])
graphData$outgoing[grepl("^wd_", graphData$outgoing)] <- 
  toupper(graphData$outgoing[grepl("^wd_", graphData$outgoing)])

# - visualize w. {visNetwork}
nodes <- unique(c(graphData$outgoing, graphData$incoming))
nodes <- data.frame(id = 1:length(nodes),
                    label = nodes,
                    stringsAsFactors = F)
edges <- graphData
colnames(edges) <- c("from", "to")
edges$from <- sapply(edges$from, function(x) {
  nodes$id[which(nodes$label == x)]
})
edges$to <- sapply(edges$to, function(x) {
  nodes$id[which(nodes$label == x)]
})

# - visualize
visNetwork(nodes, edges, width = "100%") %>%
  visIgraphLayout() %>%
  visNodes(
    shape = "dot",
    color = list(
      background = "#0085AF",
      border = "#013848",
      highlight = "#FF8000"
    ),
    shadow = list(enabled = TRUE, size = 10)
  ) %>%
  visEdges(
    shadow = FALSE,
    color = list(color = "#0085AF", highlight = "#C62F4B")
  ) %>%
  visOptions(highlightNearest = TRUE, selectedBy = "label") %>%
  visLayout(randomSeed = 11)

Goran S. Milovanović & Mike Page

DataKolektiv, 2019.

contact:


License: GPLv3 This Notebook is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This Notebook is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this Notebook. If not, see http://www.gnu.org/licenses/.


LS0tCnRpdGxlOiBTZW1hbnRpYyBXZWIgVGVjaG5vbG9naWVzIGFuZCBXaWtpZGF0YSBmcm9tIFIKYXV0aG9yOgotIG5hbWU6IEdvcmFuIFMuIE1pbG92YW5vdmnEhwogIGFmZmlsaWF0aW9uOiBXaWtpbWVkaWEgRGV1dHNjaGxhbmQsIERhdGEgU2NpZW50aXN0LCBEYXRhS29sZWt0aXYsIE93bmVyCi0gbmFtZTogTWlrZSBQYWdlCiAgYWZmaWxpYXRpb246IERhdGFLb2xla3RpdiwgSnVuaW9yIERhdGEgU2NpZW50aXN0CmRhdGU6ICJgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVkICVCICVZJylgIgphYnN0cmFjdDogCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgY29kZV9mb2xkaW5nOiBzaG93CiAgICB0aGVtZTogc3BhY2VsYWIKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwogICAgdG9jX2RlcHRoOiA1CiAgaHRtbF9kb2N1bWVudDoKICAgIHRvYzogeWVzCiAgICB0b2NfZGVwdGg6IDUKLS0tCgohW10oaW1nL0RLX0xvZ29fMTAwLnBuZykKCioqKgojIyMgTm90ZWJvb2sgMDg6IFZpc3VhbGl6YXRpb25zCioqRmVlZGJhY2sqKiBzaG91bGQgYmUgc2VuZCB0byBgZ29yYW4ubWlsb3Zhbm92aWNAZGF0YWtvbGVrdGl2LmNvbWAuIApUaGVzZSBub3RlYm9va3MgYWNjb21wYW55IHRoZSBNaWxhbm9SIHRhbGsgMjAxOS8wNi8yNS4KCioqKgoKIyMjIDEuIFNldHVwCgoqKk5vdGUuKiogVGhlIGZvbGxvd2luZyBjaHVua3MgbG9hZCBwYWNrYWdlcywgZGVmaW5lIHRoZSBwcm9qZWN0IGRpcmVjdG9yeSB0cmVlIGFuZCBzb21lIGNvbnN0YW50cy4KCmBgYHtyIGVjaG8gPSBULCBldmFsID0gVCwgbWVzc2FnZSA9IEZ9CiMjIyAtLS0gbGlicmFyaWVzCmxpYnJhcnkocGxvdGx5KQpsaWJyYXJ5KGRhdGEudGFibGUpCmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KHZpc05ldHdvcmspCmxpYnJhcnkoc3RyaW5ncikKbGlicmFyeSh0bSkKbGlicmFyeShCQm1pc2MpCmxpYnJhcnkodGV4dDJ2ZWMpCmxpYnJhcnkocGFyYWxsZWxEaXN0KQoKIyMjIC0tLSBkaXJlY3RvcmllcwpkYXRhRGlyIDwtICdkYXRhLycKYW5hbHl0aWNzRGlyIDwtICdhbmFseXRpY3MvJwpmdW5EaXIgPC0gJ2Z1bmN0aW9ucy8nCmBgYAoKIyMjIDIuIExvYWQgdGhlIHRvcGljYWwgZGlzdHJpYnV0aW9uczogYGRvY3VtZW50X3RvcGljX21hdHJpeGAgYW5kIGB3b3JkX3RvcGljX21hdHJpeGAKCmBgYHtyIGVjaG8gPSBULCBldmFsID0gVH0Kd29yZF90b3BpY19tYXRyaXggPC0gcmVhZC5jc3YoCiAgcGFzdGUwKGFuYWx5dGljc0RpciwgImFuYWx5c2lzX3dvcmRfdG9waWNfbWF0cml4LmNzdiIpLCAKICBoZWFkZXIgPSBULCAKICBjaGVjay5uYW1lcyA9IEYsIAogIHJvdy5uYW1lcyA9IDEsCiAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpCmBgYAoKIyMjIDIuIENvbmNlcHRzIERpc3RhbmNlIE1hdHJpeAoKYGBge3IgZWNobyA9IFQsIGV2YWwgPSBUfQpjb25jZXB0cyA8LSByb3duYW1lcyh3b3JkX3RvcGljX21hdHJpeCkKIyAtIEhlbGxpbmdlciBkaXN0YW5jZXMKY29uY2VwdERpc3QgPC0gcGFyRGlzdChhcy5tYXRyaXgod29yZF90b3BpY19tYXRyaXgpLAogICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJoZWxsaW5nZXIiLAogICAgICAgICAgICAgICAgICAgICAgIGRpYWcgPSBULAogICAgICAgICAgICAgICAgICAgICAgIHVwcGVyID0gVCwKICAgICAgICAgICAgICAgICAgICAgICB0aHJlYWRzID0gNykKcm0od29yZF90b3BpY19tYXRyaXgpOyBnYygpCmNvbmNlcHREaXN0TWF0IDwtIGFzLm1hdHJpeChjb25jZXB0RGlzdCkKcm0oY29uY2VwdERpc3QpOyBnYygpCnJvd25hbWVzKGNvbmNlcHREaXN0TWF0KSA8LSBjb25jZXB0cwpjb2xuYW1lcyhjb25jZXB0RGlzdE1hdCkgPC0gY29uY2VwdHMKc2F2ZVJEUyhjb25jZXB0RGlzdE1hdCwgCiAgICAgICAgcGFzdGUwKGFuYWx5dGljc0RpciwgImNvbmNlcHREaXN0TWF0LlJkcyIpCiAgICAgICAgKQpybShjb25jZXB0cykKYGBgCgojIyMgMyBWaXN1YWxpemUhCgojIyMjIDMuMSBWaXN1YWxpemUgbG9jYWwgY29uY2VwdCBuZWlnaGJvdXJob29kczogYFR3aXR0ZXJgCgpgYGB7ciBlY2hvID0gVCwgZXZhbCA9IFR9CiMgLSBsb2NhdGUgInR3aXR0ZXIiCnRlcm0gPC0gInR3aXR0ZXIiCiMgLSBzaXplIG9mIHRoZSBuZWlnaGJvdXJob29kCm5fc2l6ZSA8LSAyMAp3VGVybSA8LSB3aGljaChncmVwbCh0ZXJtLCByb3duYW1lcyhjb25jZXB0RGlzdE1hdCkpKQojIC0gZmV0Y2ggbmVpZ2hib3VyaG9vZApuZzEgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IGxlbmd0aCh3VGVybSkpCmZvciAoaSBpbiAxOmxlbmd0aCh3VGVybSkpIHsKICBuZzFbW2ldXSA8LSBuYW1lcygKICAgIHNvcnQoY29uY2VwdERpc3RNYXRbd1Rlcm1baV0sIF0sIGRlY3JlYXNpbmcgPSBGKVsxOm5fc2l6ZSsxXQogICkKfQpuYW1lcyhuZzEpIDwtIHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KVt3VGVybV0KZm9yIChpIGluIDE6bGVuZ3RoKG5nMSkpIHsKICBuZzFbW2ldXSA8LSBzZXRkaWZmKG5nMVtbaV1dLCBuYW1lcyhuZzEpW2ldKQp9Cm5nMiA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gc3VtKHNhcHBseShuZzEsIGxlbmd0aCkpKQpjIDwtIDAKZm9yIChpIGluIDE6bGVuZ3RoKG5nMSkpIHsKICBmb3IgKGogaW4gMTpsZW5ndGgobmcxW1tpXV0pKSB7CiAgICBjIDwtIGMgKyAxCiAgICBuZzJbW2NdXSA8LSBuYW1lcygKICAgIHNvcnQoCiAgICAgIGNvbmNlcHREaXN0TWF0W3doaWNoKHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KSA9PSBuZzFbW2ldXVtqXSksIF0sIAogICAgICBkZWNyZWFzaW5nID0gRilbMTpuX3NpemUrMV0KICAgICkKICB9Cn0KbmFtZXMobmcyKSA8LSB1bm5hbWUodW5saXN0KG5nMSkpCmZvciAoaSBpbiAxOmxlbmd0aChuZzIpKSB7CiAgbmcyW1tpXV0gPC0gc2V0ZGlmZihuZzJbW2ldXSwgbmFtZXMobmcyKVtpXSkKfQpncmFwaERhdGEgPC0gcmJpbmQoc3RhY2sobmcxKSwgc3RhY2sobmcyKSkKZ3JhcGhEYXRhJGluZCA8LSBhcy5jaGFyYWN0ZXIoZ3JhcGhEYXRhJGluZCkKZ3JhcGhEYXRhIDwtIGdyYXBoRGF0YVssIGMoMiwgMSldCmNvbG5hbWVzKGdyYXBoRGF0YSkgPC0gYygnb3V0Z29pbmcnLCAnaW5jb21pbmcnKQpncmFwaERhdGEkaW5jb21pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkaW5jb21pbmcpXSA8LSAKICB0b3VwcGVyKGdyYXBoRGF0YSRpbmNvbWluZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRpbmNvbWluZyldKQpncmFwaERhdGEkb3V0Z29pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkb3V0Z29pbmcpXSA8LSAKICB0b3VwcGVyKGdyYXBoRGF0YSRvdXRnb2luZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRvdXRnb2luZyldKQoKIyAtIHZpc3VhbGl6ZSB3LiB7dmlzTmV0d29ya30Kbm9kZXMgPC0gdW5pcXVlKGMoZ3JhcGhEYXRhJG91dGdvaW5nLCBncmFwaERhdGEkaW5jb21pbmcpKQpub2RlcyA8LSBkYXRhLmZyYW1lKGlkID0gMTpsZW5ndGgobm9kZXMpLAogICAgICAgICAgICAgICAgICAgIGxhYmVsID0gbm9kZXMsCiAgICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpCmVkZ2VzIDwtIGdyYXBoRGF0YQpjb2xuYW1lcyhlZGdlcykgPC0gYygiZnJvbSIsICJ0byIpCmVkZ2VzJGZyb20gPC0gc2FwcGx5KGVkZ2VzJGZyb20sIGZ1bmN0aW9uKHgpIHsKICBub2RlcyRpZFt3aGljaChub2RlcyRsYWJlbCA9PSB4KV0KfSkKZWRnZXMkdG8gPC0gc2FwcGx5KGVkZ2VzJHRvLCBmdW5jdGlvbih4KSB7CiAgbm9kZXMkaWRbd2hpY2gobm9kZXMkbGFiZWwgPT0geCldCn0pCgojIC0gdmlzdWFsaXplCnZpc05ldHdvcmsobm9kZXMsIGVkZ2VzLCB3aWR0aCA9ICIxMDAlIikgJT4lCiAgdmlzSWdyYXBoTGF5b3V0KCkgJT4lCiAgdmlzTm9kZXMoCiAgICBzaGFwZSA9ICJkb3QiLAogICAgY29sb3IgPSBsaXN0KAogICAgICBiYWNrZ3JvdW5kID0gIiMwMDg1QUYiLAogICAgICBib3JkZXIgPSAiIzAxMzg0OCIsCiAgICAgIGhpZ2hsaWdodCA9ICIjRkY4MDAwIgogICAgKSwKICAgIHNoYWRvdyA9IGxpc3QoZW5hYmxlZCA9IFRSVUUsIHNpemUgPSAxMCkKICApICU+JQogIHZpc0VkZ2VzKAogICAgc2hhZG93ID0gRkFMU0UsCiAgICBjb2xvciA9IGxpc3QoY29sb3IgPSAiIzAwODVBRiIsIGhpZ2hsaWdodCA9ICIjQzYyRjRCIikKICApICU+JQogIHZpc09wdGlvbnMoaGlnaGxpZ2h0TmVhcmVzdCA9IFRSVUUsIHNlbGVjdGVkQnkgPSAibGFiZWwiKSAlPiUKICB2aXNMYXlvdXQocmFuZG9tU2VlZCA9IDExKQpgYGAKCiMjIyMgMy4yIFZpc3VhbGl6ZSBsb2NhbCBjb25jZXB0IG5laWdoYm91cmhvb2RzOiBgR29vZ2xlYAoKYGBge3IgZWNobyA9IFQsIGV2YWwgPSBUfQojIC0gbG9jYXRlICJnb29nbGUiCnRlcm0gPC0gImdvb2dsZSIKIyAtIHNpemUgb2YgdGhlIG5laWdoYm91cmhvb2QKbl9zaXplIDwtIDUKd1Rlcm0gPC0gd2hpY2goZ3JlcGwodGVybSwgcm93bmFtZXMoY29uY2VwdERpc3RNYXQpKSkKIyAtIGZldGNoIG5laWdoYm91cmhvb2QKbmcxIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSBsZW5ndGgod1Rlcm0pKQpmb3IgKGkgaW4gMTpsZW5ndGgod1Rlcm0pKSB7CiAgbmcxW1tpXV0gPC0gbmFtZXMoCiAgICBzb3J0KGNvbmNlcHREaXN0TWF0W3dUZXJtW2ldLCBdLCBkZWNyZWFzaW5nID0gRilbMTpuX3NpemUrMV0KICApCn0KbmFtZXMobmcxKSA8LSByb3duYW1lcyhjb25jZXB0RGlzdE1hdClbd1Rlcm1dCmZvciAoaSBpbiAxOmxlbmd0aChuZzEpKSB7CiAgbmcxW1tpXV0gPC0gc2V0ZGlmZihuZzFbW2ldXSwgbmFtZXMobmcxKVtpXSkKfQpuZzIgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IHN1bShzYXBwbHkobmcxLCBsZW5ndGgpKSkKYyA8LSAwCmZvciAoaSBpbiAxOmxlbmd0aChuZzEpKSB7CiAgZm9yIChqIGluIDE6bGVuZ3RoKG5nMVtbaV1dKSkgewogICAgYyA8LSBjICsgMQogICAgbmcyW1tjXV0gPC0gbmFtZXMoCiAgICBzb3J0KAogICAgICBjb25jZXB0RGlzdE1hdFt3aGljaChyb3duYW1lcyhjb25jZXB0RGlzdE1hdCkgPT0gbmcxW1tpXV1bal0pLCBdLCAKICAgICAgZGVjcmVhc2luZyA9IEYpWzE6bl9zaXplKzFdCiAgICApCiAgfQp9Cm5hbWVzKG5nMikgPC0gdW5uYW1lKHVubGlzdChuZzEpKQpmb3IgKGkgaW4gMTpsZW5ndGgobmcyKSkgewogIG5nMltbaV1dIDwtIHNldGRpZmYobmcyW1tpXV0sIG5hbWVzKG5nMilbaV0pCn0KZ3JhcGhEYXRhIDwtIHJiaW5kKHN0YWNrKG5nMSksIHN0YWNrKG5nMikpCmdyYXBoRGF0YSRpbmQgPC0gYXMuY2hhcmFjdGVyKGdyYXBoRGF0YSRpbmQpCmdyYXBoRGF0YSA8LSBncmFwaERhdGFbLCBjKDIsIDEpXQpjb2xuYW1lcyhncmFwaERhdGEpIDwtIGMoJ291dGdvaW5nJywgJ2luY29taW5nJykKZ3JhcGhEYXRhJGluY29taW5nW2dyZXBsKCJed2RfIiwgZ3JhcGhEYXRhJGluY29taW5nKV0gPC0gCiAgdG91cHBlcihncmFwaERhdGEkaW5jb21pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkaW5jb21pbmcpXSkKZ3JhcGhEYXRhJG91dGdvaW5nW2dyZXBsKCJed2RfIiwgZ3JhcGhEYXRhJG91dGdvaW5nKV0gPC0gCiAgdG91cHBlcihncmFwaERhdGEkb3V0Z29pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkb3V0Z29pbmcpXSkKCiMgLSB2aXN1YWxpemUgdy4ge3Zpc05ldHdvcmt9Cm5vZGVzIDwtIHVuaXF1ZShjKGdyYXBoRGF0YSRvdXRnb2luZywgZ3JhcGhEYXRhJGluY29taW5nKSkKbm9kZXMgPC0gZGF0YS5mcmFtZShpZCA9IDE6bGVuZ3RoKG5vZGVzKSwKICAgICAgICAgICAgICAgICAgICBsYWJlbCA9IG5vZGVzLAogICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKQplZGdlcyA8LSBncmFwaERhdGEKY29sbmFtZXMoZWRnZXMpIDwtIGMoImZyb20iLCAidG8iKQplZGdlcyRmcm9tIDwtIHNhcHBseShlZGdlcyRmcm9tLCBmdW5jdGlvbih4KSB7CiAgbm9kZXMkaWRbd2hpY2gobm9kZXMkbGFiZWwgPT0geCldCn0pCmVkZ2VzJHRvIDwtIHNhcHBseShlZGdlcyR0bywgZnVuY3Rpb24oeCkgewogIG5vZGVzJGlkW3doaWNoKG5vZGVzJGxhYmVsID09IHgpXQp9KQoKIyAtIHZpc3VhbGl6ZQp2aXNOZXR3b3JrKG5vZGVzLCBlZGdlcywgd2lkdGggPSAiMTAwJSIpICU+JQogIHZpc0lncmFwaExheW91dCgpICU+JQogIHZpc05vZGVzKAogICAgc2hhcGUgPSAiZG90IiwKICAgIGNvbG9yID0gbGlzdCgKICAgICAgYmFja2dyb3VuZCA9ICIjMDA4NUFGIiwKICAgICAgYm9yZGVyID0gIiMwMTM4NDgiLAogICAgICBoaWdobGlnaHQgPSAiI0ZGODAwMCIKICAgICksCiAgICBzaGFkb3cgPSBsaXN0KGVuYWJsZWQgPSBUUlVFLCBzaXplID0gMTApCiAgKSAlPiUKICB2aXNFZGdlcygKICAgIHNoYWRvdyA9IEZBTFNFLAogICAgY29sb3IgPSBsaXN0KGNvbG9yID0gIiMwMDg1QUYiLCBoaWdobGlnaHQgPSAiI0M2MkY0QiIpCiAgKSAlPiUKICB2aXNPcHRpb25zKGhpZ2hsaWdodE5lYXJlc3QgPSBUUlVFLCBzZWxlY3RlZEJ5ID0gImxhYmVsIikgJT4lCiAgdmlzTGF5b3V0KHJhbmRvbVNlZWQgPSAxMSkKYGBgCgoKIyMjIyAzLjMgVmlzdWFsaXplIGxvY2FsIGNvbmNlcHQgbmVpZ2hib3VyaG9vZHM6IGBBcHBsZWAKCmBgYHtyIGVjaG8gPSBULCBldmFsID0gVH0KIyAtIGxvY2F0ZSAiQXBwbGUiCnRlcm0gPC0gImFwcGwiCiMgLSBzaXplIG9mIHRoZSBuZWlnaGJvdXJob29kCm5fc2l6ZSA8LSA1CndUZXJtIDwtIHdoaWNoKGdyZXBsKHRlcm0sIHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KSkpCiMgLSBmZXRjaCBuZWlnaGJvdXJob29kCm5nMSA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gbGVuZ3RoKHdUZXJtKSkKZm9yIChpIGluIDE6bGVuZ3RoKHdUZXJtKSkgewogIG5nMVtbaV1dIDwtIG5hbWVzKAogICAgc29ydChjb25jZXB0RGlzdE1hdFt3VGVybVtpXSwgXSwgZGVjcmVhc2luZyA9IEYpWzE6bl9zaXplKzFdCiAgKQp9Cm5hbWVzKG5nMSkgPC0gcm93bmFtZXMoY29uY2VwdERpc3RNYXQpW3dUZXJtXQpmb3IgKGkgaW4gMTpsZW5ndGgobmcxKSkgewogIG5nMVtbaV1dIDwtIHNldGRpZmYobmcxW1tpXV0sIG5hbWVzKG5nMSlbaV0pCn0KbmcyIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSBzdW0oc2FwcGx5KG5nMSwgbGVuZ3RoKSkpCmMgPC0gMApmb3IgKGkgaW4gMTpsZW5ndGgobmcxKSkgewogIGZvciAoaiBpbiAxOmxlbmd0aChuZzFbW2ldXSkpIHsKICAgIGMgPC0gYyArIDEKICAgIG5nMltbY11dIDwtIG5hbWVzKAogICAgc29ydCgKICAgICAgY29uY2VwdERpc3RNYXRbd2hpY2gocm93bmFtZXMoY29uY2VwdERpc3RNYXQpID09IG5nMVtbaV1dW2pdKSwgXSwgCiAgICAgIGRlY3JlYXNpbmcgPSBGKVsxOm5fc2l6ZSsxXQogICAgKQogIH0KfQpuYW1lcyhuZzIpIDwtIHVubmFtZSh1bmxpc3QobmcxKSkKZm9yIChpIGluIDE6bGVuZ3RoKG5nMikpIHsKICBuZzJbW2ldXSA8LSBzZXRkaWZmKG5nMltbaV1dLCBuYW1lcyhuZzIpW2ldKQp9CmdyYXBoRGF0YSA8LSByYmluZChzdGFjayhuZzEpLCBzdGFjayhuZzIpKQpncmFwaERhdGEkaW5kIDwtIGFzLmNoYXJhY3RlcihncmFwaERhdGEkaW5kKQpncmFwaERhdGEgPC0gZ3JhcGhEYXRhWywgYygyLCAxKV0KY29sbmFtZXMoZ3JhcGhEYXRhKSA8LSBjKCdvdXRnb2luZycsICdpbmNvbWluZycpCmdyYXBoRGF0YSRpbmNvbWluZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRpbmNvbWluZyldIDwtIAogIHRvdXBwZXIoZ3JhcGhEYXRhJGluY29taW5nW2dyZXBsKCJed2RfIiwgZ3JhcGhEYXRhJGluY29taW5nKV0pCmdyYXBoRGF0YSRvdXRnb2luZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRvdXRnb2luZyldIDwtIAogIHRvdXBwZXIoZ3JhcGhEYXRhJG91dGdvaW5nW2dyZXBsKCJed2RfIiwgZ3JhcGhEYXRhJG91dGdvaW5nKV0pCgojIC0gdmlzdWFsaXplIHcuIHt2aXNOZXR3b3JrfQpub2RlcyA8LSB1bmlxdWUoYyhncmFwaERhdGEkb3V0Z29pbmcsIGdyYXBoRGF0YSRpbmNvbWluZykpCm5vZGVzIDwtIGRhdGEuZnJhbWUoaWQgPSAxOmxlbmd0aChub2RlcyksCiAgICAgICAgICAgICAgICAgICAgbGFiZWwgPSBub2RlcywKICAgICAgICAgICAgICAgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gRikKZWRnZXMgPC0gZ3JhcGhEYXRhCmNvbG5hbWVzKGVkZ2VzKSA8LSBjKCJmcm9tIiwgInRvIikKZWRnZXMkZnJvbSA8LSBzYXBwbHkoZWRnZXMkZnJvbSwgZnVuY3Rpb24oeCkgewogIG5vZGVzJGlkW3doaWNoKG5vZGVzJGxhYmVsID09IHgpXQp9KQplZGdlcyR0byA8LSBzYXBwbHkoZWRnZXMkdG8sIGZ1bmN0aW9uKHgpIHsKICBub2RlcyRpZFt3aGljaChub2RlcyRsYWJlbCA9PSB4KV0KfSkKCiMgLSB2aXN1YWxpemUKdmlzTmV0d29yayhub2RlcywgZWRnZXMsIHdpZHRoID0gIjEwMCUiKSAlPiUKICB2aXNJZ3JhcGhMYXlvdXQoKSAlPiUKICB2aXNOb2RlcygKICAgIHNoYXBlID0gImRvdCIsCiAgICBjb2xvciA9IGxpc3QoCiAgICAgIGJhY2tncm91bmQgPSAiIzAwODVBRiIsCiAgICAgIGJvcmRlciA9ICIjMDEzODQ4IiwKICAgICAgaGlnaGxpZ2h0ID0gIiNGRjgwMDAiCiAgICApLAogICAgc2hhZG93ID0gbGlzdChlbmFibGVkID0gVFJVRSwgc2l6ZSA9IDEwKQogICkgJT4lCiAgdmlzRWRnZXMoCiAgICBzaGFkb3cgPSBGQUxTRSwKICAgIGNvbG9yID0gbGlzdChjb2xvciA9ICIjMDA4NUFGIiwgaGlnaGxpZ2h0ID0gIiNDNjJGNEIiKQogICkgJT4lCiAgdmlzT3B0aW9ucyhoaWdobGlnaHROZWFyZXN0ID0gVFJVRSwgc2VsZWN0ZWRCeSA9ICJsYWJlbCIpICU+JQogIHZpc0xheW91dChyYW5kb21TZWVkID0gMTEpCmBgYAoKIyMjIyAzLjQgVmlzdWFsaXplIGxvY2FsIGNvbmNlcHQgbmVpZ2hib3VyaG9vZHM6IGBBbWF6b25gCgpgYGB7ciBlY2hvID0gVCwgZXZhbCA9IFR9CiMgLSBsb2NhdGUgIkFtYXpvbiIKdGVybSA8LSAiYW1hem9uIgojIC0gc2l6ZSBvZiB0aGUgbmVpZ2hib3VyaG9vZApuX3NpemUgPC0gNQp3VGVybSA8LSB3aGljaChncmVwbCh0ZXJtLCByb3duYW1lcyhjb25jZXB0RGlzdE1hdCkpKQojIC0gZmV0Y2ggbmVpZ2hib3VyaG9vZApuZzEgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IGxlbmd0aCh3VGVybSkpCmZvciAoaSBpbiAxOmxlbmd0aCh3VGVybSkpIHsKICBuZzFbW2ldXSA8LSBuYW1lcygKICAgIHNvcnQoY29uY2VwdERpc3RNYXRbd1Rlcm1baV0sIF0sIGRlY3JlYXNpbmcgPSBGKVsxOm5fc2l6ZSsxXQogICkKfQpuYW1lcyhuZzEpIDwtIHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KVt3VGVybV0KZm9yIChpIGluIDE6bGVuZ3RoKG5nMSkpIHsKICBuZzFbW2ldXSA8LSBzZXRkaWZmKG5nMVtbaV1dLCBuYW1lcyhuZzEpW2ldKQp9Cm5nMiA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gc3VtKHNhcHBseShuZzEsIGxlbmd0aCkpKQpjIDwtIDAKZm9yIChpIGluIDE6bGVuZ3RoKG5nMSkpIHsKICBmb3IgKGogaW4gMTpsZW5ndGgobmcxW1tpXV0pKSB7CiAgICBjIDwtIGMgKyAxCiAgICBuZzJbW2NdXSA8LSBuYW1lcygKICAgIHNvcnQoCiAgICAgIGNvbmNlcHREaXN0TWF0W3doaWNoKHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KSA9PSBuZzFbW2ldXVtqXSksIF0sIAogICAgICBkZWNyZWFzaW5nID0gRilbMTpuX3NpemUrMV0KICAgICkKICB9Cn0KbmFtZXMobmcyKSA8LSB1bm5hbWUodW5saXN0KG5nMSkpCmZvciAoaSBpbiAxOmxlbmd0aChuZzIpKSB7CiAgbmcyW1tpXV0gPC0gc2V0ZGlmZihuZzJbW2ldXSwgbmFtZXMobmcyKVtpXSkKfQpncmFwaERhdGEgPC0gcmJpbmQoc3RhY2sobmcxKSwgc3RhY2sobmcyKSkKZ3JhcGhEYXRhJGluZCA8LSBhcy5jaGFyYWN0ZXIoZ3JhcGhEYXRhJGluZCkKZ3JhcGhEYXRhIDwtIGdyYXBoRGF0YVssIGMoMiwgMSldCmNvbG5hbWVzKGdyYXBoRGF0YSkgPC0gYygnb3V0Z29pbmcnLCAnaW5jb21pbmcnKQpncmFwaERhdGEkaW5jb21pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkaW5jb21pbmcpXSA8LSAKICB0b3VwcGVyKGdyYXBoRGF0YSRpbmNvbWluZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRpbmNvbWluZyldKQpncmFwaERhdGEkb3V0Z29pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkb3V0Z29pbmcpXSA8LSAKICB0b3VwcGVyKGdyYXBoRGF0YSRvdXRnb2luZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRvdXRnb2luZyldKQoKIyAtIHZpc3VhbGl6ZSB3LiB7dmlzTmV0d29ya30Kbm9kZXMgPC0gdW5pcXVlKGMoZ3JhcGhEYXRhJG91dGdvaW5nLCBncmFwaERhdGEkaW5jb21pbmcpKQpub2RlcyA8LSBkYXRhLmZyYW1lKGlkID0gMTpsZW5ndGgobm9kZXMpLAogICAgICAgICAgICAgICAgICAgIGxhYmVsID0gbm9kZXMsCiAgICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpCmVkZ2VzIDwtIGdyYXBoRGF0YQpjb2xuYW1lcyhlZGdlcykgPC0gYygiZnJvbSIsICJ0byIpCmVkZ2VzJGZyb20gPC0gc2FwcGx5KGVkZ2VzJGZyb20sIGZ1bmN0aW9uKHgpIHsKICBub2RlcyRpZFt3aGljaChub2RlcyRsYWJlbCA9PSB4KV0KfSkKZWRnZXMkdG8gPC0gc2FwcGx5KGVkZ2VzJHRvLCBmdW5jdGlvbih4KSB7CiAgbm9kZXMkaWRbd2hpY2gobm9kZXMkbGFiZWwgPT0geCldCn0pCgojIC0gdmlzdWFsaXplCnZpc05ldHdvcmsobm9kZXMsIGVkZ2VzLCB3aWR0aCA9ICIxMDAlIikgJT4lCiAgdmlzSWdyYXBoTGF5b3V0KCkgJT4lCiAgdmlzTm9kZXMoCiAgICBzaGFwZSA9ICJkb3QiLAogICAgY29sb3IgPSBsaXN0KAogICAgICBiYWNrZ3JvdW5kID0gIiMwMDg1QUYiLAogICAgICBib3JkZXIgPSAiIzAxMzg0OCIsCiAgICAgIGhpZ2hsaWdodCA9ICIjRkY4MDAwIgogICAgKSwKICAgIHNoYWRvdyA9IGxpc3QoZW5hYmxlZCA9IFRSVUUsIHNpemUgPSAxMCkKICApICU+JQogIHZpc0VkZ2VzKAogICAgc2hhZG93ID0gRkFMU0UsCiAgICBjb2xvciA9IGxpc3QoY29sb3IgPSAiIzAwODVBRiIsIGhpZ2hsaWdodCA9ICIjQzYyRjRCIikKICApICU+JQogIHZpc09wdGlvbnMoaGlnaGxpZ2h0TmVhcmVzdCA9IFRSVUUsIHNlbGVjdGVkQnkgPSAibGFiZWwiKSAlPiUKICB2aXNMYXlvdXQocmFuZG9tU2VlZCA9IDExKQpgYGAKCiMjIyMgMy41IFZpc3VhbGl6ZSBsb2NhbCBjb25jZXB0IG5laWdoYm91cmhvb2RzOiBgTWljcm9zb2Z0YAoKYGBge3IgZWNobyA9IFQsIGV2YWwgPSBUfQojIC0gbG9jYXRlICJNaWNyb3NvZnQiCnRlcm0gPC0gIm1pY3Jvc29mdCIKIyAtIHNpemUgb2YgdGhlIG5laWdoYm91cmhvb2QKbl9zaXplIDwtIDEwCndUZXJtIDwtIHdoaWNoKGdyZXBsKHRlcm0sIHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KSkpCiMgLSBmZXRjaCBuZWlnaGJvdXJob29kCm5nMSA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gbGVuZ3RoKHdUZXJtKSkKZm9yIChpIGluIDE6bGVuZ3RoKHdUZXJtKSkgewogIG5nMVtbaV1dIDwtIG5hbWVzKAogICAgc29ydChjb25jZXB0RGlzdE1hdFt3VGVybVtpXSwgXSwgZGVjcmVhc2luZyA9IEYpWzE6bl9zaXplKzFdCiAgKQp9Cm5hbWVzKG5nMSkgPC0gcm93bmFtZXMoY29uY2VwdERpc3RNYXQpW3dUZXJtXQpmb3IgKGkgaW4gMTpsZW5ndGgobmcxKSkgewogIG5nMVtbaV1dIDwtIHNldGRpZmYobmcxW1tpXV0sIG5hbWVzKG5nMSlbaV0pCn0KbmcyIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSBzdW0oc2FwcGx5KG5nMSwgbGVuZ3RoKSkpCmMgPC0gMApmb3IgKGkgaW4gMTpsZW5ndGgobmcxKSkgewogIGZvciAoaiBpbiAxOmxlbmd0aChuZzFbW2ldXSkpIHsKICAgIGMgPC0gYyArIDEKICAgIG5nMltbY11dIDwtIG5hbWVzKAogICAgc29ydCgKICAgICAgY29uY2VwdERpc3RNYXRbd2hpY2gocm93bmFtZXMoY29uY2VwdERpc3RNYXQpID09IG5nMVtbaV1dW2pdKSwgXSwgCiAgICAgIGRlY3JlYXNpbmcgPSBGKVsxOm5fc2l6ZSsxXQogICAgKQogIH0KfQpuYW1lcyhuZzIpIDwtIHVubmFtZSh1bmxpc3QobmcxKSkKZm9yIChpIGluIDE6bGVuZ3RoKG5nMikpIHsKICBuZzJbW2ldXSA8LSBzZXRkaWZmKG5nMltbaV1dLCBuYW1lcyhuZzIpW2ldKQp9CmdyYXBoRGF0YSA8LSByYmluZChzdGFjayhuZzEpLCBzdGFjayhuZzIpKQpncmFwaERhdGEkaW5kIDwtIGFzLmNoYXJhY3RlcihncmFwaERhdGEkaW5kKQpncmFwaERhdGEgPC0gZ3JhcGhEYXRhWywgYygyLCAxKV0KY29sbmFtZXMoZ3JhcGhEYXRhKSA8LSBjKCdvdXRnb2luZycsICdpbmNvbWluZycpCmdyYXBoRGF0YSRpbmNvbWluZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRpbmNvbWluZyldIDwtIAogIHRvdXBwZXIoZ3JhcGhEYXRhJGluY29taW5nW2dyZXBsKCJed2RfIiwgZ3JhcGhEYXRhJGluY29taW5nKV0pCmdyYXBoRGF0YSRvdXRnb2luZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRvdXRnb2luZyldIDwtIAogIHRvdXBwZXIoZ3JhcGhEYXRhJG91dGdvaW5nW2dyZXBsKCJed2RfIiwgZ3JhcGhEYXRhJG91dGdvaW5nKV0pCgojIC0gdmlzdWFsaXplIHcuIHt2aXNOZXR3b3JrfQpub2RlcyA8LSB1bmlxdWUoYyhncmFwaERhdGEkb3V0Z29pbmcsIGdyYXBoRGF0YSRpbmNvbWluZykpCm5vZGVzIDwtIGRhdGEuZnJhbWUoaWQgPSAxOmxlbmd0aChub2RlcyksCiAgICAgICAgICAgICAgICAgICAgbGFiZWwgPSBub2RlcywKICAgICAgICAgICAgICAgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gRikKZWRnZXMgPC0gZ3JhcGhEYXRhCmNvbG5hbWVzKGVkZ2VzKSA8LSBjKCJmcm9tIiwgInRvIikKZWRnZXMkZnJvbSA8LSBzYXBwbHkoZWRnZXMkZnJvbSwgZnVuY3Rpb24oeCkgewogIG5vZGVzJGlkW3doaWNoKG5vZGVzJGxhYmVsID09IHgpXQp9KQplZGdlcyR0byA8LSBzYXBwbHkoZWRnZXMkdG8sIGZ1bmN0aW9uKHgpIHsKICBub2RlcyRpZFt3aGljaChub2RlcyRsYWJlbCA9PSB4KV0KfSkKCiMgLSB2aXN1YWxpemUKdmlzTmV0d29yayhub2RlcywgZWRnZXMsIHdpZHRoID0gIjEwMCUiKSAlPiUKICB2aXNJZ3JhcGhMYXlvdXQoKSAlPiUKICB2aXNOb2RlcygKICAgIHNoYXBlID0gImRvdCIsCiAgICBjb2xvciA9IGxpc3QoCiAgICAgIGJhY2tncm91bmQgPSAiIzAwODVBRiIsCiAgICAgIGJvcmRlciA9ICIjMDEzODQ4IiwKICAgICAgaGlnaGxpZ2h0ID0gIiNGRjgwMDAiCiAgICApLAogICAgc2hhZG93ID0gbGlzdChlbmFibGVkID0gVFJVRSwgc2l6ZSA9IDEwKQogICkgJT4lCiAgdmlzRWRnZXMoCiAgICBzaGFkb3cgPSBGQUxTRSwKICAgIGNvbG9yID0gbGlzdChjb2xvciA9ICIjMDA4NUFGIiwgaGlnaGxpZ2h0ID0gIiNDNjJGNEIiKQogICkgJT4lCiAgdmlzT3B0aW9ucyhoaWdobGlnaHROZWFyZXN0ID0gVFJVRSwgc2VsZWN0ZWRCeSA9ICJsYWJlbCIpICU+JQogIHZpc0xheW91dChyYW5kb21TZWVkID0gMTEpCmBgYAoKIyMjIyAzLjYgVmlzdWFsaXplIGxvY2FsIGNvbmNlcHQgbmVpZ2hib3VyaG9vZHM6IGBGYWNlYm9va2AKCmBgYHtyIGVjaG8gPSBULCBldmFsID0gVH0KIyAtIGxvY2F0ZSAiRmFjZWJvb2siCnRlcm0gPC0gImZhY2Vib29rIgojIC0gc2l6ZSBvZiB0aGUgbmVpZ2hib3VyaG9vZApuX3NpemUgPC0gMwp3VGVybSA8LSB3aGljaChncmVwbCh0ZXJtLCByb3duYW1lcyhjb25jZXB0RGlzdE1hdCkpKQojIC0gZmV0Y2ggbmVpZ2hib3VyaG9vZApuZzEgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IGxlbmd0aCh3VGVybSkpCmZvciAoaSBpbiAxOmxlbmd0aCh3VGVybSkpIHsKICBuZzFbW2ldXSA8LSBuYW1lcygKICAgIHNvcnQoY29uY2VwdERpc3RNYXRbd1Rlcm1baV0sIF0sIGRlY3JlYXNpbmcgPSBGKVsxOm5fc2l6ZSsxXQogICkKfQpuYW1lcyhuZzEpIDwtIHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KVt3VGVybV0KZm9yIChpIGluIDE6bGVuZ3RoKG5nMSkpIHsKICBuZzFbW2ldXSA8LSBzZXRkaWZmKG5nMVtbaV1dLCBuYW1lcyhuZzEpW2ldKQp9Cm5nMiA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gc3VtKHNhcHBseShuZzEsIGxlbmd0aCkpKQpjIDwtIDAKZm9yIChpIGluIDE6bGVuZ3RoKG5nMSkpIHsKICBmb3IgKGogaW4gMTpsZW5ndGgobmcxW1tpXV0pKSB7CiAgICBjIDwtIGMgKyAxCiAgICBuZzJbW2NdXSA8LSBuYW1lcygKICAgIHNvcnQoCiAgICAgIGNvbmNlcHREaXN0TWF0W3doaWNoKHJvd25hbWVzKGNvbmNlcHREaXN0TWF0KSA9PSBuZzFbW2ldXVtqXSksIF0sIAogICAgICBkZWNyZWFzaW5nID0gRilbMTpuX3NpemUrMV0KICAgICkKICB9Cn0KbmFtZXMobmcyKSA8LSB1bm5hbWUodW5saXN0KG5nMSkpCmZvciAoaSBpbiAxOmxlbmd0aChuZzIpKSB7CiAgbmcyW1tpXV0gPC0gc2V0ZGlmZihuZzJbW2ldXSwgbmFtZXMobmcyKVtpXSkKfQpncmFwaERhdGEgPC0gcmJpbmQoc3RhY2sobmcxKSwgc3RhY2sobmcyKSkKZ3JhcGhEYXRhJGluZCA8LSBhcy5jaGFyYWN0ZXIoZ3JhcGhEYXRhJGluZCkKZ3JhcGhEYXRhIDwtIGdyYXBoRGF0YVssIGMoMiwgMSldCmNvbG5hbWVzKGdyYXBoRGF0YSkgPC0gYygnb3V0Z29pbmcnLCAnaW5jb21pbmcnKQpncmFwaERhdGEkaW5jb21pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkaW5jb21pbmcpXSA8LSAKICB0b3VwcGVyKGdyYXBoRGF0YSRpbmNvbWluZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRpbmNvbWluZyldKQpncmFwaERhdGEkb3V0Z29pbmdbZ3JlcGwoIl53ZF8iLCBncmFwaERhdGEkb3V0Z29pbmcpXSA8LSAKICB0b3VwcGVyKGdyYXBoRGF0YSRvdXRnb2luZ1tncmVwbCgiXndkXyIsIGdyYXBoRGF0YSRvdXRnb2luZyldKQoKIyAtIHZpc3VhbGl6ZSB3LiB7dmlzTmV0d29ya30Kbm9kZXMgPC0gdW5pcXVlKGMoZ3JhcGhEYXRhJG91dGdvaW5nLCBncmFwaERhdGEkaW5jb21pbmcpKQpub2RlcyA8LSBkYXRhLmZyYW1lKGlkID0gMTpsZW5ndGgobm9kZXMpLAogICAgICAgICAgICAgICAgICAgIGxhYmVsID0gbm9kZXMsCiAgICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpCmVkZ2VzIDwtIGdyYXBoRGF0YQpjb2xuYW1lcyhlZGdlcykgPC0gYygiZnJvbSIsICJ0byIpCmVkZ2VzJGZyb20gPC0gc2FwcGx5KGVkZ2VzJGZyb20sIGZ1bmN0aW9uKHgpIHsKICBub2RlcyRpZFt3aGljaChub2RlcyRsYWJlbCA9PSB4KV0KfSkKZWRnZXMkdG8gPC0gc2FwcGx5KGVkZ2VzJHRvLCBmdW5jdGlvbih4KSB7CiAgbm9kZXMkaWRbd2hpY2gobm9kZXMkbGFiZWwgPT0geCldCn0pCgojIC0gdmlzdWFsaXplCnZpc05ldHdvcmsobm9kZXMsIGVkZ2VzLCB3aWR0aCA9ICIxMDAlIikgJT4lCiAgdmlzSWdyYXBoTGF5b3V0KCkgJT4lCiAgdmlzTm9kZXMoCiAgICBzaGFwZSA9ICJkb3QiLAogICAgY29sb3IgPSBsaXN0KAogICAgICBiYWNrZ3JvdW5kID0gIiMwMDg1QUYiLAogICAgICBib3JkZXIgPSAiIzAxMzg0OCIsCiAgICAgIGhpZ2hsaWdodCA9ICIjRkY4MDAwIgogICAgKSwKICAgIHNoYWRvdyA9IGxpc3QoZW5hYmxlZCA9IFRSVUUsIHNpemUgPSAxMCkKICApICU+JQogIHZpc0VkZ2VzKAogICAgc2hhZG93ID0gRkFMU0UsCiAgICBjb2xvciA9IGxpc3QoY29sb3IgPSAiIzAwODVBRiIsIGhpZ2hsaWdodCA9ICIjQzYyRjRCIikKICApICU+JQogIHZpc09wdGlvbnMoaGlnaGxpZ2h0TmVhcmVzdCA9IFRSVUUsIHNlbGVjdGVkQnkgPSAibGFiZWwiKSAlPiUKICB2aXNMYXlvdXQocmFuZG9tU2VlZCA9IDExKQpgYGAKCioqKgpHb3JhbiBTLiBNaWxvdmFub3ZpxIcgJiBNaWtlIFBhZ2UKCkRhdGFLb2xla3RpdiwgMjAxOS4KCmNvbnRhY3Q6IGRhdGFrb2xla3RpdkBkYXRha29sZWt0aXYuY29tCgohW10oaW1nL0RLX0xvZ29fMTAwLnBuZykKCioqKgpMaWNlbnNlOiBbR1BMdjNdKGh0dHA6Ly93d3cuZ251Lm9yZy9saWNlbnNlcy9ncGwtMy4wLnR4dCkKVGhpcyBOb3RlYm9vayBpcyBmcmVlIHNvZnR3YXJlOiB5b3UgY2FuIHJlZGlzdHJpYnV0ZSBpdCBhbmQvb3IgbW9kaWZ5IGl0IHVuZGVyIHRoZSB0ZXJtcyBvZiB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgYXMgcHVibGlzaGVkIGJ5IHRoZSBGcmVlIFNvZnR3YXJlIEZvdW5kYXRpb24sIGVpdGhlciB2ZXJzaW9uIDMgb2YgdGhlIExpY2Vuc2UsIG9yIChhdCB5b3VyIG9wdGlvbikgYW55IGxhdGVyIHZlcnNpb24uClRoaXMgTm90ZWJvb2sgaXMgZGlzdHJpYnV0ZWQgaW4gdGhlIGhvcGUgdGhhdCBpdCB3aWxsIGJlIHVzZWZ1bCwgYnV0IFdJVEhPVVQgQU5ZIFdBUlJBTlRZOyB3aXRob3V0IGV2ZW4gdGhlIGltcGxpZWQgd2FycmFudHkgb2YgTUVSQ0hBTlRBQklMSVRZIG9yIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFLiAgU2VlIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBmb3IgbW9yZSBkZXRhaWxzLgpZb3Ugc2hvdWxkIGhhdmUgcmVjZWl2ZWQgYSBjb3B5IG9mIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBhbG9uZyB3aXRoIHRoaXMgTm90ZWJvb2suIElmIG5vdCwgc2VlIDxodHRwOi8vd3d3LmdudS5vcmcvbGljZW5zZXMvPi4KCioqKgoK