waveorder 2.2.1__py3-none-any.whl → 3.0.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.
- waveorder/_version.py +16 -3
- waveorder/acq/__init__.py +0 -0
- waveorder/acq/acq_functions.py +166 -0
- waveorder/assets/HSV_legend.png +0 -0
- waveorder/assets/JCh_legend.png +0 -0
- waveorder/assets/waveorder_plugin_logo.png +0 -0
- waveorder/calib/Calibration.py +1512 -0
- waveorder/calib/Optimization.py +470 -0
- waveorder/calib/__init__.py +0 -0
- waveorder/calib/calibration_workers.py +464 -0
- waveorder/cli/apply_inverse_models.py +328 -0
- waveorder/cli/apply_inverse_transfer_function.py +379 -0
- waveorder/cli/compute_transfer_function.py +432 -0
- waveorder/cli/gui_widget.py +58 -0
- waveorder/cli/main.py +39 -0
- waveorder/cli/monitor.py +163 -0
- waveorder/cli/option_eat_all.py +47 -0
- waveorder/cli/parsing.py +122 -0
- waveorder/cli/printing.py +16 -0
- waveorder/cli/reconstruct.py +67 -0
- waveorder/cli/settings.py +187 -0
- waveorder/cli/utils.py +175 -0
- waveorder/filter.py +1 -2
- waveorder/focus.py +136 -25
- waveorder/io/__init__.py +0 -0
- waveorder/io/_reader.py +61 -0
- waveorder/io/core_functions.py +272 -0
- waveorder/io/metadata_reader.py +195 -0
- waveorder/io/utils.py +175 -0
- waveorder/io/visualization.py +160 -0
- waveorder/models/inplane_oriented_thick_pol3d_vector.py +3 -3
- waveorder/models/isotropic_fluorescent_thick_3d.py +92 -0
- waveorder/models/isotropic_fluorescent_thin_3d.py +331 -0
- waveorder/models/isotropic_thin_3d.py +73 -72
- waveorder/models/phase_thick_3d.py +103 -4
- waveorder/napari.yaml +36 -0
- waveorder/plugin/__init__.py +9 -0
- waveorder/plugin/gui.py +1094 -0
- waveorder/plugin/gui.ui +1440 -0
- waveorder/plugin/job_manager.py +42 -0
- waveorder/plugin/main_widget.py +1605 -0
- waveorder/plugin/tab_recon.py +3294 -0
- waveorder/scripts/__init__.py +0 -0
- waveorder/scripts/launch_napari.py +13 -0
- waveorder/scripts/repeat-cal-acq-rec.py +147 -0
- waveorder/scripts/repeat-calibration.py +31 -0
- waveorder/scripts/samples.py +85 -0
- waveorder/scripts/simulate_zarr_acq.py +204 -0
- waveorder/util.py +1 -1
- waveorder/visuals/napari_visuals.py +1 -1
- waveorder-3.0.0.dist-info/METADATA +350 -0
- waveorder-3.0.0.dist-info/RECORD +69 -0
- {waveorder-2.2.1.dist-info → waveorder-3.0.0.dist-info}/WHEEL +1 -1
- waveorder-3.0.0.dist-info/entry_points.txt +5 -0
- {waveorder-2.2.1.dist-info → waveorder-3.0.0.dist-info}/licenses/LICENSE +13 -1
- waveorder-2.2.1.dist-info/METADATA +0 -188
- waveorder-2.2.1.dist-info/RECORD +0 -27
- {waveorder-2.2.1.dist-info → waveorder-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3294 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import types
|
|
8
|
+
import uuid
|
|
9
|
+
import warnings
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Annotated,
|
|
14
|
+
Any,
|
|
15
|
+
Final,
|
|
16
|
+
List,
|
|
17
|
+
Literal,
|
|
18
|
+
Union,
|
|
19
|
+
get_args,
|
|
20
|
+
get_origin,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from iohub.ngff import open_ome_zarr
|
|
24
|
+
from magicgui import widgets
|
|
25
|
+
from magicgui.type_map import get_widget_class
|
|
26
|
+
|
|
27
|
+
# FIXME avoid star import
|
|
28
|
+
# Since we are instantiating GUI widgets/elements based on pydantic model
|
|
29
|
+
# star import provides that flexibility
|
|
30
|
+
from magicgui.widgets import *
|
|
31
|
+
from napari.utils.notifications import show_error, show_info
|
|
32
|
+
from qtpy import QtCore
|
|
33
|
+
from qtpy.QtCore import QEvent, Qt, QThread, Signal
|
|
34
|
+
from qtpy.QtWidgets import *
|
|
35
|
+
|
|
36
|
+
from waveorder.plugin import job_manager
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from napari import Viewer
|
|
40
|
+
|
|
41
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
42
|
+
|
|
43
|
+
from pydantic import BaseModel, NonNegativeInt, ValidationError
|
|
44
|
+
from pydantic_core import PydanticUndefinedType
|
|
45
|
+
|
|
46
|
+
from waveorder.cli.settings import (
|
|
47
|
+
BirefringenceApplyInverseSettings,
|
|
48
|
+
BirefringenceSettings,
|
|
49
|
+
BirefringenceTransferFunctionSettings,
|
|
50
|
+
FluorescenceSettings,
|
|
51
|
+
FluorescenceTransferFunctionSettings,
|
|
52
|
+
FourierApplyInverseSettings,
|
|
53
|
+
PhaseSettings,
|
|
54
|
+
PhaseTransferFunctionSettings,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
PYDANTIC_CLASSES_DEF = (
|
|
58
|
+
BirefringenceSettings,
|
|
59
|
+
BirefringenceTransferFunctionSettings,
|
|
60
|
+
BirefringenceApplyInverseSettings,
|
|
61
|
+
PhaseSettings,
|
|
62
|
+
PhaseTransferFunctionSettings,
|
|
63
|
+
FluorescenceSettings,
|
|
64
|
+
FluorescenceTransferFunctionSettings,
|
|
65
|
+
FourierApplyInverseSettings,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
from waveorder.cli import settings
|
|
69
|
+
from waveorder.io import utils
|
|
70
|
+
|
|
71
|
+
MSG_SUCCESS = {"msg": "success"}
|
|
72
|
+
|
|
73
|
+
_validate_alert = "⚠️"
|
|
74
|
+
_validate_ok = "✅"
|
|
75
|
+
_green_dot = "🟢"
|
|
76
|
+
_red_dot = "🔴"
|
|
77
|
+
_info_icon = "ℹ️"
|
|
78
|
+
|
|
79
|
+
# For now replicate CLI processing modes - these could reside in the CLI settings file as well
|
|
80
|
+
# for consistency
|
|
81
|
+
OPTION_TO_MODEL_DICT = {
|
|
82
|
+
"birefringence": {"enabled": False, "setting": None},
|
|
83
|
+
"phase": {"enabled": False, "setting": None},
|
|
84
|
+
"fluorescence": {"enabled": False, "setting": None},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
CONTAINERS_INFO = {}
|
|
88
|
+
|
|
89
|
+
# This keeps an instance of the MyWorker class
|
|
90
|
+
# napari will not stop processes and the Hide event is not reliable
|
|
91
|
+
HAS_INSTANCE = {"val": False, "instance": None}
|
|
92
|
+
|
|
93
|
+
# Components Queue list for new Jobs spanned from single processing
|
|
94
|
+
NEW_WIDGETS_QUEUE = []
|
|
95
|
+
NEW_WIDGETS_QUEUE_THREADS = []
|
|
96
|
+
MULTI_JOBS_REFS = {}
|
|
97
|
+
ROW_POP_QUEUE = []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def unwrap_optional(ftype: Any) -> Any:
|
|
101
|
+
"""Unwrap Optional[X] to get X. Handles both Optional[X] and X | None syntax.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
ftype: A type annotation
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The unwrapped type, or ftype unchanged if not Optional
|
|
108
|
+
"""
|
|
109
|
+
origin = get_origin(ftype)
|
|
110
|
+
if origin is Union or isinstance(ftype, types.UnionType):
|
|
111
|
+
args = [a for a in get_args(ftype) if a is not type(None)]
|
|
112
|
+
if len(args) == 1:
|
|
113
|
+
return args[0]
|
|
114
|
+
return ftype
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_subclass_of(
|
|
118
|
+
ftype: Any,
|
|
119
|
+
base: type | tuple[type, ...],
|
|
120
|
+
*,
|
|
121
|
+
require_optional: bool = False,
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Check if ftype (possibly Optional-wrapped) is a subclass of base.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
ftype: A type annotation
|
|
127
|
+
base: The base type(s) to check against (can be tuple for multiple)
|
|
128
|
+
require_optional: If True, only return True if ftype was Optional-wrapped
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if the (unwrapped) ftype is a subclass of base
|
|
132
|
+
"""
|
|
133
|
+
inner = unwrap_optional(ftype)
|
|
134
|
+
if require_optional and inner is ftype:
|
|
135
|
+
return False
|
|
136
|
+
# Handle Annotated types by unwrapping them
|
|
137
|
+
if get_origin(inner) is Annotated:
|
|
138
|
+
inner = get_args(inner)[0]
|
|
139
|
+
return isinstance(inner, type) and issubclass(inner, base)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Main class for the Reconstruction tab
|
|
143
|
+
# Not efficient since instantiated from GUI
|
|
144
|
+
# Does not have access to common functions in main_widget
|
|
145
|
+
# ToDo : From main_widget and pass self reference
|
|
146
|
+
class Ui_ReconTab_Form(QWidget):
|
|
147
|
+
|
|
148
|
+
def __init__(self, parent=None, stand_alone=False):
|
|
149
|
+
super().__init__(parent)
|
|
150
|
+
self._ui = parent
|
|
151
|
+
self.stand_alone = stand_alone
|
|
152
|
+
self.viewer: Viewer = None
|
|
153
|
+
if HAS_INSTANCE["val"]:
|
|
154
|
+
self.current_dir_path = str(Path.cwd())
|
|
155
|
+
self.directory = str(Path.cwd())
|
|
156
|
+
self.input_directory = HAS_INSTANCE["input_directory"]
|
|
157
|
+
self.output_directory = HAS_INSTANCE["output_directory"]
|
|
158
|
+
self.model_directory = HAS_INSTANCE["model_directory"]
|
|
159
|
+
self.yaml_model_file = HAS_INSTANCE["yaml_model_file"]
|
|
160
|
+
else:
|
|
161
|
+
self.directory = str(Path.cwd())
|
|
162
|
+
self.current_dir_path = str(Path.cwd())
|
|
163
|
+
self.input_directory = str(Path.cwd())
|
|
164
|
+
self.output_directory = str(Path.cwd())
|
|
165
|
+
self.model_directory = str(Path.cwd())
|
|
166
|
+
self.yaml_model_file = str(Path.cwd())
|
|
167
|
+
|
|
168
|
+
self.input_directory_dataset = None
|
|
169
|
+
self.input_directory_datasetMeta = None
|
|
170
|
+
self.input_channel_names = []
|
|
171
|
+
|
|
172
|
+
# Parent (Widget) which holds the GUI ##############################
|
|
173
|
+
self.recon_tab_mainScrollArea = QScrollArea()
|
|
174
|
+
self.recon_tab_mainScrollArea.setWidgetResizable(True)
|
|
175
|
+
|
|
176
|
+
self.recon_tab_widget = QWidget()
|
|
177
|
+
self.recon_tab_widget.setSizePolicy(
|
|
178
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
179
|
+
)
|
|
180
|
+
self.recon_tab_layout = QVBoxLayout()
|
|
181
|
+
self.recon_tab_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
|
182
|
+
self.recon_tab_layout.setContentsMargins(0, 0, 0, 0)
|
|
183
|
+
self.recon_tab_layout.setSpacing(0)
|
|
184
|
+
self.recon_tab_widget.setLayout(self.recon_tab_layout)
|
|
185
|
+
self.recon_tab_mainScrollArea.setWidget(self.recon_tab_widget)
|
|
186
|
+
|
|
187
|
+
# Top Section Group - Data ##############################
|
|
188
|
+
group_box_Data_groupBox_widget = QGroupBox("Data")
|
|
189
|
+
group_box_Data_layout = QVBoxLayout()
|
|
190
|
+
group_box_Data_layout.setContentsMargins(0, 5, 0, 0)
|
|
191
|
+
group_box_Data_layout.setSpacing(0)
|
|
192
|
+
group_box_Data_groupBox_widget.setLayout(group_box_Data_layout)
|
|
193
|
+
|
|
194
|
+
# Input Data ##############################
|
|
195
|
+
self.data_input_widget = QWidget()
|
|
196
|
+
self.data_input_widget_layout = QHBoxLayout()
|
|
197
|
+
self.data_input_widget_layout.setAlignment(
|
|
198
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
199
|
+
)
|
|
200
|
+
self.data_input_widget.setLayout(self.data_input_widget_layout)
|
|
201
|
+
|
|
202
|
+
self.data_input_Label = widgets.Label(value="Input Store")
|
|
203
|
+
# self.data_input_Label.native.setMinimumWidth(97)
|
|
204
|
+
self.data_input_LineEdit = widgets.LineEdit(value=self.input_directory)
|
|
205
|
+
self.data_input_PushButton = widgets.PushButton(label="Browse")
|
|
206
|
+
# self.data_input_PushButton.native.setMinimumWidth(75)
|
|
207
|
+
self.data_input_PushButton.clicked.connect(self.browse_dir_path_input)
|
|
208
|
+
self.data_input_LineEdit.changed.connect(
|
|
209
|
+
self.read_and_set_input_path_on_validation
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self.data_input_widget_layout.addWidget(self.data_input_Label.native)
|
|
213
|
+
self.data_input_widget_layout.addWidget(
|
|
214
|
+
self.data_input_LineEdit.native
|
|
215
|
+
)
|
|
216
|
+
self.data_input_widget_layout.addWidget(
|
|
217
|
+
self.data_input_PushButton.native
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Output Data ##############################
|
|
221
|
+
self.data_output_widget = QWidget()
|
|
222
|
+
self.data_output_widget_layout = QHBoxLayout()
|
|
223
|
+
self.data_output_widget_layout.setAlignment(
|
|
224
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
225
|
+
)
|
|
226
|
+
self.data_output_widget.setLayout(self.data_output_widget_layout)
|
|
227
|
+
|
|
228
|
+
self.data_output_Label = widgets.Label(value="Output Directory")
|
|
229
|
+
self.data_output_LineEdit = widgets.LineEdit(
|
|
230
|
+
value=self.output_directory
|
|
231
|
+
)
|
|
232
|
+
self.data_output_PushButton = widgets.PushButton(label="Browse")
|
|
233
|
+
# self.data_output_PushButton.native.setMinimumWidth(75)
|
|
234
|
+
self.data_output_PushButton.clicked.connect(
|
|
235
|
+
self.browse_dir_path_output
|
|
236
|
+
)
|
|
237
|
+
self.data_output_LineEdit.changed.connect(
|
|
238
|
+
self.read_and_set_out_path_on_validation
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self.data_output_widget_layout.addWidget(self.data_output_Label.native)
|
|
242
|
+
self.data_output_widget_layout.addWidget(
|
|
243
|
+
self.data_output_LineEdit.native
|
|
244
|
+
)
|
|
245
|
+
self.data_output_widget_layout.addWidget(
|
|
246
|
+
self.data_output_PushButton.native
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.data_input_Label.native.setMinimumWidth(115)
|
|
250
|
+
self.data_output_Label.native.setMinimumWidth(115)
|
|
251
|
+
|
|
252
|
+
group_box_Data_layout.addWidget(self.data_input_widget)
|
|
253
|
+
group_box_Data_layout.addWidget(self.data_output_widget)
|
|
254
|
+
self.recon_tab_layout.addWidget(group_box_Data_groupBox_widget)
|
|
255
|
+
|
|
256
|
+
##################################
|
|
257
|
+
|
|
258
|
+
# Middle Section - Models ##############################
|
|
259
|
+
# Selection modes, New, Load, Clear
|
|
260
|
+
# Pydantic Models ScrollArea
|
|
261
|
+
|
|
262
|
+
group_box_Models_groupBox_widget = QGroupBox("Models")
|
|
263
|
+
group_box_Models_layout = QVBoxLayout()
|
|
264
|
+
group_box_Models_layout.setContentsMargins(0, 5, 0, 0)
|
|
265
|
+
group_box_Models_layout.setSpacing(0)
|
|
266
|
+
group_box_Models_groupBox_widget.setLayout(group_box_Models_layout)
|
|
267
|
+
|
|
268
|
+
self.models_widget = QWidget()
|
|
269
|
+
self.models_widget_layout = QHBoxLayout()
|
|
270
|
+
self.models_widget_layout.setAlignment(
|
|
271
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
272
|
+
)
|
|
273
|
+
self.models_widget.setLayout(self.models_widget_layout)
|
|
274
|
+
|
|
275
|
+
self.modes_selected = OPTION_TO_MODEL_DICT.copy()
|
|
276
|
+
|
|
277
|
+
# Make a copy of the Reconstruction settings mode, these will be used as template
|
|
278
|
+
for mode in self.modes_selected.keys():
|
|
279
|
+
self.modes_selected[mode]["setting"] = None
|
|
280
|
+
|
|
281
|
+
# Checkboxes for the modes to select single or combination of modes
|
|
282
|
+
for mode in self.modes_selected.keys():
|
|
283
|
+
self.modes_selected[mode]["Checkbox"] = widgets.Checkbox(
|
|
284
|
+
name=mode, label=mode
|
|
285
|
+
)
|
|
286
|
+
self.models_widget_layout.addWidget(
|
|
287
|
+
self.modes_selected[mode]["Checkbox"].native
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# PushButton to create a copy of the model - UI
|
|
291
|
+
self.models_new_PushButton = widgets.PushButton(label="New")
|
|
292
|
+
# self.models_new_PushButton.native.setMinimumWidth(100)
|
|
293
|
+
self.models_new_PushButton.clicked.connect(self.build_acq_contols)
|
|
294
|
+
|
|
295
|
+
self.models_load_PushButton = DropButton(text="Load", recon_tab=self)
|
|
296
|
+
# self.models_load_PushButton.setMinimumWidth(90)
|
|
297
|
+
|
|
298
|
+
# Passing model location label to model location selector
|
|
299
|
+
self.models_load_PushButton.clicked.connect(
|
|
300
|
+
lambda: self.browse_dir_path_model()
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# PushButton to clear all copies of models that are create for UI
|
|
304
|
+
self.models_clear_PushButton = widgets.PushButton(label="Clear")
|
|
305
|
+
# self.models_clear_PushButton.native.setMinimumWidth(110)
|
|
306
|
+
self.models_clear_PushButton.clicked.connect(self.clear_all_models)
|
|
307
|
+
|
|
308
|
+
self.models_widget_layout.addWidget(self.models_new_PushButton.native)
|
|
309
|
+
self.models_widget_layout.addWidget(self.models_load_PushButton)
|
|
310
|
+
self.models_widget_layout.addWidget(
|
|
311
|
+
self.models_clear_PushButton.native
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Middle scrollable component which will hold Editable/(vertical) Expanding UI
|
|
315
|
+
self.models_scrollArea = QScrollArea()
|
|
316
|
+
self.models_scrollArea.setWidgetResizable(True)
|
|
317
|
+
self.models_container_widget = DropWidget(self)
|
|
318
|
+
self.models_container_widget_layout = QVBoxLayout()
|
|
319
|
+
self.models_container_widget_layout.setContentsMargins(0, 0, 0, 0)
|
|
320
|
+
self.models_container_widget_layout.setSpacing(2)
|
|
321
|
+
self.models_container_widget_layout.setAlignment(
|
|
322
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
323
|
+
)
|
|
324
|
+
self.models_container_widget.setLayout(
|
|
325
|
+
self.models_container_widget_layout
|
|
326
|
+
)
|
|
327
|
+
self.models_scrollArea.setWidget(self.models_container_widget)
|
|
328
|
+
|
|
329
|
+
group_box_Models_layout.addWidget(self.models_widget)
|
|
330
|
+
group_box_Models_layout.addWidget(self.models_scrollArea)
|
|
331
|
+
|
|
332
|
+
##################################
|
|
333
|
+
|
|
334
|
+
# Create the splitter to resize Middle and Bottom Sections if required ##################################
|
|
335
|
+
splitter = QSplitter()
|
|
336
|
+
splitter.setOrientation(Qt.Orientation.Vertical)
|
|
337
|
+
splitter.setSizes([600, 200])
|
|
338
|
+
|
|
339
|
+
self.recon_tab_layout.addWidget(splitter)
|
|
340
|
+
|
|
341
|
+
# Reconstruction ##################################
|
|
342
|
+
# Run, Processing, On-The-Fly
|
|
343
|
+
group_box_Reconstruction_groupBox_widget = QGroupBox(
|
|
344
|
+
"Reconstruction Queue"
|
|
345
|
+
)
|
|
346
|
+
group_box_Reconstruction_layout = QVBoxLayout()
|
|
347
|
+
group_box_Reconstruction_layout.setContentsMargins(5, 10, 5, 5)
|
|
348
|
+
group_box_Reconstruction_layout.setSpacing(2)
|
|
349
|
+
group_box_Reconstruction_groupBox_widget.setLayout(
|
|
350
|
+
group_box_Reconstruction_layout
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
splitter.addWidget(group_box_Models_groupBox_widget)
|
|
354
|
+
splitter.addWidget(group_box_Reconstruction_groupBox_widget)
|
|
355
|
+
|
|
356
|
+
my_splitter_handle = splitter.handle(1)
|
|
357
|
+
my_splitter_handle.setStyleSheet("background: 1px rgb(128,128,128);")
|
|
358
|
+
splitter.setStyleSheet(
|
|
359
|
+
"""QSplitter::handle:pressed {background-color: #ca5;}"""
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# PushButton to validate and Run the yaml file(s) based on selection against the Input store
|
|
363
|
+
self.reconstruction_run_PushButton = widgets.PushButton(
|
|
364
|
+
name="RUN Model"
|
|
365
|
+
)
|
|
366
|
+
self.reconstruction_run_PushButton.native.setMinimumWidth(100)
|
|
367
|
+
self.reconstruction_run_PushButton.clicked.connect(
|
|
368
|
+
self.build_model_and_run
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
group_box_Reconstruction_layout.addWidget(
|
|
372
|
+
self.reconstruction_run_PushButton.native
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Tabs - Processing & On-The-Fly
|
|
376
|
+
tabs_Reconstruction = QTabWidget()
|
|
377
|
+
group_box_Reconstruction_layout.addWidget(tabs_Reconstruction)
|
|
378
|
+
|
|
379
|
+
# Table for Jobs processing entries
|
|
380
|
+
tab1_processing_widget = QWidget()
|
|
381
|
+
tab1_processing_widget_layout = QVBoxLayout()
|
|
382
|
+
tab1_processing_widget_layout.setContentsMargins(5, 5, 5, 5)
|
|
383
|
+
tab1_processing_widget_layout.setSpacing(2)
|
|
384
|
+
tab1_processing_widget.setLayout(tab1_processing_widget_layout)
|
|
385
|
+
self.proc_table_QFormLayout = QFormLayout()
|
|
386
|
+
self.proc_table_QFormLayout.setAlignment(
|
|
387
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
388
|
+
)
|
|
389
|
+
tab1_processing_form_widget = QWidget()
|
|
390
|
+
tab1_processing_form_widget.setSizePolicy(
|
|
391
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
392
|
+
)
|
|
393
|
+
tab1_processing_form_widget.setLayout(self.proc_table_QFormLayout)
|
|
394
|
+
tab1_processing_widget_layout.addWidget(tab1_processing_form_widget)
|
|
395
|
+
|
|
396
|
+
_clear_results_btn = widgets.PushButton(label="Clear Results")
|
|
397
|
+
_clear_results_btn.clicked.connect(self.clear_results_table)
|
|
398
|
+
tab1_processing_widget_layout.addWidget(_clear_results_btn.native)
|
|
399
|
+
|
|
400
|
+
# Table for On-The-Fly processing entries
|
|
401
|
+
tab2_processing_widget = QWidget()
|
|
402
|
+
tab2_processing_widget_layout = QVBoxLayout()
|
|
403
|
+
tab2_processing_widget_layout.setContentsMargins(0, 0, 0, 0)
|
|
404
|
+
tab2_processing_widget_layout.setSpacing(0)
|
|
405
|
+
tab2_processing_widget.setLayout(tab2_processing_widget_layout)
|
|
406
|
+
self.proc_OTF_table_QFormLayout = QFormLayout()
|
|
407
|
+
self.proc_OTF_table_QFormLayout.setAlignment(
|
|
408
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
409
|
+
)
|
|
410
|
+
_proc_OTF_table_widget = QWidget()
|
|
411
|
+
_proc_OTF_table_widget.setSizePolicy(
|
|
412
|
+
QSizePolicy.Expanding, QSizePolicy.Expanding
|
|
413
|
+
)
|
|
414
|
+
_proc_OTF_table_widget.setLayout(self.proc_OTF_table_QFormLayout)
|
|
415
|
+
tab2_processing_widget_layout.addWidget(_proc_OTF_table_widget)
|
|
416
|
+
tab2_processing_widget.setMaximumHeight(100)
|
|
417
|
+
|
|
418
|
+
tabs_Reconstruction.addTab(tab1_processing_widget, "Processing")
|
|
419
|
+
tabs_Reconstruction.addTab(tab2_processing_widget, "On-The-Fly")
|
|
420
|
+
|
|
421
|
+
# Editable List holding pydantic class(es) as per user selection
|
|
422
|
+
self.pydantic_classes = list()
|
|
423
|
+
self.prev_model_settings = {}
|
|
424
|
+
self.index = 0
|
|
425
|
+
self.pollData = False
|
|
426
|
+
|
|
427
|
+
# Stores Model & Components values which cause validation failure - can be highlighted on the model field as Red
|
|
428
|
+
self.modelHighlighterVals = {}
|
|
429
|
+
|
|
430
|
+
self.job_manager = job_manager.JobManager()
|
|
431
|
+
|
|
432
|
+
# handle napari's close widget and avoid starting a second instance
|
|
433
|
+
if HAS_INSTANCE["val"]:
|
|
434
|
+
self.worker: MyWorker = HAS_INSTANCE["MyWorker"]
|
|
435
|
+
self.worker.set_new_instances(
|
|
436
|
+
self.proc_table_QFormLayout, self, self._ui
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
self.worker = MyWorker(self.proc_table_QFormLayout, self, self._ui)
|
|
440
|
+
HAS_INSTANCE["val"] = True
|
|
441
|
+
HAS_INSTANCE["MyWorker"] = self.worker
|
|
442
|
+
|
|
443
|
+
self.app = QApplication.instance()
|
|
444
|
+
self.app.lastWindowClosed.connect(
|
|
445
|
+
self.myCloseEvent
|
|
446
|
+
) # this line is connection to signal close
|
|
447
|
+
|
|
448
|
+
######################################################
|
|
449
|
+
|
|
450
|
+
# our defined close event since napari doesnt do
|
|
451
|
+
def myCloseEvent(self):
|
|
452
|
+
event = QEvent(QEvent.Type.Close)
|
|
453
|
+
self.closeEvent(event)
|
|
454
|
+
# self.app.exit()
|
|
455
|
+
|
|
456
|
+
# on napari close - cleanup
|
|
457
|
+
def closeEvent(self, event):
|
|
458
|
+
if event.type() == QEvent.Type.Close:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
def hideEvent(self, event):
|
|
462
|
+
if event.type() == QEvent.Type.Hide and (
|
|
463
|
+
hasattr(self, "_ui")
|
|
464
|
+
and self._ui is not None
|
|
465
|
+
and self._ui.isVisible()
|
|
466
|
+
):
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
def showEvent(self, event):
|
|
470
|
+
if event.type() == QEvent.Type.Show:
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
def set_viewer(self, viewer):
|
|
474
|
+
self.viewer = viewer
|
|
475
|
+
|
|
476
|
+
def show_dataset(self, data_path):
|
|
477
|
+
# Show reconstruction data
|
|
478
|
+
try:
|
|
479
|
+
if self.viewer is not None:
|
|
480
|
+
self.viewer.open(data_path, plugin="napari-ome-zarr")
|
|
481
|
+
except Exception as exc:
|
|
482
|
+
self.message_box(exc.args)
|
|
483
|
+
|
|
484
|
+
def confirm_dialog(self, msg="Confirm your selection ?"):
|
|
485
|
+
qm = QMessageBox
|
|
486
|
+
ret = qm.question(
|
|
487
|
+
self.recon_tab_widget,
|
|
488
|
+
"Confirm",
|
|
489
|
+
msg,
|
|
490
|
+
qm.Yes | qm.No,
|
|
491
|
+
)
|
|
492
|
+
if ret == qm.Yes:
|
|
493
|
+
return True
|
|
494
|
+
else:
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
# Copied from main_widget
|
|
498
|
+
# ToDo: utilize common functions
|
|
499
|
+
# Input data selector
|
|
500
|
+
def browse_dir_path_input(self):
|
|
501
|
+
if len(self.pydantic_classes) > 0 and not self.confirm_dialog(
|
|
502
|
+
"Changing Input Data will reset your models. Continue ?"
|
|
503
|
+
):
|
|
504
|
+
return
|
|
505
|
+
else:
|
|
506
|
+
self.clear_all_models(silent=True)
|
|
507
|
+
try:
|
|
508
|
+
result = self.open_file_dialog(
|
|
509
|
+
self.input_directory, "dir", filter="ZARR Storage (*.zarr)"
|
|
510
|
+
)
|
|
511
|
+
# .zarr is a folder but we could implement a filter to scan for "ending with" and present those if required
|
|
512
|
+
except Exception as exc:
|
|
513
|
+
self.message_box(exc.args)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
if result == "":
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
self.data_input_LineEdit.value = result
|
|
520
|
+
|
|
521
|
+
def browse_dir_path_output(self):
|
|
522
|
+
try:
|
|
523
|
+
result = self.open_file_dialog(self.output_directory, "dir")
|
|
524
|
+
except Exception as exc:
|
|
525
|
+
self.message_box(exc.args)
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
if result == "":
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
if not Path(result).exists():
|
|
532
|
+
self.message_box("Output Directory path must exist !")
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
self.data_output_LineEdit.value = result
|
|
536
|
+
|
|
537
|
+
def browse_dir_path_inputBG(self, elem):
|
|
538
|
+
result = self.open_file_dialog(self.directory, "dir")
|
|
539
|
+
if result == "":
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
ret, ret_msg = self.validate_input_data(result, BG=True)
|
|
543
|
+
if not ret:
|
|
544
|
+
self.message_box(ret_msg)
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
elem.value = result
|
|
548
|
+
|
|
549
|
+
def validate_input_data(
|
|
550
|
+
self, input_data_folder: str, metadata=False, BG=False
|
|
551
|
+
) -> bool:
|
|
552
|
+
try:
|
|
553
|
+
self.input_channel_names = []
|
|
554
|
+
self.data_input_Label.value = "Input Store"
|
|
555
|
+
input_paths = Path(input_data_folder)
|
|
556
|
+
with open_ome_zarr(input_paths, mode="r") as dataset:
|
|
557
|
+
try:
|
|
558
|
+
self.input_channel_names = dataset.channel_names
|
|
559
|
+
self.data_input_Label.value = (
|
|
560
|
+
"Input Store" + " " + _info_icon
|
|
561
|
+
)
|
|
562
|
+
self.data_input_Label.tooltip = (
|
|
563
|
+
"Channel Names:\n- "
|
|
564
|
+
+ "\n- ".join(self.input_channel_names)
|
|
565
|
+
)
|
|
566
|
+
except Exception as exc:
|
|
567
|
+
print(exc.args)
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
string_pos = []
|
|
571
|
+
i = 0
|
|
572
|
+
for pos_paths, pos in dataset.positions():
|
|
573
|
+
string_pos.append(pos_paths)
|
|
574
|
+
if i == 0:
|
|
575
|
+
axes = pos.zgroup.attrs["multiscales"][0]["axes"]
|
|
576
|
+
string_array_n = [str(x["name"]) for x in axes]
|
|
577
|
+
string_array = [
|
|
578
|
+
str(x)
|
|
579
|
+
for x in pos.zgroup.attrs["multiscales"][0][
|
|
580
|
+
"datasets"
|
|
581
|
+
][0]["coordinateTransformations"][0]["scale"]
|
|
582
|
+
]
|
|
583
|
+
string_scale = []
|
|
584
|
+
for i in range(len(string_array_n)):
|
|
585
|
+
string_scale.append(
|
|
586
|
+
"{n}={d}".format(
|
|
587
|
+
n=string_array_n[i], d=string_array[i]
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
txt = "\n\nScale: " + ", ".join(string_scale)
|
|
591
|
+
self.data_input_Label.tooltip += txt
|
|
592
|
+
i += 1
|
|
593
|
+
txt = "\n\nFOV: " + ", ".join(string_pos)
|
|
594
|
+
self.data_input_Label.tooltip += txt
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
print(exc.args)
|
|
597
|
+
|
|
598
|
+
if not BG and metadata:
|
|
599
|
+
self.input_directory_dataset = dataset
|
|
600
|
+
|
|
601
|
+
if not BG:
|
|
602
|
+
self.pollData = False
|
|
603
|
+
zattrs = dataset.zattrs
|
|
604
|
+
if self.is_dataset_acq_running(zattrs):
|
|
605
|
+
if self.confirm_dialog(
|
|
606
|
+
msg="This seems like an in-process Acquisition. Would you like to process data on-the-fly ?"
|
|
607
|
+
):
|
|
608
|
+
self.pollData = True
|
|
609
|
+
|
|
610
|
+
return True, MSG_SUCCESS
|
|
611
|
+
raise Exception(
|
|
612
|
+
"Dataset does not appear to be a valid ome-zarr storage"
|
|
613
|
+
)
|
|
614
|
+
except Exception as exc:
|
|
615
|
+
return False, exc.args
|
|
616
|
+
|
|
617
|
+
# call back for input LineEdit path changed manually
|
|
618
|
+
# include data validation
|
|
619
|
+
def read_and_set_input_path_on_validation(self):
|
|
620
|
+
if (
|
|
621
|
+
self.data_input_LineEdit.value is None
|
|
622
|
+
or len(self.data_input_LineEdit.value) == 0
|
|
623
|
+
):
|
|
624
|
+
self.data_input_LineEdit.value = self.input_directory
|
|
625
|
+
self.message_box("Input data path cannot be empty")
|
|
626
|
+
return
|
|
627
|
+
if not Path(self.data_input_LineEdit.value).exists():
|
|
628
|
+
self.data_input_LineEdit.value = self.input_directory
|
|
629
|
+
self.message_box("Input data path must point to a valid location")
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
result = self.data_input_LineEdit.value
|
|
633
|
+
valid, ret_msg = self.validate_input_data(result)
|
|
634
|
+
|
|
635
|
+
if valid:
|
|
636
|
+
self.directory = Path(result).parent.absolute()
|
|
637
|
+
self.current_dir_path = result
|
|
638
|
+
self.input_directory = result
|
|
639
|
+
|
|
640
|
+
self.prev_model_settings = {}
|
|
641
|
+
|
|
642
|
+
self.save_last_paths()
|
|
643
|
+
else:
|
|
644
|
+
self.data_input_LineEdit.value = self.input_directory
|
|
645
|
+
self.message_box(ret_msg)
|
|
646
|
+
|
|
647
|
+
self.data_output_LineEdit.value = Path(
|
|
648
|
+
self.input_directory
|
|
649
|
+
).parent.absolute()
|
|
650
|
+
|
|
651
|
+
def read_and_set_out_path_on_validation(self):
|
|
652
|
+
if (
|
|
653
|
+
self.data_output_LineEdit.value is None
|
|
654
|
+
or len(self.data_output_LineEdit.value) == 0
|
|
655
|
+
):
|
|
656
|
+
self.data_output_LineEdit.value = self.output_directory
|
|
657
|
+
self.message_box("Output data path cannot be empty")
|
|
658
|
+
return
|
|
659
|
+
if not Path(self.data_output_LineEdit.value).exists():
|
|
660
|
+
self.data_output_LineEdit.value = self.output_directory
|
|
661
|
+
self.message_box("Output data path must point to a valid location")
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
self.output_directory = self.data_output_LineEdit.value
|
|
665
|
+
|
|
666
|
+
self.validate_model_output_paths()
|
|
667
|
+
|
|
668
|
+
def validate_model_output_paths(self):
|
|
669
|
+
if len(self.pydantic_classes) > 0:
|
|
670
|
+
for model_item in self.pydantic_classes:
|
|
671
|
+
output_LineEdit = model_item["output_LineEdit"]
|
|
672
|
+
output_Button = model_item["output_Button"]
|
|
673
|
+
model_item["output_parent_dir"] = self.output_directory
|
|
674
|
+
|
|
675
|
+
full_out_path = os.path.join(
|
|
676
|
+
Path(self.output_directory).absolute(),
|
|
677
|
+
output_LineEdit.value,
|
|
678
|
+
)
|
|
679
|
+
model_item["output"] = full_out_path
|
|
680
|
+
|
|
681
|
+
save_path_exists = (
|
|
682
|
+
True if Path(full_out_path).exists() else False
|
|
683
|
+
)
|
|
684
|
+
output_LineEdit.label = (
|
|
685
|
+
"" if not save_path_exists else (_validate_alert + " ")
|
|
686
|
+
) + "Output Data:"
|
|
687
|
+
output_LineEdit.tooltip = (
|
|
688
|
+
""
|
|
689
|
+
if not save_path_exists
|
|
690
|
+
else (_validate_alert + "Output file exists")
|
|
691
|
+
)
|
|
692
|
+
output_Button.text = (
|
|
693
|
+
"" if not save_path_exists else (_validate_alert + " ")
|
|
694
|
+
) + "Output Data:"
|
|
695
|
+
output_Button.tooltip = (
|
|
696
|
+
""
|
|
697
|
+
if not save_path_exists
|
|
698
|
+
else (_validate_alert + "Output file exists")
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def is_dataset_acq_running(self, zattrs: dict) -> bool:
|
|
702
|
+
"""
|
|
703
|
+
Checks the zattrs for CurrentDimensions & FinalDimensions key and tries to figure if
|
|
704
|
+
data acquisition is running
|
|
705
|
+
"""
|
|
706
|
+
|
|
707
|
+
required_order = ["time", "position", "z", "channel"]
|
|
708
|
+
if "CurrentDimensions" in zattrs.keys():
|
|
709
|
+
my_dict = zattrs["CurrentDimensions"]
|
|
710
|
+
sorted_dict_acq = {
|
|
711
|
+
k: my_dict[k]
|
|
712
|
+
for k in sorted(my_dict, key=lambda x: required_order.index(x))
|
|
713
|
+
}
|
|
714
|
+
if "FinalDimensions" in zattrs.keys():
|
|
715
|
+
my_dict = zattrs["FinalDimensions"]
|
|
716
|
+
sorted_dict_final = {
|
|
717
|
+
k: my_dict[k]
|
|
718
|
+
for k in sorted(my_dict, key=lambda x: required_order.index(x))
|
|
719
|
+
}
|
|
720
|
+
if sorted_dict_acq != sorted_dict_final:
|
|
721
|
+
return True
|
|
722
|
+
return False
|
|
723
|
+
|
|
724
|
+
# Output data selector
|
|
725
|
+
def browse_model_dir_path_output(self, elem):
|
|
726
|
+
result = self.open_file_dialog(self.output_directory, "save")
|
|
727
|
+
if result == "":
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
save_path_exists = True if Path(result).exists() else False
|
|
731
|
+
elem.label = "Output Data:" + (
|
|
732
|
+
"" if not save_path_exists else (" " + _validate_alert)
|
|
733
|
+
)
|
|
734
|
+
elem.tooltip = "" if not save_path_exists else "Output file exists"
|
|
735
|
+
|
|
736
|
+
elem.value = Path(result).name
|
|
737
|
+
|
|
738
|
+
self.save_last_paths()
|
|
739
|
+
|
|
740
|
+
# call back for output LineEdit path changed manually
|
|
741
|
+
def read_and_set_output_path_on_validation(self, elem1, elem2, save_path):
|
|
742
|
+
if elem1.value is None or len(elem1.value) == 0:
|
|
743
|
+
elem1.value = Path(save_path).name
|
|
744
|
+
|
|
745
|
+
save_path = os.path.join(
|
|
746
|
+
Path(self.output_directory).absolute(), elem1.value
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
save_path_exists = True if Path(save_path).exists() else False
|
|
750
|
+
elem1.label = (
|
|
751
|
+
"" if not save_path_exists else (_validate_alert + " ")
|
|
752
|
+
) + "Output Data:"
|
|
753
|
+
elem1.tooltip = (
|
|
754
|
+
""
|
|
755
|
+
if not save_path_exists
|
|
756
|
+
else (_validate_alert + "Output file exists")
|
|
757
|
+
)
|
|
758
|
+
elem2.text = (
|
|
759
|
+
"" if not save_path_exists else (_validate_alert + " ")
|
|
760
|
+
) + "Output Data:"
|
|
761
|
+
elem2.tooltip = (
|
|
762
|
+
""
|
|
763
|
+
if not save_path_exists
|
|
764
|
+
else (_validate_alert + "Output file exists")
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
self.save_last_paths()
|
|
768
|
+
|
|
769
|
+
# Copied from main_widget
|
|
770
|
+
# ToDo: utilize common functions
|
|
771
|
+
# Output data selector
|
|
772
|
+
def browse_dir_path_model(self):
|
|
773
|
+
results = self.open_file_dialog(
|
|
774
|
+
self.directory, "files", filter="YAML Files (*.yml)"
|
|
775
|
+
) # returns list
|
|
776
|
+
if len(results) == 0 or results == "":
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
self.model_directory = str(Path(results[0]).parent.absolute())
|
|
780
|
+
self.directory = self.model_directory
|
|
781
|
+
self.current_dir_path = self.model_directory
|
|
782
|
+
|
|
783
|
+
self.save_last_paths()
|
|
784
|
+
self.open_model_files(results)
|
|
785
|
+
|
|
786
|
+
def open_model_files(self, results: List):
|
|
787
|
+
pydantic_models = list()
|
|
788
|
+
for result in results:
|
|
789
|
+
self.yaml_model_file = result
|
|
790
|
+
|
|
791
|
+
with open(result, "r") as yaml_in:
|
|
792
|
+
yaml_object = utils.yaml.safe_load(
|
|
793
|
+
yaml_in
|
|
794
|
+
) # yaml_object will be a list or a dict
|
|
795
|
+
jsonString = json.dumps(self.convert(yaml_object))
|
|
796
|
+
json_out = json.loads(jsonString)
|
|
797
|
+
json_dict = dict(json_out)
|
|
798
|
+
|
|
799
|
+
selected_modes = list(OPTION_TO_MODEL_DICT.copy().keys())
|
|
800
|
+
exclude_modes = list(OPTION_TO_MODEL_DICT.copy().keys())
|
|
801
|
+
|
|
802
|
+
for k in range(len(selected_modes) - 1, -1, -1):
|
|
803
|
+
if selected_modes[k] in json_dict.keys():
|
|
804
|
+
exclude_modes.pop(k)
|
|
805
|
+
else:
|
|
806
|
+
selected_modes.pop(k)
|
|
807
|
+
|
|
808
|
+
pruned_pydantic_class, ret_msg = self.build_model(selected_modes)
|
|
809
|
+
if pruned_pydantic_class is None:
|
|
810
|
+
self.message_box(ret_msg)
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
pydantic_model, ret_msg = self.get_model_from_file(
|
|
814
|
+
self.yaml_model_file
|
|
815
|
+
)
|
|
816
|
+
if pydantic_model is None:
|
|
817
|
+
if (
|
|
818
|
+
isinstance(ret_msg, List)
|
|
819
|
+
and len(ret_msg) == 2
|
|
820
|
+
and len(ret_msg[0]["loc"]) == 3
|
|
821
|
+
and ret_msg[0]["loc"][2] == "background_path"
|
|
822
|
+
):
|
|
823
|
+
pydantic_model = pruned_pydantic_class # if only background_path fails validation
|
|
824
|
+
if "birefringence" in json_dict.keys():
|
|
825
|
+
json_dict["birefringence"]["apply_inverse"][
|
|
826
|
+
"background_path"
|
|
827
|
+
] = ""
|
|
828
|
+
else:
|
|
829
|
+
bg_loc = ""
|
|
830
|
+
self.message_box(
|
|
831
|
+
"background_path:\nPath was invalid and will be reset"
|
|
832
|
+
)
|
|
833
|
+
else:
|
|
834
|
+
self.message_box(ret_msg)
|
|
835
|
+
return
|
|
836
|
+
else:
|
|
837
|
+
# make sure "background_path" is valid
|
|
838
|
+
if "birefringence" in json_dict.keys():
|
|
839
|
+
bg_loc = json_dict["birefringence"]["apply_inverse"][
|
|
840
|
+
"background_path"
|
|
841
|
+
]
|
|
842
|
+
else:
|
|
843
|
+
bg_loc = ""
|
|
844
|
+
if bg_loc != "":
|
|
845
|
+
extension = os.path.splitext(bg_loc)[1]
|
|
846
|
+
if len(extension) > 0:
|
|
847
|
+
bg_loc = Path(
|
|
848
|
+
os.path.join(
|
|
849
|
+
str(Path(bg_loc).parent.absolute()),
|
|
850
|
+
"background.zarr",
|
|
851
|
+
)
|
|
852
|
+
)
|
|
853
|
+
else:
|
|
854
|
+
bg_loc = Path(os.path.join(bg_loc, "background.zarr"))
|
|
855
|
+
if not bg_loc.exists() or not self.validate_input_data(
|
|
856
|
+
str(bg_loc)
|
|
857
|
+
):
|
|
858
|
+
self.message_box(
|
|
859
|
+
"background_path:\nPwas invalid and will be reset"
|
|
860
|
+
)
|
|
861
|
+
json_dict["birefringence"]["apply_inverse"][
|
|
862
|
+
"background_path"
|
|
863
|
+
] = ""
|
|
864
|
+
else:
|
|
865
|
+
json_dict["birefringence"]["apply_inverse"][
|
|
866
|
+
"background_path"
|
|
867
|
+
] = str(bg_loc.parent.absolute())
|
|
868
|
+
|
|
869
|
+
pydantic_model = self.create_acq_contols2(
|
|
870
|
+
selected_modes, exclude_modes, pydantic_model, json_dict
|
|
871
|
+
)
|
|
872
|
+
if pydantic_model is None:
|
|
873
|
+
self.message_box("Error - pydantic model returned None")
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
pydantic_models.append(pydantic_model)
|
|
877
|
+
|
|
878
|
+
return pydantic_models
|
|
879
|
+
|
|
880
|
+
# useful when using close widget and not napari close and we might need them again
|
|
881
|
+
def save_last_paths(self):
|
|
882
|
+
HAS_INSTANCE["current_dir_path"] = self.current_dir_path
|
|
883
|
+
HAS_INSTANCE["input_directory"] = self.input_directory
|
|
884
|
+
HAS_INSTANCE["output_directory"] = self.output_directory
|
|
885
|
+
HAS_INSTANCE["model_directory"] = self.model_directory
|
|
886
|
+
HAS_INSTANCE["yaml_model_file"] = self.yaml_model_file
|
|
887
|
+
|
|
888
|
+
# clears the results table
|
|
889
|
+
def clear_results_table(self):
|
|
890
|
+
index = self.proc_table_QFormLayout.rowCount()
|
|
891
|
+
if index < 1:
|
|
892
|
+
self.message_box("There are no processing results to clear !")
|
|
893
|
+
return
|
|
894
|
+
if self.confirm_dialog():
|
|
895
|
+
for i in range(self.proc_table_QFormLayout.rowCount()):
|
|
896
|
+
self.proc_table_QFormLayout.removeRow(0)
|
|
897
|
+
|
|
898
|
+
def remove_row(self, row, expID):
|
|
899
|
+
try:
|
|
900
|
+
if row < self.proc_table_QFormLayout.rowCount():
|
|
901
|
+
widgetItem = self.proc_table_QFormLayout.itemAt(row)
|
|
902
|
+
if widgetItem is not None:
|
|
903
|
+
name_widget = widgetItem.widget()
|
|
904
|
+
toolTip_string = str(name_widget.toolTip)
|
|
905
|
+
if expID in toolTip_string:
|
|
906
|
+
self.proc_table_QFormLayout.removeRow(
|
|
907
|
+
row
|
|
908
|
+
) # removeRow vs takeRow for threads ?
|
|
909
|
+
except Exception as exc:
|
|
910
|
+
print(exc.args)
|
|
911
|
+
|
|
912
|
+
# marks fields on the Model that cause a validation error
|
|
913
|
+
def model_highlighter(self, errs):
|
|
914
|
+
try:
|
|
915
|
+
for uid in errs.keys():
|
|
916
|
+
self.modelHighlighterVals[uid] = {}
|
|
917
|
+
container = errs[uid]["cls"]
|
|
918
|
+
self.modelHighlighterVals[uid]["errs"] = errs[uid]["errs"]
|
|
919
|
+
self.modelHighlighterVals[uid]["items"] = []
|
|
920
|
+
self.modelHighlighterVals[uid]["tooltip"] = []
|
|
921
|
+
if len(errs[uid]["errs"]) > 0:
|
|
922
|
+
self.model_highlighter_setter(
|
|
923
|
+
errs[uid]["errs"], container, uid
|
|
924
|
+
)
|
|
925
|
+
except Exception as exc:
|
|
926
|
+
print(exc.args)
|
|
927
|
+
# more of a test feature - no need to show up
|
|
928
|
+
|
|
929
|
+
# format all model errors into a display format for napari error message box
|
|
930
|
+
def format_string_for_error_display(self, errs):
|
|
931
|
+
try:
|
|
932
|
+
ret_str = ""
|
|
933
|
+
for uid in errs.keys():
|
|
934
|
+
if len(errs[uid]["errs"]) > 0:
|
|
935
|
+
ret_str += errs[uid]["collapsibleBox"] + "\n"
|
|
936
|
+
for idx in range(len(errs[uid]["errs"])):
|
|
937
|
+
ret_str += f"{'>'.join(errs[uid]['errs'][idx]['loc'])}:\n{errs[uid]['errs'][idx]['msg']} \n"
|
|
938
|
+
ret_str += "\n"
|
|
939
|
+
return ret_str
|
|
940
|
+
except Exception as exc:
|
|
941
|
+
return ret_str
|
|
942
|
+
|
|
943
|
+
# recursively fix the container for highlighting
|
|
944
|
+
def model_highlighter_setter(
|
|
945
|
+
self, errs, container: Container, containerID, lev=0
|
|
946
|
+
):
|
|
947
|
+
try:
|
|
948
|
+
layout = container.native.layout()
|
|
949
|
+
for i in range(layout.count()):
|
|
950
|
+
item = layout.itemAt(i)
|
|
951
|
+
if item.widget():
|
|
952
|
+
widget = layout.itemAt(i).widget()
|
|
953
|
+
if (
|
|
954
|
+
(
|
|
955
|
+
not isinstance(widget._magic_widget, CheckBox)
|
|
956
|
+
and not isinstance(
|
|
957
|
+
widget._magic_widget, PushButton
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
and not isinstance(widget._magic_widget, LineEdit)
|
|
961
|
+
and isinstance(
|
|
962
|
+
widget._magic_widget._inner_widget, Container
|
|
963
|
+
)
|
|
964
|
+
and not (widget._magic_widget._inner_widget is None)
|
|
965
|
+
):
|
|
966
|
+
self.model_highlighter_setter(
|
|
967
|
+
errs,
|
|
968
|
+
widget._magic_widget._inner_widget,
|
|
969
|
+
containerID,
|
|
970
|
+
lev + 1,
|
|
971
|
+
)
|
|
972
|
+
else:
|
|
973
|
+
for idx in range(len(errs)):
|
|
974
|
+
if len(errs[idx]["loc"]) - 1 < lev:
|
|
975
|
+
pass
|
|
976
|
+
elif (
|
|
977
|
+
isinstance(widget._magic_widget, CheckBox)
|
|
978
|
+
or isinstance(widget._magic_widget, LineEdit)
|
|
979
|
+
or isinstance(widget._magic_widget, PushButton)
|
|
980
|
+
):
|
|
981
|
+
if widget._magic_widget.label == errs[idx][
|
|
982
|
+
"loc"
|
|
983
|
+
][lev].replace("_", " "):
|
|
984
|
+
if widget._magic_widget.tooltip is None:
|
|
985
|
+
widget._magic_widget.tooltip = "-\n"
|
|
986
|
+
self.modelHighlighterVals[containerID][
|
|
987
|
+
"items"
|
|
988
|
+
].append(widget._magic_widget)
|
|
989
|
+
self.modelHighlighterVals[containerID][
|
|
990
|
+
"tooltip"
|
|
991
|
+
].append(widget._magic_widget.tooltip)
|
|
992
|
+
widget._magic_widget.tooltip += (
|
|
993
|
+
errs[idx]["msg"] + "\n"
|
|
994
|
+
)
|
|
995
|
+
widget._magic_widget.native.setStyleSheet(
|
|
996
|
+
"border:1px solid rgb(255, 255, 0); border-width: 1px;"
|
|
997
|
+
)
|
|
998
|
+
elif (
|
|
999
|
+
widget._magic_widget._label_widget.value
|
|
1000
|
+
== errs[idx]["loc"][lev].replace("_", " ")
|
|
1001
|
+
):
|
|
1002
|
+
if (
|
|
1003
|
+
widget._magic_widget._label_widget.tooltip
|
|
1004
|
+
is None
|
|
1005
|
+
):
|
|
1006
|
+
widget._magic_widget._label_widget.tooltip = (
|
|
1007
|
+
"-\n"
|
|
1008
|
+
)
|
|
1009
|
+
self.modelHighlighterVals[containerID][
|
|
1010
|
+
"items"
|
|
1011
|
+
].append(
|
|
1012
|
+
widget._magic_widget._label_widget
|
|
1013
|
+
)
|
|
1014
|
+
self.modelHighlighterVals[containerID][
|
|
1015
|
+
"tooltip"
|
|
1016
|
+
].append(
|
|
1017
|
+
widget._magic_widget._label_widget.tooltip
|
|
1018
|
+
)
|
|
1019
|
+
widget._magic_widget._label_widget.tooltip += (
|
|
1020
|
+
errs[idx]["msg"] + "\n"
|
|
1021
|
+
)
|
|
1022
|
+
widget._magic_widget._label_widget.native.setStyleSheet(
|
|
1023
|
+
"border:1px solid rgb(255, 255, 0); border-width: 1px;"
|
|
1024
|
+
)
|
|
1025
|
+
if (
|
|
1026
|
+
widget._magic_widget._inner_widget.tooltip
|
|
1027
|
+
is None
|
|
1028
|
+
):
|
|
1029
|
+
widget._magic_widget._inner_widget.tooltip = (
|
|
1030
|
+
"-\n"
|
|
1031
|
+
)
|
|
1032
|
+
self.modelHighlighterVals[containerID][
|
|
1033
|
+
"items"
|
|
1034
|
+
].append(
|
|
1035
|
+
widget._magic_widget._inner_widget
|
|
1036
|
+
)
|
|
1037
|
+
self.modelHighlighterVals[containerID][
|
|
1038
|
+
"tooltip"
|
|
1039
|
+
].append(
|
|
1040
|
+
widget._magic_widget._inner_widget.tooltip
|
|
1041
|
+
)
|
|
1042
|
+
widget._magic_widget._inner_widget.tooltip += (
|
|
1043
|
+
errs[idx]["msg"] + "\n"
|
|
1044
|
+
)
|
|
1045
|
+
widget._magic_widget._inner_widget.native.setStyleSheet(
|
|
1046
|
+
"border:1px solid rgb(255, 255, 0); border-width: 1px;"
|
|
1047
|
+
)
|
|
1048
|
+
except Exception as exc:
|
|
1049
|
+
print(exc.args)
|
|
1050
|
+
|
|
1051
|
+
# recursively fix the container for highlighting
|
|
1052
|
+
def model_reset_highlighter_setter(self):
|
|
1053
|
+
try:
|
|
1054
|
+
for containerID in self.modelHighlighterVals.keys():
|
|
1055
|
+
items = self.modelHighlighterVals[containerID]["items"]
|
|
1056
|
+
tooltip = self.modelHighlighterVals[containerID]["tooltip"]
|
|
1057
|
+
i = 0
|
|
1058
|
+
for widItem in items:
|
|
1059
|
+
widItem.native.setStyleSheet(
|
|
1060
|
+
"border:1px solid rgb(0, 0, 0); border-width: 0px;"
|
|
1061
|
+
)
|
|
1062
|
+
widItem.tooltip = tooltip[i]
|
|
1063
|
+
i += 1
|
|
1064
|
+
|
|
1065
|
+
except Exception as exc:
|
|
1066
|
+
print(exc.args)
|
|
1067
|
+
|
|
1068
|
+
except Exception as exc:
|
|
1069
|
+
print(exc.args)
|
|
1070
|
+
|
|
1071
|
+
# passes msg to napari notifications
|
|
1072
|
+
def message_box(self, msg, type="exc"):
|
|
1073
|
+
if len(msg) > 0:
|
|
1074
|
+
try:
|
|
1075
|
+
json_object = msg
|
|
1076
|
+
json_txt = ""
|
|
1077
|
+
for err in json_object:
|
|
1078
|
+
json_txt = (
|
|
1079
|
+
json_txt
|
|
1080
|
+
+ "Loc: {loc}\nMsg:{msg}\nType:{type}\n\n".format(
|
|
1081
|
+
loc=err["loc"], msg=err["msg"], type=err["type"]
|
|
1082
|
+
)
|
|
1083
|
+
)
|
|
1084
|
+
json_txt = str(json_txt)
|
|
1085
|
+
# ToDo: format it better
|
|
1086
|
+
# formatted txt does not show up properly in msg-box ??
|
|
1087
|
+
except (TypeError, KeyError, AttributeError):
|
|
1088
|
+
# msg is not in expected validation error format
|
|
1089
|
+
json_txt = str(msg)
|
|
1090
|
+
|
|
1091
|
+
# show is a message box
|
|
1092
|
+
if self.stand_alone:
|
|
1093
|
+
self.message_box_stand_alone(json_txt)
|
|
1094
|
+
else:
|
|
1095
|
+
if type == "exc":
|
|
1096
|
+
show_error(json_txt)
|
|
1097
|
+
else:
|
|
1098
|
+
show_info(json_txt)
|
|
1099
|
+
|
|
1100
|
+
def message_box_stand_alone(self, msg):
|
|
1101
|
+
q = QMessageBox(
|
|
1102
|
+
QMessageBox.Warning,
|
|
1103
|
+
"Message",
|
|
1104
|
+
str(msg),
|
|
1105
|
+
parent=self.recon_tab_widget,
|
|
1106
|
+
)
|
|
1107
|
+
q.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
1108
|
+
q.setIcon(QMessageBox.Icon.Warning)
|
|
1109
|
+
q.exec_()
|
|
1110
|
+
|
|
1111
|
+
def add_widget(
|
|
1112
|
+
self, parentLayout: QVBoxLayout, expID, jID, table_entry_ID="", pos=""
|
|
1113
|
+
):
|
|
1114
|
+
|
|
1115
|
+
jID = str(jID)
|
|
1116
|
+
_cancelJobBtntext = "Cancel Job {jID} ({posName})".format(
|
|
1117
|
+
jID=jID, posName=pos
|
|
1118
|
+
)
|
|
1119
|
+
_cancelJobButton = widgets.PushButton(
|
|
1120
|
+
name="JobID", label=_cancelJobBtntext, enabled=True, value=False
|
|
1121
|
+
)
|
|
1122
|
+
_txtForInfoBox = "Updating {id}-{pos}: Please wait... \nJobID assigned: {jID} ".format(
|
|
1123
|
+
id=table_entry_ID, jID=jID, pos=pos
|
|
1124
|
+
)
|
|
1125
|
+
_scrollAreaCollapsibleBoxDisplayWidget = ScrollableLabel(
|
|
1126
|
+
text=_txtForInfoBox
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
_scrollAreaCollapsibleBoxWidgetLayout = QVBoxLayout()
|
|
1130
|
+
_scrollAreaCollapsibleBoxWidgetLayout.setAlignment(
|
|
1131
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
_scrollAreaCollapsibleBoxWidgetLayout.addWidget(
|
|
1135
|
+
_cancelJobButton.native
|
|
1136
|
+
)
|
|
1137
|
+
_scrollAreaCollapsibleBoxWidgetLayout.addWidget(
|
|
1138
|
+
_scrollAreaCollapsibleBoxDisplayWidget
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
_scrollAreaCollapsibleBoxWidget = QWidget()
|
|
1142
|
+
_scrollAreaCollapsibleBoxWidget.setLayout(
|
|
1143
|
+
_scrollAreaCollapsibleBoxWidgetLayout
|
|
1144
|
+
)
|
|
1145
|
+
_scrollAreaCollapsibleBox = QScrollArea()
|
|
1146
|
+
_scrollAreaCollapsibleBox.setWidgetResizable(True)
|
|
1147
|
+
_scrollAreaCollapsibleBox.setMinimumHeight(300)
|
|
1148
|
+
_scrollAreaCollapsibleBox.setWidget(_scrollAreaCollapsibleBoxWidget)
|
|
1149
|
+
|
|
1150
|
+
_collapsibleBoxWidgetLayout = QVBoxLayout()
|
|
1151
|
+
_collapsibleBoxWidgetLayout.addWidget(_scrollAreaCollapsibleBox)
|
|
1152
|
+
|
|
1153
|
+
_collapsibleBoxWidget = CollapsibleBox(
|
|
1154
|
+
table_entry_ID + " - " + pos
|
|
1155
|
+
) # tableEntryID, tableEntryShortDesc - should update with processing status
|
|
1156
|
+
_collapsibleBoxWidget.setSizePolicy(
|
|
1157
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1158
|
+
)
|
|
1159
|
+
_collapsibleBoxWidget.setContentLayout(_collapsibleBoxWidgetLayout)
|
|
1160
|
+
|
|
1161
|
+
parentLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
|
1162
|
+
parentLayout.addWidget(_collapsibleBoxWidget)
|
|
1163
|
+
|
|
1164
|
+
MULTI_JOBS_REFS[expID + jID] = {}
|
|
1165
|
+
MULTI_JOBS_REFS[expID + jID]["cancelBtn"] = _cancelJobButton
|
|
1166
|
+
MULTI_JOBS_REFS[expID + jID][
|
|
1167
|
+
"infobox"
|
|
1168
|
+
] = _scrollAreaCollapsibleBoxDisplayWidget
|
|
1169
|
+
NEW_WIDGETS_QUEUE.remove(expID + jID)
|
|
1170
|
+
|
|
1171
|
+
def add_table_entry_job(self, proc_params):
|
|
1172
|
+
|
|
1173
|
+
tableEntryID = proc_params["tableEntryID"]
|
|
1174
|
+
parentLayout: QVBoxLayout = proc_params["parent_layout"]
|
|
1175
|
+
|
|
1176
|
+
_cancelJobButton = widgets.PushButton(
|
|
1177
|
+
name="JobID", label="Cancel Job", value=False, enabled=False
|
|
1178
|
+
)
|
|
1179
|
+
_txtForInfoBox = "Updating {id}: Please wait...".format(
|
|
1180
|
+
id=tableEntryID
|
|
1181
|
+
)
|
|
1182
|
+
_scrollAreaCollapsibleBoxDisplayWidget = ScrollableLabel(
|
|
1183
|
+
text=_txtForInfoBox
|
|
1184
|
+
)
|
|
1185
|
+
_scrollAreaCollapsibleBoxDisplayWidget.setFixedHeight(300)
|
|
1186
|
+
|
|
1187
|
+
proc_params["table_entry_infoBox"] = (
|
|
1188
|
+
_scrollAreaCollapsibleBoxDisplayWidget
|
|
1189
|
+
)
|
|
1190
|
+
proc_params["cancelJobButton"] = _cancelJobButton
|
|
1191
|
+
parentLayout.addWidget(_cancelJobButton.native)
|
|
1192
|
+
parentLayout.addWidget(_scrollAreaCollapsibleBoxDisplayWidget)
|
|
1193
|
+
|
|
1194
|
+
return proc_params
|
|
1195
|
+
|
|
1196
|
+
def add_remove_check_OTF_table_entry(
|
|
1197
|
+
self, OTF_dir_path, bool_msg, do_check=False
|
|
1198
|
+
):
|
|
1199
|
+
if do_check:
|
|
1200
|
+
try:
|
|
1201
|
+
for row in range(self.proc_OTF_table_QFormLayout.rowCount()):
|
|
1202
|
+
widgetItem = self.proc_OTF_table_QFormLayout.itemAt(row)
|
|
1203
|
+
if widgetItem is not None:
|
|
1204
|
+
name_widget: QWidget = widgetItem.widget()
|
|
1205
|
+
name_string = str(name_widget.objectName())
|
|
1206
|
+
if OTF_dir_path in name_string:
|
|
1207
|
+
for item in name_widget.findChildren(QPushButton):
|
|
1208
|
+
_poll_Stop_PushButton: QPushButton = item
|
|
1209
|
+
return _poll_Stop_PushButton.isChecked()
|
|
1210
|
+
return False
|
|
1211
|
+
except Exception as exc:
|
|
1212
|
+
print(exc.args)
|
|
1213
|
+
return False
|
|
1214
|
+
else:
|
|
1215
|
+
if bool_msg:
|
|
1216
|
+
_poll_otf_label = ScrollableLabel(
|
|
1217
|
+
text=OTF_dir_path + " " + _green_dot
|
|
1218
|
+
)
|
|
1219
|
+
_poll_Stop_PushButton = QPushButton("Stop")
|
|
1220
|
+
_poll_Stop_PushButton.setCheckable(
|
|
1221
|
+
True
|
|
1222
|
+
) # Make the button checkable
|
|
1223
|
+
_poll_Stop_PushButton.clicked.connect(
|
|
1224
|
+
lambda: self.stop_OTF_push_button_call(
|
|
1225
|
+
_poll_otf_label, OTF_dir_path + " " + _red_dot
|
|
1226
|
+
)
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
_poll_data_widget = QWidget()
|
|
1230
|
+
_poll_data_widget.setObjectName(OTF_dir_path)
|
|
1231
|
+
_poll_data_widget_layout = QHBoxLayout()
|
|
1232
|
+
_poll_data_widget.setLayout(_poll_data_widget_layout)
|
|
1233
|
+
_poll_data_widget_layout.addWidget(_poll_otf_label)
|
|
1234
|
+
_poll_data_widget_layout.addWidget(_poll_Stop_PushButton)
|
|
1235
|
+
|
|
1236
|
+
self.proc_OTF_table_QFormLayout.insertRow(0, _poll_data_widget)
|
|
1237
|
+
else:
|
|
1238
|
+
try:
|
|
1239
|
+
for row in range(
|
|
1240
|
+
self.proc_OTF_table_QFormLayout.rowCount()
|
|
1241
|
+
):
|
|
1242
|
+
widgetItem = self.proc_OTF_table_QFormLayout.itemAt(
|
|
1243
|
+
row
|
|
1244
|
+
)
|
|
1245
|
+
if widgetItem is not None:
|
|
1246
|
+
name_widget: QWidget = widgetItem.widget()
|
|
1247
|
+
name_string = str(name_widget.objectName())
|
|
1248
|
+
if OTF_dir_path in name_string:
|
|
1249
|
+
self.proc_OTF_table_QFormLayout.removeRow(row)
|
|
1250
|
+
except Exception as exc:
|
|
1251
|
+
print(exc.args)
|
|
1252
|
+
|
|
1253
|
+
def stop_OTF_push_button_call(self, label, txt):
|
|
1254
|
+
_poll_otf_label: QLabel = label
|
|
1255
|
+
_poll_otf_label.setText(txt)
|
|
1256
|
+
self.setDisabled(True)
|
|
1257
|
+
|
|
1258
|
+
# adds processing entry to _qwidgetTabEntry_layout as row item
|
|
1259
|
+
# row item will be purged from table as processing finishes
|
|
1260
|
+
# there could be 3 tabs for this processing table status
|
|
1261
|
+
# Running, Finished, Errored
|
|
1262
|
+
def addTableEntry(
|
|
1263
|
+
self, table_entry_ID, table_entry_short_desc, proc_params
|
|
1264
|
+
):
|
|
1265
|
+
_scrollAreaCollapsibleBoxWidgetLayout = QVBoxLayout()
|
|
1266
|
+
_scrollAreaCollapsibleBoxWidgetLayout.setAlignment(
|
|
1267
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
_scrollAreaCollapsibleBoxWidget = QWidget()
|
|
1271
|
+
_scrollAreaCollapsibleBoxWidget.setLayout(
|
|
1272
|
+
_scrollAreaCollapsibleBoxWidgetLayout
|
|
1273
|
+
)
|
|
1274
|
+
_scrollAreaCollapsibleBoxWidget.setSizePolicy(
|
|
1275
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
_scrollAreaCollapsibleBox = QScrollArea()
|
|
1279
|
+
_scrollAreaCollapsibleBox.setWidgetResizable(True)
|
|
1280
|
+
_scrollAreaCollapsibleBox.setWidget(_scrollAreaCollapsibleBoxWidget)
|
|
1281
|
+
_scrollAreaCollapsibleBox.setMinimumHeight(300)
|
|
1282
|
+
_scrollAreaCollapsibleBox.setSizePolicy(
|
|
1283
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
_collapsibleBoxWidgetLayout = QVBoxLayout()
|
|
1287
|
+
_collapsibleBoxWidgetLayout.addWidget(_scrollAreaCollapsibleBox)
|
|
1288
|
+
|
|
1289
|
+
_collapsibleBoxWidget = CollapsibleBox(table_entry_ID)
|
|
1290
|
+
_collapsibleBoxWidget.setSizePolicy(
|
|
1291
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1292
|
+
)
|
|
1293
|
+
_collapsibleBoxWidget.setContentLayout(_collapsibleBoxWidgetLayout)
|
|
1294
|
+
|
|
1295
|
+
_expandingTabEntryWidgetLayout = QVBoxLayout()
|
|
1296
|
+
_expandingTabEntryWidgetLayout.setAlignment(
|
|
1297
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
1298
|
+
)
|
|
1299
|
+
_expandingTabEntryWidgetLayout.addWidget(_collapsibleBoxWidget)
|
|
1300
|
+
|
|
1301
|
+
_expandingTabEntryWidget = QWidget()
|
|
1302
|
+
_expandingTabEntryWidget.toolTip = table_entry_short_desc
|
|
1303
|
+
_expandingTabEntryWidget.setLayout(_expandingTabEntryWidgetLayout)
|
|
1304
|
+
_expandingTabEntryWidget.setSizePolicy(
|
|
1305
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
proc_params["tableEntryID"] = table_entry_ID
|
|
1309
|
+
proc_params["parent_layout"] = _scrollAreaCollapsibleBoxWidgetLayout
|
|
1310
|
+
proc_params = self.add_table_entry_job(proc_params)
|
|
1311
|
+
|
|
1312
|
+
# instead of adding, insert at 0 to keep latest entry on top
|
|
1313
|
+
self.proc_table_QFormLayout.insertRow(0, _expandingTabEntryWidget)
|
|
1314
|
+
|
|
1315
|
+
proc_params["table_layout"] = self.proc_table_QFormLayout
|
|
1316
|
+
proc_params["table_entry"] = _expandingTabEntryWidget
|
|
1317
|
+
|
|
1318
|
+
self.worker.run_in_pool(proc_params)
|
|
1319
|
+
|
|
1320
|
+
# Builds the model as required
|
|
1321
|
+
def build_model(self, selected_modes):
|
|
1322
|
+
try:
|
|
1323
|
+
birefringence = None
|
|
1324
|
+
phase = None
|
|
1325
|
+
fluorescence = None
|
|
1326
|
+
chNames = ["State0"]
|
|
1327
|
+
exclude_modes = ["birefringence", "phase", "fluorescence"]
|
|
1328
|
+
if "birefringence" in selected_modes and "phase" in selected_modes:
|
|
1329
|
+
birefringence = settings.BirefringenceSettings()
|
|
1330
|
+
phase = settings.PhaseSettings()
|
|
1331
|
+
chNames = ["State0", "State1", "State2", "State3"]
|
|
1332
|
+
exclude_modes = ["fluorescence"]
|
|
1333
|
+
elif "birefringence" in selected_modes:
|
|
1334
|
+
birefringence = settings.BirefringenceSettings()
|
|
1335
|
+
chNames = ["State0", "State1", "State2", "State3"]
|
|
1336
|
+
exclude_modes = ["fluorescence", "phase"]
|
|
1337
|
+
elif "phase" in selected_modes:
|
|
1338
|
+
phase = settings.PhaseSettings()
|
|
1339
|
+
chNames = ["BF"]
|
|
1340
|
+
exclude_modes = ["birefringence", "fluorescence"]
|
|
1341
|
+
elif "fluorescence" in selected_modes:
|
|
1342
|
+
fluorescence = settings.FluorescenceSettings()
|
|
1343
|
+
chNames = ["FL"]
|
|
1344
|
+
exclude_modes = ["birefringence", "phase"]
|
|
1345
|
+
|
|
1346
|
+
model = None
|
|
1347
|
+
try:
|
|
1348
|
+
model = settings.ReconstructionSettings(
|
|
1349
|
+
input_channel_names=chNames,
|
|
1350
|
+
birefringence=birefringence,
|
|
1351
|
+
phase=phase,
|
|
1352
|
+
fluorescence=fluorescence,
|
|
1353
|
+
)
|
|
1354
|
+
except ValidationError as exc:
|
|
1355
|
+
# use v1 and v2 differ for ValidationError - newer one is not caught properly
|
|
1356
|
+
return None, exc.errors()
|
|
1357
|
+
|
|
1358
|
+
model = self.fix_model(
|
|
1359
|
+
model, exclude_modes, "input_channel_names", chNames
|
|
1360
|
+
)
|
|
1361
|
+
return model, "+".join(selected_modes) + ": MSG_SUCCESS"
|
|
1362
|
+
|
|
1363
|
+
except Exception as exc:
|
|
1364
|
+
return None, exc.args
|
|
1365
|
+
|
|
1366
|
+
def fix_model(self, model, exclude_modes, attr_key, attr_val):
|
|
1367
|
+
"""Update model attribute while excluding specified modes."""
|
|
1368
|
+
try:
|
|
1369
|
+
data = model.model_dump(exclude=set(exclude_modes))
|
|
1370
|
+
data[attr_key] = attr_val
|
|
1371
|
+
return settings.ReconstructionSettings.model_validate(data)
|
|
1372
|
+
except ValidationError as exc:
|
|
1373
|
+
logging.error(f"fix_model failed: {exc}")
|
|
1374
|
+
return None
|
|
1375
|
+
|
|
1376
|
+
# Creates UI controls from model based on selections
|
|
1377
|
+
def build_acq_contols(self):
|
|
1378
|
+
|
|
1379
|
+
# Make a copy of selections and unsed for deletion
|
|
1380
|
+
selected_modes = []
|
|
1381
|
+
exclude_modes = []
|
|
1382
|
+
|
|
1383
|
+
for mode in self.modes_selected.keys():
|
|
1384
|
+
enabled = self.modes_selected[mode]["Checkbox"].value
|
|
1385
|
+
if not enabled:
|
|
1386
|
+
exclude_modes.append(mode)
|
|
1387
|
+
else:
|
|
1388
|
+
selected_modes.append(mode)
|
|
1389
|
+
|
|
1390
|
+
self.create_acq_contols2(selected_modes, exclude_modes)
|
|
1391
|
+
|
|
1392
|
+
def create_acq_contols2(
|
|
1393
|
+
self,
|
|
1394
|
+
selected_modes,
|
|
1395
|
+
exclude_modes,
|
|
1396
|
+
my_loaded_model=None,
|
|
1397
|
+
json_dict=None,
|
|
1398
|
+
):
|
|
1399
|
+
# duplicate settings from the prev model on new model creation
|
|
1400
|
+
if json_dict is None and len(self.pydantic_classes) > 0:
|
|
1401
|
+
ret = self.build_model_and_run(
|
|
1402
|
+
validate_return_prev_model_json_txt=True
|
|
1403
|
+
)
|
|
1404
|
+
if ret is None:
|
|
1405
|
+
return
|
|
1406
|
+
key, json_txt = ret
|
|
1407
|
+
self.prev_model_settings[key] = json.loads(json_txt)
|
|
1408
|
+
if json_dict is None:
|
|
1409
|
+
key = "-".join(selected_modes)
|
|
1410
|
+
if key in self.prev_model_settings.keys():
|
|
1411
|
+
json_dict = self.prev_model_settings[key]
|
|
1412
|
+
|
|
1413
|
+
# initialize the top container and specify what pydantic class to map from
|
|
1414
|
+
if my_loaded_model is not None:
|
|
1415
|
+
pydantic_class = my_loaded_model
|
|
1416
|
+
else:
|
|
1417
|
+
pydantic_class, ret_msg = self.build_model(selected_modes)
|
|
1418
|
+
if pydantic_class is None:
|
|
1419
|
+
self.message_box(ret_msg)
|
|
1420
|
+
return
|
|
1421
|
+
|
|
1422
|
+
# Final constant UI val and identifier
|
|
1423
|
+
_idx: Final[int] = self.index
|
|
1424
|
+
_str: Final[str] = str(uuid.uuid4())
|
|
1425
|
+
|
|
1426
|
+
# Container holding the pydantic UI components
|
|
1427
|
+
# Multiple instances/copies since more than 1 might be created
|
|
1428
|
+
recon_pydantic_container = widgets.Container(
|
|
1429
|
+
name=_str, scrollable=False
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
self.add_pydantic_to_container(
|
|
1433
|
+
pydantic_class, recon_pydantic_container, exclude_modes, json_dict
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
# Run a validation check to see if the selected options are permitted
|
|
1437
|
+
# before we create the GUI
|
|
1438
|
+
# get the kwargs from the container/class
|
|
1439
|
+
pydantic_kwargs = {}
|
|
1440
|
+
pydantic_kwargs, ret_msg = self.get_and_validate_pydantic_args(
|
|
1441
|
+
recon_pydantic_container,
|
|
1442
|
+
pydantic_class,
|
|
1443
|
+
pydantic_kwargs,
|
|
1444
|
+
exclude_modes,
|
|
1445
|
+
)
|
|
1446
|
+
if pydantic_kwargs is None:
|
|
1447
|
+
self.message_box(ret_msg)
|
|
1448
|
+
return
|
|
1449
|
+
|
|
1450
|
+
# For list element, this needs to be cleaned and parsed back as an array
|
|
1451
|
+
input_channel_names, ret_msg = self.clean_string_for_list(
|
|
1452
|
+
"input_channel_names", pydantic_kwargs["input_channel_names"]
|
|
1453
|
+
)
|
|
1454
|
+
if input_channel_names is None:
|
|
1455
|
+
self.message_box(ret_msg)
|
|
1456
|
+
return
|
|
1457
|
+
pydantic_kwargs["input_channel_names"] = input_channel_names
|
|
1458
|
+
|
|
1459
|
+
time_indices, ret_msg = self.clean_string_int_for_list(
|
|
1460
|
+
"time_indices", pydantic_kwargs["time_indices"]
|
|
1461
|
+
)
|
|
1462
|
+
if time_indices is None:
|
|
1463
|
+
self.message_box(ret_msg)
|
|
1464
|
+
return
|
|
1465
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
1466
|
+
|
|
1467
|
+
if "birefringence" in pydantic_kwargs.keys():
|
|
1468
|
+
background_path, ret_msg = self.clean_path_string_when_empty(
|
|
1469
|
+
"background_path",
|
|
1470
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
1471
|
+
"background_path"
|
|
1472
|
+
],
|
|
1473
|
+
)
|
|
1474
|
+
if background_path is None:
|
|
1475
|
+
self.message_box(ret_msg)
|
|
1476
|
+
return
|
|
1477
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
1478
|
+
"background_path"
|
|
1479
|
+
] = background_path
|
|
1480
|
+
|
|
1481
|
+
# validate and return errors if None
|
|
1482
|
+
pydantic_model, ret_msg = self.validate_pydantic_model(
|
|
1483
|
+
pydantic_class, pydantic_kwargs
|
|
1484
|
+
)
|
|
1485
|
+
if pydantic_model is None:
|
|
1486
|
+
self.message_box(ret_msg)
|
|
1487
|
+
return
|
|
1488
|
+
|
|
1489
|
+
# generate a json from the instantiated model, update the json_display
|
|
1490
|
+
# most of this will end up in a table as processing proceeds
|
|
1491
|
+
json_txt, ret_msg = self.validate_and_return_json(pydantic_model)
|
|
1492
|
+
if json_txt is None:
|
|
1493
|
+
self.message_box(ret_msg)
|
|
1494
|
+
return
|
|
1495
|
+
|
|
1496
|
+
# PushButton to delete a UI container
|
|
1497
|
+
# Use case when a wrong selection of input modes get selected eg Bire+Fl
|
|
1498
|
+
# Preferably this root level validation should occur before values arevalidated
|
|
1499
|
+
# in order to display and avoid this to occur
|
|
1500
|
+
_del_button = widgets.PushButton(name="Delete Model")
|
|
1501
|
+
|
|
1502
|
+
c_mode = "-and-".join(selected_modes)
|
|
1503
|
+
c_mode_short = "".join(
|
|
1504
|
+
item[:3].capitalize() for item in selected_modes
|
|
1505
|
+
)
|
|
1506
|
+
if c_mode in CONTAINERS_INFO.keys():
|
|
1507
|
+
CONTAINERS_INFO[c_mode] += 1
|
|
1508
|
+
else:
|
|
1509
|
+
CONTAINERS_INFO[c_mode] = 1
|
|
1510
|
+
num_str = "{:02d}".format(CONTAINERS_INFO[c_mode])
|
|
1511
|
+
c_mode_str = f"{c_mode} - {num_str}"
|
|
1512
|
+
|
|
1513
|
+
# Output Data location
|
|
1514
|
+
# These could be multiple based on user selection for each model
|
|
1515
|
+
# Inherits from Input by default at creation time
|
|
1516
|
+
name_without_ext = os.path.splitext(Path(self.input_directory).name)[0]
|
|
1517
|
+
save_path = os.path.join(
|
|
1518
|
+
Path(self.output_directory).absolute(),
|
|
1519
|
+
(
|
|
1520
|
+
name_without_ext
|
|
1521
|
+
+ ("_" + c_mode_short + "_" + num_str)
|
|
1522
|
+
+ ".zarr"
|
|
1523
|
+
),
|
|
1524
|
+
)
|
|
1525
|
+
save_path_exists = True if Path(save_path).exists() else False
|
|
1526
|
+
_output_data_loc = widgets.LineEdit(
|
|
1527
|
+
value=Path(save_path).name,
|
|
1528
|
+
tooltip=(
|
|
1529
|
+
""
|
|
1530
|
+
if not save_path_exists
|
|
1531
|
+
else (_validate_alert + " Output file exists")
|
|
1532
|
+
),
|
|
1533
|
+
)
|
|
1534
|
+
_output_data_btn = widgets.PushButton(
|
|
1535
|
+
text=("" if not save_path_exists else (_validate_alert + " "))
|
|
1536
|
+
+ "Output Data:",
|
|
1537
|
+
tooltip=(
|
|
1538
|
+
""
|
|
1539
|
+
if not save_path_exists
|
|
1540
|
+
else (_validate_alert + " Output file exists")
|
|
1541
|
+
),
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
# Passing location label to output location selector
|
|
1545
|
+
_output_data_btn.clicked.connect(
|
|
1546
|
+
lambda: self.browse_model_dir_path_output(_output_data_loc)
|
|
1547
|
+
)
|
|
1548
|
+
_output_data_loc.changed.connect(
|
|
1549
|
+
lambda: self.read_and_set_output_path_on_validation(
|
|
1550
|
+
_output_data_loc, _output_data_btn, save_path
|
|
1551
|
+
)
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
_show_CheckBox = widgets.CheckBox(
|
|
1555
|
+
name="Show after Reconstruction", value=True
|
|
1556
|
+
)
|
|
1557
|
+
_show_CheckBox.max_width = 200
|
|
1558
|
+
_validate_button = widgets.PushButton(name="Validate")
|
|
1559
|
+
|
|
1560
|
+
# Passing all UI components that would be deleted
|
|
1561
|
+
_expandingTabEntryWidget = QWidget()
|
|
1562
|
+
_del_button.clicked.connect(
|
|
1563
|
+
lambda: self.delete_model(
|
|
1564
|
+
_expandingTabEntryWidget,
|
|
1565
|
+
recon_pydantic_container.native,
|
|
1566
|
+
_output_data_loc.native,
|
|
1567
|
+
_output_data_btn.native,
|
|
1568
|
+
_show_CheckBox.native,
|
|
1569
|
+
_validate_button.native,
|
|
1570
|
+
_del_button.native,
|
|
1571
|
+
_str,
|
|
1572
|
+
)
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
# HBox for Output Data
|
|
1576
|
+
_hBox_widget = QWidget()
|
|
1577
|
+
_hBox_layout = QHBoxLayout()
|
|
1578
|
+
_hBox_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
|
1579
|
+
_hBox_widget.setLayout(_hBox_layout)
|
|
1580
|
+
_hBox_layout.addWidget(_output_data_btn.native)
|
|
1581
|
+
_hBox_layout.addWidget(_output_data_loc.native)
|
|
1582
|
+
|
|
1583
|
+
# Add this container to the main scrollable widget
|
|
1584
|
+
_scrollAreaCollapsibleBoxWidgetLayout = QVBoxLayout()
|
|
1585
|
+
_scrollAreaCollapsibleBoxWidgetLayout.setAlignment(
|
|
1586
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
_scrollAreaCollapsibleBoxWidget = MyWidget()
|
|
1590
|
+
_scrollAreaCollapsibleBoxWidget.setLayout(
|
|
1591
|
+
_scrollAreaCollapsibleBoxWidgetLayout
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
_scrollAreaCollapsibleBox = QScrollArea()
|
|
1595
|
+
_scrollAreaCollapsibleBox.setWidgetResizable(True)
|
|
1596
|
+
_scrollAreaCollapsibleBox.setWidget(_scrollAreaCollapsibleBoxWidget)
|
|
1597
|
+
|
|
1598
|
+
_collapsibleBoxWidgetLayout = QVBoxLayout()
|
|
1599
|
+
_collapsibleBoxWidgetLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
1600
|
+
|
|
1601
|
+
scrollbar = _scrollAreaCollapsibleBox.horizontalScrollBar()
|
|
1602
|
+
_scrollAreaCollapsibleBoxWidget.resized.connect(
|
|
1603
|
+
lambda: self.check_scrollbar_visibility(scrollbar)
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
_scrollAreaCollapsibleBoxWidgetLayout.addWidget(
|
|
1607
|
+
scrollbar, alignment=Qt.AlignmentFlag.AlignTop
|
|
1608
|
+
) # Place at the top
|
|
1609
|
+
|
|
1610
|
+
_collapsibleBoxWidgetLayout.addWidget(_scrollAreaCollapsibleBox)
|
|
1611
|
+
|
|
1612
|
+
_collapsibleBoxWidget = CollapsibleBox(
|
|
1613
|
+
title=c_mode_str, expanded=True if _idx == 0 else False
|
|
1614
|
+
) # tableEntryID, tableEntryShortDesc - should update with processing status
|
|
1615
|
+
|
|
1616
|
+
_validate_button.clicked.connect(
|
|
1617
|
+
lambda: self.validate_model(_str, _collapsibleBoxWidget)
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
_hBox_widget2 = QWidget()
|
|
1621
|
+
_hBox_layout2 = QHBoxLayout()
|
|
1622
|
+
_hBox_layout2.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
|
1623
|
+
_hBox_widget2.setLayout(_hBox_layout2)
|
|
1624
|
+
_hBox_layout2.addWidget(_show_CheckBox.native)
|
|
1625
|
+
_hBox_layout2.addWidget(_validate_button.native)
|
|
1626
|
+
_hBox_layout2.addWidget(_del_button.native)
|
|
1627
|
+
|
|
1628
|
+
_expandingTabEntryWidgetLayout = QVBoxLayout()
|
|
1629
|
+
_expandingTabEntryWidgetLayout.setAlignment(
|
|
1630
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
1631
|
+
)
|
|
1632
|
+
_expandingTabEntryWidgetLayout.addWidget(_collapsibleBoxWidget)
|
|
1633
|
+
|
|
1634
|
+
_expandingTabEntryWidget.toolTip = c_mode_str
|
|
1635
|
+
_expandingTabEntryWidget.setLayout(_expandingTabEntryWidgetLayout)
|
|
1636
|
+
_expandingTabEntryWidget.setSizePolicy(
|
|
1637
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1638
|
+
)
|
|
1639
|
+
_expandingTabEntryWidget.layout().setAlignment(
|
|
1640
|
+
QtCore.Qt.AlignmentFlag.AlignTop
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
_scrollAreaCollapsibleBoxWidgetLayout.addWidget(
|
|
1644
|
+
recon_pydantic_container.native
|
|
1645
|
+
)
|
|
1646
|
+
_scrollAreaCollapsibleBoxWidgetLayout.addWidget(_hBox_widget)
|
|
1647
|
+
_scrollAreaCollapsibleBoxWidgetLayout.addWidget(_hBox_widget2)
|
|
1648
|
+
|
|
1649
|
+
_scrollAreaCollapsibleBox.setMinimumHeight(
|
|
1650
|
+
_scrollAreaCollapsibleBoxWidgetLayout.sizeHint().height() + 20
|
|
1651
|
+
)
|
|
1652
|
+
_collapsibleBoxWidget.setSizePolicy(
|
|
1653
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
1654
|
+
)
|
|
1655
|
+
_collapsibleBoxWidget.setContentLayout(_collapsibleBoxWidgetLayout)
|
|
1656
|
+
|
|
1657
|
+
self.models_container_widget_layout.addWidget(_expandingTabEntryWidget)
|
|
1658
|
+
|
|
1659
|
+
# Store a copy of the pydantic container along with all its associated components and properties
|
|
1660
|
+
# We dont needs a copy of the class but storing for now
|
|
1661
|
+
# This will be used for making deletion edits and looping to create our final run output
|
|
1662
|
+
# uuid - used for identiying in editable list
|
|
1663
|
+
self.pydantic_classes.append(
|
|
1664
|
+
{
|
|
1665
|
+
"uuid": _str,
|
|
1666
|
+
"c_mode_str": c_mode_str,
|
|
1667
|
+
"collapsibleBoxWidget": _collapsibleBoxWidget,
|
|
1668
|
+
"class": pydantic_class,
|
|
1669
|
+
"input": self.data_input_LineEdit,
|
|
1670
|
+
"output": os.path.join(
|
|
1671
|
+
Path(self.output_directory).absolute(),
|
|
1672
|
+
_output_data_loc.value,
|
|
1673
|
+
),
|
|
1674
|
+
"output_parent_dir": str(
|
|
1675
|
+
Path(self.output_directory).absolute()
|
|
1676
|
+
),
|
|
1677
|
+
"output_LineEdit": _output_data_loc,
|
|
1678
|
+
"output_Button": _output_data_btn,
|
|
1679
|
+
"container": recon_pydantic_container,
|
|
1680
|
+
"selected_modes": selected_modes.copy(),
|
|
1681
|
+
"exclude_modes": exclude_modes.copy(),
|
|
1682
|
+
"poll_data": self.pollData,
|
|
1683
|
+
"show": _show_CheckBox,
|
|
1684
|
+
}
|
|
1685
|
+
)
|
|
1686
|
+
self.index += 1
|
|
1687
|
+
|
|
1688
|
+
if self.index > 1:
|
|
1689
|
+
self.reconstruction_run_PushButton.text = "RUN {n} Models".format(
|
|
1690
|
+
n=self.index
|
|
1691
|
+
)
|
|
1692
|
+
else:
|
|
1693
|
+
self.reconstruction_run_PushButton.text = "RUN Model"
|
|
1694
|
+
|
|
1695
|
+
return pydantic_model
|
|
1696
|
+
|
|
1697
|
+
def check_scrollbar_visibility(self, scrollbar):
|
|
1698
|
+
h_scrollbar = scrollbar
|
|
1699
|
+
|
|
1700
|
+
# Hide scrollbar if not needed
|
|
1701
|
+
h_scrollbar.setVisible(h_scrollbar.maximum() > h_scrollbar.minimum())
|
|
1702
|
+
|
|
1703
|
+
def validate_model(self, _str, _collapsibleBoxWidget):
|
|
1704
|
+
i = 0
|
|
1705
|
+
model_entry_item = None
|
|
1706
|
+
for item in self.pydantic_classes:
|
|
1707
|
+
if item["uuid"] == _str:
|
|
1708
|
+
model_entry_item = item
|
|
1709
|
+
break
|
|
1710
|
+
i += 1
|
|
1711
|
+
if model_entry_item is not None:
|
|
1712
|
+
cls = item["class"]
|
|
1713
|
+
cls_container = item["container"]
|
|
1714
|
+
exclude_modes = item["exclude_modes"]
|
|
1715
|
+
c_mode_str = item["c_mode_str"]
|
|
1716
|
+
|
|
1717
|
+
# build up the arguments for the pydantic model given the current container
|
|
1718
|
+
if cls is None:
|
|
1719
|
+
self.message_box("No model defined !")
|
|
1720
|
+
return
|
|
1721
|
+
|
|
1722
|
+
pydantic_kwargs = {}
|
|
1723
|
+
pydantic_kwargs, ret_msg = self.get_and_validate_pydantic_args(
|
|
1724
|
+
cls_container, cls, pydantic_kwargs, exclude_modes
|
|
1725
|
+
)
|
|
1726
|
+
if pydantic_kwargs is None:
|
|
1727
|
+
self.message_box(ret_msg)
|
|
1728
|
+
_collapsibleBoxWidget.setNewName(
|
|
1729
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1730
|
+
)
|
|
1731
|
+
return
|
|
1732
|
+
|
|
1733
|
+
input_channel_names, ret_msg = self.clean_string_for_list(
|
|
1734
|
+
"input_channel_names", pydantic_kwargs["input_channel_names"]
|
|
1735
|
+
)
|
|
1736
|
+
if input_channel_names is None:
|
|
1737
|
+
self.message_box(ret_msg)
|
|
1738
|
+
_collapsibleBoxWidget.setNewName(
|
|
1739
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1740
|
+
)
|
|
1741
|
+
return
|
|
1742
|
+
pydantic_kwargs["input_channel_names"] = input_channel_names
|
|
1743
|
+
|
|
1744
|
+
time_indices, ret_msg = self.clean_string_int_for_list(
|
|
1745
|
+
"time_indices", pydantic_kwargs["time_indices"]
|
|
1746
|
+
)
|
|
1747
|
+
if time_indices is None:
|
|
1748
|
+
self.message_box(ret_msg)
|
|
1749
|
+
_collapsibleBoxWidget.setNewName(
|
|
1750
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1751
|
+
)
|
|
1752
|
+
return
|
|
1753
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
1754
|
+
|
|
1755
|
+
time_indices, ret_msg = self.clean_string_int_for_list(
|
|
1756
|
+
"time_indices", pydantic_kwargs["time_indices"]
|
|
1757
|
+
)
|
|
1758
|
+
if time_indices is None:
|
|
1759
|
+
self.message_box(ret_msg)
|
|
1760
|
+
_collapsibleBoxWidget.setNewName(
|
|
1761
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1762
|
+
)
|
|
1763
|
+
return
|
|
1764
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
1765
|
+
|
|
1766
|
+
if "birefringence" in pydantic_kwargs.keys():
|
|
1767
|
+
background_path, ret_msg = self.clean_path_string_when_empty(
|
|
1768
|
+
"background_path",
|
|
1769
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
1770
|
+
"background_path"
|
|
1771
|
+
],
|
|
1772
|
+
)
|
|
1773
|
+
if background_path is None:
|
|
1774
|
+
self.message_box(ret_msg)
|
|
1775
|
+
_collapsibleBoxWidget.setNewName(
|
|
1776
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1777
|
+
)
|
|
1778
|
+
return
|
|
1779
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
1780
|
+
"background_path"
|
|
1781
|
+
] = background_path
|
|
1782
|
+
|
|
1783
|
+
# validate and return errors if None
|
|
1784
|
+
pydantic_model, ret_msg = self.validate_pydantic_model(
|
|
1785
|
+
cls, pydantic_kwargs
|
|
1786
|
+
)
|
|
1787
|
+
if pydantic_model is None:
|
|
1788
|
+
self.message_box(ret_msg)
|
|
1789
|
+
_collapsibleBoxWidget.setNewName(
|
|
1790
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1791
|
+
)
|
|
1792
|
+
return
|
|
1793
|
+
if ret_msg == MSG_SUCCESS:
|
|
1794
|
+
_collapsibleBoxWidget.setNewName(
|
|
1795
|
+
f"{c_mode_str} {_validate_ok}"
|
|
1796
|
+
)
|
|
1797
|
+
else:
|
|
1798
|
+
_collapsibleBoxWidget.setNewName(
|
|
1799
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
# UI components deletion - maybe just needs the parent container instead of individual components
|
|
1803
|
+
def delete_model(self, wid0, wid1, wid2, wid3, wid4, wid5, wid6, _str):
|
|
1804
|
+
|
|
1805
|
+
if not self.confirm_dialog():
|
|
1806
|
+
return False
|
|
1807
|
+
|
|
1808
|
+
if wid0 is not None:
|
|
1809
|
+
wid0.setParent(None)
|
|
1810
|
+
|
|
1811
|
+
# Find and remove the class from our pydantic model list using uuid
|
|
1812
|
+
i = 0
|
|
1813
|
+
for item in self.pydantic_classes:
|
|
1814
|
+
if item["uuid"] == _str:
|
|
1815
|
+
self.pydantic_classes.pop(i)
|
|
1816
|
+
break
|
|
1817
|
+
i += 1
|
|
1818
|
+
self.index = len(self.pydantic_classes)
|
|
1819
|
+
if self.index > 1:
|
|
1820
|
+
self.reconstruction_run_PushButton.text = "RUN {n} Models".format(
|
|
1821
|
+
n=self.index
|
|
1822
|
+
)
|
|
1823
|
+
else:
|
|
1824
|
+
self.reconstruction_run_PushButton.text = "RUN Model"
|
|
1825
|
+
|
|
1826
|
+
# Clear all the generated pydantic models and clears the pydantic model list
|
|
1827
|
+
def clear_all_models(self, silent=False):
|
|
1828
|
+
|
|
1829
|
+
if silent or self.confirm_dialog():
|
|
1830
|
+
index = self.models_container_widget_layout.count() - 1
|
|
1831
|
+
while index >= 0:
|
|
1832
|
+
myWidget = self.models_container_widget_layout.itemAt(
|
|
1833
|
+
index
|
|
1834
|
+
).widget()
|
|
1835
|
+
if myWidget is not None:
|
|
1836
|
+
myWidget.setParent(None)
|
|
1837
|
+
index -= 1
|
|
1838
|
+
self.pydantic_classes.clear()
|
|
1839
|
+
CONTAINERS_INFO.clear()
|
|
1840
|
+
self.index = 0
|
|
1841
|
+
self.reconstruction_run_PushButton.text = "RUN Model"
|
|
1842
|
+
self.prev_model_settings = {}
|
|
1843
|
+
|
|
1844
|
+
# Displays the json output from the pydantic model UI selections by user
|
|
1845
|
+
# Loops through all our stored pydantic classes
|
|
1846
|
+
def build_model_and_run(self, validate_return_prev_model_json_txt=False):
|
|
1847
|
+
# we dont want to have a partial run if there are N models
|
|
1848
|
+
# so we will validate them all first and then run in a second loop
|
|
1849
|
+
# first pass for validating
|
|
1850
|
+
# second pass for creating yaml and processing
|
|
1851
|
+
|
|
1852
|
+
if len(self.pydantic_classes) == 0:
|
|
1853
|
+
self.message_box("Please create a processing model first !")
|
|
1854
|
+
return
|
|
1855
|
+
|
|
1856
|
+
self.model_reset_highlighter_setter() # reset the container elements that might be highlighted for errors
|
|
1857
|
+
_collectAllErrors = {}
|
|
1858
|
+
_collectAllErrorsBool = True
|
|
1859
|
+
for item in self.pydantic_classes:
|
|
1860
|
+
cls = item["class"]
|
|
1861
|
+
cls_container = item["container"]
|
|
1862
|
+
selected_modes = item["selected_modes"]
|
|
1863
|
+
exclude_modes = item["exclude_modes"]
|
|
1864
|
+
uuid_str = item["uuid"]
|
|
1865
|
+
_collapsibleBoxWidget = item["collapsibleBoxWidget"]
|
|
1866
|
+
c_mode_str = item["c_mode_str"]
|
|
1867
|
+
|
|
1868
|
+
_collectAllErrors[uuid_str] = {}
|
|
1869
|
+
_collectAllErrors[uuid_str]["cls"] = cls_container
|
|
1870
|
+
_collectAllErrors[uuid_str]["errs"] = []
|
|
1871
|
+
_collectAllErrors[uuid_str]["collapsibleBox"] = c_mode_str
|
|
1872
|
+
|
|
1873
|
+
# build up the arguments for the pydantic model given the current container
|
|
1874
|
+
if cls is None:
|
|
1875
|
+
self.message_box(ret_msg)
|
|
1876
|
+
return
|
|
1877
|
+
|
|
1878
|
+
# get the kwargs from the container/class
|
|
1879
|
+
pydantic_kwargs = {}
|
|
1880
|
+
pydantic_kwargs, ret_msg = self.get_and_validate_pydantic_args(
|
|
1881
|
+
cls_container, cls, pydantic_kwargs, exclude_modes
|
|
1882
|
+
)
|
|
1883
|
+
if pydantic_kwargs is None and not _collectAllErrorsBool:
|
|
1884
|
+
self.message_box(ret_msg)
|
|
1885
|
+
return
|
|
1886
|
+
|
|
1887
|
+
# For list element, this needs to be cleaned and parsed back as an array
|
|
1888
|
+
input_channel_names, ret_msg = self.clean_string_for_list(
|
|
1889
|
+
"input_channel_names", pydantic_kwargs["input_channel_names"]
|
|
1890
|
+
)
|
|
1891
|
+
if input_channel_names is None and not _collectAllErrorsBool:
|
|
1892
|
+
self.message_box(ret_msg)
|
|
1893
|
+
return
|
|
1894
|
+
pydantic_kwargs["input_channel_names"] = input_channel_names
|
|
1895
|
+
|
|
1896
|
+
time_indices, ret_msg = self.clean_string_int_for_list(
|
|
1897
|
+
"time_indices", pydantic_kwargs["time_indices"]
|
|
1898
|
+
)
|
|
1899
|
+
if time_indices is None and not _collectAllErrorsBool:
|
|
1900
|
+
self.message_box(ret_msg)
|
|
1901
|
+
return
|
|
1902
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
1903
|
+
|
|
1904
|
+
if "birefringence" in pydantic_kwargs.keys():
|
|
1905
|
+
background_path, ret_msg = self.clean_path_string_when_empty(
|
|
1906
|
+
"background_path",
|
|
1907
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
1908
|
+
"background_path"
|
|
1909
|
+
],
|
|
1910
|
+
)
|
|
1911
|
+
if background_path is None and not _collectAllErrorsBool:
|
|
1912
|
+
self.message_box(ret_msg)
|
|
1913
|
+
return
|
|
1914
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
1915
|
+
"background_path"
|
|
1916
|
+
] = background_path
|
|
1917
|
+
|
|
1918
|
+
# validate and return errors if None
|
|
1919
|
+
pydantic_model, ret_msg = self.validate_pydantic_model(
|
|
1920
|
+
cls, pydantic_kwargs
|
|
1921
|
+
)
|
|
1922
|
+
if ret_msg == MSG_SUCCESS:
|
|
1923
|
+
_collapsibleBoxWidget.setNewName(
|
|
1924
|
+
f"{c_mode_str} {_validate_ok}"
|
|
1925
|
+
)
|
|
1926
|
+
else:
|
|
1927
|
+
_collapsibleBoxWidget.setNewName(
|
|
1928
|
+
f"{c_mode_str} {_validate_alert}"
|
|
1929
|
+
)
|
|
1930
|
+
_collectAllErrors[uuid_str]["errs"] = ret_msg
|
|
1931
|
+
if pydantic_model is None and not _collectAllErrorsBool:
|
|
1932
|
+
self.message_box(ret_msg)
|
|
1933
|
+
return
|
|
1934
|
+
|
|
1935
|
+
# generate a json from the instantiated model, update the json_display
|
|
1936
|
+
# most of this will end up in a table as processing proceeds
|
|
1937
|
+
json_txt, ret_msg = self.validate_and_return_json(pydantic_model)
|
|
1938
|
+
if json_txt is None and not _collectAllErrorsBool:
|
|
1939
|
+
self.message_box(ret_msg)
|
|
1940
|
+
return
|
|
1941
|
+
|
|
1942
|
+
# check if we collected any validation errors before continuing
|
|
1943
|
+
for uu_key in _collectAllErrors.keys():
|
|
1944
|
+
if len(_collectAllErrors[uu_key]["errs"]) > 0:
|
|
1945
|
+
self.model_highlighter(_collectAllErrors)
|
|
1946
|
+
fmt_str = self.format_string_for_error_display(
|
|
1947
|
+
_collectAllErrors
|
|
1948
|
+
)
|
|
1949
|
+
self.message_box(fmt_str)
|
|
1950
|
+
return
|
|
1951
|
+
|
|
1952
|
+
if validate_return_prev_model_json_txt:
|
|
1953
|
+
return "-".join(selected_modes), json_txt
|
|
1954
|
+
|
|
1955
|
+
# generate a time-stamp for our yaml files to avoid overwriting
|
|
1956
|
+
# files generated at the same time will have an index suffix
|
|
1957
|
+
now = datetime.datetime.now()
|
|
1958
|
+
ms = now.strftime("%f")[:3]
|
|
1959
|
+
unique_id = now.strftime("%Y_%m_%d_%H_%M_%S_") + ms
|
|
1960
|
+
|
|
1961
|
+
if self.pollData:
|
|
1962
|
+
data = open_ome_zarr(self.input_directory, mode="r")
|
|
1963
|
+
if "CurrentDimensions" in data.zattrs.keys():
|
|
1964
|
+
my_dict_time_indices = data.zattrs["CurrentDimensions"]["time"]
|
|
1965
|
+
# get the prev time_index, since this is current acq
|
|
1966
|
+
if my_dict_time_indices - 1 > 1:
|
|
1967
|
+
time_indices = list(range(0, my_dict_time_indices))
|
|
1968
|
+
else:
|
|
1969
|
+
time_indices = 0
|
|
1970
|
+
|
|
1971
|
+
pollDataThread = threading.Thread(
|
|
1972
|
+
target=self.add_poll_loop,
|
|
1973
|
+
args=(self.input_directory, my_dict_time_indices - 1),
|
|
1974
|
+
)
|
|
1975
|
+
pollDataThread.start()
|
|
1976
|
+
|
|
1977
|
+
i = 0
|
|
1978
|
+
for item in self.pydantic_classes:
|
|
1979
|
+
i += 1
|
|
1980
|
+
cls = item["class"]
|
|
1981
|
+
cls_container = item["container"]
|
|
1982
|
+
selected_modes = item["selected_modes"]
|
|
1983
|
+
exclude_modes = item["exclude_modes"]
|
|
1984
|
+
c_mode_str = item["c_mode_str"]
|
|
1985
|
+
output_LineEdit = item["output_LineEdit"]
|
|
1986
|
+
output_parent_dir = item["output_parent_dir"]
|
|
1987
|
+
|
|
1988
|
+
full_out_path = os.path.join(
|
|
1989
|
+
output_parent_dir, output_LineEdit.value
|
|
1990
|
+
)
|
|
1991
|
+
|
|
1992
|
+
# gather input/out locations
|
|
1993
|
+
input_dir = f"{item['input'].value}"
|
|
1994
|
+
output_dir = full_out_path
|
|
1995
|
+
|
|
1996
|
+
# build up the arguments for the pydantic model given the current container
|
|
1997
|
+
if cls is None:
|
|
1998
|
+
self.message_box("No model defined !")
|
|
1999
|
+
return
|
|
2000
|
+
|
|
2001
|
+
pydantic_kwargs = {}
|
|
2002
|
+
pydantic_kwargs, ret_msg = self.get_and_validate_pydantic_args(
|
|
2003
|
+
cls_container, cls, pydantic_kwargs, exclude_modes
|
|
2004
|
+
)
|
|
2005
|
+
if pydantic_kwargs is None:
|
|
2006
|
+
self.message_box(ret_msg)
|
|
2007
|
+
return
|
|
2008
|
+
|
|
2009
|
+
input_channel_names, ret_msg = self.clean_string_for_list(
|
|
2010
|
+
"input_channel_names", pydantic_kwargs["input_channel_names"]
|
|
2011
|
+
)
|
|
2012
|
+
if input_channel_names is None:
|
|
2013
|
+
self.message_box(ret_msg)
|
|
2014
|
+
return
|
|
2015
|
+
pydantic_kwargs["input_channel_names"] = input_channel_names
|
|
2016
|
+
|
|
2017
|
+
if not self.pollData:
|
|
2018
|
+
time_indices, ret_msg = self.clean_string_int_for_list(
|
|
2019
|
+
"time_indices", pydantic_kwargs["time_indices"]
|
|
2020
|
+
)
|
|
2021
|
+
if time_indices is None:
|
|
2022
|
+
self.message_box(ret_msg)
|
|
2023
|
+
return
|
|
2024
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
2025
|
+
|
|
2026
|
+
time_indices, ret_msg = self.clean_string_int_for_list(
|
|
2027
|
+
"time_indices", pydantic_kwargs["time_indices"]
|
|
2028
|
+
)
|
|
2029
|
+
if time_indices is None:
|
|
2030
|
+
self.message_box(ret_msg)
|
|
2031
|
+
return
|
|
2032
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
2033
|
+
|
|
2034
|
+
if "birefringence" in pydantic_kwargs.keys():
|
|
2035
|
+
background_path, ret_msg = self.clean_path_string_when_empty(
|
|
2036
|
+
"background_path",
|
|
2037
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
2038
|
+
"background_path"
|
|
2039
|
+
],
|
|
2040
|
+
)
|
|
2041
|
+
if background_path is None:
|
|
2042
|
+
self.message_box(ret_msg)
|
|
2043
|
+
return
|
|
2044
|
+
pydantic_kwargs["birefringence"]["apply_inverse"][
|
|
2045
|
+
"background_path"
|
|
2046
|
+
] = background_path
|
|
2047
|
+
|
|
2048
|
+
# validate and return errors if None
|
|
2049
|
+
pydantic_model, ret_msg = self.validate_pydantic_model(
|
|
2050
|
+
cls, pydantic_kwargs
|
|
2051
|
+
)
|
|
2052
|
+
if pydantic_model is None:
|
|
2053
|
+
self.message_box(ret_msg)
|
|
2054
|
+
return
|
|
2055
|
+
|
|
2056
|
+
# generate a json from the instantiated model, update the json_display
|
|
2057
|
+
# most of this will end up in a table as processing proceeds
|
|
2058
|
+
json_txt, ret_msg = self.validate_and_return_json(pydantic_model)
|
|
2059
|
+
if json_txt is None:
|
|
2060
|
+
self.message_box(ret_msg)
|
|
2061
|
+
return
|
|
2062
|
+
|
|
2063
|
+
# save the yaml files
|
|
2064
|
+
# path is next to saved data location
|
|
2065
|
+
save_config_path = str(Path(output_dir).parent.absolute())
|
|
2066
|
+
yml_file_name = "-and-".join(selected_modes)
|
|
2067
|
+
yml_file = (
|
|
2068
|
+
yml_file_name + "-" + unique_id + "-{:02d}".format(i) + ".yml"
|
|
2069
|
+
)
|
|
2070
|
+
config_path = os.path.join(save_config_path, yml_file)
|
|
2071
|
+
utils.model_to_yaml(pydantic_model, config_path)
|
|
2072
|
+
|
|
2073
|
+
# Input params for table entry
|
|
2074
|
+
# Once ALL entries are entered we can deleted ALL model containers
|
|
2075
|
+
# Table will need a low priority update thread to refresh status queried from CLI
|
|
2076
|
+
# Table entries will be purged on completion when Result is returned OK
|
|
2077
|
+
# Table entries will show an error msg when processing finishes but Result not OK
|
|
2078
|
+
# Table fields ID / DateTime, Reconstruction type, Input Location, Output Location, Progress indicator, Stop button
|
|
2079
|
+
|
|
2080
|
+
expID = "{tID}-{idx}".format(tID=unique_id, idx=i)
|
|
2081
|
+
tableID = "{tName}: ({tID}-{idx})".format(
|
|
2082
|
+
tName=c_mode_str, tID=unique_id, idx=i
|
|
2083
|
+
)
|
|
2084
|
+
tableDescToolTip = "{tName}: ({tID}-{idx})".format(
|
|
2085
|
+
tName=yml_file_name, tID=unique_id, idx=i
|
|
2086
|
+
)
|
|
2087
|
+
|
|
2088
|
+
proc_params = {}
|
|
2089
|
+
proc_params["exp_id"] = expID
|
|
2090
|
+
proc_params["desc"] = tableDescToolTip
|
|
2091
|
+
proc_params["config_path"] = str(Path(config_path).absolute())
|
|
2092
|
+
proc_params["input_path"] = str(Path(input_dir).absolute())
|
|
2093
|
+
proc_params["output_path"] = str(Path(output_dir).absolute())
|
|
2094
|
+
proc_params["output_path_parent"] = str(
|
|
2095
|
+
Path(output_dir).parent.absolute()
|
|
2096
|
+
)
|
|
2097
|
+
proc_params["show"] = item["show"].value
|
|
2098
|
+
|
|
2099
|
+
self.addTableEntry(tableID, tableDescToolTip, proc_params)
|
|
2100
|
+
|
|
2101
|
+
def add_poll_loop(self, input_data_path, last_time_index):
|
|
2102
|
+
_pydantic_classes = self.pydantic_classes.copy()
|
|
2103
|
+
required_order = ["time", "position", "z", "channel"]
|
|
2104
|
+
_pollData = True
|
|
2105
|
+
|
|
2106
|
+
tableEntryWorker = AddOTFTableEntryWorkerThread(
|
|
2107
|
+
input_data_path, True, False
|
|
2108
|
+
)
|
|
2109
|
+
tableEntryWorker.add_tableOTFentry_signal.connect(
|
|
2110
|
+
self.add_remove_check_OTF_table_entry
|
|
2111
|
+
)
|
|
2112
|
+
tableEntryWorker.start()
|
|
2113
|
+
_breakFlag = False
|
|
2114
|
+
while True:
|
|
2115
|
+
time.sleep(10)
|
|
2116
|
+
zattrs_data = None
|
|
2117
|
+
try:
|
|
2118
|
+
_stopCalled = self.add_remove_check_OTF_table_entry(
|
|
2119
|
+
input_data_path, True, do_check=True
|
|
2120
|
+
)
|
|
2121
|
+
if _stopCalled:
|
|
2122
|
+
tableEntryWorker2 = AddOTFTableEntryWorkerThread(
|
|
2123
|
+
input_data_path, False, False
|
|
2124
|
+
)
|
|
2125
|
+
tableEntryWorker2.add_tableOTFentry_signal.connect(
|
|
2126
|
+
self.add_remove_check_OTF_table_entry
|
|
2127
|
+
)
|
|
2128
|
+
tableEntryWorker2.start()
|
|
2129
|
+
|
|
2130
|
+
# let child threads finish their work before exiting the parent thread
|
|
2131
|
+
while tableEntryWorker2.isRunning():
|
|
2132
|
+
time.sleep(1)
|
|
2133
|
+
time.sleep(5)
|
|
2134
|
+
break
|
|
2135
|
+
try:
|
|
2136
|
+
data = open_ome_zarr(input_data_path, mode="r")
|
|
2137
|
+
zattrs_data = data.zattrs
|
|
2138
|
+
except PermissionError:
|
|
2139
|
+
pass # On-The-Fly dataset will throw Permission Denied when being written
|
|
2140
|
+
# Maybe we can read the zaatrs directly in that case
|
|
2141
|
+
# If this write/read is a constant issue then the zattrs 'CurrentDimensions' key
|
|
2142
|
+
# should be updated less frequently, instead of current design of updating with
|
|
2143
|
+
# each image
|
|
2144
|
+
|
|
2145
|
+
if zattrs_data is None:
|
|
2146
|
+
zattrs_data = self.load_zattrs_directly_as_dict(
|
|
2147
|
+
input_data_path
|
|
2148
|
+
)
|
|
2149
|
+
|
|
2150
|
+
if zattrs_data is not None:
|
|
2151
|
+
if "CurrentDimensions" in zattrs_data.keys():
|
|
2152
|
+
my_dict1 = zattrs_data["CurrentDimensions"]
|
|
2153
|
+
sorted_dict_acq = {
|
|
2154
|
+
k: my_dict1[k]
|
|
2155
|
+
for k in sorted(
|
|
2156
|
+
my_dict1, key=lambda x: required_order.index(x)
|
|
2157
|
+
)
|
|
2158
|
+
}
|
|
2159
|
+
my_dict_time_indices_curr = zattrs_data[
|
|
2160
|
+
"CurrentDimensions"
|
|
2161
|
+
]["time"]
|
|
2162
|
+
# print(sorted_dict_acq)
|
|
2163
|
+
|
|
2164
|
+
if "FinalDimensions" in zattrs_data.keys():
|
|
2165
|
+
my_dict2 = zattrs_data["FinalDimensions"]
|
|
2166
|
+
sorted_dict_final = {
|
|
2167
|
+
k: my_dict2[k]
|
|
2168
|
+
for k in sorted(
|
|
2169
|
+
my_dict2, key=lambda x: required_order.index(x)
|
|
2170
|
+
)
|
|
2171
|
+
}
|
|
2172
|
+
# print(sorted_dict_final)
|
|
2173
|
+
|
|
2174
|
+
# use the prev time_index, since this is current acq and we need for other dims to finish acq for this t
|
|
2175
|
+
# or when all dims match - signifying acq finished
|
|
2176
|
+
if (
|
|
2177
|
+
my_dict_time_indices_curr - 2 > last_time_index
|
|
2178
|
+
or json.dumps(sorted_dict_acq)
|
|
2179
|
+
== json.dumps(sorted_dict_final)
|
|
2180
|
+
):
|
|
2181
|
+
|
|
2182
|
+
now = datetime.datetime.now()
|
|
2183
|
+
ms = now.strftime("%f")[:3]
|
|
2184
|
+
unique_id = now.strftime("%Y_%m_%d_%H_%M_%S_") + ms
|
|
2185
|
+
|
|
2186
|
+
i = 0
|
|
2187
|
+
for item in _pydantic_classes:
|
|
2188
|
+
i += 1
|
|
2189
|
+
cls = item["class"]
|
|
2190
|
+
cls_container = item["container"]
|
|
2191
|
+
selected_modes = item["selected_modes"]
|
|
2192
|
+
exclude_modes = item["exclude_modes"]
|
|
2193
|
+
c_mode_str = item["c_mode_str"]
|
|
2194
|
+
output_LineEdit = item["output_LineEdit"]
|
|
2195
|
+
output_parent_dir = item["output_parent_dir"]
|
|
2196
|
+
|
|
2197
|
+
full_out_path = os.path.join(
|
|
2198
|
+
output_parent_dir, output_LineEdit.value
|
|
2199
|
+
)
|
|
2200
|
+
# gather input/out locations
|
|
2201
|
+
input_dir = f"{item['input'].value}"
|
|
2202
|
+
output_dir = full_out_path
|
|
2203
|
+
|
|
2204
|
+
pydantic_kwargs = {}
|
|
2205
|
+
pydantic_kwargs, ret_msg = (
|
|
2206
|
+
self.get_and_validate_pydantic_args(
|
|
2207
|
+
cls_container,
|
|
2208
|
+
cls,
|
|
2209
|
+
pydantic_kwargs,
|
|
2210
|
+
exclude_modes,
|
|
2211
|
+
)
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
input_channel_names, ret_msg = (
|
|
2215
|
+
self.clean_string_for_list(
|
|
2216
|
+
"input_channel_names",
|
|
2217
|
+
pydantic_kwargs["input_channel_names"],
|
|
2218
|
+
)
|
|
2219
|
+
)
|
|
2220
|
+
pydantic_kwargs["input_channel_names"] = (
|
|
2221
|
+
input_channel_names
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
if _pollData:
|
|
2225
|
+
if json.dumps(sorted_dict_acq) == json.dumps(
|
|
2226
|
+
sorted_dict_final
|
|
2227
|
+
):
|
|
2228
|
+
time_indices = list(
|
|
2229
|
+
range(
|
|
2230
|
+
last_time_index,
|
|
2231
|
+
my_dict_time_indices_curr,
|
|
2232
|
+
)
|
|
2233
|
+
)
|
|
2234
|
+
_breakFlag = True
|
|
2235
|
+
else:
|
|
2236
|
+
time_indices = list(
|
|
2237
|
+
range(
|
|
2238
|
+
last_time_index,
|
|
2239
|
+
my_dict_time_indices_curr - 2,
|
|
2240
|
+
)
|
|
2241
|
+
)
|
|
2242
|
+
pydantic_kwargs["time_indices"] = time_indices
|
|
2243
|
+
|
|
2244
|
+
if "birefringence" in pydantic_kwargs.keys():
|
|
2245
|
+
background_path, ret_msg = (
|
|
2246
|
+
self.clean_path_string_when_empty(
|
|
2247
|
+
"background_path",
|
|
2248
|
+
pydantic_kwargs["birefringence"][
|
|
2249
|
+
"apply_inverse"
|
|
2250
|
+
]["background_path"],
|
|
2251
|
+
)
|
|
2252
|
+
)
|
|
2253
|
+
|
|
2254
|
+
pydantic_kwargs["birefringence"][
|
|
2255
|
+
"apply_inverse"
|
|
2256
|
+
]["background_path"] = background_path
|
|
2257
|
+
|
|
2258
|
+
# validate and return errors if None
|
|
2259
|
+
pydantic_model, ret_msg = (
|
|
2260
|
+
self.validate_pydantic_model(
|
|
2261
|
+
cls, pydantic_kwargs
|
|
2262
|
+
)
|
|
2263
|
+
)
|
|
2264
|
+
|
|
2265
|
+
# save the yaml files
|
|
2266
|
+
# path is next to saved data location
|
|
2267
|
+
save_config_path = str(
|
|
2268
|
+
Path(output_dir).parent.absolute()
|
|
2269
|
+
)
|
|
2270
|
+
yml_file_name = "-and-".join(selected_modes)
|
|
2271
|
+
yml_file = (
|
|
2272
|
+
yml_file_name
|
|
2273
|
+
+ "-"
|
|
2274
|
+
+ unique_id
|
|
2275
|
+
+ "-{:02d}".format(i)
|
|
2276
|
+
+ ".yml"
|
|
2277
|
+
)
|
|
2278
|
+
config_path = os.path.join(
|
|
2279
|
+
save_config_path, yml_file
|
|
2280
|
+
)
|
|
2281
|
+
utils.model_to_yaml(pydantic_model, config_path)
|
|
2282
|
+
|
|
2283
|
+
expID = "{tID}-{idx}".format(tID=unique_id, idx=i)
|
|
2284
|
+
tableID = "{tName}: ({tID}-{idx})".format(
|
|
2285
|
+
tName=c_mode_str, tID=unique_id, idx=i
|
|
2286
|
+
)
|
|
2287
|
+
tableDescToolTip = "{tName}: ({tID}-{idx})".format(
|
|
2288
|
+
tName=yml_file_name, tID=unique_id, idx=i
|
|
2289
|
+
)
|
|
2290
|
+
|
|
2291
|
+
proc_params = {}
|
|
2292
|
+
proc_params["exp_id"] = expID
|
|
2293
|
+
proc_params["desc"] = tableDescToolTip
|
|
2294
|
+
proc_params["config_path"] = str(
|
|
2295
|
+
Path(config_path).absolute()
|
|
2296
|
+
)
|
|
2297
|
+
proc_params["input_path"] = str(
|
|
2298
|
+
Path(input_dir).absolute()
|
|
2299
|
+
)
|
|
2300
|
+
proc_params["output_path"] = str(
|
|
2301
|
+
Path(output_dir).absolute()
|
|
2302
|
+
)
|
|
2303
|
+
proc_params["output_path_parent"] = str(
|
|
2304
|
+
Path(output_dir).parent.absolute()
|
|
2305
|
+
)
|
|
2306
|
+
proc_params["show"] = False
|
|
2307
|
+
|
|
2308
|
+
tableEntryWorker1 = AddTableEntryWorkerThread(
|
|
2309
|
+
tableID, tableDescToolTip, proc_params
|
|
2310
|
+
)
|
|
2311
|
+
tableEntryWorker1.add_tableentry_signal.connect(
|
|
2312
|
+
self.addTableEntry
|
|
2313
|
+
)
|
|
2314
|
+
tableEntryWorker1.start()
|
|
2315
|
+
|
|
2316
|
+
if (
|
|
2317
|
+
json.dumps(sorted_dict_acq)
|
|
2318
|
+
== json.dumps(sorted_dict_final)
|
|
2319
|
+
and _breakFlag
|
|
2320
|
+
):
|
|
2321
|
+
|
|
2322
|
+
tableEntryWorker2 = AddOTFTableEntryWorkerThread(
|
|
2323
|
+
input_data_path, False, False
|
|
2324
|
+
)
|
|
2325
|
+
tableEntryWorker2.add_tableOTFentry_signal.connect(
|
|
2326
|
+
self.add_remove_check_OTF_table_entry
|
|
2327
|
+
)
|
|
2328
|
+
tableEntryWorker2.start()
|
|
2329
|
+
|
|
2330
|
+
# let child threads finish their work before exiting the parent thread
|
|
2331
|
+
while (
|
|
2332
|
+
tableEntryWorker1.isRunning()
|
|
2333
|
+
or tableEntryWorker2.isRunning()
|
|
2334
|
+
):
|
|
2335
|
+
time.sleep(1)
|
|
2336
|
+
time.sleep(5)
|
|
2337
|
+
break
|
|
2338
|
+
|
|
2339
|
+
last_time_index = my_dict_time_indices_curr - 2
|
|
2340
|
+
except Exception as exc:
|
|
2341
|
+
print(exc.args)
|
|
2342
|
+
print(
|
|
2343
|
+
"Exiting polling for dataset: {data_path}".format(
|
|
2344
|
+
data_path=input_data_path
|
|
2345
|
+
)
|
|
2346
|
+
)
|
|
2347
|
+
break
|
|
2348
|
+
|
|
2349
|
+
def load_zattrs_directly_as_dict(self, zattrsFilePathDir):
|
|
2350
|
+
try:
|
|
2351
|
+
file_path = os.path.join(zattrsFilePathDir, ".zattrs")
|
|
2352
|
+
f = open(file_path, "r")
|
|
2353
|
+
txt = f.read()
|
|
2354
|
+
f.close()
|
|
2355
|
+
return json.loads(txt)
|
|
2356
|
+
except Exception as exc:
|
|
2357
|
+
print(exc.args)
|
|
2358
|
+
return None
|
|
2359
|
+
|
|
2360
|
+
# ======= These function do not implement validation
|
|
2361
|
+
# They simply make the data from GUI translate to input types
|
|
2362
|
+
# that the model expects: for eg. GUI txt field will output only str
|
|
2363
|
+
# when the model needs integers
|
|
2364
|
+
|
|
2365
|
+
# util function to parse list elements displayed as string
|
|
2366
|
+
def remove_chars(self, string, chars_to_remove):
|
|
2367
|
+
for char in chars_to_remove:
|
|
2368
|
+
string = string.replace(char, "")
|
|
2369
|
+
return string
|
|
2370
|
+
|
|
2371
|
+
# util function to parse list elements displayed as string
|
|
2372
|
+
def clean_string_for_list(self, field, string):
|
|
2373
|
+
chars_to_remove = ["[", "]", "'", '"', " "]
|
|
2374
|
+
if isinstance(string, str):
|
|
2375
|
+
string = self.remove_chars(string, chars_to_remove)
|
|
2376
|
+
if len(string) == 0:
|
|
2377
|
+
return None, {"msg": field + " is invalid"}
|
|
2378
|
+
if "," in string:
|
|
2379
|
+
string = string.split(",")
|
|
2380
|
+
return string, MSG_SUCCESS
|
|
2381
|
+
if isinstance(string, str):
|
|
2382
|
+
string = [string]
|
|
2383
|
+
return string, MSG_SUCCESS
|
|
2384
|
+
return string, MSG_SUCCESS
|
|
2385
|
+
|
|
2386
|
+
# util function to parse list elements displayed as string, int, int as list of strings, int range
|
|
2387
|
+
# [1,2,3], 4,5,6 , 5-95
|
|
2388
|
+
def clean_string_int_for_list(self, field, string):
|
|
2389
|
+
chars_to_remove = ["[", "]", "'", '"', " "]
|
|
2390
|
+
if Literal[string] == Literal["all"]:
|
|
2391
|
+
return string, MSG_SUCCESS
|
|
2392
|
+
if Literal[string] == Literal[""]:
|
|
2393
|
+
return string, MSG_SUCCESS
|
|
2394
|
+
if isinstance(string, str):
|
|
2395
|
+
string = self.remove_chars(string, chars_to_remove)
|
|
2396
|
+
if len(string) == 0:
|
|
2397
|
+
return None, {"msg": field + " is invalid"}
|
|
2398
|
+
if "-" in string:
|
|
2399
|
+
string = string.split("-")
|
|
2400
|
+
if len(string) == 2:
|
|
2401
|
+
try:
|
|
2402
|
+
x = int(string[0])
|
|
2403
|
+
if not isinstance(x, int):
|
|
2404
|
+
raise
|
|
2405
|
+
except Exception as exc:
|
|
2406
|
+
return None, {
|
|
2407
|
+
"msg": field + " first range element is not an integer"
|
|
2408
|
+
}
|
|
2409
|
+
try:
|
|
2410
|
+
y = int(string[1])
|
|
2411
|
+
if not isinstance(y, int):
|
|
2412
|
+
raise
|
|
2413
|
+
except Exception as exc:
|
|
2414
|
+
return None, {
|
|
2415
|
+
"msg": field
|
|
2416
|
+
+ " second range element is not an integer"
|
|
2417
|
+
}
|
|
2418
|
+
if y > x:
|
|
2419
|
+
return list(range(x, y + 1)), MSG_SUCCESS
|
|
2420
|
+
else:
|
|
2421
|
+
return None, {
|
|
2422
|
+
"msg": field
|
|
2423
|
+
+ " second integer cannot be smaller than first"
|
|
2424
|
+
}
|
|
2425
|
+
else:
|
|
2426
|
+
return None, {"msg": field + " is invalid"}
|
|
2427
|
+
if "," in string:
|
|
2428
|
+
string = string.split(",")
|
|
2429
|
+
return string, MSG_SUCCESS
|
|
2430
|
+
return string, MSG_SUCCESS
|
|
2431
|
+
|
|
2432
|
+
# util function to set path to empty - by default empty path has a "."
|
|
2433
|
+
def clean_path_string_when_empty(self, field, string):
|
|
2434
|
+
if isinstance(string, Path) and string == Path(""):
|
|
2435
|
+
string = ""
|
|
2436
|
+
return string, MSG_SUCCESS
|
|
2437
|
+
return string, MSG_SUCCESS
|
|
2438
|
+
|
|
2439
|
+
# get the pydantic_kwargs and catches any errors in doing so
|
|
2440
|
+
def get_and_validate_pydantic_args(
|
|
2441
|
+
self, cls_container, cls, pydantic_kwargs, exclude_modes
|
|
2442
|
+
):
|
|
2443
|
+
try:
|
|
2444
|
+
try:
|
|
2445
|
+
self.get_pydantic_kwargs(
|
|
2446
|
+
cls_container, cls, pydantic_kwargs, exclude_modes
|
|
2447
|
+
)
|
|
2448
|
+
return pydantic_kwargs, MSG_SUCCESS
|
|
2449
|
+
except ValidationError as exc:
|
|
2450
|
+
return None, exc.errors()
|
|
2451
|
+
except Exception as exc:
|
|
2452
|
+
return None, exc.args
|
|
2453
|
+
|
|
2454
|
+
# validate the model and return errors for user actioning
|
|
2455
|
+
def validate_pydantic_model(self, cls, pydantic_kwargs):
|
|
2456
|
+
# instantiate the pydantic model form the kwargs we just pulled
|
|
2457
|
+
try:
|
|
2458
|
+
try:
|
|
2459
|
+
pydantic_model = (
|
|
2460
|
+
settings.ReconstructionSettings.model_validate(
|
|
2461
|
+
pydantic_kwargs
|
|
2462
|
+
)
|
|
2463
|
+
)
|
|
2464
|
+
return pydantic_model, MSG_SUCCESS
|
|
2465
|
+
except ValidationError as exc:
|
|
2466
|
+
return None, exc.errors()
|
|
2467
|
+
except Exception as exc:
|
|
2468
|
+
return None, exc.args
|
|
2469
|
+
|
|
2470
|
+
# test to make sure model converts to json which should ensure compatibility with yaml export
|
|
2471
|
+
def validate_and_return_json(self, pydantic_model):
|
|
2472
|
+
try:
|
|
2473
|
+
json_format = pydantic_model.model_dump_json(indent=4)
|
|
2474
|
+
return json_format, MSG_SUCCESS
|
|
2475
|
+
except Exception as exc:
|
|
2476
|
+
return None, exc.args
|
|
2477
|
+
|
|
2478
|
+
# gets a copy of the model from a yaml file
|
|
2479
|
+
# will get all fields (even those that are optional and not in yaml) and default values
|
|
2480
|
+
# model needs further parsing against yaml file for fields
|
|
2481
|
+
def get_model_from_file(self, model_file_path):
|
|
2482
|
+
pydantic_model = None
|
|
2483
|
+
try:
|
|
2484
|
+
try:
|
|
2485
|
+
pydantic_model = utils.yaml_to_model(
|
|
2486
|
+
model_file_path, settings.ReconstructionSettings
|
|
2487
|
+
)
|
|
2488
|
+
except ValidationError as exc:
|
|
2489
|
+
return pydantic_model, exc.errors()
|
|
2490
|
+
if pydantic_model is None:
|
|
2491
|
+
raise Exception("utils.yaml_to_model - returned a None model")
|
|
2492
|
+
return pydantic_model, MSG_SUCCESS
|
|
2493
|
+
except Exception as exc:
|
|
2494
|
+
return None, exc.args
|
|
2495
|
+
|
|
2496
|
+
# handles json with boolean properly and converts to lowercase string
|
|
2497
|
+
# as required
|
|
2498
|
+
def convert(self, obj):
|
|
2499
|
+
if isinstance(obj, bool):
|
|
2500
|
+
return str(obj).lower()
|
|
2501
|
+
if isinstance(obj, (list, tuple)):
|
|
2502
|
+
return [self.convert(item) for item in obj]
|
|
2503
|
+
if isinstance(obj, dict):
|
|
2504
|
+
return {
|
|
2505
|
+
self.convert(key): self.convert(value)
|
|
2506
|
+
for key, value in obj.items()
|
|
2507
|
+
}
|
|
2508
|
+
return obj
|
|
2509
|
+
|
|
2510
|
+
# Main function to add pydantic model to container
|
|
2511
|
+
# https://github.com/chrishavlin/miscellaneous_python/blob/main/src/pydantic_magicgui_roundtrip.py
|
|
2512
|
+
# Has limitation and can cause breakages for unhandled or incorrectly handled types
|
|
2513
|
+
# Cannot handle Union types/typing - for now being handled explicitly
|
|
2514
|
+
# Ignoring NoneType since those should be Optional but maybe needs displaying ??
|
|
2515
|
+
# ToDo: Needs revisitation, Union check
|
|
2516
|
+
# Displaying Union field "time_indices" as LineEdit component
|
|
2517
|
+
# excludes handles fields that are not supposed to show up from model_fields
|
|
2518
|
+
# json_dict adds ability to provide new set of default values at time of container creation
|
|
2519
|
+
|
|
2520
|
+
def add_pydantic_to_container(
|
|
2521
|
+
self,
|
|
2522
|
+
py_model: type[BaseModel] | BaseModel,
|
|
2523
|
+
container: widgets.Container,
|
|
2524
|
+
excludes=None,
|
|
2525
|
+
json_dict=None,
|
|
2526
|
+
):
|
|
2527
|
+
# recursively traverse a pydantic model adding widgets to a container. When a nested
|
|
2528
|
+
# pydantic model is encountered, add a new nested container
|
|
2529
|
+
if excludes is None:
|
|
2530
|
+
excludes = []
|
|
2531
|
+
|
|
2532
|
+
# Access model_fields from the class, not the instance
|
|
2533
|
+
model_class = (
|
|
2534
|
+
py_model if isinstance(py_model, type) else type(py_model)
|
|
2535
|
+
)
|
|
2536
|
+
is_instance = not isinstance(py_model, type)
|
|
2537
|
+
for field, field_def in model_class.model_fields.items():
|
|
2538
|
+
if field_def is not None and field not in excludes:
|
|
2539
|
+
# Get actual instance value if py_model is an instance, otherwise use class default
|
|
2540
|
+
if is_instance:
|
|
2541
|
+
def_val = getattr(py_model, field)
|
|
2542
|
+
else:
|
|
2543
|
+
def_val = field_def.default
|
|
2544
|
+
if isinstance(def_val, PydanticUndefinedType):
|
|
2545
|
+
def_val = None
|
|
2546
|
+
ftype = field_def.annotation
|
|
2547
|
+
|
|
2548
|
+
# Build tooltip from field metadata
|
|
2549
|
+
tooltip_parts = []
|
|
2550
|
+
if field_def.description:
|
|
2551
|
+
tooltip_parts.append(field_def.description)
|
|
2552
|
+
if field_def.metadata:
|
|
2553
|
+
constraints = [str(m) for m in field_def.metadata]
|
|
2554
|
+
tooltip_parts.extend(constraints)
|
|
2555
|
+
toolTip = " | ".join(tooltip_parts)
|
|
2556
|
+
# Handle nested Pydantic models, including Optional[Model]
|
|
2557
|
+
if (
|
|
2558
|
+
is_subclass_of(ftype, BaseModel)
|
|
2559
|
+
or ftype in PYDANTIC_CLASSES_DEF
|
|
2560
|
+
):
|
|
2561
|
+
json_val = None
|
|
2562
|
+
if json_dict is not None and field in json_dict:
|
|
2563
|
+
json_val = json_dict[field]
|
|
2564
|
+
# the field is a pydantic class, add a container for it and fill it
|
|
2565
|
+
new_widget_cls = widgets.Container
|
|
2566
|
+
new_widget = new_widget_cls(name=field)
|
|
2567
|
+
new_widget.tooltip = toolTip
|
|
2568
|
+
# Unwrap Optional[Model] to get Model before recursing
|
|
2569
|
+
unwrapped_ftype = unwrap_optional(ftype)
|
|
2570
|
+
self.add_pydantic_to_container(
|
|
2571
|
+
unwrapped_ftype, new_widget, excludes, json_val
|
|
2572
|
+
)
|
|
2573
|
+
elif isinstance(ftype, type(Union[NonNegativeInt, List, str])):
|
|
2574
|
+
if (
|
|
2575
|
+
field == "background_path"
|
|
2576
|
+
): # field == "background_path":
|
|
2577
|
+
new_widget_cls, ops = get_widget_class(
|
|
2578
|
+
def_val,
|
|
2579
|
+
Annotated[Path, {"mode": "d"}],
|
|
2580
|
+
dict(name=field, value=def_val),
|
|
2581
|
+
)
|
|
2582
|
+
new_widget = new_widget_cls(**ops)
|
|
2583
|
+
toolTip = (
|
|
2584
|
+
"Select the folder containing background.zarr"
|
|
2585
|
+
)
|
|
2586
|
+
elif field == "time_indices": # field == "time_indices":
|
|
2587
|
+
new_widget_cls, ops = get_widget_class(
|
|
2588
|
+
def_val, str, dict(name=field, value=def_val)
|
|
2589
|
+
)
|
|
2590
|
+
new_widget = new_widget_cls(**ops)
|
|
2591
|
+
else: # other Union cases
|
|
2592
|
+
new_widget_cls, ops = get_widget_class(
|
|
2593
|
+
def_val, str, dict(name=field, value=def_val)
|
|
2594
|
+
)
|
|
2595
|
+
new_widget = new_widget_cls(**ops)
|
|
2596
|
+
new_widget.tooltip = toolTip
|
|
2597
|
+
if isinstance(new_widget, widgets.EmptyWidget):
|
|
2598
|
+
warnings.warn(
|
|
2599
|
+
message=f"magicgui could not identify a widget for {py_model}.{field}, which has type {ftype}"
|
|
2600
|
+
)
|
|
2601
|
+
elif isinstance(def_val, float) or (
|
|
2602
|
+
def_val is None
|
|
2603
|
+
and is_subclass_of(
|
|
2604
|
+
ftype, (int, float), require_optional=True
|
|
2605
|
+
)
|
|
2606
|
+
):
|
|
2607
|
+
# Handle float fields, including Optional[float] with None value
|
|
2608
|
+
|
|
2609
|
+
# For Optional numeric types with None, use LineEdit instead of FloatSpinBox
|
|
2610
|
+
# This allows empty string to represent None
|
|
2611
|
+
# Note: if we entered via the is_subclass_of check, def_val is guaranteed None
|
|
2612
|
+
if def_val is None:
|
|
2613
|
+
new_widget_cls, ops = get_widget_class(
|
|
2614
|
+
None,
|
|
2615
|
+
str,
|
|
2616
|
+
dict(name=field, value=""),
|
|
2617
|
+
)
|
|
2618
|
+
new_widget = new_widget_cls(**ops)
|
|
2619
|
+
new_widget.tooltip = (
|
|
2620
|
+
toolTip + " (Optional - leave empty for None)"
|
|
2621
|
+
)
|
|
2622
|
+
else:
|
|
2623
|
+
# Regular float field
|
|
2624
|
+
def_step_size = 0.001
|
|
2625
|
+
if field == "regularization_strength":
|
|
2626
|
+
def_step_size = 0.00001
|
|
2627
|
+
|
|
2628
|
+
if def_val > -1 and def_val < 1:
|
|
2629
|
+
new_widget_cls, ops = get_widget_class(
|
|
2630
|
+
None,
|
|
2631
|
+
ftype,
|
|
2632
|
+
dict(
|
|
2633
|
+
name=field,
|
|
2634
|
+
value=def_val,
|
|
2635
|
+
step=float(def_step_size),
|
|
2636
|
+
),
|
|
2637
|
+
)
|
|
2638
|
+
new_widget = new_widget_cls(**ops)
|
|
2639
|
+
new_widget.tooltip = toolTip
|
|
2640
|
+
else:
|
|
2641
|
+
new_widget_cls, ops = get_widget_class(
|
|
2642
|
+
None,
|
|
2643
|
+
ftype,
|
|
2644
|
+
dict(name=field, value=def_val),
|
|
2645
|
+
)
|
|
2646
|
+
new_widget = new_widget_cls(**ops)
|
|
2647
|
+
new_widget.tooltip = toolTip
|
|
2648
|
+
if isinstance(new_widget, widgets.EmptyWidget):
|
|
2649
|
+
warnings.warn(
|
|
2650
|
+
message=f"magicgui could not identify a widget for {py_model}.{field}, which has type {ftype}"
|
|
2651
|
+
)
|
|
2652
|
+
else:
|
|
2653
|
+
# parse the field, add appropriate widget
|
|
2654
|
+
new_widget_cls, ops = get_widget_class(
|
|
2655
|
+
None, ftype, dict(name=field, value=def_val)
|
|
2656
|
+
)
|
|
2657
|
+
new_widget = new_widget_cls(**ops)
|
|
2658
|
+
if isinstance(new_widget, widgets.EmptyWidget):
|
|
2659
|
+
warnings.warn(
|
|
2660
|
+
message=f"magicgui could not identify a widget for {py_model}.{field}, which has type {ftype}"
|
|
2661
|
+
)
|
|
2662
|
+
else:
|
|
2663
|
+
new_widget.tooltip = toolTip
|
|
2664
|
+
if json_dict is not None and (
|
|
2665
|
+
not isinstance(new_widget, widgets.Container)
|
|
2666
|
+
or (isinstance(new_widget, widgets.FileEdit))
|
|
2667
|
+
):
|
|
2668
|
+
if field in json_dict.keys():
|
|
2669
|
+
if isinstance(new_widget, widgets.CheckBox):
|
|
2670
|
+
new_widget.value = (
|
|
2671
|
+
True if json_dict[field] == "true" else False
|
|
2672
|
+
)
|
|
2673
|
+
elif isinstance(new_widget, widgets.FileEdit):
|
|
2674
|
+
if len(json_dict[field]) > 0:
|
|
2675
|
+
extension = os.path.splitext(json_dict[field])[
|
|
2676
|
+
1
|
|
2677
|
+
]
|
|
2678
|
+
if len(extension) > 0:
|
|
2679
|
+
new_widget.value = Path(
|
|
2680
|
+
json_dict[field]
|
|
2681
|
+
).parent.absolute() # CLI accepts BG folder not .zarr
|
|
2682
|
+
else:
|
|
2683
|
+
new_widget.value = Path(json_dict[field])
|
|
2684
|
+
else:
|
|
2685
|
+
new_widget.value = json_dict[field]
|
|
2686
|
+
container.append(new_widget)
|
|
2687
|
+
|
|
2688
|
+
# refer - add_pydantic_to_container() for comments
|
|
2689
|
+
def get_pydantic_kwargs(
|
|
2690
|
+
self,
|
|
2691
|
+
container: widgets.Container,
|
|
2692
|
+
pydantic_model,
|
|
2693
|
+
pydantic_kwargs: dict,
|
|
2694
|
+
excludes=None,
|
|
2695
|
+
json_dict=None,
|
|
2696
|
+
):
|
|
2697
|
+
# given a container that was instantiated from a pydantic model, get the arguments
|
|
2698
|
+
# needed to instantiate that pydantic model from the container.
|
|
2699
|
+
# traverse model fields, pull out values from container
|
|
2700
|
+
if excludes is None:
|
|
2701
|
+
excludes = []
|
|
2702
|
+
|
|
2703
|
+
# Access model_fields from the class, not the instance
|
|
2704
|
+
model_class = (
|
|
2705
|
+
pydantic_model
|
|
2706
|
+
if isinstance(pydantic_model, type)
|
|
2707
|
+
else type(pydantic_model)
|
|
2708
|
+
)
|
|
2709
|
+
|
|
2710
|
+
for field, field_def in model_class.model_fields.items():
|
|
2711
|
+
if field in excludes:
|
|
2712
|
+
continue
|
|
2713
|
+
|
|
2714
|
+
ftype = field_def.annotation
|
|
2715
|
+
if is_subclass_of(ftype, BaseModel):
|
|
2716
|
+
# Nested Pydantic model - recurse
|
|
2717
|
+
pydantic_kwargs[field] = {}
|
|
2718
|
+
self.get_pydantic_kwargs(
|
|
2719
|
+
getattr(container, field),
|
|
2720
|
+
unwrap_optional(ftype),
|
|
2721
|
+
pydantic_kwargs[field],
|
|
2722
|
+
excludes,
|
|
2723
|
+
json_dict,
|
|
2724
|
+
)
|
|
2725
|
+
else:
|
|
2726
|
+
# Leaf field - extract value from container widget
|
|
2727
|
+
value = getattr(container, field).value
|
|
2728
|
+
# Handle Optional numeric types: convert empty string to None, parse numeric strings
|
|
2729
|
+
if is_subclass_of(
|
|
2730
|
+
ftype, (int, float), require_optional=True
|
|
2731
|
+
) and isinstance(value, str):
|
|
2732
|
+
if value == "" or value.lower() in ("none", "null"):
|
|
2733
|
+
value = None
|
|
2734
|
+
else:
|
|
2735
|
+
try:
|
|
2736
|
+
value = float(value)
|
|
2737
|
+
except (ValueError, TypeError):
|
|
2738
|
+
pass
|
|
2739
|
+
pydantic_kwargs[field] = value
|
|
2740
|
+
|
|
2741
|
+
# copied from main_widget
|
|
2742
|
+
# file open/select dialog
|
|
2743
|
+
def open_file_dialog(self, default_path, type, filter="All Files (*)"):
|
|
2744
|
+
if type == "dir":
|
|
2745
|
+
return self.open_dialog(
|
|
2746
|
+
"select a directory", str(default_path), type, filter
|
|
2747
|
+
)
|
|
2748
|
+
elif type == "file":
|
|
2749
|
+
return self.open_dialog(
|
|
2750
|
+
"select a file", str(default_path), type, filter
|
|
2751
|
+
)
|
|
2752
|
+
elif type == "files":
|
|
2753
|
+
return self.open_dialog(
|
|
2754
|
+
"select file(s)", str(default_path), type, filter
|
|
2755
|
+
)
|
|
2756
|
+
elif type == "save":
|
|
2757
|
+
return self.open_dialog(
|
|
2758
|
+
"save a file", str(default_path), type, filter
|
|
2759
|
+
)
|
|
2760
|
+
else:
|
|
2761
|
+
return self.open_dialog(
|
|
2762
|
+
"select a directory", str(default_path), type, filter
|
|
2763
|
+
)
|
|
2764
|
+
|
|
2765
|
+
def open_dialog(self, title, ref, type, filter="All Files (*)"):
|
|
2766
|
+
"""
|
|
2767
|
+
opens pop-up dialogue for the user to choose a specific file or directory.
|
|
2768
|
+
|
|
2769
|
+
Parameters
|
|
2770
|
+
----------
|
|
2771
|
+
title: (str) message to display at the top of the pop up
|
|
2772
|
+
ref: (str) reference path to start the search at
|
|
2773
|
+
type: (str) type of file the user is choosing (dir, file, or save)
|
|
2774
|
+
|
|
2775
|
+
Returns
|
|
2776
|
+
-------
|
|
2777
|
+
|
|
2778
|
+
"""
|
|
2779
|
+
|
|
2780
|
+
options = QFileDialog.DontUseNativeDialog
|
|
2781
|
+
if type == "dir":
|
|
2782
|
+
path = QFileDialog.getExistingDirectory(
|
|
2783
|
+
None, title, ref, options=options
|
|
2784
|
+
)
|
|
2785
|
+
elif type == "file":
|
|
2786
|
+
path = QFileDialog.getOpenFileName(
|
|
2787
|
+
None, title, ref, filter=filter, options=options
|
|
2788
|
+
)[0]
|
|
2789
|
+
elif type == "files":
|
|
2790
|
+
path = QFileDialog.getOpenFileNames(
|
|
2791
|
+
None, title, ref, filter=filter, options=options
|
|
2792
|
+
)[0]
|
|
2793
|
+
elif type == "save":
|
|
2794
|
+
path = QFileDialog.getSaveFileName(
|
|
2795
|
+
None, "Choose a save name", ref, filter=filter, options=options
|
|
2796
|
+
)[0]
|
|
2797
|
+
else:
|
|
2798
|
+
raise ValueError("Did not understand file dialogue type")
|
|
2799
|
+
|
|
2800
|
+
return path
|
|
2801
|
+
|
|
2802
|
+
|
|
2803
|
+
class MyWorker:
|
|
2804
|
+
"""This worker class manages the jobs queue arriving from the GUI and passes to job manager, the task and update function"""
|
|
2805
|
+
|
|
2806
|
+
def __init__(self, formLayout, tab_recon: Ui_ReconTab_Form, parentForm):
|
|
2807
|
+
super().__init__()
|
|
2808
|
+
self.formLayout: QFormLayout = formLayout
|
|
2809
|
+
self.tab_recon: Ui_ReconTab_Form = tab_recon
|
|
2810
|
+
self.ui: QWidget = parentForm
|
|
2811
|
+
self.max_cores = (
|
|
2812
|
+
1 # os.cpu_count() - no multi-threading // parallelization
|
|
2813
|
+
)
|
|
2814
|
+
# In the case of CLI, we just need to submit requests in a non-blocking way
|
|
2815
|
+
self.threadPool = 1 # int(self.max_cores / 2)
|
|
2816
|
+
self.results = {}
|
|
2817
|
+
self.clearResults = True
|
|
2818
|
+
self.executor = None
|
|
2819
|
+
# https://click.palletsprojects.com/en/stable/testing/
|
|
2820
|
+
# self.runner = CliRunner()
|
|
2821
|
+
self.isInitialized = False
|
|
2822
|
+
self.initialize()
|
|
2823
|
+
|
|
2824
|
+
def initialize(self):
|
|
2825
|
+
if not self.isInitialized:
|
|
2826
|
+
self.workerThreadRowDeletion = RowDeletionWorkerThread(
|
|
2827
|
+
self.formLayout
|
|
2828
|
+
)
|
|
2829
|
+
self.workerThreadRowDeletion.removeRowSignal.connect(
|
|
2830
|
+
self.tab_recon.remove_row
|
|
2831
|
+
)
|
|
2832
|
+
self.workerThreadRowDeletion.start()
|
|
2833
|
+
self.isInitialized = True
|
|
2834
|
+
|
|
2835
|
+
def set_new_instances(self, formLayout, tab_recon, parentForm):
|
|
2836
|
+
self.formLayout: QFormLayout = formLayout
|
|
2837
|
+
self.tab_recon: Ui_ReconTab_Form = tab_recon
|
|
2838
|
+
self.ui: QWidget = parentForm
|
|
2839
|
+
self.workerThreadRowDeletion.set_new_instances(formLayout)
|
|
2840
|
+
|
|
2841
|
+
def find_widget_row_in_layout(self, strID):
|
|
2842
|
+
layout: QFormLayout = self.formLayout
|
|
2843
|
+
for idx in range(0, layout.rowCount()):
|
|
2844
|
+
widgetItem = layout.itemAt(idx)
|
|
2845
|
+
name_widget = widgetItem.widget()
|
|
2846
|
+
toolTip_string = str(name_widget.toolTip)
|
|
2847
|
+
if strID in toolTip_string:
|
|
2848
|
+
name_widget.setParent(None)
|
|
2849
|
+
return idx
|
|
2850
|
+
return -1
|
|
2851
|
+
|
|
2852
|
+
def get_max_CPU_cores(self):
|
|
2853
|
+
return self.max_cores
|
|
2854
|
+
|
|
2855
|
+
def set_pool_threads(self, t):
|
|
2856
|
+
if t > 0 and t < self.max_cores:
|
|
2857
|
+
self.threadPool = t
|
|
2858
|
+
|
|
2859
|
+
def start_pool(self):
|
|
2860
|
+
if self.executor is None:
|
|
2861
|
+
self.executor = ThreadPoolExecutor(max_workers=self.threadPool)
|
|
2862
|
+
|
|
2863
|
+
def shut_down_pool(self):
|
|
2864
|
+
self.executor.shutdown(wait=True, cancel_futures=False)
|
|
2865
|
+
|
|
2866
|
+
def table_update_and_cleaup_thread(self, expIdx: str = "", msg: str = ""):
|
|
2867
|
+
"""This GUI update function is passed to job manager when adding a job and updates based on stdout"""
|
|
2868
|
+
# called by the subprocess thread to update GUI
|
|
2869
|
+
if expIdx != "":
|
|
2870
|
+
params = self.results[expIdx]
|
|
2871
|
+
|
|
2872
|
+
_infoBox: ScrollableLabel = params["table_entry_infoBox"]
|
|
2873
|
+
_cancelJobBtn: PushButton = params["cancelJobButton"]
|
|
2874
|
+
|
|
2875
|
+
_infoBox.setText(_infoBox.getText() + "\n" + msg)
|
|
2876
|
+
|
|
2877
|
+
if not _cancelJobBtn.enabled:
|
|
2878
|
+
_cancelJobBtn.clicked.connect(
|
|
2879
|
+
lambda: self.cancel_job(expIdx, _infoBox, _cancelJobBtn)
|
|
2880
|
+
)
|
|
2881
|
+
_cancelJobBtn.enabled = True
|
|
2882
|
+
|
|
2883
|
+
def cancel_job(self, expIdx, _infoBox, _cancelJobBtn):
|
|
2884
|
+
# called to cancel a job
|
|
2885
|
+
if self.tab_recon.confirm_dialog():
|
|
2886
|
+
_cancelJobBtn.enabled = False
|
|
2887
|
+
_infoBox.setText(_infoBox.getText() + "\n" + "Cancelled by User")
|
|
2888
|
+
self.tab_recon.job_manager.cancel_job(expIdx)
|
|
2889
|
+
|
|
2890
|
+
def process_ending(self, expIdx, exit_code=0):
|
|
2891
|
+
# called when the subprocess ends - can be success or fail
|
|
2892
|
+
# Read reconstruction data
|
|
2893
|
+
# Can be attemped even when fail return code
|
|
2894
|
+
params = self.results[expIdx]
|
|
2895
|
+
_infoBox: ScrollableLabel = params["table_entry_infoBox"]
|
|
2896
|
+
|
|
2897
|
+
showData_thread = ShowDataWorkerThread(params["output_path"])
|
|
2898
|
+
showData_thread.show_data_signal.connect(self.tab_recon.show_dataset)
|
|
2899
|
+
showData_thread.start()
|
|
2900
|
+
|
|
2901
|
+
if (
|
|
2902
|
+
self.clearResults == True and exit_code == 0
|
|
2903
|
+
): # remove processing entry when exiting without error
|
|
2904
|
+
ROW_POP_QUEUE.append(expIdx)
|
|
2905
|
+
else:
|
|
2906
|
+
_infoBox.setText(
|
|
2907
|
+
_infoBox.getText()
|
|
2908
|
+
+ "\n"
|
|
2909
|
+
+ "Process ended with return code {code}".format(
|
|
2910
|
+
code=exit_code
|
|
2911
|
+
)
|
|
2912
|
+
)
|
|
2913
|
+
|
|
2914
|
+
_infoBox.setText(_infoBox.getText() + "\n" + "Displaying data")
|
|
2915
|
+
# Wait for show thread to finish
|
|
2916
|
+
if showData_thread is not None:
|
|
2917
|
+
while showData_thread.isRunning():
|
|
2918
|
+
time.sleep(1)
|
|
2919
|
+
|
|
2920
|
+
def run_in_pool(self, params):
|
|
2921
|
+
self.start_pool()
|
|
2922
|
+
|
|
2923
|
+
self.results[params["exp_id"]] = {}
|
|
2924
|
+
self.results[params["exp_id"]] = params
|
|
2925
|
+
|
|
2926
|
+
try:
|
|
2927
|
+
# when a request on the listening port arrives with an empty path
|
|
2928
|
+
# we can assume the processing was initiated outside this application
|
|
2929
|
+
# we do not proceed with the processing and will display the results
|
|
2930
|
+
if params["input_path"] != "":
|
|
2931
|
+
self.executor.submit(self.run, params)
|
|
2932
|
+
except Exception as exc:
|
|
2933
|
+
self.results[params["exp_id"]]["error"] = str("\n".join(exc.args))
|
|
2934
|
+
|
|
2935
|
+
def run_multi_in_pool(self, multi_params_as_list):
|
|
2936
|
+
for params in multi_params_as_list:
|
|
2937
|
+
self.results[params["exp_id"]] = {}
|
|
2938
|
+
self.results[params["exp_id"]] = params
|
|
2939
|
+
|
|
2940
|
+
self.executor.map(self.run, multi_params_as_list)
|
|
2941
|
+
|
|
2942
|
+
def get_results(self):
|
|
2943
|
+
return self.results
|
|
2944
|
+
|
|
2945
|
+
def get_result(self, exp_id):
|
|
2946
|
+
return self.results[exp_id]
|
|
2947
|
+
|
|
2948
|
+
def run(self, params):
|
|
2949
|
+
# thread where work is passed to CLI which will handle the
|
|
2950
|
+
# multi-processing aspects as Jobs
|
|
2951
|
+
if params["exp_id"] not in self.results.keys():
|
|
2952
|
+
self.results[params["exp_id"]] = {}
|
|
2953
|
+
self.results[params["exp_id"]] = params
|
|
2954
|
+
|
|
2955
|
+
self.run_in_subprocess(params)
|
|
2956
|
+
|
|
2957
|
+
def run_in_subprocess(self, params):
|
|
2958
|
+
"""function that initiates the processing on the CLI in subprocess"""
|
|
2959
|
+
try:
|
|
2960
|
+
input_path = str(params["input_path"])
|
|
2961
|
+
config_path = str(params["config_path"])
|
|
2962
|
+
output_path = str(params["output_path"])
|
|
2963
|
+
uid = str(params["exp_id"])
|
|
2964
|
+
|
|
2965
|
+
cmd = [
|
|
2966
|
+
"waveorder",
|
|
2967
|
+
"reconstruct",
|
|
2968
|
+
"-i",
|
|
2969
|
+
input_path,
|
|
2970
|
+
"-c",
|
|
2971
|
+
config_path,
|
|
2972
|
+
"-o",
|
|
2973
|
+
output_path,
|
|
2974
|
+
"-uid",
|
|
2975
|
+
uid,
|
|
2976
|
+
]
|
|
2977
|
+
|
|
2978
|
+
self.tab_recon.job_manager.run_job(
|
|
2979
|
+
params["exp_id"],
|
|
2980
|
+
cmd,
|
|
2981
|
+
self.table_update_and_cleaup_thread,
|
|
2982
|
+
self.process_ending,
|
|
2983
|
+
)
|
|
2984
|
+
|
|
2985
|
+
except Exception as exc:
|
|
2986
|
+
self.results[params["exp_id"]]["error"] = (
|
|
2987
|
+
str(" ".join(cmd)) + "\n" + str("\n".join(exc.args))
|
|
2988
|
+
)
|
|
2989
|
+
|
|
2990
|
+
|
|
2991
|
+
class ShowDataWorkerThread(QThread):
|
|
2992
|
+
"""Worker thread for sending signal for adding component when request comes
|
|
2993
|
+
from a different thread"""
|
|
2994
|
+
|
|
2995
|
+
show_data_signal = Signal(str)
|
|
2996
|
+
|
|
2997
|
+
def __init__(self, path):
|
|
2998
|
+
super().__init__()
|
|
2999
|
+
self.path = path
|
|
3000
|
+
|
|
3001
|
+
def run(self):
|
|
3002
|
+
# Emit the signal to add the widget to the main thread
|
|
3003
|
+
self.show_data_signal.emit(self.path)
|
|
3004
|
+
|
|
3005
|
+
|
|
3006
|
+
class AddOTFTableEntryWorkerThread(QThread):
|
|
3007
|
+
"""Worker thread for sending signal for adding component when request comes
|
|
3008
|
+
from a different thread"""
|
|
3009
|
+
|
|
3010
|
+
add_tableOTFentry_signal = Signal(str, bool, bool)
|
|
3011
|
+
|
|
3012
|
+
def __init__(self, OTF_dir_path, bool_msg, doCheck=False):
|
|
3013
|
+
super().__init__()
|
|
3014
|
+
self.OTF_dir_path = OTF_dir_path
|
|
3015
|
+
self.bool_msg = bool_msg
|
|
3016
|
+
self.doCheck = doCheck
|
|
3017
|
+
|
|
3018
|
+
def run(self):
|
|
3019
|
+
# Emit the signal to add the widget to the main thread
|
|
3020
|
+
self.add_tableOTFentry_signal.emit(
|
|
3021
|
+
self.OTF_dir_path, self.bool_msg, self.doCheck
|
|
3022
|
+
)
|
|
3023
|
+
|
|
3024
|
+
|
|
3025
|
+
class AddTableEntryWorkerThread(QThread):
|
|
3026
|
+
"""Worker thread for sending signal for adding component when request comes
|
|
3027
|
+
from a different thread"""
|
|
3028
|
+
|
|
3029
|
+
add_tableentry_signal = Signal(str, str, dict)
|
|
3030
|
+
|
|
3031
|
+
def __init__(self, expID, desc, params):
|
|
3032
|
+
super().__init__()
|
|
3033
|
+
self.expID = expID
|
|
3034
|
+
self.desc = desc
|
|
3035
|
+
self.params = params
|
|
3036
|
+
|
|
3037
|
+
def run(self):
|
|
3038
|
+
# Emit the signal to add the widget to the main thread
|
|
3039
|
+
self.add_tableentry_signal.emit(self.expID, self.desc, self.params)
|
|
3040
|
+
|
|
3041
|
+
|
|
3042
|
+
class AddWidgetWorkerThread(QThread):
|
|
3043
|
+
"""Worker thread for sending signal for adding component when request comes
|
|
3044
|
+
from a different thread"""
|
|
3045
|
+
|
|
3046
|
+
add_widget_signal = Signal(QVBoxLayout, str, str, str, str)
|
|
3047
|
+
|
|
3048
|
+
def __init__(self, layout, expID, jID, desc, wellName):
|
|
3049
|
+
super().__init__()
|
|
3050
|
+
self.layout = layout
|
|
3051
|
+
self.expID = expID
|
|
3052
|
+
self.jID = jID
|
|
3053
|
+
self.desc = desc
|
|
3054
|
+
self.wellName = wellName
|
|
3055
|
+
|
|
3056
|
+
def run(self):
|
|
3057
|
+
# Emit the signal to add the widget to the main thread
|
|
3058
|
+
self.add_widget_signal.emit(
|
|
3059
|
+
self.layout, self.expID, self.jID, self.desc, self.wellName
|
|
3060
|
+
)
|
|
3061
|
+
|
|
3062
|
+
|
|
3063
|
+
class RowDeletionWorkerThread(QThread):
|
|
3064
|
+
"""Searches for a row based on its ID and then
|
|
3065
|
+
emits a signal to QFormLayout on the main thread for deletion"""
|
|
3066
|
+
|
|
3067
|
+
removeRowSignal = Signal(int, str)
|
|
3068
|
+
|
|
3069
|
+
def __init__(self, formLayout):
|
|
3070
|
+
super().__init__()
|
|
3071
|
+
self.formLayout = formLayout
|
|
3072
|
+
|
|
3073
|
+
def set_new_instances(self, formLayout):
|
|
3074
|
+
self.formLayout: QFormLayout = formLayout
|
|
3075
|
+
|
|
3076
|
+
# we might deal with race conditions with a shrinking table
|
|
3077
|
+
# find out widget and return its index
|
|
3078
|
+
def find_widget_row_in_layout(self, strID):
|
|
3079
|
+
layout: QFormLayout = self.formLayout
|
|
3080
|
+
for idx in range(0, layout.rowCount()):
|
|
3081
|
+
widgetItem = layout.itemAt(idx)
|
|
3082
|
+
if widgetItem is not None:
|
|
3083
|
+
name_widget = widgetItem.widget()
|
|
3084
|
+
toolTip_string = str(name_widget.toolTip)
|
|
3085
|
+
if strID in toolTip_string:
|
|
3086
|
+
name_widget.setParent(None)
|
|
3087
|
+
return idx
|
|
3088
|
+
return -1
|
|
3089
|
+
|
|
3090
|
+
def run(self):
|
|
3091
|
+
while True:
|
|
3092
|
+
if len(ROW_POP_QUEUE) > 0:
|
|
3093
|
+
stringID = ROW_POP_QUEUE.pop(0)
|
|
3094
|
+
# Emit the signal to remove the row
|
|
3095
|
+
deleteRow = self.find_widget_row_in_layout(stringID)
|
|
3096
|
+
if deleteRow > -1:
|
|
3097
|
+
self.removeRowSignal.emit(int(deleteRow), str(stringID))
|
|
3098
|
+
time.sleep(1)
|
|
3099
|
+
else:
|
|
3100
|
+
time.sleep(5)
|
|
3101
|
+
|
|
3102
|
+
|
|
3103
|
+
class DropButton(QPushButton):
|
|
3104
|
+
"""A drag & drop PushButton to load model file(s)"""
|
|
3105
|
+
|
|
3106
|
+
def __init__(self, text, parent=None, recon_tab: Ui_ReconTab_Form = None):
|
|
3107
|
+
super().__init__(text, parent)
|
|
3108
|
+
self.setAcceptDrops(True)
|
|
3109
|
+
self.recon_tab = recon_tab
|
|
3110
|
+
|
|
3111
|
+
def dragEnterEvent(self, event):
|
|
3112
|
+
if event.mimeData().hasUrls():
|
|
3113
|
+
event.acceptProposedAction()
|
|
3114
|
+
|
|
3115
|
+
def dropEvent(self, event):
|
|
3116
|
+
files = []
|
|
3117
|
+
for url in event.mimeData().urls():
|
|
3118
|
+
filepath = url.toLocalFile()
|
|
3119
|
+
files.append(filepath)
|
|
3120
|
+
self.recon_tab.open_model_files(files)
|
|
3121
|
+
|
|
3122
|
+
|
|
3123
|
+
class DropWidget(QWidget):
|
|
3124
|
+
"""A drag & drop widget container to load model file(s)"""
|
|
3125
|
+
|
|
3126
|
+
def __init__(self, recon_tab: Ui_ReconTab_Form = None):
|
|
3127
|
+
super().__init__()
|
|
3128
|
+
self.setAcceptDrops(True)
|
|
3129
|
+
self.recon_tab = recon_tab
|
|
3130
|
+
|
|
3131
|
+
def dragEnterEvent(self, event):
|
|
3132
|
+
if event.mimeData().hasUrls():
|
|
3133
|
+
event.acceptProposedAction()
|
|
3134
|
+
|
|
3135
|
+
def dropEvent(self, event):
|
|
3136
|
+
files = []
|
|
3137
|
+
for url in event.mimeData().urls():
|
|
3138
|
+
filepath = url.toLocalFile()
|
|
3139
|
+
files.append(filepath)
|
|
3140
|
+
self.recon_tab.open_model_files(files)
|
|
3141
|
+
|
|
3142
|
+
|
|
3143
|
+
class ScrollableLabel(QScrollArea):
|
|
3144
|
+
"""A scrollable label widget used for Job entry"""
|
|
3145
|
+
|
|
3146
|
+
def __init__(self, text, *args, **kwargs):
|
|
3147
|
+
super().__init__(*args, **kwargs)
|
|
3148
|
+
|
|
3149
|
+
self.label = QLabel()
|
|
3150
|
+
self.label.setWordWrap(True)
|
|
3151
|
+
self.label.setText(text)
|
|
3152
|
+
|
|
3153
|
+
layout = QVBoxLayout()
|
|
3154
|
+
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
3155
|
+
layout.addWidget(self.label)
|
|
3156
|
+
self.label.setSizePolicy(
|
|
3157
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
3158
|
+
)
|
|
3159
|
+
|
|
3160
|
+
container = QWidget()
|
|
3161
|
+
container.setLayout(layout)
|
|
3162
|
+
container.setSizePolicy(
|
|
3163
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
3164
|
+
)
|
|
3165
|
+
|
|
3166
|
+
self.setWidget(container)
|
|
3167
|
+
self.setWidgetResizable(True)
|
|
3168
|
+
self.setSizePolicy(
|
|
3169
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
3170
|
+
)
|
|
3171
|
+
self.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
3172
|
+
|
|
3173
|
+
def setText(self, text):
|
|
3174
|
+
self.label.setText(text)
|
|
3175
|
+
|
|
3176
|
+
def getText(self):
|
|
3177
|
+
return self.label.text()
|
|
3178
|
+
|
|
3179
|
+
|
|
3180
|
+
class MyWidget(QWidget):
|
|
3181
|
+
resized = Signal()
|
|
3182
|
+
|
|
3183
|
+
def __init__(self):
|
|
3184
|
+
super().__init__()
|
|
3185
|
+
|
|
3186
|
+
def resizeEvent(self, event):
|
|
3187
|
+
self.resized.emit()
|
|
3188
|
+
super().resizeEvent(event)
|
|
3189
|
+
|
|
3190
|
+
|
|
3191
|
+
class CollapsibleBox(QWidget):
|
|
3192
|
+
"""A collapsible widget"""
|
|
3193
|
+
|
|
3194
|
+
def __init__(
|
|
3195
|
+
self, title="", parent=None, hasPydanticModel=False, expanded=False
|
|
3196
|
+
):
|
|
3197
|
+
super(CollapsibleBox, self).__init__(parent)
|
|
3198
|
+
|
|
3199
|
+
self.hasPydanticModel = hasPydanticModel
|
|
3200
|
+
self.toggle_button = QToolButton(
|
|
3201
|
+
text=title, checkable=True, checked=False
|
|
3202
|
+
)
|
|
3203
|
+
self.toggle_button.setStyleSheet("QToolButton { border: none; }")
|
|
3204
|
+
self.toggle_button.setToolButtonStyle(
|
|
3205
|
+
QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
|
3206
|
+
)
|
|
3207
|
+
self.toggle_button.setArrowType(QtCore.Qt.ArrowType.RightArrow)
|
|
3208
|
+
self.toggle_button.pressed.connect(self.on_pressed)
|
|
3209
|
+
|
|
3210
|
+
self.toggle_animation = QtCore.QParallelAnimationGroup(self)
|
|
3211
|
+
|
|
3212
|
+
self.content_area = QScrollArea(maximumHeight=0, minimumHeight=0)
|
|
3213
|
+
self.content_area.setSizePolicy(
|
|
3214
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
3215
|
+
)
|
|
3216
|
+
self.content_area.setFrameShape(QFrame.Shape.NoFrame)
|
|
3217
|
+
|
|
3218
|
+
lay = QVBoxLayout(self)
|
|
3219
|
+
lay.setSpacing(0)
|
|
3220
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
3221
|
+
lay.addWidget(self.toggle_button)
|
|
3222
|
+
lay.addWidget(self.content_area)
|
|
3223
|
+
|
|
3224
|
+
self.toggle_animation.addAnimation(
|
|
3225
|
+
QtCore.QPropertyAnimation(self, b"minimumHeight")
|
|
3226
|
+
)
|
|
3227
|
+
self.toggle_animation.addAnimation(
|
|
3228
|
+
QtCore.QPropertyAnimation(self, b"maximumHeight")
|
|
3229
|
+
)
|
|
3230
|
+
self.toggle_animation.addAnimation(
|
|
3231
|
+
QtCore.QPropertyAnimation(self.content_area, b"maximumHeight")
|
|
3232
|
+
)
|
|
3233
|
+
collapsed_height = (
|
|
3234
|
+
self.sizeHint().height() - self.content_area.maximumHeight()
|
|
3235
|
+
)
|
|
3236
|
+
|
|
3237
|
+
content_height = lay.sizeHint().height()
|
|
3238
|
+
for i in range(self.toggle_animation.animationCount()):
|
|
3239
|
+
animation = self.toggle_animation.animationAt(i)
|
|
3240
|
+
animation.setDuration(500)
|
|
3241
|
+
animation.setStartValue(collapsed_height)
|
|
3242
|
+
animation.setEndValue(collapsed_height + content_height)
|
|
3243
|
+
|
|
3244
|
+
if expanded:
|
|
3245
|
+
self.on_pressed()
|
|
3246
|
+
|
|
3247
|
+
def setNewName(self, name):
|
|
3248
|
+
self.toggle_button.setText(name)
|
|
3249
|
+
|
|
3250
|
+
def on_pressed(self):
|
|
3251
|
+
checked = self.toggle_button.isChecked()
|
|
3252
|
+
self.toggle_button.setArrowType(
|
|
3253
|
+
QtCore.Qt.ArrowType.DownArrow
|
|
3254
|
+
if not checked
|
|
3255
|
+
else QtCore.Qt.ArrowType.RightArrow
|
|
3256
|
+
)
|
|
3257
|
+
self.toggle_animation.setDirection(
|
|
3258
|
+
QtCore.QAbstractAnimation.Direction.Forward
|
|
3259
|
+
if not checked
|
|
3260
|
+
else QtCore.QAbstractAnimation.Direction.Backward
|
|
3261
|
+
)
|
|
3262
|
+
self.toggle_animation.start()
|
|
3263
|
+
if checked and self.hasPydanticModel:
|
|
3264
|
+
# do model verification on close
|
|
3265
|
+
pass
|
|
3266
|
+
|
|
3267
|
+
def setContentLayout(self, layout):
|
|
3268
|
+
lay = self.content_area.layout()
|
|
3269
|
+
del lay
|
|
3270
|
+
self.content_area.setLayout(layout)
|
|
3271
|
+
collapsed_height = (
|
|
3272
|
+
self.sizeHint().height() - self.content_area.maximumHeight()
|
|
3273
|
+
)
|
|
3274
|
+
content_height = layout.sizeHint().height()
|
|
3275
|
+
for i in range(self.toggle_animation.animationCount()):
|
|
3276
|
+
animation = self.toggle_animation.animationAt(i)
|
|
3277
|
+
animation.setDuration(500)
|
|
3278
|
+
animation.setStartValue(collapsed_height)
|
|
3279
|
+
animation.setEndValue(collapsed_height + content_height)
|
|
3280
|
+
|
|
3281
|
+
content_animation = self.toggle_animation.animationAt(
|
|
3282
|
+
self.toggle_animation.animationCount() - 1
|
|
3283
|
+
)
|
|
3284
|
+
content_animation.setDuration(500)
|
|
3285
|
+
content_animation.setStartValue(0)
|
|
3286
|
+
content_animation.setEndValue(content_height)
|
|
3287
|
+
|
|
3288
|
+
|
|
3289
|
+
# VScode debugging
|
|
3290
|
+
if __name__ == "__main__":
|
|
3291
|
+
import napari
|
|
3292
|
+
|
|
3293
|
+
napari.Viewer()
|
|
3294
|
+
napari.run()
|