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.
Files changed (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. 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()