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,617 @@
|
|
|
1
|
+
"""Application for TDO analysis."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PyQt6 import QtGui, QtWidgets
|
|
7
|
+
from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
|
8
|
+
|
|
9
|
+
from pymagnetos.core.gui import BaseMainWindow
|
|
10
|
+
from pymagnetos.core.gui.widgets import BatchProcessingWidget, FileBrowserWidget
|
|
11
|
+
|
|
12
|
+
from . import widgets
|
|
13
|
+
from ._worker import DataWorker
|
|
14
|
+
|
|
15
|
+
ICON_PATH = str(Path(__file__).parent / "assets" / "icon.png")
|
|
16
|
+
REGEXP_EXPID_SEPARATORS = r"[_-]"
|
|
17
|
+
PROGRAM_NAME = "pymagnetos"
|
|
18
|
+
OUT_FORMAT = (".txt", ".csv", ".tsv", ".out")
|
|
19
|
+
LOG_LEVEL = "INFO"
|
|
20
|
+
VERSION = importlib.metadata.version(PROGRAM_NAME)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MainWindow(BaseMainWindow):
|
|
24
|
+
"""
|
|
25
|
+
A graphical user interface application for TDO analysis.
|
|
26
|
+
|
|
27
|
+
The `MainWindow` class defines the main thread with the front-end user interface.
|
|
28
|
+
|
|
29
|
+
It is built with the custom components defined in the `widgets` module. Those
|
|
30
|
+
components are referenced in the main thread with a leading `w` :
|
|
31
|
+
`self.wconfiguration` : the "Configuration" tab
|
|
32
|
+
`self.wfiles` : the "Files" tab
|
|
33
|
+
`self.wbatch` : the "Batch processing" tab
|
|
34
|
+
`self.wgraphs` : the widget holding all the graphs
|
|
35
|
+
`self.wbuttons` : the widget holding the main buttons
|
|
36
|
+
`self.wlog` : the widget that streams the log output
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
sig_worker_extract = pyqtSignal()
|
|
40
|
+
sig_worker_offset = pyqtSignal()
|
|
41
|
+
sig_worker_analyse = pyqtSignal()
|
|
42
|
+
sig_worker_tdocsv = pyqtSignal(str)
|
|
43
|
+
sig_worker_rescsv = pyqtSignal(str)
|
|
44
|
+
sig_worker_loadcsv = pyqtSignal(str)
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
# Register the widgets
|
|
48
|
+
self._type_wbatch = BatchProcessingWidget
|
|
49
|
+
self._type_wbuttons = widgets.ButtonsWidget
|
|
50
|
+
self._type_wconfiguration = widgets.ConfigurationWidget
|
|
51
|
+
self._type_wfiles = FileBrowserWidget
|
|
52
|
+
self._type_wgraphs = widgets.GraphsWidget
|
|
53
|
+
self._param_content = widgets.ParamContent
|
|
54
|
+
self._type_worker = DataWorker
|
|
55
|
+
|
|
56
|
+
super().__init__()
|
|
57
|
+
|
|
58
|
+
# Initialize window
|
|
59
|
+
self.setGeometry(300, 300, 900, 450)
|
|
60
|
+
self.setWindowTitle("TDO Analyzer")
|
|
61
|
+
|
|
62
|
+
self.logger.info(f"Running pytdo v{VERSION}")
|
|
63
|
+
|
|
64
|
+
def init_parameter_tree(self):
|
|
65
|
+
"""Create the ParameterTree."""
|
|
66
|
+
super().init_parameter_tree()
|
|
67
|
+
|
|
68
|
+
# Connect
|
|
69
|
+
self.wconfiguration.sig_syncroi_changed.connect(self.syncroi_changed)
|
|
70
|
+
self.wconfiguration.sig_spectro_nperseg_changed.connect(
|
|
71
|
+
self.update_spectro_time_window
|
|
72
|
+
)
|
|
73
|
+
self.wconfiguration.sig_timeoffset_changed.connect(self.apply_time_offset)
|
|
74
|
+
self.wconfiguration.sig_fitdeg_changed.connect(self.analyse)
|
|
75
|
+
self.wconfiguration.sig_npoints_interp_changed.connect(self.analyse)
|
|
76
|
+
self.wconfiguration.sig_curveoffset_changed.connect(self.plot_tdo_detrended)
|
|
77
|
+
|
|
78
|
+
def init_buttons(self):
|
|
79
|
+
"""Create the main buttons."""
|
|
80
|
+
super().init_buttons()
|
|
81
|
+
|
|
82
|
+
# Connect
|
|
83
|
+
self.wbuttons.sig_extract.connect(self.extract_tdo)
|
|
84
|
+
self.wbuttons.sig_analyse.connect(self.analyse)
|
|
85
|
+
self.wbuttons.sig_tdocsv.connect(self.save_tdo_csv)
|
|
86
|
+
self.wbuttons.sig_rescsv.connect(self.save_results_csv)
|
|
87
|
+
|
|
88
|
+
def init_plots(self):
|
|
89
|
+
"""Create the graphs area."""
|
|
90
|
+
super().init_plots()
|
|
91
|
+
|
|
92
|
+
# Connect ROIs
|
|
93
|
+
self.wgraphs.sig_roi1_changed.connect(self.roi1_changed)
|
|
94
|
+
self.wgraphs.sig_roi2_changed.connect(self.roi2_changed)
|
|
95
|
+
self.wconfiguration.settings_parameters.child(
|
|
96
|
+
"poly_window"
|
|
97
|
+
).sigValueChanged.connect(self.update_roi_from_poly)
|
|
98
|
+
self.wconfiguration.settings_parameters.child(
|
|
99
|
+
"fft_window"
|
|
100
|
+
).sigValueChanged.connect(self.update_roi_from_fft)
|
|
101
|
+
|
|
102
|
+
def init_log(self):
|
|
103
|
+
"""Setup logging."""
|
|
104
|
+
super()._init_log(PROGRAM_NAME, log_level=LOG_LEVEL)
|
|
105
|
+
|
|
106
|
+
def connect_worker(self):
|
|
107
|
+
"""Connect signals to the worker slots."""
|
|
108
|
+
super().connect_worker()
|
|
109
|
+
|
|
110
|
+
# Extract signal
|
|
111
|
+
self.sig_worker_extract.connect(self.worker.extract_tdo)
|
|
112
|
+
self.worker.sig_extract_finished.connect(self.extract_tdo_finished)
|
|
113
|
+
|
|
114
|
+
# Time offset
|
|
115
|
+
self.sig_worker_offset.connect(self.worker.time_offset)
|
|
116
|
+
self.worker.sig_offset_finished.connect(self.time_offset_finished)
|
|
117
|
+
|
|
118
|
+
# Field aligned
|
|
119
|
+
self.worker.sig_align_finished.connect(self.align_field_finished)
|
|
120
|
+
|
|
121
|
+
# Analysis
|
|
122
|
+
self.sig_worker_analyse.connect(self.worker.analyse)
|
|
123
|
+
self.worker.sig_analyse_finished.connect(self.analyse_finished)
|
|
124
|
+
|
|
125
|
+
# Save TDO signal as CSV
|
|
126
|
+
self.sig_worker_tdocsv.connect(self.worker.save_tdo_csv)
|
|
127
|
+
self.worker.sig_tdocsv_finished.connect(self.save_tdo_csv_finished)
|
|
128
|
+
|
|
129
|
+
# Save results as CSV
|
|
130
|
+
self.sig_worker_rescsv.connect(self.worker.save_results_csv)
|
|
131
|
+
self.worker.sig_rescsv_finished.connect(self.save_results_csv_finished)
|
|
132
|
+
|
|
133
|
+
# Load from CSV
|
|
134
|
+
self.sig_worker_loadcsv.connect(self.worker.load_csv_file)
|
|
135
|
+
self.worker.sig_load_csv_finished.connect(self.load_csv_file_finished)
|
|
136
|
+
|
|
137
|
+
@pyqtSlot()
|
|
138
|
+
def extract_tdo(self):
|
|
139
|
+
"""Extract TDO signal."""
|
|
140
|
+
if not self.check_data_loaded():
|
|
141
|
+
return
|
|
142
|
+
self.disable_buttons()
|
|
143
|
+
self.sig_worker_extract.emit()
|
|
144
|
+
|
|
145
|
+
@pyqtSlot()
|
|
146
|
+
def align_field_finished(self):
|
|
147
|
+
"""Plot the magnetic field."""
|
|
148
|
+
self.plot_field()
|
|
149
|
+
self.ind_bup = self.worker.proc.inds_inc
|
|
150
|
+
self.ind_bdown = self.worker.proc.inds_dec
|
|
151
|
+
|
|
152
|
+
@pyqtSlot()
|
|
153
|
+
def extract_tdo_finished(self):
|
|
154
|
+
"""Plot the TDO signal."""
|
|
155
|
+
# Clear plots since source signal was changed
|
|
156
|
+
self.wgraphs.tdo_field.clearPlots()
|
|
157
|
+
self.wgraphs.tdo_inverse_field.clearPlots()
|
|
158
|
+
self.wgraphs.fft.clearPlots()
|
|
159
|
+
|
|
160
|
+
self.align_field_finished()
|
|
161
|
+
self.plot_tdo()
|
|
162
|
+
self.enable_buttons()
|
|
163
|
+
|
|
164
|
+
@pyqtSlot()
|
|
165
|
+
def apply_time_offset(self):
|
|
166
|
+
"""Apply an offset between the pickup and the signal time bases."""
|
|
167
|
+
if not self.check_data_loaded():
|
|
168
|
+
return
|
|
169
|
+
self.disable_buttons()
|
|
170
|
+
self.sig_worker_offset.emit()
|
|
171
|
+
|
|
172
|
+
@pyqtSlot()
|
|
173
|
+
def time_offset_finished(self):
|
|
174
|
+
"""Re-plot signals after applying the offset."""
|
|
175
|
+
self.align_field_finished()
|
|
176
|
+
self.plot_tdo()
|
|
177
|
+
self.wgraphs.tdo_field.clearPlots()
|
|
178
|
+
self.wgraphs.tdo_inverse_field.clearPlots()
|
|
179
|
+
self.wgraphs.fft.clearPlots()
|
|
180
|
+
self.analyse()
|
|
181
|
+
|
|
182
|
+
@pyqtSlot()
|
|
183
|
+
def analyse(self):
|
|
184
|
+
"""Fit, detrend, oversample in 1/B and compute the FFT."""
|
|
185
|
+
if not self.check_tdo_extracted():
|
|
186
|
+
self.logger.warning("[GUI] TDO signal was not extracted.")
|
|
187
|
+
return
|
|
188
|
+
self.disable_buttons()
|
|
189
|
+
self.sig_worker_analyse.emit()
|
|
190
|
+
|
|
191
|
+
@pyqtSlot()
|
|
192
|
+
def analyse_finished(self):
|
|
193
|
+
"""Plot the detrended signals and the FFT after analysis."""
|
|
194
|
+
self.wgraphs.fft.clearPlots()
|
|
195
|
+
self.plot_tdo_detrended()
|
|
196
|
+
self.plot_fft()
|
|
197
|
+
self.enable_buttons()
|
|
198
|
+
|
|
199
|
+
@pyqtSlot()
|
|
200
|
+
def save_tdo_csv(self):
|
|
201
|
+
"""Save extracted TDO signal as CSV."""
|
|
202
|
+
if not self.check_data_loaded():
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
self.disable_buttons()
|
|
206
|
+
|
|
207
|
+
default_fname = self.worker.proc.get_csv_filename(suffix="-tdo")
|
|
208
|
+
fname, _ = QtWidgets.QFileDialog.getSaveFileName(
|
|
209
|
+
self,
|
|
210
|
+
"Save TDO signals as...",
|
|
211
|
+
default_fname,
|
|
212
|
+
"Text files (*.txt, *.csv, *.tsv, *.out)",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if fname:
|
|
216
|
+
self.sig_worker_tdocsv.emit(fname)
|
|
217
|
+
else:
|
|
218
|
+
self.logger.error("[GUI] Invalid output file name for CSV file.")
|
|
219
|
+
self.save_tdo_csv_finished()
|
|
220
|
+
|
|
221
|
+
@pyqtSlot()
|
|
222
|
+
def save_tdo_csv_finished(self):
|
|
223
|
+
"""Re-enable buttons."""
|
|
224
|
+
self.enable_buttons()
|
|
225
|
+
|
|
226
|
+
@pyqtSlot()
|
|
227
|
+
def save_results_csv(self):
|
|
228
|
+
"""Save final results as CSV."""
|
|
229
|
+
if not self.check_data_loaded():
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
self.disable_buttons()
|
|
233
|
+
|
|
234
|
+
default_fname = self.worker.proc.get_csv_filename(suffix="-results")
|
|
235
|
+
fname, _ = QtWidgets.QFileDialog.getSaveFileName(
|
|
236
|
+
self,
|
|
237
|
+
"Save TDO signals as...",
|
|
238
|
+
default_fname,
|
|
239
|
+
"Text files (*.txt, *.csv, *.tsv)",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if fname:
|
|
243
|
+
self.sig_worker_rescsv.emit(fname)
|
|
244
|
+
else:
|
|
245
|
+
self.logger.error("[GUI] Invalid output file name for CSV file.")
|
|
246
|
+
self.save_tdo_csv_finished()
|
|
247
|
+
|
|
248
|
+
@pyqtSlot()
|
|
249
|
+
def save_results_csv_finished(self):
|
|
250
|
+
"""Re-enable buttons."""
|
|
251
|
+
self.enable_buttons()
|
|
252
|
+
|
|
253
|
+
def load_csv_file(self, file_path: str):
|
|
254
|
+
"""Load a CSV file."""
|
|
255
|
+
if not self.check_config_loaded():
|
|
256
|
+
self.logger.error(
|
|
257
|
+
"Can't load a CSV file without loading a configuration file first."
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
self.disable_buttons()
|
|
262
|
+
self.sig_worker_loadcsv.emit(file_path)
|
|
263
|
+
|
|
264
|
+
@pyqtSlot()
|
|
265
|
+
def load_csv_file_finished(self):
|
|
266
|
+
"""Trigger action as if the TDO signal was extracted (but it was loaded)."""
|
|
267
|
+
self.extract_tdo_finished()
|
|
268
|
+
self.analyse_finished()
|
|
269
|
+
|
|
270
|
+
@pyqtSlot()
|
|
271
|
+
def batch_process(self):
|
|
272
|
+
"""Required for compatibility with the base app, but it is not implemented."""
|
|
273
|
+
self.logger.warning("Not implemented.")
|
|
274
|
+
|
|
275
|
+
@pyqtSlot()
|
|
276
|
+
def roi1_changed(self):
|
|
277
|
+
"""Trigger analysis when the ROI from the TDO signal moved."""
|
|
278
|
+
# Get time range
|
|
279
|
+
xmin, xmax = self.wgraphs.roi.getRegion()
|
|
280
|
+
|
|
281
|
+
# Check the ROI was changed since it was created
|
|
282
|
+
if (xmin, xmax) == (0, 1):
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Update parameter in the tree if the region was changed from the graph
|
|
286
|
+
if self.flag_do_update_roi:
|
|
287
|
+
self.flag_do_update_roi = False
|
|
288
|
+
self.wconfiguration.settings_parameters["poly_window"] = (
|
|
289
|
+
self.wconfiguration.get_numbers_from_text([xmin, xmax])
|
|
290
|
+
)
|
|
291
|
+
self.flag_do_update_roi = True
|
|
292
|
+
|
|
293
|
+
self.analyse()
|
|
294
|
+
|
|
295
|
+
@pyqtSlot()
|
|
296
|
+
def roi2_changed(self):
|
|
297
|
+
"""Trigger analysis when the ROI from the TDO detrended moved."""
|
|
298
|
+
# Get time range
|
|
299
|
+
xmin, xmax = self.wgraphs.roi2.getRegion()
|
|
300
|
+
|
|
301
|
+
# Check the ROI was changed since it was created
|
|
302
|
+
if (xmin, xmax) == (0, 1):
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
# Update parameter in the tree if the region was changed from the graph
|
|
306
|
+
if self.flag_do_update_roi:
|
|
307
|
+
self.flag_do_update_roi = False
|
|
308
|
+
self.wconfiguration.settings_parameters["fft_window"] = (
|
|
309
|
+
self.wconfiguration.get_numbers_from_text([xmin, xmax])
|
|
310
|
+
)
|
|
311
|
+
self.flag_do_update_roi = True
|
|
312
|
+
|
|
313
|
+
self.analyse()
|
|
314
|
+
|
|
315
|
+
@pyqtSlot()
|
|
316
|
+
def update_roi_from_poly(self):
|
|
317
|
+
"""
|
|
318
|
+
Update the ROI in the graph from "Fit: field window" in the parameter tree.
|
|
319
|
+
|
|
320
|
+
Use `flag_do_update_roi` to check if the change is done programatically or from
|
|
321
|
+
the user.
|
|
322
|
+
In the first case, this function is ignored.
|
|
323
|
+
In the second case, the ROI in the graph is updated and computation is
|
|
324
|
+
triggered.
|
|
325
|
+
"""
|
|
326
|
+
if self.flag_do_update_roi:
|
|
327
|
+
# ROI changed from the tree, update the ROI in the graph
|
|
328
|
+
new_region = self.wconfiguration.get_numbers_from_text(
|
|
329
|
+
self.wconfiguration.settings_parameters["poly_window"]
|
|
330
|
+
)
|
|
331
|
+
if len(new_region) != 2:
|
|
332
|
+
self.logger.error(f"[GUI] Invalid analysis window : {new_region}")
|
|
333
|
+
return
|
|
334
|
+
elif new_region[0] >= new_region[1]:
|
|
335
|
+
self.logger.error(f"[GUI] Invalid analysis window : {new_region}")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Update ROI, without recomputing
|
|
339
|
+
self.flag_do_update_roi = False
|
|
340
|
+
self.wgraphs.roi.setRegion(new_region)
|
|
341
|
+
self.flag_do_update_roi = True
|
|
342
|
+
else:
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
@pyqtSlot()
|
|
346
|
+
def update_roi_from_fft(self):
|
|
347
|
+
"""
|
|
348
|
+
Update the ROI in the graph from "FFT: field window" in the parameter tree.
|
|
349
|
+
|
|
350
|
+
Use `flag_do_update_roi` to check if the change is done programatically or from
|
|
351
|
+
the user.
|
|
352
|
+
In the first case, this function is ignored.
|
|
353
|
+
In the second case, the ROI in the graph is updated and computation is
|
|
354
|
+
triggered.
|
|
355
|
+
"""
|
|
356
|
+
if self.flag_do_update_roi:
|
|
357
|
+
# ROI changed from the tree, update the ROI in the graph
|
|
358
|
+
new_region = self.wconfiguration.get_numbers_from_text(
|
|
359
|
+
self.wconfiguration.settings_parameters["fft_window"]
|
|
360
|
+
)
|
|
361
|
+
if len(new_region) != 2:
|
|
362
|
+
self.logger.error(f"[GUI] Invalid analysis window : {new_region}")
|
|
363
|
+
return
|
|
364
|
+
elif new_region[0] >= new_region[1]:
|
|
365
|
+
self.logger.error(f"[GUI] Invalid analysis window : {new_region}")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Update ROI, without recomputing
|
|
369
|
+
self.flag_do_update_roi = False
|
|
370
|
+
self.wgraphs.roi2.setRegion(new_region)
|
|
371
|
+
self.flag_do_update_roi = True
|
|
372
|
+
else:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
def plot_tdo(self):
|
|
376
|
+
"""Plot TDO signal versus field and time."""
|
|
377
|
+
if not self.check_field_aligned():
|
|
378
|
+
self.worker.align_field()
|
|
379
|
+
|
|
380
|
+
## TDO signal versus field
|
|
381
|
+
self.wgraphs.sig_field.clearPlots()
|
|
382
|
+
|
|
383
|
+
# Decreasing magnetic field
|
|
384
|
+
self.wgraphs.sig_field.plot(
|
|
385
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
|
|
386
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
|
|
387
|
+
self.ind_bdown
|
|
388
|
+
],
|
|
389
|
+
pen=self.wgraphs.pen_bdown,
|
|
390
|
+
name="B down",
|
|
391
|
+
)
|
|
392
|
+
# Increasing magnetic field
|
|
393
|
+
self.wgraphs.sig_field.plot(
|
|
394
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bup],
|
|
395
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
|
|
396
|
+
self.ind_bup
|
|
397
|
+
],
|
|
398
|
+
pen=self.wgraphs.pen_bup,
|
|
399
|
+
name="B up",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
## TDO signal versus time
|
|
403
|
+
self.wgraphs.sig_time.clearPlots()
|
|
404
|
+
|
|
405
|
+
# Decreasing magnetic field
|
|
406
|
+
self.wgraphs.sig_time.plot(
|
|
407
|
+
self.worker.proc.get_data_processed("time_exp")[self.ind_bdown],
|
|
408
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
|
|
409
|
+
self.ind_bdown
|
|
410
|
+
],
|
|
411
|
+
pen=self.wgraphs.pen_bdown,
|
|
412
|
+
name="B down",
|
|
413
|
+
)
|
|
414
|
+
# Increasing magnetic field
|
|
415
|
+
self.wgraphs.sig_time.plot(
|
|
416
|
+
self.worker.proc.get_data_processed("time_exp")[self.ind_bup],
|
|
417
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
|
|
418
|
+
self.ind_bup
|
|
419
|
+
],
|
|
420
|
+
pen=self.wgraphs.pen_bup,
|
|
421
|
+
name="B up",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
@pyqtSlot()
|
|
425
|
+
def plot_tdo_detrended(self):
|
|
426
|
+
"""Plot TDO signal with background removed."""
|
|
427
|
+
if not self.check_tdo_detrended():
|
|
428
|
+
return
|
|
429
|
+
# Get parameters
|
|
430
|
+
offset = self.worker.proc.cfg.settings.offset
|
|
431
|
+
lower_b, upper_b = self.worker.proc.cfg.settings.fft_window
|
|
432
|
+
|
|
433
|
+
# Sync fit and FFT windows
|
|
434
|
+
if lower_b == -1:
|
|
435
|
+
lower_b = self.worker.proc.cfg.settings.poly_window[0]
|
|
436
|
+
if upper_b == -1:
|
|
437
|
+
upper_b = self.worker.proc.cfg.settings.poly_window[1]
|
|
438
|
+
|
|
439
|
+
## TDO detrended versus field
|
|
440
|
+
self.wgraphs.tdo_field.clearPlots()
|
|
441
|
+
self.wgraphs.sig_field.clearPlots()
|
|
442
|
+
self.plot_tdo() # to make sure data shown is synced
|
|
443
|
+
|
|
444
|
+
# Decreasing magnetic field
|
|
445
|
+
if self.worker.proc.get_data_processed(
|
|
446
|
+
self.worker.proc._tdo_det_dec_name, checkonly=True
|
|
447
|
+
):
|
|
448
|
+
self.wgraphs.tdo_field.plot(
|
|
449
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
|
|
450
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_det_dec_name)
|
|
451
|
+
- offset / 2,
|
|
452
|
+
pen=self.wgraphs.pen_bdown,
|
|
453
|
+
name="B down",
|
|
454
|
+
)
|
|
455
|
+
# Increasing magnetic field
|
|
456
|
+
if self.worker.proc.get_data_processed(
|
|
457
|
+
self.worker.proc._tdo_det_inc_name, checkonly=True
|
|
458
|
+
):
|
|
459
|
+
self.wgraphs.tdo_field.plot(
|
|
460
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bup],
|
|
461
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_det_inc_name)
|
|
462
|
+
+ offset / 2,
|
|
463
|
+
pen=self.wgraphs.pen_bup,
|
|
464
|
+
name="B up",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Fit decreasing magnetic field
|
|
468
|
+
if self.worker.proc.get_data_processed(
|
|
469
|
+
self.worker.proc._tdo_name + "_fit_dec", checkonly=True
|
|
470
|
+
):
|
|
471
|
+
self.wgraphs.sig_field.plot(
|
|
472
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
|
|
473
|
+
self.worker.proc.get_data_processed(
|
|
474
|
+
self.worker.proc._tdo_name + "_fit_dec"
|
|
475
|
+
),
|
|
476
|
+
pen=self.wgraphs.pen_fitdown,
|
|
477
|
+
name="Fit B down",
|
|
478
|
+
)
|
|
479
|
+
# Fit increasing magnetic field
|
|
480
|
+
if self.worker.proc.get_data_processed(
|
|
481
|
+
self.worker.proc._tdo_name + "_fit_inc", checkonly=True
|
|
482
|
+
):
|
|
483
|
+
self.wgraphs.sig_field.plot(
|
|
484
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bup],
|
|
485
|
+
self.worker.proc.get_data_processed(
|
|
486
|
+
self.worker.proc._tdo_name + "_fit_inc"
|
|
487
|
+
),
|
|
488
|
+
pen=self.wgraphs.pen_fitbup,
|
|
489
|
+
name="Fit B up",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
## TDO detrended versus 1/B
|
|
493
|
+
self.wgraphs.tdo_inverse_field.clearPlots()
|
|
494
|
+
|
|
495
|
+
# Plot the whole non-interpolated signal
|
|
496
|
+
# Increasing magnetic field
|
|
497
|
+
if self.worker.proc.get_data_processed(
|
|
498
|
+
self.worker.proc._tdo_det_inc_name, checkonly=True
|
|
499
|
+
):
|
|
500
|
+
self.wgraphs.tdo_inverse_field.plot(
|
|
501
|
+
1 / self.worker.proc.get_data_processed("magfield")[self.ind_bup],
|
|
502
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_det_inc_name)
|
|
503
|
+
+ offset / 2,
|
|
504
|
+
pen=self.wgraphs.pen_tdo,
|
|
505
|
+
)
|
|
506
|
+
# Decreasing magnetic field
|
|
507
|
+
if self.worker.proc.get_data_processed(
|
|
508
|
+
self.worker.proc._tdo_det_dec_name, checkonly=True
|
|
509
|
+
):
|
|
510
|
+
self.wgraphs.tdo_inverse_field.plot(
|
|
511
|
+
1 / self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
|
|
512
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_det_dec_name)
|
|
513
|
+
- offset / 2,
|
|
514
|
+
pen=self.wgraphs.pen_tdo,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Now, the oversampled signal
|
|
518
|
+
# Decreasing magnetic field
|
|
519
|
+
if self.worker.proc.get_data_processed(
|
|
520
|
+
self.worker.proc._tdo_inv_dec_name, checkonly=True
|
|
521
|
+
):
|
|
522
|
+
p0 = self.wgraphs.tdo_inverse_field.plot(
|
|
523
|
+
self.worker.proc.get_data_processed("magfield_inverse_dec"),
|
|
524
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_inv_dec_name)
|
|
525
|
+
- offset / 2,
|
|
526
|
+
pen=self.wgraphs.pen_bdown,
|
|
527
|
+
name="B down",
|
|
528
|
+
)
|
|
529
|
+
# Increasing magnetic field
|
|
530
|
+
if self.worker.proc.get_data_processed(
|
|
531
|
+
self.worker.proc._tdo_inv_inc_name, checkonly=True
|
|
532
|
+
):
|
|
533
|
+
p1 = self.wgraphs.tdo_inverse_field.plot(
|
|
534
|
+
self.worker.proc.get_data_processed("magfield_inverse_inc"),
|
|
535
|
+
self.worker.proc.get_data_processed(self.worker.proc._tdo_inv_inc_name)
|
|
536
|
+
+ offset / 2,
|
|
537
|
+
pen=self.wgraphs.pen_bup,
|
|
538
|
+
name="B up",
|
|
539
|
+
)
|
|
540
|
+
# Adjust limits
|
|
541
|
+
self.wgraphs.tdo_inverse_field.getViewBox().autoRange(
|
|
542
|
+
padding=0.1, items=[p0, p1]
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def plot_fft(self):
|
|
546
|
+
"""Plot FFT in 1/B."""
|
|
547
|
+
self.wgraphs.fft.clearPlots()
|
|
548
|
+
|
|
549
|
+
# Decreasing magnetic field
|
|
550
|
+
if self.worker.proc.get_data_processed("fft_dec", checkonly=True):
|
|
551
|
+
self.wgraphs.fft.plot(
|
|
552
|
+
self.worker.proc.get_data_processed("bfreq_dec"),
|
|
553
|
+
self.worker.proc.get_data_processed("fft_dec"),
|
|
554
|
+
pen=self.wgraphs.pen_bdown,
|
|
555
|
+
name="B down",
|
|
556
|
+
)
|
|
557
|
+
# Increasing magnetic field
|
|
558
|
+
if self.worker.proc.get_data_processed("fft_inc", checkonly=True):
|
|
559
|
+
self.wgraphs.fft.plot(
|
|
560
|
+
self.worker.proc.get_data_processed("bfreq_inc"),
|
|
561
|
+
self.worker.proc.get_data_processed("fft_inc"),
|
|
562
|
+
pen=self.wgraphs.pen_bup,
|
|
563
|
+
name="B up",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
@pyqtSlot()
|
|
567
|
+
def syncroi_changed(self):
|
|
568
|
+
"""Update fit and FFT windows sync. status."""
|
|
569
|
+
self.wgraphs._sync_roi = self.wconfiguration.syncroi_parameter.value()
|
|
570
|
+
|
|
571
|
+
@pyqtSlot()
|
|
572
|
+
def update_spectro_time_window(self):
|
|
573
|
+
"""Update the spectro. nperseg setting expressed in time."""
|
|
574
|
+
if not self.check_data_loaded():
|
|
575
|
+
return
|
|
576
|
+
if "fs_signal" not in self.worker.proc.metadata:
|
|
577
|
+
return
|
|
578
|
+
else:
|
|
579
|
+
fs = self.worker.proc.metadata["fs_signal"]
|
|
580
|
+
|
|
581
|
+
self.wconfiguration.host_parameters["spectro_time_window"] = (
|
|
582
|
+
self.wconfiguration.settings_parameters["spectro_nperseg"] / fs
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
@pyqtSlot(bool, str)
|
|
586
|
+
def select_file_in_browser(self, is_toml: bool, filepath: str):
|
|
587
|
+
"""
|
|
588
|
+
Select a file and set it as a configuration file or a new experiment ID.
|
|
589
|
+
|
|
590
|
+
Callback for when the user double-click on a file in the "Files" tab.
|
|
591
|
+
"""
|
|
592
|
+
if filepath.endswith(".csv"):
|
|
593
|
+
self.load_csv_file(filepath)
|
|
594
|
+
else:
|
|
595
|
+
super().select_file_in_browser(is_toml, filepath)
|
|
596
|
+
|
|
597
|
+
def check_tdo_extracted(self) -> bool:
|
|
598
|
+
"""Check if the TDO signal was extracted."""
|
|
599
|
+
if not self.check_data_loaded():
|
|
600
|
+
return False
|
|
601
|
+
return self.worker.proc._check_barycenters_computed()
|
|
602
|
+
|
|
603
|
+
def check_tdo_detrended(self) -> bool:
|
|
604
|
+
"""Check if the detrending was performed."""
|
|
605
|
+
if not self.check_tdo_extracted():
|
|
606
|
+
return False
|
|
607
|
+
return self.worker.proc._check_tdo_detrended()
|
|
608
|
+
|
|
609
|
+
def dropEvent(self, a0: QtGui.QDropEvent | None = None) -> None:
|
|
610
|
+
"""Load a file when it is dropped in the main window."""
|
|
611
|
+
for url in a0.mimeData().urls():
|
|
612
|
+
file_path = url.toLocalFile()
|
|
613
|
+
if file_path.endswith(OUT_FORMAT):
|
|
614
|
+
self.load_csv_file(file_path)
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
super().dropEvent(a0)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Custom widgets for pytdo, the GUI for ultra-sound experiments."""
|
|
2
|
+
|
|
3
|
+
from ._buttons import ButtonsWidget
|
|
4
|
+
from ._configuration import ConfigurationWidget
|
|
5
|
+
from ._graphs import GraphsWidget
|
|
6
|
+
from ._param_content import ParamContent
|
|
7
|
+
|
|
8
|
+
__all__ = ["ButtonsWidget", "ConfigurationWidget", "GraphsWidget", "ParamContent"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The grid with main action buttons."""
|
|
2
|
+
|
|
3
|
+
from PyQt6 import QtWidgets
|
|
4
|
+
from PyQt6.QtCore import pyqtSignal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ButtonsWidget(QtWidgets.QWidget):
|
|
8
|
+
sig_load = pyqtSignal()
|
|
9
|
+
sig_extract = pyqtSignal()
|
|
10
|
+
sig_analyse = pyqtSignal()
|
|
11
|
+
sig_tdocsv = pyqtSignal()
|
|
12
|
+
sig_rescsv = pyqtSignal()
|
|
13
|
+
sig_save_nexus = pyqtSignal()
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__()
|
|
17
|
+
|
|
18
|
+
grid = QtWidgets.QGridLayout()
|
|
19
|
+
|
|
20
|
+
self.button_load = QtWidgets.QPushButton("Load data", self)
|
|
21
|
+
self.button_extract = QtWidgets.QPushButton("Extract TDO", self)
|
|
22
|
+
self.button_analyse = QtWidgets.QPushButton("Oscillations analysis", self)
|
|
23
|
+
self.button_tdocsv = QtWidgets.QPushButton("Export TDO as CSV", self)
|
|
24
|
+
self.button_rescsv = QtWidgets.QPushButton("Export results as CSV", self)
|
|
25
|
+
self.button_save_nexus = QtWidgets.QPushButton("Save as NeXus", self)
|
|
26
|
+
|
|
27
|
+
self.disable_buttons()
|
|
28
|
+
|
|
29
|
+
self.connect_buttons()
|
|
30
|
+
|
|
31
|
+
grid.addWidget(self.button_load, 0, 0)
|
|
32
|
+
grid.addWidget(self.button_extract, 0, 1)
|
|
33
|
+
grid.addWidget(self.button_analyse, 1, 0, 1, 2)
|
|
34
|
+
|
|
35
|
+
grid_save = QtWidgets.QHBoxLayout()
|
|
36
|
+
grid_save.addWidget(self.button_tdocsv)
|
|
37
|
+
grid_save.addWidget(self.button_rescsv)
|
|
38
|
+
grid_save.addWidget(self.button_save_nexus)
|
|
39
|
+
|
|
40
|
+
grid.addLayout(grid_save, 2, 0, 1, 2)
|
|
41
|
+
|
|
42
|
+
self.setLayout(grid)
|
|
43
|
+
|
|
44
|
+
def connect_buttons(self) -> None:
|
|
45
|
+
self.button_load.clicked.connect(self.sig_load.emit)
|
|
46
|
+
self.button_extract.clicked.connect(self.sig_extract.emit)
|
|
47
|
+
self.button_analyse.clicked.connect(self.sig_analyse.emit)
|
|
48
|
+
self.button_tdocsv.clicked.connect(self.sig_tdocsv.emit)
|
|
49
|
+
self.button_rescsv.clicked.connect(self.sig_rescsv.emit)
|
|
50
|
+
self.button_save_nexus.clicked.connect(self.sig_save_nexus.emit)
|
|
51
|
+
|
|
52
|
+
def disable_buttons(self) -> None:
|
|
53
|
+
self.button_load.setEnabled(False)
|
|
54
|
+
self.button_extract.setEnabled(False)
|
|
55
|
+
self.button_analyse.setEnabled(False)
|
|
56
|
+
self.button_tdocsv.setEnabled(False)
|
|
57
|
+
self.button_rescsv.setEnabled(False)
|
|
58
|
+
self.button_save_nexus.setEnabled(False)
|
|
59
|
+
|
|
60
|
+
def enable_buttons(self) -> None:
|
|
61
|
+
self.button_load.setEnabled(False)
|
|
62
|
+
self.button_extract.setEnabled(True)
|
|
63
|
+
self.button_analyse.setEnabled(True)
|
|
64
|
+
self.button_tdocsv.setEnabled(True)
|
|
65
|
+
self.button_rescsv.setEnabled(True)
|
|
66
|
+
self.button_save_nexus.setEnabled(True)
|