sim window save/load

This commit is contained in:
2026-07-21 14:01:55 +02:00
parent cdfc891980
commit ddc004dee5
12 changed files with 532 additions and 164 deletions

View File

@@ -124,7 +124,11 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
callbacks; the simulation service retains the latest progress for polling. callbacks; the simulation service retains the latest progress for polling.
- The application owns one reusable `SimulationWindow`. Starting a run clears its - The application owns one reusable `SimulationWindow`. Starting a run clears its
progress, log, and future result views. Extend graph presentation through its progress, log, and future result views. Extend graph presentation through its
Designer-owned `resultsLayout` and the `clear_results()`/`load_results()` hooks. Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks.
- Standalone simulation-result JSON is modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document.
- The optional OpenModelica executable is persisted as - The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an not read `QSettings`. An empty path uses OMPython/PATH discovery, while an

27
BEdit/m_Test-results.json Normal file
View File

@@ -0,0 +1,27 @@
{
"format": "bedit-simulation-results",
"version": 1,
"modelName": "m_Test",
"status": {
"phase": "Simulation finished",
"currentStepSize": 0.0,
"time": 10.0,
"progress": 10000
},
"messages": [
{
"stream": "LOG_SUCCESS",
"type": "info",
"text": "The initialization finished successfully without homotopy method."
},
{
"stream": "LOG_SUCCESS",
"type": "info",
"text": "The simulation finished successfully."
}
],
"traces": [],
"metadata": {
"processResult": 0
}
}

View File

@@ -1,4 +1,10 @@
from bedit.core.simulation.service import Simulation from bedit.core.simulation.service import Simulation
from bedit.core.simulation.openmodelica import OpenModelicaInterface from bedit.core.simulation.openmodelica import OpenModelicaInterface
from bedit.core.simulation.results import SimulationResults, SimulationTrace
__all__ = ["OpenModelicaInterface", "Simulation"] __all__ = [
"OpenModelicaInterface",
"Simulation",
"SimulationResults",
"SimulationTrace",
]

View File

@@ -0,0 +1,78 @@
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
RESULTS_FORMAT = "bedit-simulation-results"
RESULTS_VERSION = 1
@dataclass
class SimulationTrace:
"""One plottable series; samples can be filled by a future result importer."""
name: str
x_values: list[float] = field(default_factory=list)
y_values: list[float] = field(default_factory=list)
x_label: str = "time"
y_label: str = ""
unit: str = ""
properties: dict[str, Any] = field(default_factory=dict)
@dataclass
class SimulationResults:
"""Serializable state displayed by the standalone simulation window."""
model_name: str = ""
status: dict[str, Any] = field(default_factory=dict)
messages: list[dict[str, str]] = field(default_factory=list)
traces: list[SimulationTrace] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"format": RESULTS_FORMAT,
"version": RESULTS_VERSION,
"modelName": self.model_name,
"status": dict(self.status),
"messages": [dict(message) for message in self.messages],
"traces": [asdict(trace) for trace in self.traces],
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "SimulationResults":
if data.get("format") != RESULTS_FORMAT:
raise ValueError("Not a BEdit simulation-results file")
if data.get("version") != RESULTS_VERSION:
raise ValueError(f"Unsupported simulation-results version: {data.get('version')!r}")
try:
traces = [SimulationTrace(**trace) for trace in data.get("traces", [])]
return cls(
model_name=str(data.get("modelName", "")),
status=dict(data.get("status", {})),
messages=[dict(message) for message in data.get("messages", [])],
traces=traces,
metadata=dict(data.get("metadata", {})),
)
except (TypeError, ValueError) as error:
raise ValueError("Malformed simulation-results data") from error
def save_simulation_results(path: str | Path, results: SimulationResults) -> None:
Path(path).write_text(
json.dumps(results.to_dict(), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
def load_simulation_results(path: str | Path) -> SimulationResults:
try:
data = json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"Could not read simulation results: {error}") from error
if not isinstance(data, dict):
raise ValueError("Simulation-results root must be an object")
return SimulationResults.from_dict(data)

Binary file not shown.

View File

@@ -51,6 +51,9 @@ class Ui_MainWindow(object):
icon3 = QIcon() icon3 = QIcon()
icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRunSimulation.setIcon(icon3) self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
self.actionSimulationWindow.setIcon(icon1)
self.actionNew = QAction(MainWindow) self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew") self.actionNew.setObjectName(u"actionNew")
icon4 = QIcon() icon4 = QIcon()
@@ -436,6 +439,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.actionSimulationWindow)
self.menuSimulation.addAction(self.actionRunSimulation) self.menuSimulation.addAction(self.actionRunSimulation)
self.fileToolbar.addAction(self.actionNew) self.fileToolbar.addAction(self.actionNew)
self.fileToolbar.addAction(self.actionOpen) self.fileToolbar.addAction(self.actionOpen)
@@ -452,6 +456,7 @@ class Ui_MainWindow(object):
self.simulationToolbar.addAction(self.actionSimulationSettings) self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters) self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompose) self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionSimulationWindow)
self.simulationToolbar.addAction(self.actionRunSimulation) self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
@@ -486,6 +491,10 @@ class Ui_MainWindow(object):
#if QT_CONFIG(shortcut) #if QT_CONFIG(shortcut)
self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None)) self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None))
#endif // QT_CONFIG(shortcut) #endif // QT_CONFIG(shortcut)
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.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)
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None)) self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))

