|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from collections.abc import Sequence |
| 4 | +from glob import glob |
| 5 | +from pathlib import Path |
| 6 | +from typing import TYPE_CHECKING, cast |
| 7 | + |
| 8 | +import numpy as np |
| 9 | + |
| 10 | +from pymatgen.analysis.structure_matcher import StructureMatcher |
| 11 | +from pymatgen.io.vasp.inputs import Kpoints |
| 12 | +from pymatgen.io.vasp.outputs import Outcar, Vasprun |
| 13 | +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer |
| 14 | +from pymatgen.util.typing import Kpoint |
| 15 | + |
| 16 | +if TYPE_CHECKING: |
| 17 | + from typing import Literal, Union |
| 18 | + |
| 19 | + from pymatgen.core import Structure |
| 20 | + |
| 21 | + UserPotcarFunctional = Union[ |
| 22 | + Literal["PBE", "PBE_52", "PBE_54", "LDA", "LDA_52", "LDA_54", "PW91", "LDA_US", "PW91_US"], None |
| 23 | + ] |
| 24 | + |
| 25 | + |
| 26 | +class BadInputSetWarning(UserWarning): |
| 27 | + """Warning class for bad but legal VASP inputs.""" |
| 28 | + |
| 29 | + |
| 30 | +def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None) -> int: |
| 31 | + """Get value of ISPIN depending on the magnetisation in the OUTCAR and vasprun.""" |
| 32 | + if outcar is not None and outcar.magnetization is not None: |
| 33 | + # Turn off spin when magmom for every site is smaller than 0.02. |
| 34 | + site_magmom = np.array([i["tot"] for i in outcar.magnetization]) |
| 35 | + return 2 if np.any(np.abs(site_magmom) > 0.02) else 1 |
| 36 | + if vasprun is not None: |
| 37 | + return 2 if vasprun.is_spin else 1 |
| 38 | + return 2 |
| 39 | + |
| 40 | + |
| 41 | +def _get_recommended_lreal(structure: Structure) -> str | bool: |
| 42 | + """Get recommended LREAL flag based on the structure.""" |
| 43 | + return "Auto" if structure.num_sites > 16 else False |
| 44 | + |
| 45 | + |
| 46 | +def _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: |
| 47 | + """Combine multiple Kpoints objects.""" |
| 48 | + _labels: list[list[str]] = [] |
| 49 | + _kpoints: list[Sequence[Kpoint]] = [] |
| 50 | + _weights = [] |
| 51 | + |
| 52 | + for kpoints_object in filter(None, kpoints_objects): |
| 53 | + if kpoints_object.style != Kpoints.supported_modes.Reciprocal: |
| 54 | + raise ValueError("Can only combine kpoints with style=Kpoints.supported_modes.Reciprocal") |
| 55 | + if kpoints_object.labels is None: |
| 56 | + _labels.append([""] * len(kpoints_object.kpts)) |
| 57 | + else: |
| 58 | + _labels.append(kpoints_object.labels) |
| 59 | + |
| 60 | + _kpoints.append(kpoints_object.kpts) |
| 61 | + _weights.append(kpoints_object.kpts_weights) |
| 62 | + |
| 63 | + labels = np.concatenate(_labels).tolist() |
| 64 | + kpoints = np.concatenate(_kpoints).tolist() |
| 65 | + weights = np.concatenate(_weights).tolist() |
| 66 | + |
| 67 | + return Kpoints( |
| 68 | + comment="Combined k-points", |
| 69 | + style=Kpoints.supported_modes.Reciprocal, |
| 70 | + num_kpts=len(kpoints), |
| 71 | + kpts=cast(Sequence[Kpoint], kpoints), |
| 72 | + labels=labels, |
| 73 | + kpts_weights=weights, |
| 74 | + ) |
| 75 | + |
| 76 | + |
| 77 | +def _apply_incar_updates(incar, updates, skip: Sequence[str] = ()) -> None: |
| 78 | + """ |
| 79 | + Apply updates to an INCAR file. |
| 80 | +
|
| 81 | + Args: |
| 82 | + incar (dict): An incar. |
| 83 | + updates (dict): Updates to apply. |
| 84 | + skip (list of str): Keys to skip. |
| 85 | + """ |
| 86 | + for k, v in updates.items(): |
| 87 | + if k in skip: |
| 88 | + continue |
| 89 | + |
| 90 | + if v is None: |
| 91 | + incar.pop(k, None) |
| 92 | + else: |
| 93 | + incar[k] = v |
| 94 | + |
| 95 | + |
| 96 | +def _remove_unused_incar_params(incar, skip: Sequence[str] = ()) -> None: |
| 97 | + """ |
| 98 | + Remove INCAR parameters that are not actively used by VASP. |
| 99 | +
|
| 100 | + Args: |
| 101 | + incar (dict): An incar. |
| 102 | + skip (list of str): Keys to skip. |
| 103 | + """ |
| 104 | + # Turn off IBRION/ISIF/POTIM if NSW = 0 |
| 105 | + opt_flags = ["EDIFFG", "IBRION", "ISIF", "POTIM"] |
| 106 | + if incar.get("NSW", 0) == 0: |
| 107 | + for opt_flag in opt_flags: |
| 108 | + if opt_flag not in skip: |
| 109 | + incar.pop(opt_flag, None) |
| 110 | + |
| 111 | + # Remove MAGMOMs if they aren't used |
| 112 | + if incar.get("ISPIN", 1) == 1 and "MAGMOM" not in skip: |
| 113 | + incar.pop("MAGMOM", None) |
| 114 | + |
| 115 | + # Turn off +U flags if +U is not even used |
| 116 | + ldau_flags = ["LDAUU", "LDAUJ", "LDAUL", "LDAUTYPE"] |
| 117 | + if incar.get("LDAU", False) is False: |
| 118 | + for ldau_flag in ldau_flags: |
| 119 | + if ldau_flag not in skip: |
| 120 | + incar.pop(ldau_flag, None) |
| 121 | + |
| 122 | + |
| 123 | +def _get_nedos(vasprun: Vasprun | None, dedos: float) -> int: |
| 124 | + """Automatic setting of nedos using the energy range and the energy step.""" |
| 125 | + if vasprun is None: |
| 126 | + return 2000 |
| 127 | + |
| 128 | + emax = max(eigs.max() for eigs in vasprun.eigenvalues.values()) |
| 129 | + emin = min(eigs.min() for eigs in vasprun.eigenvalues.values()) |
| 130 | + return int((emax - emin) / dedos) |
| 131 | + |
| 132 | + |
| 133 | +def auto_kspacing(bandgap, bandgap_tol): |
| 134 | + """Automatically set kspacing based on the bandgap""" |
| 135 | + if bandgap is None or bandgap <= bandgap_tol: # metallic |
| 136 | + return 0.22 |
| 137 | + |
| 138 | + rmin = max(1.5, 25.22 - 2.87 * bandgap) # Eq. 25 |
| 139 | + kspacing = 2 * np.pi * 1.0265 / (rmin - 1.0183) # Eq. 29 |
| 140 | + |
| 141 | + # cap kspacing at a max of 0.44, per internal benchmarking |
| 142 | + return min(kspacing, 0.44) |
| 143 | + |
| 144 | + |
| 145 | +def get_valid_magmom_struct(structure: Structure, inplace: bool = True, spin_mode: str = "auto") -> Structure: |
| 146 | + """ |
| 147 | + Make sure that the structure has valid magmoms based on the kind of calculation. |
| 148 | +
|
| 149 | + Fill in missing Magmom values. |
| 150 | +
|
| 151 | + Args: |
| 152 | + structure: The input structure |
| 153 | + inplace: True: edit magmoms of the input structure; False: return new structure |
| 154 | + spin_mode: "scalar"/"vector"/"none"/"auto" only first letter (s/v/n) is needed. |
| 155 | + dictates how the spin configuration will be determined. |
| 156 | +
|
| 157 | + - auto: read the existing magmom values and decide |
| 158 | + - scalar: use a single scalar value (for spin up/down) |
| 159 | + - vector: use a vector value for spin-orbit systems |
| 160 | + - none: Remove all the magmom information |
| 161 | +
|
| 162 | + Returns: |
| 163 | + New structure if inplace is False |
| 164 | + """ |
| 165 | + default_values = {"s": 1.0, "v": [1.0, 1.0, 1.0], "n": None} |
| 166 | + if spin_mode[0].lower() == "a": |
| 167 | + mode = "n" |
| 168 | + for site in structure: |
| 169 | + if "magmom" not in site.properties or site.properties["magmom"] is None: |
| 170 | + pass |
| 171 | + elif isinstance(site.properties["magmom"], (float, int)): |
| 172 | + if mode == "v": |
| 173 | + raise TypeError("Magmom type conflict") |
| 174 | + mode = "s" |
| 175 | + if isinstance(site.properties["magmom"], int): |
| 176 | + site.properties["magmom"] = float(site.properties["magmom"]) |
| 177 | + elif len(site.properties["magmom"]) == 3: |
| 178 | + if mode == "s": |
| 179 | + raise TypeError("Magmom type conflict") |
| 180 | + mode = "v" |
| 181 | + else: |
| 182 | + raise TypeError("Unrecognized Magmom Value") |
| 183 | + else: |
| 184 | + mode = spin_mode[0].lower() |
| 185 | + |
| 186 | + ret_struct = structure if inplace else structure.copy() |
| 187 | + for site in ret_struct: |
| 188 | + if mode == "n": |
| 189 | + if "magmom" in site.properties: |
| 190 | + site.properties.pop("magmom") |
| 191 | + elif "magmom" not in site.properties or site.properties["magmom"] is None: |
| 192 | + site.properties["magmom"] = default_values[mode] |
| 193 | + |
| 194 | + return ret_struct |
| 195 | + |
| 196 | + |
| 197 | +def get_structure_from_prev_run(vasprun, outcar=None) -> Structure: |
| 198 | + """ |
| 199 | + Process structure from previous run. |
| 200 | +
|
| 201 | + Args: |
| 202 | + vasprun (Vasprun): Vasprun that contains the final structure from previous run. |
| 203 | + outcar (Outcar): Outcar that contains the magnetization info from previous run. |
| 204 | +
|
| 205 | + Returns: |
| 206 | + Structure: The magmom-decorated structure that can be passed to get VASP input files, e.g. |
| 207 | + get_kpoints(). |
| 208 | + """ |
| 209 | + structure = vasprun.final_structure |
| 210 | + |
| 211 | + site_properties = {} |
| 212 | + # magmom |
| 213 | + if vasprun.is_spin: |
| 214 | + if outcar and outcar.magnetization: |
| 215 | + site_properties["magmom"] = [i["tot"] for i in outcar.magnetization] |
| 216 | + else: |
| 217 | + site_properties["magmom"] = vasprun.parameters["MAGMOM"] |
| 218 | + # LDAU |
| 219 | + if vasprun.parameters.get("LDAU", False): |
| 220 | + for key in ("LDAUU", "LDAUJ", "LDAUL"): |
| 221 | + vals = vasprun.incar[key] |
| 222 | + m = {} |
| 223 | + l_val = [] |
| 224 | + s = 0 |
| 225 | + for site in structure: |
| 226 | + if site.specie.symbol not in m: |
| 227 | + m[site.specie.symbol] = vals[s] |
| 228 | + s += 1 |
| 229 | + l_val.append(m[site.specie.symbol]) |
| 230 | + if len(l_val) == len(structure): |
| 231 | + site_properties.update({key.lower(): l_val}) |
| 232 | + else: |
| 233 | + raise ValueError(f"length of list {l_val} not the same as structure") |
| 234 | + |
| 235 | + return structure.copy(site_properties=site_properties) |
| 236 | + |
| 237 | + |
| 238 | +def standardize_structure(structure, sym_prec=0.1, international_monoclinic=True): |
| 239 | + """Get the symmetrically standardized structure. |
| 240 | +
|
| 241 | + Args: |
| 242 | + structure (Structure): The structure. |
| 243 | + sym_prec (float): Tolerance for symmetry finding for standardization. |
| 244 | + international_monoclinic (bool): Whether to use international |
| 245 | + convention (vs Curtarolo) for monoclinic. Defaults True. |
| 246 | +
|
| 247 | + Returns: |
| 248 | + The symmetrized structure. |
| 249 | + """ |
| 250 | + sym_finder = SpacegroupAnalyzer(structure, symprec=sym_prec) |
| 251 | + new_structure = sym_finder.get_primitive_standard_structure(international_monoclinic=international_monoclinic) |
| 252 | + |
| 253 | + # the primitive structure finding has had several bugs in the past |
| 254 | + # defend through validation |
| 255 | + vpa_old = structure.volume / len(structure) |
| 256 | + vpa_new = new_structure.volume / len(new_structure) |
| 257 | + |
| 258 | + if abs(vpa_old - vpa_new) / vpa_old > 0.02: |
| 259 | + raise ValueError(f"Standardizing cell failed! VPA old: {vpa_old}, VPA new: {vpa_new}") |
| 260 | + |
| 261 | + matcher = StructureMatcher() |
| 262 | + if not matcher.fit(structure, new_structure): |
| 263 | + raise ValueError("Standardizing cell failed! Old structure doesn't match new.") |
| 264 | + |
| 265 | + return new_structure |
| 266 | + |
| 267 | + |
| 268 | +def get_vasprun_outcar(path: str | Path, parse_dos: bool = True, parse_eigen: bool = True) -> tuple[Vasprun, Outcar]: |
| 269 | + """Get a Vasprun and Outcar from a directory. |
| 270 | +
|
| 271 | + Args: |
| 272 | + path: Path to get the vasprun.xml and OUTCAR. |
| 273 | + parse_dos: Whether to parse dos. Defaults to True. |
| 274 | + parse_eigen: Whether to parse eigenvalue. Defaults to True. |
| 275 | +
|
| 276 | + Returns: |
| 277 | + Vasprun and Outcar files. |
| 278 | + """ |
| 279 | + path = Path(path) |
| 280 | + vruns = list(glob(str(path / "vasprun.xml*"))) |
| 281 | + outcars = list(glob(str(path / "OUTCAR*"))) |
| 282 | + |
| 283 | + if len(vruns) == 0 or len(outcars) == 0: |
| 284 | + raise ValueError(f"Unable to get vasprun.xml/OUTCAR from prev calculation in {path}") |
| 285 | + vs_file_fullpath = str(path / "vasprun.xml") |
| 286 | + outcar_fullpath = str(path / "OUTCAR.gz") |
| 287 | + vs_file = vs_file_fullpath if vs_file_fullpath in vruns else max(vruns) |
| 288 | + outcar_file = outcar_fullpath if outcar_fullpath in outcars else max(outcars) |
| 289 | + return ( |
| 290 | + Vasprun(vs_file, parse_dos=parse_dos, parse_eigen=parse_eigen), |
| 291 | + Outcar(outcar_file), |
| 292 | + ) |
0 commit comments