Output Files
Each model run produces two output files (plus a third, {model}-covtab.csv, when the model declares a [covariates] block). The human-readable fit estimates default to YAML; pass --output-format json or both to additionally emit a complete, machine-readable JSON result for programmatic or agentic consumers.
Quick reference: where to find what
Different quantities live in different outputs — this table is the fastest way to find what you need.
| What you want | Where it lives | Shape | When present |
|---|---|---|---|
Covariates in sdtab (via [output]) |
{model}-sdtab.csv |
one row per observation | declared in [output] |
| Raw covariate values for all dataset records | {model}-covtab.csv |
one row per dataset record (doses + obs) | model has [covariates] block |
| ETA / EBE values per subject | ebes.csv inside .fitrx |
one row per subject | always |
[derived] computed columns |
{model}-sdtab.csv |
one row per observation | model has [derived] block |
[output] declared columns (covariates, individual PK parameters) |
{model}-sdtab.csv |
one row per observation | declared in [output] |
Key distinctions:
[output]covariates in the sdtab use LOCF — each observation row carries the covariate value that was active at that time. The covtab carries the covariate exactly as read from each row of the input dataset, including dose and event rows where sdtab has no entry.- ETA / EBE values are not written to sdtab. They live in
fit$ebe_etas(andebes.csvin the.fitrxbundle) — one row per subject. ETAs are available as context variables in[derived]expressions (e.g.KE = CL / Vcan referenceETA_CL), but are not columns in the sdtab itself.
sdtab CSV ({model}-sdtab.csv)
A CSV file with per-observation diagnostics, one row per observation per subject.
Columns — sdtab
| Column | Description |
|---|---|
ID |
Subject identifier |
TIME |
Observation time |
DV |
Observed value |
CENS |
Censoring flag (0, 1, or -1); omitted when no censored observations |
OCC |
Occasion label; omitted when model has no IOV block |
CMT |
Observation compartment; omitted for single-compartment models |
PRED |
Population prediction (eta = 0) |
IPRED |
Individual prediction (eta = EBE) |
CWRES |
Conditional weighted residual |
IWRES |
Individual weighted residual |
EBE_OFV |
Each subject’s contribution to the total OFV |
N_OBS |
Number of observations for the subject |
TAFD |
Time after first dose |
TAD |
Time after most recent dose (SS-aware) |
[derived] names |
One column per expression in the [derived] block |
[output] names |
Covariates and individual parameters declared in [output] |
Individual-parameter columns
Any name assigned in [individual_parameters] may be declared in [output], and each one reports its own value, re-evaluated on every row with that row’s EBE η, that row’s covariates, and that row’s TIME. That includes names the [structural_model] line never binds — an intermediate such as TVCL = THCL * 3, and a modeled-dose duration/rate D{n} / R{n}. The same values back the [derived] block and the ferx-r individual_estimates table.
Before ferx-core 0.4.0 an unbound name on an analytical (pk ...) model reported CL’s value instead of its own, because the column was read from the parameter’s PK slot and such a name has no slot of its own. Under the usual CL = TVCL * exp(ETA_CL) the two coincide at η = 0, so the column often looked plausible. ODE and compartment-free models were never affected. See #1356.
Residual Definitions
IWRES (Individual Weighted Residual): \[ \text{IWRES}_j = \frac{y_j - \text{IPRED}_j}{\sqrt{V_j}} \] where \(V_j\) is the residual variance evaluated at the individual prediction.
CWRES (Conditional Weighted Residual): \[ \text{CWRES}_j = \frac{y_j - f_{0,j}}{\sqrt{\tilde{R}_{jj}}} \] where \(f_0 = f(\hat{\eta}) - H\hat{\eta}\) is the linearized population prediction and \(\tilde{R} = H\Omega H^T + R\) is the conditional variance.
Example — sdtab
ID,TIME,DV,PRED,IPRED,CWRES,IWRES,EBE_OFV,N_OBS,TAFD,TAD
1,0.5,9.49,10.12,9.55,-0.23,-0.06,2.14,8,0.5,0.5
1,1.0,14.42,14.87,14.35,0.18,0.05,2.14,8,1.0,1.0
covtab CSV ({model}-covtab.csv)
Written only when the model declares a [covariates] block. Unlike sdtab (observation rows only), it echoes the declared covariate columns with one row per input dataset record, including dose and other-event rows. Missing values are written as empty cells. It is also available programmatically as FitResult::covariate_table.
Columns — covtab
| Column | Description |
|---|---|
ID |
Subject identifier |
TIME |
Record time |
EVID |
Event ID of the source row (0=obs, 1=dose, 2=other, 3=reset, 4=reset+dose) |
| declared covariates | One column per covariate in the [covariates] block, in declaration order |
Example — covtab
ID,TIME,EVID,WT,CRCL
1,0.000000,1,70.600000,73.700000
1,0.500000,0,70.600000,73.700000
1,1.000000,0,70.600000,73.700000
Fit JSON ({model}-fit.json)
The YAML above is a curated, human-oriented view of a fit. For programmatic consumers — a script, the R wrapper, or an agentic model-development loop that reads diagnostics and decides the next step — pass --output-format to get the complete FitResult as JSON:
ferx warfarin.ferx --data warfarin.csv --output-format json # {model}-fit.json only
ferx warfarin.ferx --data warfarin.csv --output-format both # both YAML and JSON--output-format yaml is the default and leaves current behaviour unchanged. The flag controls only the estimates file; the sdtab/covtab/conddist CSVs are written regardless.
Unlike the YAML, the JSON is not curated: every field of the fit — all estimates, standard errors, the covariance matrix, condition number, shrinkage, per-subject records (eta, ipred, residuals, …), warnings, and provenance (model/data hashes, ferx version, environment) — is present.
Schema guarantees (so consumers can pin against it):
- A top-level
schema_versioninteger. It is bumped only on a breaking change (renamed/removed field, changed nesting); additive fields do not bump it. - Matrices (
omega,covariance_matrix, …) serialize as{ "rows": r, "cols": c, "data": [..] }withdatain row-major order. Vectors (a subject’seta, …) serialize as flat JSON arrays. - Enums serialize as their variant name (e.g.
"method": "FoceI"); structured warnings carry{severity, category, message, source_method}plus an optionaldetailspayload, wherecategoryis a stable typedWarningCodetoken. - Non-finite floats (
NaN,±Inf) serialize as JSONnull, matching the empty-cell convention of the YAML/CSV writers.
{
"schema_version": 1,
"converged": true,
"method": "FoceI",
"ofv": -280.36,
"theta": [0.133, 7.73, 0.725],
"theta_names": ["TVCL", "TVV", "TVKA"],
"omega": { "rows": 3, "cols": 3, "data": [0.0286, 0.0, 0.0, 0.0, 0.0096, 0.0, 0.0, 0.0, 0.349] },
"cov_condition_number": 2.64,
"shrinkage_eta": [0.0002, 0.0011, 0.0019],
"subjects": [ { "id": "1", "eta": [0.028, 0.066, 0.482], "ipred": [/* … */] } ]
}Library callers get the identical payload in-process, without touching the filesystem, via FitResult::to_json_value() -> serde_json::Value.
Fit YAML ({model}-fit.yaml)
A YAML file containing parameter estimates, standard errors, and model diagnostics.
Structure
model:
converged: true
method: FOCE
covariance_method: r
objective_function:
ofv: -280.1838
aic: -266.1838
bic: -247.2804
data:
n_subjects: 10
n_observations: 110
n_parameters: 7
theta:
TVCL:
estimate: 0.132735
se: 0.014549
rse_pct: 11.0
TVV:
estimate: 7.694842
se: 0.293028
rse_pct: 3.8
omega:
omega_11:
variance: 0.028584
cv_pct: 16.9
se: 0.006394
omega_22:
variance: 0.009613
cv_pct: 9.8
se: 0.002165
sigma:
sigma_1:
estimate: 0.010638
se: 0.000788
covariance_matrix:
# optimizer parameterization: theta log-transformed when lower bound >= 0
# (identity otherwise), sigma log-transformed, omega/kappa Cholesky-factored
parameters: [TVCL, TVV, log_chol_ETA_CL, log_chol_ETA_V, sigma_1]
rows:
TVCL: [1.234567e-4, 2.345678e-5, 0.000000e+0, 0.000000e+0, 0.000000e+0]
TVV: [2.345678e-5, 1.456789e-3, 0.000000e+0, 0.000000e+0, 0.000000e+0]
log_chol_ETA_CL: [0.000000e+0, 0.000000e+0, 5.678901e-5, 0.000000e+0, 0.000000e+0]
log_chol_ETA_V: [0.000000e+0, 0.000000e+0, 0.000000e+0, 2.345678e-6, 0.000000e+0]
sigma_1: [0.000000e+0, 0.000000e+0, 0.000000e+0, 0.000000e+0, 7.890123e-7]
estimation:
focei_wall_time_secs: 11.845678
covariance_wall_time_secs: 0.500000
wall_time_secs: 12.345678
n_threads_used: 8
environment:
os: linux
arch: x86_64
in_docker: true
username: "ron"
ferx_version: 0.2.0The covariance_matrix: block is only present when the covariance step ran successfully or was regularised. The values are in optimizer space — thetas and sigma are on the log scale (or identity scale when the lower bound is negative), omega and kappa are Cholesky-factored. The parameters: list gives the canonical column order; rows: keys match that order. Omega and kappa diagonal entries appear as log_chol_<eta> — the packed value is log(L_ii) where omega = L Lᵀ (log of the Cholesky diagonal, not the variance). Off-diagonal Cholesky entries appear as chol_<eta_row>_<eta_col> (L_ij, not log-transformed). See Packed Parameter Space for the full layout and bounds.
Key Fields
- converged: whether the run reached a solution of the problem posed. It is never
trueat an objective that is not a usable number —NaN, infinite, or the clamped divergence sentinel all reportconverged: falsewith aW_NONFINITE_OBJECTIVEwarning saying which. See A non-finite objective. - ofv: Objective Function Value (-2 log-likelihood)
- aic: Akaike Information Criterion (OFV + 2p)
- bic: Bayesian Information Criterion (OFV + p * ln(n), with n the number of observations). The Delattre-style variants a model search ranks on (
mixed,iiv,random) are computed from the same result byferx_core::bic()— see Ranking candidates. - se: Standard error from the covariance step — see Which covariance estimator produced these numbers before comparing one against another tool
- rse_pct: Relative standard error as percentage (SE/estimate * 100)
- cv_pct: Coefficient of variation for omega (sqrt(variance) * 100)
- covariance_method: which estimator produced the standard errors —
r,sorrsr. Present only when a covariance matrix was produced; see below.
The estimation: block replaces the standalone {model}-timing.txt file (removed in #704): wall_time_secs measures only the estimation step itself (not model parsing or data reading — the old timing file’s elapsed_seconds covered the whole CLI run, so the two are not a like-for-like replacement, just the closest equivalent), and n_threads_used is the Rayon worker count used. wall_time_secs is further broken down (#713) into a {method}_wall_time_secs entry for each stage of method = .../methods = [...] (e.g. focei_wall_time_secs, imp_wall_time_secs for a [focei, imp] chain) plus covariance_wall_time_secs for the post-estimation FD-Hessian / SIR-fallback step, which only ever runs once, on the last estimating stage (#615). The per-stage keys measure convergence only and exclude the covariance step. The environment: block is a snapshot of the machine and account the fit ran under — os/arch from the running binary’s target, in_docker (whether /.dockerenv, KUBERNETES_SERVICE_HOST, or /proc/1/cgroup indicate a container), username ($USER/%USERNAME%, quoted/escaped since it’s free-form text), and ferx_version.
FitResult Fields (Rust API / console)
The following fields are populated on the FitResult struct returned by fit() and printed by print_results(). Except for wall_time_secs, method_wall_times_secs, covariance_wall_time_secs, n_threads_used, and environment (written to the estimation:/environment: blocks above), they are not currently written to the fit YAML — read them programmatically or from the console summary.
Shrinkage
Two shrinkage metrics are reported after every fit:
ETA shrinkage (per random effect — shrinkage_eta: Vec<f64>): \[
\text{shrinkage}_k = 1 - \frac{\sqrt{\frac{1}{n}\sum_i \hat{\eta}_{k,i}^2}}{\sqrt{\omega_{kk}}}
\]
A value near 1 means individual EBEs are all pulled toward zero — the data are not informative about that random effect. A value near 0 means the ETAs are spread consistent with the prior omega.
EPS shrinkage (scalar — shrinkage_eps: f64): \[
\text{shrinkage}_\varepsilon = 1 - \sqrt{\frac{1}{n}\sum_j \text{IWRES}_j^2}
\]
Both formulas use the uncentered second moment with n divisor, matching the NONMEM / PsN / Monolix convention — the population model assumes E[η]=0 and E[IWRES²]=1, so the natural estimator is √(Σx²/n) rather than the unbiased sample SD (which centers on the sample mean and divides by n-1). The unbiased form would inflate SD by √(n/(n-1)) and routinely produce spurious negative shrinkage on small samples.
Negative shrinkage_eps is mathematically possible and meaningful: it indicates mean(IWRES²) > 1, i.e. the residual error model does not absorb the residuals seen at the final EBE etas. Common causes include SAEM converging to a local optimum with under-fit sigma (often resolved by polishing with method = [saem, focei] or trying a different start), model misspecification for a subset of subjects (check the IWRES distribution in the sdtab for outliers), or sigma at a bound. When shrinkage_eps < -5%, ferx emits a warning to FitResult.warnings; the raw value is retained for parity with NONMEM/PsN.
Values of NaN indicate a zero-variance omega component (ETA) or fewer than two valid residuals (EPS).
Kappa shrinkage (IOV models only — shrinkage_kappa: Vec<f64> and shrinkage_kappa_by_occ: Vec<Vec<f64>>):
When the model contains kappa or block_kappa declarations, two additional shrinkage metrics are computed using the same uncentered-moment convention.
Pooled — one value per kappa parameter j, averaged over all N_\text{pairs} (subject, occasion) pairs: \[
\text{shrinkage}_{\kappa,j} = 1 - \frac{\sqrt{\frac{1}{N_{\text{pairs}}}\sum_i \sum_{q} \hat{\kappa}_{iqj}^2}}{\sqrt{\omega_{\text{iov},jj}}}
\] where q indexes occasions and N_\text{pairs} = \sum_i K_i (total subject-occasion pairs; equals N_\text{subj} \cdot K only for balanced designs).
Per-occasion slot — the same formula restricted to occasion slot occ_idx, stored in shrinkage_kappa_by_occ[occ_idx][kappa_idx]. Only reported when two or more occasions are present. Useful for identifying sparse occasions (high shrinkage in one slot suggests that occasion has little information on kappa).
Note on unbalanced designs:
occ_idxis the 0-based position within each subject’s own occasion list (order of first appearance in that subject’s rows), not the rawOCCcolumn value. When subjects have differentOCCsequences (e.g., a late-entry subject whose data begins at OCC 2), a given slot may pool kappas from different occasions across subjects. In that case, use the pooledshrinkage_kappaand interpret per-slot values with caution.
Both metrics are NaN when omega_iov diagonal is zero or fewer than two subject-occasion observations are available for that slot.
IWRES Autocorrelation
Two pooled autocorrelation diagnostics are reported after every fit:
iwres_lag1_r — pooled lag-1 Pearson correlation of IWRES across subjects. Values near 0 indicate no serial dependence; values approaching ±1 indicate strong autocorrelation.
dw_statistic — pooled Durbin-Watson statistic: \[
\text{DW} = \frac{\sum_i \sum_t (e_{i,t} - e_{i,t-1})^2}{\sum_i \sum_t e_{i,t}^2}
\]
| DW range | Interpretation |
|---|---|
| ≈ 2.0 | No autocorrelation |
| < 1.5 | Positive autocorrelation — structural model likely missing dynamics |
| > 2.5 | Negative autocorrelation — possible over-parameterization or misspecified error model |
Subjects with fewer than 2 finite IWRES values are excluded from both statistics. Both fields are NaN when no subject qualifies.
When dw_statistic < 1.5, ferx emits a warning suggesting a transit absorption model, additional compartment, or IOV on ka/F. When dw_statistic > 2.5, the warning suggests over-parameterization or a misspecified error model.
The warning used to add “SDE process noise may also help” on ODE models. It no longer does: the Kalman filter behind [diffusion] propagates the state covariance but never corrects the state mean with the observed data, so it inflates the observation variance instead of supplying the dynamics the statistic is reporting — see what the filter does not do.
Covariance Status
covariance_status: CovarianceStatus takes one of three values:
| Value | Meaning |
|---|---|
Computed |
Covariance step succeeded; SE values are valid |
Failed |
Hessian was singular or inversion failed; SE fields are None |
NotRequested |
covariance = false was set; SE fields are None |
Which covariance estimator produced these numbers
covariance_method: Option<CovarianceMethod> records which of the three estimators produced covariance_matrix, the se_* fields, cov_eigenvalues and cov_condition_number:
| Value | Written as | Matrix | NONMEM equivalent |
|---|---|---|---|
Hessian |
r |
R⁻¹ |
$COVARIANCE MATRIX=R |
CrossProduct |
s |
S⁻¹ |
$COVARIANCE MATRIX=S |
Sandwich |
rsr |
R⁻¹SR⁻¹ |
$COVARIANCE MATRIX=RSR (NONMEM’s default) |
None of those numbers is comparable across estimators. One fit measured a condition number of 1.42e8 under the sandwich and 3.68e5 under s — both correct for their estimator. Comparing a ferx condition number against NONMEM without matching the estimator compares two different quantities, and ferx’s default (r) is not NONMEM’s default (RSR). The same caution applies to the gradient mode: the same data under gradient = auto and gradient = fd measured 3.68e5 and 5.68e4, so quote gradient_method_outer / gradient_method_inner alongside the estimator.
The value is Some exactly when covariance_matrix is Some. A step that was not requested, failed, or fell back to SIR produces no matrix, no standard errors and no condition number, so there is nothing for an estimator name to describe and the field is None (and the YAML key is omitted, never null).
It is the estimator that ran, not the one requested. Above 100 free parameters a defaulted covariance_method = r is routed onto the cross-product, because the R matrix would need n(n+1)/2 re-converged objective evaluations; the substitution is reported as a warning and this field reports s. Reading covariance_method back off [fit_options] would report r for standard errors that came out of S⁻¹.
Where it appears:
- the console summary — next to the
SEcolumn heading, on theCovariance:line, and on theCondition number:line; {model}-fit.yaml—model.covariance_method, plus anestimatorkey inside thecovariance_matrix:block for tools that consume the raw matrix;{model}-fit.jsonand the.fitrxbundle — as the samer/s/rsrtoken, so a recorded value can be pasted straight back into[fit_options];- the
condition_numberandcovariance_regularizedwarningdetailspayloads — see Details payloads. Notcovariance_failed: that code means the step produced no matrix, so by the rule above there is no estimator to name and the key is omitted.
A .fitrx bundle written before this field existed loads with covariance_method: None: the file does not record it, and a default would be a fabricated provenance.
Run Record Fields
| Field | Description |
|---|---|
model_name |
Name from the .ferx file (or "Unnamed") |
ferx_version |
Version of ferx-core that produced the result |
wall_time_secs |
Wall-clock time for the complete fit (seconds) |
method_wall_times_secs |
Wall-clock convergence time per stage, parallel to method_chain (seconds); excludes the covariance step |
covariance_wall_time_secs |
Wall-clock time spent on the post-estimation covariance step; 0.0 when not run |
gradient_method_inner |
Gradient method used in the inner (EBE) loop, e.g. analytic (Dual2) or finite differences |
gradient_method_outer |
Gradient method used in the outer loop, e.g. finite differences |
uses_ode_solver |
true if the model uses the ODE solver, false for analytical PK |
n_threads_used |
Number of Rayon threads used during estimation |
nlopt_missing_algorithms |
NLopt algorithms that were requested but unavailable in this build (empty when all available) |
covariance_n_evals_estimated |
Estimated number of OFV evaluations the covariance step will run, populated only when run_covariance_step = true and n_parameters > 30 |
environment |
EnvironmentInfo — OS, CPU architecture, whether running in a container, and OS username |
Environment Info
FitResult.environment: EnvironmentInfo is detected once per fit:
| Field | Description |
|---|---|
os |
Target OS family (std::env::consts::OS), e.g. "linux", "macos", "windows" |
arch |
Target CPU architecture (std::env::consts::ARCH), e.g. "x86_64", "aarch64" |
in_docker |
true when /.dockerenv exists, KUBERNETES_SERVICE_HOST is set, or /proc/1/cgroup names a container runtime |
username |
OS username the fit ran under ($USER/%USERNAME%), "unknown" if neither is set |
Container detection is best-effort: a cgroup-v2 host with a cgroup namespace (common under containerd/CRI-O) hides the pod’s own cgroup path, which is why the Kubernetes env var check exists as a fallback alongside the /.dockerenv marker and cgroup substring match.
.fitrx bundles saved before this field existed load with an "unknown" placeholder rather than the loading machine’s environment.
EBE Convergence Diagnostics
Counters from the inner-loop (EBE) optimizer, useful for diagnosing problematic fits. Always 0 for SAEM (which uses MH sampling rather than EBE optimization).
| Field | Description |
|---|---|
ebe_convergence_warnings |
Number of outer iterations in which at least one subject had an unconverged EBE |
max_unconverged_subjects |
Worst-case unconverged-subject count seen in a single outer iteration |
total_ebe_fallbacks |
Total number of times the Nelder-Mead fallback was invoked across all subjects and outer iterations |
Optimizer Trace CSV
When optimizer_trace = true is set in [fit_options], a CSV is written to /tmp/ferx_trace_<pid>_<ts>.csv. The path is also stored in FitResult::trace_path.
Each row is one outer iteration. See the fit-options trace table for the full column reference.
Example use in R (with the ferx package):
fit <- ferx_fit("model.ferx", "data.csv", optimizer_trace = TRUE)
trace <- read.csv(fit$trace_path)
plot(trace$iter, trace$ofv, type = "l", xlab = "Iteration", ylab = "OFV")