diff --git a/src/atomate2/vasp/powerups.py b/src/atomate2/vasp/powerups.py index e700c7cf72..a050654628 100644 --- a/src/atomate2/vasp/powerups.py +++ b/src/atomate2/vasp/powerups.py @@ -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. @@ -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 @@ -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( @@ -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], @@ -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( @@ -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( @@ -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( @@ -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, + ) diff --git a/src/atomate2/vasp/sets/base.py b/src/atomate2/vasp/sets/base.py index fd8cac917a..82429658bc 100644 --- a/src/atomate2/vasp/sets/base.py +++ b/src/atomate2/vasp/sets/base.py @@ -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 @@ -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 @@ -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( @@ -1112,7 +1115,8 @@ 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 @@ -1120,6 +1124,7 @@ def _set_kspacing( # 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"] diff --git a/tests/vasp/test_sets.py b/tests/vasp/test_sets.py index 1b5ebd5e26..c9cecb0c63 100644 --- a/tests/vasp/test_sets.py +++ b/tests/vasp/test_sets.py @@ -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") @@ -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]]) @@ -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)