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,2442 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
5
|
+
import stat
|
|
6
|
+
from itertools import islice
|
|
7
|
+
from time import monotonic
|
|
8
|
+
from typing import Literal, NamedTuple
|
|
9
|
+
|
|
10
|
+
from qcodes.dataset.sqlite.database import get_DB_location
|
|
11
|
+
|
|
12
|
+
from qplot.datahandling.readonly import (
|
|
13
|
+
qcodes_read_only_connection,
|
|
14
|
+
sqlite_read_only_connection,
|
|
15
|
+
)
|
|
16
|
+
from qplot.datahandling.source_activity import source_activity_timestamp
|
|
17
|
+
from qplot.datahandling.trusted_presentation import (
|
|
18
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES,
|
|
19
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_TEXT_BYTES,
|
|
20
|
+
TRUSTED_PRESENTATION_MAX_PARAMETERS,
|
|
21
|
+
bounded_presentation_text,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
MAX_SELECTED_RUN_SETPOINT_SUMMARY_ROWS = 100_000
|
|
25
|
+
MAX_SELECTED_RUN_SETPOINT_SUMMARY_SCALAR_BYTES = 64 * 1024
|
|
26
|
+
MAX_SELECTED_RUN_SETPOINT_SUMMARY_SOURCE_BYTES = 8 * 1024 * 1024
|
|
27
|
+
MAX_SNAPSHOT_SELECTED_RUN_COLUMNS = 256
|
|
28
|
+
MAX_SNAPSHOT_SELECTED_RUN_SCALAR_BYTES = 1024 * 1024
|
|
29
|
+
MAX_SNAPSHOT_SELECTED_RUN_TOTAL_BYTES = 4 * 1024 * 1024
|
|
30
|
+
MAX_SNAPSHOT_SELECTED_OBSERVATION_BYTES = 64 * 1024
|
|
31
|
+
MAX_SNAPSHOT_SELECTED_LAYOUT_ROWS = 4_096
|
|
32
|
+
MAX_SNAPSHOT_SELECTED_PARAMETERS = 256
|
|
33
|
+
MAX_SNAPSHOT_SELECTED_SETPOINTS = 32
|
|
34
|
+
MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS = 32
|
|
35
|
+
_SNAPSHOT_SELECTED_LAYOUT_TEXT_BYTES = 512
|
|
36
|
+
_SNAPSHOT_STANDARD_RUN_COLUMNS = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"run_id",
|
|
39
|
+
"exp_id",
|
|
40
|
+
"name",
|
|
41
|
+
"result_table_name",
|
|
42
|
+
"result_counter",
|
|
43
|
+
"run_timestamp",
|
|
44
|
+
"completed_timestamp",
|
|
45
|
+
"is_completed",
|
|
46
|
+
"parameters",
|
|
47
|
+
"guid",
|
|
48
|
+
"run_description",
|
|
49
|
+
"snapshot",
|
|
50
|
+
"parent_datasets",
|
|
51
|
+
"captured_run_id",
|
|
52
|
+
"captured_counter",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
_SNAPSHOT_OBSERVATION_FIELDS = frozenset(
|
|
56
|
+
{
|
|
57
|
+
"database_modified_timestamp",
|
|
58
|
+
"expected_results",
|
|
59
|
+
"expected_results_source",
|
|
60
|
+
"measure_parameters",
|
|
61
|
+
"measurement_exception",
|
|
62
|
+
"point_shape",
|
|
63
|
+
"read_setpoint_count",
|
|
64
|
+
"result_count",
|
|
65
|
+
"setpoint_count",
|
|
66
|
+
"setpoint_count_source",
|
|
67
|
+
"setpoint_shape",
|
|
68
|
+
"setpoint_shape_source",
|
|
69
|
+
"storage_bytes",
|
|
70
|
+
"storage_bytes_estimated",
|
|
71
|
+
"sweep_parameters",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _StorageSize(NamedTuple):
|
|
77
|
+
bytes: int | None
|
|
78
|
+
accuracy: Literal["exact", "estimated", "unavailable"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _raise_if_read_aborted(cancelled_callback=None, deadline=None):
|
|
82
|
+
if cancelled_callback is not None and cancelled_callback():
|
|
83
|
+
raise InterruptedError("Database read cancelled.")
|
|
84
|
+
if deadline is not None and monotonic() >= deadline:
|
|
85
|
+
raise TimeoutError("Database read deadline exceeded.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _install_cancel_progress_handler(
|
|
89
|
+
conn,
|
|
90
|
+
cancelled_callback,
|
|
91
|
+
deadline=None,
|
|
92
|
+
):
|
|
93
|
+
if cancelled_callback is None and deadline is None:
|
|
94
|
+
return
|
|
95
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
96
|
+
set_progress_handler = getattr(conn, "set_progress_handler", None)
|
|
97
|
+
if callable(set_progress_handler):
|
|
98
|
+
set_progress_handler(
|
|
99
|
+
lambda: int(
|
|
100
|
+
bool(cancelled_callback is not None and cancelled_callback())
|
|
101
|
+
or bool(deadline is not None and monotonic() >= deadline)
|
|
102
|
+
),
|
|
103
|
+
1000,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _translate_interrupted_read(error, cancelled_callback=None, deadline=None):
|
|
108
|
+
if _sql_was_interrupted(error):
|
|
109
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _notify_connection(connection_callback, connection):
|
|
113
|
+
if connection_callback is not None:
|
|
114
|
+
connection_callback(connection)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _read_only_connection(
|
|
118
|
+
database_path,
|
|
119
|
+
expected_database_identity,
|
|
120
|
+
cancelled_callback=None,
|
|
121
|
+
deadline=None,
|
|
122
|
+
):
|
|
123
|
+
"""Open a QCoDeS view while retaining optional abort controls."""
|
|
124
|
+
kwargs = {}
|
|
125
|
+
if expected_database_identity is not None:
|
|
126
|
+
kwargs["expected_database_identity"] = expected_database_identity
|
|
127
|
+
if cancelled_callback is not None:
|
|
128
|
+
kwargs["cancelled_callback"] = cancelled_callback
|
|
129
|
+
if deadline is not None:
|
|
130
|
+
kwargs["deadline"] = deadline
|
|
131
|
+
return qcodes_read_only_connection(database_path, **kwargs)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_selected_run_setpoint_summaries(
|
|
135
|
+
database_path,
|
|
136
|
+
result_table_name,
|
|
137
|
+
setpoint_names,
|
|
138
|
+
result_count,
|
|
139
|
+
*,
|
|
140
|
+
setpoint_shape=None,
|
|
141
|
+
expected_database_identity=None,
|
|
142
|
+
cancelled_callback=None,
|
|
143
|
+
connection_callback=None,
|
|
144
|
+
deadline=None,
|
|
145
|
+
):
|
|
146
|
+
"""Return bounded, plain summaries for snapshot-mode run details.
|
|
147
|
+
|
|
148
|
+
Result-table grouping is deliberately limited to runs whose already-known
|
|
149
|
+
row count is at most ``MAX_SELECTED_RUN_SETPOINT_SUMMARY_ROWS``. Unknown
|
|
150
|
+
and larger runs retain any cheap shape-derived step counts without opening
|
|
151
|
+
SQLite. This function is part of the data layer so Qt widgets only render
|
|
152
|
+
the mapping supplied by their controller.
|
|
153
|
+
"""
|
|
154
|
+
names = tuple(
|
|
155
|
+
dict.fromkeys(str(name) for name in setpoint_names or () if str(name))
|
|
156
|
+
)
|
|
157
|
+
summaries = {}
|
|
158
|
+
try:
|
|
159
|
+
bounded_result_count = int(result_count) if result_count is not None else None
|
|
160
|
+
except (TypeError, ValueError, OverflowError):
|
|
161
|
+
bounded_result_count = None
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
database_path
|
|
165
|
+
and result_table_name
|
|
166
|
+
and names
|
|
167
|
+
and bounded_result_count is not None
|
|
168
|
+
and 0 <= bounded_result_count <= MAX_SELECTED_RUN_SETPOINT_SUMMARY_ROWS
|
|
169
|
+
):
|
|
170
|
+
summaries.update(
|
|
171
|
+
_read_selected_run_setpoint_summaries(
|
|
172
|
+
database_path,
|
|
173
|
+
result_table_name,
|
|
174
|
+
names,
|
|
175
|
+
expected_database_identity=expected_database_identity,
|
|
176
|
+
cancelled_callback=cancelled_callback,
|
|
177
|
+
connection_callback=connection_callback,
|
|
178
|
+
deadline=deadline,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
for name, steps in zip(names, setpoint_shape or (), strict=False):
|
|
183
|
+
if steps is None or steps == "":
|
|
184
|
+
continue
|
|
185
|
+
summaries.setdefault(name, {}).setdefault("steps", steps)
|
|
186
|
+
return {name: dict(summary) for name, summary in summaries.items()}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _read_selected_run_setpoint_summaries(
|
|
190
|
+
database_path,
|
|
191
|
+
result_table_name,
|
|
192
|
+
setpoint_names,
|
|
193
|
+
*,
|
|
194
|
+
expected_database_identity=None,
|
|
195
|
+
cancelled_callback=None,
|
|
196
|
+
connection_callback=None,
|
|
197
|
+
deadline=None,
|
|
198
|
+
):
|
|
199
|
+
"""Read one bounded snapshot summary batch, degrading to empty on error."""
|
|
200
|
+
conn = None
|
|
201
|
+
cursor = None
|
|
202
|
+
try:
|
|
203
|
+
conn = sqlite_read_only_connection(
|
|
204
|
+
database_path,
|
|
205
|
+
timeout=2,
|
|
206
|
+
expected_database_identity=expected_database_identity,
|
|
207
|
+
cancelled_callback=cancelled_callback,
|
|
208
|
+
deadline=deadline,
|
|
209
|
+
)
|
|
210
|
+
_notify_connection(connection_callback, conn)
|
|
211
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
212
|
+
cursor = conn.cursor()
|
|
213
|
+
summaries = _selected_run_setpoint_summaries_from_cursor(
|
|
214
|
+
cursor,
|
|
215
|
+
result_table_name,
|
|
216
|
+
setpoint_names,
|
|
217
|
+
cancelled_callback=cancelled_callback,
|
|
218
|
+
deadline=deadline,
|
|
219
|
+
)
|
|
220
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
221
|
+
return summaries
|
|
222
|
+
except (InterruptedError, TimeoutError):
|
|
223
|
+
raise
|
|
224
|
+
except sqlite3.OperationalError as error:
|
|
225
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
226
|
+
return {}
|
|
227
|
+
except Exception:
|
|
228
|
+
return {}
|
|
229
|
+
finally:
|
|
230
|
+
try:
|
|
231
|
+
_notify_connection(connection_callback, None)
|
|
232
|
+
finally:
|
|
233
|
+
if cursor is not None:
|
|
234
|
+
cursor.close()
|
|
235
|
+
if conn is not None:
|
|
236
|
+
conn.close()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _selected_run_setpoint_summaries_from_cursor(
|
|
240
|
+
cursor,
|
|
241
|
+
result_table_name,
|
|
242
|
+
setpoint_names,
|
|
243
|
+
*,
|
|
244
|
+
cancelled_callback=None,
|
|
245
|
+
deadline=None,
|
|
246
|
+
):
|
|
247
|
+
"""Return bounded summaries using an already guarded snapshot connection."""
|
|
248
|
+
quoted_table = _sqlite_identifier(result_table_name)
|
|
249
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
250
|
+
if not _selected_summary_source_within_byte_limit(cursor):
|
|
251
|
+
return {}
|
|
252
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
253
|
+
high_watermark = _result_table_integer_pk_high_watermark(
|
|
254
|
+
cursor,
|
|
255
|
+
quoted_table,
|
|
256
|
+
)
|
|
257
|
+
if (
|
|
258
|
+
high_watermark is None
|
|
259
|
+
or high_watermark > MAX_SELECTED_RUN_SETPOINT_SUMMARY_ROWS
|
|
260
|
+
):
|
|
261
|
+
return {}
|
|
262
|
+
columns = _result_table_column_types(cursor, quoted_table)
|
|
263
|
+
summaries = {}
|
|
264
|
+
for name in setpoint_names:
|
|
265
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
266
|
+
if name not in columns or columns[name] == "array":
|
|
267
|
+
continue
|
|
268
|
+
summary = _selected_run_setpoint_summary(cursor, quoted_table, name)
|
|
269
|
+
if summary:
|
|
270
|
+
summaries[name] = summary
|
|
271
|
+
return summaries
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _selected_summary_source_within_byte_limit(cursor):
|
|
275
|
+
"""Fail closed unless the whole private SQLite view is at most 8 MiB.
|
|
276
|
+
|
|
277
|
+
A small row count does not bound the bytes sorted by ``GROUP BY``: one
|
|
278
|
+
setpoint can be a multi-gigabyte BLOB, and a sparse database can have a
|
|
279
|
+
tiny logical page count but a huge file length. Check both SQLite's main
|
|
280
|
+
page extent and the exact private main/sidecar file lengths before any
|
|
281
|
+
result-table aggregate. Snapshot-mode connections own stable private
|
|
282
|
+
files, so these preflights remain valid for the following statement.
|
|
283
|
+
"""
|
|
284
|
+
limit = MAX_SELECTED_RUN_SETPOINT_SUMMARY_SOURCE_BYTES
|
|
285
|
+
try:
|
|
286
|
+
cursor.execute("PRAGMA main.page_size")
|
|
287
|
+
page_size_row = cursor.fetchone()
|
|
288
|
+
cursor.execute("PRAGMA main.page_count")
|
|
289
|
+
page_count_row = cursor.fetchone()
|
|
290
|
+
if (
|
|
291
|
+
page_size_row is None
|
|
292
|
+
or page_count_row is None
|
|
293
|
+
or len(page_size_row) != 1
|
|
294
|
+
or len(page_count_row) != 1
|
|
295
|
+
):
|
|
296
|
+
return False
|
|
297
|
+
page_size = page_size_row[0]
|
|
298
|
+
page_count = page_count_row[0]
|
|
299
|
+
if (
|
|
300
|
+
type(page_size) is not int
|
|
301
|
+
or type(page_count) is not int
|
|
302
|
+
or page_size <= 0
|
|
303
|
+
or page_count < 0
|
|
304
|
+
or page_count > limit // page_size
|
|
305
|
+
):
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
cursor.execute("PRAGMA database_list")
|
|
309
|
+
main_paths = [
|
|
310
|
+
str(row[2])
|
|
311
|
+
for row in cursor.fetchall()
|
|
312
|
+
if len(row) >= 3 and str(row[1]) == "main" and row[2]
|
|
313
|
+
]
|
|
314
|
+
if len(main_paths) != 1:
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
total_bytes = 0
|
|
318
|
+
for suffix in ("", "-wal", "-journal", "-shm"):
|
|
319
|
+
path = f"{main_paths[0]}{suffix}"
|
|
320
|
+
try:
|
|
321
|
+
file_stat = os.stat(path)
|
|
322
|
+
except FileNotFoundError:
|
|
323
|
+
if not suffix:
|
|
324
|
+
return False
|
|
325
|
+
continue
|
|
326
|
+
if not stat.S_ISREG(file_stat.st_mode) or file_stat.st_size < 0:
|
|
327
|
+
return False
|
|
328
|
+
total_bytes += file_stat.st_size
|
|
329
|
+
if total_bytes > limit:
|
|
330
|
+
return False
|
|
331
|
+
return True
|
|
332
|
+
except Exception as error:
|
|
333
|
+
if _sql_was_interrupted(error):
|
|
334
|
+
raise
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _result_table_integer_pk_high_watermark(cursor, quoted_table):
|
|
339
|
+
"""Return a conservative QCoDeS result-table row bound.
|
|
340
|
+
|
|
341
|
+
QCoDeS result tables use one ``id INTEGER PRIMARY KEY``. ``MAX(id)`` is a
|
|
342
|
+
bounded index lookup and safely overestimates the number of current rows
|
|
343
|
+
after deletions. Unknown or non-current schemas fail closed so an obsolete
|
|
344
|
+
controller ``result_count`` can never authorize an unbounded GROUP BY.
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
cursor.execute(f"PRAGMA table_info({quoted_table})")
|
|
348
|
+
primary_keys = [row for row in cursor.fetchall() if len(row) >= 6 and row[5]]
|
|
349
|
+
if len(primary_keys) != 1:
|
|
350
|
+
return None
|
|
351
|
+
primary_key = primary_keys[0]
|
|
352
|
+
if (
|
|
353
|
+
str(primary_key[1]) != "id"
|
|
354
|
+
or str(primary_key[2] or "").strip().upper() != "INTEGER"
|
|
355
|
+
or primary_key[5] != 1
|
|
356
|
+
):
|
|
357
|
+
return None
|
|
358
|
+
quoted_id = _sqlite_identifier(primary_key[1])
|
|
359
|
+
cursor.execute(f"SELECT MAX({quoted_id}) FROM {quoted_table}")
|
|
360
|
+
row = cursor.fetchone()
|
|
361
|
+
if row is None or len(row) != 1:
|
|
362
|
+
return None
|
|
363
|
+
value = row[0]
|
|
364
|
+
if value is None:
|
|
365
|
+
return 0
|
|
366
|
+
if type(value) is not int or value < 0:
|
|
367
|
+
return None
|
|
368
|
+
return value
|
|
369
|
+
except Exception as error:
|
|
370
|
+
if _sql_was_interrupted(error):
|
|
371
|
+
raise
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_snapshot_selected_run_detail(
|
|
376
|
+
database_path,
|
|
377
|
+
run_id,
|
|
378
|
+
guid,
|
|
379
|
+
run_metadata=None,
|
|
380
|
+
*,
|
|
381
|
+
expected_database_identity=None,
|
|
382
|
+
cancelled_callback=None,
|
|
383
|
+
connection_callback=None,
|
|
384
|
+
deadline=None,
|
|
385
|
+
):
|
|
386
|
+
"""Return a bounded immutable detail view without constructing a DataSet.
|
|
387
|
+
|
|
388
|
+
Every SQLite operation is finite and cancellable. Result-table values are
|
|
389
|
+
never materialised; only the existing bounded setpoint aggregates may
|
|
390
|
+
inspect a modest, already-counted result table.
|
|
391
|
+
"""
|
|
392
|
+
from qplot.datahandling.trusted_live_queries import (
|
|
393
|
+
TrustedRunRecord,
|
|
394
|
+
TrustedSelectedRunDetail,
|
|
395
|
+
TrustedSetpointSummary,
|
|
396
|
+
bounded_parameter_presentation,
|
|
397
|
+
bounded_parameter_views_from_run_metadata,
|
|
398
|
+
bounded_setpoint_presentation,
|
|
399
|
+
freeze_primitive_fields,
|
|
400
|
+
)
|
|
401
|
+
from qplot.datahandling.trusted_presentation import (
|
|
402
|
+
bounded_presentation_names,
|
|
403
|
+
build_selected_run_presentation,
|
|
404
|
+
)
|
|
405
|
+
from qplot.datahandling.trusted_snapshot import normalize_trusted_snapshot
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
run_id = int(run_id)
|
|
409
|
+
except (TypeError, ValueError, OverflowError) as error:
|
|
410
|
+
raise ValueError("run_id must be a positive integer") from error
|
|
411
|
+
guid = str(guid or "")
|
|
412
|
+
if run_id <= 0 or not guid:
|
|
413
|
+
raise ValueError("run_id and guid must identify one selected run")
|
|
414
|
+
|
|
415
|
+
conn = _read_only_connection(
|
|
416
|
+
database_path or get_DB_location(),
|
|
417
|
+
expected_database_identity,
|
|
418
|
+
cancelled_callback,
|
|
419
|
+
deadline,
|
|
420
|
+
)
|
|
421
|
+
try:
|
|
422
|
+
_notify_connection(connection_callback, conn)
|
|
423
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
424
|
+
cursor = conn.cursor()
|
|
425
|
+
columns = _snapshot_selected_run_columns(cursor)
|
|
426
|
+
values, unavailable, snapshot_omission = _bounded_snapshot_selected_run_values(
|
|
427
|
+
cursor,
|
|
428
|
+
run_id,
|
|
429
|
+
guid,
|
|
430
|
+
columns,
|
|
431
|
+
cancelled_callback=cancelled_callback,
|
|
432
|
+
deadline=deadline,
|
|
433
|
+
)
|
|
434
|
+
if values is None:
|
|
435
|
+
raise LookupError(f"Run {run_id} with GUID {guid} is no longer available.")
|
|
436
|
+
if str(values.get("guid") or "") != guid:
|
|
437
|
+
raise LookupError(f"Run {run_id} no longer has the selected GUID {guid}.")
|
|
438
|
+
|
|
439
|
+
database_values = {
|
|
440
|
+
name: value
|
|
441
|
+
for name, value in values.items()
|
|
442
|
+
if name in _SNAPSHOT_STANDARD_RUN_COLUMNS
|
|
443
|
+
}
|
|
444
|
+
database_values.pop("run_id", None)
|
|
445
|
+
materialized = materialize_run_basic_fields(database_values)
|
|
446
|
+
source_metadata = run_metadata or {}
|
|
447
|
+
for name in _SNAPSHOT_OBSERVATION_FIELDS:
|
|
448
|
+
if name not in source_metadata:
|
|
449
|
+
continue
|
|
450
|
+
value = source_metadata[name]
|
|
451
|
+
bounded = _snapshot_plain_value(value)
|
|
452
|
+
if bounded is not _SNAPSHOT_VALUE_UNAVAILABLE:
|
|
453
|
+
materialized[name] = bounded
|
|
454
|
+
materialized.update(
|
|
455
|
+
_snapshot_selected_experiment_fields(
|
|
456
|
+
cursor,
|
|
457
|
+
values.get("exp_id"),
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
materialized["guid"] = guid
|
|
461
|
+
materialized["run_id"] = run_id
|
|
462
|
+
materialized["database_modified_timestamp"] = _database_modified_timestamp(
|
|
463
|
+
cursor
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
layout_rows, layout_unavailable = _snapshot_selected_layout_rows(
|
|
467
|
+
cursor,
|
|
468
|
+
run_id,
|
|
469
|
+
)
|
|
470
|
+
parameters, source_parameters_truncated = (
|
|
471
|
+
bounded_parameter_views_from_run_metadata(
|
|
472
|
+
materialized,
|
|
473
|
+
layout_rows,
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
setpoint_names_list: list[str] = []
|
|
478
|
+
setpoint_names_seen: set[str] = set()
|
|
479
|
+
setpoint_names_truncated = False
|
|
480
|
+
for parameter in parameters:
|
|
481
|
+
for name in parameter.depends_on:
|
|
482
|
+
if not name or name in setpoint_names_seen:
|
|
483
|
+
continue
|
|
484
|
+
if len(setpoint_names_list) >= MAX_SNAPSHOT_SELECTED_SETPOINTS:
|
|
485
|
+
setpoint_names_truncated = True
|
|
486
|
+
break
|
|
487
|
+
setpoint_names_list.append(name)
|
|
488
|
+
setpoint_names_seen.add(name)
|
|
489
|
+
if setpoint_names_truncated:
|
|
490
|
+
break
|
|
491
|
+
setpoint_names = tuple(setpoint_names_list)
|
|
492
|
+
if not setpoint_names:
|
|
493
|
+
fallback_setpoints = materialized.get("sweep_parameters", ())
|
|
494
|
+
if isinstance(fallback_setpoints, (list, tuple)):
|
|
495
|
+
for index, name in enumerate(
|
|
496
|
+
islice(fallback_setpoints, MAX_SNAPSHOT_SELECTED_SETPOINTS + 1)
|
|
497
|
+
):
|
|
498
|
+
if index >= MAX_SNAPSHOT_SELECTED_SETPOINTS:
|
|
499
|
+
setpoint_names_truncated = True
|
|
500
|
+
break
|
|
501
|
+
if isinstance(name, str) and name:
|
|
502
|
+
setpoint_names_list.append(name)
|
|
503
|
+
setpoint_names = tuple(dict.fromkeys(setpoint_names_list))
|
|
504
|
+
elif fallback_setpoints:
|
|
505
|
+
setpoint_names_truncated = True
|
|
506
|
+
if setpoint_names_truncated:
|
|
507
|
+
unavailable = (*unavailable, "setpoint_summaries")
|
|
508
|
+
summary_values = _snapshot_selected_summary_values(
|
|
509
|
+
cursor,
|
|
510
|
+
materialized,
|
|
511
|
+
setpoint_names,
|
|
512
|
+
cancelled_callback=cancelled_callback,
|
|
513
|
+
deadline=deadline,
|
|
514
|
+
)
|
|
515
|
+
summaries = tuple(
|
|
516
|
+
TrustedSetpointSummary(
|
|
517
|
+
name,
|
|
518
|
+
summary.get("from"),
|
|
519
|
+
summary.get("to"),
|
|
520
|
+
_snapshot_positive_int(summary.get("steps")),
|
|
521
|
+
)
|
|
522
|
+
for name, summary in summary_values.items()
|
|
523
|
+
)
|
|
524
|
+
parameters, parameters_truncated = bounded_parameter_presentation(parameters)
|
|
525
|
+
parameters_truncated = bool(
|
|
526
|
+
parameters_truncated
|
|
527
|
+
or source_parameters_truncated
|
|
528
|
+
or materialized.get("parameters_truncated")
|
|
529
|
+
)
|
|
530
|
+
summaries, summaries_truncated = bounded_setpoint_presentation(summaries)
|
|
531
|
+
metadata = {
|
|
532
|
+
name: value
|
|
533
|
+
for name, value in values.items()
|
|
534
|
+
if (name not in _SNAPSHOT_STANDARD_RUN_COLUMNS and value is not None)
|
|
535
|
+
}
|
|
536
|
+
snapshot = normalize_trusted_snapshot(
|
|
537
|
+
values.get("snapshot"),
|
|
538
|
+
omission=snapshot_omission,
|
|
539
|
+
)
|
|
540
|
+
unavailable_fields = tuple(
|
|
541
|
+
dict.fromkeys(
|
|
542
|
+
(
|
|
543
|
+
*unavailable,
|
|
544
|
+
*layout_unavailable,
|
|
545
|
+
*(("parameters.presentation",) if parameters_truncated else ()),
|
|
546
|
+
*(
|
|
547
|
+
("setpoint_summaries.presentation",)
|
|
548
|
+
if summaries_truncated
|
|
549
|
+
else ()
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
public_unavailable_fields, _unavailable_truncated = bounded_presentation_names(
|
|
555
|
+
unavailable_fields
|
|
556
|
+
)
|
|
557
|
+
presentation = build_selected_run_presentation(
|
|
558
|
+
run_fields={**materialized, "run_id": run_id},
|
|
559
|
+
metadata_fields=metadata,
|
|
560
|
+
parameters=tuple(
|
|
561
|
+
{
|
|
562
|
+
"name": parameter.name,
|
|
563
|
+
"label": parameter.label,
|
|
564
|
+
"unit": parameter.unit,
|
|
565
|
+
"depends_on": parameter.depends_on,
|
|
566
|
+
"type": parameter.paramtype,
|
|
567
|
+
}
|
|
568
|
+
for parameter in parameters
|
|
569
|
+
),
|
|
570
|
+
snapshot_summary={
|
|
571
|
+
"Status": (
|
|
572
|
+
"available — loaded on demand"
|
|
573
|
+
if snapshot.status == "available"
|
|
574
|
+
else snapshot.status
|
|
575
|
+
),
|
|
576
|
+
"Message": snapshot.message,
|
|
577
|
+
"Input bytes": snapshot.input_bytes,
|
|
578
|
+
"Initial page nodes": len(snapshot.nodes),
|
|
579
|
+
},
|
|
580
|
+
setpoint_summaries=tuple(
|
|
581
|
+
{
|
|
582
|
+
"name": summary.name,
|
|
583
|
+
"from": summary.first,
|
|
584
|
+
"to": summary.last,
|
|
585
|
+
"steps": summary.steps,
|
|
586
|
+
}
|
|
587
|
+
for summary in summaries
|
|
588
|
+
),
|
|
589
|
+
unavailable_fields=public_unavailable_fields,
|
|
590
|
+
parameters_truncated=parameters_truncated,
|
|
591
|
+
)
|
|
592
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
593
|
+
return TrustedSelectedRunDetail(
|
|
594
|
+
run=TrustedRunRecord(
|
|
595
|
+
run_id,
|
|
596
|
+
freeze_primitive_fields(dict(presentation.run_fields)),
|
|
597
|
+
public_unavailable_fields,
|
|
598
|
+
),
|
|
599
|
+
parameters=parameters,
|
|
600
|
+
metadata=freeze_primitive_fields(dict(presentation.metadata_fields)),
|
|
601
|
+
snapshot=snapshot,
|
|
602
|
+
setpoint_summaries=summaries,
|
|
603
|
+
presentation=presentation,
|
|
604
|
+
unavailable_fields=public_unavailable_fields,
|
|
605
|
+
)
|
|
606
|
+
except sqlite3.OperationalError as error:
|
|
607
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
608
|
+
raise
|
|
609
|
+
finally:
|
|
610
|
+
try:
|
|
611
|
+
_notify_connection(connection_callback, None)
|
|
612
|
+
finally:
|
|
613
|
+
conn.close()
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
_SNAPSHOT_VALUE_UNAVAILABLE = object()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _snapshot_plain_value(value):
|
|
620
|
+
bounded, _size = _snapshot_plain_scalar(value)
|
|
621
|
+
if bounded is not _SNAPSHOT_VALUE_UNAVAILABLE:
|
|
622
|
+
return bounded
|
|
623
|
+
if not isinstance(value, (list, tuple)) or len(value) > 256:
|
|
624
|
+
return _SNAPSHOT_VALUE_UNAVAILABLE
|
|
625
|
+
|
|
626
|
+
items = []
|
|
627
|
+
total_bytes = 0
|
|
628
|
+
for item in value:
|
|
629
|
+
bounded, item_bytes = _snapshot_plain_scalar(item)
|
|
630
|
+
if bounded is _SNAPSHOT_VALUE_UNAVAILABLE:
|
|
631
|
+
return _SNAPSHOT_VALUE_UNAVAILABLE
|
|
632
|
+
total_bytes += item_bytes
|
|
633
|
+
if total_bytes > MAX_SNAPSHOT_SELECTED_OBSERVATION_BYTES:
|
|
634
|
+
return _SNAPSHOT_VALUE_UNAVAILABLE
|
|
635
|
+
items.append(bounded)
|
|
636
|
+
return tuple(items)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _snapshot_plain_scalar(value):
|
|
640
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
641
|
+
return value, 8
|
|
642
|
+
if isinstance(value, str):
|
|
643
|
+
value_bytes = len(value.encode("utf-8", errors="surrogatepass"))
|
|
644
|
+
if value_bytes <= MAX_SNAPSHOT_SELECTED_OBSERVATION_BYTES:
|
|
645
|
+
return value, value_bytes
|
|
646
|
+
return _SNAPSHOT_VALUE_UNAVAILABLE, 0
|
|
647
|
+
if isinstance(value, bytes):
|
|
648
|
+
if len(value) <= MAX_SNAPSHOT_SELECTED_OBSERVATION_BYTES:
|
|
649
|
+
return value, len(value)
|
|
650
|
+
return _SNAPSHOT_VALUE_UNAVAILABLE, 0
|
|
651
|
+
return _SNAPSHOT_VALUE_UNAVAILABLE, 0
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _snapshot_selected_run_columns(cursor):
|
|
655
|
+
cursor.execute('SELECT * FROM "runs" WHERE 0')
|
|
656
|
+
columns = tuple(description[0] for description in cursor.description or ())
|
|
657
|
+
if (
|
|
658
|
+
not {"run_id", "guid"}.issubset(columns)
|
|
659
|
+
or len(columns) > MAX_SNAPSHOT_SELECTED_RUN_COLUMNS
|
|
660
|
+
):
|
|
661
|
+
raise RuntimeError(
|
|
662
|
+
"The selected QCoDeS run schema exceeds the bounded detail plan."
|
|
663
|
+
)
|
|
664
|
+
return columns
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _bounded_snapshot_selected_run_values(
|
|
668
|
+
cursor,
|
|
669
|
+
run_id,
|
|
670
|
+
guid,
|
|
671
|
+
columns,
|
|
672
|
+
*,
|
|
673
|
+
cancelled_callback=None,
|
|
674
|
+
deadline=None,
|
|
675
|
+
):
|
|
676
|
+
from qplot.datahandling.trusted_snapshot import TrustedSnapshotOmission
|
|
677
|
+
|
|
678
|
+
values: dict[str, object] = {}
|
|
679
|
+
unavailable = []
|
|
680
|
+
accepted_bytes = 0
|
|
681
|
+
snapshot_omission = None
|
|
682
|
+
for name in columns:
|
|
683
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
684
|
+
quoted = _sqlite_identifier(name)
|
|
685
|
+
cursor.execute(
|
|
686
|
+
f"SELECT typeof({quoted}), length(CAST({quoted} AS BLOB)) "
|
|
687
|
+
'FROM "runs" WHERE "run_id" = ? AND "guid" = ? LIMIT 1',
|
|
688
|
+
(run_id, guid),
|
|
689
|
+
)
|
|
690
|
+
preflight = cursor.fetchone()
|
|
691
|
+
if preflight is None:
|
|
692
|
+
return None, (), None
|
|
693
|
+
value_type, value_bytes = preflight
|
|
694
|
+
if value_type == "null" and value_bytes is None:
|
|
695
|
+
values[name] = None
|
|
696
|
+
continue
|
|
697
|
+
if (
|
|
698
|
+
value_type not in {"integer", "real", "text", "blob"}
|
|
699
|
+
or type(value_bytes) is not int
|
|
700
|
+
or value_bytes < 0
|
|
701
|
+
):
|
|
702
|
+
raise RuntimeError(
|
|
703
|
+
"A selected-run scalar preflight returned invalid metadata."
|
|
704
|
+
)
|
|
705
|
+
if (
|
|
706
|
+
value_bytes > MAX_SNAPSHOT_SELECTED_RUN_SCALAR_BYTES
|
|
707
|
+
or accepted_bytes + value_bytes > MAX_SNAPSHOT_SELECTED_RUN_TOTAL_BYTES
|
|
708
|
+
):
|
|
709
|
+
values[name] = None
|
|
710
|
+
unavailable.append(name)
|
|
711
|
+
if name == "snapshot":
|
|
712
|
+
snapshot_omission = TrustedSnapshotOmission(
|
|
713
|
+
(
|
|
714
|
+
"payload_limit"
|
|
715
|
+
if value_bytes > MAX_SNAPSHOT_SELECTED_RUN_SCALAR_BYTES
|
|
716
|
+
else "detail_budget"
|
|
717
|
+
),
|
|
718
|
+
value_bytes,
|
|
719
|
+
)
|
|
720
|
+
continue
|
|
721
|
+
cursor.execute(
|
|
722
|
+
"SELECT CASE WHEN "
|
|
723
|
+
f"typeof({quoted}) = ? AND length(CAST({quoted} AS BLOB)) = ? "
|
|
724
|
+
f"THEN {quoted} ELSE NULL END "
|
|
725
|
+
'FROM "runs" WHERE "run_id" = ? AND "guid" = ? LIMIT 1',
|
|
726
|
+
(value_type, value_bytes, run_id, guid),
|
|
727
|
+
)
|
|
728
|
+
fetched = cursor.fetchone()
|
|
729
|
+
if fetched is None:
|
|
730
|
+
return None, (), None
|
|
731
|
+
value = fetched[0]
|
|
732
|
+
values[name] = value
|
|
733
|
+
if value is None:
|
|
734
|
+
unavailable.append(name)
|
|
735
|
+
if name == "snapshot":
|
|
736
|
+
snapshot_omission = TrustedSnapshotOmission(
|
|
737
|
+
"changed_during_read",
|
|
738
|
+
value_bytes,
|
|
739
|
+
)
|
|
740
|
+
else:
|
|
741
|
+
accepted_bytes += value_bytes
|
|
742
|
+
return values, tuple(unavailable), snapshot_omission
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _snapshot_selected_experiment_fields(cursor, exp_id):
|
|
746
|
+
if exp_id is None:
|
|
747
|
+
return {}
|
|
748
|
+
limit = _SNAPSHOT_SELECTED_LAYOUT_TEXT_BYTES
|
|
749
|
+
cursor.execute(
|
|
750
|
+
"SELECT "
|
|
751
|
+
"CASE WHEN length(CAST(name AS BLOB)) <= ? THEN name END, "
|
|
752
|
+
"CASE WHEN length(CAST(sample_name AS BLOB)) <= ? THEN sample_name END "
|
|
753
|
+
"FROM experiments WHERE exp_id = ? LIMIT 1",
|
|
754
|
+
(limit, limit, exp_id),
|
|
755
|
+
)
|
|
756
|
+
row = cursor.fetchone()
|
|
757
|
+
if row is None:
|
|
758
|
+
return {}
|
|
759
|
+
return {"exp_name": row[0], "sample_name": row[1]}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _snapshot_selected_layout_rows(cursor, run_id):
|
|
763
|
+
limit = _SNAPSHOT_SELECTED_LAYOUT_TEXT_BYTES
|
|
764
|
+
cursor.execute(
|
|
765
|
+
"SELECT layout_id, "
|
|
766
|
+
"CASE WHEN length(CAST(parameter AS BLOB)) <= ? THEN parameter END, "
|
|
767
|
+
"CASE WHEN length(CAST(label AS BLOB)) <= ? THEN label END, "
|
|
768
|
+
"CASE WHEN length(CAST(unit AS BLOB)) <= ? THEN unit END, "
|
|
769
|
+
"CASE WHEN length(CAST(inferred_from AS BLOB)) <= ? THEN inferred_from END "
|
|
770
|
+
"FROM layouts WHERE run_id = ? ORDER BY layout_id LIMIT ?",
|
|
771
|
+
(
|
|
772
|
+
limit,
|
|
773
|
+
limit,
|
|
774
|
+
limit,
|
|
775
|
+
limit,
|
|
776
|
+
run_id,
|
|
777
|
+
MAX_SNAPSHOT_SELECTED_LAYOUT_ROWS + 1,
|
|
778
|
+
),
|
|
779
|
+
)
|
|
780
|
+
rows = tuple(tuple(row) for row in cursor.fetchall())
|
|
781
|
+
if len(rows) <= MAX_SNAPSHOT_SELECTED_LAYOUT_ROWS:
|
|
782
|
+
return rows, ()
|
|
783
|
+
return rows[:MAX_SNAPSHOT_SELECTED_LAYOUT_ROWS], ("layouts",)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _snapshot_selected_summary_values(
|
|
787
|
+
cursor,
|
|
788
|
+
metadata,
|
|
789
|
+
setpoint_names,
|
|
790
|
+
*,
|
|
791
|
+
cancelled_callback=None,
|
|
792
|
+
deadline=None,
|
|
793
|
+
):
|
|
794
|
+
from qplot.datahandling.trusted_live_queries import TrustedMetadataQueryAdapter
|
|
795
|
+
|
|
796
|
+
result_count = _snapshot_positive_int(metadata.get("result_count"))
|
|
797
|
+
summaries = {}
|
|
798
|
+
if (
|
|
799
|
+
result_count is not None
|
|
800
|
+
and result_count <= MAX_SELECTED_RUN_SETPOINT_SUMMARY_ROWS
|
|
801
|
+
and metadata.get("result_table_name")
|
|
802
|
+
and setpoint_names
|
|
803
|
+
):
|
|
804
|
+
summaries.update(
|
|
805
|
+
_selected_run_setpoint_summaries_from_cursor(
|
|
806
|
+
cursor,
|
|
807
|
+
metadata["result_table_name"],
|
|
808
|
+
setpoint_names,
|
|
809
|
+
cancelled_callback=cancelled_callback,
|
|
810
|
+
deadline=deadline,
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
planned_steps = TrustedMetadataQueryAdapter._planned_setpoint_steps(
|
|
814
|
+
metadata, setpoint_names,
|
|
815
|
+
)
|
|
816
|
+
# The largest run-wide shape cannot describe independent dependent trees.
|
|
817
|
+
# Retain the controller's shape only when no dependency declaration exists
|
|
818
|
+
# and a single dependent makes its association unambiguous.
|
|
819
|
+
if (
|
|
820
|
+
not _parameter_dependencies(_json_dict(metadata.get("run_description")))
|
|
821
|
+
and len(metadata.get("measure_parameters") or ()) == 1
|
|
822
|
+
):
|
|
823
|
+
shape = metadata.get("setpoint_shape") or metadata.get("point_shape") or ()
|
|
824
|
+
if len(shape) == len(setpoint_names):
|
|
825
|
+
planned_steps = dict(zip(setpoint_names, shape, strict=True))
|
|
826
|
+
for name, steps in planned_steps.items():
|
|
827
|
+
summaries.setdefault(name, {}).setdefault("steps", steps)
|
|
828
|
+
return {name: summaries[name] for name in setpoint_names if name in summaries}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _snapshot_positive_int(value):
|
|
832
|
+
try:
|
|
833
|
+
value = int(value)
|
|
834
|
+
except (TypeError, ValueError, OverflowError):
|
|
835
|
+
return None
|
|
836
|
+
return value if value > 0 else None
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _selected_run_setpoint_summary(cursor, quoted_table, parameter):
|
|
840
|
+
"""Return first, last, and distinct count for one setpoint column."""
|
|
841
|
+
column = _sqlite_identifier(parameter)
|
|
842
|
+
try:
|
|
843
|
+
cursor.execute(f"""
|
|
844
|
+
WITH distinct_values(value, first_rowid) AS (
|
|
845
|
+
SELECT {column}, MIN(rowid)
|
|
846
|
+
FROM {quoted_table}
|
|
847
|
+
WHERE {column} IS NOT NULL
|
|
848
|
+
GROUP BY {column}
|
|
849
|
+
), summary_values(first_value, last_value, steps) AS (
|
|
850
|
+
SELECT
|
|
851
|
+
(
|
|
852
|
+
SELECT value
|
|
853
|
+
FROM distinct_values
|
|
854
|
+
ORDER BY first_rowid ASC
|
|
855
|
+
LIMIT 1
|
|
856
|
+
),
|
|
857
|
+
(
|
|
858
|
+
SELECT value
|
|
859
|
+
FROM distinct_values
|
|
860
|
+
ORDER BY first_rowid DESC
|
|
861
|
+
LIMIT 1
|
|
862
|
+
),
|
|
863
|
+
(SELECT COUNT(*) FROM distinct_values)
|
|
864
|
+
)
|
|
865
|
+
SELECT
|
|
866
|
+
CASE WHEN length(CAST(first_value AS BLOB)) <=
|
|
867
|
+
{MAX_SELECTED_RUN_SETPOINT_SUMMARY_SCALAR_BYTES}
|
|
868
|
+
THEN first_value END,
|
|
869
|
+
CASE WHEN length(CAST(last_value AS BLOB)) <=
|
|
870
|
+
{MAX_SELECTED_RUN_SETPOINT_SUMMARY_SCALAR_BYTES}
|
|
871
|
+
THEN last_value END,
|
|
872
|
+
steps
|
|
873
|
+
FROM summary_values
|
|
874
|
+
""")
|
|
875
|
+
first_value, last_value, count = cursor.fetchone()
|
|
876
|
+
count = int(count or 0)
|
|
877
|
+
except Exception as error:
|
|
878
|
+
if _sql_was_interrupted(error):
|
|
879
|
+
raise
|
|
880
|
+
return {}
|
|
881
|
+
if count <= 0 or first_value is None or last_value is None:
|
|
882
|
+
return {}
|
|
883
|
+
return {
|
|
884
|
+
"from": first_value,
|
|
885
|
+
"to": last_value,
|
|
886
|
+
"steps": count,
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def get_runs_via_sql(
|
|
891
|
+
database_path=None,
|
|
892
|
+
include_details=True,
|
|
893
|
+
cancelled_callback=None,
|
|
894
|
+
connection_callback=None,
|
|
895
|
+
expected_database_identity=None,
|
|
896
|
+
deadline=None,
|
|
897
|
+
):
|
|
898
|
+
"""
|
|
899
|
+
Read from the currently initialised QCoDeS database and fetches all data to
|
|
900
|
+
be displayed in Main Window runList
|
|
901
|
+
|
|
902
|
+
Returns
|
|
903
|
+
-------
|
|
904
|
+
outDict : dict{int: dict}
|
|
905
|
+
A nested dictionary of requried data.
|
|
906
|
+
Has layout:
|
|
907
|
+
run_id : {column_name: column_data}
|
|
908
|
+
|
|
909
|
+
"""
|
|
910
|
+
conn = _read_only_connection(
|
|
911
|
+
database_path or get_DB_location(),
|
|
912
|
+
expected_database_identity,
|
|
913
|
+
cancelled_callback,
|
|
914
|
+
deadline,
|
|
915
|
+
)
|
|
916
|
+
try:
|
|
917
|
+
_notify_connection(connection_callback, conn)
|
|
918
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
919
|
+
cursor = conn.cursor()
|
|
920
|
+
rows = _fetch_run_rows(
|
|
921
|
+
cursor,
|
|
922
|
+
empty_as_none=False,
|
|
923
|
+
include_details=include_details,
|
|
924
|
+
)
|
|
925
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
926
|
+
return rows
|
|
927
|
+
except sqlite3.OperationalError as error:
|
|
928
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
929
|
+
raise
|
|
930
|
+
finally:
|
|
931
|
+
try:
|
|
932
|
+
_notify_connection(connection_callback, None)
|
|
933
|
+
finally:
|
|
934
|
+
conn.close()
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def get_runs_basic_via_sql(
|
|
938
|
+
database_path=None,
|
|
939
|
+
cancelled_callback=None,
|
|
940
|
+
connection_callback=None,
|
|
941
|
+
expected_database_identity=None,
|
|
942
|
+
deadline=None,
|
|
943
|
+
):
|
|
944
|
+
"""
|
|
945
|
+
Read the run list without scanning result tables.
|
|
946
|
+
|
|
947
|
+
This is the fast path for opening large databases. It includes enough
|
|
948
|
+
metadata to populate the run table and measurement placeholders, while
|
|
949
|
+
leaving expensive counts, setpoint-shape inference, and storage estimates
|
|
950
|
+
to later targeted loads.
|
|
951
|
+
|
|
952
|
+
"""
|
|
953
|
+
return get_runs_via_sql(
|
|
954
|
+
database_path=database_path,
|
|
955
|
+
include_details=False,
|
|
956
|
+
cancelled_callback=cancelled_callback,
|
|
957
|
+
connection_callback=connection_callback,
|
|
958
|
+
expected_database_identity=expected_database_identity,
|
|
959
|
+
deadline=deadline,
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def iter_run_detail_batches_via_sql(
|
|
964
|
+
database_path,
|
|
965
|
+
run_ids,
|
|
966
|
+
batch_size=1,
|
|
967
|
+
infer_missing_shapes=True,
|
|
968
|
+
include_storage_bytes=True,
|
|
969
|
+
include_storage_estimate=False,
|
|
970
|
+
include_read_setpoint_count=True,
|
|
971
|
+
cancelled_callback=None,
|
|
972
|
+
connection_callback=None,
|
|
973
|
+
expected_database_identity=None,
|
|
974
|
+
deadline=None,
|
|
975
|
+
):
|
|
976
|
+
"""
|
|
977
|
+
Yield detailed run metadata in small batches.
|
|
978
|
+
|
|
979
|
+
This keeps the initial load responsive while allowing the GUI to fill in
|
|
980
|
+
expensive counts, setpoint shapes, and storage sizes progressively.
|
|
981
|
+
|
|
982
|
+
"""
|
|
983
|
+
run_ids = [run_id for run_id in run_ids if run_id is not None]
|
|
984
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
985
|
+
if not run_ids:
|
|
986
|
+
return
|
|
987
|
+
|
|
988
|
+
batch_size = max(1, int(batch_size or 1))
|
|
989
|
+
conn = _read_only_connection(
|
|
990
|
+
database_path or get_DB_location(),
|
|
991
|
+
expected_database_identity,
|
|
992
|
+
cancelled_callback,
|
|
993
|
+
deadline,
|
|
994
|
+
)
|
|
995
|
+
try:
|
|
996
|
+
_notify_connection(connection_callback, conn)
|
|
997
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
998
|
+
cursor = conn.cursor()
|
|
999
|
+
for offset in range(0, len(run_ids), batch_size):
|
|
1000
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1001
|
+
batch = run_ids[offset : offset + batch_size]
|
|
1002
|
+
placeholders = ", ".join("?" for _ in batch)
|
|
1003
|
+
rows = _fetch_run_rows(
|
|
1004
|
+
cursor,
|
|
1005
|
+
f"WHERE runs.run_id IN ({placeholders})",
|
|
1006
|
+
tuple(batch),
|
|
1007
|
+
empty_as_none=False,
|
|
1008
|
+
include_details=True,
|
|
1009
|
+
infer_missing_shapes=infer_missing_shapes,
|
|
1010
|
+
include_storage_bytes=include_storage_bytes,
|
|
1011
|
+
include_storage_estimate=include_storage_estimate,
|
|
1012
|
+
include_read_setpoint_count=include_read_setpoint_count,
|
|
1013
|
+
)
|
|
1014
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1015
|
+
yield rows or {}
|
|
1016
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1017
|
+
except sqlite3.OperationalError as error:
|
|
1018
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
1019
|
+
raise
|
|
1020
|
+
finally:
|
|
1021
|
+
try:
|
|
1022
|
+
_notify_connection(connection_callback, None)
|
|
1023
|
+
finally:
|
|
1024
|
+
conn.close()
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def iter_run_shape_batches_via_sql(
|
|
1028
|
+
database_path,
|
|
1029
|
+
run_ids,
|
|
1030
|
+
batch_size=1,
|
|
1031
|
+
cancelled_callback=None,
|
|
1032
|
+
connection_callback=None,
|
|
1033
|
+
expected_database_identity=None,
|
|
1034
|
+
deadline=None,
|
|
1035
|
+
):
|
|
1036
|
+
"""
|
|
1037
|
+
Yield setpoint-shape metadata for runs that need result-table inference.
|
|
1038
|
+
|
|
1039
|
+
This can require COUNT(DISTINCT ...) scans on result columns, so it runs
|
|
1040
|
+
after cheap row counts are visible.
|
|
1041
|
+
|
|
1042
|
+
"""
|
|
1043
|
+
run_ids = [run_id for run_id in run_ids if run_id is not None]
|
|
1044
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1045
|
+
if not run_ids:
|
|
1046
|
+
return
|
|
1047
|
+
|
|
1048
|
+
batch_size = max(1, int(batch_size or 1))
|
|
1049
|
+
conn = _read_only_connection(
|
|
1050
|
+
database_path or get_DB_location(),
|
|
1051
|
+
expected_database_identity,
|
|
1052
|
+
cancelled_callback,
|
|
1053
|
+
deadline,
|
|
1054
|
+
)
|
|
1055
|
+
try:
|
|
1056
|
+
_notify_connection(connection_callback, conn)
|
|
1057
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
1058
|
+
cursor = conn.cursor()
|
|
1059
|
+
for offset in range(0, len(run_ids), batch_size):
|
|
1060
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1061
|
+
batch = run_ids[offset : offset + batch_size]
|
|
1062
|
+
placeholders = ", ".join("?" for _ in batch)
|
|
1063
|
+
rows = _fetch_run_rows(
|
|
1064
|
+
cursor,
|
|
1065
|
+
f"WHERE runs.run_id IN ({placeholders})",
|
|
1066
|
+
tuple(batch),
|
|
1067
|
+
empty_as_none=False,
|
|
1068
|
+
include_details=True,
|
|
1069
|
+
infer_missing_shapes=True,
|
|
1070
|
+
include_storage_bytes=False,
|
|
1071
|
+
include_read_setpoint_count=False,
|
|
1072
|
+
)
|
|
1073
|
+
rows = {
|
|
1074
|
+
run_id: metadata
|
|
1075
|
+
for run_id, metadata in (rows or {}).items()
|
|
1076
|
+
if (
|
|
1077
|
+
metadata.get("setpoint_shape")
|
|
1078
|
+
or metadata.get("point_shape")
|
|
1079
|
+
or metadata.get("setpoint_count") is not None
|
|
1080
|
+
)
|
|
1081
|
+
}
|
|
1082
|
+
if rows:
|
|
1083
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1084
|
+
yield rows
|
|
1085
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1086
|
+
except sqlite3.OperationalError as error:
|
|
1087
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
1088
|
+
raise
|
|
1089
|
+
finally:
|
|
1090
|
+
try:
|
|
1091
|
+
_notify_connection(connection_callback, None)
|
|
1092
|
+
finally:
|
|
1093
|
+
conn.close()
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def iter_run_storage_batches_via_sql(
|
|
1097
|
+
database_path,
|
|
1098
|
+
run_ids,
|
|
1099
|
+
batch_size=25,
|
|
1100
|
+
cancelled_callback=None,
|
|
1101
|
+
connection_callback=None,
|
|
1102
|
+
expected_database_identity=None,
|
|
1103
|
+
deadline=None,
|
|
1104
|
+
):
|
|
1105
|
+
"""
|
|
1106
|
+
Yield per-run storage sizes after the cheap detail pass has completed.
|
|
1107
|
+
|
|
1108
|
+
Storage lookup can require a dbstat scan over the database. Keeping this as
|
|
1109
|
+
a separate pass prevents size calculation from blocking result counts and
|
|
1110
|
+
completion metadata on very large databases.
|
|
1111
|
+
|
|
1112
|
+
"""
|
|
1113
|
+
run_ids = [run_id for run_id in run_ids if run_id is not None]
|
|
1114
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1115
|
+
if not run_ids:
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1118
|
+
batch_size = max(1, int(batch_size or 1))
|
|
1119
|
+
conn = _read_only_connection(
|
|
1120
|
+
database_path or get_DB_location(),
|
|
1121
|
+
expected_database_identity,
|
|
1122
|
+
cancelled_callback,
|
|
1123
|
+
deadline,
|
|
1124
|
+
)
|
|
1125
|
+
try:
|
|
1126
|
+
_notify_connection(connection_callback, conn)
|
|
1127
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
1128
|
+
cursor = conn.cursor()
|
|
1129
|
+
run_tables = _run_storage_tables(cursor, run_ids)
|
|
1130
|
+
table_names = {
|
|
1131
|
+
metadata.get("result_table_name")
|
|
1132
|
+
for metadata in run_tables.values()
|
|
1133
|
+
if metadata.get("result_table_name")
|
|
1134
|
+
}
|
|
1135
|
+
sizes = _table_storage_bytes_by_name(cursor, table_names)
|
|
1136
|
+
if not sizes:
|
|
1137
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1138
|
+
return
|
|
1139
|
+
|
|
1140
|
+
for offset in range(0, len(run_ids), batch_size):
|
|
1141
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1142
|
+
rows = {}
|
|
1143
|
+
for run_id in run_ids[offset : offset + batch_size]:
|
|
1144
|
+
metadata = run_tables.get(run_id)
|
|
1145
|
+
if not metadata:
|
|
1146
|
+
continue
|
|
1147
|
+
|
|
1148
|
+
storage_bytes = sizes.get(metadata.get("result_table_name"))
|
|
1149
|
+
if storage_bytes is None:
|
|
1150
|
+
continue
|
|
1151
|
+
|
|
1152
|
+
rows[run_id] = {
|
|
1153
|
+
"guid": metadata.get("guid"),
|
|
1154
|
+
"storage_bytes": storage_bytes,
|
|
1155
|
+
"storage_bytes_estimated": False,
|
|
1156
|
+
}
|
|
1157
|
+
if rows:
|
|
1158
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1159
|
+
yield rows
|
|
1160
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1161
|
+
except sqlite3.OperationalError as error:
|
|
1162
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
1163
|
+
raise
|
|
1164
|
+
finally:
|
|
1165
|
+
try:
|
|
1166
|
+
_notify_connection(connection_callback, None)
|
|
1167
|
+
finally:
|
|
1168
|
+
conn.close()
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def find_new_runs(
|
|
1172
|
+
last_run_id,
|
|
1173
|
+
database_path=None,
|
|
1174
|
+
cancelled_callback=None,
|
|
1175
|
+
connection_callback=None,
|
|
1176
|
+
expected_database_identity=None,
|
|
1177
|
+
deadline=None,
|
|
1178
|
+
):
|
|
1179
|
+
"""
|
|
1180
|
+
Fetch all runs created after the last seen run ID.
|
|
1181
|
+
|
|
1182
|
+
Run IDs provide a monotonic cursor even when a run has no timestamp or
|
|
1183
|
+
multiple runs share the same timestamp.
|
|
1184
|
+
|
|
1185
|
+
Parameters
|
|
1186
|
+
----------
|
|
1187
|
+
last_run_id : int
|
|
1188
|
+
Only runs with a greater run ID will be returned.
|
|
1189
|
+
|
|
1190
|
+
Returns
|
|
1191
|
+
-------
|
|
1192
|
+
outDict : dict{int: dict}
|
|
1193
|
+
A nested dictionary of requried data.
|
|
1194
|
+
Has layout:
|
|
1195
|
+
run_id : {column_name: column_data}
|
|
1196
|
+
"""
|
|
1197
|
+
conn = _read_only_connection(
|
|
1198
|
+
database_path or get_DB_location(),
|
|
1199
|
+
expected_database_identity,
|
|
1200
|
+
cancelled_callback,
|
|
1201
|
+
deadline,
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
try:
|
|
1205
|
+
_notify_connection(connection_callback, conn)
|
|
1206
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
1207
|
+
cursor = conn.cursor()
|
|
1208
|
+
rows = _fetch_run_rows(cursor, "WHERE runs.run_id > ?", (last_run_id,))
|
|
1209
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
1210
|
+
return rows
|
|
1211
|
+
except sqlite3.OperationalError as error:
|
|
1212
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
1213
|
+
raise
|
|
1214
|
+
finally:
|
|
1215
|
+
try:
|
|
1216
|
+
_notify_connection(connection_callback, None)
|
|
1217
|
+
finally:
|
|
1218
|
+
conn.close()
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _fetch_run_rows(
|
|
1222
|
+
cursor,
|
|
1223
|
+
where="",
|
|
1224
|
+
params=(),
|
|
1225
|
+
empty_as_none=True,
|
|
1226
|
+
include_details=True,
|
|
1227
|
+
infer_missing_shapes=True,
|
|
1228
|
+
include_storage_bytes=True,
|
|
1229
|
+
include_storage_estimate=False,
|
|
1230
|
+
include_read_setpoint_count=True,
|
|
1231
|
+
storage_bytes_by_table=None,
|
|
1232
|
+
):
|
|
1233
|
+
optional_columns = _existing_run_columns(cursor, ["measurement_exception"])
|
|
1234
|
+
optional_select = "".join(
|
|
1235
|
+
f", runs.{_sqlite_identifier(column)} AS {_sqlite_identifier(column)}"
|
|
1236
|
+
for column in optional_columns
|
|
1237
|
+
)
|
|
1238
|
+
cursor.execute(
|
|
1239
|
+
f"""
|
|
1240
|
+
SELECT
|
|
1241
|
+
runs.run_id,
|
|
1242
|
+
runs.exp_id,
|
|
1243
|
+
runs.name,
|
|
1244
|
+
runs.run_timestamp,
|
|
1245
|
+
runs.completed_timestamp,
|
|
1246
|
+
runs.is_completed,
|
|
1247
|
+
runs.guid,
|
|
1248
|
+
runs.result_table_name,
|
|
1249
|
+
runs.parameters,
|
|
1250
|
+
runs.run_description,
|
|
1251
|
+
experiments.name AS exp_name,
|
|
1252
|
+
experiments.sample_name
|
|
1253
|
+
{optional_select}
|
|
1254
|
+
FROM runs
|
|
1255
|
+
LEFT JOIN experiments ON runs.exp_id = experiments.exp_id
|
|
1256
|
+
{where}
|
|
1257
|
+
""",
|
|
1258
|
+
params,
|
|
1259
|
+
)
|
|
1260
|
+
values = cursor.fetchall()
|
|
1261
|
+
|
|
1262
|
+
if len(values) == 0:
|
|
1263
|
+
return None if empty_as_none else {}
|
|
1264
|
+
|
|
1265
|
+
column_names = [desc[0] for desc in cursor.description]
|
|
1266
|
+
|
|
1267
|
+
outDict = {}
|
|
1268
|
+
database_modified_timestamp = _database_modified_timestamp(cursor)
|
|
1269
|
+
for row in values:
|
|
1270
|
+
metadata = dict(zip(column_names[1:], row[1:], strict=False))
|
|
1271
|
+
metadata["database_modified_timestamp"] = database_modified_timestamp
|
|
1272
|
+
metadata = materialize_run_basic_fields(metadata)
|
|
1273
|
+
if include_details:
|
|
1274
|
+
_add_run_detail_fields(
|
|
1275
|
+
cursor,
|
|
1276
|
+
metadata,
|
|
1277
|
+
infer_missing_shapes=infer_missing_shapes,
|
|
1278
|
+
include_storage_bytes=include_storage_bytes,
|
|
1279
|
+
include_storage_estimate=include_storage_estimate,
|
|
1280
|
+
include_read_setpoint_count=include_read_setpoint_count,
|
|
1281
|
+
storage_bytes_by_table=storage_bytes_by_table,
|
|
1282
|
+
)
|
|
1283
|
+
outDict[row[0]] = metadata
|
|
1284
|
+
|
|
1285
|
+
return outDict
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _add_run_basic_fields(metadata):
|
|
1289
|
+
run_description = _json_dict(metadata.get("run_description"))
|
|
1290
|
+
measure_parameters, sweep_parameters, parameters_truncated = (
|
|
1291
|
+
_bounded_parameter_roles(
|
|
1292
|
+
run_description,
|
|
1293
|
+
metadata.get("parameters"),
|
|
1294
|
+
)
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
point_shape, shape_truncated = _bounded_point_shape(
|
|
1298
|
+
run_description,
|
|
1299
|
+
measure_parameters,
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
metadata["measure_parameters"] = measure_parameters
|
|
1303
|
+
metadata["sweep_parameters"] = sweep_parameters
|
|
1304
|
+
metadata["parameters_truncated"] = bool(parameters_truncated or shape_truncated)
|
|
1305
|
+
metadata["point_shape"] = point_shape
|
|
1306
|
+
metadata["setpoint_shape"] = metadata["point_shape"]
|
|
1307
|
+
shape_source = "planned" if metadata["setpoint_shape"] else None
|
|
1308
|
+
metadata["setpoint_shape_source"] = shape_source
|
|
1309
|
+
expected_results, expected_shape_truncated = _bounded_expected_results_from_shapes(
|
|
1310
|
+
run_description,
|
|
1311
|
+
measure_parameters,
|
|
1312
|
+
)
|
|
1313
|
+
metadata["parameters_truncated"] = bool(
|
|
1314
|
+
metadata["parameters_truncated"] or expected_shape_truncated
|
|
1315
|
+
)
|
|
1316
|
+
metadata["expected_results"] = expected_results
|
|
1317
|
+
metadata["expected_results_source"] = (
|
|
1318
|
+
"planned" if expected_results is not None else None
|
|
1319
|
+
)
|
|
1320
|
+
metadata["setpoint_count"] = _shape_size(metadata["setpoint_shape"])
|
|
1321
|
+
metadata["setpoint_count_source"] = shape_source
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
def materialize_run_basic_fields(metadata):
|
|
1325
|
+
"""Return one run mapping with qPlot's shared derived basic fields.
|
|
1326
|
+
|
|
1327
|
+
Snapshot readers and the trusted fixed-query adapter deliberately share
|
|
1328
|
+
this transformation. Keeping it independent of cursors, connections,
|
|
1329
|
+
QCoDeS objects, and filesystem access prevents the two read paths from
|
|
1330
|
+
drifting while allowing the trusted path to materialise only primitive
|
|
1331
|
+
query results.
|
|
1332
|
+
"""
|
|
1333
|
+
materialized = dict(metadata or {})
|
|
1334
|
+
_add_run_basic_fields(materialized)
|
|
1335
|
+
return materialized
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
_OBSERVATION_UNSET = object()
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def materialize_run_observation(
|
|
1342
|
+
metadata,
|
|
1343
|
+
*,
|
|
1344
|
+
result_count=None,
|
|
1345
|
+
setpoint_shape=None,
|
|
1346
|
+
setpoint_count=None,
|
|
1347
|
+
storage_bytes=None,
|
|
1348
|
+
storage_bytes_estimated=None,
|
|
1349
|
+
read_setpoint_count=_OBSERVATION_UNSET,
|
|
1350
|
+
):
|
|
1351
|
+
"""Return shared detail fields from already-bounded observations.
|
|
1352
|
+
|
|
1353
|
+
All database work happens before this function. The snapshot path can
|
|
1354
|
+
continue collecting observations with cursors, while the trusted adapter
|
|
1355
|
+
supplies the same values from fixed supervisor queries.
|
|
1356
|
+
An omitted acquired count retains the prior observation; explicit ``None``
|
|
1357
|
+
clears a count whose structural evidence is no longer available.
|
|
1358
|
+
"""
|
|
1359
|
+
materialized = materialize_run_basic_fields(metadata)
|
|
1360
|
+
if result_count is not None:
|
|
1361
|
+
materialized["result_count"] = int(result_count)
|
|
1362
|
+
elif "result_count" not in materialized:
|
|
1363
|
+
materialized["result_count"] = None
|
|
1364
|
+
|
|
1365
|
+
if setpoint_count is not None or setpoint_shape is not None:
|
|
1366
|
+
normalized_shape = (
|
|
1367
|
+
[int(size) for size in setpoint_shape] if setpoint_shape else None
|
|
1368
|
+
)
|
|
1369
|
+
materialized["setpoint_shape"] = normalized_shape
|
|
1370
|
+
materialized["point_shape"] = _point_shape_from_setpoint_shape(
|
|
1371
|
+
normalized_shape,
|
|
1372
|
+
materialized.get("measure_parameters"),
|
|
1373
|
+
materialized.get("result_count"),
|
|
1374
|
+
)
|
|
1375
|
+
materialized["setpoint_shape_source"] = "observed" if normalized_shape else None
|
|
1376
|
+
materialized["setpoint_count"] = (
|
|
1377
|
+
int(setpoint_count) if setpoint_count is not None else None
|
|
1378
|
+
)
|
|
1379
|
+
materialized["setpoint_count_source"] = (
|
|
1380
|
+
"observed" if setpoint_count is not None else None
|
|
1381
|
+
)
|
|
1382
|
+
materialized["expected_results"] = None
|
|
1383
|
+
materialized["expected_results_source"] = None
|
|
1384
|
+
|
|
1385
|
+
if read_setpoint_count is not _OBSERVATION_UNSET:
|
|
1386
|
+
materialized["read_setpoint_count"] = (
|
|
1387
|
+
int(read_setpoint_count) if read_setpoint_count is not None else None
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
if storage_bytes is not None or storage_bytes_estimated is not None:
|
|
1391
|
+
materialized["storage_bytes"] = (
|
|
1392
|
+
int(storage_bytes) if storage_bytes is not None else None
|
|
1393
|
+
)
|
|
1394
|
+
materialized["storage_bytes_estimated"] = storage_bytes_estimated
|
|
1395
|
+
|
|
1396
|
+
_add_completed_observed_result_count(materialized)
|
|
1397
|
+
return materialized
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
def _add_run_detail_fields(
|
|
1401
|
+
cursor,
|
|
1402
|
+
metadata,
|
|
1403
|
+
infer_missing_shapes=True,
|
|
1404
|
+
include_storage_bytes=True,
|
|
1405
|
+
include_storage_estimate=False,
|
|
1406
|
+
include_read_setpoint_count=True,
|
|
1407
|
+
storage_bytes_by_table=None,
|
|
1408
|
+
):
|
|
1409
|
+
measure_parameters = metadata.get("measure_parameters") or []
|
|
1410
|
+
sweep_parameters = metadata.get("sweep_parameters") or []
|
|
1411
|
+
|
|
1412
|
+
metadata["result_count"] = _result_count(cursor, metadata.get("result_table_name"))
|
|
1413
|
+
observed_setpoints = None
|
|
1414
|
+
if infer_missing_shapes and not metadata["point_shape"]:
|
|
1415
|
+
observed_setpoints = _add_observed_shape_fields(cursor, metadata)
|
|
1416
|
+
_add_completed_observed_result_count(metadata)
|
|
1417
|
+
if include_read_setpoint_count and (
|
|
1418
|
+
not bool(metadata.get("is_completed"))
|
|
1419
|
+
or _is_keyboard_interrupt(metadata.get("measurement_exception"))
|
|
1420
|
+
):
|
|
1421
|
+
if observed_setpoints is None:
|
|
1422
|
+
observed_setpoints = _run_setpoint_observation(
|
|
1423
|
+
cursor,
|
|
1424
|
+
metadata.get("result_table_name"),
|
|
1425
|
+
_json_dict(metadata.get("run_description")),
|
|
1426
|
+
measure_parameters,
|
|
1427
|
+
sweep_parameters,
|
|
1428
|
+
)
|
|
1429
|
+
metadata["read_setpoint_count"] = observed_setpoints["count"]
|
|
1430
|
+
if include_storage_bytes:
|
|
1431
|
+
table_name = metadata.get("result_table_name")
|
|
1432
|
+
if storage_bytes_by_table is not None:
|
|
1433
|
+
storage_bytes = storage_bytes_by_table.get(table_name)
|
|
1434
|
+
storage_size = _StorageSize(
|
|
1435
|
+
storage_bytes,
|
|
1436
|
+
"exact" if storage_bytes is not None else "unavailable",
|
|
1437
|
+
)
|
|
1438
|
+
else:
|
|
1439
|
+
storage_size = _table_storage_bytes(
|
|
1440
|
+
cursor,
|
|
1441
|
+
table_name,
|
|
1442
|
+
result_count=metadata.get("result_count"),
|
|
1443
|
+
)
|
|
1444
|
+
_add_storage_size_fields(metadata, storage_size)
|
|
1445
|
+
elif include_storage_estimate:
|
|
1446
|
+
storage_bytes = _estimated_table_storage_bytes(
|
|
1447
|
+
cursor,
|
|
1448
|
+
metadata.get("result_table_name"),
|
|
1449
|
+
result_count=metadata.get("result_count"),
|
|
1450
|
+
)
|
|
1451
|
+
_add_storage_size_fields(
|
|
1452
|
+
metadata,
|
|
1453
|
+
_StorageSize(
|
|
1454
|
+
storage_bytes,
|
|
1455
|
+
"estimated" if storage_bytes is not None else "unavailable",
|
|
1456
|
+
),
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def _add_storage_size_fields(metadata, storage_size):
|
|
1461
|
+
metadata["storage_bytes"] = storage_size.bytes
|
|
1462
|
+
metadata["storage_bytes_estimated"] = {
|
|
1463
|
+
"exact": False,
|
|
1464
|
+
"estimated": True,
|
|
1465
|
+
"unavailable": None,
|
|
1466
|
+
}[storage_size.accuracy]
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def _add_observed_shape_fields(cursor, metadata):
|
|
1470
|
+
measure_parameters = metadata.get("measure_parameters") or []
|
|
1471
|
+
sweep_parameters = metadata.get("sweep_parameters") or []
|
|
1472
|
+
observed_setpoints = _run_setpoint_observation(
|
|
1473
|
+
cursor,
|
|
1474
|
+
metadata.get("result_table_name"),
|
|
1475
|
+
_json_dict(metadata.get("run_description")),
|
|
1476
|
+
measure_parameters,
|
|
1477
|
+
sweep_parameters,
|
|
1478
|
+
)
|
|
1479
|
+
setpoint_shape = observed_setpoints["shape"]
|
|
1480
|
+
metadata["setpoint_shape"] = setpoint_shape
|
|
1481
|
+
metadata["point_shape"] = _point_shape_from_setpoint_shape(
|
|
1482
|
+
setpoint_shape,
|
|
1483
|
+
measure_parameters,
|
|
1484
|
+
metadata.get("result_count"),
|
|
1485
|
+
)
|
|
1486
|
+
metadata["setpoint_shape_source"] = "observed" if setpoint_shape else None
|
|
1487
|
+
metadata["setpoint_count"] = observed_setpoints["count"]
|
|
1488
|
+
metadata["setpoint_count_source"] = (
|
|
1489
|
+
"observed" if observed_setpoints["count"] is not None else None
|
|
1490
|
+
)
|
|
1491
|
+
metadata["expected_results"] = None
|
|
1492
|
+
metadata["expected_results_source"] = None
|
|
1493
|
+
_add_completed_observed_result_count(metadata)
|
|
1494
|
+
return observed_setpoints
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _add_completed_observed_result_count(metadata):
|
|
1498
|
+
if (
|
|
1499
|
+
metadata.get("expected_results") is None
|
|
1500
|
+
and _run_is_complete(metadata)
|
|
1501
|
+
and metadata.get("result_count") is not None
|
|
1502
|
+
):
|
|
1503
|
+
metadata["expected_results"] = metadata["result_count"]
|
|
1504
|
+
metadata["expected_results_source"] = "observed"
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def _json_dict(value):
|
|
1508
|
+
if not value:
|
|
1509
|
+
return {}
|
|
1510
|
+
|
|
1511
|
+
try:
|
|
1512
|
+
decoded = json.loads(value)
|
|
1513
|
+
except (TypeError, json.JSONDecodeError, RecursionError):
|
|
1514
|
+
return {}
|
|
1515
|
+
|
|
1516
|
+
return decoded if isinstance(decoded, dict) else {}
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
def _parameter_roles(run_description, parameter_text):
|
|
1520
|
+
measure, sweep, _truncated = _bounded_parameter_roles(
|
|
1521
|
+
run_description,
|
|
1522
|
+
parameter_text,
|
|
1523
|
+
)
|
|
1524
|
+
return measure, sweep
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _bounded_parameter_roles(run_description, parameter_text):
|
|
1528
|
+
dependencies, truncated = _bounded_parameter_dependencies(run_description)
|
|
1529
|
+
measure_parameters: list[str] = []
|
|
1530
|
+
sweep_parameters: list[str] = []
|
|
1531
|
+
measure_seen: set[str] = set()
|
|
1532
|
+
sweep_seen: set[str] = set()
|
|
1533
|
+
all_seen: set[str] = set()
|
|
1534
|
+
|
|
1535
|
+
def add(target, seen, name):
|
|
1536
|
+
nonlocal truncated
|
|
1537
|
+
if name in seen:
|
|
1538
|
+
return
|
|
1539
|
+
if (
|
|
1540
|
+
name not in all_seen
|
|
1541
|
+
and len(all_seen) >= TRUSTED_PRESENTATION_MAX_PARAMETERS
|
|
1542
|
+
):
|
|
1543
|
+
truncated = True
|
|
1544
|
+
return
|
|
1545
|
+
target.append(name)
|
|
1546
|
+
seen.add(name)
|
|
1547
|
+
all_seen.add(name)
|
|
1548
|
+
|
|
1549
|
+
for parameter, dependents in dependencies.items():
|
|
1550
|
+
add(measure_parameters, measure_seen, parameter)
|
|
1551
|
+
for name in dependents:
|
|
1552
|
+
add(sweep_parameters, sweep_seen, name)
|
|
1553
|
+
|
|
1554
|
+
if isinstance(parameter_text, str):
|
|
1555
|
+
raw_parameters = parameter_text.split(",", TRUSTED_PRESENTATION_MAX_PARAMETERS)
|
|
1556
|
+
if len(raw_parameters) > TRUSTED_PRESENTATION_MAX_PARAMETERS:
|
|
1557
|
+
truncated = True
|
|
1558
|
+
raw_parameters.pop()
|
|
1559
|
+
for raw_parameter in raw_parameters:
|
|
1560
|
+
parameter, was_truncated = _bounded_parameter_identifier(
|
|
1561
|
+
raw_parameter.strip()
|
|
1562
|
+
)
|
|
1563
|
+
truncated = truncated or was_truncated
|
|
1564
|
+
if parameter and parameter not in sweep_seen:
|
|
1565
|
+
add(measure_parameters, measure_seen, parameter)
|
|
1566
|
+
elif parameter_text:
|
|
1567
|
+
truncated = True
|
|
1568
|
+
|
|
1569
|
+
return measure_parameters, sweep_parameters, truncated
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def _parameter_dependencies(run_description):
|
|
1573
|
+
dependencies, _truncated = _bounded_parameter_dependencies(run_description)
|
|
1574
|
+
return dependencies
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def _bounded_parameter_dependencies(run_description):
|
|
1578
|
+
truncated = False
|
|
1579
|
+
interdependencies = run_description.get("interdependencies_")
|
|
1580
|
+
if not isinstance(interdependencies, dict):
|
|
1581
|
+
interdependencies = {}
|
|
1582
|
+
dependencies = interdependencies.get("dependencies", {})
|
|
1583
|
+
if not isinstance(dependencies, dict) or not dependencies:
|
|
1584
|
+
return _bounded_legacy_dependencies(run_description)
|
|
1585
|
+
|
|
1586
|
+
output: dict[str, list[str]] = {}
|
|
1587
|
+
items = islice(
|
|
1588
|
+
dependencies.items(),
|
|
1589
|
+
TRUSTED_PRESENTATION_MAX_PARAMETERS + 1,
|
|
1590
|
+
)
|
|
1591
|
+
for index, (raw_parameter, setpoints) in enumerate(items):
|
|
1592
|
+
if index >= TRUSTED_PRESENTATION_MAX_PARAMETERS:
|
|
1593
|
+
truncated = True
|
|
1594
|
+
break
|
|
1595
|
+
parameter, name_truncated = _bounded_parameter_identifier(raw_parameter)
|
|
1596
|
+
truncated = truncated or name_truncated
|
|
1597
|
+
if not parameter or not isinstance(setpoints, (list, tuple)):
|
|
1598
|
+
truncated = truncated or bool(setpoints)
|
|
1599
|
+
continue
|
|
1600
|
+
bounded_setpoints = []
|
|
1601
|
+
seen = set()
|
|
1602
|
+
for dependency_index, raw_setpoint in enumerate(
|
|
1603
|
+
islice(
|
|
1604
|
+
setpoints,
|
|
1605
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES + 1,
|
|
1606
|
+
)
|
|
1607
|
+
):
|
|
1608
|
+
if dependency_index >= TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES:
|
|
1609
|
+
truncated = True
|
|
1610
|
+
break
|
|
1611
|
+
setpoint, dependency_truncated = _bounded_parameter_identifier(raw_setpoint)
|
|
1612
|
+
truncated = truncated or dependency_truncated
|
|
1613
|
+
if setpoint and setpoint not in seen:
|
|
1614
|
+
bounded_setpoints.append(setpoint)
|
|
1615
|
+
seen.add(setpoint)
|
|
1616
|
+
if bounded_setpoints:
|
|
1617
|
+
output[parameter] = bounded_setpoints
|
|
1618
|
+
return output, truncated
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
def _legacy_dependencies(run_description):
|
|
1622
|
+
dependencies, _truncated = _bounded_legacy_dependencies(run_description)
|
|
1623
|
+
return dependencies
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _bounded_legacy_dependencies(run_description):
|
|
1627
|
+
out: dict[str, list[str]] = {}
|
|
1628
|
+
truncated = False
|
|
1629
|
+
interdependencies = run_description.get("interdependencies")
|
|
1630
|
+
paramspecs = (
|
|
1631
|
+
interdependencies.get("paramspecs", [])
|
|
1632
|
+
if isinstance(interdependencies, dict)
|
|
1633
|
+
else []
|
|
1634
|
+
)
|
|
1635
|
+
if not isinstance(paramspecs, (list, tuple)):
|
|
1636
|
+
return out, bool(paramspecs)
|
|
1637
|
+
for index, paramspec in enumerate(
|
|
1638
|
+
islice(paramspecs, TRUSTED_PRESENTATION_MAX_PARAMETERS + 1)
|
|
1639
|
+
):
|
|
1640
|
+
if index >= TRUSTED_PRESENTATION_MAX_PARAMETERS:
|
|
1641
|
+
truncated = True
|
|
1642
|
+
break
|
|
1643
|
+
if not isinstance(paramspec, dict):
|
|
1644
|
+
truncated = True
|
|
1645
|
+
continue
|
|
1646
|
+
depends_on = paramspec.get("depends_on") or []
|
|
1647
|
+
name, name_truncated = _bounded_parameter_identifier(paramspec.get("name"))
|
|
1648
|
+
truncated = truncated or name_truncated
|
|
1649
|
+
if not name or not isinstance(depends_on, (list, tuple)):
|
|
1650
|
+
truncated = truncated or bool(depends_on)
|
|
1651
|
+
continue
|
|
1652
|
+
bounded_dependencies = []
|
|
1653
|
+
seen = set()
|
|
1654
|
+
for dependency_index, raw_dependency in enumerate(
|
|
1655
|
+
islice(
|
|
1656
|
+
depends_on,
|
|
1657
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES + 1,
|
|
1658
|
+
)
|
|
1659
|
+
):
|
|
1660
|
+
if dependency_index >= TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES:
|
|
1661
|
+
truncated = True
|
|
1662
|
+
break
|
|
1663
|
+
dependency, dependency_truncated = _bounded_parameter_identifier(
|
|
1664
|
+
raw_dependency
|
|
1665
|
+
)
|
|
1666
|
+
truncated = truncated or dependency_truncated
|
|
1667
|
+
if dependency and dependency not in seen:
|
|
1668
|
+
bounded_dependencies.append(dependency)
|
|
1669
|
+
seen.add(dependency)
|
|
1670
|
+
if bounded_dependencies:
|
|
1671
|
+
out[name] = bounded_dependencies
|
|
1672
|
+
return out, truncated
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _bounded_parameter_identifier(value):
|
|
1676
|
+
if not isinstance(value, str):
|
|
1677
|
+
return "", value is not None
|
|
1678
|
+
return bounded_presentation_text(
|
|
1679
|
+
value,
|
|
1680
|
+
limit=TRUSTED_PRESENTATION_MAX_PARAMETER_TEXT_BYTES,
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _point_shape(run_description, measure_parameters):
|
|
1685
|
+
shape, _truncated = _bounded_point_shape(run_description, measure_parameters)
|
|
1686
|
+
return shape
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
def _bounded_point_shape(run_description, measure_parameters):
|
|
1690
|
+
shapes = run_description.get("shapes")
|
|
1691
|
+
if not isinstance(shapes, dict):
|
|
1692
|
+
return None, False
|
|
1693
|
+
|
|
1694
|
+
best_shape = None
|
|
1695
|
+
best_size = 0
|
|
1696
|
+
truncated = False
|
|
1697
|
+
for parameter in measure_parameters:
|
|
1698
|
+
shape = shapes.get(parameter)
|
|
1699
|
+
if isinstance(shape, list) and shape:
|
|
1700
|
+
dimensions = list(islice(shape, MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS + 1))
|
|
1701
|
+
if len(dimensions) > MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS:
|
|
1702
|
+
truncated = True
|
|
1703
|
+
continue
|
|
1704
|
+
try:
|
|
1705
|
+
point_shape = [int(size) for size in dimensions]
|
|
1706
|
+
except (TypeError, ValueError):
|
|
1707
|
+
continue
|
|
1708
|
+
|
|
1709
|
+
size = _shape_size(point_shape) or 0
|
|
1710
|
+
if size > best_size:
|
|
1711
|
+
best_shape = point_shape
|
|
1712
|
+
best_size = size
|
|
1713
|
+
|
|
1714
|
+
return best_shape, truncated
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def _expected_results_from_shapes(run_description, measure_parameters):
|
|
1718
|
+
expected, _truncated = _bounded_expected_results_from_shapes(
|
|
1719
|
+
run_description,
|
|
1720
|
+
measure_parameters,
|
|
1721
|
+
)
|
|
1722
|
+
return expected
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
def _bounded_expected_results_from_shapes(run_description, measure_parameters):
|
|
1726
|
+
shapes = run_description.get("shapes")
|
|
1727
|
+
if not isinstance(shapes, dict) or not measure_parameters:
|
|
1728
|
+
return None, False
|
|
1729
|
+
|
|
1730
|
+
sizes = {}
|
|
1731
|
+
truncated = False
|
|
1732
|
+
for index, parameter in enumerate(
|
|
1733
|
+
islice(measure_parameters, TRUSTED_PRESENTATION_MAX_PARAMETERS + 1)
|
|
1734
|
+
):
|
|
1735
|
+
if index >= TRUSTED_PRESENTATION_MAX_PARAMETERS:
|
|
1736
|
+
return None, True
|
|
1737
|
+
shape = shapes.get(parameter)
|
|
1738
|
+
if not isinstance(shape, list) or not shape:
|
|
1739
|
+
return None, truncated
|
|
1740
|
+
|
|
1741
|
+
dimensions = list(islice(shape, MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS + 1))
|
|
1742
|
+
if len(dimensions) > MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS:
|
|
1743
|
+
truncated = True
|
|
1744
|
+
return None, truncated
|
|
1745
|
+
|
|
1746
|
+
size = _shape_size(dimensions)
|
|
1747
|
+
if size is None:
|
|
1748
|
+
return None, truncated
|
|
1749
|
+
sizes[parameter] = size
|
|
1750
|
+
|
|
1751
|
+
interdependencies = run_description.get("interdependencies_")
|
|
1752
|
+
inferences = (
|
|
1753
|
+
interdependencies.get("inferences", {})
|
|
1754
|
+
if isinstance(interdependencies, dict) else {}
|
|
1755
|
+
)
|
|
1756
|
+
if not isinstance(inferences, dict):
|
|
1757
|
+
return None, truncated
|
|
1758
|
+
if len(inferences) > TRUSTED_PRESENTATION_MAX_PARAMETERS:
|
|
1759
|
+
return None, True
|
|
1760
|
+
if not inferences:
|
|
1761
|
+
return (sum(sizes.values()) if sizes else None), truncated
|
|
1762
|
+
|
|
1763
|
+
# Current QCoDeS writes each top-level parameter tree separately. An
|
|
1764
|
+
# inferred channel is stored with its basis, rather than adding another
|
|
1765
|
+
# row. Multiple basis roots still contribute one tree each. Only infer a
|
|
1766
|
+
# planned row count when the complete bounded graph lies among measured
|
|
1767
|
+
# channels and its linked shape sizes agree; setpoint inference and other
|
|
1768
|
+
# ambiguous declarations retain an unknown count.
|
|
1769
|
+
inferred = set()
|
|
1770
|
+
followers: dict[str, set[str]] = {name: set() for name in sizes}
|
|
1771
|
+
remaining_bases = {name: 0 for name in sizes}
|
|
1772
|
+
for index, (name, bases) in enumerate(
|
|
1773
|
+
islice(inferences.items(), TRUSTED_PRESENTATION_MAX_PARAMETERS + 1)
|
|
1774
|
+
):
|
|
1775
|
+
if index >= TRUSTED_PRESENTATION_MAX_PARAMETERS:
|
|
1776
|
+
return None, True
|
|
1777
|
+
if name not in sizes or not isinstance(bases, (list, tuple)) or not bases:
|
|
1778
|
+
return None, truncated
|
|
1779
|
+
inferred.add(name)
|
|
1780
|
+
for basis_index, basis in enumerate(
|
|
1781
|
+
islice(bases, TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES + 1)
|
|
1782
|
+
):
|
|
1783
|
+
if basis_index >= TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES:
|
|
1784
|
+
return None, True
|
|
1785
|
+
if not isinstance(basis, str) or basis not in sizes:
|
|
1786
|
+
return None, truncated
|
|
1787
|
+
if sizes[name] != sizes[basis]:
|
|
1788
|
+
return None, truncated
|
|
1789
|
+
if name not in followers[basis]:
|
|
1790
|
+
followers[basis].add(name)
|
|
1791
|
+
remaining_bases[name] += 1
|
|
1792
|
+
|
|
1793
|
+
ready = [name for name, count in remaining_bases.items() if count == 0]
|
|
1794
|
+
visited = 0
|
|
1795
|
+
while ready:
|
|
1796
|
+
name = ready.pop()
|
|
1797
|
+
visited += 1
|
|
1798
|
+
for follower in followers[name]:
|
|
1799
|
+
remaining_bases[follower] -= 1
|
|
1800
|
+
if remaining_bases[follower] == 0:
|
|
1801
|
+
ready.append(follower)
|
|
1802
|
+
if visited != len(sizes):
|
|
1803
|
+
return None, truncated
|
|
1804
|
+
roots = sizes.keys() - inferred
|
|
1805
|
+
return (sum(sizes[name] for name in roots) if roots else None), truncated
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def _shape_size(shape):
|
|
1809
|
+
if not shape:
|
|
1810
|
+
return None
|
|
1811
|
+
|
|
1812
|
+
try:
|
|
1813
|
+
dimensions = list(islice(iter(shape), MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS + 1))
|
|
1814
|
+
if len(dimensions) > MAX_RUN_DESCRIPTION_SHAPE_DIMENSIONS:
|
|
1815
|
+
return None
|
|
1816
|
+
return math.prod(int(size) for size in dimensions)
|
|
1817
|
+
except (TypeError, ValueError):
|
|
1818
|
+
return None
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
def _database_modified_timestamp(cursor):
|
|
1822
|
+
connection = cursor.connection
|
|
1823
|
+
if hasattr(connection, "_qplot_source_activity_timestamp"):
|
|
1824
|
+
return connection._qplot_source_activity_timestamp
|
|
1825
|
+
try:
|
|
1826
|
+
cursor.execute("PRAGMA database_list")
|
|
1827
|
+
databases = cursor.fetchall()
|
|
1828
|
+
except Exception as error:
|
|
1829
|
+
if _sql_was_interrupted(error):
|
|
1830
|
+
raise
|
|
1831
|
+
return None
|
|
1832
|
+
|
|
1833
|
+
for database in databases:
|
|
1834
|
+
if len(database) < 3 or database[1] != "main" or not database[2]:
|
|
1835
|
+
continue
|
|
1836
|
+
return source_activity_timestamp(database[2])
|
|
1837
|
+
|
|
1838
|
+
return None
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def _existing_run_columns(cursor, column_names):
|
|
1842
|
+
try:
|
|
1843
|
+
cursor.execute("PRAGMA table_info(runs)")
|
|
1844
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
1845
|
+
except Exception as error:
|
|
1846
|
+
if _sql_was_interrupted(error):
|
|
1847
|
+
raise
|
|
1848
|
+
return []
|
|
1849
|
+
|
|
1850
|
+
return [column for column in column_names if column in columns]
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def _is_keyboard_interrupt(value):
|
|
1854
|
+
return bool(value and "KeyboardInterrupt" in str(value))
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
def _run_is_complete(metadata):
|
|
1858
|
+
return bool(metadata.get("completed_timestamp") or metadata.get("is_completed"))
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
def _point_shape_from_result_table(
|
|
1862
|
+
cursor,
|
|
1863
|
+
table_name,
|
|
1864
|
+
sweep_parameters,
|
|
1865
|
+
measure_parameters=None,
|
|
1866
|
+
result_count=None,
|
|
1867
|
+
):
|
|
1868
|
+
shape = _setpoint_shape_from_result_table(cursor, table_name, sweep_parameters)
|
|
1869
|
+
return _point_shape_from_setpoint_shape(shape, measure_parameters, result_count)
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
def _point_shape_from_setpoint_shape(shape, measure_parameters=None, result_count=None):
|
|
1873
|
+
if not shape:
|
|
1874
|
+
return None
|
|
1875
|
+
|
|
1876
|
+
shape_size = _shape_size(shape)
|
|
1877
|
+
measure_count = len(measure_parameters or [])
|
|
1878
|
+
if shape_size and result_count and measure_count > 1:
|
|
1879
|
+
try:
|
|
1880
|
+
result_count = int(result_count)
|
|
1881
|
+
except (TypeError, ValueError):
|
|
1882
|
+
result_count = None
|
|
1883
|
+
|
|
1884
|
+
if (
|
|
1885
|
+
result_count
|
|
1886
|
+
and result_count % shape_size == 0
|
|
1887
|
+
and result_count // shape_size == measure_count
|
|
1888
|
+
):
|
|
1889
|
+
shape = shape + [measure_count]
|
|
1890
|
+
|
|
1891
|
+
return shape
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def _setpoint_shape_from_result_table(cursor, table_name, sweep_parameters):
|
|
1895
|
+
if not table_name or not sweep_parameters:
|
|
1896
|
+
return None
|
|
1897
|
+
|
|
1898
|
+
quoted_table_name = _sqlite_identifier(table_name)
|
|
1899
|
+
columns = _result_table_column_types(cursor, quoted_table_name)
|
|
1900
|
+
if not columns or any(parameter not in columns for parameter in sweep_parameters):
|
|
1901
|
+
return None
|
|
1902
|
+
|
|
1903
|
+
observation = _setpoint_observation(
|
|
1904
|
+
cursor,
|
|
1905
|
+
quoted_table_name,
|
|
1906
|
+
sweep_parameters,
|
|
1907
|
+
columns,
|
|
1908
|
+
)
|
|
1909
|
+
return observation["shape"]
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
def _read_setpoint_count(cursor, table_name, sweep_parameters):
|
|
1913
|
+
if not table_name or not sweep_parameters:
|
|
1914
|
+
return None
|
|
1915
|
+
|
|
1916
|
+
quoted_table_name = _sqlite_identifier(table_name)
|
|
1917
|
+
columns = _result_table_column_types(cursor, quoted_table_name)
|
|
1918
|
+
if not columns or any(parameter not in columns for parameter in sweep_parameters):
|
|
1919
|
+
return None
|
|
1920
|
+
|
|
1921
|
+
if any(columns[parameter] == "array" for parameter in sweep_parameters):
|
|
1922
|
+
return None
|
|
1923
|
+
|
|
1924
|
+
return _distinct_setpoint_count(
|
|
1925
|
+
cursor,
|
|
1926
|
+
quoted_table_name,
|
|
1927
|
+
sweep_parameters,
|
|
1928
|
+
)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _run_setpoint_observation(
|
|
1932
|
+
cursor,
|
|
1933
|
+
table_name,
|
|
1934
|
+
run_description,
|
|
1935
|
+
measure_parameters,
|
|
1936
|
+
sweep_parameters,
|
|
1937
|
+
):
|
|
1938
|
+
"""Return a safe global shape and the largest per-dependent point count."""
|
|
1939
|
+
empty = {"shape": None, "count": None}
|
|
1940
|
+
if not table_name:
|
|
1941
|
+
return empty
|
|
1942
|
+
|
|
1943
|
+
quoted_table_name = _sqlite_identifier(table_name)
|
|
1944
|
+
columns = _result_table_column_types(cursor, quoted_table_name)
|
|
1945
|
+
if not columns:
|
|
1946
|
+
return empty
|
|
1947
|
+
|
|
1948
|
+
# An array cell holds an acquisition record with an unknown number of
|
|
1949
|
+
# samples. DISTINCT counts serialized records, not logical data points.
|
|
1950
|
+
# This also applies when an array dependent has scalar setpoints, or when
|
|
1951
|
+
# scalar and array dependent trees share a run. Keep planned shapes in the
|
|
1952
|
+
# caller, but do not invent a global observed shape/count from these rows.
|
|
1953
|
+
if any(
|
|
1954
|
+
columns.get(parameter) == "array"
|
|
1955
|
+
for parameter in (*measure_parameters, *sweep_parameters)
|
|
1956
|
+
):
|
|
1957
|
+
return empty
|
|
1958
|
+
|
|
1959
|
+
dependencies = _parameter_dependencies(run_description)
|
|
1960
|
+
observations = []
|
|
1961
|
+
if dependencies:
|
|
1962
|
+
for parameter in measure_parameters:
|
|
1963
|
+
setpoints = dependencies.get(parameter)
|
|
1964
|
+
if (
|
|
1965
|
+
not setpoints
|
|
1966
|
+
or parameter not in columns
|
|
1967
|
+
or any(setpoint not in columns for setpoint in setpoints)
|
|
1968
|
+
):
|
|
1969
|
+
continue
|
|
1970
|
+
observation = _setpoint_observation(
|
|
1971
|
+
cursor,
|
|
1972
|
+
quoted_table_name,
|
|
1973
|
+
setpoints,
|
|
1974
|
+
columns,
|
|
1975
|
+
dependent_parameter=parameter,
|
|
1976
|
+
)
|
|
1977
|
+
if observation["count"] is not None:
|
|
1978
|
+
observations.append((tuple(setpoints), observation))
|
|
1979
|
+
elif sweep_parameters and all(
|
|
1980
|
+
parameter in columns for parameter in sweep_parameters
|
|
1981
|
+
):
|
|
1982
|
+
observation = _setpoint_observation(
|
|
1983
|
+
cursor,
|
|
1984
|
+
quoted_table_name,
|
|
1985
|
+
sweep_parameters,
|
|
1986
|
+
columns,
|
|
1987
|
+
)
|
|
1988
|
+
if observation["count"] is not None:
|
|
1989
|
+
observations.append((tuple(sweep_parameters), observation))
|
|
1990
|
+
|
|
1991
|
+
if not observations:
|
|
1992
|
+
return empty
|
|
1993
|
+
|
|
1994
|
+
observed_count = max(observation["count"] for _, observation in observations)
|
|
1995
|
+
dependency_sets = {setpoints for setpoints, _ in observations}
|
|
1996
|
+
observed_shapes = [observation["shape"] for _, observation in observations]
|
|
1997
|
+
shared_shape = (
|
|
1998
|
+
observed_shapes[0]
|
|
1999
|
+
if (
|
|
2000
|
+
len(dependency_sets) == 1
|
|
2001
|
+
and observed_shapes[0] is not None
|
|
2002
|
+
and all(shape == observed_shapes[0] for shape in observed_shapes)
|
|
2003
|
+
)
|
|
2004
|
+
else None
|
|
2005
|
+
)
|
|
2006
|
+
return {"shape": shared_shape, "count": observed_count}
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
def _setpoint_observation(
|
|
2010
|
+
cursor,
|
|
2011
|
+
quoted_table_name,
|
|
2012
|
+
sweep_parameters,
|
|
2013
|
+
columns,
|
|
2014
|
+
dependent_parameter=None,
|
|
2015
|
+
):
|
|
2016
|
+
empty = {"shape": None, "count": None}
|
|
2017
|
+
required_columns = list(sweep_parameters)
|
|
2018
|
+
if dependent_parameter is not None:
|
|
2019
|
+
required_columns.append(dependent_parameter)
|
|
2020
|
+
if not required_columns or any(
|
|
2021
|
+
column not in columns for column in required_columns
|
|
2022
|
+
):
|
|
2023
|
+
return empty
|
|
2024
|
+
|
|
2025
|
+
if any(columns[column] == "array" for column in required_columns):
|
|
2026
|
+
return empty
|
|
2027
|
+
|
|
2028
|
+
observed_count = _distinct_setpoint_count(
|
|
2029
|
+
cursor,
|
|
2030
|
+
quoted_table_name,
|
|
2031
|
+
sweep_parameters,
|
|
2032
|
+
dependent_parameter=dependent_parameter,
|
|
2033
|
+
)
|
|
2034
|
+
if not observed_count:
|
|
2035
|
+
return empty
|
|
2036
|
+
|
|
2037
|
+
conditions = _setpoint_not_null_conditions(
|
|
2038
|
+
sweep_parameters,
|
|
2039
|
+
dependent_parameter=dependent_parameter,
|
|
2040
|
+
)
|
|
2041
|
+
distinct_counts = ", ".join(
|
|
2042
|
+
f"COUNT(DISTINCT {_sqlite_identifier(parameter)})"
|
|
2043
|
+
for parameter in sweep_parameters
|
|
2044
|
+
)
|
|
2045
|
+
try:
|
|
2046
|
+
cursor.execute(f"""
|
|
2047
|
+
SELECT {distinct_counts}
|
|
2048
|
+
FROM {quoted_table_name}
|
|
2049
|
+
WHERE {conditions}
|
|
2050
|
+
""")
|
|
2051
|
+
shape = [int(count) for count in cursor.fetchone()]
|
|
2052
|
+
except Exception as error:
|
|
2053
|
+
if _sql_was_interrupted(error):
|
|
2054
|
+
raise
|
|
2055
|
+
return {"shape": None, "count": observed_count}
|
|
2056
|
+
|
|
2057
|
+
if any(count <= 0 for count in shape) or _shape_size(shape) != observed_count:
|
|
2058
|
+
return {"shape": None, "count": observed_count}
|
|
2059
|
+
return {"shape": shape, "count": observed_count}
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
def _distinct_setpoint_count(
|
|
2063
|
+
cursor,
|
|
2064
|
+
quoted_table_name,
|
|
2065
|
+
sweep_parameters,
|
|
2066
|
+
dependent_parameter=None,
|
|
2067
|
+
):
|
|
2068
|
+
quoted_columns = ", ".join(
|
|
2069
|
+
_sqlite_identifier(parameter) for parameter in sweep_parameters
|
|
2070
|
+
)
|
|
2071
|
+
conditions = _setpoint_not_null_conditions(
|
|
2072
|
+
sweep_parameters,
|
|
2073
|
+
dependent_parameter=dependent_parameter,
|
|
2074
|
+
)
|
|
2075
|
+
try:
|
|
2076
|
+
cursor.execute(f"""
|
|
2077
|
+
SELECT COUNT(*)
|
|
2078
|
+
FROM (
|
|
2079
|
+
SELECT DISTINCT {quoted_columns}
|
|
2080
|
+
FROM {quoted_table_name}
|
|
2081
|
+
WHERE {conditions}
|
|
2082
|
+
) AS qplot_distinct_setpoints
|
|
2083
|
+
""")
|
|
2084
|
+
count = int(cursor.fetchone()[0])
|
|
2085
|
+
except Exception as error:
|
|
2086
|
+
if _sql_was_interrupted(error):
|
|
2087
|
+
raise
|
|
2088
|
+
return None
|
|
2089
|
+
return count if count > 0 else None
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
def _setpoint_not_null_conditions(sweep_parameters, dependent_parameter=None):
|
|
2093
|
+
parameters = list(sweep_parameters)
|
|
2094
|
+
if dependent_parameter is not None:
|
|
2095
|
+
parameters.append(dependent_parameter)
|
|
2096
|
+
return " AND ".join(
|
|
2097
|
+
f"{_sqlite_identifier(parameter)} IS NOT NULL" for parameter in parameters
|
|
2098
|
+
)
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
def _result_table_column_types(cursor, quoted_table_name):
|
|
2102
|
+
"""Read current QCoDeS storage types without fetching array payloads."""
|
|
2103
|
+
try:
|
|
2104
|
+
cursor.execute(f"PRAGMA table_info({quoted_table_name})")
|
|
2105
|
+
return {row[1]: str(row[2]).lower() for row in cursor.fetchall()}
|
|
2106
|
+
except Exception as error:
|
|
2107
|
+
if _sql_was_interrupted(error):
|
|
2108
|
+
raise
|
|
2109
|
+
return {}
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
def _result_count(cursor, table_name):
|
|
2113
|
+
if not table_name:
|
|
2114
|
+
return None
|
|
2115
|
+
|
|
2116
|
+
try:
|
|
2117
|
+
cursor.execute(f"SELECT COUNT(*) FROM {_sqlite_identifier(table_name)}")
|
|
2118
|
+
return cursor.fetchone()[0]
|
|
2119
|
+
except Exception as error:
|
|
2120
|
+
if _sql_was_interrupted(error):
|
|
2121
|
+
raise
|
|
2122
|
+
return None
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def _sqlite_identifier(name):
|
|
2126
|
+
return f'"{str(name).replace(chr(34), chr(34) * 2)}"'
|
|
2127
|
+
|
|
2128
|
+
|
|
2129
|
+
def _table_storage_bytes(cursor, table_name, result_count=None):
|
|
2130
|
+
if not table_name:
|
|
2131
|
+
return _StorageSize(None, "unavailable")
|
|
2132
|
+
|
|
2133
|
+
try:
|
|
2134
|
+
cursor.execute("SELECT SUM(pgsize) FROM dbstat WHERE name = ?", (table_name,))
|
|
2135
|
+
row = cursor.fetchone()
|
|
2136
|
+
value = row[0] if row else None
|
|
2137
|
+
if value is not None:
|
|
2138
|
+
return _StorageSize(int(value), "exact")
|
|
2139
|
+
except Exception as err:
|
|
2140
|
+
if _sql_was_interrupted(err):
|
|
2141
|
+
raise
|
|
2142
|
+
|
|
2143
|
+
estimated_bytes = _estimated_table_storage_bytes(
|
|
2144
|
+
cursor,
|
|
2145
|
+
table_name,
|
|
2146
|
+
result_count=result_count,
|
|
2147
|
+
)
|
|
2148
|
+
if estimated_bytes is None:
|
|
2149
|
+
return _StorageSize(None, "unavailable")
|
|
2150
|
+
return _StorageSize(estimated_bytes, "estimated")
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
def _sql_was_interrupted(error):
|
|
2154
|
+
return "interrupted" in str(error).lower()
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
def _run_storage_tables(cursor, run_ids):
|
|
2158
|
+
if not run_ids:
|
|
2159
|
+
return {}
|
|
2160
|
+
|
|
2161
|
+
rows = {}
|
|
2162
|
+
chunk_size = 500
|
|
2163
|
+
for offset in range(0, len(run_ids), chunk_size):
|
|
2164
|
+
batch = run_ids[offset : offset + chunk_size]
|
|
2165
|
+
placeholders = ", ".join("?" for _ in batch)
|
|
2166
|
+
try:
|
|
2167
|
+
cursor.execute(
|
|
2168
|
+
f"""
|
|
2169
|
+
SELECT run_id, guid, result_table_name
|
|
2170
|
+
FROM runs
|
|
2171
|
+
WHERE run_id IN ({placeholders})
|
|
2172
|
+
""",
|
|
2173
|
+
tuple(batch),
|
|
2174
|
+
)
|
|
2175
|
+
except Exception as error:
|
|
2176
|
+
if _sql_was_interrupted(error):
|
|
2177
|
+
raise
|
|
2178
|
+
continue
|
|
2179
|
+
|
|
2180
|
+
for run_id, guid, table_name in cursor.fetchall():
|
|
2181
|
+
rows[run_id] = {
|
|
2182
|
+
"guid": guid,
|
|
2183
|
+
"result_table_name": table_name,
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
return rows
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
def _table_storage_bytes_by_name(cursor, table_names):
|
|
2190
|
+
table_names = sorted({name for name in table_names if name})
|
|
2191
|
+
if not table_names:
|
|
2192
|
+
return {}
|
|
2193
|
+
|
|
2194
|
+
placeholders = ", ".join("?" for _ in table_names)
|
|
2195
|
+
try:
|
|
2196
|
+
cursor.execute(
|
|
2197
|
+
f"""
|
|
2198
|
+
SELECT name, SUM(pgsize)
|
|
2199
|
+
FROM dbstat
|
|
2200
|
+
WHERE name IN ({placeholders})
|
|
2201
|
+
GROUP BY name
|
|
2202
|
+
""",
|
|
2203
|
+
tuple(table_names),
|
|
2204
|
+
)
|
|
2205
|
+
except Exception as err:
|
|
2206
|
+
if _sql_was_interrupted(err):
|
|
2207
|
+
raise
|
|
2208
|
+
return {}
|
|
2209
|
+
|
|
2210
|
+
sizes = {}
|
|
2211
|
+
for name, value in cursor.fetchall():
|
|
2212
|
+
if name not in table_names or value is None:
|
|
2213
|
+
continue
|
|
2214
|
+
try:
|
|
2215
|
+
sizes[name] = int(value)
|
|
2216
|
+
except (TypeError, ValueError):
|
|
2217
|
+
pass
|
|
2218
|
+
|
|
2219
|
+
return sizes
|
|
2220
|
+
|
|
2221
|
+
|
|
2222
|
+
def _estimated_table_storage_bytes(cursor, table_name, result_count=None):
|
|
2223
|
+
if not table_name:
|
|
2224
|
+
return None
|
|
2225
|
+
|
|
2226
|
+
quoted_table_name = _sqlite_identifier(table_name)
|
|
2227
|
+
try:
|
|
2228
|
+
cursor.execute(f"PRAGMA table_info({quoted_table_name})")
|
|
2229
|
+
columns = cursor.fetchall()
|
|
2230
|
+
except Exception as error:
|
|
2231
|
+
if _sql_was_interrupted(error):
|
|
2232
|
+
raise
|
|
2233
|
+
return None
|
|
2234
|
+
|
|
2235
|
+
if not columns:
|
|
2236
|
+
return None
|
|
2237
|
+
|
|
2238
|
+
try:
|
|
2239
|
+
row_count = int(result_count)
|
|
2240
|
+
except (TypeError, ValueError):
|
|
2241
|
+
row_count = None
|
|
2242
|
+
|
|
2243
|
+
if row_count is None:
|
|
2244
|
+
row_count = _result_count(cursor, table_name)
|
|
2245
|
+
|
|
2246
|
+
if row_count is None:
|
|
2247
|
+
return None
|
|
2248
|
+
|
|
2249
|
+
return max(0, row_count) * _estimated_table_row_bytes(columns)
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
def _estimated_table_row_bytes(columns):
|
|
2253
|
+
row_bytes = len(columns) + 2
|
|
2254
|
+
for column in columns:
|
|
2255
|
+
column_type = str(column[2] or "").upper()
|
|
2256
|
+
if any(
|
|
2257
|
+
type_name in column_type
|
|
2258
|
+
for type_name in ("INT", "REAL", "FLOA", "DOUB", "NUM")
|
|
2259
|
+
):
|
|
2260
|
+
row_bytes += 8
|
|
2261
|
+
else:
|
|
2262
|
+
row_bytes += 32
|
|
2263
|
+
return row_bytes
|
|
2264
|
+
|
|
2265
|
+
|
|
2266
|
+
def get_run_status(
|
|
2267
|
+
guid,
|
|
2268
|
+
database_path=None,
|
|
2269
|
+
include_storage_bytes=True,
|
|
2270
|
+
cancelled_callback=None,
|
|
2271
|
+
connection_callback=None,
|
|
2272
|
+
expected_database_identity=None,
|
|
2273
|
+
deadline=None,
|
|
2274
|
+
):
|
|
2275
|
+
"""
|
|
2276
|
+
Returns completion and result count information for one run.
|
|
2277
|
+
|
|
2278
|
+
"""
|
|
2279
|
+
conn = _read_only_connection(
|
|
2280
|
+
database_path or get_DB_location(),
|
|
2281
|
+
expected_database_identity,
|
|
2282
|
+
cancelled_callback,
|
|
2283
|
+
deadline,
|
|
2284
|
+
)
|
|
2285
|
+
try:
|
|
2286
|
+
_notify_connection(connection_callback, conn)
|
|
2287
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
2288
|
+
cursor = conn.cursor()
|
|
2289
|
+
optional_columns = _existing_run_columns(cursor, ["measurement_exception"])
|
|
2290
|
+
optional_select = "".join(
|
|
2291
|
+
f", {_sqlite_identifier(column)}" for column in optional_columns
|
|
2292
|
+
)
|
|
2293
|
+
|
|
2294
|
+
cursor.execute(
|
|
2295
|
+
f"""
|
|
2296
|
+
SELECT
|
|
2297
|
+
run_timestamp,
|
|
2298
|
+
completed_timestamp,
|
|
2299
|
+
is_completed,
|
|
2300
|
+
result_table_name,
|
|
2301
|
+
run_description,
|
|
2302
|
+
parameters
|
|
2303
|
+
{optional_select}
|
|
2304
|
+
FROM runs
|
|
2305
|
+
WHERE guid=?
|
|
2306
|
+
LIMIT 1
|
|
2307
|
+
""",
|
|
2308
|
+
(guid,),
|
|
2309
|
+
)
|
|
2310
|
+
value = cursor.fetchone()
|
|
2311
|
+
if value is None:
|
|
2312
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
2313
|
+
return {}
|
|
2314
|
+
|
|
2315
|
+
status = {
|
|
2316
|
+
"run_timestamp": value[0],
|
|
2317
|
+
"completed_timestamp": value[1],
|
|
2318
|
+
"is_completed": value[2],
|
|
2319
|
+
"result_count": _result_count(cursor, value[3]),
|
|
2320
|
+
"database_modified_timestamp": _database_modified_timestamp(cursor),
|
|
2321
|
+
}
|
|
2322
|
+
if include_storage_bytes:
|
|
2323
|
+
_add_storage_size_fields(
|
|
2324
|
+
status,
|
|
2325
|
+
_table_storage_bytes(
|
|
2326
|
+
cursor,
|
|
2327
|
+
value[3],
|
|
2328
|
+
result_count=status["result_count"],
|
|
2329
|
+
),
|
|
2330
|
+
)
|
|
2331
|
+
for index, column in enumerate(optional_columns, start=6):
|
|
2332
|
+
status[column] = value[index]
|
|
2333
|
+
|
|
2334
|
+
shape_metadata = {
|
|
2335
|
+
"completed_timestamp": value[1],
|
|
2336
|
+
"is_completed": value[2],
|
|
2337
|
+
"result_table_name": value[3],
|
|
2338
|
+
"run_description": value[4],
|
|
2339
|
+
"parameters": value[5],
|
|
2340
|
+
"result_count": status["result_count"],
|
|
2341
|
+
}
|
|
2342
|
+
_add_run_basic_fields(shape_metadata)
|
|
2343
|
+
observed_setpoints = None
|
|
2344
|
+
if not shape_metadata["point_shape"]:
|
|
2345
|
+
observed_setpoints = _add_observed_shape_fields(cursor, shape_metadata)
|
|
2346
|
+
_add_completed_observed_result_count(shape_metadata)
|
|
2347
|
+
for field in (
|
|
2348
|
+
"point_shape",
|
|
2349
|
+
"setpoint_shape",
|
|
2350
|
+
"setpoint_shape_source",
|
|
2351
|
+
"setpoint_count",
|
|
2352
|
+
"setpoint_count_source",
|
|
2353
|
+
"expected_results",
|
|
2354
|
+
"expected_results_source",
|
|
2355
|
+
):
|
|
2356
|
+
status[field] = shape_metadata.get(field)
|
|
2357
|
+
|
|
2358
|
+
if not bool(value[2]) or _is_keyboard_interrupt(
|
|
2359
|
+
status.get("measurement_exception")
|
|
2360
|
+
):
|
|
2361
|
+
if observed_setpoints is None:
|
|
2362
|
+
observed_setpoints = _run_setpoint_observation(
|
|
2363
|
+
cursor,
|
|
2364
|
+
value[3],
|
|
2365
|
+
_json_dict(value[4]),
|
|
2366
|
+
shape_metadata["measure_parameters"],
|
|
2367
|
+
shape_metadata["sweep_parameters"],
|
|
2368
|
+
)
|
|
2369
|
+
status["read_setpoint_count"] = observed_setpoints["count"]
|
|
2370
|
+
|
|
2371
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
2372
|
+
return status
|
|
2373
|
+
except sqlite3.OperationalError as error:
|
|
2374
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
2375
|
+
raise
|
|
2376
|
+
finally:
|
|
2377
|
+
try:
|
|
2378
|
+
_notify_connection(connection_callback, None)
|
|
2379
|
+
finally:
|
|
2380
|
+
conn.close()
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def has_finished(
|
|
2384
|
+
guid,
|
|
2385
|
+
expected_database_identity=None,
|
|
2386
|
+
cancelled_callback=None,
|
|
2387
|
+
connection_callback=None,
|
|
2388
|
+
deadline=None,
|
|
2389
|
+
) -> float | None:
|
|
2390
|
+
"""
|
|
2391
|
+
Checks if specific run (by guid) has finished running.
|
|
2392
|
+
If the run with guid has finished, returns the completed time.
|
|
2393
|
+
Otherwise returns a NULL value which python interprets as None.
|
|
2394
|
+
|
|
2395
|
+
Parameters
|
|
2396
|
+
----------
|
|
2397
|
+
guid : str
|
|
2398
|
+
The unique id of the run to look up.
|
|
2399
|
+
|
|
2400
|
+
Returns
|
|
2401
|
+
-------
|
|
2402
|
+
completed_timestamp : float | None
|
|
2403
|
+
The completion timestamp as a Unix-time float. Returns None when the
|
|
2404
|
+
run is present but unfinished, or when no matching run exists.
|
|
2405
|
+
|
|
2406
|
+
"""
|
|
2407
|
+
conn = _read_only_connection(
|
|
2408
|
+
get_DB_location(),
|
|
2409
|
+
expected_database_identity,
|
|
2410
|
+
cancelled_callback,
|
|
2411
|
+
deadline,
|
|
2412
|
+
)
|
|
2413
|
+
|
|
2414
|
+
try:
|
|
2415
|
+
_notify_connection(connection_callback, conn)
|
|
2416
|
+
_install_cancel_progress_handler(conn, cancelled_callback, deadline)
|
|
2417
|
+
cursor = conn.cursor()
|
|
2418
|
+
|
|
2419
|
+
cursor.execute(
|
|
2420
|
+
"""
|
|
2421
|
+
SELECT
|
|
2422
|
+
completed_timestamp
|
|
2423
|
+
FROM runs
|
|
2424
|
+
WHERE guid=?
|
|
2425
|
+
LIMIT 1
|
|
2426
|
+
""",
|
|
2427
|
+
(guid,),
|
|
2428
|
+
)
|
|
2429
|
+
row = cursor.fetchone()
|
|
2430
|
+
if row is None or row[0] is None:
|
|
2431
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
2432
|
+
return None
|
|
2433
|
+
_raise_if_read_aborted(cancelled_callback, deadline)
|
|
2434
|
+
return float(row[0])
|
|
2435
|
+
except sqlite3.OperationalError as error:
|
|
2436
|
+
_translate_interrupted_read(error, cancelled_callback, deadline)
|
|
2437
|
+
raise
|
|
2438
|
+
finally:
|
|
2439
|
+
try:
|
|
2440
|
+
_notify_connection(connection_callback, None)
|
|
2441
|
+
finally:
|
|
2442
|
+
conn.close()
|