Setup R

The methods in this notebook require the topicmodels package in addition to the standard list. (A paper accompanied the release of this package; see Grun and Hornik (2011) topicmodels: An R Package for Fitting Topic Models, J of Statistical Software, 40)

require(tm)
Loading required package: tm
Loading required package: NLP
require(topicmodels)
Loading required package: topicmodels
require(stringr)
Loading required package: stringr
require(tidyverse)
Loading required package: tidyverse
Loading tidyverse: ggplot2
Loading tidyverse: tibble
Loading tidyverse: tidyr
Loading tidyverse: readr
Loading tidyverse: purrr
Loading tidyverse: dplyr
Conflicts with tidy packages ----------------------------------------------------------------------
annotate(): ggplot2, NLP
filter():   dplyr, stats
lag():      dplyr, stats
source("text_utils.R")

Simulation

You can learn a lot about what a topic model does by using it to simulate text, which we can then study using methods covered previously. Here’s a compact summary of the algorithm

Given α and K topics, generate each document in the simulated corpus as follows… 1. Draw topic proportions θ | α ∼ Dir(α) for the document These are probabilities for sampling the words in the next step. 2. For each word w_i in the document (a) Draw topic assignment z_i | θ ∼ Mult(θ) z_i indicates topic (b) Draw word w_i|z_i,β_{1:K} ∼ Mult(β_{z_i}) β_{z_i} is topic dist

To illustrate the role of the parameter alpha (\(\alpha\)) and how it affects the probabilities \(\theta\) for a document, the function rdirichlet (from \(\tt text\_utils.R\)) simulates these draws.

Each draw is a discrete probability distribution over the number of categories. The function has two arguments, alpha and the number of groups. Set the number of topics \(K = 10\). If alpha is small, then the probability is concentrated in one topic. In this case, documents will be nearly pure, with all words drawn from a single topic, or perhaps 2.

alpha  <- 0.05
K      <- 10    # number of topics
par(mfrow=c(2,2))
    plot(rdirichlet(alpha,K)); plot(rdirichlet(alpha,K))
    plot(rdirichlet(alpha,K)); plot(rdirichlet(alpha,K))

As alpha increases, the distribution over topics becomes diffuse.

alpha  <- 0.5
par(mfrow=c(2,2))
    plot(rdirichlet(alpha,K)); plot(rdirichlet(alpha,K))
    plot(rdirichlet(alpha,K)); plot(rdirichlet(alpha,K))

alpha  <- 5
par(mfrow=c(2,2))
    plot(rdirichlet(alpha,K)); plot(rdirichlet(alpha,K))
    plot(rdirichlet(alpha,K)); plot(rdirichlet(alpha,K))

Consequently, small values of \(\alpha\) imply documents are pure, drawn from very few topics, whereas larger values of \(\alpha\) indicate documents that are “blurry” and mix the topics.

Another important characteristic of the model is how distinct the topics are themselves. Do topics have overlapping words, or are they mutually exclusive? For these simulations, the constant \(\alpha_P\) controls this property. Small \(\alpha_P\) means essentially distinct topics, larger values imply more common words.

n.vocab <- 1000                     # size of vocabulary
P <-matrix(0,nrow=K,ncol=n.vocab)   # dist over words for each topic
alpha.P <- 0.05                     # small alpha implies less overlap  [ 0.05 0.10 ]
set.seed(6382)
for(i in 1:K) P[i,] <- rdirichlet(alpha.P,n.vocab)
P <- P[,order(colSums(P), decreasing=TRUE)]         # sort so common types are first
rowSums(P)                          # check that each sums to 1
 [1] 1 1 1 1 1 1 1 1 1 1

Here are some examples. (Using the square roots of the probabilities shows a bit more of the variation; without it there’s a blob near zero.)

par(mfrow=c(1,2))
    plot(P[1,], xlab="Vocabulary", ylab="Probability")  # topic dist
    plot(sqrt(P[1,]),sqrt(P[2,]),                       # disjoint if alpha.P = 0.01, some common if .1
        xlab=expression(sqrt("P"[1])),ylab=expression(sqrt("P"[2])))        

Now put labels on the “words”. Low entropy words are those that by-and-large appear in only one topic. Higher entropy words appear in several and don’t help much to identify the topic. (The function entropy is defined in \(\tt text\_utils.R\).)

ent <- apply(P,2,entropy)                   # calc entropy for each word
low.ent <- ent < 0.25                       # low entropy -> predictable
wordTypeTopic <- rep("Mix", ncol(P))
wordTypeTopic[low.ent] <- paste(apply(P[,low.ent],2,which.max))
#   check assignment
plot(ent, xlab="word types", ylab="entropy")
abline(h=0.25, col='gray')

wordTypeTopic[1:10]
 [1] "Mix" "Mix" "Mix" "Mix" "Mix" "Mix" "2"   "Mix" "10"  "10" 
round(P[,1:10],2)
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
 [1,] 0.05 0.00 0.07 0.01 0.05 0.01 0.00 0.00 0.00  0.00
 [2,] 0.02 0.01 0.00 0.00 0.00 0.00 0.08 0.00 0.00  0.00
 [3,] 0.12 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00  0.00
 [4,] 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00  0.00
 [5,] 0.00 0.00 0.00 0.10 0.00 0.00 0.00 0.00 0.00  0.00
 [6,] 0.00 0.00 0.00 0.00 0.05 0.00 0.00 0.00 0.00  0.00
 [7,] 0.00 0.00 0.04 0.00 0.00 0.00 0.00 0.00 0.00  0.00
 [8,] 0.00 0.11 0.00 0.00 0.00 0.00 0.00 0.00 0.00  0.00
 [9,] 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.01 0.00  0.00
[10,] 0.00 0.02 0.00 0.00 0.00 0.07 0.00 0.06 0.07  0.07

Now use these topic distributions to simulate documents from the topic model.

alpha <- 0.05                       # mix of topics within documents
n     <- 2000                       # number of documents
theta <- matrix(0,nrow=n,ncol=K)    # expected topic mix in each document
Z     <- matrix(0,nrow=n,ncol=K)    # realized words from each topic
avg.len <- 100                      # avg document length
set.seed(6783)
doc.len <- sort(rpois(n,avg.len), decreasing=TRUE)  # Poisson lengths
for(i in 1:n) {                     
    theta[i,] <- rdirichlet(alpha, K)       
    Z[i,] <- as.vector(rmultinom(1,doc.len[i],theta[i,])) # mix of topics
}

Plot the mix of topics in some documents.

plot(theta[1,], xlab="Topic", ylab="Share of Vocabulary", main="Topic Mix for One Document")

Compute the document-term matrix.

C <- matrix(0,nrow=n, ncol=n.vocab)  
C.ev <- C;                              # expected value of C
for(i in 1:n) { 
    C.ev[i,] <- doc.len[i] * theta[i,] %*% P # prob distribution over words for each doc        
    for(k in 1:K) if(Z[i,k]>0) C[i,]<-C[i,]+rmultinom(1,Z[i,k],P[k,])
}

Do we get something like a Zipf distribution? A bit more extreme concave shape.

mj <- colSums(C)
zipf_plot(mj)
44 y values <= 0 omitted from logarithmic plot

Call:
lm(formula = ly ~ lx, data = df[1:min(n.fit, nrow(df)), ])

Coefficients:
(Intercept)           lx  
     8.5047      -0.4912  

Let’s see what LSA thinks of the simulated document-term matrix, with CCA scaling. Does a plot of the singular vales from the wine data resemble this one?

mj <- colSums(C)        # freq of word types
ni <- rowSums(C)        # doc.len
    
C.cca  <-  C / sqrt(ni)
C.cca  <-  t(t(C) / sqrt(pmax(1,mj)))  # avoid zero divisor
udv.cca <- svd(C.cca)
U <- udv.cca$u  
V <- udv.cca$v
        
plot(udv.cca$d[1:100], log="xy", xlab="Component", ylab="Singular Value")

As in the LSA analysis of the wines, the leading singular vector is determined by the number of words in a document.

par(mfrow=c(1,2))
    plot(sqrt(ni),-udv.cca$u[,1], xlab=expression(sqrt("n"["i"])),ylab=expression("U"[1]))
    plot(sqrt(mj),-udv.cca$v[,1], xlab=expression(sqrt("m"["j"])),ylab=expression("V"[1]))

The other components are more interesting and different from those seen in the LSA of the wine ratings.

pairs(U[,2:5], 
      labels=c(expression("U"[2]), expression("U"[3]),  
               expression("U"[4]), expression("U"[5])))

j <- 2; k <- j+1;  
rownames(V) <- wordTypeTopic
plot_loadings(V, 2, 3, 0.07,cex=1.0)

plot_loadings(V, 4, 5, 0.07,cex=1.0)

Fitting topic models: simulated data

Let’s see how well methods from topicmodels recover the structure in the simulated topics. The software is simple to run. The code sometimes runs better if you are willing to tell it what to use for \(\alpha\) (the parameter that controls the purity of documents). You need to pick the number of topics. Set the seed argument to get reproducibility.

n.topics = 10
colnames(C) <- wordTypeTopic
lda <- LDA(C, n.topics, control = list(seed = 1234))
lda
A LDA_VEM topic model with 10 topics.

The properties of the estimated model are held in attributes of the lda object.

names(attributes(lda))
 [1] "control"         "alpha"           "call"            "Dim"             "k"              
 [6] "terms"           "documents"       "beta"            "gamma"           "wordassignments"
[11] "loglikelihood"   "iter"            "logLiks"         "n"               "class"          

It estimated the value of \(\alpha\) quite well (though we don’t have a standard error on that).

alpha
[1] 0.05
attributes(lda)$alpha
[1] 0.05355063

Beta holds logs of the probability distributions over the word types. The first row of beta, for example, estimates the distribution over types that go into the first topic. These provide estimates of the probability distributions \(P_k\). Notice that the indices are arbitrary: the estimated model may label \(P_1\) as the 5th topic.

beta <- attributes(lda)$beta
dim(beta)
[1]   10 1000
colnames(beta) <- attributes(lda)$terms   # the types

Here’s an example. Based on the words with higher probabililty, the first estimated probability distribution evidently estimates \(P_7\) and the second estimates \(P_3\).

