It is much more efficient to compute exp(A) %*% v, than to take the entire matrix exponential. RTMB has a function called expAv that I've written in NIMBLE recently.
I'll try to explain how to use it here. Similar to Rushing 2023, where we have Q for a transition matrix and a diagonal observation rate matrix Lambda, I'll denote p0 as the vector of initial probabilities for each state, and f_i as the vector of observed probabilities for the ith observation, given each state. When the latent states are alive, dead, f_i = c(1, 0) when it is observed. For times t_i, and a study that ends at time T the likelihood becomes,
L = p0 %*% [prod exp((Q - Lambda) t_i - t_i-1) %*% Lambda %*% f_i ]
exp((Q - Lambda (T-t_n_))] 1
The important thing to recognize is that, v_i = Lambda %*% f_i, is a vector. Then you see that the trick for never actually computing the matrix exponential but only a matrix exponential times a vector applies. For A_i = (Q - Lambda)*(t_i-t_i-1), we simply need to compute exp(A_i) %*% v_i. The final right censored observation is simply exp(A_n) %*%
1. Thus, we can use this nice little trick to avoid full matrix exponentials. I've written two versions, one that does not use sparse linear algebra but is plenty fast in the case of a small number of states, and one that implements a basic version of linear algebra where the non-zero values are passed by reference of row i and column j. Note that this function is something I wrote a few weeks ago and I've not added all the bells and whistles plus full testing so make sure you check that it is working correctly for you (compare with expm in R). You can manually change rescale_freq and tol depending on what speed/tolerance in the approximation you are after and check out RTMB::expAv for details on how it works. You will have to write a custom distribution function to use it which you can read about in
Chapter 12 Creating user-defined distributions and functions for models | NimbleUserManual.knit
Let me know if there are any issues with this. I don't think we will get this into the upcoming release of NIMBLE but hopefully it will be in the following release. If you have a small matrix exponential and want to just compute it, you can always do a `nimbleRcall` and use the matrix exponential from the expm package in your code with a cost to efficiency.
expmAvR <- nimbleFunction(
run = function(A = double(2), v = double(2)){ ## Note here v is a matrix with a single column.
N <- 100
tol <- 1e-8
rescale_freq <- 50
C <- max(abs(diag(A)))
diag(A) <- diag(A) + C
N <- qpois(tol, C, lower.tail=FALSE)
ans <- v
term <- v
log_scale <- 0
for (n in 1:N) {
term <- A %*% term / n
ans <- ans + term
if (!(n %% rescale_freq)) {
s <- sum(abs(ans))
term <- term / s
ans <- ans / s
log_scale <- log_scale + log(s)
}
}
ans <- exp(-C + log_scale) * ans
returnType(double(1))
return(ans[,1])
}
)
Sparse version:
```R
expmAvsR <- nimbleFunction(
run = function(i = integer(1), j = integer(1), vals = double(1), v = double(1)){
q <- length(v)
N <- 100
tol <- 1e-8
rescale_freq <- 50
C <- max(abs(vals))
m <- length(i)
vals[i == j] <- vals[i == j] + C
N <- qpois(tol, C, lower.tail=FALSE)
ans <- v
term <- v
log_scale <- 0
for (n in 1:N) {
tmp <- nimNumeric(0, length = q)
for( k in 1:m ){
tmp[i[k]] <- tmp[i[k]] + term[j[k]]*vals[k]
}
term <- tmp / n
ans <- ans + term
if (!(n %% rescale_freq)) {
s <- sum(abs(ans))
term <- term / s
ans <- ans / s
log_scale <- log_scale + log(s)
}
}
ans <- exp(-C + log_scale) * ans
returnType(double(1))
return(ans)
}
)