The config_ambre database

library(ambre)
set.seed(2024)
library(dplyr)

Every risk figure ambre produces is ultimately read out of one bundled object: config_ambre. It is the knowledge base of the package – pathogen concentrations, dose-response models, log-reduction credits for treatment and field barriers, exposure paths, crops and costs, all in one nested list. When you run create_scenario(), a full copy of config_ambre is embedded in each row of the scenario and mutated in place as the pipeline runs (see vignette("a-get-started", package = "ambre")).

This vignette is the practical reference for that object: what is inside it, how the tables join, and how you would adapt it to your own case.

Overview: 8 slots, 16 CSVs

config_ambre has eight top-level slots. Three of them (treatment, path, economic) are themselves lists of sub-tables, so the object holds 16 tables in total. Each table is assembled from one CSV in data-raw/ by the build script data-raw/config_ambre.R.

str(config_ambre, max.level = 1)
#> List of 9
#>  $ exposure    : spc_tbl_ [3 × 10] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..- attr(*, "spec")=List of 3
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<externalptr> 
#>  $ inflow      : spc_tbl_ [38 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..- attr(*, "spec")=List of 3
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<externalptr> 
#>  $ treatment   :List of 5
#>  $ doseresponse: spc_tbl_ [36 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..- attr(*, "spec")=List of 3
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<externalptr> 
#>  $ path        :List of 3
#>  $ crop        : spc_tbl_ [6 × 4] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..- attr(*, "spec")=List of 3
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<externalptr> 
#>  $ health      : spc_tbl_ [38 × 5] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#>   ..- attr(*, "spec")=List of 3
#>   .. ..- attr(*, "class")= chr "col_spec"
#>   ..- attr(*, "problems")=<externalptr> 
#>  $ economic    :List of 3
#>  $ regulation  :List of 2

The sub-tables live under the three list slots:

lapply(config_ambre[c("treatment", "path", "economic")], names)
#> $treatment
#> [1] "processes"        "schemes"          "barrier_path"     "barrier_specific"
#> [5] "barrier_decay"   
#> 
#> $path
#> [1] "description" "frequency"   "volume"     
#> 
#> $economic
#> [1] "water_need" "population" "cost"

Here is the full file-to-slot mapping, read straight from data-raw/config_ambre.R. This is the map you need when you want to edit the source data:

mapping <- tibble::tribble(
  ~`CSV in data-raw/`,                  ~`Slot in config_ambre`,
  "exposure.csv",                       "exposure",
  "ambre_pathogene.csv",                "inflow",
  "ambre_barriere_general.csv",         "treatment$processes",
  "treatment_schemes.csv",              "treatment$schemes",
  "ambre_barriere_voie.csv",            "treatment$barriere_path",
  "ambre_barriere_specifique.csv",      "treatment$barriere_specific",
  "ambre_barriere_deperissement.csv",   "treatment$barriere_decay",
  "ambre_doseresponse.csv",             "doseresponse",
  "ambre_voie_description.csv",         "path$description",
  "ambre_voie_frequence.csv",           "path$frequency",
  "ambre_voie_volume.csv",              "path$volume",
  "ambre_culture_hauteur.csv",          "crop",
  "ambre_sante.csv",                    "health",
  "ambre_culture_eau.csv",              "economic$water_need",
  "ambre_population.csv",               "economic$population",
  "ambre_barriere_cout.csv",            "economic$cost"
)
knitr::kable(mapping)
CSV in data-raw/ Slot in config_ambre
exposure.csv exposure
ambre_pathogene.csv inflow
ambre_barriere_general.csv treatment\(processes | |treatment_schemes.csv |treatment\)schemes
ambre_barriere_voie.csv treatment\(barriere_path | |ambre_barriere_specifique.csv |treatment\)barriere_specific
ambre_barriere_deperissement.csv treatment\(barriere_decay | |ambre_doseresponse.csv |doseresponse | |ambre_voie_description.csv |path\)description
ambre_voie_frequence.csv path\(frequency | |ambre_voie_volume.csv |path\)volume
ambre_culture_hauteur.csv crop
ambre_sante.csv health
ambre_culture_eau.csv economic\(water_need | |ambre_population.csv |economic\)population
ambre_barriere_cout.csv economic$cost

