oscura 0.7.0__py3-none-any.whl → 0.8.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 +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 +28 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +149 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +145 -23
- oscura/analyzers/waveform/spectral.py +361 -8
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/dtc/data.json +102 -17
- oscura/core/config/loader.py +0 -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 +108 -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 +279 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/visualization.py +542 -0
- oscura/visualization/__init__.py +2 -1
- oscura/visualization/batch.py +521 -0
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/waveform.py +783 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/METADATA +1 -1
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/RECORD +40 -29
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -646,9 +646,12 @@ def thd(
|
|
|
646
646
|
nfft: int | None = None,
|
|
647
647
|
return_db: bool = True,
|
|
648
648
|
) -> float:
|
|
649
|
-
"""Compute Total Harmonic Distortion.
|
|
649
|
+
"""Compute Total Harmonic Distortion per IEEE 1241-2010.
|
|
650
650
|
|
|
651
|
-
THD is the ratio of harmonic power to fundamental
|
|
651
|
+
THD is defined as the ratio of RMS harmonic power to fundamental amplitude:
|
|
652
|
+
THD = sqrt(sum(A_harmonics^2)) / A_fundamental
|
|
653
|
+
|
|
654
|
+
where harmonics are the 2nd, 3rd, ..., nth harmonic frequencies.
|
|
652
655
|
|
|
653
656
|
Args:
|
|
654
657
|
trace: Input waveform trace.
|
|
@@ -659,12 +662,14 @@ def thd(
|
|
|
659
662
|
return_db: If True, return in dB. If False, return percentage.
|
|
660
663
|
|
|
661
664
|
Returns:
|
|
662
|
-
THD in dB or percentage.
|
|
665
|
+
THD in dB (if return_db=True) or percentage (if return_db=False).
|
|
666
|
+
Always non-negative in percentage form.
|
|
663
667
|
|
|
664
668
|
Example:
|
|
665
669
|
>>> thd_db = thd(trace)
|
|
666
670
|
>>> thd_pct = thd(trace, return_db=False)
|
|
667
671
|
>>> print(f"THD: {thd_db:.1f} dB ({thd_pct:.2f}%)")
|
|
672
|
+
>>> assert thd_pct >= 0, "THD percentage must be non-negative"
|
|
668
673
|
|
|
669
674
|
References:
|
|
670
675
|
IEEE 1241-2010 Section 4.1.4.2
|
|
@@ -676,32 +681,41 @@ def thd(
|
|
|
676
681
|
result = fft(trace, window=window, nfft=nfft, detrend="mean")
|
|
677
682
|
freq, mag_db = result[0], result[1]
|
|
678
683
|
|
|
679
|
-
# Convert to linear
|
|
684
|
+
# Convert to linear magnitude
|
|
680
685
|
magnitude = 10 ** (mag_db / 20)
|
|
681
686
|
|
|
682
|
-
# Find fundamental
|
|
687
|
+
# Find fundamental (strongest peak above DC)
|
|
683
688
|
_fund_idx, fund_freq, fund_mag = _find_fundamental(freq, magnitude)
|
|
684
689
|
|
|
685
690
|
if fund_mag == 0 or fund_freq == 0:
|
|
686
691
|
return np.nan
|
|
687
692
|
|
|
688
|
-
# Find
|
|
693
|
+
# Find harmonic frequencies (2*f0, 3*f0, ..., n*f0)
|
|
689
694
|
harmonic_indices = _find_harmonic_indices(freq, fund_freq, n_harmonics)
|
|
690
695
|
|
|
691
696
|
if len(harmonic_indices) == 0:
|
|
692
697
|
return 0.0 if not return_db else -np.inf
|
|
693
698
|
|
|
694
|
-
#
|
|
699
|
+
# Compute total harmonic power: sum of squared magnitudes
|
|
695
700
|
harmonic_power = sum(magnitude[i] ** 2 for i in harmonic_indices)
|
|
696
701
|
|
|
697
|
-
# THD
|
|
702
|
+
# THD = sqrt(sum(harmonic_power)) / fundamental_amplitude
|
|
703
|
+
# This is the IEEE 1241-2010 definition
|
|
698
704
|
thd_ratio = np.sqrt(harmonic_power) / fund_mag
|
|
699
705
|
|
|
706
|
+
# Validate: THD must always be non-negative
|
|
707
|
+
if thd_ratio < 0:
|
|
708
|
+
raise ValueError(
|
|
709
|
+
f"THD ratio is negative ({thd_ratio:.6f}), indicating a calculation error. "
|
|
710
|
+
f"Fundamental: {fund_mag:.6f}, Harmonic power: {harmonic_power:.6f}"
|
|
711
|
+
)
|
|
712
|
+
|
|
700
713
|
if return_db:
|
|
701
714
|
if thd_ratio <= 0:
|
|
702
715
|
return -np.inf
|
|
703
716
|
return float(20 * np.log10(thd_ratio))
|
|
704
717
|
else:
|
|
718
|
+
# Return as percentage
|
|
705
719
|
return float(thd_ratio * 100)
|
|
706
720
|
|
|
707
721
|
|
|
@@ -1936,6 +1950,340 @@ def _extract_fft_segment(
|
|
|
1936
1950
|
return segment
|
|
1937
1951
|
|
|
1938
1952
|
|
|
1953
|
+
def find_peaks(
|
|
1954
|
+
trace: WaveformTrace,
|
|
1955
|
+
*,
|
|
1956
|
+
window: str = "hann",
|
|
1957
|
+
nfft: int | None = None,
|
|
1958
|
+
threshold_db: float = -60.0,
|
|
1959
|
+
min_distance: int = 5,
|
|
1960
|
+
n_peaks: int | None = None,
|
|
1961
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
1962
|
+
"""Find spectral peaks in FFT magnitude spectrum.
|
|
1963
|
+
|
|
1964
|
+
Identifies prominent frequency components above a threshold with
|
|
1965
|
+
minimum spacing between peaks.
|
|
1966
|
+
|
|
1967
|
+
Args:
|
|
1968
|
+
trace: Input waveform trace.
|
|
1969
|
+
window: Window function for FFT.
|
|
1970
|
+
nfft: FFT length.
|
|
1971
|
+
threshold_db: Minimum peak magnitude in dB (relative to max).
|
|
1972
|
+
min_distance: Minimum bin spacing between peaks.
|
|
1973
|
+
n_peaks: Maximum number of peaks to return (None = all).
|
|
1974
|
+
|
|
1975
|
+
Returns:
|
|
1976
|
+
Dictionary with keys:
|
|
1977
|
+
- "frequencies": Peak frequencies in Hz
|
|
1978
|
+
- "magnitudes_db": Peak magnitudes in dB
|
|
1979
|
+
- "indices": FFT bin indices of peaks
|
|
1980
|
+
|
|
1981
|
+
Example:
|
|
1982
|
+
>>> peaks = find_peaks(trace, threshold_db=-40, n_peaks=10)
|
|
1983
|
+
>>> print(f"Found {len(peaks['frequencies'])} peaks")
|
|
1984
|
+
>>> for freq, mag in zip(peaks['frequencies'], peaks['magnitudes_db']):
|
|
1985
|
+
... print(f" {freq:.1f} Hz: {mag:.1f} dB")
|
|
1986
|
+
|
|
1987
|
+
References:
|
|
1988
|
+
IEEE 1241-2010 Section 4.1.5 - Spectral Analysis
|
|
1989
|
+
"""
|
|
1990
|
+
from scipy.signal import find_peaks as sp_find_peaks
|
|
1991
|
+
|
|
1992
|
+
result = fft(trace, window=window, nfft=nfft)
|
|
1993
|
+
freq, mag_db = result[0], result[1]
|
|
1994
|
+
|
|
1995
|
+
# Find peaks using scipy
|
|
1996
|
+
# Convert threshold from dB relative to max
|
|
1997
|
+
max_mag_db = np.max(mag_db)
|
|
1998
|
+
abs_threshold = max_mag_db + threshold_db # threshold_db is negative
|
|
1999
|
+
|
|
2000
|
+
peak_indices, _ = sp_find_peaks(mag_db, height=abs_threshold, distance=min_distance)
|
|
2001
|
+
|
|
2002
|
+
# Sort by magnitude (strongest first)
|
|
2003
|
+
sorted_idx = np.argsort(mag_db[peak_indices])[::-1]
|
|
2004
|
+
peak_indices = peak_indices[sorted_idx]
|
|
2005
|
+
|
|
2006
|
+
# Limit number of peaks
|
|
2007
|
+
if n_peaks is not None:
|
|
2008
|
+
peak_indices = peak_indices[:n_peaks]
|
|
2009
|
+
|
|
2010
|
+
return {
|
|
2011
|
+
"frequencies": freq[peak_indices],
|
|
2012
|
+
"magnitudes_db": mag_db[peak_indices],
|
|
2013
|
+
"indices": peak_indices.astype(np.float64),
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
|
|
2017
|
+
def extract_harmonics(
|
|
2018
|
+
trace: WaveformTrace,
|
|
2019
|
+
*,
|
|
2020
|
+
fundamental_freq: float | None = None,
|
|
2021
|
+
n_harmonics: int = 10,
|
|
2022
|
+
window: str = "hann",
|
|
2023
|
+
nfft: int | None = None,
|
|
2024
|
+
search_width_hz: float = 50.0,
|
|
2025
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
2026
|
+
"""Extract harmonic frequencies and amplitudes from spectrum.
|
|
2027
|
+
|
|
2028
|
+
Identifies fundamental frequency (if not provided) and extracts
|
|
2029
|
+
harmonic series with frequencies and amplitudes.
|
|
2030
|
+
|
|
2031
|
+
Args:
|
|
2032
|
+
trace: Input waveform trace.
|
|
2033
|
+
fundamental_freq: Fundamental frequency in Hz. If None, auto-detected.
|
|
2034
|
+
n_harmonics: Number of harmonics to extract (excluding fundamental).
|
|
2035
|
+
window: Window function for FFT.
|
|
2036
|
+
nfft: FFT length.
|
|
2037
|
+
search_width_hz: Search range around expected harmonic frequencies.
|
|
2038
|
+
|
|
2039
|
+
Returns:
|
|
2040
|
+
Dictionary with keys:
|
|
2041
|
+
- "frequencies": Harmonic frequencies [f0, 2f0, 3f0, ...]
|
|
2042
|
+
- "amplitudes": Harmonic amplitudes (linear scale)
|
|
2043
|
+
- "amplitudes_db": Harmonic amplitudes in dB
|
|
2044
|
+
- "fundamental_freq": Detected or provided fundamental frequency
|
|
2045
|
+
|
|
2046
|
+
Example:
|
|
2047
|
+
>>> harmonics = extract_harmonics(trace, n_harmonics=5)
|
|
2048
|
+
>>> f0 = harmonics["fundamental_freq"]
|
|
2049
|
+
>>> print(f"Fundamental: {f0:.1f} Hz")
|
|
2050
|
+
>>> for i, (freq, amp_db) in enumerate(
|
|
2051
|
+
... zip(harmonics["frequencies"], harmonics["amplitudes_db"]), 1
|
|
2052
|
+
... ):
|
|
2053
|
+
... print(f" H{i}: {freq:.1f} Hz at {amp_db:.1f} dB")
|
|
2054
|
+
|
|
2055
|
+
References:
|
|
2056
|
+
IEEE 1241-2010 Section 4.1.4.2 - Harmonic Analysis
|
|
2057
|
+
"""
|
|
2058
|
+
result = fft(trace, window=window, nfft=nfft)
|
|
2059
|
+
freq, mag_db = result[0], result[1]
|
|
2060
|
+
magnitude = 10 ** (mag_db / 20)
|
|
2061
|
+
|
|
2062
|
+
# Auto-detect fundamental if not provided
|
|
2063
|
+
if fundamental_freq is None:
|
|
2064
|
+
_fund_idx, fund_freq, _fund_mag = _find_fundamental(freq, magnitude)
|
|
2065
|
+
fundamental_freq = fund_freq
|
|
2066
|
+
|
|
2067
|
+
if fundamental_freq == 0:
|
|
2068
|
+
# Return empty result
|
|
2069
|
+
return {
|
|
2070
|
+
"frequencies": np.array([]),
|
|
2071
|
+
"amplitudes": np.array([]),
|
|
2072
|
+
"amplitudes_db": np.array([]),
|
|
2073
|
+
"fundamental_freq": np.array([0.0]),
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
# Extract harmonics
|
|
2077
|
+
harmonic_freqs = []
|
|
2078
|
+
harmonic_amps = []
|
|
2079
|
+
|
|
2080
|
+
for h in range(1, n_harmonics + 2): # Include fundamental (h=1)
|
|
2081
|
+
target_freq = h * fundamental_freq
|
|
2082
|
+
if target_freq > freq[-1]:
|
|
2083
|
+
break
|
|
2084
|
+
|
|
2085
|
+
# Search around expected frequency
|
|
2086
|
+
search_mask = np.abs(freq - target_freq) <= search_width_hz
|
|
2087
|
+
if not np.any(search_mask):
|
|
2088
|
+
continue
|
|
2089
|
+
|
|
2090
|
+
# Find peak in search region
|
|
2091
|
+
search_region_mag = magnitude.copy()
|
|
2092
|
+
search_region_mag[~search_mask] = 0
|
|
2093
|
+
peak_idx = np.argmax(search_region_mag)
|
|
2094
|
+
|
|
2095
|
+
harmonic_freqs.append(float(freq[peak_idx]))
|
|
2096
|
+
harmonic_amps.append(float(magnitude[peak_idx]))
|
|
2097
|
+
|
|
2098
|
+
harmonic_freqs_arr = np.array(harmonic_freqs)
|
|
2099
|
+
harmonic_amps_arr = np.array(harmonic_amps)
|
|
2100
|
+
harmonic_amps_db = 20 * np.log10(np.maximum(harmonic_amps_arr, 1e-20))
|
|
2101
|
+
|
|
2102
|
+
return {
|
|
2103
|
+
"frequencies": harmonic_freqs_arr,
|
|
2104
|
+
"amplitudes": harmonic_amps_arr,
|
|
2105
|
+
"amplitudes_db": harmonic_amps_db,
|
|
2106
|
+
"fundamental_freq": np.array([fundamental_freq]),
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
def phase_spectrum(
|
|
2111
|
+
trace: WaveformTrace,
|
|
2112
|
+
*,
|
|
2113
|
+
window: str = "hann",
|
|
2114
|
+
nfft: int | None = None,
|
|
2115
|
+
unwrap: bool = True,
|
|
2116
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
2117
|
+
"""Compute phase spectrum from FFT.
|
|
2118
|
+
|
|
2119
|
+
Extracts phase information from frequency domain representation.
|
|
2120
|
+
|
|
2121
|
+
Args:
|
|
2122
|
+
trace: Input waveform trace.
|
|
2123
|
+
window: Window function for FFT.
|
|
2124
|
+
nfft: FFT length.
|
|
2125
|
+
unwrap: If True, unwrap phase to remove 2π discontinuities.
|
|
2126
|
+
|
|
2127
|
+
Returns:
|
|
2128
|
+
(frequencies, phase) where phase is in radians.
|
|
2129
|
+
|
|
2130
|
+
Example:
|
|
2131
|
+
>>> freq, phase = phase_spectrum(trace)
|
|
2132
|
+
>>> plt.plot(freq, phase)
|
|
2133
|
+
>>> plt.xlabel("Frequency (Hz)")
|
|
2134
|
+
>>> plt.ylabel("Phase (radians)")
|
|
2135
|
+
|
|
2136
|
+
References:
|
|
2137
|
+
Oppenheim & Schafer (2009) - Discrete-Time Signal Processing
|
|
2138
|
+
"""
|
|
2139
|
+
result = fft(trace, window=window, nfft=nfft, return_phase=True)
|
|
2140
|
+
assert len(result) == 3, "Expected 3-tuple from fft with return_phase=True"
|
|
2141
|
+
freq, _mag_db, phase = result[0], result[1], result[2]
|
|
2142
|
+
|
|
2143
|
+
if unwrap:
|
|
2144
|
+
phase = np.unwrap(phase)
|
|
2145
|
+
|
|
2146
|
+
return freq, phase
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def group_delay(
|
|
2150
|
+
trace: WaveformTrace,
|
|
2151
|
+
*,
|
|
2152
|
+
window: str = "hann",
|
|
2153
|
+
nfft: int | None = None,
|
|
2154
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
2155
|
+
"""Compute group delay from phase spectrum.
|
|
2156
|
+
|
|
2157
|
+
Group delay is the negative derivative of phase with respect to
|
|
2158
|
+
frequency, representing signal delay at each frequency.
|
|
2159
|
+
|
|
2160
|
+
Args:
|
|
2161
|
+
trace: Input waveform trace.
|
|
2162
|
+
window: Window function for FFT.
|
|
2163
|
+
nfft: FFT length.
|
|
2164
|
+
|
|
2165
|
+
Returns:
|
|
2166
|
+
(frequencies, group_delay_samples) - Delay in samples at each frequency.
|
|
2167
|
+
|
|
2168
|
+
Example:
|
|
2169
|
+
>>> freq, gd = group_delay(trace)
|
|
2170
|
+
>>> plt.plot(freq, gd)
|
|
2171
|
+
>>> plt.xlabel("Frequency (Hz)")
|
|
2172
|
+
>>> plt.ylabel("Group Delay (samples)")
|
|
2173
|
+
|
|
2174
|
+
References:
|
|
2175
|
+
Oppenheim & Schafer (2009) Section 5.1.1
|
|
2176
|
+
"""
|
|
2177
|
+
freq, phase = phase_spectrum(trace, window=window, nfft=nfft, unwrap=True)
|
|
2178
|
+
|
|
2179
|
+
# Group delay = -dφ/dω
|
|
2180
|
+
# In discrete frequency: gd[i] ≈ -(φ[i+1] - φ[i]) / (ω[i+1] - ω[i])
|
|
2181
|
+
# Convert frequency to angular frequency: ω = 2π * f
|
|
2182
|
+
omega = 2 * np.pi * freq
|
|
2183
|
+
|
|
2184
|
+
# Compute derivative using central differences
|
|
2185
|
+
gd = np.zeros_like(phase)
|
|
2186
|
+
|
|
2187
|
+
# Central difference for interior points
|
|
2188
|
+
gd[1:-1] = -(phase[2:] - phase[:-2]) / (omega[2:] - omega[:-2])
|
|
2189
|
+
|
|
2190
|
+
# Forward/backward difference for boundaries
|
|
2191
|
+
if len(phase) > 1:
|
|
2192
|
+
gd[0] = -(phase[1] - phase[0]) / (omega[1] - omega[0])
|
|
2193
|
+
gd[-1] = -(phase[-1] - phase[-2]) / (omega[-1] - omega[-2])
|
|
2194
|
+
|
|
2195
|
+
return freq, gd
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
def measure(
|
|
2199
|
+
trace: WaveformTrace,
|
|
2200
|
+
*,
|
|
2201
|
+
parameters: list[str] | None = None,
|
|
2202
|
+
include_units: bool = True,
|
|
2203
|
+
) -> dict[str, Any]:
|
|
2204
|
+
"""Compute multiple spectral measurements with consistent format.
|
|
2205
|
+
|
|
2206
|
+
Unified function for computing spectral quality metrics following IEEE 1241-2010.
|
|
2207
|
+
Matches the API pattern of oscura.analyzers.waveform.measurements.measure().
|
|
2208
|
+
|
|
2209
|
+
Args:
|
|
2210
|
+
trace: Input waveform trace.
|
|
2211
|
+
parameters: List of measurement names to compute. If None, compute all.
|
|
2212
|
+
Valid names: thd, snr, sinad, enob, sfdr, dominant_freq
|
|
2213
|
+
include_units: If True, return {value, unit} dicts. If False, return flat values.
|
|
2214
|
+
|
|
2215
|
+
Returns:
|
|
2216
|
+
Dictionary mapping measurement names to values (with units if requested).
|
|
2217
|
+
|
|
2218
|
+
Raises:
|
|
2219
|
+
InsufficientDataError: If trace is too short for analysis.
|
|
2220
|
+
AnalysisError: If computation fails.
|
|
2221
|
+
|
|
2222
|
+
Example:
|
|
2223
|
+
>>> from oscura.analyzers.waveform.spectral import measure
|
|
2224
|
+
>>> results = measure(trace)
|
|
2225
|
+
>>> print(f"THD: {results['thd']['value']}{results['thd']['unit']}")
|
|
2226
|
+
>>> print(f"SNR: {results['snr']['value']} {results['snr']['unit']}")
|
|
2227
|
+
|
|
2228
|
+
>>> # Get specific measurements only
|
|
2229
|
+
>>> results = measure(trace, parameters=["thd", "snr"])
|
|
2230
|
+
|
|
2231
|
+
>>> # Get flat values without units
|
|
2232
|
+
>>> results = measure(trace, include_units=False)
|
|
2233
|
+
>>> thd_value = results["thd"] # Just the float
|
|
2234
|
+
|
|
2235
|
+
References:
|
|
2236
|
+
IEEE 1241-2010: ADC Terminology and Test Methods
|
|
2237
|
+
"""
|
|
2238
|
+
# Define all available spectral measurements with units
|
|
2239
|
+
all_measurements = {
|
|
2240
|
+
"thd": (thd, "%"),
|
|
2241
|
+
"snr": (snr, "dB"),
|
|
2242
|
+
"sinad": (sinad, "dB"),
|
|
2243
|
+
"enob": (enob, "bits"),
|
|
2244
|
+
"sfdr": (sfdr, "dB"),
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
# Select requested measurements or all
|
|
2248
|
+
if parameters is None:
|
|
2249
|
+
selected = all_measurements
|
|
2250
|
+
else:
|
|
2251
|
+
selected = {k: v for k, v in all_measurements.items() if k in parameters}
|
|
2252
|
+
|
|
2253
|
+
results: dict[str, Any] = {}
|
|
2254
|
+
|
|
2255
|
+
for name, (func, unit) in selected.items():
|
|
2256
|
+
try:
|
|
2257
|
+
value = func(trace) # type: ignore[operator]
|
|
2258
|
+
except Exception:
|
|
2259
|
+
value = np.nan
|
|
2260
|
+
|
|
2261
|
+
if include_units:
|
|
2262
|
+
results[name] = {"value": value, "unit": unit}
|
|
2263
|
+
else:
|
|
2264
|
+
results[name] = value
|
|
2265
|
+
|
|
2266
|
+
# Add dominant frequency if requested or if computing all
|
|
2267
|
+
if parameters is None or "dominant_freq" in parameters:
|
|
2268
|
+
try:
|
|
2269
|
+
fft_result = fft(trace, return_phase=False)
|
|
2270
|
+
freq, magnitude = fft_result[0], fft_result[1]
|
|
2271
|
+
dominant_idx = int(np.argmax(np.abs(magnitude[1:]))) + 1 # Skip DC
|
|
2272
|
+
dominant_freq_value = float(freq[dominant_idx])
|
|
2273
|
+
|
|
2274
|
+
if include_units:
|
|
2275
|
+
results["dominant_freq"] = {"value": dominant_freq_value, "unit": "Hz"}
|
|
2276
|
+
else:
|
|
2277
|
+
results["dominant_freq"] = dominant_freq_value
|
|
2278
|
+
except Exception:
|
|
2279
|
+
if include_units:
|
|
2280
|
+
results["dominant_freq"] = {"value": np.nan, "unit": "Hz"}
|
|
2281
|
+
else:
|
|
2282
|
+
results["dominant_freq"] = np.nan
|
|
2283
|
+
|
|
2284
|
+
return results
|
|
2285
|
+
|
|
2286
|
+
|
|
1939
2287
|
__all__ = [
|
|
1940
2288
|
"bartlett_psd",
|
|
1941
2289
|
"clear_fft_cache",
|
|
@@ -1943,13 +2291,18 @@ __all__ = [
|
|
|
1943
2291
|
"cwt",
|
|
1944
2292
|
"dwt",
|
|
1945
2293
|
"enob",
|
|
2294
|
+
"extract_harmonics",
|
|
1946
2295
|
"fft",
|
|
1947
2296
|
"fft_chunked",
|
|
2297
|
+
"find_peaks",
|
|
1948
2298
|
"get_fft_cache_stats",
|
|
2299
|
+
"group_delay",
|
|
1949
2300
|
"hilbert_transform",
|
|
1950
2301
|
"idwt",
|
|
2302
|
+
"measure",
|
|
1951
2303
|
"mfcc",
|
|
1952
2304
|
"periodogram",
|
|
2305
|
+
"phase_spectrum",
|
|
1953
2306
|
"psd",
|
|
1954
2307
|
"psd_chunked",
|
|
1955
2308
|
"sfdr",
|
oscura/automotive/__init__.py
CHANGED
oscura/automotive/dtc/data.json
CHANGED
|
@@ -266,7 +266,12 @@
|
|
|
266
266
|
"category": "Powertrain",
|
|
267
267
|
"severity": "High",
|
|
268
268
|
"system": "Throttle Control",
|
|
269
|
-
"possible_causes": [
|
|
269
|
+
"possible_causes": [
|
|
270
|
+
"TPS circuit shorted to voltage",
|
|
271
|
+
"Faulty TPS sensor",
|
|
272
|
+
"Wiring harness open",
|
|
273
|
+
"ECM problem"
|
|
274
|
+
]
|
|
270
275
|
},
|
|
271
276
|
"P0125": {
|
|
272
277
|
"code": "P0125",
|
|
@@ -860,7 +865,12 @@
|
|
|
860
865
|
"category": "Powertrain",
|
|
861
866
|
"severity": "Medium",
|
|
862
867
|
"system": "Emissions Control",
|
|
863
|
-
"possible_causes": [
|
|
868
|
+
"possible_causes": [
|
|
869
|
+
"EGR valve stuck closed",
|
|
870
|
+
"EGR passages clogged",
|
|
871
|
+
"Faulty EGR valve",
|
|
872
|
+
"Vacuum leak"
|
|
873
|
+
]
|
|
864
874
|
},
|
|
865
875
|
"P0401": {
|
|
866
876
|
"code": "P0401",
|
|
@@ -881,7 +891,12 @@
|
|
|
881
891
|
"category": "Powertrain",
|
|
882
892
|
"severity": "Medium",
|
|
883
893
|
"system": "Emissions Control",
|
|
884
|
-
"possible_causes": [
|
|
894
|
+
"possible_causes": [
|
|
895
|
+
"EGR valve stuck open",
|
|
896
|
+
"Faulty EGR valve",
|
|
897
|
+
"EGR vacuum solenoid fault",
|
|
898
|
+
"ECM problem"
|
|
899
|
+
]
|
|
885
900
|
},
|
|
886
901
|
"P0403": {
|
|
887
902
|
"code": "P0403",
|
|
@@ -943,7 +958,12 @@
|
|
|
943
958
|
"category": "Powertrain",
|
|
944
959
|
"severity": "Low",
|
|
945
960
|
"system": "Emissions Control",
|
|
946
|
-
"possible_causes": [
|
|
961
|
+
"possible_causes": [
|
|
962
|
+
"Loose or missing fuel cap",
|
|
963
|
+
"EVAP system leak",
|
|
964
|
+
"Faulty purge valve",
|
|
965
|
+
"Faulty vent valve"
|
|
966
|
+
]
|
|
947
967
|
},
|
|
948
968
|
"P0441": {
|
|
949
969
|
"code": "P0441",
|
|
@@ -1055,7 +1075,12 @@
|
|
|
1055
1075
|
"category": "Powertrain",
|
|
1056
1076
|
"severity": "Low",
|
|
1057
1077
|
"system": "Idle Control",
|
|
1058
|
-
"possible_causes": [
|
|
1078
|
+
"possible_causes": [
|
|
1079
|
+
"Vacuum leak",
|
|
1080
|
+
"IAC valve fault",
|
|
1081
|
+
"Dirty throttle body",
|
|
1082
|
+
"PCV valve problem"
|
|
1083
|
+
]
|
|
1059
1084
|
},
|
|
1060
1085
|
"P0507": {
|
|
1061
1086
|
"code": "P0507",
|
|
@@ -1063,7 +1088,12 @@
|
|
|
1063
1088
|
"category": "Powertrain",
|
|
1064
1089
|
"severity": "Low",
|
|
1065
1090
|
"system": "Idle Control",
|
|
1066
|
-
"possible_causes": [
|
|
1091
|
+
"possible_causes": [
|
|
1092
|
+
"Vacuum leak",
|
|
1093
|
+
"IAC valve stuck open",
|
|
1094
|
+
"PCV valve stuck open",
|
|
1095
|
+
"EVAP purge valve leaking"
|
|
1096
|
+
]
|
|
1067
1097
|
},
|
|
1068
1098
|
"P0600": {
|
|
1069
1099
|
"code": "P0600",
|
|
@@ -1097,7 +1127,12 @@
|
|
|
1097
1127
|
"category": "Powertrain",
|
|
1098
1128
|
"severity": "Critical",
|
|
1099
1129
|
"system": "Engine Control Module",
|
|
1100
|
-
"possible_causes": [
|
|
1130
|
+
"possible_causes": [
|
|
1131
|
+
"ECM not programmed",
|
|
1132
|
+
"ECM programming incomplete",
|
|
1133
|
+
"Wrong software version",
|
|
1134
|
+
"ECM fault"
|
|
1135
|
+
]
|
|
1101
1136
|
},
|
|
1102
1137
|
"P0603": {
|
|
1103
1138
|
"code": "P0603",
|
|
@@ -1170,7 +1205,12 @@
|
|
|
1170
1205
|
"category": "Powertrain",
|
|
1171
1206
|
"severity": "Medium",
|
|
1172
1207
|
"system": "Charging System",
|
|
1173
|
-
"possible_causes": [
|
|
1208
|
+
"possible_causes": [
|
|
1209
|
+
"Faulty alternator",
|
|
1210
|
+
"Wiring harness problem",
|
|
1211
|
+
"Poor electrical connection",
|
|
1212
|
+
"ECM fault"
|
|
1213
|
+
]
|
|
1174
1214
|
},
|
|
1175
1215
|
"P0625": {
|
|
1176
1216
|
"code": "P0625",
|
|
@@ -1243,7 +1283,12 @@
|
|
|
1243
1283
|
"category": "Powertrain",
|
|
1244
1284
|
"severity": "High",
|
|
1245
1285
|
"system": "Transmission",
|
|
1246
|
-
"possible_causes": [
|
|
1286
|
+
"possible_causes": [
|
|
1287
|
+
"Faulty input speed sensor",
|
|
1288
|
+
"Wiring harness problem",
|
|
1289
|
+
"Sensor reluctor damaged",
|
|
1290
|
+
"TCM fault"
|
|
1291
|
+
]
|
|
1247
1292
|
},
|
|
1248
1293
|
"P0720": {
|
|
1249
1294
|
"code": "P0720",
|
|
@@ -1446,7 +1491,12 @@
|
|
|
1446
1491
|
"category": "Chassis",
|
|
1447
1492
|
"severity": "High",
|
|
1448
1493
|
"system": "ABS",
|
|
1449
|
-
"possible_causes": [
|
|
1494
|
+
"possible_causes": [
|
|
1495
|
+
"Faulty valve relay",
|
|
1496
|
+
"Relay circuit problem",
|
|
1497
|
+
"ABS module fault",
|
|
1498
|
+
"Wiring harness issue"
|
|
1499
|
+
]
|
|
1450
1500
|
},
|
|
1451
1501
|
"C0161": {
|
|
1452
1502
|
"code": "C0161",
|
|
@@ -2156,7 +2206,12 @@
|
|
|
2156
2206
|
"category": "Body",
|
|
2157
2207
|
"severity": "Low",
|
|
2158
2208
|
"system": "Lighting System",
|
|
2159
|
-
"possible_causes": [
|
|
2209
|
+
"possible_causes": [
|
|
2210
|
+
"Burned out bulb",
|
|
2211
|
+
"Wiring harness problem",
|
|
2212
|
+
"Lamp socket corrosion",
|
|
2213
|
+
"BCM fault"
|
|
2214
|
+
]
|
|
2160
2215
|
},
|
|
2161
2216
|
"B0601": {
|
|
2162
2217
|
"code": "B0601",
|
|
@@ -2177,7 +2232,12 @@
|
|
|
2177
2232
|
"category": "Body",
|
|
2178
2233
|
"severity": "Low",
|
|
2179
2234
|
"system": "Lighting System",
|
|
2180
|
-
"possible_causes": [
|
|
2235
|
+
"possible_causes": [
|
|
2236
|
+
"Burned out turn signal bulb",
|
|
2237
|
+
"Wiring harness problem",
|
|
2238
|
+
"Flasher relay fault",
|
|
2239
|
+
"BCM fault"
|
|
2240
|
+
]
|
|
2181
2241
|
},
|
|
2182
2242
|
"B0603": {
|
|
2183
2243
|
"code": "B0603",
|
|
@@ -2185,7 +2245,12 @@
|
|
|
2185
2245
|
"category": "Body",
|
|
2186
2246
|
"severity": "Low",
|
|
2187
2247
|
"system": "Lighting System",
|
|
2188
|
-
"possible_causes": [
|
|
2248
|
+
"possible_causes": [
|
|
2249
|
+
"Burned out turn signal bulb",
|
|
2250
|
+
"Wiring harness problem",
|
|
2251
|
+
"Flasher relay fault",
|
|
2252
|
+
"BCM fault"
|
|
2253
|
+
]
|
|
2189
2254
|
},
|
|
2190
2255
|
"B0604": {
|
|
2191
2256
|
"code": "B0604",
|
|
@@ -2297,7 +2362,12 @@
|
|
|
2297
2362
|
"category": "Body",
|
|
2298
2363
|
"severity": "Low",
|
|
2299
2364
|
"system": "Keyless Entry",
|
|
2300
|
-
"possible_causes": [
|
|
2365
|
+
"possible_causes": [
|
|
2366
|
+
"Key fob battery weak",
|
|
2367
|
+
"Key fob not synchronized",
|
|
2368
|
+
"BCM fault",
|
|
2369
|
+
"Receiver antenna fault"
|
|
2370
|
+
]
|
|
2301
2371
|
},
|
|
2302
2372
|
"B1300": {
|
|
2303
2373
|
"code": "B1300",
|
|
@@ -2396,7 +2466,12 @@
|
|
|
2396
2466
|
"category": "Network",
|
|
2397
2467
|
"severity": "Critical",
|
|
2398
2468
|
"system": "CAN Bus",
|
|
2399
|
-
"possible_causes": [
|
|
2469
|
+
"possible_causes": [
|
|
2470
|
+
"TCM not powered",
|
|
2471
|
+
"CAN bus wiring problem",
|
|
2472
|
+
"TCM internal fault",
|
|
2473
|
+
"CAN bus short circuit"
|
|
2474
|
+
]
|
|
2400
2475
|
},
|
|
2401
2476
|
"U0102": {
|
|
2402
2477
|
"code": "U0102",
|
|
@@ -2469,7 +2544,12 @@
|
|
|
2469
2544
|
"category": "Network",
|
|
2470
2545
|
"severity": "High",
|
|
2471
2546
|
"system": "CAN Bus",
|
|
2472
|
-
"possible_causes": [
|
|
2547
|
+
"possible_causes": [
|
|
2548
|
+
"BCM not powered",
|
|
2549
|
+
"CAN bus wiring problem",
|
|
2550
|
+
"BCM internal fault",
|
|
2551
|
+
"Ground connection issue"
|
|
2552
|
+
]
|
|
2473
2553
|
},
|
|
2474
2554
|
"U0141": {
|
|
2475
2555
|
"code": "U0141",
|
|
@@ -2477,7 +2557,12 @@
|
|
|
2477
2557
|
"category": "Network",
|
|
2478
2558
|
"severity": "High",
|
|
2479
2559
|
"system": "CAN Bus",
|
|
2480
|
-
"possible_causes": [
|
|
2560
|
+
"possible_causes": [
|
|
2561
|
+
"BCM not powered",
|
|
2562
|
+
"CAN bus wiring problem",
|
|
2563
|
+
"Module internal fault",
|
|
2564
|
+
"Connector problem"
|
|
2565
|
+
]
|
|
2481
2566
|
},
|
|
2482
2567
|
"U0151": {
|
|
2483
2568
|
"code": "U0151",
|
oscura/core/config/loader.py
CHANGED
|
@@ -225,7 +225,6 @@ def load_config(
|
|
|
225
225
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
226
226
|
|
|
227
227
|
# Search for config files if no explicit path provided
|
|
228
|
-
# use_defaults flag only controls whether to merge with DEFAULT_CONFIG
|
|
229
228
|
if config_path is None:
|
|
230
229
|
# Search standard locations
|
|
231
230
|
search_paths = [
|