Build ODEProblem from array differential equations with complete only - #5101
Closed
ChrisRackauckas-Claude wants to merge 5 commits into
Closed
ChrisRackauckas-Claude wants to merge 5 commits into
ChrisRackauckas-Claude wants to merge 5 commits into
Conversation
This was referenced Sep 9, 2026
…ile` `complete(sys)` followed by `ODEProblem` rejected array equations such as `D(u[2:n-1]) ~ lap(u)`, forcing the finite-difference use case through the scalarizing `mtkcompile`, whose output and compile time grow with the grid. `DAEProblem` already accepts them, so give the explicit ODE path the same treatment: the right-hand side of an array equation is written to a contiguous block of `du` through `array_residual_maker`, the mass matrix gets one row per element, and the residual form `D(x) .- f ~ 0` emitted by MethodOfLines is rewritten to `D(x) ~ f` first. The generated code is independent of the array length. `full_equations` expands array equations into scalar rows so that the symbolic jacobian, sparsity and initialization paths keep one equation per row, and `ODEFunction` only computes the W sparsity pattern when `sparse` or `sparsity` asks for it. Co-authored-by: Cursor <cursoragent@cursor.com>
ChrisRackauckas-Claude
force-pushed
the
preserve-array-equations-ode
branch
from
September 9, 2026 20:24
29da7c6 to
69eab0f
Compare
ODEProblem code from preserved array differential equationsODEProblem from array differential equations with complete only
AayushSabharwal
requested changes
Sep 9, 2026
Co-authored-by: Aayush Sabharwal <aayush.sabharwal@gmail.com>
SciML#2597 test Co-authored-by: Cursor <cursoragent@cursor.com>
…lements Co-authored-by: Cursor <cursoragent@cursor.com>
…rownFullBasicInit The downgrade CI environment (DiffEqBase 7.15) trips a despecialization wrapper assertion inside BrownFullBasicInit for DAEFunctions, which aborted the InterfaceII group before the new ODEProblem tests could run. The tests only need a solve to compare against the analytic solution, so hand the solver a consistent du0 and skip DAE initialization. Co-authored-by: Cursor <cursoragent@cursor.com>
Member
Author
|
Checkpoint before usage cutoff
|
Member
Author
|
CI status after 232d7a4 (the array-equation DAE tests now solve from a consistent
|
ChrisRackauckas
added a commit
that referenced
this pull request
Sep 14, 2026
…ODE support and MultiObjectiveOptimizationFunction (#5148) * feat: accept array equations on the NonlinearProblem path * fix: keep array-equation downgrade CI on DiffEqBase 7.18.1 Import unwrap explicitly in the new NL tests and raise the DiffEqBase floor so nested AutoDespecialize accepts already-unwrapped parameters. * ci: retrigger checks * Apply suggestion from @ChrisRackauckas * Zero the derivatives of array unknowns before expanding slices * Test that array unknowns carry an array equation * Gate array equations on a per-constructor capability trait Stacked on #5049: array *equations* are accepted by the implicit-DAE and nonlinear paths, but the opt-in is a hardcoded `!implicit_dae && !(constructor <: NonlinearFunction)` test in `__process_SciMLProblem` — a codegen flag and a constructor-type check doing capability duty. This treats accepting array equations as a named, per-constructor capability. - `accepts_array_equations(constructor)` gates `__process_SciMLProblem`. `DAEFunction` and `NonlinearFunction` opt in, matching #5049's behavior exactly (`HomotopyProblem` inherits nonlinear support through the same constructor). Every other problem type still needs `mtkcompile`, and opting a new constructor in is a one-line method. - `has_array_equations(eqs)` gets a docstring; it is the shared, shape-based detection predicate behind `check_array_equations` and the Jacobian guards. - `DAEFunction` throws the same `ArgumentError` for `jac = true` / `sparse = true` on array residuals that `NonlinearFunction` gets: today `jac = true` dies inside `executediff` and `sparse = true` silently builds a wrong-shaped `jac_prototype` (one row per equation instead of one per residual row). Related: #4983, #5049, #5131, #5136. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.10.21 Agent-Model: SWE-2 Max Agent-Session: local Devin CLI session, no shareable URL (workspace /home/crackauc/sandbox/tmp_20260913_101544_55869) * Build `ODEProblem` from array differential equations without `mtkcompile` `complete(sys)` followed by `ODEProblem` rejected array equations such as `D(u[2:n-1]) ~ lap(u)`, forcing the finite-difference use case through the scalarizing `mtkcompile`, whose output and compile time grow with the grid. `DAEProblem` already accepts them, so give the explicit ODE path the same treatment: the right-hand side of an array equation is written to a contiguous block of `du` through `array_residual_maker`, the mass matrix gets one row per element, and the residual form `D(x) .- f ~ 0` emitted by MethodOfLines is rewritten to `D(x) ~ f` first. The generated code is independent of the array length. `full_equations` expands array equations into scalar rows so that the symbolic jacobian, sparsity and initialization paths keep one equation per row, and `ODEFunction` only computes the W sparsity pattern when `sparse` or `sparsity` asks for it. Co-authored-by: Cursor <cursoragent@cursor.com> * Update lib/ModelingToolkitBase/src/utils.jl Co-authored-by: Aayush Sabharwal <aayush.sabharwal@gmail.com> * Expect ODEProblem to accept array equations over scalar unknowns in the #2597 test Co-authored-by: Cursor <cursoragent@cursor.com> * Assert the operator type when re-applying a slice derivative to its elements Co-authored-by: Cursor <cursoragent@cursor.com> * Solve the array-equation DAE tests from a consistent du0 instead of BrownFullBasicInit The downgrade CI environment (DiffEqBase 7.15) trips a despecialization wrapper assertion inside BrownFullBasicInit for DAEFunctions, which aborted the InterfaceII group before the new ODEProblem tests could run. The tests only need a solve to compare against the analytic solution, so hand the solver a consistent du0 and skip DAE initialization. Co-authored-by: Cursor <cursoragent@cursor.com> * Share the array-equation predicate and accept them on the ODE path `is_array_equation` (from the ODE array-equation support) and `has_array_equations` were two spellings of the same shape check; keep the per-equation predicate in `utils.jl` and make `has_array_equations` delegate to it. `ODEFunction` opts into `accepts_array_equations`, matching what its `generate_rhs` branch already assembles through `array_residual_maker`. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.10.21 Agent-Model: SWE-2 Max Agent-Session: local Devin CLI session, no shareable URL (workspace /home/crackauc/sandbox/tmp_20260913_101544_55869) * Expect array unknowns to flatten in the ODE array-equation tests Post-#5131 `flat_unknowns` handles the array unknown unconditionally, so an array equation over `u(t)[1:4]` declared as `[u]` constructs and evaluates rather than throwing "array unknowns". Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.10.21 Agent-Model: SWE-2 Max Agent-Session: local Devin CLI session, no shareable URL (workspace /home/crackauc/sandbox/tmp_20260913_101544_55869) * Update rejection tests for the ODE array-equation capability `ODEProblem` accepts array equations now, so it no longer belongs in the "still require scalarized equations" testset (`SDEProblem` rejection remains covered in the same file), and a `D(u) ~ -u` system over the array unknown `u` constructs because `flat_unknowns` handles it. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.10.21 Agent-Model: SWE-2 Max Agent-Session: local Devin CLI session, no shareable URL (workspace /home/crackauc/sandbox/tmp_20260913_101544_55869) * Reject residual-form array differential equations on the ODE path The scalar path only accepts `D(x) ~ f` from `complete`; `D(x) - f ~ 0` is a DAE and needs `mtkcompile` or `DAEProblem`. Treat `D(u[2:4]) .- f ~ 0` the same way instead of pattern-matching it back into explicit form in every consumer. Also drop the duplicate InterfaceII include of `array_equation_dae.jl` (InterfaceI already runs it) and scope the docs note about `jac`/`sparse` to array equations. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-fable-5-1 Agent-Session: https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga Claude-Session: https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga * Construct MultiObjectiveOptimizationFunction from a system's cost vector `OptimizationProblem` accepts `f::MultiObjectiveOptimizationFunction` in SciMLBase, but MTK could never produce one: `generate_cost` always folds `get_costs(sys)` through `consolidate` into a scalar. - `costs(sys)` returns the unconsolidated objective vector: the system's own costs plus the consolidated cost of each subsystem, namespaced. - `MultiObjectiveOptimizationFunction(sys)` generates a vector-valued objective over `costs(sys)`. `jac` builds the objective jacobian; `hess` is a vector with one generated hessian function per objective, the shape `OptimizationBase.instantiate_function` iterates. - The constraint fields are assembled by `generate_constraint_fields`, shared with `OptimizationFunction` instead of duplicated. - `OptimizationProblem(sys, op; multiobjective = true)` routes through it. `weights` cannot combine with it. Also drop the `jac`/`sparse` guards on `NonlinearFunction` and `DAEFunction` for array equations: since `full_equations` scalarizes array equations, the symbolic jacobian and sparsity pattern already come out with one row per residual, so both now work and are tested against ForwardDiff and the analytic stencil. The array-residual DAE tests go back to `BrownFullBasicInit`, which runs on the DiffEqBase 7.18.1 floor. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-fable-5-1 Agent-Session: https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga Claude-Session: https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga * Approve the multi-objective names as ModelingToolkit reexports `costs` and the `generate_multiobjective_*` / `calculate_multiobjective_*` functions are exported from ModelingToolkitBase and therefore reexported by the ModelingToolkit facade; list them in the root QA allow-list. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-fable-5-1 Agent-Session: https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga Claude-Session: https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga --------- Co-authored-by: utkuyilmaz1903 <utkyilmz@gmail.com> Co-authored-by: Christopher Rackauckas <accounts@chrisrackauckas.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Aayush Sabharwal <aayush.sabharwal@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
ChrisRackauckas
added a commit
that referenced
this pull request
Sep 14, 2026
- Document the PDE solution interface shared by discretizers (#5149) - Point the docs `ModelingToolkit` source at the repository root (#5152) - Unify array-equation support behind a capability trait; fold in #5101 ODE support and MultiObjectiveOptimizationFunction (#5148) - Raise SymbolicIndexingInterface compat lower bound to 0.3.47 (#5153) - Compare `complete` and `mtkcompile` array-unknown problems through symbolic indices (#5154) Agent-Harness: Claude Code Agent-Model: claude-opus-5[1m] Claude-Session: https://claude.ai/code/session_014FEzNTLFutCmTEAZ3zBg5R Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
Superseded: the ODEProblem array-DE support here was folded into #5148 ("Unify array-equation support behind a capability trait; fold in #5101 ODE support"), merged upstream and released in ModelingToolkitBase 1.75 / MTK 11.45. Verified on the released version: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rewritten (no longer stacked on #5100, which is closed): this PR makes
ODEProblem(complete(sys), ...)work for systems whose differential equations are written over array slices, the same wayDAEProblemalready does, withoutmtkcompileand without any new keyword.For a finite-difference discretization such as
D(u[2:(n - 1)]) ~ lap(u)(or the residual formD(u[2:(n - 1)]) .- lap(u) ~ 0that MethodOfLines emits),ODEProblemused to throw "The system has array equations. Callmtkcompile", andmtkcompilescalarizes, so the generated code and compile time grow withn. Now the array equation stays one equation and the generated function is independent ofn.Making
mtkcompile(tearing / index reduction) keep array equations intact is separate work; this PR stops atcomplete→ODEProblem.What changes (all in ModelingToolkitBase)
generate_rhs(explicit ODE branch): array equations are rewritten toD(x) ~ f(explicit_array_derivative_form, which handlesD(x) .- f ~ 0andD(x) .+ g ~ 0), then the right-hand sides are packed intoduwitharray_residual_maker— the same contiguous-region assembly the implicit-DAE path uses.check_lhsacceptsD(x)wherexis an array or slice whose elements are all unknowns;check_operator_variableschecks uniqueness of the derivatives element-wise, soD(u[1:3])together withD(u[3])is still rejected.calculate_massmatrixgives an array equation one row per element (identity rows for differential equations, zero rows forzeros(n) ~ g), laid out like the generated code.__process_SciMLProblemskips the array-equation check forODEFunctionconstructors (array unknowns are still rejected: passcollect(u)as unknowns, as forDAEProblem).full_equationsexpands array equations into scalar rows (after the residual → explicit rewrite), so the symbolic jacobian, sparsity pattern,tgradand the initialization system see one equation per row.jac = true, sparse = trueand the default initialization problem work from such systems.ODEFunctiononly computesW_sparsitywhensparseorsparsityis set; it was computed unconditionally and is an O(n) symbolic computation that nothing consumed otherwise.build_explicit_observed_functionrecords the element derivatives of a slice equation instead of callingdefault_totermonD(u[2:4])(which has nototermname and threw).Example
D(u) ~ -uover a whole array givesmass_matrix === Iand solves withTsit5.Tests
lib/ModelingToolkitBase/test/array_equation_ode.jl(new;array_equation_dae.jlis also now wired intoruntests.jl):D(u) ~ -ufromcomplete:mass_matrix === I, in-place and out-of-place functions, solution withTsit5, with and without the default initialization problem;duequals the discrete Laplacian, solution matches the analytic solution, themtkcompilepath and a hand-scalarizedcompletepath to1e-10, default initialization works;Exprs have the same node count forn = 24, 48, 96and contain noarray_literal;D(w[2:n-1, 2:n-1]) ~ lapmatches the scalarized system row for row;jac = true, sparse = truegives the tridiagonal-plus-boundary pattern and solves;full_equationsreturnsnscalar rows for both forms;Checked separately against the real (tearing)
mtkcompilefrom ModelingToolkit forn = 21, 41:complete→ODEProblem→Rodas5Pagrees withmtkcompile→ODEProblem→Tsit5to2e-11, with and without the initialization problem.Notes
completestill have to be written0 ~ g(as before for scalar systems);u[1] ~ 0keeps raising "The LHS cannot contain nondifferentiated variables". MethodOfLines emits its boundary conditions in the latter form, so itsODEProblempath needs either that rewrite on the MethodOfLines side or a follow-up here.n;build_initializeprob = falsegives the O(1) construction.Made with Cursor