fix: Replace mock/placeholder code with real implementations (ADR-011)
- csi_processor.py: Replace np.random.rand(10) Doppler placeholder with real temporal phase-difference FFT extraction from CSI history buffer. Returns zeros (not random) when insufficient history frames available. - csi_extractor.py: Replace np.random.rand() fallbacks in ESP32 and Atheros parsers with proper data parsing (ESP32) and explicit error raising (Atheros). Add CSIExtractionError for clear failure reporting instead of silent random data substitution. These are the two most critical mock eliminations identified in ADR-011. https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
This commit is contained in:
@@ -385,13 +385,54 @@ class CSIProcessor:
|
||||
return correlation_matrix
|
||||
|
||||
def _extract_doppler_features(self, csi_data: CSIData) -> tuple:
|
||||
"""Extract Doppler and frequency domain features."""
|
||||
# Simple Doppler estimation (would use history in real implementation)
|
||||
doppler_shift = np.random.rand(10) # Placeholder
|
||||
|
||||
# Power spectral density
|
||||
psd = np.abs(scipy.fft.fft(csi_data.amplitude.flatten(), n=128))**2
|
||||
|
||||
"""Extract Doppler and frequency domain features from temporal CSI history.
|
||||
|
||||
Computes Doppler spectrum by analyzing temporal phase differences across
|
||||
frames in self.csi_history, then applying FFT to obtain the Doppler shift
|
||||
frequency components. If fewer than 2 history frames are available, returns
|
||||
a zero-filled Doppler array (never random data).
|
||||
|
||||
Returns:
|
||||
tuple: (doppler_shift, power_spectral_density) as numpy arrays
|
||||
"""
|
||||
n_doppler_bins = 64
|
||||
|
||||
if len(self.csi_history) >= 2:
|
||||
# Build temporal phase matrix from history frames
|
||||
# Each row is the mean phase across antennas for one time step
|
||||
history_list = list(self.csi_history)
|
||||
phase_series = []
|
||||
for frame in history_list:
|
||||
# Average phase across antennas to get per-subcarrier phase
|
||||
if frame.phase.ndim == 2:
|
||||
phase_series.append(np.mean(frame.phase, axis=0))
|
||||
else:
|
||||
phase_series.append(frame.phase.flatten())
|
||||
|
||||
phase_matrix = np.array(phase_series) # shape: (num_frames, num_subcarriers)
|
||||
|
||||
# Compute temporal phase differences between consecutive frames
|
||||
phase_diffs = np.diff(phase_matrix, axis=0) # shape: (num_frames-1, num_subcarriers)
|
||||
|
||||
# Average phase diff across subcarriers for each time step
|
||||
mean_phase_diff = np.mean(phase_diffs, axis=1) # shape: (num_frames-1,)
|
||||
|
||||
# Apply FFT to get Doppler spectrum from the temporal phase differences
|
||||
doppler_spectrum = np.abs(scipy.fft.fft(mean_phase_diff, n=n_doppler_bins)) ** 2
|
||||
|
||||
# Normalize to prevent scale issues
|
||||
max_val = np.max(doppler_spectrum)
|
||||
if max_val > 0:
|
||||
doppler_spectrum = doppler_spectrum / max_val
|
||||
|
||||
doppler_shift = doppler_spectrum
|
||||
else:
|
||||
# Not enough history for Doppler estimation -- return zeros, never random
|
||||
doppler_shift = np.zeros(n_doppler_bins)
|
||||
|
||||
# Power spectral density of the current frame
|
||||
psd = np.abs(scipy.fft.fft(csi_data.amplitude.flatten(), n=128)) ** 2
|
||||
|
||||
return doppler_shift, psd
|
||||
|
||||
def _analyze_motion_patterns(self, features: CSIFeatures) -> float:
|
||||
|
||||
@@ -19,6 +19,15 @@ class CSIValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CSIExtractionError(Exception):
|
||||
"""Exception raised when CSI data extraction fails.
|
||||
|
||||
This error is raised instead of silently returning random/placeholder data.
|
||||
Callers should handle this to inform users that real hardware data is required.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class CSIData:
|
||||
"""Data structure for CSI measurements."""
|
||||
@@ -78,10 +87,32 @@ class ESP32CSIParser:
|
||||
frequency = frequency_mhz * 1e6 # MHz to Hz
|
||||
bandwidth = bandwidth_mhz * 1e6 # MHz to Hz
|
||||
|
||||
# Parse amplitude and phase arrays (simplified for now)
|
||||
# In real implementation, this would parse actual CSI matrix data
|
||||
amplitude = np.random.rand(num_antennas, num_subcarriers)
|
||||
phase = np.random.rand(num_antennas, num_subcarriers)
|
||||
# Parse amplitude and phase arrays from the remaining CSV fields.
|
||||
# Expected format after the header fields: comma-separated float values
|
||||
# representing interleaved amplitude and phase per antenna per subcarrier.
|
||||
data_values = parts[6:]
|
||||
expected_values = num_antennas * num_subcarriers * 2 # amplitude + phase
|
||||
|
||||
if len(data_values) < expected_values:
|
||||
raise CSIExtractionError(
|
||||
f"ESP32 CSI data incomplete: expected {expected_values} values "
|
||||
f"(amplitude + phase for {num_antennas} antennas x {num_subcarriers} subcarriers), "
|
||||
f"but received {len(data_values)} values. "
|
||||
"Ensure the ESP32 firmware is configured to output full CSI matrix data. "
|
||||
"See docs/hardware-setup.md for ESP32 CSI configuration."
|
||||
)
|
||||
|
||||
try:
|
||||
float_values = [float(v) for v in data_values[:expected_values]]
|
||||
except ValueError as ve:
|
||||
raise CSIExtractionError(
|
||||
f"ESP32 CSI data contains non-numeric values: {ve}. "
|
||||
"Raw CSI fields must be numeric float values."
|
||||
)
|
||||
|
||||
all_values = np.array(float_values)
|
||||
amplitude = all_values[:num_antennas * num_subcarriers].reshape(num_antennas, num_subcarriers)
|
||||
phase = all_values[num_antennas * num_subcarriers:].reshape(num_antennas, num_subcarriers)
|
||||
|
||||
return CSIData(
|
||||
timestamp=datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc),
|
||||
@@ -126,19 +157,20 @@ class RouterCSIParser:
|
||||
raise CSIParseError("Unknown router CSI format")
|
||||
|
||||
def _parse_atheros_format(self, raw_data: bytes) -> CSIData:
|
||||
"""Parse Atheros CSI format (placeholder implementation)."""
|
||||
# This would implement actual Atheros CSI parsing
|
||||
# For now, return mock data for testing
|
||||
return CSIData(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
amplitude=np.random.rand(3, 56),
|
||||
phase=np.random.rand(3, 56),
|
||||
frequency=2.4e9,
|
||||
bandwidth=20e6,
|
||||
num_subcarriers=56,
|
||||
num_antennas=3,
|
||||
snr=12.0,
|
||||
metadata={'source': 'atheros_router'}
|
||||
"""Parse Atheros CSI format.
|
||||
|
||||
Raises:
|
||||
CSIExtractionError: Always, because Atheros CSI parsing requires
|
||||
the Atheros CSI Tool binary format parser which has not been
|
||||
implemented yet. Use the ESP32 parser or contribute an
|
||||
Atheros implementation.
|
||||
"""
|
||||
raise CSIExtractionError(
|
||||
"Atheros CSI format parsing is not yet implemented. "
|
||||
"The Atheros CSI Tool outputs a binary format that requires a dedicated parser. "
|
||||
"To collect real CSI data from Atheros-based routers, you must implement "
|
||||
"the binary format parser following the Atheros CSI Tool specification. "
|
||||
"See docs/hardware-setup.md for supported hardware and data formats."
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user