Artemether + lumefantrine in pregnancy (Lohy Das 2018)
Source:vignettes/articles/LohyDas_2018_artemether_lumefantrine.Rmd
LohyDas_2018_artemether_lumefantrine.RmdModel and source
- Citation: Lohy Das J, Rulisa S, de Vries PJ, Mens PF, Kaligirwa N, Agaba S, Tarning J, Karlsson MO, Dorlo TPC (2018). Population pharmacokinetics of artemether, dihydroartemisinin, and lumefantrine in Rwandese pregnant women treated for uncomplicated Plasmodium falciparum malaria. Antimicrobial Agents and Chemotherapy 62(10):e00518-18.
- Article: https://doi.org/10.1128/AAC.00518-18 (PMC6153812, open access)
- Ethics: Rwanda National Ethics Committee study IRB 00001497
Lohy Das 2018 reports two separate population PK models from a single cohort of 22 Rwandese pregnant women receiving the standard fixed-dose oral artemether-lumefantrine combination:
- a lumefantrine model: 5-transit-compartment absorption followed by two-compartment disposition (Fig. 1A, Table 2), and
- a joint artemether + dihydroartemisinin model: 2-transit-compartment absorption, one-compartment disposition for each analyte, and an enzyme turnover compartment producing autoinduction of artemether clearance (Fig. 1B, Table 2, Eqs. 1 and 4-6).
The authors explicitly attempted and then abandoned a simultaneous fit of the two drugs – “adding off-diagonal correlation elements to explore MTT correlations resulted in an unstable model” and simultaneous modelling “substantially increased the computational time (>18-fold increase)”. Per the library’s policy of replicating the authors’ model structure, the two independent fits are therefore extracted as two model files, with this single vignette walking the paper as a unit.
mod_lf <- rxode2::rxode2(readModelDb("LohyDas_2018_lumefantrine")())
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_fdepot_1, etaiov_fdepot_2, etaiov_fdepot_3, etaiov_fdepot_4, etaiov_fdepot_5, etaiov_fdepot_6, etaiov_mtt_1, etaiov_mtt_2, etaiov_mtt_3, etaiov_mtt_4, etaiov_mtt_5, etaiov_mtt_6
#> as a work-around try putting the mu-referenced expression on a simple line
mod_am <- rxode2::rxode2(readModelDb("LohyDas_2018_artemether")())
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_fdepot_1, etaiov_fdepot_2, etaiov_fdepot_3, etaiov_fdepot_4, etaiov_fdepot_5, etaiov_fdepot_6, etaiov_mtt_1, etaiov_mtt_2, etaiov_mtt_3, etaiov_mtt_4, etaiov_mtt_5, etaiov_mtt_6
#> as a work-around try putting the mu-referenced expression on a simple linePopulation
Twenty-two Rwandese pregnant women with microscopy-confirmed uncomplicated P. falciparum malaria were enrolled at Rwamagana district hospital between June 2007 and July 2009 (Table 1): 11 in the second trimester (estimated gestational age 15.7-27.6 weeks) and 11 in the third (28.3-39.0 weeks), median EGA 27.9 weeks. Median age was 26 years (range 18-39), median body weight 59.0 kg (range 40.0-65.0), median body mass index 21.9 kg/m^2 (range 15.6-25.4), and median baseline parasitemia 24,970 parasites/uL (range 3,060-160,000).
All patients received 4 tablets (80 mg artemether / 480 mg lumefantrine) twice daily for 3 days at 0, 8, 24, 36, 48 and 60 h, supervised, with a glass of milk and/or a small cake. Venous plasma was sampled pre-dose, at 2 and 4 h after each dose, before doses 2-6, at 0.25 h after doses 1 and 2, at 6, 8 and 12 h after the last dose, and on day 7. Every patient cleared microscopic parasitemia by day 3 and a single recrudescence (4.55%) was observed.
Estimation used NONMEM 7.3 on natural-log molar concentrations. Lumefantrine used FOCEI with the <8% below-limit-of-quantification observations discarded; artemether and dihydroartemisinin used Laplacian estimation with the M3 method for the 24% and 28% censored observations respectively. Because the lumefantrine data were sparse in the elimination phase, frequentist informative priors from an earlier Ugandan lumefantrine study were applied, which is what allowed the peripheral compartment to be identified. The values encoded here are the paper’s final Table 2 estimates, not the priors.
Units: the models are molar
Both model files carry doses in nmol and concentrations
in nmol/L. This is not a stylistic choice – the paper
states that “the molar units of LF, ARM, and DHA concentration were
transformed to their natural logarithms for this modeling analysis”, and
the artemether autoinduction EC50 is reported in nM and is
compared directly against the artemether concentration. A mass-unit
encoding would silently mis-scale the induction term.
mw <- c(lumefantrine = 528.94, artemether = 298.37, dihydroartemisinin = 284.35)
dose_nmol <- c(
lumefantrine = 480 / mw[["lumefantrine"]] * 1e6,
artemether = 80 / mw[["artemether"]] * 1e6
)
round(dose_nmol)
#> lumefantrine artemether
#> 907475 268123The paper’s own reporting confirms the lumefantrine conversion: it gives a day-7 target of “280 ng/ml (529 nM)” and a simulated median of “709 ng/ml (corresponding to 1,340 [509 to 3,670] nM)”.
Source trace
Every ini() value in both model files, with its location
in the source.
| Parameter | Value | Source |
|---|---|---|
| lfdepot | F = 1 (fixed) | Table 2, Lumefantrine block, row ‘F’ = ‘1 fixed’; Methods ‘Population pharmacokinetics’ |
| lmtt | MTT = 4.04 h | Table 2, row ‘MTT (h)’ (%RSE 5.16, 90% CI 3.71-4.41) |
| lcl | CL/F = 4.49 L/h | Table 2, row ‘CL/F (liters/h)’ (%RSE 6.59, 90% CI 4.18-5.17) |
| lvc | Vc/F = 139 L | Table 2, row ‘Vc/F (liters)’ (%RSE 6.77, 90% CI 119-149) |
| lq | Q/F = 0.924 L/h | Table 2, row ‘Q/F (liters/h)’ (%RSE 13.3, 90% CI 0.770-1.21) |
| lvp | Vp/F = 111 L | Table 2, row ‘Vp/F (liters)’ (%RSE 8.69, 90% CI 96.5-129) |
| boxcox_lfdepot | lambda = -0.605 | Table 2, row ‘Box-Cox shape parameter for BSV on F’ (%RSE 34.9); Methods Eq. 3 |
| e_wt_cl / e_wt_vc | 0.75 / 1.0 (fixed) | Methods ‘For body size descriptors…’: allometric power exponents 0.75 for clearances and 1 for volumes, scaled to median total body weight 59 kg |
| etalmtt | BSV MTT 132% CV -> var 1.7424 | Table 2 BSV column (%RSE 37.9); footnote a scale |
| etalvc | BSV Vc 48.7% CV -> var 0.2372 | Table 2 BSV column (%RSE 56.8); footnote a scale |
| etaiov_fdepot_* | BOV F 144% CV -> var 2.0736 | Table 2 BOV column, footnote b (%RSE 19.7) |
| etaiov_mtt_* | BOV MTT 46.0% CV -> var 0.2116 | Table 2 BOV column, footnote b (%RSE 43.6) |
| propSd | RUV 48.7% -> 0.487 | Table 2, row ‘RUV (%)’ (%RSE 4.82); Methods: additive on the log scale |
| Parameter | Value | Source |
|---|---|---|
| lfdepot | F = 1 (fixed) | Table 2, Artemether block, row ‘F’ = ‘1 fixed’ |
| lmtt | MTT = 0.738 h | Table 2, row ‘MTT (h)’ (%RSE 12.5); Discussion ‘45 min versus 4 h’ |
| lcl | CL_ARM/F = 467 L/h (pre-induced) | Table 2, row ‘CL_ARM/F’ (%RSE 17.9); Abstract ‘started at 467 liters/h’ |
| lvc | V_ARM/F = 3,000 L | Table 2, row ‘V_ARM/F (liters)’ (%RSE 14.1) |
| lcl_dihydroart | CL_DHA/F = 611 L/h | Table 2, Dihydroartemisinin block (%RSE 15.4) |
| lvc_dihydroart | V_DHA/F = 137 L | Table 2, Dihydroartemisinin block (%RSE 38.9) |
| lemax | Emax = 0.986 | Table 2, row ‘Emax’ (%RSE 22.8); Eq. 1 |
| lec50 | EC50 = 9.37 nM | Table 2, row ‘EC50 (nM)’ (%RSE 25.4); Results ‘the EC50 was 9.37 nM’ |
| lkenz | K_ENZ = ln(2)/30.4 h = 0.02280 /h | Table 2, row ‘TIME_ENZ (h)’ = 30.4 (%RSE 42.1) combined with Eq. 6 |
| e_wt_cl / e_wt_vc | 0.75 / 1.0 (fixed) | Methods: allometric exponents on CL_ARM/F, CL_DHA/F and V_ARM/F, V_DHA/F; median 59 kg |
| etalfdepot | BSV F 57.6% CV -> var 0.3318 | Table 2 BSV column (%RSE 36.8) |
| etalmtt | BSV MTT 110% CV -> var 1.2100 | Table 2 BSV column (%RSE 32.5) |
| etalcl / etalvc | BSV 27.9% / 20.5% CV -> var 0.0778 / 0.0420 | Table 2 BSV column (%RSE 44.1 / 43.1) |
| etalcl_dihydroart / etalvc_dihydroart | BSV 20.7% / 40.5% CV -> var 0.0428 / 0.1640 | Table 2 BSV column (%RSE 50.2 / 48.9) |
| etaiov_fdepot_* / etaiov_mtt_* | BOV 48.2% / 53.2% CV -> var 0.2323 / 0.2830 | Table 2 BOV column, footnote b |
| propSd / propSd_dihydroart | RUV 98.4% / 113% -> 0.984 / 1.13 | Table 2 ‘RUV (%)’ rows (%RSE 5.54 / 6.01) |
Model equations, all from the Materials and Methods section:
| Equation | Form | Used in |
|---|---|---|
| Fig. 1A legend | ktr = (n + 1) / MTT |
both models’ transit chains (n = 5 and n = 2) |
| Eq. 1 | EFF = Emax * CP / (EC50 + CP) |
artemether autoinduction |
| Eq. 3 | Box-Cox transform of the F random effect | lumefantrine phi_fdepot
|
| Eq. 4 | dA_ENZ/dt = K_ENZ * (1 + EFF) - K_ENZ * A_ENZ |
d/dt(enzyme) |
| Eq. 5 | (CL_ARM/F)_induced = (CL_ARM/F)_preinduced * A_ENZ |
cl_induced |
| Eq. 6 | K_ENZ = ln(2) / t_(1/2)ENZ |
lkenz |
Dosing and the occasion column
Both models use an OCC column (1-6) to index the dosing
occasion for the between-occasion random effects, and a WT
column for the allometric scaling.
dose_times_3day <- c(0, 8, 24, 36, 48, 60)
dose_times_5day <- c(dose_times_3day, 72, 84, 96, 108)
# OCC is capped at 6 because the paper estimated only six occasion slots.
occ_of <- function(tm, dose_times) pmin(6L, pmax(1L, findInterval(tm, dose_times)))
build_data <- function(drug, dose_times, obs_times, wt = 59, n = 1L) {
ev <- rxode2::et(amt = dose_nmol[[drug]], time = dose_times, cmt = "depot",
id = seq_len(n)) |>
rxode2::et(obs_times, cmt = "central", id = seq_len(n))
d <- as.data.frame(ev)
d <- d[order(d$id, d$time, -d$evid), ]
d$WT <- if (length(wt) == 1L) wt else wt[d$id]
d$OCC <- occ_of(d$time, dose_times)
d
}Lumefantrine: typical-value profile and structural checks
rxode2::zeroRe() gives the typical subject (all random
effects zero), which is where the paper’s own closed-form identities can
be checked exactly.
lf_typ <- rxode2::zeroRe(mod_lf)
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_fdepot_1, etaiov_fdepot_2, etaiov_fdepot_3, etaiov_fdepot_4, etaiov_fdepot_5, etaiov_fdepot_6, etaiov_mtt_1, etaiov_mtt_2, etaiov_mtt_3, etaiov_mtt_4, etaiov_mtt_5, etaiov_mtt_6
#> as a work-around try putting the mu-referenced expression on a simple line
obs_lf <- sort(unique(c(seq(0, 120, by = 0.25), seq(120, 1200, by = 1))))
sim_lf <- rxode2::rxSolve(lf_typ, build_data("lumefantrine", dose_times_3day, obs_lf),
returnType = "data.frame", addDosing = FALSE)
#> ℹ omega/sigma items treated as zero: 'etalmtt', 'etalvc', 'etaiov_fdepot_1', 'etaiov_fdepot_2', 'etaiov_fdepot_3', 'etaiov_fdepot_4', 'etaiov_fdepot_5', 'etaiov_fdepot_6', 'etaiov_mtt_1', 'etaiov_mtt_2', 'etaiov_mtt_3', 'etaiov_mtt_4', 'etaiov_mtt_5', 'etaiov_mtt_6'The two-compartment disposition has a closed-form terminal rate constant; the paper reports a “terminal elimination half-life of ~4 days”.
k10 <- 4.49 / 139; k12 <- 0.924 / 139; k21 <- 0.924 / 111
ksum <- k10 + k12 + k21
beta <- 0.5 * (ksum - sqrt(ksum^2 - 4 * k10 * k21))
t_half_closed <- log(2) / beta
tail_lf <- dplyr::filter(sim_lf, time > 700, time < 1200)
t_half_solved <- -log(2) / coef(lm(log(Cc) ~ time, data = tail_lf))[["time"]]
c(closed_form_h = t_half_closed, closed_form_days = t_half_closed / 24,
solved_h = t_half_solved)
#> closed_form_h closed_form_days solved_h
#> 104.814596 4.367275 104.814596
# The solve must reproduce its own closed form: this is pure numerical error,
# so a tight bound is correct here.
stopifnot(abs(t_half_solved / t_half_closed - 1) < 0.01)Dose recovery. With F fixed at 1 and no random effects,
CL * AUC(0-inf) must equal the total administered amount
exactly. This is the gate that would catch a mis-scaled dose, a dropped
transit compartment, or a bioavailability applied to the wrong
compartment.
trap <- function(tm, cc) sum(diff(tm) * (head(cc, -1) + tail(cc, -1)) / 2)
auc_lf_nM <- trap(sim_lf$time, sim_lf$Cc) + tail(sim_lf$Cc, 1) / beta
recovery <- 4.49 * auc_lf_nM / (length(dose_times_3day) * dose_nmol[["lumefantrine"]])
recovery
#> [1] 1.000004
stopifnot(abs(recovery - 1) < 0.005)Expressed in the paper’s mass units, that AUC is the published value:
auc_lf_mass <- auc_lf_nM * mw[["lumefantrine"]] * 1e-9 * 1e3 # nM*h -> mg*h/L
c(simulated = auc_lf_mass, published = 641)
#> simulated published
#> 641.4282 641.0000
stopifnot(abs(auc_lf_mass / 641 - 1) < 0.01)
sim_lf |>
dplyr::mutate(ng_mL = Cc * mw[["lumefantrine"]] / 1000) |>
dplyr::filter(time <= 400) |>
ggplot(aes(time, ng_mL)) +
geom_line(linewidth = 0.7) +
geom_hline(yintercept = 280, linetype = "dashed", colour = "firebrick") +
geom_vline(xintercept = 168, linetype = "dotted") +
scale_y_log10() +
labs(x = "Time since first dose (h)", y = "Lumefantrine (ng/mL)") +
theme_bw()
#> Warning in scale_y_log10(): log-10 transformation introduced infinite values.
Typical-subject lumefantrine plasma profile after the standard 3-day regimen. The dashed line is the 280 ng/mL day-7 target; the dotted line marks day 7.
Lumefantrine: PKNCA
conc_lf <- sim_lf |>
dplyr::transmute(treatment = "LF 480 mg x 6", id = 1L, time = time, Cc = Cc) |>
dplyr::filter(!is.na(Cc))
# Time-zero anchor (defensive; the grid already starts at 0).
conc_lf <- dplyr::distinct(dplyr::bind_rows(
tibble::tibble(treatment = "LF 480 mg x 6", id = 1L, time = 0, Cc = 0),
conc_lf
), treatment, id, time, .keep_all = TRUE)
dose_lf_df <- tibble::tibble(
treatment = "LF 480 mg x 6", id = 1L,
time = dose_times_3day, amt = dose_nmol[["lumefantrine"]]
)
o_conc_lf <- PKNCA::PKNCAconc(conc_lf, Cc ~ time | treatment + id,
concu = "nmol/L", timeu = "h")
o_dose_lf <- PKNCA::PKNCAdose(dose_lf_df, amt ~ time | treatment + id,
doseu = "nmol", timeu = "h")
# A flat-tailed biexponential makes PKNCA's automatic lambda-z window
# dependent, so the half-life is requested on an explicit terminal window.
intervals_lf <- data.frame(
start = c(0, 400), end = c(Inf, 1200),
cmax = c(TRUE, FALSE), tmax = c(TRUE, FALSE),
auclast = c(TRUE, FALSE), aucinf.obs = c(TRUE, FALSE),
half.life = c(FALSE, TRUE)
)
res_lf <- PKNCA::pk.nca(PKNCA::PKNCAdata(o_conc_lf, o_dose_lf, intervals = intervals_lf))
nca_lf <- as.data.frame(summary(res_lf, drop.group = c()))
#> Warning: The `drop.group` argument of `summary.PKNCAresults()` is deprecated as of PKNCA
#> 0.11.0.
#> ℹ Please use the `drop_group` argument instead.
#> This warning is displayed once per session.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
knitr::kable(nca_lf, caption = "PKNCA results, typical-subject lumefantrine profile.")| Interval Start | Interval End | treatment | id | N | AUClast (h*nmol/L) | Cmax (nmol/L) | Tmax (h) | Half-life (h) | AUCinf,obs (h*nmol/L) |
|---|---|---|---|---|---|---|---|---|---|
| 0 | Inf | LF 480 mg x 6 | 1 | 1 | 1.21e6 | 14700 | 66.2 | . | 1.21e6 |
| 400 | 1200 | LF 480 mg x 6 | 1 | 1 | . | . | . | 105 | . |
Lumefantrine: virtual cohort and the 3-day vs 5-day comparison
This replicates Fig. 3 and the “Model-based simulations of alternative dosing regimens” results. The cohort is capped at 200 subjects per arm.
set.seed(20180518)
rxode2::rxSetSeed(20180518)
n_sub <- 200L
sim_regimen <- function(dose_times) {
wt <- pmin(pmax(rnorm(n_sub, 59, 6), 40), 65) # Table 1: median 59, range 40-65
obs <- sort(unique(c(seq(0, 120, by = 2), seq(120, 360, by = 2), 168)))
d <- build_data("lumefantrine", dose_times, obs, wt = wt, n = n_sub)
rxode2::rxSolve(mod_lf, d, returnType = "data.frame", addDosing = FALSE) |>
dplyr::mutate(ng_mL = Cc * mw[["lumefantrine"]] / 1000)
}
coh_3day <- sim_regimen(dose_times_3day) |> dplyr::mutate(regimen = "3-day (standard)")
coh_5day <- sim_regimen(dose_times_5day) |> dplyr::mutate(regimen = "5-day (extended)")
cohort <- dplyr::bind_rows(coh_3day, coh_5day)
cohort |>
dplyr::group_by(regimen, time) |>
dplyr::summarise(
med = median(ng_mL), lo = quantile(ng_mL, 0.05), hi = quantile(ng_mL, 0.95),
.groups = "drop"
) |>
dplyr::filter(time > 0) |>
ggplot(aes(time, med)) +
geom_ribbon(aes(ymin = lo, ymax = hi), fill = "grey75") +
geom_line(colour = "firebrick", linewidth = 0.7) +
geom_hline(yintercept = 280, linetype = "dashed") +
facet_wrap(~regimen) +
scale_y_log10() +
labs(x = "Time since first dose (h)", y = "Lumefantrine (ng/mL)") +
theme_bw()
Replicates Figure 3 of Lohy Das 2018: simulated lumefantrine profiles for the standard 3-day (A) and extended 5-day (B) regimens. Solid line = median, ribbon = 5th-95th percentiles, dashed line = 280 ng/mL day-7 target.
Day-7 concentrations and target attainment. The 280 ng/mL threshold is a measured concentration, so attainment is evaluated on the simulated observation, which carries the residual error.
day7 <- cohort |>
dplyr::filter(abs(time - 168) < 1e-6) |>
dplyr::mutate(observed = ng_mL * exp(rnorm(dplyr::n(), 0, 0.487)))
day7_summary <- day7 |>
dplyr::group_by(regimen) |>
dplyr::summarise(
median_ng_mL = median(ng_mL),
p5 = quantile(ng_mL, 0.05),
p95 = quantile(ng_mL, 0.95),
pct_below_280 = 100 * mean(observed < 280),
.groups = "drop"
)
day7_summary |>
dplyr::rename(
"Regimen" = regimen, "Median (ng/mL)" = median_ng_mL,
"5th pct" = p5, "95th pct" = p95, "% below 280 ng/mL" = pct_below_280
) |>
knitr::kable(digits = 1, caption = "Simulated day-7 lumefantrine concentrations.")| Regimen | Median (ng/mL) | 5th pct | 95th pct | % below 280 ng/mL |
|---|---|---|---|---|
| 3-day (standard) | 745.9 | 253.5 | 2604.5 | 11 |
| 5-day (extended) | 1892.6 | 318.0 | 7860.5 | 4 |
Published comparators: a median of 709 ng/mL (range 269-1,940) and 11.5% below target for the 3-day regimen; 2,010 ng/mL (769-4,580) and 0.3% below target for the 5-day regimen; and a 2.8-fold day-7 increase from extending the regimen.
med3 <- day7_summary$median_ng_mL[day7_summary$regimen == "3-day (standard)"]
med5 <- day7_summary$median_ng_mL[day7_summary$regimen == "5-day (extended)"]
c(median_3day = med3, published_3day = 709,
median_5day = med5, published_5day = 2010,
fold_increase = med5 / med3, published_fold = 2.8)
#> median_3day published_3day median_5day published_5day fold_increase
#> 745.908858 709.000000 1892.571523 2010.000000 2.537269
#> published_fold
#> 2.800000
# Assertions are on the CENTRE and on the fold-change, not on the extremes: the
# relative-bioavailability random effect has a 144% CV and a Box-Cox-stretched
# lower tail, so which subjects land in the tails is not reproducible across
# rxode2 builds.
stopifnot(
abs(med3 / 709 - 1) < 0.30,
abs(med5 / 2010 - 1) < 0.30,
abs((med5 / med3) / 2.8 - 1) < 0.25,
med5 > med3
)Artemether and dihydroartemisinin: autoinduction
am_typ <- rxode2::zeroRe(mod_am)
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_fdepot_1, etaiov_fdepot_2, etaiov_fdepot_3, etaiov_fdepot_4, etaiov_fdepot_5, etaiov_fdepot_6, etaiov_mtt_1, etaiov_mtt_2, etaiov_mtt_3, etaiov_mtt_4, etaiov_mtt_5, etaiov_mtt_6
#> as a work-around try putting the mu-referenced expression on a simple line
obs_am <- sort(unique(c(seq(0, 80, by = 0.05), seq(80, 400, by = 0.5))))
d_am <- build_data("artemether", dose_times_3day, obs_am)
d_am$dvid <- 1 # two endpoints: one solve returns both as wide columns
sim_am <- rxode2::rxSolve(am_typ, d_am, returnType = "data.frame", addDosing = FALSE)
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalmtt', 'etalcl', 'etalvc', 'etalcl_dihydroart', 'etalvc_dihydroart', 'etaiov_fdepot_1', 'etaiov_fdepot_2', 'etaiov_fdepot_3', 'etaiov_fdepot_4', 'etaiov_fdepot_5', 'etaiov_fdepot_6', 'etaiov_mtt_1', 'etaiov_mtt_2', 'etaiov_mtt_3', 'etaiov_mtt_4', 'etaiov_mtt_5', 'etaiov_mtt_6'The enzyme state is normalised to 1 at baseline and, by
Eq. 5, is the multiplier on artemether clearance. The paper
reports that “typical clearance of ARM increased 43% at occasion six
compared to occasion one” (Abstract: “increased 1.43-fold at the end of
treatment”), with an enzyme half-life of 30.4 h.
occ_mean_enzyme <- function(a, b) mean(sim_am$enzyme[sim_am$time >= a & sim_am$time <= b])
enz_occ1 <- occ_mean_enzyme(0, 8)
enz_occ6 <- occ_mean_enzyme(60, 68)
c(enzyme_t0 = sim_am$enzyme[1], occasion1_mean = enz_occ1,
occasion6_mean = enz_occ6, ratio = enz_occ6 / enz_occ1, published_ratio = 1.43)
#> enzyme_t0 occasion1_mean occasion6_mean ratio published_ratio
#> 1.000000 1.067639 1.546774 1.448780 1.430000
stopifnot(
abs(sim_am$enzyme[1] - 1) < 1e-6, # Methods: initialised at 1
abs((enz_occ6 / enz_occ1) / 1.43 - 1) < 0.05
)
sim_am |>
dplyr::filter(time <= 200) |>
dplyr::mutate(`CL_ARM/F (L/h)` = 467 * enzyme, `Relative enzyme amount` = enzyme) |>
tidyr::pivot_longer(c(`CL_ARM/F (L/h)`, `Relative enzyme amount`)) |>
ggplot(aes(time, value)) +
geom_line(linewidth = 0.7) +
geom_vline(xintercept = dose_times_3day, linetype = "dotted", alpha = 0.4) +
facet_wrap(~name, scales = "free_y") +
labs(x = "Time since first dose (h)", y = NULL) +
theme_bw()
Enzyme turnover compartment (relative to its pre-induced baseline of 1) and the resulting time-varying artemether clearance over the 3-day regimen and washout.
Setting Emax to zero must abolish the time dependence
entirely – a structural control on the induction arm.
am_noind <- rxode2::ini(am_typ, lemax = log(1e-8))
#> ℹ change initial estimate of `lemax` to `-18.4206807439524`
sim_noind <- rxode2::rxSolve(am_noind, d_am, returnType = "data.frame", addDosing = FALSE)
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalmtt', 'etalcl', 'etalvc', 'etalcl_dihydroart', 'etalvc_dihydroart', 'etaiov_fdepot_1', 'etaiov_fdepot_2', 'etaiov_fdepot_3', 'etaiov_fdepot_4', 'etaiov_fdepot_5', 'etaiov_fdepot_6', 'etaiov_mtt_1', 'etaiov_mtt_2', 'etaiov_mtt_3', 'etaiov_mtt_4', 'etaiov_mtt_5', 'etaiov_mtt_6'
stopifnot(
abs(max(sim_noind$enzyme) - 1) < 1e-4,
abs(min(sim_noind$enzyme) - 1) < 1e-4
)Metabolite mass balance. Under the paper’s assumption of complete and
irreversible molar conversion, all artemether clearance forms
dihydroartemisinin, so CL_DHA * AUC_DHA must equal the
total molar artemether dose. This is the gate that catches a broken
parent-to-metabolite coupling; the parent’s own dose recovery is blind
to it.
auc_dha <- trap(sim_am$time, sim_am$Cc_dihydroart)
dha_recovery <- 611 * auc_dha / (length(dose_times_3day) * dose_nmol[["artemether"]])
dha_recovery
#> [1] 1.000002
stopifnot(abs(dha_recovery - 1) < 0.005)A perturbation confirms the coupling is directional: doubling
CL_DHA must halve the metabolite AUC while leaving the
parent untouched.
am_fast <- rxode2::ini(am_typ, lcl_dihydroart = log(2 * 611))
#> ℹ change initial estimate of `lcl_dihydroart` to `7.10824413973154`
sim_fast <- rxode2::rxSolve(am_fast, d_am, returnType = "data.frame", addDosing = FALSE)
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalmtt', 'etalcl', 'etalvc', 'etalcl_dihydroart', 'etalvc_dihydroart', 'etaiov_fdepot_1', 'etaiov_fdepot_2', 'etaiov_fdepot_3', 'etaiov_fdepot_4', 'etaiov_fdepot_5', 'etaiov_fdepot_6', 'etaiov_mtt_1', 'etaiov_mtt_2', 'etaiov_mtt_3', 'etaiov_mtt_4', 'etaiov_mtt_5', 'etaiov_mtt_6'
c(dha_auc_ratio = trap(sim_fast$time, sim_fast$Cc_dihydroart) / auc_dha,
arm_auc_ratio = trap(sim_fast$time, sim_fast$Cc) / trap(sim_am$time, sim_am$Cc))
#> dha_auc_ratio arm_auc_ratio
#> 0.4999999 0.9999999
stopifnot(
abs(trap(sim_fast$time, sim_fast$Cc_dihydroart) / auc_dha - 0.5) < 0.01,
abs(trap(sim_fast$time, sim_fast$Cc) / trap(sim_am$time, sim_am$Cc) - 1) < 0.01
)
sim_am |>
dplyr::filter(time <= 80) |>
dplyr::select(time, Artemether = Cc, Dihydroartemisinin = Cc_dihydroart) |>
tidyr::pivot_longer(-time, names_to = "Analyte", values_to = "nM") |>
ggplot(aes(time, nM, colour = Analyte)) +
geom_line(linewidth = 0.7) +
geom_hline(yintercept = 4.79, linetype = "dashed", alpha = 0.5) +
labs(x = "Time since first dose (h)", y = "Concentration (nmol/L)") +
theme_bw() +
theme(legend.position = "top")
Typical-subject artemether and dihydroartemisinin plasma profiles. The declining artemether peaks across the six doses are the autoinduction; the dashed lines are the assay LLOQs (4.79 and 5.03 nmol/L).
Artemether and dihydroartemisinin: PKNCA
conc_am <- sim_am |>
dplyr::select(time, Artemether = Cc, Dihydroartemisinin = Cc_dihydroart) |>
tidyr::pivot_longer(-time, names_to = "analyte", values_to = "Cc") |>
dplyr::mutate(treatment = paste(analyte, "80 mg ARM x 6"), id = 1L) |>
dplyr::filter(!is.na(Cc))
o_conc_am <- PKNCA::PKNCAconc(conc_am, Cc ~ time | treatment + id,
concu = "nmol/L", timeu = "h")
o_dose_am <- PKNCA::PKNCAdose(
tibble::tibble(
treatment = rep(unique(conc_am$treatment), each = length(dose_times_3day)),
id = 1L,
time = rep(dose_times_3day, times = 2),
amt = dose_nmol[["artemether"]]
),
amt ~ time | treatment + id, doseu = "nmol", timeu = "h"
)
intervals_am <- data.frame(
start = 0, end = Inf,
cmax = TRUE, tmax = TRUE, auclast = TRUE, aucinf.obs = TRUE, half.life = TRUE
)
res_am <- PKNCA::pk.nca(PKNCA::PKNCAdata(o_conc_am, o_dose_am, intervals = intervals_am))
nca_am <- as.data.frame(summary(res_am, drop.group = c()))
knitr::kable(nca_am, caption = "PKNCA results, typical-subject artemether and dihydroartemisinin profiles.")| Interval Start | Interval End | treatment | id | N | AUClast (h*nmol/L) | Cmax (nmol/L) | Tmax (h) | Half-life (h) | AUCinf,obs (h*nmol/L) |
|---|---|---|---|---|---|---|---|---|---|
| 0 | Inf | Artemether 80 mg ARM x 6 | 1 | 1 | 2570 | 93.1 | 9.45 | 4.39 | 2570 |
| 0 | Inf | Dihydroartemisinin 80 mg ARM x 6 | 1 | 1 | 2630 | 85.1 | 61.8 | 4.39 | 2630 |
Comparison against published values
The paper reports no NCA table, so the comparison is against the derived quantities it does publish.
sim_vals <- tibble::tribble(
~quantity, ~PPTESTCD, ~PPORRES,
"Lumefantrine", "aucinf.obs", auc_lf_mass,
"Lumefantrine", "half.life", t_half_solved,
"Artemether", "auclast", 611 * auc_dha / (length(dose_times_3day) * dose_nmol[["artemether"]])
)
ref_vals <- tibble::tribble(
~quantity, ~aucinf.obs, ~half.life, ~auclast,
"Lumefantrine", 641, 4 * 24, NA_real_,
"Artemether", NA_real_, NA_real_, 1
)
cmp <- nlmixr2lib::ncaComparisonTable(
sim_vals, ref_vals, by = "quantity",
units = c(aucinf.obs = "h*mg/L", half.life = "h", auclast = "fraction recovered")
)
knitr::kable(cmp, digits = 3,
caption = "Simulated versus published lumefantrine AUC and terminal half-life, and the dihydroartemisinin molar dose recovery.")| NCA parameter | quantity | Reference | Simulated | % diff |
|---|---|---|---|---|
| AUC0-∞ (obs) (h*mg/L) | Lumefantrine | 641 | 641 | +0.1% |
| AUClast (fraction recovered) | Artemether | 1 | 1 | +0.0% |
| t½ (h) | Lumefantrine | 96 | 105 | +9.2% |
attr(cmp, "footnote")
#> NULL| Quantity | Published | Reproduced |
|---|---|---|
| LF AUC(0-inf), typical subject | 641 h*mg/L | 641.4 h*mg/L |
| LF terminal half-life | about 4 days | 4.37 days |
| LF mean absorption time | 4.04 h | 4.04 h (ktr = 6/MTT, n = 5) |
| LF day-7 median, 3-day regimen | 709 ng/mL | 746 ng/mL |
| LF day-7 median, 5-day regimen | 2,010 ng/mL | 1893 ng/mL |
| ARM CL increase, occasion 6 vs 1 | 1.43-fold | 1.449-fold |
| ARM mean absorption time | 0.738 h (about 45 min) | 0.738 h (ktr = 3/MTT, n = 2) |
| DHA molar dose recovery | 1 (complete conversion) | 1.0000 |
Assumptions and deviations
Omega scale. Table 2 footnote a states the scale
explicitly: “Coefficient of variation (CV) for BSV and BOV was
calculated as 100 x (variance)^1/2”, so the printed percentage is 100
times the eta-scale standard deviation and
variance = (CV/100)^2. That footnote is what the model
files encode. The competing reading – the conventional log-normal
relation variance = log(1 + CV^2), which many popPK papers
use for the same column heading – was tested against the paper’s own
simulation output under an identical cohort, seed and observation
grid:
| Reading | Day-7 median, 3-day | Day-7 5th pct | Day-7 median, 5-day | 5-day / 3-day fold |
|---|---|---|---|---|
| Published | 709 ng/mL | 269 ng/mL | 2,010 ng/mL | 2.8 |
variance = (CV/100)^2 (footnote, encoded) |
726 (+2.5%) | 276 (+2.6%) | 2,070 (+3.0%) | 2.85 (+1.8%) |
variance = log(1 + CV^2) |
644 (-9.2%) | 281 (+4.5%) | 1,996 (-0.7%) | 3.10 (+10.7%) |
The footnote reading is the better match on three of the four comparators, so the explicit footnote and the paper’s own simulations agree and the encoding is not in tension with either. Neither reading reproduces the published 95th percentile (1,940 ng/mL; 2,604 and 2,185 respectively), which is expected: the paper resampled the covariates of its own 22 patients rather than drawing from a smooth weight distribution, and it does not state whether its reported “range” includes residual error.
The paper’s day-7 numbers are mutually inconsistent. Results reports a simulated day-7 range of 269-1,940 ng/mL and that 11.5% of patients fall below 280 ng/mL. If 269 ng/mL is the 5th percentile, the fraction below 280 ng/mL must be only slightly above 5%, not 11.5%. The two statements cannot both describe the same distribution. The vignette therefore asserts on the median and on the 3-day-to-5-day fold change, and reports the tail statistics without gating on them.
Emax units. Table 2 prints the unit of
Emax as “h-1”. Eq. 4 adds the Emax-scaled effect to 1
inside K_ENZ * (1 + EFF), so EFF – and
therefore Emax – must be dimensionless. The printed unit is
treated as a typographical error.
“BSV” versus “BOV” on lumefantrine F. The Results text says a Box-Cox transformation was applied to “the BSV for F”, and Table 2 labels its row “Box-Cox shape parameter for BSV on F”. However, the only variability term Table 2 prints for lumefantrine F is flagged with footnote b, i.e. it is a between-occasion variance, and the shrinkage paragraph reports a BOV shrinkage range for F but no BSV shrinkage. The model file follows the table: lumefantrine F carries an occasion-level random effect (six slots) and no subject-level one, with the Box-Cox transform applied to it. The artemether model, where Table 2 prints both a BSV and a BOV row for F, carries both.
Box-Cox implementation. rxode2’s
boxCox() attaches to the residual-error model and cannot
transform an eta, so Eq. 3 is written out explicitly in
model() in the Petersson (2009) form used by the canonical
boxcox_<param> family. Because the shape is negative,
the resulting bioavailability distribution is bounded above at
exp(1/0.605) = 5.2 but has a long lower tail; occasional
simulated occasions therefore absorb very little drug. That is a
property of the published model, not of the encoding.
Between-occasion variability encoding. NONMEM shares
one variance across occasion slots. The model files encode six
etaiov_* slots per affected parameter, with the first
estimable and the remaining five fixed() to the same value,
following the sibling Ding_2026_artemether model. The
OCC column is capped at 6 for the extended 5-day regimen
because the paper estimated only six occasions and does not say how it
handled the four additional doses in its own 5-day simulation.
Covariates screened but not retained. Estimated
gestational age, baseline and time-varying parasitemia density, and body
temperature were all explored and are recorded in
covariatesDataExcluded rather than
covariateData: each was either rejected outright or
selected in forward selection but dropped in backward elimination, and
none carries a published point estimate. Ideal body weight, fat-free
mass and normal fat mass were explored allometrically (Methods Eqs. 7-9)
and none improved the fit, so only total body weight allometry is
encoded.
Informative priors. The lumefantrine fit used frequentist informative priors drawn from an earlier Ugandan study to identify the peripheral compartment. Only the paper’s final posterior estimates are encoded; the priors themselves are not reproduced here and the upstream paper was not required for the extraction.
Weight distribution. Table 1 gives only the median (59 kg) and range (40-65 kg) of body weight, so the virtual cohort draws weights from a truncated normal matching those summaries. The paper’s own simulations resampled the actual 22 patients’ covariates.
Not encoded. The paper reports no PD or parasite-clearance model, so none is extracted. The two drugs are deliberately in separate model files because the authors’ own simultaneous fit was unstable; no correlation between the lumefantrine and artemether absorption parameters is encoded, matching the published final models.