Skip to contents

Model and source

This paper contributes two models, extracted as two files because the authors fitted two separate final models on two different cohorts (the Phase I fit was used as a Bayesian prior for the Phase II fit, not merged with it):

  • DiDeo_2025_tideglusib_healthy – Phase I study NP031112-07A03, elderly healthy subjects (n = 54, 1832 samples, rich sampling).
  • DiDeo_2025_tideglusib_dm1 – Phase II study AMO-02-MD-2-001, adolescent and adult DM-1 patients (n = 16, 51 samples, sparse sampling), estimated with NONMEM $PRIOR NWPRI using the healthy-subject model as the prior.
#> ℹ parameter labels from comments will be replaced by 'label()'
  • DiDeo_2025_tideglusib_healthy – Two-compartment population PK model with first-order absorption and dose-dependent bioavailability for tideglusib in elderly healthy subjects (Phase I study NP031112-07A03)
#> ℹ parameter labels from comments will be replaced by 'label()'
  • DiDeo_2025_tideglusib_dm1 – Two-compartment population PK model with first-order absorption and dose-dependent bioavailability for tideglusib in adolescent and adult patients with congenital or juvenile-onset myotonic dystrophy type 1 (Phase II study AMO-02-MD-2-001), estimated using the elderly healthy subject model as a Bayesian prior

  • Citation: Di Deo A, Oosterholt S, Horrigan J, Evans S, McMorn A, Della Pasqua O. Population Pharmacokinetics of Tideglusib in Congenital and Childhood Myotonic Dystrophy Type 1: Influence of Demographic and Clinical Factors on Systemic Exposure. Pharmaceutics. 2025;17(8):1065. doi:10.3390/pharmaceutics17081065. Estimated with NONMEM $PRIOR NWPRI using the healthy-elderly-subject model of the same paper as the prior; see modellib(‘DiDeo_2025_tideglusib_healthy’).

  • Article: https://doi.org/10.3390/pharmaceutics17081065

  • Supplement: https://www.mdpi.com/article/10.3390/pharmaceutics17081065/s1

Both models share one structure: two-compartment disposition with first-order oral absorption, allometric body-weight scaling on all four disposition parameters, and a dose-dependent absorption rate constant and relative bioavailability that switch at a 400 mg per-administration threshold.

Population

Phase I (NP031112-07A03). 72 elderly subjects were randomised across six dose groups (12 per group, 9 active : 3 placebo); the 54 who received active tideglusib contributed 1832 plasma samples. Age 60-74 years (mean 64.3, s.d. 3.5), body weight 50.7-98.1 kg (mean 74.5, s.d. 9.9), 24 of 54 female. Dose groups were 300 mg b.i.d., 400 mg b.i.d., and 600, 800, 1000 and 1200 mg q.d. as an oral suspension of formulation F06-037F, given as a single dose, a 48 h washout, then 14 days of repeat dosing (Di Deo 2025 Section 2.1, Section 2.2 and Table 1, first six rows).

Phase II (AMO-02-MD-2-001). 16 adolescent and adult patients with congenital or juvenile-onset DM-1, aged 13.8-34.9 years (mean 20.9, s.d. 5.9), body weight 36.8-122.6 kg (mean 63.6, s.d. 19.6), 6 of 16 female, randomised 1:1 to 400 mg or 1000 mg q.d. for 12 weeks after a 2-week single-blind placebo run-in. Sampling was sparse – pre-dose and 2-4 h post-dose at weeks 2 and 12 – giving 51 evaluable samples, 9 of them below the 1 ng/mL LLOQ and all 9 pre-dose troughs (Section 2.2 and Table 1, last two rows).

The same information is available programmatically via each model’s population metadata (readModelDb("<model>")()$population).

Source trace

Every ini() entry carries an in-file comment naming its source location; the table below collects them. All parameter values come from Di Deo 2025 Table 2, which reports both fits side by side.

Table 2 column order. The table’s title reads “in healthy elderly subjects and DM-1 patients”, but its columns are ordered AMO-02-MD-2-001 (DM-1 patients) first, then NP031112-07A03 (healthy elderly). The column headers are correct as printed; the title order is not the column order. This was confirmed against the paper’s own precision claims: Section 3.1 reports the healthy fit as “%RSE < 27.4%” (fixed effects) and “< 41.1%” (IIV), exactly the maxima of the NP031112-07A03 RSE column, while the DM-1 claims (“< 23%”, “< 32%”) match the AMO-02-MD-2-001 maxima (22.1 and 31.6). Independently, the Discussion’s headline figures – “apparent systemic clearance of 341 L/h” and an apparent steady-state volume of “1140 L” (= V2 + V3 = 154 + 986) – are the AMO-02-MD-2-001 column.

