Example: Deep Compartment Model (DCM)

A two-compartment oral model where a small neural network replaces the analytical covariate model. The network reads subject-level covariates (WT, CRCL) and outputs a multiplicative modulator for each PK parameter — the baseline thetas carry the absolute scale, the NN learns the covariate-driven deviation on top.

This is the baseline × NN-modulator architecture from Janssen et al. 2022 (CPT:PSP, DOI 10.1002/psp4.12808).

Note

DCM requires the nn cargo feature. The default ferx-r build includes it. If fit$neural_networks is empty after fitting, rebuild with --features nn.

Model file

[parameters]
  theta TVCL(4.0,  0.1,  100.0)
  theta TVV1(40.0, 1.0,  500.0)
  theta TVQ (8.0,  0.1,  100.0)
  theta TVV2(80.0, 1.0,  500.0)
  theta TVKA(1.0,  0.01, 10.0)

  omega ETA_CL ~ 0.15
  omega ETA_V1 ~ 0.15
  omega ETA_Q  ~ 0.08
  omega ETA_V2 ~ 0.08
  omega ETA_KA ~ 0.20

  sigma PROP_ERR ~ 0.04 (sd)

[covariate_nn TYPICAL_PK]
  inputs     = [WT, CRCL]
  outputs    = [CL, V1, Q, V2, KA]
  hidden     = [8, 8]
  activation = tanh
  output_activation = softplus

[individual_parameters]
  CL = TVCL * TYPICAL_PK.CL * exp(ETA_CL)
  V1 = TVV1 * TYPICAL_PK.V1 * exp(ETA_V1)
  Q  = TVQ  * TYPICAL_PK.Q  * exp(ETA_Q)
  V2 = TVV2 * TYPICAL_PK.V2 * exp(ETA_V2)
  KA = TVKA * TYPICAL_PK.KA * exp(ETA_KA)

[structural_model]
  pk two_cpt_oral(cl=CL, v1=V1, q=Q, v2=V2, ka=KA)

[error_model]
  DV ~ proportional(PROP_ERR)

[fit_options]
  method    = focei
  maxiter   = 200
  optimizer = lbfgs

The [covariate_nn] block

The [covariate_nn NAME] block declares a feed-forward MLP:

Key Description
inputs Covariate column names from the dataset
outputs Names used in [individual_parameters] as NAME.output
hidden List of hidden-layer widths
activation Hidden activation (tanh, relu, softplus)
output_activation Output activation — softplus keeps outputs positive, matching the expected PK parameter scale

At initialisation (Glorot weights), softplus(0) ≈ 0.69 and all outputs are near 1.0, so the baseline thetas dominate and the NN starts as a near-identity modulator.

Fitting

library(ferx)
ex  <- ferx_example("warfarin_dcm")
fit <- ferx_fit(ex$model, ex$data,
                settings = list(optimizer = "lbfgs"))
print(fit)
Tip

Use optimizer = "lbfgs" (or "bobyqa") for NN-bearing models. The default SLSQP optimizer has a known issue with mu-referenced NN models — it silently no-ops at iteration 1. This is tracked in ferx-core.

Inspecting the neural network

fit$neural_networks is a list of one element per [covariate_nn] block:

nn <- fit$neural_networks[[1]]
nn$name               # "TYPICAL_PK"
nn$shape              # layer widths including input/output
nn$n_weights          # total trainable parameters (~141 for [2,8,8,5])
nn$input_names        # ["WT", "CRCL"]
nn$output_names       # ["CL", "V1", "Q", "V2", "KA"]
nn$weights_offset     # index into the flat theta vector where weights start

When does DCM help?

DCM is worth the extra parameters (~141 weights for [2, 8, 8, 5]) only when covariates are genuinely predictive and the analytical covariate model cannot capture the relationship.

The bundled script fits a DCM and a no-covariate two-compartment baseline on the same dataset and compares AIC and OFV, with covariate-importance heuristics and individual profile plots:

source(system.file("examples/ex_warfarin_dcm.R", package = "ferx"))

Reference

Janssen A. et al. (2022). Deep compartment models: A deep learning approach for the reliable prediction of time-series data in pharmacokinetic modeling. CPT Pharmacometrics Syst Pharmacol 11:934–945. DOI 10.1002/psp4.12808.