Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2aa615c
add disco to core
Shreyas-Ekanathan Feb 23, 2026
f485bdc
radau version
Shreyas-Ekanathan Feb 23, 2026
cbd2b1c
bdf and rodas
Shreyas-Ekanathan Feb 23, 2026
aff9da0
refactor disco into controllers
Shreyas-Ekanathan Mar 2, 2026
9d306b7
add disco to BDF controller
Shreyas-Ekanathan Mar 3, 2026
2158f3b
fix small edits
Shreyas-Ekanathan Mar 3, 2026
2b9a509
Update algorithms.jl
Shreyas-Ekanathan Mar 3, 2026
d8c1a3b
update disco scheme by caching problems in integrator
Shreyas-Ekanathan Mar 16, 2026
a293d6b
small optimization
Shreyas-Ekanathan Mar 16, 2026
6aa6dfa
update disco to new approach
Shreyas-Ekanathan Mar 28, 2026
b3fa9e8
update benchmarks
Shreyas-Ekanathan Mar 30, 2026
af7c3a5
further optimizations
Shreyas-Ekanathan Mar 31, 2026
f24f124
Merge branch 'master' into fresh-disco
Shreyas-Ekanathan Mar 31, 2026
3bd834f
fix benchmarks and merge issues
Shreyas-Ekanathan Mar 31, 2026
90a9bca
update vector continuous callback handling
Shreyas-Ekanathan Apr 7, 2026
6902dce
update tests
Shreyas-Ekanathan Apr 18, 2026
b472c7c
add steps for vern, BS, etc
Shreyas-Ekanathan Apr 29, 2026
ecd6d40
Merge branch 'master' into fresh-disco
Shreyas-Ekanathan May 1, 2026
77ea57e
fix rebase errors
Shreyas-Ekanathan May 1, 2026
8ac29b1
controller fix
Shreyas-Ekanathan May 5, 2026
5b1069d
fix bug
Shreyas-Ekanathan May 5, 2026
e9d4d3c
controller edits, still WIP
Shreyas-Ekanathan May 6, 2026
fc9a970
docstring and renaming
Shreyas-Ekanathan May 8, 2026
b9584cf
some edits
Shreyas-Ekanathan May 8, 2026
300e59e
tests
Shreyas-Ekanathan May 8, 2026
191b42f
bdf controllers
Shreyas-Ekanathan May 8, 2026
ee0b2bc
whoops
Shreyas-Ekanathan May 8, 2026
914a1d4
revert bdf edits
Shreyas-Ekanathan May 8, 2026
414eb2c
composite controller fix
Shreyas-Ekanathan May 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import OrdinaryDiffEqCore: alg_order, calculate_residuals!,
DAEAlgorithm, _unwrap_val, DummyController,
get_fsalfirstlast, generic_solver_docstring, _ad_chunksize_int, _ad_fdtype, _fixup_ad,
_ode_interpolant, _ode_interpolant!, has_stiff_interpolation,
_ode_addsteps!, DerivativeOrderNotPossibleError
_ode_addsteps!, DerivativeOrderNotPossibleError, set_discontinuity
using OrdinaryDiffEqSDIRK: ImplicitEulerConstantCache, ImplicitEulerCache

using TruncatedStacktraces: @truncate_stacktrace
Expand Down
12 changes: 11 additions & 1 deletion lib/OrdinaryDiffEqBDF/src/controllers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ function bdf_step_reject_controller!(integrator, cache, EEst1)
h = integrator.dt
cache.consfailcnt += 1
cache.nconsteps = 0

discontinuity_detection = cache.controller.discontinuity_detection
if discontinuity_detection
disco_dt = set_discontinuity(integrator.u, integrator.uprev, integrator, integrator.cache)
if disco_dt != -1
integrator.dt = disco_dt
return integrator.dt
end
end

if cache.consfailcnt > 1
h = h / 2
end
Expand Down Expand Up @@ -495,4 +505,4 @@ function step_accept_controller!(
cache.qwait -= 1 # countdown
end
return integrator.dt / q
end
end
1 change: 1 addition & 0 deletions lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ include("cache_utils.jl")
include("initialize_dae.jl")

include("perform_step/composite_perform_step.jl")
include("disco.jl")

include("dense/generic_dense.jl")

Expand Down
74 changes: 74 additions & 0 deletions lib/OrdinaryDiffEqCore/src/disco.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
function set_discontinuity(u, uprev, integrator, cache)
breakpointθ = find_discontinuity(u, uprev, integrator, cache)
dt = integrator.dt
t = integrator.t
if !isnan(breakpointθ) && 1e-6 < breakpointθ < 1.0
#println("Discontinuity detected at t = ", t + breakpointθ * dt)
return breakpointθ * dt
end
return -1
end

function find_discontinuity(u, uprev, integrator, cache)
cb = integrator.opts.callback
cb === nothing && return -1
isempty(cb.continuous_callbacks) && return -1
p = integrator.p
t = integrator.t
dt = integrator.dt
save_idxs = integrator.opts.save_idxs
k = integrator.k
cache = integrator.cache
differential_vars = integrator.differential_vars
θlo = zero(dt)
θhi = one(dt)
bracket = [θlo, θhi]
breakpointθ = -one(dt)
idx = 1
for i in cb.continuous_callbacks
if (!(i.maybe_discontinuity))
continue
end
disco_prob = integrator.disco_probs[idx]
disco_zero = disco_prob.f.f
disco_zero.dt = dt
disco_zero.uprev = uprev
disco_zero.u = u
disco_zero.k = k
disco_zero.cache = cache
disco_zero.differential_vars = differential_vars
disco_zero.idxs = save_idxs
disco_zero.tprev = t
disco_zero.f = integrator.f
disco_zero.p = p
if (i isa VectorContinuousCallback)
len_cb = i.len
out_prev = similar(u)
out_curr = similar(u)
i.condition(out_prev, uprev, t, integrator)
i.condition(out_curr, u, t + dt, integrator)
for j in 1:len_cb
if (out_prev[j] * out_curr[j] < zero(out_prev[j]))
disco_zero.ind = j
sol = solve(disco_prob; bracket = bracket)
tmp = sol[]
if (!isnan(tmp) && (breakpointθ == -1 || tmp < breakpointθ))
breakpointθ = tmp
end
end
end
else
out_prev = i.condition(uprev, t, integrator)
out_curr = i.condition(u, t + dt, integrator)
if (out_prev * out_curr < zero(out_prev))
sol = solve(disco_prob; bracket = bracket)
tmp = sol[]
if (!isnan(tmp) && (breakpointθ == -1 || tmp < breakpointθ))
breakpointθ = tmp
end
end
end
idx += 1
end
breakpointθ
end
85 changes: 77 additions & 8 deletions lib/OrdinaryDiffEqCore/src/integrators/controllers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,20 @@ Controller cache used by algorithms that manage step-size selection themselves
(BDF, Nordsieck, Leaping, …). Holds the scalar error estimate exposed through
`get_EEst(integrator)` and a reference to the algorithm cache so existing dispatch
on the algorithm cache continues to work.
If `discontinuity_detection` is set to true, the algorithm will run the autonomous
discontinuity detection to predict the best next timestep after step rejection.
Otherwise, it follows the default step rejection algorithm. This feature is currently
defaulted off.
"""
mutable struct DummyControllerCache{T, C} <: AbstractControllerCache
EEst::T
cache::C
discontinuity_detection::Bool
end

function setup_controller_cache(alg, cache, controller::DummyController, ::Type{E}) where {E}
return DummyControllerCache{E, typeof(cache)}(oneunit(E), cache)
discontinuity_detection = false
return DummyControllerCache{E, typeof(cache)}(oneunit(E), cache, discontinuity_detection)
end

# Algorithms with integrated controllers (BDF, Nordsieck, …) only define their
Expand Down Expand Up @@ -236,6 +242,10 @@ the interval `[qmin, qmax]`.
A step will be accepted whenever the estimated error `get_EEst(integrator)` is
less than or equal to unity. Otherwise, the step is rejected and re-tried with
the predicted step size.
If `discontinuity_detection` is set to true, the algorithm will run the autonomous
discontinuity detection to predict the best next timestep after step rejection.
Otherwise, it follows the default step rejection algorithm. This feature is currently
defaulted off.

## References

Expand All @@ -250,31 +260,35 @@ struct IController{T} <: AbstractController
gamma::T
qsteady_min::T
qsteady_max::T
discontinuity_detection::Bool
end

function IController(; qmin = 1 // 5, qmax = 10 // 1, qmax_first_step = 10000 // 1, gamma = 9 // 10, qsteady_min = 1 // 1, qsteady_max = 6 // 5)
discontinuity_detection = false
return IController{typeof(qmin)}( # FIXME combined promoted type
qmin,
qmax,
qmax_first_step,
gamma,
qsteady_min,
qsteady_max,
discontinuity_detection
)
end

function IController(alg; kwargs...)
return IController(Float64, alg; kwargs...)
end

function IController(QT, alg; qmin = nothing, qmax = nothing, qmax_first_step = nothing, gamma = nothing, qsteady_min = nothing, qsteady_max = nothing)
function IController(QT, alg; qmin = nothing, qmax = nothing, qmax_first_step = nothing, gamma = nothing, qsteady_min = nothing, qsteady_max = nothing, discontinuity_detection = nothing)
return IController{QT}(
qmin === nothing ? qmin_default(alg) : qmin,
qmax === nothing ? qmax_default(alg) : qmax,
qmax_first_step === nothing ? QT(10000) : QT(qmax_first_step),
gamma === nothing ? gamma_default(alg) : gamma,
qsteady_min === nothing ? qsteady_min_default(alg) : qsteady_min,
qsteady_max === nothing ? qsteady_max_default(alg) : qsteady_max,
discontinuity_detection === nothing ? false : discontinuity_detection
)
end

Expand Down Expand Up @@ -320,7 +334,15 @@ function step_accept_controller!(integrator, cache::IControllerCache, alg, q)
end

function step_reject_controller!(integrator, cache::IControllerCache, alg)
return integrator.dt = cache.dtreject
discontinuity_detection = cache.controller.discontinuity_detection
if discontinuity_detection
disco_dt = set_discontinuity(integrator.u, integrator.uprev, integrator, integrator.cache)
if disco_dt != -1
integrator.dt = disco_dt
return integrator.dt
end
end
return integrator.dt = cache.dtreject # TODO this does not look right.
end

reinit_controller!(integrator::SciMLBase.DEIntegrator, cache::IControllerCache) = nothing
Expand Down Expand Up @@ -351,7 +373,10 @@ the interval `[qmin, qmax]`.
A step will be accepted whenever the estimated error `get_EEst(integrator)` is
less than or equal to unity. Otherwise, the step is rejected and re-tried with
the predicted step size.

If `discontinuity_detection` is set to true, the algorithm will run the autonomous
discontinuity detection to predict the best next timestep after step rejection.
Otherwise, it follows the default step rejection algorithm. This feature is currently
defaulted off.
!!! note

The coefficients `beta1, beta2` are not scaled by the order of the method,
Expand All @@ -377,9 +402,11 @@ mutable struct PIController{T} <: AbstractController # TODO remove the mutable o
qsteady_min::T
qsteady_max::T
qoldinit::T
discontinuity_detection::Bool
end

function PIController(beta1::Real, beta2::Real; qmin = 1 // 5, qmax = 10 // 0, qmax_first_step = 10000 // 1, gamma = 9 // 10, qsteady_min = 1 // 1, qsteady_max = 6 // 5, qoldinit = 1 // 10^4)
discontinuity_detection = false
return PIController{typeof(beta1)}(
beta1,
beta2,
Expand All @@ -390,14 +417,15 @@ function PIController(beta1::Real, beta2::Real; qmin = 1 // 5, qmax = 10 // 0, q
qsteady_min,
qsteady_max,
qoldinit,
discontinuity_detection
)
end

function PIController(alg; kwargs...)
return PIController(Float64, alg; kwargs...)
end

function PIController(QT, alg; beta1 = nothing, beta2 = nothing, qmin = nothing, qmax = nothing, qmax_first_step = nothing, gamma = nothing, qsteady_min = nothing, qsteady_max = nothing, qoldinit = nothing)
function PIController(QT, alg; beta1 = nothing, beta2 = nothing, qmin = nothing, qmax = nothing, qmax_first_step = nothing, gamma = nothing, qsteady_min = nothing, qsteady_max = nothing, qoldinit = nothing, discontinuity_detection = nothing)
beta2 = beta2 === nothing ? beta2_default(alg) : beta2
beta1 = beta1 === nothing ? beta1_default(alg, beta2) : beta1
qoldinit = qoldinit === nothing ? 1 // 10^4 : qoldinit
Expand All @@ -410,7 +438,8 @@ function PIController(QT, alg; beta1 = nothing, beta2 = nothing, qmin = nothing,
gamma === nothing ? gamma_default(alg) : gamma,
qsteady_min === nothing ? qsteady_min_default(alg) : qsteady_min,
qsteady_max === nothing ? qsteady_max_default(alg) : qsteady_max,
qoldinit,
qoldinit,
discontinuity_detection === nothing ? false : discontinuity_detection
)
end

Expand Down Expand Up @@ -465,6 +494,14 @@ end
function step_reject_controller!(integrator, cache::PIControllerCache, alg)
(; controller, q11) = cache
(; qmin, gamma) = controller
discontinuity_detection = cache.controller.discontinuity_detection
if discontinuity_detection
disco_dt = set_discontinuity(integrator.u, integrator.uprev, integrator, integrator.cache)
if disco_dt != -1
integrator.dt = disco_dt
return integrator.dt
end
end
return integrator.dt /= min(inv(qmin), q11 / gamma)
end

Expand Down Expand Up @@ -517,6 +554,11 @@ Some standard controller parameters suggested in the literature are
| H211PI | `1//6` | `1//6` | `0` |
| H312PID | `1//18` | `1//9` | `1//18` |

If `discontinuity_detection` is set to true, the algorithm will run the autonomous
discontinuity detection to predict the best next timestep after step rejection.
Otherwise, it follows the default step rejection algorithm. This feature is currently
defaulted off.

!!! note

In contrast to the [`PIController`](@ref), the coefficients `beta1, beta2, beta3`
Expand Down Expand Up @@ -552,18 +594,21 @@ struct PIDController{QT, Limiter} <: AbstractController
limiter::Limiter # limiter of the dt factor (before clipping)
qsteady_min::QT
qsteady_max::QT
discontinuity_detection::Bool
end

@inline default_dt_factor_limiter(x) = one(x) + atan(x - one(x))

function PIDController(beta1::Real, beta2::Real, beta3::Real = zero(beta1); accept_safety = 0.81, limiter = default_dt_factor_limiter, qsteady_min = 1 // 1, qsteady_max = 6 // 5)
beta = map(float, promote(beta1, beta2, beta3))
discontinuity_detection = false
return PIDController{typeof(beta1), typeof(limiter)}(
beta,
accept_safety,
limiter,
qsteady_min,
qsteady_max,
discontinuity_detection
)
end

Expand All @@ -586,6 +631,7 @@ function PIDController(QT, alg; beta = nothing, accept_safety = 0.81, limiter =
limiter,
QT(qsteady_min === nothing ? qsteady_min_default(alg) : qsteady_min),
QT(qsteady_max === nothing ? qsteady_max_default(alg) : qsteady_max),
discontinuity_detection === nothing ? false : discontinuity_detection
)
end

Expand Down Expand Up @@ -683,6 +729,14 @@ function step_accept_controller!(integrator, cache::PIDControllerCache, alg, dt_
end

function step_reject_controller!(integrator, cache::PIDControllerCache, alg)
discontinuity_detection = cache.controller.discontinuity_detection
if discontinuity_detection
disco_dt = set_discontinuity(integrator.u, integrator.uprev, integrator, integrator.cache)
if disco_dt != -1
integrator.dt = disco_dt
return integrator.dt
end
end
return integrator.dt *= cache.dt_factor
end

Expand Down Expand Up @@ -738,7 +792,10 @@ integrator.dt / qacc
```

When it rejects, it's the same as the [`IController`](@ref):

If `discontinuity_detection` is set to true, the algorithm will run the autonomous
discontinuity detection to predict the best next timestep after step rejection.
Otherwise, it follows the default step rejection algorithm. This feature is currently
defaulted off.
```julia
if integrator.success_iter == 0
integrator.dt *= 0.1
Expand All @@ -754,31 +811,35 @@ struct PredictiveController{T} <: AbstractController
gamma::T
qsteady_min::T
qsteady_max::T
discontinuity_detection::Bool
end

function PredictiveController(; qmin = float(1 // 5), qmax = 10 // 1, qmax_first_step = 10000 // 1, gamma = 9 // 10, qsteady_min = 1 // 1, qsteady_max = 6 // 5)
discontinuity_detection = false
return PredictiveController{typeof(qmin)}( # FIXME combined promoted type
qmin,
qmax,
qmax_first_step,
gamma,
qsteady_min,
qsteady_max,
discontinuity_detection
)
end

function PredictiveController(alg; kwargs...)
return PredictiveController(Float64, alg; kwargs...)
end

function PredictiveController(QT, alg; qmin = nothing, qmax = nothing, qmax_first_step = nothing, gamma = nothing, qsteady_min = nothing, qsteady_max = nothing)
function PredictiveController(QT, alg; qmin = nothing, qmax = nothing, qmax_first_step = nothing, gamma = nothing, qsteady_min = nothing, qsteady_max = nothing, discontinuity_detection = nothing)
return PredictiveController{QT}(
qmin === nothing ? qmin_default(alg) : qmin,
qmax === nothing ? qmax_default(alg) : qmax,
qmax_first_step === nothing ? QT(10000) : QT(qmax_first_step),
gamma === nothing ? gamma_default(alg) : gamma,
qsteady_min === nothing ? qsteady_min_default(alg) : qsteady_min,
qsteady_max === nothing ? qsteady_max_default(alg) : qsteady_max,
discontinuity_detection === nothing ? false : discontinuity_detection
)
end

Expand Down Expand Up @@ -868,6 +929,14 @@ end
function step_reject_controller!(integrator, cache::PredictiveControllerCache, alg)
(; dt, success_iter) = integrator
(; qold) = cache
discontinuity_detection = cache.controller.discontinuity_detection
if discontinuity_detection
disco_dt = set_discontinuity(integrator.u, integrator.uprev, integrator, integrator.cache)
if disco_dt != -1
integrator.dt = disco_dt
return integrator.dt
end
end
return integrator.dt = success_iter == 0 ? 0.1 * dt : dt / qold
end

Expand Down
2 changes: 2 additions & 0 deletions lib/OrdinaryDiffEqCore/src/integrators/type.jl
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ mutable struct ODEIntegrator{
fsalfirst::FSALType
fsallast::FSALType
rng::RNGType
#disco_prob::IntervalNonlinearProblem
disco_probs::Vector{IntervalNonlinearProblem} #should we change this?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't concrete

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah what Oscar and I had discussed was that once the callbacks are function wrapped we would store a single disco_prob and remake it each time, instead of storing a vector of all of them. Not sure if you had another approach in mind though

W::WType
P::PType
sqdt::SqdtType
Expand Down
Loading
Loading