Parameter Healthy (NP031112-07A03) DM-1 (AMO-02-MD-2-001) Source location
lka log(0.78) log(0.767) Table 2, “Absorption rate constant, kA (1/h)”
lka_highdose log(0.61) log(0.609) Table 2, “kA (dose > 400 mg) (1/h)”
lcl log(327) log(341) Table 2, “Clearance, CL (L/h)”
lvc log(152) log(154) Table 2, “Volume of distribution central compartment, V2 (L)”
lq log(66.1) log(66.1) Table 2, “Intercompartmental clearance, Q (L/h)”
lvp log(1010) log(986) Table 2, “Volume of distribution peripheral compartment, V3 (L)”
lfdepot fixed(log(1)) fixed(log(1)) Table 2, “Bioavailability (doses <= 400 mg)” = 1, FIXED
lfdepot_highdose log(0.85) log(0.88) Table 2, “Bioavailability (doses > 400 mg)”
e_wt_cl_q fixed(0.75) fixed(0.75) Section 2.4.1, “fixed allometric exponents on clearance (0.75)”
e_wt_vc_vp fixed(1) fixed(1) Section 2.4.1, “…and volumes (1)”
etalcl 0.18 0.186 Table 2, “eta CL variance”
etalvc 0.33 0.321 Table 2, “eta V2 variance”
etalq 0.96 1.15 Table 2, “eta Q variance”
etalvp 0.90 1.01 Table 2, “eta V3 variance”
etalka 0.06 0.069 Table 2, “eta KA variance”
propSd sqrt(0.56) sqrt(0.54) Table 2, “Proportional error (doses <= 400 mg)”
propSd_highdose sqrt(0.45) sqrt(0.46) Table 2, “Proportional error (doses > 400 mg)”
Allometry (WT/70)^exp n/a n/a Section 2.4.1, reference weight 70 kg
2-compartment ODEs, first-order absorption n/a n/a Figure 1 schematic; Section 3.1

Two readings that had to be settled from the source

The IIV column is a VARIANCE, not a standard deviation. Table 2 prints “Population Estimate (CV%)”. Taking the DM-1 column, whose values are given to three significant figures, CV% = sqrt(variance) * 100 reproduces every printed CV to the digit, whereas the log-normal form sqrt(exp(variance) - 1) * 100 does not:

v          <- c(etalcl = 0.186, etalq = 1.15, etalvp = 1.01,
                etalvc = 0.321, etalka = 0.069)
printed_cv <- c(43.1, 107.2, 100.5, 56.7, 26.4)      # Table 2, DM-1 column
data.frame(
  variance          = v,
  `sqrt(var)*100`   = round(sqrt(v) * 100, 1),
  `sqrt(exp(v)-1)`  = round(sqrt(exp(v) - 1) * 100, 1),
  printed_cv        = printed_cv,
  check.names       = FALSE
)
#>        variance sqrt(var)*100 sqrt(exp(v)-1) printed_cv
#> etalcl    0.186          43.1           45.2       43.1
#> etalq     1.150         107.2          146.9      107.2
#> etalvp    1.010         100.5          132.1      100.5
#> etalvc    0.321          56.7           61.5       56.7
#> etalka    0.069          26.3           26.7       26.4
# The sqrt(variance) column matches the printed CV%; the log-normal one does not.
stopifnot(max(abs(sqrt(v) * 100 - printed_cv)) < 0.2)

Table 2’s footnote b prints the formula as CV = sqrt(exp(Omega^2)) x 100, which does not reproduce its own column; the arithmetic that does is sqrt(variance). nlmixr2 takes variances on the ini() diagonal, so the printed values are used directly.

The residual-error rows are variances too. Table 2’s abbreviation footnote defines sigma = residual variance, and the IIV rows of the same table are demonstrably variances (above), so the residual values are converted to the standard deviations nlmixr2 expects: propSd <- sqrt(0.54). The sqrt() is kept inline in ini() so the printed value stays visible in the source trace.

Virtual cohort

Original observed data are not publicly available. The cohorts below are virtual populations whose body-weight distributions approximate the published per-arm medians and ranges (Table 1). Each cohort is 100-200 subjects, within the 200-per-arm cap.

