Skip to content

Commit 615ebb2

Browse files
Narrow range expansion to avoid new regressions
CI surfaced three cases where the unconditional range expansion broke: 1. `inf_handling.jl`: `tspan = (0.0, Inf)` with `adaptive=false, dt=0.1` tripped `InexactError(Int64, Inf)` when constructing `(0.0 + 0.1):0.1:Inf`. Gate on `all(isfinite, tspan)`. 2. `integrator_interface_tests.jl` (`set_proposed_dt!`): the test sets `dt=0.1, adaptive=false` and later changes dt to 0.5 from a discrete callback. With the range tstops committed at init, every subsequent step gets clipped to 0.1 and diff(sol.t)[13] stays 0.1 instead of the expected 0.5. Gate on `callback === nothing`. 3. `linear_nonlinear_krylov_tests.jl` (LawsonEuler etc.): exponential RK algorithms report `isdtchangeable(alg) == false`. The range tstops have sub-ulp gaps (e.g. `diff(0.0:0.01:1.0)` is not uniform), so `handle_tstop!` interpolates back whenever `ttmp` overshoots, which resets `integrator.dt` and trips `apply_step!`'s "setup does not allow for changing dt" guard. Gate on `isdtchangeable(_alg)`. Verified locally: - Original bug case: `solve(prob, Euler(); dt = 0.1)` → 11 steps, sol.t == 0.0:0.1:1.0. - `tspan=(0, Inf)` with `adaptive=false`: returns `ReturnCode.Unstable` as expected. - `set_proposed_dt!` test: diff(sol.t)[13] == 0.5. - LawsonEuler convergence run completes. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent c1879d3 commit 615ebb2

1 file changed

Lines changed: 11 additions & 7 deletions

File tree

lib/OrdinaryDiffEqCore/src/solve.jl

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -403,14 +403,18 @@ function _ode_init(
403403
end
404404

405405
# For fixed-timestep methods with a user-supplied dt and no user-supplied
406-
# tstops, expand tstops to the range tspan[1]:dt:tspan[end]. Julia's range
407-
# semantics use TwicePrecision to avoid the floating-point drift of repeated
408-
# `t + dt + dt + ...`, so the integrator hits each step exactly and lands on
409-
# tspan[end] without a spurious tiny final step. Skipped when the user
410-
# supplied tstops to preserve existing "step at dtcache between tstops"
411-
# semantics.
406+
# tstops/d_discontinuities/callbacks, expand tstops to the range
407+
# tspan[1]:dt:tspan[end]. Julia's range semantics use TwicePrecision to
408+
# avoid the floating-point drift of repeated `t + dt + dt + ...`, so the
409+
# integrator hits each step exactly and lands on tspan[end] without a
410+
# spurious tiny final step. Skipped whenever the user supplied tstops,
411+
# d_discontinuities, or callbacks (which may mutate dt at runtime via
412+
# `set_proposed_dt!`) to preserve existing "step at dtcache" semantics,
413+
# and when tspan has non-finite endpoints.
412414
if !isnothing(dt) && !iszero(dt) && (!isadaptive(_alg) || !adaptive) &&
413-
sign(dt) == tdir && isempty(tstops) && isempty(d_discontinuities)
415+
isdtchangeable(_alg) && sign(dt) == tdir &&
416+
isempty(tstops) && isempty(d_discontinuities) &&
417+
callback === nothing && all(isfinite, tspan)
414418
tstops = ((tspan[1] + dt):dt:tspan[end]...,)
415419
end
416420

0 commit comments

Comments
 (0)