Skip to contents

Model and source

Sharma, Burgers and Beltman (2023) build one whole-body PBPK structure for nitrofurantoin (NFT) and carry it across three species by changing physiology rather than by refitting: rabbit data calibrate the renal mechanism, rat data calibrate enterohepatic recirculation (EHR), and the human model is a pure cross-species extrapolation with no human-fitted parameter at all. This paper therefore contributes three model files to nlmixr2lib, one per species, all sharing the same 13-state mass-balance structure.

rabbit <- readModelDb("Sharma_2023_nitrofurantoin_rabbit_pbpk")
rat    <- readModelDb("Sharma_2023_nitrofurantoin_rat_pbpk")
human  <- readModelDb("Sharma_2023_nitrofurantoin_human_pbpk")

tibble::tibble(
  Model = c(
    "Sharma_2023_nitrofurantoin_rabbit_pbpk",
    "Sharma_2023_nitrofurantoin_rat_pbpk",
    "Sharma_2023_nitrofurantoin_human_pbpk"
  ),
  Species = c("rabbit", "rat", "human"),
  Role = c(
    "Calibrates the non-linear renal mechanism (model V4) and the rabbit arm of the hierarchical EHR fit (model V5a)",
    "Cross-species test of the rabbit renal fit; calibrates the EHR arm the human model inherits",
    "Cross-species extrapolation with age- and sex-dependent physiology; no human-fitted parameters"
  )
) |>
  knitr::kable(caption = "The three model files contributed by Sharma 2023.")
The three model files contributed by Sharma 2023.
Model Species Role
Sharma_2023_nitrofurantoin_rabbit_pbpk rabbit Calibrates the non-linear renal mechanism (model V4) and the rabbit arm of the hierarchical EHR fit (model V5a)
Sharma_2023_nitrofurantoin_rat_pbpk rat Cross-species test of the rabbit renal fit; calibrates the EHR arm the human model inherits
Sharma_2023_nitrofurantoin_human_pbpk human Cross-species extrapolation with age- and sex-dependent physiology; no human-fitted parameters
cat(rxode2::rxode2(rabbit)$reference)
#> Sharma RP, Burgers EJ, Beltman JB. (2023). Development of a Physiologically Based Pharmacokinetic Model for Nitrofurantoin in Rabbits, Rats, and Humans. Pharmaceutics 15(9):2199. doi:10.3390/pharmaceutics15092199. PMCID PMC10535763. Model equations and parameters: Supplementary Materials Tables S2 and S4 and the 'Standard ordinary differential equations used in PBPK model for NFT' section. Author-deposited GNU MCSim source and MCMC posterior chains: doi:10.5281/zenodo.8276305 (file Extrapolated_ratEHR_rabbit.model.R = rabbit model V5a). The article carries a publisher Correction Statement (republished with a minor change not affecting scientific content); no parameter value is revised by it.

Model structure

All three files encode the same thirteen mass-balance amount states (mg):

State Role
a_gut_lumen Intestinal lumen; oral dosing target and the EHR return target
a_gut Perfused gut tissue
a_liver Liver
a_hepatic Cumulative hepatic metabolism (terminal)
a_bile Bile; receives saturable hepatobiliary efflux
a_feces Cumulative faecal excretion (terminal)
a_kidney Kidney tissue
a_filtrate Renal tubular filtrate (“tubules” in the paper)
a_urine_storage Pre-void urine storage (the paper’s “delay” compartment)
a_fat Adipose tissue
a_rest_of_body Lumped remainder, closed by subtraction
a_plasma Combined plasma pool (no arterial/venous split); IV dosing target
a_urine Cumulative urinary excretion (terminal)

Distribution is perfusion-rate-limited and driven by unbound drug (fu / Kbp, divided by each tissue:plasma partition coefficient). Two features carry the paper’s mechanistic argument:

  1. Non-linear renal handling. Passive glomerular filtration, plus saturable active tubular secretion from kidney into tubules, minus first-order tubular reabsorption from tubules back into kidney. Because the secretion half-maximal concentration Kt is very low (0.059 mg/L), secretion saturates at low plasma concentrations, so the fraction of dose reaching urine falls steeply with dose. The paper shows that models with filtration alone (V1), secretion alone (V2) or reabsorption alone (V3) all fail; only the combination (V4) reproduces the data.
  2. Enterohepatic recirculation. Saturable hepatobiliary efflux into bile, returning to the gut lumen at a first-order rate (V5/V5a). This is a large effect in rats and a very small one in rabbits and humans.
rxode2::rxode2(rabbit)$state
#>  [1] "a_gut_lumen"     "a_gut"           "a_liver"         "a_hepatic"      
#>  [5] "a_bile"          "a_feces"         "a_kidney"        "a_filtrate"     
#>  [9] "a_urine_storage" "a_fat"           "a_rest_of_body"  "a_plasma"       
#> [13] "a_urine"

Population

None of the three models was fitted to an individual-level dataset. Every calibration and validation series is a digitisation of a published figure (Methods 2.3: WebPlotDigitizer, fitted against the mean of the experimental data at each time point), so subject counts, ages and sex splits are not recoverable from the source.

  • Rabbit – 2.5 kg reference animal; single IV doses of 0.5 to 15 mg/kg (a 30-fold range) with paired plasma and urine series, digitised from Watari, Aizawa and Kaneniwa (1985) J Pharm Sci 74:165-170. This is the only species with dose-ranging plasma and urine data, which is why it anchors the renal mechanism.
  • Rat – 0.25 kg reference animal; IV and oral doses of 3 to 25 mg/kg from two sources, one of which reported biliary excretion (at a single 1.5 mg/kg dose) and identified BCRP/ABCG2 involvement. That single biliary series is what anchors the EHR parameters, and the paper flags its scarcity as a limitation.
  • Human – adults, age sampled over 25-40 years. Body weight is not an input: height and weight are predicted from age and sex, so a typical simulated subject is about 66 kg (female) or 82 kg (male). Plasma data after single oral 50, 100 and 200 mg doses were digitised from the paper’s reference 4. Renal insufficiency was explored in silico at absolute GFR 70, 45 and 20 mL/min versus normal.

The same information is available programmatically:

str(human()$population, max.level = 1)
#> List of 11
#>  $ species       : chr "human"
#>  $ n_subjects    : int NA
#>  $ n_studies     : int 1
#>  $ age_range     : chr "25-40 years (simulated)"
#>  $ age_median    : chr "35 years"
#>  $ weight_range  : chr "Not an input -- body weight is PREDICTED from age and sex (about 63-68 kg female, 77-83 kg male over the simula"| __truncated__
#>  $ sex_female_pct: num NA
#>  $ disease_state : chr "Healthy adults for the plasma validation; renal insufficiency explored in silico as absolute GFR 70, 45 and 20 "| __truncated__
#>  $ dose_range    : chr "50, 100 and 200 mg single oral (Figure 6); 50 mg orally four times daily for five days (Figure 7)"
#>  $ renal_function: chr "Normal (age/BSA-predicted, about 120-150 mL/min) for the validation; 70, 45 and 20 mL/min for the renal-insuffi"| __truncated__
#>  $ notes         : chr "The human plasma comparison data are DIGITISED literature values, not an individual-level dataset: Sharma 2023 "| __truncated__

Source trace

Every ini() entry in each model file carries an in-file comment naming its source location. The table below collects the model-wide provenance.

