-
Notifications
You must be signed in to change notification settings - Fork 0
/
packaging.qmd
206 lines (153 loc) · 7.93 KB
/
packaging.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# Packaging odin models
```{r}
#| include: false
source("common.R")
```
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:
* It requires that anyone who uses your code has a working C++ compiler; they will need to compile your model and load it into R to run things
* Compilation of the model is quite slow; large models can take more than 10s to compile. We generally will only compile a system once per R session, but this is still quite a large fixed cost to pay
* If you need to work with HPC systems, then your tasks all need to compile your code. This slows down tasks, but also means you need to take care that everyone writes to different places
* We try to paper over this, but the models that we compile on-the-fly may not always play nicely with parallel frameworks (e.g., `future`)
* As your model grows, you will accumulate a number of coupled support functions that go along with it, and a package is the easiest mechanism for supporting this
* Packages are much easier for other people to use than standalone R scripts
* There is nice tooling available to packages for configuring dependencies, tests and other automatic checks (see below).
```{r}
library(odin2)
```
## A basic package skeleton
Many tools exist to create packages; here we will use [`usethis`](https://usethis.r-lib.org/), but there's nothing magic here - you could create these files by hand if you prefer (see [Writing R Extensions](https://cran.r-project.org/doc/manuals/r-release/R-exts.html) for the official guide on R packages if this is the approach you prefer).
```{r}
#| include: false
options(
usethis.description = list(
"Authors@R" = utils::person(
"An", "Author", email = "[email protected]", role = c("aut", "cre")),
"License" = "CC0",
"Language" = "en-GB",
"Version" = "0.1.0"))
```
```{r}
path <- tempfile()
usethis::create_package(
path,
fields = list(Package = "pkg",
Title = "My Odin Model",
Description = "An example odin model"))
usethis::proj_set(path)
```
Our package now contains:
```{r}
fs::dir_tree(path)
```
`usethis` has created some skeleton files for us. The `DESCRIPTION` contains:
```{r}
#| echo: false
#| results: asis
plain_output(readLines(file.path(path, "DESCRIPTION")))
```
and the `NAMESPACE` file contains
```{r}
#| echo: false
#| results: asis
plain_output(readLines(file.path(path, "NAMESPACE")))
```
## 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 @sec-getting-started:
```{r}
#| echo: false
#| results: asis
dir.create(file.path(path, "inst/odin"), FALSE, TRUE)
writeLines(
c("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)"),
file.path(path, "inst/odin/sir.R"))
r_output(readLines(file.path(path, "inst/odin/sir.R")))
```
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:
```{r}
#| error: true
odin_package(path)
```
```{r}
usethis::use_package("dust2", "Imports")
usethis::use_package("dust2", "LinkingTo")
usethis::use_package("monty", "LinkingTo")
```
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](https://cpp11.r-lib.org/articles/cpp11.html#package)
```{r}
usethis::use_package_doc()
usethis::use_cpp11()
devtools::document(path)
fs::file_delete(file.path(path, "src/code.cpp"))
```
We can now generate our odin code:
```{r}
odin_package(path)
```
The package now contains more files:
```{r}
fs::dir_tree(path)
```
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
```{r}
#| echo: false
#| results: asis
plain_output(readLines(file.path(path, "DESCRIPTION")))
```
and `NAMESPACE` contains
```{r}
#| echo: false
#| results: asis
plain_output(readLines(file.path(path, "NAMESPACE")))
```
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!)
```{r}
#| echo: false
#| results: asis
r_output(readLines(file.path(path, "R/pkg-package.R")))
```
In `inst/dust`, `sir.cpp` contains the dust interface for our model (see the ["Writing dust2 systems" vignette](https://mrc-ide.github.io/dust2/articles/writing.html) 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
## 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.
```{r}
pkgload::load_all(path)
```
If you are using RStudio then `Ctrl-Shift-l` will load the package for you.
## 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](https://r-pkgs.org/), and to avoid repeating their material we'll just link to it:
* Create an [RStudio project](https://r-pkgs.org/workflow101.html#sec-workflow101-rstudio-projects) for your new package
* Write some [tests](https://r-pkgs.org/testing-basics.html) for your model and its support code. We'll document some basic ideas for this later.
* Create a [git repository](https://git-scm.com/) for your package and put it on [GitHub](https://github.com/). For more discussion see [this chapter in "R packages"](https://r-pkgs.org/software-development-practices.html#sec-sw-dev-practices-git-github), and [this guide on configuring R, RStudio and git to work well together](https://happygitwithr.com/).
* Set up [continuous integration](https://r-pkgs.org/software-development-practices.html#sec-sw-dev-practices-ci) so that your tests are run automatically when you make changes to your package
* Put your package into an [R universe](https://ropensci.org/r-universe/) to make it easy for others to install. Your organisation may already have one (for DIDE users, please see [this repo](https://github.com/mrc-ide/mrc-ide.r-universe.dev)), or you can [start your own](https://docs.r-universe.dev/publish/set-up.html)
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.