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 @@
+
+
@@ -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",