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:
Claude
2026-02-28 06:15:55 +00:00
parent 337dd9652f
commit fd493e5103
2 changed files with 97 additions and 24 deletions

View File

@@ -385,11 +385,52 @@ 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
"""Extract Doppler and frequency domain features from temporal CSI history.
# Power spectral density
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

View File

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