Derived columns and output
The optional [derived] and [output] blocks extend the sdtab with extra columns computed after the fit. Use them to add NCA-style summaries, PD metrics, unit-converted readouts, or any expression the optimiser does not need to see.
[derived] — computed columns
Each line in [derived] has the form name = expression. The name becomes a new sdtab column; the expression is evaluated for every observation row.
[derived]
KE = CL / V
T_HALF = 0.6931472 / KE
CMAX = max(IPRED)
AUC_24 = integral(IPRED, from=0, to=24)
Available variables
| Name | Value |
|---|---|
Individual-parameter names (CL, V, …) |
EBE-derived value for the subject |
Theta names (TVCL, TVV, …) |
Final population estimate |
Eta names (ETA_CL, …) |
Subject EBE |
Covariate names (WT, AGE, …) |
Subject covariate (constant per row) |
IPRED |
Individual prediction at the row’s time |
DV |
Observed value |
TAFD |
Time after first dose |
TAD |
Time after most recent dose |
TIME |
Nominal time |
MACHEPS |
Machine epsilon (≈ 2.22 × 10⁻¹⁶) |
Operators and functions
Standard arithmetic (+, -, *, /), comparison (<, >, <=, >=, ==, !=), logical (&&, ||, !), mod for modulo, ^ for power.
| Function | Description |
|---|---|
exp(x), log(x), sqrt(x), abs(x) |
Standard math |
floor(x), ceil(x), round(x) |
Rounding |
max(expr) |
Subject-level maximum |
min(expr) |
Subject-level minimum |
tmax(expr) |
Time at which expr is maximum |
integral(expr, from=t0, to=t1) |
Trapezoidal AUC |
integral(expr, from=t0, to=t1, step=dt) |
Grid-based AUC (IPRED only) |
integral(expr, window=W, anchor=A, step=dt) |
Periodic AUC |
integral(1.0, condition, window=W, anchor=A, step=dt) |
Time-above-threshold |
Computation kinds
Per-row (default)
The expression is evaluated independently at each observation row.
KE = CL / V
T_HALF = 0.6931472 / KE
DAY = floor(TAFD / 24) + 1
KE and T_HALF are constant per subject (they depend only on EBE parameters), but they still appear on every row. DAY varies across rows.
Aggregate
max(expr), min(expr), or tmax(expr) produce a single value per subject broadcast to all rows. An optional filter is the second argument:
CMAX = max(IPRED)
TMAX = tmax(IPRED)
CTROUGH = min(IPRED, TAD < 1e-10) # trough at dose time
CMAX_D1 = max(IPRED, TAFD < 24) # day-1 peak
CMAX_D14 = max(IPRED, TAFD >= 312 && TAFD < 336)
1e-10 for troughs. TAD == 0 is mathematically true at dose rows, but floating-point residuals from modular arithmetic can produce TAD = 1e-13 instead of 0. The guard TAD < 1e-10 is the standard idiom to reliably capture the trough without false positives.
If no row passes the filter the result is NaN.
Integral
integral(expr, from=t0, to=t1) computes the area under expr over [t0, t1) using the trapezoidal rule at observation times.
Add step=dt to force evaluation on a uniform grid — useful when the observation design is sparse or when computing time-above-threshold:
AUC_24 = integral(IPRED, from=0, to=24)
AUC_GRID = integral(IPRED, from=0, to=24, step=0.5)
AUC_DV_24 = integral(DV, from=0, to=24) # observation times only
window=W, anchor=A yields a periodic AUC: one value per W-unit window, starting at A:
AUC_TAU = integral(IPRED, window=24, anchor=0, step=0.1)
Time-above-threshold. Use integral(1.0, condition, ...) to measure hours above a threshold. Always include step= here — without it, only observation time points are evaluated and sparse sampling can miss brief threshold crossings:
TAM_TAU = integral(1.0, IPRED > MEC, window=24, anchor=0, step=0.1)
AUC calculations — recipe guide
All four common NCA-style AUC forms are supported. Choose by what you want to integrate and over which time window.
Fixed window — AUC from t₀ to t₁
Trapezoidal rule at observation time points. Fast and exact when sampling is dense:
[derived]
AUC_24 = integral(IPRED, from=0, to=24)
AUC_72 = integral(IPRED, from=0, to=72)
AUC_INF = integral(IPRED, from=0, to=168)
Fixed window with fine grid — for sparse or uneven sampling
Forces a uniform evaluation grid so the trapezoid covers gaps between observation times. Use step in the same time units as TIME:
[derived]
AUC_72_grid = integral(IPRED, from=0, to=72, step=0.5)
A smaller step is more accurate but slower at render time. step=0.1–0.5 is usually sufficient for PK profiles.
Periodic AUC — one value per dosing interval
window=W, anchor=A aligns the integration windows to the dosing schedule. Each observation row gets the AUC for its current W-unit window:
[derived]
AUC_TAU = integral(IPRED, window=24, anchor=0, step=0.1)
The window starting at anchor + n * window for integer n is chosen for each observation based on its TIME. For q24h dosing starting at TIME=0, anchor=0, window=24 gives one AUC value per day.
NCA AUC from observed values
integral(DV, ...) uses observed DV values at the actual observation times — no IPRED interpolation. The step= argument is ignored for DV integrals:
[derived]
AUC_DV_72 = integral(DV, from=0, to=72)
Time-above-threshold (TAT)
integral(1.0, condition, ...) counts time units where the condition is true. Always add step= — without it only observation time points are evaluated, and sparse sampling misses crossings between samples:
[derived]
TAT_0_5 = integral(1.0, IPRED > 0.5, from=0, to=72, step=0.1)
TAT_MEC = integral(1.0, IPRED > MEC, window=24, anchor=0, step=0.1)
Partial AUC — subset of subjects or time points
Combine a fixed window with an aggregate filter to scope the integral:
[derived]
AUC_D1 = integral(IPRED, from=0, to=24)
AUC_SS = integral(IPRED, from=312, to=336) # day 14 at steady state
ODE models
[derived] works identically whether the structural model is an analytical PK shortcut or an ODE system. The same expression language, the same time variables, and the same integral forms are available in both cases.
TIME, TAFD, TAD in ODE-based fits
All three time variables have the same steady-state-aware semantics in both [odes] and [derived]:
| Variable | Resets at | Typical use |
|---|---|---|
TIME |
Never | Absolute-clock conditions (TIME < 24 for day-1 only) |
TAFD |
First dose | Day-number, study-elapsed conditions |
TAD |
Every dose | Trough detection, within-interval AUC/TAT |
All four names TIME, T, TAFD, and TAD are injected into every ODE RHS evaluation at each solver step — not just at observation times.
AUC and time-above-threshold as ODE accumulator states
Inside [odes] you can add extra states whose derivative accumulates AUC, time above threshold, or any other running integral. Use an inline if-expression to restrict when the integrand fires:
[structural_model]
ode(obs_cmt=central, states=[depot, central, AUC_D1, TAM_INT])
[odes]
d/dt(depot) = -KA * depot
d/dt(central) = KA * depot - CL / V * central
# Accumulate day-1 AUC: integrand is central/V only while TIME < 24
d/dt(AUC_D1) = if (TIME < 24) central / V else 0.0
# Accumulate time above 0.5 mg/L within the current dosing interval
# TAD resets at each dose, so this gives a per-interval running total
d/dt(TAM_INT) = if (central / V > 0.5 && TAD < 24) 1.0 else 0.0
if syntax in [odes] — the inline form is if (condition) value else value. The block form if (cond) { ... } else { ... } is also accepted when multiple states need to branch together. Conditions support &&, ||, !, and all comparison operators.
The ODE solver evaluates the RHS at every adaptive step, so the accumulator is exact (no grid approximation). This makes the ODE approach well-suited for:
- Conditional integration over arbitrary time windows (day 1, last interval, windows defined by TIME, TAFD, or TAD)
- Any accumulated quantity that feeds back into the ODE dynamics — for example, cumulative drug exposure driving tolerance, or receptor occupancy accumulating and reducing the remaining free receptor
Reporting ODE accumulator values
The ODE state vector is not automatically written to sdtab. To report accumulated values in fit$sdtab for post-fit analysis, choose one of:
Option A — [derived] integral(): re-computes the integral from the predicted curve after the fit. Does not require an extra ODE state and appears in sdtab. Works for the vast majority of AUC/TAT use cases:
[derived]
AUC_72 = integral(IPRED, from=0, to=72, step=0.2)
TAT_24 = integral(1.0, IPRED > 0.5, from=0, to=24, step=0.1)
Option B — observe the accumulator directly: set obs_cmt to the accumulator state. The accumulator value at each observation time then becomes IPRED. This is only practical when AUC (not concentration) is the modelled endpoint.
For feedback-driven dynamics where AUC_D1 or TAM_INT feeds into another ODE equation, the accumulator must stay in [odes]; pair it with [derived] integral() if you also want the value in sdtab.
For a full worked example of TIME, TAFD, TAD, and ODE accumulators alongside [derived] see Example: ODE with TIME, TAFD, and TAD.
Sequential scoping
A derived column may reference earlier columns in the same block. Forward references are not allowed.
[derived]
KE = CL / V
T_HALF = 0.6931472 / KE # ok — KE defined above
# CL2 = KE * V # would be fine
# BAD = FUTURE + 1 # FUTURE not defined yet → parse error
Naming rules
A [derived] name must not clash with the mandatory sdtab columns (ID, TIME, DV, IPRED, CWRES, IWRES, ETA1, ETA2, …, TAFD, TAD) or with any theta, eta, or individual-parameter name. Shadowing a covariate name is allowed but emits a warning.
[output] — echo individual parameters and covariates
The [output] block lists extra columns to write to sdtab beyond the mandatory minimum. Each entry is a bare name:
[output]
CL V KA WT
| Can list | Example |
|---|---|
| Covariates | WT, AGE, SEX |
| Individual parameters | CL, V, KA |
Names already in [derived] |
redundant but harmless |
| Eta names | already in mandatory minimum; emits a warning |
Mandatory sdtab minimum (always present)
ID, TIME, DV, CENS, OCC, CMT, PRED, IPRED, CWRES, IWRES, EBE_OFV, N_OBS, TAFD, TAD
[derived] vs [output]. Names in [derived] are automatically in sdtab. Use [output] only for individual parameters and covariates that [derived] does not already compute.
Example — what [output] adds to sdtab
Without [output], individual parameters like CL, V, and KA are used during estimation but do not appear in fit$sdtab. With [output] they are echoed as extra columns:
# Without [output]: sdtab has IPRED, PRED, residuals — no CL/V/KA
[structural_model]
pk one_cpt_oral(cl=CL, v=V, ka=KA)
# With [output]: CL, V, KA are appended to every row in fit$sdtab
[output]
CL V KA
In R:
library(ferx)
ex <- ferx_example("warfarin_derived")
fit <- ferx_fit(ex$model, ex$data)
# CL, V, KA appear because the model has [output] CL V KA
head(unique(fit$sdtab[, c("ID", "CL", "V", "KA")]))Covariates work the same way — add them to [output] to carry them through to sdtab for downstream analysis:
[output]
CL V KA WT AGE
Full example
library(ferx)
ex <- ferx_example("warfarin_derived")
fit <- ferx_fit(ex$model, ex$data)The bundled warfarin_derived model demonstrates all three computation kinds plus [output]:
[derived]
KE = CL / V
T_HALF = 0.6931472 / KE
DAY = floor(TAFD / 24) + 1
CMAX = max(IPRED)
CTROUGH = min(IPRED, TAD < 1e-10)
CMAX_D1 = max(IPRED, TAFD < 24)
AUC_0_72 = integral(IPRED, from=0, to=72)
AUC_TAU = integral(IPRED, window=24, anchor=0, step=0.1)
AUC_DV_72 = integral(DV, from=0, to=72)
[output]
CL V KA
After fitting, fit$sdtab contains all derived columns plus the echoed individual parameters:
names(fit$sdtab)
# Aggregate columns: one value per subject (broadcast across rows)
unique(fit$sdtab[, c("ID", "CMAX", "AUC_0_72")])See also Example: derived columns for a full worked example.