Introduction

Firstly, we will write a function that generates random nonnegative matrices \(W\) and \(H\) such that for \(V \in \mathbb{R}^{m \times n}\) then \(W \in \mathbb{R}^{m \times r}\) and \(H \in \mathbb{R}^{r \times n}\) with \(r\) is rank of matrix \(V\).

# Load some packages
rm(list = ls())
library(tidyverse)
library(ggplot2)
library(Matrix)
library(pixmap)
# The number of iterations equal maxiters - 1
maxiters <- 101
random_initialization <- function(V)
{
    # Find rank V
    r <- rankMatrix(V)
    # Get number of row of V
    m <- dim(V)[1]
    # Get number of column of V
    n <- dim(V)[2]
    # Create matrix W (m x r) with entry in [1,2] 
    W <- matrix(runif(m*r,1,2), nrow = m, ncol = r)
    # Create matrix V (r x n) with entry in [1,2] 
    H <- matrix(runif(n*r,1,2), nrow = r, ncol = n)
    return (list(W = W,H = H))
}

Multiplicative updating algorithm

The first NMF algorithm was derived by Lee and Seung named MU.

multiplication_update <- function(V,W,H, maxiters, error = 0.1)
{
    eps <- 1e-05
    MUlog <- rep(NA, maxiters)
    MUlog[[1]] <- norm(V - W %*% H,"F")
    i <- 2
    while (i <= maxiters && MUlog[[i-1]] > error)
    {
        # update H
        H <- H * (t(W) %*% V) / ((t(W) %*% W %*% H)+eps)
        
        # update W 
        W <- W * (V %*% t(H)) / ((W %*% H %*% t(H))+eps)
        
        # Trace F norm
        MUlog[[i]] <- norm(V - W %*% H,"F")
        i <- i + 1
    }
    return (list(W = W,H = H,MUlog = MUlog))
}

Alternating Gradient Algorithm

We now introduce an alternating gradient (AG) algorithm based on Gradient Method

alternating_gradient <- function(V,W,H, maxiters = 3, error = 0.1)
{
    AGlog <- rep(NA, maxiters)
    AGlog[[1]] <- norm(V - W %*% H,"F")
    i <- 2
    while (i <= maxiters && AGlog[[i-1]] > error)
    {
        # Compute gradient when W fixed
        derivative_h <- (t(W) %*% W %*% H) - (t(W) %*% V)
        
        a <- norm(derivative_h,"F")^2
        b <- norm(W %*% (t(W) %*% W %*% H - t(W) %*% V),"F")^2
        
        infimum <- H[which(derivative_h>0)]/derivative_h[which(derivative_h>0)]
        stepsize <- min(a/b,infimum)
        
        # update H
        H <- H - stepsize*(derivative_h)
        
        # Compute gradient when H fixed
        derivative_w <- (W %*% H %*% t(H)) - (V %*% t(H))
        
        a <- norm(derivative_w,"F")^2
        b <- norm((W %*% H %*% t(H) - V %*% t(H)) %*% H,"F")^2
        
        infimum <- W[which(derivative_w>0)]/derivative_w[which(derivative_w>0)]
        stepsize <- min(a/b,infimum)
        
        # update W
        W <- W - stepsize*(derivative_w)
        
        # Trace F norm
        AGlog[[i]] <- norm(V - W %*% H,"F")
        
        i <- i + 1
        
    }
    return (list(W = W, H = H, AGlog = AGlog))
}

Alternating Projected Gradient Algorithm

Because numerical experiments show that the AG algorithm is not better than the MU algorithm, we propose an improvement of the AG algorithm by using alternating projected gradient approach which are denoted by the APG algorithm in short

