27  Architecture Overview

27.1 Class hierarchy

PKNCA uses four S3 classes: PKNCAconc and PKNCAdose feed PKNCAdata, and pk.nca() wraps the result into PKNCAresults:

classDiagram
    class PKNCAconc {
        data: data.frame
        formula: conc~time|groups
        subject: column name
        sparse: logical
        units: concu/amountu/timeu
        exclude / exclude_half.life / include_half.life
        lloq, time.nominal
        volume, duration (only if given)
    }
    class PKNCAdose {
        data: data.frame
        formula: dose~time|groups
        route: intravas./extravas.
        duration / rate
        units: doseu
        time.nominal
    }
    class PKNCAdata {
        conc: PKNCAconc
        dose: PKNCAdose
        intervals: data.frame
        impute: method string or column
        units: pknca_units_table()
        options: named list
        group_ref, grpid_cols
    }
    class PKNCAresults {
        result: data.frame incl. PPANMETH
        data: PKNCAdata
        exclude: column name
    }

    PKNCAconc --> PKNCAdata
    PKNCAdose --> PKNCAdata
    PKNCAdata --> PKNCAresults

27.2 How a calculation runs

PKNCAdata() assembles the inputs and pk.nca() does the work. At a high level (function names are from R/class-PKNCAdata.R and R/pk.calc.all.R):

flowchart TD
    A["PKNCAdata()"] --> A1["Intervals: given, or generated per group by<br/>choose.auc.intervals() (auto.interval.method)"]
    A1 --> A2["set_intervals() / check.interval.specification()<br/>validate the intervals"]
    A2 --> A3["Units table: given, or pknca_units_table()"]
    A3 --> B["pk.nca()"]
    B --> B1["assert_intervals(); convert date-time<br/>times and intervals to numbers"]
    B1 --> B2["Overlay the object's options on a<br/>copy of PKNCA.options()"]
    B2 --> B3["Expand reference intervals for<br/>secondary parameters"]
    B3 --> B4["full_join_PKNCAdata(): split concentration,<br/>dose, and interval data by group"]
    B4 --> C["pk.nca.intervals(): one call per group"]
    C --> D["pk.nca.interval(): one call per interval;<br/>dense and sparse parameters in one pass"]
    D --> E["Gather results (dense, then sparse)"]
    E --> F["pk_nca_secondary(): parameters that<br/>read a reference interval"]
    F --> G["PKNCAresults()"]

Inside pk.nca.interval(), the requested parameters are expanded with their depends, put in dependency order, and calculated one at a time. For each parameter, the arguments of its calculation function are filled from the interval data, earlier results, constants, or the options (see formalsmap); points named by include_half.life/exclude_half.life are applied before half.life is calculated; and the result’s exclude and "method" attributes become the exclude and PPANMETH columns. A function that returns a data frame contributes one result row per column. The calculation functions themselves never see a PKNCAdata object, so they can be called directly (see Utility Functions).

27.3 The interval column system

The parameter registry is the core extensibility mechanism. Every NCA parameter is registered as an interval column via add.interval.col().

# Inspect registration for auclast
cols <- get.interval.cols()
str(cols[["auclast"]])
List of 16
 $ FUN              : chr "pk.calc.auc.last"
 $ FUN_sparse       : chr "pk.calc.auclast_sparse"
 $ values           : logi [1:2] FALSE TRUE
 $ unit_type        : chr "auc"
 $ pretty_name      : chr "AUClast"
 $ desc             : chr "AUC start to last conc above LOQ"
 $ formalsmap       : list()
 $ formalsmap_sparse: list()
 $ depends          : NULL
 $ datatype         : chr "interval"
 $ pptestcd_cdisc   : chr "AUCLST"
 $ pptest_cdisc     : chr "AUC to Last Nonzero Conc"
 $ formula          : chr "$AUC_{\\text{last}} = \\sum_{k} AUC_k(C_k, C_{k+1}, t_k, t_{k+1})$"
 $ formula_note     : chr "Trapezoidal rule (linear-up/log-down by default)"
 $ tier             : chr "common"
 $ selection        : list()

