Spaces:
Running
Running
Yuan (Cyrus) Chiang
commited on
Enforce copying atoms and refactor calculator instantiation to allow custom calculator (#47)
Browse files* enforce copying atoms; refactor calculator parsings
* refactor test
* fix `generate_task_run_name`
* update readme example
* loosen a tiny bit pytest approx
* update md example to apply dispersion correction
- .github/README.md +13 -8
- mlip_arena/tasks/elasticity.py +11 -21
- mlip_arena/tasks/eos.py +10 -17
- mlip_arena/tasks/md.py +12 -22
- mlip_arena/tasks/neb.py +15 -41
- mlip_arena/tasks/optimize.py +9 -19
- mlip_arena/tasks/phonon.py +3 -5
- mlip_arena/tasks/utils.py +6 -3
- tests/test_elasticity.py +4 -3
- tests/test_eos.py +6 -4
- tests/test_md.py +4 -2
- tests/test_neb.py +4 -1
.github/README.md
CHANGED
@@ -12,9 +12,9 @@
|
|
12 |
> [!NOTE]
|
13 |
> Contributions of new tasks 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 one in [Discussion](https://github.com/atomind-ai/mlip-arena/discussions/new?category=ideas).
|
14 |
|
15 |
-
MLIP Arena is a unified platform for evaluating foundation machine learning interatomic potentials (MLIPs) beyond conventional error metrics. It focuses on revealing the
|
16 |
|
17 |
-
MLIP Arena leverages modern pythonic workflow
|
18 |
|
19 |
## Installation
|
20 |
|
@@ -46,7 +46,7 @@ DP_ENABLE_TENSORFLOW=0 pip install -e .[deepmd]
|
|
46 |
# (Optional) Install uv
|
47 |
curl -LsSf https://astral.sh/uv/install.sh | sh
|
48 |
source $HOME/.local/bin/env
|
49 |
-
# One script installation
|
50 |
bash scripts/install-macosx.sh
|
51 |
```
|
52 |
|
@@ -57,10 +57,12 @@ bash scripts/install-macosx.sh
|
|
57 |
Arena provides a unified interface to run all the compiled MLIPs. This can be achieved simply by looping through `MLIPEnum`:
|
58 |
|
59 |
```python
|
60 |
-
from mlip_arena.tasks.md import run as MD
|
61 |
-
# from mlip_arena.tasks import MD # convenient loading
|
62 |
from mlip_arena.models import MLIPEnum
|
|
|
|
|
|
|
63 |
|
|
|
64 |
from ase.build import bulk
|
65 |
|
66 |
atoms = bulk("Cu", "fcc", a=3.6)
|
@@ -70,15 +72,18 @@ results = []
|
|
70 |
for model in MLIPEnum:
|
71 |
result = MD(
|
72 |
atoms=atoms,
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
75 |
ensemble="nve",
|
76 |
dynamics="velocityverlet",
|
77 |
total_time=1e3, # 1 ps = 1e3 fs
|
78 |
time_step=2, # fs
|
79 |
)
|
80 |
results.append(result)
|
81 |
-
|
82 |
```
|
83 |
|
84 |
## Contribute
|
|
|
12 |
> [!NOTE]
|
13 |
> Contributions of new tasks 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 one in [Discussion](https://github.com/atomind-ai/mlip-arena/discussions/new?category=ideas).
|
14 |
|
15 |
+
MLIP Arena is a unified platform for evaluating foundation machine learning interatomic potentials (MLIPs) beyond conventional error metrics. It focuses on revealing the physics and chemistry learned by these models and assessing their utilitarian performance agnostic to underlying model architecture. The platform's benchmarks are specifically designed to evaluate the readiness and reliability of open-source, open-weight models in accurately reproducing both qualitative and quantitative behaviors of atomic systems.
|
16 |
|
17 |
+
MLIP Arena leverages modern pythonic workflow orchestrator [Prefect](https://www.prefect.io/) to enable advanced task/flow chaining and caching.
|
18 |
|
19 |
## Installation
|
20 |
|
|
|
46 |
# (Optional) Install uv
|
47 |
curl -LsSf https://astral.sh/uv/install.sh | sh
|
48 |
source $HOME/.local/bin/env
|
49 |
+
# One script uv pip installation
|
50 |
bash scripts/install-macosx.sh
|
51 |
```
|
52 |
|
|
|
57 |
Arena provides a unified interface to run all the compiled MLIPs. This can be achieved simply by looping through `MLIPEnum`:
|
58 |
|
59 |
```python
|
|
|
|
|
60 |
from mlip_arena.models import MLIPEnum
|
61 |
+
from mlip_arena.tasks.md import run as MD
|
62 |
+
# from mlip_arena.tasks import MD # for convenient import
|
63 |
+
from mlip_arena.tasks.utils import get_calculator
|
64 |
|
65 |
+
from ase import units
|
66 |
from ase.build import bulk
|
67 |
|
68 |
atoms = bulk("Cu", "fcc", a=3.6)
|
|
|
72 |
for model in MLIPEnum:
|
73 |
result = MD(
|
74 |
atoms=atoms,
|
75 |
+
calculator=get_calculator(
|
76 |
+
model,
|
77 |
+
calculator_kwargs=dict(), # passing into calculator
|
78 |
+
dispersion=True,
|
79 |
+
dispersion_kwargs=dict(damping='bj', xc='pbe', cutoff=40.0 * units.Bohr), # passing into TorchDFTD3Calculator
|
80 |
+
),
|
81 |
ensemble="nve",
|
82 |
dynamics="velocityverlet",
|
83 |
total_time=1e3, # 1 ps = 1e3 fs
|
84 |
time_step=2, # fs
|
85 |
)
|
86 |
results.append(result)
|
|
|
87 |
```
|
88 |
|
89 |
## Contribute
|
mlip_arena/tasks/elasticity.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
"""
|
2 |
Defines the tasks for computing the elastic tensor.
|
3 |
|
4 |
-
This module has been modified from MatCalc
|
5 |
https://github.com/materialsvirtuallab/matcalc/blob/main/src/matcalc/elasticity.py
|
6 |
|
7 |
https://github.com/materialsvirtuallab/matcalc/blob/main/LICENSE
|
@@ -41,15 +41,15 @@ from __future__ import annotations
|
|
41 |
from typing import TYPE_CHECKING, Any
|
42 |
|
43 |
import numpy as np
|
|
|
|
|
|
|
44 |
from numpy.typing import ArrayLike
|
45 |
from prefect import task
|
46 |
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
47 |
from prefect.runtime import task_run
|
48 |
from prefect.states import State
|
49 |
|
50 |
-
from ase import Atoms
|
51 |
-
from ase.optimize.optimize import Optimizer
|
52 |
-
from mlip_arena.models import MLIPEnum
|
53 |
from mlip_arena.tasks.optimize import run as OPT
|
54 |
from pymatgen.analysis.elasticity import DeformedStructureSet, ElasticTensor, Strain
|
55 |
from pymatgen.analysis.elasticity.elastic import get_strain_state_dict
|
@@ -64,7 +64,7 @@ def _generate_task_run_name():
|
|
64 |
parameters = task_run.parameters
|
65 |
|
66 |
atoms = parameters["atoms"]
|
67 |
-
calculator_name = parameters["
|
68 |
|
69 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
70 |
|
@@ -77,11 +77,7 @@ def _generate_task_run_name():
|
|
77 |
)
|
78 |
def run(
|
79 |
atoms: Atoms,
|
80 |
-
|
81 |
-
calculator_kwargs: dict | None = None,
|
82 |
-
dispersion: bool = False,
|
83 |
-
dispersion_kwargs: dict | None = None,
|
84 |
-
device: str | None = None,
|
85 |
optimizer: Optimizer | str = "BFGSLineSearch", # type: ignore
|
86 |
optimizer_kwargs: dict | None = None,
|
87 |
filter: Filter | str | None = "FrechetCell", # type: ignore
|
@@ -97,9 +93,7 @@ def run(
|
|
97 |
|
98 |
Args:
|
99 |
atoms (Atoms): The input structure.
|
100 |
-
|
101 |
-
calculator_kwargs (dict, optional): The calculator kwargs. Defaults to None.
|
102 |
-
device (str, optional): The device. Defaults to None.
|
103 |
optimizer (Optimizer | str, optional): The optimizer. Defaults to "BFGSLineSearch".
|
104 |
optimizer_kwargs (dict, optional): The optimizer kwargs. Defaults to None.
|
105 |
filter (Filter | str, optional): The filter. Defaults to "FrechetCell".
|
@@ -115,6 +109,8 @@ def run(
|
|
115 |
dict[str, Any] | State: The elastic tensor.
|
116 |
"""
|
117 |
|
|
|
|
|
118 |
OPT_ = OPT.with_options(
|
119 |
refresh_cache=not cache_opt,
|
120 |
persist_result=persist_opt,
|
@@ -122,11 +118,7 @@ def run(
|
|
122 |
|
123 |
first_relax = OPT_(
|
124 |
atoms=atoms,
|
125 |
-
|
126 |
-
calculator_kwargs=calculator_kwargs,
|
127 |
-
dispersion=dispersion,
|
128 |
-
dispersion_kwargs=dispersion_kwargs,
|
129 |
-
device=device,
|
130 |
optimizer=optimizer,
|
131 |
optimizer_kwargs=optimizer_kwargs,
|
132 |
filter=filter,
|
@@ -172,9 +164,7 @@ def run(
|
|
172 |
]
|
173 |
|
174 |
fit = fit_elastic_tensor(
|
175 |
-
strains,
|
176 |
-
stresses,
|
177 |
-
eq_stress=relaxed.get_stress(voigt=False)
|
178 |
)
|
179 |
|
180 |
return {
|
|
|
1 |
"""
|
2 |
Defines the tasks for computing the elastic tensor.
|
3 |
|
4 |
+
This module has been modified from MatCalc
|
5 |
https://github.com/materialsvirtuallab/matcalc/blob/main/src/matcalc/elasticity.py
|
6 |
|
7 |
https://github.com/materialsvirtuallab/matcalc/blob/main/LICENSE
|
|
|
41 |
from typing import TYPE_CHECKING, Any
|
42 |
|
43 |
import numpy as np
|
44 |
+
from ase import Atoms
|
45 |
+
from ase.calculators.calculator import BaseCalculator
|
46 |
+
from ase.optimize.optimize import Optimizer
|
47 |
from numpy.typing import ArrayLike
|
48 |
from prefect import task
|
49 |
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
50 |
from prefect.runtime import task_run
|
51 |
from prefect.states import State
|
52 |
|
|
|
|
|
|
|
53 |
from mlip_arena.tasks.optimize import run as OPT
|
54 |
from pymatgen.analysis.elasticity import DeformedStructureSet, ElasticTensor, Strain
|
55 |
from pymatgen.analysis.elasticity.elastic import get_strain_state_dict
|
|
|
64 |
parameters = task_run.parameters
|
65 |
|
66 |
atoms = parameters["atoms"]
|
67 |
+
calculator_name = parameters["calculator"]
|
68 |
|
69 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
70 |
|
|
|
77 |
)
|
78 |
def run(
|
79 |
atoms: Atoms,
|
80 |
+
calculator: BaseCalculator,
|
|
|
|
|
|
|
|
|
81 |
optimizer: Optimizer | str = "BFGSLineSearch", # type: ignore
|
82 |
optimizer_kwargs: dict | None = None,
|
83 |
filter: Filter | str | None = "FrechetCell", # type: ignore
|
|
|
93 |
|
94 |
Args:
|
95 |
atoms (Atoms): The input structure.
|
96 |
+
calculator (BaseCalculator): The calculator.
|
|
|
|
|
97 |
optimizer (Optimizer | str, optional): The optimizer. Defaults to "BFGSLineSearch".
|
98 |
optimizer_kwargs (dict, optional): The optimizer kwargs. Defaults to None.
|
99 |
filter (Filter | str, optional): The filter. Defaults to "FrechetCell".
|
|
|
109 |
dict[str, Any] | State: The elastic tensor.
|
110 |
"""
|
111 |
|
112 |
+
atoms = atoms.copy()
|
113 |
+
|
114 |
OPT_ = OPT.with_options(
|
115 |
refresh_cache=not cache_opt,
|
116 |
persist_result=persist_opt,
|
|
|
118 |
|
119 |
first_relax = OPT_(
|
120 |
atoms=atoms,
|
121 |
+
calculator=calculator,
|
|
|
|
|
|
|
|
|
122 |
optimizer=optimizer,
|
123 |
optimizer_kwargs=optimizer_kwargs,
|
124 |
filter=filter,
|
|
|
164 |
]
|
165 |
|
166 |
fit = fit_elastic_tensor(
|
167 |
+
strains, stresses, eq_stress=relaxed.get_stress(voigt=False)
|
|
|
|
|
168 |
)
|
169 |
|
170 |
return {
|
mlip_arena/tasks/eos.py
CHANGED
@@ -9,6 +9,9 @@ from __future__ import annotations
|
|
9 |
from typing import TYPE_CHECKING, Any
|
10 |
|
11 |
import numpy as np
|
|
|
|
|
|
|
12 |
from prefect import task
|
13 |
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
14 |
from prefect.futures import wait
|
@@ -16,9 +19,6 @@ from prefect.results import ResultRecord
|
|
16 |
from prefect.runtime import task_run
|
17 |
from prefect.states import State
|
18 |
|
19 |
-
from ase import Atoms
|
20 |
-
from ase.optimize.optimize import Optimizer
|
21 |
-
from mlip_arena.models import MLIPEnum
|
22 |
from mlip_arena.tasks.optimize import run as OPT
|
23 |
from pymatgen.analysis.eos import BirchMurnaghan
|
24 |
|
@@ -31,7 +31,7 @@ def _generate_task_run_name():
|
|
31 |
parameters = task_run.parameters
|
32 |
|
33 |
atoms = parameters["atoms"]
|
34 |
-
calculator_name = parameters["
|
35 |
|
36 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
37 |
|
@@ -41,9 +41,7 @@ def _generate_task_run_name():
|
|
41 |
)
|
42 |
def run(
|
43 |
atoms: Atoms,
|
44 |
-
|
45 |
-
calculator_kwargs: dict | None = None,
|
46 |
-
device: str | None = None,
|
47 |
optimizer: Optimizer | str = "BFGSLineSearch", # type: ignore
|
48 |
optimizer_kwargs: dict | None = None,
|
49 |
filter: Filter | str | None = "FrechetCell", # type: ignore
|
@@ -77,6 +75,8 @@ def run(
|
|
77 |
A dictionary containing the EOS data, bulk modulus, equilibrium volume, and equilibrium energy if successful. Otherwise, a prefect state object.
|
78 |
"""
|
79 |
|
|
|
|
|
80 |
OPT_ = OPT.with_options(
|
81 |
refresh_cache=not cache_opt,
|
82 |
persist_result=cache_opt,
|
@@ -84,9 +84,7 @@ def run(
|
|
84 |
|
85 |
state = OPT_(
|
86 |
atoms=atoms,
|
87 |
-
|
88 |
-
calculator_kwargs=calculator_kwargs,
|
89 |
-
device=device,
|
90 |
optimizer=optimizer,
|
91 |
optimizer_kwargs=optimizer_kwargs,
|
92 |
filter=filter,
|
@@ -118,9 +116,7 @@ def run(
|
|
118 |
|
119 |
future = OPT_.submit(
|
120 |
atoms=atoms,
|
121 |
-
|
122 |
-
calculator_kwargs=calculator_kwargs,
|
123 |
-
device=device,
|
124 |
optimizer=optimizer,
|
125 |
optimizer_kwargs=optimizer_kwargs,
|
126 |
filter=None,
|
@@ -144,9 +140,7 @@ def run(
|
|
144 |
|
145 |
state = OPT_(
|
146 |
atoms=atoms,
|
147 |
-
|
148 |
-
calculator_kwargs=calculator_kwargs,
|
149 |
-
device=device,
|
150 |
optimizer=optimizer,
|
151 |
optimizer_kwargs=optimizer_kwargs,
|
152 |
filter=None,
|
@@ -176,7 +170,6 @@ def run(
|
|
176 |
|
177 |
return {
|
178 |
"atoms": relaxed,
|
179 |
-
"calculator_name": calculator_name,
|
180 |
"eos": {"volumes": volumes, "energies": energies},
|
181 |
"K": bm.b0_GPa,
|
182 |
"b0": bm.b0,
|
|
|
9 |
from typing import TYPE_CHECKING, Any
|
10 |
|
11 |
import numpy as np
|
12 |
+
from ase import Atoms
|
13 |
+
from ase.calculators.calculator import BaseCalculator
|
14 |
+
from ase.optimize.optimize import Optimizer
|
15 |
from prefect import task
|
16 |
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
17 |
from prefect.futures import wait
|
|
|
19 |
from prefect.runtime import task_run
|
20 |
from prefect.states import State
|
21 |
|
|
|
|
|
|
|
22 |
from mlip_arena.tasks.optimize import run as OPT
|
23 |
from pymatgen.analysis.eos import BirchMurnaghan
|
24 |
|
|
|
31 |
parameters = task_run.parameters
|
32 |
|
33 |
atoms = parameters["atoms"]
|
34 |
+
calculator_name = parameters["calculator"]
|
35 |
|
36 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
37 |
|
|
|
41 |
)
|
42 |
def run(
|
43 |
atoms: Atoms,
|
44 |
+
calculator: BaseCalculator,
|
|
|
|
|
45 |
optimizer: Optimizer | str = "BFGSLineSearch", # type: ignore
|
46 |
optimizer_kwargs: dict | None = None,
|
47 |
filter: Filter | str | None = "FrechetCell", # type: ignore
|
|
|
75 |
A dictionary containing the EOS data, bulk modulus, equilibrium volume, and equilibrium energy if successful. Otherwise, a prefect state object.
|
76 |
"""
|
77 |
|
78 |
+
atoms = atoms.copy()
|
79 |
+
|
80 |
OPT_ = OPT.with_options(
|
81 |
refresh_cache=not cache_opt,
|
82 |
persist_result=cache_opt,
|
|
|
84 |
|
85 |
state = OPT_(
|
86 |
atoms=atoms,
|
87 |
+
calculator=calculator,
|
|
|
|
|
88 |
optimizer=optimizer,
|
89 |
optimizer_kwargs=optimizer_kwargs,
|
90 |
filter=filter,
|
|
|
116 |
|
117 |
future = OPT_.submit(
|
118 |
atoms=atoms,
|
119 |
+
calculator=calculator,
|
|
|
|
|
120 |
optimizer=optimizer,
|
121 |
optimizer_kwargs=optimizer_kwargs,
|
122 |
filter=None,
|
|
|
140 |
|
141 |
state = OPT_(
|
142 |
atoms=atoms,
|
143 |
+
calculator=calculator,
|
|
|
|
|
144 |
optimizer=optimizer,
|
145 |
optimizer_kwargs=optimizer_kwargs,
|
146 |
filter=None,
|
|
|
170 |
|
171 |
return {
|
172 |
"atoms": relaxed,
|
|
|
173 |
"eos": {"volumes": volumes, "energies": energies},
|
174 |
"K": bm.b0_GPa,
|
175 |
"b0": bm.b0,
|
mlip_arena/tasks/md.py
CHANGED
@@ -60,14 +60,8 @@ from pathlib import Path
|
|
60 |
from typing import Literal
|
61 |
|
62 |
import numpy as np
|
63 |
-
from prefect import task
|
64 |
-
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
65 |
-
from prefect.runtime import task_run
|
66 |
-
from scipy.interpolate import interp1d
|
67 |
-
from scipy.linalg import schur
|
68 |
-
from tqdm.auto import tqdm
|
69 |
-
|
70 |
from ase import Atoms, units
|
|
|
71 |
from ase.io import read
|
72 |
from ase.io.trajectory import Trajectory
|
73 |
from ase.md.andersen import Andersen
|
@@ -82,8 +76,12 @@ from ase.md.velocitydistribution import (
|
|
82 |
ZeroRotation,
|
83 |
)
|
84 |
from ase.md.verlet import VelocityVerlet
|
85 |
-
from
|
86 |
-
from
|
|
|
|
|
|
|
|
|
87 |
|
88 |
_valid_dynamics: dict[str, tuple[str, ...]] = {
|
89 |
"nve": ("velocityverlet",),
|
@@ -189,7 +187,7 @@ def _generate_task_run_name():
|
|
189 |
parameters = task_run.parameters
|
190 |
|
191 |
atoms = parameters["atoms"]
|
192 |
-
calculator_name = parameters["
|
193 |
|
194 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
195 |
|
@@ -201,11 +199,7 @@ def _generate_task_run_name():
|
|
201 |
)
|
202 |
def run(
|
203 |
atoms: Atoms,
|
204 |
-
|
205 |
-
calculator_kwargs: dict | None = None,
|
206 |
-
dispersion: bool = False,
|
207 |
-
dispersion_kwargs: dict | None = None,
|
208 |
-
device: str | None = None,
|
209 |
ensemble: Literal["nve", "nvt", "npt"] = "nvt",
|
210 |
dynamics: str | MolecularDynamics = "langevin",
|
211 |
time_step: float | None = None, # fs
|
@@ -221,13 +215,9 @@ def run(
|
|
221 |
restart: bool = True,
|
222 |
):
|
223 |
|
224 |
-
atoms
|
225 |
-
|
226 |
-
|
227 |
-
dispersion=dispersion,
|
228 |
-
dispersion_kwargs=dispersion_kwargs,
|
229 |
-
device=device,
|
230 |
-
)
|
231 |
|
232 |
if time_step is None:
|
233 |
# If a structure contains an isotope of hydrogen, set default `time_step`
|
|
|
60 |
from typing import Literal
|
61 |
|
62 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
from ase import Atoms, units
|
64 |
+
from ase.calculators.calculator import BaseCalculator
|
65 |
from ase.io import read
|
66 |
from ase.io.trajectory import Trajectory
|
67 |
from ase.md.andersen import Andersen
|
|
|
76 |
ZeroRotation,
|
77 |
)
|
78 |
from ase.md.verlet import VelocityVerlet
|
79 |
+
from prefect import task
|
80 |
+
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
81 |
+
from prefect.runtime import task_run
|
82 |
+
from scipy.interpolate import interp1d
|
83 |
+
from scipy.linalg import schur
|
84 |
+
from tqdm.auto import tqdm
|
85 |
|
86 |
_valid_dynamics: dict[str, tuple[str, ...]] = {
|
87 |
"nve": ("velocityverlet",),
|
|
|
187 |
parameters = task_run.parameters
|
188 |
|
189 |
atoms = parameters["atoms"]
|
190 |
+
calculator_name = parameters["calculator"]
|
191 |
|
192 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
193 |
|
|
|
199 |
)
|
200 |
def run(
|
201 |
atoms: Atoms,
|
202 |
+
calculator: BaseCalculator,
|
|
|
|
|
|
|
|
|
203 |
ensemble: Literal["nve", "nvt", "npt"] = "nvt",
|
204 |
dynamics: str | MolecularDynamics = "langevin",
|
205 |
time_step: float | None = None, # fs
|
|
|
215 |
restart: bool = True,
|
216 |
):
|
217 |
|
218 |
+
atoms = atoms.copy()
|
219 |
+
|
220 |
+
atoms.calc = calculator
|
|
|
|
|
|
|
|
|
221 |
|
222 |
if time_step is None:
|
223 |
# If a structure contains an isotope of hydrogen, set default `time_step`
|
mlip_arena/tasks/neb.py
CHANGED
@@ -41,20 +41,20 @@ from __future__ import annotations
|
|
41 |
from pathlib import Path
|
42 |
from typing import Any, Literal
|
43 |
|
44 |
-
from prefect import task
|
45 |
-
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
46 |
-
from prefect.runtime import task_run
|
47 |
-
from prefect.states import State
|
48 |
-
|
49 |
from ase import Atoms
|
|
|
50 |
from ase.filters import * # type: ignore
|
51 |
from ase.mep.neb import NEB, NEBTools
|
52 |
from ase.optimize import * # type: ignore
|
53 |
from ase.optimize.optimize import Optimizer
|
54 |
from ase.utils.forcecurve import fit_images
|
55 |
-
from
|
|
|
|
|
|
|
|
|
56 |
from mlip_arena.tasks.optimize import run as OPT
|
57 |
-
from mlip_arena.tasks.utils import
|
58 |
from pymatgen.io.ase import AseAtomsAdaptor
|
59 |
|
60 |
_valid_optimizers: dict[str, Optimizer] = {
|
@@ -83,7 +83,7 @@ def _generate_task_run_name():
|
|
83 |
else:
|
84 |
raise ValueError("No images or start atoms found in parameters")
|
85 |
|
86 |
-
calculator_name = parameters["
|
87 |
|
88 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
89 |
|
@@ -95,11 +95,7 @@ def _generate_task_run_name():
|
|
95 |
)
|
96 |
def run(
|
97 |
images: list[Atoms],
|
98 |
-
|
99 |
-
calculator_kwargs: dict | None = None,
|
100 |
-
dispersion: bool = False,
|
101 |
-
dispersion_kwargs: dict | None = None,
|
102 |
-
device: str | None = None,
|
103 |
optimizer: Optimizer | str = "MDMin", # type: ignore
|
104 |
optimizer_kwargs: dict | None = None,
|
105 |
criterion: dict | None = None,
|
@@ -127,17 +123,11 @@ def run(
|
|
127 |
dict[str, Any] | State: The energy barrier.
|
128 |
"""
|
129 |
|
130 |
-
|
131 |
-
calculator_name,
|
132 |
-
calculator_kwargs,
|
133 |
-
dispersion=dispersion,
|
134 |
-
dispersion_kwargs=dispersion_kwargs,
|
135 |
-
device=device,
|
136 |
-
)
|
137 |
|
138 |
for image in images:
|
139 |
assert isinstance(image, Atoms)
|
140 |
-
image.calc =
|
141 |
|
142 |
neb = NEB(images, climb=climb, allow_shared_calculator=True)
|
143 |
|
@@ -175,11 +165,7 @@ def run_from_endpoints(
|
|
175 |
start: Atoms,
|
176 |
end: Atoms,
|
177 |
n_images: int,
|
178 |
-
|
179 |
-
calculator_kwargs: dict | None = None,
|
180 |
-
dispersion: str | None = None,
|
181 |
-
dispersion_kwargs: dict | None = None,
|
182 |
-
device: str | None = None,
|
183 |
optimizer: Optimizer | str = "BFGS", # type: ignore
|
184 |
optimizer_kwargs: dict | None = None,
|
185 |
criterion: dict | None = None,
|
@@ -216,11 +202,7 @@ def run_from_endpoints(
|
|
216 |
refresh_cache=not cache_subtasks,
|
217 |
)(
|
218 |
atoms=start.copy(),
|
219 |
-
|
220 |
-
calculator_kwargs=calculator_kwargs,
|
221 |
-
dispersion=dispersion,
|
222 |
-
dispersion_kwargs=dispersion_kwargs,
|
223 |
-
device=device,
|
224 |
optimizer=optimizer,
|
225 |
optimizer_kwargs=optimizer_kwargs,
|
226 |
criterion=criterion,
|
@@ -231,11 +213,7 @@ def run_from_endpoints(
|
|
231 |
refresh_cache=not cache_subtasks,
|
232 |
)(
|
233 |
atoms=end.copy(),
|
234 |
-
|
235 |
-
calculator_kwargs=calculator_kwargs,
|
236 |
-
dispersion=dispersion,
|
237 |
-
dispersion_kwargs=dispersion_kwargs,
|
238 |
-
device=device,
|
239 |
optimizer=optimizer,
|
240 |
optimizer_kwargs=optimizer_kwargs,
|
241 |
criterion=criterion,
|
@@ -260,11 +238,7 @@ def run_from_endpoints(
|
|
260 |
refresh_cache=not cache_subtasks,
|
261 |
)(
|
262 |
images,
|
263 |
-
|
264 |
-
calculator_kwargs=calculator_kwargs,
|
265 |
-
dispersion=dispersion,
|
266 |
-
dispersion_kwargs=dispersion_kwargs,
|
267 |
-
device=device,
|
268 |
optimizer=optimizer,
|
269 |
optimizer_kwargs=optimizer_kwargs,
|
270 |
criterion=criterion,
|
|
|
41 |
from pathlib import Path
|
42 |
from typing import Any, Literal
|
43 |
|
|
|
|
|
|
|
|
|
|
|
44 |
from ase import Atoms
|
45 |
+
from ase.calculators.calculator import BaseCalculator
|
46 |
from ase.filters import * # type: ignore
|
47 |
from ase.mep.neb import NEB, NEBTools
|
48 |
from ase.optimize import * # type: ignore
|
49 |
from ase.optimize.optimize import Optimizer
|
50 |
from ase.utils.forcecurve import fit_images
|
51 |
+
from prefect import task
|
52 |
+
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
53 |
+
from prefect.runtime import task_run
|
54 |
+
from prefect.states import State
|
55 |
+
|
56 |
from mlip_arena.tasks.optimize import run as OPT
|
57 |
+
from mlip_arena.tasks.utils import logger, pformat
|
58 |
from pymatgen.io.ase import AseAtomsAdaptor
|
59 |
|
60 |
_valid_optimizers: dict[str, Optimizer] = {
|
|
|
83 |
else:
|
84 |
raise ValueError("No images or start atoms found in parameters")
|
85 |
|
86 |
+
calculator_name = parameters["calculator"]
|
87 |
|
88 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
89 |
|
|
|
95 |
)
|
96 |
def run(
|
97 |
images: list[Atoms],
|
98 |
+
calculator: BaseCalculator,
|
|
|
|
|
|
|
|
|
99 |
optimizer: Optimizer | str = "MDMin", # type: ignore
|
100 |
optimizer_kwargs: dict | None = None,
|
101 |
criterion: dict | None = None,
|
|
|
123 |
dict[str, Any] | State: The energy barrier.
|
124 |
"""
|
125 |
|
126 |
+
images = [image.copy() for image in images]
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
|
128 |
for image in images:
|
129 |
assert isinstance(image, Atoms)
|
130 |
+
image.calc = calculator
|
131 |
|
132 |
neb = NEB(images, climb=climb, allow_shared_calculator=True)
|
133 |
|
|
|
165 |
start: Atoms,
|
166 |
end: Atoms,
|
167 |
n_images: int,
|
168 |
+
calculator: BaseCalculator,
|
|
|
|
|
|
|
|
|
169 |
optimizer: Optimizer | str = "BFGS", # type: ignore
|
170 |
optimizer_kwargs: dict | None = None,
|
171 |
criterion: dict | None = None,
|
|
|
202 |
refresh_cache=not cache_subtasks,
|
203 |
)(
|
204 |
atoms=start.copy(),
|
205 |
+
calculator=calculator,
|
|
|
|
|
|
|
|
|
206 |
optimizer=optimizer,
|
207 |
optimizer_kwargs=optimizer_kwargs,
|
208 |
criterion=criterion,
|
|
|
213 |
refresh_cache=not cache_subtasks,
|
214 |
)(
|
215 |
atoms=end.copy(),
|
216 |
+
calculator=calculator,
|
|
|
|
|
|
|
|
|
217 |
optimizer=optimizer,
|
218 |
optimizer_kwargs=optimizer_kwargs,
|
219 |
criterion=criterion,
|
|
|
238 |
refresh_cache=not cache_subtasks,
|
239 |
)(
|
240 |
images,
|
241 |
+
calculator=calculator,
|
|
|
|
|
|
|
|
|
242 |
optimizer=optimizer,
|
243 |
optimizer_kwargs=optimizer_kwargs,
|
244 |
criterion=criterion,
|
mlip_arena/tasks/optimize.py
CHANGED
@@ -4,19 +4,18 @@ Define structure optimization tasks.
|
|
4 |
|
5 |
from __future__ import annotations
|
6 |
|
7 |
-
from prefect import task
|
8 |
-
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
9 |
-
from prefect.runtime import task_run
|
10 |
-
|
11 |
from ase import Atoms
|
|
|
12 |
from ase.constraints import FixSymmetry
|
13 |
from ase.filters import * # type: ignore
|
14 |
from ase.filters import Filter
|
15 |
from ase.optimize import * # type: ignore
|
16 |
from ase.optimize.optimize import Optimizer
|
17 |
-
from
|
18 |
-
from
|
|
|
19 |
|
|
|
20 |
|
21 |
_valid_filters: dict[str, Filter] = {
|
22 |
"Filter": Filter,
|
@@ -46,7 +45,7 @@ def _generate_task_run_name():
|
|
46 |
parameters = task_run.parameters
|
47 |
|
48 |
atoms = parameters["atoms"]
|
49 |
-
calculator_name = parameters["
|
50 |
|
51 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
52 |
|
@@ -56,11 +55,7 @@ def _generate_task_run_name():
|
|
56 |
)
|
57 |
def run(
|
58 |
atoms: Atoms,
|
59 |
-
|
60 |
-
calculator_kwargs: dict | None = None,
|
61 |
-
dispersion: bool = False,
|
62 |
-
dispersion_kwargs: dict | None = None,
|
63 |
-
device: str | None = None,
|
64 |
optimizer: Optimizer | str = BFGSLineSearch,
|
65 |
optimizer_kwargs: dict | None = None,
|
66 |
filter: Filter | str | None = None,
|
@@ -68,13 +63,8 @@ def run(
|
|
68 |
criterion: dict | None = None,
|
69 |
symmetry: bool = False,
|
70 |
):
|
71 |
-
atoms
|
72 |
-
|
73 |
-
calculator_kwargs=calculator_kwargs,
|
74 |
-
dispersion=dispersion,
|
75 |
-
dispersion_kwargs=dispersion_kwargs,
|
76 |
-
device=device,
|
77 |
-
)
|
78 |
|
79 |
if isinstance(filter, str):
|
80 |
if filter not in _valid_filters:
|
|
|
4 |
|
5 |
from __future__ import annotations
|
6 |
|
|
|
|
|
|
|
|
|
7 |
from ase import Atoms
|
8 |
+
from ase.calculators.calculator import BaseCalculator
|
9 |
from ase.constraints import FixSymmetry
|
10 |
from ase.filters import * # type: ignore
|
11 |
from ase.filters import Filter
|
12 |
from ase.optimize import * # type: ignore
|
13 |
from ase.optimize.optimize import Optimizer
|
14 |
+
from prefect import task
|
15 |
+
from prefect.cache_policies import INPUTS, TASK_SOURCE
|
16 |
+
from prefect.runtime import task_run
|
17 |
|
18 |
+
from mlip_arena.tasks.utils import logger, pformat
|
19 |
|
20 |
_valid_filters: dict[str, Filter] = {
|
21 |
"Filter": Filter,
|
|
|
45 |
parameters = task_run.parameters
|
46 |
|
47 |
atoms = parameters["atoms"]
|
48 |
+
calculator_name = parameters["calculator"]
|
49 |
|
50 |
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
51 |
|
|
|
55 |
)
|
56 |
def run(
|
57 |
atoms: Atoms,
|
58 |
+
calculator: BaseCalculator,
|
|
|
|
|
|
|
|
|
59 |
optimizer: Optimizer | str = BFGSLineSearch,
|
60 |
optimizer_kwargs: dict | None = None,
|
61 |
filter: Filter | str | None = None,
|
|
|
63 |
criterion: dict | None = None,
|
64 |
symmetry: bool = False,
|
65 |
):
|
66 |
+
atoms = atoms.copy()
|
67 |
+
atoms.calc = calculator
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
if isinstance(filter, str):
|
70 |
if filter not in _valid_filters:
|
mlip_arena/tasks/phonon.py
CHANGED
@@ -97,11 +97,9 @@ def _generate_task_run_name():
|
|
97 |
parameters = task_run.parameters
|
98 |
|
99 |
atoms = parameters["atoms"]
|
100 |
-
|
101 |
|
102 |
-
return (
|
103 |
-
f"{task_name}: {atoms.get_chemical_formula()} - {calculator.__class__.__name__}"
|
104 |
-
)
|
105 |
|
106 |
|
107 |
@task(
|
@@ -124,7 +122,7 @@ def run(
|
|
124 |
outdir: str | None = None,
|
125 |
):
|
126 |
phonon = get_phonopy(
|
127 |
-
atoms=atoms,
|
128 |
supercell_matrix=supercell_matrix,
|
129 |
min_lengths=min_lengths,
|
130 |
symprec=symprec,
|
|
|
97 |
parameters = task_run.parameters
|
98 |
|
99 |
atoms = parameters["atoms"]
|
100 |
+
calculator_name = parameters["calculator"]
|
101 |
|
102 |
+
return f"{task_name}: {atoms.get_chemical_formula()} - {calculator_name}"
|
|
|
|
|
103 |
|
104 |
|
105 |
@task(
|
|
|
122 |
outdir: str | None = None,
|
123 |
):
|
124 |
phonon = get_phonopy(
|
125 |
+
atoms=atoms.copy(),
|
126 |
supercell_matrix=supercell_matrix,
|
127 |
min_lengths=min_lengths,
|
128 |
symprec=symprec,
|
mlip_arena/tasks/utils.py
CHANGED
@@ -5,11 +5,11 @@ from __future__ import annotations
|
|
5 |
from pprint import pformat
|
6 |
|
7 |
import torch
|
8 |
-
from torch_dftd.torch_dftd3_calculator import TorchDFTD3Calculator
|
9 |
-
|
10 |
from ase import units
|
11 |
from ase.calculators.calculator import BaseCalculator
|
12 |
from ase.calculators.mixing import SumCalculator
|
|
|
|
|
13 |
from mlip_arena.models import MLIPEnum
|
14 |
|
15 |
try:
|
@@ -72,6 +72,7 @@ def get_calculator(
|
|
72 |
|
73 |
if isinstance(calculator_name, MLIPEnum) and calculator_name in MLIPEnum:
|
74 |
calc = calculator_name.value(**calculator_kwargs)
|
|
|
75 |
elif isinstance(calculator_name, str) and hasattr(MLIPEnum, calculator_name):
|
76 |
calc = MLIPEnum[calculator_name].value(**calculator_kwargs)
|
77 |
elif isinstance(calculator_name, type) and issubclass(
|
@@ -79,11 +80,13 @@ def get_calculator(
|
|
79 |
):
|
80 |
logger.warning(f"Using custom calculator class: {calculator_name}")
|
81 |
calc = calculator_name(**calculator_kwargs)
|
|
|
82 |
elif isinstance(calculator_name, BaseCalculator):
|
83 |
logger.warning(
|
84 |
f"Using custom calculator object (kwargs are ignored): {calculator_name}"
|
85 |
)
|
86 |
calc = calculator_name
|
|
|
87 |
else:
|
88 |
raise ValueError(f"Invalid calculator: {calculator_name}")
|
89 |
|
@@ -107,5 +110,5 @@ def get_calculator(
|
|
107 |
if dispersion_kwargs:
|
108 |
logger.info(pformat(dispersion_kwargs))
|
109 |
|
110 |
-
assert isinstance(calc, BaseCalculator)
|
111 |
return calc
|
|
|
5 |
from pprint import pformat
|
6 |
|
7 |
import torch
|
|
|
|
|
8 |
from ase import units
|
9 |
from ase.calculators.calculator import BaseCalculator
|
10 |
from ase.calculators.mixing import SumCalculator
|
11 |
+
from torch_dftd.torch_dftd3_calculator import TorchDFTD3Calculator
|
12 |
+
|
13 |
from mlip_arena.models import MLIPEnum
|
14 |
|
15 |
try:
|
|
|
72 |
|
73 |
if isinstance(calculator_name, MLIPEnum) and calculator_name in MLIPEnum:
|
74 |
calc = calculator_name.value(**calculator_kwargs)
|
75 |
+
calc.__str__ = lambda: calculator_name.name
|
76 |
elif isinstance(calculator_name, str) and hasattr(MLIPEnum, calculator_name):
|
77 |
calc = MLIPEnum[calculator_name].value(**calculator_kwargs)
|
78 |
elif isinstance(calculator_name, type) and issubclass(
|
|
|
80 |
):
|
81 |
logger.warning(f"Using custom calculator class: {calculator_name}")
|
82 |
calc = calculator_name(**calculator_kwargs)
|
83 |
+
calc.__str__ = lambda: f"{calc.__class__.__name__}"
|
84 |
elif isinstance(calculator_name, BaseCalculator):
|
85 |
logger.warning(
|
86 |
f"Using custom calculator object (kwargs are ignored): {calculator_name}"
|
87 |
)
|
88 |
calc = calculator_name
|
89 |
+
calc.__str__ = lambda: f"{calc.__class__.__name__}"
|
90 |
else:
|
91 |
raise ValueError(f"Invalid calculator: {calculator_name}")
|
92 |
|
|
|
110 |
if dispersion_kwargs:
|
111 |
logger.info(pformat(dispersion_kwargs))
|
112 |
|
113 |
+
assert isinstance(calc, BaseCalculator)
|
114 |
return calc
|
tests/test_elasticity.py
CHANGED
@@ -4,6 +4,7 @@ import numpy as np
|
|
4 |
import pytest
|
5 |
from mlip_arena.models import MLIPEnum
|
6 |
from mlip_arena.tasks.elasticity import run as ELASTICITY
|
|
|
7 |
from prefect.testing.utilities import prefect_test_harness
|
8 |
|
9 |
from ase.build import bulk
|
@@ -22,9 +23,9 @@ def test_elasticity(model: MLIPEnum):
|
|
22 |
with prefect_test_harness():
|
23 |
result = ELASTICITY(
|
24 |
atoms=bulk("Cu", "fcc", a=3.6),
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
optimizer="BFGSLineSearch",
|
29 |
optimizer_kwargs=None,
|
30 |
filter="FrechetCell",
|
|
|
4 |
import pytest
|
5 |
from mlip_arena.models import MLIPEnum
|
6 |
from mlip_arena.tasks.elasticity import run as ELASTICITY
|
7 |
+
from mlip_arena.tasks.utils import get_calculator
|
8 |
from prefect.testing.utilities import prefect_test_harness
|
9 |
|
10 |
from ase.build import bulk
|
|
|
23 |
with prefect_test_harness():
|
24 |
result = ELASTICITY(
|
25 |
atoms=bulk("Cu", "fcc", a=3.6),
|
26 |
+
calculator=get_calculator(
|
27 |
+
calculator_name=model.name,
|
28 |
+
),
|
29 |
optimizer="BFGSLineSearch",
|
30 |
optimizer_kwargs=None,
|
31 |
filter="FrechetCell",
|
tests/test_eos.py
CHANGED
@@ -7,6 +7,8 @@ from prefect.testing.utilities import prefect_test_harness
|
|
7 |
|
8 |
from mlip_arena.models import MLIPEnum
|
9 |
from mlip_arena.tasks.eos import run as EOS
|
|
|
|
|
10 |
|
11 |
|
12 |
@flow(persist_result=True)
|
@@ -17,9 +19,9 @@ def single_eos_flow(calculator_name, concurrent=True, cache=False):
|
|
17 |
refresh_cache=not cache,
|
18 |
)(
|
19 |
atoms=atoms,
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
optimizer="BFGSLineSearch",
|
24 |
optimizer_kwargs=None,
|
25 |
filter="FrechetCell",
|
@@ -62,4 +64,4 @@ def test_eos(model: MLIPEnum, concurrent: bool):
|
|
62 |
cache=True,
|
63 |
)
|
64 |
assert isinstance(b0_cache := result["b0"], float)
|
65 |
-
assert b0_scratch == pytest.approx(b0_cache, rel=1e-
|
|
|
7 |
|
8 |
from mlip_arena.models import MLIPEnum
|
9 |
from mlip_arena.tasks.eos import run as EOS
|
10 |
+
from mlip_arena.tasks.utils import get_calculator
|
11 |
+
|
12 |
|
13 |
|
14 |
@flow(persist_result=True)
|
|
|
19 |
refresh_cache=not cache,
|
20 |
)(
|
21 |
atoms=atoms,
|
22 |
+
calculator=get_calculator(
|
23 |
+
calculator_name=calculator_name,
|
24 |
+
),
|
25 |
optimizer="BFGSLineSearch",
|
26 |
optimizer_kwargs=None,
|
27 |
filter="FrechetCell",
|
|
|
64 |
cache=True,
|
65 |
)
|
66 |
assert isinstance(b0_cache := result["b0"], float)
|
67 |
+
assert b0_scratch == pytest.approx(b0_cache, rel=1e-5)
|
tests/test_md.py
CHANGED
@@ -6,6 +6,7 @@ from ase.build import bulk
|
|
6 |
|
7 |
from mlip_arena.models import MLIPEnum
|
8 |
from mlip_arena.tasks.md import run as MD
|
|
|
9 |
|
10 |
atoms = bulk("Cu", "fcc", a=3.6)
|
11 |
|
@@ -15,8 +16,9 @@ def test_nve(model: MLIPEnum):
|
|
15 |
|
16 |
result = MD.fn(
|
17 |
atoms,
|
18 |
-
|
19 |
-
|
|
|
20 |
ensemble="nve",
|
21 |
dynamics="velocityverlet",
|
22 |
total_time=10,
|
|
|
6 |
|
7 |
from mlip_arena.models import MLIPEnum
|
8 |
from mlip_arena.tasks.md import run as MD
|
9 |
+
from mlip_arena.tasks.utils import get_calculator
|
10 |
|
11 |
atoms = bulk("Cu", "fcc", a=3.6)
|
12 |
|
|
|
16 |
|
17 |
result = MD.fn(
|
18 |
atoms,
|
19 |
+
calculator=get_calculator(
|
20 |
+
calculator_name=model.name,
|
21 |
+
),
|
22 |
ensemble="nve",
|
23 |
dynamics="velocityverlet",
|
24 |
total_time=10,
|
tests/test_neb.py
CHANGED
@@ -3,6 +3,7 @@ import sys
|
|
3 |
import pytest
|
4 |
from mlip_arena.models import MLIPEnum
|
5 |
from mlip_arena.tasks import NEB_FROM_ENDPOINTS
|
|
|
6 |
from prefect.testing.utilities import prefect_test_harness
|
7 |
|
8 |
from ase.spacegroup import crystal
|
@@ -35,7 +36,9 @@ def test_neb(model: MLIPEnum):
|
|
35 |
start=start.copy(),
|
36 |
end=end.copy(),
|
37 |
n_images=5,
|
38 |
-
|
|
|
|
|
39 |
optimizer="FIRE2",
|
40 |
)
|
41 |
|
|
|
3 |
import pytest
|
4 |
from mlip_arena.models import MLIPEnum
|
5 |
from mlip_arena.tasks import NEB_FROM_ENDPOINTS
|
6 |
+
from mlip_arena.tasks.utils import get_calculator
|
7 |
from prefect.testing.utilities import prefect_test_harness
|
8 |
|
9 |
from ase.spacegroup import crystal
|
|
|
36 |
start=start.copy(),
|
37 |
end=end.copy(),
|
38 |
n_images=5,
|
39 |
+
calculator=get_calculator(
|
40 |
+
calculator_name=model.name,
|
41 |
+
),
|
42 |
optimizer="FIRE2",
|
43 |
)
|
44 |
|