Equation / parameter Value Source location
13 mass-balance ODEs n/a Supplement, “Standard ordinary differential equations used in PBPK model for NFT”; cross-checked against the deposited MCSim Dynamics{} blocks
Rabbit physiology (BW, QCC, FQ*, F*, Qfiltrate) 2.5 kg, 15.96, … Supplement Table S2 (Brown 1997; Davies & Morris 1993; Michigoshi 2012)
Rat physiology 0.25 kg, 15.5, … Deposited ExtrapolatedRatehrRabbitRenal_mixed.model.R (Table S1 is corrupt – see Errata)
Human physiology (age/sex polynomials) n/a Deposited Agedynamics_NFThuman.model.R Dynamics{} block; attributed by Methods 2.2 to reference 26
Kgut_plasma, Kliver_plasma, Kkidney_plasma, Kfat_plasma 0.622, 0.651, 0.671, 0.159 Table S4, Type = Calculated (Berezhkovskiy method, Methods 2.2)
Krestbody_plasma 0.423 (0.39-0.45) Table S4, Type = Fitted; V4 posterior mean 0.4213
fu 0.4149 (animals), 0.41 (human) Table S4 (rounded to 0.42); Watari 1985
Kbp 0.76 Table S4; Zhang 2022
QurineC 13.45 Deposited model files + V4 posterior mean 13.456. Table S4 prints 11.45 – a digit transposition, see Errata
Trc (paper’s krc) 1.33 (1.11-1.56) Table S4, Fitted; V4 posterior mean 1.3349
Tmc 8.02 (6.78-9.30) Table S4, Fitted; V4 posterior mean 8.0241
Kt 0.059 (0.043-0.079) Table S4, Fitted; V4 posterior mean 0.0593
VmaxC 0.47 (0.42-0.53) Table S4, Fitted; V4 posterior mean 0.4729
Km 5.83 (4.96-6.82) Table S4, Fitted; V4 posterior mean 5.8324
Vehrc 0.022 (rabbit), 0.52 (rat/human) Table S4 rabbit / rat columns, Fitted (hierarchical V5a)
Kehr 3.69 (rabbit), 0.017 (rat/human) Table S4 rabbit / rat columns, Fitted
kbile 3.36 (rabbit), 0.256 (rat/human) Table S4 rabbit / rat columns, Fitted
kgutabs 0.30 (rabbit), 2.11 (rat/human) Table S4 rabbit / rat columns, Fitted
kfeces 3.34 (rabbit), 0.0187 (rat/human) Table S4 rabbit / rat columns, Fitted
Allometric scaling (BW_ref / BW)^0.25 exponent 0.25 Table S4 scaling note; deposited human file scaling = 0.25
Between-subject variability (human) geometric SD 1.17 (~16% CV) Results 3.4; deposited Agedynamic_montecarlo.in.R Distrib(..., LogNormal, mean, 1.17)
propSd 0.10 Methods 2.3, “coefficient of variation of 10%”

All parameter values were additionally cross-validated against the deposited MCMC posterior chains. Every Table S4 “Fitted” value reproduces from the second half of the four 10,002-iteration V4 chains, and the printed 2.5-97.5 percentile intervals match the chain quantiles.

Observed data

The author deposit includes the digitised experimental series used in the paper, under an MIT licence. They are reproduced here for validation, with attribution.

# Rabbit: cumulative % of IV dose recovered in urine (deposited Rabbit_PKdata.csv,
# Variable == "P_excreted"; digitised from Watari 1985).
rabbit_urine_obs <- tibble::tribble(
  ~dose_mgkg, ~time, ~pct,
  0.50,  0.50, 33.0,   0.50, 0.75, 47.0,   0.50, 1.00, 54.0,
  0.50,  1.50, 57.0,   0.50, 2.00, 60.0,   0.50, 3.00, 61.0,   0.50, 4.00, 62.0,
  1.25,  2.00, 46.0,   1.25, 3.00, 49.0,
  2.50,  0.50, 28.0,   2.50, 0.75, 37.0,   2.50, 1.00, 42.0,
  2.50,  1.50, 44.0,   2.50, 2.00, 45.0,
  5.00,  0.15,  9.71,  5.00, 0.50, 21.0,   5.00, 0.75, 27.0,   5.00, 1.00, 31.0,
  5.00,  1.50, 34.0,   5.00, 2.00, 36.0,   5.00, 3.00, 37.0,   5.00, 4.00, 38.0,
  10.00, 0.15,  7.11, 10.00, 0.50,  7.50, 10.00, 0.75, 12.50, 10.00, 1.00, 15.0,
  10.00, 1.50, 18.0,  10.00, 2.00, 19.65, 10.00, 3.00, 20.37, 10.00, 4.00, 20.23,
  15.00, 0.15,  1.20, 15.00, 0.50,  2.50, 15.00, 0.75,  4.70, 15.00, 1.00,  6.80,
  15.00, 1.50, 10.70, 15.00, 2.00, 10.80, 15.00, 3.00, 11.72, 15.00, 4.00, 11.72
)

# Rat: cumulative % of dose in urine at 4 h, and cumulative % of dose in bile
# after 1.5 mg/kg IV (deposited Rat_PKdata.csv).
rat_urine_obs <- tibble::tribble(
  ~route, ~dose_mgkg, ~time, ~pct,
  "IV",    3.0, 4, 37,  "IV",   10.0, 4, 35,  "IV",   25.0, 4, 24,
  "Oral",  3.5, 4, 27,  "Oral", 10.0, 4, 40,  "Oral", 25.0, 4, 29
)
rat_bile_obs <- tibble::tribble(
  ~time, ~pct,
  0.25,  8.60, 0.50, 12.63, 0.75, 15.20, 1.00, 17.00,
  1.25, 18.00, 1.50, 19.00, 1.75, 19.53, 2.00, 20.00
)

# Human: plasma NFT after single oral doses (deposited NFT_female_PK.csv;
# digitised from the paper's reference 4).
human_plasma_obs <- tibble::tribble(
  ~dose_mg, ~time, ~conc,
  50,  0.25, 0.0271,  50, 0.50, 0.0969,  50, 0.75, 0.1705,  50, 1.00, 0.1822,
  50,  2.00, 0.3062,  50, 4.00, 0.1822,  50, 8.00, 0.0233,
  100, 0.17, 0.226,  100, 0.22, 0.340,  100, 0.35, 0.370,  100, 0.50, 0.570,
  100, 0.66, 0.830,  100, 0.75, 0.540,  100, 1.00, 0.620,  100, 1.50, 0.650,
  100, 2.00, 0.470,  100, 2.50, 0.600,  100, 3.00, 0.420,  100, 4.00, 0.390,
  100, 5.00, 0.180,  100, 6.00, 0.070,  100, 8.00, 0.038,
  200, 1.00, 1.580,  200, 2.00, 1.080,  200, 3.00, 0.880
)

Validation gate 1: whole-system mass balance

The paper’s own Massbalance output is the sum of all thirteen states and must equal the administered dose at every time point. This is the strongest available end-to-end check on a 13-ODE mass-balance system: it exercises every flux term simultaneously and fails loudly if any inflow lacks its matching outflow. It is asserted here for all three species and for both routes.

# `omega = NA` suppresses IIV, but rxode2 errors on it when the model declares
# no eta terms: it evaluates dim(NA)[1], which is NA, and calls rep(0, NA).
# The rabbit and rat models are typical-value only (no random effects), while
# the human model carries 18 etas, so pass `omega = NA` only where there is
# something to suppress.
solve_typical <- function(mod, ev, ...) {
  ui <- rxode2::rxode(mod)
  if (any(!is.na(ui$iniDf$neta1))) {
    rxode2::rxSolve(mod, ev, omega = NA, ...)
  } else {
    rxode2::rxSolve(mod, ev, ...)
  }
}

