Skip to content

About

Deterministic, mechanistic, auditable model-informed precision dosing (MIPD): PK/PD engine, Bayesian TDM, Monte Carlo PTA, curated cited drug library. Zero runtime dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

precision-dose

Deterministic, mechanistic, auditable model-informed precision dosing (MIPD) for one patient at a time. Python, no runtime dependencies, no network, no LLM in the decision path.

Most dosing is still one-size-fits-all, while a patient's pathology changes drug exposure: renal impairment, hepatic impairment, age and body size, CYP450 genotype, protein-binding changes and polypharmacy. Model-informed precision dosing exists in transplant, oncology and infectious disease, but the tooling is closed and hard to audit. precision-dose is a small engine that a pharmacist can read, run and defend.

precision-dose dose --patient patient.json --drug vancomycin --target auc24 --out regimen.json

returns the regimen, the predicted concentration-time curve, the probability of target attainment (PTA), the safety margin, and a pharmacist-readable rationale — with the population pharmacokinetics citation and the content hash of the exact library entry that produced it.

What this is, and what it is not

It is a clinical decision support (CDS) calculation: a mechanistic pharmacokinetic model, individualised with the patient's covariates, evaluated against a guideline target, with its uncertainty exposed. It is not a prescribing system, not an "AI therapeutics" demo, and not a platform. One patient, one question, one answer, in seconds, reproducibly.

Three commitments make it auditable rather than merely accurate-looking:

  1. Every number is traceable. Each parameter in the drug library names a citation; each derived quantity in the output names the formula that produced it; each result carries the library entry hash.
  2. Missing data is refused, never imputed. No silent defaults, no guessed allele activity scores, no interpolation of a Child-Pugh class.
  3. The model's own algebra is exposed. Covariate effects multiply, so their log-contributions add exactly; the attribution output is a decomposition, not a heuristic ranking, and the residual of the decomposition is printed.

A real case, run by this code

A 68-year-old man, 78 kg, 172 cm, serum creatinine 1.9 mg/dL, MRSA bacteraemia (MIC 1.0 mg/L), albumin 2.8 g/dL. The question: what vancomycin regimen achieves AUC24/MIC 400-600?

$ precision-dose dose --patient demo/case_vanc.json --drug vancomycin --out regimen.json

Question: Vancomycin regimen for a 68-year-old male, 78 kg, SCr 1.90 mg/dL, MIC 1.00 mg/L, target auc24_mic (400-600 h).
Renal function: CKD-EPI 2021 eGFR 38 mL/min/1.73 m2; Cockcroft-Gault CrCl 41 mL/min (TBW), 36 (IBW), 38 (ABW). This entry scales clearance with crcl_cg_total. Discarding one of these in favour of the other is a decision with clinical consequences, which is why both are shown.
Hepatic: albumin 2.8 g/dL; Child-Pugh class A (score 6); clearance multiplier 1.00 (not applied for this drug).
Individual parameters: CL 2.03 L/h, V1 36.86 L, V2 44.20 L, Q 6.95 L/h; half-life 22.7 h (effective over the dosing interval).
Clearance: 4.40 -> 2.03 L/h (-54.0%) because renal_crcl_cg_total x0.41 [crcl_cg_total 41.1 vs 100]. Of that, 1.81 L/h is the scaled component and 0.22 L/h is the non-renal intercept of the published relation (additive, not a factor).
Volume: 37.94 -> 36.86 L (-2.9%) because age x0.97 [age 68 y vs 70 y].
Contributions multiply; do not add the percentages. Log-space residual 0.0e+00 confirms the decomposition is exact.
Regimen: loading 2000 mg over 120 min (25 mg/kg of total body weight), maintenance 500 mg every 12 h over 120 min.
Predicted steady-state exposure: AUC24 493.6 mg/L*h, Cmax 26.89, trough 16.77 mg/L (90% population interval 278.2-888.9).
Probability of target attainment: 43% (95% CI 42-44%, n=5000, seed 20260101). The spread is between-patient variability, not numerical noise.
Safety: predicted auc24 493.6 vs threshold 600; margin x1.22; P(exceed) 29.2%.
Caveats:
  - no fixed regimen attains auc24_mic in 80% of patients (best 43%): the target window is narrow relative to between-patient variability. This is the quantitative case for therapeutic drug monitoring rather than a reason to widen the target.

