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