Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 24 additions & 22 deletions lib/ImplicitDiscreteSolve/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,40 @@ authors = ["vyudu <[email protected]>"]
version = "1.2.0"

[deps]
SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7"
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"
UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471"
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
NonlinearSolveBase = "be0214bd-f91f-a760-ac4e-3421ce2b2da0"
NonlinearSolveFirstOrder = "5959db7a-ea39-4486-b5fe-2dd0bf03d60d"
OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"

[extras]
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a"
[sources]
OrdinaryDiffEqCore = {path = "../OrdinaryDiffEqCore"}

[compat]
Test = "1.10.0"
AllocCheck = "0.2"
Aqua = "0.8.11"
ConcreteStructs = "0.2.3"
DiffEqBase = "6.176"
JET = "0.9.18, 0.10.4"
NonlinearSolveBase = "2.0.0"
NonlinearSolveFirstOrder = "1.9.0"
OrdinaryDiffEqCore = "1.29.0"
OrdinaryDiffEqSDIRK = "1.6.0"
Reexport = "1.2"
SciMLBase = "2.99"
SimpleNonlinearSolve = "2.7"
OrdinaryDiffEqCore = "1.29.0"
Aqua = "0.8.11"
SymbolicIndexingInterface = "0.3.38"
Test = "1.10.0"
julia = "1.10"
JET = "0.9.18, 0.10.4"
UnPack = "1.0.2"
AllocCheck = "0.2"
DiffEqBase = "6.176"
Reexport = "1.2"

[extras]
AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a"
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["OrdinaryDiffEqSDIRK", "Test", "JET", "Aqua", "AllocCheck"]

[sources.OrdinaryDiffEqCore]
path = "../OrdinaryDiffEqCore"
17 changes: 7 additions & 10 deletions lib/ImplicitDiscreteSolve/src/ImplicitDiscreteSolve.jl
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
module ImplicitDiscreteSolve

using SciMLBase
using SimpleNonlinearSolve
using UnPack
using NonlinearSolveBase
using NonlinearSolveFirstOrder
using ConcreteStructs
using SymbolicIndexingInterface: parameter_symbols
import OrdinaryDiffEqCore: OrdinaryDiffEqAlgorithm, alg_cache, OrdinaryDiffEqMutableCache,
OrdinaryDiffEqConstantCache, get_fsalfirstlast, isfsal,
initialize!, perform_step!, isdiscretecache, isdiscretealg,
alg_order, beta2_default, beta1_default, dt_required,
_initialize_dae!, DefaultInit, BrownFullBasicInit, OverrideInit
_initialize_dae!, DefaultInit, BrownFullBasicInit, OverrideInit,
@muladd, @.., _unwrap_val, OrdinaryDiffEqCore, isadaptive

using Reexport
@reexport using SciMLBase

"""
IDSolve()

Simple solver for `ImplicitDiscreteSystems`. Uses `SimpleNewtonRaphson` to solve for the next state at every timestep.
"""
struct IDSolve <: OrdinaryDiffEqAlgorithm end

include("algorithms.jl")
include("cache.jl")
include("solve.jl")
include("alg_utils.jl")
include("controller.jl")

export IDSolve

Expand Down
26 changes: 25 additions & 1 deletion lib/ImplicitDiscreteSolve/src/alg_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,33 @@ end
SciMLBase.isdiscrete(alg::IDSolve) = true

isfsal(alg::IDSolve) = false
alg_order(alg::IDSolve) = 0
alg_order(alg::IDSolve) = 1
Copy link
Member

Choose a reason for hiding this comment

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

why 1 here?

Copy link
Contributor Author

@termi-official termi-official Oct 24, 2025

Choose a reason for hiding this comment

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

This comes from the analysis shown in Deuflhard, Newton Methods for Nonlinear Problems (Section 5.1.1, see Equation 5.6 and the surrounding definition).

The algorithms here have an associated order in the sense that for a given $dt_n = t_{n+1} - t_n$ we have for some solution $\hat{u}(t_n+1)$ derived from an initial guess given at $u({t_n})$. Now we can define an associated ODE (Davidenko differential equation*) for each nonlinear problem with a "time parameter" by taking the time derivative of the time parameter, which has an analytical solution $\bar{u}(t_{n+1})$, given the same initial guess ($u({t_n})$). The solver now has order $p$ if we have $$||\hat{u}(t_{n+1}) - \bar{u}(t_{n+1})|| \leq C dt^p_n . $$
Does that explain it?

*The Davidenko differential equation for $F(u,t)$ is simply $du/dt = - dF/dx (u,t)^{-1} * dF/dt (u,t)$.

Copy link
Member

Choose a reason for hiding this comment

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

