Yuan (Cyrus) Chiang commited on
Commit
419b35b
·
unverified ·
1 Parent(s): a952a33

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 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 underlying 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 orchestractor [Prefect](https://www.prefect.io/) to enable advanced task/flow chaining and caching.
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
- calculator_name=model,
74
- calculator_kwargs={},
 
 
 
 
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["calculator_name"]
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
- calculator_name: str | MLIPEnum,
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
- calculator_name (str | MLIPEnum): The calculator name.
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
- calculator_name=calculator_name,
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["calculator_name"]
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
- calculator_name: str | MLIPEnum,
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
- calculator_name=calculator_name,
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
- calculator_name=calculator_name,
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
- calculator_name=calculator_name,
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 mlip_arena.models import MLIPEnum
86
- from mlip_arena.tasks.utils import get_calculator
 
 
 
 
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["calculator_name"]
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
- calculator_name: str | MLIPEnum,
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.calc = get_calculator(
225
- calculator_name=calculator_name,
226
- calculator_kwargs=calculator_kwargs,
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 mlip_arena.models import MLIPEnum
 
 
 
 
56
  from mlip_arena.tasks.optimize import run as OPT
57
- from mlip_arena.tasks.utils import get_calculator, logger, pformat
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["calculator_name"]
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
- calculator_name: str | MLIPEnum,
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
- calc = get_calculator(
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 = 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
- calculator_name: str | MLIPEnum,
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
- calculator_name=calculator_name,
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
- calculator_name=calculator_name,
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
- calculator_name,
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 mlip_arena.models import MLIPEnum
18
- from mlip_arena.tasks.utils import get_calculator, logger, pformat
 
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["calculator_name"]
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
- calculator_name: str | MLIPEnum,
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.calc = get_calculator(
72
- calculator_name=calculator_name,
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
- calculator = parameters["calculator"]
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
- calculator_name=model.name,
26
- calculator_kwargs={},
27
- device=None,
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
- calculator_name=calculator_name,
21
- calculator_kwargs={},
22
- device=None,
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-6)
 
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
- calculator_name=model.name,
19
- calculator_kwargs={},
 
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
- calculator_name=model.name,
 
 
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