Introduction to Pumas

This is an introduction to Pumas, a software system for pharmaceutical modeling and simulation. Pumas supports NLME (non-linear mixed effects models) estimation and simulation, NCA (non-compartmental analysis), Bioequivalence analysis, optimal design, and more.

In this tutorial we focus on NLME. The basic workflow of NLME with Pumas is:

  1. Build a model.
  2. Define subjects or populations to simulate or estimate.
  3. Analyze the results with post-processing and plots.

We will show how to build a multiple-response PK/PD model via the @model macro, define a subject with single doses, and analyze the results of the simulation. Fitting of data using any of the methods available in Pumas is showcased in the tutorials. This tutorial is designed as a broad overview of the workflow and a more in-depth treatment of each section can be found in the tutorials.

Working Example

Let us start by showing complete simulation code, and then break down how it works.

using Pumas
using PumasUtilities
using CairoMakie
######### Turnover model

inf_2cmt_lin_turnover = @model begin
    @param begin
        tvcl ∈ RealDomain(; lower = 0)
        tvvc ∈ RealDomain(; lower = 0)
        tvq ∈ RealDomain(; lower = 0)
        tvvp ∈ RealDomain(; lower = 0)
        Ω_pk ∈ PDiagDomain(4)
        σ_prop_pk ∈ RealDomain(; lower = 0)
        σ_add_pk ∈ RealDomain(; lower = 0)
        # PD parameters
        tvturn ∈ RealDomain(; lower = 0)
        tvebase ∈ RealDomain(; lower = 0)
        tvec50 ∈ RealDomain(; lower = 0)
        Ω_pd ∈ PDiagDomain(1)
        σ_add_pd ∈ RealDomain(; lower = 0)
    end

    @random begin
        ηpk ~ MvNormal(Ω_pk)
        ηpd ~ MvNormal(Ω_pd)
    end

    @pre begin
        CL = tvcl * exp(ηpk[1])
        Vc = tvvc * exp(ηpk[2])
        Q = tvq * exp(ηpk[3])
        Vp = tvvp * exp(ηpk[4])

        ebase = tvebase * exp(ηpd[1])
        ec50 = tvec50
        emax = 1
        turn = tvturn
        kout = 1 / turn
        kin0 = ebase * kout
    end

    @init begin
        Resp = ebase
    end

    @vars begin
        conc := Central / Vc
        edrug := emax * conc / (ec50 + conc)
        kin := kin0 * (1 - edrug)
    end

    @dynamics begin
        Central' = -(CL / Vc) * Central + (Q / Vp) * Peripheral - (Q / Vc) * Central
        Peripheral' = (Q / Vc) * Central - (Q / Vp) * Peripheral
        Resp' = kin - kout * Resp
    end

    @derived begin
        dv ~ @. Normal(conc, sqrt((conc * σ_prop_pk)^2 + σ_add_pk^2))
        resp ~ @. Normal(Resp, σ_add_pd)
    end
end

turnover_params = (;
    tvcl = 1.5,
    tvvc = 25.0,
    tvq = 5.0,
    tvvp = 150.0,
    tvturn = 10,
    tvebase = 10,
    tvec50 = 0.3,
    Ω_pk = Diagonal([0.05, 0.05, 0.05, 0.05]),
    Ω_pd = Diagonal([0.05]),
    σ_prop_pk = 0.15,
    σ_add_pk = 0.5,
    σ_add_pd = 0.2,
);
regimen = DosageRegimen(150; rate = 10, cmt = :Central)
pop = map(i -> Subject(; id = i, events = regimen), 1:10)
Population
  Subjects: 10
  Observations: 
sd_obstimes = [
    # single dose observation times
    0.25,
    0.5,
    0.75,
    1,
    2,
    4,
    8,
    12,
    16,
    20,
    24,
    36,
    48,
    60,
    71.9,
];
sims = simobs(inf_2cmt_lin_turnover, pop, turnover_params; obstimes = sd_obstimes)
Simulated population (Vector{<:Subject})
  Simulated subjects: 10
  Simulated variables: dv, resp
sim_plot(
    inf_2cmt_lin_turnover,
    sims;
    observations = [:dv],
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "Concentration (ng/mL)"),
)
Example block output
sim_plot(
    inf_2cmt_lin_turnover,
    sims;
    observations = [:resp],
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "Response"),
)
Example block output

In this code, we defined a nonlinear mixed effects model by describing the parameters, the random effects, the dynamical model, and the derived (result) values. Then we generated a population of 10 subjects who received a single dose of 150 mg, specified parameter values, simulated the model, and generated a plot of the results. Now let's walk through this process!

Using the Model Macro

First we define the model. The simplest way to do is via @model. Inside of the @model block we have a few subsections. The first of these is a @param block.

Pumas.@paramMacro

@param

Defines the model's fixed effects and corresponding domains, e.g. tvcl ∈ RealDomain(lower = 0). Must be used in an @model block. For example:

