AMS-BP 0.3.1__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.
Files changed (60) hide show
  1. AMS_BP/__init__.py +1 -1
  2. AMS_BP/configio/configmodels.py +26 -12
  3. AMS_BP/configio/convertconfig.py +508 -632
  4. AMS_BP/gui/README.md +77 -0
  5. AMS_BP/gui/__init__.py +0 -0
  6. AMS_BP/gui/assets/__init__.py +0 -0
  7. AMS_BP/gui/assets/drawing.svg +107 -0
  8. AMS_BP/gui/configuration_window.py +333 -0
  9. AMS_BP/gui/help_docs/__init__.py +0 -0
  10. AMS_BP/gui/help_docs/cell_help.md +45 -0
  11. AMS_BP/gui/help_docs/channels_help.md +78 -0
  12. AMS_BP/gui/help_docs/condensate_help.md +59 -0
  13. AMS_BP/gui/help_docs/detector_help.md +57 -0
  14. AMS_BP/gui/help_docs/experiment_help.md +92 -0
  15. AMS_BP/gui/help_docs/fluorophore_help.md +128 -0
  16. AMS_BP/gui/help_docs/general_help.md +43 -0
  17. AMS_BP/gui/help_docs/global_help.md +47 -0
  18. AMS_BP/gui/help_docs/laser_help.md +76 -0
  19. AMS_BP/gui/help_docs/molecule_help.md +78 -0
  20. AMS_BP/gui/help_docs/output_help.md +5 -0
  21. AMS_BP/gui/help_docs/psf_help.md +51 -0
  22. AMS_BP/gui/help_window.py +26 -0
  23. AMS_BP/gui/logging_window.py +93 -0
  24. AMS_BP/gui/main.py +255 -0
  25. AMS_BP/gui/sim_worker.py +58 -0
  26. AMS_BP/gui/template_window_selection.py +100 -0
  27. AMS_BP/gui/widgets/__init__.py +0 -0
  28. AMS_BP/gui/widgets/camera_config_widget.py +213 -0
  29. AMS_BP/gui/widgets/cell_config_widget.py +225 -0
  30. AMS_BP/gui/widgets/channel_config_widget.py +307 -0
  31. AMS_BP/gui/widgets/condensate_config_widget.py +341 -0
  32. AMS_BP/gui/widgets/experiment_config_widget.py +259 -0
  33. AMS_BP/gui/widgets/flurophore_config_widget.py +513 -0
  34. AMS_BP/gui/widgets/general_config_widget.py +47 -0
  35. AMS_BP/gui/widgets/global_config_widget.py +142 -0
  36. AMS_BP/gui/widgets/laser_config_widget.py +255 -0
  37. AMS_BP/gui/widgets/molecule_config_widget.py +714 -0
  38. AMS_BP/gui/widgets/output_config_widget.py +61 -0
  39. AMS_BP/gui/widgets/psf_config_widget.py +128 -0
  40. AMS_BP/gui/widgets/utility_widgets/__init__.py +0 -0
  41. AMS_BP/gui/widgets/utility_widgets/scinotation_widget.py +21 -0
  42. AMS_BP/gui/widgets/utility_widgets/spectrum_widget.py +115 -0
  43. AMS_BP/logging/__init__.py +0 -0
  44. AMS_BP/logging/logutil.py +83 -0
  45. AMS_BP/logging/setup_run_directory.py +35 -0
  46. AMS_BP/{run_cell_simulation.py → main_cli.py} +27 -72
  47. AMS_BP/optics/filters/filters.py +2 -0
  48. AMS_BP/resources/template_configs/metadata_configs.json +20 -0
  49. AMS_BP/resources/template_configs/sim_config.toml +408 -0
  50. AMS_BP/resources/template_configs/twocolor_widefield_timeseries_live.toml +399 -0
  51. AMS_BP/resources/template_configs/twocolor_widefield_zstack_fixed.toml +406 -0
  52. AMS_BP/resources/template_configs/twocolor_widefield_zstack_live.toml +408 -0
  53. AMS_BP/run_sim_util.py +76 -0
  54. {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/METADATA +46 -27
  55. ams_bp-0.4.0.dist-info/RECORD +103 -0
  56. ams_bp-0.4.0.dist-info/entry_points.txt +2 -0
  57. ams_bp-0.3.1.dist-info/RECORD +0 -55
  58. ams_bp-0.3.1.dist-info/entry_points.txt +0 -2
  59. {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
  60. {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,307 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import ValidationError
4
+ from PyQt6.QtWidgets import (
5
+ QComboBox,
6
+ QDoubleSpinBox,
7
+ QFormLayout,
8
+ QLineEdit,
9
+ QMessageBox,
10
+ QPushButton,
11
+ QSpinBox,
12
+ QTabWidget,
13
+ QVBoxLayout,
14
+ QWidget,
15
+ )
16
+
17
+
18
+ class ChannelConfigWidget(QWidget):
19
+ def __init__(self):
20
+ super().__init__()
21
+ self.channel_widgets = []
22
+
23
+ layout = QVBoxLayout()
24
+ form = QFormLayout()
25
+
26
+ self.validate_button = QPushButton("Validate")
27
+ self.validate_button.clicked.connect(self.validate)
28
+ layout.addWidget(self.validate_button)
29
+
30
+ self.num_channels = QSpinBox()
31
+ self.num_channels.setRange(1, 10)
32
+ self.num_channels.setValue(2)
33
+ self.num_channels.valueChanged.connect(self.update_channel_tabs)
34
+ form.addRow("Number of Channels:", self.num_channels)
35
+
36
+ self.channel_tabs = QTabWidget()
37
+ self.update_channel_tabs()
38
+
39
+ layout.addLayout(form)
40
+ layout.addWidget(self.channel_tabs)
41
+ self.setLayout(layout)
42
+
43
+ def validate(self) -> bool:
44
+ try:
45
+ from ...configio.convertconfig import create_channels
46
+
47
+ data = self.get_data()
48
+
49
+ # Full simulation-level validation
50
+ channels = create_channels({"channels": data})
51
+
52
+ # Optional: Validate shape consistency
53
+ if len(channels.names) != channels.num_channels:
54
+ raise ValueError("Channel count does not match number of names.")
55
+
56
+ QMessageBox.information(
57
+ self, "Validation Successful", "Channel parameters are valid."
58
+ )
59
+ return True
60
+
61
+ except (ValidationError, ValueError, KeyError) as e:
62
+ QMessageBox.critical(self, "Validation Error", str(e))
63
+ return False
64
+ except Exception as e:
65
+ QMessageBox.critical(self, "Unexpected Error", str(e))
66
+ return False
67
+
68
+ def update_channel_tabs(self):
69
+ self.channel_tabs.clear()
70
+ self.channel_widgets = []
71
+
72
+ for i in range(self.num_channels.value()):
73
+ self.add_channel_tab(i)
74
+
75
+ self.channel_tabs.setCurrentIndex(0)
76
+
77
+ def add_channel_tab(self, index):
78
+ tab = QWidget()
79
+ layout = QFormLayout()
80
+
81
+ # Channel name
82
+ channel_name = QLineEdit()
83
+ layout.addRow(f"Channel {index + 1} Name:", channel_name)
84
+
85
+ # Split efficiency
86
+ split_eff = QDoubleSpinBox()
87
+ split_eff.setRange(0.0, 1.0)
88
+ split_eff.setValue(1.0)
89
+ layout.addRow(f"Channel {index + 1} Split Efficiency:", split_eff)
90
+
91
+ # Excitation filter
92
+ exc_name = QLineEdit()
93
+ exc_type = QComboBox()
94
+ exc_type.addItems(["bandpass", "allow_all"])
95
+ exc_center = QSpinBox()
96
+ exc_center.setRange(0, 10000)
97
+ exc_bandwidth = QSpinBox()
98
+ exc_bandwidth.setRange(0, 10000)
99
+ exc_trans = QDoubleSpinBox()
100
+ exc_trans.setRange(0.0, 1.0)
101
+ exc_points = QSpinBox()
102
+ exc_points.setRange(1, 10000)
103
+
104
+ layout.addRow("Excitation Name:", exc_name)
105
+ layout.addRow("Excitation Type:", exc_type)
106
+ layout.addRow("Excitation Center (nm):", exc_center)
107
+ layout.addRow("Excitation Bandwidth (nm):", exc_bandwidth)
108
+ layout.addRow("Excitation Transmission Peak:", exc_trans)
109
+ layout.addRow("Excitation Points:", exc_points)
110
+
111
+ # Emission filter
112
+ em_name = QLineEdit()
113
+ em_type = QComboBox()
114
+ em_type.addItems(["bandpass", "allow_all"])
115
+ em_center = QSpinBox()
116
+ em_center.setRange(0, 10000)
117
+ em_bandwidth = QSpinBox()
118
+ em_bandwidth.setRange(0, 10000)
119
+ em_trans = QDoubleSpinBox()
120
+ em_trans.setRange(0.0, 1.0)
121
+ em_points = QSpinBox()
122
+ em_points.setRange(1, 10000)
123
+
124
+ exc_type.currentTextChanged.connect(
125
+ lambda val: self.toggle_filter_fields(
126
+ val, exc_center, exc_bandwidth, exc_trans, exc_points
127
+ )
128
+ )
129
+ em_type.currentTextChanged.connect(
130
+ lambda val: self.toggle_filter_fields(
131
+ val, em_center, em_bandwidth, em_trans, em_points
132
+ )
133
+ )
134
+
135
+ # Call once to initialize visibility
136
+ self.toggle_filter_fields(
137
+ exc_type.currentText(), exc_center, exc_bandwidth, exc_trans, exc_points
138
+ )
139
+ self.toggle_filter_fields(
140
+ em_type.currentText(), em_center, em_bandwidth, em_trans, em_points
141
+ )
142
+
143
+ layout.addRow("Emission Name:", em_name)
144
+ layout.addRow("Emission Type:", em_type)
145
+ layout.addRow("Emission Center (nm):", em_center)
146
+ layout.addRow("Emission Bandwidth (nm):", em_bandwidth)
147
+ layout.addRow("Emission Transmission Peak:", em_trans)
148
+ layout.addRow("Emission Points:", em_points)
149
+
150
+ tab.setLayout(layout)
151
+ self.channel_tabs.addTab(tab, f"Channel {index + 1}")
152
+
153
+ # Store all widget references
154
+ widgets = {
155
+ "channel_name": channel_name,
156
+ "split_efficiency": split_eff,
157
+ "exc_name": exc_name,
158
+ "exc_type": exc_type,
159
+ "exc_center": exc_center,
160
+ "exc_bandwidth": exc_bandwidth,
161
+ "exc_trans": exc_trans,
162
+ "exc_points": exc_points,
163
+ "em_name": em_name,
164
+ "em_type": em_type,
165
+ "em_center": em_center,
166
+ "em_bandwidth": em_bandwidth,
167
+ "em_trans": em_trans,
168
+ "em_points": em_points,
169
+ }
170
+
171
+ self.channel_widgets.append(widgets)
172
+
173
+ def toggle_filter_fields(
174
+ self,
175
+ filter_type,
176
+ center_field,
177
+ bandwidth_field,
178
+ transmission_field,
179
+ points_field,
180
+ ):
181
+ is_allow_all = filter_type == "allow_all"
182
+ center_field.setEnabled(not is_allow_all)
183
+ bandwidth_field.setEnabled(not is_allow_all)
184
+ transmission_field.setEnabled(not is_allow_all)
185
+ points_field.setEnabled(not is_allow_all)
186
+
187
+ def get_data(self) -> dict:
188
+ num_channels = self.num_channels.value()
189
+ data = {
190
+ "num_of_channels": num_channels,
191
+ "channel_names": [],
192
+ "split_efficiency": [],
193
+ "filters": {},
194
+ }
195
+
196
+ for i, widgets in enumerate(self.channel_widgets):
197
+ name = widgets["channel_name"].text().strip()
198
+ if not name:
199
+ raise ValueError(f"Channel {i + 1} name is required.")
200
+
201
+ data["channel_names"].append(name)
202
+ data["split_efficiency"].append(widgets["split_efficiency"].value())
203
+
204
+ # Excitation
205
+ exc_type = widgets["exc_type"].currentText()
206
+ excitation = {
207
+ "name": widgets["exc_name"].text(),
208
+ "type": exc_type,
209
+ }
210
+ if exc_type == "bandpass":
211
+ excitation.update(
212
+ {
213
+ "center_wavelength": widgets["exc_center"].value(),
214
+ "bandwidth": widgets["exc_bandwidth"].value(),
215
+ "transmission_peak": widgets["exc_trans"].value(),
216
+ "points": widgets["exc_points"].value(),
217
+ }
218
+ )
219
+ else:
220
+ excitation["points"] = widgets["exc_points"].value()
221
+
222
+ # Emission
223
+ em_type = widgets["em_type"].currentText()
224
+ emission = {
225
+ "name": widgets["em_name"].text(),
226
+ "type": em_type,
227
+ }
228
+ if em_type == "bandpass":
229
+ emission.update(
230
+ {
231
+ "center_wavelength": widgets["em_center"].value(),
232
+ "bandwidth": widgets["em_bandwidth"].value(),
233
+ "transmission_peak": widgets["em_trans"].value(),
234
+ "points": widgets["em_points"].value(),
235
+ }
236
+ )
237
+ else:
238
+ emission["points"] = widgets["em_points"].value()
239
+
240
+ data["filters"][name] = {
241
+ "filter_set_name": f"{name.capitalize()} Filter Set",
242
+ "filter_set_description": f"Sample {name.capitalize()} filter set configuration",
243
+ "excitation": excitation,
244
+ "emission": emission,
245
+ }
246
+
247
+ return data
248
+
249
+ def set_data(self, data: dict):
250
+ num_channels = data.get("num_of_channels", 0)
251
+ self.num_channels.setValue(num_channels)
252
+ self.update_channel_tabs()
253
+
254
+ channel_names = data.get("channel_names", [])
255
+ split_efficiencies = data.get("split_efficiency", [])
256
+ filters = data.get("filters", {})
257
+
258
+ for i, widgets in enumerate(self.channel_widgets):
259
+ if i >= len(channel_names):
260
+ break
261
+
262
+ name = channel_names[i]
263
+ widgets["channel_name"].setText(name)
264
+
265
+ if i < len(split_efficiencies):
266
+ widgets["split_efficiency"].setValue(split_efficiencies[i])
267
+
268
+ filter_data = filters.get(name, {})
269
+
270
+ # Excitation
271
+ excitation = filter_data.get("excitation", {})
272
+ widgets["exc_name"].setText(excitation.get("name", ""))
273
+ widgets["exc_type"].setCurrentText(excitation.get("type", "bandpass"))
274
+
275
+ widgets["exc_center"].setValue(excitation.get("center_wavelength", 0))
276
+ widgets["exc_bandwidth"].setValue(excitation.get("bandwidth", 0))
277
+ widgets["exc_trans"].setValue(excitation.get("transmission_peak", 0.0))
278
+ widgets["exc_points"].setValue(excitation.get("points", 1))
279
+
280
+ # Emission
281
+ emission = filter_data.get("emission", {})
282
+ widgets["em_name"].setText(emission.get("name", ""))
283
+ widgets["em_type"].setCurrentText(emission.get("type", "bandpass"))
284
+
285
+ widgets["em_center"].setValue(emission.get("center_wavelength", 0))
286
+ widgets["em_bandwidth"].setValue(emission.get("bandwidth", 0))
287
+ widgets["em_trans"].setValue(emission.get("transmission_peak", 0.0))
288
+ widgets["em_points"].setValue(emission.get("points", 1))
289
+
290
+ # Apply visibility logic
291
+ self.toggle_filter_fields(
292
+ widgets["exc_type"].currentText(),
293
+ widgets["exc_center"],
294
+ widgets["exc_bandwidth"],
295
+ widgets["exc_trans"],
296
+ widgets["exc_points"],
297
+ )
298
+ self.toggle_filter_fields(
299
+ widgets["em_type"].currentText(),
300
+ widgets["em_center"],
301
+ widgets["em_bandwidth"],
302
+ widgets["em_trans"],
303
+ widgets["em_points"],
304
+ )
305
+
306
+ def get_help_path(self) -> Path:
307
+ return Path(__file__).parent.parent / "help_docs" / "channels_help.md"
@@ -0,0 +1,341 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import ValidationError
4
+ from PyQt6.QtCore import pyqtSignal
5
+ from PyQt6.QtWidgets import (
6
+ QDoubleSpinBox,
7
+ QFormLayout,
8
+ QGroupBox,
9
+ QHBoxLayout,
10
+ QLabel,
11
+ QMessageBox,
12
+ QPushButton,
13
+ QScrollArea,
14
+ QSpinBox,
15
+ QTabWidget,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+
21
+ class CondensateConfigWidget(QWidget):
22
+ # Signal to notify when molecule count changes
23
+ molecule_count_changed = pyqtSignal(int)
24
+
25
+ def __init__(self):
26
+ super().__init__()
27
+ self.molecule_tabs = []
28
+ self.condensate_widgets = []
29
+ self._updating_molecule_count = False # Flag to prevent recursion
30
+ self.setup_ui()
31
+
32
+ def setup_ui(self):
33
+ layout = QVBoxLayout()
34
+
35
+ # Instructions label
36
+ instructions = QLabel(
37
+ "Configure parameters for different molecule types and their condensates. "
38
+ "Each tab represents a different molecule type."
39
+ )
40
+ instructions.setWordWrap(True)
41
+ layout.addWidget(instructions)
42
+
43
+ # Controls for adding/removing molecule types
44
+ controls_layout = QHBoxLayout()
45
+
46
+ self.molecule_count = QSpinBox()
47
+ self.molecule_count.setRange(1, 10)
48
+ self.molecule_count.setValue(1)
49
+ self.molecule_count.valueChanged.connect(self._on_molecule_count_changed)
50
+ controls_layout.addWidget(QLabel("Number of Molecule Types:"))
51
+ controls_layout.addWidget(self.molecule_count)
52
+
53
+ layout.addLayout(controls_layout)
54
+
55
+ # Tab widget for molecule types
56
+ self.tab_widget = QTabWidget()
57
+ layout.addWidget(self.tab_widget)
58
+
59
+ # Validation button
60
+ self.validate_button = QPushButton("Validate Parameters")
61
+ self.validate_button.clicked.connect(self.validate)
62
+ layout.addWidget(self.validate_button)
63
+
64
+ # Initialize with one molecule type
65
+ self.update_molecule_count(1)
66
+
67
+ self.setLayout(layout)
68
+
69
+ def _on_molecule_count_changed(self, count):
70
+ """Handle molecule count change internally and emit signal"""
71
+ if not self._updating_molecule_count:
72
+ self.update_molecule_count(count)
73
+ # Emit signal to notify other widgets
74
+ self.molecule_count_changed.emit(count)
75
+
76
+ def set_molecule_count(self, count):
77
+ """Public method to be called by other widgets to update molecule count"""
78
+ if self.molecule_count.value() != count:
79
+ self._updating_molecule_count = True
80
+ self.molecule_count.setValue(count)
81
+ self.update_molecule_count(count)
82
+ self._updating_molecule_count = False
83
+
84
+ def update_molecule_count(self, count):
85
+ """Update the number of molecule tabs"""
86
+ current_count = self.tab_widget.count()
87
+
88
+ # Add new tabs if needed
89
+ for i in range(current_count, count):
90
+ self.add_molecule_tab(i)
91
+
92
+ # Remove excess tabs if needed
93
+ while self.tab_widget.count() > count:
94
+ self.tab_widget.removeTab(count)
95
+ if self.condensate_widgets:
96
+ self.condensate_widgets.pop()
97
+
98
+ def add_molecule_tab(self, index):
99
+ """Add a new tab for a molecule type"""
100
+ molecule_widget = QWidget()
101
+ scroll_area = QScrollArea()
102
+ scroll_area.setWidgetResizable(True)
103
+
104
+ layout = QVBoxLayout(molecule_widget)
105
+
106
+ # Controls for condensate count
107
+ condensate_controls = QHBoxLayout()
108
+ condensate_count = QSpinBox()
109
+ condensate_count.setRange(1, 20)
110
+ condensate_count.setValue(1)
111
+
112
+ condensate_controls.addWidget(QLabel("Number of Condensates:"))
113
+ condensate_controls.addWidget(condensate_count)
114
+ layout.addLayout(condensate_controls)
115
+
116
+ condensate_container = QVBoxLayout()
117
+ layout.addLayout(condensate_container)
118
+
119
+ # Add first condensate
120
+ condensate_widgets = []
121
+ self.add_condensate_group(0, condensate_widgets, condensate_container)
122
+
123
+ condensate_count.valueChanged.connect(
124
+ lambda count: self.update_condensate_count(
125
+ count, condensate_widgets, condensate_container
126
+ )
127
+ )
128
+
129
+ # Density Difference per molecule type
130
+ density_layout = QHBoxLayout()
131
+ density_layout.addWidget(QLabel("Density Difference:"))
132
+
133
+ density_spin = QDoubleSpinBox()
134
+ density_spin.setRange(0, 100)
135
+ density_spin.setValue(1.0)
136
+ density_spin.setDecimals(3)
137
+ density_layout.addWidget(density_spin)
138
+ layout.addLayout(density_layout)
139
+ self.condensate_widgets.append(
140
+ {
141
+ "condensates": condensate_widgets,
142
+ "density_widget": density_spin,
143
+ }
144
+ )
145
+ molecule_widget.setLayout(layout)
146
+ scroll_area.setWidget(molecule_widget)
147
+
148
+ self.tab_widget.addTab(scroll_area, f"Molecule Type {index + 1}")
149
+
150
+ def add_condensate_group(self, index, condensate_widgets, condensate_container):
151
+ """Add a group of widgets for a single condensate"""
152
+ group = QGroupBox(f"Condensate {index + 1}")
153
+ form = QFormLayout()
154
+
155
+ # Initial center
156
+ center_layout = QHBoxLayout()
157
+ center_x = QDoubleSpinBox()
158
+ center_y = QDoubleSpinBox()
159
+ center_z = QDoubleSpinBox()
160
+
161
+ for spinbox in [center_x, center_y, center_z]:
162
+ spinbox.setRange(-1000, 1000)
163
+ spinbox.setDecimals(2)
164
+ spinbox.setSuffix(" μm")
165
+ center_layout.addWidget(spinbox)
166
+
167
+ form.addRow("Initial Center (x, y, z):", self._make_container(center_layout))
168
+
169
+ # Initial scale
170
+ scale = QDoubleSpinBox()
171
+ scale.setRange(0.01, 100)
172
+ scale.setValue(1.0)
173
+ scale.setDecimals(2)
174
+ scale.setSuffix(" μm")
175
+ form.addRow("Initial Scale:", scale)
176
+
177
+ # Diffusion coefficient
178
+ diffusion = QDoubleSpinBox()
179
+ diffusion.setRange(0, 100)
180
+ diffusion.setValue(1.0)
181
+ diffusion.setDecimals(3)
182
+ diffusion.setSuffix(" μm²/s")
183
+ form.addRow("Diffusion Coefficient:", diffusion)
184
+
185
+ # Hurst exponent
186
+ hurst = QDoubleSpinBox()
187
+ hurst.setRange(0, 1)
188
+ hurst.setValue(0.5)
189
+ hurst.setDecimals(2)
190
+ form.addRow("Hurst Exponent:", hurst)
191
+
192
+ group.setLayout(form)
193
+
194
+ condensate_container.addWidget(group)
195
+
196
+ # Store the widgets
197
+ condensate_data = {
198
+ "center": [center_x, center_y, center_z],
199
+ "scale": scale,
200
+ "diffusion": diffusion,
201
+ "hurst": hurst,
202
+ "group": group,
203
+ }
204
+ condensate_widgets.append(condensate_data)
205
+
206
+ def update_condensate_count(self, count, condensate_widgets, condensate_container):
207
+ """Update the number of condensate groups"""
208
+ current_count = len(condensate_widgets)
209
+
210
+ # Add new condensates if needed
211
+ for i in range(current_count, count):
212
+ self.add_condensate_group(i, condensate_widgets, condensate_container)
213
+
214
+ # Remove excess condensates if needed
215
+ while len(condensate_widgets) > count:
216
+ removed = condensate_widgets.pop()
217
+ removed["group"].deleteLater()
218
+
219
+ def _make_container(self, layout):
220
+ """Helper to create a container widget for a layout"""
221
+ container = QWidget()
222
+ container.setLayout(layout)
223
+ return container
224
+
225
+ def set_data(self, data: dict):
226
+ num_molecule_types = len(data["initial_centers"])
227
+ self.set_molecule_count(num_molecule_types)
228
+
229
+ for i in range(num_molecule_types):
230
+ molecule_group = self.condensate_widgets[i]
231
+
232
+ centers = data["initial_centers"][i]
233
+ scales = data["initial_scale"][i]
234
+ diffusions = data["diffusion_coefficient"][i]
235
+ hursts = data["hurst_exponent"][i]
236
+ density = data["density_dif"][i]
237
+
238
+ num_condensates = len(centers)
239
+ molecule_group_layout = self.tab_widget.widget(i).widget().layout()
240
+ self.update_condensate_count(
241
+ num_condensates,
242
+ molecule_group["condensates"],
243
+ molecule_group_layout,
244
+ )
245
+
246
+ for j in range(num_condensates):
247
+ condensate = molecule_group["condensates"][j]
248
+ for k in range(3):
249
+ condensate["center"][k].setValue(centers[j][k])
250
+ condensate["scale"].setValue(scales[j])
251
+ condensate["diffusion"].setValue(diffusions[j])
252
+ condensate["hurst"].setValue(hursts[j])
253
+
254
+ molecule_group["density_widget"].setValue(density)
255
+
256
+ def get_data(self) -> dict:
257
+ initial_centers: list[list[list[float]]] = []
258
+ initial_scale: list[list[float]] = []
259
+ diffusion_coefficient: list[list[float]] = []
260
+ hurst_exponent: list[list[float]] = []
261
+ density_dif: list[float] = []
262
+
263
+ for molecule_group in self.condensate_widgets:
264
+ molecule_widgets = molecule_group["condensates"]
265
+ density_spin = molecule_group["density_widget"]
266
+
267
+ molecule_centers: list[list[float]] = []
268
+ molecule_scales: list[float] = []
269
+ molecule_diffusions: list[float] = []
270
+ molecule_hursts: list[float] = []
271
+
272
+ for condensate in molecule_widgets:
273
+ center = [spin.value() for spin in condensate["center"]]
274
+ molecule_centers.append(center)
275
+ molecule_scales.append(condensate["scale"].value())
276
+ molecule_diffusions.append(condensate["diffusion"].value())
277
+ molecule_hursts.append(condensate["hurst"].value())
278
+
279
+ initial_centers.append(molecule_centers or [[]])
280
+ initial_scale.append(molecule_scales or [0.0])
281
+ diffusion_coefficient.append(molecule_diffusions or [0.0])
282
+ hurst_exponent.append(molecule_hursts or [0.0])
283
+ density_dif.append(density_spin.value())
284
+
285
+ return {
286
+ "initial_centers": initial_centers,
287
+ "initial_scale": initial_scale,
288
+ "diffusion_coefficient": diffusion_coefficient,
289
+ "hurst_exponent": hurst_exponent,
290
+ "density_dif": density_dif,
291
+ }
292
+
293
+ def validate(self) -> bool:
294
+ from ...cells import create_cell
295
+ from ...configio.configmodels import CondensateParameters
296
+ from ...motion import create_condensate_dict
297
+
298
+ try:
299
+ data = self.get_data()
300
+
301
+ # Step 1: Validate with Pydantic
302
+ validated = CondensateParameters(**data)
303
+
304
+ # Step 2: Validate simulation compatibility for each condensate
305
+ # Create a dummy cell just for validation context
306
+ dummy_cell = create_cell(
307
+ "SphericalCell", {"center": [0, 0, 0], "radius": 5.0}
308
+ )
309
+
310
+ num_molecules = len(validated.initial_centers)
311
+
312
+ for i in range(num_molecules):
313
+ centers = validated.initial_centers[i]
314
+ scales = validated.initial_scale[i]
315
+ diffs = validated.diffusion_coefficient[i]
316
+ hursts = validated.hurst_exponent[i]
317
+
318
+ if not (len(centers) == len(scales) == len(diffs) == len(hursts)):
319
+ raise ValueError(f"Mismatch in lengths for molecule type {i + 1}.")
320
+ create_condensate_dict(
321
+ initial_centers=centers,
322
+ initial_scale=scales,
323
+ diffusion_coefficient=diffs,
324
+ hurst_exponent=hursts,
325
+ cell=dummy_cell,
326
+ )
327
+
328
+ QMessageBox.information(
329
+ self, "Validation Successful", "Condensate parameters are valid."
330
+ )
331
+ return True
332
+
333
+ except ValidationError as e:
334
+ QMessageBox.critical(self, "Validation Error", str(e))
335
+ return False
336
+ except Exception as e:
337
+ QMessageBox.critical(self, "Validation Error", str(e))
338
+ return False
339
+
340
+ def get_help_path(self) -> Path:
341
+ return Path(__file__).parent.parent / "help_docs" / "condensate_help.md"