Simulation settings window

This commit is contained in:
Joppe Blondel
2026-07-30 21:27:15 +02:00
parent a03c6d624e
commit b3aabae5e8
9 changed files with 372 additions and 20 deletions
@@ -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))