I guess I'm confused. It's a discrete time problem, it's exact?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, let me try to explain it differently then. We have the parametric function $F(u,t)$ and want to find the solution of $F(u_2,t_2) = 0$ given a $u_1$ such that $F(u_1, t_1) = 0$. With with $t_1 &lt; t_2$ we want to find some initial guess for $u^0_2$ given $u_1$, such that the initial guess $u^0_2$ is inside the convergence radius of a Newton method to solve $F(u_2,t_2) = 0$. The obvious choice is that we can simply say our initial guess is simply $u_1$, but this is typically not really great, as $t_2 - t_1$ is often quite small. However, we can use additional information contained in $F(u,t) = 0$. To be specific the derivative with respect to the parameter $t$ contains some extra information which we can use to improve the initial guess. Here we can observe that analytically solving the associated Davidenko differential equation with initial condition $u_1$ on $[t_1, t_2]$ is equivalent to solving $F(u_2,t_2) = 0$. Furthermore, we can exploit this information to inform how large $t_2$ can be chosen, such that the Newton method is guaranteed converge for a given initial guess. Now, the order of this extrapolation polynomial is directly related to the order of the implicit discrete solver. I hope that helps.

beta2_default(alg::IDSolve) = 0
beta1_default(alg::IDSolve, beta2) = 0

dt_required(alg::IDSolve) = false
isdiscretealg(alg::IDSolve) = true

isadaptive(alg::IDSolve) = true

# @concrete struct ConvergenceRateTracing
# inner_tracing
# end

# @concrete struct ConvergenceRateTraceTrick
# incrementL2norms
# residualL2norms
# trace_wrapper
# end

# function NonlinearSolveBase.init_nonlinearsolve_trace(
# prob, alg::IDSolve, u, fu, J, δu;
# trace_level::ConvergenceRateTracing, kwargs... # This kind of dispatch does not work. Need to figure out a different way.
# )
# inner_trace = NonlinearSolveBase.init_nonlinearsolve_trace(
# prob, alg, u, fu, J, δu;
# trace_level.inner_tracing, kwargs...
# )

