Launching of simulation application

This commit is contained in:
2026-07-31 12:24:52 +02:00
parent 66737e323b
commit 89ac2d8ff8
17 changed files with 555 additions and 36 deletions

View File

@@ -161,6 +161,18 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen
- Scope destructive shortcuts to the relevant widget where appropriate. - Scope destructive shortcuts to the relevant widget where appropriate.
- Always guard the operation itself even when an action is disabled for presentation. - Always guard the operation itself even when an action is disabled for presentation.
## Simulator application
- `bedit_gui/simulation_application.py` is the composition root for the separate `bedit-sim` Qt application.
- Keep compilation GUI-independent. Shared compile entry points belong in `bedit_simulation` and must not create or depend on a `QApplication` or window.
- The simulator opens BEdit `.beb`/`.json` documents or serialized simulation `.bes`/`.json` files.
- A component selector uses default simulation settings. A stored simulation-settings block already identifies its component; do not require both selectors.
- `SimulationDatabase.active_simulation` is the persisted active settings ID selected when the Simulation Settings dialog is accepted.
- Simulation files serialize `bedit_gui.simulation_models.SimulationRoot`. JSON uses the `root_type = "simulation_root"` discriminator; binary `.bes` files use their own BES header.
- `SimulationRoot` contains source-document identity, the selected component and copied settings, and eventually results. Compiled artifacts are transient application state and must not be serialized; reopen saved simulations by compiling them again. Never serialize live Python objects.
- BEdit launches the simulator as a separate process. Compile/Open Simulation integration may transfer a `.bes` file to that process.
- Keep simulator file workflows in their own services/controllers rather than adding them to `MainWindow` or the editor document controller.
## Verification ## Verification
The old test suite and its VS Code/packaging references were deliberately removed. Do not recreate a test suite unless asked. The old test suite and its VS Code/packaging references were deliberately removed. Do not recreate a test suite unless asked.

View File