alternating_projected_gradient <- function(V,W,H, maxiters, error = 0.1)
{
    APGlog <- rep(NA, maxiters)
    APGlog[[1]] <- norm(V - W %*% H,"F")
    i <- 2
    while (i <= maxiters && APGlog[[i-1]] > error)
    {
        # Compute gradient when W fixed
        derivative_h <- (t(W) %*% W %*% H) - (t(W) %*% V)
        a <- norm(derivative_h,"F")^2
        b <- norm(W %*% (t(W) %*% W %*% H - t(W) %*% V),"F")^2
        stepsize <- a/b
        
        
        # update H
        H <- H - stepsize*(derivative_h)
        # Set all negative value in H to 0
        H <- pmax(H,0)
        
        # Compute gradient when H fixed
        derivative_w <- (W %*% H %*% t(H)) - (V %*% t(H))
        a <- norm(derivative_w,"F")^2
        b <- norm((W %*% H %*% t(H) - V %*% t(H)) %*% H,"F")^2
        stepsize <- a/b
    
        # update W
        W <- W - stepsize*(derivative_w)
        # Set all negative value in W to 0
        W <- pmax(W,0)
        
        
        # Trace F norm
        APGlog[[i]] <- norm(V - W %*% H,"F")
        i <- i + 1
        
    }
    return (list(W = W, H = H, APGlog = APGlog))
}

Conduct experiments

Let us download the ATT Faces dataset. The dataset consists of 10 black and white photos of each member of a group 40 individuals. 400 images in total.

# "https://www.kaggle.com/datasets/kasikrit/att-database-of-faces/download?datasetVersionNumber=2"

# set working directory have the dataset
setwd("D:\\faces")
# search folder have the dataset
dirs <- list.dirs("D:\\faces")
# search files .pgm
files <- list.files(pattern = ".pgm", recursive = TRUE)

V <- matrix(nrow = 92*112, ncol = length(files))

for (i in 1:length(files)){
    v <- read.pnm(file = files[i], cellres=1)
    V[,i] <- floor(as.vector(v@grey)*255)
}

plot_face <- function(arr10304, col=gray(0:255/255)){
    m <- matrix(arr10304, ncol=92,  nrow = 112)
    image(t(m[112:1,]), asp=112/92, axes = FALSE, col=col)
}

# Plot some faces
par(mfrow=c(10,20), mar=c(0, 0.2, 0, 0), oma=c(0,0,0,0))
for(i in sample(400,200)){
    plot_face(V[,i])
}

Now we NMF the matrix V, which is every column is a face image

# create matrix W and H
cre_matrix <- random_initialization(V)

res1 <- multiplication_update(V,cre_matrix$W,cre_matrix$H,maxiters)
#res2 <- alternating_gradient(V,cre_matrix$W,cre_matrix$H,maxiters)
res3 <- alternating_projected_gradient(V,cre_matrix$W,cre_matrix$H,maxiters)

df_res <- data.frame(num =1:(length(res1$MUlog)-1), value1 = res1$MUlog[2:maxiters], value2 = res3$APGlog[2:maxiters])

#Plot line graph
df_res %>%
    ggplot(aes(x = num)) +
    geom_line(aes(y = value1), color = "darkred") +
    geom_line(aes(y = value2), color= "steelblue") +
    labs(x = "Iterations", y = "||V-WH||", 
         title = "Compare the convergence rate of the algorithms with ATT faces dataset",
         caption = "Source Data: www.kaggle.com") +
    annotate("text", x=maxiters+0.6, y=res1$MUlog[maxiters], label= "MU") +
    annotate("text", x=maxiters+0.6, y=res3$APGlog[maxiters], label= "APG")

The values of cost function for MU and checking nonincreasing

