Simulation

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

The optional [simulation] block defines a virtual clinical trial design for generating simulated data. This is useful for model validation and testing.

Syntax

[simulation]
  n_subjects = N
  dose_amt   = AMOUNT
  dose_cmt   = COMPARTMENT
  seed       = SEED
  times      = [t1, t2, t3, ...]
  horizon    = T          # TTE only — administrative censoring time
Key Aliases Description
n_subjects subjects Number of virtual subjects to simulate
dose_amt dose Dose amount administered to each subject
dose_cmt cmt Dosing compartment (1-indexed)
seed Random seed for reproducible simulations
times Continuous observation time points (Gaussian endpoints)
horizon Administrative censoring time for TTE endpoints (finite, > 0)

A block must define at least one of times or horizon, matched to the model’s endpoints:

  • a model with a continuous (residual-error) endpoint requires times;
  • a model with a time-to-event endpoint requires horizon (see TTE simulation below);
  • a joint PK + TTE model requires both.

Supplying only horizon for a model that has a continuous endpoint is an error (it would otherwise simulate zero continuous observations), as is supplying only times for a TTE model.

An unknown or malformed key in [simulation] is a parse error (not silently ignored), so a typo such as n_subject, or a line missing its = such as n_subjects 6, is reported rather than falling back to the default. A horizon that is not finite and strictly positive is likewise rejected.

Example

[simulation]
  n_subjects = 100
  dose_amt   = 100
  dose_cmt   = 1
  seed       = 12345
  times      = [0.5, 1, 2, 4, 8, 12, 24]

This simulates 100 subjects, each receiving a dose of 100 units into compartment 1, with observations at 0.5, 1, 2, 4, 8, 12, and 24 hours.

Usage

Run a simulation-estimation study from the CLI:

ferx model.ferx --simulate

This will: 1. Parse the model and [simulation] block 2. Generate simulated data using the model’s default parameters and random effects 3. Fit the model to the simulated data 4. Report parameter estimates and diagnostics

Simulation Process

  1. For each subject, random effects are sampled from \(\eta_i \sim N(0, \Omega)\). For IOV (kappa) models an independent occasion random effect \(\kappa_{i,o} \sim N(0, \Omega_{\text{IOV}})\) is also drawn for each occasion \(o\) (grouped by the iov_column), so the simulated data reproduces the between-occasion variability the model parameterizes — matching NONMEM $SIM. (The post-fit NPDE/NPD diagnostics are a separate path that still holds kappas at zero; see fit options.)
  2. Individual parameters are computed using the [individual_parameters] equations
  3. Predictions are generated using the structural model
  4. Residual error is added: \(DV = IPRED + \epsilon\), where \(\epsilon \sim N(0, V)\)
  5. Observations below 0.001 are clipped to 0.001

IOV validation against NONMEM $SIM

The occasion-kappa draw is anchored to NONMEM $SIMULATION. On a shared 300-subject × 3-occasion design (1-cpt IV bolus, IOV on V, EVID=4 reset + bolus at the start of each occasion, one observation at t=0 so IPRED = AMT/V exactly), the within-subject between-occasion variance of log(IPRED) is an unbiased estimate of \(\Omega_{\text{IOV}}\):

Source between-occasion Var(log IPRED)
Truth (\(\Omega_{\text{IOV}}\)) 0.04000
NONMEM 7.6.0 $SIMULATION 0.04151
ferx simulate() (df 12000) 0.03939

All three agree within Monte-Carlo error; the previous behaviour (kappa held at zero) collapsed the ferx figure to ~0. A higher-resolution 100-replicate run (df 60000) tightens the ferx estimate to 0.03988 (0.5 SE from truth), ruling out systematic under-dispersion. The 0.03939 figure in the table is pinned by tests/simulate_iov_kappa.rs::simulate_iov_recovers_omega_iov_variance.

A second anchor pins that [scaling] is applied on the occasion-less IOV path — an IOV model simulated on data read without an iov_column, where the occasion kappa collapses to zero. On the same 1-cpt IV design with obs_scale = 1000, \(IPRED = (AMT/V)/1000\), so \(\text{mean}(\log IPRED) = \log(AMT/TVV/1000) = -4.60517\) and \(\text{Var}(\log IPRED) = \omega^2_V = 0.04\) (BSV only — no occasion component):

Source mean(log IPRED) Var(log IPRED)
Analytic (scaling applied) −4.60517 0.04000
NONMEM 7.6.0 $SIMULATION −4.60873 0.03965
ferx simulate() −4.60378 0.04138

ferx and NONMEM agree to ~0.005 on the scaled level. Before the fix, simulate() dropped the scale factor on the empty-occasion path, inflating the mean by \(\log(1000) \approx 6.9\). Pinned by tests/simulate_iov_kappa.rs::simulate_iov_scaling_matches_nonmem_occasionless.

TTE simulation

For a time-to-event model there are no continuous observations to schedule; instead each synthetic subject is followed until an event occurs or until the administrative horizon, at which point an event-free subject is right-censored. --simulate therefore generates, for each subject, one right-censored TTE row per event (cause) compartment, then draws the outcome:

  • a single [event_model] yields one row per subject — an event before the horizon, or right-censoring at the horizon;
  • competing risks (several [event_model] blocks on distinct compartments) yield one row per cause: the earliest latent event is observed and the remaining causes are right-censored at that same time, or — if no cause fires before the horizon — every cause is right-censored at the horizon.
[simulation]
  n_subjects = 200
  horizon    = 14     # follow each subject to t = 14, censoring survivors there
  seed       = 12345

Because a TTE design has no continuous times, the horizon key is required when the model has a TTE endpoint; omitting it is a parse error. See examples/tte_competing_risks.ferx.

Decoupled horizon and VPC

The horizon is an administrative censoring time decoupled from the observed event times. This matters when re-simulating an existing event-bearing dataset for a visual predictive check (VPC): without an explicit horizon a record that already carries an event imposes no censoring window (it would re-draw unbounded), which biases the simulated event-time distribution. Supplying horizon overrides each record’s per-record window so every simulated cause censors at the planned study end. The same control is available on the library API as SimulateOptions { horizon, .. }.

Covariates in Simulation

Covariates can be specified per subject in the [simulation] block. Currently, the simulated population uses default covariate values (covariates are not randomized).