# set.seed() seeds R's RNG. It does NOT seed rxode2's simulation RNG, and
# rxode2's streams are partitioned PER SOLVER THREAD -- so this cohort is
# reproducible on this machine and different on a machine with a different
# thread count. Every assertion below is written to hold for ANY cohort the
# model can produce (see known-vignette-failure-patterns.md pattern 12).
set.seed(20250216)
rxode2::rxSetSeed(20250216)

# Observation grid: dense through the absorption/peak phase (Tmax ~ 0.7 h) so
# the trapezoidal AUC and Cmax are resolved, coarser thereafter.
grid_24 <- sort(unique(c(seq(0, 4, by = 0.05), seq(4, 24, by = 0.25))))
grid_12 <- grid_24[grid_24 <= 12]

# Build one steady-state cohort. Doses run to steady state (>= 240 h of
# dosing) and observations cover only the final dosing interval, with time
# re-based so that t = 0 is the last dose.
make_cohort <- function(n, dose, ii, wt, label, model, id_offset = 0L) {
  # 720 h of dosing, not 240: IIV on Q and V3 is ~100% CV, so subjects in the
  # tail of the peripheral-distribution etas have terminal half-lives far
  # longer than the typical ~12 h and are not at steady state after 240 h.
  n_dose <- ceiling(720 / ii)
  start  <- n_dose * ii
  grid   <- if (ii == 12) grid_12 else grid_24
  dosing <- tibble(
    id = id_offset + seq_len(n), time = 0, amt = dose, evid = 1L,
    cmt = "depot", ii = ii, addl = n_dose
  )
  obs <- tidyr::crossing(
    id = id_offset + seq_len(n), time = start + grid
  ) |>
    mutate(amt = NA_real_, evid = 0L, cmt = "central", ii = 0, addl = 0L)
  bind_rows(dosing, obs) |>
    left_join(
      tibble(id = id_offset + seq_len(n), WT = wt), by = "id"
    ) |>
    mutate(
      DOSE_HIGH = as.numeric(dose > 400),
      treatment = label, model = model, dose_mg = dose, tau = ii,
      t_start = start
    ) |>
    arrange(id, time, desc(evid))
}

# Log-normal weights matched to each arm's reported median (Table 1).
lnwt <- function(n, med, sdlog) exp(rnorm(n, log(med), sdlog))

n_arm <- 100L
events <- bind_rows(
  make_cohort(n_arm,  400, 12, lnwt(n_arm, 77.1, 0.15), "Healthy 400 mg b.i.d.",
              "DiDeo_2025_tideglusib_healthy", id_offset =   0L),
  make_cohort(n_arm, 1000, 24, lnwt(n_arm, 72.5, 0.15), "Healthy 1000 mg q.d.",
              "DiDeo_2025_tideglusib_healthy", id_offset = 100L),
  make_cohort(n_arm,  400, 24, lnwt(n_arm, 60.3, 0.24), "DM-1 400 mg q.d.",
              "DiDeo_2025_tideglusib_dm1",     id_offset = 200L),
  make_cohort(n_arm, 1000, 24, lnwt(n_arm, 58.7, 0.30), "DM-1 1000 mg q.d.",
              "DiDeo_2025_tideglusib_dm1",     id_offset = 300L)
)

# Disjoint-ID guard: duplicate ids across cohorts silently merge into a single
# subject receiving the summed dose.
stopifnot(!anyDuplicated(unique(events[, c("id", "time", "evid")])))
stopifnot(n_distinct(events$id) == 4L * n_arm)

Simulation

Each model is solved over its own arms, then the two are combined.

solve_arms <- function(model_name) {
  ev <- events |> filter(model == model_name)
  if (nrow(ev) == 0L) return(NULL)
  rxode2::rxSolve(
    readModelDb(model_name), events = ev,
    keep = c("treatment", "model", "dose_mg", "tau", "t_start",
             "WT", "DOSE_HIGH")
  ) |>
    as.data.frame()
}

sim <- bind_rows(lapply(
  c("DiDeo_2025_tideglusib_healthy", "DiDeo_2025_tideglusib_dm1"),
  solve_arms
)) |>
  mutate(
    id   = as.integer(as.character(id)),
    t    = time - t_start,      # time since the last (steady-state) dose
    Cng  = Cc * 1000            # model is mg/L == ug/mL; paper reports ng/mL
  ) |>
  filter(t >= 0)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'

