Document Clustering

The video that accompanies this notebook is available at https://ucdavis.box.com/v/sts-205-notebook-5.

In this notebook we will be using hierarchical clustering to group the State of the Union Addresses into an arbitrary number of groups based on similarity in word usage.

Start by loading packages (and installing the tm package if you haven’t already done so), sourcing your functions, and building the sotu data frame.

library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ─────────────────────────────────────── tidyverse 1.3.0 ──
✓ ggplot2 3.3.3     ✓ purrr   0.3.4
✓ tibble  3.0.6     ✓ dplyr   1.0.4
✓ tidyr   1.1.2     ✓ stringr 1.4.0
✓ readr   1.4.0     ✓ forcats 0.5.0
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()
library(tidytext)
#install.packages("tm")
library(tm)
Loading required package: NLP

Attaching package: ‘NLP’

The following object is masked from ‘package:ggplot2’:

    annotate
source("functions.r")
sotu <- make_sotu()

── Column specification ────────────────────────────────────────────────────────
cols(
  year = col_double(),
  pres = col_character(),
  use_last = col_logical()
)

To get a sense of how document clustering works, we are going to start by identifying the number of times the words “united” and “america” were used in each SOTU address.

united_america <- sotu_tokenize_words() %>% filter(gram %in% c("united", "america")) %>% 
  group_by(year) %>% count(gram) %>% spread(gram, n) %>% 
  mutate(america = replace_na(america, 0), united = replace_na(united, 0))

Once we have done that, we can plot all of the addresses in two dimensions, according to the frequency with which they use each of these two words.

ggplot(united_america, aes(x = united, y = america)) + geom_point()

You can see that some addresses use “united” much more than “America” and some use “America” much more than “united”. We can use geom_label(), specifying label = year instead of geom_point() to get a sense of the time dimension.

ggplot(united_america, aes(x = united, y = america, label = year)) + geom_label()

Now we can calculate the Euclidean distance between each pair of documents (along the united and america dimensions) and hierarchically cluster them to identify any number of clusters of documents. We begin by converting the data frame from wide to narrow and then into a document-term matrix with the cast_dtm() function.

ua_dtm <- united_america %>% gather("word", "n", -year) %>% cast_dtm(year, word, n)

After that we can use the dist() function to calculate Euclidean distance between each pair of addresses and the hclust() function to cluster. Finally, we can use the plot() function to visualize the dendrogram of the cluster.

ua_cluster <- ua_dtm %>% dist() %>% hclust()
plot(ua_cluster)

The dendrogram shows how the SOTU addresses would be clustered into any number of groups, from 2 to 233. We can use the cutree() function to see which address would be in which cluster, for any given number of clusters. For example, we can select ten clusters, and then graph the addresses by frequency of “united” and “america”, with color corresponding to cluster.

cutree(ua_cluster, 10)
  1790 1790.5   1791   1792   1793   1794   1795   1796   1797   1798   1799 
     1      1      2      1      2      2      1      2      2      2      1 
  1800   1801   1802   1803   1804   1805   1806   1807   1808   1809   1810 
     1      1      1      1      1      1      1      1      1      1      2 
  1811   1812   1813   1814   1815   1816   1817   1818   1819   1820   1821 
     2      1      2      1      1      2      2      2      3      2      3 
  1822   1823   1824   1825   1826   1827   1828   1829   1830   1831   1832 
     2      2      2      2      4      3      3      2      3      2      2 
  1833   1834   1835   1836   1837   1838   1839   1840   1841   1842   1843 
     4      4      4      3      3      5      2      2      3      3      3 
  1844   1845   1846   1847   1848   1849   1850   1851   1852   1853   1854 
     3      6      7      5      4      3      2      4      3      3      4 
  1855   1856   1857   1858   1859   1860   1861   1862   1863   1864   1865 
     6      5      4      5      3      4      1      3      3      2      4 
  1866   1867   1868   1869   1870   1871   1872   1873   1874   1875   1876 
     3      2      3      4      5      3      3      4      3      5      4 
  1877   1878   1879   1880   1881   1882   1883   1884   1885   1886   1887 
     3      2      4      3      2      2      2      3      5      3      1 
  1888   1889   1890   1891   1892   1893   1894   1895   1896   1897   1898 
     2      3      3      5      4      4      4      5      3      4      7 
  1899   1900   1901   1902   1903   1904   1905   1906   1907   1908   1909 
     6      5      3      1      5      3      4      4      4      2      4 
  1910   1911   1912   1913   1914   1915   1916   1917   1918   1919   1920 
     4      5      7      1      1      1      1      1      1      1      1 
  1921   1922   1923   1924   1925   1926   1927   1928   1929   1930   1931 
     1      1      1      1      1      1      1      2      2      1      1 
  1932   1934   1935   1936   1937   1938   1939   1940   1941   1942   1943 
     1      1      1      2      1      2      1      2      1      2      8 
  1944   1945   1946   1947   1948   1949   1950   1951   1952   1953 1953.5 
     1      2      7      1      2      1      1      1      1      3      2 
  1954   1955   1956   1957   1958   1959   1960   1961 1961.5   1962   1963 
     2      2      8      2      1      1      2      2      1      8      1 
  1964   1965   1966   1967   1968   1969   1970   1971   1972   1973   1974 
     1      1      1      1      1      1      9     10     10      1     10 
  1975   1976   1977   1978   1979   1980   1981   1982   1983   1984   1985 
     1     10      1      1      1      8      6      1     10     10      1 
  1986   1987   1988   1989   1990   1991   1992   1993   1994   1995   1996 
    10     10     10     10     10      8      1      1      1     10     10 
  1997   1998   1999   2000   2001 2001.5   2002   2003   2004   2005   2006 
     9      9      9      9      1      8      9     10      9      1      9 
  2007   2008   2009   2010   2011   2012   2013   2014   2015   2016   2017 
    10      9     10     10     10      9     10      9      9     10     10 
  2018   2019 
    10      8 
