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,819 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic application oriented toward a configuration > analysis > display workflow.
|
|
3
|
+
|
|
4
|
+
It consists of a Configuration Panel on the left, a graphs area on the right, some
|
|
5
|
+
buttons on the bottom right corner and the application log on the bottom right.
|
|
6
|
+
The Configuration Panel is a tab, other tabs are : a file browser and a batch processing
|
|
7
|
+
tab.
|
|
8
|
+
|
|
9
|
+
The BaseMainWindow should be sub-classed to customize buttons and plots, connect the
|
|
10
|
+
signals to a worker embeding a Processor object. See example implementations in the
|
|
11
|
+
`pyuson` or `pytdo` modules.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from PyQt6 import QtGui, QtWidgets
|
|
19
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, pyqtSlot
|
|
20
|
+
|
|
21
|
+
from .widgets import TextLoggerWidget
|
|
22
|
+
|
|
23
|
+
REGEXP_EXPID_SEPARATORS = r"[_-]"
|
|
24
|
+
ALLOWED_FORMAT = ("toml", "json", "nx5", "nxs", "h5", "hdf5")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseMainWindow(QtWidgets.QMainWindow):
|
|
28
|
+
"""
|
|
29
|
+
A generic graphical user interface application.
|
|
30
|
+
|
|
31
|
+
The `MainWindow` class defines the main thread with the front-end user interface.
|
|
32
|
+
It creates the main general layout and include some generic functions related to
|
|
33
|
+
standard analysis processes : generate a PyQtGraph ParameterTree, connect to a
|
|
34
|
+
worker in its own thread, load and plot a magnetic field...
|
|
35
|
+
|
|
36
|
+
The main window consists of the following :
|
|
37
|
+
- A left panel with three tabs, the Configuration tab, a file browser and a Batch
|
|
38
|
+
processing tab,
|
|
39
|
+
- A right panel with all the plots. There can be any number of graphs, in any number
|
|
40
|
+
of tabs,
|
|
41
|
+
- A bottom-left box with the main action button,
|
|
42
|
+
- A bottom-right box with the log.
|
|
43
|
+
|
|
44
|
+
It is built with pre-defined widgets that must implement a specific interface :
|
|
45
|
+
One should subclass this `BaseMainWindow` class and register those widgets as
|
|
46
|
+
private attributes before calling `super().__init__()`. The widgets' class must be
|
|
47
|
+
registered, not an instance of such, e.g. do `ConfigurationWidget`, not
|
|
48
|
+
`ConfigurationWidget()`. The mandatory widgets to implement and register are :
|
|
49
|
+
|
|
50
|
+
- _type_wconfiguration : a configuration widget for the parameter tree
|
|
51
|
+
- _param_content : a simple class that provides the ParameterTree content,
|
|
52
|
+
- _type_wfiles : a file browser widget,
|
|
53
|
+
- _type_wbatch : a batch-processing widget,
|
|
54
|
+
- _type_wgraphs : a graph widget that holds all the plots,
|
|
55
|
+
- _type_wbuttons : a widget with the main action buttons,
|
|
56
|
+
- _type_worker : a worker object.
|
|
57
|
+
|
|
58
|
+
Each of thos classes are instantiated here with the `_type_` removed, e.g.
|
|
59
|
+
`_type_wgraphs` is intantiated as `wgraphs`.
|
|
60
|
+
|
|
61
|
+
Each of those components exposes signals, to which the main thread connects its own
|
|
62
|
+
methods, instead of connecting directly to the widgets (buttons, checkboxes and the
|
|
63
|
+
like). Those signals are named with a trailing `sig_` so that makes it easy to know
|
|
64
|
+
exactly what should be connected in the main thread.
|
|
65
|
+
|
|
66
|
+
When loading a configuration (or a NeXus) file, the main thread creates a worker
|
|
67
|
+
thread. The latter instantiates an Processor object that perform the actual
|
|
68
|
+
analysis.
|
|
69
|
+
|
|
70
|
+
pyqtSignals
|
|
71
|
+
-------
|
|
72
|
+
All signals emit in their corresponding methods and are connected to the
|
|
73
|
+
corresponding methods in the worker thread. Any number of supplementary signals can
|
|
74
|
+
be implemented in subclasses.
|
|
75
|
+
|
|
76
|
+
`sig_worker_load`
|
|
77
|
+
`sig_worker_batch`
|
|
78
|
+
`sig_worker_save_nexus`
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
sig_worker_load = pyqtSignal()
|
|
82
|
+
sig_worker_batch = pyqtSignal()
|
|
83
|
+
sig_worker_save_nexus = pyqtSignal(str)
|
|
84
|
+
|
|
85
|
+
_type_wbatch: type
|
|
86
|
+
_type_wbuttons: type
|
|
87
|
+
_type_wconfiguration: type
|
|
88
|
+
_type_wfiles: type
|
|
89
|
+
_type_wgraphs: type
|
|
90
|
+
_param_content: type
|
|
91
|
+
_type_worker: type
|
|
92
|
+
|
|
93
|
+
def __init__(self):
|
|
94
|
+
super().__init__()
|
|
95
|
+
|
|
96
|
+
# Set flags
|
|
97
|
+
# Check if the ROI change is done by the user or programatically
|
|
98
|
+
self.flag_do_update_roi = True
|
|
99
|
+
# Check if the experiment ID should be reloaded or if the field is just being
|
|
100
|
+
# updated
|
|
101
|
+
self.flag_do_reload_expid = True
|
|
102
|
+
|
|
103
|
+
# Initialize components
|
|
104
|
+
self.init_parameter_tree()
|
|
105
|
+
self.init_files()
|
|
106
|
+
self.init_batch_processing()
|
|
107
|
+
self.init_buttons()
|
|
108
|
+
self.init_plots()
|
|
109
|
+
self.init_log()
|
|
110
|
+
self.init_layout()
|
|
111
|
+
self.setCentralWidget(self.splitter0)
|
|
112
|
+
|
|
113
|
+
# Setup drag&drop
|
|
114
|
+
self.setAcceptDrops(True)
|
|
115
|
+
|
|
116
|
+
# Show window
|
|
117
|
+
self.show()
|
|
118
|
+
|
|
119
|
+
def init_parameter_tree(self):
|
|
120
|
+
"""
|
|
121
|
+
Create the "Configuration" tab.
|
|
122
|
+
|
|
123
|
+
It holds the PyQtGraph parameter tree. Each parameter is synchronised with the
|
|
124
|
+
`Config` object of the Processor worker.
|
|
125
|
+
"""
|
|
126
|
+
self.wconfiguration = self._type_wconfiguration(self._param_content)
|
|
127
|
+
|
|
128
|
+
# Connect
|
|
129
|
+
self.wconfiguration.sig_file_changed.connect(self.load_file)
|
|
130
|
+
self.wconfiguration.sig_expid_changed.connect(self.reload_expid)
|
|
131
|
+
self.wconfiguration.sig_autoload_changed.connect(
|
|
132
|
+
self.update_autoload_from_config
|
|
133
|
+
)
|
|
134
|
+
self.wconfiguration.sig_reload_config.connect(self.load_file)
|
|
135
|
+
self.wconfiguration.sig_save_config.connect(self.save_config)
|
|
136
|
+
|
|
137
|
+
def init_files(self):
|
|
138
|
+
"""
|
|
139
|
+
Create the "Files" tab.
|
|
140
|
+
|
|
141
|
+
It lists files in a directory, one can double-click a file to load either a
|
|
142
|
+
configuration file or change the experiment ID.
|
|
143
|
+
"""
|
|
144
|
+
self.wfiles = self._type_wfiles()
|
|
145
|
+
|
|
146
|
+
# Connect
|
|
147
|
+
self.wfiles.sig_file_selected.connect(self.select_file_in_browser)
|
|
148
|
+
self.wfiles.sig_checkbox_changed.connect(self.update_autoload_from_files)
|
|
149
|
+
|
|
150
|
+
def init_batch_processing(self):
|
|
151
|
+
"""
|
|
152
|
+
Create the "Batch processing tab.
|
|
153
|
+
|
|
154
|
+
It's multi-panel file picker, where the user can queue files to be
|
|
155
|
+
batch-processed with the same settings.
|
|
156
|
+
"""
|
|
157
|
+
self.wbatch = self._type_wbatch()
|
|
158
|
+
|
|
159
|
+
# Connect
|
|
160
|
+
self.wbatch.sig_batch_process.connect(self.batch_process)
|
|
161
|
+
|
|
162
|
+
def init_buttons(self):
|
|
163
|
+
"""Create the main action buttons."""
|
|
164
|
+
self.wbuttons = self._type_wbuttons()
|
|
165
|
+
|
|
166
|
+
# Connect
|
|
167
|
+
self.wbuttons.sig_load.connect(self.load_data)
|
|
168
|
+
self.wbuttons.sig_save_nexus.connect(self.save_nexus)
|
|
169
|
+
|
|
170
|
+
def init_plots(self):
|
|
171
|
+
"""Create the graph area that holds all the plots."""
|
|
172
|
+
self.wgraphs = self._type_wgraphs()
|
|
173
|
+
|
|
174
|
+
def init_log(self):
|
|
175
|
+
"""Initiliaze the log, calling the `_init_log()` method."""
|
|
176
|
+
raise NotImplementedError(
|
|
177
|
+
"Subclass must implement this method, using the "
|
|
178
|
+
"_init_log(program_name, log_level) method"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _init_log(self, program_name: str, log_level: str = "info"):
|
|
182
|
+
"""
|
|
183
|
+
Create the widget where the log will be printed.
|
|
184
|
+
|
|
185
|
+
The logger is shared with the package-level logger so stream is also printed in
|
|
186
|
+
stdout and the log file.
|
|
187
|
+
"""
|
|
188
|
+
log_handler = TextLoggerWidget(self)
|
|
189
|
+
|
|
190
|
+
# Set up the formatter
|
|
191
|
+
log_handler.setFormatter(
|
|
192
|
+
logging.Formatter(
|
|
193
|
+
"{asctime}.{msecs:3g} [{levelname}] : {message}",
|
|
194
|
+
style="{",
|
|
195
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Set up the logger
|
|
200
|
+
self.logger = logging.getLogger(program_name)
|
|
201
|
+
self.logger.addHandler(log_handler)
|
|
202
|
+
self.logger.setLevel(log_level)
|
|
203
|
+
|
|
204
|
+
# Store the logger widget (QPlainTextEdit)
|
|
205
|
+
self.wlog = log_handler.widget
|
|
206
|
+
|
|
207
|
+
def init_layout(self):
|
|
208
|
+
"""
|
|
209
|
+
Set up the main layout.
|
|
210
|
+
|
|
211
|
+
It is composed of a 4 panels seperated with resizable splitters.
|
|
212
|
+
Panel 1 (top left) : Tabs with Configuration, Files and Batch processing
|
|
213
|
+
Panel 2 (bottom left) : Action buttons
|
|
214
|
+
Panel 3 (top right) : Graphs
|
|
215
|
+
Panel 4 (bottom right) : Log
|
|
216
|
+
"""
|
|
217
|
+
self.splitter0 = QtWidgets.QSplitter(self)
|
|
218
|
+
splitter1 = QtWidgets.QSplitter(Qt.Orientation.Vertical, self)
|
|
219
|
+
splitter2 = QtWidgets.QSplitter(Qt.Orientation.Vertical, self)
|
|
220
|
+
|
|
221
|
+
self.config_tabs = QtWidgets.QTabWidget(self)
|
|
222
|
+
self.config_tabs.addTab(self.wconfiguration, "Configuration")
|
|
223
|
+
self.config_tabs.addTab(self.wfiles, "Files")
|
|
224
|
+
self.config_tabs.addTab(self.wbatch, "Batch processing")
|
|
225
|
+
|
|
226
|
+
splitter1.addWidget(self.config_tabs)
|
|
227
|
+
splitter1.addWidget(self.wbuttons)
|
|
228
|
+
|
|
229
|
+
splitter2.addWidget(self.wgraphs)
|
|
230
|
+
splitter2.addWidget(self.wlog)
|
|
231
|
+
|
|
232
|
+
self.splitter0.addWidget(splitter1)
|
|
233
|
+
self.splitter0.addWidget(splitter2)
|
|
234
|
+
|
|
235
|
+
@pyqtSlot()
|
|
236
|
+
def load_file(self):
|
|
237
|
+
"""
|
|
238
|
+
Load a configuration or NeXus file and start a worker.
|
|
239
|
+
|
|
240
|
+
Callback for when the "File" parameter changes, or a file is drag & dropped in
|
|
241
|
+
the window.
|
|
242
|
+
"""
|
|
243
|
+
self.create_worker()
|
|
244
|
+
self.plot_field()
|
|
245
|
+
|
|
246
|
+
if self.worker.is_dataloaded:
|
|
247
|
+
# If NeXus file, data is already ready to be shown
|
|
248
|
+
self.load_data_finished()
|
|
249
|
+
if self.check_field_aligned():
|
|
250
|
+
# Allow plotting versus field
|
|
251
|
+
self.align_field_finished()
|
|
252
|
+
else:
|
|
253
|
+
# Force align field
|
|
254
|
+
self.worker.align_field()
|
|
255
|
+
else:
|
|
256
|
+
# Enable the "Load data" button
|
|
257
|
+
self.wbuttons.button_load.setEnabled(True)
|
|
258
|
+
|
|
259
|
+
# Set experiment ID read from the configuration, without reloading it
|
|
260
|
+
self.flag_do_reload_expid = False
|
|
261
|
+
self.wconfiguration.files_parameters["expid"] = self.worker.proc.expid
|
|
262
|
+
self.flag_do_reload_expid = True
|
|
263
|
+
|
|
264
|
+
# Update file explorer directory
|
|
265
|
+
self.wfiles.file_browser.set_directory(self.worker.proc.cfg.data_directory)
|
|
266
|
+
self.wbatch.current_directory = self.worker.proc.cfg.data_directory
|
|
267
|
+
self.wbatch.prefix = re.split(REGEXP_EXPID_SEPARATORS, self.worker.proc.expid)[
|
|
268
|
+
0
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
# Enable buttons
|
|
272
|
+
self.wconfiguration.button_reload_data.setEnabled(True)
|
|
273
|
+
self.wconfiguration.button_save_config.setEnabled(True)
|
|
274
|
+
|
|
275
|
+
# Load data directly if autoload is on
|
|
276
|
+
if self.wconfiguration.files_parameters["autoload"]:
|
|
277
|
+
self.load_data()
|
|
278
|
+
|
|
279
|
+
def create_worker(self):
|
|
280
|
+
"""Create the Processor object in its own worker thread."""
|
|
281
|
+
# Delete previous instance if any
|
|
282
|
+
if hasattr(self, "worker_thread"):
|
|
283
|
+
self.reset()
|
|
284
|
+
|
|
285
|
+
# Create thread
|
|
286
|
+
self.worker_thread = QThread()
|
|
287
|
+
# Create worker
|
|
288
|
+
self.worker = self._type_worker(self.wconfiguration.files_parameters["file"])
|
|
289
|
+
|
|
290
|
+
# Move worker to thread
|
|
291
|
+
self.worker.moveToThread(self.worker_thread)
|
|
292
|
+
|
|
293
|
+
# Connect signals and slots
|
|
294
|
+
self.connect_worker()
|
|
295
|
+
|
|
296
|
+
# Start the thread
|
|
297
|
+
self.worker_thread.start()
|
|
298
|
+
|
|
299
|
+
# Read the configuration
|
|
300
|
+
self.set_tree_from_worker()
|
|
301
|
+
|
|
302
|
+
def set_tree_from_worker(self):
|
|
303
|
+
"""
|
|
304
|
+
Set the parameters in the parameter tree from the worker configuration.
|
|
305
|
+
|
|
306
|
+
Loop through all parameter in the "parameters" and "settings" section and set
|
|
307
|
+
them from the `Config` object of the Processor object.
|
|
308
|
+
"""
|
|
309
|
+
# In the Parameters section
|
|
310
|
+
for p in self.wconfiguration.param_parameters:
|
|
311
|
+
self.set_param_from_worker(p.name(), context="parameters")
|
|
312
|
+
# In the Settings section
|
|
313
|
+
for p in self.wconfiguration.settings_parameters:
|
|
314
|
+
self.set_param_from_worker(p.name(), context="settings")
|
|
315
|
+
|
|
316
|
+
def set_param_from_worker(self, param_name: str, context: str):
|
|
317
|
+
"""
|
|
318
|
+
Set a parameter read from the `Config` object of the Processor worker.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
param_name : str
|
|
323
|
+
Name of the parameter in the tree and `Config`.
|
|
324
|
+
context : {"parameters", "settings"}
|
|
325
|
+
Configuration file section.
|
|
326
|
+
"""
|
|
327
|
+
match context:
|
|
328
|
+
case "parameters":
|
|
329
|
+
if hasattr(self.worker.proc.cfg.parameters, param_name):
|
|
330
|
+
self.wconfiguration.param_parameters[param_name] = getattr(
|
|
331
|
+
self.worker.proc.cfg.parameters, param_name
|
|
332
|
+
)
|
|
333
|
+
case "settings":
|
|
334
|
+
if hasattr(self.worker.proc.cfg.settings, param_name):
|
|
335
|
+
config_value = getattr(self.worker.proc.cfg.settings, param_name)
|
|
336
|
+
if param_name in self.wconfiguration.parameters_to_parse:
|
|
337
|
+
# special case that needs to be converted to string
|
|
338
|
+
config_value = self.wconfiguration.get_numbers_from_text(
|
|
339
|
+
config_value
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
self.wconfiguration.settings_parameters[param_name] = config_value
|
|
343
|
+
case _:
|
|
344
|
+
raise ValueError(f"Error : unknown configuration section : {context}")
|
|
345
|
+
|
|
346
|
+
def connect_worker(self):
|
|
347
|
+
"""Connect signals from the main thread to tasks in the worker."""
|
|
348
|
+
# Load data and align field
|
|
349
|
+
self.sig_worker_load.connect(self.worker.load_data)
|
|
350
|
+
self.worker.sig_load_finished.connect(self.load_data_finished)
|
|
351
|
+
self.worker.sig_align_finished.connect(self.align_field_finished)
|
|
352
|
+
|
|
353
|
+
# Batch process
|
|
354
|
+
self.sig_worker_batch.connect(self.worker.batch_process)
|
|
355
|
+
self.worker.sig_batch_finished.connect(self.batch_process_finished)
|
|
356
|
+
|
|
357
|
+
# Save as NeXus
|
|
358
|
+
self.sig_worker_save_nexus.connect(self.worker.save_as_nexus)
|
|
359
|
+
self.worker.sig_save_nexus_finished.connect(self.save_nexus_finished)
|
|
360
|
+
|
|
361
|
+
# Watch changes from the parameter tree to update the worker configuration
|
|
362
|
+
self.wconfiguration.sig_parameter_changed.connect(
|
|
363
|
+
self.set_worker_config_from_tree
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
@pyqtSlot(str, str)
|
|
367
|
+
def set_worker_config_from_tree(self, param_name: str, context: str):
|
|
368
|
+
"""
|
|
369
|
+
Read parameter from the Parameter Tree and set its sibling in the Config object.
|
|
370
|
+
|
|
371
|
+
Callback for any change in the the "Parameters", "Settings" sections of the
|
|
372
|
+
parameter tree, the arguments are passed with the signal.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
param_name : str
|
|
377
|
+
Name of the parameter in the Parameter Tree.
|
|
378
|
+
context : {"parameters", "settings"}
|
|
379
|
+
Define where the key is stored in the Config object (corresponding to the
|
|
380
|
+
configuration file section).
|
|
381
|
+
"""
|
|
382
|
+
match context:
|
|
383
|
+
case "parameters":
|
|
384
|
+
setattr(
|
|
385
|
+
self.worker.proc.cfg.parameters,
|
|
386
|
+
param_name,
|
|
387
|
+
self.wconfiguration.param_parameters[param_name],
|
|
388
|
+
)
|
|
389
|
+
case "settings":
|
|
390
|
+
current_value = self.wconfiguration.settings_parameters[param_name]
|
|
391
|
+
if param_name in self.wconfiguration.parameters_to_parse:
|
|
392
|
+
# special case that needs to be parsed
|
|
393
|
+
current_value = self.wconfiguration.get_numbers_from_text(
|
|
394
|
+
current_value
|
|
395
|
+
)
|
|
396
|
+
self.logger.debug(
|
|
397
|
+
f"[GUI] Setting Config from tree : {param_name} : {current_value}"
|
|
398
|
+
)
|
|
399
|
+
setattr(self.worker.proc.cfg.settings, param_name, current_value)
|
|
400
|
+
case _:
|
|
401
|
+
raise ValueError(f"Unknown configuration section : {context}")
|
|
402
|
+
|
|
403
|
+
@pyqtSlot()
|
|
404
|
+
def reload_expid(self):
|
|
405
|
+
"""
|
|
406
|
+
Reload experiment ID, resetting the data via the worker.
|
|
407
|
+
|
|
408
|
+
`expid` is a property of the Processor object, when it is changed, it triggers a
|
|
409
|
+
reinitializtion of the object.
|
|
410
|
+
|
|
411
|
+
If data autoloading is enabled, the data is reloaded here.
|
|
412
|
+
|
|
413
|
+
Callback for a change of the experiment ID in the "Files" section of the
|
|
414
|
+
parameter tree and of the `Reload` button.
|
|
415
|
+
"""
|
|
416
|
+
if not self.check_config_loaded():
|
|
417
|
+
self.logger.warning("[GUI] No configuration file loaded.")
|
|
418
|
+
return
|
|
419
|
+
if not self.flag_do_reload_expid:
|
|
420
|
+
# The experiment ID is being changed programatically, do not update
|
|
421
|
+
return
|
|
422
|
+
# Infer experiment ID
|
|
423
|
+
expid = self.infer_expid(self.wconfiguration.files_parameters["expid"])
|
|
424
|
+
# Set it, without reloading since that's we're doing
|
|
425
|
+
self.flag_do_reload_expid = False
|
|
426
|
+
self.wconfiguration.files_parameters["expid"] = expid
|
|
427
|
+
self.flag_do_reload_expid = True
|
|
428
|
+
|
|
429
|
+
self.logger.info(
|
|
430
|
+
f"[GUI] Reloading experiment ID, from {self.worker.proc.expid} to {expid}"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Set the new experiment ID in the worker
|
|
434
|
+
self.worker.proc.expid = expid
|
|
435
|
+
self.worker.is_dataloaded = False
|
|
436
|
+
|
|
437
|
+
# Prepare
|
|
438
|
+
self.disable_buttons()
|
|
439
|
+
self.wgraphs.clear_all_plots()
|
|
440
|
+
self.plot_field()
|
|
441
|
+
self.wconfiguration.button_reload_data.setEnabled(True)
|
|
442
|
+
self.wconfiguration.button_save_config.setEnabled(True)
|
|
443
|
+
self.wbuttons.button_load.setEnabled(True)
|
|
444
|
+
|
|
445
|
+
if self.wconfiguration.files_parameters["autoload"]:
|
|
446
|
+
# Autoload data
|
|
447
|
+
self.load_data()
|
|
448
|
+
|
|
449
|
+
@pyqtSlot()
|
|
450
|
+
def save_config(self):
|
|
451
|
+
"""
|
|
452
|
+
Save current configuration as a TOML file.
|
|
453
|
+
|
|
454
|
+
A default file name is generated and a file picker dialog box is openned for the
|
|
455
|
+
user to choose an output file.
|
|
456
|
+
"""
|
|
457
|
+
if not self.check_config_loaded():
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
cfg_file = Path(self.wconfiguration.files_parameters["file"])
|
|
461
|
+
default_fname = cfg_file.with_stem(cfg_file.stem + "-2")
|
|
462
|
+
fname, _ = QtWidgets.QFileDialog.getSaveFileName(
|
|
463
|
+
self,
|
|
464
|
+
"Save configuration file as...",
|
|
465
|
+
directory=str(default_fname),
|
|
466
|
+
filter="TOML files (*.toml), JSON files (*.json)",
|
|
467
|
+
options=QtWidgets.QFileDialog.Option.DontConfirmOverwrite,
|
|
468
|
+
)
|
|
469
|
+
self.worker.proc.cfg.write(fname)
|
|
470
|
+
|
|
471
|
+
@pyqtSlot()
|
|
472
|
+
def load_data(self):
|
|
473
|
+
"""
|
|
474
|
+
Load data.
|
|
475
|
+
|
|
476
|
+
Send the signal to the worker to load data. Callback for the "Load data" button.
|
|
477
|
+
It's also used whenever a new dataset is loaded, if data auto-loading is
|
|
478
|
+
enabled.
|
|
479
|
+
"""
|
|
480
|
+
if not self.check_config_loaded():
|
|
481
|
+
self.logger.warning("[GUI] No configuration file loaded.")
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
self.disable_buttons()
|
|
485
|
+
self.sig_worker_load.emit()
|
|
486
|
+
self.logger.debug("emitted signal to load data")
|
|
487
|
+
|
|
488
|
+
@pyqtSlot()
|
|
489
|
+
def load_data_finished(self):
|
|
490
|
+
"""Callback for when the worker has finished loading data."""
|
|
491
|
+
self.enable_buttons()
|
|
492
|
+
|
|
493
|
+
@pyqtSlot()
|
|
494
|
+
def align_field_finished(self):
|
|
495
|
+
"""
|
|
496
|
+
Re-plot magnetic field with its new time vector.
|
|
497
|
+
|
|
498
|
+
Callback for when the worker has finished loading data. This is re-triggered
|
|
499
|
+
everytime the time vector is susceptible to change (subsampling, ...).
|
|
500
|
+
"""
|
|
501
|
+
# Checks
|
|
502
|
+
if not self.check_field_aligned():
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
# Re-plot magnetic field
|
|
506
|
+
self.plot_field()
|
|
507
|
+
|
|
508
|
+
# Get indexer for field up and field down
|
|
509
|
+
t = self.worker.proc.get_data_processed("time_exp")
|
|
510
|
+
b = self.worker.proc.get_data_processed("magfield")
|
|
511
|
+
self.ind_bup = t <= t[b.argmax()] # increasing B
|
|
512
|
+
self.ind_bdown = t > t[b.argmax()] # decreasing B
|
|
513
|
+
self.enable_buttons()
|
|
514
|
+
|
|
515
|
+
@pyqtSlot()
|
|
516
|
+
def save_nexus(self):
|
|
517
|
+
"""
|
|
518
|
+
Save everything as a NeXus file.
|
|
519
|
+
|
|
520
|
+
The output file is self-contained, it saves all parameters, echo indices, raw
|
|
521
|
+
and processed data.
|
|
522
|
+
|
|
523
|
+
Generate a default file name and open a file picker dialog for the user to
|
|
524
|
+
choose an output file, and send the signal to the worker to save the results as
|
|
525
|
+
NeXus.
|
|
526
|
+
Callback for the "Save as NeXus" button.
|
|
527
|
+
"""
|
|
528
|
+
if not self.check_data_loaded():
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
self.disable_buttons()
|
|
532
|
+
|
|
533
|
+
default_fname = self.worker.proc.get_nexus_filename()
|
|
534
|
+
fname, _ = QtWidgets.QFileDialog.getSaveFileName(
|
|
535
|
+
self,
|
|
536
|
+
"Save NeXus file as...",
|
|
537
|
+
default_fname,
|
|
538
|
+
"NeXus files (*.hdf5, *.h5, *.nxs, *.nx5)",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if fname:
|
|
542
|
+
self.sig_worker_save_nexus.emit(fname)
|
|
543
|
+
else:
|
|
544
|
+
self.logger.error("[GUI] Invalid output file name for NeXus file.")
|
|
545
|
+
self.save_nexus_finished()
|
|
546
|
+
|
|
547
|
+
@pyqtSlot()
|
|
548
|
+
def save_nexus_finished(self):
|
|
549
|
+
"""Callback for when the worker has finished saving as NeXus."""
|
|
550
|
+
self.enable_buttons()
|
|
551
|
+
|
|
552
|
+
@pyqtSlot(bool, str)
|
|
553
|
+
def select_file_in_browser(self, is_toml: bool, filepath: str):
|
|
554
|
+
"""
|
|
555
|
+
Select a file and set it as a configuration file or a new experiment ID.
|
|
556
|
+
|
|
557
|
+
Callback for when the user double-click on a file in the "Files" tab.
|
|
558
|
+
"""
|
|
559
|
+
if is_toml:
|
|
560
|
+
self.wconfiguration.files_parameters["file"] = filepath
|
|
561
|
+
else:
|
|
562
|
+
self.wconfiguration.files_parameters["expid"] = filepath
|
|
563
|
+
|
|
564
|
+
@pyqtSlot()
|
|
565
|
+
def batch_process(self):
|
|
566
|
+
"""
|
|
567
|
+
Run batch processing on selected files.
|
|
568
|
+
|
|
569
|
+
List files in the queue, determine corresponding experiment IDs, create a
|
|
570
|
+
progress bar and send the signal to the worker to perform the batch-processing.
|
|
571
|
+
Callback for the "Batch process" button in the "Batch processing" tab.
|
|
572
|
+
"""
|
|
573
|
+
if not self.check_config_loaded():
|
|
574
|
+
self.logger.warning("[GUI] No configuration was loaded.")
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
# List files in the queue
|
|
578
|
+
self.batch_files = self.wbatch.get_files_to_process()
|
|
579
|
+
|
|
580
|
+
if len(self.batch_files) == 0:
|
|
581
|
+
self.logger.warning("[GUI] No items in batch processing list.")
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
# Infer experiments ID from the file names
|
|
585
|
+
self.batch_expids = []
|
|
586
|
+
for file in self.batch_files:
|
|
587
|
+
self.batch_expids.append(self.infer_expid(file))
|
|
588
|
+
|
|
589
|
+
@pyqtSlot(int)
|
|
590
|
+
def batch_process_step_finished(self, idx: int):
|
|
591
|
+
"""Track progress of the batch processing."""
|
|
592
|
+
# Move file in the "Done" list
|
|
593
|
+
self.wbatch.move_to_done(self.batch_files[idx])
|
|
594
|
+
|
|
595
|
+
@pyqtSlot()
|
|
596
|
+
def batch_process_finished(self):
|
|
597
|
+
"""
|
|
598
|
+
Cleanup after batch-processing.
|
|
599
|
+
|
|
600
|
+
Callback for when the worker has finished the batch-processing.
|
|
601
|
+
"""
|
|
602
|
+
self.worker.sig_batch_progress.disconnect()
|
|
603
|
+
|
|
604
|
+
# Set the last experiment ID in the parameter tree, without reloading since it
|
|
605
|
+
# is already the currently loaded dataset
|
|
606
|
+
self.flag_do_reload_expid = False
|
|
607
|
+
self.wconfiguration.files_parameters["expid"] = self.batch_expids[-1]
|
|
608
|
+
self.flag_do_reload_expid = True
|
|
609
|
+
self.batch_files = []
|
|
610
|
+
self.batch_expids = []
|
|
611
|
+
|
|
612
|
+
@pyqtSlot()
|
|
613
|
+
def roi_changed(self):
|
|
614
|
+
"""
|
|
615
|
+
Trigger averaging and computation of attenuation and phase shift.
|
|
616
|
+
|
|
617
|
+
Callback for when the ROI is changed.
|
|
618
|
+
"""
|
|
619
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
620
|
+
|
|
621
|
+
@pyqtSlot()
|
|
622
|
+
def update_roi(self):
|
|
623
|
+
"""Update the ROI in the graph."""
|
|
624
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
625
|
+
|
|
626
|
+
def infer_expid(self, expid: str) -> str:
|
|
627
|
+
"""
|
|
628
|
+
Determine experiment ID from a file name or path.
|
|
629
|
+
|
|
630
|
+
If the input is an existing file, it uses the base file name without extension,
|
|
631
|
+
removes "-pickup" and, for WFM files, the channels number, to build the
|
|
632
|
+
experiment ID. Otherwise, it is used as is.
|
|
633
|
+
"""
|
|
634
|
+
file_path = Path(expid)
|
|
635
|
+
|
|
636
|
+
if file_path.is_file():
|
|
637
|
+
# Input is a file, get the corresponding experiment ID
|
|
638
|
+
if expid.lower().endswith(".wfm"):
|
|
639
|
+
# Remove the _chX bit for WFM files
|
|
640
|
+
res = re.sub(r"_ch\d+\.wfm$", ".wfm", expid, flags=re.IGNORECASE)
|
|
641
|
+
file_path = Path(res)
|
|
642
|
+
|
|
643
|
+
infered_expid = file_path.stem.replace("-pickup", "")
|
|
644
|
+
|
|
645
|
+
return infered_expid
|
|
646
|
+
else:
|
|
647
|
+
# Input experiment ID is a proper experiment ID, return as is
|
|
648
|
+
return expid
|
|
649
|
+
|
|
650
|
+
def plot_field(self):
|
|
651
|
+
"""
|
|
652
|
+
Display magnetic field versus time.
|
|
653
|
+
|
|
654
|
+
The pickup coil voltage and the magnetic field are shown. If it is aligned with
|
|
655
|
+
the experiment time vector, a cross-hair tracks the frame number on mouse hover.
|
|
656
|
+
"""
|
|
657
|
+
if not self.worker.proc.get_data_processed("magfield", checkonly=True):
|
|
658
|
+
self.worker.proc.compute_field()
|
|
659
|
+
|
|
660
|
+
if not self.worker.proc.get_data_processed("magfield", checkonly=True):
|
|
661
|
+
# Still no field to plot, maybe there was no pickup and data was not
|
|
662
|
+
# loaded so the pickup signal could be simulated
|
|
663
|
+
self.logger.warning("[GUI] Field was not computed, not plotting.")
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
# B(t)
|
|
667
|
+
self.wgraphs.field.clearPlots()
|
|
668
|
+
self.wgraphs.field.plot(
|
|
669
|
+
self.worker.proc.get_data_processed("magfield_time"),
|
|
670
|
+
self.worker.proc.get_data_processed("magfield"),
|
|
671
|
+
pen=self.wgraphs.pen_field,
|
|
672
|
+
)
|
|
673
|
+
self.wgraphs.field.setTitle(
|
|
674
|
+
"Magnetic field (max. field : "
|
|
675
|
+
f"{self.worker.proc.get_data_processed('magfield').max():2.2f}T)"
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# dB/dt (pickup)
|
|
679
|
+
self.wgraphs.dfield.clearPlots()
|
|
680
|
+
if self.worker.proc.get_data_raw(
|
|
681
|
+
"pickup_time", checkonly=True
|
|
682
|
+
) and self.worker.proc.get_data_raw("pickup", checkonly=True):
|
|
683
|
+
self.wgraphs.dfield.plot(
|
|
684
|
+
self.worker.proc.get_data_raw("pickup_time"),
|
|
685
|
+
self.worker.proc.get_data_raw("pickup"),
|
|
686
|
+
pen=self.wgraphs.pen_field,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def plot_var_time(self, pgplot, varname: str, mult: float = 1):
|
|
690
|
+
"""
|
|
691
|
+
Plot `varname` versus experiment time.
|
|
692
|
+
|
|
693
|
+
Used to plot attenuation and phase-shift versus time.
|
|
694
|
+
"""
|
|
695
|
+
pgplot.clearPlots()
|
|
696
|
+
pgplot.plot(
|
|
697
|
+
self.worker.proc.get_data_processed("time_exp"),
|
|
698
|
+
self.worker.proc.get_data_serie(varname) * mult,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def plot_var_field(self, pgplot, varname: str, mult: float = 1):
|
|
702
|
+
"""Plot `varname` versus magnetic field.
|
|
703
|
+
|
|
704
|
+
Field rise and decay are distinguished with a different color. Corresponding
|
|
705
|
+
indices should be already available from the `ind_bdown` and `ind_bup`
|
|
706
|
+
attributes, that are created in the `align_field_finished()` method.
|
|
707
|
+
"""
|
|
708
|
+
pgplot.clearPlots()
|
|
709
|
+
pgplot.plot(
|
|
710
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
|
|
711
|
+
self.worker.proc.get_data_serie(varname)[self.ind_bdown] * mult,
|
|
712
|
+
pen=self.wgraphs.pen_bdown,
|
|
713
|
+
name="B down",
|
|
714
|
+
)
|
|
715
|
+
pgplot.plot(
|
|
716
|
+
self.worker.proc.get_data_processed("magfield")[self.ind_bup],
|
|
717
|
+
self.worker.proc.get_data_serie(varname)[self.ind_bup] * mult,
|
|
718
|
+
pen=self.wgraphs.pen_bup,
|
|
719
|
+
name="B up",
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def disable_buttons(self):
|
|
723
|
+
"""Disable all buttons and ROIs."""
|
|
724
|
+
self.wconfiguration.disable_buttons()
|
|
725
|
+
self.wbuttons.disable_buttons()
|
|
726
|
+
self.wbatch.disable_buttons()
|
|
727
|
+
self.wgraphs.disable_rois()
|
|
728
|
+
|
|
729
|
+
def enable_buttons(self):
|
|
730
|
+
"""Enable all buttons and ROIs."""
|
|
731
|
+
self.wconfiguration.enable_buttons()
|
|
732
|
+
self.wbuttons.enable_buttons()
|
|
733
|
+
self.wbatch.enable_buttons()
|
|
734
|
+
self.wgraphs.enable_rois()
|
|
735
|
+
|
|
736
|
+
@pyqtSlot()
|
|
737
|
+
def update_autoload_from_files(self):
|
|
738
|
+
"""Update the autoload checkbox in the Configuration when toggled in Files."""
|
|
739
|
+
self.wconfiguration.files_parameters["autoload"] = (
|
|
740
|
+
self.wfiles.checkbox_autoload_data.isChecked()
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
@pyqtSlot()
|
|
744
|
+
def update_autoload_from_config(self):
|
|
745
|
+
"""Update the autoload checkbox in Files when toggled in the Configuration."""
|
|
746
|
+
self.wfiles.checkbox_autoload_data.setChecked(
|
|
747
|
+
self.wconfiguration.files_parameters["autoload"]
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def check_config_loaded(self) -> bool:
|
|
751
|
+
"""Check if a worker was initialized."""
|
|
752
|
+
if hasattr(self, "worker"):
|
|
753
|
+
return True
|
|
754
|
+
else:
|
|
755
|
+
return False
|
|
756
|
+
|
|
757
|
+
def check_data_loaded(self) -> bool:
|
|
758
|
+
"""Check if data was loaded."""
|
|
759
|
+
if not self.check_config_loaded():
|
|
760
|
+
return False
|
|
761
|
+
else:
|
|
762
|
+
if not self.worker.is_dataloaded:
|
|
763
|
+
return False
|
|
764
|
+
else:
|
|
765
|
+
return True
|
|
766
|
+
|
|
767
|
+
def check_field_aligned(self) -> bool:
|
|
768
|
+
"""Check if the magnetic field is aligned on the experiment time vector."""
|
|
769
|
+
if not self.worker.proc.get_data_processed("magfield", checkonly=True):
|
|
770
|
+
self.logger.warning("[GUI] Magnetic field was not computed.")
|
|
771
|
+
return False
|
|
772
|
+
if not self.worker.proc.get_data_processed("time_exp", checkonly=True):
|
|
773
|
+
self.logger.warning("[GUI] Experiment time vector was not built.")
|
|
774
|
+
return False
|
|
775
|
+
fieldsize = self.worker.proc.get_data_processed("magfield").size
|
|
776
|
+
timesize = self.worker.proc.get_data_processed("time_exp").size
|
|
777
|
+
if fieldsize != timesize:
|
|
778
|
+
return False
|
|
779
|
+
else:
|
|
780
|
+
return True
|
|
781
|
+
|
|
782
|
+
def reset(self):
|
|
783
|
+
"""Quit and delete worker and thread, resetting plots and parameters."""
|
|
784
|
+
# Quit thread
|
|
785
|
+
if hasattr(self, "worker_thread"):
|
|
786
|
+
self.worker_thread.quit()
|
|
787
|
+
self.worker_thread.deleteLater()
|
|
788
|
+
if hasattr(self, "worker"):
|
|
789
|
+
self.worker.deleteLater()
|
|
790
|
+
|
|
791
|
+
# Clear plots
|
|
792
|
+
self.wgraphs.clear_all_plots()
|
|
793
|
+
# Disable buttons
|
|
794
|
+
self.disable_buttons()
|
|
795
|
+
|
|
796
|
+
def dragEnterEvent(self, a0: QtGui.QDropEvent | None = None) -> None:
|
|
797
|
+
"""Handle drag&drop configuration or data file."""
|
|
798
|
+
if a0.mimeData().hasText():
|
|
799
|
+
a0.accept()
|
|
800
|
+
else:
|
|
801
|
+
a0.ignore()
|
|
802
|
+
|
|
803
|
+
def dropEvent(self, a0: QtGui.QDropEvent | None = None) -> None:
|
|
804
|
+
"""
|
|
805
|
+
Set the "File" parameter when a file is dropped in the main window.
|
|
806
|
+
|
|
807
|
+
This triggers the `load_file()` method.
|
|
808
|
+
"""
|
|
809
|
+
for url in a0.mimeData().urls():
|
|
810
|
+
file_path = url.toLocalFile()
|
|
811
|
+
if file_path.endswith(ALLOWED_FORMAT):
|
|
812
|
+
self.wconfiguration.files_parameters["file"] = file_path
|
|
813
|
+
|
|
814
|
+
def closeEvent(self, a0):
|
|
815
|
+
"""Quit."""
|
|
816
|
+
if self.check_config_loaded():
|
|
817
|
+
self.reset()
|
|
818
|
+
print("Bye !")
|
|
819
|
+
return super().closeEvent(a0)
|