7  Packaging odin models

So far, we have compiled odin code as we have needed it, using the odin() function. This works well for many uses, but has its limitations:

library(odin2)

7.1 A basic package skeleton

Many tools exist to create packages; here we will use usethis, but there’s nothing magic here - you could create these files by hand if you prefer (see Writing R Extensions for the official guide on R packages if this is the approach you prefer).

path <- tempfile()
usethis::create_package(
  path,
  fields = list(Package = "pkg",
                Title = "My Odin Model",
                Description = "An example odin model"))
#> ✔ Creating '/tmp/RtmpdxBEnl/file1d7d1b963dd/'.
#> ✔ Setting active project to "/tmp/RtmpdxBEnl/file1d7d1b963dd".
#> ✔ Creating 'R/'.
#> ✔ Writing 'DESCRIPTION'.
#> Package: pkg
#> Title: My Odin Model
#> Version: 0.1.0
#> Authors@R (parsed):
#>     * An Author <a.author@example.com> [aut, cre]
#> Description: An example odin model
#> License: CC0
#> Encoding: UTF-8
#> Language: en-GB
#> Roxygen: list(markdown = TRUE)
#> RoxygenNote: 7.3.2
#> ✔ Writing 'NAMESPACE'.
#> ✔ Setting active project to "<no active project>".
usethis::proj_set(path)
#> ✔ Setting active project to "/tmp/RtmpdxBEnl/file1d7d1b963dd".

Our package now contains:

fs::dir_tree(path)
#> /tmp/RtmpdxBEnl/file1d7d1b963dd
#> ├── DESCRIPTION
#> ├── NAMESPACE
#> └── R

usethis has created some skeleton files for us. The DESCRIPTION contains:

Package: pkg
Title: My Odin Model
Version: 0.1.0
Authors@R: 
    person("An", "Author", , "a.author@example.com", role = c("aut", "cre"))
Description: An example odin model
License: CC0
Encoding: UTF-8
Language: en-GB
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2

and the NAMESPACE file contains

# Generated by roxygen2: do not edit by hand

7.2 Adding odin code

The odin code needs to go within this package in the inst/odin directory. Code saved as inst/odin/myname.R will create a generator called myname. Here, we create a file inst/odin/sir.R containing the SIR model from Chapter 1:

deriv(S) <- -beta * S * I / N
deriv(I) <- beta * S * I / N - gamma * I
deriv(R) <- gamma * I

initial(S) <- N - I0
initial(I) <- I0
initial(R) <- 0

N <- parameter(1000)
I0 <- parameter(10)
beta <- parameter(0.2)
gamma <- parameter(0.1)

We also need to make some changes to our package:

  • We need to include dust2 as an Imports dependency
  • We need to include dust2, monty and cpp11 as LinkingTo dependencies
  • We need to arrange our package so it loads the shared library that we build

If you run odin_package() before setting this up, it will error and indicate where the problem lies:

odin_package(path)
#> ℹ Found 1 odin code file in 'inst/odin'
#> ✔ Wrote 'inst/dust/sir.cpp'
#> Error:
#> ! Expected package 'dust2' as 'Imports' in DESCRIPTION
usethis::use_package("dust2", "Imports")
#> ✔ Adding dust2 to 'Imports' field in DESCRIPTION.
#> ☐ Refer to functions with `dust2::fun()`.
usethis::use_package("dust2", "LinkingTo")
#> ✔ Adding dust2 to 'LinkingTo' field in DESCRIPTION.
usethis::use_package("monty", "LinkingTo")
#> ✔ Adding monty to 'LinkingTo' field in DESCRIPTION.

Setting up to use cpp11 is a bit more involved because of the changes that we need to make to ensure that everything links together correctly; for details see the packaging section of the cpp11 “Getting started” vignette

