Skip to content

About

NephroTDM: model-informed therapeutic drug monitoring for adult kidney transplant recipients (mycophenolic acid, tacrolimus, everolimus). Single-file, offline web app. Research use only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

14 Commits

Folders and files

Repository files navigation

NephroTDM: model-informed TDM for kidney transplantation and nephrology

License: MIT Live app

Try it: https://robterheine.github.io/nephrotdm/, or download nephrotdm.html and open it in a browser (it works from file://, offline). Research use only. Not a medical device. The app never suggests, recommends or optimizes a dose or interval.

Read this file before touching anything. It is the cold-start handover for this app. After reading it, an AI agent or a person should know what the app is, what it must never do, how it is built and how to change it safely.

What it is now (v1.5.2). Five population PK models for kidney transplantation, one card each: mycophenolic acid for adults (de Winter 2008) and for children (Heida 2024), tacrolimus for adults (Størset 2014) and for children (Schijvens 2020, refitted in Heida 2026), and everolimus for adults (Zwart 2021, Model 3). For one patient, each estimates the steady-state AUC₀–₁₂ₕ and a trough with 5–95% intervals, from the covariates and the samples entered. Tacrolimus and everolimus values are also shown corrected to a reference haematocrit. The window the probabilities refer to is the clinician's input.

How well it is checked. The closed-form engines match NONMEM 7.6 (structure to 5e-9 and POSTHOC for every drug, and a Bayesian comparison for adult MPA and the pediatric models). Every model has a calibration study. The pediatric models were also reviewed by three independent seats (pharmacometrician, UI designer, clinical pharmacologist), each with the verdict "pass with comments". No named human has signed docs/VERIFICATION_TEAM.md yet. Open points are in §11.