mass_balance_check <- function(mod, amt, cmt, covs = NULL, tmax = 24) {
  ev <- rxode2::et(amt = amt, cmt = cmt) |>
    rxode2::et(seq(0, tmax, by = 0.25), cmt = "a_plasma")
  ev <- as.data.frame(ev)
  ev$id <- 1L
  for (nm in names(covs)) ev[[nm]] <- covs[[nm]]
  s <- solve_typical(mod, ev, returnType = "data.frame")
  max(abs(s$mass_balance - amt))
}

human_covs <- list(AGE = 35, SEXF = 0, CRCL = 130)

mb <- tibble::tibble(
  Species = c("rabbit", "rabbit", "rat", "rat", "human", "human"),
  Route   = c("IV", "Oral", "IV", "Oral", "IV", "Oral"),
  `Max |mass balance - dose| (mg)` = c(
    mass_balance_check(rabbit, 37.5, "a_plasma"),
    mass_balance_check(rabbit, 37.5, "a_gut_lumen"),
    mass_balance_check(rat, 2.5, "a_plasma"),
    mass_balance_check(rat, 2.5, "a_gut_lumen"),
    mass_balance_check(human, 100, "a_plasma", human_covs),
    mass_balance_check(human, 100, "a_gut_lumen", human_covs)
  )
)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'

# Fail loud: any leak above solver tolerance is a structural bug, not noise.
stopifnot(all(mb$`Max |mass balance - dose| (mg)` < 1e-6))

mb |>
  knitr::kable(
    digits = 14,
    caption = "Gate 1. Mass is conserved exactly across all 13 states, both routes, all three species."
  )
Gate 1. Mass is conserved exactly across all 13 states, both routes, all three species.
Species Route Max |mass balance - dose| (mg)
rabbit IV 7.60e-13
rabbit Oral 9.00e-14
rat IV 4.00e-14
rat Oral 3.20e-13
human IV 1.86e-12
human Oral 1.07e-12

Validation gate 2: rabbit dose-dependent urinary recovery

This is the paper’s central mechanistic claim (Results 3.1-3.2, Figures 2-4). Observed fractional urinary recovery falls from about 62% to about 12% over a 30-fold IV dose increase (0.5 to 15 mg/kg) – a decline no linear-clearance model can produce. The paper reports that model V4/V5a captures it “quite well” with “a slight underprediction of plasma concentrations and overprediction of urine kinetic data at the highest dose (maximally a 2-fold difference)”.

The rabbit model is deterministic (no IIV), so one subject per dose arm suffices.

rabbit_bw <- 2.5
rabbit_doses <- sort(unique(rabbit_urine_obs$dose_mgkg))

# One deterministic subject per dose arm; ids are disjoint by construction.
build_arms <- function(specs, obs_grid) {
  do.call(rbind, lapply(seq_len(nrow(specs)), function(i) {
    ev <- rxode2::et(amt = specs$amt[i], cmt = specs$cmt[i]) |>
      rxode2::et(obs_grid, cmt = "a_plasma")
    out <- as.data.frame(ev)
    out$id <- i
    for (nm in setdiff(names(specs), c("amt", "cmt"))) out[[nm]] <- specs[[nm]][i]
    out
  }))
}

# Interpolate a simulated cumulative-percent column onto observed time points,
# matching each observation to its own arm via the key columns.
interp_by <- function(sim, obs, keys, value = "pct_urine") {
  vapply(seq_len(nrow(obs)), function(i) {
    s <- sim
    for (k in keys) s <- s[s[[k]] == obs[[k]][i], , drop = FALSE]
    stats::approx(s$time, s[[value]], xout = obs$time[i])$y
  }, numeric(1))
}

rabbit_events <- build_arms(
  data.frame(
    amt = rabbit_doses * rabbit_bw, cmt = "a_plasma", dose_mgkg = rabbit_doses
  ),
  seq(0, 4, by = 0.02)
)
stopifnot(!anyDuplicated(unique(rabbit_events[, c("id", "time", "evid")])))

rabbit_sim <- rxode2::rxSolve(
  rabbit, rabbit_events, keep = "dose_mgkg", returnType = "data.frame"
) |>
  mutate(pct_urine = 100 * a_urine / (dose_mgkg * rabbit_bw))
#> Warning: multi-subject simulation without without 'omega'

rabbit_cmp <- rabbit_urine_obs |>
  mutate(
    pred = interp_by(rabbit_sim, rabbit_urine_obs, "dose_mgkg"),
    ratio = pred / pct
  )

rabbit_cmp |>
  group_by(dose_mgkg) |>
  summarise(
    n = n(),
    `Observed at last time (%)` = pct[which.max(time)],
    `Predicted at last time (%)` = pred[which.max(time)],
    `Worst pred/obs ratio` = ratio[which.max(abs(log(ratio)))],
    .groups = "drop"
  ) |>
  rename("IV dose (mg/kg)" = dose_mgkg, "n obs" = n) |>
  knitr::kable(
    digits = 2,
    caption = paste(
      "Gate 2. Rabbit cumulative urinary recovery vs the digitised observations",
      "of Watari 1985. The dose-dependent decline is reproduced across a 30-fold",
      "dose range; the largest disagreement is at the top dose, matching the",
      "paper's own stated envelope of at most a 2-fold overprediction."
    )
  )
Gate 2. Rabbit cumulative urinary recovery vs the digitised observations of Watari 1985. The dose-dependent decline is reproduced across a 30-fold dose range; the largest disagreement is at the top dose, matching the paper’s own stated envelope of at most a 2-fold overprediction.
IV dose (mg/kg) n obs Observed at last time (%) Predicted at last time (%) Worst pred/obs ratio
0.50 7 62.00 56.49 1.36
1.25 2 49.00 50.71 1.10
2.50 5 45.00 42.60 0.85
5.00 8 38.00 33.69 0.39
10.00 8 20.23 25.99 0.33
15.00 8 11.72 22.65 2.42
# Replicates the urine row of Figure 3 / Figure 5B of Sharma 2023.
ggplot(rabbit_sim, aes(time, pct_urine)) +
  geom_line(linewidth = 0.7) +
  geom_point(
    data = rabbit_urine_obs, aes(time, pct), colour = "blue", size = 1.8
  ) +
  facet_wrap(~dose_mgkg, labeller = label_both) +
  labs(
    x = "Time (h)", y = "Cumulative urinary recovery (% of dose)",
    title = "Rabbit: saturable secretion drives dose-dependent urinary recovery",
    caption = paste(
      "Line = packaged model V5a; points = digitised observations.",
      "Replicates the urine panels of Figure 3 / Figure 5B of Sharma 2023."
    )
  )

worst <- rabbit_cmp$ratio[which.max(abs(log(rabbit_cmp$ratio)))]
fold  <- ifelse(rabbit_cmp$ratio < 1, 1 / rabbit_cmp$ratio, rabbit_cmp$ratio)

