github-actions[ci] commited on
Commit
11ac28c
·
0 Parent(s):

Clean sync from main branch - 2025-07-05 06:02:08

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .devcontainer/devcontainer.json +3 -0
  2. .gitattributes +12 -0
  3. .github/README.md +198 -0
  4. .github/workflows/release.yaml +96 -0
  5. .github/workflows/sync-hf.yaml +39 -0
  6. .github/workflows/test.yaml +103 -0
  7. .gitignore +169 -0
  8. .streamlit/config.toml +2 -0
  9. CITATION.cff +23 -0
  10. LICENSE +201 -0
  11. README.md +14 -0
  12. benchmarks/bzo/dft.ipynb +0 -0
  13. benchmarks/bzo/pbe/mode-1.npy +0 -0
  14. benchmarks/bzo/pbe/phonopy_params.yaml +0 -0
  15. benchmarks/c2db/ALIGNN.parquet +3 -0
  16. benchmarks/c2db/CHGNet.parquet +3 -0
  17. benchmarks/c2db/M3GNet.parquet +3 -0
  18. benchmarks/c2db/MACE-MP(M).parquet +3 -0
  19. benchmarks/c2db/MACE-MPA.parquet +3 -0
  20. benchmarks/c2db/MatterSim.parquet +3 -0
  21. benchmarks/c2db/ORBv2.parquet +3 -0
  22. benchmarks/c2db/SevenNet.parquet +3 -0
  23. benchmarks/c2db/analysis.ipynb +408 -0
  24. benchmarks/c2db/c2db-confusion_matrices.pdf +3 -0
  25. benchmarks/c2db/c2db-f1_bar.pdf +3 -0
  26. benchmarks/c2db/c2db.db +3 -0
  27. benchmarks/c2db/copy.parquet +3 -0
  28. benchmarks/c2db/run.py +213 -0
  29. benchmarks/energy_conservation/run.py +214 -0
  30. benchmarks/eos_alloy/run_Fe-Ni-Cr.ipynb +0 -0
  31. benchmarks/eos_bulk/CHGNet.parquet +3 -0
  32. benchmarks/eos_bulk/CHGNet_processed.parquet +3 -0
  33. benchmarks/eos_bulk/M3GNet.parquet +3 -0
  34. benchmarks/eos_bulk/M3GNet_processed.parquet +3 -0
  35. benchmarks/eos_bulk/MACE-MP(M).parquet +3 -0
  36. benchmarks/eos_bulk/MACE-MP(M)_processed.parquet +3 -0
  37. benchmarks/eos_bulk/MACE-MPA.parquet +3 -0
  38. benchmarks/eos_bulk/MACE-MPA_processed.parquet +3 -0
  39. benchmarks/eos_bulk/MatterSim.parquet +3 -0
  40. benchmarks/eos_bulk/MatterSim_processed.parquet +3 -0
  41. benchmarks/eos_bulk/ORBv2.parquet +3 -0
  42. benchmarks/eos_bulk/ORBv2_processed.parquet +3 -0
  43. benchmarks/eos_bulk/SevenNet.parquet +3 -0
  44. benchmarks/eos_bulk/SevenNet_processed.parquet +3 -0
  45. benchmarks/eos_bulk/analyze.py +223 -0
  46. benchmarks/eos_bulk/eSEN.parquet +3 -0
  47. benchmarks/eos_bulk/eSEN_processed.parquet +3 -0
  48. benchmarks/eos_bulk/plot.py +119 -0
  49. benchmarks/eos_bulk/preprocessing.py +12 -0
  50. benchmarks/eos_bulk/run.py +170 -0
