Skip to content

Commit 2b711f3

Browse files
Add nlstep_data to DAEFunction (SciML#1509)
`ODENLStepData` lets a symbolic front end hand an implicit stage solve a pre-built `NonlinearProblem` instead of a stage-equation closure. `ODEFunction` and `SplitFunction` carry it as `nlstep_data`; `DAEFunction` did not, so the fully implicit `0 = F(du, u, p, t)` form — which is what modified nodal analysis produces for circuit models — had no way to reach that path. `ODENLStepData` is reused unchanged rather than adding a DAE-specific type. For a fully implicit DAE both arguments of `F` are affine in the stage unknown, so the same six hooks parametrize the stage system: g(z, p') = F(gamma1 * z + outer_tmp, gamma2 * z + inner_tmp, p, t_c) `gamma2`/`inner_tmp` build the state argument exactly as in the mass-matrix form and `gamma1`/`outer_tmp` build the derivative argument, so with `gamma2 = 1`, `inner_tmp = 0` and a BDF-type `du ≈ (u - tmp) / (γ * dt)`, `gamma1` is the `gamma` of `jac(J, du, u, p, gamma, t)`. The `ODENLStepData` docstring now states this form alongside the mass-matrix one. `NLP <: Union{Nothing, ODENLStepData}` is bounded as it is on `ODEFunction`, so `widen_bounded_type_params` erases it for `AutoSpecialize` and `remake` does not narrow it back. Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
1 parent a8ce2b7 commit 2b711f3

3 files changed

Lines changed: 95 additions & 17 deletions

File tree

src/odenlstep.jl

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,50 @@
11
"""
22
ODENLStepData(nlprob, u0perm, set_gamma_c, set_outer_tmp, set_inner_tmp, nlprobmap)
33
4-
A collection of hooks for custom nonlinear stage solves in implicit ODE
4+
A collection of hooks for custom nonlinear stage solves in implicit ODE and DAE
55
algorithms.
66
7-
`ODENLStepData` lets an `ODEFunction` provide a structured
7+
`ODENLStepData` lets an `ODEFunction`, `SplitFunction` or `DAEFunction` provide a structured
88
`AbstractNonlinearProblem` template for solver packages that form implicit stage
99
equations. Before each nonlinear solve, the algorithm updates the stage guess,
1010
scaling factors, time information, and temporary vectors through the stored
1111
setter callables. After the nonlinear solve, `nlprobmap` converts the nonlinear
12-
unknown back to the state vector used by the original ODE problem.
12+
unknown back to the state vector used by the original problem.
1313
14-
The nonlinear problem should represent a stage equation of the form
15-
`M * z = outer_tmp + gamma1 * f(gamma2 * z + inner_tmp, p, t_c)`, equivalently
14+
## Mass-matrix form
15+
16+
For `M * du/dt = f(u, p, t)` the nonlinear problem should represent a stage equation of the
17+
form `M * z = outer_tmp + gamma1 * f(gamma2 * z + inner_tmp, p, t_c)`, equivalently
1618
`g(z, p') = gamma1 * f(gamma2 * z + inner_tmp, p, t_c) + outer_tmp - M * z`.
1719
Here `z` is the nonlinear stage unknown, `p` is the ODE parameter object, `t_c`
1820
is the stage evaluation time, and `gamma1`, `gamma2`, `outer_tmp`, and
1921
`inner_tmp` are supplied by the ODE algorithm.
2022
23+
## Fully implicit form
24+
25+
For `0 = F(du, u, p, t)` (a `DAEFunction`) the stage equation has the same shape, with both
26+
arguments of `F` affine in the stage unknown:
27+
`g(z, p') = F(gamma1 * z + outer_tmp, gamma2 * z + inner_tmp, p, t_c)`.
28+
`gamma2` and `inner_tmp` build the state argument from the stage unknown exactly as in the
29+
mass-matrix form, while `gamma1` and `outer_tmp` build the derivative argument. Taking the
30+
stage unknown to be the stage state (`gamma2 = 1`, `inner_tmp = 0`), a BDF-type step with
31+
`du ≈ (u - tmp) / (γ * dt)` gives `gamma1 = inv(γ * dt)` and `outer_tmp = -tmp / (γ * dt)`.
32+
With that convention `gamma1` is the `gamma` of the `DAEFunction` Jacobian signature
33+
`jac(J, du, u, p, gamma, t)`: the Jacobian of the stage residual with respect to `z` is
34+
`gamma1 * dF/d(du) + dF/du`.
35+
2136
# Fields
2237
2338
$(TYPEDFIELDS)
2439
2540
# Extension Rules
2641
27-
Symbolic-system packages construct this value and store it as an `ODEFunction`'s
28-
nonlinear-stage metadata. Solver packages may consume the six fields through their
29-
callable contracts, but must not assume concrete callable types or mutate the container.
30-
Each setter must update the object it closes over consistently with `nlprob`, and
31-
`nlprobmap` must map a completed nonlinear solution back to the ODE stage representation.
42+
Symbolic-system packages construct this value and store it as the `nlstep_data` of an
43+
`ODEFunction`, `SplitFunction` or `DAEFunction`. Solver packages may consume the six fields
44+
through their callable contracts, but must not assume concrete callable types or mutate the
45+
container. Each setter must update the object it closes over consistently with `nlprob`, and
46+
`nlprobmap` must map a completed nonlinear solution back to the stage representation of the
47+
original problem.
3248
"""
3349
struct ODENLStepData{NLProb, SetU0, SetGammaC, SetOuterTmp, SetInnerTmp, NLProbMap}
3450
"""

src/scimlfunctions.jl

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,7 +1586,8 @@ DAEFunction{iip,specialize}(f;
15861586
jac_prototype = __has_jac_prototype(f) ? f.jac_prototype : nothing,
15871587
sparsity = __has_sparsity(f) ? f.sparsity : jac_prototype,
15881588
colorvec = __has_colorvec(f) ? f.colorvec : nothing,
1589-
sys = __has_sys(f) ? f.sys : nothing)
1589+
sys = __has_sys(f) ? f.sys : nothing,
1590+
nlstep_data = __has_nlstep_data(f) ? f.nlstep_data : nothing)
15901591
```
15911592
15921593
Note that only the function `f` itself is required. This function should
@@ -1622,6 +1623,11 @@ the usage of `f`. These include:
16221623
based on the sparsity pattern. Defaults to `nothing`, which means a color vector will be
16231624
internally computed on demand when required. The cost of this operation is highly dependent
16241625
on the sparsity pattern.
1626+
- `nlstep_data`: an [`ODENLStepData`](@ref SciMLBase.ODENLStepData) holding a structured
1627+
nonlinear problem for the implicit stage solve, or `nothing`. Implicit DAE integrators
1628+
which support it solve this problem in place of building a stage-equation closure. See the
1629+
`ODENLStepData` documentation for the stage equation the nonlinear problem must represent
1630+
in the fully implicit case.
16251631
16261632
## iip: In-Place vs Out-Of-Place
16271633
@@ -1685,7 +1691,7 @@ numerically-defined functions.
16851691
struct DAEFunction{
16861692
iip, specialize, F, Ta, Tt, TJ, TJU, TJD, JVP, VJP, JP, SP, TW, TWt, TPJ, O,
16871693
TCV,
1688-
SYS, ID,
1694+
SYS, ID, NLP <: Union{Nothing, ODENLStepData},
16891695
} <:
16901696
AbstractDAEFunction{iip}
16911697
f::F
@@ -1705,6 +1711,7 @@ struct DAEFunction{
17051711
colorvec::TCV
17061712
sys::SYS
17071713
initialization_data::ID
1714+
nlstep_data::NLP
17081715
end
17091716

17101717
"""
@@ -4143,7 +4150,8 @@ function DAEFunction{iip, specialize}(
41434150
initializeprobmap = __has_initializeprobmap(f) ? f.initializeprobmap : nothing,
41444151
initializeprobpmap = __has_initializeprobpmap(f) ? f.initializeprobpmap : nothing,
41454152
initialization_data = __has_initialization_data(f) ? f.initialization_data :
4146-
nothing
4153+
nothing,
4154+
nlstep_data = __has_nlstep_data(f) ? f.nlstep_data : nothing
41474155
) where {
41484156
iip,
41494157
specialize,
@@ -4188,12 +4196,12 @@ function DAEFunction{iip, specialize}(
41884196
iip, specialize, Any, Any, Any,
41894197
Any, Any, Any, Any, Any, Any, Any,
41904198
Any, Any, Any,
4191-
Any, typeof(_colorvec), Any, Any,
4199+
Any, typeof(_colorvec), Any, Any, Union{Nothing, ODENLStepData},
41924200
}(
41934201
_f, analytic, tgrad, jac, jac_u, jac_du, jvp,
41944202
vjp, jac_prototype, sparsity,
41954203
Wfact, Wfact_t, paramjac, observed,
4196-
_colorvec, sys, initdata
4204+
_colorvec, sys, initdata, nlstep_data
41974205
)
41984206
else
41994207
DAEFunction{
@@ -4203,12 +4211,12 @@ function DAEFunction{iip, specialize}(
42034211
typeof(sparsity), typeof(Wfact), typeof(Wfact_t),
42044212
typeof(paramjac),
42054213
typeof(observed), typeof(_colorvec),
4206-
typeof(sys), typeof(initdata),
4214+
typeof(sys), typeof(initdata), typeof(nlstep_data),
42074215
}(
42084216
_f, analytic, tgrad, jac, jac_u, jac_du, jvp, vjp,
42094217
jac_prototype, sparsity, Wfact, Wfact_t,
42104218
paramjac, observed,
4211-
_colorvec, sys, initdata
4219+
_colorvec, sys, initdata, nlstep_data
42124220
)
42134221
end
42144222
end

test/function_building_error_messages.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,60 @@ DAEFunction(dfiip, vjp = dvjp)
387387
DAEFunction(dfoop, vjp = dvjp)
388388
DAEFunction{true, SciMLBase.NoSpecialize}(dfiip, observed = 1)
389389

390+
@testset "DAEFunction nlstep_data" begin
391+
nlstep = SciMLBase.ODENLStepData(
392+
NonlinearProblem((z, p) -> z, [1.0]), identity,
393+
(gamma1, gamma2, c) -> nothing, identity, identity, identity
394+
)
395+
396+
@testset "construction" begin
397+
@test DAEFunction(dfiip).nlstep_data === nothing
398+
@test typeof(DAEFunction(dfiip)).parameters[end] === Nothing
399+
@test DAEFunction(dfiip; nlstep_data = nlstep).nlstep_data === nlstep
400+
@test DAEFunction(dfoop; nlstep_data = nlstep).nlstep_data === nlstep
401+
# a `DAEFunction` passed as the function carries its `nlstep_data` over
402+
wrapped = DAEFunction{true, SciMLBase.FullSpecialize}(
403+
DAEFunction(dfiip; nlstep_data = nlstep)
404+
)
405+
@test wrapped.nlstep_data === nlstep
406+
end
407+
408+
@testset "specialization $spec" for spec in (
409+
SciMLBase.FullSpecialize, SciMLBase.NoSpecialize, SciMLBase.AutoSpecialize,
410+
)
411+
f = DAEFunction{true, spec}(dfiip; nlstep_data = nlstep)
412+
@test f.nlstep_data === nlstep
413+
414+
# `remake` reconstructs the struct through the keyword constructor, which builds
415+
# it positionally with an explicit type-parameter list. `unwrapped_f` has no
416+
# `DAEFunction` method and so is the identity here; assert it anyway so a future
417+
# method has to keep the field.
418+
@test SciMLBase.unwrapped_f(f).nlstep_data === nlstep
419+
@test SciMLBase.remake(f).nlstep_data === nlstep
420+
@test SciMLBase.remake(f; jac_prototype = zeros(1, 1)).nlstep_data === nlstep
421+
# explicit overrides still win, matching `ODEFunction`
422+
@test SciMLBase.remake(f; nlstep_data = nothing).nlstep_data === nothing
423+
424+
# AutoSpecialize erases the bounded parameters to keep the function type
425+
# model-independent; `remake` must not narrow them back.
426+
widened = SciMLBase.widen_bounded_type_params(f)
427+
@test typeof(widened).parameters[end] === Union{Nothing, SciMLBase.ODENLStepData}
428+
@test widened.nlstep_data === nlstep
429+
rewidened = SciMLBase.remake(widened; jac_prototype = zeros(1, 1))
430+
@test typeof(rewidened).parameters[end] ===
431+
Union{Nothing, SciMLBase.ODENLStepData}
432+
@test rewidened.nlstep_data === nlstep
433+
end
434+
435+
@testset "problem construction" begin
436+
prob = DAEProblem(
437+
DAEFunction(dfiip; nlstep_data = nlstep), [0.0], [1.0], (0.0, 1.0)
438+
)
439+
@test prob.f.nlstep_data === nlstep
440+
@test SciMLBase.remake(prob; u0 = [2.0]).f.nlstep_data === nlstep
441+
end
442+
end
443+
390444
# DDEFunction
391445

392446
ddefoop(u, h, p, t) = u

0 commit comments

Comments
 (0)