From bd13ea46ce150cd71a013c07c9d6346a1fccfd63 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Thu, 26 Oct 2023 12:58:47 -0700 Subject: [PATCH 1/4] Breaking: return sum of `Species` with matching `Element` in `Composition.__getitem__` (#3427) * don't rename ElementTree import * return sum of Species with matching element in Composition.__getitem__ fixes MP2020Compatibility not applying anion correction when passing in ComputedEntry with oxidation states * test_composition.py add test_getitem * improve coverage in test_process_entry_with_oxidation_state with 2nd example for ComputedStructureEntry --- .pre-commit-config.yaml | 2 +- pymatgen/core/composition.py | 9 +++- tests/core/test_composition.py | 23 ++++++---- tests/core/test_sites.py | 6 +-- tests/entries/test_compatibility.py | 42 ++++++++++++++++--- tests/io/exciting/test_inputs.py | 8 ++-- .../test_standard_transformations.py | 17 ++------ 7 files changed, 70 insertions(+), 37 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81d34482237..040973104e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.2 + rev: v0.1.3 hooks: - id: ruff args: [--fix] diff --git a/pymatgen/core/composition.py b/pymatgen/core/composition.py index e0a13b26568..2ef6b9be88d 100644 --- a/pymatgen/core/composition.py +++ b/pymatgen/core/composition.py @@ -138,7 +138,12 @@ def __init__(self, *args, strict: bool = False, **kwargs) -> None: def __getitem__(self, key: SpeciesLike) -> float: try: sp = get_el_sp(key) - return self._data.get(sp, 0) + if isinstance(sp, Species): + return self._data.get(sp, 0) + # sp is Element or str + return sum( + val for key, val in self._data.items() if getattr(key, "symbol", key) == getattr(sp, "symbol", sp) + ) except ValueError as exc: raise KeyError(f"Invalid {key=}") from exc @@ -153,7 +158,7 @@ def __contains__(self, key) -> bool: sp = get_el_sp(key) if isinstance(sp, Species): return sp in self._data - # Element or str + # key is Element or str return any(sp.symbol == s.symbol for s in self._data) except ValueError as exc: raise TypeError(f"Invalid {key=} for Composition") from exc diff --git a/tests/core/test_composition.py b/tests/core/test_composition.py index 66ba6318e7e..d322225417e 100644 --- a/tests/core/test_composition.py +++ b/tests/core/test_composition.py @@ -105,6 +105,15 @@ def test_in(self): assert Element("Fe") not in comp assert Species("Fe2+") not in comp + def test_getitem(self): + comp = Composition({"Li+": 1, "Mn3+": 2, "O2-": 4, "Li": 1}) + assert comp["Li"] == 2 + assert comp["Li+"] == 1 + assert comp["Mn3+"] == 2 + assert comp["Mn"] == 2 + assert comp["O2-"] == 4 + assert comp["O"] == 4 + def test_hill_formula(self): c = Composition("CaCO3") assert c.hill_formula == "C Ca O3" @@ -269,8 +278,8 @@ def test_reduced_formula(self): assert Composition("H6CN").get_integer_formula_and_factor(iupac_ordering=True)[0] == "CNH6" # test rounding - c = Composition({"Na": 2 - Composition.amount_tolerance / 2, "Cl": 2}) - assert c.reduced_formula == "NaCl" + comp = Composition({"Na": 2 - Composition.amount_tolerance / 2, "Cl": 2}) + assert comp.reduced_formula == "NaCl" def test_integer_formula(self): correct_reduced_formulas = [ @@ -299,8 +308,8 @@ def test_integer_formula(self): def test_num_atoms(self): correct_num_atoms = [20, 10, 7, 8, 20, 75, 2, 3] - all_natoms = [c.num_atoms for c in self.comps] - assert all_natoms == correct_num_atoms + all_n_atoms = [c.num_atoms for c in self.comps] + assert all_n_atoms == correct_num_atoms def test_weight(self): correct_weights = [ @@ -360,7 +369,7 @@ def test_from_weight_dict(self): for el in c1.elements: assert c1[el] == approx(c2[el], abs=1e-3) - def test_tofrom_weight_dict(self): + def test_to_from_weight_dict(self): for comp in self.comps: c2 = Composition().from_weight_dict(comp.to_weight_dict) comp.almost_equals(c2) @@ -520,8 +529,8 @@ def test_negative_compositions(self): # test species c1 = Composition({"Mg": 1, "Mg2+": -1}, allow_negative=True) assert c1.num_atoms == 2 - assert c1.element_composition == Composition() - assert c1.average_electroneg == 1.31 + assert c1.element_composition == Composition("Mg-1", allow_negative=True) + assert c1.average_electroneg == 0.655 def test_special_formulas(self): special_formulas = { diff --git a/tests/core/test_sites.py b/tests/core/test_sites.py index 7f2af260d49..340e84b7d9d 100644 --- a/tests/core/test_sites.py +++ b/tests/core/test_sites.py @@ -141,9 +141,9 @@ def test_distance_and_image(self): dist_old, jimage_old = get_distance_and_image_old(site1, site2) dist_new, jimage_new = site1.distance_and_image(site2) assert dist_old - dist_new > -1e-8, "New distance algo should give smaller answers!" - assert not (abs(dist_old - dist_new) < 1e-8) ^ ( - jimage_old == jimage_new - ).all(), "If old dist == new dist, images must be the same!" + assert ( + not (abs(dist_old - dist_new) < 1e-8) ^ (jimage_old == jimage_new).all() + ), "If old dist == new dist, images must be the same!" latt = Lattice.from_parameters(3.0, 3.1, 10.0, 2.96, 2.0, 1.0) site = PeriodicSite("Fe", [0.1, 0.1, 0.1], latt) site2 = PeriodicSite("Fe", [0.99, 0.99, 0.99], latt) diff --git a/tests/entries/test_compatibility.py b/tests/entries/test_compatibility.py index 01eebb42ce3..6b2f01ce945 100644 --- a/tests/entries/test_compatibility.py +++ b/tests/entries/test_compatibility.py @@ -1000,18 +1000,48 @@ def test_check_potcar(self): def test_process_entry_with_oxidation_state(self): from pymatgen.core.periodic_table import Species - entry = ComputedEntry( - {Species("Fe2+"): 2, Species("O2-"): 3}, - -1, - parameters={"is_hubbard": True, "hubbards": {"Fe": 5.3, "O": 0}, "run_type": "GGA+U"}, - ) + params = {"is_hubbard": True, "hubbards": {"Fe": 5.3, "O": 0}, "run_type": "GGA+U"} + entry = ComputedEntry({Species("Fe2+"): 2, Species("O2-"): 3}, -1, parameters=params) # Test that MaterialsProject2020Compatibility can process entries with oxidation states # https://github.com/materialsproject/pymatgen/issues/3154 compat = MaterialsProject2020Compatibility(check_potcar=False) - [processed_entry] = compat.process_entries(entry, clean=True, inplace=False) + processed_entry = compat.process_entry(entry, clean=True, inplace=False) + + assert len(processed_entry.energy_adjustments) == 2 + assert processed_entry.energy_adjustments[0].name == "MP2020 anion correction (oxide)" + assert processed_entry.energy_adjustments[1].name == "MP2020 GGA/GGA+U mixing correction (Fe)" + assert processed_entry.correction == approx(-6.572999) + assert processed_entry.energy == approx(-1 + -6.572999) + + # for https://github.com/materialsproject/pymatgen/issues/3425 + frac_coords = [ + [0.5, 0.5, 0.3797505], + [0.0, 0.0, 0.6202495], + [0.5, 0.5, 0.8632525], + [0.0, 0.0, 0.1367475], + [0.5, 0.0, 0.3608245], + [0.0, 0.5, 0.0985135], + [0.5, 0.0, 0.9014865], + [0.0, 0.5, 0.6391755], + ] + lattice = [ + [2.86877900, 0.00000000e00, 1.75662051e-16], + [-2.83779749e-16, 4.63447500e00, 2.83779749e-16], + [0.00000000e00, 0.00000000e00, 5.83250700e00], + ] + species = ["Li+", "Li+", "Mn3+", "Mn3+", "O2-", "O2-", "O2-", "O2-"] + li_mn_o = Structure(lattice, species, frac_coords) + + params = {"hubbards": {"Mn": 3.9, "O": 0, "Li": 0}, "run_type": "GGA+U"} + cse = ComputedStructureEntry(li_mn_o, -58.97, parameters=params) + processed_entry = compat.process_entry(cse, clean=True, inplace=False) assert len(processed_entry.energy_adjustments) == 2 + assert processed_entry.energy_adjustments[0].name == "MP2020 anion correction (oxide)" + assert processed_entry.energy_adjustments[1].name == "MP2020 GGA/GGA+U mixing correction (Mn)" + assert processed_entry.correction == approx(-6.084) + assert processed_entry.energy == approx(-58.97 + -6.084) class TestMITCompatibility(unittest.TestCase): diff --git a/tests/io/exciting/test_inputs.py b/tests/io/exciting/test_inputs.py index fcea5fc6217..d00ab4f5e2d 100644 --- a/tests/io/exciting/test_inputs.py +++ b/tests/io/exciting/test_inputs.py @@ -1,6 +1,6 @@ from __future__ import annotations -import xml.etree.ElementTree as ET +from xml.etree import ElementTree from numpy.testing import assert_allclose @@ -121,7 +121,7 @@ def test_writebandstr(self): "S", "R", ] - root = ET.fromstring(bandstr) + root = ElementTree.fromstring(bandstr) for plot1d in root.iter("plot1d"): for point in plot1d.iter("point"): coord.append([float(i) for i in point.get("coord").split()]) @@ -159,8 +159,8 @@ def test_paramdict(self): # read reference file filepath = f"{TEST_FILES_DIR}/input_exciting2.xml" - tree = ET.parse(filepath) + tree = ElementTree.parse(filepath) root = tree.getroot() - ref_string = ET.tostring(root, encoding="unicode") + ref_string = ElementTree.tostring(root, encoding="unicode") assert ref_string.strip() == test_string.strip() diff --git a/tests/transformations/test_standard_transformations.py b/tests/transformations/test_standard_transformations.py index 66372b7497c..5a7f6bbc8b9 100644 --- a/tests/transformations/test_standard_transformations.py +++ b/tests/transformations/test_standard_transformations.py @@ -338,21 +338,10 @@ def test_no_oxidation(self): def test_symmetrized_structure(self): trafo = OrderDisorderedStructureTransformation(symmetrized_structures=True) - c = [] - sp = [] - c.append([0.5, 0.5, 0.5]) - sp.append("Si4+") - c.append([0.45, 0.45, 0.45]) - sp.append({"Si4+": 0.5}) - c.append([0.56, 0.56, 0.56]) - sp.append({"Si4+": 0.5}) - c.append([0.25, 0.75, 0.75]) - sp.append({"Si4+": 0.5}) - c.append([0.75, 0.25, 0.25]) - sp.append({"Si4+": 0.5}) latt = Lattice.cubic(5) - struct = Structure(latt, sp, c) - test_site = PeriodicSite("Si4+", c[2], latt) + coords = [[0.5, 0.5, 0.5], [0.45, 0.45, 0.45], [0.56, 0.56, 0.56], [0.25, 0.75, 0.75], [0.75, 0.25, 0.25]] + struct = Structure(latt, [{"Si4+": 1}, *[{"Si4+": 0.5}] * 4], coords) + test_site = PeriodicSite("Si4+", coords[2], latt) struct = SymmetrizedStructure(struct, "not_real", [0, 1, 1, 2, 2], ["a", "b", "b", "c", "c"]) output = trafo.apply_transformation(struct) assert test_site in output From 368f51c0a363e28e219b2e33024055d0cf88ef98 Mon Sep 17 00:00:00 2001 From: Guido Petretto Date: Fri, 27 Oct 2023 18:50:59 +0200 Subject: [PATCH 2/4] Add lattice velocities to Poscar (#3428) * Add lattice velocities to Poscar * use auto-cleaned PymatgenTest.tmp_path over tmp_file.unlink() --------- Co-authored-by: Janosh Riebesell --- pymatgen/io/vasp/inputs.py | 37 +++++++++++++++++++++- tests/files/CONTCAR.MD.npt | 61 ++++++++++++++++++++++++++++++++++++ tests/io/vasp/test_inputs.py | 40 +++++++++++++++++------ 3 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 tests/files/CONTCAR.MD.npt diff --git a/pymatgen/io/vasp/inputs.py b/pymatgen/io/vasp/inputs.py index 469ae4877b7..ae120378821 100644 --- a/pymatgen/io/vasp/inputs.py +++ b/pymatgen/io/vasp/inputs.py @@ -89,6 +89,7 @@ def __init__( velocities: ArrayLike | None = None, predictor_corrector: ArrayLike | None = None, predictor_corrector_preamble: str | None = None, + lattice_velocities: ArrayLike | None = None, sort_structure: bool = False, ): """ @@ -108,6 +109,8 @@ def __init__( Typically parsed in MD runs. Defaults to None. predictor_corrector_preamble (str | None, optional): Preamble to the predictor corrector. Defaults to None. + lattice_velocities (ArrayLike | None, optional): Lattice velocities and current + lattice for the POSCAR. Available in MD runs with variable cell. Defaults to None. sort_structure (bool, optional): Whether to sort the structure. Useful if species are not grouped properly together. Defaults to False. """ @@ -137,6 +140,9 @@ def __init__( self.comment = structure.formula if comment is None else comment if predictor_corrector_preamble: self.structure.properties["predictor_corrector_preamble"] = predictor_corrector_preamble + + if lattice_velocities is not None and np.any(lattice_velocities): + self.structure.properties["lattice_velocities"] = np.asarray(lattice_velocities) else: raise ValueError("Disordered structure with partial occupancies cannot be converted into POSCAR!") @@ -162,6 +168,11 @@ def predictor_corrector_preamble(self): """Predictor corrector preamble in Poscar.""" return self.structure.properties.get("predictor_corrector_preamble") + @property + def lattice_velocities(self): + """Lattice velocities in Poscar (including the current lattice vectors).""" + return self.structure.properties.get("lattice_velocities") + @velocities.setter # type: ignore def velocities(self, velocities): """Setter for Poscar.velocities.""" @@ -182,6 +193,11 @@ def predictor_corrector_preamble(self, predictor_corrector_preamble): """Setter for Poscar.predictor_corrector.""" self.structure.properties["predictor_corrector"] = predictor_corrector_preamble + @lattice_velocities.setter # type: ignore + def lattice_velocities(self, lattice_velocities: ArrayLike) -> None: + """Setter for Poscar.lattice_velocities.""" + self.structure.properties["lattice_velocities"] = np.asarray(lattice_velocities) + @property def site_symbols(self) -> list[str]: """ @@ -422,6 +438,15 @@ def from_str(data, default_names=None, read_velocities=True): ) if read_velocities: + # Parse the lattice velocities and current lattice, if present. + # The header line should contain "Lattice velocities and vectors" + # There is no space between the coordinates and this section, so + # it appears in the lines of the first chunk + lattice_velocities = [] + if len(lines) > ipos + n_sites + 1 and lines[ipos + n_sites + 1].lower().startswith("l"): + for line in lines[ipos + n_sites + 3 : ipos + n_sites + 9]: + lattice_velocities.append([float(tok) for tok in line.split()]) + # Parse velocities if any velocities = [] if len(chunks) > 1: @@ -450,7 +475,7 @@ def from_str(data, default_names=None, read_velocities=True): d3 = [float(tok) for tok in lines[st + 2 * n_sites].split()] predictor_corrector.append([d1, d2, d3]) else: - velocities = predictor_corrector = predictor_corrector_preamble = None + velocities = predictor_corrector = predictor_corrector_preamble = lattice_velocities = None return Poscar( struct, @@ -460,6 +485,7 @@ def from_str(data, default_names=None, read_velocities=True): velocities=velocities, predictor_corrector=predictor_corrector, predictor_corrector_preamble=predictor_corrector_preamble, + lattice_velocities=lattice_velocities, ) @np.deprecate(message="Use get_str instead") @@ -514,6 +540,15 @@ def get_str(self, direct: bool = True, vasp4_compatible: bool = False, significa line += " " + site.species_string lines.append(line) + if self.lattice_velocities is not None: + try: + lines.append("Lattice velocities and vectors") + lines.append(" 1") + for v in self.lattice_velocities: + lines.append(" ".join(format_str.format(i) for i in v)) + except Exception: + warnings.warn("Lattice velocities are missing or corrupted.") + if self.velocities: try: lines.append("") diff --git a/tests/files/CONTCAR.MD.npt b/tests/files/CONTCAR.MD.npt new file mode 100644 index 00000000000..3eb31871ba9 --- /dev/null +++ b/tests/files/CONTCAR.MD.npt @@ -0,0 +1,61 @@ +Si8 + 1.00000000000000 + 5.6029196758839159 -0.0603693499677626 0.1110429263825206 + -0.0000000000000100 5.3747130151091689 0.0034014855406440 + 0.0000000000000068 -0.0000000000000120 5.4692974031883859 + Si + 8 +Direct + 0.6539918994283490 0.8969172295968553 0.0676681342693018 + -0.2312437400904284 0.5904002014657854 0.3939526918410268 + 0.8235113603041605 0.3274668347998835 0.7376879470910200 + 0.0234540394811329 -0.0006436675535249 -0.1173920341194730 + 0.4494229071161935 0.8354718770893421 0.6950789454078377 + 0.5785762144444008 0.4746645739302746 0.0005135261381124 + 0.0419959757024323 0.1291469345761019 0.3339785656099613 + 0.5094723570400191 0.1350290791632852 0.4745741352855470 +Lattice velocities and vectors + 1 + 0.11376865E-02 -0.20054010E-02 0.10745440E-02 + -0.80980926E-03 -0.54988058E-03 -0.11593411E-02 + 0.40755213E-03 -0.91838934E-03 0.10978311E-02 + 0.56062799E+01 -0.68862342E-01 0.11555075E+00 + -0.98606785E-14 0.53730639E+01 -0.27835157E-02 + 0.64062547E-14 -0.12698742E-13 0.54725901E+01 + + -0.26486997E-01 0.15289665E-01 -0.24183306E-01 + -0.21835373E-01 -0.48466524E-02 0.34963571E-02 + -0.36389021E-02 -0.28571877E-02 -0.18926241E-02 + 0.49389009E-02 0.28594559E-02 -0.48140896E-02 + 0.20044174E-01 -0.32290274E-03 0.67317795E-02 + 0.51447245E-02 0.75356806E-02 -0.13128815E-01 + -0.12148614E-01 -0.19846176E-01 0.16721524E-01 + -0.73911890E-02 0.29807035E-02 -0.42831313E-02 + + 1 + 3.00000000000000 + 0.10000000E+01 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.63981833E+00 0.90527242E+00 0.54714689E-01 + 0.75707184E+00 0.58754437E+00 0.39611461E+00 + 0.82156413E+00 0.32584659E+00 0.73669073E+00 + 0.26096915E-01 0.98675463E-03 0.87991397E+00 + 0.46014883E+00 0.83542905E+00 0.69854272E+00 + 0.58132923E+00 0.47890733E+00 0.99326052E+00 + 0.35495080E-01 0.11798269E+00 0.34327666E+00 + 0.50551723E+00 0.13664264E+00 0.47231051E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 + 0.00000000E+00 0.00000000E+00 0.00000000E+00 diff --git a/tests/io/vasp/test_inputs.py b/tests/io/vasp/test_inputs.py index 99130d87901..5ff0ef03bbd 100644 --- a/tests/io/vasp/test_inputs.py +++ b/tests/io/vasp/test_inputs.py @@ -4,7 +4,7 @@ import os import pickle import unittest -from pathlib import Path +from typing import TYPE_CHECKING import numpy as np import pytest @@ -32,6 +32,9 @@ ) from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest +if TYPE_CHECKING: + from pathlib import Path + class TestPoscar(PymatgenTest): def test_init(self): @@ -265,16 +268,34 @@ def test_str(self): def test_from_md_run(self): # Parsing from an MD type run with velocities and predictor corrector data poscar = Poscar.from_file(f"{TEST_FILES_DIR}/CONTCAR.MD", check_for_potcar=False) - assert np.sum(np.array(poscar.velocities)) == approx(0.0065417961324) + assert np.sum(poscar.velocities) == approx(0.0065417961324) assert poscar.predictor_corrector[0][0][0] == 0.33387820e00 assert poscar.predictor_corrector[0][1][1] == -0.10583589e-02 + assert poscar.lattice_velocities is None + + # Parsing from an MD type run with velocities, predictor corrector data and lattice velocities + poscar = Poscar.from_file(f"{TEST_FILES_DIR}/CONTCAR.MD.npt", check_for_potcar=False) + assert np.sum(poscar.velocities) == approx(-0.06193299494) + assert poscar.predictor_corrector[0][0][0] == 0.63981833 + assert poscar.lattice_velocities.sum() == approx(16.49411358474) def test_write_md_poscar(self): # Parsing from an MD type run with velocities and predictor corrector data # And writing a new POSCAR from the new structure poscar = Poscar.from_file(f"{TEST_FILES_DIR}/CONTCAR.MD", check_for_potcar=False) - path = Path("POSCAR.testing.md") + path = f"{self.tmp_path}/POSCAR.testing.md" + poscar.write_file(path) + p3 = Poscar.from_file(path) + + assert_allclose(poscar.structure.lattice.abc, p3.structure.lattice.abc, 5) + assert_allclose(poscar.velocities, p3.velocities, 5) + assert_allclose(poscar.predictor_corrector, p3.predictor_corrector, 5) + assert poscar.predictor_corrector_preamble == p3.predictor_corrector_preamble + + # Same as above except also has lattice velocities + poscar = Poscar.from_file(f"{TEST_FILES_DIR}/CONTCAR.MD.npt", check_for_potcar=False) + poscar.write_file(path) p3 = Poscar.from_file(path) @@ -282,7 +303,7 @@ def test_write_md_poscar(self): assert_allclose(poscar.velocities, p3.velocities, 5) assert_allclose(poscar.predictor_corrector, p3.predictor_corrector, 5) assert poscar.predictor_corrector_preamble == p3.predictor_corrector_preamble - path.unlink() + assert_allclose(poscar.lattice_velocities, p3.lattice_velocities, 5) def test_setattr(self): filepath = f"{TEST_FILES_DIR}/POSCAR" @@ -395,7 +416,7 @@ def test_selective_dynamics(self): ] -class TestIncar(unittest.TestCase): +class TestIncar(PymatgenTest): def setUp(self): file_name = f"{TEST_FILES_DIR}/INCAR" self.incar = Incar.from_file(file_name) @@ -564,11 +585,10 @@ def test_as_dict_and_from_dict(self): assert incar3["MAGMOM"] == [Magmom([1, 2, 3])] def test_write(self): - tempfname = Path("INCAR.testing") - self.incar.write_file(tempfname) - i = Incar.from_file(tempfname) - assert i == self.incar - tempfname.unlink() + tmp_file = f"{self.tmp_path}/INCAR.testing" + self.incar.write_file(tmp_file) + incar = Incar.from_file(tmp_file) + assert incar == self.incar def test_get_str(self): s = self.incar.get_str(pretty=True, sort_keys=True) From 9ae6121e7f1e0f91cb56b5be872f02e84f82a85e Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 27 Oct 2023 12:01:28 -0700 Subject: [PATCH 3/4] make from_str staticmethods into classmethods (#3429) --- dev_scripts/potcar_scrambler.py | 6 +- pymatgen/analysis/reaction_calculator.py | 4 +- pymatgen/core/periodic_table.py | 16 ++-- pymatgen/electronic_structure/boltztrap.py | 24 +++--- pymatgen/io/adf.py | 55 +++++++------ pymatgen/io/babel.py | 14 ++-- pymatgen/io/cif.py | 6 +- pymatgen/io/cp2k/inputs.py | 20 ++--- pymatgen/io/cssr.py | 12 +-- pymatgen/io/exciting/inputs.py | 16 ++-- pymatgen/io/feff/inputs.py | 20 ++--- pymatgen/io/feff/outputs.py | 18 ++--- pymatgen/io/gaussian.py | 16 ++-- pymatgen/io/pwscf.py | 12 +-- pymatgen/io/qchem/inputs.py | 6 +- pymatgen/io/vasp/inputs.py | 78 +++++++++---------- pymatgen/io/vasp/outputs.py | 6 +- pymatgen/io/wannier90.py | 8 +- pymatgen/io/xr.py | 12 +-- pymatgen/io/xyz.py | 12 +-- pymatgen/io/zeopp.py | 30 +++---- pymatgen/phonon/thermal_displacements.py | 17 +--- .../standard_transformations.py | 8 +- tests/io/test_adf.py | 10 +-- 24 files changed, 206 insertions(+), 220 deletions(-) diff --git a/dev_scripts/potcar_scrambler.py b/dev_scripts/potcar_scrambler.py index 37c586c670d..358cdc6d799 100644 --- a/dev_scripts/potcar_scrambler.py +++ b/dev_scripts/potcar_scrambler.py @@ -95,10 +95,10 @@ def to_file(self, filename: str): with zopen(filename, "wt") as f: f.write(self.scrambled_potcars_str) - @staticmethod - def from_file(input_filename: str, output_filename: str | None = None): + @classmethod + def from_file(cls, input_filename: str, output_filename: str | None = None): psp = Potcar.from_file(input_filename) - psp_scrambled = PotcarScrambler(psp) + psp_scrambled = cls(psp) if output_filename: psp_scrambled.to_file(output_filename) return psp_scrambled diff --git a/pymatgen/analysis/reaction_calculator.py b/pymatgen/analysis/reaction_calculator.py index 2cd65289e49..885720c75a9 100644 --- a/pymatgen/analysis/reaction_calculator.py +++ b/pymatgen/analysis/reaction_calculator.py @@ -249,8 +249,8 @@ def from_dict(cls, d): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(rxn_str): + @classmethod + def from_str(cls, rxn_str): """ Generates a balanced reaction from a string. The reaction must already be balanced. diff --git a/pymatgen/core/periodic_table.py b/pymatgen/core/periodic_table.py index b58e129b49e..66e825a871b 100644 --- a/pymatgen/core/periodic_table.py +++ b/pymatgen/core/periodic_table.py @@ -1011,8 +1011,8 @@ def from_string(cls, *args, **kwargs): """Use from_str instead.""" return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(species_string: str) -> Species: + @classmethod + def from_str(cls, species_string: str) -> Species: """Returns a Species from a string representation. Args: @@ -1051,10 +1051,10 @@ def from_str(species_string: str) -> Species: # but we need either an oxidation state or a property if oxi is None and properties == {}: - raise ValueError("Invalid Species String") + raise ValueError("Invalid species string") - return Species(sym, 0 if oxi is None else oxi, **properties) - raise ValueError("Invalid Species String") + return cls(sym, 0 if oxi is None else oxi, **properties) + raise ValueError("Invalid species string") def __repr__(self): return f"Species {self}" @@ -1294,8 +1294,8 @@ def symbol(self) -> str: def __deepcopy__(self, memo): return DummySpecies(self.symbol, self._oxi_state) - @staticmethod - def from_str(species_string: str) -> DummySpecies: + @classmethod + def from_str(cls, species_string: str) -> DummySpecies: """Returns a Dummy from a string representation. Args: @@ -1320,7 +1320,7 @@ def from_str(species_string: str) -> DummySpecies: if m.group(4): # has Spin property tokens = m.group(4).split("=") properties = {tokens[0]: float(tokens[1])} - return DummySpecies(sym, oxi, **properties) + return cls(sym, oxi, **properties) raise ValueError("Invalid DummySpecies String") def as_dict(self) -> dict: diff --git a/pymatgen/electronic_structure/boltztrap.py b/pymatgen/electronic_structure/boltztrap.py index 3885d33ae06..7934e005b8d 100644 --- a/pymatgen/electronic_structure/boltztrap.py +++ b/pymatgen/electronic_structure/boltztrap.py @@ -1924,8 +1924,8 @@ def parse_cond_and_hall(path_dir, doping_levels=None): carrier_conc, ) - @staticmethod - def from_files(path_dir, dos_spin=1): + @classmethod + def from_files(cls, path_dir, dos_spin=1): """Get a BoltztrapAnalyzer object from a set of files. Args: @@ -1935,29 +1935,29 @@ def from_files(path_dir, dos_spin=1): Returns: a BoltztrapAnalyzer object """ - run_type, warning, efermi, gap, doping_levels = BoltztrapAnalyzer.parse_outputtrans(path_dir) + run_type, warning, efermi, gap, doping_levels = cls.parse_outputtrans(path_dir) - vol = BoltztrapAnalyzer.parse_struct(path_dir) + vol = cls.parse_struct(path_dir) - intrans = BoltztrapAnalyzer.parse_intrans(path_dir) + intrans = cls.parse_intrans(path_dir) if run_type == "BOLTZ": - dos, pdos = BoltztrapAnalyzer.parse_transdos(path_dir, efermi, dos_spin=dos_spin, trim_dos=False) + dos, pdos = cls.parse_transdos(path_dir, efermi, dos_spin=dos_spin, trim_dos=False) - *cond_and_hall, carrier_conc = BoltztrapAnalyzer.parse_cond_and_hall(path_dir, doping_levels) + *cond_and_hall, carrier_conc = cls.parse_cond_and_hall(path_dir, doping_levels) - return BoltztrapAnalyzer(gap, *cond_and_hall, intrans, dos, pdos, carrier_conc, vol, warning) + return cls(gap, *cond_and_hall, intrans, dos, pdos, carrier_conc, vol, warning) if run_type == "DOS": trim = intrans["dos_type"] == "HISTO" - dos, pdos = BoltztrapAnalyzer.parse_transdos(path_dir, efermi, dos_spin=dos_spin, trim_dos=trim) + dos, pdos = cls.parse_transdos(path_dir, efermi, dos_spin=dos_spin, trim_dos=trim) - return BoltztrapAnalyzer(gap=gap, dos=dos, dos_partial=pdos, warning=warning, vol=vol) + return cls(gap=gap, dos=dos, dos_partial=pdos, warning=warning, vol=vol) if run_type == "BANDS": bz_kpoints = np.loadtxt(f"{path_dir}/boltztrap_band.dat")[:, -3:] bz_bands = np.loadtxt(f"{path_dir}/boltztrap_band.dat")[:, 1:-6] - return BoltztrapAnalyzer(bz_bands=bz_bands, bz_kpoints=bz_kpoints, warning=warning, vol=vol) + return cls(bz_bands=bz_bands, bz_kpoints=bz_kpoints, warning=warning, vol=vol) if run_type == "FERMI": if os.path.exists(f"{path_dir}/boltztrap_BZ.cube"): @@ -1966,7 +1966,7 @@ def from_files(path_dir, dos_spin=1): fs_data = read_cube_file(f"{path_dir}/fort.30") else: raise BoltztrapError("No data file found for fermi surface") - return BoltztrapAnalyzer(fermi_surface_data=fs_data) + return cls(fermi_surface_data=fs_data) raise ValueError(f"{run_type=} not recognized!") diff --git a/pymatgen/io/adf.py b/pymatgen/io/adf.py index d87c35d63bd..e0938ce4505 100644 --- a/pymatgen/io/adf.py +++ b/pymatgen/io/adf.py @@ -44,25 +44,6 @@ def is_numeric(s) -> bool: return True -def iterlines(s: str) -> Generator[str, None, None]: - r"""A generator form of s.split('\n') for reducing memory overhead. - - Args: - s (str): A multi-line string. - - Yields: - str: line - """ - prevnl = -1 - while True: - nextnl = s.find("\n", prevnl + 1) - if nextnl < 0: - yield s[(prevnl + 1) :] - break - yield s[(prevnl + 1) : nextnl] - prevnl = nextnl - - class AdfInputError(Exception): """The default error class for ADF.""" @@ -362,8 +343,8 @@ def from_dict(cls, d): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(string): + @classmethod + def from_str(cls, string: str) -> AdfKey: """ Construct an AdfKey object from the string. @@ -395,18 +376,36 @@ def is_float(s) -> bool: el = string.split() if len(el) > 1: options = [s.split("=") for s in el[1:]] if string.find("=") != -1 else el[1:] - for i, op in enumerate(options): + for idx, op in enumerate(options): # type: ignore[var-annotated, arg-type] if isinstance(op, list) and is_numeric(op[1]): op[1] = float(op[1]) if is_float(op[1]) else int(op[1]) elif is_numeric(op): - options[i] = float(op) if is_float(op) else int(op) + options[idx] = float(op) if is_float(op) else int(op) # type: ignore[index] else: options = None - return AdfKey(el[0], options) + return cls(el[0], options) if string.find("subend") != -1: raise ValueError("Nested subkeys are not supported!") + def iterlines(s: str) -> Generator[str, None, None]: + r"""A generator form of s.split('\n') for reducing memory overhead. + + Args: + s (str): A multi-line string. + + Yields: + str: line + """ + prev_nl = -1 + while True: + next_nl = s.find("\n", prev_nl + 1) + if next_nl < 0: + yield s[(prev_nl + 1) :] + break + yield s[(prev_nl + 1) : next_nl] + prev_nl = next_nl + key = None for line in iterlines(string): if line == "": @@ -414,15 +413,15 @@ def is_float(s) -> bool: el = line.strip().split() if len(el) == 0: continue - if el[0].upper() in AdfKey.block_keys: + if el[0].upper() in cls.block_keys: if key is None: - key = AdfKey.from_str(line) + key = cls.from_str(line) else: return key elif el[0].upper() == "END": - return key + return key # type: ignore[return-value] elif key is not None: - key.add_subkey(AdfKey.from_str(line)) + key.add_subkey(cls.from_str(line)) raise Exception("IncompleteKey: 'END' is missing!") diff --git a/pymatgen/io/babel.py b/pymatgen/io/babel.py index 423653e62dd..d58369ba47b 100644 --- a/pymatgen/io/babel.py +++ b/pymatgen/io/babel.py @@ -305,8 +305,8 @@ def write_file(self, filename, file_format="xyz"): mol = pybel.Molecule(self._ob_mol) return mol.write(file_format, filename, overwrite=True) - @staticmethod - def from_file(filename, file_format="xyz", return_all_molecules=False): + @classmethod + def from_file(cls, filename, file_format="xyz", return_all_molecules=False): """ Uses OpenBabel to read a molecule from a file in all supported formats. @@ -322,9 +322,9 @@ def from_file(filename, file_format="xyz", return_all_molecules=False): """ mols = pybel.readfile(str(file_format), str(filename)) if return_all_molecules: - return [BabelMolAdaptor(mol.OBMol) for mol in mols] + return [cls(mol.OBMol) for mol in mols] - return BabelMolAdaptor(next(mols).OBMol) + return cls(next(mols).OBMol) @staticmethod def from_molecule_graph(mol): @@ -345,8 +345,8 @@ def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) @needs_openbabel - @staticmethod - def from_str(string_data, file_format="xyz"): + @classmethod + def from_str(cls, string_data, file_format="xyz"): """ Uses OpenBabel to read a molecule from a string in all supported formats. @@ -359,4 +359,4 @@ def from_str(string_data, file_format="xyz"): BabelMolAdaptor object """ mols = pybel.readstring(str(file_format), str(string_data)) - return BabelMolAdaptor(mols.OBMol) + return cls(mols.OBMol) diff --git a/pymatgen/io/cif.py b/pymatgen/io/cif.py index 7aa8bed4254..40a903664dd 100644 --- a/pymatgen/io/cif.py +++ b/pymatgen/io/cif.py @@ -366,8 +366,8 @@ def is_magcif_incommensurate() -> bool: def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(cif_string: str, **kwargs) -> CifParser: + @classmethod + def from_str(cls, cif_string: str, **kwargs) -> CifParser: """ Creates a CifParser from a string. @@ -379,7 +379,7 @@ def from_str(cif_string: str, **kwargs) -> CifParser: CifParser """ stream = StringIO(cif_string) - return CifParser(stream, **kwargs) + return cls(stream, **kwargs) def _sanitize_data(self, data): """ diff --git a/pymatgen/io/cp2k/inputs.py b/pymatgen/io/cp2k/inputs.py index 0471e53b1ab..60da9c263b5 100644 --- a/pymatgen/io/cp2k/inputs.py +++ b/pymatgen/io/cp2k/inputs.py @@ -160,12 +160,12 @@ def from_dict(cls, d): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(s): + @classmethod + def from_str(cls, s): """ Initialize from a string. - Keywords must be labeled with strings. If the postprocessor finds + Keywords must be labeled with strings. If the post-processor finds that the keywords is a number, then None is return (used by the file reader). @@ -183,7 +183,7 @@ def from_str(s): args = s.split() args = list(map(postprocessor if args[0].upper() != "ELEMENT" else str, args)) args[0] = str(args[0]) - return Keyword(*args, units=units[0], description=description) + return cls(*args, units=units[0], description=description) def verbosity(self, v): """Change the printing of this keyword's description.""" @@ -730,26 +730,26 @@ def _from_dict(cls, d): .subsections, ) - @staticmethod - def from_file(file: str): + @classmethod + def from_file(cls, file: str): """Initialize from a file.""" with zopen(file, "rt") as f: txt = preprocessor(f.read(), os.path.dirname(f.name)) - return Cp2kInput.from_str(txt) + return cls.from_str(txt) @classmethod @np.deprecate(message="Use from_str instead") def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(s: str): + @classmethod + def from_str(cls, s: str): """Initialize from a string.""" lines = s.splitlines() lines = [line.replace("\t", "") for line in lines] lines = [line.strip() for line in lines] lines = [line for line in lines if line] - return Cp2kInput.from_lines(lines) + return cls.from_lines(lines) @classmethod def from_lines(cls, lines: list | tuple): diff --git a/pymatgen/io/cssr.py b/pymatgen/io/cssr.py index 613d71ef6ef..6b745271eab 100644 --- a/pymatgen/io/cssr.py +++ b/pymatgen/io/cssr.py @@ -55,8 +55,8 @@ def write_file(self, filename): with zopen(filename, "wt") as f: f.write(str(self) + "\n") - @staticmethod - def from_str(string): + @classmethod + def from_str(cls, string): """ Reads a string representation to a Cssr object. @@ -79,10 +79,10 @@ def from_str(string): if m: sp.append(m.group(1)) coords.append([float(m.group(i)) for i in range(2, 5)]) - return Cssr(Structure(latt, sp, coords)) + return cls(Structure(latt, sp, coords)) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ Reads a CSSR file to a Cssr object. @@ -93,4 +93,4 @@ def from_file(filename): Cssr object. """ with zopen(filename, "rt") as f: - return Cssr.from_str(f.read()) + return cls.from_str(f.read()) diff --git a/pymatgen/io/exciting/inputs.py b/pymatgen/io/exciting/inputs.py index e02e3197e37..297bd7ba1b4 100644 --- a/pymatgen/io/exciting/inputs.py +++ b/pymatgen/io/exciting/inputs.py @@ -70,11 +70,11 @@ def lockxyz(self, lockxyz): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(data): + @classmethod + def from_str(cls, data): """Reads the exciting input from a string.""" root = ET.fromstring(data) - speciesnode = root.find("structure").iter("species") + species_node = root.find("structure").iter("species") elements = [] positions = [] vectors = [] @@ -82,7 +82,7 @@ def from_str(data): # get title title_in = str(root.find("title").text) # Read elements and coordinates - for nodes in speciesnode: + for nodes in species_node: symbol = nodes.get("speciesfile").split(".")[0] if len(symbol.split("_")) == 2: symbol = symbol.split("_")[0] @@ -137,10 +137,10 @@ def from_str(data): lattice_in = Lattice(vectors) structure_in = Structure(lattice_in, elements, positions, coords_are_cartesian=cartesian) - return ExcitingInput(structure_in, title_in, lockxyz) + return cls(structure_in, title_in, lockxyz) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ :param filename: Filename @@ -149,7 +149,7 @@ def from_file(filename): """ with zopen(filename, "rt") as f: data = f.read().replace("\n", "") - return ExcitingInput.from_str(data) + return cls.from_str(data) def write_etree(self, celltype, cartesian=False, bandstr=False, symprec: float = 0.4, angle_tolerance=5, **kwargs): """ diff --git a/pymatgen/io/feff/inputs.py b/pymatgen/io/feff/inputs.py index d42a766663a..d73a9341f91 100644 --- a/pymatgen/io/feff/inputs.py +++ b/pymatgen/io/feff/inputs.py @@ -228,11 +228,11 @@ def formula(self): """Formula of structure.""" return self.struct.composition.formula - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """Returns Header object from file.""" - hs = Header.header_string_from_file(filename) - return Header.from_str(hs) + hs = cls.header_string_from_file(filename) + return cls.from_str(hs) @staticmethod def header_string_from_file(filename="feff.inp"): @@ -283,8 +283,8 @@ def header_string_from_file(filename="feff.inp"): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(header_str): + @classmethod + def from_str(cls, header_str): """ Reads Header string and returns Header object if header was generated by pymatgen. @@ -336,7 +336,7 @@ def from_str(header_str): struct = Structure(lattice, atomic_symbols, coords) - return Header(struct, source, comment2) + return cls(struct, source, comment2) raise ValueError("Header not generated by pymatgen, cannot return header object") @@ -675,8 +675,8 @@ def write_file(self, filename="PARAMETERS"): with zopen(filename, "wt") as f: f.write(f"{self}\n") - @staticmethod - def from_file(filename="feff.inp"): + @classmethod + def from_file(cls, filename="feff.inp"): """ Creates a Feff_tag dictionary from a PARAMETER or feff.inp file. @@ -725,7 +725,7 @@ def from_file(filename="feff.inp"): eels_dict[k] = str(v) params[str(eels_params[0].split()[0])] = eels_dict - return Tags(params) + return cls(params) @staticmethod def proc_val(key, val): diff --git a/pymatgen/io/feff/outputs.py b/pymatgen/io/feff/outputs.py index 1116d04454d..4cd0ffd5c27 100644 --- a/pymatgen/io/feff/outputs.py +++ b/pymatgen/io/feff/outputs.py @@ -42,8 +42,8 @@ def __init__(self, complete_dos, charge_transfer): self.complete_dos = complete_dos self.charge_transfer = charge_transfer - @staticmethod - def from_file(feff_inp_file="feff.inp", ldos_file="ldos"): + @classmethod + def from_file(cls, feff_inp_file="feff.inp", ldos_file="ldos"): """ Creates LDos object from raw Feff ldos files by by assuming they are numbered consecutively, i.e. ldos01.dat @@ -144,7 +144,7 @@ def from_file(feff_inp_file="feff.inp", ldos_file="ldos"): dos = Dos(efermi, dos_energies, t_dos) complete_dos = CompleteDos(structure, dos, pdoss) charge_transfer = LDos.charge_transfer_from_file(feff_inp_file, ldos_file) - return LDos(complete_dos, charge_transfer) + return cls(complete_dos, charge_transfer) @staticmethod def charge_transfer_from_file(feff_inp_file, ldos_file): @@ -287,8 +287,8 @@ def __init__(self, header, parameters, absorbing_atom, data): self.absorbing_atom = absorbing_atom self.data = np.array(data) - @staticmethod - def from_file(xmu_dat_file="xmu.dat", feff_inp_file="feff.inp"): + @classmethod + def from_file(cls, xmu_dat_file="xmu.dat", feff_inp_file="feff.inp"): """ Get Xmu from file. @@ -307,7 +307,7 @@ def from_file(xmu_dat_file="xmu.dat", feff_inp_file="feff.inp"): # site index (Note: in feff it starts from 1) # else case is species symbol absorbing_atom = parameters["TARGET"] if "RECIPROCAL" in parameters else pots.splitlines()[3].split()[2] - return Xmu(header, parameters, absorbing_atom, data) + return cls(header, parameters, absorbing_atom, data) @property def energies(self): @@ -412,8 +412,8 @@ def fine_structure(self): """Returns: Fine structure of EELS.""" return self.data[:, 3] - @staticmethod - def from_file(eels_dat_file="eels.dat"): + @classmethod + def from_file(cls, eels_dat_file="eels.dat"): """ Parse eels spectrum. @@ -424,7 +424,7 @@ def from_file(eels_dat_file="eels.dat"): Eels """ data = np.loadtxt(eels_dat_file) - return Eels(data) + return cls(data) def as_dict(self): """Returns dict representations of Xmu object.""" diff --git a/pymatgen/io/gaussian.py b/pymatgen/io/gaussian.py index 6da24dac078..e9b5f8b889b 100644 --- a/pymatgen/io/gaussian.py +++ b/pymatgen/io/gaussian.py @@ -280,8 +280,8 @@ def _parse_species(sp_str): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(contents): + @classmethod + def from_str(cls, contents): """ Creates GaussianInput from a string. @@ -328,7 +328,7 @@ def from_str(contents): spaces = 0 input_paras = {} ind += 1 - if GaussianInput._xyz_patt.match(lines[route_index + ind]): + if cls._xyz_patt.match(lines[route_index + ind]): spaces += 1 for i in range(route_index + ind, len(lines)): if lines[i].strip() == "": @@ -339,10 +339,10 @@ def from_str(contents): input_paras[d[0]] = d[1] else: coord_lines.append(lines[i].strip()) - mol = GaussianInput._parse_coords(coord_lines) + mol = cls._parse_coords(coord_lines) mol.set_charge_and_spin(charge, spin_mult) - return GaussianInput( + return cls( mol, charge=charge, spin_multiplicity=spin_mult, @@ -355,8 +355,8 @@ def from_str(contents): dieze_tag=dieze_tag, ) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ Creates GaussianInput from a file. @@ -367,7 +367,7 @@ def from_file(filename): GaussianInput object """ with zopen(filename, "r") as f: - return GaussianInput.from_str(f.read()) + return cls.from_str(f.read()) def get_zmatrix(self): """Returns a z-matrix representation of the molecule.""" diff --git a/pymatgen/io/pwscf.py b/pymatgen/io/pwscf.py index ef8a63a90b3..cea2a5b48d8 100644 --- a/pymatgen/io/pwscf.py +++ b/pymatgen/io/pwscf.py @@ -220,8 +220,8 @@ def write_file(self, filename): with open(filename, "w") as f: f.write(str(self)) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ Reads an PWInput object from a file. @@ -232,15 +232,15 @@ def from_file(filename): PWInput object """ with zopen(filename, "rt") as f: - return PWInput.from_str(f.read()) + return cls.from_str(f.read()) @classmethod @np.deprecate(message="Use from_str instead") def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(string): + @classmethod + def from_str(cls, string): """ Reads an PWInput object from a string. @@ -343,7 +343,7 @@ def input_mode(line): coords_are_cartesian=coords_are_cartesian, site_properties=site_properties, ) - return PWInput( + return cls( structure=structure, control=sections["control"], pseudo=pseudo, diff --git a/pymatgen/io/qchem/inputs.py b/pymatgen/io/qchem/inputs.py index 8bea7d319e1..2928d4fc1a8 100644 --- a/pymatgen/io/qchem/inputs.py +++ b/pymatgen/io/qchem/inputs.py @@ -347,8 +347,8 @@ def write_multi_job_file(job_list: list[QCInput], filename: str): with zopen(filename, "wt") as f: f.write(QCInput.multi_job_string(job_list)) - @staticmethod - def from_file(filename: str | Path) -> QCInput: + @classmethod + def from_file(cls, filename: str | Path) -> QCInput: """ Create QcInput from file. @@ -359,7 +359,7 @@ def from_file(filename: str | Path) -> QCInput: QcInput """ with zopen(filename, "rt") as f: - return QCInput.from_str(f.read()) + return cls.from_str(f.read()) @classmethod def from_multi_jobs_file(cls, filename: str) -> list[QCInput]: diff --git a/pymatgen/io/vasp/inputs.py b/pymatgen/io/vasp/inputs.py index ae120378821..710dfd489b1 100644 --- a/pymatgen/io/vasp/inputs.py +++ b/pymatgen/io/vasp/inputs.py @@ -224,8 +224,8 @@ def __setattr__(self, name, value): value = value.tolist() super().__setattr__(name, value) - @staticmethod - def from_file(filename, check_for_potcar=True, read_velocities=True, **kwargs) -> Poscar: + @classmethod + def from_file(cls, filename, check_for_potcar=True, read_velocities=True, **kwargs) -> Poscar: """ Reads a Poscar from a file. @@ -273,15 +273,15 @@ def from_file(filename, check_for_potcar=True, read_velocities=True, **kwargs) - except Exception: names = None with zopen(filename, "rt") as f: - return Poscar.from_str(f.read(), names, read_velocities=read_velocities) + return cls.from_str(f.read(), names, read_velocities=read_velocities) @classmethod @deprecated(message="Use from_str instead") def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(data, default_names=None, read_velocities=True): + @classmethod + def from_str(cls, data, default_names=None, read_velocities=True): """ Reads a Poscar from a string. @@ -477,7 +477,7 @@ def from_str(data, default_names=None, read_velocities=True): else: velocities = predictor_corrector = predictor_corrector_preamble = lattice_velocities = None - return Poscar( + return cls( struct, comment, selective_dynamics, @@ -783,8 +783,8 @@ def write_file(self, filename: PathLike): with zopen(filename, "wt") as f: f.write(str(self)) - @staticmethod - def from_file(filename: PathLike) -> Incar: + @classmethod + def from_file(cls, filename: PathLike) -> Incar: """Reads an Incar object from a file. Args: @@ -794,15 +794,15 @@ def from_file(filename: PathLike) -> Incar: Incar object """ with zopen(filename, "rt") as f: - return Incar.from_str(f.read()) + return cls.from_str(f.read()) @classmethod @np.deprecate(message="Use from_str instead") def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(string: str) -> Incar: + @classmethod + def from_str(cls, string: str) -> Incar: """Reads an Incar object from a string. Args: @@ -821,7 +821,7 @@ def from_str(string: str) -> Incar: val = m.group(2).strip() val = Incar.proc_val(key, val) params[key] = val - return Incar(params) + return cls(params) @staticmethod def proc_val(key: str, val: Any): @@ -1004,8 +1004,8 @@ def __str__(self): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(mode: str) -> KpointsSupportedModes: + @classmethod + def from_str(cls, mode: str) -> KpointsSupportedModes: """ :param s: String @@ -1348,8 +1348,8 @@ def automatic_linemode(divisions, ibz): num_kpts=int(divisions), ) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ Reads a Kpoints object from a KPOINTS file. @@ -1360,15 +1360,15 @@ def from_file(filename): Kpoints object """ with zopen(filename, "rt") as f: - return Kpoints.from_str(f.read()) + return cls.from_str(f.read()) @classmethod @np.deprecate(message="Use from_str instead") def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(string): + @classmethod + def from_str(cls, string): """ Reads a Kpoints object from a KPOINTS string. @@ -1386,7 +1386,7 @@ def from_str(string): # Fully automatic KPOINTS if style == "a": - return Kpoints.automatic(int(lines[3].split()[0].strip())) + return cls.automatic(int(lines[3].split()[0].strip())) coord_pattern = re.compile(r"^\s*([\d+.\-Ee]+)\s+([\d+.\-Ee]+)\s+([\d+.\-Ee]+)") @@ -1399,15 +1399,11 @@ def from_str(string): kpts_shift = [float(i) for i in lines[4].split()] except ValueError: pass - return ( - Kpoints.gamma_automatic(kpts, kpts_shift) - if style == "g" - else Kpoints.monkhorst_automatic(kpts, kpts_shift) - ) + return cls.gamma_automatic(kpts, kpts_shift) if style == "g" else cls.monkhorst_automatic(kpts, kpts_shift) # Automatic kpoints with basis if num_kpts <= 0: - style = Kpoints.supported_modes.Cartesian if style in "ck" else Kpoints.supported_modes.Reciprocal + style = cls.supported_modes.Cartesian if style in "ck" else cls.supported_modes.Reciprocal kpts = [[float(j) for j in lines[i].split()] for i in range(3, 6)] kpts_shift = [float(i) for i in lines[6].split()] return Kpoints( @@ -1421,7 +1417,7 @@ def from_str(string): # Line-mode KPOINTS, usually used with band structures if style == "l": coord_type = "Cartesian" if lines[3].lower()[0] in "ck" else "Reciprocal" - style = Kpoints.supported_modes.Line_mode + style = cls.supported_modes.Line_mode kpts = [] labels = [] patt = re.compile(r"([e0-9.\-]+)\s+([e0-9.\-]+)\s+([e0-9.\-]+)\s*!*\s*(.*)") @@ -1441,7 +1437,7 @@ def from_str(string): ) # Assume explicit KPOINTS if all else fails. - style = Kpoints.supported_modes.Cartesian if style in "ck" else Kpoints.supported_modes.Reciprocal + style = cls.supported_modes.Cartesian if style in "ck" else cls.supported_modes.Reciprocal kpts = [] kpts_weights = [] labels = [] @@ -1470,10 +1466,10 @@ def from_str(string): except IndexError: pass - return Kpoints( + return cls( comment=comment, num_kpts=num_kpts, - style=Kpoints.supported_modes[str(style)], + style=cls.supported_modes[str(style)], kpts=kpts, kpts_weights=kpts_weights, tet_number=tet_number, @@ -1819,8 +1815,8 @@ def write_file(self, filename: str) -> None: with zopen(filename, "wt") as file: file.write(str(self)) - @staticmethod - def from_file(filename: str) -> PotcarSingle: + @classmethod + def from_file(cls, filename: str) -> PotcarSingle: """ Reads PotcarSingle from file. @@ -1834,16 +1830,16 @@ def from_file(filename: str) -> PotcarSingle: try: with zopen(filename, "rt") as file: - return PotcarSingle(file.read(), symbol=symbol or None) + return cls(file.read(), symbol=symbol or None) except UnicodeDecodeError: warnings.warn("POTCAR contains invalid unicode errors. We will attempt to read it by ignoring errors.") import codecs with codecs.open(filename, "r", encoding="utf-8", errors="ignore") as file: - return PotcarSingle(file.read(), symbol=symbol or None) + return cls(file.read(), symbol=symbol or None) - @staticmethod - def from_symbol_and_functional(symbol: str, functional: str | None = None): + @classmethod + def from_symbol_and_functional(cls, symbol: str, functional: str | None = None): """ Makes a PotcarSingle from a symbol and functional. @@ -1856,7 +1852,7 @@ def from_symbol_and_functional(symbol: str, functional: str | None = None): """ functional = functional or SETTINGS.get("PMG_DEFAULT_FUNCTIONAL", "PBE") assert isinstance(functional, str) # mypy type narrowing - funcdir = PotcarSingle.functional_dir[functional] + funcdir = cls.functional_dir[functional] PMG_VASP_PSP_DIR = SETTINGS.get("PMG_VASP_PSP_DIR") if PMG_VASP_PSP_DIR is None: raise ValueError( @@ -1870,7 +1866,7 @@ def from_symbol_and_functional(symbol: str, functional: str | None = None): path = os.path.expanduser(path) path = zpath(path) if os.path.isfile(path): - return PotcarSingle.from_file(path) + return cls.from_file(path) raise OSError( f"You do not have the right POTCAR with {functional=} and {symbol=} " f"in your {PMG_VASP_PSP_DIR=}. Paths tried: {paths_to_try}" @@ -2430,8 +2426,8 @@ def from_dict(cls, d): """ return Potcar(symbols=d["symbols"], functional=d["functional"]) - @staticmethod - def from_file(filename: str): + @classmethod + def from_file(cls, filename: str): """ Reads Potcar from file. @@ -2442,7 +2438,7 @@ def from_file(filename: str): """ with zopen(filename, "rt") as f: fdata = f.read() - potcar = Potcar() + potcar = cls() functionals = [] for psingle_str in fdata.split("End of Dataset"): diff --git a/pymatgen/io/vasp/outputs.py b/pymatgen/io/vasp/outputs.py index 67b7203d7df..7c537232e0e 100644 --- a/pymatgen/io/vasp/outputs.py +++ b/pymatgen/io/vasp/outputs.py @@ -3463,8 +3463,8 @@ def __init__(self, poscar, data, data_aug=None): super().__init__(struct, data, data_aug=data_aug) self._distance_matrix = {} - @staticmethod - def from_file(filename: str): + @classmethod + def from_file(cls, filename: str): """Read a CHGCAR file. Args: @@ -3474,7 +3474,7 @@ def from_file(filename: str): Chgcar """ poscar, data, data_aug = VolumetricData.parse_file(filename) - return Chgcar(poscar, data, data_aug=data_aug) + return cls(poscar, data, data_aug=data_aug) @property def net_magnetization(self): diff --git a/pymatgen/io/wannier90.py b/pymatgen/io/wannier90.py index 257e24c2a9b..48ad3b67f46 100644 --- a/pymatgen/io/wannier90.py +++ b/pymatgen/io/wannier90.py @@ -88,8 +88,8 @@ def data(self, value: np.ndarray) -> None: self.nbnd = self.data.shape[0] self.ng = self.data.shape[-3:] - @staticmethod - def from_file(filename: str) -> object: + @classmethod + def from_file(cls, filename: str) -> object: """ Reads the UNK data from file. @@ -122,8 +122,8 @@ def from_file(filename: str) -> object: temp_data = np.empty((nbnd, 2, *ng), dtype=np.complex128) temp_data[:, 0, :, :, :] = data[::2, :, :, :] temp_data[:, 1, :, :, :] = data[1::2, :, :, :] - return Unk(ik, temp_data) - return Unk(ik, data) + return cls(ik, temp_data) + return cls(ik, data) def write_file(self, filename: str) -> None: """ diff --git a/pymatgen/io/xr.py b/pymatgen/io/xr.py index 50c07115281..3e0402ff872 100644 --- a/pymatgen/io/xr.py +++ b/pymatgen/io/xr.py @@ -73,8 +73,8 @@ def write_file(self, filename): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(string, use_cores=True, thresh=1.0e-4): + @classmethod + def from_str(cls, string, use_cores=True, thresh=1.0e-4): """ Creates an Xr object from a string representation. @@ -138,10 +138,10 @@ def from_str(string, use_cores=True, thresh=1.0e-4): else: sp.append(tmp_sp) coords.append([float(m.group(i)) for i in range(2, 5)]) - return Xr(Structure(lat, sp, coords, coords_are_cartesian=True)) + return cls(Structure(lat, sp, coords, coords_are_cartesian=True)) - @staticmethod - def from_file(filename, use_cores=True, thresh=1.0e-4): + @classmethod + def from_file(cls, filename, use_cores=True, thresh=1.0e-4): """ Reads an xr-formatted file to create an Xr object. @@ -159,4 +159,4 @@ def from_file(filename, use_cores=True, thresh=1.0e-4): file. """ with zopen(filename, "rt") as f: - return Xr.from_str(f.read(), use_cores=use_cores, thresh=thresh) + return cls.from_str(f.read(), use_cores=use_cores, thresh=thresh) diff --git a/pymatgen/io/xyz.py b/pymatgen/io/xyz.py index 7ab0cfe11ce..e4119a17a28 100644 --- a/pymatgen/io/xyz.py +++ b/pymatgen/io/xyz.py @@ -76,8 +76,8 @@ def _from_frame_string(contents) -> Molecule: def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(contents) -> XYZ: + @classmethod + def from_str(cls, contents) -> XYZ: """ Creates XYZ object from a string. @@ -99,10 +99,10 @@ def from_str(contents) -> XYZ: for xyz_match in pat.finditer(contents): xyz_text = xyz_match.group(0) mols.append(XYZ._from_frame_string(xyz_text)) - return XYZ(mols) + return cls(mols) - @staticmethod - def from_file(filename) -> XYZ: + @classmethod + def from_file(cls, filename) -> XYZ: """ Creates XYZ object from a file. @@ -113,7 +113,7 @@ def from_file(filename) -> XYZ: XYZ object """ with zopen(filename, "rt") as f: - return XYZ.from_str(f.read()) + return cls.from_str(f.read()) def as_dataframe(self): """ diff --git a/pymatgen/io/zeopp.py b/pymatgen/io/zeopp.py index be109ea0a48..48318ff08cd 100644 --- a/pymatgen/io/zeopp.py +++ b/pymatgen/io/zeopp.py @@ -96,8 +96,8 @@ def __str__(self): def from_string(cls, *args, **kwargs): return cls.from_str(*args, **kwargs) - @staticmethod - def from_str(string): + @classmethod + def from_str(cls, string): """ Reads a string representation to a ZeoCssr object. @@ -117,10 +117,10 @@ def from_str(string): lengths.insert(0, a) alpha = angles.pop(-1) angles.insert(0, alpha) - latt = Lattice.from_parameters(*lengths, *angles) + lattice = Lattice.from_parameters(*lengths, *angles) sp = [] coords = [] - chrg = [] + charge = [] for line in lines[4:]: m = re.match( r"\d+\s+(\w+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)\s+(?:0\s+){8}([0-9\-\.]+)", @@ -131,11 +131,11 @@ def from_str(string): # coords.append([float(m.group(i)) for i in xrange(2, 5)]) # Zeo++ takes x-axis along a and pymatgen takes z-axis along c coords.append([float(m.group(i)) for i in [3, 4, 2]]) - chrg.append(m.group(5)) - return ZeoCssr(Structure(latt, sp, coords, site_properties={"charge": chrg})) + charge.append(m.group(5)) + return cls(Structure(lattice, sp, coords, site_properties={"charge": charge})) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ Reads a CSSR file to a ZeoCssr object. @@ -146,7 +146,7 @@ def from_file(filename): ZeoCssr object. """ with zopen(filename, "r") as f: - return ZeoCssr.from_str(f.read()) + return cls.from_str(f.read()) class ZeoVoronoiXYZ(XYZ): @@ -163,8 +163,8 @@ def __init__(self, mol): """ super().__init__(mol) - @staticmethod - def from_str(contents): + @classmethod + def from_str(cls, contents): """ Creates Zeo++ Voronoi XYZ object from a string. from_string method of XYZ class is being redefined. @@ -188,10 +188,10 @@ def from_str(contents): # coords.append(map(float, m.groups()[1:4])) # this is 0-indexed coords.append([float(j) for j in [m.group(i) for i in [3, 4, 2]]]) prop.append(float(m.group(5))) - return ZeoVoronoiXYZ(Molecule(sp, coords, site_properties={"voronoi_radius": prop})) + return cls(Molecule(sp, coords, site_properties={"voronoi_radius": prop})) - @staticmethod - def from_file(filename): + @classmethod + def from_file(cls, filename): """ Creates XYZ object from a file. @@ -202,7 +202,7 @@ def from_file(filename): XYZ object """ with zopen(filename) as f: - return ZeoVoronoiXYZ.from_str(f.read()) + return cls.from_str(f.read()) def __str__(self) -> str: output = [str(len(self._mols[0])), self._mols[0].composition.formula] diff --git a/pymatgen/phonon/thermal_displacements.py b/pymatgen/phonon/thermal_displacements.py index ff434ffbd81..d5f75506b81 100644 --- a/pymatgen/phonon/thermal_displacements.py +++ b/pymatgen/phonon/thermal_displacements.py @@ -489,9 +489,9 @@ def sort_order(site): return self.structure.copy(site_properties=site_properties) - @staticmethod + @classmethod def from_structure_with_site_properties_Ucif( - structure: Structure, temperature: float | None = None + cls, structure: Structure, temperature: float | None = None ) -> ThermalDisplacementMatrices: """Will create this object with the help of a structure with site properties. @@ -506,18 +506,9 @@ def from_structure_with_site_properties_Ucif( Ucif_matrix = [] # U11, U22, U33, U23, U13, U12 for site in structure: - Ucif_matrix.append( - [ - site.properties["U11_cif"], - site.properties["U22_cif"], - site.properties["U33_cif"], - site.properties["U23_cif"], - site.properties["U13_cif"], - site.properties["U12_cif"], - ] - ) + Ucif_matrix.append([site.properties[f"U{idx}_cif"] for idx in (11, 22, 33, 23, 13, 12)]) - return ThermalDisplacementMatrices.from_Ucif(Ucif_matrix, structure, temperature=temperature) + return cls.from_Ucif(Ucif_matrix, structure, temperature=temperature) @staticmethod def from_cif_P1(filename: str) -> list[ThermalDisplacementMatrices]: diff --git a/pymatgen/transformations/standard_transformations.py b/pymatgen/transformations/standard_transformations.py index 037fdf17700..2ca43ab1d0a 100644 --- a/pymatgen/transformations/standard_transformations.py +++ b/pymatgen/transformations/standard_transformations.py @@ -198,7 +198,7 @@ def is_one_to_many(self) -> bool: class SupercellTransformation(AbstractTransformation): - """The SupercellTransformation replicates an unitcell to a supercell.""" + """The SupercellTransformation replicates a unit cell to a supercell.""" def __init__(self, scaling_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1))): """ @@ -211,8 +211,8 @@ def __init__(self, scaling_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1))): """ self.scaling_matrix = scaling_matrix - @staticmethod - def from_scaling_factors(scale_a=1, scale_b=1, scale_c=1): + @classmethod + def from_scaling_factors(cls, scale_a=1, scale_b=1, scale_c=1): """Convenience method to get a SupercellTransformation from a simple series of three numbers for scaling each lattice vector. Equivalent to calling the normal with [[scale_a, 0, 0], [0, scale_b, 0], @@ -226,7 +226,7 @@ def from_scaling_factors(scale_a=1, scale_b=1, scale_c=1): Returns: SupercellTransformation. """ - return SupercellTransformation([[scale_a, 0, 0], [0, scale_b, 0], [0, 0, scale_c]]) + return cls([[scale_a, 0, 0], [0, scale_b, 0], [0, 0, scale_c]]) @staticmethod def from_boundary_distance( diff --git a/tests/io/test_adf.py b/tests/io/test_adf.py index c15dfce690a..878e9b0ddfe 100644 --- a/tests/io/test_adf.py +++ b/tests/io/test_adf.py @@ -38,14 +38,14 @@ END """ -h2oxyz = """3 +h2o_xyz = """3 0.0 O -0.90293455 0.66591421 0.0 H 0.05706545 0.66591421 0.0 H -1.22338913 1.57085004 0.0 """ -rhb18xyz = """19 +rhb18_xyz = """19 0.0 Rh -0.453396 -0.375115 0.000000 B 0.168139 3.232791 0.000000 @@ -70,7 +70,7 @@ def readfile(file_object): - """ + """` Return the content of the file as a string. Parameters @@ -170,7 +170,7 @@ def test_option_operations(self): def test_atom_block_key(self): block = AdfKey("atoms") - o = Molecule.from_str(h2oxyz, "xyz") + o = Molecule.from_str(h2o_xyz, "xyz") for site in o: block.add_subkey(AdfKey(str(site.specie), list(site.coords))) assert str(block) == atoms_string @@ -252,7 +252,7 @@ def test_serialization(self): class TestAdfInput(PymatgenTest): def test_main(self): tmp_file = f"{self.tmp_path}/adf.temp" - mol = Molecule.from_str(rhb18xyz, "xyz") + mol = Molecule.from_str(rhb18_xyz, "xyz") mol.set_charge_and_spin(-1, 3) task = AdfTask("optimize", **rhb18) inp = AdfInput(task) From 2709068a2b01cfbbfda4e2dcd62361678c30c10f Mon Sep 17 00:00:00 2001 From: Six_ligand <49940294+RedStar-Iron@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:40:42 -0400 Subject: [PATCH 4/4] Update inputs.py (#3430) --- pymatgen/io/vasp/inputs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymatgen/io/vasp/inputs.py b/pymatgen/io/vasp/inputs.py index 710dfd489b1..53981b9b4ed 100644 --- a/pymatgen/io/vasp/inputs.py +++ b/pymatgen/io/vasp/inputs.py @@ -852,6 +852,7 @@ def proc_val(key: str, val: Any): "ISIF", "IBRION", "ISPIN", + "ISTART", "ICHARG", "NELM", "ISMEAR",