@model begin
  @param begin
    tvcl ∈ RealDomain(lower = 0)
    tvv ∈ RealDomain(lower = 0)
    Ω ∈ PDiagDomain(2)
    σ_prop ∈ RealDomain(lower = 0, init = 0.04)
  end
end

For PumasEMModels, the @param block uses a formula syntax for covariate relationships. A distribution from the normal family is used to indicate domain (currently only Normal, LogNormal, and LogitNormal are supported):

@emmodel begin
  @param begin
    CL ~ 1 + wt | LogNormal
    Vc ~ 1      | Normal
  end
end

this gives CL = exp(log(θ.CL[1]) + θ.CL[2] * wt) and VC = θ.VC, where θ is the parameter named tuple. If we only have 1 in the formula, then it is a scalar. If we have covariates, we have a tuple of parameters for variable.

For this model we define structural model parameters of PK and PD and their corresponding variances where applicable:

@param begin
    tvcl ∈ RealDomain(; lower = 0)
    tvvc ∈ RealDomain(; lower = 0)
    tvq ∈ RealDomain(; lower = 0)
    tvvp ∈ RealDomain(; lower = 0)
    Ω_pk ∈ PDiagDomain(4)
    σ_prop_pk ∈ RealDomain(; lower = 0)
    σ_add_pk ∈ RealDomain(; lower = 0)
    # PD parameters
    tvturn ∈ RealDomain(; lower = 0)
    tvebase ∈ RealDomain(; lower = 0)
    tvec50 ∈ RealDomain(; lower = 0)
    Ω_pd ∈ PDiagDomain(1)
    σ_add_pd ∈ RealDomain(; lower = 0)
end

Next we define our random effects using the @random block.

Pumas.@randomMacro

@random

defines the model's random effects and corresponding distribution, e.g. η ~ MvNormal(Ω). Must be used in an @model block. For example:

@model begin
  @random begin
    η ~ MvNormal(Ω)
  end
end

For PumasEMModels, the @random block is equivalent to the @params block, except that each variable defined here has an additional random effect. For example:

@emmodel begin
  @random begin
    CL ~ 1 + wt | LogNormal
    Vc ~ 1      | Normal
  end
end

this gives CL = exp(log(θ.CL[1]) + θ.CL[2] * wt + η_CL) and VC = θ.VC + η_VC, where θ is the parameter named tuple. If we only have 1 in the formula, then it is a scalar. If we have covariates, we have a tuple of parameters for variable.

The random effects are defined by a distribution from the Distributions package. For more information on defining distributions, please see the Distributions documentation. For this tutorial, we wish to have a multivariate normal of uncorrelated random effects, one for PK and another for PD, so we utilize the syntax:

@random begin
    ηpk ~ MvNormal(Ω_pk)
    ηpd ~ MvNormal(Ω_pd)
end

Now we define our pre-processing step in @pre.

Pumas.@preMacro

@pre

Pre-processes variables for the dynamic system and statistical specification. Must be used in an @model block. For example:


@model begin
  @params begin
    tvcl ∈ RealDomain(lower = 0)
    tvv ∈ RealDomain(lower = 0)
    ωCL ∈ RealDomain(lower = 0)
    ωV ∈ RealDomain(lower = 0)
    σ_prop ∈ RealDomain(lower = 0, init = 0.04)
  end
  @random begin
    ηCL ~ Normal(ωCL)
    ηV ~ Normal(ωV)
  end
  @covariates wt
  @pre begin
    CL = tvcl * (wt/70)^0.75 * exp(ηCL)
    Vc = tvv * (wt/70) * exp(ηV)
    ka = CL/Vc
    Q = Vc
  end
end

We define the values and give them a name as follows:

@pre begin
    CL = tvcl * exp(ηpk[1])
    Vc = tvvc * exp(ηpk[2])
    Q = tvq * exp(ηpk[3])
    Vp = tvvp * exp(ηpk[4])

    ebase = tvebase * exp(ηpd[1])
    ec50 = tvec50
    emax = 1
    turn = tvturn
    kout = 1 / turn
    kin0 = ebase * kout
end

Next we define the @init block.

Pumas.@initMacro

@init

Defines initial conditions of the dynamic system. Must be used in an @model block. For example:

@model begin
  @init begin
    Depot1 = 0.0
    Depot2 = 0.0
    Central = 0.0
  end
end

Any variable not mentioned in this block is assumed to have a zero for its starting value. We wish to only set the starting value for Resp, and thus we use:

@init begin
    Resp = ebase
end

Now we define our dynamics. We do this via the @dynamics block.

Pumas.@dynamicsMacro

@dynamics

Defines the dynamic system. Must be used in an @model block. For example:

@model begin
  @dynamics begin
    Depot1' = -ka*Depot1
    Depot2' = ka*Depot1 - ka*Depot2
    Central' = ka*Depot2 - Q/Vc*Central
  end
end

For our model, we use:

