Overview

ferx supports three parameterisation styles for individual parameters:

Style DSL pattern Use case
Log-normal P = TVP * exp(ETA_P) Most PK parameters (CL, V, KA) — positive by construction
Logit-normal P = inv_logit(logit(THETA_P) + ETA_P) Fractions/probabilities (F, bioavailability) — bounded (0, 1)
Additive P = TVP + ETA_P Parameters with natural additive variability (lag time, threshold)

The optimiser always works on the unbounded or transformed scale; ferx back-transforms to the natural scale when reporting results.


Log-normal ETAs (default)

The standard warfarin model uses log-normal ETAs for CL, V, and KA. This is the default: each individual parameter equals the typical value multiplied by exp(ETA), which keeps the parameter positive and gives a log-normal distribution across subjects.

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

After fitting, print() reports omega as [log-normal] with a CV%:

fit <- ferx_fit(ex$model, ex$data)
fit
# Omega (between-subject variability):
#   ETA_CL  [log-normal]  var = 0.0423   CV% = 21.1
#   ETA_V   [log-normal]  var = 0.0198   CV% = 14.2
#   ETA_KA  [log-normal]  var = 0.157    CV% = 40.9

The CV% is derived from the log-scale variance as CV% = 100 * sqrt(exp(omega_var) - 1). For small variances this is approximately 100 * sqrt(omega_var), but the exact formula is always used.

ferx_estimates() returns the omega variance on the log scale in estimate, with transform = "variance" (omega rows always use this label regardless of the ETA parameterisation style — the log-normal interpretation is conveyed by print.ferx_fit(), not by the tidy table). SE and 95 % CI columns are on the same log-variance scale:

ferx_estimates(fit)
#   param              transform  estimate      se  rse_pct  lower_95  upper_95
#   OMEGA(ETA_CL)      variance     0.0423  0.0121     28.6    0.0186    0.0660
#   OMEGA(ETA_V)       variance     0.0198  0.0089     44.9    0.0024    0.0372
#   OMEGA(ETA_KA)      variance     0.1570  0.0480     30.6    0.0629    0.2511
# estimate_natural / lower_95_natural / upper_95_natural are NA for omega rows
# (back-transform is not defined for variance parameters)

Choosing initial omega values

A common question is: “what omega value corresponds to 30% BSV?”

cv_targets <- c(10, 20, 30, 40, 50, 60, 80, 100)
data.frame(
  cv_pct    = cv_targets,
  omega_approx = round((cv_targets / 100)^2, 4),
  omega_exact  = round(log(1 + (cv_targets / 100)^2), 4)
)
#>   cv_pct omega_approx omega_exact
#> 1     10         0.01      0.0100
#> 2     20         0.04      0.0392
#> 3     30         0.09      0.0862
#> 4     40         0.16      0.1484
#> 5     50         0.25      0.2231
#> 6     60         0.36      0.3075
#> 7     80         0.64      0.4947
#> 8    100         1.00      0.6931

Use the exact formula (log(1 + CV²)) for omega variances above 30% — the approximate formula CV²/100² underestimates the required starting value and can slow convergence.

Quick rule: for CV% ≤ 30%, omega ≈ (CV/100)² is fine. For CV% > 30%, use omega = log(1 + (CV/100)²).


Logit-normal ETA (bioavailability)

When a parameter is a fraction bounded between 0 and 1, use inv_logit / logit to ensure the individual values stay in (0, 1):

F = inv_logit(logit(THETA_F) + ETA_F)

THETA_F is specified on the natural (0, 1) scale in [parameters]; ferx maps it to the logit scale internally so the optimiser works on an unbounded space. The omega for ETA_F is the variance on the logit scale.

ex_logit <- ferx_example("warfarin_logit_f")
ferx_model_inspect(ex_logit$model)
#> Model structure (warfarin_logit_f.ferx)
#>   Structural:  1-cpt oral  (TVCL, TVV, TVKA, THETA_F)
#>   IIV:         ETA_CL, ETA_V, ETA_F
#>   IOV:         none
#>   Residual:    proportional

After fitting, print() shows:

  • THETA_F with [logit scale] and a 95 % CI back-transformed through inv_logit so the CI is on the (0, 1) scale.
  • ETA_F with [logit], SD_logit, and a ±1 SD interval on the (0, 1) scale.
fit_logit <- ferx_fit(ex_logit$model, ex_logit$data)
fit_logit

