Aducanumab amyloid-beta QSP (Lin 2022)
Source:vignettes/articles/Lin_2022_aducanumab_qsp.Rmd
Lin_2022_aducanumab_qsp.RmdModel and source
Lin and colleagues (Biogen and Applied BioMath) built a quantitative systems pharmacology (QSP) model of the amyloid-beta (A-beta) pathway in Alzheimer’s disease and of the mechanism of action of aducanumab, a human IgG1 antibody that binds aggregated A-beta. The model was calibrated to literature A-beta biology, single-ascending-dose (SAD) PK and plasma A-beta, and one year of amyloid-PET SUVR from the phase Ib PRIME study, then validated against multiple-dose PK, two-year SUVR and a dose-titration cohort.
mod <- readModelDb("Lin_2022_aducanumab_qsp")
ui <- rxode2::rxode(mod)- Citation: Lin L, Hua F, Salinas C, Young C, Bussiere T, Apgar JF, Burke JM, Kandadi Muralidharan K, Rajagovindan R, Nestorov I. Quantitative systems pharmacology model for Alzheimer’s disease to predict the effect of aducanumab on brain amyloid. CPT Pharmacometrics Syst Pharmacol. 2022;11(3):362-372. doi:10.1002/psp4.12759. Reaction network, compartment volumes and parameter values from Supplementary Model Code (S2, KroneckerBio model file); parameter provenance and units from Table S2; pretreatment steady state from the Supplementary_Initial_Condition sheet (S3); equations from S4.
- Article: https://doi.org/10.1002/psp4.12759
- Supplementary material (Tables S1-S3, model code, initial conditions, equations): https://www.ebi.ac.uk/europepmc/webservices/rest/PMC8923729/supplementaryFiles
QSP. Amyloid-beta (A-beta) pathway and aducanumab mechanism-of-action model in early Alzheimer’s disease. Thirty-six mass-action ODE states in three physiologic compartments (plasma 3 L, CSF 0.139 L, brain interstitial fluid 0.261 L) plus a peripheral aducanumab compartment: APP synthesis and sequential beta-secretase (BACE) and gamma-secretase cleavage to A-beta monomer in plasma and brain ISF, shedding of soluble BACE, monomer-oligomer exchange in every compartment, oligomer-plaque exchange in brain ISF, inter-compartment transport of monomer, oligomer, soluble BACE, drug and soluble drug-A-beta complexes, two-compartment aducanumab disposition, drug binding to monomer, oligomer and plaque, and FcR-mediated antibody-dependent cellular phagocytosis (ADCP) that clears drug-oligomer and drug-plaque complexes in brain ISF. States are amounts in nmol; second-order rates divide by the volume of the compartment in which the reaction occurs. The primary PD output is the percent change in total brain plaque, which the paper equates with the percent change in amyloid-PET composite SUVR above a cutoff of 1.0. Deterministic: no between-subject variability or residual error is reported. Nominal (final) parameter set; the paper’s alternative faster-plaque-turnover set (Table S3) is reproduced in the vignette. The system is stiff and badly scaled (states span 1e-3 to 1.5e5 nmol): solve with tight tolerances, e.g. rxSolve(…, atol = 1e-14, rtol = 1e-10, maxsteps = 5e6); rxode2’s defaults fail.
The model is built as mass-action reactions between amounts (nmol). A
zero-order synthesis is an amount rate, a first-order reaction acts on
an amount, and a bimolecular reaction in a compartment of volume
V runs at k * x1 * x2 / V. This is the
convention of the KroneckerBio model file the authors deposited, and it
can be checked directly against the published initial conditions: for
example the APP-BACE complex in plasma sits at
konPP * APP * BACE / V / (koffBACE + kcatBACE) =
1e-3 * 28.936 * 88.529 / 3 / 120 = 0.0071157 nmol, which is
the published value to five digits.
Solver settings. Gamma-secretase sits at 150,000
nmol while the enzyme-substrate complexes are near 0.001 nmol, and
several binding reactions equilibrate in milliseconds (the plasma
oligomer dissociates at 1.4e5 /s). The solve is therefore stiff and
badly scaled. rxode2’s default tolerances fail, and so do some
intermediate settings (atol = 1e-12, rtol = 1e-10 fails at
0.3 mg/kg). The maintainers tested every simulation in this article at
several tolerance pairs. atol = 1e-14, rtol = 1e-10,
atol = 1e-13, rtol = 1e-10 and
atol = 1e-14, rtol = 1e-12 each solved all of them, and
gave the same results. The last of these emits step-size warnings on
multi-decade horizons. The helper below uses the first pair and falls
back to the other two. Use tolerances at least this tight for any
simulation with this model.
solve <- function(model, events, ...) {
tols <- list(c(1e-14, 1e-10), c(1e-13, 1e-10), c(1e-14, 1e-12))
for (i in seq_along(tols)) {
out <- tryCatch(
rxode2::rxSolve(
model, events, ...,
atol = tols[[i]][1], rtol = tols[[i]][2], maxsteps = 5e6,
returnType = "data.frame"
),
error = function(e) if (i == length(tols)) stop(e) else NULL
)
if (!is.null(out)) {
return(out)
}
}
}Population
The model represents a typical patient with early Alzheimer’s disease rather than a fitted population. It was calibrated to group means (Table S1): baseline concentrations of A-beta monomer, oligomer and plaque in plasma, CSF and brain interstitial fluid (ISF) from the literature; SILK A-beta kinetics in CSF; serum aducanumab PK and plasma A-beta from the SAD study (0.3-60 mg/kg, mild to moderate AD); CSF aducanumab concentrations; and 1-year SUVR from the placebo-controlled period of PRIME (1-10 mg/kg every 4 weeks, prodromal or mild AD). The paper reports no subject counts or demographics. Doses are converted from mg/kg at a body weight of 70 kg, which the maintainers inferred from Figure 2a (below).
str(ui$population, give.attr = FALSE)
#> List of 7
#> $ species : chr "human"
#> $ n_subjects : int NA
#> $ n_studies : int 3
#> $ disease_state: chr "Mild-to-moderate Alzheimer's disease (single-ascending-dose study) and prodromal or mild Alzheimer's disease wi"| __truncated__
#> $ dose_range : chr "Calibration: 0.3-60 mg/kg single i.v. dose (SAD) and 1-10 mg/kg i.v. q4w for 1 year (MAD placebo-controlled per"| __truncated__
#> $ regions : chr "Not reported (Biogen clinical program)"
#> $ notes : chr "QSP calibrated to group-mean data, not a population fit. Table S1 lists the calibration and validation data: li"| __truncated__Source trace
Every ini() value in the model file carries a comment
naming its source. The values are those of the deposited model code
(Supplementary Model Code, % Parameters), which prints four
or five significant figures; Table S2 prints three and gives the
provenance (“Fitted”, a literature reference, or “assumed”). The rows
where the two sources disagree, and how each was settled, are listed
under Parameter conflicts below.
| Element | Value used | Source |
|---|---|---|
| Plasma, CSF, brain ISF volumes | 3, 0.139, 0.261 L | Model code, % Compartments
|
| Reaction network (36 states, 65 reactions) | – | Model code, % Reactions; Equations in S4 |
| APP synthesis (plasma, ISF), degradation | 1.44405e-3, 2.5127e-4 nmol/s; 4.8135e-5 /s | Model code / Table S2; plasma rate from the S3 steady state |
| BACE synthesis (plasma, ISF), degradation, shedding | 1.08306e-3, 1.0365e-4 nmol/s; 1.2034e-5, 2.0e-7 /s | Model code / Table S2; plasma rate from the S3 steady state |
| Soluble BACE clearance | 6.4180e-5 /s | Model code / Table S2 |
| Gamma-secretase synthesis (plasma, ISF), clearance | 28.8811, 2.5127 nmol/s; 1.9254e-4 /s | Model code / Table S2 |
konPP, konPD, konPF
|
1e-3 /nM/s | Model code / Table S2 |
koffBACE, kcatBACE,
koffGamma, kcatGamma
|
119.9928, 0.0072, 215.9983, 0.0017 /s | Model code / Table S2 |
| A-beta clearance: monomer, oligomer (plasma); monomer, oligomer, plaque (ISF) | 9.627e-5, 9.627e-5, 1.9254e-5, 2.204e-8, 4.408e-9 /s | Model code / Table S2 |
kM2G; kG2M plasma, CSF, ISF |
1.4e-5; 1.4e5, 0.0028, 1.4e-8 /s | Model code (ISF value, see below) |
kG2P, kP2G_bisf
|
7e-8, 7e-11 /s | Model code / Table S2 |
| Transport plasma to/from CSF (all species) | 1.7222e-9 / 4.1667e-5 /s | Model code / Table S2 |
| Transport plasma to/from ISF: monomer, oligomer, sBACE, drug, drug-oligomer | 1.4811e-4/1.4811e-5, 1.4811e-6/1.4811e-8, 2.0056e-8/8.0226e-5, 1.6045e-6/0.0032, 1.6045e-6/1.4811e-8 /s | Model code / Table S2 |
| Transport ISF to CSF: monomer, oligomer, sBACE, drug, drug-oligomer | 1.5509e-5, 2.3264e-8, 1.5509e-5, 1.5509e-5, 2.3264e-8 /s | Model code / Table S2 |
Aducanumab elimination, k12mAb,
k21mAb
|
1.4586e-6, 2.5e-6, 1e-6 /s | Model code / Table S2 |
koffma0 (monomer) |
10 /s | Model code (see below) |
koffma1 (oligomer), koffma2 (plaque) |
0.02 /s | Table S2 and text (see below) |
FcR synthesis, degradation; koffPF;
kcatADCP
|
5.0253e-5 nmol/s, 1.9254e-4 /s; 10 /s; 0.0036 /s | Model code / Table S2 |
| Infusion | dose over 2 h into plasma | Table S2 (kinfusion); S4 |
| Initial conditions (pretreatment steady state) | 23 non-zero states | S3, Supplementary_Initial_Condition
|
| % plaque reduction | 100 (P(t) - P(0)) / P(0) | Methods, ‘SUVR data processing’ |
| Aducanumab molar mass | 150,000 g/mol | Not printed; back-solved from Figure 3a (below) |
| A-beta molar mass | 4,330 g/mol | Not printed; back-solved from Figure 2b (below) |
Parameter conflicts between the model code and Table S2
The deposited model code and Table S2 disagree on five values. Each was settled against a published output of the model rather than by preference for one document:
| Parameter | Model code | Table S2 | Used | Deciding evidence |
|---|---|---|---|---|
ksynthAPP_plasma |
0.0014 | 1.40e-3 | 1.44405e-3 nmol/s | S3 steady state |
ksynthBACE_plasma |
0.0011 | 1.10e-3 | 1.08306e-3 nmol/s | S3 steady state |
kG2M_bisf |
1.4000e-08 | 1.48e-08 | 1.40e-8 /s | S3 steady state |
koffma0 |
10 | 1.00 | 10 /s | Figure 2b |
koffma1, koffma2
|
0.0180 | 2.00e-2 | 0.02 /s | Figures 2c and 3a; text “20 nM” |
-
The two synthesis rates. The model code prints
every value at or above 0.001 with four decimals, so
0.0014and0.0011carry only two significant figures. Table S2 appears to transcribe that display. At steady state, APP synthesis must equalkclearAPP * APP + kcatBACE * APP_BACEand BACE synthesis must equal(kclearBACE + kcleave) * BACE. The published S3 state therefore fixes them at 1.44405e-3 and 1.08306e-3 nmol/s. With these two values the S3 state is a steady state of the model (checked below); with the printed0.0014every A-beta species drifts by about 1.2%, and plasma A-beta settles at 493 rather than the 500 pg/mL baseline drawn in Figure 2b. -
kG2M_bisf. With the code value 1.40e-8, every A-beta species in the S3 state is uniformly consistent. With the Table S2 value 1.48e-8 the ISF oligomer and plaque states drift away from the rest. -
koffma0. Monomer binding is what raises total plasma A-beta after a 60 mg/kg dose (Figure 2b). With the code value 10 /s the model reproduces the published curve; with 1 /s the simulated peak is about 4,600 pg/mL against a published peak of about 1,300. -
koffma1,koffma2. The text states that the drug-plaque affinity “was estimated to be 20 nM” (0.02 / 1e-3), matching Table S2. The code prints 0.0180. The 1-year and 2-year plaque curves of Figures 2c and 3a are reproduced with 0.02 (root-mean-square error 0.3 percentage points against the digitised curves), and are systematically overpredicted with 0.018 (error 2.8 points). Table S2 assumes the oligomer affinity equals the plaque affinity, so both are set to 0.02.
Pretreatment steady state
The model starts from the published pretreatment steady state (S3). The paper states that plaque had reached steady state before treatment (Results, ‘Model calibration’), so the untreated model should not move. The check below runs it for 1000 years. States that S3 prints with only three significant figures near 1e-11 nmol are excluded from the relative comparison.
ss <- solve(ui, rxode2::et(c(0, 1000 * 365.25)))
states <- ui$state
start <- unlist(ss[1, states])
end <- unlist(ss[2, states])
big <- abs(start) > 1e-6
drift <- abs(end[big] - start[big]) / abs(start[big])
signif(max(drift), 3)
#> [1] 9.86e-05
stopifnot(max(drift) < 2e-4)
baseline <- tibble::tribble(
~species, ~state, ~volume, ~paper_nM,
"A-beta monomer, plasma", "abeta_plasma", 3, 0.1,
"A-beta monomer, CSF", "abeta_csf", 0.139, 3,
"A-beta oligomer, brain ISF", "aolig_bisf", 0.261, 370,
"A-beta plaque, brain ISF", "aplaq_bisf", 0.261, 5500
) |>
mutate(model_nM = start[state] / volume)
baseline |>
dplyr::rename(
"Species" = species, "State" = state, "Volume (L)" = volume,
"Paper (approx., nM)" = paper_nM, "Model (nM)" = model_nM
) |>
knitr::kable(digits = 3, caption = "Baseline concentrations stated in the Results, 'Model calibration'.")| Species | State | Volume (L) | Paper (approx., nM) | Model (nM) |
|---|---|---|---|---|
| A-beta monomer, plasma | abeta_plasma | 3.000 | 0.1 | 0.115 |
| A-beta monomer, CSF | abeta_csf | 0.139 | 3.0 | 2.995 |
| A-beta oligomer, brain ISF | aolig_bisf | 0.261 | 370.0 | 368.306 |
| A-beta plaque, brain ISF | aplaq_bisf | 0.261 | 5500.0 | 5756.847 |
Aducanumab PK (Figure 2a)
Figure 2a shows total plasma aducanumab after single 2-hour infusions of 0.3-60 mg/kg. The paper does not state the body weight used to convert mg/kg to an amount. The maintainers digitised the model lines of Figure 2a at times free of data symbols. In ug/mL the plotted concentration depends on body weight and not on the molar mass (which cancels between dose and output). A 70 kg patient reproduces the lines to within about 1%.
fig2a <- tibble::tribble(
~mgkg, ~week, ~Cc_fig,
0.3, 4.5, 0.4549, 0.3, 8, 0.2364, 0.3, 9, 0.1936, 0.3, 13, 0.0902,
0.3, 15, 0.0616, 0.3, 17, 0.0420, 0.3, 21, 0.0200, 0.3, 22.5, 0.0146,
1, 4.5, 1.5099, 1, 8, 0.7848, 1, 9, 0.6425, 1, 13, 0.3049,
1, 15, 0.2082, 1, 17, 0.1396, 1, 21, 0.0662, 1, 22.5, 0.0495,
10, 4.5, 15.4694, 10, 8, 7.8953, 10, 9, 6.5830, 10, 13, 3.0126,
10, 15, 2.0944, 10, 17, 1.4298, 10, 21, 0.6663, 10, 22.5, 0.4982
)
wt <- 70
sad_doses <- c(0.3, 1, 3, 10, 20, 30, 60)
sad_times <- sort(unique(c(0, 10^seq(-3, log10(25 * 7), length.out = 150), fig2a$week * 7)))
sad <- bind_rows(lapply(sad_doses, function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central") |>
rxode2::et(sad_times)
solve(ui, ev) |> mutate(mgkg = d)
}))
# The time-0 row (no drug yet) cannot be drawn on a log axis.
ggplot(sad[sad$time != 0, ], aes(time / 7, Cc, colour = factor(mgkg))) +
geom_line() +
geom_point(data = fig2a, aes(week, Cc_fig), inherit.aes = FALSE, shape = 1) +
scale_y_log10() +
coord_cartesian(ylim = c(1e-2, 1e4)) +
labs(
x = "Time (weeks)", y = "Total aducanumab in plasma (ug/mL)", colour = "mg/kg",
caption = "Replicates Figure 2a of Lin 2022. Open circles: model lines digitised from the figure."
)
cmp2a <- fig2a |>
left_join(sad |> mutate(week = round(time / 7, 6)) |> select(mgkg, week, Cc), by = c("mgkg", "week")) |>
mutate(ratio = Cc_fig / Cc)
stopifnot(nrow(cmp2a) == nrow(fig2a), !anyNA(cmp2a$Cc))
summary(cmp2a$ratio)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 0.9829 0.9922 1.0013 0.9997 1.0073 1.0121
stopifnot(abs(median(cmp2a$ratio) - 1) < 0.02, all(abs(cmp2a$ratio - 1) < 0.05))Non-compartmental analysis of the SAD simulation
The paper reports no NCA. The check below confirms two properties of
the PK layer. First, exposure is nearly dose-proportional across the
200-fold SAD range (Figure 2a’s lines are parallel). The small departure
is target-mediated: binding to plaque and ADCP in brain ISF consume a
fixed amount of drug, which is a slightly larger fraction of a small
dose. AUC per mg therefore rises monotonically with dose, by about 1.4%
from 0.3 to 60 mg/kg. Second, the drug is lost almost entirely through
first-order plasma elimination, so AUC0-inf is 96-98% of
Dose / (kclearmAb * Vplasma).
# 750 days is about 29 terminal half-lives; a longer grid decays into solver
# noise and gives PKNCA non-positive concentrations to log.
nca_times <- sort(unique(c(0, 10^seq(-3, log10(750), length.out = 300))))
sad_nca <- bind_rows(lapply(sad_doses, function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central") |>
rxode2::et(nca_times)
solve(ui, ev) |> mutate(treatment = paste(d, "mg/kg"), id = 1L)
})) |>
filter(!is.na(Cc)) |>
select(id, treatment, time, Cc)
stopifnot(all(sad_nca$Cc >= 0))
dose_df <- tibble(
id = 1L, treatment = paste(sad_doses, "mg/kg"),
time = 0, amt = sad_doses * wt, dur = 2 / 24
)
conc_obj <- PKNCA::PKNCAconc(sad_nca, Cc ~ time | treatment + id)
dose_obj <- PKNCA::PKNCAdose(dose_df, amt ~ time | treatment + id, duration = "dur")
intervals <- data.frame(
start = 0, end = Inf, cmax = TRUE, tmax = TRUE,
aucinf.obs = TRUE, half.life = TRUE
)
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals))
nca_tab <- as.data.frame(nca_res) |>
filter(PPTESTCD %in% c("cmax", "tmax", "aucinf.obs", "half.life")) |>
select(treatment, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
mutate(
dose_mg = as.numeric(sub(" mg/kg", "", treatment)) * wt,
auc_per_mg = aucinf.obs / dose_mg,
fraction_plasma_elim = aucinf.obs * 1.4586e-6 * 86400 * 3 / dose_mg
) |>
arrange(dose_mg)
nca_tab |>
dplyr::rename(
"Dose" = treatment, "Cmax (ug/mL)" = cmax, "Tmax (day)" = tmax,
"AUC0-inf (ug*day/mL)" = aucinf.obs, "t1/2 (day)" = half.life,
"Dose (mg)" = dose_mg, "AUC0-inf / dose" = auc_per_mg,
"AUC x kclearmAb x V / dose" = fraction_plasma_elim
) |>
knitr::kable(digits = 3, caption = "PKNCA on the simulated SAD profiles (70 kg).")| Dose | Cmax (ug/mL) | Tmax (day) | t1/2 (day) | AUC0-inf (ug*day/mL) | Dose (mg) | AUC0-inf / dose | AUC x kclearmAb x V / dose |
|---|---|---|---|---|---|---|---|
| 0.3 mg/kg | 6.860 | 0.084 | 25.383 | 53.603 | 21 | 2.553 | 0.965 |
| 1 mg/kg | 22.866 | 0.084 | 25.384 | 178.730 | 70 | 2.553 | 0.965 |
| 3 mg/kg | 68.598 | 0.084 | 25.386 | 536.641 | 210 | 2.555 | 0.966 |
| 10 mg/kg | 228.659 | 0.084 | 25.393 | 1793.529 | 700 | 2.562 | 0.969 |
| 20 mg/kg | 457.318 | 0.084 | 25.401 | 3598.151 | 1400 | 2.570 | 0.972 |
| 30 mg/kg | 685.979 | 0.084 | 25.407 | 5410.641 | 2100 | 2.576 | 0.974 |
| 60 mg/kg | 1371.964 | 0.084 | 25.411 | 10877.773 | 4200 | 2.590 | 0.979 |
stopifnot(
nrow(nca_tab) == length(sad_doses),
# near dose proportionality over 0.3-60 mg/kg ...
diff(range(nca_tab$auc_per_mg)) / mean(nca_tab$auc_per_mg) < 0.02,
# ... with the target-mediated sink shrinking as dose rises
all(diff(nca_tab$auc_per_mg) > 0),
# nearly all of the dose is eliminated from plasma at kclearmAb
all(nca_tab$fraction_plasma_elim > 0.95 & nca_tab$fraction_plasma_elim <= 1)
)Brain penetration (Figure S2)
The model was calibrated to a CSF-to-plasma aducanumab concentration ratio of about 0.5% at steady state (Results; Figure S2).
ev <- rxode2::et(amt = 10 * wt, dur = 2 / 24, cmt = "central", ii = 28, addl = 12) |>
rxode2::et(seq(0, 364, by = 1))
csf <- solve(ui, ev) |> mutate(ratio_pct = 100 * Ccsf / Cc)
trough_ratio <- csf$ratio_pct[csf$time == 363]
trough_ratio
#> [1] 0.5017325
stopifnot(length(trough_ratio) == 1, abs(trough_ratio - 0.5) < 0.05)Plasma A-beta (Figure 2b)
Total plasma A-beta (free plus drug-bound monomer and oligomer) rises after dosing because the drug-A-beta complex is cleared about 66-fold more slowly than free monomer. Figure 2b reports concentrations in pg/mL; the S3 plasma A-beta state (0.1155 nM) is drawn at 500 pg/mL, which corresponds to a molar mass of 4,330 g/mol (A-beta 1-40).
The 60 mg/kg group had a higher baseline (about 700 pg/mL), and the figure caption states that beta-secretase was raised for that group alone. The paper does not give the adjusted value. Total A-beta is linear in the amount of A-beta produced (drug binding to monomer is far from saturation), so the published 60 mg/kg curve should equal the nominal-baseline simulation times a constant. It does: the ratio stays within about 1.37-1.41 throughout.
fig2b <- tibble::tribble(
~mgkg, ~week, ~abeta_fig,
30, 0.03, 708, 30, 0.1, 701, 30, 0.3, 636, 30, 0.5, 593,
30, 1, 548, 30, 3, 521, 30, 4, 518, 30, 5, 513,
# The 60 mg/kg line is not used at 0.03 weeks: it is still rising steeply
# there, so a one-pixel error in time moves the digitised value by >5%.
60, 0.06, 1309, 60, 0.3, 1103, 60, 0.5, 977,
60, 1, 824, 60, 3, 754
)
b_times <- sort(unique(c(0, seq(0, 6 * 7, length.out = 300), fig2b$week * 7)))
sim2b <- bind_rows(lapply(sad_doses, function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central") |>
rxode2::et(b_times)
solve(ui, ev) |> mutate(mgkg = d)
}))
ggplot(sim2b, aes(time / 7, abeta_plasma_total, colour = factor(mgkg))) +
geom_line() +
geom_point(data = fig2b, aes(week, abeta_fig), inherit.aes = FALSE, shape = 1) +
labs(
x = "Time (weeks)", y = "Total A-beta in plasma (pg/mL)", colour = "mg/kg",
caption = paste(
"Replicates Figure 2b of Lin 2022 at the nominal baseline. Open circles:",
"model lines digitised from the figure (the 60 mg/kg line sits on a raised baseline)."
)
)
cmp2b <- fig2b |>
left_join(sim2b |> mutate(week = round(time / 7, 6)) |> select(mgkg, week, abeta_plasma_total),
by = c("mgkg", "week")
) |>
mutate(ratio = abeta_fig / abeta_plasma_total)
stopifnot(nrow(cmp2b) == nrow(fig2b), !anyNA(cmp2b$ratio))
cmp2b |>
dplyr::rename(
"mg/kg" = mgkg, "Week" = week, "Figure 2b (pg/mL)" = abeta_fig,
"Model (pg/mL)" = abeta_plasma_total, "Figure / model" = ratio
) |>
knitr::kable(digits = 2)| mg/kg | Week | Figure 2b (pg/mL) | Model (pg/mL) | Figure / model |
|---|---|---|---|---|
| 30 | 0.03 | 708 | 697.64 | 1.01 |
| 30 | 0.10 | 701 | 703.17 | 1.00 |
| 30 | 0.30 | 636 | 635.13 | 1.00 |
| 30 | 0.50 | 593 | 592.16 | 1.00 |
| 30 | 1.00 | 548 | 546.88 | 1.00 |
| 30 | 3.00 | 521 | 520.16 | 1.00 |
| 30 | 4.00 | 518 | 515.80 | 1.00 |
| 30 | 5.00 | 513 | 512.33 | 1.00 |
| 60 | 0.06 | 1309 | 937.30 | 1.40 |
| 60 | 0.30 | 1103 | 781.20 | 1.41 |
| 60 | 0.50 | 977 | 689.27 | 1.42 |
| 60 | 1.00 | 824 | 594.86 | 1.39 |
| 60 | 3.00 | 754 | 541.72 | 1.39 |
Plaque reduction over one and two years (Figures 2c and 3a)
The paper compares the percent change of total brain plaque with the percent change of composite SUVR above a cutoff of 1.0. PRIME dosed every 4 weeks for one year (14 doses) and continued into a long-term extension (LTE) to week 110. In the LTE the 1 mg/kg group was switched to 3 mg/kg. The model curves of Figure 3a were digitised by the maintainers at times free of data symbols.
fig3a <- tibble::tribble(
~arm, ~week, ~pct_fig,
"1 -> 3 mg/kg", 15, -3.0, "1 -> 3 mg/kg", 20, -4.2, "1 -> 3 mg/kg", 35, -7.5,
"1 -> 3 mg/kg", 45, -9.7, "1 -> 3 mg/kg", 65, -16.7, "1 -> 3 mg/kg", 75, -22.0,
"1 -> 3 mg/kg", 90, -29.7, "1 -> 3 mg/kg", 100, -33.7,
"3 mg/kg", 15, -9.0, "3 mg/kg", 20, -11.8, "3 mg/kg", 35, -20.9, "3 mg/kg", 45, -26.2,
"3 mg/kg", 65, -35.1, "3 mg/kg", 75, -38.9, "3 mg/kg", 90, -44.3, "3 mg/kg", 100, -47.1,
"6 mg/kg", 15, -16.6, "6 mg/kg", 20, -22.0, "6 mg/kg", 35, -36.4, "6 mg/kg", 45, -44.3,
"6 mg/kg", 65, -56.6, "6 mg/kg", 75, -61.1, "6 mg/kg", 90, -67.0, "6 mg/kg", 100, -70.0,
"10 mg/kg", 15, -25.2, "10 mg/kg", 20, -32.7, "10 mg/kg", 35, -51.3, "10 mg/kg", 45, -60.7,
"10 mg/kg", 65, -73.2, "10 mg/kg", 75, -77.3, "10 mg/kg", 90, -82.1, "10 mg/kg", 100, -84.2
)
arms <- list(
"1 -> 3 mg/kg" = c(rep(1, 14), rep(3, 14)),
"3 mg/kg" = rep(3, 28), "6 mg/kg" = rep(6, 28), "10 mg/kg" = rep(10, 28)
)
p_times <- sort(unique(c(seq(0, 110 * 7, by = 7), fig3a$week * 7)))
sim3a <- bind_rows(lapply(names(arms), function(a) {
mg <- arms[[a]]
ev <- rxode2::et(time = (seq_along(mg) - 1) * 28, amt = mg * wt, dur = 2 / 24, cmt = "central") |>
rxode2::et(p_times)
solve(ui, ev) |> mutate(arm = a)
}))
ggplot(sim3a, aes(time / 7, pct_plaque, colour = arm)) +
geom_line() +
geom_point(data = fig3a, aes(week, pct_fig), inherit.aes = FALSE, shape = 1) +
labs(
x = "Time (weeks)", y = "Plaque reduction (%)", colour = NULL,
caption = "Replicates Figures 2c and 3a of Lin 2022. Open circles: model lines digitised from Figure 3a."
)
cmp3a <- fig3a |>
left_join(sim3a |> mutate(week = round(time / 7, 6)) |> select(arm, week, pct_plaque), by = c("arm", "week")) |>
mutate(diff = pct_plaque - pct_fig)
stopifnot(nrow(cmp3a) == nrow(fig3a), !anyNA(cmp3a$diff))
c(mean = mean(cmp3a$diff), rms = sqrt(mean(cmp3a$diff^2)), max_abs = max(abs(cmp3a$diff)))
#> mean rms max_abs
#> -0.2756460 0.3225926 0.5365600
stopifnot(sqrt(mean(cmp3a$diff^2)) < 1, max(abs(cmp3a$diff)) < 2)Dose titration (Figure 3b)
The titration cohort received 1 mg/kg for two doses, 3 mg/kg for four, 6 mg/kg for five and 10 mg/kg for two, all every 4 weeks. It was not used for calibration.
fig3b <- tibble::tribble(
~week, ~pct_fig,
5, -0.9, 10, -3.0, 15, -5.5, 20, -8.4, 25, -12.9,
30, -17.9, 35, -22.8, 40, -27.4, 45, -33.4, 50, -39.5
)
titr <- c(1, 1, 3, 3, 3, 3, 6, 6, 6, 6, 6, 10, 10)
ev <- rxode2::et(time = (seq_along(titr) - 1) * 28, amt = titr * wt, dur = 2 / 24, cmt = "central") |>
rxode2::et(sort(unique(c(seq(0, 52 * 7, by = 1), fig3b$week * 7))))
sim3b <- solve(ui, ev)
ggplot(sim3b, aes(time / 7, pct_plaque)) +
geom_line() +
geom_point(data = fig3b, aes(week, pct_fig), shape = 1) +
labs(
x = "Time (weeks)", y = "Plaque change (%)",
caption = "Replicates Figure 3b of Lin 2022. Open circles: model line digitised from the figure."
)
Long-term treatment and recovery (Figure 4)
Figure 4a extends every-4-week dosing to 10 years, and Figure 4c stops after one year of treatment (13 doses) and follows plaque recovery to week 519. The endogenous plaque turnover is slow (about 25 years to steady state), so plaque recovers slowly.
fig4a <- tibble::tribble(
~mgkg, ~year, ~pct_fig,
1, 1.5, -16.1, 1, 3, -27.4, 1, 5, -37.0, 1, 9.9, -46.8,
3, 1.5, -40.2, 3, 3, -59.7, 3, 5, -70.5, 3, 9.9, -75.9,
6, 1.5, -62.5, 6, 3, -80.7, 6, 5, -86.2, 6, 9.9, -87.2,
10, 1.5, -78.2, 10, 3, -90.4, 10, 5, -92.2, 10, 9.9, -92.4
)
fig4c <- tibble::tribble(
~mgkg, ~week, ~pct_fig,
1, 56, -11.3, 1, 100, -10.3, 1, 200, -7.9, 1, 300, -5.9, 1, 519, -3.5,
3, 56, -29.7, 3, 100, -27.2, 3, 200, -20.7, 3, 300, -15.9, 3, 519, -8.7,
6, 56, -49.1, 6, 100, -45.1, 6, 200, -34.3, 6, 300, -26.2, 6, 519, -14.7,
10, 56, -65.7, 10, 100, -59.9, 10, 200, -45.8, 10, 300, -35.1, 10, 519, -19.5
)
yr <- 365.25
sim4a <- bind_rows(lapply(c(1, 3, 6, 10), function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central", ii = 28, addl = 130) |>
rxode2::et(sort(unique(c(seq(0, 10 * yr, by = 14), fig4a$year * yr))))
solve(ui, ev) |> mutate(mgkg = d, regimen = "10 years of treatment")
}))
sim4c <- bind_rows(lapply(c(1, 3, 6, 10), function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central", ii = 28, addl = 12) |>
rxode2::et(sort(unique(c(seq(0, 520 * 7, by = 7), fig4c$week * 7))))
solve(ui, ev) |> mutate(mgkg = d, regimen = "1 year of treatment, then recovery")
}))
bind_rows(sim4a, sim4c) |>
ggplot(aes(time / 7, pct_plaque, colour = factor(mgkg))) +
geom_line() +
facet_wrap(~regimen) +
labs(
x = "Time (weeks)", y = "Plaque reduction (%)", colour = "mg/kg q4w",
caption = "Replicates Figures 4a and 4c of Lin 2022 (nominal parameter set)."
)
cmp4 <- bind_rows(
fig4a |>
left_join(sim4a |> mutate(year = round(time / yr, 6)) |> select(mgkg, year, pct_plaque), by = c("mgkg", "year")) |>
mutate(panel = "4a"),
fig4c |>
left_join(sim4c |> mutate(week = round(time / 7, 6)) |> select(mgkg, week, pct_plaque), by = c("mgkg", "week")) |>
mutate(panel = "4c")
) |>
mutate(diff = pct_plaque - pct_fig)
stopifnot(nrow(cmp4) == nrow(fig4a) + nrow(fig4c), !anyNA(cmp4$diff))
cmp4 |>
group_by(panel) |>
summarise(rms = sqrt(mean(diff^2)), max_abs = max(abs(diff)))
#> # A tibble: 2 × 3
#> panel rms max_abs
#> <chr> <dbl> <dbl>
#> 1 4a 0.355 0.674
#> 2 4c 0.293 0.578
stopifnot(sqrt(mean(cmp4$diff^2)) < 1.5, max(abs(cmp4$diff)) < 3)Soluble oligomer (Figure 5)
The model assumes that aducanumab clears soluble oligomer through ADCP with the same rate constants as plaque. Figure 5 shows the resulting band of free oligomer in brain ISF, which oscillates with each 4-weekly dose. The published 10 mg/kg band spans about -22% to -43% at steady state.
sim5 <- bind_rows(lapply(c(1, 3, 6, 10), function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central", ii = 28, addl = 130) |>
rxode2::et(seq(0, 10 * yr, by = 1))
solve(ui, ev) |> mutate(mgkg = d)
}))
ggplot(sim5, aes(time / yr, pct_aolig_free, colour = factor(mgkg))) +
geom_line(linewidth = 0.2) +
labs(
x = "Time (years)", y = "Free oligomer change (%)", colour = "mg/kg q4w",
caption = "Replicates Figure 5a of Lin 2022."
)
# The trough of free oligomer falls within hours of each infusion, so the band
# is measured on a fine grid over the last year rather than the daily grid
# used for the plot.
ev10 <- rxode2::et(amt = 10 * wt, dur = 2 / 24, cmt = "central", ii = 28, addl = 130) |>
rxode2::et(seq(9 * yr, 10 * yr, by = 0.02))
band10 <- range(solve(ui, ev10)$pct_aolig_free)
band10
#> [1] -43.70380 -22.57304
stopifnot(abs(band10[1] - (-43)) < 1.5, abs(band10[2] - (-22)) < 1.5)Regimens with the same total dose (Figure 6)
Figure 6 compares regimens with the same total dose and shows that they give nearly the same plaque reduction over 10 years. The figure legend lists 6 mg/kg every 4 weeks, 12 mg/kg every 8 weeks, 18 mg/kg every 12 weeks and 36 mg/kg every 24 weeks. The Results text and the caption instead list 10/20/30/60 mg/kg. The plotted plateau near -88% is the 6 mg/kg every-4-weeks plateau of Figure 4a; 10 mg/kg every 4 weeks plateaus at -92%. So the legend is correct, and the text and caption are in error.
regs <- tibble::tribble(
~label, ~mgkg, ~ii,
"6 mg/kg Q4W", 6, 28, "12 mg/kg Q8W", 12, 56,
"18 mg/kg Q12W", 18, 84, "36 mg/kg Q24W", 36, 168
)
sim6 <- bind_rows(lapply(seq_len(nrow(regs)), function(i) {
n <- ceiling(10 * yr / regs$ii[i])
ev <- rxode2::et(amt = regs$mgkg[i] * wt, dur = 2 / 24, cmt = "central", ii = regs$ii[i], addl = n - 1) |>
rxode2::et(seq(0, 10 * yr, by = 3))
solve(ui, ev) |> mutate(regimen = regs$label[i])
}))
ggplot(sim6, aes(time / 7, pct_plaque, colour = regimen)) +
geom_line() +
labs(
x = "Time (weeks)", y = "Plaque reduction (%)", colour = NULL,
caption = "Replicates Figure 6 of Lin 2022 (legend doses)."
)
late6 <- sim6 |>
filter(time > 8 * yr) |>
group_by(regimen) |>
summarise(min = min(pct_plaque), max = max(pct_plaque))
late6
#> # A tibble: 4 × 3
#> regimen min max
#> <chr> <dbl> <dbl>
#> 1 12 mg/kg Q8W -87.0 -86.5
#> 2 18 mg/kg Q12W -86.6 -85.8
#> 3 36 mg/kg Q24W -85.8 -83.4
#> 4 6 mg/kg Q4W -87.3 -87.1
q4w_late <- late6$min[late6$regimen == "6 mg/kg Q4W"]
stopifnot(length(q4w_late) == 1, abs(q4w_late - (-88)) < 1.5, all(late6$min < -85))Alternative parameter set (Figure 4b, Table S3)
To show that the 1-year SUVR data cannot identify the endogenous
plaque turnover, the authors recalibrated the model with plaque
clearance fixed at five times its nominal value (Table S3). They then
adjusted seven other parameters so that the 1-year data were still
matched. This alternative set is a robustness exploration, not the final
model: it fails to predict the week-110 SUVR data. It is not shipped as
a separate model but can be applied with ini().
The alternative set has a different pretreatment steady state, and S3
gives only the nominal one. The simulation below therefore first runs
the model untreated for 200 years. It then starts the dosing simulation
from that state (passed as inits), and measures plaque
change relative to the plaque at the first dose.
s2d <- 86400
alt <- suppressMessages(mod |>
rxode2::ini(
lkclearaplaq = log(5 * 4.4080e-09 * s2d),
lkoffma2 = log(2.0e-02 / 1.5 * s2d),
lkm2g = log(3 * 1.4e-05 * s2d),
lkg2p = log(5 * 7.0e-08 * s2d),
lk31abeta = log(4.1667e-05 / 1.5 * s2d),
lk31aolig = log(4.1667e-05 / 1.5 * s2d),
lk31bace = log(4.1667e-05 / 1.5 * s2d),
lk31mab = log(4.1667e-05 / 1.5 * s2d),
lk31mix = log(4.1667e-05 / 1.5 * s2d),
lk43abeta = log(1.5509e-05 / 1.5 * s2d),
lk43aolig = log(2.3264e-08 / 1.5 * s2d),
lk43bace = log(1.5509e-05 / 1.5 * s2d),
lk43mab = log(1.5509e-05 / 1.5 * s2d),
lk43mix = log(2.3264e-08 / 1.5 * s2d),
lksynthapp_plasma = log(1.2 * 1.44405e-03 * s2d),
lksynthapp_bisf = log(3 * 2.5127e-04 * s2d)
))
runin <- solve(alt, rxode2::et(c(0, 200 * yr)))
alt_ss <- unlist(runin[2, ui$state])
fig4b <- tibble::tribble(
~mgkg, ~year, ~pct_fig,
1, 1.5, -14.3, 1, 3, -18.5, 1, 9.9, -20.0,
3, 1.5, -35.2, 3, 3, -41.7, 3, 9.9, -43.4,
6, 1.5, -54.1, 6, 3, -59.7, 6, 9.9, -60.2,
10, 1.5, -68.2, 10, 3, -71.2, 10, 9.9, -71.3
)
sim4b <- bind_rows(lapply(c(1, 3, 6, 10), function(d) {
ev <- rxode2::et(amt = d * wt, dur = 2 / 24, cmt = "central", ii = 28, addl = 130) |>
rxode2::et(sort(unique(c(seq(0, 10 * yr, by = 14), fig4b$year * yr))))
solve(alt, ev, inits = alt_ss) |>
mutate(
mgkg = d, year = round(time / yr, 6),
pct = 100 * (plaque_bisf / plaque_bisf[1] - 1)
)
}))
alt_base <- sim4b$plaque_bisf[sim4b$mgkg == 1][1]
c(alt_baseline_plaque_nM = alt_base, nominal_baseline_plaque_nM = 1502.537143 / 0.261)
#> alt_baseline_plaque_nM nominal_baseline_plaque_nM
#> 6015.912 5756.847
ggplot(sim4b, aes(year, pct, colour = factor(mgkg))) +
geom_line() +
geom_point(data = fig4b, aes(year, pct_fig), inherit.aes = FALSE, shape = 1) +
labs(
x = "Time (years)", y = "Plaque reduction (%)", colour = "mg/kg q4w",
caption = "Replicates Figure 4b of Lin 2022 (alternative parameter set). Open circles: digitised model lines."
)
cmp4b <- fig4b |>
left_join(sim4b |> select(mgkg, year, pct), by = c("mgkg", "year")) |>
mutate(diff = pct - pct_fig)
stopifnot(nrow(cmp4b) == nrow(fig4b), !anyNA(cmp4b$diff))
c(mean = mean(cmp4b$diff), max_abs = max(abs(cmp4b$diff)))
#> mean max_abs
#> -1.948578 2.788940
stopifnot(max(abs(cmp4b$diff)) < 4)The alternative set reproduces Figure 4b to within a few percentage points. It is less exact than the nominal set because Table S3 reports the adjustments only as rounded fold changes (“1.5-fold decrease”, “3-fold increase”).
Assumptions and deviations
-
Parameter conflicts. The deposited model code and
Table S2 disagree on
ksynthAPP_plasma,ksynthBACE_plasma,kG2M_bisf,koffma0andkoffma1/koffma2. Each was settled against a published model output (the S3 steady state, Figure 2b, or Figures 2c/3a and the text); see Parameter conflicts above. No value was tuned: every value used is printed in the code, in Table S2, or follows exactly from the published steady state. - Body weight and molar masses (non-paper-derived). The paper gives doses in mg/kg and simulated in nmol but prints neither the body weight nor the molar masses. The maintainers inferred all three from the paper’s own figures: 70 kg from the Figure 2a lines (within about 1%); 150,000 g/mol for aducanumab from the Figure 3a plaque curves (root-mean-square error 0.3 percentage points, against 1.0 at the sequence-derived 145,912 g/mol that another model in this library uses); and 4,330 g/mol for A-beta from the 500 pg/mL baseline of Figure 2b. The model takes doses in mg; for a mg/kg regimen, multiply by the patient’s weight.
-
APP input. The model code writes APP synthesis
through an input
APP_IN. The paper switches this input off only to simulate the SILK labelling experiment used in calibration (Figure S1). Every other simulation has it on, so it is encoded here as a constant synthesis rate. The SILK simulation is not reproduced. - 60 mg/kg baseline. Figure 2b’s 60 mg/kg group was simulated with a raised beta-secretase level that the paper does not report. This article compares that curve after scaling by a constant factor (see Figure 2b above).
- Figure 6 doses. The Results text and the Figure 6 caption give 10, 20, 30 and 60 mg/kg; the legend and the plotted plateau correspond to 6, 12, 18 and 36 mg/kg. The legend doses are used.
- Equation typos in S4. The typeset equations (S4) differ from the model code in a few places. They show a negative sign on A-beta formation in brain ISF, drug-oligomer binding in brain ISF driven by CSF oligomer, and no oligomer-FcR binding term in the FcR equation. The model follows the reaction list of the deposited code, which is internally consistent and reproduces the published steady state and figures.
- Deterministic model. No between-subject variability or residual error is reported, and none is included.
- Errata. No correction notice for this article was found as of 2026-09-30.