rxode2 5.1.7 (development version)
New features
rxSolve(zeroVarParamHandle=)says what happens whenparamssupplies a value for an omega/sigma item whose variance is zero (sayeta.base ~ fix(0)). Such an item is dropped from the matrix that is simulated from and given to the model as a literal zero instead, which discards the supplied value:"warn"(the default) does that and says so,"ignore"does it silently, and"keep"uses the supplied value.rxSolve(safeLog=2)floorslog(0)atlog(.Machine$double.eps)the waysafeLog=TRUEdoes, but treats a negative argument as a domain error and returnsNaN.safeLog=TRUE(the default) andsafeLog=FALSEare unchanged. This is for a hand-written likelihood takinglog()of a parameter that must stay positive: undersafeLog=TRUEan invalid negative value returns a large finite number, which-log(sigma)turns into a reward of roughly+36per observation instead of a rejection.A function that produces models can now name them.
rxModelName()is ans3generic dispatched on the name of the function that was called, so arxModelName.readModelDb()method names every modelrxode2(readModelDb("PK_1cmt"))builds (here,"PK_1cmt") instead of leaving it named after the text of the call. The method is given the call and its (unevaluated) arguments, matched to the argument names of the function being called; a call with no method keeps the default name.rxModelVars(m)$indLin$wIndLinnow reports the states whoseindLin(<state>) <- <expr>forcing references a compartment, rather than always being empty. It is worked out by replaying the parsed assignments and forcings in source order, so hand-writtenmatExp()models are covered as well as converted ones, and a forcing that reaches a compartment only through an assigned variable (cp <- central/20) counts too; a forcing built only from parameters or covariates (e.g.indLin(Gc) <- Gprod) stays unflagged, as does one whose variables were reassigned to something state free before it reads them. A forcing inside anif/whilemay not run, so it adds to what the forcings before it established rather than replacing them. The entries are the 0-indexed positions in$state, named with those states.rxModelNameLhs()registers the name an assignment is making, for assignment operators likenlmixr2save’s:=(fit := nlmixr2(...)). It names the model when the model expression itself names nothing – an anonymous model function, or a call with norxModelName()method – so the model is built with that name rather than none.rxModelNameFromExpr()exposes the whole naming sequence for packages that capture a model expression withsubstitute().
Bug fixes
Model interface
ui$modelNameis now always a single character string, as it was always documented to be. It came fromas.character()of the substituted model expression, which returns one element per part of a call, sorxode2(readModelDb("PK_1cmt"))gavec("readModelDb", "PK_1cmt")and an anonymous model function gave a four-element vector including the deparsed body. The name is the tidied first deparsed line of the expression instead: a symbol keeps its name and a call becomes its own text (readModelDb("PK_1cmt")), unless arxModelName()method orrxModelNameLhs()names it better. Names wider than 60 characters are truncated. An anonymous model function names nothing, so itsmodelNameisNULLrather than a piece of its body. Values assigned by other packages (or read from models saved by earlier versions) are also collapsed to a single string on access (#1019).A trailing
#comment on anini({})line may now contain a double quote or a backslash. Such a comment is promoted to alabel()call when the model is parsed with its source refs intact, and while the label text was escaped correctly it was then interpolated into the replacement argument ofsub(), which parses backslashes and strips one level. The generatedlabel("fixed to a "small value"")did not parse, so the model failed with a bare syntax error pointing into regenerated text rather than at the offending source line. Because the promotion only runs when source refs are kept, the same model resolved fine without them – so a package build could be green while a test suite run withkeep.source = TRUEwas red on the identical file (#1195).A trailing
#comment on anini({})line keeps itslabel()when the comment itself contains a#, including the common## commentform. The code portion of the line was matched greedily, so on a line with two#it ran on to the last one and left the first sitting in the generated code, where it commented out thelabel()that had just been appended. The label was dropped silently – the model still parsed and built, it simply lost the label (#1205).
Solving
rxSolve()no longer returns silently wrong, run-to-run varying results when a multi-rowparamsdata.frame (one parameter set perid) is combined withomega = NAorsigma = NA.c()on a data.frame drops the data.frame class and yields a ragged list – the per-id columns keep their length while the appended zeros have length one – which was then read out of bounds while solving, so the random effects thatomega = NAfixes at zero were filled from unrelated memory instead. With eight or more subjects this changed the solved values on every solve of identical input, occasionally to non-finite ones. A multi-rowparamsmatrix hit the same problem from the other side:c()dropped itsdim, soomega = NA/sigma = NAfailed outright with “The following parameter(s) are required for solving”.omega = NAon a model with no between subject variability (which failed with “invalid ‘times’ argument”) andsigma = NAon a model with no residual error are now the no-ops they should be.An
omega/sigmaentry whose variance is zero (sayeta.base ~ fix(0)) is now supplied to the model as a literal zero whenparamsis a matrix, as it already was for a data.frame or a named numeric vector. Such an entry is dropped from the matrix that is simulated from, so a matrixparamsreached the solver without it andrxSolve()failed with “The following parameter(s) are required for solving”. A matrix that did supply the item kept its value where a data.frame had it replaced by zero; both replace it now, andzeroVarParamHandle=chooses (see New features).A
paramsmatrix that supplies a random effect is now recognized as supplying it, so that effect is no longer simulated on top of the supplied value.rxSolve()decided whetherparamsalready had a random effect withnames(params), which isNULLfor a matrix – its names are the column names – so the answer was always “no”. The supplied column was silently ignored and a random draw used in its place: witheta.base = 100supplied for every subject, a data.frame gave101 102 103 ...and a matrix gave0.92 1.84 2.67 .... There was no warning, and the values look reasonable unless you know what they should be.Supplying a value for one random effect no longer stops the others from being simulated. A supplied effect is dropped from the
omegabefore solving, but the subset that drops it took a single remaining effect down to a scalar, whosedimisNULL, andall(NULL == c(0L, 0L))isTRUE– so the wholeomegawas dropped and the remaining effect was neither simulated nor supplied (The following parameter(s) are required for solving: eta.b). The matchingsigmacode already guarded this.
Compilation
The statement form of
ifelse()–ifelse(cond, stmt, stmt), where each branch is a statement rather than a value – now compiles anywhere in a model. Its handler appendedif (to the code buffers without first clearing the preceding statement’s text, so the generated C ran the two together (kin=3if (t<2) {) and only a model whose first statement was anifelse()compiled. The construct now emits and normalizes exactly like the equivalentif (...) {...} else {...}, so it round-trips throughrxNorm()and translates for symengine derivatives (sensitivities, FOCEi) the same way (#1211).rxCompile()now re-parses the model it is handed whenever the parser’s current model is a different one. Code generation reads the parser’s global model state, and the old guard only checked whether some model was loaded, so a re-compile requested while an unrelated model was parsed wrote that other model’s C under this model’s name and handed back its model variables. Building a model withrxode2()never hit this (it parses, then compiles immediately), but re-loading one whose.sois gone did – as when a saved fit is restored in a new session, since its DLL lived in the original session’stempdir(). Such a fit came back solving a different model, e.g. a restored SAEM fit failing with “The following parameter(s) are required for solving: eta.v, eta.cl”.
Delay differential equations
rxOptExpr()no longer fails on apast(state, tau)whose delay duration is an expression rather than a name or a number (past(G, exp(lT)),past(G, tau*2)), which raisedunsupported lhs in optimize expressionand printed the duration into the middle of the progress bar. This madeoptExpression=TRUEunusable for such a delay differential equation; it now optimizes, and the duration follows the same common subexpression itsdelay()terms do, so the history stays matched to them.A generated delay differential equation model (
rxode2(..., calcJac=TRUE),calcSens=, or an nlmixr2 estimation model) now resolves thepast()delay duration the same way it resolves the history itself. A duration written as an intermediate (T <- exp(lT)) was emitted verbatim while everydelay()had its duration inlined, so the generated model named a duration nodelay()used any more andrxSolve()rejected it withduration 'T' does not match any delay(...). This also covers a duration or a history written withTHETA[n]/ETA[n], as every mu-referenced model is: they were left unresolved, and an unresolved history additionally emitted no per-parameter sensitivity pre-history at all.
Matrix exponential / inductive linearization
The
Al-Mohymatrix exponential evaluated the wrong Pade numerator below degree 13. The coefficients depend on the degree, and the routine read a fixed table – the degree-13 row – and truncated it, which is not the degree-p numerator. The answer stayed convergent but only to a few1e-12where every other backend reaches machine precision, and only against an exact solution is that visible. The row is now built for whichever degree was selected.The
Al-Mohymatrix exponential returned a wrong answer for a very large matrix norm. The squaring count was returned as the factor2^sin anintand clamped so it could not overflow, but clamping caps the scaling while leaving the norm untouched, so degree-13 Pade ran far outside its range and produced a plausible finite number: a one-compartment model with a rate constant of1e20returned5.1e-08for a quantity that underflows to zero. The squaring count is now carried as a count.rxSolve(indLinMatExpType=)now defaults to"Al-Mohy"rather than"expokit". With the degree bug below fixed, all four backends agree to solver tolerance and take the same steps on every problem tested, and"Al-Mohy"is the cheapest per exponential: about 4-5% on a Michaelis-Menten population and 34% on a stiff van der Pol one, where an exponential-Rosenbrock step rebuilds its operator every step and the exponential cache cannot help. On a linear model the difference is unmeasurable, the cache serving nearly every call. Results move in the last digits, as any change of exponential kernel does; passindLinMatExpType="expokit"to keep the previous one.rxSolve(indLinMatExpType="Al-Mohy")chose its Pade degree and its scaling inconsistently, which could return a silently wrong answer or a solve that never finished. The scaling came from the Al-Mohy-Higham threshold table, whose entries each belong to one specific degree, while the degree itself came fromindLinMatExpOrder(default 6) – so any matrix with a 1-norm up to the table’s largest threshold, 5.37, was evaluated at degree 6 with no scaling at all where the table calls for degree 13. Both are now taken together from the norm, as thetaylorbackend already did. A two-compartment linear model returned1.8e-06against about1e-11for every other backend, and one van der Pol subject atmu = 95.7866under an exponential Rosenbrock step ran for over 390 seconds – a bad exponential can make the error estimate unsatisfiable, so the step controller shrinks the step without limit instead of failing – where the other backends took 0.03 s. Both now agree with the other backends, and on a 50-subject stiff populationAl-Mohygoes from not completing in 418 s to 0.92 s, the fastest of the four. ConsequentlyindLinMatExpOrderno longer applies toAl-Mohy; it still applies toexpokit.rxSolve(<function or rxUi model>, method="indLin")failed with “Can only parse scalar data”. With the defaultuseLinCmt=TRUEthe ODE was first rewritten intolinCmt(), leaving a model withlinCmt()pseudo-compartments and nod/dt()for the matrix-exponential conversion to work from. That rewrite is now skipped whenmethod="indLin"is requested, so such a model integrates its own rate matrix rather than being replaced by the analytic solution.A steady-state infusion (
ss=1orss=2with arate) gave a diverging solve undermethod="indLin". Its solver was the only one that never drained the pending-dose queue, which is where the infusion’s off record is held, so the steady state itself was found correctly and the infusion was then left running for the rest of the timeline. Steady-state boluses and ordinary (non-steady-state) infusions were unaffected.method="indLin"is substantially faster. The ODE-to-matExp()conversion ran on everyrxSolve()call although it is a pure function of the model, and cost several times the solve it was preparing for; it is now done once per model (options(rxode2.indLinConvCache=FALSE)restores the old behaviour). The matrix exponential itself was recomputed on every fixed-point pass even though the rate matrix cannot change between them, and identical exponentials are now reused (RXODE2_INDLIN_NO_EXP_CACHEdisables this). Together these are several times faster on a nonlinear model and more on a linear one; no result changes.-
rxSolve(indLinJac=)chooses where the forcing Jacobian comes from whenmethod="indLin"needs one, which is only under"newton","exprb"and"exprb32"– Picard needs none, so a non-stiff model under the default scheme never forms one."symbolic"uses the model’s own analytic Jacobian, which thematExp()conversion already emits asdf()/dy()lines, and costs no extra forcing evaluations;"fd"central-differences the forcing at2nevaluations."auto"(the default) takes the symbolic one when the model carries it and falls back to finite differences otherwise, which is what happens abovegetOption("rxode2.indLinJacMaxStates")states where the emission is skipped.On cost the two are a wash at compartmental sizes – within about 25% of each other either way from 3 to 16 states, with no consistent ordering, and the symbolic emission adds a fraction of a second once at model build. The reason
"auto"prefers symbolic anyway is exactness rather than speed: an exponential Rosenbrock step’s order conditions assume the Jacobian is exact, and on a stiff van der Pol the symbolic one delivered a smaller error for the same work. rxSolve(indLinIteration="exprb32")adds the Luan-Ostermann third-order exponential Rosenbrock pair. Its embedded second-order member is"exprb"itself, so the two differ by a computable quantity and it sizes its step from that rather than from the extrapolation column – which is what"exprb"has to use and why"exprb"is held at fourth order. It is NOT the default and is not selected by"auto": measured at matched delivered accuracy it wins only on a stiff problem at a loose tolerance, by about 1.2 to 1.7 times, and loses elsewhere, badly so on a non-stiff population. The reason is the cost of the third phi function, which needs an augmented matrix three rows wider than the plain step; at the small dense systems compartmental models produce, widening the exponential costs more than the extra order saves.rxSolve(indLinIteration=)chooses howmethod="indLin"solves each relinearization step:"picard"(the previous and only behaviour),"newton", or"exprb", an exponential Rosenbrock step that does not iterate at all. Which is cheapest depends entirely on the problem – on a non-stiff model the iteration never limits the step and Picard is cheapest, while on a stiff one it is the only thing limiting it – so"auto"(the default) starts on Picard and switches only once steps are actually being cut for non-convergence. A model that never needs a Jacobian therefore never forms one. On a van der Pol oscillator integrated over a full relaxation period at matched accuracy this is about 39 times faster than Picard atmu = 100(593 relinearizations against 45,913) and about 426 times faster atmu = 1000(581 against 1,001,968), which takes a full cycle at that stiffness from impractical to routine; a Michaelis-Menten model is left on Picard and unchanged. With both schemes given their best extrapolation level, that division holds: Picard is ahead on a non-stiff model at working tolerances and the exponential Rosenbrock step is ahead on a stiff one, and at a delivered error of 1e-8 on a non-stiff model."exprb"runs at fourth order or above, since its error estimate comes from the extrapolation column and the third-order one is not reliable enough to size a step from.method="indLin"extrapolates further when it pays. Each relinearization step could previously be raised from second to third order by running it also at half length; it can now use a Romberg column of up to four entries (h,h/2,h/4,h/8) for up to fifth order, at 3, 7 and 15 fixed-point solves per step.indLinRichardson="auto"(the default) raises the level as the step the controller settles on crosses each break-even."always4"and"always5"force the new levels. On a 200-subject Michaelis-Menten solve this halves the time atatol=rtol=1e-8, and on a single subject at1e-12it is over seven times faster than the third-order step.-
indLinRichardson="auto"keeps the extrapolation level it has earned for the rest of the subject, instead of dropping back to second order at every observation and re-earning it. A step that only needs a few relinearizations never reached the break-even, so a model observed at a dozen times ran most of its profile at second order however low the break-even was set: on a 200-subject Michaelis-Menten solve the default took 0.626 s to reach a delivered error of 1e-4 where forcing the fourth-order column took 0.098 s. It is now 0.112 s, and 0.341 s rather than 0.646 s at 1e-6. The break-evens themselves are also measured rather than derived, and differ between the fixed-point and exponential-Rosenbrock steps, whose costs per level differ. A model whose forcing reads no state is unaffected: the matrix exponential is already exact for it and there is nothing to extrapolate.Two consequences for anyone reading step counts. A loose tolerance now does use extrapolation – it turns out to pay there too, taking fewer steps than the second-order path rather than the same number – and the delivered error at a loose tolerance is much smaller than before, so a ratio of errors across a tolerance sweep is no longer a way to read off the order of the default path. Use
indLinRichardson="never"for that. rxSolve(indLinMatExpType="taylor")adds a Taylor scaling-and-squaring matrix exponential, which needs no linear solve; its degree is chosen per call from the norm. It is as accurate as the default"expokit"on every problem tested, including a linear system where"Al-Mohy"at its default order is six orders of magnitude worse. The default is unchanged: profiling puts all of LAPACK at roughly 3% of a solve, so avoiding the linear solve does not pay on nonlinear problems.$counts$dadtand$counts$jacreport the matrix exponentials computed and reused for amethod="indLin"solve. Both counters were previously unused on this path.method="indLin"no longer uses the R API from inside the parallel solve. The Al-Mohy matrix-exponential backend took its workspace fromR_alloc, and the default expokit backend warned throughRWarnon a singular Pade denominator; neither is safe from a worker thread. The singular case also used to continue with an unfinished matrix, and now reports and returns zeros.method="indLin"no longer throws from inside the parallel solve. Two code paths in the inductive-linearization solver raised an R-level error from a worker thread, which crashes the session rather than reporting an error; both now report through the usual per-subject error flag.An
indLin(<state>) <- <expr>forcing that references a compartment is now evaluated at that compartment’s current value. The generated forcing function took no state vector, so the compartment kept itsNA_REALdeclaration and any state-dependent forcing (e.g. Michaelis-Menten elimination,indLin(central) <- -vmax*central/(km+central)) solved toNAundermethod="indLin". A forcing that references no state is unchanged.method="indLin"iterates again, so it is inductive linearization rather than one relinearization perhmaxsubstep. Within each substep the matrix and the forcing are rebuilt at the latest iterate while propagation restarts from the substep’s starting state, until the states reported byrxModelVars(m)$indLin$wIndLinstop moving to withinatol/rtol. Plain Picard iteration only barely contracts once the substep is comparable to the forcing’s own time scale – a Michaelis-Menten forcing with no linear elimination sits right at the stability boundary, oscillating for ~1e5 passes – so each step is relaxed by a secant estimate of the iteration’s contraction ratio. Relaxation does not move the fixed point, so the converged answer is the undamped one. Models with no forcing, or with a forcing that reads no state, keep the single-pass path and are unchanged.Converting an ODE model to
matExp()form (rxToIndLin(), and thereforerxode2(..., indLin = TRUE)andmethod="indLin"’s auto-conversion) now puts the nonlinear part of a right-hand side into anindLin()forcing instead of into a rate constant. A rate constant that reads a compartment is not a rate constant – the matrix exponential assumes the rate matrix is constant in the states – and burying the nonlinearity there meant the solver could not iterate it. Michaelis-Menten elimination now converts toindLin(central) <- -vmax*central/(km + central)with an empty rate matrix rather than tok_central_output = vmax/(km + central). This also removes a rate constant that was singular when the compartment was empty. Because these models now reach the iterating solver path, they are far more accurate: a one-compartment Michaelis-Menten solve that was about 70% off at the default settings is now within about 0.01%.rxIndLinStrategy()andrxIndLinState()no longer affect the conversion, since no way of factoring a multi-state product yields a state-free rate constant; both are kept so existing code keeps working.rxSolve(indLinRichardson=)Richardson-extrapolates eachmethod="indLin"relinearization step, raising it from second to third order: the step is run once whole and twice at half length, and since a second-order step has a quarter the error at half the length, the two answers together cancel it. That costs three fixed-point solves per step instead of one, so it only pays once the tolerance is tight enough that taking far fewer steps outweighs it."auto"(the default) decides per interval: after the first accepted step it compares the step the controller settled on against what is left of the interval, and switches when finishing at that step would take more than 27 of them – the break-even point, since a second-order step needingNsteps becomes a third-order one needing aboutN^(2/3)at three times the cost each."always"/TRUEand"never"/FALSEforce the choice. On a Michaelis-Menten model the switch-over lands at aboutatol=rtol=1e-5; at1e-8"auto"takes 544 steps where the second-order step takes 12,865.rxSolve(indLinStepSearch=)andrxSolve(indLinMaxIter=)control the fixed-point iterationmethod="indLin"runs inside each relinearization step.indLinStepSearch="secant"(the default) estimates the iteration’s contraction ratio from the last two residuals and relaxes by it, which costs nothing extra and is what makes an oscillating iteration converge at all;"exact"spends one more matrix exponential per iteration to locate the residual-minimizing factor in closed form;"none"is plain, undamped Picard. All three converge to the same answer – relaxation does not move the fixed point – so the choice trades iterations against work per iteration; on a Michaelis-Menten model the default is about five times faster than"none".indLinMaxIter(default 20) caps the iterations per step; running out is not an error, since the iteration contracts in proportion to the step and the solver reads it as a step that is too long.A
matExp()rate constant that depends on a compartment is now a parse error rather than a silently invalid model. The matrix exponential is only correct when the rate matrix is constant over the step, so ak_from_tothat reads a state breaks the method’s central assumption; the error names the constant and the compartment it reaches and points atindLin(), which is where a state-dependent term belongs and where the solver can iterate it. Models built byrxSensMatExp()are exempt for now: their sensitivity blocks are built out of rate constants throughout, and rewriting that generator is separate work.-
method="indLin"chooses its own relinearization step for models with a state-dependentindLin()forcing, instead of subdividing each interval evenly byhmax. The forward answer (matrix built at the step’s starting state) and the converged backward answer bracket the truth symmetrically, so their difference is a local error estimate that costs nothing extra; the step is then chosen from it the same way the other adaptive solvers choose theirs.atol/rtolandmaxstepsnow control the accuracy of these models andhmaxonly bounds the step. An iteration that will not converge is treated as a step that is too long and is retried shorter rather than reported, so stiff forcings that previously failed outright now solve; non-convergence is reported only once the step or the step budget runs out.$counts$slvrreports the relinearization steps actually taken, where it used to read zero. One consequence worth knowing: as with every adaptive method, the solution is now a piecewise function of the parameters, which adds a little noise to finite-difference gradients taken through it.Each step also advances on the average of the two answers the error estimate is built from, whose leading errors are equal and opposite, so what is propagated is second order where either alone is first. This costs nothing – both are already in hand – and it is what brings the step count down: the error now falls roughly in proportion to
atol/rtolrather than to their square root, so the work needed for a given accuracy grows like1/sqrt(error)instead of1/error. On the Michaelis-Menten model above, matching the accuracy the old scheme delivered at its default now takes about a twentieth of the steps, and the gap widens the more accuracy is asked for.The forward answer is evaluated at the step’s starting time as well as its starting state, so that it and the converged answer really are the two ends of one quadrature. Evaluating both at the step end cancels the state error but leaves the explicit-time error, which silently dropped any forcing that reads
tback to first order: on a Michaelis-Menten model with anexp(-t)input the error atatol=rtol=1e-9falls from 4.6e-03 to 1.1e-07.
Installation / linking
- On Windows,
STAN_THREADSand the TBB link are kept when building againstRcppParallel>= 6.2.0, which shipstbb.dll/tbbmalloc.dllwith the package again.configurenow decides whether to strip the TBB flags by looking for the TBB library inRcppParallel’slibdirectory rather than by the shape of the-Lflags it emits, so the TBB-less build introduced in 5.1.6 is used only withRcppParallel6.0.0–6.1.1, which shipped no TBB library on Windows. (The 5.1.6 release notes had this backwards:RcppParallel6.2.0 restored the TBB library on Windows rather than dropping it.)
rxode2 5.1.6
CRAN release: 2026-08-04
New features
Added the event (“jump”) sensitivity shape to rxode2’s linked function-pointer API, so a downstream package can install a model’s shape from C++ without an R round trip:
rxode2EventSensLoadFull()(all six dims, where the olderrxode2EventSensLoad()omittednParam3/useCalcJac),rxode2EventSensGetDims()/rxode2EventSensSetDims(),rxode2EventSensSetActive(),rxode2EventSensDeactivate(), andrxode2EventSensShapeSize()/rxode2EventSensShapeSave()/rxode2EventSensShapeRestore(), which snapshot and reinstall a whole shape (dims plus the model’s dosing-derivative function pointers) through a caller-owned opaque buffer. This lets several peer models with different shapes be solved through one shared solve pool, installing each batch’s shape and restoring the previous one afterwards.Added
setIndCmt()to the function-pointer API, the writer counterpart ofgetIndCmt(), so a downstream package can re-base the per-observationCMTcovariate without reaching intoop->cmtCov/ind->cov_ptrby field.getIndCmt()reports a missingCMTasNA_INTEGER, distinct from the1it returns for a model with noCMTcovariate at all (where every observation really is compartment 1), so a caller re-basing the column can leave missing rows alone.
Bug fixes
Compilation cache
- The compiled-model cache key now includes
eventSensCode, so two builds of one model whose generated C differs – event sensitivities on vs off – no longer share a.c/.sopath in the rxode2 cache directory. Previously the second build overwrote the first while any model object created earlier kept resolving its entry points by name, so it silently began executing the other variant: the declaredlhswidth was unchanged but most slots were never written, andrxSolve()returned whatever was left in the buffer. A model with no event-sensitivity code keeps exactly the prefix it had, so no existing cache entry is invalidated (#1171).
Dependencies
- The suggested
xgxris now required to be>= 1.1.6. Itsxgx_scale_x_log10()/xgx_scale_y_log10()return theggplot2scale itself from that version on, rather than a length-one list wrapping it.
Installation / linking
- Fixed the Windows build against
RcppParallel6.2.0, which no longer ships the TBB library there (6.0.0 still built).configurealready dropped the-ltbb/-ltbbmalloclink flags when they are unavailable, but still compiled with-DSTAN_THREADSand-DRCPP_PARALLEL_USE_TBB=1, which pullsstan::math’sad_tape_observer(atbb::task_scheduler_observer) into the objects and left undefined references totbb::detail::r1::observeat link time. Those defines are now dropped together with the link flags,stan-math’sinit_chainablestack.hppis kept out of the build, and the main thread’s AD tape – which that observer would otherwise have created – is constructed insrc/linCmt.cppinstead (by Jeroen Ooms).
Solving
Fixed heap corruption when
simeta()/simeps()resample inside a solve. Both go throughsimvar(), which reseeded its threefry stream withgetRxSeed1(); unlessrxSetSeed()had been called that draws from R’s own random number generator, which allocates R objects and can trigger a garbage collection. Doing so from an OpenMP worker thread corrupted R’s heap, and the session then failed later in an unrelated place (cannot get data pointer of 'NULL' objects,'rho' must be an environment,corrupted double-linked list, or a segfault). The in-solve resample now draws from a per-thread engine seeded on the main thread and touches no R API.The
simeta()/simeps()resample no longer replays the simulated parameters. Its engines are keyed off a threefry draw rather than off therunif()-derived seed handed out at solve setup, whichrxSolve()goes on to reuse for the simulatedomega/sigmadeviates; a resampledetacould therefore come out exactly equal to another subject’s simulatedeta.
rxode2 5.1.5
CRAN release: 2026-07-28
New features
library(rxode2)is faster..onLoad()no longer callsrequireNamespace()on the suggested packages (pillar,tibble,arrow,dplyr,nlme,units,digest) before registering their S3 methods. The registration helper already installs anonLoadhook when the other namespace is not loaded yet, so the eager loads only added startup cost; the methods are still registered at the same point in time from the user’s perspective.-
rxControl(sigdig=)now derives the ODE solver tolerances with one solver-independent formula – the same for stiff, non-stiff and auto-switching solvers. Thertolexponent ISsigdigandatolsits three orders below it:rtol = 10^(-sigdig)andatol = 10^(-sigdig-3). The sensitivity tolerances match the main solve (rtolSens = rtol,atolSens = atol), since gradients and covariances are built from them, and the steady-state tolerances run one order looser (ssRtol = ssRtolSens = 10*rtol,ssAtol = ssAtolSens = 10*atol). This matches hownlmixr2estderives solver tolerances from its optimizationsigdig, so the samesigdigmeans the same thing whether it is used for estimation or for a plainrxSolve().sigdigremainsNULLby default and continues to have no effect unless you pass it, so solves that do not namesigdigare unchanged.Two notes for callers who do pass it. First, the mapping is keyed to
sigdigas a request for that many significant digits, which for smallsigdigis looser than what the previous symmetricatol = rtol = 0.5*10^(-sigdig-2)gave: atsigdig = 4,rtolmoves from5e-7to1e-4, which is also looser than the1e-6defaultrtol(atolmoves the other way, from5e-7to1e-7). If you were usingsigdigto tighten a solve, raise it or setatol/rtoldirectly. Second, each tolerance is resolved independently and only when you did not supply it, so an explicitatol/rtoloverrides the main solve but does not propagate to the sensitivity or steady-state tolerances – set those directly if they should change too. The SUNDIALS public headers are now vendored into the package (
src/sundials_inc/) alongside the already-vendored SUNDIALS C sources, and theLinkingTo: sundialrdependency has been dropped. This guarantees the vendored sources always compile against headers from the same SUNDIALS release, instead of silently drifting when sundialr updates its bundled SUNDIALS (#1155). The vendored include is injected viaPKG_CPPFLAGSso it precedes the LinkingTo include flags; otherwise the older SUNDIALS copy bundled inside StanHeaders would shadow it.rxSerialize()now writes the base R types only ("xz","bzip2","base");qs2is no longer a write format.rxDeserialize()still readsqs2/qdata-serialized data and base91-encoded strings, so objects stored by earlier versions remain readable. Test data was converted from.qs2to.rds.
Bug fixes
Serialization
-
qs2moved toImports.rxDeserialize()used it without declaring it anywhere, and because the call sites named the package as a string, the dependency was invisible toR CMD checkas well. Environments that build their library from the declared dependency graph could therefore end up withoutqs2and fail to read objects stored whileqs2was an allowedrxode2.serialize.type– for example theorigDataslot of fits saved by earlier versions.qs2is now only ever read, never written.
Error models / transformations
The first and second derivatives of the Yeo-Johnson transform (
rxTBSd()andrxTBSd2()) had the wrong sign for negative values whenlambdawas exactly2. Thereyj(x) = -log(1 - x), so the derivatives are1/(1 - x)and1/(1 - x)^2, both positive; the special case returned them negated, which also contradicted the general formula’s limit and made the derivative discontinuous inlambdaat2. Since Yeo-Johnson is monotone increasing, its first derivative must be positive everywhere.-
Review of the fix above found further errors in the same transform family (all pre-existing, none introduced by that fix):
-
rxTBSd2()returned a wrong second derivative for thelogittransform (an algebra error in the closed form) and for the composedlogit + yeoJohnson/probit + yeoJohnsontransforms, where the chain rule used the first Yeo-Johnson derivative in place of the second and dropped the inner-transform curvature term. -
rxTBSi()did not invert the composedlogit + yeoJohnson/probit + yeoJohnsontransforms: it applied the forward Yeo-Johnson transform (or skipped it entirely) instead of the inverse, sorxTBSi(rxTBS(x))did not returnxforlambda != 1. This affected simulation back-transforms of those error models. - The
lambdagradient of the transform log-Jacobian (powerDL, used by estimation routines) was wrong on the negative Yeo-Johnson branch (-log1p(x)instead of-log1p(-x),NaNforx < -1), returned a spurious0at exactlylambda == 1forboxCox/yeoJohnson, was missing theprobit + yeoJohnsoncase (returnedNA), and returned a spuriouslog(x)(instead of0) for the lambda-freelnormtransform. The log-Jacobian itself (powerL) clamped the wrong term in itslogitguard, giving an unprotectedlog(0)at the upper bound. - For
boxCox/lnorm,rxTBSd()andrxTBSd2()returned the clamp constantsqrt(.Machine$double.eps)itself forxat or below the clamp instead of clampingxand evaluating the derivative formula, making the derivatives discontinuous (and ~15 orders of magnitude too small) at the boundary. The clamp now feeds the usual formula, matching how every other transform in the family handles the guard.
-
Estimation / symengine translation
- The symbolic derivatives of the relational operators (
>,<,>=,<=) are now centered on the discontinuitya == b: theatanh(2*tol - 1)shift that placed the smoothed nascent-delta bump ata - b ~ +/-0.46was removed. Since the forward pass evaluates relationals as hard booleans, the shifted bump gave sensitivity/exact-gradient consumers (e.g. FOCEI’s analytic gradient paths) a spurious derivative in a band next to the threshold; the centered rule makes the derivative consistent with the forward value. This also makes the first derivatives ofabs(),min(), andmax()exact away from the boundary (#1159).
Solving
Fixed heap corruption when
OMP_NUM_THREADSis set below the number of cores a solve asks for – as it is on CRAN check machines, which setOMP_NUM_THREADS=2. The extra-dosing pools were sized once when the package loaded, fromomp_get_max_threads()(which honorsOMP_NUM_THREADS), but they are indexed by the solving thread id, which is bounded byop$cores;rxSolve(cores=)overridesOMP_NUM_THREADSthrough OpenMP’snum_threadsclause. Every thread past the firstOMP_NUM_THREADStherefore wrote off the end of those arrays, corrupting the heap and crashing the session later in an unrelated allocation. The pools now grow to coverop$coresat solve setup, like the other per-thread pools.Fixed an out-of-bounds thread index that could segfault a solve. The internal thread id used to slice the per-thread solving buffers was not bounded by the number of threads those buffers were allocated for (
op$cores). A larger id read past the end ofgInfusionRate[]– an array of pointers – and the resulting garbage pointer crashediniSubject(); the flat per-thread arrays were silently overrun in the same way. The id is now clamped to the last valid slot, matching whatrx_get_thread()already did.Fixed a cross-subject leak in batched multi-subject
linCmt()solves: the per-thread inter-event amount buffer was never cleared between subjects, so withcores < nSubevery subject after the first on a thread could start from the previous subject’s compartment amounts (surfaced by a modeledalag()) (#1153; by Hidde van de Beek).delay()/past()models containing anif/elseblock failed to solve withunexpected 'else': the DDE helpers parsed therxNorm()text directly, which puts}andelseon separate top-level lines; the normalized text is now parsed wrapped in a{ }block. In addition, apast()history inside anif/elsebranch is now rejected with a clear error (it was invisible to validation), and delay-duration root-variable resolution now sees assignments made insideif/elsebranches (#1151).
Event tables
ev$idon an event table now returns the per-rowidcolumn (matchingas.data.frame(ev)$id) instead of the unique subject ids, so idiomatic subsets likeev[ev$id == 3, ]and per-subject assignments likeev$wt <- 50 + 20 * ev$idno longer silently recycle a short vector; the unique ids remain available viaev$env$ids.[.rxEtnow errors on a logical row index whose length matches neither 1 nor the number of rows, and columns assigned withev$col <- value(new covariates as well as previously hidden canonical columns such ascmt) now round-trip throughas.data.frame(ev)(#1154).Columns assigned explicitly on an event table (
ev$wt <- 70) are now shown in the tibble printed byprint(ev), inev$get.EventTable(), and in the compressed preview printed forev$get.dosing()/ev$get.sampling(), matchingas.data.frame(ev). They were kept and used when solving, but never displayed, so they looked like they had disappeared (#1154).ev$get.dosing()andev$get.sampling()now print the same columnsprint(ev)does regardless of how the event table is stored internally. Previously an un-grouped table printed every internal column, including hidden ones such aslow/high/durand covariates that only rode along with an imported data frame, while a compressed one printed only the shown columns. Every column is still present on the returned data frame for programmatic access, a column added or renamed on the returned frame still prints, anddplyrverbs turn it back into a plain data frame the way they already did forrxEt– including the column verbs (select(),relocate()), which subset with[rather than going throughdplyr_reconstruct().Explicitly assigned columns now survive a round trip through a data frame.
as.data.frame()tags them in arxEtExtraColsattribute thatet(),as.et()and$import.EventTable()/$importEventTable()read back, soet(as.data.frame(ev))keeps showingwtinstead of demoting it to a hidden imported covariate. A data frame built by hand carries no tag, so its covariate columns stay hidden as before.as.data.frame()on an event table still hides covariate columns that simply rode along with an imported data frame (et(data)), while showing columns assigned explicitly on the event table (ev$wt <- 70, #1154). The covariate is still used when solving. Showing every non-canonical column broke code that imports events and then joins its own covariates back ontoas.data.frame(ev), since the join producedwt.x/wt.yand the model parameter disappeared.
Model compilation
- The
parsed_md5of a model no longer depends on how many models were built before it in the session.linCmtSenswas folded into the hash but only assigned after the model was parsed, so the first build of a session hashed with an unset value and every later build hashed with the previous call’s value. Because the compiled DLL is named fromparsed_md5, the same model could get two different cache keys (and hence a redundant recompile) depending on build order. It is now set before the parse.
Installation / linking
RcppParallelis now a runtime import (added toImportswith animportFrom), so its shared library is loaded into the process beforerxode2’s.rxode2links againstRcppParallel(-lRcppParallel); withRcppParallelonly inLinkingToits DLL was not guaranteed to be loaded first, so on Windowslibrary(rxode2)could fail withLoadLibrary failure: The specified module could not be found. This surfaced with RcppParallel 6.0.0, which statically links TBB and no longer ships thetbb.dllstub that previously happened to pull the library in.On Windows with RcppParallel >= 6.0.0 (which statically links TBB through Rtools and no longer loads
tbb.dll), the stale-ltbb/-ltbbmallocflags and the-Lpath to RcppParallel’s old dynamic TBB directory thatStanHeaders::LdFlags()still emits are stripped at configure time, so the rxode2 DLL no longer records an unresolvable runtime dependency ontbb.dll. The strip is keyed to that stale-L<RcppParallel/lib>signature, so a future StanHeaders that emits corrected flags – or a user-supplied TBB viaTBB_LINK_LIB/TBB_LIB– is left untouched (#1161).The vendored SUNDIALS
*NewEmptyconstructors now allocate withcallocinstead ofmalloc, so any struct fields added by a newer SUNDIALS release are NULL (and safely ignored) rather than uninitialized (#1155).
rxode2 5.1.4
CRAN release: 2026-07-20
Bug fixes
Model piping
- Model piping no longer shares the
metaenvironment by reference between the original and the piped model..newModelAdjust()assigned the previous model’smetaenv directly (to retain sticky items), so both models shared one env – including the cached simulation model ($meta$.simModelBase). Whichever model was solved first cached its simulation model for both, so a piped model could silently drop an appended compartment/state (e.g. anonmem2rximport:mod %>% model(d/dt(AUC) <- f, append=TRUE)) or the original model could silently gain the piped model’s states/estimates. The meta env is now copied via.copyEnv()(which drops.simModelBase), so each model keeps its own cache.
Compilation
- Silenced the CRAN
-Wlto-type-mismatchwarnings seen with LTO/gcc builds. TherxSolveWarnPush()forward declaration insrc/init.cwas missing the variadic...of its definition, and the ODEPACK/DLS001/common block was declared with two inconsistent (but memory-equivalent) layouts across the LSODE/LSODA step routines. Both are now declared consistently; the fixes are layout-preserving and the Fortran solvers produce identical results.
rxode2 5.1.3
CRAN release: 2026-07-19
New features
rxOptExpr()gainschunkLinesandparallel, to optimize a large machine-generated model (a sensitivity- or Jacobian-augmented model) in contiguous cost-balanced chunks rather than in a single pass.Delay differential equations:
delay(state, T)evaluates an ODE state att - T(Monolix semantics), withpast(state, T) <- exprdefining the pre-history. Delayed states are interpolated from the solver’s dense output; delay models default to the"dop853+ros4"composite and cap the step size to the smallest delay. The dense-output/history machinery is adapted from the ‘dde’ package (Rich FitzJohn, Wes Hinsley, Imperial College), whose authors are added as contributors.Forward sensitivities for delay models, so
delay()models estimate with gradient-based methods such as FOCEi. Parameter-dependent delays are supported at first order (rxDelayD()); second/third order are generated for constant delays (rxDelayD2()/rxDelayD3()) and rejected for parameter-dependent delays (use a numeric or Gauss-Newton Hessian there).Many new ODE solver methods: a large suite of explicit Runge-Kutta tableaus (orders 3-14), stiff Rosenbrock and implicit Runge-Kutta methods (
"ros43","ros6","radauiia5","gauss6","sdirk43","backwardEuler", …), symplectic steppers, SUNDIALS CVODE ("cvode", linear solver selectable viacvodeLinSolver=), and LSODE/BDF. Implicit methods auto-generate an analytic Jacobian. New helpersrxIsStiff(),rxIsNonStiff(),rxIsImplicit(),rxIsDense(), andrxIsAutoSwitch()classify methods; see the new “ODE solvers” article.AutoSwitch composite methods written
"primary+secondary"(e.g."dop853+ros4"): a non-stiff primary with reactive fallback to a stiff secondary, in both the standard and dense-output paths.Adjoint sensitivity solving:
rxSolveAdjoint()andrxSolveAdjointRk4()return the samerx__sens_<state>_BY_<param>__output as forward sensitivities via a backward sweep. Exact discrete adjoints exist for the one-step methods ("s"suffix, e.g."dop853s"),"liblsodaadj", and"cvodesadj", including event jumps (dose/reset/replace/multiply), modeledalag/rate/dur, and steady state. Stiff adjoint and forward-sensitivity solvers integrate the augmented system with its analytic Jacobian.Jump sensitivities for dosing events (based on https://github.com/dkaschek/EventSensitivities), replacing finite differences as the default (
rxode2.eventSensoption:"jump","fd","both"). Hybrid jump sensitivities are used for matrix exponential andlinCmt()models (up to 3rd order for the ODE/matrix exponential cases).Automatic conversion of linear ODE models to
linCmt()at solve time (rxSolve(..., useLinCmt=TRUE), the default), passing detected PK parameters explicitly. Handles a central sub-system with an output-only peripheral observable; systemslinCmt()cannot represent stay explicit ODEs, and a conversion that will not compile falls back to the ODE (only rxode2).Adaptive dosing helpers (
bolus(),infuse(),replace(), etc.) now work insidelinCmt()and mixedlinCmt()+ODE models, with Jacobian handling of the dosing events;odeToLin()preserves and renames them when converting.linCmt()sensitivity (linCmtB) solves now run in parallel across subjects on the default forward-mode AD Jacobian path (linCmtSensType="AD"), which is stack-local with no shared Stan arena. The reverse-mode AD ("ADr") and finite-difference paths remain single threaded.Inductive linearization and matrix exponentials rewritten with a more NONMEM-like interface (automatic ODE->syntax translation retained) and symbolic-differentiation gradients.
Added a forward automatic-derivative linear compartment model.
ar(cor)residual term simulating continuous-time AR(1) residuals for normal, t, and cauchy error models, addable per endpoint alongside any transform;coris in[0, 1)and the lag correlation decays ascor^(time gap)(Karlsson, Beal and Sheiner 1995). Estimation is supported in nlmixr2est (nlm and focei families).lag0()/lead0()/diff0(): likelag()/lead()/diff()but return0instead ofNAwhen there is no prior/following record. A calculated variable may now reference itself throughlag()/lag0()/diff()(a first-order recurrence); a non-lag self-reference is still a required input parameter.rxOmegaVarCovDeriv(): non-CholeskyOmegapath returningOmega^{-1},log|Omega|, and their first/second derivatives with respect to each free variance-covariance element.rxExpandSens3_()generates analytic third-order forward sensitivity equations;.rxSens()gained avars3argument.For downstream packages:
rxSetSolveAtolRtol()/rxGetSolveAtolRtol()in the C function-pointer API, andsetRxThreadId()so a package can drive the per-subject solve from its own OpenMP team.rxTest()test blocks now muffle stray progress messages (e.g. “calculate sensitivities”); setoptions(rxode2.test.verbose = TRUE)to see them. Messages asserted withexpect_message()are unaffected.coef()methods forrxUimodels (and model functions). By defaultcoef()returns the fixed-effect (theta) estimates;coef(model, level = "omega")returns the random-effect variability matrix andcoef(model, level = "all")returns both.nlme::fixef()continues to return the fixed effects.
Bug fixes
The C accessors exposed through the function-pointer API (
getRxNsub(),getSolvingOptions(),getSolvingOptionsInd(), and the otherrx_solve*accessors) no longer segfault when handed aNULLor uninitialized solve. They fall back to the global solve; a scalar counter/flag accessor (nsub,nall,nobs,npars, …) simply reports zero before any solve, exactly as before, so downstream code that probes those counts at load time keeps working (for example babelmixr2’s PopED integration, which queries them from.onLoad). An accessor that must dereference a per-subject record (getSolvingOptionsInd()) instead raises a normal catchable R error stating that the solving environment is not set up, rather than dereferencing aNULLpointer and crashing the R process. This hardens downstream packages that call an accessor before their solve pointer has been populated (for example a cold firstnls/nlmfit innlmixr2est).A Jacobian entry
df(state)/dy(THETA[n])ordf(state)/dy(ETA[n])(a bracketed parameter reference, which the grammar accepts) no longer segfaults. The synthetic_THETA_n_/_ETA_n_symbol was never registered, so its index stayed-2and the model validator readtb.ss.line[-2]out of bounds. This crashednlm/FOCEi fits that re-parse their generatedcalcJacmodel (whose parameters areTHETA[n]) in the residual/table step – notably for a delay-differential-equation model whose delay parameter appears in a product of delayed states.past(state, tau)on a state with nod/dt(state)now reports that cleanly instead of corrupting the heap. The error path appended nothing to the message buffer and then trimmed a trailing',that was never written, moving the write offset before the start of the buffer; the damage surfaced as adouble free or corruptionabort on a later, unrelated parse rather than at the offending model. The message now names the property ('past(G)' present, but d/dt(G) not defined), and a property with no message branch can no longer underflow the buffer.rxOptExpr()no longer fails on a model that usespast(state, tau)and is long enough to be optimized in chunks. Apast()line only parses in a chunk that also holds the matchingd/dt(), and sensitivity augmentation appendspast()after everyd/dt()– so it reliably landed in a chunk of its own. It is now disguised for the duration of the optimization like any other compartment-scoped left-hand side, and restored byte-exactly afterwards. Together with the fix above this unblocks estimating a non-constant-history DDE (e.g. the rheumatoid arthritis model of Koch et al. 2014, J Pharmacokinet Pharmacodyn 41:291-318, Example 6).rxAppendModel()now warns (instead of erroring) when the appended models have no variables in common, so the combined model is still returned; usecommon=FALSEto suppress the warning (#520).rxFixPop()no longer tries to literally substitute a fixed mixture proportion (mix()). A mixture proportion must stay a named model-block variable, so substituting its value made the re-parse throw frommix()(“the probabilities in a mixture must be in the model block …”); a downstream caller wrappingrxFixPop()intry()leaked that error to the console during otherwise-successful mixture fits. Fixed mixture proportions are now excluded from the substitution.Tests that use datasets from the suggested
nlmixr2datapackage (theo_sd,warfarin,nmtest) now guard their use withskip_if_not_installed("nlmixr2data"), so the test suite runs cleanly whennlmixr2datais not installed (#95).
Estimation / symengine translation (rxFromSE())
Convert raw R comparison/logical operators (
>,==,&, …), not only theirrxGt()/rxEq()symengine forms; fixes “user function ‘>’ requires 0 arguments” in FOCEi models with inter-occasion variability (nlmixr2/nlmixr2#390).Recognize bare relationals on the second conversion pass of a
Subs()over aDerivative(); unblocks FOCEi IOV models that also have a between-subject eta on a parameter without IOV.The numeric-constant canonicalization now evaluates operands in
baseenv()only and guards zero-length results, fixing an “argument is of length zero” error and silent substitution of user-workspace variables (#1109).A trig function (
sin/cos/tan) whose argument is a compound expression divided by something (for examplesin(2 * 3.14 * (time - mtime1) / period)) no longer drops its whole argument. The division branch fell through without returning when the numerator was not a single token, so the argument becameNULLand the emitted C code wassin()– which failed to compile with “too few arguments to function ‘sin’”. Such models (for example an enterohepatic gallbladder model with a sinusoidal release) now build and fit (nlmixr2/nlmixr2est#513).
Model parsing / mu-referencing
- Summing two or more population parameters in an expression that has no random effect (for example a combined residual error
W <- sqrt(sigma.1. + sigma.2.)) is no longer misreported as “2+ single population parameters in a single mu-referenced expression”. That check now fires only for a genuine mu-referenced expression (one that also contains an eta), and the message names the parameters that were actually summed instead of the first parameters in the model (#471).
Delay models
calcJac=TRUErewriting (also used by the stiffros4/dop853+ros4path) no longer breaks delay models declaring literalTHETA_n_/ETA_n_parameters: constant~intermediates stay bound, the literal names are restored, andpast()history lines are re-emitted.A state read by
delay()is always kept as a real ODE, so delayed states named like sensitivities (rx__sens_*) keep their definingd/dt()and can use the stiff/dense composite directly.Delay models whose analytic Jacobian cannot be generated now fall back to
dop853(dense) instead ofliblsoda, which recorded no delay history and silently returned pre-history values.-
An lhs reading
delay()is now reported correctly in the output data frame (#1140). The dense delay history was freed at the end of each subject’s solve, so the post-solve lhs recalculation returned the constant pre-history- at every record even though the delayed value drove the ODE. The history is now kept until
rxSolveFree()releases the subject, which also plugs a leak on the discrete-adjoint (rk4s) path where it was never freed.
- at every record even though the delayed value drove the ODE. The history is now kept until
linCmt() models
Fixed a compartment-indexing bug where a model with both an error model and an in-equation compartment reference (e.g.
Cp <- peripheral1 / vp) read an unwritten slot.tad(<state>)/tlast(<state>)no longer returnNAor the wrong value when the model also declares an extracmt()for an algebraic observable (nlmixr2est#685).The automatic
linCmt()conversion no longer fires on a nonlinear model whose nonlinearity is written through a state-derived observable (e.g. Michaelis-Menten viaCc <- central / vc).The automatic
linCmt()conversion no longer changes results when the event data addresses a compartment (in a dose or an observation record) by the name of an ODE compartment the conversion renames (e.g. an ODEcentrecompartment addressed asCMT = "centre", which the conversion renames tocentral). Such a solve now falls back to the original ODE model instead of routing the record nowhere and returning all-zero predictions.Fixed the automatic
linCmt()conversion cache reusing the first model’s initial estimates for a later model that shares the samemodel({})equations but has a differentini({})block, which made structurally identical models with different parameters return identical predictions.Fixed the string form of the compartment argument in the adaptive dosing helpers (e.g.
bolus(50, cmt = "depot")).
Solving
Zero the LSODA solver work memory on allocation (
alloc_mem,callocinstead ofmalloc). The shared work block (Nordsieck historyyh, Jacobian workspacewm,acor/savf, …) was left uninitialised and parts are read before the integrator writes them on some paths (e.g. a first stiff/BDF step at an extreme point), making a solve non-deterministic. Surfaced by valgrind as reads of uninitialised LSODA memory inside FOCEi/impmap inner solves, and downstream as an occasional blown-up importance-sampling fit run after a prior (parallel) fit. Solving is otherwise unchanged.lag()/diff()(andfirst()/last()) previously returned a constant instead of the prior record’s value for calculated variables and time-varying covariates; they now read the prior record (NAon each individual’s first record) and work through the estimation/symengine path. Onlylag(x, 1)/diff(x, 1)are supported for calculated variables.Bug fix for
mix()models andiCovmodels.The
rxMemoryEstimate()RAM detection no longer calls the defunctutils::memory.limit()(which warned on every Windows solve); total RAM is now queried natively in C (GlobalMemoryStatusExon Windows,sysctlon macOS,sysconfon Linux) and available memory reuses the allocator preflight estimate (rxAvailableMemoryBytes()). This also drops thememusesuggested dependency and the shell-command fallbacks.Fixed out-of-bounds heap reads (AddressSanitizer-confirmed; results unchanged):
rxSolve()parameter setup when subjects share one event table in annsim > 1sorted solve;syncIdx()dose-index lookup;cvPost()with a 1x1omega;linCmt.hlinCmtStan2ssInf8;etTran()combineDvid;rxDerived()derived1.
rxode2 5.1.2
CRAN release: 2026-06-02
geom_cens()/stat_cens()no longer emit “Ignoring unknown aesthetics” warnings when censoring aesthetics are mapped. Documentation corrected to describe the two supported lowercase forms:lower/upper(both required) orcens(with optionallimit). The two forms cannot be mixed,lowerandupperare now required together, andlimitwithoutcensis rejected rather than silently ignored.Checks for
is.loaded()before loading a rxode2 model. This helps fix the m1 ODR issue shown in nlmixr2est.Moved
dim.rxEt()here instead of in nlmixr2est
rxode2 5.1.1
CRAN release: 2026-05-28
Various low level fixes to allow
nlmixr2estto have parallelized focei.Parallelized the
rxode2data.frame creation.Added parallel solving
miraifor clusters and HPC support.Added out of memory solve using
arrow/duckdb. These out-of-memory (rxSolveOom) solves behave like a standard solved object: it prints the$paramsand$inits(mirroring therxSolveconsole output), supports$,head(),nrow(),ncol()/dim()and the usualas.data.frame()/as_tibble()/as.data.table()coercions, and exposes the per-subject parameter table and initial conditions that are now persisted alongside the chunked data. A DuckDB query layer over the parquet chunks is used for lazy access (head(), single-column extraction, schema) when available. The chunks can also be queried lazily withdplyr(viaas.arrow()orarrow::to_duckdb()) so that filtering and aggregation are pushed down to the on-disk chunks and a possibly out-of-memory result never has to be fully materialized. The storage/query engine can be pinned with therxode2.oom.backendoption ("auto","duckdb","arrow"or"rds"); the option is also forwarded to parallel (mirai) workers.Use ALTREP for
id,sim.id, repeated simulation event columns (evid,cmt,ss,amt,rate,dur,ii,time), covariates and kept variables when blocks are identical across simulations; falls back to filled out columns when runtime event mutation is detected (evid_()push growth / per-individual event reallocation). Also factors cannot currently be represented by altrep, so they are forced to be fully represented.Change compile flags and compiler directives for rxode2 models to speed up how they run.
Have a pre-allocated context pool for lsoda in both liblsoda and lsoda (faster because memory doesn’t need to allocated and deallocated so often)
Change OMP scheduling to dynamic to try to help load-balance the ode solving per subject.
Simulation normal random numbers before integrating them into your solve.
Add
evid_()function to allow arbitrary doses and observations in a rxode2 model.Add
splitBolus()function to split or relocate doses in the final output. This is done at translation time (but is respected byevid_()) so in general is a bit faster then arbitrary doses in an estimation step fornlmixr2Add
%%operator to valid rxode2 syntaxCreate per-individual ODE solving tolerances for use in focei.
Fix potential security and memory-management issues that could lead to crashes or undefined behavior including integer overflow
Change
dop853to allow per state tolerances and parallel solving likeliblsoda.Change
dop853to be able to usedense=TRUEfor the 8th order dense polynomial interpolation between dosing events.Now
dop853can be parallelized per thread.Change mtime state-based dosing to use less memory.
Add
plogis()translation insiderxode2to it’s c-basedexpit()functionsRefactored
et()to be mostly in R, fixing many issues (#722 , #725, #858, #732, #723, #721, and #724) and allowing dosing/sampling windows to useii,addlanduntil(realized immediately)Add
linToOde()convertlinCmt()models to ODEs.Fix IOV simulation issue observed in #982.
More easily identify initial conditions (#948)
Fix sensitivities in the
linCmt()that did not match the ODE (#1018, #1012)Added in-solve addition of observations (
obs()), bolus dosesbolus(), infusion dosesinfuse()orinfuseDur(), system resetsreset(), compartment replacementreplace(), multiplicaton eventsmultiply(), and phantom/transit compartment eventsphantom(). For more granular control you can also useevid_().Refactor string comparison in
rxode2so that it is actually doing an integer comparison when running the ODE solving routine (simulation and estimation) instead of using a string comparison. It makes using strings like (sex == “male”) run faster.Add
rxMemoryEstimate()andrxMemSummary()to estimate the amount of memory that is required for a rxode2 solve.Add
tolFactor, a per individual change of the tolerances to be used in solving. This is used have individualized tolerances fromnlmixr2est.Add
serializeFileas an option to save the rxode2 C fitting data and then restore as needed.Add out of memory solve capabilities
rxode2 5.0.2
CRAN release: 2026-03-20
Allow state-dependent
dur(),rate(),alag(),mtime()now allow states to modify their behavior. The state value at the time of the event is used to calculate any changes.Fix: all six ODE solve loops now use precomputed
timeThreadvalues for event times instead of recomputing viagetTime_()withypNA, preventing NA propagation for any state-dependent lag scenario.Export the internal
.rxGetSeed()and.rxSetSeed()for use in thenlmixr2savepackage.Bug fix for
.copyUi()with the new format (5.0+) of rxode2 ui modelsWith new versions of R,
getOption()is no longer a bottleneck, so syncing to local variables is no longer done internallyAllow transforms to return
NA.Drop
magrittrand use|>instead of%>%in the examples (requires R 4.1)Change default model serialization to
bzip2and move binary code generation inside of C.Fix where getting seed saves/modifies the RNG scope, as well as a bug fix for restoring the random seed state
rxode2 5.0.1
CRAN release: 2025-12-09
Change random number generation to always return doubles internally as well as no longer take a rxode2 individual structure, this is inferred by the thread number.
Change string representation of model variables to internal binary C code (to avoid macOS M1 sanitizer issues with strings).
Allow user to change the internal serialization type with
options("rxode2.serialize.type"); Currently can be one of “qs2”, “qdata”, “base”, “bzip2” and “xz”. This option must be set beforerxode2is loaded, once loaded it keeps the option initially set. This is set toxzwhich is from base R, but could be sped up with either"qs2"(more future proof) or"qdata"(a bit faster).Removed lsoda
CDIR$ IVDEPdirective, as requested by CRAN.
rxode2 5.0.0
CRAN release: 2025-11-28
Better error for
tad(depot)whenlinCmt()doesn’t include a depot compartment.Remove
qsdependency; For rxode2 ui objects, use lists instead of serialized objects. The internal C++ code still generatesqs2sterilization objects (#950)Fixed translation for censoring/limit to account for a possible
CMTvariable before theCENS/LIMITcolumn (#951, #952)Added
dmexpit()for getting the diagonal Jacobian.Added special handling of
mixestandmixunif.
rxode2 4.1.1
CRAN release: 2025-10-08
Stacking for multiple-endpoint
ipredSimnow matches multiple-endpointsim; Issue #929Fix occasional
$propsthat threw an error with empty properties (when using properties liketad0()); Issue #924Allow mixture models
mix()to be loaded withrxS()as a step to support mixtures in nlmixr2’s focei; Issue #933.Identify the correct transformation type for
iovvariables (#936)Fix multiple compartment simulation edge cases where simulations were not being performed (#939)
When referencing
cmtin models, the variable is forced to beCMT(related to #939)Added ability to use
mixestormixunifto preserve the selected mixture estimates when performing a table step for a nlmixr2 mixture model (#942)
rxode2 4.1.0
CRAN release: 2025-08-29
Change rxui
$evaluation when completing in rstudio, fixes strange calculations popping up inrstudio(#909)Add orphan
rxode2model unloading when usingrxUnloadAll(), and change the return type to always be a boolean.Add
assertRxUiIovNoCorto assert IOVs have no correlations in them.Handle the levels for inter-occasion variability in the ui better (#614)
Create a new function
mix()that will allow mixture models to be simulated in preparation of mixture support innlmixr2. This allows mixture models to be specified as:v = mix(v1, p1, v2, p2, v3)where the probability of havingv=v1is modeled byp1,v=v2is modeled byp2, andv=v3is modeled by probability1-p1-p2.Created new functions
mlogit()andmexpit()to convert probabilities used in mixture models to log-scaled values.mlogit()converts the probabilities to log-scaled values (using root-finding) andmexpit()converts the log values into probabilities. The equation for the conversion of log to probabilities isAdded new assertion
assertRxUiNoMixwhich throws an error when a mixture model is present (iemix())Fix for label processing when calling
rxode2(uiModel)
rxode2 4.0.3
CRAN release: 2025-07-24
- For CRAN’s m1 ASAN checks of nlmixr2est, loading and unloading the same dll or by deleting the dll and recreating the exact same code, and then loading the dll will cause the ASAN check to flag an odr violation. Because of this, a mechanism to not unload dlls has been added. This allows the next version of
nlmixr2estto not have issues with Mac m1 san checks.
rxode2 4.0.2
CRAN release: 2025-07-21
At the request of CRAN, be a bit more careful so that names are not duplicated. Now include the md5 hash, a global counter and random 4 digit and number combination. In addition add the name of the original function so it will be easier to debug in the future.
Fall back to data.frame
rbindwhenrbind.rxSolve()fails
rxode2 4.0.1
CRAN release: 2025-07-17
Add the ability to use
rbindfor solvedrxode2frames.Fix
LTOissue for_rxode2_calcDerived
rxode2 4.0.0
CRAN release: 2025-07-16
Add more information errors about
NAs during solving.Fix
rxDerived()for mixed vector and non-vector input.Fix model variables for
alag(cmt)when they are defined befored/dt()orlinCmt()Just in time use of
state.ignorein the model variables, fixes negative length error observed in #857.Fix steady state bug with time-varying covariates. Now the covariates are inferred at the time of the steady state (instead of searching through the subject based on the projected time).
Rework the linear solved systems to use the wnl solutions, and threaded linear systems solve (for non-gradient solutions). This new method closes a variety of linear compartment model bugs (#261, #272, #441, #504, #564, #717, #728, #827, and #855)
-
Added new types of bounds for event tables:
3 point bounds
et(list(c(low, mid, high)))when specified this way, they will not change. Perfect for use withbabelmixr2’sPopED(#862, #863, #854)Intervals simulated by normal values instead of uniform. In this case the first seen interval will be 3 elements with NA at the end
et(list(c(mean, sd, NA), c(mean, sd))), and the other elements can simply be 2 declaring thec(mean, sd)Of course the uniform windows of
et(list(c(low, high)))still workCurrently these different types of windows cannot be mixed.
Add ability to pipe a list or named numeric as an eta with
%>% ini(~eta)Added a fix for event tables where expanding IDs in non-sequential order. In particular if the first ID is not the minimum ID when expanding the first event table, the smallest ID was not in the output table. Now the smallest ID is in the event table. (Fixes #878, #869, #870)
Added ability to pipe
ini()orlotri(), or any other expression that can be converted to an ini withas.ini(). Also allowsini()expressions to be converted to lotri withas.lotri(). Fixes #871Added new type of variability expression for simulation and estimation with focei and likelihood related methods:
+var(). This changes standard deviation parameters to variance parameters.Added new type of endpoint expression for focei estimation
+dv(). This only transforms the data and not the predictions. I can only see it being useful in model linearization.Bug fix for parameters that are in both input (
$params) and output ($lhs) that respects the order of the$lhsdeclaration (Fixes #876)Add
rxFixResto literally fix the residual estimates in a model (#889)Now modeled duration of 0 is treated as a bolus dose (#892)
rxode2 3.0.3
CRAN release: 2024-12-15
Add
logit/expitnamed expressions, that islogit(x, high=20)becomeslogit(x, 0, 20)in ui models.Updated random ui models like
rxnorm(sd=10)to accept complex numeric expressions likerxnorm(sd=10+1).Updated random ui models to accept complex non-numeric expressions like
rxnorm(sd=a+b)Rework the
tad()and related functions so they use the same interface as compartments (this way they do not depend on the order of compartments); See #815. For mu-referencing, Also allow dummy variables to ignore state requirements (iepodo(depot)in a single line will not error when parsing mu-referenced equations).Add
getRxNparsto api. This allows the development version ofbabelmixr2to better check what model is loaded and unload/reload as necessary.Add
rxUdfUiControl()to rxode2 user function to get control information from something likenlmixr2Bug fix for tracking time after dose when dosing to 2 compartments occur at the exact same time (#804, #819)
Change
transit()model so that it usestad0(),podo0()and related functions for a bit more stable simulation and estimationFix compile flags to work with BH 1.87 (#826)
rxode2 3.0.2
CRAN release: 2024-10-30
Bug fix for
api, the censoring function pointer has been updated (#801).Query
rxode2.verbose.pipeat run time instead of requiring it to be set before loadingrxode2.Have correct values at boundaries for
logit,expit,probit, andprobitInv(instead ofNA). For most cases this does not break anything.Add a new style of user function that modifies the
uiwhile parsing or just before using the function (in the presence ofdata).Used the new user function interface to allow all random functions in
rxode2ui functions to be named. For example, you can userxnorm(sd=3)instead of having to userxnorm(0, 3), althoughrxnorm()still works.
rxode2 3.0.1
CRAN release: 2024-09-22
- Explicitly initialize the order vector to stop valgrind warning (requested from CRAN)
rxode2 3.0.0
CRAN release: 2024-09-18
Breaking Changes
The model properties was moved from
$paramsto$propsso it does not conflict with the low levelrxode2model$paramsError when specifying
wdwithoutmodNameWith Linear and midpoint of a time between two points, how
rxode2handles missing values has changed. When the missing value is lower than the requested time, it will look backward until it finds the first non-missing value (or if all are missing start looking forward). When the missing value is higher than the requested time, the algorithm will look forward until it finds the first non-missing value (or if all are missing, start looking backward).The order of ODEs is now only determined by the order of
cmt()andd/dt(). Compartment properties,tad()and other compartment related variables no no longer affect compartment sorting. The optionrxode2.syntax.require.ode.firstno longer does anything.-
The handling of zeros “safely” has changed (see #775)
when
safeZero=TRUEand the denominator of a division expression is zero, use the Machine’s small number/eps(you can see this value with.Machine$double.eps)when
saveLog=TRUEand the x in thelog(x)is less than or equal to zero, change this tolog(eps)when
safePow=TRUEand the expressionx^yhas a zero forxand a negative number foryreplacexwitheps.
Since the protection for divide by zero has changed, the results will also change. This is a more conservative protection mechanism than was applied previously.
Random numbers from
rxode2are different when usingdop853,lsodaorindLinmethods. These now seed the random numbers in the same way asliblsoda, so the random number provided will be the same with different solving methods.The arguments saved in the
rxSolvefor items likethetaMatwill be the reduced matrices used in solving, not the full matrices (this will likely not break very many items)
Possible breaking changes (though unlikely)
-
iCovis no longer merged to the event dataset. This makes solving withiCovslightly faster (#743)
New features
You can remove covariances for every omega by piping with
%>% ini(diag())you can be a bit more granular by removing all covariances that have eithereta.kaoreta.clby:%>% ini(diag(eta.ka, eta.cl))or anything with correlations witheta.clwith%>% ini(diag(eta.cl))You can also remove individual covariances by
%>% ini(-cov(a, b))or%>% ini(-cor(a,b)).-
You can specify the type of interpolation applied for added dosing records (or other added records) for columns that are kept with the
keep=option inrxSolve(). This new option iskeepInterpolationand can belocffor last observation carried forward,nocbwhich is the next observation carried backward, as well asNAwhich puts aNAin all imputed data rows. See #756.Note: when interpolation is linear/midpoint for factors/characters it changes to locf with a warning (#759)
Also note, that the default keep interpolation is
na
-
Now you can specify the interpolation method per covariate in the model:
linear(var1, var2)says bothvar1andvar2would use linear interpolation when they are a time-varying covariate. You could also uselinear(var1)locf()declares variables using last observation carried forwardnocb()declares variables using next observation carried backwardmidpoint()declares variables using midpoint interpolation
linear(),locf(),locb(),midpoint(),params(),cmt()anddvid()declarations are now ignored when loading arxode2model withrxS()Strings can be assigned to variables in
rxode2.Strings can now be enclosed with a single quote as well as a double quote. This limitation was only in the rxode2 using string since the R-parser changes single quotes to double quotes. (This has no impact with
rxode2({})and ui/function form).More robust string encoding for symengine (adapted from
utils::URLencode()andutils::URLdecode())Empty arguments to
rxRename()give a warning (#688)Promoting from covariates to parameters with model piping (via
ini()) now allows setting bounds (#692)Added
assertCompartmentName(),assertCompartmentExists(),assertCompartmentNew(),testCompartmentExists(),assertVariableExists()testVariableExists(),assertVariableNew(),assertVariableName(), andassertParameterValue()to verify that a value is a valid nlmixr2 compartment name, nlmixr2 compartment/variable exists in the model, variable name, or parameter value (#726; #733)Added
assertRxUnbounded(),testRxUnbounded(),warnRxBounded()to allownlmixr2warn about methods that ignore boundaries #760Added functions
tad0(),tafd0(),tlast0()andtfirst0()that will give0instead ofNAwhen the dose has not been administered yet. This is useful for use in ODEs sinceNAs will break the solving (so can be used a bit more robustly with models like Weibull absorption).rxode2is has no more binary link tolotri, which means that changes in thelotripackage will not requirerxode2to be recompiled (in most cases) and will not crash the system.rxode2also has no more binary linkage toPreciseSumsThe binary linkage for
dparseris reduced to C structures only, making changes in dparser less likely to cause segmentation faults inrxode2if it wasn’t recompiled.A new model property has been added to
$props$cmtPropand$statePropDf. Both are data-frames showing which compartment has properties (currentlyini,f,alag,rateanddur) in therxode2ui model. This comes from the lower level model variable$statePropwhich has this information encoded in integers for each state.A new generic method
rxUiDeparsecan be used to deparse meta information into more readable expressions; This currently by default supports lower triangular matrices by lotri, but can be extended to support other types of objects like ’nlmixr2’sfoceiControl()for instance.
Bug fixes
Fix
ui$props$endpointwhen the ui endpoint is defined in terms of the ode instead of lhs. See #754Fix
ui$propswhen the ui is a linear compartment model withoutkadefined.Model extraction
modelExtract()will now extract model properties. Note that the model property ofalag(cmt)andlag(cmt)will give the same value. See #745When assigning reserved variables, the parser will error. See #744
Linear interpolation will now adjust the times as well as the values when
NAvalues are observed.Fix when keeping data has
NAvalues that it will not crash R; Also fixed some incorrectNAinterpolations. See #756When using
cmt()sometimes the next statement would be corrupted in the normalized syntax (like for instancelocf); This bug was fixed (#763)keepwill now error when trying to keep items that are in the rxode2 output data-frame and will be calculated (#764)
Big change
- At the request of CRAN, combine
rxode2parse,rxode2random, andrxode2etinto this package; The changes in each of the packages are now placed here:
rxode2et (no changes before merge)
rxode2et 2.0.11
Make the stacking more flexible to help rxode2 have more types of plots
Add
toTrialDurationby Omar Elashkar to convert event data to trial duration dataFix Issue #23 and prefer variable values over NSE values
rxode2et 2.0.9
Split off
et(),eventTable()and related functions.Also split off
rxStack()andrxCbindStudyIndividual()in this package.Added a
NEWS.mdfile to track changes to the package.
rxode2random (before merge)
- Fix a bug when simulating nested variables (#25)
rxode2random 2.1.0
-
Breaking Change changed distributions from the standard C++
<random>toboost::random. Since this is not dependent on the compiler, it makes the random numbers generated from Mac, Windows and Linux the same for every distribution. Unfortunately with a new random number transformation, the simulation results will likely be different than they were before. The exception to this is the uniform number, which was always the same between platforms.
rxode2random 2.0.12
Added function
dfWishartwhich gives (by simulation) an approximation of the degrees of freedom of a Wishart to match arsevalue.Added function
swapMatListWithCubewhich swaps omegaList with omegaCube valuesEnsure that the outputs are integers (instead of long integers) as requested by CRAN for some checking functions.
rxode2parse (fixed before merging)
- As requested by CRAN remove the C code
SET_TYPEOFwhich is no longer part of the C R API.
rxode2parse 2.0.19
Added a evid suffix of 60 for cases where evid=2 adds an on event (fixes tad() calculation in certain edge cases)
Initialize all variables to
NA
rxode2parse 2.0.18
Removed linear compartment solutions with gradients from rxode2parse (and rxode2) when compiled with intel c++ compiler (since it crashes while compiling).
Fixed
m1macstring issues as requested by CRAN
rxode2parse 2.0.17
Added ability to query R user functions in a rxode2 model (will force single threaded solve)
Moved core
rxFunParseandrxRmFunParsehere so that C and R user function clashes can be handledModel variables now tracks which compartments have a lag-time defined
For compartment with steady state doses (NONMEM equivalent SS=1, SS=2), an additional tracking time-point is added at to track the time when the lagged dose is given. As an upshot, the lagged dose will start at the steady state concentration shifted by + ii - lag in
rxode2(currently for ode systems only)This release calculates non bio-availability adjusted duration for all rates instead of trying to figure the rate duration during solving.
Make double assignment an error, ie
a <- b <-NAtimes are ignored (with warning)Steady state bolus doses with
addlare treated as non steady state events (like what is observed inNONMEM)Timsort was upgraded; drop radix support in rxode2 structure
etTransnow supports keeping logical vectors (with the appropriate version ofrxode2).Security fixes were applied as requested by CRAN
rxode2parse 2.0.16
- Import
data.tableexplicitly in the R code (before was imported only in C/C++ code)
rxode2parse 2.0.14
‘linCmt()’ translations of ‘alpha’, ‘beta’, ‘gamma’, ‘k21’, ‘k31’, ‘vc’ now error instead of ignoring ‘gamma’ and ‘k31’ to give 2 cmt solution
transit compartment internal code now changes dose to 0.0 when no dose has been administered to the depot compartment. This way dosing to the central compartment (without dosing to the transit compartment) will not give a
NAfor the depot compartment (and consequently for the central compartment)Moved
rxDerivedhere and added tests for it here as well.-
Moved
etTransParsehere and added tests for it here as well (makes up most ofetTrans). In addition the following changes were made toetTransParse()/etTrans():The internal translation (
etTrans()) will not drop times when infusions stop. Before, if the infusion stopped after the last observation the time when the infusion stopped would be dropped. This interferes withlinCmt()models.Breaking change/bug fix
evid=2are considered observations when translating data to internalrxode2event structureFix edge case to find infusion duration when it is the first item of the dosing record at time 0.
Fixed a bug for certain infusions where the
rate,iiand/orssdata items were dropped from the output whenaddDosing=TRUEAlso have internal functions to convert between classic NONMEM events and rxode2 events
Have an internal function that gives information on the linear compartmental model translation type, which could be useful for babelmixr2
‘time’ in model is now case insensitive
Use function declaration in
rxode2parseGetTranslation()to determine thread safety of functions available to rxode2Add check for correct number of function arguments to parser.
Like R, known functions can be assigned as a variable and the function can still be called (while not changing the variable value). For example you can have a variable
gammaas well as a functiongamma().Fix garbled error messages that occur with certain messages.
Fixed errors that occurred when using capitalized AMT variables in the model.
rxode2parse 2.0.12
Bug fix for strict prototypes
Removed
sprintfas noted by CRANMade
rxode2parsedll binary independent ofrxode2()
rxode2 2.1.3
CRAN release: 2024-05-28
New features
Create a function to see if a rxode2 solve is loaded in memory (
rxode2::rxSolveSetup())Create a new function that fixes the rxode2 population values in the model (and drops them in the initial estimates);
rxFixPop()
rxode2 2.1.2
CRAN release: 2024-01-30
Other changes
rxUicompression now defaults to fast compressionFixes String literal formatting issues as identified by CRAN (#643)
Removes linear compartment solutions with gradients for intel c++ compiler (since they crash the compiler).
rxode2 2.1.0
CRAN release: 2023-12-11
Breaking changes
Steady state with lag times are no longer shifted by the lag time and then solved to steady state by default. In addition the steady state at the original time of dosing is also back-calculated. If you want the old behavior you can bring back the option with
ssAtDoseTime=FALSE.“dop853” now uses the
hmax/h0values from therxControl()orrxSolve(). This may change some ODE solving using “dop853”When not specified (and xgxr is available), the x axis is no longer assumed to be in hours
New features
User defined functions can now be R functions. For many of these R functions they can be converted to C with
rxFun()(you can see the C code afterwards withrxC("funName"))Parallel solving of models that require sorting (like modeled lag times, modeled duration etc) now solve in parallel instead of downgrading to single threaded solving
Steady state infusions with a duration of infusions greater than the inter-dose interval are now supported.
Added
$symengineModelNoPruneand$symengineModelPrunefor loading models into rxode2 withrxS()-
When plotting and creating confidence intervals for multiple endpoint models simulated from a rxode2 ui model, you can plot/summarize each endpoint with
sim. (ie.confint(model, "sim")orplot(model, sim)).If you only want to summarize a subset of endpoints, you can focus on the endpoint by pre-pending the endpoint with
sim.For example if you wanted to plot/summarize only the endpointeffyou would usesim.eff. (ieconfint(model, "sim.eff")orplot(model, sim.eff)) Added
model$simulationIniModelwhich prepend the initial conditions in theini({})block to the classicrxode2({})model.Now
model$simulationModelandmodel$simulationIniModelwill save and use the initialization values from the compiled model, and will solve as if it was the original ui model.Allow
ini(model) <- NULLto drop ini block andas.ini(NULL)givesini({})(Issue #523)Add a function
modelExtract()to extract model lines to allow modifying them and then changing the model by piping or simply assigning the modified lines withmodel(ui) <- newModifiedLinesAdd Algebraic mu-referencing detection (mu2) that allows you to express mu-referenced covariates as:
Instead of the
cl <- exp(tcl + eta.cl + wt_cl * log.WT.div.70.5)That was previously required (where log.WT.div.70.5 was calculated in the data) for mu expressions. The ui now has more information to allow transformation of data internally and transformation to the old mu-referencing style to run the optimization.
Allow steady state infusions with a duration of infusion greater than the inter-dose interval to be solved.
Solves will now possibly print more information when issuing a “could not solve the system” error
The function
rxSetPipingAuto()is now exported to change the way you affect piping in your individual setupAllow covariates to be specified in the model piping, that is
mod %>% model(a=var+3, cov="var")will add"var"as a covariate.When calculating confidence intervals for
rxode2simulated objects you can now usebyto stratify the simulation summary. For example you can now stratify by gender and race by:confint(sim, "sim", by=c("race", "gender"))When calculating the intervals for
rxode2simulated objects you can now useci=FALSEso that it only calculates the default intervals without bands on each of the percentiles; You can also choose not to match the secondary bands limits withlevelsbut use your ownci=0.99for instanceA new function was introduced
meanProbs()which calculates the mean and expected confidence bands under either the normal or t distributionA related new function was introduced that calculates the mean and confidence bands under the Bernoulli/Binomial distribution (
binomProbs())When calculating the intervals for
rxode2simulated objects you can also usemean=TRUEto use the mean for the first level of confidence usingmeanProbs(). For this confidence interval you can override thenused in the confidence interval by usingn=#. You can also change this to a prediction interval instead usingpred=TRUE.Also when calculating the intervals for
rxode2simulated object you can also usemean="binom"to use the binomial distributional information (and ci) for the first level of confidence usingbinomProbs(). For this confidence interval you can override thenused in the confidence interval by usingn=#. You can also change this to a prediction interval instead usingpred=TRUE. Withpred=TRUEyou can override the number of predicted samples withm=#When plotting the
confintderived intervals from anrxode2simulation, you can now subset based on a simulated value likeplot(ci, Cc)which will only plot the variableCcthat you summarized even if you also summarizedeff(for instance).When the rxode2 ui is a compressed ui object, you can modify the ini block with
$ini <-or modify the model block with$model <-. These are equivalent toini(model) <-andmodel(model) <-, respectively. Otherwise, the object is added to the user defined components in the function (ie$meta). When the object is uncompressed, it simply assigns it to the environment instead (just like before).When printing meta information that happens to be a
lotricompatible matrix, uselotrito express it instead of the default R expression.Allow character vectors to be converted to expressions for piping (#552)
rxAppendModel()will now take an arbitrary number of models and append them together; It also has better handling of models with duplicate parameters and models withoutini()blocks (#617 / #573 / #575).keepwill now also keep attributes of the input data (with special handling forlevels); This means a broader variety of classes will be kept carrying more information with it (for example ordered factors, data frame columns with unit information, etc)Piping arguments
appendforini()andmodel()have been aligned to perform similarly. Thereforeini(append=)now can take expressions instead of simply strings andmodel(append=)can also take strings. Also model piping now can specify the integer line number to be modified just like theini()could. Alsomodel(append=FALSE)has been changed tomodel(append=NULL). While the behavior is the same when you don’t specify the argument, the behavior has changed to align withini()when piping. Hencemodel(append=TRUE)will append andmodel(append=FALSE)will now pre-pend to the model.model(append=NULL)will modify lines like the behavior ofini(append=NULL). The default ofmodel(line)modifying a line in-place still applies. While this is a breaking change, most code will perform the same.Labels can now be dropped by
ini(param=label(NULL)). Also parameters can be dropped with the idiommodel(param=NULL)orini(param=NULL)changes the parameter to a covariate to align with this idiom of dropping parametersrxRenamehas been refactored to run faster
Internal new features
Add
as.model()for list expressions, which impliesmodel(ui) <- ui$lstExprwill assign model components. It will also more robustly work with character vectorsSimulated objects from
rxSolvenow can access the model variables with$rxModelVarsSimulation models from the UI now use
rxerr.endpointinstead oferr.endpointfor thesigmaresidual error. This is to align with the convention that internally generated variables start withrxornlmixrSorting only uses timsort now, and was upgraded to the latest version from Morwenn
Bug fixes
Simulating/solving from functions/ui now prefers params over
omegaandsigmain the model (#632)Piping does not add constants to the initial estimates
When constants are specified in the
model({})block (likek <- 1), they will not be to theiniblockBug fix for
geom_amt()when theaestransformation hasxBug fix for some covariate updates that may affect multiple compartment models (like issue #581)
rxode2 2.0.14
CRAN release: 2023-10-07
CRAN requested that FORTRAN
kindbe changed as it was not portable; This was commented code, and simply removed the comment.Bug-fix for
geom_amt(); also now useslinewidthand at leastggplot2 3.4.0Some documentation was cleaned up from
rxode22.0.13
rxode2 2.0.13
CRAN release: 2023-04-22
New features
A new function
zeroRe()allows simple setting of omega and/or sigma values to zero for a model (#456)Diagonal zeros in the
omegaandsigmamatrices are treated as zeros in the model. The correspondingomegaandsigmamatrices drop columns/rows where the diagonals are zero to create a newomegaandsigmamatrix for simulation. This is the same idiom that NONMEM uses for simulation from these matrices.Add the ability to pipe model estimates from another model by
parentModel %>% ini(modelWithNewEsts)Add the ability to append model statements with piping using
%>% model(x=3, append=d/dt(depot)), still supports appending withappend=TRUEand pre-pending withappend=NA(the default is to replace lines withappend=FALSE)rxSolve’s keep argument will now maintain character and factor classes from input data with the same class (#190)
Parameter labels may now be modified via
ini(param = label("text"))(#351).Parameter order may be modified via the
appendargument toini()when piping a model. For example,ini(param = 1, append = 0)orini(param = label("text"), append = "param2")(#352).
Internal changes
If lower/upper bounds are outside the required bounds, the adjustment is displayed.
When initial values are piped that break the model’s boundary condition reset the boundary to unbounded and message which boundary was reset.
Added
as.rxUi()function to convert the following objects torxUiobjects:rxode2,rxModelVars,function. Converting nlmixr2 fits torxUiwill be placed in thes3method in the corresponding package.assertRxUi(x)now usesas.rxUi()so that it can be extended outside ofrxode2/nlmixr2.rxode2now supportsaddlwithssdosesMoved
rxDerivedtorxode2parse(and re-exported it here).Added test for transit compartment solving in absence of dosing to the transit compartment (fixed in
rxode2parsebut solving tested here)Using
ini()without any arguments on arxode2type function will return theini()block. Also added a methodini(mod) <- iniBlockto modify theiniblock is you wish.iniBlockshould be an expression.Using
model()without any arguments on arxode2type function will return themodel()block. Also added a new methodmodel(mod) <- modelBlockAdded a new method
rxode2(mod) <- modFunctionwhich allows replacing the function with a new function while maintaining the meta information about the ui (like information that comes fromnonmem2rxmodels). ThemodFunctionshould be the body of the new function, the new function, or a newrxode2ui.rxode2ui objects now have a$stickyitem inside the internal (compressed) environment. This$stickytells what variables to keep if there is a “significant” change in the ui during piping or other sort of model change. This is respected during model piping, or modifying the model withini(mod)<-,model(mod)<-,rxode2(mod)<-. A significant change is a change in the model block, a change in the number of estimates, or a change to the value of the estimates. Estimate bounds, weather an estimate is fixed or estimate label changes are not considered significant.Added
as.ini()method to convert various formats to an ini expression. It is used internally withini(mod)<-. If you want to assign something new that you can convert to an ini expression, add a method foras.ini().Added
as.model()method to convert various formats to a model expression. It is used internally withmodel(mod)<-. If you want to assign something new that you can convert to a model expression, add a method foras.model().
rxode2 2.0.11
CRAN release: 2022-11-01
Give a more meaningful error for ‘rxode2’ ui models with only error expressions
Break the ABI requirement between
roxde2()andrxode2parse()The new
rxode2parsewill fix thesprintfexclusion shown on CRAN.
rxode2 2.0.10
CRAN release: 2022-10-20
Time invariant covariates can now contain ‘NA’ values.
When a column has ‘NA’ for the entire id, now ‘rxode2’ warns about both the id and column instead of just the id.
To fix some CRAN issues in ‘nlmixr2est’, make the version dependency explicit.
rxode2 2.0.9
CRAN release: 2022-10-19
Remove log likelihoods from ‘rxode2’ to reduce compilation time and increase maintainability of ‘rxode2’. They were transferred to ‘rxode2ll’ (requested by CRAN).
Remove the parsing from ‘rxode2’ and solved linear compartment code and move to ‘rxode2parse’ to reduce the compilation time (as requested by CRAN).
Remove the random number generation from ‘rxode2’ and move to ‘rxode2random’ to reduce the compilation time (as requested by CRAN).
Remove the event table translation and generation from ‘rxode2’ and move to ‘rxode2et’ to reduce the compilation time (as requested by CRAN).
Change the
rxode2ui object so it is a compressed, serialized object by default. This could reduce theC stack sizeproblem that occurs with too many environments in R.Warn when ignoring items during simulations
Export a method to change ‘rxode2’ solve methods into internal integers
Bug fix for time invariant covariates identified as time variant covariate when the individual’s time starts after
0.
rxode2 2.0.8
CRAN release: 2022-09-23
Breaking changes
rxgammanow only allows arateinput. This aligns with the internalrxode2version ofrxgammaand clarifies how this will be used. It is also aligned with thellikGammafunction used for generalized likelihood estimation.ui
cauchysimulations now follow the ui fornormalandtdistributions, which means you can combine with transformations. This is because thecauchyis atdistribution with one degree of freedom.ui
dnorm()andnorm()are no longer equivalent toadd(). Now it allows you to use the loglikllikNorm()instead of the standardnlmixr2style focei likelihood. This is done by addingdnorm()at the end of the line. It also meansdnorm()now doesn’t take any arguments.Vandercorput normal removed (non-random number generator)
New features
Allow models in the
nlmixr2form without anini({})blockAllow model piping of an omega matrix by
f %>% ini(omegaMatrix)Standard models created with
rxode2()can no be piped into a model functionFamilies of log-likelihood were added to
rxode2so that mixed likelihood nonlinear mixed effects models may be specified and run.The memory footprint of a
rxode2solving has been reducedPiping now allow named strings (issue #249)
Bug fixes
rxode2’s symengine would convertsqrt(2)toM_SQRT_2when it should beM_SQRT2. This has been fixed; it was most noticeable in nlmixr2 log-likelihood estimation methodsrxode2treatsDVas a non-covariate withetTran(last time it would duplicate if it is in the model). This is most noticeable in the nlmixr2 log-likelihood estimation methods.
New features
A new flag (
rxFlag) has been created to tell you where in therxode2solving process you are. This is useful for debugging. If outputting this variable it will always be11or calculating the left handed equations. If you are using in conjunction with theprintf()methods, it is a double variable and should be formatted with"%f".An additional option of
fullPrinthas been added torxode2()which allowsrprintf()to be used in almost all ofrxode2()steps (inductive linearization and matrix exponential are the exception here) instead of just the integrationddtstep. It defaults toFALSE.
rxode2 2.0.7
CRAN release: 2022-05-17
Removed accidental
^Sfrom news as requested by CRAN.Bug fix for more complicated mu-referencing.
Change rxode2 md5 to only depend on the C/C++/Fortran code and headers not the R files. That way if there is binary compatibility between
nlmixr2estandrxode2, a new version ofnlmixr2estwill not need to be submitted to CRAN.
rxode2 2.0.6
CRAN release: 2022-05-09
Breaking changes
Solving controls
The options for
rxControlandrxSolveare more strict.camelCaseis now always used. Old options likeadd.covandtransit_absare no longer supported, onlyaddCovis supported.A new option,
sigdighas been added torxControl(), which controls some of the more common significant figure options likeatol,rtol,ssAtol,ssRtol, with a single option.
Simulations
For simulations,
$simulationSigmanow assumes a diagonal matrix. The sigma values are assumed to be standard normal, and uncorrelated between endpoints. Simulation with uncertainty will still draw from this identity diagonal matrixParallel solving now seeds each simulation per each individual based on the initial seed plus the simulation id. This makes the simulation reproducible regardless of the number of cores running the simulation.
Other breaking changes
Solved objects now access the underlying rxode model with
$rxode2instead of$rxodeSince this change names,
rxode2,rxodeandRxODEall perform the same function.Options were changed from
RxODE.syntaxtorxode2.syntax.Assigning states with
rxode2.syntax.assign.state(wasRxODE.syntax.assign.state) is no longer supported.Enforcing “pure” assignment syntax with
=syntax is no longer supported sorxode2.syntax.assignis no longer supported (wasRxODE.syntax.assign).Since R supports
**as an exponentiation operator, the pure syntax without**can no longer be enabled. Hencerxode2.syntax.star.pow(wasRxODE.syntax.star.pow) no longer has any effect.The “pure” syntax that requires a semicolon can no longer be enabled. Therefore
rxode2.syntax.require.semicolon(wasRxODE.syntax.require.semicolon) no longer has any effect.The syntax
state(0)can no longer be turned off.rxode2.syntax.allow.ini0(wasRxODE.syntax.allow.ini0) has been removed.Variable with dots in variable and state names like
state.nameworks in R. Therefore, “pure” syntax of excluding.values from variables cannot be enforced withrxode2.syntax.allow.dots(wasRxODE.syntax.allow.dots).The mnemonic
et(rate=model)andet(dur=model)mnemonics have been removed.rateneeds to be set to-1and-2manually instead.The function
rxode2Test()has been removed in favor of using testthat directly.Transit compartments need to use a new
evid,evid=7. That being said, thetransitAbsoption is no longer supported.IDcolumns in input parameter data frames are not sorted or merged with original dataset any more; The underlying assumption of ID order should now be checked outside ofrxode2(). Note that the event data frame is still sorted.
Additional features
The UI functions of
nlmixrhave been ported to work inrxode2directly.rxModelVars({})is now supported.You may now combine 2 models in
rxode2withrxAppendModel(). In fact, as long as the first value is a rxode2 evaluated ui model, you can usec/rbindto bind 2 or more models together.You may now append model lines with piping using
%>% model(lines, append=TRUE)you can also pre-pend lines by%>% model(lines, append=NA)You may now rename model variables, states and defined parameters with
%>% rxRename(new=old)or ifdplyris loaded:%>% rename(new=old)You can fix parameters with
%>% ini(tcl=fix)or%>% ini(fix(tcl))as well as unfix parameters with%>% ini(tcl=unfix)or%>% ini(unfix(tcl))
Internal changes
Strict R headers are enforced more places
Since there are many changes that could be incompatible, this version has been renamed to
rxode2rxode2()printout no longer uses rules and centered headings to make it display better on a larger variety of systems.