@dynamics begin
    Central' = -(CL / Vc) * Central + (Q / Vp) * Peripheral - (Q / Vc) * Central
    Peripheral' = (Q / Vc) * Central - (Q / Vp) * Peripheral
    Resp' = kin - kout * Resp
end

Next we set up alias variables that can be used later in the code. Such alias code can be setup in the @vars block.

Pumas.@varsMacro

@vars

Define variables usable in other blocks. It is recommended to define them in @pre instead if not a function of dynamic variables. Must be used in an @model block. For example:

@model begin
  @vars begin
    conc = Central/Vc
  end
end

We create aliases for the concentration and response as follows:

@vars begin
    conc := Central / Vc
    edrug := emax * conc / (ec50 + conc)
    kin := kin0 * (1 - edrug)
end

Note that the local assignment := can be used to define intermediate statements that will not be carried outside the block. This means that all the resulting data workflows from this model will not contain the intermediate variables defined with :=.

Lastly we utilize the @derived block.

Pumas.@derivedMacro

@derived

Defines the error model of the dependent variables. Must be used in an @model block. For example:

@model begin
  @derived begin
    dv ~ @. Normal(1000*Central/Vc, 1000*Central/Vc * σ)
  end
end

Our error model is defined as follows:

@derived begin
    dv ~ @. Normal(conc, sqrt((conc * σ_prop_pk)^2 + σ_add_pk^2))
    resp ~ @. Normal(Resp, σ_add_pd)
end

Here dv is the concentration and resp is the response. At any time that these outputs are sampled dv is normally distributed with mean conc and variance (conc * σ_prop_pk)^2 + σ_add_pk^2, and resp has mean Resp and a constant standard deviation σ_add_pd.

Building a Subject

Now let's build a subject to simulate the model with.

Pumas.SubjectType
Subject

The data corresponding to a single subject:

Fields:

  • id: identifier
  • observations: a named tuple of the dependent variables
  • covariates: a named tuple containing the covariates, or nothing.
  • events: a vector of Events.
  • time: a vector of time stamps for the observations

When there are time varying covariates, each covariate is interpolated with a common covariate time support. The interpolated values are then used to build a multi-valued interpolant for the complete time support.

From the multi-valued interpolant, certain discontinuities are flagged in order to use that information for the differential equation solvers and to correctly apply the analytical solution per region as applicable.

Constructor

Subject(;id = "1",
         observations = NamedTuple(),
         events = Event[],
         time = observations isa AbstractDataFrame ? observations.time : nothing,
         event_data = true,
         covariates::Union{Nothing, NamedTuple} = nothing,
         covariates_time = observations isa AbstractDataFrame ? observations.time : nothing,
         covariates_direction = :left)

Subject may be constructed from an <:AbstractDataFrame with the appropriate schema or by providing the arguments directly through separate DataFrames / structures.

Examples:

julia> Subject()
Subject
  ID: 1

julia> Subject(id = 20, events = DosageRegimen(200, ii = 24, addl = 2), covariates = (WT = 14.2, HT = 5.2))
Subject
  ID: 20
  Events: 3
  Covariates: WT, HT

julia> Subject(covariates = (WT = [14.2, 14.7], HT = fill(5.2, 2)), covariates_time = [0, 3])
Subject
  ID: 1
  Covariates: WT, HT

Our model did not make use of covariates, so we will ignore covariate components covariates, covariate_time and covariate_direction for now, and observations are only necessary for fitting parameters to data which will not be covered in this tutorial. Thus, our subject will be defined simply by its dosage regimen.

To do this, we use the DosageRegimen constructor.

Pumas.DosageRegimenType
DosageRegimen

Lazy representation of a series of Events.

Fields

  • data::DataFrame: The tabular representation of a series of Events.

  • Signature

evts = DosageRegimen(amt::Numeric;
                     time::Numeric = 0,
                     cmt::Union{Numeric,Symbol} = 1,
                     evid::Numeric = 1,
                     ii::Numeric = zero.(time),
                     addl::Numeric = 0,
                     rate::Numeric = zero.(amt)./oneunit.(time),
                     duration::Numeric = zero(amt)./oneunit.(time),
                     ss::Numeric = 0,
                     route::NCA.Route)
  • Examples
julia> DosageRegimen(100, ii = 24, addl = 6)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1    100.0     1     24.0      6      0.0       0.0     0  NullRoute

julia> DosageRegimen(50,  ii = 12, addl = 13)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1     50.0     1     12.0     13      0.0       0.0     0  NullRoute

julia> DosageRegimen(200, ii = 24, addl = 2)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1    200.0     1     24.0      2      0.0       0.0     0  NullRoute

julia> DosageRegimen(200, ii = 24, addl = 2, route = NCA.IVBolus)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1    200.0     1     24.0      2      0.0       0.0     0  IVBolus

From various DosageRegimens

  • Signature

evs = DosageRegimen(regimen1::DosageRegimen, regimen2::DosageRegimen; offset = nothing)

