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(self._database.active_simulation) 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() self._database.active_simulation = self._simulation_id() 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) 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 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))