basic runner setup changed

This commit is contained in:
2026-07-21 13:21:52 +02:00
parent dc770a0886
commit 6fb2478589
12 changed files with 375 additions and 230 deletions

View File

@@ -25,7 +25,7 @@ src/bedit/
│ ├── port_types.py # Port type definitions and compatibility │ ├── port_types.py # Port type definitions and compatibility
│ ├── serializer.py # JSON persistence │ ├── serializer.py # JSON persistence
│ ├── libraries.py # Library file discovery and parsing │ ├── libraries.py # Library file discovery and parsing
│ └── simulation/ # Qt-free simulation service and compiler/runtime code │ └── simulation/ # Qt-free composition and OpenModelica interface code
└── gui/ # All Qt-dependent code └── gui/ # All Qt-dependent code
├── app.py # QApplication startup and palette ├── app.py # QApplication startup and palette
├── main_window.py # Top-level UI orchestration ├── main_window.py # Top-level UI orchestration
@@ -105,16 +105,19 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- File → Reload Simulation Code (`Ctrl+F5`) reloads modules under - File → Reload Simulation Code (`Ctrl+F5`) reloads modules under
`bedit.core.simulation`, replaces the shared application/controller service, `bedit.core.simulation`, replaces the shared application/controller service,
and preserves the previous instance attributes where possible. and preserves the previous instance attributes where possible.
- Simulation compilation lives in `core/simulation/compiler.py`; the simulation - Modelica composition lives in `core/simulation/composer.py`; the simulation
service only owns application state and delegates compilation. Ports with service only owns application state and delegates composition. Ports with
`multipleConnections` are emitted as Modelica arrays. Their size is inferred `multipleConnections` are emitted as Modelica arrays. Their size is inferred
per component instance from graph connections and exposed while compiling as per component instance from graph connections and exposed while compiling as
`$portname_N$`; array connection endpoints receive stable one-based indices in `$portname_N$`; array connection endpoints receive stable one-based indices in
graph connection order. graph connection order.
- Blocking simulator integration belongs in `core/simulation/runner.py` and runs - OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
on its daemon worker thread. Never perform OMPython startup or simulation work persistent worker and OMC session start lazily on the first queued request.
directly on the Qt GUI thread; background failures must go to the application Never perform OMPython work directly on the Qt GUI thread. Result and error
log. callbacks run on the worker thread and must use a Qt signal before touching UI.
One lazy temporary working directory is shared by all requests in the session.
Explicit application shutdown closes OMC and removes that directory plus the
current session's OMPython log and port files; `__del__` is only a fallback.
- The optional OpenModelica executable is persisted as - The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an not read `QSettings`. An empty path uses OMPython/PATH discovery, while an

View File

@@ -1,3 +1,4 @@
from bedit.core.simulation.service import Simulation from bedit.core.simulation.service import Simulation
from bedit.core.simulation.openmodelica import OpenModelicaInterface
__all__ = ["Simulation"] __all__ = ["OpenModelicaInterface", "Simulation"]

View File

@@ -14,8 +14,8 @@ _BEVALUE_PATTERN = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\$")
@dataclass(frozen=True) @dataclass(frozen=True)
class CompilationResult: class CompositionResult:
"""The intermediate data and generated source produced by compilation.""" """The intermediate data and generated source produced by composition."""
graph: dict[str, Any] graph: dict[str, Any]
objects_by_id: dict[str, Any] objects_by_id: dict[str, Any]
@@ -23,16 +23,16 @@ class CompilationResult:
model_name: str model_name: str
def compile_graph(graph: dict[str, Any]) -> CompilationResult: def compose_graph(graph: dict[str, Any]) -> CompositionResult:
"""Clean, index, and emit a serialized component tree.""" """Clean, index, and compose a serialized component tree."""
cleaned_graph = cleanup_graph(deepcopy(graph)) cleaned_graph = cleanup_graph(deepcopy(graph))
objects_by_id = build_id_list(cleaned_graph) objects_by_id = build_id_list(cleaned_graph)
return CompilationResult( return CompositionResult(
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) model_name=model_name_for(cleaned_graph),
) )
@@ -85,7 +85,7 @@ def emit_model(
) -> str: ) -> str:
"""Emit a component and its nested definitions as Modelica source.""" """Emit a component and its nested definitions as Modelica source."""
del id_list # Kept in the public API for compiler extensions and inspection. del id_list # Kept in the public API for composer extensions and inspection.
indentation = "\t" * indent indentation = "\t" * indent
body_indent = "\t" * (indent + 1) body_indent = "\t" * (indent + 1)
model_name = model_name_for(graph) model_name = model_name_for(graph)
@@ -283,7 +283,7 @@ def _port_count_macros(
def expand_bevalues(text: str, values: dict[str, str]) -> str: def expand_bevalues(text: str, values: dict[str, str]) -> str:
"""Replace BEdit ``$name$`` macros and reject unresolved compiler values.""" """Replace BEdit ``$name$`` macros and reject unresolved composer values."""
def replace(match: re.Match[str]) -> str: def replace(match: re.Match[str]) -> str:
name = match.group(1) name = match.group(1)

View File

@@ -0,0 +1,247 @@
import json
import os
import shutil
import tempfile
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Queue
from threading import Lock, Thread, current_thread
from typing import Any
from bedit.core.application_log import get_logger
log = get_logger(__name__)
ResultCallback = Callable[[Any], None]
ErrorCallback = Callable[[Exception], None]
@dataclass(frozen=True)
class _Request:
expression: str
parsed: bool
callback: ResultCallback | None
error_callback: ErrorCallback | None
class OpenModelicaInterface:
"""Asynchronous, persistent interface to one OpenModelica session.
The worker and OMC session are created lazily for the first request. Callback
functions execute on the worker thread and must not manipulate Qt widgets.
"""
def __init__(self, executable_path: str = "") -> None:
self._executable_path = executable_path
self._lifecycle_lock = Lock()
self._queue: Queue[_Request | None] | None = None
self._worker: Thread | None = None
self._temp_dir: Path | None = None
@property
def executable_path(self) -> str:
return self._executable_path
def configure(self, executable_path: str) -> None:
"""Use a new executable path for subsequent requests."""
if executable_path == self._executable_path:
return
self.shutdown(wait=True)
self._executable_path = executable_path
def get_version(
self,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Request the OpenModelica version without blocking the caller."""
self.send_expression("getVersion()", callback, error_callback)
def load_model(
self,
model: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Load a composed model into OpenModelica"""
self.send_expression(f"loadString({json.dumps(model)})", callback, error_callback)
def compile_model(
self,
model_name: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Compile a loaded model in OpenModelica"""
self.send_expression(f"buildModel({model_name})", callback, error_callback)
def exec_system(
self,
cmd: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Execute system command from OpenModelica"""
self.send_expression(f"system(\"{cmd}\", \"system_out.txt\")", callback, error_callback)
def send_expression(
self,
expression: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
*,
parsed: bool = True,
) -> None:
"""Queue an OMC expression for ordered execution on the worker thread."""
request = _Request(expression, parsed, callback, error_callback)
self._ensure_worker().put(request)
def shutdown(self, *, wait: bool = True) -> None:
"""Ask the worker to close its OMC session after queued requests."""
with self._lifecycle_lock:
worker = self._worker
queue = self._queue
self._worker = None
self._queue = None
if queue is not None:
queue.put(None)
if wait and worker is not None and worker is not current_thread():
worker.join()
if worker is None:
self._cleanup_temp_dir()
def __del__(self) -> None:
"""Best-effort fallback; normal application shutdown is explicit."""
try:
self.shutdown(wait=False)
except Exception:
pass
def _ensure_worker(self) -> Queue[_Request | None]:
with self._lifecycle_lock:
if self._worker is not None and self._worker.is_alive():
return self._queue
temp_dir = self._ensure_temp_dir_locked()
queue: Queue[_Request | None] = Queue()
worker = Thread(
target=self._worker_main,
args=(queue, self._executable_path, temp_dir),
name="bedit-openmodelica",
daemon=True,
)
self._queue = queue
self._worker = worker
worker.start()
return queue
def _worker_main(
self,
queue: Queue[_Request | None],
executable_path: str,
temp_dir: Path,
) -> None:
omc = None
try:
while (request := queue.get()) is not None:
try:
if omc is None:
omc = _create_session(executable_path)
changed_directory = omc.sendExpression(
f"cd({json.dumps(str(temp_dir))})"
)
if not changed_directory:
raise RuntimeError(
f"OpenModelica could not use {str(temp_dir)!r}"
)
result = omc.sendExpression(
request.expression, parsed=request.parsed
)
except Exception as error:
if request.error_callback is None:
log.exception(
"OpenModelica request failed: %s", request.expression
)
else:
_deliver_callback(request.error_callback, error)
else:
if request.callback is not None:
_deliver_callback(request.callback, result)
finally:
transport_files = _ompython_transport_files(omc)
if omc is not None:
try:
omc.sendExpression("quit()")
except Exception:
log.debug("Could not close OpenModelica session", exc_info=True)
for path in transport_files:
try:
path.unlink(missing_ok=True)
except OSError:
log.debug("Could not remove OMPython file %s", path, exc_info=True)
self._cleanup_temp_dir(temp_dir)
def _ensure_temp_dir_locked(self) -> Path:
if self._temp_dir is None:
self._temp_dir = Path(tempfile.mkdtemp(prefix="bedit-openmodelica-"))
return self._temp_dir
def _cleanup_temp_dir(self, expected: Path | None = None) -> None:
with self._lifecycle_lock:
if expected is not None and self._temp_dir != expected:
temp_dir = expected
else:
temp_dir = self._temp_dir
self._temp_dir = None
if temp_dir is not None:
shutil.rmtree(temp_dir, ignore_errors=True)
def _deliver_callback(callback: Callable[[Any], None], value: Any) -> None:
try:
callback(value)
except Exception:
log.exception("OpenModelica callback failed")
def _create_session(executable_path: str):
from OMPython import OMCSessionZMQ
return OMCSessionZMQ(omhome=_openmodelica_home(executable_path))
def _ompython_transport_files(omc) -> set[Path]:
"""Return only the log and port files owned by this OMPython session."""
if omc is None:
return set()
process = getattr(omc, "omc_process", None)
if process is None:
return set()
files: set[Path] = set()
temp_dir = getattr(process, "_temp_dir", None)
file_base = getattr(process, "_omc_filebase", None)
if temp_dir is not None and file_base:
files.add(Path(temp_dir) / f"{file_base}.log")
try:
port_file = process._get_portfile_path()
except Exception:
port_file = None
if port_file is not None:
files.add(Path(port_file))
return files
def _openmodelica_home(executable_path: str) -> str | None:
"""Convert an optional omc executable path to the home expected by OMPython."""
if not executable_path.strip():
return None
path = Path(os.path.expandvars(executable_path)).expanduser()
if path.name.lower() in {"omc", "omc.exe"}:
return str(path.parent.parent)
return str(path)

View File

@@ -1,162 +0,0 @@
import json
import os
from collections.abc import Callable
from pathlib import Path
from threading import Lock, Thread
from bedit.core.application_log import get_logger
log = get_logger(__name__)
_worker_lock = Lock()
_worker: Thread | None = None
def run_validation_async(
openmodelica_path: str = "",
model: str = "",
model_name: str = "",
task: Callable[[], None] | None = None,
) -> None:
"""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("An OpenModelica task is already running")
_worker = Thread(
target=_run_safely,
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,
)
_worker.start()
def simulation_is_running() -> bool:
"""Return whether the background simulation worker is active."""
with _worker_lock:
return _worker is not None and _worker.is_alive()
def _run_safely(task: Callable[[], None]) -> None:
global _worker
try:
task()
except Exception:
log.exception("Simulation run failed")
finally:
with _worker_lock:
_worker = None
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."""
if not openmodelica_path.strip():
return None
path = Path(os.path.expandvars(openmodelica_path)).expanduser()
if path.name.lower() in {"omc", "omc.exe"}:
return str(path.parent.parent)
return str(path)

View File

@@ -1,50 +1,102 @@
from typing import Any 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.composer import compose_graph
from bedit.core.simulation.runner import run_simulation_async, run_validation_async from bedit.core.simulation.openmodelica import (
ErrorCallback,
OpenModelicaInterface,
ResultCallback,
)
log = get_logger(__name__) log = get_logger(__name__)
class Simulation: class Simulation:
"""Application-owned simulation state and compiler facade.""" """Application-owned composition state and OpenModelica interface."""
def __init__(self, *, openmodelica_path: str = "") -> None: def __init__(self, *, openmodelica_path: str = "") -> None:
self.state: dict[str, Any] = {} self.state: dict[str, Any] = {}
self.last_compilation_input: dict[str, Any] | None = None self.last_composition_input: dict[str, Any] | None = None
self.last_compilation_output: str | None = None self.last_composition_output: str | None = None
self.id_list: dict[str, Any] = {} self.id_list: dict[str, Any] = {}
self.openmodelica_path = openmodelica_path self.model_name: str | None = None
self._openmodelica_path = openmodelica_path
self.openmodelica = OpenModelicaInterface(openmodelica_path)
self.model_path : str | None = None
def compile(self, graph: dict[str, Any]) -> None: @property
"""Compile a serialized component tree and retain the result.""" def openmodelica_path(self) -> str:
return self._openmodelica_path
self._prepare_compilation(graph) @openmodelica_path.setter
run_validation_async( def openmodelica_path(self, value: str) -> None:
self.openmodelica_path, self._openmodelica_path = value
self.last_compilation_output, self.openmodelica.configure(value)
self.model_name,
def compose(self, graph: dict[str, Any]) -> None:
"""Compose and retain the active graph's Modelica representation."""
self.model_path = None
# Create openmodelica model
result = compose_graph(graph)
self.last_composition_input = result.graph
self.id_list = result.objects_by_id
self.last_composition_output = result.modelica
self.model_name = result.model_name
log.info("Composed OpenModelica model:\n%s", result.modelica)
# Compile openmodelica model
self.openmodelica.load_model(
self.last_composition_output,
lambda result: log.info("Loading OK: %s", result),
lambda error: log.info("Loading ERROR: %s", repr(error)),
) )
def _prepare_compilation(self, graph: dict[str, Any]) -> None: def _model_compiled(result):
"""Generate and retain Modelica without starting a background operation.""" log.info("Compiling OK: %s", result)
self.model_path = result[0]
result = compile_graph(graph) self.openmodelica.compile_model(
self.last_compilation_input = result.graph self.model_name,
self.id_list = result.objects_by_id _model_compiled,
self.last_compilation_output = result.modelica lambda error: log.info("Compiling ERROR: %s", repr(error)),
self.model_name = result.model_name )
def run_simulation(self, graph: dict[str, Any]) -> None: def run_simulation(self, graph: dict[str, Any]) -> None:
"""Start the simulation without blocking the calling UI thread.""" """Reserved for a future simulation request."""
# Always regenerate before running, then validate and simulate as one if self.model_path is None:
# background operation so the two phases cannot compete for the worker. self.compose(graph)
self._prepare_compilation(graph)
run_simulation_async( self.openmodelica.get_version(lambda val: log.info(val))
self.openmodelica_path, log.info(f"Running model binary {self.model_path}")
self.last_compilation_output,
self.model_name, self.openmodelica.exec_system(
f'./{self.model_name} {self.build_simulation_arguments()}',
lambda result: log.info("Running OK: %s", result),
lambda error: log.info("Running ERROR: %s", repr(error)),
) )
def shutdown(self, *, wait: bool = True) -> None:
"""Close OpenModelica and clean its generated working directory."""
self.openmodelica.shutdown(wait=wait)
self.model_path = None
def build_simulation_arguments(self) -> str:
opts = self.last_composition_input['implementation']['graph']['simulation']
args = "-port=40696 -outputFormat=csv -logFormat=xmltcp "
args += f"-startTime={opts['startTime']} -stopTime={opts['stopTime']} "
step_size = opts['intervalTime']
if opts['intervalMode'] == 'numberOfIntervals':
step_size = (opts['stopTime']-opts['startTime'])/opts['numberOfIntervals']
args += f"-stepSize={step_size}"
log.info("Running model with: %s", args)
return args

View File

@@ -421,11 +421,11 @@ class DocumentController(QObject):
if old != new: if old != new:
self.undo_stack.push(EditGraphParametersCommand(self, old, new)) self.undo_stack.push(EditGraphParametersCommand(self, old, new))
def compile_active_graph(self) -> None: def compose_active_graph(self) -> None:
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 compiling") raise ValueError("Open a graph component before composing")
self.simulation.compile(component.to_dict()) self.simulation.compose(component.to_dict())
def run_simulation(self) -> None: def run_simulation(self) -> None:
component = self.active_component component = self.active_component

View File

@@ -41,11 +41,11 @@ class Ui_MainWindow(object):
icon1 = QIcon() icon1 = QIcon()
icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionGraphParameters.setIcon(icon1) self.actionGraphParameters.setIcon(icon1)
self.actionCompile = QAction(MainWindow) self.actionCompose = QAction(MainWindow)
self.actionCompile.setObjectName(u"actionCompile") self.actionCompose.setObjectName(u"actionCompose")
icon2 = QIcon() icon2 = QIcon()
icon2.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon2.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCompile.setIcon(icon2) self.actionCompose.setIcon(icon2)
self.actionRunSimulation = QAction(MainWindow) self.actionRunSimulation = QAction(MainWindow)
self.actionRunSimulation.setObjectName(u"actionRunSimulation") self.actionRunSimulation.setObjectName(u"actionRunSimulation")
icon3 = QIcon() icon3 = QIcon()
@@ -435,7 +435,7 @@ class Ui_MainWindow(object):
self.menuHelp.addAction(self.actionAboutQt) self.menuHelp.addAction(self.actionAboutQt)
self.menuSimulation.addAction(self.actionSimulationSettings) self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters) self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompile) self.menuSimulation.addAction(self.actionCompose)
self.menuSimulation.addAction(self.actionRunSimulation) 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)
@@ -451,7 +451,7 @@ class Ui_MainWindow(object):
self.cameraToolbar.addAction(self.actionCenterView) self.cameraToolbar.addAction(self.actionCenterView)
self.simulationToolbar.addAction(self.actionSimulationSettings) self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters) self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompile) self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionRunSimulation) self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
@@ -472,12 +472,12 @@ class Ui_MainWindow(object):
#if QT_CONFIG(statustip) #if QT_CONFIG(statustip)
self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None)) self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None))
#endif // QT_CONFIG(statustip) #endif // QT_CONFIG(statustip)
self.actionCompile.setText(QCoreApplication.translate("MainWindow", u"Compile", None)) self.actionCompose.setText(QCoreApplication.translate("MainWindow", u"Compose", None))
#if QT_CONFIG(statustip) #if QT_CONFIG(statustip)
self.actionCompile.setStatusTip(QCoreApplication.translate("MainWindow", u"Compile the active graph for simulation", None)) self.actionCompose.setStatusTip(QCoreApplication.translate("MainWindow", u"Compose the active graph as an OpenModelica model", None))
#endif // QT_CONFIG(statustip) #endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut) #if QT_CONFIG(shortcut)
self.actionCompile.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None)) self.actionCompose.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
#endif // QT_CONFIG(shortcut) #endif // QT_CONFIG(shortcut)
self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None)) self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None))
#if QT_CONFIG(statustip) #if QT_CONFIG(statustip)

View File

@@ -166,7 +166,7 @@ class MainWindow(QMainWindow):
self.show_simulation_settings self.show_simulation_settings
) )
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters) self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
self.ui.actionCompile.triggered.connect(self.compile_active_graph) self.ui.actionCompose.triggered.connect(self.compose_active_graph)
self.ui.actionRunSimulation.triggered.connect(self.run_simulation) self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled) self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled) self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
@@ -239,12 +239,12 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Cannot change graph parameters", str(error)) QMessageBox.warning(self, "Cannot change graph parameters", str(error))
@Slot() @Slot()
def compile_active_graph(self) -> None: def compose_active_graph(self) -> None:
try: try:
self.document_controller.compile_active_graph() self.document_controller.compose_active_graph()
except ValueError as error: except ValueError as error:
self.log.error("Compile failed: %s", error) self.log.error("Composition failed: %s", error)
QMessageBox.warning(self, "Cannot compile", str(error)) QMessageBox.warning(self, "Cannot compose", str(error))
@Slot() @Slot()
def run_simulation(self) -> None: def run_simulation(self) -> None:
@@ -281,7 +281,7 @@ class MainWindow(QMainWindow):
self._set_graph_controls_visible(False) self._set_graph_controls_visible(False)
self.ui.actionSimulationSettings.setEnabled(False) self.ui.actionSimulationSettings.setEnabled(False)
self.ui.actionGraphParameters.setEnabled(False) self.ui.actionGraphParameters.setEnabled(False)
self.ui.actionCompile.setEnabled(False) self.ui.actionCompose.setEnabled(False)
self.ui.actionRunSimulation.setEnabled(False) self.ui.actionRunSimulation.setEnabled(False)
self._update_edit_actions() self._update_edit_actions()
return return
@@ -292,7 +292,7 @@ class MainWindow(QMainWindow):
is_graph = component.implementation_kind == "graph" is_graph = component.implementation_kind == "graph"
self.ui.actionSimulationSettings.setEnabled(is_graph) self.ui.actionSimulationSettings.setEnabled(is_graph)
self.ui.actionGraphParameters.setEnabled(is_graph) self.ui.actionGraphParameters.setEnabled(is_graph)
self.ui.actionCompile.setEnabled(is_graph) self.ui.actionCompose.setEnabled(is_graph)
self.ui.actionRunSimulation.setEnabled(is_graph) self.ui.actionRunSimulation.setEnabled(is_graph)
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget( self.ui.workspaceStack.setCurrentWidget(
@@ -760,6 +760,7 @@ class MainWindow(QMainWindow):
event.ignore() event.ignore()
return return
self.settings.setValue("window/geometry", self.saveGeometry()) self.settings.setValue("window/geometry", self.saveGeometry())
self.simulation.shutdown()
self.log.info("BEdit closed") self.log.info("BEdit closed")
self.application_logger.removeHandler(self.log_handler) self.application_logger.removeHandler(self.log_handler)
event.accept() event.accept()

View File

@@ -20,6 +20,8 @@ def _saved_instance_state(instance) -> dict:
def reload_simulation(current): def reload_simulation(current):
"""Reload the simulation package and return a fresh state-preserving instance.""" """Reload the simulation package and return a fresh state-preserving instance."""
saved_state = _saved_instance_state(current) saved_state = _saved_instance_state(current)
saved_state.pop("openmodelica", None)
current.shutdown(wait=True)
importlib.invalidate_caches() importlib.invalidate_caches()
package = importlib.import_module(SIMULATION_PACKAGE) package = importlib.import_module(SIMULATION_PACKAGE)
discovered: list[ModuleType] = [] discovered: list[ModuleType] = []
@@ -30,4 +32,5 @@ def reload_simulation(current):
package = importlib.reload(package) package = importlib.reload(package)
replacement = package.Simulation() replacement = package.Simulation()
vars(replacement).update(saved_state) vars(replacement).update(saved_state)
replacement.openmodelica.configure(replacement.openmodelica_path)
return replacement return replacement

View File

@@ -551,7 +551,7 @@
</property> </property>
<addaction name="actionSimulationSettings"/> <addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/> <addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/> <addaction name="actionCompose"/>
<addaction name="actionRunSimulation"/> <addaction name="actionRunSimulation"/>
</widget> </widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
@@ -644,7 +644,7 @@
</attribute> </attribute>
<addaction name="actionSimulationSettings"/> <addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/> <addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/> <addaction name="actionCompose"/>
<addaction name="actionRunSimulation"/> <addaction name="actionRunSimulation"/>
</widget> </widget>
<action name="actionSimulationSettings"> <action name="actionSimulationSettings">
@@ -671,16 +671,16 @@
<string>Edit parameters throughout the active graph</string> <string>Edit parameters throughout the active graph</string>
</property> </property>
</action> </action>
<action name="actionCompile"> <action name="actionCompose">
<property name="icon"> <property name="icon">
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/run-build.png</normaloff>:/icons/icons/run-build.png</iconset> <normaloff>:/icons/icons/run-build.png</normaloff>:/icons/icons/run-build.png</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Compile</string> <string>Compose</string>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Compile the active graph for simulation</string> <string>Compose the active graph as an OpenModelica model</string>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>F5</string> <string>F5</string>

View File

@@ -653,7 +653,7 @@
"startTime": 0.0, "startTime": 0.0,
"stopTime": 10.0, "stopTime": 10.0,
"intervalMode": "numberOfIntervals", "intervalMode": "numberOfIntervals",
"numberOfIntervals": 5000, "numberOfIntervals": 50,
"intervalTime": 0.002 "intervalTime": 0.002
} }
} }