This is an old revision of the document!


Time Series Clustering Using Self-Organizing Maps

Similarity Distances

library(zoo)
library(kohonen)
library(ggplot2)
library(dtw)
# Two time series
X <- c(1,1,1,4,4,4,4,4,1,1)
Y <- c(1,1,4,4,4,4,4,1,1,1)
# Euclidean distance
TSdist::DTWDistance(X, Y)
[1] 0
# Euclidean distance
TSdist::EuclideanDistance(X, Y)
[1] 4.242641
# DTW distance
alignment<-dtw::dtw(X, Y, keep.internals = TRUE, step=symmetric1, k=T)
plot(alignment)

plot(alignment,type="threeway")

alignment$localCostMatrix
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
 [1,]    0    0    3    3    3    3    3    0    0     0
 [2,]    0    0    3    3    3    3    3    0    0     0
 [3,]    0    0    3    3    3    3    3    0    0     0
 [4,]    3    3    0    0    0    0    0    3    3     3
 [5,]    3    3    0    0    0    0    0    3    3     3
 [6,]    3    3    0    0    0    0    0    3    3     3
 [7,]    3    3    0    0    0    0    0    3    3     3
 [8,]    3    3    0    0    0    0    0    3    3     3
 [9,]    0    0    3    3    3    3    3    0    0     0
[10,]    0    0    3    3    3    3    3    0    0     0
alignment$distance
[1] 0
alignment$stepsTaken
 [1] 3 1 1 1 1 1 1 1 2 1
plot(alignment$index1,alignment$index2,main="Warping function");

plot(dtw::dtw(X, Y, keep.internals = TRUE, step.pattern =rabinerJuangStepPattern(6,"c")),
     type="twoway",offset=-2)

dtwPlotTwoWay(dtw(X, Y, step = asymmetricP1, keep = T))

rabinerJuangStepPattern(6,"c")
Step pattern recursion:
g[i,j] = min(
     g[i-3,j-2] +     d[i-2,j-1] +     d[i-1,j  ] +     d[i  ,j  ] ,
     g[i-1,j-1] +     d[i  ,j  ] ,
     g[i-2,j-3] +     d[i-1,j-2] +     d[i  ,j-1] + 0 * d[i  ,j  ] ,
  )

 Normalization hint: N
plot(rabinerJuangStepPattern(6,"c"))

# Plot
zoo::plot.zoo(cbind(X, Y), plot.type = "multiple", col = c("red", "blue"), lwd = 2)

#Creates time series
ts1_x <- c(1,2,3,4,5,6,7,8,9,10)
ts1_y <- c(1,1,1,4,4,4,4,4,1,1)
ts1 <- zoo::zoo(ts1_y, ts1_x);
#Creates time series
ts2_x <- c(1,2,3,4,5,6,7,8,9,10)
ts2_y <- c(1,1,4,4,4,4,4,1,1,1)
ts2 <- zoo::zoo(ts1_x, ts1_y);
some methods for <U+393C><U+3E33>zoo<U+393C><U+3E34> objects do not work if the index entries in <U+393C><U+3E31>order.by<U+393C><U+3E32> are not unique

Plot time series

zoo::plot.zoo(cbind(ts2_y, ts1_y), plot.type = "multiple", col = c("red", "blue"), lwd = 2)

TSdist::EuclideanDistance(ts1_y, ts2_y)
[1] 4.242641
TSdist::DTWDistance(ts1_y, ts2_y)
[1] 0

Read the time series from csv file and plot all

#read time series from file
timeseries.zoo <- read.zoo("timeseries.csv", header = TRUE, sep = ",")
#zoo::plot.zoo(z, main = "Set of time series")
#Define a color for each group (4 groups)
colors<-c("#FF0000FF","#FF0000FF","#FF0000FF","#FF0000FF",
          "#00FF9FFF","#00FF9FFF","#00FF9FFF","#00FF9FFF",
          "#009FFFFF","#009FFFFF","#009FFFFF","#009FFFFF",
          "#DFFF00FF","#DFFF00FF","#DFFF00FF","#DFFF00FF")
#Plot all time series
zoo::plot.zoo(timeseries.zoo, plot.type = "multiple", col = colors, lwd = 2, main= "Set of time series")

Self-organizing maps using Kohonen package

