ferx model files (.ferx) are plain text, so you can create and modify them entirely from R without opening an editor. This vignette covers the three functions that support that workflow: ferx_model_new(), ferx_model_section(), and ferx_model_set_section().

Starting from scratch

ferx_model_new() writes a skeleton .ferx file for one of five built-in templates: "1cpt_oral" (default), "1cpt_iv", "2cpt_oral", "2cpt_iv", and "ode".

Pass print = TRUE to preview a template in the console without writing any file — useful when you are exploring or copy-pasting a starting point:

ferx_model_new(print = 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
ferx_model_new(template = "2cpt_iv", print = TRUE)

To write a file, supply a path. By default the file is also opened in your editor (edit = TRUE); pass edit = FALSE to skip that:

ferx_model_new("my_model.ferx", edit = FALSE)

Inspecting a section

ferx_model_section() extracts the body of a named section and prints it. Start with the bundled warfarin example:

ex <- ferx_example("warfarin")
ferx_model_section(ex$model, "parameters")
#> # [parameters]
#>   theta TVCL(0.134, 0.001, 10.0)
#>   theta TVV(8.1, 0.1, 500.0)
#>   theta TVKA(1.0, 0.01, 50.0)
#> 
#>   omega ETA_CL ~ 0.07
#>   omega ETA_V  ~ 0.02
#>   omega ETA_KA ~ 0.40
#> 
#>   sigma PROP_ERR ~ 0.01

The return value is the character vector of lines, returned invisibly, so you can capture it:

params <- ferx_model_section(ex$model, "parameters")
#> # [parameters]
#>   theta TVCL(0.134, 0.001, 10.0)
#>   theta TVV(8.1, 0.1, 500.0)
#>   theta TVKA(1.0, 0.01, 50.0)
#> 
#>   omega ETA_CL ~ 0.07
#>   omega ETA_V  ~ 0.02
#>   omega ETA_KA ~ 0.40
#> 
#>   sigma PROP_ERR ~ 0.01
params
#>  [1] "  theta TVCL(0.134, 0.001, 10.0)" "  theta TVV(8.1, 0.1, 500.0)"    
#>  [3] "  theta TVKA(1.0, 0.01, 50.0)"    ""                                
#>  [5] "  omega ETA_CL ~ 0.07"            "  omega ETA_V  ~ 0.02"           
#>  [7] "  omega ETA_KA ~ 0.40"            ""                                
#>  [9] "  sigma PROP_ERR ~ 0.01"          ""

Scripted read-modify-write

The extract → modify → write-back pattern lets you change a model programmatically without an editor. Here we raise the initial CL estimate in the parameters section and write the result to a temporary copy:

# work in a fresh temp file so re-knitting the vignette never reuses stale state
model_path <- tempfile(fileext = ".ferx")
stopifnot(file.copy(ex$model, model_path))

# read the section
lines <- ferx_model_section(model_path, "parameters")
#> # [parameters]
#>   theta TVCL(0.134, 0.001, 10.0)
#>   theta TVV(8.1, 0.1, 500.0)
#>   theta TVKA(1.0, 0.01, 50.0)
#> 
#>   omega ETA_CL ~ 0.07
#>   omega ETA_V  ~ 0.02
#>   omega ETA_KA ~ 0.40
#> 
#>   sigma PROP_ERR ~ 0.01

# modify: change the TVCL initial value from 0.134 to 0.5
lines <- sub("TVCL\\([^,]+,", "TVCL(0.5,", lines)

# write back
ferx_model_set_section(model_path, "parameters", lines)

# verify
ferx_model_section(model_path, "parameters")
#> # [parameters]
#>   theta TVCL(0.5, 0.001, 10.0)
#>   theta TVV(8.1, 0.1, 500.0)
#>   theta TVKA(1.0, 0.01, 50.0)
#> 
#>   omega ETA_CL ~ 0.07
#>   omega ETA_V  ~ 0.02
#>   omega ETA_KA ~ 0.40
#> 
#>   sigma PROP_ERR ~ 0.01

ferx_model_set_section() replaces only the targeted section; all other sections are left untouched.

Console-only workflow: create, tweak, fit

You can go from nothing to an estimated model without ever opening a text editor. The steps below create a one-compartment oral model file, switch the estimation method to FOCEI, and run ferx_fit():

model_path <- file.path(tempdir(), "run1.ferx")

# 1. Write a skeleton
ferx_model_new(model_path, template = "1cpt_oral", edit = FALSE)

# 2. Switch method to focei
ferx_model_set_section(model_path, "fit_options", c(
  "  method     = focei",
  "  maxiter    = 300",
  "  covariance = true"
))

# 3. Fit (uses the bundled warfarin data for illustration)
ex   <- ferx_example("warfarin")
fit  <- ferx_fit(model_path, ex$data)
print(fit)

Opening a model in an editor

ferx_model_edit() copies a bundled example to a destination directory (or leaves a user-owned file in place) and opens it in your editor. It is the interactive companion to the scripted functions above.

# Copy the bundled warfarin model to tempdir() and open it
my_model <- ferx_model_edit(ex$model, dest = tempdir())

# After editing, save the result under a new name for version control
ferx_model_edit("run1.ferx", save_as = "run2.ferx")

The dest argument controls where the copy lands. If the source file is already outside the installed package directory, ferx_model_edit() edits it in place (no copy) unless dest is set explicitly.

Validating a model file

ferx_model_validate() checks that all required sections are present and reports the result. Use it after creating or editing a model to catch missing sections before running the optimizer:

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

A model missing required sections produces a clear error list:

bad <- tempfile(fileext = ".ferx")
writeLines(c(
  "[parameters]",
  "  theta TVCL(1.0, 0.001, 100.0)",
  "[structural_model]",
  "  pk one_cpt_oral(cl=CL, v=V, ka=KA)"
), bad)
ferx_model_validate(bad)
#> Validating: file2891795ab419.ferx 
#> 
#> Sections present:
#>   parameters                     [ok]
#>   individual_parameters          [MISSING]
#>   structural_model               [ok]
#>   error_model                    [MISSING]
#> 
#> Result: INVALID
#>   * Missing required section: [individual_parameters]
#>   * Missing required section: [error_model]
#>   * Parse error: Missing [error_model] block

Overwrite guards

By default ferx_model_new() refuses to overwrite an existing file:

model_path <- tempfile(fileext = ".ferx")
ferx_model_new(model_path, edit = FALSE)            # creates the file
#> Created /tmp/RtmpR9M3HE/file289173bcd227.ferx

tryCatch(
  ferx_model_new(model_path, edit = FALSE),          # would overwrite — error
  error = function(e) message(conditionMessage(e))
)
#> /tmp/RtmpR9M3HE/file289173bcd227.ferx already exists. Use overwrite = TRUE to replace it.

Opt in explicitly with overwrite = TRUE:

ferx_model_new(model_path, template = "1cpt_iv", overwrite = TRUE, edit = FALSE)
#> Created /tmp/RtmpR9M3HE/file289173bcd227.ferx
ferx_model_section(model_path, "structural_model")
#> # [structural_model]
#>   pk one_cpt_iv_bolus(cl=CL, v=V)