Skip to content

Conversation

@paul-vdb
Copy link
Contributor

Tests included. Made a new folder to put misc algorithms in.

@paciorek @perrydv No need to merge this any time soon but thought I'd clean up this function and when you have a chance get any feedback. I'm using a sparsity hack for large matrix computation that is optional. Checking with pracma::expm this is actually much more accurate even for a 2x2 matrix. Had to actually use expm::expm for comparisons. In the future we can remove that hack if we have sparsity. It is set up for derivatives. Not a priority but I spent a bunch of time learning this so figured I'd push a tidy version before I fully moved on.

paul-vdb and others added 3 commits August 13, 2025 10:53
Tests included. Made a new folder to put misc algorithms in.
This new function uses scaling and squaring. Need to add testing and will cancel the test runs associated with this.
Documented so hopefully will pass tests now...
@paul-vdb
Copy link
Contributor Author

@paciorek when you get time have a look at miscAlgorithms.R. The funky thing that I did was hack sparsity which means that I check the matrix entries for zero and construct a sparse matrix if requested and then only loop through those values for matrix vector products. I think it's worth the speed up. See sparse = TRUE versions in expmAv. Testing was developed only for small matrices. Could add a larger more complicated matrix if you think that's helpful.

I made expmAv work for AD with a while loop. The length of that while loop will change depending on the values so may need taping resets... Have not tested and happy to just turn off the AD component for now.

@paciorek
Copy link
Contributor

@paul-vdb I'm starting to take a look. I will look at your sparsity hack.

@perrydv can you weigh in on the AD question?

@paciorek
Copy link
Contributor

@paul-vdb if you have a chance, can you fix the roxygen:

paciorek@smeagol:/var/tmp/nimble-dev/nimble/packages (expAv)> make build
Building nimble package in this directory
(R CMD build nimble)
* checking for file ‘nimble/DESCRIPTION’ ... OK
* preparing ‘nimble’:
* checking DESCRIPTION meta-information ... OK
* cleaning src
* running ‘cleanup’
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:14: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:16: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:18: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:20: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:22: unexpected section header '\value'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:25: unexpected section header '\description'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:29: unexpected section header '\details'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:39: unexpected section header '\examples'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:44: unexpected section header '\references'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:50: unexpected section header '\author'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:53: unexpected END_OF_INPUT '
'
* checking for LF line-endings in source and make files and shell scripts
* checking for empty or unneeded directories
Removed empty directory ‘nimble/tests/testthat/_snaps’
Omitted ‘LazyData’ from DESCRIPTION
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:14: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:16: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:18: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:20: unknown macro '\item'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:22: unexpected section header '\value'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:25: unexpected section header '\description'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:29: unexpected section header '\details'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:39: unexpected section header '\examples'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:44: unexpected section header '\references'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:50: unexpected section header '\author'
Warning: /tmp/RtmpvAlej9/Rbuild20f2f022b99a9a/nimble/man/expmAv.Rd:53: unexpected END_OF_INPUT '
'

@paciorek
Copy link
Contributor

A few things I'm noticing:

  • in expmA we should check that tol is length one. It's easy to mean to call expmAv(Av) and accidentally call expmA(A,v), which then tries to use v for tol and then there is an untrapped error related to the length of tol.
  • roxygen for expmA says A must have negative diagonals but that is not enforced (and should it say "non-positive")?
  • roxygen for expmAv doesn't mention anything about diagonals but then does enforce they must be non-positive.
  • let's have an example usage in the roxygen for expmAv.
  • In nimble manual tables 5.4 and 5.5 we use x for the main argument. I'm debating if we want the args here to be expmA(x) and expmAv(x,y). In looking at various other packages though, A and v seem most common. So I guess we should stick with that.
  • I'm not sure about the function names here. RTMB has expAv and implicitly has expm. And there is Matrix::expm. And similarly in the expm package. So I suggest we use expm and expAv.

Also, @paul-vdb I am renaming to miscAlgorithms.R, so please try to account for the capitalization even though I know Windows can make this hard.

@paciorek
Copy link
Contributor

