library(dplyr)
library(mrgsolve)
1 Introduction
This blog post introduces some new features / new behavior with mrgsolve versions 0.11.0
and 0.11.1
.
2 Dynamic capture
This is one of my favorite new features. I’ve written about it before, but as a review: we added a capture
argument to mread()
so you can opt in to capture additional model variables in the simulated output.
For example, let’s look at this model:
<- modlib("irm1") mod
Building irm1 ... done.
outvars(mod)
$cmt
[1] "EV1" "CENT" "PERIPH" "RESP" "EV2"
$capture
[1] "CP"
You can see the compartments (cmt
) and other model variables that are getting captured (capture
).
2.1 See model variables
We can now look into the model and see which variables are available to capture. First, coerce mod
to a list()
and then look at the cpp_variables
item:
<- as.list(mod)$cpp_variables
cpp_variables cpp_variables
type var context
1 define CP global
2 define CT global
3 define CLNL global
4 define INH global
This tells us what we could capture. Now that we have the information, let’s capture everything possible:
<- modlib("irm1", capture = cpp_variables$var) mod
Building irm1 ... done.
Now, everything is getting captured:
outvars(mod)
$cmt
[1] "EV1" "CENT" "PERIPH" "RESP" "EV2"
$capture
[1] "CP" "CT" "CLNL" "INH"
and will appear in the simulated output:
mrgsim(mod, events = ev(amt = 100), param = list(VMAX = 2))
Model: irm1
Dim: 26 x 11
Time: 0 to 24
ID: 1
ID time EV1 CENT PERIPH RESP EV2 CP CT CLNL INH
1: 1 0 0.0000 0.00 0.000 5.000 0 0.000 0.0000 1.0000 0.0000
2: 1 0 100.0000 0.00 0.000 5.000 0 0.000 0.0000 1.0000 0.0000
3: 1 1 36.7879 57.40 3.221 2.837 0 2.870 0.3221 0.4107 0.5893
4: 1 2 13.5335 70.69 8.641 1.988 0 3.534 0.8641 0.3614 0.6386
5: 1 3 4.9787 69.58 13.484 1.834 0 3.479 1.3484 0.3650 0.6350
6: 1 4 1.8316 64.48 17.119 1.875 0 3.224 1.7119 0.3828 0.6172
7: 1 5 0.6738 58.87 19.596 1.970 0 2.943 1.9596 0.4046 0.5954
8: 1 6 0.2479 53.77 21.135 2.078 0 2.689 2.1135 0.4266 0.5734
In this example, we could choose from a bunch of pre-processor directives, but the only reason for that is this is the way the model was coded. We can look at another example:
<- modlib("pbpk") mod
Building pbpk ... done.
as.list(mod)$cpp_variables %>% head()
type var context
1 double Vad main
2 double Vbo main
3 double Vbr main
4 double Vgu main
5 double Vhe main
6 double Vki main
2.2 Why would I use this???
Here’s an example: I have a covariate model going and I want to get parameter values given some covariates. I can write:
<- modlib("popex", capture = "CL,V,KA") %>% zero_re() mod
Building popex ... done.
<- tibble(WT = seq(40,140,5))
idata
mrgsim(mod, idata = idata, end = -1)
Model: popex
Dim: 21 x 10
Time: 0 to 0
ID: 21
ID time GUT CENT CL V ECL IPRED DV KA
1: 1 0 0 0 0.6572 13.71 0 0 0 0.5
2: 2 0 0 0 0.7179 15.43 0 0 0 0.5
3: 3 0 0 0 0.7770 17.14 0 0 0 0.5
4: 4 0 0 0 0.8345 18.86 0 0 0 0.5
5: 5 0 0 0 0.8908 20.57 0 0 0 0.5
6: 6 0 0 0 0.9459 22.29 0 0 0 0.5
7: 7 0 0 0 1.0000 24.00 0 0 0 0.5
8: 8 0 0 0 1.0531 25.71 0 0 0 0.5
Now, I have CL
, V
and KA
all calculated as a function of WT
.
3 Simulate with negative times
Until 0.11.0
, it was an error whenever negative times were requested in the simulation. Now, negative times are accepted without error.Keep in mind that the data set still needs to be sorted by time when passing data
.
As an example:
<- modlib("irm2") %>% param(KOUT = 0.05) mod
Building irm2 ... done.
<-
sims %>%
mod ev(amt = 100, ii = 48, addl = 1) %>%
mrgsim(start = -72, end = 240)
sims
Model: irm2
Dim: 314 x 8
Time: -72 to 240
ID: 1
ID time EV1 CENT PERIPH RESP EV2 CP
1: 1 -72 0 0 0 200 0 0
2: 1 -71 0 0 0 200 0 0
3: 1 -70 0 0 0 200 0 0
4: 1 -69 0 0 0 200 0 0
5: 1 -68 0 0 0 200 0 0
6: 1 -67 0 0 0 200 0 0
7: 1 -66 0 0 0 200 0 0
8: 1 -65 0 0 0 200 0 0
plot(sims, "EV1 CP RESP")
4 Path to NM results using NMEXT and NMXML
This is pretty subtle issue but I’ve grown very sensitive to it recently and decided we needed to change this.
When you use the $NMEXT
or $NMXML
blocks, you are asking mrgsolve
to import THETA
, OMEGA
and / or SIGMA
from a completed NONMEM run. And we need to specify the path to those results.
So far, we have had you typically specify the path relative to the current working directory. This was ok, but problems happened if you wanted to use the model from a different working directory (you’d get an error).
So we added an argument called root
to $NMEXT
and $NMXML
that lets you change how that path is interpreted.
4.1 Root path
The current default behavior is to interpret the path relative to the current working directory:
[ nmext ]
= 1001
run = "../model/nonmem" project
This is equivalent to invoking the new root
argument and setting it to "working"
:
[ nmext ]
= 1001
run = "../model/nonmem"
project = "working" root
I am encouraging you to start setting this root
argument to "cppfile"
which tells mrgsolve to interpret the path relative to wherever the model code is located:
[ nmext ]
= 1001
run = "../model/nonmem"
project = "cppfile" root
When you do this, you can load this model from any working directory as long as the file remains in the same location. I am encouraging you to start using this because I think we will eventually make "cppfile"
the default root because it makes so much more sense.
Note that you can always specify an absolute path here and in that case, it doesn’t matter what you use for the root
argument (see the discussion on here()
in the following section).
With here()
Some are also using the here::here()
function. This is totally independent of any mrgsolve
functionality, but here()
will return the absolute path relative to your Rstudio
project root. So, if all my models were in the model/nonmem
directory, I can always get the absolute path using here()
regardless of the working directory and regardless of where the cpp
file is located.
[ nmext ]
= 1001
run = here::here("model/nonmem") project
Note here that the argument / options here are evaluated by the R interpreter so that the here()
call resolves to the correct project directory.
4.2 Select table number in NMEXT
The [ nmext ]
block gains an index argument that will let you select which table in the .ext
file to import from when there are multiple.
- the default is
last
- otherwise, pass an integer (e.g. 1 for first, 3 for 3rd)
- see the
?nmext
help topic for more info on this and other argument / options for thenmxml
block
To select the 2nd table:
[ nmext ]
= 1001
run = "../model/nonmem"
project = "cppfile"
root = 2 index
The default is to select the last
table:
[ nmext ]
= 1001
run = "../model/nonmem"
project = "cppfile"
root = "last" // "first" index
4.3 Reveal results file
When you import nonmem results from an .ext
file (via [nmext]
) or an .xml
file (via [nmxml]
), the source file is now saved in the model object so that it can be checked later.
The internal model library contains an embedded nonmem run numbered 1005
; this run imports THETA
, OMEGA
and SIGMA
from a finished nonmem run. You can load this model with the modlib()
function.
<- modlib("1005") mod
Loading required namespace: xml2
Building 1005 ... done.
By coercing the model to list and checking the nm_import
item, you’ll get a character vector with the full (absolute) path to either the .ext
or .xml
file that was imported from
as.list(mod)$nm_import
[1] "/Users/kyleb/renv/renv/library/R-4.4/aarch64-apple-darwin20/mrgsolve/nonmem/1005/1005.xml"
5 Programmatic block initialization
I don’t expect you’ll be using this much but when you need it it will come in handy.
For example, let’s say you wanted to write a model with a 30 x 30 omega matrix. This would be a pain to code given the current syntax for $OMEGA
. I recently refactored and standardized the way you can initialize this block programmatically.
<- '
code $OMEGA @as_object
matrix(0, nrow = 30, ncol = 30)
'
<- mcode("omega", code, compile = FALSE)
mod
omat(mod)
We didn’t run the code because the screen would fill up, but you can try it. You will see that $OMEGA
is a 30 x 30 matrix and it was easy to code. Obviously, you’ll have to update this with all the elements but we have given you the ability to easily initialize this matrix.
In the example, we used the @as_object
directive. The other way to do it would be to create the object in $ENV
and then use @object
:
$ENV
<- matrix(0, nrow = 30, ncol = 30)
foo
$OMEGA @object foo
With this approach, use @object
and then name the object that you want to use to populate the block.
Other blocks that you can use this programmatic initialization:
$SIGMA
- return a matrix$CMT
- return a character vector of compartment names$INIT
- return a named list$PARAM
- return a named list$THETA
- return a numeric vector$FIXED
- return a named list
For example:
<- '
code $PARAM @as_object
x <- seq(1,26)
names(x) <- letters
as.list(x)
'
<- mcode("letters", code, compile = FALSE)
mod param(mod)
Model parameters (N=26):
name value . name value
a 1 | n 14
b 2 | o 15
c 3 | p 16
d 4 | q 17
e 5 | r 18
f 6 | s 19
g 7 | t 20
h 8 | u 21
i 9 | v 22
j 10 | w 23
k 11 | x 24
l 12 | y 25
m 13 | z 26
6 Check User Interrupt
Starting with version 0.11.1
, we will start periodically checking for the user interrupt signal (Esc
or Control-C
) as the simulation proceeds. This will let you kill a long-running simulation mid-stream. You can change the check interval by the interrupt
argument to mrgsim()
. Reduce the number to check more frequently and pass -1
to not check at all.