Each entry contains:

Field Purpose
FUN Name of the function that computes this parameter (character string, or NA for parameters populated by another function’s multi-column result or calculated only by FUN_sparse)
FUN_sparse Name of a sparse-PK estimator, or NA (≥ 0.13.0)
values Allowed values in the interval column (typically c(FALSE, TRUE))
unit_type Unit category used for unit assignment and conversion
pretty_name Human-readable name used in summary tables
desc Human-readable description (40 characters or fewer for SDTM; longer warns)
formalsmap Maps function arguments to interval/group/options data, other parameters, constants, or reference-interval values
formalsmap_sparse The formalsmap for FUN_sparse (≥ 0.13.0)
depends Character vector of parameters that must be computed first
datatype Calculation type; only "interval" is currently supported
pptestcd_cdisc CDISC PPTESTCD code: a string, a route-keyed list, or a dense/sparse list (≥ 0.13.0)
pptest_cdisc CDISC PPTEST label, in the same forms (≥ 0.13.0)
formula, formula_note LaTeX formula and note for documentation; formula_note is the PPANMETH fallback (≥ 0.13.0)
tier, selection Classification for automatic interval selection (≥ 0.13.0)

The registry has no sparse field (≥ 0.13.0): add.interval.col(sparse = TRUE) is retired and is an error. Whether a parameter needs sparse data is derived from the registration (a FUN_sparse and no FUN) and reported by pknca_parameter_table(), together with each parameter’s concept, tier, and route and dosing contexts. After pk.nca() first uses an entry, it can also carry internal cached fields (requires_* and arg_spec) that are not part of the registration.

27.4 formalsmap

formalsmap is how PKNCA passes the right data to each parameter function. Arguments can be sourced from these places:

Source type Example What it provides
Interval data conc, time, dose, time.dose, route Data for the current interval (times relative to the interval start)
Group data conc.group, time.group Data for the whole group
Sparse data (≥ 0.13.0) conc.sparse, time.sparse, subject The pooled individual samples (NULL for dense data)
Other parameters auc = "auclast" A previously computed NCA result
Interval bounds start, end The interval start/end times
Options options The full analysis options list
Constants (≥ 0.13.0) auc.type = I("AUCall") The value itself
Reference interval (≥ 0.13.0) auc = pknca_ref("auclast") A parameter’s value in the reference interval; makes the parameter secondary

Individual option names (e.g. auc.method) cannot be mapped through formalsmap; only the whole options list can, and parameter functions read specific settings from it via PKNCA.choose.option().

27.5 Writing a custom parameter

Registering a new parameter is a single add.interval.col() call that names the computing function; PKNCA then derives execution order from depends, assigns units from unit_type, and collects results automatically. A minimal toy registration:

# Toy illustration: ratio of two built-in parameters
pk.calc.demo.ratio <- function(cmax, cmin) {
  cmax / cmin
}

add.interval.col(
  name        = "demo.ratio",
  FUN         = "pk.calc.demo.ratio",  # must be a character string, not the function object
  unit_type   = "unitless",
  pretty_name = "Demo ratio (Cmax/Cmin)",
  desc        = "Toy example: Cmax divided by Cmin",
  depends     = c("cmax", "cmin")
)

# The registry now contains it alongside the built-in parameters
str(get.interval.cols()[["demo.ratio"]])
List of 16
 $ FUN              : chr "pk.calc.demo.ratio"
 $ FUN_sparse       : chr NA
 $ values           : logi [1:2] FALSE TRUE
 $ unit_type        : chr "unitless"
 $ pretty_name      : chr "Demo ratio (Cmax/Cmin)"
 $ desc             : chr "Toy example: Cmax divided by Cmin"
 $ formalsmap       : list()
 $ formalsmap_sparse: list()
 $ depends          : chr [1:2] "cmax" "cmin"
 $ datatype         : chr "interval"
 $ pptestcd_cdisc   : chr "demo.ratio"
 $ pptest_cdisc     : chr "Toy example: Cmax divided by Cmin"
 $ formula          : NULL
 $ formula_note     : NULL
 $ tier             : chr "uncommon"
 $ selection        : list()

