oscura 0.7.0__py3-none-any.whl → 0.10.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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +102 -17
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/spectral.py
DELETED
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
"""Spectral visualization functions.
|
|
2
|
-
|
|
3
|
-
This module provides spectrum and spectrogram plots for
|
|
4
|
-
frequency-domain analysis visualization.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.spectral import plot_spectrum, plot_spectrogram
|
|
9
|
-
>>> plot_spectrum(trace)
|
|
10
|
-
>>> plot_spectrogram(trace)
|
|
11
|
-
|
|
12
|
-
References:
|
|
13
|
-
matplotlib best practices for scientific visualization
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
20
|
-
|
|
21
|
-
import numpy as np
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
import matplotlib.pyplot as plt
|
|
25
|
-
from matplotlib.colors import Normalize # noqa: F401
|
|
26
|
-
|
|
27
|
-
HAS_MATPLOTLIB = True
|
|
28
|
-
except ImportError:
|
|
29
|
-
HAS_MATPLOTLIB = False
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
from matplotlib.axes import Axes
|
|
34
|
-
from matplotlib.figure import Figure
|
|
35
|
-
from numpy.typing import NDArray
|
|
36
|
-
|
|
37
|
-
from oscura.core.types import WaveformTrace
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _get_fft_data(
|
|
41
|
-
trace: WaveformTrace,
|
|
42
|
-
fft_result: tuple[Any, Any] | None,
|
|
43
|
-
window: str,
|
|
44
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
45
|
-
"""Get FFT data, either from cache or by computing.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
trace: Waveform trace.
|
|
49
|
-
fft_result: Pre-computed FFT result.
|
|
50
|
-
window: Window function name.
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Tuple of (frequencies, magnitudes_db).
|
|
54
|
-
"""
|
|
55
|
-
if fft_result is not None:
|
|
56
|
-
return fft_result
|
|
57
|
-
|
|
58
|
-
from oscura.analyzers.waveform.spectral import fft
|
|
59
|
-
|
|
60
|
-
return fft(trace, window=window) # type: ignore[return-value]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _scale_frequencies(
|
|
64
|
-
freq: NDArray[np.float64], freq_unit: str
|
|
65
|
-
) -> tuple[NDArray[np.float64], float, str]:
|
|
66
|
-
"""Scale frequencies to appropriate unit.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
freq: Frequency array in Hz.
|
|
70
|
-
freq_unit: Requested unit or "auto".
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Tuple of (scaled_frequencies, divisor, unit_name).
|
|
74
|
-
"""
|
|
75
|
-
if freq_unit == "auto":
|
|
76
|
-
max_freq = freq[-1]
|
|
77
|
-
freq_unit = _auto_select_freq_unit(max_freq)
|
|
78
|
-
|
|
79
|
-
freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
80
|
-
divisor = freq_divisors.get(freq_unit, 1.0)
|
|
81
|
-
return freq / divisor, divisor, freq_unit
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _set_auto_ylimits(ax: Axes, mag_db: NDArray[np.float64]) -> None:
|
|
85
|
-
"""Set reasonable y-axis limits based on data.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
ax: Matplotlib axes.
|
|
89
|
-
mag_db: Magnitude data in dB.
|
|
90
|
-
"""
|
|
91
|
-
valid_db = mag_db[np.isfinite(mag_db)]
|
|
92
|
-
if len(valid_db) == 0:
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
y_max = np.max(valid_db)
|
|
96
|
-
y_min = max(np.min(valid_db), y_max - 120) # Limit dynamic range
|
|
97
|
-
ax.set_ylim(y_min, y_max + 5)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _apply_axis_limits(
|
|
101
|
-
ax: Axes,
|
|
102
|
-
divisor: float,
|
|
103
|
-
freq_range: tuple[float, float] | None,
|
|
104
|
-
xlim: tuple[float, float] | None,
|
|
105
|
-
ylim: tuple[float, float] | None,
|
|
106
|
-
) -> None:
|
|
107
|
-
"""Apply custom axis limits if specified.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
ax: Matplotlib axes.
|
|
111
|
-
divisor: Frequency divisor for unit conversion.
|
|
112
|
-
freq_range: Frequency range in Hz (will be converted to display units).
|
|
113
|
-
xlim: X-axis limits in display units.
|
|
114
|
-
ylim: Y-axis limits.
|
|
115
|
-
"""
|
|
116
|
-
if freq_range is not None and len(freq_range) == 2:
|
|
117
|
-
# freq_range is in Hz, convert to display units
|
|
118
|
-
freq_min = freq_range[0] / divisor
|
|
119
|
-
freq_max = freq_range[1] / divisor
|
|
120
|
-
|
|
121
|
-
# For log scale, ensure minimum is positive (avoid 0 on log axis)
|
|
122
|
-
if ax.get_xscale() == "log" and freq_min <= 0:
|
|
123
|
-
freq_min = freq_max / 1000 # Use a small positive value
|
|
124
|
-
|
|
125
|
-
ax.set_xlim(freq_min, freq_max)
|
|
126
|
-
elif xlim is not None:
|
|
127
|
-
ax.set_xlim(xlim)
|
|
128
|
-
|
|
129
|
-
if ylim is not None:
|
|
130
|
-
ax.set_ylim(ylim)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _prepare_spectrum_data(
|
|
134
|
-
trace: WaveformTrace,
|
|
135
|
-
fft_result: tuple[Any, Any] | None,
|
|
136
|
-
window: str,
|
|
137
|
-
db_ref: float | None,
|
|
138
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
139
|
-
"""Prepare spectrum data with FFT and dB scaling.
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
trace: Waveform trace to analyze.
|
|
143
|
-
fft_result: Pre-computed FFT result or None.
|
|
144
|
-
window: Window function name.
|
|
145
|
-
db_ref: Reference for dB scaling or None.
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
Tuple of (frequencies, magnitudes_db).
|
|
149
|
-
"""
|
|
150
|
-
freq, mag_db = _get_fft_data(trace, fft_result, window)
|
|
151
|
-
|
|
152
|
-
# Adjust dB reference if specified
|
|
153
|
-
if db_ref is not None:
|
|
154
|
-
mag_db = mag_db - db_ref
|
|
155
|
-
|
|
156
|
-
return freq, mag_db
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _render_spectrum_plot(
|
|
160
|
-
ax: Axes,
|
|
161
|
-
freq_scaled: NDArray[np.float64],
|
|
162
|
-
mag_db: NDArray[np.float64],
|
|
163
|
-
freq_unit: str,
|
|
164
|
-
color: str,
|
|
165
|
-
title: str | None,
|
|
166
|
-
log_scale: bool,
|
|
167
|
-
show_grid: bool,
|
|
168
|
-
) -> None:
|
|
169
|
-
"""Render spectrum plot on axes.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
ax: Matplotlib axes to plot on.
|
|
173
|
-
freq_scaled: Scaled frequency array.
|
|
174
|
-
mag_db: Magnitude array in dB.
|
|
175
|
-
freq_unit: Frequency unit string.
|
|
176
|
-
color: Line color.
|
|
177
|
-
title: Plot title.
|
|
178
|
-
log_scale: Use logarithmic frequency scale.
|
|
179
|
-
show_grid: Show grid lines.
|
|
180
|
-
"""
|
|
181
|
-
ax.plot(freq_scaled, mag_db, color=color, linewidth=0.8)
|
|
182
|
-
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
183
|
-
ax.set_ylabel("Magnitude (dB)")
|
|
184
|
-
ax.set_xscale("log" if log_scale else "linear")
|
|
185
|
-
ax.set_title(title if title else "Magnitude Spectrum")
|
|
186
|
-
|
|
187
|
-
if show_grid:
|
|
188
|
-
ax.grid(True, alpha=0.3, which="both")
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def plot_spectrum(
|
|
192
|
-
trace: WaveformTrace,
|
|
193
|
-
*,
|
|
194
|
-
ax: Axes | None = None,
|
|
195
|
-
freq_unit: str = "auto",
|
|
196
|
-
db_ref: float | None = None,
|
|
197
|
-
freq_range: tuple[float, float] | None = None,
|
|
198
|
-
show_grid: bool = True,
|
|
199
|
-
color: str = "C0",
|
|
200
|
-
title: str | None = None,
|
|
201
|
-
window: str = "hann",
|
|
202
|
-
show: bool = True,
|
|
203
|
-
save_path: str | None = None,
|
|
204
|
-
figsize: tuple[float, float] = (10, 6),
|
|
205
|
-
xlim: tuple[float, float] | None = None,
|
|
206
|
-
ylim: tuple[float, float] | None = None,
|
|
207
|
-
fft_result: tuple[Any, Any] | None = None,
|
|
208
|
-
log_scale: bool = True,
|
|
209
|
-
) -> Figure:
|
|
210
|
-
"""Plot magnitude spectrum.
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
trace: Waveform trace to analyze.
|
|
214
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
215
|
-
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
|
|
216
|
-
db_ref: Reference for dB scaling. If None, uses max value.
|
|
217
|
-
freq_range: Frequency range (min, max) in Hz to display.
|
|
218
|
-
show_grid: Show grid lines.
|
|
219
|
-
color: Line color.
|
|
220
|
-
title: Plot title.
|
|
221
|
-
window: Window function for FFT.
|
|
222
|
-
show: If True, call plt.show() to display the plot.
|
|
223
|
-
save_path: Path to save the figure. If None, figure is not saved.
|
|
224
|
-
figsize: Figure size (width, height) in inches. Only used if ax is None.
|
|
225
|
-
xlim: X-axis limits (min, max) in selected frequency units.
|
|
226
|
-
ylim: Y-axis limits (min, max) in dB.
|
|
227
|
-
fft_result: Pre-computed FFT result (frequencies, magnitudes). If None, computes FFT.
|
|
228
|
-
log_scale: Use logarithmic scale for frequency axis (default True).
|
|
229
|
-
|
|
230
|
-
Returns:
|
|
231
|
-
Matplotlib Figure object.
|
|
232
|
-
|
|
233
|
-
Raises:
|
|
234
|
-
ImportError: If matplotlib is not installed.
|
|
235
|
-
ValueError: If axes must have an associated figure.
|
|
236
|
-
|
|
237
|
-
Example:
|
|
238
|
-
>>> import oscura as osc
|
|
239
|
-
>>> trace = osc.load("signal.wfm")
|
|
240
|
-
>>> fig = osc.plot_spectrum(trace, freq_unit="MHz", log_scale=True)
|
|
241
|
-
|
|
242
|
-
>>> # With pre-computed FFT
|
|
243
|
-
>>> freq, mag = osc.fft(trace)
|
|
244
|
-
>>> fig = osc.plot_spectrum(trace, fft_result=(freq, mag), show=False)
|
|
245
|
-
>>> fig.savefig("spectrum.png")
|
|
246
|
-
"""
|
|
247
|
-
if not HAS_MATPLOTLIB:
|
|
248
|
-
raise ImportError("matplotlib is required for visualization")
|
|
249
|
-
|
|
250
|
-
# Figure/axes creation
|
|
251
|
-
if ax is None:
|
|
252
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
253
|
-
else:
|
|
254
|
-
fig_temp = ax.get_figure()
|
|
255
|
-
if fig_temp is None:
|
|
256
|
-
raise ValueError("Axes must have an associated figure")
|
|
257
|
-
fig = cast("Figure", fig_temp)
|
|
258
|
-
|
|
259
|
-
# Data preparation
|
|
260
|
-
freq, mag_db = _prepare_spectrum_data(trace, fft_result, window, db_ref)
|
|
261
|
-
|
|
262
|
-
# Unit/scale selection
|
|
263
|
-
freq_scaled, divisor, freq_unit = _scale_frequencies(freq, freq_unit)
|
|
264
|
-
|
|
265
|
-
# Plotting/rendering
|
|
266
|
-
_render_spectrum_plot(ax, freq_scaled, mag_db, freq_unit, color, title, log_scale, show_grid)
|
|
267
|
-
|
|
268
|
-
# Set limits
|
|
269
|
-
_set_auto_ylimits(ax, mag_db)
|
|
270
|
-
_apply_axis_limits(ax, divisor, freq_range, xlim, ylim)
|
|
271
|
-
|
|
272
|
-
# Layout/formatting
|
|
273
|
-
fig.tight_layout()
|
|
274
|
-
|
|
275
|
-
if save_path is not None:
|
|
276
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
277
|
-
|
|
278
|
-
if show:
|
|
279
|
-
plt.show()
|
|
280
|
-
|
|
281
|
-
return fig
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def _auto_select_time_unit(max_time: float) -> str:
|
|
285
|
-
"""Select appropriate time unit based on maximum time value.
|
|
286
|
-
|
|
287
|
-
Args:
|
|
288
|
-
max_time: Maximum time value in seconds.
|
|
289
|
-
|
|
290
|
-
Returns:
|
|
291
|
-
Time unit string ("s", "ms", "us", or "ns").
|
|
292
|
-
"""
|
|
293
|
-
if max_time < 1e-6:
|
|
294
|
-
return "ns"
|
|
295
|
-
elif max_time < 1e-3:
|
|
296
|
-
return "us"
|
|
297
|
-
elif max_time < 1:
|
|
298
|
-
return "ms"
|
|
299
|
-
else:
|
|
300
|
-
return "s"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def _auto_select_freq_unit(max_freq: float) -> str:
|
|
304
|
-
"""Select appropriate frequency unit based on maximum frequency.
|
|
305
|
-
|
|
306
|
-
Args:
|
|
307
|
-
max_freq: Maximum frequency in Hz.
|
|
308
|
-
|
|
309
|
-
Returns:
|
|
310
|
-
Frequency unit string ("Hz", "kHz", "MHz", or "GHz").
|
|
311
|
-
"""
|
|
312
|
-
if max_freq >= 1e9:
|
|
313
|
-
return "GHz"
|
|
314
|
-
elif max_freq >= 1e6:
|
|
315
|
-
return "MHz"
|
|
316
|
-
elif max_freq >= 1e3:
|
|
317
|
-
return "kHz"
|
|
318
|
-
else:
|
|
319
|
-
return "Hz"
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _get_unit_multipliers() -> tuple[dict[str, float], dict[str, float]]:
|
|
323
|
-
"""Get time and frequency unit multipliers.
|
|
324
|
-
|
|
325
|
-
Returns:
|
|
326
|
-
Tuple of (time_multipliers, freq_divisors).
|
|
327
|
-
"""
|
|
328
|
-
time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
329
|
-
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
330
|
-
return time_mult, freq_div
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
def _auto_color_limits(
|
|
334
|
-
data: NDArray[np.float64], vmin: float | None, vmax: float | None
|
|
335
|
-
) -> tuple[float | None, float | None]:
|
|
336
|
-
"""Automatically determine color limits for spectrogram.
|
|
337
|
-
|
|
338
|
-
Args:
|
|
339
|
-
data: Spectrogram data in dB.
|
|
340
|
-
vmin: Minimum dB value (if None, auto-computed).
|
|
341
|
-
vmax: Maximum dB value (if None, auto-computed).
|
|
342
|
-
|
|
343
|
-
Returns:
|
|
344
|
-
Tuple of (vmin, vmax).
|
|
345
|
-
"""
|
|
346
|
-
if vmin is not None and vmax is not None:
|
|
347
|
-
return vmin, vmax
|
|
348
|
-
|
|
349
|
-
valid_db = data[np.isfinite(data)]
|
|
350
|
-
if len(valid_db) == 0:
|
|
351
|
-
return vmin, vmax
|
|
352
|
-
|
|
353
|
-
if vmax is None:
|
|
354
|
-
vmax = np.max(valid_db)
|
|
355
|
-
if vmin is None:
|
|
356
|
-
vmin = max(np.min(valid_db), vmax - 80)
|
|
357
|
-
|
|
358
|
-
return vmin, vmax
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _compute_spectrogram_data(
|
|
362
|
-
trace: WaveformTrace,
|
|
363
|
-
window: str,
|
|
364
|
-
nperseg: int | None,
|
|
365
|
-
nfft: int | None,
|
|
366
|
-
overlap: float | None,
|
|
367
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
368
|
-
"""Compute spectrogram data using STFT.
|
|
369
|
-
|
|
370
|
-
Args:
|
|
371
|
-
trace: Waveform trace to analyze.
|
|
372
|
-
window: Window function name.
|
|
373
|
-
nperseg: Segment length for STFT.
|
|
374
|
-
nfft: FFT length (overrides nperseg if specified).
|
|
375
|
-
overlap: Overlap fraction (0.0 to 1.0).
|
|
376
|
-
|
|
377
|
-
Returns:
|
|
378
|
-
Tuple of (times, frequencies, Sxx_db).
|
|
379
|
-
"""
|
|
380
|
-
from oscura.analyzers.waveform.spectral import spectrogram
|
|
381
|
-
|
|
382
|
-
if nfft is not None:
|
|
383
|
-
nperseg = nfft
|
|
384
|
-
noverlap = int(nperseg * overlap) if overlap is not None and nperseg is not None else None
|
|
385
|
-
|
|
386
|
-
return spectrogram(trace, window=window, nperseg=nperseg, noverlap=noverlap)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def _scale_spectrogram_axes(
|
|
390
|
-
times: NDArray[np.float64],
|
|
391
|
-
freq: NDArray[np.float64],
|
|
392
|
-
time_unit: str,
|
|
393
|
-
freq_unit: str,
|
|
394
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64], str, str]:
|
|
395
|
-
"""Scale time and frequency axes to appropriate units.
|
|
396
|
-
|
|
397
|
-
Args:
|
|
398
|
-
times: Time array in seconds.
|
|
399
|
-
freq: Frequency array in Hz.
|
|
400
|
-
time_unit: Time unit ("auto" or specific).
|
|
401
|
-
freq_unit: Frequency unit ("auto" or specific).
|
|
402
|
-
|
|
403
|
-
Returns:
|
|
404
|
-
Tuple of (times_scaled, freq_scaled, time_unit, freq_unit).
|
|
405
|
-
"""
|
|
406
|
-
if time_unit == "auto":
|
|
407
|
-
max_time = times[-1] if len(times) > 0 else 0
|
|
408
|
-
time_unit = _auto_select_time_unit(max_time)
|
|
409
|
-
|
|
410
|
-
if freq_unit == "auto":
|
|
411
|
-
max_freq = freq[-1] if len(freq) > 0 else 0
|
|
412
|
-
freq_unit = _auto_select_freq_unit(max_freq)
|
|
413
|
-
|
|
414
|
-
time_multipliers, freq_divisors = _get_unit_multipliers()
|
|
415
|
-
times_scaled = times * time_multipliers.get(time_unit, 1.0)
|
|
416
|
-
freq_scaled = freq / freq_divisors.get(freq_unit, 1.0)
|
|
417
|
-
|
|
418
|
-
return times_scaled, freq_scaled, time_unit, freq_unit
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
def _render_spectrogram_plot(
|
|
422
|
-
ax: Axes,
|
|
423
|
-
times_scaled: NDArray[np.float64],
|
|
424
|
-
freq_scaled: NDArray[np.float64],
|
|
425
|
-
Sxx_db: NDArray[np.float64],
|
|
426
|
-
time_unit: str,
|
|
427
|
-
freq_unit: str,
|
|
428
|
-
cmap: str,
|
|
429
|
-
vmin: float | None,
|
|
430
|
-
vmax: float | None,
|
|
431
|
-
title: str | None,
|
|
432
|
-
) -> None:
|
|
433
|
-
"""Render spectrogram plot on axes.
|
|
434
|
-
|
|
435
|
-
Args:
|
|
436
|
-
ax: Matplotlib axes to plot on.
|
|
437
|
-
times_scaled: Scaled time array.
|
|
438
|
-
freq_scaled: Scaled frequency array.
|
|
439
|
-
Sxx_db: Spectrogram data in dB.
|
|
440
|
-
time_unit: Time unit string.
|
|
441
|
-
freq_unit: Frequency unit string.
|
|
442
|
-
cmap: Colormap name.
|
|
443
|
-
vmin: Minimum dB value for color scaling.
|
|
444
|
-
vmax: Maximum dB value for color scaling.
|
|
445
|
-
title: Plot title.
|
|
446
|
-
"""
|
|
447
|
-
# Auto color limits
|
|
448
|
-
vmin, vmax = _auto_color_limits(Sxx_db, vmin, vmax)
|
|
449
|
-
|
|
450
|
-
# Plot
|
|
451
|
-
pcm = ax.pcolormesh(
|
|
452
|
-
times_scaled,
|
|
453
|
-
freq_scaled,
|
|
454
|
-
Sxx_db,
|
|
455
|
-
shading="auto",
|
|
456
|
-
cmap=cmap,
|
|
457
|
-
vmin=vmin,
|
|
458
|
-
vmax=vmax,
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
ax.set_xlabel(f"Time ({time_unit})")
|
|
462
|
-
ax.set_ylabel(f"Frequency ({freq_unit})")
|
|
463
|
-
ax.set_title(title if title else "Spectrogram")
|
|
464
|
-
|
|
465
|
-
# Colorbar
|
|
466
|
-
fig = ax.get_figure()
|
|
467
|
-
if fig is not None:
|
|
468
|
-
cbar = fig.colorbar(pcm, ax=ax)
|
|
469
|
-
cbar.set_label("Magnitude (dB)")
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
def plot_spectrogram(
|
|
473
|
-
trace: WaveformTrace,
|
|
474
|
-
*,
|
|
475
|
-
ax: Axes | None = None,
|
|
476
|
-
time_unit: str = "auto",
|
|
477
|
-
freq_unit: str = "auto",
|
|
478
|
-
cmap: str = "viridis",
|
|
479
|
-
vmin: float | None = None,
|
|
480
|
-
vmax: float | None = None,
|
|
481
|
-
title: str | None = None,
|
|
482
|
-
window: str = "hann",
|
|
483
|
-
nperseg: int | None = None,
|
|
484
|
-
nfft: int | None = None,
|
|
485
|
-
overlap: float | None = None,
|
|
486
|
-
) -> Figure:
|
|
487
|
-
"""Plot spectrogram (time-frequency representation).
|
|
488
|
-
|
|
489
|
-
Args:
|
|
490
|
-
trace: Waveform trace to analyze.
|
|
491
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
492
|
-
time_unit: Time unit ("s", "ms", "us", "auto").
|
|
493
|
-
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
|
|
494
|
-
cmap: Colormap name.
|
|
495
|
-
vmin: Minimum dB value for color scaling.
|
|
496
|
-
vmax: Maximum dB value for color scaling.
|
|
497
|
-
title: Plot title.
|
|
498
|
-
window: Window function.
|
|
499
|
-
nperseg: Segment length for STFT.
|
|
500
|
-
nfft: FFT length. If specified, overrides nperseg.
|
|
501
|
-
overlap: Overlap fraction (0.0 to 1.0). Default is 0.5 (50%).
|
|
502
|
-
|
|
503
|
-
Returns:
|
|
504
|
-
Matplotlib Figure object.
|
|
505
|
-
|
|
506
|
-
Raises:
|
|
507
|
-
ImportError: If matplotlib is not installed.
|
|
508
|
-
ValueError: If axes must have an associated figure.
|
|
509
|
-
|
|
510
|
-
Example:
|
|
511
|
-
>>> fig = plot_spectrogram(trace)
|
|
512
|
-
>>> plt.show()
|
|
513
|
-
"""
|
|
514
|
-
if not HAS_MATPLOTLIB:
|
|
515
|
-
raise ImportError("matplotlib is required for visualization")
|
|
516
|
-
|
|
517
|
-
# Figure/axes creation
|
|
518
|
-
if ax is None:
|
|
519
|
-
fig, ax = plt.subplots(figsize=(10, 4))
|
|
520
|
-
else:
|
|
521
|
-
fig_temp = ax.get_figure()
|
|
522
|
-
if fig_temp is None:
|
|
523
|
-
raise ValueError("Axes must have an associated figure")
|
|
524
|
-
fig = cast("Figure", fig_temp)
|
|
525
|
-
|
|
526
|
-
# Data preparation/computation
|
|
527
|
-
times, freq, Sxx_db = _compute_spectrogram_data(trace, window, nperseg, nfft, overlap)
|
|
528
|
-
|
|
529
|
-
# Unit/scale selection
|
|
530
|
-
times_scaled, freq_scaled, time_unit, freq_unit = _scale_spectrogram_axes(
|
|
531
|
-
times, freq, time_unit, freq_unit
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
# Plotting/rendering
|
|
535
|
-
_render_spectrogram_plot(
|
|
536
|
-
ax, times_scaled, freq_scaled, Sxx_db, time_unit, freq_unit, cmap, vmin, vmax, title
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
# Layout/formatting
|
|
540
|
-
fig.tight_layout()
|
|
541
|
-
return fig
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def plot_psd(
|
|
545
|
-
trace: WaveformTrace,
|
|
546
|
-
*,
|
|
547
|
-
ax: Axes | None = None,
|
|
548
|
-
freq_unit: str = "auto",
|
|
549
|
-
show_grid: bool = True,
|
|
550
|
-
color: str = "C0",
|
|
551
|
-
title: str | None = None,
|
|
552
|
-
window: str = "hann",
|
|
553
|
-
log_scale: bool = True,
|
|
554
|
-
) -> Figure:
|
|
555
|
-
"""Plot Power Spectral Density.
|
|
556
|
-
|
|
557
|
-
Args:
|
|
558
|
-
trace: Waveform trace to analyze.
|
|
559
|
-
ax: Matplotlib axes.
|
|
560
|
-
freq_unit: Frequency unit.
|
|
561
|
-
show_grid: Show grid lines.
|
|
562
|
-
color: Line color.
|
|
563
|
-
title: Plot title.
|
|
564
|
-
window: Window function.
|
|
565
|
-
log_scale: Use logarithmic scale for frequency axis (default True).
|
|
566
|
-
|
|
567
|
-
Returns:
|
|
568
|
-
Matplotlib Figure object.
|
|
569
|
-
|
|
570
|
-
Raises:
|
|
571
|
-
ImportError: If matplotlib is not installed.
|
|
572
|
-
ValueError: If axes must have an associated figure.
|
|
573
|
-
|
|
574
|
-
Example:
|
|
575
|
-
>>> fig = plot_psd(trace)
|
|
576
|
-
>>> plt.show()
|
|
577
|
-
"""
|
|
578
|
-
if not HAS_MATPLOTLIB:
|
|
579
|
-
raise ImportError("matplotlib is required for visualization")
|
|
580
|
-
|
|
581
|
-
from oscura.analyzers.waveform.spectral import psd
|
|
582
|
-
|
|
583
|
-
if ax is None:
|
|
584
|
-
fig, ax = plt.subplots(figsize=(10, 4))
|
|
585
|
-
else:
|
|
586
|
-
fig_temp = ax.get_figure()
|
|
587
|
-
if fig_temp is None:
|
|
588
|
-
raise ValueError("Axes must have an associated figure")
|
|
589
|
-
fig = cast("Figure", fig_temp)
|
|
590
|
-
|
|
591
|
-
# Compute PSD
|
|
592
|
-
freq, psd_db = psd(trace, window=window)
|
|
593
|
-
|
|
594
|
-
# Auto-select frequency unit
|
|
595
|
-
if freq_unit == "auto":
|
|
596
|
-
max_freq = freq[-1]
|
|
597
|
-
if max_freq >= 1e9:
|
|
598
|
-
freq_unit = "GHz"
|
|
599
|
-
elif max_freq >= 1e6:
|
|
600
|
-
freq_unit = "MHz"
|
|
601
|
-
elif max_freq >= 1e3:
|
|
602
|
-
freq_unit = "kHz"
|
|
603
|
-
else:
|
|
604
|
-
freq_unit = "Hz"
|
|
605
|
-
|
|
606
|
-
freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
607
|
-
divisor = freq_divisors.get(freq_unit, 1.0)
|
|
608
|
-
freq_scaled = freq / divisor
|
|
609
|
-
|
|
610
|
-
# Plot
|
|
611
|
-
ax.plot(freq_scaled, psd_db, color=color, linewidth=0.8)
|
|
612
|
-
|
|
613
|
-
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
614
|
-
ax.set_ylabel("PSD (dB/Hz)")
|
|
615
|
-
ax.set_xscale("log" if log_scale else "linear")
|
|
616
|
-
|
|
617
|
-
if title:
|
|
618
|
-
ax.set_title(title)
|
|
619
|
-
else:
|
|
620
|
-
ax.set_title("Power Spectral Density")
|
|
621
|
-
|
|
622
|
-
if show_grid:
|
|
623
|
-
ax.grid(True, alpha=0.3, which="both")
|
|
624
|
-
|
|
625
|
-
fig.tight_layout()
|
|
626
|
-
return fig
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
def plot_fft(
|
|
630
|
-
trace: WaveformTrace,
|
|
631
|
-
*,
|
|
632
|
-
ax: Axes | None = None,
|
|
633
|
-
show: bool = True,
|
|
634
|
-
save_path: str | None = None,
|
|
635
|
-
title: str | None = None,
|
|
636
|
-
xlabel: str = "Frequency",
|
|
637
|
-
ylabel: str = "Magnitude (dB)",
|
|
638
|
-
figsize: tuple[float, float] = (10, 6),
|
|
639
|
-
freq_unit: str = "auto",
|
|
640
|
-
log_scale: bool = True,
|
|
641
|
-
show_grid: bool = True,
|
|
642
|
-
color: str = "C0",
|
|
643
|
-
window: str = "hann",
|
|
644
|
-
xlim: tuple[float, float] | None = None,
|
|
645
|
-
ylim: tuple[float, float] | None = None,
|
|
646
|
-
) -> Figure:
|
|
647
|
-
"""Plot FFT magnitude spectrum.
|
|
648
|
-
|
|
649
|
-
Computes and plots the FFT magnitude spectrum of a waveform trace.
|
|
650
|
-
This is a convenience function that combines FFT computation and visualization.
|
|
651
|
-
|
|
652
|
-
Args:
|
|
653
|
-
trace: Waveform trace to analyze and plot.
|
|
654
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
655
|
-
show: If True, call plt.show() to display the plot.
|
|
656
|
-
save_path: Path to save the figure. If None, figure is not saved.
|
|
657
|
-
title: Plot title. If None, uses default "FFT Magnitude Spectrum".
|
|
658
|
-
xlabel: X-axis label (appended with frequency unit).
|
|
659
|
-
ylabel: Y-axis label.
|
|
660
|
-
figsize: Figure size (width, height) in inches. Only used if ax is None.
|
|
661
|
-
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "GHz", "auto").
|
|
662
|
-
log_scale: Use logarithmic scale for frequency axis.
|
|
663
|
-
show_grid: Show grid lines.
|
|
664
|
-
color: Line color.
|
|
665
|
-
window: Window function for FFT computation.
|
|
666
|
-
xlim: X-axis limits (min, max) in selected frequency units.
|
|
667
|
-
ylim: Y-axis limits (min, max) in dB.
|
|
668
|
-
|
|
669
|
-
Returns:
|
|
670
|
-
Matplotlib Figure object.
|
|
671
|
-
|
|
672
|
-
Raises:
|
|
673
|
-
ImportError: If matplotlib is not installed.
|
|
674
|
-
ValueError: If axes must have an associated figure.
|
|
675
|
-
|
|
676
|
-
Example:
|
|
677
|
-
>>> import oscura as osc
|
|
678
|
-
>>> trace = osc.load("signal.wfm")
|
|
679
|
-
>>> fig = osc.plot_fft(trace, freq_unit="MHz", show=False)
|
|
680
|
-
>>> fig.savefig("spectrum.png")
|
|
681
|
-
|
|
682
|
-
>>> # With custom styling
|
|
683
|
-
>>> fig = osc.plot_fft(trace,
|
|
684
|
-
... title="Signal FFT",
|
|
685
|
-
... log_scale=True,
|
|
686
|
-
... xlim=(1e3, 1e6),
|
|
687
|
-
... ylim=(-100, 0))
|
|
688
|
-
|
|
689
|
-
References:
|
|
690
|
-
IEEE 1241-2010: Standard for Terminology and Test Methods for
|
|
691
|
-
Analog-to-Digital Converters
|
|
692
|
-
"""
|
|
693
|
-
if not HAS_MATPLOTLIB:
|
|
694
|
-
raise ImportError("matplotlib is required for visualization")
|
|
695
|
-
|
|
696
|
-
# Setup figure and axes
|
|
697
|
-
fig, ax = _setup_plot_figure(ax, figsize)
|
|
698
|
-
|
|
699
|
-
# Plot spectrum using main plotting function
|
|
700
|
-
plot_spectrum(
|
|
701
|
-
trace,
|
|
702
|
-
ax=ax,
|
|
703
|
-
freq_unit=freq_unit,
|
|
704
|
-
show_grid=show_grid,
|
|
705
|
-
color=color,
|
|
706
|
-
title=title if title else "FFT Magnitude Spectrum",
|
|
707
|
-
window=window,
|
|
708
|
-
log_scale=log_scale,
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
# Apply custom labels and limits
|
|
712
|
-
_apply_custom_labels(ax, xlabel, ylabel)
|
|
713
|
-
_apply_axis_limits_simple(ax, xlim, ylim)
|
|
714
|
-
|
|
715
|
-
# Output handling
|
|
716
|
-
_handle_plot_output(fig, save_path, show)
|
|
717
|
-
|
|
718
|
-
return fig
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
def _setup_plot_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
722
|
-
"""Setup figure and axes for plotting.
|
|
723
|
-
|
|
724
|
-
Args:
|
|
725
|
-
ax: Existing axes or None.
|
|
726
|
-
figsize: Figure size if creating new.
|
|
727
|
-
|
|
728
|
-
Returns:
|
|
729
|
-
Tuple of (Figure, Axes).
|
|
730
|
-
"""
|
|
731
|
-
if ax is None:
|
|
732
|
-
return plt.subplots(figsize=figsize)
|
|
733
|
-
|
|
734
|
-
fig_temp = ax.get_figure()
|
|
735
|
-
if fig_temp is None:
|
|
736
|
-
raise ValueError("Axes must have an associated figure")
|
|
737
|
-
return cast("Figure", fig_temp), ax
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
def _apply_custom_labels(ax: Axes, xlabel: str, ylabel: str) -> None:
|
|
741
|
-
"""Apply custom labels to plot axes.
|
|
742
|
-
|
|
743
|
-
Args:
|
|
744
|
-
ax: Matplotlib axes.
|
|
745
|
-
xlabel: X-axis label.
|
|
746
|
-
ylabel: Y-axis label.
|
|
747
|
-
"""
|
|
748
|
-
if xlabel != "Frequency":
|
|
749
|
-
current_label = ax.get_xlabel()
|
|
750
|
-
if "(" in current_label and ")" in current_label:
|
|
751
|
-
unit = current_label[current_label.find("(") : current_label.find(")") + 1]
|
|
752
|
-
ax.set_xlabel(f"{xlabel} {unit}")
|
|
753
|
-
else:
|
|
754
|
-
ax.set_xlabel(xlabel)
|
|
755
|
-
|
|
756
|
-
if ylabel != "Magnitude (dB)":
|
|
757
|
-
ax.set_ylabel(ylabel)
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def _apply_axis_limits_simple(
|
|
761
|
-
ax: Axes, xlim: tuple[float, float] | None, ylim: tuple[float, float] | None
|
|
762
|
-
) -> None:
|
|
763
|
-
"""Apply axis limits if specified.
|
|
764
|
-
|
|
765
|
-
Args:
|
|
766
|
-
ax: Matplotlib axes.
|
|
767
|
-
xlim: X-axis limits.
|
|
768
|
-
ylim: Y-axis limits.
|
|
769
|
-
"""
|
|
770
|
-
if xlim is not None:
|
|
771
|
-
ax.set_xlim(xlim)
|
|
772
|
-
if ylim is not None:
|
|
773
|
-
ax.set_ylim(ylim)
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
def _handle_plot_output(fig: Figure, save_path: str | None, show: bool) -> None:
|
|
777
|
-
"""Handle plot output (save and/or show).
|
|
778
|
-
|
|
779
|
-
Args:
|
|
780
|
-
fig: Matplotlib figure.
|
|
781
|
-
save_path: Path to save figure.
|
|
782
|
-
show: Whether to display plot.
|
|
783
|
-
"""
|
|
784
|
-
if save_path is not None:
|
|
785
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
786
|
-
if show:
|
|
787
|
-
plt.show()
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
def _create_harmonic_labels(
|
|
791
|
-
n_harmonics: int,
|
|
792
|
-
fundamental_freq: float | None,
|
|
793
|
-
) -> list[str]:
|
|
794
|
-
"""Create x-axis labels for harmonics.
|
|
795
|
-
|
|
796
|
-
Args:
|
|
797
|
-
n_harmonics: Number of harmonics.
|
|
798
|
-
fundamental_freq: Fundamental frequency in Hz or None.
|
|
799
|
-
|
|
800
|
-
Returns:
|
|
801
|
-
List of label strings.
|
|
802
|
-
"""
|
|
803
|
-
if fundamental_freq is not None:
|
|
804
|
-
labels = [
|
|
805
|
-
f"H{i + 1}\n({(i + 1) * fundamental_freq / 1e3:.1f} kHz)"
|
|
806
|
-
if fundamental_freq >= 1000
|
|
807
|
-
else f"H{i + 1}\n({(i + 1) * fundamental_freq:.0f} Hz)"
|
|
808
|
-
for i in range(n_harmonics)
|
|
809
|
-
]
|
|
810
|
-
labels[0] = (
|
|
811
|
-
f"Fund\n({fundamental_freq / 1e3:.1f} kHz)"
|
|
812
|
-
if fundamental_freq >= 1000
|
|
813
|
-
else f"Fund\n({fundamental_freq:.0f} Hz)"
|
|
814
|
-
)
|
|
815
|
-
else:
|
|
816
|
-
labels = [f"H{i + 1}" for i in range(n_harmonics)]
|
|
817
|
-
labels[0] = "Fund"
|
|
818
|
-
|
|
819
|
-
return labels
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
def _assign_harmonic_colors(
|
|
823
|
-
harmonic_magnitudes: NDArray[np.floating[Any]],
|
|
824
|
-
) -> list[str]:
|
|
825
|
-
"""Assign colors to harmonics based on magnitude.
|
|
826
|
-
|
|
827
|
-
Args:
|
|
828
|
-
harmonic_magnitudes: Array of harmonic magnitudes in dB.
|
|
829
|
-
|
|
830
|
-
Returns:
|
|
831
|
-
List of color strings.
|
|
832
|
-
"""
|
|
833
|
-
colors = []
|
|
834
|
-
for i, mag in enumerate(harmonic_magnitudes):
|
|
835
|
-
if i == 0:
|
|
836
|
-
colors.append("#3498DB") # Blue for fundamental
|
|
837
|
-
elif mag > -30:
|
|
838
|
-
colors.append("#E74C3C") # Red for significant harmonics
|
|
839
|
-
elif mag > -50:
|
|
840
|
-
colors.append("#F39C12") # Orange for moderate
|
|
841
|
-
else:
|
|
842
|
-
colors.append("#95A5A6") # Gray for low
|
|
843
|
-
|
|
844
|
-
return colors
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _add_thd_annotation(
|
|
848
|
-
ax: Axes,
|
|
849
|
-
thd_value: float | None,
|
|
850
|
-
show_thd: bool,
|
|
851
|
-
) -> None:
|
|
852
|
-
"""Add THD annotation to plot.
|
|
853
|
-
|
|
854
|
-
Args:
|
|
855
|
-
ax: Matplotlib axes to annotate.
|
|
856
|
-
thd_value: THD value in dB or %.
|
|
857
|
-
show_thd: Show annotation flag.
|
|
858
|
-
"""
|
|
859
|
-
if show_thd and thd_value is not None:
|
|
860
|
-
if thd_value > 0:
|
|
861
|
-
thd_text = f"THD: {thd_value:.2f}%"
|
|
862
|
-
else:
|
|
863
|
-
thd_text = f"THD: {thd_value:.1f} dB"
|
|
864
|
-
|
|
865
|
-
ax.text(
|
|
866
|
-
0.98,
|
|
867
|
-
0.98,
|
|
868
|
-
thd_text,
|
|
869
|
-
transform=ax.transAxes,
|
|
870
|
-
fontsize=12,
|
|
871
|
-
fontweight="bold",
|
|
872
|
-
ha="right",
|
|
873
|
-
va="top",
|
|
874
|
-
bbox={"boxstyle": "round,pad=0.5", "facecolor": "wheat", "alpha": 0.9},
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
def plot_thd_bars(
|
|
879
|
-
harmonic_magnitudes: NDArray[np.floating[Any]],
|
|
880
|
-
*,
|
|
881
|
-
fundamental_freq: float | None = None,
|
|
882
|
-
ax: Axes | None = None,
|
|
883
|
-
figsize: tuple[float, float] = (10, 6),
|
|
884
|
-
title: str | None = None,
|
|
885
|
-
thd_value: float | None = None,
|
|
886
|
-
show_thd: bool = True,
|
|
887
|
-
reference_db: float = 0.0,
|
|
888
|
-
show: bool = True,
|
|
889
|
-
save_path: str | Path | None = None,
|
|
890
|
-
) -> Figure:
|
|
891
|
-
"""Plot THD harmonic bar chart.
|
|
892
|
-
|
|
893
|
-
Creates a bar chart showing harmonic content relative to the fundamental,
|
|
894
|
-
useful for visualizing Total Harmonic Distortion analysis results.
|
|
895
|
-
|
|
896
|
-
Args:
|
|
897
|
-
harmonic_magnitudes: Array of harmonic magnitudes in dB (relative to fundamental).
|
|
898
|
-
Index 0 = fundamental (0 dB), Index 1 = 2nd harmonic, etc.
|
|
899
|
-
fundamental_freq: Fundamental frequency in Hz (for x-axis labels).
|
|
900
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
901
|
-
figsize: Figure size in inches.
|
|
902
|
-
title: Plot title.
|
|
903
|
-
thd_value: Pre-calculated THD value in dB or % to display.
|
|
904
|
-
show_thd: Show THD annotation on plot.
|
|
905
|
-
reference_db: Reference level for the fundamental (default 0 dB).
|
|
906
|
-
show: Display plot interactively.
|
|
907
|
-
save_path: Save plot to file.
|
|
908
|
-
|
|
909
|
-
Returns:
|
|
910
|
-
Matplotlib Figure object.
|
|
911
|
-
|
|
912
|
-
Example:
|
|
913
|
-
>>> # Harmonic magnitudes relative to fundamental (in dB)
|
|
914
|
-
>>> harmonics = np.array([0, -40, -60, -55, -70, -65]) # Fund, H2, H3, H4, H5, H6
|
|
915
|
-
>>> fig = plot_thd_bars(harmonics, fundamental_freq=1000, thd_value=-38.5)
|
|
916
|
-
|
|
917
|
-
References:
|
|
918
|
-
IEEE 1241-2010: ADC Testing Standards
|
|
919
|
-
IEC 61000-4-7: Harmonics measurement
|
|
920
|
-
"""
|
|
921
|
-
if not HAS_MATPLOTLIB:
|
|
922
|
-
raise ImportError("matplotlib is required for visualization")
|
|
923
|
-
|
|
924
|
-
# Figure/axes creation
|
|
925
|
-
if ax is None:
|
|
926
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
927
|
-
else:
|
|
928
|
-
fig_temp = ax.get_figure()
|
|
929
|
-
if fig_temp is None:
|
|
930
|
-
raise ValueError("Axes must have an associated figure")
|
|
931
|
-
fig = cast("Figure", fig_temp)
|
|
932
|
-
|
|
933
|
-
# Data preparation
|
|
934
|
-
n_harmonics = len(harmonic_magnitudes)
|
|
935
|
-
x_pos = np.arange(n_harmonics)
|
|
936
|
-
labels = _create_harmonic_labels(n_harmonics, fundamental_freq)
|
|
937
|
-
colors = _assign_harmonic_colors(harmonic_magnitudes)
|
|
938
|
-
|
|
939
|
-
# Plotting/rendering
|
|
940
|
-
ax.bar(
|
|
941
|
-
x_pos, harmonic_magnitudes - reference_db, color=colors, edgecolor="black", linewidth=0.5
|
|
942
|
-
)
|
|
943
|
-
ax.axhline(0, color="gray", linestyle="--", linewidth=1, alpha=0.7)
|
|
944
|
-
|
|
945
|
-
# Annotation/labeling
|
|
946
|
-
_add_thd_annotation(ax, thd_value, show_thd)
|
|
947
|
-
ax.set_xticks(x_pos)
|
|
948
|
-
ax.set_xticklabels(labels, fontsize=9)
|
|
949
|
-
ax.set_xlabel("Harmonic", fontsize=11)
|
|
950
|
-
ax.set_ylabel("Magnitude (dB rel. to fundamental)", fontsize=11)
|
|
951
|
-
ax.grid(True, axis="y", alpha=0.3)
|
|
952
|
-
|
|
953
|
-
# Layout/formatting
|
|
954
|
-
min_mag = min(harmonic_magnitudes) - reference_db
|
|
955
|
-
ax.set_ylim(min(min_mag - 10, -80), 10)
|
|
956
|
-
|
|
957
|
-
if title:
|
|
958
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
959
|
-
else:
|
|
960
|
-
ax.set_title("Harmonic Distortion Analysis", fontsize=12, fontweight="bold")
|
|
961
|
-
|
|
962
|
-
fig.tight_layout()
|
|
963
|
-
|
|
964
|
-
if save_path is not None:
|
|
965
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
966
|
-
|
|
967
|
-
if show:
|
|
968
|
-
plt.show()
|
|
969
|
-
|
|
970
|
-
return fig
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
def plot_quality_summary(
|
|
974
|
-
metrics: dict[str, float],
|
|
975
|
-
*,
|
|
976
|
-
ax: Axes | None = None,
|
|
977
|
-
figsize: tuple[float, float] = (10, 6),
|
|
978
|
-
title: str | None = None,
|
|
979
|
-
show_specs: dict[str, float] | None = None,
|
|
980
|
-
show: bool = True,
|
|
981
|
-
save_path: str | Path | None = None,
|
|
982
|
-
) -> Figure:
|
|
983
|
-
"""Plot ADC/signal quality summary with metrics.
|
|
984
|
-
|
|
985
|
-
Creates a summary panel showing SNR, SINAD, THD, ENOB, and SFDR
|
|
986
|
-
with optional pass/fail indication against specifications.
|
|
987
|
-
|
|
988
|
-
Args:
|
|
989
|
-
metrics: Dictionary with keys like "snr", "sinad", "thd", "enob", "sfdr".
|
|
990
|
-
ax: Matplotlib axes.
|
|
991
|
-
figsize: Figure size.
|
|
992
|
-
title: Plot title.
|
|
993
|
-
show_specs: Dictionary of specification values for pass/fail.
|
|
994
|
-
show: Display plot.
|
|
995
|
-
save_path: Save path.
|
|
996
|
-
|
|
997
|
-
Returns:
|
|
998
|
-
Matplotlib Figure object.
|
|
999
|
-
|
|
1000
|
-
Example:
|
|
1001
|
-
>>> metrics = {"snr": 72.5, "sinad": 70.2, "thd": -65.3, "enob": 11.2, "sfdr": 75.8}
|
|
1002
|
-
>>> specs = {"snr": 70.0, "enob": 10.0}
|
|
1003
|
-
>>> fig = plot_quality_summary(metrics, show_specs=specs)
|
|
1004
|
-
|
|
1005
|
-
References:
|
|
1006
|
-
IEEE 1241-2010: ADC Testing Standards
|
|
1007
|
-
"""
|
|
1008
|
-
if not HAS_MATPLOTLIB:
|
|
1009
|
-
raise ImportError("matplotlib is required for visualization")
|
|
1010
|
-
|
|
1011
|
-
# Setup figure and axes
|
|
1012
|
-
fig, ax = _setup_quality_plot_axes(ax, figsize)
|
|
1013
|
-
|
|
1014
|
-
# Define metric metadata
|
|
1015
|
-
metric_info = _get_metric_info()
|
|
1016
|
-
|
|
1017
|
-
# Filter to available metrics
|
|
1018
|
-
available_metrics = [(k, v) for k, v in metrics.items() if k in metric_info]
|
|
1019
|
-
|
|
1020
|
-
if len(available_metrics) == 0:
|
|
1021
|
-
ax.text(0.5, 0.5, "No metrics available", ha="center", va="center", fontsize=14)
|
|
1022
|
-
ax.axis("off")
|
|
1023
|
-
return fig
|
|
1024
|
-
|
|
1025
|
-
# Plot metrics with pass/fail coloring
|
|
1026
|
-
_plot_quality_bars(ax, available_metrics, metric_info, show_specs)
|
|
1027
|
-
|
|
1028
|
-
# Add value labels and spec markers
|
|
1029
|
-
_add_quality_labels(ax, available_metrics, metric_info)
|
|
1030
|
-
_add_spec_markers(ax, available_metrics, show_specs)
|
|
1031
|
-
|
|
1032
|
-
# Configure axes
|
|
1033
|
-
_configure_quality_axes(ax, available_metrics, metric_info, title)
|
|
1034
|
-
|
|
1035
|
-
fig.tight_layout()
|
|
1036
|
-
|
|
1037
|
-
if save_path is not None:
|
|
1038
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
1039
|
-
|
|
1040
|
-
if show:
|
|
1041
|
-
plt.show()
|
|
1042
|
-
|
|
1043
|
-
return fig
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
def _setup_quality_plot_axes(
|
|
1047
|
-
ax: Axes | None,
|
|
1048
|
-
figsize: tuple[float, float],
|
|
1049
|
-
) -> tuple[Figure, Axes]:
|
|
1050
|
-
"""Setup figure and axes for quality summary plot.
|
|
1051
|
-
|
|
1052
|
-
Args:
|
|
1053
|
-
ax: Existing axes or None.
|
|
1054
|
-
figsize: Figure size if creating new figure.
|
|
1055
|
-
|
|
1056
|
-
Returns:
|
|
1057
|
-
Tuple of (Figure, Axes).
|
|
1058
|
-
|
|
1059
|
-
Raises:
|
|
1060
|
-
ValueError: If provided axes has no associated figure.
|
|
1061
|
-
"""
|
|
1062
|
-
if ax is None:
|
|
1063
|
-
fig, ax_obj = plt.subplots(figsize=figsize)
|
|
1064
|
-
return fig, ax_obj
|
|
1065
|
-
else:
|
|
1066
|
-
fig_temp = ax.get_figure()
|
|
1067
|
-
if fig_temp is None:
|
|
1068
|
-
raise ValueError("Axes must have an associated figure")
|
|
1069
|
-
return cast("Figure", fig_temp), ax
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
def _get_metric_info() -> dict[str, dict[str, Any]]:
|
|
1073
|
-
"""Get metric display information.
|
|
1074
|
-
|
|
1075
|
-
Returns:
|
|
1076
|
-
Dictionary mapping metric keys to display info (name, unit, higher_better).
|
|
1077
|
-
"""
|
|
1078
|
-
return {
|
|
1079
|
-
"snr": {"name": "SNR", "unit": "dB", "higher_better": True},
|
|
1080
|
-
"sinad": {"name": "SINAD", "unit": "dB", "higher_better": True},
|
|
1081
|
-
"thd": {"name": "THD", "unit": "dB", "higher_better": False},
|
|
1082
|
-
"enob": {"name": "ENOB", "unit": "bits", "higher_better": True},
|
|
1083
|
-
"sfdr": {"name": "SFDR", "unit": "dBc", "higher_better": True},
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
def _plot_quality_bars(
|
|
1088
|
-
ax: Axes,
|
|
1089
|
-
available_metrics: list[tuple[str, float]],
|
|
1090
|
-
metric_info: dict[str, dict[str, Any]],
|
|
1091
|
-
show_specs: dict[str, float] | None,
|
|
1092
|
-
) -> None:
|
|
1093
|
-
"""Plot horizontal bars for quality metrics.
|
|
1094
|
-
|
|
1095
|
-
Args:
|
|
1096
|
-
ax: Matplotlib axes.
|
|
1097
|
-
available_metrics: List of (key, value) tuples for available metrics.
|
|
1098
|
-
metric_info: Metric display information.
|
|
1099
|
-
show_specs: Specification values for pass/fail coloring.
|
|
1100
|
-
"""
|
|
1101
|
-
n_metrics = len(available_metrics)
|
|
1102
|
-
y_pos = np.arange(n_metrics)
|
|
1103
|
-
values = [v for _, v in available_metrics]
|
|
1104
|
-
|
|
1105
|
-
# Determine colors based on pass/fail
|
|
1106
|
-
colors = _determine_bar_colors(available_metrics, metric_info, show_specs)
|
|
1107
|
-
|
|
1108
|
-
# Plot horizontal bars
|
|
1109
|
-
ax.barh(y_pos, values, color=colors, edgecolor="black", linewidth=0.5)
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
def _determine_bar_colors(
|
|
1113
|
-
available_metrics: list[tuple[str, float]],
|
|
1114
|
-
metric_info: dict[str, dict[str, Any]],
|
|
1115
|
-
show_specs: dict[str, float] | None,
|
|
1116
|
-
) -> list[str]:
|
|
1117
|
-
"""Determine bar colors based on pass/fail status.
|
|
1118
|
-
|
|
1119
|
-
Args:
|
|
1120
|
-
available_metrics: List of (key, value) tuples.
|
|
1121
|
-
metric_info: Metric display information.
|
|
1122
|
-
show_specs: Specification values.
|
|
1123
|
-
|
|
1124
|
-
Returns:
|
|
1125
|
-
List of color strings (hex codes).
|
|
1126
|
-
"""
|
|
1127
|
-
colors = []
|
|
1128
|
-
for key, value in available_metrics:
|
|
1129
|
-
if show_specs and key in show_specs:
|
|
1130
|
-
spec = show_specs[key]
|
|
1131
|
-
info = metric_info[key]
|
|
1132
|
-
if info["higher_better"]:
|
|
1133
|
-
passed = value >= spec
|
|
1134
|
-
else:
|
|
1135
|
-
# For THD, more negative is better
|
|
1136
|
-
passed = value <= spec
|
|
1137
|
-
colors.append("#27AE60" if passed else "#E74C3C")
|
|
1138
|
-
else:
|
|
1139
|
-
colors.append("#3498DB")
|
|
1140
|
-
return colors
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
def _add_quality_labels(
|
|
1144
|
-
ax: Axes,
|
|
1145
|
-
available_metrics: list[tuple[str, float]],
|
|
1146
|
-
metric_info: dict[str, dict[str, Any]],
|
|
1147
|
-
) -> None:
|
|
1148
|
-
"""Add value labels to quality metric bars.
|
|
1149
|
-
|
|
1150
|
-
Args:
|
|
1151
|
-
ax: Matplotlib axes.
|
|
1152
|
-
available_metrics: List of (key, value) tuples.
|
|
1153
|
-
metric_info: Metric display information.
|
|
1154
|
-
"""
|
|
1155
|
-
for i, (key, value) in enumerate(available_metrics):
|
|
1156
|
-
unit = metric_info[key]["unit"]
|
|
1157
|
-
label_text = f"{value:.1f} {unit}"
|
|
1158
|
-
ax.text(
|
|
1159
|
-
value + 2 if value >= 0 else value - 2,
|
|
1160
|
-
i,
|
|
1161
|
-
label_text,
|
|
1162
|
-
va="center",
|
|
1163
|
-
ha="left" if value >= 0 else "right",
|
|
1164
|
-
fontsize=10,
|
|
1165
|
-
fontweight="bold",
|
|
1166
|
-
)
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
def _add_spec_markers(
|
|
1170
|
-
ax: Axes,
|
|
1171
|
-
available_metrics: list[tuple[str, float]],
|
|
1172
|
-
show_specs: dict[str, float] | None,
|
|
1173
|
-
) -> None:
|
|
1174
|
-
"""Add specification markers to quality plot.
|
|
1175
|
-
|
|
1176
|
-
Args:
|
|
1177
|
-
ax: Matplotlib axes.
|
|
1178
|
-
available_metrics: List of (key, value) tuples.
|
|
1179
|
-
show_specs: Specification values.
|
|
1180
|
-
"""
|
|
1181
|
-
if not show_specs:
|
|
1182
|
-
return
|
|
1183
|
-
|
|
1184
|
-
for i, (key, _) in enumerate(available_metrics):
|
|
1185
|
-
if key in show_specs:
|
|
1186
|
-
spec = show_specs[key]
|
|
1187
|
-
ax.plot(spec, i, "k|", markersize=20, markeredgewidth=2)
|
|
1188
|
-
ax.text(spec, i + 0.3, f"Spec: {spec}", fontsize=8, ha="center")
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
def _configure_quality_axes(
|
|
1192
|
-
ax: Axes,
|
|
1193
|
-
available_metrics: list[tuple[str, float]],
|
|
1194
|
-
metric_info: dict[str, dict[str, Any]],
|
|
1195
|
-
title: str | None,
|
|
1196
|
-
) -> None:
|
|
1197
|
-
"""Configure axes for quality summary plot.
|
|
1198
|
-
|
|
1199
|
-
Args:
|
|
1200
|
-
ax: Matplotlib axes.
|
|
1201
|
-
available_metrics: List of (key, value) tuples.
|
|
1202
|
-
metric_info: Metric display information.
|
|
1203
|
-
title: Plot title.
|
|
1204
|
-
"""
|
|
1205
|
-
n_metrics = len(available_metrics)
|
|
1206
|
-
y_pos = np.arange(n_metrics)
|
|
1207
|
-
names = [metric_info[k]["name"] for k, _ in available_metrics]
|
|
1208
|
-
|
|
1209
|
-
ax.set_yticks(y_pos)
|
|
1210
|
-
ax.set_yticklabels([str(name) for name in names], fontsize=11)
|
|
1211
|
-
ax.set_xlabel("Value", fontsize=11)
|
|
1212
|
-
ax.grid(True, axis="x", alpha=0.3)
|
|
1213
|
-
ax.invert_yaxis()
|
|
1214
|
-
|
|
1215
|
-
if title:
|
|
1216
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
1217
|
-
else:
|
|
1218
|
-
ax.set_title("Signal Quality Summary (IEEE 1241-2010)", fontsize=12, fontweight="bold")
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
__all__ = [
|
|
1222
|
-
"plot_fft",
|
|
1223
|
-
"plot_psd",
|
|
1224
|
-
"plot_spectrogram",
|
|
1225
|
-
"plot_spectrum",
|
|
1226
|
-
]
|