Skip to content

fix: resolve transitive BLQ imputation dependencies for half.life chain (#1057)#1338

Open
wangzhengdna-lang wants to merge 4 commits into
pharmaverse:mainfrom
wangzhengdna-lang:1057-fix/blq-impute-half-life-deps
Open

fix: resolve transitive BLQ imputation dependencies for half.life chain (#1057)#1338
wangzhengdna-lang wants to merge 4 commits into
pharmaverse:mainfrom
wangzhengdna-lang:1057-fix/blq-impute-half-life-deps

Conversation

@wangzhengdna-lang

@wangzhengdna-lang wangzhengdna-lang commented May 29, 2026

Copy link
Copy Markdown

Issue

Closes #1057

Description

rm_impute_obs_params() incorrectly removes BLQ imputation from half.life and lambda.z when they are calculated as standalone parameters, causing inconsistency with the same values used internally by AUCINF (AUCIFO/AUCIFP).

Root cause

The dependency check was limited to one level — it checked a parameter's direct Depends column but missed the transitive chain:

half.life -> lambda.z -> aucinf.obs (AUCIFO)
     |
     +-----> clast.pred -> aucinf.pred (AUCIFP)

Fix

Added .build_rev_deps() and .walk_forward_deps() helpers that recursively trace upstream dependencies from AUC parameters, limited to 2 levels to avoid reaching purely observational leaf params (cmax, tmax, tlast).

Definition of Done

  • half.life and lambda.z retain BLQ imputation when AUC parameters are requested
  • Purely observational parameters (cmax, tmax, tlast) still have imputation removed
  • All existing tests pass
  • New helper functions have dedicated unit tests

How to test

Requires manual verification with PK data:

  1. Load IV bolus data with terminal BLQ points
  2. Request half.life and AUCIFO in NCA parameters
  3. Run NCA — verify LAMZHL, LAMZ, and AUCIFO are internally consistent (AUCLST + Clast/LAMZ ≈ AUCIFO)

Files changed

File Change
R/intervals.R New .build_rev_deps() and .walk_forward_deps() helpers; refactored rm_impute_obs_params() with transitive dependency resolution
tests/testthat/test-intervals.R 4 new unit tests for .build_rev_deps() and .walk_forward_deps()
DESCRIPTION Version bumped
NEWS.md Bug fix entry added

Contributor checklist

  • Code passes lintr checks (0 lints on intervals.R)
  • Code passes all unit tests (123 interval-related tests pass)
  • New logic covered by unit tests — .build_rev_deps() and .walk_forward_deps() have 4 dedicated tests
  • New logic is documented — roxygen2 @noRd docs for both helpers
  • App or package changes are reflected in NEWS
  • Package version is incremented
  • R script works with the new implementation — N/A (NCA calculation logic, not script-specific)
  • Settings upload works with the new implementation — N/A (no settings changes)
  • If any .scss change was done, run data-raw/compile_css.R — N/A
  • If a package dependency was added/changed, run data-raw/test_suggests_hidden.R — N/A

Notes to reviewer

Closes #1057

…in (pharmaverse#1057)

rm_impute_obs_params() previously used a single-level dependency check:
parameters whose Depends did not directly reference an AUC param had
their BLQ imputation removed. This missed the transitive chain
half.life -> lambda.z -> aucinf.obs, causing standalone half.life to
be calculated on raw data while AUCIFO internally used a different
(imputed) half.life value.

New .resolve_upstream_deps() helper recursively traces upstream
dependencies (2 levels) from AUC parameters to identify all params
in the calculation chain. half.life and lambda.z now correctly
retain BLQ imputation when any AUC-dependent parameter is requested.

Also adds the impute column guard from pharmaverse#1266 to prevent the
"PKNCA_impute_method_FALSE" error when start_impute is FALSE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@wangzhengdna-lang

Copy link
Copy Markdown
Author

test_blq_1057.csv
Dataset for testing attached

…verse#1057)

Replace the reverse traversal (.resolve_upstream_deps) with a forward
approach using a reverse dependency table (.build_rev_deps + .walk_forward_deps).

Now follows the natural data-flow direction: for each parameter, checks
whether any of its consumers (params that list it in Depends) are in the
AUC chain. This makes the logic more intuitive — "half.life feeds lambda.z,
which feeds aucinf.obs, therefore half.life keeps imputation."

Extracted .walk_forward_deps() as a separate helper to reduce cyclomatic
complexity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Shaakon35

Copy link
Copy Markdown
Collaborator

Missing tests for dependency resolution helpers

.build_rev_deps(), .walk_forward_deps(), and the updated rm_impute_obs_params() have no unit tests. The PR states existing tests pass, but none exercise the transitive dependency resolution that is the core fix.

Suggested tests in tests/testthat/test-intervals.R:

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

describe(".walk_forward_deps", {
  it("resolves half.life via lambda.z -> aucinf.obs chain", {
    rev <- list(
      half.life = "lambda.z",
      lambda.z = "aucinf.obs",
      tmax = "half.life"
    )
    needs <- .walk_forward_deps("aucinf.obs", rev, max_depth = 2)
    expect_true("lambda.z" %in% needs)
    expect_true("half.life" %in% needs)
    # tmax should NOT be included (depth 3)
    expect_false("tmax" %in% needs)
  })
})

describe("rm_impute_obs_params", {
  it("retains imputation for half.life when aucinf.obs is requested", {
    # Verify half.life is NOT in params_not_to_impute
    # (integration test with actual metadata_nca_parameters)
  })
})

@Shaakon35

Copy link
Copy Markdown
Collaborator

Hardcoded max_depth = 2 in .walk_forward_deps() is fragile

The depth limit prevents reaching purely observational leaf params (cmax, tmax, tlast), which is the right intent. But if a future parameter introduces a 3-level dependency chain, imputation will be silently removed — reintroducing the same class of bug this PR fixes.

Two options to make this more resilient:

Option A — Remove the depth limit, use an explicit exclusion set:

.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
}

This is more explicit about what to exclude rather than relying on depth as a proxy.

Option B — Keep the depth limit but add a warning:

if (length(newly_found) > 0 && depth == max_depth) {
  warning("Dependency walk truncated at depth ", max_depth,
          ". Some upstream params may be missing from imputation set.")
}

Option A is preferred since it's self-documenting and won't break silently.

@Shaakon35

Copy link
Copy Markdown
Collaborator

.gitignore: missing trailing newline and unrelated change

Two issues:

  1. Missing trailing newline — the file still ends with \ No newline at end of file after CLAUDE.md. Add a final newline to avoid this showing up in future diffs.

  2. Unrelated change — adding CLAUDE.md to .gitignore is not related to issue Bug: Issue with HL dependent parameter calculations with BLQ & start concentration #1057. Consider moving it to a separate commit or a housekeeping PR to keep this PR focused on the BLQ imputation fix.

Fix:

# Ensure trailing newline
printf 'desktop.ini\nCLAUDE.md\n' >> .gitignore.tmp && mv .gitignore.tmp .gitignore

Or simply ensure your editor adds a final newline when saving.

wangzheng and others added 2 commits June 4, 2026 09:30
… helpers (pharmaverse#1057)

- Remove CLAUDE.md from project .gitignore (per reviewer feedback)
- Add 4 unit tests for .build_rev_deps() and .walk_forward_deps()
  covering reverse dep map construction, empty Depends handling,
  transitive chain resolution (half.life -> lambda.z -> aucinf.obs),
  and max_depth boundary enforcement

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ward_deps (pharmaverse#1057)

- Replace hardcoded max_depth=2 with explicit obs_params exclusion set
  (cmax, tmax, tlast). More robust - won't silently break if future
  parameters introduce deeper dependency chains.
- Add rm_impute_obs_params integration test verifying half.life retains
  imputation when aucinf.obs is requested.
- Ensure .gitignore has trailing newline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@wangzhengdna-lang wangzhengdna-lang left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaakon35 All three addressed:

1. Missing tests — Fixed

Tests for .build_rev_deps() and .walk_forward_deps() were already added in the previous commit. Now also added an integration test for rm_impute_obs_params() verifying that half.life retains BLQ imputation when aucinf.obs is requested.

2. Hardcoded max_depth = 2 — Replaced with explicit exclusion set

Adopted Option A: replaced the depth limit with an explicit obs_params parameter (default: c("cmax", "tmax", "tlast")). The walk now runs until no more params are found, stopping only at the listed observational params. This is self-documenting and won't silently break if future params introduce deeper dependency chains.

3. .gitignore — Fixed

CLAUDE.md was already removed in the previous commit. Added the missing trailing newline to prevent diff artifacts.

All 124 intervals tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Issue with HL dependent parameter calculations with BLQ & start concentration

3 participants