@@ -18,7 +18,7 @@ dependencies = [
bedit-graphviz = "bedit_util.graphviz:main" bedit-graphviz = "bedit_util.graphviz:main"
bedit-simulate = "bedit_util.simulate:main" bedit-simulate = "bedit_util.simulate:main"
bedit = "bedit_gui.application:main" bedit = "bedit_gui.application:main"
bedit-sim = "bedit_gui.simulation_application:main" besim = "bedit_gui.simulation_application:main"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [

21
simulation.besim.json Normal file
View File

@@ -0,0 +1,21 @@
{
"file_format_version": 1,
"root_type": "simulation_root",
"format_version": 1,
"source_document": "/home/joppe/Projects/BEdit/untitled.bedit.json",
"source_document_id": "3b6780c7-488b-471e-a784-392db7632090",
"component": "50e6ef97-f686-4400-bc01-e5a352e8cc22",
"component_path": "some_bondgraph",
"settings_name": "run bondgraph",
"settings": {
"component": "50e6ef97-f686-4400-bc01-e5a352e8cc22",
"name": "run bondgraph",
"start_time": 0.0,
"duration": 10.0,
"use_timed_steps": true,
"number_of_steps": 500,
"step_size": 0.001,
"method": "dassl",
"dassl_tolerance": 1e-06
}
}

View File

@@ -10,6 +10,7 @@ from bedit_gui.controllers.document_controller import DocumentController
from bedit_gui.controllers.log_controller import LogController from bedit_gui.controllers.log_controller import LogController
from bedit_gui.controllers.settings_controller import SettingsController from bedit_gui.controllers.settings_controller import SettingsController
from bedit_gui.controllers.simulation_settings_controller import SimulationSettingsController from bedit_gui.controllers.simulation_settings_controller import SimulationSettingsController
from bedit_gui.controllers.simulation_controller import SimulationController
from bedit_gui.controllers.undo_controller import UndoController from bedit_gui.controllers.undo_controller import UndoController
from bedit_gui.controllers.view_menu_controller import ViewMenuController from bedit_gui.controllers.view_menu_controller import ViewMenuController
from bedit_gui.controllers.window_state_controller import WindowStateController from bedit_gui.controllers.window_state_controller import WindowStateController
@@ -48,6 +49,7 @@ def main() -> int:
DocumentController(document, window) DocumentController(document, window)
SettingsController(window, settings) SettingsController(window, settings)
SimulationSettingsController(document, window) SimulationSettingsController(document, window)
SimulationController(document, window)
UndoController(document, window) UndoController(document, window)
ViewMenuController(window) ViewMenuController(window)
document_tree_controller = DocumentTreeController(document, window) document_tree_controller = DocumentTreeController(document, window)

View File

@@ -0,0 +1,89 @@
from __future__ import annotations
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
from PySide6.QtCore import QObject, QProcess, Qt
from PySide6.QtWidgets import QApplication, QMessageBox
from bedit_core.models import Component
from bedit_gui.documents import Document
from bedit_gui.services import simulation_files
from bedit_gui.services.simulation_loader import component_choices
from bedit_gui.simulation_models import CompiledModel, SimulationRoot
from bedit_gui.views.main_window import MainWindow
from bedit_simulation import ModelBuildResult, compile_component_sync
Compiler = Callable[[Component, str | Path], ModelBuildResult]
Launcher = Callable[[Path, CompiledModel], bool]
def _launch_simulator(path: Path, compiled_model: CompiledModel) -> bool:
arguments = ["-m", "bedit_gui.simulation_application", "--handoff", "--model-name", compiled_model.model_name, "--executable", compiled_model.executable, "--working-directory", compiled_model.working_directory, str(path)]
launched = QProcess.startDetached(sys.executable, arguments)
return launched[0] if isinstance(launched, tuple) else bool(launched)
class SimulationController(QObject):
"""Compile the active BEdit simulation and launch the simulator process."""
def __init__(self, document: Document, window: MainWindow, compiler: Compiler = compile_component_sync, launcher: Launcher = _launch_simulator) -> None:
super().__init__(window)
self.document = document
self.window = window
self.compiler = compiler
self.launcher = launcher
self.compiled_root: SimulationRoot | None = None
self.compiled_model: CompiledModel | None = None
window.ui.actionCompile_Model.triggered.connect(self.compile_model)
window.ui.actionOpen_Simulation_Window.triggered.connect(self.open_simulation)
def compile_model(self) -> SimulationRoot | None:
database = self.document.simulation_database()
settings = database.simulations.get(database.active_simulation) if database.active_simulation is not None else None
if settings is None:
QMessageBox.warning(self.window, "No active simulation", "Select and accept a simulation settings block first.")
return None
match = next((choice for choice in component_choices(self.document.model) if choice[0] == settings.component), None)
if match is None:
QMessageBox.critical(self.window, "Could not compile", "The active simulation references a missing component.")
return None
component_id, component, component_path = match
build_directory = Path(tempfile.mkdtemp(prefix="bedit-compiled-"))
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
self.window.statusBar().showMessage(f"Compiling {component_path}")
try:
build = self.compiler(component, build_directory)
except (OSError, RuntimeError, ValueError) as exc:
QMessageBox.critical(self.window, "Could not compile", str(exc))
self.window.statusBar().showMessage("Compilation failed")
return None
finally:
QApplication.restoreOverrideCursor()
self.compiled_model = CompiledModel(build.model_name, str(build.executable.resolve()), str(build.executable.parent.resolve()))
self.compiled_root = SimulationRoot(
format_version=1,
source_document=str(self.document.path.resolve()) if self.document.path is not None else None,
source_document_id=str(self.document.model.id),
component=component_id,
component_path=component_path,
settings_name=settings.name,
settings=settings,
)
self.window.statusBar().showMessage(f"Compiled {component_path}")
return self.compiled_root
def open_simulation(self) -> None:
root = self.compile_model()
if root is None or self.compiled_model is None:
return
transfer_directory = Path(tempfile.mkdtemp(prefix="bedit-simulator-launch-"))
transfer_path = transfer_directory / "simulation.bes"
try:
simulation_files.save(root, transfer_path)
if not self.launcher(transfer_path, self.compiled_model):
raise RuntimeError("the simulator process could not be started")
except (OSError, RuntimeError, ValueError) as exc:
QMessageBox.critical(self.window, "Could not open simulator", str(exc))

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import QObject, Qt
from PySide6.QtWidgets import QApplication, QFileDialog, QInputDialog, QMessageBox
from bedit_gui.services import document_files, simulation_files
from bedit_gui.services.simulation_loader import compile_simulation_root, component_choices, load_and_compile_bedit
from bedit_gui.simulation_models import CompiledModel, SimulationRoot
from bedit_gui.views.simulation_window import SimulationWindow
class SimulationFileController(QObject):
def __init__(self, window: SimulationWindow) -> None:
super().__init__(window)
self.window = window
self.root: SimulationRoot | None = None
self.compiled_model: CompiledModel | None = None
self.path: Path | None = None
window.ui.actionNew_Simulation_Run.triggered.connect(self.new)
window.ui.actionOpen_Simulation_Run.triggered.connect(self.open_dialog)
window.ui.actionSave_Simulation_Run.triggered.connect(self.save)
self._update_window()
def new(self) -> None:
self.root = None
self.compiled_model = None
self.path = None
self._update_window()
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:
file_path = Path(path)
if file_path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(file_path):
self.root = simulation_files.load(file_path)
self.compiled_model = compiled_model or self._recompile_with_wait_cursor(self.root)
self.path = file_path if backed_by_file else None
else:
self.root, self.compiled_model = self._compile_with_wait_cursor(file_path, component=component, simulation=simulation)
self.path = None
self._update_window()
def open_dialog(self) -> None:
filename, _ = QFileDialog.getOpenFileName(self.window, "Open Simulation", "", "Simulation and BEdit files (*.bes *.beb *.json)")
if not filename:
return
try:
path = Path(filename)
if path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(path):
self.open(path)
return
document = document_files.load(path)
database = document.metadata.get("simulation_database") if document.metadata is not None else None
entries = [(f"Simulation: {simulation.name}", None, simulation.name) for simulation in getattr(database, "simulations", {}).values()]
entries.extend((f"Component: {component_path}", component_path, None) for _component_id, _component, component_path in component_choices(document))
if not entries:
raise ValueError("the BEdit document contains no components or simulation settings")
label, accepted = QInputDialog.getItem(self.window, "Simulation Target", "Compile:", [entry[0] for entry in entries], 0, False)
if accepted:
_display, component, simulation = next(entry for entry in entries if entry[0] == label)
self.open(path, component=component, simulation=simulation)
except (OSError, RuntimeError, ValueError) as exc:
QMessageBox.critical(self.window, "Could not open simulation", str(exc))
def save(self) -> None:
if self.root is None:
return
path = self.path
if path is None:
filename, _ = QFileDialog.getSaveFileName(self.window, "Save Simulation", "simulation.bes", "BEdit simulation (*.bes);;Simulation JSON (*.json)")
if not filename:
return
path = Path(filename)
if path.suffix.lower() not in (".bes", ".json"):
path = path.with_suffix(".bes")
try:
simulation_files.save(self.root, path)
except (OSError, ValueError) as exc:
QMessageBox.critical(self.window, "Could not save simulation", str(exc))
return
self.path = path
self._update_window()
def _compile_with_wait_cursor(self, path: Path, *, component: str | None, simulation: str | None) -> tuple[SimulationRoot, CompiledModel]:
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
return load_and_compile_bedit(path, component_selector=component, simulation_selector=simulation)
finally:
QApplication.restoreOverrideCursor()
def _recompile_with_wait_cursor(self, root: SimulationRoot) -> CompiledModel:
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
return compile_simulation_root(root)
finally:
QApplication.restoreOverrideCursor()
def _update_window(self) -> None:
if self.root is None:
self.window.setWindowTitle("BEdit Simulator")
self.window.statusBar().showMessage("No simulation loaded")
self.window.ui.actionSave_Simulation_Run.setEnabled(False)
return
self.window.setWindowTitle(f"{self.root.settings.name} — BEdit Simulator")
compiled = self.compiled_model.executable if self.compiled_model is not None else "not compiled"
self.window.statusBar().showMessage(f"{self.root.component_path} · {compiled}")
self.window.ui.actionSave_Simulation_Run.setEnabled(True)

View File

@@ -16,7 +16,7 @@ from bedit_gui.commands.rename_component_command import RenameComponentCommand
from bedit_gui.commands.rename_document_command import RenameDocumentCommand from bedit_gui.commands.rename_document_command import RenameDocumentCommand
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
from bedit_gui.models import Icon, IconDatabase, SimulationDatabase from bedit_gui.models import Icon, IconDatabase, Simulation, SimulationDatabase
from bedit_gui.services import document_files from bedit_gui.services import document_files
@@ -131,6 +131,10 @@ class Document(QObject):
def simulation_database(self) -> SimulationDatabase: def simulation_database(self) -> SimulationDatabase:
return self.stored_simulation_database() or SimulationDatabase() return self.stored_simulation_database() or SimulationDatabase()
def active_simulation(self) -> Simulation | None:
database = self.simulation_database()
return database.simulations.get(database.active_simulation) if database.active_simulation is not None else None
def change_simulation_database(self, database: SimulationDatabase) -> None: def change_simulation_database(self, database: SimulationDatabase) -> None:
if database != self.stored_simulation_database(): if database != self.stored_simulation_database():
self.undo_stack.push(ChangeSimulationDatabaseCommand(self, database)) self.undo_stack.push(ChangeSimulationDatabaseCommand(self, database))

View File

@@ -176,11 +176,14 @@ class Simulation:
class SimulationDatabase: class SimulationDatabase:
format_version: int = 1 format_version: int = 1
simulations: dict[SimulationID, Simulation] = field(default_factory=dict) simulations: dict[SimulationID, Simulation] = field(default_factory=dict)
active_simulation: SimulationID | None = None
@classmethod @classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationDatabase: def from_data(cls, data: Mapping[str, Any]) -> SimulationDatabase:
sims = {SimulationID(key): Simulation.from_data(value) for key, value in data.get("simulations", {}).items()} sims = {SimulationID(key): Simulation.from_data(value) for key, value in data.get("simulations", {}).items()}
return cls(format_version=int(data.get("format_version", 1)), simulations=sims) active = data.get("active_simulation")
active_id = SimulationID(str(active)) if active is not None else None
return cls(format_version=int(data.get("format_version", 1)), simulations=sims, active_simulation=active_id if active_id in sims else None)
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {"format_version": self.format_version, "simulations": {str(key): sim.to_data() for key, sim in self.simulations.items()}} return {"format_version": self.format_version, "active_simulation": str(self.active_simulation) if self.active_simulation is not None else None, "simulations": {str(key): sim.to_data() for key, sim in self.simulations.items()}}

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
import json
import zlib
from collections.abc import Mapping
from pathlib import Path
from typing import Any
import msgpack
from bedit_gui.simulation_models import SimulationRoot
BES_MAGIC = b"BES\x00"
FILE_FORMAT_VERSION = 1
_VERSION_SIZE = 4
def load(path: str | Path) -> SimulationRoot:
file_path = Path(path)
data = _load_json(file_path) if file_path.suffix.lower() == ".json" else _load_bes(file_path)
return SimulationRoot.from_data(data)
def save(root: SimulationRoot, path: str | Path) -> None:
file_path = Path(path)
if file_path.suffix.lower() == ".json":
_save_json(root.to_data(), file_path)
elif file_path.suffix.lower() == ".bes":
_save_bes(root.to_data(), file_path)
else:
raise ValueError(f"unsupported simulation file extension {file_path.suffix!r}; expected '.json' or '.bes'")
def is_simulation_json(path: str | Path) -> bool:
file_path = Path(path)
if file_path.suffix.lower() != ".json":
return False
try:
return _load_json(file_path).get("root_type") == "simulation_root"
except (TypeError, ValueError):
return False
def _load_json(path: Path) -> Mapping[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise ValueError(f"could not read simulation JSON {path}: {exc}") from exc
if not isinstance(data, Mapping):
raise TypeError(f"simulation JSON {path} must contain an object")
return data
def _save_json(data: Mapping[str, Any], path: Path) -> None:
path.write_text(json.dumps({"file_format_version": FILE_FORMAT_VERSION, **data}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def _load_bes(path: Path) -> Mapping[str, Any]:
try:
payload = path.read_bytes()
header_end = len(BES_MAGIC) + _VERSION_SIZE
if not payload.startswith(BES_MAGIC):
raise ValueError("missing BES file header")
if len(payload) < header_end:
raise ValueError("truncated BES file header")
version = int.from_bytes(payload[len(BES_MAGIC):header_end], "big")
if version != FILE_FORMAT_VERSION:
raise ValueError(f"unsupported BES file version {version}")
data = msgpack.unpackb(zlib.decompress(payload[header_end:]), raw=False, strict_map_key=False)
except (OSError, ValueError, zlib.error, msgpack.exceptions.MsgpackException) as exc:
raise ValueError(f"could not read BES simulation {path}: {exc}") from exc
if not isinstance(data, Mapping):
raise TypeError(f"BES simulation {path} must contain a map")
return data
def _save_bes(data: Mapping[str, Any], path: Path) -> None:
encoded = zlib.compress(msgpack.packb(dict(data), use_bin_type=True))
path.write_bytes(BES_MAGIC + FILE_FORMAT_VERSION.to_bytes(_VERSION_SIZE, "big") + encoded)

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import tempfile
from pathlib import Path
from bedit_core.models import Component, ComponentID, Document, GraphImplementation
from bedit_gui.models import Simulation, SimulationMethod
from bedit_gui.services import document_files
from bedit_gui.simulation_models import CompiledModel, SimulationRoot
from bedit_simulation import compile_component_sync
def component_choices(document: Document) -> list[tuple[ComponentID, Component, str]]:
choices: list[tuple[ComponentID, Component, str]] = []
def collect(items: dict[ComponentID, Component], path: tuple[str, ...] = ()) -> None:
for component_id, component in items.items():
component_path = (*path, component.name)
choices.append((component_id, component, ".".join(component_path)))
if isinstance(component.implementation, GraphImplementation):
collect(component.implementation.graph.components, component_path)
collect(document.root)
return choices
def load_and_compile_bedit(path: str | Path, *, component_selector: str | None = None, simulation_selector: str | None = None, working_directory: str | Path | None = None, omc_command: str = "omc") -> tuple[SimulationRoot, CompiledModel]:
"""Open a BEdit document, resolve one launch target, and compile it."""
if bool(component_selector) == bool(simulation_selector):
raise ValueError("specify exactly one component or simulation settings block")
source_path = Path(path).resolve()
document = document_files.load(source_path)
choices = component_choices(document)
settings_name: str | None = None
if component_selector is not None:
component_id, component, component_path = _find_component(choices, component_selector)
settings = _default_settings(component_id)
else:
database = document.metadata.get("simulation_database") if document.metadata is not None else None
if database is None or not hasattr(database, "simulations"):
raise ValueError("the BEdit document does not contain simulation settings")
matches = [(simulation_id, simulation) for simulation_id, simulation in database.simulations.items() if simulation.name == simulation_selector or str(simulation_id) == simulation_selector]
if len(matches) != 1:
raise ValueError(f"simulation settings {simulation_selector!r} were not found or are ambiguous")
_simulation_id, settings = matches[0]
component_id, component, component_path = _find_component(choices, str(settings.component))
settings_name = settings.name
build_directory = Path(working_directory) if working_directory is not None else Path(tempfile.mkdtemp(prefix="bedit-compiled-"))
root = SimulationRoot(
format_version=1,
source_document=str(source_path),
source_document_id=str(document.id),
component=component_id,
component_path=component_path,
settings_name=settings_name,
settings=settings,
)
return root, _compile(component, build_directory, omc_command)
def compile_simulation_root(root: SimulationRoot, *, working_directory: str | Path | None = None, omc_command: str = "omc") -> CompiledModel:
if root.source_document is None:
raise ValueError("the simulation does not reference a BEdit source document and cannot be recompiled")
document = document_files.load(root.source_document)
_component_id, component, _component_path = _find_component(component_choices(document), str(root.component))
build_directory = Path(working_directory) if working_directory is not None else Path(tempfile.mkdtemp(prefix="bedit-compiled-"))
return _compile(component, build_directory, omc_command)
def _find_component(choices: list[tuple[ComponentID, Component, str]], selector: str) -> tuple[ComponentID, Component, str]:
matches = [choice for choice in choices if str(choice[0]) == selector or choice[2] == selector]
if len(matches) != 1:
raise ValueError(f"component {selector!r} was not found or is ambiguous")
return matches[0]
def _default_settings(component_id: ComponentID) -> Simulation:
return Simulation(
component=component_id,
name="Default",
start_time=0.0,
duration=1.0,
use_timed_steps=False,
number_of_steps=500,
step_size=0.002,
method=SimulationMethod.DASSL,
dassl_tolerance=1e-6,
)
def _compile(component: Component, working_directory: Path, omc_command: str) -> CompiledModel:
build = compile_component_sync(component, working_directory, omc_command=omc_command)
return CompiledModel(build.model_name, str(build.executable.resolve()), str(build.executable.parent.resolve()))

View File

@@ -1,31 +1,49 @@
from __future__ import annotations from __future__ import annotations
import sys
import argparse import argparse
import sys
def parse_arguments(): from PySide6.QtWidgets import QApplication, QMessageBox
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument( from bedit_gui.controllers.simulation_file_controller import SimulationFileController
"-f", "--file", type=str, help="Path to a BEditor BEsim file to open", default=None, required=False from bedit_gui.simulation_models import CompiledModel
) from bedit_gui.views.simulation_window import SimulationWindow
parser.add_argument(
"-c", "--component", type=str, help="Component path in BEdit file to simulate", default=None, required=False
)
parser.add_argument(
"-s", "--simulation", type=str, help="Simulation name in BEdit file to simulate", default=None, required=False
)
return parser.parse_args(), parser
def main() -> int: def parse_arguments(arguments: list[str] | None = None) -> argparse.Namespace:
try: parser = argparse.ArgumentParser(description="Open and run BEdit simulations")
args, parser = parse_arguments() parser.add_argument("file", nargs="?", help="BEdit (.beb/.json) or simulation (.bes/.json) file")
# TODO check if file is a BEditor file (.beb or .json) parser.add_argument("-f", "--file", dest="file_option", help=argparse.SUPPRESS)
if args.file and not (args.component or args.simulation): parser.add_argument("--handoff", action="store_true", help=argparse.SUPPRESS)
parser.error("Component or Simulation required when opening a BEdit file") parser.add_argument("--model-name", help=argparse.SUPPRESS)
except argparse.ArgumentError as e: parser.add_argument("--executable", help=argparse.SUPPRESS)
print(e.message) parser.add_argument("--working-directory", help=argparse.SUPPRESS)
return 1 target = parser.add_mutually_exclusive_group()
target.add_argument("-c", "--component", help="Component ID or dotted path in a BEdit file")
target.add_argument("-s", "--simulation", help="Simulation settings name or ID in a BEdit file")
args = parser.parse_args(arguments)
args.file = args.file_option or args.file
if args.handoff and not all((args.model_name, args.executable, args.working_directory)):
parser.error("a simulator handoff requires compiled-model arguments")
return args
return 0
def main(arguments: list[str] | None = None) -> int:
args = parse_arguments(arguments)
app = QApplication(sys.argv if arguments is None else [sys.argv[0], *arguments])
app.setOrganizationName("BEdit")
app.setApplicationName("BEdit Simulator")
window = SimulationWindow()
controller = SimulationFileController(window)
window.showMaximized()
if args.file:
try:
compiled_model = CompiledModel(args.model_name, args.executable, args.working_directory) if args.handoff else None
controller.open(args.file, component=args.component, simulation=args.simulation, backed_by_file=not args.handoff, compiled_model=compiled_model)
except (OSError, ValueError, RuntimeError) as exc:
QMessageBox.critical(window, "Could not open simulation", str(exc))
return app.exec()
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,11 +1,59 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass, field from dataclasses import dataclass
from enum import Enum
from typing import Any from typing import Any
from bedit_core.models import ComponentID
from bedit_gui.models import Simulation
@dataclass
class CompiledModel:
model_name: str
executable: str
working_directory: str
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> CompiledModel:
return cls(model_name=str(data["model_name"]), executable=str(data["executable"]), working_directory=str(data["working_directory"]))
def to_data(self) -> dict[str, Any]:
return {"model_name": self.model_name, "executable": self.executable, "working_directory": self.working_directory}
@dataclass @dataclass
class SimulationRoot: class SimulationRoot:
format_version: int = 1 format_version: int
model_file_path: str | None # The file path of the BEdit file if there is one source_document: str | None
source_document_id: str | None
component: ComponentID
component_path: str
settings_name: str | None
settings: Simulation
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationRoot:
if data.get("root_type") != "simulation_root":
raise ValueError("file does not contain a simulation root")
return cls(
format_version=int(data.get("format_version", 1)),
source_document=str(data["source_document"]) if data.get("source_document") is not None else None,
source_document_id=str(data["source_document_id"]) if data.get("source_document_id") is not None else None,
component=ComponentID(str(data["component"])),
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"]),
)
def to_data(self) -> dict[str, Any]:
return {
"root_type": "simulation_root",
"format_version": self.format_version,
"source_document": self.source_document,
"source_document_id": self.source_document_id,
"component": str(self.component),
"component_path": self.component_path,
"settings_name": self.settings_name,
"settings": self.settings.to_data(),
}

View File

@@ -51,7 +51,7 @@ class SimulationSettingsDialog(QDialog):
self.ui.simulationMethodComboBox.currentIndexChanged.connect(self._form_changed) self.ui.simulationMethodComboBox.currentIndexChanged.connect(self._form_changed)
self.ui.toleranceEdit.textEdited.connect(self._form_changed) self.ui.toleranceEdit.textEdited.connect(self._form_changed)
self._rebuild_list() self._rebuild_list(self._database.active_simulation)
def database(self) -> SimulationDatabase: def database(self) -> SimulationDatabase:
return deepcopy(self._database) return deepcopy(self._database)
@@ -68,6 +68,7 @@ class SimulationSettingsDialog(QDialog):
QMessageBox.warning(self, "Invalid simulation", "The DASSL tolerance must be greater than zero.") QMessageBox.warning(self, "Invalid simulation", "The DASSL tolerance must be greater than zero.")
return return
simulation.name = simulation.name.strip() simulation.name = simulation.name.strip()
self._database.active_simulation = self._simulation_id()
super().accept() super().accept()
def _rebuild_list(self, selected_id: SimulationID | None = None) -> None: def _rebuild_list(self, selected_id: SimulationID | None = None) -> None:
@@ -172,6 +173,8 @@ class SimulationSettingsDialog(QDialog):
row = self._simulation_ids.index(simulation_id) row = self._simulation_ids.index(simulation_id)
del self._database.simulations[simulation_id] del self._database.simulations[simulation_id]
self._simulation_ids.remove(simulation_id) self._simulation_ids.remove(simulation_id)
if self._database.active_simulation == simulation_id:
self._database.active_simulation = None
selected = self._simulation_ids[min(row, len(self._simulation_ids) - 1)] if self._simulation_ids else None selected = self._simulation_ids[min(row, len(self._simulation_ids) - 1)] if self._simulation_ids else None
self._rebuild_list(selected) self._rebuild_list(selected)

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
from PySide6.QtWidgets import QMainWindow
from bedit_gui.ui.generated.ui_simulation_window import Ui_SimulationWindow
class SimulationWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.ui = Ui_SimulationWindow()
self.ui.setupUi(self)
self.setWindowTitle("BEdit Simulator")

View File

@@ -6,6 +6,7 @@ from .openmodelica import (
ProcessResult, ProcessResult,
) )
from .results import SimulationResult, load_openmodelica_csv from .results import SimulationResult, load_openmodelica_csv
from .compile import compile_component, compile_component_sync
from .simulation import ( from .simulation import (
ModelBuildResult, ModelBuildResult,
ModelCheckResult, ModelCheckResult,
@@ -18,17 +19,19 @@ from .simulation import (
) )
__all__ = [ __all__ = [
"OpenModelicaError",
"OpenModelicaRunner",
"ProcessResult",
"ModelBuildResult", "ModelBuildResult",
"ModelCheckResult", "ModelCheckResult",
"ModelInfo", "ModelInfo",
"OpenModelicaError",
"OpenModelicaRunner",
"ProcessResult",
"Simulation", "Simulation",
"SimulationOptions", "SimulationOptions",
"SimulationProgress", "SimulationProgress",
"SimulationStateError",
"SimulationResult", "SimulationResult",
"SimulationStateError",
"compile_component",
"compile_component_sync",
"load_openmodelica_csv", "load_openmodelica_csv",
"simulate", "simulate",
] ]

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from bedit_core.models import Component
from .openmodelica import OpenModelicaRunner
from .simulation import ModelBuildResult, Simulation
async def compile_component(component: Component, working_directory: str | Path, *, omc_command: str = "omc", timeout: float | None = None) -> ModelBuildResult:
"""Compose and compile a component without creating a GUI application."""
simulation = Simulation(OpenModelicaRunner(omc_command, timeout=timeout))
await simulation.load(component)
return await simulation.compile(working_directory)
def compile_component_sync(component: Component, working_directory: str | Path, *, omc_command: str = "omc", timeout: float | None = None) -> ModelBuildResult:
"""Synchronous entry point for scripts and non-async suite applications."""
return asyncio.run(compile_component(component, working_directory, omc_command=omc_command, timeout=timeout))

View File

@@ -591,6 +591,7 @@
}, },
"simulation_database": { "simulation_database": {
"format_version": 1, "format_version": 1,
"active_simulation": "7778ca68-7a36-401b-b4aa-236a44c5e771",
"simulations": { "simulations": {
"7778ca68-7a36-401b-b4aa-236a44c5e771": { "7778ca68-7a36-401b-b4aa-236a44c5e771": {
"component": "50e6ef97-f686-4400-bc01-e5a352e8cc22", "component": "50e6ef97-f686-4400-bc01-e5a352e8cc22",