diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 010b527..910da33 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -25,7 +25,7 @@ src/bedit/ │ ├── port_types.py # Port type definitions and compatibility │ ├── serializer.py # JSON persistence │ ├── 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 ├── app.py # QApplication startup and palette ├── 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 `bedit.core.simulation`, replaces the shared application/controller service, and preserves the previous instance attributes where possible. -- Simulation compilation lives in `core/simulation/compiler.py`; the simulation - service only owns application state and delegates compilation. Ports with +- Modelica composition lives in `core/simulation/composer.py`; the simulation + service only owns application state and delegates composition. Ports with `multipleConnections` are emitted as Modelica arrays. Their size is inferred per component instance from graph connections and exposed while compiling as `$portname_N$`; array connection endpoints receive stable one-based indices in graph connection order. -- Blocking simulator integration belongs in `core/simulation/runner.py` and runs - on its daemon worker thread. Never perform OMPython startup or simulation work - directly on the Qt GUI thread; background failures must go to the application - log. +- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its + persistent worker and OMC session start lazily on the first queued request. + Never perform OMPython work directly on the Qt GUI thread. Result and error + 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 `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must not read `QSettings`. An empty path uses OMPython/PATH discovery, while an diff --git a/BEdit/src/bedit/core/simulation/__init__.py b/BEdit/src/bedit/core/simulation/__init__.py index 41515e1..088f9ef 100644 --- a/BEdit/src/bedit/core/simulation/__init__.py +++ b/BEdit/src/bedit/core/simulation/__init__.py @@ -1,3 +1,4 @@ from bedit.core.simulation.service import Simulation +from bedit.core.simulation.openmodelica import OpenModelicaInterface -__all__ = ["Simulation"] +__all__ = ["OpenModelicaInterface", "Simulation"] diff --git a/BEdit/src/bedit/core/simulation/compiler.py b/BEdit/src/bedit/core/simulation/composer.py similarity index 97% rename from BEdit/src/bedit/core/simulation/compiler.py rename to BEdit/src/bedit/core/simulation/composer.py index dbc7a0f..f78c8c7 100644 --- a/BEdit/src/bedit/core/simulation/compiler.py +++ b/BEdit/src/bedit/core/simulation/composer.py @@ -14,8 +14,8 @@ _BEVALUE_PATTERN = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\$") @dataclass(frozen=True) -class CompilationResult: - """The intermediate data and generated source produced by compilation.""" +class CompositionResult: + """The intermediate data and generated source produced by composition.""" graph: dict[str, Any] objects_by_id: dict[str, Any] @@ -23,16 +23,16 @@ class CompilationResult: model_name: str -def compile_graph(graph: dict[str, Any]) -> CompilationResult: - """Clean, index, and emit a serialized component tree.""" +def compose_graph(graph: dict[str, Any]) -> CompositionResult: + """Clean, index, and compose a serialized component tree.""" cleaned_graph = cleanup_graph(deepcopy(graph)) objects_by_id = build_id_list(cleaned_graph) - return CompilationResult( + return CompositionResult( graph=cleaned_graph, objects_by_id=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: """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 body_indent = "\t" * (indent + 1) model_name = model_name_for(graph) @@ -283,7 +283,7 @@ def _port_count_macros( 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: name = match.group(1) diff --git a/BEdit/src/bedit/core/simulation/openmodelica.py b/BEdit/src/bedit/core/simulation/openmodelica.py new file mode 100644 index 0000000..f487514 --- /dev/null +++ b/BEdit/src/bedit/core/simulation/openmodelica.py @@ -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) diff --git a/BEdit/src/bedit/core/simulation/runner.py b/BEdit/src/bedit/core/simulation/runner.py deleted file mode 100644 index e6d0052..0000000 --- a/BEdit/src/bedit/core/simulation/runner.py +++ /dev/null @@ -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) diff --git a/BEdit/src/bedit/core/simulation/service.py b/BEdit/src/bedit/core/simulation/service.py index a24151a..2414ab8 100644 --- a/BEdit/src/bedit/core/simulation/service.py +++ b/BEdit/src/bedit/core/simulation/service.py @@ -1,50 +1,102 @@ 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, run_validation_async +from bedit.core.simulation.composer import compose_graph +from bedit.core.simulation.openmodelica import ( + ErrorCallback, + OpenModelicaInterface, + ResultCallback, +) log = get_logger(__name__) class Simulation: - """Application-owned simulation state and compiler facade.""" + """Application-owned composition state and OpenModelica interface.""" def __init__(self, *, openmodelica_path: str = "") -> None: self.state: dict[str, Any] = {} - self.last_compilation_input: dict[str, Any] | None = None - self.last_compilation_output: str | None = None + self.last_composition_input: dict[str, Any] | None = None + self.last_composition_output: str | None = None 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: - """Compile a serialized component tree and retain the result.""" + @property + def openmodelica_path(self) -> str: + return self._openmodelica_path - self._prepare_compilation(graph) - run_validation_async( - self.openmodelica_path, - self.last_compilation_output, - self.model_name, + @openmodelica_path.setter + def openmodelica_path(self, value: str) -> None: + self._openmodelica_path = value + self.openmodelica.configure(value) + + 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: - """Generate and retain Modelica without starting a background operation.""" + def _model_compiled(result): + log.info("Compiling OK: %s", result) + self.model_path = result[0] - result = compile_graph(graph) - self.last_compilation_input = result.graph - self.id_list = result.objects_by_id - self.last_compilation_output = result.modelica - self.model_name = result.model_name + self.openmodelica.compile_model( + self.model_name, + _model_compiled, + lambda error: log.info("Compiling ERROR: %s", repr(error)), + ) 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 - # 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, + if self.model_path is None: + self.compose(graph) + + self.openmodelica.get_version(lambda val: log.info(val)) + log.info(f"Running model binary {self.model_path}") + + 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 diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py index 3f4b7be..8e3c71b 100644 --- a/BEdit/src/bedit/gui/controllers/document.py +++ b/BEdit/src/bedit/gui/controllers/document.py @@ -421,11 +421,11 @@ class DocumentController(QObject): if 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 if component is None or component.implementation_kind != "graph": - raise ValueError("Open a graph component before compiling") - self.simulation.compile(component.to_dict()) + raise ValueError("Open a graph component before composing") + self.simulation.compose(component.to_dict()) def run_simulation(self) -> None: component = self.active_component diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index 54a3ae2..16bf1ef 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -41,11 +41,11 @@ class Ui_MainWindow(object): icon1 = QIcon() icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionGraphParameters.setIcon(icon1) - self.actionCompile = QAction(MainWindow) - self.actionCompile.setObjectName(u"actionCompile") + self.actionCompose = QAction(MainWindow) + self.actionCompose.setObjectName(u"actionCompose") icon2 = QIcon() 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.setObjectName(u"actionRunSimulation") icon3 = QIcon() @@ -435,7 +435,7 @@ class Ui_MainWindow(object): self.menuHelp.addAction(self.actionAboutQt) self.menuSimulation.addAction(self.actionSimulationSettings) self.menuSimulation.addAction(self.actionGraphParameters) - self.menuSimulation.addAction(self.actionCompile) + self.menuSimulation.addAction(self.actionCompose) self.menuSimulation.addAction(self.actionRunSimulation) self.fileToolbar.addAction(self.actionNew) self.fileToolbar.addAction(self.actionOpen) @@ -451,7 +451,7 @@ class Ui_MainWindow(object): self.cameraToolbar.addAction(self.actionCenterView) self.simulationToolbar.addAction(self.actionSimulationSettings) self.simulationToolbar.addAction(self.actionGraphParameters) - self.simulationToolbar.addAction(self.actionCompile) + self.simulationToolbar.addAction(self.actionCompose) self.simulationToolbar.addAction(self.actionRunSimulation) self.retranslateUi(MainWindow) @@ -472,12 +472,12 @@ class Ui_MainWindow(object): #if QT_CONFIG(statustip) self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None)) #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) - 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) #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) self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None)) #if QT_CONFIG(statustip) diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index b3cd7c2..f103351 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -166,7 +166,7 @@ class MainWindow(QMainWindow): self.show_simulation_settings ) 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.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.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)) @Slot() - def compile_active_graph(self) -> None: + def compose_active_graph(self) -> None: try: - self.document_controller.compile_active_graph() + self.document_controller.compose_active_graph() except ValueError as error: - self.log.error("Compile failed: %s", error) - QMessageBox.warning(self, "Cannot compile", str(error)) + self.log.error("Composition failed: %s", error) + QMessageBox.warning(self, "Cannot compose", str(error)) @Slot() def run_simulation(self) -> None: @@ -281,7 +281,7 @@ class MainWindow(QMainWindow): self._set_graph_controls_visible(False) self.ui.actionSimulationSettings.setEnabled(False) self.ui.actionGraphParameters.setEnabled(False) - self.ui.actionCompile.setEnabled(False) + self.ui.actionCompose.setEnabled(False) self.ui.actionRunSimulation.setEnabled(False) self._update_edit_actions() return @@ -292,7 +292,7 @@ class MainWindow(QMainWindow): is_graph = component.implementation_kind == "graph" self.ui.actionSimulationSettings.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.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceStack.setCurrentWidget( @@ -760,6 +760,7 @@ class MainWindow(QMainWindow): event.ignore() return self.settings.setValue("window/geometry", self.saveGeometry()) + self.simulation.shutdown() self.log.info("BEdit closed") self.application_logger.removeHandler(self.log_handler) event.accept() diff --git a/BEdit/src/bedit/gui/simulation_reload.py b/BEdit/src/bedit/gui/simulation_reload.py index 7d740ed..c898485 100644 --- a/BEdit/src/bedit/gui/simulation_reload.py +++ b/BEdit/src/bedit/gui/simulation_reload.py @@ -20,6 +20,8 @@ def _saved_instance_state(instance) -> dict: def reload_simulation(current): """Reload the simulation package and return a fresh state-preserving instance.""" saved_state = _saved_instance_state(current) + saved_state.pop("openmodelica", None) + current.shutdown(wait=True) importlib.invalidate_caches() package = importlib.import_module(SIMULATION_PACKAGE) discovered: list[ModuleType] = [] @@ -30,4 +32,5 @@ def reload_simulation(current): package = importlib.reload(package) replacement = package.Simulation() vars(replacement).update(saved_state) + replacement.openmodelica.configure(replacement.openmodelica_path) return replacement diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index 55ea9b1..7195e60 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -551,7 +551,7 @@ - + @@ -644,7 +644,7 @@ - + @@ -671,16 +671,16 @@ Edit parameters throughout the active graph - + :/icons/icons/run-build.png:/icons/icons/run-build.png - Compile + Compose - Compile the active graph for simulation + Compose the active graph as an OpenModelica model F5 diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index 0dd03af..28b07ba 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -653,7 +653,7 @@ "startTime": 0.0, "stopTime": 10.0, "intervalMode": "numberOfIntervals", - "numberOfIntervals": 5000, + "numberOfIntervals": 50, "intervalTime": 0.002 } }