--- title: "The Monte-Carlo engine" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{The Monte-Carlo engine} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, fig.width = 7, fig.height = 4.5, out.width = "100%" ) ``` ```{r setup} library(ambre) set.seed(2024) ``` What separates `ambre` from a spreadsheet is that almost nothing in it is a single number. Concentrations, exposure volumes and log-reductions are all **probability distributions**, and the package draws from them thousands of times to report a *range* of risk rather than one fragile point estimate. This vignette opens up that Monte-Carlo core: the two loops it runs, the distributions it understands, and how to reproduce a result. It is the machinery behind the boxplots in `vignette("a-get-started", package = "ambre")` and the spread you interpret in `vignette("d-interpreting-risk", package = "ambre")`. ## Two dimensions: repeats and events Every simulated quantity is drawn on a grid of two indices, both controlled by `config_ambre$exposure`: ```{r exposure-control} config_ambre$exposure[, c("name", "type", "value", "min", "max")] ``` - **`number_of_repeatings`** (default **1000**) is the number of Monte-Carlo **runs**. Each run is one plausible world; together they form the *uncertainty range*. This index is called `repeatID`. - **`number_of_exposures`** (default **365**) is the number of exposure **events** in a year. This index is called `eventID`, and it is what the annual risk aggregation `1 - prod(1 - p)` runs over. So a single draw of, say, exposure volume is really a `repeatID x eventID` table -- 1000 x 365 values -- and the whole pipeline carries that shape through to the final DALYs. ## The distribution catalog The third row above, `volume_perEvent`, is a `triangle` with `min` 0.5 and `max` 3. That `type` / `value` / `min` / `max` / `mode` / `mean` / `sd` / `meanlog` / `sdlog` block is the **shared vocabulary** of the whole database: the same nine columns describe an inflow concentration, a treatment log-reduction and a cost. `create_random_distribution()` reads that vocabulary. The supported `type` values are: | `type` | drawn with | parameters used | |---|---|---| | `value` | (constant) | `value` | | `uniform` | `runif` | `min`, `max` | | `log10_uniform` | `10^runif` | `min`, `max` (as log10) | | `norm` | `rnorm` | `mean`, `sd` | | `log10_norm` | `10^rnorm` | `mean`, `sd` (as log10) | | `lognorm` | `rlnorm` | `meanlog`, `sdlog` | | `triangle` | `EnvStats::rtri` | `min`, `max`, `mode` | Call it directly to see what a draw looks like. It returns an `events` table (one row per `repeatID x eventID`) and the `paras` actually used: ```{r crd} draw <- create_random_distribution( type = "uniform", number_of_repeatings = 3, number_of_events = 5, min = 1, max = 10, debug = FALSE ) head(draw$events) draw$paras ``` In practice you rarely call it by hand. The pipeline calls `generate_random_values()`, a thin wrapper that reads one row of a config table and fills in sensible defaults before delegating. Feed it the real `volume_perEvent` row and you get the exact draw the engine would use for exposure volume: ```{r grv} vol_row <- config_ambre$exposure[config_ambre$exposure$name == "volume_perEvent", ] gv <- generate_random_values( vol_row, number_of_repeatings = 2, number_of_events = 4, debug = FALSE ) summary(gv$events$volume_perEvent) ``` The distribution schema is described further in `vignette("h-config-ambre", package = "ambre")`. ## From min/max to a spread For the normal and log-normal families you often supply only a `min` and a `max`, not a mean and standard deviation. `ambre` derives the missing parameters so that a given fraction of the mass falls inside `[min, max]`, using `get_percentile()` (a wrapper around the normal quantile) together with `default_min()` / `default_max()`. For example, the z-score that places 90% of a normal distribution within the min-max window is: ```{r percentile} ambre:::get_percentile(0.9) ``` You do not normally call these yourself, but knowing they exist explains why a `norm` row with only `min` and `max` still produces a sensible bell curve. ## A special case worth knowing `generate_random_values()` has one silent behaviour. A `triangle` whose `min` equals its `max` cannot be a triangle, so it is quietly converted to a (degenerate) `uniform` -- effectively a constant: ```{r triangle-collapse} degenerate <- data.frame( type = "triangle", value = NA, min = 2, max = 2, mode = NA, mean = NA, sd = NA, meanlog = NA, sdlog = NA ) gv2 <- generate_random_values(degenerate, number_of_repeatings = 1, number_of_events = 3, debug = FALSE) unique(gv2$events$values) ``` You will see the message *"Distribution set from 'triangle' to 'uniform' because 'min' equals 'max'"* in a real run when this happens -- it is expected, not an error. ## Why barrier credits add up Log-reductions combine by **addition in log space**. A train that removes 2 log at the plant and 3 log in the field removes 5 log overall, and 5 log means the surviving concentration is `10^-5` of the inflow. Internally `ambre` builds a wide `repeatID x eventID` table of each barrier's draw and sums the columns, so the additivity holds *run by run*, preserving the correlation structure across the Monte-Carlo sample. This is why you can compare a treatment strategy and a barrier strategy on the same footing (see `vignette("b-initial-vs-new-scenario", package = "ambre")`). ## Reproducibility Because every draw is random, set a seed before a run you want to reproduce. Same seed, same numbers: ```{r seed} set.seed(1) a <- create_random_distribution(type = "uniform", number_of_repeatings = 1, number_of_events = 4, min = 0, max = 1, debug = FALSE) set.seed(1) b <- create_random_distribution(type = "uniform", number_of_repeatings = 1, number_of_events = 4, min = 0, max = 1, debug = FALSE) identical(a$events$values, b$events$values) ``` To read the uncertainty a run expresses, summarise a quantity across `repeatID` with quantiles -- exactly what the final DALY boxplots do, and what `vignette("d-interpreting-risk", package = "ambre")` walks through.