qplotter 1.6.0__py3-none-any.whl

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