Simulation settings window
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user