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").
Every simulated quantity is drawn on a grid of two indices, both
controlled by config_ambre$exposure:
config_ambre$exposure[, c("name", "type", "value", "min", "max")]
#> # A tibble: 3 × 5
#> name type value min max
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 number_of_repeatings value 1000 NA NA
#> 2 number_of_exposures value 365 NA NA
#> 3 volume_perEvent triangle NA 0.5 3number_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 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:
draw <- create_random_distribution(
type = "uniform",
number_of_repeatings = 3,
number_of_events = 5,
min = 1, max = 10,
debug = FALSE
)
head(draw$events)
#> repeatID eventID values
#> 1 1 1 8.532483
#> 2 1 2 3.887808
#> 3 1 3 7.123270
#> 4 1 4 7.283558
#> 5 1 5 5.113083
#> 6 2 1 7.312783
draw$paras
#> type repeatings events min max
#> 1 uniform 3 5 1 10In 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:
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)
#> Length Class Mode
#> 0 NULL NULLThe distribution schema is described further in
vignette("h-config-ambre", package = "ambre").
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:
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.
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:
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)
#> [1] 2You 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.
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")).
Because every draw is random, set a seed before a run you want to reproduce. Same seed, same numbers:
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)
#> [1] TRUETo 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.