-
-
Notifications
You must be signed in to change notification settings - Fork 262
feat: accept array equations on the implicit-DAE path #4983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
16ef8be
7872431
75d2e96
d9c3814
d6640a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -37,6 +37,84 @@ $GENERATE_X_KWARGS | |||||
|
|
||||||
| All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). | ||||||
| """ | ||||||
|
|
||||||
| """ | ||||||
| Treat a derivative of an array-valued expression as a leaf, so that | ||||||
| [`expand_array_derivatives`](@ref) collects `D(u[2:4])` itself rather than descending into | ||||||
| it. Scalar variables are not atomic here, so nothing else is collected. | ||||||
| """ | ||||||
| struct ArrayDerivativeIsAtomic end | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be a callable singleton struct instead of a normal function? |
||||||
|
|
||||||
| function (::ArrayDerivativeIsAtomic)(ex::SymbolicT) | ||||||
| return isdifferential(ex) && SU.is_array_shape(SU.shape(ex)) | ||||||
| end | ||||||
|
|
||||||
| """ | ||||||
| $(TYPEDSIGNATURES) | ||||||
|
|
||||||
| Rewrite derivatives of array-valued expressions, such as `D(u[2:4])`, into arrays of the | ||||||
| corresponding scalar derivatives. Implicit-DAE codegen binds scalar `D(uᵢ)` terms to | ||||||
| elements of the `du` argument, and a derivative of a slice matches none of them. | ||||||
|
|
||||||
| Takes every residual at once so that the search and substitution caches are shared. | ||||||
| """ | ||||||
| function expand_array_derivatives(rhss::Vector{SymbolicT}) | ||||||
| terms = Set{SymbolicT}() | ||||||
| for rhs in rhss | ||||||
| SU.search_variables!(terms, rhs; is_atomic = ArrayDerivativeIsAtomic()) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||||||
| end | ||||||
| isempty(terms) && return rhss | ||||||
|
|
||||||
| subs = Dict{SymbolicT, SymbolicT}() | ||||||
| for term in terms | ||||||
| op = operation(term) | ||||||
| arg = only(arguments(term)) | ||||||
| sh = SU.shape(arg) | ||||||
| # Preserve the shape: a derivative of a 2D slice must expand to a 2D array of | ||||||
| # scalar derivatives, or it will not broadcast against the surrounding slices. | ||||||
| sz = ntuple(i -> length(sh[i]), length(sh)) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| els = [op(arg[idx]) for idx in SU.stable_eachindex(arg)] | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A better method is to build |
||||||
| subs[term] = SU.Const{VartypeT}(reshape(els, sz)) | ||||||
| end | ||||||
|
|
||||||
| subber = SU.IRSubstituter{false}(IRStructure{VartypeT}(), subs) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just reuse |
||||||
| return map(subber, rhss) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't need the |
||||||
| end | ||||||
|
|
||||||
| """ | ||||||
| $(TYPEDSIGNATURES) | ||||||
|
|
||||||
| Assemble residuals into a single array expression, where each residual writes to a | ||||||
| contiguous region of the output. An array-valued residual stands for one output row per | ||||||
| element, and writing it as a region keeps the array computation intact instead of | ||||||
| scalarizing it into one expression per row. | ||||||
|
|
||||||
| Returns `rhss` unchanged when every residual is scalar. | ||||||
| """ | ||||||
| function array_residual_maker(rhss::Vector{SymbolicT}) | ||||||
| any(rhs -> SU.is_array_shape(SU.shape(rhs)), rhss) || return rhss | ||||||
|
|
||||||
| regions = Vector{Vector{UnitRange{Int}}}(undef, length(rhss)) | ||||||
| values = similar(rhss) | ||||||
| offset = 0 | ||||||
| for (i, rhs) in enumerate(rhss) | ||||||
| sh = SU.shape(rhs) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
For now, since this code assumes it anyway. We'll have to lift this restriction to allow resizing the PDE without redoing codegen. |
||||||
| if SU.is_array_shape(sh) | ||||||
| n = prod(length, sh) | ||||||
| # Elements become consecutive output rows, so a rank > 1 residual is flattened | ||||||
| # rather than given a multidimensional region. `vec` here stays symbolic. | ||||||
| values[i] = length(sh) == 1 ? rhs : vec(rhs) | ||||||
| else | ||||||
| n = 1 | ||||||
| # `ArrayMaker` regions only accept array-valued entries. | ||||||
| values[i] = SU.Const{VartypeT}([rhs]) | ||||||
| end | ||||||
| regions[i] = [(offset + 1):(offset + n)] | ||||||
| offset += n | ||||||
| end | ||||||
| return SU.ArrayMaker{VartypeT}(regions, values) | ||||||
| end | ||||||
|
|
||||||
| function generate_rhs( | ||||||
| sys::System, opts::GeneratedFunctionOptions; | ||||||
| implicit_dae::Bool = false, scalar::Bool = false, | ||||||
|
|
@@ -57,6 +135,7 @@ function generate_rhs( | |||||
| t = get_iv(sys) | ||||||
| ddvs = nothing | ||||||
| extra_assignments = Assignment[] | ||||||
| assemble_residuals = false | ||||||
|
|
||||||
| # used for DAEProblem and ImplicitDiscreteProblem | ||||||
| if implicit_dae | ||||||
|
|
@@ -83,7 +162,11 @@ function generate_rhs( | |||||
| else | ||||||
| D = Differential(t) | ||||||
| ddvs = map(D, dvs) | ||||||
| rhss = [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] | ||||||
| rhss = SymbolicT[_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] | ||||||
| # Rewrite array derivatives to the scalar ones bound to the `du` argument. | ||||||
| # Assembly into a single array happens below, after assertions. | ||||||
| rhss = expand_array_derivatives(rhss) | ||||||
| assemble_residuals = true | ||||||
| end | ||||||
| else | ||||||
| if !override_discrete && !is_discrete_system(sys) | ||||||
|
|
@@ -94,7 +177,18 @@ function generate_rhs( | |||||
| end | ||||||
|
|
||||||
| if !isempty(assertions(sys)) && !isempty(rhss) | ||||||
| rhss[end] += unwrap(get_assertions_expr(sys)) | ||||||
| assertion_expr = unwrap(get_assertions_expr(sys)) | ||||||
| # An array-valued residual stands for several output rows, and `+` is not defined | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A future PR should probably make assertions codegen to |
||||||
| # between a symbolic array and a scalar, so add the assertion to each of its rows. | ||||||
| rhss[end] = if SU.is_array_shape(SU.shape(rhss[end])) | ||||||
| unwrap(wrap(rhss[end]) .+ assertion_expr) | ||||||
| else | ||||||
| rhss[end] + assertion_expr | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| if assemble_residuals | ||||||
| rhss = array_residual_maker(rhss) | ||||||
| end | ||||||
|
|
||||||
| # TODO: add an optional check on the ordering of observed equations | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| using ModelingToolkitBase, Test | ||
| using ModelingToolkitBase: unwrap, complete, unknowns | ||
| using Symbolics | ||
| using SciMLBase | ||
| using OrdinaryDiffEqBDF: DFBDF | ||
| using DiffEqBase: BrownFullBasicInit | ||
|
|
||
| # A system whose interior is written as one array equation over slices, as produced by a | ||
| # finite-difference PDE discretization that does not scalarize. | ||
| function heat_array_system(n) | ||
| @independent_variables t | ||
| @variables u(t)[1:n] | ||
| D = Differential(t) | ||
| dx = 1 / (n - 1) | ||
| # Residual (cardinalized) form, as a finite-difference discretization emits it: the | ||
| # derivative sits inside the expression rather than being the equation's whole LHS. | ||
| lap = (u[1:(n - 2)] .- 2 .* u[2:(n - 1)] .+ u[3:n]) ./ dx^2 | ||
| interior = broadcast(-, D(u[2:(n - 1)]), lap) ~ zeros(n - 2) | ||
| eqs = [interior, u[1] ~ 0.0, u[n] ~ 0.0] | ||
| @named sys = System(eqs, t, collect(u), []) | ||
| return complete(sys), u, t, D | ||
| end | ||
|
|
||
| @testset "array equations reach DAEProblem" begin | ||
| n = 11 | ||
| sys, u, t, D = heat_array_system(n) | ||
| xs = range(0.0, 1.0, length = n) | ||
| op = vcat( | ||
| [u[i] => sinpi(xs[i]) for i in 1:n], | ||
| [D(u[i]) => 0.0 for i in 1:n] | ||
| ) | ||
|
|
||
| prob = DAEProblem(sys, op, (0.0, 0.1); build_initializeprob = false) | ||
|
|
||
| # one output row per element of the array equation, not one per equation | ||
| @test length(prob.u0) == n | ||
| @test prob.u0 isa Vector{Float64} | ||
|
|
||
| # the interior points are differential, the two boundary points algebraic | ||
| @test prob.differential_vars !== nothing | ||
| @test count(prob.differential_vars) == n - 2 | ||
|
|
||
| # the residual evaluates: no `Differential` survives into the generated code | ||
| out = zeros(n) | ||
| du = zeros(n) | ||
| prob.f(out, du, prob.u0, prob.p, 0.0) | ||
| @test all(isfinite, out) | ||
| # with du = 0 the interior residual is minus the Laplacian, which is nonzero here | ||
| @test any(!iszero, out) | ||
| end | ||
|
|
||
| @testset "other problem types still require scalarized equations" begin | ||
| n = 11 | ||
| sys, u, t, D = heat_array_system(n) | ||
| op = [u[i] => 0.0 for i in 1:n] | ||
| # ODEProblem cannot consume array equations; the guard must remain in place | ||
| @test_throws Exception ODEProblem(sys, op, (0.0, 0.1); build_initializeprob = false) | ||
| end | ||
|
|
||
| @testset "array-equation DAE solves to the analytic solution" begin | ||
| n = 21 | ||
| sys, u, t, D = heat_array_system(n) | ||
| xs = range(0.0, 1.0, length = n) | ||
| op = vcat( | ||
| [u[i] => sinpi(xs[i]) for i in 1:n], | ||
| [D(u[i]) => 0.0 for i in 1:n] | ||
| ) | ||
| tend = 0.1 | ||
| prob = DAEProblem(sys, op, (0.0, tend); build_initializeprob = false) | ||
| # `du0` above is not consistent; the solver's own DAE initialization supplies it. | ||
| sol = solve( | ||
| prob, DFBDF(); initializealg = BrownFullBasicInit(), | ||
| reltol = 1.0e-8, abstol = 1.0e-8, saveat = [tend] | ||
| ) | ||
| @test SciMLBase.successful_retcode(sol) | ||
| exact = [exp(-pi^2 * tend) * sinpi(xi) for xi in xs] | ||
| # second-order spatial discretization on 21 points | ||
| @test maximum(abs.(sol.u[end] .- exact)) < 5.0e-3 | ||
| end | ||
|
|
||
| @testset "array equations over a 2D slice keep their shape" begin | ||
| # A derivative of a 2D slice must substitute a 2D array of scalar derivatives; a | ||
| # flattened one does not broadcast against the surrounding slices and codegen fails | ||
| # with a DimensionMismatch. | ||
| n = 6 | ||
| @independent_variables t | ||
| @variables w(t)[1:n, 1:n] | ||
| D = Differential(t) | ||
| dx = 1 / (n - 1) | ||
| inner = 2:(n - 1) | ||
| lap = ( | ||
| w[1:(n - 2), inner] .+ w[3:n, inner] .+ w[inner, 1:(n - 2)] .+ | ||
| w[inner, 3:n] .- 4 .* w[inner, inner] | ||
| ) ./ dx^2 | ||
| eqs = Equation[broadcast(-, D(w[inner, inner]), lap) ~ zeros(n - 2, n - 2)] | ||
| for i in 1:n | ||
| push!(eqs, w[i, 1] ~ 0.0) | ||
| push!(eqs, w[i, n] ~ 0.0) | ||
| end | ||
| for j in inner | ||
| push!(eqs, w[1, j] ~ 0.0) | ||
| push!(eqs, w[n, j] ~ 0.0) | ||
| end | ||
| @named sys2d = System(eqs, t, vec(collect(w)), []) | ||
| sys2d = complete(sys2d) | ||
|
|
||
| op = vcat( | ||
| [w[i, j] => 0.25 for i in 1:n, j in 1:n] |> vec, | ||
| [D(w[i, j]) => 0.0 for i in 1:n, j in 1:n] |> vec | ||
| ) | ||
| prob = DAEProblem(sys2d, op, (0.0, 0.01); build_initializeprob = false) | ||
| @test length(prob.u0) == n * n | ||
| out = zeros(n * n) | ||
| prob.f(out, zeros(n * n), prob.u0, prob.p, 0.0) | ||
| @test all(isfinite, out) | ||
| end | ||
|
|
||
| @testset "array equations written as `D(slice) ~ rhs`" begin | ||
| # The residual form above puts the derivative inside the expression. The equivalent | ||
| # `D(u[2:n-1]) ~ rhs` form has no scalar `toterm` name for its LHS, which the | ||
| # derivative-substitution machinery has to skip rather than trip over. | ||
| n = 11 | ||
| @independent_variables t | ||
| @variables u(t)[1:n] | ||
| D = Differential(t) | ||
| dx = 1 / (n - 1) | ||
| lap = (u[1:(n - 2)] .- 2 .* u[2:(n - 1)] .+ u[3:n]) ./ dx^2 | ||
| eqs = [D(u[2:(n - 1)]) ~ lap, u[1] ~ 0.0, u[n] ~ 0.0] | ||
| @named sys = System(eqs, t, collect(u), []) | ||
| sys = complete(sys) | ||
|
|
||
| xs = range(0.0, 1.0, length = n) | ||
| op = vcat([u[i] => sinpi(xs[i]) for i in 1:n], [D(u[i]) => 0.0 for i in 1:n]) | ||
| prob = DAEProblem(sys, op, (0.0, 0.1); build_initializeprob = false) | ||
| @test length(prob.u0) == n | ||
|
|
||
| # the residual matches the analytic derivative of the initial condition | ||
| out = zeros(n) | ||
| du = zeros(n) | ||
| du[2:(n - 1)] .= [-pi^2 * sinpi(x) for x in xs[2:(n - 1)]] | ||
| prob.f(out, du, prob.u0, prob.p, 0.0) | ||
| @test maximum(abs, out) < 1.0e-1 | ||
|
|
||
| sol = solve(prob, DFBDF(); initializealg = BrownFullBasicInit(), reltol = 1.0e-8, | ||
| abstol = 1.0e-8, saveat = [0.1]) | ||
| @test SciMLBase.successful_retcode(sol) | ||
| @test maximum(abs, sol.u[end] .- [exp(-pi^2 * 0.1) * sinpi(x) for x in xs]) < 1.0e-2 | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just typeassert that
shape(eq.lhs)::SU.ShapeVecTand returnprod(length, sh; init = 1)