Skip to content

Time dependent dur rate f and mtime#991

Open
mattfidler wants to merge 24 commits intomainfrom
time-dependent-dur-rate-f
Open

Time dependent dur rate f and mtime#991
mattfidler wants to merge 24 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
Copy link
Member Author

mattfidler commented Mar 2, 2026

Needs more work;

  • needs to link to current state value not just the initial state value.
  • sort is still not called at run-time (even though claude calls out that it is).
  • mtime also needs to be fixed.
  • Generated code doesn't always produce the right function ABIs missing the state values.

@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
Copy link
Member Author

This has been running in Clade code for about 1 week, I'm assuming it doesn't know how to fix the errors it generated.

Unfortunately, that means this particular feature request is done for now.

@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 2 commits March 12, 2026 07:16
…loop sort order

getSolve(i) uses sorted-position i as buffer index. The solve loop's
updateSolve propagates state through positions 0..n-1; runtime re-sorts
via refreshLagTimesIfNeeded change which raw event is at each position,
but the state chain stays consistent.

Previously, rxode2_df called iniSubject(inLhs=1) which ran sortInd using
initial-state lag values, producing a different sorted order than the
solve loop's final order (which may include runtime lag updates). Reading
getSolve(i) in rxode2_df then retrieved the wrong ODE state for some
observations — manifesting as depot appearing at the wrong time when
alag() depends on a state variable.

Fix: skip sortInd when inLhs==1. rxode2_df then iterates through the
same sorted positions as the solve loop, so getSolve(i) maps correctly.

Also remove debug REprintf calls added during investigation, and add a
numeric correctness test: with constant state_val and alag(depot)<-state_val,
dose at raw_t fires at raw_t+state_val and depot follows 100*exp(-ka*(t-t_fire)).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant