Clustering

Description of data set

In this tutorial, we will use the dataset from Wulff et al. (2023), which is part of the moursetrap package. The dataset, as prepared in the following code chunk, contains the mouse movement trajectories of participants in a two-options forced-choice paradigm. The trajectories are normalized using the mt_length_normalize() function from the moursetrap package so that all trajectories consist of 50 points (default is 20) in a 2D space.

library(mousetrap)
library(tidyverse)
dat <- data(KH2017)

# Preprocess trajectory data
dat <- KH2017 %>% mt_length_normalize(n_points = 50)
dat <- dat$ln_trajectories
dat[1:5,1:5,'xpos']; dat[1:5,1:5,'ypos'] #examles
       [,1]      [,2]       [,3]       [,4]       [,5]
id0001    0 -18.06069 -38.967198 -57.753756 -76.540313
id0002    0 -15.60052 -32.227659 -48.262305 -64.296952
id0003    0 -10.02617 -17.001541 -21.124366 -23.588654
id0004    0 -20.06305 -36.929651 -52.368759 -65.752596
id0005    0   0.00000   1.080633   3.535698   5.587012
       [,1]      [,2]      [,3]      [,4]      [,5]
id0001    0  7.695611 13.135988  23.98456  34.83314
id0002    0 11.244849 24.194179  37.87079  51.54740
id0003    0 16.565420 39.008474  62.18148  85.59222
id0004    0 -2.531525  6.049725  20.71095  37.44075
id0005    0 40.034589 80.048229 119.99954 159.97921
dat2 <- data.frame(cbind(dat[,,'xpos'], dat[,,'ypos']))

We can use the mt_heatmap() function from the moursetrap package to visualize the trajectories. The resulting plot contains 1064 mouse movement trajectories of participants. In this tutorial, we want to cluster these trajectories to make sense of this kind of data (i.e., shed light on the processes of information integration and preference formation; Wulff et al., 2023).

mt_heatmap(dat, colors = c('white', 'black'), verbose = FALSE)

Tasks

  1. Cluster the trajectories from dat2 (i.e., treating the x- and y-coordinates as features) into 5 clusters by means of agglomerative hierarchical clustering using the agnes algorithm.
k = 5

library(mlr3verse)
tsk = as_task_clust(dat2)

set.seed(42)
mdl = lrn("clust.agnes", stand = T, k = k)
mdl$train(tsk)
  1. Predict the cluster of each individual trajectory using the model from task 1. Also produce a frequency table for the relative proportion of instances in each cluster.
pred <- mdl$predict(tsk)
Warning: Learner 'clust.agnes' doesn't predict on new data and predictions may not make sense on new data
round(table(pred$partition)/length(pred$partition) * 100, 2)

    1     2     3     4     5 
95.68  0.38  3.10  0.75  0.09 
  1. Redo task 1 and cluster the trajectories into 5 clusters using agglomerative hierarchical clustering and Ward’s method (i.e., define a learner with method = “ward”). Also predict the clusters using the new model. Did the relative frequencies improve (in terms of resulting in a more balanced clustering)?
set.seed(42)
mdl = lrn("clust.agnes", stand = T, k = k, method = "ward")
mdl$train(tsk)

pred <- mdl$predict(tsk)
Warning: Learner 'clust.agnes' doesn't predict on new data and predictions may not make sense on new data
round(table(pred$partition)/length(pred$partition) * 100, 2)

    1     2     3     4     5 
26.88 21.33 30.26 17.95  3.57 

The resulting clustering is more balanced and more closely resembles the frequency proportions as reported in Wulff et al. (2023, Figure 8). However, to reassure that our clustering distinguishes between the same movement patterns, we also have to visualize the clusters (see next task).

  1. Plot the trajectories according to the clustering from task 4. For instance, use a for-loop and the mt_heatmap() function from above to produce a separate heatmap for each cluster of movement trajectories.
par(mfrow = c(1,5))
for (j in 1:k) {
  cat(mt_heatmap(dat[pred$partition == j,,], colors = c('white', 'black'), verbose = FALSE))
}

