diff --git a/content/blog/2024-09-18-nlmixr2-3.0.0-release/featured.png b/content/blog/2024-09-18-nlmixr2-3.0.0-release/featured.png new file mode 100644 index 0000000..673dd69 Binary files /dev/null and b/content/blog/2024-09-18-nlmixr2-3.0.0-release/featured.png differ diff --git a/content/blog/2024-09-18-nlmixr2-3.0.0-release/index.Rmd b/content/blog/2024-09-18-nlmixr2-3.0.0-release/index.Rmd new file mode 100644 index 0000000..53a949f --- /dev/null +++ b/content/blog/2024-09-18-nlmixr2-3.0.0-release/index.Rmd @@ -0,0 +1,642 @@ +--- +title: nlmixr2 3.0 +author: Matthew Fidler +date: '2024-09-18' +slug: [] +categories: [rxode2, nlmixr2, babelmixr2] +tags: [] +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +library(rxode2) +library(nlmixr2) +``` +# nlmixr2 3.0 is here + +`nlmixr2` 3.0 has been released and most of the related packages in +the `nlmixr2` ecosystem have been updated as well. Since there were a +few non-backward compatible changes and breaking changes, the version +has been bumped from 2 to 3. Most code will run the same, but because +of the breaking change, we changed the major version. + +The big changes are: + +- Non abi binary linkages between every package. This means you will not + get messages about needing to re-compile the packages during CRAN + updates (and it means much more stable packages and the ability to + make more frequent updates). To me, this is as important an + achievement to maintainability as removing python's `sympy` from + `nlmixr`. + +- New syntax for lower triangular matrices. I will likely discuss why + in a later blog post, but for now I will show the differences: + +```{r lotri} +# The old syntax (which is still supported) +lotri({ + a+b ~ c(1, + 0.1, 1) + c ~ 1 +}) + +# the new syntax, lower triangular matrices can be suplied row by row; +# if restarting it would restart the block matrix +lotri({ + a ~ 1 + b ~ c(0.1, 1) + c ~ 1 +}) +``` + +- `rxode2`/`nlmixr2` will re-order matrices as needed to make them in + a banded-matrix format preferred by the estimation tool. This is + done when parsing the matrices but can be seen here: + +```{r rcm} +m <- lotri({ + a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + + p ~ c(0.4, 0, 0.3, 0, 0, 0, -0.1, 0, 0, 0.2, 0, 0, 0, + 0, 0.5, 0, 0, 0, 0, 0, 1.3, 0, 0, 0, 0, 0, -0.6, 0.8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -0.2, 0, 0.3, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2.1, 0.2, 0, 0, 0.2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.4, 0, 0, 0, 0, 0, -1.1, + 0.9, 0, 0, 0, 0, 0, 0, 0, 4.7, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0.5, 0, 0.2, 0, 0, 0, 1.9) +}) + +# note that m is in a non-banded matrix format +m + +# Internally the lotri function rcm (standing for the reverse Cuthill +# McKee (RCM) algorithm algorithm) creates the banded matrix +m <- lotri::rcm(m) + +# If you specified this as an ini block, you would then see the +# following code instead: +lotri::lotriAsExpression(m, useIni=TRUE) + +# which is both much easier to read and also helps the nlmixr2 +# estimation routines. + +# If you prefer the old syntax on output you can use +# options(lotri.plusNames=TRUE) +withr::with_options(list(lotri.plusNames=TRUE),{ + lotri::lotriAsExpression(m, useIni=TRUE) +}) +``` + +- In `rxode2` simulations, `iCov` has been refactored to run faster + than it did before. + +- In `rxode2` you can now assign a variable to a string (`a <- "string"`) + +- Now you can specify per-variable interpolation (`linear(WT)`, or + `nocb(WT)` etc) inside your model. + +- There is a new type of handling of `mu`-referenced equations, called + `mu`3. (We already have + [`mu`2](https://blog.nlmixr2.org/blog/2024-07-08-mu2/)). `saem` will + now message if it is using `mu`2 or `mu`3 referencing while + modeling. Standard mu referencing is not called out. + + - There is a new log-likelihood profiling functions exported for use + with you `nlmixr2` fits. + +I have a [technical discussion on why/how things have changed](#api), +or [jump to the overall changes](#Changelog). The next section +discusses why our dependencies have changed. + +## Why has the number of dependencies changed? + +In +[2022](https://blog.nlmixr2.org/blog/2022-10-19-rxode2-2.0.9-release/), +`CRAN` requested we split `rxode2`, giving us more packages to +maintain. This was mostly because of compilation time, and the +`stan`-based, large-compilation time `rxode2ll` is still its own +separate package. + +In the last release, they requested that we reduce the number of +packages for `rxode2` because it took too much of their time to +maintain the split they requested. + +This is because the `rxode2`, `rxode2random`, `rxode2et` and +`rxode2parse` have [binary +inter-dependencies](https://cran.r-project.org/doc/manuals/R-exts.html#Linking-to-native-routines-in-other-packages-1) +between all of the packages that were split off. + +This means that `rxode2`/`nlmixr2est` had to depend on exact versions +of the packages `rxode2random`, `rxode2parse`, and `rxode2et`. If +this did not happen, you could crash R. A sentence in their manual +talks about how fragile this is: + +> **NB:** this mechanism is fragile, as changes to the interface +> provided by **packA** have to be recognised by **packB**. The +> consequences of not doing so have included serious corruption to the +> memory pool of the R session. Either **packB** has to depend on the +> exact version of packA or there needs to be a mechanism for +> **packB** to test at runtime the version of **packA** it is linked +> to matches that it was compiled against. + +I can see how this was very painful to maintain on CRAN's end (and it +was difficult to maintain from my end as well). + +I must say, that CRAN is a small group of volunteers who manage a +great number of very useful R packages. They make sure +that the packages more or less interact well with each other, which is +actually a non-trivial task. This makes `CRAN` just work, when compared +to other repositories where this is not enforced. + +So, in short, "CRAN gives, CRAN takes away, blessed be the name of +CRAN" (to mis-quote Job). + +Note that I have taken other steps to make this easier to maintain on +my side and CRAN's side too. For those who are interested I will talk +about them in below in the [`ABI/ABI` section](#api). + +
nlmixr2
3.0 has been released and most of the related packages in
+the nlmixr2
ecosystem have been updated as well. Since there were a
+few non-backward compatible changes and breaking changes, the version
+has been bumped from 2 to 3. Most code will run the same, but because
+of the breaking change, we changed the major version.
The big changes are:
+Non abi binary linkages between every package. This means you will not
+get messages about needing to re-compile the packages during CRAN
+updates (and it means much more stable packages and the ability to
+make more frequent updates). To me, this is as important an
+achievement to maintainability as removing python’s sympy
from
+nlmixr
.
New syntax for lower triangular matrices. I will likely discuss why +in a later blog post, but for now I will show the differences:
# The old syntax (which is still supported)
+lotri({
+ a+b ~ c(1,
+ 0.1, 1)
+ c ~ 1
+})
+## a b c
+## a 1.0 0.1 0
+## b 0.1 1.0 0
+## c 0.0 0.0 1
+# the new syntax, lower triangular matrices can be suplied row by row;
+# if restarting it would restart the block matrix
+lotri({
+ a ~ 1
+ b ~ c(0.1, 1)
+ c ~ 1
+})
+## a b c
+## a 1.0 0.1 0
+## b 0.1 1.0 0
+## c 0.0 0.0 1
+rxode2
/nlmixr2
will re-order matrices as needed to make them in
+a banded-matrix format preferred by the estimation tool. This is
+done when parsing the matrices but can be seen here:m <- lotri({
+ a + b + c + d + e + f + g + h + i + j + k + l + m + n + o +
+ p ~ c(0.4, 0, 0.3, 0, 0, 0, -0.1, 0, 0, 0.2, 0, 0, 0,
+ 0, 0.5, 0, 0, 0, 0, 0, 1.3, 0, 0, 0, 0, 0, -0.6, 0.8,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -0.2, 0, 0.3,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2.1, 0.2, 0, 0, 0.2,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.4, 0, 0, 0, 0, 0, -1.1,
+ 0.9, 0, 0, 0, 0, 0, 0, 0, 4.7, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0.5, 0, 0.2, 0, 0, 0, 1.9)
+})
+
+# note that m is in a non-banded matrix format
+m
+## a b c d e f g h i j k l m n o p
+## a 0.4 0.0 0 -0.1 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.2 0.0 0.0
+## b 0.0 0.3 0 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
+## c 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
+## d -0.1 0.0 0 0.2 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.2 0.0 0.0
+## e 0.0 0.0 0 0.0 0.5 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
+## f 0.0 0.0 0 0.0 0.0 1.3 -0.6 0 0.0 0.0 0.0 0.0 0.0 0.0 -1.1 0.0
+## g 0.0 0.0 0 0.0 0.0 -0.6 0.8 0 0.0 0.0 0.0 0.0 0.0 0.0 0.9 0.0
+## h 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
+## i 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.0
+## j 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 0.9 0.0 -0.2 0.0 0.0 0.0 0.5
+## k 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 0.0 0.9 0.0 0.0 0.0 0.0 0.0
+## l 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 -0.2 0.0 0.3 0.0 0.0 0.0 0.2
+## m 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 2.1 0.0 0.0 0.0
+## n 0.2 0.0 0 0.2 0.0 0.0 0.0 0 0.0 0.0 0.0 0.0 0.0 0.4 0.0 0.0
+## o 0.0 0.0 0 0.0 0.0 -1.1 0.9 0 0.0 0.0 0.0 0.0 0.0 0.0 4.7 0.0
+## p 0.0 0.0 0 0.0 0.0 0.0 0.0 0 0.0 0.5 0.0 0.2 0.0 0.0 0.0 1.9
+# Internally the lotri function rcm (standing for the reverse Cuthill
+# McKee (RCM) algorithm algorithm) creates the banded matrix
+m <- lotri::rcm(m)
+
+# If you specified this as an ini block, you would then see the
+# following code instead:
+lotri::lotriAsExpression(m, useIni=TRUE)
+## ini({
+## p ~ 1.9
+## l ~ c(0.2, 0.3)
+## j ~ c(0.5, -0.2, 0.9)
+## o ~ 4.7
+## g ~ c(0.9, 0.8)
+## f ~ c(-1.1, -0.6, 1.3)
+## n ~ 0.4
+## d ~ c(0.2, 0.2)
+## a ~ c(0.2, -0.1, 0.4)
+## m ~ 2.1
+## k ~ 0.9
+## i ~ 0.2
+## e ~ 0.5
+## b ~ 0.3
+## h ~ 0
+## c ~ 0
+## })
+# which is both much easier to read and also helps the nlmixr2
+# estimation routines.
+
+# If you prefer the old syntax on output you can use
+# options(lotri.plusNames=TRUE)
+withr::with_options(list(lotri.plusNames=TRUE),{
+ lotri::lotriAsExpression(m, useIni=TRUE)
+})
+## ini({
+## p + l + j ~ c(1.9, 0.2, 0.3, 0.5, -0.2, 0.9)
+## o + g + f ~ c(4.7, 0.9, 0.8, -1.1, -0.6, 1.3)
+## n + d + a ~ c(0.4, 0.2, 0.2, 0.2, -0.1, 0.4)
+## m ~ 2.1
+## k ~ 0.9
+## i ~ 0.2
+## e ~ 0.5
+## b ~ 0.3
+## h ~ 0
+## c ~ 0
+## })
+In rxode2
simulations, iCov
has been refactored to run faster
+than it did before.
In rxode2
you can now assign a variable to a string (a <- "string"
)
Now you can specify per-variable interpolation (linear(WT)
, or
+nocb(WT)
etc) inside your model.
There is a new type of handling of mu
-referenced equations, called
+mu
3. (We already have
+mu
2). saem
will
+now message if it is using mu
2 or mu
3 referencing while
+modeling. Standard mu referencing is not called out.
There is a new log-likelihood profiling functions exported for use
+with you nlmixr2
fits.
I have a technical discussion on why/how things have changed, +or jump to the overall changes. The next section +discusses why our dependencies have changed.
+In
+2022,
+CRAN
requested we split rxode2
, giving us more packages to
+maintain. This was mostly because of compilation time, and the
+stan
-based, large-compilation time rxode2ll
is still its own
+separate package.
In the last release, they requested that we reduce the number of
+packages for rxode2
because it took too much of their time to
+maintain the split they requested.
This is because the rxode2
, rxode2random
, rxode2et
and
+rxode2parse
have binary
+inter-dependencies
+between all of the packages that were split off.
This means that rxode2
/nlmixr2est
had to depend on exact versions
+of the packages rxode2random
, rxode2parse
, and rxode2et
. If
+this did not happen, you could crash R. A sentence in their manual
+talks about how fragile this is:
++NB: this mechanism is fragile, as changes to the interface +provided by packA have to be recognised by packB. The +consequences of not doing so have included serious corruption to the +memory pool of the R session. Either packB has to depend on the +exact version of packA or there needs to be a mechanism for +packB to test at runtime the version of packA it is linked +to matches that it was compiled against.
+
I can see how this was very painful to maintain on CRAN’s end (and it +was difficult to maintain from my end as well).
+I must say, that CRAN is a small group of volunteers who manage a
+great number of very useful R packages. They make sure
+that the packages more or less interact well with each other, which is
+actually a non-trivial task. This makes CRAN
just work, when compared
+to other repositories where this is not enforced.
So, in short, “CRAN gives, CRAN takes away, blessed be the name of +CRAN” (to mis-quote Job).
+Note that I have taken other steps to make this easier to maintain on
+my side and CRAN’s side too. For those who are interested I will talk
+about them in below in the ABI/ABI
section.
rxode2
New profile()
method for likelihood profiling (Issue #1)
Changes to work with new rxode2
No binary linking to rxode2
, lbfgsb3c
and n1q1
, which means
+that updating these will not make nlmixr2est
crash without
+recompiling.
New mu
3 referencing will take context from the model to see if the
+algebraic expression can be completed from defined model variables;
+These variable would have to be unique.
The model properties was moved from $params
to $props
so it does
+not conflict with the low level rxode2
model $params
Error when specifying wd
without modName
With Linear and midpoint of a time between two points, how rxode2
+handles missing values has changed. When the missing value is lower
+than the requested time, it will look backward until it finds the
+first non-missing value (or if all are missing start looking
+forward). When the missing value is higher than the requested time,
+the algorithm will look forward until it finds the first non-missing
+value (or if all are missing, start looking backward).
The order of ODEs is now only determined by the order of cmt()
and
+d/dt()
. Compartment properties, tad()
and other compartment
+related variables no no longer affect compartment sorting. The
+option rxode2.syntax.require.ode.first
no longer does anything.
The handling of zeros “safely” has changed (see #775)
+when safeZero=TRUE
and the denominator of a division expression
+is zero, use the Machine’s small number/eps
(you can see this
+value with .Machine$double.eps
)
when saveLog=TRUE
and the x in the log(x)
is less than or
+equal to zero, change this to log(eps)
when safePow=TRUE
and the expression x^y
has a zero for x
+and a negative number for y
replace x
with eps
.
Since the protection for divide by zero has changed, the results +will also change. This is a more conservative protection mechanism +than was applied previously.
Random numbers from rxode2
are different when using dop853
,
+lsoda
or indLin
methods. These now seed the random numbers in
+the same way as liblsoda
, so the random number provided will be
+the same with different solving methods.
The arguments saved in the rxSolve
for items like thetaMat
will
+be the reduced matrices used in solving, not the full matrices (this
+will likely not break very many items)
iCov
is no longer merged to the event dataset. This makes solving
+with iCov
slightly faster (#743)You can remove covariances for every omega by piping with %>% ini(diag())
you can be a bit more granular by removing all
+covariances that have either eta.ka
or eta.cl
by: %>% ini(diag(eta.ka, eta.cl))
or anything with correlations with
+eta.cl
with %>% ini(diag(eta.cl))
You can also remove individual covariances by %>% ini(-cov(a, b))
+or %>% ini(-cor(a,b))
.
You can specify the type of interpolation applied for added dosing
+records (or other added records) for columns that are kept with the
+keep=
option in rxSolve()
. This new option is
+keepInterpolation
and can be locf
for last observation carried
+forward, nocb
which is the next observation carried backward, as
+well as NA
which puts a NA
in all imputed data rows. See #756.
Note: when interpolation is linear/midpoint for +factors/characters it changes to locf with a warning (#759)
Also note, that the default keep interpolation is na
Now you can specify the interpolation method per covariate in the model:
+linear(var1, var2)
says both var1
and var2
would use linear
+interpolation when they are a time-varying covariate. You could
+also use linear(var1)
locf()
declares variables using last observation carried forward
nocb()
declares variables using next observation carried backward
midpoint()
declares variables using midpoint interpolation
linear()
, locf()
, locb()
, midpoint()
, params()
, cmt()
+and dvid()
declarations are now ignored when loading a rxode2
+model with rxS()
Strings can be assigned to variables in rxode2
.
Strings can now be enclosed with a single quote as well as a double
+quote. This limitation was only in the rxode2 using string since
+the R-parser changes single quotes to double quotes. (This has no
+impact with rxode2({})
and ui/function form).
More robust string encoding for symengine (adapted from
+utils::URLencode()
and utils::URLdecode()
)
Empty arguments to rxRename()
give a warning (#688)
Promoting from covariates to parameters with model piping (via ini()
) now
+allows setting bounds (#692)
Added assertCompartmentName()
, assertCompartmentExists()
,
+assertCompartmentNew()
, testCompartmentExists()
,
+assertVariableExists()
testVariableExists()
,
+assertVariableNew()
, assertVariableName()
, and
+assertParameterValue()
to verify that a value is a valid nlmixr2
+compartment name, nlmixr2 compartment/variable exists in the model,
+variable name, or parameter value (#726; #733)
Added assertRxUnbounded()
, testRxUnbounded()
, warnRxBounded()
+to allow nlmixr2
warn about methods that ignore boundaries #760
Added functions tad0()
, tafd0()
, tlast0()
and tfirst0()
that
+will give 0
instead of NA
when the dose has not been
+administered yet. This is useful for use in ODEs since NA
s will
+break the solving (so can be used a bit more robustly with models
+like Weibull absorption).
rxode2
is has no more binary link to lotri
, which means that
+changes in the lotri
package will not require rxode2
to be
+recompiled (in most cases) and will not crash the system.
rxode2
also has no more binary linkage to PreciseSums
The binary linkage for dparser
is reduced to C structures only,
+making changes in dparser less likely to cause segmentation faults
+in rxode2
if it wasn’t recompiled.
A new model property has been added to $props$cmtProp
and
+$statePropDf
. Both are data-frames showing which compartment has
+properties (currently ini
, f
, alag
, rate
and dur
)
+in the rxode2
ui model. This comes from the lower
+level model variable $stateProp
which has this information
+encoded in integers for each state.
A new generic method rxUiDeparse
can be used to deparse meta
+information into more readable expressions; This currently by
+default supports lower triangular matrices by lotri, but can be
+extended to support other types of objects like ’nlmixr2’s
+foceiControl()
for instance.
Fix ui$props$endpoint
when the ui endpoint is defined in terms of
+the ode instead of lhs. See #754
Fix ui$props
when the ui is a linear compartment model without ka
defined.
Model extraction modelExtract()
will now extract model properties. Note that the model property of alag(cmt)
and lag(cmt)
will give the same value. See #745
When assigning reserved variables, the parser will error. See #744
Linear interpolation will now adjust the times as well as the values
+when NA
values are observed.
Fix when keeping data has NA
values that it will not crash R; Also
+fixed some incorrect NA
interpolations. See #756
When using cmt()
sometimes the next statement would be corrupted
+in the normalized syntax (like for instance locf
); This bug was
+fixed (#763)
keep
will now error when trying to keep items that are in the
+rxode2 output data-frame and will be calculated (#764)
rxode2parse
, rxode2random
, and
+rxode2et
into this package; The changes in each of the packages are
+now placed here:Fix a bug when simulating nested variables for random simulation
+(was in rxode2random
#25)
As requested by CRAN remove the C
code SET_TYPEOF
which is no
+longer part of the C
R
API
for rxode2parse
I am writing this technical aside because I couldn’t find a solution
+for this problem on the internet or in the writing R extension manual
+(my internet searching skills could probably be improved). Also note
+that if you are using Rcpp
and exposing the C++
calls, this is
+done for you (but not for C
calls).
An ABI
stands for application binary interface and is determined by
+the compiler and the platform at the time of compile. Each function
+has an address in memory. This is why when some down-stream
+dependency like rxode2parse
are updated the functions that are
+specified change memory addresses. When rxode2
tries to call a
+function from the parser, R memory’s is corrupted and R crashes. Of
+course you can fix this by recompiling the rxode2
version but
+usually waiting for the binary that links was a better
+solution.
Addresses of other items other than C functions can also change (like +C/C++ structures), so direct access of these items is also something +that can cause memory corruption.
+This ABI interface is what happens when you register a C
callable in
+any package.
An API
is the Application Programming Interface. It has been
+popularized by web API
s and can be created for your R package with
+packages like plumber
, however it can be any application programming
+interface.
To create the API
I needed to do two things:
Figure out what functions I would be using (and therefore allow
+others to use) from rxode2
.
Figure out what structures that I would need to access (like the ODE +states, the calculated values of the equations etc). This will also +allow other packages to use these functions in their code as well.
The first thing that will need to be done is to create thin accessing
+functions for the underlying C
structures. Since we are using R
+which is really based on C
, they will need to be C compatible. The
+code
+below
+shows how to access the calculated variables inside rxode2
:
double *getIndLhs(rx_solving_options_ind* ind) {
+ return ind->lhs;
+}
+The next step is to create a function that grabs the memory addresses
+of these thin C structure access functions as well as any other functions
+that you will expose in the API
.
You wrap all of these functions and place them into a R list structure
+using R’s function R_MakeExternalPtrFn()
. An example function is
+below and is based on the actual rxode2
API pointer
+function:
SEXP _rxode2_rxode2Ptr(void) {
+ int pro = 0; // Counter for the number of PROTECT calls
+ // Create an external pointer for _lotriLstToMat
+ SEXP rxode2rxRmvnSEXP = PROTECT(R_MakeExternalPtrFn((DL_FUNC)&_rxode2_rxRmvnSEXP, R_NilValue, R_NilValue)); pro++;
+ // more code here
+ // Unprotect all protected objects
+ UNPROTECT(pro);
+
+ // Return the list of external pointers
+ return ret;
+}
+After creating the C
function, you register it as you a standard
+function R calls from C
. In this case we have:
void R_init_rxode2(DllInfo *info){
+ R_CallMethodDef callMethods[] = {
+ {"_rxode2_rxode2Ptr", (DL_FUNC) &_rxode2_rxode2Ptr, 0},
+ // more registered functions
+ {NULL, NULL, 0}
+ };
+ static const R_CMethodDef cMethods[] = {
+ {NULL, NULL, 0, NULL}
+ };
+
+ R_registerRoutines(info, cMethods, callMethods, NULL, NULL);
+ R_useDynamicSymbols(info, FALSE);
+}
+and of course you need to register the functions to be seen in R, +which needs the following line in the NAMESPACE:
+useDynLib(rxode2, .registration=TRUE)
+Which for rxode2
comes from the roxygen2 line:
#' @useDynLib rxode2, .registration=TRUE
+Once all of that is done, you can create an exported R function that +lists the function pointers:
+#' Get the rxode2 function pointers
+#'
+#' This function is used to get the function pointers for rxode2. This is
+#' used to allow rxode2 to have binary linkage to nlmixr2est.
+#'
+#' @return a list of function pointers
+#' @export
+#' @author Matthew L. Fidler
+#' @examples
+#' .rxode2ptrs()
+.rxode2ptrs <- function() {
+ .Call(`_rxode2_rxode2Ptr`, PACKAGE = "rxode2")
+}
+Which (of course) works for rxode2
:
rxode2::.rxode2ptrs()[1:3]
+## $rxode2rxRmvnSEXP
+## <pointer: 0x735bc70c68f0>
+##
+## $rxode2rxParProgress
+## <pointer: 0x735bc72c49c0>
+##
+## $rxode2getRxSolve_
+## <pointer: 0x735bc72c5ba0>
+For me, I want the user interface functions to be the same as what I
+declared in the API
, used in the second package (like nlmixr2est
).
To do this, I create a header called
+rxode2ptr.h
+in the inst/include
directory.
In that header, I declare the type definition of each of the functions +that will be accessed in the other package as well as an external +function pointer to the exported function:
+typedef double *(*getIndLhs_t)(rx_solving_options_ind* ind);
+extern getIndLhs_t getIndLhs;
+Then create a static inline
function (that is it is only defined
+when called) that takes the pointers from the rxode2::.rxode2ptrs()
+and assigns them the function pointer getIndLhs
:
static inline SEXP iniRxodePtrs0(SEXP p) {
+ if (_rxode2_rxRmvnSEXP_ == NULL) { // only assign if not already assigned
+ // more code
+ getIndLhs = (getIndLhs_t) R_ExternalPtrAddrFn(VECTOR_ELT(p, 23));
+ // more code
+ }
+ return R_NilValue;
+}
+Note that this function is keyed to the number of API elements exposed
+and assumes a specific order. Therefore, you should add elements
+sequentially and make sure API
needs less elements than exported by
+rxode2
(which is coded in as an exception in nlmixr2est
)
Finally we create code that defines the function pointers in a single +file and creates an R expression that will register all the C pointer +functions.
+#define iniRxode2ptr \
+ _rxode2_rxRmvnSEXP_t _rxode2_rxRmvnSEXP_ = NULL; \
+ // more code
+getIndLhs_t getIndLhs = NULL; \
+SEXP iniRxodePtrs(SEXP ptr) { \
+ return iniRxodePtrs0(ptr); \
+} \
+This is all that needs to be done for rxode2
to expose the API
nlmixr2est
For most C
(or C++
) files if the header has an accommodation for
+C++, simply using:
#include <rxode2ptr.h>
+Will register and expose the functions as needed.
+In one file, you will also need to declare the function pointers and +function pointer with the line:
+#include <rxode2ptr.h>
+// if in C++ wrap with extern "C" {}
+
+// if you want to change the name of the function you can use #define
+// iniRxodePtrs _your_fn_name
+
+iniRxode2ptr // do not include a ; since it will be flagged as a significant warning by CRAN
+When registering the Calls inside of nlmixr2est
you will need to add the ``
SEXP iniRxodePtrs(SEXP ptr);
+/// some code
+
+void R_init_nlmixr2est(DllInfo *info){
+ R_CallMethodDef callMethods[] = {
+ {"iniRxodePtrs", (DL_FUNC) &iniRxodePtrs, 1},
+ // more registered functions
+ {NULL, NULL, 0}
+ };
+ static const R_CMethodDef cMethods[] = {
+ {NULL, NULL, 0, NULL}
+ };
+
+ R_registerRoutines(info, cMethods, callMethods, NULL, NULL);
+ R_useDynamicSymbols(info, FALSE);
+}
+Last you need to register the API when loading nlmixr2est
:
# This will be saved when compiled
+# This way you will know if something has changed in the API
+rxode2.api <- names(rxode2::.rxode2ptrs())
+
+.iniRxode2Ptr <- function() {
+ .ptr <- rxode2::.rxode2ptrs()
+ .nptr <- names(.ptr)
+ # Cheks for API changes
+ if (length(rxode2.api) > length(.nptr)) {
+ stop("nlmixr2est requires a newer version of rxode2 api, cannot run nlmixr2est\ntry `install.packages(\"rxode2\")` to get a newer version of rxode2", call.=FALSE)
+ } else {
+ .nptr <- .nptr[seq_along(rxode2.api)]
+ if (!identical(rxode2.api, .nptr)) {
+ .bad <- TRUE
+ stop("nlmixr2est needs a different version of rxode2 api, cannot run nlmixr2est\ntry `install.packages(\"rxode2\")` to get a newer version of rxode2, or update both packages", call.=FALSE)
+ }
+ }
+ .Call(`iniRxodePtrs`, .ptr,
+ PACKAGE = "nlmixr2est")
+}
+
+.onLoad <- function(libname, pkgname) {
+ # other code
+ .iniRxode2Ptr()
+}
+This same procedure can be used to expose all the API
functions to
+R
.