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,942 @@
|
|
|
1
|
+
from time import perf_counter
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from PyQt6 import QtCore
|
|
5
|
+
from PyQt6 import QtWidgets as qtw
|
|
6
|
+
|
|
7
|
+
from qplot.datahandling.qcodes_cache import (
|
|
8
|
+
cache_has_no_written_data,
|
|
9
|
+
cache_is_live,
|
|
10
|
+
cache_parameter_is_synchronized,
|
|
11
|
+
restore_cache_parameter_publication_state,
|
|
12
|
+
set_cache_dataset_completed,
|
|
13
|
+
set_cache_parameter_synchronized,
|
|
14
|
+
snapshot_cache_parameter_publication_state,
|
|
15
|
+
update_cache_parameter_data,
|
|
16
|
+
)
|
|
17
|
+
from qplot.datahandling.readonly import quarantine_wal_for_replaced_database
|
|
18
|
+
from qplot.diagnostics import log_exception
|
|
19
|
+
from qplot.tools import loader
|
|
20
|
+
from qplot.tools.operation_registry import OperationValidationError
|
|
21
|
+
|
|
22
|
+
from ._dataset_handle import DatasetKey, dataset_key_matches_current_source
|
|
23
|
+
from ._refresh_interval import refresh_interval_value
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
class _PlotRefreshBase(qtw.QMainWindow):
|
|
27
|
+
_last_error_text: str | None
|
|
28
|
+
_live: bool
|
|
29
|
+
axis_data: dict[str, Any]
|
|
30
|
+
axis_param: dict[str, Any]
|
|
31
|
+
config: Any
|
|
32
|
+
dataGrid: Any
|
|
33
|
+
display_param: Any
|
|
34
|
+
ds: Any
|
|
35
|
+
end_wait: Any
|
|
36
|
+
last_ds_len: int
|
|
37
|
+
lines: dict[Any, Any]
|
|
38
|
+
monitor: QtCore.QTimer
|
|
39
|
+
oper_widget: Any
|
|
40
|
+
param: Any
|
|
41
|
+
param_dict: dict[str, Any]
|
|
42
|
+
spinBox: qtw.QDoubleSpinBox
|
|
43
|
+
threadPool: QtCore.QThreadPool
|
|
44
|
+
worker: Any
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def axis_options(self) -> dict[str, str]: ...
|
|
48
|
+
|
|
49
|
+
def _set_param_axis_labels(self) -> None: ...
|
|
50
|
+
def hide_plot_state(self) -> None: ...
|
|
51
|
+
def initFrame(self) -> None: ...
|
|
52
|
+
def monitorIntervalChanged(self, interval: float) -> None: ...
|
|
53
|
+
def show_error(
|
|
54
|
+
self,
|
|
55
|
+
title: str,
|
|
56
|
+
message: str,
|
|
57
|
+
details: str | None = None,
|
|
58
|
+
) -> None: ...
|
|
59
|
+
def show_plot_state(
|
|
60
|
+
self,
|
|
61
|
+
title: object,
|
|
62
|
+
detail: object | None = None,
|
|
63
|
+
kind: str = "info",
|
|
64
|
+
) -> None: ...
|
|
65
|
+
def show_status(self, message: str, timeout: int = 5000) -> None: ...
|
|
66
|
+
else:
|
|
67
|
+
class _PlotRefreshBase:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def plot_refresh_required(source: Any) -> bool:
|
|
72
|
+
"""Return whether a source plot still needs live/final refresh work."""
|
|
73
|
+
|
|
74
|
+
predicate = getattr(source, "_refresh_monitor_required", None)
|
|
75
|
+
if callable(predicate):
|
|
76
|
+
return bool(predicate())
|
|
77
|
+
|
|
78
|
+
dataset = getattr(source, "ds", None)
|
|
79
|
+
return bool(getattr(dataset, "running", False))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PlotRefreshMixin(_PlotRefreshBase):
|
|
83
|
+
"""
|
|
84
|
+
Worker-backed plot refresh orchestration shared by plot windows.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def _can_process_refresh(self) -> bool:
|
|
89
|
+
"""Return whether this window still has an active dataset owner."""
|
|
90
|
+
|
|
91
|
+
if not self.__dict__.get("_closed", False):
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
if self.__dict__.get("_merged_trace_users", 0) <= 0:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
dataset_holder = self.__dict__.get("_dataset_holder")
|
|
98
|
+
dataset_key = self.__dict__.get("_dataset_key")
|
|
99
|
+
if not isinstance(dataset_holder, dict) or dataset_key is None:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
handle = dataset_holder.get(dataset_key)
|
|
103
|
+
return handle is not None and getattr(handle, "users", 0) > 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _source_database_matches_key(self) -> bool:
|
|
107
|
+
"""Return whether the retained key still exactly names its source."""
|
|
108
|
+
dataset_key = self.__dict__.get("_dataset_key")
|
|
109
|
+
if not isinstance(dataset_key, DatasetKey):
|
|
110
|
+
return True
|
|
111
|
+
database_path = getattr(dataset_key, "database_path", None)
|
|
112
|
+
if not database_path:
|
|
113
|
+
return True
|
|
114
|
+
return dataset_key_matches_current_source(dataset_key)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _source_database_is_current(self) -> bool:
|
|
118
|
+
"""Stop before a replaced main, WAL, or SHM can back this plot."""
|
|
119
|
+
|
|
120
|
+
dataset_key = self.__dict__.get("_dataset_key")
|
|
121
|
+
database_path = getattr(dataset_key, "database_path", None)
|
|
122
|
+
if self._source_database_matches_key():
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
quarantine_wal_for_replaced_database(database_path)
|
|
126
|
+
worker = self.__dict__.get("worker")
|
|
127
|
+
if worker is not None and getattr(worker, "running", False):
|
|
128
|
+
cancel = getattr(worker, "cancel", None)
|
|
129
|
+
if callable(cancel):
|
|
130
|
+
cancel()
|
|
131
|
+
monitor = self.__dict__.get("monitor")
|
|
132
|
+
if monitor is not None:
|
|
133
|
+
monitor.stop()
|
|
134
|
+
self.show_status(
|
|
135
|
+
"Database or SQLite sidecar was replaced; reloading source data.",
|
|
136
|
+
5000,
|
|
137
|
+
)
|
|
138
|
+
self.show_plot_state(
|
|
139
|
+
"Database replaced",
|
|
140
|
+
"qPlot is reloading the replacement before this plot can refresh.",
|
|
141
|
+
kind="info",
|
|
142
|
+
)
|
|
143
|
+
replacement_signal = getattr(self, "database_replaced", None)
|
|
144
|
+
emit = getattr(replacement_signal, "emit", None)
|
|
145
|
+
if callable(emit):
|
|
146
|
+
emit(str(database_path))
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _begin_refresh_publication(self, worker: Any) -> None:
|
|
151
|
+
"""Capture the cache and plot fields a callback may mutate."""
|
|
152
|
+
|
|
153
|
+
publication_generation = (
|
|
154
|
+
self.__dict__.get("_qplot_publication_generation", 0) + 1
|
|
155
|
+
)
|
|
156
|
+
self._qplot_publication_generation = publication_generation
|
|
157
|
+
cache = getattr(self.ds, "cache", None)
|
|
158
|
+
parameter_name = self.param.name
|
|
159
|
+
fields = {}
|
|
160
|
+
for name in (
|
|
161
|
+
"_live",
|
|
162
|
+
"_qplot_display_synchronized",
|
|
163
|
+
"_qplot_display_uses_direct_sql",
|
|
164
|
+
"_heatmap_full_resolution",
|
|
165
|
+
"axis_data",
|
|
166
|
+
"axis_param",
|
|
167
|
+
"dataGrid",
|
|
168
|
+
"display_param",
|
|
169
|
+
"last_ds_len",
|
|
170
|
+
):
|
|
171
|
+
fields[name] = (name in self.__dict__, self.__dict__.get(name))
|
|
172
|
+
try:
|
|
173
|
+
prior_cache = (
|
|
174
|
+
snapshot_cache_parameter_publication_state(
|
|
175
|
+
cache,
|
|
176
|
+
parameter_name,
|
|
177
|
+
)
|
|
178
|
+
if cache is not None
|
|
179
|
+
else None
|
|
180
|
+
)
|
|
181
|
+
except AttributeError:
|
|
182
|
+
# Narrow legacy/test cache doubles may not expose QCoDeS' private
|
|
183
|
+
# status mappings. They cannot receive the shared-cache mutation
|
|
184
|
+
# guarded here, but their plot fields still need rollback state.
|
|
185
|
+
prior_cache = None
|
|
186
|
+
worker._qplot_publication_snapshot = {
|
|
187
|
+
"cache": cache,
|
|
188
|
+
"parameter_name": parameter_name,
|
|
189
|
+
"prior_cache": prior_cache,
|
|
190
|
+
"committed_cache": None,
|
|
191
|
+
"fields": fields,
|
|
192
|
+
"generation": publication_generation,
|
|
193
|
+
}
|
|
194
|
+
worker._qplot_source_rejected = False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _record_refresh_cache_publication(worker: Any) -> None:
|
|
199
|
+
snapshot = getattr(worker, "_qplot_publication_snapshot", None)
|
|
200
|
+
if (
|
|
201
|
+
not isinstance(snapshot, dict)
|
|
202
|
+
or snapshot.get("prior_cache") is None
|
|
203
|
+
):
|
|
204
|
+
return
|
|
205
|
+
snapshot["committed_cache"] = snapshot_cache_parameter_publication_state(
|
|
206
|
+
snapshot["cache"],
|
|
207
|
+
snapshot["parameter_name"],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _rollback_refresh_publication(self, worker: Any) -> None:
|
|
212
|
+
"""Remove stale cache/axis state before replacement recovery runs."""
|
|
213
|
+
|
|
214
|
+
snapshot = getattr(worker, "_qplot_publication_snapshot", None)
|
|
215
|
+
if not isinstance(snapshot, dict):
|
|
216
|
+
return
|
|
217
|
+
owns_publication = bool(
|
|
218
|
+
self.__dict__.get("_qplot_publication_generation")
|
|
219
|
+
== snapshot.get("generation")
|
|
220
|
+
and worker is self.__dict__.get("worker")
|
|
221
|
+
)
|
|
222
|
+
if not owns_publication:
|
|
223
|
+
worker._qplot_display_commit_ready = False
|
|
224
|
+
worker._qplot_source_rejected = True
|
|
225
|
+
worker._qplot_publication_snapshot = None
|
|
226
|
+
return
|
|
227
|
+
committed_cache = snapshot.get("committed_cache")
|
|
228
|
+
if committed_cache is not None:
|
|
229
|
+
restore_cache_parameter_publication_state(
|
|
230
|
+
snapshot["cache"],
|
|
231
|
+
snapshot["parameter_name"],
|
|
232
|
+
snapshot["prior_cache"],
|
|
233
|
+
committed_cache,
|
|
234
|
+
)
|
|
235
|
+
for name, (present, value) in snapshot["fields"].items():
|
|
236
|
+
if present:
|
|
237
|
+
self.__dict__[name] = value
|
|
238
|
+
else:
|
|
239
|
+
self.__dict__.pop(name, None)
|
|
240
|
+
worker._qplot_display_commit_ready = False
|
|
241
|
+
worker._qplot_source_rejected = True
|
|
242
|
+
worker._qplot_publication_snapshot = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _refresh_publication_source_is_current(
|
|
246
|
+
self,
|
|
247
|
+
worker: Any,
|
|
248
|
+
*,
|
|
249
|
+
clear_display=None,
|
|
250
|
+
) -> bool:
|
|
251
|
+
"""Guard one cache/display publication boundary and fail closed."""
|
|
252
|
+
|
|
253
|
+
snapshot = getattr(worker, "_qplot_publication_snapshot", None)
|
|
254
|
+
if isinstance(snapshot, dict):
|
|
255
|
+
owns_publication = bool(
|
|
256
|
+
self.__dict__.get("_qplot_publication_generation")
|
|
257
|
+
== snapshot.get("generation")
|
|
258
|
+
and worker is self.__dict__.get("worker")
|
|
259
|
+
)
|
|
260
|
+
if not owns_publication:
|
|
261
|
+
# A nested same-source callback already owns the plot. The
|
|
262
|
+
# older callback must stop harmlessly: rollback or renderer
|
|
263
|
+
# clearing here would corrupt the newer accepted publication.
|
|
264
|
+
worker._qplot_display_commit_ready = False
|
|
265
|
+
worker._qplot_publication_snapshot = None
|
|
266
|
+
return False
|
|
267
|
+
elif worker is not self.__dict__.get("worker"):
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
if self._source_database_matches_key():
|
|
271
|
+
return True
|
|
272
|
+
self._rollback_refresh_publication(worker)
|
|
273
|
+
if callable(clear_display):
|
|
274
|
+
clear_display()
|
|
275
|
+
self._source_database_is_current()
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _commit_refresh_publication(
|
|
280
|
+
self, worker: Any, *, preview_ready: bool = False
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Release rollback state after the concrete display committed."""
|
|
283
|
+
|
|
284
|
+
worker._qplot_publication_snapshot = None
|
|
285
|
+
overlay = self.__dict__.get("plot_state_overlay")
|
|
286
|
+
if overlay is not None:
|
|
287
|
+
overlay.finish(worker)
|
|
288
|
+
publish_preview = self.__dict__.get("_plot_preview_sink")
|
|
289
|
+
if (
|
|
290
|
+
preview_ready
|
|
291
|
+
and callable(publish_preview)
|
|
292
|
+
and not getattr(worker, "operations", None)
|
|
293
|
+
and getattr(worker, "heatmap_axis_ranges", None) is None
|
|
294
|
+
and not getattr(worker, "is_cancelled", lambda: False)()
|
|
295
|
+
and self._source_database_matches_key()
|
|
296
|
+
):
|
|
297
|
+
try:
|
|
298
|
+
publish_preview(self, worker)
|
|
299
|
+
except Exception as error:
|
|
300
|
+
# A supplementary image must never turn a successful plot
|
|
301
|
+
# publication into a plot load failure.
|
|
302
|
+
log_exception("Plot preview reuse failed", error, __name__)
|
|
303
|
+
|
|
304
|
+
def _refresh_monitor_required(self, dataset: Any | None = None) -> bool:
|
|
305
|
+
"""Keep polling until this plot has committed its terminal display."""
|
|
306
|
+
|
|
307
|
+
if dataset is None:
|
|
308
|
+
dataset = self.ds
|
|
309
|
+
if getattr(dataset, "running", False):
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
state = self.__dict__
|
|
313
|
+
if not state.get("_qplot_display_synchronized", True):
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
# A direct-SQL heatmap deliberately bypasses the QCoDeS cache. Its
|
|
317
|
+
# successful display commit is therefore the terminal per-plot state.
|
|
318
|
+
if state.get("_qplot_display_uses_direct_sql", False):
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
cache = getattr(dataset, "cache", None)
|
|
322
|
+
param = state.get("param")
|
|
323
|
+
if cache is None or param is None:
|
|
324
|
+
return False
|
|
325
|
+
return not cache_parameter_is_synchronized(cache, param.name)
|
|
326
|
+
|
|
327
|
+
def _sync_dataset_completion(self, dataset: Any) -> None:
|
|
328
|
+
"""Schedule this plot's outstanding final worker read."""
|
|
329
|
+
|
|
330
|
+
if not self._refresh_monitor_required(dataset):
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
worker = getattr(self, "worker", None)
|
|
334
|
+
if worker is not None and getattr(worker, "running", False):
|
|
335
|
+
self._queue_pending_refresh()
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
self.load_data()
|
|
339
|
+
|
|
340
|
+
def _ensure_refresh_monitor(self) -> None:
|
|
341
|
+
"""Restart polling when a terminal worker/display remains pending."""
|
|
342
|
+
|
|
343
|
+
if not self._can_process_refresh():
|
|
344
|
+
return
|
|
345
|
+
try:
|
|
346
|
+
dataset = self.ds
|
|
347
|
+
except (AttributeError, KeyError, RuntimeError):
|
|
348
|
+
return
|
|
349
|
+
if not self._refresh_monitor_required(dataset):
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
state = self.__dict__
|
|
353
|
+
monitor = state.get("monitor")
|
|
354
|
+
spin_box = state.get("spinBox")
|
|
355
|
+
if monitor is None or spin_box is None:
|
|
356
|
+
return
|
|
357
|
+
is_active = getattr(monitor, "isActive", None)
|
|
358
|
+
if callable(is_active) and is_active():
|
|
359
|
+
return
|
|
360
|
+
self.monitorIntervalChanged(refresh_interval_value(spin_box))
|
|
361
|
+
|
|
362
|
+
def _mark_display_synchronized(self, worker: Any) -> bool:
|
|
363
|
+
"""Publish terminal plot state after the concrete display commit."""
|
|
364
|
+
|
|
365
|
+
if worker is None or worker is not self.__dict__.get("worker"):
|
|
366
|
+
return False
|
|
367
|
+
if getattr(worker, "is_cancelled", lambda: False)():
|
|
368
|
+
return False
|
|
369
|
+
if not self._refresh_publication_source_is_current(worker):
|
|
370
|
+
return False
|
|
371
|
+
if not getattr(worker, "_qplot_display_commit_ready", False):
|
|
372
|
+
return False
|
|
373
|
+
if getattr(worker, "dataset_completed", None) is not True:
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
loaded_from_sql = bool(
|
|
377
|
+
getattr(worker, "loaded_from_sql_heatmap", False)
|
|
378
|
+
)
|
|
379
|
+
if not loaded_from_sql and not cache_parameter_is_synchronized(
|
|
380
|
+
self.ds.cache,
|
|
381
|
+
self.param.name,
|
|
382
|
+
):
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
self._qplot_display_synchronized = True
|
|
386
|
+
self._qplot_display_uses_direct_sql = loaded_from_sql
|
|
387
|
+
worker._qplot_display_commit_ready = False
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
def load_data(
|
|
391
|
+
self,
|
|
392
|
+
wait_on_thread: bool = False,
|
|
393
|
+
*,
|
|
394
|
+
force_sql_heatmap: bool = False,
|
|
395
|
+
heatmap_axis_ranges: dict[str, tuple[float, float]] | None = None,
|
|
396
|
+
heatmap_full_axis_ranges: dict[str, tuple[float, float]] | None = None,
|
|
397
|
+
status_message: str | None = None,
|
|
398
|
+
) -> bool:
|
|
399
|
+
"""
|
|
400
|
+
Produces a worker for loading/refreshing the dataset.
|
|
401
|
+
Then adds the worker to the threadPool queue to work.
|
|
402
|
+
|
|
403
|
+
Can use wait_on_thread=True to force main thread to wait for callback.
|
|
404
|
+
Recommend to avoid where possible, as effects all windows.
|
|
405
|
+
|
|
406
|
+
Parameters
|
|
407
|
+
----------
|
|
408
|
+
wait_on_thread : bool, optional
|
|
409
|
+
If true uses an QEventLoop to stop main code from running until
|
|
410
|
+
worker has finished its task. The default is False.
|
|
411
|
+
|
|
412
|
+
"""
|
|
413
|
+
if not self._can_process_refresh():
|
|
414
|
+
return False
|
|
415
|
+
if not self._source_database_is_current():
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
operations = self.oper_widget.get_data()
|
|
420
|
+
except OperationValidationError as error:
|
|
421
|
+
message = str(error)
|
|
422
|
+
self.show_status(message, 10_000)
|
|
423
|
+
self.show_plot_state(
|
|
424
|
+
"Operations not applied",
|
|
425
|
+
message,
|
|
426
|
+
kind="error",
|
|
427
|
+
)
|
|
428
|
+
return False
|
|
429
|
+
|
|
430
|
+
loader_kwargs = {
|
|
431
|
+
"read_data": True,
|
|
432
|
+
"operations": operations,
|
|
433
|
+
"force_sql_heatmap": force_sql_heatmap,
|
|
434
|
+
"max_full_heatmap_points": self.config.get(
|
|
435
|
+
"runtime_settings.max_full_heatmap_points"
|
|
436
|
+
),
|
|
437
|
+
"max_heatmap_grid_cells": self.config.get(
|
|
438
|
+
"runtime_settings.max_heatmap_grid_cells"
|
|
439
|
+
),
|
|
440
|
+
"max_heatmap_grid_side": self.config.get(
|
|
441
|
+
"runtime_settings.max_heatmap_grid_side"
|
|
442
|
+
),
|
|
443
|
+
"heatmap_axis_ranges": heatmap_axis_ranges,
|
|
444
|
+
"heatmap_full_axis_ranges": heatmap_full_axis_ranges,
|
|
445
|
+
}
|
|
446
|
+
database_identity = getattr(
|
|
447
|
+
getattr(self, "_dataset_key", None),
|
|
448
|
+
"database_identity",
|
|
449
|
+
None,
|
|
450
|
+
)
|
|
451
|
+
if database_identity is not None:
|
|
452
|
+
loader_kwargs["database_identity"] = database_identity
|
|
453
|
+
|
|
454
|
+
worker: Any = loader(
|
|
455
|
+
self.ds.cache,
|
|
456
|
+
self.param,
|
|
457
|
+
self.param_dict,
|
|
458
|
+
self.axis_options,
|
|
459
|
+
**loader_kwargs,
|
|
460
|
+
)
|
|
461
|
+
worker._qplot_operation_apply_generation = int(
|
|
462
|
+
getattr(self.oper_widget, "_apply_generation", 0)
|
|
463
|
+
)
|
|
464
|
+
dataset_key = getattr(self, "_dataset_key", None)
|
|
465
|
+
if dataset_key is not None:
|
|
466
|
+
worker.expected_database_path = dataset_key.database_path
|
|
467
|
+
worker.expected_resolved_database_path = (
|
|
468
|
+
dataset_key.resolved_database_path
|
|
469
|
+
)
|
|
470
|
+
worker.expected_sidecar_identities = dataset_key.sidecar_identities
|
|
471
|
+
worker.sidecar_identities = dataset_key.sidecar_identities
|
|
472
|
+
worker.dataset_length_at_start = self.ds.number_of_results
|
|
473
|
+
|
|
474
|
+
if status_message is not None:
|
|
475
|
+
message = status_message
|
|
476
|
+
else:
|
|
477
|
+
message = f"Loading data for {self.param.name}..."
|
|
478
|
+
self.show_status(message, 0)
|
|
479
|
+
self.show_plot_state(message, kind="loading")
|
|
480
|
+
worker.started_at = perf_counter()
|
|
481
|
+
|
|
482
|
+
# Callback
|
|
483
|
+
worker.emitter.finished.connect(
|
|
484
|
+
lambda finished, worker=worker: self.refreshPlot(finished, worker=worker)
|
|
485
|
+
)
|
|
486
|
+
worker_registry = getattr(self.threadPool, "_qplot_workers", None)
|
|
487
|
+
if worker_registry is not None:
|
|
488
|
+
worker_registry.add(worker)
|
|
489
|
+
worker.emitter.finished.connect(
|
|
490
|
+
lambda _finished, worker=worker, registry=worker_registry: (
|
|
491
|
+
registry.discard(worker)
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
# Error event handling
|
|
495
|
+
worker.emitter.errorOccurred.connect(
|
|
496
|
+
lambda err, worker=worker: self.err_raiser(err, worker=worker)
|
|
497
|
+
)
|
|
498
|
+
worker.emitter.printer.connect(
|
|
499
|
+
lambda message, worker=worker: self.worker_printer(
|
|
500
|
+
message,
|
|
501
|
+
worker=worker,
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
if wait_on_thread: # Force freeze main thread
|
|
506
|
+
hold_up = QtCore.QEventLoop()
|
|
507
|
+
self.end_wait.connect(hold_up.quit) # Release main thread event
|
|
508
|
+
|
|
509
|
+
if not getattr(self.ds, "running", False):
|
|
510
|
+
# A terminal reload supersedes the previous displayed snapshot.
|
|
511
|
+
# Keep it retryable until this worker's concrete display commit.
|
|
512
|
+
self._qplot_display_synchronized = False
|
|
513
|
+
|
|
514
|
+
# Run worker
|
|
515
|
+
self.worker = worker
|
|
516
|
+
overlay = self.__dict__.get("plot_state_overlay")
|
|
517
|
+
if overlay is not None:
|
|
518
|
+
overlay.track(worker)
|
|
519
|
+
worker_will_start = getattr(type(self), "_refresh_worker_will_start", None)
|
|
520
|
+
if callable(worker_will_start):
|
|
521
|
+
worker_will_start(self, worker)
|
|
522
|
+
try:
|
|
523
|
+
worker.start(self.threadPool)
|
|
524
|
+
except Exception:
|
|
525
|
+
worker.running = False
|
|
526
|
+
if overlay is not None:
|
|
527
|
+
overlay.show("Plot could not start", kind="error")
|
|
528
|
+
raise
|
|
529
|
+
|
|
530
|
+
if wait_on_thread:
|
|
531
|
+
hold_up.exec() # The actual place the code waits for self.end_wait.emit
|
|
532
|
+
self.end_wait.disconnect(hold_up.quit)
|
|
533
|
+
|
|
534
|
+
return True
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@QtCore.pyqtSlot()
|
|
538
|
+
def refreshWindow(self, force: bool = False) -> None:
|
|
539
|
+
"""
|
|
540
|
+
Event handler for monitor timeout and other refresh sources.
|
|
541
|
+
|
|
542
|
+
Check whether refresh should be done and attempts to refresh plot.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
force : bool, optional
|
|
547
|
+
Forces a refresh regarless of checks. The default is False.
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
self.monitor.stop()
|
|
551
|
+
if not self._can_process_refresh():
|
|
552
|
+
return
|
|
553
|
+
if not self._source_database_is_current():
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
retry = False
|
|
557
|
+
dataset = None
|
|
558
|
+
|
|
559
|
+
try:
|
|
560
|
+
dataset = self.ds
|
|
561
|
+
current_ds_len = dataset.number_of_results
|
|
562
|
+
|
|
563
|
+
# Plot has started, worker first defined in initFrame
|
|
564
|
+
if not hasattr(self, "worker"):
|
|
565
|
+
self.initFrame() # defined in children classes
|
|
566
|
+
retry = True
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
# Check if new data has been added to the dataset
|
|
570
|
+
if current_ds_len != self.last_ds_len or force:
|
|
571
|
+
if self.worker.running: # No need to run if already updating
|
|
572
|
+
self._queue_pending_refresh(force=force)
|
|
573
|
+
if force:
|
|
574
|
+
self.show_status("Refresh queued after the current load.", 3000)
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
# The actual refresh line
|
|
578
|
+
self.load_data()
|
|
579
|
+
|
|
580
|
+
elif self._refresh_monitor_required(dataset):
|
|
581
|
+
# Source completion is global, but this parameter/cache/display
|
|
582
|
+
# may still need its own successful terminal refresh.
|
|
583
|
+
self._sync_dataset_completion(dataset)
|
|
584
|
+
|
|
585
|
+
except Exception:
|
|
586
|
+
retry = True
|
|
587
|
+
raise
|
|
588
|
+
|
|
589
|
+
finally: # Ran after return or otherwise
|
|
590
|
+
|
|
591
|
+
# restart monitor
|
|
592
|
+
if retry or (
|
|
593
|
+
dataset is not None
|
|
594
|
+
and self._refresh_monitor_required(dataset)
|
|
595
|
+
):
|
|
596
|
+
self.monitorIntervalChanged(refresh_interval_value(self.spinBox))
|
|
597
|
+
|
|
598
|
+
# restard monitor if any subplots are live
|
|
599
|
+
elif dataset is not None:
|
|
600
|
+
lines = self.__dict__.get("lines")
|
|
601
|
+
if isinstance(lines, dict) and lines:
|
|
602
|
+
main_line = self.__dict__.get("line")
|
|
603
|
+
for subplot in lines.values():
|
|
604
|
+
if subplot is main_line:
|
|
605
|
+
continue
|
|
606
|
+
source = getattr(subplot, "from_win", None)
|
|
607
|
+
running = (
|
|
608
|
+
plot_refresh_required(source)
|
|
609
|
+
if source is not None
|
|
610
|
+
else bool(getattr(subplot, "running", False))
|
|
611
|
+
)
|
|
612
|
+
subplot.running = bool(running)
|
|
613
|
+
if running:
|
|
614
|
+
self.monitorIntervalChanged(refresh_interval_value(self.spinBox))
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _schedule_pending_refresh(self) -> None:
|
|
619
|
+
state = self.__dict__
|
|
620
|
+
if not state.get("_refresh_pending", False):
|
|
621
|
+
return
|
|
622
|
+
if state.get("_refresh_pending_scheduled", False):
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
self._refresh_pending_scheduled = True
|
|
626
|
+
QtCore.QTimer.singleShot(0, self._run_pending_refresh)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _queue_pending_refresh(self, *, force: bool = False) -> None:
|
|
630
|
+
"""Coalesce refreshes while retaining explicit-force provenance."""
|
|
631
|
+
|
|
632
|
+
state = self.__dict__
|
|
633
|
+
if not state.get("_refresh_pending", False):
|
|
634
|
+
self._refresh_pending_force = False
|
|
635
|
+
self._refresh_pending = True
|
|
636
|
+
if force:
|
|
637
|
+
self._refresh_pending_force = True
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _run_pending_refresh(self) -> None:
|
|
641
|
+
self._refresh_pending_scheduled = False
|
|
642
|
+
if not self.__dict__.get("_refresh_pending", False):
|
|
643
|
+
return
|
|
644
|
+
if not self._can_process_refresh():
|
|
645
|
+
self._refresh_pending = False
|
|
646
|
+
self._refresh_pending_force = False
|
|
647
|
+
return
|
|
648
|
+
if getattr(getattr(self, "worker", None), "running", False):
|
|
649
|
+
self._schedule_pending_refresh()
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
force = bool(self.__dict__.get("_refresh_pending_force", False))
|
|
653
|
+
self._refresh_pending = False
|
|
654
|
+
self._refresh_pending_force = False
|
|
655
|
+
if not force and not self._refresh_monitor_required():
|
|
656
|
+
return
|
|
657
|
+
self.refreshWindow(force=force)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@QtCore.pyqtSlot(bool)
|
|
661
|
+
def refreshPlot(
|
|
662
|
+
self,
|
|
663
|
+
finished: bool = True,
|
|
664
|
+
worker: Any | None = None,
|
|
665
|
+
) -> bool | None:
|
|
666
|
+
"""
|
|
667
|
+
Produces a shallow copy of data produced by worker.
|
|
668
|
+
This is inhertited by plot<1/2>d to actually use the loaded data.
|
|
669
|
+
|
|
670
|
+
Parameters
|
|
671
|
+
----------
|
|
672
|
+
finished : bool
|
|
673
|
+
In the event the worker had to abort, finished is False and refresh
|
|
674
|
+
is not ran.
|
|
675
|
+
|
|
676
|
+
"""
|
|
677
|
+
if worker is None:
|
|
678
|
+
worker = self.__dict__.get("worker")
|
|
679
|
+
|
|
680
|
+
if worker is None:
|
|
681
|
+
return False
|
|
682
|
+
|
|
683
|
+
current_worker = self.__dict__.get("worker")
|
|
684
|
+
if worker is not current_worker:
|
|
685
|
+
worker.running = False
|
|
686
|
+
return False
|
|
687
|
+
worker = current_worker
|
|
688
|
+
|
|
689
|
+
was_cancelled = bool(
|
|
690
|
+
getattr(worker, "is_cancelled", lambda: False)()
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
if not self._can_process_refresh():
|
|
694
|
+
worker.running = False
|
|
695
|
+
self.end_wait.emit()
|
|
696
|
+
return False
|
|
697
|
+
if not self._source_database_is_current():
|
|
698
|
+
# A worker can finish from a private snapshot after the source main
|
|
699
|
+
# file was atomically replaced. Do not let that stale result reach
|
|
700
|
+
# the shared cache or the concrete display callback.
|
|
701
|
+
worker.running = False
|
|
702
|
+
self.end_wait.emit()
|
|
703
|
+
return False
|
|
704
|
+
|
|
705
|
+
try:
|
|
706
|
+
worker._qplot_display_commit_ready = False
|
|
707
|
+
if was_cancelled:
|
|
708
|
+
worker.running = False
|
|
709
|
+
self.show_plot_state("Plot load cancelled")
|
|
710
|
+
return False
|
|
711
|
+
|
|
712
|
+
if not finished: # error in worker
|
|
713
|
+
worker.running = False
|
|
714
|
+
self.show_plot_state(
|
|
715
|
+
"Plot load failed",
|
|
716
|
+
"Check the status bar or diagnostic log for details.",
|
|
717
|
+
kind="error",
|
|
718
|
+
)
|
|
719
|
+
return False
|
|
720
|
+
|
|
721
|
+
self._begin_refresh_publication(worker)
|
|
722
|
+
# Update qcodes dataset variables if db read happened
|
|
723
|
+
if worker.read_data:
|
|
724
|
+
cache = self.ds.cache
|
|
725
|
+
name = self.param.name
|
|
726
|
+
|
|
727
|
+
cache_updated = update_cache_parameter_data(
|
|
728
|
+
cache,
|
|
729
|
+
name,
|
|
730
|
+
worker.updated_read_status,
|
|
731
|
+
worker.updated_write_status,
|
|
732
|
+
worker.cache_data,
|
|
733
|
+
dataset_completed=getattr(
|
|
734
|
+
worker,
|
|
735
|
+
"dataset_completed",
|
|
736
|
+
None,
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
if not cache_updated:
|
|
740
|
+
worker._qplot_publication_snapshot = None
|
|
741
|
+
self._queue_pending_refresh()
|
|
742
|
+
self.show_status(
|
|
743
|
+
"A newer refresh finished first; synchronising this plot...",
|
|
744
|
+
5000,
|
|
745
|
+
)
|
|
746
|
+
return False
|
|
747
|
+
|
|
748
|
+
self._record_refresh_cache_publication(worker)
|
|
749
|
+
if not self._refresh_publication_source_is_current(worker):
|
|
750
|
+
return False
|
|
751
|
+
|
|
752
|
+
if not cache_has_no_written_data(cache):
|
|
753
|
+
self._live = False
|
|
754
|
+
|
|
755
|
+
if (
|
|
756
|
+
getattr(worker, "dataset_completed", None) is True
|
|
757
|
+
and cache_is_live(self.ds.cache)
|
|
758
|
+
):
|
|
759
|
+
# An in-memory live cache is already authoritative; no database
|
|
760
|
+
# cache commit is needed before its terminal display refresh.
|
|
761
|
+
set_cache_parameter_synchronized(
|
|
762
|
+
self.ds.cache,
|
|
763
|
+
self.param.name,
|
|
764
|
+
True,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# set data to be called by plot<1/2>d.refreshPlot()
|
|
768
|
+
self.axis_data = {
|
|
769
|
+
"x": worker.axis_data["x"],
|
|
770
|
+
"y": worker.axis_data["y"],
|
|
771
|
+
}
|
|
772
|
+
self.axis_param = {
|
|
773
|
+
"x": worker.axis_param["x"],
|
|
774
|
+
"y": worker.axis_param["y"],
|
|
775
|
+
}
|
|
776
|
+
self.display_param = getattr(worker, "display_param", self.param)
|
|
777
|
+
|
|
778
|
+
# For 2d plots
|
|
779
|
+
if hasattr(worker, "dataGrid"):
|
|
780
|
+
self.dataGrid = worker.dataGrid
|
|
781
|
+
|
|
782
|
+
# I didnt want to make this a dedicated callback for the few times
|
|
783
|
+
# it is used, as the performace hit is neglible
|
|
784
|
+
# Update text
|
|
785
|
+
self._set_param_axis_labels()
|
|
786
|
+
update_coordinate_context = getattr(
|
|
787
|
+
self,
|
|
788
|
+
"_update_coordinate_context",
|
|
789
|
+
None,
|
|
790
|
+
)
|
|
791
|
+
if callable(update_coordinate_context):
|
|
792
|
+
update_coordinate_context()
|
|
793
|
+
elapsed = perf_counter() - worker.started_at
|
|
794
|
+
|
|
795
|
+
if getattr(worker, "loaded_from_sql_heatmap", False):
|
|
796
|
+
loaded_points = getattr(worker, "loaded_point_count", None)
|
|
797
|
+
if getattr(worker, "aggregated_heatmap_source", False):
|
|
798
|
+
point_text = (
|
|
799
|
+
f"{loaded_points:,} aggregated cells"
|
|
800
|
+
if loaded_points is not None
|
|
801
|
+
else "aggregated cells"
|
|
802
|
+
)
|
|
803
|
+
else:
|
|
804
|
+
point_text = (
|
|
805
|
+
f"{loaded_points:,} sampled points"
|
|
806
|
+
if loaded_points is not None
|
|
807
|
+
else "sampled points"
|
|
808
|
+
)
|
|
809
|
+
self.show_status(
|
|
810
|
+
f"Loaded {point_text} for {self.param.name} "
|
|
811
|
+
f"in {elapsed:.2f} seconds",
|
|
812
|
+
5000,
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
self.show_status(
|
|
816
|
+
f"Loaded {self.ds.number_of_results:,} points for {self.param.name} "
|
|
817
|
+
f"in {elapsed:.2f} seconds",
|
|
818
|
+
5000,
|
|
819
|
+
)
|
|
820
|
+
dataset_length = getattr(worker, "dataset_length_at_start", None)
|
|
821
|
+
if dataset_length is None:
|
|
822
|
+
dataset_length = self.ds.number_of_results
|
|
823
|
+
self.last_ds_len = dataset_length
|
|
824
|
+
from qplot.datahandling.trusted_plot import TrustedPlotDataset
|
|
825
|
+
if isinstance(self.ds, TrustedPlotDataset):
|
|
826
|
+
self.ds.number_of_results = max(self.ds.number_of_results, dataset_length)
|
|
827
|
+
overlay = self.__dict__.get("plot_state_overlay")
|
|
828
|
+
if overlay is not None:
|
|
829
|
+
overlay.rendering(worker)
|
|
830
|
+
else:
|
|
831
|
+
self.hide_plot_state()
|
|
832
|
+
if (
|
|
833
|
+
getattr(worker, "loaded_from_sql_heatmap", False)
|
|
834
|
+
and getattr(worker, "dataset_completed", None) is True
|
|
835
|
+
):
|
|
836
|
+
# Direct SQL heatmaps intentionally bypass the QCoDeS cache.
|
|
837
|
+
# Publish the global source observation here; the concrete
|
|
838
|
+
# heatmap publishes its per-plot state only after rendering.
|
|
839
|
+
set_cache_dataset_completed(self.ds.cache, True)
|
|
840
|
+
worker._qplot_display_commit_ready = bool(
|
|
841
|
+
getattr(worker, "dataset_completed", None) is True
|
|
842
|
+
and (
|
|
843
|
+
getattr(worker, "loaded_from_sql_heatmap", False)
|
|
844
|
+
or cache_parameter_is_synchronized(
|
|
845
|
+
self.ds.cache,
|
|
846
|
+
self.param.name,
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
)
|
|
850
|
+
self._record_refresh_cache_publication(worker)
|
|
851
|
+
if not self._refresh_publication_source_is_current(worker):
|
|
852
|
+
return False
|
|
853
|
+
return True
|
|
854
|
+
|
|
855
|
+
except AttributeError as err:
|
|
856
|
+
# If worker starts too quickly, overwrites data and spits out error.
|
|
857
|
+
# This should no longer be possible so making error soft error.
|
|
858
|
+
# Keep the publication snapshot until the accepted source and this
|
|
859
|
+
# worker's ownership have both been revalidated. In particular,
|
|
860
|
+
# an AttributeError after cache/axis mutation must not turn a
|
|
861
|
+
# simultaneous database replacement into a committed stale view.
|
|
862
|
+
if not self._refresh_publication_source_is_current(worker):
|
|
863
|
+
return False
|
|
864
|
+
self.show_status(f"Refresh skipped: {err}", 10_000)
|
|
865
|
+
self.show_plot_state("Refresh skipped", str(err), kind="error")
|
|
866
|
+
if not self._refresh_publication_source_is_current(worker):
|
|
867
|
+
return False
|
|
868
|
+
worker._qplot_publication_snapshot = None
|
|
869
|
+
return None
|
|
870
|
+
|
|
871
|
+
except Exception:
|
|
872
|
+
# The concrete 1D/2D callback is not entered when the base
|
|
873
|
+
# publication raises. Roll back an owned partial publication here;
|
|
874
|
+
# a source mismatch performs the same rollback and recovery inside
|
|
875
|
+
# the guard, while lost ownership leaves the newer callback alone.
|
|
876
|
+
if isinstance(
|
|
877
|
+
getattr(worker, "_qplot_publication_snapshot", None),
|
|
878
|
+
dict,
|
|
879
|
+
):
|
|
880
|
+
if self._refresh_publication_source_is_current(worker):
|
|
881
|
+
self._rollback_refresh_publication(worker)
|
|
882
|
+
if worker is self.__dict__.get("worker"):
|
|
883
|
+
self.show_plot_state("Plot display failed", kind="error")
|
|
884
|
+
raise
|
|
885
|
+
|
|
886
|
+
finally: # Allow code to move on from wait_on_thread
|
|
887
|
+
if worker is self.worker:
|
|
888
|
+
worker.running = False
|
|
889
|
+
self.end_wait.emit()
|
|
890
|
+
if (
|
|
891
|
+
not getattr(worker, "_qplot_display_commit_ready", False)
|
|
892
|
+
and not getattr(worker, "_qplot_source_rejected", False)
|
|
893
|
+
):
|
|
894
|
+
self._ensure_refresh_monitor()
|
|
895
|
+
if not getattr(worker, "_qplot_source_rejected", False):
|
|
896
|
+
self._schedule_pending_refresh()
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@QtCore.pyqtSlot(Exception)
|
|
900
|
+
def err_raiser(self, err: Exception, worker: Any | None = None) -> None:
|
|
901
|
+
if (
|
|
902
|
+
worker is not None
|
|
903
|
+
and getattr(worker, "is_cancelled", lambda: False)()
|
|
904
|
+
):
|
|
905
|
+
worker.running = False
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
if not self._can_process_refresh():
|
|
909
|
+
if worker is not None:
|
|
910
|
+
worker.running = False
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
if worker is not None and worker is not self.worker:
|
|
914
|
+
return
|
|
915
|
+
|
|
916
|
+
if worker is not None and getattr(worker, "database_replaced", False):
|
|
917
|
+
self._source_database_is_current()
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
message = f"{type(err).__name__}: {err}"
|
|
921
|
+
log_exception("Plot worker error", err, __name__)
|
|
922
|
+
self.show_status(f"Worker error: {message}", 10_000)
|
|
923
|
+
self.show_plot_state("Plot load failed", message, kind="error")
|
|
924
|
+
|
|
925
|
+
if message == self._last_error_text:
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
self._last_error_text = message
|
|
929
|
+
self.show_error("Plot Error", "A plot worker failed.", message)
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
@QtCore.pyqtSlot(str)
|
|
933
|
+
def worker_printer(self, fstr: str, worker: Any | None = None) -> None:
|
|
934
|
+
if not self._can_process_refresh():
|
|
935
|
+
return
|
|
936
|
+
|
|
937
|
+
if worker is not None and worker is not self.worker:
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
# Worker print() often does not work, so done through event handlers
|
|
941
|
+
self.show_status(fstr, 5000)
|
|
942
|
+
print(fstr)
|