better signal graphing
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -377,7 +377,7 @@
|
|||||||
"implementation": {
|
"implementation": {
|
||||||
"kind": "text",
|
"kind": "text",
|
||||||
"source": {
|
"source": {
|
||||||
"equations": "y = v+sin(time);"
|
"equations": "y = v+sin(10*time);"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user