Hooked up simulation to BEsim
This commit is contained in:
@@ -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]:
|
||||
|
||||
155
src/bedit_gui/controllers/simulation_run_controller.py
Normal file
155
src/bedit_gui/controllers/simulation_run_controller.py
Normal 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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user