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
|
@@ -24,12 +24,13 @@ import numpy as np
|
|
|
24
24
|
from scipy import signal as sp_signal
|
|
25
25
|
|
|
26
26
|
from oscura.core.exceptions import AnalysisError, InsufficientDataError
|
|
27
|
+
from oscura.core.measurement_result import make_inapplicable, make_measurement
|
|
27
28
|
from oscura.utils.windowing import get_window
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
30
31
|
from numpy.typing import NDArray
|
|
31
32
|
|
|
32
|
-
from oscura.core.types import WaveformTrace
|
|
33
|
+
from oscura.core.types import MeasurementResult, WaveformTrace
|
|
33
34
|
|
|
34
35
|
# Global FFT cache statistics
|
|
35
36
|
_fft_cache_stats = {"hits": 0, "misses": 0, "size": 128}
|
|
@@ -645,10 +646,13 @@ def thd(
|
|
|
645
646
|
window: str = "hann",
|
|
646
647
|
nfft: int | None = None,
|
|
647
648
|
return_db: bool = True,
|
|
648
|
-
) ->
|
|
649
|
-
"""Compute Total Harmonic Distortion.
|
|
649
|
+
) -> MeasurementResult:
|
|
650
|
+
"""Compute Total Harmonic Distortion per IEEE 1241-2010.
|
|
650
651
|
|
|
651
|
-
THD is the ratio of harmonic power to fundamental
|
|
652
|
+
THD is defined as the ratio of RMS harmonic power to fundamental amplitude:
|
|
653
|
+
THD = sqrt(sum(A_harmonics^2)) / A_fundamental
|
|
654
|
+
|
|
655
|
+
where harmonics are the 2nd, 3rd, ..., nth harmonic frequencies.
|
|
652
656
|
|
|
653
657
|
Args:
|
|
654
658
|
trace: Input waveform trace.
|
|
@@ -656,15 +660,15 @@ def thd(
|
|
|
656
660
|
window: Window function for FFT.
|
|
657
661
|
nfft: FFT length. If None, uses data length (no zero-padding) to
|
|
658
662
|
preserve coherent sampling per IEEE 1241-2010.
|
|
659
|
-
return_db:
|
|
663
|
+
return_db: Ignored (always returns percentage, with dB in metadata if needed).
|
|
660
664
|
|
|
661
665
|
Returns:
|
|
662
|
-
THD
|
|
666
|
+
MeasurementResult with THD as percentage, or inapplicable if cannot compute.
|
|
663
667
|
|
|
664
668
|
Example:
|
|
665
|
-
>>>
|
|
666
|
-
>>>
|
|
667
|
-
|
|
669
|
+
>>> result = thd(trace)
|
|
670
|
+
>>> if result["applicable"]:
|
|
671
|
+
... print(f"THD: {result['display']}")
|
|
668
672
|
|
|
669
673
|
References:
|
|
670
674
|
IEEE 1241-2010 Section 4.1.4.2
|
|
@@ -676,33 +680,40 @@ def thd(
|
|
|
676
680
|
result = fft(trace, window=window, nfft=nfft, detrend="mean")
|
|
677
681
|
freq, mag_db = result[0], result[1]
|
|
678
682
|
|
|
679
|
-
# Convert to linear
|
|
683
|
+
# Convert to linear magnitude
|
|
680
684
|
magnitude = 10 ** (mag_db / 20)
|
|
681
685
|
|
|
682
|
-
# Find fundamental
|
|
686
|
+
# Find fundamental (strongest peak above DC)
|
|
683
687
|
_fund_idx, fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
684
688
|
|
|
685
689
|
if fund_mag == 0 or fund_freq == 0:
|
|
686
|
-
return
|
|
690
|
+
return make_inapplicable("%", "No fundamental frequency detected")
|
|
687
691
|
|
|
688
|
-
# Find
|
|
692
|
+
# Find harmonic frequencies (2*f0, 3*f0, ..., n*f0)
|
|
689
693
|
harmonic_indices = _find_harmonic_indices(freq, fund_freq, n_harmonics)
|
|
690
694
|
|
|
691
695
|
if len(harmonic_indices) == 0:
|
|
692
|
-
return 0.0
|
|
696
|
+
return make_measurement(0.0, "%")
|
|
693
697
|
|
|
694
|
-
#
|
|
698
|
+
# Compute total harmonic power: sum of squared magnitudes
|
|
695
699
|
harmonic_power = sum(magnitude[i] ** 2 for i in harmonic_indices)
|
|
696
700
|
|
|
697
|
-
# THD
|
|
701
|
+
# THD = sqrt(sum(harmonic_power)) / fundamental_amplitude
|
|
702
|
+
# This is the IEEE 1241-2010 definition
|
|
698
703
|
thd_ratio = np.sqrt(harmonic_power) / fund_mag
|
|
699
704
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
705
|
+
# Validate: THD must always be non-negative
|
|
706
|
+
if thd_ratio < 0:
|
|
707
|
+
raise ValueError(
|
|
708
|
+
f"THD ratio is negative ({thd_ratio:.6f}), indicating a calculation error. "
|
|
709
|
+
f"Fundamental: {fund_mag:.6f}, Harmonic power: {harmonic_power:.6f}"
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if thd_ratio <= 0:
|
|
713
|
+
return make_measurement(0.0, "%")
|
|
714
|
+
|
|
715
|
+
# Return as percentage (0-100)
|
|
716
|
+
return make_measurement(float(thd_ratio * 100), "%")
|
|
706
717
|
|
|
707
718
|
|
|
708
719
|
def snr(
|
|
@@ -711,7 +722,7 @@ def snr(
|
|
|
711
722
|
n_harmonics: int = 10,
|
|
712
723
|
window: str = "hann",
|
|
713
724
|
nfft: int | None = None,
|
|
714
|
-
) ->
|
|
725
|
+
) -> MeasurementResult:
|
|
715
726
|
"""Compute Signal-to-Noise Ratio.
|
|
716
727
|
|
|
717
728
|
SNR is the ratio of signal power to noise power, excluding harmonics.
|
|
@@ -724,11 +735,12 @@ def snr(
|
|
|
724
735
|
preserve coherent sampling per IEEE 1241-2010.
|
|
725
736
|
|
|
726
737
|
Returns:
|
|
727
|
-
SNR in dB.
|
|
738
|
+
MeasurementResult with SNR in dB, or inapplicable if cannot compute.
|
|
728
739
|
|
|
729
740
|
Example:
|
|
730
|
-
>>>
|
|
731
|
-
>>>
|
|
741
|
+
>>> result = snr(trace)
|
|
742
|
+
>>> if result["applicable"]:
|
|
743
|
+
... print(f"SNR: {result['display']}")
|
|
732
744
|
|
|
733
745
|
References:
|
|
734
746
|
IEEE 1241-2010 Section 4.1.4.1
|
|
@@ -740,7 +752,7 @@ def snr(
|
|
|
740
752
|
fund_idx, fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
741
753
|
|
|
742
754
|
if fund_mag == 0 or fund_freq == 0:
|
|
743
|
-
return
|
|
755
|
+
return make_inapplicable("dB", "No fundamental frequency detected")
|
|
744
756
|
|
|
745
757
|
harmonic_indices = _find_harmonic_indices(freq, fund_freq, n_harmonics)
|
|
746
758
|
exclude_indices = _build_exclusion_set(fund_idx, harmonic_indices, len(magnitude))
|
|
@@ -749,9 +761,9 @@ def snr(
|
|
|
749
761
|
noise_power = _compute_noise_power(magnitude, exclude_indices)
|
|
750
762
|
|
|
751
763
|
if noise_power <= 0:
|
|
752
|
-
return
|
|
764
|
+
return make_inapplicable("dB", "No noise detected (perfect signal)")
|
|
753
765
|
|
|
754
|
-
return float(10 * np.log10(signal_power / noise_power))
|
|
766
|
+
return make_measurement(float(10 * np.log10(signal_power / noise_power)), "dB")
|
|
755
767
|
|
|
756
768
|
|
|
757
769
|
def _compute_magnitude_spectrum(
|
|
@@ -842,7 +854,7 @@ def sinad(
|
|
|
842
854
|
*,
|
|
843
855
|
window: str = "hann",
|
|
844
856
|
nfft: int | None = None,
|
|
845
|
-
) ->
|
|
857
|
+
) -> MeasurementResult:
|
|
846
858
|
"""Compute Signal-to-Noise and Distortion ratio.
|
|
847
859
|
|
|
848
860
|
SINAD is the ratio of signal power to noise plus distortion power.
|
|
@@ -854,11 +866,12 @@ def sinad(
|
|
|
854
866
|
preserve coherent sampling per IEEE 1241-2010.
|
|
855
867
|
|
|
856
868
|
Returns:
|
|
857
|
-
SINAD in dB.
|
|
869
|
+
MeasurementResult with SINAD in dB, or inapplicable if cannot compute.
|
|
858
870
|
|
|
859
871
|
Example:
|
|
860
|
-
>>>
|
|
861
|
-
>>>
|
|
872
|
+
>>> result = sinad(trace)
|
|
873
|
+
>>> if result["applicable"]:
|
|
874
|
+
... print(f"SINAD: {result['display']}")
|
|
862
875
|
|
|
863
876
|
References:
|
|
864
877
|
IEEE 1241-2010 Section 4.1.4.3
|
|
@@ -875,7 +888,7 @@ def sinad(
|
|
|
875
888
|
fund_idx, _fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
876
889
|
|
|
877
890
|
if fund_mag == 0:
|
|
878
|
-
return
|
|
891
|
+
return make_inapplicable("dB", "No fundamental frequency detected")
|
|
879
892
|
|
|
880
893
|
# Signal power: use 3-bin window around fundamental to capture spectral leakage
|
|
881
894
|
signal_power = 0.0
|
|
@@ -891,10 +904,10 @@ def sinad(
|
|
|
891
904
|
nad_power = total_power - signal_power
|
|
892
905
|
|
|
893
906
|
if nad_power <= 0:
|
|
894
|
-
return
|
|
907
|
+
return make_inapplicable("dB", "No noise/distortion detected (perfect signal)")
|
|
895
908
|
|
|
896
909
|
sinad_ratio = signal_power / nad_power
|
|
897
|
-
return float(10 * np.log10(sinad_ratio))
|
|
910
|
+
return make_measurement(float(10 * np.log10(sinad_ratio)), "dB")
|
|
898
911
|
|
|
899
912
|
|
|
900
913
|
def enob(
|
|
@@ -902,7 +915,7 @@ def enob(
|
|
|
902
915
|
*,
|
|
903
916
|
window: str = "hann",
|
|
904
917
|
nfft: int | None = None,
|
|
905
|
-
) ->
|
|
918
|
+
) -> MeasurementResult:
|
|
906
919
|
"""Compute Effective Number of Bits from SINAD.
|
|
907
920
|
|
|
908
921
|
ENOB = (SINAD - 1.76) / 6.02
|
|
@@ -913,21 +926,26 @@ def enob(
|
|
|
913
926
|
nfft: FFT length.
|
|
914
927
|
|
|
915
928
|
Returns:
|
|
916
|
-
ENOB in bits, or
|
|
929
|
+
MeasurementResult with ENOB in bits, or inapplicable if SINAD is invalid.
|
|
917
930
|
|
|
918
931
|
Example:
|
|
919
|
-
>>>
|
|
920
|
-
>>>
|
|
932
|
+
>>> result = enob(trace)
|
|
933
|
+
>>> if result["applicable"]:
|
|
934
|
+
... print(f"ENOB: {result['display']}")
|
|
921
935
|
|
|
922
936
|
References:
|
|
923
937
|
IEEE 1241-2010 Section 4.1.4.4
|
|
924
938
|
"""
|
|
925
|
-
|
|
939
|
+
sinad_result = sinad(trace, window=window, nfft=nfft)
|
|
940
|
+
|
|
941
|
+
if not sinad_result["applicable"] or sinad_result["value"] is None:
|
|
942
|
+
return make_inapplicable("", "SINAD unavailable")
|
|
926
943
|
|
|
927
|
-
|
|
928
|
-
|
|
944
|
+
sinad_db = sinad_result["value"]
|
|
945
|
+
if sinad_db <= 0:
|
|
946
|
+
return make_inapplicable("", "SINAD too low (≤0 dB)")
|
|
929
947
|
|
|
930
|
-
return float((sinad_db - 1.76) / 6.02)
|
|
948
|
+
return make_measurement(float((sinad_db - 1.76) / 6.02), "")
|
|
931
949
|
|
|
932
950
|
|
|
933
951
|
def sfdr(
|
|
@@ -935,7 +953,7 @@ def sfdr(
|
|
|
935
953
|
*,
|
|
936
954
|
window: str = "hann",
|
|
937
955
|
nfft: int | None = None,
|
|
938
|
-
) ->
|
|
956
|
+
) -> MeasurementResult:
|
|
939
957
|
"""Compute Spurious-Free Dynamic Range.
|
|
940
958
|
|
|
941
959
|
SFDR is the ratio of fundamental to largest spurious component.
|
|
@@ -947,11 +965,13 @@ def sfdr(
|
|
|
947
965
|
preserve coherent sampling per IEEE 1241-2010.
|
|
948
966
|
|
|
949
967
|
Returns:
|
|
950
|
-
SFDR in dBc (dB relative to carrier/fundamental)
|
|
968
|
+
MeasurementResult with SFDR in dBc (dB relative to carrier/fundamental),
|
|
969
|
+
or inapplicable if cannot compute.
|
|
951
970
|
|
|
952
971
|
Example:
|
|
953
|
-
>>>
|
|
954
|
-
>>>
|
|
972
|
+
>>> result = sfdr(trace)
|
|
973
|
+
>>> if result["applicable"]:
|
|
974
|
+
... print(f"SFDR: {result['display']}")
|
|
955
975
|
|
|
956
976
|
References:
|
|
957
977
|
IEEE 1241-2010 Section 4.1.4.5
|
|
@@ -968,7 +988,7 @@ def sfdr(
|
|
|
968
988
|
fund_idx, _fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
969
989
|
|
|
970
990
|
if fund_mag == 0:
|
|
971
|
-
return
|
|
991
|
+
return make_inapplicable("dB", "No fundamental frequency detected")
|
|
972
992
|
|
|
973
993
|
# Create mask for spurs (exclude fundamental and DC)
|
|
974
994
|
spur_mask = np.ones(len(magnitude), dtype=bool)
|
|
@@ -987,15 +1007,15 @@ def sfdr(
|
|
|
987
1007
|
# Find largest spur
|
|
988
1008
|
spur_magnitudes = magnitude[spur_mask]
|
|
989
1009
|
if len(spur_magnitudes) == 0:
|
|
990
|
-
return
|
|
1010
|
+
return make_inapplicable("dB", "No spurs detected (clean spectrum)")
|
|
991
1011
|
|
|
992
1012
|
max_spur = np.max(spur_magnitudes)
|
|
993
1013
|
|
|
994
1014
|
if max_spur <= 0:
|
|
995
|
-
return
|
|
1015
|
+
return make_inapplicable("dB", "No valid spurs detected")
|
|
996
1016
|
|
|
997
1017
|
sfdr_ratio = fund_mag / max_spur
|
|
998
|
-
return float(20 * np.log10(sfdr_ratio))
|
|
1018
|
+
return make_measurement(float(20 * np.log10(sfdr_ratio)), "dB")
|
|
999
1019
|
|
|
1000
1020
|
|
|
1001
1021
|
def hilbert_transform(
|
|
@@ -1015,7 +1035,7 @@ def hilbert_transform(
|
|
|
1015
1035
|
|
|
1016
1036
|
Example:
|
|
1017
1037
|
>>> envelope, phase, inst_freq = hilbert_transform(trace)
|
|
1018
|
-
>>> plt.plot(trace.
|
|
1038
|
+
>>> plt.plot(trace.time, envelope)
|
|
1019
1039
|
|
|
1020
1040
|
References:
|
|
1021
1041
|
Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time
|
|
@@ -1072,7 +1092,7 @@ def cwt(
|
|
|
1072
1092
|
|
|
1073
1093
|
Example:
|
|
1074
1094
|
>>> scales, freqs, coef = cwt(trace, wavelet="morlet")
|
|
1075
|
-
>>> plt.pcolormesh(trace.
|
|
1095
|
+
>>> plt.pcolormesh(trace.time, freqs, np.abs(coef))
|
|
1076
1096
|
>>> plt.ylabel("Frequency (Hz)")
|
|
1077
1097
|
|
|
1078
1098
|
References:
|
|
@@ -1936,20 +1956,444 @@ def _extract_fft_segment(
|
|
|
1936
1956
|
return segment
|
|
1937
1957
|
|
|
1938
1958
|
|
|
1959
|
+
def find_peaks(
|
|
1960
|
+
trace: WaveformTrace,
|
|
1961
|
+
*,
|
|
1962
|
+
window: str = "hann",
|
|
1963
|
+
nfft: int | None = None,
|
|
1964
|
+
threshold_db: float = -60.0,
|
|
1965
|
+
min_distance: int = 5,
|
|
1966
|
+
n_peaks: int | None = None,
|
|
1967
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
1968
|
+
"""Find spectral peaks in FFT magnitude spectrum.
|
|
1969
|
+
|
|
1970
|
+
Identifies prominent frequency components above a threshold with
|
|
1971
|
+
minimum spacing between peaks.
|
|
1972
|
+
|
|
1973
|
+
Args:
|
|
1974
|
+
trace: Input waveform trace.
|
|
1975
|
+
window: Window function for FFT.
|
|
1976
|
+
nfft: FFT length.
|
|
1977
|
+
threshold_db: Minimum peak magnitude in dB (relative to max).
|
|
1978
|
+
min_distance: Minimum bin spacing between peaks.
|
|
1979
|
+
n_peaks: Maximum number of peaks to return (None = all).
|
|
1980
|
+
|
|
1981
|
+
Returns:
|
|
1982
|
+
Dictionary with keys:
|
|
1983
|
+
- "frequencies": Peak frequencies in Hz
|
|
1984
|
+
- "magnitudes_db": Peak magnitudes in dB
|
|
1985
|
+
- "indices": FFT bin indices of peaks
|
|
1986
|
+
|
|
1987
|
+
Example:
|
|
1988
|
+
>>> peaks = find_peaks(trace, threshold_db=-40, n_peaks=10)
|
|
1989
|
+
>>> print(f"Found {len(peaks['frequencies'])} peaks")
|
|
1990
|
+
>>> for freq, mag in zip(peaks['frequencies'], peaks['magnitudes_db']):
|
|
1991
|
+
... print(f" {freq:.1f} Hz: {mag:.1f} dB")
|
|
1992
|
+
|
|
1993
|
+
References:
|
|
1994
|
+
IEEE 1241-2010 Section 4.1.5 - Spectral Analysis
|
|
1995
|
+
"""
|
|
1996
|
+
from scipy.signal import find_peaks as sp_find_peaks
|
|
1997
|
+
|
|
1998
|
+
result = fft(trace, window=window, nfft=nfft)
|
|
1999
|
+
freq, mag_db = result[0], result[1]
|
|
2000
|
+
|
|
2001
|
+
# Find peaks using scipy
|
|
2002
|
+
# Convert threshold from dB relative to max
|
|
2003
|
+
max_mag_db = np.max(mag_db)
|
|
2004
|
+
abs_threshold = max_mag_db + threshold_db # threshold_db is negative
|
|
2005
|
+
|
|
2006
|
+
peak_indices, _ = sp_find_peaks(mag_db, height=abs_threshold, distance=min_distance)
|
|
2007
|
+
|
|
2008
|
+
# Sort by magnitude (strongest first)
|
|
2009
|
+
sorted_idx = np.argsort(mag_db[peak_indices])[::-1]
|
|
2010
|
+
peak_indices = peak_indices[sorted_idx]
|
|
2011
|
+
|
|
2012
|
+
# Limit number of peaks
|
|
2013
|
+
if n_peaks is not None:
|
|
2014
|
+
peak_indices = peak_indices[:n_peaks]
|
|
2015
|
+
|
|
2016
|
+
return {
|
|
2017
|
+
"frequencies": freq[peak_indices],
|
|
2018
|
+
"magnitudes_db": mag_db[peak_indices],
|
|
2019
|
+
"indices": peak_indices.astype(np.float64),
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
|
|
2023
|
+
def extract_harmonics(
|
|
2024
|
+
trace: WaveformTrace,
|
|
2025
|
+
*,
|
|
2026
|
+
fundamental_freq: float | None = None,
|
|
2027
|
+
n_harmonics: int = 10,
|
|
2028
|
+
window: str = "hann",
|
|
2029
|
+
nfft: int | None = None,
|
|
2030
|
+
search_width_hz: float = 50.0,
|
|
2031
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
2032
|
+
"""Extract harmonic frequencies and amplitudes from spectrum.
|
|
2033
|
+
|
|
2034
|
+
Identifies fundamental frequency (if not provided) and extracts
|
|
2035
|
+
harmonic series with frequencies and amplitudes.
|
|
2036
|
+
|
|
2037
|
+
Args:
|
|
2038
|
+
trace: Input waveform trace.
|
|
2039
|
+
fundamental_freq: Fundamental frequency in Hz. If None, auto-detected.
|
|
2040
|
+
n_harmonics: Number of harmonics to extract (excluding fundamental).
|
|
2041
|
+
window: Window function for FFT.
|
|
2042
|
+
nfft: FFT length.
|
|
2043
|
+
search_width_hz: Search range around expected harmonic frequencies.
|
|
2044
|
+
|
|
2045
|
+
Returns:
|
|
2046
|
+
Dictionary with keys:
|
|
2047
|
+
- "frequencies": Harmonic frequencies [f0, 2f0, 3f0, ...]
|
|
2048
|
+
- "amplitudes": Harmonic amplitudes (linear scale)
|
|
2049
|
+
- "amplitudes_db": Harmonic amplitudes in dB
|
|
2050
|
+
- "fundamental_freq": Detected or provided fundamental frequency
|
|
2051
|
+
|
|
2052
|
+
Example:
|
|
2053
|
+
>>> harmonics = extract_harmonics(trace, n_harmonics=5)
|
|
2054
|
+
>>> f0 = harmonics["fundamental_freq"]
|
|
2055
|
+
>>> print(f"Fundamental: {f0:.1f} Hz")
|
|
2056
|
+
>>> for i, (freq, amp_db) in enumerate(
|
|
2057
|
+
... zip(harmonics["frequencies"], harmonics["amplitudes_db"]), 1
|
|
2058
|
+
... ):
|
|
2059
|
+
... print(f" H{i}: {freq:.1f} Hz at {amp_db:.1f} dB")
|
|
2060
|
+
|
|
2061
|
+
References:
|
|
2062
|
+
IEEE 1241-2010 Section 4.1.4.2 - Harmonic Analysis
|
|
2063
|
+
"""
|
|
2064
|
+
result = fft(trace, window=window, nfft=nfft)
|
|
2065
|
+
freq, mag_db = result[0], result[1]
|
|
2066
|
+
magnitude = 10 ** (mag_db / 20)
|
|
2067
|
+
|
|
2068
|
+
# Auto-detect fundamental if not provided
|
|
2069
|
+
if fundamental_freq is None:
|
|
2070
|
+
_fund_idx, fund_freq, _fund_mag = _find_fundamental(freq, magnitude)
|
|
2071
|
+
fundamental_freq = fund_freq
|
|
2072
|
+
|
|
2073
|
+
if fundamental_freq == 0:
|
|
2074
|
+
# Return empty result
|
|
2075
|
+
return {
|
|
2076
|
+
"frequencies": np.array([]),
|
|
2077
|
+
"amplitudes": np.array([]),
|
|
2078
|
+
"amplitudes_db": np.array([]),
|
|
2079
|
+
"fundamental_freq": np.array([0.0]),
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
# Extract harmonics
|
|
2083
|
+
harmonic_freqs = []
|
|
2084
|
+
harmonic_amps = []
|
|
2085
|
+
|
|
2086
|
+
for h in range(1, n_harmonics + 2): # Include fundamental (h=1)
|
|
2087
|
+
target_freq = h * fundamental_freq
|
|
2088
|
+
if target_freq > freq[-1]:
|
|
2089
|
+
break
|
|
2090
|
+
|
|
2091
|
+
# Search around expected frequency
|
|
2092
|
+
search_mask = np.abs(freq - target_freq) <= search_width_hz
|
|
2093
|
+
if not np.any(search_mask):
|
|
2094
|
+
continue
|
|
2095
|
+
|
|
2096
|
+
# Find peak in search region
|
|
2097
|
+
search_region_mag = magnitude.copy()
|
|
2098
|
+
search_region_mag[~search_mask] = 0
|
|
2099
|
+
peak_idx = np.argmax(search_region_mag)
|
|
2100
|
+
|
|
2101
|
+
harmonic_freqs.append(float(freq[peak_idx]))
|
|
2102
|
+
harmonic_amps.append(float(magnitude[peak_idx]))
|
|
2103
|
+
|
|
2104
|
+
harmonic_freqs_arr = np.array(harmonic_freqs)
|
|
2105
|
+
harmonic_amps_arr = np.array(harmonic_amps)
|
|
2106
|
+
harmonic_amps_db = 20 * np.log10(np.maximum(harmonic_amps_arr, 1e-20))
|
|
2107
|
+
|
|
2108
|
+
return {
|
|
2109
|
+
"frequencies": harmonic_freqs_arr,
|
|
2110
|
+
"amplitudes": harmonic_amps_arr,
|
|
2111
|
+
"amplitudes_db": harmonic_amps_db,
|
|
2112
|
+
"fundamental_freq": np.array([fundamental_freq]),
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def phase_spectrum(
|
|
2117
|
+
trace: WaveformTrace,
|
|
2118
|
+
*,
|
|
2119
|
+
window: str = "hann",
|
|
2120
|
+
nfft: int | None = None,
|
|
2121
|
+
unwrap: bool = True,
|
|
2122
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
2123
|
+
"""Compute phase spectrum from FFT.
|
|
2124
|
+
|
|
2125
|
+
Extracts phase information from frequency domain representation.
|
|
2126
|
+
|
|
2127
|
+
Args:
|
|
2128
|
+
trace: Input waveform trace.
|
|
2129
|
+
window: Window function for FFT.
|
|
2130
|
+
nfft: FFT length.
|
|
2131
|
+
unwrap: If True, unwrap phase to remove 2π discontinuities.
|
|
2132
|
+
|
|
2133
|
+
Returns:
|
|
2134
|
+
(frequencies, phase) where phase is in radians.
|
|
2135
|
+
|
|
2136
|
+
Example:
|
|
2137
|
+
>>> freq, phase = phase_spectrum(trace)
|
|
2138
|
+
>>> plt.plot(freq, phase)
|
|
2139
|
+
>>> plt.xlabel("Frequency (Hz)")
|
|
2140
|
+
>>> plt.ylabel("Phase (radians)")
|
|
2141
|
+
|
|
2142
|
+
References:
|
|
2143
|
+
Oppenheim & Schafer (2009) - Discrete-Time Signal Processing
|
|
2144
|
+
"""
|
|
2145
|
+
result = fft(trace, window=window, nfft=nfft, return_phase=True)
|
|
2146
|
+
assert len(result) == 3, "Expected 3-tuple from fft with return_phase=True"
|
|
2147
|
+
freq, _mag_db, phase = result[0], result[1], result[2]
|
|
2148
|
+
|
|
2149
|
+
if unwrap:
|
|
2150
|
+
phase = np.unwrap(phase)
|
|
2151
|
+
|
|
2152
|
+
return freq, phase
|
|
2153
|
+
|
|
2154
|
+
|
|
2155
|
+
def group_delay(
|
|
2156
|
+
trace: WaveformTrace,
|
|
2157
|
+
*,
|
|
2158
|
+
window: str = "hann",
|
|
2159
|
+
nfft: int | None = None,
|
|
2160
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
2161
|
+
"""Compute group delay from phase spectrum.
|
|
2162
|
+
|
|
2163
|
+
Group delay is the negative derivative of phase with respect to
|
|
2164
|
+
frequency, representing signal delay at each frequency.
|
|
2165
|
+
|
|
2166
|
+
Args:
|
|
2167
|
+
trace: Input waveform trace.
|
|
2168
|
+
window: Window function for FFT.
|
|
2169
|
+
nfft: FFT length.
|
|
2170
|
+
|
|
2171
|
+
Returns:
|
|
2172
|
+
(frequencies, group_delay_samples) - Delay in samples at each frequency.
|
|
2173
|
+
|
|
2174
|
+
Example:
|
|
2175
|
+
>>> freq, gd = group_delay(trace)
|
|
2176
|
+
>>> plt.plot(freq, gd)
|
|
2177
|
+
>>> plt.xlabel("Frequency (Hz)")
|
|
2178
|
+
>>> plt.ylabel("Group Delay (samples)")
|
|
2179
|
+
|
|
2180
|
+
References:
|
|
2181
|
+
Oppenheim & Schafer (2009) Section 5.1.1
|
|
2182
|
+
"""
|
|
2183
|
+
freq, phase = phase_spectrum(trace, window=window, nfft=nfft, unwrap=True)
|
|
2184
|
+
|
|
2185
|
+
# Group delay = -dφ/dω
|
|
2186
|
+
# In discrete frequency: gd[i] ≈ -(φ[i+1] - φ[i]) / (ω[i+1] - ω[i])
|
|
2187
|
+
# Convert frequency to angular frequency: ω = 2π * f
|
|
2188
|
+
omega = 2 * np.pi * freq
|
|
2189
|
+
|
|
2190
|
+
# Compute derivative using central differences
|
|
2191
|
+
gd = np.zeros_like(phase)
|
|
2192
|
+
|
|
2193
|
+
# Central difference for interior points
|
|
2194
|
+
gd[1:-1] = -(phase[2:] - phase[:-2]) / (omega[2:] - omega[:-2])
|
|
2195
|
+
|
|
2196
|
+
# Forward/backward difference for boundaries
|
|
2197
|
+
if len(phase) > 1:
|
|
2198
|
+
gd[0] = -(phase[1] - phase[0]) / (omega[1] - omega[0])
|
|
2199
|
+
gd[-1] = -(phase[-1] - phase[-2]) / (omega[-1] - omega[-2])
|
|
2200
|
+
|
|
2201
|
+
return freq, gd
|
|
2202
|
+
|
|
2203
|
+
|
|
2204
|
+
def measure(
|
|
2205
|
+
trace: WaveformTrace,
|
|
2206
|
+
*,
|
|
2207
|
+
parameters: list[str] | None = None,
|
|
2208
|
+
include_units: bool = True,
|
|
2209
|
+
) -> dict[str, Any]:
|
|
2210
|
+
"""Compute multiple spectral measurements with consistent format.
|
|
2211
|
+
|
|
2212
|
+
Unified function for computing spectral quality metrics following IEEE 1241-2010.
|
|
2213
|
+
Returns MeasurementResult format with applicability tracking.
|
|
2214
|
+
|
|
2215
|
+
Args:
|
|
2216
|
+
trace: Input waveform trace.
|
|
2217
|
+
parameters: List of measurement names to compute. If None, compute all.
|
|
2218
|
+
Valid names: thd, snr, sinad, enob, sfdr, dominant_freq
|
|
2219
|
+
include_units: If True, return MeasurementResult format. If False, return flat values.
|
|
2220
|
+
|
|
2221
|
+
Returns:
|
|
2222
|
+
Dictionary mapping measurement names to MeasurementResults (if include_units=True)
|
|
2223
|
+
or raw values (if include_units=False).
|
|
2224
|
+
|
|
2225
|
+
Raises:
|
|
2226
|
+
InsufficientDataError: If trace is too short for analysis.
|
|
2227
|
+
AnalysisError: If computation fails.
|
|
2228
|
+
|
|
2229
|
+
Example:
|
|
2230
|
+
>>> from oscura.analyzers.waveform.spectral import measure
|
|
2231
|
+
>>> results = measure(trace)
|
|
2232
|
+
>>> if results['thd']['applicable']:
|
|
2233
|
+
... print(f"THD: {results['thd']['display']}")
|
|
2234
|
+
|
|
2235
|
+
>>> # Get specific measurements only
|
|
2236
|
+
>>> results = measure(trace, parameters=["thd", "snr"])
|
|
2237
|
+
|
|
2238
|
+
>>> # Get flat values (legacy compatibility)
|
|
2239
|
+
>>> results = measure(trace, include_units=False)
|
|
2240
|
+
>>> thd_value = results["thd"] # float or np.nan
|
|
2241
|
+
|
|
2242
|
+
References:
|
|
2243
|
+
IEEE 1241-2010: ADC Terminology and Test Methods
|
|
2244
|
+
"""
|
|
2245
|
+
# Define all available spectral measurements
|
|
2246
|
+
all_measurements = {
|
|
2247
|
+
"thd": thd,
|
|
2248
|
+
"snr": snr,
|
|
2249
|
+
"sinad": sinad,
|
|
2250
|
+
"enob": enob,
|
|
2251
|
+
"sfdr": sfdr,
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
# Select requested measurements or all
|
|
2255
|
+
if parameters is None:
|
|
2256
|
+
selected = all_measurements
|
|
2257
|
+
else:
|
|
2258
|
+
selected = {k: v for k, v in all_measurements.items() if k in parameters}
|
|
2259
|
+
|
|
2260
|
+
results: dict[str, Any] = {}
|
|
2261
|
+
|
|
2262
|
+
for name, func in selected.items():
|
|
2263
|
+
try:
|
|
2264
|
+
measurement_result = func(trace) # type: ignore[operator]
|
|
2265
|
+
|
|
2266
|
+
if include_units:
|
|
2267
|
+
# Return full MeasurementResult
|
|
2268
|
+
results[name] = measurement_result
|
|
2269
|
+
else:
|
|
2270
|
+
# Legacy mode: extract raw value (NaN if inapplicable)
|
|
2271
|
+
if measurement_result["applicable"]:
|
|
2272
|
+
results[name] = measurement_result["value"]
|
|
2273
|
+
else:
|
|
2274
|
+
results[name] = np.nan
|
|
2275
|
+
|
|
2276
|
+
except Exception:
|
|
2277
|
+
# On error, create inapplicable result
|
|
2278
|
+
if include_units:
|
|
2279
|
+
results[name] = make_inapplicable("", "Measurement failed")
|
|
2280
|
+
else:
|
|
2281
|
+
results[name] = np.nan
|
|
2282
|
+
|
|
2283
|
+
# Add dominant frequency if requested or if computing all
|
|
2284
|
+
if parameters is None or "dominant_freq" in parameters:
|
|
2285
|
+
try:
|
|
2286
|
+
fft_result = fft(trace, return_phase=False)
|
|
2287
|
+
freq, magnitude = fft_result[0], fft_result[1]
|
|
2288
|
+
dominant_idx = int(np.argmax(np.abs(magnitude[1:]))) + 1 # Skip DC
|
|
2289
|
+
dominant_freq_value = float(freq[dominant_idx])
|
|
2290
|
+
|
|
2291
|
+
if include_units:
|
|
2292
|
+
results["dominant_freq"] = make_measurement(dominant_freq_value, "Hz")
|
|
2293
|
+
else:
|
|
2294
|
+
results["dominant_freq"] = dominant_freq_value
|
|
2295
|
+
except Exception:
|
|
2296
|
+
if include_units:
|
|
2297
|
+
results["dominant_freq"] = make_inapplicable("Hz", "FFT failed")
|
|
2298
|
+
else:
|
|
2299
|
+
results["dominant_freq"] = np.nan
|
|
2300
|
+
|
|
2301
|
+
return results
|
|
2302
|
+
|
|
2303
|
+
|
|
2304
|
+
class SpectralAnalyzer:
|
|
2305
|
+
"""Spectral analysis class wrapping functional APIs.
|
|
2306
|
+
|
|
2307
|
+
Provides object-oriented interface to spectral analysis functions
|
|
2308
|
+
for compatibility with legacy code and workflows.
|
|
2309
|
+
|
|
2310
|
+
Example:
|
|
2311
|
+
>>> analyzer = SpectralAnalyzer()
|
|
2312
|
+
>>> freqs, mags = analyzer.fft(data, sample_rate)
|
|
2313
|
+
>>> thd_value = analyzer.thd(trace)
|
|
2314
|
+
"""
|
|
2315
|
+
|
|
2316
|
+
def fft(
|
|
2317
|
+
self,
|
|
2318
|
+
data: NDArray[np.floating[Any]],
|
|
2319
|
+
sample_rate: float,
|
|
2320
|
+
*,
|
|
2321
|
+
window: str = "hann",
|
|
2322
|
+
nfft: int | None = None,
|
|
2323
|
+
detrend: Literal["none", "mean", "linear"] = "mean",
|
|
2324
|
+
return_phase: bool = False,
|
|
2325
|
+
) -> (
|
|
2326
|
+
tuple[NDArray[np.float64], NDArray[np.float64]]
|
|
2327
|
+
| tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]
|
|
2328
|
+
):
|
|
2329
|
+
"""Compute FFT of signal data.
|
|
2330
|
+
|
|
2331
|
+
Args:
|
|
2332
|
+
data: Input signal data
|
|
2333
|
+
sample_rate: Sample rate in Hz
|
|
2334
|
+
window: Window function name (default "hann")
|
|
2335
|
+
nfft: FFT size (power of 2), auto-computed if None
|
|
2336
|
+
detrend: Detrend method ('none', 'mean', or 'linear')
|
|
2337
|
+
return_phase: If True, also return phase spectrum
|
|
2338
|
+
|
|
2339
|
+
Returns:
|
|
2340
|
+
Tuple of (frequencies, magnitudes_db) or (frequencies, magnitudes_db, phase)
|
|
2341
|
+
"""
|
|
2342
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
2343
|
+
|
|
2344
|
+
# Convert to WaveformTrace for functional API
|
|
2345
|
+
trace = WaveformTrace(data=data, metadata=TraceMetadata(sample_rate=sample_rate))
|
|
2346
|
+
|
|
2347
|
+
# Call functional API (always returns dB)
|
|
2348
|
+
return fft(trace, window=window, nfft=nfft, detrend=detrend, return_phase=return_phase)
|
|
2349
|
+
|
|
2350
|
+
def thd(self, trace: WaveformTrace, fundamental_freq: float | None = None) -> MeasurementResult:
|
|
2351
|
+
"""Compute Total Harmonic Distortion."""
|
|
2352
|
+
return thd(trace)
|
|
2353
|
+
|
|
2354
|
+
def snr(self, trace: WaveformTrace, fundamental_freq: float | None = None) -> MeasurementResult:
|
|
2355
|
+
"""Compute Signal-to-Noise Ratio."""
|
|
2356
|
+
return snr(trace)
|
|
2357
|
+
|
|
2358
|
+
def sinad(
|
|
2359
|
+
self, trace: WaveformTrace, fundamental_freq: float | None = None
|
|
2360
|
+
) -> MeasurementResult:
|
|
2361
|
+
"""Compute SINAD (Signal-to-Noise-And-Distortion)."""
|
|
2362
|
+
return sinad(trace)
|
|
2363
|
+
|
|
2364
|
+
def enob(
|
|
2365
|
+
self, trace: WaveformTrace, fundamental_freq: float | None = None
|
|
2366
|
+
) -> MeasurementResult:
|
|
2367
|
+
"""Compute Effective Number of Bits."""
|
|
2368
|
+
return enob(trace)
|
|
2369
|
+
|
|
2370
|
+
def sfdr(
|
|
2371
|
+
self, trace: WaveformTrace, fundamental_freq: float | None = None
|
|
2372
|
+
) -> MeasurementResult:
|
|
2373
|
+
"""Compute Spurious-Free Dynamic Range."""
|
|
2374
|
+
return sfdr(trace)
|
|
2375
|
+
|
|
2376
|
+
|
|
1939
2377
|
__all__ = [
|
|
2378
|
+
"SpectralAnalyzer",
|
|
1940
2379
|
"bartlett_psd",
|
|
1941
2380
|
"clear_fft_cache",
|
|
1942
2381
|
"configure_fft_cache",
|
|
1943
2382
|
"cwt",
|
|
1944
2383
|
"dwt",
|
|
1945
2384
|
"enob",
|
|
2385
|
+
"extract_harmonics",
|
|
1946
2386
|
"fft",
|
|
1947
2387
|
"fft_chunked",
|
|
2388
|
+
"find_peaks",
|
|
1948
2389
|
"get_fft_cache_stats",
|
|
2390
|
+
"group_delay",
|
|
1949
2391
|
"hilbert_transform",
|
|
1950
2392
|
"idwt",
|
|
2393
|
+
"measure",
|
|
1951
2394
|
"mfcc",
|
|
1952
2395
|
"periodogram",
|
|
2396
|
+
"phase_spectrum",
|
|
1953
2397
|
"psd",
|
|
1954
2398
|
"psd_chunked",
|
|
1955
2399
|
"sfdr",
|