For the full walkthrough — parameters computed from raw concentration-time data, constants, multi-output functions, parameters that read a reference interval, sparse-only parameters, and custom summary statistics — see the Writing Custom Parameters chapter and the package vignette Writing PKNCA Parameter Functions.

27.6 Options system

PKNCA’s options are stored in a package-level environment; PKNCA.options() returns them as a named list. It is accessed in several ways:

# 1. Read all options
PKNCA.options()
$adj.r.squared.factor
[1] 1e-04

$r.squared.factor
[1] NA

$max.missing
[1] 0.5

$auc.method
[1] "lin up/log down"

$conc.na
[1] "drop"

$conc.blq
$conc.blq$first
[1] "keep"

$conc.blq$middle
[1] "drop"

$conc.blq$last
[1] "keep"


$debug
NULL

$first.tmax
[1] TRUE

$first.tmin
[1] TRUE

$allow.tmax.in.half.life
[1] FALSE

$keep_interval_cols
NULL

$min.hl.points
[1] 3

$max.hl.points
[1] Inf

$min.hl.start.time
[1] 0

$min.span.ratio
[1] 2

$max.aucinf.pext
[1] 20

$min.hl.r.squared
[1] 0.9

$progress
[1] TRUE

$tau.choices
[1] NA

$auto.interval.method
[1] "builder"

$auto.interval.tolerance
[1] 0.05

$single.dose.aucs
  start end auclast auclast_se auclast_df aucall aucall_se aucall_df aumclast
1     0  24    TRUE      FALSE      FALSE  FALSE     FALSE     FALSE    FALSE
2     0 Inf   FALSE      FALSE      FALSE  FALSE     FALSE     FALSE    FALSE
  aumclast_se aumclast_df aumcall aumcall_se aumcall_df aucint.last aucint.all
1       FALSE       FALSE   FALSE      FALSE      FALSE       FALSE      FALSE
2       FALSE       FALSE   FALSE      FALSE      FALSE       FALSE      FALSE
  aumcint.last aumcint.all    c0  cmax  cmin  tmax  tmin tlast tfirst clast.obs
1        FALSE       FALSE FALSE FALSE FALSE FALSE FALSE FALSE  FALSE     FALSE
2        FALSE       FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  FALSE     FALSE
  cl.last cl.all cl.int.all cl.int.last mrt.last mrt.all mrt.int.all
1   FALSE  FALSE      FALSE       FALSE    FALSE   FALSE       FALSE
2   FALSE  FALSE      FALSE       FALSE    FALSE   FALSE       FALSE
  mrt.int.last mrt.iv.last vss.last vss.iv.last vss.all vss.int.all
1        FALSE       FALSE    FALSE       FALSE   FALSE       FALSE
2        FALSE       FALSE    FALSE       FALSE   FALSE       FALSE
  vss.int.last   cav cav.int.last cav.int.all ctrough cstart   ptr  tlag
1        FALSE FALSE        FALSE       FALSE   FALSE  FALSE FALSE FALSE
2        FALSE FALSE        FALSE       FALSE   FALSE  FALSE FALSE FALSE
  deg.fluc swing  ceoi aucabove.predose.all aucabove.trough.all count_conc
1    FALSE FALSE FALSE                FALSE               FALSE      FALSE
2    FALSE FALSE FALSE                FALSE               FALSE      FALSE
  count_conc_measured totdose volpk    ae clr.last clr.obs clr.pred    fe
1               FALSE   FALSE FALSE FALSE    FALSE   FALSE    FALSE FALSE
2               FALSE   FALSE FALSE FALSE    FALSE   FALSE    FALSE FALSE
  ertlst ermax ertmax erint erlst ratio.cmax ratio.auclast ratio.aucint.last
