Skip to content

Time dependent dur rate f and mtime#991

Merged
mattfidler merged 52 commits intomainfrom
time-dependent-dur-rate-f
Mar 18, 2026
Merged

Time dependent dur rate f and mtime#991
mattfidler merged 52 commits intomainfrom
time-dependent-dur-rate-f

Conversation

@mattfidler
Copy link
Member

No description provided.

mattfidler and others added 9 commits March 1, 2026 10:07
Remove parse-time restriction that blocked state variable references
in f(cmt), rate(cmt), and dur(cmt) expressions. These expressions
now fall through to the default branch which emits correct state
variable references.

Absorption lag-time (alag) restriction is kept since state-dependent
lag is not supported (sort-phase ordering cannot handle it).

Addresses RxODE#216 and RxODE#222.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add double *__zzStateVar__ parameter to generated RxRate and RxDur
function signatures, and populate state variables from __zzStateVar__
instead of NA_REAL for ode_rate and ode_dur modes.

This allows state-dependent rate(cmt) and dur(cmt) expressions to
access actual compartment values at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update t_RATE and t_DUR function pointer typedefs to include a
double *y parameter for passing ODE state to rate/duration functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update getRate and getDur signatures to accept double *y and forward
it to the RATE/DUR function pointers. Update updateDur and updateRate
to pass yp through to getDur/getRate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace rx->ypNA with op->inits in sort-phase calls to updateDur
and updateRate (handleTurnOnModeledDuration, handleTurnOnModeledRate).

Using NA state caused state-dependent rate/dur expressions to return
NA, triggering badSolve=1. Using initial conditions provides valid
non-NA estimates for event ordering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In handle_evid for EVIDF_MODEL_RATE_ON and EVIDF_MODEL_DUR_ON events,
call updateRate/updateDur with the actual solver state (yp) before
reading getDoseIndexPlus1. This ensures InfusionRate and infusion
end-time use the correct state-dependent values at dose time.

Add needSortDefines.h include and forward declarations for
updateRate/updateDur to resolve the forward-reference dependency
with rxode2parseGetTime.h.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ignature and direct-index bug

- Allow alag(cmt) expressions referencing d/dt(state) syntactically by
  removing the ALAG parse-time restriction in handleDdtRhs
- Add double *__zzStateVar__ parameter to generated Lag function signature
  (matching Rate/Dur/F functions); state vars remain NA_REAL in Lag so
  state-dep alag expressions produce a clean runtime 'NA lag' error
- Update t_LAG typedef from 3-arg to 4-arg (adds double *y)
- Update getLag() to accept and forward double *y state pointer
- All sort-phase getLag calls pass rx->ypNA so state-dep lag always
  produces a runtime error (not a crash)
- Fix direct-index bug in handle_evid: ind->idx is an ix-array position
  but updateRate/updateDur expect a direct all_times index; compute
  _directIdx = (ind->idx >= 0) ? ind->ix[ind->idx] : ind->idx before
  calling updateRate/updateDur (fixes the li=0.5 regression)
- Update getRate in handleSS and getLag in handleSS to pass yp

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
State-dependent f(cmt) is now allowed when combined with modeled rate
(rate=-1) or modeled duration (rate=-2) events; update the two
expect_error() calls that tested these cases to expect_no_error().

Also update the test description to reflect the current behavior:
- rate(cmt)/dur(cmt) depending on state still error (unsupported)
- f(cmt) depending on state with fixed rate still errors
- f(cmt) depending on state with modeled rate/dur now succeeds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document the new capabilities added in this feature branch:
- State-dep f(cmt) with modeled-rate/duration events now allowed
- Runtime recomputation of modeled rate/duration with actual ODE state
- Sort-phase uses initial conditions instead of NA for better ordering
- alag(cmt) expressions using d/dt(state) now syntactically accepted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mattfidler

This comment was marked as outdated.