In one sentence per slot: exposure holds the Monte-Carlo controls (how many runs, how many events, the per-event volume); inflow is the raw pathogen concentration in the treated wastewater; treatment carries the log-reduction credits; doseresponse and health turn a dose into an infection, an illness and a DALY; path describes the 19 exposure routes; crop and economic support the agronomic and costing calculations.

The entity-relation keys

The tables are relational: they share a small set of ID columns and are joined on them during the pipeline. Knowing the keys is what lets you cross-reference (and safely extend) the database.

Key Meaning Appears in
PathogenID / PathogenGroup one of 38 pathogens (Bacteria / Viruses / Protozoa) inflow, doseresponse, health, treatment$processes
TreatmentID one barrier (treatment step or field practice) treatment$*, economic$cost
PathID one of 19 exposure routes path$description, path$frequency, path$volume
PopulationID one of 6 exposed populations path$description, economic$population, barriere_*
MatrixID the environmental compartment (Water / Air / Plant / Soil / Product) path$description, barriere_path
CropID / CropHeight one of 6 crops and its height above the irrigation system crop, economic$water_need

path$description is the hub that ties a route to who is exposed and to which matrix:

config_ambre$path$description |>
  select(PathID, PopulationName, MatrixName, PathDescription) |>
  head(4)
#> # A tibble: 4 × 4
#>   PathID PopulationName    MatrixName PathDescription                           
#>    <dbl> <chr>             <chr>      <chr>                                     
#> 1      1 Irrigation staff  Water      Ingestion of water droplets during mainte…
#> 2      2 Irrigation staff  Water      Ingestion of water droplets during a powe…
#> 3      3 Irrigation staff  Air        Inhalation of aerosols while moving throu…
#> 4      4 Maintenance staff Plant      Ingestion following direct contact with a…

And PathogenID is what links a pathogen’s concentration to its health burden:

config_ambre$health |>
  select(PathogenID, PathogenName, dalys_per_case) |>
  inner_join(
    config_ambre$inflow |> select(PathogenID, PathogenGroup),
    by = "PathogenID"
  ) |>
  head(4)
#> # A tibble: 4 × 4
#>   PathogenID PathogenName                           dalys_per_case PathogenGroup
#>        <dbl> <chr>                                           <dbl> <chr>        
#> 1          1 Campylobacter jejuni                           0.0046 Bacteria     
#> 2          2 Escherichia coli enterohemorrhagic (E…        NA      Bacteria     
#> 3          3 Escherichia coli                               0.0547 Bacteria     
#> 4          4 Legionella pneumophila                        NA      Bacteria

The barrier families

The multi-barrier philosophy of ambre rests on the PhD thesis of Alice-Rose Thomas (INSA Lyon, 2024), carried out in the Multiware research context, which catalogues up to 49 candidate log-reduction barriers covering both the treatment plant and the field. config_ambre$treatment$processes ships a curated subset of that catalogue. Every barrier has a TreatmentName prefixed by its type:

The shipped TreatmentGroup labels and the number of distinct barriers in each family are:

config_ambre$treatment$processes |>
  distinct(TreatmentID, TreatmentName, TreatmentGroup) |>
  count(TreatmentGroup, name = "n_barriers")
#> # A tibble: 3 × 2
#>   TreatmentGroup                       n_barriers
#>   <chr>                                     <int>
#> 1 Cultivation and irrigation practices         11
#> 2 Equipment                                    10
#> 3 Quality                                       8
config_ambre$treatment$processes |>
  distinct(TreatmentID, TreatmentName) |>
  mutate(family = sub("[.].*", "", TreatmentName)) |>
  count(family, name = "n_barriers")