p1 <- exp(beta[1,])
sum(p1)
[1] 1
sort(p1, decreasing=TRUE)[1:7]
       Mix        Mix          7          7          7        Mix        Mix 
0.05832417 0.05818662 0.05565437 0.04518013 0.03825313 0.03473340 0.03080214 
p2 <- exp(beta[2,])
sum(p2)
[1] 1
sort(p2, decreasing=TRUE)[1:7]
       Mix          3        Mix        Mix        Mix          3        Mix 
0.12531119 0.07178982 0.04802098 0.04178264 0.03468565 0.03186862 0.02593108 

Those are good estimates of the distributions.

par(mfrow=c(1,2))
    plot(p1, P[7,]); abline(a=0,b=1,col='gray')
    plot(p2, P[3,]); abline(a=0,b=1,col='gray')

The component gamma estimates the mix of topics in documents, the parameter denoted \(\theta\) in the simulation and slides. It’s easy to get confused with so many parameters, so use the shapes of the estimates to keep you on the right track. There has to be an dimension in gamma that goes with the number of rows of the data (rows of the DTM). We have 2000 documents and 10 topics – matching the shape of gamma.

theta.hat <- attributes(lda)$gamma
dim(theta.hat)              # mix of topics over documents
[1] 2000   10

When looking at these, remember that the first topic is “really” the seventh in the simulation (\(P_7\)), and that the second topic corresponds to \(P_3\).

round(theta.hat[1:10,],2)   # for first 10 documents
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
 [1,] 0.00 0.00 0.11 0.00 0.00 0.88 0.00 0.00 0.00  0.00
 [2,] 0.24 0.00 0.00 0.34 0.00 0.08 0.13 0.00 0.20  0.00
 [3,] 0.04 0.00 0.00 0.00 0.00 0.00 0.58 0.00 0.29  0.09
 [4,] 0.39 0.00 0.00 0.00 0.02 0.00 0.00 0.00 0.00  0.58
 [5,] 0.00 0.00 0.00 0.00 0.00 0.02 0.00 0.00 0.28  0.69
 [6,] 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.03 0.02  0.95
 [7,] 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00  0.00
 [8,] 0.00 0.01 0.00 0.00 0.03 0.00 0.00 0.00 0.00  0.96
 [9,] 0.59 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00  0.40
[10,] 0.42 0.00 0.00 0.33 0.01 0.23 0.00 0.00 0.00  0.00

How well do the topic assignments for these documents correspond to the simulated topics? To answer, consider document 7 in this table. The simulation parameters for this document show that its very pure, coming from the third topic – which is exactly what the topic model estimates. (Again, this is tricky. The second estimated topic is really the third true topic.)

round(theta[7,],3)
 [1] 0.000 0.000 0.996 0.004 0.000 0.000 0.000 0.000 0.000 0.000

Computing the matrix of correlations shows you which is which: look for a correlation near 1 in a row or column. For instance, \(P_1\) is obviously estimated in the 9th position, and \(P_2\) in the 8th. \(P_3\) is the second estimate, as found previously. \(P_4\) is less clear, but evidently estimated by the 7th – but neither \(P_4\) nor \(P_{10}\) are well determined.

r <- cor(theta, theta.hat)
rownames(r) <- paste("P",1:K)
colnames(r) <- paste("Est",1:K)
round(r,2)
     Est 1 Est 2 Est 3 Est 4 Est 5 Est 6 Est 7 Est 8 Est 9 Est 10
P 1  -0.12 -0.11 -0.11 -0.07 -0.12 -0.12 -0.17 -0.11  1.00  -0.13
P 2  -0.11 -0.10 -0.11 -0.06 -0.12 -0.11 -0.18  1.00 -0.10  -0.12
P 3  -0.10  1.00 -0.09 -0.03 -0.12 -0.09 -0.15 -0.10 -0.12  -0.10
P 4  -0.11 -0.11 -0.11 -0.07 -0.11 -0.12  0.70 -0.13 -0.11  -0.12
P 5  -0.10 -0.09  1.00 -0.06 -0.10 -0.10 -0.16 -0.11 -0.11  -0.12
P 6  -0.10 -0.10 -0.12 -0.08 -0.11 -0.13 -0.17 -0.13 -0.13   1.00
P 7   0.96 -0.09 -0.10  0.57 -0.10 -0.12 -0.17 -0.11 -0.12  -0.11
P 8  -0.11 -0.10 -0.10 -0.07 -0.11  1.00 -0.17 -0.11 -0.12  -0.13
P 9  -0.09 -0.12 -0.10 -0.06  1.00 -0.11 -0.16 -0.11 -0.12  -0.11
P 10 -0.11 -0.09 -0.11 -0.06 -0.10 -0.10  0.63 -0.11 -0.13  -0.10

The helper function topics shows the most likely topics in each document. You pick how many topics to see (but it does not show you the weights on these).

x <- topics(lda, 2)
dim(x)
[1]    2 2000
x[,1:10]

The helper terms shows the word types in each topic. Again, no weights. This function will be more useful when the word types are more interesting.

x <- terms(lda,5)
dim(x)
[1]  5 10
x
     Topic 1 Topic 2 Topic 3 Topic 4 Topic 5 Topic 6 Topic 7 Topic 8 Topic 9 Topic 10
[1,] "Mix"   "Mix"   "Mix"   "Mix"   "9"     "Mix"   "Mix"   "2"     "Mix"   "6"     
[2,] "Mix"   "3"     "5"     "Mix"   "Mix"   "8"     "10"    "Mix"   "1"     "6"     
[3,] "7"     "Mix"   "5"     "7"     "Mix"   "Mix"   "10"    "Mix"   "1"     "Mix"   
[4,] "7"     "Mix"   "5"     "Mix"   "Mix"   "Mix"   "Mix"   "Mix"   "Mix"   "6"     
[5,] "7"     "Mix"   "Mix"   "Mix"   "Mix"   "Mix"   "10"    "2"     "Mix"   "Mix"   
names(attributes(lda))
 [1] "control"         "alpha"           "call"            "Dim"             "k"              
 [6] "terms"           "documents"       "beta"            "gamma"           "wordassignments"
[11] "loglikelihood"   "iter"            "logLiks"         "n"               "class"          
ll <- attributes(lda)$loglikelihood
str(ll)
 num [1:2000] -639 -732 -755 -668 -669 ...

For this simulated example (in which the topics are well separated), topicmodels is able to separate them nicely.

How, though, would we know to estimate 10 topics. In practice, you don’t and have to fit several and compare the fits using the overall log-likelihood of the estimated model. The log-likelihood attribute gives you the contribution to the overall log-likelihood from each document. (You can use this sometimes to spot the odd case. None are particularly unusual here – because these are simulated.)

ll <- attributes(lda)$loglikelihood
length(ll)
[1] 2000
hist(ll)

Add these up, or just call the function logLik. The logLik function adds them up for you and gives a version of degrees of freedom which counts the size of the model.

sum(ll)
[1] -994639
logLik(lda)
'log Lik.' -994639 (df=10001)

To find the number of topics, compare the log-likelihoods for several fits with different numbers of topics. Negative log-likelihoods are analogous to residual sums of squares in regression. As in that situation (like \(R^2\)), the log-likelihood improves with model size/complexity. The package authors use cross-validation to pick the number of topics. (It was not clear how they did that, however.)

n.topics <- 5
lda5 <- LDA(C, n.topics, control = list(seed = 1234)) 
logLik(lda5)
'log Lik.' -1072540 (df=5001)
n.topics <- 8
lda8 <- LDA(C, n.topics, control = list(seed = 1234)) 
logLik(lda8)
'log Lik.' -1013195 (df=8001)
n.topics <- 9
lda9 <- LDA(C, n.topics, control = list(seed = 1234)) 
logLik(lda9)
'log Lik.' -994021.6 (df=9001)
n.topics <- 11
lda11 <- LDA(C, n.topics, control = list(seed = 1234)) 
logLik(lda11)
'log Lik.' -975162.9 (df=11001)

Fitting topic models: real data

Like LSA, topic models take a bag-of-words approach, so we begin by preparing the document-term matrix.

Wine <- read_csv("../data/Wine.csv", col_types = cols(alcohol = col_double()))
dim(Wine)
[1] 20508    14

This time remove the stop words.

WineCorpus <- Corpus(VectorSource(Wine$description))
replace <- content_transformer(function(text, from, to) str_replace_all(text, from, to))
toSpace <- content_transformer(function(text, pattern) str_replace_all(text, pattern, " "))
toLower <- content_transformer(function(text) tolower(text))
WineCorpus <- tm_map(WineCorpus, toLower)
WineCorpus <- tm_map(WineCorpus, replace, "wieght", "weight")
WineCorpus <- tm_map(WineCorpus, toSpace, '-|/|,|\\.')     # avoid run-in words
WineCorpus <- tm_map(WineCorpus, removePunctuation)
WineCorpus <- tm_map(WineCorpus, stripWhitespace)
WineCorpus <- tm_map(WineCorpus, removeWords, stopwords("english")) 

Now compute the document term matrix and the row ni and column mj marginal totals. This DTM is a little smaller that prior examples computed from the wine reviews, with fewer types (5,412) than in the LSA analysis.

dtm <- DocumentTermMatrix(WineCorpus)
dtm
<<DocumentTermMatrix (documents: 20508, terms: 5412)>>
Non-/sparse entries: 475966/110513330
Sparsity           : 100%
Maximal term length: 15
Weighting          : term frequency (tf)
ni <- rowSums(as.matrix(dtm))
mj <- colSums(as.matrix(dtm))
word.types <- names(mj)   # for convenience and clarity

The corpus consists of 490,734 tokens (compared to 607,335 tokens with those stop words included).

sum(as.matrix(dtm))
[1] 490734

Rare types remain and will be replaced by the symbol “OOV” as in the LSA analysis. Stopwords generally are far too common to be in this collection, so these counts are similar to the prior analysis with stopwords.

sum(mj==1)
[1] 1819
sum(mj==2)
[1] 656
sum(mj==3)
[1] 365

The following commands are the same as in the LSA analysis.

o <- order(mj, decreasing=TRUE)   # biggest to smallest
dtm <- dtm[,o]                    # permute the columns
mj <- mj[o]
dtm.oov <- dtm[,10 <= mj]
dtm.oov <- cbind(as.matrix(dtm.oov), rowSums(as.matrix(dtm[,mj < 10])))
names.oov  <- c(names(mj[10<=mj]), 'OOV')
mj.oov <- c(mj[10<=mj],sum(mj[mj<10]))
ni.oov <- ni                         
colnames(dtm.oov) <- names.oov
names(mj.oov) <- names.oov
dim(dtm.oov)
[1] 20508  1692

