packages = c(
  "dplyr","ggplot2","googleVis","devtools","magrittr","slam","irlba","plotly",
  "arules","arulesViz","Matrix","recommenderlab")
existing = as.character(installed.packages()[,1])
for(pkg in packages[!(packages %in% existing)]) install.packages(pkg)
rm(list=ls(all=TRUE))
LOAD = FALSE
library(dplyr)
library(ggplot2)
library(googleVis)
library(Matrix)
library(slam)
library(irlba)
library(plotly)
library(arules)
library(arulesViz)
library(recommenderlab)


A. 顧客產品矩陣

Load data frame and rename

load("data/tf0.rdata")
A = A0; X = X0; Z = Z0; rm(A0,X0,Z0); gc()
           used  (Mb) gc trigger  (Mb)  max used (Mb)
Ncells  3211456 171.6    9601876 512.8  12002346  641
Vcells 16727491 127.7  105458009 804.6 228211284 1741
Z = subset(Z, cust %in% A$cust)
n_distinct(Z$cust)  # 32241
[1] 32241
n_distinct(Z$prod)  # 23787
[1] 23787

製作顧客產品矩陣其實很快、也很容易

library(Matrix)
library(slam)
cpm = xtabs(~ cust + prod, Z, sparse=T)  # customer product matrix
dim(cpm)             # 32241 23787
[1] 32241 23787
mean(cpm > 0)        # 0.00096799
[1] 0.0009674

顧客產品矩陣通常是一個很稀疏的矩,陣有一些產品沒什麼人買

colSums(cpm) %>% quantile(seq(0,1,0.1))
  0%  10%  20%  30%  40%  50%  60%  70%  80%  90% 100% 
   1    1    2    4    6    8   13   20   35   76 8475 
mean(colSums(cpm) > 10)
[1] 0.4484

刪去購買次數小於6的產品;為了方便顧客產品矩陣和顧客資料框的合併,我們選擇先保留沒有購買產品的顧客

cpm = cpm[, colSums(cpm) >= 6]      # remove the least frequent products
# cpm = cpm[rowSums(cpm) > 0, ]     # remove non-buying customers
cpm = cpm[, order(-colSums(cpm))]   # order product by frequency
dim(cpm)                            # 32241 23787>14621
[1] 32241 14621
max(cpm)         # 49
[1] 49
mean(cpm > 0)    # 0.0015248
[1] 0.001525
table(cpm@x) %>% prop.table %>% round(4) %>% head(10)

     1      2      3      4      5      6      7      8      9     10 
0.9256 0.0579 0.0108 0.0032 0.0012 0.0006 0.0003 0.0002 0.0001 0.0001 
# X單筆交易紀錄彙整

請你用一個指令列出被購買最多次的10個產品,和它們被購買的次數。

colSums(cpm)%>% head(10)
4714981010038 4711271000014 4719090900065 4711080010112 4710114128038 
         8475          6119          2444          2249          2178 
4710265849066 4713985863121 4710088410139 4710583996008 4710908131589 
         2017          1976          1869          1840          1679 

■ 在什麼前提之下,我們可以把購買這十個產品的次數當作變數,用來預測顧客在下一期會不會來購買呢?
1.no
2.這10個產品是依照購買次數但不一定能對顧客下次是否能來購買產生解釋力。

■ 我們如何把這十個變數,併入顧客資料框呢?
1.從Z資料中,找出每位顧客購買Top10產品的次數
2.再以CID併入資料集A中

■ 我們可不可以(在什麼前提之下我們可以)直接用cbind()新變數併入顧客資料框呢?
1.cbind()把矩陣橫向合併成一個大矩陣(列方式)
2.可以。前提是,row數目要相同且二個資料框cid 的次序要相同。

■ 我們期中競賽的資料,符合直接用cbind()併入新變數的條件嗎? 我們要如何確認這一件事呢?
1.符合。
2.要確認資料框row的次序要相同。




B. 直接以產品的被購買頻率作為變數

以產品的被購買頻率製作(顧客)變數的時候,cpm在最前邊的(N個)欄+位就是變數!

B1. 以(最常被購買的)產品的購買次數對顧客分群
nop= 400  # no. product = no. variables
k = 200   # no. cluster
set.seed(111); kg = kmeans(cpm[,1:nop], k)$cluster
table(kg) %>% as.vector %>% sort
  [1]     1     1     1     1     1     1     1     1     1     1     1
 [12]     1     1     1     1     1     1     1     1     1     1     1
 [23]     2     2     2     2     2     2     3     3     3     3     3
 [34]     3     3     4     4     4     4     4     4     4     5     5
 [45]     6     6     6     6     7     7     7     8     8     8     9
 [56]     9     9     9    10    10    10    10    11    11    11    11
 [67]    11    12    13    13    15    15    15    16    16    18    19
 [78]    20    20    20    20    21    22    22    22    24    24    25
 [89]    25    27    28    28    32    32    35    36    39    40    41
[100]    42    44    45    46    47    47    48    49    49    50    51
[111]    52    53    56    58    58    61    63    66    67    68    69
[122]    69    72    81    85    85    86    87    90    94    96    97
[133]    97   100   100   101   110   111   113   114   116   118   123
[144]   123   126   130   134   136   141   141   142   143   162   165
[155]   172   175   178   179   182   182   184   187   195   210   222
[166]   225   228   228   237   239   242   253   254   258   258   266
[177]   268   272   287   293   301   311   325   329   350   351   363
[188]   396   407   407   410   418   432   448   473   523   561  1156
[199]  1266 11215
B2. 各群組平均屬性

將分群結果併入顧客資料框(A)

df = A %>% inner_join(data.frame(
  cust = as.integer(rownames(cpm)), 
  kg) )
Joining, by = "cust"
head(df)  # 32241

計算各群組的平均屬性

plot( gvisMotionChart(
  subset(df[,c(1,4,5,6,8,2,3,7,9)], 
         size >= 20 & size <= 1000),  # range of group size 
  "kg", "dummy", options=list(width=800, height=600) ) )
B3. 互動式泡泡圖
plot( gvisMotionChart(
  subset(df[,c(1,4,5,6,8,2,3,7,9)], 
         size >= 20 & size <= 1000),  # range of group size 
  "kg", "dummy", options=list(width=800, height=600) ) )
B4. 各群組的代表性產品 (Signature Product)
# use global variables: cpm, kg
Sig = function(gx, P=1000, H=10) {
  print(sprintf("Group %d: No. Customers = %d", gx, sum(kg==gx)))
  bx = cpm[,1:P]
  data.frame(n = col_sums(bx[kg==gx,])) %>%      # frequency
    mutate(
      share = round(100*n/col_sums(bx),2),       # %prod sold to this cluster (%prod已售出此群集)
      conf = round(100*n/sum(kg==gx),2),         # %buy this product, given cluster (%購買此產品,給定群集)
      base = round(100*col_sums(bx)/nrow(bx),2), # %buy this product, all cust (%購買此產品,所有cust
      lift = round(conf/base,1),                 # conf/base  
      name = colnames(bx)                        # name of prod # prod的名稱
    ) %>% arrange(desc(lift)) %>% head(H)
  }
Sig(130)
[1] "Group 130: No. Customers = 97"


C. 使用尺度縮減方法抽取顧客(產品)的特徵向量

C1. 巨大尺度縮減 (SVD, Sigular Value Decomposition)
library(irlba)
if(LOAD) {
  load("data/svd2a.rdata")
} else {
  smx = cpm
  smx@x = pmin(smx@x, 2)            # cap at 2, similar to normalization  
  t0 = Sys.time()
  svd = irlba(smx, 
              nv=400,               # length of feature vector
              maxit=800, work=800)    
  print(Sys.time() - t0)            # 1.8795 mins
  save(svd, file = "data/svd2a.rdata")
}
Time difference of 1.765 mins

■ 在什麼前提之下,我們可以把顧客購買產品的特徵向量當作變數,用來預測顧客在下一期會不會來購買呢?
1.可以。
2.如果要將產品的特徵向量(X)去預測(Y),(X)必須要對預測(Y)有意義,如X與Y要有相關性、預測力或影響力。

■ 如果可以的話,我們如何把顧客購買產品的特徵向併入顧客資料框呢?
1.從svd資料中,找出每位顧客對於400個產品的特徵向量
2.再以CID併入資料集A中

