Skip to content

Commit 63f2417

Browse files
simonbeyer1claude
andcommitted
feat(solve): maxOrder and stald on both backends, one named argument list per solve entry; cppDE 0.13.2
The generated solve entries take one named list with an abi entry instead of fifteen positional arguments; models generated by an earlier cppDE are refused and need to be generated again. stald ports CVODE's stability limit detection (cvSLdet) to the BDF family. The Methods vignette follows the code on the first step, the corrector, the batch threads and the Rosenbrock coefficients; the benchmarks run on dMod2's distributedComputing(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c268a36 commit 63f2417

25 files changed

Lines changed: 1227 additions & 645 deletions

‎DESCRIPTION‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
Package: cppDE
22
Title: 'C++' Code Generation for ODEs with Sensitivity Propagation
3-
Version: 0.13.1
3+
Version: 0.13.2
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",
7-
comment = "Portions of cppde_multistepper(_controller).hpp and cppde_newton.hpp derived from SUNDIALS/CVODE(S); see inst/COPYRIGHTS"),
7+
comment = "Portions of cppde_multistepper(_controller).hpp, cppde_newton.hpp and cppde_stald.hpp derived from SUNDIALS/CVODE(S); see inst/COPYRIGHTS"),
88
person("Southern Methodist University", role = "cph",
99
comment = "SUNDIALS/CVODE(S) co-copyright holder; see inst/COPYRIGHTS"),
1010
person("Karsten", "Ahnert", role = "cph",
@@ -61,6 +61,6 @@ Suggests:
6161
dplyr,
6262
scales
6363
VignetteBuilder: R.rsp
64-
Config/roxygen2/version: 8.1.0
64+
Config/roxygen2/version: 8.1.1
6565
Config/testthat/edition: 3
6666
Config/testthat/parallel: true

‎NEWS.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# cppDE 0.13.2
2+
3+
* Breaking: the generated solve entries take one named argument list. Models
4+
generated by an earlier cppDE are refused with a message; generate and
5+
compile them again.
6+
7+
* `solveODE()`, `solveODEBatch()` and `prepareBatch()` take `maxOrder`, an
8+
upper bound on the order of `"bdf"` and `"adams"`, and `stald`, stability
9+
limit detection for `"bdf"` (off by default), on both backends. Both are
10+
set per solve; the compiled model does not change.
11+
112
# cppDE 0.13.1
213

314
* The demos use ASCII only, and `scales`, which `demo("fitCoPt")` calls, is

‎R/adjointControl.R‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ adjointControl <- function(refine = FALSE, gradtol = NULL, trace = FALSE) {
4747
.adjointWanted <- function(ctl)
4848
!is.null(ctl) && ctl$trace
4949

50-
# The control onto the cotangent, the channel the generated sweep reads, so the
51-
# .Call signature stays fixed.
52-
.adjointApply <- function(ctl, model, cotangent, is_cvode) {
53-
if (is.null(ctl)) return(cotangent)
50+
# The entries of the solve argument list that hold the control. A solve
51+
# without a cotangent sweeps nothing and takes none.
52+
.adjointArgs <- function(ctl, model, has_cotangent, is_cvode) {
53+
if (is.null(ctl)) return(list())
5454
if (!inherits(ctl, "cppDEadjointControl"))
5555
stop("'adjoint' must come from adjointControl()", call. = FALSE)
5656
mode <- attr(model, "derivMode")
@@ -62,17 +62,17 @@ adjointControl <- function(refine = FALSE, gradtol = NULL, trace = FALSE) {
6262
stop("on the CVODE backend 'adjoint' takes 'gradtol' alone: the backward ",
6363
"problem is CVODES' own continuous adjoint, with no discrete steps ",
6464
"to check or report", call. = FALSE)
65-
if (!is.null(ctl$gradtol) && !is.null(cotangent))
66-
attr(cotangent, "gradtol") <- ctl$gradtol
67-
return(cotangent)
65+
if (!has_cotangent || is.null(ctl$gradtol)) return(list())
66+
return(list(gradtol = as.double(ctl$gradtol)))
6867
}
69-
if (!is.null(cotangent) && .adjointWanted(ctl))
70-
attr(cotangent, "adjointGrid") <- TRUE
71-
if (!is.null(cotangent) && ctl$refine) {
72-
attr(cotangent, "refine") <- 1
73-
if (!is.null(ctl$gradtol)) attr(cotangent, "gradtol") <- ctl$gradtol
68+
if (!has_cotangent) return(list())
69+
out <- list()
70+
if (.adjointWanted(ctl)) out$adjoint_grid <- TRUE
71+
if (ctl$refine) {
72+
out$refine <- 1
73+
if (!is.null(ctl$gradtol)) out$gradtol <- as.double(ctl$gradtol)
7474
}
75-
cotangent
75+
out
7676
}
7777

7878
# The sweep's grid as $adjoint.

‎R/cppODE.R‎

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
443443
" // Step-size bounds of a refined run, none unless the caller set them.",
444444
" cppde::step_limit_scope _cppde_sl_scope(args.limits);",
445445
"",
446-
" StepChecker checker(args.maxprogress, args.maxsteps);",
446+
" StepChecker checker(args.maxattempts, args.maxsteps);",
447447
"",
448448
sprintf(" cppde::vector_t<%s> x(%d);", numType, n_variables),
449449
sprintf(" cppde::vector_t<%s> full_params(%d);", numType, n_variables + n_params),
@@ -455,20 +455,20 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
455455
externC <- c(
456456
externC,
457457
" // Custom sensitivity initial values (length validated after n_sens is computed)",
458-
" bool has_sens1ini = (args.sens1ini != nullptr);",
459-
" const double* sens1ini = has_sens1ini ? args.sens1ini : nullptr;"
458+
" bool has_sens1ini = (args.tangent != nullptr);",
459+
" const double* sens1ini = has_sens1ini ? args.tangent : nullptr;"
460460
)
461461

462462
if (deriv2) {
463463
externC <- c(
464464
externC,
465-
" bool has_sens2ini = (args.sens2ini != nullptr);",
466-
" const double* sens2ini = has_sens2ini ? args.sens2ini : nullptr;"
465+
" bool has_sens2ini = (args.hessian != nullptr);",
466+
" const double* sens2ini = has_sens2ini ? args.hessian : nullptr;"
467467
)
468468
} else {
469469
externC <- c(
470470
externC,
471-
" if (args.sens2ini != nullptr)",
471+
" if (args.hessian != nullptr)",
472472
" return res.fail(cppde::RC_ILL_INPUT, \"hessian supplied but deriv2 = FALSE\");"
473473
)
474474
}
@@ -917,6 +917,8 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
917917
if (useNDF) "true" else "false"),
918918
sprintf(" controlledStepper.stepper().set_sens_err_con(%s);",
919919
sens_err_con_arg),
920+
" controlledStepper.stepper().set_max_order(args.max_order);",
921+
" controlledStepper.stepper().set_stald(args.stald);",
920922
# Slab priming for any AD path (heap dual<T,0> or static-N dual<T,N>),
921923
# a no-op for non-AD and nested-AD types. Emitted before the
922924
# std::move into denseStepper.
@@ -1005,15 +1007,15 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
10051007
if (use_sparse) "sparse" else "dense"),
10061008
" double abstol = args.abstol;",
10071009
" double reltol = args.reltol;",
1008-
" double root_tol = args.root_tol;",
1010+
" double root_tol = args.roottol;",
10091011
" double hini = args.hini;",
10101012
" int maxroot = args.maxroot;",
10111013
" ode_system sys(full_params, F);",
10121014
" jacobian jac(full_params, F);",
10131015
if (is_reverse) c(
10141016
" // Checkpoint store for the reverse pass: reused from the caller, or",
10151017
" // made here and freed on return unless the caller keeps it.",
1016-
" rev_state* _rev_st = static_cast<rev_state*>(args.store_in);",
1018+
" rev_state* _rev_st = static_cast<rev_state*>(args.store);",
10171019
" bool _rev_reuse = (_rev_st != nullptr);",
10181020
" std::unique_ptr<rev_state> _rev_owned;",
10191021
" if (!_rev_reuse) { _rev_owned.reset(new rev_state()); _rev_st = _rev_owned.get(); }",
@@ -1261,15 +1263,15 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
12611263
" _rev_st->out_t.assign(result_times.begin(), result_times.end());",
12621264
" _rev_st->out_y.assign(y.begin(), y.end());",
12631265
" }",
1264-
" if (args.want_store) {",
1266+
" if (args.keep_store) {",
12651267
" res.store_out = _rev_owned.release();",
12661268
" res.wrap_store = &rev_state_wrap;",
12671269
" }",
12681270
"",
12691271
" // --- The backward sweep ---",
12701272
" // No seed with a store to keep: return the checkpoints without a sweep.",
1271-
" if (args.seed == nullptr) {",
1272-
" if (args.want_store) return res.return_code;",
1273+
" if (args.cotangent == nullptr) {",
1274+
" if (args.keep_store) return res.return_code;",
12731275
" return res.fail(cppde::RC_ILL_INPUT,",
12741276
" \"a model compiled with derivMode = reverse needs a cotangent\");",
12751277
" }",
@@ -1295,7 +1297,7 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
12951297
" res.adjoint2.assign((size_t)n_phi_rows * n_sens * n_seed, 0.0);")
12961298
else character(0),
12971299
solver_decl,
1298-
" if (args.adj_trace) {",
1300+
" if (args.adjoint_grid) {",
12991301
" const int _ns = (int)_rev_store.n_steps();",
13001302
sprintf(" res.n_adj_steps = _ns; res.n_adj_states = %d;", n_variables),
13011303
" res.step_t.assign((size_t)_ns, 0.0);",
@@ -1326,7 +1328,7 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
13261328
if (!second_reverse)
13271329
sprintf(" cppde::adjoint::extrapolated_flow<%s::lu_type> _flow;", rb4_double)
13281330
else character(0)),
1329-
" _cl.trace_lambda(args.adj_trace);",
1331+
" _cl.trace_lambda(args.adjoint_grid);",
13301332
if (second_reverse) character(0)
13311333
else if (written_onestep) " if (args.refine > 0) _cl.set_refine(args.refine * args.reltol, args.abstol, args.gradtol);"
13321334
else " if (args.refine > 0) _flow.set_refine(args.refine * args.reltol, args.abstol, args.gradtol);",
@@ -1340,16 +1342,16 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
13401342
" for (int o = 0; o < n_out; ++o)",
13411343
sprintf(" for (int i = 0; i < %d; ++i)", n_variables),
13421344
sprintf(" _seed_col[(size_t)o * %d + i] =", n_variables),
1343-
sprintf(" args.seed[o + (size_t)n_out * (i + (size_t)%d * c)];", n_variables),
1345+
sprintf(" args.cotangent[o + (size_t)n_out * (i + (size_t)%d * c)];", n_variables),
13441346
if (second_reverse) c(
1345-
" if (args.seed_tan != nullptr && args.n_seed_tan > 0) {",
1347+
" if (args.curvature != nullptr && args.n_seed_tan > 0) {",
13461348
" const int _ntg = args.n_seed_tan < n_sens ? args.n_seed_tan : n_sens;",
13471349
" for (int o = 0; o < n_out; ++o)",
13481350
sprintf(" for (int i = 0; i < %d; ++i) {", n_variables),
13491351
sprintf(" auto& _sc = _seed_col[(size_t)o * %d + i];", n_variables),
13501352
" _sc.diff(0, n_sens);",
13511353
" for (int v = 0; v < _ntg; ++v)",
1352-
" _sc.d(v) = args.seed_tan[o + (size_t)n_out *",
1354+
" _sc.d(v) = args.curvature[o + (size_t)n_out *",
13531355
sprintf(" (i + (size_t)%d * ((size_t)c + (size_t)n_seed * v))];", n_variables),
13541356
" }",
13551357
" }")
@@ -1380,7 +1382,7 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
13801382
" res.adjoint[i + (size_t)n_phi_rows * c] = _cl.wx0()[i] + _cl.wp()[i];",
13811383
sprintf(" for (int j = %d; j < n_phi_rows; ++j)", n_variables),
13821384
" res.adjoint[j + (size_t)n_phi_rows * c] = _cl.wp()[j];"),
1383-
" if (args.adj_trace) {",
1385+
" if (args.adjoint_grid) {",
13841386
" const int _ns = res.n_adj_steps;",
13851387
if (written_onestep || !second_reverse) c(
13861388
" if (c == 0) {",
@@ -1455,30 +1457,21 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
14551457
}
14561458
externC <- c(
14571459
externC,
1458-
sprintf(paste0('extern "C" SEXP solve_%s(SEXP timesSEXP, SEXP paramsSEXP, ',
1459-
'SEXP sens1iniSEXP, SEXP sens2iniSEXP, SEXP fixedSEXP, SEXP abstolSEXP, ',
1460-
'SEXP reltolSEXP, SEXP maxprogressSEXP, SEXP maxstepsSEXP, SEXP hiniSEXP, ',
1461-
'SEXP root_tolSEXP, SEXP maxrootSEXP, SEXP forcingTimesSEXP, ',
1462-
'SEXP forcingValuesSEXP, SEXP seedSEXP, SEXP dimnamesSEXP) {'),
1463-
modelname),
1464-
" cppde::rbatch::solve_args a = cppde::rbatch::read_solve_args(",
1465-
" timesSEXP, paramsSEXP, sens1iniSEXP, sens2iniSEXP, fixedSEXP,",
1466-
" abstolSEXP, reltolSEXP, maxprogressSEXP, maxstepsSEXP, hiniSEXP,",
1467-
" root_tolSEXP, maxrootSEXP, forcingTimesSEXP, forcingValuesSEXP, seedSEXP);",
1468-
sprintf(" return cppde::rbatch::solve_one(a, %d, %s, %s, &solve_impl, dimnamesSEXP);",
1460+
sprintf('extern "C" SEXP solve_%s(SEXP argsSEXP) {', modelname),
1461+
" cppde::rbatch::solve_args a = cppde::rbatch::read_solve_args(argsSEXP);",
1462+
sprintf(" return cppde::rbatch::solve_one(a, %d, %s, %s, &solve_impl);",
14691463
n_variables, dflag, d2flag),
14701464
"}",
14711465
"",
1472-
sprintf('extern "C" SEXP solve_%s_batch(SEXP condsSEXP, SEXP nthreadsSEXP, SEXP dimnamesSEXP) {', modelname),
1466+
sprintf('extern "C" SEXP solve_%s_batch(SEXP condsSEXP, SEXP nthreadsSEXP) {', modelname),
14731467
" const int K = Rf_length(condsSEXP);",
14741468
" const int nt = Rf_isNull(nthreadsSEXP) ? 0 : INTEGER(nthreadsSEXP)[0];",
1475-
if (fixed_grid) "" else " (void)dimnamesSEXP;",
14761469
"",
14771470
" // Phase A: every R read and every R allocation, serially.",
14781471
" std::vector<cppde::rbatch::solve_args> A;",
14791472
" A.reserve(K);",
14801473
" for (int k = 0; k < K; ++k)",
1481-
" A.push_back(cppde::rbatch::read_cond_args(VECTOR_ELT(condsSEXP, k)));",
1474+
" A.push_back(cppde::rbatch::read_solve_args(VECTOR_ELT(condsSEXP, k)));",
14821475
" std::vector<cppde::rbatch::solve_result> R_(K);",
14831476
" SEXP out = PROTECT(Rf_allocVector(VECSXP, K));",
14841477
if (fixed_grid) paste0(
@@ -1488,7 +1481,7 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
14881481
paste0(" std::vector<std::vector<double> > EV(K, std::vector<double>(%d));\n",
14891482
" for (int k = 0; k < K; ++k) %s_fixed_event_times(A[k].params, EV[k].data());\n"),
14901483
n_ev_times, modelname) else "",
1491-
sprintf(" std::vector<cppde::rbatch::pre_ctx> P = cppde::rbatch::prealloc_batch(\n out, A, %d, %d, %s, %s, %s, dimnamesSEXP%s);\n",
1484+
sprintf(" std::vector<cppde::rbatch::pre_ctx> P = cppde::rbatch::prealloc_batch(\n out, A, %d, %d, %s, %s, %s%s);\n",
14921485
n_variables, n_total_sens, dflag, d2flag,
14931486
if (includeTimeZero) "true" else "false",
14941487
if (n_ev_times > 0L) ", &EV" else ""),
@@ -1629,6 +1622,7 @@ cppODE <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
16291622
attr(modelname, "sparse") <- use_sparse
16301623
attr(modelname, "method") <- method
16311624
attr(modelname, "useNDF") <- useNDF
1625+
attr(modelname, "solveAbi") <- .solveAbi
16321626
# Dimension names: under reparametrization the sens columns are theta slots.
16331627
# The sens dim defaults to model-parameter names; solveODE() overrides it per
16341628
# call when the tangent has a full Phi' shape.

‎R/cvode.R‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ cvode <- function(rhs, events = NULL, rootfunc = NULL, fixed = NULL, forcings =
247247
attr(modelname, "method") <- method
248248
attr(modelname, "useNDF") <- NA # not meaningful for CVODE
249249
attr(modelname, "backend") <- "cvode"
250+
attr(modelname, "solveAbi") <- .solveAbi
250251

251252
# The sens dim defaults to model-parameter names; solveODE() overrides it per
252253
# call when the tangent has a full Phi' shape.

0 commit comments

Comments
 (0)