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.9175
Version: 0.1.0.9176
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 @@ -18,6 +18,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 (#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
61 changes: 56 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,20 @@ 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 reverse dependency table and walk forward along the data-flow
# direction to find all params whose consumers eventually reach an AUC param.
rev_deps <- .build_rev_deps(metadata_nca_parameters)
needs_impute <- .walk_forward_deps(params_auc_dep, rev_deps)

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 +377,44 @@ rm_impute_obs_params <- function(data, metadata_nca_parameters = metadata_nca_pa

data
}

#' Build a reverse dependency table from the Depends column.
#' For each parameter A, returns which parameters list A as a dependency.
#' e.g., lambda.z lists half.life in Depends, so rev_deps$half.life includes lambda.z.
#' @noRd
.build_rev_deps <- 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
}

#' Walk forward through the reverse dependency table from `start_set`.
#' At each iteration, finds params whose consumers include any param
#' already in the set — i.e., params that feed into the current chain.
#' Stops when reaching purely observational leaf params (cmax, tmax, tlast)
#' to avoid including them in the imputation set.
#' @noRd
.walk_forward_deps <- function(start_set, rev_deps,
obs_params = c("cmax", "tmax", "tlast")) {
needs <- start_set
repeat {
newly_found <- character()
for (pkg in names(rev_deps)) {
if (!pkg %in% needs && !pkg %in% obs_params &&
any(rev_deps[[pkg]] %in% needs)) {
newly_found <- c(newly_found, pkg)
}
}
if (length(newly_found) == 0) break
needs <- c(needs, newly_found)
}
needs
}
79 changes: 79 additions & 0 deletions tests/testthat/test-intervals.R
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,82 @@ describe("rm_impute_obs_params", {
expect_equal(result$intervals, intervals_before)
})
})

describe(".build_rev_deps", {
it("builds reverse dependency map from Depends column", {
metadata <- data.frame(
PKNCA = c("half.life", "lambda.z", "aucinf.obs", "cmax"),
Depends = c("tmax, tlast", "half.life", "lambda.z", NA),
stringsAsFactors = FALSE
)
result <- .build_rev_deps(metadata)
expect_type(result, "list")
expect_true("lambda.z" %in% result[["half.life"]])
expect_true("aucinf.obs" %in% result[["lambda.z"]])
expect_null(result[["cmax"]])
})

it("handles empty Depends strings", {
metadata <- data.frame(
PKNCA = c("param1", "param2"),
Depends = c("", NA),
stringsAsFactors = FALSE
)
result <- .build_rev_deps(metadata)
expect_type(result, "list")
})
})

describe(".walk_forward_deps", {
it("finds half.life via transitive chain: half.life -> lambda.z -> aucinf.obs", {
metadata <- data.frame(
PKNCA = c("half.life", "lambda.z", "aucinf.obs", "cmax", "tmax"),
Depends = c("tmax, tlast", "half.life", "lambda.z", NA, NA),
stringsAsFactors = FALSE
)
rev_deps <- .build_rev_deps(metadata)
start_set <- c("aucinf.obs")
result <- .walk_forward_deps(start_set, rev_deps)
expect_true("aucinf.obs" %in% result)
expect_true("lambda.z" %in% result)
expect_true("half.life" %in% result)
expect_false("cmax" %in% result)
expect_false("tmax" %in% result)
})

it("excludes observational leaf params (cmax, tmax, tlast) regardless of depth", {
metadata <- data.frame(
PKNCA = c("half.life", "lambda.z", "aucinf.obs"),
Depends = c("tmax", "half.life", "lambda.z"),
stringsAsFactors = FALSE
)
rev_deps <- .build_rev_deps(metadata)
result <- .walk_forward_deps(c("aucinf.obs"), rev_deps)
expect_true("lambda.z" %in% result)
expect_true("half.life" %in% result)
# tmax is in default obs_params → excluded regardless of depth
expect_false("tmax" %in% result)
})
})

describe("rm_impute_obs_params integration", {
it("retains imputation for half.life when aucinf.obs is requested (#1057)", {
data <- FIXTURE_PKNCA_DATA
# Mark all intervals as having imputation
if (!"impute" %in% names(data$intervals)) {
data$intervals$impute <- "blq"
} else {
data$intervals$impute[is.na(data$intervals$impute)] <- "blq"
}
# Request only aucinf.obs and half.life
for (col in names(data$intervals)) {
if (col %in% c("start", "end", "impute", "type_interval")) next
data$intervals[[col]] <- col %in% c("aucinf.obs", "half.life")
}
result <- rm_impute_obs_params(data, metadata_nca_parameters)
# half.life should NOT have imputation removed (it feeds aucinf.obs)
expect_false(
any(grepl("half.life", result$intervals$impute_not_for_params %||% character()))
)
})
})