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 Architecture Overview
27.1 Class hierarchy
PKNCA uses four S3 classes: PKNCAconc and PKNCAdose feed PKNCAdata, and pk.nca() wraps the result into 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(...)toPKNCAdata()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 = TRUEinPKNCAconc(). - Each group reaches
pk.nca.interval()with two representations of the same samples: the arithmetic-mean profile (asconc/time) and the pooled individual samples (asconc.sparse/time.sparse, withsubject). Dense and sparse parameters are calculated in one pass. - A parameter with a
FUN_sparse(such asauclast, whose sparse estimator also returnsauclast_seandauclast_df) is calculated from the pooled samples; one without falls back toFUNon the mean profile (half-life, for example). A parameter withFUN = NAand aFUN_sparseis sparse-only, and requesting it for dense data is an error. pknca_parameter_table()$sparsereports 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.