basic runner setup changed
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from bedit.core.simulation.service import Simulation
|
||||
from bedit.core.simulation.openmodelica import OpenModelicaInterface
|
||||
|
||||
__all__ = ["Simulation"]
|
||||
__all__ = ["OpenModelicaInterface", "Simulation"]
|
||||
|
||||
@@ -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)
|
||||
247
BEdit/src/bedit/core/simulation/openmodelica.py
Normal file
247
BEdit/src/bedit/core/simulation/openmodelica.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -653,7 +653,7 @@
|
||||
"startTime": 0.0,
|
||||
"stopTime": 10.0,
|
||||
"intervalMode": "numberOfIntervals",
|
||||
"numberOfIntervals": 5000,
|
||||
"numberOfIntervals": 50,
|
||||
"intervalTime": 0.002
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user