Compare commits

...

2 Commits

Author SHA1 Message Date
b3aabae5e8 Simulation settings window 2026-07-30 21:27:15 +02:00
a03c6d624e Start of simulation settings window 2026-07-30 21:12:36 +02:00
11 changed files with 738 additions and 14 deletions

View File

@@ -9,6 +9,7 @@ from bedit_gui.controllers.clipboard_controller import ClipboardController, Docu
from bedit_gui.controllers.document_controller import DocumentController 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.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
@@ -46,6 +47,7 @@ def main() -> int:
LogController(window, settings.log_level) LogController(window, settings.log_level)
DocumentController(document, window) DocumentController(document, window)
SettingsController(window, settings) SettingsController(window, settings)
SimulationSettingsController(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,21 @@
from __future__ import annotations
from copy import deepcopy
from PySide6.QtGui import QUndoCommand
from bedit_gui.models import SimulationDatabase
class ChangeSimulationDatabaseCommand(QUndoCommand):
def __init__(self, document: object, database: SimulationDatabase) -> None:
super().__init__("Edit simulation settings")
self.document = document
self.old_database = document.stored_simulation_database()
self.new_database = deepcopy(database)
def redo(self) -> None:
self.document._set_simulation_database(self.new_database)
def undo(self) -> None:
self.document._set_simulation_database(self.old_database)

View File

@@ -2,7 +2,8 @@ from collections.abc import Callable
from functools import partial from functools import partial
from typing import Protocol from typing import Protocol
from PySide6.QtCore import QObject, QPoint, QSize, Qt from PySide6.QtCore import QEvent, QObject, QPoint, QSize, Qt
from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
from bedit_core.models import Component, ComponentID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID from bedit_core.models import Component, ComponentID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID
@@ -79,15 +80,20 @@ class DocumentTreeController(QObject):
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
window.ui.documentTree.setColumnWidth(1, 56) window.ui.documentTree.setColumnWidth(1, 56)
window.ui.documentTree.setContextMenuPolicy( self._tree_viewport = window.ui.documentTree.viewport()
Qt.ContextMenuPolicy.CustomContextMenu self._tree_viewport.installEventFilter(self)
)
window.ui.documentTree.customContextMenuRequested.connect(
self._show_context_menu
)
self._on_document_changed(document.model) self._on_document_changed(document.model)
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
if watched is self._tree_viewport and event.type() in (QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease):
assert isinstance(event, QMouseEvent)
if event.button() == Qt.MouseButton.RightButton:
if event.type() == QEvent.Type.MouseButtonRelease:
self._show_context_menu(event.position().toPoint())
return True
return super().eventFilter(watched, event)
def _on_document_changed(self, model: CoreDocument) -> None: def _on_document_changed(self, model: CoreDocument) -> None:
"""Rebuild the tree whenever New/Open replaces the core document.""" """Rebuild the tree whenever New/Open replaces the core document."""
self._show_equation_component(None) self._show_equation_component(None)

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from collections.abc import Callable
from typing import Protocol
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QDialog
from bedit_core.models import Component, ComponentID, GraphImplementation
from bedit_gui.documents import Document
from bedit_gui.models import SimulationDatabase
from bedit_gui.views.dialogs.simulation_settings_dialog import SimulationSettingsDialog
from bedit_gui.views.main_window import MainWindow
class SimulationSettingsDialogLike(Protocol):
def exec(self) -> int: ...
def database(self) -> SimulationDatabase: ...
SimulationSettingsDialogFactory = Callable[[SimulationDatabase, list[tuple[ComponentID, str]], MainWindow], SimulationSettingsDialogLike]
class SimulationSettingsController(QObject):
def __init__(self, document: Document, window: MainWindow, dialog_factory: SimulationSettingsDialogFactory = SimulationSettingsDialog) -> None:
super().__init__(window)
self.document = document
self.window = window
self.dialog_factory = dialog_factory
window.ui.actionSimulation_Settings.triggered.connect(self.open_settings)
def open_settings(self) -> None:
dialog = self.dialog_factory(self.document.simulation_database(), self._components(), self.window)
if dialog.exec() == QDialog.DialogCode.Accepted:
self.document.change_simulation_database(dialog.database())
def _components(self) -> list[tuple[ComponentID, str]]:
components: list[tuple[ComponentID, str]] = []
def collect(items: dict[ComponentID, Component], path: tuple[str, ...] = ()) -> None:
for component_id, component in items.items():
component_path = (*path, component.name)
components.append((component_id, ".".join(component_path)))
if isinstance(component.implementation, GraphImplementation):
collect(component.implementation.graph.components, component_path)
collect(self.document.model.root)
return components

View File

@@ -14,8 +14,9 @@ from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand,
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
from bedit_gui.commands.rename_component_command import RenameComponentCommand 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.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 from bedit_gui.models import Icon, IconDatabase, SimulationDatabase
from bedit_gui.services import document_files from bedit_gui.services import document_files
@@ -27,6 +28,7 @@ class Document(QObject):
modified_changed = Signal(bool) modified_changed = Signal(bool)
icon_changed = Signal(object, object) icon_changed = Signal(object, object)
equation_text_changed = Signal(object, str) equation_text_changed = Signal(object, str)
simulation_database_changed = Signal(object)
def __init__(self, parent: QObject | None = None) -> None: def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent) super().__init__(parent)
@@ -122,6 +124,44 @@ class Document(QObject):
def change_icon(self, component_id: ComponentID, icon: Icon) -> None: def change_icon(self, component_id: ComponentID, icon: Icon) -> None:
self.undo_stack.push(ChangeIconCommand(self, component_id, icon)) self.undo_stack.push(ChangeIconCommand(self, component_id, icon))
def stored_simulation_database(self) -> SimulationDatabase | None:
database = self._simulation_database(False)
return deepcopy(database) if database is not None else None
def simulation_database(self) -> SimulationDatabase:
return self.stored_simulation_database() or SimulationDatabase()
def change_simulation_database(self, database: SimulationDatabase) -> None:
if database != self.stored_simulation_database():
self.undo_stack.push(ChangeSimulationDatabaseCommand(self, database))
def _set_simulation_database(self, database: SimulationDatabase | None) -> None:
if database is None:
if self.model.metadata is not None:
self.model.metadata.pop("simulation_database", None)
else:
if self.model.metadata is None:
self.model.metadata = {}
self.model.metadata["simulation_database"] = deepcopy(database)
self.simulation_database_changed.emit(self.stored_simulation_database())
def _simulation_database(self, create: bool) -> SimulationDatabase | None:
metadata = self.model.metadata
value = metadata.get("simulation_database") if metadata is not None else None
if isinstance(value, dict):
value = SimulationDatabase.from_data(value)
metadata["simulation_database"] = value
if isinstance(value, SimulationDatabase):
return value
if not create:
return None
if metadata is None:
metadata = {}
self.model.metadata = metadata
database = SimulationDatabase()
metadata["simulation_database"] = database
return database
def _set_component_icon(self, component_id: ComponentID, icon: Icon | None) -> None: def _set_component_icon(self, component_id: ComponentID, icon: Icon | None) -> None:
if icon is None: if icon is None:
database = self._icon_database(False) database = self._icon_database(False)

View File

@@ -1,4 +1,3 @@
"""GUI document data stored inside the core document metadata field."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@@ -12,6 +11,9 @@ from bedit_core.models import ComponentID, ID, PortID
class ShapeID(ID): class ShapeID(ID):
pass pass
class SimulationID(ID):
pass
@dataclass @dataclass
class Shape: class Shape:
layer: int layer: int
@@ -32,7 +34,6 @@ class Shape:
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {"layer": self.layer, "type": self.type, "pos": list(self.pos)} return {"layer": self.layer, "type": self.type, "pos": list(self.pos)}
class LineType(Enum): class LineType(Enum):
NONE = "none" NONE = "none"
SOLID = "solid" SOLID = "solid"
@@ -40,7 +41,6 @@ class LineType(Enum):
DOTTED = "dotted" DOTTED = "dotted"
DASH_DOT = "dash_dot" DASH_DOT = "dash_dot"
@dataclass @dataclass
class Line(Shape): class Line(Shape):
type: str = field(init=False, default="line") type: str = field(init=False, default="line")
@@ -58,7 +58,6 @@ class Line(Shape):
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "end": list(self.end), "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color} return {**super().to_data(), "end": list(self.end), "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color}
@dataclass @dataclass
class Rectangle(Shape): class Rectangle(Shape):
type: str = field(init=False, default="rectangle") type: str = field(init=False, default="rectangle")
@@ -111,7 +110,6 @@ class Icon:
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()}, "port_positions": {str(key): list(position) for key, position in self.port_positions.items()}} return {"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()}, "port_positions": {str(key): list(position) for key, position in self.port_positions.items()}}
@dataclass @dataclass
class IconDatabase: class IconDatabase:
format_version: int = 1 format_version: int = 1
@@ -124,3 +122,65 @@ class IconDatabase:
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}} return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}}
class SimulationMethod(Enum):
DASSL = "dassl"
@dataclass
class Simulation:
component: ComponentID
name: str
start_time: float
duration: float
use_timed_steps: bool
number_of_steps: int
step_size: float
method: SimulationMethod
dassl_tolerance: float
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Simulation:
component = data.get("component")
use_timed_steps = data.get("use_timed_steps", False)
if isinstance(use_timed_steps, str):
use_timed_steps = use_timed_steps.lower() == "true"
return cls(
component=ComponentID(str(component)) if component else ComponentID(),
name=str(data.get("name", "")),
start_time=float(data.get("start_time", 0.0)),
duration=float(data.get("duration", 1.0)),
use_timed_steps=bool(use_timed_steps),
number_of_steps=int(data.get("number_of_steps", 500)),
step_size=float(data.get("step_size", 0.001)),
method=SimulationMethod(data.get("method", "dassl")),
dassl_tolerance=float(data.get("dassl_tolerance", 1e-6)),
)
def to_data(self) -> dict[str, Any]:
return {
"component": str(self.component),
"name": self.name,
"start_time": self.start_time,
"duration": self.duration,
"use_timed_steps": self.use_timed_steps,
"number_of_steps": self.number_of_steps,
"step_size": self.step_size,
"method": self.method.value,
"dassl_tolerance": self.dassl_tolerance,
}
@dataclass
class SimulationDatabase:
format_version: int = 1
simulations: dict[SimulationID, Simulation] = field(default_factory=dict)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationDatabase:
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)
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()}}

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from bedit_core.models import Document from bedit_core.models import Document
from bedit_core.serialization import load as load_document from bedit_core.serialization import load as load_document
from bedit_core.serialization import save as save_document from bedit_core.serialization import save as save_document
from bedit_gui.models import IconDatabase from bedit_gui.models import IconDatabase, SimulationDatabase
def load(path: str | Path) -> Document: def load(path: str | Path) -> Document:
@@ -14,6 +14,8 @@ def load(path: str | Path) -> Document:
document = load_document(path) document = load_document(path)
if document.metadata is not None and isinstance(document.metadata.get("icon_database"), dict): if document.metadata is not None and isinstance(document.metadata.get("icon_database"), dict):
document.metadata["icon_database"] = IconDatabase.from_data(document.metadata["icon_database"]) document.metadata["icon_database"] = IconDatabase.from_data(document.metadata["icon_database"])
if document.metadata is not None and isinstance(document.metadata.get("simulation_database"), dict):
document.metadata["simulation_database"] = SimulationDatabase.from_data(document.metadata["simulation_database"])
return document return document
@@ -22,4 +24,6 @@ def save(document: Document, path: str | Path) -> None:
saved_document = deepcopy(document) saved_document = deepcopy(document)
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("icon_database"), IconDatabase): if saved_document.metadata is not None and isinstance(saved_document.metadata.get("icon_database"), IconDatabase):
saved_document.metadata["icon_database"] = saved_document.metadata["icon_database"].to_data() saved_document.metadata["icon_database"] = saved_document.metadata["icon_database"].to_data()
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("simulation_database"), SimulationDatabase):
saved_document.metadata["simulation_database"] = saved_document.metadata["simulation_database"].to_data()
save_document(saved_document, path) save_document(saved_document, path)

View File

@@ -74,9 +74,20 @@
</property> </property>
<addaction name="actionAbout_QT"/> <addaction name="actionAbout_QT"/>
</widget> </widget>
<widget class="QMenu" name="menuSimulation">
<property name="title">
<string>Simulation</string>
</property>
<addaction name="actionSimulation_Settings"/>
<addaction name="actionEdit_Parameters"/>
<addaction name="separator"/>
<addaction name="actionCompile_Model"/>
<addaction name="actionOpen_Simulation_Window"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuEdit"/> <addaction name="menuEdit"/>
<addaction name="menuView"/> <addaction name="menuView"/>
<addaction name="menuSimulation"/>
<addaction name="menuHelp"/> <addaction name="menuHelp"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/> <widget class="QStatusBar" name="statusbar"/>
@@ -153,6 +164,21 @@
</layout> </layout>
</widget> </widget>
</widget> </widget>
<widget class="QToolBar" name="simToolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionSimulation_Settings"/>
<addaction name="actionEdit_Parameters"/>
<addaction name="actionCompile_Model"/>
<addaction name="actionOpen_Simulation_Window"/>
</widget>
<action name="actionOpen_File"> <action name="actionOpen_File">
<property name="icon"> <property name="icon">
<iconset resource="../../resources/resources.qrc"> <iconset resource="../../resources/resources.qrc">
@@ -386,6 +412,54 @@
<enum>QAction::MenuRole::NoRole</enum> <enum>QAction::MenuRole::NoRole</enum>
</property> </property>
</action> </action>
<action name="actionSimulation_Settings">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset>
</property>
<property name="text">
<string>Simulation Settings</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionEdit_Parameters">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/view-form-table.png</normaloff>:/icons/icons/view-form-table.png</iconset>
</property>
<property name="text">
<string>Edit Parameters</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionCompile_Model">
<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 Model</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionOpen_Simulation_Window">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<property name="text">
<string>Open Simulation Window</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="../../resources/resources.qrc"/> <include location="../../resources/resources.qrc"/>

View File

@@ -0,0 +1,258 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>562</width>
<height>451</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="simulationListLayout">
<item>
<widget class="QListWidget" name="simulationList"/>
</item>
<item>
<layout class="QHBoxLayout" name="simulationButtonLayout">
<item>
<widget class="QPushButton" name="addSimulationButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeSimulationButton">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="basicForm">
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="componentLabel">
<property name="text">
<string>Component:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nameEdit"/>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="componentCompoBox"/>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="timeForm">
<item row="0" column="0">
<widget class="QLabel" name="startTimeLabel">
<property name="text">
<string>Start time:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="simLengthLabel">
<property name="text">
<string>Simulation length:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="startTimeSpinBox">
<property name="suffix">
<string> s</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="simLengthSpinBox">
<property name="suffix">
<string> s</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="stepSizeButton">
<property name="text">
<string>Step size</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="nrOfStepsButton">
<property name="text">
<string>Number of steps</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="stepSizeLabel">
<property name="text">
<string>Step size:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="stepLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="stepSizeSpinBox">
<property name="suffix">
<string> s</string>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="value">
<double>0.001000000000000</double>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="nrOfStepsSpinBox"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="nrOfStepsLabel">
<property name="text">
<string>Number of steps:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="simulationMethodComboBox"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="simMethodLabel">
<property name="text">
<string>Simulation method:</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="dasslForm">
<item row="0" column="1">
<widget class="QLineEdit" name="toleranceEdit">
<property name="text">
<string>1e-06</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="toleranceLabel">
<property name="text">
<string>Tolerance:</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,195 @@
from __future__ import annotations
from copy import deepcopy
from PySide6.QtCore import Qt
from PySide6.QtGui import QDoubleValidator
from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox, QWidget
from bedit_core.models import ComponentID
from bedit_gui.models import Simulation, SimulationDatabase, SimulationID, SimulationMethod
from bedit_gui.ui.generated.ui_simulation_settings import Ui_Dialog
class SimulationSettingsDialog(QDialog):
"""Editor for the detached simulation database of a document."""
def __init__(self, database: SimulationDatabase, components: list[tuple[ComponentID, str]], parent: QWidget | None = None) -> None:
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.setWindowTitle("Simulation Settings")
self._database = deepcopy(database)
self._simulation_ids = list(self._database.simulations)
self._components = components
self._loading = False
self.ui.startTimeSpinBox.setRange(-1e12, 1e12)
self.ui.simLengthSpinBox.setRange(0.0, 1e12)
self.ui.stepSizeSpinBox.setRange(1e-9, 1e12)
self.ui.nrOfStepsSpinBox.setRange(1, 1_000_000_000)
tolerance_validator = QDoubleValidator(0.0, 1e12, 16, self)
tolerance_validator.setNotation(QDoubleValidator.Notation.ScientificNotation)
self.ui.toleranceEdit.setValidator(tolerance_validator)
for component_id, path in components:
self.ui.componentCompoBox.addItem(path, str(component_id))
for method in SimulationMethod:
self.ui.simulationMethodComboBox.addItem(method.value.upper(), method.value)
self.ui.simulationList.currentRowChanged.connect(self._selection_changed)
self.ui.addSimulationButton.clicked.connect(self._add_simulation)
self.ui.removeSimulationButton.clicked.connect(self._remove_simulation)
self.ui.nameEdit.textEdited.connect(self._form_changed)
self.ui.componentCompoBox.currentIndexChanged.connect(self._form_changed)
self.ui.startTimeSpinBox.valueChanged.connect(self._form_changed)
self.ui.simLengthSpinBox.valueChanged.connect(self._form_changed)
self.ui.stepSizeButton.toggled.connect(self._form_changed)
self.ui.nrOfStepsButton.toggled.connect(self._form_changed)
self.ui.stepSizeSpinBox.valueChanged.connect(self._form_changed)
self.ui.nrOfStepsSpinBox.valueChanged.connect(self._form_changed)
self.ui.simulationMethodComboBox.currentIndexChanged.connect(self._form_changed)
self.ui.toleranceEdit.textEdited.connect(self._form_changed)
self._rebuild_list()
def database(self) -> SimulationDatabase:
return deepcopy(self._database)
def accept(self) -> None:
for simulation in self._database.simulations.values():
if not simulation.name.strip():
QMessageBox.warning(self, "Invalid simulation", "Every simulation must have a name.")
return
if simulation.component not in {component_id for component_id, _ in self._components}:
QMessageBox.warning(self, "Invalid simulation", f"Select an existing component for {simulation.name}.")
return
if simulation.dassl_tolerance <= 0:
QMessageBox.warning(self, "Invalid simulation", "The DASSL tolerance must be greater than zero.")
return
simulation.name = simulation.name.strip()
super().accept()
def _rebuild_list(self, selected_id: SimulationID | None = None) -> None:
self.ui.simulationList.clear()
for simulation_id in self._simulation_ids:
item = QListWidgetItem(self._database.simulations[simulation_id].name)
item.setData(Qt.ItemDataRole.UserRole, simulation_id)
self.ui.simulationList.addItem(item)
if self._simulation_ids:
if selected_id not in self._database.simulations:
selected_id = self._simulation_ids[0]
self.ui.simulationList.setCurrentRow(self._simulation_ids.index(selected_id))
else:
self._set_editor_enabled(False)
def _selection_changed(self, row: int) -> None:
simulation_id = self._simulation_id(row)
self._set_editor_enabled(simulation_id is not None)
self.ui.removeSimulationButton.setEnabled(simulation_id is not None)
if simulation_id is not None:
self._load_simulation(self._database.simulations[simulation_id])
def _simulation_id(self, row: int | None = None) -> SimulationID | None:
if row is None:
row = self.ui.simulationList.currentRow()
if row < 0 or row >= len(self._simulation_ids):
return None
return self._simulation_ids[row]
def _load_simulation(self, simulation: Simulation) -> None:
self._loading = True
self.ui.nameEdit.setText(simulation.name)
component_index = self.ui.componentCompoBox.findData(str(simulation.component))
if component_index < 0:
self.ui.componentCompoBox.addItem(f"Missing component ({simulation.component})", str(simulation.component))
component_index = self.ui.componentCompoBox.count() - 1
self.ui.componentCompoBox.setCurrentIndex(component_index)
self.ui.startTimeSpinBox.setValue(simulation.start_time)
self.ui.simLengthSpinBox.setValue(simulation.duration)
self.ui.stepSizeButton.setChecked(simulation.use_timed_steps)
self.ui.nrOfStepsButton.setChecked(not simulation.use_timed_steps)
self.ui.stepSizeSpinBox.setValue(simulation.step_size)
self.ui.nrOfStepsSpinBox.setValue(simulation.number_of_steps)
self.ui.simulationMethodComboBox.setCurrentIndex(self.ui.simulationMethodComboBox.findData(simulation.method.value))
self.ui.toleranceEdit.setText(str(simulation.dassl_tolerance))
self._loading = False
self._update_step_inputs()
def _form_changed(self, *_args: object) -> None:
if self._loading:
return
simulation_id = self._simulation_id()
if simulation_id is None:
return
self._update_step_inputs()
simulation = self._database.simulations[simulation_id]
try:
tolerance = float(self.ui.toleranceEdit.text())
except ValueError:
tolerance = simulation.dassl_tolerance
component_data = self.ui.componentCompoBox.currentData()
component = ComponentID(component_data) if isinstance(component_data, str) and component_data else simulation.component
method_data = self.ui.simulationMethodComboBox.currentData()
method = SimulationMethod(method_data) if isinstance(method_data, str) else simulation.method
self._database.simulations[simulation_id] = Simulation(
component=component,
name=self.ui.nameEdit.text(),
start_time=self.ui.startTimeSpinBox.value(),
duration=self.ui.simLengthSpinBox.value(),
use_timed_steps=self.ui.stepSizeButton.isChecked(),
number_of_steps=self.ui.nrOfStepsSpinBox.value(),
step_size=self.ui.stepSizeSpinBox.value(),
method=method,
dassl_tolerance=tolerance,
)
self.ui.simulationList.item(self.ui.simulationList.currentRow()).setText(self.ui.nameEdit.text())
def _add_simulation(self) -> None:
if not self._components:
return
simulation_id = SimulationID()
simulation = Simulation(
component=self._components[0][0],
name=self._unique_name("Simulation"),
start_time=0.0,
duration=1.0,
use_timed_steps=False,
number_of_steps=500,
step_size=0.001,
method=SimulationMethod.DASSL,
dassl_tolerance=1e-6,
)
self._database.simulations[simulation_id] = simulation
self._simulation_ids.append(simulation_id)
self._rebuild_list(simulation_id)
def _remove_simulation(self) -> None:
simulation_id = self._simulation_id()
if simulation_id is None:
return
row = self._simulation_ids.index(simulation_id)
del self._database.simulations[simulation_id]
self._simulation_ids.remove(simulation_id)
selected = self._simulation_ids[min(row, len(self._simulation_ids) - 1)] if self._simulation_ids else None
self._rebuild_list(selected)
def _unique_name(self, base: str) -> str:
names = {simulation.name for simulation in self._database.simulations.values()}
if base not in names:
return base
index = 2
while f"{base} {index}" in names:
index += 1
return f"{base} {index}"
def _update_step_inputs(self) -> None:
timed = self.ui.stepSizeButton.isChecked()
self.ui.stepSizeSpinBox.setEnabled(timed)
self.ui.nrOfStepsSpinBox.setEnabled(not timed)
def _set_editor_enabled(self, enabled: bool) -> None:
self.ui.frame.setEnabled(enabled)
self.ui.removeSimulationButton.setEnabled(enabled)
self.ui.addSimulationButton.setEnabled(bool(self._components))

View File

@@ -588,6 +588,22 @@
} }
} }
} }
},
"simulation_database": {
"format_version": 1,
"simulations": {
"7778ca68-7a36-401b-b4aa-236a44c5e771": {
"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
}
}
} }
} }
} }