Skip to content

Commit

Permalink
Merge pull request materialsproject#561 from esoteric-ephemera/main
Browse files Browse the repository at this point in the history
Clean up VASP powerups, correct params in MP flows, test for `_set_u_params`, test for `_set_kspacing`, fix `_get_incar` method of `VaspInputSet`
  • Loading branch information
janosh authored Oct 18, 2023
2 parents 2843d04 + 3398395 commit 4e3d156
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 89 deletions.
166 changes: 79 additions & 87 deletions src/atomate2/vasp/powerups.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@
from atomate2.vasp.jobs.base import BaseVaspMaker


def update_user_incar_settings(
def update_vasp_input_generators(
flow: Job | Flow | Maker,
incar_updates: dict[str, Any],
dict_mod_updates: dict[str, Any],
name_filter: str | None = None,
class_filter: type[Maker] | None = BaseVaspMaker,
) -> Job | Flow | Maker:
"""
Update the user_incar_settings of any VaspInputGenerators in the flow.
Alternatively, if a Maker is supplied, the user_incar_settings of the maker will
be updated.
Update any VaspInputGenerators or Makers in the flow.
Note, this returns a copy of the original Job/Flow/Maker. I.e., the update does not
happen in place.
Expand All @@ -30,9 +27,9 @@ def update_user_incar_settings(
----------
flow : .Job or .Flow or .Maker
A job, flow or Maker.
incar_updates : dict
The updates to apply. Existing keys in user_incar_settings will not be modified
unless explicitly specified in ``incar_updates``.
dict_mod_updates : dict
The updates to apply. Existing keys will not be modified unless explicitly
specified in ``dict_mod_updates``.
name_filter : str or None
A filter for the name of the jobs.
class_filter : Maker or None
Expand All @@ -44,10 +41,6 @@ def update_user_incar_settings(
Job or Flow or Maker
A copy of the input flow/job/maker modified to use the updated incar settings.
"""
dict_mod_updates = {
f"input_set_generator->user_incar_settings->{k}": v
for k, v in incar_updates.items()
}
updated_flow = deepcopy(flow)
if isinstance(updated_flow, Maker):
updated_flow = updated_flow.update_kwargs(
Expand All @@ -66,6 +59,50 @@ def update_user_incar_settings(
return updated_flow


def update_user_incar_settings(
flow: Job | Flow | Maker,
incar_updates: dict[str, Any],
name_filter: str | None = None,
class_filter: type[Maker] | None = BaseVaspMaker,
) -> Job | Flow | Maker:
"""
Update the user_incar_settings of any VaspInputGenerators in the flow.
Alternatively, if a Maker is supplied, the user_incar_settings of the maker will
be updated.
Note, this returns a copy of the original Job/Flow/Maker. I.e., the update does not
happen in place.
Parameters
----------
flow : .Job or .Flow or .Maker
A job, flow or Maker.
incar_updates : dict
The updates to apply. Existing keys in user_incar_settings will not be modified
unless explicitly specified in ``incar_updates``.
name_filter : str or None
A filter for the name of the jobs.
class_filter : Maker or None
A filter for the VaspMaker class used to generate the flows. Note the class
filter will match any subclasses.
Returns
-------
Job or Flow or Maker
A copy of the input flow/job/maker modified to use the updated incar settings.
"""
return update_vasp_input_generators(
flow=flow,
dict_mod_updates={
f"input_set_generator->user_incar_settings->{k}": v
for k, v in incar_updates.items()
},
name_filter=name_filter,
class_filter=class_filter,
)


def update_user_potcar_settings(
flow: Job | Flow | Maker,
potcar_updates: dict[str, Any],
Expand Down Expand Up @@ -99,26 +136,15 @@ def update_user_potcar_settings(
Job or Flow or Maker
A copy of the input flow/job/maker modified to use the updated potcar settings.
"""
dict_mod_updates = {
f"input_set_generator->user_potcar_settings->{k}": v
for k, v in potcar_updates.items()
}
updated_flow = deepcopy(flow)
if isinstance(updated_flow, Maker):
updated_flow = updated_flow.update_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
else:
updated_flow.update_maker_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
return updated_flow
return update_vasp_input_generators(
flow=flow,
dict_mod_updates={
f"input_set_generator->user_potcar_settings->{k}": v
for k, v in potcar_updates.items()
},
name_filter=name_filter,
class_filter=class_filter,
)


def update_user_potcar_functional(
Expand Down Expand Up @@ -153,25 +179,14 @@ def update_user_potcar_functional(
Job or Flow or Maker
A copy of the input flow/job/maker modified to use the updated potcar settings.
"""
dict_mod_updates = {
"input_set_generator->user_potcar_functional": potcar_functional
}
updated_flow = deepcopy(flow)
if isinstance(updated_flow, Maker):
updated_flow = updated_flow.update_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
else:
updated_flow.update_maker_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
return updated_flow
return update_vasp_input_generators(
flow=flow,
dict_mod_updates={
"input_set_generator->user_potcar_functional": potcar_functional
},
name_filter=name_filter,
class_filter=class_filter,
)


def update_user_kpoints_settings(
Expand Down Expand Up @@ -217,23 +232,12 @@ def update_user_kpoints_settings(
f"input_set_generator->user_kpoints_settings->{k}": v
for k, v in kpoints_updates.items()
}

updated_flow = deepcopy(flow)
if isinstance(updated_flow, Maker):
updated_flow = updated_flow.update_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
else:
updated_flow.update_maker_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
return updated_flow
return update_vasp_input_generators(
flow=flow,
dict_mod_updates=dict_mod_updates,
name_filter=name_filter,
class_filter=class_filter,
)


def use_auto_ispin(
Expand Down Expand Up @@ -267,21 +271,9 @@ def use_auto_ispin(
Job or Flow or Maker
A copy of the input flow/job/maker but with auto_ispin set.
"""
dict_mod_updates = {"input_set_generator->auto_ispin": value}

updated_flow = deepcopy(flow)
if isinstance(updated_flow, Maker):
updated_flow = updated_flow.update_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
else:
updated_flow.update_maker_kwargs(
{"_set": dict_mod_updates},
name_filter=name_filter,
class_filter=class_filter,
dict_mod=True,
)
return updated_flow
return update_vasp_input_generators(
flow=flow,
dict_mod_updates={"input_set_generator->auto_ispin": value},
name_filter=name_filter,
class_filter=class_filter,
)
9 changes: 7 additions & 2 deletions src/atomate2/vasp/sets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ class VaspInputGenerator(InputGenerator):
force_gamma: bool = True
symprec: float = SETTINGS.SYMPREC
vdw: str = None
# copy _BASE_VASP_SET to ensure each class instance has its own copy
# otherwise in-place changes can affect other instances
config_dict: dict = field(default_factory=lambda: _BASE_VASP_SET)
inherit_incar: bool = None

Expand Down Expand Up @@ -692,6 +694,7 @@ def _get_incar(
elif isinstance(self.auto_kspacing, float):
# interpret auto_kspacing as bandgap and set KSPACING based on user input
bandgap = self.auto_kspacing

_set_kspacing(incar, incar_settings, self.user_incar_settings, bandgap, kpoints)

# apply updates from auto options, careful not to override user_incar_settings
Expand Down Expand Up @@ -1079,7 +1082,7 @@ def _get_kspacing(bandgap: float, tol: float = 1e-4) -> float:
kspacing = 2 * np.pi * 1.0265 / (rmin - 1.0183) # Eq. 29

# cap kspacing at a max of 0.44, per internal benchmarking
return kspacing if 0.22 < kspacing < 0.44 else 0.44
return min(kspacing, 0.44)


def _set_kspacing(
Expand Down Expand Up @@ -1112,14 +1115,16 @@ def _set_kspacing(

elif "KSPACING" in user_incar_settings:
incar["KSPACING"] = user_incar_settings["KSPACING"]
elif incar_settings.get("KSPACING") and isinstance(bandgap, float):

elif incar_settings.get("KSPACING") and isinstance(bandgap, (int, float)):
# will always default to 0.22 in first run as one
# cannot be sure if one treats a metal or
# semiconductor/insulator
incar["KSPACING"] = _get_kspacing(bandgap)
# This should default to ISMEAR=0 if band gap is not known (first computation)
# if not from_prev:
# # be careful to not override user_incar_settings

elif incar_settings.get("KSPACING"):
incar["KSPACING"] = incar_settings["KSPACING"]

Expand Down
69 changes: 69 additions & 0 deletions tests/vasp/test_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pymatgen.core import Lattice, Species, Structure

from atomate2.vasp.sets.core import StaticSetGenerator
from atomate2.vasp.sets.mp import MPMetaGGARelaxSetGenerator


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -35,6 +36,16 @@ def struct_with_magmoms(struct_no_magmoms) -> Structure:
return struct


@pytest.fixture(scope="module")
def struct_no_u_params() -> Structure:
"""Dummy SiO structure with no anticipated +U corrections"""
return Structure(
lattice=Lattice.cubic(3),
species=["Si", "O"],
coords=[[0, 0, 0], [0.5, 0.5, 0.5]],
)


def test_user_incar_settings():
structure = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ["H"], [[0, 0, 0]])

Expand Down Expand Up @@ -119,3 +130,61 @@ def test_incar_magmoms_precedence(structure, user_incar_settings, request) -> No
input_gen.config_dict["INCAR"]["MAGMOM"].get(str(s), 0.6)
for s in structure.species
]


@pytest.mark.parametrize("structure", ["struct_no_magmoms", "struct_no_u_params"])
def test_set_u_params(structure, request) -> None:
structure = request.getfixturevalue(structure)
input_gen = StaticSetGenerator()
incar = input_gen.get_input_set(structure, potcar_spec=True).incar

has_nonzero_u = (
any(
input_gen.config_dict["INCAR"]["LDAUU"]["O"].get(str(site.specie), 0) > 0
for site in structure
)
and input_gen.config_dict["INCAR"]["LDAU"]
)

if has_nonzero_u:
# if at least one site has a nonzero U value in the config_dict,
# ensure that there are LDAU* keys, and that they match expected values
# in config_dict
assert len([key for key in incar if key.startswith("LDAU")]) > 0
for LDAU_key in ["LDAUU", "LDAUJ", "LDAUL"]:
for isite, site in enumerate(structure):
assert incar[LDAU_key][isite] == input_gen.config_dict["INCAR"][
LDAU_key
]["O"].get(str(site.specie), 0)
else:
# if no sites have a nonzero U value in the config_dict,
# ensure that no keys starting with LDAU are in the INCAR
assert len([key for key in incar if key.startswith("LDAU")]) == 0


@pytest.mark.parametrize(
"bandgap, expected_params",
[
(0, {"KSPACING": 0.22, "ISMEAR": 2, "SIGMA": 0.2}),
(0.1, {"KSPACING": 0.269695615, "ISMEAR": -5, "SIGMA": 0.05}),
(1, {"KSPACING": 0.302352354, "ISMEAR": -5, "SIGMA": 0.05}),
(2, {"KSPACING": 0.349355136, "ISMEAR": -5, "SIGMA": 0.05}),
(5, {"KSPACING": 0.44, "ISMEAR": -5, "SIGMA": 0.05}),
(10, {"KSPACING": 0.44, "ISMEAR": -5, "SIGMA": 0.05}),
],
)
def test_set_kspacing_and_auto_ismear(
struct_no_magmoms, bandgap, expected_params, monkeypatch
):
static_set = MPMetaGGARelaxSetGenerator(auto_ismear=True, auto_kspacing=True)

incar = static_set._get_incar(
structure=struct_no_magmoms,
kpoints=None,
previous_incar=None,
incar_updates={},
bandgap=bandgap,
)

actual = {key: incar[key] for key in expected_params}
assert actual == pytest.approx(expected_params)

0 comments on commit 4e3d156

Please sign in to comment.