Model Workflow
Overview
ferx_fit() is the punch line, but a productive modeling session spends most of its time before and between fits — scaffolding a model file, sanity-checking initial estimates, editing one section, refitting. This chapter shows the helpers that make that loop fast.
Scaffolding a new model file
ferx_model_new() writes a complete, valid .ferx file from a template. Five templates ship today: 1cpt_oral, 1cpt_iv, 2cpt_oral, 2cpt_iv, and ode (a generic ODE skeleton).
ferx_model_new(
path = tempfile(fileext = ".ferx"),
template = "1cpt_oral",
edit = FALSE,
print = TRUE,
overwrite = TRUE
)# One-compartment oral PK model
[parameters]
theta TVCL(1.0, 0.001, 100.0)
theta TVV(10.0, 0.1, 1000.0)
theta TVKA(1.0, 0.01, 50.0)
omega ETA_CL ~ 0.09
omega ETA_V ~ 0.09
omega ETA_KA ~ 0.25
sigma PROP_ERR ~ 0.01
[individual_parameters]
CL = TVCL * exp(ETA_CL)
V = TVV * exp(ETA_V)
KA = TVKA * exp(ETA_KA)
[structural_model]
pk one_cpt_oral(cl=CL, v=V, ka=KA)
[error_model]
DV ~ proportional(PROP_ERR)
[fit_options]
method = foce
maxiter = 300
covariance = true
print = TRUE echoes the generated file. With edit = TRUE (the default), the file is opened in your editor; overwrite = FALSE (the default) refuses to clobber an existing file.
Inspecting a model before fitting
ferx_model_inspect() parses the file and reports its structural shape — model type, ETA list, IOV, residual error — without compiling or fitting:
ex <- ferx_example("warfarin")
ferx_model_inspect(ex$model)Model structure (warfarin.ferx)
Structural: 1-cpt oral (TVCL, TVV, TVKA)
IIV: ETA_CL, ETA_V, ETA_KA
IOV: none
Residual: proportional
This is the cheapest way to confirm that the file you just edited still declares the structure you think it does.
ferx_model_validate() is the syntax check: it returns TRUE if every required section is present and parseable, and prints a per-section status line:
ferx_model_validate(ex$model)Validating: warfarin.ferx
Sections present:
parameters [ok]
individual_parameters [ok]
structural_model [ok]
error_model [ok]
fit_options [ok] (optional)
Result: VALID
ferx_model_show() prints the raw file contents — handy in a Quarto chunk when the reader wants to see what is being fit.
Editing sections programmatically
For scripted workflows (e.g. covariate-search loops, simulation studies that sweep theta starts), ferx_model_section() and ferx_model_set_section() read and replace one block at a time:
ferx_model_section(ex$model, "individual_parameters")# [individual_parameters]
CL = TVCL * exp(ETA_CL)
V = TVV * exp(ETA_V)
KA = TVKA * exp(ETA_KA)
# Copy the bundled model somewhere we can edit it
tmp <- tempfile(fileext = ".ferx")
ferx_model_edit(ex$model, dest = dirname(tmp),
save_as = basename(tmp), .editor = identity)
# Replace one section
new_ip <- c(
" CL = TVCL * (WT / 70)^0.75 * exp(ETA_CL)",
" V = TVV * (WT / 70) * exp(ETA_V)",
" KA = TVKA * exp(ETA_KA)"
)
ferx_model_set_section(tmp, "individual_parameters", new_ip)ferx_model_edit() copies a bundled (or any read-only) model to a writable location and opens it. Pass .editor = identity (or any no-op function) when running non-interactively, e.g. in a knitted chapter or a CI pipeline.
Pre-fit sanity checks
ferx_check_init()
Before kicking off a 30-minute SAEM fit, verify that the model and the initial estimates produce a finite, sensible OFV. ferx_check_init() runs a handful of FOCEI iterations and returns a list with $trace (per-iteration OFV, gradient norm, step norm) and $summary (start/end OFV, OFV drop, convergence flag):
ci <- ferx_check_init(ex$model, ex$data)
ci$summary
head(ci$trace)A finite start OFV with a downward trajectory means the model and inits are healthy enough to commit to a real fit. A non-finite start OFV or an explosive gradient norm usually points to a scale mismatch, the wrong CMT numbering, or a parameter bound that excludes the true value.
ferx_inits_from_nca()
For a new drug — or when porting a model from another tool — non-compartmental analysis is a cheap source of starting values. ferx_inits_from_nca() runs an NCA on the dataset and returns theta and omega starting values:
inits <- ferx_inits_from_nca(ex$model, ex$data, method = "nca")
inits$theta TVCL TVV TVKA
0.1218031 8.5045356 0.5008098
inits$omega ETA_CL ETA_V ETA_KA
ETA_CL 0.01620114 0.00 0.0
ETA_V 0.00000000 0.02 0.0
ETA_KA 0.00000000 0.00 0.4
The same routine is invoked from ferx_fit() via the inits_from_nca argument:
| Value | Behaviour |
|---|---|
FALSE (default) |
Use initial values from the model file |
TRUE / "nca_sweep"
|
Try both file inits and NCA inits; keep the lower-OFV fit |
"nca" |
Replace file inits with NCA-derived values |
"nca_ebe" |
Use NCA inits, then refine with a quick EBE pass |
Pipe-style fitting with ferx_model()
ferx_model(data, model) bundles a data path and a model path into a single object so the whole workflow can be expressed as a native pipe chain. The data path flows in as the first argument, which makes |> natural:
ex$data |>
ferx_model(ex$model) |>
ferx_fit(method = "focei", covariance = TRUE) |>
summary()To peek at a section mid-chain, use ferx_get_section() — it returns the requested block and passes the ferx_model through invisibly:
ex$data |>
ferx_model(ex$model) |>
ferx_get_section("parameters") |> # prints [parameters] block; chain continues
ferx_fit()You can also construct the object first, then reuse it:
m <- ferx_model(ex$data, ex$model)
ferx_check_init(m)
fit <- ferx_fit(m, method = "foce")ferx_fit() reads $data from the ferx_model object automatically; supply data = explicitly to override it.
Argument order: the first argument to ferx_model() is data, the second is model — this is the reverse of the old ferx_model("pk.ferx") positional style. Old-style calls are still detected and handled, but the data-first form is strongly preferred for new code.
Putting it together
A typical first-pass workflow on a new dataset:
# 1. Scaffold a model from a template
path <- "my_model.ferx"
ferx_model_new(path, template = "1cpt_oral")
# 2. Edit (manually or programmatically), then validate and inspect
m <- ferx_model("my_data.csv", path)
ferx_model_validate(m)
ferx_model_inspect(m)
# 3. Sanity-check inits before the real fit
ferx_check_init(m)
# 4. Fit (optionally bootstrap inits from NCA) — pipe style
fit <- "my_data.csv" |>
ferx_model(path) |>
ferx_fit(inits_from_nca = TRUE)What’s next
-
Model file reference — every block of the
.ferxfile - Overview — choosing methods, optimizers, and multi-start