Skip to content

One-Compartment Oral First-Order

Single-compartment pharmacokinetic model with first-order oral absorption.


Function Signature

neopkpd.simulate_pk_oral_first_order(
    ka: float,
    cl: float,
    v: float,
    doses: list[dict],
    t0: float,
    t1: float,
    saveat: list[float],
    alg: str = "Tsit5",
    reltol: float = 1e-10,
    abstol: float = 1e-12,
    maxiters: int = 10**7,
    alag: float | None = None,
    bioavailability: float | None = None,
) -> dict

Parameters

Parameter Type Description
ka float Absorption rate constant (1/h)
cl float Apparent clearance CL/F (L/h)
v float Apparent volume V/F (L)
doses list[dict] Dose events with time and amount
t0 float Simulation start time
t1 float Simulation end time
saveat list[float] Time points for output
alag float Absorption lag time (optional)
bioavailability float Fraction absorbed F (optional)

Returns

{
    "t": [0.0, 1.0, 2.0, ...],
    "states": {
        "A_gut": [...],          # Amount in gut
        "A": [...]               # Amount in central
    },
    "observations": {
        "conc": [...]            # Plasma concentration
    },
    "metadata": {...}
}

Model Equations

\[\frac{dA_{gut}}{dt} = -K_a \cdot A_{gut}\]
\[\frac{dA}{dt} = K_a \cdot A_{gut} - k \cdot A\]
\[C = \frac{A}{V}\]

Analytical Solution (Bateman Function)

\[C(t) = \frac{F \cdot D \cdot K_a}{V \cdot (K_a - k)} \cdot (e^{-kt} - e^{-K_a t})\]

Basic Examples

Single Oral Dose

import neopkpd

result = neopkpd.simulate_pk_oral_first_order(
    ka=1.5,       # Absorption rate (1/h)
    cl=10.0,      # Clearance (L/h)
    v=50.0,       # Volume (L)
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0,
    t1=24.0,
    saveat=[i * 0.25 for i in range(97)]
)

# Find Cmax and Tmax
conc = result['observations']['conc']
t = result['t']
cmax_idx = max(range(len(conc)), key=lambda i: conc[i])

print(f"Cmax: {conc[cmax_idx]:.2f} mg/L")
print(f"Tmax: {t[cmax_idx]:.2f} h")

With Lag Time and Bioavailability

# Drug with 30-minute lag and 60% bioavailability
result = neopkpd.simulate_pk_oral_first_order(
    ka=1.5,
    cl=10.0,
    v=50.0,
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0,
    t1=24.0,
    saveat=[i * 0.25 for i in range(97)],
    alag=0.5,              # 30-minute lag
    bioavailability=0.6    # 60% absorbed
)

# Compare to theoretical
# Effective dose = 500 * 0.6 = 300 mg
# AUC = 300 / 10 = 30 mg*h/L

Multiple Dosing

BID (Twice Daily) Dosing

# 250 mg every 12 hours for 5 days
doses = [{"time": i * 12.0, "amount": 250.0} for i in range(10)]

result = neopkpd.simulate_pk_oral_first_order(
    ka=1.5,
    cl=10.0,
    v=50.0,
    doses=doses,
    t0=0.0,
    t1=120.0,
    saveat=[i * 0.5 for i in range(241)]
)

# Steady-state metrics in last interval (108-120 h)
conc = result['observations']['conc']
t = result['t']

ss_start = next(i for i, x in enumerate(t) if x >= 108)
ss_conc = conc[ss_start:]

print(f"Cmax,ss: {max(ss_conc):.2f} mg/L")
print(f"Cmin,ss: {min(ss_conc):.2f} mg/L")
print(f"Cavg,ss: {sum(ss_conc)/len(ss_conc):.2f} mg/L")

TID (Three Times Daily) Dosing

# 100 mg every 8 hours
doses = [{"time": i * 8.0, "amount": 100.0} for i in range(15)]

result = neopkpd.simulate_pk_oral_first_order(
    ka=2.0, cl=8.0, v=40.0,
    doses=doses,
    t0=0.0, t1=120.0,
    saveat=[i * 0.25 for i in range(481)]
)

Absorption Rate Effects

import neopkpd

# Compare different Ka values (same AUC, different profiles)
ka_values = [0.5, 1.0, 2.0, 4.0]

for ka in ka_values:
    result = neopkpd.simulate_pk_oral_first_order(
        ka=ka, cl=10.0, v=50.0,
        doses=[{"time": 0.0, "amount": 500.0}],
        t0=0.0, t1=24.0,
        saveat=[i * 0.1 for i in range(241)]
    )

    conc = result['observations']['conc']
    t = result['t']
    cmax_idx = max(range(len(conc)), key=lambda i: conc[i])

    print(f"Ka={ka}: Cmax={conc[cmax_idx]:.2f} mg/L, Tmax={t[cmax_idx]:.1f} h")

Expected Output:

Ka=0.5: Cmax=5.12 mg/L, Tmax=3.5 h
Ka=1.0: Cmax=6.84 mg/L, Tmax=2.0 h
Ka=2.0: Cmax=7.89 mg/L, Tmax=1.2 h
Ka=4.0: Cmax=8.55 mg/L, Tmax=0.7 h


Flip-Flop Kinetics

When Ka < k (elimination faster than absorption):

# Standard case: Ka > k (absorption-rate limited)
result_standard = neopkpd.simulate_pk_oral_first_order(
    ka=2.0, cl=10.0, v=50.0,  # k = 0.2/h, Ka = 2.0/h
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0, t1=48.0,
    saveat=[i * 0.25 for i in range(193)]
)

# Flip-flop case: Ka < k (elimination-rate limited)
result_flipflop = neopkpd.simulate_pk_oral_first_order(
    ka=0.1, cl=10.0, v=50.0,  # k = 0.2/h, Ka = 0.1/h
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0, t1=48.0,
    saveat=[i * 0.25 for i in range(193)]
)

# In flip-flop, the "terminal" slope reflects Ka, not k
print("Standard: terminal slope reflects k")
print("Flip-flop: terminal slope reflects Ka")

Tmax Calculation

Theoretical Tmax (when Ka ≠ k):

\[T_{max} = \frac{\ln(K_a/k)}{K_a - k}\]
import math

def calculate_tmax(ka, cl, v):
    k = cl / v
    if abs(ka - k) < 1e-10:
        return 1 / ka  # When Ka ≈ k
    return math.log(ka / k) / (ka - k)

# Example
ka, cl, v = 1.5, 10.0, 50.0
tmax_theoretical = calculate_tmax(ka, cl, v)
print(f"Theoretical Tmax: {tmax_theoretical:.2f} h")

# Verify with simulation
result = neopkpd.simulate_pk_oral_first_order(
    ka=ka, cl=cl, v=v,
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0, t1=24.0,
    saveat=[i * 0.01 for i in range(2401)]  # Fine resolution
)

conc = result['observations']['conc']
t = result['t']
tmax_simulated = t[max(range(len(conc)), key=lambda i: conc[i])]
print(f"Simulated Tmax: {tmax_simulated:.2f} h")

AUC Calculation

import numpy as np

result = neopkpd.simulate_pk_oral_first_order(
    ka=1.5, cl=10.0, v=50.0,
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0, t1=120.0,  # Long enough for complete elimination
    saveat=[i * 0.1 for i in range(1201)]
)

conc = np.array(result['observations']['conc'])
t = np.array(result['t'])

# AUC by trapezoidal rule
auc = np.trapz(conc, t)

print(f"AUC: {auc:.1f} mg*h/L")
print(f"Theoretical (Dose/CL): {500/10:.1f} mg*h/L")

Visualization

import neopkpd
from neopkpd.viz import plot_pk_profile

result = neopkpd.simulate_pk_oral_first_order(
    ka=1.5, cl=10.0, v=50.0,
    doses=[{"time": 0.0, "amount": 500.0}],
    t0=0.0, t1=24.0,
    saveat=[i * 0.25 for i in range(97)]
)

fig = plot_pk_profile(
    result['t'],
    result['observations']['conc'],
    title="One-Compartment Oral",
    xlabel="Time (h)",
    ylabel="Concentration (mg/L)"
)

Equations Summary

Quantity Formula
Absorption half-life \(t_{1/2,a} = 0.693/K_a\)
Elimination half-life \(t_{1/2} = 0.693 \cdot V/CL\)
Tmax \(\ln(K_a/k) / (K_a - k)\)
Cmax Complex (use simulation)
AUC \(F \cdot Dose / CL\)

See Also