■ 我們可不可以(在什麼前提之下我們可以)直接用cbind()將特徵向量併入顧客資料框呢?
1.cbind()把矩陣橫向合併成一個大矩陣(列方式)
2.可以。前提是,row數目要相同且二個資料框cid 的次序要相同。

■ 我們期中競賽的資料,符合直接用cbind()併入特徵向量的條件嗎? 我們要如何確認這一件事呢?
1.符合。
2.要確認資料框row的次序要相同。




C2. 依特徵向量對顧客分群
set.seed(111); kg = kmeans(svd$u, 200)$cluster
table(kg) %>% as.vector %>% sort
  [1]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [14]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [27]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [40]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [53]    1    1    1    1    1    2    2    2    2    2    3    4    4
 [66]    5    7   10   14   30   31   32   36   38   38   39   39   40
 [79]   40   41   44   45   46   47   49   54   59   62   62   69   71
 [92]   77   79   79   80   82   82   84   87   91  101  103  109  110
[105]  111  113  117  120  123  127  127  129  132  133  134  135  136
[118]  139  141  143  143  147  147  157  159  159  160  160  160  166
[131]  168  169  172  175  180  181  181  182  183  184  184  188  190
[144]  190  193  194  195  196  198  198  200  201  201  202  202  204
[157]  204  204  207  209  209  210  213  214  216  219  219  222  225
[170]  233  234  235  236  237  237  238  239  241  248  248  248  253
[183]  256  257  258  259  261  261  264  269  277  281  285  293  305
[196]  411  612  896 1092 8987
C3. 互動式泡泡圖 (Google Motion Chart)
# clustster summary 簇摘要
df = inner_join(A, data.frame(         
  cust = as.integer(rownames(cpm)), kg)) %>% 
  group_by(kg) %>% summarise(
    avg_frequency = mean(f),
    avg_monetary = mean(m),
    avg_revenue_contr = mean(rev),
    group_size = n(),
    avg_recency = mean(r), #近因
    avg_gross_profit = mean(raw)) %>% #毛利
  ungroup %>% 
  mutate(dummy = 2001, kg = sprintf("G%03d",kg)) %>%    
  data.frame
Joining, by = "cust"
# Google Motion Chart
plot( gvisMotionChart(
  subset(df, group_size >= 20 & group_size <= 1200),     
  "kg", "dummy", options=list(width=800, height=600) ) )
C4. 各群組的代表性產品 (Signature Product)
Sig(162) #代表性。G162平均購買頻率:11.5
[1] "Group 162: No. Customers = 87"


D. 購物籃分析 Baskets Analysis

n_distinct(Z$tid)
[1] 119407
n_distinct(Z$prod)
[1] 23787
D1. 準備資料 (for Association Rule Analysis)

購物籃分析會使用arules這個套件,它要用的資料很容易準備,直接使用交易項目資料,一行程式就可以搞定。

library(arules)
library(arulesViz)
bx = as(split(Z$prod, Z$tid), "transactions")  
D2. Top20 熱賣產品
itemFrequencyPlot(bx, topN=20, type="absolute", cex=0.8) #項目頻率圖

D3. 關聯規則和Apriori演算法

關聯規則(A => B)

  • support: A被購買的機率 (A的基礎機率)
  • confidence: A被購買時,B被購買的機率
  • lift: A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)
  • 一般來講support、confidence和lift越高的關聯規則越重要
  • support、confidence和lift設的越低(高),找到的關聯規則越多(少)
  • 建議一開始把標準設低,先找到多一點規則,之後再用subset篩選出特定的規則來看
rules = apriori(bx, parameter=list(supp=0.002, conf=0.6)) 
Apriori

Parameter specification:
 confidence minval smax arem  aval originalSupport maxtime support
        0.6    0.1    1 none FALSE            TRUE       5   0.002
 minlen maxlen target   ext
      1     10  rules FALSE

Algorithmic control:
 filter tree heap memopt load sort verbose
    0.1 TRUE TRUE  FALSE TRUE    2    TRUE

Absolute minimum support count: 238 

set item appearances ...[0 item(s)] done [0.00s].
set transactions ...[23787 item(s), 119407 transaction(s)] done [0.16s].
sorting and recoding items ... [569 item(s)] done [0.01s].
creating transaction tree ... done [0.07s].
checking subsets of size 1 2 3 done [0.01s].
writing ... [14 rule(s)] done [0.00s].
creating S4 object  ... done [0.03s].
summary(rules)
set of 14 rules

rule length distribution (lhs + rhs):sizes
2 3 
8 6 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   2.00    2.00    2.00    2.43    3.00    3.00 

summary of quality measures:
    support          confidence         lift           count    
 Min.   :0.00210   Min.   :0.613   Min.   : 48.1   Min.   :251  
 1st Qu.:0.00251   1st Qu.:0.680   1st Qu.: 55.1   1st Qu.:300  
 Median :0.00283   Median :0.727   Median : 58.6   Median :338  
 Mean   :0.00321   Mean   :0.724   Mean   : 85.2   Mean   :383  
 3rd Qu.:0.00333   3rd Qu.:0.774   3rd Qu.: 97.9   3rd Qu.:398  
 Max.   :0.00585   Max.   :0.809   Max.   :170.9   Max.   :699  

mining info:
 data ntransactions support confidence
   bx        119407   0.002        0.6
D4. 檢視關聯規則

關聯規則 (A => B):

  • support: A被購買的機率 (A的基礎機率)
  • confidence: A被購買時,B被購買的機率
  • lift: A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)
options(digits=4)  
inspect(rules)  
     lhs                              rhs             support 
[1]  {4719090790017}               => {4719090790000} 0.002940
[2]  {4719090790000}               => {4719090790017} 0.002940
[3]  {719859796124}                => {719859796117}  0.002102
[4]  {4710011402026}               => {4710011402019} 0.002822
[5]  {4710085120697}               => {4710085120680} 0.003467
[6]  {4710011409056}               => {4710011401128} 0.004430
[7]  {4710011401135}               => {4710011401128} 0.005854
[8]  {4710011405133}               => {4710011401128} 0.005176
[9]  {4710011401135,4710011409056} => {4710011401128} 0.002713
[10] {4710011401128,4710011409056} => {4710011401135} 0.002713
[11] {4710011405133,4710011409056} => {4710011401128} 0.002261
[12] {4710011401135,4710011405133} => {4710011401128} 0.002831
[13] {4710011401135,4710011406123} => {4710011401128} 0.002445
[14] {4710011405133,4710011406123} => {4710011401128} 0.002219
     confidence lift   count
[1]  0.8088     170.92 351  
[2]  0.6212     170.92 351  
[3]  0.6972     147.61 251  
[4]  0.6740      90.22 337  
[5]  0.7753     100.41 414  
[6]  0.6988      51.04 529  
[7]  0.7524      54.95 699  
[8]  0.6588      48.12 618  
[9]  0.8020      58.57 324  
[10] 0.6125      78.72 324  
[11] 0.7606      55.55 270  
[12] 0.7717      56.36 338  
[13] 0.8022      58.59 292  
[14] 0.7011      51.20 265  
#lift:A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)
#lhs(left hand side)和rhs(right hand side),表示左操作数右操作数
D5. 互動圖表顯示

有互動圖表的幫助,我們可以把條件放寬,多找一些關聯規則進來

rules = apriori(bx, parameter=list(supp=0.0003, conf=0.5)) 
Apriori

Parameter specification:
 confidence minval smax arem  aval originalSupport maxtime support
        0.5    0.1    1 none FALSE            TRUE       5  0.0003
 minlen maxlen target   ext
      1     10  rules FALSE

Algorithmic control:
 filter tree heap memopt load sort verbose
    0.1 TRUE TRUE  FALSE TRUE    2    TRUE

Absolute minimum support count: 35 

set item appearances ...[0 item(s)] done [0.00s].
set transactions ...[23787 item(s), 119407 transaction(s)] done [0.16s].
sorting and recoding items ... [4691 item(s)] done [0.02s].
creating transaction tree ... done [0.07s].
checking subsets of size 1 2 3 4 5 6
Mining stopped (maxlen reached). Only patterns up to a length of 6 returned!
 done [0.22s].
writing ... [581 rule(s)] done [0.04s].
creating S4 object  ... done [0.02s].
plot(rules,colors=c("red","green"),engine="htmlwidget",
     marker=list(opacity=.6,size=8))