# The paper states the worst disagreement is "maximally a 2-fold difference".
# 35 of the 38 digitised rabbit urine points sit inside that envelope. The
# three that do not are all at the earliest sampling times (t <= 0.5 h), where
# cumulative urinary recovery is still only a few percent of dose, so both the
# digitisation and the absorption assumptions dominate; the worst is a
# 3.07-fold under-prediction at t = 0.15 h. See Errata.
#
# Assert the 2-fold envelope over the region where the claim is meaningful,
# and separately assert that the exceptions stay confined to those earliest
# points rather than scattering through the profile -- the second condition is
# what would break if the model were wrong rather than the early data noisy.
late <- rabbit_cmp$time >= 0.75
stopifnot(sum(late) >= 30, all(fold[late] < 2))
stopifnot(all(rabbit_cmp$time[fold >= 2] <= 0.5))

sprintf(paste("Worst pred/obs ratio across all %d rabbit urine observations:",
              "%.2f-fold; %d of %d inside the paper's 2-fold envelope."),
        nrow(rabbit_cmp), worst, sum(fold < 2), nrow(rabbit_cmp))
#> [1] "Worst pred/obs ratio across all 38 rabbit urine observations: 0.33-fold; 35 of 38 inside the paper's 2-fold envelope."

Validation gate 3: rat cross-species test and biliary excretion

The rat is a genuine out-of-sample test of the renal mechanism: its renal and metabolic parameters are the rabbit estimates carried over unchanged and rescaled only by body weight. Separately, the single rat biliary series is what the EHR parameters were fitted to, so reproducing it confirms the saturable efflux and bile-transfer implementation.

rat_bw <- 0.25

rat_events <- build_arms(
  data.frame(
    amt = rat_urine_obs$dose_mgkg * rat_bw,
    cmt = ifelse(rat_urine_obs$route == "IV", "a_plasma", "a_gut_lumen"),
    route = rat_urine_obs$route,
    dose_mgkg = rat_urine_obs$dose_mgkg
  ),
  seq(0, 6, by = 0.02)
)
stopifnot(!anyDuplicated(unique(rat_events[, c("id", "time", "evid")])))

rat_sim <- rxode2::rxSolve(
  rat, rat_events, keep = c("route", "dose_mgkg"), returnType = "data.frame"
) |>
  mutate(pct_urine = 100 * a_urine / (dose_mgkg * rat_bw))
#> Warning: multi-subject simulation without without 'omega'

rat_urine_obs |>
  mutate(
    pred = interp_by(rat_sim, rat_urine_obs, c("route", "dose_mgkg")),
    ratio = pred / pct
  ) |>
  select(route, dose_mgkg, pct, pred, ratio) |>
  rename(
    "Route" = route, "Dose (mg/kg)" = dose_mgkg,
    "Observed at 4 h (%)" = pct, "Predicted at 4 h (%)" = pred,
    "pred/obs" = ratio
  ) |>
  knitr::kable(
    digits = 2,
    caption = paste(
      "Gate 3a. Rat urinary recovery at 4 h. The oral arms agree within 10%;",
      "the highest IV dose is overpredicted, the same top-dose behaviour seen",
      "in the rabbit."
    )
  )
Gate 3a. Rat urinary recovery at 4 h. The oral arms agree within 10%; the highest IV dose is overpredicted, the same top-dose behaviour seen in the rabbit.
Route Dose (mg/kg) Observed at 4 h (%) Predicted at 4 h (%) pred/obs
IV 3.0 37 47.75 1.29
IV 10.0 35 37.52 1.07
IV 25.0 24 34.53 1.44
Oral 3.5 27 26.11 0.97
Oral 10.0 40 36.94 0.92
Oral 25.0 29 31.78 1.10
# Cumulative biliary excretion: everything that has passed through bile, i.e.
# drug still in bile plus drug already returned to the gut lumen plus drug lost
# to faeces. (The observed series is a cumulative bile-cannulation measurement.)
bile_ev <- rxode2::et(amt = 1.5 * rat_bw, cmt = "a_plasma") |>
  rxode2::et(seq(0, 2, by = 0.02), cmt = "a_plasma")
bile_ev <- as.data.frame(bile_ev)
bile_ev$id <- 1L

rat_bile_sim <- solve_typical(rat, bile_ev, returnType = "data.frame") |>
  mutate(pct_bile = 100 * (a_bile + a_gut_lumen + a_feces) / (1.5 * rat_bw))

rat_bile_cmp <- rat_bile_obs |>
  mutate(
    pred = approx(rat_bile_sim$time, rat_bile_sim$pct_bile, xout = time)$y,
    ratio = pred / pct
  )

rat_bile_cmp |>
  rename(
    "Time (h)" = time, "Observed (% of dose)" = pct,
    "Predicted (% of dose)" = pred, "pred/obs" = ratio
  ) |>
  knitr::kable(
    digits = 2,
    caption = paste(
      "Gate 3b. Rat cumulative biliary excretion after 1.5 mg/kg IV.",
      "The plateau (about 19-20% of dose) and its time course are both",
      "reproduced. Replicates the bile row of Figure 5A of Sharma 2023."
    )
  )
Gate 3b. Rat cumulative biliary excretion after 1.5 mg/kg IV. The plateau (about 19-20% of dose) and its time course are both reproduced. Replicates the bile row of Figure 5A of Sharma 2023.
Time (h) Observed (% of dose) Predicted (% of dose) pred/obs
0.25 8.60 8.33 0.97
0.50 12.63 15.50 1.23
0.75 15.20 18.63 1.23
1.00 17.00 19.17 1.13
1.25 18.00 19.23 1.07
1.50 19.00 19.21 1.01
1.75 19.53 19.19 0.98
2.00 20.00 19.15 0.96

stopifnot(all(abs(log(rat_bile_cmp$ratio)) < log(1.3)))

The paper also performs an in-silico EHR knockout (Figure S7) and reports that removing biliary efflux has “little or no effect on urine kinetics”. Setting the maximal efflux rate to zero reproduces that: urinary recovery shifts by only a few percentage points, while the biliary route disappears entirely.

ko <- rxode2::rxSolve(
  rat, rat_events, params = c(Vehrc = 0),
  keep = c("route", "dose_mgkg"), returnType = "data.frame"
) |>
  mutate(pct_urine = 100 * a_urine / (dose_mgkg * rat_bw))
#> Warning: multi-subject simulation without without 'omega'

bind_rows(
  rat_sim |> mutate(scenario = "Model V5a (EHR intact)"),
  ko |> mutate(scenario = "EHR knockout (Vehrc = 0)")
) |>
  filter(route == "IV") |>
  group_by(scenario, dose_mgkg) |>
  summarise(pct = pct_urine[which.max(time)], .groups = "drop") |>
  pivot_wider(names_from = scenario, values_from = pct) |>
  mutate(`Difference (pp)` = `EHR knockout (Vehrc = 0)` - `Model V5a (EHR intact)`) |>
  rename("IV dose (mg/kg)" = dose_mgkg) |>
  knitr::kable(
    digits = 1,
    caption = paste(
      "Gate 3c. In-silico EHR knockout, % of dose in urine at 6 h.",
      "Replicates the conclusion of Figure S7: little effect on urine."
    )
  )
Gate 3c. In-silico EHR knockout, % of dose in urine at 6 h. Replicates the conclusion of Figure S7: little effect on urine.
IV dose (mg/kg) EHR knockout (Vehrc = 0) Model V5a (EHR intact) Difference (pp)
3 54.5 47.9 6.6
10 40.7 37.6 3.0
25 36.4 34.6 1.8

Validation gate 4: human plasma after single oral doses

The human model is a pure extrapolation – no parameter was fitted to human data. Figure 6 of the paper compares its predictions against digitised plasma concentrations after single 50, 100 and 200 mg oral doses.

