Exendin-4 target-mediated PK/PD in rats, monkeys and humans (Gao 2012)
Source:vignettes/articles/Gao_2012_exenatide.Rmd
Gao_2012_exenatide.RmdModel and source
Gao and Jusko (2012) fitted one target-mediated drug disposition (TMDD) structure separately in three species and then bolted a glucose-insulin feedback pharmacodynamic model onto the rat fit. This vignette covers all four models the paper contributed to the library.
| Model | Source | Species |
|---|---|---|
Gao_2012_exenatide_rat |
Table 2 | rat (Sprague-Dawley) |
Gao_2012_exenatide_monkey |
Table 3, Monkeys | rhesus monkey |
Gao_2012_exenatide_human |
Table 3, Humans | human |
Gao_2012_exenatide_glucose_insulin_rat |
Tables 2 + 4, eqs. 5-7 | rat (Sprague-Dawley) |
- Citation: Gao W, Jusko WJ (2012). Target-mediated Pharmacokinetic and Pharmacodynamic Model of Exendin-4 in Rats, Monkeys, and Humans. Drug Metabolism and Disposition 40(5):990-997. doi:10.1124/dmd.111.042291. PMCID PMC3336795.
- Article: https://doi.org/10.1124/dmd.111.042291 (PMCID PMC3336795, open access)
The drug is filed under its International Nonproprietary Name exenatide; the paper throughout uses the research name exendin-4, which is what the narrative below follows.
mod_rat <- readModelDb("Gao_2012_exenatide_rat")
mod_monkey <- readModelDb("Gao_2012_exenatide_monkey")
mod_human <- readModelDb("Gao_2012_exenatide_human")
mod_pd <- readModelDb("Gao_2012_exenatide_glucose_insulin_rat")None of the four models carries between-subject variability: every
fit in the paper is a naive-pooled fit to mean profiles in ADAPT II.
Every simulation below is therefore a single deterministic typical-value
solve per arm, and no rxode2::zeroRe() or
omega = NA is needed (there is no OMEGA to suppress).
Population
Four separate experiments feed the four models.
Rat pharmacokinetics
(Gao_2012_exenatide_rat). Male Sprague-Dawley rats, 350-370
g, n = 4-7 per route and dose group, given a single intravenous bolus, a
single subcutaneous bolus, or a continuous intravenous infusion at 0.5,
5 and 50 nmol (nmol/h for the infusion). Concentration data were
supplied by Amylin Pharmaceuticals and assayed by a two-site sandwich
assay with a minimum detectable concentration of 15 pM.
Rat pharmacodynamics
(Gao_2012_exenatide_glucose_insulin_rat). A separate cohort
of male Sprague-Dawley rats spanning 80-420 g, n = 4-8 per arm, given a
two-hour intravenous infusion of saline or of exendin-4 at 3, 30, 300 or
3000 pmol/kg/min, with an intravenous D-glucose challenge of 5.7 mmol/kg
beginning 30 min after the start of the infusion. No drug
concentrations were measured in this study: the driving
concentrations were simulated from the rat pharmacokinetic model, which
is why the packaged model carries the Table 2 pharmacokinetic parameters
alongside the Table 4 pharmacodynamic ones.
Monkey (Gao_2012_exenatide_monkey).
Male rhesus monkeys, 4.3 +/- 0.7 kg, n = 3, given a single subcutaneous
injection of 1, 3 or 10 ug/kg or a single intravenous injection of 3
ug/kg (Ai et al. 2008; the concentration-time data were digitised by Gao
and Jusko from that publication).
Human (Gao_2012_exenatide_human). Three
studies pooled: study A, eight subjects at 88.5 +/- 9.4 kg receiving
0.1-0.4 ug/kg subcutaneously; study B, eight subjects at 88.8 +/- 12.1
kg receiving 0.02-0.1 ug/kg subcutaneously; study C, eleven subjects
(body mass index 21-29 kg/m2) receiving an intravenous infusion at 0.066
pmol/kg/min for 360 min.
The same information is available programmatically, e.g.
readModelDb("Gao_2012_exenatide_glucose_insulin_rat")()$population.
Source trace
The per-parameter origin is recorded as an in-file comment next to
each ini() entry. The table below collects them in one
place.
Structure
| Equation | Source location |
|---|---|
d/dt(central), d/dt(peripheral1),
d/dt(complex)
|
Gao 2012 eqs. 1-3, p. 991 (published in concentration form
dC/dt; multiplied through by Vc for the amount
form rxode2 uses) |
f(depot), d/dt(depot)
|
Gao 2012 eq. 4, p. 992:
input(t) = ka * F * Dose * exp(-ka * t) / Vc, which is
exactly a first-order depot with bioavailability F
|
rfree <- rtot - complex / vc |
Gao 2012 eq. 3 binding term kon * (Rtot - RC) * C;
Rtot is constant (no receptor synthesis or
degradation) |
d/dt(glucose) |
Gao 2012 eq. 5, p. 992 |
d/dt(insulin) |
Gao 2012 eq. 6, p. 992 |
sadair |
Gao 2012 eq. 7, p. 992:
Sd = Smax * C / (k1 + C + k2 * C^2)
|
kin_glucose, kin_insulin
|
Gao 2012 Methods, “Pharmacodynamic Model”:
kinG = koutG * Gb and kinI = koutI * Ib
|
glucose(0), insulin(0)
|
Gao 2012 eqs. 5-6 initial conditions:
Glu(0) = Dose/VG + Gb, Ins(0) = Ib
|
(delta_g > 0) rectification |
Gao 2012 Discussion, p. 996: “When glucose is not higher than basal values, the effect is shut off” |
eq. 8 (Sd_RC, driven by the receptor complex) |
not extracted – Gao 2012 Discussion rejects it on AIC and on prior Goto-Kakizaki rat evidence |
Parameters
| Model | Parameter | Value | Source |
|---|---|---|---|
| rat, rat PK/PD | lkel |
0.0839 1/min | Table 2 (CV 10%) |
| rat, rat PK/PD |
lk12 (kpt) |
0.0282 1/min | Table 2 (CV 15%) |
| rat, rat PK/PD |
lk21 (ktp) |
0.0213 1/min | Table 2 (CV 5%) |
| rat, rat PK/PD | lvc |
43.2 mL | Table 2 (CV 12%) |
| rat, rat PK/PD | lkon |
0.0207 1/(nM*min) | Table 2 (CV 42%) |
| rat, rat PK/PD | lkoff |
0.0153 1/min | Table 2 (CV 206%) |
| rat, rat PK/PD | lkint |
0.0966 1/min | Table 2 (CV 38%) |
| rat, rat PK/PD | lrtot |
5.21 nM | Table 2 (CV 5%) |
| rat | lka |
0.00820 1/min | Table 2 ka1 (CV 9%); ka2 0.00579,
ka3 0.00273 for the 5 and 50 nmol groups |
| rat | lfdepot |
1 (fixed) | Table 2; Results “Rat PK” |
| monkey |
lkel / lk12 / lk21
|
0.0346 / 0.0143 / 0.00593 1/min | Table 3, Monkeys |
| monkey | lvc |
69.3 mL/kg | Table 3, Monkeys (CV 12%) |
| monkey |
lkon / lkoff / lkint
|
0.272 1/(pM*min) / 0.0326 / 0.00211 1/min | Table 3, Monkeys |
| monkey | lrtot |
60.6 pM | Table 3, Monkeys (CV 33%) |
| monkey | lka |
0.0244 1/min | Table 3 range 0.0244-0.0142; upper end assigned to the lowest dose per Fig. 7A |
| monkey | lfdepot |
0.688 | Table 3, Monkeys (CV 8%) – estimated, unlike rat and human |
| human |
lkel / lk12 / lk21
|
0.013 / 0.0685 / 0.0846 1/min | Table 3, Humans |
| human | lvc |
111 mL/kg | Table 3, Humans (CV 168%) |
| human |
lkon / lkoff / lkint
|
0.000411 1/(pM*min) / 0.566 / 0.00342 1/min | Table 3, Humans |
| human | lrtot |
1240 pM (fixed) | Table 3, Humans; Results “Human PK” |
| human | lka |
0.0148 1/min | Table 3 range 0.00550-0.0148; upper end assigned to the lowest doses per Fig. 7A |
| rat PK/PD | lkout_glucose |
0.046 1/min | Table 4 (CV 9%) |
| rat PK/PD | lkout_insulin |
0.483 1/min | Table 4 (CV 50%) |
| rat PK/PD | lvg |
0.208 L/kg | Table 4 (CV 5%) |
| rat PK/PD |
lsstim_insulin_glucose (SIns) |
0.157 1/nM | Table 4 (CV 46%) |
| rat PK/PD |
lsstim_glucose_insulin (SGlu) |
0.0684 1/mM | Table 4 (CV 20%) |
| rat PK/PD | lsmax |
4.67 | Table 4 (CV 30%) |
| rat PK/PD |
lkadair1 (k1) |
0.826 nM | Table 4 (CV 71%) |
| rat PK/PD |
lkadair2 (k2) |
0.0153 1/nM | Table 4 (CV 69%) |
| rat PK/PD |
FPG (Gb), INS_BL
(Ib) |
per arm | Not published – digitised from Fig. 5; see Assumptions and deviations |
| all |
addSd*, propSd*
|
0 (fixed) |
Not published – Methods gives
Vi = (sigma1 + sigma2 * Y)^2 but no sigma values |
Part 1 – the target-mediated pharmacokinetic model
Printed identities
Two arithmetic identities that the paper states in prose are
recomputed from the packaged ini() values. These do not
require a solve, and they confirm the concentration scale each species
runs on: the rat model is nanomolar (kon in 1/(nM*min))
while the monkey and human models are picomolar.
ini_of <- function(mod) {
df <- rxode2::rxode(mod)$iniDf
stats::setNames(df$est, df$name)
}
# Every concentration-time plot below uses a log y axis, so rows with a
# non-positive concentration (pre-absorption time-zero rows on the depot arms)
# have to come out. This is a PLOTTING filter and must never be reused as the
# PKNCA input filter, which needs its time-zero record kept.
plot_positive <- function(d) d[!is.na(d$Cc) & d$Cc > 0, , drop = FALSE]
p_rat <- ini_of(mod_rat)
p_monkey <- ini_of(mod_monkey)
p_human <- ini_of(mod_human)
identities <- tibble::tibble(
species = c("rat", "monkey", "human"),
scale = c("nM", "pM", "pM"),
kd_model = c(
exp(p_rat[["lkoff"]]) / exp(p_rat[["lkon"]]),
exp(p_monkey[["lkoff"]]) / exp(p_monkey[["lkon"]]),
exp(p_human[["lkoff"]]) / exp(p_human[["lkon"]])
),
kd_published = c(0.74, 0.12, 1380), # Abstract / Results; human 1.38 nM = 1380 pM
clc_model = c(
exp(p_rat[["lkel"]]) * exp(p_rat[["lvc"]]) * 1000, # mL/min (Vc absolute)
exp(p_monkey[["lkel"]]) * exp(p_monkey[["lvc"]]) * 1000, # mL/min/kg (Vc per kg)
exp(p_human[["lkel"]]) * exp(p_human[["lvc"]]) * 1000 # mL/min/kg (Vc per kg)
),
clc_published = c(3.62, 2.39, 1.48)
) |>
dplyr::mutate(
kd_pct = 100 * (kd_model - kd_published) / kd_published,
clc_pct = 100 * (clc_model - clc_published) / clc_published
)
identities |>
dplyr::select(species, scale, kd_model, kd_published, kd_pct,
clc_model, clc_published, clc_pct) |>
dplyr::rename(
"Species" = species,
"Conc. scale" = scale,
"KD = koff/kon (model)" = kd_model,
"KD published" = kd_published,
"KD % diff" = kd_pct,
"CLc = kel*Vc (model)" = clc_model,
"CLc published" = clc_published,
"CLc % diff" = clc_pct
) |>
knitr::kable(
digits = c(0, 0, 4, 2, 1, 3, 2, 1),
caption = paste(
"Printed identities recomputed from the packaged ini() values.",
"KD in the units of each species' concentration scale;",
"CLc in mL/min for the rat (absolute Vc) and mL/min/kg for monkey and human."
)
)| Species | Conc. scale | KD = koff/kon (model) | KD published | KD % diff | CLc = kel*Vc (model) | CLc published | CLc % diff |
|---|---|---|---|---|---|---|---|
| rat | nM | 0.7391 | 0.74 | -0.1 | 3.624 | 3.62 | 0.1 |
| monkey | pM | 0.1199 | 0.12 | -0.1 | 2.398 | 2.39 | 0.3 |
| human | pM | 1377.1290 | 1380.00 | -0.2 | 1.443 | 1.48 | -2.5 |
# KD is pure arithmetic on two printed estimates, so it must land on the
# published value to within display rounding of koff and kon.
stopifnot(all(abs(identities$kd_pct) < 1))
# CLc likewise, EXCEPT for humans: Table 3 prints kel to two significant
# figures (0.013), so kel*Vc can only reproduce 1.48 mL/min/kg to about 3%.
stopifnot(
abs(identities$clc_pct[identities$species == "rat"]) < 0.5,
abs(identities$clc_pct[identities$species == "monkey"]) < 0.5,
abs(identities$clc_pct[identities$species == "human"]) < 5
)Rat: single intravenous and subcutaneous doses (Figure 2)
ka was estimated separately for each subcutaneous dose
group and decreases with dose (Table 2 and Fig. 7A), with no
published functional form linking the two. The packaged model carries
the lowest-dose value, so the subcutaneous arms below override
ka per dose group – exactly what the model file’s
ini() comment instructs.
# Helper: one deterministic arm. `dose_cmt` is an ODE state name, never an
# algebraic observable such as `Cc` (see the skill's failure-pattern 2).
make_arm <- function(id, dose, dose_cmt, times, label,
rate = 0, extra = NULL) {
dosing <- data.frame(
id = id, time = 0, amt = dose, rate = rate, evid = 1L, cmt = dose_cmt
)
obs <- data.frame(
id = id, time = times, amt = NA_real_, rate = NA_real_,
evid = 0L, cmt = "central"
)
out <- dplyr::bind_rows(dosing, obs)
out$treatment <- label
if (!is.null(extra)) for (nm in names(extra)) out[[nm]] <- extra[[nm]]
dplyr::arrange(out, .data$time, dplyr::desc(.data$evid))
}
rat_doses <- c(0.5, 5, 50) # nmol
rat_ka <- c(0.00820, 0.00579, 0.00273) # Table 2: ka1, ka2, ka3
t_iv <- sort(unique(c(seq(0, 240, by = 0.25))))
t_sc <- sort(unique(c(seq(0, 1440, by = 2))))
rat_iv <- dplyr::bind_rows(lapply(seq_along(rat_doses), function(i) {
make_arm(i, rat_doses[i], "central", t_iv,
label = paste(rat_doses[i], "nmol IV"))
}))
rat_sc <- dplyr::bind_rows(lapply(seq_along(rat_doses), function(i) {
make_arm(10L + i, rat_doses[i], "depot", t_sc,
label = paste(rat_doses[i], "nmol SC"))
}))
stopifnot(!anyDuplicated(unique(rat_iv[, c("id", "time", "evid")])),
!anyDuplicated(unique(rat_sc[, c("id", "time", "evid")])))
sim_rat_iv <- rxode2::rxSolve(
mod_rat, events = rat_iv, keep = "treatment", useLinCmt = FALSE
) |> as.data.frame()
#> Warning: multi-subject simulation without without 'omega'
# The subcutaneous arms each need their own ka, so solve one arm at a time
# with the dose-group value substituted (Table 2 ka1 / ka2 / ka3).
sim_rat_sc <- dplyr::bind_rows(lapply(seq_along(rat_doses), function(i) {
ev <- dplyr::filter(rat_sc, .data$id == 10L + i)
rxode2::rxSolve(
mod_rat, events = ev, params = c(lka = log(rat_ka[i])),
keep = "treatment", useLinCmt = FALSE
) |> as.data.frame()
}))
dplyr::bind_rows(sim_rat_iv, sim_rat_sc) |>
plot_positive() |>
ggplot(aes(time, Cc, colour = treatment)) +
geom_line(linewidth = 0.7) +
scale_y_log10() +
scale_x_continuous(limits = c(0, 600)) +
labs(x = "Time (min)", y = "Exendin-4 (nmol/L)", colour = NULL,
title = "Rat: single IV and SC doses",
caption = "Replicates Figure 2 of Gao 2012.")
#> Warning: Removed 1260 rows containing missing values or values outside the scale range
#> (`geom_line()`).
The intravenous curves show the shape the paper describes: a rapid early decline at the lowest dose that flattens as the receptor pool saturates at higher doses. That nonlinearity is an emergent property of the target-binding structure, not a fitted parameter, which is what makes the next section the load-bearing test of the transcription.
Rat: the continuous-infusion clearance test (Table 1)
Table 1 reports Dose/Css from continuous intravenous
infusion at 0.5, 5 and 50 nmol/h. Because clearance falls by more than a
factor of two across that 100-fold dose range purely through receptor
saturation, reproducing it exercises kon,
koff, kint, Rtot,
kel and Vc jointly.
inf_rates <- c(0.5, 5, 50) / 60 # nmol/h -> nmol/min
inf_dur <- 720 # min; long enough to reach steady state
rat_inf <- dplyr::bind_rows(lapply(seq_along(inf_rates), function(i) {
make_arm(20L + i, dose = inf_rates[i] * inf_dur, dose_cmt = "central",
times = seq(0, inf_dur, by = 1), rate = inf_rates[i],
label = paste(c(0.5, 5, 50)[i], "nmol/h"))
}))
sim_rat_inf <- rxode2::rxSolve(
mod_rat, events = rat_inf, keep = "treatment", useLinCmt = FALSE
) |> as.data.frame()
#> Warning: multi-subject simulation without without 'omega'
css_tbl <- sim_rat_inf |>
dplyr::group_by(.data$treatment) |>
dplyr::summarise(
css_600 = .data$Cc[which.min(abs(.data$time - 600))],
css = .data$Cc[which.min(abs(.data$time - inf_dur))],
.groups = "drop"
) |>
dplyr::mutate(
rate_nmol_min = inf_rates[match(.data$treatment, paste(c(0.5, 5, 50), "nmol/h"))],
cl_model = 1000 * .data$rate_nmol_min / .data$css, # mL/min
cl_published = c(7.25, 4.42, 3.17)[match(
.data$treatment, paste(c(0.5, 5, 50), "nmol/h"))],
pct_diff = 100 * (.data$cl_model - .data$cl_published) / .data$cl_published
) |>
dplyr::arrange(.data$rate_nmol_min)
css_tbl |>
dplyr::select(treatment, css, cl_model, cl_published, pct_diff) |>
dplyr::rename(
"Infusion" = treatment,
"Css (nmol/L)" = css,
"Dose/Css, model (mL/min)" = cl_model,
"Table 1 Dose/Css (mL/min)" = cl_published,
"% difference" = pct_diff
) |>
knitr::kable(digits = c(0, 3, 2, 2, 1),
caption = "Dose-dependent clearance from continuous IV infusion, against Gao 2012 Table 1.")| Infusion | Css (nmol/L) | Dose/Css, model (mL/min) | Table 1 Dose/Css (mL/min) | % difference |
|---|---|---|---|---|
| 0.5 nmol/h | 1.205 | 6.91 | 7.25 | -4.6 |
| 5 nmol/h | 18.358 | 4.54 | 4.42 | 2.7 |
| 50 nmol/h | 224.059 | 3.72 | 3.17 | 17.3 |
# The simulation must actually have reached steady state, or the clearance
# above is meaningless. This also stops the check silently passing on a
# still-rising profile.
stopifnot(all(abs(css_tbl$css_600 / css_tbl$css - 1) < 0.01))
# Clearance must FALL with dose. This is a large structural effect (the
# published values fall 7.25 -> 3.17, a factor of 2.3) in a fully
# deterministic solve, so asserting the ordering is safe here in a way it
# would not be for a noisy per-subject statistic.
stopifnot(all(diff(css_tbl$cl_model) < 0))
stopifnot(all(diff(css_tbl$cl_published) < 0))
# Magnitude: observed range of |% difference| is 4.6 / 2.7 / 17.3 across the
# three arms. The bound is set well outside that so it stays a real gate
# without being brittle; do not tighten it to the observed maximum.
stopifnot(max(abs(css_tbl$pct_diff)) < 25)The nonlinearity is reproduced in the right direction and to within 17% across a 100-fold dose range, with the two lower arms inside 5%.
Rat: PKNCA on the intravenous-bolus arms
# Filter with `!is.na(Cc)` ONLY: adding `time > 0` or `Cc > 0` would drop the
# time-zero row PKNCA needs to anchor AUC. The observation grid already starts
# at time 0 and the dose record sorts ahead of it, so for this IV bolus the
# time-zero concentration is the model's own dose/Vc -- exactly what PKNCA
# wants. Assert that rather than patching a row in blind.
nca_in <- sim_rat_iv |>
dplyr::filter(!is.na(.data$Cc)) |>
dplyr::select(id, time, Cc, treatment) |>
dplyr::arrange(id, treatment, time)
t0 <- dplyr::filter(nca_in, .data$time == 0)
stopifnot(
nrow(t0) == dplyr::n_distinct(nca_in$id), # one time-zero row per arm
all(t0$Cc > 0) # ...and it is the post-bolus peak
)
conc_obj <- PKNCA::PKNCAconc(
nca_in, Cc ~ time | treatment + id, concu = "nmol/L", timeu = "min"
)
dose_df <- rat_iv |>
dplyr::filter(.data$evid == 1) |>
dplyr::select(id, time, amt, treatment) |>
dplyr::mutate(route = "intravascular")
dose_obj <- PKNCA::PKNCAdose(
dose_df, amt ~ time | treatment + id, doseu = "nmol", route = "route"
)
intervals <- data.frame(
start = 0, end = Inf,
cmax = TRUE, tmax = TRUE, aucinf.obs = TRUE, half.life = TRUE,
cl.obs = TRUE, vss.obs = TRUE
)
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals))
stopifnot(nrow(as.data.frame(nca_res$result)) > 0)Table 1 of the paper reports CL, Vss and
the biexponential rate constants lambda1 /
lambda2 obtained by non-compartmental analysis and
WinNonlin curve-fitting of the observed rat data.
PKNCA units come out as L/min and L, so they are converted
to the paper’s mL scale before the comparison.
sim_nca_wide <- as.data.frame(nca_res$result) |>
dplyr::filter(.data$PPTESTCD %in% c("cmax", "aucinf.obs", "half.life",
"cl.obs", "vss.obs")) |>
dplyr::select(treatment, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = "PPTESTCD", values_from = "PPORRES") |>
dplyr::mutate(
cl.obs = 1000 * .data$cl.obs, # L/min -> mL/min
vss.obs = 1000 * .data$vss.obs # L -> mL
)
published <- tibble::tribble(
~treatment, ~half.life, ~cl.obs, ~vss.obs,
"0.5 nmol IV", 0.693 / 0.0363, 4.99, 44.8,
"5 nmol IV", 0.693 / 0.0240, 3.40, 57.3,
"50 nmol IV", 0.693 / 0.0174, 3.39, 68.2
)
cmp <- nlmixr2lib::ncaComparisonTable(
simulated = dplyr::select(sim_nca_wide, treatment, half.life, cl.obs, vss.obs),
reference = published,
by = "treatment",
units = c(half.life = "min", cl.obs = "mL/min", vss.obs = "mL"),
tolerance_pct = 20
)
knitr::kable(
cmp,
digits = 2,
caption = paste(
"Simulated NCA against Gao 2012 Table 1 (t1/2 taken as 0.693/lambda2).",
"* differs from the reference by more than 20%."
)
)| NCA parameter | treatment | Reference | Simulated | % diff |
|---|---|---|---|---|
| t½ (min) | 0.5 nmol IV | 19.1 | 38.1 | +99.5%* |
| t½ (min) | 5 nmol IV | 28.9 | 37.5 | +29.8%* |
| t½ (min) | 50 nmol IV | 39.8 | 35.2 | -11.7% |
| CL/F (mL/min) | 0.5 nmol IV | 4.99 | 6.57 | +31.7%* |
| CL/F (mL/min) | 5 nmol IV | 3.4 | 4.58 | +34.8%* |
| CL/F (mL/min) | 50 nmol IV | 3.39 | 3.87 | +14.3% |
| Vss/F (mL) | 0.5 nmol IV | 44.8 | 96.4 | +115.1%* |
| Vss/F (mL) | 5 nmol IV | 57.3 | 78.6 | +37.1%* |
| Vss/F (mL) | 50 nmol IV | 68.2 | 83.5 | +22.5%* |
Several rows are starred, and the reason is documented rather than
tuned away. Table 1’s CL and Vss come from
non-compartmental analysis of observed data whose first sample
is at 5 min, fitted biexponentially, so they miss the very
rapid early target-mediated decline that the model’s true
AUC(0-Inf) includes. The paper’s own arithmetic confirms
this reading: Results states that CLc is “approximately 73%
of total clearance at the lowest dose”, and 3.62 / 4.99 (the Table 1
value) = 72.5%, whereas 3.62 divided by the model’s own 0.5 nmol
clearance is nearer 55%. A target-mediated transcription should
be validated on the infusion arm, not on the bolus arm – which
is what the previous section does.
# The 50 nmol arm is the one where receptor binding is saturated and the
# observed biexponential terminal phase IS the model's terminal phase, so
# half-life must agree there.
hl_50 <- sim_nca_wide$half.life[sim_nca_wide$treatment == "50 nmol IV"]
stopifnot(abs(hl_50 - 0.693 / 0.0174) / (0.693 / 0.0174) < 0.15)
# Model clearance must exceed the NCA clearance at every dose (the NCA misses
# early exposure), and must still fall with dose.
stopifnot(all(sim_nca_wide$cl.obs > published$cl.obs))
stopifnot(all(diff(sim_nca_wide$cl.obs[
match(published$treatment, sim_nca_wide$treatment)]) < 0))Monkey and human (Figures 3 and 4)
mw <- 4186.6 # g/mol, Gao 2012 Methods
wt_monkey <- 4.3 # kg
wt_human <- 88.5 # kg
# ug/kg -> pmol: (ug/kg * kg) / (g/mol) * 1e6
ug_kg_to_pmol <- function(ug_per_kg, wt) ug_per_kg * wt / mw * 1e6
monkey_doses <- c(1, 3, 10) # ug/kg SC; 3 ug/kg also given IV
monkey_ka <- c(0.0244, 0.0205, 0.0142) # Table 3 range; middle value from Fig. 7A
t_mh <- sort(unique(seq(0, 1440, by = 2)))
monkey_ev <- dplyr::bind_rows(lapply(seq_along(monkey_doses), function(i) {
make_arm(30L + i, ug_kg_to_pmol(monkey_doses[i], wt_monkey), "depot", t_mh,
label = paste0(monkey_doses[i], " ug/kg SC"),
extra = list(WT = wt_monkey))
}))
monkey_ev <- dplyr::bind_rows(
monkey_ev,
make_arm(40L, ug_kg_to_pmol(3, wt_monkey), "central", t_mh,
label = "3 ug/kg IV", extra = list(WT = wt_monkey))
)
sim_monkey <- dplyr::bind_rows(lapply(seq_along(monkey_doses), function(i) {
rxode2::rxSolve(
mod_monkey, events = dplyr::filter(monkey_ev, .data$id == 30L + i),
params = c(lka = log(monkey_ka[i])), keep = "treatment", useLinCmt = FALSE
) |> as.data.frame()
}))
sim_monkey <- dplyr::bind_rows(
sim_monkey,
rxode2::rxSolve(mod_monkey, events = dplyr::filter(monkey_ev, .data$id == 40L),
keep = "treatment", useLinCmt = FALSE) |> as.data.frame()
)
sim_monkey |>
plot_positive() |>
ggplot(aes(time, Cc, colour = treatment)) +
geom_line(linewidth = 0.7) +
scale_y_log10() +
scale_x_continuous(limits = c(0, 720)) +
labs(x = "Time (min)", y = "Exendin-4 (pmol/L)", colour = NULL,
title = "Monkey: single IV and SC doses",
caption = "Replicates Figure 3 of Gao 2012.")
#> Warning: Removed 1440 rows containing missing values or values outside the scale range
#> (`geom_line()`).
human_sc <- c(0.02, 0.05, 0.1, 0.2, 0.3, 0.4) # ug/kg, studies A and B
human_inf <- 0.066 # pmol/kg/min, study C, 360 min
human_ev <- dplyr::bind_rows(lapply(seq_along(human_sc), function(i) {
make_arm(50L + i, ug_kg_to_pmol(human_sc[i], wt_human), "depot",
sort(unique(seq(0, 1440, by = 2))),
label = paste0(human_sc[i], " ug/kg SC"), extra = list(WT = wt_human))
}))
sim_human_sc <- rxode2::rxSolve(
mod_human, events = human_ev, keep = "treatment", useLinCmt = FALSE
) |> as.data.frame()
#> Warning: multi-subject simulation without without 'omega'
human_inf_rate <- human_inf * wt_human # pmol/min
human_inf_ev <- make_arm(
60L, dose = human_inf_rate * 360, dose_cmt = "central",
times = sort(unique(seq(0, 600, by = 2))), rate = human_inf_rate,
label = "0.066 pmol/kg/min IV, 360 min", extra = list(WT = wt_human)
)
sim_human_inf <- rxode2::rxSolve(
mod_human, events = human_inf_ev, keep = "treatment", useLinCmt = FALSE
) |> as.data.frame()
dplyr::bind_rows(sim_human_sc, sim_human_inf) |>
plot_positive() |>
ggplot(aes(time, Cc, colour = treatment)) +
geom_line(linewidth = 0.7) +
scale_y_log10() +
scale_x_continuous(limits = c(0, 720)) +
labs(x = "Time (min)", y = "Exendin-4 (pmol/L)", colour = NULL,
title = "Human: subcutaneous doses and a 360-min intravenous infusion",
caption = "Replicates Figure 4 of Gao 2012.")
#> Warning: Removed 2160 rows containing missing values or values outside the scale range
#> (`geom_line()`).
# Dose proportionality is BROKEN by target binding in both species: doubling
# the dose more than doubles exposure once the receptor pool saturates. Check
# the direction only -- these are the published structures, not fitted targets.
sc_peak <- sim_human_sc |>
dplyr::group_by(.data$treatment) |>
dplyr::summarise(cmax = max(.data$Cc), .groups = "drop") |>
dplyr::mutate(dose = human_sc[match(
.data$treatment, paste0(human_sc, " ug/kg SC"))]) |>
dplyr::arrange(.data$dose)
stopifnot(all(diff(sc_peak$cmax) > 0))
stopifnot(sc_peak$cmax[nrow(sc_peak)] / sc_peak$cmax[1] >
max(sc_peak$dose) / min(sc_peak$dose))Allometry (Figure 7)
Figure 7 panels B, C and D print fitted simple-allometry equations
that the running text describes only qualitatively (“the allometric
exponents for CLc and koff are close to 0.75, and that for Vc is close
to 1”). Re-fitting the same regression from the packaged
ini() values at the published body weights is a
cross-species consistency check on all three model files at once.
bw <- c(rat = 0.36, monkey = wt_monkey, human = wt_human)
allom_obs <- tibble::tibble(
species = names(bw),
BW = as.numeric(bw),
CLc = c(exp(p_rat[["lkel"]]) * exp(p_rat[["lvc"]]) * 1000,
exp(p_monkey[["lkel"]]) * exp(p_monkey[["lvc"]]) * 1000 * bw[["monkey"]],
exp(p_human[["lkel"]]) * exp(p_human[["lvc"]]) * 1000 * bw[["human"]]),
Vc = c(exp(p_rat[["lvc"]]) * 1000,
exp(p_monkey[["lvc"]]) * 1000 * bw[["monkey"]],
exp(p_human[["lvc"]]) * 1000 * bw[["human"]]),
koff = c(exp(p_rat[["lkoff"]]), exp(p_monkey[["lkoff"]]), exp(p_human[["lkoff"]]))
)
fit_allom <- function(y, x) {
cf <- stats::coef(stats::lm(log(y) ~ log(x)))
c(coefficient = unname(exp(cf[[1]])), exponent = unname(cf[[2]]))
}
allom_tbl <- dplyr::bind_rows(
c(parameter = "CLc (mL/min)", fit_allom(allom_obs$CLc, allom_obs$BW)),
c(parameter = "Vc (mL)", fit_allom(allom_obs$Vc, allom_obs$BW)),
c(parameter = "koff (1/min)", fit_allom(allom_obs$koff, allom_obs$BW))
) |>
dplyr::mutate(
dplyr::across(c("coefficient", "exponent"), as.numeric),
printed_coefficient = c(5.83, 101.6, 0.022),
printed_exponent = c(0.655, 1.060, 0.663)
)
allom_tbl |>
dplyr::rename(
"Parameter" = parameter,
"Refit coefficient" = coefficient,
"Refit exponent" = exponent,
"Fig. 7 coefficient" = printed_coefficient,
"Fig. 7 exponent" = printed_exponent
) |>
knitr::kable(digits = c(0, 3, 3, 3, 3),
caption = "Simple allometry refit from the packaged parameters, against the equations printed inside Figure 7 panels B, C and D.")| Parameter | Refit coefficient | Refit exponent | Fig. 7 coefficient | Fig. 7 exponent |
|---|---|---|---|---|
| CLc (mL/min) | 5.762 | 0.654 | 5.830 | 0.655 |
| Vc (mL) | 98.658 | 0.992 | 101.600 | 1.060 |
| koff (1/min) | 0.022 | 0.666 | 0.022 | 0.663 |
# CLc and koff reproduce the printed equations essentially exactly; assert on
# both the coefficient and the exponent for those two.
clc <- allom_tbl[allom_tbl$parameter == "CLc (mL/min)", ]
koff <- allom_tbl[allom_tbl$parameter == "koff (1/min)", ]
stopifnot(
abs(clc$exponent - clc$printed_exponent) < 0.05,
abs(koff$exponent - koff$printed_exponent) < 0.05,
abs(clc$coefficient / clc$printed_coefficient - 1) < 0.10,
abs(koff$coefficient / koff$printed_coefficient - 1) < 0.10
)
# Vc does NOT reproduce as tightly; see Assumptions and deviations. Keep a
# loose gate so a gross transcription error would still be caught.
vc <- allom_tbl[allom_tbl$parameter == "Vc (mL)", ]
stopifnot(abs(vc$exponent - vc$printed_exponent) < 0.15,
abs(vc$coefficient / vc$printed_coefficient - 1) < 0.25)CLc refits to 5.76 * BW^0.659 against the
printed 5.83 * BW^0.655, and koff to
0.0220 * BW^0.666 against the printed
0.022 * BW^0.663 – both to within a percent on the
coefficient and 0.005 on the exponent. Vc refits to an
exponent near 0.99 against the printed 1.060; see Assumptions and
deviations.
Part 2 – the glucose-insulin pharmacodynamic model
The pharmacodynamic study infused exendin-4 intravenously for two hours and injected D-glucose at 5.7 mmol/kg 30 min in. Because no drug concentrations were measured, the model’s own pharmacokinetic layer supplies them.
bw_pd <- 0.36 # kg; the PK study animals whose Table 2 parameters are reused
pd_arms <- tibble::tribble(
~arm, ~rate_pmol_kg_min, ~FPG, ~INS_BL,
"saline", 0, 9.60, 304,
"3 pmol/kg/min", 3, 11.53, 399,
"30 pmol/kg/min", 30, 10.42, 330,
"300 pmol/kg/min", 300, 9.92, 287,
"3000 pmol/kg/min", 3000, 9.76, 329
)
pd_arms$arm <- factor(pd_arms$arm, levels = pd_arms$arm)
t_pd <- sort(unique(c(seq(0, 120, by = 0.25), 30, 32, 33, 35)))
make_pd_arm <- function(i) {
a <- pd_arms[i, ]
rate <- a$rate_pmol_kg_min * bw_pd / 1000 # pmol/kg/min -> nmol/min
rows <- list(
# D-glucose challenge: 5.7 mmol/kg into the per-kilogram `glucose` state.
data.frame(id = i, time = 30, amt = 5.7, rate = 0, evid = 1L,
cmt = "glucose", dvid = NA_integer_),
# Observations sit on an ODE STATE; `dvid = 1` selects the first endpoint.
# rxode2 still returns Gc and Ic as columns on these rows.
data.frame(id = i, time = t_pd, amt = NA_real_, rate = NA_real_,
evid = 0L, cmt = "central", dvid = 1L)
)
if (rate > 0) {
rows <- c(list(data.frame(id = i, time = 0, amt = rate * 120, rate = rate,
evid = 1L, cmt = "central", dvid = NA_integer_)),
rows)
}
out <- dplyr::bind_rows(rows)
out$arm <- as.character(a$arm)
out$FPG <- a$FPG
out$INS_BL <- a$INS_BL
dplyr::arrange(out, .data$time, dplyr::desc(.data$evid))
}
pd_ev <- dplyr::bind_rows(lapply(seq_len(nrow(pd_arms)), make_pd_arm))
stopifnot(!anyDuplicated(unique(pd_ev[, c("id", "time", "evid")])))
sim_pd <- rxode2::rxSolve(
mod_pd, events = pd_ev, keep = "arm", useLinCmt = FALSE
) |>
as.data.frame() |>
dplyr::mutate(arm = factor(.data$arm, levels = levels(pd_arms$arm)))
#> Warning: multi-subject simulation without without 'omega'Simulated drug concentrations (Figure 6, top)
sim_pd |>
dplyr::filter(.data$arm != "saline") |>
plot_positive() |>
ggplot(aes(time, Cc * 1000, colour = arm)) +
geom_line(linewidth = 0.7) +
scale_y_log10() +
labs(x = "Time (min)", y = "Exendin-4 (pmol/L)", colour = NULL,
title = "Exendin-4 during the pharmacodynamic-study infusions",
caption = "Replicates Figure 6 (top) of Gao 2012.")
plateau <- sim_pd |>
dplyr::filter(.data$arm != "saline") |>
dplyr::group_by(.data$arm) |>
dplyr::summarise(
c30_nM = .data$Cc[which.min(abs(.data$time - 30))],
c120_nM = .data$Cc[which.min(abs(.data$time - 120))],
.groups = "drop"
) |>
dplyr::mutate(
plateau_pM = 1000 * .data$c120_nM,
pct_of_ss = 100 * .data$c30_nM / .data$c120_nM,
# Read off the Figure 6 (top) plateaus by pixel-calibrating the log axis.
fig6_pM = c(1.4e2, 1.7e3, 2.4e4, 2.7e5)
)
plateau |>
dplyr::select(arm, plateau_pM, fig6_pM, pct_of_ss) |>
dplyr::rename(
"Infusion" = arm,
"Plateau at 120 min (pmol/L)" = plateau_pM,
"Figure 6 plateau (pmol/L)" = fig6_pM,
"% of plateau reached by 30 min" = pct_of_ss
) |>
knitr::kable(digits = c(0, 0, 0, 1),
caption = "Simulated exendin-4 plateaus against the digitised Figure 6 (top) values.")| Infusion | Plateau at 120 min (pmol/L) | Figure 6 plateau (pmol/L) | % of plateau reached by 30 min |
|---|---|---|---|
| 3 pmol/kg/min | 140 | 140 | 90.7 |
| 30 pmol/kg/min | 1562 | 1700 | 87.8 |
| 300 pmol/kg/min | 23563 | 24000 | 81.2 |
| 3000 pmol/kg/min | 276164 | 270000 | 81.2 |
# Gao 2012 Results, "Pharmacodynamics": "Exendin-4 almost (>80%) reached
# steady state after 30 min of infusion, at which time glucose was injected."
# That is a published claim, checkable directly.
stopifnot(all(plateau$pct_of_ss > 80))
# Plateaus against the digitised figure. The Figure 6 values are read off a
# four-decade log axis, so a 25% band is the resolution of the reading, not
# a slack gate: a 10-fold unit error in the pmol/kg/min -> nmol/min
# conversion would blow it by an order of magnitude.
stopifnot(all(abs(plateau$plateau_pM / plateau$fig6_pM - 1) < 0.25))Glucose and insulin (Figure 5)
sim_pd |>
dplyr::select(time, arm, Gc, Ic) |>
dplyr::mutate(Ic = .data$Ic * 1000) |> # nmol/L -> pmol/L, as plotted
tidyr::pivot_longer(c("Gc", "Ic"), names_to = "output", values_to = "value") |>
dplyr::mutate(output = dplyr::recode(.data$output,
Gc = "Glucose (mmol/L)",
Ic = "Insulin (pmol/L)")) |>
ggplot(aes(time, value, colour = arm)) +
geom_line(linewidth = 0.7) +
facet_wrap(~output, scales = "free_y") +
labs(x = "Time (min)", y = NULL, colour = NULL,
title = "Glucose and insulin after the glucose challenge at 30 min",
caption = "Replicates Figure 5 of Gao 2012.")
The insulin panel is the point of the whole paper: the peak insulin
response rises from saline through 30 pmol/kg/min and then falls
again at 3000 pmol/kg/min. That non-monotone dose-response is
produced entirely by the quadratic term of the Adair function, and it is
the reason kadair2 exists.
pd_peaks <- sim_pd |>
dplyr::group_by(.data$arm) |>
dplyr::summarise(
g_base = .data$Gc[which.min(abs(.data$time - 29))],
g_first = .data$Gc[which.min(abs(.data$time - 33))],
i_base_pM = 1000 * .data$Ic[which.min(abs(.data$time - 29))],
i_peak_pM = 1000 * max(.data$Ic),
.groups = "drop"
) |>
dplyr::mutate(
# Peak of the FITTED line in each Figure 5 right-hand panel, digitised.
fig5_peak_pM = c(750, 1230, 2000, 1950, 1180)
)
pd_peaks |>
dplyr::select(arm, i_base_pM, i_peak_pM, fig5_peak_pM, g_base, g_first) |>
dplyr::rename(
"Arm" = arm,
"Baseline insulin (pmol/L)" = i_base_pM,
"Peak insulin, model (pmol/L)" = i_peak_pM,
"Peak insulin, Fig. 5 (pmol/L)" = fig5_peak_pM,
"Baseline glucose (mmol/L)" = g_base,
"Glucose at 33 min (mmol/L)" = g_first
) |>
knitr::kable(digits = c(0, 0, 0, 0, 2, 1),
caption = "Simulated glucose and insulin against the digitised Figure 5 fitted curves.")| Arm | Baseline insulin (pmol/L) | Peak insulin, model (pmol/L) | Peak insulin, Fig. 5 (pmol/L) | Baseline glucose (mmol/L) | Glucose at 33 min (mmol/L) |
|---|---|---|---|---|---|
| saline | 304 | 744 | 750 | 9.60 | 33.3 |
| 3 pmol/kg/min | 399 | 1328 | 1230 | 11.53 | 35.0 |
| 30 pmol/kg/min | 330 | 2131 | 2000 | 10.42 | 33.5 |
| 300 pmol/kg/min | 287 | 2092 | 1950 | 9.92 | 33.0 |
| 3000 pmol/kg/min | 329 | 1287 | 1180 | 9.76 | 33.2 |
# The bell shape: the peak insulin response is maximal at one of the two
# middle arms, NOT at the top dose. This is the paper's central PD finding
# and it is fully deterministic here.
peak_arm <- as.character(pd_peaks$arm[which.max(pd_peaks$i_peak_pM)])
stopifnot(peak_arm %in% c("30 pmol/kg/min", "300 pmol/kg/min"))
stopifnot(
pd_peaks$i_peak_pM[pd_peaks$arm == "3000 pmol/kg/min"] <
pd_peaks$i_peak_pM[pd_peaks$arm == "30 pmol/kg/min"],
pd_peaks$i_peak_pM[pd_peaks$arm == "saline"] <
pd_peaks$i_peak_pM[pd_peaks$arm == "3 pmol/kg/min"]
)
# Magnitudes against the digitised Figure 5 fitted peaks. Observed spread of
# the ratio is +8% to +9% across all five arms; the 25% band leaves room for
# the digitisation while still catching a real magnitude error.
stopifnot(all(abs(pd_peaks$i_peak_pM / pd_peaks$fig5_peak_pM - 1) < 0.25))
# Baselines must come straight back out of the covariates, unchanged.
stopifnot(
all(abs(pd_peaks$g_base - pd_arms$FPG) < 1e-6),
all(abs(pd_peaks$i_base_pM - pd_arms$INS_BL) < 1e-6)
)The Adair function and the optimal infusion rate (Figure 8)
Sd = Smax * C / (k1 + C + k2 * C^2) peaks where the
denominator’s concentration-normalised form is minimised, i.e. at
C = sqrt(k1/k2). The paper reports, from the Figure 8
simulations, that “maximal insulin secretion with this experimental
design would be reached at an infusion rate of 120 pmol/kg/min”.
Sweeping the infusion rate through the packaged model tests the
pharmacokinetic and pharmacodynamic parameter sets jointly, because the
answer depends on where the pharmacokinetics puts the plateau
concentration relative to where the pharmacodynamics wants
it.
p_pd <- ini_of(mod_pd)
k1 <- exp(p_pd[["lkadair1"]])
k2 <- exp(p_pd[["lkadair2"]])
c_peak_analytic <- sqrt(k1 / k2)
conc_grid <- 10^seq(-2, 3, length.out = 400)
tibble::tibble(
C = conc_grid,
Sd = exp(p_pd[["lsmax"]]) * conc_grid / (k1 + conc_grid + k2 * conc_grid^2)
) |>
ggplot(aes(C, Sd)) +
geom_line(linewidth = 0.7) +
geom_vline(xintercept = c_peak_analytic, linetype = "dashed") +
scale_x_log10() +
labs(x = "Free exendin-4 (nmol/L)", y = "Sd",
title = "Adair biphasic drug-effect function (Gao 2012 eq. 7)",
caption = paste0("Dashed line: sqrt(k1/k2) = ",
round(c_peak_analytic, 2), " nmol/L."))
# Log grid over the studied range plus a fine grid where the peak sits, so
# the argmax is resolved to 5 pmol/kg/min.
rate_grid <- sort(unique(c(
round(10^seq(0, log10(3000), length.out = 30), 2),
seq(60, 240, by = 5)
)))
make_sweep_arm <- function(i) {
rate <- rate_grid[i] * bw_pd / 1000
dplyr::bind_rows(
data.frame(id = i, time = 0, amt = rate * 120, rate = rate, evid = 1L,
cmt = "central", dvid = NA_integer_),
data.frame(id = i, time = 30, amt = 5.7, rate = 0, evid = 1L,
cmt = "glucose", dvid = NA_integer_),
data.frame(id = i, time = seq(0, 120, by = 0.5), amt = NA_real_,
rate = NA_real_, evid = 0L, cmt = "central", dvid = 1L)
) |>
dplyr::mutate(FPG = 9.60, INS_BL = 304, rate_grp = rate_grid[i]) |>
dplyr::arrange(.data$time, dplyr::desc(.data$evid))
}
sweep_ev <- dplyr::bind_rows(lapply(seq_along(rate_grid), make_sweep_arm))
sweep <- rxode2::rxSolve(
mod_pd, events = sweep_ev, keep = "rate_grp", useLinCmt = FALSE
) |>
as.data.frame() |>
dplyr::group_by(.data$rate_grp) |>
dplyr::summarise(
peak_insulin_pM = 1000 * max(.data$Ic),
c30_nM = .data$Cc[which.min(abs(.data$time - 30))],
.groups = "drop"
)
#> Warning: multi-subject simulation without without 'omega'
best <- sweep[which.max(sweep$peak_insulin_pM), ]
ggplot(sweep, aes(rate_grp, peak_insulin_pM)) +
geom_line(linewidth = 0.7) +
geom_vline(xintercept = 120, linetype = "dashed") +
scale_x_log10() +
labs(x = "Exendin-4 infusion rate (pmol/kg/min)",
y = "Peak insulin after the glucose challenge (pmol/L)",
title = "Peak insulin response versus infusion rate",
caption = "Replicates Figure 8 of Gao 2012. Dashed line: the paper's stated optimum of 120 pmol/kg/min.")
knitr::kable(
tibble::tibble(
Quantity = c("Infusion rate maximising peak insulin (pmol/kg/min)",
"Exendin-4 at 30 min at that rate (nmol/L)",
"Adair peak sqrt(k1/k2) (nmol/L)"),
Model = c(best$rate_grp, best$c30_nM, c_peak_analytic),
Published = c(120, NA, NA)
),
digits = 2,
caption = "The paper's stated optimal infusion rate, recovered from the packaged model."
)| Quantity | Model | Published |
|---|---|---|
| Infusion rate maximising peak insulin (pmol/kg/min) | 130.00 | 120 |
| Exendin-4 at 30 min at that rate (nmol/L) | 7.20 | NA |
| Adair peak sqrt(k1/k2) (nmol/L) | 7.35 | NA |
# Gao 2012 Discussion: maximal insulin secretion at 120 pmol/kg/min. The
# model puts it at 130 on this grid. Band chosen to accept the grid
# resolution and the digitised baselines while still failing on a real error.
stopifnot(best$rate_grp > 90, best$rate_grp < 170)
# ...and it lands there for the right reason: the concentration at the time of
# the glucose challenge sits on the Adair peak.
stopifnot(abs(best$c30_nM / c_peak_analytic - 1) < 0.15)Recovering 120 pmol/kg/min from an independent sweep – and confirming
that the concentration at the moment of the glucose challenge lands on
sqrt(k1/k2) – ties the Table 2 pharmacokinetic parameters
and the Table 4 pharmacodynamic parameters together in a single number
that appears nowhere in either table.
Assumptions and deviations
No between-subject variability anywhere. All four fits are naive-pooled fits to mean profiles in ADAPT II, so there is no OMEGA block to transcribe and none is invented.
Residual error is unreported. Methods gives the variance model
Vi = (sigma1 + sigma2 * Y)^2but reports neithersigma1norsigma2, for any output. EveryaddSd/propSdis thereforefixed(0)rather than guessed. Users who want realistic residual noise must supply their own.kais per dose group, with no published functional form. Table 2 gives three rat values that decrease with dose (0.00820, 0.00579, 0.00273 1/min) and Table 3 gives only the range endpoints for monkeys (0.0244-0.0142) and humans (0.00550-0.0148). Each model file carries the lowest-dose value; this vignette overrideslkaper arm. The monkey middle value (0.0205 1/min at 3 ug/kg) is digitised from Figure 7A, not printed in any table.GbandIbare digitised, not published. Methods states only that “the baseline conditions Gb and Ib were fixed as the measured predose values”. The per-armFPGandINS_BLvalues used above were read off the pre-challenge plateau of Figure 5 by pixel-calibrating the axis labels: glucose 9.60 / 11.53 / 10.42 / 9.92 / 9.76 mmol/L and insulin 304 / 399 / 330 / 287 / 329 pmol/L for saline / 3 / 30 / 300 / 3000 pmol/kg/min. The digitisation is cross-checked by the paper’s own initial condition,Glu(0) = Dose/VG + Gb =5.7/0.208 + 9.60 = 37.0 mmol/L, which matches the plotted post-challenge peak of the saline panel. Similarly, the Figure 5 insulin peaks and the Figure 6 plateaus used as comparison targets above are digitised.The glucose-elevation term is rectified at zero. Equation 6 as printed carries the bare difference
(Glu - Gb), but the Discussion states that “when glucose is not higher than basal values, the effect is shut off”. Without the rectification, the drug term would turn negative during the post-challenge undershoot and exendin-4 would spuriously suppress insulin secretion. The model encodes the Discussion’s reading.Equation 8 is deliberately not extracted. The paper also tested the drug-receptor complex
RCas the pharmacodynamic driving function, and rejected it on AIC, on the imprecision of its estimates, and on prior Goto-Kakizaki rat evidence. Per the standing convention that a rejected alternative’s constants are omitted rather than carried asfixed(0), only equation 7 is implemented.Body weight for the pharmacodynamic arms. The pharmacodynamic cohort spans 80-420 g while the pharmacokinetic parameters it borrows come from 350-370 g animals. The paper does not state which weight it used to convert its pmol/kg/min infusion rates. This vignette uses 0.36 kg, the midpoint of the pharmacokinetic study; the resulting plateaus land within 20% of the Figure 6 values across four decades of concentration, which supports the choice.
Vcallometry does not reproduce as tightly asCLcandkoff. Refitting simple allometry from the packaged parameters at the published body weights returns5.76 * BW^0.659forCLc(printed:5.83 * BW^0.655) and0.0220 * BW^0.666forkoff(printed:0.022 * BW^0.663), but an exponent near 0.99 forVcagainst the printed 1.060. The most likely cause is the body weights the Figure 7 regression itself used, which are not stated: the humanVcpoint plotted in Figure 7C reads at roughly 1.3e4 mL against the 9.8e3 mL implied by Table 3’s 111 mL/kg at the reported 88.5 kg. Nothing in the model files was adjusted to close the gap.The bolus-arm NCA disagrees with Table 1, and that is expected. Table 1’s
CLandVssare non-compartmental values from observed data first sampled at 5 min, so they miss the rapid early target-mediated decline. The model’s clearance is correspondingly 14-35% higher. The paper’s own “approximately 73% of total clearance at the lowest dose” statement reproduces only against the Table 1 number, confirming the reading. The infusion arm, where the comparison is like-for-like, agrees to within 17%.Monkey and human binding parameters are barely identified. Table 3 reports CV% of 179-2990% on
kon,koffandkint. The monkeyKDof 0.12 pM – five thousand-fold below the rat value – should not be read as a real affinity. Both model files record this prominently in theirdescription.New canonical parameter names.
lkadair1/lkadair2(the Adair biphasic constants) and thelsstim_<driver>_<target>family (linear inter-pool stimulation factors) were ratified for this extraction and are registered ininst/references/parameter-names.mdwith this paper as the founding example.