# return ConvergenceRateTraceTrick(eltype(δu)[], eltype(fu)[], inner_trace)
# end
Comment on lines +23 to +43
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From my understanding, it should be possible to use NonlinearSolveBase.init_nonlinearsolve_trace to query convergence rate estimates. However, in the current design I cannot add a new dispatch. Should I make a PR to pull the trace level before the kwargs (i.e. NonlinearSolveBase.init_nonlinearsolve_trace(prob, alg, u, fu, J, δu, trace_level; kwargs...) for custom dispatches?

19 changes: 19 additions & 0 deletions lib/ImplicitDiscreteSolve/src/algorithms.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
IDSolve()

First order solver for `ImplicitDiscreteSystems`.
"""
struct IDSolve{NLS} <:
OrdinaryDiffEqAlgorithm
nlsolve::NLS
extrapolant::Symbol
controller::Symbol
end

function IDSolve(;
nlsolve = NewtonRaphson(),
extrapolant = :constant,
controller = :PI
)
IDSolve{typeof(nlsolve)}(nlsolve, extrapolant, controller)
end
52 changes: 41 additions & 11 deletions lib/ImplicitDiscreteSolve/src/cache.jl
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
mutable struct ImplicitDiscreteState{uType, pType, tType}
struct ImplicitDiscreteState{uType, pType, tType}
u::uType
p::pType
t_next::tType
t::tType
end

mutable struct IDSolveCache{uType} <: OrdinaryDiffEqMutableCache
mutable struct IDSolveCache{uType, cType, thetaType} <: OrdinaryDiffEqMutableCache
u::uType
uprev::uType
state::ImplicitDiscreteState
prob::Union{Nothing, SciMLBase.AbstractNonlinearProblem}
z::uType
nlcache::cType
Θks::thetaType
end

function alg_cache(alg::IDSolve, u, rate_prototype, ::Type{uEltypeNoUnits},
::Type{uBottomEltypeNoUnits}, ::Type{tTypeNoUnits}, uprev, uprev2, f, t,
dt, reltol, p, calck,
::Val{true}) where {uEltypeNoUnits, uBottomEltypeNoUnits, tTypeNoUnits}
state = ImplicitDiscreteState(isnothing(u) ? nothing : zero(u), p, t)
IDSolveCache(u, uprev, state, nothing)
end
f_nl = (resid, u_next, p) -> f(resid, u_next, p.u, p.p, p.t)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is the reasoning here to include the current u in the signature of this function, but no information on dt?

Copy link
Member

Choose a reason for hiding this comment

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

dt is just a parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess my question is simply, why is $dt$ (or tprev) not a parameter, but $uprev$ is part of the function signature?


isdiscretecache(cache::IDSolveCache) = true
u_len = isnothing(u) ? 0 : length(u)
nlls = !isnothing(f.resid_prototype) && (length(f.resid_prototype) != u_len)
unl = isnothing(u) ? Float64[] : u # FIXME nonlinear solve cannot handle nothing for u
prob = if nlls
NonlinearLeastSquaresProblem{isinplace(f)}(
NonlinearFunction(f_nl; resid_prototype = f.resid_prototype),
unl, state)
else
NonlinearProblem{isinplace(f)}(f_nl, unl, state)
end

nlcache = init(prob, alg.nlsolve)

struct IDSolveConstantCache <: OrdinaryDiffEqConstantCache
prob::Union{Nothing, SciMLBase.AbstractNonlinearProblem}
IDSolveCache(u, uprev, state.u, nlcache, uBottomEltypeNoUnits[])
end

isdiscretecache(cache::IDSolveCache) = true

function alg_cache(alg::IDSolve, u, rate_prototype, ::Type{uEltypeNoUnits},
::Type{uBottomEltypeNoUnits}, ::Type{tTypeNoUnits}, uprev, uprev2, f, t,
dt, reltol, p, calck,
::Val{false}) where {uEltypeNoUnits, uBottomEltypeNoUnits, tTypeNoUnits}
@assert !isnothing(u) "Empty u not supported with out of place functions yet."

state = ImplicitDiscreteState(isnothing(u) ? nothing : zero(u), p, t)
IDSolveCache(u, uprev, state, nothing)
f_nl = (u_next, p) -> f(u_next, p.u, p.p, p.t)

u_len = isnothing(u) ? 0 : length(u)
nlls = !isnothing(f.resid_prototype) && (length(f.resid_prototype) != u_len)
unl = isnothing(u) ? Float64[] : u # FIXME nonlinear solve cannot handle nothing for u
prob = if nlls
NonlinearLeastSquaresProblem{isinplace(f)}(
NonlinearFunction(f_nl; resid_prototype = f.resid_prototype),
unl, state)
else
NonlinearProblem{isinplace(f)}(f_nl, unl, state)
end

nlcache = init(prob, alg.nlsolve)

# FIXME Use IDSolveConstantCache?
IDSolveCache(u, uprev, state.u, nlcache, uBottomEltypeNoUnits[])
end

get_fsalfirstlast(cache::IDSolveCache, rate_prototype) = (nothing, nothing)
52 changes: 52 additions & 0 deletions lib/ImplicitDiscreteSolve/src/controller.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Base.@kwdef struct KantorovichTypeController <: OrdinaryDiffEqCore.AbstractController
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO reference and documentation

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I'm not sure what this is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, sorry. I will write the docs, no worries. I just left it here so I do not forget before merging. This is a controller derived from a posteriori estimates on how much the convergence radius in the Newton-Kantorovich theorem changes for some increment $dt_n$ and a solution given at $t_n$.

Θmin::Float64
p::Int64
Θreject::Float64 = 0.95
Θbar::Float64 = 0.5
γ::Float64 = 0.95
qmin::Float64 = 1 / 5
qmax::Float64 = 5.0
end

function OrdinaryDiffEqCore.default_controller(
alg::IDSolve, cache::IDSolveCache, _1, _2, _3)
return KantorovichTypeController(; Θmin = 1 // 8, p = 1)
end

function OrdinaryDiffEqCore.stepsize_controller!(
integrator, controller::KantorovichTypeController, alg::IDSolve
)
@inline g(x) = √(1 + 4x) - 1

# Adapt dt with a priori estimate (Eq. 5.24)
(; Θks) = integrator.cache
(; Θbar, γ, Θmin, qmin, qmax, p) = controller

Θ₀ = length(Θks) > 0 ? max(first(Θks), Θmin) : Θmin
q = clamp(γ * (g(Θbar) / (g(Θ₀)))^(1 / p), qmin, qmax)

return q
end

function OrdinaryDiffEqCore.step_accept_controller!(
integrator, controller::KantorovichTypeController, alg::IDSolve, q
)
return q * integrator.dt
end

function OrdinaryDiffEqCore.step_reject_controller!(
integrator, controller::KantorovichTypeController, alg::IDSolve
)
@inline g(x) = √(1 + 4x) - 1

# Shorten dt according to (Eq. 5.24)
(; Θks) = integrator.cache.nlcache
(; Θbar, Θreject, γ, Θmin, qmin, qmax, p) = controller
for Θk in Θks
if Θk > Θreject
q = clamp(γ * (g(Θbar) / g(Θk))^(1 / p), qmin, qmax)
integrator.dt = q * integrator.dt
return
end
end
end
Loading
Loading