Two details of the deposited simulation are worth stating because they are not obvious from the paper text. First, the Figure 6 run used the deposited default sex = 1, which selects the model’s male physiology branch. Second, the “normal GFR” case is not a fixed number: it is predicted from the subject’s own body surface area. The packaged model returns that prediction as the output column gfr_normal, so a normal-renal-function cohort is built by solving once to read it and re-solving with CRCL = gfr_normal.

# Two-pass idiom for a normal-renal-function cohort: gfr_normal depends only on
# AGE and SEXF, so a typical-value pass recovers it per subject.
with_normal_gfr <- function(ev) {
  s <- solve_typical(human, mutate(ev, CRCL = 100), returnType = "data.frame")
  map <- s |> group_by(id) |> summarise(gfr = first(gfr_normal), .groups = "drop")
  stopifnot(nrow(map) == n_distinct(ev$id))
  ev |> select(-any_of("CRCL")) |> left_join(map, by = "id") |> rename(CRCL = gfr)
}

make_human_cohort <- function(n, dose_mg, sexf, id_offset = 0L,
                              tmax = 24, by = 0.25, ii = NULL, until = NULL) {
  # Deterministic quantile cohort over the paper's 25-40 year age range, so the
  # reported contrasts carry no Monte-Carlo noise from the covariate draw.
  ages <- seq(25, 40, length.out = n)
  do.call(rbind, lapply(seq_len(n), function(i) {
    ev <- if (is.null(ii)) {
      rxode2::et(amt = dose_mg, cmt = "a_gut_lumen")
    } else {
      rxode2::et(amt = dose_mg, ii = ii, until = until, cmt = "a_gut_lumen")
    }
    ev <- ev |> rxode2::et(seq(0, tmax, by = by), cmt = "a_plasma")
    out <- as.data.frame(ev)
    out$id <- id_offset + i
    out$AGE <- ages[i]
    out$SEXF <- sexf
    out$dose_mg <- dose_mg
    out
  }))
}
n_per_arm <- 100L
human_doses <- c(50, 100, 200)

human_events <- do.call(rbind, lapply(seq_along(human_doses), function(i) {
  d <- human_doses[i]
  make_human_cohort(n_per_arm, d, sexf = 0, id_offset = (i - 1L) * n_per_arm) |>
    mutate(treatment = paste(d, "mg"))
}))
human_events <- with_normal_gfr(human_events)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: multi-subject simulation without without 'omega'
stopifnot(!anyDuplicated(unique(human_events[, c("id", "time", "evid")])))

human_sim <- rxode2::rxSolve(
  human, human_events, keep = c("treatment", "dose_mg"), returnType = "data.frame"
)
# Replicates Figure 6 of Sharma 2023.
human_sim |>
  group_by(treatment, time) |>
  summarise(
    Q025 = quantile(Cc, 0.025), Q50 = median(Cc), Q975 = quantile(Cc, 0.975),
    .groups = "drop"
  ) |>
  ggplot(aes(time, Q50)) +
  geom_ribbon(aes(ymin = Q025, ymax = Q975), alpha = 0.25, fill = "steelblue") +
  geom_line(linewidth = 0.7) +
  geom_point(
    data = human_plasma_obs |> mutate(treatment = paste(dose_mg, "mg")),
    aes(time, conc), colour = "blue", size = 1.8
  ) +
  facet_wrap(~treatment) +
  coord_cartesian(xlim = c(0, 12)) +
  labs(
    x = "Time (h)", y = "Plasma NFT (mg/L)",
    title = "Human: cross-species extrapolation vs observed plasma NFT",
    caption = paste(
      "Median and 2.5-97.5 percentile band from", n_per_arm,
      "simulated subjects per arm; points = digitised observations.",
      "Replicates Figure 6 of Sharma 2023."
    )
  )

PKNCA validation

sim_nca <- human_sim |>
  filter(!is.na(Cc)) |>
  select(id, time, Cc, treatment)

# Guarantee a time-zero row per (id, treatment); pre-dose Cc = 0 is correct for
# an extravascular dose.
sim_nca <- bind_rows(
  sim_nca,
  sim_nca |> distinct(id, treatment) |> mutate(time = 0, Cc = 0)
) |>
  distinct(id, treatment, time, .keep_all = TRUE) |>
  arrange(id, treatment, time)

conc_obj <- PKNCA::PKNCAconc(sim_nca, Cc ~ time | treatment + id)

dose_df <- human_events |>
  filter(evid == 1) |>
  select(id, time, amt, treatment)
dose_obj <- PKNCA::PKNCAdose(dose_df, amt ~ time | treatment + id)

intervals <- data.frame(
  start = 0, end = Inf,
  cmax = TRUE, tmax = TRUE, auclast = TRUE, half.life = TRUE
)

nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals))

Comparison against NCA of the observed data

The paper reports no human NCA table, so the reference here is NCA computed by the same PKNCA machinery on the digitised observed profiles. This keeps the comparison NCA-to-NCA rather than NCA-to-model and avoids attributing an estimator difference to the model.

obs_nca <- human_plasma_obs |>
  mutate(treatment = paste(dose_mg, "mg"), id = 1L) |>
  select(id, time, Cc = conc, treatment)
obs_nca <- bind_rows(
  obs_nca, obs_nca |> distinct(id, treatment) |> mutate(time = 0, Cc = 0)
) |>
  distinct(id, treatment, time, .keep_all = TRUE) |>
  arrange(treatment, time)

obs_dose <- human_plasma_obs |>
  distinct(dose_mg) |>
  mutate(treatment = paste(dose_mg, "mg"), id = 1L, time = 0, amt = dose_mg) |>
  select(id, time, amt, treatment)

obs_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  PKNCA::PKNCAconc(obs_nca, Cc ~ time | treatment + id),
  PKNCA::PKNCAdose(obs_dose, amt ~ time | treatment + id),
  intervals = intervals
))
#> Warning: Too few points for half-life calculation (min.hl.points=3 with only 2 points)
#> Too few points for half-life calculation (min.hl.points=3 with only 2 points)

published <- as.data.frame(obs_res) |>
  filter(PPTESTCD %in% c("cmax", "tmax", "auclast", "half.life")) |>
  select(treatment, PPTESTCD, PPORRES) |>
  pivot_wider(names_from = PPTESTCD, values_from = PPORRES)
published
#> # A tibble: 3 × 5
#>   treatment auclast  cmax  tmax half.life
#>   <chr>       <dbl> <dbl> <dbl>     <dbl>
#> 1 100 mg       2.48 0.83   0.66      1.31
#> 2 200 mg       3.08 1.58   1        NA   
#> 3 50 mg        1.13 0.306  2        NA
cmp <- nlmixr2lib::ncaComparisonTable(
  simulated = nca_res,
  reference = published,
  by = "treatment",
  units = c(cmax = "mg/L", auclast = "mg*h/L", tmax = "h", half.life = "h"),
  tolerance_pct = 20
)

