oscura 0.8.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/patterns/__init__.py +66 -0
- 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/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +164 -73
- 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/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/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/types.py +232 -239
- 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/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- 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 +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- 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 +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- 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.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.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,7 +646,7 @@ def thd(
|
|
|
645
646
|
window: str = "hann",
|
|
646
647
|
nfft: int | None = None,
|
|
647
648
|
return_db: bool = True,
|
|
648
|
-
) ->
|
|
649
|
+
) -> MeasurementResult:
|
|
649
650
|
"""Compute Total Harmonic Distortion per IEEE 1241-2010.
|
|
650
651
|
|
|
651
652
|
THD is defined as the ratio of RMS harmonic power to fundamental amplitude:
|
|
@@ -659,17 +660,15 @@ def thd(
|
|
|
659
660
|
window: Window function for FFT.
|
|
660
661
|
nfft: FFT length. If None, uses data length (no zero-padding) to
|
|
661
662
|
preserve coherent sampling per IEEE 1241-2010.
|
|
662
|
-
return_db:
|
|
663
|
+
return_db: Ignored (always returns percentage, with dB in metadata if needed).
|
|
663
664
|
|
|
664
665
|
Returns:
|
|
665
|
-
|
|
666
|
-
Always non-negative in percentage form.
|
|
666
|
+
MeasurementResult with THD as percentage, or inapplicable if cannot compute.
|
|
667
667
|
|
|
668
668
|
Example:
|
|
669
|
-
>>>
|
|
670
|
-
>>>
|
|
671
|
-
|
|
672
|
-
>>> assert thd_pct >= 0, "THD percentage must be non-negative"
|
|
669
|
+
>>> result = thd(trace)
|
|
670
|
+
>>> if result["applicable"]:
|
|
671
|
+
... print(f"THD: {result['display']}")
|
|
673
672
|
|
|
674
673
|
References:
|
|
675
674
|
IEEE 1241-2010 Section 4.1.4.2
|
|
@@ -688,13 +687,13 @@ def thd(
|
|
|
688
687
|
_fund_idx, fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
689
688
|
|
|
690
689
|
if fund_mag == 0 or fund_freq == 0:
|
|
691
|
-
return
|
|
690
|
+
return make_inapplicable("%", "No fundamental frequency detected")
|
|
692
691
|
|
|
693
692
|
# Find harmonic frequencies (2*f0, 3*f0, ..., n*f0)
|
|
694
693
|
harmonic_indices = _find_harmonic_indices(freq, fund_freq, n_harmonics)
|
|
695
694
|
|
|
696
695
|
if len(harmonic_indices) == 0:
|
|
697
|
-
return 0.0
|
|
696
|
+
return make_measurement(0.0, "%")
|
|
698
697
|
|
|
699
698
|
# Compute total harmonic power: sum of squared magnitudes
|
|
700
699
|
harmonic_power = sum(magnitude[i] ** 2 for i in harmonic_indices)
|
|
@@ -710,13 +709,11 @@ def thd(
|
|
|
710
709
|
f"Fundamental: {fund_mag:.6f}, Harmonic power: {harmonic_power:.6f}"
|
|
711
710
|
)
|
|
712
711
|
|
|
713
|
-
if
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
# Return as percentage
|
|
719
|
-
return float(thd_ratio * 100)
|
|
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), "%")
|
|
720
717
|
|
|
721
718
|
|
|
722
719
|
def snr(
|
|
@@ -725,7 +722,7 @@ def snr(
|
|
|
725
722
|
n_harmonics: int = 10,
|
|
726
723
|
window: str = "hann",
|
|
727
724
|
nfft: int | None = None,
|
|
728
|
-
) ->
|
|
725
|
+
) -> MeasurementResult:
|
|
729
726
|
"""Compute Signal-to-Noise Ratio.
|
|
730
727
|
|
|
731
728
|
SNR is the ratio of signal power to noise power, excluding harmonics.
|
|
@@ -738,11 +735,12 @@ def snr(
|
|
|
738
735
|
preserve coherent sampling per IEEE 1241-2010.
|
|
739
736
|
|
|
740
737
|
Returns:
|
|
741
|
-
SNR in dB.
|
|
738
|
+
MeasurementResult with SNR in dB, or inapplicable if cannot compute.
|
|
742
739
|
|
|
743
740
|
Example:
|
|
744
|
-
>>>
|
|
745
|
-
>>>
|
|
741
|
+
>>> result = snr(trace)
|
|
742
|
+
>>> if result["applicable"]:
|
|
743
|
+
... print(f"SNR: {result['display']}")
|
|
746
744
|
|
|
747
745
|
References:
|
|
748
746
|
IEEE 1241-2010 Section 4.1.4.1
|
|
@@ -754,7 +752,7 @@ def snr(
|
|
|
754
752
|
fund_idx, fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
755
753
|
|
|
756
754
|
if fund_mag == 0 or fund_freq == 0:
|
|
757
|
-
return
|
|
755
|
+
return make_inapplicable("dB", "No fundamental frequency detected")
|
|
758
756
|
|
|
759
757
|
harmonic_indices = _find_harmonic_indices(freq, fund_freq, n_harmonics)
|
|
760
758
|
exclude_indices = _build_exclusion_set(fund_idx, harmonic_indices, len(magnitude))
|
|
@@ -763,9 +761,9 @@ def snr(
|
|
|
763
761
|
noise_power = _compute_noise_power(magnitude, exclude_indices)
|
|
764
762
|
|
|
765
763
|
if noise_power <= 0:
|
|
766
|
-
return
|
|
764
|
+
return make_inapplicable("dB", "No noise detected (perfect signal)")
|
|
767
765
|
|
|
768
|
-
return float(10 * np.log10(signal_power / noise_power))
|
|
766
|
+
return make_measurement(float(10 * np.log10(signal_power / noise_power)), "dB")
|
|
769
767
|
|
|
770
768
|
|
|
771
769
|
def _compute_magnitude_spectrum(
|
|
@@ -856,7 +854,7 @@ def sinad(
|
|
|
856
854
|
*,
|
|
857
855
|
window: str = "hann",
|
|
858
856
|
nfft: int | None = None,
|
|
859
|
-
) ->
|
|
857
|
+
) -> MeasurementResult:
|
|
860
858
|
"""Compute Signal-to-Noise and Distortion ratio.
|
|
861
859
|
|
|
862
860
|
SINAD is the ratio of signal power to noise plus distortion power.
|
|
@@ -868,11 +866,12 @@ def sinad(
|
|
|
868
866
|
preserve coherent sampling per IEEE 1241-2010.
|
|
869
867
|
|
|
870
868
|
Returns:
|
|
871
|
-
SINAD in dB.
|
|
869
|
+
MeasurementResult with SINAD in dB, or inapplicable if cannot compute.
|
|
872
870
|
|
|
873
871
|
Example:
|
|
874
|
-
>>>
|
|
875
|
-
>>>
|
|
872
|
+
>>> result = sinad(trace)
|
|
873
|
+
>>> if result["applicable"]:
|
|
874
|
+
... print(f"SINAD: {result['display']}")
|
|
876
875
|
|
|
877
876
|
References:
|
|
878
877
|
IEEE 1241-2010 Section 4.1.4.3
|
|
@@ -889,7 +888,7 @@ def sinad(
|
|
|
889
888
|
fund_idx, _fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
890
889
|
|
|
891
890
|
if fund_mag == 0:
|
|
892
|
-
return
|
|
891
|
+
return make_inapplicable("dB", "No fundamental frequency detected")
|
|
893
892
|
|
|
894
893
|
# Signal power: use 3-bin window around fundamental to capture spectral leakage
|
|
895
894
|
signal_power = 0.0
|
|
@@ -905,10 +904,10 @@ def sinad(
|
|
|
905
904
|
nad_power = total_power - signal_power
|
|
906
905
|
|
|
907
906
|
if nad_power <= 0:
|
|
908
|
-
return
|
|
907
|
+
return make_inapplicable("dB", "No noise/distortion detected (perfect signal)")
|
|
909
908
|
|
|
910
909
|
sinad_ratio = signal_power / nad_power
|
|
911
|
-
return float(10 * np.log10(sinad_ratio))
|
|
910
|
+
return make_measurement(float(10 * np.log10(sinad_ratio)), "dB")
|
|
912
911
|
|
|
913
912
|
|
|
914
913
|
def enob(
|
|
@@ -916,7 +915,7 @@ def enob(
|
|
|
916
915
|
*,
|
|
917
916
|
window: str = "hann",
|
|
918
917
|
nfft: int | None = None,
|
|
919
|
-
) ->
|
|
918
|
+
) -> MeasurementResult:
|
|
920
919
|
"""Compute Effective Number of Bits from SINAD.
|
|
921
920
|
|
|
922
921
|
ENOB = (SINAD - 1.76) / 6.02
|
|
@@ -927,21 +926,26 @@ def enob(
|
|
|
927
926
|
nfft: FFT length.
|
|
928
927
|
|
|
929
928
|
Returns:
|
|
930
|
-
ENOB in bits, or
|
|
929
|
+
MeasurementResult with ENOB in bits, or inapplicable if SINAD is invalid.
|
|
931
930
|
|
|
932
931
|
Example:
|
|
933
|
-
>>>
|
|
934
|
-
>>>
|
|
932
|
+
>>> result = enob(trace)
|
|
933
|
+
>>> if result["applicable"]:
|
|
934
|
+
... print(f"ENOB: {result['display']}")
|
|
935
935
|
|
|
936
936
|
References:
|
|
937
937
|
IEEE 1241-2010 Section 4.1.4.4
|
|
938
938
|
"""
|
|
939
|
-
|
|
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")
|
|
940
943
|
|
|
941
|
-
|
|
942
|
-
|
|
944
|
+
sinad_db = sinad_result["value"]
|
|
945
|
+
if sinad_db <= 0:
|
|
946
|
+
return make_inapplicable("", "SINAD too low (≤0 dB)")
|
|
943
947
|
|
|
944
|
-
return float((sinad_db - 1.76) / 6.02)
|
|
948
|
+
return make_measurement(float((sinad_db - 1.76) / 6.02), "")
|
|
945
949
|
|
|
946
950
|
|
|
947
951
|
def sfdr(
|
|
@@ -949,7 +953,7 @@ def sfdr(
|
|
|
949
953
|
*,
|
|
950
954
|
window: str = "hann",
|
|
951
955
|
nfft: int | None = None,
|
|
952
|
-
) ->
|
|
956
|
+
) -> MeasurementResult:
|
|
953
957
|
"""Compute Spurious-Free Dynamic Range.
|
|
954
958
|
|
|
955
959
|
SFDR is the ratio of fundamental to largest spurious component.
|
|
@@ -961,11 +965,13 @@ def sfdr(
|
|
|
961
965
|
preserve coherent sampling per IEEE 1241-2010.
|
|
962
966
|
|
|
963
967
|
Returns:
|
|
964
|
-
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.
|
|
965
970
|
|
|
966
971
|
Example:
|
|
967
|
-
>>>
|
|
968
|
-
>>>
|
|
972
|
+
>>> result = sfdr(trace)
|
|
973
|
+
>>> if result["applicable"]:
|
|
974
|
+
... print(f"SFDR: {result['display']}")
|
|
969
975
|
|
|
970
976
|
References:
|
|
971
977
|
IEEE 1241-2010 Section 4.1.4.5
|
|
@@ -982,7 +988,7 @@ def sfdr(
|
|
|
982
988
|
fund_idx, _fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
983
989
|
|
|
984
990
|
if fund_mag == 0:
|
|
985
|
-
return
|
|
991
|
+
return make_inapplicable("dB", "No fundamental frequency detected")
|
|
986
992
|
|
|
987
993
|
# Create mask for spurs (exclude fundamental and DC)
|
|
988
994
|
spur_mask = np.ones(len(magnitude), dtype=bool)
|
|
@@ -1001,15 +1007,15 @@ def sfdr(
|
|
|
1001
1007
|
# Find largest spur
|
|
1002
1008
|
spur_magnitudes = magnitude[spur_mask]
|
|
1003
1009
|
if len(spur_magnitudes) == 0:
|
|
1004
|
-
return
|
|
1010
|
+
return make_inapplicable("dB", "No spurs detected (clean spectrum)")
|
|
1005
1011
|
|
|
1006
1012
|
max_spur = np.max(spur_magnitudes)
|
|
1007
1013
|
|
|
1008
1014
|
if max_spur <= 0:
|
|
1009
|
-
return
|
|
1015
|
+
return make_inapplicable("dB", "No valid spurs detected")
|
|
1010
1016
|
|
|
1011
1017
|
sfdr_ratio = fund_mag / max_spur
|
|
1012
|
-
return float(20 * np.log10(sfdr_ratio))
|
|
1018
|
+
return make_measurement(float(20 * np.log10(sfdr_ratio)), "dB")
|
|
1013
1019
|
|
|
1014
1020
|
|
|
1015
1021
|
def hilbert_transform(
|
|
@@ -1029,7 +1035,7 @@ def hilbert_transform(
|
|
|
1029
1035
|
|
|
1030
1036
|
Example:
|
|
1031
1037
|
>>> envelope, phase, inst_freq = hilbert_transform(trace)
|
|
1032
|
-
>>> plt.plot(trace.
|
|
1038
|
+
>>> plt.plot(trace.time, envelope)
|
|
1033
1039
|
|
|
1034
1040
|
References:
|
|
1035
1041
|
Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time
|
|
@@ -1086,7 +1092,7 @@ def cwt(
|
|
|
1086
1092
|
|
|
1087
1093
|
Example:
|
|
1088
1094
|
>>> scales, freqs, coef = cwt(trace, wavelet="morlet")
|
|
1089
|
-
>>> plt.pcolormesh(trace.
|
|
1095
|
+
>>> plt.pcolormesh(trace.time, freqs, np.abs(coef))
|
|
1090
1096
|
>>> plt.ylabel("Frequency (Hz)")
|
|
1091
1097
|
|
|
1092
1098
|
References:
|
|
@@ -2204,16 +2210,17 @@ def measure(
|
|
|
2204
2210
|
"""Compute multiple spectral measurements with consistent format.
|
|
2205
2211
|
|
|
2206
2212
|
Unified function for computing spectral quality metrics following IEEE 1241-2010.
|
|
2207
|
-
|
|
2213
|
+
Returns MeasurementResult format with applicability tracking.
|
|
2208
2214
|
|
|
2209
2215
|
Args:
|
|
2210
2216
|
trace: Input waveform trace.
|
|
2211
2217
|
parameters: List of measurement names to compute. If None, compute all.
|
|
2212
2218
|
Valid names: thd, snr, sinad, enob, sfdr, dominant_freq
|
|
2213
|
-
include_units: If True, return
|
|
2219
|
+
include_units: If True, return MeasurementResult format. If False, return flat values.
|
|
2214
2220
|
|
|
2215
2221
|
Returns:
|
|
2216
|
-
Dictionary mapping measurement names to
|
|
2222
|
+
Dictionary mapping measurement names to MeasurementResults (if include_units=True)
|
|
2223
|
+
or raw values (if include_units=False).
|
|
2217
2224
|
|
|
2218
2225
|
Raises:
|
|
2219
2226
|
InsufficientDataError: If trace is too short for analysis.
|
|
@@ -2222,26 +2229,26 @@ def measure(
|
|
|
2222
2229
|
Example:
|
|
2223
2230
|
>>> from oscura.analyzers.waveform.spectral import measure
|
|
2224
2231
|
>>> results = measure(trace)
|
|
2225
|
-
>>>
|
|
2226
|
-
|
|
2232
|
+
>>> if results['thd']['applicable']:
|
|
2233
|
+
... print(f"THD: {results['thd']['display']}")
|
|
2227
2234
|
|
|
2228
2235
|
>>> # Get specific measurements only
|
|
2229
2236
|
>>> results = measure(trace, parameters=["thd", "snr"])
|
|
2230
2237
|
|
|
2231
|
-
>>> # Get flat values
|
|
2238
|
+
>>> # Get flat values (legacy compatibility)
|
|
2232
2239
|
>>> results = measure(trace, include_units=False)
|
|
2233
|
-
>>> thd_value = results["thd"] #
|
|
2240
|
+
>>> thd_value = results["thd"] # float or np.nan
|
|
2234
2241
|
|
|
2235
2242
|
References:
|
|
2236
2243
|
IEEE 1241-2010: ADC Terminology and Test Methods
|
|
2237
2244
|
"""
|
|
2238
|
-
# Define all available spectral measurements
|
|
2245
|
+
# Define all available spectral measurements
|
|
2239
2246
|
all_measurements = {
|
|
2240
|
-
"thd":
|
|
2241
|
-
"snr":
|
|
2242
|
-
"sinad":
|
|
2243
|
-
"enob":
|
|
2244
|
-
"sfdr":
|
|
2247
|
+
"thd": thd,
|
|
2248
|
+
"snr": snr,
|
|
2249
|
+
"sinad": sinad,
|
|
2250
|
+
"enob": enob,
|
|
2251
|
+
"sfdr": sfdr,
|
|
2245
2252
|
}
|
|
2246
2253
|
|
|
2247
2254
|
# Select requested measurements or all
|
|
@@ -2252,16 +2259,26 @@ def measure(
|
|
|
2252
2259
|
|
|
2253
2260
|
results: dict[str, Any] = {}
|
|
2254
2261
|
|
|
2255
|
-
for name,
|
|
2262
|
+
for name, func in selected.items():
|
|
2256
2263
|
try:
|
|
2257
|
-
|
|
2258
|
-
except Exception:
|
|
2259
|
-
value = np.nan
|
|
2264
|
+
measurement_result = func(trace) # type: ignore[operator]
|
|
2260
2265
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
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
|
|
2265
2282
|
|
|
2266
2283
|
# Add dominant frequency if requested or if computing all
|
|
2267
2284
|
if parameters is None or "dominant_freq" in parameters:
|
|
@@ -2272,19 +2289,93 @@ def measure(
|
|
|
2272
2289
|
dominant_freq_value = float(freq[dominant_idx])
|
|
2273
2290
|
|
|
2274
2291
|
if include_units:
|
|
2275
|
-
results["dominant_freq"] =
|
|
2292
|
+
results["dominant_freq"] = make_measurement(dominant_freq_value, "Hz")
|
|
2276
2293
|
else:
|
|
2277
2294
|
results["dominant_freq"] = dominant_freq_value
|
|
2278
2295
|
except Exception:
|
|
2279
2296
|
if include_units:
|
|
2280
|
-
results["dominant_freq"] =
|
|
2297
|
+
results["dominant_freq"] = make_inapplicable("Hz", "FFT failed")
|
|
2281
2298
|
else:
|
|
2282
2299
|
results["dominant_freq"] = np.nan
|
|
2283
2300
|
|
|
2284
2301
|
return results
|
|
2285
2302
|
|
|
2286
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
|
+
|
|
2287
2377
|
__all__ = [
|
|
2378
|
+
"SpectralAnalyzer",
|
|
2288
2379
|
"bartlett_psd",
|
|
2289
2380
|
"clear_fft_cache",
|
|
2290
2381
|
"configure_fft_cache",
|
oscura/api/dsl/commands.py
CHANGED
|
@@ -147,18 +147,27 @@ def cmd_plot(trace: Any, **options: Any) -> None:
|
|
|
147
147
|
OscuraError: If plotting fails
|
|
148
148
|
"""
|
|
149
149
|
try:
|
|
150
|
-
|
|
150
|
+
import matplotlib.pyplot as plt
|
|
151
|
+
|
|
152
|
+
from oscura.visualization import plot_waveform
|
|
151
153
|
|
|
152
154
|
title = options.get("title", "Trace Plot")
|
|
153
155
|
annotate = options.get("annotate")
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
fig, ax = plt.subplots()
|
|
158
|
+
plot_waveform(ax, trace)
|
|
159
|
+
ax.set_title(title)
|
|
156
160
|
|
|
157
161
|
if annotate:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
ax.text(
|
|
163
|
+
0.5,
|
|
164
|
+
0.95,
|
|
165
|
+
annotate,
|
|
166
|
+
transform=ax.transAxes,
|
|
167
|
+
ha="center",
|
|
168
|
+
va="top",
|
|
169
|
+
bbox={"boxstyle": "round", "facecolor": "wheat"},
|
|
170
|
+
)
|
|
162
171
|
|
|
163
172
|
plt.show()
|
|
164
173
|
|