offset specifies if regimen2 should start after an offset following the end of the last event in regimen1.

  • Examples
julia> e1 = DosageRegimen(100, ii = 24, addl = 6)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1    100.0     1     24.0      6      0.0       0.0     0  NullRoute

julia> e2 = DosageRegimen(50, ii = 12, addl = 13)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1     50.0     1     12.0     13      0.0       0.0     0  NullRoute

julia> evs = DosageRegimen(e1, e2)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1    100.0     1     24.0      6      0.0       0.0     0  NullRoute
   2 │     0.0      1     50.0     1     12.0     13      0.0       0.0     0  NullRoute

julia> DosageRegimen(e1, e2, offset = 10)
DosageRegimen
 Row │ time     cmt    amt      evid  ii       addl   rate     duration  ss    route
     │ Float64  Int64  Float64  Int8  Float64  Int64  Float64  Float64   Int8  NCA.Route
─────┼───────────────────────────────────────────────────────────────────────────────────
   1 │     0.0      1    100.0     1     24.0      6      0.0       0.0     0  NullRoute
   2 │   178.0      1     50.0     1     12.0     13      0.0       0.0     0  NullRoute

Let's assume the dose is an infusion administered at the rate of 10 mg/hour into the Central compartment:

regimen = DosageRegimen(150; time = 0, rate = 10, cmt = :Central)
1×10 DataFrame
Rowtimecmtamtevidiiaddlratedurationssroute
Float64SymbolFloat64Int8Float64Int64Float64Float64Int8NCA.Route
10.0Central150.010.0010.015.00NullRoute

Let's define our subject to have id=1 and this multiple dosing regimen:

subject = Subject(; id = 1, events = regimen)
Subject
  ID: 1
  Events: 2

You can also create a collection of subjects as a Population.

pop = Population(map(i -> Subject(; id = i, events = regimen), 1:10))
Population
  Subjects: 10
  Observations: 

Running a Simulation

The main function for running a simulation is simobs.

Pumas.simobsFunction
simobs(
  model::AbstractPumasModel,
  population::Union{Subject, Population}
  param,
  randeffs=sample_randeffs(model, param, population);
  obstimes=nothing,
  ensemblealg=EnsembleSerial(),
  diffeq_options=NamedTuple(),
  rng=Random.default_rng(),
)

Simulate random observations from model for population with parameters param at obstimes (by default, use the times of the existing observations for each subject). If no randeffs is provided, then random ones are generated according to the distribution in the model.

Arguments

  • model may either be a PumasModel or a PumasEMModel.

  • population may either be a Population of Subjects or a single Subject.

  • param should be either a single parameter set, in the form of a NamedTuple, or a vector of such parameter sets. If a vector then each of the parameter sets in that vector will be applied in turn. Example: (; tvCL=1., tvKa=2., tvV=1.)

  • randeffs is an optional argument that, if used, should specify the random effects for each subject in population. Such random effects are specified by NamedTuples for PumasModels (e.g. (; tvCL=1., tvKa=2.)) and by Tuples for PumasEMModels (e.g. (1., 2.)). If population is a single Subject (without being enclosed in a vector) then a single such random effect specifier should be passed. If, however, population is a Population of multiple Subjects then randeffs should be a vector containing one such specifier for each Subject. The functions init_randeffs, zero_randeffs, and sample_randeffs are sometimes convenient for generating randeffs:
     randeffs = zero_randeffs(model, param, population)
     solve(model, population, param, randeffs)

If no randeffs is provided, then random ones are generated according to the distribution in the model.

  • obstimes is a keyword argument where you can pass a vector of times at which to simulate observations. The default, nothing, ensures the use of the existing observation times for each Subject.

  • ensemblealg is a keyword argument that allows you to choose between different modes of parallelization. Options include EnsembleSerial(), EnsembleThreads() and EnsembleDistributed().

  • diffeq_options is a keyword argument that takes a NamedTuple of options to pass on to the differential equation solver.

  • rng is a keyword argument where you can specify which random number generator to use.

simobs(model::PumasModel, population::Population; samples::Int = 10, simulate_error = true, rng::AbstractRNG = default_rng())

Simulates model predictions for population using the prior distributions for the population and subject-specific parameters. samples is the number of simulated predictions returned per subject. If simulate_error is false, the mean of the predictive distribution's error model will be returned, otherwise a sample from the predictive distribution's error model will be returned for each subject. rng is the pseudo-random number generator used in the sampling.

simobs(model::PumasModel, subject::Subject; samples::Int = 10, simulate_error = true, rng::AbstractRNG = default_rng())

Simulates model predictions for subject using the prior distributions for the population and subject-specific parameters. samples is the number of simulated predictions returned. If simulate_error is false, the mean of the predictive distribution's error model will be returned, otherwise a sample from the predictive distribution's error model will be returned for each subject. rng is the pseudo-random number generator used in the sampling.

simobs(b::BayesMCMCResults; subject = nothing, samples::Int = 10, simulate_error = false, rng::AbstractRNG = default_rng())