# SOM
somgrid=kohonen::somgrid(xdim=4, ydim=4, topo="rectangular", neighbourhood.fct = "gaussian")
som<-kohonen::supersom(t(as.matrix(timeseries.zoo)),somgrid,rlen = 100,  alpha=1)
plot(som,"codes",codeRendering="lines")

The time series were allocated in these neurons. Each number represents the number of neuron.

Example: the position 1 of vector represents the time series 1 in a dataset, this time series was allocated in neuron 9.

neuron_timeseries<-som$unit.classif
info_timeSeries<- data.frame(time_series=(rownames(t(timeseries.zoo))), 
                             neuron=as.integer(som$unit.classif))
                        
codes<-zoo::zoo(t(som$codes[[1]]))
                             
zoo::plot.zoo(codes, plot.type = "multiple", col = "black", lwd = 2, main= "Weight of neurons")

#plot all time series (neurons)
ggplot2::autoplot((codes), facet = NULL) + ylab("y") +xlab("time") + geom_line(size = 1) + ggtitle("Weight of neurons")

library(proxy)
library(stats)
## use hierarchical clustering to cluster the codebook vectors.
# Cut dendrogram in 4 groups
hc <- stats::hclust(dist(t(codes)),method = "ward.D2")
#Cuts a tree, e.g., as resulting from hclust, into several groups either 
#by specifying the desired number(s) of groups or the cut height(s).
som_cluster <- stats::cutree(hc, 4)
library(dendextend)
#plot dendrogram
dend <- hc%>% as.dendrogram %>%
  set("branches_k_color", k = 4) %>% set("branches_lwd", 1.2) %>%
  set("labels_cex", 0.8) %>% set("labels_colors", k = 4) %>%
  set("leaves_pch", 19) %>% set("leaves_cex", 0.5) 
ggd1 <- as.ggdend(dend)
ggplot(ggd1, horiz = FALSE) + ggtitle("Dendrogram")

#Cluster is created by a set of neurons. 
cluster<-data.frame(cluster=som_cluster,neuron= 1:dim(codes)[2])
#Timeseries, neuron and cluster
cluster_info<-merge(cluster,info_timeSeries, by="neuron")
cluster_info[order(cluster_info$time_series),]
   neuron cluster time_series
5       2       1        t1_1
7       2       1       t1_10
20      5       1        t1_2
2       1       1        t1_3
3       1       1        t1_4
6       2       1        t1_5
1       1       1        t1_6
4       1       1        t1_7
8       2       1        t1_8
9       2       1        t1_9
15      4       2        t2_1
14      3       2       t2_10
11      3       2        t2_2
16      4       2        t2_3
10      3       2        t2_4
19      4       2        t2_5
13      3       2        t2_6
18      4       2        t2_7
12      3       2        t2_8
17      4       2        t2_9
25     12       4        t3_1
29     12       4       t3_10
30     12       4        t3_2
38     16       4        t3_3
28     12       4        t3_4
26     12       4        t3_5
27     12       4        t3_6
40     16       4        t3_7
31     12       4        t3_8
39     16       4        t3_9
24     10       3        t4_1
37     14       3       t4_10
22      9       3        t4_2
32     13       3        t4_3
35     14       3        t4_4
23     10       3        t4_5
21      9       3        t4_6
33     13       3        t4_7
36     14       3        t4_8
34     13       3        t4_9
#get the neurons that corresponds each group
neuron_group1<-which(som_cluster==1)
neuron_group2<-which(som_cluster==2)
neuron_group3<-which(som_cluster==3)
neuron_group4<-which(som_cluster==4)
#Paint neurons and plot som with groups separated by hierarchical clustering.
plot(som, type="codes",codeRendering="lines", bgcol = terrain.colors(4)[som_cluster], main = "Clusters")

plot(som, type="mapping",codeRendering="lines", bgcol = terrain.colors(4)[som_cluster], main = "Clusters") 
kohonen::add.cluster.boundaries(som, som_cluster)

