Skip to content

Unify array-equation support behind a capability trait; fold in #5101 ODE support and MultiObjectiveOptimizationFunction - #5148

Merged
ChrisRackauckas merged 18 commits into
SciML:masterfrom
ChrisRackauckas-Claude:unify-array-equation-capability
Sep 14, 2026
Merged

ChrisRackauckas merged 18 commits into
SciML:masterfrom
ChrisRackauckas-Claude:unify-array-equation-capability

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Sep 13, 2026 •

Copy link
Copy Markdown
Member

Ignore until reviewed by @ChrisRackauckas.

Summary

Stacked on the merged #5131 (array unknowns via flat_unknowns); folds in the closed #5049 (array equations on the nonlinear path) and the open #5101 (array differential equations on the ODE path), and, as the last commit, the multi-objective optimization work from ChrisRackauckas-Claude:multiobjective-optimization. If this lands, #5101 and that branch are subsumed.

"Accepts array equations" is a named, per-constructor capability: accepts_array_equations(constructor) gates __process_SciMLProblem, and ODEFunction, DAEFunction and NonlinearFunction opt in. All three lower an array equation to one scalar row per element through the shared array_residual_maker; full_equations scalarizes array equations, so everything downstream of it (symbolic Jacobians, sparsity, tgrad, the initialization system) sees one row per residual without further changes.

Review of the earlier revision and what changed

The earlier revision of this PR (through d834c1a) was reviewed in full. Everything in it was correct as far as tests and probes could tell; the following was simplified or extended on top:

  • The residual-form rewrite is gone. explicit_array_derivative_form pattern-matched D(u[2:4]) .- f ~ 0 back into D(u[2:4]) ~ f in generate_rhs, calculate_massmatrix, full_equations and build_explicit_observed_function. The scalar path accepts no such thing: D(x) - f ~ 0 is a DAE and must go through mtkcompile or DAEProblem. Array equations now behave the same way (the ODE path rejects them with the same "nondifferentiated variables" error, and DAEProblem keeps accepting them, as array_equation_dae.jl tests). That removes ~50 lines and five call sites.
  • jac = true / sparse = true work for array residuals on NonlinearFunction and DAEFunction instead of throwing. Since full_equations scalarizes, calculate_jacobian and jacobian_sparsity already produce one row per residual; the guards only hid that. Tested against a ForwardDiff Jacobian (nonlinear, dense and sparse, including a solve) and against the analytic stencil with the γ term (DAE, dense). Consequence: WIP: build implicit DAE Jacobians and sparsity from scalarized array residuals #5136 is not needed for the Jacobian itself. sparse = true on a DAE in residual form still errors inside calculate_massmatrix ("Only semi-explicit constant mass matrices"); that is pre-existing and equally true for scalar residual-form DAEs, so it is out of scope here.
  • array_equation_dae.jl was included twice (InterfaceI on master, and again in InterfaceII by this PR). The duplicate is removed.
  • The array-residual DAE tests solve with BrownFullBasicInit again rather than a hand-computed consistent du0 under NoInit(), so solver-driven DAE initialization is exercised through the array residual. Build ODEProblem from array differential equations with complete only #5101 had switched to NoInit because BrownFullBasicInit tripped a ParameterDespecializationWrapper assertion on the downgrade job's DiffEqBase 7.15; the DiffEqBase = "7.18.1" floor this PR already carries is what fixes that, verified by running the test against a pinned DiffEqBase 7.18.1 (see below).
  • Docs: the "use mtkcompile before jac/sparse" notes are removed since they no longer apply; the nonlinear tutorial note describes what array equations look like instead.

Multi-objective optimization (last commit)

OptimizationProblem accepts f::MultiObjectiveOptimizationFunction in SciMLBase but MTK never produced one (generate_cost always consolidates to a scalar).

  • costs(sys): the unconsolidated objective vector (own costs, then the consolidated cost of each subsystem, namespaced). Exported and documented next to cost.
  • MultiObjectiveOptimizationFunction(sys): vector-valued objective over costs(sys); jac builds the objective Jacobian; hess is a vector of generated functions, one per objective, which is the shape OptimizationBase.instantiate_function iterates ([... for h in f.hess]). The original branch generated one function returning a vector of matrices, which that consumer cannot use.
  • The six constraint fields (cons, cons_j, cons_h, their prototypes, cons_expr) are assembled by one generate_constraint_fields shared with OptimizationFunction instead of a duplicated 25-line block.
  • OptimizationProblem(sys, op; multiobjective = true) routes through it; weights is rejected in combination with it.
  • All new public names (costs, the generate_multiobjective_* / calculate_multiobjective_* functions, the SciMLBase constructor) have docstrings and @docs entries.

Not in scope

generate_rhs residual codegen is unchanged from #5049; OOP Dual promotion for DAEProblem{false}; array constraints in generate_cons; sparse Jacobians for residual-form DAEs (see above).

Verification

Julia 1.12.7, ModelingToolkitBase test environment on this machine.

All commands run from lib/ModelingToolkitBase unless noted; GROUP=… julia --project -e 'using Pkg; Pkg.test()' for the groups.

