Skip to content

fix(indLin): evaluate the indLin() forcing at the current states (#1183) - #1199

Open
mattfidler wants to merge 3 commits into
mainfrom
fix/indlin-indf-state
Open

fix(indLin): evaluate the indLin() forcing at the current states (#1183)#1199
mattfidler wants to merge 3 commits into
mainfrom
fix/indlin-indf-state

Conversation

@mattfidler

Copy link
Copy Markdown
Member

Fixes #1183.

indLin(<state>) <- <expr> supplies the nonlinear forcing f in dy/dt = f(t,y) + A.y, but the generated IndF() took no state vector and codegen skipped the __zzStateVar__ population loop for ode_indLinVec. The state locals kept their NA_REAL declaration, so any forcing referencing a compartment (e.g. Michaelis-Menten elimination) solved to NA under method="indLin".

Correction to the issue text

The states were not read from uninitialized memory. printDoubleDeclaration() (src/codegen.h:358) emits double <name> = NA_REAL;, and prnt_vars(print_double, lhs=0, ...) skips nothing. So the behavior was a deterministic NaN forcing, not UB -- confirmed against installed 5.1.7, where rxSolve(mexp, e, method="indLin")$central came back all NA. The regression test is therefore a plain "not NA, and matches the ODE" check rather than the repeated-solve UB detector the issue proposed.

Changes

  • inst/include/rxode2parseStruct.h:55 -- t_IndF gains const double *__zzStateVar__, mirroring t_ME on the line above.
  • src/codegen.c:397 -- emit the matching signature.
  • src/codegen.c:507 -- drop ode_indLinVec from the guard (keeping ode_past, whose __zzStateVar__ is NULL by design) so the state locals are actually populated. This is the substantive half; the new argument is inert without it. Also covers the legacy $MATF path (src/parseIndLin.h), which shares the gate.
  • src/expm.cpp:387 -- pass the interval-start state yp_, the same buffer meOnly() hands to ME.

Every other t_IndF reference names the type by its typedef, so nothing else needed an edit.

Verification

  • Generated IndF now contains central = __zzStateVar__[__DDT1__]*((double)(_ON[__DDT1__])); ahead of the forcing.
  • Michaelis-Menten forcing vs. the equivalent d/dt() model: no NAs, max abs error 0.0044 at hmax=0.001 against a peak of 69.7. Error falls 3.75 -> 0.25 -> 0.02 as hmax refines, i.e. still first order in hmax (that is what fix(indLin): restore the inductive-linearization iteration #1185 addresses).
  • devtools::test(): FAIL 0 | PASS 79433 (32 warnings, 4 skips, all pre-existing).
  • Cache invalidation needs no manual bump: both edited files are inside the content digest configure turns into rxode2.md5, and the digest was verified to move (0ef602f2... unfixed vs 546084ed... fixed). Same as commit e352215292, which gave t_LAG a state argument and touched no version or md5 seed.
  • Not a downstream ABI concern: neither IndF nor t_IndF is referenced by nlmixr2est, babelmixr2, nonmem2rx or monolix2rx, and neither appears in the inst/include/rxode2ptr.h pointer table, so the append-only rule is not engaged.

Scoped to #1183 only. #1184 (wIndLin) and #1185 (restore the fixed-point iteration) are untouched; #1185 is now unblocked.

`indLin(<state>) <- <expr>` supplies the nonlinear forcing `f` in
`dy/dt = f(t,y) + A.y`, but the generated `IndF()` took no state vector and
codegen skipped the `__zzStateVar__` population loop for `ode_indLinVec`.  The
state locals kept their `NA_REAL` declaration, so any forcing referencing a
compartment (e.g. Michaelis-Menten elimination) solved to `NA` under
`method="indLin"`.

`t_IndF` now takes `const double *__zzStateVar__` the way its sibling `t_ME`
already does, codegen emits the matching signature and populates the state
locals, and `indLin()` passes the interval-start state `yp_` -- the same buffer
`meOnly()` hands to `ME`.  A forcing that references no state is unchanged.

No cache-key bump is needed: `inst/include/rxode2parseStruct.h` and
`src/codegen.c` are both inside the content digest that `configure` turns into
`rxode2.md5`, verified to change with this diff.  Same as commit e352215,
which gave `t_LAG` a state argument.
CodeFactor/lintr flagged three issues in the block added for #1183:
commented_code_linter on the leading comment, object_name_linter on the
`k_depot_central` micro constant, and pipe_consistency_linter on `%>%`.

The matExp model now uses the string form the neighbouring rxS() test already
uses, which keeps the mini-language micro-constant name out of R's namespace,
and the event table is piped with `|>` (the package already depends on
R >= 4.1). No behavior change -- test-mexp-nonmem still passes 103/103.
Copilot's re-review of #1199 noted that rewriting the matExp model as a string
to satisfy object_name_linter dropped brace-front-end coverage for a
state-referencing indLin() forcing (the pre-existing braced case at :66 only
has a constant forcing).

Adds a braced rxode2({...}) variant of the same Michaelis-Menten forcing.  It
uses a single compartment, so no k_from_to micro constant is needed and no
snake_case name enters R -- which is what tripped the linter in the first
place.  Solved against the equivalent d/dt() model.

test-mexp-nonmem: 105 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.

fix(indLin): IndF() cannot see the states it is supposed to be evaluated at

1 participant