feat(adr-105): kill synthetic signal_field — only real ESP32 data left
Continuation of ADR-105 (no synthetic outputs in production runtime). The 20×20 SignalField heatmap was generated by mapping subcarrier index k to angle 2π·k/N and dropping a Gaussian hotspot — a totally fabricated spatial layout. A single sensor has no directional info so the resulting heatmap had no correspondence to where anything actually was in the room; UI showed believable-looking but physically meaningless hotspots. Operator asked for boots-on-the- ground honesty. `generate_signal_field` now returns a zero-filled 20×1×20 grid. UI renders blank, which is the truthful state until a real multistatic localizer is wired (multi-AP attention from ADR-008 or the `MultistaticFuser` already in code). Audit of remaining fields confirmed they are either: - already gated on real data (vital_signs returns None when br < 1 BPM, persons/pose_keypoints/posture/signal_quality_score all None without model loaded), - or processed from real CSI (classification, features.mean_rssi, features.variance, enhanced_motion when multi-AP pipeline active). `--source simulate` was already disabled by an earlier change (exit code 2). `--pretrain` and `--train` synthetic fallbacks remain in code as developer tools but never touch the runtime sensing path.
This commit is contained in:
parent
9aa027e95e
commit
30244d274b
|
|
@ -1710,86 +1710,23 @@ fn parse_esp32_frame(buf: &[u8]) -> Option<Esp32Frame> {
|
|||
/// subcarriers with the highest variance produce peaks at the corresponding directions.
|
||||
fn generate_signal_field(
|
||||
_mean_rssi: f64,
|
||||
motion_score: f64,
|
||||
breathing_rate_hz: f64,
|
||||
signal_quality: f64,
|
||||
subcarrier_variances: &[f64],
|
||||
_motion_score: f64,
|
||||
_breathing_rate_hz: f64,
|
||||
_signal_quality: f64,
|
||||
_subcarrier_variances: &[f64],
|
||||
) -> SignalField {
|
||||
// ADR-105: this used to paint a 20×20 "room heatmap" by mapping each
|
||||
// subcarrier index `k` to an angle `2π·k/N` and dropping a Gaussian
|
||||
// hotspot at radius proportional to its variance — visually rich, but
|
||||
// **physically meaningless**. A single sensor has no directional
|
||||
// information, so the resulting hotspots have no correspondence to
|
||||
// where anything actually is in the room. Operator requested
|
||||
// boots-on-the-ground honesty: return a zero-filled grid. UI will
|
||||
// render blank, which is the truthful state until a real
|
||||
// multistatic localizer is wired in.
|
||||
let grid = 20usize;
|
||||
let mut values = vec![0.0f64; grid * grid];
|
||||
let center = (grid as f64 - 1.0) / 2.0;
|
||||
return SignalField { grid_size: [grid, 1, grid], values: vec![0.0; grid * grid] };
|
||||
|
||||
// Normalise subcarrier variances to [0, 1].
|
||||
let max_var = subcarrier_variances.iter().cloned().fold(0.0f64, f64::max);
|
||||
let norm_factor = if max_var > 1e-9 { max_var } else { 1.0 };
|
||||
|
||||
// For each cell, accumulate contributions from all subcarriers.
|
||||
// Each subcarrier k is assigned an angular direction proportional to its index
|
||||
// so that different subcarriers illuminate different regions of the room.
|
||||
let n_sub = subcarrier_variances.len().max(1);
|
||||
for (k, &var) in subcarrier_variances.iter().enumerate() {
|
||||
let weight = (var / norm_factor) * motion_score;
|
||||
if weight < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
// Map subcarrier index to an angle across the full 2π sweep.
|
||||
let angle = (k as f64 / n_sub as f64) * 2.0 * std::f64::consts::PI;
|
||||
// Place the hotspot at a distance proportional to the weight, capped at 40% of
|
||||
// the grid radius so it stays within the room model.
|
||||
let radius = center * 0.8 * weight.sqrt();
|
||||
let hx = center + radius * angle.cos();
|
||||
let hz = center + radius * angle.sin();
|
||||
|
||||
for z in 0..grid {
|
||||
for x in 0..grid {
|
||||
let dx = x as f64 - hx;
|
||||
let dz = z as f64 - hz;
|
||||
let dist2 = dx * dx + dz * dz;
|
||||
// Gaussian blob centred on the hotspot; spread scales with weight.
|
||||
let spread = (0.5 + weight * 2.0).max(0.5);
|
||||
values[z * grid + x] += weight * (-dist2 / (2.0 * spread * spread)).exp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Base radial attenuation from the router assumed at grid centre.
|
||||
for z in 0..grid {
|
||||
for x in 0..grid {
|
||||
let dx = x as f64 - center;
|
||||
let dz = z as f64 - center;
|
||||
let dist = (dx * dx + dz * dz).sqrt();
|
||||
let base = signal_quality * (-dist * 0.12).exp();
|
||||
values[z * grid + x] += base * 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Breathing ring: if a breathing rate was estimated add a faint annular highlight
|
||||
// at a radius corresponding to typical chest-wall displacement range.
|
||||
if breathing_rate_hz > 0.05 {
|
||||
let ring_r = center * 0.55;
|
||||
let ring_width = 1.8f64;
|
||||
for z in 0..grid {
|
||||
for x in 0..grid {
|
||||
let dx = x as f64 - center;
|
||||
let dz = z as f64 - center;
|
||||
let dist = (dx * dx + dz * dz).sqrt();
|
||||
let ring_val = 0.08 * (-(dist - ring_r).powi(2) / (2.0 * ring_width * ring_width)).exp();
|
||||
values[z * grid + x] += ring_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp and normalise to [0, 1].
|
||||
let field_max = values.iter().cloned().fold(0.0f64, f64::max);
|
||||
let scale = if field_max > 1e-9 { 1.0 / field_max } else { 1.0 };
|
||||
for v in &mut values {
|
||||
*v = (*v * scale).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
SignalField {
|
||||
grid_size: [grid, 1, grid],
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Feature extraction from ESP32 frame ──────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue