diff --git a/include/OMSimulator/Types.h b/include/OMSimulator/Types.h index fe4fa0401..3a8f9b0d7 100644 --- a/include/OMSimulator/Types.h +++ b/include/OMSimulator/Types.h @@ -140,6 +140,7 @@ typedef enum { typedef enum { oms_connection_single, ///< FMI conncection (directed & undelayed signal connection) oms_connection_bus, ///< Bus connection + oms_connection_acausal, ///< acausal connection between terminal members (potentials equal, flows sum to zero) } oms_connection_type_enu_t; /** diff --git a/src/OMSimulatorLib/ComponentFMU3ME.cpp b/src/OMSimulatorLib/ComponentFMU3ME.cpp index 9dbb5ddc4..d4aeb9aa4 100644 --- a/src/OMSimulatorLib/ComponentFMU3ME.cpp +++ b/src/OMSimulatorLib/ComponentFMU3ME.cpp @@ -203,6 +203,25 @@ oms::Component* oms::ComponentFMU3ME::NewComponent(const oms::ComRef& cref, oms: component->lsDae.getVersion() + "): " + std::to_string(component->lsDae.getResiduals().size()) + " residuals over " + std::to_string(component->derivativeVrs.size()) + " states and " + std::to_string(component->lsDae.getAlgebraicVariables().size()) + " algebraic variables"); + + // An acausal component: its terminals are connected here, and the + // residuals that stand for their flows are replaced by the equations of + // those connections. + if (component->lsDaeAcausal.load(tempDir.generic_string())) + { + if (!component->terminals.load(tempDir.generic_string())) + { + logWarning("FMU \"" + std::string(cref) + "\" carries an acausal manifest (" + LsDaeAcausalManifest::name() + + ") but no terminals; it cannot be connected acausally"); + component->lsDaeAcausal = LsDaeAcausalManifest(); + } + else + logInfo("FMU \"" + std::string(cref) + "\" is an acausal component (" + LsDaeAcausalManifest::name() + " " + + component->lsDaeAcausal.getVersion() + "): " + std::to_string(component->terminals.getTerminals().size()) + + " terminal(s), " + std::to_string(component->lsDaeAcausal.getConnectorResiduals().size()) + + " connector residual(s)" + (component->lsDaeAcausal.getStreamResiduals().empty() ? std::string() : + ", " + std::to_string(component->lsDaeAcausal.getStreamResiduals().size()) + " stream residual(s)")); + } } // update FMU info @@ -288,6 +307,43 @@ oms::Component* oms::ComponentFMU3ME::NewComponent(const oms::ComRef& cref, oms: component->connectors.push_back(new Connector(oms_causality_calculatedParameter, component->allVariables[i].getType(), component->allVariables[i].getCref(), component->getFullCref())); component->connectors.back()->setNumericType(component->allVariables[i].getNumericType()); } + // The members of an acausal component's terminals that are not inputs or + // outputs, connected acausally (SSP kind "unspecified"). A stream member is + // connected by its stream variable; the variable that stands for its inStream + // is the importer's to compute. + if (component->lsDaeAcausal.isValid()) + { + auto addConnector = [&](const Terminals::Terminal& terminal, const std::string& variableName) { + const std::string resolved = component->terminals.resolveAlias(variableName); + const Variable* v = component->getVariable(ComRef(resolved)); + if (!v) + { + logWarning("FMU \"" + std::string(cref) + "\": terminal \"" + terminal.name + "\" names an unknown variable \"" + variableName + "\""); + return; + } + if (resolved == variableName && (v->isInput() || v->isOutput())) + return; // a causal signal, already a connector + // A (calculated) parameter of a terminal, e.g. the fixed pressure of a + // boundary, is connected acausally like the other members. + for (auto it = component->connectors.begin(); it != component->connectors.end(); ++it) + if ((*it)->getName() == ComRef(variableName)) + { + delete *it; + component->connectors.erase(it); + break; + } + component->connectors.push_back(new Connector(oms_causality_bidir, v->getType(), ComRef(variableName), component->getFullCref())); + component->connectors.back()->setNumericType(v->getNumericType()); + }; + for (const Terminals::Terminal& terminal : component->terminals.getTerminals()) + { + for (const Terminals::Member& member : terminal.members) + addConnector(terminal, member.variableName); + for (const Terminals::StreamMember& member : terminal.streamMembers) + addConnector(terminal, member.outStreamVariableName); + } + } + component->connectors.push_back(NULL); component->element.setConnectors(&component->connectors[0]); @@ -2206,6 +2262,17 @@ oms_status_enu_t oms::ComponentFMU3ME::getDaeResiduals(double* residuals) return oms_status_ok; } +oms_status_enu_t oms::ComponentFMU3ME::getFloat64(const std::vector& vrs, double* values) +{ + CallClock callClock(clock); + if (vrs.empty()) + return oms_status_ok; + fmi3Status fmistatus = fmi3_getFloat64(instance, vrs.data(), vrs.size(), values, vrs.size()); + if (fmi3OK != fmistatus) + return logError_FMUCall("fmi3_getFloat64", this); + return oms_status_ok; +} + oms_status_enu_t oms::ComponentFMU3ME::getAlgebraicVariables(double* values) { CallClock callClock(clock); diff --git a/src/OMSimulatorLib/ComponentFMU3ME.h b/src/OMSimulatorLib/ComponentFMU3ME.h index 9cbd6aeb5..65a2d2457 100644 --- a/src/OMSimulatorLib/ComponentFMU3ME.h +++ b/src/OMSimulatorLib/ComponentFMU3ME.h @@ -134,6 +134,16 @@ namespace oms size_t getNumberOfDaeResiduals() const {return lsDae.getResiduals().size();} size_t getNumberOfAlgebraicVariables() const {return lsDae.getAlgebraicVariables().size();} oms_status_enu_t getDaeResiduals(double* residuals); + /** + * \brief Whether the FMU is an acausal component: one whose terminals are + * connected by the importer, which then replaces the residuals the + * acausal manifest names with the equations of its connections. + */ + bool isAcausalComponent() const {return lsDaeAcausal.isValid();} + const LsDaeAcausalManifest& getLsDaeAcausal() const {return lsDaeAcausal;} + const Terminals& getTerminals() const {return terminals;} + /// Read Float64 variables by value reference, in one call into the FMU. + oms_status_enu_t getFloat64(const std::vector& vrs, double* values); /// The value references of the continuous states, in the order /// fmi3GetContinuousStates uses — that of . const std::vector& getStateValueReferences() const {return stateVrs;} @@ -196,6 +206,9 @@ namespace oms /// fmi-ls-dae: the manifest, and whether this instance runs in DAE mode. LsDaeManifest lsDae; bool daeMode = false; + /// The acausal extension of the manifest, and the terminals it refers to. + LsDaeAcausalManifest lsDaeAcausal; + Terminals terminals; /// The value references of the model /// description, in the order fmi3GetContinuousStates uses for the states. std::vector derivativeVrs; diff --git a/src/OMSimulatorLib/Connection.cpp b/src/OMSimulatorLib/Connection.cpp index 0c72bfa34..fb0358f25 100644 --- a/src/OMSimulatorLib/Connection.cpp +++ b/src/OMSimulatorLib/Connection.cpp @@ -123,7 +123,9 @@ oms::Connection& oms::Connection::operator=(const oms::Connection& rhs) oms_status_enu_t oms::Connection::exportToSSD(pugi::xml_node &root) const { pugi::xml_node node; - if(type == oms_connection_single) + // An acausal connection is an ordinary SSD connection between connectors of + // kind "unspecified". + if(type == oms_connection_single || type == oms_connection_acausal) node = root.append_child(oms::ssp::Draft20180219::ssd::connection); else if(type == oms_connection_bus) node = root.append_child(oms::ssp::Draft20180219::bus_connection); diff --git a/src/OMSimulatorLib/Connector.cpp b/src/OMSimulatorLib/Connector.cpp index ed9a170b9..2e014fe0a 100644 --- a/src/OMSimulatorLib/Connector.cpp +++ b/src/OMSimulatorLib/Connector.cpp @@ -139,6 +139,8 @@ oms::Connector* oms::Connector::NewConnector(const pugi::xml_node& node, const s causality = oms_causality_parameter; else if (causalityString == "calculatedParameter") causality = oms_causality_calculatedParameter; + else if (causalityString == "unspecified") + causality = oms_causality_bidir; // an acausal connector (SSP 2.0) else { logError("Failed to import connector \"" + std::string(cref) + "\" with causality \"" + causalityString + "\""); @@ -231,6 +233,10 @@ oms_status_enu_t oms::Connector::exportToSSD(pugi::xml_node &root) const node.append_attribute("kind") = "calculatedParameter"; break; case oms_causality_bidir: + // SSP 2.0: a connector whose direction is given by the connections, as + // for an acausal connection of Modelica models + node.append_attribute("kind") = "unspecified"; + break; case oms_causality_undefined: break; } @@ -359,6 +365,9 @@ std::string oms::Connector::getCausalityString() const case oms_causality_calculatedParameter: return "calculatedParameter"; + case oms_causality_bidir: + return "unspecified"; + default: return std::string("unknown"); } diff --git a/src/OMSimulatorLib/LsDae.cpp b/src/OMSimulatorLib/LsDae.cpp index f6bfafbe0..c73666d01 100644 --- a/src/OMSimulatorLib/LsDae.cpp +++ b/src/OMSimulatorLib/LsDae.cpp @@ -112,3 +112,87 @@ bool oms::LsDaeManifest::load(const std::string& unzippedFmuPath) valid = true; return true; } + +bool oms::LsDaeAcausalManifest::load(const std::string& unzippedFmuPath) +{ + const filesystem::path manifest = filesystem::path(unzippedFmuPath) / filesystem::path(path()); + if (!filesystem::exists(manifest)) + return false; // not an acausal component + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(manifest.string().c_str()); + if (!result) + { + logWarning("fmi-ls-dae-acausal: could not read \"" + manifest.string() + "\": " + std::string(result.description())); + return false; + } + + const pugi::xml_node root = doc.child("fmiDAEAcausalManifest"); + if (!root) + { + logWarning("fmi-ls-dae-acausal: \"" + manifest.string() + "\" has no element"); + return false; + } + + version = root.attribute("fmi-ls:fmi-ls-version").as_string(); + for (const pugi::xml_node& node : root.child("ConnectorResiduals").children("ConnectorResidual")) + connectorResiduals.push_back({node.attribute("valueReference").as_uint(), + node.attribute("terminal").as_string(), + node.attribute("variableName").as_string()}); + for (const pugi::xml_node& node : root.child("ConnectorResiduals").children("StreamResidual")) + streamResiduals.push_back({node.attribute("valueReference").as_uint(), + node.attribute("terminal").as_string(), + node.attribute("variableName").as_string()}); + + valid = true; + return true; +} + +bool oms::Terminals::load(const std::string& unzippedFmuPath) +{ + const filesystem::path file = filesystem::path(unzippedFmuPath) / filesystem::path(path()); + if (!filesystem::exists(file)) + return false; + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(file.string().c_str()); + if (!result) + { + logWarning("could not read \"" + file.string() + "\": " + std::string(result.description())); + return false; + } + + // Only the top-level terminals: a nested terminal is not a connector an + // acausal connection is made between. + for (const pugi::xml_node& node : doc.child("fmiTerminalsAndIcons").child("Terminals").children("Terminal")) + { + Terminal terminal; + terminal.name = node.attribute("name").as_string(); + terminal.terminalKind = node.attribute("terminalKind").as_string(); + for (const pugi::xml_node& member : node.children("TerminalMemberVariable")) + terminal.members.push_back({member.attribute("variableName").as_string(), + member.attribute("memberName").as_string(), + member.attribute("variableKind").as_string()}); + for (const pugi::xml_node& member : node.children("TerminalStreamMemberVariable")) + terminal.streamMembers.push_back({member.attribute("inStreamMemberName").as_string(), + member.attribute("outStreamMemberName").as_string(), + member.attribute("inStreamVariableName").as_string(), + member.attribute("outStreamVariableName").as_string()}); + terminals.push_back(terminal); + } + + // The aliases, which a member may be: e.g. b.p of a component with a.p = b.p. + pugi::xml_document md; + const filesystem::path modelDescription = filesystem::path(unzippedFmuPath) / filesystem::path("modelDescription.xml"); + if (md.load_file(modelDescription.string().c_str())) + for (const pugi::xml_node& variable : md.child("fmiModelDescription").child("ModelVariables").children()) + for (const pugi::xml_node& alias : variable.children("Alias")) + aliasOf[alias.attribute("name").as_string()] = variable.attribute("name").as_string(); + return true; +} + +std::string oms::Terminals::resolveAlias(const std::string& variableName) const +{ + const auto it = aliasOf.find(variableName); + return it == aliasOf.end() ? variableName : it->second; +} diff --git a/src/OMSimulatorLib/LsDae.h b/src/OMSimulatorLib/LsDae.h index 309d9d064..40c3efcd1 100644 --- a/src/OMSimulatorLib/LsDae.h +++ b/src/OMSimulatorLib/LsDae.h @@ -37,6 +37,7 @@ #define _OMS_LSDAE_H_ #include +#include #include #include @@ -109,6 +110,93 @@ namespace oms std::vector> residualDependencies; std::vector continuousStateDerivatives; }; + /** + * \brief OpenModelica's extension of fmi-ls-dae for an acausal component: a + * model connected through its terminals after it was compiled. + * + * The component carries one residual per flow member of its terminals that, + * unconnected, reads flow = 0. An importer that connects the terminal drops + * those residuals and adds the equations of its connections instead: + * potentials equal, flows summing to zero. The manifest names them. + */ + class LsDaeAcausalManifest + { + public: + static const char* name() { return "org.openmodelica.fmi-ls-dae-acausal"; } + static const char* path() { return "extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml"; } + + /// A residual that stands for a flow of a terminal (ConnectorResidual), or + /// for inStream of a stream variable of a terminal (StreamResidual). + struct ConnectorResidual + { + uint32_t valueReference; ///< of the residual, one of LsDaeManifest::getResiduals() + std::string terminal; ///< the terminal the flow belongs to + std::string variableName; ///< the flow variable, or the stream variable + }; + + /// \return true if a manifest is there and could be read; a missing file is not an error. + bool load(const std::string& unzippedFmuPath); + + bool isValid() const { return valid; } + const std::string& getVersion() const { return version; } + const std::vector& getConnectorResiduals() const { return connectorResiduals; } + const std::vector& getStreamResiduals() const { return streamResiduals; } + + private: + bool valid = false; + std::string version; + std::vector connectorResiduals; + std::vector streamResiduals; + }; + + /** + * \brief The terminals of an FMI 3.0 FMU (terminalsAndIcons/terminalsAndIcons.xml), + * as far as an acausal connection needs them. + */ + class Terminals + { + public: + static const char* path() { return "terminalsAndIcons/terminalsAndIcons.xml"; } + + struct Member + { + std::string variableName; + std::string memberName; + std::string variableKind; ///< "signal" (a potential), "inflow" or "outflow" (a flow) + bool isFlow() const { return variableKind == "inflow" || variableKind == "outflow"; } + }; + + /// A stream variable and the variable that stands for its inStream + /// (TerminalStreamMemberVariable). + struct StreamMember + { + std::string inStreamMemberName; + std::string outStreamMemberName; + std::string inStreamVariableName; + std::string outStreamVariableName; + }; + + struct Terminal + { + std::string name; + std::string terminalKind; + std::vector members; + std::vector streamMembers; + }; + + /// \return true if the file is there and could be read. + bool load(const std::string& unzippedFmuPath); + + const std::vector& getTerminals() const { return terminals; } + + /// The variable a terminal member names: itself, or the variable it is an + /// FMI 3.0 of, which the FMI library does not list as a variable. + std::string resolveAlias(const std::string& variableName) const; + + private: + std::vector terminals; + std::map aliasOf; + }; } #endif diff --git a/src/OMSimulatorLib/System.cpp b/src/OMSimulatorLib/System.cpp index 1a7a26a20..1ae02c64d 100644 --- a/src/OMSimulatorLib/System.cpp +++ b/src/OMSimulatorLib/System.cpp @@ -776,7 +776,7 @@ oms_status_enu_t oms::System::exportToSSD(pugi::xml_node& node, Snapshot& snapsh for (const auto& connection : connections) { - if (connection && connection->getType() == oms_connection_single) + if (connection && (connection->getType() == oms_connection_single || connection->getType() == oms_connection_acausal)) ssdconnections.push_back(connection); else if (connection) busconnections.push_back(connection); @@ -1203,6 +1203,20 @@ oms_status_enu_t oms::System::addConnection(const oms::ComRef& crefA, const oms: if (getConnection(crefA, crefB)) return logError_ConnectionExistsAlready(crefA, crefB, this); + // An acausal connection: two members of terminals, neither one driving the + // other. A member may be connected any number of times; the system builds + // the connection sets from these and adds their equations itself. + if (conA->getCausality() == oms_causality_bidir || conB->getCausality() == oms_causality_bidir) + { + if (conA->getCausality() != conB->getCausality()) + return logError("Acausal connection between an acausal and a causal connector: " + std::string(crefA) + " -> " + std::string(crefB)); + if (conA->getType() != oms_signal_type_real || conB->getType() != oms_signal_type_real) + return logError("Acausal connection of a non-real connector: " + std::string(crefA) + " -> " + std::string(crefB)); + connections.back() = new oms::Connection(crefA, crefB, suppressUnitConversion, oms_connection_acausal); + connections.push_back(NULL); + return oms_status_ok; + } + auto componentA = getComponent(headA); if(componentA && componentA->getType() == oms_component_table) { diff --git a/src/OMSimulatorLib/SystemSC.cpp b/src/OMSimulatorLib/SystemSC.cpp index 4720250bd..f786590f5 100644 --- a/src/OMSimulatorLib/SystemSC.cpp +++ b/src/OMSimulatorLib/SystemSC.cpp @@ -44,6 +44,7 @@ #include "ssd/Tags.h" #include +#include #include #include #include @@ -256,6 +257,236 @@ static bool componentOwnsNode(const oms::Component* component, const oms::ComRef return false; } +oms_status_enu_t oms::SystemSC::collectAcausalConnections() +{ + acausalRows.clear(); + streamRows.clear(); + + // The members of the terminals of the components, by the name a connection + // of this system gives them ("R.p.v"). + struct Member + { + size_t component; + fmi3ValueReference vr; + bool flow; + double sign; ///< +1 for an inflow, -1 for an outflow, which the flow sum negates + int column; ///< its column if it is an unknown, else -1 + int residualRow; ///< for a flow or a stream, the row of the residual that stands for it, else -1 + std::string terminal; ///< "R.p", to find the rows of its terminal + bool stream = false; ///< a stream variable, connected by its outStream variable + fmi3ValueReference inStreamVr = 0; ///< for a stream, the variable that stands for its inStream + int inStreamColumn = -1; ///< and its column if it is an unknown + }; + std::map members; + std::map> rowsOfTerminal; + std::map> flowsOfTerminal; + + bool anyAcausalComponent = false; + for (size_t i = 0, j = 0; i < fmus.size(); ++i) + { + const size_t base = j; + j += nStates[i] + nAlgebraic[i]; + + ComponentFMU3ME* fmu3 = dynamic_cast(fmus[i]); + if (!fmu3 || !fmu3->isAcausalComponent()) + continue; + anyAcausalComponent = true; + const std::string prefix = std::string(fmus[i]->getCref()) + "."; + + // The unknowns of the component a member may be. + std::map columnOf; + if (daeFmus[i]) + { + const std::vector& stateVrs = daeFmus[i]->getStateValueReferences(); + for (size_t k = 0; k < stateVrs.size() && k < nStates[i]; ++k) + columnOf[stateVrs[k]] = static_cast(base + k); + const std::vector& algVrs = daeFmus[i]->getLsDae().getAlgebraicVariables(); + for (size_t k = 0; k < algVrs.size(); ++k) + columnOf[algVrs[k]] = static_cast(base + nStates[i] + k); + } + + // The row of each residual, and which of them stand for a flow. + std::map rowOfResidual; + const std::vector& residualVrs = fmu3->getLsDae().getResiduals(); + for (size_t k = 0; k < residualVrs.size(); ++k) + rowOfResidual[residualVrs[k]] = static_cast(base + k); + std::map rowOfFlow; + for (const LsDaeAcausalManifest::ConnectorResidual& r : fmu3->getLsDaeAcausal().getConnectorResiduals()) + { + const auto it = rowOfResidual.find(r.valueReference); + if (it == rowOfResidual.end()) + return logError("system \"" + std::string(getFullCref()) + "\": the acausal manifest of \"" + std::string(fmus[i]->getFullCref()) + + "\" names residual " + std::to_string(r.valueReference) + " for \"" + r.variableName + "\", which is not one of its fmi-ls-dae residuals"); + rowOfFlow[r.variableName] = it->second; + rowsOfTerminal[prefix + r.terminal].push_back(it->second); + } + std::map rowOfStream; + for (const LsDaeAcausalManifest::ConnectorResidual& r : fmu3->getLsDaeAcausal().getStreamResiduals()) + { + const auto it = rowOfResidual.find(r.valueReference); + if (it == rowOfResidual.end()) + return logError("system \"" + std::string(getFullCref()) + "\": the acausal manifest of \"" + std::string(fmus[i]->getFullCref()) + + "\" names residual " + std::to_string(r.valueReference) + " for inStream of \"" + r.variableName + "\", which is not one of its fmi-ls-dae residuals"); + rowOfStream[r.variableName] = it->second; + rowsOfTerminal[prefix + r.terminal].push_back(it->second); + } + + for (const Terminals::Terminal& terminal : fmu3->getTerminals().getTerminals()) + { + for (const Terminals::Member& m : terminal.members) + { + const Variable* v = fmu3->getVariable(ComRef(fmu3->getTerminals().resolveAlias(m.variableName))); + if (!v) + continue; + const fmi3ValueReference vr = v->getValueReferenceFMI3(); + const auto column = columnOf.find(vr); + const auto row = rowOfFlow.find(m.variableName); + members[prefix + m.variableName] = {i, vr, m.isFlow(), m.variableKind == "outflow" ? -1.0 : 1.0, + column == columnOf.end() ? -1 : column->second, + row == rowOfFlow.end() ? -1 : row->second, + prefix + terminal.name}; + if (m.isFlow()) + flowsOfTerminal[prefix + terminal.name].push_back(prefix + m.variableName); + } + for (const Terminals::StreamMember& m : terminal.streamMembers) + { + const Variable* out = fmu3->getVariable(ComRef(fmu3->getTerminals().resolveAlias(m.outStreamVariableName))); + const Variable* in = fmu3->getVariable(ComRef(fmu3->getTerminals().resolveAlias(m.inStreamVariableName))); + if (!out || !in) + continue; + const fmi3ValueReference vr = out->getValueReferenceFMI3(); + const fmi3ValueReference inVr = in->getValueReferenceFMI3(); + const auto column = columnOf.find(vr); + const auto inColumn = columnOf.find(inVr); + const auto row = rowOfStream.find(m.outStreamVariableName); + Member member = {i, vr, false, 1.0, column == columnOf.end() ? -1 : column->second, + row == rowOfStream.end() ? -1 : row->second, prefix + terminal.name}; + member.stream = true; + member.inStreamVr = inVr; + member.inStreamColumn = inColumn == columnOf.end() ? -1 : inColumn->second; + members[prefix + m.outStreamVariableName] = member; + } + } + } + + // The connection sets: the members joined by acausal connections. + std::map parent; + std::function find = [&](const std::string& x) -> std::string { + auto it = parent.find(x); + if (it == parent.end() || it->second == x) + return x; + return it->second = find(it->second); + }; + + size_t nAcausalConnections = 0; + for (const Connection* connection : getConnections()) + { + if (!connection || connection->getType() != oms_connection_acausal) + continue; + ++nAcausalConnections; + const std::string a = std::string(connection->getSignalA()); + const std::string b = std::string(connection->getSignalB()); + for (const std::string& name : {a, b}) + { + const auto it = members.find(name); + if (it == members.end()) + return logError("system \"" + std::string(getFullCref()) + "\": \"" + name + "\" is acausally connected but is not a terminal member of an acausal component"); + if (!daeFmus[it->second.component]) + return logError("system \"" + std::string(getFullCref()) + "\": \"" + name + "\" is acausally connected but its component does not run in DAE mode (fmi-ls-dae)"); + if (parent.find(name) == parent.end()) + parent[name] = name; + } + if (members[a].flow != members[b].flow) + return logError("system \"" + std::string(getFullCref()) + "\": acausal connection between a flow and a potential: " + a + " -> " + b); + if (members[a].stream != members[b].stream) + return logError("system \"" + std::string(getFullCref()) + "\": acausal connection between a stream variable and a potential: " + a + " -> " + b); + parent[find(a)] = find(b); + } + + if (0 == nAcausalConnections) + { + if (anyAcausalComponent) + logInfo("system \"" + std::string(getFullCref()) + "\": no acausal connections; the terminals of its acausal components stay unconnected (flow = 0)"); + return oms_status_ok; + } + + std::map> sets; + for (const auto& p : parent) + sets[find(p.first)].push_back(p.first); + + // The rows the connected flows free, and the equations that take them. + std::vector freedRows; + std::vector rows; + auto termOf = [&](const Member& m, double factor) -> AcausalTerm { + return {m.component, m.vr, factor, m.column, rowsOfTerminal[m.terminal]}; + }; + for (const auto& set : sets) + { + const std::vector& names = set.second; + const Member& first = members[names.front()]; + if (first.flow) + { + AcausalRow row{0, {}}; + for (const std::string& name : names) + { + const Member& m = members[name]; + if (m.residualRow < 0) + return logError("system \"" + std::string(getFullCref()) + "\": the flow \"" + name + "\" is connected, but its component's acausal manifest names no residual for it"); + freedRows.push_back(m.residualRow); + row.terms.push_back(termOf(m, m.sign)); + } + rows.push_back(row); + } + else if (first.stream) + { + // Each stream variable gets the mixing rule of the others, in the row of + // the residual that stood for its own inStream. + for (const std::string& name : names) + { + const Member& m = members[name]; + if (m.residualRow < 0) + return logError("system \"" + std::string(getFullCref()) + "\": the stream variable \"" + name + "\" is connected, but its component's acausal manifest names no residual for its inStream"); + StreamRow row{static_cast(m.residualRow), {m.component, m.inStreamVr, 1.0, m.inStreamColumn, rowsOfTerminal[m.terminal]}, {}}; + for (const std::string& other : names) + { + if (other == name) + continue; + const Member& o = members[other]; + const std::vector& flows = flowsOfTerminal[o.terminal]; + if (flows.size() != 1) + return logError("system \"" + std::string(getFullCref()) + "\": the stream variable \"" + other + "\" is connected, but its terminal has " + + std::to_string(flows.size()) + " flow variables; the mixing rule needs exactly one"); + const Member& f = members[flows.front()]; + row.others.push_back({termOf(o, 1.0), termOf(f, f.sign)}); + } + streamRows.push_back(row); + } + } + else + { + for (size_t k = 1; k < names.size(); ++k) + rows.push_back({0, {termOf(first, 1.0), termOf(members[names[k]], -1.0)}}); + } + } + + if (rows.size() != freedRows.size()) + return logError("system \"" + std::string(getFullCref()) + "\": the acausal connections give " + std::to_string(rows.size()) + + " equation(s) for the " + std::to_string(freedRows.size()) + " residual(s) of the connected flows; " + "every connection set of a terminal needs as many potentials as flows"); + + std::sort(freedRows.begin(), freedRows.end()); + for (size_t k = 0; k < rows.size(); ++k) + { + rows[k].row = static_cast(freedRows[k]); + acausalRows.push_back(rows[k]); + } + + logInfo("system \"" + std::string(getFullCref()) + "\": " + std::to_string(nAcausalConnections) + " acausal connection(s) in " + + std::to_string(sets.size()) + " connection set(s) replace " + std::to_string(freedRows.size()) + " connector residual(s)" + + (streamRows.empty() ? std::string() : " and " + std::to_string(streamRows.size()) + " stream residual(s)")); + return oms_status_ok; +} + void oms::SystemSC::buildDaeJacobianSparsity() { daeJacRowsByColumn.clear(); @@ -402,6 +633,44 @@ void oms::SystemSC::buildDaeJacobianSparsity() columnsByRow[row].push_back(static_cast(c)); } + // An acausal connection row reaches the members it sums: each one's own + // column if it is an unknown, and whatever the residuals of its terminal + // reached — those take the flow and the potentials as arguments, so their + // dependencies cover what a member that is computed by the component (an + // alias, a constant) depends on. + // A stream row reaches the same for its inStream variable and for the stream + // and flow variables of the other members. + if (!acausalRows.empty() || !streamRows.empty()) + { + const std::vector> componentColumnsByRow = columnsByRow; + auto addColumns = [&](const AcausalTerm& term, std::vector& columns) { + if (term.column >= 0) + columns.push_back(term.column); + if (term.terminalRows.empty()) + columnsAround(term.component, columns); + for (const int r : term.terminalRows) + columns.insert(columns.end(), componentColumnsByRow[r].begin(), componentColumnsByRow[r].end()); + }; + for (const AcausalRow& acausal : acausalRows) + { + std::vector& columns = columnsByRow[acausal.row]; + columns.clear(); + for (const AcausalTerm& term : acausal.terms) + addColumns(term, columns); + } + for (const StreamRow& stream : streamRows) + { + std::vector& columns = columnsByRow[stream.row]; + columns.clear(); + addColumns(stream.inStream, columns); + for (const StreamTerm& other : stream.others) + { + addColumns(other.stream, columns); + addColumns(other.flow, columns); + } + } + } + // Transpose, and drop the duplicates a conservative row may have collected. daeJacRowsByColumn.assign(n, {}); size_t nnz = 0; @@ -682,6 +951,55 @@ int oms::ida_res(sunrealtype t, N_Vector yy, N_Vector yp, N_Vector rr, void* use NV_Ith_S(rr, j) = value - NV_Ith_S(yy, j); } + // The acausal connection rows, in place of the residuals of the connected + // flows: what the terminal members now hold, summed. + for (const SystemSC::AcausalRow& acausal : system->acausalRows) + { + double sum = 0.0; + for (const SystemSC::AcausalTerm& term : acausal.terms) + { + double value = 0.0; + if (oms_status_ok != system->daeFmus[term.component]->getFloat64({term.vr}, &value)) + return -1; + sum += term.factor * value; + } + NV_Ith_S(rr, acausal.row) = sum; + } + + // The stream rows, in place of the residuals of the connected streams' + // inStream: the mixing rule of the other members of the set. + for (const SystemSC::StreamRow& stream : system->streamRows) + { + double inStream = 0.0; + if (oms_status_ok != system->daeFmus[stream.inStream.component]->getFloat64({stream.inStream.vr}, &inStream)) + return -1; + double mixed = 0.0; + if (stream.others.size() == 1) + { + if (oms_status_ok != system->daeFmus[stream.others[0].stream.component]->getFloat64({stream.others[0].stream.vr}, &mixed)) + return -1; + } + else + { + // The regularization of OpenModelica's flowThreshold, so that a set + // without flow out of the others still has a mixing temperature. + const double eps = 1e-7; + double num = 0.0, den = 0.0; + for (const SystemSC::StreamTerm& other : stream.others) + { + double h = 0.0, m = 0.0; + if (oms_status_ok != system->daeFmus[other.stream.component]->getFloat64({other.stream.vr}, &h) || + oms_status_ok != system->daeFmus[other.flow.component]->getFloat64({other.flow.vr}, &m)) + return -1; + const double w = std::max(-other.flow.factor * m, eps); + num += w * h; + den += w; + } + mixed = num / den; + } + NV_Ith_S(rr, stream.row) = inStream - mixed; + } + return 0; } @@ -908,6 +1226,21 @@ oms_status_enu_t oms::SystemSC::initialize() nDaeUnknowns += liftedConnections.size(); } + // Acausal connections are rows of the DAE system, so they need one; and + // without them an acausal component simply keeps flow = 0 at its terminals. + if (daeMode) + { + if (oms_status_ok != collectAcausalConnections()) + return oms_status_error; + } + else + { + for (const Connection* connection : getConnections()) + if (connection && connection->getType() == oms_connection_acausal) + return logError("system "" + std::string(getFullCref()) + "": acausal connection " + std::string(connection->getSignalA()) + " -> " + + std::string(connection->getSignalB()) + " needs its components in DAE mode (fmi-ls-dae)"); + } + if (daeMode && oms_solver_sc_ida != solverMethod) { // DAE mode is not a choice the user makes: a component that declares a DAE @@ -1304,6 +1637,8 @@ oms_status_enu_t oms::SystemSC::terminate() daeMode = false; nDaeUnknowns = 0; liftedConnections.clear(); + acausalRows.clear(); + streamRows.clear(); liftedLoops.clear(); daeJacRowsByColumn.clear(); daeJacColorOfColumn.clear(); @@ -1415,6 +1750,8 @@ oms_status_enu_t oms::SystemSC::reset() daeMode = false; nDaeUnknowns = 0; liftedConnections.clear(); + acausalRows.clear(); + streamRows.clear(); liftedLoops.clear(); daeJacRowsByColumn.clear(); daeJacColorOfColumn.clear(); diff --git a/src/OMSimulatorLib/SystemSC.h b/src/OMSimulatorLib/SystemSC.h index af689da2b..0eaca74b5 100644 --- a/src/OMSimulatorLib/SystemSC.h +++ b/src/OMSimulatorLib/SystemSC.h @@ -122,6 +122,17 @@ namespace oms void buildDaeJacobianSparsity(); /// Distribute the DAE unknowns y (and the state derivatives yp) into the FMUs. oms_status_enu_t setDaePoint(double t, N_Vector yy, N_Vector yp); + /** + * \brief The equations of the acausal connections, in place of the + * residuals that stood for the connected flows and inStreams. + * + * Builds the connection sets from the system's acausal connections and + * fills acausalRows and streamRows. Fails if a set mixes flows, potentials + * and stream variables, if the sets do not give as many equations as there + * are flow residuals to replace, or if a connected stream variable's terminal + * does not have exactly one flow. + */ + oms_status_enu_t collectAcausalConnections(); /// Read the point the FMUs hold back into y and yp, after a solve or an event. oms_status_enu_t getDaePoint(N_Vector yy, N_Vector yp); /// IDACalcIC over the current point, then read the consistent point back. @@ -186,6 +197,51 @@ namespace oms /// updateInputsInternal so the inner solver leaves those alone. std::vector liftedLoops; + /** + * An acausal connection set gives the equations of a node: for potentials + * v_1 = v_2 = ... = v_k, k-1 rows v_1 - v_m = 0, and for flows the single + * row sum(i_m) = 0. They take the rows of the residuals the components' + * acausal manifests name for the connected flows (flow = 0, which is what an + * unconnected terminal means), so the system stays square and the layout of + * the unknowns and the rows is the one above. + */ + struct AcausalTerm + { + size_t component; ///< index into fmus + fmi3ValueReference vr; ///< the terminal member + double factor; ///< its factor in the row + int column; ///< its column if it is an unknown, else -1 + std::vector terminalRows; ///< the rows of the residuals that stood for its terminal's flows + }; + struct AcausalRow + { + size_t row; ///< the replaced residual's row + std::vector terms; + }; + std::vector acausalRows; + + /** + * A stream variable h_i of a connection set takes the row of the residual + * that stood for its inStream (inStream(h_i) = h_i, an unconnected terminal) + * with the mixing rule of the set: + * s_i - sum_{j<>i} w_j*h_j / sum_{j<>i} w_j = 0, w_j = max(-m_j, eps) + * where s_i is the variable that stands for inStream(h_i) and m_j is the flow + * of the terminal of h_j, into its component. With two members it is + * s_i - h_j = 0. + */ + struct StreamTerm + { + AcausalTerm stream; ///< h_j + AcausalTerm flow; ///< m_j, factor -1 for an outflow + }; + struct StreamRow + { + size_t row; ///< the replaced residual's row + AcausalTerm inStream; ///< s_i + std::vector others; + }; + std::vector streamRows; + /// Per unknown, the rows that reach it; empty when the structure is unknown /// and IDA differences the Jacobian itself. std::vector> daeJacRowsByColumn; diff --git a/src/OMSimulatorPython/connection.py b/src/OMSimulatorPython/connection.py index 95b1e92fe..0e6e6cdfa 100644 --- a/src/OMSimulatorPython/connection.py +++ b/src/OMSimulatorPython/connection.py @@ -135,6 +135,11 @@ def importFromNode(node, root): def is_validConnection(source_owner: str, source_kind: Causality, dest_owner: str, dest_kind: Causality) -> bool: """Return True if the connection is allowed according to SSP 2.0 table.""" + # An acausal connection: two connectors of kind "unspecified" of elements, + # neither one driving the other. + if source_kind == Causality.unspecified or dest_kind == Causality.unspecified: + return source_owner == "Element" and dest_owner == "Element" and source_kind == dest_kind + # Allowed causality by owner type allowed_source_by_owner = { "System": {Causality.input, Causality.parameter}, diff --git a/src/OMSimulatorPython/fmu.py b/src/OMSimulatorPython/fmu.py index 16775972a..c049384b4 100644 --- a/src/OMSimulatorPython/fmu.py +++ b/src/OMSimulatorPython/fmu.py @@ -39,7 +39,7 @@ from lxml import etree as ET from OMSimulator.connector import Connector from OMSimulator.unit import Unit -from OMSimulator.variable import Variable, SignalType +from OMSimulator.variable import Variable, SignalType, Causality from OMSimulator.values import Values from OMSimulator import namespace, utils from OMSimulator.capi import Capi, Status @@ -133,6 +133,12 @@ def __init__(self, fmu_path: Union[str, Path], instanceName: str = None): self.maxOutputDerivativeOrder = 0 self._variables = [] + # An acausal component (OpenModelica's fmi-ls-dae-acausal extension): its + # terminals, {name: {memberName: (variableName, variableKind)}}, connected + # acausally by the importer. Empty for any other FMU. + self._acausalTerminals = {} + # FMI 3.0 names, {alias: Variable} + self._aliases = {} self._states = [] self._unitDefinitions = [] self._enumerationDefinitions = [] @@ -294,9 +300,39 @@ def _load_model_description(self): self._parse_typeDefinitions_fmi3(model_description) # Parse ModelVariables self._parse_variables_fmi3(model_description) + # Parse the terminals of an acausal component + self._parse_acausal_terminals(fmu_zip) except ET.XMLSyntaxError as e: raise ValueError(f'Error parsing {model_desc_name}: {e}') + _ACAUSAL_MANIFEST = 'extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml' + _TERMINALS = 'terminalsAndIcons/terminalsAndIcons.xml' + + def _parse_acausal_terminals(self, fmu_zip): + '''The terminals of an acausal component: an FMU with an fmi-ls-dae-acausal + manifest and FMI 3.0 terminals. Their members become connectors of kind + "unspecified", connected acausally.''' + names = fmu_zip.namelist() + if self._ACAUSAL_MANIFEST not in names or self._TERMINALS not in names: + return + with fmu_zip.open(self._TERMINALS) as f: + terminals = ET.fromstring(f.read()) + for terminal in terminals.findall('./Terminals/Terminal'): + members = {} + for member in terminal.findall('./TerminalMemberVariable'): + members[member.get('memberName')] = (member.get('variableName'), member.get('variableKind')) + # A stream member is connected by its stream variable; the variable that + # stands for its inStream is computed by the importer. + for member in terminal.findall('./TerminalStreamMemberVariable'): + members[member.get('outStreamMemberName')] = (member.get('outStreamVariableName'), 'stream') + self._acausalTerminals[terminal.get('name')] = members + + @property + def acausalTerminals(self) -> dict: + '''The terminals of an acausal component, {name: {memberName: (variableName, variableKind)}}; + variableKind is "stream" for the stream variable of a TerminalStreamMemberVariable.''' + return self._acausalTerminals + def _parse_default_experiment(self, model_description): """Extract default experiment settings from modelDescription.xml.""" default_experiment = model_description.find('.//DefaultExperiment') @@ -414,6 +450,10 @@ def _parse_variables_fmi3(self, model_description): self._variables.append(variable) + # The aliases, which an acausal component's terminal member may be + for alias in element.findall("Alias"): + self._aliases[alias.get("name")] = variable + # Raise an error if ModelVariables section is missing if not model_variables: raise Exception('ModelVariables section not found in modelDescription.xml') @@ -458,7 +498,12 @@ def _parse_typeDefinitions_fmi3(self, model_description): def makeConnectors(self): connectors = [] + # The members of an acausal component's terminals that are not inputs or + # outputs are connected acausally, (calculated) parameters among them. + acausal = {variableName for members in self._acausalTerminals.values() for (variableName, _) in members.values()} for var in self.variables: + if (var.isParameter() or var.isCalculatedParameter()) and str(var.name) in acausal: + continue if var.isInput() or var.isOutput() or var.isParameter() or var.isCalculatedParameter(): connector = Connector(var.name, var.causality, var.signal_type) connector.setUnit(var.unit) @@ -468,6 +513,20 @@ def makeConnectors(self): if enum.name == var.declaredType: connector.setEnumerationName(var.declaredType) connectors.append(connector) + for var in self.variables: + if str(var.name) in acausal and not (var.isInput() or var.isOutput()): + connector = Connector(var.name, Causality.unspecified, var.signal_type) + connector.setUnit(var.unit) + connector.description = var.description + connectors.append(connector) + # a member that is an alias of another variable, e.g. b.p with a.p = b.p + for name in sorted(acausal): + var = self._aliases.get(name) + if var is not None: + connector = Connector(name, Causality.unspecified, var.signal_type) + connector.setUnit(var.unit) + connector.description = var.description + connectors.append(connector) return connectors def getVariableByName(self, name: str) -> Variable | None: diff --git a/src/OMSimulatorPython/system.py b/src/OMSimulatorPython/system.py index 71bc52b1e..512ccba26 100644 --- a/src/OMSimulatorPython/system.py +++ b/src/OMSimulatorPython/system.py @@ -718,14 +718,42 @@ def _findConnector(self, element_name, connector_name): raise ValueError(f"Connector '{connector_name}' not found in element '{element_name or self.name}'") + def _acausalTerminal(self, element_name, connector_name): + """The members of a terminal of an acausal component, or None.""" + if not element_name: + return None + element = self.elements.get(CRef(element_name)) + fmu = getattr(element, 'fmu', None) + if fmu is None: + return None + return fmu.acausalTerminals.get(str(connector_name)) + def addConnection(self, startElement : str, startConnector : str, endElement : str, endConnector : str, suppressUnitConversion : bool = False): """Adds a connection to the system.""" + # Two terminals of acausal components: connect them member by member. The + # SSD holds the member connections, which is all SSP has a notion of. + startTerminal = self._acausalTerminal(startElement, startConnector) + endTerminal = self._acausalTerminal(endElement, endConnector) + if startTerminal is not None and endTerminal is not None: + if set(startTerminal.keys()) != set(endTerminal.keys()): + raise ValueError(f"Terminals '{startElement}.{startConnector}' and '{endElement}.{endConnector}' have different members: {sorted(startTerminal.keys())} and {sorted(endTerminal.keys())}") + for memberName, (startVariable, startKind) in startTerminal.items(): + (endVariable, endKind) = endTerminal[memberName] + if startKind != endKind: + raise ValueError(f"Member '{memberName}' is a {startKind} of '{startElement}.{startConnector}' but a {endKind} of '{endElement}.{endConnector}'") + self.addConnection(startElement, startVariable, endElement, endVariable, suppressUnitConversion) + return # Resolve source connector owner and causality (source_owner, source_kind) = self._findConnector(startElement, startConnector) # Resolve destination connector owner and causality (dest_owner, dest_kind) = self._findConnector(endElement, endConnector) - if Connection.is_validConnection(source_owner, source_kind, dest_owner, dest_kind): + if source_kind == Causality.unspecified and dest_kind == Causality.unspecified: + # an acausal connector may be connected any number of times + if not Connection.is_validConnection(source_owner, source_kind, dest_owner, dest_kind): + raise ValueError(f"info: Invalid acausal connection '{startElement}.{startConnector}'->'{endElement}.{endConnector}': both must be terminal members of components") + self.connections.append(Connection(startElement, startConnector, endElement, endConnector, suppressUnitConversion)) + elif Connection.is_validConnection(source_owner, source_kind, dest_owner, dest_kind): self.isConnectorAlreadyConnected(startElement, startConnector, endElement, endConnector) self.connections.append(Connection(startElement, startConnector, endElement, endConnector, suppressUnitConversion)) # flipped connection diff --git a/src/OMSimulatorPython/variable.py b/src/OMSimulatorPython/variable.py index 3111522dd..af3a491e6 100644 --- a/src/OMSimulatorPython/variable.py +++ b/src/OMSimulatorPython/variable.py @@ -49,6 +49,10 @@ class Causality(Enum): # states, of variables, of an array's dimensions) and can only be set in # Configuration Mode. fmi-ls-dae's DAE-mode switch is one. structuralParameter = 6 + # SSP 2.0: a connector whose direction is given by the connections, as for an + # acausal connection of Modelica models: the members of an acausal + # component's terminals. + unspecified = 7 class SignalType(Enum): diff --git a/testsuite/tests/resources/AcausalGround/binaries/x86_64-linux/AcausalGround.so b/testsuite/tests/resources/AcausalGround/binaries/x86_64-linux/AcausalGround.so new file mode 100755 index 000000000..78087c8be Binary files /dev/null and b/testsuite/tests/resources/AcausalGround/binaries/x86_64-linux/AcausalGround.so differ diff --git a/testsuite/tests/resources/AcausalGround/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalGround/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml new file mode 100644 index 000000000..2dbdbcceb --- /dev/null +++ b/testsuite/tests/resources/AcausalGround/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalGround/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalGround/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml new file mode 100644 index 000000000..3934f203e --- /dev/null +++ b/testsuite/tests/resources/AcausalGround/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalGround/modelDescription.xml b/testsuite/tests/resources/AcausalGround/modelDescription.xml new file mode 100644 index 000000000..3fef73d6e --- /dev/null +++ b/testsuite/tests/resources/AcausalGround/modelDescription.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalGround/terminalsAndIcons/terminalsAndIcons.xml b/testsuite/tests/resources/AcausalGround/terminalsAndIcons/terminalsAndIcons.xml new file mode 100644 index 000000000..e911b51f8 --- /dev/null +++ b/testsuite/tests/resources/AcausalGround/terminalsAndIcons/terminalsAndIcons.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalRC/binaries/x86_64-linux/AcausalRC.so b/testsuite/tests/resources/AcausalRC/binaries/x86_64-linux/AcausalRC.so new file mode 100755 index 000000000..f5c058e74 Binary files /dev/null and b/testsuite/tests/resources/AcausalRC/binaries/x86_64-linux/AcausalRC.so differ diff --git a/testsuite/tests/resources/AcausalRC/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalRC/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml new file mode 100644 index 000000000..669c81737 --- /dev/null +++ b/testsuite/tests/resources/AcausalRC/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalRC/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalRC/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml new file mode 100644 index 000000000..530a82add --- /dev/null +++ b/testsuite/tests/resources/AcausalRC/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalRC/modelDescription.xml b/testsuite/tests/resources/AcausalRC/modelDescription.xml new file mode 100644 index 000000000..11ccadd35 --- /dev/null +++ b/testsuite/tests/resources/AcausalRC/modelDescription.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalRC/terminalsAndIcons/terminalsAndIcons.xml b/testsuite/tests/resources/AcausalRC/terminalsAndIcons/terminalsAndIcons.xml new file mode 100644 index 000000000..e911b51f8 --- /dev/null +++ b/testsuite/tests/resources/AcausalRC/terminalsAndIcons/terminalsAndIcons.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalSource/binaries/x86_64-linux/AcausalSource.so b/testsuite/tests/resources/AcausalSource/binaries/x86_64-linux/AcausalSource.so new file mode 100755 index 000000000..20d038fec Binary files /dev/null and b/testsuite/tests/resources/AcausalSource/binaries/x86_64-linux/AcausalSource.so differ diff --git a/testsuite/tests/resources/AcausalSource/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalSource/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml new file mode 100644 index 000000000..5cd227171 --- /dev/null +++ b/testsuite/tests/resources/AcausalSource/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalSource/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalSource/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml new file mode 100644 index 000000000..69077b9d2 --- /dev/null +++ b/testsuite/tests/resources/AcausalSource/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalSource/modelDescription.xml b/testsuite/tests/resources/AcausalSource/modelDescription.xml new file mode 100644 index 000000000..aacde6ce4 --- /dev/null +++ b/testsuite/tests/resources/AcausalSource/modelDescription.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalSource/terminalsAndIcons/terminalsAndIcons.xml b/testsuite/tests/resources/AcausalSource/terminalsAndIcons/terminalsAndIcons.xml new file mode 100644 index 000000000..cf717848a --- /dev/null +++ b/testsuite/tests/resources/AcausalSource/terminalsAndIcons/terminalsAndIcons.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamBoundary/binaries/x86_64-linux/AcausalStreamBoundary.so b/testsuite/tests/resources/AcausalStreamBoundary/binaries/x86_64-linux/AcausalStreamBoundary.so new file mode 100755 index 000000000..b6e51dde7 Binary files /dev/null and b/testsuite/tests/resources/AcausalStreamBoundary/binaries/x86_64-linux/AcausalStreamBoundary.so differ diff --git a/testsuite/tests/resources/AcausalStreamBoundary/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalStreamBoundary/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml new file mode 100644 index 000000000..89b83f323 --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamBoundary/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamBoundary/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalStreamBoundary/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml new file mode 100644 index 000000000..4151a599b --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamBoundary/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamBoundary/modelDescription.xml b/testsuite/tests/resources/AcausalStreamBoundary/modelDescription.xml new file mode 100644 index 000000000..8d04cb395 --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamBoundary/modelDescription.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamBoundary/terminalsAndIcons/terminalsAndIcons.xml b/testsuite/tests/resources/AcausalStreamBoundary/terminalsAndIcons/terminalsAndIcons.xml new file mode 100644 index 000000000..0fb2d2810 --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamBoundary/terminalsAndIcons/terminalsAndIcons.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamVolume/binaries/x86_64-linux/AcausalStreamVolume.so b/testsuite/tests/resources/AcausalStreamVolume/binaries/x86_64-linux/AcausalStreamVolume.so new file mode 100755 index 000000000..110b17950 Binary files /dev/null and b/testsuite/tests/resources/AcausalStreamVolume/binaries/x86_64-linux/AcausalStreamVolume.so differ diff --git a/testsuite/tests/resources/AcausalStreamVolume/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalStreamVolume/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml new file mode 100644 index 000000000..da501a408 --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamVolume/extra/org.fmi-standard.fmi-ls-dae/fmi-ls-manifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamVolume/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml b/testsuite/tests/resources/AcausalStreamVolume/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml new file mode 100644 index 000000000..a480272e8 --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamVolume/extra/org.openmodelica.fmi-ls-dae-acausal/fmi-ls-manifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamVolume/modelDescription.xml b/testsuite/tests/resources/AcausalStreamVolume/modelDescription.xml new file mode 100644 index 000000000..caaff14c6 --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamVolume/modelDescription.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/AcausalStreamVolume/terminalsAndIcons/terminalsAndIcons.xml b/testsuite/tests/resources/AcausalStreamVolume/terminalsAndIcons/terminalsAndIcons.xml new file mode 100644 index 000000000..81bd568ac --- /dev/null +++ b/testsuite/tests/resources/AcausalStreamVolume/terminalsAndIcons/terminalsAndIcons.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/tests/resources/CMakeLists.txt b/testsuite/tests/resources/CMakeLists.txt index 731afdf25..4c62301f9 100644 --- a/testsuite/tests/resources/CMakeLists.txt +++ b/testsuite/tests/resources/CMakeLists.txt @@ -1,6 +1,11 @@ set(OM_OMS_TESTSUITE_RESOURCES_FMUS ${CMAKE_CURRENT_SOURCE_DIR}/A + ${CMAKE_CURRENT_SOURCE_DIR}/AcausalGround + ${CMAKE_CURRENT_SOURCE_DIR}/AcausalRC + ${CMAKE_CURRENT_SOURCE_DIR}/AcausalSource + ${CMAKE_CURRENT_SOURCE_DIR}/AcausalStreamBoundary + ${CMAKE_CURRENT_SOURCE_DIR}/AcausalStreamVolume ${CMAKE_CURRENT_SOURCE_DIR}/AircraftVehicleDemonstrator.Atmosphere_Model_FMU_sf ${CMAKE_CURRENT_SOURCE_DIR}/AircraftVehicleDemonstrator.bC ${CMAKE_CURRENT_SOURCE_DIR}/AircraftVehicleDemonstrator.cockpit diff --git a/testsuite/tests/simulation/CMakeLists.txt b/testsuite/tests/simulation/CMakeLists.txt index ae5bab293..54185ea62 100644 --- a/testsuite/tests/simulation/CMakeLists.txt +++ b/testsuite/tests/simulation/CMakeLists.txt @@ -2,6 +2,9 @@ oms_add_test( clockedStepBoundary.py DaeMode1.py DaeMode2.py + DaeMode3.py + DaeMode4.py + DaeMode5.py DualMassOscillator1.py DualMassOscillator2.py exportJson1.py diff --git a/testsuite/tests/simulation/DaeMode3.py b/testsuite/tests/simulation/DaeMode3.py new file mode 100644 index 000000000..54253436c --- /dev/null +++ b/testsuite/tests/simulation/DaeMode3.py @@ -0,0 +1,139 @@ +## status: correct +## linux: yes +## ucrt64: no +## win: no +## mac: no +## asan: no + +from OMSimulator import SSP, CRef, Settings + +Settings.suppressPath = True + +# Acausal components connected through their terminals: an ideal voltage source, +# an RC branch and a ground, each a separate FMU, wired as an electrical circuit +# after they were compiled. The FMUs carry OpenModelica's fmi-ls-dae-acausal +# manifest: per terminal flow, the residual that reads flow = 0 while the +# terminal is unconnected. The system replaces those with the equations of the +# connection sets (potentials equal, flows summing to zero) and integrates the +# whole circuit with IDA. +# +# The FMUs are exported by OpenModelica with +# --simCodeTarget=C+Rust --daeMode --fmuAcausalComponent, so they only carry +# x86_64-linux binaries: +# +# package Circuit +# connector Pin +# Real v; +# flow Real i; +# end Pin; +# model RC "R in series with C to ground" +# parameter Real R = 1, C = 1; +# Pin p; +# Real vc(start = 0, fixed = true); +# equation +# p.v - vc = R*p.i; +# C*der(vc) = p.i; +# end RC; +# model Source "ideal voltage source" +# parameter Real V = 1; +# Pin p, n; +# equation +# p.v - n.v = V; +# p.i + n.i = 0; +# end Source; +# model Ground +# Pin p; +# equation +# p.v = 0; +# end Ground; +# end Circuit; +# +# Connected: V.p -- RC.p, V.n -- G.p. Then vc(t) = 1 - exp(-t), vc(1) = 0.632121, +# and the current into RC.p is exp(-t) = 0.367879 at t = 1. +model = SSP() +model.addResource('../resources/AcausalSource.fmu', new_name='resources/AcausalSource.fmu') +model.addResource('../resources/AcausalRC.fmu', new_name='resources/AcausalRC.fmu') +model.addResource('../resources/AcausalGround.fmu', new_name='resources/AcausalGround.fmu') + +model.addComponent(CRef('default', 'V'), 'resources/AcausalSource.fmu') +model.addComponent(CRef('default', 'RC'), 'resources/AcausalRC.fmu') +model.addComponent(CRef('default', 'G'), 'resources/AcausalGround.fmu') + +# terminal to terminal: expanded into one connection per member +model.addConnection(CRef('default', 'V', 'p'), CRef('default', 'RC', 'p')) +model.addConnection(CRef('default', 'V', 'n'), CRef('default', 'G', 'p')) + +model.export('DaeMode3.ssp') + +model2 = SSP('DaeMode3.ssp') +model2.list() + +instantiated_model = model2.instantiate() +instantiated_model.setResultFile('DaeMode3_res.mat') +instantiated_model.initialize() +instantiated_model.simulate() +for name in [('RC', 'vc'), ('RC', 'p.i'), ('RC', 'p.v'), ('V', 'p.i'), ('V', 'n.i'), ('V', 'n.v'), ('G', 'p.i')]: + print(f"info: {'.'.join(name)} = {instantiated_model.getValue(CRef('default', *name)):.6f}", flush=True) +instantiated_model.terminate() +instantiated_model.delete() +## Result: +## info: FMU "V" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 0 states and 3 algebraic variables +## info: FMU "V" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s) +## info: FMU "RC" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 1 states and 2 algebraic variables +## info: FMU "RC" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## info: FMU "G" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 1 residuals over 0 states and 1 algebraic variables +## info: FMU "G" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## info: system "model.root": 4 acausal connection(s) in 4 connection set(s) replace 4 connector residual(s) +## info: system "model.root" contains a component in DAE mode (fmi-ls-dae); integrating with IDA instead of cvode +## info: system "model.root": DAE Jacobian 7x7, 21 structural nonzeros, 5 colour(s) — sparse (KLU), one residual evaluation per colour instead of one per unknown +## info: Result file: DaeMode3_res.mat (bufferSize=10) +## +## |-- Resources: +## |-- resources/AcausalGround.fmu +## |-- resources/AcausalRC.fmu +## |-- resources/AcausalSource.fmu +## |-- Active Variant: default +## |-- +## |-- Variant "default": +## |-- |-- System: default 'None' +## |-- |-- |-- Connectors: +## |-- |-- |-- Elements: +## |-- |-- |-- |-- FMU: V 'None' +## |-- |-- |-- |-- |-- path: resources/AcausalSource.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (V, Causality.parameter, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (n.v, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (p.i, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (p.v, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (n.i, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- FMU: RC 'None' +## |-- |-- |-- |-- |-- path: resources/AcausalRC.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (C, Causality.parameter, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (R, Causality.parameter, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (p.i, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (p.v, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- FMU: G 'None' +## |-- |-- |-- |-- |-- path: resources/AcausalGround.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (p.i, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- |-- |-- |-- (p.v, Causality.unspecified, SignalType.Float64, None, 'None') +## |-- |-- |-- Connections: +## |-- |-- |-- |-- V.p.i -> RC.p.i +## |-- |-- |-- |-- V.p.v -> RC.p.v +## |-- |-- |-- |-- V.n.v -> G.p.v +## |-- |-- |-- |-- V.n.i -> G.p.i +## |-- DefaultExperiment +## |-- |-- startTime: 0.0 +## |-- |-- stopTime: 1.0 +## info: RC.vc = 0.632121 +## info: RC.p.i = 0.367879 +## info: RC.p.v = 1.000000 +## info: V.p.i = -0.367879 +## info: V.n.i = 0.367879 +## info: V.n.v = 0.000000 +## info: G.p.i = -0.367879 +## info: Final Statistics for 'model.root': +## NumSteps = 1010 NumResEvals = 1014 NumLinSolvSetups = 12 +## NumNonlinSolvIters = 1012 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 +## endResult diff --git a/testsuite/tests/simulation/DaeMode4.py b/testsuite/tests/simulation/DaeMode4.py new file mode 100644 index 000000000..97b10dbdd --- /dev/null +++ b/testsuite/tests/simulation/DaeMode4.py @@ -0,0 +1,111 @@ +## status: correct +## linux: yes +## ucrt64: no +## win: no +## mac: no +## asan: no + +from OMSimulator import SSP, CRef, Settings + +Settings.suppressPath = True + +# Acausal components (see DaeMode3.py for the models), three cases: +# 1. two RC branches in parallel on one source: a node of three terminals, whose +# potentials give two equations and whose flows one, in place of the three +# residuals that stood for the connected flows; +# 2. an RC branch alone: its terminal stays unconnected, so p.i = 0; +# 3. a flow connected to a potential, which is refused. +# +# 1: vc1(t) = 1 - exp(-t), vc2(t) = 1 - exp(-t/2) with R2 = 2, and the source +# delivers exp(-t) + exp(-t/2)/2, i.e. V.p.i = -0.671145 at t = 1. +def run(model, name, values): + model.export(f'{name}.ssp') + instantiated_model = SSP(f'{name}.ssp').instantiate() + for (cref, value) in values: + instantiated_model.setValue(cref, value) + instantiated_model.setResultFile(f'{name}_res.mat') + instantiated_model.initialize() + instantiated_model.simulate() + return instantiated_model + +def show(instantiated_model, names): + for name in names: + print(f"info: {'.'.join(name)} = {instantiated_model.getValue(CRef('default', *name)):.6f}", flush=True) + instantiated_model.terminate() + instantiated_model.delete() + +# 1. a node of three terminals +model = SSP() +model.addResource('../resources/AcausalSource.fmu', new_name='resources/AcausalSource.fmu') +model.addResource('../resources/AcausalRC.fmu', new_name='resources/AcausalRC.fmu') +model.addResource('../resources/AcausalGround.fmu', new_name='resources/AcausalGround.fmu') +model.addComponent(CRef('default', 'V'), 'resources/AcausalSource.fmu') +model.addComponent(CRef('default', 'RC1'), 'resources/AcausalRC.fmu') +model.addComponent(CRef('default', 'RC2'), 'resources/AcausalRC.fmu') +model.addComponent(CRef('default', 'G'), 'resources/AcausalGround.fmu') +model.addConnection(CRef('default', 'V', 'p'), CRef('default', 'RC1', 'p')) +model.addConnection(CRef('default', 'V', 'p'), CRef('default', 'RC2', 'p')) +model.addConnection(CRef('default', 'V', 'n'), CRef('default', 'G', 'p')) +show(run(model, 'DaeMode4a', [(CRef('default', 'RC2', 'R'), 2.0)]), + [('RC1', 'vc'), ('RC2', 'vc'), ('RC1', 'p.i'), ('RC2', 'p.i'), ('V', 'p.i'), ('G', 'p.i')]) + +# 2. unconnected +model = SSP() +model.addResource('../resources/AcausalRC.fmu', new_name='resources/AcausalRC.fmu') +model.addComponent(CRef('default', 'RC'), 'resources/AcausalRC.fmu') +show(run(model, 'DaeMode4b', []), [('RC', 'vc'), ('RC', 'p.i')]) + +# 3. a flow connected to a potential +model = SSP() +model.addResource('../resources/AcausalSource.fmu', new_name='resources/AcausalSource.fmu') +model.addResource('../resources/AcausalRC.fmu', new_name='resources/AcausalRC.fmu') +model.addComponent(CRef('default', 'V'), 'resources/AcausalSource.fmu') +model.addComponent(CRef('default', 'RC'), 'resources/AcausalRC.fmu') +model.addConnection(CRef('default', 'V', 'p.i'), CRef('default', 'RC', 'p.v')) +try: + run(model, 'DaeMode4c', []) +except Exception as e: + print(f"info: refused: {e}", flush=True) +## Result: +## info: FMU "V" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 0 states and 3 algebraic variables +## info: FMU "V" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s) +## info: FMU "RC1" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 1 states and 2 algebraic variables +## info: FMU "RC1" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## info: FMU "RC2" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 1 states and 2 algebraic variables +## info: FMU "RC2" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## info: FMU "G" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 1 residuals over 0 states and 1 algebraic variables +## info: FMU "G" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## info: system "model.root": 6 acausal connection(s) in 4 connection set(s) replace 5 connector residual(s) +## info: system "model.root" contains a component in DAE mode (fmi-ls-dae); integrating with IDA instead of cvode +## info: system "model.root": DAE Jacobian 10x10, 32 structural nonzeros, 7 colour(s) — sparse (KLU), one residual evaluation per colour instead of one per unknown +## info: Result file: DaeMode4a_res.mat (bufferSize=10) +## info: RC1.vc = 0.632121 +## info: RC2.vc = 0.393469 +## info: RC1.p.i = 0.367879 +## info: RC2.p.i = 0.303265 +## info: V.p.i = -0.671145 +## info: G.p.i = -0.671145 +## info: Final Statistics for 'model.root': +## NumSteps = 1010 NumResEvals = 1014 NumLinSolvSetups = 12 +## NumNonlinSolvIters = 1012 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 +## info: FMU "RC" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 1 states and 2 algebraic variables +## info: FMU "RC" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## info: system "model.root": no acausal connections; the terminals of its acausal components stay unconnected (flow = 0) +## info: system "model.root" contains a component in DAE mode (fmi-ls-dae); integrating with IDA instead of cvode +## info: system "model.root": DAE Jacobian 3x3, 7 structural nonzeros, 3 colour(s) — sparse (KLU), one residual evaluation per colour instead of one per unknown +## info: Result file: DaeMode4b_res.mat (bufferSize=10) +## info: RC.vc = 0.000000 +## info: RC.p.i = 0.000000 +## info: Final Statistics for 'model.root': +## NumSteps = 1010 NumResEvals = 1012 NumLinSolvSetups = 12 +## NumNonlinSolvIters = 1010 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 +## info: FMU "V" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 0 states and 3 algebraic variables +## info: FMU "V" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s) +## info: FMU "RC" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 3 residuals over 1 states and 2 algebraic variables +## info: FMU "RC" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s) +## error: [collectAcausalConnections] system "model.root": acausal connection between a flow and a potential: V.p.i -> RC.p.v +## error: [initialize] Initialization of system "model.root" failed +## info: refused: Failed to initialize model: Status.error +## info: 0 warnings +## info: 2 errors +## endResult diff --git a/testsuite/tests/simulation/DaeMode5.py b/testsuite/tests/simulation/DaeMode5.py new file mode 100644 index 000000000..cb27827b0 --- /dev/null +++ b/testsuite/tests/simulation/DaeMode5.py @@ -0,0 +1,152 @@ +## status: correct +## linux: yes +## ucrt64: no +## win: no +## mac: no +## asan: no + +from OMSimulator import SSP, CRef, Settings + +Settings.suppressPath = True + +# Acausal components with stream connectors, each a separate FMU, connected +# through their terminals. Besides the residuals that stand for the flows, the +# fmi-ls-dae-acausal manifest names, per stream variable h_outflow of a terminal, +# the residual that reads inStream(h_outflow) = h_outflow while the terminal is +# unconnected; the terminal's TerminalStreamMemberVariable names the variable +# 'inStream(port.h_outflow)' that stands for inStream. The system replaces that +# residual by the mixing rule of the connection set: +# inStream(h_i) = sum_{j<>i} max(-m_j, eps)*h_j / sum_{j<>i} max(-m_j, eps) +# which is h_j for a set of two. +# +# The FMUs are exported by OpenModelica with +# --simCodeTarget=C+Rust --daeMode --fmuAcausalComponent, so they only carry +# x86_64-linux binaries: +# +# package Fluid +# connector Port +# Real p; +# flow Real m_flow; +# stream Real h_outflow; +# end Port; +# model Boundary "a fixed pressure and specific enthalpy" +# parameter Real p = 1, h = 0; +# Port port; +# Real hIn = inStream(port.h_outflow); +# equation +# port.p = p; +# port.h_outflow = h; +# end Boundary; +# model Volume "an incompressible mixing volume behind a linear flow resistance" +# parameter Real m = 1, R = 1; +# Port a, b; +# Real h(start = 1, fixed = true); +# equation +# a.m_flow + b.m_flow = 0; +# a.p - b.p = R*a.m_flow; +# m*der(h) = a.m_flow*actualStream(a.h_outflow) + b.m_flow*actualStream(b.h_outflow); +# a.h_outflow = h; +# b.h_outflow = h; +# end Volume; +# end Fluid; +# +# Two cases: +# 1. S (p = 2, h = 2) -> V -> K (p = 1): 1 kg/s, der(h) = 2 - h, so +# h(1) = 2 - exp(-1) = 1.632121, and K sees inStream = h; +# 2. S1 (p = 2, h = 2) -> V1 and S2 (p = 2, h = 4) -> V2, both into V3 -> K +# (p = 1): a set of three terminals V1.b, V2.b, V3.a at p = 5/3, with 1/3 kg/s +# through V1 and V2 and 2/3 kg/s through V3. V3 gets the mix +# (h1 + h2)/2 = 3 - 2*exp(-t/3), so h3(1) = 3 - 4*exp(-1/3) + 2*exp(-2/3) +# = 1.160709; V1.b sees the mix of the others, where only V2 flows into the +# node: h2(1) = 4 - 3*exp(-1/3) = 1.850406. +def run(model, name, values): + model.export(f'{name}.ssp') + instantiated_model = SSP(f'{name}.ssp').instantiate() + for (cref, value) in values: + instantiated_model.setValue(cref, value) + instantiated_model.setResultFile(f'{name}_res.mat') + instantiated_model.initialize() + instantiated_model.simulate() + return instantiated_model + +def show(instantiated_model, names): + for name in names: + print(f"info: {'.'.join(name)} = {instantiated_model.getValue(CRef('default', *name)):.6f}", flush=True) + instantiated_model.terminate() + instantiated_model.delete() + +def fluidModel(boundaries, volumes): + model = SSP() + model.addResource('../resources/AcausalStreamBoundary.fmu', new_name='resources/AcausalStreamBoundary.fmu') + model.addResource('../resources/AcausalStreamVolume.fmu', new_name='resources/AcausalStreamVolume.fmu') + for name in boundaries: + model.addComponent(CRef('default', name), 'resources/AcausalStreamBoundary.fmu') + for name in volumes: + model.addComponent(CRef('default', name), 'resources/AcausalStreamVolume.fmu') + return model + +# 1. a source, a volume and a sink +model = fluidModel(['S', 'K'], ['V']) +model.addConnection(CRef('default', 'S', 'port'), CRef('default', 'V', 'a')) +model.addConnection(CRef('default', 'V', 'b'), CRef('default', 'K', 'port')) +show(run(model, 'DaeMode5a', [(CRef('default', 'S', 'p'), 2.0), (CRef('default', 'S', 'h'), 2.0)]), + [('V', 'h'), ('V', 'a.m_flow'), ('K', 'hIn'), ('S', 'hIn'), ('V', "'inStream(a.h_outflow)'")]) + +# 2. two branches mixing in a node of three terminals +model = fluidModel(['S1', 'S2', 'K'], ['V1', 'V2', 'V3']) +model.addConnection(CRef('default', 'S1', 'port'), CRef('default', 'V1', 'a')) +model.addConnection(CRef('default', 'S2', 'port'), CRef('default', 'V2', 'a')) +model.addConnection(CRef('default', 'V1', 'b'), CRef('default', 'V3', 'a')) +model.addConnection(CRef('default', 'V2', 'b'), CRef('default', 'V3', 'a')) +model.addConnection(CRef('default', 'V3', 'b'), CRef('default', 'K', 'port')) +show(run(model, 'DaeMode5b', [(CRef('default', 'S1', 'p'), 2.0), (CRef('default', 'S1', 'h'), 2.0), + (CRef('default', 'S2', 'p'), 2.0), (CRef('default', 'S2', 'h'), 4.0)]), + [('V1', 'h'), ('V2', 'h'), ('V3', 'h'), ('V3', 'a.m_flow'), ('V3', 'a.p'), + ('V3', "'inStream(a.h_outflow)'"), ('V1', "'inStream(b.h_outflow)'"), ('K', 'hIn')]) +## Result: +## info: FMU "S" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 2 residuals over 0 states and 2 algebraic variables +## info: FMU "S" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s), 1 stream residual(s) +## info: FMU "K" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 2 residuals over 0 states and 2 algebraic variables +## info: FMU "K" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s), 1 stream residual(s) +## info: FMU "V" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 6 residuals over 1 states and 5 algebraic variables +## info: FMU "V" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s), 2 stream residual(s) +## info: system "model.root": 6 acausal connection(s) in 6 connection set(s) replace 4 connector residual(s) and 4 stream residual(s) +## info: system "model.root" contains a component in DAE mode (fmi-ls-dae); integrating with IDA instead of cvode +## info: system "model.root": DAE Jacobian 10x10, 55 structural nonzeros, 6 colour(s) — sparse (KLU), one residual evaluation per colour instead of one per unknown +## info: Result file: DaeMode5a_res.mat (bufferSize=10) +## info: V.h = 1.632121 +## info: V.a.m_flow = 1.000000 +## info: K.hIn = 1.632121 +## info: S.hIn = 1.632121 +## info: V.'inStream(a.h_outflow)' = 2.000000 +## info: Final Statistics for 'model.root': +## NumSteps = 1010 NumResEvals = 1015 NumLinSolvSetups = 12 +## NumNonlinSolvIters = 1013 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 +## info: FMU "S1" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 2 residuals over 0 states and 2 algebraic variables +## info: FMU "S1" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s), 1 stream residual(s) +## info: FMU "S2" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 2 residuals over 0 states and 2 algebraic variables +## info: FMU "S2" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s), 1 stream residual(s) +## info: FMU "K" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 2 residuals over 0 states and 2 algebraic variables +## info: FMU "K" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 1 terminal(s), 1 connector residual(s), 1 stream residual(s) +## info: FMU "V1" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 6 residuals over 1 states and 5 algebraic variables +## info: FMU "V1" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s), 2 stream residual(s) +## info: FMU "V2" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 6 residuals over 1 states and 5 algebraic variables +## info: FMU "V2" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s), 2 stream residual(s) +## info: FMU "V3" carries a DAE formulation (fmi-ls-dae 1.0.0-alpha.1): 6 residuals over 1 states and 5 algebraic variables +## info: FMU "V3" is an acausal component (org.openmodelica.fmi-ls-dae-acausal 0.1.0): 2 terminal(s), 2 connector residual(s), 2 stream residual(s) +## info: system "model.root": 15 acausal connection(s) in 12 connection set(s) replace 9 connector residual(s) and 9 stream residual(s) +## info: system "model.root" contains a component in DAE mode (fmi-ls-dae); integrating with IDA instead of cvode +## info: system "model.root": DAE Jacobian 24x24, 157 structural nonzeros, 12 colour(s) — sparse (KLU), one residual evaluation per colour instead of one per unknown +## info: Result file: DaeMode5b_res.mat (bufferSize=10) +## info: V1.h = 1.283469 +## info: V2.h = 1.850406 +## info: V3.h = 1.160709 +## info: V3.a.m_flow = 0.666667 +## info: V3.a.p = 1.666667 +## info: V3.'inStream(a.h_outflow)' = 1.566937 +## info: V1.'inStream(b.h_outflow)' = 1.850406 +## info: K.hIn = 1.160709 +## info: Final Statistics for 'model.root': +## NumSteps = 1010 NumResEvals = 1013 NumLinSolvSetups = 12 +## NumNonlinSolvIters = 1011 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 +## endResult