1  FALSE FALSE  FALSE FALSE FALSE      FALSE         FALSE             FALSE
2  FALSE FALSE  FALSE FALSE FALSE      FALSE         FALSE             FALSE
  ratio.aucint.all sparse_auclast sparse_auc_se sparse_auc_df sparse_aumclast
1            FALSE          FALSE         FALSE         FALSE           FALSE
2            FALSE          FALSE         FALSE         FALSE           FALSE
  sparse_aumc_se sparse_aumc_df time_above aucivlast aucivlast_se aucivlast_df
1          FALSE          FALSE      FALSE     FALSE        FALSE        FALSE
2          FALSE          FALSE      FALSE     FALSE        FALSE        FALSE
  aucivall aucivall_se aucivall_df aucivint.last aucivint.all aucivpbextlast
1    FALSE       FALSE       FALSE         FALSE        FALSE          FALSE
2    FALSE       FALSE       FALSE         FALSE        FALSE          FALSE
  aucivpbextall aucivpbextint.last aucivpbextint.all aumcivlast aumcivlast_se
1         FALSE              FALSE             FALSE      FALSE         FALSE
2         FALSE              FALSE             FALSE      FALSE         FALSE
  aumcivlast_df aumcivall aumcivall_se aumcivall_df aumcivint.last
1         FALSE     FALSE        FALSE        FALSE          FALSE
2         FALSE     FALSE        FALSE        FALSE          FALSE
  aumcivint.all half.life r.squared adj.r.squared lambda.z.corrxy lambda.z
1         FALSE     FALSE     FALSE         FALSE           FALSE    FALSE
2         FALSE      TRUE     FALSE         FALSE           FALSE    FALSE
  lambda.z.time.first lambda.z.time.last lambda.z.n.points clast.pred
1               FALSE              FALSE             FALSE      FALSE
2               FALSE              FALSE             FALSE      FALSE
  span.ratio tobit_residual adj_tobit_residual lambda.z.n.points_blq
1      FALSE          FALSE              FALSE                 FALSE
2      FALSE          FALSE              FALSE                 FALSE
  thalf.eff.last thalf.eff.iv.last kel.last kel.iv.last kel.all kel.int.all
1          FALSE             FALSE    FALSE       FALSE   FALSE       FALSE
2          FALSE             FALSE    FALSE       FALSE   FALSE       FALSE
  kel.int.last cl.iv.all cl.iv.last cl.ivint.all cl.ivint.last cl.sparse.last
1        FALSE     FALSE      FALSE        FALSE         FALSE          FALSE
2        FALSE     FALSE      FALSE        FALSE         FALSE          FALSE
  f.last f.int.last f.int.all mrt.sparse.last mrt.iv.all mrt.ivint.all
1  FALSE      FALSE     FALSE           FALSE      FALSE         FALSE
2  FALSE      FALSE     FALSE           FALSE      FALSE         FALSE
  mrt.ivint.last vz.all vz.int.all vz.int.last vz.iv.all vz.iv.last
1          FALSE  FALSE      FALSE       FALSE     FALSE      FALSE
2          FALSE  FALSE      FALSE       FALSE     FALSE      FALSE
  vz.ivint.all vz.ivint.last vz.last vss.iv.all vss.ivint.all vss.ivint.last
1        FALSE         FALSE   FALSE      FALSE         FALSE          FALSE
2        FALSE         FALSE   FALSE      FALSE         FALSE          FALSE
  vss.sparse.last aucinf.obs aucinf.obs_se aucinf.obs_df aucinf.pred
1           FALSE      FALSE         FALSE         FALSE       FALSE
2           FALSE       TRUE         FALSE         FALSE       FALSE
  aumcinf.obs aumcinf.obs_se aumcinf.obs_df aumcinf.pred aucint.inf.obs
1       FALSE          FALSE          FALSE        FALSE          FALSE
2       FALSE          FALSE          FALSE        FALSE          FALSE
  aucint.inf.pred aumcint.inf.obs aumcint.inf.pred aucivinf.obs aucivinf.obs_se
