pymagnetos 0.1.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.
- pymagnetos/__init__.py +15 -0
- pymagnetos/cli.py +40 -0
- pymagnetos/core/__init__.py +19 -0
- pymagnetos/core/_config.py +340 -0
- pymagnetos/core/_data.py +132 -0
- pymagnetos/core/_processor.py +905 -0
- pymagnetos/core/config_models.py +57 -0
- pymagnetos/core/gui/__init__.py +6 -0
- pymagnetos/core/gui/_base_mainwindow.py +819 -0
- pymagnetos/core/gui/widgets/__init__.py +19 -0
- pymagnetos/core/gui/widgets/_batch_processing.py +319 -0
- pymagnetos/core/gui/widgets/_configuration.py +167 -0
- pymagnetos/core/gui/widgets/_files.py +129 -0
- pymagnetos/core/gui/widgets/_graphs.py +93 -0
- pymagnetos/core/gui/widgets/_param_content.py +20 -0
- pymagnetos/core/gui/widgets/_popup_progressbar.py +29 -0
- pymagnetos/core/gui/widgets/_text_logger.py +32 -0
- pymagnetos/core/signal_processing.py +1004 -0
- pymagnetos/core/utils.py +85 -0
- pymagnetos/log.py +126 -0
- pymagnetos/py.typed +0 -0
- pymagnetos/pytdo/__init__.py +6 -0
- pymagnetos/pytdo/_config.py +24 -0
- pymagnetos/pytdo/_config_models.py +59 -0
- pymagnetos/pytdo/_tdoprocessor.py +1052 -0
- pymagnetos/pytdo/assets/config_default.toml +84 -0
- pymagnetos/pytdo/gui/__init__.py +26 -0
- pymagnetos/pytdo/gui/_worker.py +106 -0
- pymagnetos/pytdo/gui/main.py +617 -0
- pymagnetos/pytdo/gui/widgets/__init__.py +8 -0
- pymagnetos/pytdo/gui/widgets/_buttons.py +66 -0
- pymagnetos/pytdo/gui/widgets/_configuration.py +78 -0
- pymagnetos/pytdo/gui/widgets/_graphs.py +280 -0
- pymagnetos/pytdo/gui/widgets/_param_content.py +137 -0
- pymagnetos/pyuson/__init__.py +7 -0
- pymagnetos/pyuson/_config.py +26 -0
- pymagnetos/pyuson/_config_models.py +71 -0
- pymagnetos/pyuson/_echoprocessor.py +1901 -0
- pymagnetos/pyuson/assets/config_default.toml +92 -0
- pymagnetos/pyuson/gui/__init__.py +26 -0
- pymagnetos/pyuson/gui/_worker.py +135 -0
- pymagnetos/pyuson/gui/main.py +767 -0
- pymagnetos/pyuson/gui/widgets/__init__.py +7 -0
- pymagnetos/pyuson/gui/widgets/_buttons.py +95 -0
- pymagnetos/pyuson/gui/widgets/_configuration.py +85 -0
- pymagnetos/pyuson/gui/widgets/_graphs.py +248 -0
- pymagnetos/pyuson/gui/widgets/_param_content.py +193 -0
- pymagnetos-0.1.0.dist-info/METADATA +23 -0
- pymagnetos-0.1.0.dist-info/RECORD +51 -0
- pymagnetos-0.1.0.dist-info/WHEEL +4 -0
- pymagnetos-0.1.0.dist-info/entry_points.txt +7 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application for ultra-sound echoes analysis.
|
|
3
|
+
|
|
4
|
+
This is the main window definition, built with the custom widgets found in the widgets
|
|
5
|
+
folder.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pyqtgraph as pg
|
|
12
|
+
from PyQt6 import QtGui, QtWidgets
|
|
13
|
+
from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
|
14
|
+
|
|
15
|
+
from pymagnetos import sp
|
|
16
|
+
from pymagnetos.core.gui import BaseMainWindow, widgets
|
|
17
|
+
|
|
18
|
+
from ._worker import ProcessorWorker
|
|
19
|
+
from .widgets import ButtonsWidget, ConfigurationWidget, GraphsWidget, ParamContent
|
|
20
|
+
|
|
21
|
+
ICON_PATH = str(Path(__file__).parent / "assets" / "icon.png")
|
|
22
|
+
REGEXP_EXPID_SEPARATORS = r"[_-]"
|
|
23
|
+
PROGRAM_NAME = "pymagnetos"
|
|
24
|
+
ALLOWED_FORMAT = ("toml", "json", "nx5", "nxs", "h5", "hdf5")
|
|
25
|
+
LOG_LEVEL = "INFO"
|
|
26
|
+
VERSION = importlib.metadata.version(PROGRAM_NAME)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MainWindow(BaseMainWindow):
|
|
30
|
+
"""
|
|
31
|
+
A graphical user interface application for ultra-sound echoes analysis.
|
|
32
|
+
|
|
33
|
+
The `MainWindow` class defines the main thread with the front-end user interface.
|
|
34
|
+
|
|
35
|
+
It is built with the custom components defined in the `widgets` module. Those
|
|
36
|
+
components are referenced in the main thread with a leading `w` :
|
|
37
|
+
`self.wconfiguration` : the "Configuration" tab
|
|
38
|
+
`self.wfiles` : the "Files" tab
|
|
39
|
+
`self.wbatch` : the "Batch processing" tab
|
|
40
|
+
`self.wgraphs` : the widget holding all the graphs
|
|
41
|
+
`self.wbuttons` : the widget holding the main buttons
|
|
42
|
+
`self.wlog` : the widget that streams the log output
|
|
43
|
+
|
|
44
|
+
Each of those components exposes signals, to which the main thread connects its own
|
|
45
|
+
methods, instead of connecting directly to the widgets (buttons, checkboxes and the
|
|
46
|
+
like). Those signals are named with a trailing `sig_` so that makes it easy to know
|
|
47
|
+
exactly what should be connected in the main thread.
|
|
48
|
+
|
|
49
|
+
When loading a configuration (or a NeXus) file, the main threads creates a worker
|
|
50
|
+
thread (`DataWorker`). The latter instantiates an `EchoProcessor` object that
|
|
51
|
+
perform the actual analysis.
|
|
52
|
+
|
|
53
|
+
pyqtSignals
|
|
54
|
+
-------
|
|
55
|
+
All signals emit in their corresponding methods and are connected to the
|
|
56
|
+
corresponding methods in the worker thread.
|
|
57
|
+
|
|
58
|
+
`sig_worker_load`
|
|
59
|
+
`sig_worker_rolling`
|
|
60
|
+
`sig_worker_find_f0`
|
|
61
|
+
`sig_worker_demodulate`
|
|
62
|
+
`sig_worker_average`
|
|
63
|
+
`sig_worker_batch`
|
|
64
|
+
`sig_worker_export_csv`
|
|
65
|
+
`sig_worker_save_nexus`
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
sig_worker_rolling = pyqtSignal()
|
|
69
|
+
sig_worker_find_f0 = pyqtSignal()
|
|
70
|
+
sig_worker_demodulate = pyqtSignal()
|
|
71
|
+
sig_worker_average = pyqtSignal()
|
|
72
|
+
sig_worker_batch = pyqtSignal(list, bool, bool, bool, bool)
|
|
73
|
+
sig_worker_export_csv = pyqtSignal(str, bool)
|
|
74
|
+
|
|
75
|
+
worker: ProcessorWorker
|
|
76
|
+
|
|
77
|
+
_window_icon = ICON_PATH
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
# Register the widgets to use
|
|
81
|
+
self._type_wbatch = widgets.BatchProcessingWidget
|
|
82
|
+
self._type_wbuttons = ButtonsWidget
|
|
83
|
+
self._type_wconfiguration = ConfigurationWidget
|
|
84
|
+
self._type_wfiles = widgets.FileBrowserWidget
|
|
85
|
+
self._type_wgraphs = GraphsWidget
|
|
86
|
+
self._param_content = ParamContent
|
|
87
|
+
self._type_worker = ProcessorWorker
|
|
88
|
+
|
|
89
|
+
super().__init__()
|
|
90
|
+
|
|
91
|
+
# Initialize window
|
|
92
|
+
self.setGeometry(300, 300, 900, 450)
|
|
93
|
+
self.setWindowTitle("EchoAnalyzer")
|
|
94
|
+
self.setWindowIcon(QtGui.QIcon(self._window_icon))
|
|
95
|
+
|
|
96
|
+
self.logger.info(f"Running pyuson v{VERSION}")
|
|
97
|
+
|
|
98
|
+
def init_parameter_tree(self):
|
|
99
|
+
"""
|
|
100
|
+
Create the "Configuration" tab.
|
|
101
|
+
|
|
102
|
+
It holds the PyQtGraph parameter tree. Each parameter is synchronised with the
|
|
103
|
+
`Configuration` object of the `EchoProcess` worker.
|
|
104
|
+
"""
|
|
105
|
+
super().init_parameter_tree()
|
|
106
|
+
|
|
107
|
+
# Connect
|
|
108
|
+
self.wconfiguration.sig_echo_index_changed.connect(
|
|
109
|
+
self.update_echo_index_from_settings
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def init_demodulation(self):
|
|
113
|
+
"""
|
|
114
|
+
Additionnal initializations for digital demodulation mode.
|
|
115
|
+
|
|
116
|
+
Add the Demodulation section in the parameter tree and the frame reference plot.
|
|
117
|
+
|
|
118
|
+
It happens only when a worker is created so we can connect its signals as well,
|
|
119
|
+
instead of in `connect_worker()` for the other buttons.
|
|
120
|
+
"""
|
|
121
|
+
# Add the Demodulation section to ParameterTree
|
|
122
|
+
self.wconfiguration.init_demodulation_tree()
|
|
123
|
+
|
|
124
|
+
# Update the files filter suffix in the batch processing panel
|
|
125
|
+
self.wbatch.line_suffix.setText(".wfm")
|
|
126
|
+
self.wbatch.add_findf0_checkbox()
|
|
127
|
+
|
|
128
|
+
# Fill the demodulation parameters from the worker
|
|
129
|
+
for p in self.wconfiguration.demodulation_parameters:
|
|
130
|
+
self.set_param_from_worker(p.name(), context="demodulation")
|
|
131
|
+
|
|
132
|
+
# Add the frame reference signal plot tab
|
|
133
|
+
self.wgraphs.add_reference_in_frame_tab()
|
|
134
|
+
|
|
135
|
+
# Connect
|
|
136
|
+
self.wconfiguration.sig_find_f0.connect(self.find_f0)
|
|
137
|
+
self.sig_worker_find_f0.connect(self.worker.find_f0)
|
|
138
|
+
self.worker.sig_find_f0_finished.connect(self.find_f0_finished)
|
|
139
|
+
self.wconfiguration.sig_demodulate.connect(self.demodulate)
|
|
140
|
+
self.sig_worker_demodulate.connect(self.worker.demodulate)
|
|
141
|
+
self.worker.sig_demodulate_finished.connect(self.demodulate_finished)
|
|
142
|
+
|
|
143
|
+
def init_batch_processing(self):
|
|
144
|
+
"""
|
|
145
|
+
Create the "Batch processing tab.
|
|
146
|
+
|
|
147
|
+
It's multi-panel file picker, where the user can queue files to be
|
|
148
|
+
batch-processed with the same settings.
|
|
149
|
+
"""
|
|
150
|
+
super().init_batch_processing()
|
|
151
|
+
|
|
152
|
+
# Connect
|
|
153
|
+
self.wbatch.sig_echo_index_changed.connect(self.update_echo_index_from_batch)
|
|
154
|
+
|
|
155
|
+
def init_buttons(self):
|
|
156
|
+
"""
|
|
157
|
+
Create and connect the main action buttons.
|
|
158
|
+
|
|
159
|
+
The created buttons and their functions are :
|
|
160
|
+
Load Data : load raw data
|
|
161
|
+
Show Frames : display raw data from frames specified in the settings
|
|
162
|
+
Rolling average : apply rolling average
|
|
163
|
+
Refresh : re-compute the average, attenuation and phase-shift
|
|
164
|
+
Export as CSV : export the results for the current echo index as a CSV file
|
|
165
|
+
Save as NeXus : export all data as a single NeXus (hdf5) file
|
|
166
|
+
"""
|
|
167
|
+
super().init_buttons()
|
|
168
|
+
|
|
169
|
+
# Connect
|
|
170
|
+
self.wbuttons.sig_show.connect(self.show_frames)
|
|
171
|
+
self.wbuttons.sig_rolling.connect(self.rolling_average)
|
|
172
|
+
self.wbuttons.sig_refresh.connect(self.roi_changed)
|
|
173
|
+
self.wbuttons.sig_save_csv.connect(self.export_csv)
|
|
174
|
+
self.wbuttons.sig_save_nexus.connect(self.save_nexus)
|
|
175
|
+
|
|
176
|
+
def init_plots(self):
|
|
177
|
+
"""
|
|
178
|
+
Create the graph area that holds all the plots.
|
|
179
|
+
|
|
180
|
+
The user-draggable ROI defining the time window in which the data is averaged is
|
|
181
|
+
connected here.
|
|
182
|
+
"""
|
|
183
|
+
super().init_plots()
|
|
184
|
+
|
|
185
|
+
# Connect
|
|
186
|
+
self.wgraphs.sig_roi_changed.connect(self.roi_changed)
|
|
187
|
+
|
|
188
|
+
# Connect ROI changed from the tree
|
|
189
|
+
self.wconfiguration.settings_parameters.child(
|
|
190
|
+
"analysis_window"
|
|
191
|
+
).sigValueChanged.connect(self.update_roi)
|
|
192
|
+
|
|
193
|
+
def init_log(self):
|
|
194
|
+
"""
|
|
195
|
+
Create the widget where the log will be printed.
|
|
196
|
+
|
|
197
|
+
The logger is shared with the `pyuson` package-level logger so stream is also
|
|
198
|
+
printed in stdout and the log file.
|
|
199
|
+
"""
|
|
200
|
+
super()._init_log(PROGRAM_NAME, log_level=LOG_LEVEL)
|
|
201
|
+
|
|
202
|
+
@pyqtSlot()
|
|
203
|
+
def load_file(self):
|
|
204
|
+
"""
|
|
205
|
+
Load a configuration or NeXus file and start a worker.
|
|
206
|
+
|
|
207
|
+
Callback for when the "File" parameter changes, or a file is drag & dropped in
|
|
208
|
+
the window.
|
|
209
|
+
"""
|
|
210
|
+
super().load_file()
|
|
211
|
+
if self.worker.proc.is_digital:
|
|
212
|
+
self.init_demodulation()
|
|
213
|
+
|
|
214
|
+
def set_param_from_worker(self, param_name: str, context: str):
|
|
215
|
+
"""
|
|
216
|
+
Set a parameter read from the `Config` object of the `EchoProcessor` worker.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
param_name : str
|
|
221
|
+
Name of the parameter in the tree and `Config`.
|
|
222
|
+
context : {"parameters", "settings", "demodulation"}
|
|
223
|
+
Configuration file section.
|
|
224
|
+
"""
|
|
225
|
+
if context == "demodulation":
|
|
226
|
+
if hasattr(self.worker.proc.cfg.demodulation, param_name):
|
|
227
|
+
config_value = getattr(self.worker.proc.cfg.demodulation, param_name)
|
|
228
|
+
if param_name in self.wconfiguration.parameters_to_parse:
|
|
229
|
+
# special case that needs to be converted to string
|
|
230
|
+
config_value = self.wconfiguration.get_numbers_from_text(
|
|
231
|
+
config_value
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.wconfiguration.demodulation_parameters[param_name] = config_value
|
|
235
|
+
else:
|
|
236
|
+
super().set_param_from_worker(param_name, context)
|
|
237
|
+
|
|
238
|
+
def connect_worker(self):
|
|
239
|
+
"""
|
|
240
|
+
Connect signals from the main thread to tasks in the worker.
|
|
241
|
+
|
|
242
|
+
Note that the demodulation tasks are initialized in the
|
|
243
|
+
`init_parameter_tree_demodulation()` method instead.
|
|
244
|
+
"""
|
|
245
|
+
super().connect_worker()
|
|
246
|
+
|
|
247
|
+
# Rolling average
|
|
248
|
+
self.sig_worker_rolling.connect(self.worker.rolling_average)
|
|
249
|
+
self.worker.sig_rolling_finished.connect(self.rolling_average_finished)
|
|
250
|
+
|
|
251
|
+
# Average frames and computation
|
|
252
|
+
self.sig_worker_average.connect(self.worker.average_frame)
|
|
253
|
+
self.worker.sig_average_finished.connect(self.average_frame_finished)
|
|
254
|
+
|
|
255
|
+
# Export as CSV
|
|
256
|
+
self.sig_worker_export_csv.connect(self.worker.export_as_csv)
|
|
257
|
+
self.worker.sig_export_csv_finished.connect(self.export_csv_finished)
|
|
258
|
+
|
|
259
|
+
@pyqtSlot(str, str)
|
|
260
|
+
def set_worker_config_from_tree(self, param_name: str, context: str):
|
|
261
|
+
"""
|
|
262
|
+
Read parameter from the Parameter Tree and set its sibling in the Config object.
|
|
263
|
+
|
|
264
|
+
Callback for any change in the the "Parameters", "Settings" and "Demodulation"
|
|
265
|
+
sections of the parameter tree, the arguments are passed with the signal.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
param_name : str
|
|
270
|
+
Name of the parameter in the Parameter Tree.
|
|
271
|
+
context : {"parameters", "settings", "demodulation"}
|
|
272
|
+
Define where the key is stored in the Config object (corresponding to the
|
|
273
|
+
configuration file section).
|
|
274
|
+
"""
|
|
275
|
+
if context == "demodulation":
|
|
276
|
+
current_value = self.wconfiguration.demodulation_parameters[param_name]
|
|
277
|
+
if param_name in self.wconfiguration.parameters_to_parse:
|
|
278
|
+
# special case that needs to be parsed
|
|
279
|
+
current_value = self.wconfiguration.get_numbers_from_text(current_value)
|
|
280
|
+
setattr(self.worker.proc.cfg.demodulation, param_name, current_value)
|
|
281
|
+
else:
|
|
282
|
+
super().set_worker_config_from_tree(param_name, context)
|
|
283
|
+
|
|
284
|
+
@pyqtSlot()
|
|
285
|
+
def load_data_finished(self):
|
|
286
|
+
"""
|
|
287
|
+
Show frames after data is loaded.
|
|
288
|
+
|
|
289
|
+
Callback for when the worker has finished loading data.
|
|
290
|
+
"""
|
|
291
|
+
self.show_frames()
|
|
292
|
+
self.align_field_finished()
|
|
293
|
+
self.roi_changed()
|
|
294
|
+
super().load_data_finished()
|
|
295
|
+
|
|
296
|
+
@pyqtSlot()
|
|
297
|
+
def align_field_finished(self):
|
|
298
|
+
"""
|
|
299
|
+
Re-plot magnetic field with its new time vector.
|
|
300
|
+
|
|
301
|
+
Set the crosshair that tracks frames in the plot on mouse hover, and store
|
|
302
|
+
increasing and decreasing magnetic field indices to plot them in different
|
|
303
|
+
colors.
|
|
304
|
+
|
|
305
|
+
Callback for when the worker has finished loading data. This is re-triggered
|
|
306
|
+
everytime the time vector is susceptible to change (subsampling, ...).
|
|
307
|
+
"""
|
|
308
|
+
# Update field plot axis label
|
|
309
|
+
self.wgraphs.field.setLabel("top", "frame (#)")
|
|
310
|
+
|
|
311
|
+
super().align_field_finished()
|
|
312
|
+
|
|
313
|
+
@pyqtSlot()
|
|
314
|
+
def find_f0(self):
|
|
315
|
+
"""
|
|
316
|
+
Find center frequency.
|
|
317
|
+
|
|
318
|
+
Send the signal to the worker to find the radio-frequency from the signal (or
|
|
319
|
+
the configuration file). Callback for the "Find f0" button in the demodulation
|
|
320
|
+
section of the parameter tree.
|
|
321
|
+
"""
|
|
322
|
+
if not self.check_data_loaded():
|
|
323
|
+
return
|
|
324
|
+
self.disable_buttons()
|
|
325
|
+
self.sig_worker_find_f0.emit()
|
|
326
|
+
|
|
327
|
+
@pyqtSlot(float)
|
|
328
|
+
def find_f0_finished(self, f0: float):
|
|
329
|
+
"""
|
|
330
|
+
Update f0 in the parameter tree.
|
|
331
|
+
|
|
332
|
+
Callback for when the worker has finished the frequency detection. `f0` is
|
|
333
|
+
passed with the signal.
|
|
334
|
+
"""
|
|
335
|
+
self.wconfiguration.demodulation_parameters["f0"] = f0
|
|
336
|
+
|
|
337
|
+
self.enable_buttons()
|
|
338
|
+
|
|
339
|
+
@pyqtSlot()
|
|
340
|
+
def demodulate(self):
|
|
341
|
+
"""
|
|
342
|
+
Run the digital demodulation.
|
|
343
|
+
|
|
344
|
+
Create a progress bar and send the signal to the worker to perform the digital
|
|
345
|
+
demodulation. Callback for the "Demodulate" button in the "Demodulation" section
|
|
346
|
+
of the parameter tree.
|
|
347
|
+
"""
|
|
348
|
+
if not self.check_data_loaded():
|
|
349
|
+
return
|
|
350
|
+
self.disable_buttons()
|
|
351
|
+
|
|
352
|
+
# Create the progress bar
|
|
353
|
+
nchunks = self.worker.proc.nframes
|
|
354
|
+
self.pbar_demodulation = widgets.PopupProgressBar(
|
|
355
|
+
min=0, max=nchunks, title="Digital demodulation"
|
|
356
|
+
)
|
|
357
|
+
self.worker.sig_demodulation_progress.connect(
|
|
358
|
+
self.pbar_demodulation.update_progress
|
|
359
|
+
)
|
|
360
|
+
self.pbar_demodulation.start_progress()
|
|
361
|
+
|
|
362
|
+
self.sig_worker_demodulate.emit()
|
|
363
|
+
|
|
364
|
+
@pyqtSlot()
|
|
365
|
+
def demodulate_finished(self):
|
|
366
|
+
"""
|
|
367
|
+
Show the frames demodulated signal and run computations after demodulation.
|
|
368
|
+
|
|
369
|
+
Callback for when the worker has finished the digital demodulation.
|
|
370
|
+
"""
|
|
371
|
+
self.pbar_demodulation.close()
|
|
372
|
+
self.worker.sig_demodulation_progress.disconnect()
|
|
373
|
+
|
|
374
|
+
# Rolling average is resetted and signal might be subsampled
|
|
375
|
+
self.align_field_finished()
|
|
376
|
+
self.show_frames()
|
|
377
|
+
self.roi_changed()
|
|
378
|
+
|
|
379
|
+
self.enable_buttons()
|
|
380
|
+
|
|
381
|
+
@pyqtSlot()
|
|
382
|
+
def show_frames(self):
|
|
383
|
+
"""
|
|
384
|
+
Show some frames echoes, in amplitude and phase.
|
|
385
|
+
|
|
386
|
+
Only the data for the frames specified in the settings are shown. It might seem
|
|
387
|
+
convoluted because it is needed to fetch the correct dataset names, depending on
|
|
388
|
+
whether we're in analog or digital mode.
|
|
389
|
+
|
|
390
|
+
Callback for the "Show frames" button, also triggered whenever new data is
|
|
391
|
+
loaded (raw or demodulated).
|
|
392
|
+
"""
|
|
393
|
+
if not self.check_data_loaded():
|
|
394
|
+
return
|
|
395
|
+
elif "time_meas" not in self.worker.proc.data_raw:
|
|
396
|
+
self.logger.warning(
|
|
397
|
+
"[GUI] Data was not properly loaded, check messages above."
|
|
398
|
+
)
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
# Clear existing traces
|
|
402
|
+
self.wgraphs.amp_frame.clearPlots()
|
|
403
|
+
self.wgraphs.phase_frame.clearPlots()
|
|
404
|
+
if self.worker.proc.is_digital:
|
|
405
|
+
self.wgraphs.reference_frame.clearPlots()
|
|
406
|
+
|
|
407
|
+
frame_indices = self.worker.proc.cfg.settings.frame_indices
|
|
408
|
+
for ind in frame_indices:
|
|
409
|
+
idx = int(ind)
|
|
410
|
+
|
|
411
|
+
# Set the curve legend only for the last one
|
|
412
|
+
if idx == frame_indices[-1]:
|
|
413
|
+
legend_flag = True
|
|
414
|
+
else:
|
|
415
|
+
legend_flag = False
|
|
416
|
+
|
|
417
|
+
if self.worker.proc.is_digital:
|
|
418
|
+
# Plot raw trace
|
|
419
|
+
self.wgraphs.amp_frame.plot(
|
|
420
|
+
self.worker.proc.get_data_raw("time_meas"),
|
|
421
|
+
self.worker.proc.get_data_raw(self.worker.proc._sig_name)[:, idx],
|
|
422
|
+
pen=self.wgraphs.pen_amp,
|
|
423
|
+
)
|
|
424
|
+
# Plot demodulated results if it exists
|
|
425
|
+
iname = self.worker.proc.measurements[0]
|
|
426
|
+
qname = self.worker.proc.measurements[1]
|
|
427
|
+
if self.worker.proc.get_data_processed(
|
|
428
|
+
iname, checkonly=True
|
|
429
|
+
) and self.worker.proc.get_data_processed(qname, checkonly=True):
|
|
430
|
+
self.show_frames_demodulated()
|
|
431
|
+
# Plot reference trace
|
|
432
|
+
self.wgraphs.reference_frame.plot(
|
|
433
|
+
self.worker.proc.get_data_raw("time_meas"),
|
|
434
|
+
self.worker.proc.get_data_raw("reference")[:, idx],
|
|
435
|
+
pen=pg.mkPen(color=pg.intColor(idx), hues=len(frame_indices)),
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
# Analog mode
|
|
439
|
+
aname = self.worker.proc.measurements[0]
|
|
440
|
+
self.wgraphs.amp_frame.plot(
|
|
441
|
+
self.worker.proc.get_data_raw("time_meas"),
|
|
442
|
+
self.worker.proc.get_data_raw(aname)[:, idx],
|
|
443
|
+
pen=self.wgraphs.pen_amp,
|
|
444
|
+
)
|
|
445
|
+
iname = self.worker.proc.measurements[1]
|
|
446
|
+
self.wgraphs.phase_frame.plot(
|
|
447
|
+
self.worker.proc.get_data_raw("time_meas"),
|
|
448
|
+
self.worker.proc.get_data_raw(iname)[:, idx],
|
|
449
|
+
pen=self.wgraphs.pen_in_phase,
|
|
450
|
+
name="in-phase" if legend_flag else None,
|
|
451
|
+
)
|
|
452
|
+
qname = self.worker.proc.measurements[2]
|
|
453
|
+
self.wgraphs.phase_frame.plot(
|
|
454
|
+
self.worker.proc.get_data_raw("time_meas"),
|
|
455
|
+
self.worker.proc.get_data_raw(qname)[:, idx],
|
|
456
|
+
pen=self.wgraphs.pen_out_phase,
|
|
457
|
+
name="out-of-phase" if legend_flag else None,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def show_frames_demodulated(self):
|
|
461
|
+
"""Overlay demodulation result on raw frames."""
|
|
462
|
+
# Get measurements names
|
|
463
|
+
iname = self.worker.proc.measurements[0]
|
|
464
|
+
qname = self.worker.proc.measurements[1]
|
|
465
|
+
|
|
466
|
+
# Get correct time vector if decimation was used
|
|
467
|
+
if self.worker.proc.is_decimated:
|
|
468
|
+
xt = self.worker.proc.get_data_processed("time_meas")
|
|
469
|
+
else:
|
|
470
|
+
xt = self.worker.proc.get_data_raw("time_meas")
|
|
471
|
+
|
|
472
|
+
# Change labels
|
|
473
|
+
self.wgraphs.phase_frame.setLabel("left", "phase (rad)")
|
|
474
|
+
|
|
475
|
+
# Compute amplitude and phase of demodulated signal
|
|
476
|
+
frames_ind = [int(ind) for ind in self.worker.proc.cfg.settings.frame_indices]
|
|
477
|
+
amplitude = sp.compute_amp_iq(
|
|
478
|
+
self.worker.proc.get_data_processed(iname)[:, frames_ind],
|
|
479
|
+
self.worker.proc.get_data_processed(qname)[:, frames_ind],
|
|
480
|
+
)
|
|
481
|
+
phase = sp.compute_phase_iq(
|
|
482
|
+
self.worker.proc.get_data_processed(iname)[:, frames_ind],
|
|
483
|
+
self.worker.proc.get_data_processed(qname)[:, frames_ind],
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Rescale amplitude to show it with the raw trace on the same scale
|
|
487
|
+
amplitude = sp.rescale_a2b(
|
|
488
|
+
amplitude,
|
|
489
|
+
self.worker.proc.get_data_raw(self.worker.proc._sig_name)[:, frames_ind],
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
for ind in range(len(frames_ind)):
|
|
493
|
+
idx = int(ind)
|
|
494
|
+
self.wgraphs.amp_frame.plot(
|
|
495
|
+
xt,
|
|
496
|
+
amplitude[:, idx],
|
|
497
|
+
pen=self.wgraphs.pen_amp_demod,
|
|
498
|
+
)
|
|
499
|
+
self.wgraphs.phase_frame.plot(
|
|
500
|
+
xt,
|
|
501
|
+
phase[:, idx],
|
|
502
|
+
pen=self.wgraphs.pen_phase_demod,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
@pyqtSlot()
|
|
506
|
+
def rolling_average(self):
|
|
507
|
+
"""
|
|
508
|
+
Perform rolling average.
|
|
509
|
+
|
|
510
|
+
Send the signal to the worker to perform the rolling average. Callback for the
|
|
511
|
+
"Rolling average" button.
|
|
512
|
+
"""
|
|
513
|
+
if not self.check_data_loaded():
|
|
514
|
+
return
|
|
515
|
+
self.disable_buttons()
|
|
516
|
+
self.sig_worker_rolling.emit()
|
|
517
|
+
|
|
518
|
+
@pyqtSlot()
|
|
519
|
+
def rolling_average_finished(self):
|
|
520
|
+
"""
|
|
521
|
+
Plot magnetic field in case it was subsampled and retrigger computations.
|
|
522
|
+
|
|
523
|
+
Callback for the when the worker has finished the rolling average.
|
|
524
|
+
"""
|
|
525
|
+
self.align_field_finished()
|
|
526
|
+
self.roi_changed()
|
|
527
|
+
self.enable_buttons()
|
|
528
|
+
|
|
529
|
+
@pyqtSlot()
|
|
530
|
+
def export_csv(self):
|
|
531
|
+
"""
|
|
532
|
+
Export results for the current echo index as CSV.
|
|
533
|
+
|
|
534
|
+
Generate a default file name and open a file picker dialog for the user to
|
|
535
|
+
choose an output file. Determine if the attenuation should be converted to
|
|
536
|
+
dB/cm, and send the signal to the worker to save the results as CSV.
|
|
537
|
+
Callback for the "Export as CSV" button.
|
|
538
|
+
"""
|
|
539
|
+
if not self.check_data_loaded():
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
self.disable_buttons()
|
|
543
|
+
|
|
544
|
+
default_fname = self.worker.proc.get_csv_filename()
|
|
545
|
+
fname, _ = QtWidgets.QFileDialog.getSaveFileName(
|
|
546
|
+
self,
|
|
547
|
+
"Save current echo index as...",
|
|
548
|
+
default_fname,
|
|
549
|
+
"Text files (*.txt, *.csv, *.tsv)",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
if fname:
|
|
553
|
+
self.sig_worker_export_csv.emit(
|
|
554
|
+
fname, self.wbuttons.checkbox_to_cm.isChecked()
|
|
555
|
+
)
|
|
556
|
+
else:
|
|
557
|
+
self.logger.error("[GUI] Invalid output file name for CSV file.")
|
|
558
|
+
self.export_csv_finished()
|
|
559
|
+
|
|
560
|
+
@pyqtSlot()
|
|
561
|
+
def export_csv_finished(self):
|
|
562
|
+
"""Callback for when the worker has finished exporting as CSV."""
|
|
563
|
+
self.enable_buttons()
|
|
564
|
+
|
|
565
|
+
@pyqtSlot()
|
|
566
|
+
def batch_process(self):
|
|
567
|
+
"""
|
|
568
|
+
Run batch processing on selected files.
|
|
569
|
+
|
|
570
|
+
List files in the queue, determine corresponding experiment IDs, create a
|
|
571
|
+
progress bar and send the signal to the worker to perform the batch-processing.
|
|
572
|
+
Callback for the "Batch process" button in the "Batch processing" tab.
|
|
573
|
+
"""
|
|
574
|
+
super().batch_process()
|
|
575
|
+
|
|
576
|
+
# Get settings
|
|
577
|
+
rolling_average = self.wbatch.checkbox_rolling_average.isChecked()
|
|
578
|
+
to_csv = self.wbatch.checkbox_save_as_csv.isChecked()
|
|
579
|
+
to_cm = self.wbuttons.checkbox_to_cm.isChecked()
|
|
580
|
+
if hasattr(self.wbatch, "checkbox_find_f0"):
|
|
581
|
+
find_f0 = self.wbatch.checkbox_find_f0.isChecked()
|
|
582
|
+
else:
|
|
583
|
+
find_f0 = False
|
|
584
|
+
|
|
585
|
+
# Set up progress bar
|
|
586
|
+
self.pbar_batch = widgets.PopupProgressBar(
|
|
587
|
+
min=0, max=len(self.batch_expids), title="Batch processing"
|
|
588
|
+
)
|
|
589
|
+
self.worker.sig_batch_progress.connect(self.batch_process_step_finished)
|
|
590
|
+
self.pbar_batch.start_progress()
|
|
591
|
+
|
|
592
|
+
# Run the batch processing
|
|
593
|
+
self.disable_buttons()
|
|
594
|
+
self.sig_worker_batch.emit(
|
|
595
|
+
self.batch_expids, rolling_average, to_csv, to_cm, find_f0
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
@pyqtSlot(int)
|
|
599
|
+
def batch_process_step_finished(self, idx: int):
|
|
600
|
+
"""Track progress of the batch processing."""
|
|
601
|
+
self.pbar_batch.update_progress(idx)
|
|
602
|
+
super().batch_process_step_finished(idx)
|
|
603
|
+
|
|
604
|
+
@pyqtSlot()
|
|
605
|
+
def batch_process_finished(self):
|
|
606
|
+
"""
|
|
607
|
+
Cleanup after batch-processing.
|
|
608
|
+
|
|
609
|
+
Callback for when the worker has finished the batch-processing.
|
|
610
|
+
"""
|
|
611
|
+
self.pbar_batch.close()
|
|
612
|
+
super().batch_process_finished()
|
|
613
|
+
|
|
614
|
+
@pyqtSlot()
|
|
615
|
+
def roi_changed(self):
|
|
616
|
+
"""
|
|
617
|
+
Trigger averaging and computation of attenuation and phase shift.
|
|
618
|
+
|
|
619
|
+
Callback for when the ROI is changed.
|
|
620
|
+
"""
|
|
621
|
+
# Get time range
|
|
622
|
+
xmin, xmax = self.wgraphs.roi.getRegion()
|
|
623
|
+
|
|
624
|
+
# Check the ROI was changed since it was created
|
|
625
|
+
if (xmin, xmax) == (0, 1):
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
# Update parameter in the tree if the region was changed from the graph
|
|
629
|
+
if self.flag_do_update_roi:
|
|
630
|
+
self.flag_do_update_roi = False
|
|
631
|
+
self.wconfiguration.settings_parameters["analysis_window"] = (
|
|
632
|
+
self.wconfiguration.get_numbers_from_text([xmin, xmax])
|
|
633
|
+
)
|
|
634
|
+
self.flag_do_update_roi = True
|
|
635
|
+
|
|
636
|
+
self.logger.info(f"[GUI] Analysis window set to : {xmin, xmax}")
|
|
637
|
+
|
|
638
|
+
if not self.check_data_loaded():
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
self.average_frame()
|
|
642
|
+
|
|
643
|
+
@pyqtSlot()
|
|
644
|
+
def update_roi(self):
|
|
645
|
+
"""
|
|
646
|
+
Update the ROI in the graph from "Analysis time window" in the parameter tree.
|
|
647
|
+
|
|
648
|
+
Use `flag_do_update_roi` to check if the change is done programatically or from
|
|
649
|
+
the user.
|
|
650
|
+
In the first case, this function is ignored.
|
|
651
|
+
In the second case, the ROI in the graph is updated and computation is
|
|
652
|
+
triggered.
|
|
653
|
+
"""
|
|
654
|
+
if self.flag_do_update_roi:
|
|
655
|
+
# ROI changed from the tree, update the ROI in the graph
|
|
656
|
+
new_region = self.wconfiguration.get_numbers_from_text(
|
|
657
|
+
self.wconfiguration.settings_parameters["analysis_window"]
|
|
658
|
+
)
|
|
659
|
+
if not isinstance(new_region, list):
|
|
660
|
+
# for type checking
|
|
661
|
+
return
|
|
662
|
+
if len(new_region) != 2:
|
|
663
|
+
self.logger.error(f"[GUI] Invalid analysis window : {new_region}")
|
|
664
|
+
return
|
|
665
|
+
elif new_region[0] >= new_region[1]:
|
|
666
|
+
self.logger.error(f"[GUI] Invalid analysis window : {new_region}")
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
# Update ROI, without recomputing
|
|
670
|
+
self.flag_do_update_roi = False
|
|
671
|
+
self.wgraphs.roi.setRegion(new_region)
|
|
672
|
+
self.flag_do_update_roi = True
|
|
673
|
+
else:
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
def average_frame(self):
|
|
677
|
+
"""
|
|
678
|
+
Average frame and compute attenuation and phase-shift.
|
|
679
|
+
|
|
680
|
+
The current analysis window and echo index are used. Send the signal to the
|
|
681
|
+
worker to perform the average and the computation. This happens everytime the
|
|
682
|
+
ROI is changed.
|
|
683
|
+
"""
|
|
684
|
+
if not self.check_data_loaded():
|
|
685
|
+
return
|
|
686
|
+
self.disable_buttons()
|
|
687
|
+
self.sig_worker_average.emit()
|
|
688
|
+
|
|
689
|
+
@pyqtSlot(bool)
|
|
690
|
+
def average_frame_finished(self, status: bool):
|
|
691
|
+
"""
|
|
692
|
+
Update the plots with the new attenuation and phase-shift.
|
|
693
|
+
|
|
694
|
+
Callback for when the worker has finished averaging and computing. `status` is
|
|
695
|
+
passed with the pyqtSignal, it tells if the averaging was successful.
|
|
696
|
+
"""
|
|
697
|
+
if not status:
|
|
698
|
+
self.enable_buttons()
|
|
699
|
+
self.logger.error(
|
|
700
|
+
"[GUI] Averaging frames failed, check the messages above."
|
|
701
|
+
)
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
# Check the magnetic field is aligned
|
|
705
|
+
if not self.check_field_aligned():
|
|
706
|
+
# Force align
|
|
707
|
+
self.worker.align_field()
|
|
708
|
+
|
|
709
|
+
# Plot results
|
|
710
|
+
# Attenuation
|
|
711
|
+
# Versus magnetic field (dB/cm)
|
|
712
|
+
self.plot_var_field(self.wgraphs.amp_field, "attenuation", mult=1e-2)
|
|
713
|
+
# Versus time (dB/cm)
|
|
714
|
+
self.plot_var_time(self.wgraphs.amp_time, "attenuation", mult=1e-2)
|
|
715
|
+
|
|
716
|
+
# Phase-shift
|
|
717
|
+
# Versus magnetic field
|
|
718
|
+
self.plot_var_field(self.wgraphs.phase_field, "phaseshift")
|
|
719
|
+
# Versus time
|
|
720
|
+
self.plot_var_time(self.wgraphs.phase_time, "phaseshift")
|
|
721
|
+
|
|
722
|
+
self.enable_buttons()
|
|
723
|
+
|
|
724
|
+
def plot_field(self):
|
|
725
|
+
"""
|
|
726
|
+
Display magnetic field versus time.
|
|
727
|
+
|
|
728
|
+
The pickup coil voltage and the magnetic field are shown. If it is aligned with
|
|
729
|
+
the experiment time vector, a cross-hair tracks the frame number on mouse hover.
|
|
730
|
+
"""
|
|
731
|
+
super().plot_field()
|
|
732
|
+
|
|
733
|
+
# If data was loaded, we can show frames indices on top with a crosshair
|
|
734
|
+
if self.check_data_loaded():
|
|
735
|
+
self.wgraphs.time2frame_scale = (
|
|
736
|
+
self.worker.proc.get_data_processed("time_exp").size
|
|
737
|
+
/ self.worker.proc.get_data_processed("magfield_time")[-1]
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Create the crosshair if it does not exist yet
|
|
741
|
+
if not hasattr(self.wgraphs, "field_vline"):
|
|
742
|
+
self.wgraphs.init_field_crosshair()
|
|
743
|
+
|
|
744
|
+
@pyqtSlot()
|
|
745
|
+
def update_echo_index_from_batch(self):
|
|
746
|
+
"""Update echo index in the parameter tree when changed from the Batch tab."""
|
|
747
|
+
self.wconfiguration.settings_parameters["echo_index"] = self.wbatch.echo_index
|
|
748
|
+
|
|
749
|
+
@pyqtSlot()
|
|
750
|
+
def update_echo_index_from_settings(self):
|
|
751
|
+
"""Update echo index in the Batch tab when changed from the parameter tree."""
|
|
752
|
+
self.wbatch.echo_index = self.wconfiguration.settings_parameters["echo_index"]
|
|
753
|
+
|
|
754
|
+
def reset(self):
|
|
755
|
+
"""Quit and delete worker and thread, resetting plots and parameters."""
|
|
756
|
+
# Quit thread
|
|
757
|
+
super().reset()
|
|
758
|
+
|
|
759
|
+
# Reset demodulation parameter section
|
|
760
|
+
if (
|
|
761
|
+
hasattr(self.wconfiguration, "demodulation_parameters")
|
|
762
|
+
and self.wconfiguration.demodulation_parameters is not None
|
|
763
|
+
):
|
|
764
|
+
self.wconfiguration.host_parameters.removeChild(
|
|
765
|
+
self.wconfiguration.demodulation_parameters
|
|
766
|
+
)
|
|
767
|
+
self.wconfiguration.demodulation_parameters = None
|