diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index c78f462..b7fdd17 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -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. - The application owns one reusable `SimulationWindow`. Starting a run clears 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 `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must not read `QSettings`. An empty path uses OMPython/PATH discovery, while an diff --git a/BEdit/m_Test-results.json b/BEdit/m_Test-results.json new file mode 100644 index 0000000..d2631c4 --- /dev/null +++ b/BEdit/m_Test-results.json @@ -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 + } +} diff --git a/BEdit/src/bedit/core/simulation/__init__.py b/BEdit/src/bedit/core/simulation/__init__.py index 088f9ef..e144cbd 100644 --- a/BEdit/src/bedit/core/simulation/__init__.py +++ b/BEdit/src/bedit/core/simulation/__init__.py @@ -1,4 +1,10 @@ from bedit.core.simulation.service import Simulation from bedit.core.simulation.openmodelica import OpenModelicaInterface +from bedit.core.simulation.results import SimulationResults, SimulationTrace -__all__ = ["OpenModelicaInterface", "Simulation"] +__all__ = [ + "OpenModelicaInterface", + "Simulation", + "SimulationResults", + "SimulationTrace", +] diff --git a/BEdit/src/bedit/core/simulation/results.py b/BEdit/src/bedit/core/simulation/results.py new file mode 100644 index 0000000..3f5f1f8 --- /dev/null +++ b/BEdit/src/bedit/core/simulation/results.py @@ -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) diff --git a/BEdit/src/bedit/data/libraries/default.beb b/BEdit/src/bedit/data/libraries/default.beb new file mode 100644 index 0000000..6a1405d Binary files /dev/null and b/BEdit/src/bedit/data/libraries/default.beb differ diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index 16bf1ef..69f44d7 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -51,6 +51,9 @@ class Ui_MainWindow(object): icon3 = QIcon() icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionRunSimulation.setIcon(icon3) + self.actionSimulationWindow = QAction(MainWindow) + self.actionSimulationWindow.setObjectName(u"actionSimulationWindow") + self.actionSimulationWindow.setIcon(icon1) self.actionNew = QAction(MainWindow) self.actionNew.setObjectName(u"actionNew") icon4 = QIcon() @@ -436,6 +439,7 @@ class Ui_MainWindow(object): self.menuSimulation.addAction(self.actionSimulationSettings) self.menuSimulation.addAction(self.actionGraphParameters) self.menuSimulation.addAction(self.actionCompose) + self.menuSimulation.addAction(self.actionSimulationWindow) self.menuSimulation.addAction(self.actionRunSimulation) self.fileToolbar.addAction(self.actionNew) self.fileToolbar.addAction(self.actionOpen) @@ -452,6 +456,7 @@ class Ui_MainWindow(object): self.simulationToolbar.addAction(self.actionSimulationSettings) self.simulationToolbar.addAction(self.actionGraphParameters) self.simulationToolbar.addAction(self.actionCompose) + self.simulationToolbar.addAction(self.actionSimulationWindow) self.simulationToolbar.addAction(self.actionRunSimulation) self.retranslateUi(MainWindow) @@ -486,6 +491,10 @@ class Ui_MainWindow(object): #if QT_CONFIG(shortcut) self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None)) #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)) #if QT_CONFIG(statustip) self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None)) diff --git a/BEdit/src/bedit/gui/generated/ui_simulation_window.py b/BEdit/src/bedit/gui/generated/ui_simulation_window.py index 9c9d386..9e3ad7d 100644 --- a/BEdit/src/bedit/gui/generated/ui_simulation_window.py +++ b/BEdit/src/bedit/gui/generated/ui_simulation_window.py @@ -11,84 +11,169 @@ from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, QMetaObject, QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QLabel, QListWidget, QListWidgetItem, - QMainWindow, QProgressBar, QSizePolicy, QTabWidget, - QVBoxLayout, QWidget) +from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, + QCursor, QFont, QFontDatabase, QGradient, + QIcon, QImage, QKeySequence, QLinearGradient, + QPainter, QPalette, QPixmap, QRadialGradient, + QTransform) +from PySide6.QtWidgets import (QApplication, QDockWidget, QLabel, QListWidget, + QListWidgetItem, QMainWindow, QMenu, QMenuBar, + QProgressBar, QSizePolicy, QToolBar, QVBoxLayout, + QWidget) +from . import resources_rc class Ui_SimulationWindow(object): def setupUi(self, SimulationWindow): if not SimulationWindow.objectName(): 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.setObjectName(u"centralWidget") - self.windowLayout = QVBoxLayout(self.centralWidget) - self.windowLayout.setObjectName(u"windowLayout") - self.statusLabel = QLabel(self.centralWidget) - self.statusLabel.setObjectName(u"statusLabel") - - self.windowLayout.addWidget(self.statusLabel) - - self.progressBar = QProgressBar(self.centralWidget) - self.progressBar.setObjectName(u"progressBar") - self.progressBar.setMaximum(10000) - self.progressBar.setValue(0) - - self.windowLayout.addWidget(self.progressBar) - - self.timeLabel = QLabel(self.centralWidget) - self.timeLabel.setObjectName(u"timeLabel") - - self.windowLayout.addWidget(self.timeLabel) - - self.resultsTabs = QTabWidget(self.centralWidget) - self.resultsTabs.setObjectName(u"resultsTabs") - self.logTab = QWidget() - self.logTab.setObjectName(u"logTab") - self.logLayout = QVBoxLayout(self.logTab) - 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.centralWidget.setMaximumSize(QSize(0, 0)) + SimulationWindow.setCentralWidget(self.centralWidget) + self.menuBar = QMenuBar(SimulationWindow) + self.menuBar.setObjectName(u"menuBar") + self.menuFile = QMenu(self.menuBar) + self.menuFile.setObjectName(u"menuFile") + self.menuView = QMenu(self.menuBar) + self.menuView.setObjectName(u"menuView") + self.menuPanels = QMenu(self.menuView) + self.menuPanels.setObjectName(u"menuPanels") + self.menuToolbars = QMenu(self.menuView) + self.menuToolbars.setObjectName(u"menuToolbars") + self.menuHelp = QMenu(self.menuBar) + self.menuHelp.setObjectName(u"menuHelp") + SimulationWindow.setMenuBar(self.menuBar) + self.fileToolbar = QToolBar(SimulationWindow) + self.fileToolbar.setObjectName(u"fileToolbar") + self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) + SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar) + self.resultsDock = QDockWidget(SimulationWindow) + self.resultsDock.setObjectName(u"resultsDock") + self.resultsDockContents = QWidget() + self.resultsDockContents.setObjectName(u"resultsDockContents") + self.resultsLayout = QVBoxLayout(self.resultsDockContents) self.resultsLayout.setObjectName(u"resultsLayout") - self.resultsPlaceholder = QLabel(self.resultsTab) + self.resultsPlaceholder = QLabel(self.resultsDockContents) self.resultsPlaceholder.setObjectName(u"resultsPlaceholder") self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter) 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.resultsTabs.setCurrentIndex(0) - - QMetaObject.connectSlotsByName(SimulationWindow) # setupUi def retranslateUi(self, SimulationWindow): 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.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", 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.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)) + self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None)) # retranslateUi diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index 715825b..b32d828 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -169,6 +169,7 @@ class MainWindow(QMainWindow): ) self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters) 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.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.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) 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() def run_simulation(self) -> None: window = self._simulation_window callbacks = window.begin_run() - window.show() - window.raise_() - window.activateWindow() + self.show_simulation_window() try: self.document_controller.run_simulation(*callbacks) + window.set_model_name(self.simulation.model_name) except Exception as error: self.log.exception("Simulation run failed") window.report_start_error(error) diff --git a/BEdit/src/bedit/gui/simulation_window.py b/BEdit/src/bedit/gui/simulation_window.py index 51f723d..d4fee22 100644 --- a/BEdit/src/bedit/gui/simulation_window.py +++ b/BEdit/src/bedit/gui/simulation_window.py @@ -1,12 +1,20 @@ +import re +from pathlib import Path + 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.results import ( + SimulationResults, + load_simulation_results, + save_simulation_results, +) from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow class SimulationWindow(QMainWindow): - """Persistent, reusable view of simulation progress and results.""" + """Persistent viewer for live and previously saved simulation results.""" progressReceived = Signal(object) messageReceived = Signal(object) @@ -19,22 +27,37 @@ class SimulationWindow(QMainWindow): self.ui.setupUi(self) self._running = False 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.messageReceived.connect(self._show_message) self.simulationFinished.connect(self._show_finished) self.simulationFailed.connect(self._show_error) - def begin_run(self) -> tuple: - """Reset the view and return callbacks bound to this particular run.""" + def _connect_actions(self) -> None: + 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 self._running = True 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 ( lambda progress: self._report_progress(generation, progress), lambda message: self._report_message(generation, message), @@ -42,17 +65,110 @@ class SimulationWindow(QMainWindow): lambda error: self._report_error(generation, error), ) - def clear_results(self) -> None: - """Clear future plots and result models before a new run. + def set_model_name(self, model_name: str | None) -> None: + if model_name: + self.results.model_name = model_name + self._update_title() - Add graph widgets and their clearing logic here when simulation result - loading and plotting are implemented. The Designer-owned `resultsLayout` - is the intended container for those widgets. + def clear(self, checked: bool = False, *, model_name: str = "") -> None: + """Discard the displayed run and prepare an empty results document.""" + + 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, generation: int, progress: SimulationProgress - ) -> None: + self.ui.resultsPlaceholder.setText( + "Simulation graphs and result controls can be added here." + ) + + 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: self.progressReceived.emit(progress) @@ -69,34 +185,57 @@ class SimulationWindow(QMainWindow): self.simulationFailed.emit(str(error)) def report_start_error(self, error: Exception) -> None: - """Report an error raised before asynchronous callbacks were installed.""" - self.simulationFailed.emit(str(error)) 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.timeLabel.setText(f"Time: {progress.time:g} s") self.ui.progressBar.setValue(max(0, min(10000, progress.progress))) 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" self.ui.messageList.addItem(f"{prefix}: {message.text}") self.ui.messageList.scrollToBottom() - def _show_finished(self, _result) -> None: + def _show_finished(self, result) -> None: self._running = False + self.results.status.update(phase="Simulation finished", progress=10000) + self.results.metadata["processResult"] = result self.ui.progressBar.setValue(10000) self.ui.statusLabel.setText("Simulation finished") - self.load_results() + self.load_result_views() def _show_error(self, message: str) -> None: 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.messageList.addItem(f"Error: {message}") - def load_results(self) -> None: - """Populate future plots and result controls after a successful run.""" + def show_about(self) -> None: + QMessageBox.about( + self, + "About BEdit Simulation", + "

BEdit Simulation

" + "

View live progress and open or save simulation results.

", + ) @property def is_running(self) -> bool: 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" diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index 7195e60..08cac92 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -552,6 +552,7 @@ + @@ -645,6 +646,7 @@ + @@ -701,6 +703,18 @@ F6 + + + + :/icons/icons/configure.png:/icons/icons/configure.png + + + Simulation Window + + + Show the simulation results window + + diff --git a/BEdit/ui/simulation_window.ui b/BEdit/ui/simulation_window.ui index 371724f..1d6615e 100644 --- a/BEdit/ui/simulation_window.ui +++ b/BEdit/ui/simulation_window.ui @@ -2,90 +2,90 @@ SimulationWindow - - - 0 - 0 - 1031 - 881 - - - - Simulation - + 00900650 + Simulation - - - - - - 0 - 600 - - - - Simulation graphs and result controls can be added here. - - - Qt::AlignmentFlag::AlignCenter - - - - - - - No simulation has been run yet. - - - - - - - - - 10000 - - - 0 - - - %p% - - - - - - - Time: 0 s - - - - - - - - - 0 - - - - Simulation Log - - - - - - true - - - - - - - - + 00 + + + &File + + + + + + + + + &View + &Panels + &Toolbars + + + + + &Help + + + + + + + + + File + Qt::ToolButtonStyle::ToolButtonIconOnly + TopToolBarArea + + + + + + Results + 2 + + + Simulation graphs and result controls can be added here.Qt::AlignmentFlag::AlignCenter + + + + + Status + 8 + + + No simulation has been run yet. + 100000%p% + Time: 0 s + + + + + Log + 8 + + + true + + + + + :/icons/icons/document-open.png:/icons/icons/document-open.png + &Open…Ctrl+O + + + :/icons/icons/document-save.png:/icons/icons/document-save.png + &Save…Ctrl+S + + + :/icons/icons/document-new.png:/icons/icons/document-new.png + &Clear + + E&xitCtrl+W + &About Simulation Window + About &Qt - + diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index 3e3f243..086b172 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -56,7 +56,7 @@ "showName": false }, "library": { - "showSubtree": false + "showSubtree": true }, "implementation": { "kind": "graph",