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,513 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from PyQt6.QtCore import pyqtSignal
|
4
|
+
from PyQt6.QtWidgets import (
|
5
|
+
QComboBox,
|
6
|
+
QFormLayout,
|
7
|
+
QGroupBox,
|
8
|
+
QHBoxLayout,
|
9
|
+
QLabel,
|
10
|
+
QLineEdit,
|
11
|
+
QMessageBox,
|
12
|
+
QPushButton,
|
13
|
+
QScrollArea,
|
14
|
+
QSpinBox,
|
15
|
+
QTabWidget,
|
16
|
+
QVBoxLayout,
|
17
|
+
QWidget,
|
18
|
+
)
|
19
|
+
|
20
|
+
from .utility_widgets.scinotation_widget import scientific_input_field
|
21
|
+
from .utility_widgets.spectrum_widget import SpectrumEditorDialog
|
22
|
+
|
23
|
+
|
24
|
+
class FluorophoreConfigWidget(QWidget):
|
25
|
+
# Signal to notify when molecule count changes
|
26
|
+
mfluorophore_count_changed = pyqtSignal(int)
|
27
|
+
|
28
|
+
def __init__(self):
|
29
|
+
super().__init__()
|
30
|
+
self.fluorophore_widgets = []
|
31
|
+
self._updating_count = False
|
32
|
+
self.setup_ui()
|
33
|
+
|
34
|
+
def setup_ui(self):
|
35
|
+
layout = QVBoxLayout()
|
36
|
+
|
37
|
+
instructions = QLabel(
|
38
|
+
"Configure fluorophores and their respective states and transitions."
|
39
|
+
)
|
40
|
+
instructions.setWordWrap(True)
|
41
|
+
layout.addWidget(instructions)
|
42
|
+
|
43
|
+
# Controls for fluorophore count
|
44
|
+
controls_layout = QHBoxLayout()
|
45
|
+
self.fluorophore_count = QSpinBox()
|
46
|
+
self.fluorophore_count.setRange(1, 10)
|
47
|
+
self.fluorophore_count.setValue(1)
|
48
|
+
self.fluorophore_count.valueChanged.connect(self._on_fluorophore_count_changed)
|
49
|
+
|
50
|
+
controls_layout.addWidget(QLabel("Number of Fluorophores:"))
|
51
|
+
controls_layout.addWidget(self.fluorophore_count)
|
52
|
+
layout.addLayout(controls_layout)
|
53
|
+
|
54
|
+
self.tab_widget = QTabWidget()
|
55
|
+
layout.addWidget(self.tab_widget)
|
56
|
+
|
57
|
+
self.validate_button = QPushButton("Validate Parameters")
|
58
|
+
self.validate_button.clicked.connect(self.validate)
|
59
|
+
layout.addWidget(self.validate_button)
|
60
|
+
|
61
|
+
self.setLayout(layout)
|
62
|
+
self.update_fluorophore_count(1)
|
63
|
+
|
64
|
+
def _on_fluorophore_count_changed(self, count):
|
65
|
+
if not self._updating_count:
|
66
|
+
self.update_fluorophore_count(count)
|
67
|
+
# Emit signal to notify other widgets
|
68
|
+
self.mfluorophore_count_changed.emit(count)
|
69
|
+
|
70
|
+
def set_mfluorophore_count(self, count):
|
71
|
+
"""Public method to be called by other widgets to update fluorophore molecule count"""
|
72
|
+
if self.fluorophore_count.value() != count:
|
73
|
+
self._updating_count = True
|
74
|
+
self.fluorophore_count.setValue(count)
|
75
|
+
self.update_fluorophore_count(count)
|
76
|
+
self._updating_count = False
|
77
|
+
|
78
|
+
def update_fluorophore_count(self, count):
|
79
|
+
current_count = self.tab_widget.count()
|
80
|
+
|
81
|
+
for i in range(current_count, count):
|
82
|
+
self.add_fluorophore_tab(i)
|
83
|
+
|
84
|
+
while self.tab_widget.count() > count:
|
85
|
+
self.tab_widget.removeTab(count)
|
86
|
+
if self.fluorophore_widgets:
|
87
|
+
self.fluorophore_widgets.pop()
|
88
|
+
|
89
|
+
def add_fluorophore_tab(self, index):
|
90
|
+
fluor_widget = QWidget()
|
91
|
+
layout = QVBoxLayout(fluor_widget)
|
92
|
+
|
93
|
+
form = QFormLayout()
|
94
|
+
|
95
|
+
name_input = QLineEdit()
|
96
|
+
form.addRow("Name:", name_input)
|
97
|
+
|
98
|
+
initial_state_input = QLineEdit()
|
99
|
+
form.addRow("Initial State:", initial_state_input)
|
100
|
+
|
101
|
+
layout.addLayout(form)
|
102
|
+
|
103
|
+
# === STATES ===
|
104
|
+
states_box = QGroupBox("States")
|
105
|
+
states_layout = QVBoxLayout()
|
106
|
+
states_controls = QHBoxLayout()
|
107
|
+
|
108
|
+
add_state_btn = QPushButton("Add State")
|
109
|
+
remove_state_btn = QPushButton("Remove Last State")
|
110
|
+
states_controls.addWidget(add_state_btn)
|
111
|
+
states_controls.addWidget(remove_state_btn)
|
112
|
+
|
113
|
+
states_layout.addLayout(states_controls)
|
114
|
+
|
115
|
+
state_container = QVBoxLayout()
|
116
|
+
states_layout.addLayout(state_container)
|
117
|
+
states_box.setLayout(states_layout)
|
118
|
+
|
119
|
+
# === TRANSITIONS ===
|
120
|
+
transitions_box = QGroupBox("Transitions")
|
121
|
+
transitions_layout = QVBoxLayout()
|
122
|
+
transitions_controls = QHBoxLayout()
|
123
|
+
|
124
|
+
add_transition_btn = QPushButton("Add Transition")
|
125
|
+
remove_transition_btn = QPushButton("Remove Last Transition")
|
126
|
+
transitions_controls.addWidget(add_transition_btn)
|
127
|
+
transitions_controls.addWidget(remove_transition_btn)
|
128
|
+
|
129
|
+
transitions_layout.addLayout(transitions_controls)
|
130
|
+
|
131
|
+
transition_container = QVBoxLayout()
|
132
|
+
transitions_layout.addLayout(transition_container)
|
133
|
+
transitions_box.setLayout(transitions_layout)
|
134
|
+
|
135
|
+
# Add to main layout
|
136
|
+
layout.addWidget(states_box)
|
137
|
+
layout.addWidget(transitions_box)
|
138
|
+
|
139
|
+
scroll = QScrollArea()
|
140
|
+
scroll.setWidgetResizable(True)
|
141
|
+
scroll.setWidget(fluor_widget)
|
142
|
+
|
143
|
+
self.tab_widget.addTab(scroll, f"Fluorophore {index + 1}")
|
144
|
+
|
145
|
+
# Tracking widget refs
|
146
|
+
widget_refs = {
|
147
|
+
"name": name_input,
|
148
|
+
"initial_state": initial_state_input,
|
149
|
+
"state_container": state_container,
|
150
|
+
"transition_container": transition_container,
|
151
|
+
"states": [],
|
152
|
+
"transitions": [],
|
153
|
+
}
|
154
|
+
|
155
|
+
self.fluorophore_widgets.append(widget_refs)
|
156
|
+
|
157
|
+
# Initial state and transition
|
158
|
+
self.add_state_group(widget_refs)
|
159
|
+
self.add_transition_group(widget_refs)
|
160
|
+
|
161
|
+
# Button logic
|
162
|
+
add_state_btn.clicked.connect(lambda: self.add_state_group(widget_refs))
|
163
|
+
remove_state_btn.clicked.connect(
|
164
|
+
lambda: self.remove_last_group(widget_refs["states"])
|
165
|
+
)
|
166
|
+
|
167
|
+
add_transition_btn.clicked.connect(
|
168
|
+
lambda: self.add_transition_group(widget_refs)
|
169
|
+
)
|
170
|
+
remove_transition_btn.clicked.connect(
|
171
|
+
lambda: self.remove_last_group(widget_refs["transitions"])
|
172
|
+
)
|
173
|
+
|
174
|
+
def add_state_group(self, widget_refs):
|
175
|
+
layout = widget_refs["state_container"]
|
176
|
+
group = QGroupBox(f"State {len(widget_refs['states']) + 1}")
|
177
|
+
form = QFormLayout()
|
178
|
+
|
179
|
+
name = QLineEdit()
|
180
|
+
state_type = QComboBox()
|
181
|
+
state_type.addItems(["fluorescent", "dark", "bleached"])
|
182
|
+
form.addRow("Name:", name)
|
183
|
+
form.addRow("State Type:", state_type)
|
184
|
+
|
185
|
+
# === Parameter container ===
|
186
|
+
param_container = QWidget()
|
187
|
+
param_form = QFormLayout(param_container)
|
188
|
+
|
189
|
+
quantum_yield = scientific_input_field(0, 1, default=0.8)
|
190
|
+
param_form.addRow("Quantum Yield:", quantum_yield)
|
191
|
+
|
192
|
+
extinction = scientific_input_field(-1e12, 1e12, default=1e5)
|
193
|
+
param_form.addRow("Extinction Coefficient:", extinction)
|
194
|
+
|
195
|
+
lifetime = scientific_input_field(1e-10, 1, default=1e-8)
|
196
|
+
param_form.addRow("Fluorescent Lifetime:", lifetime)
|
197
|
+
|
198
|
+
excitation_spectrum_button = QPushButton("Edit Spectrum")
|
199
|
+
param_form.addRow("Excitation Spectrum:", excitation_spectrum_button)
|
200
|
+
excitation_spectrum_data = {"wavelengths": [], "intensities": []}
|
201
|
+
excitation_spectrum_button.clicked.connect(
|
202
|
+
lambda: self.edit_spectrum(excitation_spectrum_data)
|
203
|
+
)
|
204
|
+
|
205
|
+
emission_spectrum_button = QPushButton("Edit Spectrum")
|
206
|
+
param_form.addRow("Emission Spectrum:", emission_spectrum_button)
|
207
|
+
emission_spectrum_data = {"wavelengths": [], "intensities": []}
|
208
|
+
emission_spectrum_button.clicked.connect(
|
209
|
+
lambda: self.edit_spectrum(emission_spectrum_data)
|
210
|
+
)
|
211
|
+
|
212
|
+
# Add conditional param container to main form
|
213
|
+
form.addRow(param_container)
|
214
|
+
|
215
|
+
group.setLayout(form)
|
216
|
+
layout.addWidget(group)
|
217
|
+
|
218
|
+
# === Visibility logic ===
|
219
|
+
def update_param_visibility(state: str):
|
220
|
+
param_container.setVisible(state == "fluorescent")
|
221
|
+
|
222
|
+
state_type.currentTextChanged.connect(update_param_visibility)
|
223
|
+
update_param_visibility(state_type.currentText())
|
224
|
+
|
225
|
+
# === Store all widgets ===
|
226
|
+
widget_refs["states"].append(
|
227
|
+
{
|
228
|
+
"group": group,
|
229
|
+
"name": name,
|
230
|
+
"type": state_type,
|
231
|
+
"param_container": param_container,
|
232
|
+
"quantum_yield": quantum_yield,
|
233
|
+
"extinction": extinction,
|
234
|
+
"lifetime": lifetime,
|
235
|
+
"excitation_spectrum_button": excitation_spectrum_button,
|
236
|
+
"emission_spectrum_button": emission_spectrum_button,
|
237
|
+
"excitation_spectrum_data": excitation_spectrum_data,
|
238
|
+
"emission_spectrum_data": emission_spectrum_data,
|
239
|
+
}
|
240
|
+
)
|
241
|
+
|
242
|
+
def add_transition_group(self, widget_refs):
|
243
|
+
layout = widget_refs["transition_container"]
|
244
|
+
group = QGroupBox(f"Transition {len(widget_refs['transitions']) + 1}")
|
245
|
+
form = QFormLayout()
|
246
|
+
|
247
|
+
from_state = QLineEdit()
|
248
|
+
to_state = QLineEdit()
|
249
|
+
photon_dependent = QComboBox()
|
250
|
+
photon_dependent.addItems(["True", "False"])
|
251
|
+
base_rate = scientific_input_field(0, 1e6, default=1000.0)
|
252
|
+
form.addRow("From State:", from_state)
|
253
|
+
form.addRow("To State:", to_state)
|
254
|
+
form.addRow("Photon Dependent:", photon_dependent)
|
255
|
+
form.addRow("Base Rate:", base_rate)
|
256
|
+
|
257
|
+
# === Spectrum container ===
|
258
|
+
spectrum_container = QWidget()
|
259
|
+
spectrum_form = QFormLayout(spectrum_container)
|
260
|
+
|
261
|
+
activation_spectrum_button = QPushButton("Edit Spectrum")
|
262
|
+
activation_spectrum_data = {"wavelengths": [], "intensities": []}
|
263
|
+
activation_spectrum_button.clicked.connect(
|
264
|
+
lambda: self.edit_spectrum(activation_spectrum_data)
|
265
|
+
)
|
266
|
+
spectrum_form.addRow("Activation Spectrum:", activation_spectrum_button)
|
267
|
+
|
268
|
+
quantum_yield = scientific_input_field(0, 1, default=0.7)
|
269
|
+
spectrum_form.addRow("Quantum Yield:", quantum_yield)
|
270
|
+
|
271
|
+
extinction_coefficient = scientific_input_field(-1e12, 1e12, default=1e5)
|
272
|
+
spectrum_form.addRow("Extinction Coefficient:", extinction_coefficient)
|
273
|
+
form.addRow(spectrum_container)
|
274
|
+
|
275
|
+
group.setLayout(form)
|
276
|
+
layout.addWidget(group)
|
277
|
+
|
278
|
+
# === Visibility logic ===
|
279
|
+
def update_visibility(val: str):
|
280
|
+
is_photon = val == "True"
|
281
|
+
spectrum_container.setVisible(is_photon)
|
282
|
+
base_rate.setVisible(not is_photon)
|
283
|
+
|
284
|
+
photon_dependent.currentTextChanged.connect(update_visibility)
|
285
|
+
update_visibility(photon_dependent.currentText()) # initial state
|
286
|
+
|
287
|
+
# === Store everything ===
|
288
|
+
widget_refs["transitions"].append(
|
289
|
+
{
|
290
|
+
"group": group,
|
291
|
+
"from_state": from_state,
|
292
|
+
"to_state": to_state,
|
293
|
+
"photon_dependent": photon_dependent,
|
294
|
+
"base_rate": base_rate,
|
295
|
+
"spectrum_container": spectrum_container,
|
296
|
+
"activation_spectrum_button": activation_spectrum_button,
|
297
|
+
"activation_spectrum_data": activation_spectrum_data,
|
298
|
+
"quantum_yield": quantum_yield,
|
299
|
+
"extinction_coefficient": extinction_coefficient,
|
300
|
+
}
|
301
|
+
)
|
302
|
+
|
303
|
+
def remove_last_group(self, group_list):
|
304
|
+
if group_list:
|
305
|
+
widget = group_list.pop()
|
306
|
+
widget["group"].deleteLater()
|
307
|
+
|
308
|
+
def edit_spectrum(self, spectrum_data):
|
309
|
+
dialog = SpectrumEditorDialog(
|
310
|
+
parent=self,
|
311
|
+
wavelengths=spectrum_data.get("wavelengths", []),
|
312
|
+
intensities=spectrum_data.get("intensities", []),
|
313
|
+
)
|
314
|
+
|
315
|
+
if dialog.exec():
|
316
|
+
spectrum_data["wavelengths"] = dialog.wavelengths
|
317
|
+
spectrum_data["intensities"] = dialog.intensities
|
318
|
+
|
319
|
+
def get_data(self) -> dict:
|
320
|
+
data = {
|
321
|
+
"num_of_fluorophores": len(self.fluorophore_widgets),
|
322
|
+
"fluorophore_names": [],
|
323
|
+
}
|
324
|
+
|
325
|
+
for fluor in self.fluorophore_widgets:
|
326
|
+
name = fluor["name"].text().strip()
|
327
|
+
if not name:
|
328
|
+
raise ValueError("Each fluorophore must have a name.")
|
329
|
+
|
330
|
+
data["fluorophore_names"].append(name)
|
331
|
+
|
332
|
+
fluor_data = {
|
333
|
+
"name": name,
|
334
|
+
"initial_state": fluor["initial_state"].text().strip(),
|
335
|
+
"states": {},
|
336
|
+
"transitions": {},
|
337
|
+
}
|
338
|
+
|
339
|
+
# States
|
340
|
+
for state in fluor["states"]:
|
341
|
+
state_name = state["name"].text().strip()
|
342
|
+
state_type = state["type"].currentText()
|
343
|
+
|
344
|
+
state_data = {
|
345
|
+
"name": state_name,
|
346
|
+
"state_type": state_type,
|
347
|
+
}
|
348
|
+
|
349
|
+
if state_type == "fluorescent":
|
350
|
+
state_data.update(
|
351
|
+
{
|
352
|
+
"quantum_yield": float(state["quantum_yield"].text()),
|
353
|
+
"extinction_coefficient": float(state["extinction"].text()),
|
354
|
+
"fluorescent_lifetime": float(state["lifetime"].text()),
|
355
|
+
"excitation_spectrum": {
|
356
|
+
"wavelengths": state["excitation_spectrum_data"][
|
357
|
+
"wavelengths"
|
358
|
+
],
|
359
|
+
"intensities": state["excitation_spectrum_data"][
|
360
|
+
"intensities"
|
361
|
+
],
|
362
|
+
},
|
363
|
+
"emission_spectrum": {
|
364
|
+
"wavelengths": state["emission_spectrum_data"][
|
365
|
+
"wavelengths"
|
366
|
+
],
|
367
|
+
"intensities": state["emission_spectrum_data"][
|
368
|
+
"intensities"
|
369
|
+
],
|
370
|
+
},
|
371
|
+
}
|
372
|
+
)
|
373
|
+
|
374
|
+
fluor_data["states"][state_name] = state_data
|
375
|
+
|
376
|
+
# Transitions
|
377
|
+
for trans in fluor["transitions"]:
|
378
|
+
from_state = trans["from_state"].text().strip()
|
379
|
+
to_state = trans["to_state"].text().strip()
|
380
|
+
key = f"{from_state}_to_{to_state}"
|
381
|
+
|
382
|
+
photon_dependent = trans["photon_dependent"].currentText() == "True"
|
383
|
+
transition_data = {
|
384
|
+
"from_state": from_state,
|
385
|
+
"to_state": to_state,
|
386
|
+
"photon_dependent": photon_dependent,
|
387
|
+
}
|
388
|
+
|
389
|
+
if photon_dependent:
|
390
|
+
transition_data["spectrum"] = {
|
391
|
+
"wavelengths": trans["activation_spectrum_data"]["wavelengths"],
|
392
|
+
"intensities": trans["activation_spectrum_data"]["intensities"],
|
393
|
+
"extinction_coefficient": float(
|
394
|
+
trans["extinction_coefficient"].text()
|
395
|
+
),
|
396
|
+
"quantum_yield": float(trans["quantum_yield"].text()),
|
397
|
+
}
|
398
|
+
else:
|
399
|
+
transition_data["base_rate"] = float(trans["base_rate"].text())
|
400
|
+
|
401
|
+
fluor_data["transitions"][key] = transition_data
|
402
|
+
|
403
|
+
data[name] = fluor_data
|
404
|
+
|
405
|
+
return data
|
406
|
+
|
407
|
+
def set_data(self, data: dict):
|
408
|
+
"""Populate the UI from TOML-based fluorophore config data."""
|
409
|
+
fluor_names = data.get("fluorophore_names", [])
|
410
|
+
self.set_mfluorophore_count(len(fluor_names))
|
411
|
+
|
412
|
+
for i, name in enumerate(fluor_names):
|
413
|
+
fluor_data = data.get(name, {})
|
414
|
+
widget_refs = self.fluorophore_widgets[i]
|
415
|
+
widget_refs["name"].setText(name)
|
416
|
+
|
417
|
+
widget_refs["initial_state"].setText(fluor_data.get("initial_state", {}))
|
418
|
+
|
419
|
+
# === Load States ===
|
420
|
+
widget_refs["group"] = []
|
421
|
+
states = fluor_data.get("states", {})
|
422
|
+
for j, (state_name, state_data) in enumerate(states.items()):
|
423
|
+
if j >= len(widget_refs["states"]):
|
424
|
+
self.add_state_group(widget_refs)
|
425
|
+
state_widget = widget_refs["states"][j]
|
426
|
+
|
427
|
+
state_widget["name"].setText(state_data["name"])
|
428
|
+
idx = state_widget["type"].findText(state_data["state_type"])
|
429
|
+
if idx != -1:
|
430
|
+
state_widget["type"].setCurrentIndex(idx)
|
431
|
+
|
432
|
+
if state_data["state_type"] == "fluorescent":
|
433
|
+
state_widget["quantum_yield"].setText(
|
434
|
+
str(state_data.get("quantum_yield", 0.0))
|
435
|
+
)
|
436
|
+
state_widget["extinction"].setText(
|
437
|
+
str(state_data.get("extinction_coefficient", 0.0))
|
438
|
+
)
|
439
|
+
state_widget["lifetime"].setText(
|
440
|
+
str(state_data.get("fluorescent_lifetime", 0.0))
|
441
|
+
)
|
442
|
+
|
443
|
+
# Spectra
|
444
|
+
state_widget["excitation_spectrum_data"]["wavelengths"] = (
|
445
|
+
state_data.get("excitation_spectrum", {}).get("wavelengths", [])
|
446
|
+
)
|
447
|
+
state_widget["excitation_spectrum_data"]["intensities"] = (
|
448
|
+
state_data.get("excitation_spectrum", {}).get("intensities", [])
|
449
|
+
)
|
450
|
+
|
451
|
+
state_widget["emission_spectrum_data"]["wavelengths"] = (
|
452
|
+
state_data.get("emission_spectrum", {}).get("wavelengths", [])
|
453
|
+
)
|
454
|
+
state_widget["emission_spectrum_data"]["intensities"] = (
|
455
|
+
state_data.get("emission_spectrum", {}).get("intensities", [])
|
456
|
+
)
|
457
|
+
|
458
|
+
# === Load Transitions ===
|
459
|
+
transitions = fluor_data.get("transitions", {})
|
460
|
+
for j, (key, trans_data) in enumerate(transitions.items()):
|
461
|
+
if j >= len(widget_refs["transitions"]):
|
462
|
+
self.add_transition_group(widget_refs)
|
463
|
+
trans_widget = widget_refs["transitions"][j]
|
464
|
+
|
465
|
+
trans_widget["from_state"].setText(trans_data["from_state"])
|
466
|
+
trans_widget["to_state"].setText(trans_data["to_state"])
|
467
|
+
is_photon = trans_data.get("photon_dependent", False)
|
468
|
+
idx = trans_widget["photon_dependent"].findText(str(is_photon))
|
469
|
+
if idx != -1:
|
470
|
+
trans_widget["photon_dependent"].setCurrentIndex(idx)
|
471
|
+
|
472
|
+
if is_photon:
|
473
|
+
spectrum = trans_data.get("spectrum", {})
|
474
|
+
trans_widget["activation_spectrum_data"]["wavelengths"] = (
|
475
|
+
spectrum.get("wavelengths", [])
|
476
|
+
)
|
477
|
+
trans_widget["activation_spectrum_data"]["intensities"] = (
|
478
|
+
spectrum.get("intensities", [])
|
479
|
+
)
|
480
|
+
trans_widget["extinction_coefficient"].setText(
|
481
|
+
str(spectrum.get("extinction_coefficient", 0.0))
|
482
|
+
)
|
483
|
+
trans_widget["quantum_yield"].setText(
|
484
|
+
str(spectrum.get("quantum_yield", 0.0))
|
485
|
+
)
|
486
|
+
else:
|
487
|
+
trans_widget["base_rate"].setText(
|
488
|
+
str(trans_data.get("base_rate", 0.0))
|
489
|
+
)
|
490
|
+
|
491
|
+
def validate(self) -> bool:
|
492
|
+
try:
|
493
|
+
from ...configio.convertconfig import create_fluorophores_from_config
|
494
|
+
|
495
|
+
data = self.get_data()
|
496
|
+
# Try to build fluorophores with the backend logic
|
497
|
+
|
498
|
+
create_fluorophores_from_config({"fluorophores": data})
|
499
|
+
|
500
|
+
QMessageBox.information(
|
501
|
+
self, "Validation Successful", "Fluorophore parameters are valid."
|
502
|
+
)
|
503
|
+
return True
|
504
|
+
|
505
|
+
except ValueError as e:
|
506
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
507
|
+
return False
|
508
|
+
except Exception as e:
|
509
|
+
QMessageBox.critical(self, "Unexpected Error", str(e))
|
510
|
+
return False
|
511
|
+
|
512
|
+
def get_help_path(self) -> Path:
|
513
|
+
return Path(__file__).parent.parent / "help_docs" / "fluorophore_help.md"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from PyQt6.QtWidgets import (
|
5
|
+
QComboBox,
|
6
|
+
QFormLayout,
|
7
|
+
QLineEdit,
|
8
|
+
QWidget,
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
class GeneralConfigWidget(QWidget):
|
13
|
+
def __init__(self):
|
14
|
+
super().__init__()
|
15
|
+
layout = QFormLayout()
|
16
|
+
|
17
|
+
self.version = QLineEdit()
|
18
|
+
self.version.setPlaceholderText("e.g., 1.0")
|
19
|
+
layout.addRow("Version:", self.version)
|
20
|
+
|
21
|
+
self.length_unit = QComboBox()
|
22
|
+
self.length_unit.addItems(["um"])
|
23
|
+
layout.addRow("Length Unit:", self.length_unit)
|
24
|
+
|
25
|
+
self.time_unit = QComboBox()
|
26
|
+
self.time_unit.addItems(["ms"])
|
27
|
+
layout.addRow("Time Unit:", self.time_unit)
|
28
|
+
|
29
|
+
self.diffusion_unit = QComboBox()
|
30
|
+
self.diffusion_unit.addItems(["um^2/s"])
|
31
|
+
layout.addRow("Diffusion Unit:", self.diffusion_unit)
|
32
|
+
|
33
|
+
self.setLayout(layout)
|
34
|
+
|
35
|
+
def is_valid(self):
|
36
|
+
return bool(re.match(r"^\d+\.\d+$", self.version.text()))
|
37
|
+
|
38
|
+
def get_data(self):
|
39
|
+
return {
|
40
|
+
"version": self.version.text(),
|
41
|
+
"length_unit": self.length_unit.currentText(),
|
42
|
+
"time_unit": self.time_unit.currentText(),
|
43
|
+
"diffusion_unit": self.diffusion_unit.currentText(),
|
44
|
+
}
|
45
|
+
|
46
|
+
def get_help_path(self) -> Path:
|
47
|
+
return Path(__file__).parent.parent / "help_docs" / "general_help.md"
|
@@ -0,0 +1,142 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from PyQt6.QtWidgets import (
|
4
|
+
QFormLayout,
|
5
|
+
QHBoxLayout,
|
6
|
+
QMessageBox,
|
7
|
+
QPushButton,
|
8
|
+
QSpinBox,
|
9
|
+
QVBoxLayout,
|
10
|
+
QWidget,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class GlobalConfigWidget(QWidget):
|
15
|
+
def __init__(self):
|
16
|
+
super().__init__()
|
17
|
+
self.setup_ui()
|
18
|
+
|
19
|
+
def setup_ui(self):
|
20
|
+
layout = QVBoxLayout()
|
21
|
+
form = QFormLayout()
|
22
|
+
# Validation button
|
23
|
+
self.validate_button = QPushButton("Validate Parameters")
|
24
|
+
self.validate_button.clicked.connect(self.validate)
|
25
|
+
layout.addWidget(self.validate_button)
|
26
|
+
|
27
|
+
# Sample plane dimensions
|
28
|
+
self.sample_plane_width = QSpinBox()
|
29
|
+
self.sample_plane_width.setRange(1, 1000)
|
30
|
+
self.sample_plane_width.setSuffix(" μm")
|
31
|
+
|
32
|
+
self.sample_plane_height = QSpinBox()
|
33
|
+
self.sample_plane_height.setRange(1, 1000)
|
34
|
+
self.sample_plane_height.setSuffix(" μm")
|
35
|
+
|
36
|
+
plane_dim_layout = QHBoxLayout()
|
37
|
+
plane_dim_layout.addWidget(self.sample_plane_width)
|
38
|
+
plane_dim_layout.addWidget(self.sample_plane_height)
|
39
|
+
plane_dim_container = QWidget()
|
40
|
+
plane_dim_container.setLayout(plane_dim_layout)
|
41
|
+
form.addRow("Sample Plane Dimensions (W × H):", plane_dim_container)
|
42
|
+
|
43
|
+
# Cycle count
|
44
|
+
self.cycle_count = QSpinBox()
|
45
|
+
self.cycle_count.setRange(1, 1000)
|
46
|
+
form.addRow("Cycle Count:", self.cycle_count)
|
47
|
+
|
48
|
+
# Exposure time
|
49
|
+
self.exposure_time = QSpinBox()
|
50
|
+
self.exposure_time.setRange(1, 1000)
|
51
|
+
self.exposure_time.setSuffix(" ms")
|
52
|
+
form.addRow("Exposure Time:", self.exposure_time)
|
53
|
+
|
54
|
+
# Interval time
|
55
|
+
self.interval_time = QSpinBox()
|
56
|
+
self.interval_time.setRange(0, 1000)
|
57
|
+
self.interval_time.setSuffix(" ms")
|
58
|
+
form.addRow("Interval Time:", self.interval_time)
|
59
|
+
|
60
|
+
# Oversample motion time
|
61
|
+
self.oversample_motion_time = QSpinBox()
|
62
|
+
self.oversample_motion_time.setRange(0, 1000)
|
63
|
+
self.oversample_motion_time.setSuffix(" ms")
|
64
|
+
form.addRow("Oversample Motion Time:", self.oversample_motion_time)
|
65
|
+
|
66
|
+
# Add form to main layout
|
67
|
+
layout.addLayout(form)
|
68
|
+
|
69
|
+
# Set default values
|
70
|
+
self.set_defaults()
|
71
|
+
|
72
|
+
self.setLayout(layout)
|
73
|
+
|
74
|
+
def set_defaults(self):
|
75
|
+
"""Set default values for the form fields"""
|
76
|
+
self.sample_plane_width.setValue(50) # 1000 μm
|
77
|
+
self.sample_plane_height.setValue(50) # 1000 μm
|
78
|
+
self.cycle_count.setValue(5)
|
79
|
+
self.exposure_time.setValue(100) # 100 ms
|
80
|
+
self.interval_time.setValue(0) # 50 ms
|
81
|
+
self.oversample_motion_time.setValue(1) # 10 ms
|
82
|
+
|
83
|
+
def set_data(self, data: dict):
|
84
|
+
"""
|
85
|
+
Populate the global parameter fields using data from a config dictionary.
|
86
|
+
"""
|
87
|
+
try:
|
88
|
+
# Sample plane dimensions
|
89
|
+
dims = data.get("sample_plane_dim", [50, 50])
|
90
|
+
if isinstance(dims, list) and len(dims) == 2:
|
91
|
+
self.sample_plane_width.setValue(dims[0])
|
92
|
+
self.sample_plane_height.setValue(dims[1])
|
93
|
+
|
94
|
+
# Other scalar fields
|
95
|
+
self.cycle_count.setValue(data.get("cycle_count", 5))
|
96
|
+
self.exposure_time.setValue(data.get("exposure_time", 100))
|
97
|
+
self.interval_time.setValue(data.get("interval_time", 0))
|
98
|
+
self.oversample_motion_time.setValue(data.get("oversample_motion_time", 1))
|
99
|
+
|
100
|
+
except Exception as e:
|
101
|
+
print(f"[GlobalConfigWidget] Failed to load config: {e}")
|
102
|
+
|
103
|
+
def get_data(self):
|
104
|
+
"""Collect all form data and return as a dictionary"""
|
105
|
+
return {
|
106
|
+
"sample_plane_dim": [
|
107
|
+
self.sample_plane_width.value(),
|
108
|
+
self.sample_plane_height.value(),
|
109
|
+
],
|
110
|
+
"cycle_count": self.cycle_count.value(),
|
111
|
+
"exposure_time": self.exposure_time.value(),
|
112
|
+
"interval_time": self.interval_time.value(),
|
113
|
+
"oversample_motion_time": self.oversample_motion_time.value(),
|
114
|
+
}
|
115
|
+
|
116
|
+
def validate(self) -> bool:
|
117
|
+
try:
|
118
|
+
from ...configio.configmodels import GlobalParameters
|
119
|
+
|
120
|
+
data = self.get_data()
|
121
|
+
|
122
|
+
GlobalParameters(**data)
|
123
|
+
|
124
|
+
QMessageBox.information(
|
125
|
+
self, "Validation Successful", "Global parameters are valid."
|
126
|
+
)
|
127
|
+
return True
|
128
|
+
|
129
|
+
except TypeError as e:
|
130
|
+
QMessageBox.critical(
|
131
|
+
self, "Validation Error", f"Missing or invalid fields: {str(e)}"
|
132
|
+
)
|
133
|
+
return False
|
134
|
+
except ValueError as e:
|
135
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
136
|
+
return False
|
137
|
+
except Exception as e:
|
138
|
+
QMessageBox.critical(self, "Unexpected Error", str(e))
|
139
|
+
return False
|
140
|
+
|
141
|
+
def get_help_path(self) -> Path:
|
142
|
+
return Path(__file__).parent.parent / "help_docs" / "global_help.md"
|