Skip to content

fix: superposition converges instead of hanging when tlast < tau with auc.type "AUClast" - #588

Merged
billdenney merged 5 commits into
mainfrom
580-superposition-infinite-loop
Aug 20, 2026
Merged

fix: superposition converges instead of hanging when tlast < tau with auc.type "AUClast"#588
billdenney merged 5 commits into
mainfrom
580-superposition-infinite-loop

Conversation

@billdenney

Copy link
Copy Markdown
Member

Fixes #580.

The steady-state loop's zeros guard reset the convergence tolerance every iteration, so structural zeros from auc.type = "AUClast"/"AUCall" tail handling (when a subject's tlast < tau) made n.tau = Inf loop forever. The tolerance logic now distinguishes zeros that can still accumulate (previous behavior kept) from zeros originating after tlast, which can never become nonzero — those converge on the nonzero concentrations with a warning naming the persisting zeros, tlast, and auc.type. A generous 10,000-interval cap errors informatively as a backstop.

Verification: nine reference results (AUCinf steady state, finite n.tau, dose-scaled, multi-subject) are byte-identical before/after, verified by stash-toggling the source; the formerly-hanging Theoph case now returns in ~0.05 s with the expected warning; the cap error path is exercised in tests. Full superposition + TSS test files pass.

🤖 Generated with Claude Code

billdenney and others added 5 commits August 9, 2026 11:44
… auc.type AUClast

With auc.type="AUClast" (or "AUCall"), concentrations after tlast
extrapolate as exactly zero, so when tlast < tau the superposed profile
keeps structural zeros (for example at time 0 and tau) at steady-state.
The zero-concentration guard in the steady-state loop reset the
convergence tolerance whenever any concentration was zero, on the
assumption that all values eventually become nonzero, so
superposition() with the default n.tau=Inf looped forever (issue 580).
Two of the twelve Theoph subjects have tlast < 24, so a
superposition(conc_obj, tau=24, auc.type="AUClast") call on that
dataset hung.

Now, once a completed dosing interval draws all of its concentrations
from times after tlast, any remaining zero concentration can never
become nonzero, so steady-state is assessed on the nonzero
concentrations only, and a warning explains that zero concentrations
remain in the steady-state profile.  Zeros that can still accumulate
from later dosing intervals keep the previous guard behavior, so all
previously-converging cases are unchanged (verified byte-identical
before and after the fix for AUCinf steady-state on all Theoph
subjects, finite-n.tau AUClast, and the documented multi-dose cases).
A generous 10000-interval backstop now raises an informative error
instead of looping forever if steady-state cannot be reached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
superposition.PKNCAconc() runs each subject through parallel::mclapply().
That forks on every platform except Windows, and a warning raised inside a
forked worker never reaches the parent, so the new "zero concentrations
remain" warning was invisible to everyone not on Windows.  CI showed this as
macOS and all three Ubuntu jobs failing on the PKNCAconc expectation with
zero warnings captured while windows-latest passed, because there mclapply()
falls back to lapply().

The worker now muffles its warnings and returns the messages with the result;
the parent re-emits them after collecting the results.

The existing PKNCAconc test cannot detect this on Windows, since it passed
there before the fix, so a test exercises the worker hand-off directly and
runs identically on every platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the hand-written capture helper with purrr::quietly(), which
already returns result/output/warnings/messages and is in Imports.

Also re-emit the captured messages and printed output, not just the
warnings.  Nothing in the superposition call path emits either today, but
quietly() captures all three, so a message() added to the calculation later
would otherwise be discarded on every platform rather than only on the
forking ones -- turning a platform-specific loss into a universal one.

Messages keep their trailing newline, so they are re-emitted with
appendLF=FALSE to avoid a doubled blank line; printed output comes back
newline-stripped and is written with writeLines().

The gate test mocks superposition.numeric() so it produces a warning, a
message, and printed output, then asserts all three reach the caller once
per subject.  Without a mock this cannot be tested, since the real
calculation emits only the warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@billdenney
billdenney merged commit a4537f7 into main Aug 20, 2026
7 checks passed
@billdenney
billdenney deleted the 580-superposition-infinite-loop branch August 20, 2026 23:05
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.

superposition() never returns with auc.type = "AUClast" when a subject's tlast < tau (infinite loop at default n.tau = Inf)

1 participant