Fix spurious tiny final step for fixed-dt methods#3441
Merged
ChrisRackauckas merged 1 commit intoSciML:masterfrom Apr 16, 2026
Merged
Conversation
Contributor
Author
|
Pushed follow-up commit narrowing the range-expansion gate. The unconditional expansion tripped three additional cases that surfaced in CI:
Local runs now pass on The AD ( |
ed632e3 to
48a4bc9
Compare
When solving with a fixed-dt method (e.g. `solve(prob, Euler(); dt = 0.1)`), the accumulated `t + dt + dt + ...` drifts past `tspan[end]` by one ulp, producing a spurious trailing micro-step. PR SciML#2869 removed the old `100eps(tstop)` snap in `fixed_t_for_floatingpoint_error!` that masked this. Fix: add a floating-point tolerance (`100 * eps(max(|t|, |tstop|))`) to the `dt < distance_to_tstop` comparison in `modify_dt_for_tstops!`. When `dt ≈ distance` within rounding the integrator now takes the tstop branch, and `fixed_t_for_tstop_error!` snaps `t` to the exact tstop value — eliminating the extra step. Adds a regression test covering forward, reverse, and non-evenly- dividing `dt` on `tspan = (0.0, 1.0)`. Fixes the PositiveIntegrators.jl CI failure noted in NumericalMathematics/PositiveIntegrators.jl#192. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
48a4bc9 to
2ad6f5a
Compare
ChrisRackauckas-Claude
pushed a commit
to ChrisRackauckas-Claude/OrdinaryDiffEq.jl
that referenced
this pull request
Apr 16, 2026
Triggers a release so that Julia 1.10 CI (which lacks `[sources]` support and pulls OrdinaryDiffEqCore from the registry) picks up the tstop tolerance fix from SciML#3441. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 task
ChrisRackauckas
added a commit
that referenced
this pull request
Apr 16, 2026
- DiffEqBase 6.217.0 -> 6.218.0 (dt_epsilon default now WarnLevel in Minimal/Standard, #3445) - OrdinaryDiffEqCore 3.32.0 -> 3.33.0 (fix spurious tiny final step for fixed-dt methods, #3441) These are the only subpackages with src/ changes on master since their last registered versions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
ChrisRackauckas
added a commit
that referenced
this pull request
Apr 16, 2026
…3456) - DiffEqBase 6.217.0 -> 6.218.0 (dt_epsilon default now WarnLevel in Minimal/Standard, #3445) - OrdinaryDiffEqCore 3.32.0 -> 3.33.0 (fix spurious tiny final step for fixed-dt methods, #3441) These are the only subpackages with src/ changes on master since their last registered versions. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #2869 addressing the regression surfaced in PositiveIntegrators.jl #192. With OrdinaryDiffEqCore v3.12+,
solve(prob, Euler(); dt = 0.1)ontspan = (0.0, 1.0)started returning 12 steps instead of 11, with a spurious trailing step at0.9999999999999999followed by1.0. PR #2869 removed the100eps(tstop)snap hack that used to paper over this.Two coordinated changes fix it without re-introducing the old hack:
_ode_init: when the user specifiesdtfor a fixed-time method and did not supply their owntstops/d_discontinuities, expandtstopstotspan[1]:dt:tspan[end]. Julia's range semantics use TwicePrecision internally, so each range element is the exact floating-point representative ofk*dtand lands ontspan[end]cleanly. Gated on empty user tstops so the existingsol.t == [0, 1/3, 1/2, 5/6, 1]behavior is preserved when users bring their own tstops.modify_dt_for_tstops!: comparedtagainstdistance_to_tstopwith a small floating-point tolerance (100 * eps(max(t, tstop))). Without this,dtcache = 0.1reads as strictly less than adistance_to_tstopof0.10000000000000009and the integrator takes a full step that overshoots by one ulp, then a tiny corrective step. The tolerance guard skips infinitet/tstopso thattspan = (0.0, Inf)integrations are unaffected.Design suggested by @ChrisRackauckas in the PositiveIntegrators.jl thread:
The tolerance change in
modify_dt_for_tstops!is needed because denser tstops alone don't fix it — the strictdtcache < distance_to_tstopcomparison still fails at one ulp mismatches and routes through the drift path.Test plan
Fixed-dt: no spurious tiny final step from accumulated driftintest/interface/ode_tstops_tests.jlcovers forward, reverse, and non-evenly-dividingdtontspan = (0.0, 1.0).test/interface/ode_tstops_tests.jlstill passes (thedt = 1//3, tstops = [1/2]test that expects[0, 1/3, 1/2, 5/6, 1]is unchanged since the expansion is gated on empty user tstops).test/integrators/ode_event_tests.jlincluding thetspan = (0.0, Inf)terminate-callback case still passes.🤖 Generated with Claude Code