Summary
For multibody vehicle models, the inline-linear-SCC pass (DefaultReassembleAlgorithm(inline_linear_sccs = true)) emits a runtime dense A \ b over a system that is an order of magnitude larger than the mathematically-required core, and solves it with a non-rank-tolerant dense LU. Measured on a half-car suspension model (22 states after mtkcompile):
- The big algebraic SCC enters
get_linear_scc_linsol with 396 rows.
__reduce_linear_system! removes only 101 (the all-constant-coefficient rows) → a 296×296 dense runtime LU, evaluated every RHS call and during initialization reconstruction.
- Of the 396 rows, 384 are matched (torn) and 378 of those have a constant pivot coefficient — i.e. the irreducible simultaneous core is only ~18–24 unknowns (consistent with the same model compiled with
inline_linear_sccs=false, which yields a 48-state DAE = 22 differential + 26 algebraic).
- The emitted 296×296 pattern is 1.6% dense and, after a Dulmage–Mendelsohn analysis, ~93% block-triangular given the tearing order; only the small core is truly coupled.
The inflation mechanism: rows whose non-pivot coefficients are symbolic (every product-rule-differentiated definitional equation — rotation-matrix entries, contact-frame vectors, etc.) fail the all(isconst, coeffs) gate in __reduce_linear_system! and are kept inside the numeric solve, even though tearing already matched them with constant pivots.
Why the obvious fix doesn't work (tested)
Relaxing the gate to "pivot coefficient constant" (which is sufficient for a division-free elimination, and the alias/substitution machinery in __reduce_linear_system!/get_new_mm handles symbolic alias coefficients as-is) shrinks the system but explodes symbolically:
- First failure:
iszero(::Num) inside get_new_mm's duplicate-entry pruning goes through fraction_iszero → expand, which is exponential for the deep product-of-sums coefficients → OutOfMemoryError. (Workaroundable with a cheap syntactic zero test — see suggestion below.)
- With that bypassed,
mtkcompile succeeds (~90 s) but ODEProblem construction stalls indefinitely: the substituted core coefficients are enormous expression trees and codegen blows up.
So full symbolic elimination of the torn rows is intractable; the inflation is an inherent artifact of numeric tearing. Sequential runtime evaluation of the eliminated rows is also impossible (they depend on the core unknowns — circular).
Suggested fixes
- Solve the emitted system sparse and rank-tolerant at runtime. The pattern is already available (the
ArrayMaker regions); a sparse LU/QR with fill-reducing ordering automatically exploits the ~93% triangular structure, so the 296-system costs about as much as the 18-core. Crucially it should be rank-revealing (or regularized): these acceleration/reaction systems are legitimately rank-deficient whenever constraint reactions are statically indeterminate (vehicle with ≥2 rigid suspension mounts on one body) — the current dense \ throws SingularException (LU) and the DyadCompilerPasses LDIV rewrite produces silent Inf (unpivoted QR). See JuliaComputing/DyadCompilerPasses.jl#40.
- Independent robustness improvement: the duplicate-entry pruning in
get_new_mm should use a cheap syntactic zero test (term construction already cancels syntactically identical terms; an entry that is mathematically zero but kept is harmless), e.g. an overridable prune_iszero(x) = iszero(x) specialized for Num to a syntactic check — exact iszero(::Num) can OOM via polynomial expansion.
- Optionally expose the SCC composition (total/matched/const-pivot counts) as a debug aid — these numbers immediately reveal the inflation.
Context
Found while debugging SingularException(13) during initialization of suspension vehicle models (Multibody-style components). ModelingToolkit v11.26.8 (#master), ModelingToolkitTearing v1.14.1, StateSelection v1.9.3. Related: SciML/ModelingToolkit.jl#4237 (init reconstruction LU of indeterminate reactions), SciML/ModelingToolkit.jl#4607.
Summary
For multibody vehicle models, the inline-linear-SCC pass (
DefaultReassembleAlgorithm(inline_linear_sccs = true)) emits a runtime denseA \ bover a system that is an order of magnitude larger than the mathematically-required core, and solves it with a non-rank-tolerant dense LU. Measured on a half-car suspension model (22 states aftermtkcompile):get_linear_scc_linsolwith 396 rows.__reduce_linear_system!removes only 101 (the all-constant-coefficient rows) → a 296×296 dense runtime LU, evaluated every RHS call and during initialization reconstruction.inline_linear_sccs=false, which yields a 48-state DAE = 22 differential + 26 algebraic).The inflation mechanism: rows whose non-pivot coefficients are symbolic (every product-rule-differentiated definitional equation — rotation-matrix entries, contact-frame vectors, etc.) fail the
all(isconst, coeffs)gate in__reduce_linear_system!and are kept inside the numeric solve, even though tearing already matched them with constant pivots.Why the obvious fix doesn't work (tested)
Relaxing the gate to "pivot coefficient constant" (which is sufficient for a division-free elimination, and the alias/substitution machinery in
__reduce_linear_system!/get_new_mmhandles symbolic alias coefficients as-is) shrinks the system but explodes symbolically:iszero(::Num)insideget_new_mm's duplicate-entry pruning goes throughfraction_iszero → expand, which is exponential for the deep product-of-sums coefficients →OutOfMemoryError. (Workaroundable with a cheap syntactic zero test — see suggestion below.)mtkcompilesucceeds (~90 s) butODEProblemconstruction stalls indefinitely: the substituted core coefficients are enormous expression trees and codegen blows up.So full symbolic elimination of the torn rows is intractable; the inflation is an inherent artifact of numeric tearing. Sequential runtime evaluation of the eliminated rows is also impossible (they depend on the core unknowns — circular).
Suggested fixes
ArrayMakerregions); a sparse LU/QR with fill-reducing ordering automatically exploits the ~93% triangular structure, so the 296-system costs about as much as the 18-core. Crucially it should be rank-revealing (or regularized): these acceleration/reaction systems are legitimately rank-deficient whenever constraint reactions are statically indeterminate (vehicle with ≥2 rigid suspension mounts on one body) — the current dense\throwsSingularException(LU) and the DyadCompilerPasses LDIV rewrite produces silentInf(unpivoted QR). See JuliaComputing/DyadCompilerPasses.jl#40.get_new_mmshould use a cheap syntactic zero test (term construction already cancels syntactically identical terms; an entry that is mathematically zero but kept is harmless), e.g. an overridableprune_iszero(x) = iszero(x)specialized forNumto a syntactic check — exactiszero(::Num)can OOM via polynomial expansion.Context
Found while debugging
SingularException(13)during initialization of suspension vehicle models (Multibody-style components). ModelingToolkit v11.26.8 (#master), ModelingToolkitTearing v1.14.1, StateSelection v1.9.3. Related: SciML/ModelingToolkit.jl#4237 (init reconstruction LU of indeterminate reactions), SciML/ModelingToolkit.jl#4607.