Model and source
- Citation: Yin W, Facius A, Asgharnejad M, Lahu G, Vakilynejad M. Population pharmacokinetics, enzyme occupancy, and pharmacodynamic modeling of soticlestat in patients with developmental and epileptic encephalopathies. Clin Transl Sci. 2024;17(3):e13722. doi:10.1111/cts.13722. The CH24H enzyme-occupancy sub-model (ke0, Emax, EC50, gamma) is carried forward unchanged and FIXED from the healthy-volunteer model of Yin W, Facius A, Asgharnejad M, Wang S, Rosen L, Bhattacharya A, Lahu G, Vakilynejad M. Modeling and simulation of soticlestat pharmacokinetics, brain enzyme occupancy, and pharmacodynamics in healthy volunteers. Clin Transl Sci. 2023;16(8):1422-1434. doi:10.1111/cts.13517.
- Description: Joint population PK / CH24H enzyme-occupancy (EO) / 24S-hydroxycholesterol (24HC) pharmacodynamic model for soticlestat (TAK-935), a cholesterol 24-hydroxylase inhibitor, in healthy volunteers and patients with developmental and epileptic encephalopathies (DEE) including Dravet syndrome and Lennox-Gastaut syndrome (Yin 2024). Two-compartment PK with first-order absorption and an absorption lag time; a plasma-to-brain effect compartment whose concentration drives both a fixed sigmoid Emax CH24H enzyme-occupancy read-out and a semimechanistic sigmoid Imax inhibitory indirect-response turnover model for 24HC. Covariates: formulation on lag time and ka; dose on ka, Q, Vp and relative bioavailability; BMI on ka (participants aged <= 18 y only); strong CYP3A-inducing antiseizure comedication and Chinese descent on ka; patient (vs healthy-volunteer) status on CL; Japanese and Chinese descent on Q; eGFR on Vp; body weight and alpha-1-acid glycoprotein on relative bioavailability; and age (hockey stick below 17.5 y), alpha-1-acid glycoprotein and body weight on baseline 24HC.
- Article: https://doi.org/10.1111/cts.13722
- Supplement (Appendix S1, NONMEM control streams for both final
models): https://doi.org/10.1111/cts.13722 (Supporting
Information
CTS-17-e13722-s001)
Soticlestat (TAK-935) is a first-in-class selective inhibitor of cholesterol 24-hydroxylase (CH24H / CYP46A1), the brain-specific enzyme that converts cholesterol to 24S-hydroxycholesterol (24HC). It is in development for the developmental and epileptic encephalopathies (DEEs) Dravet syndrome (DS) and Lennox-Gastaut syndrome (LGS).
Yin 2024 extends the healthy-volunteer model of Yin 2023 (doi:10.1111/cts.13517) to
patients with DEEs. Two NONMEM runs are reported, and this package ships
them as one model file because the second run consumes
the first: the PK/EO/PD run reads each subject’s individual
ALAG1_, KA_, CL_,
V2_, Q_, V3_ and F1_
from the data set (a sequential IPP fit), so a self-contained rxode2
model must carry the PK layer explicitly. The single file therefore
reproduces Table 1a (popPK) and Table 1b (PK/EO/PD) together.
The three linked layers are:
-
PK – two-compartment disposition with first-order
absorption and an absorption lag time (NONMEM
ADVAN5, Appendix S1 popPK$PROBLEM). -
Effect site / enzyme occupancy – a plasma-to-brain
effect compartment (
effect, a concentration) driving a fixed sigmoid Emax CH24H enzyme-occupancy read-out. Not re-estimated here: “The PK/EO model was not updated because no additional EO data were available”, so all four EO parameters carryFIXand are inherited from Yin 2023. -
24HC turnover – a semimechanistic indirect-response
model in which the effect-site concentration inhibits 24HC synthesis
through a sigmoid Imax function (NONMEM
ADVAN13, Appendix S1 PK/EO/PD$PROBLEM).
Population
Eight phase I / II trials contributed data (Table S1). Two overlapping analysis sets were used: 218 individuals (110 healthy volunteers, 108 patients; 3288 soticlestat concentrations, 8732 dosing events) for the popPK model, and 306 individuals (132 healthy, 174 patients; 2621 plasma 24HC concentrations, 8703 dosing events) for the PK/EO/PD model. The bioavailability study TAK-935-1005 was excluded from the PD analysis because 24HC was not collected, and the open-label ELEKTRA extension was excluded from both.
From Table S2, the popPK analysis set had mean (SD) age 24.1 (14.5) years, body weight 56.6 (24.4) kg, BMI 21.8 (5.4) kg/m^2, eGFR 166 (54.1) mL/min/1.73 m^2 and AGP 19.7 (4.22) mg/dL; 67% were of White ethnic background, 11% Black, 11% Japanese and 9% Chinese. Patient diagnoses were LGS (44), Dravet syndrome (26), unspecified DEE (18), CDKL5 deficiency disorder (12) and 15q duplication syndrome (8). Soticlestat was given as an oral solution (phase I) or as tablets, including via G-tube / PEG tube in the paediatric trials, over 15-1350 mg single doses and 100-300 mg b.i.d. titrated regimens.
Missing AGP was imputed at the observed median of 20 mg/dL; four of
the eight trials did not collect it at all (Table S2 footnote a), which
is what the AAG_MISSING covariate flags.
The same information is available programmatically via
readModelDb("Yin_2024_soticlestat")()$population.
Source trace
Every ini() entry carries an in-file comment naming its
source location in
inst/modeldb/specificDrugs/Yin_2024_soticlestat.R. They are
collected here.
Structural and covariate parameters (Table 1a, popPK model)
| Parameter | Value | Source location |
|---|---|---|
lka |
8.39 1/h | Table 1a, Absorption rate (ka), TV |
lcl |
4.2 L/h | Table 1a, Elimination clearance (CL), TV |
lvc |
3.01 L | Table 1a, Central volume (Vc), TV |
lq |
1.15 L/h | Table 1a, Distribution clearance (Q), TV |
lvp |
7.8 L | Table 1a, Peripheral volume (Vp), TV |
ltlag |
0.133 h | Table 1a, Lag time of the first compartment (ALAG1), TV |
lfdepot |
0.0216 (fixed) | Table 1a, Bioavailability (F1), TV, Fixed |
e_form_tablet_tlag |
+23.8% | Table 1a, ALAG1 Non-OS formulation effect |
e_form_tablet_ka |
-43.7% | Table 1a, ka Non-OS formulation effect |
e_dose_ka |
-0.753 (fixed) | Table 1a, ka Dose effect, exponent |
e_bmi_ka |
2.24 | Table 1a, ka BMI effect, exponent |
e_conmed_eiaed_ka |
-66.2% | Table 1a, ka Strong CYP3A enzyme inducer effect |
e_race_chinese_ka |
-63.7% | Table 1a, ka Chinese descent effect |
e_dis_healthy_cl |
-22.8% | Table 1a, CL Patient effect |
e_dose_q |
-0.218 (fixed) | Table 1a, Q Dose effect, exponent |
e_race_japanese_q |
-42.7% | Table 1a, Q Japanese descent effect |
e_race_chinese_q |
-75.7% | Table 1a, Q Chinese descent effect |
e_dose_vp |
-0.214 (fixed) | Table 1a, Vp Dose effect, exponent |
e_crcl_vp |
-0.406 | Table 1a, Vp eGFR effect, exponent |
e_dose_fdepot |
0.204 (fixed) | Table 1a, F1 Dose effect, exponent |
e_wt_fdepot |
-0.593 | Table 1a, F1 Body weight effect, exponent |
e_aag_fdepot |
0.544 | Table 1a, F1 AGP effect, exponent |
e_aag_missing_fdepot |
0.42 | Table 1a, F1 “BSV explained by AGP” |
etalka, etalq, etalvp,
etalfdepot
|
1.02, 0.436, 0.625, 0.527 | Table 1a, BSV rows – see Errata on the reported scale |
propSd |
48.3% | Table 1a, Residual variability, Proportional |
addSd |
0.001 ng/mL (fixed) | Table 1a, Residual variability, Additive |
Reference (centering) values come from Appendix S1 $PK:
DOSE 300 mg, BMI 23.6 kg/m^2, eGFR 162.4 mL/min/1.73 m^2, WT 65.9 kg (on
F1), AAG 20 mg/dL.
Enzyme-occupancy and 24HC parameters
| Parameter | Value | Source location |
|---|---|---|
lke0 (KPLBR) |
0.254 1/h (fixed) | Appendix S1 PK/EO/PD $THETA 3, FIX (from
Yin 2023) |
lemax (EMAX) |
100% (fixed) | Appendix S1 PK/EO/PD $THETA 4, FIX (from
Yin 2023) |
lec50 (EC50) |
5.86 ng/mL (fixed) | Appendix S1 PK/EO/PD $THETA 5, FIX (from
Yin 2023) |
lhill_eo (EGAM) |
0.769 (fixed) | Appendix S1 PK/EO/PD $THETA 6, FIX (from
Yin 2023) |
lrbase |
50.5 ng/mL | Table 1b, Baseline 24HC (BL24HC), TV |
e_age_bp_rbase |
17.5 years | Table 1b, Age effect cutoff |
e_age_rbase |
-0.511 | Table 1b, Age effect, exponent |
e_aag_rbase |
0.215 | Table 1b, AGP effect, exponent |
e_wt_rbase |
-0.256 | Table 1b, Body weight effect, exponent |
lkout |
0.0199 1/h | Table 1b, 24HC degradation rate (kout), TV |
limax |
92% | Table 1b, Maximum inhibition of 24HC production (Imax), TV |
lic50 |
9.85 ng/mL | Table 1b, Effect-site concentration for 50% maximum effect (IC50), TV |
lhill_hc24 |
0.881 | Table 1b, Shape parameter (gamma), TV |
iplac (I0) |
0 (fixed) | Appendix S1 PK/EO/PD $THETA 9, FIX
|
etalrbase |
0.0811 (variance) | Table 1b, BL24HC BSV |
etalic50 |
0.636 (variance) | Table 1b, IC50 BSV |
addSd_hc24 |
3.91 ng/mL | Table 1b, Residual variability, Additive |
Reference (centering) values from Appendix S1 PK/EO/PD
$PK: AAG 20 mg/dL, WT 55.25 kg; the age breakpoint 17.5 y
is itself an estimated parameter.
Equations
| Equation | Source location |
|---|---|
d/dt(depot) = -ka * depot |
Appendix S1 PK/EO/PD $DES, DADT(1)
|
d/dt(central) = ka*depot - cl/vc*central - q/vc*central + q/vp*peripheral1 |
$DES, DADT(2)
|
d/dt(peripheral1) = q/vc*central - q/vp*peripheral1 |
$DES, DADT(3)
|
d/dt(effect) = ke0 * (Cc - effect) |
$DES, DADT(4) =
KPLBR*(A(2)/S2 - A(4))
|
d/dt(hc24) = kin*eff - kout*hc24,
hc24(0) = rbase, kin = rbase*kout
|
$DES, DADT(5); $PK
A_0(5) = BL, KIN = BL*KOUT
|
eff = 1 - iplac - imax*Ce^g/(Ce^g + ic50^g)/100 |
$DES EFF
|
occ = emax*Ce^h/(ec50^h + Ce^h) |
$ERROR OCC
|
Cc = 1000 * central / vc |
$PK S2 = V2/1000 (mg / L -> ng/mL) |
Dimensional analysis
The 24HC layer mixes a concentration state with rate constants, so each ODE term is checked explicitly.
| Term | Units | Result |
|---|---|---|
ka * depot |
(1/h) x mg | mg/h |
cl/vc * central |
(L/h)/(L) x mg | mg/h |
Cc = 1000 * central / vc |
(mg/L) x 1000 | ng/mL |
ke0 * (Cc - effect) |
(1/h) x (ng/mL) | (ng/mL)/h |
kin = rbase * kout |
(ng/mL) x (1/h) | (ng/mL)/h |
kout * hc24 |
(1/h) x (ng/mL) | (ng/mL)/h |
eff, occ/100, iplac
|
unitless | unitless |
d/dt(effect) and d/dt(hc24) both resolve to
(ng/mL)/h, matching states held in ng/mL. Note that effect
and hc24 are concentration states, not
amounts – d/dt(effect) is driven by Cc, a
concentration, exactly as DADT(4) = KPLBR*(A(2)/S2 - A(4))
is in the control stream. The factor 1000 in Cc is the only
unit conversion in the model and reproduces
S2 = V2/1000.
Reference subject and virtual cohorts
mod <- readModelDb("Yin_2024_soticlestat")
# Reference patient for the paper's dose-response and dosing simulations:
# "a reference patient with a DEE who was aged 45 years, not of Asian descent,
# and with a body weight of 70 kg" (Simulations, first paragraph). Remaining
# covariates take the reference values defined in the covariate section:
# BMI 21.8 kg/m^2, eGFR 166.2 mL/min/1.73 m^2, AGP 20 mg/dL.
ref_covariates <- function(dose, wt = 70, healthy = 0) {
data.frame(
DOSE = dose,
WT = wt,
BMI = 21.8,
AGE = 45,
CRCL = 166.2,
AAG = 20,
AAG_MISSING = 0,
FORM_TABLET = 1, # patients received tablets
DIS_HEALTHY = healthy,
RACE_JAPANESE = 0,
RACE_CHINESE = 0,
CONMED_EIAED = 0
)
}
doses <- c(100, 200, 300)The event tables below place observation rows on the
central ODE state and carry an explicit
dvid = 1L. Both are required: this model declares two
residual endpoints (Cc and hc24), and rxode2
rejects an observation record with no dvid when a DV-id
mapping is active. cmt names the ODE state, never the
observable – writing cmt = "Cc" would append an observable
slot and is the classic compartment-renumbering bug. rxode2 returns
every algebraic observable (Cc, occ,
hc24Chg) and every state (effect,
hc24) as an output column at those rows, so one set of
observation records exercises all three layers.
set.seed(20240301)
# Dosing records: 21 days of b.i.d. dosing. The paper's hypothetical schedule
# is 21 days of treatment followed by a 7-day washout.
dose_records <- function(dose) {
data.frame(time = seq(0, 24 * 21 - 12, by = 12), amt = dose, evid = 1L,
cmt = "depot", dvid = NA_integer_)
}
obs_records <- function(times) {
data.frame(time = times, amt = NA_real_, evid = 0L,
cmt = "central", dvid = 1L)
}
build_arm <- function(dose, id, times) {
ev <- rbind(dose_records(dose), obs_records(times))
ev <- ev[order(ev$time, -ev$evid), ]
ev$id <- id
cbind(ev, ref_covariates(dose)[rep(1, nrow(ev)), ], row.names = NULL)
}
# Typical-value arms: whole 28-day window on a 1 h grid, with a fine grid over
# the final steady-state dosing interval.
typ_times <- sort(unique(c(seq(0, 24 * 28, by = 1), seq(24 * 20, 24 * 21, by = 0.1))))
events_typ <- dplyr::bind_rows(
lapply(seq_along(doses), function(i) build_arm(doses[i], i, typ_times))
) |>
dplyr::mutate(regimen = paste0(DOSE, " mg b.i.d."))
# Between-subject-variability cohort for the steady-state EO / 24HC endpoints
# and for the AUC prediction interval. 200 subjects per arm is the cap; the
# BSV on IC50 is large (variance 0.636), so the smallest cohort that keeps the
# median endpoints stable across seeds is used.
n_per_arm <- 200
bsv_times <- seq(24 * 20, 24 * 21, by = 0.25)
events_bsv <- dplyr::bind_rows(lapply(seq_along(doses), function(i) {
dplyr::bind_rows(lapply(seq_len(n_per_arm), function(k) {
build_arm(doses[i], (i - 1L) * n_per_arm + k, bsv_times)
}))
})) |>
dplyr::mutate(regimen = paste0(DOSE, " mg b.i.d."))
stopifnot(!anyDuplicated(unique(events_bsv[, c("id", "time", "evid")])))Simulation
useLinCmt = FALSE is required: this is a multi-output
model over five ODE states, and rxode2’s default
ODE-to-linCmt() auto-conversion corrupts the endpoint
mapping for models of this shape.
sim_typ <- rxode2::rxSolve(
rxode2::zeroRe(mod), events = events_typ,
keep = c("regimen", "DOSE"), useLinCmt = FALSE
) |> as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalq', 'etalvp', 'etalfdepot', 'etalrbase', 'etalic50'
#> Warning: multi-subject simulation without without 'omega'
# Seeded immediately before the stochastic solve so the reported medians are
# reproducible: the preceding zeroRe() solve otherwise advances the RNG stream
# and the published-value comparisons below would shift from run to run.
set.seed(20240301)
sim_bsv <- rxode2::rxSolve(
mod, events = events_bsv,
keep = c("regimen", "DOSE"), useLinCmt = FALSE
) |> as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
stopifnot(nrow(sim_typ) > 0, nrow(sim_bsv) > 0, !all(is.na(sim_typ$Cc)))Replicate published figures
Figure 4b – soticlestat, enzyme occupancy and 24HC over 21 days plus washout
# Replicates Figure 4(b) of Yin 2024: typical-value profiles of soticlestat
# plasma concentration, CH24H enzyme occupancy, and percent change from
# baseline 24HC over 21 days of b.i.d. dosing followed by a 7-day washout.
sim_typ |>
dplyr::filter(!is.na(Cc)) |>
dplyr::select(time, regimen, Cc, occ, hc24Chg) |>
tidyr::pivot_longer(c(Cc, occ, hc24Chg), names_to = "endpoint") |>
dplyr::mutate(endpoint = factor(
endpoint, c("Cc", "occ", "hc24Chg"),
c("Soticlestat (ng/mL)", "CH24H enzyme occupancy (%)",
"24HC change from baseline (%)")
)) |>
ggplot(aes(time / 24, value, colour = regimen)) +
geom_line() +
facet_wrap(~endpoint, ncol = 1, scales = "free_y") +
labs(x = "Time (days)", y = NULL, colour = NULL,
title = "Figure 4(b) -- 21 days b.i.d. dosing then 7 days washout",
caption = "Replicates Figure 4(b) of Yin 2024.") +
theme(legend.position = "top")
The paper reports that steady-state 24HC inhibition is “achieved within a week of treatment and maintained over time”, with minimal PK and EO accumulation.
inhib <- sim_typ |>
dplyr::filter(!is.na(hc24Chg)) |>
dplyr::group_by(regimen) |>
dplyr::summarise(
day7 = hc24Chg[which.min(abs(time - 24 * 7))],
day14 = hc24Chg[which.min(abs(time - 24 * 14))],
day21 = hc24Chg[which.min(abs(time - 24 * 21))],
washout = hc24Chg[which.min(abs(time - 24 * 28))],
.groups = "drop"
) |>
dplyr::mutate(frac_of_ss_at_day7 = day7 / day21)
inhib |>
dplyr::rename(
"Regimen" = regimen,
"Day 7 (%)" = day7,
"Day 14 (%)" = day14,
"Day 21 (%)" = day21,
"Day 28, 7 d after last dose (%)" = washout,
"Day 7 as a fraction of day 21" = frac_of_ss_at_day7
) |>
knitr::kable(digits = 2,
caption = "24HC change from baseline over the 21-day dosing period and after the 7-day washout.")| Regimen | Day 7 (%) | Day 14 (%) | Day 21 (%) | Day 28, 7 d after last dose (%) | Day 7 as a fraction of day 21 |
|---|---|---|---|---|---|
| 100 mg b.i.d. | -66.91 | -69.34 | -69.42 | -3.32 | 0.96 |
| 200 mg b.i.d. | -76.41 | -79.15 | -79.25 | -4.06 | 0.96 |
| 300 mg b.i.d. | -80.20 | -83.07 | -83.17 | -4.47 | 0.96 |
stopifnot(
# "Steady-state levels for 24HC inhibition were achieved within a week": at
# day 7 the model reaches ~96% of the day-21 inhibition at every dose. The
# residual 4% is the tail of the turnover half-life, log(2)/0.0199 = 34.8 h,
# so day 7 is 4.8 half-lives in.
all(inhib$frac_of_ss_at_day7 > 0.95),
# "...and were maintained over time": day 14 to day 21 is essentially flat.
all(abs(inhib$day14 - inhib$day21) < 0.2),
# 24HC returns close to baseline within the 7-day washout.
all(abs(inhib$washout) < 6)
)Figure 4a / Simulations – average steady-state EO and 24HC change
The paper reports that with 100, 200 and 300 mg b.i.d. over 12 h at steady state, average EO (median of individual simulated values) was 81.7%, 89.2% and 92.2%, and average change from baseline 24HC was -71.6%, -80.6% and -84.1%.
# Replicates the dose-response summary of Figure 4(a) of Yin 2024.
ss_window <- sim_bsv |>
dplyr::filter(!is.na(Cc), time >= 24 * 20, time <= 24 * 20 + 12)
per_subject <- ss_window |>
dplyr::group_by(regimen, DOSE, id) |>
dplyr::summarise(occ_avg = mean(occ), hc24_avg = mean(hc24Chg), .groups = "drop")
pd_summary <- per_subject |>
dplyr::group_by(regimen, DOSE) |>
dplyr::summarise(occ_med = median(occ_avg), hc24_med = median(hc24_avg),
.groups = "drop") |>
dplyr::arrange(DOSE) |>
dplyr::mutate(occ_pub = c(81.7, 89.2, 92.2), hc24_pub = c(-71.6, -80.6, -84.1))
pd_summary |>
dplyr::select(-DOSE) |>
dplyr::rename(
"Regimen" = regimen,
"Average EO, simulated (%)" = occ_med,
"Average 24HC change, simulated (%)" = hc24_med,
"Average EO, published (%)" = occ_pub,
"Average 24HC change, published (%)" = hc24_pub
) |>
knitr::kable(digits = 1,
caption = sprintf(paste("Average enzyme occupancy and 24HC change over 12 h at steady state:",
"median of %d simulated subjects per arm vs the Yin 2024 Simulations section."),
n_per_arm))| Regimen | Average EO, simulated (%) | Average 24HC change, simulated (%) | Average EO, published (%) | Average 24HC change, published (%) |
|---|---|---|---|---|
| 100 mg b.i.d. | 79.8 | -70.1 | 81.7 | -71.6 |
| 200 mg b.i.d. | 87.8 | -79.6 | 89.2 | -80.6 |
| 300 mg b.i.d. | 91.4 | -83.2 | 92.2 | -84.1 |
# Both endpoints are bounded percentages, so an absolute-percentage-point
# tolerance is the right scale. EO is compared against Emax = 100% and the
# 24HC change against Imax = -92%, as in the paper's Figure 4(a) axes.
#
# The 3.5-point tolerance is sized from the Monte Carlo noise, not chosen to
# make the test pass: repeating this cohort across five seeds at 200 subjects
# per arm gave a largest deviation of 2.1 points on EO and 3.1 points on the
# 24HC change. A mis-transcribed Imax, IC50 or shape parameter would move these
# medians by well over 10 points, so the test still discriminates. The residual
# systematic offset (EO simulated slightly low, inhibition slightly shallow) is
# expected: the paper simulated over the trial's paediatric-to-adult covariate
# distribution, whereas this cohort fixes covariates at the 70 kg reference
# patient.
stopifnot(
max(abs(pd_summary$occ_med - pd_summary$occ_pub)) < 3.5,
max(abs(pd_summary$hc24_med - pd_summary$hc24_pub)) < 3.5,
# Monotone dose-response in both endpoints (seed-independent).
all(diff(pd_summary$occ_med) > 0),
all(diff(pd_summary$hc24_med) < 0)
)Figure 5 / Table 2 – adult reference steady-state AUC
Table 2’s footnote reports median (90% PI) steady-state AUC24 for the
adult reference dosages as 1030 (432, 2470), 2380 (995, 5700) and 3870
(1620, 9280) ng*h/mL for 100, 200 and 300 mg b.i.d. Because
between-subject variability enters AUC only through the log-normal
etalfdepot (BSV on CL is fixed to zero), the median of the
simulated distribution equals the typical value, and the width of the
prediction interval is a direct read-out of omega_F1.
auc_typ <- sim_typ |>
dplyr::filter(!is.na(Cc), time >= 24 * 20, time <= 24 * 21) |>
dplyr::group_by(regimen, DOSE) |>
dplyr::summarise(
auc24 = sum(diff(time) * (head(Cc, -1) + tail(Cc, -1)) / 2),
.groups = "drop"
) |>
dplyr::arrange(DOSE) |>
dplyr::mutate(auc24_pub = c(1030, 2380, 3870),
pct_diff = 100 * (auc24 - auc24_pub) / auc24_pub)
auc_typ |>
dplyr::select(-DOSE) |>
dplyr::rename(
"Regimen" = regimen,
"AUC24 simulated (ng*h/mL)" = auc24,
"AUC24 published (ng*h/mL)" = auc24_pub,
"Difference (%)" = pct_diff
) |>
knitr::kable(digits = c(0, 0, 0, 1),
caption = "Typical-value steady-state AUC24 vs the adult reference values in the Table 2 footnote of Yin 2024.")| Regimen | AUC24 simulated (ng*h/mL) | AUC24 published (ng*h/mL) | Difference (%) |
|---|---|---|---|
| 100 mg b.i.d. | 1032 | 1030 | 0.2 |
| 200 mg b.i.d. | 2373 | 2380 | -0.3 |
| 300 mg b.i.d. | 3863 | 3870 | -0.2 |
PKNCA validation of the PK layer
The published reference exposures (Cmax,ss 1253 ng/mL and Ctrough,ss
27 ng/mL at 300 mg b.i.d.) are typical-value
predictions for the reference patient, so NCA is run on the
typical-value arms. A median across a variable population is not the
same quantity: ka carries very large between-subject
variability (omega 1.02), and slow absorbers have flatter peaks, so the
median individual Cmax sits well below the typical-value Cmax. Comparing
the median against a typical-value reference would look like a model
error when it is a mismatch of estimands.
NCA covers the final 12-h steady-state dosing interval (day 20 to day 20.5).
tau_start <- 24 * 20
nca_window <- sim_typ |>
dplyr::filter(!is.na(Cc), time >= tau_start, time <= tau_start + 12) |>
dplyr::mutate(time = time - tau_start) |>
dplyr::select(id, time, Cc, regimen)
# Guarantee a time = 0 record per (id, regimen) so PKNCA can anchor AUC0-tau.
nca_window <- dplyr::bind_rows(
nca_window,
nca_window |>
dplyr::group_by(id, regimen) |>
dplyr::slice_min(time, n = 1, with_ties = FALSE) |>
dplyr::ungroup() |>
dplyr::mutate(time = 0)
) |>
dplyr::distinct(id, regimen, time, .keep_all = TRUE) |>
dplyr::arrange(id, time)
stopifnot(nrow(nca_window) > 0, all(nca_window$Cc >= 0),
all(table(nca_window$id) > 100))
conc_obj <- PKNCA::PKNCAconc(as.data.frame(nca_window), Cc ~ time | regimen + id)
dose_df <- events_typ |>
dplyr::filter(evid == 1, time == tau_start) |>
dplyr::mutate(time = 0) |>
dplyr::select(id, time, amt, regimen) |>
as.data.frame()
dose_obj <- PKNCA::PKNCAdose(dose_df, amt ~ time | regimen + id)
intervals <- data.frame(
start = 0, end = 12,
cmax = TRUE, tmax = TRUE, auclast = TRUE, cmin = TRUE
)
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals))Comparison against published exposures
The paper reports steady-state exposures for the reference patient at
300 mg b.i.d.: AUCss,24 3848 ng*h/mL, Cmax,ss 1253 ng/mL and Ctrough,ss
27 ng/mL. The AUC24 references for all three dose levels come from the
Table 2 footnote. Cmax and Cmin are computed
over one 12-h interval; at steady state under b.i.d. dosing the profile
repeats every 12 h, so AUC24 is twice the per-interval
auclast.
sim_nca <- as.data.frame(nca_res) |>
dplyr::filter(PPTESTCD %in% c("cmax", "tmax", "auclast", "cmin")) |>
dplyr::select(regimen, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
dplyr::mutate(auc24 = 2 * auclast)
published <- tibble::tribble(
~regimen, ~cmax, ~cmin, ~auc24,
"100 mg b.i.d.", NA, NA, 1030,
"200 mg b.i.d.", NA, NA, 2380,
"300 mg b.i.d.", 1253, 27, 3870
)
nca_cmp <- sim_nca |>
dplyr::select(regimen, cmax, cmin, tmax, auc24) |>
dplyr::left_join(published, by = "regimen", suffix = c("_sim", "_pub")) |>
dplyr::mutate(
auc_pct = 100 * (auc24_sim - auc24_pub) / auc24_pub,
cmax_pct = 100 * (cmax_sim - cmax_pub) / cmax_pub,
cmin_pct = 100 * (cmin_sim - cmin_pub) / cmin_pub
)
nca_cmp |>
dplyr::rename(
"Regimen" = regimen,
"Cmax simulated (ng/mL)" = cmax_sim,
"Cmax published (ng/mL)" = cmax_pub,
"Cmax difference (%)" = cmax_pct,
"Ctrough simulated (ng/mL)" = cmin_sim,
"Ctrough published (ng/mL)" = cmin_pub,
"Ctrough difference (%)" = cmin_pct,
"Tmax simulated (h)" = tmax,
"AUC24 simulated (ng*h/mL)" = auc24_sim,
"AUC24 published (ng*h/mL)" = auc24_pub,
"AUC24 difference (%)" = auc_pct
) |>
knitr::kable(digits = 1,
caption = "Typical-value steady-state exposures from PKNCA vs the published values in Yin 2024.")| Regimen | Cmax simulated (ng/mL) | Ctrough simulated (ng/mL) | Tmax simulated (h) | AUC24 simulated (ng*h/mL) | Cmax published (ng/mL) | Ctrough published (ng/mL) | AUC24 published (ng*h/mL) | AUC24 difference (%) | Cmax difference (%) | Ctrough difference (%) |
|---|---|---|---|---|---|---|---|---|---|---|
| 100 mg b.i.d. | 405.1 | 8.0 | 0.4 | 1031.0 | NA | NA | 1030 | 0.1 | NA | NA |
| 200 mg b.i.d. | 830.5 | 16.3 | 0.5 | 2371.1 | NA | NA | 2380 | -0.4 | NA | NA |
| 300 mg b.i.d. | 1251.6 | 24.8 | 0.5 | 3861.1 | 1253 | 27 | 3870 | -0.2 | -0.1 | -8.2 |
# AUC and Cmax reproduce the published typical values closely. Ctrough is the
# most demanding of the three -- it is a single point at the end of the
# interval, an order of magnitude smaller than Cmax, and the published 27 ng/mL
# is quoted to two significant figures.
stopifnot(
max(abs(nca_cmp$auc_pct)) < 2,
abs(nca_cmp$cmax_pct[nca_cmp$regimen == "300 mg b.i.d."]) < 2,
abs(nca_cmp$cmin_pct[nca_cmp$regimen == "300 mg b.i.d."]) < 15
)The AUC prediction interval pins the between-subject-variability scale
Steady-state AUC depends on between-subject variability only through
etalfdepot, so the 90% prediction interval of AUC
identifies omega_F1 directly. This is the check that
settles how Table 1a’s BSV column should be read (see Errata).
Steady-state AUC is log-normal here, so omega_F1 is
recovered as the SD of log(AUC). That estimator is used
rather than the empirical 5th/95th percentiles because sample quantiles
from a few hundred draws are badly noisy: the same cohort across five
seeds gave 90% PI ratios spanning 4.3-6.6 but sd(log(AUC))
spanning only 0.45-0.56.
auc_ind <- sim_bsv |>
dplyr::filter(!is.na(Cc), time >= 24 * 20, time <= 24 * 20 + 12) |>
dplyr::group_by(regimen, id) |>
dplyr::summarise(auc = sum(diff(time) * (head(Cc, -1) + tail(Cc, -1)) / 2),
.groups = "drop")
# omega implied by the published Table 2 footnote prediction intervals.
published_ratio <- c(2470 / 432, 5700 / 995, 9280 / 1620)
auc_bsv <- auc_ind |>
dplyr::group_by(regimen) |>
dplyr::summarise(omega_hat = sd(log(auc)), .groups = "drop") |>
dplyr::mutate(
pi_ratio_implied = exp(2 * qnorm(0.95) * omega_hat),
omega_published = log(published_ratio) / (2 * qnorm(0.95)),
pi_ratio_published = published_ratio
)
auc_bsv |>
dplyr::rename(
"Regimen" = regimen,
"omega_F1 from sd(log AUC), simulated" = omega_hat,
"Implied 90% PI ratio, simulated" = pi_ratio_implied,
"omega_F1 implied by the paper" = omega_published,
"Published 90% PI ratio" = pi_ratio_published
) |>
knitr::kable(digits = 3,
caption = "Between-subject variability in steady-state AUC, simulated vs the Table 2 footnote of Yin 2024.")| Regimen | omega_F1 from sd(log AUC), simulated | Implied 90% PI ratio, simulated | omega_F1 implied by the paper | Published 90% PI ratio |
|---|---|---|---|---|
| 100 mg b.i.d. | 0.555 | 6.211 | 0.530 | 5.718 |
| 200 mg b.i.d. | 0.485 | 4.933 | 0.531 | 5.729 |
| 300 mg b.i.d. | 0.554 | 6.186 | 0.531 | 5.728 |
# This is the discriminating test between the two readings of Table 1a's BSV
# column. The packaged value is omega_F1 = 0.527 (the table value read as an
# SD). Had it been a variance, omega_F1 would be sqrt(0.527) = 0.726.
stopifnot(
# consistent with the SD reading, and with the paper's own implied ~0.530
all(abs(auc_bsv$omega_hat - 0.527) < 0.1),
# and decisively inconsistent with the variance reading
all(auc_bsv$omega_hat < 0.65)
)Endogenous-layer checks (24HC turnover)
A turnover biomarker is not a PKNCA target, so the 24HC layer is validated with the steady-state, perturbation-recovery and flux checks appropriate to an indirect-response model.
Steady-state hold
With no drug, hc24 must sit at its individual baseline
indefinitely.
ev_nodose <- cbind(
obs_records(seq(0, 24 * 60, by = 6)),
ref_covariates(300)[rep(1, length(seq(0, 24 * 60, by = 6))), ],
row.names = NULL
)
ss <- rxode2::rxSolve(rxode2::zeroRe(mod), ev_nodose, useLinCmt = FALSE) |>
as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalq', 'etalvp', 'etalfdepot', 'etalrbase', 'etalic50'
rbase_ref <- unique(round(ss$rbase, 10))
cat("Typical baseline 24HC for a 45-year-old, 70 kg patient (ng/mL):",
format(rbase_ref, digits = 6), "\n")
#> Typical baseline 24HC for a 45-year-old, 70 kg patient (ng/mL): 47.5317
cat("Range of hc24 over 60 days with no dose (ng/mL):",
format(range(ss$hc24), digits = 10), "\n")
#> Range of hc24 over 60 days with no dose (ng/mL): 47.53169965 47.53169965
# No drift over 60 days.
stopifnot(diff(range(ss$hc24)) < 1e-6,
abs(mean(ss$hc24) - rbase_ref) < 1e-6)The reference subject is 45 years old, above the 17.5-year
breakpoint, so the age factor is exactly 1 and the baseline is
50.5 * (20/20)^0.215 * (70/55.25)^-0.256.
Perturbation recovery
Displacing hc24 away from baseline must return it
monotonically to baseline. The displacement is applied as a bolus into
the hc24 state rather than via
rxSolve(inits = ), because the model sets
hc24(0) <- rbase inside model() and an
initial condition declared there overrides the inits
argument.
perturb_arm <- function(mult) {
delta <- (mult - 1) * rbase_closed # signed bolus onto the baseline
ev <- rbind(
data.frame(time = 0, amt = delta, evid = 1L, cmt = "hc24", dvid = NA_integer_),
obs_records(seq(0, 24 * 60, by = 6))
)
ev <- ev[order(ev$time, -ev$evid), ]
ev <- cbind(ev, ref_covariates(300)[rep(1, nrow(ev)), ], row.names = NULL)
s <- rxode2::rxSolve(rxode2::zeroRe(mod), ev, useLinCmt = FALSE) |> as.data.frame()
data.frame(mult = mult, time = s$time, hc24 = s$hc24)
}
recover <- dplyr::bind_rows(lapply(c(0.5, 2.0), perturb_arm))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalq', 'etalvp', 'etalfdepot', 'etalrbase', 'etalic50'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etalfdepot
#> as a work-around try putting the mu-referenced expression on a simple line
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalq', 'etalvp', 'etalfdepot', 'etalrbase', 'etalic50'
# The bolus must actually have displaced the state to the intended multiple --
# a perturbation test that silently starts at baseline proves nothing.
start_vals <- recover |>
dplyr::group_by(mult) |>
dplyr::summarise(start = dplyr::first(hc24), .groups = "drop")
stopifnot(all(abs(start_vals$start - start_vals$mult * rbase_closed) < 1e-6))
ggplot(recover, aes(time / 24, hc24, colour = factor(mult))) +
geom_line() +
geom_hline(yintercept = rbase_closed, linetype = "dashed") +
labs(x = "Time (days)", y = "24HC (ng/mL)", colour = "Initial / baseline",
title = "Perturbation recovery to the reported baseline")
endpoints <- recover |>
dplyr::group_by(mult) |>
dplyr::summarise(final = dplyr::last(hc24), .groups = "drop")
stopifnot(all(abs(endpoints$final - rbase_closed) < 1e-3))
# Monotone approach in both directions.
stopifnot(
all(diff(recover$hc24[recover$mult == 0.5]) > 0),
all(diff(recover$hc24[recover$mult == 2.0]) < 0)
)The observed recovery half-life must equal
log(2) / kout.
# Log-linear decay of the displacement from baseline. The window is capped at
# 350 h (~10 half-lives) so the fit is not contaminated by solver noise once the
# displacement has decayed below the integrator's absolute tolerance.
low <- recover |>
dplyr::filter(mult == 0.5, time <= 350) |>
dplyr::mutate(disp = rbase_closed - hc24) |>
dplyr::filter(disp > 1e-6)
stopifnot(nrow(low) > 10)
fit <- lm(log(disp) ~ time, data = low)
t_half_obs <- -log(2) / coef(fit)[[2]]
t_half_exp <- log(2) / 0.0199
cat(sprintf("Recovery half-life: observed %.3f h, expected log(2)/kout = %.3f h\n",
t_half_obs, t_half_exp))
#> Recovery half-life: observed 34.832 h, expected log(2)/kout = 34.832 h
stopifnot(abs(t_half_obs - t_half_exp) < 0.01)Flux balance at steady state
At baseline, synthesis and degradation must cancel exactly. Under
maximal inhibition the model’s floor for 24HC is
rbase * (1 - Imax/100).
kout_ref <- 0.0199
kin_ref <- rbase_closed * kout_ref
cat(sprintf("kin = rbase * kout = %.6f * %.4f = %.6f (ng/mL)/h\n",
rbase_closed, kout_ref, kin_ref))
#> kin = rbase * kout = 47.531700 * 0.0199 = 0.945881 (ng/mL)/h
cat(sprintf("kout * hc24 at baseline = %.6f (ng/mL)/h\n", kout_ref * rbase_closed))
#> kout * hc24 at baseline = 0.945881 (ng/mL)/h
stopifnot(abs(kin_ref - kout_ref * rbase_closed) < 1e-12)
# Asymptotic floor under complete saturation of the inhibitory sigmoid.
floor_pct <- -92
cat(sprintf("Asymptotic maximal 24HC change from baseline: %.1f%%\n", floor_pct))
#> Asymptotic maximal 24HC change from baseline: -92.0%
# The simulated steady-state inhibition must sit above (i.e. be less negative
# than) the Imax floor at every dose.
stopifnot(all(pd_summary$hc24_med > floor_pct))Assumptions and deviations
Errata – the reported scale of the between-subject variability
The footnote to Table 1 states “Reported on variance scale”
for the rows marked a in both panels. That
is correct for panel (b) but not for panel (a), and
this model file ships panel (a)’s values as standard deviations. Three
independent lines of evidence:
-
The paper’s own simulations. Steady-state AUC
carries between-subject variability only through
etalfdepot(BSV on CL is fixed to zero), so the Table 2 footnote’s 90% prediction intervals identifyomega_F1exactly:log(2470/432) / (2 x 1.645) = 0.530, and the same ratio recurs at all three dose levels. Table 1a reports 0.527. Reading it as a variance would implyomega_F1 = 0.726and a PI ratio near 10.9 – roughly twice the published width. Thebsv-scalechunk above re-derives this from the packaged model. -
The control stream.
0.527^2 = 0.2777reproduces the popPK$OMEGAentry for F1 (0.2779) to four significant digits; the same holds for Q (0.436^2 = 0.190vs0.1857) and Vp (0.625^2 = 0.391vs0.3894). Every non-kafixed effect in that run moved by under 5% from its initial value, so a doubling of three variance components is not credible. -
Panel (b) genuinely is on the variance scale, and
reading it as an SD is not: the baseline-24HC BSV of 0.0811 is within
11% of the PK/EO/PD
$OMEGAinitial 0.09074, whereas an SD reading would require the variance to have fallen 14-fold during a re-estimation that only removed 17 outlier observations.
The file therefore stores etalka = 1.02^2,
etalq = 0.436^2, etalvp = 0.625^2 and
etalfdepot = 0.527^2 (panel a, SD-scale source) and
etalrbase = 0.0811, etalic50 = 0.636 (panel b,
variance-scale source).
Other assumptions and deviations
- One model file, two NONMEM runs. The PK/EO/PD run reads individual PK parameters from data columns rather than estimating them, so it is not self-contained. The packaged model reproduces the PK layer from Table 1a and couples it to the EO and 24HC layers from Table 1b, which is the only form in which the paper’s PK/EO/PD model can be simulated without the original data set.
-
Enzyme-occupancy parameters are inherited, not
re-estimated.
ke0,Emax,EC50and the EO shape parameter carryFIXin the PK/EO/PD control stream and originate in Yin 2023; they arefixed()here and do not appear in Table 1. -
V3~ASIANis omitted. The control stream carries an Asian-descent effect on peripheral volume with coefficient0 FIX, making the term an exact no-op; it has no row in Table 1a. It is documented undercovariatesDataExcludedrather than encoded as a fixed-zero multiplier. -
CRUSHEDis omitted. It appears in$INPUTand$TABLEbut is never referenced in$PKor$ERRORof either final model. -
The 24HC residual error is purely additive. Its
proportional component is
0 FIXin the control stream, so onlyaddSd_hc24is carried. -
AGP is kept in mg/dL. The
AAGregister default is g/L, but the effect enters as the ratio(AAG/20)^exponentin which the unit cancels; keeping the paper’s mg/dL makes the printed reference value 20 directly checkable. The equivalent canonical value is 0.2 g/L. -
DIS_HEALTHYcarries the patient effect on its complement. The source column isPATIENT(1 = patient). The published typical CL of 4.2 L/h is the healthy-volunteer value, so the model writes(1 + e_dis_healthy_cl/100 * (1 - DIS_HEALTHY))to preserve that anchor. -
etalfdepotis not mu-referenced. Because the AGP-missing flag scales the eta itself (exp((1 + theta * AAG_MISSING) * eta)), rxode2 warns that the eta defaults to non-mu-referenced. This reproduces the published structure and is correct for simulation; it would matter only for a re-fit. - Reference subject weight. The paper’s covariate-analysis reference subject weighs 56.6 kg, but its reported steady-state exposures (AUCss,24 3848 ng*h/mL at 300 mg b.i.d.) match the 70 kg reference patient used for the dosing simulations, not the 56.6 kg one – 3848 sits alongside Table 2’s adult reference 3870 and its 45-100 kg row value 3850. The vignette uses the 70 kg reference patient throughout.
- Virtual cohorts, not observed data. Individual data are available only on request (Data Availability Statement), so all figures use virtual subjects at the published reference covariates.