From be58264d072839fb020bcd99b276fd0ad1206d7b Mon Sep 17 00:00:00 2001 From: Joel Eliason Date: Wed, 1 Jul 2026 15:46:28 -0600 Subject: [PATCH] CVODEBase: enforce mxstep budget on the CV_ONE_STEP path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CVodeSetMaxNumSteps(mxstep) bounds internal steps PER CVode() call. On the CV_NORMAL path a whole interval is one call, so CVODE raises CV_TOO_MUCH_WORK at mxstep and stiff draws fail fast. But the scenario/evolve output loops run CV_ONE_STEP — one internal step per call — so that ceiling is never reached and the loops have NO total-work cap: a stiff / near-singular draw crawls in sub-ULP steps until an external per-sim timeout kills it (minutes of wall time). The earlier 500000 -> 50000 mxstep reduction only ever protected the CV_NORMAL path for this reason. simOdeStepOne now checks getNumSteps() (cumulative steps since the last re-init, i.e. per integration segment — the same unit a CV_NORMAL call spans) and raises CV_TOO_MUCH_WORK once it crosses mxstep, so a bad draw fails in seconds (~18s at mxstep=50000) identical to CV_NORMAL, becoming NaN downstream instead of a multi-minute straggler. --- cpp/src/CVODEBase.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cpp/src/CVODEBase.cpp b/cpp/src/CVODEBase.cpp index e551d81..08c808b 100644 --- a/cpp/src/CVODEBase.cpp +++ b/cpp/src/CVODEBase.cpp @@ -599,6 +599,21 @@ double CVODEBase::simOdeStepOne(double tEndClamp){ if (flag != CV_TOO_CLOSE) { check_flag(&flag, "CVode", 1); } + // CV_ONE_STEP advances exactly one internal step per CVode() call, so the + // CVodeSetMaxNumSteps(mxstep) ceiling — which bounds steps PER call — never + // trips here the way it does on the CV_NORMAL path (where a whole interval + // is a single call and CVODE raises CV_TOO_MUCH_WORK at mxstep). Without a + // manual guard the one-step scenario/evolve loops have NO total-work + // ceiling: a stiff / near-singular draw crawls in sub-ULP steps until an + // external per-sim timeout kills it (minutes of wall time). getNumSteps() is + // the cumulative step count since the last re-init (i.e. per integration + // segment — the same unit a CV_NORMAL call spans), so once it crosses mxstep + // the segment is grinding. Raise CV_TOO_MUCH_WORK to fail the draw in + // seconds, identical to what CV_NORMAL would produce → NaN downstream. + if (getNumSteps() > mxstep) { + int too_much = CV_TOO_MUCH_WORK; + check_flag(&too_much, "CVode (CV_ONE_STEP mxstep budget exceeded)", 1); + } save_y(); update_y_other(); return static_cast(t_ret);