.devcontainer/devcontainer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a525cdb835f1b6c36c5d09b1663e2dc0b2e5a40b97214fc9ee2fc0366b9df622
3
+ size 986
.gitattributes ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.json filter=lfs diff=lfs merge=lfs -text
2
+ *.parquet filter=lfs diff=lfs merge=lfs -text
3
+ *.db filter=lfs diff=lfs merge=lfs -text
4
+ examples/mof/classification/SevenNet.pkl filter=lfs diff=lfs merge=lfs -text
5
+ examples/mof/classification/input.pkl filter=lfs diff=lfs merge=lfs -text
6
+ examples/mof/classification/M3GNet.pkl filter=lfs diff=lfs merge=lfs -text
7
+ examples/mof/classification/MACE-MPA.pkl filter=lfs diff=lfs merge=lfs -text
8
+ examples/mof/classification/MACE-MP(M).pkl filter=lfs diff=lfs merge=lfs -text
9
+ examples/mof/classification/MatterSim.pkl filter=lfs diff=lfs merge=lfs -text
10
+ examples/mof/classification/ORBv2.pkl filter=lfs diff=lfs merge=lfs -text
11
+ *.pdf filter=lfs diff=lfs merge=lfs -text
12
+ *.png filter=lfs diff=lfs merge=lfs -text
.github/README.md ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <h1>⚔️ MLIP Arena ⚔️</h1>
3
+ <a href="https://openreview.net/forum?id=ysKfIavYQE#discussion"><img alt="Static Badge" src="https://img.shields.io/badge/ICLR-AI4Mat-blue"></a>
4
+ <a href="https://huggingface.co/spaces/atomind/mlip-arena"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Space-blue" alt="Hugging Face"></a>
5
+ <a href="https://github.com/atomind-ai/mlip-arena/actions"><img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/atomind-ai/mlip-arena/test.yaml"></a>
6
+ <a href="https://pypi.org/project/mlip-arena/"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/mlip-arena"></a>
7
+ <a href="https://pypi.org/project/mlip-arena/"><img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/mlip-arena"></a>
8
+ <a href="https://zenodo.org/doi/10.5281/zenodo.13704399"><img src="https://zenodo.org/badge/776930320.svg" alt="DOI"></a>
9
+ <!-- <a href="https://discord.gg/W8WvdQtT8T"><img alt="Discord" src="https://img.shields.io/discord/1299613474820984832?logo=discord"> -->
10
+ </a>
11
+ </div>
12
+
13
+ Foundation machine learning interatomic potentials (MLIPs), trained on extensive databases containing millions of density functional theory (DFT) calculations, have revolutionized molecular and materials modeling, but existing benchmarks suffer from data leakage, limited transferability, and an over-reliance on error-based metrics tied to specific density functional theory (DFT) references.
14
+
15
+ We introduce MLIP Arena, a unified benchmark platform for evaluating foundation MLIP performance beyond conventional error metrics. It focuses on revealing the physical soundness learned by MLIPs and assessing their utilitarian performance agnostic to underlying model architecture and training dataset.
16
+
17
+ ***By moving beyond static DFT references and revealing the important failure modes*** of current foundation MLIPs in real-world settings, MLIP Arena provides a reproducible framework to guide the next-generation MLIP development toward improved predictive accuracy and runtime efficiency while maintaining physical consistency.
18
+
19
+ MLIP Arena leverages modern pythonic workflow orchestrator 💙
20
+ [Prefect](https://www.prefect.io/) 💙
21
+ to enable advanced task/flow chaining and caching.
22
+
23
+ ![Thumnail](../serve/assets/workflow.png)
24
+
25
+ > [!NOTE]
26
+ > Contributions of new tasks through PRs are very welcome! If you're interested in joining the effort, please reach out to Yuan at [[email protected]](mailto:[email protected]). See [project page](https://github.com/orgs/atomind-ai/projects/1) for some outstanding tasks, or propose new feature requests in [Discussion](https://github.com/atomind-ai/mlip-arena/discussions/new?category=ideas).
27
+
28
+ ## Announcement
29
+
30
+ - **[April 8, 2025]** [🎉 **MLIP Arena is accepted as an ICLR AI4Mat Spotlight!** 🎉](https://openreview.net/forum?id=ysKfIavYQE#discussion) Huge thanks to all co-authors for their contributions!
31
+
32
+
33
+ ## Installation
34
+
35
+ ### From PyPI (prefect workflow only, without pretrained models)
36
+
37
+ ```bash
38
+ pip install mlip-arena
39
+ ```
40
+
41
+ ### From source (with integrated pretrained models, advanced)
42
+
43
+ > [!CAUTION]
44
+ > We strongly recommend clean build in a new virtual environment due to the compatibility issues between multiple popular MLIPs. We provide a single installation script using `uv` for minimal package conflicts and fast installation!
45
+
46
+ > [!CAUTION]
47
+ > To automatically download farichem OMat24 checkpoint, please make sure you have gained downloading access to their HuggingFace [***model repo***](https://huggingface.co/facebook/OMAT24) (not dataset repo), and login locally on your machine through `huggginface-cli login` (see [HF hub authentication](https://huggingface.co/docs/huggingface_hub/en/quick-start#authentication))
48
+
49
+ **Linux**
50
+
51
+ ```bash
52
+ # (Optional) Install uv, way faster than pip, why not? :)
53
+ curl -LsSf https://astral.sh/uv/install.sh | sh
54
+ source $HOME/.local/bin/env
55
+
56
+ git clone https://github.com/atomind-ai/mlip-arena.git
57
+ cd mlip-arena
58
+
59
+ # One script uv pip installation
60
+ bash scripts/install.sh
61
+ ```
62
+
63
+ > [!TIP]
64
+ > Sometimes installing all compiled models takes all the available local storage. Optional pip flag `--no-cache` could be uesed. `uv cache clean` will be helpful too.
65
+
66
+ **Mac**
67
+
68
+ ```bash
69
+ # (Optional) Install uv
70
+ curl -LsSf https://astral.sh/uv/install.sh | sh
71
+ source $HOME/.local/bin/env
72
+ # One script uv pip installation
73
+ bash scripts/install-macosx.sh
74
+ ```
75
+
76
+ ## Quickstart
77
+
78
+ ### The first example: Molecular Dynamics
79
+
80
+ Arena provides a unified interface to run all the compiled MLIPs. This can be achieved simply by looping through `MLIPEnum`:
81
+
82
+ ```python
83
+ from mlip_arena.models import MLIPEnum
84
+ from mlip_arena.tasks import MD
85
+ from mlip_arena.tasks.utils import get_calculator
86
+
87
+ from ase import units
88
+ from ase.build import bulk
89
+
90
+ atoms = bulk("Cu", "fcc", a=3.6) * (5, 5, 5)
91
+
92
+ results = []
93
+
94
+ for model in MLIPEnum:
95
+ result = MD(
96
+ atoms=atoms,
97
+ calculator=get_calculator(
98
+ model,
99
+ calculator_kwargs=dict(), # passing into calculator
100
+ dispersion=True,
101
+ dispersion_kwargs=dict(
102
+ damping='bj', xc='pbe', cutoff=40.0 * units.Bohr
103
+ ), # passing into TorchDFTD3Calculator
104
+ ), # compatible with custom ASE Calculator
105
+ ensemble="nve", # nvt, nvt available
106
+ dynamics="velocityverlet", # compatible with any ASE Dynamics objects and their class names
107
+ total_time=1e3, # 1 ps = 1e3 fs
108
+ time_step=2, # fs
109
+ )
110
+ results.append(result)
111
+ ```
112
+
113
+ ### 🚀 Parallelize Benchmarks at Scale
114
+
115
+ To run multiple benchmarks in parallel, add `.submit` before the task function and wrap all the tasks into a flow to dispatch the tasks to worker for concurrent execution. See Prefect Doc on [tasks](https://docs.prefect.io/v3/develop/write-tasks) and [flow](https://docs.prefect.io/v3/develop/write-flows) for more details.
116
+
117
+ ```python
118
+ ...
119
+ from prefect import flow
120
+
121
+ @flow
122
+ def run_all_tasks:
123
+
124
+ futures = []
125
+ for model in MLIPEnum:
126
+ future = MD.submit(
127
+ atoms=atoms,
128
+ ...
129
+ )
130
+ future.append(future)
131
+
132
+ return [f.result(raise_on_failure=False) for f in futures]
133
+ ```
134
+
135
+ For a more practical example using HPC resources, please now refer to [MD stability benchmark](../benchmarks/stability/temperature.ipynb).
136
+
137
+ ### List of implemented tasks
138
+
139
+ The implemented tasks are available under `mlip_arena.tasks.<module>.run` or `from mlip_arena.tasks import *` for convenient imports (currently doesn't work if [phonopy](https://phonopy.github.io/phonopy/install.html) is not installed).
140
+
141
+ - [OPT](../mlip_arena/tasks/optimize.py#L56): Structure optimization
142
+ - [EOS](../mlip_arena/tasks/eos.py#L42): Equation of state (energy-volume scan)
143
+ - [MD](../mlip_arena/tasks/md.py#L200): Molecular dynamics with flexible dynamics (NVE, NVT, NPT) and temperature/pressure scheduling (annealing, shearing, *etc*)
144
+ - [PHONON](../mlip_arena/tasks/phonon.py#L110): Phonon calculation driven by [phonopy](https://phonopy.github.io/phonopy/install.html)
145
+ - [NEB](../mlip_arena/tasks/neb.py#L96): Nudged elastic band
146
+ - [NEB_FROM_ENDPOINTS](../mlip_arena/tasks/neb.py#L164): Nudge elastic band with convenient image interpolation (linear or IDPP)
147
+ - [ELASTICITY](../mlip_arena/tasks/elasticity.py#L78): Elastic tensor calculation
148
+
149
+ ### Contribute and Development
150
+
151
+ PRs are welcome. Please clone the repo and submit PRs with changes.
152
+
153
+ To make change to huggingface space, fetch large files from git lfs first and run streamlit:
154
+
155
+ ```
156
+ git lfs fetch --all
157
+ git lfs pull
158
+ streamlit run serve/app.py
159
+ ```
160
+
161
+ ### Add new benchmark tasks (WIP)
162
+
163
+ > [!NOTE]
164
+ > Please reuse, extend, or chain the general tasks defined [above](#list-of-implemented-tasks)
165
+
166
+ ### Add new MLIP models
167
+
168
+ If you have pretrained MLIP models that you would like to contribute to the MLIP Arena and show benchmark in real-time, there are two ways:
169
+
170
+ #### External ASE Calculator (easy)
171
+
172
+ 1. Implement new ASE Calculator class in [mlip_arena/models/externals](../mlip_arena/models/externals).
173
+ 2. Name your class with awesome model name and add the same name to [registry](../mlip_arena/models/registry.yaml) with metadata.
174
+
175
+ > [!CAUTION]
176
+ > Remove unneccessary outputs under `results` class attributes to avoid error for MD simulations. Please refer to [CHGNet](../mlip_arena/models/externals/chgnet.py) as an example.
177
+
178
+ #### Hugging Face Model (recommended, difficult)
179
+
180
+ 0. Inherit Hugging Face [ModelHubMixin](https://huggingface.co/docs/huggingface_hub/en/package_reference/mixins) class to your awesome model class definition. We recommend [PytorchModelHubMixin](https://huggingface.co/docs/huggingface_hub/en/package_reference/mixins#huggingface_hub.PyTorchModelHubMixin).
181
+ 1. Create a new [Hugging Face Model](https://huggingface.co/new) repository and upload the model file using [push_to_hub function](https://huggingface.co/docs/huggingface_hub/en/package_reference/mixins#huggingface_hub.ModelHubMixin.push_to_hub).
182
+ 2. Follow the template to code the I/O interface for your model [here](../mlip_arena/models/README.md).
183
+ 3. Update model [registry](../mlip_arena/models/registry.yaml) with metadata
184
+
185
+ ## Citation
186
+
187
+ If you find the work useful, please consider citing the following:
188
+
189
+ ```bibtex
190
+ @inproceedings{
191
+ chiang2025mlip,
192
+ title={{MLIP} Arena: Advancing Fairness and Transparency in Machine Learning Interatomic Potentials through an Open and Accessible Benchmark Platform},
193
+ author={Yuan Chiang and Tobias Kreiman and Elizabeth Weaver and Ishan Amin and Matthew Kuner and Christine Zhang and Aaron Kaplan and Daryl Chrzan and Samuel M Blau and Aditi S. Krishnapriyan and Mark Asta},
194
+ booktitle={AI for Accelerated Materials Design - ICLR 2025},
195
+ year={2025},
196
+ url={https://openreview.net/forum?id=ysKfIavYQE}
197
+ }
198
+ ```
.github/workflows/release.yaml ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: write # Ensure write access to push tags
8
+
9
+ jobs:
10
+ pypi:
11
+ name: Publish to PyPI
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ # Step 1: Checkout the code
16
+ - name: Checkout code
17
+ uses: actions/checkout@v3
18
+
19
+ # Step 2: Set up Python
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v4
22
+ with:
23
+ python-version: '3.x'
24
+
25
+ # Step 3: Install dependencies
26
+ - name: Install dependencies
27
+ run: pip install toml requests
28
+
29
+ # Step 4: Extract current version from pyproject.toml
30
+ - name: Extract current version
31
+ id: get_version
32
+ run: |
33
+ VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])")
34
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
35
+
36
+ # Step 5: Get latest version from PyPI
37
+ - name: Get latest version from PyPI
38
+ id: get_pypi_version
39
+ run: |
40
+ LATEST_PYPI_VERSION=$(python -c "import toml; import requests; PACKAGE_NAME = toml.load('pyproject.toml')['project']['name']; response = requests.get(f'https://pypi.org/pypi/{PACKAGE_NAME}/json'); print(response.json()['info']['version'])")
41
+ echo "LATEST_PYPI_VERSION=$LATEST_PYPI_VERSION" >> $GITHUB_ENV
42
+
43
+ # Step 6: Compare current version with the latest tag
44
+ - name: Check if version is bumped
45
+ id: check_version
46
+ run: |
47
+ if [ "${{ env.VERSION }}" = "${{ env.LATEST_PYPI_VERSION }}" ]; then
48
+ echo "Version not bumped. Exiting."
49
+ echo "version_bumped=false" >> $GITHUB_ENV
50
+ else
51
+ echo "Version bumped. Proceeding."
52
+ echo "version_bumped=true" >> $GITHUB_ENV
53
+ fi
54
+
55
+ # Step 5: Remove problematic optional dependencies
56
+ - name: Strip problematic optional dependencies
57
+ run: |
58
+ python - <<EOF
59
+ import toml
60
+ from pathlib import Path
61
+
62
+ pyproject_path = Path("pyproject.toml")
63
+ data = toml.loads(pyproject_path.read_text())
64
+
65
+ # Process optional dependencies
66
+ optional_deps = data.get("project", {}).get("optional-dependencies", {})
67
+ for key, deps in optional_deps.items():
68
+ new_deps = []
69
+ for dep in deps:
70
+ if "@git" in dep:
71
+ dep = dep.split("@git")[0].strip() # Remove everything after "@git"
72
+ new_deps.append(dep)
73
+ optional_deps[key] = new_deps
74
+
75
+ pyproject_path.write_text(toml.dumps(data))
76
+ EOF
77
+
78
+ # Step 7: Install Flit (only if version bumped)
79
+ - name: Install Flit
80
+ if: env.version_bumped == 'true'
81
+ run: pip install flit
82
+
83
+ # Step 8: Create .pypirc file (only if version bumped)
84
+ - name: Create .pypirc file
85
+ if: env.version_bumped == 'true'
86
+ run: |
87
+ echo "[pypi]" > ~/.pypirc
88
+ echo "username = __token__" >> ~/.pypirc
89
+ echo "password = ${{ secrets.PYPI_API_TOKEN }}" >> ~/.pypirc
90
+
91
+ # Step 9: Build and publish package (only if version bumped)
92
+ - name: Build and Publish Package
93
+ if: env.version_bumped == 'true'
94
+ run: |
95
+ flit build
96
+ flit publish
.github/workflows/sync-hf.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: [Python Test]
6
+ branches: [main]
7
+ types: [completed]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ sync-to-hub:
12
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+ lfs: true
19
+
20
+ - name: Push to hub
21
+ env:
22
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
23
+ run: |
24
+ # Configure Git user identity
25
+ git config user.name "github-actions[ci]"
26
+ git config user.email "github-actions[ci]@users.noreply.github.com"
27
+
28
+ # Configure LFS tracking
29
+ git lfs track "*.pdf"
30
+ git lfs track "*.png"
31
+
32
+ # Create a new orphan branch (no history)
33
+ git checkout --orphan hf-clean
34
+
35
+ git add .
36
+ git commit -m "Clean sync from main branch - $(date '+%Y-%m-%d %H:%M:%S')"
37
+
38
+ # Force push to Hugging Face main branch
39
+ git push -f https://HF_USERNAME:[email protected]/spaces/atomind/mlip-arena hf-clean:main
.github/workflows/test.yaml ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ env:
10
+ UV_SYSTEM_PYTHON: 1
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+
16
+ strategy:
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12"]
19
+
20
+ steps:
21
+ - name: Checkout PR with full history
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v6
28
+ with:
29
+ enable-cache: true
30
+ cache-dependency-glob: "pyproject.toml"
31
+
32
+ - name: Set up Python ${{ matrix.python-version }}
33
+ uses: actions/setup-python@v5
34
+ with:
35
+ python-version: ${{ matrix.python-version }}
36
+
37
+ - name: Install dependencies
38
+ run: bash scripts/install-linux.sh
39
+
40
+ - name: List dependencies
41
+ run: pip list
42
+
43
+ - name: Login to Hugging Face
44
+ env:
45
+ HF_TOKEN: ${{ secrets.HF_TOKEN_READ_ONLY }}
46
+ run: huggingface-cli login --token $HF_TOKEN
47
+
48
+ - name: Run tests
49
+ env:
50
+ PREFECT_API_KEY: ${{ secrets.PREFECT_API_KEY }}
51
+ PREFECT_API_URL: ${{ secrets.PREFECT_API_URL }}
52
+ run: pytest -vra -n 5 --dist=loadscope tests
53
+
54
+ - name: Squash commits and trial push to Hugging Face
55
+ if: github.event_name == 'pull_request'
56
+ id: trial_push
57
+ env:
58
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
59
+ TRIAL_BRANCH: trial-sync-${{ github.sha }}-${{ matrix.python-version }}
60
+ run: |
61
+ # Configure Git user identity
62
+ git config user.name "github-actions[ci]"
63
+ git config user.email "github-actions[ci]@users.noreply.github.com"
64
+
65
+ # Install Git LFS
66
+ sudo apt-get update
67
+ sudo apt-get install -y git-lfs
68
+ git lfs install
69
+
70
+ # Configure LFS tracking for binary files (only for HF push)
71
+ git lfs track "*.pdf"
72
+ git lfs track "*.png"
73
+
74
+ git add .gitattributes
75
+
76
+ # Setup LFS for the remote
77
+ git lfs fetch
78
+ git lfs checkout
79
+
80
+ # Rebase and squash all PR commits into one
81
+ BASE=$(git merge-base origin/main HEAD)
82
+ git reset --soft $BASE
83
+
84
+ # Re-add all files (binary files will now be tracked by LFS)
85
+ git add .
86
+ git commit -m "Squashed commit from PR #${{ github.event.pull_request.number }}"
87
+
88
+ # Create a new orphan branch (no history)
89
+ git checkout --orphan hf-clean
90
+
91
+ git add .
92
+ git commit -m "Clean sync from main branch - $(date '+%Y-%m-%d %H:%M:%S')"
93
+
94
+ # Push to temporary branch on Hugging Face
95
+ git push -f https://HF_USERNAME:[email protected]/spaces/atomind/mlip-arena HEAD:refs/heads/$TRIAL_BRANCH
96
+
97
+ - name: Delete trial branch from Hugging Face
98
+ if: steps.trial_push.outcome == 'success'
99
+ env:
100
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
101
+ TRIAL_BRANCH: trial-sync-${{ github.sha }}-${{ matrix.python-version }}
102
+ run: |
103
+ git push https://HF_USERNAME:[email protected]/spaces/atomind/mlip-arena --delete $TRIAL_BRANCH || true
.gitignore ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.out
2
+ *.extxyz
3
+ *.traj
4
+ mlip_arena/tasks/*/
5
+ benchmarks/
6
+ lab/
7
+ manuscripts/
8
+ datasets/
9
+
10
+ # Byte-compiled / optimized / DLL files
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+
15
+ # C extensions
16
+ *.so
17
+
18
+ # Distribution / packaging
19
+ .Python
20
+ build/
21
+ develop-eggs/
22
+ dist/
23
+ downloads/
24
+ eggs/
25
+ .eggs/
26
+ lib/
27
+ lib64/
28
+ parts/
29
+ sdist/
30
+ var/
31
+ wheels/
32
+ share/python-wheels/
33
+ *.egg-info/
34
+ .installed.cfg
35
+ *.egg
36
+ MANIFEST
37
+
38
+ # PyInstaller
39
+ # Usually these files are written by a python script from a template
40
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
41
+ *.manifest
42
+ *.spec
43
+
44
+ # Installer logs
45
+ pip-log.txt
46
+ pip-delete-this-directory.txt
47
+
48
+ # Unit test / coverage reports
49
+ htmlcov/
50
+ .tox/
51
+ .nox/
52
+ .coverage
53
+ .coverage.*
54
+ .cache
55
+ nosetests.xml
56
+ coverage.xml
57
+ *.cover
58
+ *.py,cover
59
+ .hypothesis/
60
+ .pytest_cache/
61
+ cover/
62
+
63
+ # Translations
64
+ *.mo
65
+ *.pot
66
+
67
+ # Django stuff:
68
+ *.log
69
+ local_settings.py
70
+ db.sqlite3
71
+ db.sqlite3-journal
72
+
73
+ # Flask stuff:
74
+ instance/
75
+ .webassets-cache
76
+
77
+ # Scrapy stuff:
78
+ .scrapy
79
+
80
+ # Sphinx documentation
81
+ docs/_build/
82
+
83
+ # PyBuilder
84
+ .pybuilder/
85
+ target/
86
+
87
+ # Jupyter Notebook
88
+ .ipynb_checkpoints
89
+
90
+ # IPython
91
+ profile_default/
92
+ ipython_config.py
93
+
94
+ # pyenv
95
+ # For a library or package, you might want to ignore these files since the code is
96
+ # intended to run in multiple environments; otherwise, check them in:
97
+ # .python-version
98
+
99
+ # pipenv
100
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
101
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
102
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
103
+ # install all needed dependencies.
104
+ #Pipfile.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ #poetry.lock
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ #pdm.lock
116
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
117
+ # in version control.
118
+ # https://pdm.fming.dev/#use-with-ide
119
+ .pdm.toml
120
+
121
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
122
+ __pypackages__/
123
+
124
+ # Celery stuff
125
+ celerybeat-schedule
126
+ celerybeat.pid
127
+
128
+ # SageMath parsed files
129
+ *.sage.py
130
+
131
+ # Environments
132
+ .env
133
+ .venv
134
+ env/
135
+ venv/
136
+ ENV/
137
+ env.bak/
138
+ venv.bak/
139
+
140
+ # Spyder project settings
141
+ .spyderproject
142
+ .spyproject
143
+
144
+ # Rope project settings
145
+ .ropeproject
146
+
147
+ # mkdocs documentation
148
+ /site
149
+
150
+ # mypy
151
+ .mypy_cache/
152
+ .dmypy.json
153
+ dmypy.json
154
+
155
+ # Pyre type checker
156
+ .pyre/
157
+
158
+ # pytype static type analyzer
159
+ .pytype/
160
+
161
+ # Cython debug symbols
162
+ cython_debug/
163
+
164
+ # PyCharm
165
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
166
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
167
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
168
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
169
+ #.idea/
.streamlit/config.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [server]
2
+ fileWatcherType = "poll"
CITATION.cff ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This CITATION.cff file was generated with cffinit.
2
+ # Visit https://bit.ly/cffinit to generate yours today!
3
+
4
+ cff-version: 1.2.0
5
+ title: MLIP Arena
6
+ message: >-
7
+ If you use this software, please cite it using the
8
+ metadata from this file.
9
+ type: software
10
+ authors:
11
+ - given-names: Yuan
12
+ family-names: Chiang
13
14
+ affiliation: Lawrence Berkeley National Laboratory
15
+ orcid: 'https://orcid.org/0000-0002-4017-7084'
16
+ repository-code: 'https://github.com/atomind-ai/mlip-arena'
17
+ keywords:
18
+ - Quantum Chemistry
19
+ - Foundation Model
20
+ - Interatomic Potentials
21
+ - Machine Learning
22
+ - Force Fields
23
+ license: Apache-2.0
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MLIP Arena
3
+ emoji: ⚛
4
+ sdk: streamlit
5
+ sdk_version: 1.43.2 # The latest supported version
6
+ python_version: 3.11
7
+ app_file: serve/app.py
8
+ colorFrom: indigo
9
+ colorTo: yellow
10
+ pinned: true
11
+ short_description: Benchmark machine learning interatomic potential at scale
12
+ ---
13
+
14
+
benchmarks/bzo/dft.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
benchmarks/bzo/pbe/mode-1.npy ADDED
Binary file (248 Bytes). View file
 
benchmarks/bzo/pbe/phonopy_params.yaml ADDED
The diff for this file is too large to render. See raw diff
 
benchmarks/c2db/ALIGNN.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce4d250afce0a7ef62dd27c5531b1e3a91f761035cc595e64ff6aae225e4ad73
3
+ size 272171
benchmarks/c2db/CHGNet.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6063fa72efb16a5255b79f5e1a03bd13409ed129016496ff1f494c6f83b98be
3
+ size 292909
benchmarks/c2db/M3GNet.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:32e1517a85a1b64f12fb262a0948a95be58c69edde133ce7ddf683154b8f2a95
3
+ size 290358
benchmarks/c2db/MACE-MP(M).parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f722eac6799bfecaa02188d59475862895a639cc596fa8b7d1e9d2b96cfb415b
3
+ size 293633
benchmarks/c2db/MACE-MPA.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c3ea679b5f6c9940358a2121a496544be91ba01ed8383509c65773f9fc69b9ec
3
+ size 293820
benchmarks/c2db/MatterSim.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d150c1b31b99ddcbbf21401189289aead13791c683aa379d75163b8bc4dbc6b4
3
+ size 293177
benchmarks/c2db/ORBv2.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2496f96d4aff1536936e58e65c1d608cc1953d41006221ba62ea2daab23f30b
3
+ size 293012
benchmarks/c2db/SevenNet.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0c2ee18ce70f24f70e65d70c2e54151e86dd0ccb3e412b8fbbc572e44e8bf5e8
3
+ size 293973
benchmarks/c2db/analysis.ipynb ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "0625f0a1",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import random\n",
11
+ "from pathlib import Path\n",
12
+ "\n",
13
+ "import numpy as np\n",
14
+ "from ase.db import connect\n",
15
+ "\n",
16
+ "random.seed(0)\n",
17
+ "\n",
18
+ "DATA_DIR = Path(\".\")\n",
19
+ "\n",
20
+ "db = connect(DATA_DIR / \"c2db.db\")\n",
21
+ "random_indices = random.sample(range(1, len(db) + 1), 1000)\n"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": null,
27
+ "id": "005708b9",
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "import itertools\n",
32
+ "\n",
33
+ "import pandas as pd\n",
34
+ "import phonopy\n",
35
+ "from tqdm.auto import tqdm\n",
36
+ "\n",
37
+ "from mlip_arena.models import MLIPEnum\n",
38
+ "\n",
39
+ "for row, model in tqdm(\n",
40
+ " itertools.product(db.select(filter=lambda r: r[\"id\"] in random_indices), MLIPEnum)\n",
41
+ "):\n",
42
+ " uid = row[\"uid\"]\n",
43
+ "\n",
44
+ " if Path(f\"{model.name}.parquet\").exists():\n",
45
+ " df = pd.read_parquet(f\"{model.name}.parquet\")\n",
46
+ " if uid in df[\"uid\"].unique():\n",
47
+ " continue\n",
48
+ " else:\n",
49
+ " df = pd.DataFrame(columns=[\"model\", \"uid\", \"eigenvalues\", \"frequencies\"])\n",
50
+ "\n",
51
+ " try:\n",
52
+ " path = Path(model.name) / uid\n",
53
+ " phonon = phonopy.load(path / \"phonopy.yaml\")\n",
54
+ " frequencies = phonon.get_frequencies(q=(0, 0, 0))\n",
55
+ "\n",
56
+ " data = np.load(path / \"elastic.npz\")\n",
57
+ "\n",
58
+ " eigenvalues = data[\"eigenvalues\"]\n",
59
+ "\n",
60
+ " new_row = pd.DataFrame(\n",
61
+ " [\n",
62
+ " {\n",
63
+ " \"model\": model.name,\n",
64
+ " \"uid\": uid,\n",
65
+ " \"eigenvalues\": eigenvalues,\n",
66
+ " \"frequencies\": frequencies,\n",
67
+ " }\n",
68
+ " ]\n",
69
+ " )\n",
70
+ "\n",
71
+ " df = pd.concat([df, new_row], ignore_index=True)\n",
72
+ " df.drop_duplicates(subset=[\"model\", \"uid\"], keep=\"last\", inplace=True)\n",
73
+ "\n",
74
+ " df.to_parquet(f\"{model.name}.parquet\", index=False)\n",
75
+ " except Exception:\n",
76
+ " pass\n"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "code",
81
+ "execution_count": 6,
82
+ "id": "b8d87638",
83
+ "metadata": {},
84
+ "outputs": [],
85
+ "source": [
86
+ "uids = []\n",
87
+ "stabilities = []\n",
88
+ "for row in db.select(filter=lambda r: r[\"id\"] in random_indices):\n",
89
+ " stable = row.key_value_pairs[\"dyn_stab\"]\n",
90
+ " if stable.lower() == \"unknown\":\n",
91
+ " stable = None\n",
92
+ " else:\n",
93
+ " stable = True if stable.lower() == \"yes\" else False\n",
94
+ " uids.append(row.key_value_pairs[\"uid\"])\n",
95
+ " stabilities.append(stable)\n",
96
+ "\n",
97
+ "\n",
98
+ "stabilities = np.array(stabilities)\n",
99
+ "\n",
100
+ "(stabilities == True).sum(), (stabilities == False).sum(), (stabilities == None).sum()"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "markdown",
105
+ "id": "a3c516a7",
106
+ "metadata": {},
107
+ "source": []
108
+ },
109
+ {
110
+ "cell_type": "code",
111
+ "execution_count": 104,
112
+ "id": "0052d0ff",
113
+ "metadata": {},
114
+ "outputs": [],
115
+ "source": [
116
+ "%matplotlib inline\n",
117
+ "\n",
118
+ "from pathlib import Path\n",
119
+ "\n",
120
+ "import numpy as np\n",
121
+ "import pandas as pd\n",
122
+ "from matplotlib import pyplot as plt\n",
123
+ "from sklearn.metrics import (\n",
124
+ " ConfusionMatrixDisplay,\n",
125
+ " classification_report,\n",
126
+ " confusion_matrix,\n",
127
+ ")\n",
128
+ "\n",
129
+ "from mlip_arena.models import MLIPEnum\n",
130
+ "\n",
131
+ "thres = -1e-7\n",
132
+ "\n",
133
+ "select_models = [\n",
134
+ " \"ALIGNN\",\n",
135
+ " \"CHGNet\",\n",
136
+ " \"M3GNet\",\n",
137
+ " \"MACE-MP(M)\",\n",
138
+ " \"MACE-MPA\",\n",
139
+ " \"MatterSim\",\n",
140
+ " \"ORBv2\",\n",
141
+ " \"SevenNet\",\n",
142
+ "]\n",
143
+ "\n",
144
+ "with plt.style.context(\"default\"):\n",
145
+ " # plt.rcParams.update({\n",
146
+ " # # \"title.fontsize\": 10,\n",
147
+ " # \"axes.titlesize\": 10,\n",
148
+ " # \"axes.labelsize\": 8,\n",
149
+ " # })\n",
150
+ "\n",
151
+ " SMALL_SIZE = 8\n",
152
+ " MEDIUM_SIZE = 10\n",
153
+ " BIGGER_SIZE = 12\n",
154
+ " plt.rcParams.update(\n",
155
+ " {\n",
156
+ " \"font.size\": SMALL_SIZE,\n",
157
+ " \"axes.titlesize\": MEDIUM_SIZE,\n",
158
+ " \"axes.labelsize\": MEDIUM_SIZE,\n",
159
+ " \"xtick.labelsize\": MEDIUM_SIZE,\n",
160
+ " \"ytick.labelsize\": MEDIUM_SIZE,\n",
161
+ " \"legend.fontsize\": SMALL_SIZE,\n",
162
+ " \"figure.titlesize\": BIGGER_SIZE,\n",
163
+ " }\n",
164
+ " )\n",
165
+ "\n",
166
+ " fig, axs = plt.subplots(\n",
167
+ " nrows=int(np.ceil(len(MLIPEnum) / 4)),\n",
168
+ " ncols=4,\n",
169
+ " figsize=(6, 3 * int(np.ceil(len(select_models) / 4))),\n",
170
+ " sharey=True,\n",
171
+ " sharex=True,\n",
172
+ " layout=\"constrained\",\n",
173
+ " )\n",
174
+ " axs = axs.flatten()\n",
175
+ " plot_idx = 0\n",
176
+ "\n",
177
+ " for model in MLIPEnum:\n",
178
+ " fpath = DATA_DIR / f\"{model.name}.parquet\"\n",
179
+ " if not fpath.exists():\n",
180
+ " continue\n",
181
+ "\n",
182
+ " if model.name not in select_models:\n",
183
+ " continue\n",
184
+ "\n",
185
+ " df = pd.read_parquet(fpath)\n",
186
+ " df[\"eigval_min\"] = df[\"eigenvalues\"].apply(\n",
187
+ " lambda x: x.min() if np.isreal(x).all() else thres\n",
188
+ " )\n",
189
+ " df[\"freq_min\"] = df[\"frequencies\"].apply(\n",
190
+ " lambda x: x.min() if np.isreal(x).all() else thres\n",
191
+ " )\n",
192
+ " df[\"dyn_stab\"] = ~np.logical_or(\n",
193
+ " df[\"eigval_min\"] < thres, df[\"freq_min\"] < thres\n",
194
+ " )\n",
195
+ "\n",
196
+ " arg = np.argsort(uids)\n",
197
+ " uids_sorted = np.array(uids)[arg]\n",
198
+ " stabilities_sorted = stabilities[arg]\n",
199
+ "\n",
200
+ " sorted_df = (\n",
201
+ " df[df[\"uid\"].isin(uids_sorted)].set_index(\"uid\").reindex(uids_sorted)\n",
202
+ " )\n",
203
+ " mask = ~(stabilities_sorted == None)\n",
204
+ "\n",
205
+ " y_true = stabilities_sorted[mask].astype(\"int\")\n",
206
+ " y_pred = sorted_df[\"dyn_stab\"][mask].fillna(-1).astype(\"int\")\n",
207
+ " cm = confusion_matrix(y_true, y_pred, labels=[1, 0, -1])\n",
208
+ "\n",
209
+ " ax = axs[plot_idx]\n",
210
+ " ConfusionMatrixDisplay(\n",
211
+ " cm, display_labels=[\"stable\", \"unstable\", \"missing\"]\n",
212
+ " ).plot(ax=ax, cmap=\"Blues\", colorbar=False)\n",
213
+ "\n",
214
+ " ax.set_title(model.name)\n",
215
+ " ax.set_xlabel(\"Predicted\")\n",
216
+ " ax.set_ylabel(\"True\")\n",
217
+ " ax.set_xticks([0, 1, 2])\n",
218
+ " ax.set_xticklabels([\"stable\", \"unstable\", \"missing\"])\n",
219
+ " ax.set_yticks([0, 1, 2])\n",
220
+ " ax.set_yticklabels([\"stable\", \"unstable\", \"missing\"])\n",
221
+ "\n",
222
+ " plot_idx += 1\n",
223
+ "\n",
224
+ " # Hide unused subplots\n",
225
+ " for i in range(plot_idx, len(axs)):\n",
226
+ " fig.delaxes(axs[i])\n",
227
+ "\n",
228
+ " # plt.tight_layout()\n",
229
+ " plt.savefig(\"c2db-confusion_matrices.pdf\", bbox_inches=\"tight\")\n",
230
+ " plt.show()\n"
231
+ ]
232
+ },
233
+ {
234
+ "cell_type": "code",
235
+ "execution_count": 52,
236
+ "id": "573b3c38",
237
+ "metadata": {},
238
+ "outputs": [],
239
+ "source": [
240
+ "import pandas as pd\n",
241
+ "from sklearn.metrics import confusion_matrix\n",
242
+ "\n",
243
+ "from mlip_arena.models import MLIPEnum\n",
244
+ "\n",
245
+ "thres = -1e-7\n",
246
+ "\n",
247
+ "summary_df = pd.DataFrame(columns=[\"Model\", \"Stable F1\", \"Unstable F1\", \"Weighted F1\"])\n",
248
+ "\n",
249
+ "for model in MLIPEnum:\n",
250
+ " fpath = DATA_DIR / f\"{model.name}.parquet\"\n",
251
+ "\n",
252
+ " if not fpath.exists() or model.name not in select_models:\n",
253
+ " # print(f\"File {fpath} does not exist\")\n",
254
+ " continue\n",
255
+ " df = pd.read_parquet(fpath)\n",
256
+ "\n",
257
+ " df[\"eigval_min\"] = df[\"eigenvalues\"].apply(\n",
258
+ " lambda x: x.min() if np.isreal(x).all() else thres\n",
259
+ " )\n",
260
+ " df[\"freq_min\"] = df[\"frequencies\"].apply(\n",
261
+ " lambda x: x.min() if np.isreal(x).all() else thres\n",
262
+ " )\n",
263
+ " df[\"dyn_stab\"] = ~np.logical_or(df[\"eigval_min\"] < thres, df[\"freq_min\"] < thres)\n",
264
+ "\n",
265
+ " arg = np.argsort(uids)\n",
266
+ " uids = np.array(uids)[arg]\n",
267
+ " stabilities = stabilities[arg]\n",
268
+ "\n",
269
+ " sorted_df = df[df[\"uid\"].isin(uids)].sort_values(by=\"uid\")\n",
270
+ "\n",
271
+ " # sorted_df = sorted_df.reindex(uids).reset_index()\n",
272
+ " sorted_df = sorted_df.set_index(\"uid\").reindex(uids) # .loc[uids].reset_index()\n",
273
+ "\n",
274
+ " sorted_df = sorted_df.loc[uids]\n",
275
+ " # mask = ~np.logical_or(sorted_df['dyn_stab'].isna().values, stabilities == None)\n",
276
+ " mask = ~(stabilities == None)\n",
277
+ "\n",
278
+ " y_true = stabilities[mask].astype(\"int\")\n",
279
+ " y_pred = sorted_df[\"dyn_stab\"][mask].fillna(-1).astype(\"int\")\n",
280
+ " cm = confusion_matrix(y_true, y_pred, labels=[1, 0, -1])\n",
281
+ " # print(model)\n",
282
+ " # print(cm)\n",
283
+ " # print(classification_report(y_true, y_pred, labels=[1, 0], target_names=['stable', 'unstable'], digits=3, output_dict=False))\n",
284
+ "\n",
285
+ " report = classification_report(\n",
286
+ " y_true,\n",
287
+ " y_pred,\n",
288
+ " labels=[1, 0],\n",
289
+ " target_names=[\"stable\", \"unstable\"],\n",
290
+ " digits=3,\n",
291
+ " output_dict=True,\n",
292
+ " )\n",
293
+ "\n",
294
+ " summary_df = pd.concat(\n",
295
+ " [\n",
296
+ " summary_df,\n",
297
+ " pd.DataFrame(\n",
298
+ " [\n",
299
+ " {\n",
300
+ " \"Model\": model.name,\n",
301
+ " \"Stable F1\": report[\"stable\"][\"f1-score\"],\n",
302
+ " \"Unstable F1\": report[\"unstable\"][\"f1-score\"],\n",
303
+ " \"Macro F1\": report[\"macro avg\"][\"f1-score\"],\n",
304
+ " # 'Micro F1': report['micro avg']['f1-score'],\n",
305
+ " \"Weighted F1\": report[\"weighted avg\"][\"f1-score\"],\n",
306
+ " }\n",
307
+ " ]\n",
308
+ " ),\n",
309
+ " ],\n",
310
+ " ignore_index=True,\n",
311
+ " )\n",
312
+ "\n",
313
+ " # break"
314
+ ]
315
+ },
316
+ {
317
+ "cell_type": "code",
318
+ "execution_count": 85,
319
+ "id": "df660870",
320
+ "metadata": {},
321
+ "outputs": [],
322
+ "source": [
323
+ "summary_df = summary_df.sort_values(by=[\"Macro F1\", \"Weighted F1\"], ascending=False)\n",
324
+ "summary_df.to_latex(\"c2db_summary_table.tex\", index=False, float_format=\"%.3f\")"
325
+ ]
326
+ },
327
+ {
328
+ "cell_type": "code",
329
+ "execution_count": 103,
330
+ "id": "18f4a59b",
331
+ "metadata": {},
332
+ "outputs": [],
333
+ "source": [
334
+ "from matplotlib import cm\n",
335
+ "\n",
336
+ "# Metrics and bar settings\n",
337
+ "metrics = [\"Stable F1\", \"Unstable F1\", \"Macro F1\", \"Weighted F1\"]\n",
338
+ "bar_width = 0.2\n",
339
+ "x = np.arange(len(summary_df))\n",
340
+ "\n",
341
+ "# Get Set2 colormap (as RGBA)\n",
342
+ "cmap = plt.get_cmap(\"tab20\")\n",
343
+ "colors = {metric: cmap(i) for i, metric in enumerate(metrics)}\n",
344
+ "\n",
345
+ "with plt.style.context(\"default\"):\n",
346
+ " plt.rcParams.update(\n",
347
+ " {\n",
348
+ " \"font.size\": SMALL_SIZE,\n",
349
+ " \"axes.titlesize\": MEDIUM_SIZE,\n",
350
+ " \"axes.labelsize\": MEDIUM_SIZE,\n",
351
+ " \"xtick.labelsize\": MEDIUM_SIZE,\n",
352
+ " \"ytick.labelsize\": MEDIUM_SIZE,\n",
353
+ " \"legend.fontsize\": SMALL_SIZE,\n",
354
+ " \"figure.titlesize\": BIGGER_SIZE,\n",
355
+ " }\n",
356
+ " )\n",
357
+ "\n",
358
+ " fig, ax = plt.subplots(figsize=(4, 3), layout=\"constrained\")\n",
359
+ "\n",
360
+ " # Bar positions\n",
361
+ " positions = {\n",
362
+ " \"Stable F1\": x - 1.5 * bar_width,\n",
363
+ " \"Unstable F1\": x - 0.5 * bar_width,\n",
364
+ " \"Macro F1\": x + 0.5 * bar_width,\n",
365
+ " \"Weighted F1\": x + 1.5 * bar_width,\n",
366
+ " }\n",
367
+ "\n",
368
+ " # Plot each metric with assigned color\n",
369
+ " for metric, pos in positions.items():\n",
370
+ " ax.bar(\n",
371
+ " pos, summary_df[metric], width=bar_width, label=metric, color=colors[metric]\n",
372
+ " )\n",
373
+ "\n",
374
+ " ax.set_xlabel(\"Model\")\n",
375
+ " ax.set_ylabel(\"F1 Score\")\n",
376
+ " # ax.set_title('F1 Scores by Model and Class')\n",
377
+ " ax.set_xticks(x)\n",
378
+ " ax.set_xticklabels(summary_df[\"Model\"], rotation=45, ha=\"right\")\n",
379
+ " ax.legend(ncols=2, bbox_to_anchor=(0.5, 1), loc=\"upper center\", fontsize=SMALL_SIZE)\n",
380
+ " # ax.legend(ncols=2, fontsize=SMALL_SIZE)\n",
381
+ " ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
382
+ " plt.tight_layout()\n",
383
+ " plt.ylim(0, 0.9)\n",
384
+ " plt.grid(axis=\"y\", linestyle=\"--\", alpha=0.6)\n",
385
+ "\n",
386
+ " plt.savefig(\"c2db_f1_bar.pdf\", bbox_inches=\"tight\")\n",
387
+ " plt.show()"
388
+ ]
389
+ },
390
+ {
391
+ "cell_type": "code",
392
+ "execution_count": null,
393
+ "id": "1c50f705",
394
+ "metadata": {},
395
+ "outputs": [],
396
+ "source": []
397
+ }
398
+ ],
399
+ "metadata": {
400
+ "kernelspec": {
401
+ "display_name": "mlip-arena",
402
+ "language": "python",
403
+ "name": "mlip-arena"
404
+ }
405
+ },
406
+ "nbformat": 4,
407
+ "nbformat_minor": 5
408
+ }
benchmarks/c2db/c2db-confusion_matrices.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:463968f63e87ca0a7acd2e719cc481d0e3c5f5dd69ccf8f8659bddf6aa3b1e93
3
+ size 21238
benchmarks/c2db/c2db-f1_bar.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3d0c862d4efa2d9c83ac4fbe26eeef66a8f8017b37d955b70e414fdbea94aabd
3
+ size 17883
benchmarks/c2db/c2db.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:caf58205692de480e06149ac43a437385f18e14582e7d9a8dab8b3cb5d4bd678
3
+ size 70762496
benchmarks/c2db/copy.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7fdc16667361b10bfb032862d5d0610c242d75cb88f7f3883c43a406b245e991
3
+ size 21349
benchmarks/c2db/run.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import product
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from dask.distributed import Client
7
+ from dask_jobqueue import SLURMCluster
8
+ from mlip_arena.models import MLIPEnum
9
+ from mlip_arena.tasks import ELASTICITY, OPT, PHONON
10
+ from mlip_arena.tasks.optimize import run as OPT
11
+ from mlip_arena.tasks.utils import get_calculator
12
+ from numpy import linalg as LA
13
+ from prefect import flow, task
14
+ from prefect_dask import DaskTaskRunner
15
+ from tqdm.auto import tqdm
16
+
17
+ from ase.db import connect
18
+
19
+ select_models = [
20
+ "ALIGNN",
21
+ "CHGNet",
22
+ "M3GNet",
23
+ "MACE-MP(M)",
24
+ "MACE-MPA",
25
+ "MatterSim",
26
+ "ORBv2",
27
+ "SevenNet",
28
+ ]
29
+
30
+ def elastic_tensor_to_voigt(C):
31
+ """
32
+ Convert a rank-4 (3x3x3x3) elastic tensor into a rank-2 (6x6) tensor using Voigt notation.
33
+
34
+ Parameters:
35
+ C (numpy.ndarray): A 3x3x3x3 elastic tensor.
36
+
37
+ Returns:
38
+ numpy.ndarray: A 6x6 elastic tensor in Voigt notation.
39
+ """
40
+ # voigt_map = {
41
+ # (0, 0): 0, (1, 1): 1, (2, 2): 2, # Normal components
42
+ # (1, 2): 3, (2, 1): 3, # Shear components
43
+ # (0, 2): 4, (2, 0): 4,
44
+ # (0, 1): 5, (1, 0): 5
45
+ # }
46
+ voigt_map = {
47
+ (0, 0): 0,
48
+ (1, 1): 1,
49
+ (2, 2): -1, # Normal components
50
+ (1, 2): -1,
51
+ (2, 1): -1, # Shear components
52
+ (0, 2): -1,
53
+ (2, 0): -1,
54
+ (0, 1): 2,
55
+ (1, 0): 2,
56
+ }
57
+
58
+ C_voigt = np.zeros((3, 3))
59
+
60
+ for i in range(3):
61
+ for j in range(3):
62
+ for k in range(3):
63
+ for l in range(3):
64
+ alpha = voigt_map[(i, j)]
65
+ beta = voigt_map[(k, l)]
66
+
67
+ if alpha == -1 or beta == -1:
68
+ continue
69
+
70
+ factor = 1
71
+ # if alpha in [3, 4, 5]:
72
+ if alpha == 2:
73
+ factor = factor * (2**0.5)
74
+ if beta == 2:
75
+ factor = factor * (2**0.5)
76
+
77
+ C_voigt[alpha, beta] = C[i, j, k, l] * factor
78
+
79
+ return C_voigt
80
+
81
+
82
+ # -
83
+
84
+
85
+ @task
86
+ def run_one(model, row):
87
+ if Path(f"{model.name}.pkl").exists():
88
+ df = pd.read_pickle(f"{model.name}.pkl")
89
+
90
+ # if row.key_value_pairs.get('uid', None) in df['uid'].unique():
91
+ # pass
92
+ else:
93
+ df = pd.DataFrame(columns=["model", "uid", "eigenvalues", "frequencies"])
94
+
95
+ atoms = row.toatoms()
96
+ # print(data := row.key_value_pairs)
97
+
98
+ calc = get_calculator(model)
99
+
100
+ result_opt = OPT(
101
+ atoms,
102
+ calc,
103
+ optimizer="FIRE",
104
+ criterion=dict(fmax=0.05, steps=500),
105
+ symmetry=True,
106
+ )
107
+
108
+ atoms = result_opt["atoms"]
109
+
110
+ result_elastic = ELASTICITY(
111
+ atoms,
112
+ calc,
113
+ optimizer="FIRE",
114
+ criterion=dict(fmax=0.05, steps=500),
115
+ pre_relax=False,
116
+ )
117
+
118
+ elastic_tensor = elastic_tensor_to_voigt(result_elastic["elastic_tensor"])
119
+ eigenvalues, eigenvectors = LA.eig(elastic_tensor)
120
+
121
+ outdir = Path(f"{model.name}") / row.key_value_pairs.get(
122
+ "uid", atoms.get_chemical_formula()
123
+ )
124
+ outdir.mkdir(parents=True, exist_ok=True)
125
+
126
+ np.savez(outdir / "elastic.npz", tensor=elastic_tensor, eigenvalues=eigenvalues)
127
+
128
+ result_phonon = PHONON(
129
+ atoms,
130
+ calc,
131
+ supercell_matrix=(2, 2, 1),
132
+ outdir=outdir,
133
+ )
134
+
135
+ frequencies = result_phonon["phonon"].get_frequencies(q=(0, 0, 0))
136
+
137
+ new_row = pd.DataFrame(
138
+ [
139
+ {
140
+ "model": model.name,
141
+ "uid": row.key_value_pairs.get("uid", None),
142
+ "eigenvalues": eigenvalues,
143
+ "frequencies": frequencies,
144
+ }
145
+ ]
146
+ )
147
+
148
+ df = pd.concat([df, new_row], ignore_index=True)
149
+ df.drop_duplicates(subset=["model", "uid"], keep="last", inplace=True)
150
+
151
+ df.to_pickle(f"{model.name}.pkl")
152
+
153
+
154
+ @flow
155
+ def run_all():
156
+ import random
157
+
158
+ random.seed(0)
159
+
160
+ futures = []
161
+ with connect("c2db.db") as db:
162
+ random_indices = random.sample(range(1, len(db) + 1), 1000)
163
+ for row, model in tqdm(
164
+ product(db.select(filter=lambda r: r["id"] in random_indices), MLIPEnum)
165
+ ):
166
+ if model.name not in select_models:
167
+ continue
168
+ future = run_one.submit(model, row)
169
+ futures.append(future)
170
+ return [f.result(raise_on_failure=False) for f in futures]
171
+
172
+
173
+ # +
174
+
175
+
176
+ if __name__ == "__main__":
177
+ nodes_per_alloc = 1
178
+ gpus_per_alloc = 1
179
+ ntasks = 1
180
+
181
+ cluster_kwargs = dict(
182
+ cores=1,
183
+ memory="64 GB",
184
+ processes=1,
185
+ shebang="#!/bin/bash",
186
+ account="matgen",
187
+ walltime="00:30:00",
188
+ # job_cpu=128,
189
+ job_mem="0",
190
+ job_script_prologue=[
191
+ "source ~/.bashrc",
192
+ "module load python",
193
+ "source activate /pscratch/sd/c/cyrusyc/.conda/dev",
194
+ ],
195
+ job_directives_skip=["-n", "--cpus-per-task", "-J"],
196
+ job_extra_directives=[
197
+ "-J c2db",
198
+ "-q regular",
199
+ f"-N {nodes_per_alloc}",
200
+ "-C gpu",
201
+ f"-G {gpus_per_alloc}",
202
+ ],
203
+ )
204
+
205
+ cluster = SLURMCluster(**cluster_kwargs)
206
+ print(cluster.job_script())
207
+ cluster.adapt(minimum_jobs=25, maximum_jobs=50)
208
+ client = Client(cluster)
209
+ # -
210
+
211
+ run_all.with_options(
212
+ task_runner=DaskTaskRunner(address=client.scheduler.address), log_prints=True
213
+ )()
benchmarks/energy_conservation/run.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Task for running MD simulations and computing the differential entropy
3
+ of the simulated structures with respect to a reference dataset.
4
+
5
+ See https://github.com/dskoda/quests for differential entropy details.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from datetime import datetime
12
+
13
+ import numpy as np
14
+ from ase.io import read
15
+ from prefect import task
16
+ from prefect.cache_policies import INPUTS, TASK_SOURCE
17
+ from prefect.runtime import task_run
18
+
19
+ from mlip_arena.models import MLIPEnum
20
+ from mlip_arena.tasks.md import run as MD
21
+ from mlip_arena.tasks.utils import logger
22
+
23
+ try:
24
+ from quests.descriptor import get_descriptors
25
+ from quests.entropy import delta_entropy
26
+ except ImportError as e:
27
+ logger.warning(e)
28
+ logger.warning(
29
+ "quests is not installed. Please install it using `pip install quests` or following the instructions at https://github.com/dskoda/quests to use this module."
30
+ )
31
+
32
+
33
+ def get_entropy_from_path(
34
+ subset_path, dataset_path, dataset_desc_out_path, k=32, cutoff=5.0, h=0.015
35
+ ):
36
+ """
37
+ Computes the differential entropy of a subset of structures with respect
38
+ to a reference dataset.
39
+
40
+ Arguments:
41
+ subset_path (str): Path to the file containing the subset of structures.
42
+ dataset_path (str): Path to the file containing the full dataset of structures without the subset.
43
+ dataset_desc_out_path (str): Path to save the descriptors of the full dataset.
44
+ k (int, optional): Number of nearest neighbors used for descriptor calculation. Default is 32.
45
+ cutoff (float, optional): Cutoff distance for descriptor calculation. Default is 5.0.
46
+ h (float, optional): Bandwidth for the Gaussian kernel. Default is 0.015.
47
+
48
+ Returns:
49
+ np.ndarray: The differential entropy of the subset with respect to the dataset.
50
+ """
51
+
52
+ x_structures = read(dataset_path, index=":")
53
+ x_desc = get_descriptors(x_structures, k=k, cutoff=cutoff)
54
+ np.save(dataset_desc_out_path, x_desc)
55
+
56
+ y_structures = read(subset_path, index=":")
57
+ y_desc = get_descriptors(y_structures, k=k, cutoff=cutoff)
58
+
59
+ dH = delta_entropy(y_desc, x_desc, h=h)
60
+ return dH
61
+
62
+
63
+ def get_trajectory_entropy(
64
+ trajectory_dir,
65
+ start_idx,
66
+ end_idx,
67
+ step,
68
+ dataset_desc_path,
69
+ k=32,
70
+ cutoff=5.0,
71
+ h=0.015,
72
+ ):
73
+ """
74
+ Computes the differential entropy of a subset of structures in a trajectory with respect
75
+ to a reference dataset.
76
+
77
+ Arguments:
78
+ trajectory_dir (str): Path to the directory containing the trajectory files.
79
+ start_idx (int): Starting index of the subset of structures to select from each trajectory.
80
+ end_idx (int): Ending index of the subset of structures to select from each trajectory.
81
+ step (int): Step size of the subset of structures to select from each trajectory.
82
+ dataset_desc_path (str): Path to the file containing the descriptors of the full dataset of structures without the subset.
83
+ k (int, optional): Number of nearest neighbors used for descriptor calculation. Default is 32.
84
+ cutoff (float, optional): Cutoff distance for descriptor calculation. Default is 5.0.
85
+ h (float, optional): Bandwidth for the Gaussian kernel. Default is 0.015.
86
+
87
+ Choose start_idx, end_idx, step to select which structures to compute the differential entropy for, based on what sliding window is chosen.
88
+ e.g. window of size 5 with stride 2 means we select every other structure starting at index 2 (middle of the first window) to the -2 index (middle of the last window)
89
+
90
+ Returns:
91
+ np.ndarray: The differential entropy of the subset of structures in the trajectory with respect to the dataset.
92
+ """
93
+ structures = []
94
+ for traj_file in sorted(os.listdir(trajectory_dir)):
95
+ traj = read(os.path.join(trajectory_dir, traj_file), index=":")
96
+ every_other = traj[start_idx:end_idx:step]
97
+ structures.extend(every_other)
98
+
99
+ desc = get_descriptors(structures, k=k, cutoff=cutoff)
100
+ x_desc = np.load(dataset_desc_path)
101
+ dH = delta_entropy(desc, x_desc, h=h)
102
+ return dH
103
+
104
+
105
+ def run_simulations(model_names, structures, out_dir):
106
+ """
107
+ Runs simulations on a list of structures.
108
+
109
+ Parameters:
110
+ model_names (list[str]): List of models to use.
111
+ structures (list[ase.Atoms]): List of structures to simulate.
112
+ out_dir (str): Directory to save the simulation trajectories to.
113
+
114
+ Notes:
115
+ Structures are replicated to have at least 100 atoms and at most 500 atoms.
116
+ Structures are simulated with NVE MD at 1000 K for 5 ps.
117
+ Simulation trajectories are saved to files in out_dir, with each file named according to the index of the structure in the list.
118
+ """
119
+ min_atoms = 100
120
+ max_atoms = 500
121
+
122
+ futures = []
123
+
124
+ for model_name in model_names:
125
+ os.makedirs(out_dir, exist_ok=True)
126
+ model = MLIPEnum[model_name]
127
+ calc = model.value()
128
+
129
+ for i, atoms in enumerate(structures):
130
+ logger.info(
131
+ f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Running {model_name} on structure number {i}"
132
+ )
133
+
134
+ # Replicate the structure
135
+ n_atoms = len(atoms)
136
+ rep_factor = int(
137
+ np.ceil((min_atoms / n_atoms) ** (1 / 3))
138
+ ) # cube root since it's a 3D replication
139
+ supercell_atoms = atoms.repeat((rep_factor, rep_factor, rep_factor))
140
+ if len(supercell_atoms) > max_atoms:
141
+ logger.info(
142
+ f"Skipping structure {i} because it has too many atoms ({len(supercell_atoms)} > {max_atoms})"
143
+ )
144
+ continue # skip if it becomes too large
145
+
146
+ # Run NVE MD @ 1000K for 5 ps
147
+ future = MD.submit(
148
+ supercell_atoms,
149
+ calculator=calc,
150
+ ensemble="nve",
151
+ dynamics="velocityverlet",
152
+ time_step=1.0, # fs
153
+ total_time=5000, # 5 ps = 5000 fs
154
+ temperature=1000.0,
155
+ traj_file=f"{out_dir}/{i}.traj",
156
+ traj_interval=100,
157
+ zero_linear_momentum=True,
158
+ zero_angular_momentum=True,
159
+ )
160
+ futures.append(future)
161
+
162
+ return [f.result(raise_on_failure=False) for f in futures]
163
+
164
+
165
+ def _generate_task_run_name():
166
+ task_name = task_run.task_name
167
+ parameters = task_run.parameters
168
+
169
+ trajectory_dir = parameters["trajectory_dir"]
170
+ dataset_desc_path = parameters["dataset_desc_path"]
171
+
172
+ return f"{task_name}: {trajectory_dir} - {dataset_desc_path}"
173
+
174
+
175
+ @task(
176
+ name="Entropy along trajectory",
177
+ task_run_name=_generate_task_run_name,
178
+ cache_policy=TASK_SOURCE + INPUTS,
179
+ )
180
+ def run(
181
+ dataset_path,
182
+ model_names,
183
+ structures,
184
+ trajectory_dir,
185
+ start_idx,
186
+ end_idx,
187
+ step,
188
+ dataset_desc_path,
189
+ dH_out_path,
190
+ k=32,
191
+ cutoff=5.0,
192
+ h=0.015,
193
+ ):
194
+ # Get descriptors for the dataset. This should exclude the subset of structures used for simulations.
195
+ # This may take a while if the dataset is large - in that case, would recommend splitting the structures into separate chunks.
196
+ x_structures = read(dataset_path, index=":")
197
+ x_desc = get_descriptors(x_structures, k=k, cutoff=cutoff)
198
+ np.save(dataset_desc_path, x_desc)
199
+
200
+ # Run simulations
201
+ run_simulations(model_names, structures, trajectory_dir)
202
+
203
+ # Get entropy for structures along trajectories
204
+ dH = get_trajectory_entropy(
205
+ trajectory_dir,
206
+ start_idx,
207
+ end_idx,
208
+ step,
209
+ dataset_desc_path,
210
+ k=k,
211
+ cutoff=cutoff,
212
+ h=h,
213
+ )
214
+ np.save(dH_out_path, dH)
benchmarks/eos_alloy/run_Fe-Ni-Cr.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
benchmarks/eos_bulk/CHGNet.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:68871d694e93a3c3e7e272b9cbd87d3757e3bc689f30f3189db232d76e629c07
3
+ size 429910
benchmarks/eos_bulk/CHGNet_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bfde7530e6b0d2df5a30e1b7e3ec124fb2a86f6da8e35d2548d37d10a1eff1b1
3
+ size 387425
benchmarks/eos_bulk/M3GNet.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53dde465b5e10edd677f131f8a531e3dfc36303dd7ec7b9df0060c19847494d9
3
+ size 427419
benchmarks/eos_bulk/M3GNet_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eb43a3c74f3340100b1adb21b3f2d075451e1ffe88ac6d6662741bc4a0576eb8
3
+ size 397450
benchmarks/eos_bulk/MACE-MP(M).parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ff9769eeb83042129767aeff975eb04dee8efae12e96fbd46cd3039eeda26705
3
+ size 427896
benchmarks/eos_bulk/MACE-MP(M)_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7e5507cdc5fe558b5d3fe2ea8f1dd577ac444e82c5347b5fbe738a4f855dffcb
3
+ size 397379
benchmarks/eos_bulk/MACE-MPA.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53fcd188baddd4d5e797c5aa3de1b4368db711ebd29b7877cfe224856ba9d171
3
+ size 428888
benchmarks/eos_bulk/MACE-MPA_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f3032d5a156febdd9580fa3d86cb1a84236374bcac6ccb22d18a948767db502
3
+ size 394748
benchmarks/eos_bulk/MatterSim.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e6717650b97782de6f90e4473075410fe4540279eb39338d2234d3c9399079b3
3
+ size 389586
benchmarks/eos_bulk/MatterSim_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb1f10a60495f5e88ea8cf737fd7b47d1c471fda422374ee519d14f531c732f8
3
+ size 290191
benchmarks/eos_bulk/ORBv2.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ae13c9af1ae7fafe2a42ed4c47e2ba0f036abfa64a87ca517b92d89c62fcbfd9
3
+ size 427105
benchmarks/eos_bulk/ORBv2_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7eb0a3060b8a2d3541b8fb1083176c88aae0a8be0008e84d5770998b01742216
3
+ size 402554
benchmarks/eos_bulk/SevenNet.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64be88ec2632cdabf79daa01acb2cf2ef19fef0557813df5502c4f71ec566f4e
3
+ size 428341
benchmarks/eos_bulk/SevenNet_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f484928f5086e8d1411a198ac69bfe44313597909b72c8676e9131cce1660f1
3
+ size 398295
benchmarks/eos_bulk/analyze.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from ase.db import connect
6
+ from scipy import stats
7
+
8
+ from mlip_arena.models import REGISTRY, MLIPEnum
9
+
10
+ DATA_DIR = Path(__file__).parent.absolute()
11
+
12
+
13
+ def load_wbm_structures():
14
+ """
15
+ Load the WBM structures from a ASE DB file.
16
+ """
17
+ with connect(DATA_DIR.parent / "wbm_structures.db") as db:
18
+ for row in db.select():
19
+ yield row.toatoms(add_additional_information=True)
20
+
21
+ def gather_results():
22
+ for model in MLIPEnum:
23
+ if "eos_bulk" not in REGISTRY[model.name].get("gpu-tasks", []):
24
+ continue
25
+
26
+ if (DATA_DIR / f"{model.name}.parquet").exists():
27
+ continue
28
+
29
+ all_data = []
30
+
31
+ for atoms in load_wbm_structures():
32
+ fpath = Path(model.name) / f"{atoms.info['key_value_pairs']['wbm_id']}.pkl"
33
+ if not fpath.exists():
34
+ continue
35
+
36
+ all_data.append(pd.read_pickle(fpath))
37
+
38
+ df = pd.concat(all_data, ignore_index=True)
39
+ df.to_parquet(DATA_DIR / f"{model.name}.parquet")
40
+
41
+
42
+ def summarize():
43
+ summary_table = pd.DataFrame(
44
+ columns=[
45
+ "model",
46
+ "energy-diff-flip-times",
47
+ "tortuosity",
48
+ "spearman-compression-energy",
49
+ "spearman-compression-derivative",
50
+ "spearman-tension-energy",
51
+ "missing",
52
+ ]
53
+ )
54
+
55
+
56
+ for model in MLIPEnum:
57
+ fpath = DATA_DIR / f"{model.name}.parquet"
58
+ if not fpath.exists():
59
+ continue
60
+ df_raw_results = pd.read_parquet(fpath)
61
+
62
+ df_analyzed = pd.DataFrame(
63
+ columns=[
64
+ "model",
65
+ "structure",
66
+ "formula",
67
+ "volume-ratio",
68
+ "energy-delta-per-atom",
69
+ "energy-diff-flip-times",
70
+ "energy-delta-per-volume-b0",
71
+ "tortuosity",
72
+ "spearman-compression-energy",
73
+ "spearman-compression-derivative",
74
+ "spearman-tension-energy",
75
+ "missing",
76
+ ]
77
+ )
78
+
79
+ for wbm_struct in load_wbm_structures():
80
+ structure_id = wbm_struct.info["key_value_pairs"]["wbm_id"]
81
+
82
+ try:
83
+ results = df_raw_results.loc[df_raw_results["id"] == structure_id]
84
+ b0 = results["b0"].values[0]
85
+ # vol0 = results["v0"].values[0]
86
+ results = results["eos"].values[0]
87
+ es = np.array(results["energies"])
88
+ vols = np.array(results["volumes"])
89
+
90
+ indices = np.argsort(vols)
91
+ vols = vols[indices]
92
+ es = es[indices]
93
+
94
+ imine = len(es) // 2
95
+ # min_center_val = np.min(es[imid - 1 : imid + 2])
96
+ # imine = np.where(es == min_center_val)[0][0]
97
+ emin = es[imine]
98
+ vol0 = vols[imine]
99
+
100
+ interpolated_volumes = [
101
+ (vols[i] + vols[i + 1]) / 2 for i in range(len(vols) - 1)
102
+ ]
103
+ ediff = np.diff(es)
104
+ ediff_sign = np.sign(ediff)
105
+ mask = ediff_sign != 0
106
+ ediff = ediff[mask]
107
+ ediff_sign = ediff_sign[mask]
108
+ ediff_flip = np.diff(ediff_sign) != 0
109
+
110
+ etv = np.sum(np.abs(np.diff(es)))
111
+
112
+ data = {
113
+ "model": model.name,
114
+ "structure": structure_id,
115
+ "formula": wbm_struct.get_chemical_formula(),
116
+ "missing": False,
117
+ "volume-ratio": vols / vol0,
118
+ "energy-delta-per-atom": (es - emin) / len(wbm_struct),
119
+ "energy-diff-flip-times": np.sum(ediff_flip).astype(int),
120
+ "energy-delta-per-volume-b0": (es - emin) / (b0*vol0),
121
+ "tortuosity": etv / (abs(es[0] - emin) + abs(es[-1] - emin)),
122
+ "spearman-compression-energy": stats.spearmanr(
123
+ vols[:imine], es[:imine]
124
+ ).statistic,
125
+ "spearman-compression-derivative": stats.spearmanr(
126
+ interpolated_volumes[:imine], ediff[:imine]
127
+ ).statistic,
128
+ "spearman-tension-energy": stats.spearmanr(
129
+ vols[imine:], es[imine:]
130
+ ).statistic,
131
+ }
132
+
133
+ except Exception as e:
134
+ print(e)
135
+ data = {
136
+ "model": model.name,
137
+ "structure": structure_id,
138
+ "formula": wbm_struct.get_chemical_formula(),
139
+ "missing": True,
140
+ "volume-ratio": None,
141
+ "energy-delta-per-atom": None,
142
+ "energy-delta-per-volume-b0": None,
143
+ "energy-diff-flip-times": None,
144
+ "tortuosity": None,
145
+ "spearman-compression-energy": None,
146
+ "spearman-compression-derivative": None,
147
+ "spearman-tension-energy": None,
148
+ }
149
+
150
+ df_analyzed = pd.concat([df_analyzed, pd.DataFrame([data])], ignore_index=True)
151
+
152
+ df_analyzed.to_parquet(DATA_DIR / f"{model.name}_processed.parquet")
153
+ # json_fpath = DATA_DIR / f"EV_scan_analyzed_{model.name}.json"
154
+
155
+ # df_analyzed.to_json(json_fpath, orient="records")
156
+
157
+ valid_results = df_analyzed[df_analyzed["missing"] == False]
158
+
159
+ analysis_summary = {
160
+ "model": model.name,
161
+ "energy-diff-flip-times": valid_results["energy-diff-flip-times"].mean(),
162
+ "energy-diff-flip-times-std": valid_results["energy-diff-flip-times"].std(),
163
+ "tortuosity": valid_results["tortuosity"].mean(),
164
+ "tortuosity-std": valid_results["tortuosity"].std(),
165
+ "spearman-compression-energy": valid_results[
166
+ "spearman-compression-energy"
167
+ ].mean(),
168
+ "spearman-compression-energy-std": valid_results["spearman-compression-energy"].std(),
169
+ "spearman-compression-derivative": valid_results[
170
+ "spearman-compression-derivative"
171
+ ].mean(),
172
+ "spearman-compression-derivative-std": valid_results[
173
+ "spearman-compression-derivative"
174
+ ].std(),
175
+ "spearman-tension-energy": valid_results["spearman-tension-energy"].mean(),
176
+ "spearman-tension-energy-std": valid_results["spearman-tension-energy"].std(),
177
+ "missing": len(df_analyzed[df_analyzed["missing"] == True]),
178
+ }
179
+ summary_table = pd.concat(
180
+ [summary_table, pd.DataFrame([analysis_summary])], ignore_index=True
181
+ )
182
+
183
+
184
+ flip_rank = (
185
+ (summary_table["energy-diff-flip-times"] - 1)
186
+ .abs()
187
+ .rank(ascending=True, method="min")
188
+ )
189
+ tortuosity_rank = summary_table["tortuosity"].rank(ascending=True, method="min")
190
+ spearman_compression_energy_rank = summary_table["spearman-compression-energy"].rank(
191
+ method="min"
192
+ )
193
+ spearman_compression_derivative_rank = summary_table[
194
+ "spearman-compression-derivative"
195
+ ].rank(ascending=False, method="min")
196
+ spearman_tension_energy_rank = summary_table["spearman-tension-energy"].rank(
197
+ ascending=False, method="min"
198
+ )
199
+ missing_rank = summary_table["missing"].rank(ascending=True, method="min")
200
+
201
+ rank_aggr = (
202
+ flip_rank
203
+ + tortuosity_rank
204
+ + spearman_compression_energy_rank
205
+ + spearman_compression_derivative_rank
206
+ + spearman_tension_energy_rank
207
+ + missing_rank
208
+ )
209
+ rank = rank_aggr.rank(method="min")
210
+
211
+ summary_table.insert(1, "rank", rank.astype(int))
212
+ summary_table.insert(2, "rank-aggregation", rank_aggr.astype(int))
213
+ summary_table = summary_table.sort_values(by="rank", ascending=True)
214
+ summary_table = summary_table.reset_index(drop=True)
215
+
216
+ summary_table.to_csv(DATA_DIR / "summary.csv", index=False)
217
+ summary_table.to_latex(DATA_DIR / "summary.tex", index=False, float_format="%.3f")
218
+
219
+ return summary_table
220
+
221
+ if __name__ == "__main__":
222
+ gather_results()
223
+ summarize()
benchmarks/eos_bulk/eSEN.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4503e17151b7376bbd88dc8c4767747e7290e8eae898e050b0a231a5c447e3e6
3
+ size 427652
benchmarks/eos_bulk/eSEN_processed.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d7f754d8e18f645c1608e86286245c11611d5af34f3bd0bbc4a5b63b851a0dee
3
+ size 393790
benchmarks/eos_bulk/plot.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ import pandas as pd
6
+ from ase.db import connect
7
+
8
+ from mlip_arena.models import REGISTRY as MODELS
9
+
10
+ DATA_DIR = Path(__file__).parent.absolute()
11
+
12
+ # Use a qualitative color palette from matplotlib
13
+ palette_name = "tab10" # Better for distinguishing multiple lines
14
+ color_sequence = plt.get_cmap(palette_name).colors
15
+
16
+ valid_models = [
17
+ model
18
+ for model, metadata in MODELS.items()
19
+ if "eos_bulk" in metadata.get("gpu-tasks", [])
20
+ ]
21
+
22
+ def load_wbm_structures():
23
+ """
24
+ Load the WBM structures from a ASE DB file.
25
+ """
26
+ with connect(DATA_DIR.parent / "wbm_structures.db") as db:
27
+ for row in db.select():
28
+ yield row.toatoms(add_additional_information=True)
29
+
30
+ # # Collect valid models first
31
+ # valid_models = []
32
+ # for model_name in valid_models:
33
+ # fpath = DATA_DIR / f"{model_name}_processed.parquet"
34
+ # if fpath.exists():
35
+ # df = pd.read_parquet(fpath)
36
+ # if len(df) > 0:
37
+ # valid_models.append(model)
38
+
39
+ # # Ensure we're showing all 8 models
40
+ # if len(valid_models) < 8:
41
+ # print(f"Warning: Only found {len(valid_models)} valid models instead of 8")
42
+
43
+ # Set up the grid layout
44
+ n_models = len(valid_models)
45
+ n_cols = 4 # Use 4 columns
46
+ n_rows = (n_models + n_cols - 1) // n_cols # Ceiling division to get required rows
47
+
48
+ # Create figure with enough space for all subplots
49
+ fig = plt.figure(
50
+ figsize=(6, 1.25 * n_rows), # Wider for better readability
51
+ constrained_layout=True, # Better than tight_layout for this case
52
+ )
53
+
54
+ # Create grid of subplots
55
+ axes = []
56
+ for i in range(n_models):
57
+ ax = plt.subplot(n_rows, n_cols, i+1)
58
+ axes.append(ax)
59
+
60
+ SMALL_SIZE = 6
61
+ MEDIUM_SIZE = 8
62
+ LARGE_SIZE = 10
63
+
64
+ # Fill in the subplots with data
65
+ for i, model_name in enumerate(valid_models):
66
+ fpath = DATA_DIR / f"{model_name}_processed.parquet"
67
+ df = pd.read_parquet(fpath)
68
+
69
+ ax = axes[i]
70
+ valid_structures = []
71
+
72
+ for j, (_, row) in enumerate(df.iterrows()):
73
+ structure_id = row["structure"]
74
+ formula = row.get("formula", "")
75
+ if isinstance(row["volume-ratio"], (list, np.ndarray)) and isinstance(
76
+ row["energy-delta-per-volume-b0"], (list, np.ndarray)
77
+ ):
78
+ vol_strain = row["volume-ratio"]
79
+ energy_delta = row["energy-delta-per-volume-b0"]
80
+ color = color_sequence[j % len(color_sequence)]
81
+ ax.plot(
82
+ vol_strain,
83
+ energy_delta,
84
+ color=color,
85
+ linewidth=1,
86
+ alpha=0.9,
87
+ )
88
+ valid_structures.append(structure_id)
89
+
90
+ # Set subplot title
91
+ ax.set_title(f"{model_name} ({len(valid_structures)})", fontsize=MEDIUM_SIZE)
92
+
93
+ # Only add y-label to leftmost plots (those with index divisible by n_cols)
94
+ if i % n_cols == 0:
95
+ ax.set_ylabel("$\\frac{\\Delta E}{B V_0}$", fontsize=MEDIUM_SIZE)
96
+ else:
97
+ ax.set_ylabel("")
98
+
99
+ # Only add x-label to bottom row plots
100
+ # Check if this plot is in the bottom row
101
+ is_bottom_row = (i // n_cols) == (n_rows - 1) or (i >= n_models - n_cols)
102
+ if is_bottom_row:
103
+ ax.set_xlabel("$V/V_0$", fontsize=MEDIUM_SIZE)
104
+ else:
105
+ ax.set_xlabel("")
106
+
107
+ ax.set_ylim(-0.02, 0.1) # Consistent y-limits
108
+ ax.axvline(x=1, linestyle="--", color="gray", alpha=0.7)
109
+ ax.tick_params(axis="both", which="major", labelsize=MEDIUM_SIZE)
110
+
111
+ # Make sure all subplots share the x and y limits
112
+ for ax in axes:
113
+ ax.set_xlim(0.8, 1.2) # Adjust these as needed
114
+ ax.set_ylim(-0.02, 0.1)
115
+
116
+ # Save the figure with all plots
117
+ plt.savefig(DATA_DIR / "eos-bulk-grid.png", dpi=300, bbox_inches="tight")
118
+ plt.savefig(DATA_DIR / "eos-bulk-grid.pdf", bbox_inches="tight")
119
+ # plt.show()
benchmarks/eos_bulk/preprocessing.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from ase.db import connect
4
+ from pymatgen.core import Structure
5
+
6
+ with open("wbm_structures.json") as f:
7
+ structs = json.load(f)
8
+
9
+ with connect("wbm_structures.db") as db:
10
+ for id, s in structs.items():
11
+ atoms = Structure.from_dict(s).to_ase_atoms(msonable=False)
12
+ db.write(atoms, wbm_id=id)
benchmarks/eos_bulk/run.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import functools
2
+ from pathlib import Path
3
+
4
+ import pandas as pd
5
+ from ase import Atoms
6
+ from ase.db import connect
7
+ from dask.distributed import Client
8
+ from dask_jobqueue import SLURMCluster
9
+ from prefect import flow, task
10
+ from prefect.cache_policies import INPUTS, TASK_SOURCE
11
+ from prefect.runtime import task_run
12
+ from prefect_dask import DaskTaskRunner
13
+
14
+ from mlip_arena.models import REGISTRY, MLIPEnum
15
+ from mlip_arena.tasks.utils import get_calculator
16
+
17
+
18
+ @task
19
+ def load_wbm_structures():
20
+ """
21
+ Load the WBM structures from an ASE database file.
22
+
23
+ Reads structures from 'wbm_structures.db' and yields them as ASE Atoms objects
24
+ with additional metadata preserved from the database.
25
+
26
+ Yields:
27
+ ase.Atoms: Individual atomic structures from the WBM database with preserved
28
+ metadata in the .info dictionary.
29
+ """
30
+ with connect("../wbm_structures.db") as db:
31
+ for row in db.select():
32
+ yield row.toatoms(add_additional_information=True)
33
+
34
+
35
+ # def save_result(
36
+ # tsk: Task,
37
+ # run: TaskRun,
38
+ # state: State,
39
+ # model_name: str,
40
+ # id: str,
41
+ # ):
42
+ # result = run.state.result()
43
+
44
+ # assert isinstance(result, dict)
45
+
46
+ # result["method"] = model_name
47
+ # result["id"] = id
48
+ # result.pop("atoms", None)
49
+
50
+ # fpath = Path(f"{model_name}")
51
+ # fpath.mkdir(exist_ok=True)
52
+
53
+ # fpath = fpath / f"{result['id']}.pkl"
54
+
55
+ # df = pd.DataFrame([result])
56
+ # df.to_pickle(fpath)
57
+
58
+
59
+ @task(
60
+ name="EOS bulk - WBM",
61
+ task_run_name=lambda: f"{task_run.task_name}: {task_run.parameters['atoms'].get_chemical_formula()} - {task_run.parameters['model'].name}",
62
+ cache_policy=TASK_SOURCE + INPUTS,
63
+ )
64
+ def eos_bulk(atoms: Atoms, model: MLIPEnum):
65
+
66
+ from mlip_arena.tasks.eos import run as EOS
67
+ from mlip_arena.tasks.optimize import run as OPT
68
+
69
+ calculator = get_calculator(
70
+ model
71
+ ) # avoid sending entire model over prefect and select freer GPU
72
+
73
+ result = OPT.with_options(
74
+ refresh_cache=True,
75
+ )(
76
+ atoms,
77
+ calculator,
78
+ optimizer="FIRE",
79
+ criterion=dict(
80
+ fmax=0.1,
81
+ ),
82
+ )
83
+ result = EOS.with_options(
84
+ refresh_cache=True,
85
+ # on_completion=[functools.partial(
86
+ # save_result,
87
+ # model_name=model.name,
88
+ # id=atoms.info["key_value_pairs"]["wbm_id"],
89
+ # )],
90
+ )(
91
+ atoms=result["atoms"],
92
+ calculator=calculator,
93
+ optimizer="FIRE",
94
+ npoints=21,
95
+ max_abs_strain=0.2,
96
+ concurrent=False
97
+ )
98
+
99
+ result["method"] = model.name
100
+ result["id"] = atoms.info["key_value_pairs"]["wbm_id"]
101
+ result.pop("atoms", None)
102
+
103
+ fpath = Path(f"{model.name}")
104
+ fpath.mkdir(exist_ok=True)
105
+
106
+ fpath = fpath / f"{result['id']}.pkl"
107
+
108
+ df = pd.DataFrame([result])
109
+ df.to_pickle(fpath)
110
+
111
+ return df
112
+
113
+
114
+ @flow
115
+ def submit_tasks():
116
+ futures = []
117
+ for atoms in load_wbm_structures():
118
+ model = MLIPEnum["eSEN"]
119
+ # for model in MLIPEnum:
120
+ if "eos_bulk" not in REGISTRY[model.name].get("gpu-tasks", []):
121
+ continue
122
+ try:
123
+ result = eos_bulk.with_options(
124
+ refresh_cache=True
125
+ ).submit(atoms, model)
126
+ futures.append(result)
127
+ except Exception:
128
+ # print(f"Failed to submit task for {model.name}: {e}")
129
+ continue
130
+ return [f.result(raise_on_failure=False) for f in futures]
131
+
132
+
133
+ if __name__ == "__main__":
134
+ nodes_per_alloc = 1
135
+ gpus_per_alloc = 1
136
+ ntasks = 1
137
+
138
+ cluster_kwargs = dict(
139
+ cores=1,
140
+ memory="64 GB",
141
+ shebang="#!/bin/bash",
142
+ account="m3828",
143
+ walltime="00:30:00",
144
+ job_mem="0",
145
+ job_script_prologue=[
146
+ "source ~/.bashrc",
147
+ "module load python",
148
+ "module load cudatoolkit/12.4",
149
+ "source activate /pscratch/sd/c/cyrusyc/.conda/dev",
150
+ ],
151
+ job_directives_skip=["-n", "--cpus-per-task", "-J"],
152
+ job_extra_directives=[
153
+ "-J eos_bulk",
154
+ "-q regular",
155
+ f"-N {nodes_per_alloc}",
156
+ "-C gpu",
157
+ f"-G {gpus_per_alloc}",
158
+ # "--exclusive",
159
+ ],
160
+ )
161
+
162
+ cluster = SLURMCluster(**cluster_kwargs)
163
+ print(cluster.job_script())
164
+ cluster.adapt(minimum_jobs=50, maximum_jobs=50)
165
+ client = Client(cluster)
166
+
167
+ submit_tasks.with_options(
168
+ task_runner=DaskTaskRunner(address=client.scheduler.address),
169
+ log_prints=True,
170
+ )()