This codebase implements CSI-based user equipment (UE) positioning with supervised training, often referred to as neural positioning.
This repository accompanies the paper [2] and implements the transformer variant of the neural positioning architectures compared therein (the MLP baseline from [2] is not included in this codebase but can be found in IIP-Group/neural-positioning). The implemented pipeline maps channel-state information (CSI) from multi-AP Wi-Fi and 5G NR testbeds directly to absolute 2D UE positions. It is evaluated across three real-world datasets under four complementary generalization scenarios: random partitioning (baseline), unseen spatial regions, unseen UE trajectories, and cross-week consistency.
The pipeline implemented in this repository trains a transformer network to predict absolute UE positions from CSI amplitude features. Features are extracted from the 5G uplink channel (PUSCH) for 5G CSI data, or from the Wi-Fi uplink channel for Wi-Fi CSI data. The pipeline uses:
-
CSI Features: At each sample instant
$n$ , the CSI tensor$\mathbf{H}_n \in \mathbb{C}^{B \times A \times W}$ is collected from$B$ APs each with$A$ antennas over$W$ subcarriers. Per-sample features are formed by taking element-wise absolute values and normalizing by the Frobenius norm, preserving inter-AP power ratios. Specifically:-
CAEZ-5G (Outdoor and Hallway): The full 3 276 subcarriers are low-pass filtered and downsampled by a factor of 12, yielding 273 active subcarriers per antenna. The resulting CSI tensor of shape
(N, B, A, W)is passed to the transformer after Frobenius normalization. -
CAEZ-WIFI (Office): All 104 active subcarriers of the 40 MHz IEEE 802.11a Wi-Fi uplink are used. Per 50 ms window, mean |CSI| is taken over samples in each AP bin, then each window is Frobenius-normalised in
feature_extraction_simple()(seemain.py).
-
CAEZ-5G (Outdoor and Hallway): The full 3 276 subcarriers are low-pass filtered and downsampled by a factor of 12, yielding 273 active subcarriers per antenna. The resulting CSI tensor of shape
-
Neural Network Architecture: A transformer that processes one token per AP. Each AP
$b$ contributes one token, formed by projecting the flattened$(A \times W)$ -dimensional CSI slice through a two-layer MLP embedding with ReLU activations. A learned positional encoding is added per AP to inject AP identity. The token sequence is processed by two transformer encoder layers, each with multi-head self-attention and a feed-forward network (FFN). All encoder token outputs are concatenated and passed through a four-layer regression head with ReLU activations and a final sigmoid, producing normalized position estimates$\hat{\mathbf{x}}_n \in [0,1]^2$ , which are rescaled to metres at evaluation time. -
Training: The network is trained with MSE loss between predicted and ground-truth normalized positions. Position labels are normalized to
$[0, 1]$ using per-axis minimum and maximum coordinates computed from the baseline training fold (the random training portion after spatial and temporal holdouts are removed); the same min/max mapping is applied to all evaluation sets. Training uses the AdamW optimizer with cosine annealing learning rate decay to a minimum of$10^{-6}$ , Kaiming uniform weight initialization, and a weight decay of$10^{-4}$ . In [2], the transformer is trained for 1000 epochs with batch size 32; in this repository,epochsandbatch_sizein theconfigdict at the top ofmain.pycontrol these settings. -
Dataset Split: A single model is trained once and evaluated under complementary scenarios. For 5G datasets, spatial and temporal holdouts are removed from the pool first, then a random train/test split forms the baseline. For Wi-Fi Office datasets, fixed 80/20 folds per week (A/B and C/D) are shared across all three
dataset_typechoices; see Usage for the fold table. Spatial and temporal holdouts are carved from the selected training fold only. For further information and to download the dataset files, visit https://caez.ethz.ch.
The following versions were used for development and testing:
- Python 3.11.3
- NumPy 1.24.3
- PyTorch 2.0.1
- SciPy 1.10.1
- Matplotlib 3.7.1
- pandas 1.5.3
- h5py 3.7.0
- tqdm 4.65.0
Other compatible versions of Python 3 and these packages should work as well.
All datasets are hosted on the CAEZ website. This repository adds two dataset-generation scripts for the newly released datasets introduced in [2]:
| Dataset | Generation script |
|---|---|
| CAEZ-5G-HALLWAY | gen_dataset_hallway.py |
| CAEZ-WIFI-OFFICE-2WEEKS (week 1 and week 2) | make_wifi_dataset_office2weeks.py |
For both of these datasets you can either:
- Download the pre-generated
.npzfiles from CAEZ and use them directly for training, or - Download the raw measurement archives from CAEZ and run the matching script above to build the
.npzyourself.
The published .npz files are produced with the default parameters baked into these scripts (no extra flags required for a matching rebuild). The scripts are updated versions of the older gen_dataset.py and make_wifi_dataset.py workflows in IIP-Group/neural-positioning; see Step 1 under each dataset below for inputs, commands, and output layout.
CAEZ-5G-OUTDOOR (used for evaluation in [2] but not generated here) is available as a preprocessed .npz on CAEZ, or can be built with gen_dataset.py from the IIP-Group/neural-positioning repository.
Paths for training: dataset generation does not read DATA_ROOT. Pass input and output locations on the command line when running the scripts. DATA_ROOT is only used by main.py to locate .npz files for training (Step 2).
The 5G NR datasets contain CSI measurements captured from four O-RUs, each equipped with four antennas. Data was recorded at 51 Hz, with 3,276 subcarriers that are subsequently downsampled by a factor of 12, resulting in 273 active subcarriers. CSI is extracted from the uplink PUSCH channel, spanning a 100 MHz bandwidth centered at 3.45 GHz.
CAEZ-5G-OUTDOOR: Use the processed .npz file from the CAEZ webiste (WorldViz ground truth and outdoor measurement metadata; no generation script in this repo) or generate as described above.
CAEZ-5G-HALLWAY: Unpack the raw archive and run gen_dataset_hallway.py on the PyAerial PUSCH CSI pickles and position CSV (gt-positions-chopper.csv with timestamp, position_x, position_y). Position timestamps (NTP) are shifted by 37 s by default to align with CSI timestamps (PTP) before interpolation. Output is a single *_with_pos.npz with keys H, noise_var, H_max_sc, timestamps, pos, and leap_second_offset. The O-RU positions are shipped with the archive as a .npz file and should be placed in the same location as the position CSV to be added to the output dataset.
python gen_dataset_hallway.py -data_path /path/to/csi \
--pos_csv /path/to/gt-positions-chopper.csv \
--output /path/to/hallway_with_pos.npzOptional flags: --leap_offset (default 37.0), --output (auto-named if omitted). Defaults for -data_path and --pos_csv are ./csi and ./gt-positions-chopper.csv.
Set the base directory where your .npz files live:
export DATA_ROOT=/path/to/your/data/Expected layout under DATA_ROOT (default data_path / dataset entries in the DATASETS dict in main.py; edit those if your files differ):
caez_5g_outdoor/caez_5g_outdoor_full.npz
5g_hallway/5g_hallway_abs_before_downsampling_12_sorted_4_rx_ant_with_pos.npz
Place or copy the outdoor and/or hallway .npz files under DATA_ROOT using the layout above or edit DATASETS in main.py.
Set dataset_type to "5g_outdoor" or "5g_hallway" in the config dict at the top of main.py, then run:
python main.pyThe Wi-Fi dataset features CSI data collected from 4 APs, each equipped with 4 antennas. The CSI was extracted from Wi-Fi frames spanning a 40 MHz bandwidth centered at 5.23 GHz and including 104 active subcarriers. Data was gathered across two measurement campaigns (week 1 and week 2), conducted about one week apart in the same environment.
Run make_wifi_dataset_office2weeks.py once per campaign (week 1 and week 2) on the unpacked measurement folder (ap1/, ap2/, … with HDF5 takes, plus gt-positions-chopper.csv; optional ap_pos.npz with key ap_pos). The ground truth positions are read from the CSV file. CSI is grouped into 50 ms windows (--window-len, default 0.05); positions are interpolated to each window with gap-limited validity (--max-gt-gap, default 0.5 s). Windows with non-finite pos are removed before save.
python make_wifi_dataset_office2weeks.py /path/to/caez-wifi-office-week1 \
--output /path/to/caez-wifi-office-week1.npz
python make_wifi_dataset_office2weeks.py /path/to/caez-wifi-office-week2 \
--output /path/to/caez-wifi-office-week2.npzThe output .npz stores ragged per-window bins (csi_bins, timestamp_bins, snr_bins, samples_per_bin) and pos, timestamps, window_len, grid_start_timestamp, ap_numbers, and related metadata—not the dense H layout of the legacy make_wifi_dataset.py.
Set the base directory where your .npz files live:
export DATA_ROOT=/path/to/your/data/Expected layout under DATA_ROOT (default data_path / dataset entries in the DATASETS dict in main.py; edit those if your files differ):
caez-wifi-office-week1/caez-wifi-office-week1.npz
caez-wifi-office-week2/caez-wifi-office-week2.npz
Place or copy the week 1 and week 2 .npz files under DATA_ROOT using the layout above or edit DATASETS in main.py.
Set dataset_type in the config dict at the top of main.py.
Both week-1 and week-2 .npz files are always loaded. A fixed 80/20 split (seed 0) is applied independently to each week:
| Fold | Week | Role |
|---|---|---|
| A | Week 1 | 80% train |
| B | Week 1 | 20% test |
| C | Week 2 | 80% train |
| D | Week 2 | 20% test |
These four folds are reused for every Wi-Fi dataset_type; which folds are used for training, baseline testing, and cross-week testing is listed in the table under Cross-Week Consistency in Generalization Scenarios.
Spatial and temporal holdouts (if enabled) are removed from the training fold only before fitting; baseline and cross-week test folds B/D are left intact.
Then run:
python main.pyFour evaluation scenarios are implemented and can be toggled in the config dict. They correspond to the testing scenarios described in [2].
All scenarios share a single trained model. Spatial and temporal holdout samples are removed from the training pool/fold so they are never seen during training. For 5G, holdouts precede a random 80/20 baseline split. For Wi-Fi, the fixed folds A/B/C/D are defined first; holdouts are then applied only to the selected training fold.
The de facto standard evaluation scenario used in the literature. For 5G, after spatial and temporal holdouts are removed, the remaining pool is randomly partitioned into 80% training and 20% test. For Wi-Fi, the baseline test set is the fixed fold B, D, or B ∪ D depending on dataset_type (see the Cross-Week Consistency table below). This serves as the reference against which other generalization results are compared.
Holds out all samples whose ground-truth position falls within multiple fixed circles of radius circle_radius. The withheld samples are never seen during training and are used exclusively for evaluation. Circle placement depends on the used dataset. All use fixed circles defined in _CUSTOM_CIRCLE_CENTERS in create_validation_dataset.py.
Holds out a contiguous block of temporal_val_ratio × N samples centred on the recording (or on the week-1 portion of the training fold for wifi_office_both). The withheld samples are never seen during training and are used exclusively for evaluation.
Approximately 1% of the (training) sequence, corresponding to roughly 1 minute of CSI acquisition, is withheld. This yields unseen trajectory lengths of 21 m (5G Hallway), 41 m (5G Outdoor), 13 m (Wi-Fi Office Week 1), and 8 m (Wi-Fi Office Week 2).
Uses the fixed folds A/B/C/D so that single-week and joint-training runs are comparable (same train and test subsets where they overlap).
dataset_type |
Train | Baseline test | Cross-week output |
|---|---|---|---|
wifi_office_week1 |
A | B → train_test.csv |
D → cross_week_validation.csv |
wifi_office_week2 |
C | D → train_test.csv |
B → cross_week_validation.csv |
wifi_office_both |
A ∪ C | B ∪ D → train_test.csv |
B → cross_week_validation_week1.csv, D → cross_week_validation_week2.csv |
Note on [2], Fig. cross-week bar plot: Earlier code scored full weeks after joint training (training/test overlap) and/or used protocol variants that are not identical to this fold design. Regenerate that figure from runs that use the A/B/C/D table above.
All options are controlled through the config dict at the top of main.py. Training loads .npz files from DATASETS, which builds paths as data_path + dataset. By default, data_path is a subdirectory of DATA_ROOT (set via environment variable or the default in main.py). Edit DATASETS if your files use different folder or file names.
| Key | Description |
|---|---|
dataset_type |
One of 5g_outdoor, 5g_hallway, wifi_office_week1, wifi_office_week2, wifi_office_both
|
results_folder |
Sub-folder under results/ where outputs are saved. Default: <dataset_type>_<YYMMDD_HHMM> (set to a string to override) |
epochs |
Number of training epochs (1000 in [2]) |
learning_rate |
Initial learning rate for AdamW |
batch_size |
Mini-batch size (32 in [2]) |
d_model |
Transformer embedding dimension |
num_heads |
Number of self-attention heads |
num_layers |
Number of transformer encoder layers |
d_ff |
FFN hidden dimension |
dropout |
Dropout rate in embedding, encoder, and head |
weight_decay |
AdamW weight decay |
enable_spatial_val |
Whether to run the unseen-regions spatial validation |
enable_temporal_val |
Whether to run the unseen-trajectory temporal validation |
circle_radius |
Circle radius in metres for spatial splits |
All outputs are saved to results/<results_folder>/:
train_test.csv,spatial_validation.csv,temporal_validation.csv,cross_week_validation.csv,cross_week_validation_week1.csv,cross_week_validation_week2.csv: per-sample predicted and ground-truth positions with Euclidean error in metres. For Wi-Fi, see the fold table under Usage for which CSV maps to folds B, D, or B ∪ D.cdf.png: CDF of positioning error for all enabled splits.loss_curve.png: per-epoch train and validation MSE loss (linear scale).loss_curve_log.png: per-epoch train and validation MSE loss (log scale).position_error_map_<split>.png: three-panel figure per split showing ground-truth positions, predicted positions, and an error heatmap coloured by positioning error.config.csv: full configuration and model parameter count.
main.py: Main training, evaluation, and plotting script.gen_dataset_hallway.py: CAEZ-5G-HALLWAY dataset generation (CSI pickles + CSV positions).make_wifi_dataset_office2weeks.py: CAEZ-WIFI-OFFICE week 1/2 dataset generation (HDF5 takes + chopper GT).NN_models.py:CSITransformerNetmodel definition andTransformerModeltrainer wrapper.create_dataset.py: Dataset classes for PyTorch.create_validation_dataset.py: Functions implementing the generalization scenarios from [2].feature_extraction.py: CSI feature extraction functionsparam_config.py: Configuration parameters and plotting utilities
If you use this code (or parts of it), you must cite reference [2].
Code is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
[1] R. Wiesmayr, F. Zumegen, S. Taner, C. Dick, and C. Studer, "CSI-based user positioning, channel charting, and device classification with an NVIDIA 5G testbed," in Asilomar Conf. Signals, Syst., Comput., Oct. 2025.
[2] T.-Y. Müller, F. Zumegen, R. Wiesmayr, and C. Studer, "Spatial and Temporal Generalization of CSI-based Neural Positioning," Jun. 2026, arXiv preprint https://arxiv.org/abs/2606.18150