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

@@ -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",
"<h3>BEdit Simulation</h3>"
"<p>View live progress and open or save simulation results.</p>",
)
@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"