Hooked up simulation to BEsim
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QObject, Qt
|
||||
from PySide6.QtCore import QObject, Signal, Qt
|
||||
from PySide6.QtWidgets import QApplication, QDialog, QFileDialog, QInputDialog, QMessageBox
|
||||
|
||||
from bedit_gui.services import document_files, simulation_files
|
||||
@@ -17,6 +17,8 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SimulationFileController(QObject):
|
||||
runtime_changed = Signal()
|
||||
|
||||
def __init__(self, window: SimulationWindow) -> None:
|
||||
super().__init__(window)
|
||||
self.window = window
|
||||
@@ -34,6 +36,7 @@ class SimulationFileController(QObject):
|
||||
self.compiled_model = None
|
||||
self.path = None
|
||||
self._update_window()
|
||||
self.runtime_changed.emit()
|
||||
logger.info("Created new simulation")
|
||||
|
||||
def open(self, path: str | Path, *, component: str | None = None, simulation: str | None = None, backed_by_file: bool = True, compiled_model: CompiledModel | None = None) -> None:
|
||||
@@ -51,6 +54,7 @@ class SimulationFileController(QObject):
|
||||
raise
|
||||
self._log_compilation()
|
||||
self._update_window()
|
||||
self.runtime_changed.emit()
|
||||
logger.info("Opened simulation: %s", file_path)
|
||||
|
||||
def open_dialog(self) -> None:
|
||||
@@ -111,9 +115,12 @@ class SimulationFileController(QObject):
|
||||
self.root.component = updated.component
|
||||
self.root.component_path = next((path for component_id, _component, path in components if component_id == updated.component), self.root.component_path)
|
||||
self.root.settings_name = updated.name
|
||||
self.root.current_end_time = updated.start_time
|
||||
self.root.results.clear()
|
||||
if component_changed:
|
||||
self.compiled_model = None
|
||||
self._update_window()
|
||||
self.runtime_changed.emit()
|
||||
logger.info("Updated simulation settings: %s", updated.name)
|
||||
|
||||
def _source_components(self) -> list[tuple]:
|
||||
|
||||
155
src/bedit_gui/controllers/simulation_run_controller.py
Normal file
155
src/bedit_gui/controllers/simulation_run_controller.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from threading import Thread
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from bedit_gui.controllers.simulation_file_controller import SimulationFileController
|
||||
from bedit_gui.services.application_logging import get_logger
|
||||
from bedit_gui.simulation_models import CompiledModel
|
||||
from bedit_gui.views.simulation_window import SimulationWindow
|
||||
from bedit_simulation import SimulationCancelledError, SimulationResult, SimulationRunSettings, SimulationSession
|
||||
|
||||
logger = get_logger(__name__)
|
||||
SessionFactory = Callable[[CompiledModel, SimulationRunSettings, float | None, list[SimulationResult]], SimulationSession]
|
||||
|
||||
|
||||
def _create_session(compiled: CompiledModel, settings: SimulationRunSettings, current_end_time: float | None, results: list[SimulationResult]) -> SimulationSession:
|
||||
return SimulationSession(compiled.model_name, compiled.executable, settings, current_end_time=current_end_time, results=results)
|
||||
|
||||
|
||||
class SimulationRunController(QObject):
|
||||
run_completed = Signal(object)
|
||||
run_cancelled = Signal()
|
||||
run_failed = Signal(object)
|
||||
|
||||
def __init__(self, window: SimulationWindow, files: SimulationFileController, session_factory: SessionFactory = _create_session) -> None:
|
||||
super().__init__(window)
|
||||
self.window = window
|
||||
self.files = files
|
||||
self.session_factory = session_factory
|
||||
self.session: SimulationSession | None = None
|
||||
self._running = False
|
||||
self._reset_pending = False
|
||||
|
||||
window.ui.actionRun_Simulation.triggered.connect(self.start)
|
||||
window.ui.actionStop_Simulation.triggered.connect(self.stop)
|
||||
window.ui.actionRestart_Simulation.triggered.connect(self.restart)
|
||||
files.runtime_changed.connect(self._load_session)
|
||||
self.run_completed.connect(self._completed)
|
||||
self.run_cancelled.connect(self._cancelled)
|
||||
self.run_failed.connect(self._failed)
|
||||
self._load_session()
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running or self.session is None:
|
||||
return
|
||||
start_time = self.session.current_end_time
|
||||
stop_time = start_time + self.session.settings.duration
|
||||
self._running = True
|
||||
self._update_actions()
|
||||
logger.info("Starting simulation from %s to %s", start_time, stop_time)
|
||||
Thread(target=self._run_worker, args=(self.session,), name="besim-run", daemon=True).start()
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._running or self.session is None:
|
||||
return
|
||||
self.session.cancel()
|
||||
logger.info("Stopping simulation")
|
||||
|
||||
def restart(self) -> None:
|
||||
if self.session is None:
|
||||
return
|
||||
if self._running:
|
||||
self._reset_pending = True
|
||||
logger.info("Reset requested; stopping the active simulation")
|
||||
self.stop()
|
||||
return
|
||||
self._reset()
|
||||
|
||||
def _reset(self) -> None:
|
||||
if self.session is None:
|
||||
return
|
||||
self.session.reset()
|
||||
self._store_session_state()
|
||||
logger.info("Reset simulation to start time %s", self.session.current_end_time)
|
||||
self._update_actions()
|
||||
|
||||
def _run_worker(self, session: SimulationSession) -> None:
|
||||
try:
|
||||
result = asyncio.run(session.run_next())
|
||||
except SimulationCancelledError:
|
||||
self.run_cancelled.emit()
|
||||
except (OSError, RuntimeError, TypeError, ValueError) as exc:
|
||||
self.run_failed.emit(exc)
|
||||
else:
|
||||
self.run_completed.emit(result)
|
||||
|
||||
def _completed(self, result: SimulationResult) -> None:
|
||||
self._running = False
|
||||
self._store_session_state()
|
||||
logger.info("Simulation completed at time %s", self.session.current_end_time if self.session is not None else "unknown")
|
||||
if result.process_output.strip():
|
||||
logger.info("Simulation output:\n%s", result.process_output.strip())
|
||||
if result.process_errors.strip():
|
||||
logger.warning("Simulation errors:\n%s", result.process_errors.strip())
|
||||
self._finish_run()
|
||||
|
||||
def _cancelled(self) -> None:
|
||||
self._running = False
|
||||
logger.info("Simulation stopped")
|
||||
self._finish_run()
|
||||
|
||||
def _failed(self, error: Exception) -> None:
|
||||
self._running = False
|
||||
logger.error("Simulation failed: %s", error, exc_info=(type(error), error, error.__traceback__))
|
||||
self._finish_run()
|
||||
|
||||
def _finish_run(self) -> None:
|
||||
self._update_actions()
|
||||
if self._reset_pending:
|
||||
self._reset_pending = False
|
||||
self._reset()
|
||||
|
||||
def _load_session(self) -> None:
|
||||
root = self.files.root
|
||||
compiled = self.files.compiled_model
|
||||
self._reset_pending = False
|
||||
if root is None or compiled is None:
|
||||
self.session = None
|
||||
else:
|
||||
settings = root.settings
|
||||
try:
|
||||
run_settings = SimulationRunSettings(
|
||||
start_time=settings.start_time,
|
||||
duration=settings.duration,
|
||||
use_timed_steps=settings.use_timed_steps,
|
||||
number_of_steps=settings.number_of_steps,
|
||||
step_size=settings.step_size,
|
||||
tolerance=settings.dassl_tolerance,
|
||||
method=settings.method.value,
|
||||
)
|
||||
self.session = self.session_factory(compiled, run_settings, root.current_end_time, root.results)
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
self.session = None
|
||||
logger.exception("Could not initialize the simulation runtime")
|
||||
self._update_actions()
|
||||
|
||||
def _update_actions(self) -> None:
|
||||
available = self.session is not None
|
||||
self.window.ui.actionRun_Simulation.setEnabled(available and not self._running)
|
||||
self.window.ui.actionStop_Simulation.setEnabled(available and self._running)
|
||||
self.window.ui.actionRestart_Simulation.setEnabled(available)
|
||||
for action in (self.window.ui.actionNew_Simulation_Run, self.window.ui.actionOpen_Simulation_Run, self.window.ui.actionSave_Simulation_Run, self.window.ui.actionSimulation_Options):
|
||||
action.setEnabled(not self._running and (available or action is not self.window.ui.actionSimulation_Options))
|
||||
if self.session is not None:
|
||||
state = "running" if self._running else "ready"
|
||||
self.window.statusBar().showMessage(f"Simulation {state} · current time {self.session.current_end_time}")
|
||||
|
||||
def _store_session_state(self) -> None:
|
||||
if self.session is None or self.files.root is None:
|
||||
return
|
||||
self.files.root.current_end_time = self.session.current_end_time
|
||||
self.files.root.results = list(self.session.results)
|
||||
@@ -7,6 +7,7 @@ from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from bedit_gui.controllers.simulation_file_controller import SimulationFileController
|
||||
from bedit_gui.controllers.log_controller import LogController
|
||||
from bedit_gui.controllers.simulation_run_controller import SimulationRunController
|
||||
from bedit_gui.simulation_models import CompiledModel
|
||||
from bedit_gui.services.application_settings import SimulationApplicationSettings
|
||||
from bedit_gui.views.simulation_window import SimulationWindow
|
||||
@@ -42,6 +43,7 @@ def main(arguments: list[str] | None = None) -> int:
|
||||
|
||||
settings = SimulationApplicationSettings()
|
||||
LogController(window, settings.log_level)
|
||||
SimulationRunController(window, controller)
|
||||
|
||||
window.showMaximized()
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from bedit_core.models import ComponentID
|
||||
from bedit_gui.models import Simulation
|
||||
from bedit_simulation import SimulationResult
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -33,6 +34,8 @@ class SimulationRoot:
|
||||
component_path: str
|
||||
settings_name: str | None
|
||||
settings: Simulation
|
||||
current_end_time: float | None = None
|
||||
results: list[SimulationResult] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> SimulationRoot:
|
||||
@@ -46,6 +49,8 @@ class SimulationRoot:
|
||||
component_path=str(data.get("component_path", "")),
|
||||
settings_name=str(data["settings_name"]) if data.get("settings_name") is not None else None,
|
||||
settings=Simulation.from_data(data["settings"]),
|
||||
current_end_time=float(data["current_end_time"]) if data.get("current_end_time") is not None else None,
|
||||
results=[_result_from_data(result) for result in data.get("results", [])],
|
||||
)
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
@@ -58,4 +63,18 @@ class SimulationRoot:
|
||||
"component_path": self.component_path,
|
||||
"settings_name": self.settings_name,
|
||||
"settings": self.settings.to_data(),
|
||||
"current_end_time": self.current_end_time,
|
||||
"results": [_result_to_data(result) for result in self.results],
|
||||
}
|
||||
|
||||
|
||||
def _result_from_data(data: Mapping[str, Any]) -> SimulationResult:
|
||||
raw_columns = data.get("data", {})
|
||||
if not isinstance(raw_columns, Mapping):
|
||||
raise TypeError("simulation result data must be a mapping")
|
||||
columns = {str(name): [float(value) for value in values] for name, values in raw_columns.items()}
|
||||
return SimulationResult(model_name=str(data.get("model_name", "")), data=columns, process_output=str(data.get("process_output", "")), process_errors=str(data.get("process_errors", "")))
|
||||
|
||||
|
||||
def _result_to_data(result: SimulationResult) -> dict[str, Any]:
|
||||
return {"model_name": result.model_name, "data": result.data, "process_output": result.process_output, "process_errors": result.process_errors}
|
||||
|
||||
@@ -70,6 +70,9 @@ class SimulationSettingsDialog(QDialog):
|
||||
if simulation.dassl_tolerance <= 0:
|
||||
QMessageBox.warning(self, "Invalid simulation", "The DASSL tolerance must be greater than zero.")
|
||||
return
|
||||
if simulation.duration <= 0:
|
||||
QMessageBox.warning(self, "Invalid simulation", "The simulation length must be greater than zero.")
|
||||
return
|
||||
simulation.name = simulation.name.strip()
|
||||
self._database.active_simulation = self._simulation_id()
|
||||
super().accept()
|
||||
|
||||
@@ -4,6 +4,7 @@ from .openmodelica import (
|
||||
OpenModelicaError,
|
||||
OpenModelicaRunner,
|
||||
ProcessResult,
|
||||
SimulationCancelledError,
|
||||
)
|
||||
from .results import SimulationResult, load_openmodelica_csv
|
||||
from .compile import compile_component, compile_component_sync
|
||||
@@ -17,6 +18,7 @@ from .simulation import (
|
||||
SimulationStateError,
|
||||
simulate,
|
||||
)
|
||||
from .runtime import SimulationRunSettings, SimulationSession
|
||||
|
||||
__all__ = [
|
||||
"ModelBuildResult",
|
||||
@@ -26,9 +28,12 @@ __all__ = [
|
||||
"OpenModelicaRunner",
|
||||
"ProcessResult",
|
||||
"Simulation",
|
||||
"SimulationCancelledError",
|
||||
"SimulationOptions",
|
||||
"SimulationProgress",
|
||||
"SimulationResult",
|
||||
"SimulationRunSettings",
|
||||
"SimulationSession",
|
||||
"SimulationStateError",
|
||||
"compile_component",
|
||||
"compile_component_sync",
|
||||
|
||||
@@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -27,6 +29,10 @@ class OpenModelicaError(RuntimeError):
|
||||
"""Raised when OMC cannot start or reports a failure."""
|
||||
|
||||
|
||||
class SimulationCancelledError(RuntimeError):
|
||||
"""Raised when a running compiled simulation is cancelled."""
|
||||
|
||||
|
||||
ProcessExecutor = Callable[
|
||||
[Sequence[str], Path, float | None, Mapping[str, str] | None],
|
||||
ProcessResult,
|
||||
@@ -89,6 +95,58 @@ class OpenModelicaRunner:
|
||||
"""Run OMC on an asyncio worker thread."""
|
||||
return await _run_on_worker(self._run, script, working_directory)
|
||||
|
||||
def _run_cancellable(self, script: Path, working_directory: Path, cancel_event: Event) -> ProcessResult:
|
||||
command = (*self.command, str(script.resolve()))
|
||||
return self._run_cancellable_process(command, working_directory, cancel_event)
|
||||
|
||||
def _run_cancellable_process(self, command: Sequence[str], working_directory: Path, cancel_event: Event) -> ProcessResult:
|
||||
process_environment = None
|
||||
if self.environment is not None:
|
||||
process_environment = {**os.environ, **self.environment}
|
||||
try:
|
||||
process = subprocess.Popen(list(command), cwd=working_directory, env=process_environment, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=os.name != "nt")
|
||||
except OSError as exc:
|
||||
raise OpenModelicaError(f"simulation executable could not start: {exc}") from exc
|
||||
deadline = time.monotonic() + self.timeout if self.timeout is not None else None
|
||||
while True:
|
||||
if cancel_event.is_set():
|
||||
_terminate_process(process)
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
_kill_process(process)
|
||||
stdout, stderr = process.communicate()
|
||||
raise SimulationCancelledError("simulation was cancelled")
|
||||
if deadline is not None and time.monotonic() >= deadline:
|
||||
_kill_process(process)
|
||||
process.communicate()
|
||||
raise OpenModelicaError(f"simulation timed out after {self.timeout} seconds")
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=0.05)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
continue
|
||||
result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr)
|
||||
if result.return_code != 0:
|
||||
details = result.stderr.strip() or result.stdout.strip()
|
||||
suffix = f": {details}" if details else ""
|
||||
raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}")
|
||||
return result
|
||||
|
||||
|
||||
def _terminate_process(process: subprocess.Popen[str]) -> None:
|
||||
if os.name == "nt":
|
||||
process.terminate()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
|
||||
|
||||
def _kill_process(process: subprocess.Popen[str]) -> None:
|
||||
if os.name == "nt":
|
||||
process.kill()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
|
||||
|
||||
def _command_parts(command: str | Sequence[str]) -> tuple[str, ...]:
|
||||
if isinstance(command, str):
|
||||
@@ -140,7 +198,7 @@ async def _run_on_worker(
|
||||
def invoke() -> None:
|
||||
try:
|
||||
results.append(operation(*args, **kwargs))
|
||||
except BaseException as error:
|
||||
except Exception as error: # noqa: BLE001 - worker must relay operation failures
|
||||
errors.append(error)
|
||||
finally:
|
||||
finished.set()
|
||||
|
||||
65
src/bedit_simulation/runtime.py
Normal file
65
src/bedit_simulation/runtime.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from .openmodelica import OpenModelicaRunner
|
||||
from .results import SimulationResult
|
||||
from .simulation import Simulation, SimulationOptions, SimulationStateError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SimulationRunSettings:
|
||||
start_time: float = 0.0
|
||||
duration: float = 1.0
|
||||
use_timed_steps: bool = False
|
||||
number_of_steps: int = 500
|
||||
step_size: float = 0.002
|
||||
tolerance: float = 1e-6
|
||||
method: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.duration <= 0:
|
||||
raise ValueError("simulation duration must be positive")
|
||||
if self.number_of_steps <= 0:
|
||||
raise ValueError("number of steps must be positive")
|
||||
if self.step_size <= 0:
|
||||
raise ValueError("step size must be positive")
|
||||
if self.tolerance <= 0:
|
||||
raise ValueError("simulation tolerance must be positive")
|
||||
|
||||
|
||||
class SimulationSession:
|
||||
"""Run consecutive time ranges for one compiled model."""
|
||||
|
||||
def __init__(self, model_name: str, executable: str | Path, settings: SimulationRunSettings, *, current_end_time: float | None = None, results: Sequence[SimulationResult] = (), runner: OpenModelicaRunner | None = None) -> None:
|
||||
self.settings = settings
|
||||
self.current_end_time = settings.start_time if current_end_time is None else current_end_time
|
||||
self.results = list(results)
|
||||
self._simulation = Simulation(runner)
|
||||
self._simulation.load_compiled(model_name, executable)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._simulation.is_running
|
||||
|
||||
async def run_next(self) -> SimulationResult:
|
||||
start_time = self.current_end_time
|
||||
stop_time = start_time + self.settings.duration
|
||||
intervals = math.ceil(self.settings.duration / self.settings.step_size) if self.settings.use_timed_steps else self.settings.number_of_steps
|
||||
options = SimulationOptions(start_time=start_time, stop_time=stop_time, number_of_intervals=intervals, tolerance=self.settings.tolerance, method=self.settings.method)
|
||||
result = await self._simulation.run(options)
|
||||
self.current_end_time = stop_time
|
||||
self.results.append(result)
|
||||
return result
|
||||
|
||||
def cancel(self) -> bool:
|
||||
return self._simulation.cancel()
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.is_running:
|
||||
raise SimulationStateError("cannot reset while a simulation is running")
|
||||
self.current_end_time = self.settings.start_time
|
||||
self.results.clear()
|
||||
@@ -107,6 +107,9 @@ class Simulation:
|
||||
self.last_result: SimulationResult | None = None
|
||||
self._progress = SimulationProgress()
|
||||
self._progress_lock = Lock()
|
||||
self._cancel_event = Event()
|
||||
self._running_lock = Lock()
|
||||
self._running = False
|
||||
|
||||
@staticmethod
|
||||
def _compose(component: Component) -> CompositionResult:
|
||||
@@ -179,6 +182,25 @@ class Simulation:
|
||||
with self._progress_lock:
|
||||
self._progress = progress
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
with self._running_lock:
|
||||
return self._running
|
||||
|
||||
def cancel(self) -> bool:
|
||||
"""Request cancellation of the active compiled simulation run."""
|
||||
running = self.is_running
|
||||
self._cancel_event.set()
|
||||
return running
|
||||
|
||||
def load_compiled(self, model_name: str, executable: str | Path) -> None:
|
||||
"""Load an existing compiled model without composing or compiling it."""
|
||||
executable_path = Path(executable)
|
||||
if not executable_path.is_file():
|
||||
raise ValueError(f"compiled simulation executable does not exist: {executable_path}")
|
||||
self._set_model("", model_name)
|
||||
self.last_build = ModelBuildResult(model_name=model_name, executable=executable_path, output="", errors="")
|
||||
|
||||
def _active_model(self) -> tuple[str, str]:
|
||||
if self._modelica is None or self._model_name is None:
|
||||
raise SimulationStateError(
|
||||
@@ -411,14 +433,17 @@ class Simulation:
|
||||
raise SimulationStateError(
|
||||
"working_directory must be the directory used by build()"
|
||||
)
|
||||
with self._running_lock:
|
||||
if self._running:
|
||||
raise SimulationStateError("a simulation is already running")
|
||||
self._running = True
|
||||
self._set_progress(SimulationProgress())
|
||||
result = await _run_on_worker(
|
||||
self._run_built_model,
|
||||
model_name,
|
||||
build.executable,
|
||||
options,
|
||||
directory,
|
||||
)
|
||||
try:
|
||||
result = await _run_on_worker(self._run_built_model, model_name, build.executable, options, directory)
|
||||
finally:
|
||||
self._cancel_event.clear()
|
||||
with self._running_lock:
|
||||
self._running = False
|
||||
self.last_result = result
|
||||
return result
|
||||
|
||||
@@ -509,12 +534,9 @@ class Simulation:
|
||||
if options.method:
|
||||
arguments.append(f"-s={options.method}")
|
||||
script_path = directory / _RUN_SCRIPT_FILE
|
||||
script_path.write_text(
|
||||
_executable_script(arguments, directory),
|
||||
encoding="utf-8",
|
||||
)
|
||||
script_path.write_text(_executable_script(arguments, directory), encoding="utf-8")
|
||||
try:
|
||||
process = self.runner._run(script_path, directory)
|
||||
process = self.runner._run_cancellable(script_path, directory, self._cancel_event)
|
||||
finally:
|
||||
command_finished.set()
|
||||
reader.join(timeout=20)
|
||||
@@ -649,17 +671,13 @@ async def simulate(
|
||||
)
|
||||
|
||||
|
||||
def _executable_script(
|
||||
arguments: Sequence[str],
|
||||
working_directory: Path,
|
||||
) -> str:
|
||||
def _executable_script(arguments: Sequence[str], working_directory: Path) -> str:
|
||||
"""Create an OMC script that starts a compiled simulation binary."""
|
||||
command = shlex.join(arguments)
|
||||
return "\n".join(
|
||||
[
|
||||
f"cd({json.dumps(str(working_directory.resolve()))});",
|
||||
f"status := system({json.dumps(command)}, "
|
||||
f"{json.dumps(_RUN_OUTPUT_FILE)});",
|
||||
f"status := system({json.dumps(command)}, {json.dumps(_RUN_OUTPUT_FILE)});",
|
||||
"if status <> 0 then",
|
||||
f" print(readFile({json.dumps(_RUN_OUTPUT_FILE)}));",
|
||||
" exit(1);",
|
||||
|
||||
Reference in New Issue
Block a user