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,2572 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from PyQt6 import (
|
|
6
|
+
QtCore,
|
|
7
|
+
QtGui,
|
|
8
|
+
)
|
|
9
|
+
from PyQt6 import (
|
|
10
|
+
QtWidgets as qtw,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from qplot.datahandling.trusted_live_queries import (
|
|
14
|
+
TrustedParameterView,
|
|
15
|
+
TrustedRunRecord,
|
|
16
|
+
TrustedSelectedRunDetail,
|
|
17
|
+
TrustedSetpointSummary,
|
|
18
|
+
)
|
|
19
|
+
from qplot.datahandling.trusted_presentation import (
|
|
20
|
+
TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES,
|
|
21
|
+
bounded_presentation_error,
|
|
22
|
+
bounded_presentation_text,
|
|
23
|
+
bounded_selected_run_fields,
|
|
24
|
+
normalize_presentation_tree,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .._commands import (
|
|
28
|
+
configure_action,
|
|
29
|
+
create_action,
|
|
30
|
+
plot_measurement_command_spec,
|
|
31
|
+
)
|
|
32
|
+
from .._config_persistence import persist_config_value
|
|
33
|
+
from ._run_formatting import ( # noqa: F401
|
|
34
|
+
complete_cell_sort_value,
|
|
35
|
+
format_complete_cell,
|
|
36
|
+
format_duration_dhms,
|
|
37
|
+
format_parameter_list,
|
|
38
|
+
format_parameter_list_html,
|
|
39
|
+
format_point_count,
|
|
40
|
+
format_progress,
|
|
41
|
+
format_progress_percent,
|
|
42
|
+
format_run_duration,
|
|
43
|
+
format_run_state,
|
|
44
|
+
format_run_status,
|
|
45
|
+
format_storage_size,
|
|
46
|
+
format_time_taken_seconds,
|
|
47
|
+
format_timestamp,
|
|
48
|
+
measured_parameter_count,
|
|
49
|
+
point_count_sort_value,
|
|
50
|
+
progress_percent_value,
|
|
51
|
+
run_failed,
|
|
52
|
+
run_is_complete,
|
|
53
|
+
run_tooltip_plain_text,
|
|
54
|
+
run_tooltip_text,
|
|
55
|
+
time_taken_seconds,
|
|
56
|
+
)
|
|
57
|
+
from .details_tables import (
|
|
58
|
+
FULL_VALUE_ID_ROLE,
|
|
59
|
+
CopyableTableWidget,
|
|
60
|
+
SnapshotTreePageRequest,
|
|
61
|
+
SnapshotTreeValueRequest,
|
|
62
|
+
TrustedFullValueDialog,
|
|
63
|
+
WrappedValueDelegate, # noqa: F401 - re-exported for compatibility
|
|
64
|
+
format_value,
|
|
65
|
+
infoTree,
|
|
66
|
+
snapshot_parameters,
|
|
67
|
+
)
|
|
68
|
+
from .preview import (
|
|
69
|
+
COLLAPSE_MINIMUM_RATIO,
|
|
70
|
+
PreviewTab,
|
|
71
|
+
)
|
|
72
|
+
from .run_list_items import (
|
|
73
|
+
MEASUREMENT_PREVIEW_SIZE,
|
|
74
|
+
EqualsAlignedDelegate,
|
|
75
|
+
RunPreviewCell,
|
|
76
|
+
SortableTreeWidgetItem,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
MAX_RUN_PREVIEW_WIDGETS = 500
|
|
80
|
+
RUN_LIST_EVENT_YIELD_ROWS = 250
|
|
81
|
+
RUN_TABLE_COLUMN_WIDTHS_KEY = "GUI.run_table_column_widths"
|
|
82
|
+
RUN_TABLE_VISIBLE_COLUMNS_KEY = "GUI.run_table_visible_columns"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RunList(qtw.QTreeWidget):
|
|
86
|
+
"""
|
|
87
|
+
A modified PyQt6.QtWidgets.QTreeWidget, formated as a list which displays
|
|
88
|
+
all run_ids and other properties found in self.cols.
|
|
89
|
+
|
|
90
|
+
All QTreeWidgetItem are converted to SortableTreeWidgetItem to allow the user to sort
|
|
91
|
+
by any columns.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
column_ids = (
|
|
96
|
+
"run_id",
|
|
97
|
+
"experiment",
|
|
98
|
+
"sample",
|
|
99
|
+
"measurements",
|
|
100
|
+
"setpoints",
|
|
101
|
+
"name",
|
|
102
|
+
"started",
|
|
103
|
+
"completed",
|
|
104
|
+
"status",
|
|
105
|
+
"duration",
|
|
106
|
+
"size",
|
|
107
|
+
"guid",
|
|
108
|
+
)
|
|
109
|
+
column_labels = {
|
|
110
|
+
"run_id": "ID",
|
|
111
|
+
"experiment": "Experiment",
|
|
112
|
+
"sample": "Sample",
|
|
113
|
+
"measurements": "Measurements",
|
|
114
|
+
"setpoints": "Setpoints",
|
|
115
|
+
"name": "Name",
|
|
116
|
+
"started": "Started",
|
|
117
|
+
"completed": "Completed",
|
|
118
|
+
"status": "Status",
|
|
119
|
+
"duration": "Duration",
|
|
120
|
+
"size": "Size",
|
|
121
|
+
"guid": "GUID",
|
|
122
|
+
}
|
|
123
|
+
cols = [
|
|
124
|
+
"ID",
|
|
125
|
+
"Experiment",
|
|
126
|
+
"Sample",
|
|
127
|
+
"Measurements",
|
|
128
|
+
"Setpoints",
|
|
129
|
+
"Name",
|
|
130
|
+
"Started",
|
|
131
|
+
"Completed",
|
|
132
|
+
"Status",
|
|
133
|
+
"Duration",
|
|
134
|
+
"Size",
|
|
135
|
+
"GUID",
|
|
136
|
+
]
|
|
137
|
+
default_visible_column_ids = (
|
|
138
|
+
"run_id",
|
|
139
|
+
"measurements",
|
|
140
|
+
"setpoints",
|
|
141
|
+
"started",
|
|
142
|
+
"status",
|
|
143
|
+
"duration",
|
|
144
|
+
"size",
|
|
145
|
+
)
|
|
146
|
+
default_visible_columns = (
|
|
147
|
+
"ID",
|
|
148
|
+
"Measurements",
|
|
149
|
+
"Setpoints",
|
|
150
|
+
"Started",
|
|
151
|
+
"Status",
|
|
152
|
+
"Duration",
|
|
153
|
+
"Size",
|
|
154
|
+
)
|
|
155
|
+
column_widths = {
|
|
156
|
+
"ID": 44,
|
|
157
|
+
"Measurements": 96,
|
|
158
|
+
"Experiment": 120,
|
|
159
|
+
"Sample": 112,
|
|
160
|
+
"Name": 150,
|
|
161
|
+
"Status": 140,
|
|
162
|
+
"Duration": 96,
|
|
163
|
+
"Size": 62,
|
|
164
|
+
"Completed": 142,
|
|
165
|
+
"GUID": 286,
|
|
166
|
+
}
|
|
167
|
+
elastic_column_widths = {
|
|
168
|
+
"Setpoints": 170,
|
|
169
|
+
"Started": 142,
|
|
170
|
+
}
|
|
171
|
+
representative_column_values = {
|
|
172
|
+
"ID": "9999",
|
|
173
|
+
"Experiment": "experiment-name",
|
|
174
|
+
"Sample": "sample-name",
|
|
175
|
+
"Name": "run-name",
|
|
176
|
+
"Setpoints": "1,200,120 = 10,001 × 60",
|
|
177
|
+
"Started": "2026-05-04 13:05:16",
|
|
178
|
+
"Status": "Interrupted (100.0%)",
|
|
179
|
+
"Duration": "57,116.6 s",
|
|
180
|
+
"Size": "116 MB",
|
|
181
|
+
"Completed": "2026-05-04 13:05:16",
|
|
182
|
+
"GUID": "00000000-0000-0000-0000-000000000000",
|
|
183
|
+
}
|
|
184
|
+
readable_column_widths = {
|
|
185
|
+
"ID": 37,
|
|
186
|
+
"Measurements": 92,
|
|
187
|
+
"Experiment": 96,
|
|
188
|
+
"Sample": 88,
|
|
189
|
+
"Name": 112,
|
|
190
|
+
"Setpoints": 100,
|
|
191
|
+
"Started": 128,
|
|
192
|
+
"Status": 132,
|
|
193
|
+
"Duration": 84,
|
|
194
|
+
"Size": 54,
|
|
195
|
+
"Completed": 128,
|
|
196
|
+
"GUID": 220,
|
|
197
|
+
}
|
|
198
|
+
minimum_column_widths = {
|
|
199
|
+
"ID": 34,
|
|
200
|
+
"Measurements": 84,
|
|
201
|
+
"Experiment": 80,
|
|
202
|
+
"Sample": 72,
|
|
203
|
+
"Name": 80,
|
|
204
|
+
"Setpoints": 80,
|
|
205
|
+
"Started": 84,
|
|
206
|
+
"Status": 72,
|
|
207
|
+
"Duration": 68,
|
|
208
|
+
"Size": 50,
|
|
209
|
+
"Completed": 84,
|
|
210
|
+
"GUID": 120,
|
|
211
|
+
}
|
|
212
|
+
compact_growth_order = (
|
|
213
|
+
"Measurements",
|
|
214
|
+
"Experiment",
|
|
215
|
+
"Sample",
|
|
216
|
+
"Name",
|
|
217
|
+
"Started",
|
|
218
|
+
"Completed",
|
|
219
|
+
"Duration",
|
|
220
|
+
"Size",
|
|
221
|
+
"Status",
|
|
222
|
+
"GUID",
|
|
223
|
+
"Setpoints",
|
|
224
|
+
"ID",
|
|
225
|
+
)
|
|
226
|
+
preferred_growth_order = (
|
|
227
|
+
"Setpoints",
|
|
228
|
+
"Name",
|
|
229
|
+
"Experiment",
|
|
230
|
+
"Sample",
|
|
231
|
+
"Started",
|
|
232
|
+
"Completed",
|
|
233
|
+
"Duration",
|
|
234
|
+
"Size",
|
|
235
|
+
"Measurements",
|
|
236
|
+
"Status",
|
|
237
|
+
"GUID",
|
|
238
|
+
"ID",
|
|
239
|
+
)
|
|
240
|
+
compact_shrink_order = (
|
|
241
|
+
"Setpoints",
|
|
242
|
+
"GUID",
|
|
243
|
+
"Name",
|
|
244
|
+
"Experiment",
|
|
245
|
+
"Sample",
|
|
246
|
+
"Started",
|
|
247
|
+
"Completed",
|
|
248
|
+
"Measurements",
|
|
249
|
+
"Duration",
|
|
250
|
+
"Status",
|
|
251
|
+
"Size",
|
|
252
|
+
"ID",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
selected = QtCore.pyqtSignal([str])
|
|
256
|
+
nonSingleSelection = QtCore.pyqtSignal()
|
|
257
|
+
plot = QtCore.pyqtSignal([str])
|
|
258
|
+
previewPlotRequested = QtCore.pyqtSignal(str, str)
|
|
259
|
+
previewExportRequested = QtCore.pyqtSignal(str, str)
|
|
260
|
+
_shortcut_keys = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
*args,
|
|
265
|
+
initalize=False,
|
|
266
|
+
initialize=None,
|
|
267
|
+
config=None,
|
|
268
|
+
**kargs,
|
|
269
|
+
):
|
|
270
|
+
super().__init__(*args, **kargs)
|
|
271
|
+
# Retained as no-op compatibility flags. Database loading is owned by
|
|
272
|
+
# the controller and arrives through addRuns/setRuns.
|
|
273
|
+
_ = initalize, initialize
|
|
274
|
+
|
|
275
|
+
self.watching: list[SortableTreeWidgetItem] = []
|
|
276
|
+
self.preview_cells: dict[str, RunPreviewCell] = {}
|
|
277
|
+
self._preview_publication_suspended = False
|
|
278
|
+
self._items_by_guid: dict[str, SortableTreeWidgetItem] = {}
|
|
279
|
+
self._resizing_columns = False
|
|
280
|
+
self._manual_column_widths = False
|
|
281
|
+
self._config = config
|
|
282
|
+
self._saved_column_widths = None
|
|
283
|
+
self._column_width_cache: dict[str, int] = {}
|
|
284
|
+
self._column_width_save_timer = QtCore.QTimer(self)
|
|
285
|
+
self._column_width_save_timer.setSingleShot(True)
|
|
286
|
+
self._column_width_save_timer.setInterval(250)
|
|
287
|
+
self._column_width_save_timer.timeout.connect(self._persist_column_widths)
|
|
288
|
+
self._preview_widgets_enabled = True
|
|
289
|
+
self.maxRunId = 0
|
|
290
|
+
|
|
291
|
+
self.setColumnCount(len(self.cols))
|
|
292
|
+
self.setHeaderLabels(self.cols)
|
|
293
|
+
header = self.header()
|
|
294
|
+
if header is not None:
|
|
295
|
+
header.setStretchLastSection(False)
|
|
296
|
+
header.setMinimumSectionSize(32)
|
|
297
|
+
header.setMinimumHeight(
|
|
298
|
+
max(header.sizeHint().height(), self.fontMetrics().height() + 8)
|
|
299
|
+
)
|
|
300
|
+
for column in range(len(self.cols)):
|
|
301
|
+
header.setSectionResizeMode(
|
|
302
|
+
column,
|
|
303
|
+
qtw.QHeaderView.ResizeMode.Interactive,
|
|
304
|
+
)
|
|
305
|
+
self.setRootIsDecorated(False)
|
|
306
|
+
self.setIndentation(0)
|
|
307
|
+
self.setUniformRowHeights(False)
|
|
308
|
+
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
309
|
+
self.setHorizontalScrollMode(qtw.QAbstractItemView.ScrollMode.ScrollPerPixel)
|
|
310
|
+
self._setpoints_delegate = EqualsAlignedDelegate(self)
|
|
311
|
+
self.setItemDelegateForColumn(
|
|
312
|
+
self.cols.index("Setpoints"),
|
|
313
|
+
self._setpoints_delegate,
|
|
314
|
+
)
|
|
315
|
+
self._column_width_cache = self._preferred_column_widths()
|
|
316
|
+
self._apply_column_widths(
|
|
317
|
+
[self._column_width_cache[name] for name in self.cols]
|
|
318
|
+
)
|
|
319
|
+
self.apply_configured_column_widths()
|
|
320
|
+
self.apply_configured_column_visibility()
|
|
321
|
+
|
|
322
|
+
if header is not None:
|
|
323
|
+
header.sectionResized.connect(self._column_resized)
|
|
324
|
+
header.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
|
325
|
+
header.customContextMenuRequested.connect(self._open_header_menu)
|
|
326
|
+
|
|
327
|
+
# Slot connections
|
|
328
|
+
self.itemSelectionChanged.connect(self.onSelect)
|
|
329
|
+
self.itemDoubleClicked.connect(self._double_clicked)
|
|
330
|
+
|
|
331
|
+
# Setup Context Menu
|
|
332
|
+
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
|
333
|
+
self.customContextMenuRequested.connect(self.prepareMenu)
|
|
334
|
+
|
|
335
|
+
context_action = create_action(
|
|
336
|
+
"context.show",
|
|
337
|
+
self,
|
|
338
|
+
status_tip="Show run-list context menu",
|
|
339
|
+
)
|
|
340
|
+
context_action.setShortcutContext(
|
|
341
|
+
QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut
|
|
342
|
+
)
|
|
343
|
+
context_action.triggered.connect(self.openKeyboardMenu)
|
|
344
|
+
self.addAction(context_action)
|
|
345
|
+
|
|
346
|
+
def addRuns(self, runs, *, continue_loading=None, commit_check=None):
|
|
347
|
+
"""
|
|
348
|
+
Adds Row to table.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
runs : dict{int: dict}
|
|
353
|
+
Row data to be added.
|
|
354
|
+
Plain run metadata supplied by the controller.
|
|
355
|
+
|
|
356
|
+
"""
|
|
357
|
+
if not runs:
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
previous_max_run_id = self.maxRunId
|
|
361
|
+
added_items: list[SortableTreeWidgetItem] = []
|
|
362
|
+
guid_history: list[
|
|
363
|
+
tuple[
|
|
364
|
+
str,
|
|
365
|
+
SortableTreeWidgetItem,
|
|
366
|
+
SortableTreeWidgetItem | None,
|
|
367
|
+
]
|
|
368
|
+
] = []
|
|
369
|
+
preview_history: list[tuple[str, RunPreviewCell, RunPreviewCell | None]] = []
|
|
370
|
+
committed = False
|
|
371
|
+
|
|
372
|
+
def roll_back_added_items():
|
|
373
|
+
for guid, cell, previous_cell in reversed(preview_history):
|
|
374
|
+
if self.preview_cells.get(guid) is not cell:
|
|
375
|
+
continue
|
|
376
|
+
if previous_cell is None:
|
|
377
|
+
self.preview_cells.pop(guid, None)
|
|
378
|
+
else:
|
|
379
|
+
self.preview_cells[guid] = previous_cell
|
|
380
|
+
cell.deleteLater()
|
|
381
|
+
|
|
382
|
+
for item in reversed(added_items):
|
|
383
|
+
for watching_index in range(len(self.watching) - 1, -1, -1):
|
|
384
|
+
if self.watching[watching_index] is item:
|
|
385
|
+
del self.watching[watching_index]
|
|
386
|
+
item_index = self.indexOfTopLevelItem(item)
|
|
387
|
+
if item_index >= 0:
|
|
388
|
+
self.takeTopLevelItem(item_index)
|
|
389
|
+
|
|
390
|
+
for guid, item, previous_item in reversed(guid_history):
|
|
391
|
+
if self._items_by_guid.get(guid) is not item:
|
|
392
|
+
continue
|
|
393
|
+
if previous_item is None:
|
|
394
|
+
self._items_by_guid.pop(guid, None)
|
|
395
|
+
else:
|
|
396
|
+
self._items_by_guid[guid] = previous_item
|
|
397
|
+
|
|
398
|
+
self.maxRunId = previous_max_run_id
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
if (
|
|
402
|
+
self._preview_widgets_enabled
|
|
403
|
+
and self.topLevelItemCount() + len(runs) > MAX_RUN_PREVIEW_WIDGETS
|
|
404
|
+
):
|
|
405
|
+
self._disable_measurement_preview_widgets()
|
|
406
|
+
|
|
407
|
+
# Prevent constant resorting while rows are added.
|
|
408
|
+
self.setSortingEnabled(False)
|
|
409
|
+
|
|
410
|
+
run_count = len(runs)
|
|
411
|
+
for row_index, (run_id, metadata) in enumerate(runs.items(), start=1):
|
|
412
|
+
if callable(continue_loading) and not continue_loading():
|
|
413
|
+
return False
|
|
414
|
+
append_to_watching = False
|
|
415
|
+
measurement_count = measured_parameter_count(metadata)
|
|
416
|
+
arr = self._run_column_texts(run_id, metadata)
|
|
417
|
+
|
|
418
|
+
if not run_is_complete(metadata):
|
|
419
|
+
append_to_watching = True
|
|
420
|
+
|
|
421
|
+
# Convert arr to easy to sort QTreeWidgetItem
|
|
422
|
+
item = SortableTreeWidgetItem(arr)
|
|
423
|
+
item.set_guid(str(metadata.get("guid") or ""))
|
|
424
|
+
item.run_metadata = dict(metadata)
|
|
425
|
+
for col_name in ("ID", "Setpoints", "Size"):
|
|
426
|
+
item.setTextAlignment(
|
|
427
|
+
self.cols.index(col_name),
|
|
428
|
+
QtCore.Qt.AlignmentFlag.AlignRight
|
|
429
|
+
| QtCore.Qt.AlignmentFlag.AlignVCenter,
|
|
430
|
+
)
|
|
431
|
+
item.setTextAlignment(
|
|
432
|
+
self.cols.index("Status"), QtCore.Qt.AlignmentFlag.AlignCenter
|
|
433
|
+
)
|
|
434
|
+
item.setTextAlignment(
|
|
435
|
+
self.cols.index("Duration"),
|
|
436
|
+
QtCore.Qt.AlignmentFlag.AlignRight
|
|
437
|
+
| QtCore.Qt.AlignmentFlag.AlignVCenter,
|
|
438
|
+
)
|
|
439
|
+
item.setData(
|
|
440
|
+
self.cols.index("Measurements"),
|
|
441
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
442
|
+
measurement_count,
|
|
443
|
+
)
|
|
444
|
+
item.setData(
|
|
445
|
+
self.cols.index("Measurements"),
|
|
446
|
+
QtCore.Qt.ItemDataRole.AccessibleTextRole,
|
|
447
|
+
self._measurement_accessible_text(metadata, measurement_count),
|
|
448
|
+
)
|
|
449
|
+
item.setSizeHint(
|
|
450
|
+
self.cols.index("Measurements"),
|
|
451
|
+
QtCore.QSize(0, MEASUREMENT_PREVIEW_SIZE + 6),
|
|
452
|
+
)
|
|
453
|
+
item.setData(
|
|
454
|
+
self.cols.index("Setpoints"),
|
|
455
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
456
|
+
point_count_sort_value(metadata),
|
|
457
|
+
)
|
|
458
|
+
item.setData(
|
|
459
|
+
self.cols.index("Started"),
|
|
460
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
461
|
+
metadata.get("run_timestamp"),
|
|
462
|
+
)
|
|
463
|
+
item.setData(
|
|
464
|
+
self.cols.index("Completed"),
|
|
465
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
466
|
+
metadata.get("completed_timestamp"),
|
|
467
|
+
)
|
|
468
|
+
item.setData(
|
|
469
|
+
self.cols.index("Status"),
|
|
470
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
471
|
+
complete_cell_sort_value(metadata),
|
|
472
|
+
)
|
|
473
|
+
item.setData(
|
|
474
|
+
self.cols.index("Duration"),
|
|
475
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
476
|
+
time_taken_seconds(metadata),
|
|
477
|
+
)
|
|
478
|
+
item.setData(
|
|
479
|
+
self.cols.index("Size"),
|
|
480
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
481
|
+
metadata.get("storage_bytes"),
|
|
482
|
+
)
|
|
483
|
+
item.update_tooltip()
|
|
484
|
+
|
|
485
|
+
# Add to top. Record every mutation before the next operation
|
|
486
|
+
# so a nested-event abort or widget error can remove the whole
|
|
487
|
+
# page instead of leaving an unacknowledged prefix visible.
|
|
488
|
+
self.addTopLevelItem(item)
|
|
489
|
+
added_items.append(item)
|
|
490
|
+
previous_item = self._items_by_guid.get(item.guid)
|
|
491
|
+
guid_history.append((item.guid, item, previous_item))
|
|
492
|
+
self._items_by_guid[item.guid] = item
|
|
493
|
+
if self._preview_widgets_enabled:
|
|
494
|
+
previous_cell = self.preview_cells.get(item.guid)
|
|
495
|
+
try:
|
|
496
|
+
self._set_measurement_preview_cell(item, measurement_count)
|
|
497
|
+
finally:
|
|
498
|
+
current_cell = self.preview_cells.get(item.guid)
|
|
499
|
+
if (
|
|
500
|
+
current_cell is not None
|
|
501
|
+
and current_cell is not previous_cell
|
|
502
|
+
):
|
|
503
|
+
preview_history.append(
|
|
504
|
+
(item.guid, current_cell, previous_cell)
|
|
505
|
+
)
|
|
506
|
+
else:
|
|
507
|
+
self._set_compact_measurement_cell(item, measurement_count)
|
|
508
|
+
|
|
509
|
+
# If unfinished run
|
|
510
|
+
if append_to_watching:
|
|
511
|
+
self.watching.append(item)
|
|
512
|
+
|
|
513
|
+
if row_index < run_count and row_index % RUN_LIST_EVENT_YIELD_ROWS == 0:
|
|
514
|
+
# Basic-row construction is deliberately bounded so a large
|
|
515
|
+
# trusted page or initial run list cannot monopolise Qt. Detail
|
|
516
|
+
# work starts only after addRuns returns (or after the refresh
|
|
517
|
+
# worker receives its explicit publication acknowledgement).
|
|
518
|
+
QtCore.QCoreApplication.processEvents(
|
|
519
|
+
QtCore.QEventLoop.ProcessEventsFlag.AllEvents,
|
|
520
|
+
5,
|
|
521
|
+
)
|
|
522
|
+
if callable(continue_loading) and not continue_loading():
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
# Callers that stage database-backed rows may need one last source
|
|
526
|
+
# identity observation after the nested event yields above. Keep
|
|
527
|
+
# it inside this transaction so failure removes the entire page.
|
|
528
|
+
if callable(commit_check) and not commit_check():
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
self._setpoints_delegate.invalidate_width_cache()
|
|
532
|
+
self.setSortingEnabled(True)
|
|
533
|
+
# maxRunId is the accepted GUI cursor. Publish it only after every
|
|
534
|
+
# row in this page and the final view update have succeeded.
|
|
535
|
+
self.maxRunId = max(previous_max_run_id, max(runs, default=0))
|
|
536
|
+
committed = True
|
|
537
|
+
return True
|
|
538
|
+
finally:
|
|
539
|
+
if not committed:
|
|
540
|
+
roll_back_added_items()
|
|
541
|
+
self._setpoints_delegate.invalidate_width_cache()
|
|
542
|
+
self.setSortingEnabled(True)
|
|
543
|
+
|
|
544
|
+
def updateRuns(self, runs):
|
|
545
|
+
"""
|
|
546
|
+
Merge updated metadata into existing rows.
|
|
547
|
+
|
|
548
|
+
Background detail loading uses this to fill expensive columns without
|
|
549
|
+
rebuilding the table or disturbing the user's selection.
|
|
550
|
+
|
|
551
|
+
"""
|
|
552
|
+
if not runs:
|
|
553
|
+
return {}
|
|
554
|
+
|
|
555
|
+
updated = {}
|
|
556
|
+
sorting_enabled = self.isSortingEnabled()
|
|
557
|
+
sort_column = self.sortColumn()
|
|
558
|
+
mutable_columns = {
|
|
559
|
+
self.cols.index(name)
|
|
560
|
+
for name in (
|
|
561
|
+
"Measurements",
|
|
562
|
+
"Setpoints",
|
|
563
|
+
"Experiment",
|
|
564
|
+
"Sample",
|
|
565
|
+
"Name",
|
|
566
|
+
"Started",
|
|
567
|
+
"Status",
|
|
568
|
+
"Duration",
|
|
569
|
+
"Size",
|
|
570
|
+
"Completed",
|
|
571
|
+
"GUID",
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
suspend_sorting = sorting_enabled and sort_column in mutable_columns
|
|
575
|
+
if suspend_sorting:
|
|
576
|
+
self.setSortingEnabled(False)
|
|
577
|
+
for run_id, metadata in runs.items():
|
|
578
|
+
guid = metadata.get("guid")
|
|
579
|
+
item = self._item_for_guid(guid)
|
|
580
|
+
if item is None:
|
|
581
|
+
continue
|
|
582
|
+
|
|
583
|
+
merged_metadata = dict(metadata)
|
|
584
|
+
if (
|
|
585
|
+
item.run_metadata.get("storage_bytes") is not None
|
|
586
|
+
and item.run_metadata.get("storage_bytes_estimated") is False
|
|
587
|
+
and merged_metadata.get("storage_bytes_estimated") is True
|
|
588
|
+
):
|
|
589
|
+
merged_metadata.pop("storage_bytes", None)
|
|
590
|
+
merged_metadata.pop("storage_bytes_estimated", None)
|
|
591
|
+
|
|
592
|
+
item.run_metadata.update(merged_metadata)
|
|
593
|
+
self._refresh_run_item(item)
|
|
594
|
+
self._sync_watching_item(item)
|
|
595
|
+
updated[run_id] = dict(item.run_metadata)
|
|
596
|
+
|
|
597
|
+
self._setpoints_delegate.invalidate_width_cache()
|
|
598
|
+
if suspend_sorting:
|
|
599
|
+
self.setSortingEnabled(True)
|
|
600
|
+
return updated
|
|
601
|
+
|
|
602
|
+
def _refresh_run_item(self, item):
|
|
603
|
+
metadata = item.run_metadata
|
|
604
|
+
measurement_count = measured_parameter_count(metadata)
|
|
605
|
+
|
|
606
|
+
self._refresh_metadata_columns(item)
|
|
607
|
+
|
|
608
|
+
measurements_col = self.cols.index("Measurements")
|
|
609
|
+
item.setData(
|
|
610
|
+
measurements_col,
|
|
611
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
612
|
+
measurement_count,
|
|
613
|
+
)
|
|
614
|
+
item.setData(
|
|
615
|
+
measurements_col,
|
|
616
|
+
QtCore.Qt.ItemDataRole.AccessibleTextRole,
|
|
617
|
+
self._measurement_accessible_text(metadata, measurement_count),
|
|
618
|
+
)
|
|
619
|
+
item.setSizeHint(
|
|
620
|
+
measurements_col,
|
|
621
|
+
QtCore.QSize(0, MEASUREMENT_PREVIEW_SIZE + 6),
|
|
622
|
+
)
|
|
623
|
+
cell = self.preview_cells.get(item.guid)
|
|
624
|
+
if self._preview_widgets_enabled and (
|
|
625
|
+
cell is None or cell.placeholder_count != measurement_count
|
|
626
|
+
):
|
|
627
|
+
self._set_measurement_preview_cell(item, measurement_count)
|
|
628
|
+
elif not self._preview_widgets_enabled:
|
|
629
|
+
self._set_compact_measurement_cell(item, measurement_count)
|
|
630
|
+
elif cell is not None:
|
|
631
|
+
cell.update_placeholder_metadata(metadata)
|
|
632
|
+
|
|
633
|
+
setpoints_col = self.cols.index("Setpoints")
|
|
634
|
+
item.setText(setpoints_col, format_point_count(metadata))
|
|
635
|
+
item.setData(
|
|
636
|
+
setpoints_col,
|
|
637
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
638
|
+
point_count_sort_value(metadata),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
complete_col = self.cols.index("Status")
|
|
642
|
+
item.setText(complete_col, format_complete_cell(metadata))
|
|
643
|
+
item.setData(
|
|
644
|
+
complete_col,
|
|
645
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
646
|
+
complete_cell_sort_value(metadata),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
duration_col = self.cols.index("Duration")
|
|
650
|
+
item.setText(duration_col, format_time_taken_seconds(metadata))
|
|
651
|
+
item.setData(
|
|
652
|
+
duration_col,
|
|
653
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
654
|
+
time_taken_seconds(metadata),
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
size_col = self.cols.index("Size")
|
|
658
|
+
item.setText(size_col, format_storage_size(metadata.get("storage_bytes")))
|
|
659
|
+
item.setData(
|
|
660
|
+
size_col,
|
|
661
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
662
|
+
metadata.get("storage_bytes"),
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
item.update_tooltip()
|
|
666
|
+
|
|
667
|
+
@classmethod
|
|
668
|
+
def _run_column_texts(cls, run_id, metadata):
|
|
669
|
+
values = {
|
|
670
|
+
"ID": str(run_id),
|
|
671
|
+
"Measurements": "",
|
|
672
|
+
"Setpoints": format_point_count(metadata),
|
|
673
|
+
"Experiment": cls._metadata_cell_text(metadata.get("exp_name")),
|
|
674
|
+
"Sample": cls._metadata_cell_text(metadata.get("sample_name")),
|
|
675
|
+
"Name": cls._metadata_cell_text(metadata.get("name")),
|
|
676
|
+
"Started": format_timestamp(metadata.get("run_timestamp")),
|
|
677
|
+
"Status": format_complete_cell(metadata),
|
|
678
|
+
"Duration": format_time_taken_seconds(metadata),
|
|
679
|
+
"Size": format_storage_size(metadata.get("storage_bytes")),
|
|
680
|
+
"Completed": cls._format_completed_timestamp(metadata),
|
|
681
|
+
"GUID": cls._metadata_cell_text(metadata.get("guid")),
|
|
682
|
+
}
|
|
683
|
+
return [values[name] for name in cls.cols]
|
|
684
|
+
|
|
685
|
+
@staticmethod
|
|
686
|
+
def _metadata_cell_text(value):
|
|
687
|
+
return "" if value is None else str(value)
|
|
688
|
+
|
|
689
|
+
@staticmethod
|
|
690
|
+
def _format_completed_timestamp(metadata):
|
|
691
|
+
completed_timestamp = metadata.get("completed_timestamp")
|
|
692
|
+
if completed_timestamp:
|
|
693
|
+
return format_timestamp(completed_timestamp)
|
|
694
|
+
if run_is_complete(metadata):
|
|
695
|
+
return "unknown"
|
|
696
|
+
return "Ongoing"
|
|
697
|
+
|
|
698
|
+
def _refresh_metadata_columns(self, item):
|
|
699
|
+
metadata = item.run_metadata
|
|
700
|
+
values = {
|
|
701
|
+
"Experiment": self._metadata_cell_text(metadata.get("exp_name")),
|
|
702
|
+
"Sample": self._metadata_cell_text(metadata.get("sample_name")),
|
|
703
|
+
"Name": self._metadata_cell_text(metadata.get("name")),
|
|
704
|
+
"Started": format_timestamp(metadata.get("run_timestamp")),
|
|
705
|
+
"Completed": self._format_completed_timestamp(metadata),
|
|
706
|
+
"GUID": self._metadata_cell_text(metadata.get("guid") or item.guid),
|
|
707
|
+
}
|
|
708
|
+
for name, value in values.items():
|
|
709
|
+
item.setText(self.cols.index(name), value)
|
|
710
|
+
|
|
711
|
+
item.setData(
|
|
712
|
+
self.cols.index("Started"),
|
|
713
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
714
|
+
metadata.get("run_timestamp"),
|
|
715
|
+
)
|
|
716
|
+
item.setData(
|
|
717
|
+
self.cols.index("Completed"),
|
|
718
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
719
|
+
metadata.get("completed_timestamp"),
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def _sync_watching_item(self, item):
|
|
723
|
+
watching = item in self.watching
|
|
724
|
+
complete = run_is_complete(item.run_metadata)
|
|
725
|
+
if complete and watching:
|
|
726
|
+
self.watching.remove(item)
|
|
727
|
+
elif not complete and not watching:
|
|
728
|
+
self.watching.append(item)
|
|
729
|
+
|
|
730
|
+
def clear(self):
|
|
731
|
+
self.preview_cells = {}
|
|
732
|
+
self._items_by_guid = {}
|
|
733
|
+
self._preview_widgets_enabled = True
|
|
734
|
+
self.setUniformRowHeights(False)
|
|
735
|
+
self._setpoints_delegate.invalidate_width_cache()
|
|
736
|
+
super().clear()
|
|
737
|
+
|
|
738
|
+
def _set_measurement_preview_cell(self, item, measurement_count):
|
|
739
|
+
column = self.cols.index("Measurements")
|
|
740
|
+
cell = RunPreviewCell(item.guid, measurement_count, self)
|
|
741
|
+
cell.update_placeholder_metadata(item.run_metadata)
|
|
742
|
+
cell.plotRequested.connect(self._preview_plot_requested)
|
|
743
|
+
cell.exportRequested.connect(self._preview_export_requested)
|
|
744
|
+
accessible_text = self._measurement_accessible_text(
|
|
745
|
+
item.run_metadata,
|
|
746
|
+
measurement_count,
|
|
747
|
+
)
|
|
748
|
+
cell.setAccessibleName(accessible_text)
|
|
749
|
+
cell.setAccessibleDescription(
|
|
750
|
+
"Measurement previews. Focus a preview for plot and export actions."
|
|
751
|
+
)
|
|
752
|
+
self.preview_cells[item.guid] = cell
|
|
753
|
+
self.setItemWidget(item, column, cell)
|
|
754
|
+
|
|
755
|
+
def _set_compact_measurement_cell(self, item, measurement_count):
|
|
756
|
+
column = self.cols.index("Measurements")
|
|
757
|
+
item.setText(column, str(measurement_count))
|
|
758
|
+
item.setSizeHint(column, QtCore.QSize(0, 22))
|
|
759
|
+
item.setToolTip(
|
|
760
|
+
column,
|
|
761
|
+
"Inline previews are disabled for this large run list. "
|
|
762
|
+
"Select the run to use the Preview tab.",
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
def _disable_measurement_preview_widgets(self):
|
|
766
|
+
column = self.cols.index("Measurements")
|
|
767
|
+
for guid, cell in tuple(self.preview_cells.items()):
|
|
768
|
+
item = self._items_by_guid.get(guid)
|
|
769
|
+
if item is not None:
|
|
770
|
+
self.removeItemWidget(item, column)
|
|
771
|
+
self._set_compact_measurement_cell(
|
|
772
|
+
item,
|
|
773
|
+
measured_parameter_count(item.run_metadata),
|
|
774
|
+
)
|
|
775
|
+
cell.deleteLater()
|
|
776
|
+
self.preview_cells.clear()
|
|
777
|
+
self._preview_widgets_enabled = False
|
|
778
|
+
self.setUniformRowHeights(True)
|
|
779
|
+
|
|
780
|
+
@QtCore.pyqtSlot(str, object)
|
|
781
|
+
def set_run_previews(self, guid, previews):
|
|
782
|
+
if self._preview_publication_suspended:
|
|
783
|
+
return
|
|
784
|
+
cell = self.preview_cells.get(guid)
|
|
785
|
+
if cell is not None:
|
|
786
|
+
cell.show_previews(previews)
|
|
787
|
+
|
|
788
|
+
def accepts_run_preview(self, guid):
|
|
789
|
+
"""Return whether one inline cell can own a decoded thumbnail now."""
|
|
790
|
+
|
|
791
|
+
return (
|
|
792
|
+
not self._preview_publication_suspended
|
|
793
|
+
and str(guid or "") in self.preview_cells
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
def run_preview_is_ready(self, guid):
|
|
797
|
+
"""Expose bounded inline-cell readiness for acceptance diagnostics."""
|
|
798
|
+
|
|
799
|
+
cell = self.preview_cells.get(str(guid or ""))
|
|
800
|
+
return bool(cell is not None and cell._has_rendered_previews)
|
|
801
|
+
|
|
802
|
+
@QtCore.pyqtSlot(str, bool)
|
|
803
|
+
def set_run_preview_generating(self, guid, generating):
|
|
804
|
+
if self._preview_publication_suspended:
|
|
805
|
+
return
|
|
806
|
+
cell = self.preview_cells.get(guid)
|
|
807
|
+
if cell is not None:
|
|
808
|
+
cell.set_generating(generating)
|
|
809
|
+
|
|
810
|
+
def set_preview_publication_suspended(self, suspended):
|
|
811
|
+
"""Gate source-bound preview callbacks during a run-list transaction."""
|
|
812
|
+
previous = self._preview_publication_suspended
|
|
813
|
+
self._preview_publication_suspended = bool(suspended)
|
|
814
|
+
return previous
|
|
815
|
+
|
|
816
|
+
def show_run_preview_placeholders(self):
|
|
817
|
+
"""Discard rendered run-list previews without changing row metadata."""
|
|
818
|
+
for cell in tuple(self.preview_cells.values()):
|
|
819
|
+
cell.show_placeholders(generating=False)
|
|
820
|
+
|
|
821
|
+
@QtCore.pyqtSlot(str, str)
|
|
822
|
+
def _preview_plot_requested(self, guid, parameter):
|
|
823
|
+
item = self._item_for_guid(guid)
|
|
824
|
+
if item is not None:
|
|
825
|
+
self.setCurrentItem(item)
|
|
826
|
+
self.previewPlotRequested.emit(guid, parameter)
|
|
827
|
+
|
|
828
|
+
@QtCore.pyqtSlot(str, str)
|
|
829
|
+
def _preview_export_requested(self, guid, parameter):
|
|
830
|
+
item = self._item_for_guid(guid)
|
|
831
|
+
if item is not None:
|
|
832
|
+
self.setCurrentItem(item)
|
|
833
|
+
self.previewExportRequested.emit(guid, parameter)
|
|
834
|
+
|
|
835
|
+
def _item_for_guid(self, guid):
|
|
836
|
+
return self._items_by_guid.get(guid)
|
|
837
|
+
|
|
838
|
+
@staticmethod
|
|
839
|
+
def _measurement_accessible_text(metadata, measurement_count):
|
|
840
|
+
parameters = [
|
|
841
|
+
str(parameter)
|
|
842
|
+
for parameter in metadata.get("measure_parameters", [])
|
|
843
|
+
if parameter
|
|
844
|
+
]
|
|
845
|
+
noun = "measurement" if measurement_count == 1 else "measurements"
|
|
846
|
+
summary = f"{measurement_count} {noun}"
|
|
847
|
+
if parameters:
|
|
848
|
+
summary += f": {', '.join(parameters)}"
|
|
849
|
+
return summary
|
|
850
|
+
|
|
851
|
+
def _column_resized(self, column, old_size, new_size):
|
|
852
|
+
if self._resizing_columns or old_size == new_size:
|
|
853
|
+
return
|
|
854
|
+
|
|
855
|
+
if 0 <= column < len(self.cols) and new_size >= 32:
|
|
856
|
+
self._column_width_cache[self.cols[column]] = new_size
|
|
857
|
+
self._manual_column_widths = True
|
|
858
|
+
if self._config is not None:
|
|
859
|
+
self._column_width_save_timer.start()
|
|
860
|
+
|
|
861
|
+
def reset_column_widths(self):
|
|
862
|
+
self._column_width_save_timer.stop()
|
|
863
|
+
if self._config is not None and not persist_config_value(
|
|
864
|
+
self,
|
|
865
|
+
self._config,
|
|
866
|
+
RUN_TABLE_COLUMN_WIDTHS_KEY,
|
|
867
|
+
[],
|
|
868
|
+
"the run-table column widths",
|
|
869
|
+
):
|
|
870
|
+
return False
|
|
871
|
+
|
|
872
|
+
self._saved_column_widths = None
|
|
873
|
+
self._manual_column_widths = False
|
|
874
|
+
self._column_width_cache = self._preferred_column_widths()
|
|
875
|
+
self._resize_columns(force=True)
|
|
876
|
+
return True
|
|
877
|
+
|
|
878
|
+
def apply_configured_column_widths(self):
|
|
879
|
+
"""Apply saved widths, or restore responsive defaults when none exist."""
|
|
880
|
+
widths = self._configured_column_widths()
|
|
881
|
+
self._column_width_save_timer.stop()
|
|
882
|
+
if widths is None:
|
|
883
|
+
self._saved_column_widths = None
|
|
884
|
+
self._manual_column_widths = False
|
|
885
|
+
self._column_width_cache = self._preferred_column_widths()
|
|
886
|
+
self._resize_columns(force=True)
|
|
887
|
+
return
|
|
888
|
+
|
|
889
|
+
self._apply_column_widths(widths)
|
|
890
|
+
self._saved_column_widths = widths
|
|
891
|
+
self._manual_column_widths = True
|
|
892
|
+
|
|
893
|
+
def _configured_column_widths(self):
|
|
894
|
+
if self._config is None:
|
|
895
|
+
return None
|
|
896
|
+
widths = self._config.get(RUN_TABLE_COLUMN_WIDTHS_KEY)
|
|
897
|
+
if not widths:
|
|
898
|
+
return None
|
|
899
|
+
return list(widths)
|
|
900
|
+
|
|
901
|
+
def _apply_column_widths(self, widths):
|
|
902
|
+
self._resizing_columns = True
|
|
903
|
+
try:
|
|
904
|
+
for column, (name, width) in enumerate(zip(self.cols, widths, strict=True)):
|
|
905
|
+
self._column_width_cache[name] = width
|
|
906
|
+
self.setColumnWidth(column, width)
|
|
907
|
+
finally:
|
|
908
|
+
self._resizing_columns = False
|
|
909
|
+
|
|
910
|
+
def _persist_column_widths(self):
|
|
911
|
+
if self._config is None or not self._manual_column_widths:
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
for column, name in enumerate(self.cols):
|
|
915
|
+
if not self.isColumnHidden(column) and self.columnWidth(column) >= 32:
|
|
916
|
+
self._column_width_cache[name] = self.columnWidth(column)
|
|
917
|
+
persisted_widths = [
|
|
918
|
+
max(32, self._column_width_cache.get(name, 32)) for name in self.cols
|
|
919
|
+
]
|
|
920
|
+
previous_widths = self._saved_column_widths
|
|
921
|
+
|
|
922
|
+
def rollback():
|
|
923
|
+
if previous_widths is None:
|
|
924
|
+
self._manual_column_widths = False
|
|
925
|
+
self._resize_columns(force=True)
|
|
926
|
+
else:
|
|
927
|
+
self._apply_column_widths(previous_widths)
|
|
928
|
+
self._manual_column_widths = True
|
|
929
|
+
|
|
930
|
+
if persist_config_value(
|
|
931
|
+
self,
|
|
932
|
+
self._config,
|
|
933
|
+
RUN_TABLE_COLUMN_WIDTHS_KEY,
|
|
934
|
+
persisted_widths,
|
|
935
|
+
"the run-table column widths",
|
|
936
|
+
rollback,
|
|
937
|
+
):
|
|
938
|
+
self._saved_column_widths = [
|
|
939
|
+
max(32, self._column_width_cache.get(name, 32)) for name in self.cols
|
|
940
|
+
]
|
|
941
|
+
|
|
942
|
+
def visible_column_ids(self):
|
|
943
|
+
return [
|
|
944
|
+
column_id
|
|
945
|
+
for column, column_id in enumerate(self.column_ids)
|
|
946
|
+
if not self.isColumnHidden(column)
|
|
947
|
+
]
|
|
948
|
+
|
|
949
|
+
def visible_columns(self):
|
|
950
|
+
return [
|
|
951
|
+
self.column_labels[column_id] for column_id in self.visible_column_ids()
|
|
952
|
+
]
|
|
953
|
+
|
|
954
|
+
def apply_configured_column_visibility(self):
|
|
955
|
+
"""Show the columns selected in the current configuration."""
|
|
956
|
+
self._apply_visible_column_ids(self._configured_visible_column_ids())
|
|
957
|
+
|
|
958
|
+
def _configured_visible_column_ids(self):
|
|
959
|
+
if self._config is None:
|
|
960
|
+
return list(self.default_visible_column_ids)
|
|
961
|
+
configured = self._config.get(RUN_TABLE_VISIBLE_COLUMNS_KEY)
|
|
962
|
+
configured_set = set(configured)
|
|
963
|
+
return [
|
|
964
|
+
column_id for column_id in self.column_ids if column_id in configured_set
|
|
965
|
+
]
|
|
966
|
+
|
|
967
|
+
def set_column_visible(self, column, visible):
|
|
968
|
+
"""Show or hide one column and persist the complete visible set."""
|
|
969
|
+
column_id = self._resolve_column_id(column)
|
|
970
|
+
visible_ids = self.visible_column_ids()
|
|
971
|
+
currently_visible = column_id in visible_ids
|
|
972
|
+
if bool(visible) == currently_visible:
|
|
973
|
+
return True
|
|
974
|
+
|
|
975
|
+
if visible:
|
|
976
|
+
visible_ids.append(column_id)
|
|
977
|
+
else:
|
|
978
|
+
visible_ids.remove(column_id)
|
|
979
|
+
visible_set = set(visible_ids)
|
|
980
|
+
visible_ids = [
|
|
981
|
+
candidate for candidate in self.column_ids if candidate in visible_set
|
|
982
|
+
]
|
|
983
|
+
|
|
984
|
+
if self._config is not None and not persist_config_value(
|
|
985
|
+
self,
|
|
986
|
+
self._config,
|
|
987
|
+
RUN_TABLE_VISIBLE_COLUMNS_KEY,
|
|
988
|
+
visible_ids,
|
|
989
|
+
"the run-table columns",
|
|
990
|
+
):
|
|
991
|
+
return False
|
|
992
|
+
|
|
993
|
+
self._apply_visible_column_ids(visible_ids)
|
|
994
|
+
return True
|
|
995
|
+
|
|
996
|
+
def reset_column_visibility(self):
|
|
997
|
+
visible_ids = list(self.default_visible_column_ids)
|
|
998
|
+
if self._config is not None and not persist_config_value(
|
|
999
|
+
self,
|
|
1000
|
+
self._config,
|
|
1001
|
+
RUN_TABLE_VISIBLE_COLUMNS_KEY,
|
|
1002
|
+
visible_ids,
|
|
1003
|
+
"the run-table columns",
|
|
1004
|
+
):
|
|
1005
|
+
return False
|
|
1006
|
+
|
|
1007
|
+
self._apply_visible_column_ids(visible_ids)
|
|
1008
|
+
return True
|
|
1009
|
+
|
|
1010
|
+
def _resolve_column_id(self, column):
|
|
1011
|
+
if column in self.column_ids:
|
|
1012
|
+
return column
|
|
1013
|
+
for column_id, label in self.column_labels.items():
|
|
1014
|
+
if column == label:
|
|
1015
|
+
return column_id
|
|
1016
|
+
raise ValueError(f"Unknown run-table column: {column!r}")
|
|
1017
|
+
|
|
1018
|
+
def _apply_visible_column_ids(self, visible_ids):
|
|
1019
|
+
visible_set = set(visible_ids)
|
|
1020
|
+
for column, name in enumerate(self.cols):
|
|
1021
|
+
width = self.columnWidth(column)
|
|
1022
|
+
if not self.isColumnHidden(column) and width >= 32:
|
|
1023
|
+
self._column_width_cache[name] = width
|
|
1024
|
+
|
|
1025
|
+
self._resizing_columns = True
|
|
1026
|
+
try:
|
|
1027
|
+
for column, (column_id, name) in enumerate(
|
|
1028
|
+
zip(self.column_ids, self.cols, strict=True)
|
|
1029
|
+
):
|
|
1030
|
+
is_visible = column_id in visible_set
|
|
1031
|
+
self.setColumnHidden(column, not is_visible)
|
|
1032
|
+
if is_visible:
|
|
1033
|
+
self.setColumnWidth(
|
|
1034
|
+
column,
|
|
1035
|
+
max(32, self._column_width_cache.get(name, 32)),
|
|
1036
|
+
)
|
|
1037
|
+
finally:
|
|
1038
|
+
self._resizing_columns = False
|
|
1039
|
+
|
|
1040
|
+
sort_column = self.sortColumn()
|
|
1041
|
+
if (
|
|
1042
|
+
self.isSortingEnabled()
|
|
1043
|
+
and 0 <= sort_column < len(self.cols)
|
|
1044
|
+
and self.isColumnHidden(sort_column)
|
|
1045
|
+
and visible_set
|
|
1046
|
+
):
|
|
1047
|
+
first_visible_column = next(
|
|
1048
|
+
column
|
|
1049
|
+
for column, column_id in enumerate(self.column_ids)
|
|
1050
|
+
if column_id in visible_set
|
|
1051
|
+
)
|
|
1052
|
+
header = self.header()
|
|
1053
|
+
sort_order = (
|
|
1054
|
+
header.sortIndicatorOrder()
|
|
1055
|
+
if header is not None
|
|
1056
|
+
else QtCore.Qt.SortOrder.AscendingOrder
|
|
1057
|
+
)
|
|
1058
|
+
self.sortItems(first_visible_column, sort_order)
|
|
1059
|
+
|
|
1060
|
+
if not self._manual_column_widths:
|
|
1061
|
+
self._resize_columns(force=True)
|
|
1062
|
+
|
|
1063
|
+
def _build_header_menu(self):
|
|
1064
|
+
menu = qtw.QMenu(self)
|
|
1065
|
+
columns_menu = menu.addMenu("Columns")
|
|
1066
|
+
visible_ids = set(self.visible_column_ids())
|
|
1067
|
+
for column_id in self.column_ids:
|
|
1068
|
+
label = self.column_labels[column_id]
|
|
1069
|
+
action = columns_menu.addAction(label)
|
|
1070
|
+
action.setCheckable(True)
|
|
1071
|
+
action.setChecked(column_id in visible_ids)
|
|
1072
|
+
action.setProperty("runTableColumnId", column_id)
|
|
1073
|
+
action.triggered.connect(
|
|
1074
|
+
lambda checked, selected=column_id, source=action: (
|
|
1075
|
+
self._toggle_column_from_action(
|
|
1076
|
+
selected,
|
|
1077
|
+
checked,
|
|
1078
|
+
source,
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
columns_menu.addSeparator()
|
|
1084
|
+
defaults_action = columns_menu.addAction("Restore defaults")
|
|
1085
|
+
defaults_action.triggered.connect(self.reset_column_visibility)
|
|
1086
|
+
|
|
1087
|
+
menu.addSeparator()
|
|
1088
|
+
reset_action = menu.addAction("Reset column widths")
|
|
1089
|
+
reset_action.triggered.connect(self.reset_column_widths)
|
|
1090
|
+
return menu
|
|
1091
|
+
|
|
1092
|
+
def _toggle_column_from_action(self, column_id, visible, action):
|
|
1093
|
+
if not self.set_column_visible(column_id, visible):
|
|
1094
|
+
action.setChecked(column_id in self.visible_column_ids())
|
|
1095
|
+
|
|
1096
|
+
def _open_header_menu(self, pos):
|
|
1097
|
+
header = self.header()
|
|
1098
|
+
if header is None:
|
|
1099
|
+
return
|
|
1100
|
+
menu = self._build_header_menu()
|
|
1101
|
+
menu.exec(header.mapToGlobal(pos))
|
|
1102
|
+
|
|
1103
|
+
def _resize_columns(self, force=False):
|
|
1104
|
+
if self._manual_column_widths and not force:
|
|
1105
|
+
return
|
|
1106
|
+
|
|
1107
|
+
header = self.header()
|
|
1108
|
+
if header is not None:
|
|
1109
|
+
header.setStretchLastSection(False)
|
|
1110
|
+
header.setMinimumSectionSize(32)
|
|
1111
|
+
|
|
1112
|
+
for col in range(len(self.cols)):
|
|
1113
|
+
header.setSectionResizeMode(col, qtw.QHeaderView.ResizeMode.Interactive)
|
|
1114
|
+
|
|
1115
|
+
viewport = self.viewport()
|
|
1116
|
+
available_width = viewport.width() if viewport is not None else 0
|
|
1117
|
+
visible_names = self.visible_columns()
|
|
1118
|
+
if not visible_names:
|
|
1119
|
+
return
|
|
1120
|
+
|
|
1121
|
+
preferred_widths = {
|
|
1122
|
+
name: width
|
|
1123
|
+
for name, width in self._preferred_column_widths().items()
|
|
1124
|
+
if name in visible_names
|
|
1125
|
+
}
|
|
1126
|
+
preferred_width = sum(preferred_widths.values())
|
|
1127
|
+
if available_width <= 0:
|
|
1128
|
+
available_width = preferred_width
|
|
1129
|
+
|
|
1130
|
+
if available_width < preferred_width:
|
|
1131
|
+
readable_widths = {
|
|
1132
|
+
name: width
|
|
1133
|
+
for name, width in self.readable_column_widths.items()
|
|
1134
|
+
if name in visible_names
|
|
1135
|
+
}
|
|
1136
|
+
minimum_widths = {
|
|
1137
|
+
name: width
|
|
1138
|
+
for name, width in self.minimum_column_widths.items()
|
|
1139
|
+
if name in visible_names
|
|
1140
|
+
}
|
|
1141
|
+
readable_width = sum(readable_widths.values())
|
|
1142
|
+
if available_width < readable_width:
|
|
1143
|
+
widths = self._grow_column_widths(
|
|
1144
|
+
minimum_widths,
|
|
1145
|
+
readable_widths,
|
|
1146
|
+
available_width,
|
|
1147
|
+
[
|
|
1148
|
+
name
|
|
1149
|
+
for name in self.compact_growth_order
|
|
1150
|
+
if name in visible_names
|
|
1151
|
+
],
|
|
1152
|
+
)
|
|
1153
|
+
else:
|
|
1154
|
+
widths = self._grow_column_widths(
|
|
1155
|
+
readable_widths,
|
|
1156
|
+
preferred_widths,
|
|
1157
|
+
available_width,
|
|
1158
|
+
[
|
|
1159
|
+
name
|
|
1160
|
+
for name in self.preferred_growth_order
|
|
1161
|
+
if name in visible_names
|
|
1162
|
+
],
|
|
1163
|
+
)
|
|
1164
|
+
else:
|
|
1165
|
+
extra_width = max(0, available_width - preferred_width)
|
|
1166
|
+
widths = dict(preferred_widths)
|
|
1167
|
+
if "Setpoints" in widths and "Started" in widths:
|
|
1168
|
+
setpoints_extra = (extra_width * 2) // 3
|
|
1169
|
+
widths["Setpoints"] += setpoints_extra
|
|
1170
|
+
widths["Started"] += extra_width - setpoints_extra
|
|
1171
|
+
else:
|
|
1172
|
+
elastic_name = next(
|
|
1173
|
+
(
|
|
1174
|
+
name
|
|
1175
|
+
for name in (
|
|
1176
|
+
"Setpoints",
|
|
1177
|
+
"Name",
|
|
1178
|
+
"Started",
|
|
1179
|
+
"Experiment",
|
|
1180
|
+
"Sample",
|
|
1181
|
+
"GUID",
|
|
1182
|
+
)
|
|
1183
|
+
if name in widths
|
|
1184
|
+
),
|
|
1185
|
+
visible_names[-1],
|
|
1186
|
+
)
|
|
1187
|
+
widths[elastic_name] += extra_width
|
|
1188
|
+
|
|
1189
|
+
self._resizing_columns = True
|
|
1190
|
+
try:
|
|
1191
|
+
for name, width in widths.items():
|
|
1192
|
+
self._column_width_cache[name] = width
|
|
1193
|
+
self.setColumnWidth(self.cols.index(name), width)
|
|
1194
|
+
finally:
|
|
1195
|
+
self._resizing_columns = False
|
|
1196
|
+
|
|
1197
|
+
def _preferred_column_widths(self):
|
|
1198
|
+
"""Return roomy widths adjusted for the active platform font."""
|
|
1199
|
+
widths = {
|
|
1200
|
+
**self.elastic_column_widths,
|
|
1201
|
+
**self.column_widths,
|
|
1202
|
+
}
|
|
1203
|
+
metrics = QtGui.QFontMetrics(self.font())
|
|
1204
|
+
for name, value in self.representative_column_values.items():
|
|
1205
|
+
widths[name] = max(widths[name], metrics.horizontalAdvance(value) + 12)
|
|
1206
|
+
return widths
|
|
1207
|
+
|
|
1208
|
+
def _grow_column_widths(self, base_widths, target_widths, available_width, order):
|
|
1209
|
+
widths = dict(base_widths)
|
|
1210
|
+
deficit = sum(widths.values()) - available_width
|
|
1211
|
+
if deficit > 0:
|
|
1212
|
+
return widths
|
|
1213
|
+
|
|
1214
|
+
extra_width = max(0, available_width - sum(widths.values()))
|
|
1215
|
+
for name in order:
|
|
1216
|
+
target = target_widths.get(name, widths.get(name, 0))
|
|
1217
|
+
grow_by = min(max(0, target - widths.get(name, 0)), extra_width)
|
|
1218
|
+
widths[name] = widths.get(name, 0) + grow_by
|
|
1219
|
+
extra_width -= grow_by
|
|
1220
|
+
if extra_width <= 0:
|
|
1221
|
+
break
|
|
1222
|
+
return widths
|
|
1223
|
+
|
|
1224
|
+
def resizeEvent(self, event):
|
|
1225
|
+
super().resizeEvent(event)
|
|
1226
|
+
self._resize_columns()
|
|
1227
|
+
|
|
1228
|
+
def setRuns(self, runs):
|
|
1229
|
+
"""
|
|
1230
|
+
Reset the table from controller-supplied plain run metadata.
|
|
1231
|
+
|
|
1232
|
+
"""
|
|
1233
|
+
self.clear()
|
|
1234
|
+
self.watching = []
|
|
1235
|
+
self.preview_cells.clear()
|
|
1236
|
+
self._items_by_guid.clear()
|
|
1237
|
+
self.maxRunId = 0
|
|
1238
|
+
|
|
1239
|
+
self.addRuns(runs)
|
|
1240
|
+
return runs
|
|
1241
|
+
|
|
1242
|
+
def all_run_metadata(self):
|
|
1243
|
+
runs = {}
|
|
1244
|
+
for index in range(self.topLevelItemCount()):
|
|
1245
|
+
item = self.topLevelItem(index)
|
|
1246
|
+
if item is None:
|
|
1247
|
+
continue
|
|
1248
|
+
|
|
1249
|
+
run_id: int | str
|
|
1250
|
+
try:
|
|
1251
|
+
run_id = int(item.text(0))
|
|
1252
|
+
except ValueError:
|
|
1253
|
+
run_id = item.text(0)
|
|
1254
|
+
|
|
1255
|
+
runs[run_id] = dict(getattr(item, "run_metadata", {}))
|
|
1256
|
+
return runs
|
|
1257
|
+
|
|
1258
|
+
def visible_run_ids(self, limit=50):
|
|
1259
|
+
run_ids: list[int | str] = []
|
|
1260
|
+
viewport = self.viewport()
|
|
1261
|
+
if viewport is None:
|
|
1262
|
+
return run_ids
|
|
1263
|
+
|
|
1264
|
+
viewport_rect = viewport.rect()
|
|
1265
|
+
for index in range(self.topLevelItemCount()):
|
|
1266
|
+
item = self.topLevelItem(index)
|
|
1267
|
+
if item is None:
|
|
1268
|
+
continue
|
|
1269
|
+
|
|
1270
|
+
rect = self.visualItemRect(item)
|
|
1271
|
+
if not rect.isValid():
|
|
1272
|
+
continue
|
|
1273
|
+
if rect.bottom() < viewport_rect.top():
|
|
1274
|
+
continue
|
|
1275
|
+
if rect.top() > viewport_rect.bottom():
|
|
1276
|
+
if run_ids:
|
|
1277
|
+
break
|
|
1278
|
+
continue
|
|
1279
|
+
|
|
1280
|
+
run_id = self._item_run_id(item)
|
|
1281
|
+
if run_id is None:
|
|
1282
|
+
continue
|
|
1283
|
+
run_ids.append(run_id)
|
|
1284
|
+
if len(run_ids) >= limit:
|
|
1285
|
+
break
|
|
1286
|
+
return run_ids
|
|
1287
|
+
|
|
1288
|
+
def selected_run_ids(self):
|
|
1289
|
+
run_ids = []
|
|
1290
|
+
for item in self.selectedItems():
|
|
1291
|
+
if isinstance(item, SortableTreeWidgetItem):
|
|
1292
|
+
run_id = self._item_run_id(item)
|
|
1293
|
+
if run_id is not None:
|
|
1294
|
+
run_ids.append(run_id)
|
|
1295
|
+
return run_ids
|
|
1296
|
+
|
|
1297
|
+
def run_id_for_guid(self, guid):
|
|
1298
|
+
item = self._item_for_guid(guid)
|
|
1299
|
+
if item is None:
|
|
1300
|
+
return None
|
|
1301
|
+
return self._item_run_id(item)
|
|
1302
|
+
|
|
1303
|
+
def _item_run_id(self, item):
|
|
1304
|
+
try:
|
|
1305
|
+
return int(item.text(0))
|
|
1306
|
+
except (TypeError, ValueError):
|
|
1307
|
+
text = item.text(0)
|
|
1308
|
+
return text if text else None
|
|
1309
|
+
|
|
1310
|
+
def checkWatching(self, statuses):
|
|
1311
|
+
"""
|
|
1312
|
+
Apply controller-supplied statuses to unfinished runs.
|
|
1313
|
+
|
|
1314
|
+
"""
|
|
1315
|
+
statuses = statuses or {}
|
|
1316
|
+
to_remove = []
|
|
1317
|
+
updated_runs = {}
|
|
1318
|
+
for run in self.watching:
|
|
1319
|
+
status = statuses.get(run.guid, {})
|
|
1320
|
+
if not status:
|
|
1321
|
+
continue
|
|
1322
|
+
|
|
1323
|
+
if status.get("database_modified_timestamp") is not None:
|
|
1324
|
+
run.run_metadata["database_modified_timestamp"] = status[
|
|
1325
|
+
"database_modified_timestamp"
|
|
1326
|
+
]
|
|
1327
|
+
|
|
1328
|
+
if status.get("run_timestamp") is not None:
|
|
1329
|
+
run.run_metadata["run_timestamp"] = status["run_timestamp"]
|
|
1330
|
+
|
|
1331
|
+
if status.get("is_completed") is not None:
|
|
1332
|
+
run.run_metadata["is_completed"] = bool(status["is_completed"])
|
|
1333
|
+
|
|
1334
|
+
shape_metadata_changed = False
|
|
1335
|
+
for field in (
|
|
1336
|
+
"point_shape",
|
|
1337
|
+
"setpoint_shape",
|
|
1338
|
+
"setpoint_shape_source",
|
|
1339
|
+
"setpoint_count",
|
|
1340
|
+
"setpoint_count_source",
|
|
1341
|
+
"expected_results",
|
|
1342
|
+
"expected_results_source",
|
|
1343
|
+
):
|
|
1344
|
+
if field not in status:
|
|
1345
|
+
continue
|
|
1346
|
+
if run.run_metadata.get(field) != status[field]:
|
|
1347
|
+
shape_metadata_changed = True
|
|
1348
|
+
# None is meaningful here: it clears a stale early inference.
|
|
1349
|
+
run.run_metadata[field] = status[field]
|
|
1350
|
+
|
|
1351
|
+
if status.get("result_count") is not None:
|
|
1352
|
+
run.run_metadata["result_count"] = status["result_count"]
|
|
1353
|
+
|
|
1354
|
+
if status.get("result_count") is not None or shape_metadata_changed:
|
|
1355
|
+
points_col = self.cols.index("Setpoints")
|
|
1356
|
+
run.setText(points_col, format_point_count(run.run_metadata))
|
|
1357
|
+
run.setData(
|
|
1358
|
+
points_col,
|
|
1359
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1360
|
+
point_count_sort_value(run.run_metadata),
|
|
1361
|
+
)
|
|
1362
|
+
complete_col = self.cols.index("Status")
|
|
1363
|
+
run.setText(complete_col, format_complete_cell(run.run_metadata))
|
|
1364
|
+
run.setData(
|
|
1365
|
+
complete_col,
|
|
1366
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1367
|
+
progress_percent_value(run.run_metadata),
|
|
1368
|
+
)
|
|
1369
|
+
time_taken_col = self.cols.index("Duration")
|
|
1370
|
+
run.setText(time_taken_col, format_time_taken_seconds(run.run_metadata))
|
|
1371
|
+
run.setData(
|
|
1372
|
+
time_taken_col,
|
|
1373
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1374
|
+
time_taken_seconds(run.run_metadata),
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
completion_metadata_changed = False
|
|
1378
|
+
if "read_setpoint_count" in status:
|
|
1379
|
+
run.run_metadata["read_setpoint_count"] = status["read_setpoint_count"]
|
|
1380
|
+
completion_metadata_changed = True
|
|
1381
|
+
|
|
1382
|
+
if status.get("measurement_exception") is not None:
|
|
1383
|
+
run.run_metadata["measurement_exception"] = status[
|
|
1384
|
+
"measurement_exception"
|
|
1385
|
+
]
|
|
1386
|
+
completion_metadata_changed = True
|
|
1387
|
+
|
|
1388
|
+
if completion_metadata_changed:
|
|
1389
|
+
complete_col = self.cols.index("Status")
|
|
1390
|
+
run.setText(complete_col, format_complete_cell(run.run_metadata))
|
|
1391
|
+
run.setData(
|
|
1392
|
+
complete_col,
|
|
1393
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1394
|
+
complete_cell_sort_value(run.run_metadata),
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
if status.get("storage_bytes") is not None:
|
|
1398
|
+
storage_col = self.cols.index("Size")
|
|
1399
|
+
run.run_metadata["storage_bytes"] = status["storage_bytes"]
|
|
1400
|
+
run.setText(storage_col, format_storage_size(status["storage_bytes"]))
|
|
1401
|
+
run.setData(
|
|
1402
|
+
storage_col,
|
|
1403
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1404
|
+
status["storage_bytes"],
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
completed_timestamp = status.get("completed_timestamp")
|
|
1408
|
+
if completed_timestamp is not None:
|
|
1409
|
+
run.run_metadata["completed_timestamp"] = completed_timestamp
|
|
1410
|
+
|
|
1411
|
+
if run_is_complete(run.run_metadata):
|
|
1412
|
+
complete_col = self.cols.index("Status")
|
|
1413
|
+
run.setText(complete_col, format_complete_cell(run.run_metadata))
|
|
1414
|
+
run.setData(
|
|
1415
|
+
complete_col,
|
|
1416
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1417
|
+
complete_cell_sort_value(run.run_metadata),
|
|
1418
|
+
)
|
|
1419
|
+
time_taken_col = self.cols.index("Duration")
|
|
1420
|
+
run.setText(time_taken_col, format_time_taken_seconds(run.run_metadata))
|
|
1421
|
+
run.setData(
|
|
1422
|
+
time_taken_col,
|
|
1423
|
+
QtCore.Qt.ItemDataRole.UserRole,
|
|
1424
|
+
time_taken_seconds(run.run_metadata),
|
|
1425
|
+
)
|
|
1426
|
+
to_remove.append(run)
|
|
1427
|
+
|
|
1428
|
+
self._refresh_metadata_columns(run)
|
|
1429
|
+
run.update_tooltip()
|
|
1430
|
+
run_id: int | str
|
|
1431
|
+
try:
|
|
1432
|
+
run_id = int(run.text(0))
|
|
1433
|
+
except ValueError:
|
|
1434
|
+
run_id = run.text(0)
|
|
1435
|
+
updated_runs[run_id] = dict(run.run_metadata)
|
|
1436
|
+
|
|
1437
|
+
# Remove runs outside for loops to prevent interfering with loop indexing
|
|
1438
|
+
for run in to_remove:
|
|
1439
|
+
self.watching.remove(run)
|
|
1440
|
+
|
|
1441
|
+
return updated_runs
|
|
1442
|
+
|
|
1443
|
+
@QtCore.pyqtSlot(QtCore.QPoint)
|
|
1444
|
+
def prepareMenu(self, pos):
|
|
1445
|
+
"""
|
|
1446
|
+
Produces the context menu at mouse position on right click.
|
|
1447
|
+
Allows user to open specific plots from the selected run.
|
|
1448
|
+
|
|
1449
|
+
Selects the row under the pointer before building actions so every menu
|
|
1450
|
+
command targets the row that was actually clicked.
|
|
1451
|
+
|
|
1452
|
+
Parameters
|
|
1453
|
+
----------
|
|
1454
|
+
pos : PyQt6.QtCore.QPoint
|
|
1455
|
+
The cursor position to open the menu at.
|
|
1456
|
+
|
|
1457
|
+
"""
|
|
1458
|
+
main = self.main_window()
|
|
1459
|
+
if main is None:
|
|
1460
|
+
return
|
|
1461
|
+
|
|
1462
|
+
item = self.itemAt(pos)
|
|
1463
|
+
if item is None:
|
|
1464
|
+
main.show_status("Right-click a run to open its plot menu.", 3000)
|
|
1465
|
+
return
|
|
1466
|
+
item = cast(qtw.QTreeWidgetItem, item)
|
|
1467
|
+
while True:
|
|
1468
|
+
parent = item.parent()
|
|
1469
|
+
if parent is None:
|
|
1470
|
+
break
|
|
1471
|
+
item = parent
|
|
1472
|
+
if not isinstance(item, SortableTreeWidgetItem):
|
|
1473
|
+
return
|
|
1474
|
+
|
|
1475
|
+
if self.currentItem() is not item or item not in self.selectedItems():
|
|
1476
|
+
self.clearSelection()
|
|
1477
|
+
self.setCurrentItem(item)
|
|
1478
|
+
item.setSelected(True)
|
|
1479
|
+
|
|
1480
|
+
menu = qtw.QMenu(self)
|
|
1481
|
+
|
|
1482
|
+
open_all = create_action(
|
|
1483
|
+
"run.plot_selected_all",
|
|
1484
|
+
menu,
|
|
1485
|
+
text="&Plot all",
|
|
1486
|
+
)
|
|
1487
|
+
self._set_action_shortcut(open_all, "run.plot_selected_all")
|
|
1488
|
+
open_all.triggered.connect(lambda _,: main.open_selected_run_all())
|
|
1489
|
+
menu.addAction(open_all)
|
|
1490
|
+
|
|
1491
|
+
if main.ds is None:
|
|
1492
|
+
parameter_targets = [
|
|
1493
|
+
(str(name), None)
|
|
1494
|
+
for name in item.run_metadata.get("measure_parameters", ())
|
|
1495
|
+
if name
|
|
1496
|
+
]
|
|
1497
|
+
else:
|
|
1498
|
+
parameter_targets = [
|
|
1499
|
+
(param.name, param)
|
|
1500
|
+
for param in main.ds.get_parameters()
|
|
1501
|
+
if param.depends_on
|
|
1502
|
+
]
|
|
1503
|
+
|
|
1504
|
+
# Create an action for all dependant parameters in the loaded dataset,
|
|
1505
|
+
# linking the coresponding parameter to the openPlot.
|
|
1506
|
+
for itr, (parameter_name, param) in enumerate(parameter_targets):
|
|
1507
|
+
open_win = QtGui.QAction(f" - {parameter_name}", menu)
|
|
1508
|
+
if itr < 9:
|
|
1509
|
+
self._set_action_shortcut(
|
|
1510
|
+
open_win,
|
|
1511
|
+
plot_measurement_command_spec(itr),
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
# Due to the for loop, the lambda function sets param as an optional
|
|
1515
|
+
# default. Otherwise, param is set by the last iteration of the for loop.
|
|
1516
|
+
# This will be done a few times through the program but this note
|
|
1517
|
+
# may be missing
|
|
1518
|
+
if param is None:
|
|
1519
|
+
open_win.triggered.connect(
|
|
1520
|
+
lambda _, name=parameter_name: main.open_selected_measurement(name)
|
|
1521
|
+
)
|
|
1522
|
+
else:
|
|
1523
|
+
open_win.triggered.connect(
|
|
1524
|
+
lambda _, selected_param=param: main.openPlot(
|
|
1525
|
+
params=[selected_param]
|
|
1526
|
+
)
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
menu.addAction(open_win)
|
|
1530
|
+
|
|
1531
|
+
# Display context menu
|
|
1532
|
+
menu.exec(self.mapToGlobal(pos))
|
|
1533
|
+
|
|
1534
|
+
@QtCore.pyqtSlot()
|
|
1535
|
+
def openKeyboardMenu(self):
|
|
1536
|
+
"""
|
|
1537
|
+
Opens the run context menu from the keyboard.
|
|
1538
|
+
|
|
1539
|
+
"""
|
|
1540
|
+
item = self.currentItem()
|
|
1541
|
+
pos = self.visualItemRect(item).center() if item else self.rect().center()
|
|
1542
|
+
self.prepareMenu(pos)
|
|
1543
|
+
|
|
1544
|
+
def main_window(self):
|
|
1545
|
+
"""
|
|
1546
|
+
Returns the owning main window regardless of intermediate layouts.
|
|
1547
|
+
|
|
1548
|
+
"""
|
|
1549
|
+
window = self.window()
|
|
1550
|
+
if hasattr(window, "ds") and hasattr(window, "openPlot"):
|
|
1551
|
+
return window
|
|
1552
|
+
|
|
1553
|
+
parent = self.parentWidget()
|
|
1554
|
+
while parent is not None:
|
|
1555
|
+
if hasattr(parent, "ds") and hasattr(parent, "openPlot"):
|
|
1556
|
+
return parent
|
|
1557
|
+
parent = parent.parentWidget()
|
|
1558
|
+
|
|
1559
|
+
return None
|
|
1560
|
+
|
|
1561
|
+
def _set_action_shortcut(self, action, command):
|
|
1562
|
+
"""
|
|
1563
|
+
Sets a context-menu action shortcut.
|
|
1564
|
+
|
|
1565
|
+
"""
|
|
1566
|
+
configure_action(action, command)
|
|
1567
|
+
action.setShortcutContext(QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
|
1568
|
+
if hasattr(action, "setShortcutVisibleInContextMenu"):
|
|
1569
|
+
action.setShortcutVisibleInContextMenu(True)
|
|
1570
|
+
|
|
1571
|
+
@QtCore.pyqtSlot()
|
|
1572
|
+
def onSelect(self):
|
|
1573
|
+
"""
|
|
1574
|
+
Event handler for right/click on table.
|
|
1575
|
+
This emits a signal connected to:
|
|
1576
|
+
qplot.windows.main.MainWindow.updateSelected()
|
|
1577
|
+
for further loading.
|
|
1578
|
+
|
|
1579
|
+
Returns
|
|
1580
|
+
-------
|
|
1581
|
+
None.
|
|
1582
|
+
|
|
1583
|
+
"""
|
|
1584
|
+
if len(self.selectedItems()) == 1: # Check multiple items are not selected
|
|
1585
|
+
item = self.selectedItems()[0]
|
|
1586
|
+
if isinstance(item, SortableTreeWidgetItem):
|
|
1587
|
+
self.selected.emit(item.guid)
|
|
1588
|
+
return
|
|
1589
|
+
|
|
1590
|
+
# A zero- or multiple-row selection must explicitly invalidate the
|
|
1591
|
+
# MainWindow's single-run action target. Previously it emitted no
|
|
1592
|
+
# signal, leaving the previous single selection available to plot and
|
|
1593
|
+
# CSV actions even though it was no longer visible in the run list.
|
|
1594
|
+
self.nonSingleSelection.emit()
|
|
1595
|
+
|
|
1596
|
+
@QtCore.pyqtSlot(qtw.QTreeWidgetItem, int)
|
|
1597
|
+
def _double_clicked(self, item, column):
|
|
1598
|
+
"""
|
|
1599
|
+
Emits a signal to tell qplot.windows.main.MainWindow to open all params
|
|
1600
|
+
of selected row.
|
|
1601
|
+
|
|
1602
|
+
Parameters
|
|
1603
|
+
----------
|
|
1604
|
+
Unused but required by signal
|
|
1605
|
+
|
|
1606
|
+
"""
|
|
1607
|
+
self.plot.emit(None)
|
|
1608
|
+
|
|
1609
|
+
def add_plot(self, target_win, param):
|
|
1610
|
+
"""
|
|
1611
|
+
Event handler for add _ to _ context menu option
|
|
1612
|
+
|
|
1613
|
+
Parameters
|
|
1614
|
+
----------
|
|
1615
|
+
target_win : qplot.windows.plotWin.plotWidget
|
|
1616
|
+
The subplot will be added to this window.
|
|
1617
|
+
param : qcodes.dataset.descriptions.param_spec.ParamSpec
|
|
1618
|
+
The depandant parameter that will be added to the target_win.
|
|
1619
|
+
|
|
1620
|
+
"""
|
|
1621
|
+
main = self.main_window()
|
|
1622
|
+
if main is None:
|
|
1623
|
+
return
|
|
1624
|
+
|
|
1625
|
+
selected = self.selectedItems()
|
|
1626
|
+
if not selected:
|
|
1627
|
+
return
|
|
1628
|
+
selected_item = selected[0]
|
|
1629
|
+
if not isinstance(selected_item, SortableTreeWidgetItem):
|
|
1630
|
+
return
|
|
1631
|
+
|
|
1632
|
+
main.add_trace_to_plot(target_win, selected_item.guid, param.name, param=param)
|
|
1633
|
+
|
|
1634
|
+
def add_all(self, target_win, param_dict):
|
|
1635
|
+
"""
|
|
1636
|
+
Event handler for add _ to _ context menu all action.
|
|
1637
|
+
Add all plots which are able to be added to the target window
|
|
1638
|
+
|
|
1639
|
+
Parameters
|
|
1640
|
+
----------
|
|
1641
|
+
target_win : qplot.windows.plotWin.plotWidget
|
|
1642
|
+
The subplot will be added to this window.
|
|
1643
|
+
param_dict : dict{qcodes.dataset.descriptions.param_spec.ParamSpec}
|
|
1644
|
+
A dictionary of all parameters to try to add.
|
|
1645
|
+
|
|
1646
|
+
"""
|
|
1647
|
+
for param, depends_on in param_dict.items():
|
|
1648
|
+
if depends_on == target_win.param.depends_on_:
|
|
1649
|
+
self.add_plot(target_win, param)
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
class moreInfo(qtw.QTabWidget):
|
|
1653
|
+
snapshotPageRequested = QtCore.pyqtSignal(object)
|
|
1654
|
+
snapshotFullValueRequested = QtCore.pyqtSignal(object)
|
|
1655
|
+
|
|
1656
|
+
def __init__(self, *args, preview_size=None):
|
|
1657
|
+
super().__init__(*args)
|
|
1658
|
+
self.setObjectName("runDetailsTabs")
|
|
1659
|
+
self._trusted_full_values = {}
|
|
1660
|
+
self._full_value_dialog = None
|
|
1661
|
+
self._trusted_detail_identity = None
|
|
1662
|
+
self._trusted_snapshot_session_token = ""
|
|
1663
|
+
self._pending_snapshot_value_request = None
|
|
1664
|
+
|
|
1665
|
+
self.overview = CopyableTableWidget()
|
|
1666
|
+
self.parameters = CopyableTableWidget()
|
|
1667
|
+
self.preview = PreviewTab(preview_size=preview_size)
|
|
1668
|
+
self._update_preview_minimum_height()
|
|
1669
|
+
self.metadata = infoTree(expand_all=True, truncate_values=True)
|
|
1670
|
+
self.snapshot = infoTree(expand_all=True, truncate_values=False)
|
|
1671
|
+
self.raw = infoTree(
|
|
1672
|
+
expand_all=False,
|
|
1673
|
+
truncate_values=False,
|
|
1674
|
+
expand_top_level=True,
|
|
1675
|
+
)
|
|
1676
|
+
self.metadata.fullValueRequested.connect(self._show_trusted_full_value)
|
|
1677
|
+
self.raw.fullValueRequested.connect(self._show_trusted_full_value)
|
|
1678
|
+
self.snapshot.snapshotPageRequested.connect(self._relay_snapshot_page_request)
|
|
1679
|
+
self.snapshot.snapshotFullValueRequested.connect(
|
|
1680
|
+
self._relay_snapshot_full_value_request
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
self._setup_table(self.overview, ["Field", "Value"])
|
|
1684
|
+
self._setup_table(
|
|
1685
|
+
self.parameters,
|
|
1686
|
+
["Name", "Label", "Unit", "From", "To", "Steps", "Delay", "Instrument"],
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
self.addTab(self.overview, "Overview")
|
|
1690
|
+
self.addTab(self.parameters, "Sweep parameters")
|
|
1691
|
+
self.addTab(self.preview, "Preview")
|
|
1692
|
+
self.addTab(self.metadata, "Metadata")
|
|
1693
|
+
self.addTab(self.snapshot, "Snapshot")
|
|
1694
|
+
self.addTab(self.raw, "Raw key-value")
|
|
1695
|
+
self.currentChanged.connect(self._preview_tab_changed)
|
|
1696
|
+
self._preview_tab_changed(self.currentIndex())
|
|
1697
|
+
|
|
1698
|
+
def _preview_tab_changed(self, index):
|
|
1699
|
+
self.preview.set_preview_active(self.widget(index) is self.preview)
|
|
1700
|
+
|
|
1701
|
+
def set_preview_size(self, preview_size):
|
|
1702
|
+
self.preview.set_preview_size(preview_size)
|
|
1703
|
+
self._update_preview_minimum_height()
|
|
1704
|
+
|
|
1705
|
+
def _update_preview_minimum_height(self):
|
|
1706
|
+
preferred_height = self.preview.preferred_tab_height() + 36
|
|
1707
|
+
self.setMinimumHeight(max(1, round(preferred_height * COLLAPSE_MINIMUM_RATIO)))
|
|
1708
|
+
|
|
1709
|
+
def _setup_table(self, table, headers):
|
|
1710
|
+
table.setObjectName("detailsTable")
|
|
1711
|
+
table.setColumnCount(len(headers))
|
|
1712
|
+
table.setHorizontalHeaderLabels(headers)
|
|
1713
|
+
table.verticalHeader().hide()
|
|
1714
|
+
table.verticalHeader().setMinimumSectionSize(16)
|
|
1715
|
+
table.verticalHeader().setDefaultSectionSize(20)
|
|
1716
|
+
table.horizontalHeader().setFixedHeight(22)
|
|
1717
|
+
table.setAlternatingRowColors(True)
|
|
1718
|
+
table.setEditTriggers(qtw.QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
1719
|
+
table.setSelectionBehavior(qtw.QAbstractItemView.SelectionBehavior.SelectRows)
|
|
1720
|
+
table.setTextElideMode(QtCore.Qt.TextElideMode.ElideRight)
|
|
1721
|
+
table.setWordWrap(False)
|
|
1722
|
+
table.horizontalHeader().setStretchLastSection(True)
|
|
1723
|
+
|
|
1724
|
+
def setInfo(
|
|
1725
|
+
self,
|
|
1726
|
+
info,
|
|
1727
|
+
dataset=None,
|
|
1728
|
+
run_metadata=None,
|
|
1729
|
+
setpoint_summaries=None,
|
|
1730
|
+
):
|
|
1731
|
+
self.clear()
|
|
1732
|
+
|
|
1733
|
+
self._set_overview(info, dataset, run_metadata=run_metadata)
|
|
1734
|
+
self._set_parameters(info, dataset, setpoint_summaries)
|
|
1735
|
+
self.preview.set_current_run(dataset)
|
|
1736
|
+
self.metadata.setInfo(info.get("MetaData", {}))
|
|
1737
|
+
self.snapshot.setInfo(info.get("Snapshot", {}))
|
|
1738
|
+
self.raw.setInfo(info)
|
|
1739
|
+
|
|
1740
|
+
def set_trusted_run_detail(self, detail: TrustedSelectedRunDetail) -> None:
|
|
1741
|
+
"""Render a selected trusted run without dataset or database access."""
|
|
1742
|
+
self._set_plain_run_detail(detail, preview_guid=None)
|
|
1743
|
+
|
|
1744
|
+
def set_trusted_derived_metadata(
|
|
1745
|
+
self,
|
|
1746
|
+
run_metadata,
|
|
1747
|
+
parameters,
|
|
1748
|
+
setpoint_summaries,
|
|
1749
|
+
metadata,
|
|
1750
|
+
) -> None:
|
|
1751
|
+
"""Render one Stage 5B metadata publication without database access."""
|
|
1752
|
+
|
|
1753
|
+
run_metadata = self._trusted_run_metadata(run_metadata)
|
|
1754
|
+
guid = run_metadata.get("guid")
|
|
1755
|
+
self.clear()
|
|
1756
|
+
self._set_trusted_overview(run_metadata, parameters)
|
|
1757
|
+
self._set_trusted_parameters(
|
|
1758
|
+
tuple(parameters or ()),
|
|
1759
|
+
tuple(setpoint_summaries or ()),
|
|
1760
|
+
{},
|
|
1761
|
+
)
|
|
1762
|
+
self.preview.set_current_guid(guid)
|
|
1763
|
+
self.metadata.setBoundedView(normalize_presentation_tree(metadata or {}))
|
|
1764
|
+
unavailable = normalize_presentation_tree(
|
|
1765
|
+
{
|
|
1766
|
+
"Status": (
|
|
1767
|
+
"Snapshot detail is not part of bounded background derived work."
|
|
1768
|
+
)
|
|
1769
|
+
}
|
|
1770
|
+
)
|
|
1771
|
+
self.snapshot.setBoundedView(unavailable)
|
|
1772
|
+
self.raw.setBoundedView(
|
|
1773
|
+
normalize_presentation_tree(
|
|
1774
|
+
{
|
|
1775
|
+
"Run": run_metadata,
|
|
1776
|
+
"Derived metadata": metadata or {},
|
|
1777
|
+
}
|
|
1778
|
+
)
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
def set_snapshot_run_detail(self, detail: TrustedSelectedRunDetail) -> None:
|
|
1782
|
+
"""Render snapshot detail primitives and preview by plain GUID."""
|
|
1783
|
+
if not isinstance(detail, TrustedSelectedRunDetail):
|
|
1784
|
+
raise TypeError("detail must be a TrustedSelectedRunDetail.")
|
|
1785
|
+
guid = detail.run.as_dict().get("guid")
|
|
1786
|
+
self._set_plain_run_detail(detail, preview_guid=guid)
|
|
1787
|
+
|
|
1788
|
+
def _set_plain_run_detail(self, detail, *, preview_guid) -> None:
|
|
1789
|
+
if not isinstance(detail, TrustedSelectedRunDetail):
|
|
1790
|
+
raise TypeError("detail must be a TrustedSelectedRunDetail.")
|
|
1791
|
+
|
|
1792
|
+
detail_run = detail.run.as_dict()
|
|
1793
|
+
detail_guid = str(detail_run.get("guid") or "")
|
|
1794
|
+
detail_identity = (detail.run.run_id, detail_guid)
|
|
1795
|
+
raw_expanded = (
|
|
1796
|
+
self.raw.expandedPaths()
|
|
1797
|
+
if self._trusted_detail_identity == detail_identity
|
|
1798
|
+
else None
|
|
1799
|
+
)
|
|
1800
|
+
self.clear()
|
|
1801
|
+
self._trusted_detail_identity = detail_identity
|
|
1802
|
+
self._trusted_full_values = {
|
|
1803
|
+
value.identifier: value for value in detail.presentation.full_values
|
|
1804
|
+
}
|
|
1805
|
+
run_metadata = detail_run
|
|
1806
|
+
run_metadata.setdefault("run_id", detail.run.run_id)
|
|
1807
|
+
snapshot = detail.snapshot
|
|
1808
|
+
presentation = detail.presentation
|
|
1809
|
+
snapshot_parameters = {
|
|
1810
|
+
parameter.name: dict(parameter.fields) for parameter in snapshot.parameters
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
self._set_trusted_overview(run_metadata, detail.parameters)
|
|
1814
|
+
self._set_trusted_parameters(
|
|
1815
|
+
detail.parameters,
|
|
1816
|
+
detail.setpoint_summaries,
|
|
1817
|
+
snapshot_parameters,
|
|
1818
|
+
parameters_truncated=presentation.parameters_truncated,
|
|
1819
|
+
)
|
|
1820
|
+
if preview_guid is None:
|
|
1821
|
+
self.preview.show_trusted_live_placeholder()
|
|
1822
|
+
else:
|
|
1823
|
+
self.preview.set_current_guid(preview_guid)
|
|
1824
|
+
self.metadata.setBoundedView(presentation.metadata)
|
|
1825
|
+
self.snapshot.setBoundedView(snapshot, expand_all=False)
|
|
1826
|
+
self._trusted_snapshot_session_token = self.snapshot.snapshotSessionToken
|
|
1827
|
+
self.raw.setBoundedView(
|
|
1828
|
+
presentation.raw,
|
|
1829
|
+
expanded_paths=raw_expanded,
|
|
1830
|
+
)
|
|
1831
|
+
self.snapshot.expandDefaultSnapshotContainer()
|
|
1832
|
+
|
|
1833
|
+
@QtCore.pyqtSlot(str, str)
|
|
1834
|
+
def _show_trusted_full_value(self, identifier, path) -> None:
|
|
1835
|
+
value = self._trusted_full_values.get(str(identifier))
|
|
1836
|
+
if value is None:
|
|
1837
|
+
return
|
|
1838
|
+
dialog = self._full_value_dialog
|
|
1839
|
+
if dialog is None:
|
|
1840
|
+
dialog = TrustedFullValueDialog(self)
|
|
1841
|
+
self._full_value_dialog = dialog
|
|
1842
|
+
dialog.show_value(
|
|
1843
|
+
path=str(path),
|
|
1844
|
+
text=value.text,
|
|
1845
|
+
utf8_bytes=value.utf8_bytes,
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
@QtCore.pyqtSlot(object)
|
|
1849
|
+
def _relay_snapshot_page_request(self, request) -> None:
|
|
1850
|
+
if (
|
|
1851
|
+
isinstance(request, SnapshotTreePageRequest)
|
|
1852
|
+
and request.session_token
|
|
1853
|
+
and request.session_token == self._trusted_snapshot_session_token
|
|
1854
|
+
):
|
|
1855
|
+
self.snapshotPageRequested.emit(request)
|
|
1856
|
+
|
|
1857
|
+
@QtCore.pyqtSlot(object)
|
|
1858
|
+
def _relay_snapshot_full_value_request(self, request) -> None:
|
|
1859
|
+
if (
|
|
1860
|
+
not isinstance(request, SnapshotTreeValueRequest)
|
|
1861
|
+
or not request.session_token
|
|
1862
|
+
or request.session_token != self._trusted_snapshot_session_token
|
|
1863
|
+
or self._pending_snapshot_value_request is not None
|
|
1864
|
+
):
|
|
1865
|
+
return
|
|
1866
|
+
self._pending_snapshot_value_request = request
|
|
1867
|
+
self.snapshotFullValueRequested.emit(request)
|
|
1868
|
+
|
|
1869
|
+
def accept_snapshot_page(self, page) -> bool:
|
|
1870
|
+
"""Publish one already-bounded Snapshot page on the Qt owner thread."""
|
|
1871
|
+
|
|
1872
|
+
if QtCore.QThread.currentThread() != self.thread():
|
|
1873
|
+
raise RuntimeError("Snapshot page publication must run on the Qt thread.")
|
|
1874
|
+
return self.snapshot.acceptSnapshotPage(page)
|
|
1875
|
+
|
|
1876
|
+
def reject_snapshot_page(self, request) -> bool:
|
|
1877
|
+
"""Release one failed page request without exposing stale content."""
|
|
1878
|
+
|
|
1879
|
+
return self.reject_snapshot_request(request)
|
|
1880
|
+
|
|
1881
|
+
def reject_snapshot_request(self, request) -> bool:
|
|
1882
|
+
"""Release one exact failed Snapshot request without changing content."""
|
|
1883
|
+
|
|
1884
|
+
if QtCore.QThread.currentThread() != self.thread():
|
|
1885
|
+
raise RuntimeError("Snapshot request rejection must run on the Qt thread.")
|
|
1886
|
+
if isinstance(request, SnapshotTreePageRequest):
|
|
1887
|
+
return self.snapshot.rejectSnapshotPage(request)
|
|
1888
|
+
if (
|
|
1889
|
+
isinstance(request, SnapshotTreeValueRequest)
|
|
1890
|
+
and request == self._pending_snapshot_value_request
|
|
1891
|
+
and request.session_token == self._trusted_snapshot_session_token
|
|
1892
|
+
):
|
|
1893
|
+
self._pending_snapshot_value_request = None
|
|
1894
|
+
return True
|
|
1895
|
+
return False
|
|
1896
|
+
|
|
1897
|
+
def show_snapshot_full_value(
|
|
1898
|
+
self,
|
|
1899
|
+
request: SnapshotTreeValueRequest,
|
|
1900
|
+
result,
|
|
1901
|
+
) -> bool:
|
|
1902
|
+
"""Show exact Snapshot text only after its complete request fence wins."""
|
|
1903
|
+
|
|
1904
|
+
if QtCore.QThread.currentThread() != self.thread():
|
|
1905
|
+
raise RuntimeError("Snapshot value publication must run on the Qt thread.")
|
|
1906
|
+
if (
|
|
1907
|
+
not isinstance(request, SnapshotTreeValueRequest)
|
|
1908
|
+
or request != self._pending_snapshot_value_request
|
|
1909
|
+
or request.session_token != self._trusted_snapshot_session_token
|
|
1910
|
+
):
|
|
1911
|
+
return False
|
|
1912
|
+
text = getattr(result, "text", None)
|
|
1913
|
+
utf8_bytes = getattr(result, "utf8_bytes", None)
|
|
1914
|
+
if not isinstance(text, str) or type(utf8_bytes) is not int or utf8_bytes < 0:
|
|
1915
|
+
return False
|
|
1916
|
+
try:
|
|
1917
|
+
if len(text.encode("utf-8")) != utf8_bytes:
|
|
1918
|
+
return False
|
|
1919
|
+
except UnicodeEncodeError:
|
|
1920
|
+
return False
|
|
1921
|
+
self._pending_snapshot_value_request = None
|
|
1922
|
+
dialog = self._full_value_dialog
|
|
1923
|
+
if dialog is None:
|
|
1924
|
+
dialog = TrustedFullValueDialog(self)
|
|
1925
|
+
self._full_value_dialog = dialog
|
|
1926
|
+
dialog.show_value(
|
|
1927
|
+
path=request.path,
|
|
1928
|
+
text=text,
|
|
1929
|
+
utf8_bytes=utf8_bytes,
|
|
1930
|
+
)
|
|
1931
|
+
return True
|
|
1932
|
+
|
|
1933
|
+
def invalidate_trusted_snapshot(self) -> None:
|
|
1934
|
+
"""Drop the active Snapshot session, its items, and any exact viewer."""
|
|
1935
|
+
|
|
1936
|
+
if QtCore.QThread.currentThread() != self.thread():
|
|
1937
|
+
raise RuntimeError("Snapshot invalidation must run on the Qt thread.")
|
|
1938
|
+
self._trusted_snapshot_session_token = ""
|
|
1939
|
+
self._pending_snapshot_value_request = None
|
|
1940
|
+
self.snapshot.invalidateSnapshot()
|
|
1941
|
+
dialog = self._full_value_dialog
|
|
1942
|
+
self._full_value_dialog = None
|
|
1943
|
+
if dialog is not None:
|
|
1944
|
+
dialog.discard_value()
|
|
1945
|
+
dialog.deleteLater()
|
|
1946
|
+
|
|
1947
|
+
def invalidate_trusted_full_values(self) -> None:
|
|
1948
|
+
"""Drop the sole selected-detail backing and any open exact viewer."""
|
|
1949
|
+
|
|
1950
|
+
self._trusted_full_values = {}
|
|
1951
|
+
self.invalidate_trusted_snapshot()
|
|
1952
|
+
for tree in (self.metadata, self.raw):
|
|
1953
|
+
iterator = qtw.QTreeWidgetItemIterator(tree)
|
|
1954
|
+
while iterator.value() is not None:
|
|
1955
|
+
item = iterator.value()
|
|
1956
|
+
item.setData(0, FULL_VALUE_ID_ROLE, None)
|
|
1957
|
+
item.setData(1, FULL_VALUE_ID_ROLE, None)
|
|
1958
|
+
iterator += 1
|
|
1959
|
+
|
|
1960
|
+
def set_trusted_run_loading(
|
|
1961
|
+
self,
|
|
1962
|
+
run: TrustedRunRecord | Mapping[str, object] | None = None,
|
|
1963
|
+
) -> None:
|
|
1964
|
+
"""Render cached basics and loading placeholders without I/O."""
|
|
1965
|
+
run_metadata = self._trusted_run_metadata(run)
|
|
1966
|
+
self.clear()
|
|
1967
|
+
self._set_trusted_overview(run_metadata)
|
|
1968
|
+
self._set_parameter_message("Loading run details...")
|
|
1969
|
+
self.preview.show_trusted_live_placeholder()
|
|
1970
|
+
self._set_bounded_placeholder_views(
|
|
1971
|
+
run_metadata,
|
|
1972
|
+
"Status",
|
|
1973
|
+
"Loading run details...",
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
def set_snapshot_run_loading(
|
|
1977
|
+
self,
|
|
1978
|
+
run: TrustedRunRecord | Mapping[str, object] | None = None,
|
|
1979
|
+
) -> None:
|
|
1980
|
+
"""Render snapshot basics immediately and queue preview by GUID."""
|
|
1981
|
+
run_metadata = self._trusted_run_metadata(run)
|
|
1982
|
+
self.clear()
|
|
1983
|
+
self._set_trusted_overview(run_metadata)
|
|
1984
|
+
self._set_parameter_message("Loading run details...")
|
|
1985
|
+
self.preview.set_current_guid(run_metadata.get("guid"))
|
|
1986
|
+
self._set_bounded_placeholder_views(
|
|
1987
|
+
run_metadata,
|
|
1988
|
+
"Status",
|
|
1989
|
+
"Loading run details...",
|
|
1990
|
+
)
|
|
1991
|
+
|
|
1992
|
+
def set_snapshot_run_unavailable(
|
|
1993
|
+
self,
|
|
1994
|
+
run: TrustedRunRecord | Mapping[str, object] | None = None,
|
|
1995
|
+
) -> None:
|
|
1996
|
+
"""Render fallback basics without starting selected-detail I/O.
|
|
1997
|
+
|
|
1998
|
+
Snapshot-fallback previews retain their separately accepted GUID path,
|
|
1999
|
+
but ordinary row selection does not prepare another private database
|
|
2000
|
+
snapshot merely to fill the detail tabs.
|
|
2001
|
+
"""
|
|
2002
|
+
run_metadata = self._trusted_run_metadata(run)
|
|
2003
|
+
message = (
|
|
2004
|
+
"Detailed run metadata is deferred in snapshot fallback. "
|
|
2005
|
+
"Use an explicit plot or CSV action to materialise this run."
|
|
2006
|
+
)
|
|
2007
|
+
self.clear()
|
|
2008
|
+
self._set_trusted_overview(run_metadata)
|
|
2009
|
+
self._set_parameter_message("Detailed run metadata unavailable")
|
|
2010
|
+
self.preview.set_current_guid(run_metadata.get("guid"))
|
|
2011
|
+
self._set_bounded_placeholder_views(run_metadata, "Status", message)
|
|
2012
|
+
|
|
2013
|
+
def set_trusted_run_error(
|
|
2014
|
+
self,
|
|
2015
|
+
error: BaseException | str,
|
|
2016
|
+
run: TrustedRunRecord | Mapping[str, object] | None = None,
|
|
2017
|
+
) -> None:
|
|
2018
|
+
"""Render a bounded trusted-detail failure while retaining basics."""
|
|
2019
|
+
run_metadata = self._trusted_run_metadata(run)
|
|
2020
|
+
message = bounded_presentation_error(error)
|
|
2021
|
+
self.clear()
|
|
2022
|
+
self._set_trusted_overview(run_metadata)
|
|
2023
|
+
self._set_parameter_message("Run details unavailable")
|
|
2024
|
+
self.preview.show_trusted_live_placeholder()
|
|
2025
|
+
self._set_bounded_placeholder_views(run_metadata, "Error", message)
|
|
2026
|
+
|
|
2027
|
+
def set_snapshot_run_error(
|
|
2028
|
+
self,
|
|
2029
|
+
error: BaseException | str,
|
|
2030
|
+
run: TrustedRunRecord | Mapping[str, object] | None = None,
|
|
2031
|
+
) -> None:
|
|
2032
|
+
"""Retain snapshot preview and basics when detail loading fails."""
|
|
2033
|
+
run_metadata = self._trusted_run_metadata(run)
|
|
2034
|
+
message = bounded_presentation_error(error)
|
|
2035
|
+
self.clear()
|
|
2036
|
+
self._set_trusted_overview(run_metadata)
|
|
2037
|
+
self._set_parameter_message("Run details unavailable")
|
|
2038
|
+
self.preview.set_current_guid(run_metadata.get("guid"))
|
|
2039
|
+
self._set_bounded_placeholder_views(run_metadata, "Error", message)
|
|
2040
|
+
|
|
2041
|
+
def _set_bounded_placeholder_views(self, run_metadata, field, message):
|
|
2042
|
+
status_view = normalize_presentation_tree({field: message})
|
|
2043
|
+
raw_view = normalize_presentation_tree({"Run": run_metadata, field: message})
|
|
2044
|
+
self.metadata.setBoundedView(status_view)
|
|
2045
|
+
self.snapshot.setBoundedView(status_view)
|
|
2046
|
+
self.raw.setBoundedView(raw_view)
|
|
2047
|
+
|
|
2048
|
+
def update_live_run_details(self, run_metadata):
|
|
2049
|
+
"""Patch authoritative live fields without rebuilding run details."""
|
|
2050
|
+
if not self._has_authoritative_run_state(run_metadata):
|
|
2051
|
+
return
|
|
2052
|
+
|
|
2053
|
+
is_completed = run_metadata.get("is_completed")
|
|
2054
|
+
updates = {
|
|
2055
|
+
"Status": self._status_text(None, run_metadata),
|
|
2056
|
+
"Duration": self._time_taken_from_metadata(run_metadata),
|
|
2057
|
+
"Started": self._run_timestamp(None, run_metadata, "run_timestamp"),
|
|
2058
|
+
}
|
|
2059
|
+
data_points = self._trusted_data_points_text(run_metadata)
|
|
2060
|
+
if data_points is not None:
|
|
2061
|
+
updates["Data points"] = data_points
|
|
2062
|
+
elif any(
|
|
2063
|
+
field in run_metadata
|
|
2064
|
+
for field in ("read_setpoint_count", "setpoint_count")
|
|
2065
|
+
):
|
|
2066
|
+
# An explicit unavailable count invalidates the previously shown
|
|
2067
|
+
# value. A status-only update carries no count observation.
|
|
2068
|
+
row = self._overview_row("Data points")
|
|
2069
|
+
if row is not None:
|
|
2070
|
+
self.overview.removeRow(row)
|
|
2071
|
+
if bool(is_completed):
|
|
2072
|
+
updates["Completed"] = self._run_timestamp(
|
|
2073
|
+
None,
|
|
2074
|
+
run_metadata,
|
|
2075
|
+
"completed_timestamp",
|
|
2076
|
+
)
|
|
2077
|
+
|
|
2078
|
+
vertical_scroll_bar = cast(
|
|
2079
|
+
qtw.QScrollBar,
|
|
2080
|
+
self.overview.verticalScrollBar(),
|
|
2081
|
+
)
|
|
2082
|
+
horizontal_scroll_bar = cast(
|
|
2083
|
+
qtw.QScrollBar,
|
|
2084
|
+
self.overview.horizontalScrollBar(),
|
|
2085
|
+
)
|
|
2086
|
+
vertical_scroll = vertical_scroll_bar.value()
|
|
2087
|
+
horizontal_scroll = horizontal_scroll_bar.value()
|
|
2088
|
+
for field, value in updates.items():
|
|
2089
|
+
if self._has_value(value):
|
|
2090
|
+
self._set_overview_value(field, value)
|
|
2091
|
+
vertical_scroll_bar.setValue(vertical_scroll)
|
|
2092
|
+
horizontal_scroll_bar.setValue(horizontal_scroll)
|
|
2093
|
+
|
|
2094
|
+
def clear(self):
|
|
2095
|
+
self.invalidate_trusted_full_values()
|
|
2096
|
+
self._trusted_detail_identity = None
|
|
2097
|
+
self.overview.setRowCount(0)
|
|
2098
|
+
self.parameters.clearSpans()
|
|
2099
|
+
self.parameters.setRowCount(0)
|
|
2100
|
+
self.preview.clear_current_run()
|
|
2101
|
+
self.metadata.clear()
|
|
2102
|
+
self.snapshot.clear()
|
|
2103
|
+
self.raw.clear()
|
|
2104
|
+
|
|
2105
|
+
def scrollToTop(self):
|
|
2106
|
+
self.overview.scrollToTop()
|
|
2107
|
+
self.parameters.scrollToTop()
|
|
2108
|
+
self.metadata.scrollToTop()
|
|
2109
|
+
self.snapshot.scrollToTop()
|
|
2110
|
+
self.raw.scrollToTop()
|
|
2111
|
+
|
|
2112
|
+
def _set_overview(self, info, dataset, run_metadata=None):
|
|
2113
|
+
structure = info.get("Data Structure", {})
|
|
2114
|
+
param_info = {
|
|
2115
|
+
key: value for key, value in structure.items() if isinstance(value, dict)
|
|
2116
|
+
}
|
|
2117
|
+
measured = list((run_metadata or {}).get("measure_parameters") or [])
|
|
2118
|
+
setpoints = list((run_metadata or {}).get("sweep_parameters") or [])
|
|
2119
|
+
if not measured and not setpoints and dataset is not None:
|
|
2120
|
+
params = list(dataset.get_parameters())
|
|
2121
|
+
setpoint_names = {
|
|
2122
|
+
axis for param in params for axis in getattr(param, "depends_on_", ())
|
|
2123
|
+
}
|
|
2124
|
+
measured = [
|
|
2125
|
+
getattr(param, "name", "")
|
|
2126
|
+
for param in params
|
|
2127
|
+
if getattr(param, "name", "") not in setpoint_names
|
|
2128
|
+
]
|
|
2129
|
+
setpoints = [
|
|
2130
|
+
getattr(param, "name", "")
|
|
2131
|
+
for param in params
|
|
2132
|
+
if getattr(param, "name", "") in setpoint_names
|
|
2133
|
+
]
|
|
2134
|
+
elif not measured and not setpoints:
|
|
2135
|
+
measured = [
|
|
2136
|
+
name for name, details in param_info.items() if details.get("axes")
|
|
2137
|
+
]
|
|
2138
|
+
setpoints = [
|
|
2139
|
+
name for name, details in param_info.items() if not details.get("axes")
|
|
2140
|
+
]
|
|
2141
|
+
|
|
2142
|
+
rows = [
|
|
2143
|
+
("Status", self._status_text(dataset, run_metadata)),
|
|
2144
|
+
("Data points", structure.get("Data points")),
|
|
2145
|
+
("Duration", self._time_taken_value(dataset, info, run_metadata)),
|
|
2146
|
+
("Measured parameters", ", ".join(measured)),
|
|
2147
|
+
("Setpoints", ", ".join(setpoints)),
|
|
2148
|
+
("Started", self._run_timestamp(dataset, run_metadata, "run_timestamp")),
|
|
2149
|
+
(
|
|
2150
|
+
"Completed",
|
|
2151
|
+
self._run_timestamp(dataset, run_metadata, "completed_timestamp"),
|
|
2152
|
+
),
|
|
2153
|
+
("Experiment", self._run_attr(dataset, run_metadata, "exp_name")),
|
|
2154
|
+
("Sample", self._run_attr(dataset, run_metadata, "sample_name")),
|
|
2155
|
+
("Name", self._run_attr(dataset, run_metadata, "name")),
|
|
2156
|
+
("GUID", self._run_attr(dataset, run_metadata, "guid")),
|
|
2157
|
+
]
|
|
2158
|
+
rows = [(key, value) for key, value in rows if self._has_value(value)]
|
|
2159
|
+
|
|
2160
|
+
self._fill_key_value_table(self.overview, rows)
|
|
2161
|
+
|
|
2162
|
+
def _set_parameters(self, info, dataset, setpoint_summaries=None):
|
|
2163
|
+
params = list(dataset.get_parameters()) if dataset is not None else []
|
|
2164
|
+
snapshot_params = snapshot_parameters(info.get("Snapshot"))
|
|
2165
|
+
seen_axes = set()
|
|
2166
|
+
for param in params:
|
|
2167
|
+
for axis in getattr(param, "depends_on_", ()):
|
|
2168
|
+
if axis in seen_axes:
|
|
2169
|
+
continue
|
|
2170
|
+
seen_axes.add(axis)
|
|
2171
|
+
|
|
2172
|
+
setpoint_summaries = {
|
|
2173
|
+
str(name): dict(summary)
|
|
2174
|
+
for name, summary in (setpoint_summaries or {}).items()
|
|
2175
|
+
}
|
|
2176
|
+
setpoint_rows = []
|
|
2177
|
+
measured_rows = []
|
|
2178
|
+
|
|
2179
|
+
for param in params:
|
|
2180
|
+
name = getattr(param, "name", "")
|
|
2181
|
+
snap = snapshot_params.get(name, {})
|
|
2182
|
+
is_setpoint = name in seen_axes and not getattr(param, "depends_on_", ())
|
|
2183
|
+
values = self._parameter_row_values(
|
|
2184
|
+
param, snap, is_setpoint, setpoint_summaries
|
|
2185
|
+
)
|
|
2186
|
+
|
|
2187
|
+
if is_setpoint:
|
|
2188
|
+
setpoint_rows.append(values)
|
|
2189
|
+
else:
|
|
2190
|
+
measured_rows.append(values)
|
|
2191
|
+
|
|
2192
|
+
self._fill_parameter_groups(setpoint_rows, measured_rows)
|
|
2193
|
+
|
|
2194
|
+
def _set_trusted_overview(self, run_metadata, parameters=()):
|
|
2195
|
+
run_metadata = dict(bounded_selected_run_fields(run_metadata or {}))
|
|
2196
|
+
if parameters and not (
|
|
2197
|
+
run_metadata.get("measure_parameters")
|
|
2198
|
+
or run_metadata.get("sweep_parameters")
|
|
2199
|
+
):
|
|
2200
|
+
setpoint_names = {
|
|
2201
|
+
name for parameter in parameters for name in parameter.depends_on
|
|
2202
|
+
}
|
|
2203
|
+
run_metadata["measure_parameters"] = [
|
|
2204
|
+
parameter.name
|
|
2205
|
+
for parameter in parameters
|
|
2206
|
+
if parameter.name not in setpoint_names
|
|
2207
|
+
]
|
|
2208
|
+
run_metadata["sweep_parameters"] = [
|
|
2209
|
+
parameter.name
|
|
2210
|
+
for parameter in parameters
|
|
2211
|
+
if parameter.name in setpoint_names
|
|
2212
|
+
]
|
|
2213
|
+
run_metadata = dict(bounded_selected_run_fields(run_metadata))
|
|
2214
|
+
info = {
|
|
2215
|
+
"Data Structure": {
|
|
2216
|
+
"Data points": self._trusted_data_points_text(run_metadata),
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
self._set_overview(info, None, run_metadata=run_metadata)
|
|
2220
|
+
|
|
2221
|
+
@staticmethod
|
|
2222
|
+
def _trusted_logical_point_count(run_metadata):
|
|
2223
|
+
acquired = run_metadata.get("read_setpoint_count")
|
|
2224
|
+
if type(acquired) is int and acquired >= 0:
|
|
2225
|
+
return acquired
|
|
2226
|
+
observed = run_metadata.get("setpoint_count")
|
|
2227
|
+
if (
|
|
2228
|
+
run_metadata.get("setpoint_count_source") == "observed"
|
|
2229
|
+
and type(observed) is int
|
|
2230
|
+
and observed >= 0
|
|
2231
|
+
):
|
|
2232
|
+
return observed
|
|
2233
|
+
return None
|
|
2234
|
+
|
|
2235
|
+
@classmethod
|
|
2236
|
+
def _trusted_data_points_text(cls, run_metadata):
|
|
2237
|
+
logical_count = cls._trusted_logical_point_count(run_metadata)
|
|
2238
|
+
if logical_count is None:
|
|
2239
|
+
return None
|
|
2240
|
+
if run_metadata.get("setpoint_shape") or run_metadata.get("point_shape"):
|
|
2241
|
+
formatted = format_point_count(run_metadata)
|
|
2242
|
+
if formatted != "unknown":
|
|
2243
|
+
return formatted
|
|
2244
|
+
return f"{logical_count:,}"
|
|
2245
|
+
|
|
2246
|
+
def _set_trusted_parameters(
|
|
2247
|
+
self,
|
|
2248
|
+
parameters: tuple[TrustedParameterView, ...],
|
|
2249
|
+
summaries: tuple[TrustedSetpointSummary, ...],
|
|
2250
|
+
snapshot_params,
|
|
2251
|
+
*,
|
|
2252
|
+
parameters_truncated=False,
|
|
2253
|
+
) -> None:
|
|
2254
|
+
seen_axes = set()
|
|
2255
|
+
for parameter in parameters:
|
|
2256
|
+
for axis in parameter.depends_on:
|
|
2257
|
+
if axis in seen_axes:
|
|
2258
|
+
continue
|
|
2259
|
+
seen_axes.add(axis)
|
|
2260
|
+
|
|
2261
|
+
summary_values = {
|
|
2262
|
+
summary.name: {
|
|
2263
|
+
"from": summary.first,
|
|
2264
|
+
"to": summary.last,
|
|
2265
|
+
"steps": summary.steps,
|
|
2266
|
+
}
|
|
2267
|
+
for summary in summaries
|
|
2268
|
+
}
|
|
2269
|
+
setpoint_rows = []
|
|
2270
|
+
measured_rows = []
|
|
2271
|
+
for parameter in parameters:
|
|
2272
|
+
snap = snapshot_params.get(parameter.name, {})
|
|
2273
|
+
is_setpoint = parameter.name in seen_axes and not parameter.depends_on
|
|
2274
|
+
values = self._parameter_row_values(
|
|
2275
|
+
parameter,
|
|
2276
|
+
snap,
|
|
2277
|
+
is_setpoint,
|
|
2278
|
+
summary_values,
|
|
2279
|
+
)
|
|
2280
|
+
if is_setpoint:
|
|
2281
|
+
setpoint_rows.append(values)
|
|
2282
|
+
else:
|
|
2283
|
+
measured_rows.append(values)
|
|
2284
|
+
|
|
2285
|
+
self._fill_parameter_groups(setpoint_rows, measured_rows)
|
|
2286
|
+
if parameters_truncated:
|
|
2287
|
+
row = self.parameters.rowCount()
|
|
2288
|
+
self.parameters.insertRow(row)
|
|
2289
|
+
self.parameters.setSpan(row, 0, 1, self.parameters.columnCount())
|
|
2290
|
+
item = self._table_item(
|
|
2291
|
+
"Additional or oversized parameter details were omitted at "
|
|
2292
|
+
"presentation limits."
|
|
2293
|
+
)
|
|
2294
|
+
font = item.font()
|
|
2295
|
+
font.setItalic(True)
|
|
2296
|
+
item.setFont(font)
|
|
2297
|
+
self.parameters.setItem(row, 0, item)
|
|
2298
|
+
self._resize_table(self.parameters)
|
|
2299
|
+
|
|
2300
|
+
def _fill_parameter_groups(self, setpoint_rows, measured_rows):
|
|
2301
|
+
groups = [
|
|
2302
|
+
("Set parameters", setpoint_rows),
|
|
2303
|
+
("Measure parameters", measured_rows),
|
|
2304
|
+
]
|
|
2305
|
+
self.parameters.clearSpans()
|
|
2306
|
+
self.parameters.setRowCount(sum(1 + len(rows) for _, rows in groups))
|
|
2307
|
+
|
|
2308
|
+
row = 0
|
|
2309
|
+
for heading, rows in groups:
|
|
2310
|
+
self._set_parameter_heading_row(row, heading)
|
|
2311
|
+
row += 1
|
|
2312
|
+
for values in rows:
|
|
2313
|
+
for col, value in enumerate(values):
|
|
2314
|
+
self.parameters.setItem(
|
|
2315
|
+
row, col, self._table_item(value, max_len=80)
|
|
2316
|
+
)
|
|
2317
|
+
row += 1
|
|
2318
|
+
|
|
2319
|
+
self._resize_table(self.parameters)
|
|
2320
|
+
|
|
2321
|
+
def _set_parameter_message(self, message):
|
|
2322
|
+
self.parameters.clearSpans()
|
|
2323
|
+
self.parameters.setRowCount(1)
|
|
2324
|
+
self.parameters.setSpan(0, 0, 1, self.parameters.columnCount())
|
|
2325
|
+
item = self._table_item(message)
|
|
2326
|
+
font = item.font()
|
|
2327
|
+
font.setItalic(True)
|
|
2328
|
+
item.setFont(font)
|
|
2329
|
+
self.parameters.setItem(0, 0, item)
|
|
2330
|
+
self._resize_table(self.parameters)
|
|
2331
|
+
|
|
2332
|
+
def _trusted_run_metadata(self, run):
|
|
2333
|
+
if run is None:
|
|
2334
|
+
return {}
|
|
2335
|
+
if isinstance(run, TrustedRunRecord):
|
|
2336
|
+
metadata = dict(run.fields)
|
|
2337
|
+
metadata.setdefault("run_id", run.run_id)
|
|
2338
|
+
return dict(bounded_selected_run_fields(metadata))
|
|
2339
|
+
if isinstance(run, Mapping):
|
|
2340
|
+
return dict(bounded_selected_run_fields(run))
|
|
2341
|
+
raise TypeError("run must be a TrustedRunRecord, mapping, or None.")
|
|
2342
|
+
|
|
2343
|
+
def _set_parameter_heading_row(self, row, heading):
|
|
2344
|
+
for col in range(self.parameters.columnCount()):
|
|
2345
|
+
item = self._table_item(heading if col == 0 else "")
|
|
2346
|
+
font = item.font()
|
|
2347
|
+
font.setBold(True)
|
|
2348
|
+
item.setFont(font)
|
|
2349
|
+
item.setToolTip(heading)
|
|
2350
|
+
self.parameters.setItem(row, col, item)
|
|
2351
|
+
|
|
2352
|
+
def _parameter_row_values(self, param, snap, is_setpoint, setpoint_summaries):
|
|
2353
|
+
name = getattr(param, "name", "")
|
|
2354
|
+
common = [
|
|
2355
|
+
name,
|
|
2356
|
+
getattr(param, "label", "") or snap.get("label", ""),
|
|
2357
|
+
getattr(param, "unit", "") or snap.get("unit", ""),
|
|
2358
|
+
]
|
|
2359
|
+
instrument = snap.get("instrument_name", snap.get("instrument", ""))
|
|
2360
|
+
|
|
2361
|
+
if not is_setpoint:
|
|
2362
|
+
return common + ["", "", "", "", instrument]
|
|
2363
|
+
|
|
2364
|
+
summary = setpoint_summaries.get(name, {})
|
|
2365
|
+
return common + [
|
|
2366
|
+
summary.get("from", snap.get("value", snap.get("raw_value", ""))),
|
|
2367
|
+
summary.get("to", snap.get("value", snap.get("raw_value", ""))),
|
|
2368
|
+
summary.get("steps", ""),
|
|
2369
|
+
self._parameter_delay(snap),
|
|
2370
|
+
instrument,
|
|
2371
|
+
]
|
|
2372
|
+
|
|
2373
|
+
def _parameter_delay(self, snap):
|
|
2374
|
+
for key in ("delay", "post_delay", "inter_delay"):
|
|
2375
|
+
value = snap.get(key)
|
|
2376
|
+
if self._has_value(value):
|
|
2377
|
+
return value
|
|
2378
|
+
return ""
|
|
2379
|
+
|
|
2380
|
+
def _time_taken_value(self, dataset, info, run_metadata=None):
|
|
2381
|
+
if self._has_authoritative_run_state(run_metadata):
|
|
2382
|
+
return self._time_taken_from_metadata(run_metadata)
|
|
2383
|
+
|
|
2384
|
+
started = self._dataset_attr(dataset, "run_timestamp_raw")
|
|
2385
|
+
completed = self._dataset_attr(dataset, "completed_timestamp_raw")
|
|
2386
|
+
if not self._has_value(started):
|
|
2387
|
+
started = self._dataset_attr(dataset, "run_timestamp")
|
|
2388
|
+
if not self._has_value(completed):
|
|
2389
|
+
completed = self._dataset_attr(dataset, "completed_timestamp")
|
|
2390
|
+
if not self._has_value(started):
|
|
2391
|
+
return ""
|
|
2392
|
+
|
|
2393
|
+
end = completed if self._has_value(completed) else (
|
|
2394
|
+
(run_metadata or {}).get("database_modified_timestamp")
|
|
2395
|
+
)
|
|
2396
|
+
if not self._has_value(end):
|
|
2397
|
+
return ""
|
|
2398
|
+
try:
|
|
2399
|
+
seconds = max(
|
|
2400
|
+
0, self._timestamp_seconds(end) - self._timestamp_seconds(started)
|
|
2401
|
+
)
|
|
2402
|
+
except (TypeError, ValueError):
|
|
2403
|
+
return ""
|
|
2404
|
+
|
|
2405
|
+
per_point = self._time_per_point(seconds, info, dataset)
|
|
2406
|
+
if self._has_value(per_point):
|
|
2407
|
+
return f"{seconds:.2f} s\t({format_duration_dhms(seconds)}; {per_point} s/point)"
|
|
2408
|
+
return f"{seconds:.2f} s\t({format_duration_dhms(seconds)})"
|
|
2409
|
+
|
|
2410
|
+
def _time_taken_from_metadata(self, run_metadata):
|
|
2411
|
+
seconds = time_taken_seconds(run_metadata)
|
|
2412
|
+
if seconds is None:
|
|
2413
|
+
return ""
|
|
2414
|
+
|
|
2415
|
+
points = self._trusted_logical_point_count(run_metadata)
|
|
2416
|
+
per_point = self._time_per_point(
|
|
2417
|
+
seconds,
|
|
2418
|
+
{"Data Structure": {"Data points": points}},
|
|
2419
|
+
None,
|
|
2420
|
+
)
|
|
2421
|
+
if self._has_value(per_point):
|
|
2422
|
+
return f"{seconds:.2f} s\t({format_duration_dhms(seconds)}; {per_point} s/point)"
|
|
2423
|
+
return f"{seconds:.2f} s\t({format_duration_dhms(seconds)})"
|
|
2424
|
+
|
|
2425
|
+
def _timestamp_seconds(self, value):
|
|
2426
|
+
if isinstance(value, (int, float)):
|
|
2427
|
+
return float(value)
|
|
2428
|
+
|
|
2429
|
+
if isinstance(value, datetime):
|
|
2430
|
+
return value.timestamp()
|
|
2431
|
+
|
|
2432
|
+
text = str(value)
|
|
2433
|
+
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"):
|
|
2434
|
+
try:
|
|
2435
|
+
return datetime.strptime(text, fmt).timestamp()
|
|
2436
|
+
except ValueError:
|
|
2437
|
+
pass
|
|
2438
|
+
|
|
2439
|
+
return float(value)
|
|
2440
|
+
|
|
2441
|
+
def _time_per_point(self, seconds, info, dataset):
|
|
2442
|
+
points = info.get("Data Structure", {}).get("Data points")
|
|
2443
|
+
try:
|
|
2444
|
+
points = float(points)
|
|
2445
|
+
except (TypeError, ValueError):
|
|
2446
|
+
return ""
|
|
2447
|
+
|
|
2448
|
+
if points <= 0:
|
|
2449
|
+
return ""
|
|
2450
|
+
|
|
2451
|
+
return f"{seconds / points:.3g}"
|
|
2452
|
+
|
|
2453
|
+
def _fill_key_value_table(self, table, rows):
|
|
2454
|
+
table.setRowCount(len(rows))
|
|
2455
|
+
for row, (key, value) in enumerate(rows):
|
|
2456
|
+
table.setItem(row, 0, self._table_item(key))
|
|
2457
|
+
table.setItem(row, 1, self._table_item(value, max_len=140))
|
|
2458
|
+
self._resize_table(table)
|
|
2459
|
+
|
|
2460
|
+
def _resize_table(self, table):
|
|
2461
|
+
header = table.horizontalHeader()
|
|
2462
|
+
last_col = table.columnCount() - 1
|
|
2463
|
+
stretch_cols = {last_col}
|
|
2464
|
+
if table.columnCount() > 2:
|
|
2465
|
+
stretch_cols.update({0, 1})
|
|
2466
|
+
|
|
2467
|
+
for col in range(table.columnCount()):
|
|
2468
|
+
if col in stretch_cols:
|
|
2469
|
+
header.setSectionResizeMode(col, qtw.QHeaderView.ResizeMode.Stretch)
|
|
2470
|
+
else:
|
|
2471
|
+
header.setSectionResizeMode(
|
|
2472
|
+
col, qtw.QHeaderView.ResizeMode.ResizeToContents
|
|
2473
|
+
)
|
|
2474
|
+
header.setStretchLastSection(False)
|
|
2475
|
+
for row in range(table.rowCount()):
|
|
2476
|
+
table.setRowHeight(row, 20)
|
|
2477
|
+
|
|
2478
|
+
def _table_item(self, value, max_len=None):
|
|
2479
|
+
text = format_value(value, max_len=max_len)
|
|
2480
|
+
item = qtw.QTableWidgetItem(text)
|
|
2481
|
+
tooltip, _truncated = bounded_presentation_text(
|
|
2482
|
+
format_value(value),
|
|
2483
|
+
limit=TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES,
|
|
2484
|
+
)
|
|
2485
|
+
item.setToolTip(tooltip)
|
|
2486
|
+
return item
|
|
2487
|
+
|
|
2488
|
+
def _dataset_attr(self, dataset, name):
|
|
2489
|
+
if dataset is None:
|
|
2490
|
+
return ""
|
|
2491
|
+
value = getattr(dataset, name, "")
|
|
2492
|
+
return value() if callable(value) else value
|
|
2493
|
+
|
|
2494
|
+
def _run_attr(self, dataset, run_metadata, name):
|
|
2495
|
+
if run_metadata is not None:
|
|
2496
|
+
value = run_metadata.get(name)
|
|
2497
|
+
if self._has_value(value):
|
|
2498
|
+
return value
|
|
2499
|
+
return self._dataset_attr(dataset, name)
|
|
2500
|
+
|
|
2501
|
+
def _run_timestamp(self, dataset, run_metadata, name):
|
|
2502
|
+
if self._has_authoritative_run_state(run_metadata):
|
|
2503
|
+
if name == "completed_timestamp" and not bool(
|
|
2504
|
+
run_metadata.get("is_completed")
|
|
2505
|
+
):
|
|
2506
|
+
return ""
|
|
2507
|
+
value = run_metadata.get(name)
|
|
2508
|
+
else:
|
|
2509
|
+
value = self._dataset_attr(dataset, name)
|
|
2510
|
+
if isinstance(value, (int, float)):
|
|
2511
|
+
return datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M:%S")
|
|
2512
|
+
return value
|
|
2513
|
+
|
|
2514
|
+
def _status_text(self, dataset, run_metadata=None):
|
|
2515
|
+
if self._has_authoritative_run_state(run_metadata):
|
|
2516
|
+
return format_run_state(run_metadata)
|
|
2517
|
+
|
|
2518
|
+
running = self._dataset_attr(dataset, "running")
|
|
2519
|
+
if running is True:
|
|
2520
|
+
return format_run_state({"is_completed": False})
|
|
2521
|
+
if running is False:
|
|
2522
|
+
return format_run_state({"is_completed": True})
|
|
2523
|
+
return ""
|
|
2524
|
+
|
|
2525
|
+
def _has_authoritative_run_state(self, run_metadata):
|
|
2526
|
+
return run_metadata is not None and "is_completed" in run_metadata
|
|
2527
|
+
|
|
2528
|
+
def _set_overview_value(self, field, value):
|
|
2529
|
+
row = self._overview_row(field)
|
|
2530
|
+
if row is None:
|
|
2531
|
+
row = self._overview_insert_row(field)
|
|
2532
|
+
self.overview.insertRow(row)
|
|
2533
|
+
self.overview.setItem(row, 0, self._table_item(field))
|
|
2534
|
+
self.overview.setItem(row, 1, self._table_item(value))
|
|
2535
|
+
self.overview.setRowHeight(row, 20)
|
|
2536
|
+
|
|
2537
|
+
def _overview_row(self, field):
|
|
2538
|
+
for row in range(self.overview.rowCount()):
|
|
2539
|
+
item = self.overview.item(row, 0)
|
|
2540
|
+
if item is not None and item.text() == field:
|
|
2541
|
+
return row
|
|
2542
|
+
return None
|
|
2543
|
+
|
|
2544
|
+
def _overview_insert_row(self, field):
|
|
2545
|
+
field_order = (
|
|
2546
|
+
"Status",
|
|
2547
|
+
"Data points",
|
|
2548
|
+
"Duration",
|
|
2549
|
+
"Measured parameters",
|
|
2550
|
+
"Setpoints",
|
|
2551
|
+
"Started",
|
|
2552
|
+
"Completed",
|
|
2553
|
+
"Experiment",
|
|
2554
|
+
"Sample",
|
|
2555
|
+
"Name",
|
|
2556
|
+
"GUID",
|
|
2557
|
+
)
|
|
2558
|
+
target_order = field_order.index(field)
|
|
2559
|
+
for row in range(self.overview.rowCount()):
|
|
2560
|
+
item = self.overview.item(row, 0)
|
|
2561
|
+
if item is None:
|
|
2562
|
+
continue
|
|
2563
|
+
try:
|
|
2564
|
+
current_order = field_order.index(item.text())
|
|
2565
|
+
except ValueError:
|
|
2566
|
+
continue
|
|
2567
|
+
if current_order > target_order:
|
|
2568
|
+
return row
|
|
2569
|
+
return self.overview.rowCount()
|
|
2570
|
+
|
|
2571
|
+
def _has_value(self, value):
|
|
2572
|
+
return value is not None and value != ""
|