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.
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 2The 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 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 BacteriaThe 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:
Q. – Quality: the
wastewater-treatment process (Activated Sludge, Maturation Pond, UV
Reactor, Chlorination, …).E. – Equipment: on-field
hardware (drip irrigation, signage, fences, personal protective
equipment, …).P. – Practices: cultivation
and irrigation practices (non-edible crop, night-time irrigation,
cooking, peeling, washing, …).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 8config_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 7So 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")).
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 NAAnd 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 5000The 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 NAHow 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().
Because the object is rebuilt from plain CSVs, adapting it is a three-step loop: edit the right CSV, rebuild, verify.
TreatmentID, a type from the legend
above, and the columns that type requires.data-raw/config_ambre.R, which re-reads the 16 CSVs and
writes data/config_ambre.rda.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] 6query_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):
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:
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.
vignette("a-get-started", package = "ambre") – how
config_ambre is embedded in a scenario and mutated through
the pipeline.vignette("g-monte-carlo-engine", package = "ambre") –
how the distribution block is sampled into 1000 x 365 values.vignette("c-input-file", package = "ambre") – the Excel
catalogues (crops, populations, paths, barriers) whose allowed values
come from these tables.vignette("e-economic-analysis", package = "ambre") –
how economic$cost, water_need and
population drive the costing.