Skip to content

Commit

Permalink
Docs update, forcefield elastic convenience maker, forcefield enum hy…
Browse files Browse the repository at this point in the history
…dration (#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
  • Loading branch information
esoteric-ephemera authored Nov 26, 2024
1 parent 4b84d78 commit 6349766
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 14 deletions.
34 changes: 32 additions & 2 deletions docs/user/codes/vasp.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,39 @@ Afterwards, equation of state fits are performed with phonopy.

### Equation of State Workflow

An equation of state workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant
An equation of state (EOS) workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant
volumes are performed. Additional static calculations might be performed afterwards to arrive at more
accurate energies. Then, an equation of state fit is performed with pymatgen.
accurate energies. Then, an EOS fit is performed with pymatgen.

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).

#### Materials Project-compliant workflows

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.

**Summary:** For Materials Project-compliant equation of state (EOS) workflows, the user should use:
* `MPGGAEosMaker` for faster, lower-accuracy calculation with the PBE-GGA
* `MPMetaGGAEosMaker` for higher-accuracy but slower calculations with the r<sup>2</sup>SCAN meta-GGA
* `MPLegacyEosMaker` for consistency with the PBE-GGA data currently distributed by the Materials Project

#### Implementation details

The Materials Project-compliant EOS flows, jobs, and sets currently use three prefixes to indicate their usage.
* `MPGGA`: MP-compatible PBE-GGA (current)
* `MPMetaGGA`: MP-compatible r<sup>2</sup>SCAN meta-GGA (current)
* `MPLegacy`: a reproduction of the atomate 1 implementation, described in
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

For reference, the original atomate workflows can be found here:
* [`atomate.vasp.workflows.base.wf_bulk_modulus`](https://github.com/hackingmaterials/atomate/blob/main/atomate/vasp/workflows/presets/core.py#L564)
* [`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)

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
of Latimer *et al.* not showing strong sensitivity when the "number of ***k***-points per reciprocal atom" (KPPRA) is at least 3,000.

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.

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.

### LOBSTER

Expand Down
14 changes: 14 additions & 0 deletions src/atomate2/forcefields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any


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

@classmethod
def _missing_(cls, value: Any) -> Any:
"""Allow input of str(MLFF) as valid enum."""
if isinstance(value, str):
value = value.split("MLFF.")[-1]
for member in cls:
if member.value == value:
return member
return None


def _get_formatted_ff_name(force_field_name: str | MLFF) -> str:
"""
Expand Down
62 changes: 56 additions & 6 deletions src/atomate2/forcefields/flows/elastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from atomate2 import SETTINGS
from atomate2.common.flows.elastic import BaseElasticMaker
from atomate2.forcefields import MLFF, _get_formatted_ff_name
from atomate2.forcefields.jobs import ForceFieldRelaxMaker

if TYPE_CHECKING:
from typing import Any

from typing_extensions import Self

# default options for the forcefield makers in ElasticMaker
_DEFAULT_RELAX_KWARGS: dict[str, Any] = {
"force_field_name": "CHGNet",
"relax_kwargs": {"fmax": 0.00001},
}


@dataclass
class ElasticMaker(BaseElasticMaker):
Expand Down Expand Up @@ -62,16 +75,12 @@ class ElasticMaker(BaseElasticMaker):
symprec: float = SETTINGS.SYMPREC
bulk_relax_maker: ForceFieldRelaxMaker | None = field(
default_factory=lambda: ForceFieldRelaxMaker(
force_field_name="CHGNet",
relax_cell=True,
relax_kwargs={"fmax": 0.00001},
relax_cell=True, **_DEFAULT_RELAX_KWARGS
)
)
elastic_relax_maker: ForceFieldRelaxMaker | None = field(
default_factory=lambda: ForceFieldRelaxMaker(
force_field_name="CHGNet",
relax_cell=False,
relax_kwargs={"fmax": 0.00001},
relax_cell=False, **_DEFAULT_RELAX_KWARGS
)
) # constant volume relaxation
max_failed_deformations: int | float | None = None
Expand All @@ -89,3 +98,44 @@ def prev_calc_dir_argname(self) -> str | None:
Note: this is only applicable if a relax_maker is specified; i.e., two
calculations are performed for each ordering (relax -> static)
"""

@classmethod
def from_force_field_name(
cls,
force_field_name: str | MLFF,
mlff_kwargs: dict | None = None,
**kwargs,
) -> Self:
"""
Create an elastic flow from a forcefield name.
Parameters
----------
force_field_name : str or .MLFF
The name of the force field.
mlff_kwargs : dict or None (default)
kwargs to pass to `ForceFieldRelaxMaker`.
**kwargs
Additional kwargs to pass to ElasticMaker.
Returns
-------
ElasticMaker
"""
default_kwargs: dict[str, Any] = {
**_DEFAULT_RELAX_KWARGS,
**(mlff_kwargs or {}),
"force_field_name": _get_formatted_ff_name(force_field_name),
}
return cls(
name=f"{str(force_field_name).split('MLFF.')[-1]} elastic",
bulk_relax_maker=ForceFieldRelaxMaker(
relax_cell=True,
**default_kwargs,
),
elastic_relax_maker=ForceFieldRelaxMaker(
relax_cell=False,
**default_kwargs,
),
**kwargs,
)
7 changes: 6 additions & 1 deletion src/atomate2/forcefields/flows/eos.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Flows to generate EOS fits using CHGNet, M3GNet, or MACE."""
"""Flows to generate EOS fits using machine learned interatomic potentials."""

from __future__ import annotations

Expand Down Expand Up @@ -62,6 +62,7 @@ def from_force_field_name(
cls,
force_field_name: str | MLFF,
relax_initial_structure: bool = True,
**kwargs,
) -> Self:
"""
Create an EOS flow from a forcefield name.
Expand All @@ -72,6 +73,9 @@ def from_force_field_name(
The name of the force field.
relax_initial_structure: bool = True
Whether to relax the initial structure before performing an EOS fit.
**kwargs
Additional kwargs to pass to ElasticMaker
Returns
-------
Expand All @@ -89,6 +93,7 @@ def from_force_field_name(
force_field_name=force_field_name, relax_cell=False
),
static_maker=None,
**kwargs,
)


Expand Down
20 changes: 15 additions & 5 deletions tests/forcefields/flows/test_elastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
from atomate2.forcefields.jobs import ForceFieldRelaxMaker


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

flow = ElasticMaker(
bulk_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=True),
elastic_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=False),
).make(si_prim)
if convenience_constructor:
common_kwds.pop("force_field_name")
flow = ElasticMaker.from_force_field_name(
force_field_name="MACE",
mlff_kwargs=common_kwds,
).make(si_prim)
else:
flow = ElasticMaker(
bulk_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=True),
elastic_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=False),
).make(si_prim)

# run the flow or job and ensure that it finished running successfully
responses = run_locally(flow, create_folders=True, ensure_success=True)
Expand Down
6 changes: 6 additions & 0 deletions tests/forcefields/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
from atomate2.forcefields.utils import ase_calculator


@pytest.mark.parametrize(("force_field"), [mlff.value for mlff in MLFF])
def test_mlff(force_field: str):
mlff = MLFF(force_field)
assert mlff == MLFF(str(mlff)) == MLFF(str(mlff).split(".")[-1])


@pytest.mark.parametrize(("force_field"), ["CHGNet", "MACE"])
def test_ext_load(force_field: str):
decode_dict = {
Expand Down

0 comments on commit 6349766

Please sign in to comment.