ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
ml_feature_trackers.h
Go to the documentation of this file.
1/*
2 * ESPectre - Production ML Feature Trackers
3 *
4 * Minimal shared tracker for promoted normalized amplitude-shape dynamics.
5 *
6 * Author: Francesco Pace <francesco.pace@gmail.com>
7 * SPDX-License-Identifier: GPL-3.0-only
8 * Commercial licensing available under separate agreement; see LICENSING.md.
9 */
10#pragma once
11
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <cstddef>
16#include <cstdint>
17#include <cstring>
18
19#include "csi_format.h"
20#include "detector_limits.h"
21
22namespace espectre {
23
24constexpr uint8_t HT20_LIVE_BAND_SIZE = 56U;
25constexpr std::array<uint8_t, HT20_LIVE_BAND_SIZE> HT20_LIVE_BINS = {
26 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
27 24, 25, 26, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
28 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
29};
30constexpr uint8_t CHANNEL_SHAPE_SUBBAND_COUNT = 8U;
31constexpr uint8_t CHANNEL_SHAPE_SUBBAND_SIZE =
33constexpr uint32_t CHANNEL_SHAPE_BIN_US = 80000U;
34constexpr uint32_t CHANNEL_SHAPE_WINDOW_US = 1000000U;
35constexpr uint8_t CHANNEL_SHAPE_WINDOW_BINS =
36 static_cast<uint8_t>((CHANNEL_SHAPE_WINDOW_US + CHANNEL_SHAPE_BIN_US - 1U) /
41constexpr std::array<std::array<float, CHANNEL_SHAPE_SUBBAND_COUNT>,
44 {{0.3535533906f, 0.4903926402f, 0.4619397663f, 0.4157348062f, 0.3535533906f, 0.2777851165f, 0.1913417162f, 0.0975451610f}},
45 {{0.3535533906f, 0.4157348062f, 0.1913417162f, -0.0975451610f, -0.3535533906f, -0.4903926402f, -0.4619397663f, -0.2777851165f}},
46 {{0.3535533906f, 0.2777851165f, -0.1913417162f, -0.4903926402f, -0.3535533906f, 0.0975451610f, 0.4619397663f, 0.4157348062f}},
47 {{0.3535533906f, 0.0975451610f, -0.4619397663f, -0.2777851165f, 0.3535533906f, 0.4157348062f, -0.1913417162f, -0.4903926402f}},
48 {{0.3535533906f, -0.0975451610f, -0.4619397663f, 0.2777851165f, 0.3535533906f, -0.4157348062f, -0.1913417162f, 0.4903926402f}},
49 {{0.3535533906f, -0.2777851165f, -0.1913417162f, 0.4903926402f, -0.3535533906f, -0.0975451610f, 0.4619397663f, -0.4157348062f}},
50 {{0.3535533906f, -0.4157348062f, 0.1913417162f, 0.0975451610f, -0.3535533906f, 0.4903926402f, -0.4619397663f, 0.2777851165f}},
51 {{0.3535533906f, -0.4903926402f, 0.4619397663f, -0.4157348062f, 0.3535533906f, -0.2777851165f, 0.1913417162f, -0.0975451610f}},
52 }};
53
54inline float motion_participation(const float* energy, uint8_t count) {
55 if (energy == nullptr || count == 0U) {
56 return 0.0f;
57 }
58 float total = 0.0f;
59 float squared = 0.0f;
60 for (uint8_t i = 0; i < count; i++) {
61 total += energy[i];
62 squared += energy[i] * energy[i];
63 }
64 if (total <= 0.0f || squared <= 0.0f) {
65 return 0.0f;
66 }
67 return (total * total) / (static_cast<float>(count) * squared);
68}
69
71 public:
72 void configure(bool enabled) {
73 enabled_ = enabled;
74 clear();
75 }
76
77 void clear() {
78 bin_count_ = 0U;
79 current_profile_count_ = 0U;
80 current_bin_ = 0U;
81 has_current_bin_ = false;
82 has_previous_raw_ = false;
83 previous_raw_.fill(0);
84 for (auto& bin : bins_) {
85 bin.index = 0U;
86 bin.modes.fill(0.0f);
87 bin.kendall_order = 0U;
88 bin.kendall_valid = 0U;
89 }
90 for (auto& profile : current_profiles_) {
91 profile.fill(0.0f);
92 }
93 }
94
95 void process_packet(const int8_t* csi_data, size_t csi_len,
96 uint64_t timestamp_us,
97 const float* subcarrier_energies = nullptr,
98 uint8_t subcarrier_count = 0U) {
99 if (!enabled_ || csi_data == nullptr || csi_len < HT20_CSI_LEN) {
100 return;
101 }
102 if (has_previous_raw_ &&
103 std::memcmp(previous_raw_.data(), csi_data, HT20_CSI_LEN) == 0) {
104 return;
105 }
106 std::memcpy(previous_raw_.data(), csi_data, HT20_CSI_LEN);
107 has_previous_raw_ = true;
108
109 const uint64_t bin_index = timestamp_us / CHANNEL_SHAPE_BIN_US;
110 if (!has_current_bin_) {
111 current_bin_ = bin_index;
112 has_current_bin_ = true;
113 } else if (bin_index != current_bin_) {
114 finalize_current_bin_();
115 current_bin_ = bin_index;
116 current_profile_count_ = 0U;
117 trim_(bin_index);
118 }
119 if (current_profile_count_ >= CHANNEL_SHAPE_MAX_PROFILES_PER_BIN) {
120 return;
121 }
122 fill_profile_(csi_data, subcarrier_energies, subcarrier_count,
123 current_profiles_[current_profile_count_]);
124 current_profile_count_++;
125 }
126
128 float& excess_path,
130 float& kendall_lag_excess) const {
132 excess_path = 0.0f;
134 kendall_lag_excess = 0.0f;
135 std::array<PathPoint, CHANNEL_SHAPE_WINDOW_BINS + 1U> path{};
136 const uint8_t count = build_path_(path);
137 if (count < 2U) {
138 return;
139 }
140
141 Profile spread_energy{};
142 for (uint8_t i = 1U; i < count; i++) {
143 if (path[i].index - path[i - 1U].index != 1U) continue;
144 for (uint8_t subband = 0U;
145 subband < CHANNEL_SHAPE_SUBBAND_COUNT; subband++) {
146 float delta = 0.0f;
147 for (uint8_t mode = 0U;
148 mode < CHANNEL_SHAPE_SUBBAND_COUNT; mode++) {
149 delta += (path[i].modes[mode] - path[i - 1U].modes[mode]) *
150 CHANNEL_SHAPE_DCT[subband][mode];
151 }
152 spread_energy[subband] += delta * delta;
153 }
154 }
156 spread_energy.data(), CHANNEL_SHAPE_SUBBAND_COUNT);
157 if (count < 3U) return;
158
159 std::array<float, CHANNEL_SHAPE_WINDOW_BINS - 1U> innovation_samples{};
160 std::array<float, CHANNEL_SHAPE_WINDOW_BINS - 1U> excess_samples{};
161 uint8_t innovation_count = 0U;
162 uint8_t excess_count = 0U;
163 Profile first_modes = path[0].modes;
164 Profile middle_modes = path[1].modes;
165 for (uint8_t i = 2U; i < count; i++) {
166 const Profile last_modes = path[i].modes;
167 const uint64_t previous_dt = path[i - 1U].index - path[i - 2U].index;
168 const uint64_t current_dt = path[i].index - path[i - 1U].index;
169 float first_norm_squared = 0.0f;
170 float second_norm_squared = 0.0f;
171 float chord_norm_squared = 0.0f;
172 float first_high_squared = 0.0f;
173 float second_high_squared = 0.0f;
174 float chord_high_squared = 0.0f;
175 float innovation_low_squared = 0.0f;
176 float innovation_high_squared = 0.0f;
177 const float ratio = previous_dt > 0U
178 ? static_cast<float>(current_dt) /
179 static_cast<float>(previous_dt)
180 : 0.0f;
181 for (uint8_t j = 0U; j < CHANNEL_SHAPE_SUBBAND_COUNT; j++) {
182 const float first_delta = middle_modes[j] - first_modes[j];
183 const float second_delta = last_modes[j] - middle_modes[j];
184 const float chord_delta = last_modes[j] - first_modes[j];
185 first_norm_squared += first_delta * first_delta;
186 second_norm_squared += second_delta * second_delta;
187 chord_norm_squared += chord_delta * chord_delta;
188 if (j >= 4U) {
189 first_high_squared += first_delta * first_delta;
190 second_high_squared += second_delta * second_delta;
191 chord_high_squared += chord_delta * chord_delta;
192 }
193 if (previous_dt > 0U && current_dt > 0U && j > 0U) {
194 const float residual = second_delta - ratio * first_delta;
195 if (j < 4U) {
196 innovation_low_squared += residual * residual;
197 } else {
198 innovation_high_squared += residual * residual;
199 }
200 }
201 }
202 if (previous_dt > 0U && current_dt > 0U) {
203 innovation_samples[innovation_count++] = std::max(
204 0.0f, innovation_low_squared - innovation_high_squared);
205 }
206 // Parseval: the orthonormal DCT preserves full-profile L2 distances.
207 const float raw_excess = std::sqrt(first_norm_squared) +
208 std::sqrt(second_norm_squared) -
209 std::sqrt(chord_norm_squared);
210 const float high_excess = std::sqrt(first_high_squared) +
211 std::sqrt(second_high_squared) -
212 std::sqrt(chord_high_squared);
213 excess_samples[excess_count++] =
214 std::max(0.0f, raw_excess - std::max(0.0f, high_excess));
215 first_modes = middle_modes;
216 middle_modes = last_modes;
217 }
219 median_(innovation_samples.data(), innovation_count);
220 excess_path = median_(excess_samples.data(), excess_count);
221
222 std::array<float, CHANNEL_SHAPE_WINDOW_BINS - 2U> kendall_samples{};
223 uint8_t kendall_count = 0U;
224 for (uint8_t i = 3U; i < count; i++) {
225 if (path[i].index - path[i - 3U].index != 3U) continue;
226 float long_distance = 0.0f;
227 if (!kendall_distance_(path[i], path[i - 3U], long_distance)) continue;
228 float local_sum = 0.0f;
229 bool local_ok = true;
230 for (uint8_t lag = 0U; lag < 3U; lag++) {
231 float local_distance = 0.0f;
232 if (!kendall_distance_(
233 path[i - lag], path[i - lag - 1U], local_distance)) {
234 local_ok = false;
235 break;
236 }
237 local_sum += local_distance;
238 }
239 if (!local_ok) continue;
240 kendall_samples[kendall_count++] =
241 std::max(0.0f, long_distance - local_sum / 3.0f);
242 }
243 kendall_lag_excess = median_(kendall_samples.data(), kendall_count);
244 }
245
247 float& excess_path,
248 float& shape_spread_subband) const {
249 float kendall = 0.0f;
251 shape_spread_subband, kendall);
252 }
253
255 float& excess_path) const {
256 float spread = 0.0f;
257 float kendall = 0.0f;
259 kendall);
260 }
261
263 float innovation = 0.0f;
264 float excess = 0.0f;
265 trajectory_features(innovation, excess);
266 return innovation;
267 }
268
269 float excess_path() const {
270 float innovation = 0.0f;
271 float excess = 0.0f;
272 trajectory_features(innovation, excess);
273 return excess;
274 }
275
276 float shape_spread_subband() const {
277 float innovation = 0.0f;
278 float excess = 0.0f;
279 float spread = 0.0f;
280 trajectory_features(innovation, excess, spread);
281 return spread;
282 }
283
285 float innovation = 0.0f;
286 float excess = 0.0f;
287 float spread = 0.0f;
288 float kendall = 0.0f;
289 trajectory_features(innovation, excess, spread, kendall);
290 return kendall;
291 }
292
293 private:
294 using Profile = std::array<float, CHANNEL_SHAPE_SUBBAND_COUNT>;
295 struct PathPoint {
296 uint64_t index{0U};
297 Profile modes{};
298 uint32_t kendall_order{0U};
299 uint32_t kendall_valid{0U};
300 };
301
302 static uint8_t popcount32_(uint32_t value) {
303 uint8_t count = 0U;
304 while (value != 0U) {
305 count = static_cast<uint8_t>(count + (value & 1U));
306 value >>= 1U;
307 }
308 return count;
309 }
310
311 static void kendall_signature_(const Profile& profile, uint32_t& order,
312 uint32_t& valid) {
313 order = 0U;
314 valid = 0U;
315 float maximum = 0.0f;
316 for (float value : profile) {
317 if (value > maximum) maximum = value;
318 }
319 if (maximum <= 0.0f) return;
320 const float threshold =
322 uint8_t bit = 0U;
323 for (uint8_t left = 0U; left + 1U < CHANNEL_SHAPE_SUBBAND_COUNT; left++) {
324 for (uint8_t right = left + 1U; right < CHANNEL_SHAPE_SUBBAND_COUNT;
325 right++) {
326 const float difference = profile[left] - profile[right];
327 const uint32_t flag = 1U << bit;
328 if (std::fabs(difference) > threshold) {
329 valid |= flag;
330 if (difference > 0.0f) order |= flag;
331 }
332 bit++;
333 }
334 }
335 }
336
337 static bool kendall_distance_(const PathPoint& current,
338 const PathPoint& reference, float& distance) {
339 const uint32_t common = current.kendall_valid & reference.kendall_valid;
340 const uint8_t comparable = popcount32_(common);
342 distance = 0.0f;
343 return false;
344 }
345 const uint8_t discordant = popcount32_(
346 (current.kendall_order ^ reference.kendall_order) & common);
347 distance = static_cast<float>(discordant) / static_cast<float>(comparable);
348 return true;
349 }
350
351 static float median_(float* values, uint8_t count) {
352 if (count == 0U) return 0.0f;
353 std::sort(values, values + count);
354 const uint8_t middle = count / 2U;
355 return count % 2U == 0U ? 0.5f * (values[middle - 1U] + values[middle])
356 : values[middle];
357 }
358
359 static float norm_(const Profile& values, uint8_t start) {
360 float total = 0.0f;
361 for (uint8_t i = start; i < CHANNEL_SHAPE_SUBBAND_COUNT; i++) {
362 total += values[i] * values[i];
363 }
364 return std::sqrt(total);
365 }
366
367 static Profile dct_modes_(const Profile& values) {
368 Profile modes{};
369 for (uint8_t mode = 0U; mode < CHANNEL_SHAPE_SUBBAND_COUNT; mode++) {
370 for (uint8_t i = 0U; i < CHANNEL_SHAPE_SUBBAND_COUNT; i++) {
371 modes[mode] += values[i] * CHANNEL_SHAPE_DCT[i][mode];
372 }
373 }
374 return modes;
375 }
376
377 static void fill_profile_(const int8_t* csi_data,
378 const float* subcarrier_energies,
379 uint8_t subcarrier_count,
380 Profile& out) {
381 out.fill(0.0f);
382 float total = 0.0f;
383 for (uint8_t i = 0U; i < HT20_LIVE_BAND_SIZE; i++) {
384 const uint8_t subcarrier = HT20_LIVE_BINS[i];
385 float energy = 0.0f;
386 if (subcarrier_energies != nullptr && subcarrier < subcarrier_count) {
387 energy = subcarrier_energies[subcarrier];
388 } else {
389 const float imag = static_cast<float>(csi_data[subcarrier * 2U]);
390 const float real = static_cast<float>(csi_data[subcarrier * 2U + 1U]);
391 energy = real * real + imag * imag;
392 }
393 out[i / CHANNEL_SHAPE_SUBBAND_SIZE] += energy;
394 total += energy;
395 }
396 if (total <= 0.0f) return;
397 for (float& value : out) value = std::sqrt(value / total);
398 }
399
400 Profile median_current_profile_() const {
401 Profile result{};
402 if (current_profile_count_ == 0U) return result;
403 for (uint8_t dimension = 0U; dimension < CHANNEL_SHAPE_SUBBAND_COUNT;
404 dimension++) {
405 std::array<float, CHANNEL_SHAPE_MAX_PROFILES_PER_BIN> values{};
406 for (uint8_t i = 0U; i < current_profile_count_; i++) {
407 values[i] = current_profiles_[i][dimension];
408 }
409 result[dimension] = median_(values.data(), current_profile_count_);
410 }
411 const float length = norm_(result, 0U);
412 if (length > 0.0f) {
413 for (float& value : result) value /= length;
414 }
415 return result;
416 }
417
418 void finalize_current_bin_() {
419 if (!has_current_bin_ || current_profile_count_ == 0U) return;
420 if (bin_count_ >= CHANNEL_SHAPE_WINDOW_BINS) {
421 for (uint8_t i = 1U; i < bin_count_; i++) bins_[i - 1U] = bins_[i];
422 bin_count_--;
423 }
424 bins_[bin_count_].index = current_bin_;
425 const Profile profile = median_current_profile_();
426 kendall_signature_(profile, bins_[bin_count_].kendall_order,
427 bins_[bin_count_].kendall_valid);
428 bins_[bin_count_].modes = dct_modes_(profile);
429 bin_count_++;
430 }
431
432 void trim_(uint64_t current_bin) {
433 const uint64_t first_bin = current_bin >= CHANNEL_SHAPE_WINDOW_BINS - 1U
434 ? current_bin - CHANNEL_SHAPE_WINDOW_BINS + 1U
435 : 0U;
436 uint8_t first = 0U;
437 while (first < bin_count_ && bins_[first].index < first_bin) first++;
438 if (first == 0U) return;
439 for (uint8_t i = first; i < bin_count_; i++) bins_[i - first] = bins_[i];
440 bin_count_ = static_cast<uint8_t>(bin_count_ - first);
441 }
442
443 uint8_t build_path_(
444 std::array<PathPoint, CHANNEL_SHAPE_WINDOW_BINS + 1U>& path) const {
445 for (uint8_t i = 0U; i < bin_count_; i++) path[i] = bins_[i];
446 uint8_t count = bin_count_;
447 if (current_profile_count_ > 0U && count < path.size()) {
448 const Profile profile = median_current_profile_();
449 path[count].index = current_bin_;
450 kendall_signature_(profile, path[count].kendall_order,
451 path[count].kendall_valid);
452 path[count].modes = dct_modes_(profile);
453 count++;
454 }
455 return count;
456 }
457
458 bool enabled_{false};
459 std::array<PathPoint, CHANNEL_SHAPE_WINDOW_BINS> bins_{};
460 uint8_t bin_count_{0U};
461 uint64_t current_bin_{0U};
462 bool has_current_bin_{false};
463 std::array<Profile, CHANNEL_SHAPE_MAX_PROFILES_PER_BIN> current_profiles_{};
464 uint8_t current_profile_count_{0U};
465 std::array<int8_t, HT20_CSI_LEN> previous_raw_{};
466 bool has_previous_raw_{false};
467};
468
469} // namespace espectre
void trajectory_features(float &coherent_innovation_energy, float &excess_path, float &shape_spread_subband, float &kendall_lag_excess) const
void trajectory_features(float &coherent_innovation_energy, float &excess_path, float &shape_spread_subband) const
void process_packet(const int8_t *csi_data, size_t csi_len, uint64_t timestamp_us, const float *subcarrier_energies=nullptr, uint8_t subcarrier_count=0U)
void trajectory_features(float &coherent_innovation_energy, float &excess_path) const
float motion_participation(const float *energy, uint8_t count)
constexpr uint8_t HT20_LIVE_BAND_SIZE
constexpr uint8_t CHANNEL_SHAPE_SUBBAND_SIZE
constexpr uint8_t CHANNEL_SHAPE_MAX_PROFILES_PER_BIN
constexpr uint8_t CHANNEL_SHAPE_SUBBAND_COUNT
constexpr std::array< std::array< float, CHANNEL_SHAPE_SUBBAND_COUNT >, CHANNEL_SHAPE_SUBBAND_COUNT > CHANNEL_SHAPE_DCT
constexpr uint32_t CHANNEL_SHAPE_WINDOW_US
constexpr uint16_t L1_DELTA_LAG_MAX
constexpr std::array< uint8_t, HT20_LIVE_BAND_SIZE > HT20_LIVE_BINS
constexpr uint8_t CHANNEL_SHAPE_KENDALL_MIN_COMPARABLE_PAIRS
constexpr uint32_t CHANNEL_SHAPE_BIN_US
constexpr float CHANNEL_SHAPE_KENDALL_RELATIVE_DEADBAND
constexpr uint8_t CHANNEL_SHAPE_WINDOW_BINS
constexpr uint16_t HT20_CSI_LEN
Definition csi_types.h:19