united_america$cluster <- factor(cutree(ua_cluster, 10))
head(united_america)

ggplot(united_america, aes(x = united, y = america, color = cluster, label = year)) + geom_label()

As you see, the colors are grouped togethr, because we are using the same criteria to cluster documents as to graph them. We can write a for loop to graph any number of clusters.

for(i in 1:20) {
  united_america$cluster <- factor(cutree(ua_cluster, i))
  print(ggplot(united_america, aes(x = united, y = america, color = cluster, label = year)) + geom_label())
}

For this rather trivial clustering exercise, we grouped documents solely on the basis of the frequency of the words “united” and “America”. But we can take into account any number of words (or other features) when clustering.

Here we will cluster using the 1000 most frequent words across the whole corpus. Now we are clustering by distance in 1000-dimensional space.

#Identify the thousand most frequent words
top_thousand <- sotu_tokenize_words() %>% count(gram) %>% top_n(1000)
Selecting by n
#Make a document-term matrix of the thousand most frequent words
sotu_words_dtm <- sotu_tokenize_words() %>% filter(gram %in% top_thousand$gram) %>% 
                        group_by(year) %>% count(gram) %>% cast_dtm(year, gram, n) 

Once we have the distance matrix, we can plot it in two dimensions with multidimensional scaling (cmdscale()) just to see what it looks like in two dimensions. This is similar to the kind of plots produced with principal components analysis

sotu_words_dist <- dist(sotu_words_dtm)
plot(cmdscale(sotu_words_dist, k = 2))

As you can see, most of the addresses are very tightly clustered together, but two are very different from the others. Now we can cluster and plot the dendrogram.

sotu_words_cluster <- hclust(sotu_words_dist)
plot(sotu_words_cluster)

As you can see, two addresses (1946 and 1981) are very different from the others. These are the two we saw in the bottom right corner of the two-dimensional plot.

Now let’s write a function to color the labels on our two-dimensional plot according to any number of clusters from our hierarchical cluster.

plot_cluster <- function(nclust) {
  data.frame(cmdscale(sotu_words_dist, k = 2)) %>% 
    mutate(cluster = cutree(sotu_words_cluster, nclust), year = sotu$year) %>%
  ggplot(aes(x = X1, y = X2, color = factor(cluster), label = year)) + geom_label()
}
for(i in 2:20) {
  print(plot_cluster(i))
}

The scatterplot and the dendrogram provide more or less the same information, showing the distance between any two addresses and the clusters that they are assigned into at any level of hierarchy. But they don’t tell us anything about the contents of the clusters. The code below is a function that, for any number of clusters, identifies the top ten words by tf-idf. As you will see, instead of calculating tf-idf manually the way we did in Notebook 3, we are using the bind_tf_idf() function from the tidytext package.

#Function to identify the 10 most uniquely characteristic words of each cluster
cluster_words <- function(nclust) {
  #Add a column to sotu indicating which cluster an address is in (for nclust clusters)
  sotu %>% mutate(cluster = cutree(sotu_words_cluster, nclust)) %>%
    #Unnest tokens and keep only words in the top thousand
    unnest_tokens(gram, text) %>% filter(gram %in% top_thousand$gram) %>%
    #Calculate tf-idf for each word by cluster
    group_by(cluster) %>% count(gram) %>% bind_tf_idf(gram, cluster, n) %>%
    #Collapse list of words for each cluster into a single string
    group_by(cluster) %>% top_n(10, tf_idf) %>% summarize(words = str_c(gram, collapse = ", "))
}
cluster_words(3) 
cluster_words(50)

Now for any number of clusters, we can plot the clusters and see which words are distinctive of each one.

plot_cluster(4)

cluster_words(4)
plot_cluster(20)

cluster_words(20)

We don’t have to cluster on words. We can also use bigrams, trigrams, or any other unit of analysis. This time, let’s use the top 100 bigrams as our clustering features.

