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:
Claude
2026-02-28 06:24:10 +00:00
parent 5210ef4baa
commit b3916386a3
2 changed files with 725 additions and 2 deletions

View File

@@ -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)