Source code for gui_widgets.inputForm

from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QFileDialog, QPushButton, QDoubleSpinBox, QComboBox, QGroupBox, QFormLayout, QMessageBox, QCheckBox
)

from traits.api import TraitError
from calculation.aperture import Aperture
from io_tools.load_from_json import load_from_json

[docs] class InputForm(QWidget): """ User interface component for inputting resonator configuration parameters. This form includes user inputs for: - Geometry (cylinder or cuboid) - Aperture (form, amount, dimensions, damping) - Boundary conditions (inner/outer endings) - Environmental conditions (temperature, humidity) - Loading configuration files (JSON) Attributes: result_view (ResultView): Reference to the result view for accessing selected axes. """ def __init__(self) -> None: """ Initialize the input form with all required widgets, tooltips, default values, layout logic, and initial visibility for geometry and aperture configuration. Args: None Returns: None """ super().__init__() self.controller = None self.main_layout = QVBoxLayout() # === 1. GEOMETRY === self.group_geometry = QGroupBox("Geometry") geo_layout = QFormLayout() self.combo_shape = QComboBox() self.combo_shape.addItems(["Cylinder", "Cuboid"]) self.combo_shape.setToolTip("Select the shape of the resonator: cylindrical or cuboid.") self.combo_shape.currentTextChanged.connect(self.update_inputs) geo_layout.addRow("Form:", self.combo_shape) self.spin_r = QDoubleSpinBox(); self.spin_r.setPrefix("r = "); self.spin_r.setValue(0.1); self.spin_r.setSuffix(" m"); self.spin_r.setRange(0, 1e6) self.spin_h = QDoubleSpinBox(); self.spin_h.setPrefix("h = "); self.spin_h.setValue(0.2); self.spin_h.setSuffix(" m"); self.spin_h.setRange(0, 1e6) self.spin_l = QDoubleSpinBox(); self.spin_l.setPrefix("l = "); self.spin_l.setValue(0.2); self.spin_l.setSuffix(" m"); self.spin_l.setRange(0, 1e6) self.spin_b = QDoubleSpinBox(); self.spin_b.setPrefix("b = "); self.spin_b.setValue(0.1); self.spin_b.setSuffix(" m"); self.spin_b.setRange(0, 1e6) self.spin_t = QDoubleSpinBox(); self.spin_t.setPrefix("h = "); self.spin_t.setValue(0.05); self.spin_t.setSuffix(" m"); self.spin_t.setRange(0, 1e6) # Tool-Tipps: self.spin_r.setToolTip("Radius of the cylindrical resonator (m).") self.spin_h.setToolTip("Height of the cylindrical resonator (m).") self.spin_l.setToolTip("Length of the cuboid resonator (m).") self.spin_b.setToolTip("Width of the cuboid resonator (m).") self.spin_t.setToolTip("Height (or thickness) of the cuboid resonator (m).") geo_layout.addRow(self.spin_r) geo_layout.addRow(self.spin_h) geo_layout.addRow(self.spin_l) geo_layout.addRow(self.spin_b) geo_layout.addRow(self.spin_t) self.group_geometry.setLayout(geo_layout) self.main_layout.addWidget(self.group_geometry) # === 2. Aperture === self.group_opening = QGroupBox("Aperture Properties") opening_layout = QFormLayout() self.add_damping_cb = QCheckBox("additional damping") self.spin_xi = QDoubleSpinBox(); self.spin_xi.setPrefix("xi = "); self.spin_xi.setValue(0); self.spin_xi.setSuffix(" Pa·s/m"); self.spin_xi.setRange(0, 1e6) self.spin_xi.setDisabled(True) self.add_damping_cb.stateChanged.connect(self.toggle_damping_input) opening_layout.addRow(self.add_damping_cb) opening_layout.addRow(self.spin_xi) self.combo_aperture_form = self.add_enum_combobox("Aperture form:", "form", opening_layout) self.combo_aperture_form.currentTextChanged.connect(self.update_opening) self.spin_n = QDoubleSpinBox(); self.spin_n.setPrefix("n = "); self.spin_n.setValue(1); self.spin_n.setDecimals(0); self.spin_n.setRange(0, 1e6) self.spin_L = QDoubleSpinBox(); self.spin_L.setPrefix("L = "); self.spin_L.setValue(0.02); self.spin_L.setSuffix(" m"); self.spin_L.setRange(0, 1e6) self.spin_r_open = QDoubleSpinBox(); self.spin_r_open.setPrefix("r_open = "); self.spin_r_open.setValue(0.005); self.spin_r_open.setSuffix(" m"); self.spin_r_open.setRange(0, 1e6) self.spin_b_slit = QDoubleSpinBox(); self.spin_b_slit.setPrefix("b_slit = "); self.spin_b_slit.setValue(0.01); self.spin_b_slit.setSuffix(" m"); self.spin_b_slit.setRange(0, 1e6) self.spin_l_slit = QDoubleSpinBox(); self.spin_l_slit.setPrefix("l_slit = "); self.spin_l_slit.setValue(0.05); self.spin_l_slit.setSuffix(" m"); self.spin_l_slit.setRange(0, 1e6) # Tool-Tipps: self.add_damping_cb.setToolTip("Enable additional acoustic damping in the aperture.") self.spin_xi.setToolTip("Damping coefficient xi (Pa·s/m). Only used if additional damping is active.") self.combo_aperture_form.setToolTip("Geometric form of the aperture used in acoustic calculations (e.g. 'tube', 'slit').") self.spin_n.setToolTip("Number of openings in the resonator wall.") self.spin_L.setToolTip("Physical length of the aperture, often equal to the wall thickness (m).") self.spin_r_open.setToolTip("Radius of the circular aperture (m).") self.spin_b_slit.setToolTip("Width of the slit aperture (m).") self.spin_l_slit.setToolTip("Length of the slit aperture (m).") opening_layout.addRow(self.spin_n) opening_layout.addRow(self.spin_L) opening_layout.addRow(self.spin_r_open) opening_layout.addRow(self.spin_b_slit) opening_layout.addRow(self.spin_l_slit) self.group_opening.setLayout(opening_layout) self.main_layout.addWidget(self.group_opening) # === 3. ENDING === self.group_endings = QGroupBox("Boundary Configuration") endings_layout = QFormLayout() self.combo_inner_ending = self.add_enum_combobox("Inner End:", "inner_ending", endings_layout) self.combo_outer_ending = self.add_enum_combobox("Outer End:", "outer_ending", endings_layout) # Tool-Tipps: self.combo_inner_ending.setToolTip("Type of inner boundary of the aperture (e.g. open or flanged).") self.combo_outer_ending.setToolTip("Type of outer boundary of the aperture (e.g. open or flanged).") self.group_endings.setLayout(endings_layout) self.main_layout.addWidget(self.group_endings) # === 4. ENVIRONMENTAL CONDITIONS === self.group_conditions = QGroupBox("Environmental Conditions") cond_layout = QFormLayout() self.spin_T = QDoubleSpinBox() self.spin_T.setPrefix("T = "); self.spin_T.setValue(20); self.spin_T.setSuffix(" °C") self.spin_T.setMinimum(-273.15); self.spin_T.setMaximum(1e6); self.spin_T.setDecimals(2) self.spin_H = QDoubleSpinBox() self.spin_H.setPrefix("H = "); self.spin_H.setValue(50); self.spin_H.setSuffix(" %") self.spin_H.setRange(0.0, 100.0); self.spin_H.setDecimals(2) # Tool-Tipps: self.spin_T.setToolTip("Ambient temperature (°C).") self.spin_H.setToolTip("Relative humidity (%).") cond_layout.addRow(self.spin_T) cond_layout.addRow(self.spin_H) self.group_conditions.setLayout(cond_layout) self.main_layout.addWidget(self.group_conditions) # === 5. Load Configurations JSON Format === self.button_load_json = QPushButton("Load Configuration (JSON)") self.button_load_json.clicked.connect(self.load_from_json_file) self.main_layout.addWidget(self.button_load_json) self.setLayout(self.main_layout) self.update_inputs("Cylinder") self.update_opening("tube")
[docs] def toggle_damping_input(self, state: int) -> None: """ Enable or disable the damping coefficient input based on the checkbox state. Args: state (int): Qt.Checked (2) oder Qt.Unchecked (0) Returns: None """ self.spin_xi.setEnabled(state == 2) # 2 = Qt.Checked
[docs] def add_enum_combobox(self, label: str, trait_name: str, layout) -> QComboBox: """ Create a QComboBox filled with enum values from a given trait. Args: label (str): Label shown next to the combobox. trait_name (str): Name of the trait to extract enum values from. layout (QFormLayout): Layout to which the row will be added. Returns: QComboBox: The created and populated combobox. """ combo = QComboBox() values = Aperture().trait(trait_name).handler.values combo.addItems(values) layout.addRow(label, combo) return combo
[docs] def update_inputs(self, shape: str) -> None: """ Show or hide input fields depending on selected geometry shape. Args: shape (str): The selected shape ('Cylinder' or 'Cuboid'). Returns: None """ for w in [self.spin_r, self.spin_h, self.spin_l, self.spin_b, self.spin_t]: w.setVisible(False) if shape == "Cylinder": self.spin_r.setVisible(True); self.spin_h.setVisible(True) else: self.spin_l.setVisible(True); self.spin_b.setVisible(True); self.spin_t.setVisible(True)
[docs] def update_opening(self, shape: str) -> None: """ Show or hide aperture dimension inputs depending on aperture type. Args: shape (str): The selected aperture form ('tube' or 'slit'). Returns: None """ for w in [self.spin_r_open, self.spin_b_slit, self.spin_l_slit]: w.setVisible(False) if shape == "tube": self.spin_r_open.setVisible(True) else: self.spin_b_slit.setVisible(True); self.spin_l_slit.setVisible(True)
[docs] def get_inputs(self) -> dict | None: """ Collect and validate all user inputs to build the simulation input dictionary. Args: None Returns: dict | None: A dictionary with geometry, aperture, environmental, and plot parameters, or None if input validation failed. """ # --- Geometry --- if self.combo_shape.currentText() == "Cylinder": geometry = { 'shape': 'cylinder', 'radius': self.spin_r.value(), 'height': self.spin_h.value() } else: geometry = { 'shape': 'cuboid', 'l': self.spin_l.value(), 'b': self.spin_b.value(), 'h': self.spin_t.value() } # --- Aperture --- form = self.combo_aperture_form.currentText() aperture_kwargs = { 'form': form, 'amount': int(self.spin_n.value()), 'length': self.spin_L.value(), 'inner_ending': self.combo_inner_ending.currentText(), 'outer_ending': self.combo_outer_ending.currentText() } if form == 'tube': aperture_kwargs['radius'] = self.spin_r_open.value() elif form == 'slit': aperture_kwargs['width'] = self.spin_b_slit.value() aperture_kwargs['height'] = self.spin_l_slit.value() aperture_kwargs['additional_dampening'] = self.add_damping_cb.isChecked() # Nur setzen, wenn aktiviert if self.add_damping_cb.isChecked(): aperture_kwargs['xi'] = self.spin_xi.value() # --- Environmental Conditions --- conditions = { 'temperature': self.spin_T.value(), 'humidity': self.spin_H.value() / 100 # Umwandlung von Prozent in Dezimal } # --- Plot Settings --- plot_settings = { 'x_axis': self.result_view.combo_x.currentText(), 'y_axis': self.result_view.combo_y.currentText() } # Optional: Aperture-Objekt mit Traits validieren try: aperture = Aperture(**aperture_kwargs) except TraitError as e: QMessageBox.critical(self, "Input Error", f"Invalid aperture configuration:\n{e}") return None return { 'geometry': geometry, 'aperture': aperture, 'conditions': conditions, 'plot': plot_settings }
[docs] def load_from_json_file(self) -> None: """ Load simulation parameters from a JSON file and update all form fields accordingly. Args: None Returns: None """ path, _ = QFileDialog.getOpenFileName(self, "Load JSON", "", "JSON Files (*.json)") if not path: return try: simulation = load_from_json(path) resonator = simulation.resonator aperture = resonator.aperture geometry = resonator.geometry medium = simulation.sim_params.medium # --- Geometry --- if geometry.form == 'cylinder': self.combo_shape.setCurrentText("Cylinder") self.spin_r.setValue(geometry.radius) self.spin_h.setValue(geometry.height) elif geometry.form == 'cuboid': self.combo_shape.setCurrentText("Cuboid") self.spin_l.setValue(geometry.x) self.spin_b.setValue(geometry.y) self.spin_t.setValue(geometry.z) # --- Aperture --- self.combo_aperture_form.setCurrentText(aperture.form) self.spin_n.setValue(aperture.amount) self.spin_L.setValue(aperture.length) self.combo_inner_ending.setCurrentText(aperture.inner_ending) self.combo_outer_ending.setCurrentText(aperture.outer_ending) if aperture.form == "tube": self.spin_r_open.setValue(aperture.radius) elif aperture.form == "slit": self.spin_b_slit.setValue(aperture.width) self.spin_l_slit.setValue(aperture.height) # --- Additional Damping --- self.add_damping_cb.setChecked(aperture.additional_dampening) self.spin_xi.setEnabled(aperture.additional_dampening) if aperture.additional_dampening: self.spin_xi.setValue(aperture.xi) # --- Conditions --- self.spin_T.setValue(medium.temperature_celsius) self.spin_H.setValue(medium.rel_humidity * 100) if self.controller: self.controller.calculate_and_show() QMessageBox.information(self, "Success", "Settings loaded successfully.") except Exception as e: QMessageBox.critical(self, "Error", f"Could not load settings:\n{e}")