# The solve must produce real concentrations for every arm -- guard against a
# silently empty or all-NA result (a gate that cannot go red is worse than none).
stopifnot(nrow(sim) > 0, !all(is.na(sim$Cc)))
stopifnot(n_distinct(sim$treatment) == 4L)
stopifnot(all(tapply(sim$Cng, sim$treatment, function(x) any(x > 0))))
stopifnot(all(sim$Cng >= 0))

Structural check: the dose-dependent switches fire

DOSE_HIGH gates three quantities at once. ka is returned per subject by the solver, so the switch can be read directly off the output.

sim |>
  group_by(treatment, DOSE_HIGH) |>
  summarise(ka_min = min(ka), ka_max = max(ka), .groups = "drop") |>
  knitr::kable(digits = 3, caption = "Absorption rate constant by dose group.")
Absorption rate constant by dose group.
treatment DOSE_HIGH ka_min ka_max
DM-1 1000 mg q.d. 1 0.307 1.161
DM-1 400 mg q.d. 0 0.402 1.554
Healthy 1000 mg q.d. 1 0.338 0.992
Healthy 400 mg b.i.d. 0 0.425 1.469

# Typical-value ka must equal the two printed Table 2 values exactly.
ka_tv <- function(model, dose) {
  m0 <- rxode2::zeroRe(readModelDb(model))
  ev <- rxode2::et(amt = dose, cmt = "depot") |> rxode2::et(c(0, 1))
  s  <- rxode2::rxSolve(m0, ev, params = data.frame(
    WT = 70, DOSE_HIGH = as.numeric(dose > 400)), addDosing = FALSE)
  unique(as.data.frame(s)$ka)
}
stopifnot(
  abs(ka_tv("DiDeo_2025_tideglusib_dm1",      400) - 0.767) < 1e-6,
  abs(ka_tv("DiDeo_2025_tideglusib_dm1",     1000) - 0.609) < 1e-6,
  abs(ka_tv("DiDeo_2025_tideglusib_healthy",  400) - 0.78 ) < 1e-6,
  abs(ka_tv("DiDeo_2025_tideglusib_healthy", 1000) - 0.61 ) < 1e-6
)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'

Structural check: steady-state mass balance pins the fixed bioavailability

At steady state the amount cleared over one dosing interval must equal the absorbed dose: CL * AUCtau = F * dose. It is the cheapest gate that pins the fixed F: F is 1 by fiat at doses <= 400 mg and 0.88 (DM-1) / 0.85 (healthy) above, and a mis-encoded switch, allometric exponent or unit would show up here immediately.

The gate is applied to typical-value profiles. There both sides of the identity use the same parameters and the only discrepancy is integration error, so a tight bound is the correct assertion.

trap <- function(t, c) sum(diff(t) * (head(c, -1) + tail(c, -1)) / 2)

grid_fine <- sort(unique(c(seq(0, 4, by = 0.01), seq(4, 24, by = 0.05))))

mb_typical <- function(model, dose, ii, wt) {
  m0     <- rxode2::zeroRe(readModelDb(model))
  n_dose <- ceiling(1440 / ii)          # long run: guarantee steady state
  start  <- n_dose * ii
  g      <- grid_fine[grid_fine <= ii]
  ev     <- rxode2::et(amt = dose, cmt = "depot", ii = ii, addl = n_dose) |>
    rxode2::et(start + g)
  s <- rxode2::rxSolve(m0, ev, params = data.frame(
    WT = wt, DOSE_HIGH = as.numeric(dose > 400)), addDosing = FALSE) |>
    as.data.frame()
  s$t <- s$time - start
  s   <- s[order(s$t), ]
  fF  <- if (dose > 400) {
    if (model == "DiDeo_2025_tideglusib_dm1") 0.88 else 0.85
  } else 1
  tibble::tibble(
    model = model, dose_mg = dose, tau = ii, WT = wt,
    ratio = unique(s$cl) * trap(s$t, s$Cc) / (fF * dose)
  )
}

mb_grid <- expand.grid(
  model    = c("DiDeo_2025_tideglusib_healthy", "DiDeo_2025_tideglusib_dm1"),
  regimen  = c("400 q12h", "400 q24h", "1000 q24h", "1200 q24h"),
  WT       = c(40, 70, 100),
  stringsAsFactors = FALSE
)
mb_grid$dose <- as.numeric(sub(" .*", "", mb_grid$regimen))
mb_grid$ii   <- as.numeric(sub(".*q([0-9]+)h", "\\1", mb_grid$regimen))

mb_tv <- bind_rows(lapply(seq_len(nrow(mb_grid)), function(i) {
  mb_typical(mb_grid$model[i], mb_grid$dose[i], mb_grid$ii[i], mb_grid$WT[i])
}))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalq', 'etalvp', 'etalka'