View File

@@ -11,84 +11,169 @@
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect, QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt) QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QFont, QFontDatabase, QGradient, QIcon, QCursor, QFont, QFontDatabase, QGradient,
QImage, QKeySequence, QLinearGradient, QPainter, QIcon, QImage, QKeySequence, QLinearGradient,
QPalette, QPixmap, QRadialGradient, QTransform) QPainter, QPalette, QPixmap, QRadialGradient,
from PySide6.QtWidgets import (QApplication, QLabel, QListWidget, QListWidgetItem, QTransform)
QMainWindow, QProgressBar, QSizePolicy, QTabWidget, from PySide6.QtWidgets import (QApplication, QDockWidget, QLabel, QListWidget,
QVBoxLayout, QWidget) QListWidgetItem, QMainWindow, QMenu, QMenuBar,
QProgressBar, QSizePolicy, QToolBar, QVBoxLayout,
QWidget)
from . import resources_rc
class Ui_SimulationWindow(object): class Ui_SimulationWindow(object):
def setupUi(self, SimulationWindow): def setupUi(self, SimulationWindow):
if not SimulationWindow.objectName(): if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow") SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(720, 480) SimulationWindow.resize(900, 650)
self.actionOpen = QAction(SimulationWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon = QIcon()
icon.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon)
self.actionSave = QAction(SimulationWindow)
self.actionSave.setObjectName(u"actionSave")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon1)
self.actionClear = QAction(SimulationWindow)
self.actionClear.setObjectName(u"actionClear")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon2)
self.actionExit = QAction(SimulationWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionAbout = QAction(SimulationWindow)
self.actionAbout.setObjectName(u"actionAbout")
self.actionAboutQt = QAction(SimulationWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt")
self.centralWidget = QWidget(SimulationWindow) self.centralWidget = QWidget(SimulationWindow)
self.centralWidget.setObjectName(u"centralWidget") self.centralWidget.setObjectName(u"centralWidget")
self.windowLayout = QVBoxLayout(self.centralWidget) self.centralWidget.setMaximumSize(QSize(0, 0))
self.windowLayout.setObjectName(u"windowLayout") SimulationWindow.setCentralWidget(self.centralWidget)
self.statusLabel = QLabel(self.centralWidget) self.menuBar = QMenuBar(SimulationWindow)
self.statusLabel.setObjectName(u"statusLabel") self.menuBar.setObjectName(u"menuBar")
self.menuFile = QMenu(self.menuBar)
self.windowLayout.addWidget(self.statusLabel) self.menuFile.setObjectName(u"menuFile")
self.menuView = QMenu(self.menuBar)
self.progressBar = QProgressBar(self.centralWidget) self.menuView.setObjectName(u"menuView")
self.progressBar.setObjectName(u"progressBar") self.menuPanels = QMenu(self.menuView)
self.progressBar.setMaximum(10000) self.menuPanels.setObjectName(u"menuPanels")
self.progressBar.setValue(0) self.menuToolbars = QMenu(self.menuView)
self.menuToolbars.setObjectName(u"menuToolbars")
self.windowLayout.addWidget(self.progressBar) self.menuHelp = QMenu(self.menuBar)
self.menuHelp.setObjectName(u"menuHelp")
self.timeLabel = QLabel(self.centralWidget) SimulationWindow.setMenuBar(self.menuBar)
self.timeLabel.setObjectName(u"timeLabel") self.fileToolbar = QToolBar(SimulationWindow)
self.fileToolbar.setObjectName(u"fileToolbar")
self.windowLayout.addWidget(self.timeLabel) self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.resultsTabs = QTabWidget(self.centralWidget) self.resultsDock = QDockWidget(SimulationWindow)
self.resultsTabs.setObjectName(u"resultsTabs") self.resultsDock.setObjectName(u"resultsDock")
self.logTab = QWidget() self.resultsDockContents = QWidget()
self.logTab.setObjectName(u"logTab") self.resultsDockContents.setObjectName(u"resultsDockContents")
self.logLayout = QVBoxLayout(self.logTab) self.resultsLayout = QVBoxLayout(self.resultsDockContents)
self.logLayout.setObjectName(u"logLayout")
self.messageList = QListWidget(self.logTab)
self.messageList.setObjectName(u"messageList")
self.messageList.setAlternatingRowColors(True)
self.logLayout.addWidget(self.messageList)
self.resultsTabs.addTab(self.logTab, "")
self.resultsTab = QWidget()
self.resultsTab.setObjectName(u"resultsTab")
self.resultsLayout = QVBoxLayout(self.resultsTab)
self.resultsLayout.setObjectName(u"resultsLayout") self.resultsLayout.setObjectName(u"resultsLayout")
self.resultsPlaceholder = QLabel(self.resultsTab) self.resultsPlaceholder = QLabel(self.resultsDockContents)
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder") self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter) self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.resultsLayout.addWidget(self.resultsPlaceholder) self.resultsLayout.addWidget(self.resultsPlaceholder)
self.resultsTabs.addTab(self.resultsTab, "") self.resultsDock.setWidget(self.resultsDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.resultsDock)
self.statusDock = QDockWidget(SimulationWindow)
self.statusDock.setObjectName(u"statusDock")
self.statusDockContents = QWidget()
self.statusDockContents.setObjectName(u"statusDockContents")
self.statusLayout = QVBoxLayout(self.statusDockContents)
self.statusLayout.setObjectName(u"statusLayout")
self.statusLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel")
self.windowLayout.addWidget(self.resultsTabs) self.statusLayout.addWidget(self.statusLabel)
SimulationWindow.setCentralWidget(self.centralWidget) self.progressBar = QProgressBar(self.statusDockContents)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000)
self.progressBar.setValue(0)
self.statusLayout.addWidget(self.progressBar)
self.timeLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel")
self.statusLayout.addWidget(self.timeLabel)
self.statusDock.setWidget(self.statusDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock)
self.logDock = QDockWidget(SimulationWindow)
self.logDock.setObjectName(u"logDock")
self.logDockContents = QWidget()
self.logDockContents.setObjectName(u"logDockContents")
self.logLayout = QVBoxLayout(self.logDockContents)
self.logLayout.setObjectName(u"logLayout")
self.messageList = QListWidget(self.logDockContents)
self.messageList.setObjectName(u"messageList")
self.messageList.setAlternatingRowColors(True)
self.logLayout.addWidget(self.messageList)
self.logDock.setWidget(self.logDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logDock)
self.menuBar.addAction(self.menuFile.menuAction())
self.menuBar.addAction(self.menuView.menuAction())
self.menuBar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClear)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionExit)
self.menuView.addAction(self.menuPanels.menuAction())
self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionClear)
self.retranslateUi(SimulationWindow) self.retranslateUi(SimulationWindow)
self.resultsTabs.setCurrentIndex(0)
QMetaObject.connectSlotsByName(SimulationWindow) QMetaObject.connectSlotsByName(SimulationWindow)
# setupUi # setupUi
def retranslateUi(self, SimulationWindow): def retranslateUi(self, SimulationWindow):
SimulationWindow.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Simulation", None)) SimulationWindow.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Simulation", None))
self.actionOpen.setText(QCoreApplication.translate("SimulationWindow", u"&Open\u2026", None))
#if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("SimulationWindow", u"&Save\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut)
self.actionClear.setText(QCoreApplication.translate("SimulationWindow", u"&Clear", None))
self.actionExit.setText(QCoreApplication.translate("SimulationWindow", u"E&xit", None))
#if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
self.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
self.resultsDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", None))
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None)) self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None)) self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None)) self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
self.resultsTabs.setTabText(self.resultsTabs.indexOf(self.logTab), QCoreApplication.translate("SimulationWindow", u"Simulation Log", None)) self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None))
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", None))
self.resultsTabs.setTabText(self.resultsTabs.indexOf(self.resultsTab), QCoreApplication.translate("SimulationWindow", u"Results", None))
# retranslateUi # retranslateUi

