Author: Rich FitzJohn
Date: 2025-02-07

# odin functions

#### Rich FitzJohn

#### 2025-02-07

`odin` supports many functions that you’d expect to see for constructing differential equation models; primarily mathematical functions available through R’s “Rmath” library. These include all mathematical operations, and many more obscure mathematical functions. Special support is provided for working with arrays. A further set of functions is available for working discrete time stochastic models.

## Basic operators

- `+` – Plus: Both infix (`a + b`) and prefix (`+a`) versions supported (e.g., `1 + 2` → `3`)

- `-` – Minus: Both infix (`a - b`) and prefix (`-a`) versions supported (e.g., `10 - 1` → `9`)

- `*` – Multiply: Multiply two numbers together (e.g., `2 * 6` → `12`)

- `/` – Divide: Divide two numbers (e.g., `12 / 6` → `2`)

- `^` – Power: Raise the first number to the power of the second. Either number may be a floating point number (e.g., `2.3 ^ 1.2` → `2.716898`)

- `(` – Parenthesis: Group expressions together (e.g., `(1 + 5) * 2` → `12`)

- `if` – Conditional: Inline conditional statement. This takes a form slightly different to typically seen in R with the result of the statement directly assigned (e.g., `if (9 > 10) 1 else 2` → `2`)

Because general programming is not supported in `odin` and because every line must contain an assignment, instead of writing

    if (mycondition) {
      a <- true_value
    } else {
      a <- false_value
    }

instead write

    a <- if (mycondition) true_value else false_value

(this works in normal R too!)

## Array support

There are a group of functions for interacting with arrays

- `sum` – Sum: Compute sum over an array, or over some dimension of an array

- `length` – Length: Total length of an array

- `dim` – Length of one of an array’s dimensions: If an array `x` has 10 rows and 20 columns, then `dim(x, 1)` is `10` and `dim(x, 2)` is `20`. Note that this differs from `dim` in R and `dim(x, i)` is closer to `dim(x)[[i]]`

- `[` – Subset an array: See below

- `interpolate` – Interpolate an array over time: See below

When working with arrays, use generally implies a “for loop” in the generated C code. For example, in the example in the main package vignette the derivatives are computed as

    deriv(y[]) <- r[i] * y[i] * (1 - sum(ay[i, ]))

The indexes on the right hand side can be one of `i`, `j`, `k`, `l` `i5`, `i6`, `i7` or `i8` corresponding to the index on the left hand side being iterated over (`odin` supports arrays up to 8 dimensions). The left-hand-side here contains no explicit entry (`y[]`) which is equivalent to `y[1:length(y)]`, which expands (approximately) to the “for loop”

    for (i in 1:length(y)) {
      deriv(y[i]) <- r[i] * y[i] * (1 - sum(ay[i, ]))
    }

(except slightly different, and in C).

Similarly, the expression

    ay[, ] <- a[i, j] * y[j]

involves loops over two dimensions (`ay[, ]` becomes `ay[1:dim(ay, 1), 1:dim(ay, 2)]` and so the loop becomes

    for (i in 1:dim(ay, 1)) {
      for (j in 1:dim(ay, 2)) {
        ay[i, j] <- a[i, j] * y[j]
      }
    }

Due to constraints with using C, few things can be used as an index; in particular the following will not work:

    idx <- if (t > 5) 2 else 1
    x <- vec[idx]

(or where `idx` is some general odin variable as the result of a different assignment). You must use `as.integer` to cast this to integer immediately before indexing:

    idx <- if (t > 5) 2 else 1
    x <- vec[as.integer(idx)]

This will truncate the value (same behaviour as `truncate`) so be warned if passing in things that may be approximately integer - you may want to use `as.integer(round(x))` in that case.

The interpolation functions are described in more detail in the main package vignette

## Operators

A number of logical-returning operators exist, primarily to support the `if` statement; all the usual comparison operators exist (though not vectorised `|` or `&`).

- `>` – Greater than (e.g., `1 > 2` → `FALSE`)

- `<` – Less than (e.g., `1 < 2` → `TRUE`)

- `>=` – Greater than or equal to (e.g., `1 >= 2` → `FALSE`)

- `<=` – Less than or equal to (e.g., `1 <= 2` → `TRUE`)

- `==` – Is exactly equal to (e.g., `1 == 1` → `TRUE`)

- `!=` – Is not exactly equal to (e.g., `1 != 2` → `TRUE`)

- `&&` – Boolean AND (e.g., `(1 == 1) && (2 > 1)` → `TRUE`)

- `||` – Boolean OR (e.g., `(1 == 1) && (2 > 1)` → `TRUE`)

Be wary of strict equality with `==` or `!=` as numbers may be floating point numbers, which have some surprising properties for the uninitiated, for example

    sqrt(3)^2 == 3

    ## [1] FALSE

## Mathematical operators

- `%%` – Modulo: Finds the remainder after division of one number by another (e.g., `123 %% 100` → `23`)

- `%/%` – Integer divide: Different to floating point division, effectively the full number of times one number divides into another (e.g., `20 %/% 7` → `2`)

- `abs` – Absolute value (e.g., `abs(-1)` → `1`)

- `sign` – Sign function: Returns the sign of its argument as either -1, 0 or 1, which may be useful for multiplying by another argument (e.g., `sign(-100)` → `-1`)

- `round` – Round a number (e.g., `round(1.23)` → `1`; `round(1.23, 1)` → `1.2`)

- `floor` – Floor of a number: Largest integer not greater than the provided number (e.g., `floor(6.5)` → `6`)

- `ceiling` – Ceiling of a number: Smallest integer not less than the provided number (e.g., `ceiling(6.5)` → `7`)

- `trunc` – Truncate a number: Round a number towards zero

- `max` – Maximum: Returns maximum of all arguments given (e.g., `max(2, 6, 1)` → `6`)

- `min` – Minimum (e.g., `min(2, 6, 1)` → `1`)

- `exp` – Exponential function (e.g., `exp(1)` → `2.718282`)

- `expm1` – Computes exp(x) - 1 accurately for small |x| (e.g., `exp(1)` → `1.718282`)

- `log` – Logarithmic function (e.g., `log(1)` → `0`)

- `log2` – Logarithmic function in base 2 (e.g., `log2(1024)` → `10`)

- `log10` – Logarithmic function in base 10 (e.g., `log10(1000)` → `3`)

- `log1p` – Computes log(x + 1) accurately for small |x| (e.g., `log1p(1)` → `0.6931472`)

- `sqrt` – Square root function (e.g., `sqrt(4)` → `2`)

- `beta` – Beta function (e.g., `beta(3, 5)` → `0.00952381`)

- `lbeta` – Log beta function (e.g., `lbeta(3, 5)` → `-4.65396`)

- `choose` – Binomial coefficients (e.g., `choose(60, 3)` → `34220`)

- `lchoose` – Log binomial coefficients (e.g., `choose(60, 3)` → `10.44057`)

- `gamma` – Gamma function (e.g., `gamma(10)` → `362880`)

- `lgamma` – Log gamma function (e.g., `lgamma(10)` → `12.80183`)

The exact for `%%` and `%/%` for floating point numbers and signed numbers are complicated - please see `?Arithmetic`. The rules for operators in `odin` are exactly those in R as the same underlying functions are used.

Similarly, for the differences between `round`, `floor`, `ceiling` and `truncate`, see the help page `?round`. Note that R’s behaviour for rounding away from 0.5 is exactly followed and that this slightly changed behaviour at version 4.0.0

All the usual trig functions are also available:

- `cos` – Cosine function

- `sin` – Sine function

- `tan` – Tangent function

- `acos` – Arc-cosine function

- `asin` – Arc-sin function

- `atan` – Arc-tangent function

- `atan2` – Two-arg arc-tangent function

- `cosh` – Hyperbolic cosine function

- `sinh` – Hyperbolic sine function

- `tanh` – Hyperbolic tangent function

- `acosh` – Hyperbolic arc-cosine function

- `asinh` – Hyperbolic arc-sine function

- `atanh` – Hyperbolic arc-tangent function

## Stochastic models

For discrete time stochastic models, all of R’s normal stochastic distribution functions are available:

- `unif_rand` – Standard uniform distribution: Sample from the uniform distribution on [0, 1] - more efficient than but equivalent to runif(0, 1)

- `norm_rand` – Standard normal distribution: Sample from the standard normal distribution - more efficient than but equivalent to rnorm(0, 1)

- `exp_rand` – Standard exponential distribution: Sample from the exponential distribution with rate 1 - more efficient than but equivalent to rexp(1)

- `rbeta` – Beta distribution: With parameters shape1 and shape2 (see `?rbeta` for details)

- `rbinom` – Binomial distribution: With parameters `size` (number of trials) and `prob` (probability of success)

- `rcauchy` – Cauchy distribution: With parameters `location` and `scale`

- `rchisq` – Chi-Squared distribution: With parameter `df`

- `rexp` – Exponential distribution: With parameter `rate`

- `rf` – F-distribution: With parameter `df1` and `df2

- `rgamma` – Gamma distribution: With parameters `shape` and `rate`

- `rgeom` – Geometric distribution: Distribution with parameters `prob`

- `rhyper` – Hypergeometric distribution: With parameters `m` (the number of white balls in the urn), `n` (the number of black balls in the urn) and `k` (the number of balls drawn from the urn)

- `rlogis` – Logistic distribution: With parameters `location` and `scale`

- `rlnorm` – Log-normal distribution: With parameters `meanlog` and `sdlog`

- `rnbinom` – Negative binomial distribution: With parameters `size`, `prob` and `mu`

- `rnorm` – Normal distribution: With parameters `mean` and `sd`

- `rpois` – Poisson distribution: With parameter `lambda`

- `rt` – Student’s t distribution: With parameter `df`

- `runif` – uniform distribution: With parameters `min` and `max`

- `rweibull` – Weibull distribution: With parameters `shape` and `scale`

- `rwilcox` – Wilcoxon rank sum statistic distribution: With parameters `n` and `m`

- `rsignrank` – Wilcoxon signed rank statistic distribution: With parameter `n`

With random number functions we can write:

    x <- runif(10, 20)

which will generate a random number from the uniform distribution. If you write:

    x[] <- runif(10, 20)

then each element of `x` will be filled with a different random number drawn from this distribution (which is generally what you want). Random numbers are considered to be time varying which means they will automatically generate each time step, so if you write

    x <- rnorm(0, 10)
    update(y[]) <- y[i] + x

then at each time step, each element of `y` will be updated by the same random number from a normal distribution with a mean of zero and a standard deviation of 10 - the number will change each time step but be the same for each element of `y` in the example above.

In addition, two functions that are vector returning and require some care to use:

- `rmultinom` – multinomial distribution: The first parameter is the number of samples and the second is the per-class probability and must be a vector

- `rmhyper` – Multivariate hypergeometric distribution: The first parameter is the number of samples and the second is the per-class count and must be a vector

Both these functions require a vector input (of probabilities for `rmultinom` and of counts for `rmhyper`) and return a vector the same length. So the expression

    y[] <- rmultinom(10, p)

will produce a vector `y` of samples from the multinomial distribution with parameters `size = 10` (so after wards `sum(y)` is 10) and probabilities `p`. It is very important that `y` and `p` have the same size.

At the moment it is not possible to use expressions like

    y[1, ] <- rmultinom(10, p[i, ])

but this is planned for implementation in the future. A full example of using `rmultinom` is given in the discrete models vignette.