Simulates model predictions using the posterior samples for the population and subject-specific parameters. The predictions are either for the subject with index subject or the whole population if no index is input. samples is the number of simulated predictions returned per subject. If simulate_error is false, the mean of the predictive distribution's error model will be returned, otherwise a sample from the predictive distribution's error model will be returned for each subject. rng is the pseudo-random number generator used in the sampling.

simobs(pmi::FittedPumasModelInference, population::Population = pmi.fpm.data, randeffs::Union{Nothing, AbstractVector{<:NamedTuple}} = nothing; samples::Int, rng = default_rng(), kwargs...,)

Simulates observations from the fitted model using a truncated multivariate normal distribution for the parameter values when a covariance estimate is available and using non-parametric sampling for simulated inference and bootstrapping. When a covariance estimate is available, the optimal parameter values are used as the mean and the variance-covariance estimate is used as the covariance matrix. Rejection sampling is used to avoid parameter values outside the parameter domains. Each sample uses a different parameter value. samples is the number of samples to sample. population is the population of subjects which defaults to the population associated the fitted model. randeffs can be set to a vector of named tuples, one for each sample. If randeffs is not specified (the default behaviour), it will be sampled from its distribution.

simobs(fpm::FittedPumasModel, [population::Population,] vcov::AbstractMatrix, randeffs::Union{Nothing, AbstractVector{<:NamedTuple}} = nothing; samples::Int, rng = default_rng(), kwargs...)

Simulates observations from the fitted model using a truncated multi-variate normal distribution for the parameter values. The optimal parameter values are used for the mean and the user supplied variance-covariance (vcov) is used as the covariance matrix. Rejection sampling is used to avoid parameter values outside the parameter domains. Each sample uses a different parameter value. samples is the number of samples to sample. population is the population of subjects which defaults to the population associated the fitted model so it's optional to pass. randeffs can be set to a vector of named tuples, one for each sample. If randeffs is not specified (the default behaviour), it will be sampled from its distribution.

Let's simulate subject 1 with a set of chosen parameters:

turnover_params = (;
    tvcl = 1.5,
    tvvc = 25.0,
    tvq = 5.0,
    tvvp = 150.0,
    tvturn = 10,
    tvebase = 10,
    tvec50 = 0.3,
    Ω_pk = Diagonal([0.05, 0.05, 0.05, 0.05]),
    Ω_pd = Diagonal([0.05]),
    σ_prop_pk = 0.15,
    σ_add_pk = 0.5,
    σ_add_pd = 0.2,
)

sims = simobs(inf_2cmt_lin_turnover, subject, turnover_params; obstimes = sd_obstimes)
SimulatedObservations
  Simulated variables: dv, resp
  Time: [0.25, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0, 36.0, 48.0, 60.0, 71.9]

We can then plot the simulated observations by using the sim_plot function:

sim_plot(inf_2cmt_lin_turnover, sims; observations = :dv)
Example block output

Note that we can further modify the plot, see Customizing Plots. For example,

sim_plot(
    inf_2cmt_lin_turnover,
    sims,
    observations = :dv,
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "Concentration (ng/mL)"),
)
Example block output

When we only give the parameters, the random effects are automatically sampled from their distributions. If we wish to prescribe a value for the random effects, we pass initial values similarly:

vrandeffs = [(; ηpk = rand(4), ηpd = rand()) for _ in pop]
sims =
    simobs(inf_2cmt_lin_turnover, pop, turnover_params, vrandeffs; obstimes = sd_obstimes)
Simulated population (Vector{<:Subject})
  Simulated subjects: 10
  Simulated variables: dv, resp

If a population simulation is required with the random effects set to zero, then it is possible to use the zero_randeffs function:

etas = zero_randeffs(inf_2cmt_lin_turnover, turnover_params, pop)

sims = simobs(inf_2cmt_lin_turnover, pop, turnover_params, etas; obstimes = sd_obstimes)

fig = Figure(; resolution = (1000, 600), fontsize = 18)
ax1 = Axis(fig[1, 1]; xlabel = "Time (hr)", ylabel = "Concentration (ng/mL)")
ax2 = Axis(fig[1, 2]; xlabel = "Time (hr)", ylabel = "Response")
sim_plot!(ax1, inf_2cmt_lin_turnover, sims; observations = [:dv])

sim_plot!(ax2, inf_2cmt_lin_turnover, sims; observations = [:resp])

fig
Example block output

You still see variability in the plot above, mainly due to the residual variability components in the model. If we set the variance parameters to tiny values as below

turnover_params_wo_sigma =
    (turnover_params..., σ_prop_pk = 1e-20, σ_add_pk = 1e-20, σ_add_pd = 1e-20)
