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
│ ├── 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

View File

@@ -1,3 +1,4 @@
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)
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)

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 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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

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

View File

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