AMS-BP 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AMS_BP/__init__.py +1 -1
- AMS_BP/configio/configmodels.py +32 -18
- AMS_BP/configio/convertconfig.py +508 -632
- AMS_BP/gui/README.md +77 -0
- AMS_BP/gui/__init__.py +0 -0
- AMS_BP/gui/assets/__init__.py +0 -0
- AMS_BP/gui/assets/drawing.svg +107 -0
- AMS_BP/gui/configuration_window.py +333 -0
- AMS_BP/gui/help_docs/__init__.py +0 -0
- AMS_BP/gui/help_docs/cell_help.md +45 -0
- AMS_BP/gui/help_docs/channels_help.md +78 -0
- AMS_BP/gui/help_docs/condensate_help.md +59 -0
- AMS_BP/gui/help_docs/detector_help.md +57 -0
- AMS_BP/gui/help_docs/experiment_help.md +92 -0
- AMS_BP/gui/help_docs/fluorophore_help.md +128 -0
- AMS_BP/gui/help_docs/general_help.md +43 -0
- AMS_BP/gui/help_docs/global_help.md +47 -0
- AMS_BP/gui/help_docs/laser_help.md +76 -0
- AMS_BP/gui/help_docs/molecule_help.md +78 -0
- AMS_BP/gui/help_docs/output_help.md +5 -0
- AMS_BP/gui/help_docs/psf_help.md +51 -0
- AMS_BP/gui/help_window.py +26 -0
- AMS_BP/gui/logging_window.py +93 -0
- AMS_BP/gui/main.py +255 -0
- AMS_BP/gui/sim_worker.py +58 -0
- AMS_BP/gui/template_window_selection.py +100 -0
- AMS_BP/gui/widgets/__init__.py +0 -0
- AMS_BP/gui/widgets/camera_config_widget.py +213 -0
- AMS_BP/gui/widgets/cell_config_widget.py +225 -0
- AMS_BP/gui/widgets/channel_config_widget.py +307 -0
- AMS_BP/gui/widgets/condensate_config_widget.py +341 -0
- AMS_BP/gui/widgets/experiment_config_widget.py +259 -0
- AMS_BP/gui/widgets/flurophore_config_widget.py +513 -0
- AMS_BP/gui/widgets/general_config_widget.py +47 -0
- AMS_BP/gui/widgets/global_config_widget.py +142 -0
- AMS_BP/gui/widgets/laser_config_widget.py +255 -0
- AMS_BP/gui/widgets/molecule_config_widget.py +714 -0
- AMS_BP/gui/widgets/output_config_widget.py +61 -0
- AMS_BP/gui/widgets/psf_config_widget.py +128 -0
- AMS_BP/gui/widgets/utility_widgets/__init__.py +0 -0
- AMS_BP/gui/widgets/utility_widgets/scinotation_widget.py +21 -0
- AMS_BP/gui/widgets/utility_widgets/spectrum_widget.py +115 -0
- AMS_BP/logging/__init__.py +0 -0
- AMS_BP/logging/logutil.py +83 -0
- AMS_BP/logging/setup_run_directory.py +35 -0
- AMS_BP/{run_cell_simulation.py → main_cli.py} +27 -72
- AMS_BP/optics/filters/filters.py +2 -0
- AMS_BP/resources/template_configs/metadata_configs.json +20 -0
- AMS_BP/resources/template_configs/sim_config.toml +408 -0
- AMS_BP/resources/template_configs/twocolor_widefield_timeseries_live.toml +399 -0
- AMS_BP/resources/template_configs/twocolor_widefield_zstack_fixed.toml +406 -0
- AMS_BP/resources/template_configs/twocolor_widefield_zstack_live.toml +408 -0
- AMS_BP/run_sim_util.py +76 -0
- AMS_BP/sim_microscopy.py +2 -2
- {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/METADATA +59 -34
- ams_bp-0.4.0.dist-info/RECORD +103 -0
- ams_bp-0.4.0.dist-info/entry_points.txt +2 -0
- ams_bp-0.3.0.dist-info/RECORD +0 -55
- ams_bp-0.3.0.dist-info/entry_points.txt +0 -2
- {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
- {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/licenses/LICENSE +0 -0
| @@ -0,0 +1,213 @@ | |
| 1 | 
            +
            from pathlib import Path
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from pydantic import ValidationError
         | 
| 4 | 
            +
            from PyQt6.QtWidgets import (
         | 
| 5 | 
            +
                QComboBox,
         | 
| 6 | 
            +
                QDoubleSpinBox,
         | 
| 7 | 
            +
                QFormLayout,
         | 
| 8 | 
            +
                QHBoxLayout,
         | 
| 9 | 
            +
                QMessageBox,
         | 
| 10 | 
            +
                QPushButton,
         | 
| 11 | 
            +
                QSpinBox,
         | 
| 12 | 
            +
                QVBoxLayout,
         | 
| 13 | 
            +
                QWidget,
         | 
| 14 | 
            +
            )
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            from .utility_widgets.spectrum_widget import SpectrumEditorDialog
         | 
| 17 | 
            +
             | 
| 18 | 
            +
             | 
| 19 | 
            +
            class CameraConfigWidget(QWidget):
         | 
| 20 | 
            +
                def __init__(self):
         | 
| 21 | 
            +
                    super().__init__()
         | 
| 22 | 
            +
                    layout = QVBoxLayout()
         | 
| 23 | 
            +
                    form = QFormLayout()
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    self.validate_button = QPushButton("Validate")
         | 
| 26 | 
            +
                    self.validate_button.clicked.connect(self.validate)
         | 
| 27 | 
            +
                    layout.addWidget(self.validate_button)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    # Camera type (Only "CMOS" is available)
         | 
| 30 | 
            +
                    self.camera_type = QComboBox()
         | 
| 31 | 
            +
                    self.camera_type.addItems(["CMOS"])
         | 
| 32 | 
            +
                    form.addRow("Camera Type:", self.camera_type)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    # Pixel count (width and height)
         | 
| 35 | 
            +
                    self.pixel_width = QSpinBox()
         | 
| 36 | 
            +
                    self.pixel_width.setRange(1, 10000)
         | 
| 37 | 
            +
                    self.pixel_height = QSpinBox()
         | 
| 38 | 
            +
                    self.pixel_height.setRange(1, 10000)
         | 
| 39 | 
            +
                    form.addRow(
         | 
| 40 | 
            +
                        "Pixel Count (Width x Height):",
         | 
| 41 | 
            +
                        self._hbox([self.pixel_width, self.pixel_height]),
         | 
| 42 | 
            +
                    )
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    # Pixel detector size
         | 
| 45 | 
            +
                    self.pixel_detector_size = QDoubleSpinBox()
         | 
| 46 | 
            +
                    self.pixel_detector_size.setRange(0, 100)
         | 
| 47 | 
            +
                    form.addRow("Pixel Detector Size (µm):", self.pixel_detector_size)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    # Magnification
         | 
| 50 | 
            +
                    self.magnification = QSpinBox()
         | 
| 51 | 
            +
                    self.magnification.setRange(1, 100)
         | 
| 52 | 
            +
                    form.addRow("Magnification:", self.magnification)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    # Dark current
         | 
| 55 | 
            +
                    self.dark_current = QDoubleSpinBox()
         | 
| 56 | 
            +
                    self.dark_current.setRange(0, 1000)
         | 
| 57 | 
            +
                    form.addRow("Dark Current (electrons/pixel/sec):", self.dark_current)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    # Readout noise
         | 
| 60 | 
            +
                    self.readout_noise = QDoubleSpinBox()
         | 
| 61 | 
            +
                    self.readout_noise.setRange(0, 1000)
         | 
| 62 | 
            +
                    form.addRow("Readout Noise (electrons RMS):", self.readout_noise)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    # Bit depth
         | 
| 65 | 
            +
                    self.bit_depth = QSpinBox()
         | 
| 66 | 
            +
                    self.bit_depth.setRange(8, 16)
         | 
| 67 | 
            +
                    form.addRow("Bit Depth:", self.bit_depth)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    # Sensitivity
         | 
| 70 | 
            +
                    self.sensitivity = QDoubleSpinBox()
         | 
| 71 | 
            +
                    self.sensitivity.setRange(0, 100)
         | 
| 72 | 
            +
                    form.addRow("Sensitivity (electrons/ADU):", self.sensitivity)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    # Base ADU
         | 
| 75 | 
            +
                    self.base_adu = QSpinBox()
         | 
| 76 | 
            +
                    self.base_adu.setRange(0, 65535)
         | 
| 77 | 
            +
                    form.addRow("Base ADU:", self.base_adu)
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    # Binning size
         | 
| 80 | 
            +
                    self.binning_size = QSpinBox()
         | 
| 81 | 
            +
                    self.binning_size.setRange(1, 10)
         | 
| 82 | 
            +
                    form.addRow("Binning Size:", self.binning_size)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    # Quantum efficiency
         | 
| 85 | 
            +
                    self.quantum_efficiency_button = QPushButton("Edit Quantum Efficiency")
         | 
| 86 | 
            +
                    self.quantum_efficiency_button.clicked.connect(self.edit_quantum_efficiency)
         | 
| 87 | 
            +
                    form.addRow("Quantum Efficiency:", self.quantum_efficiency_button)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    layout.addLayout(form)
         | 
| 90 | 
            +
                    self.setLayout(layout)
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def _hbox(self, widgets):
         | 
| 93 | 
            +
                    box = QHBoxLayout()
         | 
| 94 | 
            +
                    for w in widgets:
         | 
| 95 | 
            +
                        box.addWidget(w)
         | 
| 96 | 
            +
                    container = QWidget()
         | 
| 97 | 
            +
                    container.setLayout(box)
         | 
| 98 | 
            +
                    return container
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def edit_quantum_efficiency(self):
         | 
| 101 | 
            +
                    # Open the SpectrumEditorDialog for editing quantum efficiency
         | 
| 102 | 
            +
                    dialog = SpectrumEditorDialog(
         | 
| 103 | 
            +
                        parent=self,
         | 
| 104 | 
            +
                        wavelengths=[],  # Pass an empty list or preloaded wavelengths
         | 
| 105 | 
            +
                        intensities=[],  # Pass an empty list or preloaded intensities
         | 
| 106 | 
            +
                        intensity_name="QE",
         | 
| 107 | 
            +
                    )
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    if dialog.exec():
         | 
| 110 | 
            +
                        # Handle the updated quantum efficiency data (wavelengths, intensities)
         | 
| 111 | 
            +
                        self.quantum_efficiency_data = {
         | 
| 112 | 
            +
                            "wavelengths": dialog.wavelengths,
         | 
| 113 | 
            +
                            "quantum_efficiency": dialog.intensities,
         | 
| 114 | 
            +
                        }
         | 
| 115 | 
            +
                        # You can now use this data wherever needed, e.g., saving or validation
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def validate(self) -> bool:
         | 
| 118 | 
            +
                    from ...configio.convertconfig import create_quantum_efficiency_from_config
         | 
| 119 | 
            +
                    from ...optics.camera.detectors import (
         | 
| 120 | 
            +
                        CMOSDetector,
         | 
| 121 | 
            +
                    )
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    try:
         | 
| 124 | 
            +
                        data = self.get_data()
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                        # Convert QE before passing
         | 
| 127 | 
            +
                        qe = create_quantum_efficiency_from_config(data["quantum_efficiency"])
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                        # Prepare CMOS-specific parameters only (drop "type" and "quantum_efficiency")
         | 
| 130 | 
            +
                        camera_data = {
         | 
| 131 | 
            +
                            k: v for k, v in data.items() if k not in {"type", "quantum_efficiency"}
         | 
| 132 | 
            +
                        }
         | 
| 133 | 
            +
                        camera_data["pixel_size"] = (
         | 
| 134 | 
            +
                            camera_data["pixel_detector_size"] / camera_data["magnification"]
         | 
| 135 | 
            +
                        )
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                        # Attempt to construct the detector
         | 
| 138 | 
            +
                        detector = CMOSDetector(**camera_data)
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                        # If no exception, validation is successful
         | 
| 141 | 
            +
                        QMessageBox.information(
         | 
| 142 | 
            +
                            self, "Validation Successful", "Camera parameters are valid."
         | 
| 143 | 
            +
                        )
         | 
| 144 | 
            +
                        return True
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                    except ValidationError as e:
         | 
| 147 | 
            +
                        QMessageBox.critical(self, "Validation Error", str(e))
         | 
| 148 | 
            +
                        return False
         | 
| 149 | 
            +
                    except Exception as e:
         | 
| 150 | 
            +
                        QMessageBox.critical(self, "Validation Error", str(e))
         | 
| 151 | 
            +
                        return False
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                def get_data(self):
         | 
| 154 | 
            +
                    camera_data = {
         | 
| 155 | 
            +
                        "type": self.camera_type.currentText(),
         | 
| 156 | 
            +
                        "pixel_count": [self.pixel_width.value(), self.pixel_height.value()],
         | 
| 157 | 
            +
                        "pixel_detector_size": self.pixel_detector_size.value(),
         | 
| 158 | 
            +
                        "magnification": self.magnification.value(),
         | 
| 159 | 
            +
                        "dark_current": self.dark_current.value(),
         | 
| 160 | 
            +
                        "readout_noise": self.readout_noise.value(),
         | 
| 161 | 
            +
                        "bit_depth": self.bit_depth.value(),
         | 
| 162 | 
            +
                        "sensitivity": self.sensitivity.value(),
         | 
| 163 | 
            +
                        "base_adu": self.base_adu.value(),
         | 
| 164 | 
            +
                        "binning_size": self.binning_size.value(),
         | 
| 165 | 
            +
                        "quantum_efficiency": convert_dict_to_2_list(
         | 
| 166 | 
            +
                            self.quantum_efficiency_data
         | 
| 167 | 
            +
                        ),  # Use edited data here
         | 
| 168 | 
            +
                    }
         | 
| 169 | 
            +
                    return camera_data
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                def set_data(self, data: dict):
         | 
| 172 | 
            +
                    # Camera type
         | 
| 173 | 
            +
                    camera_type = data.get("type", "CMOS")
         | 
| 174 | 
            +
                    idx = self.camera_type.findText(camera_type)
         | 
| 175 | 
            +
                    if idx >= 0:
         | 
| 176 | 
            +
                        self.camera_type.setCurrentIndex(idx)
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                    # Pixel count
         | 
| 179 | 
            +
                    pixel_count = data.get("pixel_count", [512, 512])
         | 
| 180 | 
            +
                    if len(pixel_count) == 2:
         | 
| 181 | 
            +
                        self.pixel_width.setValue(pixel_count[0])
         | 
| 182 | 
            +
                        self.pixel_height.setValue(pixel_count[1])
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                    self.pixel_detector_size.setValue(data.get("pixel_detector_size", 6.5))
         | 
| 185 | 
            +
                    self.magnification.setValue(data.get("magnification", 60))
         | 
| 186 | 
            +
                    self.dark_current.setValue(data.get("dark_current", 1.0))
         | 
| 187 | 
            +
                    self.readout_noise.setValue(data.get("readout_noise", 1.0))
         | 
| 188 | 
            +
                    self.bit_depth.setValue(data.get("bit_depth", 16))
         | 
| 189 | 
            +
                    self.sensitivity.setValue(data.get("sensitivity", 0.5))
         | 
| 190 | 
            +
                    self.base_adu.setValue(data.get("base_adu", 100))
         | 
| 191 | 
            +
                    self.binning_size.setValue(data.get("binning_size", 1))
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                    # Quantum efficiency
         | 
| 194 | 
            +
                    raw_qe = data.get("quantum_efficiency", [])
         | 
| 195 | 
            +
                    wavelengths = [pair[0] for pair in raw_qe]
         | 
| 196 | 
            +
                    efficiencies = [pair[1] for pair in raw_qe]
         | 
| 197 | 
            +
                    self.quantum_efficiency_data = {
         | 
| 198 | 
            +
                        "wavelengths": wavelengths,
         | 
| 199 | 
            +
                        "quantum_efficiency": efficiencies,
         | 
| 200 | 
            +
                    }
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                def get_help_path(self) -> Path:
         | 
| 203 | 
            +
                    return Path(__file__).parent.parent / "help_docs" / "detector_help.md"
         | 
| 204 | 
            +
             | 
| 205 | 
            +
             | 
| 206 | 
            +
            def convert_dict_to_2_list(dict_c):
         | 
| 207 | 
            +
                wl_qe = []
         | 
| 208 | 
            +
                wl = dict_c["wavelengths"]
         | 
| 209 | 
            +
                qe = dict_c["quantum_efficiency"]
         | 
| 210 | 
            +
                for i in range(len(wl)):
         | 
| 211 | 
            +
                    lista = [wl[i], qe[i]]
         | 
| 212 | 
            +
                    wl_qe.append(lista)
         | 
| 213 | 
            +
                return wl_qe
         | 
| @@ -0,0 +1,225 @@ | |
| 1 | 
            +
            from pathlib import Path
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from pydantic import ValidationError
         | 
| 4 | 
            +
            from PyQt6.QtWidgets import (
         | 
| 5 | 
            +
                QComboBox,
         | 
| 6 | 
            +
                QDoubleSpinBox,
         | 
| 7 | 
            +
                QFormLayout,
         | 
| 8 | 
            +
                QHBoxLayout,
         | 
| 9 | 
            +
                QMessageBox,
         | 
| 10 | 
            +
                QPushButton,
         | 
| 11 | 
            +
                QStackedWidget,
         | 
| 12 | 
            +
                QVBoxLayout,
         | 
| 13 | 
            +
                QWidget,
         | 
| 14 | 
            +
            )
         | 
| 15 | 
            +
             | 
| 16 | 
            +
             | 
| 17 | 
            +
            class CellConfigWidget(QWidget):
         | 
| 18 | 
            +
                def __init__(self):
         | 
| 19 | 
            +
                    super().__init__()
         | 
| 20 | 
            +
                    layout = QVBoxLayout()
         | 
| 21 | 
            +
                    form = QFormLayout()
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    self.validate_button = QPushButton("Validate")
         | 
| 24 | 
            +
                    self.validate_button.clicked.connect(self.validate)
         | 
| 25 | 
            +
                    layout.addWidget(self.validate_button)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    self.cell_type = QComboBox()
         | 
| 28 | 
            +
                    self.cell_type.addItems(
         | 
| 29 | 
            +
                        ["SphericalCell", "RodCell", "RectangularCell", "OvoidCell"]
         | 
| 30 | 
            +
                    )
         | 
| 31 | 
            +
                    self.cell_type.currentIndexChanged.connect(self.update_params)
         | 
| 32 | 
            +
                    form.addRow("Cell Type:", self.cell_type)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    self.param_stack = QStackedWidget()
         | 
| 35 | 
            +
                    self.param_widgets = {
         | 
| 36 | 
            +
                        "SphericalCell": self.make_spherical_widget(),
         | 
| 37 | 
            +
                        "RodCell": self.make_rod_widget(),
         | 
| 38 | 
            +
                        "RectangularCell": self.make_rect_widget(),
         | 
| 39 | 
            +
                        "OvoidCell": self.make_ovoid_widget(),
         | 
| 40 | 
            +
                    }
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    for widget in self.param_widgets.values():
         | 
| 43 | 
            +
                        self.param_stack.addWidget(widget)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    layout.addLayout(form)
         | 
| 46 | 
            +
                    layout.addWidget(self.param_stack)
         | 
| 47 | 
            +
                    self.setLayout(layout)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def validate(self) -> bool:
         | 
| 50 | 
            +
                    from ...cells import create_cell
         | 
| 51 | 
            +
                    from ...configio.configmodels import CellParameters
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    try:
         | 
| 54 | 
            +
                        data = self.get_data()
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                        # Validate the Pydantic model first
         | 
| 57 | 
            +
                        cell_params = CellParameters(**data)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        # Try creating the cell with backend logic
         | 
| 60 | 
            +
                        create_cell(cell_params.cell_type, cell_params.params)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                        # Success
         | 
| 63 | 
            +
                        QMessageBox.information(
         | 
| 64 | 
            +
                            self, "Validation Successful", "Cell parameters are valid."
         | 
| 65 | 
            +
                        )
         | 
| 66 | 
            +
                        return True
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    except ValidationError as e:
         | 
| 69 | 
            +
                        QMessageBox.critical(self, "Validation Error", str(e))
         | 
| 70 | 
            +
                        return False
         | 
| 71 | 
            +
                    except Exception as e:
         | 
| 72 | 
            +
                        QMessageBox.critical(self, "Validation Error", str(e))
         | 
| 73 | 
            +
                        return False
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def update_params(self, index):
         | 
| 76 | 
            +
                    self.param_stack.setCurrentIndex(index)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def make_spherical_widget(self):
         | 
| 79 | 
            +
                    widget = QWidget()
         | 
| 80 | 
            +
                    form = QFormLayout()
         | 
| 81 | 
            +
                    self.center_s = [QDoubleSpinBox() for _ in range(3)]
         | 
| 82 | 
            +
                    for c in self.center_s:
         | 
| 83 | 
            +
                        c.setRange(0, 1e5)
         | 
| 84 | 
            +
                    self.radius_s = QDoubleSpinBox()
         | 
| 85 | 
            +
                    self.radius_s.setRange(0, 1e4)
         | 
| 86 | 
            +
                    form.addRow("Center (x,y,z):", self._hbox(self.center_s))
         | 
| 87 | 
            +
                    form.addRow("Radius:", self.radius_s)
         | 
| 88 | 
            +
                    widget.setLayout(form)
         | 
| 89 | 
            +
                    return widget
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def make_rod_widget(self):
         | 
| 92 | 
            +
                    widget = QWidget()
         | 
| 93 | 
            +
                    form = QFormLayout()
         | 
| 94 | 
            +
                    self.center_r = [QDoubleSpinBox() for _ in range(3)]
         | 
| 95 | 
            +
                    self.direction_r = [QDoubleSpinBox() for _ in range(3)]
         | 
| 96 | 
            +
                    self.height_r = QDoubleSpinBox()
         | 
| 97 | 
            +
                    self.radius_r = QDoubleSpinBox()
         | 
| 98 | 
            +
                    for w in self.center_r + self.direction_r:
         | 
| 99 | 
            +
                        w.setRange(0, 1e5)
         | 
| 100 | 
            +
                    form.addRow("Center:", self._hbox(self.center_r))
         | 
| 101 | 
            +
                    form.addRow("Direction:", self._hbox(self.direction_r))
         | 
| 102 | 
            +
                    form.addRow("Height:", self.height_r)
         | 
| 103 | 
            +
                    form.addRow("Radius:", self.radius_r)
         | 
| 104 | 
            +
                    widget.setLayout(form)
         | 
| 105 | 
            +
                    return widget
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def make_rect_widget(self):
         | 
| 108 | 
            +
                    widget = QWidget()
         | 
| 109 | 
            +
                    form = QFormLayout()
         | 
| 110 | 
            +
                    self.bounds = [QDoubleSpinBox() for _ in range(6)]
         | 
| 111 | 
            +
                    for b in self.bounds:
         | 
| 112 | 
            +
                        b.setRange(0, 1e5)
         | 
| 113 | 
            +
                    form.addRow("Bounds [xmin,xmax,ymin,ymax,zmin,zmax]:", self._hbox(self.bounds))
         | 
| 114 | 
            +
                    widget.setLayout(form)
         | 
| 115 | 
            +
                    return widget
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def make_ovoid_widget(self):
         | 
| 118 | 
            +
                    widget = QWidget()
         | 
| 119 | 
            +
                    form = QFormLayout()
         | 
| 120 | 
            +
                    self.center_o = [QDoubleSpinBox() for _ in range(3)]
         | 
| 121 | 
            +
                    self.xradius = QDoubleSpinBox()
         | 
| 122 | 
            +
                    self.yradius = QDoubleSpinBox()
         | 
| 123 | 
            +
                    self.zradius = QDoubleSpinBox()
         | 
| 124 | 
            +
                    for c in self.center_o:
         | 
| 125 | 
            +
                        c.setRange(0, 1e5)
         | 
| 126 | 
            +
                    form.addRow("Center:", self._hbox(self.center_o))
         | 
| 127 | 
            +
                    form.addRow("X Radius:", self.xradius)
         | 
| 128 | 
            +
                    form.addRow("Y Radius:", self.yradius)
         | 
| 129 | 
            +
                    form.addRow("Z Radius:", self.zradius)
         | 
| 130 | 
            +
                    widget.setLayout(form)
         | 
| 131 | 
            +
                    return widget
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def _hbox(self, widgets):
         | 
| 134 | 
            +
                    box = QHBoxLayout()
         | 
| 135 | 
            +
                    for w in widgets:
         | 
| 136 | 
            +
                        box.addWidget(w)
         | 
| 137 | 
            +
                    container = QWidget()
         | 
| 138 | 
            +
                    container.setLayout(box)
         | 
| 139 | 
            +
                    return container
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def get_data(self):
         | 
| 142 | 
            +
                    ctype = self.cell_type.currentText()
         | 
| 143 | 
            +
                    if ctype == "SphericalCell":
         | 
| 144 | 
            +
                        return {
         | 
| 145 | 
            +
                            "cell_type": ctype,
         | 
| 146 | 
            +
                            "params": {
         | 
| 147 | 
            +
                                "center": [s.value() for s in self.center_s],
         | 
| 148 | 
            +
                                "radius": self.radius_s.value(),
         | 
| 149 | 
            +
                            },
         | 
| 150 | 
            +
                        }
         | 
| 151 | 
            +
                    elif ctype == "RodCell":
         | 
| 152 | 
            +
                        return {
         | 
| 153 | 
            +
                            "cell_type": ctype,
         | 
| 154 | 
            +
                            "params": {
         | 
| 155 | 
            +
                                "center": [s.value() for s in self.center_r],
         | 
| 156 | 
            +
                                "direction": [s.value() for s in self.direction_r],
         | 
| 157 | 
            +
                                "height": self.height_r.value(),
         | 
| 158 | 
            +
                                "radius": self.radius_r.value(),
         | 
| 159 | 
            +
                            },
         | 
| 160 | 
            +
                        }
         | 
| 161 | 
            +
                    elif ctype == "RectangularCell":
         | 
| 162 | 
            +
                        return {
         | 
| 163 | 
            +
                            "cell_type": ctype,
         | 
| 164 | 
            +
                            "params": {"bounds": [b.value() for b in self.bounds]},
         | 
| 165 | 
            +
                        }
         | 
| 166 | 
            +
                    elif ctype == "OvoidCell":
         | 
| 167 | 
            +
                        return {
         | 
| 168 | 
            +
                            "cell_type": ctype,
         | 
| 169 | 
            +
                            "params": {
         | 
| 170 | 
            +
                                "center": [s.value() for s in self.center_o],
         | 
| 171 | 
            +
                                "xradius": self.xradius.value(),
         | 
| 172 | 
            +
                                "yradius": self.yradius.value(),
         | 
| 173 | 
            +
                                "zradius": self.zradius.value(),
         | 
| 174 | 
            +
                            },
         | 
| 175 | 
            +
                        }
         | 
| 176 | 
            +
                    else:
         | 
| 177 | 
            +
                        return {"cell_type": ctype, "params": {}}
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                def set_data(self, data: dict):
         | 
| 180 | 
            +
                    try:
         | 
| 181 | 
            +
                        cell_type = data.get("cell_type", "RodCell")
         | 
| 182 | 
            +
                        self.cell_type.setCurrentText(cell_type)
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                        params = data.get("params", {})
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                        if cell_type == "SphericalCell":
         | 
| 187 | 
            +
                            center = params.get("center", [0, 0, 0])
         | 
| 188 | 
            +
                            radius = params.get("radius", 1.0)
         | 
| 189 | 
            +
                            for i, val in enumerate(center):
         | 
| 190 | 
            +
                                self.center_s[i].setValue(val)
         | 
| 191 | 
            +
                            self.radius_s.setValue(radius)
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                        elif cell_type == "RodCell":
         | 
| 194 | 
            +
                            center = params.get("center", [0, 0, 0])
         | 
| 195 | 
            +
                            direction = params.get("direction", [1, 0, 0])
         | 
| 196 | 
            +
                            height = params.get("height", 1.0)
         | 
| 197 | 
            +
                            radius = params.get("radius", 0.5)
         | 
| 198 | 
            +
                            for i, val in enumerate(center):
         | 
| 199 | 
            +
                                self.center_r[i].setValue(val)
         | 
| 200 | 
            +
                            for i, val in enumerate(direction):
         | 
| 201 | 
            +
                                self.direction_r[i].setValue(val)
         | 
| 202 | 
            +
                            self.height_r.setValue(height)
         | 
| 203 | 
            +
                            self.radius_r.setValue(radius)
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                        elif cell_type == "RectangularCell":
         | 
| 206 | 
            +
                            bounds = params.get("bounds", [0, 10, 0, 10, 0, 10])
         | 
| 207 | 
            +
                            for i, val in enumerate(bounds):
         | 
| 208 | 
            +
                                self.bounds[i].setValue(val)
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                        elif cell_type == "OvoidCell":
         | 
| 211 | 
            +
                            center = params.get("center", [0, 0, 0])
         | 
| 212 | 
            +
                            xr = params.get("xradius", 1.0)
         | 
| 213 | 
            +
                            yr = params.get("yradius", 1.0)
         | 
| 214 | 
            +
                            zr = params.get("zradius", 1.0)
         | 
| 215 | 
            +
                            for i, val in enumerate(center):
         | 
| 216 | 
            +
                                self.center_o[i].setValue(val)
         | 
| 217 | 
            +
                            self.xradius.setValue(xr)
         | 
| 218 | 
            +
                            self.yradius.setValue(yr)
         | 
| 219 | 
            +
                            self.zradius.setValue(zr)
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                    except Exception as e:
         | 
| 222 | 
            +
                        print(f"[CellConfigWidget] Failed to load data: {e}")
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                def get_help_path(self) -> Path:
         | 
| 225 | 
            +
                    return Path(__file__).parent.parent / "help_docs" / "cell_help.md"
         |