We need to tell the software how many topics to fit. Because the algoritm uses a randomized procedure to initialize estimates (such as for the topic distributions), set the seed to be able to reproduce the results. It may also be useful to set the parameter \(\alpha\) that controls the “document complexity”.

n.topics = 8  # Why 8?  Eight format nicely on the output!
lda <- LDA(dtm.oov, n.topics, control = list(seed = 1234))
lda
A LDA_VEM topic model with 8 topics.

This estimate seeems too large, but let’s see what we got. (Setting a smaller value of alpha seems a bit artificial and forces the software to fit a model it does not “like” – and didn’t seem to help much.)

attributes(lda)$alpha
[1] 49.71126

Use the functions terms and topics to obtain interpretive clues to what the topic model has found. This shows the top 8 word types in the topics.

terms(lda,10)

And topics gives the composition of the reviews, showing the leading topics in each document. Topic 10 seems to be the “everything” topic.

x <- t(topics(lda,3))
x[1:10,]

It would seem there are too many “common” words that have overwhelmed the topic modeling. One simple thing to do in this case is remove from the analysis words that show up in almost every document.

C <- as.matrix(dtm.oov)
dim(C)
[1] 20508  1692
prop <- colSums(0 < C)/nrow(C)
tibble(prop=prop) %>%
    ggplot(aes(prop)) + geom_histogram() + scale_x_log10(breaks=c(0.01,0.05,0.25,1))

sort(prop, decreasing=TRUE)[1:20]
   aromas    medium    finish     entry      body     fruit      full    bodied     leads 
0.9166179 0.8060269 0.5691437 0.4498732 0.4443632 0.4083772 0.3791691 0.3772674 0.3201677 
  acidity    fruity       OOV    palate       dry      fade  finishes   tannins    cherry 
0.3039789 0.2927638 0.2888141 0.2728691 0.2705286 0.2618490 0.2496587 0.2477082 0.2396626 
    apple    follow 
0.2253755 0.2106007 

Let’s try topic modeling without those that appear in more than 25% of the documents.

C <- C[,order(prop, decreasing=TRUE)]
C <- C[,-(1:15)]
colnames(C)[1:4]
[1] "finishes" "tannins"  "cherry"   "apple"   
n.topics = 8  
lda <- LDA(C, n.topics, control = list(seed = 1234))
lda
A LDA_VEM topic model with 8 topics.

The estimate of \(\alpha\) remains large, but let’s see what we got this time.

attributes(lda)$alpha
[1] 39.18128

Use the functions terms and topics to obtain interpretive clues to what the topic model has found. This shows the top 8 word types in the topics. Again, the topics are dominated by the common words.

terms(lda,10)
      Topic 1   Topic 2    Topic 3   Topic 4    Topic 5    Topic 6    Topic 7   Topic 8
 [1,] "ripe"    "cherry"   "cherry"  "spice"    "tannins"  "cherry"   "flavors" "apple"
 [2,] "dried"   "apple"    "yet"     "sweet"    "apple"    "tart"     "tannins" "good" 
 [3,] "cherry"  "finishes" "tart"    "finishes" "good"     "sweet"    "oak"     "oak"  
 [4,] "tart"    "follow"   "light"   "tangy"    "light"    "dryish"   "black"   "light"
 [5,] "berry"   "ripe"     "tannins" "berry"    "flavors"  "follow"   "dried"   "soft" 
 [6,] "oak"     "tangy"    "tangy"   "round"    "finishes" "tannins"  "earth"   "pear" 
 [7,] "pepper"  "tart"     "crisp"   "mineral"  "dried"    "finishes" "follow"  "ripe" 
 [8,] "yet"     "pepper"   "spice"   "soft"     "crisp"    "accented" "depth"   "spice"
 [9,] "flavors" "oak"      "lemon"   "flavors"  "oak"      "yet"      "pear"    "dried"
[10,] "apple"   "pear"     "supple"  "nice"     "rich"     "roasted"  "wine"    "lemon"

So I will get more aggressive and wipe out more of these common words.

prop <- colSums(0 < C)/nrow(C)
sum(0.10 < prop)
[1] 33
names(prop[0.10 < prop])
 [1] "finishes" "tannins"  "cherry"   "apple"    "follow"   "flavors"  "yet"      "tart"    
 [9] "dried"    "oak"      "spice"    "good"     "light"    "tangy"    "soft"     "ripe"    
[17] "supple"   "rich"     "round"    "black"    "sweet"    "nice"     "crisp"    "depth"   
[25] "notes"    "pear"     "wine"     "dryish"   "lemon"    "balanced" "earth"    "berry"   
[33] "citrus"  
C <- C[,-(1:33)]

Check to make sure I’ve not wipe out an entire document.

any(rowSums(C)==0)
[1] FALSE

Okay, now fit the topic model.

n.topics = 8  
lda <- LDA(C, n.topics, control = list(seed = 1234))
attributes(lda)$alpha
[1] 31.51749
terms(lda,10)
      Topic 1      Topic 2         Topic 3      Topic 4     Topic 5   Topic 6      Topic 7    
 [1,] "slightly"   "lively"        "pair"       "silky"     "pepper"  "peach"      "pepper"   
 [2,] "moderately" "concentration" "accented"   "spicy"     "earthy"  "silky"      "accented" 
 [3,] "lively"     "cedar"         "pepper"     "mineral"   "cedar"   "moderately" "vanilla"  
 [4,] "chocolate"  "earthy"        "tannin"     "custard"   "skin"    "touch"      "chocolate"
 [5,] "pie"        "tannin"        "long"       "melon"     "mineral" "spicy"      "baked"    
 [6,] "cedar"      "herbal"        "melon"      "baked"     "lively"  "apricot"    "roasted"  
 [7,] "long"       "nicely"        "moderately" "vanilla"   "creamy"  "accented"   "nut"      
 [8,] "red"        "varietal"      "firm"       "roasted"   "baked"   "peel"       "great"    
 [9,] "pineapple"  "touch"         "earthy"     "pineapple" "red"     "baked"      "delicate" 
[10,] "nut"        "drink"         "well"       "green"     "currant" "varietal"   "slightly" 
      Topic 8    
 [1,] "firm"     
 [2,] "chocolate"
 [3,] "spicy"    
 [4,] "accented" 
 [5,] "earthy"   
 [6,] "long"     
 [7,] "peel"     
 [8,] "pie"      
 [9,] "plum"     
[10,] "cedar"    

Perhaps better, but it seems there just aren’t very many well-separated topics in these reviews. So look for fewer, like 2.

n.topics = 2 
lda <- LDA(C, n.topics, control = list(seed = 1234))
attributes(lda)$alpha
[1] 39.9038
terms(lda,10)
      Topic 1      Topic 2        
 [1,] "spicy"      "concentration"
 [2,] "chocolate"  "lively"       
 [3,] "accented"   "earthy"       
 [4,] "baked"      "skin"         
 [5,] "moderately" "cedar"        
 [6,] "table"      "tannin"       
 [7,] "pepper"     "moderate"     
 [8,] "vanilla"    "mineral"      
 [9,] "silky"      "touch"        
[10,] "long"       "pepper"       

Perhaps we should not be too surprised that topic models don’t seem well matched to these data. Recall the differences between the spectrum of the simulated data (the number of topics was clear) to that of the wine data. There just is not a clearly defined set of separated topics.

