diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index e2cf2b6..1f173ee 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -104,6 +104,11 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant named BEdit `$name$` macro completions editable there; arbitrary `$name$` expressions are highlighted as BEvalues. Highlight colors and bold/italic styles are persisted under `syntax//` in application settings. +- Text component sources may contain private Modelica declarations in + `source.declarations`. The text-definition editor exposes declarations above + `source.initialEquations` and `source.equations`, with the same highlighting + and completion in all three fields. The composer emits each in its matching + Modelica section. - Application-wide messages use `core.application_log.get_logger()`. The main window installs the Qt log-panel handler; core code must only use standard Python logging and must not import the GUI handler. @@ -116,6 +121,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant per component instance from graph connections and exposed while compiling as `$portname_N$`; array connection endpoints receive stable one-based indices in graph connection order. +- Simulation → Export Model composes through `Simulation.compose_source()` without + building the model, then writes the generated source as a `.mo` file. - OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its persistent worker and OMC session start lazily on the first queued request. Never perform OMPython work directly on the Qt GUI thread. Result and error diff --git a/BEdit/src/bedit/core/model.py b/BEdit/src/bedit/core/model.py index c50a699..c2a68e6 100644 --- a/BEdit/src/bedit/core/model.py +++ b/BEdit/src/bedit/core/model.py @@ -344,11 +344,19 @@ class Component: if kind == "text": raw_source = implementation.get("source", {}) equations = raw_source.get("equations", "") + declarations = raw_source.get("declarations", "") + initial_equations = raw_source.get("initialEquations", "") parameters = data.get("parameters", raw_source.get("parameters", [])) if not isinstance(equations, str): raise ValueError("Text component equations must be a string") + if not isinstance(declarations, str): + raise ValueError("Text component declarations must be a string") + if not isinstance(initial_equations, str): + raise ValueError("Text component initial equations must be a string") source = { "equations": equations, + "declarations": declarations, + "initialEquations": initial_equations, } if not isinstance(parameters, list): raise ValueError("Component parameters must be a list") diff --git a/BEdit/src/bedit/core/simulation/composer.py b/BEdit/src/bedit/core/simulation/composer.py index f78c8c7..59cd668 100644 --- a/BEdit/src/bedit/core/simulation/composer.py +++ b/BEdit/src/bedit/core/simulation/composer.py @@ -123,6 +123,14 @@ def emit_model( f"{body_indent}parameter {parameter_type} {parameter_name} = {value};" ) + if implementation_kind == "text": + declarations = str(implementation.get("source", {}).get("declarations", "")) + declarations = expand_bevalues(declarations, macros) + lines.extend( + f"{body_indent}{line}" if line.strip() else "" + for line in declarations.splitlines() + ) + if implementation_kind == "graph": for block in nested_graph.get("blocks", []): block_type = model_name_for(block) @@ -134,8 +142,9 @@ def emit_model( f"{body_indent}{junction_type} {_junction_name(junction['id'])};" ) - lines.append(f"{indentation}equation") if implementation_kind == "graph": + lines.append(f"{indentation}equation") + blocks = {block["id"]: block for block in nested_graph.get("blocks", [])} junctions = { junction["id"]: junction for junction in nested_graph.get("junctions", []) @@ -151,8 +160,21 @@ def emit_model( # Connector types may require different equations in future. lines.append(f"{body_indent}{target} = {source};") else: + initial_equations = str( + implementation.get("source", {}).get("initialEquations", "") + ) + initial_equations = expand_bevalues(initial_equations, macros) equations = str(implementation.get("source", {}).get("equations", "")) equations = expand_bevalues(equations, macros) + + if initial_equations.strip(): + lines.append(f"{indentation}initial equation") + lines.extend( + f"{body_indent}{line}" if line.strip() else "" + for line in initial_equations.splitlines() + ) + + lines.append(f"{indentation}equation") lines.extend( f"{body_indent}{line}" if line.strip() else "" for line in equations.splitlines() @@ -161,7 +183,6 @@ def emit_model( lines.append(f"{indentation}end {model_name};") return "\n".join(lines) + "\n" - def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]: """Remove annotations and UI-only data from a serialized component tree.""" diff --git a/BEdit/src/bedit/core/simulation/service.py b/BEdit/src/bedit/core/simulation/service.py index cd40d74..83182d3 100644 --- a/BEdit/src/bedit/core/simulation/service.py +++ b/BEdit/src/bedit/core/simulation/service.py @@ -47,13 +47,7 @@ class Simulation: """Compose and retain the active graph's Modelica representation.""" self.model_path = None - - # Create openmodelica model - result = compose_graph(graph) - self.last_composition_input = result.graph - self.id_list = result.objects_by_id - self.last_composition_output = result.modelica - self.model_name = result.model_name + self.compose_source(graph) def _model_compiled(result): log.info("Compiling OK: %s", result) @@ -79,6 +73,17 @@ class Simulation: error_callback, ) + def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]: + """Compose Modelica source without asking OpenModelica to build it.""" + + result = compose_graph(graph) + self.last_composition_input = result.graph + self.id_list = result.objects_by_id + self.last_composition_output = result.modelica + self.model_name = result.model_name + # log.info("Composed OpenModelica model:\n%s", result.modelica) + return result.model_name, result.modelica + def run_simulation( self, graph: dict[str, Any], diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py index 6f6be93..a280a13 100644 --- a/BEdit/src/bedit/gui/controllers/document.py +++ b/BEdit/src/bedit/gui/controllers/document.py @@ -168,7 +168,13 @@ class DocumentController(QObject): name=self._available_component_name(base_name, self.document.roots.values(), number), implementation_kind=kind, icon=Icon(text="Graph" if kind == "graph" else "Text"), - source={"equations": ""} if kind == "text" else {}, + source={ + "declarations": "", + "initialEquations": "", + "equations": "", + } + if kind == "text" + else {}, ) self.undo_stack.push(AddComponentCommand(self, None, component)) self.activate_component(component.id) @@ -187,7 +193,13 @@ class DocumentController(QObject): name=self._available_component_name(base_name, owner.graph.blocks.values(), number), implementation_kind=kind, icon=Icon(text="Graph" if kind == "graph" else "Text"), - source={"equations": ""} if kind == "text" else {}, + source={ + "declarations": "", + "initialEquations": "", + "equations": "", + } + if kind == "text" + else {}, ) self.undo_stack.push(AddComponentCommand(self, owner_id, component)) return component.id @@ -427,6 +439,12 @@ class DocumentController(QObject): raise ValueError("Open a graph component before composing") self.simulation.compose(component.to_dict()) + def compose_active_graph_source(self) -> tuple[str, str]: + component = self.active_component + if component is None or component.implementation_kind != "graph": + raise ValueError("Open a graph component before exporting a model") + return self.simulation.compose_source(component.to_dict()) + def run_simulation( self, progress_callback=None, @@ -636,6 +654,8 @@ class DocumentController(QObject): self, inputs: list[Port], outputs: list[Port], + declarations: str, + initial_equations: str, equations: str, parameters: list[Parameter], ) -> None: @@ -682,6 +702,8 @@ class DocumentController(QObject): "outputs": [port.to_dict() for port in outputs], "source": { "equations": equations, + "declarations": declarations, + "initialEquations": initial_equations, }, "parameters": [parameter.to_dict() for parameter in parameters], } diff --git a/BEdit/src/bedit/gui/editors/text_definition.py b/BEdit/src/bedit/gui/editors/text_definition.py index b688651..48094eb 100644 --- a/BEdit/src/bedit/gui/editors/text_definition.py +++ b/BEdit/src/bedit/gui/editors/text_definition.py @@ -14,7 +14,7 @@ PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1 class TextDefinitionEditor(QWidget): - """Editor for a text component's equations, ports, and parameters.""" + """Editor for a text component's Modelica source, ports, and parameters.""" modifiedChanged = Signal(bool) definitionEdited = Signal() @@ -32,7 +32,10 @@ class TextDefinitionEditor(QWidget): QHeaderView.ResizeMode.Stretch ) self.ui.columnSplitter.setSizes([560, 340]) + self.ui.sourceSplitter.setSizes([180, 180, 240]) self.ui.definitionSplitter.setSizes([300, 300]) + self.ui.declarationsEdit.textChanged.connect(self._mark_modified) + self.ui.initialEquationsEdit.textChanged.connect(self._mark_modified) self.ui.equationsEdit.textChanged.connect(self._mark_modified) self.ui.portsTable.cellChanged.connect(self._symbols_modified) self.ui.parametersTable.cellChanged.connect(self._symbols_modified) @@ -52,6 +55,14 @@ class TextDefinitionEditor(QWidget): def equations(self) -> str: return self.ui.equationsEdit.toPlainText() + @property + def declarations(self) -> str: + return self.ui.declarationsEdit.toPlainText() + + @property + def initial_equations(self) -> str: + return self.ui.initialEquationsEdit.toPlainText() + @property def ports(self) -> tuple[list[Port], list[Port]]: inputs: list[Port] = [] @@ -87,12 +98,16 @@ class TextDefinitionEditor(QWidget): def set_definition( self, + declarations: str, + initial_equations: str, equations: str, inputs: list[Port], outputs: list[Port], parameters: list[Parameter], ) -> None: self._loading = True + self.ui.declarationsEdit.setPlainText(declarations) + self.ui.initialEquationsEdit.setPlainText(initial_equations) self.ui.equationsEdit.setPlainText(equations) self.ui.portsTable.setRowCount(0) for port in inputs: @@ -102,11 +117,7 @@ class TextDefinitionEditor(QWidget): self.ui.parametersTable.setRowCount(0) for parameter in parameters: self._append_parameter(parameter) - self.ui.equationsEdit.set_symbols( - [port.name for port in inputs], - [port.name for port in outputs], - [parameter.name for parameter in parameters], - ) + self._set_editor_symbols(inputs, outputs, parameters) self._loading = False self.set_modified(False) self._update_buttons() @@ -128,11 +139,22 @@ class TextDefinitionEditor(QWidget): def _refresh_editor_symbols(self) -> None: inputs, outputs = self.ports - self.ui.equationsEdit.set_symbols( + self._set_editor_symbols(inputs, outputs, self.parameters) + + def _set_editor_symbols( + self, + inputs: list[Port], + outputs: list[Port], + parameters: list[Parameter], + ) -> None: + symbols = ( [port.name for port in inputs], [port.name for port in outputs], - [parameter.name for parameter in self.parameters], + [parameter.name for parameter in parameters], ) + self.ui.declarationsEdit.set_symbols(*symbols) + self.ui.initialEquationsEdit.set_symbols(*symbols) + self.ui.equationsEdit.set_symbols(*symbols) def _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox: combo = QComboBox(self) diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index 2279e0d..6efcf61 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -56,50 +56,53 @@ class Ui_MainWindow(object): icon4 = QIcon() icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionSimulationWindow.setIcon(icon4) + self.actionExportModel = QAction(MainWindow) + self.actionExportModel.setObjectName(u"actionExportModel") + icon5 = QIcon() + icon5.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionExportModel.setIcon(icon5) self.actionNew = QAction(MainWindow) self.actionNew.setObjectName(u"actionNew") - icon5 = QIcon() - icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionNew.setIcon(icon5) + icon6 = QIcon() + icon6.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionNew.setIcon(icon6) self.actionRotateClockwise = QAction(MainWindow) self.actionRotateClockwise.setObjectName(u"actionRotateClockwise") - icon6 = QIcon() - icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionRotateClockwise.setIcon(icon6) + icon7 = QIcon() + icon7.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionRotateClockwise.setIcon(icon7) self.actionZoomIn = QAction(MainWindow) self.actionZoomIn.setObjectName(u"actionZoomIn") - icon7 = QIcon() - icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionZoomIn.setIcon(icon7) + icon8 = QIcon() + icon8.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionZoomIn.setIcon(icon8) self.actionZoomOut = QAction(MainWindow) self.actionZoomOut.setObjectName(u"actionZoomOut") - icon8 = QIcon() - icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionZoomOut.setIcon(icon8) + icon9 = QIcon() + icon9.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionZoomOut.setIcon(icon9) self.actionCenterView = QAction(MainWindow) self.actionCenterView.setObjectName(u"actionCenterView") - icon9 = QIcon() - icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCenterView.setIcon(icon9) + icon10 = QIcon() + icon10.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCenterView.setIcon(icon10) self.actionOpen = QAction(MainWindow) self.actionOpen.setObjectName(u"actionOpen") - icon10 = QIcon() - icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionOpen.setIcon(icon10) + icon11 = QIcon() + icon11.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionOpen.setIcon(icon11) self.actionReloadLibraries = QAction(MainWindow) self.actionReloadLibraries.setObjectName(u"actionReloadLibraries") self.actionReloadSimulation = QAction(MainWindow) self.actionReloadSimulation.setObjectName(u"actionReloadSimulation") self.actionSave = QAction(MainWindow) self.actionSave.setObjectName(u"actionSave") - icon11 = QIcon() - icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionSave.setIcon(icon11) + icon12 = QIcon() + icon12.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSave.setIcon(icon12) self.actionSaveAs = QAction(MainWindow) self.actionSaveAs.setObjectName(u"actionSaveAs") - icon12 = QIcon() - icon12.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionSaveAs.setIcon(icon12) + self.actionSaveAs.setIcon(icon5) self.actionExit = QAction(MainWindow) self.actionExit.setObjectName(u"actionExit") self.actionClose = QAction(MainWindow) @@ -295,7 +298,7 @@ class Ui_MainWindow(object): self.rotateToolButton = QToolButton(self.workspaceHeader) self.rotateToolButton.setObjectName(u"rotateToolButton") - self.rotateToolButton.setIcon(icon6) + self.rotateToolButton.setIcon(icon7) self.workspaceHeaderLayout.addWidget(self.rotateToolButton) @@ -441,6 +444,7 @@ class Ui_MainWindow(object): self.menuSimulation.addAction(self.actionSimulationSettings) self.menuSimulation.addAction(self.actionGraphParameters) self.menuSimulation.addAction(self.actionCompose) + self.menuSimulation.addAction(self.actionExportModel) self.menuSimulation.addAction(self.actionSimulationWindow) self.menuSimulation.addAction(self.actionRunSimulation) self.fileToolbar.addAction(self.actionNew) @@ -496,6 +500,10 @@ class Ui_MainWindow(object): self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None)) #if QT_CONFIG(statustip) self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None)) +#endif // QT_CONFIG(statustip) + self.actionExportModel.setText(QCoreApplication.translate("MainWindow", u"Export Model\u2026", None)) +#if QT_CONFIG(statustip) + self.actionExportModel.setStatusTip(QCoreApplication.translate("MainWindow", u"Save the composed OpenModelica model to a file", None)) #endif // QT_CONFIG(statustip) self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None)) #if QT_CONFIG(statustip) diff --git a/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py b/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py index e5a5350..c7bb86b 100644 --- a/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py +++ b/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py @@ -34,7 +34,31 @@ class Ui_TextDefinitionEditor(object): self.columnSplitter.setObjectName(u"columnSplitter") self.columnSplitter.setOrientation(Qt.Orientation.Horizontal) self.columnSplitter.setChildrenCollapsible(False) - self.equationsGroup = QGroupBox(self.columnSplitter) + self.sourceSplitter = QSplitter(self.columnSplitter) + self.sourceSplitter.setObjectName(u"sourceSplitter") + self.sourceSplitter.setOrientation(Qt.Orientation.Vertical) + self.sourceSplitter.setChildrenCollapsible(False) + self.declarationsGroup = QGroupBox(self.sourceSplitter) + self.declarationsGroup.setObjectName(u"declarationsGroup") + self.declarationsLayout = QVBoxLayout(self.declarationsGroup) + self.declarationsLayout.setObjectName(u"declarationsLayout") + self.declarationsEdit = OpenModelicaEditor(self.declarationsGroup) + self.declarationsEdit.setObjectName(u"declarationsEdit") + + self.declarationsLayout.addWidget(self.declarationsEdit) + + self.sourceSplitter.addWidget(self.declarationsGroup) + self.initialEquationsGroup = QGroupBox(self.sourceSplitter) + self.initialEquationsGroup.setObjectName(u"initialEquationsGroup") + self.initialEquationsLayout = QVBoxLayout(self.initialEquationsGroup) + self.initialEquationsLayout.setObjectName(u"initialEquationsLayout") + self.initialEquationsEdit = OpenModelicaEditor(self.initialEquationsGroup) + self.initialEquationsEdit.setObjectName(u"initialEquationsEdit") + + self.initialEquationsLayout.addWidget(self.initialEquationsEdit) + + self.sourceSplitter.addWidget(self.initialEquationsGroup) + self.equationsGroup = QGroupBox(self.sourceSplitter) self.equationsGroup.setObjectName(u"equationsGroup") self.equationsLayout = QVBoxLayout(self.equationsGroup) self.equationsLayout.setObjectName(u"equationsLayout") @@ -43,7 +67,8 @@ class Ui_TextDefinitionEditor(object): self.equationsLayout.addWidget(self.equationsEdit) - self.columnSplitter.addWidget(self.equationsGroup) + self.sourceSplitter.addWidget(self.equationsGroup) + self.columnSplitter.addWidget(self.sourceSplitter) self.definitionSplitter = QSplitter(self.columnSplitter) self.definitionSplitter.setObjectName(u"definitionSplitter") self.definitionSplitter.setOrientation(Qt.Orientation.Vertical) @@ -141,6 +166,8 @@ class Ui_TextDefinitionEditor(object): # setupUi def retranslateUi(self, TextDefinitionEditor): + self.declarationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Declarations", None)) + self.initialEquationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Initial Equations", None)) self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None)) self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None)) ___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0) diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index e427870..41903ef 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -181,6 +181,7 @@ class MainWindow(QMainWindow): ) self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters) self.ui.actionCompose.triggered.connect(self.compose_active_graph) + self.ui.actionExportModel.triggered.connect(self.export_model) self.ui.actionSimulationWindow.triggered.connect(self.show_simulation_window) self.ui.actionRunSimulation.triggered.connect(self.run_simulation) self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled) @@ -261,6 +262,35 @@ class MainWindow(QMainWindow): self.log.error("Composition failed: %s", error) QMessageBox.warning(self, "Cannot compose", str(error)) + @Slot() + def export_model(self) -> None: + try: + model_name, source = self.document_controller.compose_active_graph_source() + except ValueError as error: + self.log.error("Model export composition failed: %s", error) + QMessageBox.warning(self, "Cannot Export Model", str(error)) + return + file_name, _selected_filter = QFileDialog.getSaveFileName( + self, + "Export OpenModelica Model", + f"{model_name}.mo", + "Modelica Models (*.mo);;All Files (*)", + ) + if not file_name: + return + path = Path(file_name) + if not path.suffix: + path = path.with_suffix(".mo") + temporary_path = path.with_suffix(path.suffix + ".tmp") + try: + temporary_path.write_text(source, encoding="utf-8") + temporary_path.replace(path) + except OSError as error: + self.log.error("Could not export model %s: %s", path, error) + QMessageBox.critical(self, "Cannot Export Model", str(error)) + return + self.log.info("Exported OpenModelica model to %s", path) + @Slot() def show_simulation_window(self) -> None: self._simulation_window.show() @@ -307,6 +337,7 @@ class MainWindow(QMainWindow): self.ui.actionSimulationSettings.setEnabled(False) self.ui.actionGraphParameters.setEnabled(False) self.ui.actionCompose.setEnabled(False) + self.ui.actionExportModel.setEnabled(False) self.ui.actionRunSimulation.setEnabled(False) self._update_edit_actions() return @@ -318,6 +349,7 @@ class MainWindow(QMainWindow): self.ui.actionSimulationSettings.setEnabled(is_graph) self.ui.actionGraphParameters.setEnabled(is_graph) self.ui.actionCompose.setEnabled(is_graph) + self.ui.actionExportModel.setEnabled(is_graph) self.ui.actionRunSimulation.setEnabled(is_graph) self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceStack.setCurrentWidget( @@ -505,6 +537,8 @@ class MainWindow(QMainWindow): if component is None: return self.ui.textDefinitionEditor.set_definition( + component.source.get("declarations", ""), + component.source.get("initialEquations", ""), component.source.get("equations", ""), component.inputs, component.outputs, @@ -522,7 +556,8 @@ class MainWindow(QMainWindow): answer = QMessageBox.question( self, "Apply text component changes?", - "The text component has unapplied equation, port, or parameter changes.", + "The text component has unapplied declaration, initial-equation, " + "equation, port, or parameter changes.", QMessageBox.StandardButton.Apply | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, @@ -540,6 +575,8 @@ class MainWindow(QMainWindow): self.document_controller.replace_active_text_definition( inputs, outputs, + self.ui.textDefinitionEditor.declarations, + self.ui.textDefinitionEditor.initial_equations, self.ui.textDefinitionEditor.equations, self.ui.textDefinitionEditor.parameters, ) @@ -672,6 +709,8 @@ class MainWindow(QMainWindow): if scene is not None: scene.update() self.ui.graphView.viewport().update() + self.ui.textDefinitionEditor.ui.declarationsEdit.reload_highlighting() + self.ui.textDefinitionEditor.ui.initialEquationsEdit.reload_highlighting() self.ui.textDefinitionEditor.ui.equationsEdit.reload_highlighting() def _document_opened_changed(self, opened: bool) -> None: diff --git a/BEdit/src/bedit/gui/simulation_window.py b/BEdit/src/bedit/gui/simulation_window.py index f2483d2..cbadc2f 100644 --- a/BEdit/src/bedit/gui/simulation_window.py +++ b/BEdit/src/bedit/gui/simulation_window.py @@ -691,7 +691,27 @@ def _safe_file_stem(model_name: str) -> str: def _signal_tree_parts(signal_name: str) -> tuple[str, ...]: - """Split dots and array indices into display hierarchy segments.""" + """Split a result name into readable component, variable, and index levels. + + OpenModelica emits state derivatives as names such as ``der(block.x)``. + Keep those traces available, but display them below a ``Derivatives`` group + on their owning component instead of creating a misleading top-level + ``der(block`` branch. + """ + + derivative = re.fullmatch(r"der\((.+)\)", signal_name) + if derivative is not None: + inner_name = derivative.group(1) + inner_parts = _signal_tree_parts(inner_name) + if inner_parts: + index_count = len(re.findall(r"\[[^\]]+\]", inner_name.rsplit(".", 1)[-1])) + variable_index = max(0, len(inner_parts) - index_count - 1) + return ( + *inner_parts[:variable_index], + "Derivatives", + *inner_parts[variable_index:], + ) + return ("Derivatives", signal_name) parts: list[str] = [] for segment in signal_name.split("."): diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index 605041c..5eec852 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -552,6 +552,7 @@ + @@ -715,6 +716,18 @@ Show the simulation results window + + + + :/icons/icons/document-save-as.png:/icons/icons/document-save-as.png + + + Export Model… + + + Save the composed OpenModelica model to a file + + diff --git a/BEdit/ui/text_definition_editor.ui b/BEdit/ui/text_definition_editor.ui index 5fe9fdd..409398a 100644 --- a/BEdit/ui/text_definition_editor.ui +++ b/BEdit/ui/text_definition_editor.ui @@ -12,11 +12,27 @@ Qt::Orientation::Horizontal false - - Equations - - - + + Qt::Orientation::Vertical + false + + Declarations + + + + + + Initial Equations + + + + + + Equations + + + + Qt::Orientation::Vertical diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index 69d365e..a59707f 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -165,7 +165,9 @@ "implementation": { "kind": "text", "source": { - "equations": "y = v;" + "equations": "y = v;", + "declarations": "", + "initialEquations": "" } } }, @@ -270,7 +272,9 @@ "implementation": { "kind": "text", "source": { - "equations": "y = k*u;" + "equations": "y = k*u;", + "declarations": "", + "initialEquations": "" } } }, @@ -377,7 +381,9 @@ "implementation": { "kind": "text", "source": { - "equations": "y = v+sin(10*time);" + "equations": "y = v+sin(10*time);", + "declarations": "", + "initialEquations": "" } } }, @@ -482,7 +488,9 @@ "implementation": { "kind": "text", "source": { - "equations": "y = k*u;" + "equations": "y = k*u;", + "declarations": "", + "initialEquations": "" } } }, @@ -579,7 +587,255 @@ "implementation": { "kind": "text", "source": { - "equations": "y = sum(u[i] for i in 1:$u_N$ );" + "equations": "y = sum(u[i] for i in 1:$u_N$ );", + "declarations": "", + "initialEquations": "" + } + } + }, + { + "id": "78c8fcb6-bd46-4b85-aa91-3276a8d1e8cb", + "name": "Integrate0", + "position": { + "x": 224.0, + "y": -96.0 + }, + "rotation": 0.0, + "interface": { + "inputs": [ + { + "id": "port-df34ce84", + "name": "u", + "position": { + "x": 0.0, + "y": 0.0 + }, + "properties": { + "iconPosition": { + "x": 64.0, + "y": 64.0 + } + }, + "type": "signal", + "multipleConnections": false + } + ], + "outputs": [ + { + "id": "port-fe9e6486", + "name": "y", + "position": { + "x": 0.0, + "y": 0.0 + }, + "properties": { + "iconPosition": { + "x": 88.0, + "y": 40.0 + } + }, + "type": "signal", + "multipleConnections": false + } + ] + }, + "parameters": [ + { + "id": "parameter-61a43a86", + "name": "y_start", + "type": "real", + "value": "0" + } + ], + "icon": { + "shape": "rectangle", + "fill": "#f4f4f4", + "border": "#303030", + "text": "Text", + "size": { + "width": 128.0, + "height": 128.0 + }, + "elements": [ + { + "cornerRadius": 5.0, + "fill": "#f4f4f4", + "height": 64.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#303030", + "type": "rectangle", + "width": 64.0, + "x": 32.0, + "y": 32.0 + }, + { + "color": "#00007f", + "fill": "#ffffff", + "fontSize": 24.0, + "height": 48.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#00007f", + "text": "dt", + "type": "text", + "width": 48.0, + "x": 48.0, + "y": 40.0 + }, + { + "type": "text", + "x": 32.0, + "y": 32.0, + "width": 32.0, + "height": 56.0, + "fill": "none", + "stroke": "#00007f", + "lineWidth": 1.5, + "lineStyle": "none", + "text": "\u222b", + "fontSize": 35.0, + "color": "#00007f" + } + ] + }, + "properties": { + "showName": true + }, + "library": { + "showSubtree": true + }, + "implementation": { + "kind": "text", + "source": { + "equations": "der(y) = u;", + "declarations": "", + "initialEquations": "y = y_start;" + } + } + }, + { + "id": "f2caf59f-5688-46b4-b17d-bdc911580d7b", + "name": "Differentiate0", + "position": { + "x": 224.0, + "y": 32.0 + }, + "rotation": 0.0, + "interface": { + "inputs": [ + { + "id": "port-df34ce84", + "name": "u", + "position": { + "x": 0.0, + "y": 0.0 + }, + "properties": { + "iconPosition": { + "x": 64.0, + "y": 64.0 + } + }, + "type": "signal", + "multipleConnections": false + } + ], + "outputs": [ + { + "id": "port-fe9e6486", + "name": "y", + "position": { + "x": 0.0, + "y": 0.0 + }, + "properties": { + "iconPosition": { + "x": 88.0, + "y": 40.0 + } + }, + "type": "signal", + "multipleConnections": false + } + ] + }, + "parameters": [ + { + "id": "parameter-61a43a86", + "name": "x_start", + "type": "real", + "value": "0" + }, + { + "id": "parameter-d110667b", + "name": "y_start", + "type": "real", + "value": "0" + }, + { + "id": "parameter-873b503d", + "name": "k", + "type": "real", + "value": "1" + }, + { + "id": "parameter-a55752c7", + "name": "T", + "type": "real", + "value": "0.01" + } + ], + "icon": { + "shape": "rectangle", + "fill": "#f4f4f4", + "border": "#303030", + "text": "Text", + "size": { + "width": 128.0, + "height": 128.0 + }, + "elements": [ + { + "cornerRadius": 5.0, + "fill": "#f4f4f4", + "height": 64.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#303030", + "type": "rectangle", + "width": 64.0, + "x": 32.0, + "y": 32.0 + }, + { + "color": "#00007f", + "fill": "#ffffff", + "fontSize": 22.0, + "height": 48.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#00007f", + "text": "d/dt", + "type": "text", + "width": 64.0, + "x": 32.0, + "y": 40.0 + } + ] + }, + "properties": { + "showName": true + }, + "library": { + "showSubtree": true + }, + "implementation": { + "kind": "text", + "source": { + "equations": "assert(T > 0, \"Differentiate time constant T must be positive\");\n\nder(x) = (u - x)/T;\ny = (k/T)*(u - x);", + "declarations": "Real x(start=x_start);", + "initialEquations": "y = y_start;" } } } @@ -644,6 +900,41 @@ "properties": { "waypoints": [] } + }, + { + "id": "33e15cc2-7f33-469e-91a8-921c04f6b823", + "source": { + "block": "59bb51be-8185-4b0b-a894-e38db61aff18", + "port": "port-a2f0dea6" + }, + "target": { + "block": "78c8fcb6-bd46-4b85-aa91-3276a8d1e8cb", + "port": "port-df34ce84" + }, + "name": "", + "properties": { + "waypoints": [] + } + }, + { + "id": "26e10b34-f168-4492-b4f2-fb46d8a4aafa", + "source": { + "block": "59bb51be-8185-4b0b-a894-e38db61aff18", + "port": "port-a2f0dea6" + }, + "target": { + "block": "f2caf59f-5688-46b4-b17d-bdc911580d7b", + "port": "port-df34ce84" + }, + "name": "", + "properties": { + "waypoints": [ + { + "x": 160.0, + "y": 96.0 + } + ] + } } ], "annotations": [],