Hooked up simulation to BEsim

This commit is contained in:
2026-08-01 12:25:11 +02:00
parent a38d461a36
commit 2128fb00b4
11 changed files with 8241 additions and 24 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import QObject, Qt
from PySide6.QtCore import QObject, Signal, Qt
from PySide6.QtWidgets import QApplication, QDialog, QFileDialog, QInputDialog, QMessageBox
from bedit_gui.services import document_files, simulation_files
@@ -17,6 +17,8 @@ logger = get_logger(__name__)
class SimulationFileController(QObject):
runtime_changed = Signal()
def __init__(self, window: SimulationWindow) -> None:
super().__init__(window)
self.window = window
@@ -34,6 +36,7 @@ class SimulationFileController(QObject):
self.compiled_model = None
self.path = None
self._update_window()
self.runtime_changed.emit()
logger.info("Created new simulation")
def open(self, path: str | Path, *, component: str | None = None, simulation: str | None = None, backed_by_file: bool = True, compiled_model: CompiledModel | None = None) -> None:
@@ -51,6 +54,7 @@ class SimulationFileController(QObject):
raise
self._log_compilation()
self._update_window()
self.runtime_changed.emit()
logger.info("Opened simulation: %s", file_path)
def open_dialog(self) -> None:
@@ -111,9 +115,12 @@ class SimulationFileController(QObject):
self.root.component = updated.component
self.root.component_path = next((path for component_id, _component, path in components if component_id == updated.component), self.root.component_path)
self.root.settings_name = updated.name
self.root.current_end_time = updated.start_time
self.root.results.clear()
if component_changed:
self.compiled_model = None
self._update_window()
self.runtime_changed.emit()
logger.info("Updated simulation settings: %s", updated.name)
def _source_components(self) -> list[tuple]:

View File

@@ -0,0 +1,155 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from threading import Thread
from PySide6.QtCore import QObject, Signal
from bedit_gui.controllers.simulation_file_controller import SimulationFileController
from bedit_gui.services.application_logging import get_logger
from bedit_gui.simulation_models import CompiledModel
from bedit_gui.views.simulation_window import SimulationWindow
from bedit_simulation import SimulationCancelledError, SimulationResult, SimulationRunSettings, SimulationSession
logger = get_logger(__name__)
SessionFactory = Callable[[CompiledModel, SimulationRunSettings, float | None, list[SimulationResult]], SimulationSession]
def _create_session(compiled: CompiledModel, settings: SimulationRunSettings, current_end_time: float | None, results: list[SimulationResult]) -> SimulationSession:
return SimulationSession(compiled.model_name, compiled.executable, settings, current_end_time=current_end_time, results=results)
class SimulationRunController(QObject):
run_completed = Signal(object)
run_cancelled = Signal()
run_failed = Signal(object)
def __init__(self, window: SimulationWindow, files: SimulationFileController, session_factory: SessionFactory = _create_session) -> None:
super().__init__(window)
self.window = window
self.files = files
self.session_factory = session_factory
self.session: SimulationSession | None = None
self._running = False
self._reset_pending = False
window.ui.actionRun_Simulation.triggered.connect(self.start)
window.ui.actionStop_Simulation.triggered.connect(self.stop)
window.ui.actionRestart_Simulation.triggered.connect(self.restart)
files.runtime_changed.connect(self._load_session)
self.run_completed.connect(self._completed)
self.run_cancelled.connect(self._cancelled)
self.run_failed.connect(self._failed)
self._load_session()
def start(self) -> None:
if self._running or self.session is None:
return
start_time = self.session.current_end_time
stop_time = start_time + self.session.settings.duration
self._running = True
self._update_actions()
logger.info("Starting simulation from %s to %s", start_time, stop_time)
Thread(target=self._run_worker, args=(self.session,), name="besim-run", daemon=True).start()
def stop(self) -> None:
if not self._running or self.session is None:
return
self.session.cancel()
logger.info("Stopping simulation")
def restart(self) -> None:
if self.session is None:
return
if self._running:
self._reset_pending = True
logger.info("Reset requested; stopping the active simulation")
self.stop()
return
self._reset()
def _reset(self) -> None:
if self.session is None:
return
self.session.reset()
self._store_session_state()
logger.info("Reset simulation to start time %s", self.session.current_end_time)
self._update_actions()
def _run_worker(self, session: SimulationSession) -> None:
try:
result = asyncio.run(session.run_next())
except SimulationCancelledError:
self.run_cancelled.emit()
except (OSError, RuntimeError, TypeError, ValueError) as exc:
self.run_failed.emit(exc)
else:
self.run_completed.emit(result)
def _completed(self, result: SimulationResult) -> None:
self._running = False
self._store_session_state()
logger.info("Simulation completed at time %s", self.session.current_end_time if self.session is not None else "unknown")
if result.process_output.strip():
logger.info("Simulation output:\n%s", result.process_output.strip())
if result.process_errors.strip():
logger.warning("Simulation errors:\n%s", result.process_errors.strip())
self._finish_run()
def _cancelled(self) -> None:
self._running = False
logger.info("Simulation stopped")
self._finish_run()
def _failed(self, error: Exception) -> None:
self._running = False
logger.error("Simulation failed: %s", error, exc_info=(type(error), error, error.__traceback__))
self._finish_run()
def _finish_run(self) -> None:
self._update_actions()
if self._reset_pending:
self._reset_pending = False
self._reset()
def _load_session(self) -> None:
root = self.files.root
compiled = self.files.compiled_model
self._reset_pending = False
if root is None or compiled is None:
self.session = None
else:
settings = root.settings
try:
run_settings = SimulationRunSettings(
start_time=settings.start_time,
duration=settings.duration,
use_timed_steps=settings.use_timed_steps,
number_of_steps=settings.number_of_steps,
step_size=settings.step_size,
tolerance=settings.dassl_tolerance,
method=settings.method.value,
)
self.session = self.session_factory(compiled, run_settings, root.current_end_time, root.results)
except (OSError, RuntimeError, ValueError):
self.session = None
logger.exception("Could not initialize the simulation runtime")
self._update_actions()
def _update_actions(self) -> None:
available = self.session is not None
self.window.ui.actionRun_Simulation.setEnabled(available and not self._running)
self.window.ui.actionStop_Simulation.setEnabled(available and self._running)
self.window.ui.actionRestart_Simulation.setEnabled(available)
for action in (self.window.ui.actionNew_Simulation_Run, self.window.ui.actionOpen_Simulation_Run, self.window.ui.actionSave_Simulation_Run, self.window.ui.actionSimulation_Options):
action.setEnabled(not self._running and (available or action is not self.window.ui.actionSimulation_Options))
if self.session is not None:
state = "running" if self._running else "ready"
self.window.statusBar().showMessage(f"Simulation {state} · current time {self.session.current_end_time}")
def _store_session_state(self) -> None:
if self.session is None or self.files.root is None:
return
self.files.root.current_end_time = self.session.current_end_time
self.files.root.results = list(self.session.results)

View File

@@ -7,6 +7,7 @@ from PySide6.QtWidgets import QApplication, QMessageBox
from bedit_gui.controllers.simulation_file_controller import SimulationFileController
from bedit_gui.controllers.log_controller import LogController
from bedit_gui.controllers.simulation_run_controller import SimulationRunController
from bedit_gui.simulation_models import CompiledModel
from bedit_gui.services.application_settings import SimulationApplicationSettings
from bedit_gui.views.simulation_window import SimulationWindow
@@ -42,6 +43,7 @@ def main(arguments: list[str] | None = None) -> int:
settings = SimulationApplicationSettings()
LogController(window, settings.log_level)
SimulationRunController(window, controller)
window.showMaximized()

View File

@@ -1,11 +1,12 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any
from bedit_core.models import ComponentID
from bedit_gui.models import Simulation
from bedit_simulation import SimulationResult
@dataclass
@@ -33,6 +34,8 @@ class SimulationRoot:
component_path: str
settings_name: str | None
settings: Simulation
current_end_time: float | None = None
results: list[SimulationResult] = field(default_factory=list)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationRoot:
@@ -46,6 +49,8 @@ class SimulationRoot:
component_path=str(data.get("component_path", "")),
settings_name=str(data["settings_name"]) if data.get("settings_name") is not None else None,
settings=Simulation.from_data(data["settings"]),
current_end_time=float(data["current_end_time"]) if data.get("current_end_time") is not None else None,
results=[_result_from_data(result) for result in data.get("results", [])],
)
def to_data(self) -> dict[str, Any]:
@@ -58,4 +63,18 @@ class SimulationRoot:
"component_path": self.component_path,
"settings_name": self.settings_name,
"settings": self.settings.to_data(),
"current_end_time": self.current_end_time,
"results": [_result_to_data(result) for result in self.results],
}
def _result_from_data(data: Mapping[str, Any]) -> SimulationResult:
raw_columns = data.get("data", {})
if not isinstance(raw_columns, Mapping):
raise TypeError("simulation result data must be a mapping")
columns = {str(name): [float(value) for value in values] for name, values in raw_columns.items()}
return SimulationResult(model_name=str(data.get("model_name", "")), data=columns, process_output=str(data.get("process_output", "")), process_errors=str(data.get("process_errors", "")))
def _result_to_data(result: SimulationResult) -> dict[str, Any]:
return {"model_name": result.model_name, "data": result.data, "process_output": result.process_output, "process_errors": result.process_errors}

View File

@@ -70,6 +70,9 @@ class SimulationSettingsDialog(QDialog):
if simulation.dassl_tolerance <= 0:
QMessageBox.warning(self, "Invalid simulation", "The DASSL tolerance must be greater than zero.")
return
if simulation.duration <= 0:
QMessageBox.warning(self, "Invalid simulation", "The simulation length must be greater than zero.")
return
simulation.name = simulation.name.strip()
self._database.active_simulation = self._simulation_id()
super().accept()