odelia is an ODE solver implemented in C++ header files, using an adaptive-step
Runge-Kutta 4-5 method, with an interface to R via Rcpp. The solver runs entirely
in compiled code, so it is fast, and ODE systems can be templated on their scalar
type to support automatic differentiation (AD) — letting you compute exact
gradients of a solution with respect to its parameters for use in optimisation and
calibration.
The core solver was first developed by Rich FitzJohn as part of the plant package. This package spins that code out so it can be used more widely.
- Adaptive-step RK4-5 integrator running entirely in C++ (~30-90x faster than
the equivalent solved via
deSolve; see the Lorenz example). - Automatic differentiation of ODE solutions w.r.t. parameters and initial conditions, via the vendored XAD library — enabling gradient-based parameter fitting.
- External drivers: time-varying forcing variables, smoothly interpolated with cubic splines and queried by the system at each step.
- Header-only C++ core that other Rcpp packages can link against.
- Friendly R6 wrappers around the C++ objects.
odelia compiles C++ from source, so you need a working C++ toolchain
(Rtools on Windows, Xcode command-line tools on macOS, a recent g++/clang on
Linux). Then:
# install.packages("remotes")
remotes::install_github("traitecoevo/odelia")odelia ships with one ready-to-run system, the classic
Lorenz system, so you can try the
solver out of the box. It is bundled purely as a demonstration — to model
your own problem you write a small C++ system class (and a thin Rcpp/R6
interface) that the same solver then drives. See
Building your own model
for a complete worked example.
The example below solves the bundled Lorenz system:
library(odelia)
# Define the system (parameters) and its initial state
lz <- LorenzSystem$new(sigma = 10, R = 28, b = 8 / 3)
lz$set_state(c(1, 1, 1)) # autonomous system, so no time needed
# Solver control settings (tolerances, step sizes) use sensible defaults
ctrl <- OdeControl$new()
# Build a solver (a "runner") and advance with adaptive stepping
runner <- Lorenz_Solver$new(lz$ptr, ctrl$ptr)
runner$advance_adaptive(seq(0, 100, by = 0.01))
# Collect output as a tibble
out <- runner$history()For more, see the vignettes (also rendered on the package website):
vignette("odelia")— getting started with the core abstractions (source).vignette("parameter-fitting")— recovering parameters with automatic differentiation (source).
And the worked examples:
- Building your own model with external drivers — a leaf-thermal model showing how to define your own ODE system in C++ and drive it with time-varying forcing (source).
- Lorenz system — also includes a speed comparison
against
deSolve.
Because the solver can be templated on an AD scalar type, you can recover exact
gradients of a loss (the mismatch between the solution and a set of target
observations) with respect to the system parameters, and hand them to a
gradient-based optimiser such as optim():
# An AD-enabled runner exposes a $fit() method returning loss and gradient
ad_runner <- Lorenz_Solver$new(lz$ptr, ctrl$ptr, active = TRUE)
ad_runner$set_target(times, target_vals, obs_index)
res <- ad_runner$fit(params = c(sigma = 12, R = 30, b = 3))
res$loss # scalar mismatch with the target trajectory
res$gradient # exact gradient w.r.t. each parameterA complete optimisation workflow (recovering known Lorenz parameters) is walked
through in vignette("parameter-fitting").
odelia is organised around a few core abstractions:
- System — your ODE model. A C++ class that holds parameters and state and
knows how to compute its rates (right-hand side)
dy/dt. Templated on its scalar type so it works with bothdoubleand AD types. - Stepper — the numerical integration scheme (adaptive RK4-5) that takes one step of the system, estimating the error to choose the next step size.
- Solver / runner — drives the stepper forward over a requested set of times, applying step-size control and collecting the solution history.
- Drivers — external, time-varying forcing variables (e.g. air temperature) that the system queries during integration. Supplied as time series and interpolated with cubic splines.
- Control (
OdeControl) — the solver's tuning knobs: absolute and relative tolerances, state/derivative scaling, and minimum/maximum/initial step sizes.
This package is released under the GNU Affero General Public License v3 (AGPL-3). The vendored XAD library retains its own license; see inst/include/XAD/LICENSE.md.
Contributions are welcome. By submitting a pull request or code to this repository, you agree to the terms of the Contributor License Agreement.
Linking from another package? If you
LinkingTo: odeliaand instantiateSolver, read ARCHITECTURE.md — it documents how the compiled XADTapesymbols are resolved per platform, what your package must do, and the invariants that must not be broken.
An ODE system in odelia consists of:
-
A system header (
inst/include/odelia/examples/*.hpp) — C++ class defining the ODE. Must implementode_size(),set_ode_state(),ode_state(), andode_rates(). Template on scalar type for AD support. -
An Rcpp interface (
src/*_interface.cpp) —[[Rcpp::export]]functions exposing the system to R. -
An R wrapper (
R/*-interface.R, optional) — R6 classes providing a friendlier API around the external pointers. -
A demo script (
examples/*/demo.R) — Runnable demonstration.
Use the Makefile targets below depending on the level of test coverage you want.
make testRuns package tests after local compile. Fast default for day-to-day work.
make test-localRuns testthat::test_local() from the source tree.
make test-installedInstalls with tests and runs testthat::test_package() against the installed package copy.
odelia is part of the plant family of packages in the
traitecoevo org, built around the
plant forest model. Docs hub:
https://traitecoevo.github.io/overstorey/.
Contributing: please skim the family
issue guide
before filing — issues across the family are triaged on
board #5, and cross-package context lives in
plant-meta.