zhangrenchao commited on
Commit
3549cf5
·
verified ·
1 Parent(s): 60c2561

Add engineering reproduction package

Browse files
.gitattributes CHANGED
@@ -1,35 +1,13 @@
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
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
2
  *.h5 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
3
  *.onnx filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
4
  *.pt filter=lfs diff=lfs merge=lfs -text
5
  *.pth filter=lfs diff=lfs merge=lfs -text
 
6
  *.safetensors filter=lfs diff=lfs merge=lfs -text
7
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
8
+ *.npy filter=lfs diff=lfs merge=lfs -text
9
+ *.npz filter=lfs diff=lfs merge=lfs -text
10
+ *.pickle filter=lfs diff=lfs merge=lfs -text
11
+ *.pkl filter=lfs diff=lfs merge=lfs -text
12
  *.tar filter=lfs diff=lfs merge=lfs -text
 
 
 
 
13
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
README.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-4.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Multi-Source Earth Observation
9
+ - Geospatial Embeddings
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong>
15
+ <span style="font-size: 30px;">AlphaEarthFoundations</span>
16
+ </strong>
17
+ </p>
18
+
19
+ # Model Introduction
20
+
21
+ AlphaEarthFoundations is a multi-source spatiotemporal embedding field model for global Earth observation mapping. It jointly encodes sparse and asynchronous data, including optical imagery, radar, LiDAR, climate, elevation, land cover, and geographic text, into 64-dimensional unit-sphere embeddings that support classification, regression, and change detection with limited labels.
22
+
23
+ Paper: AlphaEarth Foundations: An embedding field model for accurate and efficient global mapping from sparse label data
24
+ https://arxiv.org/abs/2507.22291
25
+
26
+ # Model Description
27
+
28
+ AlphaEarth Foundations was proposed by research teams at Google DeepMind and Google. The model was trained on more than three billion observations. It takes Sentinel-2, Sentinel-1, and Landsat-8/9 time-series imagery as input and uses data including PALSAR-2, ERA5-Land, GEDI, GRACE, Copernicus DEM, NLCD, and geographic text as learning targets. The model is suitable for multi-source Earth observation representation learning, thematic mapping with sparse labels, biophysical variable estimation, and time-series change detection.
29
+
30
+ # Use Cases
31
+
32
+ | Use Case | Description |
33
+ | :---: | :--- |
34
+ | Multi-source spatiotemporal representation | Fuse asynchronous time-series observations from Sentinel-2, Sentinel-1, and Landsat-8/9. |
35
+ | Mapping with sparse labels | Train kNN or linear predictors using 64-dimensional embeddings and a small number of point labels. |
36
+ | Biophysical variable estimation | Regress continuous variables such as land surface emissivity and evapotranspiration from embeddings. |
37
+ | Land surface change detection | Compare unit-sphere embeddings from different validity periods to perform supervised or unsupervised change detection. |
38
+ | Local engineering validation | Use a small amount of synthetic data that preserves the paper's actual dimensions to validate training, inference, quantization, evaluation, and visualization workflows. |
39
+ | Multi-GPU training | Launch distributed data-parallel training with `torchrun`. |
40
+
41
+ # Usage Instructions
42
+
43
+ ## 1.OneCode
44
+
45
+ Experience intelligent, one-click AI4S programming through the OneCode online environment:
46
+
47
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
48
+
49
+ ## 2.Download and Installation
50
+
51
+ ```bash
52
+ hf download OneScience-Group/AlphaEarthFoundations --local-dir ./AlphaEarthFoundations
53
+ cd AlphaEarthFoundations
54
+ ```
55
+
56
+ ### Environment Dependencies
57
+
58
+ **Hardware Requirements**
59
+
60
+ - A GPU or DCU is recommended.
61
+ - A CPU can be used to verify connectivity with the default small-sample configuration; full training and global inference require large-scale acceleration resources.
62
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
63
+
64
+ **DCU Environment**
65
+
66
+ ```bash
67
+ # Activate DTK and CONDA first
68
+ conda create -n onescience311 python=3.11 -y
69
+ conda activate onescience311
70
+ # uv installation is supported
71
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
72
+ ```
73
+
74
+ **GPU Environment**
75
+
76
+ ```bash
77
+ # Activate CONDA first
78
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
79
+ conda activate onescience311
80
+ # uv installation is supported
81
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
82
+ ```
83
+
84
+ ### Training Data
85
+
86
+ 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 preserves the paper's 1.28 km × 1.28 km extent, 10-meter grid, 65 Sentinel-2 frames, 17 Sentinel-1 frames, 21 Landsat frames, and the explicitly disclosed input-source and training-target channel specifications; only the number of samples and the default internal model width are reduced.
87
+
88
+ ```bash
89
+ python scripts/fake_data.py
90
+ ```
91
+
92
+ ### Training
93
+
94
+ ```bash
95
+ python scripts/train.py
96
+ ```
97
+
98
+ For multi-GPU training, use:
99
+
100
+ ```bash
101
+ 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
102
+ ```
103
+
104
+ The default configuration is intended for rapid workflow validation. Formal experiments should use the multi-temporal data, complete task labels, model configuration, and training schedule corresponding to the paper.
105
+
106
+ ```text
107
+ result/checkpoints/alphaearthfoundations.pt
108
+ result/training/metrics.json
109
+ ```
110
+
111
+ ### Trained Weights
112
+
113
+ This repository does not include synthetic or trained weights. Training generates `result/checkpoints/alphaearthfoundations.pt`; Google and Google DeepMind have not released the paper's v2.0/v2.1 model weights or checkpoints.
114
+
115
+ ### Inference
116
+
117
+ ```bash
118
+ python scripts/inference.py
119
+ ```
120
+
121
+ Inference loads the training checkpoint and generates float32 unit-sphere embeddings, the paper's `s8²` signed int8 quantized embeddings, and reconstruction results for nine target categories. The outputs are saved to:
122
+
123
+ ```text
124
+ result/output/predictions.npz
125
+ ```
126
+
127
+ ### Evaluation and Visualization
128
+
129
+ ```bash
130
+ python scripts/result.py
131
+ ```
132
+
133
+ The evaluation reports reconstruction MAE or classification error rates for each training source, the mean embedding norm, and `s8²` quantization error. It also follows the paper's lightweight transfer approach by running kNN with `k=1`, kNN with `k=3`, unregularized linear classification, and linear regression; reporting Balanced Accuracy and R²; and generating comparison plots of the A01, A16, and A09 embedding axes against the land-cover target. Results on synthetic data are intended only to validate the engineering workflow and do not represent the paper's complete performance on 15 real downstream datasets.
134
+
135
+ ```text
136
+ result/evaluation/metrics.json
137
+ result/evaluation/comparison.png
138
+ ```
139
+
140
+ # Official OneScience Information
141
+
142
+ | Platform | OneScience Main Repository | Skills Repository |
143
+ | --- | --- | --- |
144
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
145
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
146
+
147
+ # Citation and License
148
+
149
+ This repository is an engineering reproduction of the original AlphaEarth Foundations paper.
150
+
151
+ Use of the code and data in this repository remains subject to the licenses and terms of use of their respective projects.
README_zh.md ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: CC-BY-4.0
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 多源地球观测
10
+ - 地理空间嵌入
11
+ frameworks: PyTorch
12
+ datasets:
13
+ - Google Satellite Embedding V1 Annual
14
+ ---
15
+
16
+ <p align="center">
17
+ <strong>
18
+ <span style="font-size: 30px;">AlphaEarthFoundations</span>
19
+ </strong>
20
+ </p>
21
+
22
+ # 模型介绍
23
+
24
+ AlphaEarthFoundations 是面向全球地球观测制图的多源时空嵌入场模型,将光学、雷达、LiDAR、气候、高程、土地覆盖和地理文本等稀疏且异步的数据统一编码为 64 维单位球嵌入,以少量标签支持分类、回归和变化检测。
25
+
26
+ 论文:AlphaEarth Foundations: An embedding field model for accurate and efficient global mapping from sparse label data
27
+ https://arxiv.org/abs/2507.22291
28
+
29
+ # 模型描述
30
+
31
+ AlphaEarth Foundations 由 Google DeepMind 与 Google 的研究团队提出。模型使用超过 30 亿次观测训练,输入 Sentinel-2、Sentinel-1 和 Landsat-8/9 时序影像,并以 PALSAR-2、ERA5-Land、GEDI、GRACE、Copernicus DEM、NLCD 和地理文本等数据作为学习目标。模型适用于多源地球观测表征学习、稀疏标签专题制图、生物物理变量估计和时序变化检测。
32
+
33
+ # 适用场景
34
+
35
+ | 场景 | 说明 |
36
+ | :---: | :--- |
37
+ | 多源时空表征 | 融合 Sentinel-2、Sentinel-1 和 Landsat-8/9 的异步时序观测。 |
38
+ | 稀疏标签制图 | 使用 64 维 embedding 和少量点标签训练 kNN 或线性预测器。 |
39
+ | 生物物理变量估计 | 基于 embedding 回归地表发射率、蒸散量等连续变量。 |
40
+ | 地表变化检测 | 比较不同有效时段的单位球 embedding,执行监督或无监督变化检测。 |
41
+ | 本地工程验证 | 使用保持论文真实维度的少量虚拟数据检查训练、推理、量化、评估和可视化流程。 |
42
+ | 多卡训练 | 通过 `torchrun` 启动分布式数据并行训练。 |
43
+
44
+ # 使用说明
45
+
46
+ ## 1.OneCode
47
+
48
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
49
+
50
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
51
+
52
+ ## 2.下载安装
53
+
54
+ ```bash
55
+ modelscope download --model OneScience/AlphaEarthFoundations --local_dir ./AlphaEarthFoundations
56
+ cd AlphaEarthFoundations
57
+ ```
58
+
59
+ ### 环境依赖
60
+
61
+ **硬件要求**
62
+
63
+ - 推荐使用 GPU 或 DCU 运行。
64
+ - CPU 可用于默认小样本配置的连通性验证,完整训练和全球推理需要大规模加速资源。
65
+ - DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
66
+
67
+ **DCU环境**
68
+
69
+ ```bash
70
+ # 请首先激活DTK及CONDA
71
+ conda create -n onescience311 python=3.11 -y
72
+ conda activate onescience311
73
+ # 支持uv安装
74
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
75
+ ```
76
+
77
+ **GPU环境**
78
+
79
+ ```bash
80
+ # 请首先激活CONDA
81
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
82
+ conda activate onescience311
83
+ # 支持uv安装
84
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
85
+ ```
86
+
87
+ ### 训练数据介绍
88
+
89
+ 本仓库使用少量虚拟样本验证工程流程,训练数据和测试数据分别保存为 `data/train.npz` 和 `data/test.npz`。虚拟数据保持论文的 1.28 km × 1.28 km、10 米网格、Sentinel-2 65 帧、Sentinel-1 17 帧、Landsat 21 帧,以及论文明确公开的输入源和训练目标通道规格;仅样本数量和默认模型内部宽度被缩小。
90
+
91
+ ```bash
92
+ python scripts/fake_data.py
93
+ ```
94
+
95
+ ### 训练
96
+
97
+ ```bash
98
+ python scripts/train.py
99
+ ```
100
+
101
+ 多卡训练可使用:
102
+
103
+ ```bash
104
+ 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
105
+ ```
106
+
107
+ 默认配置面向快速流程验证;开展正式实验时,应使用论文对应的多时相数据、完整任务标签、模型配置和训练周期。
108
+
109
+ ```text
110
+ result/checkpoints/alphaearthfoundations.pt
111
+ result/training/metrics.json
112
+ ```
113
+
114
+ ### 训练权重
115
+
116
+ 本仓库不内置虚拟权重或训练权重。执行训练后生成 `result/checkpoints/alphaearthfoundations.pt`;Google 和 Google DeepMind 尚未公开论文 v2.0/v2.1 模型权重或 checkpoint。
117
+
118
+ ### 推理
119
+
120
+ ```bash
121
+ python scripts/inference.py
122
+ ```
123
+
124
+ 推理加载训练 checkpoint,生成 float32 单位球 embedding、论文 `s8²` 有符号 int8 量化 embedding 和九类目标重建结果,并保存到:
125
+
126
+ ```text
127
+ result/output/predictions.npz
128
+ ```
129
+
130
+ ### 评估和可视化
131
+
132
+ ```bash
133
+ python scripts/result.py
134
+ ```
135
+
136
+ 评估报告各训练源的重建 MAE 或分类错误率、embedding 平均范数和 `s8²` 量化误差;同时按论文的轻量迁移方式执行 kNN `k=1`、kNN `k=3`、无正则线性分类和线性回归,报告 Balanced Accuracy 与 R²,并生成 A01/A16/A09 三个 embedding 轴与土地覆盖目标的对比图。虚拟数据结���仅用于验证工程流程,不代表论文在 15 个真实下游数据集上的完整性能。
137
+
138
+ ```text
139
+ result/evaluation/metrics.json
140
+ result/evaluation/comparison.png
141
+ ```
142
+
143
+ # OneScience官方信息
144
+
145
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
146
+ | --- | --- | --- |
147
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
148
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
149
+
150
+ # 引用与许可证
151
+
152
+ 本仓库为 AlphaEarth Foundations 原始论文的工程复现版本。
153
+
154
+ 本仓库代码和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+ data:
3
+ root: data
4
+ format_version: aef_synthetic_paper_spec_v3
5
+ train_samples: 2
6
+ test_samples: 1
7
+ image_size: 128
8
+ input_sources:
9
+ sentinel2: {timesteps: 65, channels: 5, geometry_dim: 0}
10
+ sentinel1: {timesteps: 17, channels: 5, geometry_dim: 2}
11
+ landsat: {timesteps: 21, channels: 7, geometry_dim: 0}
12
+ target_sources:
13
+ sentinel2: {channels: 5, type: continuous, weight: 1.0, loss_grid_m: 10, shift_pixels: 2, geometry_dim: 0}
14
+ sentinel1: {channels: 5, type: continuous, weight: 1.0, loss_grid_m: 10, shift_pixels: 2, geometry_dim: 2}
15
+ landsat: {channels: 7, type: continuous, weight: 1.0, loss_grid_m: 30, geometry_dim: 0}
16
+ palsar2: {channels: 3, type: continuous, weight: 1.0, loss_grid_m: 30, geometry_dim: 2}
17
+ era5_land: {channels: 12, type: continuous, weight: 1.0, loss_grid_m: 10, geometry_dim: 0}
18
+ gedi: {channels: 101, type: continuous, weight: 1.0, loss_grid_m: 20, geometry_dim: 0}
19
+ grace: {channels: 1, type: continuous, weight: 0.5, loss_grid_m: 1280, geometry_dim: 0}
20
+ glo30: {channels: 1, type: continuous, weight: 1.0, loss_grid_m: 30, geometry_dim: 0, derived_channels: undisclosed}
21
+ nlcd: {channels: 16, type: categorical, weight: 0.5, loss_grid_m: 30, geometry_dim: 0}
22
+ model:
23
+ precision_dim: 8
24
+ time_dim: 16
25
+ space_dim: 32
26
+ embedding_dim: 64
27
+ num_blocks: 1
28
+ num_heads: 4
29
+ decoder_hidden_dim: 32
30
+ vmf_kappa: 8000.0
31
+ max_geometry_dim: 2
32
+ paper_model:
33
+ precision_dim: 128
34
+ time_dim: 512
35
+ space_dim: 1024
36
+ embedding_dim: 64
37
+ num_blocks: 15
38
+ parameter_count: approximately_480M
39
+ train:
40
+ epochs: 1
41
+ batch_size: 2
42
+ learning_rate: 0.0005
43
+ weight_decay: 0.000001
44
+ reconstruction_weight: 1.0
45
+ uniformity_weight: 0.05
46
+ consistency_weight: 0.02
47
+ text_weight: 0.001
48
+ source_dropout: {sentinel2: 0.0, sentinel1: 0.3, landsat: 0.3}
49
+ frame_dropout: {sentinel2: 0.5, sentinel1: 0.3, landsat: 0.3}
50
+ num_workers: 0
51
+ runtime:
52
+ device: auto
53
+ paths:
54
+ checkpoint: result/checkpoints/alphaearthfoundations.pt
55
+ training_metrics: result/training/metrics.json
56
+ inference_dir: result/output
57
+ evaluation_dir: result/evaluation
config.json ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "AlphaEarthFoundations",
3
+ "model_type": "alphaearthfoundations",
4
+ "architectures": [
5
+ "AlphaEarthFoundations",
6
+ "STPBlock",
7
+ "ConditionalDecoder"
8
+ ],
9
+ "framework": "PyTorch",
10
+ "domain": "earth-observation",
11
+ "task": "multi-source-geospatial-embedding",
12
+ "implementation": {
13
+ "entry_point": "model/alphaearthfoundations.py",
14
+ "scope": "engineering reproduction of a multi-source spatiotemporal embedding field for sparse-label global Earth observation mapping",
15
+ "train_script": "scripts/train.py",
16
+ "inference_script": "scripts/inference.py",
17
+ "evaluation_script": "scripts/result.py",
18
+ "synthetic_data_script": "scripts/fake_data.py"
19
+ },
20
+ "architecture": {
21
+ "family": "spatiotemporal precision-time-space embedding field",
22
+ "input_sources": [
23
+ "sentinel2",
24
+ "sentinel1",
25
+ "landsat"
26
+ ],
27
+ "precision_dim": 8,
28
+ "time_dim": 16,
29
+ "space_dim": 32,
30
+ "embedding_dim": 64,
31
+ "num_blocks": 1,
32
+ "num_heads": 4,
33
+ "decoder_hidden_dim": 32,
34
+ "vmf_kappa": 8000.0,
35
+ "max_geometry_dim": 2,
36
+ "embedding_normalization": "L2 unit sphere",
37
+ "paper_model": {
38
+ "precision_dim": 128,
39
+ "time_dim": 512,
40
+ "space_dim": 1024,
41
+ "embedding_dim": 64,
42
+ "num_blocks": 15,
43
+ "parameter_count": "approximately 480M"
44
+ }
45
+ },
46
+ "data": {
47
+ "datasets": [
48
+ "Google Satellite Embedding V1 Annual"
49
+ ],
50
+ "protocol": "aef_synthetic_paper_spec_v3",
51
+ "format": "NPZ",
52
+ "train_file": "data/train.npz",
53
+ "test_file": "data/test.npz",
54
+ "image_size": 128,
55
+ "spatial_extent_km": [
56
+ 1.28,
57
+ 1.28
58
+ ],
59
+ "grid_resolution_m": 10,
60
+ "input_sources": {
61
+ "sentinel2": {
62
+ "timesteps": 65,
63
+ "channels": 5,
64
+ "geometry_dim": 0
65
+ },
66
+ "sentinel1": {
67
+ "timesteps": 17,
68
+ "channels": 5,
69
+ "geometry_dim": 2
70
+ },
71
+ "landsat": {
72
+ "timesteps": 21,
73
+ "channels": 7,
74
+ "geometry_dim": 0
75
+ }
76
+ },
77
+ "target_sources": {
78
+ "sentinel2": {"channels": 5, "type": "continuous", "loss_grid_m": 10},
79
+ "sentinel1": {"channels": 5, "type": "continuous", "loss_grid_m": 10},
80
+ "landsat": {"channels": 7, "type": "continuous", "loss_grid_m": 30},
81
+ "palsar2": {"channels": 3, "type": "continuous", "loss_grid_m": 30},
82
+ "era5_land": {"channels": 12, "type": "continuous", "loss_grid_m": 10},
83
+ "gedi": {"channels": 101, "type": "continuous", "loss_grid_m": 20},
84
+ "grace": {"channels": 1, "type": "continuous", "loss_grid_m": 1280},
85
+ "glo30": {"channels": 1, "type": "continuous", "loss_grid_m": 30},
86
+ "nlcd": {"channels": 16, "type": "categorical", "loss_grid_m": 30}
87
+ },
88
+ "required_metadata": [
89
+ "format_version",
90
+ "data_source",
91
+ "timestamps",
92
+ "frame_available",
93
+ "channel_available",
94
+ "pixel_valid",
95
+ "geometry",
96
+ "support_period",
97
+ "valid_period",
98
+ "text_target"
99
+ ]
100
+ },
101
+ "configuration_sources": [
102
+ "conf/config.yaml",
103
+ "model/alphaearthfoundations.py",
104
+ "scripts/fake_data.py",
105
+ "scripts/train.py",
106
+ "scripts/inference.py",
107
+ "scripts/result.py"
108
+ ]
109
+ }
configuration.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "geospatial-embedding-field",
4
+ "model": "AlphaEarthFoundations",
5
+ "paper": "https://arxiv.org/abs/2507.22291",
6
+ "dataset": "GOOGLE/SATELLITE_EMBEDDING/V1/ANNUAL",
7
+ "license": "CC-BY-4.0",
8
+ "allow_remote": true,
9
+ "model": {"type": "AlphaEarthFoundations", "repo_id": "OneScience/AlphaEarthFoundations"},
10
+ "pipeline": {"type": "earth-observation-embedding"}
11
+ }
model/alphaearthfoundations.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Engineering reproduction of AlphaEarth Foundations from the paper specification."""
2
+
3
+ import math
4
+
5
+ import torch
6
+ from torch import nn
7
+ from torch.nn import functional as F
8
+
9
+
10
+ def sinusoidal_timecode(timestamps, dim, origin=None, scale=365.25 * 24 * 3600 * 1000):
11
+ if origin is None:
12
+ origin = timestamps.amin(dim=1, keepdim=True)
13
+ values = (timestamps.double() - origin.double()) / scale
14
+ values = values.float()
15
+ frequencies = torch.exp(
16
+ torch.arange(0, dim, 2, device=timestamps.device) * (-math.log(10000.0) / dim)
17
+ )
18
+ angles = values.unsqueeze(-1) * frequencies
19
+ code = torch.zeros(*timestamps.shape, dim, device=timestamps.device)
20
+ code[..., 0::2] = angles.sin()
21
+ code[..., 1::2] = angles.cos()
22
+ return code
23
+
24
+
25
+ class STPBlock(nn.Module):
26
+ """Parallel precision, time and space operators with learned pyramid exchange."""
27
+
28
+ def __init__(self, precision_dim, time_dim, space_dim, num_heads):
29
+ super().__init__()
30
+ self.precision = nn.Sequential(
31
+ nn.GroupNorm(1, precision_dim),
32
+ nn.Conv2d(precision_dim, precision_dim, 3, padding=1),
33
+ nn.GELU(),
34
+ nn.Conv2d(precision_dim, precision_dim, 3, padding=1),
35
+ )
36
+ self.time_norm = nn.LayerNorm(time_dim)
37
+ self.time_attention = nn.MultiheadAttention(time_dim, num_heads, batch_first=True)
38
+ self.space_norm = nn.LayerNorm(space_dim)
39
+ self.space_attention = nn.MultiheadAttention(space_dim, num_heads, batch_first=True)
40
+ self.to_precision = nn.ModuleList([nn.Conv2d(time_dim, precision_dim, 1), nn.Conv2d(space_dim, precision_dim, 1)])
41
+ self.to_time = nn.Conv2d(precision_dim, time_dim, 1)
42
+ self.to_space = nn.Conv2d(precision_dim, space_dim, 1)
43
+
44
+ def forward(self, precision, time, space, frame_available):
45
+ batch, frames = precision.shape[:2]
46
+ p_size, t_size, s_size = precision.shape[-2:], time.shape[-2:], space.shape[-2:]
47
+ p = precision.flatten(0, 1)
48
+ p = p + self.precision(p)
49
+
50
+ sequence = time.permute(0, 3, 4, 1, 2).reshape(-1, frames, time.shape[2])
51
+ normalized = self.time_norm(sequence)
52
+ time_mask = (~frame_available.bool())[:, None, None, :].expand(batch, *t_size, frames).reshape(-1, frames)
53
+ sequence = sequence + self.time_attention(
54
+ normalized, normalized, normalized, key_padding_mask=time_mask, need_weights=False
55
+ )[0]
56
+ time = sequence.reshape(batch, *t_size, frames, -1).permute(0, 3, 4, 1, 2)
57
+
58
+ available = frame_available[:, :, None, None, None].to(space.dtype)
59
+ spatial = (space * available).sum(dim=1) / available.sum(dim=1).clamp_min(1)
60
+ spatial = spatial.flatten(2).transpose(1, 2)
61
+ normalized = self.space_norm(spatial)
62
+ spatial = spatial + self.space_attention(normalized, normalized, normalized, need_weights=False)[0]
63
+ spatial = spatial.transpose(1, 2).reshape(batch, -1, *s_size)
64
+ space = space + spatial[:, None]
65
+
66
+ t_flat, s_flat = time.flatten(0, 1), space.flatten(0, 1)
67
+ precision = p + self.to_precision[0](F.interpolate(t_flat, p_size, mode="bilinear", align_corners=False))
68
+ precision = precision + self.to_precision[1](F.interpolate(s_flat, p_size, mode="bilinear", align_corners=False))
69
+ time = time + self.to_time(F.interpolate(p, t_size, mode="bilinear", align_corners=False)).unflatten(0, (batch, frames))
70
+ space = space + self.to_space(F.interpolate(p, s_size, mode="bilinear", align_corners=False)).unflatten(0, (batch, frames))
71
+ return precision.unflatten(0, (batch, frames)), time, space
72
+
73
+
74
+ class ConditionalDecoder(nn.Module):
75
+ def __init__(self, embedding_dim, condition_dim, hidden_dim, output_dim):
76
+ super().__init__()
77
+ self.condition = nn.Linear(condition_dim, hidden_dim)
78
+ self.network = nn.Sequential(
79
+ nn.Conv2d(embedding_dim + hidden_dim, hidden_dim, 1),
80
+ nn.GELU(),
81
+ nn.Conv2d(hidden_dim, hidden_dim, 1),
82
+ nn.GELU(),
83
+ nn.Conv2d(hidden_dim, output_dim, 1),
84
+ )
85
+
86
+ def forward(self, embedding, condition):
87
+ context = self.condition(condition)[:, :, None, None].expand(-1, -1, *embedding.shape[-2:])
88
+ return self.network(torch.cat([embedding, context], dim=1))
89
+
90
+
91
+ class AlphaEarthFoundations(nn.Module):
92
+ def __init__(self, input_sources, target_sources, config):
93
+ super().__init__()
94
+ p_dim, t_dim, s_dim = config["precision_dim"], config["time_dim"], config["space_dim"]
95
+ self.input_names = list(input_sources)
96
+ self.target_sources = target_sources
97
+ self.embedding_dim = config["embedding_dim"]
98
+ self.vmf_kappa = float(config["vmf_kappa"])
99
+ self.projectors = nn.ModuleDict({
100
+ name: nn.Sequential(nn.Conv2d(spec["channels"], p_dim, 3, stride=2, padding=1), nn.GELU())
101
+ for name, spec in input_sources.items()
102
+ })
103
+ self.time_projector = nn.Conv2d(p_dim, t_dim, 3, stride=4, padding=1)
104
+ self.space_projector = nn.Conv2d(p_dim, s_dim, 3, stride=8, padding=1)
105
+ self.time_context = nn.Linear(t_dim, t_dim)
106
+ self.blocks = nn.ModuleList([
107
+ STPBlock(p_dim, t_dim, s_dim, config["num_heads"]) for _ in range(config["num_blocks"])
108
+ ])
109
+ self.summary_query = nn.Linear(t_dim * 2, p_dim)
110
+ self.embedding_head = nn.Conv2d(p_dim, self.embedding_dim, 1)
111
+ self.embedding_upsample = nn.ConvTranspose2d(p_dim, p_dim, 4, stride=2, padding=1)
112
+ condition_dim = t_dim + config["max_geometry_dim"]
113
+ self.decoders = nn.ModuleDict({
114
+ name: ConditionalDecoder(self.embedding_dim, condition_dim, config["decoder_hidden_dim"], spec["channels"])
115
+ for name, spec in target_sources.items()
116
+ })
117
+
118
+ def _summarize(self, precision, availability, period, origin):
119
+ period_codes = sinusoidal_timecode(period, self.time_context.in_features, origin)
120
+ query = self.summary_query(period_codes.flatten(1))
121
+ scores = (precision * query[:, None, :, None, None]).sum(dim=2).mean(dim=(-1, -2))
122
+ scores = scores.masked_fill(~availability.bool(), torch.finfo(scores.dtype).min)
123
+ summary = (precision * scores.softmax(dim=1)[:, :, None, None, None]).sum(dim=1)
124
+ return F.normalize(self.embedding_head(self.embedding_upsample(summary)), dim=1)
125
+
126
+ def forward(self, sources, timestamps, valid_period, frame_available, target_times=None,
127
+ target_geometry=None, target_periods=None):
128
+ precision_parts, code_parts = [], []
129
+ origin = torch.cat(list(timestamps.values()), dim=1).amin(dim=1, keepdim=True)
130
+ for name in self.input_names:
131
+ values = sources[name]
132
+ batch, frames = values.shape[:2]
133
+ projected = self.projectors[name](values.flatten(0, 1)).unflatten(0, (batch, frames))
134
+ precision_parts.append(projected)
135
+ code_parts.append(sinusoidal_timecode(timestamps[name], self.time_context.in_features, origin))
136
+ availability = torch.cat([frame_available[name] for name in self.input_names], dim=1)
137
+ precision = torch.cat(precision_parts, dim=1)
138
+ codes = torch.cat(code_parts, dim=1)
139
+ time = self.time_projector(precision.flatten(0, 1)).unflatten(0, precision.shape[:2])
140
+ time = time + self.time_context(codes)[:, :, :, None, None]
141
+ space = self.space_projector(precision.flatten(0, 1)).unflatten(0, precision.shape[:2])
142
+ for block in self.blocks:
143
+ precision, time, space = block(precision, time, space, availability)
144
+
145
+ embedding = self._summarize(precision, availability, valid_period, origin)
146
+ outputs = {"embedding": embedding}
147
+ if target_times is not None:
148
+ outputs["reconstructions"] = {}
149
+ for name in self.target_sources:
150
+ source_embedding = self._summarize(precision, availability, target_periods[name], origin)
151
+ if self.training:
152
+ source_embedding = F.normalize(
153
+ source_embedding + torch.randn_like(source_embedding) / math.sqrt(self.vmf_kappa), dim=1
154
+ )
155
+ relative_time = (
156
+ (target_times[name] - target_periods[name][:, 0]).float()
157
+ / (target_periods[name][:, 1] - target_periods[name][:, 0]).float().clamp_min(1)
158
+ )
159
+ time_code = sinusoidal_timecode(
160
+ relative_time[:, None], self.time_context.in_features,
161
+ torch.zeros_like(relative_time[:, None]), scale=1.0
162
+ )[:, 0]
163
+ geometry = target_geometry[name]
164
+ outputs["reconstructions"][name] = self.decoders[name](source_embedding, torch.cat([time_code, geometry], dim=1))
165
+ return outputs
166
+
167
+
168
+ def _pool_continuous(values, grid_m):
169
+ if grid_m == 10:
170
+ return values
171
+ size = max(1, round(values.shape[-1] * 10 / grid_m))
172
+ return F.adaptive_avg_pool2d(values, (size, size))
173
+
174
+
175
+ def _shift_invariant_l1(prediction, target, mask, radius):
176
+ losses = []
177
+ for dy in range(-radius, radius + 1):
178
+ for dx in range(-radius, radius + 1):
179
+ shifted = torch.roll(prediction, (dy, dx), dims=(-2, -1))
180
+ valid = mask.clone()
181
+ if dy > 0: valid[..., :dy, :] = 0
182
+ if dy < 0: valid[..., dy:, :] = 0
183
+ if dx > 0: valid[..., :, :dx] = 0
184
+ if dx < 0: valid[..., :, dx:] = 0
185
+ losses.append((torch.abs(shifted - target) * valid).sum() / valid.sum().clamp_min(1))
186
+ return torch.stack(losses).amin()
187
+
188
+
189
+ def compute_losses(teacher, student, targets, masks, text_target, target_sources, weights):
190
+ reconstruction = teacher["embedding"].new_zeros(())
191
+ components = {}
192
+ for name, spec in target_sources.items():
193
+ prediction, target, mask = teacher["reconstructions"][name], targets[name], masks[name]
194
+ grid_m = int(spec["loss_grid_m"])
195
+ if spec["type"] == "categorical":
196
+ size = max(1, round(prediction.shape[-1] * 10 / grid_m))
197
+ prediction = F.adaptive_avg_pool2d(prediction, (size, size))
198
+ one_hot = F.one_hot(target.long(), num_classes=prediction.shape[1]).permute(0, 3, 1, 2).float()
199
+ target = F.adaptive_avg_pool2d(one_hot, (size, size)).argmax(dim=1)
200
+ mask = F.adaptive_avg_pool2d(mask, (size, size))
201
+ value = F.cross_entropy(prediction, target, reduction="none")
202
+ value = (value * mask[:, 0]).sum() / mask[:, 0].sum().clamp_min(1)
203
+ else:
204
+ if spec.get("shift_pixels", 0):
205
+ value = _shift_invariant_l1(prediction, target, mask, int(spec["shift_pixels"]))
206
+ else:
207
+ prediction, target, mask = (_pool_continuous(item, grid_m) for item in (prediction, target, mask))
208
+ value = (torch.abs(prediction - target) * mask).sum() / mask.sum().clamp_min(1)
209
+ components[f"reconstruction_{name}"] = value
210
+ reconstruction = reconstruction + float(spec["weight"]) * value
211
+ flat = teacher["embedding"].permute(0, 2, 3, 1).reshape(-1, teacher["embedding"].shape[1])
212
+ rotated = torch.roll(flat, max(1, flat.shape[0] // 2), dims=0)
213
+ uniformity = (flat * rotated).sum(dim=1).abs().mean()
214
+ consistency = 1.0 - (teacher["embedding"] * student["embedding"]).sum(dim=1).mean()
215
+ pooled = F.normalize(teacher["embedding"].mean(dim=(2, 3)), dim=1)
216
+ normalized_text = F.normalize(text_target, dim=1)
217
+ logits = pooled @ normalized_text.transpose(0, 1)
218
+ labels = torch.arange(len(logits), device=logits.device)
219
+ text_alignment = 0.5 * (F.cross_entropy(logits, labels) + F.cross_entropy(logits.transpose(0, 1), labels))
220
+ total = (weights["reconstruction"] * reconstruction + weights["uniformity"] * uniformity
221
+ + weights["consistency"] * consistency + weights["text"] * text_alignment)
222
+ components.update(reconstruction=reconstruction, uniformity=uniformity,
223
+ consistency=consistency, text_alignment=text_alignment, total=total)
224
+ return total, components
225
+
226
+
227
+ def quantize_embeddings(embedding, power=2, scale=127.5):
228
+ transformed = embedding.abs().pow(1.0 / power) * embedding.sign()
229
+ return torch.round(transformed * scale).clamp(-127, 127).to(torch.int8)
230
+
231
+
232
+ def dequantize_embeddings(quantized, power=2, scale=127.5):
233
+ values = quantized.float() / scale
234
+ return values.abs().pow(power) * values.sign()
scripts/fake_data.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate sparse synthetic observations following the paper-confirmed data contract."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import yaml
8
+
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ DAY_MS = 86_400_000
12
+ TARGET_ONLY_SLOTS = 3
13
+
14
+
15
+ def load_config():
16
+ return yaml.safe_load((ROOT / "conf/config.yaml").read_text())
17
+
18
+
19
+ def standardized_signal(rng, count, frames, channels, size, source_phase):
20
+ y, x = np.mgrid[-1:1:complex(size), -1:1:complex(size)].astype(np.float32)
21
+ base = np.sin(3 * np.pi * x) * np.cos(2 * np.pi * y) + 0.4 * x + 0.2 * y
22
+ values = np.empty((count, frames, channels, size, size), np.float32)
23
+ for sample in range(count):
24
+ phase = rng.uniform(0, 2 * np.pi) + source_phase
25
+ for step in range(frames):
26
+ seasonal = np.sin(2 * np.pi * step / max(frames, 1) + phase)
27
+ for channel in range(channels):
28
+ values[sample, step, channel] = base + 0.08 * channel + 0.25 * seasonal
29
+ values[sample] += rng.normal(0, 0.04, values[sample].shape)
30
+ values -= values.mean(axis=(0, 1, 3, 4), keepdims=True)
31
+ values /= values.std(axis=(0, 1, 3, 4), keepdims=True).clip(1e-6)
32
+ return np.clip(values, -6, 6).astype(np.float32)
33
+
34
+
35
+ def quality_mask(rng, count, frames, channels, size, sparse=False):
36
+ probability = 0.08 if not sparse else 0.92
37
+ mask = rng.random((count, frames, 1, size, size)) > probability
38
+ if not sparse:
39
+ mask[:, :, :, :2] = False
40
+ return np.repeat(mask, channels, axis=2).astype(np.float32)
41
+
42
+
43
+ def make_split(path, count, config, seed):
44
+ rng = np.random.default_rng(seed)
45
+ data, size = config["data"], config["data"]["image_size"]
46
+ start = np.datetime64("2020-01-01", "ms").astype(np.int64)
47
+ payload = {"format_version": np.asarray(data["format_version"]), "data_source": np.asarray("synthetic")}
48
+
49
+ for source_index, (name, spec) in enumerate(data["input_sources"].items()):
50
+ frames, channels = spec["timesteps"], spec["channels"]
51
+ payload[name] = standardized_signal(rng, count, frames, channels, size, source_index)
52
+ days = np.linspace(0, 364, frames, dtype=np.int64)
53
+ payload[f"timestamps_{name}"] = np.tile(start + days * DAY_MS + source_index, (count, 1))
54
+ available = np.ones((count, frames), np.bool_)
55
+ available[:, -max(1, frames // 10):] = False
56
+ payload[f"frame_available_{name}"] = available
57
+ channel_available = np.ones((count, frames, channels), np.bool_)
58
+ if name == "sentinel1":
59
+ channel_available[..., :4] = False
60
+ for sample in range(count):
61
+ for step in range(frames):
62
+ pair = (0, 1) if (sample + step) % 2 == 0 else (2, 3)
63
+ channel_available[sample, step, list(pair)] = True
64
+ payload[f"channel_available_{name}"] = channel_available
65
+ payload[f"pixel_valid_{name}"] = quality_mask(rng, count, frames, channels, size)
66
+ geometry = np.zeros((count, frames, config["model"]["max_geometry_dim"]), np.float32)
67
+ if name == "sentinel1":
68
+ geometry[..., 0] = rng.uniform(-np.pi, np.pi, (count, frames))
69
+ geometry[..., 1] = rng.uniform(1.6, 1.8, (count, frames))
70
+ payload[f"geometry_{name}"] = geometry
71
+
72
+ for target_index, (name, spec) in enumerate(data["target_sources"].items()):
73
+ if name in data["input_sources"]:
74
+ continue
75
+ channels = 1 if spec["type"] == "categorical" else spec["channels"]
76
+ values = standardized_signal(rng, count, TARGET_ONLY_SLOTS, channels, size, target_index + 3)
77
+ if spec["type"] == "categorical":
78
+ bins = np.linspace(values.min(), values.max(), spec["channels"] + 1)[1:-1]
79
+ values = np.digitize(values[:, :, 0], bins).astype(np.int64)
80
+ payload[f"target_sequence_{name}"] = values
81
+ payload[f"target_timestamps_{name}"] = np.tile(
82
+ start + np.asarray([30, 182, 330], np.int64) * DAY_MS + target_index, (count, 1)
83
+ )
84
+ mask_channels = 1 if spec["type"] == "categorical" else spec["channels"]
85
+ payload[f"target_pixel_valid_{name}"] = quality_mask(
86
+ rng, count, TARGET_ONLY_SLOTS, mask_channels, size, sparse=name == "gedi"
87
+ )
88
+ geometry = np.zeros((count, TARGET_ONLY_SLOTS, config["model"]["max_geometry_dim"]), np.float32)
89
+ if name == "palsar2":
90
+ geometry[..., 0] = rng.integers(0, 2, (count, TARGET_ONLY_SLOTS))
91
+ geometry[..., 1] = rng.integers(0, 2, (count, TARGET_ONLY_SLOTS))
92
+ payload[f"target_geometry_{name}"] = geometry
93
+ payload[f"target_frame_available_{name}"] = np.ones((count, TARGET_ONLY_SLOTS), np.bool_)
94
+
95
+ payload["support_period"] = np.tile(np.array([start, start + 365 * DAY_MS], np.int64), (count, 1))
96
+ payload["valid_period"] = np.tile(np.array([start + 60 * DAY_MS, start + 300 * DAY_MS], np.int64), (count, 1))
97
+ text = rng.normal(size=(count, config["model"]["embedding_dim"])).astype(np.float32)
98
+ payload["text_target"] = text / np.linalg.norm(text, axis=1, keepdims=True)
99
+ np.savez_compressed(path, **payload)
100
+
101
+
102
+ def main():
103
+ parser = argparse.ArgumentParser()
104
+ parser.add_argument("--force", action="store_true")
105
+ args = parser.parse_args()
106
+ config = load_config()
107
+ data_dir = ROOT / config["data"]["root"]
108
+ data_dir.mkdir(parents=True, exist_ok=True)
109
+ for offset, (name, count) in enumerate((("train.npz", config["data"]["train_samples"]),
110
+ ("test.npz", config["data"]["test_samples"]))):
111
+ target = data_dir / name
112
+ if args.force or not target.exists():
113
+ make_split(target, count, config, config["seed"] + offset)
114
+ print(f"generated={target.relative_to(ROOT)} samples={count} format={config['data']['format_version']}")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate float and paper-style signed-int8 annual embedding fields."""
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
+ sys.path.insert(0, str(ROOT / "scripts"))
14
+ from model.alphaearthfoundations import AlphaEarthFoundations, dequantize_embeddings, quantize_embeddings
15
+ from train import AEFDataset, device_from_config, unpack
16
+
17
+
18
+ def main():
19
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
20
+ torch.manual_seed(config["seed"])
21
+ device = device_from_config(config)
22
+ checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=True)
23
+ if checkpoint["format_version"] != config["data"]["format_version"]:
24
+ raise ValueError("Checkpoint and data formats do not match")
25
+ model = AlphaEarthFoundations(checkpoint["input_sources"], checkpoint["target_sources"], checkpoint["model_config"]).to(device)
26
+ model.load_state_dict(checkpoint["model"])
27
+ model.eval()
28
+ dataset = AEFDataset(ROOT / config["data"]["root"] / "test.npz", config)
29
+ embeddings, quantized, restored = [], [], []
30
+ reconstruction = {name: [] for name in config["data"]["target_sources"]}
31
+ selected_targets = {name: [] for name in config["data"]["target_sources"]}
32
+ selected_masks = {name: [] for name in config["data"]["target_sources"]}
33
+ with torch.no_grad():
34
+ for index in range(len(dataset)):
35
+ batch = {key: value.unsqueeze(0) for key, value in dataset[index].items()}
36
+ (sources, timestamps, frame_available, targets, masks, target_times,
37
+ target_periods, geometry) = unpack(batch, config, device)
38
+ output = model(sources, timestamps, batch["valid_period"].to(device), frame_available,
39
+ target_times, geometry, target_periods)
40
+ q = quantize_embeddings(output["embedding"])
41
+ embeddings.append(output["embedding"].cpu().numpy())
42
+ quantized.append(q.cpu().numpy())
43
+ restored.append(dequantize_embeddings(q).cpu().numpy())
44
+ for name, values in output["reconstructions"].items():
45
+ reconstruction[name].append(values.cpu().numpy())
46
+ selected_targets[name].append(targets[name].cpu().numpy())
47
+ selected_masks[name].append(masks[name].cpu().numpy())
48
+ output_dir = ROOT / config["paths"]["inference_dir"]
49
+ output_dir.mkdir(parents=True, exist_ok=True)
50
+ payload = {"embedding": np.concatenate(embeddings), "embedding_s8_power2": np.concatenate(quantized),
51
+ "embedding_dequantized": np.concatenate(restored)}
52
+ payload.update({f"reconstruction_{name}": np.concatenate(values) for name, values in reconstruction.items()})
53
+ payload.update({f"target_{name}": np.concatenate(values) for name, values in selected_targets.items()})
54
+ payload.update({f"mask_{name}": np.concatenate(values) for name, values in selected_masks.items()})
55
+ np.savez_compressed(output_dir / "predictions.npz", **payload)
56
+ print(f"predictions={(output_dir / 'predictions.npz').relative_to(ROOT)}")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
scripts/result.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate reconstruction, spherical embeddings, quantization and low-shot transfer."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ import yaml
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+
13
+
14
+ def balanced_accuracy(target, prediction):
15
+ scores = [(prediction[target == label] == label).mean() for label in np.unique(target)]
16
+ return float(np.mean(scores))
17
+
18
+
19
+ def transfer_metrics(embedding, class_target, regression_target, seed):
20
+ rng = np.random.default_rng(seed)
21
+ features = embedding.transpose(0, 2, 3, 1).reshape(-1, embedding.shape[1])
22
+ classes = class_target.reshape(-1)
23
+ regression = regression_target.reshape(-1)
24
+ train_indices, test_indices = [], []
25
+ for label in np.unique(classes):
26
+ indices = np.flatnonzero(classes == label)
27
+ rng.shuffle(indices)
28
+ split = min(10, max(1, len(indices) // 3))
29
+ train_indices.extend(indices[:split])
30
+ test_indices.extend(indices[split:])
31
+ train_indices, test_indices = np.asarray(train_indices), np.asarray(test_indices)
32
+ x_train, x_test = features[train_indices], features[test_indices]
33
+ y_train, y_test = classes[train_indices], classes[test_indices]
34
+ distances = ((x_test[:, None] - x_train[None]) ** 2).sum(axis=-1)
35
+ transfer = {}
36
+ for k in (1, 3):
37
+ neighbors = np.argpartition(distances, min(k, len(x_train)) - 1, axis=1)[:, :k]
38
+ votes = y_train[neighbors]
39
+ prediction = np.asarray([np.bincount(row).argmax() for row in votes])
40
+ transfer[f"knn_k{k}_balanced_accuracy"] = balanced_accuracy(y_test, prediction)
41
+ labels = np.unique(classes)
42
+ one_hot = np.stack([np.where(y_train == label, 1.0, -1.0) for label in labels], axis=1)
43
+ design = np.column_stack([x_train, np.ones(len(x_train))])
44
+ coefficients = np.linalg.lstsq(design, one_hot, rcond=None)[0]
45
+ class_prediction = labels[np.argmax(np.column_stack([x_test, np.ones(len(x_test))]) @ coefficients, axis=1)]
46
+ transfer["linear_balanced_accuracy"] = balanced_accuracy(y_test, class_prediction)
47
+ regression_coefficients = np.linalg.lstsq(design, regression[train_indices], rcond=None)[0]
48
+ regression_prediction = np.column_stack([x_test, np.ones(len(x_test))]) @ regression_coefficients
49
+ residual = ((regression[test_indices] - regression_prediction) ** 2).sum()
50
+ total = ((regression[test_indices] - regression[test_indices].mean()) ** 2).sum()
51
+ transfer["linear_regression_r2"] = float(1.0 - residual / max(total, 1e-12))
52
+ transfer["train_pixels"] = int(len(train_indices))
53
+ transfer["test_pixels"] = int(len(test_indices))
54
+ return transfer
55
+
56
+
57
+ def main():
58
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
59
+ predictions = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
60
+ embedding, restored = predictions["embedding"], predictions["embedding_dequantized"]
61
+ metrics = {
62
+ "samples": int(len(embedding)),
63
+ "mean_embedding_norm": float(np.linalg.norm(embedding, axis=1).mean()),
64
+ "s8_power2_quantization_mae": float(np.abs(embedding - restored).mean()),
65
+ "reconstruction_mae": {},
66
+ "low_shot_transfer": transfer_metrics(
67
+ embedding, predictions["target_nlcd"], predictions["target_sentinel2"][:, 0], config["seed"]
68
+ ),
69
+ }
70
+ for name, spec in config["data"]["target_sources"].items():
71
+ prediction = predictions[f"reconstruction_{name}"]
72
+ target = predictions[f"target_{name}"]
73
+ mask = predictions[f"mask_{name}"]
74
+ if spec["type"] == "categorical":
75
+ metrics["reconstruction_mae"][name] = float(
76
+ (((prediction.argmax(axis=1) != target) * mask[:, 0]).sum()) / max(mask[:, 0].sum(), 1)
77
+ )
78
+ else:
79
+ metrics["reconstruction_mae"][name] = float((np.abs(prediction - target) * mask).sum() / max(mask.sum(), 1))
80
+ evaluation_dir = ROOT / config["paths"]["evaluation_dir"]
81
+ evaluation_dir.mkdir(parents=True, exist_ok=True)
82
+ (evaluation_dir / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
83
+ rgb = embedding[0, [1, 16, 9]].transpose(1, 2, 0)
84
+ rgb = np.clip((rgb + 0.3) / 0.6, 0, 1)
85
+ figure, axes = plt.subplots(1, 2, figsize=(8, 4))
86
+ axes[0].imshow(rgb)
87
+ axes[0].set_title("AEF axes A01/A16/A09")
88
+ axes[1].imshow(predictions["target_nlcd"][0], cmap="tab20", vmin=0, vmax=15)
89
+ axes[1].set_title("Synthetic NLCD target")
90
+ for axis in axes:
91
+ axis.axis("off")
92
+ figure.tight_layout()
93
+ figure.savefig(evaluation_dir / "comparison.png", dpi=160)
94
+ plt.close(figure)
95
+ print(json.dumps(metrics, indent=2))
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
scripts/train.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train AEF reconstruction, uniformity, consistency and text objectives."""
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.alphaearthfoundations import AlphaEarthFoundations, compute_losses
18
+
19
+
20
+ class AEFDataset(Dataset):
21
+ def __init__(self, path, config):
22
+ self.data = np.load(path)
23
+ self.input_sources = config["data"]["input_sources"]
24
+ self.target_sources = config["data"]["target_sources"]
25
+ format_version = str(self.data["format_version"])
26
+ if format_version != config["data"]["format_version"]:
27
+ raise ValueError(f"Expected format {config['data']['format_version']}, got {format_version}")
28
+ for name, spec in self.input_sources.items():
29
+ expected = (spec["timesteps"], spec["channels"], config["data"]["image_size"], config["data"]["image_size"])
30
+ if self.data[name].shape[1:] != expected:
31
+ raise ValueError(f"{name} has shape {self.data[name].shape[1:]}, expected {expected}")
32
+
33
+ def __len__(self):
34
+ return len(self.data["valid_period"])
35
+
36
+ def __getitem__(self, index):
37
+ item = {name: torch.from_numpy(self.data[name][index]).float() for name in self.input_sources}
38
+ item.update({f"timestamps_{name}": torch.from_numpy(self.data[f"timestamps_{name}"][index]).long()
39
+ for name in self.input_sources})
40
+ for name in self.input_sources:
41
+ item[f"frame_available_{name}"] = torch.from_numpy(self.data[f"frame_available_{name}"][index])
42
+ item[f"channel_available_{name}"] = torch.from_numpy(self.data[f"channel_available_{name}"][index])
43
+ item[f"pixel_valid_{name}"] = torch.from_numpy(self.data[f"pixel_valid_{name}"][index]).float()
44
+ item[f"geometry_{name}"] = torch.from_numpy(self.data[f"geometry_{name}"][index]).float()
45
+ for name, spec in self.target_sources.items():
46
+ if name in self.input_sources:
47
+ continue
48
+ values = torch.from_numpy(self.data[f"target_sequence_{name}"][index])
49
+ item[f"target_sequence_{name}"] = values.long() if spec["type"] == "categorical" else values.float()
50
+ item[f"target_timestamps_{name}"] = torch.from_numpy(self.data[f"target_timestamps_{name}"][index]).long()
51
+ item[f"target_pixel_valid_{name}"] = torch.from_numpy(self.data[f"target_pixel_valid_{name}"][index]).float()
52
+ item[f"target_geometry_{name}"] = torch.from_numpy(self.data[f"target_geometry_{name}"][index]).float()
53
+ item[f"target_frame_available_{name}"] = torch.from_numpy(self.data[f"target_frame_available_{name}"][index])
54
+ item["support_period"] = torch.from_numpy(self.data["support_period"][index]).long()
55
+ item["valid_period"] = torch.from_numpy(self.data["valid_period"][index]).long()
56
+ item["text_target"] = torch.from_numpy(self.data["text_target"][index]).float()
57
+ return item
58
+
59
+
60
+ def device_from_config(config, local_rank=0):
61
+ requested = config["runtime"]["device"]
62
+ if requested == "auto":
63
+ return torch.device("cuda", local_rank) if torch.cuda.is_available() else torch.device("cpu")
64
+ return torch.device(requested)
65
+
66
+
67
+ def _select_indices(available):
68
+ selected = []
69
+ for row in available:
70
+ candidates = torch.nonzero(row, as_tuple=False).flatten()
71
+ choice = torch.randint(len(candidates), (), device=candidates.device)
72
+ selected.append(candidates[choice])
73
+ return torch.stack(selected).to(available.device)
74
+
75
+
76
+ def _gather(values, indices):
77
+ return values[torch.arange(len(values), device=values.device), indices]
78
+
79
+
80
+ def unpack(batch, config, device, remove_input_targets=True):
81
+ input_names, target_specs = config["data"]["input_sources"], config["data"]["target_sources"]
82
+ sources, frame_available = {}, {}
83
+ timestamps = {name: batch[f"timestamps_{name}"].to(device) for name in input_names}
84
+ targets, masks, target_times, target_periods, geometry = {}, {}, {}, {}, {}
85
+ for name in input_names:
86
+ values = batch[name].to(device)
87
+ available = batch[f"frame_available_{name}"].to(device).clone()
88
+ channel_available = batch[f"channel_available_{name}"].to(device)
89
+ values = values * channel_available[:, :, :, None, None]
90
+ indices = _select_indices(available)
91
+ targets[name] = _gather(values, indices)
92
+ masks[name] = _gather(batch[f"pixel_valid_{name}"].to(device), indices) * _gather(channel_available, indices)[:, :, None, None]
93
+ target_times[name] = _gather(timestamps[name], indices)
94
+ target_periods[name] = torch.stack([
95
+ target_times[name] - 5 * 86_400_000, target_times[name] + 5 * 86_400_000
96
+ ], dim=1)
97
+ geometry[name] = _gather(batch[f"geometry_{name}"].to(device), indices)
98
+ if remove_input_targets:
99
+ available[torch.arange(len(available), device=device), indices] = False
100
+ sources[name] = values * available[:, :, None, None, None]
101
+ frame_available[name] = available
102
+ for name in target_specs:
103
+ if name in input_names:
104
+ continue
105
+ available = batch[f"target_frame_available_{name}"].to(device)
106
+ indices = _select_indices(available)
107
+ targets[name] = _gather(batch[f"target_sequence_{name}"].to(device), indices)
108
+ masks[name] = _gather(batch[f"target_pixel_valid_{name}"].to(device), indices)
109
+ target_times[name] = _gather(batch[f"target_timestamps_{name}"].to(device), indices)
110
+ target_periods[name] = torch.stack([
111
+ target_times[name] - 5 * 86_400_000, target_times[name] + 5 * 86_400_000
112
+ ], dim=1)
113
+ geometry[name] = _gather(batch[f"target_geometry_{name}"].to(device), indices)
114
+ return sources, timestamps, frame_available, targets, masks, target_times, target_periods, geometry
115
+
116
+
117
+ def perturb_sources(sources, frame_available, timestamps, support_period, config):
118
+ perturbed, perturbed_available = {}, {}
119
+ source_dropout, frame_dropout = config["train"]["source_dropout"], config["train"]["frame_dropout"]
120
+ for name, values in sources.items():
121
+ output, available = values + 0.01 * torch.randn_like(values), frame_available[name].clone()
122
+ source_mask = torch.rand(values.shape[0], 1, 1, 1, 1, device=values.device) < source_dropout[name]
123
+ strategy = torch.randint(3, (1,), device=values.device).item()
124
+ if strategy == 0:
125
+ dropped = torch.rand_like(available.float()) < frame_dropout[name]
126
+ else:
127
+ midpoint = support_period[:, :1] + (support_period[:, 1:] - support_period[:, :1]) // 2
128
+ dropped = timestamps[name] >= midpoint if strategy == 1 else timestamps[name] < midpoint
129
+ available = available & ~dropped & ~source_mask[:, 0, 0, 0]
130
+ perturbed[name] = output * available[:, :, None, None, None]
131
+ perturbed_available[name] = available
132
+ return perturbed, perturbed_available
133
+
134
+
135
+ def main():
136
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
137
+ torch.manual_seed(config["seed"])
138
+ distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
139
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
140
+ if distributed:
141
+ torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
142
+ rank = torch.distributed.get_rank() if distributed else 0
143
+ device = device_from_config(config, local_rank)
144
+ if device.type == "cuda":
145
+ torch.cuda.set_device(device)
146
+
147
+ dataset = AEFDataset(ROOT / config["data"]["root"] / "train.npz", config)
148
+ sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
149
+ loader = DataLoader(dataset, batch_size=config["train"]["batch_size"], sampler=sampler,
150
+ shuffle=sampler is None, num_workers=config["train"]["num_workers"])
151
+ model = AlphaEarthFoundations(config["data"]["input_sources"], config["data"]["target_sources"], config["model"]).to(device)
152
+ if distributed:
153
+ model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
154
+ optimizer = torch.optim.Adam(model.parameters(), lr=config["train"]["learning_rate"],
155
+ weight_decay=config["train"]["weight_decay"])
156
+ weights = {name: config["train"][f"{name}_weight"] for name in ("reconstruction", "uniformity", "consistency", "text")}
157
+ history = []
158
+ for epoch in range(config["train"]["epochs"]):
159
+ if sampler:
160
+ sampler.set_epoch(epoch)
161
+ model.train()
162
+ totals = {}
163
+ for batch in loader:
164
+ sources, timestamps, frame_available, targets, masks, target_times, target_periods, geometry = unpack(batch, config, device)
165
+ valid_period = batch["valid_period"].to(device)
166
+ teacher = model(sources, timestamps, valid_period, frame_available, target_times, geometry, target_periods)
167
+ perturbed, perturbed_available = perturb_sources(
168
+ sources, frame_available, timestamps, batch["support_period"].to(device), config
169
+ )
170
+ student = model(perturbed, timestamps, valid_period, perturbed_available)
171
+ loss, components = compute_losses(teacher, student, targets, masks, batch["text_target"].to(device),
172
+ config["data"]["target_sources"], weights)
173
+ optimizer.zero_grad(set_to_none=True)
174
+ loss.backward()
175
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
176
+ optimizer.step()
177
+ for name, value in components.items():
178
+ totals[name] = totals.get(name, 0.0) + float(value.detach())
179
+ metrics = {name: value / len(loader) for name, value in totals.items()}
180
+ history.append({"epoch": epoch + 1, **metrics})
181
+ if rank == 0:
182
+ print(f"epoch={epoch + 1} total_loss={metrics['total']:.6f} reconstruction={metrics['reconstruction']:.6f}")
183
+
184
+ if rank == 0:
185
+ checkpoint = ROOT / config["paths"]["checkpoint"]
186
+ metrics_path = ROOT / config["paths"]["training_metrics"]
187
+ checkpoint.parent.mkdir(parents=True, exist_ok=True)
188
+ metrics_path.parent.mkdir(parents=True, exist_ok=True)
189
+ state = model.module.state_dict() if distributed else model.state_dict()
190
+ torch.save({"model": state, "model_config": config["model"], "input_sources": config["data"]["input_sources"],
191
+ "target_sources": config["data"]["target_sources"], "format_version": config["data"]["format_version"]}, checkpoint)
192
+ metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n")
193
+ print(f"checkpoint={checkpoint.relative_to(ROOT)}")
194
+ if distributed:
195
+ torch.distributed.destroy_process_group()
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
weight/.gitkeep ADDED
File without changes