Skip to content

fix: linCmtB(which1 = -3) now handles infusions (#1236) - #1273

Merged
mattfidler merged 1 commit into
mainfrom
inf-1236
Aug 19, 2026
Merged

fix: linCmtB(which1 = -3) now handles infusions (#1236)#1273
mattfidler merged 1 commit into
mainfrom
inf-1236

Conversation

@mattfidler

Copy link
Copy Markdown
Member

Summary

Fixes #1236. linCmtB(which1 = -3) -- the dose-time (moving-boundary)
sensitivity added in #1235 -- previously returned NA_REAL for any individual
whose regimen contained an infusion, because the derivative needs dA/dt
(which includes the infusion rate), and ind->InfusionRate is only maintained
while solving. rxode2_df.cpp's output pass recomputes lhs from the saved
amounts after iniSubject() clears the rates, so the rate read there was
always 0.

  • linCmtB() now caches the live infusion rate per output index while it is
    genuinely solving that index, mirroring the existing fx/Jacobian Alast
    cache (getAdvan(idx)/lc.restoreFx()/lc.restoreJac()), keyed on the same
    !ind->doSS && ind->solvedIdx >= idx condition that already distinguishes a
    fresh solve from a re-query (e.g. the output pass).
  • linCmtBdoseTime() reads that cache back on a re-query instead of the
    by-then-cleared live rate.
  • A steady-state infusion still returns NA_REAL: it establishes its amounts
    analytically (handleSSinf8()/solveSSinf()), and for a one-shot SS dose
    with no following schedule the solver deliberately leaves InfusionRate at
    0 afterward -- the cache-based value there is well-defined but not
    physically meaningful (verified empirically: true steady state should give
    -dA/dt = 0, but reading the cache at that index gave a nonzero answer).
    Steady-state bolus never touches InfusionRate and was already exact.

Both correctness bugs an independent Antigravity/Gemini review raised (a
stale ind->idx sentinel during extra-dose injection; dense dop853
intra-segment observations) were checked against the actual code and do not
apply here: the extra-dose/pushDosingEvent path is reachable only from
inside the steady-state branch of handle_evid, which the new
linCmtHasSsInfusion() guard already refuses regardless of cache state; and
dense output is unconditionally disabled for any model with numLin > 0
(rxData.cpp), i.e. any model that could call linCmtB() at all. Both were
confirmed unreachable by tracing the call graph and by passing tests that
exercise the surrounding code paths (steady-state infusions, method = "dop853").

Test plan

  • tests/testthat/test-lincmt-dose-time-sens.R: infusion regimens (single,
    multiple, duration-specified, oral-into-depot, IV-with-peripheral, mixed
    linCmt() + d/dt(), method = "dop853") match a central finite
    difference to ~1e-10; steady-state infusions (periodic and continuous)
    still report NA.
  • tests/testthat/test-lincmt-dose-time-sens-guard.R (unaffected linCmtB(which1 = -3) assumes every dose shares one alag(): no per-compartment dose-time sensitivity #1237
    guard) still passes.
  • tests/testthat/test-lincmt-solve-sens.R, test-steady-state.R,
    test-event-sensitivities*.R, test-infusion-bolus.R,
    test-lincmt-*.R all pass with no new failures.
  • Full clean rebuild (rm -f src/*.o, required since a shared header
    changed) with no new warnings from the changed files.
  • Reviewed with an independent Antigravity (Gemini) pass; findings
    triaged above.

The dose-time (moving-boundary) sensitivity needs dA/dt, which includes
the infusion rate -- but ind->InfusionRate is only maintained while
solving, so the output pass (which recomputes lhs from the saved
amounts after iniSubject() clears the rates) previously saw rate 0 and
had to refuse with NA_REAL for any individual with an infusion.

linCmtB() now caches the live rate per output index while genuinely
solving it (mirroring the existing fx/Jacobian Alast cache keyed on
ind->idx/ind->solvedIdx/ind->doSS), and linCmtBdoseTime() reads that
cache back on a re-query instead of the by-then-cleared live rate. A
steady-state infusion still returns NA_REAL: it establishes its
amounts analytically and, for a one-shot SS dose with no following
schedule, InfusionRate afterward does not reflect a well-defined
ongoing rate (verified empirically, not just reasoned, that using the
cache there gives a plausible but physically wrong answer).
@mattfidler
mattfidler merged commit 9acc32a into main Aug 19, 2026
9 of 10 checks passed
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.

linCmtB(which1 = -3) returns NA for infusions: InfusionRate is not carried into the output pass

1 participant