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.
Files changed (51) hide show
  1. pymagnetos/__init__.py +15 -0
  2. pymagnetos/cli.py +40 -0
  3. pymagnetos/core/__init__.py +19 -0
  4. pymagnetos/core/_config.py +340 -0
  5. pymagnetos/core/_data.py +132 -0
  6. pymagnetos/core/_processor.py +905 -0
  7. pymagnetos/core/config_models.py +57 -0
  8. pymagnetos/core/gui/__init__.py +6 -0
  9. pymagnetos/core/gui/_base_mainwindow.py +819 -0
  10. pymagnetos/core/gui/widgets/__init__.py +19 -0
  11. pymagnetos/core/gui/widgets/_batch_processing.py +319 -0
  12. pymagnetos/core/gui/widgets/_configuration.py +167 -0
  13. pymagnetos/core/gui/widgets/_files.py +129 -0
  14. pymagnetos/core/gui/widgets/_graphs.py +93 -0
  15. pymagnetos/core/gui/widgets/_param_content.py +20 -0
  16. pymagnetos/core/gui/widgets/_popup_progressbar.py +29 -0
  17. pymagnetos/core/gui/widgets/_text_logger.py +32 -0
  18. pymagnetos/core/signal_processing.py +1004 -0
  19. pymagnetos/core/utils.py +85 -0
  20. pymagnetos/log.py +126 -0
  21. pymagnetos/py.typed +0 -0
  22. pymagnetos/pytdo/__init__.py +6 -0
  23. pymagnetos/pytdo/_config.py +24 -0
  24. pymagnetos/pytdo/_config_models.py +59 -0
  25. pymagnetos/pytdo/_tdoprocessor.py +1052 -0
  26. pymagnetos/pytdo/assets/config_default.toml +84 -0
  27. pymagnetos/pytdo/gui/__init__.py +26 -0
  28. pymagnetos/pytdo/gui/_worker.py +106 -0
  29. pymagnetos/pytdo/gui/main.py +617 -0
  30. pymagnetos/pytdo/gui/widgets/__init__.py +8 -0
  31. pymagnetos/pytdo/gui/widgets/_buttons.py +66 -0
  32. pymagnetos/pytdo/gui/widgets/_configuration.py +78 -0
  33. pymagnetos/pytdo/gui/widgets/_graphs.py +280 -0
  34. pymagnetos/pytdo/gui/widgets/_param_content.py +137 -0
  35. pymagnetos/pyuson/__init__.py +7 -0
  36. pymagnetos/pyuson/_config.py +26 -0
  37. pymagnetos/pyuson/_config_models.py +71 -0
  38. pymagnetos/pyuson/_echoprocessor.py +1901 -0
  39. pymagnetos/pyuson/assets/config_default.toml +92 -0
  40. pymagnetos/pyuson/gui/__init__.py +26 -0
  41. pymagnetos/pyuson/gui/_worker.py +135 -0
  42. pymagnetos/pyuson/gui/main.py +767 -0
  43. pymagnetos/pyuson/gui/widgets/__init__.py +7 -0
  44. pymagnetos/pyuson/gui/widgets/_buttons.py +95 -0
  45. pymagnetos/pyuson/gui/widgets/_configuration.py +85 -0
  46. pymagnetos/pyuson/gui/widgets/_graphs.py +248 -0
  47. pymagnetos/pyuson/gui/widgets/_param_content.py +193 -0
  48. pymagnetos-0.1.0.dist-info/METADATA +23 -0
  49. pymagnetos-0.1.0.dist-info/RECORD +51 -0
  50. pymagnetos-0.1.0.dist-info/WHEEL +4 -0
  51. 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)