Skip to content

fmi-ls-dae: acausal connections through FMU terminals (#1686) - #1687

Open
adrpo wants to merge 1 commit into
OpenModelica:masterfrom
adrpo:fmi-ls-dae-acausal
Open

adrpo wants to merge 1 commit into
OpenModelica:masterfrom
adrpo:fmi-ls-dae-acausal

Conversation

@adrpo

@adrpo adrpo commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Implements #1686, part of #1432: acausal components, each a separate FMI 3.0 Model Exchange FMU with fmi-ls-dae, connected through their terminals in an SSP and integrated as one DAE with IDA. It builds on #1646. The FMUs come from OpenModelica with --simCodeTarget=C+Rust --daeMode --fmuAcausalComponent (OpenModelica/OpenModelica#17665, not merged yet).

What the FMUs carry

  • the fmi-ls-dae manifest;
  • FMI 3.0 terminals: one per connector, inflow for flows and signal for potentials, and a TerminalStreamMemberVariable per stream variable that pairs it with the variable standing for its inStream ('inStream(port.h_outflow)');
  • extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml, which names the residual standing for each flow (ConnectorResidual) and for each stream's inStream (StreamResidual). While its terminal is unconnected, those residuals read flow = 0 and inStream(h) = h.

What this does

  • Connectors: the members of an acausal component's terminals (p.v, p.i) become connectors of SSP 2.0 kind unspecified, the kind SSP defines "for acausal connections of Modelica models". In C++ they are oms_causality_bidir.

  • Connections: a connection between two such connectors is acausal (oms_connection_acausal).

    • It stays out of the directed graphs.
    • A member may be connected any number of times.
    • The SSD stores it as an ordinary ssd:Connection.
    • In Python, addConnection between two terminals expands them member by member and refuses terminals whose members differ.
  • System: in DAE mode, SystemSC::collectAcausalConnections builds the connection sets and replaces the residuals of the connected flows with the equations of the sets:

    • k-1 rows v_1 - v_m = 0 for the potentials of a set of k members;
    • one row sum(i_m) = 0 for its flows.

    The unknowns don't change, and the system stays square. Each row reads the members' values from the FMU. That is needed because a member may be computed by the component, not be an unknown: in the source, n.i is the alias -p.i, and the ground's p.v is a constant. A row's sparsity is the member's own column plus the columns the replaced residuals reach, since those take the flow and the potentials as arguments.

  • Streams: a stream variable is connected like the other members, by its member name. Each connected stream's StreamResidual row becomes the mixing rule of its set:

    • s_i - sum_{j≠i} max(-m_j, eps)*h_j / sum_{j≠i} max(-m_j, eps) = 0, where s_i stands for inStream(h_i), m_j is the flow of h_j's terminal into its component, and eps = 1e-7 (OpenModelica's flowThreshold);
    • for two members it is s_i - h_j = 0.

    The terminal of a connected stream must have exactly one flow. The row's sparsity is built as for the other rows, from the inStream variable and the other members' stream and flow variables.

  • Aliases and parameters: a terminal member may be an FMI 3.0 <Alias> (b.p in a component with a.p = b.p). The FMI library doesn't list aliases, so the terminals reader resolves them from modelDescription.xml, in C++ and in Python. A terminal member that is a (calculated) parameter, such as a boundary's fixed pressure, is connected acausally too.

  • Refused: a set that mixes flows, potentials and stream variables, sets that don't give as many equations as they free residuals, and an acausal connection whose component is not in DAE mode.

model.addComponent(CRef('default', 'V'), 'resources/AcausalSource.fmu')
model.addComponent(CRef('default', 'RC'), 'resources/AcausalRC.fmu')
model.addComponent(CRef('default', 'G'), 'resources/AcausalGround.fmu')
model.addConnection(CRef('default', 'V', 'p'), CRef('default', 'RC', 'p'))
model.addConnection(CRef('default', 'V', 'n'), CRef('default', 'G', 'p'))

Tests

Three electrical FMUs (source, RC branch, ground) and two fluid FMUs (a pressure/enthalpy boundary, and a mixing volume behind a flow resistance), exported with the compiler of OpenModelica/OpenModelica#17665. The models are in the comments of DaeMode3.py and DaeMode5.py. Like DaeMode1/2, the tests only run on Linux: the FMUs carry only x86_64-linux binaries, and those need glibc 2.38.

