Repository navigation
Add SuiteSparse, and build SUNDIALS' IDA and KLU - #111
Merged
Merged
Conversation
OMSimulator's strongly coupled system gains a DAE mode (fmi-ls-dae): where a component exposes the residuals of F(t, x, x', z) = 0 instead of an explicit ODE, the master integrates them with IDA rather than differentiating them into something CVODE can take. That wants the IDA library built and an alias beside the CVODE and KINSOL ones. Refs OpenModelica/OMSimulator#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to adrpo/OMSimulator
that referenced
this pull request
Sep 8, 2026
The other half: a strongly coupled system that holds a component in DAE mode is integrated as one implicit DAE instead of an explicit ODE. The unknowns are laid out per component, [x_i | z_i], and so are the rows: a component in DAE mode contributes the residuals it declares, an ODE component the explicit rows x'_i - f_i(x, u, t). Both blocks are square, so the system is, and the two mix — a DAE component next to an ordinary FMU needs nothing special. IDASetId says which unknowns are differential, and IDACalcIC solves for the algebraic ones and for the derivatives, at the start and again after every event: the components initialize consistently with themselves, not with the coupled system, and an event moves them the same way. The coupling stays where it was for CVODE: updateInputs() propagates connected outputs into inputs before the rows are evaluated, so the residuals see a consistent coupling instead of the value left over from the last accepted step, and a loop spanning components is still resolved by AlgLoop. Lifting the connection equations into residual rows of their own — which is what would let one global implicit system replace that inner solve — is the next step, not this one. * SystemSC: ida_res / ida_roots, the [x | z] layout, setDaePoint/getDaePoint, calcConsistentInitialConditions, doStepIDA, and the IDA setup and teardown beside the CVODE ones. Dense Jacobian for now; the manifest's residual dependencies are a sparsity pattern for KLU, which matters as soon as the system is more than small. * oms_solver_sc_ida: DAE mode is not really a choice — a component that declares a DAE formulation has no explicit ODE for CVODE to integrate — so the system switches to IDA on its own and says so. The solver still has a name, for the SSD and for --solver. * ComponentFMU3ME: DAE mode is switched on at instantiation, which is where Configuration Mode is. The checks that need the state count moved to validateDaeMode(), called from initialize(): an FMU only reports how many continuous states it has once it has left Initialization Mode. * instantiated_model.py: SolverType duplicates the C enum's *values*, so inserting a solver renumbered oms_ma/oms_mav/oms_mav2 and setSolver() started choosing the wrong one — 37 testsuite failures with no hint of why. Values fixed and the duplication called out in a comment; deriving them from the C API would be better. Requires OpenModelica/OMSimulator-3rdParty#111 (SUNDIALS_ENABLE_IDA was OFF). Tested with a --daeMode Model Exchange FMU exported by OpenModelica (OpenModelica/OpenModelica#16622): it simulates through IDA, and the trajectory agrees with the same model exported as an ordinary ODE FMU and run through CVODE by this same master to ~1e-8, state event included. ctest: 117/117, the same as master. Refs OpenModelica#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
OMSimulator's strongly coupled system gains a DAE mode (fmi-ls-dae) whose Jacobian has a structure — the components' manifests say which unknowns each residual reaches — so it is held sparse and wants a sparse factorization. SUNDIALS_ENABLE_KLU was off for want of a SuiteSparse to build it against. SuiteSparse comes in as a submodule pinned to v7.12.2, the same release OpenModelica carries, and the build is delegated to its own CMakeLists exactly as OMCompiler/3rdParty does it: only suitesparse_config, amd, colamd, btf and klu are enabled, static, no OpenMP, CUDA, Fortran or demos. KLU_USE_CHOLMOD is off, so CHOLMOD and its dependencies are not needed. SUNDIALS' FindKLU prefers an existing SuiteSparse::KLU target as long as it is not pointed at a KLU elsewhere, so it picks up the in-tree one on its own; the compatibility checks have to be off because they try_compile() against a target that is in-tree rather than imported. SUNDIALS_INDEX_SIZE was already 64, which is what SuiteSparse' int64 KLU entry points want. Refs OpenModelica/OMSimulator#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to adrpo/OMSimulator
that referenced
this pull request
Sep 8, 2026
The structure the manifests give is what a sparse solver wants, so the Jacobian is held as a CSC matrix and factorized by KLU instead of being a dense one. The pattern never changes, so KLU reuses its symbolic factorization across the run, and the matrix carries its structure in the same arrays as its values — which is why SUNMatZero is not called on it: it would wipe the pattern, and every entry is written each time anyway, structure and value together. Without a usable structure nothing changes: the matrix is dense and IDA differences it itself. Needs OpenModelica/OMSimulator-3rdParty#111, which brings in SuiteSparse and turns SUNDIALS_ENABLE_KLU on. Same answers as the dense factorization, to the last digit or two of the trajectory, and the same step counts: x(0.5) x(1.0) loop equation dense 0.817368712680 0.701445480172 2.7e-14 KLU 0.817368712680 0.701445480171 2.6e-14 omc 0.817368180018 0.701445143664 - ctest: 117/117. Refs OpenModelica#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
Configuring failed on the Linux and Windows CI images, which have no BLAS:
Could NOT find BLAS (missing: BLAS_LIBRARIES)
SuiteSparse/SuiteSparse_config/cmake_modules/SuiteSparseBLAS.cmake:276
SuiteSparse_config looks for one unless told that nothing needs it, and nothing
here does: BLAS is used by CHOLMOD, UMFPACK and SPQR, none of which are enabled,
and KLU only reaches them through KLU_USE_CHOLMOD, which is off. Neither AMD,
COLAMD, BTF nor KLU names BLAS in its build or its sources. OpenModelica does not
hit this because it requires BLAS/LAPACK anyway; OMSimulator does not.
SUITESPARSE_REQUIRE_BLAS OFF skips the search, but the path it takes instead
still labels the BLAS it did not look for and its threading checks dereference
that label unguarded, so an empty BLA_VENDOR is a configure error one file
later. Any non-empty value does — the checks are all guarded on a specific
vendor, nothing is searched for and nothing is linked — so it is set to
"Generic".
Checked with a clean configure and build without a BLAS on the search path: KLU
builds and links, and OMSimulator's testsuite passes 117/117.
Refs OpenModelica/OMSimulator#1645
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to adrpo/OMSimulator
that referenced
this pull request
Oct 31, 2026
The other half: a strongly coupled system that holds a component in DAE mode is integrated as one implicit DAE instead of an explicit ODE. The unknowns are laid out per component, [x_i | z_i], and so are the rows: a component in DAE mode contributes the residuals it declares, an ODE component the explicit rows x'_i - f_i(x, u, t). Both blocks are square, so the system is, and the two mix — a DAE component next to an ordinary FMU needs nothing special. IDASetId says which unknowns are differential, and IDACalcIC solves for the algebraic ones and for the derivatives, at the start and again after every event: the components initialize consistently with themselves, not with the coupled system, and an event moves them the same way. The coupling stays where it was for CVODE: updateInputs() propagates connected outputs into inputs before the rows are evaluated, so the residuals see a consistent coupling instead of the value left over from the last accepted step, and a loop spanning components is still resolved by AlgLoop. Lifting the connection equations into residual rows of their own — which is what would let one global implicit system replace that inner solve — is the next step, not this one. * SystemSC: ida_res / ida_roots, the [x | z] layout, setDaePoint/getDaePoint, calcConsistentInitialConditions, doStepIDA, and the IDA setup and teardown beside the CVODE ones. Dense Jacobian for now; the manifest's residual dependencies are a sparsity pattern for KLU, which matters as soon as the system is more than small. * oms_solver_sc_ida: DAE mode is not really a choice — a component that declares a DAE formulation has no explicit ODE for CVODE to integrate — so the system switches to IDA on its own and says so. The solver still has a name, for the SSD and for --solver. * ComponentFMU3ME: DAE mode is switched on at instantiation, which is where Configuration Mode is. The checks that need the state count moved to validateDaeMode(), called from initialize(): an FMU only reports how many continuous states it has once it has left Initialization Mode. * instantiated_model.py: SolverType duplicates the C enum's *values*, so inserting a solver renumbered oms_ma/oms_mav/oms_mav2 and setSolver() started choosing the wrong one — 37 testsuite failures with no hint of why. Values fixed and the duplication called out in a comment; deriving them from the C API would be better. Requires OpenModelica/OMSimulator-3rdParty#111 (SUNDIALS_ENABLE_IDA was OFF). Tested with a --daeMode Model Exchange FMU exported by OpenModelica (OpenModelica/OpenModelica#16622): it simulates through IDA, and the trajectory agrees with the same model exported as an ordinary ODE FMU and run through CVODE by this same master to ~1e-8, state event included. ctest: 117/117, the same as master. Refs OpenModelica#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to adrpo/OMSimulator
that referenced
this pull request
Oct 31, 2026
The structure the manifests give is what a sparse solver wants, so the Jacobian is held as a CSC matrix and factorized by KLU instead of being a dense one. The pattern never changes, so KLU reuses its symbolic factorization across the run, and the matrix carries its structure in the same arrays as its values — which is why SUNMatZero is not called on it: it would wipe the pattern, and every entry is written each time anyway, structure and value together. Without a usable structure nothing changes: the matrix is dense and IDA differences it itself. Needs OpenModelica/OMSimulator-3rdParty#111, which brings in SuiteSparse and turns SUNDIALS_ENABLE_KLU on. Same answers as the dense factorization, to the last digit or two of the trajectory, and the same step counts: x(0.5) x(1.0) loop equation dense 0.817368712680 0.701445480172 2.7e-14 KLU 0.817368712680 0.701445480171 2.6e-14 omc 0.817368180018 0.701445143664 - ctest: 117/117. Refs OpenModelica#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to adrpo/OMSimulator
that referenced
this pull request
Nov 1, 2026
The other half: a strongly coupled system that holds a component in DAE mode is integrated as one implicit DAE instead of an explicit ODE. The unknowns are laid out per component, [x_i | z_i], and so are the rows: a component in DAE mode contributes the residuals it declares, an ODE component the explicit rows x'_i - f_i(x, u, t). Both blocks are square, so the system is, and the two mix — a DAE component next to an ordinary FMU needs nothing special. IDASetId says which unknowns are differential, and IDACalcIC solves for the algebraic ones and for the derivatives, at the start and again after every event: the components initialize consistently with themselves, not with the coupled system, and an event moves them the same way. The coupling stays where it was for CVODE: updateInputs() propagates connected outputs into inputs before the rows are evaluated, so the residuals see a consistent coupling instead of the value left over from the last accepted step, and a loop spanning components is still resolved by AlgLoop. Lifting the connection equations into residual rows of their own — which is what would let one global implicit system replace that inner solve — is the next step, not this one. * SystemSC: ida_res / ida_roots, the [x | z] layout, setDaePoint/getDaePoint, calcConsistentInitialConditions, doStepIDA, and the IDA setup and teardown beside the CVODE ones. Dense Jacobian for now; the manifest's residual dependencies are a sparsity pattern for KLU, which matters as soon as the system is more than small. * oms_solver_sc_ida: DAE mode is not really a choice — a component that declares a DAE formulation has no explicit ODE for CVODE to integrate — so the system switches to IDA on its own and says so. The solver still has a name, for the SSD and for --solver. * ComponentFMU3ME: DAE mode is switched on at instantiation, which is where Configuration Mode is. The checks that need the state count moved to validateDaeMode(), called from initialize(): an FMU only reports how many continuous states it has once it has left Initialization Mode. * instantiated_model.py: SolverType duplicates the C enum's *values*, so inserting a solver renumbered oms_ma/oms_mav/oms_mav2 and setSolver() started choosing the wrong one — 37 testsuite failures with no hint of why. Values fixed and the duplication called out in a comment; deriving them from the C API would be better. fmu.py has a second copy, _FMU_SOLVER, which still sent 6 for 'ma': that is oms_solver_wc_min now, SystemWC::setSolver refuses it, and the command line could no longer run any Co-Simulation FMU. Fixed, with 'ida' added there too, and covered by simulation/fmuSimulation3.py, which selects every solver by name on both kinds of an FMU. Requires OpenModelica/OMSimulator-3rdParty#111 (SUNDIALS_ENABLE_IDA was OFF). Tested with a --daeMode Model Exchange FMU exported by OpenModelica (OpenModelica/OpenModelica#16622): it simulates through IDA, and the trajectory agrees with the same model exported as an ordinary ODE FMU and run through CVODE by this same master to ~1e-8, state event included. ctest: 117/117, the same as master. Refs OpenModelica#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to adrpo/OMSimulator
that referenced
this pull request
Nov 1, 2026
The structure the manifests give is what a sparse solver wants, so the Jacobian is held as a CSC matrix and factorized by KLU instead of being a dense one. The pattern never changes, so KLU reuses its symbolic factorization across the run, and the matrix carries its structure in the same arrays as its values — which is why SUNMatZero is not called on it: it would wipe the pattern, and every entry is written each time anyway, structure and value together. Without a usable structure nothing changes: the matrix is dense and IDA differences it itself. Needs OpenModelica/OMSimulator-3rdParty#111, which brings in SuiteSparse and turns SUNDIALS_ENABLE_KLU on. Same answers as the dense factorization, to the last digit or two of the trajectory, and the same step counts: x(0.5) x(1.0) loop equation dense 0.817368712680 0.701445480172 2.7e-14 KLU 0.817368712680 0.701445480171 2.6e-14 omc 0.817368180018 0.701445143664 - ctest: 117/117. Refs OpenModelica#1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7
adrpo
added a commit
to OpenModelica/OMSimulator
that referenced
this pull request
Nov 1, 2026
…#1645) (#1646) * Read an FMU's fmi-ls-dae manifest and switch it into DAE mode (#1645) The first half of DAE-FMU support: recognising one and being able to put it in DAE mode. The integration itself — an IDA path in SystemSC over the union of the components' states and algebraic variables, with the connection equations as residual rows — is the follow-up, and nothing here changes how a system is simulated yet. A Model Exchange FMU carrying extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml exposes the residuals of F(t, x, x', z) = 0 and lets the importer solve for the algebraic variables z beside the states, instead of iterating on them behind its own interface. It is an ordinary ODE FMU until the importer sets the structural parameter the manifest names. * LsDae: reads the manifest — the enable parameter, the algebraic variables, and the residuals with their dependencies (the sparsity a DAE master differences the Jacobian with). No manifest is not an error; it is the ordinary FMU. * ComponentFMU3ME: loads it when the FMU is unpacked, remembers the <ContinuousStateDerivative> value references (the model description's own order, which pairs them with the states), and gains enableDaeMode() — Configuration Mode, the switch, and the checks that the system is square and fully implicit — plus the residual, algebraic-variable and derivative accessors a master needs. * variable.py: FMI 3.0's structuralParameter causality. Without it Causality[causality] raises a KeyError and OMSimulator cannot even load an FMU that has one — which every fmi-ls-dae FMU does. Tested against a --daeMode Model Exchange FMU exported by OpenModelica (OpenModelica/OpenModelica#16622): OMSimulator reads the manifest and reports "3 residuals over 1 states and 2 algebraic variables", and the FMU's own refusal to run as an ODE FMU comes through as a diagnostic instead of a crash. Ordinary ODE FMUs are unaffected. Refs #1645, OpenModelica/OpenModelica#16622 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7 * Integrate a system with DAE components using IDA (#1645) The other half: a strongly coupled system that holds a component in DAE mode is integrated as one implicit DAE instead of an explicit ODE. The unknowns are laid out per component, [x_i | z_i], and so are the rows: a component in DAE mode contributes the residuals it declares, an ODE component the explicit rows x'_i - f_i(x, u, t). Both blocks are square, so the system is, and the two mix — a DAE component next to an ordinary FMU needs nothing special. IDASetId says which unknowns are differential, and IDACalcIC solves for the algebraic ones and for the derivatives, at the start and again after every event: the components initialize consistently with themselves, not with the coupled system, and an event moves them the same way. The coupling stays where it was for CVODE: updateInputs() propagates connected outputs into inputs before the rows are evaluated, so the residuals see a consistent coupling instead of the value left over from the last accepted step, and a loop spanning components is still resolved by AlgLoop. Lifting the connection equations into residual rows of their own — which is what would let one global implicit system replace that inner solve — is the next step, not this one. * SystemSC: ida_res / ida_roots, the [x | z] layout, setDaePoint/getDaePoint, calcConsistentInitialConditions, doStepIDA, and the IDA setup and teardown beside the CVODE ones. Dense Jacobian for now; the manifest's residual dependencies are a sparsity pattern for KLU, which matters as soon as the system is more than small. * oms_solver_sc_ida: DAE mode is not really a choice — a component that declares a DAE formulation has no explicit ODE for CVODE to integrate — so the system switches to IDA on its own and says so. The solver still has a name, for the SSD and for --solver. * ComponentFMU3ME: DAE mode is switched on at instantiation, which is where Configuration Mode is. The checks that need the state count moved to validateDaeMode(), called from initialize(): an FMU only reports how many continuous states it has once it has left Initialization Mode. * instantiated_model.py: SolverType duplicates the C enum's *values*, so inserting a solver renumbered oms_ma/oms_mav/oms_mav2 and setSolver() started choosing the wrong one — 37 testsuite failures with no hint of why. Values fixed and the duplication called out in a comment; deriving them from the C API would be better. fmu.py has a second copy, _FMU_SOLVER, which still sent 6 for 'ma': that is oms_solver_wc_min now, SystemWC::setSolver refuses it, and the command line could no longer run any Co-Simulation FMU. Fixed, with 'ida' added there too, and covered by simulation/fmuSimulation3.py, which selects every solver by name on both kinds of an FMU. Requires OpenModelica/OMSimulator-3rdParty#111 (SUNDIALS_ENABLE_IDA was OFF). Tested with a --daeMode Model Exchange FMU exported by OpenModelica (OpenModelica/OpenModelica#16622): it simulates through IDA, and the trajectory agrees with the same model exported as an ordinary ODE FMU and run through CVODE by this same master to ~1e-8, state event included. ctest: 117/117, the same as master. Refs #1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7 * FMI 3.0 ModelStructure dependencies are value references FMI 3.0 changed this from FMI 2.0: the `dependencies` attribute of an <Output>, <InitialUnknown> and the rest holds **value references**, not one-based indices into the variable list. Both FMI 3.0 components read them as indices, the FMI 2.0 way, so the dependency graph was wired to whichever variables happened to sit at those positions — and an FMU whose dependency names value reference 0, a perfectly ordinary one, was rejected outright: warning: Output q has bad dependency on variable with index 0 which couldn't be resolved error: B: erroneous dependencies detected in modelDescription.xml which is how this was found: two connected FMI 3.0 FMUs, the driven one with its input at value reference 0. Resolve them as value references. ComponentFMUME keeps the index reading, which is right for FMI 2.0. Refs #1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7 * Algebraic loops across components as rows of the DAE system (#1645) An algebraic loop over connections was solved by an inner KINSOL Newton, run to convergence inside every residual evaluation, on top of whatever solver each FMU hides behind its own interface. Where IDA drives the system it does not have to be: the loop's connections become unknowns and rows of the system itself, and the coupling constraint gets the integrator's own error control. The translation is direct, because it is the same system the inner solver had: each connection of the loop contributes one unknown — the value of the input it drives — and one row, output - input = 0. They are algebraic unknowns, they follow the components' in y, and updateInputs() leaves those loops alone since IDA has already set their inputs. A loop is only lifted if all of its connections carry reals; one with an integer or boolean connection keeps its own solver, and so does every loop when the system is not in DAE mode, where nothing here applies. Initialization and events leave every loop to IDACalcIC rather than to the inner solver. They have to: a component in DAE mode does not solve for its own algebraic outputs — the master does — so "set the input, read the output back" has nothing behind it, and KINSOL fails on a singular Jacobian. The consistent initial conditions are solved for the whole coupled system instead, which is where that belongs. Tested with a loop across two FMUs, one of them --daeMode, against the same two components written as a single Modelica model and simulated by OpenModelica: x(0.5) x(1.0) loop equation IDA, loop as rows 0.817368712680 0.701445480172 2.7e-14 CVODE + KINSOL 0.817433688498 0.701528323696 8.7e-08 monolithic omc 0.817368180018 0.701445143664 - The lifted rows hold the connection to machine precision instead of the inner solver's tolerance, and land two orders of magnitude closer to the monolithic answer. (The KINSOL run of the same loop needs --directionalDerivatives=false: it wants a Jacobian the FMU does not provide, where the lifted rows are part of the one IDA differences itself.) ctest: 117/117, the same as master. Refs #1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7 * Difference the DAE Jacobian by colour, out of the manifests (#1645) IDA's own difference quotient costs one residual evaluation per unknown, and every one of those calls into every FMU of the system. The components' manifests say which unknowns each residual reaches, which is enough to colour the columns: two share a colour when no row reaches both, so one evaluation differences all of them at once. The whole Jacobian then costs as many evaluations as there are colours, which for a coupled system is a handful however many unknowns there are: 1 pair of components: 5x5, 16 nonzeros, 5 colours 4 pairs: 20x20, 64 nonzeros, 5 colours 8 pairs: 40x40, 128 nonzeros, 5 colours Where a manifest's <Residual> names dependencies they are used exactly; where it names none, or names a value reference that is not one of the unknowns, the row falls back to the whole component. An ODE component's rows and the rows of a lifted loop fall back the same way. Every case is a superset of the true structure, which is what a coloured difference quotient needs — omitting a nonzero silently attributes one column's perturbation to another. Two things the manifest does not say, and one it says in a way that had to be read correctly: * A residual's dependency on an *input* is not in it: the dependencies it declares are those of the FMU's own DAE Jacobian, whose columns are the states and the algebraic variables. An input only becomes an unknown here, where the master carries it for a lifted loop, so those columns are added to every row of the component regardless. Without them the Jacobian is wrong by exactly the -1 of the input's own row. * The `derivative` attribute of a Float64 is a *value reference* in FMI 3.0 where FMI 2.0 had an index — the same change as the ModelStructure dependencies, and fmi4c passes the raw attribute through under its FMI 2.0 name. Read as an index, `derivative="0"` (a perfectly ordinary value reference) meant "no state", and the structure was discarded. The linear solver is still dense: KLU wants SuiteSparse, which OMSimulator's 3rdParty does not carry. The structure is built and kept in a form a sparse matrix can take directly, so that is a small step once it does — and the evaluations, not the factorization, are what a coupled system pays for at this size. Checked against a column-at-a-time difference quotient of the same system: the coloured Jacobian agrees to the last bit, and the loop model's results and step counts are unchanged from the dense-differenced ones. ctest: 117/117. Refs #1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7 * Factorize the DAE Jacobian with KLU (#1645) The structure the manifests give is what a sparse solver wants, so the Jacobian is held as a CSC matrix and factorized by KLU instead of being a dense one. The pattern never changes, so KLU reuses its symbolic factorization across the run, and the matrix carries its structure in the same arrays as its values — which is why SUNMatZero is not called on it: it would wipe the pattern, and every entry is written each time anyway, structure and value together. Without a usable structure nothing changes: the matrix is dense and IDA differences it itself. Needs OpenModelica/OMSimulator-3rdParty#111, which brings in SuiteSparse and turns SUNDIALS_ENABLE_KLU on. Same answers as the dense factorization, to the last digit or two of the trajectory, and the same step counts: x(0.5) x(1.0) loop equation dense 0.817368712680 0.701445480172 2.7e-14 KLU 0.817368712680 0.701445480171 2.6e-14 omc 0.817368180018 0.701445143664 - ctest: 117/117. Refs #1645 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfnWV25k2u9x4CHxcnQbF7 * Test a system with DAE-mode FMUs (#1645) Nothing in the testsuite ran the DAE path, so codecov reported most of this PR as uncovered: the fmi-ls-dae manifest, the switch into DAE mode, IDA, the loop lifted into rows and the KLU factorization only ran in hand-made checks. * simulation/DaeMode1.py: DaeLoopA (--daeMode) and an ordinary LoopB in an algebraic loop, through an SSP. The system switches to IDA by itself, lifts the loop's two connections into rows and colours the 5x5 Jacobian. x(1) = 0.701445, the closed-loop solution 0.5 + 0.5*exp(-1/1.1). * simulation/DaeMode2.py: DaeLoopA alone through the FMU API, once with IDA selected by name and once with the default CVODE, which the system replaces with IDA. Both give x(1) = 0.683940 = 0.5 + 0.5*exp(-1). Both FMUs were exported by OpenModelica with --simCodeTarget=C+Rust (the ls-dae export of OpenModelica#17185), so they carry x86_64-linux binaries only and the tests are enabled for linux only. They need glibc 2.38 and run on the 24.04 and 26.04 images; ASan is off for them, not verified here. Refs #1645 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two things OpenModelica/OMSimulator#1645 needs, both of which the file's own comment said were missing: "No ARKODE/CVODES/IDA/IDAS, and no KLU — unlike OMC we have no SuiteSparse in 3rdParty to build it against."
OMSimulator's strongly coupled system gains a DAE mode (fmi-ls-dae). A component that exposes the residuals of
F(t, x, x', z) = 0instead of an explicit ODE cannot be integrated by CVODE: the master carries the component's algebraic variables beside the states and drives the residuals to zero with IDA. The components' manifests say which unknowns each residual reaches, so that Jacobian has a structure, is held sparse, and wants KLU to factorize it.SUNDIALS IDA
SUNDIALS_ENABLE_IDAon, andoms::3rd::sundials::idabeside the CVODE and KINSOL aliases.SuiteSparse, for KLU
A submodule pinned to v7.12.2 — the same release OpenModelica carries — with the build delegated to its own CMakeLists exactly as
OMCompiler/3rdParty/CMakeLists.txtdoes it:SUITESPARSE_ENABLE_PROJECTSlimited tosuitesparse_config;amd;colamd;btf;klu(OMC also builds umfpack; nothing here needs it),KLU_USE_CHOLMODoff, so CHOLMOD and its dependencies stay out.SUNDIALS'
FindKLUprefers an existingSuiteSparse::KLUtarget as long as it is not pointed at a KLU somewhere else, so it picks up the in-tree one on its own — nothing is handed to it.SUNDIALS_ENABLE_KLU_CHECKShas to be off: the checkstry_compile()againstSUNDIALS::KLU, which here aliases an in-tree target rather than an imported one and is invisible to the separate try_compile project.SUNDIALS_INDEX_SIZEwas already 64, which is what SuiteSparse' int64 KLU entry points want.No BLAS
SuiteSparse_config looks for a BLAS unless told that nothing needs one, and the first push failed to configure on the Linux and Windows CI images, which have none. Nothing here needs one: BLAS is used by CHOLMOD, UMFPACK and SPQR, none of which are enabled, and KLU only reaches them through
KLU_USE_CHOLMOD, which is off — neither AMD, COLAMD, BTF nor KLU names BLAS in its build or its sources. OpenModelica does not hit this because it requires BLAS/LAPACK anyway; OMSimulator does not.So
SUITESPARSE_REQUIRE_BLASis off. The path that takes instead still labels the BLAS it did not look for, and its threading checks dereference that label unguarded, so an emptyBLA_VENDORis a configure error one file later — any non-empty value does, since the checks are all guarded on a specific vendor and nothing is searched for or linked, so it is set toGeneric. Verified with a clean configure and build with no BLAS on the search path: KLU builds and links, and OMSimulator's testsuite passes 117/117 against it.Configuring prints one harmless warning, the same one OMC documents:
FindKLUtriesfind_package(KLU CONFIG)first, and on a machine with SuiteSparse' own CMake package files installed that reports "found KLUConfig.cmake but it set KLU_FOUND to FALSE".SuiteSparse::KLUis already a target by then, so the system package can neither be imported nor override it, and the in-treeKLU_staticwins.Licensing note for the record: AMD, BTF, COLAMD and KLU are LGPL-2.1-or-later, and SuiteSparse_config is BSD-3-Clause — the same set OpenModelica already ships.
Generated by Claude Code