qplotter 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. qplotter-1.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,221 @@
1
+ import os
2
+ from dataclasses import dataclass, field
3
+
4
+ from PyQt6 import QtCore
5
+
6
+ from qplot.datahandling.file_identity import (
7
+ DatabaseFileIdentity,
8
+ DatabaseInstance,
9
+ canonical_database_path,
10
+ database_file_identity,
11
+ database_instance,
12
+ database_sidecar_identities,
13
+ logical_database_path,
14
+ )
15
+
16
+ _CAPTURE_CURRENT_SIDECARS: frozenset[DatabaseFileIdentity] = frozenset()
17
+
18
+
19
+ def close_dataset_connection(dataset: object) -> bool:
20
+ """Close a dataset's backing connection when it exposes one."""
21
+
22
+ connection = getattr(dataset, "conn", None)
23
+ close = getattr(connection, "close", None)
24
+ if not callable(close):
25
+ return False
26
+ close()
27
+ return True
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class DatasetKey:
32
+ """Identifies a dataset within one particular database file."""
33
+
34
+ database_path: str
35
+ guid: str
36
+ database_identity: DatabaseFileIdentity | None = None
37
+ resolved_database_path: str | None = None
38
+ sidecar_identities: frozenset[DatabaseFileIdentity] = field(
39
+ default=_CAPTURE_CURRENT_SIDECARS,
40
+ compare=False,
41
+ hash=False,
42
+ repr=False,
43
+ )
44
+
45
+ def __post_init__(self) -> None:
46
+ logical_path = logical_database_path(self.database_path)
47
+ object.__setattr__(self, "database_path", logical_path)
48
+ object.__setattr__(self, "guid", str(self.guid))
49
+ if self.resolved_database_path is None:
50
+ object.__setattr__(
51
+ self,
52
+ "resolved_database_path",
53
+ canonical_database_path(logical_path),
54
+ )
55
+ else:
56
+ object.__setattr__(
57
+ self,
58
+ "resolved_database_path",
59
+ canonical_database_path(self.resolved_database_path),
60
+ )
61
+ if self.database_identity is None:
62
+ resolved_path = self.resolved_database_path
63
+ assert resolved_path is not None
64
+ object.__setattr__(
65
+ self,
66
+ "database_identity",
67
+ database_file_identity(resolved_path),
68
+ )
69
+ # Identity distinguishes an omitted argument from an explicitly bound
70
+ # empty sidecar set. The latter must stay empty: recapturing a WAL/SHM
71
+ # that appeared later would silently rebind an existing request.
72
+ if self.sidecar_identities is _CAPTURE_CURRENT_SIDECARS:
73
+ object.__setattr__(
74
+ self,
75
+ "sidecar_identities",
76
+ database_sidecar_identities(
77
+ logical_path,
78
+ self.resolved_database_path,
79
+ ),
80
+ )
81
+
82
+
83
+ def dataset_key_matches_database_instance(
84
+ dataset_key: DatasetKey,
85
+ current_instance: DatabaseInstance,
86
+ *,
87
+ allow_new_sidecars: bool = False,
88
+ ) -> bool:
89
+ """Return whether an observation still represents ``dataset_key``.
90
+
91
+ A transient explicit-action key may allow the first WAL/SHM appearance
92
+ while its read-only DataSet is being materialised. A key retained by a
93
+ plot is final and therefore requires the exact observed sidecar set.
94
+ """
95
+
96
+ if current_instance.logical_path != dataset_key.database_path:
97
+ return False
98
+ if current_instance.resolved_path != dataset_key.resolved_database_path:
99
+ return False
100
+
101
+ expected_identity = dataset_key.database_identity
102
+ if expected_identity is None:
103
+ if current_instance.identity is not None or os.path.isfile(
104
+ dataset_key.database_path
105
+ ):
106
+ return False
107
+ elif current_instance.identity != expected_identity:
108
+ return False
109
+
110
+ expected_sidecars = dataset_key.sidecar_identities
111
+ if allow_new_sidecars:
112
+ return expected_sidecars.issubset(current_instance.sidecar_identities)
113
+ return expected_sidecars == current_instance.sidecar_identities
114
+
115
+
116
+ def dataset_key_matches_current_source(
117
+ dataset_key: DatasetKey,
118
+ *,
119
+ allow_new_sidecars: bool = False,
120
+ ) -> bool:
121
+ """Inspect only filesystem identity metadata for a dataset source."""
122
+
123
+ return dataset_key_matches_database_instance(
124
+ dataset_key,
125
+ database_instance(dataset_key.database_path),
126
+ allow_new_sidecars=allow_new_sidecars,
127
+ )
128
+
129
+
130
+ def bind_dataset_key_to_current_source(
131
+ dataset_key: DatasetKey,
132
+ ) -> DatasetKey | None:
133
+ """Promote compatible first sidecars into an exact retained-plot key."""
134
+
135
+ current_instance = database_instance(dataset_key.database_path)
136
+ if not dataset_key_matches_database_instance(
137
+ dataset_key,
138
+ current_instance,
139
+ allow_new_sidecars=True,
140
+ ):
141
+ return None
142
+
143
+ bound_key = DatasetKey(
144
+ current_instance.logical_path,
145
+ dataset_key.guid,
146
+ current_instance.identity,
147
+ current_instance.resolved_path,
148
+ sidecar_identities=current_instance.sidecar_identities,
149
+ )
150
+ # Close the metadata-only capture race before returning a key that callers
151
+ # may retain for automatic refreshes.
152
+ if not dataset_key_matches_current_source(bound_key):
153
+ return None
154
+ return bound_key
155
+
156
+
157
+ @dataclass(frozen=True, slots=True)
158
+ class TraceKey:
159
+ """Identifies one trace source in one database-backed dataset."""
160
+
161
+ dataset_key: DatasetKey
162
+ parameter_name: str
163
+ sweep_id: int | None = None
164
+
165
+ def __post_init__(self) -> None:
166
+ object.__setattr__(self, "parameter_name", str(self.parameter_name))
167
+ if self.sweep_id is not None:
168
+ object.__setattr__(self, "sweep_id", int(self.sweep_id))
169
+
170
+
171
+ @dataclass
172
+ class DatasetHandle:
173
+ """
174
+ Tracks a dataset held open by one or more plot windows.
175
+
176
+ """
177
+
178
+ dataset: object
179
+ users: int = 1
180
+ delete_timer: QtCore.QTimer | None = None
181
+ closed: bool = False
182
+ database_identity: DatabaseFileIdentity | None = None
183
+ sidecar_identities: frozenset[DatabaseFileIdentity] | None = None
184
+
185
+ def retain(self):
186
+ """
187
+ Records another active user and cancels pending delayed deletion.
188
+
189
+ """
190
+ self.users += 1
191
+ self.cancel_delete_timer()
192
+
193
+
194
+ def release(self):
195
+ """
196
+ Records that one active user has closed.
197
+
198
+ """
199
+ self.users -= 1
200
+ return self.users
201
+
202
+
203
+ def cancel_delete_timer(self):
204
+ """
205
+ Stops any pending delayed deletion timer.
206
+
207
+ """
208
+ if self.delete_timer is not None:
209
+ self.delete_timer.stop()
210
+ self.delete_timer = None
211
+
212
+
213
+ def close(self):
214
+ """Cancel pending eviction and deterministically close the dataset."""
215
+
216
+ self.cancel_delete_timer()
217
+ if self.closed:
218
+ return False
219
+ closed_connection = close_dataset_connection(self.dataset)
220
+ self.closed = True
221
+ return closed_connection
@@ -0,0 +1,48 @@
1
+ """Reject renderer conversions that merge distinct recorded values."""
2
+
3
+ import numpy as np
4
+
5
+ DISPLAY_PRECISION_MESSAGE = (
6
+ "Distinct recorded values become identical at display precision. "
7
+ "Apply a subtraction operation to view relative values. "
8
+ "The original samples remain available for CSV export and processing."
9
+ )
10
+
11
+ HEATMAP_DISPLAY_RANGE_MESSAGE = (
12
+ "The heatmap coordinate range cannot be displayed safely. "
13
+ "Recorded samples remain available for CSV export and processing."
14
+ )
15
+
16
+
17
+ def heatmap_display_range_is_finite(geometry, pixel_size=(1., 1.)):
18
+ """Require finite image and viewport scales before installing Qt transforms."""
19
+ if not np.all(np.isfinite(geometry.rect)):
20
+ return False
21
+ spans = np.asarray(geometry.rect[2:])
22
+ cells = np.asarray((geometry.x.count, geometry.y.count))
23
+ with np.errstate(over="ignore", divide="ignore", invalid="ignore"):
24
+ return bool(np.all(np.isfinite(spans / cells))
25
+ and np.all(np.isfinite(np.maximum(pixel_size, cells) / spans)))
26
+
27
+
28
+ def float_display_loses_distinctions(values):
29
+ """Check exact integer/object values before a renderer converts to float.
30
+
31
+ Ordinary float64 and narrower samples are already representable. Only
32
+ potentially inexact inputs need sorting; adjacent distinct exact values
33
+ must remain distinct after conversion, including fractional object cells.
34
+ """
35
+ values = np.asarray(values)
36
+ if values.dtype.kind not in "iuO" or values.size < 2:
37
+ return False
38
+ if values.dtype.kind in "iu" and (
39
+ values.dtype.itemsize < 8
40
+ or (np.min(values) >= -(2**53) and np.max(values) <= 2**53)
41
+ ):
42
+ return False
43
+ rendered = values.astype(float)
44
+ unique = np.unique(values[np.isfinite(rendered)])
45
+ if unique.size < 2:
46
+ return False
47
+ rendered_unique = unique.astype(float)
48
+ return bool(np.any(rendered_unique[1:] == rendered_unique[:-1]))
@@ -0,0 +1,70 @@
1
+ import json
2
+
3
+ from PyQt6 import QtCore
4
+
5
+ RUN_PREVIEW_MIME = "application/x-qplot-run-preview"
6
+
7
+
8
+ def make_run_preview_mime(
9
+ guid, parameter, axes=None, database_path=None, *, axes_pending=False):
10
+ payload: dict[str, str | list[str] | bool] = {
11
+ "guid": str(guid or ""),
12
+ "parameter": str(parameter or ""),
13
+ "axes": [str(axis) for axis in (axes or [])],
14
+ }
15
+ if database_path:
16
+ payload["database_path"] = str(database_path)
17
+ if axes_pending:
18
+ payload["axes_pending"] = True
19
+ mime_data = QtCore.QMimeData()
20
+ mime_data.setData(
21
+ RUN_PREVIEW_MIME,
22
+ json.dumps(payload).encode("utf-8")
23
+ )
24
+ mime_data.setText(f"{payload['parameter']} from {payload['guid']}")
25
+ return mime_data
26
+
27
+
28
+ def run_preview_payload_from_mime(mime_data):
29
+ if mime_data is None or not mime_data.hasFormat(RUN_PREVIEW_MIME):
30
+ return None
31
+
32
+ try:
33
+ payload = json.loads(bytes(mime_data.data(RUN_PREVIEW_MIME)).decode("utf-8"))
34
+ except (TypeError, ValueError, json.JSONDecodeError):
35
+ return None
36
+
37
+ guid = str(payload.get("guid") or "")
38
+ parameter = str(payload.get("parameter") or "")
39
+ if not guid or not parameter:
40
+ return None
41
+
42
+ axes = payload.get("axes") or []
43
+ if isinstance(axes, str):
44
+ axes = [axes]
45
+ elif not isinstance(axes, (list, tuple)):
46
+ axes = []
47
+
48
+ normalised: dict[str, str | list[str] | bool] = {
49
+ "guid": guid,
50
+ "parameter": parameter,
51
+ "axes": [str(axis) for axis in axes],
52
+ }
53
+ database_path = str(payload.get("database_path") or "")
54
+ if database_path:
55
+ normalised["database_path"] = database_path
56
+ if payload.get("axes_pending") is True:
57
+ normalised["axes_pending"] = True
58
+ return normalised
59
+
60
+
61
+ def preview_drop_is_compatible(target_axes, payload):
62
+ """Check plot dimensionality; physical-axis compatibility is handled on drop."""
63
+ axes = tuple(str(axis) for axis in (payload.get("axes") or []))
64
+ target_axes = tuple(str(axis) for axis in target_axes)
65
+ if not axes and payload.get("axes_pending") is True:
66
+ # Pending thumbnails have a run/parameter identity but no rendered
67
+ # axes yet. The add action loads the actual parameter before choosing
68
+ # axes and requesting approval for any remaining incompatibility.
69
+ return len(target_axes) in (1, 2)
70
+ return len(target_axes) in (1, 2) and len(axes) in (1, 2) and len(set(axes)) == len(axes)