mb_tv |>
  mutate(F_used = ifelse(dose_mg > 400,
           ifelse(model == "DiDeo_2025_tideglusib_dm1", 0.88, 0.85), 1)) |>
  group_by(model, dose_mg, tau, F_used) |>
  summarise(min_ratio = min(ratio), max_ratio = max(ratio), .groups = "drop") |>
  knitr::kable(digits = 4,
    caption = "Typical-value steady-state mass balance across body weights 40/70/100 kg: CL * AUCtau / (F * dose). Exactly 1 if the ODEs, allometry, bioavailability switch and units are all consistent.")
Typical-value steady-state mass balance across body weights 40/70/100 kg: CL * AUCtau / (F * dose). Exactly 1 if the ODEs, allometry, bioavailability switch and units are all consistent.
model dose_mg tau F_used min_ratio max_ratio
DiDeo_2025_tideglusib_dm1 400 12 1.00 1 1
DiDeo_2025_tideglusib_dm1 400 24 1.00 1 1
DiDeo_2025_tideglusib_dm1 1000 24 0.88 1 1
DiDeo_2025_tideglusib_dm1 1200 24 0.88 1 1
DiDeo_2025_tideglusib_healthy 400 12 1.00 1 1
DiDeo_2025_tideglusib_healthy 400 24 1.00 1 1
DiDeo_2025_tideglusib_healthy 1000 24 0.85 1 1
DiDeo_2025_tideglusib_healthy 1200 24 0.85 1 1

# Deterministic identity -> tight bound is correct (numerical error only).
stopifnot(max(abs(mb_tv$ratio - 1)) < 0.005)

Across the simulated cohorts the same identity holds at the centre, with a small negative tail: IIV on Q and V3 is ~100% CV, so a handful of subjects per arm have terminal half-lives long enough that even 720 h of dosing leaves them fractionally short of steady state. That is a property of the published variance model, not of the encoding, so the cohort check asserts on robust quantiles rather than the extreme.

mass_balance <- sim |>
  group_by(id, treatment, model, dose_mg, tau) |>
  arrange(t, .by_group = TRUE) |>
  summarise(
    cleared  = first(cl) * trap(t, Cc),                    # mg cleared over tau
    absorbed = first(dose_mg) * ifelse(first(dose_mg) > 400,
                 ifelse(first(model) == "DiDeo_2025_tideglusib_dm1", 0.88, 0.85), 1),
    .groups  = "drop"
  ) |>
  mutate(ratio = cleared / absorbed)

mass_balance |>
  group_by(treatment) |>
  summarise(min_ratio = min(ratio), median_ratio = median(ratio),
            q95_abs_dev = quantile(abs(ratio - 1), 0.95),
            .groups = "drop") |>
  knitr::kable(digits = 4,
    caption = "Cohort steady-state mass balance by arm.")
Cohort steady-state mass balance by arm.
treatment min_ratio median_ratio q95_abs_dev
DM-1 1000 mg q.d. 0.9753 0.9999 0.0027
DM-1 400 mg q.d. 0.9927 0.9998 0.0010
Healthy 1000 mg q.d. 0.9886 0.9998 0.0010
Healthy 400 mg b.i.d. 0.9920 0.9997 0.0021

# Centre: the identity must hold to a tight tolerance in the median subject.
stopifnot(max(abs(tapply(mass_balance$ratio, mass_balance$treatment,
                         median) - 1)) < 0.01)
# Envelope: robust to which subjects land in the long-half-life tail.
stopifnot(quantile(abs(mass_balance$ratio - 1), 0.95) < 0.05)

Replicate published figures

Figure 3 – concentration-time profiles in DM-1 patients by dose group

sim |>
  filter(model == "DiDeo_2025_tideglusib_dm1") |>
  group_by(treatment, t) |>
  summarise(
    Q05 = quantile(Cng, 0.05), Q50 = median(Cng), Q95 = quantile(Cng, 0.95),
    .groups = "drop"
  ) |>
  ggplot(aes(t, Q50)) +
  geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.25) +
  geom_line() +
  facet_wrap(~treatment) +
  scale_y_log10() +
  labs(x = "Time after last dose (h)", y = "Tideglusib (ng/mL)",
       title = "Steady-state profiles in DM-1 patients",
       caption = "Replicates Figure 3 of Di Deo 2025 (median and 90% prediction interval).")

Figure 5 – higher exposure in lighter subjects