LS0tCnRpdGxlOiAiVGV4dCBhcyBEYXRhOiBUb3BpYyBNb2RlbHMiCm91dHB1dDogaHRtbF9ub3RlYm9vawphdXRob3I6IFJvYmVydCBTdGluZQpkYXRlOiBKdWx5IDIwMTcKLS0tCgojIFNldHVwIFIKClRoZSBtZXRob2RzIGluIHRoaXMgbm90ZWJvb2sgcmVxdWlyZSB0aGUgYHRvcGljbW9kZWxzYCBwYWNrYWdlIGluIGFkZGl0aW9uIHRvIHRoZSBzdGFuZGFyZCBsaXN0LiAgKEEgcGFwZXIgYWNjb21wYW5pZWQgdGhlIHJlbGVhc2Ugb2YgdGhpcyBwYWNrYWdlOyBzZWUgR3J1biBhbmQgSG9ybmlrICgyMDExKSB0b3BpY21vZGVsczogQW4gUiBQYWNrYWdlIGZvciBGaXR0aW5nIFRvcGljIE1vZGVscywgSiBvZiBTdGF0aXN0aWNhbCBTb2Z0d2FyZSwgNDApCgpgYGB7cn0KcmVxdWlyZSh0bSkKcmVxdWlyZSh0b3BpY21vZGVscykKCnJlcXVpcmUoc3RyaW5ncikKcmVxdWlyZSh0aWR5dmVyc2UpCgpzb3VyY2UoInRleHRfdXRpbHMuUiIpCmBgYAoKCiMgU2ltdWxhdGlvbgoKWW91IGNhbiBsZWFybiBhIGxvdCBhYm91dCB3aGF0IGEgdG9waWMgbW9kZWwgZG9lcyBieSB1c2luZyBpdCB0byBzaW11bGF0ZSB0ZXh0LCB3aGljaCB3ZSBjYW4gdGhlbiBzdHVkeSB1c2luZyBtZXRob2RzIGNvdmVyZWQgcHJldmlvdXNseS4gIEhlcmUncyBhIGNvbXBhY3Qgc3VtbWFyeSBvZiB0aGUgYWxnb3JpdGhtCgpHaXZlbiDOsSBhbmQgSyB0b3BpY3MsIGdlbmVyYXRlIGVhY2ggZG9jdW1lbnQgaW4gdGhlIHNpbXVsYXRlZCBjb3JwdXMgYXMgZm9sbG93cy4uLgogICAgMS4gRHJhdyB0b3BpYyBwcm9wb3J0aW9ucyDOuCB8IM6xIOKIvCBEaXIozrEpIGZvciB0aGUgZG9jdW1lbnQKICAgICAgIFRoZXNlIGFyZSBwcm9iYWJpbGl0aWVzIGZvciBzYW1wbGluZyB0aGUgd29yZHMgaW4gdGhlIG5leHQgc3RlcC4KICAgIDIuIEZvciBlYWNoIHdvcmQgd19pIGluIHRoZSBkb2N1bWVudAoJCShhKSBEcmF3IHRvcGljIGFzc2lnbm1lbnQgel9pIHwgzrgg4oi8IE11bHQozrgpCQkgICB6X2kgaW5kaWNhdGVzIHRvcGljCgkJKGIpIERyYXcgd29yZCB3X2l8el9pLM6yX3sxOkt9IOKIvCBNdWx0KM6yX3t6X2l9KQkgICDOsl97el9pfSBpcyB0b3BpYyBkaXN0CQoKVG8gaWxsdXN0cmF0ZSB0aGUgcm9sZSBvZiB0aGUgcGFyYW1ldGVyIGFscGhhICgkXGFscGhhJCkgYW5kIGhvdyBpdCBhZmZlY3RzIHRoZSBwcm9iYWJpbGl0aWVzICRcdGhldGEkIGZvciBhIGRvY3VtZW50LCB0aGUgZnVuY3Rpb24gYHJkaXJpY2hsZXRgIChmcm9tICRcdHQgdGV4dFxfdXRpbHMuUiQpIHNpbXVsYXRlcyB0aGVzZSBkcmF3cy4gIAoKRWFjaCBkcmF3IGlzIGEgZGlzY3JldGUgcHJvYmFiaWxpdHkgZGlzdHJpYnV0aW9uIG92ZXIgdGhlIG51bWJlciBvZiBjYXRlZ29yaWVzLiAgVGhlIGZ1bmN0aW9uIGhhcyB0d28gYXJndW1lbnRzLCBhbHBoYSBhbmQgdGhlIG51bWJlciBvZiBncm91cHMuICBTZXQgdGhlIG51bWJlciBvZiB0b3BpY3MgJEsgPSAxMCQuICBJZiBhbHBoYSBpcyBzbWFsbCwgdGhlbiB0aGUgcHJvYmFiaWxpdHkgaXMgY29uY2VudHJhdGVkIGluIG9uZSB0b3BpYy4gIEluIHRoaXMgY2FzZSwgZG9jdW1lbnRzIHdpbGwgYmUgbmVhcmx5IHB1cmUsIHdpdGggYWxsIHdvcmRzIGRyYXduIGZyb20gYSBzaW5nbGUgdG9waWMsIG9yIHBlcmhhcHMgMi4KCmBgYHtyfQphbHBoYSAgPC0gMC4wNQpLICAgICAgPC0gMTAJIyBudW1iZXIgb2YgdG9waWNzCnBhcihtZnJvdz1jKDIsMikpCiAgICBwbG90KHJkaXJpY2hsZXQoYWxwaGEsSykpOyBwbG90KHJkaXJpY2hsZXQoYWxwaGEsSykpCiAgICBwbG90KHJkaXJpY2hsZXQoYWxwaGEsSykpOyBwbG90KHJkaXJpY2hsZXQoYWxwaGEsSykpCmBgYAoKQXMgYWxwaGEgaW5jcmVhc2VzLCB0aGUgZGlzdHJpYnV0aW9uIG92ZXIgdG9waWNzIGJlY29tZXMgZGlmZnVzZS4gIAoKYGBge3J9CmFscGhhICA8LSAwLjUKcGFyKG1mcm93PWMoMiwyKSkKICAgIHBsb3QocmRpcmljaGxldChhbHBoYSxLKSk7IHBsb3QocmRpcmljaGxldChhbHBoYSxLKSkKICAgIHBsb3QocmRpcmljaGxldChhbHBoYSxLKSk7IHBsb3QocmRpcmljaGxldChhbHBoYSxLKSkKYGBgCgpgYGB7cn0KYWxwaGEgIDwtIDUKcGFyKG1mcm93PWMoMiwyKSkKICAgIHBsb3QocmRpcmljaGxldChhbHBoYSxLKSk7IHBsb3QocmRpcmljaGxldChhbHBoYSxLKSkKICAgIHBsb3QocmRpcmljaGxldChhbHBoYSxLKSk7IHBsb3QocmRpcmljaGxldChhbHBoYSxLKSkKYGBgCgpDb25zZXF1ZW50bHksIHNtYWxsIHZhbHVlcyBvZiAkXGFscGhhJCBpbXBseSBkb2N1bWVudHMgYXJlIHB1cmUsIGRyYXduIGZyb20gdmVyeSBmZXcgdG9waWNzLCB3aGVyZWFzIGxhcmdlciB2YWx1ZXMgb2YgJFxhbHBoYSQgaW5kaWNhdGUgZG9jdW1lbnRzIHRoYXQgYXJlICJibHVycnkiIGFuZCBtaXggdGhlIHRvcGljcy4gCgpBbm90aGVyIGltcG9ydGFudCBjaGFyYWN0ZXJpc3RpYyBvZiB0aGUgbW9kZWwgaXMgaG93IGRpc3RpbmN0IHRoZSB0b3BpY3MgYXJlIHRoZW1zZWx2ZXMuICBEbyB0b3BpY3MgaGF2ZSBvdmVybGFwcGluZyB3b3Jkcywgb3IgYXJlIHRoZXkgbXV0dWFsbHkgZXhjbHVzaXZlPyAgRm9yIHRoZXNlIHNpbXVsYXRpb25zLCB0aGUgY29uc3RhbnQgJFxhbHBoYV9QJCBjb250cm9scyB0aGlzIHByb3BlcnR5LiAgU21hbGwgJFxhbHBoYV9QJCBtZWFucyBlc3NlbnRpYWxseSBkaXN0aW5jdCB0b3BpY3MsIGxhcmdlciB2YWx1ZXMgaW1wbHkgbW9yZSBjb21tb24gd29yZHMuCgpgYGB7cn0Kbi52b2NhYiA8LSAxMDAwIAkJICAgICAgICAgICAgIyBzaXplIG9mIHZvY2FidWxhcnkKUCA8LW1hdHJpeCgwLG5yb3c9SyxuY29sPW4udm9jYWIpICAgIyBkaXN0IG92ZXIgd29yZHMgZm9yIGVhY2ggdG9waWMKCmFscGhhLlAgPC0gMC4wNSAgIAkJCSAgICAgICAgIyBzbWFsbCBhbHBoYSBpbXBsaWVzIGxlc3Mgb3ZlcmxhcCAgWyAwLjA1IDAuMTAgXQpzZXQuc2VlZCg2MzgyKQpmb3IoaSBpbiAxOkspIFBbaSxdIDwtIHJkaXJpY2hsZXQoYWxwaGEuUCxuLnZvY2FiKQpQIDwtIFBbLG9yZGVyKGNvbFN1bXMoUCksIGRlY3JlYXNpbmc9VFJVRSldCQkgICAgIyBzb3J0IHNvIGNvbW1vbiB0eXBlcyBhcmUgZmlyc3QKCnJvd1N1bXMoUCkgICAgICAgICAgICAgICAgICAgICAgICAgICMgY2hlY2sgdGhhdCBlYWNoIHN1bXMgdG8gMQpgYGAKCkhlcmUgYXJlIHNvbWUgZXhhbXBsZXMuICAoVXNpbmcgdGhlIHNxdWFyZSByb290cyBvZiB0aGUgcHJvYmFiaWxpdGllcyBzaG93cyBhIGJpdCBtb3JlIG9mIHRoZSB2YXJpYXRpb247IHdpdGhvdXQgaXQgdGhlcmUncyBhIGJsb2IgbmVhciB6ZXJvLikKCmBgYHtyfQpwYXIobWZyb3c9YygxLDIpKQogICAgcGxvdChQWzEsXSwgeGxhYj0iVm9jYWJ1bGFyeSIsIHlsYWI9IlByb2JhYmlsaXR5IikJIyB0b3BpYyBkaXN0CiAgICBwbG90KHNxcnQoUFsxLF0pLHNxcnQoUFsyLF0pLAkJICAgICAgICAgICAgICAgICMgZGlzam9pbnQgaWYgYWxwaGEuUCA9IDAuMDEsIHNvbWUgY29tbW9uIGlmIC4xCgkJeGxhYj1leHByZXNzaW9uKHNxcnQoIlAiWzFdKSkseWxhYj1leHByZXNzaW9uKHNxcnQoIlAiWzJdKSkpICAgICAJCmBgYAoKTm93IHB1dCBsYWJlbHMgb24gdGhlICJ3b3JkcyIuICBMb3cgZW50cm9weSB3b3JkcyBhcmUgdGhvc2UgdGhhdCBieS1hbmQtbGFyZ2UgYXBwZWFyIGluIG9ubHkgb25lIHRvcGljLiAgSGlnaGVyIGVudHJvcHkgd29yZHMgYXBwZWFyIGluIHNldmVyYWwgYW5kIGRvbid0IGhlbHAgbXVjaCB0byBpZGVudGlmeSB0aGUgdG9waWMuICAoVGhlIGZ1bmN0aW9uIGBlbnRyb3B5YCBpcyBkZWZpbmVkIGluICRcdHQgdGV4dFxfdXRpbHMuUiQuKQoKYGBge3J9CmVudCA8LSBhcHBseShQLDIsZW50cm9weSkgICAgICAgICAgICAgICAgICAgIyBjYWxjIGVudHJvcHkgZm9yIGVhY2ggd29yZAoKbG93LmVudCA8LSBlbnQgPCAwLjI1CQkJCQkJIyBsb3cgZW50cm9weSAtPiBwcmVkaWN0YWJsZQp3b3JkVHlwZVRvcGljIDwtIHJlcCgiTWl4IiwgbmNvbChQKSkKd29yZFR5cGVUb3BpY1tsb3cuZW50XSA8LSBwYXN0ZShhcHBseShQWyxsb3cuZW50XSwyLHdoaWNoLm1heCkpCgojCWNoZWNrIGFzc2lnbm1lbnQKcGxvdChlbnQsIHhsYWI9IndvcmQgdHlwZXMiLCB5bGFiPSJlbnRyb3B5IikKYWJsaW5lKGg9MC4yNSwgY29sPSdncmF5JykKYGBgCgpgYGB7cn0Kd29yZFR5cGVUb3BpY1sxOjEwXQpyb3VuZChQWywxOjEwXSwyKQpgYGAKCk5vdyB1c2UgdGhlc2UgdG9waWMgZGlzdHJpYnV0aW9ucyB0byBzaW11bGF0ZSBkb2N1bWVudHMgZnJvbSB0aGUgdG9waWMgbW9kZWwuCgpgYGB7cn0KYWxwaGEgPC0gMC4wNQkJCQkgICAgICAgICMgbWl4IG9mIHRvcGljcyB3aXRoaW4gZG9jdW1lbnRzCm4gICAgIDwtIDIwMDAJCQkJCQkjIG51bWJlciBvZiBkb2N1bWVudHMKdGhldGEgPC0gbWF0cml4KDAsbnJvdz1uLG5jb2w9SykJIyBleHBlY3RlZCB0b3BpYyBtaXggaW4gZWFjaCBkb2N1bWVudApaCSAgPC0gbWF0cml4KDAsbnJvdz1uLG5jb2w9SykJIyByZWFsaXplZCB3b3JkcyBmcm9tIGVhY2ggdG9waWMKYXZnLmxlbiA8LSAxMDAJCQkgIAkJCSMgYXZnIGRvY3VtZW50IGxlbmd0aAoKc2V0LnNlZWQoNjc4MykKZG9jLmxlbiA8LSBzb3J0KHJwb2lzKG4sYXZnLmxlbiksIGRlY3JlYXNpbmc9VFJVRSkgICMgUG9pc3NvbiBsZW5ndGhzCmZvcihpIGluIDE6bikgewkJCQkJCQoJdGhldGFbaSxdIDwtIHJkaXJpY2hsZXQoYWxwaGEsIEspCQkKCVpbaSxdIDwtIGFzLnZlY3RvcihybXVsdGlub20oMSxkb2MubGVuW2ldLHRoZXRhW2ksXSkpICMgbWl4IG9mIHRvcGljcwp9CmBgYAoKUGxvdCB0aGUgbWl4IG9mIHRvcGljcyBpbiBzb21lIGRvY3VtZW50cy4KCmBgYHtyfQpwbG90KHRoZXRhWzEsXSwgeGxhYj0iVG9waWMiLCB5bGFiPSJTaGFyZSBvZiBWb2NhYnVsYXJ5IiwgbWFpbj0iVG9waWMgTWl4IGZvciBPbmUgRG9jdW1lbnQiKQpgYGAKCkNvbXB1dGUgdGhlIGRvY3VtZW50LXRlcm0gbWF0cml4LgoKYGBge3J9CkMgPC0gbWF0cml4KDAsbnJvdz1uLCBuY29sPW4udm9jYWIpCSAKQy5ldiA8LSBDOwkJCQkJCQkJIyBleHBlY3RlZCB2YWx1ZSBvZiBDCmZvcihpIGluIDE6bikgewkKCUMuZXZbaSxdIDwtIGRvYy5sZW5baV0gKiB0aGV0YVtpLF0gJSolIFAgIyBwcm9iIGRpc3RyaWJ1dGlvbiBvdmVyIHdvcmRzIGZvciBlYWNoIGRvYwkJCglmb3IoayBpbiAxOkspIGlmKFpbaSxrXT4wKSBDW2ksXTwtQ1tpLF0rcm11bHRpbm9tKDEsWltpLGtdLFBbayxdKQp9CmBgYAoKRG8gd2UgZ2V0IHNvbWV0aGluZyBsaWtlIGEgWmlwZiBkaXN0cmlidXRpb24/ICBBIGJpdCBtb3JlIGV4dHJlbWUgY29uY2F2ZSBzaGFwZS4KCmBgYHtyfQptaiA8LSBjb2xTdW1zKEMpCnppcGZfcGxvdChtaikKYGBgCgpMZXQncyBzZWUgd2hhdCBMU0EgdGhpbmtzIG9mIHRoZSBzaW11bGF0ZWQgZG9jdW1lbnQtdGVybSBtYXRyaXgsIHdpdGggQ0NBIHNjYWxpbmcuICBEb2VzIGEgcGxvdCBvZiB0aGUgc2luZ3VsYXIgdmFsZXMgZnJvbSB0aGUgd2luZSBkYXRhIHJlc2VtYmxlIHRoaXMgb25lPwoKYGBge3J9Cm1qIDwtIGNvbFN1bXMoQykJCSMgZnJlcSBvZiB3b3JkIHR5cGVzCm5pIDwtIHJvd1N1bXMoQykJCSMgZG9jLmxlbgoJCkMuY2NhICA8LSAgQyAvIHNxcnQobmkpCkMuY2NhICA8LSAgdCh0KEMpIC8gc3FydChwbWF4KDEsbWopKSkgICMgYXZvaWQgemVybyBkaXZpc29yCgp1ZHYuY2NhIDwtIHN2ZChDLmNjYSkKClUgPC0gdWR2LmNjYSR1CQpWIDwtIHVkdi5jY2EkdgoJCQpwbG90KHVkdi5jY2EkZFsxOjEwMF0sIGxvZz0ieHkiLCB4bGFiPSJDb21wb25lbnQiLCB5bGFiPSJTaW5ndWxhciBWYWx1ZSIpCmBgYAoKQXMgaW4gdGhlIExTQSBhbmFseXNpcyBvZiB0aGUgd2luZXMsIHRoZSBsZWFkaW5nIHNpbmd1bGFyIHZlY3RvciBpcyBkZXRlcm1pbmVkIGJ5IHRoZSBudW1iZXIgb2Ygd29yZHMgaW4gYSBkb2N1bWVudC4KCmBgYHtyfQpwYXIobWZyb3c9YygxLDIpKQogICAgcGxvdChzcXJ0KG5pKSwtdWR2LmNjYSR1WywxXSwgeGxhYj1leHByZXNzaW9uKHNxcnQoIm4iWyJpIl0pKSx5bGFiPWV4cHJlc3Npb24oIlUiWzFdKSkKCXBsb3Qoc3FydChtaiksLXVkdi5jY2EkdlssMV0sIHhsYWI9ZXhwcmVzc2lvbihzcXJ0KCJtIlsiaiJdKSkseWxhYj1leHByZXNzaW9uKCJWIlsxXSkpCmBgYAoKVGhlIG90aGVyIGNvbXBvbmVudHMgYXJlIG1vcmUgaW50ZXJlc3RpbmcgYW5kIGRpZmZlcmVudCBmcm9tIHRob3NlIHNlZW4gaW4gdGhlIExTQSBvZiB0aGUgd2luZSByYXRpbmdzLgoKYGBge3J9CnBhaXJzKFVbLDI6NV0sIAogICAgICBsYWJlbHM9YyhleHByZXNzaW9uKCJVIlsyXSksIGV4cHJlc3Npb24oIlUiWzNdKSwgCQogICAgICAgICAgICAgICBleHByZXNzaW9uKCJVIls0XSksIGV4cHJlc3Npb24oIlUiWzVdKSkpCmBgYAoKYGBge3J9CmogPC0gMjsgayA8LSBqKzE7ICAKCnJvd25hbWVzKFYpIDwtIHdvcmRUeXBlVG9waWMKCnBsb3RfbG9hZGluZ3MoViwgMiwgMywgMC4wNyxjZXg9MS4wKQpwbG90X2xvYWRpbmdzKFYsIDQsIDUsIDAuMDcsY2V4PTEuMCkKYGBgCgojIEZpdHRpbmcgdG9waWMgbW9kZWxzOiBzaW11bGF0ZWQgZGF0YQoKTGV0J3Mgc2VlIGhvdyB3ZWxsIG1ldGhvZHMgZnJvbSBgdG9waWNtb2RlbHNgIHJlY292ZXIgdGhlIHN0cnVjdHVyZSBpbiB0aGUgc2ltdWxhdGVkIHRvcGljcy4gVGhlIHNvZnR3YXJlIGlzIHNpbXBsZSB0byBydW4uICBUaGUgY29kZSBzb21ldGltZXMgcnVucyBiZXR0ZXIgaWYgeW91IGFyZSB3aWxsaW5nIHRvIHRlbGwgaXQgd2hhdCB0byB1c2UgZm9yICRcYWxwaGEkICh0aGUgcGFyYW1ldGVyIHRoYXQgY29udHJvbHMgdGhlIHB1cml0eSBvZiBkb2N1bWVudHMpLiAgWW91IG5lZWQgdG8gcGljayB0aGUgbnVtYmVyIG9mIHRvcGljcy4gIFNldCB0aGUgYHNlZWRgIGFyZ3VtZW50IHRvIGdldCByZXByb2R1Y2liaWxpdHkuCgpgYGB7cn0Kbi50b3BpY3MgPSAxMApjb2xuYW1lcyhDKSA8LSB3b3JkVHlwZVRvcGljCgpsZGEgPC0gTERBKEMsIG4udG9waWNzLCBjb250cm9sID0gbGlzdChzZWVkID0gMTIzNCkpICAjIHJhbmRvbSBpbml0aWFsaXphdGlvbgpsZGEKYGBgCgpUaGUgcHJvcGVydGllcyBvZiB0aGUgZXN0aW1hdGVkIG1vZGVsIGFyZSBoZWxkIGluIGBhdHRyaWJ1dGVzYCBvZiB0aGUgYGxkYWAgb2JqZWN0LgoKYGBge3J9Cm5hbWVzKGF0dHJpYnV0ZXMobGRhKSkKYGBgCgoKSXQgZXN0aW1hdGVkIHRoZSB2YWx1ZSBvZiAkXGFscGhhJCBxdWl0ZSB3ZWxsICh0aG91Z2ggd2UgZG9uJ3QgaGF2ZSBhIHN0YW5kYXJkIGVycm9yIG9uIHRoYXQpLgoKYGBge3J9CmFscGhhCmF0dHJpYnV0ZXMobGRhKSRhbHBoYQpgYGAKCkJldGEgaG9sZHMgKmxvZ3MqIG9mIHRoZSBwcm9iYWJpbGl0eSBkaXN0cmlidXRpb25zIG92ZXIgdGhlIHdvcmQgdHlwZXMuICBUaGUgZmlyc3Qgcm93IG9mIGJldGEsIGZvciBleGFtcGxlLCBlc3RpbWF0ZXMgdGhlIGRpc3RyaWJ1dGlvbiBvdmVyIHR5cGVzIHRoYXQgZ28gaW50byB0aGUgZmlyc3QgdG9waWMuICBUaGVzZSBwcm92aWRlIGVzdGltYXRlcyBvZiB0aGUgcHJvYmFiaWxpdHkgZGlzdHJpYnV0aW9ucyAkUF9rJC4gIE5vdGljZSB0aGF0IHRoZSBpbmRpY2VzIGFyZSBhcmJpdHJhcnk6IHRoZSBlc3RpbWF0ZWQgbW9kZWwgbWF5IGxhYmVsICRQXzEkIGFzIHRoZSA1dGggdG9waWMuCgpgYGB7cn0KYmV0YSA8LSBhdHRyaWJ1dGVzKGxkYSkkYmV0YQpkaW0oYmV0YSkKCmNvbG5hbWVzKGJldGEpIDwtIGF0dHJpYnV0ZXMobGRhKSR0ZXJtcyAgICMgdGhlIHR5cGVzCmBgYAoKSGVyZSdzIGFuIGV4YW1wbGUuICBCYXNlZCBvbiB0aGUgd29yZHMgd2l0aCBoaWdoZXIgcHJvYmFiaWxpbHR5LCB0aGUgZmlyc3QgZXN0aW1hdGVkIHByb2JhYmlsaXR5IGRpc3RyaWJ1dGlvbiBldmlkZW50bHkgZXN0aW1hdGVzICRQXzckIGFuZCB0aGUgc2Vjb25kIGVzdGltYXRlcyAkUF8zJC4KCmBgYHtyfQpwMSA8LSBleHAoYmV0YVsxLF0pCnN1bShwMSkKc29ydChwMSwgZGVjcmVhc2luZz1UUlVFKVsxOjddCmBgYAoKYGBge3J9CnAyIDwtIGV4cChiZXRhWzIsXSkKc3VtKHAyKQpzb3J0KHAyLCBkZWNyZWFzaW5nPVRSVUUpWzE6N10KYGBgCgpUaG9zZSBhcmUgZ29vZCBlc3RpbWF0ZXMgb2YgdGhlIGRpc3RyaWJ1dGlvbnMuCgpgYGB7cn0KcGFyKG1mcm93PWMoMSwyKSkKICAgIHBsb3QocDEsIFBbNyxdKTsgYWJsaW5lKGE9MCxiPTEsY29sPSdncmF5JykKICAgIHBsb3QocDIsIFBbMyxdKTsgYWJsaW5lKGE9MCxiPTEsY29sPSdncmF5JykKYGBgCgpUaGUgY29tcG9uZW50IGBnYW1tYWAgZXN0aW1hdGVzIHRoZSBtaXggb2YgdG9waWNzIGluIGRvY3VtZW50cywgdGhlIHBhcmFtZXRlciBkZW5vdGVkICRcdGhldGEkIGluIHRoZSBzaW11bGF0aW9uIGFuZCBzbGlkZXMuICBJdCdzIGVhc3kgdG8gZ2V0IGNvbmZ1c2VkIHdpdGggc28gbWFueSBwYXJhbWV0ZXJzLCBzbyB1c2UgdGhlIHNoYXBlcyBvZiB0aGUgZXN0aW1hdGVzIHRvIGtlZXAgeW91IG9uIHRoZSByaWdodCB0cmFjay4gIFRoZXJlIGhhcyB0byBiZSBhbiBkaW1lbnNpb24gaW4gYGdhbW1hYCB0aGF0IGdvZXMgd2l0aCB0aGUgbnVtYmVyIG9mIHJvd3Mgb2YgdGhlIGRhdGEgKHJvd3Mgb2YgdGhlIERUTSkuICBXZSBoYXZlIDIwMDAgZG9jdW1lbnRzIGFuZCAxMCB0b3BpY3MgLS0gbWF0Y2hpbmcgdGhlIHNoYXBlIG9mIGBnYW1tYWAuCgpgYGB7cn0KdGhldGEuaGF0IDwtIGF0dHJpYnV0ZXMobGRhKSRnYW1tYQpkaW0odGhldGEuaGF0KQkJCQkjIG1peCBvZiB0b3BpY3Mgb3ZlciBkb2N1bWVudHMKYGBgCgpXaGVuIGxvb2tpbmcgYXQgdGhlc2UsIHJlbWVtYmVyIHRoYXQgdGhlIGZpcnN0IHRvcGljIGlzICJyZWFsbHkiIHRoZSBzZXZlbnRoIGluIHRoZSBzaW11bGF0aW9uICgkUF83JCksIGFuZCB0aGF0IHRoZSBzZWNvbmQgdG9waWMgY29ycmVzcG9uZHMgdG8gJFBfMyQuCgpgYGB7cn0Kcm91bmQodGhldGEuaGF0WzE6MTAsXSwyKQkjIGZvciBmaXJzdCAxMCBkb2N1bWVudHMKYGBgCgpIb3cgd2VsbCBkbyB0aGUgdG9waWMgYXNzaWdubWVudHMgZm9yIHRoZXNlIGRvY3VtZW50cyBjb3JyZXNwb25kIHRvIHRoZSBzaW11bGF0ZWQgdG9waWNzPyBUbyBhbnN3ZXIsIGNvbnNpZGVyIGRvY3VtZW50IDcgaW4gdGhpcyB0YWJsZS4gIFRoZSBzaW11bGF0aW9uIHBhcmFtZXRlcnMgZm9yIHRoaXMgZG9jdW1lbnQgc2hvdyB0aGF0IGl0cyB2ZXJ5IHB1cmUsIGNvbWluZyBmcm9tIHRoZSB0aGlyZCB0b3BpYyAtLSB3aGljaCBpcyBleGFjdGx5IHdoYXQgdGhlIHRvcGljIG1vZGVsIGVzdGltYXRlcy4gIChBZ2FpbiwgdGhpcyBpcyB0cmlja3kuICBUaGUgc2Vjb25kIGVzdGltYXRlZCB0b3BpYyBpcyByZWFsbHkgdGhlIHRoaXJkIHRydWUgdG9waWMuKQoKYGBge3J9CnJvdW5kKHRoZXRhWzcsXSwzKQpgYGAKCkNvbXB1dGluZyB0aGUgbWF0cml4IG9mIGNvcnJlbGF0aW9ucyBzaG93cyB5b3Ugd2hpY2ggaXMgd2hpY2g6IGxvb2sgZm9yIGEgY29ycmVsYXRpb24gbmVhciAxIGluIGEgcm93IG9yIGNvbHVtbi4gRm9yIGluc3RhbmNlLCAkUF8xJCBpcyBvYnZpb3VzbHkgZXN0aW1hdGVkIGluIHRoZSA5dGggcG9zaXRpb24sIGFuZCAkUF8yJCBpbiB0aGUgOHRoLiAgJFBfMyQgaXMgdGhlIHNlY29uZCBlc3RpbWF0ZSwgYXMgZm91bmQgcHJldmlvdXNseS4gICRQXzQkIGlzIGxlc3MgY2xlYXIsIGJ1dCBldmlkZW50bHkgZXN0aW1hdGVkIGJ5IHRoZSA3dGggLS0gYnV0IG5laXRoZXIgJFBfNCQgbm9yICRQX3sxMH0kIGFyZSB3ZWxsIGRldGVybWluZWQuCgpgYGB7cn0KciA8LSBjb3IodGhldGEsIHRoZXRhLmhhdCkKcm93bmFtZXMocikgPC0gcGFzdGUoIlAiLDE6SykKY29sbmFtZXMocikgPC0gcGFzdGUoIkVzdCIsMTpLKQpyb3VuZChyLDIpCmBgYAoKVGhlIGhlbHBlciBmdW5jdGlvbiBgdG9waWNzYCBzaG93cyB0aGUgbW9zdCBsaWtlbHkgdG9waWNzIGluIGVhY2ggZG9jdW1lbnQuIFlvdSBwaWNrIGhvdyBtYW55IHRvcGljcyB0byBzZWUgKGJ1dCBpdCBkb2VzIG5vdCBzaG93IHlvdSB0aGUgd2VpZ2h0cyBvbiB0aGVzZSkuCgpgYGB7cn0KeCA8LSB0b3BpY3MobGRhLCAyKQpkaW0oeCkKYGBgCgpgYGB7cn0KeFssMToxMF0KYGBgCgpUaGUgaGVscGVyIGB0ZXJtc2Agc2hvd3MgdGhlIHdvcmQgdHlwZXMgaW4gZWFjaCB0b3BpYy4gIEFnYWluLCBubyB3ZWlnaHRzLiAgVGhpcyBmdW5jdGlvbiB3aWxsIGJlIG1vcmUgdXNlZnVsIHdoZW4gdGhlIHdvcmQgdHlwZXMgYXJlIG1vcmUgaW50ZXJlc3RpbmcuCgpgYGB7cn0KeCA8LSB0ZXJtcyhsZGEsNSkKZGltKHgpCmBgYApgYGB7cn0KeApgYGAKCgpgYGB7cn0KbmFtZXMoYXR0cmlidXRlcyhsZGEpKQpgYGAKCgpgYGB7cn0KbGwgPC0gYXR0cmlidXRlcyhsZGEpJGxvZ2xpa2VsaWhvb2QKc3RyKGxsKQpgYGAKCkZvciB0aGlzIHNpbXVsYXRlZCBleGFtcGxlIChpbiB3aGljaCB0aGUgdG9waWNzIGFyZSB3ZWxsIHNlcGFyYXRlZCksIGB0b3BpY21vZGVsc2AgaXMgYWJsZSB0byBzZXBhcmF0ZSB0aGVtIG5pY2VseS4gIAoKSG93LCB0aG91Z2gsIHdvdWxkIHdlIGtub3cgdG8gZXN0aW1hdGUgMTAgdG9waWNzLiAgSW4gcHJhY3RpY2UsIHlvdSBkb24ndCBhbmQgaGF2ZSB0byBmaXQgc2V2ZXJhbCBhbmQgY29tcGFyZSB0aGUgZml0cyB1c2luZyB0aGUgb3ZlcmFsbCBsb2ctbGlrZWxpaG9vZCBvZiB0aGUgZXN0aW1hdGVkIG1vZGVsLiAgVGhlIGxvZy1saWtlbGlob29kIGF0dHJpYnV0ZSBnaXZlcyB5b3UgdGhlIGNvbnRyaWJ1dGlvbiB0byB0aGUgb3ZlcmFsbCBsb2ctbGlrZWxpaG9vZCBmcm9tIGVhY2ggZG9jdW1lbnQuICAoWW91IGNhbiB1c2UgdGhpcyBzb21ldGltZXMgdG8gc3BvdCB0aGUgb2RkIGNhc2UuICBOb25lIGFyZSBwYXJ0aWN1bGFybHkgdW51c3VhbCBoZXJlIC0tIGJlY2F1c2UgdGhlc2UgYXJlIHNpbXVsYXRlZC4pCgpgYGB7cn0KbGwgPC0gYXR0cmlidXRlcyhsZGEpJGxvZ2xpa2VsaWhvb2QKbGVuZ3RoKGxsKQpgYGAKCmBgYHtyfQpoaXN0KGxsKQpgYGAKCkFkZCB0aGVzZSB1cCwgb3IganVzdCBjYWxsIHRoZSBmdW5jdGlvbiBgbG9nTGlrYC4gIFRoZSBgbG9nTGlrYCBmdW5jdGlvbiBhZGRzIHRoZW0gdXAgZm9yIHlvdSAqYW5kKiBnaXZlcyBhIHZlcnNpb24gb2YgZGVncmVlcyBvZiBmcmVlZG9tIHdoaWNoIGNvdW50cyB0aGUgc2l6ZSBvZiB0aGUgbW9kZWwuIAoKYGBge3J9CnN1bShsbCkKbG9nTGlrKGxkYSkKYGBgCgpUbyBmaW5kIHRoZSBudW1iZXIgb2YgdG9waWNzLCBjb21wYXJlIHRoZSBsb2ctbGlrZWxpaG9vZHMgZm9yIHNldmVyYWwgZml0cyB3aXRoIGRpZmZlcmVudCBudW1iZXJzIG9mIHRvcGljcy4gIE5lZ2F0aXZlIGxvZy1saWtlbGlob29kcyBhcmUgYW5hbG9nb3VzIHRvIHJlc2lkdWFsIHN1bXMgb2Ygc3F1YXJlcyBpbiByZWdyZXNzaW9uLiAgQXMgaW4gdGhhdCBzaXR1YXRpb24gKGxpa2UgJFJeMiQpLCB0aGUgbG9nLWxpa2VsaWhvb2QgaW1wcm92ZXMgd2l0aCBtb2RlbCBzaXplL2NvbXBsZXhpdHkuICBUaGUgcGFja2FnZSBhdXRob3JzIHVzZSBjcm9zcy12YWxpZGF0aW9uIHRvIHBpY2sgdGhlIG51bWJlciBvZiB0b3BpY3MuIChJdCB3YXMgbm90IGNsZWFyIGhvdyB0aGV5IGRpZCB0aGF0LCBob3dldmVyLikKCmBgYHtyfQpuLnRvcGljcyA8LSA1CmxkYTUgPC0gTERBKEMsIG4udG9waWNzLCBjb250cm9sID0gbGlzdChzZWVkID0gMTIzNCkpIApsb2dMaWsobGRhNSkKYGBgCgpgYGB7cn0Kbi50b3BpY3MgPC0gOApsZGE4IDwtIExEQShDLCBuLnRvcGljcywgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEyMzQpKSAKbG9nTGlrKGxkYTgpCmBgYApgYGB7cn0Kbi50b3BpY3MgPC0gOQpsZGE5IDwtIExEQShDLCBuLnRvcGljcywgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEyMzQpKSAKbG9nTGlrKGxkYTkpCmBgYApgYGB7cn0Kbi50b3BpY3MgPC0gMTEKbGRhMTEgPC0gTERBKEMsIG4udG9waWNzLCBjb250cm9sID0gbGlzdChzZWVkID0gMTIzNCkpIApsb2dMaWsobGRhMTEpCmBgYAoKCiMgRml0dGluZyB0b3BpYyBtb2RlbHM6IHJlYWwgZGF0YQoKTGlrZSBMU0EsIHRvcGljIG1vZGVscyB0YWtlIGEgYmFnLW9mLXdvcmRzIGFwcHJvYWNoLCBzbyB3ZSBiZWdpbiBieSBwcmVwYXJpbmcgdGhlIGRvY3VtZW50LXRlcm0gbWF0cml4LgoKYGBge3J9CldpbmUgPC0gcmVhZF9jc3YoIi4uL2RhdGEvV2luZS5jc3YiLCBjb2xfdHlwZXMgPSBjb2xzKGFsY29ob2wgPSBjb2xfZG91YmxlKCkpKQpkaW0oV2luZSkKYGBgCgpUaGlzIHRpbWUgcmVtb3ZlIHRoZSBzdG9wIHdvcmRzLgoKYGBge3J9CldpbmVDb3JwdXMgPC0gQ29ycHVzKFZlY3RvclNvdXJjZShXaW5lJGRlc2NyaXB0aW9uKSkKCnJlcGxhY2UgPC0gY29udGVudF90cmFuc2Zvcm1lcihmdW5jdGlvbih0ZXh0LCBmcm9tLCB0bykgc3RyX3JlcGxhY2VfYWxsKHRleHQsIGZyb20sIHRvKSkKdG9TcGFjZSA8LSBjb250ZW50X3RyYW5zZm9ybWVyKGZ1bmN0aW9uKHRleHQsIHBhdHRlcm4pIHN0cl9yZXBsYWNlX2FsbCh0ZXh0LCBwYXR0ZXJuLCAiICIpKQp0b0xvd2VyIDwtIGNvbnRlbnRfdHJhbnNmb3JtZXIoZnVuY3Rpb24odGV4dCkgdG9sb3dlcih0ZXh0KSkKCldpbmVDb3JwdXMgPC0gdG1fbWFwKFdpbmVDb3JwdXMsIHRvTG93ZXIpCldpbmVDb3JwdXMgPC0gdG1fbWFwKFdpbmVDb3JwdXMsIHJlcGxhY2UsICJ3aWVnaHQiLCAid2VpZ2h0IikKV2luZUNvcnB1cyA8LSB0bV9tYXAoV2luZUNvcnB1cywgdG9TcGFjZSwgJy18L3wsfFxcLicpICAgICAjIGF2b2lkIHJ1bi1pbiB3b3JkcwpXaW5lQ29ycHVzIDwtIHRtX21hcChXaW5lQ29ycHVzLCByZW1vdmVQdW5jdHVhdGlvbikKV2luZUNvcnB1cyA8LSB0bV9tYXAoV2luZUNvcnB1cywgc3RyaXBXaGl0ZXNwYWNlKQpXaW5lQ29ycHVzIDwtIHRtX21hcChXaW5lQ29ycHVzLCByZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpIApgYGAKCk5vdyBjb21wdXRlIHRoZSBkb2N1bWVudCB0ZXJtIG1hdHJpeCBhbmQgdGhlIHJvdyBgbmlgIGFuZCBjb2x1bW4gYG1qYCBtYXJnaW5hbCB0b3RhbHMuIFRoaXMgRFRNIGlzIGEgbGl0dGxlIHNtYWxsZXIgdGhhdCBwcmlvciBleGFtcGxlcyBjb21wdXRlZCBmcm9tIHRoZSB3aW5lIHJldmlld3MsIHdpdGggZmV3ZXIgdHlwZXMgKDUsNDEyKSB0aGFuIGluIHRoZSBMU0EgYW5hbHlzaXMuICAKCmBgYHtyfQpkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KFdpbmVDb3JwdXMpCmR0bQoKbmkgPC0gcm93U3Vtcyhhcy5tYXRyaXgoZHRtKSkKbWogPC0gY29sU3Vtcyhhcy5tYXRyaXgoZHRtKSkKCndvcmQudHlwZXMgPC0gbmFtZXMobWopICAgIyBmb3IgY29udmVuaWVuY2UgYW5kIGNsYXJpdHkKYGBgCgpUaGUgY29ycHVzIGNvbnNpc3RzIG9mIDQ5MCw3MzQgdG9rZW5zIChjb21wYXJlZCB0byA2MDcsMzM1IHRva2VucyB3aXRoIHRob3NlIHN0b3Agd29yZHMgaW5jbHVkZWQpLgoKYGBge3J9CnN1bShhcy5tYXRyaXgoZHRtKSkKYGBgCgpSYXJlIHR5cGVzIHJlbWFpbiBhbmQgd2lsbCBiZSByZXBsYWNlZCBieSB0aGUgc3ltYm9sICJPT1YiIGFzIGluIHRoZSBMU0EgYW5hbHlzaXMuICBTdG9wd29yZHMgZ2VuZXJhbGx5IGFyZSBmYXIgdG9vIGNvbW1vbiB0byBiZSBpbiB0aGlzIGNvbGxlY3Rpb24sIHNvIHRoZXNlIGNvdW50cyBhcmUgc2ltaWxhciB0byB0aGUgcHJpb3IgYW5hbHlzaXMgd2l0aCBzdG9wd29yZHMuCgpgYGB7cn0Kc3VtKG1qPT0xKQpzdW0obWo9PTIpCnN1bShtaj09MykKYGBgCgpUaGUgZm9sbG93aW5nIGNvbW1hbmRzIGFyZSB0aGUgc2FtZSBhcyBpbiB0aGUgTFNBIGFuYWx5c2lzLgoKYGBge3J9Cm8gPC0gb3JkZXIobWosIGRlY3JlYXNpbmc9VFJVRSkgICAjIGJpZ2dlc3QgdG8gc21hbGxlc3QKZHRtIDwtIGR0bVssb10gICAgICAgICAgICAgICAgICAgICMgcGVybXV0ZSB0aGUgY29sdW1ucwptaiA8LSBtaltvXQoKZHRtLm9vdiA8LSBkdG1bLDEwIDw9IG1qXQpkdG0ub292IDwtIGNiaW5kKGFzLm1hdHJpeChkdG0ub292KSwgcm93U3Vtcyhhcy5tYXRyaXgoZHRtWyxtaiA8IDEwXSkpKQpuYW1lcy5vb3YgIDwtIGMobmFtZXMobWpbMTA8PW1qXSksICdPT1YnKQoKbWoub292IDwtIGMobWpbMTA8PW1qXSxzdW0obWpbbWo8MTBdKSkKbmkub292IDwtIG5pICAgICAgICAgICAgICAgICAgICAgICAgIAoKY29sbmFtZXMoZHRtLm9vdikgPC0gbmFtZXMub292Cm5hbWVzKG1qLm9vdikgPC0gbmFtZXMub292CgpkaW0oZHRtLm9vdikKYGBgCgpXZSBuZWVkIHRvIHRlbGwgdGhlIHNvZnR3YXJlIGhvdyBtYW55IHRvcGljcyB0byBmaXQuICBCZWNhdXNlIHRoZSBhbGdvcml0bSB1c2VzIGEgcmFuZG9taXplZCBwcm9jZWR1cmUgdG8gaW5pdGlhbGl6ZSBlc3RpbWF0ZXMgKHN1Y2ggYXMgZm9yIHRoZSB0b3BpYyBkaXN0cmlidXRpb25zKSwgc2V0IHRoZSBzZWVkIHRvIGJlIGFibGUgdG8gcmVwcm9kdWNlIHRoZSByZXN1bHRzLiAgSXQgbWF5IGFsc28gYmUgdXNlZnVsIHRvIHNldCB0aGUgcGFyYW1ldGVyICRcYWxwaGEkIHRoYXQgY29udHJvbHMgdGhlICJkb2N1bWVudCBjb21wbGV4aXR5Ii4KCmBgYHtyfQpuLnRvcGljcyA9IDggICMgV2h5IDg/ICBFaWdodCBmb3JtYXQgbmljZWx5IG9uIHRoZSBvdXRwdXQhCgpsZGEgPC0gTERBKGR0bS5vb3YsIG4udG9waWNzLCBjb250cm9sID0gbGlzdChzZWVkID0gMTIzNCkpCiMgbGRhIDwtIExEQShkdG0ub292LCBuLnRvcGljcywgY29udHJvbCA9IGxpc3QoYWxwaGE9MC4xLCBzZWVkID0gMTIzNCkpICAjIHNldCBhbHBoYQoKbGRhCmBgYAoKVGhpcyBlc3RpbWF0ZSBzZWVlbXMgdG9vIGxhcmdlLCBidXQgbGV0J3Mgc2VlIHdoYXQgd2UgZ290LiAgKFNldHRpbmcgYSBzbWFsbGVyIHZhbHVlIG9mIGBhbHBoYWAgc2VlbXMgYSBiaXQgYXJ0aWZpY2lhbCBhbmQgZm9yY2VzIHRoZSBzb2Z0d2FyZSB0byBmaXQgYSBtb2RlbCBpdCBkb2VzIG5vdCAibGlrZSIgLS0gYW5kIGRpZG4ndCBzZWVtIHRvIGhlbHAgbXVjaC4pCgpgYGB7cn0KYXR0cmlidXRlcyhsZGEpJGFscGhhCmBgYAoKVXNlIHRoZSBmdW5jdGlvbnMgYHRlcm1zYCBhbmQgYHRvcGljc2AgdG8gb2J0YWluIGludGVycHJldGl2ZSBjbHVlcyB0byB3aGF0IHRoZSB0b3BpYyBtb2RlbCBoYXMgZm91bmQuIFRoaXMgc2hvd3MgdGhlIHRvcCA4IHdvcmQgdHlwZXMgaW4gdGhlIHRvcGljcy4KCmBgYHtyfQp0ZXJtcyhsZGEsMTApCmBgYAoKQW5kIGB0b3BpY3NgIGdpdmVzIHRoZSBjb21wb3NpdGlvbiBvZiB0aGUgcmV2aWV3cywgc2hvd2luZyB0aGUgbGVhZGluZyB0b3BpY3MgaW4gZWFjaCBkb2N1bWVudC4gIFRvcGljIDEwIHNlZW1zIHRvIGJlIHRoZSAiZXZlcnl0aGluZyIgdG9waWMuCgpgYGB7cn0KeCA8LSB0KHRvcGljcyhsZGEsMykpCnhbMToxMCxdCmBgYAoKSXQgd291bGQgc2VlbSB0aGVyZSBhcmUgdG9vIG1hbnkgImNvbW1vbiIgd29yZHMgdGhhdCBoYXZlIG92ZXJ3aGVsbWVkIHRoZSB0b3BpYyBtb2RlbGluZy4gIE9uZSBzaW1wbGUgdGhpbmcgdG8gZG8gaW4gdGhpcyBjYXNlIGlzIHJlbW92ZSBmcm9tIHRoZSBhbmFseXNpcyB3b3JkcyB0aGF0IHNob3cgdXAgaW4gYWxtb3N0IGV2ZXJ5IGRvY3VtZW50LgoKYGBge3J9CkMgPC0gYXMubWF0cml4KGR0bS5vb3YpCmRpbShDKQpgYGAKCgpgYGB7cn0KcHJvcCA8LSBjb2xTdW1zKDAgPCBDKS9ucm93KEMpCnRpYmJsZShwcm9wPXByb3ApICU+JQogICAgZ2dwbG90KGFlcyhwcm9wKSkgKyBnZW9tX2hpc3RvZ3JhbSgpICsgc2NhbGVfeF9sb2cxMChicmVha3M9YygwLjAxLDAuMDUsMC4yNSwxKSkKYGBgCgpgYGB7cn0Kc29ydChwcm9wLCBkZWNyZWFzaW5nPVRSVUUpWzE6MjBdCmBgYAoKTGV0J3MgdHJ5IHRvcGljIG1vZGVsaW5nIHdpdGhvdXQgdGhvc2UgdGhhdCBhcHBlYXIgaW4gbW9yZSB0aGFuIDI1JSBvZiB0aGUgZG9jdW1lbnRzLgoKYGBge3J9CkMgPC0gQ1ssb3JkZXIocHJvcCwgZGVjcmVhc2luZz1UUlVFKV0KQyA8LSBDWywtKDE6MTUpXQpjb2xuYW1lcyhDKVsxOjRdCmBgYAoKYGBge3J9Cm4udG9waWNzID0gOCAgCgpsZGEgPC0gTERBKEMsIG4udG9waWNzLCBjb250cm9sID0gbGlzdChzZWVkID0gMTIzNCkpCmxkYQpgYGAKClRoZSBlc3RpbWF0ZSBvZiAkXGFscGhhJCByZW1haW5zIGxhcmdlLCBidXQgbGV0J3Mgc2VlIHdoYXQgd2UgZ290IHRoaXMgdGltZS4KCmBgYHtyfQphdHRyaWJ1dGVzKGxkYSkkYWxwaGEKYGBgCgpVc2UgdGhlIGZ1bmN0aW9ucyBgdGVybXNgIGFuZCBgdG9waWNzYCB0byBvYnRhaW4gaW50ZXJwcmV0aXZlIGNsdWVzIHRvIHdoYXQgdGhlIHRvcGljIG1vZGVsIGhhcyBmb3VuZC4gVGhpcyBzaG93cyB0aGUgdG9wIDggd29yZCB0eXBlcyBpbiB0aGUgdG9waWNzLiAgQWdhaW4sIHRoZSB0b3BpY3MgYXJlIGRvbWluYXRlZCBieSB0aGUgY29tbW9uIHdvcmRzLgoKYGBge3J9CnRlcm1zKGxkYSwxMCkKYGBgCgpTbyBJIHdpbGwgZ2V0IG1vcmUgYWdncmVzc2l2ZSBhbmQgd2lwZSBvdXQgbW9yZSBvZiB0aGVzZSBjb21tb24gd29yZHMuCgpgYGB7cn0KcHJvcCA8LSBjb2xTdW1zKDAgPCBDKS9ucm93KEMpCnN1bSgwLjEwIDwgcHJvcCkKYGBgCmBgYHtyfQpuYW1lcyhwcm9wWzAuMTAgPCBwcm9wXSkKYGBgCgpgYGB7cn0KQyA8LSBDWywtKDE6MzMpXQpgYGAKCkNoZWNrIHRvIG1ha2Ugc3VyZSBJJ3ZlIG5vdCB3aXBlIG91dCBhbiBlbnRpcmUgZG9jdW1lbnQuCgpgYGB7cn0KYW55KHJvd1N1bXMoQyk9PTApCmBgYAoKT2theSwgbm93IGZpdCB0aGUgdG9waWMgbW9kZWwuCgpgYGB7cn0Kbi50b3BpY3MgPSA4ICAKCmxkYSA8LSBMREEoQywgbi50b3BpY3MsIGNvbnRyb2wgPSBsaXN0KHNlZWQgPSAxMjM0KSkKYXR0cmlidXRlcyhsZGEpJGFscGhhCmBgYAoKYGBge3J9CnRlcm1zKGxkYSwxMCkKYGBgCgpQZXJoYXBzIGJldHRlciwgYnV0IGl0IHNlZW1zIHRoZXJlIGp1c3QgYXJlbid0IHZlcnkgbWFueSB3ZWxsLXNlcGFyYXRlZCB0b3BpY3MgaW4gdGhlc2UgcmV2aWV3cy4gIFNvIGxvb2sgZm9yIGZld2VyLCBsaWtlIDIuCgpgYGB7cn0Kbi50b3BpY3MgPSAyIAoKbGRhIDwtIExEQShDLCBuLnRvcGljcywgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEyMzQpKQphdHRyaWJ1dGVzKGxkYSkkYWxwaGEKYGBgCgpgYGB7cn0KdGVybXMobGRhLDEwKQpgYGAKClBlcmhhcHMgd2Ugc2hvdWxkIG5vdCBiZSB0b28gc3VycHJpc2VkIHRoYXQgdG9waWMgbW9kZWxzIGRvbid0IHNlZW0gd2VsbCBtYXRjaGVkIHRvIHRoZXNlIGRhdGEuICBSZWNhbGwgdGhlIGRpZmZlcmVuY2VzIGJldHdlZW4gdGhlIHNwZWN0cnVtIG9mIHRoZSBzaW11bGF0ZWQgZGF0YSAodGhlIG51bWJlciBvZiB0b3BpY3Mgd2FzIGNsZWFyKSB0byB0aGF0IG9mIHRoZSB3aW5lIGRhdGEuICBUaGVyZSBqdXN0IGlzIG5vdCBhIGNsZWFybHkgZGVmaW5lZCBzZXQgb2Ygc2VwYXJhdGVkIHRvcGljcy4KCg==