dadadaxi commited on
Commit
f15d29e
·
verified ·
1 Parent(s): a7b8f09

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +22 -10
  2. .gitignore +6 -0
  3. README.md +236 -0
  4. conf/adapter/default.yaml +16 -0
  5. conf/csp.yaml +14 -0
  6. conf/data_module/alex_mp_20.yaml +49 -0
  7. conf/data_module/mp_20.yaml +54 -0
  8. conf/default.yaml +13 -0
  9. conf/finetune.yaml +18 -0
  10. conf/lightning_module/default.yaml +17 -0
  11. conf/lightning_module/diffusion_module/corruption/csp.yaml +15 -0
  12. conf/lightning_module/diffusion_module/corruption/default.yaml +27 -0
  13. conf/lightning_module/diffusion_module/csp.yaml +19 -0
  14. conf/lightning_module/diffusion_module/default.yaml +18 -0
  15. conf/lightning_module/diffusion_module/model/mattergen.yaml +29 -0
  16. conf/lightning_module/diffusion_module/model/property_embeddings/chemical_system.yaml +10 -0
  17. conf/lightning_module/diffusion_module/model/property_embeddings/dft_band_gap.yaml +10 -0
  18. conf/lightning_module/diffusion_module/model/property_embeddings/dft_bulk_modulus.yaml +11 -0
  19. conf/lightning_module/diffusion_module/model/property_embeddings/dft_mag_density.yaml +10 -0
  20. conf/lightning_module/diffusion_module/model/property_embeddings/energy_above_hull.yaml +10 -0
  21. conf/lightning_module/diffusion_module/model/property_embeddings/hhi_score.yaml +10 -0
  22. conf/lightning_module/diffusion_module/model/property_embeddings/ml_bulk_modulus.yaml +11 -0
  23. conf/lightning_module/diffusion_module/model/property_embeddings/space_group.yaml +10 -0
  24. conf/trainer/default.yaml +38 -0
  25. configuration.json +1 -0
  26. csv_to_dataset.py +47 -0
  27. demo/configs/finetune_dft_mag_density_smoke.yaml +18 -0
  28. demo/configs/generate_base.yaml +7 -0
  29. demo/configs/generate_dft_mag_density.yaml +9 -0
  30. demo/configs/train_8dcu.yaml +14 -0
  31. demo/run.sh +61 -0
  32. finetune.py +64 -0
  33. generate.py +44 -0
  34. model/__init__.py +19 -0
  35. model/adapter.py +128 -0
  36. model/common/__init__.py +3 -0
  37. model/common/diffusion/__init__.py +0 -0
  38. model/common/diffusion/corruption.py +284 -0
  39. model/common/diffusion/predictors_correctors.py +98 -0
  40. model/common/gemnet/__init__.py +0 -0
  41. model/common/gemnet/cgmanifest.json +16 -0
  42. model/common/gemnet/gemnet-dT.json +20 -0
  43. model/common/gemnet/gemnet.py +778 -0
  44. model/common/gemnet/gemnet_ctrl.py +268 -0
  45. model/common/gemnet/initializers.py +49 -0
  46. model/common/gemnet/utils.py +299 -0
  47. model/common/globals.py +8 -0
  48. model/common/loss.py +56 -0
  49. model/common/utils/__init__.py +0 -0
  50. model/common/utils/config_utils.py +53 -0
.gitattributes CHANGED
@@ -1,35 +1,47 @@
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
  *.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
+ *.bin.* filter=lfs diff=lfs merge=lfs -text
5
  *.bz2 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
  *.model filter=lfs diff=lfs merge=lfs -text
12
  *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
13
  *.onnx filter=lfs diff=lfs merge=lfs -text
14
  *.ot filter=lfs diff=lfs merge=lfs -text
15
  *.parquet filter=lfs diff=lfs merge=lfs -text
16
  *.pb filter=lfs diff=lfs merge=lfs -text
 
 
17
  *.pt filter=lfs diff=lfs merge=lfs -text
18
  *.pth filter=lfs diff=lfs merge=lfs -text
19
  *.rar filter=lfs diff=lfs merge=lfs -text
 
20
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
21
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
22
  *.tflite filter=lfs diff=lfs merge=lfs -text
23
  *.tgz filter=lfs diff=lfs merge=lfs -text
 
24
  *.xz filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
+ *.zstandard filter=lfs diff=lfs merge=lfs -text
27
+ *.tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.db* filter=lfs diff=lfs merge=lfs -text
29
+ *.ark* filter=lfs diff=lfs merge=lfs -text
30
+ **/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
31
+ **/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
32
+ **/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
33
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
34
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
35
+ *.gguf* filter=lfs diff=lfs merge=lfs -text
36
+ *.ggml filter=lfs diff=lfs merge=lfs -text
37
+ *.llamafile* filter=lfs diff=lfs merge=lfs -text
38
+ *.pt2 filter=lfs diff=lfs merge=lfs -text
39
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
40
+ *.npy filter=lfs diff=lfs merge=lfs -text
41
+ *.npz filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+ *.tar filter=lfs diff=lfs merge=lfs -text
45
+ *.wasm filter=lfs diff=lfs merge=lfs -text
46
  *.zst filter=lfs diff=lfs merge=lfs -text