TOP CANDIDATES (common random numbers, same seed)
      dose  interval    PTA  median target
       500        12  42.9%        492.728
      1000        24  42.9%        492.728
      2000        48  42.9%        492.728
      1750        48  40.6%        431.137
      2250        48  40.2%        554.319
      1250        24  35.9%        615.910
      2500        48  35.9%        615.910
       250         8  33.4%        369.546

Three things in that output are the point of the tool:

  • 43% PTA is the honest answer. A fixed dose cannot put 80% of a population inside a 400-600 window when clearance varies by 36% CV. The tool says so and names the consequence: TDM, not a wider target. P(exceed) 29.2% is the other half of that sentence.
  • CrCl 41 (TBW), 36 (IBW), 38 (ABW) are all printed because the guidelines disagree about which weight to use, and the library declares which one its model was estimated against. CKD-EPI 2021 eGFR is shown alongside, because it is the right number for CKD staging and the wrong one for most drug labels.
  • 500 mg q12h, 1000 mg q24h and 2000 mg q48h attain the target equally often (42.9% each) because they deliver the same 1000 mg/day: exposure is set by the daily dose over clearance, not by the interval. The tool breaks the tie towards the interval closest to the one the guideline actually writes (12 h) and prints the whole table, so the choice can be argued with rather than merely trusted.
  • Clearance is 1.81 L/h of renal component plus 0.22 L/h of non-renal intercept. They are reported separately because they combine additively: at CrCl 41 the intercept is 11% of total clearance, and at CrCl 10 it would be 35%. Folding it into a multiplier would have made the log-space attribution a lie.

Adding measured levels: Bayesian dose optimisation

Three doses of 1000 mg q12h later, a peak of 42.0 mg/L at 26 h and a trough of 28.5 mg/L at 36 h. The population model predicted 39.6 and 26.9; the patient is clearing the drug more slowly than the covariates suggested.

$ precision-dose tdm --patient demo/case_vanc.json --drug vancomycin --levels demo/levels_vanc.json

BAYESIAN TDM
============
OFV 2.275223; levels 2; converged True
  CL   prior 2.026 -> posterior 1.456 (CV 16.1128%, information ratio 0.5703)
  V1   prior 36.856 -> posterior 33.169 (CV 0.5323%, information ratio 0.2885)
  V2   prior 44.200 -> posterior 26.804 (CV 1.4981%, information ratio 0.5084)

    time  observed  predicted     wres    err %
   26.00    42.000     39.586     0.54     -5.8
   36.00    28.500     26.945     0.34     -5.5
...
Clearance: 4.40 -> 1.46 L/h (-66.9%) because renal_crcl_cg_total x0.41 [crcl_cg_total 41.1 vs 100]; tdm_posterior x0.81 [MAP estimate from the measured levels]. Of that, 1.24 L/h is the scaled component and 0.22 L/h is the non-renal intercept of the published relation (additive, not a factor).
Regimen: loading 2000 mg over 120 min (25 mg/kg of total body weight), maintenance 250 mg every 8 h over 120 min.
Predicted steady-state exposure: AUC24 515.2 mg/L*h, Cmax 24.39, trough 19.24 mg/L (90% population interval 274.7-982.1).
Probability of target attainment: 39% (95% CI 37-40%, n=5000, seed 20260101). The spread is between-patient variability, not numerical noise.

The information ratio (1 − posterior variance / prior variance) is the number most Bayesian tools hide: 0.57 means the two levels moved the clearance estimate about half way from the population prior to the truth. It is not a confidence interval pretending that two samples are a dataset. The re-dosing PTA then integrates the posterior uncertainty as well as between-patient variability, and the attribution folds the posterior in as its own exact factor (tdm_posterior x0.81) so the breakdown still multiplies out to the clearance actually used.

Install and run

python -m venv .venv && . .venv/bin/activate
pip install -e .            # no runtime dependencies
precision-dose library list
precision-dose validate     # published-regimen anchors, exits non-zero on drift

Commands:

