feat(train): TrainingConfig subcarrier-layout presets + real MmFiDataset loader test (#537)

Closes the remaining doable items from the 2026-05-11 training-pipeline audit:

#6 (CSI format default = 56-sc / 1 NIC) + #7 (multi-band 168-sc mesh not in
config): new `TrainingConfig::for_subcarriers(native, target)` plus named
presets `mmfi()` (114→56), `ht40_192()` (≈192-sc ESP32 HT40 → 56) and
`multiband_168()` (168-sc ADR-078 multi-band mesh → 56). Non-MM-Fi CSI shapes
are now first-class instead of requiring manual `native_subcarriers` /
`num_subcarriers` overrides; the field docs list the supported source counts
and the multi-NIC mapping (a 2–3-node mesh currently rides on `n_rx` until a
dedicated node dimension lands). Model input width stays `num_subcarriers`; the
presets only vary the resampling input.

#4 (proof.rs uses synthetic data): reframed — a deterministic proof *must* use
a reproducible source, so `verify-training` correctly stays on
`SyntheticCsiDataset`. The real gap was that nothing exercised the on-disk
`MmFiDataset` path. New `tests/test_real_loader.rs` writes synthetic CSI to
`.npy` files in the `MmFiDataset::discover` layout, loads it back, and checks
the resulting `CsiSample` — covering the no-interp case, the
subcarrier-interpolation branch, and the empty-root case. Adds `ndarray` /
`ndarray-npy` as dev-deps for the fixture writing.

cargo check + cargo test -p wifi-densepose-train --no-default-features: clean,
all existing tests green, 3 new loader tests + the updated config doctest pass.
Purely additive — no model-shape change, no tch-module change.
This commit is contained in:
rUv
2026-05-11 23:49:00 -04:00
committed by GitHub
parent eaedfded6f
commit c604ca1150
4 changed files with 153 additions and 5 deletions
+61 -5
View File
@@ -15,6 +15,15 @@
//!
//! assert_eq!(cfg.num_subcarriers, 56);
//! assert_eq!(cfg.num_keypoints, 17);
//!
//! // Adapt for a non-MM-Fi source — e.g. an ESP32 HT40 capture (~192 raw
//! // subcarriers) or the ADR-078 multi-band mesh (168). The model still sees
//! // `num_subcarriers`; the loader resamples the native count down to it.
//! let ht40 = TrainingConfig::ht40_192();
//! assert_eq!(ht40.native_subcarriers, 192);
//! assert!(ht40.needs_subcarrier_interp());
//! let mesh = TrainingConfig::for_subcarriers(168, 56);
//! assert_eq!(mesh.native_subcarriers, 168);
//! ```
use serde::{Deserialize, Serialize};
@@ -36,16 +45,26 @@ pub struct TrainingConfig {
// -----------------------------------------------------------------------
// Data / Signal
// -----------------------------------------------------------------------
/// Number of subcarriers after interpolation (system target).
/// Number of subcarriers after interpolation (the *model's* input width).
///
/// The model always sees this many subcarriers regardless of the raw
/// hardware output. Default: **56**.
/// hardware output; [`crate::subcarrier::interpolate_subcarriers`] resamples
/// `native_subcarriers` → `num_subcarriers` when they differ. Default: **56**.
pub num_subcarriers: usize,
/// Number of subcarriers in the raw dataset before interpolation.
/// Number of subcarriers in the *raw* dataset, before interpolation.
///
/// MM-Fi provides 114 subcarriers; set this to 56 when the dataset
/// already matches the target count. Default: **114**.
/// Common sources: MM-Fi = 114, ESP32 HT20 = 56, ESP32 HT40 ≈ 192 (or 114),
/// multi-band mesh = 168 (ADR-078). When it equals [`Self::num_subcarriers`]
/// no interpolation happens ([`Self::needs_subcarrier_interp`]). For the
/// non-MM-Fi shapes prefer the preset constructors
/// ([`Self::for_subcarriers`], [`Self::ht40_192`], [`Self::multiband_168`])
/// over overriding both fields by hand. Default: **114**.
///
/// **Multi-NIC note:** a 23-node CSI mesh currently maps onto the existing
/// `[T, n_tx, n_rx, n_sc]` layout by treating the nodes' receive chains as
/// extra `n_rx` (i.e. `num_antennas_rx = nodes × per_node_rx`); a dedicated
/// node dimension is a separate dataset-loader change.
pub native_subcarriers: usize,
/// Number of transmit antennas. Default: **3**.
@@ -238,6 +257,43 @@ impl TrainingConfig {
Ok(())
}
/// Build a config for a dataset whose raw CSI has `native` subcarriers,
/// resampling to `target` (the model's input width) before training.
///
/// All other fields take their [`Default`] values. Prefer this over
/// overriding `native_subcarriers` / `num_subcarriers` directly so the
/// relationship between the dataset's shape and the model's is explicit.
#[must_use]
pub fn for_subcarriers(native: usize, target: usize) -> Self {
Self {
native_subcarriers: native,
num_subcarriers: target,
..Self::default()
}
}
/// Preset for the MM-Fi dataset (114 raw subcarriers → 56). Identical to
/// [`Self::default()`]; provided as a named counterpart to the other
/// presets.
#[must_use]
pub fn mmfi() -> Self {
Self::default()
}
/// Preset for ESP32 HT40 captures (≈192 raw subcarriers → 56). Use
/// [`Self::for_subcarriers`] if your capture reports a different native
/// count (some HT40 firmwares yield 114).
#[must_use]
pub fn ht40_192() -> Self {
Self::for_subcarriers(192, 56)
}
/// Preset for the ADR-078 multi-band mesh (168 raw subcarriers → 56).
#[must_use]
pub fn multiband_168() -> Self {
Self::for_subcarriers(168, 56)
}
/// Returns `true` when the native dataset subcarrier count differs from the
/// model's target count and interpolation is therefore required.
pub fn needs_subcarrier_interp(&self) -> bool {