better signal graphing

This commit is contained in:
2026-07-21 15:24:24 +02:00
parent 35d933ccbb
commit eb8f77ce64
4 changed files with 82 additions and 8 deletions

View File

@@ -131,6 +131,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
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 Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks. `clear_result_views()`/`load_result_views()` hooks.
- Simulation-window geometry, dock/toolbar state, and central-results visibility
persist under `simulationWindow/` through `application_settings()`.
- Standalone simulation results are modeled in `core/simulation/results.py`. - Standalone simulation results are modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document. traces so the simulation window can open results without an active document.
@@ -149,6 +151,9 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
Signals tree checkboxes edit the active graph's traces, and each page embeds a Signals tree checkboxes edit the active graph's traces, and each page embeds a
Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default
`time`), selectable from the Signals tree context menu. `time`), selectable from the Signals tree context menu.
- Rerunning the same composed model retains graph tabs, ordering, titles, X axes,
and surviving trace settings while replacing numeric data. Missing signals are
pruned from traces/X-axis selection and newly returned columns appear in the tree.
- 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

View File

@@ -274,7 +274,7 @@ class MainWindow(QMainWindow):
self.show_simulation_window() self.show_simulation_window()
try: try:
self.document_controller.run_simulation(*callbacks) self.document_controller.run_simulation(*callbacks)
window.set_model_name(self.simulation.model_name) window.prepare_run_model(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

@@ -4,6 +4,7 @@ from pathlib import Path
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure from matplotlib.figure import Figure
from PySide6.QtCore import Qt, Signal from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFileDialog, QFileDialog,
QInputDialog, QInputDialog,
@@ -25,6 +26,7 @@ from bedit.core.simulation.results import (
save_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
from bedit.gui.preferences import application_settings
class SimulationWindow(QMainWindow): class SimulationWindow(QMainWindow):
@@ -39,6 +41,7 @@ class SimulationWindow(QMainWindow):
super().__init__(parent) super().__init__(parent)
self.ui = Ui_SimulationWindow() self.ui = Ui_SimulationWindow()
self.ui.setupUi(self) self.ui.setupUi(self)
self.settings = application_settings()
self._running = False self._running = False
self._run_generation = 0 self._run_generation = 0
self._file_path: Path | None = None self._file_path: Path | None = None
@@ -51,6 +54,7 @@ class SimulationWindow(QMainWindow):
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
) )
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height()) self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
self._restore_window_layout()
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)
@@ -84,12 +88,40 @@ class SimulationWindow(QMainWindow):
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction()) self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.workspaceToolbar.toggleViewAction()) self.ui.menuToolbars.addAction(self.ui.workspaceToolbar.toggleViewAction())
def begin_run(self, model_name: str = "") -> tuple: def _restore_window_layout(self) -> None:
"""Reset the viewer and return callbacks bound to this run.""" geometry = self.settings.value("simulationWindow/geometry")
if geometry is not None:
self.restoreGeometry(geometry)
state = self.settings.value("simulationWindow/state")
if state is not None:
self.restoreState(state)
results_visible = self.settings.value(
"simulationWindow/resultsVisible", True, type=bool
)
self.ui.actionToggleResults.setChecked(results_visible)
self.clear(model_name=model_name) def _save_window_layout(self) -> None:
self.settings.setValue("simulationWindow/geometry", self.saveGeometry())
self.settings.setValue("simulationWindow/state", self.saveState())
self.settings.setValue(
"simulationWindow/resultsVisible",
self.ui.actionToggleResults.isChecked(),
)
self.settings.sync()
def begin_run(self) -> tuple:
"""Reset transient run state while retaining the current workspace."""
self._run_generation += 1
generation = self._run_generation generation = self._run_generation
self._running = True self._running = True
self._file_path = None
self.results.status = {}
self.results.messages.clear()
self.results.metadata.clear()
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.ui.statusLabel.setText("Preparing simulation…") self.ui.statusLabel.setText("Preparing simulation…")
return ( return (
lambda progress: self._report_progress(generation, progress), lambda progress: self._report_progress(generation, progress),
@@ -98,10 +130,19 @@ class SimulationWindow(QMainWindow):
lambda error: self._report_error(generation, error), lambda error: self._report_error(generation, error),
) )
def set_model_name(self, model_name: str | None) -> None: def prepare_run_model(self, model_name: str | None) -> None:
if model_name: """Retain graph configuration only when rerunning the same model."""
if not model_name:
return
if self.results.model_name != model_name:
self.results = SimulationResults(model_name=model_name)
self.clear_result_views()
self.load_result_views()
else:
self.results.model_name = model_name self.results.model_name = model_name
self._update_title() self.ui.statusLabel.setText("Preparing simulation…")
self._update_title()
def clear(self, checked: bool = False, *, model_name: str = "") -> None: def clear(self, checked: bool = False, *, model_name: str = "") -> None:
"""Discard the displayed run and prepare an empty results document.""" """Discard the displayed run and prepare an empty results document."""
@@ -141,12 +182,23 @@ class SimulationWindow(QMainWindow):
self._rebuild_signal_tree() self._rebuild_signal_tree()
def _rebuild_graph_tabs(self) -> None: def _rebuild_graph_tabs(self) -> None:
current_page = self.ui.graphTabs.currentWidget()
current_graph_id = (
current_page.graph_id
if isinstance(current_page, GraphWorkspacePage)
else None
)
self._rebuilding_graph_tabs = True self._rebuilding_graph_tabs = True
self._clear_graph_tab_widgets() self._clear_graph_tab_widgets()
for graph in self.results.graphs: for graph in self.results.graphs:
self.ui.graphTabs.addTab( self.ui.graphTabs.addTab(
GraphWorkspacePage(graph, self.results), graph.title GraphWorkspacePage(graph, self.results), graph.title
) )
if current_graph_id is not None:
for index, graph in enumerate(self.results.graphs):
if graph.id == current_graph_id:
self.ui.graphTabs.setCurrentIndex(index)
break
self._rebuilding_graph_tabs = False self._rebuilding_graph_tabs = False
self._update_graph_actions() self._update_graph_actions()
self._sync_signal_checks() self._sync_signal_checks()
@@ -439,6 +491,19 @@ class SimulationWindow(QMainWindow):
self.results.status.update(phase="Simulation finished", progress=10000) self.results.status.update(phase="Simulation finished", progress=10000)
if isinstance(result, SimulationExecutionResult): if isinstance(result, SimulationExecutionResult):
self.results.data = result.data self.results.data = result.data
available_signals = set(result.data)
for graph in self.results.graphs:
graph.traces = [
trace
for trace in graph.traces
if trace.name in available_signals
]
if graph.x_axis not in available_signals:
graph.x_axis = (
"time"
if "time" in available_signals
else next(iter(result.data), "")
)
self.results.metadata["processReturnCode"] = result.return_code self.results.metadata["processReturnCode"] = result.return_code
self.results.metadata["sourceResultFile"] = Path(result.result_file).name self.results.metadata["sourceResultFile"] = Path(result.result_file).name
else: else:
@@ -464,6 +529,10 @@ class SimulationWindow(QMainWindow):
"<p>View live progress and open or save simulation results.</p>", "<p>View live progress and open or save simulation results.</p>",
) )
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name)
self._save_window_layout()
super().closeEvent(event)
@property @property
def is_running(self) -> bool: def is_running(self) -> bool:
return self._running return self._running

View File

@@ -377,7 +377,7 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": "y = v+sin(time);" "equations": "y = v+sin(10*time);"
} }
} }
}, },