It appears that our clustering distinguishes similar choice strategies as found in Wulff et al. (2023, Figure 8). However, not all five prototypes (straight, curved, cCoM = continuous change of mind, dCoM = discrete change of mind, and dCoM2 = discrete double change of mind) are recovered.

  1. Redo task 1 and cluster the trajectories into 5 clusters using partitional (i.e., \(k\)-means) clustering. Also redo task 2 and predict the clusters using the new model.
set.seed(42)
mdl = lrn("clust.kmeans", centers = k)
mdl$train(tsk)

pred <- mdl$predict(tsk)
round(table(pred$partition)/length(pred$partition) * 100, 2)

    1     2     3     4     5 
 4.04 39.38 12.88 10.81 32.89 
  1. Compare the results from task 5 to the results with the hierarchical clustering by redoing task 4, that is, visualizing the trajectories of the partitional clustering. Do you observe any performance differences?
par(mfrow = c(1,5))
for (j in 1:k) {
  cat(mt_heatmap(dat[pred$partition == j,,], colors = c('white', 'black'), verbose = FALSE))
}

Partitional clustering seems to perform slightly better in discriminating between all five prototype strategies as originally reported in Wulff et al. (2023; i.e., straight, curved, cCoM = continuous change of mind, dCoM = discrete change of mind, and dCoM2 = discrete double change of mind). This can also be confirmed by plotting the prototypes of each cluster (see next task).

  1. Bonus: Also plot the prototypes of your \(k\)-means clustering solution.
df <- array(NA, c(2, 50, 2), dimnames = list(NULL, NULL, c('xpos', 'ypos'))) #auxiliary data frame to submit the prototypes in the format needed for plotting with mt_heatmap()

par(mfrow = c(1,5))
for (j in 1:k) {
  df[1,,'xpos'] <- mdl$model$centers[j,paste0('X',1:50)]
  df[1,,'ypos'] <- mdl$model$centers[j,paste0('X',51:100)]
  
  mt_heatmap(df, colors = c('white', 'blue'), verbose = FALSE)
}

The prototypes are indeed very similar to the original ones of Wulff et al. (2023).

  1. Bonus: Prior to performing the clustering, do a principal component analysis (PCA). Then, redo task 6 (i.e., compare agglomerative hierarchicalc lustering using Ward’s method and \(k\)-means partitional clustering) with both clusterings specified using only these 5 principal components (PCs) as features that explain the highest amount of variance in the data. (Hint: You can select a subset of PCs that should be used for the modeling by adding a filter pipeline operation after the pca pipeline operation. You can filter for “variance” using the flt() function and set a corresponding fraction for using only these PCs for the clustering that explain the highest amount of variance in the data)

Agnes clustering:

set.seed(42)
mdl <- as_learner(po("pca") %>>%
                    po("filter", filter = flt("variance"), filter.frac = 0.05) %>>%
                    lrn('clust.agnes', stand = T, k = k, method = "ward"))
mdl$train(tsk)

mdl$model$variance$features #The PCs that were actually used as features
[1] "PC1" "PC2" "PC3" "PC4" "PC5"
pred <- mdl$predict(tsk)
Warning: Learner 'clust.agnes' doesn't predict on new data and predictions may not make sense on new data
This happened PipeOp clust.agnes's $predict()
round(table(pred$partition)/length(pred$partition) * 100, 2)

    1     2     3     4     5 
54.51 24.44 15.04  5.26  0.75 
par(mfrow = c(1,5))
for (j in 1:k) {
  cat(mt_heatmap(dat[pred$partition == j,,], colors = c('white', 'black'), verbose = FALSE))
}

\(k\)-means clustering:

set.seed(42)
mdl <- as_learner(po("pca") %>>% 
                    po("filter", filter = flt("variance"), filter.frac = 0.05) %>>%
                    lrn('clust.kmeans', centers = k))
mdl$train(tsk)
 
