Sigmoid Emax (Hill) Model¶
Extended Emax model with Hill coefficient (gamma) controlling the steepness of the concentration-response curve.
Function Signature¶
neopkpd.simulate_pkpd_sigmoid_emax(
cl: float,
v: float,
doses: list[dict],
e0: float,
emax: float,
ec50: float,
gamma: float,
t0: float,
t1: float,
saveat: list[float],
pk_kind: str = "OneCompIVBolus",
ka: 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 |
e0 |
float | Baseline effect |
emax |
float | Maximum effect change |
ec50 |
float | Concentration at 50% Emax |
gamma |
float | Hill coefficient (steepness) |
pk_kind |
str | PK model type |
ka |
float | Absorption rate (for oral) |
Hill Equation¶
\[E(C) = E_0 + \frac{E_{max} \cdot C^\gamma}{EC_{50}^\gamma + C^\gamma}\]
Effect of Gamma¶
| Gamma | Curve Shape |
|---|---|
| < 1 | Shallow, gradual |
| = 1 | Standard Emax (hyperbolic) |
| > 1 | Steep, sigmoidal |
| >> 3 | Near-threshold |
Basic Example¶
import neopkpd
result = neopkpd.simulate_pkpd_sigmoid_emax(
cl=5.0,
v=50.0,
doses=[{"time": 0.0, "amount": 500.0}],
e0=0.0,
emax=100.0,
ec50=2.0,
gamma=3.0, # Steep response
t0=0.0,
t1=24.0,
saveat=[i * 0.25 for i in range(97)]
)
effect = result['observations']['effect']
t = result['t']
print(f"Max effect: {max(effect):.1f}")
Comparing Gamma Values¶
import neopkpd
gamma_values = [0.5, 1.0, 2.0, 3.0, 5.0]
print("Gamma | Max Effect | Time > 50% effect")
print("-" * 45)
for gamma in gamma_values:
result = neopkpd.simulate_pkpd_sigmoid_emax(
cl=5.0, v=50.0,
doses=[{"time": 0.0, "amount": 500.0}],
e0=0.0, emax=100.0, ec50=2.0, gamma=gamma,
t0=0.0, t1=24.0,
saveat=[i * 0.1 for i in range(241)]
)
effect = result['observations']['effect']
t = result['t']
max_effect = max(effect)
# Time above 50% of Emax
above_50 = [i for i, e in enumerate(effect) if e > 50]
if above_50:
duration = (t[above_50[-1]] - t[above_50[0]])
else:
duration = 0
print(f"{gamma:5.1f} | {max_effect:10.1f} | {duration:6.1f} h")
Steepness at EC50¶
Higher gamma = narrower therapeutic window:
import neopkpd
# Calculate concentration at EC10 and EC90 for different gamma
ec50 = 2.0
print("Gamma | EC10 (mg/L) | EC90 (mg/L) | EC90/EC10")
print("-" * 50)
for gamma in [1.0, 2.0, 3.0, 5.0]:
ec10 = ec50 * (0.1 / 0.9) ** (1/gamma)
ec90 = ec50 * (0.9 / 0.1) ** (1/gamma)
print(f"{gamma:5.1f} | {ec10:11.3f} | {ec90:11.2f} | {ec90/ec10:9.1f}")
Key insight: Higher gamma compresses the concentration range between EC10 and EC90.
Clinical Example: Neuromuscular Blockade¶
import neopkpd
# Rocuronium: steep response (gamma = 3-4)
result = neopkpd.simulate_pkpd_sigmoid_emax(
cl=3.0, v=15.0,
doses=[{"time": 0.0, "amount": 50.0}],
e0=100.0, # 100% baseline twitch
emax=-100.0, # Complete block possible
ec50=1.0, # mcg/mL
gamma=3.5, # Steep response
t0=0.0, t1=2.0, # 2 hours
saveat=[i * 0.02 for i in range(101)]
)
effect = result['observations']['effect']
t = result['t']
# Time to onset (twitch < 10%)
onset_idx = next((i for i, e in enumerate(effect) if e < 10), None)
if onset_idx:
print(f"Onset time (to <10% twitch): {t[onset_idx]*60:.1f} seconds")
# Duration of action (until twitch recovers to 25%)
recovery_idx = next((i for i, e in enumerate(effect[onset_idx:]) if e > 25), None)
if recovery_idx:
duration = t[onset_idx + recovery_idx] - t[onset_idx]
print(f"Duration of action: {duration*60:.1f} minutes")
Therapeutic Index¶
import neopkpd
# Drug with narrow therapeutic index (high gamma)
ec50_efficacy = 2.0
ec50_toxicity = 8.0 # 4× separation
gamma = 3.0
# Find therapeutic window (>80% efficacy with <20% toxicity)
def effect_at_conc(c, e0, emax, ec50, gamma):
return e0 + emax * c**gamma / (ec50**gamma + c**gamma)
print("Concentration | Efficacy | Toxicity | Therapeutic?")
print("-" * 55)
for c in [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]:
eff = effect_at_conc(c, 0, 100, ec50_efficacy, gamma)
tox = effect_at_conc(c, 0, 100, ec50_toxicity, gamma)
therapeutic = "Yes" if eff > 80 and tox < 20 else "No"
print(f"{c:13.1f} | {eff:8.1f}% | {tox:8.1f}% | {therapeutic}")
Multiple Dosing with Steep Response¶
import neopkpd
# With steep gamma, small concentration changes cause large effect changes
doses = [{"time": i * 8.0, "amount": 200.0} for i in range(6)]
result = neopkpd.simulate_pkpd_sigmoid_emax(
cl=5.0, v=50.0,
doses=doses,
e0=0.0, emax=100.0, ec50=2.0, gamma=4.0,
t0=0.0, t1=48.0,
saveat=[i * 0.25 for i in range(193)]
)
effect = result['observations']['effect']
# With high gamma, effect has "all-or-none" appearance
print(f"Peak effect: {max(effect):.1f}")
print(f"Trough effect: {min(effect):.1f}")
print(f"Fluctuation: {max(effect) - min(effect):.1f}")
Visualization¶
import neopkpd
from neopkpd.viz import plot_pkpd_profile
result = neopkpd.simulate_pkpd_sigmoid_emax(
cl=5.0, v=50.0,
doses=[{"time": 0.0, "amount": 500.0}],
e0=0.0, emax=100.0, ec50=2.0, gamma=3.0,
t0=0.0, t1=24.0,
saveat=[i * 0.25 for i in range(97)]
)
fig = plot_pkpd_profile(
result['t'],
result['observations']['conc'],
result['observations']['effect'],
title="Sigmoid Emax PKPD (gamma=3)",
effect_label="Effect (%)"
)
Equations Summary¶
| Quantity | Formula |
|---|---|
| Effect | \(E_0 + \frac{E_{max} \cdot C^\gamma}{EC_{50}^\gamma + C^\gamma}\) |
| C at fraction f | \(EC_{50} \cdot (f/(1-f))^{1/\gamma}\) |
| Slope at EC50 | \(\gamma \cdot E_{max} / (4 \cdot EC_{50})\) |
| EC90/EC10 ratio | \(81^{1/\gamma}\) |
See Also¶
- Direct Emax - gamma = 1 case
- Effect Compartment - With delay
- Indirect Response - Mechanism-based