Migrating from NONMEM

This page maps familiar NONMEM control stream concepts to their OpenPKPD equivalents.

For the current support classification behind these mappings, see ../user_guide/validation_matrix.md. Some mappings are exact at the syntax layer but still have narrower runtime or validation support than full NONMEM.

Control stream → Python API

NONMEM record

OpenPKPD equivalent

$PROBLEM title

.problem("title")

$DATA file.csv IGNORE=#

.data("file.csv")

$INPUT ID TIME AMT DV EVID

Inferred from column names (auto-mapping)

$SUBROUTINES ADVAN2 TRANS2

.subroutines(advan=2, trans=2)

$PK $ERROR

.pk("…") / .error("…")

$THETA (0,1.5,20)

.theta([(0, 1.5, 20)])

$OMEGA 0.5 0.3

.omega([0.5, 0.3])

$OMEGA BLOCK(2) 0.4 0.1 0.3

.omega([[0.4,0.1],[0.1,0.3]])

$SIGMA 0.1

.sigma(0.1)

$ESTIMATION METHOD=COND INTER

.estimation(method="FOCE", interaction=True)

$ESTIMATION METHOD=ZERO

.estimation(method="FO")

$ESTIMATION METHOD=SAEM

.estimation(method="SAEM")

$COVARIANCE

.covariance()

OpenPKPD also supports non-NONMEM FOCE/FOCEI robustness options in the Python API and control-stream parser, including outer-optimizer selection, fallback polish optimizers, best-iterate retention, and structured retry controls. These are OpenPKPD extensions rather than direct NONMEM mappings.

Estimation method names

NONMEM METHOD=

OpenPKPD method=

ZERO

"FO"

COND

"FOCE"

COND INTER

"FOCE" + interaction=True

COND LAPLACE

"LAPLACIAN"

SAEM

"SAEM"

IMP

"IMP"

IMPMAP

"IMPMAP"

Practical migration guidance:

  • FO, FOCE, FOCEI, and LAPLACIAN are the strongest current migration targets from a validation standpoint

  • SAEM, IMP / IMPMAP, BAYES(Laplace), and NONPARAMETRIC are real native surfaces, but their empirical validation breadth is still narrower than the core FO/FOCEI path

  • native BAYES(NUTS) should currently be treated as an experimental / second-tier backend rather than as blanket NONMEM-parity MCMC support

NM-TRAN code blocks

$PK and $ERROR blocks are written identically to NONMEM. The compiler handles:

NM-TRAN

Python translation

THETA(n)

theta[n-1]

ETA(n)

eta[n-1]

EPS(n)

eps[n-1]

F

Predicted value from PK model

Y

Predicted observation (set in $ERROR)

T

Current observation time

IPRED

Individual prediction

EXP(x)

math.exp(x)

LOG(x)

math.log(x)

SQRT(x)

math.sqrt(x)

DADT(n)

dadt[n-1] (in $DES)

A(n)

a[n-1] (compartment amount)

Using a .ctl file directly

OpenPKPD can parse real NONMEM control streams:

from openpkpd.parser.control_stream import ControlStream

cs = ControlStream.from_file("run001.ctl")
# Inspect parsed records
cs.problem.title
cs.theta_records[0].specs        # List[ThetaSpec]
cs.estimation_records[0].method  # "FOCE"

Use the CLI to parse and inspect without fitting:

openpkpd parse run001.ctl
openpkpd parse run001.ctl --json   # Machine-readable JSON

Output files

OpenPKPD writes the same output files as NONMEM 7.x:

NONMEM file

Description

.lst

Estimation log + parameter table

.ext

Parameter estimates by iteration

.phi

Empirical Bayes estimates (post-hoc ETAs)

.cov

Covariance matrix

.cor

Correlation matrix

sdtab / patab

$TABLE output

Current limitations

OpenPKPD now supports useful runtime subsets for several records that were previously listed as simply “not implemented”. Treat the control-stream user guide as the source of truth for the current parser/runtime/export contract:

The most important current limitations are:

  • $MIXTURE — partial runtime subset only. The native runner currently supports NSPOP=n with dedicated .mix.json and .mix_assignments.csv artifacts, but not the full NONMEM mixture workflow surface.

  • $PRIOR — partial runtime subset only. The native runner supports the Gaussian/NWPRI-oriented subset documented in the control-stream guide, not the full NONMEM prior semantics.

  • $SIMULATION — partial runtime subset only. ONLYSIMULATION, SUBPROBLEMS=n, the first parsed seed, and .sim.csv artifact generation are supported; broader NONMEM simulation semantics are not.

  • Round-trip write supportControlStream.to_string() / .write() can serialize the current parsed control-stream representation, but this should be treated as supported for the documented subset, not as a blanket guarantee for every NONMEM record combination.

Previously listed as limitations but now implemented:

Feature

Status

ADVAN5 (general linear, matrix exponential)

✅ Implemented

ADVAN7

✅ Implemented; overlaps ADVAN5 and is usually not needed as a separate selector

ADVAN6/8 (general nonlinear ODE, stiff/nonstiff)

✅ Implemented

ADVAN11/12 (3-compartment IV/oral)

✅ Implemented

ADVAN13 (stiff ODE + forward sensitivity)

✅ Implemented (partial)

IOV (inter-occasion variability)

✅ Implemented

NUTS/BAYES (MCMC via PyMC/native backend)

✅ Implemented, with backend-specific support boundaries

Control stream serialization (ControlStream.to_string() / .write())

✅ Implemented for the documented subset