Source code for mosaic.dts.equilibration_dialog

"""
Dialog functions used throughout the GUI.

Copyright (c) 2024-2026 European Molecular Biology Laboratory

Author: Valentin Maurer <valentin.maurer@embl-hamburg.de>
"""

from pathlib import Path

from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
    QDialog,
    QVBoxLayout,
    QGridLayout,
    QLabel,
    QGroupBox,
    QPushButton,
    QWidget,
)

from ..icons import icon
from ..stylesheets import Colors
from ..widgets import DialogFooter, PathSelector, MosaicMessageBox
from ..widgets.settings import create_setting_widget, get_widget_value, set_widget_value


def _make_setting(parameter, default, min_val=0, description=""):
    """Create a setting dict compatible with create_setting_widget."""
    setting_type = "float" if isinstance(default, float) else "number"
    return {
        "parameter": parameter,
        "label": parameter.replace("_", " ").title(),
        "type": setting_type,
        "default": default,
        "min": min_val,
        "description": description,
    }


[docs] class MeshEquilibrationDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Mesh Equilibration") self.setMinimumWidth(350) self._settings = [ _make_setting( "average_edge_length", 40.0, 0, "Average edge length of mesh." ), _make_setting("lower_bound", 35.0, 0, "Minimum edge length of mesh (lc1)."), _make_setting("upper_bound", 45.0, 0, "Maximum edge length of mesh (lc0)."), _make_setting("steps", 5000, 0, "Number of minimization steps."), _make_setting("kappa_b", 300.0, 0, "Bending energy coefficient (kappa_b)."), _make_setting( "kappa_a", 1e6, 0, "Area conservation coefficient (kappa_a)." ), _make_setting( "kappa_v", 1e6, 0, "Volume conservation coefficient (kappa_v)." ), _make_setting("kappa_c", 0.0, 0, "Curvature energy coefficient (kappa_c)."), _make_setting("kappa_t", 1e5, 0, "Edge tension coefficient (kappa_t)."), _make_setting( "kappa_r", 1e3, 0, "Surface repulsion coefficient (kappa_r)." ), _make_setting("volume_fraction", 1.1, 0, "Fraction VN/V0."), _make_setting("area_fraction", 1.1, 0, "Fraction AN/A0."), _make_setting( "scaling_lower", 1.0, 0, "Lower bound for rescaled mesh edge length." ), ] self._widgets = {} self._setup_ui() def _add_row(self, grid, row, left, right=None): """Add one or two label:widget pairs to a grid row.""" label = QLabel(left["label"]) label.setToolTip(left.get("description", "")) widget = create_setting_widget(left) self._widgets[left["parameter"]] = widget grid.addWidget(label, row, 0) grid.addWidget(widget, row, 1) if right is not None: label_r = QLabel(right["label"]) label_r.setToolTip(right.get("description", "")) widget_r = create_setting_widget(right) self._widgets[right["parameter"]] = widget_r grid.addWidget(label_r, row, 2) grid.addWidget(widget_r, row, 3) def _setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(16, 16, 16, 16) layout.setSpacing(12) basic_group = QGroupBox("Basic") basic = QGridLayout(basic_group) basic.setColumnStretch(1, 1) basic.setColumnStretch(3, 1) basic.setHorizontalSpacing(12) basic.setVerticalSpacing(8) out_label = QLabel("Output Directory") out_label.setToolTip("Directory where the equilibrated mesh will be written") self.output_selector = PathSelector( placeholder="Directory for equilibrated mesh", mode="directory", ) basic.addWidget(out_label, 0, 0) basic.addWidget(self.output_selector, 0, 1, 1, 3) self._add_row(basic, 1, self._settings[0], self._settings[3]) self._widgets["average_edge_length"].valueChanged.connect(self._update_bounds) self._add_row(basic, 2, self._settings[1], self._settings[2]) layout.addWidget(basic_group) self._toggle_btn = QPushButton(" Advanced") self._toggle_btn.setIcon(icon("ph.caret-right", role="muted")) self._toggle_btn.setCheckable(True) self._toggle_btn.setAutoDefault(False) self._toggle_btn.setDefault(False) self._toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor) self._toggle_btn.setStyleSheet( f""" QPushButton {{ background: transparent; border: none; border-bottom: 1px solid {Colors.BORDER_DARK}; border-radius: 0px; text-align: left; padding: 6px 0px; color: {Colors.TEXT_MUTED}; }} QPushButton:hover {{ color: {Colors.TEXT_SECONDARY}; }} """ ) self._toggle_btn.toggled.connect(self._toggle_advanced) layout.addWidget(self._toggle_btn) self._advanced = QWidget() adv = QVBoxLayout(self._advanced) adv.setContentsMargins(0, 4, 0, 0) adv.setSpacing(10) energy_group = QGroupBox("Energy Coefficients") energy = QGridLayout(energy_group) energy.setColumnStretch(1, 1) energy.setColumnStretch(3, 1) energy.setHorizontalSpacing(12) energy.setVerticalSpacing(8) row = 0 for i in range(4, 10, 2): self._add_row(energy, row, self._settings[i], self._settings[i + 1]) row += 1 adv.addWidget(energy_group) constraints_group = QGroupBox("Constraints") constraints = QGridLayout(constraints_group) constraints.setColumnStretch(1, 1) constraints.setColumnStretch(3, 1) constraints.setHorizontalSpacing(12) constraints.setVerticalSpacing(8) self._add_row(constraints, 0, self._settings[10], self._settings[11]) self._add_row(constraints, 1, self._settings[12]) adv.addWidget(constraints_group) self._advanced.setVisible(False) layout.addWidget(self._advanced) layout.addStretch() self.footer = DialogFooter(dialog=self, margin=(0, 12, 0, 0)) self.footer.accept_button.setEnabled(False) layout.addWidget(self.footer) self.output_selector.path_input.textChanged.connect(self._update_accept_state) def _toggle_advanced(self, expanded): """Toggle visibility of advanced parameters.""" icon_name = "ph.caret-down" if expanded else "ph.caret-right" self._toggle_btn.setIcon(icon(icon_name, role="muted")) self._advanced.setVisible(expanded) self.adjustSize() def _update_bounds(self, value): """Update lower and upper bounds when edge length changes.""" try: val = float(value) set_widget_value(self._widgets["lower_bound"], val * 0.75) set_widget_value(self._widgets["upper_bound"], val * 1.25) except (ValueError, KeyError): pass def _update_accept_state(self, *_): """Enable the accept button only when a directory is set.""" path = self.output_selector.get_path().strip() self.footer.accept_button.setEnabled(bool(path))
[docs] def accept(self): """Ensure the output directory exists (creating it if needed).""" path = self.output_selector.get_path().strip() try: Path(path).mkdir(parents=True, exist_ok=True) except OSError as exc: MosaicMessageBox.warning( self, "Invalid Output Directory", f"Cannot use '{path}':\n{exc}", ) return return super().accept()
[docs] def get_output_directory(self) -> str: """Return the selected output directory.""" return self.output_selector.get_path().strip()
[docs] def get_parameters(self): """Get the current parameters.""" return { param: get_widget_value(widget) for param, widget in self._widgets.items() }