This release is a pre-release and may not be stable for production use.
PKflow
A composable command-line workflow tool for pharmacometric modeling.
PKflow turns the run → diagnose → compare → report loop of population PK/PD modeling into a handful of scriptable commands. Fit a NONMEM model, collect its results into a tidy, file-based format, and generate goodness-of-fit plots, VPCs, bootstrap confidence intervals, shrinkage tables, η–covariate plots, and a shareable report — all from the terminal or as a Python library.
It is a clean-room Python rewrite of the ideas behind the classic Pirana workbench, with three deliberate design choices:
- File-based, not a database. Every run is a self-contained directory
(
results.yaml+ parquet sidecars) — diffable, reproducible, and git-friendly. - A thin backend protocol. Modeling-engine specifics live behind a small
parse / run / collectinterface. Today that backend is NONMEM (via pharmpy); the diagnostics, workflows, and report layers are engine-agnostic. - Pure functions you can test. The statistics (VPC binning, bootstrap CIs, shrinkage, comparison tables) are pure and unit-tested without needing NONMEM.
Status: early alpha (
0.1.0a7). The NONMEM workflow below works end-to-end against a realnmfebinary. APIs may still change.
Table of contents
- Install
- Quickstart
- Configuration
- Examples — one per feature
- Run directory layout
- Architecture
- Development
- Contributing
- Roadmap
- Citation
- Acknowledgements
- Author
- License
Install
Requires Python ≥ 3.10.
pip install pkflow
To actually run models you also need:
- A NONMEM installation with an
nmfescript onPATH(or point at it viapkflow config set nmfe <path>— see Configuration). - pandoc (system package) — only for
report --format html|docx|pdf. Markdown reports and everything else need no extra tooling. - A PDF engine — only for
report --format pdf. pkflow drives whichever it finds first:weasyprint(pip install weasyprint),wkhtmltopdf, or a LaTeX engine (tectonic/xelatex/pdflatex).
Python dependencies (installed automatically): pharmpy-core, pandas,
pyarrow, plotnine, scikit-misc, jinja2, pyyaml, typer, tomli-w.
Quickstart
# 0. Tell pkflow where NONMEM lives (once, stored in ~/.config/pkflow/config.toml)
pkflow config set nmfe /opt/nm760/run/nmfe76
# 1. Fit a model — creates runs/<name>_<timestamp>/
pkflow run model.ctl
# 2..N. Downstream commands default to the LAST successful run, so you can
# drop the run-dir argument entirely:
pkflow show
pkflow diagnose
pkflow vpc
pkflow shrinkage
pkflow report --format pdf --gof
Every command is independent and operates on a saved run directory, so you can
re-run, re-collect, and re-diagnose without re-fitting. Pass an explicit
<run_dir> whenever you want a run other than the most recent one:
pkflow show runs/model_20260609_120000/
pkflow diagnose runs/model_20260609_120000/
Configuration
pkflow reads configuration from two layered TOML files (last wins):
built-in defaults < ~/.config/pkflow/config.toml < ./pkflow.toml < CLI flags
Manage it with pkflow config — no need to hand-edit files:
pkflow config show # resolved values + their source
pkflow config set nmfe /opt/nm760/run/nmfe76 # user-global (default scope)
pkflow config set --project runs_dir output # project-local ./pkflow.toml
pkflow config get nmfe
pkflow config unset nmfe
pkflow config path # where the user file lives
| Key | Default | Meaning |
|---|---|---|
backend |
nonmem |
Modeling engine (only NONMEM today) |
executor |
local |
Local subprocess runner |
nmfe |
nmfe75 |
Path to your NONMEM nmfe script |
runs_dir |
runs |
Where run directories are created |
Use the user scope for machine-wide settings like the nmfe path, and the
project scope (--project, writes ./pkflow.toml) for per-study overrides.
Per-invocation flags such as --backend / --runs-dir win over both.
Examples
The examples below use a 2-compartment IV model warfarin.ctl. Replace it with
your own control stream — PKflow reads $INPUT, $DATA, parameter blocks, and
result files (.lst, .ext, .phi) through pharmpy.
1. Run a model
pkflow run warfarin.ctl
→ runs/warfarin_20260609_120000
status: ok ofv: 1234.56 (21.9s)
run creates an isolated run directory, copies the dataset in and rewrites
$DATA so models with relative data paths just work, executes NONMEM, then
collects everything into results.yaml + parquet sidecars (parameters,
predictions, η estimates, covariates).
2. Inspect saved results
pkflow show runs/warfarin_20260609_120000/
run : warfarin_20260609_120000
backend : nonmem
status : ok
ofv : 1234.56
aic/bic : 1250.56 / 1278.10
cond # : 18.3
parameters:
name type estimate se rse_pct
CL theta 0.134 0.0042 3.1
V1 theta 8.110 0.2100 2.6
Q theta 0.220 0.0180 8.2
OMEGA_1_1 omega 0.091 0.0150 16.4
show reads only the saved files — no NONMEM needed. Use pkflow collect <run_dir> to re-parse the NONMEM output of an existing run without re-fitting.
3. Compare runs
Rank competing models side by side. ΔOFV is relative to the best (lowest) run; failed runs are excluded from the "best" calculation.
pkflow compare runs/base_*/ runs/covCL_*/ runs/covCL_V_*/ --sort ofv --gof
run_id status ofv delta_ofv n_params aic bic condition_number
covCL_V_20260609 ok 1208.9 0.0 9 1226.9 1236.9 18.3
covCL_20260609 ok 1210.2 1.3 7 1224.2 1234.2 18.3
base_20260609 ok 1234.5 25.6 5 1244.5 1254.5 18.3
→ compare/comparison.csv
→ compare/compare_gof.png # overlaid DV-vs-PRED, colored by run
4. Bootstrap confidence intervals
Nonparametric case-resampling bootstrap: subjects are resampled with replacement (and relabeled to keep duplicates distinct), the model is refit on each replicate, and percentile CIs are reported. Non-converged replicates are excluded and counted.
pkflow bootstrap warfarin.ctl --n 200 --ci 0.95
→ runs/warfarin_20260609_121500 (200 replicates)
converged: 196/200
name original_est boot_median boot_se ci_lo ci_hi n_success
CL 0.134 0.135 0.0051 0.125 0.145 196
V1 8.110 8.090 0.2400 7.640 8.580 196
OMEGA_1_1 0.091 0.087 0.0190 0.052 0.128 196
→ runs/.../bootstrap/bootstrap_summary.csv
Per-replicate run directories are cleaned up automatically; the summary and the
raw per-replicate estimates (replicate_params.parquet) are kept.
5. Goodness-of-fit plots
The standard 4-panel GOF (DV-vs-PRED, DV-vs-IPRED, CWRES-vs-PRED, CWRES-vs-TIME), rendered with plotnine:
pkflow diagnose runs/warfarin_20260609_120000/
runs/.../diagnostics/dv_vs_pred.png
runs/.../diagnostics/dv_vs_ipred.png
runs/.../diagnostics/cwres_vs_pred.png
runs/.../diagnostics/cwres_vs_time.png
→ 4 plot(s) in runs/.../diagnostics
GOF needs a
$TABLEwithDV PRED IPRED CWRES TIMEwritten to ansdtab-style file so PKflow can find it.
6. Visual Predictive Check (VPC)
PKflow converts the fitted model to a simulation ($SIMULATION with N
subproblems), runs it, bins observations by time, and overlays the observed
5/50/95 percentiles on the simulated prediction intervals.
pkflow vpc runs/warfarin_20260609_120000/ --n-sim 500 --n-bins 10
→ runs/.../diagnostics/vpc.png (+ vpc.csv with the binned percentiles)
7. η / ε shrinkage
A shrinkage table (flagging values above a threshold, default 30%) plus a faceted histogram of the individual η estimates.
pkflow shrinkage runs/warfarin_20260609_120000/ --threshold 0.30
parameter kind shrinkage shrinkage_pct high
ETA_1 eta 0.0868 8.68 False
ETA_2 eta 0.4171 41.71 True
ETA_3 eta 0.6388 63.88 True
→ runs/.../diagnostics/shrinkage_table.csv
→ runs/.../diagnostics/eta_distributions.png
8. η–covariate plots
Scatter of each η against each subject-level covariate, with a linear trend.
Covariates are auto-detected (constant-within-subject, varying across
subjects); override with --cov.
# auto-detect covariates
pkflow etacov runs/warfarin_20260609_120000/
# or name them explicitly
pkflow etacov runs/warfarin_20260609_120000/ --cov WT --cov SEX --cov AGE
→ runs/.../diagnostics/eta_covariates.png (facet grid: η rows × covariate cols)
→ runs/.../diagnostics/eta_covariates.csv
9. Reports (md / html / docx / pdf)
Assemble fit summary, parameter table, shrinkage, any bootstrap result, and the
diagnostic plots into one document. Markdown is the canonical render; HTML, Word,
and PDF are produced via pandoc.
# Markdown (no extra dependencies)
pkflow report runs/warfarin_20260609_120000/ --format md
# Word document, generating GOF plots first and embedding them
pkflow report runs/warfarin_20260609_120000/ --format docx --gof
# PDF (needs a PDF engine: weasyprint / wkhtmltopdf / a LaTeX engine)
pkflow report --format pdf --gof # defaults to the last run
# HTML
pkflow report --format html
→ runs/.../report/report.pdf
docx/html/pdf require pandoc on PATH; pdf additionally needs a PDF
engine — pkflow auto-detects weasyprint, wkhtmltopdf, tectonic, xelatex,
or pdflatex (in that order) and gives a clear, actionable error if none is
installed.
10. Use it as a Python library
Everything the CLI does is available as importable functions. The statistics are
pure — feed them a Results object (from a saved run or constructed in memory):
from pathlib import Path
from pkflow import backends
from pkflow.executors import LocalExecutor
from pkflow.model import Results
from pkflow.compare import build_table
from pkflow.diagnostics import save_gof, shrinkage_table
from pkflow.workflows import bootstrap
be = backends.get("nonmem")
ex = LocalExecutor({"nmfe": "/opt/nm760/run/nmfe76"})
# parse → run → collect
model = be.parse(Path("warfarin.ctl"))
handle = be.run(model, Path("runs/wf"), ex)
res = be.collect(model, Path("runs/wf"), handle)
res.save(Path("runs/wf"))
# load a saved run later
res = Results.load(Path("runs/wf"))
# pure analytics
table = build_table([Results.load(p) for p in Path("runs").glob("*/")])
shr = shrinkage_table(res, threshold=0.3)
save_gof(res, Path("runs/wf/diagnostics"))
# a full bootstrap workflow
boot = bootstrap(model, res, be, ex, Path("runs/wf"), n=200, seed=1234)
print(boot.summary)
Run directory layout
A run directory is the unit of reproducibility:
runs/warfarin_20260609_120000/
├── results.yaml # fit metadata: status, ofv, aic/bic, cond#, shrinkage
├── parameters.parquet # estimates + SE + RSE%
├── predictions.parquet # $TABLE output (DV/PRED/IPRED/CWRES/...)
├── etas.parquet # individual η estimates
├── covariates.parquet # per-subject covariates
├── warfarin.ctl # the control stream that was run
├── diagnostics/ # GOF, VPC, shrinkage, η-covariate PNGs + CSVs
├── bootstrap/ # bootstrap_summary.csv + replicate_params.parquet
└── report/ # report.md / .html / .docx
Architecture
pkflow/
├── cli.py # typer entrypoint — every command is a thin wrapper
├── config.py # pkflow.toml loader
├── compare.py # cross-run table + overlaid GOF (pure functions)
├── model/
│ ├── base.py # backend-agnostic Model
│ └── results.py # unified Results + save/load (yaml + parquet)
├── backends/
│ ├── base.py # Backend protocol: parse / run / collect / simulate
│ └── nonmem.py # pharmpy-backed NONMEM implementation
├── executors/
│ └── local.py # local subprocess runner
├── diagnostics/
│ ├── gof.py # 4-panel goodness-of-fit
│ ├── vpc.py # backend-agnostic VPC (compute + plot)
│ └── shrinkage.py # shrinkage table, η distributions, η-covariate plots
├── workflows/
│ └── bootstrap.py # case-resampling bootstrap (pure stats + orchestrator)
└── report/
├── render.py # context builder + Jinja2 markdown + pandoc convert
└── templates/ # run_report.md.j2
Extending it is meant to be small:
- A new backend (e.g. another estimation engine) = one file implementing
parse / run / collect. - A new executor (e.g. Slurm/SGE) = one file implementing
submit / wait.
The diagnostics, comparison, bootstrap, and report layers consume the unified
Results object and don't care which engine produced it.
Development
pip install -e ".[dev]"
python -m pytest # full suite
The test suite covers every module. Pure-function tests (config, results,
compare, bootstrap, VPC math, shrinkage, report rendering) run without
NONMEM using in-memory Results; NONMEM-dependent paths are exercised with a
real .mod template and stubbed/faked boundaries. Pandoc-dependent report tests
skip automatically when pandoc is absent.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for
details. In short:
- Open an issue to discuss bugs or feature ideas before large changes.
- Follow test-driven development — add a failing test first, then the implementation. Keep statistics as pure functions where possible.
- Run
python -m pytestand make sure the suite is green before opening a PR.
Roadmap
- Categorical-covariate boxplots in η–covariate plots
- Cluster executors (
slurm,sge) - Additional report sections and templating hooks
The backend protocol is intentionally general, but the project is focused on NONMEM for now.
Citation
If you use PKflow in your research, please cite it:
@software{zhang_pkflow,
author = {Zhang, Yufeng},
title = {PKflow: A composable command-line workflow tool for pharmacometric modeling},
year = {2026},
url = {https://github.com/kinginsun/pkflow}
}
Acknowledgements
PKflow stands on the shoulders of excellent open-source work:
- pharmpy — NONMEM control-stream parsing and result handling.
- plotnine — grammar-of-graphics plotting for all diagnostics.
- pandas, Typer, Jinja2, and pandoc.
- The original Pirana workbench, whose workflow inspired this rewrite.
Author
Yufeng Zhang School of Pharmacy, The Chinese University of Hong Kong (CUHK) Contact: zhangyf@cuhk.edu.hk
License
Released under the MIT License — see LICENSE.
MIT License
Copyright (c) 2026 Yufeng Zhang
PKflow is an independent Python project and is not affiliated with the original Pirana software.
Metadata
Release files for pkflow 0.1.0a7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pkflow-0.1.0a7.tar.gz | 61.4 kB | With provenance | Details for pkflow-0.1.0a7.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pkflow-0.1.0a7-py3-none-any.whl | Python 3 | none | any | Details for pkflow-0.1.0a7-py3-none-any.whl |
Total release size: 100.7 kB
Release files / pkflow-0.1.0a7.tar.gz
| Download URL | pkflow-0.1.0a7.tar.gz |
|---|---|
| Size | 61.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3a40056338d9ba09d0deea8cad588c4552e4692b42fccacce40cd19421e54fc5
|
|
BLAKE2b-256 checksum How to use checksums |
ef53b4cfc140d3741e11af77561bb5f25ae0e35876a65033cc0164d9d9f75ff5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.
Transparency logRelease files / pkflow-0.1.0a7-py3-none-any.whl
| Download URL | pkflow-0.1.0a7-py3-none-any.whl |
|---|---|
| Size | 39.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe4260072b0655dce6926f7a51a1d001642ee02ccb3b8a479bd7fb7b9c82777f
|
|
BLAKE2b-256 checksum How to use checksums |
c3255bee8453e147f6b4300948840f3a10eaeac9ffa37d949275fbe554ac51aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.
Transparency logSecurity for pkflow 0.1.0a7
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pkflow-0.1.0a7.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.