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
TIMEandtime - 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_Fexactly (the logit and inv_logit cancel). ETA_Fshifts each individual’s F on the logit scale, symmetrically around the typical value.- The estimated
THETA_Fin the output is directly interpretable as the typical bioavailability. omega ETA_Fis 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.