Test Circuit Result Analytic
DaeMode3.py source, RC, ground vc(1) = 0.632121, i = 0.367879 1 − e⁻¹, e⁻¹
DaeMode4.py two RC branches in parallel (a three-way node), R2 = 2 vc2(1) = 0.393469, source current 0.671145 1 − e^-0.5, e⁻¹ + e^-0.5/2
DaeMode4.py an RC alone, unconnected p.i = 0 flow = 0
DaeMode4.py a flow connected to a potential refused
DaeMode5.py boundary (p = 2, h = 2), volume, boundary (p = 1) h(1) = 1.632121, 1 kg/s 2 − e⁻¹
DaeMode5.py two branches (h = 2 and 4) mixing in a node of three terminals, then a volume h3(1) = 1.160709, node p = 1.666667, mix 1.566937 3 − 4e^(−1/3) + 2e^(−2/3), 5/3, (h1 + h2)/2

Locally, ctest gives 132 tests, 0 failed.

Not in this PR

  • components that prescribe a flow (a mass flow source, a current source): an FMU solves its own initial system with the placeholder residuals as ordinary equations, where the prescribed flow contradicts flow = 0, so exitInitializationMode fails. DaeMode5 therefore uses pressure boundaries and flow resistances;
  • array connectors;
  • nested terminals;
  • index detection across FMUs: two state potentials joined by a connection give index 2.

🤖 Generated with Claude Code

@adrpo adrpo self-assigned this Nov 1, 2026
@adrpo adrpo added the project/openscaling OpenSCALING related issues and pull requests label Nov 1, 2026
@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.86585% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 31.36%. Comparing base (3046f82) to head (bf1c8ad).

Files with missing lines Patch % Lines
src/OMSimulatorLib/SystemSC.cpp 77.88% 17 Missing and 29 partials ⚠️
src/OMSimulatorLib/ComponentFMU3ME.cpp 54.28% 7 Missing and 9 partials ⚠️
src/OMSimulatorLib/LsDae.cpp 78.94% 7 Missing and 5 partials ⚠️
src/OMSimulatorLib/System.cpp 22.22% 3 Missing and 4 partials ⚠️
src/OMSimulatorLib/Connector.cpp 0.00% 6 Missing ⚠️
src/OMSimulatorLib/Connection.cpp 0.00% 1 Missing ⚠️
src/OMSimulatorLib/LsDae.h 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1687      +/-   ##
==========================================
+ Coverage   30.22%   31.36%   +1.13%     
==========================================
  Files          74       74              
  Lines       14661    14989     +328     
  Branches     9432     9646     +214     
==========================================
+ Hits         4431     4701     +270     
- Misses       8892     8911      +19     
- Partials     1338     1377      +39     
Flag Coverage Δ
linux 31.36% <72.86%> (+1.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)

Acausal components, each an FMI 3.0 Model Exchange FMU with fmi-ls-dae
and OpenModelica's fmi-ls-dae-acausal manifest, are connected through
their terminals and integrated as one DAE with IDA.

- The members of an acausal component's terminals become connectors of
  SSP 2.0 kind "unspecified". A connection between two of them is
  acausal (oms_connection_acausal): it stays out of the directed graphs,
  a member may be connected any number of times, and the SSD stores it
  as an ordinary connection.
- SystemSC builds the connection sets and replaces the residuals the
  manifest names for the connected flows (flow = 0) with the equations
  of the sets: k-1 rows for the potentials, one flow-sum row. The system
  stays square; the rows' sparsity comes from the replaced residuals.
  Mixed or unbalanced sets, and acausal connections outside DAE mode,
  are refused.
- Stream variables: the manifest names, per stream variable of a terminal,
  the residual that reads inStream(h) = h (StreamResidual), and the
  terminal's TerminalStreamMemberVariable names the variable that stands
  for its inStream. The stream variable is connected like the other
  members, and SystemSC replaces each connected stream's residual by the
  mixing rule of its set, s_i - sum_{j<>i} max(-m_j, eps)*h_j /
  sum_{j<>i} max(-m_j, eps) (s_i - h_j for two members), m_j being the
  flow of h_j's terminal.
- A terminal member may be an FMI 3.0 <Alias> (b.p with a.p = b.p), which
  the FMI library does not list; it is resolved from modelDescription.xml.
  A terminal member that is a (calculated) parameter, e.g. the fixed
  pressure of a boundary, is connected acausally too.
- Python: Causality.unspecified, the terminals read from the FMU, and
  addConnection between two terminals expands them member by member.

Tests: DaeMode3.py (source, RC, ground) and DaeMode4.py (a three-way
node, an unconnected component, a flow connected to a potential) with
three electrical FMUs, and DaeMode5.py (a source, a volume and a sink; two
branches mixing in a node of three terminals) with two fluid FMUs, all
exported by OpenModelica --fmuAcausalComponent
(OpenModelica/OpenModelica#17665), x86_64-linux only.

Refs OpenModelica#1686, OpenModelica#1432

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adrpo

adrpo commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

This has a connection with how SSP terminals are handled #1548.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

project/openscaling OpenSCALING related issues and pull requests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant