Skip to content

Commit 6349766

Browse files
Docs update, forcefield elastic convenience maker, forcefield enum hydration (#1072)
* update EOS docs * udpate docs with implementation details * add convenience constructor method for forcefield ElasticMaker * allow MLFF enum to treat str(MLFF.) as valid member * precommit * add small MLFF test
1 parent 4b84d78 commit 6349766

File tree

6 files changed

+129
-14
lines changed

6 files changed

+129
-14
lines changed

docs/user/codes/vasp.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,39 @@ Afterwards, equation of state fits are performed with phonopy.
270270

271271
### Equation of State Workflow
272272

273-
An equation of state workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant
273+
An equation of state (EOS) workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant
274274
volumes are performed. Additional static calculations might be performed afterwards to arrive at more
275-
accurate energies. Then, an equation of state fit is performed with pymatgen.
275+
accurate energies. Then, an EOS fit is performed with pymatgen.
276+
277+
The output of the workflow is, by default, a dictionary containing the energy and volume data generated with DFT, in addition to fitted equation of state parameters for all models currently available in pymatgen (Murnaghan, Birch-Murnaghan, Poirier-Tarantola, and Vinet/UBER).
278+
279+
#### Materials Project-compliant workflows
280+
281+
If the user wishes to reproduce the EOS data currently in the Materials Project, they should use the atomate 1-compatible `MPLegacy`-prefixed flows (and jobs and input sets). For performing updated PBE-GGA EOS flows with Materials Project-compliant parameters, the user should use the `MPGGA`-prefixed classes. Lastly, the `MPMetaGGA`-prefixed classes allow the user to perform Materials Project-compliant r<sup>2</sup>SCAN EOS workflows.
282+
283+
**Summary:** For Materials Project-compliant equation of state (EOS) workflows, the user should use:
284+
* `MPGGAEosMaker` for faster, lower-accuracy calculation with the PBE-GGA
285+
* `MPMetaGGAEosMaker` for higher-accuracy but slower calculations with the r<sup>2</sup>SCAN meta-GGA
286+
* `MPLegacyEosMaker` for consistency with the PBE-GGA data currently distributed by the Materials Project
287+
288+
#### Implementation details
289+
290+
The Materials Project-compliant EOS flows, jobs, and sets currently use three prefixes to indicate their usage.
291+
* `MPGGA`: MP-compatible PBE-GGA (current)
292+
* `MPMetaGGA`: MP-compatible r<sup>2</sup>SCAN meta-GGA (current)
293+
* `MPLegacy`: a reproduction of the atomate 1 implementation, described in
294+
K. Latimer, S. Dwaraknath, K. Mathew, D. Winston, and K.A. Persson, npj Comput. Materials **vol. 4**, p. 40 (2018), DOI: 10.1038/s41524-018-0091-x
295+
296+
For reference, the original atomate workflows can be found here:
297+
* [`atomate.vasp.workflows.base.wf_bulk_modulus`](https://github.com/hackingmaterials/atomate/blob/main/atomate/vasp/workflows/presets/core.py#L564)
298+
* [`atomate.vasp.workflows.base.bulk_modulus.get_wf_bulk_modulus`](https://github.com/hackingmaterials/atomate/blob/main/atomate/vasp/workflows/base/bulk_modulus.py#L21)
299+
300+
In the original atomate 1 workflow and the atomate2 `MPLegacyEosMaker`, the k-point density is **extremely** high. This is despite the convergence tests in the supplementary information
301+
of Latimer *et al.* not showing strong sensitivity when the "number of ***k***-points per reciprocal atom" (KPPRA) is at least 3,000.
302+
303+
To make the `MPGGAEosMaker` and `MPMetaGGAEosMaker` more tractable for high-throughput jobs, their input sets (`MPGGAEos{Relax,Static}SetGenerator` and `MPMetaGGAEos{Relax,Static}SetGenerator` respectively) still use the highest ***k***-point density in standard Materials Project jobs, `KSPACING = 0.22` Å<sup>-1</sup>, which is comparable to KPPRA = 3,000.
304+
305+
This choice is justified by Fig. S12 of the supplemantary information of Latimer *et al.*, which shows that all fitted EOS parameters (equilibrium energy $E_0$, equilibrium volume $V_0$, bulk modulus $B_0$, and bulk modulus pressure derivative $B_1$) do not deviate by more than 1.5%, and typically by less than 0.1%, from well-converged values when KPPRA = 3,000.
276306

277307
### LOBSTER
278308

src/atomate2/forcefields/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from __future__ import annotations
44

55
from enum import Enum
6+
from typing import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from typing import Any
610

711

812
class MLFF(Enum): # TODO inherit from StrEnum when 3.11+
@@ -17,6 +21,16 @@ class MLFF(Enum): # TODO inherit from StrEnum when 3.11+
1721
Nequip = "Nequip"
1822
SevenNet = "SevenNet"
1923

24+
@classmethod
25+
def _missing_(cls, value: Any) -> Any:
26+
"""Allow input of str(MLFF) as valid enum."""
27+
if isinstance(value, str):
28+
value = value.split("MLFF.")[-1]
29+
for member in cls:
30+
if member.value == value:
31+
return member
32+
return None
33+
2034

2135
def _get_formatted_ff_name(force_field_name: str | MLFF) -> str:
2236
"""

src/atomate2/forcefields/flows/elastic.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,24 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
6+
from typing import TYPE_CHECKING
67

78
from atomate2 import SETTINGS
89
from atomate2.common.flows.elastic import BaseElasticMaker
10+
from atomate2.forcefields import MLFF, _get_formatted_ff_name
911
from atomate2.forcefields.jobs import ForceFieldRelaxMaker
1012

13+
if TYPE_CHECKING:
14+
from typing import Any
15+
16+
from typing_extensions import Self
17+
18+
# default options for the forcefield makers in ElasticMaker
19+
_DEFAULT_RELAX_KWARGS: dict[str, Any] = {
20+
"force_field_name": "CHGNet",
21+
"relax_kwargs": {"fmax": 0.00001},
22+
}
23+
1124

1225
@dataclass
1326
class ElasticMaker(BaseElasticMaker):
@@ -62,16 +75,12 @@ class ElasticMaker(BaseElasticMaker):
6275
symprec: float = SETTINGS.SYMPREC
6376
bulk_relax_maker: ForceFieldRelaxMaker | None = field(
6477
default_factory=lambda: ForceFieldRelaxMaker(
65-
force_field_name="CHGNet",
66-
relax_cell=True,
67-
relax_kwargs={"fmax": 0.00001},
78+
relax_cell=True, **_DEFAULT_RELAX_KWARGS
6879
)
6980
)
7081
elastic_relax_maker: ForceFieldRelaxMaker | None = field(
7182
default_factory=lambda: ForceFieldRelaxMaker(
72-
force_field_name="CHGNet",
73-
relax_cell=False,
74-
relax_kwargs={"fmax": 0.00001},
83+
relax_cell=False, **_DEFAULT_RELAX_KWARGS
7584
)
7685
) # constant volume relaxation
7786
max_failed_deformations: int | float | None = None
@@ -89,3 +98,44 @@ def prev_calc_dir_argname(self) -> str | None:
8998
Note: this is only applicable if a relax_maker is specified; i.e., two
9099
calculations are performed for each ordering (relax -> static)
91100
"""
101+
102+
@classmethod
103+
def from_force_field_name(
104+
cls,
105+
force_field_name: str | MLFF,
106+
mlff_kwargs: dict | None = None,
107+
**kwargs,
108+
) -> Self:
109+
"""
110+
Create an elastic flow from a forcefield name.
111+
112+
Parameters
113+
----------
114+
force_field_name : str or .MLFF
115+
The name of the force field.
116+
mlff_kwargs : dict or None (default)
117+
kwargs to pass to `ForceFieldRelaxMaker`.
118+
**kwargs
119+
Additional kwargs to pass to ElasticMaker.
120+
121+
Returns
122+
-------
123+
ElasticMaker
124+
"""
125+
default_kwargs: dict[str, Any] = {
126+
**_DEFAULT_RELAX_KWARGS,
127+
**(mlff_kwargs or {}),
128+
"force_field_name": _get_formatted_ff_name(force_field_name),
129+
}
130+
return cls(
131+
name=f"{str(force_field_name).split('MLFF.')[-1]} elastic",
132+
bulk_relax_maker=ForceFieldRelaxMaker(
133+
relax_cell=True,
134+
**default_kwargs,
135+
),
136+
elastic_relax_maker=ForceFieldRelaxMaker(
137+
relax_cell=False,
138+
**default_kwargs,
139+
),
140+
**kwargs,
141+
)

src/atomate2/forcefields/flows/eos.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Flows to generate EOS fits using CHGNet, M3GNet, or MACE."""
1+
"""Flows to generate EOS fits using machine learned interatomic potentials."""
22

33
from __future__ import annotations
44

@@ -62,6 +62,7 @@ def from_force_field_name(
6262
cls,
6363
force_field_name: str | MLFF,
6464
relax_initial_structure: bool = True,
65+
**kwargs,
6566
) -> Self:
6667
"""
6768
Create an EOS flow from a forcefield name.
@@ -72,6 +73,9 @@ def from_force_field_name(
7273
The name of the force field.
7374
relax_initial_structure: bool = True
7475
Whether to relax the initial structure before performing an EOS fit.
76+
**kwargs
77+
Additional kwargs to pass to ElasticMaker
78+
7579
7680
Returns
7781
-------
@@ -89,6 +93,7 @@ def from_force_field_name(
8993
force_field_name=force_field_name, relax_cell=False
9094
),
9195
static_maker=None,
96+
**kwargs,
9297
)
9398

9499

tests/forcefields/flows/test_elastic.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
from atomate2.forcefields.jobs import ForceFieldRelaxMaker
88

99

10-
def test_elastic_wf_with_mace(clean_dir, si_structure, test_dir):
10+
@pytest.mark.parametrize("convenience_constructor", [True, False])
11+
def test_elastic_wf_with_mace(
12+
clean_dir, si_structure, test_dir, convenience_constructor: bool
13+
):
1114
si_prim = SpacegroupAnalyzer(si_structure).get_primitive_standard_structure()
1215
model_path = f"{test_dir}/forcefields/mace/MACE.model"
1316
common_kwds = {
@@ -16,10 +19,17 @@ def test_elastic_wf_with_mace(clean_dir, si_structure, test_dir):
1619
"relax_kwargs": {"fmax": 0.00001},
1720
}
1821

19-
flow = ElasticMaker(
20-
bulk_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=True),
21-
elastic_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=False),
22-
).make(si_prim)
22+
if convenience_constructor:
23+
common_kwds.pop("force_field_name")
24+
flow = ElasticMaker.from_force_field_name(
25+
force_field_name="MACE",
26+
mlff_kwargs=common_kwds,
27+
).make(si_prim)
28+
else:
29+
flow = ElasticMaker(
30+
bulk_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=True),
31+
elastic_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=False),
32+
).make(si_prim)
2333

2434
# run the flow or job and ensure that it finished running successfully
2535
responses = run_locally(flow, create_folders=True, ensure_success=True)

tests/forcefields/test_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
from atomate2.forcefields.utils import ase_calculator
55

66

7+
@pytest.mark.parametrize(("force_field"), [mlff.value for mlff in MLFF])
8+
def test_mlff(force_field: str):
9+
mlff = MLFF(force_field)
10+
assert mlff == MLFF(str(mlff)) == MLFF(str(mlff).split(".")[-1])
11+
12+
713
@pytest.mark.parametrize(("force_field"), ["CHGNet", "MACE"])
814
def test_ext_load(force_field: str):
915
decode_dict = {

0 commit comments

Comments
 (0)