Section 3.4 reports that “higher exposure is expected to occur in subjects with lower body weight”. With a fixed dose and allometric clearance this is a structural consequence, so the trend – not a specific slope – is the claim to check.

expo <- sim |>
  group_by(id, treatment, WT) |>
  arrange(t, .by_group = TRUE) |>
  summarise(auc = trap(t, Cng), .groups = "drop")

ggplot(expo, aes(WT, auc)) +
  geom_point(alpha = 0.35, size = 0.9) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE, linewidth = 0.6) +
  facet_wrap(~treatment, scales = "free_y") +
  labs(x = "Body weight (kg)", y = "AUC over the dosing interval (ng*h/mL)",
       title = "Exposure decreases with body weight",
       caption = "Replicates Figure 5 of Di Deo 2025.")


# The claim is a NEGATIVE weight-exposure association in every arm -- a
# structural consequence of allometric CL at a fixed dose -- so the SIGN is
# what this asserts.
#
# The magnitude is not uniform across arms and must not be asserted as if it
# were. Rank correlation is attenuated when body weight varies little relative
# to the IIV on clearance, and the two healthy arms draw weight with sdlog 0.15
# against 0.24-0.30 in the DM-1 arms. Measured over three configurations
# (2 and 8 solver threads, n_arm 100 and 800) the healthy arms realise -0.246
# to -0.319 while the DM-1 arms reach -0.276 to -0.564. An earlier
# `all(rho < -0.3)` therefore failed on the healthy arms for reasons that had
# nothing to do with the model being wrong.
#
# -0.15 holds for every cohort measured, with the smallest observed magnitude
# (0.246) a comfortable margin away, and the gate still goes red if the sign
# flips or the association collapses -- which is the thing worth catching.
rho <- expo |>
  group_by(treatment) |>
  summarise(rho = cor(WT, auc, method = "spearman"), .groups = "drop")
knitr::kable(rho, digits = 3, caption = "Spearman correlation of AUC with body weight.")
Spearman correlation of AUC with body weight.
treatment rho
DM-1 1000 mg q.d. -0.276
DM-1 400 mg q.d. -0.407
Healthy 1000 mg q.d. -0.258
Healthy 400 mg b.i.d. -0.367
stopifnot(all(rho$rho < -0.15))

PKNCA validation

NCA is computed over the final (steady-state) dosing interval, with time re-based so t = 0 is the last dose.

# Filter ONLY on !is.na(Cc): adding `t > 0` or `Cng > 0` would drop the
# time-zero row that PKNCA needs to anchor AUC.
sim_nca <- sim |>
  filter(!is.na(Cng)) |>
  select(id, time = t, Cc = Cng, treatment)

# Guarantee a time-zero record per subject (extravascular pre-dose value).
sim_nca <- bind_rows(
  sim_nca,
  sim_nca |> distinct(id, treatment) |> mutate(time = 0, Cc = 0)
) |>
  distinct(id, treatment, time, .keep_all = TRUE) |>
  arrange(id, treatment, time)

stopifnot(nrow(sim_nca) > 0)

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

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

# The paper reports AUC(0-12) for all four arms of Table 3, so NCA runs over
# 0-12 h. For the b.i.d. arm that is a full dosing interval; for the q.d. arms
# it is the paper's own reporting window.
intervals <- data.frame(
  start = 0, end = 12,
  cmax = TRUE, tmax = TRUE, auclast = TRUE
)

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

# Guard: NCA must have produced rows for all four arms (pattern 10 -- a
# lookup that matches nothing makes every downstream all() vacuously TRUE).
stopifnot(n_distinct(as.data.frame(nca_res)$treatment) == 4L)

Comparison against published NCA (Table 3)

published <- tibble::tribble(
  ~treatment,               ~cmax,  ~tmax, ~auclast,
  "Healthy 400 mg b.i.d.",   504.7,  0.7,    949.7,
  "Healthy 1000 mg q.d.",    702.0,  0.7,   1749.8,
  "DM-1 400 mg q.d.",        513.5,  0.75,  1218.1,
  "DM-1 1000 mg q.d.",      1170.9,  0.7,   3145.7
)