Form factor. A single-file, dependency-free, offline HTML app. The deliverable is nephrotdm.html (about 625 KB with the embedded fonts), generated by build.mjs from index.html and src/*. It was mpa-tdm.html up to v1.1.1; that old file was deleted in 1.5.1. It must keep working when opened from file://: no server, no CDNs, no npm packages in the artifact, and no network needed (the one exception is the page-view counter on the published site, rule 2).

Where the detail is. Release notes per version (docs/RELEASE_NOTES_V*.md); one hand-off per added model (docs/HANDOFF_EVEROLIMUS_M3.md, docs/HANDOFF_PEDIATRIC.md); the evidence next to them (docs/CALIBRATION_RESULTS*.md, docs/NONMEM_CROSSCHECK*.md, docs/VERIFICATION_*.md, docs/REVIEW_PEDIATRIC_*.md); the source readings (docs/SOURCE_ANALYSIS_*.md, docs/MODEL_ANALYSIS_*.md).

History in short.

  • 1.5.2 adds an anonymous visit counter on the published site only (GoatCounter, an image request, rule 2 and docs/RELEASE_NOTES_V152.md). Nothing else changes.
  • 1.5.1 answers the adversarial review of 13 October 2026 (docs/ADVERSARIAL_REVIEW_V150.md, every finding and what was done in docs/ADVERSARIAL_FIXES_V151.md): a mistimed dose can no longer turn into a silent 6-fold AUC, the report is tied to the run it describes, the haematocrit is read from the latest sample, a convergence check that passed on NaN was fixed, session import is stricter, Ctrl/Cmd+P prints the report, and the report fits one page on Letter and A4. Details in docs/RELEASE_NOTES_V151.md.
  • 1.5.0 adds the two pediatric models (§14). The adult drugs are unchanged: bit-identity records V16, V16b and the everolimus record, a byte-level snapshot of the MPA and tacrolimus UI text, and a browser comparison with 1.4.0.
  • 1.4.0 adds everolimus (§13), the one-page A4 report, the visual refresh with embedded IBM Plex fonts, and the field-fit audit.
  • 1.3.0 adds adult tacrolimus (§12) and renames the app NephroTDM. (1.2.0 and 1.2.1 were never published.)
  • 1.1.x is the de Winter 2008 MPA model going live, together with the result of the methods audit: closed-form propagation and exact steady state (about 0.5 s per fit), a converged sampler with R̂/ESS diagnostics, the EC-MPS membership-sampler fix, and the removal of the assay, LLOQ, error-multiplier and IV inputs (censored samples are not supported). Evidence: docs/METHODS_AUDIT_V101.md, docs/CALIBRATION_RESULTS.md, docs/NONMEM_CROSSCHECK.md, tests/golden.json, tests/nonmem_golden.json.

Published. The app is at https://robterheine.github.io/nephrotdm/ (repository Robterheine/nephrotdm, main, only index.html, which is the built nephrotdm.html; Pages from the root). The source is in Robterheine/nephrotdm-source (main), the same layout as complementtdm and complementtdm-source. The earlier MPA-only page (mpatdm) was retired. To republish: edit this folder, run npm test and npm run build, copy the project into a clone of nephrotdm-source (leaving out .git, node_modules and docs/AUTHOR_QUERY*), copy that clone's freshly built nephrotdm.html to index.html in a clone of nephrotdm, then commit and push both. Never push without the owner's go.

1. What this app is, and is not

Is: a clinician-facing TDM tool for kidney transplantation. For one specific patient it estimates the steady-state AUC₀–12h with posterior uncertainty, the AUC over a 12-hour dosing interval at steady state (a regimen with another interval is reported as its 12-hour equivalent, AUC₁₂ = AUC₀₋ₓ × 12/x). The input is an established population PK model plus limited sampling, for example a 3-point schedule. The app shows the probability that the exposure lies inside a window with both a lower and an upper bound, and lets the clinician explore what a different maintenance dose would do, at the fitted interval or a chosen alternative (12 or 24 h), using that same fitted posterior. The windows are the clinician's input; the app takes no position on them.

Every drug also reports a trough. For MPA it is shown for information only, because there is no trough target (the IATDMCT consensus says trough correlates poorly with AUC and advises against C0-guided adjustment). Tacrolimus and everolimus have trough windows.

This README grew with the app. §1 and §6 to §8 describe the engine and the adult MPA model, which came first; §12 to §14 say what is different for each later drug.

Is not, ever: a dose advisor. No dose optimization, no titration logic, no label or SmPC regimens, no wording that implies the app recommends anything. The user enters the regimen; the app shows consequences. This rule is absolute and is tested.

Intended user: clinical pharmacologist, TDM pharmacist or physician. Tone: clinical, plain-language, cautious, cited.

2. Quick start

cd mpa-tdm
npm test                      # == run_all.js (111 MPA/engine tests) && test_tacrolimus.js (60) && test_everolimus.js (39) && test_pediatric.js (64) && test_report.js (9) && test_refresh.js (17) && test_audit150.js (22) && test_visits.js (6); all must stay green
npm run build                 # == node build.mjs  → nephrotdm.html (the deliverable)
node tools/verify_model.mjs             # model-ingestion gate: must pass (MPA, tacrolimus, everolimus and the two pediatric blocks)
  • Runtime: any modern desktop browser; open nephrotdm.html directly. Nothing to install.
  • Development: Node ≥ 16. No node_modules, no dependencies of any kind.
  • Dev loop: edit index.html / src/* → npm test → npm run build. Never test changes only in the built file.

3. The golden rules

  1. Never edit nephrotdm.html by hand. It is generated. Edit index.html and src/*, then npm run build.
  2. Dependency-free and offline, forever. No CDNs, no remote fonts (the IBM Plex fonts are embedded as data URIs), no fetch of external resources, no npm packages. The artifact works from file://. One exception, since 1.5.2: a single page-view image request to GoatCounter (src/visits.js), made only when the page runs on https://robterheine.github.io, carrying the page and the referring site and nothing entered by the user, honouring Do Not Track. A downloaded copy, localhost and every other host send nothing and show no error. It is an image, not a script, so no third-party code runs in a page where patient data are typed. tests/test_visits.js holds this. Any further network use needs the owner's decision.
  3. Time is in HOURS everywhere in this app: engine, ODE rates, grids, doses, observations, charts, form fields, session JSON, report. Units follow the model: mg, mg/L and mg·h/L for MPA (the pediatric model takes mg of MMF and applies no conversion), µg, µg/L and µg·h/L inside the tacrolimus and everolimus engines (doses are typed in mg and converted once, in custom.doseToEngine). Do not port code from the complement-TDM app in the parent folder without converting units: that app uses DAYS in its engine. Day-to-hour constants are allowed in three places only: the recency time constant (365 days, bayes.js), calendar-day indexing of occasions (tacrolimus.js, mpaped.js), and the EC-MPS 24 h tiling check (model.js). Nothing else in src/ may contain / 24 or * 24; tests/test_audit150.js (H5) holds the list, so a new use has to be added there on purpose.
  4. The version lives in exactly one place: src/version.js. package.json must match; build.mjs warns and the test suite fails if they drift. Bump both for every user-visible change.
  5. npm test after every change, green before done. Any new assertion must be shown to fail first (see §9).
  6. No dosing strategies, no preprogrammed regimens. The app never suggests, recommends, or optimizes a dose or interval.
  7. Every clinical number is traceable to a cited source and reproducible from the session JSON. 7b. A green check is not evidence until it can go red. Show new tests failing against pre-fix code before trusting them against the fix. 7c. Never put a step grid on a clinical number field. Use step="any" + novalidate and validate in JS.
  8. Escape all user-entered text before putting it into HTML (report, tables, tooltips). Never interpolate raw user input.
  9. Keep the main screen light. Long explanations belong in About, the drug's background dialog ("Mycophenolic acid background", "Tacrolimus background", ...), tooltips or collapsed folds, not on the main cards.
  10. Research use only. The disclaimer stays visible on screen and inside the printed report.

4. Coding skills (Karpathy discipline, adapted)

These are the behavioral rules for agents working here. They are adapted from andrej-karpathy-skills (MIT) and merged with this repo's conventions. CLAUDE.md in the parent repository carries the same text (it is not part of the published repository). Where they overlap with the golden rules above (e.g. §4.4 and rule 7b), the golden rules are the authoritative, more specific version.

4.1 Think before coding

Don't assume. Don't hide confusion. Surface tradeoffs.

  • State assumptions explicitly before implementing. If uncertain, ask.
  • If multiple interpretations exist, present them. Don't pick silently.
  • If a simpler approach exists, say so. Push back when warranted.
  • If something is unclear, stop, name what's confusing, ask.

4.2 Simplicity first

Minimum code that solves the problem. Nothing speculative.

  • No features beyond what was asked.
  • No abstractions for single-use code.
  • No "flexibility" or "configurability" that wasn't requested.
  • No error handling for impossible scenarios.
  • If it could be a third as long, rewrite it that short.

4.3 Surgical changes

Touch only what you must. Clean up only your own mess.

  • Don't "improve" adjacent code, comments, or formatting.
  • Don't refactor things that aren't broken.
  • Match existing style (here: ES5-ish var, IIFE modules, one ECU namespace, see §5), even where you'd do it differently.
  • If you notice unrelated dead code, mention it, but don't delete it unless asked.
  • Remove imports/variables/functions that YOUR change made unused.

The test: every changed line should trace directly to the request.

4.4 Goal-driven execution

For multi-step work, state a brief plan first, then execute step → verify → step:

1. [Step] -> verify: [check]
2. [Step] -> verify: [check]

Every new test assertion must be run against pre-fix code and shown to fail before it is trusted against the fix (this repo does this literally: sabotage the fix, re-run, confirm red, restore, re-run, confirm green).

Tradeoff note: this discipline biases toward caution over speed. For a truly trivial change (a typo, a copy fix), use judgment.

5. Repository map

nephrotdm.html            GENERATED single-file app, the deliverable. Never edit.
LICENSE, LICENSE-fonts.txt  The project licence (MIT) and the SIL Open Font License text for the embedded IBM Plex fonts (the same text sits in a comment in the built page).
index.html                Development entry page (loads src/app.css + src/*.js).
build.mjs                 Inlines app.css + the src modules into nephrotdm.html;
                          warns if package.json and src/version.js drift.
package.json              Two scripts only: test, build. No dependencies.

src/
  version.js              ECU.VERSION, the single source of truth for the version.
  tacrolimus.js           ECU.model.drugs.tac: the tacrolimus spec and ALL its model code (indivParams,
                          closed-form simulate with per-dose F and ka, observation transform, assay
                          conversion, occasion bookkeeping, exposure). Reached from model.js/bayes.js
                          only through `spec.custom` hooks, so the MPA code path is untouched.
  texts_tac.js            Tacrolimus background / getting-started / About copy (no window numbers). Registers ECU.drugTexts.tac.
  everolimus.js           ECU.model.drugs.evr: the everolimus spec and ALL its model code (closed-form Erlang-5 + 3-compartment solution,
                          steady state, blood transform, exposure). Also reached only through `spec.custom` hooks. `spec.ui` carries its
                          presentation flags (and tacrolimus's): noun, weight, predDose, occasions, badge, chartTitle, shrinkEta, modelLine.
  texts_evr.js            Everolimus texts and result/report/explorer sentences; registers ECU.drugTexts.evr (ui.js reads the registry).
  tacped.js               ECU.model.drugs.tacped: pediatric tacrolimus: the liver engine with Erlang-3 absorption, weight scaling, per-dose formulation (KA, F), exact steady-state sum
                          (ssh, with a fallback near a pole), Simpson exposure grid, scope warnings. Reached only through `spec.custom`.
  mpaped.js               ECU.model.drugs.mpaped: pediatric MPA: two poles, Erlang-2 absorption, albumin and weight scaling, one occasion eta on F per sampled calendar day,
                          AUC = dose/CL exactly. `spec.ui.bloodCorrection: false` makes ui.js and report.js drop the haematocrit, corrected rows and trough window.
  texts_tacped.js         Pediatric tacrolimus texts; registers ECU.drugTexts.tacped.
  texts_mpaped.js         Pediatric MPA texts; registers ECU.drugTexts.mpaped.
  fonts.css               IBM Plex (Sans variable, Mono 500/600), Latin subset, embedded as data URIs (SIL OFL); the build inlines it before app.css.
  report.js               The one-page A4 report for every drug: ECU.report.build(ctx), pure (ui.js builds ctx from the run snapshot). Per-drug facts are in `spec.report` (lead tile, scope, window source, reading notes, short citation). Greyscale-safe, nothing under 12 px.
  author_photo.js         The author photo (inline WebP, ~39 KB base64) shown at the bottom of About; the same image as in complementtdm.
  parallel.js             Runs the independent MCMC chains of a fit on a pool of Web Workers built from the page's own inlined
                          scripts; falls back, with identical draws, to the in-process run. Used by every drug that declares `perChainSeeds`
                          (all but adult MPA; bayes.js `chainSeed`, `runChainTask`); adult MPA keeps its single-stream sampler. The worker
                          source list (`SOURCES`) must name every drug file. See docs/PERFORMANCE_AUDIT_V121.md.
  model.js                ECU.model: drug registry and the picker order (`DRUG_ORDER`), the adult MPA spec, ODE solver,
                          event loop, simulate(), aucFromConc(), intervalGrid(),
                          covariateFields(), installStubSpec()/restorePendingSpec().
  bayes.js                ECU.bayes: the engine: makeOfv() (prior + residual),
                          mapEstimate() (multi-restart Nelder–Mead, adult MPA) and mapBFGS() (drugs with
                          `spec.custom`), laplaceCov(),
                          runMCMC() (RW Metropolis, seeded RNG), runFit() (the main
                          entry: fit + AUC + trough + window probabilities + draws),
                          doseScan() (steady-state dose explorer), probAbove/Below/
                          Between().
  chart.js                ECU.chart: a dependency-free SVG concentration–time chart
                          (linear/log-y, band, observed points, no libraries).
  diagnostics.js          ECU.diagnostics: the fit-diagnostics panel for the modal that
                          opens via the “Fit diagnostics” button (enabled after a
                          forecast): observed-vs-predicted chart, sample-by-sample
                          residuals, posterior parameter ranges, shrinkage bars,
                          MCMC quality (residualRows/gofChart/summaryHint/panelHtml).
  ui.js                   ECU.ui: the app shell: pending gating, model-driven covariate
                          rows, dosing/sampling entry, run flow, results grid, dose
                          explorer, report printing, session export/import, autosave,
                          modals (About / Getting started / the drug's background),
                          help popovers. Exposes state, buildRunInput(), validateRun(),
                          effectiveDoses(), windowBounds(), aboutHtml()… for tests.
  app.css                 All styles, including the print stylesheet (§7).

tests/
  harness.js              Minimal xUnit: t(), assert(), eq(), near(), throws(),
                          rejects(), truthy(), falsy(), runAll() (async). Exit code
                          is wired: a failing suite fails the process.
  test_visits.js          The visit counter (6 tests): only on the published site, only the page and the referring site, nothing offline, Do Not Track, never throws.
  test_audit150.js        The findings of the 13 October 2026 adversarial review (22 tests): interval sanity, latest-sample haematocrit, the NaN convergence gate, session import and round trip,
                          the report snapshot, the explorer guard, Ctrl+P, the harness's own timeout and rejection handling, and the day/hour constant guard (rule 3).
  test_refresh.js         The visual refresh (17 tests): fonts, tap size, header and Session menu, one primary action, five drug cards, result tiles, workspace, stepper, phone, wiring of the pediatric files.
  test_report.js          The one-page report (9 tests): order, citations, per-drug tiles, escaping, safety notes, greyscale and type size, print setup.
  test_pediatric.js       The pediatric suite (64 tests): independent matrix-exponential oracle (`tools/nonmem_verify/ped/ped_oracle.mjs`), the section 4.4 reference numbers, NONMEM structure and
                          POSTHOC fixtures, KA near a pole, occasions, scope, texts, the screen and report behaviour, regression records.
  mpaped_regression.json, tacped_regression.json, nonmem_ped_*   its fixtures (re-record only for an intended change; cases in `tools/ped_regression_cases.mjs`).
  test_everolimus.js      The everolimus suite (39 tests): matrix-exponential and RK4 oracles, the §3.5 reference table, Eq. 3, NONMEM fixtures
                          (structure and POSTHOC), worker pool, wiring, copy rules, the UI-text snapshot of MPA and tacrolimus, regression record.
  evr_regression.json, ui_text_snapshot.json, nonmem_evr_struct.*, nonmem_evr_posthoc.json   its fixtures (re-record only for an intended change).
  test_tacrolimus.js      The tacrolimus suite (60 tests, incl. an independent RK4 oracle, the Størset
                          anchors, the sabotage-verified invariants and V16, the MPA regression against
                          mpa_v111_regression.json).
  run_all.js              The MPA/engine suite (111 tests): pending gate, engine P0/P1
                          guards under the stub spec, clinical metadata, UI copy,
                          version sync. Edit here to add tests.
  golden.json             Golden reference values for the de Winter 2008 implementation
                          (analytic mass balance and published anchors).

tools/
  nonmem_verify/          NONMEM cross-check (structural model + POSTHOC vs the app's MAP). `run.sh`
                          runs it end to end with your NONMEM; results in docs/NONMEM_CROSSCHECK.md;
                          `tests/nonmem_golden.json` carries NONMEM's numbers into `npm test`.
  record_tac_regression.mjs  Re-records tests/tac_v121_regression.json (bit-identity baseline for optimisations).
  calibrate_storset.mjs   Tacrolimus simulation–recovery study (9 cells; results in docs/CALIBRATION_RESULTS_STORSET.md).
  calibrate_evr.mjs       Everolimus simulation–recovery study (12 cells; docs/CALIBRATION_RESULTS_EVR.md, tables by assemble_calibration_evr.mjs).
  audit_fields.js         Browser audit: does the text in every visible field fit its box? `window.auditFields()` after loading the built page; run it at 320 to 1920 px, for every drug and schedule mode (it found the clipped date and select fields of 4 Oct).
  record_evr_regression.mjs, record_ui_snapshot.mjs   Re-record tests/evr_regression.json; record the MPA/tacrolimus UI text (only BEFORE a deliberate change).
  record_ped_regression.mjs, ped_regression_cases.mjs   Re-record tests/mpaped_regression.json and tests/tacped_regression.json (the cases live in one shared module).
  nonmem_verify/ped/      Pediatric NONMEM check: `run_ped.sh` (structure; `POSTHOC=1` and `BAYES=1` for the later parts), the independent oracle, the authors' verbatim streams.
  calibrate_ped*.mjs      The independent calibration and benchmark of the pediatric models (written by the statistician seat; results in docs/CALIBRATION_RESULTS_PEDIATRIC.md).
  evr_prototype/          Matrix-exponential and RK4 oracles and the prototype closed form for everolimus (used by the tests).
  nonmem_verify/evr/      Everolimus NONMEM check: `bash tools/nonmem_verify/evr/run_evr.sh` (structure + POSTHOC); export_golden_evr.mjs writes the test fixtures.
  verify_model.mjs        Model-ingestion gate (MPA, tacrolimus, everolimus and the two pediatric blocks). All must pass.

docs/
  RELEASE_NOTES_V110/V120/V140/V150.md   What each release changed, with its evidence and open items.
  HANDOFF_EVEROLIMUS_M3.md, HANDOFF_PEDIATRIC.md   The plan, decisions and source readings of the two later model additions.
  VERIFICATION_TEAM.md, VERIFICATION_PEDIATRIC.md  Review gates and verdicts; REVIEW_PEDIATRIC_*.md are the three independent reviews.
  CALIBRATION_RESULTS*.md, NONMEM_CROSSCHECK*.md   Simulation-recovery studies and the NONMEM comparisons, per drug.
  MODEL_ANALYSIS_*.md, IMPLEMENTATION_PLAN_*.md, SOURCE_ANALYSIS_*.md   Transcription records, plans and source readings.
  METHODS_AUDIT_V101.md, ADVERSARIAL_*.md, PERFORMANCE_AUDIT_V121.md   The audits (all P0/P1 items fixed; P3-2 in-table dose editing is open).

Module pattern (match it): every src/*.js is an IIFE that attaches to the single global namespace root.ECU and ends with })(typeof window !== 'undefined' ? window : globalThis); so the same files run in the browser and under Node for tests. ES5 style (var, function declarations), no build transpilation, no modules, no bundler.

6. The engine in one page

Units of the adult MPA engine: hours, mg, mg/L, mg·h/L (rule 3). The tacrolimus and everolimus engines work in µg and µg/L; §12 to §14 say so.

  • OFV(η) = η' Ω⁻¹ η (prior) + Gaussian residual likelihood with log-transformed variance. Censored (< LLOQ) samples are not supported (owner decision, v1.1): there is no LLOQ input, no per-sample box and no M3 likelihood, and runFit refuses a sample carrying lloq rather than dropping it or fitting a value (a zero biases AUC by ~27%, the v0.2.4 audit defect S1/C1). Users omit such samples; an old session file's censored samples are left out on import with a message. There is likewise no assay field and no error-multiplier input (the model has no assay covariate).
  • MAP: multi-restart Nelder–Mead over η. Posterior: random-walk Metropolis started at MAP, proposal scaled from a Laplace Hessian, 40% burn-in + thinning, seeded mulberry32 RNG → results are reproducible run-to-run.
  • AUC₀–12h (app-wide target): trapezoid integration of each posterior curve over [t_last dose, t_last dose + τ] on an hour grid (24 intervals), then normalized per draw: AUC₁₂ = (AUC₀₋ₓ/x)·12 (identity at x=12). Because the transform is positive-linear, quantiles map exactly and window probabilities computed on the normalized chain equal the raw chain tested against bounds × 12/x, so the forecast targets the AUC₀–12h window whatever interval the patient uses. aucRaw/aucChain keep the as-simulated AUC₀₋ₓ; the UI shows the provenance ("from AUC₀–24h … × 12/24"). Summary is the posterior median with a 5–95% interval.
  • Trough = C(t_last + τ). Never min(curve). No target, informational only.
  • Window probabilities: strict P(lo < AUC < hi) plus P(below lo) and P(above hi); both bounds are real. Defaults 30–60 mg·h/L (§7) and are editable clinical input.
  • Single-dose guard: in full-schedule mode with only one dose, the app labels the AUC as single dose from zero, not steady state (badge, warning, report note) instead of presenting it as an SS value (audit defect S2/C2, fixed in v0.2.4).
  • Dose explorer (doseScan): replays a full maintenance regimen (ssNDoses doses, default 10) at the candidate amount and the user-chosen interval (12 or 24 h) to true steady state, reusing this patient's posterior draws. It does not refit and does not pick doses. All AUCs are normalized to a 12-hour equivalent, AUC₁₂ = (AUC₀₋ₓ/x)·12, applied per posterior draw, the same contract as the main forecast (see the AUC bullet above). aucRaw carries the as-simulated AUC₀₋ₓ for display; the trough stays C(x), never normalized. Output contract: expected AUC₁₂ + 5–95% interval, the three window probabilities, predicted trough.
  • Steady-state input mode: the UI accepts "dose + interval" directly and reconstructs the implied schedule (effectiveDoses()); full-schedule mode accepts actual dose events. Both feed the same runFit.

The generic machinery is also tested through installStubSpec() (a tiny 1-compartment oral model inside model.js): tests unlock the stub, prove the machinery, then restorePendingSpec(). The stub never ships enabled; the real MPA spec is live.

Other drugs. A drug with spec.custom supplies its own parameters, simulation, ingestion and exposure hooks. It takes the BFGS MAP (Nelder–Mead is unreliable beyond about 8 dimensions) and reports the steady state of the last regimen on a typical day, not the history that was typed (§12 to §14). One thing is shared by every drug: makeOfv floors the residual variance at 1e-6. NONMEM's likelihood has no such floor; it matters only for predictions below about 2 mg/L (MPA) or 5 ng/L (tacrolimus). The owner decided to leave it.

7. Clinical metadata (already integrated, cited, and tested)

  • Therapeutic window default 30–60 mg·h/L for kidney transplant, MMF + CNI ± steroids, adult & pediatric (Bergan et al., Ther Drug Monit 2021;43:150–197, grade B, II).
  • The on-screen window hint is deliberately a short pointer ("See “Mycophenolic acid background” for reference values and best practices for TDM", golden rule 9; the ≤120-char limit is test-enforced). The full per-indication table lives in the "Mycophenolic acid background" modal: liver 30–60; heart >36; lung none established; adult HSCT AUC₀–₂₄h >30 (different interval); proliferative lupus nephritis ≈50 (prefer 45–60; Wuttiputhanun et al., Lupus Sci Med 2024;11:e001093); childhood nephrotic syndrome >45–50 (Querfeld & Weber, Pediatr Nephrol 2018;33(12):2253–2265); other autoimmune use has no validated target (local practice). The ⓘ help on the window fields carries the same list in short form. Transplant targets apply mainly to the first posttransplant year and are not validated for EC-MPS.
  • Sampling guidance: MMF ≥2 samples (typical LSS 20 min, 1 h, 3 h); EC-MPS 3–4 samples (1.5 h, 2 h, 4 h ± 6 h); trough-only discouraged. Shown at sample entry and in Getting started.
  • Formulation is the only covariate and is printed in the report. There is no assay field: the model has no assay covariate, so recording one implied an effect it does not have (immunoassays overestimate MPA 15–37% vs LC-MS/MS; the About text says so).
  • The therapeutic window is clinical input, prefilled from the spec, editable, with its source shown. The app takes no position on the window.

When a clinical number changes, update the spec in src/model.js, keep the citation with it, and keep the tests that lock it (tests/run_all.js, "Consensus clinical metadata" section) in step. They assert the hint cites Bergan and covers every indication, so an accidental revert to kidney-only text fails the suite.

8. Model ingestion: done for de Winter 2008, and the procedure for the next model

The MPA spec in src/model.js now carries the de Winter 2008 model (final model 4): CL 16, Q 22, V1 40, V2 518 (L/h, L), ka 4.1 (MMF) / 3.0 (EC-MPS) per hour, tlag 0.30 h (MMF) or the 51/32/17% morning mixture (0.95/1.88/4.83 h) + 9.04 h evening lag (EC-MPS), log-scale residual σ 0.39, no allometry, no covariates except formulation. The full analysis, scrutiny (S1–S12) and validation matrix live in docs/MODEL_ANALYSIS_DEWINTER_2008.md and docs/IMPLEMENTATION_PLAN_DEWINTER_2008.md.

The ingestion procedure (kept for the next model):

  1. Fill the spec fields in src/model.js (structure, THETA, ETA + iiv, SIGMA, FORMS, covariates). Convert any day-based parameters to hours at this boundary only (rule 3).
  2. Set pending: false and write info/article/assumptions (the model card reads them).
  3. Extend covariateFields(); the Patient card generates covariate inputs from it.
  4. npm test green; golden values into tests/golden.json.
  5. node tools/verify_model.mjs (strict) must pass.
  6. npm run build; bump the version.

For a new drug, do not edit the engine. Use the spec.custom route that everolimus and the pediatric drugs took: copy src/everolimus.js and src/texts_evr.js as the template, then register the new files in build.mjs (FILES), index.html (script tags and the hidden #pt-drug select), src/parallel.js (SOURCES), tools/verify_model.mjs and DRUG_ORDER in src/model.js. Add a card (spec.card), spec.report and the texts; the picker, the UI and the report read the spec. The handoffs of §13 and §14 list the decisions each addition needed.

Numerical validation of the adult MPA model (see the docs): mass-balance golden anchors (MMF 1000 mg → AUC₁₂ = 739/16 ≈ 46.2), the analytic IV bi-exponential, the EC-MPS 24h-periodicity invariant (morning + evening windows = 2 × dose/CL), SS depth 30 doses (terminal t½ ≈ 40 h), and the V10 simulation–recovery calibration with the S1/Ω-approximation robustness arms.

9. Testing discipline

  • The suites are tests/run_all.js (adult MPA and the engine) and test_tacrolimus.js, test_everolimus.js, test_pediatric.js, test_report.js, test_refresh.js, test_audit150.js and test_visits.js, all on tests/harness.js (async t() cases; await runAll() is wired in each runner; the exit code reflects failures). Failures go to stderr, so read both streams. The harness gives every test a timeout (300 s, TEST_TIMEOUT_MS to change it) and fails the run on an unhandled promise rejection.
  • What the regression records are, and are not. V16, V16b and the pediatric records were recorded from the code at the time, so they prove that a later change did not move the numbers, not that the numbers were right at recording. The independent evidence is the NONMEM cross-checks, the oracles and the exact anchors (S2/S3). A sampler-only defect that was already present when a record was made would be pinned as correct; the NONMEM BAYES comparison is what covers that.
  • Every assertion must be able to fail. Before trusting a new test, break the thing it guards and watch it go red, then restore and watch it go green. Tests that cannot fail are deleted or fixed.
  • Engine tests use installStubSpec() / restorePendingSpec() and never leave the stub installed.
  • UI-copy tests load src/ui.js under Node (it no-ops DOM binding without a document) and assert on the exported aboutHtml() / gettingStartedBodyHtml() / backgroundHtml() strings; copy changes must keep these in step.
  • The version-sync test compares src/version.js with package.json; run npm test after any bump.

10. Conventions and gotchas

  • Print: the report is #reportSheet; @media print hides everything else. Any new visual work must not break this (verify with Print → preview).
  • Modals are div.modal-backdrop[hidden] + .modal; the close-ids list in ui.js must include every new modal; Escape and backdrop-click close them.
  • Help popovers: button.info[data-help="key"] + the HELP map in ui.js.
  • Session export/import and autosave serialize ECU.ui.state; new fields must round-trip (add a test).
  • Time inputs are civil datetime-local strings converted with dtLocalToHours() / hoursToDtLocal(), timezone-naive by design.
  • The parent repo contains a sibling complement-TDM app (mAbs) with a similar shape but a days-based engine and a different CSS. Treat it as inspiration for patterns only, never as copy-paste source.

11. Open items (v1.5.1, 13 October 2026)

  • Adversarial review of 13 October. Fixed: C1, C2, H1 to H6 and most of the Medium and Low items, each with a test that failed on 1.5.0 first. Not changed, with reasons, in docs/ADVERSARIAL_FIXES_V151.md: the multi-start MAP (below), the roughly 50 source-text tests, the confirmation on a single row delete, and the dose-advice copy bans.

  • Pediatric drugs. Three independent seats reviewed them and each gave "pass with comments" (docs/REVIEW_PEDIATRIC_*.md, docs/VERIFICATION_PEDIATRIC.md). The wording they asked for was applied afterwards and has not been re-reviewed. Still open: the tacrolimus 3-point NRMSE in the benchmark replication (16.3 % simulated against 7.8 % published; accepted as a documented point, to be put to the authors); the owner's check of the Brunet 2019 trough ranges and of Schijvens 2020; a print check of the report in Safari itself (1.5.1 checked A4 and Letter in headless Chrome and scales a tall report to fit one page; before that, one report configuration was 16 px over the 1103 px safety margin); a multi-start MAP (single-start can end in a worse mode; a 1.5.1 candidate); two-line MPA card names at 320 px and the fold position of the first input with five cards (designer and owner decisions). See docs/RELEASE_NOTES_V150.md, "Open items".

  • Everolimus. No named human has reviewed it (docs/VERIFICATION_TEAM.md, addendum written). The page is far above the old ~350 KB budget (the owner accepted the growth for the fonts). No interaction field, no transplant-date field and no AUC window, by owner decision.

  • Tacrolimus (adult). The standard kidney window is shipped (trough 4–12, AUC 150–210; the AUC upper bound is the app's choice, docs/SOURCE_ANALYSIS_BRUNET_2019.md). There is no transplant-date field (the first-weeks bias is stated in the text, not detected); once-daily and other schedules are refused, not modelled. See docs/ADVERSARIAL_AUDIT_V121.md.

Carried over from the MPA releases

  • Author queries (S1/diagonal-Ω): full Ω covariance and the ω² convention from the de Winter authors: drafted (the drafts, docs/AUTHOR_QUERY*.md, are kept private and are not in this repository); the robustness arms measured the impact as AUC-benign for rich sampling; with sparse data (trough only) the choice moves the interval width ≈10 % and P(window) a few pp; stated in the model card. The paper reports IIV as % only (Table III); the conversion is our reading.
  • EC-MPS window anchoring (S12): shipped in v1.0.1: M.aucAnchor auto-anchors the AUC window at the most recent morning dose for evening-ending EC-MPS schedules (fit unchanged, only the derived window moves; UI/report/diagnostics carry the note).
  • P3-2: dose/observation tables are delete-only; in-place editing would help.
  • ~~Browser runtime ~30–60 s per fit~~: resolved in v1.1.0: closed-form propagation and exact steady state, ≈0.5 s per fit; no worker thread needed.
  • EC-MPS convergence: 5–15 % of EC-MPS fits do not pass the R̂/ESS check (slow mixing of the discrete absorption subgroup; more iterations do not fix it). The app flags them. A sampler change (joint η/membership moves) is the remedy.
  • MAP in the diagnostics dialog is a single-start optimum and can be a local minimum with sparse data (NONMEM's POSTHOC behaves the same; docs/NONMEM_CROSSCHECK.md). Reported AUC/trough/probabilities use the posterior and are unaffected. Multi-start MAP and a marginal EC-MPS subgroup choice would change the displayed individual predictions. Not done, owner's call.
  • Verification gate (docs/VERIFICATION_TEAM.md): the verdicts for the adult drugs and everolimus are pending; no independent human review has signed. The pediatric verdicts are from agent seats, not named people.
  • Step grids remain on pt-wt, pt-age, ss-interval (golden rule 7c; not on the audit's list).

12. Tacrolimus (v1.2.0): what is different

  • Units: engine in µg and µg/L (doses are entered in mg; the single mg→µg conversion is custom.doseToEngine; a test forbids * 1000 and the immunoassay constants anywhere else). Time is still in hours (golden rule 3).
  • Model: plasma PK (2-compartment, ka, lag 0.41 h), scaled to fat-free mass; whole blood = Cp·(1+Hct·418/(Cp+3.8)); CYP3A5 and prednisolone act on CL and F; Ω is a correlated 3×3 block (CL, V1, Q) plus one κ on F and, only for days with a sample ≤ 4 h after a dose, one κ on ka, per sampled day (12 most recent). The first-day bioavailability effect and any genotype mixture are not modelled (owner decisions D2, D4).
  • What is reported: steady state of the last regimen on a typical day (κ = 0), also when a finite history was typed; AUC₀–₁₂ₕ and trough, actual (haematocrit of the latest sample) and corrected to 0.35. An unset window gives intervals without probabilities.
  • Assay: LC-MS/MS or Abbott CMIA (Størset 2014 Eq. 1). Results are shown on the chosen scale.
  • MAP/sampler: BFGS MAP (Nelder–Mead is unreliable beyond ~8 dimensions) and a dimension-scaled random-walk step; MPA keeps its validated settings.
  • fit.shrink is shrinkage, Var(post)/ω² (1 = the samples left the population variance in place; fixed in v1.2.1: until v1.2.0 it held information gained, 1 − Var/ω², and the MPA bars and sparse-data note read it backwards; docs/METHODS_AUDIT_V101.md, ST4 addendum). Tacrolimus still shows and tests information gained (1 − fit.shrink), deliberately: its bars are red when low and its note fires below 0.3.

13. Everolimus (v1.4.0): what is different

  • Model: Model 3 of Zwart 2021, used exactly as coded in the supplement (mg and mg/L there; µg and µg/L in the engine, binding constants ×1000). Five equal absorption stages, a liver compartment with flow-limited extraction (plasma flow QH·(1 − Ht), so haematocrit is a PK covariate), two-compartment plasma disposition, saturable red-cell binding. Three etas (CLINT, V3, FU), diagonal Ω, log-scale residual (SIGMA.LOG), no occasion layer. Prednisolone at 20 mg/day or more multiplies CLINT by 1.44 (a required two-way choice, no default).
  • Closed form: src/everolimus.js solves the three-pole disposition (cubic, trigonometric) and convolves it with the Erlang-5 input; steady state is exact (geometric sum). The small-x and λ > k ranges use a tail series with no division by (k − λ). If the cubic does not give three real negative roots the run is refused (poles() returns null), never approximated.
  • What is reported: steady state of the last regimen, AUC₀–₁₂ₕ and trough, actual (haematocrit of the latest sample) and corrected to 0.38 (Eq. 3: the same plasma curve read at 0.38). No AUC window ships; trough sets evr-cni (3–8, default) and evr-nocni (6–10). An unset window gives intervals without probabilities. A run without a sample is allowed (population forecast).
  • Haematocrit: one value for the PK path (the latest sample's), each sample's own for the blood reading. NONMEM uses the Ht of the record at the end of each interval; the measured cost is mean 1.0 %, max 6.2 % in earlier-sample predictions (test: NONMEM: the everolimus structural model ...).
  • Assay: LC-MS/MS only, one fixed line, no assay field. Scope: 10–14 h interval, 0.25–5 mg, 0.5–60 µg/L, haematocrit 0.10–0.65 L/L; checks in inputProblems.
  • UI wiring: ui.js reads spec.ui flags and ECU.drugTexts[id]; a drug without an entry gets neutral text. Never hand-edit a drug's wording into a shared string: the snapshot test (tests/ui_text_snapshot.json) holds MPA's and tacrolimus's text and fails on any change.
  • Accuracy claims in About come from Zwart 2021 Table 2 (Model 3), checked against the PDF; "about two thirds within ±30 %" is Table 2's 65–69 %.
  • Verification record: docs/RELEASE_NOTES_V140.md (checks, sabotage list) and docs/CALIBRATION_RESULTS_EVR.md.

14. Pediatric MPA and pediatric tacrolimus (v1.5.0): what is different

  • Registration: src/mpaped.js and src/tacped.js are spec.custom specs like everolimus; the card order is DRUG_ORDER in model.js; nothing else in ui.js, report.js or diagnostics.js names a drug (a test checks it). Flags on the spec: ui.bloodCorrection (false for MPA), doseForms (per-dose formulation), ui.windowOn: 'corrected' (tacrolimus pediatric), ui.doseNoun, scope and custom.scopeWarnings (the range the model was built on: a warning, never a refusal).
  • MPA: dose in mg of MMF, concentration mg/L of MPA, no 0.739 factor (test MI3: 600 mg gives AUC 58.7, not 43.4). Albumin normalised by 34 g/L and the exponent -2.49 (the ESM text and Eq. 1 say 35 and -2.5; run 57 and Table 2 agree with 34). Steady-state AUC = dose x exp(kappa)/CL exactly. Occasion = calendar day, a pre-dose sample belongs to the previous dose's day, up to 10 sampled days.
  • Tacrolimus: the formulation is a property of the dose (d.form), carried by ssHistory, prepare, runFit (steady-state spec) and doseScan. NONMEM applies the record's KA to doses still in transit; the app gives each dose its own (difference at most 1.3e-3 at 12-hourly dosing). The corrected value is the same plasma curve read at haematocrit 0.35; the windows are judged on it.
  • Do not use NONMEM SS=1 with ADVAN6 as the tacrolimus reference for fast absorption (4 % off at KA about 60 /h); the stored fixtures use explicit 80-dose trains. Run NONMEM BAYES jobs one at a time (two in one directory overwrite each other's work files) and do not mark every parameter FIX in a BAYES stream (no estimation step).
  • Verification tools: tools/nonmem_verify/ped/run_ped.sh (structure; POSTHOC=1, BAYES=1 for the later parts), tools/calibrate_ped*.mjs (the independent calibration and benchmark, written by the statistician seat), tools/record_ped_regression.mjs.
  • Which value the window is judged on. The tacrolimus targets are whole-blood values that the sources take to refer to haematocrit 0.35, so pediatric tacrolimus judges the corrected value (ui.windowOn: 'corrected'). The tile, the chips, the printout and the saved session say which value and which window set (state.winSet, windowNoteOf); typed bounds read "your own bounds". Pediatric MPA has one set (30 to 60) and no corrected value.
  • Conventions. The reported AUC is the typical day (occasion effects zero), and unsampled dosing days carry an effect of zero. The true AUC of a single day scatters about it by ×/÷ 1.2 to 1.3 (one SD). The one-haematocrit PK path costs a few per cent on average (up to about 20 % for some earlier samples) when the haematocrit differs by more than about 0.05 between samples. Warnings, never refusals: weight outside the data (MPA 12.9 to 79.9 kg, tacrolimus 9.1 to 78 kg), albumin outside 24 to 42 g/L, haematocrit outside 0.15 to 0.60. Refusals: weight outside 3 to 200 kg, albumin outside 5 to 50 g/L, haematocrit outside 0.10 to 0.70.
  • Not in the app, by decision: the starting-dose table of Heida 2024, mg/m² guidance, an mg/kg dose check, an age field, a transplant-date field, a "sampled-day" line (D3), the app's own simulation figures in its text, a rename of "Run forecast".

About

NephroTDM: model-informed therapeutic drug monitoring for adult kidney transplant recipients (mycophenolic acid, tacrolimus, everolimus). Single-file, offline web app. Research use only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages