# Guide to adding a distribution

## Background

RTMBdist provides additional probability distributions that work inside `RTMB::MakeTape` for automatic differentiation (AD). RTMB records a computation tape by operator overloading: every arithmetic operation on an `advector` is intercepted. All code must therefore be plain R arithmetic — anything that drops below R to compiled C++ is invisible to the tape. This is why built-in distributions such as `dgamma` must be called as `RTMB::dgamma`, not `stats::dgamma`, and why `@import RTMB` is needed whenever a function uses mathematical operations (`exp`, `log`, `sqrt`, etc.).

Every density function must support three modes:

1. Plain R evaluation — standard use outside of RTMB.
2. AD tape recording — called with an `advector`; gradients must flow.
3. Simulation and OSA residuals — called with a `simref` or `osa` object.

## What to implement

At minimum, provide a density (`d<dist>`) and a random-number generator (`r<dist>`). A CDF (`p<dist>`) and quantile function (`q<dist>`) are strongly preferred; they are required for OSA residuals and quantile-based tests. All four functions must be pure R arithmetic.

## Naming conventions

Follow the `d`/`p`/`q`/`r` prefix convention. Spell out distribution names rather than abbreviating — `gompertz`, `invgamma`, `laplace` — unless the name becomes unwieldy. Reparameterised variants take a `2` suffix: `beta2`, `gamma2`.

## Files to create or modify

File | What to do
--- | ---
`R/<dist>.R` | implement `d`, `p`, `q`, `r` functions
`tests/testthat/test-<dist>.R` | add standard tests
`vignettes/distlist.Rmd` | add one bullet in alphabetical order

## Implementing the density function

    dfoo <- function(x, theta1, theta2 = 1, log = FALSE) {

      # 1. Validate inputs — skip during AD recording
      if (!ad_context()) {
        args <- as.list(environment())
        simulation_check(args)           # catches likelihood written in wrong order
        if (any(theta1 <= 0)) stop("theta1 must be positive")
      }

      # 2. Simulation and OSA escapes
      if (inherits(x, "simref"))
        return(dGenericSim("dfoo", x = x, theta1 = theta1, theta2 = theta2, log = log))
      if (inherits(x, "osa"))
        return(dGenericOSA("dfoo", x = x, theta1 = theta1, theta2 = theta2, log = log))

      # 3. Log-density
      logdens <- ...

      if (log) return(logdens)
      exp(logdens)
    }

A few rules:

- Always compute in log space and only call `exp()` at the very end.
- The string passed to `dGenericSim`/`dGenericOSA` must exactly match the function name, because RTMB uses it to look up the corresponding `p<dist>` or `r<dist>`.
- Parameter validation and `simulation_check` must be inside the `if (!ad_context())` guard — they would fail or give wrong results on `advector` inputs.

## Implementing the CDF

    pfoo <- function(q, theta1, theta2 = 1, lower.tail = TRUE, log.p = FALSE) {
      if (!ad_context()) {
        if (any(theta1 <= 0)) stop("theta1 must be positive")
      }

      p <- ...   # compute CDF using RTMB:: functions where needed

      if (!lower.tail) p <- 1 - p
      if (log.p) p <- log(p)
      p
    }

No `simulation_check` is needed in `p<dist>`. Apply `lower.tail` and `log.p` at the end, in that order.

## Implementing the quantile function

    qfoo <- function(p, theta1, theta2 = 1, lower.tail = TRUE, log.p = FALSE) {
      if (!ad_context()) {
        if (any(theta1 <= 0)) stop("theta1 must be positive")
      }

      if (log.p) p <- exp(p)          # undo transformations first ...
      if (!lower.tail) p <- 1 - p     # ... in the reverse order of pfoo

      ...   # compute quantile
    }

## Implementing the RNG

    rfoo <- function(n, theta1, theta2 = 1) {
      if (any(theta1 <= 0)) stop("theta1 must be positive")
      ...
    }

The RNG is never called during AD, so no `ad_context()` guard is needed. Two common approaches:

- Inversion: `qfoo(runif(n), theta1, theta2)` — works whenever `q<dist>` exists.
- Direct formula: use a known relationship to a base distribution, e.g. `log(1 + rexp(n, rate = eta)) / b` for the Gompertz distribution.

## Numerical stability

Prefer numerically stable forms whenever the argument may be close to zero:

Instead of | Write
--- | ---
`exp(x) - 1` | `expm1(x)`
`log(1 + x)` | `log1p(x)`
`1 - exp(-x)` | `-expm1(-x)`
`x^2`, `x^3` | `x * x`, `x * x * x`
`x^a` (non-integer exponent) | `exp(a * log(x))`

Avoid expressions that produce `0 * (-Inf)` — this evaluates to `NaN` and kills the gradient. Common fixes:

- Add a small offset before taking a log: `log(x + .Machine$double.xmin)`.
- Clamp a log term that can hit `-Inf` at a boundary: `as.finite(log(...))`.

## Conditional logic

Never branch on the value of `x` inside the tape with plain `if/else` or `ifelse` — the condition is not recorded and the gradient will be wrong. Use the smooth indicator helpers from `aaa_utils.R` instead:

Helper | Meaning
--- | ---
`iszero(x)` | 1 if x == 0, else 0
`isnonzero(x)` | 1 if x != 0, else 0
`ispos(x)` | 1 if x >= 0, else 0
`isneg(x)` | 1 if x < 0, else 0

These are defined as pure arithmetic and propagate gradients correctly. Look distributions already implemented to see how to use this.

## Zero-inflated distributions

Two utility functions handle the mixture in a tape-compatible way:

- `log_zi(x, logdens, zeroprob)` — continuous ZI: point mass at 0, density for x > 0.
- `log_zi_discrete(x, logdens, zeroprob)` — discrete ZI: adds inflation mass on top of the PMF at 0.

Typical usage:

    logdens <- RTMB::dgamma(x + .Machine$double.xmin, shape = shape, scale = scale, log = TRUE)
    logdens <- log_zi(x, logdens, zeroprob)

The `eps` offset prevents `log(0)` before `log_zi` has a chance to zero out the weight.

## Reparameterised variants

Transform parameters at the top and delegate to the base function:

    dfoo2 <- function(x, mean, sd, log = FALSE) {
      if (!ad_context()) { ... }
      if (inherits(x, "simref")) return(dGenericSim("dfoo2", ...))
      if (inherits(x, "osa"))    return(dGenericOSA("dfoo2", ...))

      shape <- mean * mean / (sd * sd)
      scale <- sd * sd / mean
      dfoo(x, shape = shape, scale = scale, log = log)
    }

Do not skip the `simulation_check` and simref/osa escapes just because the base function also has them.

## Documentation

Use the `@name` / `NULL` trick to attach the shared Roxygen block to a dummy object and give each function `@rdname`:

    #' Foo distribution
    #'
    #' Density, distribution function, quantile function, and random generation
    #' for the Foo distribution.
    #'
    #' @details
    #' The Foo distribution with parameter \eqn{\theta > 0} has density
    #' \deqn{f(x;\,\theta) = \theta e^{-\theta x}, \quad x \ge 0.}
    #'
    #' @param x,q vector of quantiles
    #' @param p vector of probabilities
    #' @param n number of random values to return
    #' @param theta positive rate parameter
    #' @param log,log.p logical; if \code{TRUE}, probabilities/densities are
    #'   returned as \eqn{\log(p)}.
    #' @param lower.tail logical; if \code{TRUE} (default), probabilities are
    #'   \eqn{P[X \le x]}, otherwise \eqn{P[X > x]}.
    #' @references \url{https://en.wikipedia.org/wiki/...}
    #' @return \code{dfoo} gives the density, \code{pfoo} gives the distribution
    #'   function, \code{qfoo} gives the quantile function, and \code{rfoo}
    #'   generates random deviates.
    #' @examples
    #' x <- rfoo(1, theta = 2)
    #' d <- dfoo(x, theta = 2)
    #' p <- pfoo(x, theta = 2)
    #' q <- qfoo(p, theta = 2)
    #' @name foo
    NULL

    #' @rdname foo
    #' @export
    #' @import RTMB
    dfoo <- function(x, theta, log = FALSE) { ... }

Write the `\deqn{}` in clean mathematical form — not the numerically stable implementation. Use `@import RTMB` on every function that uses arithmetic or mathematical operations. Use `@importFrom stats r<base>` in the RNG for the base sampler.

## Tests

Place tests in `tests/testthat/test-<dist>.R`. Use two parameter combinations and always include an AD gradient check.

Continuous distribution:

    test_that("foo passes standard distribution checks (theta=1)", {
      check_continuous_dist(dfoo, pfoo, qfoo,
        xs = c(0.2, 0.5, 1, 2),
        lower = 0, upper = Inf,
        theta = 1)
    })

    test_that("foo passes standard distribution checks (theta=3)", {
      check_continuous_dist(dfoo, pfoo, qfoo,
        xs = c(0.1, 0.4, 0.8, 1.5),
        lower = 0, upper = Inf,
        theta = 3)
    })

    test_that("foo AD gradient has no NaN", {
      check_ad_gradient(dfoo, rfoo, theta = 1)
    })

Choose `xs` so that `pfoo(xs)` stays well away from 0 and 1 — the `q(p(x))` round-trip returns `Inf` or `-Inf` when the CDF saturates in double precision.

Other helpers for different distribution types:

Type | Helper
--- | ---
Continuous | `check_continuous_dist`
Continuous without quantile function | `check_continuous_dist(..., qfun = NULL)`
Zero-inflated continuous | `check_zeroinfl_dist`
Mixed/inflated (ZI+OI etc.) | `check_inflated_dist`
Discrete | `check_discrete_dist`

## Vignette entry

Add one bullet to `vignettes/distlist.Rmd` in the correct section (Continuous / Discrete / Multivariate) in alphabetical order:

    * [`foo(theta)`](../reference/foo.html): Foo distribution parameterised by rate `theta`