cmp <- nlmixr2lib::ncaComparisonTable(
  simulated     = nca_res,
  reference     = published,
  by            = "treatment",
  units         = c(cmax = "ng/mL", tmax = "h", auclast = "ng*h/mL"),
  tolerance_pct = 20
)
knitr::kable(
  cmp,
  caption = "Simulated vs. published NCA (Di Deo 2025 Table 3). * differs from reference by >20%."
)
Simulated vs. published NCA (Di Deo 2025 Table 3). * differs from reference by >20%.
NCA parameter treatment Reference Simulated % diff
Cmax (ng/mL) Healthy 400 mg b.i.d. 505 453 -10.3%
Cmax (ng/mL) Healthy 1000 mg q.d. 702 797 +13.5%
Cmax (ng/mL) DM-1 400 mg q.d. 514 475 -7.6%
Cmax (ng/mL) DM-1 1000 mg q.d. 1170 937 -20.0%
Tmax (h) Healthy 400 mg b.i.d. 0.7 0.625 -10.7%
Tmax (h) Healthy 1000 mg q.d. 0.7 0.7 +0.0%
Tmax (h) DM-1 400 mg q.d. 0.75 0.7 -6.7%
Tmax (h) DM-1 1000 mg q.d. 0.7 0.7 +0.0%
AUClast (ng*h/mL) Healthy 400 mg b.i.d. 950 1150 +21.3%*
AUClast (ng*h/mL) Healthy 1000 mg q.d. 1750 2360 +35.0%*
AUClast (ng*h/mL) DM-1 400 mg q.d. 1220 1230 +1.3%
AUClast (ng*h/mL) DM-1 1000 mg q.d. 3150 2710 -14.0%

The DM-1 400 mg arm reproduces Table 3 closely. The other three arms deviate, and the deviations are not in a consistent direction – the healthy arms run high while the DM-1 1000 mg arm runs low – which rules out a systematic transcription or unit error in the model. See “Assumptions and deviations” for the diagnosis; no parameter was tuned to close these gaps.

The reference exposure of Section 2.7 is reproduced

Section 2.7 specifies a well-defined large simulation: adults of body weight 70-75 kg receiving 1000 mg q.d. under fasting conditions, reported as AUC(0-24) = 2560.8 (1282.5-5286.7) ng/mL*h and Cmax = 929.6 (446.4-1834.4) ng/mL. Unlike the Table 3 arms this is a specified virtual cohort rather than a median over 8-9 real subjects, so it is the more reliable target.

set.seed(4242)
n_ref  <- 200L
ref_ev <- make_cohort(n_ref, 1000, 24, runif(n_ref, 70, 75),
                      "Reference adult 70-75 kg", "DiDeo_2025_tideglusib_dm1",
                      id_offset = 900L)

ref <- rxode2::rxSolve(readModelDb("DiDeo_2025_tideglusib_dm1"),
                       events = ref_ev, keep = c("treatment", "t_start")) |>
  as.data.frame() |>
  mutate(id = as.integer(as.character(id)), t = time - t_start,
         Cng = Cc * 1000) |>
  filter(t >= 0)
#> ℹ parameter labels from comments will be replaced by 'label()'

ref_pk <- ref |>
  group_by(id) |>
  arrange(t, .by_group = TRUE) |>
  summarise(cmax = max(Cng), auc24 = trap(t, Cng), .groups = "drop")

ref_tab <- tibble::tibble(
  Parameter = c("AUC(0-24) (ng*h/mL)", "Cmax (ng/mL)"),
  Simulated = c(sprintf("%.1f (%.1f-%.1f)", median(ref_pk$auc24),
                        quantile(ref_pk$auc24, .05), quantile(ref_pk$auc24, .95)),
                sprintf("%.1f (%.1f-%.1f)", median(ref_pk$cmax),
                        quantile(ref_pk$cmax, .05), quantile(ref_pk$cmax, .95))),
  Published = c("2560.8 (1282.5-5286.7)", "929.6 (446.4-1834.4)"),
  `% diff (median)` = c(
    sprintf("%+.1f%%", 100 * (median(ref_pk$auc24) - 2560.8) / 2560.8),
    sprintf("%+.1f%%", 100 * (median(ref_pk$cmax) -  929.6) /  929.6))
)
knitr::kable(ref_tab, caption = "Section 2.7 reference adult exposure, median (5th-95th percentile).")
Section 2.7 reference adult exposure, median (5th-95th percentile).
Parameter Simulated Published % diff (median)
AUC(0-24) (ng*h/mL) 2393.2 (1220.3-4306.8) 2560.8 (1282.5-5286.7) -6.5%
Cmax (ng/mL) 736.6 (431.3-1457.5) 929.6 (446.4-1834.4) -20.8%