1           FALSE           FALSE            FALSE        FALSE           FALSE
2           FALSE           FALSE            FALSE        FALSE           FALSE
  aucivinf.obs_df aucivinf.pred aucivpbextinf.obs aucivpbextinf.pred
1           FALSE         FALSE             FALSE              FALSE
2           FALSE         FALSE             FALSE              FALSE
  aumcivinf.obs aumcivinf.obs_se aumcivinf.obs_df aumcivinf.pred aucpext.obs
1         FALSE            FALSE            FALSE          FALSE       FALSE
2         FALSE            FALSE            FALSE          FALSE       FALSE
  aucpext.pred kel.iv.all kel.ivint.all kel.ivint.last kel.sparse.last cl.obs
1        FALSE      FALSE         FALSE          FALSE           FALSE  FALSE
2        FALSE      FALSE         FALSE          FALSE           FALSE  FALSE
  cl.pred cl.int.inf.obs cl.int.inf.pred cl.iv.obs cl.iv.pred f.obs f.pred
1   FALSE          FALSE           FALSE     FALSE      FALSE FALSE  FALSE
2   FALSE          FALSE           FALSE     FALSE      FALSE FALSE  FALSE
  f.int.obs f.int.pred mrt.obs mrt.pred mrt.int.inf.obs mrt.int.inf.pred
1     FALSE      FALSE   FALSE    FALSE           FALSE            FALSE
2     FALSE      FALSE   FALSE    FALSE           FALSE            FALSE
  mrt.iv.obs mrt.iv.pred mrt.md.obs mrt.md.pred mrt.ivmd.obs mrt.ivmd.pred
1      FALSE       FALSE      FALSE       FALSE        FALSE         FALSE
2      FALSE       FALSE      FALSE       FALSE        FALSE         FALSE
  vz.obs vz.pred vz.int.inf.obs vz.int.inf.pred vz.iv.obs vz.iv.pred
1  FALSE   FALSE          FALSE           FALSE     FALSE      FALSE
2  FALSE   FALSE          FALSE           FALSE     FALSE      FALSE
  vz.sparse.last vss.obs vss.pred vss.iv.obs vss.iv.pred vss.md.obs vss.md.pred
1          FALSE   FALSE    FALSE      FALSE       FALSE      FALSE       FALSE
2          FALSE   FALSE    FALSE      FALSE       FALSE      FALSE       FALSE
  vss.ivmd.obs vss.ivmd.pred vss.int.inf.obs vss.int.inf.pred cav.int.inf.obs
1        FALSE         FALSE           FALSE            FALSE           FALSE
2        FALSE         FALSE           FALSE            FALSE           FALSE
  cav.int.inf.pred ratio.aucinf.obs ratio.aucinf.pred thalf.eff.obs
1            FALSE            FALSE             FALSE         FALSE
2            FALSE            FALSE             FALSE         FALSE
  thalf.eff.pred thalf.eff.iv.obs thalf.eff.iv.pred kel.obs kel.pred kel.iv.obs
1          FALSE            FALSE             FALSE   FALSE    FALSE      FALSE
2          FALSE            FALSE             FALSE   FALSE    FALSE      FALSE
  kel.iv.pred kel.int.inf.obs kel.int.inf.pred auclast.dn aucall.dn
1       FALSE           FALSE            FALSE      FALSE     FALSE
2       FALSE           FALSE            FALSE      FALSE     FALSE
  aucinf.obs.dn aucinf.pred.dn aumclast.dn aumcall.dn aumcinf.obs.dn
1         FALSE          FALSE       FALSE      FALSE          FALSE
2         FALSE          FALSE       FALSE      FALSE          FALSE
  aumcinf.pred.dn cmax.dn cmin.dn clast.obs.dn clast.pred.dn cav.dn ctrough.dn
1           FALSE   FALSE   FALSE        FALSE         FALSE  FALSE      FALSE
2           FALSE   FALSE   FALSE        FALSE         FALSE  FALSE      FALSE
  clr.last.dn clr.obs.dn clr.pred.dn demo.ratio
