Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +22 -10
- .gitignore +6 -0
- README.md +236 -0
- conf/adapter/default.yaml +16 -0
- conf/csp.yaml +14 -0
- conf/data_module/alex_mp_20.yaml +49 -0
- conf/data_module/mp_20.yaml +54 -0
- conf/default.yaml +13 -0
- conf/finetune.yaml +18 -0
- conf/lightning_module/default.yaml +17 -0
- conf/lightning_module/diffusion_module/corruption/csp.yaml +15 -0
- conf/lightning_module/diffusion_module/corruption/default.yaml +27 -0
- conf/lightning_module/diffusion_module/csp.yaml +19 -0
- conf/lightning_module/diffusion_module/default.yaml +18 -0
- conf/lightning_module/diffusion_module/model/mattergen.yaml +29 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/chemical_system.yaml +10 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/dft_band_gap.yaml +10 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/dft_bulk_modulus.yaml +11 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/dft_mag_density.yaml +10 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/energy_above_hull.yaml +10 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/hhi_score.yaml +10 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/ml_bulk_modulus.yaml +11 -0
- conf/lightning_module/diffusion_module/model/property_embeddings/space_group.yaml +10 -0
- conf/trainer/default.yaml +38 -0
- configuration.json +1 -0
- csv_to_dataset.py +47 -0
- demo/configs/finetune_dft_mag_density_smoke.yaml +18 -0
- demo/configs/generate_base.yaml +7 -0
- demo/configs/generate_dft_mag_density.yaml +9 -0
- demo/configs/train_8dcu.yaml +14 -0
- demo/run.sh +61 -0
- finetune.py +64 -0
- generate.py +44 -0
- model/__init__.py +19 -0
- model/adapter.py +128 -0
- model/common/__init__.py +3 -0
- model/common/diffusion/__init__.py +0 -0
- model/common/diffusion/corruption.py +284 -0
- model/common/diffusion/predictors_correctors.py +98 -0
- model/common/gemnet/__init__.py +0 -0
- model/common/gemnet/cgmanifest.json +16 -0
- model/common/gemnet/gemnet-dT.json +20 -0
- model/common/gemnet/gemnet.py +778 -0
- model/common/gemnet/gemnet_ctrl.py +268 -0
- model/common/gemnet/initializers.py +49 -0
- model/common/gemnet/utils.py +299 -0
- model/common/globals.py +8 -0
- model/common/loss.py +56 -0
- model/common/utils/__init__.py +0 -0
- 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)
|