print(res1$MUlog)
##   [1] 1602251.58   74945.71   74779.13   74771.46   74763.36   74754.63
##   [7]   74745.04   74734.33   74722.17   74708.17   74691.83   74672.52
##  [13]   74649.48   74621.71   74588.00   74546.80   74496.14   74433.60
##  [19]   74356.13   74259.98   74140.56   73992.35   73808.86   73582.70
##  [25]   73305.86   72970.28   72568.83   72096.80   71553.79   70945.58
##  [31]   70285.30   69592.67   68890.94   68201.72   67539.79   66910.21
##  [37]   66308.56   65723.74   65141.44   64547.22   63928.72   63277.56
##  [43]   62591.58   61877.42   61151.18   60433.58   59741.02   59080.29
##  [49]   58450.82   57849.49   57273.47   56720.83   56190.24   55680.56
##  [55]   55190.61   54719.10   54264.60   53825.59   53400.53   52987.94
##  [61]   52586.40   52194.65   51811.57   51436.22   51067.81   50705.70
##  [67]   50349.38   49998.46   49652.64   49311.70   48975.49   48643.91
##  [73]   48316.92   47994.51   47676.68   47363.50   47055.00   46751.26
##  [79]   46452.31   46158.22   45869.00   45584.65   45305.16   45030.48
##  [85]   44760.54   44495.25   44234.50   43978.15   43726.09   43478.14
##  [91]   43234.18   42994.03   42757.56   42524.62   42295.07   42068.78
##  [97]   41845.62   41625.49   41408.28   41193.88   40982.22
cat("Is nonincreasing?:", !is.unsorted(rev(res1$MUlog)))
## Is nonincreasing?: TRUE

The values of cost function for APG and checking nonincreasing