47
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+
4
+ outputs/
5
+ demo/slurm_*.out
6
+ demo/slurm_*.err
README.md ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - ""
4
+ language:
5
+ - en
6
+ license: mit
7
+ tags:
8
+ - OneScience
9
+ - MatterGen
10
+ - materials-science
11
+ - crystal-generation
12
+ - diffusion-model
13
+ - graph-neural-network
14
+ - conditional-generation
15
+ - training
16
+ - fine-tuning
17
+ tasks: []
18
+ ---
19
+
20
+ <p align="center">
21
+ <strong>
22
+ <span style="font-size: 30px;">MatterGen</span>
23
+ </strong>
24
+ </p>
25
+
26
+ # Model Introduction
27
+
28
+ MatterGen is a generative model for inorganic materials developed by Microsoft Research. It jointly generates elemental compositions, unit cells, and periodic atomic coordinates, and supports candidate crystal generation conditioned on material properties.
29
+
30
+ Paper: *MatterGen: a generative model for inorganic materials design*<br>
31
+ Reference implementation: https://github.com/microsoft/mattergen
32
+
33
+ # Model Description
34
+
35
+ MatterGen uses a diffusion model to learn distributions over atomic species, fractional coordinates, and crystal lattices. It supports unconditional crystal generation and conditional generation based on properties such as chemical system, space group, magnetic density, band gap, and bulk modulus.
36
+
37
+ This repository contains the OneScience-adapted MatterGen model, diffusion and sampling code, and entry points for training, property fine-tuning, crystal generation, and data conversion. Model code is located in `model/`; data processing, general-purpose network layers, and property embeddings are provided by OneScience MatChem.
38
+
39
+ # Use Cases
40
+
41
+ | Use case | Description |
42
+ | :---: | :--- |
43
+ | Unconditional crystal generation | Generate new candidate crystal structures with the base checkpoint |
44
+ | Property-conditioned generation | Generate structures for target properties such as magnetic density, band gap, and bulk modulus |
45
+ | Fixed-composition structure prediction | Search for possible crystal structures for a specified composition |
46
+ | Training from scratch | Train with MP-20 or data compatible with the MatterGen cache format |
47
+ | Property fine-tuning | Add a property adapter to a pretrained model and fine-tune it |
48
+ | Environment connectivity check | Verify OneScience, the checkpoint, and GPU/DCU availability by generating one sample |
49
+
50
+ # Usage
51
+
52
+ ## 1. Using OneCode
53
+
54
+ Try intelligent, one-click AI4S programming in the OneCode online environment:
55
+
56
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
57
+
58
+ ## 2. Manual Installation and Usage
59
+
60
+ **Hardware requirements**
61
+
62
+ - A GPU or DCU is recommended.
63
+ - A CPU can be used for import and configuration connectivity checks, but full generation and training will be slow.
64
+ - A DCU requires DTK to match the PyTorch build. Single-sample generation has been verified with DTK 25.04.2 and `torch 2.5.1+das.opt1.dtk25042`.
65
+
66
+ ### Download the Model Package
67
+
68
+ ```bash
69
+ hf download --model OneScience-Sugon/Mattergen --local-dir ./Mattergen
70
+ cd Mattergen
71
+ ```
72
+
73
+ ### Install the Runtime Environment
74
+
75
+ **DCU environment**
76
+
77
+ ```bash
78
+ # Activate DTK and conda first
79
+ module load compiler/dtk/25.04.2
80
+ conda create -n onescience311 python=3.11 -y
81
+ conda activate onescience311
82
+ # uv installation is also supported
83
+ pip install onescience[matchem-dcu] \
84
+ -i http://mirrors.onescience.ai:3141/pypi/simple/ \
85
+ --trusted-host mirrors.onescience.ai
86
+ ```
87
+
88
+ **GPU environment**
89
+
90
+ ```bash
91
+ # Activate conda first
92
+ conda create -n onescience311 python=3.11 -y \
93
+ libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
94
+ conda activate onescience311
95
+ # uv installation is also supported
96
+ pip install onescience[matchem-gpu] \
97
+ -i http://mirrors.onescience.ai:3141/pypi/simple/ \
98
+ --trusted-host mirrors.onescience.ai
99
+ ```
100
+
101
+ ### Training Data
102
+
103
+ MatterGen training uses a converted data cache. The MP-20 dataset is available on Hugging Face and can be downloaded directly:
104
+
105
+ ```bash
106
+ hf download --dataset OneScience-Sugon/mp20 --local-dir ./datasets/mp20
107
+ ```
108
+
109
+ The downloaded MatterGen cache is located at:
110
+
111
+ ```text
112
+ datasets/mp20/data/MP20/cache/mp_20/
113
+ ├── train/
114
+ ├── val/
115
+ └── test/
116
+ ```
117
+
118
+ The default training YAML cannot use the downloaded data until you add its actual path under `args`. For example, update `demo/configs/train_8dcu.yaml` as follows:
119
+
120
+ ```yaml
121
+ args:
122
+ data_module: mp_20
123
+ data_module.root_dir: ./datasets/mp20/data/MP20/cache/mp_20
124
+ ```
125
+
126
+ For property fine-tuning, also add `data_module.root_dir` under `args` in `demo/configs/finetune_dft_mag_density_smoke.yaml`. If you downloaded the data elsewhere, use the corresponding absolute path.
127
+
128
+ Convert custom CSV data first:
129
+
130
+ ```bash
131
+ python csv_to_dataset.py \
132
+ --csv-folder /path/to/csv_folder \
133
+ --dataset-name my_dataset \
134
+ --cache-folder ./datasets/cache
135
+ ```
136
+
137
+ ### Trained Weights
138
+
139
+ After downloading the model package with the Hugging Face command above, the pretrained checkpoints are located in `weight/`:
140
+
141
+ ```text
142
+ weight/
143
+ ├── mattergen_base/
144
+ │ ├── config.yaml
145
+ │ └── checkpoints/
146
+ │ └── last.ckpt
147
+ └── dft_mag_density/
148
+ ├── config.yaml
149
+ └── checkpoints/
150
+ └── last.ckpt
151
+ ```
152
+
153
+ The repository also provides property-conditioned checkpoints for `chemical_system`, `dft_band_gap`, `dft_mag_density`, `ml_bulk_modulus`, and `space_group`. When using a demo YAML, set `checkpoint` in `demo/configs/generate_base.yaml` or `demo/configs/generate_dft_mag_density.yaml` to the corresponding `./weight/<model_name>` path. For fine-tuning, update `adapter.model_path` in `demo/configs/finetune_dft_mag_density_smoke.yaml`.
154
+
155
+ ### Inference
156
+
157
+ Generate one crystal structure unconditionally:
158
+
159
+ ```bash
160
+ python generate.py \
161
+ --checkpoint ./weight/mattergen_base \
162
+ --output outputs/generate/mattergen_base \
163
+ --batch-size 1 \
164
+ --num-batches 1
165
+ ```
166
+
167
+ The generated outputs include:
168
+
169
+ ```text
170
+ outputs/generate/mattergen_base/
171
+ ├── generated_crystals.extxyz
172
+ └── generated_crystals_cif.zip
173
+ ```
174
+
175
+ Magnetic-density-conditioned generation requires the matching property checkpoint:
176
+
177
+ ```bash
178
+ python generate.py \
179
+ --checkpoint ./weight/dft_mag_density \
180
+ --output outputs/generate/dft_mag_density \
181
+ --batch-size 1 \
182
+ --num-batches 1 \
183
+ --properties '{"dft_mag_density": 0.15}'
184
+ ```
185
+
186
+ You can also use the demo YAML files:
187
+
188
+ ```bash
189
+ cd demo
190
+ bash run.sh --config configs/generate_base.yaml
191
+ bash run.sh --config configs/generate_dft_mag_density.yaml
192
+ ```
193
+
194
+ ### Training
195
+
196
+ Submit an eight-DCU training job with the MP-20 cache:
197
+
198
+ ```bash
199
+ cd demo
200
+ bash run.sh --config configs/train_8dcu.yaml --submit
201
+ ```
202
+
203
+ You can also launch training directly from the repository root with Hydra arguments:
204
+
205
+ ```bash
206
+ python train.py \
207
+ data_module=mp_20 \
208
+ data_module.root_dir=./datasets/mp20/data/MP20/cache/mp_20 \
209
+ trainer.devices=1 \
210
+ data_module.batch_size.train=4
211
+ ```
212
+
213
+ ### Fine-Tuning
214
+
215
+ The repository provides a smoke-test configuration for magnetic-density property fine-tuning. By default, it runs only one training batch and one validation batch:
216
+
217
+ ```bash
218
+ cd demo
219
+ bash run.sh --config configs/finetune_dft_mag_density_smoke.yaml
220
+ ```
221
+
222
+ For a full fine-tuning run, copy the YAML and update `adapter.model_path`, `data_module.properties`, the property embedding, batch size, and number of epochs. Remove `trainer.limit_train_batches` and `trainer.limit_val_batches`.
223
+
224
+ # Official OneScience Resources
225
+
226
+ | Platform | OneScience Main Repository | Skills Repository |
227
+ | --- | --- | --- |
228
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
229
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
230
+
231
+ ---
232
+
233
+ # Citation and License
234
+
235
+ - The MatterGen-related code comes from the MatChem examples in the OneScience project and refers to the upstream MatterGen project (https://github.com/microsoft/mattergen). The upstream MatterGen code is released under the [MIT License](https://github.com/microsoft/mattergen/blob/main/LICENSE).
236
+ - If you use MatterGen training or generation results in research, please cite the original MatterGen paper, the relevant OneScience projects, and the datasets used.
conf/adapter/default.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pretrained_name: null
2
+ model_path: ${oc.env:ONESCIENCE_MODELS_DIR,/public/share/sugonhpcapp01/onestore/onemodels}/mattergen/mattergen_base
3
+ load_epoch: last
4
+ full_finetuning: true
5
+
6
+ adapter:
7
+ # these arguments are used to initialize GemNetTAdapter
8
+ # more args are added by the finetuning script during runtime
9
+ _target_: model.adapter.GemNetTAdapter
10
+ property_embeddings_adapt: {}
11
+
12
+ defaults: []
13
+ # path/to/config_dir@attribute.name: config_file_name
14
+ ## e.g., insert values from dft_bulk_modulus.yaml in /lightning_module/diffusion_module/model/property_embeddings/
15
+ ## into adapter.property_embeddings_adapt[dft_bulk_modulus]
16
+ # - /lightning_module/diffusion_module/model/property_embeddings@adapter.property_embeddings_adapt.dft_bulk_modulus: dft_bulk_modulus
conf/csp.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hydra:
2
+ run:
3
+ dir: ${oc.env:OUTPUT_DIR,outputs/singlerun/${now:%Y-%m-%d}/${now:%H-%M-%S}}
4
+
5
+
6
+ auto_resume: true
7
+
8
+ defaults:
9
+ - data_module: mp_20
10
+ - trainer: default
11
+ - lightning_module: default
12
+ - lightning_module/diffusion_module: csp
13
+ - lightning_module/diffusion_module/model: mattergen
14
+ - lightning_module/diffusion_module/corruption: csp
conf/data_module/alex_mp_20.yaml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.datapipes.materials.mattergen.datamodule.CrystDataModule
2
+ _recursive_: true
3
+ properties: []
4
+ # Supported properties:
5
+ # - dft_bulk_modulus
6
+ # - dft_band_gap
7
+ # - dft_mag_density
8
+ # - ml_bulk_modulus
9
+ # - hhi_score
10
+ # - space_group
11
+ # - energy_above_hull
12
+
13
+ dataset_transforms:
14
+ - _target_: onescience.datapipes.materials.mattergen.dataset_transform.filter_sparse_properties
15
+ _partial_: true
16
+
17
+ transforms:
18
+ - _target_: onescience.datapipes.materials.mattergen.transform.symmetrize_lattice
19
+ _partial_: true
20
+ - _target_: onescience.datapipes.materials.mattergen.transform.set_chemical_system_string
21
+ _partial_: true
22
+
23
+ average_density: 0.05771451654022283 # atoms/Angstrom**3 : this is used in models/scripts/run.py to set lattice_limit_density
24
+ root_dir: ${oc.env:ONESCIENCE_DATASETS_DIR,/public/share/sugonhpcapp01/onestore/onedatasets}/matchem/mattergen/cache/alex_mp_20
25
+
26
+ train_dataset:
27
+ _target_: onescience.datapipes.materials.mattergen.dataset.CrystalDataset.from_cache_path
28
+ cache_path: ${data_module.root_dir}/train
29
+ properties: ${data_module.properties}
30
+ transforms: ${data_module.transforms}
31
+ dataset_transforms: ${data_module.dataset_transforms}
32
+
33
+ val_dataset:
34
+ _target_: onescience.datapipes.materials.mattergen.dataset.CrystalDataset.from_cache_path
35
+ cache_path: ${data_module.root_dir}/val
36
+ properties: ${data_module.properties}
37
+ transforms: ${data_module.transforms}
38
+ dataset_transforms: ${data_module.dataset_transforms}
39
+
40
+ num_workers:
41
+ train: 0
42
+ val: 0
43
+
44
+ batch_size:
45
+ # total batch size of 512, adjust for number of devices, nodes, and gradient accumulation
46
+ train: ${eval:'(512 // ${trainer.accumulate_grad_batches}) // (${trainer.devices} * ${trainer.num_nodes})'}
47
+ val: ${eval:'(512 // ${trainer.accumulate_grad_batches}) // (${trainer.devices} * ${trainer.num_nodes})'}
48
+
49
+ max_epochs: 2200
conf/data_module/mp_20.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.datapipes.materials.mattergen.datamodule.CrystDataModule
2
+ _recursive_: true
3
+ properties: []
4
+ # Supported properties:
5
+ # - dft_bulk_modulus
6
+ # - dft_band_gap
7
+ # - dft_mag_density
8
+
9
+ transforms:
10
+ - _target_: onescience.datapipes.materials.mattergen.transform.symmetrize_lattice
11
+ _partial_: true
12
+ - _target_: onescience.datapipes.materials.mattergen.transform.set_chemical_system_string
13
+ _partial_: true
14
+
15
+ dataset_transforms:
16
+ - _target_: onescience.datapipes.materials.mattergen.dataset_transform.filter_sparse_properties
17
+ _partial_: true
18
+
19
+ average_density: 0.05771451654022283 # atoms/Angstrom**3 : this is used in models/scripts/run.py to set lattice_limit_density
20
+ root_dir: ${oc.env:ONESCIENCE_DATASETS_DIR}/matchem/mattergen/cache/mp_20
21
+
22
+ train_dataset:
23
+ _target_: onescience.datapipes.materials.mattergen.dataset.CrystalDataset.from_cache_path
24
+ cache_path: ${data_module.root_dir}/train
25
+ properties: ${data_module.properties}
26
+ transforms: ${data_module.transforms}
27
+ dataset_transforms: ${data_module.dataset_transforms}
28
+
29
+ val_dataset:
30
+ _target_: onescience.datapipes.materials.mattergen.dataset.CrystalDataset.from_cache_path
31
+ cache_path: ${data_module.root_dir}/val
32
+ properties: ${data_module.properties}
33
+ transforms: ${data_module.transforms}
34
+ dataset_transforms: ${data_module.dataset_transforms}
35
+
36
+ test_dataset:
37
+ _target_: onescience.datapipes.materials.mattergen.dataset.CrystalDataset.from_cache_path
38
+ cache_path: ${data_module.root_dir}/test
39
+ properties: ${data_module.properties}
40
+ transforms: ${data_module.transforms}
41
+ dataset_transforms: ${data_module.dataset_transforms}
42
+
43
+ num_workers:
44
+ train: 0
45
+ val: 0
46
+ test: 0
47
+
48
+ batch_size:
49
+ # total batch size of 512, adjust for number of devices, nodes, and gradient accumulation
50
+ train: ${eval:'(512 // ${trainer.accumulate_grad_batches}) // (${trainer.devices} * ${trainer.num_nodes})'}
51
+ val: ${eval:'(64 // (${trainer.devices} * ${trainer.num_nodes}))'}
52
+ test: ${eval:'(64 // (${trainer.devices} * ${trainer.num_nodes}))'}
53
+
54
+ max_epochs: 900
conf/default.yaml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hydra:
2
+ run:
3
+ dir: ${oc.env:OUTPUT_DIR,outputs/singlerun/${now:%Y-%m-%d}/${now:%H-%M-%S}}
4
+
5
+ auto_resume: True
6
+
7
+ defaults:
8
+ - data_module: mp_20
9
+ - trainer: default
10
+ - lightning_module: default
11
+ - lightning_module/diffusion_module: default
12
+ - lightning_module/diffusion_module/model: mattergen
13
+ - lightning_module/diffusion_module/corruption: default
conf/finetune.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hydra:
2
+ run:
3
+ dir: ${oc.env:OUTPUT_DIR,outputs/singlerun/${now:%Y-%m-%d}/${now:%H-%M-%S}}
4
+
5
+ defaults:
6
+ - data_module: mp_20
7
+ - trainer: default
8
+ - lightning_module: default
9
+ - adapter: default
10
+
11
+ trainer:
12
+ max_epochs: 200
13
+ logger:
14
+ job_type: train_finetune # override default defined in defaults.trainer yaml file
15
+
16
+ lightning_module:
17
+ optimizer_partial:
18
+ lr: 5e-6
conf/lightning_module/default.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: model.diffusion.lightning_module.DiffusionLightningModule
2
+ optimizer_partial:
3
+ lr: 1e-4
4
+ _target_: torch.optim.Adam
5
+ _partial_: true
6
+ scheduler_partials:
7
+ - scheduler:
8
+ _target_: torch.optim.lr_scheduler.ReduceLROnPlateau
9
+ factor: 0.6
10
+ patience: 100
11
+ min_lr: 1e-6
12
+ verbose: true
13
+ _partial_: true
14
+ interval: epoch
15
+ frequency: 1
16
+ monitor: loss_train
17
+ strict: true
conf/lightning_module/diffusion_module/corruption/csp.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: model.diffusion.corruption.multi_corruption.MultiCorruption
2
+ sdes:
3
+ pos:
4
+ _target_: model.common.diffusion.corruption.NumAtomsVarianceAdjustedWrappedVESDE
5
+ wrapping_boundary: 1.0
6
+ sigma_max: 5.0
7
+ limit_info_key: num_atoms
8
+
9
+ cell:
10
+ _target_: model.common.diffusion.corruption.LatticeVPSDE.from_vpsde_config
11
+ vpsde_config:
12
+ beta_min: 0.1
13
+ beta_max: 20
14
+ limit_density: ${data_module.average_density}
15
+ limit_var_scaling_constant: 0.25
conf/lightning_module/diffusion_module/corruption/default.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: model.diffusion.corruption.multi_corruption.MultiCorruption
2
+ sdes:
3
+ pos:
4
+ _target_: model.common.diffusion.corruption.NumAtomsVarianceAdjustedWrappedVESDE
5
+ wrapping_boundary: 1.0
6
+ sigma_max: 5.0
7
+ limit_info_key: num_atoms
8
+
9
+ cell:
10
+ _target_: model.common.diffusion.corruption.LatticeVPSDE.from_vpsde_config
11
+ vpsde_config:
12
+ beta_min: 0.1
13
+ beta_max: 20
14
+ limit_density: ${data_module.average_density}
15
+ limit_var_scaling_constant: 0.25
16
+
17
+ discrete_corruptions:
18
+ atomic_numbers:
19
+ _target_: model.diffusion.corruption.d3pm_corruption.D3PMCorruption
20
+ offset: 1
21
+ d3pm:
22
+ _target_: model.diffusion.d3pm.d3pm.MaskDiffusion
23
+ dim: 101
24
+ schedule:
25
+ _target_: model.diffusion.d3pm.d3pm.create_discrete_diffusion_schedule
26
+ kind: standard
27
+ num_steps: 1000
conf/lightning_module/diffusion_module/csp.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: model.diffusion.diffusion_module.DiffusionModule
2
+ model: mattergen
3
+ corruption: csp
4
+
5
+ loss_fn:
6
+ _target_: model.common.loss.MaterialsLoss
7
+ reduce: sum
8
+ include_pos: True
9
+ include_cell: True
10
+ include_atomic_numbers: False
11
+ weights:
12
+ cell: 1.0
13
+ pos: 0.1
14
+
15
+
16
+ pre_corruption_fn:
17
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.SetEmbeddingType
18
+ p_unconditional: 0.2
19
+ dropout_fields_iid: false
conf/lightning_module/diffusion_module/default.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: model.diffusion.diffusion_module.DiffusionModule
2
+ loss_fn:
3
+ _target_: model.common.loss.MaterialsLoss
4
+ reduce: sum
5
+ include_pos: True
6
+ include_cell: True
7
+ include_atomic_numbers: True
8
+ d3pm_hybrid_lambda: 0.01
9
+ weights:
10
+ cell: 1.0
11
+ pos: 0.1
12
+ atomic_numbers: 1.0
13
+ model: mattergen
14
+ corruption: default
15
+ pre_corruption_fn:
16
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.SetEmbeddingType
17
+ p_unconditional: 0.2
18
+ dropout_fields_iid: false
conf/lightning_module/diffusion_module/model/mattergen.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: model.denoiser.GemNetTDenoiser
2
+ hidden_dim: 512
3
+ gemnet:
4
+ _target_: model.common.gemnet.gemnet.GemNetT
5
+ num_targets: 1
6
+ latent_dim: ${eval:'${..hidden_dim} * (1 + len(${..property_embeddings}))'} # 1 is for time encoding.
7
+ atom_embedding:
8
+ _target_: onescience.modules.layer.mattergen.embedding_block.AtomEmbedding
9
+ emb_size: ${...hidden_dim}
10
+ with_mask_type: ${eval:'${...denoise_atom_types} and "${...atom_type_diffusion}" == "mask"'}
11
+ emb_size_atom: ${..hidden_dim}
12
+ emb_size_edge: ${..hidden_dim}
13
+ max_neighbors: 50
14
+ max_cell_images_per_dim: 5
15
+ cutoff: 7.
16
+ num_blocks: 4
17
+ regress_stress: true
18
+ otf_graph: true
19
+ scale_file: ${oc.env:PROJECT_ROOT}/common/gemnet/gemnet-dT.json
20
+ denoise_atom_types: true
21
+ atom_type_diffusion: mask
22
+ property_embeddings_adapt: {}
23
+ property_embeddings: {}
24
+ defaults: [] # NOTE: to train a conditional model, unccoment entries such as property_embeddings@property_embeddings.chemical_system: chemical_system below and edit/add properties to the defaults list as desired.
25
+ # see https://stackoverflow.com/questions/71356361/selecting-multiple-configs-from-a-config-group-in-hydra-without-using-an-explici
26
+ # add via config override: +lightning_module/diffusion_module/model/property_embeddings@lightning_module.diffusion_module.model.property_embeddings.dft_bulk_modulus=dft_bulk_modulus
27
+ # delete via config override: ~lightning_module/diffusion_module/model/property_embeddings@lightning_module.diffusion_module.model.property_embeddings.chemical_system
28
+ # - property_embeddings@property_embeddings.chemical_system: chemical_system
29
+ # - property_embeddings@property_embeddings.dft_bulk_modulus: dft_bulk_modulus
conf/lightning_module/diffusion_module/model/property_embeddings/chemical_system.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: chemical_system
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.ChemicalSystemMultiHotEmbedding
8
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: torch.nn.Identity
conf/lightning_module/diffusion_module/model/property_embeddings/dft_band_gap.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: dft_band_gap
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: model.diffusion.model_utils.NoiseLevelEncoding
8
+ d_model: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: model.common.utils.data_utils.StandardScalerTorch
conf/lightning_module/diffusion_module/model/property_embeddings/dft_bulk_modulus.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: dft_bulk_modulus
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: model.diffusion.model_utils.NoiseLevelEncoding
8
+ d_model: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: model.common.utils.data_utils.StandardScalerTorch
11
+ log10_transform: true
conf/lightning_module/diffusion_module/model/property_embeddings/dft_mag_density.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: dft_mag_density
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: model.diffusion.model_utils.NoiseLevelEncoding
8
+ d_model: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: model.common.utils.data_utils.StandardScalerTorch
conf/lightning_module/diffusion_module/model/property_embeddings/energy_above_hull.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: energy_above_hull
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: model.diffusion.model_utils.NoiseLevelEncoding
8
+ d_model: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: model.common.utils.data_utils.StandardScalerTorch
conf/lightning_module/diffusion_module/model/property_embeddings/hhi_score.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: hhi_score
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: model.diffusion.model_utils.NoiseLevelEncoding
8
+ d_model: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: model.common.utils.data_utils.StandardScalerTorch
conf/lightning_module/diffusion_module/model/property_embeddings/ml_bulk_modulus.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: ml_bulk_modulus
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: model.diffusion.model_utils.NoiseLevelEncoding
8
+ d_model: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: model.common.utils.data_utils.StandardScalerTorch
11
+ log10_transform: true
conf/lightning_module/diffusion_module/model/property_embeddings/space_group.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.PropertyEmbedding
2
+ name: space_group
3
+ unconditional_embedding_module:
4
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.EmbeddingVector
5
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
6
+ conditional_embedding_module:
7
+ _target_: onescience.modules.embedding.mattergen_property_embeddings.SpaceGroupEmbeddingVector
8
+ hidden_dim: ${lightning_module.diffusion_module.model.hidden_dim}
9
+ scaler:
10
+ _target_: torch.nn.Identity
conf/trainer/default.yaml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: pytorch_lightning.Trainer
2
+ accelerator: 'gpu'
3
+ devices: 1
4
+ num_nodes: 1
5
+ precision: 32
6
+ max_epochs: ${data_module.max_epochs}
7
+ accumulate_grad_batches: 1
8
+ gradient_clip_val: 0.5
9
+ gradient_clip_algorithm: value
10
+ check_val_every_n_epoch: 5
11
+ strategy:
12
+ _target_: pytorch_lightning.strategies.ddp.DDPStrategy
13
+ find_unused_parameters: true
14
+
15
+ logger:
16
+ _target_: pytorch_lightning.loggers.WandbLogger
17
+ project: crystal-generation
18
+ job_type: train
19
+ settings:
20
+ _target_: wandb.Settings
21
+ start_method: fork
22
+ _save_requirements: False
23
+
24
+ callbacks:
25
+ - _target_: pytorch_lightning.callbacks.LearningRateMonitor
26
+ logging_interval: step
27
+ log_momentum: False
28
+ - _target_: pytorch_lightning.callbacks.ModelCheckpoint
29
+ monitor: loss_val
30
+ mode: min
31
+ save_top_k: 1
32
+ save_last: True
33
+ verbose: false
34
+ every_n_epochs: 1
35
+ filename: "{epoch}-{loss_val:.2f}"
36
+ - _target_: pytorch_lightning.callbacks.TQDMProgressBar
37
+ refresh_rate: 50
38
+ - _target_: onescience.datapipes.materials.mattergen.callback.SetPropertyScalers
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
csv_to_dataset.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert MatterGen CSV splits into cached CrystalDataset directories."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from onescience.datapipes.materials.mattergen import CrystalDataset
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(
11
+ description="Convert every CSV file in a directory to MatterGen cache format."
12
+ )
13
+ parser.add_argument(
14
+ "--csv-folder",
15
+ type=Path,
16
+ required=True,
17
+ help="Directory containing split files such as train.csv and val.csv.",
18
+ )
19
+ parser.add_argument(
20
+ "--dataset-name",
21
+ required=True,
22
+ help="Dataset directory name created below --cache-folder.",
23
+ )
24
+ parser.add_argument(
25
+ "--cache-folder",
26
+ type=Path,
27
+ required=True,
28
+ help="Parent directory in which the dataset cache is created.",
29
+ )
30
+ args = parser.parse_args()
31
+
32
+ if not args.csv_folder.is_dir():
33
+ parser.error(f"CSV directory does not exist: {args.csv_folder}")
34
+
35
+ csv_files = sorted(args.csv_folder.glob("*.csv"))
36
+ if not csv_files:
37
+ parser.error(f"No CSV files found in: {args.csv_folder}")
38
+
39
+ dataset_root = args.cache_folder / args.dataset_name
40
+ for csv_path in csv_files:
41
+ cache_path = dataset_root / csv_path.stem
42
+ print(f"Processing {csv_path} -> {cache_path}")
43
+ CrystalDataset.from_csv(csv_path=csv_path, cache_path=cache_path)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
demo/configs/finetune_dft_mag_density_smoke.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 属性微调 smoke test。修改属性和 batch size。
2
+ task: finetune
3
+ args:
4
+ adapter.model_path: ${ONESCIENCE_MODELS_DIR}/mattergen/mattergen_base
5
+ data_module: mp_20
6
+ data_module.properties: '[dft_mag_density]'
7
+ +lightning_module/diffusion_module/model/property_embeddings@adapter.adapter.property_embeddings_adapt.dft_mag_density: dft_mag_density
8
+ trainer.devices: 1
9
+ trainer.num_nodes: 1
10
+ trainer.max_epochs: 1
11
+ trainer.check_val_every_n_epoch: 1
12
+ +trainer.limit_train_batches: 1
13
+ +trainer.limit_val_batches: 1
14
+ +trainer.num_sanity_val_steps: 0
15
+ data_module.batch_size.train: 1
16
+ data_module.batch_size.val: 1
17
+ ~trainer.logger: true
18
+ ~trainer.strategy: true
demo/configs/generate_base.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # 基础模型单样本生成。修改 checkpoint、output、batch_size 和 num_batches。
2
+ task: generate
3
+ args:
4
+ checkpoint: ${ONESCIENCE_MODELS_DIR}/mattergen/mattergen_base
5
+ output: outputs/generate/mattergen_base
6
+ batch_size: 1
7
+ num_batches: 1
demo/configs/generate_dft_mag_density.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # 磁密度条件生成。checkpoint 必须与 properties 中的属性匹配。
2
+ task: generate
3
+ args:
4
+ checkpoint: ${ONESCIENCE_MODELS_DIR}/mattergen/dft_mag_density
5
+ output: outputs/generate/dft_mag_density
6
+ batch_size: 1
7
+ num_batches: 1
8
+ properties:
9
+ dft_mag_density: 0.15
demo/configs/train_8dcu.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 8 卡从头训练。优先修改 batch size 和梯度累积。
2
+ task: train
3
+ args:
4
+ data_module: mp_20
5
+ trainer.devices: 8
6
+ trainer.num_nodes: 1
7
+ trainer.accumulate_grad_batches: 16
8
+ data_module.batch_size.train: 4
9
+ data_module.batch_size.val: 4
10
+ data_module.batch_size.test: 4
11
+ data_module.num_workers.train: 2
12
+ data_module.num_workers.val: 2
13
+ data_module.num_workers.test: 2
14
+ ~trainer.logger: true
demo/run.sh ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ CONFIG=""
5
+ MODE="run"
6
+ while [[ $# -gt 0 ]]; do
7
+ case "$1" in
8
+ --config) CONFIG="$2"; shift 2 ;;
9
+ --config=*) CONFIG="${1#*=}"; shift ;;
10
+ --submit) MODE="submit"; shift ;;
11
+ -h|--help) echo "Usage: bash run.sh --config configs/<name>.yaml [--submit]"; exit 0 ;;
12
+ *) echo "Unknown argument: $1" >&2; exit 2 ;;
13
+ esac
14
+ done
15
+ [[ -n "$CONFIG" ]] || { echo "Please specify --config configs/<name>.yaml" >&2; exit 2; }
16
+ [[ "$CONFIG" = /* ]] || CONFIG="$DEMO_DIR/$CONFIG"
17
+ [[ -f "$CONFIG" ]] || { echo "Config not found: $CONFIG" >&2; exit 2; }
18
+ if [[ -z "${CONDA_PREFIX:-}" || -z "${ONESCIENCE_MODELS_DIR:-}" ]]; then
19
+ export MATCHEM_CONDA_NAME="${MATCHEM_CONDA_NAME:-onescience-mattergen-source}"
20
+ source "$DEMO_DIR/../../matchem_env.sh"
21
+ fi
22
+ CMD=$(python3 - "$CONFIG" "$DEMO_DIR/.." <<'PY'
23
+ import os, shlex, sys, yaml
24
+ import json
25
+ cfg = yaml.safe_load(open(sys.argv[1]))
26
+ root = os.path.abspath(sys.argv[2])
27
+ task = cfg.get("task", "train")
28
+ script = {"train": "train.py", "finetune": "finetune.py", "generate": "generate.py"}.get(task)
29
+ if not script: raise SystemExit(f"Unsupported task: {task}")
30
+ args = []
31
+ for key, value in cfg.get("args", {}).items():
32
+ if isinstance(value, bool):
33
+ if value: args.append(f"--{key.replace('_', '-')}")
34
+ elif value is not None:
35
+ value = json.dumps(value) if isinstance(value, (dict, list)) else str(value)
36
+ value = os.path.expandvars(value)
37
+ args.extend([f"--{key.replace('_', '-')}", value] if task == "generate" else [f"{key}={value}"])
38
+ print(" ".join(map(shlex.quote, [sys.executable, os.path.join(root, script), *args])))
39
+ PY
40
+ )
41
+ echo "Running MatterGen: $CMD"
42
+ if [[ "$MODE" == "submit" ]]; then
43
+ TASK=$(python3 - "$CONFIG" <<'PY'
44
+ import sys, yaml
45
+ print(yaml.safe_load(open(sys.argv[1])).get("task", "train"))
46
+ PY
47
+ )
48
+ [[ "$TASK" == "train" ]] || { echo "--submit currently supports train configs only" >&2; exit 2; }
49
+ DEVICES=$(python3 - "$CONFIG" <<'PY'
50
+ import sys, yaml
51
+ print(yaml.safe_load(open(sys.argv[1])).get("args", {}).get("trainer.devices", 8))
52
+ PY
53
+ )
54
+ sbatch \
55
+ --gres="dcu:$DEVICES" \
56
+ --export="ALL,CONFIG_PATH=$CONFIG,SCRIPT_DIR=$DEMO_DIR/.." \
57
+ "$DEMO_DIR/../submit_train.sh"
58
+ exit 0
59
+ fi
60
+ cd "$DEMO_DIR/.."
61
+ eval "$CMD"
finetune.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fine-tune MatterGen with a pretrained checkpoint and property adapter."""
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import hydra
9
+ import omegaconf
10
+ import pytorch_lightning as pl
11
+ import torch
12
+ from omegaconf import OmegaConf, open_dict
13
+ from pytorch_lightning.cli import SaveConfigCallback
14
+
15
+ from model.diffusion.run import (
16
+ AddConfigCallback,
17
+ SimpleParser,
18
+ maybe_instantiate,
19
+ )
20
+ from model.finetune import (
21
+ init_adapter_lightningmodule_from_pretrained,
22
+ )
23
+
24
+ EXAMPLE_DIR = Path(__file__).resolve().parent
25
+ os.environ.setdefault(
26
+ "OUTPUT_DIR",
27
+ str(EXAMPLE_DIR / "outputs" / "finetune" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
28
+ )
29
+
30
+
31
+ @hydra.main(
32
+ config_path=str(EXAMPLE_DIR / "conf"),
33
+ config_name="finetune",
34
+ version_base="1.1",
35
+ )
36
+ def main(cfg: omegaconf.DictConfig) -> None:
37
+ """Build the data, adapter model, and Trainer, then start fine-tuning."""
38
+ torch.set_float32_matmul_precision("high")
39
+ trainer: pl.Trainer = maybe_instantiate(cfg.trainer, pl.Trainer)
40
+ datamodule: pl.LightningDataModule = maybe_instantiate(
41
+ cfg.data_module, pl.LightningDataModule
42
+ )
43
+
44
+ model, lightning_module_cfg = init_adapter_lightningmodule_from_pretrained(
45
+ cfg.adapter, cfg.lightning_module
46
+ )
47
+ with open_dict(cfg):
48
+ cfg.lightning_module = lightning_module_cfg
49
+
50
+ resolved_config = OmegaConf.to_container(cfg, resolve=True)
51
+ print(json.dumps(resolved_config, indent=4))
52
+ trainer.callbacks.append(
53
+ SaveConfigCallback(
54
+ parser=SimpleParser(),
55
+ config=resolved_config,
56
+ overwrite=True,
57
+ )
58
+ )
59
+ trainer.callbacks.append(AddConfigCallback(resolved_config))
60
+ trainer.fit(model=model, datamodule=datamodule, ckpt_path=None)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
generate.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from model.common.utils.data_classes import MatterGenCheckpointInfo
7
+ from model.generator import CrystalGenerator
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(description="Generate crystals with MatterGen")
12
+ parser.add_argument("--checkpoint", required=True)
13
+ parser.add_argument("--output", default="outputs/mattergen")
14
+ parser.add_argument("--batch-size", type=int, default=1)
15
+ parser.add_argument("--num-batches", type=int, default=1)
16
+ parser.add_argument(
17
+ "--properties",
18
+ type=json.loads,
19
+ default=None,
20
+ help='Condition values as JSON, for example {"dft_mag_density": 0.15}.',
21
+ )
22
+ parser.add_argument("--record-trajectories", action="store_true")
23
+ args = parser.parse_args()
24
+ if not os.path.isdir(args.checkpoint):
25
+ parser.error(f"checkpoint directory does not exist: {args.checkpoint}")
26
+ checkpoint_info = MatterGenCheckpointInfo(
27
+ model_path=Path(args.checkpoint).expanduser().resolve(),
28
+ load_epoch="last",
29
+ )
30
+ generator = CrystalGenerator(
31
+ checkpoint_info=checkpoint_info,
32
+ batch_size=args.batch_size,
33
+ num_batches=args.num_batches,
34
+ properties_to_condition_on=args.properties,
35
+ record_trajectories=args.record_trajectories,
36
+ )
37
+ structures = generator.generate(
38
+ output_dir=Path(args.output).expanduser().resolve()
39
+ )
40
+ print(f"Generated {len(structures)} structures in {args.output}")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
model/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ __version__ = "1.0.0"
4
+ """MatterGen crystal structure generation model."""
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .generator import CrystalGenerator
10
+
11
+ __all__ = ["CrystalGenerator"]
12
+
13
+
14
+ def __getattr__(name: str):
15
+ if name == "CrystalGenerator":
16
+ from .generator import CrystalGenerator
17
+
18
+ return CrystalGenerator
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
model/adapter.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from typing import Callable
5
+
6
+ import torch
7
+
8
+ from onescience.datapipes.materials.mattergen.chemgraph import ChemGraph
9
+ from onescience.datapipes.materials.mattergen.types import PropertySourceId
10
+ from .denoiser import GemNetTDenoiser, get_chemgraph_from_denoiser_output
11
+ from onescience.modules.embedding.mattergen_property_embeddings import (
12
+ ZerosEmbedding,
13
+ get_property_embeddings,
14
+ get_use_unconditional_embedding,
15
+ )
16
+
17
+ BatchTransform = Callable[[ChemGraph], ChemGraph]
18
+
19
+
20
+ class GemNetTAdapter(GemNetTDenoiser):
21
+ """
22
+ Denoiser layerwise adapter with GemNetT. On top of a model.denoiser.GemNetTDenoiser,
23
+ additionally inputs <property_embeddings_adapt> that specifies extra conditions to be conditioned on.
24
+ """
25
+
26
+ def __init__(self, property_embeddings_adapt: torch.nn.ModuleDict, *args, **kwargs):
27
+ super().__init__(*args, **kwargs)
28
+
29
+ # ModuleDict[PropertyName, PropertyEmbedding] -- conditions adding by this adapter
30
+ self.property_embeddings_adapt = torch.nn.ModuleDict(property_embeddings_adapt)
31
+
32
+ # sanity check keys are required by the adapter that already exist in the base model
33
+ assert all(
34
+ [
35
+ k not in self.property_embeddings.keys()
36
+ for k in self.property_embeddings_adapt.keys()
37
+ ]
38
+ ), f"One of adapter conditions {self.property_embeddings_adapt.keys()} already exists in base model {self.property_embeddings.keys()}, please remove."
39
+
40
+ # we make the choice that new adapter fields do not alter the unconditional score
41
+ # we therefore need the unconditional embedding for all properties added in the adapter
42
+ # to return 0. We hack the unconditional embedding module here to achieve that
43
+ for property_embedding in self.property_embeddings_adapt.values():
44
+ property_embedding.unconditional_embedding_module = ZerosEmbedding(
45
+ hidden_dim=property_embedding.unconditional_embedding_module.hidden_dim,
46
+ )
47
+
48
+ def forward(
49
+ self,
50
+ x: ChemGraph,
51
+ t: torch.Tensor,
52
+ ) -> ChemGraph:
53
+ """
54
+ augment <z_per_crystal> with <self.condition_embs_adapt>.
55
+ """
56
+ (frac_coords, lattice, atom_types, num_atoms, batch,) = (
57
+ x["pos"],
58
+ x["cell"],
59
+ x["atomic_numbers"],
60
+ x["num_atoms"],
61
+ x.get_batch_idx("pos"),
62
+ )
63
+ # (num_atoms, hidden_dim) (num_crysts, 3)
64
+ t_enc = self.noise_level_encoding(t).to(lattice.device)
65
+ z_per_crystal = t_enc
66
+
67
+ # shape = (Nbatch, sum(hidden_dim of all properties in condition_on_adapt))
68
+ conditions_base_model: torch.Tensor = get_property_embeddings(
69
+ property_embeddings=self.property_embeddings, batch=x
70
+ )
71
+
72
+ if len(conditions_base_model) > 0:
73
+ z_per_crystal = torch.cat([z_per_crystal, conditions_base_model], dim=-1)
74
+
75
+ # compose into a dict
76
+ conditions_adapt_dict = {}
77
+ conditions_adapt_mask_dict = {}
78
+ for cond_field, property_embedding in self.property_embeddings_adapt.items():
79
+ conditions_adapt_dict[cond_field] = property_embedding.forward(batch=x)
80
+ try:
81
+ conditions_adapt_mask_dict[cond_field] = get_use_unconditional_embedding(
82
+ batch=x, cond_field=cond_field
83
+ )
84
+ except KeyError:
85
+ # no values have been provided for the conditional field,
86
+ # interpret this as the user wanting an unconditional score
87
+ conditions_adapt_mask_dict[cond_field] = torch.ones_like(
88
+ x["num_atoms"], dtype=torch.bool
89
+ ).reshape(-1, 1)
90
+
91
+ output = self.gemnet(
92
+ z=z_per_crystal,
93
+ frac_coords=frac_coords,
94
+ atom_types=atom_types,
95
+ num_atoms=num_atoms,
96
+ batch=batch,
97
+ lengths=None,
98
+ angles=None,
99
+ lattice=lattice,
100
+ # we construct the graph on the fly, hence pass None for these:
101
+ edge_index=None,
102
+ to_jimages=None,
103
+ num_bonds=None,
104
+ cond_adapt=conditions_adapt_dict,
105
+ cond_adapt_mask=conditions_adapt_mask_dict, # when True use unconditional embedding
106
+ )
107
+
108
+ pred_atom_types = self.fc_atom(output.node_embeddings)
109
+
110
+ return get_chemgraph_from_denoiser_output(
111
+ pred_atom_types=pred_atom_types,
112
+ pred_lattice_eps=output.stress,
113
+ pred_cart_pos_eps=output.forces,
114
+ training=self.training,
115
+ element_mask_func=self.element_mask_func,
116
+ x_input=x,
117
+ )
118
+
119
+ @property
120
+ def cond_fields_model_was_trained_on(self) -> list[PropertySourceId]:
121
+ """
122
+ We adopt the convention that all property embeddings are stored in torch.nn.ModuleDicts of
123
+ name property_embeddings or property_embeddings_adapt in the case of a fine tuned model.
124
+
125
+ This function returns the list of all field names that a given score model was trained to
126
+ condition on.
127
+ """
128
+ return list(self.property_embeddings) + list(self.property_embeddings_adapt)
model/common/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ __version__ = "0.1.0"
model/common/diffusion/__init__.py ADDED
File without changes
model/common/diffusion/corruption.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ import torch
5
+ from omegaconf import DictConfig
6
+
7
+ from ...diffusion.corruption.corruption import B, BatchedData, maybe_expand
8
+ from ...diffusion.corruption.sde_lib import SDE as DiffSDE
9
+ from ...diffusion.corruption.sde_lib import VESDE as DiffVESDE
10
+ from ...diffusion.corruption.sde_lib import VPSDE
11
+ from ...diffusion.wrapped.wrapped_sde import WrappedVESDE
12
+
13
+
14
+ def expand(a, x_shape, left=False):
15
+ a_dim = len(a.shape)
16
+ if left:
17
+ return a.reshape(*(((1,) * (len(x_shape) - a_dim)) + a.shape))
18
+ else:
19
+ return a.reshape(*(a.shape + ((1,) * (len(x_shape) - a_dim))))
20
+
21
+
22
+ def make_noise_symmetric_preserve_variance(noise: torch.Tensor) -> torch.Tensor:
23
+ """Makes the noise matrix symmetric, preserving the variance. Assumes i.i.d. noise for each dimension.
24
+
25
+ Args:
26
+ noise (torch.Tensor): Input noise matrix, must be a batched square matrix, i.e., have shape (batch_size, dim, dim).
27
+
28
+ Returns:
29
+ torch.Tensor: The symmetric noise matrix, with the same variance as the input.
30
+ """
31
+ assert (
32
+ len(noise.shape) == 3 and noise.shape[1] == noise.shape[2]
33
+ ), "Symmetric noise only works for square-matrix-shaped data."
34
+ # Var[1/sqrt(2) * (eps_i + eps_j)] = 0.5 Var[eps_i] + 0.5 Var[eps_j] = Var[noise]
35
+ # Special treatment of the diagonal elements, i.e., those we leave unchanged via masking.
36
+ return (1 / (2**0.5)) * (1 - torch.eye(3, device=noise.device)[None]) * (
37
+ noise + noise.transpose(1, 2)
38
+ ) + torch.eye(3, device=noise.device)[None] * noise
39
+
40
+
41
+ class LatticeVPSDE(VPSDE):
42
+ @staticmethod
43
+ def from_vpsde_config(vpsde_config: DictConfig):
44
+ return LatticeVPSDE(
45
+ **vpsde_config,
46
+ )
47
+
48
+ def __init__(
49
+ self,
50
+ beta_min: float = 0.1,
51
+ beta_max: float = 20,
52
+ limit_density: float | None = 0.05,
53
+ limit_var_scaling_constant: float = 0.25,
54
+ **kwargs,
55
+ ):
56
+ """Variance-preserving SDE with drift coefficient changing linearly over time."""
57
+ super().__init__()
58
+ self.beta_0 = beta_min
59
+ self.beta_1 = beta_max
60
+
61
+ # each crystal is diffused to have expected lattice vectors
62
+ # based on the number of atoms per crystal and self.limit_density
63
+ # units=(atoms/Angstrom**3)
64
+ self.limit_density = limit_density
65
+ self.limit_var_scaling_constant = limit_var_scaling_constant
66
+
67
+ self._limit_info_key = "num_atoms"
68
+
69
+ @property
70
+ def limit_info_key(self) -> str:
71
+ return self._limit_info_key
72
+
73
+ def beta(self, t: torch.Tensor) -> torch.Tensor:
74
+ return self.beta_0 + t * (self.beta_1 - self.beta_0)
75
+
76
+ def _marginal_mean_coeff(self, t: torch.Tensor) -> torch.Tensor:
77
+ log_mean_coeff = -0.25 * t**2 * (self.beta_1 - self.beta_0) - 0.5 * t * self.beta_0
78
+ return torch.exp(log_mean_coeff)
79
+
80
+ def marginal_prob(
81
+ self,
82
+ x: torch.Tensor,
83
+ t: torch.Tensor,
84
+ batch_idx: B = None,
85
+ batch: BatchedData | None = None,
86
+ ) -> tuple[torch.Tensor, torch.Tensor]:
87
+ assert batch is not None
88
+ mean_coeff = self._marginal_mean_coeff(t)
89
+ # x: shape [batch_size, *x.shape[1:]]
90
+ # t, limit_info: shape [batch_size,]
91
+ limit_mean = self.get_limit_mean(x=x, batch=batch)
92
+ limit_var = self.get_limit_var(x=x, batch=batch)
93
+ mean_coeff_expanded = maybe_expand(mean_coeff, batch_idx, x)
94
+ mean = mean_coeff_expanded * x + (1 - mean_coeff_expanded) * limit_mean
95
+ std = torch.sqrt((1.0 - mean_coeff_expanded**2) * limit_var)
96
+ return mean, std
97
+
98
+ def mean_coeff_and_std(
99
+ self,
100
+ x: torch.Tensor,
101
+ t: torch.Tensor,
102
+ batch_idx: B = None,
103
+ batch: BatchedData | None = None,
104
+ ) -> tuple[torch.Tensor, torch.Tensor]:
105
+ """Returns mean coefficient and standard deviation of marginal distribution at time t."""
106
+ mean_coeff = self._marginal_mean_coeff(t)
107
+ std = self.marginal_prob(x, t, batch_idx, batch)[1]
108
+ return maybe_expand(mean_coeff, batch=None, like=x), std
109
+
110
+ def get_limit_mean(self, x: torch.Tensor, batch: BatchedData) -> torch.Tensor:
111
+ # x: shape [batch_size, *x.shape[1:]]
112
+ # limit_info: shape [batch_size,], a 1d tensor containing number of atoms per crystal
113
+ # self.limit_density = limit_info / mean lattice vector length**3
114
+
115
+ # shape=[Ncrystals,]
116
+ n_atoms = batch[self.limit_info_key]
117
+
118
+ # shape=[Ncrystals, 3, 3]
119
+ return torch.pow(
120
+ torch.eye(3, device=x.device).expand(len(n_atoms), 3, 3)
121
+ * n_atoms[:, None, None]
122
+ / self.limit_density,
123
+ 1.0 / 3,
124
+ ).to(x.device)
125
+
126
+ def get_limit_var(self, x: torch.Tensor, batch: BatchedData) -> torch.Tensor:
127
+ """
128
+ Returns the element-wise variance of the limit distribution.
129
+ NOTE: even though we have a different limit variance per data
130
+ dimension we still sample IID for each element per data point.
131
+ We do NOT do any correlated sampling over data dimensions per
132
+ data point.
133
+
134
+ Return shape=x.shape
135
+ """
136
+
137
+ # x: shape [batch_size, *x.shape[1:]]
138
+ # limit_info: shape [batch_size,]
139
+ # necessary for mypy
140
+ n_atoms = batch[self.limit_info_key]
141
+
142
+ # expand to fit shape of data, shape = (n_crystals, 1, 1)
143
+ n_atoms_expanded = expand(n_atoms, x.shape)
144
+
145
+ # shape = (n_crystals, 3, 3)
146
+ n_atoms_expanded = torch.tile(n_atoms_expanded, (1, 3, 3))
147
+
148
+ # scale limit standard deviation to be proportional to number atoms = n_atoms**(1/3)
149
+ # per lattice vector. We hope that prod_i std_i scales as the standard deviation
150
+ # of the actual volume. NOTE: we return variance here, hence 2 in the power
151
+ # shape=(Ncrystals, 3, 3) for limit_info.shape=[Ncrystals,]
152
+ out = torch.pow(n_atoms_expanded, 2.0 / 3).to(x.device) * self.limit_var_scaling_constant
153
+
154
+ return out
155
+
156
+ def sample_marginal(
157
+ self,
158
+ x: torch.Tensor,
159
+ t: torch.Tensor,
160
+ batch_idx: B = None,
161
+ batch: BatchedData | None = None,
162
+ ) -> torch.Tensor:
163
+ mean, std = self.marginal_prob(x=x, t=t, batch=batch)
164
+ z = torch.randn_like(x)
165
+ z = make_noise_symmetric_preserve_variance(z)
166
+ return mean + expand(std, z.shape) * z
167
+
168
+ def prior_sampling(
169
+ self,
170
+ shape: torch.Size | tuple,
171
+ conditioning_data: BatchedData | None = None,
172
+ batch_idx: B = None,
173
+ ) -> torch.Tensor:
174
+ x_sample = torch.randn(*shape)
175
+ x_sample = make_noise_symmetric_preserve_variance(x_sample)
176
+ assert conditioning_data is not None
177
+ limit_info = conditioning_data[self.limit_info_key]
178
+ x_sample = x_sample.to(limit_info.device)
179
+ limit_mean = self.get_limit_mean(x=x_sample, batch=conditioning_data)
180
+ limit_var = self.get_limit_var(x=x_sample, batch=conditioning_data)
181
+ # shape=[Nbatch,...] for shape[0]=Nbatch
182
+ return x_sample * limit_var.sqrt() + limit_mean
183
+
184
+ def sde(
185
+ self,
186
+ x: torch.Tensor,
187
+ t: torch.Tensor,
188
+ batch_idx: B = None,
189
+ batch: BatchedData | None = None,
190
+ ) -> tuple[torch.Tensor, torch.Tensor]:
191
+ assert batch is not None
192
+ # x: shape [batch_size, *x.shape[1:]]
193
+ # t, limit_info: shape [batch_size,]
194
+ # if a per data-point limit mean is supplied, expand to shape of data
195
+ # shape=x.shape
196
+ limit_mean = self.get_limit_mean(x=x, batch=batch)
197
+
198
+ # if a per data-point limit variance is supplied, shape=[x.shape[0], ]
199
+ limit_var = self.get_limit_var(x=x, batch=batch)
200
+
201
+ beta_t = self.beta(t)
202
+ drift = (
203
+ -0.5
204
+ * expand(
205
+ beta_t,
206
+ x.shape,
207
+ )
208
+ * (x - limit_mean)
209
+ )
210
+ diffusion = torch.sqrt(expand(beta_t, limit_var.shape) * limit_var)
211
+ # drift.shape=[Nbatch,...], diffusion.shape=[Nbatch,] for x.shape[0]=Nbatch
212
+ return maybe_expand(drift, batch_idx), maybe_expand(diffusion, batch_idx)
213
+
214
+
215
+ class NumAtomsVarianceAdjustedWrappedVESDE(WrappedVESDE):
216
+ """Wrapped VESDE with variance adjusted by number of atoms. We divide the standard deviation by the cubic root of the number of atoms.
217
+ The goal is to reduce the influence by the cell size on the variance of the fractional coordinates.
218
+ """
219
+
220
+ def __init__(
221
+ self,
222
+ wrapping_boundary: float | torch.Tensor = 1.0,
223
+ sigma_min: float = 0.01,
224
+ sigma_max: float = 5.0,
225
+ limit_info_key: str = "num_atoms",
226
+ ):
227
+ super().__init__(
228
+ sigma_min=sigma_min, sigma_max=sigma_max, wrapping_boundary=wrapping_boundary
229
+ )
230
+ self.limit_info_key = limit_info_key
231
+
232
+ def std_scaling(self, batch: BatchedData) -> torch.Tensor:
233
+ return batch[self.limit_info_key] ** (-1 / 3)
234
+
235
+ def marginal_prob(
236
+ self,
237
+ x: torch.Tensor,
238
+ t: torch.Tensor,
239
+ batch_idx: B = None,
240
+ batch: BatchedData | None = None,
241
+ ) -> tuple[torch.Tensor, torch.Tensor]:
242
+ mean, std = super().marginal_prob(x, t, batch_idx, batch)
243
+ assert (
244
+ batch is not None
245
+ ), "batch must be provided when using NumAtomsVarianceAdjustedWrappedVESDEMixin"
246
+ std_scale = self.std_scaling(batch)
247
+ std = std * maybe_expand(std_scale, batch_idx, like=std)
248
+ return mean, std
249
+
250
+ def prior_sampling(
251
+ self,
252
+ shape: torch.Size | tuple,
253
+ conditioning_data: BatchedData | None = None,
254
+ batch_idx=None,
255
+ ) -> torch.Tensor:
256
+ _super = super()
257
+ assert isinstance(self, DiffSDE) and hasattr(_super, "prior_sampling")
258
+ assert (
259
+ conditioning_data is not None
260
+ ), "batch must be provided when using NumAtomsVarianceAdjustedWrappedVESDEMixin"
261
+ num_atoms = conditioning_data[self.limit_info_key]
262
+ batch_idx = torch.repeat_interleave(
263
+ torch.arange(num_atoms.shape[0], device=num_atoms.device), num_atoms, dim=0
264
+ )
265
+ std_scale = self.std_scaling(conditioning_data)
266
+ # prior sample is randn() * sigma_max, so we need additionally multiply by std_scale to get the correct variance.
267
+ # We call VESDE.prior_sampling (a "grandparent" function) because the super() prior_sampling already does the wrapping,
268
+ # which means we couldn't do the variance adjustment here anymore otherwise.
269
+ prior_sample = DiffVESDE.prior_sampling(self, shape=shape).to(num_atoms.device)
270
+ return self.wrap(prior_sample * maybe_expand(std_scale, batch_idx, like=prior_sample))
271
+
272
+ def sde(
273
+ self,
274
+ x: torch.Tensor,
275
+ t: torch.Tensor,
276
+ batch_idx: B = None,
277
+ batch: BatchedData | None = None,
278
+ ) -> tuple[torch.Tensor, torch.Tensor]:
279
+ sigma = self.marginal_prob(x, t, batch_idx, batch)[1]
280
+ sigma_min = self.marginal_prob(x, torch.zeros_like(t), batch_idx, batch)[1]
281
+ sigma_max = self.marginal_prob(x, torch.ones_like(t), batch_idx, batch)[1]
282
+ drift = torch.zeros_like(x)
283
+ diffusion = sigma * torch.sqrt(2 * (sigma_max.log() - sigma_min.log()))
284
+ return drift, diffusion
model/common/diffusion/predictors_correctors.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ import torch
5
+
6
+ from ...common.diffusion import corruption as sde_lib
7
+ from ...common.utils.data_utils import compute_lattice_polar_decomposition
8
+ from ...diffusion.corruption.corruption import Corruption, maybe_expand
9
+ from ...diffusion.data.batched_data import BatchedData
10
+ from ...diffusion.sampling import predictors_correctors as pc
11
+ from ...diffusion.sampling.predictors import AncestralSamplingPredictor
12
+
13
+ SampleAndMean = tuple[torch.Tensor, torch.Tensor]
14
+
15
+
16
+ class LatticeAncestralSamplingPredictor(AncestralSamplingPredictor):
17
+ @classmethod
18
+ def is_compatible(cls, corruption: Corruption) -> bool:
19
+ _super = super()
20
+ assert hasattr(_super, "is_compatible")
21
+ return _super.is_compatible(corruption) or isinstance(corruption, sde_lib.LatticeVPSDE)
22
+
23
+ def update_given_score(
24
+ self,
25
+ *,
26
+ x: torch.Tensor,
27
+ t: torch.Tensor,
28
+ dt: torch.Tensor,
29
+ batch_idx: torch.LongTensor,
30
+ score: torch.Tensor,
31
+ batch: BatchedData | None,
32
+ ) -> SampleAndMean:
33
+ x_coeff, score_coeff, std = self._get_coeffs(
34
+ x=x,
35
+ t=t,
36
+ dt=dt,
37
+ batch_idx=batch_idx,
38
+ batch=batch,
39
+ )
40
+ # mean = (x + score * beta**2 - limit_mean)/(1-beta) + limit_mean
41
+ # <=> mean = x / (1-beta) + score * beta**2 / (1-beta) + limit_mean * (1 - 1/(1-beta))
42
+ # => mean_coeff = 1 - x_coeff = 1 - 1/(1-beta)
43
+ mean_coeff = 1 - x_coeff
44
+ # Sample random noise.
45
+ z = sde_lib.make_noise_symmetric_preserve_variance(torch.randn_like(x_coeff))
46
+ assert hasattr(self.corruption, "get_limit_mean") # mypy
47
+ mean = (
48
+ x_coeff * x
49
+ + score_coeff * score
50
+ + mean_coeff * self.corruption.get_limit_mean(x=x, batch=batch)
51
+ )
52
+ sample = mean + std * z
53
+ return sample, mean
54
+
55
+
56
+ # create a langevin corrector that accepts LatticeVPSDE
57
+ class LatticeLangevinDiffCorrector(pc.LangevinCorrector):
58
+ @classmethod
59
+ def is_compatible(cls, corruption: Corruption) -> bool:
60
+ _super = super()
61
+ assert hasattr(_super, "is_compatible")
62
+ return _super.is_compatible(corruption) or isinstance(corruption, sde_lib.LatticeVPSDE)
63
+
64
+ def step_given_score(
65
+ self,
66
+ *,
67
+ x: torch.Tensor,
68
+ batch_idx: torch.LongTensor | None,
69
+ score: torch.Tensor,
70
+ t: torch.Tensor,
71
+ dt: torch.Tensor,
72
+ ) -> SampleAndMean:
73
+ assert isinstance(self.corruption, sde_lib.LatticeVPSDE)
74
+ alpha = self.get_alpha(t, dt=dt)
75
+ snr = self.snr
76
+ noise = torch.randn_like(x)
77
+ noise = sde_lib.make_noise_symmetric_preserve_variance(noise)
78
+
79
+ # [batch_size, ] or [num_atoms, ] if batch_idx is not None
80
+ grad_norm_square = torch.square(score).reshape(score.shape[0], -1).sum(dim=1)
81
+ noise_norm_square = torch.square(noise).reshape(noise.shape[0], -1).sum(dim=1)
82
+ # Average over items, leading to scalars.
83
+ grad_norm = grad_norm_square.sqrt().mean()
84
+ noise_norm = noise_norm_square.sqrt().mean()
85
+
86
+ # If gradient is zero (i.e., we are sampling from an improper distribution that's flat over the whole of R^n)
87
+ # the step_size blows up. Clip step_size to avoid this.
88
+ # The EGNN reports zero scores when there are no edges between nodes.
89
+ step_size = (snr * noise_norm / grad_norm) ** 2 * 2 * alpha
90
+ step_size = torch.minimum(step_size, self.max_step_size)
91
+ step_size[grad_norm == 0, :] = self.max_step_size
92
+ step_size = maybe_expand(step_size, batch_idx, score)
93
+ mean = x + step_size * score
94
+ x = mean + torch.sqrt(step_size * 2) * noise
95
+
96
+ x = compute_lattice_polar_decomposition(x)
97
+ mean = compute_lattice_polar_decomposition(mean)
98
+ return x, mean
model/common/gemnet/__init__.py ADDED
File without changes
model/common/gemnet/cgmanifest.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json.schemastore.org/component-detection-manifest.json",
3
+ "version": 1,
4
+ "registrations":[
5
+ {
6
+ "component": {
7
+ "type": "git",
8
+ "git": {
9
+ "repositoryUrl": "https://github.com/FAIR-Chem/fairchem",
10
+ "commitHash": "65c2d6246e69169f43949858d39550d2a635c7e0"
11
+ }
12
+ },
13
+ "developmentDependency" : false
14
+ }
15
+ ]
16
+ }
model/common/gemnet/gemnet-dT.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "comment": "tri_gaussian128, from https://github.com/FAIR-Chem/fairchem/blob/main/configs/s2ef/all/gemnet/scaling_factors/gemnet-dT.json",
3
+ "TripInteraction_1_had_rbf": 18.873615264892578,
4
+ "TripInteraction_1_sum_cbf": 7.996850490570068,
5
+ "AtomUpdate_1_sum": 1.220463752746582,
6
+ "TripInteraction_2_had_rbf": 16.10817527770996,
7
+ "TripInteraction_2_sum_cbf": 7.614634037017822,
8
+ "AtomUpdate_2_sum": 0.9690994620323181,
9
+ "TripInteraction_3_had_rbf": 15.01930046081543,
10
+ "TripInteraction_3_sum_cbf": 7.025179862976074,
11
+ "AtomUpdate_3_sum": 0.8903237581253052,
12
+ "OutBlock_0_sum": 1.6437848806381226,
13
+ "OutBlock_0_had": 16.161039352416992,
14
+ "OutBlock_1_sum": 1.1077653169631958,
15
+ "OutBlock_1_had": 13.54678726196289,
16
+ "OutBlock_2_sum": 0.9477927684783936,
17
+ "OutBlock_2_had": 12.754337310791016,
18
+ "OutBlock_3_sum": 0.9059251546859741,
19
+ "OutBlock_3_had": 13.484951972961426
20
+ }
model/common/gemnet/gemnet.py ADDED
@@ -0,0 +1,778 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Copyright (c) Facebook, Inc. and its affiliates.
3
+ Copyright (c) Microsoft Corporation.
4
+ Licensed under the MIT License.
5
+ Adapted from https://github.com/FAIR-Chem/fairchem/blob/main/src/fairchem/core/models/gemnet/gemnet.py.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Optional, Tuple
10
+
11
+ # import numpy as np
12
+ import torch
13
+ import torch.nn as nn
14
+ from torch_scatter import scatter
15
+ from torch_sparse import SparseTensor
16
+
17
+ from onescience.modules.layer.mattergen.atom_update_block import OutputBlock
18
+ from onescience.modules.layer.mattergen.base_layers import Dense
19
+ from onescience.modules.layer.mattergen.efficient import EfficientInteractionDownProjection
20
+ from onescience.modules.layer.mattergen.embedding_block import EdgeEmbedding
21
+ from onescience.modules.layer.mattergen.interaction_block import InteractionBlockTripletsOnly
22
+ from onescience.modules.layer.mattergen.radial_basis import RadialBasis
23
+ from onescience.modules.layer.mattergen.scaling import AutomaticFit
24
+ from onescience.modules.layer.mattergen.spherical_basis import CircularBasisLayer
25
+ from ...common.gemnet.utils import (
26
+ inner_product_normalized,
27
+ mask_neighbors,
28
+ ragged_range,
29
+ repeat_blocks,
30
+ )
31
+ from ...common.utils.data_utils import (
32
+ frac_to_cart_coords_with_lattice,
33
+ get_pbc_distances,
34
+ lattice_params_to_matrix_torch,
35
+ radius_graph_pbc,
36
+ )
37
+ from ...common.utils.globals import MODELS_PROJECT_ROOT, get_device, get_pyg_device
38
+ from ...common.utils.lattice_score import edge_score_to_lattice_score_frac_symmetric
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ModelOutput:
43
+ energy: torch.Tensor
44
+ node_embeddings: torch.Tensor
45
+ forces: Optional[torch.Tensor] = None
46
+ stress: Optional[torch.Tensor] = None
47
+
48
+
49
+ class RBFBasedLatticeUpdateBlock(torch.nn.Module):
50
+ # Lattice update block that mimics GemNet's edge processing, e.g., uses radial basis functions.
51
+ def __init__(
52
+ self,
53
+ emb_size: int,
54
+ activation: str,
55
+ emb_size_rbf: int,
56
+ emb_size_edge: int,
57
+ num_heads: int = 1,
58
+ ):
59
+ super().__init__()
60
+ self.num_out = num_heads
61
+ self.mlp = nn.Sequential(
62
+ Dense(emb_size, emb_size, activation=activation), Dense(emb_size, emb_size)
63
+ )
64
+ self.dense_rbf_F = Dense(emb_size_rbf, emb_size_edge, activation=None, bias=False)
65
+ self.out_forces = Dense(emb_size_edge, num_heads, bias=False, activation=None)
66
+
67
+ def compute_score_per_edge(
68
+ self,
69
+ edge_emb: torch.Tensor, # [Num_edges, emb_dim]
70
+ rbf: torch.Tensor, # [Num_edges, num_rbf_bases]
71
+ ) -> torch.Tensor:
72
+ x_F = self.mlp(edge_emb)
73
+ rbf_emb_F = self.dense_rbf_F(rbf) # (nEdges, emb_size_edge)
74
+ x_F_rbf = x_F * rbf_emb_F
75
+ # x_F = self.scale_rbf_F(x_F, x_F_rbf)
76
+ x_F = self.out_forces(x_F_rbf) # (nEdges, self.num_out)
77
+ return x_F
78
+
79
+
80
+ class RBFBasedLatticeUpdateBlockFrac(RBFBasedLatticeUpdateBlock):
81
+ # Lattice update block that mimics GemNet's edge processing, e.g., uses radial basis functions.
82
+ def __init__(
83
+ self,
84
+ emb_size: int,
85
+ activation: str,
86
+ emb_size_rbf: int,
87
+ emb_size_edge: int,
88
+ num_heads: int = 1,
89
+ ):
90
+ super().__init__(
91
+ emb_size=emb_size,
92
+ activation=activation,
93
+ emb_size_rbf=emb_size_rbf,
94
+ emb_size_edge=emb_size_edge,
95
+ num_heads=num_heads,
96
+ )
97
+
98
+ def forward(
99
+ self,
100
+ edge_emb: torch.Tensor, # [Num_edges, emb_dim]
101
+ edge_index: torch.Tensor, # [2, Num_edges]
102
+ distance_vec: torch.Tensor, # [Num_edges, 3]
103
+ lattice: torch.Tensor, # [Num_crystals, 3, 3]
104
+ batch: torch.Tensor, # [Num_atoms, ]
105
+ rbf: torch.Tensor, # [Num_edges, num_rbf_bases]
106
+ normalize_score: bool = True,
107
+ ) -> torch.Tensor:
108
+ edge_scores = self.compute_score_per_edge(edge_emb=edge_emb, rbf=rbf)
109
+ if normalize_score:
110
+ num_edges = scatter(torch.ones_like(distance_vec[:, 0]), batch[edge_index[0]])
111
+ edge_scores /= num_edges[batch[edge_index[0]], None]
112
+ outs = []
113
+ for i in range(self.num_out):
114
+ lattice_update = edge_score_to_lattice_score_frac_symmetric(
115
+ score_d=edge_scores[:, i],
116
+ edge_index=edge_index,
117
+ edge_vectors=distance_vec,
118
+ batch=batch,
119
+ )
120
+ outs.append(lattice_update)
121
+ outs = torch.stack(outs, dim=-1).sum(-1)
122
+ # [Batch_size, 3, 3]
123
+ return outs
124
+
125
+
126
+ class GemNetT(torch.nn.Module):
127
+ """
128
+ GemNet-T, triplets-only variant of GemNet
129
+
130
+ Parameters
131
+ ----------
132
+ num_targets: int
133
+ Number of prediction targets.
134
+
135
+ num_spherical: int
136
+ Controls maximum frequency.
137
+ num_radial: int
138
+ Controls maximum frequency.
139
+ num_blocks: int
140
+ Number of building blocks to be stacked.
141
+
142
+ atom_embedding: torch.nn.Module
143
+ a module that embeds atomic numbers into vectors of size emb_dim_atomic_number.
144
+ emb_size_atom: int
145
+ Embedding size of the atoms. This can be different from emb_dim_atomic_number.
146
+ emb_size_edge: int
147
+ Embedding size of the edges.
148
+ emb_size_trip: int
149
+ (Down-projected) Embedding size in the triplet message passing block.
150
+ emb_size_rbf: int
151
+ Embedding size of the radial basis transformation.
152
+ emb_size_cbf: int
153
+ Embedding size of the circular basis transformation (one angle).
154
+ emb_size_bil_trip: int
155
+ Embedding size of the edge embeddings in the triplet-based message passing block after the bilinear layer.
156
+ num_before_skip: int
157
+ Number of residual blocks before the first skip connection.
158
+ num_after_skip: int
159
+ Number of residual blocks after the first skip connection.
160
+ num_concat: int
161
+ Number of residual blocks after the concatenation.
162
+ num_atom: int
163
+ Number of residual blocks in the atom embedding blocks.
164
+ cutoff: float
165
+ Embedding cutoff for interactomic directions in Angstrom.
166
+ rbf: dict
167
+ Name and hyperparameters of the radial basis function.
168
+ envelope: dict
169
+ Name and hyperparameters of the envelope function.
170
+ cbf: dict
171
+ Name and hyperparameters of the cosine basis function.
172
+ output_init: str
173
+ Initialization method for the final dense layer.
174
+ activation: str
175
+ Name of the activation function.
176
+ scale_file: str
177
+ Path to the json file containing the scaling factors.
178
+ encoder_mode: bool
179
+ if <True>, use the encoder mode of the model, i.e. only get the atom/edge embedddings.
180
+ """
181
+
182
+ def __init__(
183
+ self,
184
+ num_targets: int,
185
+ latent_dim: int,
186
+ atom_embedding: torch.nn.Module,
187
+ num_spherical: int = 7,
188
+ num_radial: int = 128,
189
+ num_blocks: int = 3,
190
+ emb_size_atom: int = 512,
191
+ emb_size_edge: int = 512,
192
+ emb_size_trip: int = 64,
193
+ emb_size_rbf: int = 16,
194
+ emb_size_cbf: int = 16,
195
+ emb_size_bil_trip: int = 64,
196
+ num_before_skip: int = 1,
197
+ num_after_skip: int = 2,
198
+ num_concat: int = 1,
199
+ num_atom: int = 3,
200
+ regress_stress: bool = False,
201
+ cutoff: float = 6.0,
202
+ max_neighbors: int = 50,
203
+ rbf: dict = {"name": "gaussian"},
204
+ envelope: dict = {"name": "polynomial", "exponent": 5},
205
+ cbf: dict = {"name": "spherical_harmonics"},
206
+ otf_graph: bool = False,
207
+ output_init: str = "HeOrthogonal",
208
+ activation: str = "swish",
209
+ max_cell_images_per_dim: int = 5,
210
+ encoder_mode: bool = False, #
211
+ **kwargs,
212
+ ):
213
+ super().__init__()
214
+ scale_file = f"{MODELS_PROJECT_ROOT}/common/gemnet/gemnet-dT.json"
215
+ assert scale_file is not None, "`scale_file` is required."
216
+
217
+ self.encoder_mode = encoder_mode
218
+ self.num_targets = num_targets
219
+ assert num_blocks > 0
220
+ self.num_blocks = num_blocks
221
+ emb_dim_atomic_number = getattr(atom_embedding, "emb_size")
222
+
223
+ self.cutoff = cutoff
224
+
225
+ self.max_neighbors = max_neighbors
226
+
227
+ self.max_cell_images_per_dim = max_cell_images_per_dim
228
+
229
+ self.otf_graph = otf_graph
230
+
231
+ self.regress_stress = regress_stress
232
+ # we might want to take care of permutation invariance w.r.t. the order of the lattice vectors, though I don't think this is critical.
233
+ self.angle_edge_emb = nn.Sequential(
234
+ nn.Linear(emb_size_edge + 3, emb_size_edge),
235
+ nn.ReLU(),
236
+ nn.Linear(emb_size_edge, emb_size_edge),
237
+ )
238
+
239
+ AutomaticFit.reset() # make sure that queue is empty (avoid potential error)
240
+
241
+ # ---------------------------------- Basis Functions ---------------------------------- ###
242
+ self.radial_basis = RadialBasis(
243
+ num_radial=num_radial,
244
+ cutoff=cutoff,
245
+ rbf=rbf,
246
+ envelope=envelope,
247
+ )
248
+
249
+ radial_basis_cbf3 = RadialBasis(
250
+ num_radial=num_radial,
251
+ cutoff=cutoff,
252
+ rbf=rbf,
253
+ envelope=envelope,
254
+ )
255
+ self.cbf_basis3 = CircularBasisLayer(
256
+ num_spherical,
257
+ radial_basis=radial_basis_cbf3,
258
+ cbf=cbf,
259
+ efficient=True,
260
+ )
261
+ # ------------------------------------------------------------------------------------- ###
262
+
263
+ # --------------------------------- Update lattice MLP -------------------------------- ###
264
+ self.regress_stress = regress_stress
265
+ self.lattice_out_blocks = nn.ModuleList(
266
+ [
267
+ RBFBasedLatticeUpdateBlockFrac(
268
+ emb_size_edge,
269
+ activation,
270
+ emb_size_rbf,
271
+ emb_size_edge,
272
+ )
273
+ for _ in range(num_blocks + 1)
274
+ ]
275
+ )
276
+ self.mlp_rbf_lattice = Dense(
277
+ num_radial,
278
+ emb_size_rbf,
279
+ activation=None,
280
+ bias=False,
281
+ )
282
+ # ------------------------------------------------------------------------------------- ###
283
+ # ------------------------------- Share Down Projections ------------------------------ ###
284
+ # Share down projection across all interaction blocks
285
+ self.mlp_rbf3 = Dense(
286
+ num_radial,
287
+ emb_size_rbf,
288
+ activation=None,
289
+ bias=False,
290
+ )
291
+ self.mlp_cbf3 = EfficientInteractionDownProjection(num_spherical, num_radial, emb_size_cbf)
292
+
293
+ # Share the dense Layer of the atom embedding block across the interaction blocks
294
+ self.mlp_rbf_h = Dense(
295
+ num_radial,
296
+ emb_size_rbf,
297
+ activation=None,
298
+ bias=False,
299
+ )
300
+ self.mlp_rbf_out = Dense(
301
+ num_radial,
302
+ emb_size_rbf,
303
+ activation=None,
304
+ bias=False,
305
+ )
306
+ # ------------------------------------------------------------------------------------- ###
307
+
308
+ self.atom_emb = atom_embedding
309
+ self.atom_latent_emb = nn.Linear(emb_dim_atomic_number + latent_dim, emb_size_atom)
310
+ self.edge_emb = EdgeEmbedding(
311
+ emb_size_atom, num_radial, emb_size_edge, activation=activation
312
+ )
313
+
314
+ out_blocks = []
315
+ int_blocks = []
316
+
317
+ # Interaction Blocks
318
+ interaction_block = InteractionBlockTripletsOnly # GemNet-(d)T
319
+ for i in range(num_blocks):
320
+ int_blocks.append(
321
+ interaction_block(
322
+ emb_size_atom=emb_size_atom,
323
+ emb_size_edge=emb_size_edge,
324
+ emb_size_trip=emb_size_trip,
325
+ emb_size_rbf=emb_size_rbf,
326
+ emb_size_cbf=emb_size_cbf,
327
+ emb_size_bil_trip=emb_size_bil_trip,
328
+ num_before_skip=num_before_skip,
329
+ num_after_skip=num_after_skip,
330
+ num_concat=num_concat,
331
+ num_atom=num_atom,
332
+ activation=activation,
333
+ scale_file=scale_file,
334
+ name=f"IntBlock_{i+1}",
335
+ )
336
+ )
337
+
338
+ for i in range(num_blocks + 1):
339
+ out_blocks.append(
340
+ OutputBlock(
341
+ emb_size_atom=emb_size_atom,
342
+ emb_size_edge=emb_size_edge,
343
+ emb_size_rbf=emb_size_rbf,
344
+ nHidden=num_atom,
345
+ num_targets=num_targets,
346
+ activation=activation,
347
+ output_init=output_init,
348
+ direct_forces=True,
349
+ scale_file=scale_file,
350
+ name=f"OutBlock_{i}",
351
+ )
352
+ )
353
+
354
+ self.out_blocks = torch.nn.ModuleList(out_blocks)
355
+ self.int_blocks = torch.nn.ModuleList(int_blocks)
356
+
357
+ self.shared_parameters = [
358
+ (self.mlp_rbf3, self.num_blocks),
359
+ (self.mlp_cbf3, self.num_blocks),
360
+ (self.mlp_rbf_h, self.num_blocks),
361
+ (self.mlp_rbf_out, self.num_blocks + 1),
362
+ ]
363
+
364
+ def get_triplets(
365
+ self, edge_index: torch.Tensor, num_atoms: int
366
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
367
+ """
368
+ Get all b->a for each edge c->a.
369
+ It is possible that b=c, as long as the edges are distinct.
370
+
371
+ Returns
372
+ -------
373
+ id3_ba: torch.Tensor, shape (num_triplets,)
374
+ Indices of input edge b->a of each triplet b->a<-c
375
+ id3_ca: torch.Tensor, shape (num_triplets,)
376
+ Indices of output edge c->a of each triplet b->a<-c
377
+ id3_ragged_idx: torch.Tensor, shape (num_triplets,)
378
+ Indices enumerating the copies of id3_ca for creating a padded matrix
379
+ """
380
+ idx_s, idx_t = edge_index # c->a (source=c, target=a)
381
+
382
+ value = torch.arange(idx_s.size(0), device=idx_s.device, dtype=idx_s.dtype)
383
+ # Possibly contains multiple copies of the same edge (for periodic interactions)
384
+ pyg_device = get_pyg_device() if idx_s.device != torch.device("cpu") else idx_s.device
385
+ torch_device = get_device() if idx_s.device != torch.device("cpu") else idx_s.device
386
+ adj = SparseTensor(
387
+ row=idx_t.to(pyg_device),
388
+ col=idx_s.to(pyg_device),
389
+ value=value.to(pyg_device),
390
+ sparse_sizes=(num_atoms.to(pyg_device), num_atoms.to(pyg_device)),
391
+ )
392
+ adj_edges = adj[idx_t.to(pyg_device)].to(torch_device)
393
+
394
+ # Edge indices (b->a, c->a) for triplets.
395
+ id3_ba = adj_edges.storage.value().to(torch_device)
396
+ id3_ca = adj_edges.storage.row().to(torch_device)
397
+
398
+ # Remove self-loop triplets
399
+ # Compare edge indices, not atom indices to correctly handle periodic interactions
400
+ mask = id3_ba != id3_ca
401
+ id3_ba = id3_ba[mask]
402
+ id3_ca = id3_ca[mask]
403
+
404
+ # Get indices to reshape the neighbor indices b->a into a dense matrix.
405
+ # id3_ca has to be sorted for this to work.
406
+ num_triplets = torch.bincount(id3_ca, minlength=idx_s.size(0))
407
+ id3_ragged_idx = ragged_range(num_triplets)
408
+
409
+ return id3_ba, id3_ca, id3_ragged_idx
410
+
411
+ def select_symmetric_edges(self, tensor, mask, reorder_idx, inverse_neg):
412
+ # Mask out counter-edges
413
+ tensor_directed = tensor[mask]
414
+ # Concatenate counter-edges after normal edges
415
+ sign = 1 - 2 * inverse_neg
416
+ tensor_cat = torch.cat([tensor_directed, sign * tensor_directed])
417
+ # Reorder everything so the edges of every image are consecutive
418
+ tensor_ordered = tensor_cat[reorder_idx]
419
+ return tensor_ordered
420
+
421
+ def reorder_symmetric_edges(
422
+ self,
423
+ edge_index: torch.Tensor,
424
+ cell_offsets: torch.Tensor,
425
+ neighbors: torch.Tensor,
426
+ edge_dist: torch.Tensor,
427
+ edge_vector: torch.Tensor,
428
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
429
+ """
430
+ Reorder edges to make finding counter-directional edges easier.
431
+
432
+ Some edges are only present in one direction in the data,
433
+ since every atom has a maximum number of neighbors. Since we only use i->j
434
+ edges here, we lose some j->i edges and add others by
435
+ making it symmetric.
436
+ We could fix this by merging edge_index with its counter-edges,
437
+ including the cell_offsets, and then running torch.unique.
438
+ But this does not seem worth it.
439
+ """
440
+
441
+ # Generate mask
442
+ mask_sep_atoms = edge_index[0] < edge_index[1]
443
+ # Distinguish edges between the same (periodic) atom by ordering the cells
444
+ cell_earlier = (
445
+ (cell_offsets[:, 0] < 0)
446
+ | ((cell_offsets[:, 0] == 0) & (cell_offsets[:, 1] < 0))
447
+ | ((cell_offsets[:, 0] == 0) & (cell_offsets[:, 1] == 0) & (cell_offsets[:, 2] < 0))
448
+ )
449
+ mask_same_atoms = edge_index[0] == edge_index[1]
450
+ mask_same_atoms &= cell_earlier
451
+ mask = mask_sep_atoms | mask_same_atoms
452
+
453
+ # Mask out counter-edges
454
+ edge_index_new = edge_index[mask[None, :].expand(2, -1)].view(2, -1)
455
+
456
+ # Concatenate counter-edges after normal edges
457
+ edge_index_cat = torch.cat(
458
+ [
459
+ edge_index_new,
460
+ torch.stack([edge_index_new[1], edge_index_new[0]], dim=0),
461
+ ],
462
+ dim=1,
463
+ )
464
+
465
+ # Count remaining edges per image
466
+ batch_edge = torch.repeat_interleave(
467
+ torch.arange(neighbors.size(0), device=edge_index.device),
468
+ neighbors,
469
+ )
470
+ batch_edge = batch_edge[mask]
471
+ neighbors_new = 2 * torch.bincount(batch_edge, minlength=neighbors.size(0))
472
+
473
+ # Create indexing array
474
+ edge_reorder_idx = repeat_blocks(
475
+ neighbors_new // 2,
476
+ repeats=2,
477
+ continuous_indexing=True,
478
+ repeat_inc=edge_index_new.size(1),
479
+ )
480
+
481
+ # Reorder everything so the edges of every image are consecutive
482
+ edge_index_new = edge_index_cat[:, edge_reorder_idx]
483
+ cell_offsets_new = self.select_symmetric_edges(cell_offsets, mask, edge_reorder_idx, True)
484
+ edge_dist_new = self.select_symmetric_edges(edge_dist, mask, edge_reorder_idx, False)
485
+ edge_vector_new = self.select_symmetric_edges(edge_vector, mask, edge_reorder_idx, True)
486
+
487
+ return (
488
+ edge_index_new,
489
+ cell_offsets_new,
490
+ neighbors_new,
491
+ edge_dist_new,
492
+ edge_vector_new,
493
+ )
494
+
495
+ def select_edges(
496
+ self,
497
+ edge_index: torch.Tensor,
498
+ cell_offsets: torch.Tensor,
499
+ neighbors: torch.Tensor,
500
+ edge_dist: torch.Tensor,
501
+ edge_vector: torch.Tensor,
502
+ cutoff: Optional[float] = None,
503
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
504
+ if cutoff is not None:
505
+ edge_mask = edge_dist <= cutoff
506
+
507
+ edge_index = edge_index[:, edge_mask]
508
+ cell_offsets = cell_offsets[edge_mask]
509
+ neighbors = mask_neighbors(neighbors, edge_mask)
510
+ edge_dist = edge_dist[edge_mask]
511
+ edge_vector = edge_vector[edge_mask]
512
+
513
+ return edge_index, cell_offsets, neighbors, edge_dist, edge_vector
514
+
515
+ def generate_interaction_graph(
516
+ self,
517
+ cart_coords: torch.Tensor,
518
+ lattice: torch.Tensor,
519
+ num_atoms: torch.Tensor,
520
+ edge_index: torch.Tensor,
521
+ to_jimages: torch.Tensor,
522
+ num_bonds: torch.Tensor,
523
+ ) -> Tuple[
524
+ Tuple[torch.Tensor, torch.Tensor],
525
+ torch.Tensor,
526
+ torch.Tensor,
527
+ torch.Tensor,
528
+ torch.Tensor,
529
+ torch.Tensor,
530
+ torch.Tensor,
531
+ torch.Tensor,
532
+ torch.Tensor,
533
+ ]:
534
+ if self.otf_graph:
535
+ edge_index, to_jimages, num_bonds = radius_graph_pbc(
536
+ cart_coords=cart_coords,
537
+ lattice=lattice,
538
+ num_atoms=num_atoms,
539
+ radius=self.cutoff,
540
+ max_num_neighbors_threshold=self.max_neighbors,
541
+ max_cell_images_per_dim=self.max_cell_images_per_dim,
542
+ )
543
+
544
+ # Switch the indices, so the second one becomes the target index,
545
+ # over which we can efficiently aggregate.
546
+ out = get_pbc_distances(
547
+ cart_coords,
548
+ edge_index,
549
+ lattice,
550
+ to_jimages,
551
+ num_atoms,
552
+ num_bonds,
553
+ coord_is_cart=True,
554
+ return_offsets=True,
555
+ return_distance_vec=True,
556
+ )
557
+
558
+ edge_index = out["edge_index"]
559
+ D_st = out["distances"]
560
+ # These vectors actually point in the opposite direction.
561
+ # But we want to use col as idx_t for efficient aggregation.
562
+ V_st = -out["distance_vec"] / D_st[:, None]
563
+
564
+ (
565
+ edge_index,
566
+ cell_offsets,
567
+ neighbors,
568
+ D_st,
569
+ V_st,
570
+ ) = self.reorder_symmetric_edges(edge_index, to_jimages, num_bonds, D_st, V_st)
571
+
572
+ # Indices for swapping c->a and a->c (for symmetric MP)
573
+ block_sizes = neighbors // 2
574
+
575
+ # Remove 0 sizes
576
+ block_sizes = torch.masked_select(block_sizes, block_sizes > 0)
577
+ id_swap = repeat_blocks(
578
+ block_sizes,
579
+ repeats=2,
580
+ continuous_indexing=False,
581
+ start_idx=block_sizes[0],
582
+ block_inc=block_sizes[:-1] + block_sizes[1:],
583
+ repeat_inc=-block_sizes,
584
+ )
585
+
586
+ id3_ba, id3_ca, id3_ragged_idx = self.get_triplets(
587
+ edge_index,
588
+ num_atoms=num_atoms.sum(),
589
+ )
590
+
591
+ return (
592
+ edge_index,
593
+ neighbors,
594
+ D_st,
595
+ V_st,
596
+ id_swap,
597
+ id3_ba,
598
+ id3_ca,
599
+ id3_ragged_idx,
600
+ cell_offsets,
601
+ )
602
+
603
+ def forward(
604
+ self,
605
+ z: torch.Tensor,
606
+ frac_coords: torch.Tensor,
607
+ atom_types: torch.Tensor,
608
+ num_atoms: torch.Tensor,
609
+ batch: torch.Tensor,
610
+ lengths: Optional[torch.Tensor] = None,
611
+ angles: Optional[torch.Tensor] = None,
612
+ edge_index: Optional[torch.Tensor] = None,
613
+ to_jimages: Optional[torch.Tensor] = None,
614
+ num_bonds: Optional[torch.Tensor] = None,
615
+ lattice: Optional[torch.Tensor] = None,
616
+ ) -> ModelOutput:
617
+ """
618
+ args:
619
+ z: (N_cryst, num_latent)
620
+ frac_coords: (N_atoms, 3)
621
+ atom_types: (N_atoms, ) with D3PM need to use atomic number
622
+ num_atoms: (N_cryst,)
623
+ lengths: (N_cryst, 3) (optional, either lengths and angles or lattice must be passed)
624
+ angles: (N_cryst, 3) (optional, either lengths and angles or lattice must be passed)
625
+ edge_index: (2, N_edge) (optional, only needed if self.otf_graph is False)
626
+ to_jimages: (N_edge, 3) (optional, only needed if self.otf_graph is False)
627
+ num_bonds: (N_cryst,) (optional, only needed if self.otf_graph is False)
628
+ lattice: (N_cryst, 3, 3) (optional, either lengths and angles or lattice must be passed)
629
+ returns:
630
+ atom_frac_coords: (N_atoms, 3)
631
+ atom_types: (N_atoms, MAX_ATOMIC_NUM)
632
+ """
633
+
634
+ if self.otf_graph:
635
+ assert all(
636
+ [edge_index is None, to_jimages is None, num_bonds is None]
637
+ ), "OTF graph construction is active but received input graph information."
638
+ else:
639
+ assert not any(
640
+ [edge_index is None, to_jimages is None, num_bonds is None]
641
+ ), "OTF graph construction is off but received no input graph information."
642
+
643
+ assert (angles is None and lengths is None) != (
644
+ lattice is None
645
+ ), "Either lattice or lengths and angles must be provided, not both or none."
646
+ if angles is not None and lengths is not None:
647
+ lattice = lattice_params_to_matrix_torch(lengths, angles)
648
+ assert lattice is not None
649
+ distorted_lattice = lattice
650
+
651
+ pos = frac_to_cart_coords_with_lattice(frac_coords, num_atoms, lattice=distorted_lattice)
652
+
653
+ atomic_numbers = atom_types
654
+
655
+ (
656
+ edge_index,
657
+ neighbors,
658
+ D_st,
659
+ V_st,
660
+ id_swap,
661
+ id3_ba,
662
+ id3_ca,
663
+ id3_ragged_idx,
664
+ to_jimages,
665
+ ) = self.generate_interaction_graph(
666
+ pos, distorted_lattice, num_atoms, edge_index, to_jimages, num_bonds
667
+ )
668
+ idx_s, idx_t = edge_index
669
+
670
+ # Calculate triplet angles
671
+ cosφ_cab = inner_product_normalized(V_st[id3_ca], V_st[id3_ba])
672
+ rad_cbf3, cbf3 = self.cbf_basis3(D_st, cosφ_cab, id3_ca)
673
+
674
+ rbf = self.radial_basis(D_st)
675
+
676
+ # Embedding block
677
+ h = self.atom_emb(atomic_numbers)
678
+ # Merge z and atom embedding
679
+ if z is not None:
680
+ z_per_atom = z[batch]
681
+ h = torch.cat([h, z_per_atom], dim=1)
682
+ # Combine all embeddings
683
+ h = self.atom_latent_emb(h)
684
+ # (nAtoms, emb_size_atom)
685
+ m = self.edge_emb(h, rbf, idx_s, idx_t) # (nEdges, emb_size_edge)
686
+ batch_edge = batch[edge_index[0]]
687
+ cosines = torch.cosine_similarity(V_st[:, None], distorted_lattice[batch_edge], dim=-1)
688
+ m = torch.cat([m, cosines], dim=-1)
689
+ m = self.angle_edge_emb(m)
690
+
691
+ rbf3 = self.mlp_rbf3(rbf)
692
+ cbf3 = self.mlp_cbf3(rad_cbf3, cbf3, id3_ca, id3_ragged_idx)
693
+
694
+ rbf_h = self.mlp_rbf_h(rbf)
695
+ rbf_out = self.mlp_rbf_out(rbf)
696
+
697
+ E_t, F_st = self.out_blocks[0](h, m, rbf_out, idx_t)
698
+
699
+ distance_vec = V_st * D_st[:, None]
700
+
701
+ lattice_update = None
702
+ rbf_lattice = self.mlp_rbf_lattice(rbf)
703
+ lattice_update = self.lattice_out_blocks[0](
704
+ edge_emb=m,
705
+ edge_index=edge_index,
706
+ distance_vec=distance_vec,
707
+ lattice=distorted_lattice,
708
+ batch=batch,
709
+ rbf=rbf_lattice,
710
+ normalize_score=True,
711
+ )
712
+ F_fully_connected = torch.tensor(0.0, device=distorted_lattice.device)
713
+ for i in range(self.num_blocks):
714
+ # Interaction block
715
+ h, m = self.int_blocks[i](
716
+ h=h,
717
+ m=m,
718
+ rbf3=rbf3,
719
+ cbf3=cbf3,
720
+ id3_ragged_idx=id3_ragged_idx,
721
+ id_swap=id_swap,
722
+ id3_ba=id3_ba,
723
+ id3_ca=id3_ca,
724
+ rbf_h=rbf_h,
725
+ idx_s=idx_s,
726
+ idx_t=idx_t,
727
+ ) # (nAtoms, emb_size_atom), (nEdges, emb_size_edge)
728
+
729
+ E, F = self.out_blocks[i + 1](h, m, rbf_out, idx_t)
730
+ # (nAtoms, num_targets), (nEdges, num_targets)
731
+ F_st += F
732
+ E_t += E
733
+ rbf_lattice = self.mlp_rbf_lattice(rbf)
734
+ lattice_update += self.lattice_out_blocks[i + 1](
735
+ edge_emb=m,
736
+ edge_index=edge_index,
737
+ distance_vec=distance_vec,
738
+ lattice=distorted_lattice,
739
+ batch=batch,
740
+ rbf=rbf_lattice,
741
+ normalize_score=True,
742
+ )
743
+
744
+ nMolecules = torch.max(batch) + 1
745
+
746
+ if self.encoder_mode:
747
+ return E_t
748
+ # always use sum aggregation
749
+ E_t = scatter(
750
+ E_t, batch, dim=0, dim_size=nMolecules, reduce="sum"
751
+ ) # (nMolecules, num_targets)
752
+
753
+ # always output energy, forces and node embeddings
754
+ output = dict(energy=E_t, node_embeddings=h)
755
+
756
+ # map forces in edge directions
757
+ F_st_vec = F_st[:, :, None] * V_st[:, None, :]
758
+ # (nEdges, num_targets, 3)
759
+ F_t = scatter(
760
+ F_st_vec,
761
+ idx_t,
762
+ dim=0,
763
+ dim_size=num_atoms.sum(),
764
+ reduce="add",
765
+ ) # (nAtoms, num_targets, 3)
766
+ F_t = F_t.squeeze(1) # (nAtoms, 3)
767
+ output["forces"] = F_t + F_fully_connected
768
+
769
+ if self.regress_stress:
770
+ # optionally get predicted stress tensor
771
+ # shape=(Nbatch, 3, 3)
772
+ output["stress"] = lattice_update
773
+
774
+ return ModelOutput(**output)
775
+
776
+ @property
777
+ def num_params(self):
778
+ return sum(p.numel() for p in self.parameters())
model/common/gemnet/gemnet_ctrl.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # Copyright (c) Microsoft Corporation.
3
+ # Licensed under the MIT License.
4
+
5
+ # Adapted from https://github.com/FAIR-Chem/fairchem/blob/main/src/fairchem/core/models/gemnet/gemnet.py.
6
+
7
+ from typing import Dict, List, Optional
8
+
9
+ # import numpy as np
10
+ import torch
11
+ import torch.nn as nn
12
+ from torch_scatter import scatter
13
+
14
+ from onescience.datapipes.materials.mattergen.types import PropertySourceId
15
+ from ...common.gemnet.gemnet import GemNetT, ModelOutput
16
+ from ...common.gemnet.utils import inner_product_normalized
17
+ from ...common.utils.data_utils import (
18
+ frac_to_cart_coords_with_lattice,
19
+ lattice_params_to_matrix_torch,
20
+ )
21
+
22
+
23
+ class GemNetTCtrl(GemNetT):
24
+ """
25
+ GemNet-T, triplets-only variant of GemNet
26
+
27
+ This variation allows for layerwise conditional control for the purpose of
28
+ conditional finetuning. It adds the following on top of GemNetT:
29
+
30
+ for each condition in <condition_on_adapt>:
31
+
32
+ 1. a series of adapt layers that take the concatenation of the node embedding
33
+ and the condition embedding, process it with an MLP. There is one adapt layer
34
+ for each GemNetT message passing block.
35
+ 2. a series of mixin layers that take the output of the adapt layer and mix it in
36
+ to the atom embedding. There is one mixin layer for each GemNetT message passing block.
37
+ The mixin layers are initialized to zeros so at the beginning of training, the model
38
+ outputs exactly the same scores as the base GemNetT model.
39
+
40
+ """
41
+
42
+ def __init__(self, condition_on_adapt: List[PropertySourceId], *args, **kwargs):
43
+ super().__init__(*args, **kwargs)
44
+
45
+ self.condition_on_adapt = condition_on_adapt
46
+ self.cond_adapt_layers = nn.ModuleDict()
47
+ self.cond_mixin_layers = nn.ModuleDict()
48
+ # default value for emb_size_atom is 512
49
+ self.emb_size_atom = kwargs["emb_size_atom"] if "emb_size_atom" in kwargs else 512
50
+
51
+ for cond in condition_on_adapt:
52
+ adapt_layers = []
53
+ mixin_layers = []
54
+
55
+ for _ in range(self.num_blocks):
56
+ adapt_layers.append(
57
+ nn.Sequential(
58
+ nn.Linear(self.emb_size_atom * 2, self.emb_size_atom),
59
+ nn.ReLU(),
60
+ nn.Linear(self.emb_size_atom, self.emb_size_atom),
61
+ )
62
+ )
63
+ mixin_layers.append(nn.Linear(self.emb_size_atom, self.emb_size_atom, bias=False))
64
+ nn.init.zeros_(mixin_layers[-1].weight)
65
+
66
+ self.cond_adapt_layers[cond] = torch.nn.ModuleList(adapt_layers)
67
+ self.cond_mixin_layers[cond] = torch.nn.ModuleList(mixin_layers)
68
+
69
+ def forward(
70
+ self,
71
+ z: torch.Tensor,
72
+ frac_coords: torch.Tensor,
73
+ atom_types: torch.Tensor,
74
+ num_atoms: torch.Tensor,
75
+ batch: torch.Tensor,
76
+ lengths: Optional[torch.Tensor] = None,
77
+ angles: Optional[torch.Tensor] = None,
78
+ edge_index: Optional[torch.Tensor] = None,
79
+ to_jimages: Optional[torch.Tensor] = None,
80
+ num_bonds: Optional[torch.Tensor] = None,
81
+ lattice: Optional[torch.Tensor] = None,
82
+ charges: Optional[torch.Tensor] = None,
83
+ cond_adapt: Optional[Dict[PropertySourceId, torch.Tensor]] = None,
84
+ cond_adapt_mask: Optional[Dict[PropertySourceId, torch.Tensor]] = None,
85
+ ) -> ModelOutput:
86
+ """
87
+ args:
88
+ z: (N_cryst, num_latent)
89
+ frac_coords: (N_atoms, 3)
90
+ atom_types: (N_atoms, ) with D3PM need to use atomic number
91
+ num_atoms: (N_cryst,)
92
+ lengths: (N_cryst, 3) (optional, either lengths and angles or lattice must be passed)
93
+ angles: (N_cryst, 3) (optional, either lengths and angles or lattice must be passed)
94
+ edge_index: (2, N_edge) (optional, only needed if self.otf_graph is False)
95
+ to_jimages: (N_edge, 3) (optional, only needed if self.otf_graph is False)
96
+ num_bonds: (N_cryst,) (optional, only needed if self.otf_graph is False)
97
+ lattice: (N_cryst, 3, 3) (optional, either lengths and angles or lattice must be passed)
98
+ cond_adapt: (N_cryst, num_cond, dim_cond) (optional, conditional signal for score prediction)
99
+ cond_adapt_mask: (N_cryst, num_cond) (optional, mask for which data points receive conditional signal)
100
+ returns:
101
+ atom_frac_coords: (N_atoms, 3)
102
+ atom_types: (N_atoms, MAX_ATOMIC_NUM)
103
+ """
104
+
105
+ if self.otf_graph:
106
+ assert all(
107
+ [edge_index is None, to_jimages is None, num_bonds is None]
108
+ ), "OTF graph construction is active but received input graph information."
109
+ else:
110
+ assert not any(
111
+ [edge_index is None, to_jimages is None, num_bonds is None]
112
+ ), "OTF graph construction is off but received no input graph information."
113
+
114
+ assert (angles is None and lengths is None) != (
115
+ lattice is None
116
+ ), "Either lattice or lengths and angles must be provided, not both or none."
117
+ if angles is not None and lengths is not None:
118
+ lattice = lattice_params_to_matrix_torch(lengths, angles)
119
+ assert lattice is not None
120
+ distorted_lattice = lattice
121
+
122
+ pos = frac_to_cart_coords_with_lattice(frac_coords, num_atoms, lattice=distorted_lattice)
123
+
124
+ atomic_numbers = atom_types
125
+
126
+ (
127
+ edge_index,
128
+ neighbors,
129
+ D_st,
130
+ V_st,
131
+ id_swap,
132
+ id3_ba,
133
+ id3_ca,
134
+ id3_ragged_idx,
135
+ to_jimages,
136
+ ) = self.generate_interaction_graph(
137
+ pos, distorted_lattice, num_atoms, edge_index, to_jimages, num_bonds
138
+ )
139
+ idx_s, idx_t = edge_index
140
+
141
+ # Calculate triplet angles
142
+ cosφ_cab = inner_product_normalized(V_st[id3_ca], V_st[id3_ba])
143
+ rad_cbf3, cbf3 = self.cbf_basis3(D_st, cosφ_cab, id3_ca)
144
+
145
+ rbf = self.radial_basis(D_st)
146
+
147
+ # Embedding block
148
+ h = self.atom_emb(atomic_numbers)
149
+ # Merge z and atom embedding
150
+ if z is not None:
151
+ z_per_atom = z[batch]
152
+ h = torch.cat([h, z_per_atom], dim=1)
153
+ h = self.atom_latent_emb(h)
154
+ # (nAtoms, emb_size_atom)
155
+ m = self.edge_emb(h, rbf, idx_s, idx_t) # (nEdges, emb_size_edge)
156
+ batch_edge = batch[edge_index[0]]
157
+ cosines = torch.cosine_similarity(V_st[:, None], distorted_lattice[batch_edge], dim=-1)
158
+ m = torch.cat([m, cosines], dim=-1)
159
+ m = self.angle_edge_emb(m)
160
+
161
+ rbf3 = self.mlp_rbf3(rbf)
162
+ cbf3 = self.mlp_cbf3(rad_cbf3, cbf3, id3_ca, id3_ragged_idx)
163
+
164
+ rbf_h = self.mlp_rbf_h(rbf)
165
+ rbf_out = self.mlp_rbf_out(rbf)
166
+
167
+ E_t, F_st = self.out_blocks[0](h, m, rbf_out, idx_t)
168
+
169
+ distance_vec = V_st * D_st[:, None]
170
+
171
+ lattice_update = None
172
+ rbf_lattice = self.mlp_rbf_lattice(rbf)
173
+ lattice_update = self.lattice_out_blocks[0](
174
+ edge_emb=m,
175
+ edge_index=edge_index,
176
+ distance_vec=distance_vec,
177
+ lattice=distorted_lattice,
178
+ batch=batch,
179
+ rbf=rbf_lattice,
180
+ normalize_score=True,
181
+ )
182
+
183
+ # currently only working for a single cond adapt property.
184
+ # to extend to multi-properties,
185
+ # use a ModuleDict for adapt layers and mixin layers.
186
+ # use a dictionary to track the conditions?
187
+
188
+ if cond_adapt is not None and cond_adapt_mask is not None:
189
+ cond_adapt_per_atom = {}
190
+ cond_adapt_mask_per_atom = {}
191
+ for cond in self.condition_on_adapt:
192
+ cond_adapt_per_atom[cond] = cond_adapt[cond][batch]
193
+ # 1 = use conditional embedding, 0 = use unconditional embedding
194
+ cond_adapt_mask_per_atom[cond] = 1.0 - cond_adapt_mask[cond][batch].float()
195
+
196
+ for i in range(self.num_blocks):
197
+ h_adapt = torch.zeros_like(h)
198
+ for cond in self.condition_on_adapt:
199
+ h_adapt_cond = self.cond_adapt_layers[cond][i](
200
+ torch.cat([h, cond_adapt_per_atom[cond]], dim=-1)
201
+ )
202
+ h_adapt_cond = self.cond_mixin_layers[cond][i](h_adapt_cond)
203
+ # cond_adapt_mask_per_atom[cond] is 1.0 if we want to use conditional embedding and 0 for unconditional embedding
204
+ h_adapt += cond_adapt_mask_per_atom[cond] * h_adapt_cond
205
+ h = h + h_adapt
206
+
207
+ # Interaction block
208
+ h, m = self.int_blocks[i](
209
+ h=h,
210
+ m=m,
211
+ rbf3=rbf3,
212
+ cbf3=cbf3,
213
+ id3_ragged_idx=id3_ragged_idx,
214
+ id_swap=id_swap,
215
+ id3_ba=id3_ba,
216
+ id3_ca=id3_ca,
217
+ rbf_h=rbf_h,
218
+ idx_s=idx_s,
219
+ idx_t=idx_t,
220
+ ) # (nAtoms, emb_size_atom), (nEdges, emb_size_edge)
221
+
222
+ E, F = self.out_blocks[i + 1](h, m, rbf_out, idx_t)
223
+ # (nAtoms, num_targets), (nEdges, num_targets)
224
+ F_st += F
225
+ E_t += E
226
+ rbf_lattice = self.mlp_rbf_lattice(rbf)
227
+ lattice_update += self.lattice_out_blocks[i + 1](
228
+ edge_emb=m,
229
+ edge_index=edge_index,
230
+ distance_vec=distance_vec,
231
+ lattice=distorted_lattice,
232
+ batch=batch,
233
+ rbf=rbf_lattice,
234
+ normalize_score=True,
235
+ )
236
+
237
+ nMolecules = torch.max(batch) + 1
238
+
239
+ # always use sum aggregation
240
+ E_t = scatter(
241
+ E_t, batch, dim=0, dim_size=nMolecules, reduce="sum"
242
+ ) # (nMolecules, num_targets)
243
+
244
+ # always output energy, forces and node embeddings
245
+ output = dict(energy=E_t, node_embeddings=h)
246
+
247
+ # map forces in edge directions
248
+ F_st_vec = F_st[:, :, None] * V_st[:, None, :]
249
+ # (nEdges, num_targets, 3)
250
+ F_t = scatter(
251
+ F_st_vec,
252
+ idx_t,
253
+ dim=0,
254
+ dim_size=num_atoms.sum(),
255
+ reduce="add",
256
+ ) # (nAtoms, num_targets, 3)
257
+ F_t = F_t.squeeze(1) # (nAtoms, 3)
258
+ output["forces"] = F_t
259
+
260
+ if self.regress_stress:
261
+ # shape=(Nbatch, 3, 3)
262
+ output["stress"] = lattice_update
263
+
264
+ return ModelOutput(**output)
265
+
266
+ @property
267
+ def num_params(self):
268
+ return sum(p.numel() for p in self.parameters())
model/common/gemnet/initializers.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Copyright (c) Facebook, Inc. and its affiliates.
3
+ Copyright (c) Microsoft Corporation.
4
+ Licensed under the MIT License.
5
+ Adapted from https://github.com/FAIR-Chem/fairchem/blob/main/src/fairchem/core/models/gemnet/initializers.py.
6
+ """
7
+
8
+ import torch
9
+
10
+
11
+ # This function is not type annotated because mypy complains that axis could be either an integer or a tuple of integers,
12
+ # even though this is precicely how torch.var_mean works
13
+ def _standardize(kernel):
14
+ """
15
+ Makes sure that N*Var(W) = 1 and E[W] = 0
16
+ """
17
+ eps = 1e-6
18
+
19
+ if len(kernel.shape) == 3:
20
+ axis = (0, 1) # last dimension is output dimension
21
+ else:
22
+ axis = 1
23
+
24
+ var, mean = torch.var_mean(kernel, dim=axis, unbiased=True, keepdim=True)
25
+ kernel = (kernel - mean) / (var + eps) ** 0.5
26
+ return kernel
27
+
28
+
29
+ def he_orthogonal_init(tensor: torch.Tensor) -> torch.Tensor:
30
+ """
31
+ Generate a weight matrix with variance according to He (Kaiming) initialization.
32
+ Based on a random (semi-)orthogonal matrix neural networks
33
+ are expected to learn better when features are decorrelated
34
+ (stated by eg. "Reducing overfitting in deep networks by decorrelating representations",
35
+ "Dropout: a simple way to prevent neural networks from overfitting",
36
+ "Exact solutions to the nonlinear dynamics of learning in deep linear neural networks")
37
+ """
38
+ tensor = torch.nn.init.orthogonal_(tensor)
39
+
40
+ if len(tensor.shape) == 3:
41
+ fan_in = tensor.shape[:-1].numel()
42
+ else:
43
+ fan_in = tensor.shape[1]
44
+
45
+ with torch.no_grad():
46
+ tensor.data = _standardize(tensor.data)
47
+ tensor.data *= (1 / fan_in) ** 0.5
48
+
49
+ return tensor
model/common/gemnet/utils.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Copyright (c) Facebook, Inc. and its affiliates.
3
+ Copyright (c) Microsoft Corporation.
4
+ Licensed under the MIT License.
5
+ Adapted from https://github.com/FAIR-Chem/fairchem/blob/main/src/fairchem/core/models/gemnet/utils.py.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict, Optional, Tuple
10
+
11
+ import torch
12
+ from torch_scatter import segment_csr
13
+
14
+
15
+ def read_json(path: str) -> Dict:
16
+ """"""
17
+ if not path.endswith(".json"):
18
+ raise UserWarning(f"Path {path} is not a json-path.")
19
+
20
+ with open(path, "r") as f:
21
+ content = json.load(f)
22
+ return content
23
+
24
+
25
+ def update_json(path: str, data: Dict):
26
+ """"""
27
+ if not path.endswith(".json"):
28
+ raise UserWarning(f"Path {path} is not a json-path.")
29
+
30
+ content = read_json(path)
31
+ content.update(data)
32
+ write_json(path, content)
33
+
34
+
35
+ def write_json(path: str, data: Dict):
36
+ """"""
37
+ if not path.endswith(".json"):
38
+ raise UserWarning(f"Path {path} is not a json-path.")
39
+
40
+ with open(path, "w", encoding="utf-8") as f:
41
+ json.dump(data, f, ensure_ascii=False, indent=4)
42
+
43
+
44
+ def read_value_json(path: str, key: str) -> Optional[Any]:
45
+ """"""
46
+ content = read_json(path)
47
+
48
+ if key in content.keys():
49
+ return content[key]
50
+ else:
51
+ return None
52
+
53
+
54
+ def ragged_range(sizes: torch.Tensor) -> torch.Tensor:
55
+ """Multiple concatenated ranges.
56
+
57
+ Examples
58
+ --------
59
+ sizes = [1 4 2 3]
60
+ Return: [0 0 1 2 3 0 1 0 1 2]
61
+ """
62
+ assert sizes.dim() == 1
63
+ if sizes.sum() == 0:
64
+ return sizes.new_empty(0)
65
+
66
+ # Remove 0 sizes
67
+ sizes_nonzero = sizes > 0
68
+ if not torch.all(sizes_nonzero):
69
+ sizes = torch.masked_select(sizes, sizes_nonzero)
70
+
71
+ # Initialize indexing array with ones as we need to setup incremental indexing
72
+ # within each group when cumulatively summed at the final stage.
73
+ id_steps = torch.ones(sizes.sum(), dtype=torch.long, device=sizes.device)
74
+ id_steps[0] = 0
75
+ insert_index = sizes[:-1].cumsum(0)
76
+ insert_val = (1 - sizes)[:-1]
77
+
78
+ # Assign index-offsetting values
79
+ id_steps[insert_index] = insert_val
80
+
81
+ # Finally index into input array for the group repeated o/p
82
+ res = id_steps.cumsum(0)
83
+ return res
84
+
85
+
86
+ def repeat_blocks(
87
+ sizes: torch.Tensor,
88
+ repeats: torch.Tensor,
89
+ continuous_indexing: bool = True,
90
+ start_idx: int = 0,
91
+ block_inc: int = 0,
92
+ repeat_inc: int = 0,
93
+ ) -> torch.Tensor:
94
+ """Repeat blocks of indices.
95
+ Adapted from https://stackoverflow.com/questions/51154989/numpy-vectorized-function-to-repeat-blocks-of-consecutive-elements
96
+
97
+ continuous_indexing: Whether to keep increasing the index after each block
98
+ start_idx: Starting index
99
+ block_inc: Number to increment by after each block,
100
+ either global or per block. Shape: len(sizes) - 1
101
+ repeat_inc: Number to increment by after each repetition,
102
+ either global or per block
103
+
104
+ Examples
105
+ --------
106
+ sizes = [1,3,2] ; repeats = [3,2,3] ; continuous_indexing = False
107
+ Return: [0 0 0 0 1 2 0 1 2 0 1 0 1 0 1]
108
+ sizes = [1,3,2] ; repeats = [3,2,3] ; continuous_indexing = True
109
+ Return: [0 0 0 1 2 3 1 2 3 4 5 4 5 4 5]
110
+ sizes = [1,3,2] ; repeats = [3,2,3] ; continuous_indexing = True ;
111
+ repeat_inc = 4
112
+ Return: [0 4 8 1 2 3 5 6 7 4 5 8 9 12 13]
113
+ sizes = [1,3,2] ; repeats = [3,2,3] ; continuous_indexing = True ;
114
+ start_idx = 5
115
+ Return: [5 5 5 6 7 8 6 7 8 9 10 9 10 9 10]
116
+ sizes = [1,3,2] ; repeats = [3,2,3] ; continuous_indexing = True ;
117
+ block_inc = 1
118
+ Return: [0 0 0 2 3 4 2 3 4 6 7 6 7 6 7]
119
+ sizes = [0,3,2] ; repeats = [3,2,3] ; continuous_indexing = True
120
+ Return: [0 1 2 0 1 2 3 4 3 4 3 4]
121
+ sizes = [2,3,2] ; repeats = [2,0,2] ; continuous_indexing = True
122
+ Return: [0 1 0 1 5 6 5 6]
123
+ """
124
+ assert sizes.dim() == 1
125
+ assert all(sizes >= 0)
126
+
127
+ # Remove 0 sizes
128
+ sizes_nonzero = sizes > 0
129
+ if not torch.all(sizes_nonzero):
130
+ assert block_inc == 0 # Implementing this is not worth the effort
131
+ sizes = torch.masked_select(sizes, sizes_nonzero)
132
+ if isinstance(repeats, torch.Tensor):
133
+ repeats = torch.masked_select(repeats, sizes_nonzero)
134
+ if isinstance(repeat_inc, torch.Tensor):
135
+ repeat_inc = torch.masked_select(repeat_inc, sizes_nonzero)
136
+
137
+ if isinstance(repeats, torch.Tensor):
138
+ assert all(repeats >= 0)
139
+ insert_dummy = repeats[0] == 0
140
+ if insert_dummy:
141
+ one = sizes.new_ones(1)
142
+ zero = sizes.new_zeros(1)
143
+ sizes = torch.cat((one, sizes))
144
+ repeats = torch.cat((one, repeats))
145
+ if isinstance(block_inc, torch.Tensor):
146
+ block_inc = torch.cat((zero, block_inc))
147
+ if isinstance(repeat_inc, torch.Tensor):
148
+ repeat_inc = torch.cat((zero, repeat_inc))
149
+ else:
150
+ assert repeats >= 0
151
+ insert_dummy = False
152
+
153
+ # Get repeats for each group using group lengths/sizes
154
+ r1 = torch.repeat_interleave(torch.arange(len(sizes), device=sizes.device), repeats)
155
+
156
+ # Get total size of output array, as needed to initialize output indexing array
157
+ N = (sizes * repeats).sum()
158
+
159
+ # Initialize indexing array with ones as we need to setup incremental indexing
160
+ # within each group when cumulatively summed at the final stage.
161
+ # Two steps here:
162
+ # 1. Within each group, we have multiple sequences, so setup the offsetting
163
+ # at each sequence lengths by the seq. lengths preceding those.
164
+ id_ar = torch.ones(N, dtype=torch.long, device=sizes.device)
165
+ id_ar[0] = 0
166
+ insert_index = sizes[r1[:-1]].cumsum(0)
167
+ insert_val = (1 - sizes)[r1[:-1]]
168
+
169
+ if isinstance(repeats, torch.Tensor) and torch.any(repeats == 0):
170
+ diffs = r1[1:] - r1[:-1]
171
+ indptr = torch.cat((sizes.new_zeros(1), diffs.cumsum(0)))
172
+ if continuous_indexing:
173
+ # If a group was skipped (repeats=0) we need to add its size
174
+ insert_val += segment_csr(sizes[: r1[-1]], indptr, reduce="sum")
175
+
176
+ # Add block increments
177
+ if isinstance(block_inc, torch.Tensor):
178
+ insert_val += segment_csr(block_inc[: r1[-1]], indptr, reduce="sum")
179
+ else:
180
+ insert_val += block_inc * (indptr[1:] - indptr[:-1])
181
+ if insert_dummy:
182
+ insert_val[0] -= block_inc
183
+ else:
184
+ idx = r1[1:] != r1[:-1]
185
+ if continuous_indexing:
186
+ # 2. For each group, make sure the indexing starts from the next group's
187
+ # first element. So, simply assign 1s there.
188
+ insert_val[idx] = 1
189
+
190
+ # Add block increments
191
+ insert_val[idx] += block_inc
192
+
193
+ # Add repeat_inc within each group
194
+ if isinstance(repeat_inc, torch.Tensor):
195
+ insert_val += repeat_inc[r1[:-1]]
196
+ if isinstance(repeats, torch.Tensor):
197
+ repeat_inc_inner = repeat_inc[repeats > 0][:-1]
198
+ else:
199
+ repeat_inc_inner = repeat_inc[:-1]
200
+ else:
201
+ insert_val += repeat_inc
202
+ repeat_inc_inner = repeat_inc
203
+
204
+ # Subtract the increments between groups
205
+ if isinstance(repeats, torch.Tensor):
206
+ repeats_inner = repeats[repeats > 0][:-1]
207
+ else:
208
+ repeats_inner = repeats
209
+ insert_val[r1[1:] != r1[:-1]] -= repeat_inc_inner * repeats_inner
210
+
211
+ # Assign index-offsetting values
212
+ id_ar[insert_index] = insert_val
213
+
214
+ if insert_dummy:
215
+ id_ar = id_ar[1:]
216
+ if continuous_indexing:
217
+ id_ar[0] -= 1
218
+
219
+ # Set start index now, in case of insertion due to leading repeats=0
220
+ id_ar[0] += start_idx
221
+
222
+ # Finally index into input array for the group repeated o/p
223
+ res = id_ar.cumsum(0)
224
+ return res
225
+
226
+
227
+ def calculate_interatomic_vectors(
228
+ R: torch.Tensor, id_s: torch.Tensor, id_t: torch.Tensor, offsets_st: torch.Tensor
229
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
230
+ """
231
+ Calculate the vectors connecting the given atom pairs,
232
+ considering offsets from periodic boundary conditions (PBC).
233
+
234
+ Parameters
235
+ ----------
236
+ R: Tensor, shape = (nAtoms, 3)
237
+ Atom positions.
238
+ id_s: Tensor, shape = (nEdges,)
239
+ Indices of the source atom of the edges.
240
+ id_t: Tensor, shape = (nEdges,)
241
+ Indices of the target atom of the edges.
242
+ offsets_st: Tensor, shape = (nEdges,)
243
+ PBC offsets of the edges.
244
+ Subtract this from the correct direction.
245
+
246
+ Returns
247
+ -------
248
+ (D_st, V_st): tuple
249
+ D_st: Tensor, shape = (nEdges,)
250
+ Distance from atom t to s.
251
+ V_st: Tensor, shape = (nEdges,)
252
+ Unit direction from atom t to s.
253
+ """
254
+ Rs = R[id_s]
255
+ Rt = R[id_t]
256
+ # ReLU prevents negative numbers in sqrt
257
+ if offsets_st is None:
258
+ V_st = Rt - Rs # s -> t
259
+ else:
260
+ V_st = Rt - Rs + offsets_st # s -> t
261
+ D_st = torch.sqrt(torch.sum(V_st**2, dim=1))
262
+ V_st = V_st / D_st[..., None]
263
+ return D_st, V_st
264
+
265
+
266
+ def inner_product_normalized(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
267
+ """
268
+ Calculate the inner product between the given normalized vectors,
269
+ giving a result between -1 and 1.
270
+ """
271
+ return torch.sum(x * y, dim=-1).clamp(min=-1, max=1)
272
+
273
+
274
+ def mask_neighbors(neighbors: torch.Tensor, edge_mask: torch.Tensor) -> torch.Tensor:
275
+ neighbors_old_indptr = torch.cat([neighbors.new_zeros(1), neighbors])
276
+ neighbors_old_indptr = torch.cumsum(neighbors_old_indptr, dim=0)
277
+ neighbors = segment_csr(edge_mask.long(), neighbors_old_indptr)
278
+ return neighbors
279
+
280
+
281
+ def get_k_index_product_set(
282
+ num_k_x: torch.LongTensor, num_k_y: torch.LongTensor, num_k_z: torch.LongTensor
283
+ ) -> tuple[torch.FloatTensor, int]:
284
+ # Get a box of k-lattice indices around (0,0,0)
285
+ k_index_sets = (
286
+ torch.arange(-num_k_x, num_k_x + 1, dtype=torch.float),
287
+ torch.arange(-num_k_y, num_k_y + 1, dtype=torch.float),
288
+ torch.arange(-num_k_z, num_k_z + 1, dtype=torch.float),
289
+ )
290
+ k_index_product_set = torch.cartesian_prod(*k_index_sets)
291
+ # Because our "signal" is real-valued, for the Fourier transform it holds that
292
+ # F(omega) = F*(-omega) (where F* is the complex conjugate of F). Thus, we only
293
+ # need to consider the positive half of the Fourier space.
294
+ k_index_product_set = k_index_product_set[k_index_product_set.shape[0] // 2 + 1 :]
295
+
296
+ # Amount of k-points
297
+ num_k_degrees_of_freedom = k_index_product_set.shape[0]
298
+
299
+ return k_index_product_set, num_k_degrees_of_freedom
model/common/globals.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from pathlib import Path
5
+
6
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
7
+ GENERATED_CRYSTALS_ZIP_FILE_NAME = "generated_crystals_cif.zip"
8
+ GENERATED_CRYSTALS_EXTXYZ_FILE_NAME = "generated_crystals.extxyz"
model/common/loss.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from functools import partial
5
+ from typing import Dict, Literal, Optional
6
+
7
+ from ..diffusion.losses import SummedFieldLoss, denoising_score_matching
8
+ from ..diffusion.model_target import ModelTarget
9
+ from ..diffusion.training.field_loss import FieldLoss, d3pm_loss
10
+ from ..diffusion.wrapped.wrapped_normal_loss import wrapped_normal_loss
11
+
12
+
13
+ class MaterialsLoss(SummedFieldLoss):
14
+ def __init__(
15
+ self,
16
+ reduce: Literal["sum", "mean"] = "mean",
17
+ d3pm_hybrid_lambda: float = 0.0,
18
+ include_pos: bool = True,
19
+ include_cell: bool = True,
20
+ include_atomic_numbers: bool = True,
21
+ weights: Optional[Dict[str, float]] = None,
22
+ ):
23
+ model_targets = {"pos": ModelTarget.score_times_std, "cell": ModelTarget.score_times_std}
24
+ self.fields_to_score = []
25
+ self.categorical_fields = []
26
+ loss_fns: Dict[str, FieldLoss] = {}
27
+ if include_pos:
28
+ self.fields_to_score.append("pos")
29
+ loss_fns["pos"] = partial(
30
+ wrapped_normal_loss,
31
+ reduce=reduce,
32
+ model_target=model_targets["pos"],
33
+ )
34
+ if include_cell:
35
+ self.fields_to_score.append("cell")
36
+ loss_fns["cell"] = partial(
37
+ denoising_score_matching,
38
+ reduce=reduce,
39
+ model_target=model_targets["cell"],
40
+ )
41
+ if include_atomic_numbers:
42
+ model_targets["atomic_numbers"] = ModelTarget.logits
43
+ self.fields_to_score.append("atomic_numbers")
44
+ self.categorical_fields.append("atomic_numbers")
45
+ loss_fns["atomic_numbers"] = partial(
46
+ d3pm_loss,
47
+ reduce=reduce,
48
+ d3pm_hybrid_lambda=d3pm_hybrid_lambda,
49
+ )
50
+ self.reduce = reduce
51
+ self.d3pm_hybrid_lambda = d3pm_hybrid_lambda
52
+ super().__init__(
53
+ loss_fns=loss_fns,
54
+ weights=weights,
55
+ model_targets=model_targets,
56
+ )
model/common/utils/__init__.py ADDED
File without changes
model/common/utils/config_utils.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ import argparse
5
+ import sys
6
+ from typing import Callable, TypeVar, cast
7
+
8
+ from omegaconf import OmegaConf
9
+
10
+ R = TypeVar("R")
11
+
12
+
13
+ def get_config(argv: list[str] | None, config_cls: Callable[..., R]) -> R:
14
+ """
15
+ Utility function to get OmegaConf config options.
16
+
17
+ Args:
18
+ argv: Either a list of command line arguments to parse, or None.
19
+ If None, this argument is set from sys.argv.
20
+ config_cls: Dataclass object specifying config structure
21
+ (i.e. which fields to expect in the config).
22
+ It should be the class itself, NOT an instance of the class.
23
+
24
+ Returns:
25
+ Config object, which will pass as an instance of `config_cls` among other things.
26
+ Note: the type for this could be specified more carefully, but OmegaConf's typing
27
+ system is a bit complex. See OmegaConf's docs for "structured" for more info.
28
+ """
29
+
30
+ if argv is None:
31
+ argv = sys.argv[1:]
32
+ # Parse command line arguments
33
+ parser = argparse.ArgumentParser(allow_abbrev=False) # prevent prefix matching issues
34
+ parser.add_argument(
35
+ "--config",
36
+ type=str,
37
+ action="append",
38
+ default=list(),
39
+ help="Path to a yaml config file. "
40
+ "Argument can be repeated multiple times, with later configs overwriting previous ones.",
41
+ )
42
+ args, config_changes = parser.parse_known_args(argv)
43
+
44
+ # Read configs from file and command line
45
+ conf_yamls = [OmegaConf.load(c) for c in args.config]
46
+ conf_cli = OmegaConf.from_cli(config_changes)
47
+
48
+ # Make merged config options
49
+ # CLI options take priority over YAML file options
50
+ schema = OmegaConf.structured(config_cls)
51
+ config = OmegaConf.merge(schema, *conf_yamls, conf_cli)
52
+ OmegaConf.set_readonly(config, True) # should not be written to
53
+ return cast(R, config)