#> # A tibble: 3 × 2
#>   family n_barriers
#>   <chr>       <int>
#> 1 E              10
#> 2 P              11
#> 3 Q               7

So the bundled database currently encodes 28 distinct barriers across three families – a subset of the PhD’s 49, and an intentionally extensible one. Credits from several barriers combine by simple log-additivity when a scenario stacks a treatment scheme and a field barrier (vignette("b-treatment-vs-multibarrier", package = "ambre")).

Anatomy of a distribution

Eight of the sixteen tables (exposure, inflow, treatment$processes, the three barriere_* tables, path$frequency and path$volume) share the same nine-column distribution block:

type | value | min | max | mode | mean | sd | meanlog | sdlog

This is what makes ambre a Monte-Carlo engine: instead of a single number, a barrier or a concentration is a distribution that gets resampled on every run. The type column decides which of the other columns are actually read:

knitr::kable(tibble::tribble(
  ~type,           ~`columns read`,   ~draws,
  "value",         "value",           "a constant, repeated",
  "uniform",       "min, max",        "runif(min, max)",
  "triangle",      "min, max, mode",  "EnvStats::rtri(min, max, mode)",
  "norm",          "mean, sd",        "rnorm(mean, sd)",
  "lognorm",       "meanlog, sdlog",  "rlnorm(meanlog, sdlog)",
  "log10_uniform", "min, max",        "10^runif(min, max)",
  "log10_norm",    "mean, sd",        "10^rnorm(mean, sd)"
))
type columns read draws
value value a constant, repeated
uniform min, max runif(min, max)
triangle min, max, mode EnvStats::rtri(min, max, mode)
norm mean, sd rnorm(mean, sd)
lognorm meanlog, sdlog rlnorm(meanlog, sdlog)
log10_uniform min, max 10^runif(min, max)
log10_norm mean, sd 10^rnorm(mean, sd)

Here is a real barrier row. Q.1 - Activated Sludge removes a uniform 0.5-to-1 log of protozoa; every other distribution column is NA because a uniform draw only needs min and max:

config_ambre$treatment$processes |>
  filter(TreatmentName == "Q.1 - Activated Sludge") |>
  select(TreatmentName, PathogenGroup, type, value, min, max, mode)
#> # A tibble: 3 × 7
#>   TreatmentName          PathogenGroup type    value   min   max mode 
#>   <chr>                  <chr>         <chr>   <lgl> <dbl> <dbl> <lgl>
#> 1 Q.1 - Activated Sludge Bacteria      uniform NA      1       2 NA   
#> 2 Q.1 - Activated Sludge Viruses       uniform NA      1       2 NA   
#> 3 Q.1 - Activated Sludge Protozoa      uniform NA      0.5     1 NA

And the matching inflow row: the raw Campylobacter jejuni concentration is a uniform draw between 100 and 5000 organisms per litre:

config_ambre$inflow |>
  filter(PathogenName == "Campylobacter jejuni") |>
  select(PathogenName, PathogenGroup, type, value, min, max)
#> # A tibble: 1 × 6
#>   PathogenName         PathogenGroup type    value   min   max
#>   <chr>                <chr>         <chr>   <lgl> <dbl> <dbl>
#> 1 Campylobacter jejuni Bacteria      uniform NA      100  5000

The exposure slot uses the same block for the sampling controls themselves – 1000 runs, 365 events, and a triangular per-event volume:

config_ambre$exposure
#> # A tibble: 3 × 10
#>   name                 type    value   min   max mode  mean  sd    meanlog sdlog
#>   <chr>                <chr>   <dbl> <dbl> <dbl> <lgl> <lgl> <lgl> <lgl>   <lgl>
#> 1 number_of_repeatings value    1000  NA      NA NA    NA    NA    NA      NA   
#> 2 number_of_exposures  value     365  NA      NA NA    NA    NA    NA      NA   
#> 3 volume_perEvent      triang…    NA   0.5     3 NA    NA    NA    NA      NA

