Skip to content

Commit 3be79c7

Browse files
feat: accept array equations on the NonlinearProblem path
1 parent 6b06080 commit 3be79c7

10 files changed

Lines changed: 813 additions & 58 deletions

File tree

docs/src/API/model_building.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,11 @@ It is also possible (though not always advisable) to build numerical problems fr
160160
passing them through `mtkcompile`. To do this, the system must first be marked as "complete" via
161161
the `complete` function. This process is used to indicate that a system will not be modified
162162
further and allows ModelingToolkit to perform any necessary preprocessing to it. `mtkcompile`
163-
calls `complete` internally.
163+
calls `complete` internally. `NonlinearProblem` and `NonlinearLeastSquaresProblem`
164+
accept unscalarized array equations with scalar unknowns (`collect(u)`). Each
165+
array equation contributes one residual row per element and may be mixed with
166+
scalar equations. Use `mtkcompile` before requesting `jac = true` or
167+
`sparse = true`.
164168

165169
```@docs
166170
complete

docs/src/tutorials/nonlinear.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ Jacobian function:
4141
prob = NonlinearProblem(ns, vcat(guesses, ps), jac = true)
4242
sol = solve(prob, NewtonRaphson())
4343
```
44+
45+
!!! note "Array residuals"
46+
47+
`NonlinearProblem` and `NonlinearLeastSquaresProblem` accept unscalarized
48+
array equations after [`complete`](@ref) when unknowns are scalar
49+
(`collect(u)`). Use [`mtkcompile`](@ref) before requesting `jac = true` or
50+
`sparse = true`. Time-dependent systems are converted to steady state.

lib/ModelingToolkitBase/src/problems/nonlinearproblem.jl

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,31 @@ function SciMLBase.NonlinearFunction{iip, spec}(
2828
) where {iip, spec, E}
2929
check_complete(sys, NonlinearFunction)
3030
opts.check_compatibility && check_compatible_system(NonlinearFunction, sys)
31+
check_array_unknowns(unknowns(sys))
3132

3233
(; u0, p, jac, sparse, analytic, simplify, initialization_data) = opts
3334
codegen_opts = opts.codegen
3435

36+
if (jac || sparse) && has_array_equations(equations(sys))
37+
throw(
38+
ArgumentError(
39+
"Array residuals do not support `jac = true` or `sparse = true`. " *
40+
"Call `mtkcompile` first."
41+
)
42+
)
43+
end
44+
3545
f = generate_rhs(sys, codegen_opts)
3646

3747
if spec === SciMLBase.FunctionWrapperSpecialize && iip
3848
if u0 === nothing || p === nothing
3949
error("u0, and p must be specified for FunctionWrapperSpecialize on NonlinearFunction.")
4050
end
51+
resid = resid_prototype === nothing ? u0 : resid_prototype
4152
if E
42-
f = :($(SciMLBase.wrapfun_iip)($f, ($u0, $u0, $p)))
53+
f = :($(SciMLBase.wrapfun_iip)($f, ($resid, $u0, $p)))
4354
else
44-
f = SciMLBase.wrapfun_iip(f, (u0, u0, p))
55+
f = SciMLBase.wrapfun_iip(f, (resid, u0, p))
4556
end
4657
end
4758

@@ -181,8 +192,8 @@ end
181192
_iip = resolve_iip(iip, op)
182193
f, u0,
183194
p = process_SciMLProblem(
184-
NonlinearFunction{_iip}, sys, op;
185-
check_length, expression, kwargs...
195+
NonlinearFunction{_iip, spec}, sys, op;
196+
check_length, check_compatibility, expression, kwargs...
186197
)
187198

188199
if lb === nothing && ub === nothing

lib/ModelingToolkitBase/src/systems/abstractsystem.jl

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3277,13 +3277,30 @@ function Base.eltype(::Type{<:TreeIterator{ModelingToolkitBase.AbstractSystem}})
32773277
return ModelingToolkitBase.AbstractSystem
32783278
end
32793279

3280-
function check_array_equations_unknowns(eqs, dvs)
3281-
if any(eq -> eq isa Equation && Symbolics.isarraysymbolic(eq.lhs), eqs)
3282-
throw(ArgumentError("The system has array equations. Call `mtkcompile` to handle such equations or scalarize them manually."))
3280+
function has_array_equations(eqs)
3281+
return any(eq -> eq isa Equation && SU.is_array_shape(SU.shape(eq.lhs)), eqs)
3282+
end
3283+
3284+
const ARRAY_EQUATIONS_ERROR = "The system has array equations. Call `mtkcompile` to handle such equations or scalarize them manually."
3285+
const ARRAY_UNKNOWNS_ERROR = "The system has array unknowns. Call `mtkcompile` to handle this or scalarize them manually with `collect(u)`."
3286+
3287+
function check_array_equations(eqs)
3288+
if has_array_equations(eqs)
3289+
throw(ArgumentError(ARRAY_EQUATIONS_ERROR))
32833290
end
3284-
return if any(x -> Symbolics.isarraysymbolic(x), dvs)
3285-
throw(ArgumentError("The system has array unknowns. Call `mtkcompile` to handle this or scalarize them manually."))
3291+
return nothing
3292+
end
3293+
3294+
function check_array_unknowns(dvs)
3295+
if any(Symbolics.isarraysymbolic, dvs)
3296+
throw(ArgumentError(ARRAY_UNKNOWNS_ERROR))
32863297
end
3298+
return nothing
3299+
end
3300+
3301+
function check_array_equations_unknowns(eqs, dvs)
3302+
check_array_equations(eqs)
3303+
return check_array_unknowns(dvs)
32873304
end
32883305