1       FALSE      FALSE       FALSE      FALSE
2       FALSE      FALSE       FALSE      FALSE

$allow_partial_missing_units
[1] FALSE

$hl_method
[1] "log-linear"

$sparse_lambda_z_se
[1] "delta"

$tobit_n_points_penalty
[1] 0

$tobit_optim_control
list()
# 2. Read a specific option
PKNCA.options("min.hl.points")
[1] 3
# 3. Set globally — save the old value first so you can restore it
old_val <- PKNCA.options("min.hl.points")[[1]]
PKNCA.options(min.hl.points = 4)
PKNCA.options("min.hl.points")
[1] 4
# 4. Restore by passing the saved value as a named argument
PKNCA.options(min.hl.points = old_val)
PKNCA.options("min.hl.points")
[1] 3

Prefer passing options = list(...) to PKNCAdata() over mutating global options. Global changes persist across analyses in the same session. At the start of each run, pk.nca() overlays the object’s options on a copy of the global options and uses that copy for the calculations; the global options themselves are not modified. PKNCA_options_defaults() (≥ 0.13.0) returns the package defaults without resetting anything.

27.7 Sparse PK

Sparse PK (one or two samples per subject, common in preclinical or pediatric studies) shares the dense code path (≥ 0.13.0):

  • Set sparse = TRUE in PKNCAconc().
  • Each group reaches pk.nca.interval() with two representations of the same samples: the arithmetic-mean profile (as conc/time) and the pooled individual samples (as conc.sparse/time.sparse, with subject). Dense and sparse parameters are calculated in one pass.
  • A parameter with a FUN_sparse (such as auclast, whose sparse estimator also returns auclast_se and auclast_df) is calculated from the pooled samples; one without falls back to FUN on the mean profile (half-life, for example). A parameter with FUN = NA and a FUN_sparse is sparse-only, and requesting it for dense data is an error.
  • pknca_parameter_table()$sparse reports which parameters need sparse data.

For the full workflow, see the Sparse NCA chapter and the vignette Sparse NCA Calculations.

27.8 Conditions and messages

Errors, warnings, and messages are signaled with rlang and carry classes named pknca_error_*, pknca_warning_*, and pknca_message_* (≥ 0.13.0), so code can catch them by class rather than by message text. Several older classes were renamed in 0.13.0 (for example, pknca_conc_none is now pknca_warning_no_concentration). Input validation uses checkmate. With verbose = TRUE, pk.nca() reports one “Starting PK NCA calculations.” message (pknca_message_pk_start).

# Catch a PKNCA error by its class
tryCatch(
  add.interval.col(name = "x_ref", FUN = "pk.calc.cmax", unit_type = "conc",
                   pretty_name = "x", desc = "Reserved name"),
  pknca_error_param_name_reserved = function(e) class(e)[1]
)
[1] "pknca_error_param_name_reserved"

27.9 Key functions

All of these are exported except where marked internal:

Function Purpose
get.interval.cols() Return full parameter registry
pknca_parameter_table() Classification of every parameter (concept, tier, sparse, secondary, contexts)
get.parameter.deps() Parameters calculated from a parameter, or with recursive = TRUE what it is calculated from
PKNCA:::sort_interval_cols() Topological sort → execution order (internal, unexported)
assert_intervals() Validate an interval specification against a PKNCAdata object
pk.nca.interval() Compute all parameters for one interval/group; takes conc.sparse, time.sparse, conc.sparse.group, and time.sparse.group for sparse data (its sparse argument was removed in 0.13.0)
pknca_units_table() Build unit assignment/conversion table
PKNCA_impute_method_*() Built-in imputation methods (listed by pknca_impute_methods())
pk.calc.auc.last() Core AUC calculation; returns a "method" attribute
pk.calc.half.life() λz regression with best-fit selection

This overview covers the surface. The business rules (002-pk.business.rules.R) and the validation framework (v60-PKNCA-validation.Rmd) in the PKNCA sources are richer topics worth exploring further.