Debugging odin models can be challenging because:
Here, we outline some strategies for debugging, and describe the new features that aim to make this easier.
print()
As of odin 1.4.5, you can print the value of some variables in the middle of running your model. We will expand and change this functionality in future versions, your feedback is very welcome.
Consider the simple model below, which illustrates the idea:
gen <- odin2::odin({
update(x) <- Normal(x, 1)
initial(x) <- 1
print("x: {x}")
})
sys <- dust_system_create(gen(), list(), 1)
dust_system_run_to_time(sys, 10)
#> [0.000000] x: 0.000000
#> [1.000000] x: 0.499577
#> [2.000000] x: 0.836336
#> [3.000000] x: 1.427859
#> [4.000000] x: 2.002370
#> [5.000000] x: 2.051949
#> [6.000000] x: -0.543754
#> [7.000000] x: -1.858990
#> [8.000000] x: -0.528796
#> [9.000000] x: -0.037171
Here we’ve told odin that we want to watch the variable
x
and print its value at every evaluation (the third line
of the model code. When we run the model it prints out the time in
square brackets then the debug information following. Notice that we
only requested the solution at times 0 and 0.1 but the debug information
shows every point in time that the ODE solver evaluated this system of
equations.
While this function shares its name with R’s print()
it
has entirely different functionality.
print
format stringsFor print formatting, we use glue to drive the formatting, and if you have used that package the format will feel familiar.
The most simple usage is as above; you can refer to variables within
{curly braces}
; so long as your variable is a scalar this
will work. Outside of curly braces the string is printed verbatim.
If your model takes many steps, or if you want to narrow down on a
problem, you may want to enable conditional display of your debug
information. Use the argument when =
to control display,
such as
which will display the value of x
when it is greater
than 1. You can chain together expressions with parentheses and
&&
or ||
and reference any value in
your system. For example:
You can control the way that quantities are displayed through the use of formatting options. The formatting is the same as used by R, so you can experiment in the console easily. The default is to print as a generic floating point number, so this:
is roughly equivalent to writing
See ?sprintf
for more information; but this defaults to
6 decimal places of precision. This may not be appropriate if you are
dealing with numbers that are very large or very small; these both look
a bit silly:
The first loses all information - the only non-zero parts of the number fall after the precision cut-off, while in the second the 6 decimal places just add noise. So above we might prefer:
which we could write in odin’s approach as
Anything after the ;
is interpreted as a format
specifier. You could also do
which would format y
to 2 decimal places. We follow here
the example of the sprintf
transformer example in glue by not including the %
placeholder, but allow all formats that the underlying library
supports.
This is an experimental interface, and it has not been exposed to much real-world use. As such it is possible that you might write fairly innocent looking code and it produce a compiler error rather than a nicer R error - please let us know so we can fix this.
print
{x; d}
) for
variables that are merely integer-like, or you will get unexpected junk
output out. You can however write {as.integer(x); d}
which
will do a conversion to integer and then print thatSometimes just looking at the generated code can be helpful. You can
do this with odin_show
:
odin_show({
initial(x) <- 0
update(x) <- Normal(x, 1)
})
#>
#> ── odin code: ──────────────────────────────────────────────────────────────────
#> #include <dust2/common.hpp>
#> // [[dust2::class(odin)]]
#> // [[dust2::time_type(discrete)]]
#> class odin {
#> public:
#> odin() = delete;
#> using real_type = double;
#> using rng_state_type = monty::random::generator<real_type>;
#> struct shared_state {
#> struct offset_type {
#> struct {
#> size_t x;
#> } state;
#> } offset;
#> };
#> struct internal_state {};
#> using data_type = dust2::no_data;
#> static dust2::packing packing_state(const shared_state& shared) {
#> return dust2::packing{
#> {"x", {}}
#> };
#> }
#> static dust2::packing packing_gradient(const shared_state& shared) {
#> return dust2::packing{
#> };
#> }
#> static shared_state build_shared(cpp11::list parameters) {
#> shared_state::offset_type offset;
#> offset.state.x = 0;
#> return shared_state{offset};
#> }
#> static internal_state build_internal(const shared_state& shared) {
#> return internal_state{};
#> }
#> static void update_shared(cpp11::list parameters, shared_state& shared) {
#> }
#> static void update_internal(const shared_state& shared, internal_state& internal) {
#> }
#> static void initial(real_type time, const shared_state& shared, internal_state& internal, rng_state_type& rng_state, real_type* state) {
#> state[0] = 0;
#> }
#> static void update(real_type time, real_type dt, const real_type* state, const shared_state& shared, internal_state& internal, rng_state_type& rng_state, real_type* state_next) {
#> const auto x = state[0];
#> state_next[0] = monty::random::normal<real_type>(rng_state, x, 1);
#> }
#> static auto zero_every(const shared_state& shared) {
#> return dust2::zero_every_type<real_type>();
#> }
#> };
To show just a particular method (usually update
or
rhs
), use the what
argument, for example:
odin_show({
initial(x) <- 0
update(x) <- Normal(x, 1)
}, what = "update")
#>
#> ── odin code (update): ─────────────────────────────────────────────────────────
#> static void update(real_type time, real_type dt, const real_type* state, const shared_state& shared, internal_state& internal, rng_state_type& rng_state, real_type* state_next) {
#> const auto x = state[0];
#> state_next[0] = monty::random::normal<real_type>(rng_state, x, 1);
#> }
This is new and very experimental, you can ask odin to drop you into
a debugger (implemented on top of R’s browser()
to explore
the values of your variables at some point within a model run. To do
this, add a call to browser()
somewhere within your odin
code and recompile.
The browser
function in odin accepts arguments
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 stepNote that these are not the same as R’s browser()
!
For example, to debug the simple SIR model from
vignette("odin")
we might write:
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:
However, when you run the system you will pause part way through evaluation:
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())
and the prompt has changed to Browse[1]>
(unfortunately we can’t change this easily)
Here, you can see the things that odin and dust know about:
and you can inspect values or perform calculations:
If you press c
or n
, then odin the system
will proceed to the next step and drop you back into the debugger. You
can exit with Q
which will return you to the console with
an error. You can also run
and then press c
to continue to the end of your
simulation.
Changes that you make to variables within the debugger are not (currently) reflected back into the model. We’d be very happy to discuss this sort of workflow if it seems useful.