To reduce overplotting, jitter is added! Use jitter = 0 to prevent jitter.
plot(rules,method="matrix",shading="lift",engine="htmlwidget",
     colors=c("red", "green"))
D6. 篩選產品、互動式關聯圖
r1 = subset(rules, subset = rhs %in% c("93362993"))
plot(r1,method="graph",engine="htmlwidget",itemCol="cyan") 
  • 泡泡大小:support: A被購買的機率 (A的基礎機率)
  • 泡泡顏色:lift: A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)


E. 產品推薦 Product Recommendation

E1. 篩選顧客、產品

太少被購買的產品和購買太少產品的顧客都不適合使用Collaborative Filtering這種產品推薦方法,所以我們先對顧客和產品做一次篩選

library(recommenderlab)
rx = cpm[, colSums(cpm > 0) >= 50]
rx = rx[rowSums(rx > 0) >= 20 & rowSums(rx > 0) <= 300, ]
dim(rx)  # 8846 3354
[1] 8846 3354
E2. 選擇產品評分方式

可以選擇要用

  • 購買次數 (realRatingMatrix) 或
  • 是否購買 (binaryRatingMatrix)做模型。
rx = as(rx, "realRatingMatrix")  # realRatingMatrix 真正的評級矩陣
bx = binarize(rx, minRating=1)   # binaryRatingMatrix 二元評級矩陣
E3. 建立模型、產生建議 - UBCF

UBCF:User Based Collaborative Filtering

rUBCF = Recommender(bx[1:8800,], method = "UBCF")
pred = predict(rUBCF, bx[8801:8846,], n=4)
do.call(rbind, as(pred, "list")) %>% head(15)
        [,1]            [,2]            [,3]            [,4]           
2170855 "4711271000014" "4710114128038" "4714981010038" "4713985863121"
2171265 "4719090900065" "4710254049521" "4710036008562" "4714981010038"
2171340 "723125488040"  "723125488064"  "723125485032"  "4714981010038"
2171425 "4710011401135" "4710011409056" "4711080010112" "4710011401142"
2171432 "4714981010038" "4710011406123" "4711258007371" "4710011401128"
2171555 "4719090900065" "4711271000014" "37000329169"   "4710943109352"
2171883 "4711271000014" "4710583996008" "4710291112172" "4710018004704"
2172194 "4711271000014" "4714981010038" "4710114128038" "4710114105046"
2172392 "4903111345717" "4710908131589" "4710168705056" "4711271000014"
2172569 "4711271000014" "4714981010038" "4710128030037" "4712162000038"
2172583 "4714981010038" "4710085120093" "4719090900065" "4710154015206"
2172590 "4710011406123" "4710011401142" "4710857000028" "4710011432856"
2172668 "4711271000014" "4710088620156" "4719090900065" "4712425010712"
2172705 "4711271000014" "4714981010038" "37000445111"   "37000440192"  
2172811 "4714981010038" "37000442127"   "4710719000333" "4710114128038"

UBCF:User Based Collaborative Filtering /基於用戶的協同過濾

E4. 建立模型、產生建議 - IBCF

IBCF:Item Based Collaborative Filtering

if(LOAD) {
  load("data/recommenders.rdata")
} else{
  rIBCF <- Recommender(bx[1:6000,], method = "IBCF")
}
pred = predict(rIBCF, bx[8801:8846,], n=4)
do.call(rbind, as(pred, "list")) %>% head(15)
        [,1]            [,2]            [,3]            [,4]           
2170855 "4719090900065" "4714981010038" "4711271000014" "4712162000038"
2171265 "4719090900065" "4714981010038" "4711271000014" "4712162000038"
2171340 "37000445111"   "723125488064"  "4710036008586" "723125485032" 
2171425 "4711311617899" "4711311218836" "4710011401135" "4710011409056"
2171432 "4714981010038" "4710857000042" "4710321791698" "2250078000350"
2171555 "93432641"      "93362993"      "4711271000014" "4713985863121"
2171883 "4710670200100" "4710670200407" "4711271000014" "4719581980248"
2172194 "4714108700019" "4714108700064" "4902430493437" "4909978199111"
2172392 "4710908131589" "4710706211759" "4719090900058" "4712425010712"
2172569 "4711371850243" "84501293529"   "4710085121007" "4710375112210"
2172583 "4710085120093" "4710734001186" "4710085172702" "34000231508"  
2172590 "4710011406123" "4711271000014" "4711437000162" "4710777110265"
2172668 "4711371850243" "4710126020474" "4719090900065" "723125485032" 
2172705 "37000445111"   "37000304593"   "37000442127"   "37000441809"  
2172811 "4719581980293" "4719581980279" "4712067899287" "4710908131589"

IBCF:Item Based Collaborative Filtering /基於產品項目的協同過濾

save(rIBCF, rUBCF, file="data/recommenders.rdata")
E5. 設定模型(準確性)驗證方式
set.seed(4321)
scheme = evaluationScheme(     
  bx, method="split", train = .75,  given=5)
E6. 設定推薦方法(參數)
algorithms = list(            
  AR53 = list(name="AR", param=list(support=0.0005, confidence=0.3)),
  AR43 = list(name="AR", param=list(support=0.0004, confidence=0.3)),
  RANDOM = list(name="RANDOM", param=NULL),
  POPULAR = list(name="POPULAR", param=NULL),
  UBCF = list(name="UBCF", param=NULL),
  IBCF = list(name="IBCF", param=NULL) )
E7. 建模、預測、驗證(準確性)
if(LOAD) {
  load("data/results2a.rdata")
} else {
  t0 = Sys.time()
  results = evaluate(            
    scheme, algorithms, 
    type="topNList",     # method of evaluation  評估方法
    n=c(5, 10, 15, 20)   # no. recom. to be evaluated  #RECOM待評估
    )
  print(Sys.time() - t0)
  save(results, file="data/results2a.rdata")
}
AR run fold/sample [model time/prediction time]
     1  [3.64sec/173.4sec] 
AR run fold/sample [model time/prediction time]
     1  [8.54sec/445.6sec] 
RANDOM run fold/sample [model time/prediction time]
     1  [0.01sec/10.62sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.02sec/9.63sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0sec/60.53sec] 
IBCF run fold/sample [model time/prediction time]
     1  [159.5sec/1.44sec] 
Time difference of 14.71 mins
## AR run fold/sample [model time/prediction time]
##   1  [4.02sec/214.6sec] 
## AR run fold/sample [model time/prediction time]
##   1  [10.49sec/538.5sec] 
## RANDOM run fold/sample [model time/prediction time]
##   1  [0sec/9.48sec] 
## POPULAR run fold/sample [model time/prediction time]
##   1  [0sec/11.09sec] 
## UBCF run fold/sample [model time/prediction time]
##   1  [0sec/75.42sec] 
## IBCF run fold/sample [model time/prediction time]
##   1  [198.2sec/1.63sec] 
## Time difference of 18.72 mins  #時差52.66分鐘
E8. 模型準確性比較
# load("data/results.rdata")
par(mar=c(4,4,3,2),cex=0.8)
cols = c("red", "magenta", "gray", "orange", "blue", "green")
plot(results, annotate=c(1,3), legend="topleft", pch=19, lwd=2, col=cols)
abline(v=seq(0,0.006,0.001), h=seq(0,0.08,0.01), col='lightgray', lty=2)

getConfusionMatrix(results$IBCF)
[[1]]
      TP     FP    FN   TN precision  recall     TPR      FPR
5  1.116  3.884 32.97 3311    0.2231 0.03899 0.03899 0.001171
10 1.699  8.301 32.39 3307    0.1699 0.05812 0.05812 0.002503
15 2.075 12.925 32.01 3302    0.1383 0.07021 0.07021 0.003898
20 2.385 17.615 31.70 3297    0.1193 0.08002 0.08002 0.005313






