Skip to content

Commit f4dd4e6

Browse files
simonbeyer1claude
andcommitted
feat: cppDE 0.11.0, ptc() for steady states, equilibrate relative to each state
- ptc() solves f(x, p) = 0 for a cppFUN() model by pseudo-transient continuation: least-squares steps (I/dt - J) d = f with the rows C d = total - C x, dt controlled by the local error of the implicit Euler step. It follows the flow to a stable steady state and turns into Newton near it; flow = FALSE solves plain equations. Positive variables are updated multiplicatively and projected onto C x = total. The loop runs in C++ on the model's forward entry and LAPACK. - rootfunc = "equilibrate" stops once |dx/dt| <= roottol |x| + abstol for every state, in both backends. The absolute test stopped small states too early and large ones too late. Example in inst/examples/ptc.R, tests in test-ptc.R and test-ode-equilibrate.R. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5f04c36 commit f4dd4e6

23 files changed

Lines changed: 719 additions & 23 deletions

‎DESCRIPTION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: cppDE
22
Title: 'C++' Code Generation for ODEs with Sensitivity Propagation
3-
Version: 0.10.5
3+
Version: 0.11.0
44
Authors@R: c(
55
person("Simon", "Beyer", , "simon.beyer@fdm.uni-freiburg.de", role = c("aut", "cre")),
66
person("Lawrence Livermore National Security", role = "cph",

‎NAMESPACE‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export(diagnostics)
1212
export(forkGuard)
1313
export(install_libs)
1414
export(prepareBatch)
15+
export(ptc)
1516
export(solveBatch)
1617
export(solveODE)
1718
export(solveODEBatch)

‎NEWS.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# cppDE 0.11.0
2+
3+
* New `ptc()`: steady state of a `cppFUN()` model by pseudo-transient
4+
continuation, with linear constraints `C x = total` and positive variables.
5+
Implicit Euler steps under local error control follow the flow to a stable
6+
steady state and become Newton steps near it; `flow = FALSE` solves plain
7+
equations.
8+
* `rootfunc = "equilibrate"` stops once every `|dx/dt| <= roottol |x| + abstol`,
9+
in both backends. The absolute `|dx/dt| < roottol` stopped small states too
10+
early and large ones too late.
11+
112
# cppDE 0.10.5
213

314
* **Bug fix.** `derivMode = "reverse"` and `"forward-reverse"` return the

‎R/cppODE.R‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
#' variable), `value` (expression to apply), `method` (`"replace"`,
1212
#' `"add"`, or `"multiply"`), and exactly one of `time` (event time) or
1313
#' `root` (root expression). `rootfunc` terminates integration:
14-
#' `"equilibrate"` stops at steady state; a character vector of
15-
#' expressions stops at the first zero crossing.
14+
#' `"equilibrate"` stops at steady state (see `roottol` in [solveODE()]); a
15+
#' character vector of expressions stops at the first zero crossing.
1616
#'
1717
#' A time event fires when its time lies between the first and the last
1818
#' integration time, the first included and the last not; with

‎R/ptc.R‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#' Steady state by pseudo-transient continuation
2+
#'
3+
#' Solves `f(x, p) = 0` for variables of a [cppFUN()] model, one output per
4+
#' solved variable, optionally under linear constraints `C x = total`.
5+
#'
6+
#' Each step solves `(I/dt - J) d = f` together with the constraint rows in the
7+
#' least-squares sense. Small `dt` gives implicit Euler steps along
8+
#' `dx/dt = f`, large `dt` Newton steps; `dt` follows the local error of the
9+
#' pseudo-time step, so the iteration follows the flow to a stable steady
10+
#' state and converges quadratically near it. With `flow = FALSE` the steps
11+
#' follow the Newton flow and any regular root of plain equations is returned.
12+
#'
13+
#' @param fun A compiled [cppFUN()] object with forward derivatives.
14+
#' @param x Named numeric, values of all variables of `fun`. The solved
15+
#' variables start there, the others keep their values.
16+
#' @param parms Named numeric, values of the parameters of `fun`.
17+
#' @param solve Names of the variables to solve for. Default all.
18+
#' @param rows Names of the outputs of `fun` paired with `solve`, in the same
19+
#' order. Default `solve`.
20+
#' @param C,total Constraint matrix with columns named by `solve`, and its
21+
#' right-hand side: `C x = total`. Default none.
22+
#' @param flow `TRUE` if the outputs are the right-hand side of an ODE,
23+
#' `FALSE` for plain equations.
24+
#' @param positive Keep the solved variables positive: the step is taken
25+
#' multiplicatively and the constraints are restored by `x exp(C' lambda)`.
26+
#' @param controls Named list: `rtol` (default `1e-10`) and `atol` (`1e-14`),
27+
#' every row within `rtol` of its turnover `sum_k |df_i/dx_k| |x_k|` plus
28+
#' `atol` times its diagonal; `flowTol` (`1`), relative local error of a
29+
#' pseudo-time step; `maxit` (`400`); `dtInit` (`1e-2`), the first
30+
#' pseudo-time step, relative to the fastest rate if `flow = TRUE`; `zmax`
31+
#' (`5`), the largest log change of a variable per step.
32+
#' @return A list with the named solution `x` (all variables), `converged`,
33+
#' `iterations` and `message`.
34+
#' @references Kelley CT, Keyes DE (1998). Convergence analysis of
35+
#' pseudo-transient continuation. SIAM J Numer Anal 35(2):508-523.
36+
#' @example inst/examples/ptc.R
37+
#' @export
38+
ptc <- function(fun, x, parms, solve = names(x), rows = solve, C = NULL, total = NULL,
39+
flow = TRUE, positive = TRUE, controls = list()) {
40+
vars <- attr(fun, "variables"); pars <- attr(fun, "parameters")
41+
outs <- names(attr(fun, "equations")); mn <- attr(fun, "modelname")
42+
if (is.null(vars) || is.null(mn)) stop("'fun' must be a cppFUN() object")
43+
if (!"forward" %in% attr(fun, "derivMode")) stop("'fun' needs forward derivatives")
44+
if (!all(vars %in% names(x))) stop("'x' misses ", toString(setdiff(vars, names(x))))
45+
if (!all(pars %in% names(parms))) stop("'parms' misses ", toString(setdiff(pars, names(parms))))
46+
if (!all(solve %in% vars)) stop("unknown variables in 'solve': ", toString(setdiff(solve, vars)))
47+
if (length(rows) != length(solve) || !all(rows %in% outs))
48+
stop("'rows' must name one output of 'fun' per solved variable")
49+
if (is.null(C)) {
50+
C <- matrix(0, 0, length(solve)); total <- numeric(0)
51+
} else {
52+
if (!all(solve %in% colnames(C))) stop("'C' needs a column per solved variable")
53+
C <- C[, solve, drop = FALSE]
54+
if (length(total) != nrow(C)) stop("'total' needs one value per row of 'C'")
55+
}
56+
def <- list(rtol = 1e-10, atol = 1e-14, flowTol = 1, maxit = 400L, dtInit = 1e-2, zmax = 5)
57+
bad <- setdiff(names(controls), names(def))
58+
if (length(bad)) stop("unknown controls: ", toString(bad))
59+
ctrl <- utils::modifyList(def, as.list(controls))
60+
ctrl$flow <- as.numeric(isTRUE(flow)); ctrl$positive <- as.numeric(isTRUE(positive))
61+
ctrl <- lapply(ctrl, as.double)
62+
x0 <- as.double(x[vars])
63+
if (isTRUE(positive) && any(x0[match(solve, vars)] <= 0))
64+
stop("with positive = TRUE the solved variables must start positive")
65+
66+
sym <- .nativeSym(paste0(mn, "_eval_ad"))
67+
if (is.null(sym)) stop("compile '", mn, "' first")
68+
addr <- getNativeSymbolInfo(sym$name, PACKAGE = sym$dll)$address
69+
res <- .Call(C_cppde_ptc, addr, length(outs), x0, as.double(parms[pars]),
70+
as.integer(match(solve, vars) - 1L), as.integer(match(rows, outs) - 1L),
71+
matrix(as.double(C), nrow(C)), as.double(total), ctrl)
72+
xo <- setNames(x0, vars); xo[solve] <- res$x
73+
list(x = xo, converged = res$converged, iterations = res$iterations, message = res$message)
74+
}

‎R/solveODE.R‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,9 @@
659659
#' @param roottol Tolerance for root finding in root-triggered events.
660660
#' Default `1e-6`. An event `value` that depends on `time` evaluates the
661661
#' firing time directly, so its derivatives inherit this tolerance rather
662-
#' than `reltol`.
662+
#' than `reltol`. With `rootfunc = "equilibrate"` the integration stops once
663+
#' every `|dx/dt| <= roottol |x| + abstol`; the native backend includes the
664+
#' sensitivities.
663665
#' @param maxroot Maximum number of triggers per root event. Default `1`.
664666
#' @param onFailure How to react when the solver returns a non-zero
665667
#' return code. One of `"stop"` (default; raise an error with the solver

‎inst/examples/example_equilibrate.R‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ rhs <- c(
1414
pA = "k1*A*R-k2*pA"
1515
)
1616

17-
# Equilibrate - stoppt wenn alle Ableitungen (inkl. Sensitivitaeten) < roottol
17+
# Equilibrate: stops once every |dx/dt| <= roottol |x| + abstol,
18+
# sensitivities included
1819
model <- cppODE(
1920
rhs = rhs,
2021
rootfunc = "equilibrate",

‎inst/examples/ptc.R‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
\donttest{
2+
if (codegenAvailable()) {
3+
## Enzyme E converts S to P, P returns to S. E + ES and S + ES + P are
4+
## conserved, so the steady state is fixed by the two totals.
5+
f <- c(E = "-kon * E * S + (koff + kcat) * ES",
6+
S = "-kon * E * S + koff * ES + kback * P",
7+
ES = " kon * E * S - (koff + kcat) * ES",
8+
P = " kcat * ES - kback * P")
9+
enz <- cppFUN(f, variables = names(f), parameters = c("kon", "koff", "kcat", "kback"),
10+
modelname = "ptc_enzyme", outdir = tempdir(), compile = TRUE)
11+
C <- rbind(Etot = c(E = 1, S = 0, ES = 1, P = 0),
12+
Stot = c(E = 0, S = 1, ES = 1, P = 1))
13+
ss <- ptc(enz, x = c(E = 1, S = 1, ES = 1, P = 1),
14+
parms = c(kon = 1, koff = 0.5, kcat = 0.3, kback = 0.1),
15+
C = C, total = c(1, 10))
16+
ss$x
17+
enz$func(E = ss$x[["E"]], S = ss$x[["S"]], ES = ss$x[["ES"]], P = ss$x[["P"]],
18+
kon = 1, koff = 0.5, kcat = 0.3, kback = 0.1)
19+
}
20+
}

‎inst/include/cppde/cppde_dual_math.hpp‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,33 @@ inline double max_abs_all_levels_vec(const State& v) {
647647
return m;
648648
}
649649

650+
// steady_all_levels(f, x, tol, atol): every |f| <= tol |x| + atol over a value
651+
// and all of its tangent slots, with f the time derivative of x. The steady-state
652+
// check of rootfunc = "equilibrate".
653+
template<class T>
654+
inline std::enable_if_t<std::is_arithmetic_v<T>, bool>
655+
steady_all_levels(const T& f, const T& x, double tol, double atol) {
656+
return std::abs(static_cast<double>(f)) <= tol * std::abs(static_cast<double>(x)) + atol;
657+
}
658+
659+
template<class T, unsigned N>
660+
inline bool steady_all_levels(const dual<T, N>& f, const dual<T, N>& x, double tol, double atol) {
661+
auto& fm = const_cast<dual<T, N>&>(f);
662+
auto& xm = const_cast<dual<T, N>&>(x);
663+
if (!steady_all_levels(fm.x(), xm.x(), tol, atol)) return false;
664+
unsigned nd = fm.size(), nx = xm.size();
665+
for (unsigned i = 0; i < nd; ++i)
666+
if (!steady_all_levels(fm.d(i), i < nx ? xm.d(i) : T(0), tol, atol)) return false;
667+
return true;
668+
}
669+
670+
template<class State>
671+
inline bool steady_all_levels_vec(const State& f, const State& x, double tol, double atol) {
672+
for (std::size_t i = 0; i < f.size(); ++i)
673+
if (!steady_all_levels(f[i], x[i], tol, atol)) return false;
674+
return true;
675+
}
676+
650677
} // namespace cppde
651678

652679
#endif // CPPDE_DUAL_MATH_HPP

‎inst/include/cppde/cppde_events.hpp‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,14 @@ inline bool direction_matches(double last_val, int direction) {
108108
// Steady-state termination helper (threshold check, no root-finding)
109109
// ============================================================================
110110

111+
// Steady state: every |dx/dt| <= tol |x| + atol, on every AD level.
111112
template<class System, class State, class Time>
112-
std::function<bool(const State&, const Time&)> make_steady_state_termination(System& sys, double tol) {
113-
return [&sys, tol](const State& x, const Time& t) -> bool {
113+
std::function<bool(const State&, const Time&)> make_steady_state_termination(System& sys, double tol,
114+
double atol) {
115+
return [&sys, tol, atol](const State& x, const Time& t) -> bool {
114116
State dxdt(x.size());
115117
sys(x, dxdt, t);
116-
return cppde::max_abs_all_levels_vec(dxdt) < tol;
118+
return cppde::steady_all_levels_vec(dxdt, x, tol, atol);
117119
};
118120
}
119121

0 commit comments

Comments
 (0)