Simulation progression
This commit is contained in:
@@ -9,10 +9,12 @@ description = "A Bondgraph and block scheme simulator"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"DyMat>=0.7,<1",
|
||||
"matplotlib>=3.8,<4",
|
||||
"msgpack>=1.0,<2",
|
||||
"OMPython>=3.6,<5",
|
||||
"PySide6>=6.7,<7",
|
||||
"scipy>=1.12,<2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -25,6 +25,7 @@ class SimulationRunController(QObject):
|
||||
run_cancelled = Signal()
|
||||
run_failed = Signal(object)
|
||||
run_progress = Signal(int)
|
||||
segment_completed = Signal()
|
||||
simulation_state_changed = Signal()
|
||||
|
||||
def __init__(self, window: SimulationWindow, files: SimulationFileController, session_factory: SessionFactory = _create_session) -> None:
|
||||
@@ -45,6 +46,7 @@ class SimulationRunController(QObject):
|
||||
self.run_cancelled.connect(self._cancelled)
|
||||
self.run_failed.connect(self._failed)
|
||||
self.run_progress.connect(self.window.ui.progressBar.setValue)
|
||||
self.segment_completed.connect(self._segment_completed)
|
||||
self._load_session()
|
||||
|
||||
@property
|
||||
@@ -98,7 +100,7 @@ class SimulationRunController(QObject):
|
||||
self.run_completed.emit(result)
|
||||
|
||||
async def _run_with_progress(self, session: SimulationSession) -> SimulationResult:
|
||||
task = asyncio.create_task(session.run_next())
|
||||
task = asyncio.create_task(session.run_next_segmented(lambda _result: self.segment_completed.emit()))
|
||||
last_progress = -1
|
||||
while not task.done():
|
||||
progress = min(session.get_progress(), 99)
|
||||
@@ -108,6 +110,12 @@ class SimulationRunController(QObject):
|
||||
await asyncio.sleep(0.1)
|
||||
return await task
|
||||
|
||||
def _segment_completed(self) -> None:
|
||||
self._store_session_state()
|
||||
if self.session is not None:
|
||||
self.window.statusBar().showMessage(f"Simulation running · current time {self.session.current_end_time}")
|
||||
logger.debug("Simulation segment completed at time %s", self.session.current_end_time)
|
||||
|
||||
def _completed(self, result: SimulationResult) -> None:
|
||||
self._running = False
|
||||
self._store_session_state()
|
||||
|
||||
@@ -6,7 +6,7 @@ from .openmodelica import (
|
||||
ProcessResult,
|
||||
SimulationCancelledError,
|
||||
)
|
||||
from .results import SimulationResult, load_openmodelica_csv
|
||||
from .results import SimulationResult, load_openmodelica_csv, load_openmodelica_mat
|
||||
from .compile import compile_component, compile_component_sync
|
||||
from .simulation import (
|
||||
ModelBuildResult,
|
||||
@@ -38,5 +38,6 @@ __all__ = [
|
||||
"compile_component",
|
||||
"compile_component_sync",
|
||||
"load_openmodelica_csv",
|
||||
"load_openmodelica_mat",
|
||||
"simulate",
|
||||
]
|
||||
|
||||
@@ -44,3 +44,25 @@ def load_openmodelica_csv(path: str | Path) -> dict[str, list[float]]:
|
||||
except StopIteration as error:
|
||||
raise ValueError("simulation result CSV is empty") from error
|
||||
return columns
|
||||
|
||||
|
||||
def load_openmodelica_mat(path: str | Path) -> dict[str, list[float]]:
|
||||
"""Load the MATLAB v4 result layout written by OpenModelica."""
|
||||
try:
|
||||
from DyMat import DyMatFile
|
||||
except ImportError:
|
||||
raise ValueError("reading OpenModelica MAT results requires DyMat") from None
|
||||
try:
|
||||
result = DyMatFile(str(Path(path)))
|
||||
blocks = result.blocks()
|
||||
time_block = 2 if 2 in blocks else blocks[-1]
|
||||
times = result.abscissa(time_block, valuesOnly=True).astype(float).tolist()
|
||||
columns = {"time": times}
|
||||
for name in result.names():
|
||||
values = result.data(name).astype(float).tolist()
|
||||
if len(values) != len(times):
|
||||
values = [values[0]] * len(times)
|
||||
columns[name] = values
|
||||
except (IndexError, KeyError, OSError, TypeError, ValueError) as error:
|
||||
raise ValueError(f"could not read OpenModelica MAT results: {error}") from error
|
||||
return columns
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,6 +9,8 @@ from .openmodelica import OpenModelicaRunner
|
||||
from .results import SimulationResult
|
||||
from .simulation import Simulation, SimulationOptions, SimulationStateError
|
||||
|
||||
_SEGMENTS_PER_RUN = 10
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SimulationRunSettings:
|
||||
@@ -40,26 +42,70 @@ class SimulationSession:
|
||||
self.results = list(results)
|
||||
self._simulation = Simulation(runner)
|
||||
self._simulation.load_compiled(model_name, executable)
|
||||
self._segments_completed = 0
|
||||
self._segments_total = 1
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._simulation.is_running
|
||||
|
||||
def get_progress(self) -> int:
|
||||
return self._simulation.get_progress()
|
||||
return min(100, int((self._segments_completed + self._simulation.get_progress() / 100) * 100 / self._segments_total))
|
||||
|
||||
async def run_next(self) -> SimulationResult:
|
||||
start_time = self.settings.start_time
|
||||
stop_time = self.current_end_time + self.settings.duration
|
||||
run_duration = stop_time - start_time
|
||||
intervals = math.ceil(run_duration / self.settings.step_size) if self.settings.use_timed_steps else math.ceil(self.settings.number_of_steps * run_duration / self.settings.duration)
|
||||
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)
|
||||
# TODO: Continue from OpenModelica restart state instead of recomputing the full time range.
|
||||
self.current_end_time = stop_time
|
||||
self.results[:] = [result]
|
||||
return await self.run_next_segmented()
|
||||
|
||||
async def run_next_segmented(self, segment_completed: Callable[[SimulationResult], None] | None = None) -> SimulationResult:
|
||||
"""Advance one configured duration in restartable, plot-ready segments."""
|
||||
previous_file = self._restart_file()
|
||||
if previous_file is None and self.current_end_time != self.settings.start_time:
|
||||
self.current_end_time = self.settings.start_time
|
||||
self.results.clear()
|
||||
run_start_time = self.current_end_time
|
||||
run_stop_time = run_start_time + self.settings.duration
|
||||
segment_duration = self.settings.duration / _SEGMENTS_PER_RUN
|
||||
intervals = self._segment_intervals(segment_duration)
|
||||
self._segments_completed = 0
|
||||
self._segments_total = _SEGMENTS_PER_RUN
|
||||
result: SimulationResult | None = None
|
||||
for segment in range(_SEGMENTS_PER_RUN):
|
||||
start_time = self.current_end_time
|
||||
stop_time = run_stop_time if segment == _SEGMENTS_PER_RUN - 1 else run_start_time + (segment + 1) * segment_duration
|
||||
result_file = self._simulation.last_build.executable.parent / f"{self._simulation.last_build.executable.stem}_segment_{len(self.results):06d}.mat"
|
||||
options = SimulationOptions(
|
||||
start_time=start_time,
|
||||
stop_time=stop_time,
|
||||
number_of_intervals=intervals,
|
||||
tolerance=self.settings.tolerance,
|
||||
method=self.settings.method,
|
||||
result_file=result_file,
|
||||
initialization_file=previous_file,
|
||||
initialization_time=start_time if previous_file is not None else None,
|
||||
skip_initialization=previous_file is not None,
|
||||
)
|
||||
result = await self._simulation.run(options)
|
||||
self.current_end_time = stop_time
|
||||
self.results.append(result)
|
||||
previous_file = result.result_file
|
||||
self._segments_completed += 1
|
||||
if segment_completed is not None:
|
||||
segment_completed(result)
|
||||
assert result is not None
|
||||
self._segments_total = 1
|
||||
self._segments_completed = 1
|
||||
return result
|
||||
|
||||
def _segment_intervals(self, segment_duration: float) -> int:
|
||||
if self.settings.use_timed_steps:
|
||||
return max(1, math.ceil(segment_duration / self.settings.step_size))
|
||||
return max(1, math.ceil(self.settings.number_of_steps / _SEGMENTS_PER_RUN))
|
||||
|
||||
def _restart_file(self) -> Path | None:
|
||||
for result in reversed(self.results):
|
||||
if result.result_file is not None and result.result_file.is_file():
|
||||
return result.result_file
|
||||
return None
|
||||
|
||||
def cancel(self) -> bool:
|
||||
return self._simulation.cancel()
|
||||
|
||||
@@ -68,3 +114,5 @@ class SimulationSession:
|
||||
raise SimulationStateError("cannot reset while a simulation is running")
|
||||
self.current_end_time = self.settings.start_time
|
||||
self.results.clear()
|
||||
self._segments_completed = 0
|
||||
self._segments_total = 1
|
||||
|
||||
@@ -22,7 +22,7 @@ from .openmodelica import (
|
||||
ProcessResult,
|
||||
_run_on_worker,
|
||||
)
|
||||
from .results import SimulationResult, load_openmodelica_csv
|
||||
from .results import SimulationResult, load_openmodelica_csv, load_openmodelica_mat
|
||||
|
||||
_MODEL_FILE = "model.mo"
|
||||
_RUN_SCRIPT_FILE = "run.mos"
|
||||
@@ -39,6 +39,10 @@ class SimulationOptions:
|
||||
number_of_intervals: int = 500
|
||||
tolerance: float = 1e-6
|
||||
method: str | None = None
|
||||
result_file: str | Path | None = None
|
||||
initialization_file: str | Path | None = None
|
||||
initialization_time: float | None = None
|
||||
skip_initialization: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.stop_time <= self.start_time:
|
||||
@@ -473,7 +477,7 @@ class Simulation:
|
||||
options: SimulationOptions,
|
||||
directory: Path,
|
||||
) -> SimulationResult:
|
||||
result_path = directory / f"{executable.stem}_res.csv"
|
||||
result_path = Path(options.result_file) if options.result_file is not None else directory / f"{executable.stem}_res.csv"
|
||||
process = self._run_executable(executable, options, directory)
|
||||
if not result_path.is_file():
|
||||
details = process.stderr.strip() or process.stdout.strip()
|
||||
@@ -491,7 +495,7 @@ class Simulation:
|
||||
)
|
||||
return SimulationResult(
|
||||
model_name=model_name,
|
||||
data=load_openmodelica_csv(result_path),
|
||||
data=load_openmodelica_mat(result_path) if result_path.suffix.lower() == ".mat" else load_openmodelica_csv(result_path),
|
||||
process_output=process.stdout,
|
||||
process_errors=process.stderr,
|
||||
result_file=result_path,
|
||||
@@ -529,10 +533,18 @@ class Simulation:
|
||||
f"-stopTime={options.stop_time}",
|
||||
f"-stepSize={step_size}",
|
||||
f"-tolerance={options.tolerance}",
|
||||
"-outputFormat=csv",
|
||||
f"-outputFormat={'mat' if options.result_file is not None and Path(options.result_file).suffix.lower() == '.mat' else 'csv'}",
|
||||
f"-port={port}",
|
||||
"-logFormat=xmltcp",
|
||||
]
|
||||
if options.result_file is not None:
|
||||
arguments.extend([f"-r={Path(options.result_file).resolve()}", "-emit_protected", "-ignoreHideResult"])
|
||||
if options.initialization_file is not None:
|
||||
arguments.append(f"-iif={Path(options.initialization_file).resolve()}")
|
||||
if options.initialization_time is not None:
|
||||
arguments.append(f"-iit={options.initialization_time}")
|
||||
if options.skip_initialization:
|
||||
arguments.append("-iim=none")
|
||||
if options.method:
|
||||
arguments.append(f"-s={options.method}")
|
||||
script_path = directory / _RUN_SCRIPT_FILE
|
||||
|
||||
+1
-1
@@ -2049,7 +2049,7 @@
|
||||
"name": "run bondgraph",
|
||||
"start_time": 0.0,
|
||||
"duration": 10.0,
|
||||
"use_timed_steps": true,
|
||||
"use_timed_steps": false,
|
||||
"number_of_steps": 500,
|
||||
"step_size": 0.001,
|
||||
"method": "dassl",
|
||||
|
||||
Reference in New Issue
Block a user