qplotter 1.6.0b3__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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +63 -0
- qplot/_metadata.py +12 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +573 -0
- qplot/configuration/config_schema.json +319 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +579 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2330 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2561 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +127 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +868 -0
- qplot/datahandling/trusted_heatmap.py +241 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1894 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1733 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +114 -0
- qplot/tools/general.py +134 -0
- qplot/tools/heatmap_geometry.py +381 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +625 -0
- qplot/tools/sample_statistics.py +56 -0
- qplot/tools/worker.py +2787 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4904 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_dragdrop.py +71 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +169 -0
- qplot/windows/_native_averaging.py +173 -0
- qplot/windows/_native_transforms.py +389 -0
- qplot/windows/_plot1d_snap.py +608 -0
- qplot/windows/_plot1d_traces.py +2183 -0
- qplot/windows/_plot2d_colorbar.py +1126 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2197 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +1993 -0
- qplot/windows/_plot_actions.py +2695 -0
- qplot/windows/_plot_appearance.py +276 -0
- qplot/windows/_plot_axis_scaling.py +1485 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +110 -0
- qplot/windows/_plot_marquee.py +656 -0
- qplot/windows/_plot_refresh.py +942 -0
- qplot/windows/_plot_state.py +239 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +635 -0
- qplot/windows/_refresh_interval.py +70 -0
- qplot/windows/_run_controls.py +828 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +531 -0
- qplot/windows/_subplots/subplot2d.py +918 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +413 -0
- qplot/windows/main.py +1635 -0
- qplot/windows/plot1d.py +311 -0
- qplot/windows/plot2d.py +1901 -0
- qplotter-1.6.0b3.dist-info/METADATA +318 -0
- qplotter-1.6.0b3.dist-info/RECORD +119 -0
- qplotter-1.6.0b3.dist-info/WHEEL +5 -0
- qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from PyQt6 import QtCore
|
|
8
|
+
from PyQt6 import QtWidgets as qtw
|
|
9
|
+
|
|
10
|
+
from qplot.windows._dataset_handle import DatasetKey, TraceKey
|
|
11
|
+
from qplot.windows._native_transforms import NativePlotDataItem
|
|
12
|
+
from qplot.windows._plotWin import plotWidget
|
|
13
|
+
from qplot.windows._widgets import (
|
|
14
|
+
expandingComboBox,
|
|
15
|
+
picker_1d,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextmanager
|
|
20
|
+
def _blocked_signals(widget: Any) -> Iterator[None]:
|
|
21
|
+
"""Block one widget only for the duration of a synchronous update."""
|
|
22
|
+
|
|
23
|
+
signals_were_blocked = widget.blockSignals(True)
|
|
24
|
+
try:
|
|
25
|
+
yield
|
|
26
|
+
finally:
|
|
27
|
+
widget.blockSignals(bool(signals_were_blocked))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class _OverlayState:
|
|
32
|
+
visible: bool
|
|
33
|
+
title: str
|
|
34
|
+
detail: str
|
|
35
|
+
detail_visible: bool
|
|
36
|
+
stylesheet: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class _CutAxisState:
|
|
41
|
+
"""One coherent, committed cut-axis UI and display snapshot."""
|
|
42
|
+
|
|
43
|
+
sweep_indep: str
|
|
44
|
+
fixed_indep: str
|
|
45
|
+
axis_selection: dict[str, str]
|
|
46
|
+
slider_range: tuple[int, int]
|
|
47
|
+
slider_index: int
|
|
48
|
+
slider_enabled: bool
|
|
49
|
+
text: str
|
|
50
|
+
fixed_index: int
|
|
51
|
+
fixed_value: float
|
|
52
|
+
fixed_data: np.ndarray
|
|
53
|
+
axis_data: dict[str, np.ndarray]
|
|
54
|
+
axis_param: dict[str, Any]
|
|
55
|
+
data_grid: np.ndarray
|
|
56
|
+
display_param: Any
|
|
57
|
+
overlay: _OverlayState | None
|
|
58
|
+
refresh_pending: bool
|
|
59
|
+
refresh_pending_force: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class _AxisChangeTransaction:
|
|
64
|
+
"""A requested axis pair plus the coherent state it supersedes."""
|
|
65
|
+
|
|
66
|
+
identifier: int
|
|
67
|
+
prior: _CutAxisState
|
|
68
|
+
requested_sweep: str
|
|
69
|
+
requested_fixed: str
|
|
70
|
+
requested_index: int = 0
|
|
71
|
+
worker: Any | None = None
|
|
72
|
+
owns_pending_refresh: bool = False
|
|
73
|
+
finalized: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class sweeper(plotWidget):
|
|
77
|
+
"""
|
|
78
|
+
A plotWidget which displays a 1d sweep on an 2d plot.
|
|
79
|
+
Produced throught he context menu of the plotItem in plot2d.
|
|
80
|
+
|
|
81
|
+
Produces a cursor on its source plot2d to display the location of the sweep.
|
|
82
|
+
Both plots become linked, any changes to the cursor or sweep will update
|
|
83
|
+
their counterpart.
|
|
84
|
+
"""
|
|
85
|
+
operation_kind = "sweeper"
|
|
86
|
+
sweep_moved = QtCore.pyqtSignal([int, str, str, float, object])
|
|
87
|
+
merge_compatibility_changed = QtCore.pyqtSignal()
|
|
88
|
+
remove_sweep = QtCore.pyqtSignal([int])
|
|
89
|
+
|
|
90
|
+
def __init__(self,
|
|
91
|
+
dataset_key: DatasetKey, # Had to handle separately to *args
|
|
92
|
+
sweep_id : int,
|
|
93
|
+
sweep_indep : str,
|
|
94
|
+
fixed_indep : str,
|
|
95
|
+
fixed_value : float,
|
|
96
|
+
*args,
|
|
97
|
+
**kargs
|
|
98
|
+
):
|
|
99
|
+
self.sweep_id = sweep_id
|
|
100
|
+
self.sweep_indep = sweep_indep
|
|
101
|
+
self.fixed_indep = fixed_indep
|
|
102
|
+
self.fixed_value = float(fixed_value)
|
|
103
|
+
self.fixed_index = 0
|
|
104
|
+
self._axis_change_serial = 0
|
|
105
|
+
self._axis_change_transaction: _AxisChangeTransaction | None = None
|
|
106
|
+
|
|
107
|
+
self.line: Any = None
|
|
108
|
+
|
|
109
|
+
super().__init__(dataset_key, *args, **kargs)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _set_cut_trace_identity(self) -> None:
|
|
113
|
+
"""Give this cut a unique internal key and a distinguishable label."""
|
|
114
|
+
|
|
115
|
+
self.label = f"{self.label} [cut {self.sweep_id + 1}]"
|
|
116
|
+
self._trace_key = TraceKey(
|
|
117
|
+
self._dataset_key,
|
|
118
|
+
self.param.name,
|
|
119
|
+
sweep_id=self.sweep_id,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def initAxes(self):
|
|
124
|
+
"""
|
|
125
|
+
Adds to left toolbar to allow for sweep parameter control
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
self._set_cut_trace_identity()
|
|
129
|
+
|
|
130
|
+
# Got back to default before line picker
|
|
131
|
+
super().initAxes()
|
|
132
|
+
|
|
133
|
+
# Set correct display on axis picker
|
|
134
|
+
with _blocked_signals(self.axis_dropdown["x"]):
|
|
135
|
+
self.axis_dropdown["x"].setCurrentIndex(
|
|
136
|
+
self.axis_dropdown["x"].findText(self.sweep_indep)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Disable y axis box, for display only
|
|
140
|
+
with _blocked_signals(self.axis_dropdown["y"]):
|
|
141
|
+
self.axis_dropdown["y"].setEditable(True)
|
|
142
|
+
y_axis_line_edit = self.axis_dropdown["y"].lineEdit()
|
|
143
|
+
if y_axis_line_edit is not None:
|
|
144
|
+
y_axis_line_edit.setReadOnly(True)
|
|
145
|
+
self.axis_dropdown["y"].setDisabled(True)
|
|
146
|
+
self.axis_dropdown["y"].setCurrentText(self.param.name)
|
|
147
|
+
|
|
148
|
+
# Add cut-axis controls to the Data axes dock.
|
|
149
|
+
main_line = picker_1d(self, self.config, [self.label])
|
|
150
|
+
main_line.option_box.setCurrentIndex(0)
|
|
151
|
+
main_line.option_box.setDisabled(True)
|
|
152
|
+
main_line.del_box.setDisabled(True)
|
|
153
|
+
main_line.axis_side.setDisabled(True)
|
|
154
|
+
main_line.color_box.setColor(self.config.theme.colors[0])
|
|
155
|
+
main_line.color_box.selectedColor.connect(
|
|
156
|
+
lambda col: self.line.setPen(col)
|
|
157
|
+
)
|
|
158
|
+
main_line.color_box.selectedColor.connect( # emit update to main
|
|
159
|
+
lambda _: self.update_sweep()
|
|
160
|
+
)
|
|
161
|
+
self.axes_dock.addWidget(main_line)
|
|
162
|
+
main_line.adjustSize()
|
|
163
|
+
|
|
164
|
+
# Add picker for changing sweep location using x axis options since
|
|
165
|
+
# y param is not in that
|
|
166
|
+
self.picker = fixed_var_picker(
|
|
167
|
+
self,
|
|
168
|
+
[self.axis_dropdown["x"].itemText(i) for i in range(self.axis_dropdown["x"].count())],
|
|
169
|
+
)
|
|
170
|
+
self.axes_dock.addWidget(self.picker)
|
|
171
|
+
|
|
172
|
+
# Set up picker options
|
|
173
|
+
self.picker.option_box.setCurrentIndex(
|
|
174
|
+
self.picker.option_box.findText(self.fixed_indep)
|
|
175
|
+
)
|
|
176
|
+
self._axis_selection = {
|
|
177
|
+
"x": self.sweep_indep,
|
|
178
|
+
"y": self.fixed_indep,
|
|
179
|
+
}
|
|
180
|
+
self.picker.option_box.currentIndexChanged.connect(self.change_fixed_param)
|
|
181
|
+
self.picker.slider.valueChanged.connect(self.change_index)
|
|
182
|
+
self.picker.slider.setEnabled(False)
|
|
183
|
+
|
|
184
|
+
# Push all widgets to top
|
|
185
|
+
self.axes_dock.content_layout.addStretch()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def initFrame(self):
|
|
189
|
+
"""
|
|
190
|
+
Sets up the initial plot and starting data.
|
|
191
|
+
|
|
192
|
+
Note, is copy of plot1d.initFrame
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
self.line = NativePlotDataItem(connect="all")
|
|
196
|
+
self.plot.addItem(self.line)
|
|
197
|
+
self._install_axis_scale_trace_handler(self.line)
|
|
198
|
+
|
|
199
|
+
# Wait for loader to finish to enure needed data is collected.
|
|
200
|
+
self.load_data()
|
|
201
|
+
self.show_status("Cut plot ready; loading data...", 5000)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _capture_overlay_state(self) -> _OverlayState | None:
|
|
205
|
+
overlay = self.__dict__.get("plot_state_overlay")
|
|
206
|
+
if overlay is None:
|
|
207
|
+
return None
|
|
208
|
+
return _OverlayState(
|
|
209
|
+
visible=not overlay.frame.isHidden(),
|
|
210
|
+
title=overlay.title_label.text(),
|
|
211
|
+
detail=overlay.detail_label.text(),
|
|
212
|
+
detail_visible=not overlay.detail_label.isHidden(),
|
|
213
|
+
stylesheet=overlay.frame.styleSheet(),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _capture_axis_state(self) -> _CutAxisState:
|
|
218
|
+
slider = self.picker.slider
|
|
219
|
+
axis_data = {
|
|
220
|
+
key: np.array(value, copy=True)
|
|
221
|
+
for key, value in self.__dict__.get("axis_data", {}).items()
|
|
222
|
+
}
|
|
223
|
+
return _CutAxisState(
|
|
224
|
+
sweep_indep=self.sweep_indep,
|
|
225
|
+
fixed_indep=self.fixed_indep,
|
|
226
|
+
axis_selection=dict(
|
|
227
|
+
self.__dict__.get(
|
|
228
|
+
"_axis_selection",
|
|
229
|
+
{"x": self.sweep_indep, "y": self.fixed_indep},
|
|
230
|
+
)
|
|
231
|
+
),
|
|
232
|
+
slider_range=(slider.minimum(), slider.maximum()),
|
|
233
|
+
slider_index=slider.value(),
|
|
234
|
+
slider_enabled=slider.isEnabled(),
|
|
235
|
+
text=self.picker.text_box.text(),
|
|
236
|
+
fixed_index=self.fixed_index,
|
|
237
|
+
fixed_value=float(self.fixed_value),
|
|
238
|
+
fixed_data=np.array(
|
|
239
|
+
self.__dict__.get("fixed_indep_data", np.asarray([])),
|
|
240
|
+
copy=True,
|
|
241
|
+
),
|
|
242
|
+
axis_data=axis_data,
|
|
243
|
+
axis_param=dict(self.__dict__.get("axis_param", {})),
|
|
244
|
+
data_grid=np.array(
|
|
245
|
+
self.__dict__.get("dataGrid", np.empty((0, 0))),
|
|
246
|
+
copy=True,
|
|
247
|
+
),
|
|
248
|
+
display_param=self.__dict__.get("display_param"),
|
|
249
|
+
overlay=self._capture_overlay_state(),
|
|
250
|
+
refresh_pending=bool(self.__dict__.get("_refresh_pending", False)),
|
|
251
|
+
refresh_pending_force=bool(
|
|
252
|
+
self.__dict__.get("_refresh_pending_force", False)
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _set_combo_text(combo: Any, text: str) -> None:
|
|
259
|
+
with _blocked_signals(combo):
|
|
260
|
+
index = combo.findText(text)
|
|
261
|
+
if index >= 0:
|
|
262
|
+
combo.setCurrentIndex(index)
|
|
263
|
+
else:
|
|
264
|
+
combo.setCurrentText(text)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _set_axis_controls(self, sweep_indep: str, fixed_indep: str) -> None:
|
|
268
|
+
self._set_combo_text(self.axis_dropdown["x"], sweep_indep)
|
|
269
|
+
self._set_combo_text(self.picker.option_box, fixed_indep)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _set_slider_state(
|
|
273
|
+
self,
|
|
274
|
+
slider_range: tuple[int, int],
|
|
275
|
+
index: int,
|
|
276
|
+
enabled: bool,
|
|
277
|
+
) -> None:
|
|
278
|
+
slider = self.picker.slider
|
|
279
|
+
with _blocked_signals(slider):
|
|
280
|
+
slider.setRange(*slider_range)
|
|
281
|
+
set_value = getattr(slider, "setValue", None)
|
|
282
|
+
if callable(set_value):
|
|
283
|
+
set_value(index)
|
|
284
|
+
slider.setEnabled(enabled)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _restore_overlay_state(self, state: _OverlayState | None) -> None:
|
|
288
|
+
if state is None:
|
|
289
|
+
return
|
|
290
|
+
overlay = self.__dict__.get("plot_state_overlay")
|
|
291
|
+
if overlay is None:
|
|
292
|
+
return
|
|
293
|
+
overlay.title_label.setText(state.title)
|
|
294
|
+
overlay.detail_label.setText(state.detail)
|
|
295
|
+
overlay.detail_label.setVisible(state.detail_visible)
|
|
296
|
+
overlay.frame.setStyleSheet(state.stylesheet)
|
|
297
|
+
if state.visible:
|
|
298
|
+
overlay.frame.show()
|
|
299
|
+
overlay.frame.raise_()
|
|
300
|
+
else:
|
|
301
|
+
overlay.frame.hide()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _restore_axis_state(
|
|
305
|
+
self,
|
|
306
|
+
transaction: _AxisChangeTransaction,
|
|
307
|
+
*,
|
|
308
|
+
preserve_overlay: bool,
|
|
309
|
+
) -> None:
|
|
310
|
+
state = transaction.prior
|
|
311
|
+
self._set_axis_controls(state.sweep_indep, state.fixed_indep)
|
|
312
|
+
self.sweep_indep = state.sweep_indep
|
|
313
|
+
self.fixed_indep = state.fixed_indep
|
|
314
|
+
self._axis_selection = dict(state.axis_selection)
|
|
315
|
+
self.fixed_index = state.fixed_index
|
|
316
|
+
self.fixed_value = state.fixed_value
|
|
317
|
+
self.fixed_indep_data = np.array(state.fixed_data, copy=True)
|
|
318
|
+
self.axis_data = {
|
|
319
|
+
key: np.array(value, copy=True)
|
|
320
|
+
for key, value in state.axis_data.items()
|
|
321
|
+
}
|
|
322
|
+
self.axis_param = dict(state.axis_param)
|
|
323
|
+
self.dataGrid = np.array(state.data_grid, copy=True)
|
|
324
|
+
self.display_param = state.display_param
|
|
325
|
+
self._set_slider_state(
|
|
326
|
+
state.slider_range,
|
|
327
|
+
state.slider_index,
|
|
328
|
+
state.slider_enabled,
|
|
329
|
+
)
|
|
330
|
+
self.picker.text_box.setText(state.text)
|
|
331
|
+
|
|
332
|
+
line = self.__dict__.get("line")
|
|
333
|
+
if line is not None:
|
|
334
|
+
line.setData(
|
|
335
|
+
x=self.axis_data.get("x", np.asarray([])),
|
|
336
|
+
y=self.axis_data.get("y", np.asarray([])),
|
|
337
|
+
)
|
|
338
|
+
set_labels = getattr(self, "_set_param_axis_labels", None)
|
|
339
|
+
if callable(set_labels) and {"x", "y"}.issubset(self.axis_param):
|
|
340
|
+
set_labels()
|
|
341
|
+
if not preserve_overlay:
|
|
342
|
+
self._restore_overlay_state(state.overlay)
|
|
343
|
+
|
|
344
|
+
if transaction.owns_pending_refresh and transaction.worker is None:
|
|
345
|
+
self._refresh_pending = state.refresh_pending
|
|
346
|
+
self._refresh_pending_force = state.refresh_pending_force
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _commit_axis_controls(self, transaction: _AxisChangeTransaction) -> None:
|
|
350
|
+
self._set_axis_controls(
|
|
351
|
+
transaction.requested_sweep,
|
|
352
|
+
transaction.requested_fixed,
|
|
353
|
+
)
|
|
354
|
+
self.sweep_indep = transaction.requested_sweep
|
|
355
|
+
self.fixed_indep = transaction.requested_fixed
|
|
356
|
+
self._axis_selection = {
|
|
357
|
+
"x": transaction.requested_sweep,
|
|
358
|
+
"y": transaction.requested_fixed,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _commit_loaded_cut(self, transaction: _AxisChangeTransaction) -> None:
|
|
363
|
+
x_data = np.asarray(self.axis_data.get("x", []))
|
|
364
|
+
fixed_data = np.asarray(self.axis_data.get("y", []))
|
|
365
|
+
data_grid = np.asarray(self.dataGrid)
|
|
366
|
+
row_count = min(fixed_data.size, data_grid.shape[0])
|
|
367
|
+
column_count = min(x_data.size, data_grid.shape[1])
|
|
368
|
+
|
|
369
|
+
self._commit_axis_controls(transaction)
|
|
370
|
+
self.fixed_indep_data = fixed_data[:row_count]
|
|
371
|
+
self.axis_data["x"] = x_data[:column_count]
|
|
372
|
+
self.dataGrid = data_grid[:row_count, :column_count]
|
|
373
|
+
self.fixed_index = min(transaction.requested_index, row_count - 1)
|
|
374
|
+
self.fixed_value = float(self.fixed_indep_data[self.fixed_index])
|
|
375
|
+
self.axis_param["y"] = getattr(self, "display_param", self.param)
|
|
376
|
+
self._set_slider_state((0, row_count - 1), self.fixed_index, True)
|
|
377
|
+
self.picker.text_box.setText(
|
|
378
|
+
self.formatNum(self.fixed_indep_data[self.fixed_index])
|
|
379
|
+
)
|
|
380
|
+
self._set_param_axis_labels()
|
|
381
|
+
self.update_sweep()
|
|
382
|
+
self.merge_compatibility_changed.emit()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _commit_empty_cut(self, transaction: _AxisChangeTransaction) -> None:
|
|
386
|
+
self._commit_axis_controls(transaction)
|
|
387
|
+
self.fixed_indep_data = np.asarray([])
|
|
388
|
+
self.axis_data = {"x": np.asarray([]), "y": np.asarray([])}
|
|
389
|
+
self.dataGrid = np.empty((0, 0))
|
|
390
|
+
self.fixed_index = 0
|
|
391
|
+
self.fixed_value = 0.0
|
|
392
|
+
if "y" in self.axis_param:
|
|
393
|
+
self.axis_param["y"] = getattr(self, "display_param", self.param)
|
|
394
|
+
self._set_slider_state((0, 0), 0, False)
|
|
395
|
+
self.picker.text_box.setText("")
|
|
396
|
+
self.line.setData([], [])
|
|
397
|
+
if {"x", "y"}.issubset(self.axis_param):
|
|
398
|
+
self._set_param_axis_labels()
|
|
399
|
+
self.trace_updated.emit()
|
|
400
|
+
self.merge_compatibility_changed.emit()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _finalize_axis_change(
|
|
404
|
+
self,
|
|
405
|
+
transaction: _AxisChangeTransaction,
|
|
406
|
+
outcome: str,
|
|
407
|
+
*,
|
|
408
|
+
preserve_overlay: bool = False,
|
|
409
|
+
) -> bool:
|
|
410
|
+
"""Commit or roll back the current transaction exactly once."""
|
|
411
|
+
|
|
412
|
+
if transaction.finalized:
|
|
413
|
+
return False
|
|
414
|
+
if self.__dict__.get("_axis_change_transaction") is not transaction:
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
transaction.finalized = True
|
|
418
|
+
self._axis_change_transaction = None
|
|
419
|
+
try:
|
|
420
|
+
if outcome == "success":
|
|
421
|
+
self._commit_loaded_cut(transaction)
|
|
422
|
+
elif outcome == "empty":
|
|
423
|
+
self._commit_empty_cut(transaction)
|
|
424
|
+
else:
|
|
425
|
+
self._restore_axis_state(
|
|
426
|
+
transaction,
|
|
427
|
+
preserve_overlay=preserve_overlay,
|
|
428
|
+
)
|
|
429
|
+
except Exception:
|
|
430
|
+
if outcome in {"success", "empty"}:
|
|
431
|
+
self._restore_axis_state(transaction, preserve_overlay=False)
|
|
432
|
+
raise
|
|
433
|
+
finally:
|
|
434
|
+
# No transaction is allowed to own a blocked slider after returning.
|
|
435
|
+
self.picker.slider.blockSignals(False)
|
|
436
|
+
return True
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _active_transaction_for_worker(
|
|
440
|
+
self,
|
|
441
|
+
worker: Any,
|
|
442
|
+
) -> _AxisChangeTransaction | None:
|
|
443
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
444
|
+
if transaction is None or transaction.worker is not worker:
|
|
445
|
+
return None
|
|
446
|
+
return transaction
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _refresh_worker_will_start(self, worker: Any) -> None:
|
|
450
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
451
|
+
if transaction is not None and transaction.worker is None:
|
|
452
|
+
transaction.worker = worker
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def load_data(self, *args: Any, **kwargs: Any) -> bool:
|
|
456
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
457
|
+
try:
|
|
458
|
+
launched = super().load_data(*args, **kwargs)
|
|
459
|
+
except Exception:
|
|
460
|
+
if transaction is not None:
|
|
461
|
+
self._finalize_axis_change(transaction, "failure")
|
|
462
|
+
raise
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
not launched
|
|
466
|
+
and transaction is not None
|
|
467
|
+
and self.__dict__.get("_axis_change_transaction") is transaction
|
|
468
|
+
):
|
|
469
|
+
self._finalize_axis_change(
|
|
470
|
+
transaction,
|
|
471
|
+
"failure",
|
|
472
|
+
preserve_overlay=True,
|
|
473
|
+
)
|
|
474
|
+
return launched
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _source_database_is_current(self) -> bool:
|
|
478
|
+
is_current = super()._source_database_is_current()
|
|
479
|
+
if not is_current:
|
|
480
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
481
|
+
if transaction is not None:
|
|
482
|
+
self._finalize_axis_change(
|
|
483
|
+
transaction,
|
|
484
|
+
"failure",
|
|
485
|
+
preserve_overlay=True,
|
|
486
|
+
)
|
|
487
|
+
return is_current
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def err_raiser(self, err: Exception, worker: Any | None = None) -> None:
|
|
491
|
+
transaction = self._active_transaction_for_worker(worker)
|
|
492
|
+
cancelled = bool(
|
|
493
|
+
worker is not None
|
|
494
|
+
and getattr(worker, "is_cancelled", lambda: False)()
|
|
495
|
+
)
|
|
496
|
+
try:
|
|
497
|
+
super().err_raiser(err, worker=worker)
|
|
498
|
+
finally:
|
|
499
|
+
if transaction is not None:
|
|
500
|
+
self._finalize_axis_change(
|
|
501
|
+
transaction,
|
|
502
|
+
"failure",
|
|
503
|
+
preserve_overlay=not cancelled,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@QtCore.pyqtSlot(bool)
|
|
508
|
+
def refreshPlot(self, finished : bool = True, worker=None):
|
|
509
|
+
"""
|
|
510
|
+
Event handler for worker callback
|
|
511
|
+
Fetches values from data from worker in super().refreshPlot
|
|
512
|
+
|
|
513
|
+
Updates display (see self.update_sweep) and slider as needed
|
|
514
|
+
|
|
515
|
+
Parameters
|
|
516
|
+
----------
|
|
517
|
+
finished : bool
|
|
518
|
+
In the event the worker had to abort, finished is False and refresh
|
|
519
|
+
is not ran.
|
|
520
|
+
|
|
521
|
+
"""
|
|
522
|
+
plot_worker = worker if worker is not None else self.worker
|
|
523
|
+
transaction = self._active_transaction_for_worker(plot_worker)
|
|
524
|
+
try:
|
|
525
|
+
refreshed = super().refreshPlot(finished, worker=worker)
|
|
526
|
+
except Exception:
|
|
527
|
+
if transaction is not None:
|
|
528
|
+
self._finalize_axis_change(transaction, "failure")
|
|
529
|
+
raise
|
|
530
|
+
|
|
531
|
+
if not refreshed:
|
|
532
|
+
if transaction is not None:
|
|
533
|
+
cancelled = bool(
|
|
534
|
+
getattr(plot_worker, "is_cancelled", lambda: False)()
|
|
535
|
+
)
|
|
536
|
+
self._finalize_axis_change(
|
|
537
|
+
transaction,
|
|
538
|
+
"failure",
|
|
539
|
+
preserve_overlay=bool(not cancelled and not finished),
|
|
540
|
+
)
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
x_data = np.asarray(self.axis_data.get("x", []))
|
|
545
|
+
fixed_data = np.asarray(self.axis_data.get("y", []))
|
|
546
|
+
data_grid = np.asarray(self.dataGrid)
|
|
547
|
+
has_cut_data = (
|
|
548
|
+
x_data.size > 0
|
|
549
|
+
and fixed_data.size > 0
|
|
550
|
+
and data_grid.ndim == 2
|
|
551
|
+
and data_grid.shape[0] > 0
|
|
552
|
+
and data_grid.shape[1] > 0
|
|
553
|
+
)
|
|
554
|
+
if transaction is not None:
|
|
555
|
+
outcome = "success" if has_cut_data else "empty"
|
|
556
|
+
self._finalize_axis_change(transaction, outcome)
|
|
557
|
+
elif not has_cut_data:
|
|
558
|
+
self.fixed_indep_data = np.asarray([])
|
|
559
|
+
self.axis_data = {"x": np.asarray([]), "y": np.asarray([])}
|
|
560
|
+
self.dataGrid = np.empty((0, 0))
|
|
561
|
+
self.fixed_index = 0
|
|
562
|
+
self._set_slider_state((0, 0), 0, False)
|
|
563
|
+
self.picker.text_box.setText("")
|
|
564
|
+
self.line.setData([], [])
|
|
565
|
+
self.trace_updated.emit()
|
|
566
|
+
else:
|
|
567
|
+
row_count = min(fixed_data.size, data_grid.shape[0])
|
|
568
|
+
column_count = min(x_data.size, data_grid.shape[1])
|
|
569
|
+
self.fixed_indep_data = fixed_data[:row_count]
|
|
570
|
+
self.axis_data["x"] = x_data[:column_count]
|
|
571
|
+
self.dataGrid = data_grid[:row_count, :column_count]
|
|
572
|
+
self.fixed_index = self._fixed_index_for_value(self.fixed_value)
|
|
573
|
+
self.fixed_value = float(self.fixed_indep_data[self.fixed_index])
|
|
574
|
+
self.axis_param["y"] = getattr(self, "display_param", self.param)
|
|
575
|
+
self._set_slider_state(
|
|
576
|
+
(0, row_count - 1),
|
|
577
|
+
self.fixed_index,
|
|
578
|
+
True,
|
|
579
|
+
)
|
|
580
|
+
self.picker.text_box.setText(
|
|
581
|
+
self.formatNum(self.fixed_indep_data[self.fixed_index])
|
|
582
|
+
)
|
|
583
|
+
self._set_param_axis_labels()
|
|
584
|
+
self.update_sweep()
|
|
585
|
+
|
|
586
|
+
if not has_cut_data:
|
|
587
|
+
self.show_status(
|
|
588
|
+
f"Waiting for plottable data for {self.param.name}...",
|
|
589
|
+
5000,
|
|
590
|
+
)
|
|
591
|
+
self.show_plot_state(
|
|
592
|
+
"Waiting for plottable cut data",
|
|
593
|
+
f"{self.param.name} has no cut data yet.",
|
|
594
|
+
kind="empty",
|
|
595
|
+
)
|
|
596
|
+
self._mark_display_synchronized(plot_worker)
|
|
597
|
+
except Exception:
|
|
598
|
+
active = self._active_transaction_for_worker(plot_worker)
|
|
599
|
+
if active is not None:
|
|
600
|
+
self._finalize_axis_change(active, "failure")
|
|
601
|
+
raise
|
|
602
|
+
finally:
|
|
603
|
+
overlay = self.__dict__.get("plot_state_overlay")
|
|
604
|
+
if overlay is not None:
|
|
605
|
+
overlay.finish(plot_worker)
|
|
606
|
+
plot_worker.running = False
|
|
607
|
+
self.picker.slider.blockSignals(False)
|
|
608
|
+
self._ensure_refresh_monitor()
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def axis_options(self) -> dict:
|
|
613
|
+
"""
|
|
614
|
+
Alter axis_options for correct data fetch from worker
|
|
615
|
+
|
|
616
|
+
Returns
|
|
617
|
+
-------
|
|
618
|
+
dict
|
|
619
|
+
The required axes data.
|
|
620
|
+
|
|
621
|
+
"""
|
|
622
|
+
return {"x": self.axis_dropdown["x"].currentText(), "y": self.picker.option_box.currentText()}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@QtCore.pyqtSlot(bool)
|
|
626
|
+
def closeEvent(self, event):
|
|
627
|
+
"""
|
|
628
|
+
Post close admin, emits to 2d main plot to remove sweep cursor
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
unused byt reauired by slot
|
|
633
|
+
|
|
634
|
+
"""
|
|
635
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
636
|
+
if transaction is not None:
|
|
637
|
+
self._finalize_axis_change(transaction, "failure")
|
|
638
|
+
try:
|
|
639
|
+
super().closeEvent(event)
|
|
640
|
+
self.remove_sweep.emit(self.sweep_id)
|
|
641
|
+
finally:
|
|
642
|
+
self.picker.slider.blockSignals(False)
|
|
643
|
+
|
|
644
|
+
###############################################################################
|
|
645
|
+
# Events/Slots
|
|
646
|
+
|
|
647
|
+
def update_sweep(self, emit = True):
|
|
648
|
+
"""
|
|
649
|
+
Refresh 1d plot when there is a change in parameter or value
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
emit : bool, optional
|
|
654
|
+
Whether to emit a signal to parent 2d plot. The default is True.
|
|
655
|
+
|
|
656
|
+
"""
|
|
657
|
+
# Get correct row for y data
|
|
658
|
+
fixed_values = np.asarray(getattr(self, "fixed_indep_data", []))
|
|
659
|
+
if fixed_values.size > self.fixed_index:
|
|
660
|
+
self.fixed_value = float(fixed_values[self.fixed_index])
|
|
661
|
+
elif not hasattr(self, "fixed_value"):
|
|
662
|
+
self.fixed_value = float(self.fixed_index)
|
|
663
|
+
self.axis_data["y"] = self.dataGrid[self.fixed_index, :]
|
|
664
|
+
|
|
665
|
+
# update line
|
|
666
|
+
self.line.setData(
|
|
667
|
+
x=self.axis_data["x"],
|
|
668
|
+
y=self.axis_data["y"],
|
|
669
|
+
)
|
|
670
|
+
self.trace_updated.emit()
|
|
671
|
+
|
|
672
|
+
if emit:
|
|
673
|
+
# Tell source graph to update scan line on source graph
|
|
674
|
+
self.sweep_moved.emit(
|
|
675
|
+
self.sweep_id,
|
|
676
|
+
*self.axis_options.values(),
|
|
677
|
+
self.fixed_value,
|
|
678
|
+
self.line.opts['pen']
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@QtCore.pyqtSlot(int)
|
|
683
|
+
def change_index(self, index):
|
|
684
|
+
"""
|
|
685
|
+
Event handler for picker.slider changing value
|
|
686
|
+
Changes index value of the fixed parameter and refreshes plot.
|
|
687
|
+
|
|
688
|
+
Parameters
|
|
689
|
+
----------
|
|
690
|
+
index : int
|
|
691
|
+
Value slider was changed to.
|
|
692
|
+
|
|
693
|
+
"""
|
|
694
|
+
# Update display box
|
|
695
|
+
self.picker.text_box.setText(
|
|
696
|
+
self.formatNum(self.fixed_indep_data[index])
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Update plot
|
|
700
|
+
self.fixed_index = index
|
|
701
|
+
self.fixed_value = float(self.fixed_indep_data[index])
|
|
702
|
+
self.update_sweep()
|
|
703
|
+
|
|
704
|
+
self._set_param_axis_labels()
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _cancel_current_refresh(self) -> None:
|
|
708
|
+
worker = self.__dict__.get("worker")
|
|
709
|
+
if worker is None or not getattr(worker, "running", False):
|
|
710
|
+
return
|
|
711
|
+
if getattr(worker, "is_cancelled", lambda: False)():
|
|
712
|
+
return
|
|
713
|
+
cancel = getattr(worker, "cancel", None)
|
|
714
|
+
if callable(cancel):
|
|
715
|
+
cancel()
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _supersede_axis_change(self) -> None:
|
|
719
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
720
|
+
if transaction is None:
|
|
721
|
+
return
|
|
722
|
+
worker = transaction.worker
|
|
723
|
+
self._finalize_axis_change(transaction, "failure")
|
|
724
|
+
if worker is not None and getattr(worker, "running", False):
|
|
725
|
+
cancel = getattr(worker, "cancel", None)
|
|
726
|
+
if callable(cancel):
|
|
727
|
+
cancel()
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _begin_axis_change(
|
|
731
|
+
self,
|
|
732
|
+
requested_sweep: str,
|
|
733
|
+
requested_fixed: str,
|
|
734
|
+
) -> None:
|
|
735
|
+
self._supersede_axis_change()
|
|
736
|
+
try:
|
|
737
|
+
prior = self._capture_axis_state()
|
|
738
|
+
except Exception:
|
|
739
|
+
self._set_axis_controls(self.sweep_indep, self.fixed_indep)
|
|
740
|
+
self.picker.slider.blockSignals(False)
|
|
741
|
+
raise
|
|
742
|
+
self._axis_change_serial += 1
|
|
743
|
+
transaction = _AxisChangeTransaction(
|
|
744
|
+
identifier=self._axis_change_serial,
|
|
745
|
+
prior=prior,
|
|
746
|
+
requested_sweep=requested_sweep,
|
|
747
|
+
requested_fixed=requested_fixed,
|
|
748
|
+
)
|
|
749
|
+
self._axis_change_transaction = transaction
|
|
750
|
+
try:
|
|
751
|
+
self._set_axis_controls(requested_sweep, requested_fixed)
|
|
752
|
+
with _blocked_signals(self.picker.slider):
|
|
753
|
+
self.picker.slider.setEnabled(False)
|
|
754
|
+
self.picker.slider.blockSignals(False)
|
|
755
|
+
self.show_plot_state(
|
|
756
|
+
"Updating cut axes",
|
|
757
|
+
f"Loading {requested_sweep} at fixed {requested_fixed}.",
|
|
758
|
+
kind="loading",
|
|
759
|
+
)
|
|
760
|
+
self._cancel_current_refresh()
|
|
761
|
+
self.refreshWindow(force=True)
|
|
762
|
+
except Exception:
|
|
763
|
+
self._finalize_axis_change(transaction, "failure")
|
|
764
|
+
raise
|
|
765
|
+
|
|
766
|
+
if self.__dict__.get("_axis_change_transaction") is not transaction:
|
|
767
|
+
return
|
|
768
|
+
if transaction.worker is not None:
|
|
769
|
+
return
|
|
770
|
+
if self.__dict__.get("_refresh_pending", False):
|
|
771
|
+
transaction.owns_pending_refresh = not prior.refresh_pending
|
|
772
|
+
return
|
|
773
|
+
self._finalize_axis_change(transaction, "failure")
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@QtCore.pyqtSlot(int)
|
|
777
|
+
def change_fixed_param(self, index):
|
|
778
|
+
"""
|
|
779
|
+
Event handler for fixed parameter dropdown selector. (picker.option_box)
|
|
780
|
+
Updates the parameter on the x axis of the sweep and resets fixed
|
|
781
|
+
parameter index to 0.
|
|
782
|
+
If the parameter changed to is the current sweep parameter, switches
|
|
783
|
+
them.
|
|
784
|
+
|
|
785
|
+
Parameters
|
|
786
|
+
----------
|
|
787
|
+
Unused but required by slot
|
|
788
|
+
|
|
789
|
+
"""
|
|
790
|
+
del index
|
|
791
|
+
requested_fixed = self.picker.option_box.currentText()
|
|
792
|
+
requested_sweep = self.axis_dropdown["x"].currentText()
|
|
793
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
794
|
+
previous_fixed = (
|
|
795
|
+
transaction.requested_fixed
|
|
796
|
+
if transaction is not None
|
|
797
|
+
else self.fixed_indep
|
|
798
|
+
)
|
|
799
|
+
if requested_fixed == requested_sweep:
|
|
800
|
+
requested_sweep = previous_fixed
|
|
801
|
+
self._begin_axis_change(requested_sweep, requested_fixed)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@QtCore.pyqtSlot()
|
|
805
|
+
def change_axis(self, key : str):
|
|
806
|
+
"""
|
|
807
|
+
Event handler for x axis dropdown selector.
|
|
808
|
+
Updates the parameter on the x axis of the sweep and resets fixed
|
|
809
|
+
parameter index to 0.
|
|
810
|
+
If the parameter changed to is the current fixed parameter, switches
|
|
811
|
+
them.
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
Parameters
|
|
815
|
+
----------
|
|
816
|
+
key : str
|
|
817
|
+
The key of which box to change. Will be x in all cases but required
|
|
818
|
+
by definition in parent
|
|
819
|
+
|
|
820
|
+
"""
|
|
821
|
+
del key
|
|
822
|
+
requested_sweep = self.axis_dropdown["x"].currentText()
|
|
823
|
+
requested_fixed = self.picker.option_box.currentText()
|
|
824
|
+
transaction = self.__dict__.get("_axis_change_transaction")
|
|
825
|
+
previous_sweep = (
|
|
826
|
+
transaction.requested_sweep
|
|
827
|
+
if transaction is not None
|
|
828
|
+
else self.sweep_indep
|
|
829
|
+
)
|
|
830
|
+
if requested_sweep == requested_fixed:
|
|
831
|
+
requested_fixed = previous_sweep
|
|
832
|
+
self._begin_axis_change(requested_sweep, requested_fixed)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
@QtCore.pyqtSlot(int, float)
|
|
836
|
+
def update_sweep_line(self, sweep_id, fixed_value):
|
|
837
|
+
"""
|
|
838
|
+
Event handler for moving sweep cursor on source plot.
|
|
839
|
+
|
|
840
|
+
Parameters
|
|
841
|
+
----------
|
|
842
|
+
sweep_id : int
|
|
843
|
+
The sweep id of the line moved. Confirms that this is the intened
|
|
844
|
+
plot to adjust
|
|
845
|
+
fixed_value : float
|
|
846
|
+
The physical fixed-axis coordinate selected on the source plot.
|
|
847
|
+
|
|
848
|
+
"""
|
|
849
|
+
if sweep_id != self.sweep_id:
|
|
850
|
+
return
|
|
851
|
+
|
|
852
|
+
self.fixed_value = float(fixed_value)
|
|
853
|
+
if not getattr(self, "fixed_indep_data", np.asarray([])).size:
|
|
854
|
+
return
|
|
855
|
+
self.fixed_index = self._fixed_index_for_value(self.fixed_value)
|
|
856
|
+
|
|
857
|
+
with _blocked_signals(self.picker.slider):
|
|
858
|
+
self.picker.slider.setValue(self.fixed_index)
|
|
859
|
+
self.picker.text_box.setText(
|
|
860
|
+
self.formatNum(self.fixed_indep_data[self.fixed_index])
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
self.update_sweep(emit = False)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _fixed_index_for_value(self, value: float) -> int:
|
|
867
|
+
values = np.asarray(self.fixed_indep_data, dtype=float)
|
|
868
|
+
finite = np.flatnonzero(np.isfinite(values))
|
|
869
|
+
if finite.size == 0:
|
|
870
|
+
return 0
|
|
871
|
+
nearest = int(np.argmin(np.abs(values[finite] - float(value))))
|
|
872
|
+
return int(finite[nearest])
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class fixed_var_picker(qtw.QWidget):
|
|
876
|
+
"""
|
|
877
|
+
A custom QWidget which contains other widgets to interact with and control
|
|
878
|
+
the static/fixed parameter of the heat map while viewing the 1d sweep.
|
|
879
|
+
|
|
880
|
+
Contains:
|
|
881
|
+
self.option_box: Changing the fixed parameter
|
|
882
|
+
self.slider: change which value the plot is looking at
|
|
883
|
+
self.text_box: visual display of current sweep value
|
|
884
|
+
|
|
885
|
+
Note, uses custom HBoxLayout to set up context menu within dock widget.
|
|
886
|
+
See qplot.windows._widgets.toolbar
|
|
887
|
+
"""
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def __init__(self, main, items):
|
|
891
|
+
super().__init__()
|
|
892
|
+
|
|
893
|
+
layout = qtw.QVBoxLayout(self)
|
|
894
|
+
|
|
895
|
+
# Set up layouts with customised context menus
|
|
896
|
+
row_1 = main.axes_dock.HBox_context(main.axes_dock.event_filter)
|
|
897
|
+
row_2 = main.axes_dock.HBox_context(main.axes_dock.event_filter)
|
|
898
|
+
|
|
899
|
+
row_1.addWidget(qtw.QLabel("Fixed Varaible: "))
|
|
900
|
+
|
|
901
|
+
# Create box to change paramater
|
|
902
|
+
self.option_box = expandingComboBox()
|
|
903
|
+
self.option_box.addItems(items)
|
|
904
|
+
row_1.addWidget(self.option_box)
|
|
905
|
+
|
|
906
|
+
# Switches fixed parameter
|
|
907
|
+
self.slider = qtw.QSlider(QtCore.Qt.Orientation.Horizontal)
|
|
908
|
+
self.slider.setTickPosition(qtw.QSlider.TickPosition.TicksBelow)
|
|
909
|
+
row_2.addWidget(self.slider)
|
|
910
|
+
|
|
911
|
+
# Update user to change
|
|
912
|
+
self.text_box = qtw.QLineEdit()
|
|
913
|
+
self.text_box.setReadOnly(True)
|
|
914
|
+
self.text_box.setMaximumWidth(main._label_width)
|
|
915
|
+
row_2.addWidget(self.text_box)
|
|
916
|
+
|
|
917
|
+
layout.addLayout(row_1)
|
|
918
|
+
layout.addLayout(row_2)
|