Skip to contents

Simulation with covariates or input parameters

Sometimes your NONMEM model can have covariates that you may wish to simulate from; this simulation exercise shows a few methods to simulate with the covariates from NONMEM.

Step 0: input the model

In this case, we will use the Friberg myelosuppresion model originally contributed as an example by Yuan Xiong.

With the simulated data in nlmixr2, babelmixr2, and some manual edits to simplify the model we run NONMEM 7.4.3.

Note in this case there are some PK parameters that are in the model that require some special handling to simulate with uncertainty or even with different dosing scenarios.

For any simulation scenario, we need to import the NONMEM model:

# Since this is an included example, we import the model from the
# `nonmem2rx` package.  This is done by the `system.file()` command:
wbcModel <- system.file("wbc/wbc.lst", package="nonmem2rx")

wbc <- nonmem2rx(wbcModel)
#>  getting information from  '/home/runner/work/_temp/Library/nonmem2rx/wbc/wbc.lst'
#>  reading in xml file
#>  done
#>  reading in phi file
#>  done
#>  reading in lst file
#>  abbreviated list parsing
#>  done
#>  done
#>  splitting control stream by records
#>  done
#>  Processing record $INPUT
#>  Processing record $MODEL
#>  Processing record $THETA
#>  Processing record $OMEGA
#>  Processing record $SIGMA
#>  Processing record $PROBLEM
#>  Processing record $DATA
#>  Processing record $SUBROUTINES
#>  Processing record $PK
#>  Processing record $DES
#>  Processing record $ERROR
#>  Processing record $ESTIMATION
#>  Ignore record $ESTIMATION
#>  Processing record $COVARIANCE
#>  Ignore record $COVARIANCE
#>  Processing record $TABLE
#>  change initial estimate of `theta1` to `1.83169895537931`
#>  change initial estimate of `theta2` to `8.37329670479077`
#>  change initial estimate of `theta3` to `6.37739634773425`
#>  change initial estimate of `theta4` to `-11.558011558`
#>  change initial estimate of `theta5` to `0.464650000001741`
#>  change initial estimate of `eta1` to `0.0979049999946534`
#>  change initial estimate of `eta2` to `2.99999999999372e-06`
#>  change initial estimate of `eta3` to `1.99999999999944e-05`
#>  read in nonmem input data (for model validation): /home/runner/work/_temp/Library/nonmem2rx/wbc/wbc.csv
#>  ignoring lines that begin with a letter (IGNORE=@)'
#>  applying names specified by $INPUT
#>  done
#> using C compiler: ‘gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
#>  read in nonmem IPRED data (for model validation): /home/runner/work/_temp/Library/nonmem2rx/wbc/wbc.pred
#>  done
#>  read in nonmem ETA data (for model validation): /home/runner/work/_temp/Library/nonmem2rx/wbc/wbc.eta
#>  done
#>  changing most variables to lower case
#>  done
#>  replace theta names
#>  done
#>  replace eta names
#>  done
#>  renaming compartments
#>  done
#> using C compiler: ‘gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
#>  solving ipred problem
#>  done
#>  solving pred problem
#>  done

