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.
Files changed (61) hide show
  1. AMS_BP/__init__.py +1 -1
  2. AMS_BP/configio/configmodels.py +32 -18
  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/sim_microscopy.py +2 -2
  55. {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/METADATA +59 -34
  56. ams_bp-0.4.0.dist-info/RECORD +103 -0
  57. ams_bp-0.4.0.dist-info/entry_points.txt +2 -0
  58. ams_bp-0.3.0.dist-info/RECORD +0 -55
  59. ams_bp-0.3.0.dist-info/entry_points.txt +0 -2
  60. {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
  61. {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"