Ok, I've looked at the sparsity hack. Some comments:

  • In expmA, at the end, there should be no need for expM <- expMsmall. Just operate on expMsmall (or call it expM everywhere). Though presumably in C++ we won't make an actual copy.
  • tmp should be of length q[1], no? Though maybe A would never not be square so might not matter.
  • in R and therefore in nimble, matrices are stored contiguously by column ("column-major") so it would be more efficient to have the outer loop be over the columns not over the rows when you populate indexi, indexj, values. However that is only the case if the full matrix won't fit in the CPU cache. And given our known scaling issues, users might not get to working with any matrices so big they don't fit in the cache.
  • relatedly, the standard sparse matrix structure is a bit different than storing row and column indices. Google "compressed sparse column". I don't think it much matters, but we could implement CSC instead of what you've done.
  • if in most use cases A would be sparse, maybe the default for sparse should be TRUE.

I guess I'm somewhat inclined to make the simple switch to reverse the ordering of the two for loops but to leave your use of row and column indices rather than use CSC.

@paul-vdb
Copy link
Contributor Author

Thanks @paciorek . I'll make those updates soon. Good points and happy to change the order of the columns vs rows and will look at what CSC does and see if I can make the hack good enough to just make it default.

@paciorek
Copy link
Contributor

One other note. This might be over-engineering, but we might only want to do sparsity if A is "big enough". That also depends on how many iterations one is likely to do in the while loop.

@paul-vdb
Copy link
Contributor Author

paul-vdb commented Oct 15, 2025

@paciorek I've made a lot of the changes you suggested including the compressed column sparsity. I think it might be over thinking it for small matrices. If they are small, then the sparse bit won't save much time but I don't think it will hurt, especially with the new CCS changes.

I've been trying to get rid of the while loop and instead estimate the number of iterations needed. I'm hoping to have that done today and will push a new version soon. Thanks!!!

I also generalized it for not just non-positive diagonals, but am explicit that the matrix must be square.

Updated both expmAv and expmA to work for any square matrix, not just non-positive diagonals.

Changed ROxygen for updates.

Made both able to have derivatives although this is untested as of yet. Included more tests for larger matrices.

Tested against expm::expm and found comparable speed.
@paul-vdb
Copy link
Contributor Author

@paciorek I made all the requested changes minus a check for length(tol) > 1. I feel a bit dense here but if(dim(tol)[1] > 1) stop("tol' must be a scalar.")` doesn't work as it is declared a scalar and has no length...

All the algorithms from Sherlock assumed a Markov Jump process (non positive diagonals on A). When this isn't the case you don't want to scale the matrix, but you still need to figure out how many iterations to run. I followed RTMB logic that was more general and used the bound on the spectrum to compute the number of iterations.

I also had issues with gradients and qpois which is what is in the Sherlock paper. Instead of qpois he offers another algorithm to efficient compute the right number of iterations. I hacked that algorithm to be close enough, just the average between the upper and lower bound, with some basic checks to see if it's works and it seems to. Now, the while loop is back to a for loop and both expmA and expmAv have buildDerivs = TRUE and compile. However, I didn't do any testing on the accuracy of the derivatives. I suspect that they will need to be retaped on each call due to the number of iterations of the for loop changing in response to the values of A.

It might be best to just turn them off for now and come back around on that in the next release...

derivs were not ready so not making them available yet. Need to do some testing. Mainly with the for loop depending on the matrix provided.
@paul-vdb
Copy link
Contributor Author

Note I removed buildDerivs = TRUE for now. Didn't have it working and can circle back on that for the next release.

@paciorek
Copy link
Contributor

Just responding in terms of tol -- I was playing around with the R versions of the functions, uncompiled, and that's where I made the mistake of using expmA(A,v). Feel free to just ignore that comment.

@paciorek
Copy link
Contributor

@paul-vdb I'm happy to merge this in if you're happy with the implementation and testing. I haven't dug into the algorithmic details.

Could you let me know your thoughts about function and argument naming (see my post from a few posts ago in this discussion).

@paul-vdb
Copy link
Contributor Author

@paciorek I missed the naming bit going through that list... Pushing those changes now.
Read through documentation and made edits.
Added a test for more extreme values that will lead to matching overflow with expm and RTMB.
I am feeling good about it.

I commented out the getNPrec function I had added to be able to do derivatives and defaulted back to qpois to find the number of iterations for a level of precision.

Name changes
added slightly more extreme test
switched from getNPrec algorithm back to simpler qpois.
@paciorek paciorek merged commit fa86a1e into devel Oct 28, 2025
0 of 8 checks passed
@paciorek paciorek deleted the expAv branch October 28, 2025 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants