diff --git a/BEdit/src/bedit/core/simulation/compiler.py b/BEdit/src/bedit/core/simulation/compiler.py index d87cd39..dbc7a0f 100644 --- a/BEdit/src/bedit/core/simulation/compiler.py +++ b/BEdit/src/bedit/core/simulation/compiler.py @@ -20,6 +20,7 @@ class CompilationResult: graph: dict[str, Any] objects_by_id: dict[str, Any] modelica: str + model_name: str def compile_graph(graph: dict[str, Any]) -> CompilationResult: @@ -31,6 +32,7 @@ def compile_graph(graph: dict[str, Any]) -> CompilationResult: graph=cleaned_graph, objects_by_id=objects_by_id, modelica=emit_model(cleaned_graph, objects_by_id), + model_name=model_name_for(cleaned_graph) ) diff --git a/BEdit/src/bedit/core/simulation/runner.py b/BEdit/src/bedit/core/simulation/runner.py index 9661922..e6d0052 100644 --- a/BEdit/src/bedit/core/simulation/runner.py +++ b/BEdit/src/bedit/core/simulation/runner.py @@ -1,3 +1,4 @@ +import json import os from collections.abc import Callable from pathlib import Path @@ -11,18 +12,48 @@ _worker_lock = Lock() _worker: Thread | None = None -def run_simulation_async( - openmodelica_path: str = "", task: Callable[[], None] | None = None +def run_validation_async( + openmodelica_path: str = "", + model: str = "", + model_name: str = "", + task: Callable[[], None] | None = None, ) -> None: - """Start one simulation task in a background thread and return immediately.""" + """Start one validation task in a background thread and return immediately.""" global _worker with _worker_lock: if _worker is not None and _worker.is_alive(): - raise RuntimeError("A simulation is already running") + raise RuntimeError("An OpenModelica task is already running") _worker = Thread( target=_run_safely, - args=(task or (lambda: _run_openmodelica(openmodelica_path)),), + args=( + task + or (lambda: _run_validation(openmodelica_path, model, model_name)), + ), + name="bedit-simulation", + daemon=True, + ) + _worker.start() + + +def run_simulation_async( + openmodelica_path: str = "", + model: str = "", + model_name: str = "", + task: Callable[[], None] | None = None, +) -> None: + """Start one validation-and-simulation task and return immediately.""" + + global _worker + with _worker_lock: + if _worker is not None and _worker.is_alive(): + raise RuntimeError("An OpenModelica task is already running") + _worker = Thread( + target=_run_safely, + args=( + task + or (lambda: _run_openmodelica(openmodelica_path, model, model_name)), + ), name="bedit-simulation", daemon=True, ) @@ -47,15 +78,78 @@ def _run_safely(task: Callable[[], None]) -> None: _worker = None -def _run_openmodelica(openmodelica_path: str = "") -> None: - """Create the OpenModelica session and perform the current runner stub work.""" +def _run_validation( + openmodelica_path: str = "", model: str = "", model_name: str = "" +) -> None: + """Create the OpenModelica session and perform a validation""" from OMPython import OMCSessionZMQ omhome = _openmodelica_home(openmodelica_path) omc = OMCSessionZMQ(omhome=omhome) + _validate_model(omc, model, model_name) + + +def _run_openmodelica( + openmodelica_path: str = "", model: str = "", model_name: str = "" +) -> None: + """Validate and run the current simulation stub in one OMC session.""" + + from OMPython import OMCSessionZMQ + + omhome = _openmodelica_home(openmodelica_path) + omc = OMCSessionZMQ(omhome=omhome) + _validate_model(omc, model, model_name) + log.info("OpenModelica Version: %s", omc.sendExpression("getVersion()")) + loaded = omc.sendExpression(f"loadString({json.dumps(model)})") + if loaded is not True: + details = _get_error_string(omc) + raise RuntimeError(f"OpenModelica could not load the model: {details}") + + result = omc.sendExpression(f"checkModel({model_name})") + details = _get_error_string(omc) + if not isinstance(result, str) or "completed successfully" not in result: + message = details or result or "Unknown OpenModelica validation error" + raise RuntimeError(f"OpenModelica model validation failed: {message}") + +def _validate_model(omc, model: str, model_name: str) -> None: + if not model: + raise ValueError("There is no compiled Modelica source to validate") + if not model_name: + raise ValueError("The compiled model has no Modelica name") + + loaded = omc.sendExpression(f"loadString({json.dumps(model)})") + if loaded is not True: + details = _get_error_string(omc) + raise RuntimeError(f"OpenModelica could not load the model: {details}") + + result = omc.sendExpression(f"checkModel({model_name})") + details = _get_error_string(omc) + if not isinstance(result, str) or "completed successfully" not in result: + message = details or result or "Unknown OpenModelica validation error" + raise RuntimeError(f"OpenModelica model validation failed: {message}") + log.info("%s", result) + if details: + log.warning("OpenModelica validation messages: %s", details) + + +def _get_error_string(omc) -> str: + """Read OMC's deliberately unparsed error response as plain text.""" + + raw = omc.sendExpression("getErrorString()", parsed=False) + if not isinstance(raw, str): + return str(raw or "") + raw = raw.strip() + if not raw: + return "" + try: + decoded = json.loads(raw) + except (TypeError, json.JSONDecodeError): + return raw + return decoded if isinstance(decoded, str) else str(decoded) + def _openmodelica_home(openmodelica_path: str) -> str | None: """Convert an optional omc executable path to the home expected by OMPython.""" diff --git a/BEdit/src/bedit/core/simulation/service.py b/BEdit/src/bedit/core/simulation/service.py index 389eb87..a24151a 100644 --- a/BEdit/src/bedit/core/simulation/service.py +++ b/BEdit/src/bedit/core/simulation/service.py @@ -2,7 +2,7 @@ from typing import Any from bedit.core.application_log import get_logger from bedit.core.simulation.compiler import compile_graph -from bedit.core.simulation.runner import run_simulation_async +from bedit.core.simulation.runner import run_simulation_async, run_validation_async log = get_logger(__name__) @@ -21,13 +21,30 @@ class Simulation: def compile(self, graph: dict[str, Any]) -> None: """Compile a serialized component tree and retain the result.""" + self._prepare_compilation(graph) + run_validation_async( + self.openmodelica_path, + self.last_compilation_output, + self.model_name, + ) + + def _prepare_compilation(self, graph: dict[str, Any]) -> None: + """Generate and retain Modelica without starting a background operation.""" + result = compile_graph(graph) self.last_compilation_input = result.graph self.id_list = result.objects_by_id self.last_compilation_output = result.modelica - log.info("Generated Modelica model:\n%s", self.last_compilation_output) + self.model_name = result.model_name - def run_simulation(self) -> None: + def run_simulation(self, graph: dict[str, Any]) -> None: """Start the simulation without blocking the calling UI thread.""" - run_simulation_async(self.openmodelica_path) + # Always regenerate before running, then validate and simulate as one + # background operation so the two phases cannot compete for the worker. + self._prepare_compilation(graph) + run_simulation_async( + self.openmodelica_path, + self.last_compilation_output, + self.model_name, + ) diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py index 21c94e1..3f4b7be 100644 --- a/BEdit/src/bedit/gui/controllers/document.py +++ b/BEdit/src/bedit/gui/controllers/document.py @@ -431,7 +431,7 @@ class DocumentController(QObject): component = self.active_component if component is None or component.implementation_kind != "graph": raise ValueError("Open a graph component before running a simulation") - self.simulation.run_simulation() + self.simulation.run_simulation(component.to_dict()) def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None: item = ( diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index 41d0d2c..54a3ae2 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -379,6 +379,8 @@ class Ui_MainWindow(object): self.menuToolbars.setObjectName(u"menuToolbars") self.menuHelp = QMenu(self.menubar) self.menuHelp.setObjectName(u"menuHelp") + self.menuSimulation = QMenu(self.menubar) + self.menuSimulation.setObjectName(u"menuSimulation") MainWindow.setMenuBar(self.menubar) self.fileToolbar = QToolBar(MainWindow) self.fileToolbar.setObjectName(u"fileToolbar") @@ -405,6 +407,7 @@ class Ui_MainWindow(object): self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuEdit.menuAction()) self.menubar.addAction(self.menuView.menuAction()) + self.menubar.addAction(self.menuSimulation.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) self.menuFile.addAction(self.actionNew) self.menuFile.addAction(self.actionOpen) @@ -430,6 +433,10 @@ class Ui_MainWindow(object): self.menuView.addAction(self.menuToolbars.menuAction()) self.menuHelp.addAction(self.actionAbout) self.menuHelp.addAction(self.actionAboutQt) + self.menuSimulation.addAction(self.actionSimulationSettings) + self.menuSimulation.addAction(self.actionGraphParameters) + self.menuSimulation.addAction(self.actionCompile) + self.menuSimulation.addAction(self.actionRunSimulation) self.fileToolbar.addAction(self.actionNew) self.fileToolbar.addAction(self.actionOpen) self.fileToolbar.addAction(self.actionSave) @@ -618,6 +625,7 @@ class Ui_MainWindow(object): self.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None)) self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None)) self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None)) + self.menuSimulation.setTitle(QCoreApplication.translate("MainWindow", u"Simulation", None)) self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None)) self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None)) self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None)) diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index 884311c..55ea9b1 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -545,9 +545,19 @@ + + + Simulation + + + + + + + @@ -654,8 +664,12 @@ :/icons/icons/configure.png:/icons/icons/configure.png - Graph Parameters - Edit parameters throughout the active graph + + Graph Parameters + + + Edit parameters throughout the active graph + @@ -677,9 +691,15 @@ :/icons/icons/media-playback-start.png:/icons/icons/media-playback-start.png - Run - Run the simulation - F6 + + Run + + + Run the simulation + + + F6 + diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index 6602f5f..0dd03af 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -56,7 +56,7 @@ "showName": false }, "library": { - "showSubtree": true + "showSubtree": false }, "implementation": { "kind": "graph",