#Find top 100 bigrams
top_hundred <- sotu_tokenize_bigrams() %>% count(gram) %>% top_n(100)
Selecting by n
#Make document term matrix from top 100 bigrams
sotu_bigrams_dtm <- sotu_tokenize_bigrams() %>% filter(gram %in% top_hundred$gram) %>% 
                        group_by(year) %>% count(gram) %>% cast_dtm(year, gram, n) 
#Calculate Euclidean distances
sotu_bigrams_dist <- dist(sotu_bigrams_dtm)
#Plot on two dimensions
plot(cmdscale(sotu_bigrams_dist, k = 2))

#Cluster hierarchically
sotu_bigrams_cluster <- hclust(sotu_bigrams_dist)
#Plot dendrogram
plot(sotu_bigrams_cluster)

Write a function to plot and view the words for any number of clusters.

plot_and_view <- function(nclust) {
  graph <- data.frame(cmdscale(sotu_bigrams_dist, k = 2)) %>% 
    mutate(cluster = cutree(sotu_bigrams_cluster, nclust), year = sotu$year) %>% 
    ggplot(aes(x = X1, y = X2, color = factor(cluster), label = year)) + geom_label()
  dataframe <- sotu %>% mutate(cluster = cutree(sotu_bigrams_cluster, nclust)) %>%
    unnest_tokens(gram, text, token = "ngrams", n = 2) %>% filter(gram %in% top_hundred$gram) %>%
    group_by(cluster) %>% count(gram) %>% bind_tf_idf(gram, cluster, n) %>%
    group_by(cluster) %>% top_n(10, tf_idf) %>% summarize(bigrams = str_c(gram, collapse = ", "))
  print(graph)
  return(dataframe)
}
plot_and_view(10)

Clustering is a way of exploring your corpus. Different numbers of clusters and different clustering criteria will be more or less salient for different corpora. Remember that clustering is not magic. Documents are being grouped by the criteria you specify, so they can only show you patterns that you are looking for. Also remember that two documents can use almost the exact same words and still have very different meanings.