32893306
"""

lib/ModelingToolkitBase/src/systems/codegen.jl

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ const EXPERIMENTAL_WARNING = """
1515
This API is experimental and may change in a future non-breaking release.
1616
"""
1717

18-
"""
19-
Treat a derivative of an array-valued expression as a leaf, so that
20-
[`expand_array_derivatives!`](@ref) collects `D(u[2:4])` itself rather than descending into
21-
it. Scalar variables are not atomic here, so nothing else is collected.
22-
"""
23-
function array_derivative_is_atomic(ex::SymbolicT)
24-
return isdifferential(ex) && SU.is_array_shape(SU.shape(ex))
25-
end
26-
2718
"""
2819
$(TYPEDSIGNATURES)
2920
@@ -43,25 +34,7 @@ function expand_array_derivatives!(rhss::Vector{SymbolicT}, ir::IRStructure{Vart
4334
end
4435
isempty(terms) && return rhss
4536

46-
subs = Dict{SymbolicT, SymbolicT}()
47-
for term in terms
48-
op = operation(term)
49-
arg = only(arguments(term))
50-
sh = SU.shape(arg)::SU.ShapeVecT
51-
# Preserve the shape: a derivative of a 2D slice must expand to a 2D array of
52-
# scalar derivatives, or it will not broadcast against the surrounding slices.
53-
arrargs = Symbolics.SArgsT()
54-
sizehint!(arrargs, prod(length, sh; init = 1) + 1)
55-
push!(arrargs, SU.Const{VartypeT}(size(arg)))
56-
for idx in SU.stable_eachindex(arg)
57-
push!(arrargs, op(arg[idx]))
58-
end
59-
subs[term] = Symbolics.STerm(
60-
SU.array_literal, arrargs; type = symtype(arg), shape = sh
61-
)
62-
end
63-
64-
subber = SU.IRSubstituter{false}(ir, subs)
37+
subber = SU.IRSubstituter{false}(ir, array_derivative_expansion_map(terms))
6538
map!(subber, rhss, rhss)
6639
return rhss
6740
end
@@ -100,6 +73,88 @@ function array_residual_maker(rhss::Vector{SymbolicT})
10073
return SU.ArrayMaker{VartypeT}(regions, values)
10174
end
10275

76+
"""
77+
residual_eltype(x)
78+
79+
Return the numeric element type contributed by `x`, or `Union{}`.
80+
"""
81+
function residual_eltype(x)
82+
x = SciMLBase.unwrap_parameters(x)
83+
if x isa Number
84+
return typeof(x)
85+
elseif x isa AbstractArray
86+
T = eltype(x)
87+
return T === Any ? Union{} : T
88+
elseif SciMLStructures.isscimlstructure(x)
89+
tun = first(SciMLStructures.canonicalize(SciMLStructures.Tunable(), x))
90+
return residual_eltype(tun)
91+
else
92+
return Union{}
93+
end
94+
end
95+
96+
"""
97+
similar_for_residual(prototype, extras...)
98+
99+
Return an allocator using `prototype` and the promoted runtime element type.
100+
"""
101+
function similar_for_residual(prototype, extras...)
102+
T = residual_eltype(prototype)
103+
T === Union{} && (T = Float64)
104+
for x in extras
105+
T = promote_type(T, residual_eltype(x))
106+
end
107+
return sz -> similar(prototype, T, sz)
108+
end
109+
110+
function residual_allocator_arg(arg)
111+
name = arg isa DestructuredArgs ? arg.name : arg
112+
name isa SymbolicT && return name
113+
name isa Symbol || throw(
114+
ArgumentError(
115+
"Cannot form a residual allocator argument from $(typeof(arg))."
116+
)
117+
)
118+
return SSym(name; type = Any, shape = SU.ShapeVecT())
119+
end
120+
121+
function residual_allocator_term(args)
122+
return STerm(
123+
similar_for_residual, SArgsT((map(residual_allocator_arg, args)...,));
124+
type = SU.FnType{Tuple, Any, Any},
125+
shape = SU.ShapeVecT(),
126+
)
127+
end
128+
129+
function inject_similar_for_residual(body, alloc_term)
130+
if body isa Let
131+
return Let(
132+
body.pairs, inject_similar_for_residual(body.body, alloc_term),
133+
body.let_block
134+
)
135+
elseif body isa SymbolicT && Code.supports_with_allocator(body)
136+
# The public helper would wrap the symbolic allocator in `Const`.
137+
return STerm(
138+
Code.with_allocator,
139+
SArgsT((alloc_term, body));
140+
type = SU.symtype(body),
141+
shape = SU.shape(body),
142+
)
143+
else
144+
return body
145+
end
146+
end
147+
148+
function wrap_oop_similar_for_residual(fn)
149+
fn isa Func || return fn
150+
isempty(fn.args) && return fn
151+
alloc_term = residual_allocator_term(fn.args)
152+
return Func(
153+
fn.args, fn.kwargs, inject_similar_for_residual(fn.body, alloc_term),
154+
fn.pre
155+
)
156+
end
157+
103158
"""
104159
$(TYPEDSIGNATURES)
105160
@@ -121,6 +176,10 @@ $GENERATE_X_KWARGS
121176
by default, which leaves the standard codegen path byte-identical.
122177
123178
All other keyword arguments are forwarded to [`build_function_wrapper`](@ref).
179+
180+
Time-independent systems use `_iszero(lhs) ? rhs : rhs - lhs` and
181+
`array_residual_maker`. No `du`. Out-of-place ArrayMaker residuals promote
182+
their element type from the function arguments.
124183
"""
125184
function generate_rhs(
126185
sys::System, opts::GeneratedFunctionOptions;
@@ -175,6 +234,9 @@ function generate_rhs(
175234
expand_array_derivatives!(rhss, get_irstructure(sys))
176235
assemble_residuals = true
177236
end
237+
elseif !is_time_dependent(sys)
238+
rhss = SymbolicT[_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs]
239+
assemble_residuals = true
178240
else
179241
if !override_discrete && !is_discrete_system(sys)
180242
check_operator_variables(eqs, Differential)
@@ -223,10 +285,20 @@ function generate_rhs(
223285
(; p_end = (t === nothing ? length(args) : length(args) - 1) - length(extra_args))
224286

225287
u_arg = scalar ? -1 : (implicit_dae ? 2 : 1)
288+
codegen_opts = opts.codegen
289+
if !implicit_dae && assemble_residuals && rhss isa SymbolicT &&
290+
Code.supports_with_allocator(rhss)
291+
# `ArrayMaker` otherwise allocates a `Float64` buffer out of place.
292+
oop_wrap, iip_wrap = codegen_opts.wrap_code
293+
codegen_opts = setproperties(
294+
codegen_opts,
295+
(; wrap_code = (wrap_oop_similar_for_residual oop_wrap, iip_wrap))
296+
)
297+
end
226298
res = build_function_wrapper(
227299
sys, rhss, collect(Any, args), BuildFunctionWrapperOptions(;
228300
p_start, extra_assignments, u_arg, n_param_buffers, p_end_kw...,
229-
codegen_function_options = opts.codegen
301+
codegen_function_options = codegen_opts
230302
)
231303
)
232304
nargs = length(args) - length(p) + 1

lib/ModelingToolkitBase/src/systems/problem_utils.jl

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2198,9 +2198,13 @@ function __process_SciMLProblem(
21982198
iv = has_iv(sys) ? get_iv(sys) : nothing
21992199
eqs = equations(sys)
22002200

2201-
# Implicit-DAE codegen expands an array equation into one output row per element, so
2202-
# array equations are usable there. Every other problem type still needs `mtkcompile`.
2203-
implicit_dae || check_array_equations_unknowns(eqs, dvs)
2201+
if !implicit_dae
2202+
if constructor <: NonlinearFunction
2203+
check_array_unknowns(dvs)
2204+
else
2205+
check_array_equations_unknowns(eqs, dvs)
2206+
end
2207+
end
22042208

22052209
op = build_operating_point(sys, op; fast_path = true)
22062210

@@ -2318,17 +2322,20 @@ function __process_SciMLProblem(
23182322
du0 = nothing
23192323
end
23202324

2321-
if constructor <: NonlinearFunction && length(dvs) != length(eqs)
2322-
kwargs = merge(
2323-
kwargs,
2324-
(;
2325-
resid_prototype = u0_constructor(
2326-
calculate_resid_prototype(
2327-
length(eqs), u0, p
2328-
)
2329-
),
2325+
if constructor <: NonlinearFunction
2326+
nrows = count_equation_rows(eqs)
2327+
if length(dvs) != nrows
2328+
kwargs = merge(
2329+
kwargs,
2330+
(;
2331+
resid_prototype = u0_constructor(
2332+
calculate_resid_prototype(
2333+
nrows, u0, p
2334+
)
2335+
),
2336+
)
23302337
)
2331-
)
2338+
end
23322339
end
23332340

23342341
f = constructor(

lib/ModelingToolkitBase/src/systems/system.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,11 +1345,11 @@ end
13451345
13461346
Given a time-dependent system `sys` of ODEs, convert it to a time-independent system of
13471347
nonlinear equations that solve for the steady-state of the unknowns. This is done by
1348-
replacing every derivative `D(x)` of an unknown `x` with zero. Note that this process
1349-
does not retain noise equations, brownian terms, jumps or costs associated with `sys`.
1350-
All other information such as initial conditions, bindings, guesses, observed and
1351-
initialization equations are retained. The independent variable of `sys` becomes a
1352-
parameter of the returned system.
1348+
replacing every derivative `D(x)` of an unknown `x` with zero. Array derivatives are
1349+
expanded first. Note that this process does not retain noise equations, brownian terms,
1350+
jumps or costs associated with `sys`. All other information such as initial conditions,
1351+
bindings, guesses, observed and initialization equations are retained. The independent
1352+
variable of `sys` becomes a parameter of the returned system.
13531353
13541354
If `sys` is hierarchical (it contains subsystems) this transformation will be applied
13551355
recursively to all subsystems. The output system will be marked as `complete` if and only
@@ -1362,7 +1362,7 @@ function NonlinearSystem(sys::System)
13621362
if !is_time_dependent(sys)
13631363
throw(ArgumentError("`NonlinearSystem` constructor expects a time-dependent `System`"))
13641364
end
1365-
eqs = equations(sys)
1365+
eqs = expand_array_derivatives(equations(sys))
13661366
obs = observed(sys)
13671367
D = Differential(get_iv(sys))
13681368
subrules = Dict([D(x) => 0.0 for x in unknowns(sys)])

lib/ModelingToolkitBase/src/utils.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,44 @@ isoperator(::Type{op}) where {op <: SU.Operator} = Base.Fix2(isoperator, op)
688688
isdifferential(expr) = isoperator(expr, Differential)
689689
isdiffeq(eq) = isdifferential(eq.lhs) || isoperator(eq.lhs, Shift)
690690

691+
function array_derivative_is_atomic(ex::SymbolicT)
692+
return isdifferential(ex) && SU.is_array_shape(SU.shape(ex))
693+
end
694+
695+
function array_derivative_expansion(term::SymbolicT)
696+
op = operation(term)
697+
arg = only(arguments(term))
698+
sh = SU.shape(arg)::SU.ShapeVecT
699+
# Preserve rank for broadcasts against surrounding slices.
700+
arrargs = Symbolics.SArgsT()
701+
sizehint!(arrargs, prod(length, sh; init = 1) + 1)
702+
push!(arrargs, SU.Const{VartypeT}(size(arg)))
703+
for idx in SU.stable_eachindex(arg)
704+
push!(arrargs, op(arg[idx]))
705+
end
706+
return Symbolics.STerm(
707+
SU.array_literal, arrargs; type = symtype(arg), shape = sh
708+
)
709+
end
710+
711+
function array_derivative_expansion_map(terms)
712+
subs = Dict{SymbolicT, SymbolicT}()
713+
for term in terms
714+
subs[term] = array_derivative_expansion(term)
715+
end
716+
return subs
717+
end
718+
719+
function expand_array_derivatives(eqs::Vector{Equation})
720+
terms = Set{SymbolicT}()
721+
for eq in eqs
722+
SU.search_variables!(terms, eq; is_atomic = array_derivative_is_atomic)
723+
end
724+
isempty(terms) && return eqs
725+
subs = array_derivative_expansion_map(terms)
726+
return map(eq -> substitute(eq, subs), eqs)
727+
end
728+
691729
isvariable(x::Num)::Bool = isvariable(value(x))
692730
function isvariable(x)
693731
x isa SymbolicT || return false

0 commit comments

Comments
 (0)