command what it does
precision-dose dose --patient p.json --drug vancomycin [--target auc24_mic] [--out r.json] [--trace] regimen, curve, PTA, safety margin, attribution, rationale
precision-dose tdm --patient p.json --drug vancomycin --levels l.json MAP estimate from measured levels, then re-dose
precision-dose library list|show|validate inspect and check the curated library
precision-dose validate run the validation anchors
precision-dose serve the same engine over a local HTTP API

Exit codes: 0 ok, 2 invalid input, 3 refusal, 4 target unattainable (the best candidate is still returned, never hidden).

--trace prints the calculation: every formula with the patient's numbers substituted.

HTTP API

precision-dose serve --host 127.0.0.1 --port 8765
curl -s localhost:8765/v1/dose -H 'content-type: application/json' \
  -d '{"patient": {...}, "drug": "vancomycin", "options": {"pta_n": 5000}}'

POST /v1/dose, POST /v1/tdm, GET /v1/library/{drug}, GET /healthz. Stateless, no storage, no auth. The JSON body is byte-identical to the CLI output for the same input — there is one engine, and the HTTP layer is a transport.

The model

Everything is mechanistic; the only fitted component is the population parameter set in the library entry, which comes from a published population pharmacokinetic model.

CL = CL_ref · (renal/renal_ref)^θ · (WT/70)^0.75 · CP · PGx · DDI · fu
V  = V_ref  · (WT/70)^1.0   · CP
  • Structural models: one- and two-compartment linear models plus phenytoin's Michaelis-Menten elimination. Linear systems are solved exactly with the matrix exponential, and a periodic regimen is evaluated at its fixed point with one linear solve: no step-size error, mass balance to machine precision, and no dependence on a sampling grid. The biexponential closed form is a unit test.
  • Renal function: eGFR by CKD-EPI 2021 and Cockcroft-Gault CrCl with three weight metrics. Which one scales clearance is declared by the drug entry, because guidelines differ and the choice changes the answer.
  • Hepatic impairment: Child-Pugh class scales clearance only where the entry supplies a cited per-drug multiplier. MELD is not used: it predicts mortality, not metabolic capacity. An incomplete Child-Pugh means no scaling plus a warning.
  • Pharmacogenomics (CPIC): diplotype → activity score → phenotype, from versioned CPIC tables. An allele without a consensus score is refused rather than defaulted to 1.0. Effects are applied to the fraction of clearance the enzyme accounts for, or — for phenytoin — to Vmax, which reproduces the CPIC 25%/50% dose guidance as a consequence of the mechanism rather than as a lookup.
  • Drug-drug interactions (DDI): competitive (Ki and unbound concentration), mechanism-based inactivation (kinact/kdeg) and induction (Emax) are supported and are not interchangeable. Effects flow through the fraction metabolised, so an inhibitor of a pathway the drug does not use changes nothing — and the output says so.
  • Binding: free fraction from albumin where the entry declares it matters, with the free-drug-is-active distinction. Vancomycin's TDM target is a total concentration and the engine refuses to free-correct it.
  • Dialysis: intradialytic clearance declared by the entry, with the between-session rebound emerging from the two-compartment model rather than being hard-coded. A renally cleared drug without an intradialytic clearance is refused for dialysis patients.

PTA and regimen selection

Monte Carlo over the library's between-subject variability, fixed seed, common random numbers across candidates (so the ranking is not Monte Carlo noise), Wilson 95% intervals, and a documented inverse-normal transform (Wichura AS241) so results are stable across Python builds. Candidates come from the entry's constraints — strengths, permitted intervals, maximum single and daily doses — not from a free grid, and the ones that were excluded are listed.

Determinism

Same input, same bytes. Proven by tests across repeated runs, fresh processes, different working directories, permuted input key order, and PYTHONHASHSEED=0/1/random. Output has sorted keys, quantised floats, no timestamps, no hostnames, no locale-dependent formatting.

The drug library

Six entries, each chosen to exercise a different mechanism:

drug mechanism exercised decision basis
vancomycin two-compartment, renal scaling with a non-renal intercept, AUC24/MIC target PK target
tacrolimus hepatic CYP3A4/3A5, whole-blood trough, measured azole DDI PK target
apixaban label criteria, not clearance label criteria
phenytoin Michaelis-Menten saturation, CYP2C9, salt factor PK target
lithium purely renal, mmol units, 12-h monitoring level PK target
digoxin renal clearance with a heart-failure-dependent non-renal term, P-gp interaction, 6-h sampling window PK target

