proper modelica text model editing
This commit is contained in:
@@ -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/<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
|
||||
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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("."):
|
||||
|
||||
@@ -552,6 +552,7 @@
|
||||
<addaction name="actionSimulationSettings"/>
|
||||
<addaction name="actionGraphParameters"/>
|
||||
<addaction name="actionCompose"/>
|
||||
<addaction name="actionExportModel"/>
|
||||
<addaction name="actionSimulationWindow"/>
|
||||
<addaction name="actionRunSimulation"/>
|
||||
</widget>
|
||||
@@ -715,6 +716,18 @@
|
||||
<string>Show the simulation results window</string>
|
||||
</property>
|
||||
</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">
|
||||
<property name="icon">
|
||||
<iconset resource="../resources/resources.qrc">
|
||||
|
||||
@@ -12,11 +12,27 @@
|
||||
<widget class="QSplitter" name="columnSplitter">
|
||||
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
|
||||
<property name="childrenCollapsible"><bool>false</bool></property>
|
||||
<widget class="QGroupBox" name="equationsGroup">
|
||||
<property name="title"><string>Equations</string></property>
|
||||
<layout class="QVBoxLayout" name="equationsLayout">
|
||||
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
|
||||
</layout>
|
||||
<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">
|
||||
<property name="title"><string>Equations</string></property>
|
||||
<layout class="QVBoxLayout" name="equationsLayout">
|
||||
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QSplitter" name="definitionSplitter">
|
||||
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
Reference in New Issue
Block a user