
Tooluniverse Pharmacokinetics
- 82 installs
- 1.6k repo stars
- Updated August 4, 2026
- mims-harvard/tooluniverse
Helps with ai & agent building tasks.
About
tooluniverse-pharmacokinetics is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tooluniverse-pharmacokinetics
- AI & Agent Building
- AI-coding skill
Tooluniverse Pharmacokinetics by the numbers
- 82 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,183 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mims-harvard/tooluniverse --skill tooluniverse-pharmacokineticsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 82 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | August 4, 2026 |
| Repository | mims-harvard/tooluniverse ↗ |
What it does
Helps with ai & agent building tasks.
Files
Pharmacokinetic (PK) Analysis — Non-Compartmental Analysis
Turn a concentration-vs-time profile after a dose into the standard PK parameters, and compute bioavailability from IV + oral data. Non-compartmental analysis (NCA) is the model-independent workhorse used for most PK reporting.
When to use this
- You have measured plasma/serum (or other matrix) drug concentrations at known times after a dose.
- You need Cmax/Tmax/AUC/half-life/clearance/Vd, or absolute bioavailability F.
- Comparing exposure (AUC, Cmax) between formulations, doses, or routes.
This is measured-data PK. For predicting ADMET properties from a chemical structure, use tooluniverse-admet-prediction.
Step 1 — Prepare the concentration-time data
| Issue | What to do |
|---|---|
| Units — be consistent | One time unit (h), one concentration unit (mg/L or ng/mL), one dose unit (mg). Pass them as time_unit/conc_unit/dose_unit. CL and Vd come back in derived units (e.g. L/h, L). |
| Route matters | Set route to iv or po/oral. CL and Vd are only directly interpretable for IV data; from oral data they are apparent (CL/F, Vd/F) because absorption is incomplete. |
| Include t=0 | For IV bolus include the t=0 (back-extrapolated) point; for oral the pre-dose value is usually 0. |
| BLQ (below limit of quantification) | Leading BLQs before the first measurable → treat as 0; BLQs in the terminal tail → drop them (don't set to 0, it corrupts the terminal slope). |
| Sampling design | You need enough late points to define the terminal phase (≥3 points clearly in the log-linear decline) or the half-life and AUC0-∞ are unreliable. |
| Single vs multiple dose | NCA here assumes a single dose. For steady-state, analyze one dosing interval (AUC0-τ) and say so. |
Step 2 — Run NCA
tu run NCA_compute_parameters '{
"times":[0,0.5,1,2,4,8,12,24],
"concentrations":[0,2.5,4.8,6.1,4.2,2.1,1.0,0.2],
"dose":100, "route":"iv",
"dose_unit":"mg", "conc_unit":"mg/L", "time_unit":"h"}'Returns Cmax, Tmax, Clast, Tlast, AUC0_last, AUC0-inf, AUC_extrapolation_pct, lambda_z, t_half, r_squared_terminal_fit, clearance_CL, volume_distribution_Vd, MRT_iv, with a units block. AUC uses the FDA/EMA linear-up / log-down trapezoidal method.
For a CSV profile (with BLQ handling), scripts/nca_from_csv.py computes the same parameters locally.
Other tools:
NCA_fit_one_compartment— fit a 1-compartment model (k, V, CL) when you want a parametric model instead of NCA.NCA_calculate_bioavailability— absolute F fromauc_po,dose_po,auc_iv,dose_iv(see Step 4).
Step 3 — Interpret the parameters
| Parameter | Meaning | Notes / sanity |
|---|---|---|
| Cmax / Tmax | Peak concentration & time to peak — absorption rate/extent. | For IV bolus Cmax is at t=0; a later Tmax means absorption (oral) or distribution. |
| AUC0-t / AUC0-∞ | Total exposure (area under the curve). The key exposure metric. | AUC0-∞ extrapolates the tail using Clast/lambda_z. |
| AUC_extrapolation_pct | % of AUC0-∞ that was extrapolated beyond the last point. | >20% → AUC0-∞ (and anything derived from it) is unreliable; report AUC0-last instead and note insufficient sampling. |
| lambda_z / t_half | Terminal elimination rate constant and half-life. | Trust only if r_squared_terminal_fit ≥ ~0.95 and ≥3 terminal points were used. |
| CL (clearance) | Volume cleared per time = Dose/AUC0-∞ (IV). | From oral data this is CL/F (apparent). |
| Vd | Volume of distribution = CL/lambda_z (IV). | From oral data this is Vd/F (apparent). |
| MRT | Mean residence time. | Longer MRT = slower overall elimination. |
Step 4 — Absolute bioavailability (F)
F needs the same drug given both IV and orally (ideally same subjects, dose-normalized):
tu run NCA_calculate_bioavailability '{"auc_po":35.0,"dose_po":200,"auc_iv":43.4,"dose_iv":100}'F = (AUC_po / Dose_po) / (AUC_iv / Dose_iv). Report as a fraction or %. F near 1 = well absorbed; low F = poor absorption or high first-pass metabolism. F > 1 signals a data/dosing error (recheck units and doses).
Step 5 — Quality gotchas (state these)
- Extrapolation >20% → don't report AUC0-∞/CL/Vd as reliable; the profile wasn't followed long enough.
- Bad terminal fit (
r_squared_terminal_fit< 0.9, or <3 tail points) → half-life is unreliable. - CL/Vd from oral data are apparent (CL/F, Vd/F) — never present them as true clearance/volume without IV data.
- Units drive CL/Vd — a wrong conc unit silently scales them. Always check the returned
unitsblock. - Flip-flop kinetics (absorption slower than elimination) makes the "terminal" slope reflect absorption, not elimination — suspect it when oral t½ ≫ IV t½.
Honest limitations
- NCA is model-independent and robust but gives no mechanistic structure (no separate absorption/distribution rate constants) — use
NCA_fit_one_compartmentor population PK for that. - AUC accuracy depends entirely on sampling density around Cmax and in the terminal phase.
- Single-dose assumptions; for steady state analyze one interval (AUC0-τ) and accumulation separately.
Related skills
tooluniverse-admet-prediction— predict ADME properties from structure (no measured data).tooluniverse-dose-response— IC50/EC50 potency from concentration-response (not time-course).tooluniverse-statistical-modeling— compare PK parameters across groups.
#!/usr/bin/env python3
"""Non-compartmental PK analysis from a concentration-time CSV, for the
tooluniverse-pharmacokinetics skill.
Computes Cmax, Tmax, AUC0-last, AUC0-inf, terminal lambda_z / half-life, CL,
Vd, and the % AUC extrapolated — using the FDA/EMA linear-up / log-down
trapezoidal rule, matching NCA_compute_parameters. Use for CSV profiles or
when you need explicit BLQ handling.
CSV columns: time, concentration (one row per sample; BLQ as empty or 'BLQ').
Leading BLQs become 0; trailing BLQs in the terminal tail are dropped.
Usage:
python nca_from_csv.py --input profile.csv --dose 100 --route iv
"""
import argparse
import csv
import math
def _read(path):
times, concs = [], []
with open(path, newline="") as fh:
for row in csv.DictReader(fh):
try:
t = float(row["time"])
except (KeyError, ValueError):
continue
raw = (row.get("concentration") or "").strip()
c = None if raw == "" or raw.upper() == "BLQ" else float(raw)
times.append(t)
concs.append(c)
return times, concs
def _clean_blq(times, concs):
# Leading BLQ -> 0; drop trailing/interior None (terminal BLQ corrupts slope).
seen_measurable = False
out_t, out_c = [], []
for t, c in sorted(zip(times, concs)):
if c is None:
if not seen_measurable:
out_t.append(t)
out_c.append(0.0)
# else: drop terminal BLQ
else:
seen_measurable = True
out_t.append(t)
out_c.append(c)
return out_t, out_c
def _auc_linlog(t, c):
"""Linear-up / log-down trapezoidal AUC."""
auc = 0.0
for i in range(1, len(t)):
dt = t[i] - t[i - 1]
c0, c1 = c[i - 1], c[i]
if c1 >= c0 or c0 <= 0 or c1 <= 0:
auc += dt * (c0 + c1) / 2 # linear (up, or any zero)
else:
auc += dt * (c0 - c1) / math.log(c0 / c1) # log (down)
return auc
def _terminal(t, c, n_min=3):
"""Regress ln(C) on t over the last >=3 positive points; return lambda_z, r^2, n."""
pts = [(ti, ci) for ti, ci in zip(t, c) if ci > 0]
best = None
for k in range(n_min, len(pts) + 1):
sub = pts[-k:]
xs = [p[0] for p in sub]
ys = [math.log(p[1]) for p in sub]
n = len(sub)
mx, my = sum(xs) / n, sum(ys) / n
sxx = sum((x - mx) ** 2 for x in xs)
sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
if sxx == 0:
continue
slope = sxy / sxx
if slope >= 0:
continue
yhat = [my + slope * (x - mx) for x in xs]
ss_res = sum((y - yh) ** 2 for y, yh in zip(ys, yhat))
ss_tot = sum((y - my) ** 2 for y in ys)
r2 = 1 - ss_res / ss_tot if ss_tot else 0
if best is None or r2 > best[1]:
best = (-slope, r2, n)
return best # (lambda_z, r2, n) or None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--input", required=True)
ap.add_argument("--dose", type=float, required=True)
ap.add_argument("--route", default="iv", choices=["iv", "po", "oral"])
args = ap.parse_args()
t, c = _clean_blq(*_read(args.input))
if len(t) < 4:
raise SystemExit("Need >=4 measurable concentration-time points.")
cmax = max(c)
tmax = t[c.index(cmax)]
auc_last = _auc_linlog(t, c)
clast = next(ci for ci in reversed(c) if ci > 0)
term = _terminal(t, c)
print(f"\nNCA ({args.route.upper()}), {len(t)} points\n")
print(f"Cmax = {cmax:.4g} Tmax = {tmax:.4g}")
print(f"AUC0-last = {auc_last:.4g}")
if term:
lz, r2, n = term
thalf = math.log(2) / lz
auc_inf = auc_last + clast / lz
extrap = (auc_inf - auc_last) / auc_inf * 100
cl = args.dose / auc_inf
vd = cl / lz
apparent = "" if args.route == "iv" else "/F (apparent)"
print(f"lambda_z = {lz:.4g} (terminal r^2={r2:.4f}, n={n}) t_half = {thalf:.4g}")
print(f"AUC0-inf = {auc_inf:.4g} extrapolated = {extrap:.1f}%")
print(f"CL{apparent} = {cl:.4g} Vd{apparent} = {vd:.4g}")
if extrap > 20:
print(" ! AUC extrapolation > 20% — AUC0-inf / CL / Vd unreliable; report AUC0-last and sample longer.")
if r2 < 0.9 or n < 3:
print(" ! terminal fit weak (r^2<0.9 or <3 points) — half-life unreliable.")
if args.route != "iv":
print(" note: oral data -> CL and Vd are apparent (CL/F, Vd/F), not true clearance/volume.")
else:
print(" ! could not define a terminal elimination phase (no >=3 declining positive points).")
if __name__ == "__main__":
main()
"""Tests for the pharmacokinetics skill helper script (NCA)."""
import math
import pathlib
import sys
import pytest
sys.path.insert(0, str(pathlib.Path(__file__).parent / "scripts"))
import nca_from_csv as nca # noqa: E402
pytestmark = pytest.mark.unit
def test_auc_linear_segment():
# ascending segment uses the linear trapezoid: 0.5*(c0+c1)*dt
auc = nca._auc_linlog([0, 2], [0, 4])
assert auc == pytest.approx(0.5 * (0 + 4) * 2, rel=1e-9)
def test_auc_log_down_segment():
# declining segment uses the log trapezoid: dt*(c0-c1)/ln(c0/c1)
auc = nca._auc_linlog([0, 1], [8, 2])
assert auc == pytest.approx(1 * (8 - 2) / math.log(8 / 2), rel=1e-9)
def test_terminal_slope_recovered():
# pure log-linear decline with lambda_z = 0.2
t = [4, 8, 12, 16]
c = [math.exp(-0.2 * x) for x in t]
lz, r2, n = nca._terminal(t, c)
assert lz == pytest.approx(0.2, rel=1e-6)
assert r2 > 0.999
assert n >= 3
def test_blq_cleaning_leading_zero_and_terminal_drop():
# leading BLQ -> 0; terminal BLQ -> dropped
t, c = nca._clean_blq([0, 1, 2, 3], [None, 5.0, 2.0, None])
assert c[0] == 0.0 # leading BLQ became 0
assert 3 not in t # terminal BLQ time dropped
assert c[1:] == [5.0, 2.0]