apixaban is included deliberately: its dose is set by three patient criteria and an interaction class, not by a target exposure, so the tool reports criteria and refuses to invent a concentration curve. Making that distinction visible is part of the point.

Each entry carries its citations, the target definition and its source, the safety threshold, the monitoring strategy, the curator, the review date, and an honest known_gaps list that is printed in every rationale. Entries whose numbers cannot be sourced ship as status = "unresolved" and the engine refuses to dose them.

Curation is enforced by code. precision-dose library validate rejects an entry with a parameter that names a citation that does not exist, a target without a source, a non-positive variance, a fraction-metabolised sum above 1, or a leftover placeholder. Add a drug by following CURATION.md.

Validation

precision-dose validate reproduces published anchors and exits non-zero on drift:

ckd_epi_2021_reference_case        PASS       2/2    ok
cockcroft_gault_reference_case     PASS       3/3    ok
vancomycin_ashp_2020_auc_target    PASS       4/4    ok
apixaban_label_dose_reduction      PASS       5/5    ok
phenytoin_michaelis_menten         PASS       3/3    ok
lithium_12h_level                  PASS       3/3    ok
tacrolimus_cyp3a5_and_azole_ddi    PASS       4/4    ok
paediatric_refusal                 PASS       0/0    E_AGE_OUT_OF_SCOPE
digoxin_heart_failure_amiodarone   PASS       5/5    ok
9/9 cases passed

Plus 89 unit and property tests: analytic PK oracles, mass balance, monotonicity (AUC in dose, CL in renal function, CL in inhibitor strength, V in weight), Bayesian parameter recovery, determinism, no-network, and the edge cases (dialysis, severe hepatic impairment, obesity, age 19 and 95, missing height, anuria).

Limitations, stated plainly

  • Adults only. Paediatric allometry and maturation are not implemented, so patients under 18 are refused rather than dosed with adult exponents.
  • One model per drug. Population models disagree; each entry names the one it uses and lists the disagreement in known_gaps. Swapping the model is a curation decision, not a code change.
  • Some parameters are declared assumptions. Tacrolimus IIV and absorption rate are assumptions, marked as such and bounded by tests; they widen the PTA but do not move the point estimate. The entry says so in every output.
  • No FHIR server client. The input schema is deliberately flat so an HL7 FHIR Patient/Observation/MedicationRequest bundle maps onto it without a database; the adapter is the next milestone, not a shipped feature.
  • Not a medical device. It computes and explains; a pharmacist decides. The output carries that statement, and the license says the same thing in fewer words.

Repository layout

src/precision_dose/
  clin.py          patient validation, body size, eGFR/CrCl, Child-Pugh
  pgx.py           CPIC activity scores -> phenotype -> multiplier
  ddi.py           competitive / mechanism-based / induction algebra
  pk.py            exact linear compartment engine, Michaelis-Menten, dialysis timeline
  engine.py        individualisation, PTA, selection, output assembly
  bayes.py         MAP estimation, posterior covariance, information ratio
  attribution.py   exact log-space covariate decomposition
  report.py        deterministic rationale and calculation trace
  library/         curated TOML entries + interaction table (schema-validated)
  cli.py, serve.py, validation.py
validation/cases/  published anchors as JSON
tests/             unit and property tests, standard library only
demo/              the cases used above

Keywords

model-informed precision dosing (MIPD) · Bayesian dose optimization · therapeutic drug monitoring (TDM) · pharmacometrics · population pharmacokinetics · clinical decision support (CDS) · HL7 FHIR · CYP450 · eGFR · Child-Pugh · pharmacogenomics (CPIC) · drug-drug interactions (DDI) · probability of target attainment (PTA) · vancomycin · tacrolimus · phenytoin · lithium · renal impairment · dose optimisation · clinical pharmacology · precision medicine

License

MIT. See LICENSE for the licence and DISCLAIMER.md for the clinical scope: this is a computational aid for qualified clinicians, not a medical device, and it ships with no warranty of fitness for clinical use.

About

Deterministic, mechanistic, auditable model-informed precision dosing (MIPD): PK/PD engine, Bayesian TDM, Monte Carlo PTA, curated cited drug library. Zero runtime dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages