Skip to content

eventSens: .rxEventSensCExpr() only translates THETA[n]/ETA[n] from the first vector element, emitting uncompilable C #1196

Description

@mattfidler

Reported downstream as nlmixr2/nlmixr2#408 ("Error building model - food effect not running?").

Summary

.rxEventSensCExpr() (R/eventSens.R) only collects THETA[n]/ETA[n] tokens from the first element of its (vectorized) expr argument. Any index that appears solely in a later element is never rewritten to the codegen local _THETA_n_/_ETA_n_, so raw symengine array syntax leaks into the generated C and the model fails to compile:

error: 'ETA' undeclared (first use in this function)
error: 'THETA' undeclared (first use in this function)

This is a real codegen bug — it reproduces on Linux with a perfectly good toolchain. The reporter in #408 was sent down an Rtools rabbit hole because of the diagnostic wording (filed separately).

Root cause

R/eventSens.R:806:

.toks <- unique(regmatches(expr, gregexpr(paste0(kind, "\\[[0-9]+\\]"), expr))[[1]])

expr is a character vector — one entry per event-sensitivity assignment line — but [[1]] keeps only the matches from element 1. gsub() then substitutes those tokens across all elements, so whatever happens to appear first is translated and the rest silently survive as ETA[n]/THETA[n].

Minimal demonstration against the installed function:

e <- c("exp(ETA[4]+THETA[4])*(FOOD!=0)", "exp(ETA[5]+THETA[5])*(FOOD==0)")
rxode2:::.rxEventSensCExpr(e, character(0))
#> [1] "exp(_ETA_4_+_THETA_4_)*(FOOD!=0)"  "exp(ETA[5]+THETA[5])*(FOOD==0)"
#>                                              ^^^^^^^^^^^^^^^^ not translated

Resulting generated C (from rxode2::rxLastCompile()$c):

void ..._dDur(int _cSub, double __t, double *__zzStateVar__, double *_dDurSave){
  _dDurSave[3] = exp(_ETA_4_+_THETA_4_)*(FOOD!=0);   // OK
  _dDurSave[4] = exp(ETA[5]+THETA[5])*(FOOD==0);     // does not compile
}

Suggested fix

Collect tokens across every element:

.toks <- unique(unlist(regmatches(expr, gregexpr(paste0(kind, "\\[[0-9]+\\]"), expr))))

Verified this produces the correct output for the vector above:

[1] "exp(_ETA_4_+_THETA_4_)*(FOOD!=0)" "exp(_ETA_5_+_THETA_5_)*(FOOD==0)"

Reproducible example

Needs a model where a dosing event modifier (dur(), f(), ...) depends on two or more distinct etas, so that .rxEventSensCLines() emits more than one assignment line for the same buffer:

library(nlmixr2)

d <- nlmixr2data::theo_sd
d$FOOD <- ifelse(d$ID %% 2 == 0, 1, 0)
d$RATE <- ifelse(d$EVID == 1, -2, 0)   # duration dosing

mod <- function() {
  ini({
    tka <- 0.5; tcl <- 1; tv <- 3.5; tD1a <- 0.1; tD1b <- 0.1
    eta.ka ~ 0.1; eta.cl ~ 0.1; eta.v ~ 0.1; eta.D1a ~ 0.1; eta.D1b ~ 0.1
    prop.sd <- 0.1
  })
  model({
    is_fed <- (FOOD != 0); is_fast <- (FOOD == 0)
    ka <- exp(tka + eta.ka); cl <- exp(tcl + eta.cl); v <- exp(tv + eta.v)
    D1 <- is_fed * (exp(tD1a + eta.D1a)) + is_fast * (exp(tD1b + eta.D1b))
    dur(depot) <- D1
    d/dt(depot)  = -ka * depot
    d/dt(center) =  ka * depot - cl/v * center
    cp <- center/v
    cp ~ prop(prop.sd)
  })
}

nlmixr2(mod, d, est = "posthoc")   # Error: error building model

Scope

Narrowed by variant testing:

variant result
two-eta expression in dur() fails
two-eta expression in f() fails
same expression used only in the ODE / cp (no event modifier) OK
single-eta dur() / f() / lag() OK

So it requires at least two distinct etas contributing separate lines to the same event-sensitivity buffer. The some etas defaulted to non-mu referenced warning that accompanies these models is a red herring — mu-referencing both branches on simple lines yields byte-identical C and fails the same way.

Workaround (for reference)

Restructuring so every emitted line contains all the indices avoids it, e.g. folding the branches inside a single exp():

D1 <- exp(is_fed * (tD1a + eta.D1a) + is_fast * (tD1b + eta.D1b))

This is algebraically identical when the indicators are mutually exclusive, and it compiles and fits.

Environment

  • R 4.6.1, Linux, gcc 14.2.0 (working toolchain)
  • rxode2 5.1.7, nlmixr2est 7.0.2, nlmixr2 6.0.0
  • Reporter in Remove cerr_stream #408: Windows, R 4.6.1, rxode2 5.1.6, nlmixr2est 7.0.2

Regression test suggestion

A test asserting that generated event-sensitivity C contains no bare ETA[/THETA[ would cover this directly, plus a unit test on .rxEventSensCExpr() with a length-2 input.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions