Transit Compartment PD Model¶
PD model with a chain of transit compartments to model delayed drug effects and signal transduction cascades.
Function Signature¶
neopkpd.simulate_pkpd_transit_compartment(
cl: float,
v: float,
doses: list[dict],
n_transit: int,
ktr: float,
e0: float,
emax: float,
ec50: float,
gamma: float,
t0: float,
t1: float,
saveat: list[float],
pk_kind: str = "OneCompIVBolus",
ka: float | None = None,
q: float | None = None,
v2: float | None = None,
alg: str = "Tsit5",
reltol: float = 1e-10,
abstol: float = 1e-12,
maxiters: int = 10**7,
) -> dict
Parameters¶
| Parameter | Type | Description |
|---|---|---|
cl |
float | Clearance (L/h) |
v |
float | Volume of distribution (L) |
doses |
list[dict] | Dose events |
n_transit |
int | Number of transit compartments (1-20 typical) |
ktr |
float | Transit rate constant (1/h) |
e0 |
float | Baseline effect/signal |
emax |
float | Maximum effect above baseline |
ec50 |
float | Concentration at 50% of Emax |
gamma |
float | Hill coefficient (steepness) |
Key Derived Parameter¶
Mean Transit Time (MTT): $\(MTT = \frac{N + 1}{K_{tr}}\)$
Model Equations¶
Drug-induced signal: $\(Signal(C) = E_0 + \frac{E_{max} \cdot C^\gamma}{EC_{50}^\gamma + C^\gamma}\)$
Transit compartment chain: $\(\frac{dA_1}{dt} = K_{tr} \cdot (Signal(C) - A_1)\)$
\[\frac{dA_i}{dt} = K_{tr} \cdot (A_{i-1} - A_i) \quad \text{for } i = 2..N\]
Final effect: $\(Effect = A_N\)$
Returns¶
{
"t": [0.0, 1.0, ...],
"states": {
"A_central": [...], # Drug amount
"transit_1": [...], # Transit compartments
...
},
"observations": {
"conc": [...], # Drug concentration
"effect": [...] # Final effect
},
"metadata": {...}
}
Basic Example¶
import neopkpd
result = neopkpd.simulate_pkpd_transit_compartment(
cl=5.0,
v=50.0,
doses=[{"time": 0.0, "amount": 500.0}],
n_transit=5, # 5 transit compartments
ktr=0.5, # Transit rate
e0=100.0, # Baseline effect
emax=-50.0, # Maximum decrease
ec50=5.0,
gamma=1.0,
t0=0.0,
t1=168.0, # 1 week
saveat=[i * 1.0 for i in range(169)]
)
effect = result['observations']['effect']
conc = result['observations']['conc']
t = result['t']
# MTT = (5+1) / 0.5 = 12 hours
print(f"Mean Transit Time: {(5+1)/0.5:.1f} h")
print(f"Max effect: {min(effect):.1f}") # Minimum because emax is negative
print(f"Time to max effect: {t[effect.index(min(effect))]} h")
Effect of Number of Transit Compartments¶
import neopkpd
n_transit_values = [1, 3, 5, 10, 20]
ktr = 0.5 # Fixed ktr
print("N transit | MTT (h) | T_nadir (h) | Nadir effect")
print("-" * 55)
for n in n_transit_values:
result = neopkpd.simulate_pkpd_transit_compartment(
cl=5.0, v=50.0,
doses=[{"time": 0.0, "amount": 500.0}],
n_transit=n, ktr=ktr, e0=100.0, emax=-60.0, ec50=5.0, gamma=1.0,
t0=0.0, t1=168.0,
saveat=[i * 0.5 for i in range(337)]
)
effect = result['observations']['effect']
t = result['t']
mtt = (n + 1) / ktr
nadir = min(effect)
t_nadir = t[effect.index(nadir)]
print(f"{n:9d} | {mtt:7.1f} | {t_nadir:11.1f} | {nadir:12.1f}")
Expected: More compartments = sharper nadir, closer to MTT.
Clinical Example: Myelosuppression¶
import neopkpd
# Chemotherapy-induced neutropenia
# Typical: 5-7 transit compartments, MTT ~5 days for neutrophils
mtt_days = 5.0
n_transit = 5
ktr = (n_transit + 1) / (mtt_days * 24) # Convert to 1/h
result = neopkpd.simulate_pkpd_transit_compartment(
cl=3.0, v=30.0,
doses=[{"time": 0.0, "amount": 100.0}], # Single chemo dose
n_transit=n_transit,
ktr=ktr,
e0=5.0, # Baseline ANC (10^9/L)
emax=-4.5, # Max 90% reduction
ec50=1.0,
gamma=1.5,
t0=0.0, t1=504.0, # 3 weeks
saveat=[i * 2.0 for i in range(253)]
)
effect = result['observations']['effect']
t = result['t']
# Grade neutropenia thresholds
print("Neutropenia Profile:")
print(f" Baseline ANC: 5.0 x10^9/L")
nadir = min(effect)
t_nadir = t[effect.index(nadir)]
print(f" Nadir ANC: {nadir:.2f} x10^9/L at day {t_nadir/24:.1f}")
# Time to recovery
for i, e in enumerate(effect):
if i > effect.index(nadir) and e > 1.5:
print(f" Recovery to >1.5 x10^9/L: day {t[i]/24:.1f}")
break
# Grade classification
if nadir < 0.5:
print(" Grade 4 (severe)")
elif nadir < 1.0:
print(" Grade 3 (moderate)")
elif nadir < 1.5:
print(" Grade 2 (mild)")
Multiple Chemotherapy Cycles¶
import neopkpd
# Every 3 weeks (21 days)
cycle_length = 21 * 24 # hours
n_cycles = 4
doses = [{"time": i * cycle_length, "amount": 100.0} for i in range(n_cycles)]
mtt_days = 5.0
n_transit = 5
ktr = (n_transit + 1) / (mtt_days * 24)
result = neopkpd.simulate_pkpd_transit_compartment(
cl=3.0, v=30.0,
doses=doses,
n_transit=n_transit, ktr=ktr,
e0=5.0, emax=-4.0, ec50=1.0, gamma=1.5,
t0=0.0, t1=n_cycles * cycle_length + 168,
saveat=[i * 4.0 for i in range(500)]
)
effect = result['observations']['effect']
t = result['t']
print("Cycle-by-Cycle Nadir:")
for cycle in range(n_cycles):
start_h = cycle * cycle_length
end_h = (cycle + 1) * cycle_length
start_idx = int(start_h / 4)
end_idx = int(end_h / 4)
cycle_effect = effect[start_idx:end_idx]
nadir = min(cycle_effect)
nadir_t = t[start_idx + cycle_effect.index(nadir)]
print(f" Cycle {cycle+1}: Nadir = {nadir:.2f} at day {nadir_t/24:.0f}")
Thrombocytopenia Model¶
import neopkpd
# Platelet dynamics: longer MTT than neutrophils (~8-10 days)
mtt_days = 8.0
n_transit = 5
ktr = (n_transit + 1) / (mtt_days * 24)
result = neopkpd.simulate_pkpd_transit_compartment(
cl=3.0, v=30.0,
doses=[{"time": 0.0, "amount": 100.0}],
n_transit=n_transit, ktr=ktr,
e0=250.0, # Baseline platelets (10^9/L)
emax=-200.0, # Max 80% reduction
ec50=1.0,
gamma=2.0,
t0=0.0, t1=672.0, # 4 weeks
saveat=[i * 2.0 for i in range(337)]
)
effect = result['observations']['effect']
t = result['t']
nadir = min(effect)
t_nadir = t[effect.index(nadir)]
print("Thrombocytopenia Profile:")
print(f" Baseline platelets: 250 x10^9/L")
print(f" Nadir: {nadir:.0f} x10^9/L at day {t_nadir/24:.1f}")
# Recovery
for i, e in enumerate(effect):
if i > effect.index(nadir) and e > 100:
print(f" Recovery to >100 x10^9/L: day {t[i]/24:.1f}")
break
Varying MTT with Fixed N¶
import neopkpd
mtt_values = [12, 24, 48, 72, 120] # hours
n_transit = 5
print("MTT (h) | Ktr (1/h) | T_nadir (h)")
print("-" * 40)
for mtt in mtt_values:
ktr = (n_transit + 1) / mtt
result = neopkpd.simulate_pkpd_transit_compartment(
cl=5.0, v=50.0,
doses=[{"time": 0.0, "amount": 500.0}],
n_transit=n_transit, ktr=ktr,
e0=100.0, emax=-50.0, ec50=5.0, gamma=1.0,
t0=0.0, t1=336.0,
saveat=[i * 1.0 for i in range(337)]
)
effect = result['observations']['effect']
t = result['t']
t_nadir = t[effect.index(min(effect))]
print(f"{mtt:7d} | {ktr:9.4f} | {t_nadir:11.1f}")
Stimulation Model (G-CSF Effect)¶
import neopkpd
# G-CSF stimulates neutrophil production
# Positive Emax = stimulation
result = neopkpd.simulate_pkpd_transit_compartment(
cl=0.5, v=5.0, # Typical G-CSF PK
doses=[{"time": i * 24, "amount": 5.0} for i in range(7)], # Daily dosing
n_transit=5,
ktr=0.05, # Slow effect
e0=3.0, # Low baseline (neutropenic patient)
emax=10.0, # Stimulation
ec50=0.1,
gamma=1.0,
t0=0.0, t1=336.0,
saveat=[i * 2.0 for i in range(169)]
)
effect = result['observations']['effect']
t = result['t']
print("G-CSF Effect on ANC:")
print(f" Baseline ANC: 3.0 x10^9/L")
print(f" Peak ANC: {max(effect):.1f} x10^9/L")
print(f" Day 7 ANC: {effect[84]:.1f} x10^9/L")
Equations Summary¶
| Quantity | Formula |
|---|---|
| MTT | \((N+1) / K_{tr}\) |
| Signal | \(E_0 + E_{max} \cdot C^\gamma / (EC_{50}^\gamma + C^\gamma)\) |
| Transit rate | \(K_{tr} \cdot (A_{i-1} - A_i)\) |
| CV of delay | \(1 / \sqrt{N+1}\) |
| Peak effect lag | \(\approx MTT\) from peak concentration |
See Also¶
- Indirect Response - Simpler delay mechanism
- Effect Compartment - Single delay compartment
- Disease Progression - Tumor growth models