Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ inst/shiny/log/*
.DS_Store
# {shinytest2}: Ignore new debug snapshots for `$expect_values()`
*_.new.png
desktop.ini
desktop.ini
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: aNCA
Title: (Pre-)Clinical NCA in a Dynamic Shiny App
Version: 0.1.0.9184
Version: 0.1.0.9187
Authors@R: c(
person("Ercan", "Suekuer", email = "ercan.suekuer@roche.com", role = "aut",
comment = c(ORCID = "0009-0001-1626-1526")),
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
* Partial interval parameters section supports calculations beyond `AUCINT`: `RCAMINT`, `AUCINTD`, `CAVGINT`, and others. Table starts empty by default with a Remove Row button (#524, #1249)
* "Min. Points for Half-life" setting added (range 2–10, default 3) (#1155)
* BLQ imputation rules via `NCA Setup > Data Imputation` (#139)

### Bug Fixes
* Half-life (LAMZHL) and lambda.z (LAMZ) now correctly retain BLQ imputation when half-life-dependent parameters (AUCIFO, AUCIFP) are requested. Previously `rm_impute_obs_params()` removed imputation from `half.life` because its dependency check was limited to one level — missing the transitive chain `half.life -> lambda.z -> aucinf.obs`. The dependency resolution now recursively traverses upstream dependencies to include all parameters in the AUC calculation chain, and also walks downstream so AUC consumers (e.g., `vss.obs`, `vz.obs`) keep the same imputed data (#1057).
* General Exclusions section for in-app NCA exclusions, with "Excl. TLG" checkbox per entry (#851, #1018)
* Parameter Exclusions tab: exclude individual PK parameter rows from descriptive statistics and ADPP export via PPSUMFL/PPSUMRSN flags (#1040)
* NCA flag rules (NCAwXRS) from ADNCA standards — flagged records are excluded from NCA (#752)
Expand Down
118 changes: 113 additions & 5 deletions R/intervals.R
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ update_main_intervals <- function(
# and apply it only for non-observational parameters

if (!is.null(blq_imputation_rule)) {
# Ensure impute column exists so dplyr mutate below references the
# column rather than the function parameter (which could be FALSE
# from YAML settings, causing "PKNCA_impute_method_FALSE" error).
if (!"impute" %in% names(data$intervals)) {
data$intervals$impute <- NA_character_
}
data$intervals <- data$intervals %>%
mutate(
impute = ifelse(
Expand Down Expand Up @@ -324,16 +330,26 @@ update_main_intervals <- function(
#' @import dplyr
#'
rm_impute_obs_params <- function(data, metadata_nca_parameters = metadata_nca_parameters) {
# Don't impute parameters that are not AUC dependent
# Don't impute parameters that are not AUC dependent.
# Parameters in the AUC calculation chain (half.life feeds lambda.z,
# which feeds aucinf.obs) must all use the same BLQ-imputed data (#1057).
params_auc_dep <- metadata_nca_parameters %>%
filter(grepl("auc|aumc", PKNCA) | grepl("auc", Depends)) %>%
pull(PKNCA)

# Build consumer map ("who consumes X") and walk upstream from AUC params
# to find all parameters in their dependency chain (#1057).
consumer_map <- .build_consumer_map(metadata_nca_parameters)
needs_impute <- .find_upstream_deps(params_auc_dep, consumer_map)

# Walk downstream too: AUC consumers (e.g., vss.obs = f(cl.obs, mrt.obs),
# vz.obs = f(cl.obs, lambda.z)) compute AUC-derived quantities internally,
# so they must use the same imputed data for consistency (#1057).
dependency_map <- .build_dependency_map(metadata_nca_parameters)
needs_impute <- .find_downstream_consumers(needs_impute, dependency_map)

params_not_to_impute <- metadata_nca_parameters %>%
filter(
!grepl("auc|aumc", PKNCA),
!grepl(paste0(params_auc_dep, collapse = "|"), Depends)
) %>%
filter(!PKNCA %in% needs_impute) %>%
pull(PKNCA) %>%
intersect(names(PKNCA::get.interval.cols()))

Expand Down Expand Up @@ -367,3 +383,95 @@ rm_impute_obs_params <- function(data, metadata_nca_parameters = metadata_nca_pa

data
}

#' Build a consumer map from the Depends column.
#' For each parameter A, returns which parameters consume A (list A as a dependency).
#' e.g., lambda.z lists half.life in Depends, so consumer_map$half.life = "lambda.z".
#' @noRd
.build_consumer_map <- function(metadata) {
rev <- list()
for (i in seq_len(nrow(metadata))) {
pkg_name <- metadata$PKNCA[i]
dep_str <- metadata$Depends[i]
if (is.na(dep_str) || dep_str == "") next
dep_list <- trimws(strsplit(dep_str, ",")[[1]])
for (d in dep_list) {
rev[[d]] <- unique(c(rev[[d]], pkg_name))
}
}
rev
}

#' Build a dependency map from the Depends column.
#' Mirror image of `.build_consumer_map()`: for each parameter A, returns the
#' parameters A depends on.
#' e.g., vss.obs lists "cl.obs, mrt.obs" in Depends, so
#' dependency_map$vss.obs = c("cl.obs", "mrt.obs").
#' @noRd
.build_dependency_map <- function(metadata) {
fwd <- list()
for (i in seq_len(nrow(metadata))) {
pkg_name <- metadata$PKNCA[i]
dep_str <- metadata$Depends[i]
if (is.na(dep_str) || dep_str == "") next
fwd[[pkg_name]] <- trimws(strsplit(dep_str, ",")[[1]])
}
fwd
}

#' Check if a parameter is a new upstream consumer of the current chain.
#' @noRd
.is_new_upstream_consumer <- function(pkg, consumer_map, needs, obs_params) {
!pkg %in% needs && !pkg %in% obs_params && any(consumer_map[[pkg]] %in% needs)
}

#' Find all upstream dependencies transitively from `start_set`.
#' Walks the consumer map to collect params that feed into the current chain,
#' stopping at purely observational leaf params (cmax, tmax, tlast).
#'
#' @param start_set Character vector of starting PKNCA parameter names.
#' @param consumer_map Named list from `.build_consumer_map()`.
#' @param obs_params Character vector of observational params to exclude.
#' Must be kept in sync with metadata_nca_parameters when new leaf params
#' are added. Default: cmax, tmax, tlast.
#' @param max_iter Maximum iterations to guard against infinite loops from
#' circular dependencies. 50 is generous (~40 params in real metadata).
#' @noRd
.find_upstream_deps <- function(start_set, consumer_map,
obs_params = c("cmax", "tmax", "tlast"),
max_iter = 50L) {
needs <- start_set
for (iter in seq_len(max_iter)) {
newly_found <- character()
for (pkg in names(consumer_map)) {
if (.is_new_upstream_consumer(pkg, consumer_map, needs, obs_params)) {
newly_found <- c(newly_found, pkg)
}
}
if (length(newly_found) == 0) break
needs <- c(needs, newly_found)
}
needs
}

#' Find all downstream consumers transitively from `start_set`.
#' Params whose Depends list intersects the current chain consume AUC-derived
#' quantities (e.g., vss.obs consumes cl.obs and mrt.obs) and therefore compute
#' AUC internally — they must use the same BLQ-imputed data (#1057).
#'
#' The fixpoint walk is direction-agnostic: `.find_upstream_deps()` follows
#' "who consumes X" edges, so passing the reverse (dependency) map from
#' `.build_dependency_map()` walks the graph downstream. Purely observational
#' leaf params (cmax, tmax, tlast) are excluded as in the upstream walk.
#'
#' @param start_set Character vector of starting PKNCA parameter names.
#' @param dependency_map Named list from `.build_dependency_map()`.
#' @param obs_params Character vector of observational params to exclude.
#' @param max_iter Maximum iterations to guard against infinite loops from
#' circular dependencies.
#' @noRd
.find_downstream_consumers <- function(start_set, dependency_map,
obs_params = c("cmax", "tmax", "tlast"),
max_iter = 50L) {
.find_upstream_deps(start_set, dependency_map, obs_params, max_iter)
}
3 changes: 3 additions & 0 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ AEFRLT
AFRLT
ARRLT
ATPTREF
AUCIFO
AUCIFOD
AUCIFP
AUCINT
AUCLSTD
AUCPEO
Expand Down Expand Up @@ -58,6 +60,7 @@ INTRAVASCULAR
Kezia
Kobana
LAMZ
LAMZHL
LAMZLL
LAMZNPT
LAMZSPN
Expand Down
13 changes: 13 additions & 0 deletions tests/testthat/data/test-blq-ADNCA.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"STUDYID","USUBJID","PCSPEC","PARAM","DOSETRT","DOSEA","DOSEU","ROUTE","ADOSEDUR","AVAL","AVALU","AFRLT","NFRLT","ARRLT","NRRLT","RRLTU","ATPTREF","TRT01A","METABFL"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,100,"ng/mL",0.5,0.5,0.5,0.5,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,80,"ng/mL",1,1,1,1,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,65,"ng/mL",1.5,1.5,1.5,1.5,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,52,"ng/mL",2,2,2,2,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,35,"ng/mL",3,3,3,3,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,25,"ng/mL",4,4,4,4,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,12,"ng/mL",6,6,6,6,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,6,"ng/mL",8,8,8,8,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,2.5,"ng/mL",12,12,12,12,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,0,"ng/mL",24,24,24,24,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,0,"ng/mL",36,36,36,36,"hr","DOSE 1","DrugA 10 mg","N"
"S1","BLQ-TEST-01","SERUM","DrugA","DrugA",10,"mg","INTRAVENOUS BOLUS",0,0,"ng/mL",48,48,48,48,"hr","DOSE 1","DrugA 10 mg","N"
Loading