usethis::use_package_doc()
#> ✔ Writing 'R/pkg-package.R'.
#> ☐ Run `devtools::document()` to update package-level documentation.
usethis::use_cpp11()
#> ✔ Creating 'src/'.
#> ✔ Adding "*.o", "*.so", and "*.dll" to 'src/.gitignore'.
#> ✔ Adding "@useDynLib pkg, .registration = TRUE" to 'R/pkg-package.R'.
#> ☐ Run `devtools::document()` to update 'NAMESPACE'.
#> ✔ Adding cpp11 to 'LinkingTo' field in DESCRIPTION.
#> ✔ Writing 'src/code.cpp'.
devtools::document(path)
#> ℹ Updating pkg documentation
#> Writing 'NAMESPACE'
#> ℹ Loading pkg
#> ℹ 1 functions decorated with [[cpp11::register]]
#> 
#> ✔ generated file 'cpp11.R'
#> 
#> ✔ generated file 'cpp11.cpp'
#> 
#> ℹ Re-compiling pkg (debug build)
#> ── R CMD INSTALL ───────────────────────────────────────────────────────────────
#> * installing *source* package ‘pkg’ ...
#> ** using staged installation
#> ** libs
#> using C++ compiler: ‘g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
#> g++ -std=gnu++17 -I"/opt/R/4.4.2/lib/R/include" -DNDEBUG  -I'/home/runner/work/_temp/Library/cpp11/include' -I'/home/runner/work/_temp/Library/dust2/include' -I'/home/runner/work/_temp/Library/monty/include' -I/usr/local/include    -fpic  -g -O2  -UNDEBUG -Wall -pedantic -g -O0  -c code.cpp -o code.o
#> g++ -std=gnu++17 -I"/opt/R/4.4.2/lib/R/include" -DNDEBUG  -I'/home/runner/work/_temp/Library/cpp11/include' -I'/home/runner/work/_temp/Library/dust2/include' -I'/home/runner/work/_temp/Library/monty/include' -I/usr/local/include    -fpic  -g -O2  -UNDEBUG -Wall -pedantic -g -O0  -c cpp11.cpp -o cpp11.o
#> g++ -std=gnu++17 -shared -L/opt/R/4.4.2/lib/R/lib -L/usr/local/lib -o pkg.so code.o cpp11.o -L/opt/R/4.4.2/lib/R/lib -lR
#> installing to /tmp/RtmpdxBEnl/devtools_install_1d7d24f9545b/00LOCK-file1d7d1b963dd/00new/pkg/libs
#> ** checking absolute paths in shared objects and dynamic libraries
#> * DONE (pkg)
#> Writing 'pkg-package.Rd'
fs::file_delete(file.path(path, "src/code.cpp"))

We can now generate our odin code:

odin_package(path)
#> ℹ Found 1 odin code file in 'inst/odin'
#> ℹ 'inst/dust/sir.cpp' is up to date
#> ℹ Working in package 'pkg' at '/tmp/RtmpdxBEnl/file1d7d1b963dd'
#> ℹ Found 1 system
#> ✔ Wrote 'src/sir.cpp'
#> ✔ Wrote 'R/dust.R'
#> ✔ Wrote 'src/Makevars'
#> ℹ 13 functions decorated with [[cpp11::register]]
#> ✔ generated file 'cpp11.R'
#> ✔ generated file 'cpp11.cpp'

The package now contains more files:

fs::dir_tree(path)
#> /tmp/RtmpdxBEnl/file1d7d1b963dd
#> ├── DESCRIPTION
#> ├── NAMESPACE
#> ├── R
#> │   ├── cpp11.R
#> │   ├── dust.R
#> │   └── pkg-package.R
#> ├── inst
#> │   ├── dust
#> │   │   └── sir.cpp
#> │   └── odin
#> │       └── sir.R
#> ├── man
#> │   └── pkg-package.Rd
#> └── src
#>     ├── Makevars
#>     ├── code.o
#>     ├── cpp11.cpp
#>     ├── cpp11.o
#>     ├── pkg.so
#>     └── sir.cpp

Almost every new file here should not be edited directly (and all contain a line at the start to that effect).

Our DESCRIPTION now contains

Package: pkg
Title: My Odin Model
Version: 0.1.0
Authors@R: 
    person("An", "Author", , "a.author@example.com", role = c("aut", "cre"))
