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.
Files changed (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. 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)