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,714 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from pydantic import ValidationError
|
4
|
+
from PyQt6.QtCore import pyqtSignal
|
5
|
+
from PyQt6.QtWidgets import (
|
6
|
+
QCheckBox,
|
7
|
+
QComboBox,
|
8
|
+
QDoubleSpinBox,
|
9
|
+
QFormLayout,
|
10
|
+
QGridLayout,
|
11
|
+
QHBoxLayout,
|
12
|
+
QLabel,
|
13
|
+
QMessageBox,
|
14
|
+
QPushButton,
|
15
|
+
QScrollArea,
|
16
|
+
QSpinBox,
|
17
|
+
QTabWidget,
|
18
|
+
QVBoxLayout,
|
19
|
+
QWidget,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
class MoleculeConfigWidget(QWidget):
|
24
|
+
# Signal to notify when molecule count changes
|
25
|
+
molecule_count_changed = pyqtSignal(int)
|
26
|
+
|
27
|
+
def __init__(self):
|
28
|
+
super().__init__()
|
29
|
+
|
30
|
+
self.main_layout = QVBoxLayout()
|
31
|
+
self.setLayout(self.main_layout)
|
32
|
+
|
33
|
+
# Number of molecule types spinner
|
34
|
+
self.num_types_layout = QHBoxLayout()
|
35
|
+
self.num_types_label = QLabel("Number of molecule types:")
|
36
|
+
self.num_types_spinner = QSpinBox()
|
37
|
+
self.num_types_spinner.setRange(1, 10)
|
38
|
+
self.num_types_spinner.setValue(1)
|
39
|
+
self.num_types_spinner.valueChanged.connect(self._on_molecule_count_changed)
|
40
|
+
self.num_types_layout.addWidget(self.num_types_label)
|
41
|
+
self.num_types_layout.addWidget(self.num_types_spinner)
|
42
|
+
self.num_types_layout.addStretch()
|
43
|
+
|
44
|
+
# Validate button
|
45
|
+
self.validate_button = QPushButton("Validate")
|
46
|
+
self.validate_button.clicked.connect(self.validate)
|
47
|
+
self.num_types_layout.addWidget(self.validate_button)
|
48
|
+
|
49
|
+
self.main_layout.addLayout(self.num_types_layout)
|
50
|
+
|
51
|
+
# Create tab widget to hold molecule type configs
|
52
|
+
self.tabs = QTabWidget()
|
53
|
+
self.main_layout.addWidget(self.tabs)
|
54
|
+
|
55
|
+
# Initialize with one molecule type
|
56
|
+
self.molecule_type_widgets = []
|
57
|
+
self.update_molecule_types(1)
|
58
|
+
|
59
|
+
def _on_molecule_count_changed(self, count):
|
60
|
+
self.update_molecule_types(count)
|
61
|
+
self.molecule_count_changed.emit(count)
|
62
|
+
|
63
|
+
def set_molecule_count(self, count):
|
64
|
+
if self.num_types_spinner.value() != count:
|
65
|
+
self.num_types_spinner.blockSignals(True)
|
66
|
+
self.num_types_spinner.setValue(count)
|
67
|
+
self.num_types_spinner.blockSignals(False)
|
68
|
+
self.update_molecule_types(count)
|
69
|
+
self.molecule_count_changed.emit(count)
|
70
|
+
|
71
|
+
def update_molecule_types(self, num_types):
|
72
|
+
# Store current data if any
|
73
|
+
current_data = self.get_data() if self.molecule_type_widgets else None
|
74
|
+
|
75
|
+
# Remove existing tabs
|
76
|
+
while self.tabs.count() > 0:
|
77
|
+
self.tabs.removeTab(0)
|
78
|
+
|
79
|
+
self.molecule_type_widgets = []
|
80
|
+
|
81
|
+
# Create new tabs
|
82
|
+
for i in range(num_types):
|
83
|
+
molecule_widget = MoleculeTypeWidget(i)
|
84
|
+
self.molecule_type_widgets.append(molecule_widget)
|
85
|
+
self.tabs.addTab(molecule_widget, f"Molecule Type {i+1}")
|
86
|
+
|
87
|
+
# Restore data only if tab count matches current data
|
88
|
+
if current_data and len(current_data["num_molecules"]) == num_types:
|
89
|
+
self.set_data_from_widget_selection(current_data)
|
90
|
+
|
91
|
+
def validate(self) -> bool:
|
92
|
+
try:
|
93
|
+
data = self.get_data()
|
94
|
+
|
95
|
+
# This will validate the schema using the backend logic
|
96
|
+
from ...configio.configmodels import MoleculeParameters
|
97
|
+
from ...configio.convertconfig import create_dataclass_schema
|
98
|
+
|
99
|
+
_ = create_dataclass_schema(MoleculeParameters, data)
|
100
|
+
|
101
|
+
QMessageBox.information(
|
102
|
+
self, "Validation Successful", "Molecule parameters are valid."
|
103
|
+
)
|
104
|
+
return True
|
105
|
+
except ValidationError as e:
|
106
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
107
|
+
return False
|
108
|
+
except ValueError as e:
|
109
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
110
|
+
return False
|
111
|
+
|
112
|
+
def set_data(self, config: dict):
|
113
|
+
"""
|
114
|
+
Load molecule configuration from TOML config format.
|
115
|
+
"""
|
116
|
+
try:
|
117
|
+
# Validate format first
|
118
|
+
from ...configio.configmodels import MoleculeParameters
|
119
|
+
from ...configio.convertconfig import create_dataclass_schema
|
120
|
+
|
121
|
+
validated = create_dataclass_schema(MoleculeParameters, config)
|
122
|
+
|
123
|
+
# Determine number of types
|
124
|
+
num_types = len(validated.num_molecules)
|
125
|
+
self.set_molecule_count(num_types)
|
126
|
+
|
127
|
+
# Build compatible format to use set_data_from_widget_state()
|
128
|
+
parsed = {
|
129
|
+
"num_molecules": validated.num_molecules,
|
130
|
+
"track_type": validated.track_type,
|
131
|
+
"diffusion_coefficient": validated.diffusion_coefficient,
|
132
|
+
"hurst_exponent": validated.hurst_exponent,
|
133
|
+
"allow_transition_probability": validated.allow_transition_probability,
|
134
|
+
"transition_matrix_time_step": validated.transition_matrix_time_step,
|
135
|
+
"diffusion_transition_matrix": validated.diffusion_transition_matrix,
|
136
|
+
"hurst_transition_matrix": validated.hurst_transition_matrix,
|
137
|
+
"state_probability_diffusion": validated.state_probability_diffusion,
|
138
|
+
"state_probability_hurst": validated.state_probability_hurst,
|
139
|
+
}
|
140
|
+
|
141
|
+
self.set_data_from_widget_selection(parsed)
|
142
|
+
|
143
|
+
except Exception as e:
|
144
|
+
print(f"[MoleculeConfigWidget] Failed to load from config: {e}")
|
145
|
+
|
146
|
+
def get_data(self):
|
147
|
+
num_molecules = []
|
148
|
+
track_type = []
|
149
|
+
diffusion_coefficient = []
|
150
|
+
hurst_exponent = []
|
151
|
+
allow_transition_probability = []
|
152
|
+
transition_matrix_time_step = []
|
153
|
+
diffusion_transition_matrix = []
|
154
|
+
hurst_transition_matrix = []
|
155
|
+
state_probability_diffusion = []
|
156
|
+
state_probability_hurst = []
|
157
|
+
|
158
|
+
for widget in self.molecule_type_widgets:
|
159
|
+
type_data = widget.get_data()
|
160
|
+
|
161
|
+
num_molecules.append(type_data["num_molecules"])
|
162
|
+
track_type.append(type_data["track_type"])
|
163
|
+
diffusion_coefficient.append(type_data["diffusion_coefficient"])
|
164
|
+
hurst_exponent.append(type_data["hurst_exponent"])
|
165
|
+
allow_transition_probability.append(
|
166
|
+
type_data["allow_transition_probability"]
|
167
|
+
)
|
168
|
+
transition_matrix_time_step.append(type_data["transition_matrix_time_step"])
|
169
|
+
diffusion_transition_matrix.append(type_data["diffusion_transition_matrix"])
|
170
|
+
hurst_transition_matrix.append(type_data["hurst_transition_matrix"])
|
171
|
+
state_probability_diffusion.append(type_data["state_probability_diffusion"])
|
172
|
+
state_probability_hurst.append(type_data["state_probability_hurst"])
|
173
|
+
|
174
|
+
# Normalize shapes: make sure every list has the same shape
|
175
|
+
def ensure_nested_list_shape(lst, expected_len, default_val):
|
176
|
+
return [x if len(x) > 0 else [default_val] for x in lst]
|
177
|
+
|
178
|
+
def pad_matrix_list(mat_list, expected_size):
|
179
|
+
result = []
|
180
|
+
for mat in mat_list:
|
181
|
+
if len(mat) == 0:
|
182
|
+
result.append([[1.0]])
|
183
|
+
else:
|
184
|
+
result.append(mat)
|
185
|
+
return result
|
186
|
+
|
187
|
+
diffusion_coefficient = ensure_nested_list_shape(
|
188
|
+
diffusion_coefficient, len(num_molecules), 1.0
|
189
|
+
)
|
190
|
+
hurst_exponent = ensure_nested_list_shape(
|
191
|
+
hurst_exponent, len(num_molecules), 0.5
|
192
|
+
)
|
193
|
+
state_probability_diffusion = ensure_nested_list_shape(
|
194
|
+
state_probability_diffusion, len(num_molecules), 1.0
|
195
|
+
)
|
196
|
+
state_probability_hurst = ensure_nested_list_shape(
|
197
|
+
state_probability_hurst, len(num_molecules), 1.0
|
198
|
+
)
|
199
|
+
|
200
|
+
diffusion_transition_matrix = pad_matrix_list(
|
201
|
+
diffusion_transition_matrix, len(num_molecules)
|
202
|
+
)
|
203
|
+
hurst_transition_matrix = pad_matrix_list(
|
204
|
+
hurst_transition_matrix, len(num_molecules)
|
205
|
+
)
|
206
|
+
return {
|
207
|
+
"num_molecules": num_molecules,
|
208
|
+
"track_type": track_type,
|
209
|
+
"diffusion_coefficient": diffusion_coefficient,
|
210
|
+
"hurst_exponent": hurst_exponent,
|
211
|
+
"allow_transition_probability": allow_transition_probability,
|
212
|
+
"transition_matrix_time_step": transition_matrix_time_step,
|
213
|
+
"diffusion_transition_matrix": diffusion_transition_matrix,
|
214
|
+
"hurst_transition_matrix": hurst_transition_matrix,
|
215
|
+
"state_probability_diffusion": state_probability_diffusion,
|
216
|
+
"state_probability_hurst": state_probability_hurst,
|
217
|
+
}
|
218
|
+
|
219
|
+
def set_data_from_widget_selection(self, data):
|
220
|
+
num_types = min(len(data["num_molecules"]), len(self.molecule_type_widgets))
|
221
|
+
self.num_types_spinner.setValue(num_types)
|
222
|
+
|
223
|
+
for i in range(num_types):
|
224
|
+
type_data = {
|
225
|
+
"num_molecules": data["num_molecules"][i],
|
226
|
+
"track_type": data["track_type"][i],
|
227
|
+
"diffusion_coefficient": data["diffusion_coefficient"][i],
|
228
|
+
"hurst_exponent": data["hurst_exponent"][i],
|
229
|
+
"allow_transition_probability": data["allow_transition_probability"][i],
|
230
|
+
"transition_matrix_time_step": data["transition_matrix_time_step"][i],
|
231
|
+
"diffusion_transition_matrix": data["diffusion_transition_matrix"][i],
|
232
|
+
"hurst_transition_matrix": data["hurst_transition_matrix"][i],
|
233
|
+
"state_probability_diffusion": data["state_probability_diffusion"][i],
|
234
|
+
"state_probability_hurst": data["state_probability_hurst"][i],
|
235
|
+
}
|
236
|
+
self.molecule_type_widgets[i].set_data(type_data)
|
237
|
+
|
238
|
+
|
239
|
+
class MoleculeTypeWidget(QWidget):
|
240
|
+
def __init__(self, type_index):
|
241
|
+
super().__init__()
|
242
|
+
self.type_index = type_index
|
243
|
+
|
244
|
+
# Create a scroll area to handle potentially large configs
|
245
|
+
self.scroll = QScrollArea()
|
246
|
+
self.scroll.setWidgetResizable(True)
|
247
|
+
|
248
|
+
self.container = QWidget()
|
249
|
+
self.layout = QVBoxLayout(self.container)
|
250
|
+
|
251
|
+
# Basic parameters
|
252
|
+
self.form_layout = QFormLayout()
|
253
|
+
|
254
|
+
# Number of molecules
|
255
|
+
self.num_molecules = QSpinBox()
|
256
|
+
self.num_molecules.setRange(1, 100000)
|
257
|
+
self.num_molecules.setValue(100)
|
258
|
+
self.form_layout.addRow("Number of molecules:", self.num_molecules)
|
259
|
+
|
260
|
+
# Track type
|
261
|
+
self.track_type = QComboBox()
|
262
|
+
self.track_type.addItems(["constant", "fbm"])
|
263
|
+
self.track_type.currentTextChanged.connect(self.update_visibility)
|
264
|
+
self.form_layout.addRow("Track type:", self.track_type)
|
265
|
+
|
266
|
+
# Transition probability
|
267
|
+
self.allow_transition = QCheckBox("Allow transition probability")
|
268
|
+
self.allow_transition.setChecked(False)
|
269
|
+
self.allow_transition.stateChanged.connect(self.update_visibility)
|
270
|
+
self.form_layout.addRow("", self.allow_transition)
|
271
|
+
|
272
|
+
# Transition matrix time step
|
273
|
+
self.transition_time_step = QSpinBox()
|
274
|
+
self.transition_time_step.setRange(1, 10000)
|
275
|
+
self.transition_time_step.setValue(100)
|
276
|
+
self.transition_time_step.setSuffix(" ms")
|
277
|
+
self.form_layout.addRow(
|
278
|
+
"Transition matrix time step:", self.transition_time_step
|
279
|
+
)
|
280
|
+
|
281
|
+
self.layout.addLayout(self.form_layout)
|
282
|
+
|
283
|
+
# Diffusion coefficient section
|
284
|
+
self.diffusion_group = QWidget()
|
285
|
+
self.diffusion_layout = QVBoxLayout(self.diffusion_group)
|
286
|
+
|
287
|
+
self.diffusion_header = QHBoxLayout()
|
288
|
+
self.diffusion_label = QLabel("<b>Diffusion Coefficients</b>")
|
289
|
+
self.diffusion_count = QSpinBox()
|
290
|
+
self.diffusion_count.setValue(1)
|
291
|
+
self.diffusion_count.valueChanged.connect(self.update_diffusion_coefficients)
|
292
|
+
self.diffusion_header.addWidget(self.diffusion_label)
|
293
|
+
self.diffusion_header.addWidget(QLabel("Number of states:"))
|
294
|
+
self.diffusion_header.addWidget(self.diffusion_count)
|
295
|
+
self.diffusion_header.addStretch()
|
296
|
+
|
297
|
+
self.diffusion_layout.addLayout(self.diffusion_header)
|
298
|
+
|
299
|
+
self.diffusion_grid = QGridLayout()
|
300
|
+
self.diffusion_grid.addWidget(QLabel("Coefficient (μm²/s)"), 0, 0)
|
301
|
+
self.diffusion_grid.addWidget(
|
302
|
+
QLabel("Initial Fraction in this State (0-1)"), 0, 1
|
303
|
+
)
|
304
|
+
|
305
|
+
self.diffusion_coefficients = []
|
306
|
+
self.diffusion_amounts = []
|
307
|
+
|
308
|
+
self.update_diffusion_coefficients(1)
|
309
|
+
self.diffusion_layout.addLayout(self.diffusion_grid)
|
310
|
+
|
311
|
+
# Transition matrix for diffusion
|
312
|
+
self.diffusion_matrix_widget = TransitionMatrixWidget("Diffusion Coefficient")
|
313
|
+
self.diffusion_layout.addWidget(self.diffusion_matrix_widget)
|
314
|
+
|
315
|
+
self.layout.addWidget(self.diffusion_group)
|
316
|
+
|
317
|
+
# Hurst exponent section (visible only for fbm)
|
318
|
+
self.hurst_group = QWidget()
|
319
|
+
self.hurst_layout = QVBoxLayout(self.hurst_group)
|
320
|
+
|
321
|
+
self.hurst_header = QHBoxLayout()
|
322
|
+
self.hurst_label = QLabel("<b>Hurst Exponents</b>")
|
323
|
+
self.hurst_count = QSpinBox()
|
324
|
+
self.hurst_count.setRange(1, 10) # set minimum to 1
|
325
|
+
self.hurst_count.setValue(1)
|
326
|
+
self.hurst_count.valueChanged.connect(self.update_hurst_exponents)
|
327
|
+
self.hurst_header.addWidget(self.hurst_label)
|
328
|
+
self.hurst_header.addWidget(QLabel("Number of states:"))
|
329
|
+
self.hurst_header.addWidget(self.hurst_count)
|
330
|
+
self.hurst_header.addStretch()
|
331
|
+
|
332
|
+
self.hurst_layout.addLayout(self.hurst_header)
|
333
|
+
|
334
|
+
self.hurst_grid = QGridLayout()
|
335
|
+
self.hurst_grid.addWidget(QLabel("Exponent (0-1)"), 0, 0)
|
336
|
+
self.hurst_grid.addWidget(QLabel("Initial Fraction in this State (0-1)"), 0, 1)
|
337
|
+
|
338
|
+
self.hurst_exponents = []
|
339
|
+
self.hurst_amounts = []
|
340
|
+
|
341
|
+
self.update_hurst_exponents(1)
|
342
|
+
self.hurst_layout.addLayout(self.hurst_grid)
|
343
|
+
|
344
|
+
# Transition matrix for Hurst
|
345
|
+
self.hurst_matrix_widget = TransitionMatrixWidget("Hurst Exponent")
|
346
|
+
self.hurst_layout.addWidget(self.hurst_matrix_widget)
|
347
|
+
|
348
|
+
self.layout.addWidget(self.hurst_group)
|
349
|
+
|
350
|
+
# Set the container as the scroll area widget
|
351
|
+
self.scroll.setWidget(self.container)
|
352
|
+
|
353
|
+
# Main layout for this widget
|
354
|
+
main_layout = QVBoxLayout(self)
|
355
|
+
main_layout.addWidget(self.scroll)
|
356
|
+
|
357
|
+
# Connect signals for dependency updates
|
358
|
+
self.diffusion_count.valueChanged.connect(
|
359
|
+
lambda val: self.diffusion_matrix_widget.update_matrix_size(val)
|
360
|
+
)
|
361
|
+
self.hurst_count.valueChanged.connect(
|
362
|
+
lambda val: self.hurst_matrix_widget.update_matrix_size(val)
|
363
|
+
)
|
364
|
+
|
365
|
+
# Update visibility based on initial values
|
366
|
+
self.update_visibility()
|
367
|
+
|
368
|
+
def update_visibility(self):
|
369
|
+
track = self.track_type.currentText()
|
370
|
+
is_fbm = track == "fbm"
|
371
|
+
is_constant = track == "constant"
|
372
|
+
allow_transitions = self.allow_transition.isChecked()
|
373
|
+
|
374
|
+
# Hide/show all motion-related groups
|
375
|
+
self.diffusion_group.setVisible(not is_constant)
|
376
|
+
self.hurst_group.setVisible(is_fbm and not is_constant)
|
377
|
+
|
378
|
+
# Hide/show allow_transition checkbox and transition step field
|
379
|
+
self.allow_transition.setVisible(not is_constant)
|
380
|
+
label = self.form_layout.labelForField(self.transition_time_step)
|
381
|
+
if label:
|
382
|
+
label.setVisible(not is_constant and allow_transitions)
|
383
|
+
self.transition_time_step.setVisible(not is_constant and allow_transitions)
|
384
|
+
|
385
|
+
# Transition matrices visibility
|
386
|
+
self.diffusion_matrix_widget.setVisible(not is_constant and allow_transitions)
|
387
|
+
self.hurst_matrix_widget.setVisible(
|
388
|
+
not is_constant and is_fbm and allow_transitions
|
389
|
+
)
|
390
|
+
|
391
|
+
def update_diffusion_coefficients(self, count):
|
392
|
+
# Store current total amount
|
393
|
+
current_total = (
|
394
|
+
sum(spin.value() for spin in self.diffusion_amounts)
|
395
|
+
if self.diffusion_amounts
|
396
|
+
else 1.0
|
397
|
+
)
|
398
|
+
|
399
|
+
# Clear existing items
|
400
|
+
while len(self.diffusion_coefficients) > count:
|
401
|
+
# Remove the last row
|
402
|
+
self.diffusion_grid.removeWidget(self.diffusion_coefficients[-1])
|
403
|
+
self.diffusion_grid.removeWidget(self.diffusion_amounts[-1])
|
404
|
+
self.diffusion_coefficients[-1].deleteLater()
|
405
|
+
self.diffusion_amounts[-1].deleteLater()
|
406
|
+
self.diffusion_coefficients.pop()
|
407
|
+
self.diffusion_amounts.pop()
|
408
|
+
|
409
|
+
# Add new items if needed
|
410
|
+
while len(self.diffusion_coefficients) < count:
|
411
|
+
row = len(self.diffusion_coefficients) + 1
|
412
|
+
coeff = QDoubleSpinBox()
|
413
|
+
coeff.setRange(0, 1000)
|
414
|
+
coeff.setValue(1.0)
|
415
|
+
coeff.setSuffix(" μm²/s")
|
416
|
+
coeff.setDecimals(3)
|
417
|
+
|
418
|
+
amount = QDoubleSpinBox()
|
419
|
+
amount.setRange(0, 1)
|
420
|
+
amount.setValue(1.0 / count) # Default value will be adjusted later
|
421
|
+
amount.setSingleStep(0.1)
|
422
|
+
amount.setDecimals(2)
|
423
|
+
amount.valueChanged.connect(self.normalize_diffusion_amounts)
|
424
|
+
|
425
|
+
self.diffusion_grid.addWidget(coeff, row, 0)
|
426
|
+
self.diffusion_grid.addWidget(amount, row, 1)
|
427
|
+
|
428
|
+
self.diffusion_coefficients.append(coeff)
|
429
|
+
self.diffusion_amounts.append(amount)
|
430
|
+
|
431
|
+
# Normalize the amounts to sum to 1
|
432
|
+
self.normalize_diffusion_amounts()
|
433
|
+
|
434
|
+
def normalize_diffusion_amounts(self):
|
435
|
+
# Get the sender (which spin box was changed)
|
436
|
+
sender = self.sender()
|
437
|
+
|
438
|
+
# Skip if this wasn't triggered by a spin box change
|
439
|
+
if not sender or sender not in self.diffusion_amounts:
|
440
|
+
# Equal distribution
|
441
|
+
if self.diffusion_amounts:
|
442
|
+
for amount in self.diffusion_amounts:
|
443
|
+
amount.blockSignals(True)
|
444
|
+
amount.setValue(1.0 / len(self.diffusion_amounts))
|
445
|
+
amount.blockSignals(False)
|
446
|
+
return
|
447
|
+
|
448
|
+
# Get total and adjust other values proportionally
|
449
|
+
total = sum(spin.value() for spin in self.diffusion_amounts)
|
450
|
+
|
451
|
+
if total > 0:
|
452
|
+
# If total > 1, scale down others
|
453
|
+
if total > 1.0:
|
454
|
+
# How much we need to reduce by
|
455
|
+
excess = total - 1.0
|
456
|
+
|
457
|
+
# Get the sum of other amounts
|
458
|
+
other_sum = total - sender.value()
|
459
|
+
|
460
|
+
# Adjust each amount proportionally
|
461
|
+
for amount in self.diffusion_amounts:
|
462
|
+
if amount != sender:
|
463
|
+
if other_sum > 0:
|
464
|
+
# Reduce proportionally
|
465
|
+
amount.blockSignals(True)
|
466
|
+
new_value = max(
|
467
|
+
0,
|
468
|
+
amount.value()
|
469
|
+
- (excess * (amount.value() / other_sum)),
|
470
|
+
)
|
471
|
+
amount.setValue(new_value)
|
472
|
+
amount.blockSignals(False)
|
473
|
+
else:
|
474
|
+
# If other sum is 0, we can't adjust proportionally
|
475
|
+
amount.blockSignals(True)
|
476
|
+
amount.setValue(0)
|
477
|
+
amount.blockSignals(False)
|
478
|
+
|
479
|
+
def update_hurst_exponents(self, count):
|
480
|
+
# Store current total amount
|
481
|
+
current_total = (
|
482
|
+
sum(spin.value() for spin in self.hurst_amounts)
|
483
|
+
if self.hurst_amounts
|
484
|
+
else 1.0
|
485
|
+
)
|
486
|
+
|
487
|
+
# Clear existing items
|
488
|
+
while len(self.hurst_exponents) > count:
|
489
|
+
# Remove the last row
|
490
|
+
self.hurst_grid.removeWidget(self.hurst_exponents[-1])
|
491
|
+
self.hurst_grid.removeWidget(self.hurst_amounts[-1])
|
492
|
+
self.hurst_exponents[-1].deleteLater()
|
493
|
+
self.hurst_amounts[-1].deleteLater()
|
494
|
+
self.hurst_exponents.pop()
|
495
|
+
self.hurst_amounts.pop()
|
496
|
+
|
497
|
+
# Add new items if needed
|
498
|
+
while len(self.hurst_exponents) < count:
|
499
|
+
row = len(self.hurst_exponents) + 1
|
500
|
+
exp = QDoubleSpinBox()
|
501
|
+
exp.setRange(0, 1)
|
502
|
+
exp.setValue(0.5)
|
503
|
+
exp.setSingleStep(0.1)
|
504
|
+
exp.setDecimals(2)
|
505
|
+
|
506
|
+
amount = QDoubleSpinBox()
|
507
|
+
amount.setRange(0, 1)
|
508
|
+
amount.setValue(1.0 / count) # Default value will be adjusted later
|
509
|
+
amount.setSingleStep(0.1)
|
510
|
+
amount.setDecimals(2)
|
511
|
+
amount.valueChanged.connect(self.normalize_hurst_amounts)
|
512
|
+
|
513
|
+
self.hurst_grid.addWidget(exp, row, 0)
|
514
|
+
self.hurst_grid.addWidget(amount, row, 1)
|
515
|
+
|
516
|
+
self.hurst_exponents.append(exp)
|
517
|
+
self.hurst_amounts.append(amount)
|
518
|
+
|
519
|
+
# Normalize the amounts to sum to 1
|
520
|
+
self.normalize_hurst_amounts()
|
521
|
+
|
522
|
+
def normalize_hurst_amounts(self):
|
523
|
+
# Get the sender (which spin box was changed)
|
524
|
+
sender = self.sender()
|
525
|
+
|
526
|
+
# Skip if this wasn't triggered by a spin box change
|
527
|
+
if not sender or sender not in self.hurst_amounts:
|
528
|
+
# Equal distribution
|
529
|
+
if self.hurst_amounts:
|
530
|
+
for amount in self.hurst_amounts:
|
531
|
+
amount.blockSignals(True)
|
532
|
+
amount.setValue(1.0 / len(self.hurst_amounts))
|
533
|
+
amount.blockSignals(False)
|
534
|
+
return
|
535
|
+
|
536
|
+
# Get total and adjust other values proportionally
|
537
|
+
total = sum(spin.value() for spin in self.hurst_amounts)
|
538
|
+
|
539
|
+
if total > 0:
|
540
|
+
# If total > 1, scale down others
|
541
|
+
if total > 1.0:
|
542
|
+
# How much we need to reduce by
|
543
|
+
excess = total - 1.0
|
544
|
+
|
545
|
+
# Get the sum of other amounts
|
546
|
+
other_sum = total - sender.value()
|
547
|
+
|
548
|
+
# Adjust each amount proportionally
|
549
|
+
for amount in self.hurst_amounts:
|
550
|
+
if amount != sender:
|
551
|
+
if other_sum > 0:
|
552
|
+
# Reduce proportionally
|
553
|
+
amount.blockSignals(True)
|
554
|
+
new_value = max(
|
555
|
+
0,
|
556
|
+
amount.value()
|
557
|
+
- (excess * (amount.value() / other_sum)),
|
558
|
+
)
|
559
|
+
amount.setValue(new_value)
|
560
|
+
amount.blockSignals(False)
|
561
|
+
else:
|
562
|
+
# If other sum is 0, we can't adjust proportionally
|
563
|
+
amount.blockSignals(True)
|
564
|
+
amount.setValue(0)
|
565
|
+
amount.blockSignals(False)
|
566
|
+
|
567
|
+
def get_data(self):
|
568
|
+
# Get diffusion coefficients and state probabilities
|
569
|
+
diff_coeff = [spin.value() for spin in self.diffusion_coefficients]
|
570
|
+
diff_prob = [spin.value() for spin in self.diffusion_amounts]
|
571
|
+
|
572
|
+
# Get Hurst exponents and state probabilities (only if visible)
|
573
|
+
hurst_exp = (
|
574
|
+
[spin.value() for spin in self.hurst_exponents]
|
575
|
+
if self.hurst_group.isVisible()
|
576
|
+
else []
|
577
|
+
)
|
578
|
+
hurst_prob = (
|
579
|
+
[spin.value() for spin in self.hurst_amounts]
|
580
|
+
if self.hurst_group.isVisible()
|
581
|
+
else []
|
582
|
+
)
|
583
|
+
|
584
|
+
# Transition matrices
|
585
|
+
diff_matrix = self.diffusion_matrix_widget.get_matrix()
|
586
|
+
hurst_matrix = (
|
587
|
+
self.hurst_matrix_widget.get_matrix()
|
588
|
+
if self.hurst_group.isVisible()
|
589
|
+
else []
|
590
|
+
)
|
591
|
+
|
592
|
+
return {
|
593
|
+
"num_molecules": self.num_molecules.value(),
|
594
|
+
"track_type": self.track_type.currentText(),
|
595
|
+
"diffusion_coefficient": diff_coeff,
|
596
|
+
"hurst_exponent": hurst_exp,
|
597
|
+
"allow_transition_probability": self.allow_transition.isChecked(),
|
598
|
+
"transition_matrix_time_step": self.transition_time_step.value(),
|
599
|
+
"diffusion_transition_matrix": diff_matrix,
|
600
|
+
"hurst_transition_matrix": hurst_matrix,
|
601
|
+
"state_probability_diffusion": diff_prob,
|
602
|
+
"state_probability_hurst": hurst_prob,
|
603
|
+
}
|
604
|
+
|
605
|
+
def set_data(self, data):
|
606
|
+
self.num_molecules.setValue(data["num_molecules"])
|
607
|
+
|
608
|
+
index = self.track_type.findText(data["track_type"])
|
609
|
+
if index >= 0:
|
610
|
+
self.track_type.setCurrentIndex(index)
|
611
|
+
|
612
|
+
self.allow_transition.setChecked(data["allow_transition_probability"])
|
613
|
+
self.transition_time_step.setValue(data["transition_matrix_time_step"])
|
614
|
+
|
615
|
+
# Diffusion Coefficients
|
616
|
+
diff_count = len(data["diffusion_coefficient"])
|
617
|
+
self.diffusion_count.setValue(diff_count)
|
618
|
+
for i in range(diff_count):
|
619
|
+
if i < len(self.diffusion_coefficients):
|
620
|
+
self.diffusion_coefficients[i].setValue(
|
621
|
+
data["diffusion_coefficient"][i]
|
622
|
+
)
|
623
|
+
self.diffusion_amounts[i].setValue(
|
624
|
+
data["state_probability_diffusion"][i]
|
625
|
+
)
|
626
|
+
|
627
|
+
# Hurst Exponents (only if track_type == "fbm")
|
628
|
+
if data["track_type"] == "fbm":
|
629
|
+
hurst_count = max(1, len(data["hurst_exponent"])) # ensure at least 1
|
630
|
+
self.hurst_count.setValue(hurst_count)
|
631
|
+
for i in range(hurst_count):
|
632
|
+
if i < len(self.hurst_exponents):
|
633
|
+
self.hurst_exponents[i].setValue(data["hurst_exponent"][i])
|
634
|
+
self.hurst_amounts[i].setValue(data["state_probability_hurst"][i])
|
635
|
+
|
636
|
+
self.hurst_matrix_widget.set_matrix(data["hurst_transition_matrix"])
|
637
|
+
else:
|
638
|
+
self.hurst_count.setValue(0)
|
639
|
+
|
640
|
+
# Set diffusion matrix
|
641
|
+
self.diffusion_matrix_widget.set_matrix(data["diffusion_transition_matrix"])
|
642
|
+
|
643
|
+
# Refresh UI visibility
|
644
|
+
self.update_visibility()
|
645
|
+
|
646
|
+
|
647
|
+
class TransitionMatrixWidget(QWidget):
|
648
|
+
def __init__(self, title):
|
649
|
+
super().__init__()
|
650
|
+
self.layout = QVBoxLayout(self)
|
651
|
+
|
652
|
+
self.layout.addWidget(QLabel(f"<b>{title} Transition Matrix</b>"))
|
653
|
+
self.layout.addWidget(
|
654
|
+
QLabel(
|
655
|
+
"Probabilities of transitioning between states (rows must sum to 1.0):"
|
656
|
+
)
|
657
|
+
)
|
658
|
+
|
659
|
+
self.grid_container = QWidget()
|
660
|
+
self.grid_layout = QGridLayout(self.grid_container)
|
661
|
+
self.layout.addWidget(self.grid_container)
|
662
|
+
|
663
|
+
self.spinboxes = []
|
664
|
+
self.update_matrix_size(1)
|
665
|
+
|
666
|
+
def update_matrix_size(self, size):
|
667
|
+
# Clear existing grid
|
668
|
+
while self.grid_layout.count():
|
669
|
+
item = self.grid_layout.takeAt(0)
|
670
|
+
if item.widget():
|
671
|
+
item.widget().deleteLater()
|
672
|
+
|
673
|
+
self.spinboxes = []
|
674
|
+
|
675
|
+
# Add column headers (to state)
|
676
|
+
for i in range(size):
|
677
|
+
self.grid_layout.addWidget(QLabel(f"To {i+1}"), 0, i + 1)
|
678
|
+
|
679
|
+
# Add row headers (from state)
|
680
|
+
for i in range(size):
|
681
|
+
self.grid_layout.addWidget(QLabel(f"From {i+1}"), i + 1, 0)
|
682
|
+
|
683
|
+
# Create transition probability spinboxes
|
684
|
+
for i in range(size):
|
685
|
+
row = []
|
686
|
+
for j in range(size):
|
687
|
+
spin = QDoubleSpinBox()
|
688
|
+
spin.setRange(0, 1)
|
689
|
+
spin.setValue(
|
690
|
+
1.0 / size if i == j else 0.0
|
691
|
+
) # Default to self-transitions
|
692
|
+
spin.setSingleStep(0.1)
|
693
|
+
spin.setDecimals(2)
|
694
|
+
self.grid_layout.addWidget(spin, i + 1, j + 1)
|
695
|
+
row.append(spin)
|
696
|
+
self.spinboxes.append(row)
|
697
|
+
|
698
|
+
def get_matrix(self):
|
699
|
+
matrix = []
|
700
|
+
for row in self.spinboxes:
|
701
|
+
matrix.append([spin.value() for spin in row])
|
702
|
+
return matrix
|
703
|
+
|
704
|
+
def set_matrix(self, matrix):
|
705
|
+
size = len(matrix)
|
706
|
+
if size != len(self.spinboxes):
|
707
|
+
self.update_matrix_size(size)
|
708
|
+
|
709
|
+
for i in range(size):
|
710
|
+
for j in range(min(size, len(matrix[i]))):
|
711
|
+
self.spinboxes[i][j].setValue(matrix[i][j])
|
712
|
+
|
713
|
+
def get_help_path(self) -> Path:
|
714
|
+
return Path(__file__).parent.parent / "help_docs" / "molecule_help.md"
|