@mattfidler mattfidler changed the title Time dependent dur rate f Time dependent dur rate f and mtime Mar 2, 2026
mattfidler and others added 5 commits March 2, 2026 13:40
- Remove TMTIME restriction in parseDdt.h that blocked state variable
  references in mtime(var) <- expr definitions
- Add double *__zzStateVar__ parameter to generated mtime C function so
  state values are available during model-time computation
- Update t_calc_mtime typedef to accept state pointer (double *y)
- Move calc_mtime() call in iniSubject to after u_inis/memcpy so
  initial conditions are populated in ind->solve before use
- Pre-copy op->inits to ind->solve when no u_inis but nMtime > 0,
  ensuring state-dep mtime expressions get valid non-NA initial values
- Add test: mtime(t1) <- intestine * 0.01 parses and solves correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In handleTurnOnModeledDuration and handleTurnOnModeledRate, use
ind->solve (per-subject initial conditions from u_inis) instead of
op->inits (global all-subject estimates) when op->neq > 0. When
op->neq == 0 there is no ODE state so fall back to op->inits.

iniSubject populates ind->solve via memcpy(op->inits) + u_inis before
sortInd is called, so ind->solve has correct per-subject values at
sort time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Sorted=0

- Add reSortMainTimeline(ind, startI): re-sorts ind->ix[startI..n-1]
  using ind->timeThread as sort key (wraps timsort SORT macro).
- Add recomputeMtimeIfNeeded(rx, ind, yp, nextI): recomputes ind->mtime
  with current state yp and updates timeThread for changed mtime events;
  returns 1 if any mtime value changed.
- Refactor sortInd to call reSortMainTimeline instead of inline SORT.
- In handle_evid at EVIDF_MODEL_RATE_ON / DUR_ON events, set
  ind->mainSorted = 0 (guarded by ind->idx >= 0 to skip extra doses).
- In all 6 solve loops (ind_indLin0, ind_liblsoda0, ind_lsoda0,
  ind_linCmt0H, ind_linCmt0, ind_dop0): add re-sort trigger at top of
  event loop and mtime recomputation after handleEvid1/handleSS.
  The re-sort refreshes timeThread for remaining stop events (using
  getTime_ to include lag correction) and mtime events before calling
  reSortMainTimeline(ind, i).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests verify that:
1. State-dependent modeled rate uses per-subject initial state at sort
   phase, producing different trajectories for subjects with different
   initial conditions.
2. Runtime re-sort after rate recompute places stop event correctly
   (no NA output, no solver errors).

NEWS entries document the three bug fixes: per-subject initial conditions
at sort time, main timeline re-sort after runtime rate/dur update, and
mtime recomputation at dose events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mattfidler and others added 6 commits March 3, 2026 08:39
…branch)

The `ode_lag` branch in codegen.c previously set all state variables to
NA_REAL, causing runtime NA errors whenever a state-dependent alag()
expression was evaluated.  Merge it with the general else-branch so that
Rate/Dur/F/Lag functions all populate state vars from `__zzStateVar__`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five related fixes for state-dependent rate/duration/alag interaction:

1. updateDur/updateRate: store absolute lagged stop time (lagged_start +
   duration) in all_times[stop_raw] instead of unlagged (t + duration).
   This ensures that when alag() and dur()/rate() are both state-dependent,
   the stop event lands at the correct absolute time.

2. getTimeCalculateInfusionTimes: RATE_OFF/DUR_OFF cases now return
   getAllTimes() directly (already the absolute stop time) instead of
   re-applying lag via getLag — which would have double-lagged stop events.

3. getTime__: add RATE_OFF/DUR_OFF guard in update=0 path to likewise
   return getAllTimes() directly.

4. getTimeCalculateInfusionTimes catch-all: replace rx->ypNA with
   ind->solve so sort-time lag uses per-subject initial state.

5. handleInfusionItem: replace rx->ypNA with ind->solve for amt>0 and
   tB computations so constant-alag infusion start/stop times are computed
   with the per-subject initial state rather than a sentinel NA array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…igger

Three changes across all 6 solve loops and one new helper:

1. refreshLagTimesIfNeeded(): new helper that, when needSortAlag is set,
   recomputes timeThread entries for remaining bolus/ON dose events using
   the current state yp via getLag().  Skips RATE_OFF/DUR_OFF (store
   absolute times) and all EVIDF_INF_RATE events (stop key = lagged_start
   + f*dur, not raw_stop + lag — handled at sort time via handleInfusionItem).
   Returns 1 if any time changed; callers set mainSorted=0 to trigger re-sort.

2. xout computation: changed from getTime_(ind->ix[i], ind) (which called
   getLag with rx->ypNA) to ind->timeThread[ind->ix[i]] (precomputed
   correct lag-adjusted value from sortInd / runtime refresh).

3. Re-sort trigger: stop event refresh now uses getAllTimes(ind, _raw)
   instead of getTime_(_raw, ind), since stop events already store
   absolute lagged times after commit fix(getTime).

4. All 6 loops call refreshLagTimesIfNeeded after recomputeMtimeIfNeeded,
   setting mainSorted=0 when lag-adjusted times change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three tests covering the new state-dependent lag functionality:
- Sort phase uses actual initial state (not NA) for alag(cmt) <- state expr
- dur(cmt) + constant alag: stop event placed at absolute lagged time
- Multiple doses: runtime lag refresh produces no NAs across dose events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document four fixes:
- State-dep alag() now uses actual state values (not NA) at sort phase
- dur(cmt)/rate(cmt) stop events store absolute lagged times correctly
  when combined with alag()
- xout uses precomputed timeThread values (correct lag at each event)
- Runtime lag refresh via refreshLagTimesIfNeeded re-sorts timeline when
  state-dep lag values change between events

Note cache invalidation: cached compiled models must be rebuilt after
installing this version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation

1. Skip evid==3 (reset events): getWh(evid=3) gives cmt=-1, which
   causes _alag[-1] array underrun (UB) inside RxLag.

2. Zero ind->curShift before the scan loop, matching the sortInd
   convention.  After a reset event curShift = -maxShift; without
   zeroing, getLag returns rawTime + lag + maxShift instead of
   rawTime + lag, producing a spurious 'changed' flag and a
   corrupted event timeline.

3. Set ind->idx = j (the ix[] position) inside the loop before
   calling getLag/_update_par_ptr.  getValue() in approx.cpp reads
   y[ind->ix[ind->idx]], so the covariate (e.g. lagt) is looked up
   at the sorted position of the dose event being processed — not at
   the position of the preceding observation event (which has lagt=0).

All three changes save and restore the affected fields after the loop
so no side-effects leak into subsequent event handling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mattfidler
Copy link
Member Author

@john-harrold the last commit made me laugh -- it said I fixed everything that I broke, but it had just forgot the context. It was "justifying" itself for only fixing some of the bugs it was introducing.

@mattfidler

This comment was marked as outdated.

@mattfidler mattfidler closed this Mar 10, 2026
@mattfidler

This comment was marked as outdated.

Both liblsoda (C) and dlsoda (Fortran) refuse to integrate when
|tout - t| < 2*UROUND*max(|tout|,|t|) with the error "tout too close
to t to start integration". The prior isSameTime threshold was only
1*DBL_EPSILON, creating a gap where the check would pass the guard but
the solver would reject the step.

For id=509 in nmtest (SS infusion with lag and modeled rate), the
EVIDF_INF_RATE stop-event time computed via handleInfusionItem differed
from the solver's current xp by ~1.5 ULPs at t≈12 and t≈31.28, which
fell in the 1x–2x epsilon gap. Widening isSameTime to 2*DBL_EPSILON
eliminates these spurious "too close" failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mattfidler mattfidler reopened this Mar 12, 2026
State-dependent lag/F/dur/rate/mtime expressions (including those using
d/dt(state) terms) now parse successfully and compile, so the
corresponding tests should expect success rather than failure.

Changes:
- First group (rxode2): mtime(z)=d/dt(x)+3 now compiles fine
- Second group (rxode2parse): {f,F,alag,lag,rate,dur}(depot)=d/dt(central)+3
  and mtime(z)=d/dt(x)+3 now parse successfully

Jacobian-dependent expressions (df(x)/dt(ETA[1])) remain invalid and
their badParse tests are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mattfidler and others added 3 commits March 16, 2026 22:16
Add mtime0[90] to rx_solving_options_ind to hold the initial sort-time
mtime values. recomputeMtimeIfNeeded() now only re-evaluates mtime[k]
when the solver is at ind->mtime0[k] (guarded by isSameTime), then
marks mtime0[k] = R_NegInf to prevent double-fire.

Previously mtime was re-evaluated after every solver step, converging
to a fixed-point T = expr(state(T)). Now it fires at T0 (the initial
sort time computed from initial state) with one optional update when
the solver reaches T0.

Also update test-mtime.R: use sparse sampling grids that exclude mtime
fire times (so the mtime event adds its own time point), and rewrite
test 4 to verify fires at T0 not the old fixed-point Tfp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mattfidler mattfidler requested a review from Copilot March 17, 2026 03:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enables and validates state-dependent expressions for dosing modifiers (alag, rate, dur, f) and model times (mtime) by passing the current ODE state into generated helper functions, recomputing timing at runtime where needed, and re-sorting event timelines to preserve correct temporal ordering.

Changes:

  • Allow parsing and execution of state-dependent mtime(), alag(), rate(), dur(), and f() expressions (including dependence on d/dt(...)).
  • Update solver internals to compute event times from timeThread, recompute lag/mtime at runtime, and re-sort timelines when timing changes.
  • Add extensive test coverage for state-dependent behavior across alag, rate, dur, f, sorting, and mtime.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/testthat/test-state-dep-sort.R Adds regression tests ensuring sort-phase uses per-subject initial state for state-dependent rate.
tests/testthat/test-state-dep-rate.R Adds analytical tests verifying state-dependent modeled rate behavior and infusion stop-times.
tests/testthat/test-state-dep-lag.R Adds tests for state-dependent lag and NA regressions, including multi-dose refresh.
tests/testthat/test-state-dep-f.R Adds analytical tests for state-dependent bioavailability across single/multi-dose scenarios.
tests/testthat/test-state-dep-dur.R Adds analytical tests verifying state-dependent modeled duration and correct stop-times.
tests/testthat/test-parsing.R Updates parsing expectations to allow state-dependent mtime and dosing modifiers.
tests/testthat/test-mtime.R Adds tests for state-dependent mtime scheduling and runtime behavior.
tests/testthat/test-model-rate.R Updates expectation: f(state) now allowed with modeled rate/duration infusions in specific cases.
tests/testthat/test-alag.R Removes a test that previously enforced runtime errors for state-dependent alag.
src/parseDdt.h Removes parse-time restrictions preventing state-dependent mtime/alag/rate/dur/f.
src/par_solve.h Moves calc_mtime timing and adds mtime0 capture + conditional final sortInd.
src/par_solve.cpp Adds runtime recomputation for mtime and lag, timeline re-sort utilities, and fixes an incorrect std::copy.
src/linCmt.cpp Widens isSameTime tolerance slightly.
src/handle_evid.cpp Includes get-time header to enable runtime update helpers.
src/codegen.c Extends generated Lag/Rate/Dur/mtime signatures to accept state array and populates state vars from it.
inst/include/rxode2parseStruct.h Extends function pointer typedefs and adds mtime0 + mainSorted fields.
inst/include/rxode2parseHandleEvid.h Adds runtime update hooks for modeled rate/duration and flags timeline re-sort.
inst/include/rxode2parseGetTime.h Threads state (y) through lag/rate/dur helpers and fixes stop-time to use lagged start.
inst/include/rxode2.h Widens isSameTime tolerance slightly.
R/summary.R Adjusts summary.rxC() to return the C text invisibly.
NEWS.md Documents new/changed behavior for state-dependent timing and parsing.
CLAUDE.md Updates tester workflow commands and adds note on viewing generated C code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

mattfidler and others added 5 commits March 16, 2026 23:12
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mattfidler
Copy link
Member Author

The f(state) <- d/dt(state) needs to bring back a proper parsing error.

@mattfidler
Copy link
Member Author

@copilot modify claude.md for copilot instructions md file

Copy link
Contributor

Copilot AI commented Mar 17, 2026

@mattfidler I've opened a new pull request, #996, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends rxode2’s event-time machinery to correctly support state-dependent dosing properties (alag, modeled rate/dur, f) and model-time events (mtime) by evaluating them against the appropriate ODE state and re-sorting timelines when event times change.

Changes:

  • Update generated model helper callables (lag/rate/dur/mtime) and the solver/event pipeline to pass real state vectors, recompute state-dependent times at runtime, and re-sort the main timeline when needed.
  • Allow additional previously-rejected combinations (notably f(cmt) with modeled rate=-1 / rate=-2) and add .rxLastCompileSuccess() to distinguish parser failures from compiler failures in tests.
  • Add comprehensive test coverage for state-dependent alag, rate, dur, f, mtime, and sorting/resorting behavior; update NEWS and contributor docs.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/testthat/test-ui-rename.R Switch a couple of tests from %>% to base `
tests/testthat/test-state-dep-sort.R New tests exercising per-subject init state in sort and runtime re-sort behavior.
tests/testthat/test-state-dep-rate.R New analytical tests for modeled rate(cmt) depending on state.
tests/testthat/test-state-dep-lag.R New tests for state-dependent alag() (sort-time + runtime refresh) and dur+alag interaction.
tests/testthat/test-state-dep-f.R New analytical tests for state-dependent f(cmt) including multi-dose behavior.
tests/testthat/test-state-dep-dur.R New analytical tests for modeled dur(cmt) depending on state.
tests/testthat/test-parsing.R Track last compile success across expected parser errors; adjust some test descriptions.
tests/testthat/test-mtime.R New tests for state-dependent mtime() scheduling semantics.
tests/testthat/test-model-rate.R Update expectations: allow f(state) with modeled rate/duration; keep other error cases.
tests/testthat/test-alag.R Remove a test expecting runtime errors for state-dependent alag() (now supported).
src/par_solve.h Initialize mtime using per-subject initial state; adjust sorting behavior between solve/LHS phases.
src/par_solve.cpp Add mtime requeue + lag refresh + main timeline re-sort support across all solve loops.
src/linCmt.cpp Relax isSameTime() tolerance.
src/handle_evid.cpp Include getTime header to access updateRate/updateDur helpers.
src/codegen.c Change generated Lag/Rate/Dur/mtime signatures and populate state from provided state vector.
inst/include/rxode2parseStruct.h Add mtime0 and mainSorted; update callable typedef signatures.
inst/include/rxode2parseHandleEvid.h Add sorting-related includes and runtime recompute hooks for modeled rate/dur.
inst/include/rxode2parseGetTime.h Pass state into Lag/Rate/Dur; store absolute lagged stop-times; adjust time retrieval.
inst/include/rxode2.h Relax isSameTime() tolerance.
R/summary.R Make summary.rxC() return the generated code text (invisibly) while printing it.
R/rxode2.R Add .rxLastCompileSuccess() and track compilation success state for tests/debugging.
NEWS.md Document the new/changed state-dependent timing semantics and related fixes.
CLAUDE.md Update local test/check commands to remove stale object files first; add tip for printing generated C.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

mattfidler and others added 4 commits March 17, 2026 21:21
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mattfidler mattfidler merged commit 1d0d883 into main Mar 18, 2026
5 of 8 checks passed
@mattfidler mattfidler deleted the time-dependent-dur-rate-f branch March 18, 2026 14:41
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.

3 participants