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,259 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from PyQt6.QtWidgets import (
|
|
5
|
+
QComboBox,
|
|
6
|
+
QDoubleSpinBox,
|
|
7
|
+
QFormLayout,
|
|
8
|
+
QHBoxLayout,
|
|
9
|
+
QLabel,
|
|
10
|
+
QLineEdit,
|
|
11
|
+
QMessageBox,
|
|
12
|
+
QPushButton,
|
|
13
|
+
QSpinBox,
|
|
14
|
+
QTabWidget,
|
|
15
|
+
QVBoxLayout,
|
|
16
|
+
QWidget,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExperimentConfigWidget(QWidget):
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
self.laser_power_widgets = {}
|
|
25
|
+
self.laser_position_widgets = {}
|
|
26
|
+
|
|
27
|
+
layout = QVBoxLayout()
|
|
28
|
+
form = QFormLayout()
|
|
29
|
+
|
|
30
|
+
# Experiment Info
|
|
31
|
+
self.name_field = QLineEdit()
|
|
32
|
+
form.addRow("Experiment Name:", self.name_field)
|
|
33
|
+
|
|
34
|
+
self.desc_field = QLineEdit()
|
|
35
|
+
form.addRow("Description:", self.desc_field)
|
|
36
|
+
|
|
37
|
+
self.type_field = QComboBox()
|
|
38
|
+
self.type_field.addItems(["time-series", "z-stack"])
|
|
39
|
+
form.addRow("Experiment Type:", self.type_field)
|
|
40
|
+
|
|
41
|
+
# Z Position (just one for time-series)
|
|
42
|
+
self.z_position_inputs: List[QDoubleSpinBox] = []
|
|
43
|
+
|
|
44
|
+
self.z_position_container = QWidget()
|
|
45
|
+
self.z_position_layout = QVBoxLayout()
|
|
46
|
+
self.z_position_container.setLayout(self.z_position_layout)
|
|
47
|
+
form.addRow("Z Position(s):", self.z_position_container)
|
|
48
|
+
|
|
49
|
+
self.add_z_button = QPushButton("Add Z-Position")
|
|
50
|
+
self.remove_z_button = QPushButton("Remove Z-Position")
|
|
51
|
+
self.remove_z_button.clicked.connect(self.remove_z_position_field)
|
|
52
|
+
self.add_z_button.clicked.connect(self.add_z_position_field)
|
|
53
|
+
self.type_field.currentTextChanged.connect(self.update_z_position_mode)
|
|
54
|
+
z_button_row = QHBoxLayout()
|
|
55
|
+
z_button_row.addWidget(self.add_z_button)
|
|
56
|
+
z_button_row.addWidget(self.remove_z_button)
|
|
57
|
+
layout.addLayout(z_button_row)
|
|
58
|
+
# XY Offset
|
|
59
|
+
self.xyoffset = [QDoubleSpinBox() for _ in range(2)]
|
|
60
|
+
for box in self.xyoffset:
|
|
61
|
+
box.setRange(-1e5, 1e5)
|
|
62
|
+
form.addRow("XY Offset (x, y):", self._hbox(self.xyoffset))
|
|
63
|
+
|
|
64
|
+
# Exposure and Interval wrapped in a QWidget
|
|
65
|
+
self.timing_widget = QWidget()
|
|
66
|
+
timing_layout = QFormLayout(self.timing_widget)
|
|
67
|
+
|
|
68
|
+
self.exposure = QSpinBox()
|
|
69
|
+
self.exposure.setRange(0, 100000000)
|
|
70
|
+
timing_layout.addRow("Exposure Time (ms):", self.exposure)
|
|
71
|
+
|
|
72
|
+
self.interval = QSpinBox()
|
|
73
|
+
self.interval.setRange(0, 100000000)
|
|
74
|
+
timing_layout.addRow("Interval Time (ms):", self.interval)
|
|
75
|
+
|
|
76
|
+
form.addRow(self.timing_widget)
|
|
77
|
+
|
|
78
|
+
self.update_z_position_mode(self.type_field.currentText())
|
|
79
|
+
layout.addLayout(form)
|
|
80
|
+
|
|
81
|
+
# Laser Tabs
|
|
82
|
+
self.laser_tabs = QTabWidget()
|
|
83
|
+
layout.addWidget(QLabel("Active Laser Parameters:"))
|
|
84
|
+
layout.addWidget(self.laser_tabs)
|
|
85
|
+
|
|
86
|
+
# Validate Button
|
|
87
|
+
self.validate_button = QPushButton("Validate")
|
|
88
|
+
self.validate_button.clicked.connect(self.validate)
|
|
89
|
+
layout.addWidget(self.validate_button)
|
|
90
|
+
|
|
91
|
+
self.setLayout(layout)
|
|
92
|
+
|
|
93
|
+
def update_z_position_mode(self, mode: str):
|
|
94
|
+
# Clear existing
|
|
95
|
+
for i in reversed(range(self.z_position_layout.count())):
|
|
96
|
+
item = self.z_position_layout.itemAt(i).widget()
|
|
97
|
+
if item:
|
|
98
|
+
item.setParent(None)
|
|
99
|
+
self.z_position_inputs.clear()
|
|
100
|
+
|
|
101
|
+
if mode == "time-series":
|
|
102
|
+
# One input only
|
|
103
|
+
z_input = QDoubleSpinBox()
|
|
104
|
+
z_input.setRange(-1e5, 1e5)
|
|
105
|
+
self.z_position_inputs.append(z_input)
|
|
106
|
+
self.z_position_layout.addWidget(z_input)
|
|
107
|
+
self.add_z_button.setVisible(False)
|
|
108
|
+
self.add_z_button.setVisible(False)
|
|
109
|
+
self.remove_z_button.setVisible(False)
|
|
110
|
+
self.timing_widget.setVisible(False)
|
|
111
|
+
else:
|
|
112
|
+
# Start with two for z-stack
|
|
113
|
+
for _ in range(2):
|
|
114
|
+
self.add_z_position_field()
|
|
115
|
+
self.add_z_button.setVisible(True)
|
|
116
|
+
self.add_z_button.setVisible(True)
|
|
117
|
+
self.remove_z_button.setVisible(True)
|
|
118
|
+
self.timing_widget.setVisible(True)
|
|
119
|
+
|
|
120
|
+
def remove_z_position_field(self):
|
|
121
|
+
if len(self.z_position_inputs) > 1:
|
|
122
|
+
z_widget = self.z_position_inputs.pop()
|
|
123
|
+
self.z_position_layout.removeWidget(z_widget)
|
|
124
|
+
z_widget.setParent(None)
|
|
125
|
+
else:
|
|
126
|
+
QMessageBox.warning(
|
|
127
|
+
self, "Cannot Remove", "At least one Z-position is required."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def add_z_position_field(self):
|
|
131
|
+
z_input = QDoubleSpinBox()
|
|
132
|
+
z_input.setRange(-1e5, 1e5)
|
|
133
|
+
self.z_position_inputs.append(z_input)
|
|
134
|
+
self.z_position_layout.addWidget(z_input)
|
|
135
|
+
|
|
136
|
+
def _hbox(self, widgets):
|
|
137
|
+
box = QHBoxLayout()
|
|
138
|
+
for w in widgets:
|
|
139
|
+
box.addWidget(w)
|
|
140
|
+
container = QWidget()
|
|
141
|
+
container.setLayout(box)
|
|
142
|
+
return container
|
|
143
|
+
|
|
144
|
+
def set_active_lasers(self, laser_names: List[str], powers=None, positions=None):
|
|
145
|
+
self.laser_tabs.clear()
|
|
146
|
+
self.laser_power_widgets.clear()
|
|
147
|
+
self.laser_position_widgets.clear()
|
|
148
|
+
|
|
149
|
+
powers = powers or [0.0] * len(laser_names)
|
|
150
|
+
positions = positions or [[0.0, 0.0, 0.0] for _ in laser_names]
|
|
151
|
+
|
|
152
|
+
for i, name in enumerate(laser_names):
|
|
153
|
+
tab = QWidget()
|
|
154
|
+
form = QFormLayout()
|
|
155
|
+
|
|
156
|
+
# Power
|
|
157
|
+
power = QDoubleSpinBox()
|
|
158
|
+
power.setValue(powers[i] if i < len(powers) else 0.0)
|
|
159
|
+
form.addRow("Power (W):", power)
|
|
160
|
+
|
|
161
|
+
# Position
|
|
162
|
+
pos_spins = [QDoubleSpinBox() for _ in range(3)]
|
|
163
|
+
for j, s in enumerate(pos_spins):
|
|
164
|
+
s.setValue(
|
|
165
|
+
positions[i][j]
|
|
166
|
+
if i < len(positions) and j < len(positions[i])
|
|
167
|
+
else 0.0
|
|
168
|
+
)
|
|
169
|
+
form.addRow("Position (x, y, z):", self._hbox(pos_spins))
|
|
170
|
+
|
|
171
|
+
tab.setLayout(form)
|
|
172
|
+
self.laser_tabs.addTab(tab, name)
|
|
173
|
+
|
|
174
|
+
self.laser_power_widgets[name] = power
|
|
175
|
+
self.laser_position_widgets[name] = pos_spins
|
|
176
|
+
|
|
177
|
+
def get_data(self):
|
|
178
|
+
data = {
|
|
179
|
+
"name": self.name_field.text(),
|
|
180
|
+
"description": self.desc_field.text(),
|
|
181
|
+
"experiment_type": self.type_field.currentText(),
|
|
182
|
+
"z_position": [z.value() for z in self.z_position_inputs],
|
|
183
|
+
"laser_names_active": list(self.laser_power_widgets.keys()),
|
|
184
|
+
"laser_powers_active": [
|
|
185
|
+
w.value() for w in self.laser_power_widgets.values()
|
|
186
|
+
],
|
|
187
|
+
"laser_positions_active": [
|
|
188
|
+
[w.value() for w in self.laser_position_widgets[name]]
|
|
189
|
+
for name in self.laser_position_widgets
|
|
190
|
+
],
|
|
191
|
+
"xyoffset": [w.value() for w in self.xyoffset],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if data["experiment_type"] == "z-stack":
|
|
195
|
+
data["exposure_time"] = self.exposure.value()
|
|
196
|
+
data["interval_time"] = self.interval.value()
|
|
197
|
+
|
|
198
|
+
return data
|
|
199
|
+
|
|
200
|
+
def set_data(self, data: dict):
|
|
201
|
+
self.name_field.setText(data.get("name", ""))
|
|
202
|
+
self.desc_field.setText(data.get("description", ""))
|
|
203
|
+
|
|
204
|
+
experiment_type = data.get("experiment_type", "time-series")
|
|
205
|
+
idx = self.type_field.findText(experiment_type)
|
|
206
|
+
if idx >= 0:
|
|
207
|
+
self.type_field.setCurrentIndex(idx)
|
|
208
|
+
|
|
209
|
+
self.update_z_position_mode(experiment_type)
|
|
210
|
+
z_positions = data.get("z_position", [])
|
|
211
|
+
if experiment_type == "time-series" and z_positions:
|
|
212
|
+
self.z_position_inputs[0].setValue(z_positions[0])
|
|
213
|
+
elif experiment_type == "z-stack":
|
|
214
|
+
for i in range(len(self.z_position_inputs), len(z_positions)):
|
|
215
|
+
self.add_z_position_field()
|
|
216
|
+
for i, val in enumerate(z_positions):
|
|
217
|
+
if i < len(self.z_position_inputs):
|
|
218
|
+
self.z_position_inputs[i].setValue(val)
|
|
219
|
+
|
|
220
|
+
# XY Offset
|
|
221
|
+
xyoffset = data.get("xyoffset", [0, 0])
|
|
222
|
+
for i in range(min(2, len(xyoffset))):
|
|
223
|
+
self.xyoffset[i].setValue(xyoffset[i])
|
|
224
|
+
|
|
225
|
+
# Exposure and Interval only for z-stack
|
|
226
|
+
if experiment_type == "z-stack":
|
|
227
|
+
self.exposure.setValue(data.get("exposure_time", 100))
|
|
228
|
+
self.interval.setValue(data.get("interval_time", 0))
|
|
229
|
+
else:
|
|
230
|
+
self.exposure.setValue(0)
|
|
231
|
+
self.interval.setValue(0)
|
|
232
|
+
|
|
233
|
+
# Laser data in one call
|
|
234
|
+
laser_names = data.get("laser_names_active", [])
|
|
235
|
+
powers = data.get("laser_powers_active", [])
|
|
236
|
+
positions = data.get("laser_positions_active", [])
|
|
237
|
+
self.set_active_lasers(laser_names, powers=powers, positions=positions)
|
|
238
|
+
|
|
239
|
+
def validate(self) -> bool:
|
|
240
|
+
try:
|
|
241
|
+
from ...configio.convertconfig import create_experiment_from_config
|
|
242
|
+
|
|
243
|
+
data = self.get_data()
|
|
244
|
+
config_dict = {"experiment": data}
|
|
245
|
+
|
|
246
|
+
# This function will raise if the dataclass constructor or __post_init__ fails
|
|
247
|
+
configEXP, funcEXP = create_experiment_from_config(config_dict)
|
|
248
|
+
|
|
249
|
+
QMessageBox.information(
|
|
250
|
+
self, "Validation Successful", "Experiment parameters are valid."
|
|
251
|
+
)
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def get_help_path(self) -> Path:
|
|
259
|
+
return Path(__file__).parent.parent / "help_docs" / "experiment_help.md"
|