Preserve array differential equations with mtkcompile(sys; scalarize_arrays = false) - #5100
ChrisRackauckas-Claude wants to merge 6 commits into
Conversation
…_arrays = false)` Structural simplification scalarizes every array equation before matching, so for a finite-difference discretization such as `D(u[2:(n - 1)]) ~ lap(u)` the compiled system, its generated code and the compile time all grow with the number of grid points. The array form is only preserved today by skipping `mtkcompile` altogether and building a `DAEProblem` from the completed system. Add the `scalarize_arrays` keyword to `mtkcompile`. With `scalarize_arrays = false` the system is compiled by the tearing-free compiler in ModelingToolkitBase (`__mtkcompile_no_tearing`, which ModelingToolkit's `__mtkcompile` dispatches to), which now keeps first-order array differential equations intact: the differentiated array or slice becomes a contiguous block of the unknowns, residual-form equations `D(x) .- f ~ 0` are rewritten to `D(x) ~ f`, and array algebraic equations are scalarized so that trivially defined elements (boundary conditions) become observed. The compiled system records the choice under `ScalarizeArraysCtx` (`arrays_scalarized`), which the initialization system inherits. Code generation then keeps the array form all the way through: - `expand_array_derivatives!` binds a derivative of a slice to a `view` of `du` when its scalar derivatives are consecutive (SciML#5097), instead of an `array_literal` of them. - Arrays whose elements are partly unknowns and partly observed are reconstructed from views of the argument buffers plus the observed elements, rather than through an observed equation listing every element. - `default_toterm` handles derivatives of slices (`D(u[2:4])` becomes `uˍt(t)[2:4]`). - `write_possibly_indexed_array!` broadcasts a scalar written to an array key (SciML#5093), so the default derivative guess no longer fails for array derivative variables. - `full_equations` scalarizes preserved equations so that jacobians, mass matrices and sparsity patterns keep one row per scalar equation. Also wire the existing `array_equation_dae.jl` test into the test suite and add tests that the compiled heat equation has one equation and generated `DAEProblem` code of the same size for n = 24, 48, 96, solves correctly with and without an initialization problem, and matches the scalarized compilation. Co-authored-by: Cursor <cursoragent@cursor.com>
…, SciML#5097) Co-authored-by: Cursor <cursoragent@cursor.com>
…rownFullBasicInit `BrownFullBasicInit()` fails on the oldest supported SciMLBase/DiffEqBase combination (the downgrade CI job) because of an unrelated parameter despecialization bug, and the tests do not depend on it: the discrete Laplacian of the initial condition is the consistent initial derivative, so pass it explicitly and solve with `NoInit()`. Also assert the residual vanishes for that `du0`. Co-authored-by: Cursor <cursoragent@cursor.com>
The QA suite requires every `@public` name to be rendered in the docs. Co-authored-by: Cursor <cursoragent@cursor.com>
… QA list Co-authored-by: Cursor <cursoragent@cursor.com>
…eserving path `__mtkcompile_no_tearing` and `count_equation_rows` are internal helpers reached from ModelingToolkit, like `__mtkcompile` before them. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Closing this as an intermediate step that is not the direction we want.
What is useful here has been split out:
|
| Returns `nothing` if the array cannot be reconstructed: some element is neither in a buffer | ||
| nor has a fallback, or the array is not indexed from one. | ||
| """ | ||
| function partial_array_reconstruction( |
There was a problem hiding this comment.
I don't think this should be necessary. array_variable_assignments in codegen_utils.jl should handle this. If that generates suboptimal code (such as a large array_literal) we should fix it and make it use e.g. ArrayMaker.
There was a problem hiding this comment.
Oh I see that's exactly what this is doing.
There was a problem hiding this comment.
This function should be checked using Cthulhu/@code_typed to make sure the closures don't cause excessive boxing from that one Julia bug. It would be nice to just use explicit let captures anyway.
| push!(element_exprs, name) | ||
| name | ||
| end | ||
| push!(writes, Expr(:(=), Expr(:ref, result, Tuple(carts[i])...), value)) |
There was a problem hiding this comment.
I guess the splat here is fine but it would be nice to just not have to build carts since it will never infer.
| push!(used_buffers, name) | ||
| src = Expr(:call, view, name, pos:(pos + len - 1)) | ||
| if N > 1 | ||
| src = Expr(:call, reshape, src, map(length, region)...) |
There was a problem hiding this comment.
| src = Expr(:call, reshape, src, map(length, region)...) | |
| src = Expr(:call, reshape, src) | |
| append!(src.args, Iterators.map(length, region)) |
| if N > 1 | ||
| src = Expr(:call, reshape, src, map(length, region)...) | ||
| end | ||
| dest = Expr(:call, view, result, region...) |
There was a problem hiding this comment.
| dest = Expr(:call, view, result, region...) | |
| dest = Expr(:call, view, result) | |
| append!(dest.args, region) |
| if !isempty(present) | ||
| lo = Tuple(carts[first(present)]) | ||
| hi = lo | ||
| for i in present |
There was a problem hiding this comment.
I have a gut feeling that this loop would really benefit from not using CartesianIndices, or at least turning lo and hi into Vector{Int}/Memory{Int} so that the broadcasts infer. We can even make it operate in-place.
| end | ||
| region = ntuple(d -> lo[d]:hi[d], N) | ||
| lin = LinearIndices(sz) | ||
| block = [lin[c] for c in CartesianIndices(region)] |
There was a problem hiding this comment.
block's ndims won't infer. Does this need to be an Array{T, N} or can it just be a Vector{T}? If the latter, it's better to do block = Vector{T}(undef, some_length) and write to it in a loop.
There was a problem hiding this comment.
CartesianIndices(region) may not be 1-indexed, but lin is. Won't this cause problems? I guess what this is trying to do is block = [prod(Tuple(c)) - prod(lo) for c in CartesianIndices(region)]?
| alloc = if isempty(used_buffers) | ||
| Expr(:call, Expr(:curly, Array, eltype_expr, N), :undef, sz...) | ||
| else | ||
| Expr(:call, similar, first(used_buffers), eltype_expr, sz...) | ||
| end |
There was a problem hiding this comment.
| alloc = if isempty(used_buffers) | |
| Expr(:call, Expr(:curly, Array, eltype_expr, N), :undef, sz...) | |
| else | |
| Expr(:call, similar, first(used_buffers), eltype_expr, sz...) | |
| end | |
| alloc = if isempty(used_buffers) | |
| Expr(:call, Expr(:curly, Array, eltype_expr, N), :undef) | |
| else | |
| Expr(:call, similar, first(used_buffers), eltype_expr) | |
| end | |
| append!(alloc.args, Iterators.map(length, sh)) |
No splat, no need for sz.
| push!(body.args, result) | ||
| push!(assignments, Assignment(arrvar, body)) | ||
| return assignments | ||
| end |
There was a problem hiding this comment.
This function would be much easier to understand with some more comments.
| """ | ||
| function partial_array_reconstruction( | ||
| arrvar::SymbolicT, idxs::Vector{Tuple{Int, Int}}, argument_name, buffer_offset::Int, | ||
| element_fallback |
There was a problem hiding this comment.
Might want to @nospecialize the argument_name and element_fallback
| slice); any other slice is read one element at a time. Slices with an element in no buffer | ||
| are skipped. | ||
| """ | ||
| function array_slice_assignments( |
There was a problem hiding this comment.
The same general comments from above apply here.
|
Superseded checkpoint pointers (this PR remains closed as the wrong intermediate):
|
Summary
mtkcompilescalarizes every array equation before structural simplification, so for a finite-difference discretization such asD(u[2:(n - 1)]) ~ lap(u)the compiled system, the generated code and the compile time all grow with the number of grid points. MethodOfLines currently gets O(1) scaling only by skippingmtkcompileentirely and building aDAEProblemfrom the completed system.This PR adds
mtkcompile(sys; scalarize_arrays = false), which compiles the system without tearing/index reduction and keeps first-order array differential equations intact, so thatDAEProblem(and, with #5101,ODEProblem) code generation is independent of the array length. It is the first of two ModelingToolkit PRs; the ODE code generation half is stacked on top of this one.What changes
mtkcompile(sys; scalarize_arrays = false)(ModelingToolkitBase). Dispatches to the tearing-free compiler (__mtkcompile_no_tearing, the former__mtkcompileof ModelingToolkitBase; ModelingToolkit's__mtkcompileoverride returns early to it). That compiler now:D(x) ~ rhswherexis an array of unknowns or a slice of one, ordering the unknowns so the elements of each differentiated slice form a contiguous block matching the rows of the equation;D(x) .- f ~ 0(what MethodOfLines emits) toD(x) ~ f;ScalarizeArraysCtxmetadata key (arrays_scalarized(sys)), which the initialization system inherits (mirrorsHomotopyCtx).ArgumentErrorwithscalarize_arrays = false.expand_array_derivatives!binds a derivative of a slice to aviewofduwhen its scalar derivatives are consecutive indu, instead of anarray_literalof them (fixes Preserve array derivatives in implicit DAE residual code generation #5097).views of the argument buffers plus the observed elements (partial_array_reconstruction), instead of through an observed equation that lists every element. Guarded by!arrays_scalarized(sys), so the default path is unchanged.default_totermhandles derivatives of slices:D(u[2:4])becomesuˍt(t)[2:4].full_equationsscalarizes preserved equations so jacobians/mass matrices/sparsity keep one row per scalar equation.write_possibly_indexed_array!broadcasts a scalar written to an array (or slice) key, so the scalar default derivative guess for an arrayuˍt(t)no longer producesvec(::Bool)(fixes DAEProblem from a system with array-form differential equations fails with MethodError: no method matching vec(::Bool) #5093 — the reproducer from the issue is now a test).get_initialization_problem_typecounts equation rows and copes with anothingtearing state (no SCC problem without tearing).test/array_equation_dae.jl(previously not inruntests.jl) is wired in, plus newtest/array_equations_preserved.jl.Example
How to verify the O(1) scaling
test/array_equations_preserved.jlchecks, forn = 24, 48, 96, that the compiled system has exactly one equation and that the generated in-placeDAEProblemfunction (generate_rhs(sys, GeneratedFunctionOptions(; expression = Val{true}); implicit_dae = true)) has the same number ofExprnodes and noarray_literal. The same file checks the solution against the scalarized compilation and the analytic solution of the heat equation, with and without an initialization problem, and a 2D slice.Limitations
D(x) ~ forD(x) .- f ~ 0), and algebraic equations that do not trivially define an unknown are kept as algebraic equations. Higher-order array equations are scalarized and order-reduced as before.ODEProblemfrom such a system still requires the follow-up BuildODEProblemfrom array differential equations withcompleteonly #5101; here it keeps throwing the existing "array equations" error.Tests run locally
Initialization,SymbolicIndexingInterface,InterfaceI,InterfaceII: pass, except the pre-existingJumpSystem Testerror (FieldError: type NamedTuple has no field abstolfromaffect_toleranceincallbacks.jlon anSSAIntegrator), which also fails onmasterwith JumpProcesses 9.32.2 and is untouched here.InterfaceI: pass (1507 passed, 3 broken, as on master).test/Discretization/problem_construction.jlagainst this branch (including the new ODE path from Keep array equations through the compiled ODEProblem path MethodOfLines.jl#688): pass.CI notes
__mtkcompileearly return,count_equation_rows) need the ModelingToolkitBase in this PR. They will resolve once ModelingToolkitBase 1.70.0 is registered.downgrade-mtkbase (InterfaceII)andsublibrary-ci InterfaceIIfail only on the pre-existingJumpSystem Testerror above (the same job fails on Sundials 6.7.1 + MCI 0.3 source for x86 InterfaceI #5098).sublibrary-ci QAfails only on JET: the QA environment now resolves JET 0.12.1, which reports ~245 "local variable may be undefined" findings in Moshi@matchexpansions across ModelingToolkitBase (files this PR does not touch); Recognize copied symbolic missing bindings #5073 fails the same way with 234 findings. The ModelingToolkitQAjob (JET 0.11.6) passes.InterfaceI (x86)fails to precompile Sundials/QuasiMonteCarlo on 32-bit, as onmaster(Sundials 6.7.1 + MCI 0.3 source for x86 InterfaceI #5098).buildfails on the pre-existingunbound_inputs@refininteractive_simulation.md, fixed by Qualify unbound_inputs @ref in the interactive simulation tutorial #5099; the new@docsblock here renders.Related: #5093, #5097, SciML/MethodOfLines.jl (array-form
DAEProblempath).Made with Cursor