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.
- AMS_BP/__init__.py +1 -1
- AMS_BP/configio/configmodels.py +26 -12
- 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-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/METADATA +46 -27
- 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.1.dist-info/RECORD +0 -55
- ams_bp-0.3.1.dist-info/entry_points.txt +0 -2
- {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
- {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"
|