using Pumas
using PumasUtilities
using AlgebraOfGraphics
using CairoMakie
using Random
using DataFrames
TMDD Part 2: The Complete Mechanistic Model
1 Introduction
This is Part 2 of our comprehensive TMDD tutorial series. Here we implement the complete mechanistic TMDD model as proposed by Mager & Jusko (2001). This model explicitly tracks ligand, receptor, and complex dynamics.
Tutorial Series Overview:
- Part 1: Understanding TMDD - Conceptual foundations and parameter meaning 1b. Part 1b: TMDD Dynamics Through the Four Phases - Deep dive into phase characterization using Peletier & Gabrielsson (2012)
- Part 2: The Full TMDD Model (this tutorial) - Complete mechanistic model implementation
- Part 3: TMDD Approximations - QE, QSS, and Michaelis-Menten models
- Part 4: Special Cases and Extensions - Advanced TMDD scenarios
- Part 5: Practical Workflows - Fitting, diagnostics, and model selection
2 Learning Objectives
By the end of this tutorial, you will be able to:
- Write the complete TMDD differential equations
- Implement one- and two-compartment full TMDD models in Pumas
- Understand the role of initial conditions and baseline states
- Simulate and visualize all model species (ligand, receptor, complex)
- Handle practical implementation challenges (units, concentrations, stiffness)
- Perform parameter sensitivity analysis
3 Libraries
4 Mathematical Framework
The full TMDD model describes the interaction between a drug (ligand, \(L\)) and its pharmacological target (receptor, \(R\)) through a system of ordinary differential equations.
4.1 One-Compartment Model Equations
For a one-compartment model with IV dosing:
Ligand (free drug) dynamics:
\[ \frac{dL}{dt} = -k_{el} \cdot L - k_{on} \cdot L \cdot R + k_{off} \cdot LR \]
Receptor (free target) dynamics:
\[ \frac{dR}{dt} = k_{syn} - k_{deg} \cdot R - k_{on} \cdot L \cdot R + k_{off} \cdot LR \]
Complex (drug-target) dynamics:
\[ \frac{dLR}{dt} = k_{on} \cdot L \cdot R - k_{off} \cdot LR - k_{int} \cdot LR \]
Where the parameters are:
| Parameter | Units | Description |
|---|---|---|
| \(k_{el}\) | hr\(^{-1}\) | Linear elimination rate constant (\(= CL/V_c\)) |
| \(k_{on}\) | nM\(^{-1}\)hr\(^{-1}\) | Second-order association rate constant |
| \(k_{off}\) | hr\(^{-1}\) | First-order dissociation rate constant |
| \(k_{syn}\) | nM/hr | Zero-order receptor synthesis rate |
| \(k_{deg}\) | hr\(^{-1}\) | First-order receptor degradation rate |
| \(k_{int}\) | hr\(^{-1}\) | First-order complex internalization rate |
4.2 Initial Conditions
Before drug administration, the system is at steady state:
- Ligand: \(L(0) = 0\) (no drug before dosing)
- Receptor: \(R(0) = R_0 = k_{syn}/k_{deg}\) (baseline receptor level)
- Complex: \(LR(0) = 0\) (no complex before dosing)
The baseline receptor concentration \(R_0\) is determined by the balance between synthesis and degradation.
4.3 Mass Balance Relationships
The total ligand and total receptor concentrations are conserved:
\[ L_{tot} = L + LR \]
\[ R_{tot} = R + LR \]
These relationships are useful for model verification and data analysis.
5 One-Compartment Full TMDD Model in Pumas
Let’s implement the one-compartment full TMDD model:
tmdd_full_1cmt = @model begin
@metadata begin
desc = "One-compartment Full TMDD Model"
timeu = u"hr"
end
@param begin
"""
Clearance (L/hr)
"""
tvCl ∈ RealDomain(lower = 0)
"""
Central volume of distribution (L)
"""
tvVc ∈ RealDomain(lower = 0)
"""
Receptor synthesis rate (nM/hr)
"""
tvKsyn ∈ RealDomain(lower = 0)
"""
Receptor degradation rate (hr⁻¹)
"""
tvKdeg ∈ RealDomain(lower = 0)
"""
Association rate constant (nM⁻¹hr⁻¹)
"""
tvKon ∈ RealDomain(lower = 0)
"""
Dissociation rate constant (hr⁻¹)
"""
tvKoff ∈ RealDomain(lower = 0)
"""
Internalization rate constant (hr⁻¹)
"""
tvKint ∈ RealDomain(lower = 0)
end
@pre begin
Cl = tvCl
Vc = tvVc
Ksyn = tvKsyn
Kdeg = tvKdeg
Kon = tvKon
Koff = tvKoff
Kint = tvKint
end
@dosecontrol begin
# Bioavailability trick: dose enters as concentration (amount/Vc)
bioav = (Ligand = 1 / tvVc,)
end
@init begin
# Baseline receptor level before drug administration
Receptor = Ksyn / Kdeg
end
@vars begin
# Derived rate constants and quantities
Kel := Cl / Vc
R0 := Ksyn / Kdeg
KD := Koff / Kon
# Total concentrations (mass balance)
Ltot := Ligand + Complex
Rtot := Receptor + Complex
# Target occupancy and receptor ratio
TargetOccupancy := Complex / Rtot
ReceptorRatio := Receptor / R0
end
@dynamics begin
# Free ligand dynamics
Ligand' = -Kel * Ligand - Kon * Ligand * Receptor + Koff * Complex
# Free receptor dynamics
Receptor' = Ksyn - Kdeg * Receptor - Kon * Ligand * Receptor + Koff * Complex
# Ligand-receptor complex dynamics
Complex' = Kon * Ligand * Receptor - Koff * Complex - Kint * Complex
end
@derived begin
"""
Free Ligand Concentration (nM)
"""
Free_Ligand = @. Ligand
"""
Free Receptor Concentration (nM)
"""
Free_Receptor = @. Receptor
"""
Complex Concentration (nM)
"""
Complex_Conc = @. Complex
"""
Total Ligand Concentration (nM)
"""
Total_Ligand = @. Ltot
"""
Total Receptor Concentration (nM)
"""
Total_Receptor = @. Rtot
"""
Target Occupancy (fraction)
"""
Target_Occupancy = @. TargetOccupancy
end
endPumasModel
Parameters: tvCl, tvVc, tvKsyn, tvKdeg, tvKon, tvKoff, tvKint
Random effects:
Covariates:
Dynamical system variables: Ligand, Receptor, Complex
Dynamical system type: Nonlinear ODE
Derived: Free_Ligand, Free_Receptor, Complex_Conc, Total_Ligand, Total_Receptor, Target_Occupancy
Observed: Free_Ligand, Free_Receptor, Complex_Conc, Total_Ligand, Total_Receptor, Target_Occupancy
Notice the bioav = (Ligand = 1 / tvVc,) in @dosecontrol. This converts the dose (in amount, e.g., mg) directly to concentration (e.g., mg/L or nM) by dividing by the volume of distribution. This approach simplifies the model equations since all state variables are concentrations.
5.1 Setting Up Parameters
# Biologically plausible parameters for a therapeutic mAb
# Based on typical mAb values from the literature
param_1cmt = (
tvCl = 0.006, # Clearance: ~0.15 L/day
tvVc = 3.0, # Central volume: 3 L (plasma volume)
tvKsyn = 0.04, # Synthesis: ~1 nmol/day → R0 = 5 nM
tvKdeg = 0.008, # Degradation: ~0.2 day⁻¹ (t½ ~87 hr)
tvKon = 0.33, # Association: ~8 nM⁻¹day⁻¹
tvKoff = 0.33, # Dissociation: ~8 day⁻¹ (KD = 1 nM)
tvKint = 0.002, # Internalization: ~0.04 day⁻¹
)
# Calculate derived quantities
R0 = param_1cmt.tvKsyn / param_1cmt.tvKdeg
KD = param_1cmt.tvKoff / param_1cmt.tvKon
println("Baseline receptor (R0): $(R0) nM")
println("Dissociation constant (KD): $(KD) nM")Baseline receptor (R0): 5.0 nM
Dissociation constant (KD): 1.0 nM
5.2 Simulating Across Dose Range
Show/Hide Code
# Create population with multiple doses
# Wide dose range spanning 3 orders of magnitude for clear TMDD visualization
doses = [0.5, 5.0, 50.0, 500.0] # nmol (equivalent to ~0.075, 0.75, 7.5, 75 mg for a 150 kDa mAb)
dose_labels = ["0.5 nmol", "5 nmol", "50 nmol", "500 nmol"]
Random.seed!(42)
pop_1cmt = [
Subject(
id = i,
events = DosageRegimen(dose, time = 0, cmt = 1),
covariates = (dose_level = dose,),
observations = (
Free_Ligand = nothing,
Free_Receptor = nothing,
Complex_Conc = nothing,
Total_Ligand = nothing,
Total_Receptor = nothing,
Target_Occupancy = nothing,
),
) for (i, dose) in enumerate(doses)
]
# Simulate (extended time for typical mAb kinetics)
obstimes = 0.1:2:672 # Up to 4 weeks
sim_1cmt = simobs(tmdd_full_1cmt, pop_1cmt, param_1cmt, obstimes = obstimes)Simulated population (Vector{<:Subject})
Simulated subjects: 4
Simulated variables: Free_Ligand, Free_Receptor, Complex_Conc, Total_Ligand, Total_Receptor, Target_Occupancy
6 Visualizing Model Output
Show/Hide Code
# Convert simulation results to DataFrame for plotting
df_1cmt = DataFrame(sim_1cmt)
# Create dose label mapping
dose_label_df = DataFrame(id = string.(1:4), Dose = dose_labels)
df_1cmt = innerjoin(df_1cmt, dose_label_df, on = :id)6.1 Free Ligand Concentration
Show/Hide Code
plt =
data(filter(row -> row.time <= 336, df_1cmt)) *
mapping(
:time => "Time (hr)",
:Free_Ligand => "Free Ligand Concentration (nM)";
color = :Dose,
) *
visual(Lines; linewidth = 2)
draw(
plt;
axis = (yscale = Makie.pseudolog10, title = "One-Compartment Full TMDD: Free Ligand"),
)6.2 Receptor Dynamics
Show/Hide Code
plt =
data(filter(row -> row.time <= 504, df_1cmt)) *
mapping(
:time => "Time (hr)",
:Free_Receptor => "Free Receptor Concentration (nM)";
color = :Dose,
) *
visual(Lines; linewidth = 2)
fg = draw(plt; axis = (title = "One-Compartment Full TMDD: Free Receptor",))
# Add baseline reference line using Makie
hlines!(fg.figure[1, 1], [R0], color = :gray, linestyle = :dash, label = "Baseline (R0)")
fgHigher doses lead to greater suppression of free receptor. After the drug is cleared, receptors recover to baseline through ongoing synthesis. The time course of recovery is governed by \(k_{syn}\) and \(k_{deg}\).
6.3 Complex Formation
Show/Hide Code
plt =
data(filter(row -> row.time <= 672, df_1cmt)) *
mapping(
:time => "Time (hr)",
:Complex_Conc => "Complex Concentration (nM)";
color = :Dose,
) *
visual(Lines; linewidth = 2)
draw(plt; axis = (title = "One-Compartment Full TMDD: Drug-Target Complex",))6.4 Target Occupancy
Target occupancy is a pharmacodynamically relevant metric showing the fraction of receptors bound by drug:
Show/Hide Code
plt =
data(df_1cmt) *
mapping(
:time => "Time (hr)",
:Target_Occupancy => "Target Occupancy (fraction)";
color = :Dose,
) *
visual(Lines; linewidth = 2)
draw(plt; axis = (title = "One-Compartment Full TMDD: Target Occupancy",))6.5 Complete Species Overview
Show/Hide Code
# Select middle dose for demonstration
dose_idx = 3
df_single = filter(row -> row.id == string(dose_idx), df_1cmt)
# Stack to long format for faceted plot (excluding Target Occupancy which has different y scale)
df_species = stack(
df_single,
[:Free_Ligand, :Free_Receptor, :Complex_Conc],
variable_name = :Species,
value_name = :Concentration,
)
# Create faceted plot for concentrations
plt =
data(df_species) *
mapping(
:time => "Time (hr)",
:Concentration => "Concentration (nM)";
layout = :Species,
) *
visual(Lines; linewidth = 2)
draw(plt; figure = (title = "Dose: $(dose_labels[dose_idx])",))7 Four-Phase Characterization of TMDD Profiles
Peletier & Gabrielsson (2012) provided a detailed characterization of TMDD concentration-time profiles, identifying four distinct phases that occur following drug administration. Understanding these phases is crucial for interpreting PK data and extracting mechanistic information from concentration-time curves.
For a comprehensive exploration of the four phases including parameter identification, constant-rate infusion dynamics, and comparison with the Michaelis-Menten approximation, see Part 1b: TMDD Dynamics Through the Four Phases. That tutorial uses the original Peletier parameters to reproduce key figures from the paper.
7.1 The Four Phases
On a semi-logarithmic plot, the TMDD concentration profile can be divided into four phases:
| Phase | Name | Characteristic | Dominant Process |
|---|---|---|---|
| A | Initial binding | Rapid initial decline | Drug-target association |
| B | Target saturation | Linear (parallel) decline | Linear elimination with saturated target |
| C | Transition | Non-linear decline | Target-mediated elimination returning |
| D | Terminal | Linear (parallel) decline | Slowest elimination process |
7.2 Phase A: Rapid Initial Binding
Immediately after dosing, free ligand rapidly binds to available receptor. The timescale of this phase is approximately:
\[ \tau_A \sim \frac{1}{k_{on} \cdot L_0} \]
where \(L_0\) is the initial ligand concentration. For high-affinity drugs with large \(k_{on}\), this phase may be very brief and difficult to observe.
7.3 Phase B: Target-Saturated Elimination
Once the target is saturated (\(L \gg K_D\) and \(LR \approx R_{tot}\)), the ligand elimination becomes approximately linear with rate constant:
\[ \lambda_B \approx k_{el} + \frac{k_{int} \cdot R_0}{K_M + L} \]
At high ligand concentrations, this simplifies to approximately \(k_{el}\), representing the non-target-mediated (linear) clearance pathway.
7.4 Phase C: Transition Phase
As ligand concentration decreases and approaches the target concentration, the system transitions from saturated to unsaturated kinetics. This phase exhibits non-linear behavior:
- Target begins to recover from suppression
- Target-mediated clearance increases
- Clearance becomes concentration-dependent
The concentration range for this phase is roughly \(K_D < L < R_0\).
7.5 Phase D: Terminal Phase
At low ligand concentrations (\(L \ll K_D\)), the terminal phase is reached. The terminal slope depends on the relative rates of processes:
\[ \lambda_D \approx k_{int} \text{ (internalization-limited)} \]
or
\[ \lambda_D \approx k_{el} + \frac{k_{int} \cdot R_0}{K_D} \text{ (if linear elimination is slow)} \]
A key insight from Peletier & Gabrielsson (2012) is that the terminal slope in TMDD is often determined by the internalization rate (\(k_{int}\)) rather than the linear elimination rate (\(k_{el}\)). This differs from the Michaelis-Menten approximation, which can overestimate the terminal elimination rate.
7.6 Visualizing the Four Phases
To clearly visualize all four phases as characterized by Peletier & Gabrielsson (2012), we use parameters that produce distinct phase separation. The key is having:
- Sufficiently high \(k_{on}\) for visible Phase A
- Low \(k_{el}\) relative to target-mediated clearance for clear Phase B
- Appropriate \(R_0\) and \(K_D\) values for visible Phase C transition
- Slow \(k_{int}\) for extended Phase D
Show/Hide Code
# Parameters tuned to show clear four-phase behavior
# Based on Peletier & Gabrielsson (2012) characteristic profiles
param_4phase = (
tvCl = 0.001, # Low linear clearance to emphasize TMDD
tvVc = 2.5, # Central volume
tvKsyn = 0.5, # Higher synthesis → R0 = 10 nM
tvKdeg = 0.05, # Degradation rate
tvKon = 1.0, # Fast association for visible Phase A
tvKoff = 0.1, # Slow dissociation (KD = 0.1 nM, high affinity)
tvKint = 0.02, # Internalization rate for clear terminal phase
)
R0_4phase = param_4phase.tvKsyn / param_4phase.tvKdeg
KD_4phase = param_4phase.tvKoff / param_4phase.tvKon
# Multiple doses spanning range to show dose-dependent phase behavior
doses_4phase = [10.0, 25.0, 100.0, 250.0, 1000.0]
dose_labels_4phase = ["10 nmol", "25 nmol", "100 nmol", "250 nmol", "1000 nmol"]
pop_4phase = [
Subject(
id = i,
events = DosageRegimen(dose, time = 0, cmt = 1),
observations = (Free_Ligand = nothing,),
) for (i, dose) in enumerate(doses_4phase)
]
# Extended time to capture terminal phase
obstimes_4phase = [0.01; 0.1:0.5:2; 3:1:20; 25:5:100; 110:10:500]
sim_4phase = simobs(tmdd_full_1cmt, pop_4phase, param_4phase, obstimes = obstimes_4phase)
df_4phase = DataFrame(sim_4phase)
# Add dose labels
dose_label_4phase_df = DataFrame(id = string.(1:5), Dose = dose_labels_4phase)
df_4phase = innerjoin(df_4phase, dose_label_4phase_df, on = :id)
# Create the multi-dose plot
fig = Figure(size = (900, 500))
ax = Axis(
fig[1, 1],
xlabel = "Time (hr)",
ylabel = "Free Ligand Concentration (nM)",
title = "Four-Phase TMDD Profiles Across Doses",
yscale = log10,
limits = (nothing, nothing, 0.01, 2000),
)
colors = [:red, :blue, :green, :purple, :orange]
for (i, dose_label) in enumerate(dose_labels_4phase)
df_dose = filter(row -> row.Dose == dose_label, df_4phase)
lines!(
ax,
df_dose.time,
max.(df_dose.Free_Ligand, 0.001),
linewidth = 2,
color = colors[i],
label = dose_label,
)
end
# Add legend
Legend(fig[1, 2], ax, "Dose", framevisible = false)
figThe plot above demonstrates how the four phases manifest differently across doses:
- Low doses (10-25 nmol): Phases A and B are brief; the profile quickly transitions through C to D
- Intermediate doses (100 nmol): All four phases are visible with clear transitions
- High doses (250-1000 nmol): Extended Phase B with parallel decline; delayed transition to C and D
Show/Hide Code
# Single dose annotation plot
df_single_4phase = filter(row -> row.Dose == "100 nmol", df_4phase)
fig = Figure(size = (900, 550))
ax = Axis(
fig[1, 1],
xlabel = "Time (hr)",
ylabel = "Free Ligand Concentration (nM)",
title = "Four-Phase Characterization (100 nmol dose)",
yscale = log10,
limits = (nothing, nothing, 0.01, 200),
)
# Plot concentration
lines!(
ax,
df_single_4phase.time,
max.(df_single_4phase.Free_Ligand, 0.001),
linewidth = 3,
color = :steelblue,
)
# Phase boundaries (approximate for this parameter set)
vlines!(ax, [1], color = :gray, linestyle = :dash, alpha = 0.6)
vlines!(ax, [15], color = :gray, linestyle = :dash, alpha = 0.6)
vlines!(ax, [80], color = :gray, linestyle = :dash, alpha = 0.6)
# Phase labels with descriptions
text!(ax, 0.3, 80, text = "A", fontsize = 16, font = :bold, color = :darkred)
text!(ax, 5, 25, text = "B", fontsize = 16, font = :bold, color = :darkgreen)
text!(ax, 35, 3, text = "C", fontsize = 16, font = :bold, color = :darkorange)
text!(ax, 200, 0.15, text = "D", fontsize = 16, font = :bold, color = :darkblue)
# Add phase descriptions
text!(ax, 0.15, 25, text = "Rapid\nbinding", fontsize = 10, color = :darkred)
text!(ax, 3, 8, text = "Target\nsaturated", fontsize = 10, color = :darkgreen)
text!(ax, 25, 0.8, text = "Transition", fontsize = 10, color = :darkorange)
text!(ax, 150, 0.05, text = "Terminal", fontsize = 10, color = :darkblue)
# Add reference lines for key concentrations
hlines!(ax, [R0_4phase], color = :gray, linestyle = :dot, alpha = 0.5)
text!(ax, 350, R0_4phase * 1.3, text = "R₀", fontsize = 10, color = :gray)
hlines!(ax, [KD_4phase], color = :gray, linestyle = :dot, alpha = 0.5)
text!(ax, 350, KD_4phase * 1.5, text = "KD", fontsize = 10, color = :gray)
fig7.7 Parameter Identification from Phases
Each phase provides information about specific TMDD parameters (Peletier & Gabrielsson, 2012):
| Phase | Observable | Parameters Informed |
|---|---|---|
| A | Duration of rapid decline | \(k_{on}\), initial \(L_0\) |
| B | Slope during saturation | \(k_{el}\) (linear clearance) |
| C | Curvature, transition point | \(K_D\) or \(K_{SS}\), \(R_0\) |
| D | Terminal slope | \(k_{int}\) (internalization) |
When analyzing TMDD data:
Identify the terminal phase (D) first - its slope gives information about \(k_{int}\)
Look for the transition (C) - the inflection point relates to \(K_D\) and \(R_0\)
The saturated phase (B) slope informs linear clearance \(k_{el}\)
Phase A may require early, dense sampling to observe
:::
8 Two-Compartment Full TMDD Model
For many drugs, distribution to peripheral tissues is significant. The two-compartment model adds a peripheral compartment for free ligand:
8.1 Equations
The peripheral compartment follows standard linear distribution:
\[ \frac{dL_p}{dt} = k_{12} \cdot L \cdot V_c - k_{21} \cdot L_p \]
And the central ligand equation becomes:
\[ \frac{dL}{dt} = -k_{el} \cdot L - k_{on} \cdot L \cdot R + k_{off} \cdot LR - k_{12} \cdot L + k_{21} \cdot \frac{L_p}{V_c} \]
Where:
- \(k_{12} = Q/V_c\) (distribution rate from central to peripheral)
- \(k_{21} = Q/V_p\) (distribution rate from peripheral to central)
- \(Q\) = Intercompartmental clearance
- \(V_p\) = Peripheral volume of distribution
8.2 Pumas Implementation
tmdd_full_2cmt = @model begin
@metadata begin
desc = "Two-compartment Full TMDD Model"
timeu = u"hr"
end
@param begin
"""
Clearance (L/hr)
"""
tvCl ∈ RealDomain(lower = 0)
"""
Central volume of distribution (L)
"""
tvVc ∈ RealDomain(lower = 0)
"""
Intercompartmental clearance (L/hr)
"""
tvQ ∈ RealDomain(lower = 0)
"""
Peripheral volume of distribution (L)
"""
tvVp ∈ RealDomain(lower = 0)
"""
Receptor synthesis rate (nM/hr)
"""
tvKsyn ∈ RealDomain(lower = 0)
"""
Receptor degradation rate (hr⁻¹)
"""
tvKdeg ∈ RealDomain(lower = 0)
"""
Association rate constant (nM⁻¹hr⁻¹)
"""
tvKon ∈ RealDomain(lower = 0)
"""
Dissociation rate constant (hr⁻¹)
"""
tvKoff ∈ RealDomain(lower = 0)
"""
Internalization rate constant (hr⁻¹)
"""
tvKint ∈ RealDomain(lower = 0)
end
@pre begin
Cl = tvCl
Vc = tvVc
Q = tvQ
Vp = tvVp
Ksyn = tvKsyn
Kdeg = tvKdeg
Kon = tvKon
Koff = tvKoff
Kint = tvKint
end
@dosecontrol begin
bioav = (Ligand = 1 / tvVc,)
end
@init begin
Receptor = Ksyn / Kdeg
end
@vars begin
Kel := Cl / Vc
K12 := Q / Vc
K21 := Q / Vp
R0 := Ksyn / Kdeg
KD := Koff / Kon
Ltot := Ligand + Complex
Rtot := Receptor + Complex
TargetOccupancy := Complex / Rtot
end
@dynamics begin
# Free ligand in central compartment
Ligand' =
-Kel * Ligand - Kon * Ligand * Receptor + Koff * Complex - K12 * Ligand +
K21 * (Peripheral / Vc)
# Free ligand in peripheral compartment (as amount)
Peripheral' = K12 * Ligand * Vc - K21 * Peripheral
# Free receptor dynamics (central only)
Receptor' = Ksyn - Kdeg * Receptor - Kon * Ligand * Receptor + Koff * Complex
# Complex dynamics (central only)
Complex' = Kon * Ligand * Receptor - Koff * Complex - Kint * Complex
end
@derived begin
Free_Ligand = @. Ligand
Free_Receptor = @. Receptor
Complex_Conc = @. Complex
Peripheral_Conc = @. Peripheral / Vp
Total_Ligand = @. Ltot
Target_Occupancy = @. TargetOccupancy
end
endPumasModel
Parameters: tvCl, tvVc, tvQ, tvVp, tvKsyn, tvKdeg, tvKon, tvKoff, tvKint
Random effects:
Covariates:
Dynamical system variables: Ligand, Peripheral, Receptor, Complex
Dynamical system type: Nonlinear ODE
Derived: Free_Ligand, Free_Receptor, Complex_Conc, Peripheral_Conc, Total_Ligand, Target_Occupancy
Observed: Free_Ligand, Free_Receptor, Complex_Conc, Peripheral_Conc, Total_Ligand, Target_Occupancy
8.3 Simulating the Two-Compartment Model
Show/Hide Code
# Parameters for two-compartment model
# Based on typical mAb values from the literature
param_2cmt = (
tvCl = 0.006, # Clearance: ~0.15 L/day
tvVc = 3.0, # Central volume: 3 L
tvQ = 0.02, # Intercompartmental clearance: ~0.45 L/day
tvVp = 3.0, # Peripheral volume: 3 L
tvKsyn = 0.04, # Synthesis rate: ~1 nmol/day
tvKdeg = 0.008, # Degradation rate: ~0.2 day⁻¹
tvKon = 0.33, # Association rate: ~8 nM⁻¹day⁻¹
tvKoff = 0.33, # Dissociation rate: ~8 day⁻¹
tvKint = 0.002, # Internalization rate: ~0.04 day⁻¹
)
pop_2cmt = [
Subject(
id = i,
events = DosageRegimen(dose, time = 0, cmt = 1),
observations = (
Free_Ligand = nothing,
Peripheral_Conc = nothing,
Free_Receptor = nothing,
Complex_Conc = nothing,
),
) for (i, dose) in enumerate(doses)
]
sim_2cmt = simobs(tmdd_full_2cmt, pop_2cmt, param_2cmt, obstimes = obstimes)Simulated population (Vector{<:Subject})
Simulated subjects: 4
Simulated variables: Free_Ligand, Free_Receptor, Complex_Conc, Peripheral_Conc, Total_Ligand, Target_Occupancy
8.4 Comparing One- and Two-Compartment Models
Show/Hide Code
dose_idx = 3 # 10 mg/kg
# Prepare data for model comparison
df_2cmt = DataFrame(sim_2cmt)
df_2cmt = innerjoin(df_2cmt, dose_label_df, on = :id)
# Filter to dose of interest and early time
df_1cmt_sub = filter(row -> row.id == string(dose_idx) && row.time <= 24, df_1cmt)
df_2cmt_sub = filter(row -> row.id == string(dose_idx) && row.time <= 24, df_2cmt)
# Panel 1: Model comparison
df_1cmt_sub.Model .= "1-compartment"
df_2cmt_sub.Model .= "2-compartment"
df_comp = vcat(
select(df_1cmt_sub, :time, :Free_Ligand, :Model),
select(df_2cmt_sub, :time, :Free_Ligand, :Model),
)
plt1 =
data(df_comp) *
mapping(
:time => "Time (hr)",
:Free_Ligand => "Free Ligand (nM)";
color = :Model,
linestyle = :Model,
) *
visual(Lines; linewidth = 2)
# Panel 2: Central vs Peripheral for 2-cmt
df_cmt_long = stack(
df_2cmt_sub,
[:Free_Ligand, :Peripheral_Conc],
variable_name = :Compartment,
value_name = :Concentration,
)
df_cmt_long.Compartment =
replace.(
string.(df_cmt_long.Compartment),
"Free_Ligand" => "Central",
"Peripheral_Conc" => "Peripheral",
)
plt2 =
data(df_cmt_long) *
mapping(
:time => "Time (hr)",
:Concentration => "Concentration (nM)";
color = :Compartment,
linestyle = :Compartment,
) *
visual(Lines; linewidth = 2)
fig = Figure(size = (900, 400))
draw!(
fig[1, 1],
plt1;
axis = (yscale = Makie.pseudolog10, title = "Free Ligand Comparison"),
)
draw!(
fig[1, 2],
plt2;
axis = (yscale = Makie.pseudolog10, title = "Central vs Peripheral (2-cmt)"),
)
fig9 Parameter Sensitivity Analysis
Understanding how each parameter affects model output is crucial for model development and interpretation.
Show/Hide Code
# Base parameter set (literature-based values)
base_params = (
tvCl = 0.006,
tvVc = 3.0,
tvKsyn = 0.04,
tvKdeg = 0.008,
tvKon = 0.33,
tvKoff = 0.33,
tvKint = 0.002,
)
# Single subject at fixed dose
single_pop = [
Subject(
id = 1,
events = DosageRegimen(50, time = 0, cmt = 1),
observations = (Free_Ligand = nothing,),
),
]
# Parameters to vary and their ranges
param_variations = [
(:tvKon, [0.1, 0.33, 0.5, 1.0], "kon"),
(:tvKoff, [0.1, 0.33, 1.0, 3.0], "koff"),
(:tvKint, [0.001, 0.002, 0.005, 0.01], "kint"),
(:tvKdeg, [0.004, 0.008, 0.016, 0.032], "kdeg"),
]
# Build DataFrame with all parameter variations
df_sens = DataFrame()
for (param_name, values, label) in param_variations
for val in values
modified_params = merge(base_params, NamedTuple{(param_name,)}((val,)))
sim = simobs(tmdd_full_1cmt, single_pop, modified_params, obstimes = obstimes)
sim_df = DataFrame(sim)
sim_df.Parameter .= label
sim_df.Value .= string(val)
append!(df_sens, sim_df)
end
end
plt =
data(df_sens) *
mapping(
:time => "Time (hr)",
:Free_Ligand => "Free Ligand (nM)";
color = :Value => "Value",
layout = :Parameter,
) *
visual(Lines; linewidth = 2)
draw(plt; axis = (yscale = Makie.pseudolog10,))10 Practical Implementation Considerations
10.1 Units and Scaling
When implementing TMDD models, careful attention to units is essential:
| Quantity | Common Units | Alternative |
|---|---|---|
| Concentration | nM, pM | ng/mL, μg/mL |
| Volume | L | mL |
| Clearance | L/hr | mL/min |
| Rate constants | hr⁻¹ | day⁻¹, min⁻¹ |
| \(k_{on}\) | nM⁻¹hr⁻¹ | M⁻¹s⁻¹ |
Ensure all parameters have consistent units. A common source of error is mixing concentration units (e.g., nM for ligand, ng/mL for parameters).
10.2 Handling Stiff ODEs
The full TMDD model can be numerically stiff, especially when:
- \(k_{on}\) is large (fast binding)
- There are large concentration gradients
Pumas uses adaptive ODE solvers that handle stiffness automatically. For particularly challenging systems, you can specify solver options.
10.3 Initial Condition Considerations
The receptor initial condition \(R_0 = k_{syn}/k_{deg}\) assumes steady state before dosing. If this assumption doesn’t hold (e.g., disease state alters baseline), you can:
- Estimate \(R_0\) directly as a parameter
- Model disease-related changes in \(k_{syn}\) or \(k_{deg}\)
- Use time-varying parameters
# Example: Estimating R0 directly instead of deriving from ksyn/kdeg
tmdd_with_R0 = @model begin
@param begin
tvCl ∈ RealDomain(lower = 0)
tvVc ∈ RealDomain(lower = 0)
tvR0 ∈ RealDomain(lower = 0) # Directly estimated
tvKdeg ∈ RealDomain(lower = 0)
tvKon ∈ RealDomain(lower = 0)
tvKoff ∈ RealDomain(lower = 0)
tvKint ∈ RealDomain(lower = 0)
end
@pre begin
Cl = tvCl
Vc = tvVc
R0 = tvR0
Kdeg = tvKdeg
Kon = tvKon
Koff = tvKoff
Kint = tvKint
# Derive Ksyn to maintain consistency
Ksyn = R0 * Kdeg
end
@dosecontrol begin
bioav = (Ligand = 1 / tvVc,)
end
@init begin
Receptor = R0
end
@dynamics begin
Ligand' = -(Cl / Vc) * Ligand - Kon * Ligand * Receptor + Koff * Complex
Receptor' = Ksyn - Kdeg * Receptor - Kon * Ligand * Receptor + Koff * Complex
Complex' = Kon * Ligand * Receptor - Koff * Complex - Kint * Complex
end
@derived begin
Free_Ligand = @. Ligand
end
endPumasModel
Parameters: tvCl, tvVc, tvR0, tvKdeg, tvKon, tvKoff, tvKint
Random effects:
Covariates:
Dynamical system variables: Ligand, Receptor, Complex
Dynamical system type: Nonlinear ODE
Derived: Free_Ligand
Observed: Free_Ligand
11 Verifying Model Behavior
11.1 Mass Balance Check
A useful verification is to check that mass balance holds:
Show/Hide Code
# Verify mass balance: Ltot should be conserved (minus elimination)
df_mass = filter(row -> row.id == "3", df_1cmt)
plt =
data(df_mass) *
mapping(:time => "Time (hr)", :Total_Ligand => "Total Ligand (nM)") *
visual(Lines; linewidth = 2)
draw(plt; axis = (title = "Mass Balance Verification: Total Ligand Over Time",))11.2 Steady-State Verification
Before dosing, the receptor should be at steady state:
Show/Hide Code
# Simulate without dosing to verify steady state
no_dose_pop = [Subject(id = 1, observations = (Free_Receptor = nothing,))]
sim_nodose = simobs(tmdd_full_1cmt, no_dose_pop, param_1cmt, obstimes = 0:10:200)
println("Expected R0: $(R0) nM")
println("Simulated R (should be constant at R0):")
println(" t=0: $(sim_nodose[1].observations.Free_Receptor[1]) nM")
println(" t=100: $(sim_nodose[1].observations.Free_Receptor[11]) nM")
println(" t=200: $(sim_nodose[1].observations.Free_Receptor[21]) nM")Expected R0: 5.0 nM
Simulated R (should be constant at R0):
t=0: 5.0 nM
t=100: 5.0 nM
t=200: 5.0 nM
12 Summary
In this tutorial, we covered:
- Mathematical framework: Complete ODE system for full TMDD
- One-compartment model: Basic implementation with ligand, receptor, and complex
- Two-compartment model: Extension with peripheral distribution
- Visualization: All species dynamics across dose range
- Parameter sensitivity: How each parameter affects model output
- Practical considerations: Units, stiffness, initial conditions
The full TMDD model provides the most mechanistic description but may be overparameterized for typical data. In Part 3, we explore simplified approximations (QE, QSS, MM) that maintain key TMDD features while improving identifiability.
The full TMDD model was first introduced by Mager & Jusko (2001), with subsequent refinements by Mager & Krzyzanski (2005). The four-phase characterization of TMDD profiles was described in detail by Peletier & Gabrielsson (2012), and comprehensive tutorials are available from Dua et al. (2015).