ferx_estimates() populates estimate_natural, lower_95_natural, and upper_95_natural for logit-transformed thetas. The CI is computed symmetrically on the logit scale and then back-transformed through inv_logit, giving an asymmetric interval on the (0, 1) scale:

est <- ferx_estimates(fit_logit)
est[est$param == "THETA_F", ]
#   param    transform  estimate    se  rse_pct  lower_95  upper_95  estimate_natural  lower_95_natural  upper_95_natural
#   THETA_F  logit        0.847  0.143     16.9     0.567     1.127             0.700             0.638             0.755
# estimate         — logit scale (what the optimiser works with)
# estimate_natural — inv_logit(estimate) = probability on (0, 1) scale
# lower/upper_95_natural — asymmetric CI back-transformed to (0, 1)

The omega for ETA_F has transform = "variance" (variance on the logit scale, symmetric CI — same label as all omega rows):

est[est$param == "OMEGA(ETA_F)", ]
#   param         transform  estimate     se  rse_pct
#   OMEGA(ETA_F)  variance     0.0631  0.028     44.4

Additive ETA (lag time)

When the BSV on a parameter has an additive rather than multiplicative structure — for example, a lag time where the subject-level deviation is in hours — use a plain sum:

TLAG = TVTLAG + ETA_TLAG

ETA_TLAG follows a normal distribution with mean 0 and variance given by the corresponding omega element. The individual lag times are therefore normally distributed around TVTLAG.

ex_add <- ferx_example("warfarin_additive_eta")
ferx_model_inspect(ex_add$model)
#> Model structure (warfarin_additive_eta.ferx)
#>   Structural:  1-cpt oral  (TVCL, TVV, TVKA, TVTLAG)
#>   IIV:         ETA_CL, ETA_V, ETA_TLAG
#>   IOV:         none
#>   Residual:    proportional

After fitting, the ETA_TLAG row in the omega block is printed with [additive] and an SD in the original units (hours):

fit_add <- ferx_fit(ex_add$model, ex_add$data)
fit_add

ferx_estimates() returns transform = "variance" for that omega element (all omega rows use this label). The estimate is the variance in the original units (hours²), and the CI is symmetric — no back-transform is applied:

ferx_estimates(fit_add)
#   param              transform  estimate     se  rse_pct  lower_95  upper_95
#   OMEGA(ETA_TLAG)    variance     0.312   0.098     31.4     0.120     0.504
# SD in original units = sqrt(0.312) ≈ 0.56 h
# estimate_natural / lower_95_natural / upper_95_natural are NA (not applicable)

Mixing transforms in one model

All three styles can appear in the same model. The standard two-compartment oral model with a logit bioavailability and an additive lag time would declare:

[individual_parameters]
  CL   = TVCL  * exp(ETA_CL)        # log-normal
  V1   = TVV1  * exp(ETA_V1)        # log-normal
  KA   = TVKA  * exp(ETA_KA)        # log-normal
  F    = inv_logit(logit(THETA_F) + ETA_F)   # logit-normal
  TLAG = TVTLAG + ETA_TLAG          # additive

[structural_model]
  pk two_cpt_oral(cl=CL, v1=V1, q=Q, v2=V2, ka=KA, f=F, lagtime=TLAG)

Each ETA is handled independently; the omega matrix contains the corresponding variances on the respective scales (log, logit, or natural units).

Running the mixed-transform model

Using the bundled two-compartment with covariate example as a proxy for a multi-transform model (it uses log-normal ETAs on all parameters), you can verify the pattern compiles and fits:

ex2 <- ferx_example("two_cpt_oral_cov")
ferx_model_inspect(ex2$model)
fit2 <- ferx_fit(ex2$model, ex2$data, method = "gn", covariance = FALSE)
fit2$theta

A model combining logit and additive ETAs follows exactly the same path — ferx_fit() detects each ETA’s parameterisation from the [individual_parameters] expressions and applies the correct transform automatically.


Summary table

Transform Omega interpretation Print label ferx_estimates() columns
Log-normal variance on log scale [log-normal] + CV% transform = "variance", symmetric CI on log-var scale
Logit-normal variance on logit scale [logit] + SD_logit + ±1 SD interval transform = "variance"; logit theta gets estimate_natural etc.
Additive variance in original units [additive] + SD transform = "variance", symmetric CI in original units²