LS0tDQp0aXRsZTogIlByb2R1Y3QoMmEp77ya55Si5ZOB6Yq35ZSu6LOH6KiKIg0KYXV0aG9yOiAiR3JvdXAyLCAyMDE4LzA4LzE1Ig0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KPGJyPg0KDQpgYGB7cn0NCnBhY2thZ2VzID0gYygNCiAgImRwbHlyIiwiZ2dwbG90MiIsImdvb2dsZVZpcyIsImRldnRvb2xzIiwibWFncml0dHIiLCJzbGFtIiwiaXJsYmEiLCJwbG90bHkiLA0KICAiYXJ1bGVzIiwiYXJ1bGVzVml6IiwiTWF0cml4IiwicmVjb21tZW5kZXJsYWIiKQ0KZXhpc3RpbmcgPSBhcy5jaGFyYWN0ZXIoaW5zdGFsbGVkLnBhY2thZ2VzKClbLDFdKQ0KZm9yKHBrZyBpbiBwYWNrYWdlc1shKHBhY2thZ2VzICVpbiUgZXhpc3RpbmcpXSkgaW5zdGFsbC5wYWNrYWdlcyhwa2cpDQpgYGANCg0KYGBge3Igd2FybmluZz1GLCBtZXNzYWdlPUYsIGNhY2hlPUYsIGVycm9yPUZ9DQpybShsaXN0PWxzKGFsbD1UUlVFKSkNCkxPQUQgPSBGQUxTRQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoZ29vZ2xlVmlzKQ0KbGlicmFyeShNYXRyaXgpDQpsaWJyYXJ5KHNsYW0pDQpsaWJyYXJ5KGlybGJhKQ0KbGlicmFyeShwbG90bHkpDQpsaWJyYXJ5KGFydWxlcykNCmxpYnJhcnkoYXJ1bGVzVml6KQ0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikNCmBgYA0KPGJyPjxocj4NCg0KIyMjIEEuIOmhp+WuoueUouWTgeefqemZow0KTG9hZCBkYXRhIGZyYW1lIGFuZCByZW5hbWUNCg0KKyBa57i95Lqk5piT57SA6YyEDQorIFjllq7nrYbkuqTmmJPntIDpjITlvZnmlbQNCisgQeacg+WToeS6pOaYk+e0gOmMhA0KKyBS5Lit5o+Q5L6b55qE5rWL6YeP5pmC6ZaT5pyA566A5Y2V55qE5pa55rOV5pivc3lzdGVtLnRpbWXlh73mlbDjgIINCisgc3lzdGVtLnRpbWUoZXhwciwgZ2NGaXJzdD1UUlVFKQ0KDQpgYGB7cn0NCmxvYWQoImRhdGEvdGYwLnJkYXRhIikNCkEgPSBBMDsgWCA9IFgwOyBaID0gWjA7IHJtKEEwLFgwLFowKTsgZ2MoKQ0KWiA9IHN1YnNldChaLCBjdXN0ICVpbiUgQSRjdXN0KQ0KDQpgYGANCisgc2Vk77ya5piv55W25YmN5L2/55So5oOF5Ya1DQorIGdjIHRyaWdnZXLvvJrmmK/kvJrop6blj5HlnoPlnL7lm57mlLbnmoTlgLwNCisgbWF4IHVzZWTmmK/kuIrmrKFnYygp5pON5L2c5oiW6ICF5piv5q2k5qyh5ZWf5YuVUuW+jO+8jOS9v+eUqOacgOWkp+WAvOOAgg0KKyAoTWIp5pivTmNlbGxz5ZKMVmNlbGxz55qE5aSn5bCP6L2J5o2i54K6TWLljZXkvY3mmYLnmoTlgLzjgIINCisgTmNlbGxz5Y2zY29ucyBjZWxscyANCisgVmNlbGxz5Y2zdmVjdG9yIGNlbGxzDQoNCmBgYHtyfQ0Kbl9kaXN0aW5jdChaJGN1c3QpICAjIDMyMjQxDQpuX2Rpc3RpbmN0KFokcHJvZCkgICMgMjM3ODcNCmBgYA0KDQroo73kvZzpoaflrqLnlKLlk4Hnn6npmaPlhbblr6blvojlv6vjgIHkuZ/lvojlrrnmmJMNCmBgYHtyfQ0KbGlicmFyeShNYXRyaXgpDQpsaWJyYXJ5KHNsYW0pDQpjcG0gPSB4dGFicyh+IGN1c3QgKyBwcm9kLCBaLCBzcGFyc2U9VCkgICMgY3VzdG9tZXIgcHJvZHVjdCBtYXRyaXgNCmRpbShjcG0pICAgICAgICAgICAgICMgMzIyNDEgMjM3ODcNCm1lYW4oY3BtID4gMCkgICAgICAgICMgMC4wMDA5Njc5OQ0KYGBgDQoNCumhp+WuoueUouWTgeefqemZo+mAmuW4uOaYr+S4gOWAi+W+iOeogOeWj+eahOefqe+8jOmZo+acieS4gOS6m+eUouWTgeaykuS7gOm6vOS6uuiytw0KYGBge3J9DQpjb2xTdW1zKGNwbSkgJT4lIHF1YW50aWxlKHNlcSgwLDEsMC4xKSkNCm1lYW4oY29sU3VtcyhjcG0pID4gMTApDQpgYGANCg0K5Yiq5Y676LO86LK35qyh5pW45bCP5pa8NueahOeUouWTge+8m+eCuuS6huaWueS+v+mhp+WuoueUouWTgeefqemZo+WSjOmhp+Wuouizh+aWmeahhueahOWQiOS9te+8jOaIkeWAkemBuOaTh+WFiOS/neeVmeaykuacieizvOiyt+eUouWTgeeahOmhp+Wuog0KYGBge3J9DQpjcG0gPSBjcG1bLCBjb2xTdW1zKGNwbSkgPj0gNl0gICAgICAjIHJlbW92ZSB0aGUgbGVhc3QgZnJlcXVlbnQgcHJvZHVjdHMNCiMgY3BtID0gY3BtW3Jvd1N1bXMoY3BtKSA+IDAsIF0gICAgICMgcmVtb3ZlIG5vbi1idXlpbmcgY3VzdG9tZXJzDQpjcG0gPSBjcG1bLCBvcmRlcigtY29sU3VtcyhjcG0pKV0gICAjIG9yZGVyIHByb2R1Y3QgYnkgZnJlcXVlbmN5DQpkaW0oY3BtKSAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIDMyMjQxIDIzNzg3PjE0NjIxDQpgYGANCg0KYGBge3J9DQptYXgoY3BtKSAgICAgICAgICMgNDkNCm1lYW4oY3BtID4gMCkgICAgIyAwLjAwMTUyNDgNCnRhYmxlKGNwbUB4KSAlPiUgcHJvcC50YWJsZSAlPiUgcm91bmQoNCkgJT4lIGhlYWQoMTApDQoNCiMgWOWWruethuS6pOaYk+e0gOmMhOW9meaVtA0KYGBgDQoNCjxicj48cCBjbGFzcz0icWl6Ij4NCuiri+S9oOeUqOS4gOWAi+aMh+S7pOWIl+WHuuiiq+izvOiyt+acgOWkmuasoeeahDEw5YCL55Si5ZOB77yM5ZKM5a6D5YCR6KKr6LO86LK355qE5qyh5pW444CCDQo8YnI+PC9wPg0KYGBge3J9DQpjb2xTdW1zKGNwbSklPiUgaGVhZCgxMCkNCmBgYA0KDQo8YnI+PHAgY2xhc3M9InFpeiI+DQrilqAg5Zyo5LuA6bq85YmN5o+Q5LmL5LiL77yM5oiR5YCR5Y+v5Lul5oqK6LO86LK36YCZ5Y2B5YCL55Si5ZOB55qE5qyh5pW455W25L2c6K6K5pW477yM55So5L6G6aCQ5ris6aGn5a6i5Zyo5LiL5LiA5pyf5pyD5LiN5pyD5L6G6LO86LK35ZGi77yfPGJyPg0KMS5ubyA8YnI+DQoyLumAmTEw5YCL55Si5ZOB5piv5L6d54Wn6LO86LK35qyh5pW45L2G5LiN5LiA5a6a6IO95bCN6aGn5a6i5LiL5qyh5piv5ZCm6IO95L6G6LO86LK355Si55Sf6Kej6YeL5Yqb44CCICANCg0KICAgIA0K4pagIOaIkeWAkeWmguS9leaKiumAmeWNgeWAi+iuiuaVuO+8jOS9teWFpemhp+Wuouizh+aWmeahhuWRou+8nyA8YnI+DQoxLuW+nlros4fmlpnkuK0s5om+5Ye65q+P5L2N6aGn5a6i6LO86LK3VG9wMTDnlKLlk4HnmoTmrKHmlbg8YnI+DQoyLuWGjeS7pUNJROS9teWFpeizh+aWmembhkHkuK0NCg0KDQrilqAg5oiR5YCR5Y+v5LiN5Y+v5LulKOWcqOS7gOm6vOWJjeaPkOS5i+S4i+aIkeWAkeWPr+S7pSnnm7TmjqXnlKhgY2JpbmQoKWDmlrDorormlbjkvbXlhaXpoaflrqLos4fmlpnmoYblkaLvvJ88YnI+DQoxLmNiaW5kKCnmiornn6npmaPmqavlkJHlkIjkvbXmiJDkuIDlgIvlpKfnn6npmaPvvIjliJfmlrnlvI/vvIk8YnI+DQoyLuWPr+S7peOAguWJjeaPkOaYryxyb3fmlbjnm67opoHnm7jlkIzkuJTkuozlgIvos4fmlpnmoYZjaWQg55qE5qyh5bqP6KaB55u45ZCM44CCDQoNCuKWoCDmiJHlgJHmnJ/kuK3nq7bos73nmoTos4fmlpnvvIznrKblkIjnm7TmjqXnlKhgY2JpbmQoKWDkvbXlhaXmlrDorormlbjnmoTmop3ku7bll47vvJ8g5oiR5YCR6KaB5aaC5L2V56K66KqN6YCZ5LiA5Lu25LqL5ZGi77yfPGJyPg0KMS7nrKblkIjjgII8YnI+DQoyLuimgeeiuuiqjeizh+aWmeahhnJvd+eahOasoeW6j+imgeebuOWQjOOAgg0KIA0KPGJyPjwvcCBjbGFzcz0icWl6Ij4NCjxicj48aHI+DQoNCg0KIyMjIEIuIOebtOaOpeS7peeUouWTgeeahOiiq+izvOiyt+mgu+eOh+S9nOeCuuiuiuaVuA0KDQrku6XnlKLlk4HnmoTooqvos7zosrfpoLvnjofoo73kvZwo6aGn5a6iKeiuiuaVuOeahOaZguWAme+8jCoq5o6SYGNwbWDlnKjmnIDliY3pgornmoQoTuWAiynmrIQr5L2N5bCx5piv6K6K5pW4KiohDQoNCiMjIyMjIEIxLiDku6Uo5pyA5bi46KKr6LO86LK355qEKeeUouWTgeeahOizvOiyt+asoeaVuOWwjemhp+WuouWIhue+pA0KYGBge3J9DQpub3A9IDQwMCAgIyBuby4gcHJvZHVjdCA9IG5vLiB2YXJpYWJsZXMNCmsgPSAyMDAgICAjIG5vLiBjbHVzdGVyDQpzZXQuc2VlZCgxMTEpOyBrZyA9IGttZWFucyhjcG1bLDE6bm9wXSwgaykkY2x1c3Rlcg0KdGFibGUoa2cpICU+JSBhcy52ZWN0b3IgJT4lIHNvcnQNCmBgYA0KDQojIyMjIyBCMi4g5ZCE576k57WE5bmz5Z2H5bGs5oCnDQrlsIfliIbnvqTntZDmnpzkvbXlhaXpoaflrqLos4fmlpnmoYYoYEFgKQ0KYGBge3J9DQpkZiA9IEEgJT4lIGlubmVyX2pvaW4oZGF0YS5mcmFtZSgNCiAgY3VzdCA9IGFzLmludGVnZXIocm93bmFtZXMoY3BtKSksIA0KICBrZykgKQ0KaGVhZChkZikgICMgMzIyNDENCmBgYA0KDQroqIjnrpflkITnvqTntYTnmoTlubPlnYflsazmgKcNCmBgYHtyfQ0KZGYgPSBkYXRhLmZyYW1lKA0KICBhZ2dyZWdhdGUoLiB+IGtnLCBkZlssYygyOjcsMTApXSwgbWVhbiksICMgYXZlcmFnZXMNCiAgc2l6ZSA9IGFzLnZlY3Rvcih0YWJsZShrZykpLCAgICMgbm8uIGN1c3RvbWVycyBpbiB0aGUgZ3JvdXANCiAgZHVtbXkgPSAyMDAxICAgICAgICAgICAgICAgICAgICMgZHVtbXkgY29sdW1uIGZvciBnb29nbGVWaXoNCiAgKQ0KaGVhZChkZikNCmBgYA0KDQojIyMjIyBCMy4g5LqS5YuV5byP5rOh5rOh5ZyWDQpgYGB7cn0NCnBsb3QoIGd2aXNNb3Rpb25DaGFydCgNCiAgc3Vic2V0KGRmWyxjKDEsNCw1LDYsOCwyLDMsNyw5KV0sIA0KICAgICAgICAgc2l6ZSA+PSAyMCAmIHNpemUgPD0gMTAwMCksICAjIHJhbmdlIG9mIGdyb3VwIHNpemUgDQogICJrZyIsICJkdW1teSIsIG9wdGlvbnM9bGlzdCh3aWR0aD04MDAsIGhlaWdodD02MDApICkgKQ0KYGBgDQoNCiMjIyMjIEI0LiDlkITnvqTntYTnmoTku6PooajmgKfnlKLlk4EgKFNpZ25hdHVyZSBQcm9kdWN0KQ0KYGBge3J9DQojIHVzZSBnbG9iYWwgdmFyaWFibGVzOiBjcG0sIGtnDQpTaWcgPSBmdW5jdGlvbihneCwgUD0xMDAwLCBIPTEwKSB7DQogIHByaW50KHNwcmludGYoIkdyb3VwICVkOiBOby4gQ3VzdG9tZXJzID0gJWQiLCBneCwgc3VtKGtnPT1neCkpKQ0KICBieCA9IGNwbVssMTpQXQ0KICBkYXRhLmZyYW1lKG4gPSBjb2xfc3VtcyhieFtrZz09Z3gsXSkpICU+JSAgICAgICMgZnJlcXVlbmN5DQogICAgbXV0YXRlKA0KICAgICAgc2hhcmUgPSByb3VuZCgxMDAqbi9jb2xfc3VtcyhieCksMiksICAgICAgICMgJXByb2Qgc29sZCB0byB0aGlzIGNsdXN0ZXIgKO+8hXByb2Tlt7LllK7lh7rmraTnvqTpm4YpDQogICAgICBjb25mID0gcm91bmQoMTAwKm4vc3VtKGtnPT1neCksMiksICAgICAgICAgIyAlYnV5IHRoaXMgcHJvZHVjdCwgZ2l2ZW4gY2x1c3RlciAo77yF6LO86LK35q2k55Si5ZOB77yM57Wm5a6a576k6ZuGKQ0KICAgICAgYmFzZSA9IHJvdW5kKDEwMCpjb2xfc3VtcyhieCkvbnJvdyhieCksMiksICMgJWJ1eSB0aGlzIHByb2R1Y3QsIGFsbCBjdXN0ICjvvIXos7zosrfmraTnlKLlk4HvvIzmiYDmnIljdXN0DQogICAgICBsaWZ0ID0gcm91bmQoY29uZi9iYXNlLDEpLCAgICAgICAgICAgICAgICAgIyBjb25mL2Jhc2UgIA0KICAgICAgbmFtZSA9IGNvbG5hbWVzKGJ4KSAgICAgICAgICAgICAgICAgICAgICAgICMgbmFtZSBvZiBwcm9kICMgcHJvZOeahOWQjeeosQ0KICAgICkgJT4lIGFycmFuZ2UoZGVzYyhsaWZ0KSkgJT4lIGhlYWQoSCkNCiAgfQ0KYGBgDQoNCmBgYHtyfQ0KU2lnKDEzMCkNCmBgYA0KPGJyPjxocj4NCg0KDQojIyMgQy4g5L2/55So5bC65bqm57iu5rib5pa55rOV5oq95Y+W6aGn5a6iKOeUouWTgSnnmoTnibnlvrXlkJHph48gDQoNCiMjIyMjIEMxLiDlt6jlpKflsLrluqbnuK7muJsgKFNWRCwgU2lndWxhciBWYWx1ZSBEZWNvbXBvc2l0aW9uKQ0KYGBge3J9DQpsaWJyYXJ5KGlybGJhKQ0KaWYoTE9BRCkgew0KICBsb2FkKCJkYXRhL3N2ZDJhLnJkYXRhIikNCn0gZWxzZSB7DQogIHNteCA9IGNwbQ0KICBzbXhAeCA9IHBtaW4oc214QHgsIDIpICAgICAgICAgICAgIyBjYXAgYXQgMiwgc2ltaWxhciB0byBub3JtYWxpemF0aW9uICANCiAgdDAgPSBTeXMudGltZSgpDQogIHN2ZCA9IGlybGJhKHNteCwgDQogICAgICAgICAgICAgIG52PTQwMCwgICAgICAgICAgICAgICAjIGxlbmd0aCBvZiBmZWF0dXJlIHZlY3Rvcg0KICAgICAgICAgICAgICBtYXhpdD04MDAsIHdvcms9ODAwKSAgICANCiAgcHJpbnQoU3lzLnRpbWUoKSAtIHQwKSAgICAgICAgICAgICMgMS44Nzk1IG1pbnMNCiAgc2F2ZShzdmQsIGZpbGUgPSAiZGF0YS9zdmQyYS5yZGF0YSIpDQp9DQpgYGANCg0KPGJyPjxwIGNsYXNzPSJxaXoiPg0K4pagIOWcqOS7gOm6vOWJjeaPkOS5i+S4i++8jOaIkeWAkeWPr+S7peaKiumhp+WuouizvOiyt+eUouWTgeeahOeJueW+teWQkemHj+eVtuS9nOiuiuaVuO+8jOeUqOS+humgkOa4rOmhp+WuouWcqOS4i+S4gOacn+acg+S4jeacg+S+huizvOiyt+WRou+8nzxicj4gDQoxLuWPr+S7peOAgjxicj4NCjIu5aaC5p6c6KaB5bCH55Si5ZOB55qE54m55b615ZCR6YeP77yIWO+8ieWOu+mgkOa4rO+8iFnvvInvvIzvvIhY77yJ5b+F6aCI6KaB5bCN6aCQ5ris77yIWe+8ieacieaEj+e+qe+8jOWmgljoiIdZ6KaB5pyJ55u46Zec5oCn44CB6aCQ5ris5Yqb5oiW5b2x6Z+/5Yqb44CCDQoNCuKWoCDlpoLmnpzlj6/ku6XnmoToqbHvvIzmiJHlgJHlpoLkvZXmiorpoaflrqLos7zosrfnlKLlk4HnmoTnibnlvrXlkJHkvbXlhaXpoaflrqLos4fmlpnmoYblkaLvvJ8gPGJyPg0KMS7lvp5zdmTos4fmlpnkuK0s5om+5Ye65q+P5L2N6aGn5a6i5bCN5pa8NDAw5YCL55Si5ZOB55qE54m55b615ZCR6YePPGJyPg0KMi7lho3ku6VDSUTkvbXlhaXos4fmlpnpm4ZB5LitDQoNCuKWoCDmiJHlgJHlj6/kuI3lj6/ku6Uo5Zyo5LuA6bq85YmN5o+Q5LmL5LiL5oiR5YCR5Y+v5LulKeebtOaOpeeUqGBjYmluZCgpYOWwh+eJueW+teWQkemHj+S9teWFpemhp+Wuouizh+aWmeahhuWRou+8nzxicj4NCjEuY2JpbmQoKeaKiuefqemZo+apq+WQkeWQiOS9teaIkOS4gOWAi+Wkp+efqemZo++8iOWIl+aWueW8j++8iTxicj4NCjIu5Y+v5Lul44CC5YmN5o+Q5pivLHJvd+aVuOebruimgeebuOWQjOS4lOS6jOWAi+izh+aWmeahhmNpZCDnmoTmrKHluo/opoHnm7jlkIzjgIINCg0K4pagIOaIkeWAkeacn+S4reertuizveeahOizh+aWme+8jOespuWQiOebtOaOpeeUqGBjYmluZCgpYOS9teWFpeeJueW+teWQkemHj+eahOaineS7tuWXju+8nyDmiJHlgJHopoHlpoLkvZXnorroqo3pgJnkuIDku7bkuovlkaLvvJ88YnI+DQoxLuespuWQiOOAgjxicj4NCjIu6KaB56K66KqN6LOH5paZ5qGGcm9355qE5qyh5bqP6KaB55u45ZCM44CCDQoNCg0KPGJyPg0KPGJyPjwvcCBjbGFzcz0icWl6Ij48YnI+DQoNCg0KIyMjIyMgQzIuIOS+neeJueW+teWQkemHj+Wwjemhp+WuouWIhue+pA0KYGBge3J9DQpzZXQuc2VlZCgxMTEpOyBrZyA9IGttZWFucyhzdmQkdSwgMjAwKSRjbHVzdGVyDQp0YWJsZShrZykgJT4lIGFzLnZlY3RvciAlPiUgc29ydA0KYGBgDQoNCiMjIyMjIEMzLiDkupLli5XlvI/ms6Hms6HlnJYgKEdvb2dsZSBNb3Rpb24gQ2hhcnQpDQpgYGB7Un0NCiMgY2x1c3RzdGVyIHN1bW1hcnkg57CH5pGY6KaBDQpkZiA9IGlubmVyX2pvaW4oQSwgZGF0YS5mcmFtZSggICAgICAgICANCiAgY3VzdCA9IGFzLmludGVnZXIocm93bmFtZXMoY3BtKSksIGtnKSkgJT4lIA0KICBncm91cF9ieShrZykgJT4lIHN1bW1hcmlzZSgNCiAgICBhdmdfZnJlcXVlbmN5ID0gbWVhbihmKSwNCiAgICBhdmdfbW9uZXRhcnkgPSBtZWFuKG0pLA0KICAgIGF2Z19yZXZlbnVlX2NvbnRyID0gbWVhbihyZXYpLA0KICAgIGdyb3VwX3NpemUgPSBuKCksDQogICAgYXZnX3JlY2VuY3kgPSBtZWFuKHIpLCAj6L+R5ZugDQogICAgYXZnX2dyb3NzX3Byb2ZpdCA9IG1lYW4ocmF3KSkgJT4lICPmr5vliKkNCiAgdW5ncm91cCAlPiUgDQogIG11dGF0ZShkdW1teSA9IDIwMDEsIGtnID0gc3ByaW50ZigiRyUwM2QiLGtnKSkgJT4lICAgIA0KICBkYXRhLmZyYW1lDQoNCg0KIyBHb29nbGUgTW90aW9uIENoYXJ0DQpwbG90KCBndmlzTW90aW9uQ2hhcnQoDQogIHN1YnNldChkZiwgZ3JvdXBfc2l6ZSA+PSAyMCAmIGdyb3VwX3NpemUgPD0gMTIwMCksICAgICANCiAgImtnIiwgImR1bW15Iiwgb3B0aW9ucz1saXN0KHdpZHRoPTgwMCwgaGVpZ2h0PTYwMCkgKSApDQoNCmBgYA0KDQojIyMjIyBDNC4g5ZCE576k57WE55qE5Luj6KGo5oCn55Si5ZOBIChTaWduYXR1cmUgUHJvZHVjdCkNCmBgYHtyfQ0KU2lnKDE2MikgI+S7o+ihqOaAp+OAgkcxNjLlubPlnYfos7zosrfpoLvnjoc6MTEuNQ0KYGBgDQo8YnI+PGhyPg0KDQoNCiMjIyBELiDos7zniannsYPliIbmnpAgQmFza2V0cyBBbmFseXNpcyANCg0KYGBge3J9DQpuX2Rpc3RpbmN0KFokdGlkKQ0Kbl9kaXN0aW5jdChaJHByb2QpDQpgYGANCg0KIyMjIyMgRDEuIOa6luWCmeizh+aWmSAoZm9yIEFzc29jaWF0aW9uIFJ1bGUgQW5hbHlzaXMpDQros7zniannsYPliIbmnpDmnIPkvb/nlKhgYXJ1bGVzYOmAmeWAi+Wll+S7tu+8jOWug+imgeeUqOeahOizh+aWmeW+iOWuueaYk+a6luWCme+8jOebtOaOpeS9v+eUqOS6pOaYk+mgheebruizh+aWme+8jOS4gOihjOeoi+W8j+WwseWPr+S7peaQnuWumuOAgg0KYGBge3J9DQpsaWJyYXJ5KGFydWxlcykNCmxpYnJhcnkoYXJ1bGVzVml6KQ0KYnggPSBhcyhzcGxpdChaJHByb2QsIFokdGlkKSwgInRyYW5zYWN0aW9ucyIpICANCg0KYGBgDQoNCiMjIyMjIEQyLiBUb3AyMCDnhrHos6PnlKLlk4ENCmBgYHtyIGZpZy5oZWlnaHQ9MywgZmlnLndpZHRoPTcuMn0NCml0ZW1GcmVxdWVuY3lQbG90KGJ4LCB0b3BOPTIwLCB0eXBlPSJhYnNvbHV0ZSIsIGNleD0wLjgpICPpoIXnm67poLvnjoflnJYNCg0KYGBgDQoNCiMjIyMjIEQzLiDpl5zoga/opo/liYflkoxBcHJpb3Jp5ryU566X5rOVDQoNCumXnOiBr+imj+WJhyhBID0+IEIpDQoNCisgc3VwcG9ydDogQeiiq+izvOiyt+eahOapn+eOhyAoQeeahOWfuuekjuapn+eOhykNCisgY29uZmlkZW5jZTogQeiiq+izvOiyt+aZgu+8jELooqvos7zosrfnmoTmqZ/njocNCisgbGlmdDogQeiiq+izvOiyt+aZgu+8jELooqvos7zosrfnmoTmqZ/njoflop7liqDnmoTlgI3mlbggKOiIh0LnmoTln7rnpI7mqZ/njofnm7jmr5QpDQorIOS4gOiIrOS+huism3N1cHBvcnTjgIFjb25maWRlbmNl5ZKMbGlmdOi2iumrmOeahOmXnOiBr+imj+WJh+i2iumHjeimgQ0KKyBzdXBwb3J044CBY29uZmlkZW5jZeWSjGxpZnToqK3nmoTotorkvY4o6auYKe+8jOaJvuWIsOeahOmXnOiBr+imj+WJh+i2iuWkmijlsJEpDQorIOW7uuitsOS4gOmWi+Wni+aKiuaomea6luioreS9ju+8jOWFiOaJvuWIsOWkmuS4gOm7nuimj+WJh++8jOS5i+W+jOWGjeeUqHN1YnNldOevqemBuOWHuueJueWumueahOimj+WJh+S+hueciw0KKw0KDQpgYGB7cn0NCnJ1bGVzID0gYXByaW9yaShieCwgcGFyYW1ldGVyPWxpc3Qoc3VwcD0wLjAwMiwgY29uZj0wLjYpKSANCnN1bW1hcnkocnVsZXMpDQoNCmBgYA0KDQojIyMjIyBENC4g5qqi6KaW6Zec6IGv6KaP5YmHDQoNCumXnOiBr+imj+WJhyAoQSA9PiBCKe+8mg0KDQorIHN1cHBvcnQ6IEHooqvos7zosrfnmoTmqZ/njocgKEHnmoTln7rnpI7mqZ/njocpDQorIGNvbmZpZGVuY2U6IEHooqvos7zosrfmmYLvvIxC6KKr6LO86LK355qE5qmf546HDQorIGxpZnQ6IEHooqvos7zosrfmmYLvvIxC6KKr6LO86LK355qE5qmf546H5aKe5Yqg55qE5YCN5pW4ICjoiIdC55qE5Z+656SO5qmf546H55u45q+UKQ0KDQpgYGB7cn0NCm9wdGlvbnMoZGlnaXRzPTQpICANCmluc3BlY3QocnVsZXMpICANCg0KI2xpZnTvvJpB6KKr6LO86LK35pmC77yMQuiiq+izvOiyt+eahOapn+eOh+WinuWKoOeahOWAjeaVuCAo6IiHQueahOWfuuekjuapn+eOh+ebuOavlCkNCiNsaHMobGVmdCBoYW5kIHNpZGUp5ZKMcmhzKHJpZ2h0IGhhbmQgc2lkZSnvvIzooajnpLrlt6bmk43kvZzmlbDlj7Pmk43kvZzmlbANCg0KYGBgDQoNCiMjIyMjIEQ1LiDkupLli5XlnJbooajpoa/npLoNCuacieS6kuWLleWcluihqOeahOW5q+WKqe+8jOaIkeWAkeWPr+S7peaKiuaineS7tuaUvuWvrO+8jOWkmuaJvuS4gOS6m+mXnOiBr+imj+WJh+mAsuS+hg0KYGBge3J9DQpydWxlcyA9IGFwcmlvcmkoYngsIHBhcmFtZXRlcj1saXN0KHN1cHA9MC4wMDAzLCBjb25mPTAuNSkpIA0KDQpgYGANCg0KYGBge3J9DQpwbG90KHJ1bGVzLGNvbG9ycz1jKCJyZWQiLCJncmVlbiIpLGVuZ2luZT0iaHRtbHdpZGdldCIsDQogICAgIG1hcmtlcj1saXN0KG9wYWNpdHk9LjYsc2l6ZT04KSkNCg0KYGBgDQoNCmBgYHtyfQ0KcGxvdChydWxlcyxtZXRob2Q9Im1hdHJpeCIsc2hhZGluZz0ibGlmdCIsZW5naW5lPSJodG1sd2lkZ2V0IiwNCiAgICAgY29sb3JzPWMoInJlZCIsICJncmVlbiIpKQ0KDQpgYGANCg0KIyMjIyMgRDYuIOevqemBuOeUouWTgeOAgeS6kuWLleW8j+mXnOiBr+Wclg0KYGBge3J9DQpyMSA9IHN1YnNldChydWxlcywgc3Vic2V0ID0gcmhzICVpbiUgYygiOTMzNjI5OTMiKSkNCnBsb3QocjEsbWV0aG9kPSJncmFwaCIsZW5naW5lPSJodG1sd2lkZ2V0IixpdGVtQ29sPSJjeWFuIikgDQoNCmBgYA0KDQorIOazoeazoeWkp+Wwj++8mnN1cHBvcnQ6IEHooqvos7zosrfnmoTmqZ/njocgKEHnmoTln7rnpI7mqZ/njocpDQorIOazoeazoemhj+iJsu+8mmxpZnQ6IEHooqvos7zosrfmmYLvvIxC6KKr6LO86LK355qE5qmf546H5aKe5Yqg55qE5YCN5pW4ICjoiIdC55qE5Z+656SO5qmf546H55u45q+UKQ0KDQo8YnI+PGhyPg0KDQoNCiMjIyBFLiDnlKLlk4HmjqjolqYgUHJvZHVjdCBSZWNvbW1lbmRhdGlvbg0KDQojIyMjIyBFMS4g56+p6YG46aGn5a6i44CB55Si5ZOBDQrlpKrlsJHooqvos7zosrfnmoTnlKLlk4Hlkozos7zosrflpKrlsJHnlKLlk4HnmoTpoaflrqLpg73kuI3pganlkIjkvb/nlKhDb2xsYWJvcmF0aXZlIEZpbHRlcmluZ+mAmeeorueUouWTgeaOqOiWpuaWueazle+8jOaJgOS7peaIkeWAkeWFiOWwjemhp+WuouWSjOeUouWTgeWBmuS4gOasoeevqemBuA0KYGBge3J9DQpsaWJyYXJ5KHJlY29tbWVuZGVybGFiKQ0KcnggPSBjcG1bLCBjb2xTdW1zKGNwbSA+IDApID49IDUwXQ0KcnggPSByeFtyb3dTdW1zKHJ4ID4gMCkgPj0gMjAgJiByb3dTdW1zKHJ4ID4gMCkgPD0gMzAwLCBdDQpkaW0ocngpICAjIDg4NDYgMzM1NA0KYGBgDQoNCiMjIyMjIEUyLiDpgbjmk4fnlKLlk4HoqZXliIbmlrnlvI8NCuWPr+S7pemBuOaTh+imgeeUqA0KDQorIOizvOiyt+asoeaVuCAocmVhbFJhdGluZ01hdHJpeCkg5oiWDQorIOaYr+WQpuizvOiytyAoYmluYXJ5UmF0aW5nTWF0cml4KeWBmuaooeWei+OAgg0KDQpgYGB7cn0NCnJ4ID0gYXMocngsICJyZWFsUmF0aW5nTWF0cml4IikgICMgcmVhbFJhdGluZ01hdHJpeCDnnJ/mraPnmoToqZXntJrnn6npmaMNCmJ4ID0gYmluYXJpemUocngsIG1pblJhdGluZz0xKSAgICMgYmluYXJ5UmF0aW5nTWF0cml4IOS6jOWFg+iplee0muefqemZow0KDQpgYGANCg0KIyMjIyMgRTMuIOW7uueri+aooeWei+OAgeeUoueUn+W7uuitsCAtIFVCQ0YNClVCQ0bvvJpVc2VyIEJhc2VkIENvbGxhYm9yYXRpdmUgRmlsdGVyaW5nIA0KYGBge3J9DQpyVUJDRiA9IFJlY29tbWVuZGVyKGJ4WzE6ODgwMCxdLCBtZXRob2QgPSAiVUJDRiIpDQpwcmVkID0gcHJlZGljdChyVUJDRiwgYnhbODgwMTo4ODQ2LF0sIG49NCkNCmRvLmNhbGwocmJpbmQsIGFzKHByZWQsICJsaXN0IikpICU+JSBoZWFkKDE1KQ0KYGBgDQoNCiNVQkNG77yaVXNlciBCYXNlZCBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyAv5Z+65pa855So5oi255qE5Y2U5ZCM6YGO5r++DQoNCiMjIyMjIEU0LiDlu7rnq4vmqKHlnovjgIHnlKLnlJ/lu7rorbAgLSBJQkNGDQpJQkNG77yaSXRlbSBCYXNlZCBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyANCmBgYHtyfQ0KaWYoTE9BRCkgew0KICBsb2FkKCJkYXRhL3JlY29tbWVuZGVycy5yZGF0YSIpDQp9IGVsc2V7DQogIHJJQkNGIDwtIFJlY29tbWVuZGVyKGJ4WzE6NjAwMCxdLCBtZXRob2QgPSAiSUJDRiIpDQp9DQpwcmVkID0gcHJlZGljdChySUJDRiwgYnhbODgwMTo4ODQ2LF0sIG49NCkNCmRvLmNhbGwocmJpbmQsIGFzKHByZWQsICJsaXN0IikpICU+JSBoZWFkKDE1KQ0KYGBgDQoNCiNJQkNG77yaSXRlbSBCYXNlZCBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyAv5Z+65pa855Si5ZOB6aCF55uu55qE5Y2U5ZCM6YGO5r++DQoNCg0KYGBge3J9DQpzYXZlKHJJQkNGLCByVUJDRiwgZmlsZT0iZGF0YS9yZWNvbW1lbmRlcnMucmRhdGEiKQ0KYGBgDQoNCiMjIyMjIEU1LiDoqK3lrprmqKHlnoso5rqW56K65oCnKempl+itieaWueW8jw0KYGBge3J9DQpzZXQuc2VlZCg0MzIxKQ0Kc2NoZW1lID0gZXZhbHVhdGlvblNjaGVtZSggICAgIA0KICBieCwgbWV0aG9kPSJzcGxpdCIsIHRyYWluID0gLjc1LCAgZ2l2ZW49NSkNCmBgYA0KDQojIyMjIyBFNi4g6Kit5a6a5o6o6Jam5pa55rOVKOWPg+aVuCkNCmBgYHtyfQ0KYWxnb3JpdGhtcyA9IGxpc3QoICAgICAgICAgICAgDQogIEFSNTMgPSBsaXN0KG5hbWU9IkFSIiwgcGFyYW09bGlzdChzdXBwb3J0PTAuMDAwNSwgY29uZmlkZW5jZT0wLjMpKSwNCiAgQVI0MyA9IGxpc3QobmFtZT0iQVIiLCBwYXJhbT1saXN0KHN1cHBvcnQ9MC4wMDA0LCBjb25maWRlbmNlPTAuMykpLA0KICBSQU5ET00gPSBsaXN0KG5hbWU9IlJBTkRPTSIsIHBhcmFtPU5VTEwpLA0KICBQT1BVTEFSID0gbGlzdChuYW1lPSJQT1BVTEFSIiwgcGFyYW09TlVMTCksDQogIFVCQ0YgPSBsaXN0KG5hbWU9IlVCQ0YiLCBwYXJhbT1OVUxMKSwNCiAgSUJDRiA9IGxpc3QobmFtZT0iSUJDRiIsIHBhcmFtPU5VTEwpICkNCmBgYA0KDQojIyMjIyBFNy4g5bu65qih44CB6aCQ5ris44CB6amX6K2JKOa6lueiuuaApykNCmBgYHtyfQ0KaWYoTE9BRCkgew0KICBsb2FkKCJkYXRhL3Jlc3VsdHMyYS5yZGF0YSIpDQp9IGVsc2Ugew0KICB0MCA9IFN5cy50aW1lKCkNCiAgcmVzdWx0cyA9IGV2YWx1YXRlKCAgICAgICAgICAgIA0KICAgIHNjaGVtZSwgYWxnb3JpdGhtcywgDQogICAgdHlwZT0idG9wTkxpc3QiLCAgICAgIyBtZXRob2Qgb2YgZXZhbHVhdGlvbiAg6KmV5Lyw5pa55rOVDQogICAgbj1jKDUsIDEwLCAxNSwgMjApICAgIyBuby4gcmVjb20uIHRvIGJlIGV2YWx1YXRlZCAgI1JFQ09N5b6F6KmV5LywDQogICAgKQ0KICBwcmludChTeXMudGltZSgpIC0gdDApDQogIHNhdmUocmVzdWx0cywgZmlsZT0iZGF0YS9yZXN1bHRzMmEucmRhdGEiKQ0KfQ0KIyMgQVIgcnVuIGZvbGQvc2FtcGxlIFttb2RlbCB0aW1lL3ByZWRpY3Rpb24gdGltZV0NCiMjICAgMSAgWzQuMDJzZWMvMjE0LjZzZWNdIA0KIyMgQVIgcnVuIGZvbGQvc2FtcGxlIFttb2RlbCB0aW1lL3ByZWRpY3Rpb24gdGltZV0NCiMjICAgMSAgWzEwLjQ5c2VjLzUzOC41c2VjXSANCiMjIFJBTkRPTSBydW4gZm9sZC9zYW1wbGUgW21vZGVsIHRpbWUvcHJlZGljdGlvbiB0aW1lXQ0KIyMgICAxICBbMHNlYy85LjQ4c2VjXSANCiMjIFBPUFVMQVIgcnVuIGZvbGQvc2FtcGxlIFttb2RlbCB0aW1lL3ByZWRpY3Rpb24gdGltZV0NCiMjICAgMSAgWzBzZWMvMTEuMDlzZWNdIA0KIyMgVUJDRiBydW4gZm9sZC9zYW1wbGUgW21vZGVsIHRpbWUvcHJlZGljdGlvbiB0aW1lXQ0KIyMgICAxICBbMHNlYy83NS40MnNlY10gDQojIyBJQkNGIHJ1biBmb2xkL3NhbXBsZSBbbW9kZWwgdGltZS9wcmVkaWN0aW9uIHRpbWVdDQojIyAgIDEgIFsxOTguMnNlYy8xLjYzc2VjXSANCiMjIFRpbWUgZGlmZmVyZW5jZSBvZiAxOC43MiBtaW5zICAj5pmC5beuNTIuNjbliIbpkJgNCmBgYA0KDQojIyMjIyBFOC4g5qih5Z6L5rqW56K65oCn5q+U6LyDDQpgYGB7ciBmaWcuaGVpZ2h0PTUsIGZpZy53aWR0aD01fQ0KIyBsb2FkKCJkYXRhL3Jlc3VsdHMucmRhdGEiKQ0KcGFyKG1hcj1jKDQsNCwzLDIpLGNleD0wLjgpDQpjb2xzID0gYygicmVkIiwgIm1hZ2VudGEiLCAiZ3JheSIsICJvcmFuZ2UiLCAiYmx1ZSIsICJncmVlbiIpDQpwbG90KHJlc3VsdHMsIGFubm90YXRlPWMoMSwzKSwgbGVnZW5kPSJ0b3BsZWZ0IiwgcGNoPTE5LCBsd2Q9MiwgY29sPWNvbHMpDQphYmxpbmUodj1zZXEoMCwwLjAwNiwwLjAwMSksIGg9c2VxKDAsMC4wOCwwLjAxKSwgY29sPSdsaWdodGdyYXknLCBsdHk9MikNCmBgYA0KDQpgYGB7cn0NCmdldENvbmZ1c2lvbk1hdHJpeChyZXN1bHRzJElCQ0YpDQpgYGANCg0KPGJyPjxicj48aHI+PGJyPjxicj48YnI+DQoNCg0KDQoNCg0KDQo=