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,389 @@
|
|
|
1
|
+
"""qPlot's coordinate-safe mapping for PyQtGraph's native line controls."""
|
|
2
|
+
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pyqtgraph as pg
|
|
8
|
+
from pyqtgraph.graphicsItems.PlotDataItem import PlotDataset
|
|
9
|
+
|
|
10
|
+
from qplot.datahandling.parameter_data import numeric_isfinite
|
|
11
|
+
from qplot.tools.plot_tools import (
|
|
12
|
+
_center_float_samples,
|
|
13
|
+
_fraction_as_float,
|
|
14
|
+
_integer_differences,
|
|
15
|
+
_integer_samples,
|
|
16
|
+
)
|
|
17
|
+
from qplot.tools.sample_statistics import finite_mean
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _safe_difference(values: np.ndarray) -> np.ndarray:
|
|
21
|
+
"""Subtract samples before narrowing arithmetic can overflow or wrap."""
|
|
22
|
+
if values.dtype.kind == "b":
|
|
23
|
+
# NumPy's Boolean diff reports changes, losing their direction.
|
|
24
|
+
return np.diff(values.astype(np.int8)).astype(np.float64)
|
|
25
|
+
if values.dtype.kind in "iu":
|
|
26
|
+
# Narrow integers fit in int64 even after subtraction. For 64-bit
|
|
27
|
+
# integers, Python integers cover the full signed/unsigned difference
|
|
28
|
+
# range. Converting samples to float first would erase adjacent steps
|
|
29
|
+
# above 2**53; casting uint64 to int64 would wrap at the signed boundary.
|
|
30
|
+
dtype = np.int64 if values.dtype.itemsize < 8 else object
|
|
31
|
+
return np.diff(values.astype(dtype)).astype(np.float64)
|
|
32
|
+
if values.dtype.kind == "O":
|
|
33
|
+
# Limit operations retain integers alongside fractional clipped cells.
|
|
34
|
+
# Subtract adjacent integer cells before their float view is made.
|
|
35
|
+
return _integer_differences(values)
|
|
36
|
+
if values.dtype.kind == "f" and values.dtype.itemsize < 8:
|
|
37
|
+
# np.diff retains float16/float32, so even finite operands can
|
|
38
|
+
# overflow before the derivative divides or FFT validates the span.
|
|
39
|
+
return np.diff(values.astype(np.float64))
|
|
40
|
+
return np.diff(values)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _center_integer_samples(values: np.ndarray) -> np.ndarray:
|
|
44
|
+
"""Center integer samples before float conversion can erase their steps."""
|
|
45
|
+
if not len(values):
|
|
46
|
+
return np.array([], dtype=np.float64)
|
|
47
|
+
# Python integers avoid both uint64 wraparound and int64 overflow. Using
|
|
48
|
+
# the exact numerator also retains fractional means above float's integer
|
|
49
|
+
# precision (for example, four adjacent samples near 2**63).
|
|
50
|
+
total = sum(map(int, values))
|
|
51
|
+
count = len(values)
|
|
52
|
+
return np.fromiter(
|
|
53
|
+
((count * int(value) - total) / count for value in values),
|
|
54
|
+
dtype=np.float64, count=count,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _center_object_samples(values: np.ndarray) -> np.ndarray:
|
|
59
|
+
"""Keep integer steps when a limit leaves mixed integer/float cells."""
|
|
60
|
+
numeric = values.astype(np.float64)
|
|
61
|
+
if not np.all(np.isfinite(numeric)):
|
|
62
|
+
return numeric - np.mean(numeric)
|
|
63
|
+
exact = [Fraction(value) for value in values]
|
|
64
|
+
mean = sum(exact) / len(exact)
|
|
65
|
+
return np.fromiter(
|
|
66
|
+
(_fraction_as_float(value - mean) for value in exact),
|
|
67
|
+
dtype=np.float64, count=len(exact),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _safe_secant(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
72
|
+
"""Recover finite quotients when either finite difference overflows."""
|
|
73
|
+
with np.errstate(over="ignore", invalid="ignore", divide="ignore"):
|
|
74
|
+
numerator, denominator = _safe_difference(y), _safe_difference(x)
|
|
75
|
+
result = numerator / denominator
|
|
76
|
+
mixed_objects = ((x.dtype.kind == "O" and not _integer_samples(x))
|
|
77
|
+
or (y.dtype.kind == "O" and not _integer_samples(y)))
|
|
78
|
+
repair = ((~np.isfinite(numerator) | ~np.isfinite(denominator) | mixed_objects)
|
|
79
|
+
& numeric_isfinite(x[:-1]) & numeric_isfinite(x[1:])
|
|
80
|
+
& numeric_isfinite(y[:-1]) & numeric_isfinite(y[1:]))
|
|
81
|
+
for index in np.flatnonzero(repair):
|
|
82
|
+
# This is a rare fallback. Exact fractions avoid both difference
|
|
83
|
+
# overflow and underflow while dividing by a huge coordinate span.
|
|
84
|
+
cells = [value.item() if isinstance(value, np.generic) else value
|
|
85
|
+
for value in (x[index], x[index + 1], y[index], y[index + 1])]
|
|
86
|
+
left_x, right_x, left_y, right_y = map(Fraction, cells)
|
|
87
|
+
distance = right_x - left_x
|
|
88
|
+
if distance:
|
|
89
|
+
quotient = (right_y - left_y) / distance
|
|
90
|
+
try:
|
|
91
|
+
result[index] = float(quotient)
|
|
92
|
+
except OverflowError:
|
|
93
|
+
result[index] = np.inf if quotient > 0 else -np.inf
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _FFTCoordinatesError(ValueError):
|
|
98
|
+
"""Coordinates cannot define a native coordinate-domain FFT."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _interpolate_fft_samples(targets, coordinates, samples):
|
|
102
|
+
"""Repair only interpolation segments whose finite difference overflows."""
|
|
103
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
104
|
+
result = np.interp(targets, coordinates, samples)
|
|
105
|
+
differences = np.diff(samples)
|
|
106
|
+
segments = np.searchsorted(coordinates, targets, side="right") - 1
|
|
107
|
+
segments = np.clip(segments, 0, len(coordinates) - 2)
|
|
108
|
+
repair = (~np.isfinite(differences[segments])
|
|
109
|
+
& np.isfinite(samples[segments]) & np.isfinite(samples[segments + 1]))
|
|
110
|
+
for index in np.flatnonzero(repair):
|
|
111
|
+
segment = segments[index]
|
|
112
|
+
left, right, target = map(Fraction, (
|
|
113
|
+
float(coordinates[segment]), float(coordinates[segment + 1]), float(targets[index]),
|
|
114
|
+
))
|
|
115
|
+
before, after = map(Fraction, (float(samples[segment]), float(samples[segment + 1])))
|
|
116
|
+
result[index] = float(before + (after - before) * (target - left) / (right - left))
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _normalized_real_fft(samples):
|
|
121
|
+
"""Bound FFT sums with binary scaling without erasing small source cells."""
|
|
122
|
+
count = len(samples)
|
|
123
|
+
if not np.all(np.isfinite(samples)):
|
|
124
|
+
return np.fft.rfft(samples) / count
|
|
125
|
+
largest = float(np.max(np.abs(samples)))
|
|
126
|
+
if largest <= np.finfo(float).max / count:
|
|
127
|
+
return np.fft.rfft(samples) / count
|
|
128
|
+
# Each butterfly is bounded by the sum of absolute source amplitudes.
|
|
129
|
+
# A length-sized binary divisor bounds that sum and retains normal small
|
|
130
|
+
# samples exactly; dividing by the largest sample can erase whole signals.
|
|
131
|
+
shift = (count - 1).bit_length()
|
|
132
|
+
scaled = np.ldexp(samples, -shift)
|
|
133
|
+
# Binary scaling may still round subnormal source cells. Transform just
|
|
134
|
+
# that tiny tail separately, with exact binary normalization in its range.
|
|
135
|
+
tail = (samples != 0) & (np.abs(scaled) < np.finfo(float).tiny)
|
|
136
|
+
if np.any(tail):
|
|
137
|
+
scaled[tail] = 0.
|
|
138
|
+
spectrum: np.ndarray = np.fft.rfft(scaled) / count
|
|
139
|
+
# Roundoff at the largest representable source must not turn bounded
|
|
140
|
+
# components into infinity while their binary units are restored.
|
|
141
|
+
bound = np.ldexp(largest, -shift)
|
|
142
|
+
spectrum = (np.ldexp(np.clip(spectrum.real, -bound, bound), shift)
|
|
143
|
+
+ 1j * np.ldexp(np.clip(spectrum.imag, -bound, bound), shift))
|
|
144
|
+
if np.any(tail):
|
|
145
|
+
small = np.where(tail, samples, 0.)
|
|
146
|
+
tail_shift = -int(np.frexp(np.max(np.abs(small)))[1])
|
|
147
|
+
tail_spectrum = np.fft.rfft(np.ldexp(small, tail_shift)) / count
|
|
148
|
+
spectrum += (np.ldexp(tail_spectrum.real, -tail_shift)
|
|
149
|
+
+ 1j * np.ldexp(tail_spectrum.imag, -tail_shift))
|
|
150
|
+
return spectrum
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _prepare_fft_coordinates(x: np.ndarray) -> tuple[np.ndarray, np.ndarray, float, bool]:
|
|
154
|
+
"""Return increasing interpolation coordinates and the frequency step."""
|
|
155
|
+
message = "FFT requires finite, strictly monotonic coordinates with a nonzero span."
|
|
156
|
+
if x.dtype.kind == "b":
|
|
157
|
+
x = x.astype(np.uint8)
|
|
158
|
+
elif x.dtype.kind == "f" and x.dtype.itemsize < 8:
|
|
159
|
+
# Widen the coordinates too: x[-1] - x[0], interpolation, and the
|
|
160
|
+
# uniformity check must all operate beyond the source dtype's range.
|
|
161
|
+
x = x.astype(np.float64)
|
|
162
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
163
|
+
dx = _safe_difference(x)
|
|
164
|
+
if not np.all(numeric_isfinite(x)) or len(x) == 0:
|
|
165
|
+
raise _FFTCoordinatesError(message)
|
|
166
|
+
# Retain PyQtGraph's supported one-sample DC spectrum.
|
|
167
|
+
if len(x) == 1:
|
|
168
|
+
return x, dx, 1., False
|
|
169
|
+
reverse = bool(np.all(dx < 0))
|
|
170
|
+
if reverse:
|
|
171
|
+
x, dx = x[::-1], -dx[::-1]
|
|
172
|
+
elif not np.all(dx > 0):
|
|
173
|
+
raise _FFTCoordinatesError(message)
|
|
174
|
+
coordinate_scale = 1.
|
|
175
|
+
if x.dtype.kind in "iuO":
|
|
176
|
+
# Rebase before float conversion to retain nearby steps above 2**53
|
|
177
|
+
# without unsigned/signed overflow.
|
|
178
|
+
exact: np.ndarray = x.astype(object)
|
|
179
|
+
if x.dtype.kind == "O" and not _integer_samples(x):
|
|
180
|
+
exact = cast(np.ndarray, np.frompyfunc(Fraction, 1, 1)(exact))
|
|
181
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
182
|
+
try:
|
|
183
|
+
rebased = (exact - exact[0]).astype(np.float64)
|
|
184
|
+
except (TypeError, OverflowError):
|
|
185
|
+
exact = cast(np.ndarray, np.frompyfunc(Fraction, 1, 1)(exact))
|
|
186
|
+
rebased = np.fromiter(
|
|
187
|
+
(_fraction_as_float(value - exact[0]) for value in exact),
|
|
188
|
+
dtype=np.float64, count=len(exact),
|
|
189
|
+
)
|
|
190
|
+
if np.all(np.isfinite(rebased)):
|
|
191
|
+
x = rebased
|
|
192
|
+
else:
|
|
193
|
+
# Mixed float/integer coordinate records also use object cells.
|
|
194
|
+
# Their finite span can exceed float64 before rebasing finishes.
|
|
195
|
+
# Scale exact differences before converting them to float.
|
|
196
|
+
coordinate_scale = float(np.max(np.abs(x.astype(np.float64))))
|
|
197
|
+
anchor = Fraction(exact[0])
|
|
198
|
+
scale = Fraction(coordinate_scale)
|
|
199
|
+
x = np.fromiter(
|
|
200
|
+
(float((Fraction(value) - anchor) / scale) for value in exact),
|
|
201
|
+
dtype=np.float64, count=len(exact),
|
|
202
|
+
)
|
|
203
|
+
dx = np.diff(x)
|
|
204
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
205
|
+
span = float(x[-1] - x[0])
|
|
206
|
+
if not np.isfinite(span):
|
|
207
|
+
# A finite sweep may cross zero over more than float64's range.
|
|
208
|
+
# Interpolate on scaled coordinates and restore units in frequency.
|
|
209
|
+
coordinate_scale = float(np.max(np.abs(x)))
|
|
210
|
+
x = x / coordinate_scale
|
|
211
|
+
dx = np.diff(x)
|
|
212
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
213
|
+
spacing = float(x[-1] - x[0]) / (len(x) - 1)
|
|
214
|
+
if not (np.isfinite(spacing) and spacing > 0 and np.all(np.diff(x) > 0)):
|
|
215
|
+
raise _FFTCoordinatesError(message)
|
|
216
|
+
period = spacing * len(x)
|
|
217
|
+
frequency_step = ((1. / period) if np.isfinite(period)
|
|
218
|
+
else (1. / spacing) / len(x)) / coordinate_scale
|
|
219
|
+
# Finite acquired coordinates can imply an unrepresentable reciprocal
|
|
220
|
+
# axis. Reject before multiplying the bins (including 0 * infinity).
|
|
221
|
+
if (not np.isfinite(frequency_step) or frequency_step <= 0
|
|
222
|
+
or not np.isfinite(frequency_step * (len(x) // 2))):
|
|
223
|
+
raise _FFTCoordinatesError("FFT requires finite, representable frequencies.")
|
|
224
|
+
return x, dx, frequency_step, reverse
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def native_fft_coordinate_error(x: np.ndarray | None) -> str | None:
|
|
228
|
+
"""Validate populated traces; pending or empty curves have no spectrum."""
|
|
229
|
+
if x is None or len(x) == 0:
|
|
230
|
+
return None
|
|
231
|
+
try:
|
|
232
|
+
_prepare_fft_coordinates(x)
|
|
233
|
+
except _FFTCoordinatesError as error:
|
|
234
|
+
return str(error)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class NativePlotDataItem(pg.PlotDataItem):
|
|
239
|
+
"""Keep acquisition order while preparing native derivatives and FFTs."""
|
|
240
|
+
|
|
241
|
+
_datasetMapped: PlotDataset | None
|
|
242
|
+
_qplot_fft_error: str | None = None
|
|
243
|
+
|
|
244
|
+
def _fourierTransform(self, x, y):
|
|
245
|
+
"""Prepare increasing FFT coordinates without changing source data.
|
|
246
|
+
|
|
247
|
+
PyQtGraph's resampling assumes increasing coordinates. Reverse paired
|
|
248
|
+
samples only for strictly decreasing sweeps; sorting a reversing sweep
|
|
249
|
+
would merge different acquisition branches into a fictitious signal.
|
|
250
|
+
The window preflights all traces before enabling FFT. This defensive
|
|
251
|
+
empty mapping also keeps setData safe until its change signal lets the
|
|
252
|
+
window roll back FFT after a refresh introduces unsupported coordinates.
|
|
253
|
+
Mean subtraction has already run in the mapped dataset before this hook.
|
|
254
|
+
"""
|
|
255
|
+
self._qplot_fft_error = None
|
|
256
|
+
try:
|
|
257
|
+
x, dx, frequency_step, reverse = _prepare_fft_coordinates(x)
|
|
258
|
+
except _FFTCoordinatesError as error:
|
|
259
|
+
self._qplot_fft_error = str(error)
|
|
260
|
+
return np.array([], dtype=float), np.array([], dtype=float)
|
|
261
|
+
if len(x) == 1:
|
|
262
|
+
# abs(minimum signed integer) wraps in the recorded dtype.
|
|
263
|
+
# The FFT spectrum uses floating magnitudes, including its DC bin.
|
|
264
|
+
return np.array([0.]), np.abs(y.astype(np.float64))
|
|
265
|
+
if reverse:
|
|
266
|
+
y = y[::-1]
|
|
267
|
+
if y.dtype.kind == "f" and y.dtype.itemsize < 8:
|
|
268
|
+
# NumPy's FFT retains float32 precision. Widen before its internal
|
|
269
|
+
# unnormalized sum can overflow, not just before normalization.
|
|
270
|
+
y = y.astype(np.float64)
|
|
271
|
+
dc_offset = None
|
|
272
|
+
if y.dtype.kind in "iu":
|
|
273
|
+
dc_offset = Fraction(sum(map(int, y)), len(y))
|
|
274
|
+
y = _center_integer_samples(y)
|
|
275
|
+
elif y.dtype.kind == "O":
|
|
276
|
+
numeric = y.astype(np.float64)
|
|
277
|
+
if (np.all(np.isfinite(numeric))
|
|
278
|
+
and np.max(np.abs(numeric)) <= np.finfo(float).max / 2.):
|
|
279
|
+
# FFT converts integer/object arrays to float. Remove their
|
|
280
|
+
# exact offset first so adjacent recorded integers survive;
|
|
281
|
+
# restore only DC after any nonuniform interpolation.
|
|
282
|
+
exact = [Fraction(value.item() if isinstance(value, np.generic) else value)
|
|
283
|
+
for value in y]
|
|
284
|
+
dc_offset = sum(exact, Fraction()) / len(exact)
|
|
285
|
+
y = np.array([float(value - dc_offset) for value in exact])
|
|
286
|
+
else:
|
|
287
|
+
y = numeric
|
|
288
|
+
# Keep the ordinary interpolation semantics: nonuniform sweeps have
|
|
289
|
+
# the mean of the resampled signal, not the acquired sample mean.
|
|
290
|
+
resampled = np.any(np.abs(dx - dx[0]) > abs(dx[0]) / 1000)
|
|
291
|
+
float_dc = None
|
|
292
|
+
if dc_offset is None and np.all(np.isfinite(y)) and not resampled:
|
|
293
|
+
# FFT accumulation can erase a measured cancellation residual.
|
|
294
|
+
# Compute DC before amplitude scaling can round that residual.
|
|
295
|
+
float_dc = finite_mean(y)
|
|
296
|
+
float_anchor = None
|
|
297
|
+
if dc_offset is None and y.dtype.kind == "f" and np.all(np.isfinite(y)):
|
|
298
|
+
# A large floating DC offset can leak into small AC bins during
|
|
299
|
+
# FFT accumulation. Nearby same-sign values satisfy Sterbenz's
|
|
300
|
+
# lemma: subtracting this recorded anchor retains every source
|
|
301
|
+
# difference exactly, including subnormals. Wider signals keep
|
|
302
|
+
# their existing scaling path so anchoring cannot erase samples.
|
|
303
|
+
anchor = float(y[0])
|
|
304
|
+
absolute = np.abs(y)
|
|
305
|
+
if (anchor != 0. and np.all(np.signbit(y) == np.signbit(anchor))
|
|
306
|
+
and np.min(absolute) >= abs(anchor) / 2.
|
|
307
|
+
and np.max(absolute) / 2. <= abs(anchor)):
|
|
308
|
+
float_anchor = Fraction(anchor)
|
|
309
|
+
y = y - anchor
|
|
310
|
+
# Match PyQtGraph's uniformity tolerance and normalized real FFT.
|
|
311
|
+
if resampled:
|
|
312
|
+
uniform_x = np.linspace(x[0], x[-1], len(x))
|
|
313
|
+
y = _interpolate_fft_samples(uniform_x, x, y)
|
|
314
|
+
if dc_offset is None and np.all(np.isfinite(y)):
|
|
315
|
+
if float_anchor is None:
|
|
316
|
+
float_dc = finite_mean(y)
|
|
317
|
+
else:
|
|
318
|
+
# Resampling changes the centered mean. Restore its sign
|
|
319
|
+
# and the exact anchor before rounding the resulting DC.
|
|
320
|
+
centered_mean = sum((Fraction(float(value)) for value in y), Fraction()) / len(y)
|
|
321
|
+
float_dc = float(float_anchor + centered_mean)
|
|
322
|
+
magnitudes = np.abs(_normalized_real_fft(y))
|
|
323
|
+
if np.all(np.isfinite(y)):
|
|
324
|
+
# The normalized magnitude cannot exceed the largest source cell.
|
|
325
|
+
# Retain that bound when hypot's final rounding reaches overflow.
|
|
326
|
+
magnitudes = np.minimum(magnitudes, np.max(np.abs(y)))
|
|
327
|
+
if dc_offset is not None:
|
|
328
|
+
# Resampling may change the centered mean. Restore its signed
|
|
329
|
+
# value before taking the magnitude, retaining the exact offset.
|
|
330
|
+
if resampled:
|
|
331
|
+
resampled_mean = sum((Fraction(float(value)) for value in y), Fraction()) / len(y)
|
|
332
|
+
dc_offset += resampled_mean
|
|
333
|
+
magnitudes[0] = abs(float(dc_offset))
|
|
334
|
+
elif float_dc is not None:
|
|
335
|
+
magnitudes[0] = abs(float_dc)
|
|
336
|
+
frequencies = np.arange(len(magnitudes), dtype=float) * frequency_step
|
|
337
|
+
return frequencies, magnitudes
|
|
338
|
+
|
|
339
|
+
def _getDisplayDataset(self) -> PlotDataset | None:
|
|
340
|
+
# Isolate the private PyQtGraph mapping hook here. Seed only the mapped
|
|
341
|
+
# cache; upstream still owns log scaling, clipping, downsampling, curve
|
|
342
|
+
# updates and cache invalidation through its ordinary native setters.
|
|
343
|
+
source = self._dataset
|
|
344
|
+
if source is not None and self._datasetMapped is None:
|
|
345
|
+
if self.opts["derivativeMode"] or self.opts["phasemapMode"]:
|
|
346
|
+
# Native derivative/phase mapping uses original samples,
|
|
347
|
+
# overriding mean subtraction and (in phase mode) FFT. Phase
|
|
348
|
+
# mode replaces both coordinates, so FFT/log-X must not
|
|
349
|
+
# discard its first sample.
|
|
350
|
+
x = source.y[:-1] if self.opts["phasemapMode"] else source.x[:-1]
|
|
351
|
+
if self.opts["phasemapMode"] and x.dtype.kind == "O":
|
|
352
|
+
x = x.astype(np.float64)
|
|
353
|
+
y = _safe_secant(source.x, source.y)
|
|
354
|
+
elif self.opts["subtractMeanMode"] and source.y.dtype.kind in "iu":
|
|
355
|
+
x = source.x
|
|
356
|
+
y = _center_integer_samples(source.y)
|
|
357
|
+
if self.opts["fftMode"]:
|
|
358
|
+
x, y = self._fourierTransform(x, y)
|
|
359
|
+
if self.opts["logMode"][0]:
|
|
360
|
+
x, y = x[1:], y[1:]
|
|
361
|
+
elif self.opts["subtractMeanMode"] and source.y.dtype.kind == "f":
|
|
362
|
+
x = source.x
|
|
363
|
+
y = _center_float_samples(source.y)
|
|
364
|
+
if self.opts["fftMode"]:
|
|
365
|
+
x, y = self._fourierTransform(x, y)
|
|
366
|
+
if self.opts["logMode"][0]:
|
|
367
|
+
x, y = x[1:], y[1:]
|
|
368
|
+
elif source.y.dtype.kind == "O" or source.x.dtype.kind == "O":
|
|
369
|
+
# PyQtGraph's painter cannot call isfinite on object cells.
|
|
370
|
+
# Keep the original dataset exact for CSV/cursor access and
|
|
371
|
+
# map only the on-screen values to float.
|
|
372
|
+
x = source.x
|
|
373
|
+
y = source.y if self.opts["fftMode"] else source.y.astype(np.float64)
|
|
374
|
+
if self.opts["subtractMeanMode"]:
|
|
375
|
+
y = (_center_object_samples(source.y) if source.y.dtype.kind == "O"
|
|
376
|
+
else _center_float_samples(y))
|
|
377
|
+
if self.opts["fftMode"]:
|
|
378
|
+
x, y = self._fourierTransform(x, y)
|
|
379
|
+
if self.opts["logMode"][0]:
|
|
380
|
+
x, y = x[1:], y[1:]
|
|
381
|
+
else:
|
|
382
|
+
return super()._getDisplayDataset()
|
|
383
|
+
if x.dtype.kind == "O":
|
|
384
|
+
x = x.astype(np.float64)
|
|
385
|
+
mapped = PlotDataset(x, y)
|
|
386
|
+
if True in self.opts["logMode"]:
|
|
387
|
+
mapped.applyLogMapping(self.opts["logMode"])
|
|
388
|
+
self._datasetMapped = mapped
|
|
389
|
+
return super()._getDisplayDataset()
|