feat: Add commodity sensing unit tests and fix feature extractor bugs
Add comprehensive test suite (36 tests) for the ADR-013 commodity sensing module covering all components: RingBuffer, SimulatedCollector determinism, feature extraction (time-domain stats, FFT spectral analysis, band power isolation), CUSUM change-point detection, presence/motion classification, and end-to-end CommodityBackend pipeline. Fix feature_extractor.py: add missing _trim_to_window method that caused AttributeError on the WifiSample extraction path, add post-trim sample count guard, and handle constant-signal edge case in skewness/kurtosis computation to prevent scipy RuntimeWarning. https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
This commit is contained in:
@@ -103,6 +103,8 @@ class RssiFeatureExtractor:
|
||||
|
||||
# Trim to window
|
||||
samples = self._trim_to_window(samples)
|
||||
if len(samples) < 4:
|
||||
return RssiFeatures(n_samples=len(samples))
|
||||
rssi = np.array([s.rssi_dbm for s in samples], dtype=np.float64)
|
||||
timestamps = np.array([s.timestamp for s in samples], dtype=np.float64)
|
||||
|
||||
@@ -158,6 +160,17 @@ class RssiFeatureExtractor:
|
||||
|
||||
return features
|
||||
|
||||
# -- window trimming -----------------------------------------------------
|
||||
|
||||
def _trim_to_window(self, samples: List[WifiSample]) -> List[WifiSample]:
|
||||
"""Keep only samples within the most recent ``window_seconds``."""
|
||||
if not samples:
|
||||
return samples
|
||||
latest_ts = samples[-1].timestamp
|
||||
cutoff = latest_ts - self._window_seconds
|
||||
trimmed = [s for s in samples if s.timestamp >= cutoff]
|
||||
return trimmed
|
||||
|
||||
# -- time-domain ---------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
@@ -165,10 +178,16 @@ class RssiFeatureExtractor:
|
||||
features.mean = float(np.mean(rssi))
|
||||
features.variance = float(np.var(rssi, ddof=1)) if len(rssi) > 1 else 0.0
|
||||
features.std = float(np.std(rssi, ddof=1)) if len(rssi) > 1 else 0.0
|
||||
features.skewness = float(scipy_stats.skew(rssi, bias=False)) if len(rssi) > 2 else 0.0
|
||||
features.kurtosis = float(scipy_stats.kurtosis(rssi, bias=False)) if len(rssi) > 3 else 0.0
|
||||
features.range = float(np.ptp(rssi))
|
||||
|
||||
# Guard against constant signals where higher moments are undefined
|
||||
if features.std < 1e-12:
|
||||
features.skewness = 0.0
|
||||
features.kurtosis = 0.0
|
||||
else:
|
||||
features.skewness = float(scipy_stats.skew(rssi, bias=False)) if len(rssi) > 2 else 0.0
|
||||
features.kurtosis = float(scipy_stats.kurtosis(rssi, bias=False)) if len(rssi) > 3 else 0.0
|
||||
|
||||
q75, q25 = np.percentile(rssi, [75, 25])
|
||||
features.iqr = float(q75 - q25)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user