LS0tCnRpdGxlOiAiTm90ZWJvb2sgNSAtIERvY3VtZW50IENsdXN0ZXJpbmciCmF1dGhvcjogIkVtaWx5IEtsYW5jaGVyIE1lcmNoYW50IgpkYXRlOiAiU1RTIDIwNSIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKIyBEb2N1bWVudCBDbHVzdGVyaW5nClRoZSB2aWRlbyB0aGF0IGFjY29tcGFuaWVzIHRoaXMgbm90ZWJvb2sgaXMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vdWNkYXZpcy5ib3guY29tL3Yvc3RzLTIwNS1ub3RlYm9vay01LgoKSW4gdGhpcyBub3RlYm9vayB3ZSB3aWxsIGJlIHVzaW5nIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIHRvIGdyb3VwIHRoZSBTdGF0ZSBvZiB0aGUgVW5pb24gQWRkcmVzc2VzIGludG8gYW4gYXJiaXRyYXJ5IG51bWJlciBvZiBncm91cHMgYmFzZWQgb24gc2ltaWxhcml0eSBpbiB3b3JkIHVzYWdlLgoKU3RhcnQgYnkgbG9hZGluZyBwYWNrYWdlcyAoYW5kIGluc3RhbGxpbmcgdGhlIGB0bWAgcGFja2FnZSBpZiB5b3UgaGF2ZW4ndCBhbHJlYWR5IGRvbmUgc28pLCBzb3VyY2luZyB5b3VyIGZ1bmN0aW9ucywgYW5kIGJ1aWxkaW5nIHRoZSBgc290dWAgZGF0YSBmcmFtZS4KYGBge3J9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KHRpZHl0ZXh0KQojaW5zdGFsbC5wYWNrYWdlcygidG0iKQpsaWJyYXJ5KHRtKQpzb3VyY2UoImZ1bmN0aW9ucy5yIikKc290dSA8LSBtYWtlX3NvdHUoKQpgYGAKVG8gZ2V0IGEgc2Vuc2Ugb2YgaG93IGRvY3VtZW50IGNsdXN0ZXJpbmcgd29ya3MsIHdlIGFyZSBnb2luZyB0byBzdGFydCBieSBpZGVudGlmeWluZyB0aGUgbnVtYmVyIG9mIHRpbWVzIHRoZSB3b3JkcyAidW5pdGVkIiBhbmQgImFtZXJpY2EiIHdlcmUgdXNlZCBpbiBlYWNoIFNPVFUgYWRkcmVzcy4gCmBgYHtyfQp1bml0ZWRfYW1lcmljYSA8LSBzb3R1X3Rva2VuaXplX3dvcmRzKCkgJT4lIGZpbHRlcihncmFtICVpbiUgYygidW5pdGVkIiwgImFtZXJpY2EiKSkgJT4lIAogIGdyb3VwX2J5KHllYXIpICU+JSBjb3VudChncmFtKSAlPiUgc3ByZWFkKGdyYW0sIG4pICU+JSAKICBtdXRhdGUoYW1lcmljYSA9IHJlcGxhY2VfbmEoYW1lcmljYSwgMCksIHVuaXRlZCA9IHJlcGxhY2VfbmEodW5pdGVkLCAwKSkKYGBgCk9uY2Ugd2UgaGF2ZSBkb25lIHRoYXQsIHdlIGNhbiBwbG90IGFsbCBvZiB0aGUgYWRkcmVzc2VzIGluIHR3byBkaW1lbnNpb25zLCBhY2NvcmRpbmcgdG8gdGhlIGZyZXF1ZW5jeSB3aXRoIHdoaWNoIHRoZXkgdXNlIGVhY2ggb2YgdGhlc2UgdHdvIHdvcmRzLgpgYGB7cn0KZ2dwbG90KHVuaXRlZF9hbWVyaWNhLCBhZXMoeCA9IHVuaXRlZCwgeSA9IGFtZXJpY2EpKSArIGdlb21fcG9pbnQoKQpgYGAKWW91IGNhbiBzZWUgdGhhdCBzb21lIGFkZHJlc3NlcyB1c2UgInVuaXRlZCIgbXVjaCBtb3JlIHRoYW4gIkFtZXJpY2EiIGFuZCBzb21lIHVzZSAiQW1lcmljYSIgbXVjaCBtb3JlIHRoYW4gInVuaXRlZCIuIFdlIGNhbiB1c2UgYGdlb21fbGFiZWwoKWAsIHNwZWNpZnlpbmcgYGxhYmVsID0geWVhcmAgaW5zdGVhZCBvZiBgZ2VvbV9wb2ludCgpYCB0byBnZXQgYSBzZW5zZSBvZiB0aGUgdGltZSBkaW1lbnNpb24uCmBgYHtyfQpnZ3Bsb3QodW5pdGVkX2FtZXJpY2EsIGFlcyh4ID0gdW5pdGVkLCB5ID0gYW1lcmljYSwgbGFiZWwgPSB5ZWFyKSkgKyBnZW9tX2xhYmVsKCkKYGBgCgoKTm93IHdlIGNhbiBjYWxjdWxhdGUgdGhlICoqRXVjbGlkZWFuIGRpc3RhbmNlKiogYmV0d2VlbiBlYWNoIHBhaXIgb2YgZG9jdW1lbnRzIChhbG9uZyB0aGUgdW5pdGVkIGFuZCBhbWVyaWNhIGRpbWVuc2lvbnMpIGFuZCAqKmhpZXJhcmNoaWNhbGx5IGNsdXN0ZXIqKiB0aGVtIHRvIGlkZW50aWZ5IGFueSBudW1iZXIgb2YgY2x1c3RlcnMgb2YgZG9jdW1lbnRzLiBXZSBiZWdpbiBieSBjb252ZXJ0aW5nIHRoZSBkYXRhIGZyYW1lIGZyb20gd2lkZSB0byBuYXJyb3cgYW5kIHRoZW4gaW50byBhICoqZG9jdW1lbnQtdGVybSBtYXRyaXgqKiB3aXRoIHRoZSBgY2FzdF9kdG0oKWAgZnVuY3Rpb24uIApgYGB7cn0KdWFfZHRtIDwtIHVuaXRlZF9hbWVyaWNhICU+JSBnYXRoZXIoIndvcmQiLCAibiIsIC15ZWFyKSAlPiUgY2FzdF9kdG0oeWVhciwgd29yZCwgbikKYGBgCgpBZnRlciB0aGF0IHdlIGNhbiB1c2UgdGhlIGBkaXN0KClgIGZ1bmN0aW9uIHRvIGNhbGN1bGF0ZSBFdWNsaWRlYW4gZGlzdGFuY2UgYmV0d2VlbiBlYWNoIHBhaXIgb2YgYWRkcmVzc2VzIGFuZCB0aGUgYGhjbHVzdCgpYCBmdW5jdGlvbiB0byBjbHVzdGVyLiBGaW5hbGx5LCB3ZSBjYW4gdXNlIHRoZSBgcGxvdCgpYCBmdW5jdGlvbiB0byB2aXN1YWxpemUgdGhlIGRlbmRyb2dyYW0gb2YgdGhlIGNsdXN0ZXIuCmBgYHtyfQp1YV9jbHVzdGVyIDwtIHVhX2R0bSAlPiUgZGlzdCgpICU+JSBoY2x1c3QoKQpwbG90KHVhX2NsdXN0ZXIpCmBgYApUaGUgZGVuZHJvZ3JhbSBzaG93cyBob3cgdGhlIFNPVFUgYWRkcmVzc2VzIHdvdWxkIGJlIGNsdXN0ZXJlZCBpbnRvIGFueSBudW1iZXIgb2YgZ3JvdXBzLCBmcm9tIDIgdG8gMjMzLiBXZSBjYW4gdXNlIHRoZSBgY3V0cmVlKClgIGZ1bmN0aW9uIHRvIHNlZSB3aGljaCBhZGRyZXNzIHdvdWxkIGJlIGluIHdoaWNoIGNsdXN0ZXIsIGZvciBhbnkgZ2l2ZW4gbnVtYmVyIG9mIGNsdXN0ZXJzLiBGb3IgZXhhbXBsZSwgd2UgY2FuIHNlbGVjdCB0ZW4gY2x1c3RlcnMsIGFuZCB0aGVuIGdyYXBoIHRoZSBhZGRyZXNzZXMgYnkgZnJlcXVlbmN5IG9mICJ1bml0ZWQiIGFuZCAiYW1lcmljYSIsIHdpdGggY29sb3IgY29ycmVzcG9uZGluZyB0byBjbHVzdGVyLgpgYGB7cn0KY3V0cmVlKHVhX2NsdXN0ZXIsIDEwKQp1bml0ZWRfYW1lcmljYSRjbHVzdGVyIDwtIGZhY3RvcihjdXRyZWUodWFfY2x1c3RlciwgMTApKQpoZWFkKHVuaXRlZF9hbWVyaWNhKQoKZ2dwbG90KHVuaXRlZF9hbWVyaWNhLCBhZXMoeCA9IHVuaXRlZCwgeSA9IGFtZXJpY2EsIGNvbG9yID0gY2x1c3RlciwgbGFiZWwgPSB5ZWFyKSkgKyBnZW9tX2xhYmVsKCkKYGBgCkFzIHlvdSBzZWUsIHRoZSBjb2xvcnMgYXJlIGdyb3VwZWQgdG9nZXRociwgYmVjYXVzZSB3ZSBhcmUgdXNpbmcgdGhlIHNhbWUgY3JpdGVyaWEgdG8gY2x1c3RlciBkb2N1bWVudHMgYXMgdG8gZ3JhcGggdGhlbS4gV2UgY2FuIHdyaXRlIGEgZm9yIGxvb3AgdG8gZ3JhcGggYW55IG51bWJlciBvZiBjbHVzdGVycy4KYGBge3J9CmZvcihpIGluIDE6MjApIHsKICB1bml0ZWRfYW1lcmljYSRjbHVzdGVyIDwtIGZhY3RvcihjdXRyZWUodWFfY2x1c3RlciwgaSkpCiAgcHJpbnQoZ2dwbG90KHVuaXRlZF9hbWVyaWNhLCBhZXMoeCA9IHVuaXRlZCwgeSA9IGFtZXJpY2EsIGNvbG9yID0gY2x1c3RlciwgbGFiZWwgPSB5ZWFyKSkgKyBnZW9tX2xhYmVsKCkpCn0KYGBgCgpGb3IgdGhpcyByYXRoZXIgdHJpdmlhbCBjbHVzdGVyaW5nIGV4ZXJjaXNlLCB3ZSBncm91cGVkIGRvY3VtZW50cyBzb2xlbHkgb24gdGhlIGJhc2lzIG9mIHRoZSBmcmVxdWVuY3kgb2YgdGhlIHdvcmRzICJ1bml0ZWQiIGFuZCAiQW1lcmljYSIuIEJ1dCB3ZSBjYW4gdGFrZSBpbnRvIGFjY291bnQgYW55IG51bWJlciBvZiB3b3JkcyAob3Igb3RoZXIgZmVhdHVyZXMpIHdoZW4gY2x1c3RlcmluZy4gCgpIZXJlIHdlIHdpbGwgY2x1c3RlciB1c2luZyB0aGUgMTAwMCBtb3N0IGZyZXF1ZW50IHdvcmRzIGFjcm9zcyB0aGUgd2hvbGUgY29ycHVzLiBOb3cgd2UgYXJlIGNsdXN0ZXJpbmcgYnkgZGlzdGFuY2UgaW4gMTAwMC1kaW1lbnNpb25hbCBzcGFjZS4KYGBge3J9CiNJZGVudGlmeSB0aGUgdGhvdXNhbmQgbW9zdCBmcmVxdWVudCB3b3Jkcwp0b3BfdGhvdXNhbmQgPC0gc290dV90b2tlbml6ZV93b3JkcygpICU+JSBjb3VudChncmFtKSAlPiUgdG9wX24oMTAwMCkKI01ha2UgYSBkb2N1bWVudC10ZXJtIG1hdHJpeCBvZiB0aGUgdGhvdXNhbmQgbW9zdCBmcmVxdWVudCB3b3Jkcwpzb3R1X3dvcmRzX2R0bSA8LSBzb3R1X3Rva2VuaXplX3dvcmRzKCkgJT4lIGZpbHRlcihncmFtICVpbiUgdG9wX3Rob3VzYW5kJGdyYW0pICU+JSAKICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXBfYnkoeWVhcikgJT4lIGNvdW50KGdyYW0pICU+JSBjYXN0X2R0bSh5ZWFyLCBncmFtLCBuKSAKYGBgCk9uY2Ugd2UgaGF2ZSB0aGUgZGlzdGFuY2UgbWF0cml4LCB3ZSBjYW4gcGxvdCBpdCBpbiB0d28gZGltZW5zaW9ucyB3aXRoIG11bHRpZGltZW5zaW9uYWwgc2NhbGluZyAoYGNtZHNjYWxlKClgKSBqdXN0IHRvIHNlZSB3aGF0IGl0IGxvb2tzIGxpa2UgaW4gdHdvIGRpbWVuc2lvbnMuIFRoaXMgaXMgc2ltaWxhciB0byB0aGUga2luZCBvZiBwbG90cyBwcm9kdWNlZCB3aXRoIHByaW5jaXBhbCBjb21wb25lbnRzIGFuYWx5c2lzCmBgYHtyfQpzb3R1X3dvcmRzX2Rpc3QgPC0gZGlzdChzb3R1X3dvcmRzX2R0bSkKcGxvdChjbWRzY2FsZShzb3R1X3dvcmRzX2Rpc3QsIGsgPSAyKSkKYGBgCkFzIHlvdSBjYW4gc2VlLCBtb3N0IG9mIHRoZSBhZGRyZXNzZXMgYXJlIHZlcnkgdGlnaHRseSBjbHVzdGVyZWQgdG9nZXRoZXIsIGJ1dCB0d28gYXJlIHZlcnkgZGlmZmVyZW50IGZyb20gdGhlIG90aGVycy4gTm93IHdlIGNhbiBjbHVzdGVyIGFuZCBwbG90IHRoZSBkZW5kcm9ncmFtLgpgYGB7cn0Kc290dV93b3Jkc19jbHVzdGVyIDwtIGhjbHVzdChzb3R1X3dvcmRzX2Rpc3QpCnBsb3Qoc290dV93b3Jkc19jbHVzdGVyKQpgYGAKQXMgeW91IGNhbiBzZWUsIHR3byBhZGRyZXNzZXMgKDE5NDYgYW5kIDE5ODEpIGFyZSB2ZXJ5IGRpZmZlcmVudCBmcm9tIHRoZSBvdGhlcnMuIFRoZXNlIGFyZSB0aGUgdHdvIHdlIHNhdyBpbiB0aGUgYm90dG9tIHJpZ2h0IGNvcm5lciBvZiB0aGUgdHdvLWRpbWVuc2lvbmFsIHBsb3QuCgpOb3cgbGV0J3Mgd3JpdGUgYSBmdW5jdGlvbiB0byBjb2xvciB0aGUgbGFiZWxzIG9uIG91ciB0d28tZGltZW5zaW9uYWwgcGxvdCBhY2NvcmRpbmcgdG8gYW55IG51bWJlciBvZiBjbHVzdGVycyBmcm9tIG91ciBoaWVyYXJjaGljYWwgY2x1c3Rlci4KYGBge3J9CnBsb3RfY2x1c3RlciA8LSBmdW5jdGlvbihuY2x1c3QpIHsKICBkYXRhLmZyYW1lKGNtZHNjYWxlKHNvdHVfd29yZHNfZGlzdCwgayA9IDIpKSAlPiUgCiAgICBtdXRhdGUoY2x1c3RlciA9IGN1dHJlZShzb3R1X3dvcmRzX2NsdXN0ZXIsIG5jbHVzdCksIHllYXIgPSBzb3R1JHllYXIpICU+JQogIGdncGxvdChhZXMoeCA9IFgxLCB5ID0gWDIsIGNvbG9yID0gZmFjdG9yKGNsdXN0ZXIpLCBsYWJlbCA9IHllYXIpKSArIGdlb21fbGFiZWwoKQp9CmZvcihpIGluIDI6MjApIHsKICBwcmludChwbG90X2NsdXN0ZXIoaSkpCn0KYGBgClRoZSBzY2F0dGVycGxvdCBhbmQgdGhlIGRlbmRyb2dyYW0gcHJvdmlkZSBtb3JlIG9yIGxlc3MgdGhlIHNhbWUgaW5mb3JtYXRpb24sIHNob3dpbmcgdGhlIGRpc3RhbmNlIGJldHdlZW4gYW55IHR3byBhZGRyZXNzZXMgYW5kIHRoZSBjbHVzdGVycyB0aGF0IHRoZXkgYXJlIGFzc2lnbmVkIGludG8gYXQgYW55IGxldmVsIG9mIGhpZXJhcmNoeS4gQnV0IHRoZXkgZG9uJ3QgdGVsbCB1cyBhbnl0aGluZyBhYm91dCB0aGUgY29udGVudHMgb2YgdGhlIGNsdXN0ZXJzLiBUaGUgY29kZSBiZWxvdyBpcyBhIGZ1bmN0aW9uIHRoYXQsIGZvciBhbnkgbnVtYmVyIG9mIGNsdXN0ZXJzLCBpZGVudGlmaWVzIHRoZSB0b3AgdGVuIHdvcmRzIGJ5IHRmLWlkZi4gQXMgeW91IHdpbGwgc2VlLCBpbnN0ZWFkIG9mIGNhbGN1bGF0aW5nIHRmLWlkZiBtYW51YWxseSB0aGUgd2F5IHdlIGRpZCBpbiBOb3RlYm9vayAzLCB3ZSBhcmUgdXNpbmcgdGhlIGBiaW5kX3RmX2lkZigpYCBmdW5jdGlvbiBmcm9tIHRoZSBgdGlkeXRleHRgIHBhY2thZ2UuCmBgYHtyfQojRnVuY3Rpb24gdG8gaWRlbnRpZnkgdGhlIDEwIG1vc3QgdW5pcXVlbHkgY2hhcmFjdGVyaXN0aWMgd29yZHMgb2YgZWFjaCBjbHVzdGVyCmNsdXN0ZXJfd29yZHMgPC0gZnVuY3Rpb24obmNsdXN0KSB7CiAgI0FkZCBhIGNvbHVtbiB0byBzb3R1IGluZGljYXRpbmcgd2hpY2ggY2x1c3RlciBhbiBhZGRyZXNzIGlzIGluIChmb3IgbmNsdXN0IGNsdXN0ZXJzKQogIHNvdHUgJT4lIG11dGF0ZShjbHVzdGVyID0gY3V0cmVlKHNvdHVfd29yZHNfY2x1c3RlciwgbmNsdXN0KSkgJT4lCiAgICAjVW5uZXN0IHRva2VucyBhbmQga2VlcCBvbmx5IHdvcmRzIGluIHRoZSB0b3AgdGhvdXNhbmQKICAgIHVubmVzdF90b2tlbnMoZ3JhbSwgdGV4dCkgJT4lIGZpbHRlcihncmFtICVpbiUgdG9wX3Rob3VzYW5kJGdyYW0pICU+JQogICAgI0NhbGN1bGF0ZSB0Zi1pZGYgZm9yIGVhY2ggd29yZCBieSBjbHVzdGVyCiAgICBncm91cF9ieShjbHVzdGVyKSAlPiUgY291bnQoZ3JhbSkgJT4lIGJpbmRfdGZfaWRmKGdyYW0sIGNsdXN0ZXIsIG4pICU+JQogICAgI0NvbGxhcHNlIGxpc3Qgb2Ygd29yZHMgZm9yIGVhY2ggY2x1c3RlciBpbnRvIGEgc2luZ2xlIHN0cmluZwogICAgZ3JvdXBfYnkoY2x1c3RlcikgJT4lIHRvcF9uKDEwLCB0Zl9pZGYpICU+JSBzdW1tYXJpemUod29yZHMgPSBzdHJfYyhncmFtLCBjb2xsYXBzZSA9ICIsICIpKQp9CmNsdXN0ZXJfd29yZHMoMykgCmNsdXN0ZXJfd29yZHMoNTApCmBgYApOb3cgZm9yIGFueSBudW1iZXIgb2YgY2x1c3RlcnMsIHdlIGNhbiBwbG90IHRoZSBjbHVzdGVycyBhbmQgc2VlIHdoaWNoIHdvcmRzIGFyZSBkaXN0aW5jdGl2ZSBvZiBlYWNoIG9uZS4KYGBge3J9CnBsb3RfY2x1c3Rlcig0KQpjbHVzdGVyX3dvcmRzKDQpCmBgYApgYGB7cn0KcGxvdF9jbHVzdGVyKDIwKQpjbHVzdGVyX3dvcmRzKDIwKQpgYGAKV2UgZG9uJ3QgaGF2ZSB0byBjbHVzdGVyIG9uIHdvcmRzLiBXZSBjYW4gYWxzbyB1c2UgYmlncmFtcywgdHJpZ3JhbXMsIG9yIGFueSBvdGhlciB1bml0IG9mIGFuYWx5c2lzLiBUaGlzIHRpbWUsIGxldCdzIHVzZSB0aGUgdG9wIDEwMCBiaWdyYW1zIGFzIG91ciBjbHVzdGVyaW5nIGZlYXR1cmVzLgpgYGB7cn0KI0ZpbmQgdG9wIDEwMCBiaWdyYW1zCnRvcF9odW5kcmVkIDwtIHNvdHVfdG9rZW5pemVfYmlncmFtcygpICU+JSBjb3VudChncmFtKSAlPiUgdG9wX24oMTAwKQojTWFrZSBkb2N1bWVudCB0ZXJtIG1hdHJpeCBmcm9tIHRvcCAxMDAgYmlncmFtcwpzb3R1X2JpZ3JhbXNfZHRtIDwtIHNvdHVfdG9rZW5pemVfYmlncmFtcygpICU+JSBmaWx0ZXIoZ3JhbSAlaW4lIHRvcF9odW5kcmVkJGdyYW0pICU+JSAKICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXBfYnkoeWVhcikgJT4lIGNvdW50KGdyYW0pICU+JSBjYXN0X2R0bSh5ZWFyLCBncmFtLCBuKSAKI0NhbGN1bGF0ZSBFdWNsaWRlYW4gZGlzdGFuY2VzCnNvdHVfYmlncmFtc19kaXN0IDwtIGRpc3Qoc290dV9iaWdyYW1zX2R0bSkKI1Bsb3Qgb24gdHdvIGRpbWVuc2lvbnMKcGxvdChjbWRzY2FsZShzb3R1X2JpZ3JhbXNfZGlzdCwgayA9IDIpKQpgYGAKYGBge3J9CiNDbHVzdGVyIGhpZXJhcmNoaWNhbGx5CnNvdHVfYmlncmFtc19jbHVzdGVyIDwtIGhjbHVzdChzb3R1X2JpZ3JhbXNfZGlzdCkKI1Bsb3QgZGVuZHJvZ3JhbQpwbG90KHNvdHVfYmlncmFtc19jbHVzdGVyKQpgYGAKV3JpdGUgYSBmdW5jdGlvbiB0byBwbG90IGFuZCB2aWV3IHRoZSB3b3JkcyBmb3IgYW55IG51bWJlciBvZiBjbHVzdGVycy4KYGBge3J9CnBsb3RfYW5kX3ZpZXcgPC0gZnVuY3Rpb24obmNsdXN0KSB7CiAgZ3JhcGggPC0gZGF0YS5mcmFtZShjbWRzY2FsZShzb3R1X2JpZ3JhbXNfZGlzdCwgayA9IDIpKSAlPiUgCiAgICBtdXRhdGUoY2x1c3RlciA9IGN1dHJlZShzb3R1X2JpZ3JhbXNfY2x1c3RlciwgbmNsdXN0KSwgeWVhciA9IHNvdHUkeWVhcikgJT4lIAogICAgZ2dwbG90KGFlcyh4ID0gWDEsIHkgPSBYMiwgY29sb3IgPSBmYWN0b3IoY2x1c3RlciksIGxhYmVsID0geWVhcikpICsgZ2VvbV9sYWJlbCgpCiAgZGF0YWZyYW1lIDwtIHNvdHUgJT4lIG11dGF0ZShjbHVzdGVyID0gY3V0cmVlKHNvdHVfYmlncmFtc19jbHVzdGVyLCBuY2x1c3QpKSAlPiUKICAgIHVubmVzdF90b2tlbnMoZ3JhbSwgdGV4dCwgdG9rZW4gPSAibmdyYW1zIiwgbiA9IDIpICU+JSBmaWx0ZXIoZ3JhbSAlaW4lIHRvcF9odW5kcmVkJGdyYW0pICU+JQogICAgZ3JvdXBfYnkoY2x1c3RlcikgJT4lIGNvdW50KGdyYW0pICU+JSBiaW5kX3RmX2lkZihncmFtLCBjbHVzdGVyLCBuKSAlPiUKICAgIGdyb3VwX2J5KGNsdXN0ZXIpICU+JSB0b3BfbigxMCwgdGZfaWRmKSAlPiUgc3VtbWFyaXplKGJpZ3JhbXMgPSBzdHJfYyhncmFtLCBjb2xsYXBzZSA9ICIsICIpKQogIHByaW50KGdyYXBoKQogIHJldHVybihkYXRhZnJhbWUpCn0KcGxvdF9hbmRfdmlldygxMCkKYGBgCkNsdXN0ZXJpbmcgaXMgYSB3YXkgb2YgZXhwbG9yaW5nIHlvdXIgY29ycHVzLiBEaWZmZXJlbnQgbnVtYmVycyBvZiBjbHVzdGVycyBhbmQgZGlmZmVyZW50IGNsdXN0ZXJpbmcgY3JpdGVyaWEgd2lsbCBiZSBtb3JlIG9yIGxlc3Mgc2FsaWVudCBmb3IgZGlmZmVyZW50IGNvcnBvcmEuIFJlbWVtYmVyIHRoYXQgY2x1c3RlcmluZyBpcyBub3QgbWFnaWMuIERvY3VtZW50cyBhcmUgYmVpbmcgZ3JvdXBlZCBieSB0aGUgY3JpdGVyaWEgKnlvdSogc3BlY2lmeSwgc28gdGhleSBjYW4gb25seSBzaG93IHlvdSBwYXR0ZXJucyB0aGF0IHlvdSBhcmUgbG9va2luZyBmb3IuIEFsc28gcmVtZW1iZXIgdGhhdCB0d28gZG9jdW1lbnRzIGNhbiB1c2UgYWxtb3N0IHRoZSBleGFjdCBzYW1lIHdvcmRzIGFuZCBzdGlsbCBoYXZlIHZlcnkgZGlmZmVyZW50IG1lYW5pbmdzLgo=