proper modelica text model editing

This commit is contained in:
2026-07-21 17:16:06 +02:00
parent 2f6487e510
commit 48ac9e2f59
13 changed files with 557 additions and 58 deletions

View File

@@ -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$` named BEdit `$name$` macro completions editable there; arbitrary `$name$`
expressions are highlighted as BEvalues. Highlight colors and bold/italic expressions are highlighted as BEvalues. Highlight colors and bold/italic
styles are persisted under `syntax/<category>/` in application settings. styles are persisted under `syntax/<category>/` 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 - Application-wide messages use `core.application_log.get_logger()`. The main
window installs the Qt log-panel handler; core code must only use standard window installs the Qt log-panel handler; core code must only use standard
Python logging and must not import the GUI handler. 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 per component instance from graph connections and exposed while compiling as
`$portname_N$`; array connection endpoints receive stable one-based indices in `$portname_N$`; array connection endpoints receive stable one-based indices in
graph connection order. 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 - OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
persistent worker and OMC session start lazily on the first queued request. 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 Never perform OMPython work directly on the Qt GUI thread. Result and error

View File

@@ -344,11 +344,19 @@ class Component:
if kind == "text": if kind == "text":
raw_source = implementation.get("source", {}) raw_source = implementation.get("source", {})
equations = raw_source.get("equations", "") equations = raw_source.get("equations", "")
declarations = raw_source.get("declarations", "")
initial_equations = raw_source.get("initialEquations", "")
parameters = data.get("parameters", raw_source.get("parameters", [])) parameters = data.get("parameters", raw_source.get("parameters", []))
if not isinstance(equations, str): if not isinstance(equations, str):
raise ValueError("Text component equations must be a string") 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 = { source = {
"equations": equations, "equations": equations,
"declarations": declarations,
"initialEquations": initial_equations,
} }
if not isinstance(parameters, list): if not isinstance(parameters, list):
raise ValueError("Component parameters must be a list") raise ValueError("Component parameters must be a list")

View File

@@ -123,6 +123,14 @@ def emit_model(
f"{body_indent}parameter {parameter_type} {parameter_name} = {value};" 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": if implementation_kind == "graph":
for block in nested_graph.get("blocks", []): for block in nested_graph.get("blocks", []):
block_type = model_name_for(block) block_type = model_name_for(block)
@@ -134,8 +142,9 @@ def emit_model(
f"{body_indent}{junction_type} {_junction_name(junction['id'])};" f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
) )
lines.append(f"{indentation}equation")
if implementation_kind == "graph": if implementation_kind == "graph":
lines.append(f"{indentation}equation")
blocks = {block["id"]: block for block in nested_graph.get("blocks", [])} blocks = {block["id"]: block for block in nested_graph.get("blocks", [])}
junctions = { junctions = {
junction["id"]: junction for junction in nested_graph.get("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. # Connector types may require different equations in future.
lines.append(f"{body_indent}{target} = {source};") lines.append(f"{body_indent}{target} = {source};")
else: else:
initial_equations = str(
implementation.get("source", {}).get("initialEquations", "")
)
initial_equations = expand_bevalues(initial_equations, macros)
equations = str(implementation.get("source", {}).get("equations", "")) equations = str(implementation.get("source", {}).get("equations", ""))
equations = expand_bevalues(equations, macros) 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( lines.extend(
f"{body_indent}{line}" if line.strip() else "" f"{body_indent}{line}" if line.strip() else ""
for line in equations.splitlines() for line in equations.splitlines()
@@ -161,7 +183,6 @@ def emit_model(
lines.append(f"{indentation}end {model_name};") lines.append(f"{indentation}end {model_name};")
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"
def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]: def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]:
"""Remove annotations and UI-only data from a serialized component tree.""" """Remove annotations and UI-only data from a serialized component tree."""

View File

@@ -47,13 +47,7 @@ class Simulation:
"""Compose and retain the active graph's Modelica representation.""" """Compose and retain the active graph's Modelica representation."""
self.model_path = None self.model_path = None
self.compose_source(graph)
# 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
def _model_compiled(result): def _model_compiled(result):
log.info("Compiling OK: %s", result) log.info("Compiling OK: %s", result)
@@ -79,6 +73,17 @@ class Simulation:
error_callback, 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( def run_simulation(
self, self,
graph: dict[str, Any], graph: dict[str, Any],

View File

@@ -168,7 +168,13 @@ class DocumentController(QObject):
name=self._available_component_name(base_name, self.document.roots.values(), number), name=self._available_component_name(base_name, self.document.roots.values(), number),
implementation_kind=kind, implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"), 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.undo_stack.push(AddComponentCommand(self, None, component))
self.activate_component(component.id) self.activate_component(component.id)
@@ -187,7 +193,13 @@ class DocumentController(QObject):
name=self._available_component_name(base_name, owner.graph.blocks.values(), number), name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
implementation_kind=kind, implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"), 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)) self.undo_stack.push(AddComponentCommand(self, owner_id, component))
return component.id return component.id
@@ -427,6 +439,12 @@ class DocumentController(QObject):
raise ValueError("Open a graph component before composing") raise ValueError("Open a graph component before composing")
self.simulation.compose(component.to_dict()) 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( def run_simulation(
self, self,
progress_callback=None, progress_callback=None,
@@ -636,6 +654,8 @@ class DocumentController(QObject):
self, self,
inputs: list[Port], inputs: list[Port],
outputs: list[Port], outputs: list[Port],
declarations: str,
initial_equations: str,
equations: str, equations: str,
parameters: list[Parameter], parameters: list[Parameter],
) -> None: ) -> None:
@@ -682,6 +702,8 @@ class DocumentController(QObject):
"outputs": [port.to_dict() for port in outputs], "outputs": [port.to_dict() for port in outputs],
"source": { "source": {
"equations": equations, "equations": equations,
"declarations": declarations,
"initialEquations": initial_equations,
}, },
"parameters": [parameter.to_dict() for parameter in parameters], "parameters": [parameter.to_dict() for parameter in parameters],
} }

View File

@@ -14,7 +14,7 @@ PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1
class TextDefinitionEditor(QWidget): 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) modifiedChanged = Signal(bool)
definitionEdited = Signal() definitionEdited = Signal()
@@ -32,7 +32,10 @@ class TextDefinitionEditor(QWidget):
QHeaderView.ResizeMode.Stretch QHeaderView.ResizeMode.Stretch
) )
self.ui.columnSplitter.setSizes([560, 340]) self.ui.columnSplitter.setSizes([560, 340])
self.ui.sourceSplitter.setSizes([180, 180, 240])
self.ui.definitionSplitter.setSizes([300, 300]) 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.equationsEdit.textChanged.connect(self._mark_modified)
self.ui.portsTable.cellChanged.connect(self._symbols_modified) self.ui.portsTable.cellChanged.connect(self._symbols_modified)
self.ui.parametersTable.cellChanged.connect(self._symbols_modified) self.ui.parametersTable.cellChanged.connect(self._symbols_modified)
@@ -52,6 +55,14 @@ class TextDefinitionEditor(QWidget):
def equations(self) -> str: def equations(self) -> str:
return self.ui.equationsEdit.toPlainText() 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 @property
def ports(self) -> tuple[list[Port], list[Port]]: def ports(self) -> tuple[list[Port], list[Port]]:
inputs: list[Port] = [] inputs: list[Port] = []
@@ -87,12 +98,16 @@ class TextDefinitionEditor(QWidget):
def set_definition( def set_definition(
self, self,
declarations: str,
initial_equations: str,
equations: str, equations: str,
inputs: list[Port], inputs: list[Port],
outputs: list[Port], outputs: list[Port],
parameters: list[Parameter], parameters: list[Parameter],
) -> None: ) -> None:
self._loading = True self._loading = True
self.ui.declarationsEdit.setPlainText(declarations)
self.ui.initialEquationsEdit.setPlainText(initial_equations)
self.ui.equationsEdit.setPlainText(equations) self.ui.equationsEdit.setPlainText(equations)
self.ui.portsTable.setRowCount(0) self.ui.portsTable.setRowCount(0)
for port in inputs: for port in inputs:
@@ -102,11 +117,7 @@ class TextDefinitionEditor(QWidget):
self.ui.parametersTable.setRowCount(0) self.ui.parametersTable.setRowCount(0)
for parameter in parameters: for parameter in parameters:
self._append_parameter(parameter) self._append_parameter(parameter)
self.ui.equationsEdit.set_symbols( self._set_editor_symbols(inputs, outputs, parameters)
[port.name for port in inputs],
[port.name for port in outputs],
[parameter.name for parameter in parameters],
)
self._loading = False self._loading = False
self.set_modified(False) self.set_modified(False)
self._update_buttons() self._update_buttons()
@@ -128,11 +139,22 @@ class TextDefinitionEditor(QWidget):
def _refresh_editor_symbols(self) -> None: def _refresh_editor_symbols(self) -> None:
inputs, outputs = self.ports 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 inputs],
[port.name for port in outputs], [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: def _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox:
combo = QComboBox(self) combo = QComboBox(self)

View File

@@ -56,50 +56,53 @@ class Ui_MainWindow(object):
icon4 = QIcon() icon4 = QIcon()
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationWindow.setIcon(icon4) 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 = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew") self.actionNew.setObjectName(u"actionNew")
icon5 = QIcon() icon6 = QIcon()
icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon6.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon5) self.actionNew.setIcon(icon6)
self.actionRotateClockwise = QAction(MainWindow) self.actionRotateClockwise = QAction(MainWindow)
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise") self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
icon6 = QIcon() icon7 = QIcon()
icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon7.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon6) self.actionRotateClockwise.setIcon(icon7)
self.actionZoomIn = QAction(MainWindow) self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn") self.actionZoomIn.setObjectName(u"actionZoomIn")
icon7 = QIcon() icon8 = QIcon()
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon8.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon7) self.actionZoomIn.setIcon(icon8)
self.actionZoomOut = QAction(MainWindow) self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut") self.actionZoomOut.setObjectName(u"actionZoomOut")
icon8 = QIcon() icon9 = QIcon()
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon9.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon8) self.actionZoomOut.setIcon(icon9)
self.actionCenterView = QAction(MainWindow) self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView") self.actionCenterView.setObjectName(u"actionCenterView")
icon9 = QIcon() icon10 = QIcon()
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon10.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon9) self.actionCenterView.setIcon(icon10)
self.actionOpen = QAction(MainWindow) self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen") self.actionOpen.setObjectName(u"actionOpen")
icon10 = QIcon() icon11 = QIcon()
icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon11.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon10) self.actionOpen.setIcon(icon11)
self.actionReloadLibraries = QAction(MainWindow) self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries") self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionReloadSimulation = QAction(MainWindow) self.actionReloadSimulation = QAction(MainWindow)
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation") self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
self.actionSave = QAction(MainWindow) self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave") self.actionSave.setObjectName(u"actionSave")
icon11 = QIcon() icon12 = QIcon()
icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon12.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon11) self.actionSave.setIcon(icon12)
self.actionSaveAs = QAction(MainWindow) self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs") self.actionSaveAs.setObjectName(u"actionSaveAs")
icon12 = QIcon() self.actionSaveAs.setIcon(icon5)
icon12.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon12)
self.actionExit = QAction(MainWindow) self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit") self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow) self.actionClose = QAction(MainWindow)
@@ -295,7 +298,7 @@ class Ui_MainWindow(object):
self.rotateToolButton = QToolButton(self.workspaceHeader) self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton") self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon6) self.rotateToolButton.setIcon(icon7)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton) self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
@@ -441,6 +444,7 @@ class Ui_MainWindow(object):
self.menuSimulation.addAction(self.actionSimulationSettings) self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters) self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompose) self.menuSimulation.addAction(self.actionCompose)
self.menuSimulation.addAction(self.actionExportModel)
self.menuSimulation.addAction(self.actionSimulationWindow) self.menuSimulation.addAction(self.actionSimulationWindow)
self.menuSimulation.addAction(self.actionRunSimulation) self.menuSimulation.addAction(self.actionRunSimulation)
self.fileToolbar.addAction(self.actionNew) self.fileToolbar.addAction(self.actionNew)
@@ -496,6 +500,10 @@ class Ui_MainWindow(object):
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None)) self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
#if QT_CONFIG(statustip) #if QT_CONFIG(statustip)
self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None)) 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) #endif // QT_CONFIG(statustip)
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None)) self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
#if QT_CONFIG(statustip) #if QT_CONFIG(statustip)

View File

@@ -34,7 +34,31 @@ class Ui_TextDefinitionEditor(object):
self.columnSplitter.setObjectName(u"columnSplitter") self.columnSplitter.setObjectName(u"columnSplitter")
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal) self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
self.columnSplitter.setChildrenCollapsible(False) 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.equationsGroup.setObjectName(u"equationsGroup")
self.equationsLayout = QVBoxLayout(self.equationsGroup) self.equationsLayout = QVBoxLayout(self.equationsGroup)
self.equationsLayout.setObjectName(u"equationsLayout") self.equationsLayout.setObjectName(u"equationsLayout")
@@ -43,7 +67,8 @@ class Ui_TextDefinitionEditor(object):
self.equationsLayout.addWidget(self.equationsEdit) 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 = QSplitter(self.columnSplitter)
self.definitionSplitter.setObjectName(u"definitionSplitter") self.definitionSplitter.setObjectName(u"definitionSplitter")
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical) self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
@@ -141,6 +166,8 @@ class Ui_TextDefinitionEditor(object):
# setupUi # setupUi
def retranslateUi(self, TextDefinitionEditor): 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.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None)) self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0) ___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)

View File

@@ -181,6 +181,7 @@ class MainWindow(QMainWindow):
) )
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters) self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
self.ui.actionCompose.triggered.connect(self.compose_active_graph) 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.actionSimulationWindow.triggered.connect(self.show_simulation_window)
self.ui.actionRunSimulation.triggered.connect(self.run_simulation) self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled) 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) self.log.error("Composition failed: %s", error)
QMessageBox.warning(self, "Cannot compose", str(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() @Slot()
def show_simulation_window(self) -> None: def show_simulation_window(self) -> None:
self._simulation_window.show() self._simulation_window.show()
@@ -307,6 +337,7 @@ class MainWindow(QMainWindow):
self.ui.actionSimulationSettings.setEnabled(False) self.ui.actionSimulationSettings.setEnabled(False)
self.ui.actionGraphParameters.setEnabled(False) self.ui.actionGraphParameters.setEnabled(False)
self.ui.actionCompose.setEnabled(False) self.ui.actionCompose.setEnabled(False)
self.ui.actionExportModel.setEnabled(False)
self.ui.actionRunSimulation.setEnabled(False) self.ui.actionRunSimulation.setEnabled(False)
self._update_edit_actions() self._update_edit_actions()
return return
@@ -318,6 +349,7 @@ class MainWindow(QMainWindow):
self.ui.actionSimulationSettings.setEnabled(is_graph) self.ui.actionSimulationSettings.setEnabled(is_graph)
self.ui.actionGraphParameters.setEnabled(is_graph) self.ui.actionGraphParameters.setEnabled(is_graph)
self.ui.actionCompose.setEnabled(is_graph) self.ui.actionCompose.setEnabled(is_graph)
self.ui.actionExportModel.setEnabled(is_graph)
self.ui.actionRunSimulation.setEnabled(is_graph) self.ui.actionRunSimulation.setEnabled(is_graph)
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget( self.ui.workspaceStack.setCurrentWidget(
@@ -505,6 +537,8 @@ class MainWindow(QMainWindow):
if component is None: if component is None:
return return
self.ui.textDefinitionEditor.set_definition( self.ui.textDefinitionEditor.set_definition(
component.source.get("declarations", ""),
component.source.get("initialEquations", ""),
component.source.get("equations", ""), component.source.get("equations", ""),
component.inputs, component.inputs,
component.outputs, component.outputs,
@@ -522,7 +556,8 @@ class MainWindow(QMainWindow):
answer = QMessageBox.question( answer = QMessageBox.question(
self, self,
"Apply text component changes?", "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.Apply
| QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel, | QMessageBox.StandardButton.Cancel,
@@ -540,6 +575,8 @@ class MainWindow(QMainWindow):
self.document_controller.replace_active_text_definition( self.document_controller.replace_active_text_definition(
inputs, inputs,
outputs, outputs,
self.ui.textDefinitionEditor.declarations,
self.ui.textDefinitionEditor.initial_equations,
self.ui.textDefinitionEditor.equations, self.ui.textDefinitionEditor.equations,
self.ui.textDefinitionEditor.parameters, self.ui.textDefinitionEditor.parameters,
) )
@@ -672,6 +709,8 @@ class MainWindow(QMainWindow):
if scene is not None: if scene is not None:
scene.update() scene.update()
self.ui.graphView.viewport().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() self.ui.textDefinitionEditor.ui.equationsEdit.reload_highlighting()
def _document_opened_changed(self, opened: bool) -> None: def _document_opened_changed(self, opened: bool) -> None:

View File

@@ -691,7 +691,27 @@ def _safe_file_stem(model_name: str) -> str:
def _signal_tree_parts(signal_name: str) -> tuple[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] = [] parts: list[str] = []
for segment in signal_name.split("."): for segment in signal_name.split("."):

View File

@@ -552,6 +552,7 @@
<addaction name="actionSimulationSettings"/> <addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/> <addaction name="actionGraphParameters"/>
<addaction name="actionCompose"/> <addaction name="actionCompose"/>
<addaction name="actionExportModel"/>
<addaction name="actionSimulationWindow"/> <addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/> <addaction name="actionRunSimulation"/>
</widget> </widget>
@@ -715,6 +716,18 @@
<string>Show the simulation results window</string> <string>Show the simulation results window</string>
</property> </property>
</action> </action>
<action name="actionExportModel">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Export Model…</string>
</property>
<property name="statusTip">
<string>Save the composed OpenModelica model to a file</string>
</property>
</action>
<action name="actionNew"> <action name="actionNew">
<property name="icon"> <property name="icon">
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">

View File

@@ -12,12 +12,28 @@
<widget class="QSplitter" name="columnSplitter"> <widget class="QSplitter" name="columnSplitter">
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property> <property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property> <property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QSplitter" name="sourceSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="declarationsGroup">
<property name="title"><string>Declarations</string></property>
<layout class="QVBoxLayout" name="declarationsLayout">
<item><widget class="OpenModelicaEditor" name="declarationsEdit"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="initialEquationsGroup">
<property name="title"><string>Initial Equations</string></property>
<layout class="QVBoxLayout" name="initialEquationsLayout">
<item><widget class="OpenModelicaEditor" name="initialEquationsEdit"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="equationsGroup"> <widget class="QGroupBox" name="equationsGroup">
<property name="title"><string>Equations</string></property> <property name="title"><string>Equations</string></property>
<layout class="QVBoxLayout" name="equationsLayout"> <layout class="QVBoxLayout" name="equationsLayout">
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item> <item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
</layout> </layout>
</widget> </widget>
</widget>
<widget class="QSplitter" name="definitionSplitter"> <widget class="QSplitter" name="definitionSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property> <property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property> <property name="childrenCollapsible"><bool>false</bool></property>

View File

@@ -165,7 +165,9 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": "y = v;" "equations": "y = v;",
"declarations": "",
"initialEquations": ""
} }
} }
}, },
@@ -270,7 +272,9 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": "y = k*u;" "equations": "y = k*u;",
"declarations": "",
"initialEquations": ""
} }
} }
}, },
@@ -377,7 +381,9 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": "y = v+sin(10*time);" "equations": "y = v+sin(10*time);",
"declarations": "",
"initialEquations": ""
} }
} }
}, },
@@ -482,7 +488,9 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": "y = k*u;" "equations": "y = k*u;",
"declarations": "",
"initialEquations": ""
} }
} }
}, },
@@ -579,7 +587,255 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "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": { "properties": {
"waypoints": [] "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": [], "annotations": [],