
rxode2 and PKPDsim: A Feature Comparison
Source:vignettes/articles/rxode2-pkpdsim-comparison.Rmd
rxode2-pkpdsim-comparison.RmdIntroduction
Both rxode2 and PKPDsim are open-source R packages for ODE-based pharmacokinetic-pharmacodynamic simulation. They occupy a similar niche and share many capabilities, but differ in model language, solver design, and intended use cases.
rxode2 uses its own domain-specific mini-language (
d/dt(cmt)), compiles it to C, and solves using a thread-safe C LSODA implementation with OpenMP parallelism across subjects. It integrates tightly with nlmixr2 for parameter estimation via symbolic Jacobians.PKPDsim uses C++ array notation (
dAdt[1] = ...) for ODE definitions, compiled vianew_ode_model(), and solves using the Boost::odeint C++ library. It separates a fast inner solver (sim_core()) from the full-featured outer function (sim()) and has a built-in literature model library and model-package export system. It was developed by InsightRX with a focus on clinical dose individualization.
Both are on CRAN and are MIT or similarly permissive open-source licenses.
Shared capabilities
- ODE-based PK/PD simulation
- Between-subject variability (BSV / IIV)
- Residual error models
- Time-varying covariates
- Population simulation across many subjects
- Complex dosing regimens (bolus, infusion, oral)
- Integration with nlmixr2 for parameter estimation
- C/C++ compiled model code for speed
Features rxode2 has that PKPDsim does not
Readable domain-specific model language
rxode2 uses a natural ODE notation that closely mirrors mathematical writing:
library(rxode2)
mod <- rxode2({
KA <- exp(tka + eta.ka)
CL <- exp(tcl + eta.cl)
d/dt(depot) <- -KA * depot
d/dt(centr) <- KA * depot - CL / V * centr
cp <- centr / V
})PKPDsim uses C++ array indexing (A[1],
dAdt[1]) which is more verbose and requires familiarity
with zero-based vs one-based indexing conventions:
NONMEM-compatible event datasets
rxode2 accepts standard NONMEM-format data frames (with
EVID, AMT, CMT,
TIME, ID, RATE, etc.) directly as
event tables. This makes it straightforward to reuse datasets from
NONMEM workflows or produce datasets compatible with multiple tools.
PKPDsim uses its own new_regimen() API for dosing, which
is clean and simple but is not directly interchangeable with
NONMEM-format datasets.
OpenMP parallelism across subjects
rxode2’s LSODA solver is implemented in thread-safe C, enabling genuine parallel ODE solving across subjects via OpenMP within a single R process. PKPDsim loops over subjects in R, calling the Boost::odeint C++ solver for each subject sequentially.
As PKPDsim’s own documentation notes, rxode2 is substantially faster
than PKPDsim in iterative contexts (e.g., population estimation or
optimal design) because rxode2 separates pre- and post-processing from
the solver by default, while PKPDsim’s sim() includes this
overhead on every call. PKPDsim’s sim_core() mitigates this
but requires the user to manually manage the separation.
# rxode2: pre/post processing is separated by default
# Iterative calls re-use the compiled model with minimal overhead
rxSolve(mod, et, params = p) # fast in any context
# PKPDsim: use sim_core() for iterative use to avoid repeated overhead
design <- sim(mod, regimen = r, return_design = TRUE)
sim_core(design = design, ode = mod) # fast inner loop onlyModel piping for incremental modification
rxode2 model objects support the native R pipe (|>)
to clone and modify a model without rewriting the full
specification.
base <- rxode2({
d/dt(depot) <- -KA * depot
d/dt(centr) <- KA * depot - CL / V * centr
cp <- centr / V
})
full <- base |>
model({ KA <- exp(tka + eta.ka); CL <- exp(tcl + eta.cl) },
append = FALSE) |>
ini({ tka <- log(0.5); eta.ka ~ 0.09; tcl <- log(4); eta.cl ~ 0.09; V <- 10 }) |>
model(cp ~ add(add.sd), append = TRUE) |>
ini(add.sd = 0.5)PKPDsim has no equivalent R-level model modification API; changes
require editing the new_ode_model() call.
Symbolic Jacobians and exact gradients for estimation
rxode2 automatically derives the symbolic Jacobian and forward- sensitivity equations used by nlmixr2’s FOCEi algorithm for exact gradient-based estimation. PKPDsim’s nlmixr2 integration uses finite-difference gradients.
1–3 compartment analytical solutions with exact gradients
linCmt() provides one-, two-, and three-compartment
analytical PK solutions with gradients computed via Stan math
auto-differentiation. PKPDsim’s reparametrization argument
offers standardised re-parameterisation for 1–3-compartment models, but
these are still solved as ODEs rather than analytically.
Features available in both tools (with different approaches)
Literature model libraries
Both tools provide curated libraries of published PK/PD models, though they are organised differently.
PKPDsim ships models as self-contained installable R packages via its own distribution system:
# See what models are available
available_default_literature_models()
# Install and use a literature model
install_default_literature_model("pk_busulfan_mccune")
library(pkbusulfanmccune)
sim(
ode = pkbusulfanmccune::model(),
parameters = pkbusulfanmccune::parameters(),
regimen = new_regimen(amt = 100, n = 4, interval = 6)
)rxode2 models from the literature are available via the
nlmixr2lib package (part of the nlmixr2 ecosystem), which
provides a collection of published models as rxode2 model functions
ready for simulation or estimation:
Inter-occasion variability (IOV)
Both tools support inter-occasion variability, with different syntax.
In rxode2, IOV is specified in the ini({}) block using
the | occ condition syntax, then included in the model
equation:
mod <- rxode2(function() {
ini({
tcl <- log(4)
eta.cl ~ 0.09 # IIV on CL
iov.cl ~ 0.04 | occ # IOV on CL, keyed by occasion variable `occ`
})
model({
CL <- exp(tcl + eta.cl + iov.cl)
d/dt(centr) <- -(CL / V) * centr
})
})PKPDsim specifies IOV via the dedicated iov argument to
new_ode_model():
Features PKPDsim has that rxode2 does not
Model export as installable R packages
PKPDsim can export a compiled model as a proper R package (with
versioning, documentation, and default parameters) via the
package argument to new_ode_model(). This
makes it easy to share a specific model version as a reproducible,
installable unit.
new_ode_model(
code = "dAdt[1] = -(CL/V)*A[1]",
obs = list(cmt = 1, scale = "V"),
parameters = list(CL = 5, V = 50),
package = "myPKmodel",
version = "1.0.0"
)rxode2 models can be included in packages (see the
rxUse() vignette) but there is no single-function
model-package export workflow.
Event-specific C++ hooks (pk_code,
dose_code)
new_ode_model() accepts pk_code (C++ code
executed at every event) and dose_code (C++ code executed
only at dose events). These allow model-specific logic — such as
parameter recomputation at dose time or post-dose state resets — to be
embedded directly in the compiled model.
mod <- new_ode_model(
code = "dAdt[1] = -(CL/V)*A[1];",
pk_code = "CL = TVCL * pow(WT/70, 0.75);", # recompute at every event
dose_code = "A[1] = A[1] + oral_dose_fraction * dose;"
)rxode2 handles event-specific behaviour through model equations and the event table but does not expose separate C++ hooks at the event level.
Mixture models as a first-class argument
PKPDsim supports mixture models (subpopulations with different
parameter values and associated probabilities) directly via the
mixture argument to new_ode_model():
mod <- new_ode_model(
code = "dAdt[1] = -(CL/V)*A[1];",
mixture = list(
CL = list(values = c(2, 10), probability = 0.8)
)
)rxode2 supports within-model mixture via the mix()
function, but it is not a single dedicated argument at the
model-definition level.
Summary table
| Feature | rxode2 | PKPDsim |
|---|---|---|
| License | GPL ≥ 3 | MIT |
| CRAN | yes | yes |
| ODE language | rxode2 mini-language (d/dt(cmt)) |
C++ array notation (dAdt[1]) |
| Solver | thread-safe C LSODA; Fortran LSODA; DOP853 | Boost::odeint C++ |
| OpenMP parallelism | yes (thread-safe solver) | no (R loop over subjects) |
| Iterative-use speed | fast by default (separated pre/post) | use sim_core() explicitly |
| NONMEM-format datasets | yes | no (new_regimen() API) |
| 1–3 cmt analytical solution |
linCmt() with exact gradients |
reparametrization (ODE-based) |
| Symbolic Jacobians / sensitivities | yes | no (finite differences) |
| nlmixr2 estimation | yes (exact gradients via FOCEi) | yes (finite-difference gradients) |
Model piping (\|>) |
yes | no |
| Literature model library | yes (nlmixr2lib) |
yes (install_default_literature_model()) |
| Export model as R package | via rxUse() (manual) |
built-in (package argument) |
pk_code / dose_code C++ hooks |
no | yes |
| Inter-occasion variability (IOV) | yes (iov.cl ~ 0.2 \| occ in ini({})) |
yes (iov argument) |
| Mixture models |
mix() function |
mixture argument |
| Time-varying covariates | yes | yes (new_covariate()) |
| Between-subject variability | yes | yes (iiv argument) |
| Residual error | yes | yes (ruv argument) |