wi-lab commited on
Commit
a65a228
·
verified ·
1 Parent(s): 98360ca

Release LWM Competition Package

Browse files
README.md ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ # ⚡ LARGE WIRELESS MODELS (LWMs) 2025 CHALLENGE
4
+
5
+ **The goal is to improve performance across five wireless downstream tasks by optimizing a baseline LWM and/or designing new downstream models**
6
+
7
+ [![Model Hub](https://img.shields.io/badge/🤗%20HuggingFace-Model%20Hub-orange?style=flat-square)](https://huggingface.co/wi-lab/lwm-v1.1)
8
+ [![Tutorials](https://img.shields.io/badge/🎓%20Tutorials-Available-brightgreen?style=flat-square)](https://lwm-wireless.net/tutorials)
9
+ [![Website](https://img.shields.io/badge/🌐%20Website-lwm--wireless.net-blue?style=flat-square)](https://lwm-wireless.net/)
10
+ [![Contact](https://img.shields.io/badge/📬%20Contact-lwmwireless@gmail.com-red?style=flat-square)](mailto:lwmwireless@gmail.com)
11
+
12
+ <p align="center">
13
+ <a href="#-challenge-overview">Challenge Overview</a> •
14
+ <a href="#-provided-materials">Provided Materials</a> •
15
+ <a href="#-getting-started">Getting Started</a> •
16
+ <a href="#-submission-process">Submission Process</a> •
17
+ <a href="#-tutorials">Tutorials</a> •
18
+ <a href="#-citation">Citation</a> •
19
+ <a href="#-community--support">Community & Support</a> •
20
+ <a href="#-team">Team</a>
21
+ </p>
22
+
23
+ &nbsp;
24
+
25
+ <a target="_blank" href="https://huggingface.co/spaces/wi-lab/lwm-interactive-demo">
26
+ <img src="https://img.shields.io/badge/▶️%20Try%20Interactive%20Demo-HuggingFace%20Spaces-yellow?style=for-the-badge" height="36px" alt="Try Interactive Demo"/>
27
+ </a>
28
+
29
+ &nbsp;
30
+
31
+ </div>
32
+
33
+ # 📡 Large Wireless Model (LWM) Challenge
34
+
35
+ Welcome to the official repository of the **LWM 2025 Challenge**, a competition designed to advance the state of foundation models in wireless communications and sensing. Participants are invited to optimize a provided baseline Large Wireless Model (LWM) and design downstream models to tackle five core wireless tasks with limited labeled data.
36
+
37
+ ---
38
+
39
+ ## 🧠 About LWM
40
+
41
+ **Large Wireless Model (LWM) 1.1** is a Transformer-based foundation model pre-trained using self-supervised learning on over 1 million unlabeled wireless channel samples. It generates rich, task-agnostic embeddings that significantly outperform raw channel representations on downstream tasks—especially when data is scarce or noisy or downstream models need to be simple.
42
+
43
+ ---
44
+
45
+ ## 🏁 Challenge Overview
46
+
47
+ Participants are given:
48
+
49
+ - A pre-trained LWM 1.1 checkpoint
50
+ - Baseline downstream task models
51
+ - Training, validation, and public test sets for each task
52
+ - Helper functions and templates
53
+
54
+ Your goal is to improve the **Composite Generalization Score (CG-Score)** across these five tasks:
55
+
56
+ 1. **LoS/NLoS Classification** – F1-score
57
+ 2. **Sub-6 GHz Channel to mmWave Beam Prediction** – Top-1 Beam F1-score
58
+ 3. **Channel Interpolation** – Normalized MSE
59
+ 4. **Channel Estimation** – Normalized MSE
60
+ 5. **Localization** – Normalized Localization Error
61
+
62
+ Final rankings are based on hidden test sets evaluated by the organizers.
63
+
64
+ ---
65
+
66
+ ## 📦 Provided Materials
67
+
68
+ This repository contains:
69
+
70
+ - `pretrained_model.py` — Loads the baseline or your refined LWM model
71
+ - `train_heads.py` — The main script for training and evaluating all task-specific models. **This file must not be modified.** It is provided as a standardized template to ensure fairness and consistency across all teams. Participants must design their submissions to align with this script. The organizers will use an equivalent version of `train_heads.py` for final evaluation, and any deviation from the expected structure will result in automatic disqualification.
72
+ - `train_heads_config.py` — Contains training configs and model head definitions
73
+ - `train_lwm.py` — Contains LWM 1.1 pre-training and dataset reproducibility script
74
+ - `utils.py` — Helper functions (training, scoring, data handling)
75
+ - `task_{t}/` — Contains the training, validation, and public test sets for each downstream task. These datasets are used for jointly fine-tuning your refined LWM and training the corresponding task-specific models. While downstream training is restricted to the provided datasets, you are free to use any dataset for LWM pre-training. Participants are granted early access to the **DeepMIMO v4** dataset, which offers new, large-scale scenarios suitable for extended LWM refinement.
76
+ - `requirements.yml` — Conda environment file for dependency setup
77
+
78
+ ---
79
+
80
+ ## 🚀 Getting Started
81
+
82
+ ### 📥 Clone the repo
83
+
84
+ ```bash
85
+ git clone https://github.com/wireless-intelligence-lab/lwm-competition-2025.git
86
+ cd lwm-competition-2025
87
+ ```
88
+
89
+ ### 🛠️ Set up the environment
90
+ ```bash
91
+ conda env create -f requirements.yml
92
+ conda activate lwm_env
93
+ ```
94
+
95
+ ### 🧪 Run baseline pipeline
96
+ ```bash
97
+ python train_heads.py
98
+ ```
99
+ This jointly finetunes LWM and trains downstream heads, evaluates on public test sets, and creates a submission ZIP file.
100
+
101
+
102
+ ### 🧩 Submission Process
103
+ 1. Refine your LWM or downstream heads
104
+ 2. Update `pretrained_model.py`, `train_heads_config.py`, and `utils.py`.
105
+ 3. Run:
106
+ ```bash
107
+ python train_heads.py
108
+ ```
109
+ 4. Submit the generated ZIP file to the competition portal
110
+
111
+ 🛑 **Do not modify `train_heads.py`.** While you may adapt it for local development or experimentation, your final submission **must be fully compatible with the original, unmodified version** provided. The evaluation script used by the organizers assumes this exact structure—any deviation may result in disqualification.
112
+
113
+ ---
114
+
115
+ ## 📚 Tutorials
116
+
117
+ Visit the official tutorials page:
118
+
119
+ 👉 [https://lwm-wireless.net/tutorials](https://lwm-wireless.net/tutorials)
120
+
121
+ ---
122
+
123
+ ## 🧪 Citation
124
+
125
+ If you use the LWM model or its components, please cite:
126
+
127
+ ```bibtex
128
+ @misc{alikhani2025largewirelessmodellwm,
129
+ title={Large Wireless Model (LWM): A Foundation Model for Wireless Channels},
130
+ author={Sadjad Alikhani and Gouranga Charan and Ahmed Alkhateeb},
131
+ year={2025},
132
+ eprint={2411.08872},
133
+ archivePrefix={arXiv},
134
+ primaryClass={cs.IT},
135
+ url={https://arxiv.org/abs/2411.08872},
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 👥 Community & Support
142
+
143
+ * 💬 [Discussion Forum](https://huggingface.co/wi-lab/lwm-v1.1/discussions)
144
+ * 📨 Contact: [lwmwireless@gmail.com](mailto:lwmwireless@gmail.com)
145
+
146
+ ---
147
+
148
+ ## 👨‍🔬 Team
149
+
150
+ Developed by the [Wireless Intelligence Lab](https://wi-lab.net) at Arizona State University.
151
+
152
+ <p align="center">
153
+ <a href="https://scholar.google.com/citations?user=PKjnTR4AAAAJ&hl=en" target="_blank">
154
+ <img src="https://img.shields.io/badge/👤 Sadjad Alikhani-Click to view Scholar profile-blue?style=for-the-badge" alt="Sadjad Alikhani">
155
+ </a>
156
+ &nbsp;
157
+ <a href="https://scholar.google.com/citations?user=MHKcvFMAAAAJ&hl=en" target="_blank">
158
+ <img src="https://img.shields.io/badge/👤 Gouranga Charan-Click to view Scholar profile-green?style=for-the-badge" alt="Gouranga Charan">
159
+ </a>
160
+ &nbsp;
161
+ <a href="https://scholar.google.com/citations?user=dLHw2qcAAAAJ&hl=en" target="_blank">
162
+ <img src="https://img.shields.io/badge/👤 Ahmed Alkhateeb-Click to view Scholar profile-orange?style=for-the-badge" alt="Ahmed Alkhateeb">
163
+ </a>
164
+ </p>
165
+
166
+
167
+
168
+
169
+
170
+
model_checkpoint.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:485611f1a0f819f9c673827b8e613887b39672e97072bd7a412866b49d8dd40f
3
+ size 9960738
pretrained_model.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import numpy as np
5
+
6
+ class LayerNormalization(nn.Module):
7
+ """
8
+ Custom Layer Normalization module with learnable scale and bias.
9
+
10
+ Args:
11
+ d_model (int): Dimensionality of the input embeddings.
12
+ eps (float): A small constant added to variance to avoid division by zero.
13
+ """
14
+ def __init__(self, d_model: int, eps: float = 1e-6) -> None:
15
+ super().__init__()
16
+ self.eps = eps
17
+ self.alpha = nn.Parameter(torch.ones(d_model))
18
+ self.bias = nn.Parameter(torch.zeros(d_model))
19
+
20
+ def forward(self, x):
21
+ mean = x.mean(dim=-1, keepdim=True)
22
+ std = x.std(dim=-1, keepdim=True)
23
+ return self.alpha * (x - mean) / (std + self.eps) + self.bias
24
+
25
+ class Embedding(nn.Module):
26
+ """
27
+ Input embedding module with linear projection and positional encoding.
28
+
29
+ Args:
30
+ element_length (int): Length of each input element (e.g., patch size).
31
+ d_model (int): Output embedding dimension.
32
+ max_len (int): Maximum sequence length for positional embeddings.
33
+ """
34
+ def __init__(self, element_length, d_model, max_len=513):
35
+ super().__init__()
36
+ self.element_length = element_length
37
+ self.d_model = d_model
38
+ self.proj = nn.Linear(element_length, d_model)
39
+ self.pos_embed = nn.Embedding(max_len, d_model)
40
+ self.norm = LayerNormalization(d_model)
41
+
42
+ def forward(self, x):
43
+ seq_len = x.size(1)
44
+ pos = torch.arange(seq_len, dtype=torch.long, device=x.device)
45
+ pos_encodings = self.pos_embed(pos)
46
+ tok_emb = self.proj(x.float())
47
+ embedding = tok_emb + pos_encodings
48
+ return self.norm(embedding)
49
+
50
+ class ScaledDotProductAttention(nn.Module):
51
+ """
52
+ Computes scaled dot-product attention.
53
+
54
+ Args:
55
+ d_k (int): Dimensionality of the key vectors.
56
+ """
57
+ def __init__(self, d_k):
58
+ super().__init__()
59
+ self.d_k = d_k
60
+
61
+ def forward(self, Q, K, V):
62
+ scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(self.d_k)
63
+ attn = F.softmax(scores, dim=-1)
64
+ context = torch.matmul(attn, V)
65
+ return context, attn
66
+
67
+ class MultiHeadAttention(nn.Module):
68
+ """
69
+ Multi-head attention mechanism.
70
+
71
+ Args:
72
+ d_model (int): Total input/output dimension.
73
+ n_heads (int): Number of attention heads.
74
+ dropout (float): Dropout probability applied after attention.
75
+ """
76
+ def __init__(self, d_model, n_heads, dropout):
77
+ super().__init__()
78
+ self.d_k = d_model // n_heads
79
+ self.d_v = d_model // n_heads
80
+ self.n_heads = n_heads
81
+ self.W_Q = nn.Linear(d_model, self.d_k * n_heads)
82
+ self.W_K = nn.Linear(d_model, self.d_k * n_heads)
83
+ self.W_V = nn.Linear(d_model, self.d_v * n_heads)
84
+ self.linear = nn.Linear(n_heads * self.d_v, d_model)
85
+ self.dropout = nn.Dropout(dropout)
86
+ self.scaled_dot_attn = ScaledDotProductAttention(self.d_k)
87
+
88
+ def forward(self, Q, K, V):
89
+ residual, batch_size = Q, Q.size(0)
90
+ q_s = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
91
+ k_s = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
92
+ v_s = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_v).transpose(1, 2)
93
+
94
+ context, attn = self.scaled_dot_attn(q_s, k_s, v_s)
95
+ output = context.transpose(1, 2).contiguous().view(batch_size, -1, self.n_heads * self.d_v)
96
+ output = self.linear(output)
97
+ return residual + self.dropout(output), attn
98
+
99
+ class PoswiseFeedForwardNet(nn.Module):
100
+ """
101
+ Position-wise feed-forward network applied to each token independently.
102
+
103
+ Args:
104
+ d_model (int): Input and output dimensionality.
105
+ d_ff (int): Hidden layer size in the feed-forward block.
106
+ dropout (float): Dropout rate applied between layers.
107
+ """
108
+ def __init__(self, d_model, d_ff, dropout):
109
+ super().__init__()
110
+ self.fc1 = nn.Linear(d_model, d_ff)
111
+ self.fc2 = nn.Linear(d_ff, d_model)
112
+ self.dropout = nn.Dropout(dropout)
113
+
114
+ def forward(self, x):
115
+ return self.fc2(self.dropout(F.relu(self.fc1(x))))
116
+
117
+ class EncoderLayer(nn.Module):
118
+ """
119
+ Transformer encoder block composed of multi-head self-attention,
120
+ feed-forward network, and layer normalization.
121
+
122
+ Args:
123
+ d_model (int): Embedding dimension.
124
+ n_heads (int): Number of attention heads.
125
+ d_ff (int): Hidden size of the feed-forward subnetwork.
126
+ dropout (float): Dropout probability.
127
+ """
128
+ def __init__(self, d_model, n_heads, d_ff, dropout):
129
+ super().__init__()
130
+ self.enc_self_attn = MultiHeadAttention(d_model, n_heads, dropout)
131
+ self.pos_ffn = PoswiseFeedForwardNet(d_model, d_ff, dropout)
132
+ self.norm1 = LayerNormalization(d_model)
133
+ self.norm2 = LayerNormalization(d_model)
134
+
135
+ def forward(self, enc_inputs):
136
+ attn_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs)
137
+ attn_outputs = self.norm1(enc_inputs + attn_outputs)
138
+ ff_outputs = self.pos_ffn(attn_outputs)
139
+ enc_outputs = self.norm2(attn_outputs + ff_outputs)
140
+ return enc_outputs, attn
141
+
142
+ class lwm(nn.Module):
143
+ """
144
+ Large Wireless Model (LWM): A Transformer-based encoder model for
145
+ extracting rich embeddings from wireless channel data.
146
+
147
+ Args:
148
+ element_length (int): Dimensionality of input tokens.
149
+ d_model (int): Embedding dimension used throughout the network.
150
+ n_layers (int): Number of Transformer encoder layers.
151
+ max_len (int): Maximum number of tokens (sequence length).
152
+ n_heads (int): Number of self-attention heads.
153
+ dropout (float): Dropout probability used across the model.
154
+ """
155
+ def __init__(self, element_length=32, d_model=128, n_layers=12, max_len=513, n_heads=8, dropout=0.1):
156
+ super().__init__()
157
+
158
+ self.element_length = element_length
159
+ self.d_model = d_model
160
+ self.n_layers = n_layers
161
+ self.max_len = max_len
162
+ self.n_heads = n_heads
163
+ self.dropout = dropout
164
+
165
+ self.embedding = Embedding(element_length, d_model, max_len)
166
+ self.layers = nn.ModuleList(
167
+ [EncoderLayer(d_model, n_heads, d_model*4, dropout) for _ in range(n_layers)]
168
+ )
169
+ self.linear = nn.Linear(d_model, d_model)
170
+ self.norm = LayerNormalization(d_model)
171
+
172
+ embed_weight = self.embedding.proj.weight
173
+ _, n_dim = embed_weight.size()
174
+ self.decoder = nn.Linear(d_model, n_dim, bias=False)
175
+ self.decoder_bias = nn.Parameter(torch.zeros(n_dim))
176
+
177
+ def forward(self, input_ids, masked_pos=None):
178
+ """
179
+ Forward pass of the LWM model.
180
+
181
+ Args:
182
+ input_ids (torch.Tensor): Input tensor of shape (B, T, element_length), where
183
+ B is batch size, T is sequence length.
184
+ masked_pos (torch.Tensor, optional): Indices of masked positions for patch prediction.
185
+ If provided, returns logits for these positions.
186
+
187
+ Returns:
188
+ Tuple[torch.Tensor, torch.Tensor] if masked_pos is provided:
189
+ - logits_lm: Predicted values for masked positions.
190
+ - output: Full contextualized embeddings for all tokens.
191
+
192
+ torch.Tensor if masked_pos is None:
193
+ - output: Full contextualized embeddings of shape (B, T, d_model).
194
+ """
195
+ output = self.embedding(input_ids)
196
+
197
+ for layer in self.layers:
198
+ output, attn = layer(output)
199
+
200
+ if masked_pos is not None:
201
+ masked_pos = masked_pos.long()[:, :, None].expand(-1, -1, output.size(-1))
202
+ h_masked = torch.gather(output, 1, masked_pos)
203
+ h_masked = self.norm(F.relu(self.linear(h_masked)))
204
+ logits_lm = self.decoder(h_masked) + self.decoder_bias
205
+ return logits_lm, output
206
+ else:
207
+ return output
requirements.yml ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: lwm_env
2
+ channels:
3
+ - pytorch
4
+ - nvidia
5
+ - conda-forge
6
+ dependencies:
7
+ - asttokens=3.0.0=pyhd8ed1ab_1
8
+ - blas=1.0=mkl
9
+ - brotli-python=1.1.0=py312h275cf98_2
10
+ - bzip2=1.0.8=h2466b09_7
11
+ - ca-certificates=2025.4.26=h4c7d964_0
12
+ - certifi=2025.4.26=pyhd8ed1ab_0
13
+ - cffi=1.17.1=py312h4389bb4_0
14
+ - charset-normalizer=3.4.2=pyhd8ed1ab_0
15
+ - cloudpickle=3.1.1=pyhd8ed1ab_0
16
+ - colorama=0.4.6=pyhd8ed1ab_1
17
+ - comm=0.2.2=pyhd8ed1ab_1
18
+ - cpython=3.12.10=py312hd8ed1ab_0
19
+ - cuda-cccl=12.9.27=0
20
+ - cuda-cccl_win-64=12.9.27=0
21
+ - cuda-cudart=12.1.105=0
22
+ - cuda-cudart-dev=12.1.105=0
23
+ - cuda-cupti=12.1.105=0
24
+ - cuda-libraries=12.1.0=0
25
+ - cuda-libraries-dev=12.1.0=0
26
+ - cuda-nvrtc=12.1.105=0
27
+ - cuda-nvrtc-dev=12.1.105=0
28
+ - cuda-nvtx=12.1.105=0
29
+ - cuda-opencl=12.9.19=0
30
+ - cuda-opencl-dev=12.9.19=0
31
+ - cuda-profiler-api=12.9.19=0
32
+ - cuda-runtime=12.1.0=0
33
+ - cuda-version=12.9=3
34
+ - debugpy=1.8.14=py312h275cf98_0
35
+ - decorator=5.2.1=pyhd8ed1ab_0
36
+ - exceptiongroup=1.3.0=pyhd8ed1ab_0
37
+ - executing=2.2.0=pyhd8ed1ab_0
38
+ - filelock=3.18.0=pyhd8ed1ab_0
39
+ - freetype=2.13.3=h57928b3_1
40
+ - h2=4.2.0=pyhd8ed1ab_0
41
+ - hpack=4.1.0=pyhd8ed1ab_0
42
+ - hyperframe=6.1.0=pyhd8ed1ab_0
43
+ - idna=3.10=pyhd8ed1ab_1
44
+ - importlib-metadata=8.7.0=pyhe01879c_1
45
+ - intel-openmp=2025.1.0=h57928b3_980
46
+ - ipykernel=6.29.5=pyh4bbf305_0
47
+ - ipython=8.36.0=pyh9ab4c32_0
48
+ - jedi=0.19.2=pyhd8ed1ab_1
49
+ - jinja2=3.1.6=pyhd8ed1ab_0
50
+ - joblib=1.5.1=pyhd8ed1ab_0
51
+ - jupyter_client=8.6.3=pyhd8ed1ab_1
52
+ - jupyter_core=5.8.0=pyh5737063_0
53
+ - khronos-opencl-icd-loader=2024.10.24=h2466b09_1
54
+ - krb5=1.21.3=hdf4eb48_0
55
+ - lcms2=2.17=hbcf6048_0
56
+ - lerc=4.0.0=h6470a55_1
57
+ - libblas=3.9.0=1_h8933c1f_netlib
58
+ - libcblas=3.9.0=12_hb3dda5d_netlib
59
+ - libcublas=12.1.0.26=0
60
+ - libcublas-dev=12.1.0.26=0
61
+ - libcufft=11.0.2.4=0
62
+ - libcufft-dev=11.0.2.4=0
63
+ - libcurand=10.3.10.19=0
64
+ - libcurand-dev=10.3.10.19=0
65
+ - libcusolver=11.4.4.55=0
66
+ - libcusolver-dev=11.4.4.55=0
67
+ - libcusparse=12.0.2.55=0
68
+ - libcusparse-dev=12.0.2.55=0
69
+ - libdeflate=1.22=h2466b09_0
70
+ - libexpat=2.6.3=he0c23c2_0
71
+ - libffi=3.4.2=h8ffe710_5
72
+ - libfreetype=2.13.3=h57928b3_1
73
+ - libfreetype6=2.13.3=h0b5ce68_1
74
+ - libhwloc=2.11.2=default_hc8275d1_1000
75
+ - libiconv=1.18=h135ad9c_1
76
+ - libjpeg-turbo=3.1.0=h2466b09_0
77
+ - liblapack=3.9.0=12_h13b7882_netlib
78
+ - libnpp=12.0.2.50=0
79
+ - libnpp-dev=12.0.2.50=0
80
+ - libnvjitlink=12.1.105=0
81
+ - libnvjitlink-dev=12.1.105=0
82
+ - libnvjpeg=12.1.1.14=0
83
+ - libnvjpeg-dev=12.1.1.14=0
84
+ - libpng=1.6.47=h7a4582a_0
85
+ - libsodium=1.0.20=hc70643c_0
86
+ - libsqlite=3.46.1=h2466b09_0
87
+ - libtiff=4.7.0=hfc51747_1
88
+ - libuv=1.50.0=h2466b09_0
89
+ - libwebp=1.5.0=h3b0e114_0
90
+ - libwebp-base=1.5.0=h3b0e114_0
91
+ - libxcb=1.16=h013a479_1
92
+ - libxml2=2.13.8=h442d1da_0
93
+ - libzlib=1.3.1=h2466b09_1
94
+ - llvmlite=0.44.0=py312h1f7db74_1
95
+ - m2w64-gcc-libgfortran=5.3.0=6
96
+ - m2w64-gcc-libs=5.3.0=7
97
+ - m2w64-gcc-libs-core=5.3.0=7
98
+ - m2w64-gmp=6.1.0=2
99
+ - m2w64-libwinpthread-git=5.0.0.4634.697f757=2
100
+ - markupsafe=3.0.2=py312h31fea79_1
101
+ - matplotlib-inline=0.1.7=pyhd8ed1ab_1
102
+ - mkl=2023.1.0=h6a75c08_48682
103
+ - mpmath=1.3.0=pyhd8ed1ab_1
104
+ - msys2-conda-epoch=20160418=1
105
+ - nest-asyncio=1.6.0=pyhd8ed1ab_1
106
+ - networkx=3.4.2=pyh267e887_2
107
+ - numba=0.61.2=py312hcccf92d_0
108
+ - numpy=2.2.6=py312h3150e54_0
109
+ - opencl-headers=2024.10.24=he0c23c2_0
110
+ - openjpeg=2.5.3=h4d64b90_0
111
+ - openssl=3.5.0=ha4e3fda_1
112
+ - packaging=25.0=pyh29332c3_1
113
+ - parso=0.8.4=pyhd8ed1ab_1
114
+ - pickleshare=0.7.5=pyhd8ed1ab_1004
115
+ - pillow=10.4.0=py312h381445a_1
116
+ - pip=24.2=pyh8b19718_1
117
+ - platformdirs=4.3.8=pyhe01879c_0
118
+ - prompt-toolkit=3.0.51=pyha770c72_0
119
+ - psutil=7.0.0=py312h4389bb4_0
120
+ - pthread-stubs=0.4=hcd874cb_1001
121
+ - pthreads-win32=2.9.1=h2466b09_4
122
+ - pure_eval=0.2.3=pyhd8ed1ab_1
123
+ - pycparser=2.22=pyh29332c3_1
124
+ - pygments=2.19.1=pyhd8ed1ab_0
125
+ - pynndescent=0.5.13=pyhd8ed1ab_1
126
+ - pysocks=1.7.1=pyh09c184e_7
127
+ - python=3.12.6=hce54a09_1_cpython
128
+ - python-dateutil=2.9.0.post0=pyhff2d567_1
129
+ - python_abi=3.12=7_cp312
130
+ - pytorch=2.5.1=py3.12_cuda12.1_cudnn9_0
131
+ - pytorch-cuda=12.1=hde6ce7c_6
132
+ - pytorch-mutex=1.0=cuda
133
+ - pywin32=307=py312h275cf98_3
134
+ - pyyaml=6.0.2=py312h31fea79_2
135
+ - pyzmq=26.4.0=py312hd7027bb_0
136
+ - requests=2.32.3=pyhd8ed1ab_1
137
+ - scikit-learn=1.6.1=py312h816cc57_0
138
+ - setuptools=75.1.0=pyhd8ed1ab_0
139
+ - six=1.17.0=pyhd8ed1ab_0
140
+ - spyder-kernels=3.0.5=win_pyh7428d3b_0
141
+ - stack_data=0.6.3=pyhd8ed1ab_1
142
+ - sympy=1.14.0=pyh04b8f61_5
143
+ - tbb=2021.13.0=h62715c5_1
144
+ - threadpoolctl=3.6.0=pyhecae5ae_0
145
+ - tk=8.6.13=h5226925_1
146
+ - tornado=6.5.1=py312h4389bb4_0
147
+ - tqdm=4.67.1=pyhd8ed1ab_1
148
+ - traitlets=5.14.3=pyhd8ed1ab_1
149
+ - typing_extensions=4.13.2=pyh29332c3_0
150
+ - tzdata=2024a=h8827d51_1
151
+ - ucrt=10.0.22621.0=h57928b3_0
152
+ - umap-learn=0.5.7=py312h2e8e312_1
153
+ - urllib3=2.4.0=pyhd8ed1ab_0
154
+ - vc=14.3=h8a93ad2_21
155
+ - vc14_runtime=14.40.33810=ha82c5b3_21
156
+ - vs2015_runtime=14.40.33810=h3bf8584_21
157
+ - wcwidth=0.2.13=pyhd8ed1ab_1
158
+ - wheel=0.44.0=pyhd8ed1ab_0
159
+ - win_inet_pton=1.1.0=pyh7428d3b_8
160
+ - xorg-kbproto=1.0.7=hcd874cb_1002
161
+ - xorg-libx11=1.8.9=h0076a8d_1
162
+ - xorg-libxau=1.0.11=hcd874cb_0
163
+ - xorg-libxdmcp=1.1.3=hcd874cb_0
164
+ - xorg-xextproto=7.3.0=hcd874cb_1003
165
+ - xorg-xproto=7.0.31=hcd874cb_1007
166
+ - xz=5.2.6=h8d14728_0
167
+ - yaml=0.2.5=h8ffe710_2
168
+ - zeromq=4.3.5=ha9f60a1_7
169
+ - zipp=3.22.0=pyhd8ed1ab_0
170
+ - zstandard=0.23.0=py312h4389bb4_2
171
+ - zstd=1.5.7=hbeecb71_2
172
+ - pip:
173
+ - contourpy==1.3.2
174
+ - cycler==0.12.1
175
+ - fonttools==4.58.0
176
+ - kiwisolver==1.4.8
177
+ - matplotlib==3.10.3
178
+ - pyparsing==3.2.3
179
+ - scipy==1.15.3
180
+ - torchaudio==2.5.1
181
+ - torchvision==0.20.1
182
+ - deepmimo
183
+ prefix: C:\Users\salikha4\AppData\Local\miniforge3\envs\lwm_env
task_1/config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": 1,
3
+ "n_total_candidates": 85055,
4
+ "n_train_samples": 6,
5
+ "n_val_samples": 500,
6
+ "n_public_test_samples": 1500,
7
+ "n_antennas_bs": 8,
8
+ "n_subcarriers": 8,
9
+ "max_head_parameters": 500000,
10
+ "max_wrapper_parameters": 500000,
11
+ "report_loss_function": "CrossEntropyLoss",
12
+ "num_classes": 2,
13
+ "bounding_box_coord": null,
14
+ "snr": 10
15
+ }
task_1/test_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fcbe4647720f2c8b33a3928dc1535a5f1d1e974c76e589bfe2a6da3b3c2e956
3
+ size 775460
task_1/train_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4db0f33461da632166ebc0a550f8e37e2968d47c27c5bd551da6f6d8ad22540a
3
+ size 4586
task_1/val_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be18692b4527d205ad0db47a7c20d60b5fc4a71e30e039744febdb6131a51a85
3
+ size 259486
task_2/config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": 2,
3
+ "n_total_candidates": 67769,
4
+ "n_train_samples": 1000,
5
+ "n_val_samples": 500,
6
+ "n_public_test_samples": 1500,
7
+ "n_antennas_bs": 64,
8
+ "n_subcarriers": 16,
9
+ "max_head_parameters": 500000,
10
+ "max_wrapper_parameters": 5000000,
11
+ "report_loss_function": "CrossEntropyLoss",
12
+ "num_classes": 64,
13
+ "bounding_box_coord": null,
14
+ "snr": null
15
+ }
task_2/test_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9bca2d3256e34727edc60368990d455ae63773a37b604d04aaac71aaac7d6dab
3
+ size 12295460
task_2/train_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d7a7055d6ce7949424f16a356d1038e82a47acb97621e27ec651452c14c9cdf2
3
+ size 8197482
task_2/val_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:931ff8ac11532dc0dba852e13c1aa94bd6b2613377ff6ddbf39035628647cfbf
3
+ size 4099486
task_3/config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": 3,
3
+ "n_total_candidates": 74226,
4
+ "n_train_samples": 300,
5
+ "n_val_samples": 500,
6
+ "n_public_test_samples": 1500,
7
+ "n_antennas_bs": 64,
8
+ "n_subcarriers": 64,
9
+ "max_head_parameters": 500000,
10
+ "max_wrapper_parameters": 5000000,
11
+ "report_loss_function": "MSELoss",
12
+ "num_classes": null,
13
+ "bounding_box_coord": null,
14
+ "snr": null
15
+ }
task_3/train_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:60150d8975c362133c104a65ec7d036e48a16c947fff3a0eedcc934530759b81
3
+ size 19662314
task_3/val_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d48d0644282d658ef2954f64b8fdce8b17056355c0f9b49674d9197acd3c1c1d
3
+ size 32769502
task_4/config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": 4,
3
+ "n_total_candidates": 75666,
4
+ "n_train_samples": 250,
5
+ "n_val_samples": 500,
6
+ "n_public_test_samples": 1500,
7
+ "n_antennas_bs": 32,
8
+ "n_subcarriers": 32,
9
+ "max_head_parameters": 500000,
10
+ "max_wrapper_parameters": 5000000,
11
+ "report_loss_function": "MSELoss",
12
+ "num_classes": null,
13
+ "bounding_box_coord": null,
14
+ "snr": 10
15
+ }
task_4/test_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a6d9555fa19cd9c633c643029c5c1fc8b8984c352a731cac184c8529c772fe8
3
+ size 24577508
task_4/train_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5210cd429217f85010d329b6f91837945d7bd7092f4d00b7c802a67c18f358ff
3
+ size 4097514
task_4/val_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34901cf96ac5acf22578c3622aeca9e815ede2b0032bfe15702de48ed72ad32e
3
+ size 8193502
task_5/config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": 5,
3
+ "n_total_candidates": 75044,
4
+ "n_train_samples": 450,
5
+ "n_val_samples": 500,
6
+ "n_public_test_samples": 1500,
7
+ "n_antennas_bs": 256,
8
+ "n_subcarriers": 32,
9
+ "max_head_parameters": 500000,
10
+ "max_wrapper_parameters": 5000000,
11
+ "report_loss_function": "MSELoss",
12
+ "num_classes": null,
13
+ "bounding_box_coord": [
14
+ [
15
+ -144.9029998779297,
16
+ -164.35699462890625
17
+ ],
18
+ [
19
+ 145.0970001220703,
20
+ 163.64300537109375
21
+ ]
22
+ ],
23
+ "snr": null
24
+ }
task_5/train_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bec5d102cd89fa1b0e697db6f311821081422d702ca783c201c5ae68e155d174
3
+ size 29496298
task_5/val_data.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:40c81e686ea6d5fbac4b3ea5102ebb92585193a0f4da5ad20b7440160aedd7ca
3
+ size 32773470
train_heads.py ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import json
5
+ import numpy as np
6
+ from torch.utils.data import TensorDataset, DataLoader
7
+ import shutil
8
+ from tqdm import tqdm
9
+ from sklearn.metrics import f1_score
10
+ import matplotlib.pyplot as plt
11
+ from typing import Optional, Tuple, List, Dict, Any
12
+ import sys
13
+ import warnings
14
+ warnings.filterwarnings("ignore")
15
+ from utils import embedding_space_visual, tokenizer, plot_radar_chart
16
+ from pretrained_model import lwm
17
+ import train_heads_config as thc
18
+
19
+ # Set environment variable for CuBLAS deterministic behavior
20
+ os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
21
+
22
+ # Ensure deterministic behavior
23
+ torch.backends.cudnn.deterministic = True
24
+ torch.backends.cudnn.benchmark = False
25
+ torch.use_deterministic_algorithms(True)
26
+
27
+ # Worker initialization for DataLoader to ensure reproducible shuffling
28
+ def worker_init_fn(worker_id):
29
+ np.random.seed(42 + worker_id)
30
+
31
+ # List of TaskHeads
32
+ task_heads = [
33
+ thc.LosNlosClassificationHead,
34
+ thc.BeamPredictionHead,
35
+ thc.ChannelInterpolationHead,
36
+ thc.ChannelEstimationHead,
37
+ thc.ChannelChartingHead
38
+ ]
39
+
40
+ # Fine-tuning wrapper for LWM and downstream model
41
+ class FineTuningWrapper(nn.Module):
42
+ def __init__(self, model, task_head, fine_tune_layers="full"):
43
+ """
44
+ Initialize the FineTuningWrapper to manage fine-tuning of a model with a task-specific head.
45
+
46
+ Args:
47
+ model (nn.Module): The base model (e.g., LWM) to be fine-tuned.
48
+ task_head (nn.Module): The task-specific head for downstream tasks.
49
+ fine_tune_layers (str or list, optional): Specifies which layers to fine-tune.
50
+ If "full", all model layers are unfrozen. If a list, only specified layers are unfrozen.
51
+ Defaults to "full".
52
+
53
+ Raises:
54
+ ValueError: If a specified layer in fine_tune_layers is not found in the model.
55
+ """
56
+ super().__init__()
57
+ self.model = model
58
+ self.task_head = task_head
59
+
60
+ # Freeze all layers initially
61
+ for param in self.model.parameters():
62
+ param.requires_grad = False
63
+
64
+ # Handle fine-tuning layers
65
+ if fine_tune_layers is not None:
66
+ if fine_tune_layers == "full":
67
+ # Unfreeze all layers if "full" is specified
68
+ for param in self.model.parameters():
69
+ param.requires_grad = True
70
+ else:
71
+ # Get a list of all available layer names in the model
72
+ available_layers = [name for name, _ in self.model.named_parameters()]
73
+
74
+ # Validate that specified layers exist in the model
75
+ for layer in fine_tune_layers:
76
+ if not any(layer in lname for lname in available_layers):
77
+ raise ValueError(
78
+ f"Layer '{layer}' not found in the model. "
79
+ f"Available layers: {available_layers}"
80
+ )
81
+
82
+ # Unfreeze only the specified layers
83
+ for name, param in self.model.named_parameters():
84
+ if any(layer in name for layer in fine_tune_layers):
85
+ param.requires_grad = True
86
+
87
+ def forward(self, x, input_type="cls_emb", selected_tokens=None):
88
+ """
89
+ Forward pass through the model and task head, processing input based on specified type.
90
+
91
+ Args:
92
+ x (torch.Tensor): Input tensor to the model.
93
+ input_type (str, optional): Type of embedding to extract from the model.
94
+ Options: "raw", "cls_emb", "channel_emb", "combined", "mean_pooled",
95
+ "arbitrary_concat", "arbitrary_meanPooled". Defaults to "cls_emb".
96
+ selected_tokens (list, optional): List of token indices for "arbitrary_concat"
97
+ or "arbitrary_meanPooled" input types. Defaults to None.
98
+
99
+ Returns:
100
+ torch.Tensor: Output of the task head after processing the input embeddings.
101
+ """
102
+ if input_type == "raw":
103
+ # Use the original raw channel input directly for the downstream task
104
+ task_input = x
105
+
106
+ else:
107
+ # Pass input through the LWM model to obtain transformer embeddings
108
+ embeddings = self.model(x)
109
+
110
+ if input_type == "cls_emb":
111
+ # Extract only the [CLS] token embedding (assumed to be at index 0)
112
+ task_input = embeddings[:, [0]]
113
+
114
+ elif input_type == "channel_emb":
115
+ # Use all patch embeddings except the [CLS] token
116
+ task_input = embeddings[:, 1:]
117
+
118
+ elif input_type == "combined":
119
+ # Concatenate [CLS] and patch embeddings for full representation
120
+ task_input = embeddings
121
+
122
+ elif input_type == "mean_pooled":
123
+ # Compute the mean over all token embeddings and retain sequence dimension
124
+ task_input = torch.mean(embeddings, dim=1).unsqueeze(1)
125
+
126
+ elif input_type == "arbitrary_concat":
127
+ # Concatenate a selected subset of token embeddings by index
128
+ # `selected_tokens` should be a list of token indices to include
129
+ task_input = embeddings[:, selected_tokens]
130
+
131
+ elif input_type == "arbitrary_meanPooled":
132
+ # Compute mean-pooled embedding over a selected subset of tokens
133
+ # and add a singleton sequence dimension
134
+ task_input = torch.mean(embeddings[:, selected_tokens], dim=1).unsqueeze(1)
135
+
136
+ return self.task_head(task_input)
137
+
138
+ def nmse(y_true, y_pred):
139
+ """
140
+ Calculate the Normalized Mean Squared Error (NMSE) between true and predicted values.
141
+
142
+ Args:
143
+ y_true (array-like): Ground truth values.
144
+ y_pred (array-like): Predicted values.
145
+
146
+ Returns:
147
+ float: The NMSE value, computed as the mean squared error divided by the mean
148
+ squared magnitude of the true values.
149
+ """
150
+ y_true = np.array(y_true)
151
+ y_pred = np.array(y_pred)
152
+ return np.mean(np.abs(y_true - y_pred)**2) / np.mean(np.abs(y_true)**2)
153
+
154
+ def pow2db(nmse):
155
+ """
156
+ Convert a Normalized Mean Squared Error (NMSE) value to decibels (dB).
157
+
158
+ Args:
159
+ nmse (float): The NMSE value to convert.
160
+
161
+ Returns:
162
+ float: The NMSE value in decibels, calculated as 10 * log10(nmse).
163
+ """
164
+ return 10 * np.log10(nmse)
165
+
166
+ def finetune(
167
+ base_model: nn.Module,
168
+ train_loader: DataLoader,
169
+ val_loader: Optional[DataLoader] = None,
170
+ test_loader: Optional[DataLoader] = None,
171
+ input_type: str = "cls_emb",
172
+ fine_tune_layers: Optional[str] = None,
173
+ optimizer_config: Optional[Dict[str, Any]] = None,
174
+ scheduler_config: Optional[Dict[str, Any]] = None,
175
+ epochs: int = 50,
176
+ device: str = "cuda",
177
+ task: Optional[str] = None,
178
+ d_model: Optional[int] = None,
179
+ sequence_length: Optional[int] = None,
180
+ selected_tokens: Optional[List[int]] = None,
181
+ bbox_coord: Optional[float] = None,
182
+ max_head_pars: int = 1e5,
183
+ max_wrapper_pars: int = 3e6,
184
+ ) -> Tuple[nn.Module, List[float], List[float], List[float], List[float], List[torch.Tensor], List[torch.Tensor]]:
185
+ """
186
+ Fine-tune a pre-trained base model with a task-specific head on a given dataset.
187
+
188
+ Args:
189
+ base_model (nn.Module): Pre-trained base model (e.g., LWM) to fine-tune.
190
+ train_loader (DataLoader): DataLoader for the training dataset.
191
+ val_loader (Optional[DataLoader]): DataLoader for the validation dataset. Defaults to None.
192
+ test_loader (Optional[DataLoader]): DataLoader for the test dataset. Defaults to None.
193
+ input_type (str): Type of input embedding to use. Options: 'cls_emb', 'mean_pooled',
194
+ 'channel_emb', 'combined', 'arbitrary_meanPooled'. Defaults to 'cls_emb'.
195
+ fine_tune_layers (Optional[str]): Layers to fine-tune in the base model. If 'full', all
196
+ layers are fine-tuned; if a list, only specified layers are fine-tuned. Defaults to None.
197
+ optimizer_config (Optional[Dict[str, Any]]): Configuration for the optimizer.
198
+ Defaults to {'lr': 1e-3} if None.
199
+ scheduler_config (Optional[Dict[str, Any]]): Configuration for the learning rate scheduler.
200
+ Defaults to {'step_size': 1000, 'gamma': 0.99} if None.
201
+ epochs (int): Number of training epochs. Defaults to 50.
202
+ device (str): Device for training ('cuda' or 'cpu'). Defaults to 'cuda'.
203
+ task (Optional[str]): Task name. Options: 'LosNlosClassification', 'BeamPrediction',
204
+ 'ChannelInterpolation', 'ChannelEstimation', 'ChannelCharting'. Defaults to None.
205
+ d_model (Optional[int]): Dimensionality of the model embeddings. Required.
206
+ sequence_length (Optional[int]): Length of the input sequence. Required for
207
+ 'channel_emb' or 'combined' input types.
208
+ selected_tokens (Optional[List[int]]): List of token indices for 'arbitrary_meanPooled'
209
+ or 'arbitrary_concat' input types. Defaults to None.
210
+ bbox_coord (Optional[float]): Bounding box coordinate (not used in the function).
211
+ Defaults to None.
212
+ max_head_pars (int): Maximum allowed parameters in the task head. Defaults to 100,000.
213
+ max_wrapper_pars (int): Maximum allowed parameters in the wrapper. Defaults to 3,000,000.
214
+
215
+ Returns:
216
+ Tuple containing:
217
+ - nn.Module: Fine-tuned wrapper model.
218
+ - List[float]: Training losses per epoch.
219
+ - List[float]: Validation losses per epoch.
220
+ - List[float]: Test loss (single value) after training.
221
+ - List[float]: Task-specific score (e.g., F1-score or normalized score).
222
+ - List[torch.Tensor]: Ground truth labels from the test set.
223
+ - List[torch.Tensor]: Predictions from the test set.
224
+
225
+ Raises:
226
+ ValueError: If task, d_model, or input_type is invalid, or required parameters
227
+ (e.g., sequence_length, selected_tokens) are missing.
228
+ """
229
+ # Validate inputs
230
+ if task is None or d_model is None:
231
+ raise ValueError("Task and d_model must be provided.")
232
+ if input_type not in ["cls_emb", "mean_pooled", "channel_emb", "combined", "arbitrary_meanPooled"]:
233
+ raise ValueError(f"Invalid input_type: {input_type}")
234
+
235
+ # Determine number of patches based on input type
236
+ if input_type in ["cls_emb", "mean_pooled", "arbitrary_meanPooled"]:
237
+ n_patches = 1
238
+ elif input_type == "channel_emb":
239
+ if sequence_length is None:
240
+ raise ValueError("sequence_length must be provided for input_type 'channel_emb'.")
241
+ n_patches = sequence_length - 1
242
+ elif input_type == "combined":
243
+ if sequence_length is None:
244
+ raise ValueError("sequence_length must be provided for input_type 'combined'.")
245
+ n_patches = sequence_length
246
+ else: # arbitrary_meanPooled
247
+ if selected_tokens is None:
248
+ raise ValueError("selected_tokens must be provided for input_type 'arbitrary_meanPooled'.")
249
+ n_patches = len(selected_tokens)
250
+
251
+ # Define input dimension
252
+ input_dim = (n_patches, d_model)
253
+
254
+ # Dynamically determine output_dim for regression tasks
255
+ output_dim = None
256
+ if task in ["ChannelInterpolation", "ChannelEstimation"]:
257
+ for batch in train_loader:
258
+ output_dim = batch[1].shape[1:]
259
+ break # Use the first batch to determine output shape
260
+
261
+ # Handle DataParallel models
262
+ if isinstance(base_model, nn.DataParallel):
263
+ base_model = base_model.module
264
+
265
+ # Initialize task-specific head
266
+ if task == "LosNlosClassification":
267
+ task_head = thc.LosNlosClassificationHead(input_dim)
268
+ elif task == "BeamPrediction":
269
+ task_head = thc.BeamPredictionHead(input_dim)
270
+ elif task == "ChannelInterpolation":
271
+ if output_dim is None:
272
+ raise ValueError("output_dim could not be determined for ChannelInterpolation.")
273
+ task_head = thc.ChannelInterpolationHead(input_dim, output_dim)
274
+ elif task == "ChannelEstimation":
275
+ if output_dim is None:
276
+ raise ValueError("output_dim could not be determined for ChannelEstimation.")
277
+ task_head = thc.ChannelEstimationHead(input_dim, output_dim)
278
+ elif task == "ChannelCharting":
279
+ task_head = thc.ChannelChartingHead(input_dim)
280
+ else:
281
+ raise ValueError(f"Unsupported task: {task}")
282
+
283
+ # Set up loss criterion
284
+ if task in ["LosNlosClassification", "BeamPrediction"]:
285
+ criterion = nn.CrossEntropyLoss()
286
+ elif task in ["ChannelInterpolation", "ChannelEstimation", "ChannelCharting"]:
287
+ criterion = nn.MSELoss()
288
+
289
+ # Initialize the fine-tuning wrapper
290
+ fine_tune_layers_config = None if task == "LosNlosClassification" else fine_tune_layers
291
+ wrapper = FineTuningWrapper(
292
+ model=base_model,
293
+ task_head=task_head,
294
+ fine_tune_layers=fine_tune_layers_config
295
+ )
296
+ wrapper = wrapper.to(device)
297
+
298
+ n_head_pars = count_parameters(wrapper.task_head)
299
+ n_wrapper_pars = count_parameters(wrapper)
300
+ print(f"\nNumber of head parameters: {n_head_pars}")
301
+ print(f"Number of wrapper parameters: {n_wrapper_pars}\n")
302
+ if n_head_pars > max_head_pars or n_wrapper_pars > max_wrapper_pars:
303
+ reasons = []
304
+ if n_head_pars > max_head_pars:
305
+ reasons.append(
306
+ f"head parameters ({n_head_pars}) exceed maximum allowed ({max_head_pars})"
307
+ )
308
+ if n_wrapper_pars > max_wrapper_pars:
309
+ reasons.append(
310
+ f"wrapper parameters ({n_wrapper_pars}) exceed maximum allowed ({max_wrapper_pars})"
311
+ )
312
+ print("Stopping run because " + " and ".join(reasons))
313
+ sys.exit(1)
314
+
315
+ # Save universal LWM weights
316
+ os.makedirs("submission", exist_ok=True)
317
+ torch.save(base_model.state_dict(), "submission/model_checkpoint.pth")
318
+ shutil.copy("pretrained_model.py", "submission/pretrained_model.py")
319
+ shutil.copy("utils.py", "submission/utils.py")
320
+ shutil.copy("train_heads_config.py", "submission/train_heads_config.py")
321
+ shutil.copy("train_heads.py", "submission/train_heads.py")
322
+
323
+ # Set default optimizer config if not provided
324
+ if optimizer_config is None:
325
+ optimizer_config = {"lr": 1e-3}
326
+ optimizer = torch.optim.Adam(wrapper.parameters(), **optimizer_config)
327
+
328
+ # Set up the scheduler
329
+ if scheduler_config is None:
330
+ scheduler_config = {"step_size": 1000, "gamma": 0.99}
331
+ scheduler = torch.optim.lr_scheduler.StepLR(
332
+ optimizer,
333
+ step_size=scheduler_config["step_size"],
334
+ gamma=scheduler_config["gamma"]
335
+ )
336
+
337
+ # Initialize training utilities
338
+ train_losses, val_losses, f1_scores = [], [], []
339
+ predictions, ground_truth = [], []
340
+
341
+ # Training loop
342
+ for epoch in range(epochs):
343
+ wrapper.train()
344
+ epoch_loss = 0.0
345
+ batch_count = 0
346
+ train_preds, train_targets = [], []
347
+
348
+ # Prepare a single validation batch
349
+ val_batch = None
350
+ val_iterator = iter(val_loader) if val_loader else None
351
+ if val_iterator:
352
+ try:
353
+ val_batch = next(val_iterator)
354
+ except StopIteration:
355
+ val_iterator = None
356
+
357
+ with tqdm(train_loader, desc=f"Task Epoch {epoch + 1}/{epochs}", leave=True) as progress_bar:
358
+ for batch in progress_bar:
359
+ input_data, targets = batch[0].to(device), batch[1].to(device)
360
+ optimizer.zero_grad()
361
+
362
+ outputs = wrapper(input_data,
363
+ input_type=input_type,
364
+ selected_tokens=selected_tokens)
365
+ if task in ["LosNlosClassification", "BeamPrediction"]:
366
+ preds = torch.argmax(outputs, dim=1).cpu().numpy()
367
+ train_preds.extend(preds)
368
+ train_targets.extend(targets.cpu().numpy())
369
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
370
+ train_preds.extend(outputs.cpu().detach().numpy().flatten())
371
+ train_targets.extend(targets.cpu().detach().numpy().flatten())
372
+ loss = criterion(outputs, targets)
373
+
374
+ loss.backward()
375
+ optimizer.step()
376
+
377
+ epoch_loss += loss.item()
378
+ batch_count += 1
379
+ running_avg_loss = epoch_loss / batch_count
380
+
381
+ train_metric = None
382
+ if task in ["LosNlosClassification", "BeamPrediction"] and train_preds and train_targets:
383
+ train_metric = f1_score(train_targets, train_preds, average="weighted")
384
+ elif task in ["ChannelInterpolation", "ChannelEstimation"] and train_preds and train_targets:
385
+ train_metric = nmse(train_targets, train_preds)
386
+
387
+ val_loss = 0.0
388
+ val_preds, val_targets = [], []
389
+ if val_batch:
390
+ wrapper.eval()
391
+ with torch.no_grad():
392
+ val_input_data, val_targets_batch = val_batch[0].to(device), val_batch[1].to(device)
393
+ val_outputs = wrapper(val_input_data, input_type=input_type)
394
+ if task in ["LosNlosClassification", "BeamPrediction"]:
395
+ val_preds = torch.argmax(val_outputs, dim=1).cpu().numpy()
396
+ val_targets = val_targets_batch.cpu().numpy()
397
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
398
+ val_preds = val_outputs.cpu().numpy().flatten()
399
+ val_targets = val_targets_batch.cpu().numpy().flatten()
400
+ val_loss = criterion(val_outputs, val_targets_batch).item()
401
+
402
+ avg_val_loss = val_loss if val_loss > 0 else None
403
+ val_metric = None
404
+ if task in ["LosNlosClassification", "BeamPrediction"] and len(val_preds) and len(val_targets):
405
+ val_metric = f1_score(val_targets, val_preds, average="weighted")
406
+ elif task in ["ChannelInterpolation", "ChannelEstimation"] and len(val_preds) and len(val_targets):
407
+ val_metric = nmse(val_targets, val_preds)
408
+
409
+ # Switch back to training mode for the next batch
410
+ wrapper.train()
411
+
412
+ postfix_dict = {
413
+ "Batch Loss": f"{loss.item():.6f}",
414
+ "Avg Train Loss": f"{running_avg_loss:.6f}",
415
+ }
416
+ if train_metric is not None:
417
+ if task in ["LosNlosClassification", "BeamPrediction"]:
418
+ postfix_dict["Train F1-Score"] = f"{train_metric:.4f}"
419
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
420
+ postfix_dict["Train NMSE"] = f"{pow2db(train_metric):.6f}"
421
+ if avg_val_loss is not None:
422
+ postfix_dict["Avg Val Loss"] = f"{avg_val_loss:.6f}"
423
+ if val_metric is not None:
424
+ if task in ["LosNlosClassification", "BeamPrediction"]:
425
+ postfix_dict["Val F1-Score"] = f"{val_metric:.4f}"
426
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
427
+ postfix_dict["Val NMSE"] = f"{pow2db(val_metric):.6f}"
428
+
429
+ progress_bar.set_postfix(postfix_dict)
430
+ progress_bar.refresh()
431
+
432
+ avg_train_loss = epoch_loss / len(train_loader)
433
+ train_losses.append(avg_train_loss)
434
+
435
+ train_metric = None
436
+ if task in ["LosNlosClassification", "BeamPrediction"] and train_preds and train_targets:
437
+ train_metric = f1_score(train_targets, train_preds, average="weighted")
438
+ elif task in ["ChannelInterpolation", "ChannelEstimation"] and train_preds and train_targets:
439
+ train_metric = nmse(train_targets, train_preds)
440
+
441
+ val_loss = 0.0
442
+ val_preds, val_targets = [], []
443
+ if val_loader:
444
+ wrapper.eval()
445
+ with torch.no_grad():
446
+ for batch in val_loader:
447
+ input_data, targets = batch[0].to(device), batch[1].to(device)
448
+ outputs = wrapper(input_data, input_type=input_type)
449
+ if task in ["LosNlosClassification", "BeamPrediction"]:
450
+ preds = torch.argmax(outputs, dim=1).cpu().numpy()
451
+ val_preds.extend(preds)
452
+ val_targets.extend(targets.cpu().numpy())
453
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
454
+ val_preds.extend(outputs.cpu().numpy().flatten())
455
+ val_targets.extend(targets.cpu().numpy().flatten())
456
+ elif task == "ChannelCharting":
457
+ val_preds.extend(outputs.cpu().numpy().flatten())
458
+ val_targets.extend(targets.cpu().numpy().flatten())
459
+ loss = criterion(outputs, targets)
460
+ val_loss += loss.item()
461
+
462
+ avg_val_loss = val_loss / len(val_loader)
463
+ val_losses.append(avg_val_loss)
464
+
465
+ val_metric = None
466
+ if task in ["LosNlosClassification", "BeamPrediction"] and val_preds and val_targets:
467
+ val_metric = f1_score(val_targets, val_preds, average="weighted")
468
+ f1_scores.append(val_metric)
469
+ elif task in ["ChannelInterpolation", "ChannelEstimation"] and val_preds and val_targets:
470
+ val_metric = nmse(val_targets, val_preds)
471
+ elif task == "ChannelCharting" and val_preds and val_targets:
472
+ val_metric = np.mean(np.abs(np.array(val_targets) - np.array(val_preds)))
473
+
474
+ if val_metric is not None and task == "ChannelCharting":
475
+ print(f"Validation Prediction Error (meters) at epoch {epoch + 1}: {val_metric:.2f}")
476
+
477
+ postfix_dict = {
478
+ "Avg Train Loss": f"{avg_train_loss:.6f}",
479
+ }
480
+ if train_metric is not None:
481
+ if task in ["LosNlosClassification", "BeamPrediction"]:
482
+ postfix_dict["Train F1-Score"] = f"{train_metric:.4f}"
483
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
484
+ postfix_dict["Train NMSE"] = f"{pow2db(train_metric):.6f}"
485
+ if avg_val_loss is not None:
486
+ postfix_dict["Avg Val Loss"] = f"{avg_val_loss:.6f}"
487
+ if val_metric is not None:
488
+ if task in ["LosNlosClassification", "BeamPrediction"]:
489
+ postfix_dict["Val F1-Score"] = f"{val_metric:.4f}"
490
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
491
+ postfix_dict["Val NMSE"] = f"{pow2db(val_metric):.6f}"
492
+
493
+ progress_bar.set_postfix(postfix_dict)
494
+ progress_bar.refresh()
495
+
496
+ scheduler.step()
497
+
498
+ # Test evaluation
499
+ test_loss = 0.0
500
+ test_preds, test_targets = [], []
501
+ if test_loader:
502
+ wrapper.eval()
503
+ with torch.no_grad():
504
+ for batch in test_loader:
505
+ input_data, targets = batch[0].to(device), batch[1].to(device)
506
+ outputs = wrapper(input_data, input_type=input_type)
507
+ if epoch == epochs - 1:
508
+ predictions.append(outputs)
509
+ ground_truth.append(targets)
510
+ if task in ["LosNlosClassification", "BeamPrediction"]:
511
+ preds = torch.argmax(outputs, dim=1).cpu().numpy()
512
+ test_preds.extend(preds)
513
+ test_targets.extend(targets.cpu().numpy())
514
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
515
+ test_preds.extend(outputs.cpu().numpy().flatten())
516
+ test_targets.extend(targets.cpu().numpy().flatten())
517
+ elif task == "ChannelCharting":
518
+ test_preds.extend(outputs.cpu().numpy().flatten())
519
+ test_targets.extend(targets.cpu().numpy().flatten())
520
+ loss = criterion(outputs, targets)
521
+ test_loss += loss.item()
522
+
523
+ avg_test_loss = test_loss / len(test_loader)
524
+
525
+ test_metric = None
526
+ if task in ["LosNlosClassification", "BeamPrediction"] and test_preds and test_targets:
527
+ test_metric = f1_score(test_targets, test_preds, average="weighted")
528
+ elif task in ["ChannelInterpolation", "ChannelEstimation"] and test_preds and test_targets:
529
+ test_metric = nmse(test_targets, test_preds)
530
+ elif task == "ChannelCharting" and test_preds and test_targets:
531
+ test_metric = np.mean(np.abs(np.array(test_targets) - np.array(test_preds)))
532
+
533
+ print(f"Test Loss: {avg_test_loss:.6f}")
534
+ if test_metric is not None:
535
+ if task in ["LosNlosClassification", "BeamPrediction"]:
536
+ print(f"Test F1-Score: {test_metric:.4f}")
537
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
538
+ print(f"Test NMSE (dB): {pow2db(test_metric):.6f}")
539
+ elif task == "ChannelCharting":
540
+ print(f"Test Prediction Error (meters): {test_metric:.2f}")
541
+
542
+ plt.figure(figsize=(10, 6), dpi=300)
543
+ plt.plot(range(1, epochs + 1), train_losses, label="Train Loss")
544
+ if val_losses:
545
+ plt.plot(range(1, epochs + 1), val_losses, label="Validation Loss")
546
+ plt.xlabel("Epoch")
547
+ plt.ylabel("Loss")
548
+ plt.title("Learning Curves")
549
+ plt.legend()
550
+ plt.grid(True)
551
+ plt.show()
552
+
553
+ test_losses = [avg_test_loss] if test_loader else []
554
+
555
+ if task in ["LosNlosClassification", "BeamPrediction"]:
556
+ score = test_metric
557
+ elif task in ["ChannelInterpolation", "ChannelEstimation"]:
558
+ db_value = pow2db(test_metric)
559
+ db_min, db_max = -20.0, 0.0
560
+ normalized = (db_value - db_min) / (db_max - db_min)
561
+ score = 1.0 - normalized
562
+ score = max(0.0, min(1.0, score))
563
+ elif task == "ChannelCharting":
564
+ localization_error = max(0.0, min(100.0, test_metric))
565
+ score = (100.0 - localization_error) / 100.0
566
+
567
+ print("\n=============================================================")
568
+ print(f"The score for the {task} task is {score:.5f}")
569
+ print("=============================================================\n")
570
+
571
+ return wrapper, train_losses, val_losses, test_losses, score, ground_truth, predictions
572
+
573
+ def count_parameters(model):
574
+ """
575
+ Calculate the total number of learnable parameters in a PyTorch model.
576
+
577
+ Args:
578
+ model (nn.Module): The PyTorch model to count parameters for.
579
+
580
+ Returns:
581
+ int: The total number of parameters that require gradients.
582
+ """
583
+ return sum(p.numel() for p in model.parameters() if p.requires_grad)
584
+
585
+ # Set device
586
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
587
+
588
+ # Process each task
589
+ scores = []
590
+ num_tasks = 5
591
+ for t in range(1, num_tasks + 1):
592
+ # Set random seed for reproducibility
593
+ seed = thc.training_configs[t-1]["seed"]
594
+ torch.manual_seed(seed)
595
+ np.random.seed(seed)
596
+ torch.cuda.manual_seed_all(seed) # For multi-GPU setups
597
+
598
+ # Load universal LWM
599
+ pretrained_checkpoint_path = "model_checkpoint.pth"
600
+ universal_lwm = lwm().to(device)
601
+ checkpoint = torch.load(pretrained_checkpoint_path, map_location=device)
602
+ clean_state_dict = {k.replace("module.", ""): v for k, v in checkpoint.items()}
603
+ universal_lwm.load_state_dict(clean_state_dict)
604
+
605
+ # Create task directory
606
+ task_dir = f"task_{t}"
607
+ os.makedirs(task_dir, exist_ok=True)
608
+
609
+ # Load task configuration
610
+ with open(f"{task_dir}/config.json", "r") as f:
611
+ config = json.load(f)
612
+
613
+ # Load data
614
+ train_data = torch.load(f"{task_dir}/train_data.pt", map_location="cpu")
615
+ val_data = torch.load(f"{task_dir}/val_data.pt", map_location="cpu") if os.path.exists(f"{task_dir}/val_data.pt") else None
616
+ test_data = torch.load(f"{task_dir}/test_data.pt", map_location="cpu") if os.path.exists(f"{task_dir}/test_data.pt") else None
617
+
618
+ # Retrieve training configuration and task head
619
+ training_config = thc.training_configs[t-1]
620
+ TaskHead = task_heads[t-1]
621
+
622
+ # Display task name
623
+ task_name = training_config['task']
624
+ title = f" Task {t}: {task_name} "
625
+ border = "+" + "-" * len(title) + "+"
626
+ print()
627
+ print(border)
628
+ print(f"|{title}|")
629
+ print(border)
630
+ print()
631
+
632
+ # Extract channels and labels
633
+ train_channels = train_data["channels"]
634
+ val_channels = val_data["channels"] if val_data else None
635
+ test_channels = test_data["channels"] if test_data else None
636
+ if t <= 2:
637
+ train_labels = train_data["labels"].to(device).long()
638
+ val_labels = val_data["labels"].to(device).long() if val_data else None
639
+ test_labels = test_data["labels"].to(device).long() if test_data else None
640
+ else:
641
+ train_labels = train_data["labels"].to(device)
642
+ val_labels = val_data["labels"].to(device) if val_data else None
643
+ test_labels = test_data["labels"].to(device) if test_data else None
644
+
645
+ # Tokenize input data
646
+ train_tokens = tokenizer(train_channels)
647
+ val_tokens = tokenizer(val_channels) if val_channels is not None else None
648
+ test_tokens = tokenizer(test_channels) if test_channels is not None else None
649
+
650
+ # Determine sequence length
651
+ sequence_length = train_tokens.shape[1]
652
+
653
+ # Create datasets and data loaders
654
+ train_dataset = TensorDataset(train_tokens, train_labels)
655
+ train_loader = DataLoader(
656
+ train_dataset,
657
+ batch_size=training_config["batch_size"],
658
+ shuffle=True,
659
+ worker_init_fn=worker_init_fn,
660
+ num_workers=0 # Single-threaded for reproducibility
661
+ )
662
+ if val_data:
663
+ val_dataset = TensorDataset(val_tokens, val_labels)
664
+ val_loader = DataLoader(
665
+ val_dataset,
666
+ batch_size=training_config["batch_size"],
667
+ shuffle=False,
668
+ worker_init_fn=worker_init_fn,
669
+ num_workers=0
670
+ )
671
+ else:
672
+ val_loader = None
673
+ if test_data:
674
+ test_dataset = TensorDataset(test_tokens, test_labels)
675
+ test_loader = DataLoader(
676
+ test_dataset,
677
+ batch_size=training_config["batch_size"],
678
+ shuffle=False,
679
+ worker_init_fn=worker_init_fn,
680
+ num_workers=0
681
+ )
682
+ else:
683
+ test_loader = None
684
+
685
+ # Visualize embeddings before fine-tuning
686
+ embeddings = embedding_space_visual(
687
+ universal_lwm,
688
+ test_tokens,
689
+ input_type=training_config["input_type"],
690
+ batch_size=training_config["batch_size"],
691
+ selected_tokens=training_config["selected_tokens"],
692
+ task=training_config["task"],
693
+ labels=test_labels if t <= 2 or t == 5 else None,
694
+ visualization=True,
695
+ visualization_method="tsne",
696
+ device=device
697
+ )
698
+
699
+ # Fine-tune the model
700
+ wrapper, train_losses, val_losses, test_losses, score, ground_truth, predictions = finetune(
701
+ base_model=universal_lwm,
702
+ train_loader=train_loader,
703
+ val_loader=val_loader,
704
+ test_loader=test_loader,
705
+ input_type=training_config["input_type"],
706
+ fine_tune_layers=training_config["fine_tune_layers"],
707
+ optimizer_config=training_config["optimizer_config"],
708
+ scheduler_config=training_config["scheduler"],
709
+ epochs=training_config["epochs"],
710
+ task=training_config["task"],
711
+ d_model=universal_lwm.d_model,
712
+ sequence_length=sequence_length,
713
+ selected_tokens=training_config["selected_tokens"],
714
+ bbox_coord=config["bounding_box_coord"] if t == 5 else None,
715
+ max_head_pars=config["max_head_parameters"],
716
+ max_wrapper_pars=config["max_wrapper_parameters"],
717
+ device=device
718
+ )
719
+
720
+ # Visualize embeddings after fine-tuning
721
+ finetuned_embeddings = embedding_space_visual(
722
+ wrapper.model,
723
+ test_tokens,
724
+ input_type=training_config["input_type"],
725
+ batch_size=training_config["batch_size"],
726
+ selected_tokens=training_config["selected_tokens"],
727
+ task=training_config["task"],
728
+ labels=test_labels if t <= 2 or t == 5 else None,
729
+ visualization=True,
730
+ visualization_method="tsne",
731
+ device=device
732
+ )
733
+
734
+ # Create submission directory
735
+ task_dir = f"submission/task_{t}"
736
+ os.makedirs(task_dir, exist_ok=True)
737
+
738
+ # Save fine-tuned wrapper model weights
739
+ wrapper_weights_path = os.path.join(task_dir, "wrapper.pt")
740
+ torch.save(wrapper.state_dict(), wrapper_weights_path)
741
+ print(f"Saved wrapper weights for task {t} to {wrapper_weights_path}")
742
+
743
+ # Save ground truth and predictions
744
+ ground_truth_path = os.path.join(task_dir, "ground_truth.pt")
745
+ predictions_path = os.path.join(task_dir, "predictions.pt")
746
+ torch.save(ground_truth, ground_truth_path)
747
+ torch.save(predictions, predictions_path)
748
+ print(f"Saved ground truth and predictions for task {t}")
749
+
750
+ # Save task score
751
+ score_path = os.path.join(task_dir, "score.json")
752
+ with open(score_path, "w") as f:
753
+ json.dump(float(score), f, indent=7)
754
+ print(f"Saved task score to {score_path}")
755
+
756
+ scores.append(float(score))
757
+
758
+ # Calculate and save composite score
759
+ composite_score = np.mean(scores)
760
+ composite_score_path = os.path.join("submission", "composite_score.json")
761
+ with open(composite_score_path, "w") as f:
762
+ json.dump(composite_score, f, indent=7)
763
+ print("Saved composite score")
764
+
765
+ # Create zip archive
766
+ shutil.make_archive("submission", format="zip", root_dir="submission")
767
+
768
+ # Define task names and baseline scores
769
+ task_names = ["LoS/NLoS\nClassification", "Beam\nPrediction", "Channel\nInterpolation", "Channel\nEstimation", "User\nLocalization"]
770
+ baseline_scores = [
771
+ 0.9396,
772
+ 0.6137,
773
+ 0.4165,
774
+ 0.4576,
775
+ 0.6711
776
+ ]
777
+ plot_radar_chart(task_names, scores, baseline_scores)
train_heads_config.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from utils import patch_reconstructor
4
+
5
+ # Define TaskHead for each task
6
+ class LosNlosClassificationHead(nn.Module):
7
+ """
8
+ Task head for LoS/NLoS classification.
9
+
10
+ Takes flattened patch embeddings as input and outputs class logits for binary classification.
11
+
12
+ Args:
13
+ input_dim (tuple): (n_patches, d_model) — number of patches and feature dimension.
14
+ """
15
+ def __init__(self, input_dim):
16
+ super().__init__()
17
+ n_patches, d_model = input_dim
18
+ flattened_dim = n_patches * d_model
19
+ self.classifier = nn.Sequential(
20
+ nn.Linear(flattened_dim, 8),
21
+ nn.BatchNorm1d(8),
22
+ nn.ReLU(),
23
+ nn.Dropout(0.1),
24
+ nn.Linear(8, 2),
25
+ )
26
+
27
+ def forward(self, x):
28
+ batch_size = x.size(0)
29
+ x = x.view(batch_size, -1)
30
+ x = self.classifier(x)
31
+ return x
32
+
33
+ class BeamPredictionHead(nn.Module):
34
+ """
35
+ Task head for mmWave beam index prediction.
36
+
37
+ Processes flattened patch embeddings and outputs logits over 64 possible beam indices.
38
+
39
+ Args:
40
+ input_dim (tuple): (n_patches, d_model) — number of patches and feature dimension.
41
+ """
42
+ def __init__(self, input_dim):
43
+ super().__init__()
44
+ n_patches, d_model = input_dim
45
+ flattened_dim = n_patches * d_model
46
+ self.classifier = nn.Sequential(
47
+ nn.Linear(flattened_dim, 256),
48
+ nn.BatchNorm1d(256),
49
+ nn.ReLU(),
50
+ nn.Dropout(0.1),
51
+ nn.Linear(256, 128),
52
+ nn.BatchNorm1d(128),
53
+ nn.ReLU(),
54
+ nn.Linear(128, 64)
55
+ )
56
+ def forward(self, x):
57
+ batch_size = x.size(0)
58
+ x = x.view(batch_size, -1)
59
+ x = self.classifier(x)
60
+ return x
61
+
62
+ class ChannelInterpolationHead(nn.Module):
63
+ """
64
+ Task head for reconstructing missing channel values from patch embeddings.
65
+
66
+ Applies a linear layer to each patch and reconstructs the full channel using patch_reconstructor.
67
+
68
+ Args:
69
+ input_dim (tuple): (n_patches, d_model).
70
+ output_dim (tuple): (target_channels, n_rows, n_cols) — shape of the output channel matrix.
71
+ """
72
+ def __init__(self, input_dim, output_dim):
73
+ super().__init__()
74
+ n_patches, d_model = input_dim
75
+ target_channels, self.n_rows, self.n_cols = output_dim
76
+ self.fcn = nn.Sequential(
77
+ nn.Linear(d_model, 32)
78
+ )
79
+
80
+ def forward(self, x):
81
+ batch_size, n_patches, d_model = x.size()
82
+ x = x.reshape(batch_size * n_patches, d_model)
83
+ x = self.fcn(x)
84
+ x = x.reshape(batch_size, n_patches, 32)
85
+ x = patch_reconstructor(x, self.n_rows, self.n_cols)
86
+ return x
87
+
88
+ class ChannelEstimationHead(nn.Module):
89
+ """
90
+ Task head for full channel estimation from embeddings.
91
+
92
+ Similar to interpolation but typically used for denoising or noisy reconstruction.
93
+
94
+ Args:
95
+ input_dim (tuple): (n_patches, d_model).
96
+ output_dim (tuple): (target_channels, n_rows, n_cols) — shape of the target full-resolution channel.
97
+ """
98
+ def __init__(self, input_dim, output_dim):
99
+ super().__init__()
100
+ n_patches, d_model = input_dim
101
+ target_channels, self.n_rows, self.n_cols = output_dim
102
+ self.fcn = nn.Sequential(
103
+ nn.Linear(d_model, 32)
104
+ )
105
+
106
+ def forward(self, x):
107
+ batch_size, n_patches, d_model = x.size()
108
+ x = x.reshape(batch_size * n_patches, d_model)
109
+ x = self.fcn(x)
110
+ x = x.reshape(batch_size, n_patches, 32)
111
+ x = patch_reconstructor(x, self.n_rows, self.n_cols)
112
+ return x
113
+
114
+ class ChannelChartingHead(nn.Module):
115
+ """
116
+ Task head for 2D channel charting (e.g., learning spatial topology).
117
+
118
+ Reduces the flattened embeddings into 2D coordinates.
119
+
120
+ Args:
121
+ input_dim (tuple): (n_patches, d_model).
122
+ """
123
+ def __init__(self, input_dim):
124
+ super().__init__()
125
+ n_patches, d_model = input_dim
126
+ flattened_dim = n_patches * d_model
127
+ self.fcn = nn.Sequential(
128
+ nn.Linear(flattened_dim, 64),
129
+ nn.ReLU(),
130
+ nn.Linear(64, 32),
131
+ nn.ReLU(),
132
+ nn.Linear(32, 2)
133
+ )
134
+
135
+ def forward(self, x):
136
+ batch_size = x.size(0)
137
+ x = x.view(batch_size, -1)
138
+ x = self.fcn(x)
139
+ return x
140
+
141
+ # training_configs is a list of dictionaries, each specifying the setup for one downstream task.
142
+ # Each entry includes:
143
+ # - task: Name of the task.
144
+ # - optimizer_config: Learning rate for the optimizer.
145
+ # - scheduler: Step size and decay rate for the learning rate scheduler.
146
+ # - epochs: Total number of training epochs.
147
+ # - batch_size: Number of samples per batch.
148
+ # - loss_function: Loss type ("CrossEntropyLoss" or "MSELoss").
149
+ # - seed: Random seed for reproducibility.
150
+ # - fine_tune_layers: Specifies which parts of the LWM model to fine-tune:
151
+ # • "full" means all layers are trainable
152
+ # • A list like ["layers.10", "layers.11"] specifies partial fine-tuning
153
+ # - input_type: Type of embedding input used from LWM:
154
+ # • "cls_emb", "channel_emb", "mean_pooled", etc.
155
+ # - selected_tokens: Specific tokens to select for input, used in some tasks.
156
+
157
+ training_configs = [
158
+ { # Task 1
159
+ "task": "LosNlosClassification",
160
+ "optimizer_config": {"lr": 1e-3},
161
+ "scheduler": {"step_size": 20, "gamma": 0.5},
162
+ "epochs": 200,
163
+ "batch_size": 128,
164
+ "seed": 42,
165
+ "fine_tune_layers": ["layers.9", "layers.10", "layers.11"],
166
+ "input_type": "cls_emb",
167
+ "selected_tokens": None
168
+ },
169
+ { # Task 2
170
+ "task": "BeamPrediction",
171
+ "optimizer_config": {"lr": 1e-3},
172
+ "scheduler": {"step_size": 20, "gamma": 0.8},
173
+ "epochs": 70,
174
+ "batch_size": 128,
175
+ "seed": 42,
176
+ "fine_tune_layers": "full",
177
+ "input_type": "mean_pooled",
178
+ "selected_tokens": None
179
+ },
180
+ { # Task 3
181
+ "task": "ChannelInterpolation",
182
+ "optimizer_config": {"lr": 1e-2},
183
+ "scheduler": {"step_size": 25, "gamma": 0.2},
184
+ "epochs": 100,
185
+ "batch_size": 128,
186
+ "seed": 42,
187
+ "fine_tune_layers": ["layers.10", "layers.11"],
188
+ "input_type": "channel_emb",
189
+ "selected_tokens": None
190
+ },
191
+ { # Task 4
192
+ "task": "ChannelEstimation",
193
+ "optimizer_config": {"lr": 1e-2},
194
+ "scheduler": {"step_size": 50, "gamma": 0.3},
195
+ "epochs": 200,
196
+ "batch_size": 128,
197
+ "seed": 42,
198
+ "fine_tune_layers": ["layers.9", "layers.10", "layers.11"],
199
+ "input_type": "channel_emb",
200
+ "selected_tokens": None
201
+ },
202
+ { # Task 5
203
+ "task": "ChannelCharting",
204
+ "optimizer_config": {"lr": 1e-3},
205
+ "scheduler": {"step_size": 40, "gamma": 0.6},
206
+ "epochs": 150,
207
+ "batch_size": 128,
208
+ "seed": 42,
209
+ "fine_tune_layers": ["layers.10", "layers.11"],
210
+ "input_type": "mean_pooled",
211
+ "selected_tokens": None
212
+ }
213
+ ]
train_lwm.py ADDED
@@ -0,0 +1,818 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # 1. IMPORTS AND WARNINGS SETUP
3
+ # - Load necessary PyTorch modules, utilities, and suppress UserWarnings
4
+ # =============================================================================
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch.utils.data import DataLoader, random_split, TensorDataset
8
+ import torch.optim as optim
9
+ from utils import (generate_channels_and_labels, tokenizer_train,
10
+ create_train_dataloader, count_parameters, train_lwm)
11
+ import numpy as np
12
+ import pretrained_model # Assuming this contains the LWM model definition
13
+ from torch.optim.lr_scheduler import LambdaLR
14
+ from torch.optim import AdamW
15
+ import warnings
16
+ warnings.filterwarnings("ignore", category=UserWarning)
17
+
18
+ # =============================================================================
19
+ # 2. SCENARIO LIST DEFINITION
20
+ # - Define the list of scenario names to iterate over for data generation
21
+ # =============================================================================
22
+ def scenarios_list():
23
+ scen_list = np.array([
24
+ 'city_0_newyork_3p5_lwm',
25
+ 'city_1_losangeles_3p5_lwm',
26
+ 'city_2_chicago_3p5_lwm',
27
+ 'city_3_houston_3p5_lwm',
28
+ 'city_4_phoenix_3p5_lwm',
29
+ 'city_5_philadelphia_3p5_lwm',
30
+ 'city_6_miami_3p5_lwm',
31
+ 'city_7_sandiego_3p5_lwm',
32
+ 'city_8_dallas_3p5_lwm',
33
+ 'city_9_sanfrancisco_3p5_lwm',
34
+ 'city_10_austin_3p5_lwm',
35
+ 'city_11_santaclara_3p5_lwm',
36
+ 'city_12_fortworth_3p5_lwm',
37
+ 'city_13_columbus_3p5_lwm',
38
+ 'city_14_charlotte_3p5_lwm',
39
+ 'city_15_indianapolis_3p5_lwm',
40
+ 'city_16_sanfrancisco_3p5_lwm',
41
+ 'city_17_seattle_3p5_lwm',
42
+ 'city_18_denver_3p5_lwm',
43
+ 'city_19_oklahoma_3p5_lwm',
44
+ 'asu_campus_3p5',
45
+ 'o1_3p5',
46
+ 'boston5G_3p5'
47
+ ])
48
+ return scen_list
49
+
50
+ # =============================================================================
51
+ # 3. SCENARIO PROPERTIES MAPPING
52
+ # - Map each scenario name to its corresponding rows, antenna count, and subcarrier count
53
+ # =============================================================================
54
+
55
+ def scenario_prop():
56
+ row_column_users = {
57
+ 'city_0_newyork_3p5_lwm': {
58
+ 'n_rows': 109,
59
+ 'n_per_row': 291,
60
+ 'grid_idx': 1,
61
+ 'n_ant_bs': 8,
62
+ 'n_subcarriers': 32
63
+ },
64
+ 'city_1_losangeles_3p5_lwm': {
65
+ 'n_rows': 142,
66
+ 'n_per_row': 201,
67
+ 'grid_idx': 1,
68
+ 'n_ant_bs': 8,
69
+ 'n_subcarriers': 64
70
+ },
71
+ 'city_2_chicago_3p5_lwm': {
72
+ 'n_rows': 139,
73
+ 'n_per_row': 200,
74
+ 'grid_idx': 1,
75
+ 'n_ant_bs': 8,
76
+ 'n_subcarriers': 128
77
+ },
78
+ 'city_3_houston_3p5_lwm': {
79
+ 'n_rows': 154,
80
+ 'n_per_row': 202,
81
+ 'grid_idx': 1,
82
+ 'n_ant_bs': 8,
83
+ 'n_subcarriers': 256
84
+ },
85
+ 'city_4_phoenix_3p5_lwm': {
86
+ 'n_rows': 198,
87
+ 'n_per_row': 214,
88
+ 'grid_idx': 1,
89
+ 'n_ant_bs': 8,
90
+ 'n_subcarriers': 512
91
+ },
92
+ 'city_5_philadelphia_3p5_lwm': {
93
+ 'n_rows': 239,
94
+ 'n_per_row': 164,
95
+ 'grid_idx': 1,
96
+ 'n_ant_bs': 8,
97
+ 'n_subcarriers': 1024
98
+ },
99
+ 'city_6_miami_3p5_lwm': {
100
+ 'n_rows': 199,
101
+ 'n_per_row': 216,
102
+ 'grid_idx': 1,
103
+ 'n_ant_bs': 16,
104
+ 'n_subcarriers': 32
105
+ },
106
+ 'city_7_sandiego_3p5_lwm': {
107
+ 'n_rows': 176,
108
+ 'n_per_row': 207,
109
+ 'grid_idx': 1,
110
+ 'n_ant_bs': 16,
111
+ 'n_subcarriers': 64
112
+ },
113
+ 'city_8_dallas_3p5_lwm': {
114
+ 'n_rows': 207,
115
+ 'n_per_row': 190,
116
+ 'grid_idx': 1,
117
+ 'n_ant_bs': 16,
118
+ 'n_subcarriers': 128
119
+ },
120
+ 'city_9_sanfrancisco_3p5_lwm': {
121
+ 'n_rows': 196,
122
+ 'n_per_row': 206,
123
+ 'grid_idx': 1,
124
+ 'n_ant_bs': 16,
125
+ 'n_subcarriers': 256
126
+ },
127
+ 'city_10_austin_3p5_lwm': {
128
+ 'n_rows': 255,
129
+ 'n_per_row': 137,
130
+ 'grid_idx': 1,
131
+ 'n_ant_bs': 16,
132
+ 'n_subcarriers': 512
133
+ },
134
+ 'city_11_santaclara_3p5_lwm': {
135
+ 'n_rows': 117,
136
+ 'n_per_row': 285,
137
+ 'grid_idx': 1,
138
+ 'n_ant_bs': 32,
139
+ 'n_subcarriers': 32
140
+ },
141
+ 'city_12_fortworth_3p5_lwm': {
142
+ 'n_rows': 214,
143
+ 'n_per_row': 179,
144
+ 'grid_idx': 1,
145
+ 'n_ant_bs': 32,
146
+ 'n_subcarriers': 64
147
+ },
148
+ 'city_13_columbus_3p5_lwm': {
149
+ 'n_rows': 178,
150
+ 'n_per_row': 240,
151
+ 'grid_idx': 1,
152
+ 'n_ant_bs': 32,
153
+ 'n_subcarriers': 128
154
+ },
155
+ 'city_14_charlotte_3p5_lwm': {
156
+ 'n_rows': 216,
157
+ 'n_per_row': 177,
158
+ 'grid_idx': 1,
159
+ 'n_ant_bs': 32,
160
+ 'n_subcarriers': 256
161
+ },
162
+ 'city_15_indianapolis_3p5_lwm': {
163
+ 'n_rows': 200,
164
+ 'n_per_row': 196,
165
+ 'grid_idx': 1,
166
+ 'n_ant_bs': 64,
167
+ 'n_subcarriers': 32
168
+ },
169
+ 'city_16_sanfrancisco_3p5_lwm': {
170
+ 'n_rows': 201,
171
+ 'n_per_row': 208,
172
+ 'grid_idx': 1,
173
+ 'n_ant_bs': 64,
174
+ 'n_subcarriers': 64
175
+ },
176
+ 'city_17_seattle_3p5_lwm': {
177
+ 'n_rows': 185,
178
+ 'n_per_row': 205,
179
+ 'grid_idx': 1,
180
+ 'n_ant_bs': 64,
181
+ 'n_subcarriers': 128
182
+ },
183
+ 'city_18_denver_3p5_lwm': {
184
+ 'n_rows': 212,
185
+ 'n_per_row': 204,
186
+ 'grid_idx': 1,
187
+ 'n_ant_bs': 128,
188
+ 'n_subcarriers': 32
189
+ },
190
+ 'city_19_oklahoma_3p5_lwm': {
191
+ 'n_rows': 204,
192
+ 'n_per_row': 188,
193
+ 'grid_idx': 1,
194
+ 'n_ant_bs': 128,
195
+ 'n_subcarriers': 64
196
+ },
197
+ 'asu_campus_3p5_v1': {
198
+ 'n_rows': [0, 1*int(321/20)],
199
+ 'n_per_row': 411,
200
+ 'grid_idx': 1,
201
+ 'n_ant_bs': 8,
202
+ 'n_subcarriers': 32
203
+ },
204
+ 'asu_campus_3p5_v2': {
205
+ 'n_rows': [1*int(321/20), 2*int(321/20)],
206
+ 'n_per_row': 411,
207
+ 'grid_idx': 1,
208
+ 'n_ant_bs': 8,
209
+ 'n_subcarriers': 64
210
+ },
211
+ 'asu_campus_3p5_v3': {
212
+ 'n_rows': [2*int(321/20), 3*int(321/20)],
213
+ 'n_per_row': 411,
214
+ 'grid_idx': 1,
215
+ 'n_ant_bs': 8,
216
+ 'n_subcarriers': 128
217
+ },
218
+ 'asu_campus_3p5_v4': {
219
+ 'n_rows': [3*int(321/20), 4*int(321/20)],
220
+ 'n_per_row': 411,
221
+ 'grid_idx': 1,
222
+ 'n_ant_bs': 8,
223
+ 'n_subcarriers': 256
224
+ },
225
+ 'asu_campus_3p5_v5': {
226
+ 'n_rows': [4*int(321/20), 5*int(321/20)],
227
+ 'n_per_row': 411,
228
+ 'grid_idx': 1,
229
+ 'n_ant_bs': 8,
230
+ 'n_subcarriers': 512
231
+ },
232
+ 'asu_campus_3p5_v6': {
233
+ 'n_rows': [5*int(321/20), 6*int(321/20)],
234
+ 'n_per_row': 411,
235
+ 'grid_idx': 1,
236
+ 'n_ant_bs': 8,
237
+ 'n_subcarriers': 1024
238
+ },
239
+ 'asu_campus_3p5_v7': {
240
+ 'n_rows': [6*int(321/20), 7*int(321/20)],
241
+ 'n_per_row': 411,
242
+ 'grid_idx': 1,
243
+ 'n_ant_bs': 16,
244
+ 'n_subcarriers': 32
245
+ },
246
+ 'asu_campus_3p5_v8': {
247
+ 'n_rows': [7*int(321/20), 8*int(321/20)],
248
+ 'n_per_row': 411,
249
+ 'grid_idx': 1,
250
+ 'n_ant_bs':16,
251
+ 'n_subcarriers': 64
252
+ },
253
+ 'asu_campus_3p5_v9': {
254
+ 'n_rows': [8*int(321/20), 9*int(321/20)],
255
+ 'n_per_row': 411,
256
+ 'grid_idx': 1,
257
+ 'n_ant_bs': 16,
258
+ 'n_subcarriers': 128
259
+ },
260
+ 'asu_campus_3p5_v10': {
261
+ 'n_rows': [9*int(321/20), 10*int(321/20)],
262
+ 'n_per_row': 411,
263
+ 'grid_idx': 1,
264
+ 'n_ant_bs': 16,
265
+ 'n_subcarriers': 256
266
+ },
267
+ 'asu_campus_3p5_v11': {
268
+ 'n_rows': [10*int(321/20), 11*int(321/20)],
269
+ 'n_per_row': 411,
270
+ 'grid_idx': 1,
271
+ 'n_ant_bs': 16,
272
+ 'n_subcarriers': 512
273
+ },
274
+ 'asu_campus_3p5_v12': {
275
+ 'n_rows': [11*int(321/20), 12*int(321/20)],
276
+ 'n_per_row': 411,
277
+ 'grid_idx': 1,
278
+ 'n_ant_bs': 32,
279
+ 'n_subcarriers': 32
280
+ },
281
+ 'asu_campus_3p5_v13': {
282
+ 'n_rows': [12*int(321/20), 13*int(321/20)],
283
+ 'n_per_row': 411,
284
+ 'grid_idx': 1,
285
+ 'n_ant_bs': 32,
286
+ 'n_subcarriers': 64
287
+ },
288
+ 'asu_campus_3p5_v14': {
289
+ 'n_rows': [13*int(321/20), 14*int(321/20)],
290
+ 'n_per_row': 411,
291
+ 'grid_idx': 1,
292
+ 'n_ant_bs': 32,
293
+ 'n_subcarriers': 128
294
+ },
295
+ 'asu_campus_3p5_v15': {
296
+ 'n_rows': [14*int(321/20), 15*int(321/20)],
297
+ 'n_per_row': 411,
298
+ 'grid_idx': 1,
299
+ 'n_ant_bs': 32,
300
+ 'n_subcarriers': 256
301
+ },
302
+ 'asu_campus_3p5_v16': {
303
+ 'n_rows': [15*int(321/20), 16*int(321/20)],
304
+ 'n_per_row': 411,
305
+ 'grid_idx': 1,
306
+ 'n_ant_bs': 64,
307
+ 'n_subcarriers': 32
308
+ },
309
+ 'asu_campus_3p5_v17': {
310
+ 'n_rows': [16*int(321/20), 17*int(321/20)],
311
+ 'n_per_row': 411,
312
+ 'grid_idx': 1,
313
+ 'n_ant_bs': 64,
314
+ 'n_subcarriers': 64
315
+ },
316
+ 'asu_campus_3p5_v18': {
317
+ 'n_rows': [17*int(321/20), 18*int(321/20)],
318
+ 'n_per_row': 411,
319
+ 'grid_idx': 1,
320
+ 'n_ant_bs': 64,
321
+ 'n_subcarriers': 128
322
+ },
323
+ 'asu_campus_3p5_v19': {
324
+ 'n_rows': [18*int(321/20), 19*int(321/20)],
325
+ 'n_per_row': 411,
326
+ 'grid_idx': 1,
327
+ 'n_ant_bs': 128,
328
+ 'n_subcarriers': 32
329
+ },
330
+ 'asu_campus_3p5_v20': {
331
+ 'n_rows': [19*int(321/20), 20*int(321/20)],
332
+ 'n_per_row': 411,
333
+ 'grid_idx': 1,
334
+ 'n_ant_bs': 128,
335
+ 'n_subcarriers': 64
336
+ },
337
+ 'boston5G_3p5_v1': {
338
+ 'n_rows': [812 + 0, 812 + 1*int((1622-812)/20)],
339
+ 'n_per_row': 595,
340
+ 'grid_idx': 2,
341
+ 'n_ant_bs': 8,
342
+ 'n_subcarriers': 32
343
+ },
344
+ 'boston5G_3p5_v2': {
345
+ 'n_rows': [812 + 1*int((1622-812)/20), 812 + 2*int((1622-812)/20)],
346
+ 'n_per_row': 595,
347
+ 'grid_idx': 2,
348
+ 'n_ant_bs': 8,
349
+ 'n_subcarriers': 64
350
+ },
351
+ 'boston5G_3p5_v3': {
352
+ 'n_rows': [812 + 2*int((1622-812)/20), 812 + 3*int((1622-812)/20)],
353
+ 'n_per_row': 595,
354
+ 'grid_idx': 2,
355
+ 'n_ant_bs': 8,
356
+ 'n_subcarriers': 128
357
+ },
358
+ 'boston5G_3p5_v4': {
359
+ 'n_rows': [812 + 3*int((1622-812)/20), 812 + 4*int((1622-812)/20)],
360
+ 'n_per_row': 595,
361
+ 'grid_idx': 2,
362
+ 'n_ant_bs': 8,
363
+ 'n_subcarriers': 256
364
+ },
365
+ 'boston5G_3p5_v5': {
366
+ 'n_rows': [812 + 4*int((1622-812)/20), 812 + 5*int((1622-812)/20)],
367
+ 'n_per_row': 595,
368
+ 'grid_idx': 2,
369
+ 'n_ant_bs': 8,
370
+ 'n_subcarriers': 512
371
+ },
372
+ 'boston5G_3p5_v6': {
373
+ 'n_rows': [812 + 5*int((1622-812)/20), 812 + 6*int((1622-812)/20)],
374
+ 'n_per_row': 595,
375
+ 'grid_idx': 2,
376
+ 'n_ant_bs': 8,
377
+ 'n_subcarriers': 1024
378
+ },
379
+ 'boston5G_3p5_v7': {
380
+ 'n_rows': [812 + 6*int((1622-812)/20), 812 + 7*int((1622-812)/20)],
381
+ 'n_per_row': 595,
382
+ 'grid_idx': 2,
383
+ 'n_ant_bs': 16,
384
+ 'n_subcarriers': 32
385
+ },
386
+ 'boston5G_3p5_v8': {
387
+ 'n_rows': [812 + 7*int((1622-812)/20), 812 + 8*int((1622-812)/20)],
388
+ 'n_per_row': 595,
389
+ 'grid_idx': 2,
390
+ 'n_ant_bs':16,
391
+ 'n_subcarriers': 64
392
+ },
393
+ 'boston5G_3p5_v9': {
394
+ 'n_rows': [812 + 8*int((1622-812)/20), 812 + 9*int((1622-812)/20)],
395
+ 'n_per_row': 595,
396
+ 'grid_idx': 2,
397
+ 'n_ant_bs': 16,
398
+ 'n_subcarriers': 128
399
+ },
400
+ 'boston5G_3p5_v10': {
401
+ 'n_rows': [812 + 9*int((1622-812)/20), 812 + 10*int((1622-812)/20)],
402
+ 'n_per_row': 595,
403
+ 'grid_idx': 2,
404
+ 'n_ant_bs': 16,
405
+ 'n_subcarriers': 256
406
+ },
407
+ 'boston5G_3p5_v11': {
408
+ 'n_rows': [812 + 10*int((1622-812)/20), 812 + 11*int((1622-812)/20)],
409
+ 'n_per_row': 595,
410
+ 'grid_idx': 2,
411
+ 'n_ant_bs': 16,
412
+ 'n_subcarriers': 512
413
+ },
414
+ 'boston5G_3p5_v12': {
415
+ 'n_rows': [812 + 11*int((1622-812)/20), 812 + 12*int((1622-812)/20)],
416
+ 'n_per_row': 595,
417
+ 'grid_idx': 2,
418
+ 'n_ant_bs': 32,
419
+ 'n_subcarriers': 32
420
+ },
421
+ 'boston5G_3p5_v13': {
422
+ 'n_rows': [812 + 12*int((1622-812)/20), 812 + 13*int((1622-812)/20)],
423
+ 'n_per_row': 595,
424
+ 'grid_idx': 2,
425
+ 'n_ant_bs': 32,
426
+ 'n_subcarriers': 64
427
+ },
428
+ 'boston5G_3p5_v14': {
429
+ 'n_rows': [812 + 13*int((1622-812)/20), 812 + 14*int((1622-812)/20)],
430
+ 'n_per_row': 595,
431
+ 'grid_idx': 2,
432
+ 'n_ant_bs': 32,
433
+ 'n_subcarriers': 128
434
+ },
435
+ 'boston5G_3p5_v15': {
436
+ 'n_rows': [812 + 14*int((1622-812)/20), 812 + 15*int((1622-812)/20)],
437
+ 'n_per_row': 595,
438
+ 'grid_idx': 2,
439
+ 'n_ant_bs': 32,
440
+ 'n_subcarriers': 256
441
+ },
442
+ 'boston5G_3p5_v16': {
443
+ 'n_rows': [812 + 15*int((1622-812)/20), 812 + 16*int((1622-812)/20)],
444
+ 'n_per_row': 595,
445
+ 'grid_idx': 2,
446
+ 'n_ant_bs': 64,
447
+ 'n_subcarriers': 32
448
+ },
449
+ 'boston5G_3p5_v17': {
450
+ 'n_rows': [812 + 16*int((1622-812)/20), 812 + 17*int((1622-812)/20)],
451
+ 'n_per_row': 595,
452
+ 'grid_idx': 2,
453
+ 'n_ant_bs': 64,
454
+ 'n_subcarriers': 64
455
+ },
456
+ 'boston5G_3p5_v18': {
457
+ 'n_rows': [812 + 17*int((1622-812)/20), 812 + 18*int((1622-812)/20)],
458
+ 'n_per_row': 595,
459
+ 'grid_idx': 2,
460
+ 'n_ant_bs': 64,
461
+ 'n_subcarriers': 128
462
+ },
463
+ 'boston5G_3p5_v19': {
464
+ 'n_rows': [812 + 18*int((1622-812)/20), 812 + 19*int((1622-812)/20)],
465
+ 'n_per_row': 595,
466
+ 'grid_idx': 2,
467
+ 'n_ant_bs': 128,
468
+ 'n_subcarriers': 32
469
+ },
470
+ 'boston5G_3p5_v20': {
471
+ 'n_rows': [812 + 19*int((1622-812)/20), 812 + 20*int((1622-812)/20)],
472
+ 'n_per_row': 595,
473
+ 'grid_idx': 2,
474
+ 'n_ant_bs': 128,
475
+ 'n_subcarriers': 64
476
+ },
477
+ 'o1_3p5_v1': {
478
+ 'n_rows': [0*int(3852/12), 1*int(3852/12)],
479
+ 'n_per_row': 181,
480
+ 'grid_idx': 1,
481
+ 'n_ant_bs': 8,
482
+ 'n_subcarriers': 32
483
+ },
484
+ 'o1_3p5_v2': {
485
+ 'n_rows': [1*int(3852/12), 2*int(3852/12)],
486
+ 'n_per_row': 181,
487
+ 'grid_idx': 1,
488
+ 'n_ant_bs': 8,
489
+ 'n_subcarriers': 64
490
+ },
491
+ 'o1_3p5_v3': {
492
+ 'n_rows': [2*int(3852/12), 3*int(3852/12)],
493
+ 'n_per_row': 181,
494
+ 'grid_idx': 1,
495
+ 'n_ant_bs': 8,
496
+ 'n_subcarriers': 128
497
+ },
498
+ 'o1_3p5_v4': {
499
+ 'n_rows': [3*int(3852/12), 4*int(3852/12)],
500
+ 'n_per_row': 181,
501
+ 'grid_idx': 1,
502
+ 'n_ant_bs': 8,
503
+ 'n_subcarriers': 256
504
+ },
505
+ 'o1_3p5_v5': {
506
+ 'n_rows': [4*int(3852/12), 5*int(3852/12)],
507
+ 'n_per_row': 181,
508
+ 'grid_idx': 1,
509
+ 'n_ant_bs': 8,
510
+ 'n_subcarriers': 512
511
+ },
512
+ 'o1_3p5_v6': {
513
+ 'n_rows': [5*int(3852/12), 6*int(3852/12)],
514
+ 'n_per_row': 181,
515
+ 'grid_idx': 1,
516
+ 'n_ant_bs': 8,
517
+ 'n_subcarriers': 1024
518
+ },
519
+ 'o1_3p5_v7': {
520
+ 'n_rows': [6*int(3852/12), 7*int(3852/12)],
521
+ 'n_per_row': 181,
522
+ 'grid_idx': 1,
523
+ 'n_ant_bs': 16,
524
+ 'n_subcarriers': 32
525
+ },
526
+ 'o1_3p5_v8': {
527
+ 'n_rows': [7*int(3852/12), 8*int(3852/12)],
528
+ 'n_per_row': 181,
529
+ 'grid_idx': 1,
530
+ 'n_ant_bs': 16,
531
+ 'n_subcarriers': 64
532
+ },
533
+ 'o1_3p5_v9': {
534
+ 'n_rows': [8*int(3852/12), 2750],
535
+ 'n_per_row': 181,
536
+ 'grid_idx': 1,
537
+ 'n_ant_bs': 16,
538
+ 'n_subcarriers': 128
539
+ },
540
+ 'o1_3p5_v10': {
541
+ 'n_rows': [2751, 10*int(3852/12)],
542
+ 'n_per_row': 181,
543
+ 'grid_idx': 2,
544
+ 'n_ant_bs': 16,
545
+ 'n_subcarriers': 256
546
+ },
547
+ 'o1_3p5_v11': {
548
+ 'n_rows': [10*int(3852/12), 11*int(3852/12)],
549
+ 'n_per_row': 181,
550
+ 'grid_idx': 2,
551
+ 'n_ant_bs': 16,
552
+ 'n_subcarriers': 512
553
+ },
554
+ 'o1_3p5_v12': {
555
+ 'n_rows': [11*int(3852/12), 3851],
556
+ 'n_per_row': 181,
557
+ 'grid_idx': 2,
558
+ 'n_ant_bs': 32,
559
+ 'n_subcarriers': 32
560
+ },
561
+ 'o1_3p5_v13': {
562
+ 'n_rows': [3852, 12*int(3852/12)+1*int(1351/10)],
563
+ 'n_per_row': 361,
564
+ 'grid_idx': 3,
565
+ 'n_ant_bs': 32,
566
+ 'n_subcarriers': 64
567
+ },
568
+ 'o1_3p5_v14': {
569
+ 'n_rows': [12*int(3852/12)+1*int(1351/10), 12*int(3852/12)+2*int(1351/10)],
570
+ 'n_per_row': 181,
571
+ 'grid_idx': 3,
572
+ 'n_ant_bs': 32,
573
+ 'n_subcarriers': 128
574
+ },
575
+ 'o1_3p5_v15': {
576
+ 'n_rows': [12*int(3852/12)+2*int(1351/10), 12*int(3852/12)+3*int(1351/10)],
577
+ 'n_per_row': 181,
578
+ 'grid_idx': 3,
579
+ 'n_ant_bs': 32,
580
+ 'n_subcarriers': 256
581
+ },
582
+ 'o1_3p5_v16': {
583
+ 'n_rows': [12*int(3852/12)+3*int(1351/10), 12*int(3852/12)+4*int(1351/10)],
584
+ 'n_per_row': 181,
585
+ 'grid_idx': 3,
586
+ 'n_ant_bs': 64,
587
+ 'n_subcarriers': 32
588
+ },
589
+ 'o1_3p5_v17': {
590
+ 'n_rows': [12*int(3852/12)+4*int(1351/10), 12*int(3852/12)+5*int(1351/10)],
591
+ 'n_per_row': 181,
592
+ 'grid_idx': 3,
593
+ 'n_ant_bs': 64,
594
+ 'n_subcarriers': 64
595
+ },
596
+ 'o1_3p5_v18': {
597
+ 'n_rows': [12*int(3852/12)+5*int(1351/10), 12*int(3852/12)+6*int(1351/10)],
598
+ 'n_per_row': 181,
599
+ 'grid_idx': 3,
600
+ 'n_ant_bs': 64,
601
+ 'n_subcarriers': 128
602
+ },
603
+ 'o1_3p5_v19': {
604
+ 'n_rows': [12*int(3852/12)+6*int(1351/10), 12*int(3852/12)+7*int(1351/10)],
605
+ 'n_per_row': 181,
606
+ 'grid_idx': 3,
607
+ 'n_ant_bs': 128,
608
+ 'n_subcarriers': 32
609
+ },
610
+ 'o1_3p5_v20': {
611
+ 'n_rows': [12*int(3852/12)+7*int(1351/10), 12*int(3852/12)+8*int(1351/10)],
612
+ 'n_per_row': 181,
613
+ 'grid_idx': 3,
614
+ 'n_ant_bs': 128,
615
+ 'n_subcarriers': 64
616
+ }}
617
+ return row_column_users
618
+
619
+ # =============================================================================
620
+ # 4. TRAINING PARAMETERS AND HYPERPARAMETERS
621
+ # - Set training epochs, batch sizes, learning rates, model dimensions, etc.
622
+ # =============================================================================
623
+
624
+ EPOCHS = 50
625
+ BATCH_SIZE = 128
626
+ VAL_BATCH_SIZE = 64
627
+ WARMUP_EPOCHS = 5
628
+ BASE_LR = 5e-4
629
+ MIN_LR = 1e-8
630
+ N_ROWS = 4
631
+ N_COLUMNS = 4
632
+ ELEMENT_LENGTH = N_ROWS * N_COLUMNS * 2
633
+ D_MODEL = 128
634
+ MAX_LEN = 513
635
+ N_LAYERS = 12
636
+ N_ANT_BS = 64
637
+ N_SUBCARRIERS = 64
638
+ device_idx = 0
639
+ WEIGHT_DECAY = 0.05
640
+ BETA1 = 0.9
641
+ BETA2 = 0.999
642
+ MASK_PERCENT = 0.4
643
+ N_HEADS = 8
644
+ DROPOUT = 0.1
645
+ task = ["LosNlosClassification",
646
+ "BeamPrediction",
647
+ "ChannelInterpolation",
648
+ "ChannelEstimation",
649
+ "ChannelCharting",
650
+ None][-1]
651
+
652
+ # =============================================================================
653
+ # 5. DATA GENERATION LOOP
654
+ # - Iterate over scenarios and base station indices to generate channel samples and labels
655
+ # - Handle both full-scenario and zoned sub-scenarios for campus and Boston data
656
+ # =============================================================================
657
+
658
+ scenarios = scenarios_list()
659
+
660
+ channels = []
661
+ labels = []
662
+ scenario_properties = scenario_prop()
663
+ preprocessed_data = []
664
+
665
+ for scenario in scenarios[:-3]:
666
+ for bs_idx in range (1,4):
667
+ scenario_channels, scenario_labels = generate_channels_and_labels(
668
+ n_ant_bs=scenario_properties[scenario]["n_ant_bs"],
669
+ n_subcarriers=scenario_properties[scenario]["n_subcarriers"],
670
+ bs_idx=bs_idx,
671
+ scenario_name=scenario,
672
+ task=task,
673
+ n_beams=64
674
+ )
675
+ labels.extend(scenario_labels)
676
+ channels.append(scenario_channels)
677
+
678
+ bs_idxs = [[1], [4, 15], [2]]
679
+ for scenario_idx, scenario in enumerate(scenarios[-3:]):
680
+ for bs_idx in bs_idxs[scenario_idx]:
681
+ for zone in range (20):
682
+ row_start = scenario_properties[scenario+f"_v{zone+1}"]["n_rows"][0]
683
+ row_end = scenario_properties[scenario+f"_v{zone+1}"]["n_rows"][1]
684
+ grid_idx = scenario_properties[scenario+f"_v{zone+1}"]["grid_idx"]-1
685
+ scenario_channels, scenario_labels = generate_channels_and_labels(
686
+ n_ant_bs=scenario_properties[scenario+f"_v{zone+1}"]["n_ant_bs"],
687
+ n_subcarriers=scenario_properties[scenario+f"_v{zone+1}"]["n_subcarriers"],
688
+ grid_idx=grid_idx,
689
+ bs_idx=bs_idx,
690
+ scenario_name=scenario,
691
+ rows=np.arange(row_start, row_end),
692
+ task=task,
693
+ n_beams=64
694
+ )
695
+
696
+ if scenario_channels.numel() == 0:
697
+ print(f"No candidate user in zone {zone} for scenario {scenario} has a path to bs_idx {bs_idx} (All channels are zero)")
698
+ continue
699
+
700
+ labels.extend(scenario_labels)
701
+ channels.append(scenario_channels)
702
+
703
+ # =============================================================================
704
+ # 6. DATA TOKENIZATION
705
+ # - Tokenize channel matrices into input sequences with masking for pretraining
706
+ # =============================================================================
707
+
708
+ preprocessed_data = tokenizer_train(
709
+ channels,
710
+ max_len=MAX_LEN,
711
+ masking_percent=MASK_PERCENT,
712
+ mask=True,
713
+ seed=42
714
+ )
715
+
716
+ # =============================================================================
717
+ # 7. TRAIN/VALIDATION/TEST SPLIT
718
+ # - Split each tokenized dataset into train, validation, and test subsets with a fixed random seed
719
+ # =============================================================================
720
+
721
+ SEED = 42
722
+ torch.manual_seed(SEED)
723
+ np.random.seed(SEED)
724
+ train_ratio = 0.8
725
+ val_ratio = 0.2
726
+ train_data = {}
727
+ val_data = {}
728
+ test_data = {}
729
+
730
+ for key, samples in preprocessed_data.items():
731
+ print(f"key: {key}")
732
+ total_samples = len(samples)
733
+ train_size = int(train_ratio * total_samples)
734
+ val_size = int(val_ratio * total_samples)
735
+ test_size = total_samples - train_size - val_size
736
+
737
+ train_data[key], val_data[key], test_data[key] = random_split(
738
+ samples, [train_size, val_size, test_size]
739
+ )
740
+
741
+ # =============================================================================
742
+ # 8. DATALOADER CREATION
743
+ # - Build PyTorch DataLoader objects for batched training and validation
744
+ # =============================================================================
745
+
746
+ train_loaders = create_train_dataloader(train_data, batch_size=BATCH_SIZE, shuffle=True)
747
+ val_loaders = create_train_dataloader(val_data, batch_size=VAL_BATCH_SIZE, shuffle=False)
748
+
749
+ # =============================================================================
750
+ # 9. MODEL INITIALIZATION
751
+ # - Instantiate the LWM transformer model and optionally load pre-trained weights
752
+ # - Wrap with DataParallel for multi-GPU support
753
+ # =============================================================================
754
+
755
+ gpu_ids = [1] # device_idx
756
+ device = torch.device(f"cuda:{gpu_ids[0]}" if torch.cuda.is_available() else "cpu")
757
+ model = pretrained_model.lwm(
758
+ element_length=ELEMENT_LENGTH,
759
+ d_model=D_MODEL,
760
+ n_layers=N_LAYERS,
761
+ max_len=MAX_LEN,
762
+ n_heads=N_HEADS,
763
+ dropout=DROPOUT
764
+ ).to(device)
765
+
766
+ # Optional: Load pre-trained model
767
+ load_model = False
768
+ if load_model:
769
+ model.load_state_dict(torch.load("models/model_checkpoint.pth", map_location=device))
770
+ print("Pre-trained model loaded successfully.")
771
+
772
+ # Use DataParallel for multi-GPU support
773
+ model = nn.DataParallel(model, device_ids=gpu_ids)
774
+ print(f"Model loaded successfully on GPU {device.index}")
775
+ n_parameters = count_parameters(model)
776
+ print(f"Number of trainable parameters: {n_parameters:,}")
777
+
778
+ # =============================================================================
779
+ # 10. OPTIMIZER AND LEARNING RATE SCHEDULER
780
+ # - Configure AdamW optimizer and a cosine-with-warmup LR schedule based on total steps
781
+ # =============================================================================
782
+
783
+ TOTAL_STEPS = sum(len(loader) for loader in train_loaders.values()) * EPOCHS
784
+ WARMUP_STEPS = sum(len(loader) for loader in train_loaders.values()) * WARMUP_EPOCHS
785
+
786
+ optimizer = AdamW(
787
+ model.parameters(),
788
+ lr=BASE_LR,
789
+ betas=(BETA1, BETA2),
790
+ weight_decay=WEIGHT_DECAY
791
+ )
792
+
793
+ def lr_lambda(current_step):
794
+ if current_step < WARMUP_STEPS:
795
+ return current_step / WARMUP_STEPS
796
+ else:
797
+ scaled_progress = (current_step - WARMUP_STEPS) / (TOTAL_STEPS - WARMUP_STEPS)
798
+ cosine_decay = 0.5 * (1 + np.cos(np.pi * scaled_progress))
799
+ return cosine_decay * (BASE_LR - MIN_LR) / BASE_LR + MIN_LR / BASE_LR
800
+
801
+ scheduler = LambdaLR(optimizer, lr_lambda=lr_lambda)
802
+
803
+ # =============================================================================
804
+ # 11. PRE-TRAINING LOOP
805
+ # - Call the train_lwm utility to run the pre-training epochs, logging metrics and saving models
806
+ # =============================================================================
807
+
808
+ pretrained_model = train_lwm(
809
+ model,
810
+ train_loaders,
811
+ val_loaders,
812
+ optimizer,
813
+ scheduler,
814
+ EPOCHS,
815
+ device=device,
816
+ save_dir="pretrained_models",
817
+ log_file="training_log.csv"
818
+ )
utils.py ADDED
@@ -0,0 +1,1119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import numpy as np
4
+ import os
5
+ from tqdm import tqdm
6
+ import pickle
7
+ import deepmimo as dm
8
+ from collections import defaultdict
9
+ from torch.utils.data import TensorDataset, DataLoader
10
+ import matplotlib.pyplot as plt
11
+ from sklearn.decomposition import PCA
12
+ from sklearn.manifold import TSNE
13
+ import umap
14
+ from math import pi
15
+ import zipfile
16
+ import shutil
17
+
18
+ def generate_channels_and_labels(
19
+ n_ant_bs=16,
20
+ n_subcarriers=64,
21
+ bs_idx=3,
22
+ grid_idx=0,
23
+ scenario_name=0,
24
+ scenario_idx=None,
25
+ rows=None,
26
+ task="LosNlosClassification",
27
+ n_beams=64
28
+ ):
29
+ """
30
+ Generate wireless channel samples and task-specific labels for pre-training or evaluation.
31
+
32
+ Args:
33
+ n_ant_bs (int): Number of antennas at the base station. Defaults to 16.
34
+ n_subcarriers (int): Number of subcarriers per channel. Defaults to 64.
35
+ bs_idx (int): Index of the base station to generate data for. Defaults to 3.
36
+ scenario_idx (int): Index of the scenario to select from available scenarios. Defaults to 0.
37
+ task (str): Task for which to generate labels. Defaults to "LosNlosClassification".
38
+ n_beams (int): Number of beams for beam-related tasks. Defaults to 64.
39
+
40
+ Returns:
41
+ tuple: A tuple containing:
42
+ - channels (list of torch.Tensor): List of generated channel tensors.
43
+ - labels (list): Corresponding task-specific labels.
44
+ """
45
+ if scenario_idx:
46
+ scenario_name = dm.search({})[scenario_idx]
47
+
48
+ channels, labels = dataset_generator(
49
+ n_ant_bs=n_ant_bs,
50
+ n_subcarriers=n_subcarriers,
51
+ grid_idx=grid_idx,
52
+ scenario_name=scenario_name,
53
+ rows=rows,
54
+ bs_idx=bs_idx,
55
+ task=task,
56
+ n_beams=n_beams
57
+ )
58
+
59
+ return channels, labels
60
+
61
+ def dataset_generator(
62
+ n_ant_bs=32,
63
+ n_subcarriers=32,
64
+ scenario_name="city_0_newyork_3p5",
65
+ grid_idx=0,
66
+ rows=None,
67
+ bs_idx=1,
68
+ save_dir="data",
69
+ task="LosNlosClassification",
70
+ n_beams=64,
71
+ snr=None,
72
+ seed=42
73
+ ):
74
+ """
75
+ Generate wireless channel data and task-specific labels using DeepMIMO dataset.
76
+
77
+ Args:
78
+ n_ant_bs (int): Number of antennas at the base station. Defaults to 32.
79
+ n_subcarriers (int): Number of subcarriers per channel. Defaults to 32.
80
+ scenario_name (str): Name of the scenario for data generation. Defaults to "city_0_newyork_3p5".
81
+ bs_idx (int): Index of the base station to generate data for. Defaults to 1.
82
+ save_dir (str): Directory to save generated data. Defaults to "data".
83
+ task (str): Task for which to generate labels. Defaults to "LosNlosClassification".
84
+ n_beams (int): Number of beams for beam-related tasks. Defaults to 64.
85
+ snr (float, optional): Signal-to-noise ratio for adding Gaussian noise in robust beamforming task.
86
+ Defaults to None.
87
+ seed (int): Random seed for reproducibility in noise generation. Defaults to 42.
88
+
89
+ Returns:
90
+ tuple: A tuple containing:
91
+ - cleaned_deepmimo_data (torch.Tensor): Cleaned channel data tensor.
92
+ - labels (torch.Tensor or list): Task-specific labels or NaN-filled tensor if task is None.
93
+ """
94
+ os.makedirs(save_dir, exist_ok=True)
95
+ print(f"\nGenerating data for scenario: {scenario_name}, BS #{bs_idx}")
96
+ deepmimo_data = DeepMIMO_data_gen(scenario_name,
97
+ n_ant_bs,
98
+ 1, n_subcarriers,
99
+ bs_idx=bs_idx,
100
+ row_indices=rows,
101
+ grid_idx=grid_idx)
102
+ if task is not None:
103
+ labels = label_gen(deepmimo_data, task, scenario_name, bs_idx=bs_idx, n_beams=n_beams)
104
+ else:
105
+ n_channels = len(np.where(deepmimo_data.los != -1)[0])
106
+ labels = np.nan * torch.ones(n_channels)
107
+ cleaned_deepmimo_data = deepmimo_data_cleaning(deepmimo_data)
108
+ if snr is not None and task in["ChannelEstimation", "LosNlosClassification"]:
109
+ cleaned_deepmimo_data = generate_gaussian_noise(cleaned_deepmimo_data, snr, seed=seed)
110
+ return cleaned_deepmimo_data.squeeze(1), labels
111
+
112
+ def label_gen(data, task, scenario_name, bs_idx=1, n_beams=64):
113
+ """
114
+ Generate task-specific labels for wireless channel data.
115
+
116
+ Args:
117
+ data (object): DeepMIMO data object containing channel, LOS status, and receiver positions.
118
+ task (str): Task for which to generate labels. Options: 'LosNlosClassification',
119
+ 'BeamPrediction', 'ChannelCharting', 'ChannelEstimation', 'ChannelInterpolation'.
120
+ scenario_name (str): Name of the scenario for data generation.
121
+ bs_idx (int): Index of the base station. Defaults to 1.
122
+ n_beams (int): Number of beams for beam-related tasks. Defaults to 64.
123
+
124
+ Returns:
125
+ torch.Tensor: Task-specific labels for the valid data indices.
126
+ """
127
+ labels = 0
128
+ idxs = np.where(data.los != -1)[0]
129
+
130
+ if len(idxs) == 0: # Users with no path (do not need them for pre-training). You should set the task to None in train_lwm.py
131
+ labels = np.full(data.n_ue, np.nan, dtype=float)
132
+
133
+ else:
134
+ if task == 'LosNlosClassification':
135
+
136
+ labels = data.los[idxs].astype(int)
137
+ dm.plot_coverage(data.rx_pos[idxs], data.los[idxs], cbar_title='LoS status')
138
+
139
+ elif task == 'BeamPrediction':
140
+
141
+ parameters = get_parameters()
142
+ n_users = len(data.channel)
143
+ n_subbands = 1
144
+ fov = 180
145
+
146
+ # Setup Beamformers
147
+ beam_angles = np.around(np.arange(-fov/2, fov/2+.1, fov/(n_beams-1)), 2)
148
+
149
+ F1 = np.array([dm.steering_vec(data.ch_params.bs_antenna.shape, phi=azi).squeeze()
150
+ for azi in beam_angles])
151
+
152
+ full_dbm = np.zeros((n_beams, n_subbands, n_users), dtype=float)
153
+ for ue_idx in tqdm(range(n_users), desc='Computing the channel for each user'):
154
+ if data.los[ue_idx] == -1:
155
+ full_dbm[:,:,ue_idx] = np.nan
156
+ else:
157
+ chs = F1 @ data.channel[ue_idx]
158
+ full_linear = np.abs(np.mean(chs.squeeze().reshape((n_beams, n_subbands, -1)), axis=-1))
159
+ full_dbm[:,:,ue_idx] = np.around(20*np.log10(full_linear) + 30, 1)
160
+
161
+ best_beams = np.argmax(np.mean(full_dbm,axis=1), axis=0)
162
+ best_beams = best_beams.astype(float)
163
+ best_beams[np.isnan(full_dbm[0,0,:])] = np.nan
164
+
165
+ dm.plot_coverage(data.rx_pos[idxs], best_beams[idxs], bs_pos=data.tx_pos,
166
+ bs_ori=parameters.bs_antenna.rotation*np.pi/180,
167
+ cbar_title='Best beam index')
168
+
169
+ labels = best_beams[idxs].astype(int)
170
+
171
+ elif task == 'ChannelCharting':
172
+
173
+ labels = torch.tensor(data.rx_pos[:,:2][idxs]).to(dtype=torch.float32)
174
+
175
+ elif task == 'ChannelEstimation':
176
+
177
+ channels = torch.tensor(data.channel[idxs]*1e6, dtype=torch.complex64).squeeze(1)
178
+ labels = torch.stack((channels.real, channels.imag), dim=1)
179
+
180
+ elif task == 'ChannelInterpolation':
181
+
182
+ channels = torch.tensor(data.channel[idxs]*1e6, dtype=torch.complex64).squeeze(1)
183
+ labels = torch.stack((channels.real, channels.imag), dim=1)
184
+
185
+ labels = torch.tensor(labels)
186
+
187
+ return labels
188
+
189
+ def generate_gaussian_noise(data, snr_db, seed=42):
190
+ """
191
+ Add complex Gaussian noise to channel data based on a specified signal-to-noise ratio (SNR).
192
+
193
+ Args:
194
+ data (torch.Tensor): Input complex-valued channel data with shape (n_samples, 1, n_ant, n_sc).
195
+ snr_db (float): Signal-to-noise ratio in decibels.
196
+ seed (int): Random seed for reproducibility of noise generation. Defaults to 42.
197
+
198
+ Returns:
199
+ torch.Tensor: Noisy channel data with the same shape as the input, with added complex Gaussian noise.
200
+ """
201
+ torch.manual_seed(seed)
202
+ data = data.squeeze(1) # Shape: (n_samples, n_ant, n_sc)
203
+ flat_data = data.view(data.size(0), -1)
204
+
205
+ # Compute signal power
206
+ signal_power = torch.mean(flat_data.abs() ** 2, dim=1, keepdim=True)
207
+ snr_linear = 10 ** (snr_db / 10)
208
+ noise_power = signal_power / snr_linear
209
+
210
+ # Generate noise
211
+ noise_real = torch.randn_like(flat_data.real) * torch.sqrt(noise_power / 2)
212
+ noise_imag = torch.randn_like(flat_data.imag) * torch.sqrt(noise_power / 2)
213
+ noise = torch.complex(noise_real, noise_imag)
214
+
215
+ # Reshape noise and add to data
216
+ noise = noise.view_as(data)
217
+ noisy_data = data + noise
218
+ noisy_data = noisy_data.unsqueeze(1)
219
+
220
+ return noisy_data
221
+
222
+ # REMOVE ZERO CHANNELS AND SCALE
223
+ def deepmimo_data_cleaning(deepmimo_data):
224
+ """
225
+ Clean DeepMIMO channel data by removing invalid channels and scaling the valid ones.
226
+
227
+ Args:
228
+ deepmimo_data (object): DeepMIMO data object containing channel data and LOS status.
229
+
230
+ Returns:
231
+ torch.Tensor: Cleaned and scaled channel data as a complex-valued tensor with dtype torch.complex64.
232
+ """
233
+ idxs = np.where(deepmimo_data.los != -1)[0]
234
+ cleaned_deepmimo_data = deepmimo_data.channel[idxs]
235
+ return torch.tensor(cleaned_deepmimo_data, dtype=torch.complex64) * 1e6
236
+
237
+ def manual_unzip_scenario(scenario_name):
238
+ """Manually unzip a downloaded scenario to avoid DeepMIMO corruption issues."""
239
+ scenarios_dir = os.path.join(os.getcwd(), "deepmimo_scenarios")
240
+ zip_path = os.path.join(scenarios_dir, f"{scenario_name}_downloaded.zip")
241
+ scenario_path = os.path.join(scenarios_dir, scenario_name)
242
+
243
+ # Remove existing unzipped folder if it exists
244
+ if os.path.exists(scenario_path):
245
+ print(f"Removing existing scenario folder: {scenario_path}")
246
+ shutil.rmtree(scenario_path)
247
+
248
+ # Manually unzip the downloaded file
249
+ if os.path.exists(zip_path):
250
+ print(f"Manually unzipping: {zip_path}")
251
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
252
+ zip_ref.extractall(scenarios_dir)
253
+ print(f"Successfully extracted to: {scenario_path}")
254
+ return True
255
+ else:
256
+ print(f"Zip file not found: {zip_path}")
257
+ return False
258
+
259
+ # Data Generation
260
+ def DeepMIMO_data_gen(scenario, num_ant_hor, num_ant_vert, n_subcarriers, bs_idx, row_indices, grid_idx):
261
+ """
262
+ Generates wireless channel data for a specified DeepMIMO scenario and base station configuration.
263
+
264
+ This function downloads and loads the specified DeepMIMO scenario, filters the users (if row_indices are given),
265
+ and computes the MIMO channels using the provided antenna and subcarrier configuration. It also plots the
266
+ line-of-sight (LoS) coverage map for visualization.
267
+
268
+ Args:
269
+ scenario (str): Name of the DeepMIMO scenario (e.g., 'o1_3p5').
270
+ num_ant_hor (int): Number of horizontal antennas at the base station (for the UPA configuration).
271
+ num_ant_vert (int): Number of vertical antennas at the base station.
272
+ n_subcarriers (int): Number of subcarriers to simulate per user.
273
+ bs_idx (int): Index of the base station to extract channels from.
274
+ row_indices (list or None): List of user row indices to subset the dataset. If None, all users are included.
275
+
276
+ Returns:
277
+ object: A DeepMIMO data object containing:
278
+ - Computed channel matrices for selected users and base station.
279
+ - User metadata including location, LoS status, and other parameters.
280
+ - Scenario-specific attributes for analysis and visualization.
281
+ """
282
+ parameters = get_parameters(num_ant_hor, num_ant_vert, n_subcarriers)
283
+ dm.download(scenario)
284
+
285
+ # Manual unzip for LWM scenarios to avoid corruption
286
+ if "lwm" in scenario:
287
+ manual_unzip_scenario(scenario)
288
+
289
+ data = dm.load(scenario, tx_sets=[bs_idx], rx_sets=[grid_idx])
290
+
291
+ if row_indices is not None:
292
+ if grid_idx == 0:
293
+ row_idxs = data.get_row_idxs(row_indices) # example: row_indices = np.arange(40,60)
294
+ data = data.subset(row_idxs)
295
+ elif grid_idx == 1:
296
+ if scenario == "o1_3p5":
297
+ col_idxs = data.get_col_idxs(row_indices-2751)
298
+ data = data.subset(col_idxs)
299
+ elif scenario == "boston5G_3p5":
300
+ row_idxs = data.get_row_idxs(row_indices-812)
301
+ data = data.subset(row_idxs)
302
+ elif grid_idx == 2:
303
+ if scenario == "o1_3p5":
304
+ col_idxs = data.get_col_idxs(row_indices-3852)
305
+ data = data.subset(col_idxs)
306
+
307
+ # data.plot_coverage(data.los)
308
+ data.compute_channels(parameters)
309
+
310
+ return data
311
+
312
+ def get_parameters(num_ant_hor=32, num_ant_vert=1, n_subcarriers=32):
313
+ """
314
+ Generate channel parameters for DeepMIMO dataset generation.
315
+
316
+ Args:
317
+ num_ant_hor (int): Number of horizontal antennas at the base station. Defaults to 32.
318
+ num_ant_vert (int): Number of vertical antennas at the base station. Defaults to 1.
319
+ n_subcarriers (int): Number of subcarriers per channel. Defaults to 32.
320
+ bs_idx (int): Index of the base station. Defaults to 1.
321
+
322
+ Returns:
323
+ dm.ChannelGenParameters: Configured channel parameters object for DeepMIMO data generation.
324
+ """
325
+ # Create channel parameters with all options
326
+ ch_params = dm.ChannelParameters()
327
+
328
+ # Antenna parameters
329
+
330
+ # Base station antenna parameters
331
+ ch_params.bs_antenna.rotation = np.array([0, 0, -135]) # [az, el, pol] in degrees
332
+ ch_params.bs_antenna.fov = np.array([360, 180]) # [az, el] in degrees
333
+ ch_params.bs_antenna.shape = np.array([num_ant_hor, num_ant_vert]) # [horizontal, vertical] elements
334
+ ch_params.bs_antenna.spacing = 0.5 # Element spacing in wavelengths
335
+
336
+ # User equipment antenna parameters
337
+ ch_params.ue_antenna.rotation = np.array([0, 0, 0]) # [az, el, pol] in degrees
338
+ ch_params.ue_antenna.fov = np.array([360, 180]) # [az, el] in degrees
339
+ ch_params.ue_antenna.shape = np.array([1, 1]) # [horizontal, vertical] elements
340
+ ch_params.ue_antenna.spacing = 0.5 # Element spacing in wavelengths
341
+
342
+ # Channel parameters
343
+ ch_params.freq_domain = True # Whether to compute frequency domain channels
344
+ ch_params.num_paths = 20 # Number of paths
345
+
346
+ # OFDM parameters
347
+ subcarrier_spacing = 30e3
348
+ ch_params.ofdm.subcarriers = n_subcarriers # Number of subcarriers
349
+ ch_params.ofdm.selected_subcarriers = np.arange(n_subcarriers) # Which subcarriers to generate
350
+ ch_params.ofdm.bandwidth = subcarrier_spacing * n_subcarriers # Bandwidth in Hz
351
+ ch_params.ofdm.rx_filter = 0
352
+
353
+ return ch_params
354
+
355
+ def tokenizer_train(channels,
356
+ max_len=513,
357
+ masking_percent=0.40,
358
+ mask=False,
359
+ seed=42):
360
+ """
361
+ Tokenize wireless channel data into patches and optionally apply masking.
362
+
363
+ Args:
364
+ channels (torch.Tensor or list): Input channel data to be tokenized.
365
+ max_len (int): Maximum sequence length for tokenized samples. Defaults to 513.
366
+ masking_percent (float): Percentage of patches to mask if mask is True. Defaults to 0.40.
367
+ mask (bool): Whether to apply masking to the tokenized samples. Defaults to False.
368
+ seed (int): Random seed for reproducibility in masking. Defaults to 42.
369
+
370
+ Returns:
371
+ dict or torch.Tensor: If mask is True, returns a dictionary mapping sequence lengths to lists
372
+ of tokenized samples. If mask is False, returns a tensor of stacked tokenized samples.
373
+ """
374
+ patches = [patch_maker(channel_set, patch_rows=4, patch_cols=4) for channel_set in channels]
375
+ patches = [patch for patch_list in patches for patch in patch_list]
376
+ print("\nTotal number of samples:", len(patches))
377
+
378
+ grouped_data = defaultdict(list) # Group samples by sequence length
379
+ grouped_data_2 = []
380
+
381
+ for user_idx in tqdm(range(len(patches)), desc="Processing items"):
382
+ patch_size = patches[user_idx].shape[1]
383
+ n_patches = patches[user_idx].shape[0]
384
+ n_masks_half = int(masking_percent * n_patches)
385
+
386
+ word2id = {
387
+ '[CLS]': 0.2 * np.ones((patch_size)),
388
+ '[MASK]': 0.1 * np.ones((patch_size))
389
+ }
390
+
391
+ sample = make_sample(
392
+ user_idx, patches, word2id, n_patches, n_masks_half, patch_size, mask=mask, seed=seed
393
+ )
394
+
395
+ if mask:
396
+ seq_length = len(sample[0])
397
+ grouped_data[seq_length].append(sample)
398
+ else:
399
+ grouped_data_2.append(sample)
400
+
401
+ if mask:
402
+ normalized_grouped_data = {i: grouped_data[key] for i, key in enumerate(sorted(grouped_data.keys()))}
403
+ else:
404
+ normalized_grouped_data = torch.stack(grouped_data_2, dim=0)
405
+
406
+ return normalized_grouped_data
407
+
408
+ def tokenizer(channels,
409
+ max_len=513,
410
+ masking_percent=0.40,
411
+ mask=False,
412
+ seed=42):
413
+ """
414
+ Tokenize wireless channel data into patches and optionally apply masking.
415
+
416
+ Args:
417
+ channels (torch.Tensor or list): Input channel data to be tokenized.
418
+ max_len (int): Maximum sequence length for tokenized samples. Defaults to 513.
419
+ masking_percent (float): Percentage of patches to mask if mask is True. Defaults to 0.40.
420
+ mask (bool): Whether to apply masking to the tokenized samples. Defaults to False.
421
+ seed (int): Random seed for reproducibility in masking. Defaults to 42.
422
+
423
+ Returns:
424
+ dict or torch.Tensor: If mask is True, returns a dictionary mapping sequence lengths to lists
425
+ of tokenized samples. If mask is False, returns a tensor of stacked tokenized samples.
426
+ """
427
+ patches = patch_maker(channels, patch_rows=4, patch_cols=4)
428
+ print("\nTotal number of samples:", len(patches))
429
+
430
+ grouped_data = defaultdict(list) # Group samples by sequence length
431
+ grouped_data_2 = []
432
+
433
+ for user_idx in tqdm(range(len(patches)), desc="Processing items"):
434
+ patch_size = patches[user_idx].shape[1]
435
+ n_patches = patches[user_idx].shape[0]
436
+ n_masks_half = int(masking_percent * n_patches)
437
+
438
+ word2id = {
439
+ '[CLS]': 0.2 * np.ones((patch_size)),
440
+ '[MASK]': 0.1 * np.ones((patch_size))
441
+ }
442
+
443
+ sample = make_sample(
444
+ user_idx, patches, word2id, n_patches, n_masks_half, patch_size, mask=mask, seed=seed
445
+ )
446
+
447
+ if mask:
448
+ seq_length = len(sample[0])
449
+ grouped_data[seq_length].append(sample)
450
+ else:
451
+ grouped_data_2.append(sample)
452
+
453
+ if mask:
454
+ normalized_grouped_data = {i: grouped_data[key] for i, key in enumerate(sorted(grouped_data.keys()))}
455
+ else:
456
+ normalized_grouped_data = torch.stack(grouped_data_2, dim=0)
457
+
458
+ return normalized_grouped_data
459
+
460
+ def make_sample(user_idx, patch, word2id, n_patches, n_masks, patch_size, mask=True, seed=None):
461
+ """
462
+ Create a tokenized sample from patch data, optionally applying masking for a specific user.
463
+
464
+ Args:
465
+ user_idx (int): Index of the user whose patch data is to be processed.
466
+ patch (numpy.ndarray or torch.Tensor): Patch data for all users.
467
+ word2id (dict): Dictionary mapping special tokens ('[CLS]', '[MASK]') to their representations.
468
+ n_patches (int): Number of patches in the input data.
469
+ n_masks (int): Number of patches to mask if mask is True.
470
+ patch_size (int): Size of each patch.
471
+ mask (bool): Whether to apply masking to the sample. Defaults to True.
472
+ seed (int, optional): Random seed for reproducibility in masking. Defaults to None.
473
+
474
+ Returns:
475
+ torch.Tensor or list: If mask is False, returns a tensor of input IDs with [CLS] prepended.
476
+ If mask is True, returns a list containing input IDs, masked tokens, and masked positions.
477
+ """
478
+ if seed is not None:
479
+ np.random.seed(seed)
480
+
481
+ # Step 1: Retrieve tokens and prepend [CLS]
482
+ tokens = patch[user_idx]
483
+ input_ids = np.vstack((word2id['[CLS]'], tokens))
484
+
485
+ # Step 2: Mask real and imaginary patches
486
+ tokens_size = int(n_patches) # int(n_patches / 2)
487
+ masked_pos = np.random.choice(range(1, tokens_size), size=n_masks, replace=False)
488
+
489
+ masked_tokens = []
490
+ for pos in masked_pos:
491
+ original_masked_tokens = input_ids[pos].copy()
492
+ masked_tokens.append(original_masked_tokens)
493
+ if mask:
494
+ rnd_num = np.random.rand()
495
+ if rnd_num < 0.1:
496
+ input_ids[pos] = np.random.rand(patch_size) # Replace with random values
497
+ elif rnd_num < 0.9:
498
+ input_ids[pos] = word2id['[MASK]'] # Replace with [MASK]
499
+
500
+ if not mask:
501
+ return torch.tensor(input_ids)
502
+ else:
503
+ return [input_ids, masked_tokens, masked_pos]
504
+
505
+ # Patch GENERATION
506
+ def patch_maker(original_ch, patch_rows=4, patch_cols=4):
507
+ """
508
+ Converts complex-valued channel matrices into flattened, interleaved real-imaginary patch embeddings.
509
+
510
+ This function takes a batch of complex-valued 2D channel matrices (one per sample), splits the real
511
+ and imaginary components, interleaves them along the last dimension, and divides the result into
512
+ non-overlapping patches of specified size. The output is a set of flattened patches per sample,
513
+ ready for use in models like Transformers.
514
+
515
+ Args:
516
+ original_ch (np.ndarray): Input array of shape (n_samples, n_rows, n_cols) with complex values.
517
+ patch_rows (int): Number of rows per patch. Default is 4.
518
+ patch_cols (int): Number of columns per patch. Default is 4.
519
+
520
+ Returns:
521
+ np.ndarray: Array of shape (n_samples, n_patches, patch_rows * patch_cols * 2), where each patch
522
+ is flattened and contains interleaved real and imaginary parts.
523
+ """
524
+ # Step 1: Remove the singleton channel dimension
525
+ n_samples, n_rows, n_cols = original_ch.shape # Unpack shape
526
+ # original_ch = original_ch[:, 0] # Remove the singleton dimension
527
+
528
+ # Step 2: Split into real and imaginary parts and interleave them
529
+ flat_real = original_ch.real
530
+ flat_imag = original_ch.imag
531
+
532
+ # Interleave real and imaginary parts along the last axis
533
+ interleaved = np.empty((n_samples, n_rows, n_cols * 2), dtype=np.float32)
534
+ interleaved[:, :, 0::2] = flat_real
535
+ interleaved[:, :, 1::2] = flat_imag
536
+
537
+ # Step 3: Compute the number of patches along rows and columns
538
+ n_patches_rows = int(np.ceil(n_rows / patch_rows))
539
+ n_patches_cols = int(np.ceil(n_cols / patch_cols))
540
+
541
+ # Step 4: Pad the matrix if necessary to make it divisible by patch size
542
+ padded_rows = n_patches_rows * patch_rows - n_rows
543
+ padded_cols = n_patches_cols * patch_cols - n_cols
544
+ if padded_rows > 0 or padded_cols > 0:
545
+ interleaved = np.pad(
546
+ interleaved,
547
+ ((0, 0), (0, padded_rows), (0, padded_cols * 2)), # Double padding for interleaved axis
548
+ mode='constant',
549
+ constant_values=0,
550
+ )
551
+
552
+ # Step 5: Create patches by dividing into blocks
553
+ n_samples, padded_rows, padded_cols = interleaved.shape
554
+ padded_cols //= 2 # Adjust for interleaving (real and imaginary parts count as one)
555
+ patches = []
556
+
557
+ for i in range(0, padded_rows, patch_rows):
558
+ for j in range(0, padded_cols, patch_cols):
559
+ patch = interleaved[:, i:i + patch_rows, j * 2:(j + patch_cols) * 2]
560
+ patches.append(patch.reshape(n_samples, -1)) # Flatten each patch
561
+
562
+ # Step 6: Stack patches to form the final array
563
+ patches = np.stack(patches, axis=1) # Shape: (num_samples, n_patches, patch_rows * patch_cols * 2)
564
+
565
+ return patches
566
+
567
+ def patch_reconstructor(patches, original_rows, original_cols, patch_rows=4, patch_cols=4):
568
+ """
569
+ Reconstructs the original channel matrix with real and imaginary parts as separate channels using PyTorch.
570
+
571
+ Args:
572
+ patches (torch.Tensor): Patches of shape (n_samples, n_patches, patch_rows * patch_cols * 2)
573
+ original_rows (int): Original number of rows (n_rows)
574
+ original_cols (int): Original number of columns (n_cols)
575
+ patch_rows (int): Number of rows per patch (default: 4)
576
+ patch_cols (int): Number of columns per patch (default: 4)
577
+
578
+ Returns:
579
+ torch.Tensor: Reconstructed channel matrix of shape (n_samples, 2, original_rows, original_cols)
580
+ where channel 0 is real and channel 1 is imaginary
581
+ """
582
+ # Step 1: Extract dimensions
583
+ n_samples, n_patches, patch_size = patches.shape
584
+ assert patch_size == patch_rows * patch_cols * 2, "Patch size does not match patch_rows * patch_cols * 2"
585
+
586
+ # Step 2: Compute the number of patches along rows and columns
587
+ # Use integer division since no padding is needed
588
+ n_patches_rows = original_rows // patch_rows
589
+ n_patches_cols = original_cols // patch_cols
590
+ assert n_patches == n_patches_rows * n_patches_cols, "Number of patches does not match expected grid"
591
+
592
+ # Step 3: Reshape patches back into 2D blocks
593
+ patches_2d = patches.reshape(n_samples, n_patches_rows, n_patches_cols, patch_rows, patch_cols * 2)
594
+
595
+ # Step 4: Reconstruct the interleaved matrix
596
+ # No padding, so use original dimensions directly
597
+ interleaved = torch.zeros((n_samples, original_rows, original_cols * 2), dtype=torch.float32, device=patches.device)
598
+ for i in range(n_patches_rows):
599
+ for j in range(n_patches_cols):
600
+ interleaved[:, i * patch_rows:(i + 1) * patch_rows, j * patch_cols * 2:(j + 1) * patch_cols * 2] = \
601
+ patches_2d[:, i, j, :, :]
602
+
603
+ # Step 5: De-interleave real and imaginary parts
604
+ flat_real = interleaved[:, :, 0::2]
605
+ flat_imag = interleaved[:, :, 1::2]
606
+
607
+ # Step 6: Stack real and imaginary parts as separate channels along axis=1
608
+ reconstructed = torch.stack((flat_real, flat_imag), dim=1) # Shape: (n_samples, 2, original_rows, original_cols)
609
+
610
+ return reconstructed
611
+
612
+ def create_train_dataloader(grouped_data, batch_size, shuffle):
613
+ """
614
+ Creates a dictionary of DataLoaders from grouped input data based on sequence lengths.
615
+
616
+ This function processes pre-grouped training data where each key in the dictionary corresponds
617
+ to a specific sequence length, and each value is a list of training samples. It converts the
618
+ data into PyTorch tensors and constructs a separate DataLoader for each sequence length.
619
+
620
+ Args:
621
+ grouped_data (dict): A dictionary where keys are sequence lengths (e.g., 1, 2, ..., T), and
622
+ values are lists of tuples (input_ids, masked_tokens, masked_pos).
623
+ batch_size (int): Batch size to use for the DataLoaders.
624
+ shuffle (bool): Whether to shuffle the data during loading.
625
+
626
+ Returns:
627
+ dict: A dictionary mapping each sequence length to its corresponding DataLoader.
628
+ """
629
+ dataloaders = {}
630
+
631
+ for seq_length, group in grouped_data.items():
632
+
633
+ print(f"dataloader in progress ...\nkey: {seq_length}")
634
+
635
+ ## Uncomment the following line if you run out of memory during pre-training
636
+ # batch_size = batch_size // 8 if seq_length >= 5 else batch_size
637
+
638
+ # Unpack samples for the current group
639
+ input_ids, masked_tokens, masked_pos = zip(*group)
640
+
641
+ # Convert to tensors
642
+ input_ids_tensor = torch.tensor(input_ids, dtype=torch.float32)
643
+ masked_tokens_tensor = torch.tensor(masked_tokens, dtype=torch.float32)
644
+ masked_pos_tensor = torch.tensor(masked_pos, dtype=torch.long)
645
+
646
+ # Create TensorDataset and DataLoader
647
+ dataset = TensorDataset(input_ids_tensor, masked_tokens_tensor, masked_pos_tensor)
648
+ dataloaders[seq_length] = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, pin_memory=True)
649
+
650
+ return dataloaders
651
+
652
+ def count_parameters(model):
653
+ """
654
+ Counts the number of trainable parameters in a PyTorch model.
655
+
656
+ Args:
657
+ model (torch.nn.Module): The model to inspect.
658
+
659
+ Returns:
660
+ int: Total number of trainable parameters (i.e., those with requires_grad=True).
661
+ """
662
+ return sum(p.numel() for p in model.parameters() if p.requires_grad)
663
+
664
+ def nmse_loss(y_true, y_pred):
665
+ """Compute Normalized Mean Squared Error (NMSE) using PyTorch.
666
+
667
+ Args:
668
+ y_true (torch.Tensor): Ground truth tensor.
669
+ y_pred (torch.Tensor): Predicted tensor.
670
+
671
+ Returns:
672
+ torch.Tensor: NMSE value.
673
+ """
674
+ # Ensure inputs are torch tensors
675
+ y_true = torch.as_tensor(y_true)
676
+ y_pred = torch.as_tensor(y_pred)
677
+
678
+ # Compute NMSE: mean((y_true - y_pred)^2) / mean(y_true^2)
679
+ squared_diff = torch.mean((y_true - y_pred) ** 2)
680
+ squared_true = torch.mean(y_true ** 2)
681
+ nmse = squared_diff / squared_true
682
+
683
+ return nmse
684
+
685
+ def train_lwm(model, train_loaders, val_loaders, optimizer, scheduler, epochs, device, save_dir="models", log_file="training_log.csv"):
686
+ """
687
+ Trains the Large Wireless Model (LWM) using masked channel modeling on grouped datasets of various sequence lengths.
688
+
689
+ The training alternates between training and evaluation every 2 epochs. For each sequence length,
690
+ a separate DataLoader is used. MSE is used for the training objective, and both MSE and NMSE are computed
691
+ during validation for performance tracking.
692
+
693
+ Args:
694
+ model (torch.nn.Module): The LWM model to train.
695
+ train_loaders (dict): Dictionary mapping sequence length to DataLoader for training data.
696
+ val_loaders (dict): Dictionary mapping sequence length to DataLoader for validation data.
697
+ optimizer (torch.optim.Optimizer): Optimizer to use for training.
698
+ scheduler (torch.optim.lr_scheduler._LRScheduler): Learning rate scheduler.
699
+ epochs (int): Total number of epochs to train for.
700
+ device (torch.device): Device to train on ('cuda' or 'cpu').
701
+ save_dir (str, optional): Directory to save the best model checkpoints. Default is "models".
702
+ log_file (str, optional): CSV path to log training/validation metrics. Default is "training_log.csv".
703
+
704
+ Returns:
705
+ model (torch.nn.Module): The trained model with the best checkpoint (based on validation MSE).
706
+ """
707
+ # Create save directory if it doesn't exist
708
+ os.makedirs(save_dir, exist_ok=True)
709
+
710
+ # Initialize loss criterion
711
+ criterion = nn.MSELoss(reduction='sum') # Sum reduction for manual averaging
712
+
713
+ # Initialize lists to store losses
714
+ train_mse_losses = []
715
+ val_mse_losses = []
716
+ val_nmse_losses = []
717
+ best_val_mse = float('inf')
718
+
719
+ for epoch in range(epochs):
720
+ # Training loop
721
+ model.train()
722
+ train_mse = 0.0
723
+ train_samples = 0
724
+
725
+ print(f"\nEpoch {epoch + 1}/{epochs} [Training]")
726
+ for length, train_loader in train_loaders.items():
727
+ print(f"Processing sequences of length {length}")
728
+ with tqdm(train_loader, desc=f"Length {length} [Training]", unit="batch") as t:
729
+ for batch in t:
730
+ optimizer.zero_grad()
731
+
732
+ # Move data to device
733
+ input_ids, masked_tokens, masked_pos = [b.to(device) for b in batch]
734
+
735
+ # Forward pass
736
+ logits_lm = model(input_ids, masked_pos)[0]
737
+
738
+ # Compute MSE loss
739
+ loss = criterion(masked_tokens, logits_lm)
740
+ loss.backward()
741
+ optimizer.step()
742
+ scheduler.step()
743
+
744
+ train_mse += loss.item()
745
+ train_samples += input_ids.shape[0]
746
+
747
+ # Update progress bar with MSE
748
+ t.set_postfix({"mse": train_mse / train_samples, "lr": scheduler.get_last_lr()[0]})
749
+
750
+ # Average MSE across training samples
751
+ train_mse = train_mse / max(train_samples, 1)
752
+ train_mse_losses.append(train_mse)
753
+
754
+ # Validation loop every 2 epochs
755
+ if epoch % 2 == 0:
756
+ model.eval()
757
+ val_mse = 0.0
758
+ val_nmse = 0.0
759
+ val_samples = 0
760
+
761
+ with torch.no_grad():
762
+ print(f"\nEpoch {epoch + 1}/{epochs} [Validation]")
763
+ for length, val_loader in val_loaders.items():
764
+ print(f"Processing sequences of length {length}")
765
+ with tqdm(val_loader, desc=f"Length {length} [Validation]", unit="batch") as t:
766
+ for batch in t:
767
+ # Move data to device
768
+ input_ids, masked_tokens, masked_pos = [b.to(device) for b in batch]
769
+
770
+ # Forward pass
771
+ logits_lm = model(input_ids, masked_pos)[0]
772
+
773
+ # Compute MSE loss
774
+ mse = criterion(masked_tokens, logits_lm)
775
+ val_mse += mse.item()
776
+
777
+ # Compute NMSE for reporting
778
+ masked_tokens_np = masked_tokens.cpu().numpy()
779
+ logits_lm_np = logits_lm.cpu().numpy()
780
+ nmse = nmse_loss(masked_tokens_np, logits_lm_np)
781
+ val_nmse += nmse * input_ids.shape[0]
782
+
783
+ val_samples += input_ids.shape[0]
784
+
785
+ # Update progress bar with both MSE and NMSE
786
+ t.set_postfix({"mse": val_mse / val_samples, "nmse": val_nmse / val_samples})
787
+
788
+ # Average MSE and NMSE across validation samples
789
+ val_mse = val_mse / max(val_samples, 1)
790
+ val_nmse = val_nmse / max(val_samples, 1)
791
+ val_mse_losses.append(val_mse)
792
+ val_nmse_losses.append(val_nmse)
793
+
794
+ # Save model if validation MSE improves
795
+ if val_mse < best_val_mse:
796
+ best_val_mse = val_mse
797
+ model_path = os.path.join(save_dir, f"lwm_epoch{epoch+1}_train{train_mse:.4f}_val{val_mse:.4f}.pth")
798
+ torch.save(model.state_dict(), model_path)
799
+ print(f"Model saved: {model_path}")
800
+
801
+ # Log the results
802
+ print(f" Train MSE: {train_mse:.4f}")
803
+ if epoch % 2 == 0:
804
+ print(f" Validation MSE: {val_mse:.4f}")
805
+ print(f" Validation NMSE: {val_nmse:.4f}")
806
+ print(f" Learning Rate: {scheduler.get_last_lr()[0]:.6e}")
807
+
808
+ # Plot losses after each epoch
809
+ plt.figure(figsize=(10, 6))
810
+ plt.plot(range(1, len(train_mse_losses) + 1), train_mse_losses, label="Train MSE")
811
+ if val_mse_losses: # Plot validation only if it exists
812
+ plt.plot(range(1, len(val_mse_losses) + 1), val_mse_losses, label="Validation MSE")
813
+ plt.plot(range(1, len(val_nmse_losses) + 1), val_nmse_losses, label="Validation NMSE")
814
+ plt.xlabel("Epochs")
815
+ plt.ylabel("Loss")
816
+ plt.title("Training and Validation Losses")
817
+ plt.legend()
818
+ plt.grid(True)
819
+ plt.show()
820
+
821
+ print("Training and validation complete.")
822
+ return model
823
+
824
+ def inference(model, tokens, batch_size=128, device="cuda"):
825
+ """
826
+ Runs inference using a trained model to extract embeddings from input tokens.
827
+
828
+ This function processes the input tokens in batches (without shuffling), moves them to the
829
+ specified device, passes them through the model, and aggregates the outputs.
830
+
831
+ Args:
832
+ model (torch.nn.Module): The trained model used for inference.
833
+ tokens (torch.Tensor): Input tensor of shape (N, ...) representing the tokenized data.
834
+ batch_size (int, optional): Batch size for inference. Default is 128.
835
+ device (str or torch.device, optional): Device to run inference on. Default is "cuda".
836
+
837
+ Returns:
838
+ torch.Tensor: A tensor of shape (N, D), where D is the embedding dimension produced by the model.
839
+ """
840
+ dataset = TensorDataset(tokens)
841
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
842
+
843
+ embeddings = []
844
+ model.eval()
845
+ with torch.no_grad():
846
+ with tqdm(dataloader, desc="Inference", unit="batch") as t:
847
+ for batch in t:
848
+
849
+ input_ids = batch[0].to(device)
850
+ output = model(input_ids)
851
+ embeddings.append(output)
852
+
853
+ output_total = torch.cat(embeddings, dim=0).float()
854
+ return output_total
855
+
856
+ def visualize_embeddings(embeddings, labels=None, method="tsne", label=None):
857
+ """
858
+ Visualizes high-dimensional embeddings in 2D using PCA, UMAP, or t-SNE.
859
+
860
+ This function reduces the dimensionality of embeddings to two components and visualizes them
861
+ with an optional color-coding based on provided labels. It supports three reduction methods:
862
+ PCA (linear), UMAP (nonlinear, preserves local/global structure), and t-SNE (nonlinear, local structure).
863
+
864
+ Args:
865
+ embeddings (torch.Tensor or np.ndarray): Embedding matrix of shape (n_samples, n_features).
866
+ labels (torch.Tensor or np.ndarray, optional): Class labels of shape (n_samples,). If provided,
867
+ each class will be visualized with a distinct color.
868
+ method (str): Dimensionality reduction method: one of {'pca', 'umap', 'tsne'}. Default is 'tsne'.
869
+ label (str, optional): Title for the plot. Defaults to "Embedding Visualization" if not provided.
870
+
871
+ Raises:
872
+ ValueError: If an unsupported dimensionality reduction method is specified.
873
+
874
+ Returns:
875
+ None. Displays a 2D scatter plot of the embeddings.
876
+ """
877
+ # to numpy
878
+ if isinstance(embeddings, torch.Tensor):
879
+ embeddings = embeddings.cpu().numpy()
880
+ if labels is not None and isinstance(labels, torch.Tensor):
881
+ labels = labels.cpu().numpy()
882
+
883
+ # choose reducer
884
+ m = method.lower()
885
+ if m == "pca":
886
+ reducer = PCA(n_components=2)
887
+ elif m == "umap":
888
+ reducer = umap.UMAP(n_components=2, n_neighbors=16, random_state=42)
889
+ elif m == "tsne":
890
+ reducer = TSNE(n_components=2, random_state=42, init="random")
891
+ else:
892
+ raise ValueError("Invalid method. Choose 'pca', 'umap', or 'tsne'.")
893
+
894
+ Z = reducer.fit_transform(embeddings)
895
+
896
+ plt.figure(figsize=(10, 8))
897
+ if labels is not None:
898
+ num_classes = len(np.unique(labels))
899
+ colors = plt.cm.get_cmap("tab10", num_classes)
900
+
901
+ for class_idx in range(num_classes):
902
+ class_points = Z[labels == class_idx]
903
+ plt.scatter(
904
+ class_points[:, 0], class_points[:, 1],
905
+ label=f"Class {class_idx}",
906
+ alpha=0.6,
907
+ cmap=colors
908
+ )
909
+ else:
910
+ plt.scatter(Z[:, 0], Z[:, 1], color="C0", alpha=0.6, label="Samples")
911
+
912
+ title = label or "Embedding Visualization"
913
+ plt.title(f"{title} ({method.upper()})")
914
+ plt.xlabel("Component 1")
915
+ plt.ylabel("Component 2")
916
+ plt.show()
917
+
918
+
919
+ def embedding_space_visual(model, data, input_type="cls_emb", device="cpu", batch_size=64, task=None, visualization=False, labels=None, visualization_method="tsne", selected_tokens=0):
920
+ """
921
+ Extracts embeddings from a model and optionally visualizes the embedding space.
922
+
923
+ Supports different types of embeddings from the model (e.g., CLS token, mean-pooled embeddings),
924
+ and provides 2D visualization using t-SNE, PCA, or UMAP. Also supports angular-based clustering
925
+ if the task is "ChannelCharting".
926
+
927
+ Args:
928
+ model (torch.nn.Module): The trained model for embedding extraction.
929
+ data (torch.Tensor): Input tensor of shape (N, ...). For non-'raw' types, it's tokenized input.
930
+ input_type (str): Type of embedding to extract. Options:
931
+ - 'cls_emb': Extract CLS token (first token) embeddings.
932
+ - 'channel_emb': Extract all non-CLS token embeddings.
933
+ - 'combined': Use the full output from the model.
934
+ - 'mean_pooled': Mean of all token embeddings.
935
+ - 'arbitrary_concat': Select specific token index (given by `selected_tokens`).
936
+ - 'arbitrary_meanPooled': Mean-pool over selected token indices.
937
+ - 'raw': Use the input `data` as is.
938
+ device (str or torch.device): The device for computation ('cuda' or 'cpu').
939
+ batch_size (int): Batch size for inference.
940
+ task (str, optional): If "ChannelCharting", performs position-based angular clustering.
941
+ visualization (bool): Whether to visualize the embeddings.
942
+ labels (torch.Tensor or np.ndarray, optional): Ground-truth labels or 2D positions used for coloring the plot.
943
+ visualization_method (str): One of {'tsne', 'pca', 'umap'} for 2D visualization.
944
+ selected_tokens (int, list, or tensor): Used for 'arbitrary_concat' or 'arbitrary_meanPooled' types.
945
+
946
+ Returns:
947
+ torch.Tensor: Output embedding tensor of shape (N, D) ready for downstream tasks or analysis.
948
+ """
949
+ print("\nPreparing for LWM inference and embedding space visualization ...")
950
+ if input_type == "raw":
951
+ output_total = data
952
+ else:
953
+ dataset = TensorDataset(data)
954
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
955
+
956
+ embeddings = []
957
+ model.eval()
958
+ with torch.no_grad():
959
+ with tqdm(dataloader, desc="Inference", unit="batch") as t:
960
+ for batch in t:
961
+
962
+ input_ids = batch[0].to(device)
963
+ output = model(input_ids)
964
+
965
+ if input_type == "cls_emb":
966
+ batch_embeddings = output[:, 0]
967
+ elif input_type == "channel_emb":
968
+ batch_embeddings = output[:, 1:]
969
+ elif input_type == "combined":
970
+ batch_embeddings = output
971
+ elif input_type == "mean_pooled":
972
+ batch_embeddings = torch.mean(output, dim=1).unsqueeze(1)
973
+ elif input_type == "arbitrary_concat":
974
+ batch_embeddings = output[:, selected_tokens]
975
+ elif input_type == "arbitrary_meanPooled":
976
+ batch_embeddings = torch.mean(output[:, selected_tokens], dim=1).unsqueeze(1)
977
+
978
+ embeddings.append(batch_embeddings)
979
+
980
+
981
+ output_total = torch.cat(embeddings, dim=0).float()
982
+
983
+ if visualization:
984
+
985
+ if task in ["ChannelCharting"]:
986
+
987
+ positions = labels.cpu().numpy()
988
+ x_coords = positions[:, 0]
989
+ y_coords = positions[:, 1]
990
+ center_x = np.mean(x_coords)
991
+ center_y = np.mean(y_coords)
992
+ x_shifted = x_coords - center_x
993
+ y_shifted = y_coords - center_y
994
+ angles = np.arctan2(y_shifted, x_shifted)
995
+ angles = (angles + 2 * np.pi) % (2 * np.pi)
996
+ n_clusters = 8
997
+ sector_size = (2 * np.pi) / n_clusters # Size of each sector in radians
998
+ labels = np.floor(angles / sector_size).astype(int)
999
+ labels = np.clip(labels, 0, n_clusters - 1)
1000
+
1001
+ plt.figure(figsize=(10, 8))
1002
+ if labels is not None:
1003
+ # Color-code by labels if provided
1004
+ num_classes = len(np.unique(labels))
1005
+ colors = plt.cm.get_cmap("tab10", num_classes)
1006
+
1007
+ for class_idx in range(num_classes):
1008
+ class_points = positions[labels == class_idx]
1009
+ plt.scatter(
1010
+ class_points[:, 0], class_points[:, 1],
1011
+ label=f"Class {class_idx}",
1012
+ alpha=0.6,
1013
+ cmap=colors
1014
+ )
1015
+ else:
1016
+ # Plot all points in a single color if no labels
1017
+ plt.scatter(
1018
+ positions[:, 0], positions[:, 1],
1019
+ color="blue", # Default color for unlabeled data
1020
+ alpha=0.6,
1021
+ label="Samples"
1022
+ )
1023
+ plt.title("Original 2D Positions with 5 Clusters")
1024
+ plt.xlabel("X Coordinate")
1025
+ plt.ylabel("Y Coordinate")
1026
+ plt.legend()
1027
+ plt.grid(True, linestyle="--", alpha=0.3)
1028
+ plt.show()
1029
+
1030
+ visualize_embeddings(output_total.view(output_total.size(0), -1),
1031
+ labels=labels,
1032
+ method=visualization_method,
1033
+ label="Embedding Space")
1034
+
1035
+ return output_total
1036
+
1037
+ def plot_radar_chart(task_names, optimized_scores, baseline_scores, title="Task Performance Comparison", figsize=(8, 8), save_path="submission/chart.png"):
1038
+ """
1039
+ Plot a dark-themed radar chart comparing optimized and baseline scores.
1040
+
1041
+ Args:
1042
+ task_names (list): List of task names (e.g., ["LoS/NLoS Classification", ...]).
1043
+ optimized_scores (list): List of optimized performance scores.
1044
+ baseline_scores (list): List of baseline performance scores.
1045
+ title (str): Title of the chart (default: "Task Performance Comparison").
1046
+ figsize (tuple): Figure size (width, height) in inches (default: (8, 8)).
1047
+ save_path (str): Path to save the figure (default: "submission/chart.png").
1048
+
1049
+ Raises:
1050
+ ValueError: If input lists have mismatched lengths or are empty.
1051
+ """
1052
+ # Input validation
1053
+ if not task_names or not optimized_scores or not baseline_scores:
1054
+ raise ValueError("All input lists (task_names, optimized_scores, baseline_scores) must not be empty")
1055
+ if not (len(task_names) == len(optimized_scores) == len(baseline_scores)):
1056
+ raise ValueError("All input lists must have the same length")
1057
+
1058
+ # Number of variables (tasks)
1059
+ num_tasks = len(task_names)
1060
+
1061
+ # Repeat the first score to close the radar chart
1062
+ angles = [n / float(num_tasks) * 2 * pi for n in range(num_tasks)]
1063
+ angles += angles[:1]
1064
+ optimized = optimized_scores + optimized_scores[:1]
1065
+ baseline = baseline_scores + baseline_scores[:1]
1066
+
1067
+ # Figure & Axes Setup
1068
+ plt.style.use('default') # Reset to default style to avoid global dark background
1069
+ fig, ax = plt.subplots(
1070
+ figsize=figsize, dpi=300,
1071
+ subplot_kw=dict(polar=True)
1072
+ )
1073
+ fig.patch.set_facecolor('#1a1a1a') # Dark gray background for the figure (outside the circle)
1074
+ ax.set_facecolor('#1a1a1a') # Dark background for the plot (inside the circle)
1075
+ ax.patch.set_alpha(1)
1076
+
1077
+ ax.set_theta_offset(pi / 2)
1078
+ ax.set_theta_direction(-1)
1079
+
1080
+ # Grid & Labels
1081
+ ax.set_xticks(angles[:-1])
1082
+ ax.set_xticklabels(task_names, fontsize=14, fontweight='bold', color='white')
1083
+ ax.set_rlabel_position(0)
1084
+ ax.set_yticks([0.2, 0.4, 0.6, 0.8, 1.0])
1085
+ ax.set_yticklabels(['20%', '40%', '60%', '80%', '100%'], fontsize=11, color='#bbbbbb')
1086
+ ax.set_ylim(0, 1.15)
1087
+ ax.grid(color='#bbbbbb', linestyle=':', linewidth=0.8)
1088
+
1089
+ # Glow Effect Function
1090
+ def plot_with_glow(x, y, color, label):
1091
+ for lw, alpha in zip([15, 10, 6, 3], [0.02, 0.04, 0.06, 0.08]):
1092
+ ax.plot(x, y, linewidth=lw, color=color, alpha=alpha, zorder=1)
1093
+ ax.plot(x, y, linewidth=2.5, color=color, label=label, zorder=2)
1094
+ ax.scatter(x, y, s=100, color=color, edgecolors='white', linewidth=1.5, zorder=3)
1095
+
1096
+ # Plot optimized & baseline
1097
+ plot_with_glow(angles, optimized, color='#00ffff', label='Optimized')
1098
+ plot_with_glow(angles, baseline, color='#ff5588', label='Baseline')
1099
+
1100
+ # Layered Fill
1101
+ ax.fill_between(angles, optimized, color='#00ffff', alpha=0.12, zorder=1)
1102
+ ax.fill_between(angles, baseline, color='#ff5588', alpha=0.12, zorder=1)
1103
+
1104
+ # Custom Legend
1105
+ legend_elements = [
1106
+ plt.Line2D([0], [0], color='#00ffff', lw=3, label='Optimized'),
1107
+ plt.Line2D([0], [0], color='#ff5588', lw=3, label='Baseline')
1108
+ ]
1109
+ ax.legend(
1110
+ handles=legend_elements,
1111
+ loc='upper right', bbox_to_anchor=(1.15, 1.15),
1112
+ frameon=True, facecolor='#bbbbbb', edgecolor='#555555', fontsize=18
1113
+ )
1114
+
1115
+ # Adjust layout
1116
+ plt.tight_layout()
1117
+ plt.savefig(save_path, dpi=300, bbox_inches='tight', transparent=False)
1118
+ plt.show()
1119
+