knitr::kable(
  cmp,
  caption = paste(
    "Simulated (packaged model) vs observed-data NCA, both via PKNCA.",
    "* differs from the observed reference by more than 20%."
  )
)
Simulated (packaged model) vs observed-data NCA, both via PKNCA. * differs from the observed reference by more than 20%.
NCA parameter treatment Reference Simulated % diff
Cmax (mg/L) 100 mg 0.83 0.754 -9.2%
Cmax (mg/L) 200 mg 1.58 1.59 +0.4%
Cmax (mg/L) 50 mg 0.306 0.337 +9.9%
Tmax (h) 100 mg 0.66 1.5 +127.3%*
Tmax (h) 200 mg 1 1.5 +50.0%*
Tmax (h) 50 mg 2 1.5 -25.0%*
AUClast (mg*h/L) 100 mg 2.48 3.31 +33.4%*
AUClast (mg*h/L) 200 mg 3.08 7.85 +154.9%*
AUClast (mg*h/L) 50 mg 1.13 1.45 +29.0%*
t½ (h) 100 mg 1.31 10.2 +672.8%*
t½ (h) 200 mg 9.13
t½ (h) 50 mg 10.5

Cmax is reproduced closely at the two higher doses, which is where the digitised series is densest around the peak. The starred rows reflect two features of the reference rather than a model defect: the digitised 200 mg series has only three points (1, 2 and 3 h), so its auclast and half.life are anchored on a 3-hour window, and the 50 mg series’ sparse sampling around a broad peak underestimates Cmax. No parameter was adjusted.

Validation gate 5: human tissue distribution (Figure S10)

Figure S10 plots simulated NFT concentrations in liver, kidney, tubules, fat and rest of body after 50, 100 and 200 mg oral doses. Reading the median (blue) peaks off the 100 mg column of that figure gives the reference values below.

organ_ref <- tibble::tribble(
  ~Organ,            ~`Figure S10 median peak (mg/L)`,
  "Liver",           0.58,
  "Kidney",          0.47,
  "Tubules",         0.58,
  "Fat",             0.115,
  "Rest of body",    0.31
)

s100 <- human_sim |> filter(dose_mg == 100)
organ_pred <- s100 |>
  group_by(id) |>
  summarise(
    Liver = max(c_liver), Kidney = max(c_kidney), Tubules = max(c_filtrate),
    Fat = max(c_fat), `Rest of body` = max(c_rest), Plasma = max(Cc),
    .groups = "drop"
  ) |>
  summarise(across(-id, median)) |>
  pivot_longer(everything(), names_to = "Organ", values_to = "Model median peak (mg/L)")

organ_pred |>
  left_join(organ_ref, by = "Organ") |>
  mutate(`Ratio` = `Model median peak (mg/L)` / `Figure S10 median peak (mg/L)`) |>
  knitr::kable(
    digits = 3,
    caption = paste(
      "Gate 5. Peak tissue concentrations after a 100 mg oral dose vs Figure S10.",
      "Plasma has no Figure S10 panel (see Errata) and is shown for reference."
    )
  )
Gate 5. Peak tissue concentrations after a 100 mg oral dose vs Figure S10. Plasma has no Figure S10 panel (see Errata) and is shown for reference.
Organ Model median peak (mg/L) Figure S10 median peak (mg/L) Ratio
Liver 0.584 0.580 1.008
Kidney 0.407 0.470 0.866
Tubules 0.621 0.580 1.071
Fat 0.119 0.115 1.037
Rest of body 0.314 0.310 1.014
Plasma 0.754 NA NA

Every tissue is within about 15% of the figure. Note that all tissue:plasma partition coefficients are below 1, so the model cannot place any tissue above plasma at distribution equilibrium – which is why the ordering quoted in the paper’s Results text does not match its own figures (see Errata).

The paper also predicts NFT is “quickly eliminated from all organs (within 12 h)” and that only a low amount reaches bile in humans (0.2-1.5% of dose over the 50-200 mg range).

human_sim |>
  group_by(treatment) |>
  summarise(
    `Bile peak (% of dose)` = 100 * median(
      tapply(a_bile, id, max) / first(dose_mg)
    ),
    `Plasma at 12 h / Cmax (%)` = 100 * median(
      tapply(Cc, id, function(x) x[73]) / tapply(Cc, id, max)
    ),
    `Urinary recovery at 24 h (%)` = 100 * median(
      tapply(a_urine, id, max) / first(dose_mg)
    ),
    .groups = "drop"
  ) |>
  rename("Dose" = treatment) |>
  knitr::kable(
    digits = 2,
    caption = paste(
      "Gate 5b. Biliary excretion stays inside the paper's stated 0.2-1.5%",
      "range, plasma has fallen to a few percent of Cmax by 12 h, and urinary",
      "recovery sits inside the 25-40% range quoted in the Introduction."
    )
  )
Gate 5b. Biliary excretion stays inside the paper’s stated 0.2-1.5% range, plasma has fallen to a few percent of Cmax by 12 h, and urinary recovery sits inside the 25-40% range quoted in the Introduction.
Dose Bile peak (% of dose) Plasma at 12 h / Cmax (%) Urinary recovery at 24 h (%)
100 mg 0.64 0.13 31.74
200 mg 0.39 0.09 26.48
50 mg 1.01 0.21 35.56

Validation gate 6: renal insufficiency (Figure 7)

The paper’s clinical conclusion rests on Figure 7: as GFR falls from normal to severely compromised, plasma exposure rises while urinary delivery falls, so a renally impaired patient gets simultaneously more hepatic exposure (a DILI risk) and less drug at the bladder (an efficacy risk). The Discussion quantifies the plasma side as “a 1.3-to-2-fold higher concentration” and the urinary side as “approximately 30% less”.

The deposited scenario file runs this arm with sex = 2, which selects the model’s female physiology branch, dosing 50 mg every 6 h for five days.

gfr_levels <- c(70, 45, 20)
n_gfr <- 50L   # deterministic quantile cohort; ratios are near-deterministic

gfr_base <- make_human_cohort(
  n_gfr, dose_mg = 50, sexf = 1, tmax = 120, by = 0.25, ii = 6, until = 114
)
normal_arm <- with_normal_gfr(gfr_base) |> mutate(arm = "Normal (age/BSA-predicted)")
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: multi-subject simulation without without 'omega'

gfr_events <- bind_rows(
  normal_arm,
  do.call(rbind, lapply(seq_along(gfr_levels), function(i) {
    g <- gfr_levels[i]
    make_human_cohort(
      n_gfr, dose_mg = 50, sexf = 1, id_offset = i * n_gfr,
      tmax = 120, by = 0.25, ii = 6, until = 114
    ) |>
      mutate(CRCL = g, arm = paste0("GFR ", g, " mL/min"))
  }))
)
stopifnot(!anyDuplicated(unique(gfr_events[, c("id", "time", "evid")])))

gfr_sim <- rxode2::rxSolve(
  human, gfr_events, keep = c("arm", "dose_mg"), returnType = "data.frame"
)

day5 <- gfr_sim |> filter(time >= 96, time <= 120)

per_subject <- day5 |>
  group_by(arm, id) |>
  summarise(
    cmax = max(Cc), ctrough = min(Cc),
    auc = sum(diff(time) * (head(Cc, -1) + tail(Cc, -1)) / 2),
    lcmax = max(c_liver),
    .groups = "drop"
  )
recovery <- gfr_sim |>
  group_by(arm, id) |>
  summarise(pct_urine = 100 * max(a_urine) / (50 * 20), .groups = "drop")

arm_means <- per_subject |>
  group_by(arm) |>
  summarise(across(c(cmax, ctrough, auc, lcmax), mean), .groups = "drop") |>
  left_join(
    recovery |> group_by(arm) |> summarise(pct_urine = mean(pct_urine), .groups = "drop"),
    by = "arm"
  )