print(res3$APGlog)
##   [1] 1602251.58   75873.05   74236.22   73379.21   72487.46   71401.22
##   [7]   70016.61   67633.52   65026.66   63065.69   60901.15   59400.99
##  [13]   57845.89   56624.44   55471.00   54241.67   53303.92   52206.64
##  [19]   51349.92   50459.52   49711.63   48990.77   48370.41   47759.05
##  [25]   47222.18   46690.85   46218.31   45745.35   45318.66   44890.06
##  [31]   44497.70   44103.83   43739.11   43373.64   43032.24   42690.66
##  [37]   42369.23   42047.80   41743.64   41439.73   41151.09   40863.13
##  [43]   40588.76   40315.29   40054.00   39793.73   39544.49   39296.28
##  [49]   39058.12   38820.98   38593.16   38366.63   38148.62   37932.10
##  [55]   37723.35   37516.16   37316.00   37117.61   36925.39   36735.02
##  [61]   36550.27   36367.29   36189.54   36013.66   35842.56   35673.46
##  [67]   35508.39   35345.55   35186.28   35029.52   34875.71   34724.60
##  [73]   34576.06   34430.17   34286.55   34145.80   34006.99   33871.06
##  [79]   33736.73   33605.27   33475.25   33348.04   33222.13   33099.00
##  [85]   32977.01   32857.84   32739.69   32624.35   32509.82   32398.05
##  [91]   32286.90   32178.46   32070.50   31965.20   31860.21   31757.94
##  [97]   31655.88   31556.55   31457.27   31360.75   31264.15
cat("Is nonincreasing?:", !is.unsorted(rev(res3$APGlog)))
## Is nonincreasing?: TRUE
LS0tDQp0aXRsZTogIk5vbi1uZWdhdGl2ZSBNYXRyaXggRmFjdG9yaXphdGlvbiB3aXRoIHNvbWUgYWxnb3JpdGhtcyBNVSxBRyxBUEciDQphdXRob3I6ICJBdXRob3I6IFRyYW4gRGF0IFRpbiINCmRhdGU6ICIxMC8xMC8yMDIyIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICAgIGNvZGVfZm9sZGluZzogc2hvdw0KICAgIGhpZ2hsaWdodDogcHlnbWVudHMNCiAgICB0aGVtZTogZmxhdGx5DQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZmxvYXQ6IHllcw0KICBwZGZfZG9jdW1lbnQ6DQogICAgdG9jOiB5ZXMNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkNCmBgYA0KDQojIyBJbnRyb2R1Y3Rpb24NCg0KRmlyc3RseSwgd2Ugd2lsbCB3cml0ZSBhIGZ1bmN0aW9uIHRoYXQgZ2VuZXJhdGVzIHJhbmRvbSBub25uZWdhdGl2ZSBtYXRyaWNlcyAkVyQgYW5kICRIJCBzdWNoIHRoYXQgZm9yICRWIFxpbiBcbWF0aGJie1J9XnttIFx0aW1lcyBufSQgdGhlbiAkVyBcaW4gXG1hdGhiYntSfV57bSBcdGltZXMgcn0kIGFuZCAkSCBcaW4gXG1hdGhiYntSfV57ciBcdGltZXMgbn0kIHdpdGggJHIkIGlzIHJhbmsgb2YgbWF0cml4ICRWJC4gDQoNCg0KYGBge1J9DQojIExvYWQgc29tZSBwYWNrYWdlcw0Kcm0obGlzdCA9IGxzKCkpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoTWF0cml4KQ0KbGlicmFyeShwaXhtYXApDQojIFRoZSBudW1iZXIgb2YgaXRlcmF0aW9ucyBlcXVhbCBtYXhpdGVycyAtIDENCm1heGl0ZXJzIDwtIDEwMQ0KYGBgDQoNCmBgYHtSfQ0KcmFuZG9tX2luaXRpYWxpemF0aW9uIDwtIGZ1bmN0aW9uKFYpDQp7DQogICAgIyBGaW5kIHJhbmsgVg0KICAgIHIgPC0gcmFua01hdHJpeChWKQ0KICAgICMgR2V0IG51bWJlciBvZiByb3cgb2YgVg0KICAgIG0gPC0gZGltKFYpWzFdDQogICAgIyBHZXQgbnVtYmVyIG9mIGNvbHVtbiBvZiBWDQogICAgbiA8LSBkaW0oVilbMl0NCiAgICAjIENyZWF0ZSBtYXRyaXggVyAobSB4IHIpIHdpdGggZW50cnkgaW4gWzEsMl0gDQogICAgVyA8LSBtYXRyaXgocnVuaWYobSpyLDEsMiksIG5yb3cgPSBtLCBuY29sID0gcikNCiAgICAjIENyZWF0ZSBtYXRyaXggViAociB4IG4pIHdpdGggZW50cnkgaW4gWzEsMl0gDQogICAgSCA8LSBtYXRyaXgocnVuaWYobipyLDEsMiksIG5yb3cgPSByLCBuY29sID0gbikNCiAgICByZXR1cm4gKGxpc3QoVyA9IFcsSCA9IEgpKQ0KfQ0KYGBgDQoNCg0KIyMgTXVsdGlwbGljYXRpdmUgdXBkYXRpbmcgYWxnb3JpdGhtDQoNClRoZSBmaXJzdCBOTUYgYWxnb3JpdGhtIHdhcyBkZXJpdmVkIGJ5IExlZSBhbmQgU2V1bmcgbmFtZWQgTVUuIA0KDQpgYGB7Un0NCm11bHRpcGxpY2F0aW9uX3VwZGF0ZSA8LSBmdW5jdGlvbihWLFcsSCwgbWF4aXRlcnMsIGVycm9yID0gMC4xKQ0Kew0KICAgIGVwcyA8LSAxZS0wNQ0KICAgIE1VbG9nIDwtIHJlcChOQSwgbWF4aXRlcnMpDQogICAgTVVsb2dbWzFdXSA8LSBub3JtKFYgLSBXICUqJSBILCJGIikNCiAgICBpIDwtIDINCiAgICB3aGlsZSAoaSA8PSBtYXhpdGVycyAmJiBNVWxvZ1tbaS0xXV0gPiBlcnJvcikNCiAgICB7DQogICAgICAgICMgdXBkYXRlIEgNCiAgICAgICAgSCA8LSBIICogKHQoVykgJSolIFYpIC8gKCh0KFcpICUqJSBXICUqJSBIKStlcHMpDQogICAgICAgIA0KICAgICAgICAjIHVwZGF0ZSBXIA0KICAgICAgICBXIDwtIFcgKiAoViAlKiUgdChIKSkgLyAoKFcgJSolIEggJSolIHQoSCkpK2VwcykNCiAgICAgICAgDQogICAgICAgICMgVHJhY2UgRiBub3JtDQogICAgICAgIE1VbG9nW1tpXV0gPC0gbm9ybShWIC0gVyAlKiUgSCwiRiIpDQogICAgICAgIGkgPC0gaSArIDENCiAgICB9DQogICAgcmV0dXJuIChsaXN0KFcgPSBXLEggPSBILE1VbG9nID0gTVVsb2cpKQ0KfQ0KYGBgDQoNCiMjIEFsdGVybmF0aW5nIEdyYWRpZW50IEFsZ29yaXRobQ0KDQpXZSBub3cgaW50cm9kdWNlIGFuIGFsdGVybmF0aW5nIGdyYWRpZW50IChBRykgYWxnb3JpdGhtIGJhc2VkIG9uIEdyYWRpZW50IE1ldGhvZA0KDQpgYGB7Un0NCmFsdGVybmF0aW5nX2dyYWRpZW50IDwtIGZ1bmN0aW9uKFYsVyxILCBtYXhpdGVycyA9IDMsIGVycm9yID0gMC4xKQ0Kew0KICAgIEFHbG9nIDwtIHJlcChOQSwgbWF4aXRlcnMpDQogICAgQUdsb2dbWzFdXSA8LSBub3JtKFYgLSBXICUqJSBILCJGIikNCiAgICBpIDwtIDINCiAgICB3aGlsZSAoaSA8PSBtYXhpdGVycyAmJiBBR2xvZ1tbaS0xXV0gPiBlcnJvcikNCiAgICB7DQogICAgICAgICMgQ29tcHV0ZSBncmFkaWVudCB3aGVuIFcgZml4ZWQNCiAgICAgICAgZGVyaXZhdGl2ZV9oIDwtICh0KFcpICUqJSBXICUqJSBIKSAtICh0KFcpICUqJSBWKQ0KICAgICAgICANCiAgICAgICAgYSA8LSBub3JtKGRlcml2YXRpdmVfaCwiRiIpXjINCiAgICAgICAgYiA8LSBub3JtKFcgJSolICh0KFcpICUqJSBXICUqJSBIIC0gdChXKSAlKiUgViksIkYiKV4yDQogICAgICAgIA0KICAgICAgICBpbmZpbXVtIDwtIEhbd2hpY2goZGVyaXZhdGl2ZV9oPjApXS9kZXJpdmF0aXZlX2hbd2hpY2goZGVyaXZhdGl2ZV9oPjApXQ0KICAgICAgICBzdGVwc2l6ZSA8LSBtaW4oYS9iLGluZmltdW0pDQogICAgICAgIA0KICAgICAgICAjIHVwZGF0ZSBIDQogICAgICAgIEggPC0gSCAtIHN0ZXBzaXplKihkZXJpdmF0aXZlX2gpDQogICAgICAgIA0KICAgICAgICAjIENvbXB1dGUgZ3JhZGllbnQgd2hlbiBIIGZpeGVkDQogICAgICAgIGRlcml2YXRpdmVfdyA8LSAoVyAlKiUgSCAlKiUgdChIKSkgLSAoViAlKiUgdChIKSkNCiAgICAgICAgDQogICAgICAgIGEgPC0gbm9ybShkZXJpdmF0aXZlX3csIkYiKV4yDQogICAgICAgIGIgPC0gbm9ybSgoVyAlKiUgSCAlKiUgdChIKSAtIFYgJSolIHQoSCkpICUqJSBILCJGIileMg0KICAgICAgICANCiAgICAgICAgaW5maW11bSA8LSBXW3doaWNoKGRlcml2YXRpdmVfdz4wKV0vZGVyaXZhdGl2ZV93W3doaWNoKGRlcml2YXRpdmVfdz4wKV0NCiAgICAgICAgc3RlcHNpemUgPC0gbWluKGEvYixpbmZpbXVtKQ0KICAgICAgICANCiAgICAgICAgIyB1cGRhdGUgVw0KICAgICAgICBXIDwtIFcgLSBzdGVwc2l6ZSooZGVyaXZhdGl2ZV93KQ0KICAgICAgICANCiAgICAgICAgIyBUcmFjZSBGIG5vcm0NCiAgICAgICAgQUdsb2dbW2ldXSA8LSBub3JtKFYgLSBXICUqJSBILCJGIikNCiAgICAgICAgDQogICAgICAgIGkgPC0gaSArIDENCiAgICAgICAgDQogICAgfQ0KICAgIHJldHVybiAobGlzdChXID0gVywgSCA9IEgsIEFHbG9nID0gQUdsb2cpKQ0KfQ0KYGBgDQoNCiMjIEFsdGVybmF0aW5nIFByb2plY3RlZCBHcmFkaWVudCBBbGdvcml0aG0NCg0KQmVjYXVzZSBudW1lcmljYWwgZXhwZXJpbWVudHMgc2hvdyB0aGF0IHRoZSBBRyBhbGdvcml0aG0gaXMgbm90IGJldHRlciB0aGFuIHRoZSBNVSBhbGdvcml0aG0sIHdlIHByb3Bvc2UgYW4gaW1wcm92ZW1lbnQgb2YgdGhlIEFHIGFsZ29yaXRobSBieSB1c2luZyBhbHRlcm5hdGluZyBwcm9qZWN0ZWQgZ3JhZGllbnQgYXBwcm9hY2ggd2hpY2ggYXJlIGRlbm90ZWQgYnkgdGhlIEFQRyBhbGdvcml0aG0gaW4gc2hvcnQNCg0KYGBge1J9DQphbHRlcm5hdGluZ19wcm9qZWN0ZWRfZ3JhZGllbnQgPC0gZnVuY3Rpb24oVixXLEgsIG1heGl0ZXJzLCBlcnJvciA9IDAuMSkNCnsNCiAgICBBUEdsb2cgPC0gcmVwKE5BLCBtYXhpdGVycykNCiAgICBBUEdsb2dbWzFdXSA8LSBub3JtKFYgLSBXICUqJSBILCJGIikNCiAgICBpIDwtIDINCiAgICB3aGlsZSAoaSA8PSBtYXhpdGVycyAmJiBBUEdsb2dbW2ktMV1dID4gZXJyb3IpDQogICAgew0KICAgICAgICAjIENvbXB1dGUgZ3JhZGllbnQgd2hlbiBXIGZpeGVkDQogICAgICAgIGRlcml2YXRpdmVfaCA8LSAodChXKSAlKiUgVyAlKiUgSCkgLSAodChXKSAlKiUgVikNCiAgICAgICAgYSA8LSBub3JtKGRlcml2YXRpdmVfaCwiRiIpXjINCiAgICAgICAgYiA8LSBub3JtKFcgJSolICh0KFcpICUqJSBXICUqJSBIIC0gdChXKSAlKiUgViksIkYiKV4yDQogICAgICAgIHN0ZXBzaXplIDwtIGEvYg0KICAgICAgICANCiAgICAgICAgDQogICAgICAgICMgdXBkYXRlIEgNCiAgICAgICAgSCA8LSBIIC0gc3RlcHNpemUqKGRlcml2YXRpdmVfaCkNCiAgICAgICAgIyBTZXQgYWxsIG5lZ2F0aXZlIHZhbHVlIGluIEggdG8gMA0KICAgICAgICBIIDwtIHBtYXgoSCwwKQ0KICAgICAgICANCiAgICAgICAgIyBDb21wdXRlIGdyYWRpZW50IHdoZW4gSCBmaXhlZA0KICAgICAgICBkZXJpdmF0aXZlX3cgPC0gKFcgJSolIEggJSolIHQoSCkpIC0gKFYgJSolIHQoSCkpDQogICAgICAgIGEgPC0gbm9ybShkZXJpdmF0aXZlX3csIkYiKV4yDQogICAgICAgIGIgPC0gbm9ybSgoVyAlKiUgSCAlKiUgdChIKSAtIFYgJSolIHQoSCkpICUqJSBILCJGIileMg0KICAgICAgICBzdGVwc2l6ZSA8LSBhL2INCiAgICANCiAgICAgICAgIyB1cGRhdGUgVw0KICAgICAgICBXIDwtIFcgLSBzdGVwc2l6ZSooZGVyaXZhdGl2ZV93KQ0KICAgICAgICAjIFNldCBhbGwgbmVnYXRpdmUgdmFsdWUgaW4gVyB0byAwDQogICAgICAgIFcgPC0gcG1heChXLDApDQogICAgICAgIA0KICAgICAgICANCiAgICAgICAgIyBUcmFjZSBGIG5vcm0NCiAgICAgICAgQVBHbG9nW1tpXV0gPC0gbm9ybShWIC0gVyAlKiUgSCwiRiIpDQogICAgICAgIGkgPC0gaSArIDENCiAgICAgICAgDQogICAgfQ0KICAgIHJldHVybiAobGlzdChXID0gVywgSCA9IEgsIEFQR2xvZyA9IEFQR2xvZykpDQp9DQpgYGANCg0KIyMgQ29uZHVjdCBleHBlcmltZW50cw0KDQpMZXQgdXMgZG93bmxvYWQgdGhlIEFUVCBGYWNlcyBkYXRhc2V0LiBUaGUgZGF0YXNldCBjb25zaXN0cyBvZiAxMCBibGFjayBhbmQgd2hpdGUgcGhvdG9zIG9mIGVhY2ggbWVtYmVyIG9mIGEgZ3JvdXAgNDAgaW5kaXZpZHVhbHMuIDQwMCBpbWFnZXMgaW4gdG90YWwuDQoNCmBgYHtSfQ0KIyAiaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9kYXRhc2V0cy9rYXNpa3JpdC9hdHQtZGF0YWJhc2Utb2YtZmFjZXMvZG93bmxvYWQ/ZGF0YXNldFZlcnNpb25OdW1iZXI9MiINCg0KIyBzZXQgd29ya2luZyBkaXJlY3RvcnkgaGF2ZSB0aGUgZGF0YXNldA0Kc2V0d2QoIkQ6XFxmYWNlcyIpDQojIHNlYXJjaCBmb2xkZXIgaGF2ZSB0aGUgZGF0YXNldA0KZGlycyA8LSBsaXN0LmRpcnMoIkQ6XFxmYWNlcyIpDQojIHNlYXJjaCBmaWxlcyAucGdtDQpmaWxlcyA8LSBsaXN0LmZpbGVzKHBhdHRlcm4gPSAiLnBnbSIsIHJlY3Vyc2l2ZSA9IFRSVUUpDQoNClYgPC0gbWF0cml4KG5yb3cgPSA5MioxMTIsIG5jb2wgPSBsZW5ndGgoZmlsZXMpKQ0KDQpmb3IgKGkgaW4gMTpsZW5ndGgoZmlsZXMpKXsNCiAgICB2IDwtIHJlYWQucG5tKGZpbGUgPSBmaWxlc1tpXSwgY2VsbHJlcz0xKQ0KICAgIFZbLGldIDwtIGZsb29yKGFzLnZlY3Rvcih2QGdyZXkpKjI1NSkNCn0NCg0KcGxvdF9mYWNlIDwtIGZ1bmN0aW9uKGFycjEwMzA0LCBjb2w9Z3JheSgwOjI1NS8yNTUpKXsNCiAgICBtIDwtIG1hdHJpeChhcnIxMDMwNCwgbmNvbD05MiwgIG5yb3cgPSAxMTIpDQogICAgaW1hZ2UodChtWzExMjoxLF0pLCBhc3A9MTEyLzkyLCBheGVzID0gRkFMU0UsIGNvbD1jb2wpDQp9DQoNCiMgUGxvdCBzb21lIGZhY2VzDQpwYXIobWZyb3c9YygxMCwyMCksIG1hcj1jKDAsIDAuMiwgMCwgMCksIG9tYT1jKDAsMCwwLDApKQ0KZm9yKGkgaW4gc2FtcGxlKDQwMCwyMDApKXsNCiAgICBwbG90X2ZhY2UoVlssaV0pDQp9DQoNCmBgYA0KDQpOb3cgd2UgTk1GIHRoZSBtYXRyaXggViwgd2hpY2ggaXMgZXZlcnkgY29sdW1uIGlzIGEgZmFjZSBpbWFnZQ0KDQpgYGB7Un0NCiMgY3JlYXRlIG1hdHJpeCBXIGFuZCBIDQpjcmVfbWF0cml4IDwtIHJhbmRvbV9pbml0aWFsaXphdGlvbihWKQ0KDQpyZXMxIDwtIG11bHRpcGxpY2F0aW9uX3VwZGF0ZShWLGNyZV9tYXRyaXgkVyxjcmVfbWF0cml4JEgsbWF4aXRlcnMpDQojcmVzMiA8LSBhbHRlcm5hdGluZ19ncmFkaWVudChWLGNyZV9tYXRyaXgkVyxjcmVfbWF0cml4JEgsbWF4aXRlcnMpDQpyZXMzIDwtIGFsdGVybmF0aW5nX3Byb2plY3RlZF9ncmFkaWVudChWLGNyZV9tYXRyaXgkVyxjcmVfbWF0cml4JEgsbWF4aXRlcnMpDQoNCmRmX3JlcyA8LSBkYXRhLmZyYW1lKG51bSA9MToobGVuZ3RoKHJlczEkTVVsb2cpLTEpLCB2YWx1ZTEgPSByZXMxJE1VbG9nWzI6bWF4aXRlcnNdLCB2YWx1ZTIgPSByZXMzJEFQR2xvZ1syOm1heGl0ZXJzXSkNCg0KI1Bsb3QgbGluZSBncmFwaA0KZGZfcmVzICU+JQ0KICAgIGdncGxvdChhZXMoeCA9IG51bSkpICsNCiAgICBnZW9tX2xpbmUoYWVzKHkgPSB2YWx1ZTEpLCBjb2xvciA9ICJkYXJrcmVkIikgKw0KICAgIGdlb21fbGluZShhZXMoeSA9IHZhbHVlMiksIGNvbG9yPSAic3RlZWxibHVlIikgKw0KICAgIGxhYnMoeCA9ICJJdGVyYXRpb25zIiwgeSA9ICJ8fFYtV0h8fCIsIA0KICAgICAgICAgdGl0bGUgPSAiQ29tcGFyZSB0aGUgY29udmVyZ2VuY2UgcmF0ZSBvZiB0aGUgYWxnb3JpdGhtcyB3aXRoIEFUVCBmYWNlcyBkYXRhc2V0IiwNCiAgICAgICAgIGNhcHRpb24gPSAiU291cmNlIERhdGE6IHd3dy5rYWdnbGUuY29tIikgKw0KICAgIGFubm90YXRlKCJ0ZXh0IiwgeD1tYXhpdGVycyswLjYsIHk9cmVzMSRNVWxvZ1ttYXhpdGVyc10sIGxhYmVsPSAiTVUiKSArDQogICAgYW5ub3RhdGUoInRleHQiLCB4PW1heGl0ZXJzKzAuNiwgeT1yZXMzJEFQR2xvZ1ttYXhpdGVyc10sIGxhYmVsPSAiQVBHIikNCmBgYA0KDQpUaGUgdmFsdWVzIG9mIGNvc3QgZnVuY3Rpb24gZm9yIE1VIGFuZCBjaGVja2luZyBub25pbmNyZWFzaW5nDQpgYGB7Un0NCnByaW50KHJlczEkTVVsb2cpDQpjYXQoIklzIG5vbmluY3JlYXNpbmc/OiIsICFpcy51bnNvcnRlZChyZXYocmVzMSRNVWxvZykpKQ0KYGBgDQoNClRoZSB2YWx1ZXMgb2YgY29zdCBmdW5jdGlvbiBmb3IgQVBHIGFuZCBjaGVja2luZyBub25pbmNyZWFzaW5nDQoNCmBgYHtSfQ0KcHJpbnQocmVzMyRBUEdsb2cpDQpjYXQoIklzIG5vbmluY3JlYXNpbmc/OiIsICFpcy51bnNvcnRlZChyZXYocmVzMyRBUEdsb2cpKSkNCmBgYA0KDQojIyBSZWZlcmVuY2VzDQpbMV0gaHR0cHM6Ly9hbGJlcnRvbHVtYnJlcmFzLm5ldC9wb3N0cy9OTUZfTGVlX1NldW5nLmh0bWwNCg0KDQo=