View File

@@ -169,6 +169,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.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)
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled) self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
@@ -248,15 +249,20 @@ 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 show_simulation_window(self) -> None:
self._simulation_window.show()
self._simulation_window.raise_()
self._simulation_window.activateWindow()
@Slot() @Slot()
def run_simulation(self) -> None: def run_simulation(self) -> None:
window = self._simulation_window window = self._simulation_window
callbacks = window.begin_run() callbacks = window.begin_run()
window.show() self.show_simulation_window()
window.raise_()
window.activateWindow()
try: try:
self.document_controller.run_simulation(*callbacks) self.document_controller.run_simulation(*callbacks)
window.set_model_name(self.simulation.model_name)
except Exception as error: except Exception as error:
self.log.exception("Simulation run failed") self.log.exception("Simulation run failed")
window.report_start_error(error) window.report_start_error(error)

View File

@@ -1,12 +1,20 @@
import re
from pathlib import Path
from PySide6.QtCore import Signal from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import (
SimulationResults,
load_simulation_results,
save_simulation_results,
)
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
class SimulationWindow(QMainWindow): class SimulationWindow(QMainWindow):
"""Persistent, reusable view of simulation progress and results.""" """Persistent viewer for live and previously saved simulation results."""
progressReceived = Signal(object) progressReceived = Signal(object)
messageReceived = Signal(object) messageReceived = Signal(object)
@@ -19,22 +27,37 @@ class SimulationWindow(QMainWindow):
self.ui.setupUi(self) self.ui.setupUi(self)
self._running = False self._running = False
self._run_generation = 0 self._run_generation = 0
self._file_path: Path | None = None
self.results = SimulationResults()
self._connect_actions()
self._populate_view_menu()
self.progressReceived.connect(self._show_progress) self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message) self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished) self.simulationFinished.connect(self._show_finished)
self.simulationFailed.connect(self._show_error) self.simulationFailed.connect(self._show_error)
def begin_run(self) -> tuple: def _connect_actions(self) -> None:
"""Reset the view and return callbacks bound to this particular run.""" self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_results)
self.ui.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
)
self._run_generation += 1 def _populate_view_menu(self) -> None:
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.resultsDock):
self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
def begin_run(self, model_name: str = "") -> tuple:
"""Reset the viewer and return callbacks bound to this run."""
self.clear(model_name=model_name)
generation = self._run_generation generation = self._run_generation
self._running = True self._running = True
self.ui.statusLabel.setText("Preparing simulation…") self.ui.statusLabel.setText("Preparing simulation…")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_results()
return ( return (
lambda progress: self._report_progress(generation, progress), lambda progress: self._report_progress(generation, progress),
lambda message: self._report_message(generation, message), lambda message: self._report_message(generation, message),
@@ -42,17 +65,110 @@ class SimulationWindow(QMainWindow):
lambda error: self._report_error(generation, error), lambda error: self._report_error(generation, error),
) )
def clear_results(self) -> None: def set_model_name(self, model_name: str | None) -> None:
"""Clear future plots and result models before a new run. if model_name:
self.results.model_name = model_name
self._update_title()
Add graph widgets and their clearing logic here when simulation result def clear(self, checked: bool = False, *, model_name: str = "") -> None:
loading and plotting are implemented. The Designer-owned `resultsLayout` """Discard the displayed run and prepare an empty results document."""
is the intended container for those widgets.
del checked
self._run_generation += 1
self._running = False
self._file_path = None
self.results = SimulationResults(model_name=model_name)
self.ui.statusLabel.setText("No simulation has been run yet.")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_result_views()
self._update_title()
def clear_result_views(self) -> None:
"""Clear custom plots before a run or loaded document is displayed.
Future graph widgets should be placed in the Designer-owned
``resultsLayout`` and reset here.
""" """
def _report_progress( self.ui.resultsPlaceholder.setText(
self, generation: int, progress: SimulationProgress "Simulation graphs and result controls can be added here."
) -> None: )
def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.traces``.
This is the intended integration point for a future plotting widget.
"""
trace_count = len(self.results.traces)
if trace_count:
self.ui.resultsPlaceholder.setText(
f"{trace_count} trace(s) loaded; add graph rendering here."
)
def open_results(self) -> None:
file_name, _selected_filter = QFileDialog.getOpenFileName(
self,
"Open Simulation Results",
"",
"BEdit Simulation Results (*.json);;JSON Files (*.json)",
)
if not file_name:
return
try:
results = load_simulation_results(file_name)
except ValueError as error:
QMessageBox.warning(self, "Cannot Open Simulation Results", str(error))
return
self._run_generation += 1
self._running = False
self._file_path = Path(file_name)
self.results = results
self._display_results()
def save_results(self) -> None:
file_path = self._file_path
if file_path is None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.json"
file_name, _selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results",
default_name,
"BEdit Simulation Results (*.json);;JSON Files (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() != ".json":
file_path = file_path.with_suffix(".json")
try:
save_simulation_results(file_path, self.results)
except OSError as error:
QMessageBox.warning(self, "Cannot Save Simulation Results", str(error))
return
self._file_path = file_path
self._update_title()
def _display_results(self) -> None:
status = self.results.status
self.ui.statusLabel.setText(str(status.get("phase", "Loaded results")))
self.ui.progressBar.setValue(int(status.get("progress", 0)))
self.ui.timeLabel.setText(f"Time: {float(status.get('time', 0)):g} s")
self.ui.messageList.clear()
for message in self.results.messages:
prefix = message.get("stream") or message.get("type") or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.get('text', '')}")
self.clear_result_views()
self.load_result_views()
self._update_title()
def _update_title(self) -> None:
name = self.results.model_name or "Simulation"
self.setWindowTitle(f"{name} — Simulation")
def _report_progress(self, generation: int, progress: SimulationProgress) -> None:
if generation == self._run_generation: if generation == self._run_generation:
self.progressReceived.emit(progress) self.progressReceived.emit(progress)
@@ -69,34 +185,57 @@ class SimulationWindow(QMainWindow):
self.simulationFailed.emit(str(error)) self.simulationFailed.emit(str(error))
def report_start_error(self, error: Exception) -> None: def report_start_error(self, error: Exception) -> None:
"""Report an error raised before asynchronous callbacks were installed."""
self.simulationFailed.emit(str(error)) self.simulationFailed.emit(str(error))
def _show_progress(self, progress: SimulationProgress) -> None: def _show_progress(self, progress: SimulationProgress) -> None:
self.results.status = {
"phase": progress.phase,
"currentStepSize": progress.current_step_size,
"time": progress.time,
"progress": progress.progress,
}
self.ui.statusLabel.setText(progress.phase or "Running") self.ui.statusLabel.setText(progress.phase or "Running")
self.ui.timeLabel.setText(f"Time: {progress.time:g} s") self.ui.timeLabel.setText(f"Time: {progress.time:g} s")
self.ui.progressBar.setValue(max(0, min(10000, progress.progress))) self.ui.progressBar.setValue(max(0, min(10000, progress.progress)))
def _show_message(self, message: SimulationMessage) -> None: def _show_message(self, message: SimulationMessage) -> None:
self.results.messages.append(
{"stream": message.stream, "type": message.type, "text": message.text}
)
prefix = message.stream or message.type or "OpenModelica" prefix = message.stream or message.type or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.text}") self.ui.messageList.addItem(f"{prefix}: {message.text}")
self.ui.messageList.scrollToBottom() self.ui.messageList.scrollToBottom()
def _show_finished(self, _result) -> None: def _show_finished(self, result) -> None:
self._running = False self._running = False
self.results.status.update(phase="Simulation finished", progress=10000)
self.results.metadata["processResult"] = result
self.ui.progressBar.setValue(10000) self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished") self.ui.statusLabel.setText("Simulation finished")
self.load_results() self.load_result_views()
def _show_error(self, message: str) -> None: def _show_error(self, message: str) -> None:
self._running = False self._running = False
self.results.status["phase"] = "Simulation failed"
self.results.messages.append(
{"stream": "BEdit", "type": "error", "text": message}
)
self.ui.statusLabel.setText("Simulation failed") self.ui.statusLabel.setText("Simulation failed")
self.ui.messageList.addItem(f"Error: {message}") self.ui.messageList.addItem(f"Error: {message}")
def load_results(self) -> None: def show_about(self) -> None:
"""Populate future plots and result controls after a successful run.""" QMessageBox.about(
self,
"About BEdit Simulation",
"<h3>BEdit Simulation</h3>"
"<p>View live progress and open or save simulation results.</p>",
)
@property @property
def is_running(self) -> bool: def is_running(self) -> bool:
return self._running return self._running
def _safe_file_stem(model_name: str) -> str:
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
return stem or "simulation"

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="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/> <addaction name="actionRunSimulation"/>
</widget> </widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
@@ -645,6 +646,7 @@
<addaction name="actionSimulationSettings"/> <addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/> <addaction name="actionGraphParameters"/>
<addaction name="actionCompose"/> <addaction name="actionCompose"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/> <addaction name="actionRunSimulation"/>
</widget> </widget>
<action name="actionSimulationSettings"> <action name="actionSimulationSettings">
@@ -701,6 +703,18 @@
<string>F6</string> <string>F6</string>
</property> </property>
</action> </action>
<action name="actionSimulationWindow">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset>
</property>
<property name="text">
<string>Simulation Window</string>
</property>
<property name="statusTip">
<string>Show the simulation results window</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

@@ -2,90 +2,90 @@
<ui version="4.0"> <ui version="4.0">
<class>SimulationWindow</class> <class>SimulationWindow</class>
<widget class="QMainWindow" name="SimulationWindow"> <widget class="QMainWindow" name="SimulationWindow">
<property name="geometry"> <property name="geometry"><rect><x>0</x><y>0</y><width>900</width><height>650</height></rect></property>
<rect> <property name="windowTitle"><string>Simulation</string></property>
<x>0</x>
<y>0</y>
<width>1031</width>
<height>881</height>
</rect>
</property>
<property name="windowTitle">
<string>Simulation</string>
</property>
<widget class="QWidget" name="centralWidget"> <widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="verticalLayout"> <property name="maximumSize"><size><width>0</width><height>0</height></size></property>
<item>
<widget class="QLabel" name="resultsPlaceholder">
<property name="minimumSize">
<size>
<width>0</width>
<height>600</height>
</size>
</property>
<property name="text">
<string>Simulation graphs and result controls can be added here.</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>No simulation has been run yet.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="format">
<string>%p%</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time: 0 s</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTabWidget" name="resultsTabs">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="logTab">
<attribute name="title">
<string>Simulation Log</string>
</attribute>
<layout class="QVBoxLayout" name="logLayout">
<item>
<widget class="QListWidget" name="messageList">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget> </widget>
<widget class="QMenuBar" name="menuBar">
<widget class="QMenu" name="menuFile">
<property name="title"><string>&amp;File</string></property>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="separator"/>
<addaction name="actionClear"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title"><string>&amp;View</string></property>
<widget class="QMenu" name="menuPanels"><property name="title"><string>&amp;Panels</string></property></widget>
<widget class="QMenu" name="menuToolbars"><property name="title"><string>&amp;Toolbars</string></property></widget>
<addaction name="menuPanels"/>
<addaction name="menuToolbars"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title"><string>&amp;Help</string></property>
<addaction name="actionAbout"/>
<addaction name="actionAboutQt"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="fileToolbar">
<property name="windowTitle"><string>File</string></property>
<property name="toolButtonStyle"><enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum></property>
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionClear"/>
</widget>
<widget class="QDockWidget" name="resultsDock">
<property name="windowTitle"><string>Results</string></property>
<attribute name="dockWidgetArea"><number>2</number></attribute>
<widget class="QWidget" name="resultsDockContents">
<layout class="QVBoxLayout" name="resultsLayout">
<item><widget class="QLabel" name="resultsPlaceholder"><property name="text"><string>Simulation graphs and result controls can be added here.</string></property><property name="alignment"><set>Qt::AlignmentFlag::AlignCenter</set></property></widget></item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="statusDock">
<property name="windowTitle"><string>Status</string></property>
<attribute name="dockWidgetArea"><number>8</number></attribute>
<widget class="QWidget" name="statusDockContents">
<layout class="QVBoxLayout" name="statusLayout">
<item><widget class="QLabel" name="statusLabel"><property name="text"><string>No simulation has been run yet.</string></property></widget></item>
<item><widget class="QProgressBar" name="progressBar"><property name="maximum"><number>10000</number></property><property name="value"><number>0</number></property><property name="format"><string>%p%</string></property></widget></item>
<item><widget class="QLabel" name="timeLabel"><property name="text"><string>Time: 0 s</string></property></widget></item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="logDock">
<property name="windowTitle"><string>Log</string></property>
<attribute name="dockWidgetArea"><number>8</number></attribute>
<widget class="QWidget" name="logDockContents">
<layout class="QVBoxLayout" name="logLayout">
<item><widget class="QListWidget" name="messageList"><property name="alternatingRowColors"><bool>true</bool></property></widget></item>
</layout>
</widget>
</widget>
<action name="actionOpen">
<property name="icon"><iconset resource="../resources/resources.qrc"><normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset></property>
<property name="text"><string>&amp;Open…</string></property><property name="shortcut"><string>Ctrl+O</string></property>
</action>
<action name="actionSave">
<property name="icon"><iconset resource="../resources/resources.qrc"><normaloff>:/icons/icons/document-save.png</normaloff>:/icons/icons/document-save.png</iconset></property>
<property name="text"><string>&amp;Save…</string></property><property name="shortcut"><string>Ctrl+S</string></property>
</action>
<action name="actionClear">
<property name="icon"><iconset resource="../resources/resources.qrc"><normaloff>:/icons/icons/document-new.png</normaloff>:/icons/icons/document-new.png</iconset></property>
<property name="text"><string>&amp;Clear</string></property>
</action>
<action name="actionExit"><property name="text"><string>E&amp;xit</string></property><property name="shortcut"><string>Ctrl+W</string></property></action>
<action name="actionAbout"><property name="text"><string>&amp;About Simulation Window</string></property></action>
<action name="actionAboutQt"><property name="text"><string>About &amp;Qt</string></property></action>
</widget> </widget>
<resources/> <resources><include location="../resources/resources.qrc"/></resources>
<connections/> <connections/>
</ui> </ui>

View File

@@ -56,7 +56,7 @@
"showName": false "showName": false
}, },
"library": { "library": {
"showSubtree": false "showSubtree": true
}, },
"implementation": { "implementation": {
"kind": "graph", "kind": "graph",