(tvcl = 1.5,
 tvvc = 25.0,
 tvq = 5.0,
 tvvp = 150.0,
 tvturn = 10,
 tvebase = 10,
 tvec50 = 0.3,
 Ω_pk = [0.05 0.0 0.0 0.0; 0.0 0.05 0.0 0.0; 0.0 0.0 0.05 0.0; 0.0 0.0 0.0 0.05],
 Ω_pd = [0.05;;],
 σ_prop_pk = 1.0e-20,
 σ_add_pk = 1.0e-20,
 σ_add_pd = 1.0e-20,)

and perform the simulation without random effects again we get only the population mean:

etas = zero_randeffs(inf_2cmt_lin_turnover, turnover_params, pop)

sims = simobs(
    inf_2cmt_lin_turnover,
    pop,
    turnover_params_wo_sigma,
    etas;
    obstimes = sd_obstimes,
)

fig = Figure(; resolution = (1000, 600), fontsize = 18)
ax1 = Axis(fig[1, 1]; xlabel = "Time (hr)", ylabel = "Concentration (ng/mL)")
ax2 = Axis(fig[1, 2]; xlabel = "Time (hr)", ylabel = "Response")

sim_plot!(ax1, inf_2cmt_lin_turnover, sims; observations = [:dv])

sim_plot!(ax2, inf_2cmt_lin_turnover, sims; observations = [:resp])

fig
Example block output

Handling the resulting simulation data

The resulting sims object contains the results of the simulation for each subject. The results for the ith subject can be retrieved by sims[i].

We can convert the simulation results to Pumas Subjects using the constructor broadcasted over all elements of sims:

sims_subjects = Subject.(sims)
Population
  Subjects: 10
  Observations: dv, resp

Then sims_subjects can be used to fit the model parameters based on the simulated dataset. We can also turn the simulated population into a DataFrame with the DataFrame constructor:

DataFrame(sims[1])
16×30 DataFrame
Rowidtimedvrespevidamtcmtratedurationssiirouteηpk_1ηpk_2ηpk_3ηpk_4ηpd_1CentralPeripheralRespCLVcQVpebaseec50emaxturnkoutkin0
String?Float64Float64?Float64?Int64?Float64?Symbol?Float64?Float64?Int8?Float64?NCA.Route?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Int64?Int64?Float64?Float64?
110.0missingmissing1150.0Central10.015.000.0NullRoute0.00.00.00.00.0missingmissingmissing1.525.05.0150.010.00.31100.11.0
210.250.0968269.966630missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.02.420650.06099929.966631.525.05.0150.010.00.31100.11.0
310.50.1875979.888390missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.04.689930.2382039.888391.525.05.0150.010.00.31100.11.0
410.750.2727319.784090missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.06.818280.5233749.784091.525.05.0150.010.00.31100.11.0
511.00.3526169.663470missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.08.81540.9088439.663471.525.05.0150.010.00.31100.11.0
612.00.6265319.105460missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.015.66333.318139.105461.525.05.0150.010.00.31100.11.0
714.01.011527.936490missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.025.288111.19167.936491.525.05.0150.010.00.31100.11.0
818.01.430675.957480missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.035.766833.23055.957481.525.05.0150.010.00.31100.11.0
9112.01.658694.525620missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.041.467358.2114.525621.525.05.0150.010.00.31100.11.0
10116.01.470423.528070missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.036.760482.72873.528071.525.05.0150.010.00.31100.11.0
11120.00.817013.105220missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.020.425292.56123.105221.525.05.0150.010.00.31100.11.0
12124.00.5993893.092280missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.014.984793.86673.092281.525.05.0150.010.00.31100.11.0
13136.00.4670293.557060missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.011.675787.98343.557061.525.05.0150.010.00.31100.11.0
14148.00.4267723.897150missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.010.669380.96333.897151.525.05.0150.010.00.31100.11.0
15160.00.3923674.143960missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.09.8091674.45594.143961.525.05.0150.010.00.31100.11.0
16171.90.3610734.360540missingmissingmissingmissingmissingmissingmissing0.00.00.00.00.09.0268368.51774.360541.525.05.0150.010.00.31100.11.0

From there, any Julia tools can be used to analyze these arrays and DataFrames. Or you can conveniently export this to a CSV, Excel, or SAS data file.

Conducting an Estimation

With a model and population you can fit your model. Model fitting in Pumas has the purpose of estimate parameters and is done by calling the fit function with the following positional arguments:

  1. a PumasModel constructed via @model, or a PumasEMModel constructed via @emmodel.
  2. a Population or a Subject.
  3. a named tuple of the initial parameter values.
  4. an inference algorithm.

If you want to use the model's initial parameter values declared inside the @param block, you can do so with init_params(model). Note that this will fall back to the parameters' default values if you do not specify an initial value inside the @param block.

Pumas supports the following inference algorithms:

  • Maximum Likelihood Estimation:

    • NaivePooled: first order approximation without random-effects.
    • FO: first-order approximation.
    • FOCE: first-order conditional estimation.
    • LaplaceI: second-order Laplace approximation.
    • SAEM: Stochastic Approximation using the Expectation-Maximization algorithm. Note that for this you use @emmodel instead of @model.
  • Bayesian inference:

    • BayesMCMC: MCMC using No-U-Turn Sampler (NUTS).
    • MarginalMCMC: MCMC using No-U-Turn Sampler (NUTS), but marginalizing out the subject-specific parameters.
    • MAP: Maximum A Posteriori estimation.

Now we can fit our model using one of the simulated populations from the previous section:

turnover_params = (;
    tvcl = 1.5,
    tvvc = 25.0,
    tvq = 5.0,
    tvvp = 150.0,
    tvturn = 10,
    tvebase = 10,
    tvec50 = 0.3,
    Ω_pk = Diagonal([0.05, 0.05, 0.05, 0.05]),
    Ω_pd = Diagonal([0.05]),
    σ_prop_pk = 0.15,
    σ_add_pk = 0.5,
    σ_add_pd = 0.2,
)

sims = simobs(inf_2cmt_lin_turnover, pop, turnover_params; obstimes = sd_obstimes)
Simulated population (Vector{<:Subject})
  Simulated subjects: 10
  Simulated variables: dv, resp
fit_2cmt_lin_turnover =
    fit(inf_2cmt_lin_turnover, Subject.(sims), init_params(inf_2cmt_lin_turnover), FOCE())
FittedPumasModel

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:               Nonlinear ODE
Solver(s):                          (Vern7,Rodas5)

Log-likelihood value:                   -138.07117
Number of subjects:                             10
Number of parameters:         Fixed      Optimized
                                  0             15
Observation records:         Active        Missing
    dv:                         150              0
    resp:                       150              0
    Total:                      300              0

--------------------------
              Estimate
--------------------------
tvcl           1.4363
tvvc          30.033
tvq            6.1795
tvvp         141.1
Ω_pk₁,₁        9.1997e-5
Ω_pk₂,₂        0.088404
Ω_pk₃,₃        0.016013
Ω_pk₄,₄        0.082186
σ_prop_pk      0.00010523
σ_add_pk       0.50236
tvturn         9.1055
tvebase        9.6404
tvec50         0.317
Ω_pd₁,₁        0.022554
σ_add_pd       0.20868
--------------------------

Calculating confidence intervals and Bootstrap

You can calculate confidence intervals using the infer function (by default 95% CIs):

infer_2cmt_lin_turnover = infer(fit_2cmt_lin_turnover)
Asymptotic inference results

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:               Nonlinear ODE
Solver(s):                          (Vern7,Rodas5)

Log-likelihood value:                   -138.07117
Number of subjects:                             10
Number of parameters:         Fixed      Optimized
                                  0             15
Observation records:         Active        Missing
    dv:                         150              0
    resp:                       150              0
    Total:                      300              0

------------------------------------------------
              Estimate       SE      95.0% C.I.
------------------------------------------------
tvcl           1.4363      NaN     [NaN; NaN]
tvvc          30.033       NaN     [NaN; NaN]
tvq            6.1795      NaN     [NaN; NaN]
tvvp         141.1         NaN     [NaN; NaN]
Ω_pk₁,₁        9.1997e-5   NaN     [NaN; NaN]
Ω_pk₂,₂        0.088404    NaN     [NaN; NaN]
Ω_pk₃,₃        0.016013    NaN     [NaN; NaN]
Ω_pk₄,₄        0.082186    NaN     [NaN; NaN]
σ_prop_pk      0.00010523  NaN     [NaN; NaN]
σ_add_pk       0.50236     NaN     [NaN; NaN]
tvturn         9.1055      NaN     [NaN; NaN]
tvebase        9.6404      NaN     [NaN; NaN]
tvec50         0.317       NaN     [NaN; NaN]
Ω_pd₁,₁        0.022554    NaN     [NaN; NaN]
σ_add_pd       0.20868     NaN     [NaN; NaN]
------------------------------------------------


Variance-covariance matrix could not be
evaluated. The random effects may be over-
parameterized. Check the coefficients for
variance estimates near zero.

You can also convert the output to a DataFrame with the coeftable function:

coeftable(infer_2cmt_lin_turnover)
15×5 DataFrame
Rowparameterestimateseci_lowerci_upper
StringFloat64Float64Float64Float64
1tvcl1.43628NaNNaNNaN
2tvvc30.0325NaNNaNNaN
3tvq6.17948NaNNaNNaN
4tvvp141.104NaNNaNNaN
5Ω_pk₁,₁9.19969e-5NaNNaNNaN
6Ω_pk₂,₂0.088404NaNNaNNaN
7Ω_pk₃,₃0.0160131NaNNaNNaN
8Ω_pk₄,₄0.0821856NaNNaNNaN
9σ_prop_pk0.000105228NaNNaNNaN
10σ_add_pk0.502362NaNNaNNaN
11tvturn9.10551NaNNaNNaN
12tvebase9.64038NaNNaNNaN
13tvec500.317NaNNaNNaN
14Ω_pd₁,₁0.0225543NaNNaNNaN
15σ_add_pd0.208678NaNNaNNaN

