199 lines
9.3 KiB
Python
199 lines
9.3 KiB
Python
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))
|