What Result
GROUP=InterfaceI InterfaceI | 1732 pass | 5 broken | 1737 | 43m52s — Testing ModelingToolkitBase tests passed. The 5 broken are pre-existing @test_brokens; this PR adds none.
GROUP=InterfaceII every top-level testset green, tests passed: Array Equation ODEProblem Test | 51 | 51, Array-equation Nonlinear | 172 | 172, NonlinearSystem Test | 113 | 113, Code Generation Test | 65 | 65, JumpSystem Test | 4207 | 4207, … (2 pre-existing broken in Optimal Control + Constraints).
test/array_equation_dae.jl (InterfaceI member, rerun alone after the last test edit) array_equation_dae.jl | 32 | 32 | 1m02s
test/optimization/multiobjective.jl (new, Optimization group) optimization/multiobjective.jl | 35 | 35 | 8m38s
test/optimization/optimizationsystem.jl (shares generate_constraint_fields) 126 pass | 1 broken (pre-existing) | 127 | 3m22s
GROUP=QA JET level tests 54 | 54; Aqua: 20 of 21 pass. The one failure is JET.report_package(...; mode = :typo) with 234 reports, all local variable ##And#…/##Call#… may be undefined from Moshi.@match expansions, none in code this PR touches. That check has never been green on master since it was enabled (#4832); a separate agent reproduced 234 on clean master with the same JET/Moshi/SymbolicUtils versions, reduced it to a Moshi+JET-only MWE, and confirmed 0 reports with the upstream fix Roger-luo/Moshi.jl#93. Tracked in #5063; no new issue opened.
BrownFullBasicInit on the array-residual DAE at the compat floor scratch env with DiffEqBase pinned to 7.18.1 (and separately 7.21.1): retcode Success, max error 3.0e-3 against the analytic heat solution, same as the test asserts.
Root package GROUP=QA (repo root) QA | 40 | 40 | 4m35s, Testing ModelingToolkit tests passed after adding the six new reexported names to test/qa/qa.jl's allow-list (the first CI run failed only that check).
Runic (--check --diff) and typos over every changed file clean.
Docs (julia --project=docs docs/make.jl, repo root) builds through Doctest, ExpandTemplates (all @example/@docs blocks), CrossReferences, CheckDocument (checkdocs = :exports) and RenderDocument; only pre-existing size_threshold_warn warnings. The external linkcheck step was disabled for the local rerun after GitHub answered 429 to the NEWS.md link on the first run.

Not verified: Julia LTS / pre; the downgrade CI job itself (only the 7.18.1 pin above); Optimization group's dynamic_optimization.jl / test_infiniteopt.jl (the Pyomo/CasADi pixi install fails on this machine's network, unrelated); downstream packages; GPU. sparse = true on residual-form DAEs is a pre-existing calculate_massmatrix limitation and is intentionally not covered.

Judgment calls to push back on: (1) dropping the residual-form ODE rewrite from #5101 (array D(u) .- f ~ 0 now needs DAEProblem/mtkcompile, exactly like scalar); (2) hess on the multi-objective function is a vector of per-objective functions rather than one function returning a vector of matrices, chosen to match OptimizationBase; (3) jac/sparse guards removed instead of extended.

CI on ee92d65 (first push) and e4f7d29 (second push)

Everything is green except the following, all of which reproduce on master's own CI and are not touched by this PR:

Links

🤖 Generated with Claude Code (model: claude-fable-5-1); the earlier revision of this PR was generated with Devin CLI (model: SWE-2 Max).

https://claude.ai/code/session_01VorhgJYhDasyVLpGNrviga

utkuyilmaz1903 and others added 7 commits September 13, 2026 23:37
Import unwrap explicitly in the new NL tests and raise the DiffEqBase floor so nested AutoDespecialize accepts already-unwrapped parameters.
Stacked on SciML#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 SciML#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: SciML#4983, SciML#5049, SciML#5131, SciML#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)
@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the unify-array-equation-capability branch from 7fa965b to ca570b3 Compare September 13, 2026 21:20
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Gate array equations on a per-constructor capability trait Gate array equations on a per-constructor capability trait (stacked on #5049) Sep 13, 2026
ChrisRackauckas and others added 8 commits September 13, 2026 17:29
…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>
Co-authored-by: Aayush Sabharwal <aayush.sabharwal@gmail.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>
`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)
Post-SciML#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)
`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)
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Gate array equations on a per-constructor capability trait (stacked on #5049) Unify array-equation support behind a capability trait; fold in #5101 ODE support and MultiObjectiveOptimizationFunction Sep 13, 2026
@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the unify-array-equation-capability branch from 9403b2a to d834c1a Compare September 13, 2026 22:02
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Unify array-equation support behind a capability trait; fold in #5101 ODE support and MultiObjectiveOptimizationFunction Unify array-equation support behind a capability trait; fold in #5101 ODE support Sep 13, 2026
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
`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
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Unify array-equation support behind a capability trait; fold in #5101 ODE support Unify array-equation support behind a capability trait; fold in #5101 ODE support and MultiObjectiveOptimizationFunction Sep 14, 2026
`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
@ChrisRackauckas

Copy link
Copy Markdown
Member

I finally like it, and there's a lot riding on it downstream so I'm going for it, but @AayushSabharwal please take a look and if any clean ups are required we can modify post.

@ChrisRackauckas
ChrisRackauckas merged commit 0ed8046 into SciML:master Sep 14, 2026
97 of 110 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants