Individual Parameters

Maturity: beta — see Feature Maturity for what this means.

The [individual_parameters] block defines how population parameters (theta), random effects (eta), and covariates combine to produce individual PK parameters.

Syntax

PARAM = expression

Each line assigns a PK parameter using an arithmetic expression that can reference:

  • Theta parameters – names defined in [parameters] (e.g., TVCL, TVV)
  • Eta random effects – names defined as omega parameters (e.g., ETA_CL, ETA_V)
  • Covariates – column names from the data file (e.g., WT, CRCL)
  • Event time – the built-ins TIME and time
  • Constants – numeric literals (e.g., 70, 0.75)

Supported Operators and Functions

Operator/Function Example
+, -, *, / TVCL * WT / 70
^ (power) (WT/70)^0.75
exp() exp(ETA_CL)
log(), ln() log(TVCL)
sqrt() sqrt(WT)
abs() abs(ETA_CL)
Parentheses TVCL * (WT/70)^0.75
Comparisons (in if conditions) WT > 70, SEX == 1, AGE != 0
Logical (in if conditions) && (and), \|\| (or), ! (not)
Inline conditional if (SEX == 1) TVCL * 1.5 else TVCL

Conditional Logic (if / else)

Two forms are supported and may be combined freely.

Block form

[individual_parameters]
  if (WT > 70) {
    CL = TVCL * (WT / 70)^0.75 * exp(ETA_CL)
  } else if (SEX == 1) {
    CL = TVCL * 1.2 * exp(ETA_CL)
  } else {
    CL = TVCL * exp(ETA_CL)
  }
  V = TVV * exp(ETA_V)

The block form is appropriate when the body contains multiple statements or when several alternative branches are needed. Conditions support comparison operators (<, <=, >, >=, ==, !=) and logical operators (&&, ||, !); parentheses group sub-conditions.

Inline (ternary) form

[individual_parameters]
  CL = if (SEX == 1) TVCL * 1.5 else TVCL
  V  = TVV  * exp(ETA_V)

The inline form produces a value and can appear anywhere an expression is allowed. Both then and else branches are required.

Time-dependent parameters

TIME and time are built-ins, not dataset covariates. They evaluate to the current PK event time when ferx evaluates [individual_parameters]: dose records, observations, and EVID=2 parameter-update records each see their own event time. This supports NONMEM-style $PK switches such as:

[individual_parameters]
  if (TIME > 45) {
    CL = TVCL_LATE * exp(ETA_CL)
  } else {
    CL = TVCL * exp(ETA_CL)
  }
  V = TVV * exp(ETA_V)

For analytical models, using TIME/time routes the subject through the same event-driven parameter-switching path used for time-varying covariates. This matches the NONMEM $PK IF (TIME.GT.45) ... event-time convention: $PK is re-evaluated at each event, and the analytical amount is advanced across each inter-event interval with the parameters in effect at the interval’s end event. For reset-stacked data where the displayed sdtab TIME may use the raw per-occasion clock, this built-in follows the internal monotone PK event clock.

Numerical check against NONMEM

A one-compartment IV bolus (dose 100 at t = 0, V = 10) with a CL switch at TIME = 10 from CL_E = 1 to CL_L = 5, observed at t = 5 and t = 20:

[individual_parameters]
  if (TIME > 10) { CL = CL_L } else { CL = CL_E }
  V = V
Obs TIME Active CL advance ferx PRED NONMEM $PK IF(TIME.GT.10)
5 CL_E over [0, 5] 10·e^(−0.5) = 6.065307 6.065307
20 CL_E [0,5] then CL_L [5,20] 10·e^(−8.0) = 0.003355 0.003355

The t = 20 prediction is not 10·e^(−2) ≈ 1.35 (what a frozen TIME = 0 would give, since the switch would never fire) and not 10·e^(−10) (what a single CL_L applied over the whole history would give): the value 10·e^(−8) is the event-driven result NONMEM produces, where CL_L governs only the [5, 20] advance. This equivalence is locked by tests/time_origin.rs::time_builtin_cl_switch_matches_nonmem_event_time_semantics; the same TIME event clock drives ODE right-hand sides (tests/time_origin.rs::ode_time_builtin_*) and [derived] columns (tests/derived_output.rs::derived_time_builtin_echoes_per_observation_time).

Analytic gradient & SE check against NONMEM (#486)

On closed-form (non-IOV) 1-/2-/3-cpt models, a TIME-switched structural parameter gets the exact analytic FOCE/FOCEI gradient (the per-event time is threaded into the same event-driven Dual2/Dual1 sensitivity walk used for time-varying covariates), not finite differences — so the optimiser, its standard errors, and NONMEM’s METHOD=1 INTER agree. Fitting a 60-subject 1-cpt IV bolus simulated with a CL switch at TIME = 45 (IF (TIME.GT.45) CL = TVCL_LATE …):

Parameter ferx (analytic) NONMEM METHOD=1 INTER
OFV −3737.394 −3737.394
TVCL 2.1029 (SE 0.0733) 2.1029 (SE 0.0736)
TVCL_LATE 1.0457 (SE 0.0381) 1.0456 (SE 0.0383)
TVV 53.107 (SE 1.364) 53.108 (SE 1.364)
ω²(CL) 0.07212 (SE 0.01343) 0.07212 (SE 0.01344)
ω²(V) 0.03458 (SE 0.00692) 0.03458 (SE 0.00692)
PROP_ERR 0.10274 (SE 0.00299) 0.10274 (SE 0.00299)

The same dataset fit with an ODE version of the model (ode(states=[central]) with d/dt(central) = -(CL/V)·central and a Form-C [scaling] y = central/V readout) reproduces the fit under its own analytic gradient — OFV −3737.395, TVCL 2.1029, TVCL_LATE 1.0457, TVV 53.107, identical SEs — confirming the ODE route threads the per-event TIME the same way (the ~0.002 OFV difference is ODE-integrator vs NONMEM’s analytic ADVAN1).

Estimates match to 4–5 significant figures and standard errors to ~0.3%. The analytic gradient itself is pinned against central finite differences of the production predictor across every supported route: time_builtin_provider_matches_fd_of_production (closed-form non-IOV, 1-/2-cpt IV switch and 1-cpt oral), iov_time_builtin_provider_matches_fd_of_predict_iov (closed-form IOV, stacked [η_bsv, κ]), and ode_time_builtin_provider_matches_fd_of_production (non-IOV ODE), and ode_iov_time_builtin_provider_matches_fd_of_predict_iov (ODE IOV) — including in combination with an η-dependent ExpressionScale obs_scale (the event-driven walk now applies the subject-static scale quotient post-walk). A direct pk(...=TIME) structural mapping is served analytically too: the parser desugars the mapped slot into a hidden individual parameter (__ferx_pktime_*), so it is exactly the explicit [individual_parameters] form and rides the same per-event walk (time_builtin_direct_pk_mapping_matches_fd_of_production, time_builtin_direct_pk_mapping_equivalent_to_explicit_indiv_param).

Interaction with mu-referencing

When the assignment to a parameter is wrapped in an if block, the (ETA → THETA) relationship is no longer unconditional, so ferx skips mu-reference detection for that parameter. Unconditional assignments in the same block continue to be detected normally.

Tip: if you want mu-referencing for a covariate-adjusted parameter, keep the assignment unconditional and bury the conditional inside the covariate term (e.g. CL = TVCL * (if (WT > 70) (WT / 70)^0.75 else 1.0) * exp(ETA_CL)).

Common Parameterizations

Exponential (log-normal) random effects

The standard approach for PK parameters that must be positive:

[individual_parameters]
  CL = TVCL * exp(ETA_CL)
  V  = TVV  * exp(ETA_V)
  KA = TVKA * exp(ETA_KA)

Allometric scaling with covariates

[individual_parameters]
  CL = TVCL * (WT/70)^0.75 * exp(ETA_CL)
  V  = TVV  * (WT/70)^1.0  * exp(ETA_V)

Estimated covariate effects

Use additional theta parameters for covariate coefficients:

[parameters]
  theta TVCL(0.134, 0.001, 10.0)
  theta THETA_WT(0.75, 0.01, 2.0)
  theta THETA_CRCL(0.5, 0.01, 2.0)

[individual_parameters]
  CL = TVCL * (WT/70)^THETA_WT * (CRCL/100)^THETA_CRCL * exp(ETA_CL)

Logit-normal bioavailability

Use inv_logit(logit(THETA_F) + ETA_F) to constrain bioavailability to (0, 1). The starting value for THETA_F is set directly on the (0, 1) scale — whatever fraction you specify is what the optimiser uses as the typical F:

[parameters]
  theta THETA_F(0.70, 0.001, 0.999)  # typical bioavailability = 70%
  omega ETA_F ~ 0.10                 # BSV on the logit scale

[individual_parameters]
  F = inv_logit(logit(THETA_F) + ETA_F)
  • When ETA_F = 0, F_i = THETA_F exactly (the logit and inv_logit cancel).
  • ETA_F shifts each individual’s F on the logit scale, symmetrically around the typical value.
  • The estimated THETA_F in the output is directly interpretable as the typical bioavailability.
  • omega ETA_F is variance on the logit scale — not the variance of F itself.

The alternative form inv_logit(THETA_F + ETA_F) is also supported, where THETA_F is on the logit scale (e.g., logit(0.70) ≈ 0.847). This is less readable but may be useful when comparing with NONMEM models.

Automatic MU-referencing

When a line matches one of the patterns

PARAM = THETA * exp(ETA)              # multiplicative / log-normal
PARAM = THETA * <anything> * exp(ETA) # multiplicative with covariate terms
PARAM = exp(log(THETA) + ETA)         # canonical MU form
PARAM = THETA + ETA                   # additive

ferx records the (ETA → THETA) mapping and uses it to re-centre the inner-loop ETA search at each outer iteration — reproducing NONMEM / nlmixr2’s MU-referencing behaviour without requiring you to write an explicit MU_i line. See the FAQ for details on which patterns are and are not detected, and for the mu_referencing = false escape hatch.

Covariate Detection

Any uppercase identifier in the expression that does not match a theta name, eta name, or built-in such as TIME is automatically treated as a covariate. The covariate value is read from the corresponding column in the data file.

For example, in CL = TVCL * (WT/70)^0.75 * exp(ETA_CL): - TVCL matches a theta parameter - ETA_CL matches an omega parameter - WT matches neither, so it is treated as a covariate column

PK Parameter Names

The parameter names on the left side of each assignment must map to recognized PK parameter names:

Name PK Parameter
CL Clearance
V or V1 Volume of distribution (central compartment)
Q Intercompartmental clearance
V2 Peripheral volume
KA Absorption rate constant
F Bioavailability (default 1.0 if omitted)
LAGTIME (alias: ALAG) Dose/absorption lagtime (default 0.0 if omitted) — see Lagtime

For ODE models, the parameter names are user-defined and passed as a flat vector to the ODE right-hand side function.