For inference with bootstrap-based confidence intervals you can use Bootstrap as an optional second positional argument in infer:

Pumas.BootstrapType
Bootstrap(; rng=Random.default_rng, samples=200, stratify_by=nothing, ensemblealg=EnsembleThreads())

Constructs a Bootstrap object that allows the user to use bootstrapping with the supplies setting in infer function calls. See ?infer and https://docs.pumas.ai/stable/analysis/inference/ for more information about obtaining inference results.

The keyword arguments are as follows:

  • rng: used to select a pseudo-random number generator to beused for the random resamples of the datasets.
  • samples: controls the number of resampled Populations.
  • resamples: the number of model parameter samples from the original sample distribution to re-sample (by weighed re-sampling without replacement). The number of resamples must be less than that of the original sample. An original sample-to-resample ratio of at least 5:1 is suggested.
  • stratify_by control the stratification used in the re-sampling scheme. Should be a set to a Symbol with the name of a covariate it is possible to stratify by a covariate with a finite number of possible values (e.g :study for a covariate named study).
  • ensemblealg controls the parallization across fit calls. Each fit is fitted using the ensemblealg that was used in the original fit.
infer_boot_2cmt_lin_turnover =
    infer(fit_2cmt_lin_turnover, Bootstrap(; ensemblealg = EnsembleThreads()))
Bootstrap inference results

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:               Nonlinear ODE
Solver(s):                          (Vern7,Rodas5)

Log-likelihood value:                   -138.07117
Number of subjects:                             10
Number of parameters:         Fixed      Optimized
                                  0             15
Observation records:         Active        Missing
    dv:                         150              0
    resp:                       150              0
    Total:                      300              0

-------------------------------------------------------------------------------
              Estimate             SE                        95.0% C.I.
-------------------------------------------------------------------------------
tvcl           1.4363            0.13736           [ 1.2455    ;   1.8138    ]
tvvc          30.033             5.1597            [18.317     ;  37.605     ]
tvq            6.1795            0.91063           [ 5.1616    ;   8.6843    ]
tvvp         141.1              20.879             [99.364     ; 180.64      ]
Ω_pk₁,₁        9.1997e-5         0.018623          [ 4.0998e-9 ;   0.062351  ]
Ω_pk₂,₂        0.088404          0.06658           [ 0.0080113 ;   0.26063   ]
Ω_pk₃,₃        0.016013          0.016288          [ 0.0       ;   0.047688  ]
Ω_pk₄,₄        0.082186          0.036301          [ 8.6324e-9 ;   0.12966   ]
σ_prop_pk      0.00010523        1.4613e-7         [ 0.00010487;   0.00010547]
σ_add_pk       0.50236           0.024075          [ 0.44284   ;   0.53467   ]
tvturn         9.1055            0.45068           [ 8.0821    ;   9.8399    ]
tvebase        9.6404            0.45367           [ 8.8884    ;  10.713     ]
tvec50         0.317             0.026488          [ 0.27839   ;   0.38695   ]
Ω_pd₁,₁        0.022554          0.0082728         [ 0.0056466 ;   0.035544  ]
σ_add_pd       0.20868           0.015262          [ 0.17678   ;   0.23813   ]
-------------------------------------------------------------------------------
Successful fits: 200 out of 200
Unique resampled populations: 200
No stratification.

You can also convert the Bootstrap inference results to a DataFrame with coeftable:

coeftable(infer_boot_2cmt_lin_turnover)
15×5 DataFrame
Rowparameterestimateseci_lowerci_upper
StringFloat64Float64Float64Float64
1tvcl1.436280.1373631.24551.81381
2tvvc30.03255.159718.316937.6048
3tvq6.179480.9106255.161568.6843
4tvvp141.10420.87999.3644180.639
5Ω_pk₁,₁9.19969e-50.01862294.09982e-90.0623507
6Ω_pk₂,₂0.0884040.066580.008011350.260633
7Ω_pk₃,₃0.01601310.01628830.00.0476882
8Ω_pk₄,₄0.08218560.03630068.63245e-90.129658
9σ_prop_pk0.0001052281.46128e-70.0001048710.00010547
10σ_add_pk0.5023620.02407540.442840.534671
11tvturn9.105510.4506838.082069.83991
12tvebase9.640380.4536678.8883510.7133
13tvec500.3170.0264880.2783890.386946
14Ω_pd₁,₁0.02255430.00827280.005646550.0355437
15σ_add_pd0.2086780.01526220.1767760.238127

Conclusion

This tutorial covered the basic workflow of building a model, simulating observations from it, and conducting an estimation. You can also see subsequent tutorials that discuss the components in more detail, such as:

  1. More detailed treatment of specifying populations, dosage regimens, and covariates.
  2. Reading in dosage regimens and observations from standard input data.
  3. Fitting models with different estimation methods.