zhangrenchao commited on
Commit
4c4d99c
·
verified ·
1 Parent(s): 60c2561

Add engineering reproduction package

Browse files
.gitattributes CHANGED
@@ -1,35 +1,4 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
1
  *.ckpt filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  *.pt filter=lfs diff=lfs merge=lfs -text
3
+ *.npz filter=lfs diff=lfs merge=lfs -text
4
+ *.npy filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Multi-Temporal Earth Observation
9
+ - Remote Sensing Foundation Model
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong>
15
+ <span style="font-size: 30px;">Prithvi-EO</span>
16
+ </strong>
17
+ </p>
18
+
19
+ # Model Introduction
20
+
21
+ Prithvi-EO-2.0 is a foundation model for multi-temporal Earth observation data. It encodes HLS multispectral time series, image acquisition dates, and geographic locations into unified representations and reconstructs masked spatiotemporal patches through a Masked Autoencoder. It can be used for remote-sensing classification, semantic segmentation, regression, and environmental change monitoring.
22
+
23
+ Paper: Prithvi-EO-2.0: A Versatile Multi-Temporal Foundation Model for Earth Observation Applications
24
+ https://arxiv.org/abs/2412.02732
25
+
26
+ # Model Description
27
+
28
+ Prithvi-EO-2.0 was jointly proposed by IBM, NASA, and the Jülich Supercomputing Centre. The model was trained on 4.2 million global four-timestamp samples from NASA's Harmonized Landsat Sentinel-2 dataset. It is suitable for disaster response, land-cover and crop mapping, ecosystem dynamics monitoring, and remote-sensing image classification, segmentation, and regression tasks.
29
+
30
+ # Use Cases
31
+
32
+ | Use Case | Description |
33
+ | :---: | :--- |
34
+ | Multi-temporal representation | Uses a Transformer to jointly encode spatial and temporal information from four time steps. |
35
+ | Remote-sensing image reconstruction | Uses a 3D Masked Autoencoder to reconstruct masked multispectral spatiotemporal patches. |
36
+ | Spatiotemporal metadata modeling | Integrates year, day of year, latitude, and longitude, with support for randomly dropping metadata during training. |
37
+ | Local engineering validation | Uses a small number of synthetic HLS samples to verify the training, inference, evaluation, visualization, and checkpoint workflows. |
38
+ | Multi-GPU training | Launches distributed data-parallel training through `torchrun`. |
39
+
40
+ # Usage Instructions
41
+
42
+ ## 1. OneCode
43
+
44
+ Experience intelligent, one-click AI4S programming through the OneCode online environment:
45
+
46
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
47
+
48
+ ## 2. Download and Installation
49
+
50
+ ```bash
51
+ hf download OneScience-Group/PrithviEO --local-dir ./PrithviEO
52
+ cd PrithviEO
53
+ ```
54
+
55
+ ### Environment Dependencies
56
+
57
+ **Hardware Requirements**
58
+
59
+ - A GPU or DCU is recommended.
60
+ - A CPU can be used to verify connectivity with the default small-sample configuration; training an official-size model requires large-scale accelerated computing resources.
61
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
62
+
63
+ **DCU Environment**
64
+
65
+ ```bash
66
+ # Activate DTK and Conda first
67
+ conda create -n onescience311 python=3.11 -y
68
+ conda activate onescience311
69
+ # Installation with uv is supported
70
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
71
+ ```
72
+
73
+ **GPU Environment**
74
+
75
+ ```bash
76
+ # Activate Conda first
77
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
78
+ conda activate onescience311
79
+ # Installation with uv is supported
80
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
81
+ ```
82
+
83
+ ### Training Data
84
+
85
+ This repository uses a small number of synthetic samples to validate the engineering workflow. The training and test data are stored in `data/train.npz` and `data/test.npz`, respectively. The synthetic data preserve the four time steps and six HLS common bands used in the paper. The band order is Blue, Green, Red, Narrow NIR, SWIR1, and SWIR2, and normalization uses the officially published means and standard deviations.
86
+
87
+ The synthetic data preserve the `4×224×224` spatiotemporal dimensions used for training in the paper, and each sample's complete image tensor has dimensions `6×4×224×224`. Only the number of samples, model width, model depth, and number of training epochs are reduced. This setup validates the 3D Patch Embedding, spatiotemporal positional encoding, temporal and location metadata encoding, and MAE training workflow; it does not represent the data distribution or training scale of the official 4.2 million HLS samples.
88
+
89
+ ```bash
90
+ python scripts/fake_data.py
91
+ ```
92
+
93
+ ### Training
94
+
95
+ ```bash
96
+ python scripts/train.py
97
+ ```
98
+
99
+ For multi-GPU training, use:
100
+
101
+ ```bash
102
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
103
+ ```
104
+
105
+ The default configuration is intended for rapid workflow validation. Formal experiments should use real HLS time series, the official 300M or 600M configuration, and the complete training schedule.
106
+
107
+ ```text
108
+ result/checkpoints/prithvi_eo.pt
109
+ result/training/metrics.json
110
+ ```
111
+
112
+ ### Trained Weights
113
+
114
+ This repository does not include synthetic or official weights in `weight/`. IBM and NASA have published the Prithvi-EO-2.0 tiny, 100M, 300M, and 600M weights on Hugging Face, including the TL versions with temporal and location encodings:
115
+
116
+ https://huggingface.co/ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL
117
+
118
+ This repository is a reduced, independent engineering implementation. Its model parameter names and dimensions are not compatible with the official weights. To use the official weights, use TerraTorch or the implementation and data preprocessing workflow provided by the official repository.
119
+
120
+ ### Inference
121
+
122
+ ```bash
123
+ python scripts/inference.py
124
+ ```
125
+
126
+ Inference loads the training checkpoint, generates the CLS embedding, spatiotemporal patch embeddings, multi-temporal reconstructions of masked patches, and the mask, and saves them to:
127
+
128
+ ```text
129
+ result/output/predictions.npz
130
+ ```
131
+
132
+ ### Evaluation and Visualization
133
+
134
+ ```bash
135
+ python scripts/result.py
136
+ ```
137
+
138
+ The evaluation measures masked-patch MSE, full spatiotemporal reconstruction MAE, reconstruction error for each time step, and embedding norms. It also generates comparison plots of the input images, reconstructions, and absolute errors for all four time steps. Results on synthetic data only validate the engineering workflow and do not represent GEO-Bench performance or performance on real downstream tasks reported in the paper.
139
+
140
+ ```text
141
+ result/evaluation/metrics.json
142
+ result/evaluation/comparison.png
143
+ ```
144
+
145
+ # Official OneScience Information
146
+
147
+ | Platform | OneScience Main Repository | Skills Repository |
148
+ | --- | --- | --- |
149
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
150
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
151
+
152
+ # Citation and License
153
+
154
+ This repository is an independent engineering reproduction based on the publicly available specifications from the Prithvi-EO-2.0 paper.
155
+
156
+ Use of this repository's code, the official model weights, and the data remains subject to the licenses and terms of use of their respective projects.
README_zh.md ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: Apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 多时相地球观测
10
+ - 遥感基础模型
11
+ frameworks: PyTorch
12
+ datasets:
13
+ - Harmonized Landsat Sentinel-2
14
+ ---
15
+
16
+ <p align="center">
17
+ <strong>
18
+ <span style="font-size: 30px;">Prithvi-EO</span>
19
+ </strong>
20
+ </p>
21
+
22
+ # 模型介绍
23
+
24
+ Prithvi-EO-2.0 是面向多时相地球观测数据的基础模型,将 HLS 多光谱时间序列、影像获取日期和地理位置编码为统一表征,并通过 Masked Autoencoder 重建被遮挡的时空 Patch,可用于遥感分类、语义分割、回归和环境变化监测。
25
+
26
+ 论文:Prithvi-EO-2.0: A Versatile Multi-Temporal Foundation Model for Earth Observation Applications
27
+ https://arxiv.org/abs/2412.02732
28
+
29
+ # 模型描述
30
+
31
+ Prithvi-EO-2.0 由 IBM、NASA 和 Jülich Supercomputing Centre 联合提出。模型使用 NASA Harmonized Landsat Sentinel-2 数据集中 420 万个全球四时相样本训练。模型适用于灾害响应、土地覆盖与作物制图、生态系统动态监测,以及遥感影像分类、分割和回归任务。
32
+
33
+ # 适用场景
34
+
35
+ | 场景 | 说明 |
36
+ | :---: | :--- |
37
+ | 多时相表征 | 使用 Transformer 同时编码四个时间步的空间和时间信息。 |
38
+ | 遥感影像重建 | 使用 3D Masked Autoencoder 重建被遮挡的多光谱时空 Patch。 |
39
+ | 时空元数据建模 | 融合年份、年积日、纬度和经度,并支持训练时随机丢弃元数据。 |
40
+ | 本地工程验证 | 使用少量虚拟 HLS 样本检查训练、推理、评估、可视化和 checkpoint 流程。 |
41
+ | 多卡训练 | 通过 `torchrun` 启动分布式数据并行训练。 |
42
+
43
+ # 使用说明
44
+
45
+ ## 1.OneCode
46
+
47
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
48
+
49
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
50
+
51
+ ## 2.下载安装
52
+
53
+ ```bash
54
+ modelscope download --model OneScience/Prithvi-EO --local_dir ./PrithviEO
55
+ cd PrithviEO
56
+ ```
57
+
58
+ ### 环境依赖
59
+
60
+ **硬件要求**
61
+
62
+ - 推荐使用 GPU 或 DCU 运行。
63
+ - CPU 可用于默认小样本配置的连通性验证,官方尺寸模型训练需要大规模加速资源。
64
+ - DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
65
+
66
+ **DCU环境**
67
+
68
+ ```bash
69
+ # 请首先激活DTK及CONDA
70
+ conda create -n onescience311 python=3.11 -y
71
+ conda activate onescience311
72
+ # 支持uv安装
73
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
74
+ ```
75
+
76
+ **GPU环境**
77
+
78
+ ```bash
79
+ # 请首先激活CONDA
80
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
81
+ conda activate onescience311
82
+ # 支持uv安装
83
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
84
+ ```
85
+
86
+ ### 训练数据介绍
87
+
88
+ 本仓库使用少量虚拟样本验证工程流程,训练数据和测试数据分别保存为 `data/train.npz` 和 `data/test.npz`。虚拟数据保持论文的四时间步与六个 HLS 公共波段,波段顺序为 Blue、Green、Red、Narrow NIR、SWIR1 和 SWIR2,并使用官方公开的均值与标准差完成归一化。
89
+
90
+ 虚拟数据保持论文训练使用的 4×224×224 时空尺寸,每个样本的完整影像张量为 6×4×224×224。当前仅缩小样本数量、模型宽度、模型深度和训练周期,用于验证 3D Patch Embedding、时空位置编码、时间与位置元数据编码和 MAE 训练流程,不代表官方 420 万个 HLS 样本的数据分布与训练规模。
91
+
92
+ ```bash
93
+ python scripts/fake_data.py
94
+ ```
95
+
96
+ ### 训练
97
+
98
+ ```bash
99
+ python scripts/train.py
100
+ ```
101
+
102
+ 多卡训练可使用:
103
+
104
+ ```bash
105
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
106
+ ```
107
+
108
+ 默认配置面向快速流程验证;开展正式实验时,应使用真实 HLS 时间序列、官方 300M 或 600M 配置和完整训练周期。
109
+
110
+ ```text
111
+ result/checkpoints/prithvi_eo.pt
112
+ result/training/metrics.json
113
+ ```
114
+
115
+ ### 训练权重
116
+
117
+ 本仓库不在 `weight/` 中内置虚拟权重或官方权重。IBM 和 NASA 已在 Hugging Face 公开 Prithvi-EO-2.0 的 tiny、100M、300M 和 600M 权重,包括带时间与位置编码的 TL 版本:
118
+
119
+ https://huggingface.co/ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL
120
+
121
+ 本仓库是缩小的独立工程实现,模型参数名称和尺寸不与官方权重兼容。需要使用官方权重时,应采用 TerraTorch 或官方仓库提供的实现和数据预处理流程。
122
+
123
+ ### 推理
124
+
125
+ ```bash
126
+ python scripts/inference.py
127
+ ```
128
+
129
+ 推理加载训练 checkpoint,生成 CLS embedding、时空 Patch embedding、被遮挡 Patch 的多时相重建结果和 Mask,并保存到:
130
+
131
+ ```text
132
+ result/output/predictions.npz
133
+ ```
134
+
135
+ ### 评估和可视化
136
+
137
+ ```bash
138
+ python scripts/result.py
139
+ ```
140
+
141
+ 评估模型的 masked-patch MSE、完整时空重建 MAE、各时间步重建误差和 embedding 范数。评估过程同时生成四个时间步的输入影像、重建结果和绝对误差对比图。虚拟数据结果仅用于验证工程流程,不代表论文中的 GEO-Bench 或真实下游任务性能。
142
+
143
+ ```text
144
+ result/evaluation/metrics.json
145
+ result/evaluation/comparison.png
146
+ ```
147
+
148
+ # OneScience官方信息
149
+
150
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
151
+ | --- | --- | --- |
152
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
153
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
154
+
155
+ # 引用与许可证
156
+
157
+ 本仓库为 Prithvi-EO-2.0 论文公开规格的独立工程复现版本。
158
+
159
+ 本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+ data:
3
+ root: data
4
+ format_version: prithvi_eo_2_synthetic_engineering_v1
5
+ train_samples: 4
6
+ test_samples: 2
7
+ channels: 6
8
+ frames: 4
9
+ image_size: 224
10
+ band_names: [Blue, Green, Red, Narrow_NIR, SWIR1, SWIR2]
11
+ band_codes: [B02, B03, B04, B8A, B11, B12]
12
+ mean: [1087.0, 1342.0, 1433.0, 2734.0, 1958.0, 1363.0]
13
+ std: [2248.0, 2179.0, 2178.0, 1850.0, 1242.0, 1049.0]
14
+ model:
15
+ input_size: [4, 224, 224]
16
+ patch_size: [1, 16, 16]
17
+ channels: 6
18
+ mask_ratio: 0.75
19
+ encoder_dim: 96
20
+ encoder_depth: 2
21
+ encoder_heads: 4
22
+ decoder_dim: 64
23
+ decoder_depth: 1
24
+ decoder_heads: 4
25
+ mlp_ratio: 4
26
+ metadata_dropout: 0.1
27
+ norm_pix_loss: false
28
+ paper_model_300M_TL:
29
+ input_size: [4, 224, 224]
30
+ patch_size: [1, 16, 16]
31
+ encoder_dim: 1024
32
+ encoder_depth: 24
33
+ encoder_heads: 16
34
+ decoder_dim: 512
35
+ decoder_depth: 8
36
+ decoder_heads: 16
37
+ parameter_count: 300M
38
+ paper_model_600M_TL:
39
+ input_size: [4, 224, 224]
40
+ patch_size: [1, 14, 14]
41
+ encoder_dim: 1280
42
+ encoder_depth: 32
43
+ encoder_heads: 16
44
+ decoder_dim: 512
45
+ decoder_depth: 8
46
+ decoder_heads: 16
47
+ parameter_count: 600M
48
+ paper_training:
49
+ samples: 4.2M
50
+ validation_samples: 46k
51
+ epochs: 400
52
+ global_batch_size: 3840
53
+ maximum_learning_rate: 0.0005
54
+ warmup_epochs: 40
55
+ weight_decay: 0.05
56
+ train:
57
+ epochs: 1
58
+ batch_size: 2
59
+ learning_rate: 0.0005
60
+ weight_decay: 0.000001
61
+ num_workers: 0
62
+ runtime:
63
+ device: auto
64
+ paths:
65
+ checkpoint: result/checkpoints/prithvi_eo.pt
66
+ training_metrics: result/training/metrics.json
67
+ inference_dir: result/output
68
+ evaluation_dir: result/evaluation
config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "PrithviEO",
3
+ "model_type": "prithvi_eo",
4
+ "architectures": ["PrithviEO2", "CoordinateEncoder", "Transformer"],
5
+ "framework": "PyTorch",
6
+ "domain": "earth-observation",
7
+ "task": "multi-temporal-remote-sensing-representation-learning",
8
+ "implementation": {
9
+ "entry_point": "model/prithvi_eo.py",
10
+ "scope": "engineering reproduction of the Prithvi-EO-2.0 temporal-location masked autoencoder",
11
+ "train_script": "scripts/train.py",
12
+ "inference_script": "scripts/inference.py",
13
+ "evaluation_script": "scripts/result.py",
14
+ "synthetic_data_script": "scripts/fake_data.py"
15
+ },
16
+ "architecture": {
17
+ "family": "three-dimensional vision-transformer masked autoencoder",
18
+ "input_size": [4, 224, 224],
19
+ "patch_size": [1, 16, 16],
20
+ "in_channels": 6,
21
+ "mask_ratio": 0.75,
22
+ "encoder_dim": 96,
23
+ "encoder_depth": 2,
24
+ "encoder_heads": 4,
25
+ "decoder_dim": 64,
26
+ "decoder_depth": 1,
27
+ "decoder_heads": 4,
28
+ "metadata": ["year", "day_of_year", "latitude", "longitude"],
29
+ "outputs": ["loss", "embedding", "patch_embeddings", "reconstruction", "mask"]
30
+ },
31
+ "data": {
32
+ "datasets": ["Harmonized Landsat Sentinel-2"],
33
+ "protocol": "prithvi_eo_2_synthetic_engineering_v1",
34
+ "format": "NPZ",
35
+ "train_file": "data/train.npz",
36
+ "test_file": "data/test.npz",
37
+ "input_shape": ["N", 6, 4, 224, 224],
38
+ "temporal_shape": ["N", 4, 2],
39
+ "location_shape": ["N", 2],
40
+ "bands": ["B02", "B03", "B04", "B8A", "B11", "B12"],
41
+ "required_metadata": ["format_version", "data_source", "temporal_coords", "location_coords"]
42
+ },
43
+ "configuration_sources": ["conf/config.yaml", "model/prithvi_eo.py", "scripts/fake_data.py", "scripts/train.py", "scripts/inference.py", "scripts/result.py"]
44
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "multi_temporal_earth_observation_representation_learning",
4
+ "model": "Prithvi-EO-2.0",
5
+ "input_format": "BCTHW",
6
+ "protocol": "engineering_3d_mae_training_and_inference",
7
+ "default_config": "conf/config.yaml",
8
+ "training": "scripts/train.py",
9
+ "inference": "scripts/inference.py",
10
+ "evaluation": "scripts/result.py",
11
+ "visualization": "scripts/result.py"
12
+ }
model/prithvi_eo.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pure-PyTorch engineering reproduction of the Prithvi-EO-2.0 TL MAE."""
2
+
3
+ import math
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+
9
+ def sincos_1d(positions, dim):
10
+ if dim % 2:
11
+ raise ValueError("sine/cosine dimensions must be even")
12
+ frequencies = torch.exp(
13
+ torch.arange(dim // 2, device=positions.device, dtype=positions.dtype)
14
+ * (-math.log(10000.0) / max(dim // 2, 1))
15
+ )
16
+ angles = positions.unsqueeze(-1) * frequencies
17
+ return torch.cat((angles.sin(), angles.cos()), dim=-1)
18
+
19
+
20
+ def sincos_3d(frames, height, width, dim, device, dtype):
21
+ if dim % 16:
22
+ raise ValueError("3D position dimension must be divisible by 16")
23
+ width_dim, height_dim, time_dim = 6 * dim // 16, 6 * dim // 16, 4 * dim // 16
24
+ time, row, column = torch.meshgrid(
25
+ torch.arange(frames, device=device, dtype=dtype),
26
+ torch.arange(height, device=device, dtype=dtype),
27
+ torch.arange(width, device=device, dtype=dtype),
28
+ indexing="ij",
29
+ )
30
+ return torch.cat((
31
+ sincos_1d(column.reshape(-1), width_dim),
32
+ sincos_1d(row.reshape(-1), height_dim),
33
+ sincos_1d(time.reshape(-1), time_dim),
34
+ ), dim=-1)
35
+
36
+
37
+ def patchify(values, patch_size):
38
+ batch, channels, frames, height, width = values.shape
39
+ pt, ph, pw = patch_size
40
+ if frames % pt or height % ph or width % pw:
41
+ raise ValueError("input dimensions must be divisible by patch_size")
42
+ return values.reshape(
43
+ batch, channels, frames // pt, pt, height // ph, ph, width // pw, pw
44
+ ).permute(0, 2, 4, 6, 3, 5, 7, 1).reshape(batch, -1, pt * ph * pw * channels)
45
+
46
+
47
+ def unpatchify(patches, channels, output_size, patch_size):
48
+ batch = patches.shape[0]
49
+ frames, height, width = output_size
50
+ pt, ph, pw = patch_size
51
+ return patches.reshape(
52
+ batch, frames // pt, height // ph, width // pw, pt, ph, pw, channels
53
+ ).permute(0, 7, 1, 4, 2, 5, 3, 6).reshape(batch, channels, frames, height, width)
54
+
55
+
56
+ class Transformer(nn.Module):
57
+ def __init__(self, dim, depth, heads, mlp_ratio):
58
+ super().__init__()
59
+ layer = nn.TransformerEncoderLayer(
60
+ dim, heads, int(dim * mlp_ratio), activation="gelu", batch_first=True, norm_first=True
61
+ )
62
+ self.blocks = nn.TransformerEncoder(layer, depth)
63
+ self.norm = nn.LayerNorm(dim)
64
+
65
+ def forward(self, values):
66
+ return self.norm(self.blocks(values))
67
+
68
+
69
+ class CoordinateEncoder(nn.Module):
70
+ def __init__(self, dim, scale=0.1):
71
+ super().__init__()
72
+ if dim % 4:
73
+ raise ValueError("coordinate embedding dimension must be divisible by four")
74
+ self.dim = dim
75
+ self.scale = nn.Parameter(torch.tensor(float(scale)))
76
+
77
+ def forward(self, coordinates):
78
+ return self.scale * torch.cat((
79
+ sincos_1d(coordinates[..., 0], self.dim // 2),
80
+ sincos_1d(coordinates[..., 1], self.dim // 2),
81
+ ), dim=-1)
82
+
83
+
84
+ class PrithviEO2(nn.Module):
85
+ def __init__(self, config):
86
+ super().__init__()
87
+ self.config = dict(config)
88
+ self.input_size = tuple(int(value) for value in config["input_size"])
89
+ self.patch_size = tuple(int(value) for value in config["patch_size"])
90
+ self.channels = int(config["channels"])
91
+ self.mask_ratio = float(config["mask_ratio"])
92
+ self.metadata_dropout = float(config["metadata_dropout"])
93
+ self.norm_pix_loss = bool(config.get("norm_pix_loss", False))
94
+ enc_dim, dec_dim = int(config["encoder_dim"]), int(config["decoder_dim"])
95
+ self.patch_embed = nn.Conv3d(
96
+ self.channels, enc_dim, kernel_size=self.patch_size, stride=self.patch_size
97
+ )
98
+ self.cls_token = nn.Parameter(torch.randn(1, 1, enc_dim) * 0.02)
99
+ self.encoder = Transformer(enc_dim, int(config["encoder_depth"]), int(config["encoder_heads"]),
100
+ float(config["mlp_ratio"]))
101
+ self.encoder_to_decoder = nn.Linear(enc_dim, dec_dim)
102
+ self.mask_token = nn.Parameter(torch.randn(1, 1, dec_dim) * 0.02)
103
+ self.decoder = Transformer(dec_dim, int(config["decoder_depth"]), int(config["decoder_heads"]),
104
+ float(config["mlp_ratio"]))
105
+ patch_volume = math.prod(self.patch_size) * self.channels
106
+ self.decoder_prediction = nn.Linear(dec_dim, patch_volume)
107
+ self.time_encoder = CoordinateEncoder(enc_dim)
108
+ self.location_encoder = CoordinateEncoder(enc_dim)
109
+ self.decoder_time_encoder = CoordinateEncoder(dec_dim)
110
+ self.decoder_location_encoder = CoordinateEncoder(dec_dim)
111
+
112
+ def _grid(self, pixels):
113
+ return tuple(size // patch for size, patch in zip(pixels.shape[-3:], self.patch_size))
114
+
115
+ def _metadata(self, temporal, location, grid, encoder=True):
116
+ frames, height, width = grid
117
+ time_encoder = self.time_encoder if encoder else self.decoder_time_encoder
118
+ location_encoder = self.location_encoder if encoder else self.decoder_location_encoder
119
+ temporal_embedding = time_encoder(temporal)
120
+ temporal_embedding = temporal_embedding[:, :, None, :].expand(-1, -1, height * width, -1).reshape(
121
+ len(temporal), frames * height * width, -1
122
+ )
123
+ location_embedding = location_encoder(location)[:, None, :].expand(-1, frames * height * width, -1)
124
+ if self.training and self.metadata_dropout:
125
+ time_keep = (torch.rand(len(temporal), 1, 1, device=temporal.device) >= self.metadata_dropout).to(temporal.dtype)
126
+ location_keep = (torch.rand(len(location), 1, 1, device=location.device) >= self.metadata_dropout).to(location.dtype)
127
+ temporal_embedding = temporal_embedding * time_keep
128
+ location_embedding = location_embedding * location_keep
129
+ return temporal_embedding + location_embedding
130
+
131
+ def _encoded_tokens(self, pixels, temporal, location):
132
+ grid = self._grid(pixels)
133
+ tokens = self.patch_embed(pixels).flatten(2).transpose(1, 2)
134
+ position = sincos_3d(*grid, tokens.shape[-1], tokens.device, tokens.dtype)
135
+ tokens = tokens + position[None] + self._metadata(temporal, location, grid, encoder=True)
136
+ return tokens, grid
137
+
138
+ def encode(self, pixels, temporal, location):
139
+ tokens, _ = self._encoded_tokens(pixels, temporal, location)
140
+ cls = self.cls_token.expand(len(pixels), -1, -1)
141
+ encoded = self.encoder(torch.cat((cls, tokens), dim=1))
142
+ return encoded[:, 0], encoded[:, 1:]
143
+
144
+ def forward(self, pixels, temporal, location, mask_ratio=None):
145
+ ratio = self.mask_ratio if mask_ratio is None else float(mask_ratio)
146
+ tokens, grid = self._encoded_tokens(pixels, temporal, location)
147
+ batch, length, dim = tokens.shape
148
+ keep = max(1, int(length * (1.0 - ratio)))
149
+ ordering = torch.rand(batch, length, device=pixels.device).argsort(dim=1)
150
+ visible_indices, masked_indices = ordering[:, :keep], ordering[:, keep:]
151
+ visible = tokens.gather(1, visible_indices[:, :, None].expand(-1, -1, dim))
152
+ encoded = self.encoder(torch.cat((self.cls_token.expand(batch, -1, -1), visible), dim=1))
153
+ embedding = encoded[:, 0]
154
+ visible_decoder = self.encoder_to_decoder(encoded[:, 1:])
155
+ decoder_tokens = self.mask_token.expand(batch, length, -1).clone()
156
+ decoder_tokens.scatter_(1, visible_indices[:, :, None].expand(-1, -1, visible_decoder.shape[-1]), visible_decoder)
157
+ position = sincos_3d(*grid, decoder_tokens.shape[-1], decoder_tokens.device, decoder_tokens.dtype)
158
+ decoder_tokens = decoder_tokens + position[None] + self._metadata(temporal, location, grid, encoder=False)
159
+ predictions = self.decoder_prediction(self.decoder(decoder_tokens))
160
+ targets = patchify(pixels, self.patch_size)
161
+ if self.norm_pix_loss:
162
+ mean, variance = targets.mean(dim=-1, keepdim=True), targets.var(dim=-1, keepdim=True)
163
+ targets = (targets - mean) / (variance + 1e-6).sqrt()
164
+ mask = torch.zeros(batch, length, device=pixels.device)
165
+ mask.scatter_(1, masked_indices, 1.0)
166
+ patch_mse = (predictions - targets).pow(2).mean(dim=-1)
167
+ loss = (patch_mse * mask).sum() / mask.sum().clamp_min(1)
168
+ reconstruction = unpatchify(predictions, self.channels, pixels.shape[-3:], self.patch_size)
169
+ return {
170
+ "loss": loss,
171
+ "embedding": embedding,
172
+ "patch_embeddings": encoded[:, 1:],
173
+ "reconstruction": reconstruction,
174
+ "mask": mask,
175
+ }
scripts/fake_data.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate deterministic HLS-like four-timestamp samples for engineering validation."""
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import yaml
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+
11
+
12
+ def make_split(path, count, config, seed):
13
+ rng = np.random.default_rng(seed)
14
+ data = config["data"]
15
+ channels, frames, size = int(data["channels"]), int(data["frames"]), int(data["image_size"])
16
+ means = np.asarray(data["mean"], np.float32)
17
+ stds = np.asarray(data["std"], np.float32)
18
+ y, x = np.mgrid[-1:1:complex(size), -1:1:complex(size)].astype(np.float32)
19
+ pixels = np.empty((count, channels, frames, size, size), np.float32)
20
+ temporal = np.empty((count, frames, 2), np.float32)
21
+ location = np.empty((count, 2), np.float32)
22
+ class_target = np.empty(count, np.int64)
23
+ regression_target = np.empty(count, np.float32)
24
+ for sample in range(count):
25
+ latitude, longitude = rng.uniform(-70, 70), rng.uniform(-180, 180)
26
+ start_day = int(rng.integers(1, 80))
27
+ days = np.clip(start_day + np.arange(frames) * int(rng.integers(45, 100)), 1, 365)
28
+ temporal[sample, :, 0] = 2018 + sample % 5
29
+ temporal[sample, :, 1] = days
30
+ location[sample] = (latitude, longitude)
31
+ phase = rng.uniform(0, 2 * np.pi)
32
+ class_target[sample] = int(np.sin(phase) > 0)
33
+ regression_target[sample] = np.cos(phase) + latitude / 180
34
+ for step, day in enumerate(days):
35
+ seasonal = np.sin(2 * np.pi * day / 365 + phase)
36
+ landscape = np.sin(2.5 * np.pi * x + phase) * np.cos(2 * np.pi * y - phase)
37
+ landscape += 0.35 * x + 0.2 * y + 0.25 * seasonal
38
+ for channel in range(channels):
39
+ normalized = landscape + 0.12 * channel + rng.normal(0, 0.04, (size, size))
40
+ pixels[sample, channel, step] = normalized * stds[channel] + means[channel]
41
+ payload = {
42
+ "format_version": np.asarray(data["format_version"]),
43
+ "data_source": np.asarray("synthetic_hls_like"),
44
+ "pixels": pixels,
45
+ "temporal_coords": temporal,
46
+ "location_coords": location,
47
+ "class_target": class_target,
48
+ "regression_target": regression_target,
49
+ }
50
+ np.savez_compressed(path, **payload)
51
+
52
+
53
+ def main():
54
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
55
+ output = ROOT / config["data"]["root"]
56
+ output.mkdir(parents=True, exist_ok=True)
57
+ for offset, (filename, count) in enumerate((
58
+ ("train.npz", int(config["data"]["train_samples"])),
59
+ ("test.npz", int(config["data"]["test_samples"])),
60
+ )):
61
+ path = output / filename
62
+ if not path.exists():
63
+ make_split(path, count, config, int(config["seed"]) + offset)
64
+ print(f"generated={path.relative_to(ROOT)} samples={count} format={config['data']['format_version']}")
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run multi-temporal reconstruction and embedding inference."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+ import yaml
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(ROOT))
13
+ from model.prithvi_eo import PrithviEO2
14
+ from train import PrithviDataset, device_from_config
15
+
16
+
17
+ def main():
18
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
19
+ device = device_from_config(config)
20
+ checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=True)
21
+ if checkpoint["format_version"] != config["data"]["format_version"]:
22
+ raise ValueError("checkpoint and data formats are incompatible")
23
+ model = PrithviEO2(checkpoint["model_config"]).to(device)
24
+ model.load_state_dict(checkpoint["model"])
25
+ model.eval()
26
+ dataset = PrithviDataset(ROOT / config["data"]["root"] / "test.npz", config)
27
+ pixels = torch.stack([dataset[index]["pixels"] for index in range(len(dataset))]).to(device)
28
+ temporal = torch.stack([dataset[index]["temporal"] for index in range(len(dataset))]).to(device)
29
+ location = torch.stack([dataset[index]["location"] for index in range(len(dataset))]).to(device)
30
+ torch.manual_seed(int(config["seed"]))
31
+ with torch.no_grad():
32
+ output = model(pixels, temporal, location)
33
+ cls_embedding, patch_embeddings = model.encode(pixels, temporal, location)
34
+ target = ROOT / config["paths"]["inference_dir"] / "predictions.npz"
35
+ target.parent.mkdir(parents=True, exist_ok=True)
36
+ np.savez_compressed(
37
+ target,
38
+ format_version=np.asarray(config["data"]["format_version"]),
39
+ pixels=pixels.cpu().numpy(),
40
+ reconstruction=output["reconstruction"].cpu().numpy(),
41
+ mask=output["mask"].cpu().numpy(),
42
+ embedding=cls_embedding.cpu().numpy(),
43
+ patch_embeddings=patch_embeddings.cpu().numpy(),
44
+ temporal_coords=temporal.cpu().numpy(),
45
+ location_coords=location.cpu().numpy(),
46
+ class_target=dataset.data["class_target"],
47
+ regression_target=dataset.data["regression_target"],
48
+ masked_patch_mse=np.asarray(float(output["loss"])),
49
+ )
50
+ print(f"predictions={target.relative_to(ROOT)}")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
scripts/result.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate Prithvi reconstruction and visualize temporal HLS samples."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import matplotlib
7
+ matplotlib.use("Agg")
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import yaml
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+
15
+
16
+ def main():
17
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
18
+ prediction = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
19
+ pixels, reconstruction = prediction["pixels"], prediction["reconstruction"]
20
+ error = np.abs(reconstruction - pixels)
21
+ per_frame = error.mean(axis=(0, 1, 3, 4))
22
+ embeddings = prediction["embedding"]
23
+ metrics = {
24
+ "samples": int(len(pixels)),
25
+ "masked_patch_mse": float(prediction["masked_patch_mse"]),
26
+ "reconstruction_mae": float(error.mean()),
27
+ "per_frame_reconstruction_mae": [float(value) for value in per_frame],
28
+ "mean_embedding_norm": float(np.linalg.norm(embeddings, axis=1).mean()),
29
+ }
30
+ output = ROOT / config["paths"]["evaluation_dir"]
31
+ output.mkdir(parents=True, exist_ok=True)
32
+ (output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
33
+ figure, axes = plt.subplots(3, int(config["data"]["frames"]), figsize=(12, 8))
34
+ for frame in range(int(config["data"]["frames"])):
35
+ source = pixels[0, [2, 1, 0], frame].transpose(1, 2, 0)
36
+ rebuilt = reconstruction[0, [2, 1, 0], frame].transpose(1, 2, 0)
37
+ low, high = np.percentile(source, (2, 98))
38
+ source = np.clip((source - low) / max(high - low, 1e-6), 0, 1)
39
+ rebuilt = np.clip((rebuilt - low) / max(high - low, 1e-6), 0, 1)
40
+ axes[0, frame].imshow(source)
41
+ axes[1, frame].imshow(rebuilt)
42
+ axes[2, frame].imshow(error[0, :, frame].mean(axis=0), cmap="magma")
43
+ axes[0, frame].set_title(f"time {frame + 1}")
44
+ for axis in axes[:, frame]:
45
+ axis.axis("off")
46
+ axes[0, 0].set_ylabel("input")
47
+ axes[1, 0].set_ylabel("reconstruction")
48
+ axes[2, 0].set_ylabel("absolute error")
49
+ figure.tight_layout()
50
+ figure.savefig(output / "comparison.png", dpi=150)
51
+ plt.close(figure)
52
+ print(f"metrics={output.relative_to(ROOT) / 'metrics.json'}")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
scripts/train.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train the reduced Prithvi-EO-2.0 temporal-location MAE."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ import torch
10
+ import yaml
11
+ from torch.nn.parallel import DistributedDataParallel
12
+ from torch.utils.data import DataLoader, Dataset, DistributedSampler
13
+
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(ROOT))
17
+ from model.prithvi_eo import PrithviEO2
18
+
19
+
20
+ class PrithviDataset(Dataset):
21
+ def __init__(self, path, config):
22
+ self.data = np.load(path)
23
+ self.config = config
24
+ if str(self.data["format_version"]) != config["data"]["format_version"]:
25
+ raise ValueError("incompatible data format")
26
+ expected = (
27
+ int(config["data"]["channels"]), int(config["data"]["frames"]),
28
+ int(config["data"]["image_size"]), int(config["data"]["image_size"]),
29
+ )
30
+ if self.data["pixels"].shape[1:] != expected:
31
+ raise ValueError(f"pixels have shape {self.data['pixels'].shape[1:]}, expected {expected}")
32
+ self.mean = torch.tensor(config["data"]["mean"], dtype=torch.float32)[:, None, None, None]
33
+ self.std = torch.tensor(config["data"]["std"], dtype=torch.float32)[:, None, None, None]
34
+
35
+ def __len__(self):
36
+ return len(self.data["pixels"])
37
+
38
+ def __getitem__(self, index):
39
+ pixels = torch.from_numpy(self.data["pixels"][index]).float()
40
+ return {
41
+ "pixels": (pixels - self.mean) / self.std,
42
+ "temporal": torch.from_numpy(self.data["temporal_coords"][index]).float(),
43
+ "location": torch.from_numpy(self.data["location_coords"][index]).float(),
44
+ }
45
+
46
+
47
+ def device_from_config(config, local_rank=0):
48
+ requested = config["runtime"]["device"]
49
+ if requested == "auto":
50
+ return torch.device("cuda", local_rank) if torch.cuda.is_available() else torch.device("cpu")
51
+ return torch.device(requested)
52
+
53
+
54
+ def main():
55
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
56
+ torch.manual_seed(int(config["seed"]))
57
+ distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
58
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
59
+ if distributed:
60
+ torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
61
+ rank = torch.distributed.get_rank() if distributed else 0
62
+ device = device_from_config(config, local_rank)
63
+ if device.type == "cuda":
64
+ torch.cuda.set_device(device)
65
+ dataset = PrithviDataset(ROOT / config["data"]["root"] / "train.npz", config)
66
+ sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
67
+ loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), sampler=sampler,
68
+ shuffle=sampler is None, num_workers=int(config["train"]["num_workers"]))
69
+ model = PrithviEO2(config["model"]).to(device)
70
+ if distributed:
71
+ model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
72
+ optimizer = torch.optim.AdamW(model.parameters(), lr=float(config["train"]["learning_rate"]),
73
+ weight_decay=float(config["train"]["weight_decay"]), betas=(0.9, 0.95))
74
+ history = []
75
+ for epoch in range(int(config["train"]["epochs"])):
76
+ if sampler:
77
+ sampler.set_epoch(epoch)
78
+ model.train()
79
+ total, steps = 0.0, 0
80
+ for batch in loader:
81
+ output = model(batch["pixels"].to(device), batch["temporal"].to(device), batch["location"].to(device))
82
+ optimizer.zero_grad(set_to_none=True)
83
+ output["loss"].backward()
84
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
85
+ optimizer.step()
86
+ total += float(output["loss"].detach())
87
+ steps += 1
88
+ metrics = {"epoch": epoch + 1, "masked_patch_mse": total / max(steps, 1)}
89
+ history.append(metrics)
90
+ if rank == 0:
91
+ print(f"epoch={epoch + 1} masked_patch_mse={metrics['masked_patch_mse']:.6f}")
92
+ if rank == 0:
93
+ checkpoint = ROOT / config["paths"]["checkpoint"]
94
+ metrics_path = ROOT / config["paths"]["training_metrics"]
95
+ checkpoint.parent.mkdir(parents=True, exist_ok=True)
96
+ metrics_path.parent.mkdir(parents=True, exist_ok=True)
97
+ state = model.module.state_dict() if distributed else model.state_dict()
98
+ torch.save({"model": state, "model_config": config["model"],
99
+ "format_version": config["data"]["format_version"]}, checkpoint)
100
+ metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n")
101
+ print(f"checkpoint={checkpoint.relative_to(ROOT)}")
102
+ if distributed:
103
+ torch.distributed.destroy_process_group()
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
weight/.gitkeep ADDED
File without changes