diff --git a/doc/UsersGuide/source/OMEdit.rst b/doc/UsersGuide/source/OMEdit.rst index 310e7e7db..1f0d2cbe8 100644 --- a/doc/UsersGuide/source/OMEdit.rst +++ b/doc/UsersGuide/source/OMEdit.rst @@ -4,84 +4,87 @@ Graphical Modelling =================== -OMSimulator has an optional dependency to OpenModelica in order to -utilize the graphical modelling editor OMEdit. This feature requires -to install the full OpenModelica tool suite, which includes -OMSimulator. The independent stand-alone version doesn't provide any -graphical modelling editor. +OMSimulator provides a graphical modelling environment through OMEdit, the +OpenModelica Connection Editor. This feature requires a full OpenModelica +installation that includes OMSimulator. -Composite models are imported and exported in the System Structure Description (SSD) format, -which is part of the System Structure and Parameterization (SSP) standard. +Composite models are imported and exported in the System Structure Description +(SSD) format, which is part of the System Structure and Parameterization (SSP) +standard. See also `FMI documentation `_ and `SSP documentation `_. -.. figure :: images/omedit_01.png - :name: oms-omedit-mainwindow-browsers +Architecture +------------ + +OMEdit communicates with OMSimulator through a Python-based ZMQ server: - OMEdit MainWindow and Browsers. +- **OMSimulatorGuiServer.py** — handles all GUI-driven requests (model + management, element queries, solver settings, connections, etc.) +- **OMSimulatorSimulationServer.py** — handles simulation execution and + result streaming + +The GUI server is started automatically when the first SSP model is opened +or created. A notification in the Messages Browser confirms the server is +running, including the script path and ZMQ endpoint. New SSP Model ---------------------- +------------- -A new and empty SSP model can be created from `File->New->SSP` menu item. +A new and empty SSP model can be created from ``File -> New -> SSP`` menu item. .. figure :: images/omedit_02.png OMEdit: New SSP Model -That will open a dialog to enter the names of the model and the root -system and to choose the root systems type. - -There are three types available: - - TLM - Transmission Line Modeling System - - Weakly Coupled - Connected Co-Simulation FMUs System - - Strongly Coupled - Connected Model-Exchange FMUs System +A dialog opens to enter the model name and the name of the root system. .. figure :: images/omedit_03.png OMEdit: New SSP Model Dialog -.. figure :: images/omedit_04.png +.. figure :: images/omedit_01.png OMEdit: Newly created empty root system of SSP model -Add System ----------- +Open SSP Model +-------------- -When a new model is created a root system is always generated. -If you need to have another system in your root system you can -add it with `SSP->Add System`. +An existing SSP file (``.ssp``) can be opened from ``File -> Open Model/Library``. +If a model with the same name is already loaded, an error is reported and the +file is not loaded again. -For example only a weakly coupled system (Co-Simulation) can integrate strongly coupled -system (Model Exchange). Therefore, the weakly coupled system must -be selected from the Libraries Browser and the respective menu item -can be selected: +Add System and Sub-Systems +-------------------------- + +A root system is always created together with the model. Additional subsystems +can be added inside the root system via ``SSP -> Add System``. .. figure :: images/omedit_05.png OMEdit: Add System -That will pop-up a dialog to enter the names of the new system. +A dialog opens to enter the name of the new system. .. figure :: images/omedit_06.png OMEdit: Add System Dialog + Add SubModel ------------ -A sub-model is typically an FMU, but it also can be result file. In -order to import a sub-model, the respective system must be selected -and the action can be selected from the menu bar: +A sub-model is typically an FMU, but it can also be a result file (CSV). To +add a sub-model, select the target system in the Libraries Browser and choose +``SSP -> Add SubModel``. .. figure :: images/omedit_07.png OMEdit: Add SubModel -The file browser will open to select an FMU (.fmu) or result file -(.csv) as a subsmodel. -Then a dialog opens to choose the name of the new sub-model. +A file browser opens to select an FMU (``.fmu``) or result file (``.csv``). +A dialog then opens to set the name for the new sub-model. .. figure :: images/omedit_08.png @@ -91,26 +94,121 @@ Then a dialog opens to choose the name of the new sub-model. OMEdit: Root system with added FMU. +Replace SubModel +---------------- + +An existing sub-model can be replaced with a different FMU by right-clicking +on the component in the diagram view and selecting ``Replace SubModel`` from +the context menu. + +.. figure :: images/omedit_replace_submodel.png + + OMEdit: Replace SubModel Dialog. + +A file browser opens to select the new FMU. The replacement is performed in +two steps: + +1. **Dry run** (``dryRun=true``) — OMEdit first performs a dry run without + making any changes to the model. It checks for interface differences between + the old and new FMU, such as added, removed, or modified connectors and + parameters. If any changes are detected, they are reported in the Messages + Browser and a warning dialog is shown. The model remains unchanged at this + stage, allowing the user to review the impact before deciding to proceed. + +2. **Replacement** (``dryRun=false``) — If the user confirms, the replacement + is carried out and all detected interface changes are applied to the model. + Existing connections to removed or renamed connectors are dropped, and new + connectors are available for connecting. + +.. figure :: images/omedit_replace_submodel_contextmenu.png + + OMEdit: Replace SubModel context menu. + + + +Simulation Setup +---------------- + +Select ``Simulation -> Simulation Setup`` to configure simulation parameters +before running. The dialog has two tabs: **General** and **Solver Settings**. + +The **General** tab covers start time, stop time, and result file settings. + +.. figure :: images/omedit_simulation_setup.png + + OMEdit: Simulation Setup — Solver Settings tab with a solver configuration. + +The **Solver Settings** tab is divided into two parts: + +**Solver Configurations** + +This table lists the named solver configurations available in the model. +Each configuration has a name and a method. Click **Add** to create a new +solver configuration — a new row appears in the table with a default method +of ``oms-ma``. The method can be changed using the combo box in the Method +column. + +.. figure :: images/omedit_simulation_setup_solver_settings.png + + OMEdit: Simulation Setup — Solver Settings tab with a solver configuration. + +To fine-tune a solver's numerical parameters, select the row and click +**Edit**. This opens the Solver Settings dialog: + +.. figure :: images/omedit_simulation_setup_solver_edit.png + + OMEdit: Solver Settings dialog. + +The available fields depend on the solver method: + +- **Fixed-step solvers** (``oms-ma``, ``euler``) — Fixed Step Size is editable; + Initial Step Size, Minimum Step Size, and Maximum Step Size are disabled. +- **Variable-step solvers** (``oms-mav``, ``oms-mav-2``, ``cvode``) — Initial + Step Size, Minimum Step Size, Maximum Step Size, and Relative Tolerance are + editable; Fixed Step Size is disabled. + +Click **Remove** to delete the selected solver configuration. + +**Component Assignments** + +This table lists all FMU components in the model. Each component can be +assigned one of the named solver configurations using the combo box in the +Solver column. The available solvers are filtered by the FMI kind of each +component: + +- **Co-Simulation FMUs** — only co-simulation masters are selectable + (``oms-ma``, ``oms-mav``, ``oms-mav-2``). +- **Model-Exchange FMUs** — only ODE integrators are selectable + (``cvode``, ``euler``). +- **me+cs FMUs** — all solvers are available. + + +.. figure :: images/omedit_component_assignment_solver.png + + OMEdit: Solver Assignments in Components + +Incompatible solvers are shown as disabled in the combo box with a tooltip +explaining why. If no solver is assigned, ``(none)`` is used and the +system default applies. + Simulate -------- -Select the simulate button (symbol with green arrow) or select -`Simulation->Simulate` from the menu in OMEdit to simulate the -SSP model. +Select the simulate button (green arrow) or ``Simulation -> Simulate`` to +run the SSP model. Results are streamed back in real time via the simulation +server. + Dual Mass Oscillator Example ---------------------------- -The dual mass oscillator example from our testsuite is -a simple example one can recreate using components from the -Modelica Standard Library. -After splitting the model into two models and exporting each -as an Model-Exchange and Co-Simulation FMU. +The dual mass oscillator example from the test suite demonstrates a typical +SSP workflow. The model is split into two sub-models, each exported as an FMU, +and then connected in an SSP. .. figure :: images/DualMassOscillator.png - Dual mass oscillator Modelica model (diagramm view) and FMUs - + Dual mass oscillator Modelica model (diagram view) and FMUs .. figure :: images/omedit_10.png diff --git a/doc/UsersGuide/source/OMSimulatorPython3.rst b/doc/UsersGuide/source/OMSimulatorPython3.rst index c590b2ca4..a080f8436 100644 --- a/doc/UsersGuide/source/OMSimulatorPython3.rst +++ b/doc/UsersGuide/source/OMSimulatorPython3.rst @@ -158,6 +158,8 @@ The following example demonstrates how to .. include:: api/oms3/initialize.rst +.. include:: api/oms3/setSolver.rst + .. include:: api/oms3/simulate.rst .. include:: api/oms3/terminate.rst diff --git a/doc/UsersGuide/source/api/oms3/setSolver.rst b/doc/UsersGuide/source/api/oms3/setSolver.rst new file mode 100644 index 000000000..25d2ef4dc --- /dev/null +++ b/doc/UsersGuide/source/api/oms3/setSolver.rst @@ -0,0 +1,134 @@ +Solver +------ + +OMSimulator supports different solver methods depending on the FMI kind of the +components in the system. Solvers are configured by name in the Simulation +Setup dialog and assigned to individual FMU components. + +Available Solvers +~~~~~~~~~~~~~~~~~ + +**Co-Simulation (CS) Solvers** + +Co-simulation solvers act as masters that drive the co-simulation protocol. +They are used for FMUs that implement the co-simulation interface (CS FMUs). + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Method + - Description + * - ``oms_ma`` + - **Master Algorithm** — fixed-step co-simulation master. The step size + is controlled by the ``fixedStepSize`` setting. + * - ``oms_mav`` + - **Master Algorithm with Variable Step** — variable-step co-simulation + master. Uses ``initialStepSize``, ``minimumStepSize``, and + ``maximumStepSize`` to control the step size, and ``relativeTolerance`` + for error control. + * - ``oms_mav2`` + - **Master Algorithm with Variable Step (v2)** — an improved variable-step + co-simulation master with enhanced error estimation. Supports the same + settings as ``oms_mav``. + +**Model-Exchange (ME) Solvers** + +Model-exchange solvers are ODE integrators used for FMUs that implement the +model-exchange interface (ME FMUs). They integrate the continuous-time +equations provided by the FMU. + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Method + - Description + * - ``cvode`` + - **CVODE** — variable-step, variable-order ODE solver from the SUNDIALS + suite. Suitable for stiff and non-stiff systems. Supports + ``initialStepSize``, ``minimumStepSize``, ``maximumStepSize``, and + ``relativeTolerance``. + * - ``euler`` + - **Explicit Euler** — simple fixed-step explicit Euler integrator. + Suitable for non-stiff systems. Controlled by ``fixedStepSize``. + +Solver Settings +~~~~~~~~~~~~~~~ + +The following settings can be configured for each named solver: + +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - Setting + - Applies to + - Description + * - ``relativeTolerance`` + - ``oms_mav``, ``oms_mav2``, ``cvode`` + - Relative error tolerance used for step-size control. + * - ``fixedStepSize`` + - ``oms_ma``, ``euler`` + - The fixed communication / integration step size. + * - ``initialStepSize`` + - ``oms_mav``, ``oms_mav2``, ``cvode`` + - The initial step size at the start of simulation. + * - ``minimumStepSize`` + - ``oms_mav``, ``oms_mav2``, ``cvode`` + - The minimum allowed step size. + * - ``maximumStepSize`` + - ``oms_mav``, ``oms_mav2``, ``cvode`` + - The maximum allowed step size. + +Solver Assignment +~~~~~~~~~~~~~~~~~ + +Each FMU component in a system can be assigned one named solver configuration. +The available solvers are filtered based on the FMI kind of the component: + +- **CS FMUs** — only co-simulation solvers (``oms_ma``, ``oms_mav``, + ``oms_mav2``) are available. +- **ME FMUs** — only model-exchange solvers (``cvode``, ``euler``) are + available. +- **me+cs FMUs** — all solvers are available. + +If no solver is assigned to a component, the system default ``oms_ma`` is applied. + +**Example** + +The following example creates two named solver configurations — one using the +fixed-step Euler method and one using the variable-step CVODE solver — and +assigns them to individual components. + +.. code-block:: python + + from OMSimulator import SSP, CRef, Settings + + Settings.suppressPath = True + + model = SSP() + + # Add FMU resources + model.addResource('../resources/Modelica.Blocks.Math.Add.fmu', new_name='resources/Add.fmu') + model.addResource('../resources/Modelica.Blocks.Math.Gain.fmu', new_name='resources/Gain.fmu') + + # Instantiate components + model.addComponent(CRef('default', 'Add1'), 'resources/Add.fmu') + model.addComponent(CRef('default', 'Add2'), 'resources/Add.fmu') + model.addComponent(CRef('default', 'Gain1'), 'resources/Gain.fmu') + model.addComponent(CRef('default', 'Gain2'), 'resources/Gain.fmu') + + # Define solver configurations + solver1 = {'name': 'solver1', 'method': 'euler', 'fixedStepSize': 1e-3} + model.newSolver(solver1) + + solver2 = {'name': 'solver2', 'method': 'cvode', 'relativeTolerance': 1e-4, + 'initialStepSize': 1e-6, 'minimumStepSize': 1e-12, 'maximumStepSize': 0.001} + model.newSolver(solver2) + + # Assign solvers to components + model.setSolver(CRef('default', 'Add1'), 'solver1') + model.setSolver(CRef('default', 'Add2'), 'solver2') + model.setSolver(CRef('default', 'Gain1'), 'solver1') + model.setSolver(CRef('default', 'Gain2'), 'solver2') diff --git a/doc/UsersGuide/source/images/omedit_01.png b/doc/UsersGuide/source/images/omedit_01.png index bb6efda32..967fedc71 100755 Binary files a/doc/UsersGuide/source/images/omedit_01.png and b/doc/UsersGuide/source/images/omedit_01.png differ diff --git a/doc/UsersGuide/source/images/omedit_02.png b/doc/UsersGuide/source/images/omedit_02.png index bd10534a1..50a69bb9c 100755 Binary files a/doc/UsersGuide/source/images/omedit_02.png and b/doc/UsersGuide/source/images/omedit_02.png differ diff --git a/doc/UsersGuide/source/images/omedit_03.png b/doc/UsersGuide/source/images/omedit_03.png index 8274bcf5f..534fc233e 100755 Binary files a/doc/UsersGuide/source/images/omedit_03.png and b/doc/UsersGuide/source/images/omedit_03.png differ diff --git a/doc/UsersGuide/source/images/omedit_04.png b/doc/UsersGuide/source/images/omedit_04.png index 95cdf9e16..4d7fae70a 100755 Binary files a/doc/UsersGuide/source/images/omedit_04.png and b/doc/UsersGuide/source/images/omedit_04.png differ diff --git a/doc/UsersGuide/source/images/omedit_05.png b/doc/UsersGuide/source/images/omedit_05.png index e0d52ccb1..91405bb69 100755 Binary files a/doc/UsersGuide/source/images/omedit_05.png and b/doc/UsersGuide/source/images/omedit_05.png differ diff --git a/doc/UsersGuide/source/images/omedit_06.png b/doc/UsersGuide/source/images/omedit_06.png index 7c02a3256..b42654d62 100755 Binary files a/doc/UsersGuide/source/images/omedit_06.png and b/doc/UsersGuide/source/images/omedit_06.png differ diff --git a/doc/UsersGuide/source/images/omedit_07.png b/doc/UsersGuide/source/images/omedit_07.png index 692b2ae27..a61946029 100755 Binary files a/doc/UsersGuide/source/images/omedit_07.png and b/doc/UsersGuide/source/images/omedit_07.png differ diff --git a/doc/UsersGuide/source/images/omedit_08.png b/doc/UsersGuide/source/images/omedit_08.png index 53c2821f9..5303e6177 100755 Binary files a/doc/UsersGuide/source/images/omedit_08.png and b/doc/UsersGuide/source/images/omedit_08.png differ diff --git a/doc/UsersGuide/source/images/omedit_09.png b/doc/UsersGuide/source/images/omedit_09.png index a80672147..7ead8aeb5 100755 Binary files a/doc/UsersGuide/source/images/omedit_09.png and b/doc/UsersGuide/source/images/omedit_09.png differ diff --git a/doc/UsersGuide/source/images/omedit_10.png b/doc/UsersGuide/source/images/omedit_10.png index cd6c0e1fd..aca465809 100755 Binary files a/doc/UsersGuide/source/images/omedit_10.png and b/doc/UsersGuide/source/images/omedit_10.png differ diff --git a/doc/UsersGuide/source/images/omedit_component_assignment_solver.png b/doc/UsersGuide/source/images/omedit_component_assignment_solver.png new file mode 100644 index 000000000..4a818afdc Binary files /dev/null and b/doc/UsersGuide/source/images/omedit_component_assignment_solver.png differ diff --git a/doc/UsersGuide/source/images/omedit_replace_submodel.png b/doc/UsersGuide/source/images/omedit_replace_submodel.png new file mode 100644 index 000000000..7814c3624 Binary files /dev/null and b/doc/UsersGuide/source/images/omedit_replace_submodel.png differ diff --git a/doc/UsersGuide/source/images/omedit_replace_submodel_contextmenu.png b/doc/UsersGuide/source/images/omedit_replace_submodel_contextmenu.png new file mode 100644 index 000000000..7921ca123 Binary files /dev/null and b/doc/UsersGuide/source/images/omedit_replace_submodel_contextmenu.png differ diff --git a/doc/UsersGuide/source/images/omedit_simulation_setup.png b/doc/UsersGuide/source/images/omedit_simulation_setup.png new file mode 100644 index 000000000..a8ab1c6d8 Binary files /dev/null and b/doc/UsersGuide/source/images/omedit_simulation_setup.png differ diff --git a/doc/UsersGuide/source/images/omedit_simulation_setup_solver_edit.png b/doc/UsersGuide/source/images/omedit_simulation_setup_solver_edit.png new file mode 100644 index 000000000..3f737e340 Binary files /dev/null and b/doc/UsersGuide/source/images/omedit_simulation_setup_solver_edit.png differ diff --git a/doc/UsersGuide/source/images/omedit_simulation_setup_solver_settings.png b/doc/UsersGuide/source/images/omedit_simulation_setup_solver_settings.png new file mode 100644 index 000000000..894eb67df Binary files /dev/null and b/doc/UsersGuide/source/images/omedit_simulation_setup_solver_settings.png differ diff --git a/src/OMSimulatorPython/__init__.py b/src/OMSimulatorPython/__init__.py index 9f6113262..4cc7bdc32 100644 --- a/src/OMSimulatorPython/__init__.py +++ b/src/OMSimulatorPython/__init__.py @@ -40,6 +40,8 @@ from OMSimulator.connection import Connection from OMSimulator.connector import Connector from OMSimulator.cref import CRef +from OMSimulator.component import Component +from OMSimulator.componenttable import ComponentTable from OMSimulator.fmu import FMU from OMSimulator.settings import Settings from OMSimulator.ssd import SSD @@ -57,6 +59,8 @@ 'Connection', 'Connector', 'CRef', + 'Component', + 'ComponentTable', 'FMU', 'Float32', 'Float64', diff --git a/src/OMSimulatorPython/capi.py b/src/OMSimulatorPython/capi.py index 81432932b..4be89da00 100644 --- a/src/OMSimulatorPython/capi.py +++ b/src/OMSimulatorPython/capi.py @@ -44,33 +44,6 @@ class Status(Enum): fatal = 4 pending = 5 -## C structure for connection geometry to properly pass the data from Python to C API -class ssd_connection_geometry_t(ctypes.Structure): - _fields_ = [ - ("pointsX", ctypes.POINTER(ctypes.c_double)), - ("pointsY", ctypes.POINTER(ctypes.c_double)), - ("n", ctypes.c_uint) - ] -## C structure for connector geometry to properly pass the data from Python to C API -class ssd_connector_geometry_t(ctypes.Structure): - _fields_ = [ - ("x", ctypes.c_double), - ("y", ctypes.c_double) - ] -## C structure for element geometry to properly pass the data from Python to C API -class ssd_element_geometry_t(ctypes.Structure): - _fields_ = [ - ("x1", ctypes.c_double), - ("y1", ctypes.c_double), - ("x2", ctypes.c_double), - ("y2", ctypes.c_double), - ("rotation", ctypes.c_double), - ("iconSource", ctypes.c_char_p), - ("iconRotation", ctypes.c_double), - ("iconFlip", ctypes.c_bool), - ("iconFixedAspectRatio", ctypes.c_bool), - ] - class capi: def __init__(self): dirname = os.path.dirname(__file__) @@ -103,6 +76,8 @@ def __init__(self): self.obj.oms_compareSimulationResults.restype = ctypes.c_int self.obj.oms_delete.argtypes = [ctypes.c_char_p] self.obj.oms_delete.restype = ctypes.c_int + self.obj.oms_doStep.argtypes = [ctypes.c_char_p] + self.obj.oms_doStep.restype = ctypes.c_int self.obj.oms_getVersion.argtypes = None self.obj.oms_getVersion.restype = ctypes.c_char_p self.obj.oms_getBoolean.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_bool)] @@ -111,6 +86,12 @@ def __init__(self): self.obj.oms_getInteger.restype = ctypes.c_int self.obj.oms_getReal.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] self.obj.oms_getReal.restype = ctypes.c_int + self.obj.oms_getTime.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] + self.obj.oms_getTime.restype = ctypes.c_int + self.obj.oms_getStartTime.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] + self.obj.oms_getStartTime.restype = ctypes.c_int + self.obj.oms_getStopTime.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] + self.obj.oms_getStopTime.restype = ctypes.c_int self.obj.oms_getString.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)] self.obj.oms_getString.restype = ctypes.c_int self.obj.oms_getVariableType.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)] @@ -129,14 +110,8 @@ def __init__(self): self.obj.oms_setCommandLineOption.restype = ctypes.c_int self.obj.oms_setConnectionLinearTransformation.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_double] self.obj.oms_setConnectionLinearTransformation.restype = ctypes.c_int - self.obj.oms_setConnectionGeometry.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ssd_connection_geometry_t)] - self.obj.oms_setConnectionGeometry.restype = ctypes.c_int - self.obj.oms_setConnectorGeometry.argtypes = [ctypes.c_char_p, ctypes.POINTER(ssd_connector_geometry_t)] - self.obj.oms_setConnectorGeometry.restype = ctypes.c_int self.obj.oms_setConnectorNumericType.argtypes = [ctypes.c_char_p, ctypes.c_int] self.obj.oms_setConnectorNumericType.restype = ctypes.c_int - self.obj.oms_setElementGeometry.argtypes = [ctypes.c_char_p, ctypes.POINTER(ssd_element_geometry_t)] - self.obj.oms_setElementGeometry.restype = ctypes.c_int self.obj.oms_setTempDirectory.argtypes = [ctypes.c_char_p] self.obj.oms_setTempDirectory.restype = ctypes.c_int self.obj.oms_setExportName.argtypes = [ctypes.c_char_p, ctypes.c_char_p] @@ -167,6 +142,8 @@ def __init__(self): self.obj.oms_setVariableStepSize.restype = ctypes.c_int self.obj.oms_setLogFile.argtypes = [ctypes.c_char_p] self.obj.oms_setLogFile.restype = ctypes.c_int + self.obj.oms_setWorkingDirectory.argtypes = [ctypes.c_char_p] + self.obj.oms_setWorkingDirectory.restype = ctypes.c_int self.obj.oms_setLoggingInterval.argtypes = [ctypes.c_char_p, ctypes.c_double] self.obj.oms_setLoggingInterval.restype = ctypes.c_int self.obj.oms_setLoggingLevel.argtypes = [ctypes.c_int] @@ -209,6 +186,10 @@ def delete(self, cref): status = self.obj.oms_delete(cref.encode()) return Status(status) + def doStep(self, cref): + status = self.obj.oms_doStep(cref.encode()) + return Status(status) + def getVersion(self): return self.obj.oms_getVersion().decode('utf-8') @@ -270,40 +251,12 @@ def setConnectionLinearTransformation(self, crefA, crefB, factor, offset): status = self.obj.oms_setConnectionLinearTransformation(crefA.encode(), crefB.encode(), factor, offset) return Status(status) - def setConnectionGeometry(self, crefA, crefB, pointsX, pointsY): - '''Set the connection geometry for a connection between two connectors. - The connection geometry is defined by a list of points (pointsX, pointsY) that define the path of the connection in the diagram.''' - n = len(pointsX) - if n != len(pointsY): - raise ValueError("pointsX and pointsY must have the same length") - geometry = ssd_connection_geometry_t( - (ctypes.c_double * n)(*pointsX), - (ctypes.c_double * n)(*pointsY), - n - ) - status = self.obj.oms_setConnectionGeometry(crefA.encode(), crefB.encode(), ctypes.byref(geometry)) - return Status(status) - - def setConnectorGeometry(self, cref, x, y): - '''Set the connector geometry for a connector. - The connector geometry is defined by a point (x, y) that defines the position of the connector in the diagram.''' - geometry = ssd_connector_geometry_t(x, y) - status = self.obj.oms_setConnectorGeometry(cref.encode(), ctypes.byref(geometry)) - return Status(status) - def setConnectorNumericType(self, cref, numericType): '''Set the numeric type for a connector. This is used to specify the numeric type of the connector, which is important for FMI3 connectors that can have different numeric types (e.g., float32, float64, int32, int64).''' status = self.obj.oms_setConnectorNumericType(cref.encode(), numericType) return Status(status) - def setElementGeometry(self, cref, x1, y1, x2, y2, rotation=0.0, iconSource=b"", iconRotation=0.0, iconFlip=False, iconFixedAspectRatio=False): - '''Set the element geometry for a model or system. - The element geometry is defined by a bounding box (x1, y1, x2, y2) that defines the position and size of the element in the diagram, as well as optional rotation and icon information.''' - geometry = ssd_element_geometry_t(x1, y1, x2, y2, rotation, iconSource, iconRotation, iconFlip, iconFixedAspectRatio) - status = self.obj.oms_setElementGeometry(cref.encode(), ctypes.byref(geometry)) - return Status(status) - def setTempDirectory(self, newTempDir): status = self.obj.oms_setTempDirectory(newTempDir.encode()) return Status(status) @@ -344,7 +297,7 @@ def setSolver(self, cref, solver): status = self.obj.oms_setSolver(cref.encode(), solver) return Status(status) - def setResultFile(self, cref, filename, bufferSize=1) -> Status: + def setResultFile(self, cref, filename, bufferSize=10) -> Status: '''Set the result file for a model or system. The bufferSize specifies how many values are buffered before writing to the file.''' status = self.obj.oms_setResultFile(cref.encode(), filename.encode(), bufferSize) @@ -360,6 +313,21 @@ def setStopTime(self, cref, stopTime) -> Status: status = self.obj.oms_setStopTime(cref.encode(), stopTime) return Status(status) + def getStartTime(self, cref): + startTime = ctypes.c_double() + status = self.obj.oms_getStartTime(cref.encode(), ctypes.byref(startTime)) + return [startTime.value, Status(status)] + + def getStopTime(self, cref): + stopTime = ctypes.c_double() + status = self.obj.oms_getStopTime(cref.encode(), ctypes.byref(stopTime)) + return [stopTime.value, Status(status)] + + def getTime(self, cref): + time = ctypes.c_double() + status = self.obj.oms_getTime(cref.encode(), ctypes.byref(time)) + return [time.value, Status(status)] + def setTolerance(self, cref, relativeTolerance) -> Status: '''Set the relative and absolute tolerance for the simulation.''' status = self.obj.oms_setTolerance(cref.encode(), relativeTolerance) @@ -390,6 +358,10 @@ def setLogFile(self, filename): status = self.obj.oms_setLogFile(filename.encode()) return Status(status) + def setWorkingDirectory(self, newWorkingDir): + status = self.obj.oms_setWorkingDirectory(newWorkingDir.encode()) + return Status(status) + def setLoggingInterval(self, cref, loggingInterval): status = self.obj.oms_setLoggingInterval(cref.encode(), loggingInterval) return Status(status) diff --git a/src/OMSimulatorPython/component.py b/src/OMSimulatorPython/component.py index c389a7854..13662ab63 100644 --- a/src/OMSimulatorPython/component.py +++ b/src/OMSimulatorPython/component.py @@ -54,12 +54,19 @@ def __init__(self, name: CRef, fmuPath: Path | str, implementation=None, connect self.parameterResources = [] self.metaDataResources = [] self.implementation = implementation + self.fmu = None # set by addComponent when the FMU is loaded def addConnector(self, connector): if connector in self.connectors: raise ValueError(f"Connector '{connector.name}' already exists in {self.name}") self.connectors.append(connector) + def getConnector(self, cref: CRef): + for connector in self.connectors: + if connector.name == cref: + return connector + return None + def deleteConnector(self, cref: CRef): for i, connector in enumerate(self.connectors): if connector.name == cref: @@ -198,7 +205,7 @@ def exportToSSD(self, node): ## export Annotations if self.solver: from OMSimulator import utils - utils.exportAnnotations(component_node, self.solver) + utils.exportSimulationInformation(component_node, self.solver) def setValue(self, cref:str, value, unit=None, description = None): for connector in self.connectors: @@ -208,8 +215,18 @@ def setValue(self, cref:str, value, unit=None, description = None): ## it is possible that the parameter is not defined as connector but only in the ssv file or ssm mapping, so we allow setting values without types self.value.setValue(cref, value, None, unit, description) - def getValue(self, cref:str): - return self.value.getValue(cref) + def getValue(self, cref: str): + entry = self.value.getValue(cref) + if entry is not None: + return entry + # Fall back to the default start value from modeldescription.xml. + # modelDescriptionStartValue is a raw string parsed from XML — returned as-is; + # the caller is responsible for converting to the target numeric type. + if self.fmu is not None: + for var in self.fmu.variables: + if str(var.name) == str(cref): + return var.modelDescriptionStartValue + return None def setSolver(self, name: str): self.solver = name \ No newline at end of file diff --git a/src/OMSimulatorPython/componenttable.py b/src/OMSimulatorPython/componenttable.py index 3ea79d2d1..210c67df5 100644 --- a/src/OMSimulatorPython/componenttable.py +++ b/src/OMSimulatorPython/componenttable.py @@ -34,6 +34,7 @@ from lxml import etree as ET from OMSimulator.cref import CRef from OMSimulator.connector import Connector +from OMSimulator.elementgeometry import ElementGeometry from OMSimulator.variable import Causality, SignalType from OMSimulator import namespace from OMSimulator.capi import Capi @@ -62,6 +63,8 @@ def __init__(self, name: CRef, filePath: Path | str, connectors : list | None = self.filePath = filePath self.connectors = connectors self._solver = None + self.elementgeometry = None + @property def solver(self): @@ -81,6 +84,11 @@ def list(self, prefix=""): for connector in self.connectors: connector.list(prefix=prefix + " |--") + ## list component element geometry + if self.elementgeometry: + print(f"{prefix} ElementGeometry:") + self.elementgeometry.list(prefix=prefix + " |--") + def exportToSSD(self, node): component_node = ET.SubElement(node, namespace.tag("ssd", "Component")) component_node.set("name", str(self.name)) @@ -92,3 +100,7 @@ def exportToSSD(self, node): ## export component connectors for connector in self.connectors: connector.exportToSSD(connectors_node) + + ## export component element geometry + if self.elementgeometry: + self.elementgeometry.exportToSSD(component_node) \ No newline at end of file diff --git a/src/OMSimulatorPython/fmu.py b/src/OMSimulatorPython/fmu.py index 97b79eba9..c525ad1fb 100644 --- a/src/OMSimulatorPython/fmu.py +++ b/src/OMSimulatorPython/fmu.py @@ -59,6 +59,17 @@ def __init__(self, fmu_path: Union[str, Path], instanceName: str = None): self._generationTool = None self._generationDateAndTime = None self._variableNamingConvention = None + + self.canBeInstantiatedOnlyOncePerProcess = False + self.canGetAndSetFMUstate = False + self.canNotUseMemoryManagementFunctions = False + self.canSerializeFMUstate = False + self.completedIntegratorStepNotNeeded = False + self.needsExecutionTool = False + self.providesDirectionalDerivative = False + self.canInterpolateInputs = False + self.maxOutputDerivativeOrder = 0 + self._variables = [] self._states = [] self._unitDefinitions = [] @@ -73,6 +84,10 @@ def __init__(self, fmu_path: Union[str, Path], instanceName: str = None): def fmiVersion(self): return self._fmiVersion + @property + def fmuPath(self): + return self._fmu_path + @property def modelName(self): return self._modelName @@ -110,6 +125,18 @@ def variables(self): def states(self): return self._states + def _get_bool(self, element, attr, default=False): + value = element.get(attr) + if value is None: + return default + return value.lower() == "true" + + def _get_int(self, element, attr, default=0): + value = element.get(attr) + if value is None: + return default + return int(value) + def _load_model_description(self): '''Extract and parse the modelDescription.xml from the FMU zip archive.''' if not self._fmu_path.exists(): @@ -136,8 +163,11 @@ def _load_model_description(self): self._generationDateAndTime = model_description.get('generationDateAndTime') self._variableNamingConvention = model_description.get('variableNamingConvention') - has_me = model_description.find('.//{*}ModelExchange') is not None - has_cs = model_description.find('.//{*}CoSimulation') is not None + me = model_description.find('.//{*}ModelExchange') + cs = model_description.find('.//{*}CoSimulation') + + has_me = me is not None + has_cs = cs is not None if has_me and has_cs: self._fmuType = 'me_cs' @@ -146,6 +176,28 @@ def _load_model_description(self): elif has_cs: self._fmuType = 'cs' + ## CoSimulation attributes + if cs is not None: + self.canBeInstantiatedOnlyOncePerProcess = self._get_bool(cs, "canBeInstantiatedOnlyOncePerProcess") + self.canGetAndSetFMUstate = self._get_bool(cs, "canGetAndSetFMUstate") + self.canNotUseMemoryManagementFunctions = self._get_bool(cs, "canNotUseMemoryManagementFunctions") + self.canSerializeFMUstate = self._get_bool(cs, "canSerializeFMUstate") + self.completedIntegratorStepNotNeeded = False + self.needsExecutionTool = self._get_bool(cs, "needsExecutionTool") + self.providesDirectionalDerivative = self._get_bool(cs, "providesDirectionalDerivative") + self.canInterpolateInputs = self._get_bool(cs, "canInterpolateInputs") + self.maxOutputDerivativeOrder = self._get_int(cs, "maxOutputDerivativeOrder") + + ## ModelExchange attributes + if me is not None: + self.canBeInstantiatedOnlyOncePerProcess = self._get_bool(me, "canBeInstantiatedOnlyOncePerProcess") + self.canGetAndSetFMUstate = self._get_bool(me, "canGetAndSetFMUstate") + self.canNotUseMemoryManagementFunctions = self._get_bool(me, "canNotUseMemoryManagementFunctions") + self.canSerializeFMUstate = self._get_bool(me, "canSerializeFMUstate") + self.completedIntegratorStepNotNeeded = self._get_bool(me, "completedIntegratorStepNotNeeded") + self.needsExecutionTool = self._get_bool(me, "needsExecutionTool") + self.providesDirectionalDerivative = self._get_bool(me, "providesDirectionalDerivative") + # Parse default experiment settings self._parse_default_experiment(model_description) diff --git a/src/OMSimulatorPython/instantiated_model.py b/src/OMSimulatorPython/instantiated_model.py index c6cebcd7b..8b3885de4 100644 --- a/src/OMSimulatorPython/instantiated_model.py +++ b/src/OMSimulatorPython/instantiated_model.py @@ -46,16 +46,28 @@ class SolverType(Enum): '''Enumeration for solver method to map with c api.''' euler = 2 cvode = 3 - + oms_ma = 6 + oms_mav = 7 + oms_mav2 = 8 class SystemType(Enum): '''Enumeration for system type to map with c api.''' wc = 1 sc = 2 sc3 = 3 +# Solver methods that produce a WC system; all others produce SC. +_WC_SOLVER_METHODS = {"oms_ma", "oms_mav", "oms_mav2"} + +def _system_type_for_method(method: str) -> SystemType: + return SystemType.wc if method in _WC_SOLVER_METHODS else SystemType.sc + +def _system_type_label(system_type: SystemType) -> str: + return "oms_system_wc" if system_type == SystemType.wc else "oms_system_sc" + class InstantiatedModel: _suppress_path_set = False # Class variable to track if suppressPath has been set def __init__(self, json_description, system: System, resources: dict): + #print(f"info: Instantiating model with JSON description:\n{json_description}", flush=True) config = json.loads(json_description) self.modelName = "model" ## create random name, but we cannot commits test as jenkins will gerate new model name self.apiCall = [] @@ -81,65 +93,91 @@ def __init__(self, json_description, system: System, resources: dict): self.apiCall.append(f'oms_newModel("{self.modelName}")') # Extract all simulation units + # processElements guarantees every unit has a solver (CS=oms_ma, ME=cvode by default). sim_units = config.get("simulation units", []) - # Count the number of unique solvers for determining system type (WC/SC) - solver_units = [unit for unit in sim_units if "solver" in unit] - num_solvers = len(solver_units) - - # --- Add systems depending on number of solvers --- - if num_solvers == 1: - # Only one solver unit → SC system directly under root - status = Capi.addSystem(f"{self.modelName}.root", SystemType.sc.value) # 2 = oms_system_sc - if status != Status.ok: - raise RuntimeError(f"Failed to create root SC system: {status}") - self.apiCall.append(f'oms_addSystem("{self.modelName}.root", "oms_system_sc")') + num_units = len(sim_units) + # print(f"Number of simulation units: {num_units}", flush=True) + + # --- Determine root system type --- + # Single unit : root takes that solver's own type (WC or SC directly, no wrapper). + # Multiple units : always WC root so each solver unit becomes a subsystem. + if num_units == 1: + root_type = _system_type_for_method(sim_units[0].get("solver", {}).get("method", "")) else: - # Multiple solver units or zero solvers top-level WC system - status = Capi.addSystem(f"{self.modelName}.root", SystemType.wc.value) # 1 = oms_system_wc - if status != Status.ok: - raise RuntimeError(f"Failed to create root WC system: {status}") - self.apiCall.append(f'oms_addSystem("{self.modelName}.root", "oms_system_wc")') + root_type = SystemType.wc + + root_label = _system_type_label(root_type) + status = Capi.addSystem(f"{self.modelName}.root", root_type.value) + export_name = f"{self.system.name}" # Use the system's name as the export name for the root system + if export_name not in self.mappedCrefs: + self.mappedCrefs[export_name] = f"{self.modelName}.root" + if status != Status.ok: + raise RuntimeError(f"Failed to create root system: {status}") + self.apiCall.append(f'oms_addSystem("{self.modelName}.root", "{root_label}")') + + # Step 1: Create subsystems, add all components, populate mappedCrefs + # We must add every component before processing any connections, because a + # connection in one simulation unit may reference a component that lives in a + # different unit (cross-unit connection at the WC root level). - # Iterate over simulation units + unit_solver_paths = [] # parallel list: solver_path for each sim_unit for unit in sim_units: - ## check if unit has solver as key - solvername = "None" - solver = unit.get("solver") - if num_solvers > 1 and solver: - solvername = unit["solver"]['name'] + solver = unit.get("solver", {}) + method = solver.get("method", "") + # WC-method units (oms_ma etc.) cannot be nested under a WC root — + # their components go directly into model.root. + # SC-method units (cvode, euler) get their own named subsystem. + if num_units > 1 and method not in _WC_SOLVER_METHODS: + solvername = solver['name'] solver_path = f"{self.modelName}.root.{solvername}" - self.apiCall.append(f'oms_addSystem("{solver_path}", "oms_system_sc")') - status = Capi.addSystem(f"model.root.{solvername}", SystemType.sc.value) # 2 = oms_system_sc + sub_type = _system_type_for_method(method) + sub_label = _system_type_label(sub_type) + self.apiCall.append(f'oms_addSystem("{solver_path}", "{sub_label}")') + status = Capi.addSystem(f"model.root.{solvername}", sub_type.value) if status != Status.ok: raise RuntimeError(f"Failed to add oms_addSystem: {status}") else: solver_path = f"{self.modelName}.root" - ## set solver method and tolerance if provided + ## set solver method and settings if provided if solver: - method = solver.get("method") - tolerance = solver.get("tolerance") - stepSize = solver.get("stepSize") - - if method in ("cvode", "euler"): - status = Capi.setSolver(solver_path, SolverType[method].value) # 3=cvode, 2=euler + if method in ("cvode", "euler", "oms_ma", "oms_mav", "oms_mav2"): + status = Capi.setSolver(solver_path, SolverType[method].value) if status != Status.ok: raise RuntimeError(f"Failed to set solver: {status}") self.apiCall.append(f'oms_setSolver("{solver_path}", "{method}")') - if tolerance is not None: - status = Capi.setTolerance(solver_path, float(tolerance)) + relativeTolerance = solver.get("relativeTolerance") + if relativeTolerance is not None: + status = Capi.setTolerance(solver_path, float(relativeTolerance)) if status != Status.ok: raise RuntimeError(f"Failed to set tolerance: {status}") - self.apiCall.append(f'oms_setTolerance("{solver_path}", {float(tolerance)})') + self.apiCall.append(f'oms_setTolerance("{solver_path}", {float(relativeTolerance)})') + + # variable-step solvers: initialStepSize, minimumStepSize, maximumStepSize + if method in ("cvode", "oms_mav", "oms_mav2"): + initialStepSize = solver.get("initialStepSize") + minimumStepSize = solver.get("minimumStepSize") + maximumStepSize = solver.get("maximumStepSize") + if any(v is not None for v in (initialStepSize, minimumStepSize, maximumStepSize)): + initialStepSize = float(initialStepSize) if initialStepSize is not None else 1e-6 + minimumStepSize = float(minimumStepSize) if minimumStepSize is not None else 1e-12 + maximumStepSize = float(maximumStepSize) if maximumStepSize is not None else 0.001 + status = Capi.setVariableStepSize(solver_path, initialStepSize, minimumStepSize, maximumStepSize) + if status != Status.ok: + raise RuntimeError(f"Failed to set variable step size: {status}") + self.apiCall.append(f'oms_setVariableStepSize("{solver_path}", {initialStepSize}, {minimumStepSize}, {maximumStepSize})') + + # fixed-step solvers: fixedStepSize + if method in ("oms_ma", "euler"): + fixedStepSize = solver.get("fixedStepSize") + if fixedStepSize is not None: + status = Capi.setFixedStepSize(solver_path, float(fixedStepSize)) + if status != Status.ok: + raise RuntimeError(f"Failed to set fixed step size: {status}") + self.apiCall.append(f'oms_setFixedStepSize("{solver_path}", {float(fixedStepSize)})') - if stepSize is not None: - status = Capi.setFixedStepSize(solver_path, float(stepSize)) - if status != Status.ok: - raise RuntimeError(f"Failed to set step size: {status}") - self.apiCall.append(f'oms_setFixedStepSize("{solver_path}", {float(stepSize)})') - listofsystems = [] ## add components for comp in unit["components"]: if len(comp["name"]) <= 2: @@ -148,15 +186,11 @@ def __init__(self, json_description, system: System, resources: dict): ## add prefix to nested systems to avoid name conflicts while flattening the system during instantiation, e.g. sub-system1_Add1, sub-system1_Gain1 comp_name = f"{comp['name'][-2]}_{comp['name'][-1]}" comp_path = ".".join([solver_path] + [comp_name]) - currentSystem = ".".join(comp["name"][:-1]) - if currentSystem not in listofsystems: - listofsystems.append(currentSystem) self.apiCall.append(f'oms_addSubModel("{comp_path}", "{comp["path"]}")') status = Capi.addSubModel(comp_path, comp["path"]) if status != Status.ok: raise RuntimeError(f"Failed to add oms_addSubModel: {status}") export_name = ".".join(comp["name"]) - #print(f"Setting export name for {comp_path} to {export_name}") ## parse connector geometry for the component if exist and set it to capi after adding the component, this is needed for proper mapping of connector geometry if "connectors" in comp: @@ -175,23 +209,6 @@ def __init__(self, json_description, system: System, resources: dict): raise RuntimeError(f"Failed to set connector geometry for {connector_path}: {status}") self.apiCall.append(f'oms_setConnectorGeometry("{connector_path}", {x}, {y})') - ## parse element geometry for the component if exist and set it to capi after adding the component, this is needed for proper mapping of element geometry - if "element geometry" in comp: - geometry = comp["element geometry"] - x1 = geometry.get("x1", 0.0) - y1 = geometry.get("y1", 0.0) - x2 = geometry.get("x2", 0.0) - y2 = geometry.get("y2", 0.0) - rotation = geometry.get("rotation") - iconSource = geometry.get("iconSource", b"") - iconRotation = geometry.get("iconRotation", 0.0) - iconFlip = geometry.get("iconFlip", False) - iconFixedAspectRatio = geometry.get("iconFixedAspectRatio", False) - status = Capi.setElementGeometry(comp_path, x1, y1, x2, y2, rotation, iconSource, iconRotation, iconFlip, iconFixedAspectRatio) - if status != Status.ok: - raise RuntimeError(f"Failed to set element geometry for {comp_path}: {status}") - self.apiCall.append(f'oms_setElementGeometry("{comp_path}", {x1}, {y1}, {x2}, {y2}, {rotation}, "{iconSource}", {iconRotation}, {iconFlip}, {iconFixedAspectRatio})') - if not export_name in self.mappedCrefs: self.mappedCrefs[export_name] = comp_path self.mappedCrefs[".".join(comp["name"][:-1])] = solver_path # map parent system too for connector lookup @@ -199,15 +216,14 @@ def __init__(self, json_description, system: System, resources: dict): if status != Status.ok: raise RuntimeError(f"Failed to set export name: {status}") - ## add top level system connectors before adding connections - for current_system in listofsystems: - ## top level connectors: - if current_system == self.system.name: - self._addConnector(self.system.connectors, self.system.name) - ## add top sub-system level connectors mapped with currentSystem - self.addConnectorFromElements(self.system.elements, current_system) + unit_solver_paths.append(solver_path) - ## add connections + # Add connectors for the root system and all subsystems in one recursive pass. + # This replaces per-unit listofsystems tracking and duplication entirely. + self._addConnectorsRecursive(self.system, self.system.name) + + # Step 2: Add all connections (mappedCrefs is now fully populated) + for unit in sim_units: for connection in unit["connections"]: start_element = ".".join(connection['start element'] + [connection['start connector']]) end_element = ".".join(connection['end element'] + [connection['end connector']]) @@ -231,14 +247,6 @@ def __init__(self, json_description, system: System, resources: dict): status = Capi.setConnectionLinearTransformation(start, end, float(factor), float(offset)) if status != Status.ok: raise RuntimeError(f"Failed to set connection linear transformation: {status}") - ## add connection geometry if exist - if "connection geometry" in connection: - pointsX = connection["connection geometry"]["pointsX"] - pointsY = connection["connection geometry"]["pointsY"] - self.apiCall.append(f'oms_setConnectionGeometry("{start}", "{end}", {pointsX}, {pointsY})') - status = Capi.setConnectionGeometry(start, end, pointsX, pointsY) - if status != Status.ok: - raise RuntimeError(f"Failed to set connection geometry: {status}") ## set start values self.setStartValues(self.system.value, self.system.name, self.system.parameterMapping) @@ -255,6 +263,9 @@ def __init__(self, json_description, system: System, resources: dict): ## set start and stop time from ssp self.setStartTime(float(config["simulation settings"]['start time'])) self.setStopTime(float(config["simulation settings"]['stop time'])) + ## set result file name from ssp + self.setResultFile(config["simulation settings"]['result file'], int(config["simulation settings"]['buffer size'])) + self.setLoggingInterval(float(config["simulation settings"]['logging interval'])) def setStartValuesFromElements(self, elements, systemName): for key, element in elements.items(): @@ -382,6 +393,24 @@ def apply_start_value(self, value_path:str, value, type): case _: raise TypeError(f"Unsupported type: {type}") + def _addConnectorsRecursive(self, system: System, system_name: str): + """Walk the system tree and add connectors for every system/subsystem + that was actually instantiated (i.e. present in mappedCrefs).""" + if system_name not in self.mappedCrefs: + # connector-only subsystems (no FMU components) are not added during Step 1; + # derive their OMS path from the parent so their connectors can be flattened there. + parent_name = system_name.rsplit(".", 1)[0] if "." in system_name else None + if parent_name and parent_name in self.mappedCrefs: + self.mappedCrefs[system_name] = self.mappedCrefs[parent_name] + else: + return + if system.connectors: + self._addConnector(system.connectors, system_name) + for element in system.elements.values(): + if isinstance(element, System): + child_name = f"{system_name}.{element.name}" + self._addConnectorsRecursive(element, child_name) + def _addConnector(self, connectors, systemName): for connector in connectors: connector_name =".".join([systemName]+[str(connector.name)]) @@ -402,14 +431,6 @@ def _addConnector(self, connectors, systemName): status = Capi.setConnectorNumericType(connector_path, connector.numericType.value) if status != Status.ok: raise RuntimeError(f"Failed to set connector numeric type for {connector_path}: {status}") - ## set connector geometry if exist - if connector.connectorGeometry: - x = connector.connectorGeometry.x - y = connector.connectorGeometry.y - self.apiCall.append(f'oms_setConnectorGeometry("{connector_path}", {x}, {y})') - status = Capi.setConnectorGeometry(connector_path, x, y) - if status != Status.ok: - raise RuntimeError(f"Failed to set connector geometry for {connector_path}: {status}") export_name = systemName if not connector_name in self.mappedCrefs: @@ -421,17 +442,6 @@ def _addConnector(self, connectors, systemName): if status != Status.ok: raise RuntimeError(f"Failed to set alias name: {status}") - def addConnectorFromElements(self, elements, currentSystem): - ## add connectors mapped with currentSystem - for key, element in elements.items(): - connector_path = ".".join([self.system.name, str(element.name)]) - if currentSystem == connector_path: - self._addConnector(element.connectors, connector_path) - - ## recurse into subsystem - if isinstance(element, System): - self.addConnectorFromElements(element.elements, currentSystem) - def dumpApiCalls(self): """Returns the generated API calls as a string.""" return "\n".join(self.apiCall) @@ -567,8 +577,8 @@ def stepUntil(self, stopTime): if status != Status.ok: raise RuntimeError(f"Failed to step until {stopTime}: {status}") - def setResultFile(self, filename: str): - status = Capi.setResultFile("model", filename) + def setResultFile(self, filename: str, bufferSize: int = 10): + status = Capi.setResultFile("model", filename, bufferSize) if status !=Status.ok: raise RuntimeError(f"Failed to setResultFile {filename}: {status}") @@ -577,6 +587,30 @@ def setStartTime(self, startTime: float): if status != Status.ok: raise RuntimeError(f"Failed to set start time: {status}") + def getStartTime(self) -> float: + startTime, status = Capi.getStartTime(self.modelName) + if status != Status.ok: + raise RuntimeError(f"Failed to get start time: {status}") + return startTime + + def getStopTime(self) -> float: + stopTime, status = Capi.getStopTime(self.modelName) + if status != Status.ok: + raise RuntimeError(f"Failed to get stop time: {status}") + return stopTime + + def getTime(self) -> float: + time, status = Capi.getTime(self.modelName) + if status != Status.ok: + raise RuntimeError(f"Failed to get time: {status}") + return time + + + def doStep(self): + status = Capi.doStep(self.modelName) + if status != Status.ok: + raise RuntimeError(f"Failed to do step: {status}") + def setStopTime(self, stopTime: float): if self.fmuInstantitated is False: raise RuntimeError("FMU must be instantiated before setting stop time") diff --git a/src/OMSimulatorPython/ssd.py b/src/OMSimulatorPython/ssd.py index de1cc07ab..ad04ac3d6 100644 --- a/src/OMSimulatorPython/ssd.py +++ b/src/OMSimulatorPython/ssd.py @@ -36,6 +36,7 @@ from lxml import etree as ET from OMSimulator import capi from OMSimulator.cref import CRef +from OMSimulator.connector import Connector from OMSimulator.fmu import FMU from OMSimulator.settings import suppress_path_to_str from OMSimulator.system import System @@ -60,6 +61,14 @@ def __init__(self, name: str): self.system = System(name) self.startTime = 0.0 self.stopTime = 1.0 + self.resultFile = self._name + "_res.mat" + self.loggingInterval = 0.0 + self.bufferSize = 10 + ## default solver settings + self.initialStepSize = 1e-6 + self.minimumStepSize = 1e-12 + self.maximumStepSize = 1e-3 + self.tolerance = 1e-4 self.unitDefinitions = list() self.enumerationDefinitions = list() @@ -137,6 +146,10 @@ def addComponent(self, cref: CRef, resource: str, inst : FMU | None = None): subcref = self._validateCref(cref) return self.system.addComponent(subcref, resource, inst) + def replaceComponent(self, cref: CRef, resource: str, inst : FMU | None = None, dryRun: bool = False): + subcref = self._validateCref(cref) + return self.system.replaceComponent(subcref, resource, inst, dryRun) + def delete(self, cref: CRef): subcref = self._validateCref(cref) if subcref is None: @@ -145,6 +158,14 @@ def delete(self, cref: CRef): return return self.system.delete(subcref) + def rename(self, cref: CRef, new_name: CRef): + subcref = self._validateCref(cref) + if subcref is None: + # Renaming the root system itself. + self.system.name = str(new_name) + return + self.system.rename(subcref, new_name) + def addSSVReference(self, cref: CRef, resource1: str, resource2: str | None = None): subcref = self._validateCref(cref) self.system.addSSVReference(subcref, resource1, resource2) @@ -191,6 +212,12 @@ def addConnection(self, cref1: CRef, cref2: CRef): #logger.debug(f"Adding connection from {subcref1} to {subcref2}") self.system._addConnection(subcref1, subcref2) + def deleteConnection(self, cref1: CRef, cref2: CRef): + subcref1 = self._validateCref(cref1) + subcref2 = self._validateCref(cref2) + #logger.debug(f"Deleting connection from {subcref1} to {subcref2}") + self.system._deleteConnection(subcref1, subcref2) + def newSolver(self, options: dict): self.system.solvers.append(options) @@ -208,7 +235,11 @@ def setValue(self, cref: CRef, value, unit = None, description = None): def getValue(self, cref: CRef): subcref = self._validateCref(cref) - self.system.getValue(subcref) + return self.system.getValue(subcref) + + def getElement(self, cref: CRef): + subcref = self._validateCref(cref) + return self.system.getElement(subcref) def mapParameter(self, cref: CRef, source: str, target: str): '''Maps a parameter from source to target in the system.''' @@ -221,10 +252,24 @@ def addSystem(self, cref: CRef): self.system.addSystem(cref.pop_first(first=self._name)) + def addConnector(self, cref: CRef, connector: Connector): + subcref = self._validateCref(cref) + self.system.addConnectorHelper(subcref, connector) + + def getConnector(self, cref: CRef): + subcref = self._validateCref(cref) + return self.system.getConnector(subcref) + + def getConnection(self, crefA: CRef, crefB: CRef): + subcrefA = self._validateCref(crefA) + subcrefB = self._validateCref(crefB) + return self.system.getConnection(subcrefA, subcrefB) + def instantiate(self, resources: dict | None = None, tempdir: str | None = None ) -> InstantiatedModel: if self.system is None: raise ValueError("Variant doesn't contain a system") - json_desc = self.system.generateJson(resources, tempdir, self.startTime, self.stopTime) + simulation_info = {"startTime": self.startTime, "stopTime": self.stopTime, "resultFile": self.resultFile, "loggingInterval": self.loggingInterval, "bufferSize": self.bufferSize} + json_desc = self.system.generateJson(resources, tempdir, simulation_info) return InstantiatedModel(json_desc, self.system, resources) def list(self, prefix=""): @@ -263,10 +308,11 @@ def export(self, filename: str | None = None): generationDateAndTime=datetime.now().isoformat() ) - self.system.export(root) + if self.system is not None: + self.system.export(root) + self._exportEnumerationDefinitions(root) + self._exportUnitDefinitions(root) - self._exportEnumerationDefinitions(root) - self._exportUnitDefinitions(root) self._exportDefaultExperiment(root) xml_content = ET.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True).decode("utf-8") @@ -303,3 +349,5 @@ def _exportDefaultExperiment(self, node): default_experiment = ET.SubElement(node, namespace.tag("ssd", "DefaultExperiment")) default_experiment.set("startTime", str(self.startTime)) default_experiment.set("stopTime", str(self.stopTime)) + simulation_info = { "resultFile": self.resultFile, "loggingInterval": str(self.loggingInterval), "bufferSize": str(self.bufferSize)} + utils.exportSimulationInformation(default_experiment, [simulation_info]) diff --git a/src/OMSimulatorPython/ssp.py b/src/OMSimulatorPython/ssp.py index 1001282cf..f177833a0 100644 --- a/src/OMSimulatorPython/ssp.py +++ b/src/OMSimulatorPython/ssp.py @@ -43,6 +43,7 @@ from OMSimulator.ssv import SSV from OMSimulator.ssm import SSM from OMSimulator.componenttable import ResultReader +from OMSimulator.connector import Connector from OMSimulator import SSD, CRef, namespace from lxml import etree as ET @@ -168,11 +169,27 @@ def addComponent(self, cref: CRef, resource: str): return self.activeVariant.addComponent(cref, resource, inst=fmu_inst) + def replaceComponent(self, cref: CRef, resource: str, dryRun: bool = False): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + + ## look up in the resource if exist and then use that instance + fmu_inst = None + if resource in self.resources: + fmu_inst = self.resources[resource] + + return self.activeVariant.replaceComponent(cref, resource, inst=fmu_inst, dryRun=dryRun) + def delete(self, cref: CRef): if self.activeVariant is None: raise ValueError("No active variant set in the SSP.") self.activeVariant.delete(cref) + def rename(self, cref: CRef, new_name: CRef): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + self.activeVariant.rename(cref, new_name) + def newSolver(self, options: dict): if self.activeVariant is None: raise ValueError("No active variant set in the SSP.") @@ -287,6 +304,12 @@ def addConnection(self, cref1: CRef, cref2: CRef): self.activeVariant.addConnection(cref1, cref2) + def deleteConnection(self, cref1: CRef, cref2: CRef): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + + self.activeVariant.deleteConnection(cref1, cref2) + def _getComponentResourcePath(self, cref: CRef): return self.activeVariant._getComponentResourcePath(cref) @@ -324,7 +347,12 @@ def getValue(self, cref: CRef): if fmu_inst and not fmu_inst.varExist(cref.last()): raise KeyError(f"Variable '{cref.last()}' does not exist in the variables list of component '{resource}'") - self.activeVariant.getValue(cref) + return self.activeVariant.getValue(cref) + + def getElement(self, cref: CRef): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + return self.activeVariant.getElement(cref) def mapParameter(self, cref: CRef, source: str, target: str): """Maps a parameter from source to target in the active variant.""" @@ -346,6 +374,24 @@ def addSystem(self, cref: CRef): self.activeVariant.addSystem(cref) + def addConnector(self, cref: CRef, connector: Connector): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + + self.activeVariant.addConnector(cref, connector) + + def getConnector(self, cref: CRef): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + + return self.activeVariant.getConnector(cref) + + def getConnection(self, crefA: CRef, crefB: CRef): + if self.activeVariant is None: + raise ValueError("No active variant set in the SSP.") + + return self.activeVariant.getConnection(crefA, crefB) + def add(self, element): '''Adds an SSD or a list/iterable of SSDs to the SSP.''' if isinstance(element, SSD): @@ -394,7 +440,6 @@ def instantiate(self): def export(self, filename: str): '''Exports the SSP to file''' logger.debug(f"Exporting SSP to {filename} using temp directory: {self.temp_dir}") - exported_count = 0 for ssd in self.variants.values(): if ssd.dirty: diff --git a/src/OMSimulatorPython/system.py b/src/OMSimulatorPython/system.py index 647ed9105..5e4373191 100644 --- a/src/OMSimulatorPython/system.py +++ b/src/OMSimulatorPython/system.py @@ -44,6 +44,7 @@ from OMSimulator.fmu import FMU from OMSimulator.ssm import SSM from OMSimulator.values import Values +from OMSimulator.variable import Causality, SignalType from OMSimulator import Capi, CRef, namespace, utils @@ -149,7 +150,7 @@ def importFromNode(node, ssd, resources: dict | None = None): utils.parseParameterBindings(node, system, resources) utils.parseMetaData(node, system, resources) system.elements = utils.parseElements(node, resources) - system.solvers = utils.parseAnnotations(node) + system.solvers = utils.parseSimulationInformation(node) Connection.importFromNode(node, system) return system @@ -157,6 +158,60 @@ def importFromNode(node, ssd, resources: dict | None = None): logger.error(f"Error parsing System: {e}") raise + def addConnectorHelper(self, cref: CRef, connector: Connector): + if cref is None: + self.addConnector(connector) + return + + first = cref.first() + + match self.elements.get(first): + case System(): + self.elements[first].addConnectorHelper(cref.pop_first(), connector) + + def getConnector(self, cref: CRef): + first = cref.first() + ## Check if the cref is a top level system connector + ## or allow non existing connectors to support parameter mapping throgh SSM inline or ssm file by checking if cref + connector = self._connectorExists(first) + if connector is not None: + return connector + + match self.elements.get(first): + case System(): + return self.elements[first].getConnector(cref.pop_first()) + case Component(): + return self.elements[first].getConnector(cref.pop_first()) + + def split_cref(self, cref: CRef): + head = cref.first() + tail = cref.pop_first() + + # connector-only cref like ".input" + if tail is None: + return "", str(head) + + return (head, tail) + + def getConnection(self, crefA: CRef, crefB: CRef): + (headA, tailA) = self.split_cref(crefA) + (headB, tailB) = self.split_cref(crefB) + + for connection in self.connections: + if (str(connection.startElement) == str(headA) and str(connection.startConnector) == str(tailA)) and (str(connection.endElement) == str(headB) and str(connection.endConnector) == str(tailB)): + return connection + if (str(connection.startElement) == str(headB) and str(connection.startConnector) == str(tailB)) and (str(connection.endElement) == str(headA) and str(connection.endConnector) == str(tailA)): + return connection + + ## recurse into subsystems to find the connection + for element in self.elements.values(): + if isinstance(element, System): + connection = element.getConnection(crefA.pop_first(), crefB.pop_first()) + if connection: + return connection + + return None + def addConnector(self, connector): if connector in self.connectors: raise ValueError(f"Connector '{connector.name}' already exists in {self.name}") @@ -245,6 +300,7 @@ def addComponent(self, cref: CRef, resource: str, inst = None): fmuType = inst._fmuType if inst else None component = Component(first, resource, fmuType, connectors, unitDefinitions, enumerationDefinitions) component.fmuType = inst.fmuType if inst else None + component.fmu = inst # keep FMU reference for modeldescription start value fallback self.elements[first] = component return component elif isinstance(inst, ResultReader) or (inst is None and resource.endswith(".csv")): @@ -255,6 +311,118 @@ def addComponent(self, cref: CRef, resource: str, inst = None): raise TypeError( f"Unknown component instance for '{first}' in '{self.name}'. " f"Please add the component from the top-level model.") + def replaceComponent(self, cref: CRef, resource: str, inst=None, dryRun: bool = False): + first = cref.first() + + # recurse into subsystem before any type checks + if not cref.is_root(): + if first not in self.elements: + raise ValueError(f"System '{first}' not found in '{self.name}'") + return self.elements[first].replaceComponent(cref.pop_first(), resource, inst, dryRun) + + # check component must exist at this level + old_component = self.elements.get(first) + if old_component is None: + raise ValueError(f"Component '{first}' not found in '{self.name}'") + + if not isinstance(old_component, (Component, ComponentTable)): + raise ValueError(f"Element '{first}' is not a replaceable component") + + import copy + snapshot = copy.deepcopy(self) if dryRun else None + + warnings = [] + # create replacement component — FMU or CSV/table + if isinstance(inst, ResultReader) or (inst is None and resource.endswith(".csv")): + new_component = ComponentTable(first, resource, inst.connectors if inst else []) + else: + new_component = Component(first, resource, inst._fmuType, inst.makeConnectors(), inst._unitDefinitions, inst._enumerationDefinitions) + new_component.fmu = inst + new_component.fmuType = inst.fmuType + + # preserve OMEdit geometry + new_component.elementgeometry = copy.deepcopy(old_component.elementgeometry) + # preserve values and resources + new_component.value = copy.deepcopy(old_component.value) + new_component.parameterMapping = copy.deepcopy(old_component.parameterMapping) + new_component.parameterResources = copy.deepcopy(old_component.parameterResources) + new_component.metaDataResources = copy.deepcopy(old_component.metaDataResources) + new_component.solver = old_component.solver + + # connector lookup + old_connectors = {str(c.name): c for c in old_component.connectors} + new_connectors = {str(c.name): c for c in new_component.connectors} + + # validate existing connections + connections_to_remove = [] + for connection in self.connections: + connector_name = None + if str(connection.startElement) == str(first): + connector_name = str(connection.startConnector) + elif str(connection.endElement) == str(first): + connector_name = str(connection.endConnector) + else: + continue + old_connector = old_connectors.get(connector_name) + new_connector = new_connectors.get(connector_name) + conn_str = f"{connection.startElement}.{connection.startConnector} ==> {connection.endElement}.{connection.endConnector}" + + # connector removed + if new_connector is None: + warnings.append( + f"deleting connection \"{conn_str}\", as signal \"{connector_name}\" couldn't be resolved " + f"to any signal in the replaced submodel \"{resource}\"") + connections_to_remove.append(connection) + continue + + # causality changed + if (old_connector is not None + and old_connector.causality is not None + and new_connector.causality is not None + and old_connector.causality != new_connector.causality): + warnings.append( + f"deleting connection \"{conn_str}\", as signal \"{connector_name}\" causality changed " + f"in the replaced submodel \"{resource}\"") + connections_to_remove.append(connection) + continue + + # signal type changed + if (old_connector is not None + and old_connector.signal_type is not None + and new_connector.signal_type is not None + and old_connector.signal_type != new_connector.signal_type): + warnings.append( + f"deleting connection \"{conn_str}\", as signal \"{connector_name}\" type changed " + f"in the replaced submodel \"{resource}\"") + connections_to_remove.append(connection) + + # remove invalid connections + for connection in connections_to_remove: + if connection in self.connections: + self.connections.remove(connection) + + # cleanup start values (FMU only — ComponentTable has no variables) + if inst is not None and hasattr(inst, "variables"): + valid_variables = {str(variable.name) for variable in inst.variables} + for parameter_name in list(new_component.value.start_values.keys()): + if str(parameter_name) not in valid_variables: + del new_component.value.start_values[parameter_name] + warnings.append( + f"deleting start value \"{first}.{parameter_name}\" in \"inline\" resources, " + f"because the identifier couldn't be resolved to any system signal in the replacing model" + ) + # replace component + self.elements[first] = new_component + + # rollback for dry run + if dryRun: + self.__dict__.clear() + self.__dict__.update(snapshot.__dict__) + return warnings + + del old_component + return warnings + def addSSVReference(self, cref: CRef, resource1: str, resource2: str | None = None): ## top level system if cref is None: @@ -490,20 +658,41 @@ def _addConnection(self, cref1: CRef, cref2: CRef) -> None: # Add the connections to top level system self.addConnection(start_element, start_connector, end_element, end_connector) + def _deleteConnection(self, crefA: CRef, crefB: CRef) -> None: + """Deletes a connection from the system.""" + startElement, startConnector = self.split_cref(crefA) + endElement, endConnector = self.split_cref(crefB) + + for connection in self.connections[:]: + if (connection.startElement == str(startElement) and connection.startConnector == str(startConnector) and connection.endElement == str(endElement) and connection.endConnector == str(endConnector)): + self.connections.remove(connection) + return + + for key, element in self.elements.items(): + if isinstance(element, System): + self.elements[key]._deleteConnection(crefA.pop_first(), crefB.pop_first()) + def isConnectorAlreadyConnected(self, startElement : str, startConnector : str, endElement : str, endConnector : str): """Check if a connection is valid in the system.""" for conn in self.connections: - if (conn.startElement == startElement and conn.startConnector == startConnector and - conn.endElement == endElement and conn.endConnector == endConnector): - raise ValueError(f"Connection from '{startElement}.{startConnector}' to '{endElement}.{endConnector}' already exists") + if (conn.endElement == str(endElement) and conn.endConnector == str(endConnector)): + raise ValueError(f"Connector '{conn.endElement}.{conn.endConnector}' is already connected to {conn.startElement}.{conn.startConnector}'") def _findConnector(self, element_name, connector_name): """Returns (owner_string, causality) or (None, None) if not found.""" - connectors = ( - self.connectors # System level - if not element_name else - self.elements[CRef(element_name)].connectors # Element level - ) + if not element_name: + connectors = self.connectors # System level + else: + key = CRef(element_name) + if key not in self.elements: + known = [str(k) for k in self.elements.keys()] + raise ValueError( + f"Component '{element_name}' not found in system '{self.name}'. " + f"Known elements: {known}. " + f"If you renamed a component, make sure to update all connection " + f"references (startElement/endElement) in too." + ) + connectors = self.elements[key].connectors # Element level for con in connectors: if str(con.name) == str(connector_name): owner_str = "System" if not element_name else "Element" @@ -535,11 +724,69 @@ def _connectorExists(self, cref: CRef) -> Connector | None: return connector return None + def rename(self, cref: CRef, new_name: CRef): + """Renames a component or subsystem identified by cref to new_name.""" + first = cref.first() + ## Check if the cref is a top level system or subsystemconnector + connector = self._connectorExists(first) + if connector is not None: + return self.renameConnector(connector, new_name) + + match self.elements.get(first): + case System() | Component(): + if cref.is_root(): + # cref points directly at the element to rename — do it here. + self.renameComponent(first, new_name) + else: + # Recurse into the subsystem. + self.elements[first].rename(cref.pop_first(), new_name) + case _: + raise ValueError(f"Element '{first}' not found in system '{self.name}'") + + def renameConnector(self, connector: Connector, new_name: CRef): + """Renames a connector identified by cref to new_name.""" + ## Check if the cref is a top level system connector + old_str = str(connector.name) + connector.name = new_name + ## rename all connection references to this connector in the system and subsystems + for connection in self.connections: + if str(connection.startConnector) == old_str: + connection.startConnector = new_name + if str(connection.endConnector) == old_str: + connection.endConnector = new_name + ## rename start values associated with connector + for key in list(self.value.start_values.keys()): + if str(key) == old_str: + self.value.start_values[new_name] = self.value.start_values.pop(key) + + def renameComponent(self, old_name: CRef, new_name: CRef): + """Renames a component/subsystem and updates all connection references in this system.""" + if old_name not in self.elements: + raise ValueError(f"Component '{old_name}' not found in system '{self.name}'") + if new_name in self.elements: + raise ValueError(f"Component '{new_name}' already exists in system '{self.name}'") + # Re-key the elements dict + element = self.elements.pop(old_name) + element.name = new_name + self.elements[new_name] = element + # Update all connection start/end element references + old_str = str(old_name) + new_str = str(new_name) + for connection in self.connections: + if str(connection.startElement) == old_str: + connection.startElement = new_str + if str(connection.endElement) == old_str: + connection.endElement = new_str + def _deleteConnector(self, cref: CRef) -> bool: """Delete connector if it exists.""" for i, connector in enumerate(self.connectors): if connector.name == cref: self.deleteAllConnection(cref) + ## delete start values associated with connector + for key in list(self.value.start_values.keys()): + if str(key) == str(cref): + del self.value.start_values[key] del self.connectors[i] return True return False @@ -560,6 +807,8 @@ def _getComponentResourcePath(self, cref): match element: case Component(): return (element.fmuPath, element) + case ComponentTable(): + return (element.filePath, element) case System(): return element._getComponentResourcePath(cref.pop_first()) case _: @@ -589,17 +838,33 @@ def getValue(self, cref: CRef): ## or allow non existing connectors to support parameter mapping throgh SSM inline or ssm file by checking if cref connector = self._connectorExists(first) if connector or cref.is_root(): - self.value.getValue(cref) - return + return self.value.getValue(cref) match self.elements.get(first): case System(): - self.elements[first].getValue(cref.pop_first()) + return self.elements[first].getValue(cref.pop_first()) case Component(): - self.elements[first].getValue(cref.last()) + return self.elements[first].getValue(cref.last()) case _: raise ValueError(f"Element '{first}' in system '{self.name}' is neither a System nor a Component or a Connector") + def getElement(self, cref: CRef): + if cref is None: + return self + + first = cref.first() + element = self.elements.get(first) + + if isinstance(element, System): + if cref.is_root(): + return element + return element.getElement(cref.pop_first()) + + if isinstance(element, (Component, ComponentTable)): + return element + + raise ValueError(f"Element '{first}' in system '{self.name}' is neither a System nor a Component") + def mapParameter(self, cref: CRef, source: str, target: str): if cref is None: self.parameterMapping.mapParameter(source, target) @@ -648,7 +913,7 @@ def setSolver(self, cref: CRef, name: str): raise ValueError(f"Component '{first}' not found in {self.name}") self.elements[first].setSolver(name) - def generateJson(self, resources: dict | None = None, tempdir : str | None = None, startTime = None, stopTime = None) -> str: + def generateJson(self, resources: dict | None = None, tempdir : str | None = None, simulation_info: dict | None = None) -> str: """Instantiates the system and its components.""" data = { "simulation units": [] @@ -677,19 +942,34 @@ def generateJson(self, resources: dict | None = None, tempdir : str | None = Non unit["solver"] = { "name": solver, "method": solver_config.get("method"), - "tolerance": solver_config.get("tolerance"), - "stepSize": solver_config.get("stepSize") + "relativeTolerance": solver_config.get("relativeTolerance"), + "initialStepSize": solver_config.get("initialStepSize"), + "minimumStepSize": solver_config.get("minimumStepSize"), + "maximumStepSize": solver_config.get("maximumStepSize"), + "fixedStepSize": solver_config.get("fixedStepSize") } else: raise ValueError(f"Solver '{solver}' not found in solver list.") data["simulation units"].append(unit) + # Validate: at most one WC-method solver unit is allowed (nested WC under WC is not supported) + _WC_METHODS = {"oms_ma", "oms_mav", "oms_mav2"} + wc_units = [u for u in data["simulation units"] if u.get("solver", {}).get("method") in _WC_METHODS] + if len(wc_units) > 1: + wc_names = [u["solver"]["name"] for u in wc_units] + raise ValueError( + f"Multiple WC solver units found: {wc_names}. " + f"Nested WC systems are not supported. All CS FMUs without an explicit solver " + f"share one WC unit automatically." + ) + # Add top-level simulation metadata - data["result file"] = "simulation_result.csv" data["simulation settings"] = { - "start time": startTime, - "stop time": stopTime, - "tolerance": 1e-6 + "start time": simulation_info.get("startTime"), + "stop time": simulation_info.get("stopTime"), + "result file": simulation_info.get("resultFile"), + "logging interval": simulation_info.get("loggingInterval"), + "buffer size": simulation_info.get("bufferSize") } # Dump JSON @@ -715,9 +995,27 @@ def processElements(self, elements_dict: dict, connections: list, data: dict, so self.solvers.append({ "name": solver_name, "method": "cvode", - "tolerance": 1e-4 }) element.solver = solver_name + ## default CS FMUs with no solver to oms_ma + elif element.solver is None: + solver_name = "oms_ma_default" + if not any(s.get("name") == solver_name for s in self.solvers): + self.solvers.append({ + "name": solver_name, + "method": "oms_ma" + }) + element.solver = solver_name + ## CS FMUs must not use SC solvers (cvode/euler) + elif fmuType == "cs": + _SC_METHODS = {"cvode", "euler"} + assigned_solver = next((s for s in self.solvers if s.get("name") == element.solver), None) + if assigned_solver and assigned_solver.get("method") in _SC_METHODS: + raise ValueError( + f"CS FMU '{element.name}' is assigned solver '{element.solver}' " + f"with method '{assigned_solver.get('method')}', but CS FMUs can only use " + f"WC master algorithms (oms_ma, oms_mav, oms_mav2)." + ) ## add connectors info for the component in the json, this is needed for propagating connector geomtery to capi connector_info = [] @@ -727,35 +1025,23 @@ def processElements(self, elements_dict: dict, connections: list, data: dict, so "causality": connector.causality.name if connector.causality else None, "type": connector.signal_type.name if connector.signal_type else None, }) - ## add connector geometry if available - if connector.connectorGeometry: - connector_info[-1]["geometry"] = { - "x": connector.connectorGeometry.x if connector.connectorGeometry else None, - "y": connector.connectorGeometry.y if connector.connectorGeometry else None - } solver_groups[element.solver].append({ "name": [self.name] + ([systemName] if systemName else []) + [str(element.name)], "type": fmuType, "path": str(Path(tempdir, str(element.fmuPath))) if tempdir is not None else str(element.fmuPath), "connectors": connector_info }) - ## add element geometry if available, this is needed for propagating element geometry to capi - if element.elementgeometry: - element_geometry = { - "x1": element.elementgeometry.x1, - "y1": element.elementgeometry.y1, - "x2": element.elementgeometry.x2, - "y2": element.elementgeometry.y2, - "rotation": element.elementgeometry.rotation, - "iconSource": element.elementgeometry.icon_source, - "iconRotation": element.elementgeometry.icon_rotation, - "iconFlip": element.elementgeometry.icon_flip, - "iconFixedAspectRatio": element.elementgeometry.icon_fixed_aspect_ratio - } - solver_groups[element.solver][-1]["element geometry"] = element_geometry - componentSolver[str(element.name)] = element.solver elif isinstance(element, ComponentTable): + ## default tables with no solver to oms_ma + if element.solver is None: + solver_name = "oms_ma_default" + if not any(s.get("name") == solver_name for s in self.solvers): + self.solvers.append({ + "name": solver_name, + "method": "oms_ma" + }) + element.solver = solver_name ## add connectors info for the component in the json, this is needed for propagating connector geomtery to capi connector_info = [] for connector in element.connectors: @@ -800,13 +1086,6 @@ def processElements(self, elements_dict: dict, connections: list, data: dict, so "factor": connection.linearTransformation.factor, "offset": connection.linearTransformation.offset } - ## add connection geometry if available - if connection.connectionGeometry: - connection_info["connection geometry"] = { - "pointsX": connection.connectionGeometry.pointsX, - "pointsY": connection.connectionGeometry.pointsY - } - solver_connections[solver].append(connection_info) def export(self, root): @@ -818,8 +1097,9 @@ def export(self, root): for connector in self.connectors: connector.exportToSSD(connectors_node) - if self.elementgeometry: - self.elementgeometry.exportToSSD(node) + ## export element geometry if available for system, subsystem and components + if self.elementgeometry: + self.elementgeometry.exportToSSD(node) ## export top level parameter bindings self.value.exportToSSD(node, self.parameterMapping) @@ -865,6 +1145,6 @@ def export(self, root): ## export ssd annotations if self.solvers: - utils.exportAnnotations(node, self.solvers) + utils.exportSimulationInformation(node, self.solvers) return node diff --git a/src/OMSimulatorPython/utils.py b/src/OMSimulatorPython/utils.py index 98c7706ef..ecfed12a2 100644 --- a/src/OMSimulatorPython/utils.py +++ b/src/OMSimulatorPython/utils.py @@ -61,7 +61,12 @@ def parseDefaultExperiment(node, root): return root.startTime = default_experiment.get("startTime") root.stopTime = default_experiment.get("stopTime") - ##TODO parse ssd:annotation + simulation_info = parseSimulationInformation(default_experiment) + if simulation_info: + for info in simulation_info: + root.resultFile = info.get("resultFile", root.resultFile) + root.loggingInterval = info.get("loggingInterval", root.loggingInterval) + root.bufferSize = info.get("bufferSize", root.bufferSize) def parseElements(node, resources = None): @@ -87,10 +92,10 @@ def parseElements(node, resources = None): parseParameterBindings(system, elements[name], resources) ## check for meta tag parseMetaData(system, elements[name], resources) - solvers = parseAnnotations(system) - if solvers: - for solver in solvers: - elements[name].solver = solver.get("name") + simulationInformation = parseSimulationInformation(system) + if simulationInformation: + for info in simulationInformation: + elements[name].solver = info.get("name") elements[name].elements = parseElements(system, resources) # recursively parse nested elements in the sub-system Connection.importFromNode(system, elements[name]) # parse connections for the sub-system @@ -114,18 +119,21 @@ def parseElements(node, resources = None): elements[name] = Component(name, source) elements[name].implementation = implementation elements[name].description = description + if resources is not None: + elements[name].fmu = resources.get(source) # attach FMU for modeldescription start value fallback in getValue elements[name].connectors = Connector.importFromNode(component) elements[name].elementgeometry = ElementGeometry.importFromNode(component) parseParameterBindings(component, elements[name], resources) ## check for meta tag parseMetaData(component, elements[name], resources) - solvers = parseAnnotations(component) - if solvers: - for solver in solvers: - elements[name].solver = solver.get("name") + simulationInformation = parseSimulationInformation(component) + if simulationInformation: + for info in simulationInformation: + elements[name].solver = info.get("name") elif (comp_type == "application/table" or comp_type == "text/csv"): elements[name] = ComponentTable(name, source) elements[name].connectors = Connector.importFromNode(component) + elements[name].elementgeometry = ElementGeometry.importFromNode(component) return elements @@ -243,28 +251,28 @@ def validateSSP(root, filename : str, schema_file : str): # warn the user instead of raising an exception warnings.warn(message, UserWarning) -def exportAnnotations(node, solvers): - """Export annotations to the XML node""" +def exportSimulationInformation(node, simulationInformation): + """Export simulation information annotations to the XML node""" ssd_annotation_node = ET.SubElement(node, namespace.tag("ssd", "Annotations")) annotation_node = ET.SubElement(ssd_annotation_node, namespace.tag("ssc", "Annotation")) annotation_node.set("type", "org.openmodelica") oms_annotation_node = ET.SubElement(annotation_node, namespace.tag("oms", "Annotations")) - if (isinstance(solvers, list)): - for solver in solvers: + if (isinstance(simulationInformation, list)): + for info in simulationInformation: oms_simulationInformation_node = ET.SubElement(oms_annotation_node, namespace.tag("oms", "SimulationInformation")) - for key, value in solver.items(): + for key, value in info.items(): oms_simulationInformation_node.set(key, str(value)) else: oms_simulationInformation_node = ET.SubElement(oms_annotation_node, namespace.tag("oms", "SimulationInformation")) - oms_simulationInformation_node.set("name", solvers) + oms_simulationInformation_node.set("name", simulationInformation) -def parseAnnotations(node): +def parseSimulationInformation(node): """Extract and print system annotations""" annotations_node = node.find("ssd:Annotations", namespaces=namespace.ns) if annotations_node is None: return [] - solvers = [] + simulationInformation = [] for annotation in annotations_node.findall("ssc:Annotation", namespaces=namespace.ns): type = annotation.get("type") if type == "org.openmodelica": @@ -272,6 +280,6 @@ def parseAnnotations(node): if oms_annotation is not None: oms_simulationInformation = oms_annotation.findall("oms:SimulationInformation", namespaces=namespace.ns) for sim in oms_simulationInformation: - solvers.append(sim.attrib) + simulationInformation.append(sim.attrib) - return solvers + return simulationInformation diff --git a/src/OMSimulatorPython/values.py b/src/OMSimulatorPython/values.py index b4bcd05fd..e1f7e6b2e 100644 --- a/src/OMSimulatorPython/values.py +++ b/src/OMSimulatorPython/values.py @@ -46,7 +46,7 @@ def setValue(self, name, value, type : SignalType | None, unit=None, description self.start_values[name] = (value, type, unit, description) def getValue(self, name): - return self.start_values.get(name) + return self.start_values.get(name)[0] if name in self.start_values else None def empty(self) -> bool: return not self.start_values diff --git a/src/OMSimulatorServer/CMakeLists.txt b/src/OMSimulatorServer/CMakeLists.txt index 5ee47cd3a..58f95f79f 100644 --- a/src/OMSimulatorServer/CMakeLists.txt +++ b/src/OMSimulatorServer/CMakeLists.txt @@ -1,4 +1,6 @@ project(OMSimulatorServer) -install(FILES OMSimulatorServer.py - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/OMSimulator/scripts) +install(FILES + OMSimulatorSimulationServer.py + OMSimulatorGuiServer.py + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/OMSimulator/scripts) diff --git a/src/OMSimulatorServer/OMSimulatorGuiServer.py b/src/OMSimulatorServer/OMSimulatorGuiServer.py new file mode 100644 index 000000000..eac2b9826 --- /dev/null +++ b/src/OMSimulatorServer/OMSimulatorGuiServer.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 + +# This file is part of OpenModelica. +# +# Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC), +# c/o Linköpings universitet, Department of Computer and Information Science, +# SE-58183 Linköping, Sweden. +# +# All rights reserved. +# +# THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR +# THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. +# ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES +# RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL +# VERSION 3, ACCORDING TO RECIPIENTS CHOICE. +# +# The OpenModelica software and the OSMC (Open Source Modelica Consortium) +# Public License (OSMC-PL) are obtained from OSMC, either from the above +# address, from the URLs: +# http://www.openmodelica.org or +# https://github.com/OpenModelica/ or +# http://www.ida.liu.se/projects/OpenModelica, +# and in the OpenModelica distribution. +# +# GNU AGPL version 3 is obtained from: +# https://www.gnu.org/licenses/licenses.html#GPL +# +# This program is distributed WITHOUT ANY WARRANTY; without +# even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH +# IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. +# +# See the full OSMC Public License conditions for more details. + + +import argparse +import traceback +import zmq +from OMSimulator import SSP, CRef, System, Component, ComponentTable, Connector, Causality, SignalType +from OMSimulator.ssd import SSD +from OMSimulator.elementgeometry import ElementGeometry +from OMSimulator.connector import ConnectorGeometry +from OMSimulator.connection import ConnectionGeometry + +'''OMSimulator GUI Server for modelling SSP files interactively from OMEdit.''' +class OMSGuiServer: + def __init__(self, endpoint): + self._context = zmq.Context() + self._socket = self._context.socket(zmq.REP) + self._socket.connect(endpoint) + self._poller = zmq.Poller() + self._poller.register(self._socket, zmq.POLLIN) + + # Multiple SSP models can be open simultaneously.Keyed by model name (activeVariant.name) + self.models = {} # model_name -> SSP + + # ----------------------------------- + # main loop + # ----------------------------------- + def run(self): + try: + while True: + socks = dict(self._poller.poll(500)) + if self._socket not in socks: + continue + msg = self._socket.recv_json() + if "batch" in msg: + for cmd in msg["batch"]: + self.handle(cmd) + reply = {"status": "ack"} + else: + try: + reply = self.handle(msg) + except Exception as e: + print(f"Unhandled exception in handle({msg.get('method', '?')}): {e}", flush=True) + traceback.print_exc() + reply = {"status": "failed", "method": msg.get("method", ""), "error": str(e)} + self._socket.send_json(reply) + if reply.get("status") == "shutdown": + self.close() + break + except KeyboardInterrupt: + print("Ctrl+C received, shutting down...", flush=True) + finally: + self.close() + + # ----------------------------------- + # dispatcher + # ----------------------------------- + def handle(self, msg): + method = msg.get("method") + args = msg.get("args", {}) + model_name = msg.get("model") # every command carries the target model name + try: + return self._dispatch(method, args, model_name) + except Exception as e: + traceback.print_exc() + return {"status": "failed", "method": method or "unknown", "error": str(e)} + + def _get_model(self, model_name): + """Return the SSP for model_name, or raise a descriptive error.""" + model = self.models.get(model_name) + if model is None: + raise ValueError(f"Model '{model_name}' not found. Available: {list(self.models.keys())}") + return model + + def _dispatch(self, method, args, model_name=None): + + # ---------- getVersion ---------- + if method == "getVersion": + import OMSimulator as oms + return {"status": "ok", "method": method, "version": "OMSimulator v" + oms.__version__} + + # ---------- new model ---------- + # These methods don't require an existing model — handle before _get_model(). + if method == "newModel": + name = args.get("name", "default") + ssp = SSP() + # SSP() always creates a variant keyed as "default" — re-key it to the requested name. + ssd = ssp.variants.pop("default") + ssd.name = name + ssd.resultFile = name + "_res.mat" + ssp.variants[name] = ssd + ssp.activeVariantName = name + ssp.activeVariant.system.name = args.get("system_name", "default") + self.models[name] = ssp + return {"status": "ok", "method": method} + + # ---------- import file ---------- + if method == "importFile": + ssp = SSP(args["file"]) + name = ssp.activeVariant.name + if name in self.models: + return {"status": "fail", "method": method, "modelName": name, + "error": f"Model '{name}' is already loaded."} + self.models[name] = ssp + return {"status": "ok", "method": method, "modelName": name} + + # ---------- delete model ---------- + if method == "deleteModel": + if model_name in self.models: + del self.models[model_name] + return {"status": "ok", "method": method} + + # ---------- shutdown ---------- + if method == "shutdown": + self.models.clear() + return {"status": "shutdown"} + + # All methods below require an existing model — look it up once. + model = self._get_model(model_name) + + # --------- add system ---------- + if method == "addSystem": + cref_parts = list(args["cref"]) + root_name = model.activeVariant.system.name + # OMEdit sends ["Root", "subsystem"]; Python SSP expects ["modelName", "subsystem"] + if cref_parts and cref_parts[0] == root_name: + cref_parts = [model_name] + cref_parts[1:] + elif cref_parts and cref_parts[0] != model_name: + cref_parts = [model_name] + cref_parts + cref = CRef(*cref_parts) + model.addSystem(cref) + return {"status": "ok", "method": method} + + # ---------- export snapshot ---------- + if method == "exportSnapshot": + xml = model.activeVariant.export(filename=None) + return {"status": "ok", "method": method, "xml": xml} + + # ---------- import snapshot ---------- + if method == "importSnapshot": + snapshot_xml = args.get("snapshot", "") + # Parse the snapshot XML into a new SSD, reusing existing resources (FMUs). + new_ssd = SSD.importFromString(snapshot_xml, model.resources) + new_name = new_ssd.name + # Root system name — mirrors OMS2's new_root_cref return so C++ can build + # the correct newEditedCref when the root system is renamed in the text editor. + new_root_cref = new_ssd.system.name if new_ssd.system is not None else "" + old_name = model.activeVariantName + # Replace the variant in the SSP variants dict. + del model.variants[old_name] + model.variants[new_name] = new_ssd + model._activeVariantName = new_name + # If the top-level model name changed, re-key self.models. + if new_name != model_name: + self.models[new_name] = self.models.pop(model_name) + #print(f"importSnapshot: '{model_name}' -> '{new_name}' (root: '{new_root_cref}')", flush=True) + return {"status": "ok", "method": method, "modelName": new_name, "rootCref": new_root_cref} + + # ---------- export to file ---------- + if method == "export": + model.export(args["file"]) + return {"status": "ok", "method": method} + + # ---------- add resource ---------- + if method == "addResource": + model.addResource(args["source"], new_name=args["new_name"]) + return {"status": "ok", "method": method} + + # ---------- delete ---------- + if method == "delete": + cref_parts = list(args["cref"]) + # Empty cref means the top-level model itself should be deleted. + if not cref_parts: + del self.models[model_name] + else: + cref = CRef(*cref_parts) + model.delete(cref) + return {"status": "ok", "method": method} + + # ---------- setValue ---------- + if method == "setValue": + cref = CRef(*args["cref"]) + model.setValue(cref, args["value"]) + return {"status": "ok", "method": method} + + # ---------- getValue ---------- + if method == "getValue": + cref = CRef(*args["cref"]) + value = model.getValue(cref) + return {"status": "ok", "method": method, "value": str(value)} + + # ---------- get/set start time ---------- + if method == "getStartTime": + return {"status": "ok", "method": method, "value": str(model.activeVariant.startTime)} + + if method == "setStartTime": + model.activeVariant.startTime = args["value"] + return {"status": "ok", "method": method} + + # ---------- get/set stop time ---------- + if method == "getStopTime": + return {"status": "ok", "method": method, "value": str(model.activeVariant.stopTime)} + + if method == "setStopTime": + model.activeVariant.stopTime = args["value"] + return {"status": "ok", "method": method} + + # ---------- get/set result file ---------- + if method == "getResultFile": + return {"status": "ok", "method": method, "fileName": model.activeVariant.resultFile, "bufferSize": str(model.activeVariant.bufferSize)} + + if method == "setResultFile": + model.activeVariant.resultFile = args.get("fileName", "model_res.mat") + model.activeVariant.bufferSize = args.get("bufferSize", 10) + return {"status": "ok", "method": method} + + if method == "setLoggingInterval": + model.activeVariant.loggingInterval = args.get("loggingInterval", 0.0) + return {"status": "ok", "method": method} + + # ---------- getTolerance---------- + if method == "getTolerance": + return {"status": "ok", "method": method, "value": str(model.activeVariant.tolerance)} + + # ---------- setTolerance ---------- + if method == "setTolerance": + model.activeVariant.tolerance = args["value"] + return {"status": "ok", "method": method} + + ## ---------- get/set step size ---------- + if method == "getVariableStepSize": + return {"status": "ok", "method": method, "initialStepSize": str(model.activeVariant.initialStepSize), "minimumStepSize": str(model.activeVariant.minimumStepSize), "maximumStepSize": str(model.activeVariant.maximumStepSize)} + + if method == "setVariableStepSize": + model.activeVariant.initialStepSize = args["initialStepSize"] + model.activeVariant.minimumStepSize = args["minimumStepSize"] + model.activeVariant.maximumStepSize = args["maximumStepSize"] + return {"status": "ok", "method": method} + + ## --------- get/set fixed step size ---------- + if method == "getFixedStepSize": + return {"status": "ok", "method": method, "value": str(model.activeVariant.maximumStepSize)} + + ## --------- set fixed step size (sets variable step size with equal initial, min, max) ---------- + if method == "setFixedStepSize": + step_size = args["value"] + model.activeVariant.initialStepSize = step_size + model.activeVariant.minimumStepSize = step_size + model.activeVariant.maximumStepSize = step_size + return {"status": "ok", "method": method} + + # ---------- add component ---------- + if method == "addComponent": + ## add resources only if not already present, to avoid unnecessary copying of FMUs when the same one is instantiated multiple times. + if model.resources.get(args["new_name"]) is None: + model.addResource(args["source"], new_name=args["new_name"]) + cref = CRef(*args["cref"]) + model.addComponent(cref, args["new_name"]) + return {"status": "ok", "method": method} + + # ---------- replace component ---------- + if method == "replaceComponent": + dry_run = args.get("dryRun", False) + if model.resources.get(args["new_name"]) is None: + model.addResource(args["source"], new_name=args["new_name"]) + cref = CRef(*args["cref"]) + warnings = model.replaceComponent(cref, args["new_name"], dryRun=dry_run) + return {"status": "ok", "method": method, "warnings": warnings or []} + + # ---------- add connector ---------- + if method == "addConnector": + CAUSALITY_MAP = { + "Parameter": Causality.parameter, + "Input": Causality.input, + "Output": Causality.output, + } + SIGNALTYPE_MAP = { + "Real": SignalType.Real, + "Integer": SignalType.Integer, + "Boolean": SignalType.Boolean, + "String": SignalType.String, + "Enum": SignalType.Enumeration, + } + causality = CAUSALITY_MAP[args["causality"]] + signal_type = SIGNALTYPE_MAP[args["type"]] + cref = CRef(*args["cref"]) + model.addConnector(cref, Connector(args["name"], causality, signal_type)) + return {"status": "ok", "method": method} + + # ---------- get elements ---------- + if method == "getElements": + json_elements = self.serializeElement(model.activeVariant.system, model) + return {"status": "ok", "method": method, "elements": [json_elements]} + + # ---------- set element geometry ---------- + if method == "setElementGeometry": + cref = CRef(*args["cref"]) + root_name = model.activeVariant.system.name + if cref.is_root() and str(cref.first()) == root_name: + element = model.activeVariant.system + else: + element = model.getElement(cref) + g = args["geometry"] + element.elementgeometry = ElementGeometry( + x1=g.get("x1", -10.0), y1=g.get("y1", -10.0), + x2=g.get("x2", 10.0), y2=g.get("y2", 10.0), + rotation=g.get("rotation", 0.0), + icon_source=g.get("iconSource"), + icon_rotation=g.get("iconRotation", 0.0), + icon_flip=g.get("iconFlip", False), + icon_fixed_aspect_ratio=g.get("iconFixedAspectRatio", False), + ) + return {"status": "ok", "method": method} + + # ---------- set connector geometry ---------- + if method == "setConnectorGeometry": + cref = CRef(*args["cref"]) + connector = model.getConnector(cref) + if connector is None: + return {"status": "failed", "method": method, "error": f"Connector '{cref}' not found"} + g = args["geometry"] + connector.connectorGeometry = ConnectorGeometry(x=g.get("x", 0.5), y=g.get("y", 0.5)) + return {"status": "ok", "method": method} + + # ---------- set connection geometry ---------- + if method == "setConnectionGeometry": + crefA = CRef(*args["crefA"]) + crefB = CRef(*args["crefB"]) + g = args["geometry"] + connection = model.getConnection(crefA, crefB) + if connection is not None: + connection.connectionGeometry = ConnectionGeometry(g.get("pointsX", []), g.get("pointsY", [])) + return {"status": "ok", "method": method} + + # ---------- add connection ---------- + if method == "addConnection": + crefA = CRef(*args["crefA"]) + crefB = CRef(*args["crefB"]) + model.addConnection(crefA, crefB) + return {"status": "ok", "method": method} + + # ---------- delete connection ---------- + if method == "deleteConnection": + crefA = CRef(*args["crefA"]) + crefB = CRef(*args["crefB"]) + model.deleteConnection(crefA, crefB) + return {"status": "ok", "method": method} + + # ---------- solver settings ---------- + if method == "getSolverSettings": + # Return all named solver configs and per-component assignments. + solver_list = [] + for solver in model.activeVariant.system.solvers: + solver_dict = {} + for key, value in solver.items(): + solver_dict[str(key)] = str(value) + solver_list.append(solver_dict) + + assignments = {} + for elem_name, element in model.activeVariant.system.elements.items(): + if hasattr(element, 'solver') and element.solver: + assignments[str(elem_name)] = str(element.solver) + if isinstance(element, System): + for comp_name, comp in element.elements.items(): + if hasattr(comp, 'solver') and comp.solver: + assignments[str(comp_name)] = str(comp.solver) + + return {"status": "ok", "method": method, "solvers": solver_list, "assignments": assignments} + + if method == "setSolverSettings": + # Replace all solver configs and re-apply component assignments. + model.activeVariant.system.solvers = [] + for solver in args.get("solvers", []): + model.newSolver(solver) + for comp_name, solver_name in args.get("assignments", {}).items(): + comp_ref = comp_name.split(".")[1:] # assuming comp_name is like "subsystem.component" + cref = CRef(*comp_ref) + model.setSolver(cref, solver_name) + return {"status": "ok", "method": method} + + # ---------- rename ---------- + if method == "rename": + cref_parts = list(args.get("cref", [])) + new_name = args["newName"] + if not cref_parts: + # Top-level model rename: re-key self.models and update the SSD name. + if new_name != model_name: + model.activeVariant.name = new_name + model._activeVariantName = new_name + model.variants[new_name] = model.variants.pop(model_name) + self.models[new_name] = self.models.pop(model_name) + else: + # Component/subsystem rename — delegate to the SSP API. + model.rename(CRef(*cref_parts), CRef(new_name)) + return {"status": "ok", "method": method} + + # ---------- component-level solver assignment ---------- + if method == "setSolver": + cref = CRef(*args["cref"]) + model.setSolver(cref, args["solver"]) + return {"status": "ok", "method": method} + + return {"status": "failed", "method": method, "error": f"Unknown method: {method!r}"} + + # ----------------------------------- + # serialization helpers + # ----------------------------------- + def serializeElementGeometry(self, element): + g = getattr(element, "elementgeometry", None) + if g is None: + return {"x1": -10.0, "y1": -10.0, "x2": 10.0, "y2": 10.0, + "rotation": 0.0, "iconSource": None, "iconRotation": 0.0, + "iconFlip": False, "iconFixedAspectRatio": False} + return { + "x1": g.x1 if g.x1 is not None else -10.0, + "y1": g.y1 if g.y1 is not None else -10.0, + "x2": g.x2 if g.x2 is not None else 10.0, + "y2": g.y2 if g.y2 is not None else 10.0, + "rotation": g.rotation if g.rotation is not None else 0.0, + "iconSource": g.icon_source, + "iconRotation": g.icon_rotation if g.icon_rotation is not None else 0.0, + "iconFlip": g.icon_flip if g.icon_flip is not None else False, + "iconFixedAspectRatio": g.icon_fixed_aspect_ratio if g.icon_fixed_aspect_ratio is not None else False, + } + + def serializeConnectors(self, element): + raw = list(getattr(element, "connectors", [])) + inputs = [c for c in raw if c.causality and c.causality.name.lower() == "input"] + outputs = [c for c in raw if c.causality and c.causality.name.lower() == "output"] + + def default_y(index, count): + return float(index + 1) / float(count + 1) + + result = [] + for c in raw: + causality = c.causality.name.lower() if c.causality else "" + signal_type = c.signal_type.name.lower() if c.signal_type else "" + geom = getattr(c, "connectorGeometry", None) + if geom: + x, y = (geom.x if geom.x is not None else 0.5), (geom.y if geom.y is not None else 0.5) + elif causality == "input": + x, y = 0.0, default_y(inputs.index(c), len(inputs)) + elif causality == "output": + x, y = 1.0, default_y(outputs.index(c), len(outputs)) + else: + x, y = 0.5, 0.5 + result.append({ + "type": "connector", "name": str(c.name), + "causality": causality, "signalType": signal_type, + "geometry": {"x": x, "y": y} + }) + return result + + def getFMUInfo(self, element, model): + fmu_inst = model.resources.get(element.fmuPath) + if not fmu_inst: + return {} + fields = [ + "description", "fmiVersion", "generationTool", "guid", + "generationDateAndTime", "modelName", + "canBeInstantiatedOnlyOncePerProcess", "canGetAndSetFMUstate", + "canNotUseMemoryManagementFunctions", "canSerializeFMUstate", + "completedIntegratorStepNotNeeded", "needsExecutionTool", + "providesDirectionalDerivative", "canInterpolateInputs", + "maxOutputDerivativeOrder", + ] + info = {f: getattr(fmu_inst, f, None) for f in fields} + info["fmiKind"] = getattr(fmu_inst, "fmuType", None) + info["path"] = str(getattr(fmu_inst, "fmuPath", "")) + return info + + def serializeConnectionGeometry(self, connection): + g = getattr(connection, "connectionGeometry", None) + if g is None: + return {"pointsX": [], "pointsY": []} + return { + "pointsX": list(g.pointsX) if g.pointsX is not None else [], + "pointsY": list(g.pointsY) if g.pointsY is not None else [], + } + + def serializeConnections(self, system): + result = [] + for conn in getattr(system, "connections", []): + result.append({ + "conA": ".".join([str(conn.startElement), str(conn.startConnector)]), + "conB": ".".join([str(conn.endElement), str(conn.endConnector)]), + "type": "connection", + "geometry": self.serializeConnectionGeometry(conn) + }) + return result + + def serializeElement(self, element, model): + if element is None: + return + node = { + "name": str(element._name) if isinstance(element, System) else str(element.name), + "type": "system" if isinstance(element, System) else ("componenttable" if isinstance(element, ComponentTable) else "component"), + "elements": [], + "connectors": self.serializeConnectors(element), + } + if isinstance(element, (System, Component, ComponentTable)): + node["geometry"] = self.serializeElementGeometry(element) + if isinstance(element, Component): + node["fmuInfo"] = self.getFMUInfo(element, model) + if isinstance(element, ComponentTable): + node["filePath"] = str(element.filePath) + if isinstance(element, System): + node["connections"] = self.serializeConnections(element) + for child in element.elements.values(): + node["elements"].append(self.serializeElement(child, model)) + return node + + # ----------------------------------- + # close + # ----------------------------------- + def close(self): + if self._socket is not None and not self._socket.closed: + print("OMS server shutting down", flush=True) + self._socket.close() + self._context.term() + self._socket = None + self._context = None + +def _main(): + parser = argparse.ArgumentParser(description='OMS-GUI-SERVER', allow_abbrev=False) + parser.add_argument('--endpoint-rep', default=None) + args = parser.parse_args() + server = OMSGuiServer(args.endpoint_rep) + server.run() + +if __name__ == "__main__": + _main() diff --git a/src/OMSimulatorServer/OMSimulatorServer.py b/src/OMSimulatorServer/OMSimulatorSimulationServer.py similarity index 77% rename from src/OMSimulatorServer/OMSimulatorServer.py rename to src/OMSimulatorServer/OMSimulatorSimulationServer.py index 8679c5489..5a130f15b 100755 --- a/src/OMSimulatorServer/OMSimulatorServer.py +++ b/src/OMSimulatorServer/OMSimulatorSimulationServer.py @@ -37,15 +37,15 @@ import argparse import json import math -import sys import threading import time +import sys -import OMSimulator as oms +from OMSimulator import SSP, CRef, Capi import zmq __author__ = 'Lennart Ochel ' -__version__ = oms.__version__ +__version__ = Capi.getVersion() __copyright__ = '''\ Copyright (c) 2018-CurrentYear, Open Source Modelica Consortium (OSMC), c/o Linköpings universitet, Department of Computer and Information Science, @@ -68,7 +68,7 @@ def __init__(self, model, result_file, interactive, endpoint_pub, endpoint_rep): self._alive = True self._context = zmq.Context() - self._model = oms.importFile(model) + self._model = SSP(model) self._mutex = threading.Lock() self._pause = interactive self._socket_rep = None @@ -79,9 +79,11 @@ def __init__(self, model, result_file, interactive, endpoint_pub, endpoint_rep): if result_file: self._model.resultFile = result_file + #self.print(f'Model loaded: {self._model.list()}') # extract all available signals - self._signals = self._model.getAllSignals() - + #self._signals = self._model.getAllSignals() + #self._signals = [CRef('test', 'step', 'height'), CRef('test', 'step', 'offset'), CRef('test', 'step', 'y')] + self._signals = [] if interactive and not endpoint_rep: self.print('flag --endpoint-rep is mandatory in interactive simulation mode') sys.exit(1) @@ -160,11 +162,21 @@ def _main(self): def run(self): self.pub_msg('status', {'progress': 0}) - time_ = self._model.time - startTime = self._model.startTime - stopTime = self._model.stopTime - self._model.instantiate() - self._model.initialize() + inst_model = self._model.instantiate() + inst_model.setResultFile(self._model.activeVariant.resultFile) + + time_ = float(inst_model.getTime()) + startTime = float(self._model.activeVariant.startTime) + stopTime = float(self._model.activeVariant.stopTime) + + inst_model.setStartTime(startTime) + inst_model.setStopTime(stopTime) + + inst_model.initialize() + + # for signal in self._signals: + # print("enable signal", signal, flush=True) + # print(f"Value for signal {signal}: {inst_model.getValue(signal)}") while True: if self._pause: @@ -179,22 +191,24 @@ def run(self): if self._activeSignals: results = {'time': time_} for signal in self._activeSignals: - type_ = self._signals[signal]['type'] - if type_ == 'Real': - results[signal] = oms.getReal(signal) - elif type_ == 'Integer': - results[signal] = oms.getInteger(signal) - elif type_ == 'Boolean': - results[signal] = oms.getBoolean(signal) + # type_ = self._signals[signal]['type'] + # if type_ == 'Real': + # results[signal] = oms.getReal(signal) + # elif type_ == 'Integer': + # results[signal] = oms.getInteger(signal) + # elif type_ == 'Boolean': + # results[signal] = oms.getBoolean(signal) + results[signal] = inst_model.getValue(signal) self.pub_msg('results', results) with self._mutex: - self._model.doStep() - time_ = self._model.time + inst_model.doStep() + time_ = float(inst_model.getTime()) + #print(f"time: {time_} : {stopTime}", flush=True) if time_ >= stopTime and self._alive: break - self._model.terminate() - self._model.delete() + inst_model.terminate() + inst_model.delete() self.pub_msg('status', {'progress': 100}) def _main(): @@ -208,13 +222,19 @@ def _main(): parser.add_argument('--option', default='', nargs='+', help='defines optional command line options') parser.add_argument('--result-file', default=None, help='defines the result file') parser.add_argument('--temp', default=None, help='defines the temp directory') + parser.add_argument('--log-file', default=None, help='defines the log file') + parser.add_argument('--working-directory', default=None, help='defines the working directory') args = parser.parse_args() - oms.setCommandLineOption(' '.join(list(map((lambda x: x[1:-1]), args.option)))) - oms.setLoggingLevel(args.logLevel) + Capi.setCommandLineOption(' '.join(list(map((lambda x: x[1:-1]), args.option)))) + Capi.setLoggingLevel(args.logLevel) if args.temp: - oms.setTempDirectory(args.temp) + Capi.setTempDirectory(args.temp) + if args.log_file: + Capi.setLogFile(args.log_file) + if args.working_directory: + Capi.setWorkingDirectory(args.working_directory) server = Server(args.model, args.result_file, args.interactive, args.endpoint_pub, args.endpoint_rep) # run simulation thread diff --git a/testsuite/AircraftVehicleDemonstrator/AircraftVehicleDemonstrator.py b/testsuite/AircraftVehicleDemonstrator/AircraftVehicleDemonstrator.py index 20d34b15a..20f8c01e4 100644 --- a/testsuite/AircraftVehicleDemonstrator/AircraftVehicleDemonstrator.py +++ b/testsuite/AircraftVehicleDemonstrator/AircraftVehicleDemonstrator.py @@ -195,7 +195,7 @@ ## model.root.consumer_B ## model.root.consumer_A ## model.root.cockpit -## info: Result file: AircraftVehicleDemonstrator_res.mat (bufferSize=1) +## info: Result file: AircraftVehicleDemonstrator_res.mat (bufferSize=10) ## info: 6 warnings ## info: 0 errors ## endResult diff --git a/testsuite/AircraftVehicleDemonstrator/acvs_simulate.py b/testsuite/AircraftVehicleDemonstrator/acvs_simulate.py index 4508bc4f8..bb496a1c4 100644 --- a/testsuite/AircraftVehicleDemonstrator/acvs_simulate.py +++ b/testsuite/AircraftVehicleDemonstrator/acvs_simulate.py @@ -51,7 +51,7 @@ ## model.root.ecs_hw ## model.root.consumer ## model.root.adaption -## info: Result file: acvs_res.csv (bufferSize=1) +## info: Result file: acvs_res.csv (bufferSize=10) ## info: 2 warnings ## info: 0 errors ## endResult diff --git a/testsuite/AircraftVehicleDemonstrator/embrace1.py b/testsuite/AircraftVehicleDemonstrator/embrace1.py index f15c6924d..bc925f7e1 100644 --- a/testsuite/AircraftVehicleDemonstrator/embrace1.py +++ b/testsuite/AircraftVehicleDemonstrator/embrace1.py @@ -61,7 +61,7 @@ instantiated_model.delete() ## Result: -## info: Result file: embrace1_res.mat (bufferSize=1) +## info: Result file: embrace1_res.mat (bufferSize=10) ## info: Initialize: ## info: embrace.root.ECS_HW.coolinPackAir.looptype: 2 ## info: embrace.root.ECS_HW.eCS.MaxCoolPower.k : 5.0 diff --git a/testsuite/AircraftVehicleDemonstrator/embrace2.py b/testsuite/AircraftVehicleDemonstrator/embrace2.py index 91d4025a3..1c0cb93ca 100644 --- a/testsuite/AircraftVehicleDemonstrator/embrace2.py +++ b/testsuite/AircraftVehicleDemonstrator/embrace2.py @@ -65,7 +65,7 @@ instantiated_model.delete() ## Result: -## info: Result file: embrace2_res.mat (bufferSize=1) +## info: Result file: embrace2_res.mat (bufferSize=10) ## info: Initialize: ## info: embrace.root.ECS_HW.coolinPackAir.looptype: 2 ## info: embrace.root.ECS_HW.eCS.MaxCoolPower.k : 5.0 diff --git a/testsuite/api/Makefile b/testsuite/api/Makefile index 926d25b4c..28fadca48 100644 --- a/testsuite/api/Makefile +++ b/testsuite/api/Makefile @@ -23,6 +23,8 @@ NewSSP.py \ NewSSP2.py \ NewSSP3.py \ ResultReader.py \ +rename1.py \ +ReplaceComponent1.py \ setMappingSSM1.py \ setMappingSSM2.py \ setMappingSSM3.py \ diff --git a/testsuite/api/ReplaceComponent1.py b/testsuite/api/ReplaceComponent1.py new file mode 100644 index 000000000..8ae9dc84f --- /dev/null +++ b/testsuite/api/ReplaceComponent1.py @@ -0,0 +1,121 @@ +## status: correct +## teardown_command: rm -rf ReplaceComponent1.ssp +## linux: yes +## ucrt64: yes +## win: yes +## mac: yes + + +from OMSimulator import SSP, CRef, Settings + +Settings.suppressPath = True + + +# This example creates a new SSP file with an FMU instantiated as a component and sets two differents solver for the components and the system. +# It then exports the SSP file and re-imports it to verify the solver settings and the simulates the model. + +model = SSP() +model.addResource('../resources/replaceA.fmu', new_name='resources/replaceA.fmu') +model.addResource('../resources/replaceB.fmu', new_name='resources/replaceB.fmu') +model.addResource('../resources/replaceA_extended.fmu', new_name='resources/replaceA_extended.fmu') + +model.addComponent(CRef('default', 'A'), 'resources/replaceA.fmu') +model.addComponent(CRef('default', 'B'), 'resources/replaceB.fmu') + +model.setValue(CRef('default', 'A', 'u'), 10.0) +model.setValue(CRef('default', 'B', 'u1'), -13.0) +model.setValue(CRef('default', 'A', 't'), -10.0) +model.setValue(CRef('default', 'B', 'z'), -15.0) + + +model.addConnection(CRef('default', 'A', 'y'), CRef('default', 'B', 'u')) +model.addConnection(CRef('default', 'A', 'dummy'), CRef('default', 'B', 'u1')) + +model.export('ReplaceComponent1.ssp') + +model2 = SSP('ReplaceComponent1.ssp') +model2.list() + +print("Info : After replacement", flush=True) +warnings = model2.replaceComponent(CRef('default', 'A'), 'resources/replaceA_extended.fmu') +model2.list() +for warning in warnings: + print("Warning :", warning, flush=True) + +## Result: +## +## |-- Resources: +## |-- resources/replaceA.fmu +## |-- resources/replaceA_extended.fmu +## |-- resources/replaceB.fmu +## |-- Active Variant: default +## |-- +## |-- Variant "default": +## |-- |-- System: default 'None' +## |-- |-- |-- Connectors: +## |-- |-- |-- Elements: +## |-- |-- |-- |-- FMU: A 'None' +## |-- |-- |-- |-- |-- path: resources/replaceA.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (dummy, Causality.output, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (t, Causality.parameter, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real u, 10.0, None, 'None') +## |-- |-- |-- |-- |-- |-- (Real t, -10.0, None, 'None') +## |-- |-- |-- |-- FMU: B 'None' +## |-- |-- |-- |-- |-- path: resources/replaceB.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (u1, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (z, Causality.parameter, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real u1, -13.0, None, 'None') +## |-- |-- |-- |-- |-- |-- (Real z, -15.0, None, 'None') +## |-- |-- |-- Connections: +## |-- |-- |-- |-- A.y -> B.u +## |-- |-- |-- |-- A.dummy -> B.u1 +## |-- DefaultExperiment +## |-- |-- startTime: 0.0 +## |-- |-- stopTime: 1.0 +## Info : After replacement +## +## |-- Resources: +## |-- resources/replaceA.fmu +## |-- resources/replaceA_extended.fmu +## |-- resources/replaceB.fmu +## |-- Active Variant: default +## |-- +## |-- Variant "default": +## |-- |-- System: default 'None' +## |-- |-- |-- Connectors: +## |-- |-- |-- Elements: +## |-- |-- |-- |-- FMU: A 'None' +## |-- |-- |-- |-- |-- path: resources/replaceA_extended.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (y1, Causality.output, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (foo, Causality.parameter, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real u, 10.0, None, 'None') +## |-- |-- |-- |-- FMU: B 'None' +## |-- |-- |-- |-- |-- path: resources/replaceB.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (u1, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- |-- (z, Causality.parameter, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real u1, -13.0, None, 'None') +## |-- |-- |-- |-- |-- |-- (Real z, -15.0, None, 'None') +## |-- |-- |-- Connections: +## |-- |-- |-- |-- A.y -> B.u +## |-- DefaultExperiment +## |-- |-- startTime: 0.0 +## |-- |-- stopTime: 1.0 +## Warning : deleting connection "A.dummy ==> B.u1", as signal "dummy" couldn't be resolved to any signal in the replaced submodel "resources/replaceA_extended.fmu" +## Warning : deleting start value "A.t" in "inline" resources, because the identifier couldn't be resolved to any system signal in the replacing model +## endResult diff --git a/testsuite/api/rename1.py b/testsuite/api/rename1.py new file mode 100644 index 000000000..488a90497 --- /dev/null +++ b/testsuite/api/rename1.py @@ -0,0 +1,169 @@ +## status: correct +## teardown_command: +## linux: yes +## ucrt64: yes +## win: yes +## mac: yes + +from OMSimulator import SSP, CRef, Settings, Connector, Causality, SignalType + +Settings.suppressPath = True + + +# This example creates a new SSP file with an FMU instantiated as a component and sets two differents solver for the components and the system. +# It then exports the SSP file and re-imports it to verify the solver settings and the simulates the model. + +model = SSP() +model.addResource('../resources/Modelica.Blocks.Math.Add.fmu', new_name='resources/Add.fmu') +model.addResource('../resources/Modelica.Blocks.Math.Gain.fmu', new_name='resources/Gain.fmu') +## add top level system connector +model.activeVariant.system.addConnector(Connector('input1', Causality.parameter, SignalType.Real)) +## add subsystem +model.addSystem(CRef('default', 'sub-system')) +## add top level sub-system connector +model.activeVariant.system.elements[CRef('sub-system')].addConnector(Connector('input2', Causality.input, SignalType.Real)) + +model.setValue(CRef('default', 'input1'), 5.0) +model.setValue(CRef('default', 'sub-system', 'input2'), 10.0) +model.addComponent(CRef('default', 'Add1'), 'resources/Add.fmu') +model.addComponent(CRef('default', 'Gain1'), 'resources/Gain.fmu') +model.addComponent(CRef('default', 'sub-system', 'Add1'), 'resources/Add.fmu') +model.addComponent(CRef('default', 'sub-system', 'Gain1'), 'resources/Gain.fmu') + +model.addConnection(CRef('default', 'input1'), CRef('default', 'Add1', 'u1')) +model.addConnection(CRef('default', 'Gain1', 'y'), CRef('default', 'Add1', 'u1')) +model.addConnection(CRef('default', 'sub-system', 'input2'), CRef('default', 'sub-system', 'Add1', 'u1')) +model.addConnection(CRef('default', 'sub-system', 'Gain1', 'y'), CRef('default', 'sub-system', 'Add1', 'u1')) + +model.list() +model.rename(CRef('default', 'input1'), 'I1') +model.rename(CRef('default', 'sub-system', 'input2'), 'I2') +model.rename(CRef('default', 'Add1'), 'Adder1') +model.rename(CRef('default', 'Gain1'), 'Multiplier1') +model.rename(CRef('default', 'sub-system', 'Add1'), 'Adder2') +model.rename(CRef('default', 'sub-system', 'Gain1'), 'Multiplier2') +print(f"info: After renaming components and connectors:") +model.list() + + +## Result: +## +## |-- Resources: +## |-- resources/Add.fmu +## |-- resources/Gain.fmu +## |-- Active Variant: default +## |-- +## |-- Variant "default": None +## |-- |-- System: default 'None' +## |-- |-- |-- Connectors: +## |-- |-- |-- |-- (input1, Causality.parameter, SignalType.Real, None, 'None') +## |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- (Real input1, 5.0, None, 'None') +## |-- |-- |-- Elements: +## |-- |-- |-- |-- System: sub-system 'None' +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (input2, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real input2, 10.0, None, 'None') +## |-- |-- |-- |-- |-- Elements: +## |-- |-- |-- |-- |-- |-- FMU: Add1 'None' +## |-- |-- |-- |-- |-- |-- |-- path: resources/Add.fmu +## |-- |-- |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- |-- |-- (u1, Causality.input, SignalType.Real, None, 'Connector of Real input signal 1') +## |-- |-- |-- |-- |-- |-- |-- |-- (u2, Causality.input, SignalType.Real, None, 'Connector of Real input signal 2') +## |-- |-- |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Connector of Real output signal') +## |-- |-- |-- |-- |-- |-- |-- |-- (k1, Causality.parameter, SignalType.Real, None, 'Gain of input signal 1') +## |-- |-- |-- |-- |-- |-- |-- |-- (k2, Causality.parameter, SignalType.Real, None, 'Gain of input signal 2') +## |-- |-- |-- |-- |-- |-- FMU: Gain1 'None' +## |-- |-- |-- |-- |-- |-- |-- path: resources/Gain.fmu +## |-- |-- |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'Input signal connector') +## |-- |-- |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Output signal connector') +## |-- |-- |-- |-- |-- |-- |-- |-- (k, Causality.parameter, SignalType.Real, 1, 'Gain value multiplied with input signal') +## |-- |-- |-- |-- |-- Connections: +## |-- |-- |-- |-- |-- |-- .input2 -> Add1.u1 +## |-- |-- |-- |-- |-- |-- Gain1.y -> Add1.u1 +## |-- |-- |-- |-- FMU: Add1 'None' +## |-- |-- |-- |-- |-- path: resources/Add.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u1, Causality.input, SignalType.Real, None, 'Connector of Real input signal 1') +## |-- |-- |-- |-- |-- |-- (u2, Causality.input, SignalType.Real, None, 'Connector of Real input signal 2') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Connector of Real output signal') +## |-- |-- |-- |-- |-- |-- (k1, Causality.parameter, SignalType.Real, None, 'Gain of input signal 1') +## |-- |-- |-- |-- |-- |-- (k2, Causality.parameter, SignalType.Real, None, 'Gain of input signal 2') +## |-- |-- |-- |-- FMU: Gain1 'None' +## |-- |-- |-- |-- |-- path: resources/Gain.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'Input signal connector') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Output signal connector') +## |-- |-- |-- |-- |-- |-- (k, Causality.parameter, SignalType.Real, 1, 'Gain value multiplied with input signal') +## |-- |-- |-- Connections: +## |-- |-- |-- |-- .input1 -> Add1.u1 +## |-- |-- |-- |-- Gain1.y -> Add1.u1 +## |-- UnitDefinitions: +## |-- |-- Unit: 1 +## |-- |-- |-- BaseUnit: +## |-- DefaultExperiment +## |-- |-- startTime: 0.0 +## |-- |-- stopTime: 1.0 +## info: After renaming components and connectors: +## +## |-- Resources: +## |-- resources/Add.fmu +## |-- resources/Gain.fmu +## |-- Active Variant: default +## |-- +## |-- Variant "default": None +## |-- |-- System: default 'None' +## |-- |-- |-- Connectors: +## |-- |-- |-- |-- (I1, Causality.parameter, SignalType.Real, None, 'None') +## |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- (Real I1, 5.0, None, 'None') +## |-- |-- |-- Elements: +## |-- |-- |-- |-- System: sub-system 'None' +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (I2, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real I2, 10.0, None, 'None') +## |-- |-- |-- |-- |-- Elements: +## |-- |-- |-- |-- |-- |-- FMU: Adder2 'None' +## |-- |-- |-- |-- |-- |-- |-- path: resources/Add.fmu +## |-- |-- |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- |-- |-- (u1, Causality.input, SignalType.Real, None, 'Connector of Real input signal 1') +## |-- |-- |-- |-- |-- |-- |-- |-- (u2, Causality.input, SignalType.Real, None, 'Connector of Real input signal 2') +## |-- |-- |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Connector of Real output signal') +## |-- |-- |-- |-- |-- |-- |-- |-- (k1, Causality.parameter, SignalType.Real, None, 'Gain of input signal 1') +## |-- |-- |-- |-- |-- |-- |-- |-- (k2, Causality.parameter, SignalType.Real, None, 'Gain of input signal 2') +## |-- |-- |-- |-- |-- |-- FMU: Multiplier2 'None' +## |-- |-- |-- |-- |-- |-- |-- path: resources/Gain.fmu +## |-- |-- |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'Input signal connector') +## |-- |-- |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Output signal connector') +## |-- |-- |-- |-- |-- |-- |-- |-- (k, Causality.parameter, SignalType.Real, 1, 'Gain value multiplied with input signal') +## |-- |-- |-- |-- |-- Connections: +## |-- |-- |-- |-- |-- |-- .I2 -> Adder2.u1 +## |-- |-- |-- |-- |-- |-- Multiplier2.y -> Adder2.u1 +## |-- |-- |-- |-- FMU: Adder1 'None' +## |-- |-- |-- |-- |-- path: resources/Add.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u1, Causality.input, SignalType.Real, None, 'Connector of Real input signal 1') +## |-- |-- |-- |-- |-- |-- (u2, Causality.input, SignalType.Real, None, 'Connector of Real input signal 2') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Connector of Real output signal') +## |-- |-- |-- |-- |-- |-- (k1, Causality.parameter, SignalType.Real, None, 'Gain of input signal 1') +## |-- |-- |-- |-- |-- |-- (k2, Causality.parameter, SignalType.Real, None, 'Gain of input signal 2') +## |-- |-- |-- |-- FMU: Multiplier1 'None' +## |-- |-- |-- |-- |-- path: resources/Gain.fmu +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (u, Causality.input, SignalType.Real, None, 'Input signal connector') +## |-- |-- |-- |-- |-- |-- (y, Causality.output, SignalType.Real, None, 'Output signal connector') +## |-- |-- |-- |-- |-- |-- (k, Causality.parameter, SignalType.Real, 1, 'Gain value multiplied with input signal') +## |-- |-- |-- Connections: +## |-- |-- |-- |-- .I1 -> Adder1.u1 +## |-- |-- |-- |-- Multiplier1.y -> Adder1.u1 +## |-- UnitDefinitions: +## |-- |-- Unit: 1 +## |-- |-- |-- BaseUnit: +## |-- DefaultExperiment +## |-- |-- startTime: 0.0 +## |-- |-- stopTime: 1.0 +## endResult diff --git a/testsuite/dcmotor/dcmotor.py b/testsuite/dcmotor/dcmotor.py index 304a8e0f0..e13b5ad9e 100644 --- a/testsuite/dcmotor/dcmotor.py +++ b/testsuite/dcmotor/dcmotor.py @@ -210,7 +210,7 @@ ## warning: [fmi2Warning] SuT_emachine_model/: fmi2SetupExperiment: tolerance control not supported for fmuType fmi2ModelExchange, setting toleranceDefined to fmi2False ## warning: [fmi2Warning] SuT_edrive_mass/: fmi2SetupExperiment: tolerance control not supported for fmuType fmi2ModelExchange, setting toleranceDefined to fmi2False ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: dc_motor_res.mat (bufferSize=1) +## info: Result file: dc_motor_res.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 10663 NumRhsEvals = 10833 NumLinSolvSetups = 606 ## NumNonlinSolvIters = 10832 NumNonlinSolvConvFails = 0 NumErrTestFails = 26 diff --git a/testsuite/dcmotor/dcmotorMETest.py b/testsuite/dcmotor/dcmotorMETest.py index 7154564ec..f72651adf 100644 --- a/testsuite/dcmotor/dcmotorMETest.py +++ b/testsuite/dcmotor/dcmotorMETest.py @@ -21,7 +21,7 @@ model.addComponent(CRef('default', 'emachine_model'), 'resources/emachine_model.fmu') model.addComponent(CRef('default', 'stimuli_model'), 'resources/stimuli_model.fmu') -solver1 = {'name' : 'solver1', 'method': 'cvode', 'tolerance': 1e-4} +solver1 = {'name' : 'solver1', 'method': 'cvode', 'relativeTolerance': 1e-4} model.newSolver(solver1) model.setSolver(CRef('default', 'edrive_mass'), 'solver1') @@ -44,7 +44,7 @@ ## warning: [fmi2Warning] emachine_model/: fmi2SetupExperiment: tolerance control not supported for fmuType fmi2ModelExchange, setting toleranceDefined to fmi2False ## warning: [fmi2Warning] edrive_mass/: fmi2SetupExperiment: tolerance control not supported for fmuType fmi2ModelExchange, setting toleranceDefined to fmi2False ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: dc_motor_me_res.mat (bufferSize=1) +## info: Result file: dc_motor_me_res.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 10001 NumRhsEvals = 10002 NumLinSolvSetups = 501 ## NumNonlinSolvIters = 10001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/parker/ParkerSimSimulation.py b/testsuite/parker/ParkerSimSimulation.py index a67ec12f7..6ed122ff2 100644 --- a/testsuite/parker/ParkerSimSimulation.py +++ b/testsuite/parker/ParkerSimSimulation.py @@ -30,7 +30,7 @@ ## info: Alg. loop (size 13/57) ## model.root.valve_model ## model.root.interface_model -## info: Result file: parker_simulation_res.mat (bufferSize=1) +## info: Result file: parker_simulation_res.mat (bufferSize=10) ## Setting hidden to 1 ## Setting hidden to 1 ## Setting hidden to 1 diff --git a/testsuite/reference-fmus/3.0/BouncingBall.py b/testsuite/reference-fmus/3.0/BouncingBall.py index 58ed6b913..d5d6e4757 100644 --- a/testsuite/reference-fmus/3.0/BouncingBall.py +++ b/testsuite/reference-fmus/3.0/BouncingBall.py @@ -72,12 +72,12 @@ ## Result: ## Loading FMI version 3... -## info: Result file: BouncingBall-cs.mat (bufferSize=1) +## info: Result file: BouncingBall-cs.mat (bufferSize=10) ## signal h is equal ## signal v is equal ## Loading FMI version 3... ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: BouncingBall-me.mat (bufferSize=1) +## info: Result file: BouncingBall-me.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 503 NumRhsEvals = 504 NumLinSolvSetups = 27 ## NumNonlinSolvIters = 503 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/reference-fmus/3.0/Dahlquist.py b/testsuite/reference-fmus/3.0/Dahlquist.py index 26828557c..8798557ba 100644 --- a/testsuite/reference-fmus/3.0/Dahlquist.py +++ b/testsuite/reference-fmus/3.0/Dahlquist.py @@ -62,11 +62,11 @@ ## Result: ## Loading FMI version 3... -## info: Result file: Dahlquist-cs.mat (bufferSize=1) +## info: Result file: Dahlquist-cs.mat (bufferSize=10) ## signal x is equal ## Loading FMI version 3... ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: Dahlquist-me.mat (bufferSize=1) +## info: Result file: Dahlquist-me.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 10001 NumRhsEvals = 10002 NumLinSolvSetups = 501 ## NumNonlinSolvIters = 10001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/reference-fmus/3.0/Resource.py b/testsuite/reference-fmus/3.0/Resource.py index d69186d92..f459adb8c 100644 --- a/testsuite/reference-fmus/3.0/Resource.py +++ b/testsuite/reference-fmus/3.0/Resource.py @@ -53,12 +53,12 @@ ## Result: ## Loading FMI version 3... -## info: Result file: Resource-cs.mat (bufferSize=1) +## info: Result file: Resource-cs.mat (bufferSize=10) ## info: Resource.y: 97 ## Loading FMI version 3... ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: Resource-me.mat (bufferSize=1) +## info: Result file: Resource-me.mat (bufferSize=10) ## info: Resource.y: 97 ## info: Final Statistics for 'model.root': ## NumSteps = 10001 NumRhsEvals = 10002 NumLinSolvSetups = 501 diff --git a/testsuite/reference-fmus/3.0/Stair.py b/testsuite/reference-fmus/3.0/Stair.py index 2b3c2efef..194276432 100644 --- a/testsuite/reference-fmus/3.0/Stair.py +++ b/testsuite/reference-fmus/3.0/Stair.py @@ -65,12 +65,12 @@ ## Result: ## Loading FMI version 3... -## info: Result file: Stair-cs.mat (bufferSize=1) +## info: Result file: Stair-cs.mat (bufferSize=10) ## signal counter is equal ## Loading FMI version 3... ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.200000 -## info: Result file: Stair-me.mat (bufferSize=1) +## info: Result file: Stair-me.mat (bufferSize=10) ## info: Simulation terminated by FMU model.root.Stair at time 9.000000 ## info: Final Statistics for 'model.root': ## NumSteps = 0 NumRhsEvals = 0 NumLinSolvSetups = 0 diff --git a/testsuite/reference-fmus/3.0/VanDerPol.py b/testsuite/reference-fmus/3.0/VanDerPol.py index cb1e9c19a..9bf70a835 100644 --- a/testsuite/reference-fmus/3.0/VanDerPol.py +++ b/testsuite/reference-fmus/3.0/VanDerPol.py @@ -73,12 +73,12 @@ ## Result: ## Loading FMI version 3... -## info: Result file: VanDerPol-cs.mat (bufferSize=1) +## info: Result file: VanDerPol-cs.mat (bufferSize=10) ## signal x0 is equal ## signal x1 is equal ## Loading FMI version 3... ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: VanDerPol-me.mat (bufferSize=1) +## info: Result file: VanDerPol-me.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 20002 NumRhsEvals = 20004 NumLinSolvSetups = 1002 ## NumNonlinSolvIters = 20003 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/reference-fmus/BouncingBall.py b/testsuite/reference-fmus/BouncingBall.py index df1253f93..dc98de6f3 100644 --- a/testsuite/reference-fmus/BouncingBall.py +++ b/testsuite/reference-fmus/BouncingBall.py @@ -42,7 +42,7 @@ model2.addResource('../resources/BouncingBall.fmu', new_name='resources/BouncingBall.fmu') model2.addComponent(CRef('default', 'BouncingBall'), 'resources/BouncingBall.fmu') -solver2 = {'name' : 'solver2', 'method': 'cvode', 'tolerance': 1e-5} +solver2 = {'name' : 'solver2', 'method': 'cvode', 'relativeTolerance': 1e-5} model2.newSolver(solver2) model2.setSolver(CRef('default', 'BouncingBall'), 'solver2') @@ -70,11 +70,11 @@ ## Result: -## info: Result file: BouncingBall-cs.mat (bufferSize=1) +## info: Result file: BouncingBall-cs.mat (bufferSize=10) ## signal h is equal ## signal v is equal ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: BouncingBall-me.mat (bufferSize=1) +## info: Result file: BouncingBall-me.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 503 NumRhsEvals = 504 NumLinSolvSetups = 27 ## NumNonlinSolvIters = 503 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/reference-fmus/Dahlquist.py b/testsuite/reference-fmus/Dahlquist.py index 67f8e5966..309dd02be 100644 --- a/testsuite/reference-fmus/Dahlquist.py +++ b/testsuite/reference-fmus/Dahlquist.py @@ -58,10 +58,10 @@ ## Result: -## info: Result file: Dahlquist-cs.mat (bufferSize=1) +## info: Result file: Dahlquist-cs.mat (bufferSize=10) ## signal x is equal ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: Dahlquist-me.mat (bufferSize=1) +## info: Result file: Dahlquist-me.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 10001 NumRhsEvals = 10002 NumLinSolvSetups = 501 ## NumNonlinSolvIters = 10001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/reference-fmus/Feedthrough.py b/testsuite/reference-fmus/Feedthrough.py index 3b31ef6d3..7dfda88d7 100644 --- a/testsuite/reference-fmus/Feedthrough.py +++ b/testsuite/reference-fmus/Feedthrough.py @@ -72,7 +72,7 @@ ## Result: -## info: Result file: Feedthrough-cs.mat (bufferSize=1) +## info: Result file: Feedthrough-cs.mat (bufferSize=10) ## info: Parameter default.Feedthrough.String_parameter will not be stored in the result file, because the signal type is not supported ## info: Feedthrough.Boolean_output: True ## info: Feedthrough.Enumeration_output: 2 @@ -81,7 +81,7 @@ ## info: Feedthrough.Int32_output: 5 ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: Feedthrough-me.mat (bufferSize=1) +## info: Result file: Feedthrough-me.mat (bufferSize=10) ## info: Parameter default.Feedthrough.String_parameter will not be stored in the result file, because the signal type is not supported ## info: Feedthrough.Boolean_output: True ## info: Feedthrough.Enumeration_output: 2 diff --git a/testsuite/reference-fmus/Resource.py b/testsuite/reference-fmus/Resource.py index 2c1ef95a5..ab1f7dc24 100644 --- a/testsuite/reference-fmus/Resource.py +++ b/testsuite/reference-fmus/Resource.py @@ -52,11 +52,11 @@ instantiated_model.delete() ## Result: -## info: Result file: Resource-cs.mat (bufferSize=1) +## info: Result file: Resource-cs.mat (bufferSize=10) ## info: Resource.y: 97 ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: Resource-me.mat (bufferSize=1) +## info: Result file: Resource-me.mat (bufferSize=10) ## info: Resource.y: 97 ## info: Final Statistics for 'model.root': ## NumSteps = 10001 NumRhsEvals = 10002 NumLinSolvSetups = 501 diff --git a/testsuite/reference-fmus/Stair.py b/testsuite/reference-fmus/Stair.py index 8451f43fe..b2c3b62f5 100644 --- a/testsuite/reference-fmus/Stair.py +++ b/testsuite/reference-fmus/Stair.py @@ -38,7 +38,7 @@ model2.addResource('../resources/Stair.fmu', new_name='resources/Stair.fmu') model2.addComponent(CRef('default', 'Stair'), 'resources/Stair.fmu') -solver2 = {'name' : 'solver2', 'method': 'cvode', 'tolerance': 1e-5, 'stepSize': 0.2} +solver2 = {'name' : 'solver2', 'method': 'cvode', 'relativeTolerance': 1e-5, 'maximumStepSize': 0.2} model2.newSolver(solver2) model2.setSolver(CRef('default', 'Stair'), 'solver2') @@ -62,12 +62,12 @@ ## Result: -## info: Result file: Stair-cs.mat (bufferSize=1) +## info: Result file: Stair-cs.mat (bufferSize=10) ## info: fmi2_doStep discarded for FMU "model.root.Stair" ## signal counter is equal ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.200000 -## info: Result file: Stair-me.mat (bufferSize=1) +## info: Result file: Stair-me.mat (bufferSize=10) ## info: Simulation terminated by FMU model.root.Stair at time 9.000000 ## info: Final Statistics for 'model.root': ## NumSteps = 0 NumRhsEvals = 0 NumLinSolvSetups = 0 diff --git a/testsuite/reference-fmus/VanDerPol.py b/testsuite/reference-fmus/VanDerPol.py index 393c119be..e4ad6d851 100644 --- a/testsuite/reference-fmus/VanDerPol.py +++ b/testsuite/reference-fmus/VanDerPol.py @@ -71,11 +71,11 @@ ## Result: -## info: Result file: VanDerPol-cs.mat (bufferSize=1) +## info: Result file: VanDerPol-cs.mat (bufferSize=10) ## signal x0 is equal ## signal x1 is equal ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: VanDerPol-me.mat (bufferSize=1) +## info: Result file: VanDerPol-me.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 20002 NumRhsEvals = 20004 NumLinSolvSetups = 1002 ## NumNonlinSolvIters = 20003 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/simulation/DualMassOscillator.py b/testsuite/simulation/DualMassOscillator.py index a2701fd6d..d000df64d 100644 --- a/testsuite/simulation/DualMassOscillator.py +++ b/testsuite/simulation/DualMassOscillator.py @@ -52,7 +52,7 @@ instantiated_model.delete() ## Result: -## info: Result file: DualMassOscillator_res.mat (bufferSize=1) +## info: Result file: DualMassOscillator_res.mat (bufferSize=10) ## info: Initialization ## info: default.system1.x1: 0.0 ## info: default.system2.x2: 0.5 diff --git a/testsuite/simulation/FMI3_SimpleSimulation1.py b/testsuite/simulation/FMI3_SimpleSimulation1.py index c088e4567..027886e58 100644 --- a/testsuite/simulation/FMI3_SimpleSimulation1.py +++ b/testsuite/simulation/FMI3_SimpleSimulation1.py @@ -60,7 +60,7 @@ ## |-- |-- stopTime: 1.0 ## ## Loading FMI version 3... -## info: Result file: FMI3_SimpleSimulation1_res.mat (bufferSize=1) +## info: Result file: FMI3_SimpleSimulation1_res.mat (bufferSize=10) ## info: After simulation: ## info: default.BouncingBall.g: -9.81 ## info: default.BouncingBall.e: 0.7 diff --git a/testsuite/simulation/FMI3_SimpleSimulation2.py b/testsuite/simulation/FMI3_SimpleSimulation2.py index ec441334c..809802163 100644 --- a/testsuite/simulation/FMI3_SimpleSimulation2.py +++ b/testsuite/simulation/FMI3_SimpleSimulation2.py @@ -72,7 +72,7 @@ ## |-- |-- stopTime: 1.0 ## ## Loading FMI version 3... -## info: Result file: FMI3_SimpleSimulation2_res.mat (bufferSize=1) +## info: Result file: FMI3_SimpleSimulation2_res.mat (bufferSize=10) ## info: After simulation: ## info: default.BouncingBall.g: 10.81 ## info: default.BouncingBall.e: 0.9 diff --git a/testsuite/simulation/FMI3_SimpleSimulation3.py b/testsuite/simulation/FMI3_SimpleSimulation3.py index 8b90d59ce..aad3f7274 100644 --- a/testsuite/simulation/FMI3_SimpleSimulation3.py +++ b/testsuite/simulation/FMI3_SimpleSimulation3.py @@ -70,7 +70,7 @@ ## |-- |-- stopTime: 1.0 ## ## Loading FMI version 3... -## info: Result file: FMI3_SimpleSimulation3_res.mat (bufferSize=1) +## info: Result file: FMI3_SimpleSimulation3_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1: 10.81 ## info: default.BouncingBall.g: 10.81 diff --git a/testsuite/simulation/LinearTransformation1.py b/testsuite/simulation/LinearTransformation1.py index 374928571..41308b6ab 100644 --- a/testsuite/simulation/LinearTransformation1.py +++ b/testsuite/simulation/LinearTransformation1.py @@ -61,7 +61,7 @@ ## |-- |-- startTime: 0.0 ## |-- |-- stopTime: 1.0 ## -## info: Result file: LinearTransformation1_res.mat (bufferSize=1) +## info: Result file: LinearTransformation1_res.mat (bufferSize=10) ## info: After simulation: ## info: default.Gain1.y: 0.0 ## info: default.Add1.u1: 3.0 diff --git a/testsuite/simulation/LinearTransformation2.py b/testsuite/simulation/LinearTransformation2.py index 077e67a12..d50d887fb 100644 --- a/testsuite/simulation/LinearTransformation2.py +++ b/testsuite/simulation/LinearTransformation2.py @@ -73,7 +73,7 @@ ## ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: LinearTransformation2_res.mat (bufferSize=1) +## info: Result file: LinearTransformation2_res.mat (bufferSize=10) ## info: After simulation: ## info: default.Gain1.y: 0.0 ## info: default.Add1.u1: 3.0 diff --git a/testsuite/simulation/LinearTransformation3.py b/testsuite/simulation/LinearTransformation3.py index c99f388e4..3059130e3 100644 --- a/testsuite/simulation/LinearTransformation3.py +++ b/testsuite/simulation/LinearTransformation3.py @@ -87,7 +87,7 @@ ## info: default.Gain1.u: 14.0 ## info: default.Gain1.y: 182.0 ## info: default.Add1.u1: 0.0 -## info: Result file: LinearTransformation3_res.mat (bufferSize=1) +## info: Result file: LinearTransformation3_res.mat (bufferSize=10) ## info: After simulation: ## info: default.Gain1.k: 13.0 ## info: default.Gain1.u: 14.0 diff --git a/testsuite/simulation/Makefile b/testsuite/simulation/Makefile index d3820f3b9..5c1b71d38 100644 --- a/testsuite/simulation/Makefile +++ b/testsuite/simulation/Makefile @@ -28,6 +28,8 @@ SimpleSimulation12.py \ SimpleSimulation13.py \ SimpleSimulation14.py \ SimpleSimulation15.py \ +SimpleSimulation16.py \ +SimpleSimulation17.py \ stepUntil.py \ FMI3_SimpleSimulation1.py \ FMI3_SimpleSimulation2.py \ diff --git a/testsuite/simulation/NestedSystemSimulation1.py b/testsuite/simulation/NestedSystemSimulation1.py index d2175ea22..72c7c81c3 100644 --- a/testsuite/simulation/NestedSystemSimulation1.py +++ b/testsuite/simulation/NestedSystemSimulation1.py @@ -164,7 +164,7 @@ ## info: default.sub-system1.Gain1.u: 0.0 ## info: default.sub-system1.Gain1.y: 0.0 ## info: default.sub-system1.Add1.u1: 0.0 -## info: Result file: NestedSystemSimulation1_res.mat (bufferSize=1) +## info: Result file: NestedSystemSimulation1_res.mat (bufferSize=10) ## info: After simulation: ## info: default.input1: 3.0 ## info: default.param1: 0.0 diff --git a/testsuite/simulation/NestedSystemSimulation2.py b/testsuite/simulation/NestedSystemSimulation2.py index 674b800bf..1acb258f1 100644 --- a/testsuite/simulation/NestedSystemSimulation2.py +++ b/testsuite/simulation/NestedSystemSimulation2.py @@ -189,7 +189,7 @@ ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root.solver2': 0.001000 ## info: model doesn't contain any continuous state -## info: Result file: NestedSystemSimulation2_res.mat (bufferSize=1) +## info: Result file: NestedSystemSimulation2_res.mat (bufferSize=10) ## info: After simulation: ## info: default.input1: 3.0 ## info: default.param1: 0.0 diff --git a/testsuite/simulation/NestedSystemSimulation3.py b/testsuite/simulation/NestedSystemSimulation3.py index 8838c301f..34349c5d1 100644 --- a/testsuite/simulation/NestedSystemSimulation3.py +++ b/testsuite/simulation/NestedSystemSimulation3.py @@ -187,7 +187,7 @@ ## info: default.sub-system1.Add1.u1: 0.0 ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: NestedSystemSimulation3_res.mat (bufferSize=1) +## info: Result file: NestedSystemSimulation3_res.mat (bufferSize=10) ## info: After simulation: ## info: default.input1: 3.0 ## info: default.param1: 0.0 diff --git a/testsuite/simulation/PIController.py b/testsuite/simulation/PIController.py index e6d5e9ff2..dacf0465a 100644 --- a/testsuite/simulation/PIController.py +++ b/testsuite/simulation/PIController.py @@ -226,7 +226,7 @@ ## |-- |-- startTime: 0.0 ## |-- |-- stopTime: 1.0 ## -## info: Result file: PI_Controller.mat (bufferSize=1) +## info: Result file: PI_Controller.mat (bufferSize=10) ## info: Parameter settings ## info: default.addP.k1 : 1.0 ## info: default.addP.k2 : -1.0 diff --git a/testsuite/simulation/QuarterCarModel.py b/testsuite/simulation/QuarterCarModel.py index 1898694fd..a8bf00fcd 100644 --- a/testsuite/simulation/QuarterCarModel.py +++ b/testsuite/simulation/QuarterCarModel.py @@ -51,5 +51,5 @@ ## Result: -## info: Result file: QuarterCarModel.DisplacementDisplacement_res.mat (bufferSize=1) +## info: Result file: QuarterCarModel.DisplacementDisplacement_res.mat (bufferSize=10) ## endResult diff --git a/testsuite/simulation/SimpleSimulation1.py b/testsuite/simulation/SimpleSimulation1.py index 407035808..13f2e9dbd 100644 --- a/testsuite/simulation/SimpleSimulation1.py +++ b/testsuite/simulation/SimpleSimulation1.py @@ -108,7 +108,7 @@ ## |-- |-- stopTime: 4.0 ## ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation1_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation1_res.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 3001 NumRhsEvals = 3002 NumLinSolvSetups = 151 ## NumNonlinSolvIters = 3001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/simulation/SimpleSimulation10.py b/testsuite/simulation/SimpleSimulation10.py index 41c0f8b3b..22e45d510 100644 --- a/testsuite/simulation/SimpleSimulation10.py +++ b/testsuite/simulation/SimpleSimulation10.py @@ -197,8 +197,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation10_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation10_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -210,7 +210,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation11.py b/testsuite/simulation/SimpleSimulation11.py index 52221b1cb..12bb6eeab 100644 --- a/testsuite/simulation/SimpleSimulation11.py +++ b/testsuite/simulation/SimpleSimulation11.py @@ -190,8 +190,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation11_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation11_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -203,7 +203,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation12.py b/testsuite/simulation/SimpleSimulation12.py index 9bb27547b..b7a3f34dd 100644 --- a/testsuite/simulation/SimpleSimulation12.py +++ b/testsuite/simulation/SimpleSimulation12.py @@ -247,8 +247,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation12_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation12_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -260,7 +260,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation13.py b/testsuite/simulation/SimpleSimulation13.py index 4cc5aaa86..8cd1848ac 100644 --- a/testsuite/simulation/SimpleSimulation13.py +++ b/testsuite/simulation/SimpleSimulation13.py @@ -174,8 +174,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation13_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation13_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -187,7 +187,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation14.py b/testsuite/simulation/SimpleSimulation14.py index 81b8c8c6e..0f329c9e3 100644 --- a/testsuite/simulation/SimpleSimulation14.py +++ b/testsuite/simulation/SimpleSimulation14.py @@ -185,8 +185,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation14_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation14_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -198,7 +198,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation15.py b/testsuite/simulation/SimpleSimulation15.py index e21237230..c21de5840 100644 --- a/testsuite/simulation/SimpleSimulation15.py +++ b/testsuite/simulation/SimpleSimulation15.py @@ -59,7 +59,7 @@ instantiated_model.delete() ## Result: -## info: Result file: SimpleSimulation15_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation15_res.mat (bufferSize=10) ## ## |-- Resources: ## |-- resources/Add.fmu diff --git a/testsuite/simulation/SimpleSimulation16.py b/testsuite/simulation/SimpleSimulation16.py index 110f9757e..6af3f4b31 100644 --- a/testsuite/simulation/SimpleSimulation16.py +++ b/testsuite/simulation/SimpleSimulation16.py @@ -99,7 +99,7 @@ ## |-- |-- startTime: 0.0 ## |-- |-- stopTime: 1.0 ## -## info: Result file: SimpleSimulation16_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation16_res.mat (bufferSize=10) ## default.Sine.y : -2.4492127076447545e-16 ## default.sub-system.u : -2.4492127076447545e-16 ## default.sub-system.Add2.u1: -2.4492127076447545e-16 diff --git a/testsuite/simulation/SimpleSimulation17.py b/testsuite/simulation/SimpleSimulation17.py new file mode 100644 index 000000000..6c8e174ef --- /dev/null +++ b/testsuite/simulation/SimpleSimulation17.py @@ -0,0 +1,71 @@ +## status: correct +## teardown_command: rm -rf SimpleSimulation17.ssp SimpleSimulation17_res.mat +## linux: yes +## ucrt64: yes +## win: yes +## mac: yes + +from OMSimulator import SSP, CRef, Settings, Connector, Causality, SignalType + +Settings.suppressPath = True + + +# This example creates a new SSP file with only top level system connectors and a sub-system with a connector. +# It then exports the SSP file and re-imports and simulates the model. The example also shows how to set and get values of top level system connectors and sub-system connectors. + +model = SSP() +## add top level system connector +model.activeVariant.system.addConnector(Connector('input1', Causality.input, SignalType.Real)) +model.setValue(CRef('default', 'input1'), 100.0) +## add subsystem +model.addSystem(CRef('default', 'sub-system')) +## add top level sub-system connector +model.activeVariant.system.elements[CRef('sub-system')].addConnector(Connector('input2', Causality.input, SignalType.Real)) +model.setValue(CRef('default', 'sub-system', 'input2'), 200.0) + +model.export('SimpleSimulation17.ssp') + +model2 = SSP('SimpleSimulation17.ssp') +model2.list() +instantiated_model = model2.instantiate() ## internally generate the json file and also set the model state like virgin, +instantiated_model.setResultFile("SimpleSimulation17_res.mat") +print(f"info: After instantiation:") +print(f"info: default.input1 : {instantiated_model.getValue(CRef('default', 'input1'))}", flush=True) +print(f"info: default.sub-system.input2: {instantiated_model.getValue(CRef('default', 'sub-system', 'input2'))}", flush=True) +instantiated_model.initialize() +instantiated_model.simulate() +print(f"info: After simulation:") +print(f"info: default.input1 : {instantiated_model.getValue(CRef('default', 'input1'))}", flush=True) +print(f"info: default.sub-system.input2: {instantiated_model.getValue(CRef('default', 'sub-system', 'input2'))}", flush=True) + +instantiated_model.terminate() +instantiated_model.delete() + +## Result: +## +## |-- Resources: +## |-- Active Variant: default +## |-- +## |-- Variant "default": +## |-- |-- System: default 'None' +## |-- |-- |-- Connectors: +## |-- |-- |-- |-- (input1, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- (Real input1, 100.0, None, 'None') +## |-- |-- |-- Elements: +## |-- |-- |-- |-- System: sub-system 'None' +## |-- |-- |-- |-- |-- Connectors: +## |-- |-- |-- |-- |-- |-- (input2, Causality.input, SignalType.Real, None, 'None') +## |-- |-- |-- |-- |-- Inline Parameter Bindings: +## |-- |-- |-- |-- |-- |-- (Real input2, 200.0, None, 'None') +## |-- DefaultExperiment +## |-- |-- startTime: 0.0 +## |-- |-- stopTime: 1.0 +## info: After instantiation: +## info: default.input1 : 100.0 +## info: default.sub-system.input2: 200.0 +## info: Result file: SimpleSimulation17_res.mat (bufferSize=10) +## info: After simulation: +## info: default.input1 : 100.0 +## info: default.sub-system.input2: 200.0 +## endResult diff --git a/testsuite/simulation/SimpleSimulation2.py b/testsuite/simulation/SimpleSimulation2.py index ebf0d1818..e2cdb788d 100644 --- a/testsuite/simulation/SimpleSimulation2.py +++ b/testsuite/simulation/SimpleSimulation2.py @@ -71,5 +71,5 @@ ## |-- |-- startTime: 0.0 ## |-- |-- stopTime: 1.0 ## -## info: Result file: SimpleSimulation2_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation2_res.mat (bufferSize=10) ## endResult diff --git a/testsuite/simulation/SimpleSimulation3.py b/testsuite/simulation/SimpleSimulation3.py index 6c46e75ce..6a38e901a 100644 --- a/testsuite/simulation/SimpleSimulation3.py +++ b/testsuite/simulation/SimpleSimulation3.py @@ -120,7 +120,7 @@ ## info: model doesn't contain any continuous state ## info: maximum step size for 'model.root.solver2': 0.001000 ## info: model doesn't contain any continuous state -## info: Result file: SimpleSimulation3_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation3_res.mat (bufferSize=10) ## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/simulation/SimpleSimulation4.py b/testsuite/simulation/SimpleSimulation4.py index bf5ed3d4c..f37391001 100644 --- a/testsuite/simulation/SimpleSimulation4.py +++ b/testsuite/simulation/SimpleSimulation4.py @@ -47,7 +47,7 @@ ## info: After instantiation: ## info: default.CauerLowPassAnalog.R1.T: 300.15 ## info: default.CauerLowPassAnalog.C1.C: 1.072 -## info: Result file: SimpleSimulation4_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation4_res.mat (bufferSize=10) ## info: After Simulation: ## info: default.CauerLowPassAnalog.R1.T: 300.15 ## info: default.CauerLowPassAnalog.C1.C: 1.072 diff --git a/testsuite/simulation/SimpleSimulation5.py b/testsuite/simulation/SimpleSimulation5.py index bd84ddb1e..bf478a171 100644 --- a/testsuite/simulation/SimpleSimulation5.py +++ b/testsuite/simulation/SimpleSimulation5.py @@ -56,7 +56,7 @@ ## info: default.Gain1.u: 6.0 ## info: default.Gain1.y: 12.0 ## info: default.Add1.u1: 0.0 -## info: Result file: SimpleSimulation5_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation5_res.mat (bufferSize=10) ## info: After simulation: ## info: default.Gain1.k: 2.0 ## info: default.Gain1.u: 6.0 diff --git a/testsuite/simulation/SimpleSimulation6.py b/testsuite/simulation/SimpleSimulation6.py index ccb07357b..5efa1f120 100644 --- a/testsuite/simulation/SimpleSimulation6.py +++ b/testsuite/simulation/SimpleSimulation6.py @@ -62,7 +62,7 @@ ## info: default.Gain1.u: 6.0 ## info: default.Gain1.y: 12.0 ## info: default.Add1.u1: 0.0 -## info: Result file: SimpleSimulation6_res.mat (bufferSize=1) +## info: Result file: SimpleSimulation6_res.mat (bufferSize=10) ## info: After simulation: ## info: default.Gain1.k: 2.0 ## info: default.Gain1.u: 6.0 diff --git a/testsuite/simulation/SimpleSimulation7.py b/testsuite/simulation/SimpleSimulation7.py index 45a18164a..a5ed34a11 100644 --- a/testsuite/simulation/SimpleSimulation7.py +++ b/testsuite/simulation/SimpleSimulation7.py @@ -87,8 +87,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation7_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation7_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 100.0 ## info: default.Gain1.k: 2.0 @@ -99,7 +99,7 @@ ## info: default.sub-system.Gain2.k: 1.0 ## info: default.sub-system.Gain2.u: 200.0 ## info: default.sub-system.Gain2.y: 200.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation8.py b/testsuite/simulation/SimpleSimulation8.py index 6299fd20f..9ac12f5d7 100644 --- a/testsuite/simulation/SimpleSimulation8.py +++ b/testsuite/simulation/SimpleSimulation8.py @@ -163,8 +163,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation8_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation8_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -176,7 +176,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/SimpleSimulation9.py b/testsuite/simulation/SimpleSimulation9.py index 02180156f..71ef20c1e 100644 --- a/testsuite/simulation/SimpleSimulation9.py +++ b/testsuite/simulation/SimpleSimulation9.py @@ -165,8 +165,8 @@ ## info: default.sub-system.Gain2.u: 0.0 ## info: default.sub-system.Gain2.y: 0.0 ## info: model doesn't contain any continuous state -## info: maximum step size for 'model.root': 0.001000 -## info: Result file: SimpleSimulation9_res.mat (bufferSize=1) +## info: maximum step size for 'model.root.solver2': 0.001000 +## info: Result file: SimpleSimulation9_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 200.0 ## info: default.input1 : 300.0 @@ -178,7 +178,7 @@ ## info: default.sub-system.Gain2.k: 500.0 ## info: default.sub-system.Gain2.u: 400.0 ## info: default.sub-system.Gain2.y: 200000.0 -## info: Final Statistics for 'model.root': +## info: Final Statistics for 'model.root.solver2': ## NumSteps = 1001 NumRhsEvals = 1002 NumLinSolvSetups = 51 ## NumNonlinSolvIters = 1001 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 ## endResult diff --git a/testsuite/simulation/exportJson4.py b/testsuite/simulation/exportJson4.py index 1a26385f3..2918229b2 100644 --- a/testsuite/simulation/exportJson4.py +++ b/testsuite/simulation/exportJson4.py @@ -122,7 +122,7 @@ ## info: default.Add1.u2 : 3.0 ## info: default.Add1.y : 3.0 ## info: model doesn't contain any continuous state -## info: Result file: export_json4_res.mat (bufferSize=1) +## info: Result file: export_json4_res.mat (bufferSize=10) ## info: After simulation: ## info: default.param1 : 2.0 ## info: default.sub-system.input : 5.0 diff --git a/testsuite/simulation/fmuSimulation1.py b/testsuite/simulation/fmuSimulation1.py index 7fd650afc..e97a4d0a2 100644 --- a/testsuite/simulation/fmuSimulation1.py +++ b/testsuite/simulation/fmuSimulation1.py @@ -46,7 +46,7 @@ ## info: k: 5.0 ## info: u: 10.0 ## info: y: 50.0 -## info: Result file: Gain_res.mat (bufferSize=1) +## info: Result file: Gain_res.mat (bufferSize=10) ## info: After Simulation: ## info: k: 5.0 ## info: u: 10.0 diff --git a/testsuite/simulation/pwmtest.py b/testsuite/simulation/pwmtest.py index f74d722c4..daae5212e 100644 --- a/testsuite/simulation/pwmtest.py +++ b/testsuite/simulation/pwmtest.py @@ -10,9 +10,10 @@ Settings.suppressPath = True model = SSP('../resources/PWMTest.ssp') -# solver2 = {'name' : 'solver2', 'method': 'cvode', 'tolerance': 1e-4} -# model.newSolver(solver2) -# model.setSolver(CRef('Root', 'SCSystem', 'circuit'), 'solver2') +solver2 = {'name' : 'solver2', 'method': 'cvode', 'tolerance': 1e-4} +model.newSolver(solver2) +model.setSolver(CRef('Root', 'SCSystem', 'circuit'), 'solver2') +model.setSolver(CRef('Root', 'pulse'), 'solver2') model.list() print("", flush=True) @@ -57,6 +58,8 @@ ## |-- |-- |-- |-- |-- |-- |-- |-- (resistor.useHeatPort, Causality.calculatedParameter, SignalType.Boolean, None, 'None') ## |-- |-- |-- |-- |-- |-- |-- ElementGeometry: ## |-- |-- |-- |-- |-- |-- |-- |-- (x1:-17.96226, y1:-12.64151, x2:2.03774, y2:7.35849, rotation:0.0, icon_source:None, icon_rotation:0.0, icon_flip:False, icon_fixed_aspect_ratio:False) +## |-- |-- |-- |-- |-- |-- |-- Solver Settings: +## |-- |-- |-- |-- |-- |-- |-- |-- name: solver2 ## |-- |-- |-- |-- |-- Connections: ## |-- |-- |-- |-- |-- |-- .u -> circuit.u ## |-- |-- |-- |-- |-- |-- |-- ConnectionGeometry: (pointsX: [-37.5, -37.5], pointsY: [-2.0, -3.0]) @@ -80,11 +83,14 @@ ## |-- |-- |-- |-- |-- |-- (Real offset, 0.0, None, 'None') ## |-- |-- |-- |-- |-- |-- (Real amplitude, 220.0, None, 'None') ## |-- |-- |-- |-- |-- |-- (Integer nperiod, -1, None, 'None') +## |-- |-- |-- |-- |-- Solver Settings: +## |-- |-- |-- |-- |-- |-- name: solver2 ## |-- |-- |-- Connections: ## |-- |-- |-- |-- pulse.y -> SCSystem.u ## |-- |-- |-- |-- |-- ConnectionGeometry: (pointsX: [3.0], pointsY: [0.0]) ## |-- |-- |-- Solver Settings: ## |-- |-- |-- |-- () +## |-- |-- |-- |-- (name=solver2, method=cvode, tolerance=0.0001) ## |-- UnitDefinitions: ## |-- |-- Unit: s ## |-- |-- |-- BaseUnit: s: 1 @@ -93,7 +99,7 @@ ## |-- |-- stopTime: 1.000000 ## ## info: maximum step size for 'model.root': 0.001000 -## info: Result file: pwm_res.mat (bufferSize=1) +## info: Result file: pwm_res.mat (bufferSize=10) ## info: Final Statistics for 'model.root': ## NumSteps = 0 NumRhsEvals = 0 NumLinSolvSetups = 0 ## NumNonlinSolvIters = 0 NumNonlinSolvConvFails = 0 NumErrTestFails = 0 diff --git a/testsuite/simulation/stepUntil.py b/testsuite/simulation/stepUntil.py index 263da433f..c7978ad37 100644 --- a/testsuite/simulation/stepUntil.py +++ b/testsuite/simulation/stepUntil.py @@ -32,7 +32,7 @@ fmu.delete() ## Result: -## info: Result file: Sine_res.mat (bufferSize=1) +## info: Result file: Sine_res.mat (bufferSize=10) ## info: time: 0.1, y: 0.5877852522924735 ## info: time: 0.2, y: 0.9510565162951539 ## info: time: 0.3, y: 0.9510565162951532 diff --git a/testsuite/simulation/tankYPipe.py b/testsuite/simulation/tankYPipe.py index 9c00dfbcd..159daf79b 100644 --- a/testsuite/simulation/tankYPipe.py +++ b/testsuite/simulation/tankYPipe.py @@ -96,5 +96,5 @@ ## |-- |-- startTime: 0.0 ## |-- |-- stopTime: 1.0 ## -## info: Result file: tankYpipe_rest.mat (bufferSize=1) +## info: Result file: tankYpipe_rest.mat (bufferSize=10) ## endResult