print(wbc)
#>  ── rxode2-based free-form 7-cmt ODE model ────────────────────────────────────── 
#>  ── Initalization: ──  
#> Fixed Effects ($theta): 
#>  log_CIRC0    log_MTT  log_SLOPU  log_GAMMA   prop.err 
#>   1.831699   8.373297   6.377396 -11.558012   0.464650 
#> 
#> Omega ($omega): 
#>           eta.CIRC0 eta.MTT eta.SLOPU
#> eta.CIRC0  0.097905   0e+00     0e+00
#> eta.MTT    0.000000   3e-06     0e+00
#> eta.SLOPU  0.000000   0e+00     2e-05
#> 
#> States ($state or $stateDf): 
#>   Compartment Number Compartment Name
#> 1                  1            CENTR
#> 2                  2           PERIPH
#> 3                  3             PROL
#> 4                  4              TR1
#> 5                  5              TR2
#> 6                  6              TR3
#> 7                  7           c.CIRC
#>  ── Model (Normalized Syntax): ── 
#> function() {
#>     description <- "wbc"
#>     dfObs <- 176
#>     dfSub <- 45
#>     sigma <- lotri({
#>         eps1 ~ 1
#>     })
#>     thetaMat <- lotri({
#>         log_CIRC0 + log_MTT + log_SLOPU + log_GAMMA + prop.err + 
#>             eps1 + eta.CIRC0 + omega.2.1 + eta.MTT + omega.3.1 + 
#>             omega.3.2 + eta.SLOPU ~ c(0.00339803, -0.00171728, 
#>             0.00224653, -0.00142939, 0.00545118, 0.0311551, 0.0107932, 
#>             -0.0333153, -0.18915, 3.18077, 8.44599e-05, -0.000185993, 
#>             -0.00110008, 0.0241601, 0.000189655, 0, 0, 0, 0, 
#>             0, 0, -0.00103234, 0.00125366, 0.00108067, 0.000599458, 
#>             3.39316e-05, 0, 0.000977024, 0, 0, 0, 0, 0, 0, 0, 
#>             0, 5.9718e-08, -5.65051e-08, 6.21392e-09, 8.42426e-07, 
#>             6.68526e-09, 0, -5.00488e-08, 0, 7.54658e-12, 0, 
#>             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#>             0, 0, 0, 3.70943e-07, -1.8945e-07, 1.25175e-06, -1.7346e-06, 
#>             1.59707e-09, 0, -3.13414e-07, 0, 5.308e-11, 0, 0, 
#>             4.20267e-10)
#>     })
#>     validation <- c("IPRED relative difference compared to Nonmem IPRED: 0%; 95% percentile: (0%,0%); rtol=7.82e-11", 
#>         "IPRED absolute difference compared to Nonmem IPRED: 95% percentile: (4.96e-11, 1.28e-06); atol=5.24e-10", 
#>         "PRED relative difference compared to Nonmem PRED: 0%; 95% percentile: (0%,0%); rtol=6.72e-11", 
#>         "PRED absolute difference compared to Nonmem PRED: 95% percentile: (1.4e-11,4.89e-05) atol=6.72e-11")
#>     ini({
#>         log_CIRC0 <- 1.83169895537931
#>         label("1 - log_CIRC0")
#>         log_MTT <- 8.37329670479077
#>         label("2 - log_MTT")
#>         log_SLOPU <- 6.37739634773425
#>         label("3 - log_SLOPU")
#>         log_GAMMA <- -11.558011558
#>         label("4 - log_GAMMA")
#>         prop.err <- c(0, 0.464650000001741)
#>         label("5 - prop.err")
#>         eta.CIRC0 ~ 0.0979049999946534
#>         eta.MTT ~ 2.99999999999372e-06
#>         eta.SLOPU ~ 1.99999999999944e-05
#>     })
#>     model({
#>         cmt(CENTR)
#>         cmt(PERIPH)
#>         cmt(PROL)
#>         cmt(TR1)
#>         cmt(TR2)
#>         cmt(TR3)
#>         cmt(c.CIRC)
#>         mu_1 <- log_CIRC0
#>         mu_2 <- log_MTT
#>         mu_3 <- log_SLOPU
#>         circ0 <- exp(mu_1 + eta.CIRC0)
#>         mtt <- exp(mu_2 + eta.MTT)
#>         slopu <- exp(mu_3 + eta.SLOPU)
#>         gamma <- exp(log_GAMMA)
#>         rxini.rxddta3. <- circ0
#>         PROL(0) <- rxini.rxddta3.
#>         rxini.rxddta4. <- circ0
#>         TR1(0) <- rxini.rxddta4.
#>         rxini.rxddta5. <- circ0
#>         TR2(0) <- rxini.rxddta5.
#>         rxini.rxddta6. <- circ0
#>         TR3(0) <- rxini.rxddta6.
#>         rxini.rxddta7. <- circ0
#>         c.CIRC(0) <- rxini.rxddta7.
#>         cl <- CLI
#>         v1 <- V1I
#>         v2 <- V2I
#>         RXR1 <- 204
#>         conc <- CENTR/v1
#>         NN <- 3
#>         ktr <- (NN + 1)/mtt
#>         edrug <- 1 - slopu * conc
#>         fdbk <- (circ0/c.CIRC)^gamma
#>         circ <- c.CIRC
#>         d/dt(CENTR) <- PERIPH * RXR1/v2 - CENTR * (cl/v1 + RXR1/v1)
#>         d/dt(PERIPH) <- CENTR * RXR1/v1 - PERIPH * RXR1/v2
#>         d/dt(PROL) <- ktr * PROL * edrug * fdbk - ktr * PROL
#>         d/dt(TR1) <- ktr * PROL - ktr * TR1
#>         d/dt(TR2) <- ktr * TR1 - ktr * TR2
#>         d/dt(TR3) <- ktr * TR2 - ktr * TR3
#>         d/dt(c.CIRC) <- ktr * TR3 - ktr * c.CIRC
#>         f <- CENTR
#>         ipred <- c.CIRC
#>         w <- sqrt((ipred * prop.err)^2)
#>         if (w == 0) 
#>             w <- 1
#>         y <- ipred + w * eps1
#>     })
#> }
#>  ── nonmem2rx translation notes ($notes): ──  
#>    • some NONMEM input has tied times; they are offset by a small offset 
#>    • $MODEL NCOMPARTMENTS/NEQUILIBRIUM/NPARAMETERS statement(s) ignored 
#>  ── nonmem2rx extra properties: ──  
#> 
#> Sigma ($sigma): 
#>      eps1
#> eps1    1
#> 
#> other properties include: $nonmemData, $etaData
#> captured NONMEM table outputs: $predData, $ipredData
#> NONMEM/rxode2 comparison data: $iwresCompare, $predCompare, $ipredCompare
#> NONMEM/rxode2 composite comparison: $predAtol, $predRtol, $ipredAtol, $ipredRtol, $iwresAtol, $iwresRtol

# note the NONMEM vs rxode2 models validate well. You can see this in
# the validation code:
message(paste(wbc$meta$validation, collapse="\n"))
#> IPRED relative difference compared to Nonmem IPRED: 0%; 95% percentile: (0%,0%); rtol=7.82e-11
#> IPRED absolute difference compared to Nonmem IPRED: 95% percentile: (4.96e-11, 1.28e-06); atol=5.24e-10
#> PRED relative difference compared to Nonmem PRED: 0%; 95% percentile: (0%,0%); rtol=6.72e-11
#> PRED absolute difference compared to Nonmem PRED: 95% percentile: (1.4e-11,4.89e-05) atol=6.72e-11

Option #1: simulate with the same conditions as the input model

The easiest way to simulate with uncertainty is to use the original NONMEM input dataset. If we want to simulate covariates from here, we simply add resample=TRUE:

sim <- rxSolve(wbc, resample=TRUE, nStud=500)
#>  using nocb interpolation like NONMEM, specify directly to change
#>  using addlKeepsCov=TRUE like NONMEM, specify directly to change
#>  using addlDropSs=TRUE like NONMEM, specify directly to change
#>  using ssAtDoseTime=TRUE like NONMEM, specify directly to change
#>  using safeZero=FALSE since NONMEM does not use protection by default
#>  using ss2cancelAllPending=FALSE since NONMEM does not cancel pending doses with SS=2
#>  using dfSub=45 from NONMEM
#>  using dfObs=176 from NONMEM
#>  using thetaMat from NONMEM
#>  using sigma from NONMEM
#>  using NONMEM's data for solving
#>  using NONMEM specified atol=1e-12
#>  using NONMEM specified rtol=1e-06
#>  using NONMEM specified ssAtol=1e-12
#>  thetaMat has too many items, ignored: 'omega.2.1', 'omega.3.1', 'omega.3.2'
#> [====|====|====|====|====|====|====|====|====|====] 0:00:05
#> Warning: corrected 'thetaMat' to be a symmetric, positive definite matrix

In this case every individual re-samples from the original dataset’s covariates. In this particular case, the dosing changes per individual and it may not be what you wish to share with the team but may be a way to see how the model is performing relative to the data. Binning may be necessary, as with a typical VPC

Option 2: simulate with a different condition (with resampled PK parameters/covariates)

In this case, you may wish to simulate a study that has similar covariates as the NONMEM model in general (and also with resampling)

First lets simulate 410 every 20 days. We can easily add this by creating a event table with the same input PK parameters as the NONMEM dataset.

# first create the base event table with the nubmer of individuals
# matching the NONMEM dataset:
ev <- et(amt=410, ii=20*24, until=365*24) %>% # Add dosing 20 days apart for a year
  et(seq(0, 365*24, by=7*24)) %>% # Assume weekly observations
  et(id=seq_along(unique(wbc$nonmemData$ID))) %>% # Match the number of subjects modeled
  as.data.frame # convert to data.frame

# Now create the PK covariates
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:data.table':
#> 
#>     between, first, last
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
pkCov <- wbc$nonmemData %>%
  filter(!duplicated(ID)) %>% # only get one observation per id
  select(CLI, V1I, V2I) # select the covariates

pkCov$id <- seq_along(pkCov$CLI) # add the covariates per id

# Then merge the PK covariates to the original event table
ev <- merge(pkCov, ev)

# Last simulate with replacement with the new data frame

sim <- rxSolve(wbc, ev, resample=TRUE, nStud=100)
#>  using nocb interpolation like NONMEM, specify directly to change
#>  using addlKeepsCov=TRUE like NONMEM, specify directly to change
#>  using addlDropSs=TRUE like NONMEM, specify directly to change
#>  using ssAtDoseTime=TRUE like NONMEM, specify directly to change
#>  using safeZero=FALSE since NONMEM does not use protection by default
#>  using ss2cancelAllPending=FALSE since NONMEM does not cancel pending doses with SS=2
#>  using dfSub=45 from NONMEM
#>  using dfObs=176 from NONMEM
#>  using thetaMat from NONMEM
#>  using sigma from NONMEM
#>  using NONMEM specified atol=1e-12
#>  using NONMEM specified rtol=1e-06
#>  using NONMEM specified ssAtol=1e-12
#>  thetaMat has too many items, ignored: 'omega.2.1', 'omega.3.1', 'omega.3.2'
#> Warning: corrected 'thetaMat' to be a symmetric, positive definite matrix

ci <- confint(sim, "y")
#> summarizing data...
#> done

plot(ci)

This may be closer a constant theoretical dosing regimen you may wish to explore.

Option 3: simulate a larger study with a different condition (resampled PK parameters/covariates)

Another option is to create a larger dataset (that is a multiple of the original dataset). In this case, I will assume that the new study will have 225 patients, which is a 5 fold increase in subjects compared to the NONMEM input.

# first create the base event table with the nubmer of individuals
# matching the NONMEM dataset:
ev <- et(amt=410, ii=20*24, until=365*24) %>% # Add dosing 20 days apart for a year
  et(seq(0, 365*24, by=7*24)) %>% # Assume weekly observations
  et(id=seq(1, max(wbc$nonmemData$ID)*5)) %>% # Match the number of subjects modeled
  as.data.frame # convert to data.frame

# Now create the PK covariates
library(dplyr)

pkCov <- wbc$nonmemData %>%
  filter(!duplicated(ID)) %>% # only get one observation per id
  select(CLI, V1I, V2I) # select the covariates

# expand the covariates by 5

pkCov <- do.call("rbind",
                 lapply(1:5, function(i) {
                   pkCov
                 }))


pkCov$id <- seq_along(pkCov$CLI) # add the covariates per id

# Then merge the PK covariates to the original event table
ev <- merge(pkCov, ev)

# Last simulate with replacement with the new data frame

sim <- rxSolve(wbc, ev, resample=TRUE, nStud=100)
#>  using nocb interpolation like NONMEM, specify directly to change
#>  using addlKeepsCov=TRUE like NONMEM, specify directly to change
#>  using addlDropSs=TRUE like NONMEM, specify directly to change
#>  using ssAtDoseTime=TRUE like NONMEM, specify directly to change
#>  using safeZero=FALSE since NONMEM does not use protection by default
#>  using ss2cancelAllPending=FALSE since NONMEM does not cancel pending doses with SS=2
#>  using dfSub=45 from NONMEM
#>  using dfObs=176 from NONMEM
#>  using thetaMat from NONMEM
#>  using sigma from NONMEM
#>  using NONMEM specified atol=1e-12
#>  using NONMEM specified rtol=1e-06
#>  using NONMEM specified ssAtol=1e-12
#>  thetaMat has too many items, ignored: 'omega.2.1', 'omega.3.1', 'omega.3.2'
#> [====|====|====|====|====|====|====|====|====|====] 0:01:35
#> Warning: corrected 'thetaMat' to be a symmetric, positive definite matrix

ci <- confint(sim, "y")
#> summarizing data...
#> done

plot(ci)

Other options

You can also simulation without uncertainty and use covariates by resampling by hand (or even simulating new covariates manually).

I believe that reampling keeps any hidden correlations between covariates, and should be used whenever possible. At the time of this writing, resampling can only occur when the new event table is a multiple of the input dataest. Eventually a feature may be added to resample from an input dataset directly.

Note that resampling will also work with time-varying covariates. The time-varying covariates would be imputed based on the input times per subject.