Description: An example odin model
License: CC0
Encoding: UTF-8
Language: en-GB
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Imports: 
    dust2
LinkingTo: 
    cpp11,
    dust2,
    monty

and NAMESPACE contains

# Generated by roxygen2: do not edit by hand

useDynLib(pkg, .registration = TRUE)

In R/:

  • cpp11.R is the glue code generated by cpp11
  • dust.R is glue code generated by dust2
  • pkg-package.R was generated by usethis::use_package_doc() and holds the special roxygen2 comments that caused devtools::document() to write our NAMESPACE file (you can edit this file!)
#' @keywords internal
"_PACKAGE"

## usethis namespace: start
#' @useDynLib pkg, .registration = TRUE
## usethis namespace: end
NULL

In inst/dust, sir.cpp contains the dust interface for our model (see the “Writing dust2 systems” vignette if you are curious)

In man/, pkg-package.Rd contains help files generated by roxygen2

In src/

  • Makevars contains code to allow OpenMP to work to parallelise the system
  • cpp11.cpp is glue code generated by cpp11
  • sir.cpp is the full system code generated by dust2
  • code.cpp was added by cpp11 and can be removed
  • The .o and .so (or .dll on Windows) files are generated by the compiler

7.3 Development of the package

Once your odin code is in a package, you will want to iterate over it. Previously you might have had scripts that you used source() on to load into the session. Now your workflow looks like:

  1. Edit the odin code in inst/odin
  2. Run odin_package()
  3. Load the package with pkgload::load_all()

If you edit code in R/ you don’t need to run step 2, and running step 3 is enough.

pkgload::load_all(path)
#> ℹ Loading pkg
#> ℹ 13 functions decorated with [[cpp11::register]]
#> 
#> ✔ generated file 'cpp11.R'
#> 
#> ✔ generated file 'cpp11.cpp'
#> 
#> ℹ Re-compiling pkg (debug build)
#> ── R CMD INSTALL ───────────────────────────────────────────────────────────────
#> * installing *source* package ‘pkg’ ...
#> ** using staged installation
#> ** libs
#> using C++ compiler: ‘g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
#> g++ -std=gnu++17 -I"/opt/R/4.4.2/lib/R/include" -DNDEBUG  -I'/home/runner/work/_temp/Library/cpp11/include' -I'/home/runner/work/_temp/Library/dust2/include' -I'/home/runner/work/_temp/Library/monty/include' -I/usr/local/include   -fopenmp -fpic  -g -O2  -UNDEBUG -Wall -pedantic -g -O0  -c cpp11.cpp -o cpp11.o
#> g++ -std=gnu++17 -I"/opt/R/4.4.2/lib/R/include" -DNDEBUG  -I'/home/runner/work/_temp/Library/cpp11/include' -I'/home/runner/work/_temp/Library/dust2/include' -I'/home/runner/work/_temp/Library/monty/include' -I/usr/local/include   -fopenmp -fpic  -g -O2  -UNDEBUG -Wall -pedantic -g -O0  -c sir.cpp -o sir.o
#> g++ -std=gnu++17 -shared -L/opt/R/4.4.2/lib/R/lib -L/usr/local/lib -o pkg.so cpp11.o sir.o -fopenmp -L/opt/R/4.4.2/lib/R/lib -lR
#> installing to /tmp/RtmpdxBEnl/devtools_install_1d7d1998459/00LOCK-file1d7d1b963dd/00new/pkg/libs
#> ** checking absolute paths in shared objects and dynamic libraries
#> * DONE (pkg)

If you are using RStudio then Ctrl-Shift-l will load the package for you.

7.4 Next steps

Once you have a model in a package, then you are within the realms of normal R package development, and nothing here is specific to odin. However, if you are not familiar with package development we hope these tips will be useful.

A good place to look for information on package development is Hadley Wickham and Jenny Bryan’s “R packages” book, and to avoid repeating their material we’ll just link to it:

This looks like a lot of work, but most of the setup here can be configured in a few lines. Writing tests is the only part that requires more than configuration, and that is something that you will tend to get better at over time.