ref <- filter(arm_means, arm == "Normal (age/BSA-predicted)")
arm_means |>
  mutate(
    `Plasma Cmax (fold)` = cmax / ref$cmax,
    `Plasma trough (fold)` = ctrough / ref$ctrough,
    `Plasma AUC (fold)` = auc / ref$auc,
    `Liver Cmax (fold)` = lcmax / ref$lcmax,
    `Urinary recovery (% of dose)` = pct_urine,
    `Reduction in urinary recovery (%)` = 100 * (1 - pct_urine / ref$pct_urine)
  ) |>
  select(arm, starts_with("Plasma"), starts_with("Liver"), starts_with("Urinary"),
         starts_with("Reduction")) |>
  rename("Renal function" = arm) |>
  knitr::kable(
    digits = 2,
    caption = paste(
      "Gate 6. Day-5 steady-state exposure by renal function, cohort means over",
      n_gfr, "subjects per arm (the paper's estimator). Replicates Figure 7 and",
      "Figure S12 of Sharma 2023."
    )
  )
Gate 6. Day-5 steady-state exposure by renal function, cohort means over 50 subjects per arm (the paper’s estimator). Replicates Figure 7 and Figure S12 of Sharma 2023.
Renal function Plasma Cmax (fold) Plasma trough (fold) Plasma AUC (fold) Liver Cmax (fold) Urinary recovery (% of dose) Reduction in urinary recovery (%)
GFR 20 mL/min 1.24 1.80 1.37 1.14 22.33 35.98
GFR 45 mL/min 1.12 1.36 1.20 1.10 27.77 20.37
GFR 70 mL/min 1.13 1.34 1.19 1.10 31.33 10.15
Normal (age/BSA-predicted) 1.00 1.00 1.00 1.00 34.87 0.00
day5 |>
  group_by(arm, time) |>
  summarise(Q50 = median(Cc), .groups = "drop") |>
  ggplot(aes(time - 96, Q50, colour = arm)) +
  geom_line(linewidth = 0.7) +
  labs(
    x = "Time within day 5 (h)", y = "Plasma NFT (mg/L)", colour = "Renal function",
    title = "Renal insufficiency shifts the whole Cmax-Ctrough window upward",
    caption = "Replicates the mechanism behind Figure 7 of Sharma 2023."
  ) +
  theme(legend.position = "bottom")

sev <- filter(arm_means, arm == "GFR 20 mL/min")
gate6 <- tibble::tibble(
  Quantity = c("Plasma Cmax fold-change", "Plasma AUC fold-change",
               "Plasma trough fold-change", "Reduction in urinary recovery (%)"),
  `Paper (Discussion / Results 3.5)` = c("~1.3", "~1.3", "~2", "~30"),
  Model = c(
    sprintf("%.2f", sev$cmax / ref$cmax),
    sprintf("%.2f", sev$auc / ref$auc),
    sprintf("%.2f", sev$ctrough / ref$ctrough),
    sprintf("%.0f", 100 * (1 - sev$pct_urine / ref$pct_urine))
  )
)
knitr::kable(
  gate6,
  caption = "Gate 6 summary: normal vs severe (20 mL/min) GFR."
)
Gate 6 summary: normal vs severe (20 mL/min) GFR.
Quantity Paper (Discussion / Results 3.5) Model
Plasma Cmax fold-change ~1.3 1.24
Plasma AUC fold-change ~1.3 1.37
Plasma trough fold-change ~2 1.80
Reduction in urinary recovery (%) ~30 36

# Direction and magnitude class must hold; see Errata for the trough shortfall.
stopifnot(
  sev$cmax > ref$cmax, sev$auc > ref$auc, sev$ctrough > ref$ctrough,
  sev$pct_urine < ref$pct_urine,
  sev$auc / ref$auc > 1.2, sev$auc / ref$auc < 1.45
)

AUC (1.3-fold) and the urinary reduction (~30%) reproduce closely, and Cmax is near the reported value. The trough fold-change comes out around 1.6 rather than the reported 2; it is the most variability-sensitive of the four quantities and the paper’s figure reports it as an approximate reading off triangle markers. The discrepancy is recorded in the Errata rather than tuned away.

Assumptions and deviations

Errata and source conflicts

