--- title: "What-if: calibrating exposure" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{What-if: calibrating exposure} %\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) ``` ```{r libs} library(dplyr) library(purrr) ``` Two numbers decide how much of the water each person actually meets: - the **volume** swallowed or inhaled per exposure event (litres per event), and - the **frequency** of those events (how many times it happen per year). Together with the pathogen concentration, these drive the *dose*, and the dose drives the risk. This vignette is about the exposure side of the model: where those two numbers come from by default, how to read them, and how to **override** them to ask "what if?" — including the two most common management measures, personal protective equipment and reduced-contact irrigation. If you are new to the pipeline, read `vignette("a-get-started", package = "ambre")` first. The barrier-side levers (treatment plants, on-field practices, their log-reductions) are a different story, told in `vignette("b-initial-vs-new-scenario", package = "ambre")`. ## How exposure is set by default You never set volume, frequency or concentration by hand in a normal run. When you call `inflow_concentration()` (which `run_qmra_intial_situation()` and `run_qmra_supplementary_process()` call for you), it runs three updates in order: ``` inflow_concentration() ├─ update_pathogen() # pathogen concentration ├─ update_frequency() # number of events / year, per path └─ update_volume() # litres per event, per path ``` ```{r default-exposure, results = "hide"} scenario <- create_scenario( system.file("input_1culture_2pop.xlsx", package = "ambre") ) scenario_conc <- inflow_concentration( scenario = scenario, pathogenName = "Campylobacter jejuni" ) ``` `update_pathogen()` look up **inflow** database. `update_frequency()` and `update_volume()` look up the **exposure path** of each scenario row and copy the path-specific numbers out of the shipped database `config_ambre$path` into that row's embedded `config$exposure` table. So the model does **not** use one generic volume for every path — each crop × population × path combination gets its own exposure profile. You can see the result. The `config$exposure` table has three rows — `number_of_repeatings` (the Monte-Carlo count), `number_of_exposures` (events per year) and `volume_perEvent`: ```{r default-exposure-show} # Row 1 of this two-row scenario: irrigation-staff droplet ingestion scenario_conc$config[[1]]$exposure ``` Two quirks are worth knowing up front, and we return to them at the end: - `update_frequency()` writes only the path's **`max`** frequency, and it writes it into the `number_of_exposures` slot — the annual event count *is* the frequency. - both updates use **positional** writes (`min[[3]]`, `max[[3]]`, `value[[2]]`) that assume the exposure table keeps its current row order. ## Inspect the base numbers Before changing anything, look at what the model is assuming. Start from the PathogenName, you can see the default value of concentration for the simulated pathogen : ```{r default-concentration-show} # Row 1 of this two-row scenario: Campylobacter jejuni concentration dplyr::filter(scenario_conc$config[[1]]$inflow, PathogenName == "Campylobacter jejuni") ``` Then start from a path description and get its `PathID` with `query_exp_path()`: ```{r inspect-pathid} pid <- query_exp_path( pathName = "Ingestion of water droplets during maintenance of the irrigation system" ) pid ``` Then read the per-path volume and frequency straight from the database. Both helpers accept a vector of IDs, so you can inspect a whole scenario at once: ```{r inspect-values} scenario$PathID # the two paths in this input file query_volume(scenario$PathID) # litres per event: min / max query_frequency(scenario$PathID) # events per year: min / max ``` These are exactly the numbers `update_volume()` and `update_frequency()` inject. The volumes here are tiny — `0.001` litre is one millilitre of incidentally swallowed droplets — the frequency `40`–`60` becomes `60` because only the maximum is used and the concentration is between 100 and 500 log/L. ## Override for a what-if To explore sensitivity you replace those numbers with your own. Two helpers do this, and both expect **one value per scenario row**. `update_volume_with_desired_value()` takes a `volume` argument that is a data frame (or list) with a `min`, a `max` and a `type` column. Use `min == max` for a fixed volume, or `min < max` for a distribution spread according to the `type` specified: ```{r override-volume} scenario_ppe <- update_volume_with_desired_value( scenario = scenario, volume = data.frame( min = c(0.0005, 0.0005), # 0.5 mL per event, one entry per row max = c(0.0005, 0.0005), type = c("triangle", "triangle") ) ) scenario_ppe$config[[1]]$exposure ``` `update_frequency_with_desired_value()` takes a plain numeric **vector** of **integer**, one number of events per year per row: ```{r override-frequency} scenario_night <- update_frequency_with_desired_value( scenario = scenario, frequency = c(30L, 30L) # cap both paths at 30 events / year ) scenario_night$config[[1]]$exposure ``` The `volume_perEvent` row now reads `0.0005`, and `number_of_exposures` reads `30`: your what-if values have replaced the ones the database would have injected. `update_concentration()` takes a **data.frame** with **PathogenName**, **min**, **max** and **type** of ditribution law values. ```{r override-concentration} concentration_custom <- data.frame(PathogenName = c("Campylobacter jejuni"), min = c(1), max = c(2), type = c("uniform")) scenario_pathogen <- update_pathogen(scenario = scenario, pathoName = concentration_custom$PathogenName) scenario_low_pathogen <- update_concentration(scenario = scenario_pathogen , concentration = concentration_custom) dplyr::filter(scenario_low_pathogen$config[[1]]$inflow, PathogenName == "Campylobacter jejuni") ``` ## Modelling management measures Read those two overrides as **interventions**. This is the exposure-side complement to the barrier log-reductions in `vignette("b-treatment-vs-multibarrier", package = "ambre")`: instead of removing pathogens from the water, you change how much of the water each person meets. - **Personal protective equipment (gloves, masks, hygiene rules)** lowers the **volume** ingested or inhaled per event. Model it by setting `volume_perEvent` below the default — a gloved worker who swallows a fraction of a millilitre instead of several. - **Night-time irrigation, signage, fencing, keeping workers off wet fields** lowers the **frequency** of contact. Model it by setting `number_of_exposures` to a smaller annual count. Because the annual risk aggregates as $1 - \prod(1 - p)$ over events, fewer events mean lower annual risk even when each event is unchanged. Both are assumptions *you* choose and should defend — they are not credited from a barrier database. (The `barriere_path` / `barriere_specific` tables in `config_ambre` that *would* encode route-specific exposure reductions are shipped data, not yet wired into the engine.) ## A mini sensitivity study Let us quantify how much the volume assumption matters. One catch first: `run_qmra_initial_situation()` and `run_qmra_supplementary_process()` re-derives the per-path volumes from the database on every call (via `inflow_concentration()`), so it would overwrite any override you set. To inject a what-if value you should use `run_qmra_custom` function: Run it twice on the same file and pathogen — once with a pessimistic bare-hand volume, once with a PPE volume ten times smaller: ```{r qmra-custom, results = "hide"} sc <- create_scenario(system.file("input_1culture_2pop.xlsx", package = "ambre")) regulation_reduction <- config_ambre$regulation$regulation_value |> dplyr::filter(Country == "France") |> dplyr::select(-c(Concentration, Country, RegulationID)) regulation_concentration <- config_ambre$regulation$regulation_value |> dplyr::filter(Country == "France") |> dplyr::select(-c(Country, RegulationID, Reduction)) concentration_custom <- data.frame(PathogenName = c("Rotavirus"), min = c(1000), max = c(2000), type = c("uniform")) set.seed(2024) bare <- run_qmra_custom(scenario = sc, concentration = concentration_custom, volume = data.frame(min=c(0.005,0.005), max = c(0.01, 0.01), type = c("triangle", "triangle")), frequency = c(48L, 60L), regulationLog = regulation_reduction, regulationConcentration = regulation_concentration, initialSituation = TRUE) # min 5 mL, max 10 mL set.seed(2024) ppe <- run_qmra_custom(scenario = sc, concentratio = concentration_custom, volume = data.frame(min=c(0.0005,0.0005), max = c(0.001, 0.001), type = c("triangle", "triangle")), frequency = c(48L, 60L), regulationLog = regulation_reduction, regulationConcentration = regulation_concentration, initialSituation = TRUE) # min 0.5 mL min 0.1 mL ``` As the the other functions `run_qmra_*` this custom function return 2 plots and to formattable. See `d-interpreting-risk`for more detail on the output. ```{r qmra-custom-comparison, results = "hide"} library(ggplot2) cowplot::plot_grid( bare$dalys$Rotavirus + labs(subtitle = "min 5 mL, max 10 mL") + theme(plot.subtitle = element_text(hjust = 0.5)), ppe$dalys$Rotavirus + labs(subtitle = "min 0.5 mL min 0.1 mL") + theme(plot.subtitle = element_text(hjust = 0.5)), ncol = 2, align = "h" ) ``` The risk tracks the volume almost proportionally: a tenfold cut in swallowed volume gives roughly a tenfold cut in DALYs. More tellingly, the 95th percentile crosses the line — the bare-hand upper tail sits **above** 1e-6 while the PPE tail drops **below** it. Deciding whether a scenario meets the target can come down to this single exposure assumption. For how to read these ranges against the regulatory line, see `vignette("d-interpreting-risk", package = "ambre")`; for why every number is a distribution rather than a point, see `vignette("g-monte-carlo-engine", package = "ambre")`. ## Technical notes and caveats - **Positional writes are fragile.** `update_volume()` writes `config$exposure$min[[3]]` and `max[[3]]`; `update_frequency()` writes `config$exposure$value[[2]]`; the `_with_desired_value()` helpers do the same. The indices assume `volume_perEvent` is the third row and `number_of_exposures` the second. If the exposure table's row order ever changed, these would silently target the wrong parameter — so always re-inspect `config$exposure` after an override, as we did above. - **Frequency uses the maximum only.** `update_frequency()` copies the path's `max` frequency (ignoring `min`) into `number_of_exposures`. The default run is therefore conservative on contact frequency. Your own `update_frequency_with_desired_value()` value is used exactly as given. - **Volume units.** `volume_perEvent` multiplies the inflow concentration (organisms per litre) to yield organisms per event, so it is a volume in **litres**. The per-path values (roughly `1e-4` to `1.5e-2` L) are fractions of a millilitre up to a few millilitres of incidentally ingested water — or, for consumption paths, an equivalent mass of product treated as a volume. The CSVs carry no explicit unit column, so keep your overrides on the same litre basis. - **Overrides and the one-call entry points.** Because `run_qmra_initial_situation()` and `run_qmra_supplementary_process()` rebuild the exposure profile from the database each time, the `_with_desired_value()` helpers only take effect in a hand-assembled chain like the one above. There is no exposure-override argument on the one-call functions.