Bisphenol A / S / F / AF PBPK (Karrer 2018)
Source:vignettes/articles/Karrer_2018_bisphenols_pbpk.Rmd
Karrer_2018_bisphenols_pbpk.RmdModel and source
- Citation: Karrer C, Roiss T, von Goetz N, Gramec Skledar D, Peterlin Masic L, Hungerbuhler K. (2018). Physiologically Based Pharmacokinetic (PBPK) Modeling of the Bisphenols BPA, BPS, BPF, and BPAF with New Experimental Metabolic Parameters: Comparing the Pharmacokinetic Behavior of BPA with Its Substitutes. Environmental Health Perspectives 126(7):077002.
- Article: https://doi.org/10.1289/EHP2739 (PMCID PMC6108829)
- ETH Zurich. Original implementation in R with the
deSolvepackage; the complete model code and its input tables are deposited in the paper’s Supplemental Material.
Bisphenol A (BPA) is a high-production-volume plasticiser and a known
endocrine disruptor; BPS, BPF, and BPAF are the structural analogs
increasingly used to replace it. This paper builds a whole-body PBPK
model for each bisphenol and its glucuronide and sulfate conjugates,
after both peroral and dermal exposure, to compare the internal
exposures that would result from replacing BPA with each analog. The BPA
model is a recalibration of Yang et al. (2015) against the Thayer et
al. (2015) biomonitoring data (the authors found and fixed a body-weight
scaling error in the published gut-glucuronidation vmax);
BPS was calibrated against Oh et al. (2018); BPF and BPAF could not be
calibrated (no in-vivo data exist) and were parameterised by analogy
with new in-vitro glucuronidation kinetics measured in human
microsomes.
Four models are packaged, one per bisphenol, matching the four the authors built:
mod_bpa <- nlmixr2lib::readModelDb("Karrer_2018_bisphenolA_pbpk")
mod_bps <- nlmixr2lib::readModelDb("Karrer_2018_bisphenolS_pbpk")
mod_bpf <- nlmixr2lib::readModelDb("Karrer_2018_bisphenolF_pbpk")
mod_bpaf <- nlmixr2lib::readModelDb("Karrer_2018_bisphenolAF_pbpk")They share one ODE structure and the chemical-independent physiology;
they differ in molecular weight, tissue:serum partition coefficients,
glucuronidation kinetics (and its functional form), the fraction of
glucuronide subject to enterohepatic recirculation (EHR), and the
oral-uptake / urinary-clearance constants inherited from the calibrated
BPA or BPS model. All four are deterministic typical-value
simulators: the paper reports single point estimates with no
between-subject variance and no residual-error model, so none of the
files carries eta* or residual-error terms. Inventing
variances would be fabrication; see Assumptions, deviations and
errata.
Population and biological context
data.frame(
Field = c("Species", "Reference individual", "Routes", "Doses compared", "Focus"),
Value = c(
"Human (physiology Edginton et al. 2006 / ICRP 2002)",
"Adult female, 60 kg, 30 years (Table 4); structure also covers infants, toddlers, children, adolescents and adult males via Table S1",
"Peroral (gastric emptying then gut + hepatic first pass) and dermal (two parallel skin-surface depots: thermal paper and personal care products)",
"Single 500 ng/kg BW peroral or dermal (Fig 4); age/route-specific environmental estimates (Fig 5)",
"Females of childbearing age; serum and gonadal concentrations of the unconjugated bisphenols"
)
) |> knitr::kable()| Field | Value |
|---|---|
| Species | Human (physiology Edginton et al. 2006 / ICRP 2002) |
| Reference individual | Adult female, 60 kg, 30 years (Table 4); structure also covers infants, toddlers, children, adolescents and adult males via Table S1 |
| Routes | Peroral (gastric emptying then gut + hepatic first pass) and dermal (two parallel skin-surface depots: thermal paper and personal care products) |
| Doses compared | Single 500 ng/kg BW peroral or dermal (Fig 4); age/route-specific environmental estimates (Fig 5) |
| Focus | Females of childbearing age; serum and gonadal concentrations of the unconjugated bisphenols |
The reference physiology (cardiac output qcc, the
fv_* tissue-volume fractions and the fq_*
blood-flow fractions) is held at the adult-female values. Cardiac output
is an absolute per-age-group quantity and is not
body-weight scaled; the tissue volumes, the saturable-metabolism
vmax terms, the first-order rate constants and the urinary
clearances all scale with body weight WT (linearly, by
liver mass, by bw^-0.25, or by bw^0.75 as the
deposited code dictates). To simulate another age group, override
WT, qcc and the fv_* /
fq_* fractions with that group’s row from Table S1.
Model structure
Twenty-one ODE states per bisphenol: eleven for the parent, four each for the glucuronide and sulfate, and two dermal skin-surface depots.
data.frame(
Layer = c(
rep("Parent", 11), rep("Glucuronide", 4), rep("Sulfate", 4), rep("Dermal depots", 2)
),
State = c(
"stomach", "a_small_intestine", "a_liver", "a_fat", "a_gonads", "a_skin",
"a_brain", "a_rapidly_perfused", "a_slowly_perfused", "plasma", "urine",
"a_small_intestine_gluc", "gut_lumen_gluc", "serum_gluc", "urine_gluc",
"a_small_intestine_sulf", "gut_lumen_sulf", "serum_sulf", "urine_sulf",
"depot_td_thermalpaper", "depot_td_personalcare"
),
Role = c(
"Oral dose site; empties to the small intestine at rate ge",
"Enterocyte lumen; gut glucuronidation vs absorption to liver compete",
"Liver; gut-absorbed parent + first-pass glucuronidation and sulfation",
"Fat (flow-limited)", "Gonads (flow-limited; endocrine-disruption endpoint)",
"Skin (flow-limited; receives dermal penetration)", "Brain (flow-limited)",
"Richly perfused lump (flow-limited)", "Slowly perfused lump (flow-limited)",
"Well-mixed serum pool (arterial supply + venous return - renal loss)",
"Cumulative urinary parent",
"Glucuronide formed in enterocytes", "Glucuronide in the gut EHR pool",
"Glucuronide in serum submodule", "Cumulative urinary glucuronide",
"Sulfate formed in enterocytes (inert: gut sulfation off)",
"Sulfate in the gut EHR pool (inert: no sulfate EHR)",
"Sulfate in serum submodule", "Cumulative urinary sulfate",
"Skin-surface depot, thermal paper (20% absorbed, 6 h penetration half-life)",
"Skin-surface depot, personal care products (60% absorbed, 10 min half-life)"
)
) |> knitr::kable()| Layer | State | Role |
|---|---|---|
| Parent | stomach | Oral dose site; empties to the small intestine at rate ge |
| Parent | a_small_intestine | Enterocyte lumen; gut glucuronidation vs absorption to liver compete |
| Parent | a_liver | Liver; gut-absorbed parent + first-pass glucuronidation and sulfation |
| Parent | a_fat | Fat (flow-limited) |
| Parent | a_gonads | Gonads (flow-limited; endocrine-disruption endpoint) |
| Parent | a_skin | Skin (flow-limited; receives dermal penetration) |
| Parent | a_brain | Brain (flow-limited) |
| Parent | a_rapidly_perfused | Richly perfused lump (flow-limited) |
| Parent | a_slowly_perfused | Slowly perfused lump (flow-limited) |
| Parent | plasma | Well-mixed serum pool (arterial supply + venous return - renal loss) |
| Parent | urine | Cumulative urinary parent |
| Glucuronide | a_small_intestine_gluc | Glucuronide formed in enterocytes |
| Glucuronide | gut_lumen_gluc | Glucuronide in the gut EHR pool |
| Glucuronide | serum_gluc | Glucuronide in serum submodule |
| Glucuronide | urine_gluc | Cumulative urinary glucuronide |
| Sulfate | a_small_intestine_sulf | Sulfate formed in enterocytes (inert: gut sulfation off) |
| Sulfate | gut_lumen_sulf | Sulfate in the gut EHR pool (inert: no sulfate EHR) |
| Sulfate | serum_sulf | Sulfate in serum submodule |
| Sulfate | urine_sulf | Cumulative urinary sulfate |
| Dermal depots | depot_td_thermalpaper | Skin-surface depot, thermal paper (20% absorbed, 6 h penetration half-life) |
| Dermal depots | depot_td_personalcare | Skin-surface depot, personal care products (60% absorbed, 10 min half-life) |
Source trace
Every parameter in each model file carries an in-file comment pointing to its source location. The chemical-specific parameters come from the following tables.
data.frame(
Quantity = c(
"Molecular weight (g/mol)",
"Tissue:serum partition coefficients",
"Hepatic glucuronidation Km / vmax / Ksi",
"Enterocyte glucuronidation Km / vmax / Ksi",
"Hepatic sulfation Km / vmax (all = BPA)",
"Gastric emptying, enterocyte volume",
"Oral uptake small intestine -> liver",
"Urinary clearance (parent)",
"EHR fraction (1 - met1g)",
"Dermal extent of absorption / half-life / period",
"Peroral uptake period (3 min)",
"Reference physiology (qcc, fv_*, fq_*)"
),
Source = c(
"Discussion (glucuronide MW minus glucuronyl increment 176.12); BPA from deposited code",
"Table 6 (Doerge et al. 2011 for BPA; Zhang and Zhang 2006 QSAR otherwise)",
"Table 7 ('used in basic model'; Ksi only for BPAF liver)",
"Table 7 ('used in basic model'; Ksi for BPS, BPF, BPAF gut)",
"Table 7 (Kurebayashi et al. 2010; approximated for the analogs)",
"Table 8 / deposited code",
"Table 8 (BPA 2.1; BPS/BPAF 5.0 calibrated; BPF = BPA)",
"Table 8 (BPA/BPF 0.06; BPS/BPAF 0.30)",
"Table 8 (BPA 10%, BPS 67%, BPF 0%, BPAF 70%)",
"Table 5",
"deposited code (period.O = 3/60 h), not Table 5's 0.25 h",
"Table S1 / Table C3 (adult female)"
)
) |> knitr::kable()| Quantity | Source |
|---|---|
| Molecular weight (g/mol) | Discussion (glucuronide MW minus glucuronyl increment 176.12); BPA from deposited code |
| Tissue:serum partition coefficients | Table 6 (Doerge et al. 2011 for BPA; Zhang and Zhang 2006 QSAR otherwise) |
| Hepatic glucuronidation Km / vmax / Ksi | Table 7 (‘used in basic model’; Ksi only for BPAF liver) |
| Enterocyte glucuronidation Km / vmax / Ksi | Table 7 (‘used in basic model’; Ksi for BPS, BPF, BPAF gut) |
| Hepatic sulfation Km / vmax (all = BPA) | Table 7 (Kurebayashi et al. 2010; approximated for the analogs) |
| Gastric emptying, enterocyte volume | Table 8 / deposited code |
| Oral uptake small intestine -> liver | Table 8 (BPA 2.1; BPS/BPAF 5.0 calibrated; BPF = BPA) |
| Urinary clearance (parent) | Table 8 (BPA/BPF 0.06; BPS/BPAF 0.30) |
| EHR fraction (1 - met1g) | Table 8 (BPA 10%, BPS 67%, BPF 0%, BPAF 70%) |
| Dermal extent of absorption / half-life / period | Table 5 |
| Peroral uptake period (3 min) | deposited code (period.O = 3/60 h), not Table 5’s 0.25 h |
| Reference physiology (qcc, fv_, fq_) | Table S1 / Table C3 (adult female) |
Validation
The Supplemental Material deposits the authors’ complete
deSolve model code, so the strongest validation is that the
packaged rxode2 model reproduces that executable source.
The two headline predictions the paper states in the text are the serum
Cmax of unconjugated BPAF in a 60 kg adult female after a single 500
ng/kg BW dose: 1.79 pM peroral and 7.66 pM dermal
(Results, “Comparison of Model Predictions”; Figure 4A).
# Molecular weights (g/mol); see the source trace above.
mw <- c(BPA = 228.28, BPS = 249.88, BPF = 199.88, BPAF = 335.88)
mods <- list(BPA = mod_bpa, BPS = mod_bps, BPF = mod_bpf, BPAF = mod_bpaf)
WT <- 60 # adult female reference (Table 4)
# Single-dose scenario: amt (nmol) = dose (ng/kg) * WT / MW, dosed into the
# named compartment with a modeled zero-order duration (rate = -2). Peroral
# EoA = 100% (applied as the dose); dermal EoA enters via the model's f().
solve_single <- function(mod, bp, route, dose_ng_per_kg = 500,
tmax = 120, dt = 0.05) {
dose_nmol <- dose_ng_per_kg * WT / mw[[bp]]
cmt <- if (route == "oral") "stomach" else "depot_td_thermalpaper"
ev <- rxode2::et(amt = dose_nmol, cmt = cmt, rate = -2) |>
rxode2::et(seq(0, tmax, by = dt))
rxode2::rxSolve(mod, ev, c(WT = WT), returnType = "data.frame")
}BPAF single-dose serum Cmax (reproducing the deposited code)
bpaf_oral <- solve_single(mod_bpaf, "BPAF", "oral", tmax = 48, dt = 0.02)
bpaf_derm <- solve_single(mod_bpaf, "BPAF", "dermal", tmax = 120, dt = 0.05)
bpaf_cmp <- data.frame(
Route = c("Peroral", "Dermal (thermal paper)"),
`Paper Fig 4 (pM)` = c(1.79, 7.66),
`Deposited code (pM)` = c(1.566, 7.745),
`This model (pM)` = c(max(bpaf_oral$Cc) * 1000, max(bpaf_derm$Cc) * 1000),
check.names = FALSE
)
bpaf_cmp |>
dplyr::mutate(dplyr::across(dplyr::where(is.numeric), \(x) round(x, 3))) |>
knitr::kable()| Route | Paper Fig 4 (pM) | Deposited code (pM) | This model (pM) |
|---|---|---|---|
| Peroral | 1.79 | 1.566 | 1.566 |
| Dermal (thermal paper) | 7.66 | 7.745 | 7.745 |
The packaged rxode2 model reproduces the deposited
deSolve code to better than 0.1% on both routes. The dermal
prediction is within 1.1% of the value the paper prints in Figure 4
(7.745 vs 7.66 pM); the peroral prediction (1.566 pM) is 12% below the
printed 1.79 pM. That gap is a paper-versus-deposited-code
discrepancy, not a transcription error: running the authors’ own
deposited single-dose code with the printed Table 6-8 parameters gives
1.566 pM, and the rxode2 model matches it. The paper itself
notes that its deposited input tables carry roundings; see
Assumptions, deviations and errata. The value is
not tuned to the printed figure (tuning a parameter to
hit a target is prohibited).
stopifnot(
# rxode2 reproduces the deposited-code BPAF serum Cmax to <0.5%.
abs(max(bpaf_oral$Cc) * 1000 - 1.566) < 0.02,
abs(max(bpaf_derm$Cc) * 1000 - 7.745) < 0.05,
# BPAF is absorbed with heavy gut + hepatic first pass, so peroral serum is
# far lower than dermal -- the paper's qualitative finding for BPAF.
max(bpaf_oral$Cc) < max(bpaf_derm$Cc)
)Cross-compound ordering (Figure 4A)
The paper’s central comparative finding is that, across both routes, BPS gives the highest serum Cmax and BPAF the lowest, with BPA and BPF similar. The packaged models reproduce that ordering.
cc <- lapply(names(mods), function(bp) {
so <- solve_single(mods[[bp]], bp, "oral", tmax = 48, dt = 0.02)
sd <- solve_single(mods[[bp]], bp, "dermal", tmax = 120, dt = 0.05)
data.frame(
Bisphenol = bp,
`Peroral Cmax (pM)` = max(so$Cc) * 1000,
`Dermal Cmax (pM)` = max(sd$Cc) * 1000,
check.names = FALSE
)
}) |> dplyr::bind_rows()
cc |>
dplyr::mutate(dplyr::across(dplyr::where(is.numeric), \(x) signif(x, 3))) |>
knitr::kable()| Bisphenol | Peroral Cmax (pM) | Dermal Cmax (pM) |
|---|---|---|
| BPA | 29.00 | 14.60 |
| BPS | 569.00 | 28.00 |
| BPF | 35.40 | 17.20 |
| BPAF | 1.57 | 7.75 |
oral <- setNames(cc$`Peroral Cmax (pM)`, cc$Bisphenol)
derm <- setNames(cc$`Dermal Cmax (pM)`, cc$Bisphenol)
stopifnot(
# BPS highest, BPAF lowest, for BOTH routes (Fig 4A / Results text).
which.max(oral) == which(names(oral) == "BPS"),
which.min(oral) == which(names(oral) == "BPAF"),
which.max(derm) == which(names(derm) == "BPS"),
which.min(derm) == which(names(derm) == "BPAF"),
# BPA and BPF are "rather similar" (within a factor of 2).
abs(log(oral[["BPF"]] / oral[["BPA"]])) < log(2)
)Serum concentration-time profiles
prof <- lapply(names(mods), function(bp) {
so <- solve_single(mods[[bp]], bp, "oral", tmax = 96, dt = 0.1)
sd <- solve_single(mods[[bp]], bp, "dermal", tmax = 96, dt = 0.1)
dplyr::bind_rows(
data.frame(Bisphenol = bp, Route = "Peroral", time = so$time, Cc_pM = so$Cc * 1000),
data.frame(Bisphenol = bp, Route = "Dermal", time = sd$time, Cc_pM = sd$Cc * 1000)
)
}) |> dplyr::bind_rows()
ggplot(dplyr::filter(prof, time > 0, Cc_pM > 0), aes(time, Cc_pM, colour = Bisphenol)) +
geom_line() +
facet_wrap(~Route, scales = "free_y") +
scale_y_log10() +
labs(x = "Time (h)", y = "Serum concentration (pM)") +
theme_bw()
Unconjugated-bisphenol serum profiles in a 60 kg adult female after a single 500 ng/kg BW peroral or dermal dose (reproducing Figure 4A).
Mass balance
A single peroral dose must be fully accounted for at long times as parent in urine plus the two conjugates in urine plus whatever remains in the body. With the renal and metabolic sinks the dose is essentially completely excreted.
mb <- solve_single(mod_bpaf, "BPAF", "oral", tmax = 240, dt = 0.5)
dose_nmol <- 500 * WT / mw[["BPAF"]]
body_states <- c(
"stomach", "a_small_intestine", "a_liver", "a_fat", "a_gonads", "a_skin",
"a_brain", "a_rapidly_perfused", "a_slowly_perfused", "plasma",
"a_small_intestine_gluc", "gut_lumen_gluc", "serum_gluc",
"a_small_intestine_sulf", "gut_lumen_sulf", "serum_sulf"
)
last <- mb[nrow(mb), ]
recovered <- last$urine + last$urine_gluc + last$urine_sulf +
sum(as.numeric(last[body_states]))
data.frame(
Quantity = c("Dose (nmol)", "Urine parent", "Urine glucuronide",
"Urine sulfate", "Remaining in body", "Total recovered / dose"),
Value = c(
round(dose_nmol, 3), round(last$urine, 3), round(last$urine_gluc, 3),
round(last$urine_sulf, 3), round(sum(as.numeric(last[body_states])), 3),
round(recovered / dose_nmol, 4)
)
) |> knitr::kable()| Quantity | Value |
|---|---|
| Dose (nmol) | 89.318 |
| Urine parent | 0.170 |
| Urine glucuronide | 88.574 |
| Urine sulfate | 0.573 |
| Remaining in body | 0.000 |
| Total recovered / dose | 1.000 |
Assumptions, deviations and errata
-
Deposited code is authoritative; the packaged model
reproduces it exactly. The Supplemental Material deposits the
authors’
deSolvecode and its input tables. The packagedrxode2model reproduces that code to machine precision on both exposure routes for BPAF (1.566 pM peroral, 7.745 pM dermal). No parameter was tuned to any figure. - Peroral BPAF, code vs printed figure. The deposited code gives 1.566 pM for the BPAF single-dose peroral serum Cmax, 12% below the 1.79 pM the paper prints in Figure 4. The deposited input tables carry 2-3 significant-figure roundings of the full parameter set (the paper notes, e.g., that input table C2 mis-shifts one age group’s cardiac output), which is the most likely origin of the small difference; the figure’s exact simulation inputs are not recoverable from the published material. The model follows the deposited code and the printed Table 6-8 parameters.
-
Oral uptake period. Table 5 prints a peroral uptake
period of 0.25 h, but the deposited code uses 3 min
(
period.O <- 3/60). The code value is used: 3 min reproduces the published peroral tmax, 0.25 h does not. -
Conjugate volume of distribution. Table 8 lists the
conjugate volume of distribution as 0.0435 L/kg (ICRP), but the
deposited code sets it equal to the age-group plasma volume
(
VbodygC <- VplasmaC). The code is followed. -
Single dermal source. The age-comparison figures
(Fig 4/5) use one dermal source at a time (thermal paper, 20% absorbed,
6 h half-life). The second depot (
depot_td_personalcare, 60% / 10 min) is present and is used when both sources act in parallel (the environmental-exposure scenarios); the validation here doses only the thermal-paper depot, matching Figure 4. -
Inert sulfate gut pools. Gut sulfation is switched
off in the deposited code (
vmaxgutsC ~ 0,fguts = 0) and no sulfate undergoes EHR (met1s = 1), soa_small_intestine_sulfandgut_lumen_sulfstay empty. They are carried for structural symmetry with the deposited code and so that a future sulfation scenario can switch them on. - Molecular weights. BPA (228.28) is from the deposited code. The analog parent weights are derived from the Discussion’s glucuronide weights (BPS-g 426, BPF-g 376, BPAF-g 512 g/mol) minus the glucuronyl increment (176.12), giving 249.88, 199.88 and 335.88 g/mol. The BPAF value so derived reproduces the published serum Cmax, which independently checks it.
- No IIV or residual error. The paper’s basic models are deterministic typical-value simulators; the 2D Monte-Carlo uncertainty/variability analysis (Tables S6-S7) is a separate layer not reproduced here. Inventing variances would be fabrication.
- BPF / BPAF are uncalibrated. No in-vivo BPF or BPAF data exist; their uptake and clearance constants are inherited from the calibrated BPA (BPF) and BPS (BPAF) models, their EHR fractions are educated guesses from glucuronide molecular weight, and the paper states their predictions “should be considered with caution”.