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
Designer-owned `resultsLayout` and the
`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`.
Its versioned schema retains model status, messages, metadata, and plottable
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
Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default
`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
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
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()
try:
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:
self.log.exception("Simulation run failed")
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.figure import Figure
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QFileDialog,
QInputDialog,
@@ -25,6 +26,7 @@ from bedit.core.simulation.results import (
save_simulation_results,
)
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
from bedit.gui.preferences import application_settings
class SimulationWindow(QMainWindow):
@@ -39,6 +41,7 @@ class SimulationWindow(QMainWindow):
super().__init__(parent)
self.ui = Ui_SimulationWindow()
self.ui.setupUi(self)
self.settings = application_settings()
self._running = False
self._run_generation = 0
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.setFixedHeight(self.ui.statusDock.sizeHint().height())
self._restore_window_layout()
self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message)
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.workspaceToolbar.toggleViewAction())
def begin_run(self, model_name: str = "") -> tuple:
"""Reset the viewer and return callbacks bound to this run."""
def _restore_window_layout(self) -> None:
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
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…")
return (
lambda progress: self._report_progress(generation, progress),
@@ -98,9 +130,18 @@ class SimulationWindow(QMainWindow):
lambda error: self._report_error(generation, error),
)
def set_model_name(self, model_name: str | None) -> None:
if model_name:
def prepare_run_model(self, model_name: str | None) -> None:
"""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.ui.statusLabel.setText("Preparing simulation…")
self._update_title()
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
@@ -141,12 +182,23 @@ class SimulationWindow(QMainWindow):
self._rebuild_signal_tree()
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._clear_graph_tab_widgets()
for graph in self.results.graphs:
self.ui.graphTabs.addTab(
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._update_graph_actions()
self._sync_signal_checks()
@@ -439,6 +491,19 @@ class SimulationWindow(QMainWindow):
self.results.status.update(phase="Simulation finished", progress=10000)
if isinstance(result, SimulationExecutionResult):
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["sourceResultFile"] = Path(result.result_file).name
else:
@@ -464,6 +529,10 @@ class SimulationWindow(QMainWindow):
"<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
def is_running(self) -> bool:
return self._running

View File

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