How these rows are turned into 1000 x 365 random values is the subject of vignette("g-monte-carlo-engine", package = "ambre"), via create_random_distribution() and generate_random_values().

Customizing the database for your case

Because the object is rebuilt from plain CSVs, adapting it is a three-step loop: edit the right CSV, rebuild, verify.

  1. Edit the CSV that maps to the slot you want to change (see the mapping table above). Add a coherent row – a fresh TreatmentID, a type from the legend above, and the columns that type requires.
  2. Rebuild by re-running data-raw/config_ambre.R, which re-reads the 16 CSVs and writes data/config_ambre.rda.
  3. Verify the new entry is reachable with the query_* helpers.

The chunk below writes to data-raw/ and rebuilds the dataset, so it is shown for reference only (eval = FALSE):

# 1. add a new quality barrier to the treatment CSV
csv_path <- file.path("data-raw", "ambre_barriere_general.csv")
new_barrier <- data.frame(
  TreatmentID = 99, TreatmentName = "Q.8 - Ozonation",
  TreatmentGroup = "Quality", PathogenGroup = "Bacteria",
  type = "uniform", value = NA, min = 2, max = 4,
  mode = NA, mean = NA, sd = NA, meanlog = NA, sdlog = NA
)
readr::write_csv(
  rbind(readr::read_csv(csv_path), new_barrier),
  csv_path
)

# 2. rebuild data/config_ambre.rda from the 16 CSVs
source(file.path("data-raw", "config_ambre.R"))

# 3. verify the new barrier resolves to its ID
query_barrier(barrierName = "Q.8 - Ozonation")

The verification helpers each translate a name into the ID the engine works with (and error early if the name is unknown), so they are the quickest way to confirm an edit landed. They run against the live config_ambre:

query_pathogen(pathogenName = "Campylobacter jejuni")
#> [1] 1
query_barrier(barrierName = "Q.3 - UV Reactor")
#> [1] 3
query_crop(cropName = "Tomato")
#> [1] 6

query_volume() and query_frequency() take a PathID instead and return the per-path min/max block that gets injected into a scenario’s exposure (this is what update_volume() / update_frequency() do automatically inside the pipeline):

query_volume(pathID = 1)
#> # A tibble: 1 × 2
#>     min   max
#>   <dbl> <dbl>
#> 1 0.001 0.001
query_frequency(pathID = 1)
#> # A tibble: 1 × 2
#>     min   max
#>   <dbl> <dbl>
#> 1    40    60

Known pitfalls

A few sharp edges to keep in mind before you rely on or extend the database.

The tables are spec_tbl_df. Because they are read with readr, each slot carries a readr column specification and an extra class:

class(config_ambre$crop)
#> [1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame"

This is harmless – dplyr verbs work as usual – but if you compare tables with identical() or write your own I/O you may trip over the spec attribute. Filtering or mutating usually drops it.

economic$cost is the least polished table. mean / meanlog are currently all NA, so a normally- or lognormally-distributed cost you add expecting mean/meanlog will not be picked up, see vignette("g-monte-carlo-engine", package = "ambre").

CropHeight is a signed height in centimetres above the irrigation outlet: -1 means underground (Onion), 0 means at ground level (Potato, Salad), and positive values mean the edible part sits above the spray (Tomato 25, Corn seed / Apple 50). It is the intended key for height-dependent barrier credits.

config_ambre$crop |>
  select(CropName, CropHeight)
#> # A tibble: 6 × 2
#>   CropName  CropHeight
#>   <chr>          <dbl>
#> 1 Corn seed         50
#> 2 Potato             0
#> 3 Onion             -1
#> 4 Apple             50
#> 5 Salad              0
#> 6 Tomato            25

See also