--- title: "Migrating from odin 1.x.x" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Migrating from odin 1.x.x} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) source("helpers.R") fast_and_quiet_dust() ``` This vignette discusses key differences with odin version 1. ```{r} library(odin2) ``` # New features Some of these features were present in versions of `odin.dust` and many derive from underlying support in `dust2`. * Comparison to data and likelihood support (introduced in `odin.dust`) * Automatic differentiation * More efficient setting of the subset of parameters you are likely to use while fitting (use the `constant` argument to `parameter()`) * Multiple parameter sets at once (introduced in `odin.dust` but expanded here) * Run multiple copies of a system at once in parallel (introduced in `odin.dust`) * Built-in support for periodic variable resetting (e.g., for computing daily incidence) * Better (we hope) error messages * Better debugging tools (see `vignette("debugging")`) ## Planned * Optional array bounds checking, both during compilation and during runtime (in the latter case with a performance penalty once enabled). ## Hoped * Support for multinomial samples and other vector-valued functions # Missing features This list is incomplete, and we'll expand it as we work through the tests. We're not currently quite at MVP stage yet, so expect that most things don't work! Things we do plan on implementing: * Delay differential equations, e.g. `y_lag <- delay(y, tau)` (`mrc-5434`) * Compile-time parameter substitution (`mrc-5575`) * Compilation to JavaScript * Compilation to GPU Things that we plan to drop in this version * Many details in `config()` and `options` Note that many errors are not caught as odin errors, and invalid odin code will be accepted and generate C++ code that fails to compile. # Changes in syntax ## `user()` becomes `parameter()` This might be the largest user-visible change, and we'll add a translation system for this. Previously, to support parameters you might write ```r a <- user(4) ``` which says that `a` is a user-supplied parameter with a default value of `4`. In most cases this now simply becomes ```r a <- parameter(4) ``` The `integer` argument accepted by `user` has now changed: * `user(integer = TRUE)` becomes `parameter(type = "integer")` * `user(integer = FALSE)` becomes `parameter(type = "real")` This translation can be done automatically in most cases, and will be done (with a warning) by default if possible. You should update your code with the suggested fix, however, as this translation will be removed in a future version. ## Compare keyword is now removed. In comparisons such as ```r compare(d) ~ Normal(0, 1) ``` The `compare` keyword, and the `~` only occur together. This has been simplified, and is now written as: ```r d ~ Normal(0, 1) ``` which reads as: `d` is normally distributed with a mean of 0 and standard deviation of 1. ## Vector parameters assign without array indices Previously, if you had a vector parameter you had to write ```r a[] <- parameter() dim(a) <- 10 ``` (though with `user()`, as in the previous section). However, the array index here does not really add anything as we already know how many dimensions `a` has from the `dim` call. So now you should write ```r a <- parameter() dim(a) <- 10 ``` which makes it clearer that **all** of `a` is assigned by the parameter call. ## Vector/matrix/array parameters whose size is determined by input require `rank` argument What a mouthful. Previously you might have written ```r a[, ] <- user() dim(a) <- user() ``` which means "`a` is a matrix whose dimensions are determined by the input we are given on initialisation". Because of the previous change the first line changes to ```r a <- parameter() ``` but that means that we no longer know that `a` has two dimensions. That's ok because we've moved the responsibility for this into the `dim()` assignment line anyway (internally). So for now you write ```r dim(a) <- parameter(rank = 2) ``` which conveys the same intent. We may make this slightly more friendly in future (see `vignette("functions")`). ## Interpolate results assign without array indices Previously, if you had an `interpolate()` call that returned a vector (or higher-dimension array) you had to write ```r v[] <- interpolate(a, b, "constant") ``` but now you should drop the `[]`, as for the `parameter()` case above, as you are replacing *all* of `v` at once, writing: ```r v <- interpolate(a, b, "constant") ``` ## Discrete-time models have a more solid time basis Previously, discrete time models used `step` to count steps forward as unsigned integers, usually from zero. Many models added a parameter (or constant) `dt` representing the timestep and then a variable `time` which represented the time as a real-valued number. For example you might have `dt` of 0.25 and then your model stops at times `[0, 0.25, 0.5, 0.75, 1]` for steps `[0, 1, 2, 3, 4]`. We formalise this approach now having discrete time systems be explicitly in terms of the same time basis as ODE models (that is, some real valued time axis). When you initialise a model you pass in `dt`, which must be an even divisor of 1 (so 0.5, 0.25, 0.2, etc). We then take steps of this size. The wrinkle is that (at least for now) the model will only return control back to you, or state back to you, at integer-valued times. We may relax this in future to allow returning at any time value that is a multiple of `dt`. This will cause a few issues for using old code, which we cover below. ### Assignments to `dt` You may have models that assign to `dt`, either directly or as a parameter. You can no longer do this as `dt` will be provided by `dust` (see `dust2::dust_system_create()`). We can automatically remove these (with a warning) in some cases. ### Assignments to `time` Conventionally, many models would write ```r time <- step * dt ``` which is the linear transformation of time that dust2 now does. We can remove these statements and your model should work as intended. ### Use of `step` All other uses of `step` are problematic and will need manual fixing. We will try and accumulate migration strategies here, so please let us know if you have had to do anything not listed. **Access "interpolated" values from a grid**: In `sircovid` we used `step` as an array index, in order to support time-varying inputs (e.g., vaccine allocation schedules, rates of contact). This is no longer supported (at all) because `dt` is changed separately from the inputs. Instead you should use odin's interpolation functions. **Periodic resetting**: You may have written: ``` a <- if (step %% freq == 0) b else c ``` to have some quantity that took different values every `freq` steps, where `freq` is usually `1/dt` or `m/dt` where `m` is an integer. You should rewrite this to use `time`: ``` a <- if (time * dt / m == 0) b else c ``` ## The name of the time variable in discrete time models has changed Previously, time was `t` but we have moved this to `time` to be a little more explicit. We can automatically migrate your code in many cases, unless you have defined a variable `time` already. ## Random number function calls have changed Previously we used the same names as R's random-number-drawing functions, for example `rbinom` for drawing from a binomial distribution. This has changed to use the distribution name instead. The motivating reason for this change was that in odin we might write ```r rbinom(size, prob) ``` but if you were writing this in R you would write ``` rbinom(1, size, prob) ``` with the first argument being the number of draws from the distribution in question. This departure in arguments feels needlessly confusing! If you were using `odin` without `odin.dust` then this did compile to a call to one of R's underlying random number functions so this connection was reasonable but from version 2 we use monty's parallelisable random number distributions. The mapping is: * `rbeta()` to `Beta` * `rbinom()` to `Binomial` * `rcauchy()` to `Cauchy` (unsupported for now) * `rchisq()` to `ChiSquared` (unsupported for now) * `rexp()` to `Exponential` * `rf()` to `F` (unsupported for now) * `rgamma()` to `Gamma` * `rgeometric()` to `Geometric` (unsupported for now) * `rhyper()` to `Hypergeometric` * `rlogis()` to `Logistic` (unsupported for now) * `rlnorm()` to `Lognormal` (unsupported for now) * `rnbinom()` to `NegativeBinomial` * `rnorm()` to `Normal` * `rpois()` to `Poisson` * `rt()` to `T` (unsupported for now) * `runif()` to `Uniform` * `rweibull()` to `Weibull` (unsupported for now) (Not all of these are implemented yet). ## System size cannot be changed after creation This limitation comes from our implementation in `dust2` and it is possible to relax it in some settings. However, it is fairly important for efficiently running the system within a pMCMC context where we save state periodically. If your system has a parameter that affects the number of state variables in the system (e.g., the number of age categories that a compartment is stratified by), you may not change this after initialisation. This will be prevented by the parser once arrays are implemented. # Changes in the way arrays are handled The two-argument form of `dim()` has been removed, as we did not believe it was used and it is confusingly different to R. Previously you could write `dim(x, 3)` to get the length of the third dimension of `x`; this is no longer supported. Please let us know if this is a problem. # General changes This package replaces `odin.dust` and will eventually replace `odin` (as in, we'll copy the entire `odin2` code into `odin` to become version 2.0.0 of that package). The relationship between packages has changed. Previously `mcstate` "knew" about `dust` models and so you had to use `odin.dust` practically to use the statistical machinery in `mcstate`. We've changed this around now, so that `odin2` "knows" about `monty` and can create systems that will work well with `monty`. We now depend on `monty`, so if you have `odin2` installed you can start working towards fitting models immediately. # Known limitations ## Much slower compilation time Because we now compile to C++ via `dust2`, the compilation times have massively increased. Previously, compilation of a simple model took less than a second, but now this will take 6 seconds or so. You can alleviate this to a degree during development by specifying `debug = TRUE` when compiling, which reduces this down to about 3 seconds. These times are from my workstation but I expect the relative differences to hold (we're probably 10x slower than previously but can be "only" 5x slower if you turn off optimisation). If you were previously using `odin.dust` you should notice little change here. # Updating old code If you compile odin code that contains any of the changes above, it will try and update the code to the new version and keep going: ```{r} gen <- odin2::odin({ initial(x) <- 1 deriv(x) <- x + a / t a <- user(2) }) ``` This model contains two issues that can be easily rewritten; the solution to this rewriting is printed to screen and the model is compiled as if you had rewritten them. Not everything can be rewritten, especially changes involving `step`: ```{r, error = TRUE, purl = FALSE} gen <- odin2::odin({ initial(x) <- 1 update(x) <- x + a / step a <- user(2) }) ``` In this case, odin errors and tries to indicate where you have work to do (and directs you to this document!) For code saved into a file, you can use `odin_migrate` to migrate code from the old syntax to the new; this will preserve comments and formatting except for code that is rewritten so it should be fairly unintrusive. ```{r, include = FALSE} path <- tempfile(fileext = ".R") writeLines(c( "initial(x) <- 1", "deriv(x) <- x + a / t", "a <- user(2)"), path) ``` For example, in `path` (a temporary file for this vignette) we have saved the code from above: ```{r echo = FALSE, results = "asis"} r_output(readLines(path)) ``` We can migrate this in-place with: ```{r} odin_migrate(path, path) ``` and now the code contains: ```{r echo = FALSE, results = "asis"} r_output(readLines(path)) ```