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)
Note

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.10.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.

Note

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

Tip

[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.


See also