#Plot the weight vector of neurons. 
#Are they similar with the time series set?
v1<-ggplot2::autoplot(codes[,neuron_group1], facet = NULL) + ylab("y") + geom_line(size = 1) 
v2<-ggplot2::autoplot(codes[,neuron_group2], facet = NULL) + ylab("y") + geom_line(size = 1) 
v3<-ggplot2::autoplot(codes[,neuron_group3], facet = NULL) + ylab("y") + geom_line(size = 1) 
v4<-ggplot2::autoplot(codes[,neuron_group4], facet = NULL) + ylab("y") + geom_line(size = 1) 
gridExtra::grid.arrange(v1, v2,v3, v4, ncol=2,nrow=2, top="Weight of Neurons")

#plot the time series that are grouping in same cluster
LS0tDQp0aXRsZTogIlRpbWUgU2VyaWVzIENsdXN0ZXJpbmcgVXNpbmcgU2VsZi1Pcmdhbml6aW5nIE1hcHMiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KU2ltaWxhcml0eSBEaXN0YW5jZXMNCg0KDQpgYGB7cn0NCmxpYnJhcnkoem9vKQ0KbGlicmFyeShrb2hvbmVuKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShkdHcpDQpgYGANCg0KYGBge3J9DQoNCiMgVHdvIHRpbWUgc2VyaWVzDQpYIDwtIGMoMSwxLDEsNCw0LDQsNCw0LDEsMSkNClkgPC0gYygxLDEsNCw0LDQsNCw0LDEsMSwxKQ0KDQojIEV1Y2xpZGVhbiBkaXN0YW5jZQ0KVFNkaXN0OjpEVFdEaXN0YW5jZShYLCBZKQ0KDQoNCiMgRXVjbGlkZWFuIGRpc3RhbmNlDQpUU2Rpc3Q6OkV1Y2xpZGVhbkRpc3RhbmNlKFgsIFkpDQoNCiMgRFRXIGRpc3RhbmNlDQphbGlnbm1lbnQ8LWR0dzo6ZHR3KFgsIFksIGtlZXAuaW50ZXJuYWxzID0gVFJVRSwgc3RlcD1zeW1tZXRyaWMxLCBrPVQpDQoNCnBsb3QoYWxpZ25tZW50KQ0KDQpwbG90KGFsaWdubWVudCx0eXBlPSJ0aHJlZXdheSIpDQoNCmFsaWdubWVudCRsb2NhbENvc3RNYXRyaXgNCmFsaWdubWVudCRkaXN0YW5jZQ0KYWxpZ25tZW50JHN0ZXBzVGFrZW4NCg0KcGxvdChhbGlnbm1lbnQkaW5kZXgxLGFsaWdubWVudCRpbmRleDIsbWFpbj0iV2FycGluZyBmdW5jdGlvbiIpOw0KDQpwbG90KGR0dzo6ZHR3KFgsIFksIGtlZXAuaW50ZXJuYWxzID0gVFJVRSwgc3RlcC5wYXR0ZXJuID1yYWJpbmVySnVhbmdTdGVwUGF0dGVybig2LCJjIikpLA0KICAgICB0eXBlPSJ0d293YXkiLG9mZnNldD0tMikNCg0KZHR3UGxvdFR3b1dheShkdHcoWCwgWSwgc3RlcCA9IGFzeW1tZXRyaWNQMSwga2VlcCA9IFQpKQ0KDQpyYWJpbmVySnVhbmdTdGVwUGF0dGVybig2LCJjIikNCnBsb3QocmFiaW5lckp1YW5nU3RlcFBhdHRlcm4oNiwiYyIpKQ0KDQojIFBsb3QNCnpvbzo6cGxvdC56b28oY2JpbmQoWCwgWSksIHBsb3QudHlwZSA9ICJtdWx0aXBsZSIsIGNvbCA9IGMoInJlZCIsICJibHVlIiksIGx3ZCA9IDIpDQoNCg0KI0NyZWF0ZXMgdGltZSBzZXJpZXMNCnRzMV94IDwtIGMoMSwyLDMsNCw1LDYsNyw4LDksMTApDQp0czFfeSA8LSBjKDEsMSwxLDQsNCw0LDQsNCwxLDEpDQp0czEgPC0gem9vOjp6b28odHMxX3ksIHRzMV94KTsNCg0KI0NyZWF0ZXMgdGltZSBzZXJpZXMNCnRzMl94IDwtIGMoMSwyLDMsNCw1LDYsNyw4LDksMTApDQp0czJfeSA8LSBjKDEsMSw0LDQsNCw0LDQsMSwxLDEpDQp0czIgPC0gem9vOjp6b28odHMxX3gsIHRzMV95KTsNCg0KYGBgDQpQbG90IHRpbWUgc2VyaWVzDQpgYGB7cn0NCnpvbzo6cGxvdC56b28oY2JpbmQodHMyX3ksIHRzMV95KSwgcGxvdC50eXBlID0gIm11bHRpcGxlIiwgY29sID0gYygicmVkIiwgImJsdWUiKSwgbHdkID0gMikNCg0KVFNkaXN0OjpFdWNsaWRlYW5EaXN0YW5jZSh0czFfeSwgdHMyX3kpDQoNClRTZGlzdDo6RFRXRGlzdGFuY2UodHMxX3ksIHRzMl95KQ0KYGBgDQoNClJlYWQgdGhlIHRpbWUgc2VyaWVzIGZyb20gY3N2IGZpbGUgYW5kIHBsb3QgYWxsDQoNCmBgYHtyfQ0KI3JlYWQgdGltZSBzZXJpZXMgZnJvbSBmaWxlDQp0aW1lc2VyaWVzLnpvbyA8LSByZWFkLnpvbygidGltZXNlcmllcy5jc3YiLCBoZWFkZXIgPSBUUlVFLCBzZXAgPSAiLCIpDQoNCiN6b286OnBsb3Quem9vKHosIG1haW4gPSAiU2V0IG9mIHRpbWUgc2VyaWVzIikNCg0KI0RlZmluZSBhIGNvbG9yIGZvciBlYWNoIGdyb3VwICg0IGdyb3VwcykNCmNvbG9yczwtYygiI0ZGMDAwMEZGIiwiI0ZGMDAwMEZGIiwiI0ZGMDAwMEZGIiwiI0ZGMDAwMEZGIiwNCiAgICAgICAgICAiIzAwRkY5RkZGIiwiIzAwRkY5RkZGIiwiIzAwRkY5RkZGIiwiIzAwRkY5RkZGIiwNCiAgICAgICAgICAiIzAwOUZGRkZGIiwiIzAwOUZGRkZGIiwiIzAwOUZGRkZGIiwiIzAwOUZGRkZGIiwNCiAgICAgICAgICAiI0RGRkYwMEZGIiwiI0RGRkYwMEZGIiwiI0RGRkYwMEZGIiwiI0RGRkYwMEZGIikNCmBgYA0KDQoNCmBgYHtyfQ0KDQojUGxvdCBhbGwgdGltZSBzZXJpZXMNCnpvbzo6cGxvdC56b28odGltZXNlcmllcy56b28sIHBsb3QudHlwZSA9ICJtdWx0aXBsZSIsIGNvbCA9IGNvbG9ycywgbHdkID0gMiwgbWFpbj0gIlNldCBvZiB0aW1lIHNlcmllcyIpDQoNCmBgYA0KDQpTZWxmLW9yZ2FuaXppbmcgbWFwcyB1c2luZyBLb2hvbmVuIHBhY2thZ2UNCmBgYHtyfQ0KIyBTT00NCnNvbWdyaWQ9a29ob25lbjo6c29tZ3JpZCh4ZGltPTQsIHlkaW09NCwgdG9wbz0icmVjdGFuZ3VsYXIiLCBuZWlnaGJvdXJob29kLmZjdCA9ICJnYXVzc2lhbiIpDQpzb208LWtvaG9uZW46OnN1cGVyc29tKHQoYXMubWF0cml4KHRpbWVzZXJpZXMuem9vKSksc29tZ3JpZCxybGVuID0gMTAwLCAgYWxwaGE9MSkNCg0KDQpgYGANCg0KDQpgYGB7cn0NCnBsb3Qoc29tLCJjb2RlcyIsY29kZVJlbmRlcmluZz0ibGluZXMiKQ0KYGBgDQpUaGUgdGltZSBzZXJpZXMgd2VyZSBhbGxvY2F0ZWQgaW4gdGhlc2UgbmV1cm9ucy4NCkVhY2ggbnVtYmVyIHJlcHJlc2VudHMgdGhlIG51bWJlciBvZiBuZXVyb24uDQoNCkV4YW1wbGU6IHRoZSBwb3NpdGlvbiAxIG9mIHZlY3RvciByZXByZXNlbnRzIHRoZSB0aW1lIHNlcmllcyAxIGluIGEgZGF0YXNldCwgdGhpcyB0aW1lIHNlcmllcw0Kd2FzIGFsbG9jYXRlZCBpbiBuZXVyb24gOS4NCg0KYGBge3J9DQpuZXVyb25fdGltZXNlcmllczwtc29tJHVuaXQuY2xhc3NpZg0KDQppbmZvX3RpbWVTZXJpZXM8LSBkYXRhLmZyYW1lKHRpbWVfc2VyaWVzPShyb3duYW1lcyh0KHRpbWVzZXJpZXMuem9vKSkpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmV1cm9uPWFzLmludGVnZXIoc29tJHVuaXQuY2xhc3NpZikpDQogICAgICAgICAgICAgICAgICAgICAgICANCmNvZGVzPC16b286Onpvbyh0KHNvbSRjb2Rlc1tbMV1dKSkNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDQpgYGANCg0KDQpgYGB7cn0NCg0Kem9vOjpwbG90Lnpvbyhjb2RlcywgcGxvdC50eXBlID0gIm11bHRpcGxlIiwgY29sID0gImJsYWNrIiwgbHdkID0gMiwgbWFpbj0gIldlaWdodCBvZiBuZXVyb25zIikNCiNwbG90IGFsbCB0aW1lIHNlcmllcyAobmV1cm9ucykNCmdncGxvdDI6OmF1dG9wbG90KChjb2RlcyksIGZhY2V0ID0gTlVMTCkgKyB5bGFiKCJ5IikgK3hsYWIoInRpbWUiKSArIGdlb21fbGluZShzaXplID0gMSkgKyBnZ3RpdGxlKCJXZWlnaHQgb2YgbmV1cm9ucyIpDQoNCg0KDQpsaWJyYXJ5KHByb3h5KQ0KbGlicmFyeShzdGF0cykNCg0KIyMgdXNlIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIHRvIGNsdXN0ZXIgdGhlIGNvZGVib29rIHZlY3RvcnMuDQojIEN1dCBkZW5kcm9ncmFtIGluIDQgZ3JvdXBzDQpoYyA8LSBzdGF0czo6aGNsdXN0KGRpc3QodChjb2RlcykpLG1ldGhvZCA9ICJ3YXJkLkQyIikNCg0KI0N1dHMgYSB0cmVlLCBlLmcuLCBhcyByZXN1bHRpbmcgZnJvbSBoY2x1c3QsIGludG8gc2V2ZXJhbCBncm91cHMgZWl0aGVyIA0KI2J5IHNwZWNpZnlpbmcgdGhlIGRlc2lyZWQgbnVtYmVyKHMpIG9mIGdyb3VwcyBvciB0aGUgY3V0IGhlaWdodChzKS4NCnNvbV9jbHVzdGVyIDwtIHN0YXRzOjpjdXRyZWUoaGMsIDQpDQoNCmxpYnJhcnkoZGVuZGV4dGVuZCkNCiNwbG90IGRlbmRyb2dyYW0NCmRlbmQgPC0gaGMlPiUgYXMuZGVuZHJvZ3JhbSAlPiUNCiAgc2V0KCJicmFuY2hlc19rX2NvbG9yIiwgayA9IDQpICU+JSBzZXQoImJyYW5jaGVzX2x3ZCIsIDEuMikgJT4lDQogIHNldCgibGFiZWxzX2NleCIsIDAuOCkgJT4lIHNldCgibGFiZWxzX2NvbG9ycyIsIGsgPSA0KSAlPiUNCiAgc2V0KCJsZWF2ZXNfcGNoIiwgMTkpICU+JSBzZXQoImxlYXZlc19jZXgiLCAwLjUpIA0KZ2dkMSA8LSBhcy5nZ2RlbmQoZGVuZCkNCmdncGxvdChnZ2QxLCBob3JpeiA9IEZBTFNFKSArIGdndGl0bGUoIkRlbmRyb2dyYW0iKQ0KDQojQ2x1c3RlciBpcyBjcmVhdGVkIGJ5IGEgc2V0IG9mIG5ldXJvbnMuIA0KY2x1c3RlcjwtZGF0YS5mcmFtZShjbHVzdGVyPXNvbV9jbHVzdGVyLG5ldXJvbj0gMTpkaW0oY29kZXMpWzJdKQ0KDQojVGltZXNlcmllcywgbmV1cm9uIGFuZCBjbHVzdGVyDQpjbHVzdGVyX2luZm88LW1lcmdlKGNsdXN0ZXIsaW5mb190aW1lU2VyaWVzLCBieT0ibmV1cm9uIikNCmNsdXN0ZXJfaW5mb1tvcmRlcihjbHVzdGVyX2luZm8kdGltZV9zZXJpZXMpLF0NCg0KDQojZ2V0IHRoZSBuZXVyb25zIHRoYXQgY29ycmVzcG9uZHMgZWFjaCBncm91cA0KbmV1cm9uX2dyb3VwMTwtd2hpY2goc29tX2NsdXN0ZXI9PTEpDQpuZXVyb25fZ3JvdXAyPC13aGljaChzb21fY2x1c3Rlcj09MikNCm5ldXJvbl9ncm91cDM8LXdoaWNoKHNvbV9jbHVzdGVyPT0zKQ0KbmV1cm9uX2dyb3VwNDwtd2hpY2goc29tX2NsdXN0ZXI9PTQpDQoNCiNQYWludCBuZXVyb25zIGFuZCBwbG90IHNvbSB3aXRoIGdyb3VwcyBzZXBhcmF0ZWQgYnkgaGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcuDQpwbG90KHNvbSwgdHlwZT0iY29kZXMiLGNvZGVSZW5kZXJpbmc9ImxpbmVzIiwgYmdjb2wgPSB0ZXJyYWluLmNvbG9ycyg0KVtzb21fY2x1c3Rlcl0sIG1haW4gPSAiQ2x1c3RlcnMiKQ0KcGxvdChzb20sIHR5cGU9Im1hcHBpbmciLGNvZGVSZW5kZXJpbmc9ImxpbmVzIiwgYmdjb2wgPSB0ZXJyYWluLmNvbG9ycyg0KVtzb21fY2x1c3Rlcl0sIG1haW4gPSAiQ2x1c3RlcnMiKSANCmtvaG9uZW46OmFkZC5jbHVzdGVyLmJvdW5kYXJpZXMoc29tLCBzb21fY2x1c3RlcikNCg0KI1Bsb3QgdGhlIHdlaWdodCB2ZWN0b3Igb2YgbmV1cm9ucy4gDQojQXJlIHRoZXkgc2ltaWxhciB3aXRoIHRoZSB0aW1lIHNlcmllcyBzZXQ/DQp2MTwtZ2dwbG90Mjo6YXV0b3Bsb3QoY29kZXNbLG5ldXJvbl9ncm91cDFdLCBmYWNldCA9IE5VTEwpICsgeWxhYigieSIpICsgZ2VvbV9saW5lKHNpemUgPSAxKSANCnYyPC1nZ3Bsb3QyOjphdXRvcGxvdChjb2Rlc1ssbmV1cm9uX2dyb3VwMl0sIGZhY2V0ID0gTlVMTCkgKyB5bGFiKCJ5IikgKyBnZW9tX2xpbmUoc2l6ZSA9IDEpIA0KdjM8LWdncGxvdDI6OmF1dG9wbG90KGNvZGVzWyxuZXVyb25fZ3JvdXAzXSwgZmFjZXQgPSBOVUxMKSArIHlsYWIoInkiKSArIGdlb21fbGluZShzaXplID0gMSkgDQp2NDwtZ2dwbG90Mjo6YXV0b3Bsb3QoY29kZXNbLG5ldXJvbl9ncm91cDRdLCBmYWNldCA9IE5VTEwpICsgeWxhYigieSIpICsgZ2VvbV9saW5lKHNpemUgPSAxKSANCmdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlKHYxLCB2Mix2MywgdjQsIG5jb2w9Mixucm93PTIsIHRvcD0iV2VpZ2h0IG9mIE5ldXJvbnMiKQ0KDQoNCiNwbG90IHRoZSB0aW1lIHNlcmllcyB0aGF0IGFyZSBncm91cGluZyBpbiBzYW1lIGNsdXN0ZXINCg0KDQoNCmBgYA0KDQoNCg==
</html>


Navigation