new runner layout, still todo

This commit is contained in:
2026-07-20 22:03:43 +02:00
parent 38e3d7aae9
commit dc770a0886
7 changed files with 159 additions and 18 deletions

View File

@@ -20,6 +20,7 @@ class CompilationResult:
graph: dict[str, Any] graph: dict[str, Any]
objects_by_id: dict[str, Any] objects_by_id: dict[str, Any]
modelica: str modelica: str
model_name: str
def compile_graph(graph: dict[str, Any]) -> CompilationResult: def compile_graph(graph: dict[str, Any]) -> CompilationResult:
@@ -31,6 +32,7 @@ def compile_graph(graph: dict[str, Any]) -> CompilationResult:
graph=cleaned_graph, graph=cleaned_graph,
objects_by_id=objects_by_id, objects_by_id=objects_by_id,
modelica=emit_model(cleaned_graph, objects_by_id), modelica=emit_model(cleaned_graph, objects_by_id),
model_name=model_name_for(cleaned_graph)
) )

View File

@@ -1,3 +1,4 @@
import json
import os import os
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
@@ -11,18 +12,48 @@ _worker_lock = Lock()
_worker: Thread | None = None _worker: Thread | None = None
def run_simulation_async( def run_validation_async(
openmodelica_path: str = "", task: Callable[[], None] | None = None openmodelica_path: str = "",
model: str = "",
model_name: str = "",
task: Callable[[], None] | 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 global _worker
with _worker_lock: with _worker_lock:
if _worker is not None and _worker.is_alive(): 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( _worker = Thread(
target=_run_safely, 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", name="bedit-simulation",
daemon=True, daemon=True,
) )
@@ -47,15 +78,78 @@ def _run_safely(task: Callable[[], None]) -> None:
_worker = None _worker = None
def _run_openmodelica(openmodelica_path: str = "") -> None: def _run_validation(
"""Create the OpenModelica session and perform the current runner stub work.""" openmodelica_path: str = "", model: str = "", model_name: str = ""
) -> None:
"""Create the OpenModelica session and perform a validation"""
from OMPython import OMCSessionZMQ from OMPython import OMCSessionZMQ
omhome = _openmodelica_home(openmodelica_path) omhome = _openmodelica_home(openmodelica_path)
omc = OMCSessionZMQ(omhome=omhome) 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()")) 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: def _openmodelica_home(openmodelica_path: str) -> str | None:
"""Convert an optional omc executable path to the home expected by OMPython.""" """Convert an optional omc executable path to the home expected by OMPython."""

View File

@@ -2,7 +2,7 @@ from typing import Any
from bedit.core.application_log import get_logger from bedit.core.application_log import get_logger
from bedit.core.simulation.compiler import compile_graph 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__) log = get_logger(__name__)
@@ -21,13 +21,30 @@ class Simulation:
def compile(self, graph: dict[str, Any]) -> None: def compile(self, graph: dict[str, Any]) -> None:
"""Compile a serialized component tree and retain the result.""" """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) result = compile_graph(graph)
self.last_compilation_input = result.graph self.last_compilation_input = result.graph
self.id_list = result.objects_by_id self.id_list = result.objects_by_id
self.last_compilation_output = result.modelica 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.""" """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,
)

View File

@@ -431,7 +431,7 @@ class DocumentController(QObject):
component = self.active_component component = self.active_component
if component is None or component.implementation_kind != "graph": if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before running a simulation") 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: def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
item = ( item = (

View File

@@ -379,6 +379,8 @@ class Ui_MainWindow(object):
self.menuToolbars.setObjectName(u"menuToolbars") self.menuToolbars.setObjectName(u"menuToolbars")
self.menuHelp = QMenu(self.menubar) self.menuHelp = QMenu(self.menubar)
self.menuHelp.setObjectName(u"menuHelp") self.menuHelp.setObjectName(u"menuHelp")
self.menuSimulation = QMenu(self.menubar)
self.menuSimulation.setObjectName(u"menuSimulation")
MainWindow.setMenuBar(self.menubar) MainWindow.setMenuBar(self.menubar)
self.fileToolbar = QToolBar(MainWindow) self.fileToolbar = QToolBar(MainWindow)
self.fileToolbar.setObjectName(u"fileToolbar") self.fileToolbar.setObjectName(u"fileToolbar")
@@ -405,6 +407,7 @@ class Ui_MainWindow(object):
self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction()) self.menubar.addAction(self.menuEdit.menuAction())
self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuView.menuAction())
self.menubar.addAction(self.menuSimulation.menuAction())
self.menubar.addAction(self.menuHelp.menuAction()) self.menubar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionNew) self.menuFile.addAction(self.actionNew)
self.menuFile.addAction(self.actionOpen) self.menuFile.addAction(self.actionOpen)
@@ -430,6 +433,10 @@ class Ui_MainWindow(object):
self.menuView.addAction(self.menuToolbars.menuAction()) self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout) self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt) 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.actionNew)
self.fileToolbar.addAction(self.actionOpen) self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave) self.fileToolbar.addAction(self.actionSave)
@@ -618,6 +625,7 @@ class Ui_MainWindow(object):
self.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None)) self.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None)) self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", 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.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None)) self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None)) self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))

View File

@@ -545,9 +545,19 @@
<addaction name="actionAbout"/> <addaction name="actionAbout"/>
<addaction name="actionAboutQt"/> <addaction name="actionAboutQt"/>
</widget> </widget>
<widget class="QMenu" name="menuSimulation">
<property name="title">
<string>Simulation</string>
</property>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/>
<addaction name="actionRunSimulation"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuEdit"/> <addaction name="menuEdit"/>
<addaction name="menuView"/> <addaction name="menuView"/>
<addaction name="menuSimulation"/>
<addaction name="menuHelp"/> <addaction name="menuHelp"/>
</widget> </widget>
<widget class="QToolBar" name="fileToolbar"> <widget class="QToolBar" name="fileToolbar">
@@ -654,8 +664,12 @@
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset> <normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset>
</property> </property>
<property name="text"><string>Graph Parameters</string></property> <property name="text">
<property name="statusTip"><string>Edit parameters throughout the active graph</string></property> <string>Graph Parameters</string>
</property>
<property name="statusTip">
<string>Edit parameters throughout the active graph</string>
</property>
</action> </action>
<action name="actionCompile"> <action name="actionCompile">
<property name="icon"> <property name="icon">
@@ -677,9 +691,15 @@
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/media-playback-start.png</normaloff>:/icons/icons/media-playback-start.png</iconset> <normaloff>:/icons/icons/media-playback-start.png</normaloff>:/icons/icons/media-playback-start.png</iconset>
</property> </property>
<property name="text"><string>Run</string></property> <property name="text">
<property name="statusTip"><string>Run the simulation</string></property> <string>Run</string>
<property name="shortcut"><string>F6</string></property> </property>
<property name="statusTip">
<string>Run the simulation</string>
</property>
<property name="shortcut">
<string>F6</string>
</property>
</action> </action>
<action name="actionNew"> <action name="actionNew">
<property name="icon"> <property name="icon">

View File

@@ -56,7 +56,7 @@
"showName": false "showName": false
}, },
"library": { "library": {
"showSubtree": true "showSubtree": false
}, },
"implementation": { "implementation": {
"kind": "graph", "kind": "graph",