mdl$model$variance$features #The PCs that were actually used as features
[1] "PC1" "PC2" "PC3" "PC4" "PC5"
pred <- mdl$predict(tsk)
round(table(pred$partition)/length(pred$partition) * 100, 2)

    1     2     3     4     5 
39.29 32.99 10.81 12.88  4.04 
par(mfrow = c(1,5))
for (j in 1:k) {
  cat(mt_heatmap(dat[pred$partition == j,,], colors = c('white', 'black'), verbose = FALSE))
}

While hierarchical clustering now performs worse compared to using the information contained in all available data, for partitional \(k\)-means clustering, it seems that all the necessary information to discriminate between the five original prototype strategies is contained in the first five PCs.

LS0tDQp0aXRsZTogIk1vZHVsZSAzOiBUdXRvcmlhbDogQ2x1c3RlcmluZyINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KZWRpdG9yX29wdGlvbnM6IA0KICBjaHVua19vdXRwdXRfdHlwZTogaW5saW5lDQotLS0NCg0KIyBDbHVzdGVyaW5nDQoNCiMjIERlc2NyaXB0aW9uIG9mIGRhdGEgc2V0DQoNCkluIHRoaXMgdHV0b3JpYWwsIHdlIHdpbGwgdXNlIHRoZSBkYXRhc2V0IGZyb20gV3VsZmYgZXQgYWwuICgyMDIzKSwgd2hpY2ggaXMgcGFydCBvZiB0aGUgYG1vdXJzZXRyYXBgIHBhY2thZ2UuIFRoZSBkYXRhc2V0LCBhcyBwcmVwYXJlZCBpbiB0aGUgZm9sbG93aW5nIGNvZGUgY2h1bmssIGNvbnRhaW5zIHRoZSBtb3VzZSBtb3ZlbWVudCB0cmFqZWN0b3JpZXMgb2YgcGFydGljaXBhbnRzIGluIGEgdHdvLW9wdGlvbnMgZm9yY2VkLWNob2ljZSBwYXJhZGlnbS4gVGhlIHRyYWplY3RvcmllcyBhcmUgbm9ybWFsaXplZCB1c2luZyB0aGUgYG10X2xlbmd0aF9ub3JtYWxpemUoKWAgZnVuY3Rpb24gZnJvbSB0aGUgYG1vdXJzZXRyYXBgIHBhY2thZ2Ugc28gdGhhdCBhbGwgdHJhamVjdG9yaWVzIGNvbnNpc3Qgb2YgNTAgcG9pbnRzIChkZWZhdWx0IGlzIDIwKSBpbiBhIDJEIHNwYWNlLg0KDQpgYGB7cn0NCmxpYnJhcnkobW91c2V0cmFwKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpkYXQgPC0gZGF0YShLSDIwMTcpDQoNCiMgUHJlcHJvY2VzcyB0cmFqZWN0b3J5IGRhdGENCmRhdCA8LSBLSDIwMTcgJT4lIG10X2xlbmd0aF9ub3JtYWxpemUobl9wb2ludHMgPSA1MCkNCmRhdCA8LSBkYXQkbG5fdHJhamVjdG9yaWVzDQpkYXRbMTo1LDE6NSwneHBvcyddOyBkYXRbMTo1LDE6NSwneXBvcyddICNleGFtbGVzDQpkYXQyIDwtIGRhdGEuZnJhbWUoY2JpbmQoZGF0WywsJ3hwb3MnXSwgZGF0WywsJ3lwb3MnXSkpDQpgYGANCg0KV2UgY2FuIHVzZSB0aGUgYG10X2hlYXRtYXAoKWAgZnVuY3Rpb24gZnJvbSB0aGUgYG1vdXJzZXRyYXBgIHBhY2thZ2UgdG8gdmlzdWFsaXplIHRoZSB0cmFqZWN0b3JpZXMuIFRoZSByZXN1bHRpbmcgcGxvdCBjb250YWlucyAxMDY0IG1vdXNlIG1vdmVtZW50IHRyYWplY3RvcmllcyBvZiBwYXJ0aWNpcGFudHMuIEluIHRoaXMgdHV0b3JpYWwsIHdlIHdhbnQgdG8gY2x1c3RlciB0aGVzZSB0cmFqZWN0b3JpZXMgdG8gbWFrZSBzZW5zZSBvZiB0aGlzIGtpbmQgb2YgZGF0YSAoaS5lLiwgc2hlZCBsaWdodCBvbiB0aGUgcHJvY2Vzc2VzIG9mIGluZm9ybWF0aW9uIGludGVncmF0aW9uIGFuZCBwcmVmZXJlbmNlIGZvcm1hdGlvbjsgV3VsZmYgZXQgYWwuLCAyMDIzKS4NCg0KYGBge3J9DQptdF9oZWF0bWFwKGRhdCwgY29sb3JzID0gYygnd2hpdGUnLCAnYmxhY2snKSwgdmVyYm9zZSA9IEZBTFNFKQ0KYGBgDQoNCiMjIFRhc2tzDQoNCjEuICBDbHVzdGVyIHRoZSB0cmFqZWN0b3JpZXMgZnJvbSBgZGF0MmAgKGkuZS4sIHRyZWF0aW5nIHRoZSB4LSBhbmQgeS1jb29yZGluYXRlcyBhcyBmZWF0dXJlcykgaW50byA1IGNsdXN0ZXJzIGJ5IG1lYW5zIG9mIGFnZ2xvbWVyYXRpdmUgaGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcgdXNpbmcgdGhlIGBhZ25lc2AgYWxnb3JpdGhtLg0KDQpgYGB7cn0NCmsgPSA1DQoNCmxpYnJhcnkobWxyM3ZlcnNlKQ0KdHNrID0gYXNfdGFza19jbHVzdChkYXQyKQ0KDQpzZXQuc2VlZCg0MikNCm1kbCA9IGxybigiY2x1c3QuYWduZXMiLCBzdGFuZCA9IFQsIGsgPSBrKQ0KbWRsJHRyYWluKHRzaykNCmBgYA0KDQoyLiAgUHJlZGljdCB0aGUgY2x1c3RlciBvZiBlYWNoIGluZGl2aWR1YWwgdHJhamVjdG9yeSB1c2luZyB0aGUgbW9kZWwgZnJvbSB0YXNrIDEuIEFsc28gcHJvZHVjZSBhIGZyZXF1ZW5jeSB0YWJsZSBmb3IgdGhlIHJlbGF0aXZlIHByb3BvcnRpb24gb2YgaW5zdGFuY2VzIGluIGVhY2ggY2x1c3Rlci4NCg0KYGBge3J9DQpwcmVkIDwtIG1kbCRwcmVkaWN0KHRzaykNCnJvdW5kKHRhYmxlKHByZWQkcGFydGl0aW9uKS9sZW5ndGgocHJlZCRwYXJ0aXRpb24pICogMTAwLCAyKQ0KYGBgDQoNCjMuICBSZWRvIHRhc2sgMSBhbmQgY2x1c3RlciB0aGUgdHJhamVjdG9yaWVzIGludG8gNSBjbHVzdGVycyB1c2luZyBhZ2dsb21lcmF0aXZlIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIGFuZCBXYXJkJ3MgbWV0aG9kIChpLmUuLCBkZWZpbmUgYSBsZWFybmVyIHdpdGggbWV0aG9kID0gIndhcmQiKS4gQWxzbyBwcmVkaWN0IHRoZSBjbHVzdGVycyB1c2luZyB0aGUgbmV3IG1vZGVsLiBEaWQgdGhlIHJlbGF0aXZlIGZyZXF1ZW5jaWVzIGltcHJvdmUgKGluIHRlcm1zIG9mIHJlc3VsdGluZyBpbiBhIG1vcmUgYmFsYW5jZWQgY2x1c3RlcmluZyk/DQoNCmBgYHtyfQ0Kc2V0LnNlZWQoNDIpDQptZGwgPSBscm4oImNsdXN0LmFnbmVzIiwgc3RhbmQgPSBULCBrID0gaywgbWV0aG9kID0gIndhcmQiKQ0KbWRsJHRyYWluKHRzaykNCg0KcHJlZCA8LSBtZGwkcHJlZGljdCh0c2spDQpyb3VuZCh0YWJsZShwcmVkJHBhcnRpdGlvbikvbGVuZ3RoKHByZWQkcGFydGl0aW9uKSAqIDEwMCwgMikNCmBgYA0KDQpUaGUgcmVzdWx0aW5nIGNsdXN0ZXJpbmcgaXMgbW9yZSBiYWxhbmNlZCBhbmQgbW9yZSBjbG9zZWx5IHJlc2VtYmxlcyB0aGUgZnJlcXVlbmN5IHByb3BvcnRpb25zIGFzIHJlcG9ydGVkIGluIFd1bGZmIGV0IGFsLiAoMjAyMywgRmlndXJlIDgpLiBIb3dldmVyLCB0byByZWFzc3VyZSB0aGF0IG91ciBjbHVzdGVyaW5nIGRpc3Rpbmd1aXNoZXMgYmV0d2VlbiB0aGUgc2FtZSBtb3ZlbWVudCBwYXR0ZXJucywgd2UgYWxzbyBoYXZlIHRvIHZpc3VhbGl6ZSB0aGUgY2x1c3RlcnMgKHNlZSBuZXh0IHRhc2spLg0KDQo0LiAgUGxvdCB0aGUgdHJhamVjdG9yaWVzIGFjY29yZGluZyB0byB0aGUgY2x1c3RlcmluZyBmcm9tIHRhc2sgNC4gRm9yIGluc3RhbmNlLCB1c2UgYSBgZm9yYC1sb29wIGFuZCB0aGUgYG10X2hlYXRtYXAoKWAgZnVuY3Rpb24gZnJvbSBhYm92ZSB0byBwcm9kdWNlIGEgc2VwYXJhdGUgaGVhdG1hcCBmb3IgZWFjaCBjbHVzdGVyIG9mIG1vdmVtZW50IHRyYWplY3Rvcmllcy4NCg0KYGBge3J9DQpwYXIobWZyb3cgPSBjKDEsNSkpDQpmb3IgKGogaW4gMTprKSB7DQogIGNhdChtdF9oZWF0bWFwKGRhdFtwcmVkJHBhcnRpdGlvbiA9PSBqLCxdLCBjb2xvcnMgPSBjKCd3aGl0ZScsICdibGFjaycpLCB2ZXJib3NlID0gRkFMU0UpKQ0KfQ0KYGBgDQoNCkl0IGFwcGVhcnMgdGhhdCBvdXIgY2x1c3RlcmluZyBkaXN0aW5ndWlzaGVzIHNpbWlsYXIgY2hvaWNlIHN0cmF0ZWdpZXMgYXMgZm91bmQgaW4gV3VsZmYgZXQgYWwuICgyMDIzLCBGaWd1cmUgOCkuIEhvd2V2ZXIsIG5vdCBhbGwgZml2ZSBwcm90b3R5cGVzIChzdHJhaWdodCwgY3VydmVkLCBjQ29NID0gY29udGludW91cyBjaGFuZ2Ugb2YgbWluZCwgZENvTSA9IGRpc2NyZXRlIGNoYW5nZSBvZiBtaW5kLCBhbmQgZENvTTIgPSBkaXNjcmV0ZSBkb3VibGUgY2hhbmdlIG9mIG1pbmQpIGFyZSByZWNvdmVyZWQuDQoNCjUuICBSZWRvIHRhc2sgMSBhbmQgY2x1c3RlciB0aGUgdHJhamVjdG9yaWVzIGludG8gNSBjbHVzdGVycyB1c2luZyBwYXJ0aXRpb25hbCAoaS5lLiwgJGskLW1lYW5zKSBjbHVzdGVyaW5nLiBBbHNvIHJlZG8gdGFzayAyIGFuZCBwcmVkaWN0IHRoZSBjbHVzdGVycyB1c2luZyB0aGUgbmV3IG1vZGVsLg0KDQpgYGB7cn0NCnNldC5zZWVkKDQyKQ0KbWRsID0gbHJuKCJjbHVzdC5rbWVhbnMiLCBjZW50ZXJzID0gaykNCm1kbCR0cmFpbih0c2spDQoNCnByZWQgPC0gbWRsJHByZWRpY3QodHNrKQ0Kcm91bmQodGFibGUocHJlZCRwYXJ0aXRpb24pL2xlbmd0aChwcmVkJHBhcnRpdGlvbikgKiAxMDAsIDIpDQpgYGANCg0KNi4gIENvbXBhcmUgdGhlIHJlc3VsdHMgZnJvbSB0YXNrIDUgdG8gdGhlIHJlc3VsdHMgd2l0aCB0aGUgaGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcgYnkgcmVkb2luZyB0YXNrIDQsIHRoYXQgaXMsIHZpc3VhbGl6aW5nIHRoZSB0cmFqZWN0b3JpZXMgb2YgdGhlIHBhcnRpdGlvbmFsIGNsdXN0ZXJpbmcuIERvIHlvdSBvYnNlcnZlIGFueSBwZXJmb3JtYW5jZSBkaWZmZXJlbmNlcz8NCg0KYGBge3J9DQpwYXIobWZyb3cgPSBjKDEsNSkpDQpmb3IgKGogaW4gMTprKSB7DQogIGNhdChtdF9oZWF0bWFwKGRhdFtwcmVkJHBhcnRpdGlvbiA9PSBqLCxdLCBjb2xvcnMgPSBjKCd3aGl0ZScsICdibGFjaycpLCB2ZXJib3NlID0gRkFMU0UpKQ0KfQ0KYGBgDQoNClBhcnRpdGlvbmFsIGNsdXN0ZXJpbmcgc2VlbXMgdG8gcGVyZm9ybSBzbGlnaHRseSBiZXR0ZXIgaW4gZGlzY3JpbWluYXRpbmcgYmV0d2VlbiBhbGwgZml2ZSBwcm90b3R5cGUgc3RyYXRlZ2llcyBhcyBvcmlnaW5hbGx5IHJlcG9ydGVkIGluIFd1bGZmIGV0IGFsLiAoMjAyMzsgaS5lLiwgc3RyYWlnaHQsIGN1cnZlZCwgY0NvTSA9IGNvbnRpbnVvdXMgY2hhbmdlIG9mIG1pbmQsIGRDb00gPSBkaXNjcmV0ZSBjaGFuZ2Ugb2YgbWluZCwgYW5kIGRDb00yID0gZGlzY3JldGUgZG91YmxlIGNoYW5nZSBvZiBtaW5kKS4gVGhpcyBjYW4gYWxzbyBiZSBjb25maXJtZWQgYnkgcGxvdHRpbmcgdGhlIHByb3RvdHlwZXMgb2YgZWFjaCBjbHVzdGVyIChzZWUgbmV4dCB0YXNrKS4NCg0KNy4gIEJvbnVzOiBBbHNvIHBsb3QgdGhlIHByb3RvdHlwZXMgb2YgeW91ciAkayQtbWVhbnMgY2x1c3RlcmluZyBzb2x1dGlvbi4NCg0KYGBge3J9DQpkZiA8LSBhcnJheShOQSwgYygyLCA1MCwgMiksIGRpbW5hbWVzID0gbGlzdChOVUxMLCBOVUxMLCBjKCd4cG9zJywgJ3lwb3MnKSkpICNhdXhpbGlhcnkgZGF0YSBmcmFtZSB0byBzdWJtaXQgdGhlIHByb3RvdHlwZXMgaW4gdGhlIGZvcm1hdCBuZWVkZWQgZm9yIHBsb3R0aW5nIHdpdGggbXRfaGVhdG1hcCgpDQoNCnBhcihtZnJvdyA9IGMoMSw1KSkNCmZvciAoaiBpbiAxOmspIHsNCiAgZGZbMSwsJ3hwb3MnXSA8LSBtZGwkbW9kZWwkY2VudGVyc1tqLHBhc3RlMCgnWCcsMTo1MCldDQogIGRmWzEsLCd5cG9zJ10gPC0gbWRsJG1vZGVsJGNlbnRlcnNbaixwYXN0ZTAoJ1gnLDUxOjEwMCldDQogIA0KICBtdF9oZWF0bWFwKGRmLCBjb2xvcnMgPSBjKCd3aGl0ZScsICdibHVlJyksIHZlcmJvc2UgPSBGQUxTRSkNCn0NCmBgYA0KDQpUaGUgcHJvdG90eXBlcyBhcmUgaW5kZWVkIHZlcnkgc2ltaWxhciB0byB0aGUgb3JpZ2luYWwgb25lcyBvZiBXdWxmZiBldCBhbC4gKDIwMjMpLg0KDQo4LiAgQm9udXM6IFByaW9yIHRvIHBlcmZvcm1pbmcgdGhlIGNsdXN0ZXJpbmcsIGRvIGEgcHJpbmNpcGFsIGNvbXBvbmVudCBhbmFseXNpcyAoUENBKS4gVGhlbiwgcmVkbyB0YXNrIDYgKGkuZS4sIGNvbXBhcmUgYWdnbG9tZXJhdGl2ZSBoaWVyYXJjaGljYWxjIGx1c3RlcmluZyB1c2luZyBXYXJkJ3MgbWV0aG9kIGFuZCAkayQtbWVhbnMgcGFydGl0aW9uYWwgY2x1c3RlcmluZykgd2l0aCBib3RoIGNsdXN0ZXJpbmdzIHNwZWNpZmllZCB1c2luZyBvbmx5IHRoZXNlIDUgcHJpbmNpcGFsIGNvbXBvbmVudHMgKFBDcykgYXMgZmVhdHVyZXMgdGhhdCBleHBsYWluIHRoZSBoaWdoZXN0IGFtb3VudCBvZiB2YXJpYW5jZSBpbiB0aGUgZGF0YS4gKEhpbnQ6IFlvdSBjYW4gc2VsZWN0IGEgc3Vic2V0IG9mIFBDcyB0aGF0IHNob3VsZCBiZSB1c2VkIGZvciB0aGUgbW9kZWxpbmcgYnkgYWRkaW5nIGEgYGZpbHRlcmAgcGlwZWxpbmUgb3BlcmF0aW9uIGFmdGVyIHRoZSBgcGNhYCBwaXBlbGluZSBvcGVyYXRpb24uIFlvdSBjYW4gZmlsdGVyIGZvciAidmFyaWFuY2UiIHVzaW5nIHRoZSBgZmx0KClgIGZ1bmN0aW9uIGFuZCBzZXQgYSBjb3JyZXNwb25kaW5nIGZyYWN0aW9uIGZvciB1c2luZyBvbmx5IHRoZXNlIFBDcyBmb3IgdGhlIGNsdXN0ZXJpbmcgdGhhdCBleHBsYWluIHRoZSBoaWdoZXN0IGFtb3VudCBvZiB2YXJpYW5jZSBpbiB0aGUgZGF0YSkNCg0KQWduZXMgY2x1c3RlcmluZzoNCg0KYGBge3J9DQpzZXQuc2VlZCg0MikNCm1kbCA8LSBhc19sZWFybmVyKHBvKCJwY2EiKSAlPj4lDQogICAgICAgICAgICAgICAgICAgIHBvKCJmaWx0ZXIiLCBmaWx0ZXIgPSBmbHQoInZhcmlhbmNlIiksIGZpbHRlci5mcmFjID0gMC4wNSkgJT4+JQ0KICAgICAgICAgICAgICAgICAgICBscm4oJ2NsdXN0LmFnbmVzJywgc3RhbmQgPSBULCBrID0gaywgbWV0aG9kID0gIndhcmQiKSkNCm1kbCR0cmFpbih0c2spDQoNCm1kbCRtb2RlbCR2YXJpYW5jZSRmZWF0dXJlcyAjVGhlIFBDcyB0aGF0IHdlcmUgYWN0dWFsbHkgdXNlZCBhcyBmZWF0dXJlcw0KDQpwcmVkIDwtIG1kbCRwcmVkaWN0KHRzaykNCnJvdW5kKHRhYmxlKHByZWQkcGFydGl0aW9uKS9sZW5ndGgocHJlZCRwYXJ0aXRpb24pICogMTAwLCAyKQ0KDQpwYXIobWZyb3cgPSBjKDEsNSkpDQpmb3IgKGogaW4gMTprKSB7DQogIGNhdChtdF9oZWF0bWFwKGRhdFtwcmVkJHBhcnRpdGlvbiA9PSBqLCxdLCBjb2xvcnMgPSBjKCd3aGl0ZScsICdibGFjaycpLCB2ZXJib3NlID0gRkFMU0UpKQ0KfQ0KYGBgDQoNCiRrJC1tZWFucyBjbHVzdGVyaW5nOg0KDQpgYGB7cn0NCnNldC5zZWVkKDQyKQ0KbWRsIDwtIGFzX2xlYXJuZXIocG8oInBjYSIpICU+PiUgDQogICAgICAgICAgICAgICAgICAgIHBvKCJmaWx0ZXIiLCBmaWx0ZXIgPSBmbHQoInZhcmlhbmNlIiksIGZpbHRlci5mcmFjID0gMC4wNSkgJT4+JQ0KICAgICAgICAgICAgICAgICAgICBscm4oJ2NsdXN0LmttZWFucycsIGNlbnRlcnMgPSBrKSkNCm1kbCR0cmFpbih0c2spDQogDQptZGwkbW9kZWwkdmFyaWFuY2UkZmVhdHVyZXMgI1RoZSBQQ3MgdGhhdCB3ZXJlIGFjdHVhbGx5IHVzZWQgYXMgZmVhdHVyZXMNCg0KcHJlZCA8LSBtZGwkcHJlZGljdCh0c2spDQpyb3VuZCh0YWJsZShwcmVkJHBhcnRpdGlvbikvbGVuZ3RoKHByZWQkcGFydGl0aW9uKSAqIDEwMCwgMikNCg0KcGFyKG1mcm93ID0gYygxLDUpKQ0KZm9yIChqIGluIDE6aykgew0KICBjYXQobXRfaGVhdG1hcChkYXRbcHJlZCRwYXJ0aXRpb24gPT0gaiwsXSwgY29sb3JzID0gYygnd2hpdGUnLCAnYmxhY2snKSwgdmVyYm9zZSA9IEZBTFNFKSkNCn0NCmBgYA0KDQpXaGlsZSBoaWVyYXJjaGljYWwgY2x1c3RlcmluZyBub3cgcGVyZm9ybXMgd29yc2UgY29tcGFyZWQgdG8gdXNpbmcgdGhlIGluZm9ybWF0aW9uIGNvbnRhaW5lZCBpbiBhbGwgYXZhaWxhYmxlIGRhdGEsIGZvciBwYXJ0aXRpb25hbCAkayQtbWVhbnMgY2x1c3RlcmluZywgaXQgc2VlbXMgdGhhdCBhbGwgdGhlIG5lY2Vzc2FyeSBpbmZvcm1hdGlvbiB0byBkaXNjcmltaW5hdGUgYmV0d2VlbiB0aGUgZml2ZSBvcmlnaW5hbCBwcm90b3R5cGUgc3RyYXRlZ2llcyBpcyBjb250YWluZWQgaW4gdGhlIGZpcnN0IGZpdmUgUENzLg==