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:

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.

Note

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