Back to NIMBLE Vignettes

 

Here we’ll show how to call the Matrix::tcrossprod function from the Matrix package inside a compiled nimbleFunction.

 

Setup

A function to construct a full (dense) matrix, from the component i (row indices), j (column indices), and x (non-zero values) sparse representation.

makeMatrix <- function(i, j, x, nr, nc, ijx) {
    ## ijx is an (optional, alternate) representation of {i,j,x} as a 3-column array
    if(!missing(ijx))   { i <- ijx[,1]; j <- ijx[,2]; x <- ijx[,3] }
    if(length(i) != length(j) | length(i) != length(x)) stop()
    if(missing(nr)) nr <- max(i)
    if(missing(nc)) nc <- max(j)
    mat <- array(0, c(nr, nc))
    for(ind in seq_along(i))   mat[i[ind], j[ind]] <- x[ind]
    return(mat)
}

 

Try using makeMatrix, and also the standard tcrossprod function from the base package.

i <- c(1,3,7,7)
j <- c(1,1,3,4)
x <- c(1,2,3,4)

A <- makeMatrix(i, j, x)
A
##      [,1] [,2] [,3] [,4]
## [1,]    1    0    0    0
## [2,]    0    0    0    0
## [3,]    2    0    0    0
## [4,]    0    0    0    0
## [5,]    0    0    0    0
## [6,]    0    0    0    0
## [7,]    0    0    3    4
A %*% t(A)    ## what tcrossprod(A) does
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    1    0    2    0    0    0    0
## [2,]    0    0    0    0    0    0    0
## [3,]    2    0    4    0    0    0    0
## [4,]    0    0    0    0    0    0    0
## [5,]    0    0    0    0    0    0    0
## [6,]    0    0    0    0    0    0    0
## [7,]    0    0    0    0    0    0   25
base::tcrossprod(A)    ## standard tcrossprod from base package
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    1    0    2    0    0    0    0
## [2,]    0    0    0    0    0    0    0
## [3,]    2    0    4    0    0    0    0
## [4,]    0    0    0    0    0    0    0
## [5,]    0    0    0    0    0    0    0
## [6,]    0    0    0    0    0    0    0
## [7,]    0    0    0    0    0    0   25

 

Matrix Package

Now we’ll do the same tcrossprod calculation using the sparse representation and the Matrix::tcrossprod function.

library(Matrix)

Asparse <- Matrix::sparseMatrix(i, j, x = x)
Asparse
## 7 x 4 sparse Matrix of class "dgCMatrix"
##             
## [1,] 1 . . .
## [2,] . . . .
## [3,] 2 . . .
## [4,] . . . .
## [5,] . . . .
## [6,] . . . .
## [7,] . . 3 4
Matrix::tcrossprod(Asparse)
## 7 x 7 sparse Matrix of class "dsCMatrix"
##                    
## [1,] 1 . 2 . . .  .
## [2,] . . . . . .  .
## [3,] 2 . 4 . . .  .
## [4,] . . . . . .  .
## [5,] . . . . . .  .
## [6,] . . . . . .  .
## [7,] . . . . . . 25

 

Calling Matrix::tcrossprod from NIMBLE

Now, we’re ready to build a wrapper function for the Matrix::tcrossprod function, then use nimbleRcall to register this as a nimbleFunction, that can be used inside other compiled nimbleFunctions.

This function below, R_sparse_crossprod is a regular R function wrapping a call to Matrix::tcrossprod. The key point here is that the input arguments are basic numeric vectors and the return value is a numeric array. These “basic” input and output forms are required for use of this function in other nimbleFunctions. But inside the body of this function, any regular R code is fine.

R_sparse_crossprod <- function(i, j, x) {
    require(Matrix)
    Asparse <- Matrix::sparseMatrix(i, j, x = x)
    ans.dsCMatrix <- Matrix::tcrossprod(Asparse)
    ans.dgTMatrix <- as(ans.dsCMatrix, 'dgTMatrix')
    i <- ans.dgTMatrix@i + 1
    j <- ans.dgTMatrix@j + 1
    x <- ans.dgTMatrix@x
    ijx <- cbind(i, j, x)
    return(ijx)
}

Check regular R execution of our R function:

ijx <- R_sparse_crossprod(i, j, x)
ijx
##      i j  x
## [1,] 1 1  1
## [2,] 3 1  2
## [3,] 1 3  2
## [4,] 3 3  4
## [5,] 7 7 25
makeMatrix(ijx = ijx)
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    1    0    2    0    0    0    0
## [2,]    0    0    0    0    0    0    0
## [3,]    2    0    4    0    0    0    0
## [4,]    0    0    0    0    0    0    0
## [5,]    0    0    0    0    0    0    0
## [6,]    0    0    0    0    0    0    0
## [7,]    0    0    0    0    0    0   25

 

Now, we’re ready to call this R function R_sparse_crossprod from within a nimbleFunction.

library(nimble)

Register use of the R_sparse_crossprod function, essentially just like any other nimbleFunction, using nimbleRcall. This is where we specify the forms of the (basic numeric) inputs and outputs to our R function.

See help(nimbleRcall).

nimble_sparse_crossprod <- nimbleRcall(
    prototype = function(i = double(1), j = double(1), x = double(1)) {},
    returnType = double(2),
    Rfun = 'R_sparse_crossprod'
)

For example, here we’ll define another nimbleFunction (e.g., this could represent a SGV density evaluation) which will internally use nimble_sparse_crossprod in order to call Matrix::tcrossprod.

This nimbleFunction will be R-executable, and also compilable and executable – just like any other nimbleFunction, but this one also happens to call out to arbitrary R code.

Rnf <- nimbleFunction(
    run = function(i = double(1), j = double(1), x = double(1)) {
        ijx <- nimble_sparse_crossprod(i, j, x)
        returnType(double(2))
        return(ijx)
    }
)
     
Cnf <- compileNimble(Rnf)
## compiling... this may take a minute. Use 'showCompilerOutput = TRUE' to see C++ compilation details.
## compilation finished.
Rnf(i, j, x)
##      i j  x
## [1,] 1 1  1
## [2,] 3 1  2
## [3,] 1 3  2
## [4,] 3 3  4
## [5,] 7 7 25
Cnf(i, j, x)
##      [,1] [,2] [,3]
## [1,]    1    1    1
## [2,]    3    1    2
## [3,]    1    3    2
## [4,]    3    3    4
## [5,]    7    7   25
makeMatrix(ijx = Rnf(i, j, x))
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    1    0    2    0    0    0    0
## [2,]    0    0    0    0    0    0    0
## [3,]    2    0    4    0    0    0    0
## [4,]    0    0    0    0    0    0    0
## [5,]    0    0    0    0    0    0    0
## [6,]    0    0    0    0    0    0    0
## [7,]    0    0    0    0    0    0   25
makeMatrix(ijx = Cnf(i, j, x))
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    1    0    2    0    0    0    0
## [2,]    0    0    0    0    0    0    0
## [3,]    2    0    4    0    0    0    0
## [4,]    0    0    0    0    0    0    0
## [5,]    0    0    0    0    0    0    0
## [6,]    0    0    0    0    0    0    0
## [7,]    0    0    0    0    0    0   25

 

Timing Comparisons

Some brief timing comparisons, between regular R execution of R_sparse_crosspord wrapper for Matrix::tcrossprod, compared against call-back from compiled execution though NIMBLE using the Cnf compiled nimbleFunction.

compare <- function(n) {
    set.seed(0)
    i <- ceiling(runif(n, 1, n))
    j <- ceiling(runif(n, 1, n))
    x <- 1:n
    R.time <- system.time(R.ans <- R_sparse_crossprod(i, j, x))[3]
    C.nimble.time <- system.time(C.nimble.ans <- Cnf(i, j, x))[3]
    if(!identical(unname(R.ans), C.nimble.ans)) stop('error')
    return(data.frame(n = n,
                      R.time = as.numeric(R.time),
                      C.nimble.time = as.numeric(C.nimble.time)))
}

p <- 6
nvals <- 10^(1:p)
as.data.frame(t(sapply(nvals, compare)))
##         n R.time C.nimble.time
## 1      10  0.001         0.001
## 2     100  0.001         0.001
## 3    1000  0.002         0.001
## 4   10000  0.004         0.005
## 5  100000  0.039         0.048
## 6 1000000  1.608         1.351