Release LWM Competition Package
Browse files- README.md +170 -0
- model_checkpoint.pth +3 -0
- pretrained_model.py +207 -0
- requirements.yml +183 -0
- task_1/config.json +15 -0
- task_1/test_data.pt +3 -0
- task_1/train_data.pt +3 -0
- task_1/val_data.pt +3 -0
- task_2/config.json +15 -0
- task_2/test_data.pt +3 -0
- task_2/train_data.pt +3 -0
- task_2/val_data.pt +3 -0
- task_3/config.json +15 -0
- task_3/train_data.pt +3 -0
- task_3/val_data.pt +3 -0
- task_4/config.json +15 -0
- task_4/test_data.pt +3 -0
- task_4/train_data.pt +3 -0
- task_4/val_data.pt +3 -0
- task_5/config.json +24 -0
- task_5/train_data.pt +3 -0
- task_5/val_data.pt +3 -0
- train_heads.py +777 -0
- train_heads_config.py +213 -0
- train_lwm.py +818 -0
- utils.py +1119 -0
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 |
+
[](https://huggingface.co/wi-lab/lwm-v1.1)
|
| 8 |
+
[](https://lwm-wireless.net/tutorials)
|
| 9 |
+
[](https://lwm-wireless.net/)
|
| 10 |
+
[](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 |
+
|
| 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 |
+
|
| 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 |
+
|
| 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 |
+
|
| 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 |
+
|