feat: Add 12 ADRs for RuVector RVF integration and proof-of-reality #31
@@ -385,12 +385,53 @@ class CSIProcessor:
|
|||||||
return correlation_matrix
|
return correlation_matrix
|
||||||
|
|
||||||
def _extract_doppler_features(self, csi_data: CSIData) -> tuple:
|
def _extract_doppler_features(self, csi_data: CSIData) -> tuple:
|
||||||
"""Extract Doppler and frequency domain features."""
|
"""Extract Doppler and frequency domain features from temporal CSI history.
|
||||||
# Simple Doppler estimation (would use history in real implementation)
|
|
||||||
doppler_shift = np.random.rand(10) # Placeholder
|
|
||||||
|
|
||||||
# Power spectral density
|
Computes Doppler spectrum by analyzing temporal phase differences across
|
||||||
psd = np.abs(scipy.fft.fft(csi_data.amplitude.flatten(), n=128))**2
|
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
|
return doppler_shift, psd
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ class CSIValidationError(Exception):
|
|||||||
pass
|
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
|
@dataclass
|
||||||
class CSIData:
|
class CSIData:
|
||||||
"""Data structure for CSI measurements."""
|
"""Data structure for CSI measurements."""
|
||||||
@@ -78,10 +87,32 @@ class ESP32CSIParser:
|
|||||||
frequency = frequency_mhz * 1e6 # MHz to Hz
|
frequency = frequency_mhz * 1e6 # MHz to Hz
|
||||||
bandwidth = bandwidth_mhz * 1e6 # MHz to Hz
|
bandwidth = bandwidth_mhz * 1e6 # MHz to Hz
|
||||||
|
|
||||||
# Parse amplitude and phase arrays (simplified for now)
|
# Parse amplitude and phase arrays from the remaining CSV fields.
|
||||||
# In real implementation, this would parse actual CSI matrix data
|
# Expected format after the header fields: comma-separated float values
|
||||||
amplitude = np.random.rand(num_antennas, num_subcarriers)
|
# representing interleaved amplitude and phase per antenna per subcarrier.
|
||||||
phase = np.random.rand(num_antennas, num_subcarriers)
|
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(
|
return CSIData(
|
||||||
timestamp=datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc),
|
timestamp=datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc),
|
||||||
@@ -126,19 +157,20 @@ class RouterCSIParser:
|
|||||||
raise CSIParseError("Unknown router CSI format")
|
raise CSIParseError("Unknown router CSI format")
|
||||||
|
|
||||||
def _parse_atheros_format(self, raw_data: bytes) -> CSIData:
|
def _parse_atheros_format(self, raw_data: bytes) -> CSIData:
|
||||||
"""Parse Atheros CSI format (placeholder implementation)."""
|
"""Parse Atheros CSI format.
|
||||||
# This would implement actual Atheros CSI parsing
|
|
||||||
# For now, return mock data for testing
|
Raises:
|
||||||
return CSIData(
|
CSIExtractionError: Always, because Atheros CSI parsing requires
|
||||||
timestamp=datetime.now(timezone.utc),
|
the Atheros CSI Tool binary format parser which has not been
|
||||||
amplitude=np.random.rand(3, 56),
|
implemented yet. Use the ESP32 parser or contribute an
|
||||||
phase=np.random.rand(3, 56),
|
Atheros implementation.
|
||||||
frequency=2.4e9,
|
"""
|
||||||
bandwidth=20e6,
|
raise CSIExtractionError(
|
||||||
num_subcarriers=56,
|
"Atheros CSI format parsing is not yet implemented. "
|
||||||
num_antennas=3,
|
"The Atheros CSI Tool outputs a binary format that requires a dedicated parser. "
|
||||||
snr=12.0,
|
"To collect real CSI data from Atheros-based routers, you must implement "
|
||||||
metadata={'source': 'atheros_router'}
|
"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