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,7 @@
|
|
|
1
|
+
"""Custom widgets for pyuson, the GUI for ultra-sound experiments."""
|
|
2
|
+
|
|
3
|
+
from ._buttons import ButtonsWidget
|
|
4
|
+
from ._configuration import ConfigurationWidget, ParamContent
|
|
5
|
+
from ._graphs import GraphsWidget
|
|
6
|
+
|
|
7
|
+
__all__ = ["ButtonsWidget", "ConfigurationWidget", "GraphsWidget", "ParamContent"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""The widget that hosts the grid with the main action buttons for pyuson."""
|
|
2
|
+
|
|
3
|
+
from PyQt6 import QtWidgets
|
|
4
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ButtonsWidget(QtWidgets.QWidget):
|
|
8
|
+
"""
|
|
9
|
+
The main action buttons.
|
|
10
|
+
|
|
11
|
+
pyqtSignals
|
|
12
|
+
-------
|
|
13
|
+
sig_load : emits when the "Load data" button is clicked.
|
|
14
|
+
sig_show : emits when the "Show frames" button is clicked.
|
|
15
|
+
sig_rolling : emits when the "Rolling average" button is clicked.
|
|
16
|
+
sig_refresh : emits when the "Refresh" button is clicked.
|
|
17
|
+
sig_save_csv : emits when the "Export as CSV" button is clicked.
|
|
18
|
+
sig_save_nexus : emits when the "Save as NeXus" button is clicked.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
sig_load = pyqtSignal()
|
|
22
|
+
sig_show = pyqtSignal()
|
|
23
|
+
sig_rolling = pyqtSignal()
|
|
24
|
+
sig_refresh = pyqtSignal()
|
|
25
|
+
sig_save_csv = pyqtSignal()
|
|
26
|
+
sig_save_nexus = pyqtSignal()
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
super().__init__()
|
|
30
|
+
"""Initialize buttons."""
|
|
31
|
+
# Create grid
|
|
32
|
+
grid = QtWidgets.QGridLayout()
|
|
33
|
+
|
|
34
|
+
# Create buttons
|
|
35
|
+
# Load data
|
|
36
|
+
self.button_load = QtWidgets.QPushButton("Load data", self)
|
|
37
|
+
# Show some frames
|
|
38
|
+
self.button_show = QtWidgets.QPushButton("Show frames", self)
|
|
39
|
+
# Apply moving mean filter
|
|
40
|
+
self.button_rolling = QtWidgets.QPushButton("Rolling average", self)
|
|
41
|
+
# Trigger computation of attenuation and phase shift
|
|
42
|
+
self.button_refresh = QtWidgets.QPushButton("Refresh", self)
|
|
43
|
+
# Export as CSV
|
|
44
|
+
self.button_save_csv = QtWidgets.QPushButton("Export as CSV", self)
|
|
45
|
+
# Checkbox to convert to dB/cm
|
|
46
|
+
self.checkbox_to_cm = QtWidgets.QCheckBox("Convert to dB/cm in CSV", self)
|
|
47
|
+
self.checkbox_to_cm.setChecked(True)
|
|
48
|
+
|
|
49
|
+
# Save as NeXus
|
|
50
|
+
self.button_save_nexus = QtWidgets.QPushButton("Save as NeXus", self)
|
|
51
|
+
|
|
52
|
+
# Disable all buttons
|
|
53
|
+
self.disable_buttons()
|
|
54
|
+
|
|
55
|
+
# Connect them
|
|
56
|
+
self.connect_buttons()
|
|
57
|
+
|
|
58
|
+
# Add buttons to layout
|
|
59
|
+
grid.addWidget(self.button_load, 0, 0)
|
|
60
|
+
grid.addWidget(self.button_show, 0, 1)
|
|
61
|
+
grid.addWidget(self.button_rolling, 1, 0)
|
|
62
|
+
grid.addWidget(self.button_refresh, 1, 1)
|
|
63
|
+
grid.addWidget(self.button_save_csv, 2, 0)
|
|
64
|
+
grid.addWidget(self.button_save_nexus, 2, 1)
|
|
65
|
+
grid.addWidget(self.checkbox_to_cm, 3, 0, Qt.AlignmentFlag.AlignHCenter)
|
|
66
|
+
|
|
67
|
+
# Set widget layout
|
|
68
|
+
self.setLayout(grid)
|
|
69
|
+
|
|
70
|
+
def connect_buttons(self) -> None:
|
|
71
|
+
"""Connect the buttons to their signals."""
|
|
72
|
+
self.button_load.clicked.connect(self.sig_load.emit)
|
|
73
|
+
self.button_show.clicked.connect(self.sig_show.emit)
|
|
74
|
+
self.button_rolling.clicked.connect(self.sig_rolling.emit)
|
|
75
|
+
self.button_refresh.clicked.connect(self.sig_refresh.emit)
|
|
76
|
+
self.button_save_csv.clicked.connect(self.sig_save_csv.emit)
|
|
77
|
+
self.button_save_nexus.clicked.connect(self.sig_save_nexus.emit)
|
|
78
|
+
|
|
79
|
+
def disable_buttons(self) -> None:
|
|
80
|
+
"""Disable all buttons."""
|
|
81
|
+
self.button_load.setEnabled(False)
|
|
82
|
+
self.button_rolling.setEnabled(False)
|
|
83
|
+
self.button_show.setEnabled(False)
|
|
84
|
+
self.button_refresh.setEnabled(False)
|
|
85
|
+
self.button_save_csv.setEnabled(False)
|
|
86
|
+
self.button_save_nexus.setEnabled(False)
|
|
87
|
+
|
|
88
|
+
def enable_buttons(self) -> None:
|
|
89
|
+
"""Enable all buttons (except Load data)."""
|
|
90
|
+
self.button_load.setEnabled(False)
|
|
91
|
+
self.button_rolling.setEnabled(True)
|
|
92
|
+
self.button_show.setEnabled(True)
|
|
93
|
+
self.button_refresh.setEnabled(True)
|
|
94
|
+
self.button_save_csv.setEnabled(True)
|
|
95
|
+
self.button_save_nexus.setEnabled(True)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The Configuration widget for pyuson."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtCore import pyqtSignal
|
|
6
|
+
from pyqtgraph.parametertree import Parameter
|
|
7
|
+
|
|
8
|
+
from pymagnetos.core.gui.widgets import BaseConfigurationWidget
|
|
9
|
+
|
|
10
|
+
from ._param_content import ParamContent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigurationWidget(BaseConfigurationWidget):
|
|
14
|
+
"""
|
|
15
|
+
Initialize a PyQtGraph ParameterTree.
|
|
16
|
+
|
|
17
|
+
pyqtSignals
|
|
18
|
+
-------
|
|
19
|
+
sig_echo_index_changed : emits when the echo index (in the Settings section ) is
|
|
20
|
+
changed.
|
|
21
|
+
sig_find_f0 : emits when the "Find f0" button is clicked.
|
|
22
|
+
sig_demodulate : emits when the "Demodulate" button is clicked.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
sig_echo_index_changed = pyqtSignal()
|
|
26
|
+
sig_find_f0 = pyqtSignal()
|
|
27
|
+
sig_demodulate = pyqtSignal()
|
|
28
|
+
|
|
29
|
+
def __init__(self, param_content: type[ParamContent]) -> None:
|
|
30
|
+
super().__init__(param_content)
|
|
31
|
+
|
|
32
|
+
def init_demodulation_tree(self) -> None:
|
|
33
|
+
"""Create ParameterTree for demodulation parameters."""
|
|
34
|
+
if not hasattr(self._param_content, "children_demodulation"):
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Could not add the demodulation parameter tree, not in digital mode"
|
|
37
|
+
)
|
|
38
|
+
self.demodulation_parameters = Parameter.create(
|
|
39
|
+
name="Demodulation",
|
|
40
|
+
type="group",
|
|
41
|
+
children=self._param_content.children_demodulation,
|
|
42
|
+
)
|
|
43
|
+
self.host_parameters.addChild(self.demodulation_parameters)
|
|
44
|
+
|
|
45
|
+
# Store buttons as the others
|
|
46
|
+
self.button_findf0 = self.demodulation_parameters.child("find_f0")
|
|
47
|
+
self.button_findf0.setOpts(enabled=False)
|
|
48
|
+
self.button_demodulate = self.demodulation_parameters.child("demodulate")
|
|
49
|
+
self.button_demodulate.setOpts(enabled=False)
|
|
50
|
+
|
|
51
|
+
self.connect_to_signals_demodulation()
|
|
52
|
+
|
|
53
|
+
def connect_to_signals(self) -> None:
|
|
54
|
+
"""Connect changes to signals."""
|
|
55
|
+
super().connect_to_signals()
|
|
56
|
+
|
|
57
|
+
# Special case for echo_index to sync it with another spinbox in the Batch
|
|
58
|
+
# processing tab
|
|
59
|
+
self.settings_parameters.child("echo_index").sigValueChanged.connect(
|
|
60
|
+
self.sig_echo_index_changed.emit
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def connect_to_signals_demodulation(self) -> None:
|
|
64
|
+
"""Additionnal connections for demodulation."""
|
|
65
|
+
self.button_findf0.sigActivated.connect(self.sig_find_f0.emit)
|
|
66
|
+
self.button_demodulate.sigActivated.connect(self.sig_demodulate.emit)
|
|
67
|
+
|
|
68
|
+
for p in self.demodulation_parameters:
|
|
69
|
+
self.demodulation_parameters.child(p.name()).sigValueChanged.connect(
|
|
70
|
+
partial(self.sig_parameter_changed.emit, p.name(), "demodulation")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def enable_buttons(self) -> None:
|
|
74
|
+
super().enable_buttons()
|
|
75
|
+
if hasattr(self, "button_findf0"):
|
|
76
|
+
self.button_findf0.setOpts(enabled=True)
|
|
77
|
+
if hasattr(self, "button_demodulate"):
|
|
78
|
+
self.button_demodulate.setOpts(enabled=True)
|
|
79
|
+
|
|
80
|
+
def disable_buttons(self) -> None:
|
|
81
|
+
super().disable_buttons()
|
|
82
|
+
if hasattr(self, "button_findf0"):
|
|
83
|
+
self.button_findf0.setOpts(enabled=False)
|
|
84
|
+
if hasattr(self, "button_demodulate"):
|
|
85
|
+
self.button_demodulate.setOpts(enabled=False)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""The graph area widget for pyuson."""
|
|
2
|
+
|
|
3
|
+
import pyqtgraph as pg
|
|
4
|
+
from PyQt6 import QtCore, QtWidgets
|
|
5
|
+
from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
|
6
|
+
|
|
7
|
+
from pymagnetos.core.gui.widgets import BaseGraphsWidget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GraphsWidget(BaseGraphsWidget):
|
|
11
|
+
"""
|
|
12
|
+
The graphs area with all the plots.
|
|
13
|
+
|
|
14
|
+
pyqtSignals
|
|
15
|
+
-------
|
|
16
|
+
sig_roi_changed : emits when the draggable time-window moved.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
sig_roi_changed = pyqtSignal()
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
# Internal conversion factor from time to frames
|
|
25
|
+
self._time2frame_scale = 1.0
|
|
26
|
+
|
|
27
|
+
# Create empty canvases in tabs
|
|
28
|
+
field_tab = self.create_field_plot()
|
|
29
|
+
frame_tab = self.create_frame_plot()
|
|
30
|
+
amplitude_tab = self.create_amplitude_plot()
|
|
31
|
+
phase_tab = self.create_phase_plot()
|
|
32
|
+
|
|
33
|
+
self.init_coordinates_on_hover()
|
|
34
|
+
|
|
35
|
+
# Create a main splitter for rows (vertical)
|
|
36
|
+
main_splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical)
|
|
37
|
+
|
|
38
|
+
# Create a top splitter for the first row (horizontal)
|
|
39
|
+
top_splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal)
|
|
40
|
+
top_splitter.addWidget(field_tab)
|
|
41
|
+
top_splitter.addWidget(frame_tab)
|
|
42
|
+
top_splitter.setStretchFactor(0, 1) # field_tab stretch factor
|
|
43
|
+
# frame_tab spans 3 columns
|
|
44
|
+
top_splitter.setStretchFactor(1, 3)
|
|
45
|
+
|
|
46
|
+
# Create a bottom splitter for the second row (horizontal)
|
|
47
|
+
bottom_splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal)
|
|
48
|
+
bottom_splitter.addWidget(amplitude_tab)
|
|
49
|
+
bottom_splitter.addWidget(phase_tab)
|
|
50
|
+
# amplitude_tab spans 2 columns
|
|
51
|
+
bottom_splitter.setStretchFactor(0, 2)
|
|
52
|
+
# phase_tab spans 2 columns
|
|
53
|
+
bottom_splitter.setStretchFactor(1, 2)
|
|
54
|
+
|
|
55
|
+
# Add the top and bottom splitters to the main splitter
|
|
56
|
+
main_splitter.addWidget(top_splitter)
|
|
57
|
+
main_splitter.addWidget(bottom_splitter)
|
|
58
|
+
main_splitter.setStretchFactor(0, 1) # top row stretch factor
|
|
59
|
+
main_splitter.setStretchFactor(1, 1) # bottom row stretch factor
|
|
60
|
+
|
|
61
|
+
# Set the main splitter as the central widget
|
|
62
|
+
layout = QtWidgets.QVBoxLayout()
|
|
63
|
+
layout.addWidget(main_splitter)
|
|
64
|
+
self.setLayout(layout)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def time2frame_scale(self) -> float:
|
|
68
|
+
return self._time2frame_scale
|
|
69
|
+
|
|
70
|
+
@time2frame_scale.setter
|
|
71
|
+
def time2frame_scale(self, value: float) -> None:
|
|
72
|
+
self._time2frame_scale = value
|
|
73
|
+
self.field.getAxis("top").setScale(self.time2frame_scale)
|
|
74
|
+
self.dfield.getAxis("top").setScale(self.time2frame_scale)
|
|
75
|
+
|
|
76
|
+
def create_field_plot(self) -> QtWidgets.QTabWidget:
|
|
77
|
+
tab = QtWidgets.QTabWidget(self)
|
|
78
|
+
|
|
79
|
+
# magnetic field (integrated)
|
|
80
|
+
self.field = pg.PlotWidget(title="Magnetic field")
|
|
81
|
+
self.field.setLabel("bottom", "time (s)")
|
|
82
|
+
self.field.setLabel("top", "frame (#) - field not aligned")
|
|
83
|
+
self.field.setLabel("left", "field (T)")
|
|
84
|
+
self.field.showGrid(y=True)
|
|
85
|
+
self._plots_list.add(self.field)
|
|
86
|
+
|
|
87
|
+
# pickup coil voltage (measured)
|
|
88
|
+
self.dfield = pg.PlotWidget(title="Pickup")
|
|
89
|
+
self.dfield.setLabel("bottom", "time (s)")
|
|
90
|
+
self.dfield.setLabel("top", "frame (#)")
|
|
91
|
+
self.dfield.setLabel("left", "pickup (V)")
|
|
92
|
+
self.dfield.showGrid(y=True)
|
|
93
|
+
self._plots_list.add(self.dfield)
|
|
94
|
+
|
|
95
|
+
tab.addTab(self.field, "B(t)")
|
|
96
|
+
tab.addTab(self.dfield, "Pickup")
|
|
97
|
+
|
|
98
|
+
return tab
|
|
99
|
+
|
|
100
|
+
def create_frame_plot(self) -> QtWidgets.QTabWidget:
|
|
101
|
+
tab = QtWidgets.QTabWidget(self)
|
|
102
|
+
|
|
103
|
+
# amplitude
|
|
104
|
+
self.amp_frame = pg.PlotWidget(title="Frames amplitude")
|
|
105
|
+
self.amp_frame.setLabel("bottom", "time (µs)")
|
|
106
|
+
self.amp_frame.setLabel("left", "signal (V)")
|
|
107
|
+
self.amp_frame.showGrid(y=True)
|
|
108
|
+
self.roi = pg.LinearRegionItem() # time range selector
|
|
109
|
+
self.roi.sigRegionChangeFinished.connect(self.roi1_changed)
|
|
110
|
+
self.roi.sigRegionChangeFinished.connect(self.sig_roi_changed.emit)
|
|
111
|
+
self.amp_frame.addItem(self.roi, ignoreBounds=True) # ty:ignore[unknown-argument]
|
|
112
|
+
self._plots_list.add(self.amp_frame)
|
|
113
|
+
|
|
114
|
+
# phases
|
|
115
|
+
self.phase_frame = pg.PlotWidget(title="Frames phase")
|
|
116
|
+
self.phase_frame.setLabel("bottom", "time (µs)")
|
|
117
|
+
self.phase_frame.setLabel("left", "signal (V)")
|
|
118
|
+
self.phase_frame.showGrid(y=True)
|
|
119
|
+
self.roi2 = pg.LinearRegionItem() # twin in phase plot
|
|
120
|
+
self.phase_frame.addItem(self.roi2, ignoreBounds=True) # ty:ignore[unknown-argument]
|
|
121
|
+
self.roi2.sigRegionChangeFinished.connect(self.roi2_changed)
|
|
122
|
+
self.phase_frame.getPlotItem().addLegend()
|
|
123
|
+
self._plots_list.add(self.phase_frame)
|
|
124
|
+
|
|
125
|
+
tab.addTab(self.amp_frame, "Frame amplitude")
|
|
126
|
+
tab.addTab(self.phase_frame, "Frame phase")
|
|
127
|
+
|
|
128
|
+
# Keep a reference to it to add more plots if needed
|
|
129
|
+
self.tab_frame_plot = tab
|
|
130
|
+
|
|
131
|
+
return tab
|
|
132
|
+
|
|
133
|
+
def add_reference_in_frame_tab(self) -> None:
|
|
134
|
+
"""Add a plot for the reference signal for digital demodulation."""
|
|
135
|
+
if hasattr(self, "reference_frame") and isinstance(
|
|
136
|
+
self.reference_frame, pg.PlotWidget
|
|
137
|
+
):
|
|
138
|
+
# Already has a reference frame plot
|
|
139
|
+
return
|
|
140
|
+
self.reference_frame = pg.PlotWidget(title="Frames reference")
|
|
141
|
+
self.reference_frame.setLabel("bottom", "time (µs)")
|
|
142
|
+
self.reference_frame.setLabel("left", "signal (V)")
|
|
143
|
+
self.reference_frame.showGrid(y=True)
|
|
144
|
+
self._plots_list.add(self.reference_frame)
|
|
145
|
+
self.coordinates_on_hover(self.reference_frame)
|
|
146
|
+
|
|
147
|
+
self.tab_frame_plot.addTab(self.reference_frame, "Frame reference")
|
|
148
|
+
|
|
149
|
+
def create_amplitude_plot(self) -> QtWidgets.QTabWidget:
|
|
150
|
+
tab = QtWidgets.QTabWidget(self)
|
|
151
|
+
|
|
152
|
+
# vs field
|
|
153
|
+
self.amp_field = pg.PlotWidget(title="Amplitude (B)")
|
|
154
|
+
self.amp_field.setLabel("bottom", "field (T)")
|
|
155
|
+
self.amp_field.setLabel("left", "attenuation (dB/cm)")
|
|
156
|
+
self.amp_field.showGrid(y=True)
|
|
157
|
+
self.amp_field.getPlotItem().addLegend()
|
|
158
|
+
self._plots_list.add(self.amp_field)
|
|
159
|
+
|
|
160
|
+
# vs amplitude
|
|
161
|
+
self.amp_time = pg.PlotWidget(title="Amplitude (t)")
|
|
162
|
+
self.amp_time.setLabel("bottom", "time (s)")
|
|
163
|
+
self.amp_time.setLabel("left", "attenuation (dB/cm)")
|
|
164
|
+
self.amp_time.showGrid(y=True)
|
|
165
|
+
self._plots_list.add(self.amp_time)
|
|
166
|
+
|
|
167
|
+
tab.addTab(self.amp_field, "Amplitude (B)")
|
|
168
|
+
tab.addTab(self.amp_time, "Amplitude (t)")
|
|
169
|
+
|
|
170
|
+
return tab
|
|
171
|
+
|
|
172
|
+
def create_phase_plot(self) -> QtWidgets.QTabWidget:
|
|
173
|
+
tab = QtWidgets.QTabWidget(self)
|
|
174
|
+
|
|
175
|
+
# vs field
|
|
176
|
+
self.phase_field = pg.PlotWidget(title="Phase (B)")
|
|
177
|
+
self.phase_field.setLabel("bottom", "field (T)")
|
|
178
|
+
self.phase_field.setLabel("left", "dphi/phi")
|
|
179
|
+
self.phase_field.showGrid(y=True)
|
|
180
|
+
self.phase_field.getPlotItem().addLegend()
|
|
181
|
+
self._plots_list.add(self.phase_field)
|
|
182
|
+
|
|
183
|
+
# vs plot
|
|
184
|
+
self.phase_time = pg.PlotWidget(title="Phase (t)")
|
|
185
|
+
self.phase_time.setLabel("bottom", "time (s)")
|
|
186
|
+
self.phase_time.setLabel("left", "dphi/phi")
|
|
187
|
+
self.phase_time.showGrid(y=True)
|
|
188
|
+
self._plots_list.add(self.phase_time)
|
|
189
|
+
|
|
190
|
+
tab.addTab(self.phase_field, "Phase (B)")
|
|
191
|
+
tab.addTab(self.phase_time, "Phase (t)")
|
|
192
|
+
|
|
193
|
+
return tab
|
|
194
|
+
|
|
195
|
+
def init_plot_style(self) -> None:
|
|
196
|
+
"""Set up PyQtGraph line styles."""
|
|
197
|
+
w0 = 1
|
|
198
|
+
w1 = 1
|
|
199
|
+
self.pen_field = pg.mkPen("#c7c7c7", width=w0)
|
|
200
|
+
self.pen_amp = pg.mkPen("#1f77b480", width=w0)
|
|
201
|
+
self.pen_amp_demod = pg.mkPen("#d6272880", width=w0)
|
|
202
|
+
self.pen_in_phase = pg.mkPen("#ff7f0e80", width=w1)
|
|
203
|
+
self.pen_out_phase = pg.mkPen("#17becf80", width=w1)
|
|
204
|
+
self.pen_phase_demod = pg.mkPen("#9467bd80", width=w0)
|
|
205
|
+
self.pen_bup = pg.mkPen("#2ca02cbf", width=w0)
|
|
206
|
+
self.pen_bdown = pg.mkPen("#d62728bf", width=w0)
|
|
207
|
+
|
|
208
|
+
def init_field_crosshair(self) -> None:
|
|
209
|
+
"""Create line cursor on field plot that tracks frame number."""
|
|
210
|
+
self.field_vline = pg.InfiniteLine(angle=90, movable=False)
|
|
211
|
+
self.field.addItem(self.field_vline, ignoreBounds=True) # ty:ignore[unknown-argument]
|
|
212
|
+
self.field.scene().sigMouseMoved.connect(self.field_crosshair_moved) # ty:ignore[possibly-missing-attribute]
|
|
213
|
+
|
|
214
|
+
def field_crosshair_moved(self, evt) -> None:
|
|
215
|
+
"""Define the callback function when the cursor is moved."""
|
|
216
|
+
pos = evt
|
|
217
|
+
vb = self.field.getViewBox()
|
|
218
|
+
if self.field.sceneBoundingRect().contains(pos):
|
|
219
|
+
mouse_point = vb.mapSceneToView(pos)
|
|
220
|
+
index = int(mouse_point.x() * self.time2frame_scale)
|
|
221
|
+
self.field.setLabel("top", f"frame (#), current : {index}")
|
|
222
|
+
self.field_vline.setPos(mouse_point.x())
|
|
223
|
+
|
|
224
|
+
@pyqtSlot()
|
|
225
|
+
def roi1_changed(self) -> None:
|
|
226
|
+
"""Update ROI in the phase panel when the ROI in the amplitude panel moved."""
|
|
227
|
+
roi1 = self.roi.getRegion() # amplitude panel
|
|
228
|
+
roi2 = self.roi2.getRegion() # phase panel
|
|
229
|
+
if roi1 != roi2:
|
|
230
|
+
self.roi2.setRegion(self.roi.getRegion())
|
|
231
|
+
|
|
232
|
+
@pyqtSlot()
|
|
233
|
+
def roi2_changed(self) -> None:
|
|
234
|
+
"""Update ROI in the amplitude panel when the ROI in the phase panel moved."""
|
|
235
|
+
roi1 = self.roi.getRegion() # amplitude panel
|
|
236
|
+
roi2 = self.roi2.getRegion() # phase panel
|
|
237
|
+
if roi1 != roi2:
|
|
238
|
+
self.roi.setRegion(self.roi2.getRegion())
|
|
239
|
+
|
|
240
|
+
def enable_rois(self) -> None:
|
|
241
|
+
"""Enable moving ROIs."""
|
|
242
|
+
self.roi.setMovable(True)
|
|
243
|
+
self.roi2.setMovable(True)
|
|
244
|
+
|
|
245
|
+
def disable_rois(self) -> None:
|
|
246
|
+
"""Disable moving ROIs."""
|
|
247
|
+
self.roi.setMovable(False)
|
|
248
|
+
self.roi2.setMovable(False)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""ParameterTree content for pyuson."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from pymagnetos.core.gui.widgets import BaseParamContent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParamContent(BaseParamContent):
|
|
9
|
+
# List of parameters that will need to be parsed from string to list
|
|
10
|
+
PARAMS_TO_PARSE = ["frame_indices", "range_baseline", "analysis_window"]
|
|
11
|
+
|
|
12
|
+
# Define parameters in the "Parameters" section
|
|
13
|
+
children_parameters = [
|
|
14
|
+
dict(
|
|
15
|
+
name="pickup_surface",
|
|
16
|
+
type="float",
|
|
17
|
+
limits=[0, np.inf],
|
|
18
|
+
value=None,
|
|
19
|
+
suffix="m²",
|
|
20
|
+
siPrefix=False,
|
|
21
|
+
readonly=True,
|
|
22
|
+
title="Pickup surface",
|
|
23
|
+
),
|
|
24
|
+
dict(
|
|
25
|
+
name="pickup_samplerate",
|
|
26
|
+
type="float",
|
|
27
|
+
limits=[0, np.inf],
|
|
28
|
+
value=250e3,
|
|
29
|
+
step=1e3,
|
|
30
|
+
suffix="Hz",
|
|
31
|
+
siPrefix=True,
|
|
32
|
+
readonly=True,
|
|
33
|
+
title="Pickup sample rate",
|
|
34
|
+
),
|
|
35
|
+
dict(
|
|
36
|
+
name="sample_length",
|
|
37
|
+
type="float",
|
|
38
|
+
limits=[0, np.inf],
|
|
39
|
+
value=None,
|
|
40
|
+
step=0.5e-3,
|
|
41
|
+
suffix="m",
|
|
42
|
+
siPrefix=True,
|
|
43
|
+
readonly=True,
|
|
44
|
+
title="Sample length",
|
|
45
|
+
),
|
|
46
|
+
dict(
|
|
47
|
+
name="sample_speed",
|
|
48
|
+
type="float",
|
|
49
|
+
limits=[0, np.inf],
|
|
50
|
+
value=None,
|
|
51
|
+
step=0.5e3,
|
|
52
|
+
suffix="m/s",
|
|
53
|
+
siPrefix=True,
|
|
54
|
+
readonly=True,
|
|
55
|
+
title="Sample speed",
|
|
56
|
+
),
|
|
57
|
+
dict(
|
|
58
|
+
name="detection_mode",
|
|
59
|
+
type="list",
|
|
60
|
+
limits=["reflection", "transmission"],
|
|
61
|
+
value="reflection",
|
|
62
|
+
readonly=True,
|
|
63
|
+
title="Detection mode",
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Define parameters in the "Settings" section (editable)
|
|
68
|
+
children_settings = [
|
|
69
|
+
dict(
|
|
70
|
+
name="echo_index",
|
|
71
|
+
type="int",
|
|
72
|
+
limits=[1, np.inf],
|
|
73
|
+
step=1,
|
|
74
|
+
value=1,
|
|
75
|
+
title="Echo index",
|
|
76
|
+
),
|
|
77
|
+
dict(name="frame_indices", type="str", title="Displayed frames"),
|
|
78
|
+
dict(
|
|
79
|
+
name="rolling_mean_wlen",
|
|
80
|
+
type="int",
|
|
81
|
+
limits=[0, np.inf],
|
|
82
|
+
step=1,
|
|
83
|
+
value=0,
|
|
84
|
+
title="Rolling mean window length",
|
|
85
|
+
),
|
|
86
|
+
dict(
|
|
87
|
+
name="rolling_mean_subsample",
|
|
88
|
+
type="bool",
|
|
89
|
+
limits=[True, False],
|
|
90
|
+
value=False,
|
|
91
|
+
title="Rolling mean subsampling",
|
|
92
|
+
),
|
|
93
|
+
dict(name="range_baseline", type="str", value="", title="Range baseline"),
|
|
94
|
+
dict(
|
|
95
|
+
name="analysis_window", type="str", value="", title="Analysis time window"
|
|
96
|
+
),
|
|
97
|
+
dict(
|
|
98
|
+
name="max_phase_jump",
|
|
99
|
+
type="float",
|
|
100
|
+
value=0.5,
|
|
101
|
+
step=0.1,
|
|
102
|
+
limits=[0, np.inf],
|
|
103
|
+
title="Max. phase jump (* π)",
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
children_demodulation = [
|
|
108
|
+
dict(
|
|
109
|
+
name="f0",
|
|
110
|
+
type="float",
|
|
111
|
+
limits=[0, np.inf],
|
|
112
|
+
step=10e6,
|
|
113
|
+
value=0,
|
|
114
|
+
suffix="Hz",
|
|
115
|
+
siPrefix=True,
|
|
116
|
+
title="f0",
|
|
117
|
+
),
|
|
118
|
+
dict(
|
|
119
|
+
name="fft_nframes",
|
|
120
|
+
type="int",
|
|
121
|
+
limits=[0, np.inf],
|
|
122
|
+
step=10,
|
|
123
|
+
value=0,
|
|
124
|
+
title="FFT : nframes",
|
|
125
|
+
),
|
|
126
|
+
dict(
|
|
127
|
+
name="detrend",
|
|
128
|
+
type="bool",
|
|
129
|
+
limits=[False, True],
|
|
130
|
+
value=False,
|
|
131
|
+
title="FFT : detrend",
|
|
132
|
+
),
|
|
133
|
+
dict(
|
|
134
|
+
name="findsig_nframes",
|
|
135
|
+
type="int",
|
|
136
|
+
limit=[0, np.inf],
|
|
137
|
+
step=10,
|
|
138
|
+
value=0,
|
|
139
|
+
title="Ref. finder : nframes",
|
|
140
|
+
),
|
|
141
|
+
dict(
|
|
142
|
+
name="findsig_nstd",
|
|
143
|
+
type="float",
|
|
144
|
+
limit=[0, np.inf],
|
|
145
|
+
step=0.1,
|
|
146
|
+
value=1.5,
|
|
147
|
+
title="Ref. finder : nstd",
|
|
148
|
+
),
|
|
149
|
+
dict(
|
|
150
|
+
name="findsig_extend",
|
|
151
|
+
type="float",
|
|
152
|
+
limit=[-np.inf, np.inf],
|
|
153
|
+
step=0.01,
|
|
154
|
+
value=-0.10,
|
|
155
|
+
title="Ref. finder : ext. frac.",
|
|
156
|
+
),
|
|
157
|
+
dict(
|
|
158
|
+
name="chunksize",
|
|
159
|
+
type="int",
|
|
160
|
+
limits=[-1, np.inf],
|
|
161
|
+
step=100,
|
|
162
|
+
value=0,
|
|
163
|
+
title="Chunk size",
|
|
164
|
+
),
|
|
165
|
+
dict(
|
|
166
|
+
name="decimate_factor",
|
|
167
|
+
type="int",
|
|
168
|
+
limits=[0, np.inf],
|
|
169
|
+
step=1,
|
|
170
|
+
value=0,
|
|
171
|
+
title="Decimation factor",
|
|
172
|
+
),
|
|
173
|
+
dict(
|
|
174
|
+
name="filter_order",
|
|
175
|
+
type="int",
|
|
176
|
+
limits=[1, np.inf],
|
|
177
|
+
step=1,
|
|
178
|
+
value=10,
|
|
179
|
+
title="Filter order",
|
|
180
|
+
),
|
|
181
|
+
dict(
|
|
182
|
+
name="filter_fc",
|
|
183
|
+
type="float",
|
|
184
|
+
limits=[0, np.inf],
|
|
185
|
+
step=10e6,
|
|
186
|
+
value=300e6,
|
|
187
|
+
suffix="Hz",
|
|
188
|
+
siPrefix=True,
|
|
189
|
+
title="Filter cut-off frequency",
|
|
190
|
+
),
|
|
191
|
+
dict(name="find_f0", type="action", title="Find f0"),
|
|
192
|
+
dict(name="demodulate", type="action", title="Demodulate"),
|
|
193
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pymagnetos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Applications for high magnetic field analysis
|
|
5
|
+
Author: Guillaume Le Goc
|
|
6
|
+
Author-email: Guillaume Le Goc <guillaume.le-goc@lncmi.cnrs.fr>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Requires-Dist: fftekwfm>=0.3.0.1
|
|
11
|
+
Requires-Dist: matplotlib>=3.10.8
|
|
12
|
+
Requires-Dist: nexusformat>=2.0.0
|
|
13
|
+
Requires-Dist: numpy>=2.4.1
|
|
14
|
+
Requires-Dist: pydantic>=2.12.5
|
|
15
|
+
Requires-Dist: pyqt6>=6.10.2
|
|
16
|
+
Requires-Dist: pyqtgraph>=0.14.0
|
|
17
|
+
Requires-Dist: rich>=14.3.1
|
|
18
|
+
Requires-Dist: scipy>=1.17.0
|
|
19
|
+
Requires-Dist: tomlkit>=0.14.0
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Project-URL: Source Code, https://gitlab.in2p3.fr/himagnetos/pymagnetos
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|