# AUC is the exposure metric the paper's dose rationale is built on, and it is
# insensitive to the unreported residual-error treatment (see deviations
# below): with and without residual error it moved only 2422 -> 2446 in
# development. Realised median % differences of -4.1 / -5.2 / -3.6 across
# thread counts; 20 leaves headroom while still breaking on a mis-transcribed
# clearance, dose or unit, which move AUC by tens of percent.
stopifnot(abs(100 * (median(ref_pk$auc24) - 2560.8) / 2560.8) < 20)
# Envelope check, robust to which subjects land in the tails.
stopifnot(median(ref_pk$auc24) > 1500, median(ref_pk$auc24) < 4000)

Assumptions and deviations

  • Body-weight distributions are assumed. The paper reports only per-arm median and range (Table 1), not the full distribution. Each arm’s weights are drawn log-normally about the published median with a spread chosen to approximate the published range. The Section 2.7 reference cohort is the exception – the paper specifies a 70-75 kg uniform range, so no assumption is needed there, which is why it is the primary quantitative gate.

  • Concentration units. Both models are parameterised in the paper’s own units (dose mg, CL L/h, V L), so Cc is natively mg/L (= ug/mL). The paper reports ng/mL, so the vignette multiplies by 1000 rather than hardcoding a conversion constant in model().

  • Table 3 is reproduced for the DM-1 400 mg arm and deviates for the other three; the model was not tuned to close the gaps. Three findings support leaving the model as the parameter table specifies:

    1. Steady-state mass balance is exact (CL * AUCtau / (F * dose) = 1.000 to four decimals, above). The ODE system, allometric scaling, bioavailability switch and units are therefore internally consistent, and the encoded clearance is exactly Table 2’s.
    2. The deviations have opposite signs. The healthy arms simulate higher AUC(0-12) than Table 3 while the DM-1 1000 mg arm simulates lower (see the comparison table above; at typical values and each arm’s median weight the discrepancies are +19.8%, +35.0%, +1.1% and -12.3% for the four arms in Table 3 order). A transcription, unit or structural error would bias every arm the same way.
    3. Table 3’s healthy rows are internally inconsistent with Table 2. For the healthy 400 mg b.i.d. arm, Table 3’s own Css of 79.2 ng/mL implies AUC(0-24) = 1901 ngh/mL, hence an apparent clearance of 800 mg / 1.901 mgh/L = 421 L/h. Table 2’s healthy CL is 327 L/h, which is 351 L/h at that arm’s median 77.1 kg. The two published tables imply clearances differing by ~20%. Table 2 is the parameter table and is what the model encodes.

    A contributing factor for all four arms is that Table 3’s medians are taken over only 8-9 subjects carrying very large IIV (Q and V3 have 95-107% CV), so each published median is itself an imprecise statistic, and the median of a cohort is not the typical-value prediction when IIV is that large.

  • Cmax depends on an unreported simulation detail, so it is reported but not gated. Section 2.5 says secondary parameters were derived by NCA from “simulated individual concentration vs. time profiles” without stating whether residual error was included. That choice moves Cmax a long way and brackets the published value: for the Section 2.7 reference cohort, NCA on the individual predictions gives a median Cmax about 19% below the published 929.6 ng/mL, while adding the model’s proportional residual error (67% CV at this dose) gives roughly 1430 ng/mL, about 54% above it. AUC over the same two runs moved only 2422 -> 2446 ng*h/mL, i.e. barely at all, because a maximum is inflated by noise whereas an integral averages it out. AUC is therefore the robust target and carries the assertion; Cmax is shown in the tables without one.

  • No maturation function. The paper extrapolates the weight-banded paediatric regimen of Table 5 down to 5 kg using allometry alone. The Discussion states this explicitly as a limitation: “as there is incomplete data on the primary route of metabolism in vivo, neither a maturation function nor an age-dependent allometric exponent were included”. The models reproduce that choice, so predictions in young children carry the paper’s own caveat.

  • Food and meal type are documented but not modelled. In the Phase II study every dose was given after an overnight fast with food restricted for at least an hour afterwards, so no fed-vs-fasted contrast is identifiable. Sections 2.6 and 3.3 explore the timing and type of the first post-dose meal post hoc against derived NCA metrics in 30 records and find no effect, so no meal term enters model(). These screened-but-unretained covariates (AGE, BMI, SEXF, FED) are recorded in each model’s covariatesDataExcluded metadata.

  • All parameter values come from the paper’s Table 2; none was derived from a figure, correspondence, or an upstream model. The supplement was not required – it contains inclusion/exclusion criteria, bioanalytical method detail, and additional summary tables and figures, but no model parameters.