- A switch on the time, such as the pulse
piecewise(1, time > ts && time <= t2, 0), is located as a root like a switch on a state, on both backends.cvode()stepped over such a pulse where the right-hand side was zero before it, and the sensitivities of the native backend missed the jump where a parameter moves the switching time. !after an operator, as ink*!(x > 1), parses. It negates as in R, up to the next&&,||, comma or closing bracket.?expressionsexplains arithmetic, comparisons and logic, with an example that solves the same equations withcppODE()andcvode().
- New
adjointControl(), passed asadjointtosolveODE(),solveODEBatch(),prepareBatch()andsolveBatch(), controls what a reverse solve does beyond its gradient. Withrefine = TRUEthe sweep is checked step by step against the local error test of the CVODES backward problem: the cotangent of the states toreltolandabstol, and withgradtoleach step's share of the gradient toreltolandgradtol. A tolerance thereby means the same as on acvode()reverse model. A step that fails is swept again in substeps.bdfandadamssweep the flow between their grid points by extrapolated linearly implicit Euler instead of the multistep scheme, so their gradient is that of the flow along the computed trajectory. Withtrace = TRUEthe solve returns$adjoint, the grid and adjoint of the sweep and, underrefine, the substeps each step took. - Breaking:
solveODE(),solveODEBatch()andprepareBatch()loseadjointGridanderrWeights, andsolveBatch()loseserrWeights.adjoint = adjointControl(trace = TRUE)returns the grid as$adjointin place of$adjointGrid; the step-size weights have no replacement. - Breaking: the value pass of a reverse solve takes the grid of a value run,
as the forward run of CVODES' adjoint analysis does.
sensErrConno longer integrates a control tangent there and has no effect on a reverse model. Where that grid misses the gradient, as on a trajectory at rest or under a forcing the state does not see, useadjointControl(refine = TRUE). - Breaking: the limit of consecutive rejected steps is the argument
maxattemptsofsolveODE(),solveODEBatch(),prepareBatch()and of the per-condition lists. install_libs()is nowinstallLibs(); the old name remains as a deprecated alias.ptc()takes the tolerances incontrolsasreltolandabstol, as the solvers do. The old namesrtolandatolstill work and warn.- The help pages state every default, list the fields of
$diagnosticsand the columns of$tracein?solveODE, and give the signatures and shapes of the functionscppFUN()returns.?solveODEno longer calls acotangentrequired whenkeepStore = TRUElets a reverse solve run without one. cvode()reverse models take events androotfunc: the forward run is split at the jumps, one CVODES memory per stretch, and the backward sweep takes the adjoint and the gradient through each jump, including event times that move with the parameters or the state. They also takeadjointControl(gradtol =), which puts the gradient quadrature into the backward problem's error test.cvode()reverse models with a sparse Jacobian no longer crash in the backward pass: every KLU setup factorises afresh, so the replay from a checkpoint takes the steps the run stored.adamssolves its corrector by Newton's method, as CVODES does with a linear solver attached, in place of the PECE fixed point. It no longer stalls on stiff components; the step trace losespece_itersandpece_diverged.- The Newton step on an iteration matrix factorised at an earlier gamma takes
the factor gamma / gammap, and the BDF scaling 2 / (1 + gamrat) only under
BDF. Under
adamsa stale matrix had locked the order at five and collapsed the step. - The reverse sweep of
bdfandadamsno longer reuses a step's operators for a later step of the same size whose history rescales by a different factor. Where step sizes alternate, as at switching times, the gradient was wrong by up to orders of magnitude. rb4keeps its fourth order on a right-hand side that reads the time or a forcing: the coefficient of df/dt in the fourth stage, taken over from Boost.Odeint, had the wrong sign and reduced the method to first order there.- Faster reverse sweeps: the transposed solve refines against a held factorisation where that is cheaper than factorising each step, generated kernels keep their temporaries on the stack, the pullbacks of the right-hand side and of the Jacobian take one pass each, and KLU skips its check of the matrices the generated code assembles.
solveBatch()gainsstore, the checkpoint stores of a prepared reverse batch.compile(output =)builds the objects of the combined librarycoresat a time.installLibs()and./configurebuild SUNDIALS 7.9.0 and SuiteSparse 7.14.1 by default, with the CMake option names of SUNDIALS 7.7 and later; older pins keep the old names../configureprefers the cache prefix of the pinned versions over the lexically last one.adjointControl(trace = TRUE)returnsfailuresin$adjoint, the checked steps that met the test at no number of substeps;etaisNAwherebdfandadamssweep the flow.- The coloured Jacobian, which
CPPDE_JAC = "colour"forces and large sparse models choose, compiles: the reverse Jacobian-vector product no longer seeds the parameters. - The checked sweep keeps the seeds before a step's start, and the extrapolated flow takes intervals in either direction of time.
cvode()reverse models on KLU factorise afresh throughSUNLinSol_KLUReInit()rather than a SUNDIALS internal.- A comparison in the right-hand side whose sides differ by an amount that
reads a state, in
piecewise()or a logical operator, andHeaviside()orsign()of such an amount switch where that amount crosses zero, on both backends. The solve holds each switch as a mode, locates the root and sets the mode there, so forward tangents of first and second order, the reverse gradient and its curvature, andcvode()'s forward and adjoint sensitivities take the jump of f. A switch adds no output row and is not limited bymaxroot. A solution that slides along a switching surface stops with an error. cvode()reverse models with root events interpolate the forward state by Hermite: the polynomial interpolation read a wrong state at the start of every stretch after a root, and the gradient was wrong from there on.- Root events that cross zero within one step fire each at its own time, the earliest first, rather than all at the first of them.
- A terminal
rootfuncends the output with one row at its root. The native backend wrote the root time twice, the first copy 1e-15 before it. ./configurefinds a SUNDIALS built with MPI, as Debian ships it: it adds the MPI link flags, without which the probe failed andcvode()was disabled.erf(),erfc()andatan2()take forward derivatives of first and second order, socppODE()withderivMode = "forward"or"forward-forward"andcppFUN()accept them ascvode()and the reverse modes did.
forcingValues()builds with clang and libc++ (macOS):R_NO_REMAPbefore the R headers, as inptc().
- A forcing holds the value of its first and last point outside them, where
it was 0 before, and a single point gives a constant. Both apply to
cppODE()andcvode()models. - New
forcingValues()evaluates forcings as the models see them.
ptc()builds with clang and libc++ (macOS):R_NO_REMAPbefore the R headers, whoselength()macro broke<vector>.
ptc(): the absolute floor of a row's residual scale isatoltimes its largest partial derivative, not its diagonal. A row whose own rate is tiny but whose partner's is not (Smad2 next to pSmad2) was scaled up to 1e12 times too tight, the step matrix became numerically singular andptc()only shrankdt.
- New
ptc(): steady state of acppFUN()model by pseudo-transient continuation, with linear constraintsC x = totaland positive variables. Implicit Euler steps under local error control follow the flow to a stable steady state and become Newton steps near it;flow = FALSEsolves plain equations. rootfunc = "equilibrate"stops once every|dx/dt| <= roottol |x| + abstol, in both backends. The absolute|dx/dt| < roottolstopped small states too early and large ones too late.
- Bug fix.
derivMode = "reverse"and"forward-reverse"return the gradient the forward mode returns where the trajectory is at rest. Their forward run took its steps from the state's error alone. On a trajectory that stays exactly zero, such as a state that starts at 0 and is not driven within the grid, that error is zero, the step grew without bound, and the adjoint on that grid was off by more than its size: +0.80 against -0.43 for a derivative with respect to a zero initial state on0:50. Where a state is small againstabstol / reltolthe grid was too coarse for the same reason, which left the gradients of the PEtab benchmark models up to 1e-5 off at a tolerance of 1e-8. The forward run now integrates a control tangent,z' = J(x) zalong a fixed direction of the initial state, on the same steps as the state, and its error joins the state's in the error test as a sensitivity's does undersensErrCon. On those models the reverse solve now takes about as many steps as the forward mode and gets its accuracy, at 1.4 to 6 times the time it took before.sensErrCon = FALSE, now accepted by both reverse modes, keeps the grid of a value-only run. Reverse models withmethod = "tsit5"generate their Jacobian for it. - With
sensErrCon = FALSEthe order-decrease candidate of"bdf"and"adams"reads the state alone, as the error test does. It read the sensitivities, so the grid could depend on them.
- Bug fix. A pulse such as
piecewise(1, time > ts && time <= t2, 0)on a state at rest is no longer stepped over. Beforetsthe right-hand side and with it the error estimate are zero, so the step size grew until one step went across the whole pulse: without sensitivities under"bdf"and"adams"for the pulse from 60 to 90 on0:180, and for a short pulse late in the grid under every method, with sensitivities too."rb4"withuseDenseOutput = FALSEstopped at such a jump with "Maximum number of steps exceeded".cppODE()now solves every comparison,Heaviside()andsign()of the right-hand side on time and parameters alone that is affine in time for its switching time. The solve stops a few doubles in front of each switching time inside the grid, followsfover them to the jump and restarts past it, as at a jump in time since 0.10.3. The switching times add no output rows, and the reverse mode transposes the crossing. - After an event,
"rb4"and"tsit5"with dense output also restart their step-size controller, and"tsit5"evaluates its first stage anew rather than reusing the last stage from before the event.
- Bug fix. A
"bdf"or"adams"solve crosses a jump of the right-hand side in time, such as the step inputpiecewise(0, time - ts < 0, 1). From rest under a tightabstol, no step containing the jump passed the error test above the resolution oft, and a solve without sensitivities stopped in front of it with "Too many failed steps in dense output stepper". A step that did cross it left the history with the right-hand side from before the jump, and the solve stopped just behind it. When the step size fails at that floor, the solve now locates the jump off(t, x)intwithin the failed step, followsfover the few doubles up to it and restarts on the far side as after an event. With no jump ahead, it restarts where it stands, once per point of time. The reverse mode transposes both. A stall that persists stops at once with "Step size fell below the resolution of t".
- Bug fix. A
"bdf"or"adams"solve with sensitivities that starts late under a tight tolerance takes its first step again. A sensitivity starting at zero with a rate far aboveabstolcrosses the bounds of the first-step estimate; the port of CVODES'cvHinthen took the upper bound instead of their geometric mean, and att0 = 280withabstol = 1e-12that step lay below a tick oft.t + hrounded back tot, and the solve stopped with "Too many failed steps in dense output stepper". The estimate now takes the geometric mean, ascvHindoes, and the first step of every method is at least four ticks ofmax(|t0|, |t_final|, 1). - Bug fix. A fixed-time event fires only from the first time of the
integration grid up to, but not including, the last one, and
includeTimeZeroadds 0 to that grid. An event before the grid used to move the start of the integration back to its own time, and one after it added a row beyond the last requested time. An event at the last time is no longer applied, which also ends the failure it caused under"bdf"and"adams". Two consecutive solves over[t0, t1]and[t1, t2]now reproduce one solve over[t0, t2], which multiple shooting relies on. A grid of a single time applies the events at that time, as the start of a longer grid does. The rule holds for event times that depend on parameters, in the batch, in the reverse mode and incvode(). - Bug fix. The reverse mode counted the cotangent of the first output row twice when a fixed event sat at the first time, so the derivatives with respect to the initial state were wrong.
- Bug fix. A reverse solve over a single time no longer crashes R. It integrates nothing, so the cotangent of the initial state is the seed and the parameters receive only what an event at that time contributes. The sweep read the state count off the first checkpoint, which such a run never writes.
cvode()reports the state after an event at the first time in the first row, as the native backend does, and no longer applies an event that lies before the first time.
cppFUN()builds"forward"only by default."forward-reverse"is a mode of its own;"reverse"no longer builds it.- The forward-reverse
vjpis generated as derivative code instead of runningvjpover a dual. It compiles in a fraction of the time and memory and runs about twice as fast. - Products of four or more factors are differentiated through shared prefix and suffix products, and chained adjoints and tangents stay one factor. Reverse and forward-reverse code grows linearly, not quadratically, with product length.
- Reverse mode.
cppODE(..., derivMode = "reverse")computes the gradient of a seeded functional by a discrete adjoint, at a cost that does not grow with the number of parameters."forward-reverse"adds its curvature, the Hessian applied to the tangent, and"forward-forward"the full Hessian. All four methods, events, roots, forcings, the sparse solver and the batch entries are covered.cvode(..., derivMode = "reverse")wraps the CVODES adjoint behind the same interface.keepStore/storereuse a forward pass,errWeightsweights the step size by the adjoint andadjointGridreturns the backward grid. - Derivative names. Arguments and results are called
tangent,hessian,cotangentandcurvature, replacingsens1ini/sens1,sens2ini/sens2,seed/adjointandseedTangent/adjoint2.cppFUN()takestangentX,tangentP,hessianXandhessianP,evaluate()returnsy,tangentandhessian, andvjp()covers both orders.funCpp()is nowcppFUN(); no old name is kept as an alias. - Code generation on an expression graph. The code generator builds a
hash-consed expression graph of the model and differentiates it by automatic
differentiation; SymPy is left for unusual syntax. Generation grows linearly
with the model, long linear sums become tables and regular structure becomes
loops, so models with thousands of states generate and compile in about a
minute.
derivMode = "symbolic"andderivSymb()are gone, and acppFUN()object runs compiled code only. solveODE(..., sensErrCon = FALSE)lets forward sensitivities ride the grid of a value run. Tangent storage is allocated on the heap, without a compile-time limit (nStackis gone).- Bug fixes. Second derivatives through fixed and root events, including
resets and roots that read the clock, agree with forward over forward to
1e-10. The upper half of
$hessianno longer drifts on long stiff runs. Unnamed models no longer repeat their names underset.seed(). The CVODES adjoint converges on models with many parameters and returns its result through the batch. A forcing that multiplies a state compiles,cvode()reads aterminalcolumn given as text, and therb4initial step and time-only sparse Jacobian entries are right. codegenAvailable()reports whether a model can be generated and compiled without installing anything; the examples run only then.- Python 3.9 or newer and R 4.3 or newer are required. The methods vignette covers the reverse mode and benchmarks against CVODES, and every export has an example.
- Bug fix. OpenMP is detected on Windows.
configure.winreadSHLIB_OPENMP_CXXFLAGSfromR_HOME/etc/Makeconf, which on Windows lives under the architecture subdirectory, so every install ran serially and built models without-fopenmp.
- A threaded BLAS no longer deadlocks a forked worker. BLAS is pinned to one
thread for the width of every
fork()and restored in the parent. forkGuard()reports which BLAS cppDE steers, its thread count and whether the handler is installed. The guard is installed when the DLL loads.- The BLAS thread-count probe covers FlexiBLAS and BLIS alongside MKL and OpenBLAS.
- The Windows branch of that probe searched the process image, which exports none of these, so the two-runtime guard did nothing there.
- The package has a
src/, holding the fork guard's entry point.
- A
piecewisetranslates, ascppde::select(cond, a, b). Both branches are evaluated, so each has to be safe to evaluate. &&,||and!are accepted in equations, grouped by Python's parser.- An expression that does not parse names itself and gives a reason.
- A model symbol can no longer collide with an identifier the generator emits.
A parameter named
stdused to rewritestd::powintop[18]::pow. - A symbol named after a C++ keyword compiles,
defaultandintincluded. - A symbol named after a Python keyword is rejected and named, rather than silently renamed. SymPy parses through Python's parser.
- The
doublelocals of a root event'sG_ttlambda are named by position. cppde::value_of(x)is the value accessor across arithmetic types, both dual orders and their expression templates.
- A root event whose crossing falls exactly on an evaluated time now fires. Detection and bisection both tested the sign product strictly.
- A root event no longer fires again on the crossing it just handled.
- A fixed event that makes a root condition true now fires it, with the resets riding on the jump's surface. Terminal conditions are excluded.
- The step size is re-estimated after every event, not only for the multistep methods.
funCpp()substitutes all symbols in one pass. A parameter named after a generated array rewrote the slots already emitted.compile()no longer repeats the OpenMP and KLU flags that the constructors already recorded on the model.inst/examples/example_saltation.Rchecks the sensitivity transport against a SymPy solution to first and second order, over five models.- Generated entry points are dispatched by name and shared object rather than by a cached address, so loading and unloading are without consequence. A model whose library is gone names what it is missing.
- Scoping the lookup to one shared object also stops two models sharing an entry point name from reaching into each other.
clearNativeSymbols()drops the remembered name pairings. It is no longer needed after loading or unloading.
- Test suite over solvers, sensitivities, events, reparametrisation and the batch path.
solveODEBatch()solves many conditions in one.Call, over OpenMP threads, with results identical to the serial path.prepareBatch()andsolveBatch()reuse a validated handle across solves.batchAvailable()reports why a batch would run serially.
./configuredetects SUNDIALS, SuiteSparse/KLU and OpenMP by linking a test binary; a missing library disables only its own feature.install_libs()builds SUNDIALS and SuiteSparse into a per-user cache.
funCpp()compiles algebraic functions with optional derivatives, through forward-mode AD or analytic SymPy derivatives.
solveODE()integrates a model and returns states with first and second order parameter sensitivities.diagnostics()prints the solver statistics.
cppODE()andcvode()generate, compile and load a solver for an ODE system and return a model handle.
compile()builds generated sources throughR CMD SHLIB.- Native symbol lookups are cached;
clearNativeSymbols()drops the cache.
- C++ generation for ODE, CVODE and funCpp models, with common subexpression elimination and Jacobian sparsity detection.
derivSymb()exposes symbolic first and second derivatives through SymPy.
- Event engine with saltation corrections, root finding and PCHIP forcings.
- Rosenbrock4 and Tsit5 single-step steppers with embedded error estimate and dense output.
- Variable-order BDF/NDF and Adams multistep steppers in Nordsieck form, with a Newton corrector.
- AD aware dense LU through LAPACK and sparse LU through KLU.
- Thread-local bump arena and contiguous tangent slab as tangent storage.
dual2nd<T, N>for second order sensitivities.
- Expression templates collapse right-hand side temporaries into one fused chain-rule loop.
- Forward-mode dual numbers
dual<T, N>for first order sensitivities, with a static and a heap allocated specialisation.
- Package skeleton and licence.