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.
Files changed (40) hide show
  1. oscura/__init__.py +1 -1
  2. oscura/analyzers/eye/__init__.py +5 -1
  3. oscura/analyzers/eye/generation.py +501 -0
  4. oscura/analyzers/jitter/__init__.py +6 -6
  5. oscura/analyzers/jitter/timing.py +419 -0
  6. oscura/analyzers/patterns/__init__.py +28 -0
  7. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  8. oscura/analyzers/power/__init__.py +35 -12
  9. oscura/analyzers/statistics/__init__.py +4 -0
  10. oscura/analyzers/statistics/basic.py +149 -0
  11. oscura/analyzers/statistics/correlation.py +47 -6
  12. oscura/analyzers/waveform/__init__.py +2 -0
  13. oscura/analyzers/waveform/measurements.py +145 -23
  14. oscura/analyzers/waveform/spectral.py +361 -8
  15. oscura/automotive/__init__.py +1 -1
  16. oscura/automotive/dtc/data.json +102 -17
  17. oscura/core/config/loader.py +0 -1
  18. oscura/core/schemas/device_mapping.json +8 -2
  19. oscura/core/schemas/packet_format.json +24 -4
  20. oscura/core/schemas/protocol_definition.json +12 -2
  21. oscura/core/types.py +108 -0
  22. oscura/reporting/__init__.py +88 -1
  23. oscura/reporting/automation.py +348 -0
  24. oscura/reporting/citations.py +374 -0
  25. oscura/reporting/core.py +54 -0
  26. oscura/reporting/formatting/__init__.py +11 -0
  27. oscura/reporting/formatting/measurements.py +279 -0
  28. oscura/reporting/html.py +57 -0
  29. oscura/reporting/interpretation.py +431 -0
  30. oscura/reporting/summary.py +329 -0
  31. oscura/reporting/visualization.py +542 -0
  32. oscura/visualization/__init__.py +2 -1
  33. oscura/visualization/batch.py +521 -0
  34. oscura/workflows/__init__.py +2 -0
  35. oscura/workflows/waveform.py +783 -0
  36. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/METADATA +1 -1
  37. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/RECORD +40 -29
  38. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
  39. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
  40. {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 power.
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 harmonics
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
- # Sum harmonic power
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 ratio
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",
@@ -49,7 +49,7 @@ try:
49
49
  __version__ = version("oscura")
50
50
  except Exception:
51
51
  # Fallback for development/testing when package not installed
52
- __version__ = "0.7.0"
52
+ __version__ = "0.8.0"
53
53
 
54
54
  __all__ = [
55
55
  "CANMessage",
@@ -266,7 +266,12 @@
266
266
  "category": "Powertrain",
267
267
  "severity": "High",
268
268
  "system": "Throttle Control",
269
- "possible_causes": ["TPS circuit shorted to voltage", "Faulty TPS sensor", "Wiring harness open", "ECM problem"]
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": ["EGR valve stuck closed", "EGR passages clogged", "Faulty EGR valve", "Vacuum leak"]
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": ["EGR valve stuck open", "Faulty EGR valve", "EGR vacuum solenoid fault", "ECM problem"]
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": ["Loose or missing fuel cap", "EVAP system leak", "Faulty purge valve", "Faulty vent valve"]
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": ["Vacuum leak", "IAC valve fault", "Dirty throttle body", "PCV valve problem"]
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": ["Vacuum leak", "IAC valve stuck open", "PCV valve stuck open", "EVAP purge valve leaking"]
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": ["ECM not programmed", "ECM programming incomplete", "Wrong software version", "ECM fault"]
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": ["Faulty alternator", "Wiring harness problem", "Poor electrical connection", "ECM fault"]
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": ["Faulty input speed sensor", "Wiring harness problem", "Sensor reluctor damaged", "TCM fault"]
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": ["Faulty valve relay", "Relay circuit problem", "ABS module fault", "Wiring harness issue"]
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": ["Burned out bulb", "Wiring harness problem", "Lamp socket corrosion", "BCM fault"]
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": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
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": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
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": ["Key fob battery weak", "Key fob not synchronized", "BCM fault", "Receiver antenna fault"]
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": ["TCM not powered", "CAN bus wiring problem", "TCM internal fault", "CAN bus short circuit"]
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": ["BCM not powered", "CAN bus wiring problem", "BCM internal fault", "Ground connection issue"]
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": ["BCM not powered", "CAN bus wiring problem", "Module internal fault", "Connector problem"]
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",
@@ -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 = [