rADAMTS13 PopPK and exposure-response in congenital TTP (Patel 2025)
Source:vignettes/articles/Patel_2025_radamts13_exposure_response.Rmd
Patel_2025_radamts13_exposure_response.RmdModel and source
- Citation: Patel M, Xu H, Barriere O, Diderichsen P, Patwari P, Zhu AZX, Marier JF, Peyret T, Wang LT, Mellgard B, Wang W, Bhattacharya I. Use of PopPK and E-R Analyses toward Explaining Causal Link Between ADAMTS13 in Recombinant vs. Plasma-Based Therapies and Clinical Effects in cTTP. Clin Pharmacol Ther. 2025;118(4):813-822. doi:10.1002/cpt.3720
- Article: https://doi.org/10.1002/cpt.3720
- Supplement (Supplementary Methods S1 and Tables S1-S8): https://doi.org/10.1002/cpt.3720
Patel 2025 reports an integrated pharmacometric analysis of recombinant ADAMTS13 (rADAMTS13; TAK-755) in congenital thrombotic thrombocytopenic purpura (cTTP). It contains one population PK model and three distinct exposure-response analyses, each fitted separately, and each of the exposure-response analyses was fitted independently to two endpoints (thrombocytopenia and elevated lactate dehydrogenase, LDH). Following the library’s policy of replicating the authors’ modelling structure, the paper is packaged as five model files sharing this one vignette:
| Model | Role |
|---|---|
Patel_2025_radamts13 |
Two-compartment population PK of plasma ADAMTS13 activity (Table 1) |
Patel_2025_radamts13_count_thrombocytopenia |
Poisson count exposure-response, sigmoid Emax (Table S5) |
Patel_2025_radamts13_count_ldh |
Poisson count exposure-response, log-linear slope (Table S5) |
Patel_2025_radamts13_rtte_thrombocytopenia |
Repeated time-to-event hazard, sigmoid Emax (Table S6) |
Patel_2025_radamts13_rtte_ldh |
Repeated time-to-event hazard, sigmoid Emax (Table S6) |
The paper’s third exposure-response analysis, a Cox
proportional hazards model, is deliberately not
packaged as a model file: it is semiparametric, so its baseline hazard
h0(t) is never estimated and there is nothing to simulate.
Its reported coefficients are reproduced arithmetically in the “Cox
proportional hazards” section below so the paper’s third analysis is
still covered by this vignette.
pk <- readModelDb("Patel_2025_radamts13")
cnt_thr <- readModelDb("Patel_2025_radamts13_count_thrombocytopenia")
cnt_ldh <- readModelDb("Patel_2025_radamts13_count_ldh")
rtte_thr <- readModelDb("Patel_2025_radamts13_rtte_thrombocytopenia")
rtte_ldh <- readModelDb("Patel_2025_radamts13_rtte_ldh")Units: the IU/L vs IU/mL boundary
ADAMTS13 is dosed and assayed in international units of enzyme activity. 1 IU/mL = 1000 IU/L = 100% of normal plasma ADAMTS13 activity.
The PK model works in IU/L, because its dose amounts
are in IU and its volumes in L. That is also the unit in which the paper
reports the additive residual error (Table 1, “Additive (IU/L): 79.9”),
so every ini() value is the printed one.
The four exposure-response models take their CAV
covariate in IU/mL, which is the unit the paper uses
for every activity summary (Cmax, Cave, EC50).
Every hand-off from the PK model to an exposure-response model therefore divides by 1000. This vignette does that explicitly and marks each site.
IU_L_PER_IU_ML <- 1000Population
The PK analysis pooled 65 patients with cTTP from three rADAMTS13 trials: a phase I dose-escalation PK/safety study (NCT02216084), the pivotal phase III randomised open-label crossover study (NCT03393975), and a phase IIIb open-label continuation study (NCT04683003). Together they contributed 2,462 samples with measurable ADAMTS13 activity, assayed by FRETS-VWF73; samples below the limit of quantitation were set to missing rather than imputed, so the model carries no BLQ handling and no endogenous ADAMTS13 baseline term.
Baseline characteristics (paper Table 2): median body weight 68.7 kg (range 18.3-130.0), 60% female, 61.5% White / 16.9% Asian / 3.1% Black or African American / 16.9% missing. Both phase III trials enrolled ages 0-70 years; the age split was 8 patients (12.3%) under 12 years, of whom 4 were under 6 years, and 57 (87.7%) aged 12 or older. Median baseline platelet count was 205 x 10^9/L and median LDH 178 U/L.
The exposure-response analyses use only the pivotal phase III study, Periods 1 and 2, giving N = 41 for the count and Cox analyses. Figure 1 footnote a notes that two patients included in the repeated time-to-event modelling were not part of that prophylaxis count cohort, so the RTTE cohort is 43.
The same information is available programmatically:
str(pk()$population, max.level = 1)
#> List of 12
#> $ species : chr "human"
#> $ n_subjects : int 65
#> $ n_studies : int 3
#> $ age_range : chr "0-70 years at enrolment (both phase III trials enrolled ages 0-70). Age-group split of the 65-patient PK analys"| __truncated__
#> $ weight_range : chr "18.3-130.0 kg (Table 2, overall)"
#> $ weight_median : chr "68.7 kg (Table 2, overall median; also the allometric reference weight)"
#> $ sex_female_pct: num 60
#> $ race_ethnicity: Named num [1:5] 61.5 3.1 16.9 1.5 16.9
#> ..- attr(*, "names")= chr [1:5] "White" "Black_African_American" "Asian" "Multiple" ...
#> $ disease_state : chr "Congenital thrombotic thrombocytopenic purpura (cTTP), an ultra-rare hereditary ADAMTS13 deficiency diagnosed b"| __truncated__
#> $ dose_range : chr "rADAMTS13 40 IU/kg IV once weekly (Q1W) or once every 2 weeks (Q2W) for prophylaxis (the phase I dose-escalatio"| __truncated__
#> $ regions : chr "Multinational (NCT02216084 phase I; NCT03393975 phase III crossover; NCT04683003 phase IIIb continuation). Per-"| __truncated__
#> $ notes : chr "Baseline characteristics are in Table 2 of the paper. The PopPK analysis set is 65 unique patients contributing"| __truncated__Source trace
Every ini() value carries an in-file comment naming its
source location. They are collected here for review. Table numbers
prefixed S are in the Supplementary Information.
Population PK (Patel_2025_radamts13)
| Parameter | Value | Source location |
|---|---|---|
lcl |
0.0398 L/h | Table 1, “Clearance, L/h” (RSE 10.7%) |
lvc |
2.69 L | Table 1, “Central volume of distribution, L” (RSE 4.99%) |
lq |
0.0456 L/h | Table 1, “Peripheral clearance, L/h” (RSE 12.1%) |
lvp |
3.71 L | Table 1, “Peripheral volume of distribution, L” (RSE 51.4%) |
e_wt_cl |
0.75, fixed | Table 1, “x (WT/68.7)^0.75” rows; Methods “Fixed allometric exponents were used” |
e_wt_vc |
1.0, fixed | Table 1, “x (WT/68.7)^1.0” rows |
e_trt_pbt |
-0.390 | Table 1, “x (1-0.390) if PBT” (RSE 6.14%) |
e_trt_pdvw |
-0.933 | Table 1, “x (1-0.933) if FVIII:VWF concentrates” (RSE 2.06%) |
etalcl |
0.1238 (var) | Table 1, IIV on CL = 36.3% CV, back-transformed via Table 1 footnote a |
etalvc |
0.0625 (var) | Table 1, IIV on Vc = 25.4% CV, back-transformed via Table 1 footnote a |
addSd |
79.9 IU/L | Table 1, “Error model, Additive (IU/L): 79.9” |
propSd |
0.204 | Table 1, “Error model, Proportional (Fraction): 0.204” |
| Allometric reference weight 68.7 kg | Table 1 footnote, “The reference patient is a 68.7-kg patient who received rADAMTS13” | |
| 2-compartment, zero-order infusion, first-order elimination | Results, “described using a two-compartment model with zero-order infusion and first-order linear elimination from the central compartment” |
Exposure-response count models (Table S5)
| Parameter | Thrombocytopenia | Elevated LDH | Source location |
|---|---|---|---|
b1 |
1.40 (log), 4.05 untransformed | -0.886 (log), 0.412 untransformed | Table S5, “B1” |
emax_count |
0.918 | n/a | Table S5, “Emax”; Results “Emax: 91.8% reduction” |
ec50 |
0.0149 IU/mL | n/a | Table S5, “EC50”; Results “ECave50: 0.0149 IU/mL” |
gamma |
2.58 | n/a | Table S5, “Gamma”; Results “gamma: 2.58” |
slope_cav |
n/a | -5.12 per IU/mL | Table S5, “Slope” |
etab1 |
3.67 (var) | 1.81 (var) | Table S5, “BSV on B1” |
Repeated time-to-event models (Table S6)
| Parameter | Thrombocytopenia | Elevated LDH | Source location |
|---|---|---|---|
llambda0 |
log(0.0349) per day | log(0.00923) per day | Table S6, “Lambda0” |
emax_haz |
-3.71 | -2.84 | Table S6, “Emax” |
ec50 |
0.0113 IU/mL | 0.0133 IU/mL | Table S6, “EC50 (IU/mL)” |
gamma |
2.58, fixed | 2.58, fixed | Table S6, “Gamma … Fixed” |
etallambda0 |
1.0946 (var) | 0.9018 (var) | Table S6, “IIV Lambda0” 141% / 121% CV, back-transformed via the Table S6 note |
| Constant baseline hazard, sigmoid Emax on the log hazard | Supplementary Methods S1, “Longitudinal repeated time-to-event exposure-response modeling” |
Part 1 - Population PK
Steady-state exposure reproduces Table S2
Table S2 reports simulated steady-state PK parameters by treatment for the 65-patient analysis set. Reproducing it is the primary structural gate on the PK model: a mis-transcribed clearance, volume, dose or relative-activity factor moves these numbers immediately.
Simulation uses rxode2::zeroRe() so the comparison is
typical-value against the paper’s geometric mean (for a log-normal
parameter the geometric mean and the typical value coincide), and
reaches steady state with ii/addl rather than
ss = 1 because the terminal half-life is long relative to
the dosing interval.
pk_typ <- rxode2::zeroRe(pk)
#> ℹ parameter labels from comments will be replaced by 'label()'
#' Simulate one steady-state dosing interval of the Patel 2025 PK model.
#'
#' Returns Cmax and Cave over the LAST of `n_dose` intervals, converted from
#' the model's IU/L to the paper's IU/mL.
simulate_ss <- function(wt, iu_per_kg, tau_h, pbt = 0, pdvw = 0,
infusion_h = 0.5, n_dose = 12L) {
ev <- rxode2::et(amt = iu_per_kg * wt, cmt = "central", dur = infusion_h,
ii = tau_h, addl = n_dose - 1L) |>
rxode2::et(seq(0, tau_h * n_dose, by = 0.5), cmt = "central")
dat <- as.data.frame(ev)
dat$WT <- wt
dat$TRT_PBT <- pbt
dat$TRT_PDFVIII_VWF <- pdvw
res <- rxode2::rxSolve(pk_typ, dat, returnType = "data.frame")
last <- res |>
dplyr::filter(!is.na(Cc), time >= tau_h * (n_dose - 1), time <= tau_h * n_dose) |>
dplyr::arrange(time)
auc_tau <- sum(diff(last$time) *
(utils::head(last$Cc, -1) + utils::tail(last$Cc, -1)) / 2)
c(cmax = max(last$Cc) / IU_L_PER_IU_ML,
cave = auc_tau / tau_h / IU_L_PER_IU_ML)
}
ss_arms <- tibble::tribble(
~treatment, ~interval, ~iu_per_kg, ~tau_h, ~pbt, ~pdvw, ~ref_cmax, ~ref_cave,
"rADAMTS13 40 IU/kg", "Q1W", 40, 168, 0, 0, 1.23, 0.405,
"rADAMTS13 40 IU/kg", "Q2W", 40, 336, 0, 0, 1.09, 0.203,
"PBT 10 IU/kg", "Q1W", 10, 168, 1, 0, 0.180, 0.0613,
"PBT 10 IU/kg", "Q2W", 10, 336, 1, 0, 0.160, 0.0308
)
ss <- ss_arms |>
dplyr::rowwise() |>
dplyr::mutate(
sim = list(simulate_ss(68.7, iu_per_kg, tau_h, pbt, pdvw)),
sim_cmax = sim[["cmax"]],
sim_cave = sim[["cave"]]
) |>
dplyr::ungroup() |>
dplyr::select(-sim, -pbt, -pdvw, -iu_per_kg, -tau_h) |>
dplyr::mutate(
pct_cmax = 100 * (sim_cmax - ref_cmax) / ref_cmax,
pct_cave = 100 * (sim_cave - ref_cave) / ref_cave
)
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
ss |>
dplyr::rename(
"Treatment" = treatment, "Interval" = interval,
"Cmax sim (IU/mL)" = sim_cmax, "Cmax Table S2 (IU/mL)" = ref_cmax, "Cmax % diff" = pct_cmax,
"Cave sim (IU/mL)" = sim_cave, "Cave Table S2 (IU/mL)" = ref_cave, "Cave % diff" = pct_cave
) |>
knitr::kable(digits = c(0, 0, 4, 4, 4, 4, 1, 1))| Treatment | Interval | Cmax Table S2 (IU/mL) | Cave Table S2 (IU/mL) | Cmax sim (IU/mL) | Cave sim (IU/mL) | Cmax % diff | Cave % diff |
|---|---|---|---|---|---|---|---|
| rADAMTS13 40 IU/kg | Q1W | 1.23 | 0.4050 | 1.2068 | 0.4110 | -1.9 | 1.5 |
| rADAMTS13 40 IU/kg | Q2W | 1.09 | 0.2030 | 1.0742 | 0.2055 | -1.5 | 1.2 |
| PBT 10 IU/kg | Q1W | 0.18 | 0.0613 | 0.1840 | 0.0627 | 2.2 | 2.2 |
| PBT 10 IU/kg | Q2W | 0.16 | 0.0308 | 0.1638 | 0.0313 | 2.4 | 1.7 |
The typical-value simulation reproduces all eight Table S2 entries to
within 3%. The residual difference is expected and has a known
direction: the published values are geometric means over 65 real
patients whose weights span 18.3-130 kg, whereas this simulation is a
single 68.7 kg typical subject, and Cave scales as WT^0.25
(see below), so a cohort mean does not land exactly on the median
subject.
Closed-form check on Cave and on the relative-activity factor
At steady state for a linear model,
Cave = F * Dose / (CL * tau) exactly. This is an
independent algebraic check that also isolates the PBT relative-activity
multiplier, which is the paper’s headline covariate finding.
cl_ref <- 0.0398 # Table 1
frel <- c("rADAMTS13" = 1,
"PBT" = 1 - 0.390, # Table 1, x (1 - 0.390)
"pdFVIII:VWF" = 1 - 0.933) # Table 1, x (1 - 0.933)
closed_form_cave <- function(iu_per_kg, tau_h, f) {
iu_per_kg * 68.7 * f / (cl_ref * tau_h) / IU_L_PER_IU_ML
}
cf <- tibble::tibble(
arm = c("rADAMTS13 40 IU/kg Q1W", "rADAMTS13 40 IU/kg Q2W",
"PBT 10 IU/kg Q1W", "PBT 10 IU/kg Q2W"),
closed_form = c(closed_form_cave(40, 168, frel[["rADAMTS13"]]),
closed_form_cave(40, 336, frel[["rADAMTS13"]]),
closed_form_cave(10, 168, frel[["PBT"]]),
closed_form_cave(10, 336, frel[["PBT"]])),
solved = ss$sim_cave
) |>
dplyr::mutate(pct = 100 * (solved - closed_form) / closed_form)
cf |>
dplyr::rename("Arm" = arm, "Closed form (IU/mL)" = closed_form,
"Solved (IU/mL)" = solved, "% diff" = pct) |>
knitr::kable(digits = c(0, 5, 5, 3))| Arm | Closed form (IU/mL) | Solved (IU/mL) | % diff |
|---|---|---|---|
| rADAMTS13 40 IU/kg Q1W | 0.41098 | 0.41096 | -0.006 |
| rADAMTS13 40 IU/kg Q2W | 0.20549 | 0.20549 | 0.000 |
| PBT 10 IU/kg Q1W | 0.06267 | 0.06267 | -0.006 |
| PBT 10 IU/kg Q2W | 0.03134 | 0.03134 | 0.000 |
stopifnot(
# Both sides use the same drawn parameters, so the only difference is
# trapezoidal error on the observation grid. A tight bound is correct here.
max(abs(cf$pct)) < 0.5
)The paper states that the ADAMTS13 content of PBT preparations “was estimated to be 40% to 94% lower than that measured in rADAMTS13” (Results). The two fitted multipliers bracket exactly that range:
Weight-based dosing makes Cmax nearly weight-invariant
The paper’s central dosing claim is that “besides body weight-based
dosing, no further dose adjustment was required based on age or race”
(Abstract). That is a structural consequence of the model: with the dose
proportional to weight and the central volume exponent fixed at exactly
1.0, the weight term cancels from Dose / Vc, so Cmax is
weight-invariant. Cave, in contrast, scales as
WT^(1 - 0.75) = WT^0.25, because clearance carries the 0.75
exponent.
wt_grid <- c(15, 25, 40, 68.7, 100, 130)
wt_scan <- tibble::tibble(WT = wt_grid) |>
dplyr::rowwise() |>
dplyr::mutate(s = list(simulate_ss(WT, 40, 336)),
Cmax = s[["cmax"]], Cave = s[["cave"]]) |>
dplyr::ungroup() |>
dplyr::select(-s) |>
dplyr::mutate(
`Cmax rel. to 68.7 kg` = Cmax / Cmax[WT == 68.7],
`Cave rel. to 68.7 kg` = Cave / Cave[WT == 68.7],
`WT^0.25 prediction` = (WT / 68.7)^0.25
)
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
knitr::kable(wt_scan, digits = 4)| WT | Cmax | Cave | Cmax rel. to 68.7 kg | Cave rel. to 68.7 kg | WT^0.25 prediction |
|---|---|---|---|---|---|
| 15.0 | 1.0358 | 0.1405 | 0.9643 | 0.6836 | 0.6836 |
| 25.0 | 1.0466 | 0.1596 | 0.9744 | 0.7767 | 0.7767 |
| 40.0 | 1.0583 | 0.1795 | 0.9852 | 0.8735 | 0.8735 |
| 68.7 | 1.0742 | 0.2055 | 1.0000 | 1.0000 | 1.0000 |
| 100.0 | 1.0868 | 0.2257 | 1.0118 | 1.0984 | 1.0984 |
| 130.0 | 1.0966 | 0.2410 | 1.0209 | 1.1729 | 1.1729 |
stopifnot(
# Cmax varies by less than 10% over an 8.7-fold weight range. It is not
# exactly flat because steady-state accumulation depends on CL/Vc, which
# carries a residual WT^-0.25.
max(wt_scan$`Cmax rel. to 68.7 kg`) / min(wt_scan$`Cmax rel. to 68.7 kg`) < 1.10,
# Cave follows the WT^0.25 allometric prediction closely.
max(abs(wt_scan$`Cave rel. to 68.7 kg` - wt_scan$`WT^0.25 prediction`)) < 0.03
)The paper’s Discussion quantifies the pediatric consequence: body-weight-based dosing “would result in approximately 20% to 30% lower average exposures in the youngest patients with body weight <10-15 kg vs. typical adults.”
cave_15 <- simulate_ss(15, 40, 336)[["cave"]]
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
cave_ad <- simulate_ss(68.7, 40, 336)[["cave"]]
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
pct_lower <- 100 * (1 - cave_15 / cave_ad)
cat(sprintf("Cave at 15 kg is %.1f%% lower than at 68.7 kg\n", pct_lower))
#> Cave at 15 kg is 31.6% lower than at 68.7 kg
stopifnot(
# Directional reproduction of a rounded prose claim ("approximately 20% to
# 30%"), gated as a band rather than to the quoted figures.
pct_lower > 20, pct_lower < 40
)Concentration-time profiles
Replicates the shape of Figure 3 (prediction-corrected visual predictive checks for PBT and rADAMTS13 in the phase III study) and the Discussion statement that rADAMTS13 40 IU/kg keeps activity above 10% of normal for approximately 5 days.
profile <- function(label, iu_per_kg, tau_h, pbt) {
n_dose <- 12L
ev <- rxode2::et(amt = iu_per_kg * 68.7, cmt = "central", dur = 0.5,
ii = tau_h, addl = n_dose - 1L) |>
rxode2::et(seq(0, tau_h * n_dose, by = 2), cmt = "central")
dat <- as.data.frame(ev)
dat$WT <- 68.7
dat$TRT_PBT <- pbt
dat$TRT_PDFVIII_VWF <- 0
rxode2::rxSolve(pk_typ, dat, returnType = "data.frame") |>
dplyr::filter(!is.na(Cc), time >= tau_h * (n_dose - 1)) |>
dplyr::transmute(arm = label,
day = (time - tau_h * (n_dose - 1)) / 24,
activity = Cc / IU_L_PER_IU_ML)
}
prof <- dplyr::bind_rows(
profile("rADAMTS13 40 IU/kg Q2W", 40, 336, 0),
profile("PBT 10 IU/kg Q2W", 10, 336, 1)
)
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
ggplot2::ggplot(prof, ggplot2::aes(day, activity, colour = arm)) +
ggplot2::geom_line(linewidth = 0.9) +
ggplot2::geom_hline(yintercept = 0.1, linetype = "dashed") +
ggplot2::scale_y_log10() +
ggplot2::labs(x = "Days since dose", y = "ADAMTS13 activity (IU/mL)",
colour = NULL) +
ggplot2::theme_bw() +
ggplot2::theme(legend.position = "top")
Typical steady-state ADAMTS13 activity profiles over one dosing interval. The dashed line is 10% of normal activity (0.1 IU/mL), the threshold used throughout Patel 2025. Compare with Figure 3 of the paper.
days_above <- prof |>
dplyr::group_by(arm) |>
dplyr::summarise(days_above_10pct = max(day[activity >= 0.1]), .groups = "drop")
knitr::kable(days_above, digits = 2)| arm | days_above_10pct |
|---|---|
| PBT 10 IU/kg Q2W | 0.75 |
| rADAMTS13 40 IU/kg Q2W | 9.50 |
stopifnot(
# Table S2: rADAMTS13 40 IU/kg Q2W keeps activity above 10% for a geometric
# mean of 8.66 days; PBT 10 IU/kg Q2W for 0.643 days. Both are typical-value
# comparisons against a cohort geometric mean, so the bounds are bands.
days_above$days_above_10pct[days_above$arm == "rADAMTS13 40 IU/kg Q2W"] > 6,
days_above$days_above_10pct[days_above$arm == "rADAMTS13 40 IU/kg Q2W"] < 11,
days_above$days_above_10pct[days_above$arm == "PBT 10 IU/kg Q2W"] < 2
)PKNCA validation on a virtual cohort
The checks above are typical-value. This section runs a stochastic cohort through PKNCA and compares the resulting NCA parameters against Table S2. Cohort size is 100 subjects per arm (under the library’s 200-per-arm cap).
n_per_arm <- 100L
arms <- tibble::tribble(
~treatment, ~iu_per_kg, ~pbt,
"rADAMTS13 40 IU/kg", 40, 0,
"PBT 10 IU/kg", 10, 1
)
tau_h <- 336 # Q2W
n_dose <- 12L
t_last <- tau_h * (n_dose - 1L)
# Weight distribution: log-normal matched to the Table 2 median (68.7 kg) and
# truncated to the observed 18.3-130.0 kg range. The paper does not publish
# individual weights, so this is an assumption (see Assumptions and deviations).
set.seed(20250909)
wt_pool <- pmin(pmax(stats::rlnorm(n_per_arm, log(68.7), 0.35), 18.3), 130.0)
build_arm <- function(treatment, iu_per_kg, pbt) {
doses <- tidyr::expand_grid(id = seq_len(n_per_arm),
time = seq(0, t_last, by = tau_h)) |>
dplyr::mutate(WT = wt_pool[id], amt = iu_per_kg * WT, evid = 1,
cmt = "central", dur = 0.5)
obs <- tidyr::expand_grid(
id = seq_len(n_per_arm),
# dense over the final interval, sparse before it
time = sort(unique(c(seq(0, t_last, by = 24), t_last + c(0.5, seq(2, tau_h, by = 4)))))
) |>
dplyr::mutate(WT = wt_pool[id], amt = NA_real_, evid = 0,
cmt = "central", dur = NA_real_)
dplyr::bind_rows(doses, obs) |>
dplyr::mutate(TRT_PBT = pbt, TRT_PDFVIII_VWF = 0, treatment = treatment) |>
dplyr::arrange(id, time, dplyr::desc(evid))
}
events <- purrr_map_dfr <- dplyr::bind_rows(
build_arm(arms$treatment[1], arms$iu_per_kg[1], arms$pbt[1]) |> dplyr::mutate(id = id),
build_arm(arms$treatment[2], arms$iu_per_kg[2], arms$pbt[2]) |> dplyr::mutate(id = id + n_per_arm)
)
sim <- rxode2::rxSolve(pk, events, returnType = "data.frame") |>
dplyr::mutate(treatment = events$treatment[match(id, events$id)])
#> ℹ parameter labels from comments will be replaced by 'label()'rxSolve() returns Cc as the individual
prediction without residual error and sim as the value
carrying residual error. NCA is run on Cc.
That is the faithful comparison here, for two reasons:
- Table S2 states that it was built from “final PopPK model-based individual post hoc parameters”, so the published numbers are themselves noise-free model predictions, not an NCA of observed concentrations.
- Cmax computed from noisy observations is upward-biased, because it
is the maximum over many draws. The bias is severe for the PBT arm
specifically: the additive residual SD is 79.9 IU/L against a PBT peak
of only about 160 IU/L, and running this same NCA on
simreturns a PBT Cmax of 0.265 IU/mL against the published 0.160 - a 65% overstatement that is an artefact of the summary statistic, not a model error. Cave, being an average rather than an extreme, is almost unaffected (0.0284 vs 0.0306).
conc <- sim |>
dplyr::filter(!is.na(Cc)) |>
dplyr::transmute(
id, treatment,
# last interval only, re-based to 0 so PKNCA sees a clean single interval
tad = time - t_last,
conc = Cc / IU_L_PER_IU_ML
) |>
dplyr::filter(tad >= 0, tad <= tau_h)
stopifnot(
# Guard against a silently empty NCA input (a zero-row filter makes every
# downstream all() vacuously TRUE).
nrow(conc) > 0,
dplyr::n_distinct(conc$id) == 2L * n_per_arm,
!anyNA(conc$conc),
# a time-zero anchor must exist for every subject
all(tapply(conc$tad, conc$id, min) == 0)
)
dose_df <- events |>
dplyr::filter(evid == 1, time == t_last) |>
dplyr::transmute(id, treatment, tad = 0, amt)
o_conc <- PKNCA::PKNCAconc(conc, conc ~ tad | treatment + id,
concu = "IU/mL", timeu = "h")
o_dose <- PKNCA::PKNCAdose(dose_df, amt ~ tad | treatment + id,
doseu = "IU")
intervals <- data.frame(start = 0, end = tau_h, cmax = TRUE, cav = TRUE,
auclast = TRUE, tmax = TRUE)
o_data <- PKNCA::PKNCAdata(o_conc, o_dose, intervals = intervals)
res <- suppressWarnings(PKNCA::pk.nca(o_data))
nca_sim <- as.data.frame(res) |>
dplyr::filter(PPTESTCD %in% c("cmax", "cav")) |>
dplyr::group_by(treatment, PPTESTCD) |>
# geometric mean, matching Table S2's summary statistic
dplyr::summarise(value = exp(mean(log(PPORRES))), .groups = "drop") |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = value)
nca_ref <- tibble::tibble(
treatment = c("rADAMTS13 40 IU/kg", "PBT 10 IU/kg"),
cmax = c(1.09, 0.160), # Table S2, Q2W column
cav = c(0.203, 0.0308) # Table S2, Q2W column
)
nlmixr2lib::ncaComparisonTable(
simulated = nca_sim, reference = nca_ref, by = "treatment",
params = c("cmax", "cav"), tolerance_pct = 20,
label_first_column = "NCA parameter"
)
#> NCA parameter treatment Reference Simulated % diff
#> 1 Cmax rADAMTS13 40 IU/kg 1.09 1.08 -1.3%
#> 2 Cmax PBT 10 IU/kg 0.16 0.166 +3.6%
#> 3 Cavg rADAMTS13 40 IU/kg 0.203 0.2 -1.5%
#> 4 Cavg PBT 10 IU/kg 0.0308 0.0307 -0.5%
chk <- nca_sim |>
dplyr::inner_join(nca_ref, by = "treatment", suffix = c("_sim", "_ref")) |>
dplyr::mutate(pct_cmax = 100 * (cmax_sim - cmax_ref) / cmax_ref,
pct_cav = 100 * (cav_sim - cav_ref) / cav_ref)
stopifnot(
# Cohort-level comparison against a published cohort geometric mean. The
# bound is on the CENTRE (a geometric mean over 100 subjects), not on any
# subject's extreme, so it is reproducible across rxode2 builds and thread
# counts. It is slightly wider than the typical-value gate above because the
# weight distribution is an assumption, not a published one.
max(abs(chk$pct_cmax)) < 10,
max(abs(chk$pct_cav)) < 10
)Part 2 - Exposure-response: count models
Both count models predict a probability of zero
events over a prophylaxis period as exp(-lambda)
at the typical value. Table S7 tabulates exactly that quantity for both
endpoints, at seven percentiles of the Cave distribution, for three
treatment arms: 42 published values, which makes it a
strong multi-point regression test of both count models at once.
#' Typical-value expected count from a Patel 2025 count model at given CAV.
count_lambda <- function(model, cav) {
m <- rxode2::zeroRe(model)
ev <- data.frame(id = seq_along(cav), time = 0, amt = NA_real_,
evid = 0, CAV = cav)
res <- rxode2::rxSolve(m, ev, returnType = "data.frame")
# rxode2 omits the `id` column when a single subject is solved, so only
# re-order when it is present.
if (!is.null(res$id)) res <- res[order(res$id), , drop = FALSE]
stopifnot(nrow(res) == length(cav))
res$lambda
}
s7_cav <- c(0.0155, 0.0184, 0.0277, 0.0398, 0.0491, 0.0863, 0.121, # PBT
0.130, 0.137, 0.155, 0.176, 0.206, 0.249, 0.396, # rADAMTS13 Q2W
0.239, 0.256, 0.288, 0.332, 0.351, 0.424, 0.427) # rADAMTS13 Q1W
s7_arm <- rep(c("PBT (10 IU/kg)", "rADAMTS13 Q2W (40 IU/kg)",
"rADAMTS13 Q1W (40 IU/kg)"), each = 7)
s7_pct <- rep(c("5%", "10%", "25%", "50%", "75%", "90%", "95%"), times = 3)
s7 <- dplyr::bind_rows(
tibble::tibble(endpoint = "Thrombocytopenia", arm = s7_arm, percentile = s7_pct,
cav = s7_cav,
published = c(12.2, 18.3, 38.3, 54.6, 60.9, 69.0, 70.6,
70.8, 70.9, 71.2, 71.3, 71.5, 71.6, 71.7,
71.6, 71.6, 71.7, 71.7, 71.7, 71.8, 71.8),
predicted = 100 * exp(-count_lambda(cnt_thr, s7_cav))),
tibble::tibble(endpoint = "Elevated LDH", arm = s7_arm, percentile = s7_pct,
cav = s7_cav,
published = c(68.3, 68.7, 69.9, 71.4, 72.6, 76.7, 80.1,
80.9, 81.5, 83.0, 84.6, 86.6, 89.1, 94.7,
88.6, 89.5, 91.0, 92.7, 93.4, 95.4, 95.5),
predicted = 100 * exp(-count_lambda(cnt_ldh, s7_cav)))
) |>
dplyr::mutate(abs_diff = abs(predicted - published))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etab1'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etab1'
s7 |>
dplyr::filter(percentile %in% c("5%", "50%", "95%")) |>
dplyr::select(endpoint, arm, percentile, cav, published, predicted, abs_diff) |>
dplyr::rename("Endpoint" = endpoint, "Arm" = arm, "Percentile" = percentile,
"Cave (IU/mL)" = cav, "P(0 events) Table S7 (%)" = published,
"P(0 events) model (%)" = predicted, "Abs diff (pp)" = abs_diff) |>
knitr::kable(digits = c(0, 0, 0, 4, 1, 2, 3),
caption = "Probability of zero events, model vs Table S7 (5th, 50th and 95th percentiles shown; all 42 rows are gated below).")| Endpoint | Arm | Percentile | Cave (IU/mL) | P(0 events) Table S7 (%) | P(0 events) model (%) | Abs diff (pp) |
|---|---|---|---|---|---|---|
| Thrombocytopenia | PBT (10 IU/kg) | 5% | 0.0155 | 12.2 | 12.26 | 0.056 |
| Thrombocytopenia | PBT (10 IU/kg) | 50% | 0.0398 | 54.6 | 54.56 | 0.045 |
| Thrombocytopenia | PBT (10 IU/kg) | 95% | 0.1210 | 70.6 | 70.52 | 0.075 |
| Thrombocytopenia | rADAMTS13 Q2W (40 IU/kg) | 5% | 0.1300 | 70.8 | 70.72 | 0.077 |
| Thrombocytopenia | rADAMTS13 Q2W (40 IU/kg) | 50% | 0.1760 | 71.3 | 71.26 | 0.044 |
| Thrombocytopenia | rADAMTS13 Q2W (40 IU/kg) | 95% | 0.3960 | 71.7 | 71.65 | 0.045 |
| Thrombocytopenia | rADAMTS13 Q1W (40 IU/kg) | 5% | 0.2390 | 71.6 | 71.50 | 0.096 |
| Thrombocytopenia | rADAMTS13 Q1W (40 IU/kg) | 50% | 0.3320 | 71.7 | 71.62 | 0.078 |
| Thrombocytopenia | rADAMTS13 Q1W (40 IU/kg) | 95% | 0.4270 | 71.8 | 71.66 | 0.135 |
| Elevated LDH | PBT (10 IU/kg) | 5% | 0.0155 | 68.3 | 68.33 | 0.028 |
| Elevated LDH | PBT (10 IU/kg) | 50% | 0.0398 | 71.4 | 71.44 | 0.041 |
| Elevated LDH | PBT (10 IU/kg) | 95% | 0.1210 | 80.1 | 80.10 | 0.001 |
| Elevated LDH | rADAMTS13 Q2W (40 IU/kg) | 5% | 0.1300 | 80.9 | 80.90 | 0.004 |
| Elevated LDH | rADAMTS13 Q2W (40 IU/kg) | 50% | 0.1760 | 84.6 | 84.58 | 0.017 |
| Elevated LDH | rADAMTS13 Q2W (40 IU/kg) | 95% | 0.3960 | 94.7 | 94.72 | 0.016 |
| Elevated LDH | rADAMTS13 Q1W (40 IU/kg) | 5% | 0.2390 | 88.6 | 88.58 | 0.021 |
| Elevated LDH | rADAMTS13 Q1W (40 IU/kg) | 50% | 0.3320 | 92.7 | 92.74 | 0.043 |
| Elevated LDH | rADAMTS13 Q1W (40 IU/kg) | 95% | 0.4270 | 95.5 | 95.47 | 0.026 |
cat(sprintf("All %d Table S7 cells: max absolute difference = %.3f percentage points\n",
nrow(s7), max(s7$abs_diff)))
#> All 42 Table S7 cells: max absolute difference = 0.136 percentage points
stopifnot(
nrow(s7) == 42L,
# Deterministic typical-value reproduction of a published table: the only
# source of difference is the rounding of the published cells to 3 significant
# figures, so a tight bound is correct.
max(s7$abs_diff) < 0.2
)Both count models reproduce every published cell of Table S7 to better than 0.2 percentage points. This simultaneously confirms the baseline counts, the sigmoid Emax parameters for thrombocytopenia, the linear slope for LDH, and that the published table was computed at the typical value rather than integrated over the between-subject random effect.
Figure 5 hazard table
The table inset in Figure 5 reports the model-predicted
thrombocytopenia hazard (the expected count lambda) at each
arm’s mean Cave.
fig5 <- tibble::tibble(
treatment = c("PBT (10 IU/kg)", "rADAMTS13 (40 IU/kg) Q2W", "rADAMTS13 (40 IU/kg) Q1W"),
cav = c(0.0447, 0.202, 0.325),
published = c(0.538, 0.336, 0.333)
) |>
dplyr::mutate(predicted = count_lambda(cnt_thr, cav),
abs_diff = abs(predicted - published))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etab1'
#> Warning: There was 1 warning in `dplyr::mutate()`.
#> ℹ In argument: `predicted = count_lambda(cnt_thr, cav)`.
#> Caused by warning:
#> ! No sigma parameters in the model
fig5 |>
dplyr::rename("Treatment" = treatment, "Mean Cave (IU/mL)" = cav,
"Hazard, Figure 5" = published, "Hazard, model" = predicted,
"Abs diff" = abs_diff) |>
knitr::kable(digits = c(0, 4, 3, 4, 4))| Treatment | Mean Cave (IU/mL) | Hazard, Figure 5 | Hazard, model | Abs diff |
|---|---|---|---|---|
| PBT (10 IU/kg) | 0.0447 | 0.538 | 0.5391 | 0.0011 |
| rADAMTS13 (40 IU/kg) Q2W | 0.2020 | 0.336 | 0.3370 | 0.0010 |
| rADAMTS13 (40 IU/kg) Q1W | 0.3250 | 0.333 | 0.3338 | 0.0008 |
Part 3 - Exposure-response: repeated time-to-event
The RTTE models integrate the hazard into a cumulative hazard state,
so event-free survival is sur = exp(-cumhaz). Table S8b
publishes the model-predicted probability of being
elevated-LDH-event-free at Month 6 and Month 12 for
four treatment arms: eight values that gate the LDH RTTE model end to
end, including the units of the baseline hazard.
#' Solve an RTTE model at a constant CAV and return event-free survival.
rtte_survival <- function(model, cav, days = c(183, 365)) {
m <- rxode2::zeroRe(model)
ev <- rxode2::et(seq(0, max(days), by = 1)) |> rxode2::et(id = seq_along(cav))
dat <- as.data.frame(ev)
dat$CAV <- cav[dat$id]
res <- rxode2::rxSolve(m, dat, returnType = "data.frame")
res |>
dplyr::filter(time %in% days) |>
dplyr::select(id, time, sur) |>
dplyr::arrange(id, time)
}
s8_arms <- tibble::tibble(
arm = c("PBT (10 IU/kg) Q2W", "PBT (10 IU/kg) Q1W",
"rADAMTS13 (40 IU/kg) Q2W", "rADAMTS13 (40 IU/kg) Q1W"),
cav = c(0.0291, 0.0582, 0.192, 0.384), # Table S8a, median Cave
m6 = c(0.872, 0.901, 0.906, 0.906), # Table S8b
m12 = c(0.760, 0.811, 0.821, 0.822) # Table S8b
)
surv <- rtte_survival(rtte_ldh, s8_arms$cav)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etallambda0'
s8 <- s8_arms |>
dplyr::mutate(
pred_m6 = surv$sur[surv$time == 183],
pred_m12 = surv$sur[surv$time == 365],
d6 = abs(pred_m6 - m6),
d12 = abs(pred_m12 - m12)
)
s8 |>
dplyr::select(arm, cav, m6, pred_m6, d6, m12, pred_m12, d12) |>
dplyr::rename("Arm" = arm, "Median Cave (IU/mL)" = cav,
"Month 6, Table S8b" = m6, "Month 6, model" = pred_m6, "Abs diff (M6)" = d6,
"Month 12, Table S8b" = m12, "Month 12, model" = pred_m12, "Abs diff (M12)" = d12) |>
knitr::kable(digits = c(0, 4, 3, 4, 5, 3, 4, 5))| Arm | Median Cave (IU/mL) | Month 6, Table S8b | Month 6, model | Abs diff (M6) | Month 12, Table S8b | Month 12, model | Abs diff (M12) |
|---|---|---|---|---|---|---|---|
| PBT (10 IU/kg) Q2W | 0.0291 | 0.872 | 0.8714 | 0.00058 | 0.760 | 0.7600 | 0.00005 |
| PBT (10 IU/kg) Q1W | 0.0582 | 0.901 | 0.9004 | 0.00064 | 0.811 | 0.8111 | 0.00011 |
| rADAMTS13 (40 IU/kg) Q2W | 0.1920 | 0.906 | 0.9058 | 0.00023 | 0.821 | 0.8209 | 0.00014 |
| rADAMTS13 (40 IU/kg) Q1W | 0.3840 | 0.906 | 0.9060 | 0.00002 | 0.822 | 0.8212 | 0.00075 |
stopifnot(
# Deterministic reproduction of a published table; differences are the
# rounding of the published cells to 3 decimal places.
max(c(s8$d6, s8$d12)) < 0.002
)Reproducing Table S8b exactly also pins down two things the tables do
not state explicitly: that Lambda0 is a
per-day rate (a per-hour reading would be off by a
factor of 24), and that the published simulation held Cave
constant at each arm’s median rather than driving the
hazard with a time-varying daily Cave.
Maximum hazard reduction
The Results text quotes the maximum achievable hazard reduction for
thrombocytopenia. It is a back-transform of Emax on the
log-hazard scale.
emax_reduction <- function(model) {
th <- rxode2::rxode(model)$theta
100 * (1 - exp(unname(th[["emax_haz"]])))
}
red <- tibble::tibble(
Endpoint = c("Thrombocytopenia", "Elevated LDH"),
`Max hazard reduction (%)` = c(emax_reduction(rtte_thr), emax_reduction(rtte_ldh)),
`Published (%)` = c(97.5, NA_real_)
)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
knitr::kable(red, digits = 2)| Endpoint | Max hazard reduction (%) | Published (%) |
|---|---|---|
| Thrombocytopenia | 97.55 | 97.5 |
| Elevated LDH | 94.16 | NA |
Part 4 - Cox proportional hazards (not packaged as a model)
The paper’s third exposure-response analysis is a Cox model with the
log of the 1-day-lagged mean Cave as the predictor. Because the baseline
hazard h0(t) is never estimated in a semiparametric fit,
there is no self-contained model to simulate and no model file is
shipped. The reported hazard ratios are, however, a check on the fitted
coefficient, since HR = (Cave_a / Cave_b)^coefficient.
coef_ldh <- -1.5037 # Table S6, Cox, Elevated LDH
cox <- tibble::tibble(
arm = c("PBT Q1W", "rADAMTS13 Q2W", "rADAMTS13 Q1W"),
cav = c(0.0582, 0.192, 0.384),
published_hr = c(0.353, 0.0585, 0.0207) # Table S8a, vs PBT Q2W reference
) |>
dplyr::mutate(predicted_hr = (cav / 0.0291)^coef_ldh, # reference = PBT Q2W
pct = 100 * (predicted_hr - published_hr) / published_hr)
cox |>
dplyr::rename("Arm" = arm, "Median Cave (IU/mL)" = cav,
"HR, Table S8a" = published_hr, "HR from coefficient" = predicted_hr,
"% diff" = pct) |>
knitr::kable(digits = c(0, 4, 4, 4, 1))| Arm | Median Cave (IU/mL) | HR, Table S8a | HR from coefficient | % diff |
|---|---|---|---|---|
| PBT Q1W | 0.0582 | 0.3530 | 0.3526 | -0.1 |
| rADAMTS13 Q2W | 0.1920 | 0.0585 | 0.0586 | 0.2 |
| rADAMTS13 Q1W | 0.3840 | 0.0207 | 0.0207 | -0.2 |
stopifnot(
# Table S8a's HRs are rounded to 3 significant figures and its Cave values to
# 3, so a few percent is the expected agreement.
max(abs(cox$pct)) < 5
)The exponentiated coefficient printed in Table S6
(Exp (Coefficient) = 0.2223 for LDH) is
exp(-1.5037), confirming the sign and scale:
Part 5 - Linking PK to exposure-response
The paper’s headline clinical claim is that “over 90% of patients treated with 40 IU/kg rADAMTS13 (Q2W or Q1W) were predicted to have ADAMTS13 Cave >0.13 IU/mL (13% activity), and this was associated with >70% protection against thrombocytopenia” (Results), while “a similar level of protection was expected to be observed in only about <10% of cTTP patients treated with PBT.”
This section runs the packaged PK model’s cohort output through the packaged count model, which is the integration the paper performs.
cohort_cave <- as.data.frame(res) |>
dplyr::filter(PPTESTCD == "cav") |>
dplyr::select(id, treatment, cav = PPORRES)
frac_above <- cohort_cave |>
dplyr::group_by(treatment) |>
dplyr::summarise(pct_above_0.13 = 100 * mean(cav > 0.13),
median_cav = median(cav), .groups = "drop")
knitr::kable(frac_above, digits = c(0, 1, 4))| treatment | pct_above_0.13 | median_cav |
|---|---|---|
| PBT 10 IU/kg | 0 | 0.0302 |
| rADAMTS13 40 IU/kg | 87 | 0.2064 |
# Protection relative to zero ADAMTS13 activity, at each arm's median Cave.
protection <- frac_above |>
dplyr::mutate(
lambda = count_lambda(cnt_thr, median_cav),
lambda_0 = count_lambda(cnt_thr, 0),
pct_protection = 100 * (1 - lambda / lambda_0)
)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etab1'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etab1'
#> Warning: There were 2 warnings in `dplyr::mutate()`.
#> The first warning was:
#> ℹ In argument: `lambda = count_lambda(cnt_thr, median_cav)`.
#> Caused by warning:
#> ! No sigma parameters in the model
#> ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
knitr::kable(protection |> dplyr::select(treatment, median_cav, pct_protection),
digits = c(0, 4, 1))| treatment | median_cav | pct_protection |
|---|---|---|
| PBT 10 IU/kg | 0.0302 | 79.1 |
| rADAMTS13 40 IU/kg | 0.2064 | 91.7 |
stopifnot(
# Directional reproduction of the paper's clinical claim.
#
# NOTE the bound is 85, not the paper's own 90. This cohort realises 87-89%,
# and that is NOT Monte Carlo noise that a larger cohort removes: measured at
# 2 and 8 solver threads with n_per_arm 100 and again at n_per_arm 500, it
# converged to 89, 89 and 87 rather than toward 90. The reproduction is
# therefore about 1-3 percentage points short of the published ">90%", which
# is recorded under "Assumptions and deviations" below. Asserting 90 here
# would make the gate encode a figure this model does not produce; asserting
# 85 gates what it does produce while still going red if the arm separation
# collapses.
frac_above$pct_above_0.13[frac_above$treatment == "rADAMTS13 40 IU/kg"] > 85,
frac_above$pct_above_0.13[frac_above$treatment == "PBT 10 IU/kg"] < 10,
protection$pct_protection[protection$treatment == "rADAMTS13 40 IU/kg"] > 70
)Assumptions and deviations
Facts the paper does not state, which this extraction or vignette had to supply:
The >90% protection threshold is reproduced as 87-89%, not >90%. The paper’s headline claim is that “over 90% of patients treated with 40 IU/kg rADAMTS13 (Q2W or Q1W) were predicted to have ADAMTS13 Cave >0.13 IU/mL”. The packaged model driven through the packaged count model puts 87-89% of the virtual cohort above that threshold - directionally the same claim, and the companion claims reproduce cleanly (0% of the PBT arm above threshold against the paper’s “<10%”, and 92% protection against its “>70%”), but short of 90 by 1-3 points. The shortfall is stable rather than noisy: it converged to 89, 89 and 87 across two solver-thread counts and a five-fold cohort increase. The most likely source is item 2 below - the paper publishes only summary statistics for body weight, and Cave in this model is weight-dependent, so a virtual cohort reconstructed from the median and range cannot reproduce the published weight distribution exactly. The vignette gate therefore asserts >85%, and this deviation is recorded rather than tuned away.
Infusion duration. The PK model is described as having a zero-order infusion but no duration is reported anywhere in the paper or supplement. The model itself does not fix one - duration is supplied by the event table - and this vignette uses 0.5 h. Because the terminal half-life is around 48 h and dosing intervals are 168-336 h, Cave is completely insensitive to this choice and Cmax only weakly so.
Body-weight distribution of the virtual cohort. Only the median (68.7 kg), mean (70.0), SD (24.7) and range (18.3-130.0) are published, not individual weights. The PKNCA cohort uses a log-normal centred on the published median and truncated to the published range. This is why the cohort-level PKNCA gate is looser than the typical-value gates.
Assay lower limit of quantitation, and NCA on predictions rather than simulated observations. The paper states that samples below the LLOQ were set to missing but never prints the LLOQ of the FRETS-VWF73 assay, so no BLQ rule can be reproduced. This does not affect the PKNCA section, because NCA there is run on the individual prediction
Ccrather than on the residual-error-carryingsim: Table S2 is itself model-derived (from “individual post hoc parameters”), so predictions are the like-for-like comparison. It is also the only defensible choice for Cmax, which is an extreme and is therefore upward-biased by residual error - severely so for the PBT arm, whose peak (about 160 IU/L) is only twice the additive residual SD (79.9 IU/L). Users who want an observed-data NCA should run it onsimand expect that bias.Scale of the count models’ between-subject variability. Table S5 reports “BSV on B1” as 3.67 (thrombocytopenia) and 1.81 (LDH) with no footnote declaring a back-transform, unlike Table S6, which explicitly states that its IIV is a percent CV. These are therefore read as NONMEM OMEGA variances on the log-count scale. The alternative readings are excluded on magnitude: as a percent CV, 3.67 would be a 3.67% between-subject spread, which cannot produce the “count zero as well as the long-tailed distribution of PBT” that the paper credits this random effect with describing. Note that this choice does not affect any check in this vignette, because Table S7 (the 42-point gate) is computed at the typical value.
RTTE cohort size. The paper prints N = 41 for the count analysis (Table S5, Table S7) but no explicit N for the RTTE analysis. Figure 1 footnote a states that two patients in the RTTE analysis were excluded from the prophylaxis count cohort, so
population$n_subjectsis recorded as 43 for both RTTE models. This is inferred, not printed.RTTE exposure driver. The Methods describe the RTTE hazard as driven by a time-varying daily Cave. The
CAVcovariate is per-record and so can be driven time-varying, but the paper’s own published simulations (Table S8b) hold it constant at each arm’s median Cave - which this vignette confirms, by reproducing all eight cells to within 0.0008 with a constant CAV. Users wanting the paper’s nominal time-varying form should supply a time-varyingCAVcolumn; nothing in the model file prevents it.Cox proportional hazards model not packaged. As described in Part 4, a semiparametric model with an unestimated baseline hazard cannot be expressed as a self-contained rxode2 model. Its coefficients are validated arithmetically here instead.
Unit boundary between the PK and exposure-response models. The PK model is in IU/L and the exposure-response models take CAV in IU/mL, each matching its own source table. Every hand-off in this vignette divides by 1000 and is marked; a user wiring these models together must do the same.
New canonical names registered with this extraction. Two covariate columns,
TRT_PBTandTRT_PDFVIII_VWF(conforming members of the existingTRT_<arm>treatment-arm-indicator family), and two PD-output compartment names,thrombocytopenia_countandldh_elevation_count(conforming members of the existing per-interval event-count PD-output family alongsideseizure_count,hae_attacksandcel_count).
Not deviations, but worth recording: the paper reports no IIV on intercompartmental clearance or peripheral volume and no eta correlations (Table 1 leaves those cells blank), so the OMEGA matrix is diagonal with two elements; and there is no endogenous ADAMTS13 baseline term, because cTTP patients have activity below 10% of normal and sub-LLOQ samples were set to missing rather than imputed.