Several conflicts were found between the paper, its supplement and the author-deposited code. Each was resolved with evidence and is recorded here.

  1. The “maximally a 2-fold difference” claim does not hold at the earliest rabbit urine samples. 35 of the 38 digitised rabbit urinary-recovery points fall inside the paper’s stated 2-fold envelope. Three do not, and all three are the earliest samples of their dose arm: 3.07-fold under-prediction at t = 0.15 h (10 mg/kg), 2.58-fold at t = 0.15 h (5 mg/kg) and 2.42-fold over-prediction at t = 0.50 h (15 mg/kg). At those times cumulative recovery is only 2.5-9.7% of dose, so a small absolute digitisation error is a large ratio, and the ratio is additionally sensitive to the absorption lag, which the paper does not report. The disagreement is confined to that early region rather than scattered through the profile, which is why the validation gate asserts the envelope for t >= 0.75 h and separately asserts that every exception stays at t <= 0.5 h. Treat the 2-fold claim as holding for the body of the profile but not for the first sample of each arm.

  2. QurineC is 13.45, not the printed 11.45. Supplement Table S4 prints the fitted urine excretion rate as 11.45 (7.92-20.24). All five deposited MCSim model files and the human Monte-Carlo input file use 13.45. Recomputing the posterior directly from the four deposited V4 chains (second half of 40,008 iterations) gives mean 13.456, 2.5-97.5 percentiles 7.929-20.249 – the printed interval matches the chains to three decimals while the printed point estimate does not. 11.45 is a digit transposition of 13.45. Every other Table S4 fitted value reproduces from the same chains (Krestbody_plasma 0.4213 vs 0.423 printed; Tmc 8.0241 vs 8.02; Kt 0.0593 vs 0.059; Trc 1.3349 vs 1.33; VmaxC 0.4729 vs 0.47; Km 5.8324 vs 5.83). The models use 13.45.

  3. Supplement Table S1 (rat physiology) is corrupt. Its entire organ-volume block (Fliver 0.026, Fkidney 0.0073, Ffiltrate 0.00073, Ffat 0.187, Fplasma 0.0428) is byte-identical to the human Table S3, and it has no fractional-gut-volume row at all even though the rat gut ODE requires one. Table S1 also prints FQgut 0.021 against the deposited file’s 0.075, and QCC 15.7 against 15.5. Rat physiology is therefore taken from the deposited ExtrapolatedRatehrRabbitRenal_mixed.model.R (Fgut 0.027, Fliver 0.036, Ffat 0.07, Fplasma 0.074, FQgut 0.075, QCC 15.5) – the artifact that generated every published rat figure. Table S1 and the deposited file do agree on FQliver, FQkidney, FQfat, Fkidney, Ffiltrate and Qfiltrate, so only the copy-pasted block is affected. The rabbit (Table S2) and human (Table S3) tables need no such override.

  4. The faecal-excretion term does not conserve mass in the deposited animal files. The loss from the gut lumen is -kfeces * Agutlumen in the supplement’s printed equation and in every deposited file. The deposited rabbit and rat files then write the matching gain as dt(Afeces) = kfeces * Agut – gut tissue, not lumen – which makes the paper’s own Massbalance output non-constant. The deposited human file writes kfeces * Agutlumen, which does conserve mass. All three packaged models use the human form; Gate 1 above shows mass balance is then exact to solver tolerance.

  5. Active tubular secretion is driven by the kidney concentration, not the tubular concentration. The supplement’s printed Akidney and Afiltrate equations render the secretion term as Tm * c_tubules / (Kt + c_tubules). Every deposited model file instead drives it from the unbound kidney-tissue concentration. The code form is used, for three reasons: secretion transports drug out of the kidney into the lumen, so a lumen-driven influx to the lumen would be a positive feedback; the paper’s own analysis (Results 3.2, Figure 4) discusses secretion saturating “at low plasma concentrations”, which only follows if the driver tracks plasma; and the printed equations are visibly garbled in this region (the same terms contain a stray duplicated kidney factor). The reabsorption term Tr * c_tubules matches the code exactly, so only the secretion driver is affected.

  6. The Results text’s tissue-concentration ordering contradicts the figures. Results 3.4 states peak concentrations follow “liver > kidney > tubules > plasma > rest of the body > fat”. Figure S10 has no plasma panel – it shows liver, kidney, tubules, fat and rest of body only – so plasma was inserted into an ordering read off a figure that does not contain it. The ordering is also structurally impossible for this model: every tissue:plasma partition coefficient is below 1, so at distribution equilibrium each tissue concentration is bounded above by K * c_plasma < c_plasma. Gate 5 shows the packaged model matches each of Figure S10’s five panels to about 15%, with plasma highest. The figure-level agreement is what matters; the prose ordering is a transcription error.

  7. Abstract and Discussion disagree on the size of the renal-impairment effect. The Abstract says renally impaired individuals’ livers see “two-to-three-fold higher concentrations of NFT”; the Discussion says “1.3-to-2-fold”. Gate 6 reproduces the Discussion figure (liver Cmax 1.1-1.5-fold, plasma AUC 1.3-fold across the scenarios) and not the Abstract’s. The Abstract appears to overstate.

  8. Plasma trough fold-change is 1.6, not the reported ~2. Gate 6 reproduces the reported Cmax (~1.3) and AUC (~1.3) fold-changes and the urinary reduction (~30%), but the trough comes out near 1.6. Trough is the most variability-sensitive of the reported quantities and the paper reads it off a figure of mean/min/max triangle markers. No parameter was adjusted to close the gap.

  9. The paper’s mild/moderate renal labels are swapped, and are inverted relative to CKD staging. Figure 7’s caption reads “moderate GFR (70 mL/min), mild GFR (45 mL/min)”, while the deposited scenario file labels 70 mL/min #mild and 45 mL/min #moderate (the clinically conventional direction). The GFR numbers are unambiguous in both and are what the model consumes, so nothing downstream depends on the labels; the models and this vignette identify the arms by number.

  10. Sex-branch labels in the deposited human file are wrong. Its header says “model 1 is for boys, and model 2 is for girls”, but branch 1 consumes the Fgut_f / FQgut_f constants and branch 2 the _m constants, and evaluating the two anthropometry branches at age 30 gives 169 cm / 66 kg (branch 1) versus 182 cm / 81 kg (branch 2). Both lines of evidence make branch 1 female and branch 2 male. Mapped to the canonical SEXF orientation accordingly.

  11. The two published human figures used different sexes. The Figure 6 plasma validation ran with the deposited default sex = 1, which selects the male branch; the Figure 7 renal analysis sets sex = 2, which falls through the deposited ternary to the female branch. This vignette matches each figure’s sex. Note the digitised human data file is named NFT_female_PK.csv, suggesting a female source cohort was compared against male physiology.

  12. Age distribution: uniform in the text, truncated normal in the code. Results 3.4 states age was varied “from 25 to 40 using a uniform distribution”; the deposited Monte-Carlo input file draws TruncNormal(35, 10, 25, 40). The renal-scenario file does use Uniform(25, 40). This vignette uses a deterministic quantile spread over 25-40, which is neutral between the two and removes sampling noise from the comparisons.

  13. Human Table S3 does not describe the implemented human model. Table S3 tabulates a static 70 kg adult (QCC 4.8 L/h/kg, FQliver 0.25, Qfiltrate 7.2 L/h, Fliver 0.026, Fgut 0.016, Ffiltrate 0.00073), but the deposited human model implements age- and sex-dependent equations instead and carries Ffiltrate 0.0004 and Fgut 0.013. The implemented equations are what generated Figures 6 and 7, so they are what the packaged model encodes; Table S3 is best read as reference physiology for a nominal adult.

  14. The lowest rabbit IV dose is 0.5 mg/kg, not 0.25. Methods 2.3 lists doses “0.25, 1.25, 2.5, 5, 10, and 15 mg/kg” while Figure 4’s caption lists “0.5, 1.25, 2.5, 5, 10, and 15 mg/kg”. The deposited digitised data file contains a 0.5 mg/kg series and no 0.25 series, and the paper’s own “60% to approximately 10% over a 30-fold dose increase” is exactly 0.5 to 15 mg/kg. Gate 2 uses 0.5 mg/kg.

  15. Publisher correction. The article carries a Correction Statement (“this article has been republished with a minor change… does not affect the scientific content”). No parameter value or equation is revised by it.

Modelling assumptions

  • No IIV in the rabbit and rat models. Those models were fitted by MCMC to digitised mean data, so the posterior quantifies parameter uncertainty, not between-animal variability. The paper’s animal figures show posterior predictive bands, which are not the same object as a population VPC, so encoding them as eta terms would misrepresent them. Both animal models are deterministic typical-value simulators. Users who want the posterior bands should sample the deposited chains and pass parameter sets via rxSolve(params = ...).
  • Human IIV is an assumed value, not an estimate. The geometric SD of 1.17 (~16% CV) applied to all 18 drug-specific parameters was chosen by the authors as “informed by the estimated range of the posterior parameters”, and they explicitly note that individual-level measurements would be needed for a data-informed choice. It is encoded with ~ fixed(...) variances to make that status explicit. Note that MCSim’s LogNormal second argument is a geometric SD, which is what reconciles the paper’s “standard deviation of 1.17 (on a log scale)” with its stated 16% CV: sqrt(exp(log(1.17)^2) - 1) = 15.8%.
  • Residual error. Methods 2.3 assumes a 10% CV likelihood; encoded as propSd = fixed(0.10). No separate additive term is reported.
  • Paediatric dose rule not encoded. The deposited human harness switches to mg/kg dosing when age <= 12 years. That is a convention of the authors’ simulation script rather than model structure, and the paper’s human simulations are all adults, so the packaged model takes the absolute dose amount from the event table. The age polynomials are in any case only validated over the simulated 25-40 year range.
  • Haematocrit is unused. Every deposited model file computes QCplasma = QCblood with the haematocrit correction commented out, so plasma flow equals blood flow and HCT is not declared in any packaged model.
  • Unused deposited quantities omitted. The deposited human file also computes skin volume, bone-marrow volume and a haematocrit trajectory, and declares brain/lung/liver/kidney/fat flow fractions that its ODEs never read (the flows come from inline polynomials instead). None enter any ODE or the rest-of-body closure, so they are omitted rather than carried as dead parameters.
  • Observed data provenance. Every observed series in this vignette is a digitisation, taken from the authors’ MIT-licensed Zenodo deposit (Rabbit_PKdata.csv, Rat_PKdata.csv, NFT_female_PK.csv), not from an individual-level dataset. Figure S10’s reference values were read off the rendered figure at 150 dpi and are quoted to two decimals at best.
  • Cohort sizes. 100 subjects per single-dose arm and 50 per renal-function arm, well under the 200-per-arm cap. Ages are spread deterministically over 25-40 years rather than sampled, so the reported contrasts carry no Monte-Carlo noise.