diff --git a/_quarto.yml b/_quarto.yml index 130b7d6..48b49a3 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -17,6 +17,7 @@ book: - arrays.qmd - interpolation.qmd - data.qmd + - debugging.qmd - packaging.qmd - part: "Monty" chapters: diff --git a/debugging.qmd b/debugging.qmd new file mode 100644 index 0000000..613d619 --- /dev/null +++ b/debugging.qmd @@ -0,0 +1,209 @@ +# Debugging {#sec-debugging} + +```{r} +#| include: false +source("common.R") +``` + +Odin provides some special functionality to help you debug your model:- +* You can `print()` the values of variables in your model as it runs +* You can use the `browser()` function in the same way as you would in R. +* You can examine the code generated from your model with `odin_show()`. + + +```{r} +library(odin2) +library(dust2) +``` + +## Using `print()` + +In the code below, we can print out the value of `x` as we go. + + +```{r} +gen <- odin({ + update(x) <- Normal(x, 1) + initial(x) <- 1 + print("x: {x}") +}) +sys <- dust_system_create(gen()) +dust_system_run_to_time(sys, 10) +``` + +Here we see the value of x for all of our 10 time points. If we want to be +more selective, we can choose when we would like to print using the `when` +argument to `print()` like this: + + +```{r} +gen <- odin({ + update(x) <- Normal(x, 1) + initial(x) <- 1 + print("x above 1: {x}", when = x > 1) +}) +sys <- dust_system_create(gen()) +dust_system_run_to_time(sys, 10) +``` + +The `when` function can take more complicated logical expressions, including +brackets, and `||` and `&&` for `or` and `and` respectively. While at present +we can only have one `print()` line within your model, you can print multiple +variables togther like this: + +```{r} +gen <- odin({ + update(x) <- Normal(x, 1) + initial(x) <- 1 + update(y) <- Normal(x, 2) + initial(y) <- 1 + print("x above 1: {x}, y = {y}", when = x > 1) +}) +sys <- dust_system_create(gen()) +dust_system_run_to_time(sys, 10) +``` + +### Formatting + +We use [glue](https://glue.tidyverse.org/) to drive the formatting, which may +feel familiar to you already. Above, we surrounded variables with +`{curly braces}` to print their value instead of their name, where all other +text was printed verbatim. + +You can control the precision of numbers being printed in the form below: + +```{r} +gen <- odin({ + update(x) <- Normal(x, 1) + initial(x) <- 1 + print("x: {x; .2f}") +}) +sys <- dust_system_create(gen()) +dust_system_run_to_time(sys, 10) +``` + +Here, the `.2f` gives us two decimal places; you may recognise that format +if you've used `sprintf()`. By default, variables are printed as floats (`f`), +and you may sometimes find `g` useful for particularly small or large values. + +Note that this is an experimental; see the debugging vignettes for current +limitations. + + +## Using the interactive debugger, `browser()` + +You can use the `browser()` function to ask odin to drop you into a debugger +which will appear very similar to doing so in R, but has a couple of arguments +that are specific to odin. + +* `phase`: the system phase where the debugger should be inserted; this will typically be `update` or `deriv`. +* `when`: optionally a condition that should be satisfied for the debugger to be triggered. You will typically want to set this or it will be called at every step. + +For example, suppose we are interested in what happens at the end of an SIR +model run, when in the SIR model from `vignette("odin")` we might write: + +```{r} +gen <- odin({ + p_IR <- 1 - exp(-gamma * dt) + N <- parameter(1000) + p_SI <- 1 - exp(-(beta * I / N * dt)) + n_SI <- Binomial(S, p_SI) + n_IR <- Binomial(I, p_IR) + update(S) <- S - n_SI + update(I) <- I + n_SI - n_IR + update(R) <- R + n_IR + initial(S) <- N - I0 + initial(I) <- I0 + initial(R) <- 0 + beta <- parameter(0.2) + gamma <- parameter(0.1) + I0 <- parameter(10) + + browser(phase = "update", when = I < 10 && time > 20) +}) +``` + +The location of the call to `browser()` does not matter; it will be activated at +the **end** of the phase. The condition here might be something we cook up to +look at what happens as the number of individuals in the infected class starts +tailing off at the end of the simulation. + +We create and initialise the system as normal: + +```{r} +sys <- dust_system_create(gen()) +dust_system_set_state_initial(sys) +``` + +However, when you run the system you will pause part way through evaluation: + +```r +dust_system_run_to_time(sys, 200) +ℹ dust browser ('update'; time = 117): see `?dust_browser()` for help +Called from: eval(substitute(expr), data, enclos = parent.frame()) +Browse[1]> +``` + +Now you can explore things that odin and dust know about, in a similar way +that you would in R. + +```r +Browse[1]> ls() + [1] "beta" "gamma" "I" "I0" "N" "n_IR" "n_SI" "p_IR" "p_SI" +[10] "R" "S" "time" +``` + +and you can inspect values or perform calculations: + +```r +Browse[1]> N +[1] 1000 +Browse[1]> I +[1] 9 +Browse[1]> S +[1] 178 +Browse[1]> I / S +[1] 0.0505618 +``` + +You can change values for testing just within this browser environment, but +note that the changes will *not* be injected back into your model. + +If you press `c` or `n`, then odin the system will proceed to the next step and +drop you back into the debugger, when it reaches the browser command again, +just like in R. So if your model had a variable called `n`, you would have to +type `message(n)`, or use `sprintf` to see its value. You can exit with `Q +which will return you to the console with an error. + +To cancel all future browser calls and run to the end of your simulation:- + +```r +dust_browser_continue() +``` + +and then the next time you press `c` to continue, your simulation will run +to completion. + +## Showing the generated code + +Sometimes just looking at the generated code can be helpful. You can do this +with `odin_show`: + +```{r} +odin_show({ + initial(x) <- 2 + update(x) <- Normal(x, 3) +}) +``` + +Above may seem quite a bit of code, but if you look for the `initial` function +and the `update` function at the bottom you will see the code representing your +two model lines. To simplify things a little, we can show just a particular +method (usually `update` or `rhs`), with the `what` argument, for example: + +```{r} +odin_show({ + initial(x) <- 2 + update(x) <- Normal(x, 3) +}, what = "update") +```