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,650 @@
1
+ """
2
+ FUNCTIONS TO BE PASSED TO THE WORKER VIA OPERATIONS
3
+ PLEASE IMPORT INTO qplot.windows._widget.operations AND ADD TO RESPECTIVE CLASS
4
+ AT BOTTOM OF FILE.
5
+ The code should handle the rest.
6
+
7
+ Function .worker.loader.do_operations() will give each operation
8
+ data_dict{x : np.array, y : np.array, z : np.array | None} as only arguemnt.
9
+ The operations tab can pass 1 user defined input of type: int, float, str or a
10
+ list of options.
11
+ To pass other arguments, please use lambda functions, i.e.:
12
+ "func" : lambda data_dict: subtract_mean("x", data_dict)
13
+
14
+ .worker.loader.do_operations() expects a dictionary to be returned which is
15
+ used to find which properties to update the keyed value.
16
+ """
17
+ import math
18
+ from decimal import Decimal
19
+ from fractions import Fraction
20
+
21
+ import numpy as np
22
+
23
+ from qplot.datahandling.parameter_data import numeric_isfinite, numeric_isnan
24
+ from qplot.tools.sample_statistics import sample_fraction
25
+
26
+
27
+ def _integer_differences(values, axis=0, step=1, *, cancelled_callback=None):
28
+ """Subtract before conversion; int64 and uint64 gaps can exceed int64."""
29
+ exact_values = np.moveaxis(values.astype(object), axis, -1)
30
+ if values.dtype.kind == "O" and not _integer_samples(values, cancelled_callback):
31
+ # Python also rounds integers before subtracting binary floats.
32
+ # Exact mixed cells preserve steps above 2**53 and allow subtraction
33
+ # between decimal clipping bounds and original binary floats.
34
+ # Missing cells and infinities cannot be converted to Fraction. Keep
35
+ # those as floats so they propagate through only their own stencils.
36
+ finite = numeric_isfinite(exact_values)
37
+ left_values, right_values = exact_values[..., :-step], exact_values[..., step:]
38
+
39
+ def mixed_differences():
40
+ pairs = zip(left_values.flat, right_values.flat,
41
+ finite[..., :-step].flat, finite[..., step:].flat, strict=True)
42
+ for index, (left, right, left_finite, right_finite) in enumerate(pairs):
43
+ if index % 1024 == 0:
44
+ _check_cancelled(cancelled_callback)
45
+ if left_finite and right_finite:
46
+ difference = _sample_fraction(right) - _sample_fraction(left)
47
+ else:
48
+ difference = float(right) - float(left)
49
+ yield _fraction_as_float(difference)
50
+
51
+ numeric = np.fromiter(mixed_differences(), dtype=np.float64,
52
+ count=left_values.size).reshape(left_values.shape)
53
+ return np.moveaxis(numeric, -1, axis)
54
+ with np.errstate(over="ignore", invalid="ignore"):
55
+ differences = exact_values[..., step:] - exact_values[..., :-step]
56
+ try:
57
+ numeric = differences.astype(np.float64)
58
+ except OverflowError:
59
+ # Fraction differences from decimal clipping may exceed float64.
60
+ # Keep signed overflow so finite-input secant/stencil repair can
61
+ # divide the exact difference before rounding the resulting quotient.
62
+ def rounded_cells():
63
+ for index, value in enumerate(differences.flat):
64
+ if index % 1024 == 0:
65
+ _check_cancelled(cancelled_callback)
66
+ yield _fraction_as_float(value)
67
+
68
+ numeric = np.fromiter(rounded_cells(), dtype=np.float64,
69
+ count=differences.size).reshape(differences.shape)
70
+ return np.moveaxis(numeric, -1, axis)
71
+
72
+
73
+ def _checked_samples(values, cancelled_callback):
74
+ for index, value in enumerate(values.flat):
75
+ if index % 1024 == 0:
76
+ _check_cancelled(cancelled_callback)
77
+ yield value
78
+ _check_cancelled(cancelled_callback)
79
+
80
+
81
+ def _integer_samples(values, cancelled_callback=None):
82
+ if values.dtype.kind in "iu":
83
+ return True
84
+ # Joined QCoDeS records can use object cells to retain mixed int64/uint64.
85
+ return values.dtype.kind == "O" and all(
86
+ isinstance(value, (int, np.integer)) and not isinstance(value, (bool, np.bool_))
87
+ for value in _checked_samples(values, cancelled_callback)
88
+ )
89
+
90
+
91
+ def _numeric_object_samples(values, cancelled_callback=None):
92
+ return values.dtype.kind == "O" and all(
93
+ isinstance(value, (int, float, Decimal, Fraction, np.integer, np.floating))
94
+ and not isinstance(value, (bool, np.bool_))
95
+ for value in _checked_samples(values, cancelled_callback)
96
+ )
97
+
98
+
99
+ def _gradient_with_integer_samples(
100
+ data, coordinates, spacing, axis, integer_data, *, cancelled_callback=None):
101
+ """Use gradient's first-order edges and three-point interior stencil."""
102
+ samples = np.moveaxis(np.asarray(data), axis, -1)
103
+ mixed_objects = samples.dtype.kind == "O" and not _integer_samples(samples, cancelled_callback)
104
+ dtype = np.float64 if integer_data else np.result_type(samples.dtype, np.float64)
105
+ result = np.empty(samples.shape, dtype=dtype)
106
+ if integer_data:
107
+ differences = _integer_differences(samples, axis=-1,
108
+ cancelled_callback=cancelled_callback)
109
+ else:
110
+ samples = samples.astype(dtype, copy=False)
111
+ with np.errstate(over="ignore", invalid="ignore"):
112
+ differences = np.diff(samples, axis=-1)
113
+ with np.errstate(over="ignore", invalid="ignore", divide="ignore"):
114
+ slopes = differences / spacing
115
+ if integer_data:
116
+ # Mixed QCoDeS integer/float records use object cells. Their finite
117
+ # floating differences can overflow too; divide the exact recorded
118
+ # difference before rounding, including the first-order edge slopes.
119
+ repair = ((~np.isfinite(differences) | mixed_objects)
120
+ & numeric_isfinite(samples[..., 1:])
121
+ & numeric_isfinite(samples[..., :-1]))
122
+ for index, position in enumerate(zip(*np.nonzero(repair), strict=True)):
123
+ if index % 1024 == 0:
124
+ _check_cancelled(cancelled_callback)
125
+ left = position[-1]
126
+ row = position[:-1]
127
+ distance = _sample_fraction(coordinates[left + 1]) - _sample_fraction(coordinates[left])
128
+ difference = (_sample_fraction(samples[row][left + 1])
129
+ - _sample_fraction(samples[row][left]))
130
+ slopes[position] = _fraction_as_float(difference / distance)
131
+ else:
132
+ # A finite sample jump can exceed float's range before division by a
133
+ # large, finite spacing restores a representable secant slope.
134
+ repair = (~np.isfinite(differences) & np.isfinite(samples[..., 1:])
135
+ & np.isfinite(samples[..., :-1]))
136
+ if np.any(repair):
137
+ distances = np.broadcast_to(spacing, differences.shape)
138
+ slopes[repair] = (
139
+ samples[..., 1:][repair] / 2. - samples[..., :-1][repair] / 2.
140
+ ) / (distances[repair] / 2.)
141
+
142
+ if np.all(spacing == spacing[0]):
143
+ dx = spacing[0]
144
+ if integer_data:
145
+ # The uniform central stencil uses only its two outer samples.
146
+ # Summing adjacent differences would involve a missing centre and
147
+ # erase a valid derivative (or subtract opposite infinities).
148
+ outer_difference = _integer_differences(
149
+ samples, axis=-1, step=2, cancelled_callback=cancelled_callback,
150
+ )
151
+ if abs(dx) > np.finfo(float).max / 2:
152
+ central = (outer_difference / 2.) / dx
153
+ else:
154
+ central = outer_difference / (2. * dx)
155
+ repair = ((~np.isfinite(outer_difference) | mixed_objects)
156
+ & numeric_isfinite(samples[..., 2:])
157
+ & numeric_isfinite(samples[..., :-2]))
158
+ for index, position in enumerate(zip(*np.nonzero(repair), strict=True)):
159
+ if index % 1024 == 0:
160
+ _check_cancelled(cancelled_callback)
161
+ left = position[-1]
162
+ row = position[:-1]
163
+ distance = (_sample_fraction(coordinates[left + 2])
164
+ - _sample_fraction(coordinates[left]))
165
+ difference = (_sample_fraction(samples[row][left + 2])
166
+ - _sample_fraction(samples[row][left]))
167
+ central[position] = _fraction_as_float(difference / distance)
168
+ result[..., 1:-1] = central
169
+ else:
170
+ with np.errstate(over="ignore", invalid="ignore"):
171
+ outer_difference = samples[..., 2:] - samples[..., :-2]
172
+ denominator = 2. * dx
173
+ central = outer_difference / denominator
174
+ # Both the two-step difference and twice the spacing can exceed
175
+ # float's range even when their quotient is small. Halve the
176
+ # operands first only for those stencils; ordinary small values
177
+ # retain the direct subtraction's precision and missingness.
178
+ repair = (np.isfinite(samples[..., 2:])
179
+ & np.isfinite(samples[..., :-2])
180
+ & (~np.isfinite(outer_difference) | ~np.isfinite(denominator)))
181
+ if np.any(repair):
182
+ central[repair] = (
183
+ samples[..., 2:][repair] / 2. - samples[..., :-2][repair] / 2.
184
+ ) / dx
185
+ result[..., 1:-1] = central
186
+ else:
187
+ dx1, dx2 = spacing[:-1], spacing[1:]
188
+ # Blend the adjacent secant slopes. Normalized spacings avoid
189
+ # under/overflow in the products used by the equivalent three-point
190
+ # coefficients, and subtraction still precedes weighting so stored
191
+ # offsets cannot erase representable differences.
192
+ scale = np.maximum(np.abs(dx1), np.abs(dx2))
193
+ before, after = dx1 / scale, dx2 / scale
194
+ total = before + after
195
+ with np.errstate(over="ignore", invalid="ignore"):
196
+ left_term = (after / total) * slopes[..., :-1]
197
+ right_term = (before / total) * slopes[..., 1:]
198
+ central = left_term + right_term
199
+ if samples.dtype.kind != "c":
200
+ # Individual secants can overflow while their weighted stencil
201
+ # remains finite. Recover only fully finite source stencils;
202
+ # acquired NaN/Inf samples must retain their usual propagation.
203
+ # Finite opposing terms can also cancel after their individual
204
+ # differences or weights have rounded. Recover small residuals
205
+ # from the recorded stencil, not its already rounded secants.
206
+ cancellation = ((np.signbit(left_term) != np.signbit(right_term))
207
+ & (np.abs(central) <= np.sqrt(np.finfo(float).eps)
208
+ * np.maximum(np.abs(left_term), np.abs(right_term))))
209
+ # A normalized spacing can underflow before multiplication by a
210
+ # large secant restores a representable derivative. Replay those
211
+ # stencils even when the rounded weighted terms have the same sign.
212
+ underflow = ((np.abs(before) < np.finfo(float).tiny)
213
+ | (np.abs(after) < np.finfo(float).tiny))
214
+ repair = ((~np.isfinite(central) | cancellation | underflow | mixed_objects)
215
+ & numeric_isfinite(samples[..., :-2])
216
+ & numeric_isfinite(samples[..., 1:-1])
217
+ & numeric_isfinite(samples[..., 2:]))
218
+ for index, position in enumerate(zip(*np.nonzero(repair), strict=True)):
219
+ if index % 1024 == 0:
220
+ _check_cancelled(cancelled_callback)
221
+ centre = position[-1] + 1
222
+ row = position[:-1]
223
+
224
+ # Subtract the original coordinates exactly. Rounded spacing
225
+ # loses the small residual when opposing large slopes cancel.
226
+ left, middle, right = map(_sample_fraction, coordinates[centre - 1:centre + 2])
227
+ before_step, after_step = middle - left, right - middle
228
+ before_value, middle_value, after_value = map(
229
+ _sample_fraction, samples[row][centre - 1:centre + 2],
230
+ )
231
+ derivative = (
232
+ after_step * (middle_value - before_value) / before_step
233
+ + before_step * (after_value - middle_value) / after_step
234
+ ) / (before_step + after_step)
235
+ try:
236
+ central[position] = float(derivative)
237
+ except OverflowError:
238
+ central[position] = np.inf if derivative > 0 else -np.inf
239
+ result[..., 1:-1] = central
240
+
241
+ result[..., 0] = slopes[..., 0]
242
+ result[..., -1] = slopes[..., -1]
243
+ return np.moveaxis(result, -1, axis)
244
+
245
+
246
+ def _check_cancelled(cancelled_callback):
247
+ if cancelled_callback is not None and cancelled_callback():
248
+ raise InterruptedError("Plot operation cancelled.")
249
+
250
+
251
+ def _fraction_as_float(value):
252
+ """Round an exact derived value, retaining signed overflow as infinity."""
253
+ try:
254
+ return float(value)
255
+ except OverflowError:
256
+ return np.inf if value > 0 else -np.inf
257
+
258
+
259
+ def _sample_fraction(value):
260
+ """Use the exact recorded scalar, including NumPy integer/float cells."""
261
+ return sample_fraction(value)
262
+
263
+
264
+ def _center_float_samples(values, axis=-1, *, ignore_nan=False, cancelled_callback=None):
265
+ """Remove a float mean without rounding a large offset into the result.
266
+
267
+ Subtract a recorded anchor before accumulation. Rows whose range exceeds
268
+ float64 use a scaled mean instead, retaining the usual NaN/Inf semantics.
269
+ Process groups of complete rows, checking cancellation between groups.
270
+ Temporary arrays contain at most 65536 cells or one complete row.
271
+ """
272
+ values = np.asarray(values)
273
+ dtype = np.result_type(values.dtype, np.float64)
274
+ samples = np.moveaxis(values.astype(dtype, copy=False), axis, -1)
275
+ if samples.shape[-1] == 0:
276
+ return values.astype(dtype, copy=True)
277
+ rows = samples.reshape(-1, samples.shape[-1])
278
+ centered = np.empty(rows.shape, dtype=dtype)
279
+ mean_func = np.nanmean if ignore_nan else np.mean
280
+ chunk_size = max(1, 65536 // samples.shape[-1])
281
+ for start in range(0, len(rows), chunk_size):
282
+ _check_cancelled(cancelled_callback)
283
+ chunk = rows[start:start + chunk_size]
284
+ finite = np.isfinite(chunk)
285
+ indices = np.argmax(finite, axis=1)
286
+ anchors = chunk[np.arange(len(chunk)), indices, None]
287
+ with np.errstate(over="ignore", invalid="ignore"):
288
+ shifted = chunk - anchors
289
+ offset_mean = mean_func(shifted, axis=1, keepdims=True)
290
+ result = shifted - offset_mean
291
+ overflow = (np.any(np.isinf(shifted) & finite, axis=1)
292
+ | ~np.isfinite(offset_mean[:, 0]))
293
+ infinite_source = np.any(np.isinf(chunk), axis=1)
294
+ expected_finite_mean = (np.any(finite, axis=1) if ignore_nan
295
+ else np.all(finite, axis=1))
296
+ fallback = overflow & expected_finite_mean & ~infinite_source
297
+ if np.any(fallback):
298
+ large = chunk[fallback]
299
+ scale = np.nanmax(np.abs(large), axis=1, keepdims=True)
300
+ mean = mean_func(large / scale, axis=1, keepdims=True) * scale
301
+ result[fallback] = large - mean
302
+ # Anchoring around a large positive/negative sample can round
303
+ # away small source cells across zero before the mean is formed.
304
+ # Sum those original values accurately; nearby same-sign offsets
305
+ # keep the anchor path so fractional offset means remain intact.
306
+ mixed_sign = (np.any(chunk < 0, axis=1) & np.any(chunk > 0, axis=1)
307
+ & expected_finite_mean & ~infinite_source)
308
+ # A rounded mean of subnormal differences can underflow before
309
+ # subtraction. Round each exact residual instead, including the
310
+ # halfway case whose correctly rounded residual is signed zero.
311
+ subnormal = np.any((np.abs(shifted) > 0)
312
+ & (np.abs(shifted) < np.finfo(float).tiny), axis=1)
313
+ sensitive = ((mixed_sign | subnormal)
314
+ & expected_finite_mean & ~infinite_source)
315
+ for row_index in np.flatnonzero(sensitive):
316
+ _check_cancelled(cancelled_callback)
317
+ original = chunk[row_index][finite[row_index]]
318
+
319
+ def values_for_sum(values):
320
+ for index, value in enumerate(values):
321
+ if index % 1024 == 0:
322
+ _check_cancelled(cancelled_callback)
323
+ yield float(value)
324
+
325
+ nonzero = np.abs(original[original != 0])
326
+ # With more than a mantissa's range between samples, rounding
327
+ # the mean itself can change the small residual by one ULP.
328
+ # Subtract exact means in just these sensitive rows.
329
+ exact_required = (subnormal[row_index]
330
+ or np.max(nonzero) / (2. ** 53) > np.min(nonzero))
331
+ if not exact_required:
332
+ try:
333
+ mean = math.fsum(values_for_sum(original)) / len(original)
334
+ except OverflowError:
335
+ # The exact total can overflow despite a finite mean.
336
+ exact_required = True
337
+ if exact_required:
338
+ total = Fraction()
339
+ exact_values = []
340
+ for value in values_for_sum(original):
341
+ exact_value = sample_fraction(value)
342
+ exact_values.append(exact_value)
343
+ total += exact_value
344
+ exact_mean = total / len(original)
345
+ residuals = np.empty(len(original))
346
+ for index, value in enumerate(exact_values):
347
+ if index % 1024 == 0:
348
+ _check_cancelled(cancelled_callback)
349
+ difference = value - exact_mean
350
+ residuals[index] = _fraction_as_float(difference)
351
+ result[row_index, finite[row_index]] = residuals
352
+ else:
353
+ result[row_index] = chunk[row_index] - mean
354
+ if np.any(infinite_source):
355
+ special = chunk[infinite_source]
356
+ result[infinite_source] = special - mean_func(special, axis=1, keepdims=True)
357
+ centered[start:start + len(chunk)] = result
358
+ _check_cancelled(cancelled_callback)
359
+ return np.moveaxis(centered.reshape(samples.shape), -1, axis)
360
+
361
+
362
+ def subtract_mean(
363
+ axis : str,
364
+ data_dict : dict,
365
+ cancelled_callback=None,
366
+ ):
367
+ """
368
+ Subtracts the mean from the dataGrid based on the axis.
369
+
370
+ Parameters
371
+ ----------
372
+ axis : str
373
+ Which axis to caculate the mean on.
374
+ run through rows (axis="y")
375
+ run through cols (axis="x")
376
+ data_dict : dict{str, np.ndarry}
377
+ This function only uses data_dict["z"] :
378
+ the 2d numpy array dataGrid of the plot to opperate on
379
+
380
+ Returns
381
+ -------
382
+ dataGrid : dict{str: np.ndarray}
383
+ returns the updated dictionary in the the form:
384
+ {"z": dataGrid}
385
+
386
+ """
387
+ dataGrid = data_dict["z"]
388
+ num_axis = 1 if axis == "x" else 0
389
+
390
+ _check_cancelled(cancelled_callback)
391
+ if dataGrid.dtype.kind in "iuO":
392
+ # Center exact samples before conversion; NaN holes remain missing.
393
+ # Work one row/column at a time so this path stays cancellable.
394
+ samples = np.moveaxis(dataGrid, num_axis, -1)
395
+ centered = np.full(samples.shape, np.nan, dtype=float)
396
+ for position in np.ndindex(samples.shape[:-1]):
397
+ _check_cancelled(cancelled_callback)
398
+ row = samples[position]
399
+ valid = ~numeric_isnan(row)
400
+ values = row[valid]
401
+ if not values.size:
402
+ continue
403
+ if np.all(numeric_isfinite(values)):
404
+ exact = []
405
+ for index, value in enumerate(values):
406
+ if index % 1024 == 0:
407
+ _check_cancelled(cancelled_callback)
408
+ exact.append(sample_fraction(value))
409
+ total = Fraction()
410
+ for start in range(0, len(exact), 1024):
411
+ _check_cancelled(cancelled_callback)
412
+ total += sum(exact[start:start + 1024])
413
+ mean = total / len(exact)
414
+ residuals = np.empty(len(exact))
415
+ for index, value in enumerate(exact):
416
+ if index % 1024 == 0:
417
+ _check_cancelled(cancelled_callback)
418
+ residuals[index] = _fraction_as_float(value - mean)
419
+ centered[position][valid] = residuals
420
+ else:
421
+ numeric = values.astype(float)
422
+ centered[position][valid] = numeric - np.mean(numeric)
423
+ dataGrid = np.moveaxis(centered, -1, num_axis)
424
+ elif dataGrid.dtype.kind == "f":
425
+ dataGrid = _center_float_samples(
426
+ dataGrid, num_axis, ignore_nan=True,
427
+ cancelled_callback=cancelled_callback,
428
+ )
429
+ else:
430
+ # Narrow floats must be widened before accumulation and subtraction.
431
+ dataGrid = dataGrid.astype(np.result_type(dataGrid.dtype, np.float64), copy=False)
432
+ mean = np.nanmean(dataGrid, axis=num_axis, keepdims=True)
433
+ _check_cancelled(cancelled_callback)
434
+ dataGrid = dataGrid - mean
435
+ _check_cancelled(cancelled_callback)
436
+
437
+ return {"z" : dataGrid}
438
+
439
+
440
+ def pass_filter(
441
+ which : str,
442
+ limit : float | Decimal,
443
+ data_dict : dict,
444
+ cancelled_callback=None,
445
+ ):
446
+ """
447
+ Filters dependant parameter data to set values outside the limit to the
448
+ limit
449
+
450
+ Parameters
451
+ ----------
452
+ which : str
453
+ Whether to do a low or high pass filter.
454
+ low - sets maximum allowed value
455
+ high - sets minimum allowed value
456
+ limit : float
457
+ The boundary value.
458
+ data_dict : dict{str, np.ndarry}
459
+ The data array to operate on.
460
+ This uses the dependant parameter data. (data_dict["y"] or data_dict["z"])
461
+ Integer samples that remain inside the bound retain their exact value.
462
+ Partial fractional clipping may return mixed integer/decimal object cells.
463
+
464
+ Returns
465
+ -------
466
+ data : dict{str: np.ndarray}
467
+ returns the updated dictionary in the the form:
468
+ {"z": new_data} for 2d
469
+ or
470
+ {"y": new_data} for 1d
471
+
472
+ """
473
+ # Get y for 1d or z for 2d
474
+ axis = "z" if data_dict["z"] is not None else "y"
475
+ data = np.asarray(data_dict[axis])
476
+
477
+ # The controls retain the user's decimal text; direct float callers keep
478
+ # their float value. Compare integers with a decimal bound exactly.
479
+ exact_limit = limit if isinstance(limit, Decimal) else Decimal(str(limit))
480
+ float_limit = float(limit)
481
+
482
+ # Set the bounds for arrays that already contain floating-point samples.
483
+ limit_arr: tuple[float | None, float | None]
484
+ if which == "low":
485
+ limit_arr = (None, float_limit)
486
+ elif which == "high":
487
+ limit_arr = (float_limit, None)
488
+ else:
489
+ raise KeyError(f'Invalid value for which: {which}. Must be: "high" or "low"')
490
+
491
+ _check_cancelled(cancelled_callback)
492
+ if data.dtype.kind in "iuO" and not exact_limit.is_nan():
493
+ # NumPy promotes an integer array to float before clipping against a
494
+ # float bound, even when no element changes. Compare Python integers
495
+ # with the bound first, so adjacent int64/uint64 values stay distinct.
496
+ exact_data = data.astype(object)
497
+ # Decimal comparisons with NaN raise InvalidOperation. Missing cells
498
+ # are never clipped, including holes in an integer heatmap.
499
+ valid = ~numeric_isnan(exact_data)
500
+ clipped = np.zeros(data.shape, dtype=bool)
501
+ clipped[valid] = (exact_data[valid] > exact_limit if which == "low"
502
+ else exact_data[valid] < exact_limit)
503
+ if not np.any(clipped):
504
+ new_data = data.copy()
505
+ elif (data.dtype.kind in "iu" and exact_limit.is_finite()
506
+ and exact_limit == exact_limit.to_integral_value()
507
+ and int(np.iinfo(data.dtype).min) <= exact_limit
508
+ <= int(np.iinfo(data.dtype).max)):
509
+ new_data = data.copy()
510
+ new_data[clipped] = int(exact_limit)
511
+ elif np.all(clipped):
512
+ new_data = np.full(data.shape, exact_limit, dtype=object)
513
+ else:
514
+ # A fractional bound and unchanged large integers need different
515
+ # scalar types. Object cells retain both exactly for later ops and
516
+ # the original-data CSV exporter.
517
+ new_data = exact_data
518
+ new_data[clipped] = exact_limit
519
+ else:
520
+ new_data = np.clip(data, *limit_arr)
521
+ _check_cancelled(cancelled_callback)
522
+
523
+ return {axis : new_data}
524
+
525
+
526
+ def differentiate(
527
+ dx : str,
528
+ data_dict : dict,
529
+ cancelled_callback=None,
530
+ ):
531
+ """
532
+ Differentiates the dependant parameter data with respect to the input dx
533
+
534
+ Parameters
535
+ ----------
536
+ dx : str
537
+ The axis to perform the differentiation against.
538
+ data_dict : dict{str : np.ndarry}
539
+ The data array to operate on.
540
+ This uses the dependant parameter data. (data_dict["y"] or data_dict["z"])
541
+ and an independant to find spacing. Coordinates must be finite and
542
+ strictly increasing or decreasing in their supplied order.
543
+
544
+ Returns
545
+ -------
546
+ data : dict{str: np.ndarray}
547
+ returns the updated dictionary in the the form:
548
+ {"z": new_data} for 2d
549
+ or
550
+ {"y": new_data} for 1d
551
+
552
+ """
553
+ _check_cancelled(cancelled_callback)
554
+ if dx not in ["x", "y"]:
555
+ raise KeyError(f'Invalid value for dx: {dx}, must be "x" or "y".')
556
+
557
+ # Get y for 1d or z for 2d
558
+ if data_dict["z"] is not None:
559
+ key = "z"
560
+ axis_num = 1 if dx == "x" else 0
561
+
562
+ else:
563
+ key = "y"
564
+ axis_num = 0
565
+
566
+ data = np.asarray(data_dict[key])
567
+ coordinates = np.asarray(data_dict[dx])
568
+ if coordinates.ndim != 1 or coordinates.size < 2:
569
+ raise ValueError("Differentiation requires at least two axis coordinates.")
570
+ integer_coordinates = (_integer_samples(coordinates, cancelled_callback)
571
+ or _numeric_object_samples(coordinates, cancelled_callback))
572
+ if integer_coordinates:
573
+ spacing = _integer_differences(coordinates, cancelled_callback=cancelled_callback)
574
+ else:
575
+ coordinates = coordinates.astype(float)
576
+ spacing = np.diff(coordinates)
577
+ if ((not integer_coordinates and not np.all(np.isfinite(coordinates)))
578
+ or not np.all(np.isfinite(spacing))):
579
+ raise ValueError("Differentiation axis coordinates must be finite.")
580
+ if np.any(spacing == 0):
581
+ raise ValueError("Differentiation axis coordinates must not repeat.")
582
+ # Reversals can make gradient's three-point stencils singular. Keep the
583
+ # acquisition order: sorting or merging samples would erase hysteresis.
584
+ if not (np.all(spacing > 0) or np.all(spacing < 0)):
585
+ raise ValueError(
586
+ "Differentiation axis coordinates must be strictly increasing or "
587
+ "decreasing; reversing sweeps are not supported."
588
+ )
589
+
590
+ _check_cancelled(cancelled_callback)
591
+ integer_data = (_integer_samples(data, cancelled_callback)
592
+ or _numeric_object_samples(data, cancelled_callback))
593
+ new_data = _gradient_with_integer_samples(
594
+ data, coordinates, spacing, axis_num, integer_data,
595
+ cancelled_callback=cancelled_callback,
596
+ )
597
+ _check_cancelled(cancelled_callback)
598
+
599
+ return {key : new_data}
600
+
601
+
602
+ def fill_heatmap(
603
+ which : str,
604
+ data_dict : dict,
605
+ max_depth : int = 10,
606
+ cancelled_callback=None,
607
+ ):
608
+ _check_cancelled(cancelled_callback)
609
+ data = data_dict["z"].copy()
610
+ if which == "below":
611
+ lines = (data[:, column] for column in range(data.shape[1]))
612
+ elif which == "right":
613
+ lines = (data[row, :] for row in range(data.shape[0]))
614
+ else:
615
+ raise KeyError(f'Invalid value for which: {which}, must be "below" or "right".')
616
+
617
+ if max_depth <= 0:
618
+ return {"z": data}
619
+
620
+ for line in lines:
621
+ _check_cancelled(cancelled_callback)
622
+ position = 0
623
+ while position < len(line):
624
+ if position % 1024 == 0:
625
+ _check_cancelled(cancelled_callback)
626
+ if not numeric_isnan(line[position]):
627
+ position += 1
628
+ continue
629
+
630
+ gap_start = position
631
+ while position < len(line) and numeric_isnan(line[position]):
632
+ if position % 1024 == 0:
633
+ _check_cancelled(cancelled_callback)
634
+ position += 1
635
+
636
+ gap_length = position - gap_start
637
+ bounded = gap_start > 0 and position < len(line)
638
+ if bounded and gap_length <= max_depth:
639
+ line[gap_start:position] = line[gap_start - 1]
640
+
641
+ _check_cancelled(cancelled_callback)
642
+ return {"z" : data}
643
+
644
+
645
+ def integrate(
646
+ dx : str,
647
+ data_dict : dict
648
+ ):
649
+ # TO DO
650
+ pass