diff --git a/dev_scripts/potcar_scrambler.py b/dev_scripts/potcar_scrambler.py index fe6d659cf94..639c366bc7c 100644 --- a/dev_scripts/potcar_scrambler.py +++ b/dev_scripts/potcar_scrambler.py @@ -4,6 +4,7 @@ import shutil import warnings from glob import glob +from typing import TYPE_CHECKING import numpy as np from monty.os.path import zpath @@ -14,6 +15,9 @@ from pymatgen.io.vasp.sets import _load_yaml_config from pymatgen.util.testing import VASP_IN_DIR +if TYPE_CHECKING: + from typing_extensions import Self + class PotcarScrambler: """ @@ -34,18 +38,14 @@ class PotcarScrambler: from existing POTCAR `input_filename` """ - def __init__(self, potcars: Potcar | PotcarSingle): - if isinstance(potcars, PotcarSingle): - self.PSP_list = [potcars] - else: - self.PSP_list = potcars + def __init__(self, potcars: Potcar | PotcarSingle) -> None: + self.PSP_list = [potcars] if isinstance(potcars, PotcarSingle) else potcars self.scrambled_potcars_str = "" for psp in self.PSP_list: scrambled_potcar_str = self.scramble_single_potcar(psp) self.scrambled_potcars_str += scrambled_potcar_str - return - def _rand_float_from_str_with_prec(self, input_str: str, bloat: float = 1.5): + def _rand_float_from_str_with_prec(self, input_str: str, bloat: float = 1.5) -> float: n_prec = len(input_str.split(".")[1]) bd = max(1, bloat * abs(float(input_str))) return round(bd * np.random.rand(1)[0], n_prec) @@ -53,7 +53,7 @@ def _rand_float_from_str_with_prec(self, input_str: str, bloat: float = 1.5): def _read_fortran_str_and_scramble(self, input_str: str, bloat: float = 1.5): input_str = input_str.strip() - if input_str.lower() in ("t", "f", "true", "false"): + if input_str.lower() in {"t", "f", "true", "false"}: return bool(np.random.randint(2)) if input_str.upper() == input_str.lower() and input_str[0].isnumeric(): @@ -68,7 +68,7 @@ def _read_fortran_str_and_scramble(self, input_str: str, bloat: float = 1.5): except ValueError: return input_str - def scramble_single_potcar(self, potcar: PotcarSingle): + def scramble_single_potcar(self, potcar: PotcarSingle) -> str: """ Scramble the body of a POTCAR, retain the PSCTR header information. @@ -124,12 +124,12 @@ def scramble_single_potcar(self, potcar: PotcarSingle): ) return scrambled_potcar_str - def to_file(self, filename: str): + def to_file(self, filename: str) -> None: with zopen(filename, mode="wt") as file: file.write(self.scrambled_potcars_str) @classmethod - def from_file(cls, input_filename: str, output_filename: str | None = None): + def from_file(cls, input_filename: str, output_filename: str | None = None) -> Self: psp = Potcar.from_file(input_filename) psp_scrambled = cls(psp) if output_filename: @@ -137,7 +137,7 @@ def from_file(cls, input_filename: str, output_filename: str | None = None): return psp_scrambled -def generate_fake_potcar_libraries(): +def generate_fake_potcar_libraries() -> None: """ To test the `_gen_potcar_summary_stats` function in `pymatgen.io.vasp.inputs`, need a library of fake POTCARs which do not violate copyright @@ -173,7 +173,7 @@ def generate_fake_potcar_libraries(): break -def potcar_cleanser(): +def potcar_cleanser() -> None: """ Function to replace copyrighted POTCARs used in io.vasp.sets testing with dummy POTCARs that have scrambled PSP and kinetic energy values diff --git a/dev_scripts/regen_libxcfunc.py b/dev_scripts/regen_libxcfunc.py index 2965acab22e..1c0491d112c 100755 --- a/dev_scripts/regen_libxcfunc.py +++ b/dev_scripts/regen_libxcfunc.py @@ -50,16 +50,16 @@ def write_libxc_docs_json(xc_funcs, json_path): xc_funcs = deepcopy(xc_funcs) # Remove XC_FAMILY from Family and XC_ from Kind to make strings more human-readable. - for d in xc_funcs.values(): - d["Family"] = d["Family"].replace("XC_FAMILY_", "", 1) - d["Kind"] = d["Kind"].replace("XC_", "", 1) + for dct in xc_funcs.values(): + dct["Family"] = dct["Family"].replace("XC_FAMILY_", "", 1) + dct["Kind"] = dct["Kind"].replace("XC_", "", 1) # Build lightweight version with a subset of keys. - for num, d in xc_funcs.items(): - xc_funcs[num] = {key: d[key] for key in ("Family", "Kind", "References")} + for num, dct in xc_funcs.items(): + xc_funcs[num] = {key: dct[key] for key in ("Family", "Kind", "References")} # Descriptions are optional for opt in ("Description 1", "Description 2"): - desc = d.get(opt) + desc = dct.get(opt) if desc is not None: xc_funcs[num][opt] = desc diff --git a/pymatgen/alchemy/filters.py b/pymatgen/alchemy/filters.py index 64f7cb2b382..3f1bf77091e 100644 --- a/pymatgen/alchemy/filters.py +++ b/pymatgen/alchemy/filters.py @@ -94,7 +94,7 @@ def __repr__(self): ] ) - def as_dict(self): + def as_dict(self) -> dict: """Returns: MSONable dict.""" return { "@module": type(self).__module__, diff --git a/pymatgen/alchemy/materials.py b/pymatgen/alchemy/materials.py index 8c102e99143..5be0288de4a 100644 --- a/pymatgen/alchemy/materials.py +++ b/pymatgen/alchemy/materials.py @@ -23,6 +23,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + from pymatgen.alchemy.filters import AbstractStructureFilter @@ -212,7 +214,7 @@ def write_vasp_input( **kwargs: All keyword args supported by the VASP input set. """ vasp_input_set(self.final_structure, **kwargs).write_input(output_dir, make_dir_if_not_present=create_directory) - with open(f"{output_dir}/transformations.json", mode="w") as file: + with open(f"{output_dir}/transformations.json", mode="w", encoding="utf-8") as file: json.dump(self.as_dict(), file) def __str__(self) -> str: @@ -267,7 +269,7 @@ def from_cif_str( transformations: list[AbstractTransformation] | None = None, primitive: bool = True, occupancy_tolerance: float = 1.0, - ) -> TransformedStructure: + ) -> Self: """Generates TransformedStructure from a cif string. Args: @@ -311,7 +313,7 @@ def from_poscar_str( cls, poscar_string: str, transformations: list[AbstractTransformation] | None = None, - ) -> TransformedStructure: + ) -> Self: """Generates TransformedStructure from a poscar string. Args: @@ -339,12 +341,12 @@ def as_dict(self) -> dict[str, Any]: dct["@module"] = type(self).__module__ dct["@class"] = type(self).__name__ dct["history"] = jsanitize(self.history) - dct["last_modified"] = str(datetime.datetime.utcnow()) + dct["last_modified"] = str(datetime.datetime.now(datetime.timezone.utc)) dct["other_parameters"] = jsanitize(self.other_parameters) return dct @classmethod - def from_dict(cls, dct: dict) -> TransformedStructure: + def from_dict(cls, dct: dict) -> Self: """Creates a TransformedStructure from a dict.""" struct = Structure.from_dict(dct) return cls(struct, history=dct["history"], other_parameters=dct.get("other_parameters")) @@ -376,7 +378,7 @@ def to_snl(self, authors: list[str], **kwargs) -> StructureNL: return StructureNL(self.final_structure, authors, history=history, **kwargs) @classmethod - def from_snl(cls, snl: StructureNL) -> TransformedStructure: + def from_snl(cls, snl: StructureNL) -> Self: """Create TransformedStructure from SNL. Args: diff --git a/pymatgen/alchemy/transmuters.py b/pymatgen/alchemy/transmuters.py index 41b5a3a71f2..e4b4b4e1ffd 100644 --- a/pymatgen/alchemy/transmuters.py +++ b/pymatgen/alchemy/transmuters.py @@ -20,6 +20,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + __author__ = "Shyue Ping Ong, Will Richards" __copyright__ = "Copyright 2012, The Materials Project" __version__ = "0.1" @@ -42,7 +44,7 @@ def __init__( transformations=None, extend_collection: int = 0, ncores: int | None = None, - ): + ) -> None: """Initializes a transmuter from an initial list of pymatgen.alchemy.materials.TransformedStructure. @@ -71,7 +73,16 @@ def __getitem__(self, index): def __getattr__(self, name): return [getattr(x, name) for x in self.transformed_structures] - def undo_last_change(self): + def __len__(self): + return len(self.transformed_structures) + + def __str__(self): + output = ["Current structures", "------------"] + for x in self.transformed_structures: + output.append(str(x.final_structure)) + return "\n".join(output) + + def undo_last_change(self) -> None: """Undo the last transformation in the TransformedStructure. Raises: @@ -80,7 +91,7 @@ def undo_last_change(self): for x in self.transformed_structures: x.undo_last_change() - def redo_next_change(self): + def redo_next_change(self) -> None: """Redo the last undone transformation in the TransformedStructure. Raises: @@ -89,9 +100,6 @@ def redo_next_change(self): for x in self.transformed_structures: x.redo_next_change() - def __len__(self): - return len(self.transformed_structures) - def append_transformation(self, transformation, extend_collection=False, clear_redo=True): """Appends a transformation to all TransformedStructures. @@ -178,12 +186,6 @@ def add_tags(self, tags): """ self.set_parameter("tags", tags) - def __str__(self): - output = ["Current structures", "------------"] - for x in self.transformed_structures: - output.append(str(x.final_structure)) - return "\n".join(output) - def append_transformed_structures(self, trafo_structs_or_transmuter): """Method is overloaded to accept either a list of transformed structures or transmuter, it which case it appends the second transmuter"s @@ -201,7 +203,7 @@ def append_transformed_structures(self, trafo_structs_or_transmuter): self.transformed_structures.extend(trafo_structs_or_transmuter) @classmethod - def from_structures(cls, structures, transformations=None, extend_collection=0): + def from_structures(cls, structures, transformations=None, extend_collection=0) -> Self: """Alternative constructor from structures rather than TransformedStructures. @@ -256,7 +258,7 @@ def __init__(self, cif_string, transformations=None, primitive=True, extend_coll super().__init__(transformed_structures, transformations, extend_collection) @classmethod - def from_filenames(cls, filenames, transformations=None, primitive=True, extend_collection=False): + def from_filenames(cls, filenames, transformations=None, primitive=True, extend_collection=False) -> Self: """Generates a TransformedStructureCollection from a cif, possibly containing multiple structures. @@ -269,7 +271,7 @@ def from_filenames(cls, filenames, transformations=None, primitive=True, extend_ """ cif_files = [] for filename in filenames: - with open(filename) as file: + with open(filename, encoding="utf-8") as file: cif_files.append(file.read()) return cls( "\n".join(cif_files), @@ -308,7 +310,7 @@ def from_filenames(cls, poscar_filenames, transformations=None, extend_collectio """ trafo_structs = [] for filename in poscar_filenames: - with open(filename) as file: + with open(filename, encoding="utf-8") as file: trafo_structs.append(TransformedStructure.from_poscar_str(file.read(), [])) return StandardTransmuter(trafo_structs, transformations, extend_collection=extend_collection) diff --git a/pymatgen/analysis/adsorption.py b/pymatgen/analysis/adsorption.py index 44d0c9330cb..c091d18fa38 100644 --- a/pymatgen/analysis/adsorption.py +++ b/pymatgen/analysis/adsorption.py @@ -24,7 +24,11 @@ from pymatgen.util.coord import in_coord_list_pbc if TYPE_CHECKING: + import matplotlib.pyplot as plt from numpy.typing import ArrayLike + from typing_extensions import Self + + from pymatgen.core.surface import Slab __author__ = "Joseph Montoya" __copyright__ = "Copyright 2016, The Materials Project" @@ -54,7 +58,7 @@ class AdsorbateSiteFinder: """ def __init__( - self, slab, selective_dynamics: bool = False, height: float = 0.9, mi_vec: ArrayLike | None = None + self, slab: Slab, selective_dynamics: bool = False, height: float = 0.9, mi_vec: ArrayLike | None = None ) -> None: """Create an AdsorbateSiteFinder object. @@ -88,7 +92,7 @@ def from_bulk_and_miller( center_slab=True, selective_dynamics=False, undercoord_threshold=0.09, - ): + ) -> Self: """This method constructs the adsorbate site finder from a bulk structure and a miller index, which allows the surface sites to be determined from the difference in bulk and slab coordination, as @@ -132,7 +136,7 @@ def from_bulk_and_miller( vnn_surface = VoronoiNN(tol=0.05, allow_pathological=True) - surf_props, undercoords = [], [] + surf_props, under_coords = [], [] this_mi_vec = get_mi_vec(this_slab) mi_mags = [np.dot(this_mi_vec, site.coords) for site in this_slab] average_mi_mag = np.average(mi_mags) @@ -140,20 +144,20 @@ def from_bulk_and_miller( bulk_coord = this_slab.site_properties["bulk_coordinations"][n] slab_coord = len(vnn_surface.get_nn(this_slab, n)) mi_mag = np.dot(this_mi_vec, site.coords) - undercoord = (bulk_coord - slab_coord) / bulk_coord - undercoords += [undercoord] - if undercoord > undercoord_threshold and mi_mag > average_mi_mag: + under_coord = (bulk_coord - slab_coord) / bulk_coord + under_coords += [under_coord] + if under_coord > undercoord_threshold and mi_mag > average_mi_mag: surf_props += ["surface"] else: surf_props += ["subsurface"] new_site_properties = { "surface_properties": surf_props, - "undercoords": undercoords, + "undercoords": under_coords, } new_slab = this_slab.copy(site_properties=new_site_properties) return cls(new_slab, selective_dynamics) - def find_surface_sites_by_height(self, slab, height=0.9, xy_tol=0.05): + def find_surface_sites_by_height(self, slab: Slab, height=0.9, xy_tol=0.05): """This method finds surface sites by determining which sites are within a threshold value in height from the topmost site in a list of sites. @@ -179,7 +183,8 @@ def find_surface_sites_by_height(self, slab, height=0.9, xy_tol=0.05): # sort surface sites by height surf_sites = [s for (h, s) in zip(m_projs[mask], surf_sites)] surf_sites.reverse() - unique_sites, unique_perp_fracs = [], [] + unique_sites: list = [] + unique_perp_fracs: list = [] for site in surf_sites: this_perp = site.coords - np.dot(site.coords, self.mvec) this_perp_frac = slab.lattice.get_fractional_coords(this_perp) @@ -190,7 +195,7 @@ def find_surface_sites_by_height(self, slab, height=0.9, xy_tol=0.05): return surf_sites - def assign_site_properties(self, slab, height=0.9): + def assign_site_properties(self, slab: Slab, height=0.9): """Assigns site properties.""" if "surface_properties" in slab.site_properties: return slab @@ -260,8 +265,8 @@ def find_adsorption_sites( ads_sites["subsurface"] = ss_sites if "bridge" in positions or "hollow" in positions: mesh = self.get_extended_surface_mesh() - sop = get_rot(self.slab) - dt = Delaunay([sop.operate(m.coords)[:2] for m in mesh]) + symm_op = get_rot(self.slab) + dt = Delaunay([symm_op.operate(m.coords)[:2] for m in mesh]) # TODO: refactor below to properly account for >3-fold for v in dt.simplices: if -1 not in v: @@ -383,8 +388,8 @@ def add_adsorbate(self, molecule: Molecule, ads_coord, repeat=None, translate=Tr molecule.translate_sites(vector=[-x, -y, -z]) if reorient: # Reorient the molecule along slab m_index - sop = get_rot(self.slab) - molecule.apply_operation(sop.inverse) + symm_op = get_rot(self.slab) + molecule.apply_operation(symm_op.inverse) struct = self.slab.copy() if repeat: struct.make_supercell(repeat) @@ -553,20 +558,20 @@ def generate_substitution_structures( sym_slab = SpacegroupAnalyzer(self.slab).get_symmetrized_structure() # Define a function for substituting a site - def substitute(site, i): + def substitute(site, idx): slab = self.slab.copy() props = self.slab.site_properties if sub_both_sides: # Find an equivalent site on the other surface - eq_indices = next(indices for indices in sym_slab.equivalent_indices if i in indices) + eq_indices = next(indices for indices in sym_slab.equivalent_indices if idx in indices) for ii in eq_indices: if f"{sym_slab[ii].frac_coords[2]:.6f}" != f"{site.frac_coords[2]:.6f}": props["surface_properties"][ii] = "substitute" slab.replace(ii, atom) break - props["surface_properties"][i] = "substitute" - slab.replace(i, atom) + props["surface_properties"][idx] = "substitute" + slab.replace(idx, atom) slab.add_site_property("surface_properties", props["surface_properties"]) return slab @@ -598,7 +603,7 @@ def get_mi_vec(slab): return mvec / np.linalg.norm(mvec) -def get_rot(slab): +def get_rot(slab: Slab) -> SymmOp: """Gets the transformation to rotate the z axis into the miller index.""" new_z = get_mi_vec(slab) a, _b, _c = slab.lattice.matrix @@ -621,8 +626,8 @@ def reorient_z(structure): to the A-B plane. """ struct = structure.copy() - sop = get_rot(struct) - struct.apply_operation(sop) + symm_op = get_rot(struct) + struct.apply_operation(symm_op) return struct @@ -632,8 +637,8 @@ def reorient_z(structure): def plot_slab( - slab, - ax, + slab: Slab, + ax: plt.Axes, scale=0.8, repeat=5, window=1.5, @@ -668,7 +673,7 @@ def plot_slab( corner = [0, 0, slab.lattice.get_fractional_coords(coords[-1])[-1]] corner = slab.lattice.get_cartesian_coords(corner)[:2] verts = orig_cell[:2, :2] - lattsum = verts[0] + verts[1] + lattice_sum = verts[0] + verts[1] # inverse coords, sites, alphas, to show other side of slab if inverse: alphas = np.array(reversed(alphas)) @@ -677,11 +682,11 @@ def plot_slab( # Draw circles at sites and stack them accordingly for n, coord in enumerate(coords): r = sites[n].species.elements[0].atomic_radius * scale - ax.add_patch(patches.Circle(coord[:2] - lattsum * (repeat // 2), r, color="w", zorder=2 * n)) + ax.add_patch(patches.Circle(coord[:2] - lattice_sum * (repeat // 2), r, color="w", zorder=2 * n)) color = color_dict[sites[n].species.elements[0].symbol] ax.add_patch( patches.Circle( - coord[:2] - lattsum * (repeat // 2), + coord[:2] - lattice_sum * (repeat // 2), r, facecolor=color, alpha=alphas[n], @@ -698,12 +703,12 @@ def plot_slab( inverse_slab.make_supercell([1, 1, -1]) asf = AdsorbateSiteFinder(inverse_slab) ads_sites = asf.find_adsorption_sites()["all"] - sop = get_rot(orig_slab) - ads_sites = [sop.operate(ads_site)[:2].tolist() for ads_site in ads_sites] + symm_op = get_rot(orig_slab) + ads_sites = [symm_op.operate(ads_site)[:2].tolist() for ads_site in ads_sites] ax.plot(*zip(*ads_sites), color="k", marker="x", markersize=10, mew=1, linestyle="", zorder=10000) # Draw unit cell if draw_unit_cell: - verts = np.insert(verts, 1, lattsum, axis=0).tolist() + verts = np.insert(verts, 1, lattice_sum, axis=0).tolist() verts += [[0.0, 0.0]] verts = [[0.0, 0.0], *verts] codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] @@ -712,8 +717,8 @@ def plot_slab( patch = patches.PathPatch(path, facecolor="none", lw=2, alpha=0.5, zorder=2 * n + 2) ax.add_patch(patch) ax.set_aspect("equal") - center = corner + lattsum / 2.0 - extent = np.max(lattsum) + center = corner + lattice_sum / 2.0 + extent = np.max(lattice_sum) lim_array = [center - extent * window, center + extent * window] x_lim = [ele[0] for ele in lim_array] y_lim = [ele[1] for ele in lim_array] diff --git a/pymatgen/analysis/chemenv/connectivity/connected_components.py b/pymatgen/analysis/chemenv/connectivity/connected_components.py index 2518dc769cf..15affef625c 100644 --- a/pymatgen/analysis/chemenv/connectivity/connected_components.py +++ b/pymatgen/analysis/chemenv/connectivity/connected_components.py @@ -238,7 +238,10 @@ def __init__( "__init__", "Trying to add edge with some unexistent node ...", ) - if links_data is not None: + if links_data is None: + edge_data = None + + else: if (env_node1, env_node2, key) in links_data: edge_data = links_data[(env_node1, env_node2, key)] elif (env_node2, env_node1, key) in links_data: @@ -249,8 +252,7 @@ def __init__( edge_data = links_data[(env_node2, env_node1)] else: edge_data = None - else: - edge_data = None + if edge_data: self._connected_subgraph.add_edge(env_node1, env_node2, key, **edge_data) else: @@ -859,7 +861,7 @@ def from_dict(cls, dct: dict) -> Self: return cls(graph=graph) @classmethod - def from_graph(cls, g): + def from_graph(cls, g) -> Self: """ Constructor for the ConnectedComponent object from a graph of the connected component. diff --git a/pymatgen/analysis/chemenv/coordination_environments/chemenv_strategies.py b/pymatgen/analysis/chemenv/coordination_environments/chemenv_strategies.py index 7494cd34c0e..63a32c88460 100644 --- a/pymatgen/analysis/chemenv/coordination_environments/chemenv_strategies.py +++ b/pymatgen/analysis/chemenv/coordination_environments/chemenv_strategies.py @@ -62,7 +62,7 @@ class DistanceCutoffFloat(float, StrategyOption): allowed_values = "Real number between 1 and +infinity" - def __new__(cls, cutoff): + def __new__(cls, cutoff) -> Self: """Special float that should be between 1 and infinity. Args: @@ -96,7 +96,7 @@ class AngleCutoffFloat(float, StrategyOption): allowed_values = "Real number between 0 and 1" - def __new__(cls, cutoff): + def __new__(cls, cutoff) -> Self: """Special float that should be between 0 and 1. Args: @@ -130,7 +130,7 @@ class CSMFloat(float, StrategyOption): allowed_values = "Real number between 0 and 100" - def __new__(cls, cutoff): + def __new__(cls, cutoff) -> Self: """Special float that should be between 0 and 100. Args: @@ -166,7 +166,7 @@ class AdditionalConditionInt(int, StrategyOption): for integer, description in AdditionalConditions.CONDITION_DESCRIPTION.items(): allowed_values += f" - {integer} for {description!r}\n" - def __new__(cls, integer): + def __new__(cls, integer) -> Self: """Special int representing additional conditions.""" if str(int(integer)) != str(integer): raise ValueError(f"Additional condition {integer} is not an integer") @@ -479,7 +479,7 @@ def as_dict(self): raise NotImplementedError @classmethod - def from_dict(cls, dct) -> AbstractChemenvStrategy: + def from_dict(cls, dct) -> Self: """ Reconstructs the SimpleAbundanceChemenvStrategy object from a dict representation of the SimpleAbundanceChemenvStrategy object created using the as_dict method. @@ -860,7 +860,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> SimplestChemenvStrategy: + def from_dict(cls, dct: dict) -> Self: """ Reconstructs the SimplestChemenvStrategy object from a dict representation of the SimplestChemenvStrategy object created using the as_dict method. @@ -1065,7 +1065,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> SimpleAbundanceChemenvStrategy: + def from_dict(cls, dct: dict) -> Self: """ Reconstructs the SimpleAbundanceChemenvStrategy object from a dict representation of the SimpleAbundanceChemenvStrategy object created using the as_dict method. @@ -1240,7 +1240,7 @@ def __eq__(self, other): ) @classmethod - def from_dict(cls, dct) -> TargetedPenaltiedAbundanceChemenvStrategy: + def from_dict(cls, dct) -> Self: """ Reconstructs the TargetedPenaltiedAbundanceChemenvStrategy object from a dict representation of the TargetedPenaltiedAbundanceChemenvStrategy object created using the as_dict method. @@ -2924,7 +2924,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> WeightedNbSetChemenvStrategy: + def from_dict(cls, dct: dict) -> Self: """ Reconstructs the WeightedNbSetChemenvStrategy object from a dict representation of the WeightedNbSetChemenvStrategy object created using the as_dict method. @@ -3101,7 +3101,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> MultiWeightsChemenvStrategy: + def from_dict(cls, dct: dict) -> Self: """ Reconstructs the MultiWeightsChemenvStrategy object from a dict representation of the MultipleAbundanceChemenvStrategy object created using the as_dict method. diff --git a/pymatgen/analysis/chemenv/coordination_environments/coordination_geometries.py b/pymatgen/analysis/chemenv/coordination_environments/coordination_geometries.py index 287247996b0..b4c5168cb8d 100644 --- a/pymatgen/analysis/chemenv/coordination_environments/coordination_geometries.py +++ b/pymatgen/analysis/chemenv/coordination_environments/coordination_geometries.py @@ -245,7 +245,7 @@ def safe_separation_permutations(self, ordered_plane=False, ordered_point_groups number of permutations. add_opposite: Whether to add the permutations from the second group before the first group as well. - Returns + Returns: list[int]: safe permutations. """ s0 = list(range(len(self.point_groups[0]))) diff --git a/pymatgen/analysis/chemenv/coordination_environments/structure_environments.py b/pymatgen/analysis/chemenv/coordination_environments/structure_environments.py index 2660d1af49c..a95bc9943fa 100644 --- a/pymatgen/analysis/chemenv/coordination_environments/structure_environments.py +++ b/pymatgen/analysis/chemenv/coordination_environments/structure_environments.py @@ -1252,7 +1252,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> StructureEnvironments: + def from_dict(cls, dct: dict) -> Self: """ Reconstructs the StructureEnvironments object from a dict representation of the StructureEnvironments created using the as_dict method. @@ -1481,7 +1481,7 @@ def __init__( self.valences_origin = valences_origin @classmethod - def from_structure_environments(cls, strategy, structure_environments, valences=None, valences_origin=None): + def from_structure_environments(cls, strategy, structure_environments, valences=None, valences_origin=None) -> Self: """ Construct a LightStructureEnvironments object from a strategy and a StructureEnvironments object. @@ -1497,10 +1497,10 @@ def from_structure_environments(cls, strategy, structure_environments, valences= """ structure = structure_environments.structure strategy.set_structure_environments(structure_environments=structure_environments) - coordination_environments = [None] * len(structure) - neighbors_sets = [None] * len(structure) - _all_nbs_sites = [] - all_nbs_sites = [] + coordination_environments: list = [None] * len(structure) + neighbors_sets: list = [None] * len(structure) + _all_nbs_sites: list = [] + all_nbs_sites: list = [] if valences is None: valences = structure_environments.valences if valences_origin is None: @@ -1515,7 +1515,7 @@ def from_structure_environments(cls, strategy, structure_environments, valences= coordination_environments[idx] = [] neighbors_sets[idx] = [] site_ces = [] - site_nbs_sets = [] + site_nbs_sets: list = [] for ce_and_neighbors in site_ces_and_nbs_list: _all_nbs_sites_indices = [] # Coordination environment @@ -1561,6 +1561,7 @@ def from_structure_environments(cls, strategy, structure_environments, valences= site_nbs_sets.append(nb_set) coordination_environments[idx] = site_ces neighbors_sets[idx] = site_nbs_sets + return cls( strategy=strategy, coordination_environments=coordination_environments, @@ -2020,7 +2021,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct) -> LightStructureEnvironments: + def from_dict(cls, dct) -> Self: """ Reconstructs the LightStructureEnvironments object from a dict representation of the LightStructureEnvironments created using the as_dict method. @@ -2339,7 +2340,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> ChemicalEnvironments: + def from_dict(cls, dct: dict) -> Self: """ Reconstructs the ChemicalEnvironments object from a dict representation of the ChemicalEnvironments created using the as_dict method. diff --git a/pymatgen/analysis/chemenv/coordination_environments/voronoi.py b/pymatgen/analysis/chemenv/coordination_environments/voronoi.py index 52e04ed586a..6d23f0c9da6 100644 --- a/pymatgen/analysis/chemenv/coordination_environments/voronoi.py +++ b/pymatgen/analysis/chemenv/coordination_environments/voronoi.py @@ -33,7 +33,7 @@ __date__ = "Feb 20, 2016" -def from_bson_voronoi_list2(bson_nb_voro_list2, structure): +def from_bson_voronoi_list2(bson_nb_voro_list2: list[PeriodicSite], structure: Structure): """ Returns the voronoi_list needed for the VoronoiContainer object from a bson-encoded voronoi_list. @@ -45,21 +45,22 @@ def from_bson_voronoi_list2(bson_nb_voro_list2, structure): The voronoi_list needed for the VoronoiContainer (with PeriodicSites as keys of the dictionary - not allowed in the BSON format). """ - voronoi_list = [None] * len(bson_nb_voro_list2) - for isite, voro in enumerate(bson_nb_voro_list2): - if voro is None or voro == "None": + voronoi_list: list[list[dict] | None] = [None] * len(bson_nb_voro_list2) + + for idx, voro in enumerate(bson_nb_voro_list2): + if voro in (None, "None"): continue - voronoi_list[isite] = [] + + voronoi_list[idx] = [] for psd, dct in voro: struct_site = structure[dct["index"]] - periodic_site = PeriodicSite( + dct["site"] = PeriodicSite( struct_site._species, struct_site.frac_coords + psd[1], struct_site._lattice, properties=struct_site.properties, ) - dct["site"] = periodic_site - voronoi_list[isite].append(dct) + voronoi_list[idx].append(dct) # type: ignore[union-attr] return voronoi_list @@ -957,6 +958,7 @@ def from_dict(cls, dct: dict) -> Self: voronoi_list2 = from_bson_voronoi_list2(dct["bson_nb_voro_list2"], structure) maximum_distance_factor = dct.get("maximum_distance_factor") minimum_angle_factor = dct.get("minimum_angle_factor") + return cls( structure=structure, voronoi_list2=voronoi_list2, diff --git a/pymatgen/analysis/elasticity/elastic.py b/pymatgen/analysis/elasticity/elastic.py index 53db12e9d38..fae802b2837 100644 --- a/pymatgen/analysis/elasticity/elastic.py +++ b/pymatgen/analysis/elasticity/elastic.py @@ -27,6 +27,7 @@ from collections.abc import Sequence from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core import Structure @@ -50,7 +51,7 @@ class NthOrderElasticTensor(Tensor): GPa_to_eV_A3 = Unit("GPa").get_conversion_factor(Unit("eV ang^-3")) symbol = "C" - def __new__(cls, input_array, check_rank=None, tol: float = 1e-4): + def __new__(cls, input_array, check_rank=None, tol: float = 1e-4) -> Self: """ Args: input_array (): @@ -92,7 +93,7 @@ def energy_density(self, strain, convert_GPa_to_eV=True): return e_density @classmethod - def from_diff_fit(cls, strains, stresses, eq_stress=None, order=2, tol: float = 1e-10): + def from_diff_fit(cls, strains, stresses, eq_stress=None, order=2, tol: float = 1e-10) -> Self: """ Takes a list of strains and stresses, and returns a list of coefficients for a polynomial fit of the given order. @@ -132,7 +133,7 @@ class ElasticTensor(NthOrderElasticTensor): in units of eV/A^3. """ - def __new__(cls, input_array, tol: float = 1e-4): + def __new__(cls, input_array, tol: float = 1e-4) -> Self: """ Create an ElasticTensor object. The constructor throws an error if the shape of the input_matrix argument is not 3x3x3x3, i. e. in true tensor notation. Issues a @@ -459,7 +460,7 @@ def get_structure_property_dict( return sp_dict @classmethod - def from_pseudoinverse(cls, strains, stresses): + def from_pseudoinverse(cls, strains, stresses) -> Self: """ Class method to fit an elastic tensor from stress/strain data. Method uses Moore-Penrose pseudo-inverse to invert @@ -483,7 +484,7 @@ def from_pseudoinverse(cls, strains, stresses): return cls.from_voigt(voigt_fit) @classmethod - def from_independent_strains(cls, strains, stresses, eq_stress=None, vasp=False, tol: float = 1e-10): + def from_independent_strains(cls, strains, stresses, eq_stress=None, vasp=False, tol: float = 1e-10) -> Self: """ Constructs the elastic tensor least-squares fit of independent strains @@ -522,7 +523,7 @@ class ComplianceTensor(Tensor): since the compliance tensor has a unique vscale. """ - def __new__(cls, s_array): + def __new__(cls, s_array) -> Self: """ Args: s_array (): @@ -555,7 +556,7 @@ def __init__(self, c_list: Sequence) -> None: super().__init__(c_list) @classmethod - def from_diff_fit(cls, strains, stresses, eq_stress=None, tol: float = 1e-10, order=3): + def from_diff_fit(cls, strains, stresses, eq_stress=None, tol: float = 1e-10, order=3) -> Self: """ Generates an elastic tensor expansion via the fitting function defined below in diff_fit. diff --git a/pymatgen/analysis/elasticity/strain.py b/pymatgen/analysis/elasticity/strain.py index 00e428a1be0..9e04c052836 100644 --- a/pymatgen/analysis/elasticity/strain.py +++ b/pymatgen/analysis/elasticity/strain.py @@ -20,6 +20,7 @@ from collections.abc import Sequence from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core.structure import Structure @@ -38,7 +39,7 @@ class Deformation(SquareTensor): symbol = "d" - def __new__(cls, deformation_gradient): + def __new__(cls, deformation_gradient) -> Self: """ Create a Deformation object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. @@ -81,7 +82,7 @@ def apply_to_structure(self, structure: Structure): return def_struct @classmethod - def from_index_amount(cls, matrix_pos, amt): + def from_index_amount(cls, matrix_pos, amt) -> Self: """ Factory method for constructing a Deformation object from a matrix position and amount. @@ -158,7 +159,7 @@ class Strain(SquareTensor): symbol = "e" - def __new__(cls, strain_matrix): + def __new__(cls, strain_matrix) -> Self: """ Create a Strain object. Note that the constructor uses __new__ rather than __init__ according to the standard method of @@ -185,7 +186,7 @@ def __array_finalize__(self, obj): self._vscale = getattr(obj, "_vscale", None) @classmethod - def from_deformation(cls, deformation: ArrayLike) -> Strain: + def from_deformation(cls, deformation: ArrayLike) -> Self: """ Factory method that returns a Strain object from a deformation gradient. @@ -197,7 +198,7 @@ def from_deformation(cls, deformation: ArrayLike) -> Strain: return cls(0.5 * (np.dot(dfm.trans, dfm) - np.eye(3))) @classmethod - def from_index_amount(cls, idx, amount): + def from_index_amount(cls, idx: tuple | int, amount: float) -> Self: """ Like Deformation.from_index_amount, except generates a strain from the zero 3x3 tensor or Voigt vector with @@ -208,7 +209,7 @@ def from_index_amount(cls, idx, amount): idx (tuple or integer): index to be perturbed, can be Voigt or full-tensor notation amount (float): amount to perturb selected index """ - if np.array(idx).ndim == 0: + if isinstance(idx, int): v = np.zeros(6) v[idx] = amount return cls.from_voigt(v) diff --git a/pymatgen/analysis/elasticity/stress.py b/pymatgen/analysis/elasticity/stress.py index f4eaa9234fd..5502d8addc5 100644 --- a/pymatgen/analysis/elasticity/stress.py +++ b/pymatgen/analysis/elasticity/stress.py @@ -8,6 +8,7 @@ import math import numpy as np +from typing_extensions import Self from pymatgen.core.tensors import SquareTensor @@ -29,7 +30,7 @@ class Stress(SquareTensor): symbol = "s" - def __new__(cls, stress_matrix): + def __new__(cls, stress_matrix) -> Self: """ Create a Stress object. Note that the constructor uses __new__ rather than __init__ according to the standard method of diff --git a/pymatgen/analysis/ewald.py b/pymatgen/analysis/ewald.py index 3227f171868..71af766e688 100644 --- a/pymatgen/analysis/ewald.py +++ b/pymatgen/analysis/ewald.py @@ -6,7 +6,7 @@ from copy import copy, deepcopy from datetime import datetime from math import log, pi, sqrt -from typing import Any +from typing import TYPE_CHECKING, Any from warnings import warn import numpy as np @@ -17,6 +17,9 @@ from pymatgen.core.structure import Structure from pymatgen.util.due import Doi, due +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Shyue Ping Ong, William Davidson Richard" __copyright__ = "Copyright 2011, The Materials Project" __credits__ = "Christopher Fischer" @@ -444,7 +447,7 @@ def as_dict(self, verbosity: int = 0) -> dict: } @classmethod - def from_dict(cls, dct: dict[str, Any], fmt: str | None = None, **kwargs) -> EwaldSummation: + def from_dict(cls, dct: dict[str, Any], fmt: str | None = None, **kwargs) -> Self: """Create an EwaldSummation instance from JSON-serialized dictionary. Args: diff --git a/pymatgen/analysis/ferroelectricity/polarization.py b/pymatgen/analysis/ferroelectricity/polarization.py index 39f60890a7e..e6bb247b4c3 100644 --- a/pymatgen/analysis/ferroelectricity/polarization.py +++ b/pymatgen/analysis/ferroelectricity/polarization.py @@ -56,6 +56,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + from pymatgen.core.sites import PeriodicSite @@ -172,7 +174,7 @@ def __init__(self, p_elecs, p_ions, structures, p_elecs_in_cartesian=True, p_ion self.structures = structures @classmethod - def from_outcars_and_structures(cls, outcars, structures, calc_ionic_from_zval=False): + def from_outcars_and_structures(cls, outcars, structures, calc_ionic_from_zval=False) -> Self: """ Create Polarization object from list of Outcars and Structures in order of nonpolar to polar. diff --git a/pymatgen/analysis/gb/grain.py b/pymatgen/analysis/gb/grain.py index eb28ff894ef..9ba882cb8ba 100644 --- a/pymatgen/analysis/gb/grain.py +++ b/pymatgen/analysis/gb/grain.py @@ -22,6 +22,7 @@ from collections.abc import Sequence from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core.trajectory import Vector3D from pymatgen.util.typing import CompositionLike @@ -282,7 +283,7 @@ def as_dict(self): return dct @classmethod - def from_dict(cls, dct: dict) -> GrainBoundary: # type: ignore[override] + def from_dict(cls, dct: dict) -> Self: # type: ignore[override] """ Generates a GrainBoundary object from a dictionary created by as_dict(). diff --git a/pymatgen/analysis/graphs.py b/pymatgen/analysis/graphs.py index 9d3b1ec5700..05574631f6f 100644 --- a/pymatgen/analysis/graphs.py +++ b/pymatgen/analysis/graphs.py @@ -1156,8 +1156,8 @@ def __mul__(self, scaling_matrix): # this could probably be a lot smaller tol = 0.05 - for u, v, k, dct in new_g.edges(keys=True, data=True): - to_jimage = dct["to_jimage"] # for node v + for u, v, k, data in new_g.edges(keys=True, data=True): + to_jimage = data["to_jimage"] # for node v # reduce unnecessary checking if to_jimage != (0, 0, 0): @@ -1165,29 +1165,24 @@ def __mul__(self, scaling_matrix): n_u = u % len(self.structure) n_v = v % len(self.structure) - # get fractional coordinates of where atoms defined - # by edge are expected to be, relative to original - # lattice (keeping original lattice has + # get fractional coordinates of where atoms defined by edge are expected + # to be, relative to original lattice (keeping original lattice has # significant benefits) v_image_frac = np.add(self.structure[n_v].frac_coords, to_jimage) u_frac = self.structure[n_u].frac_coords - # using the position of node u as a reference, - # get relative Cartesian coordinates of where - # atoms defined by edge are expected to be + # using the position of node u as a reference, get relative Cartesian + # coordinates of where atoms defined by edge are expected to be v_image_cart = orig_lattice.get_cartesian_coords(v_image_frac) u_cart = orig_lattice.get_cartesian_coords(u_frac) v_rel = np.subtract(v_image_cart, u_cart) - # now retrieve position of node v in - # new supercell, and get asgolute Cartesian - # coordinates of where atoms defined by edge - # are expected to be - v_expec = new_structure[u].coords + v_rel + # now retrieve position of node v in new supercell, and get absolute + # Cartesian coordinates of where atoms defined by edge are expected to be + v_expect = new_structure[u].coords + v_rel - # now search in new structure for these atoms - # query returns (distance, index) - v_present = kd_tree.query(v_expec) + # now search in new structure for these atoms query returns (distance, index) + v_present = kd_tree.query(v_expect) v_present = v_present[1] if v_present[0] <= tol else None # check if image sites now present in supercell @@ -1196,10 +1191,10 @@ def __mul__(self, scaling_matrix): if v_present is not None: new_u = u new_v = v_present - new_d = dct.copy() + new_data = data.copy() # node now inside supercell - new_d["to_jimage"] = (0, 0, 0) + new_data["to_jimage"] = (0, 0, 0) edges_to_remove.append((u, v, k)) @@ -1211,7 +1206,7 @@ def __mul__(self, scaling_matrix): new_u, new_v = new_v, new_u edges_inside_supercell.append({new_u, new_v}) - edges_to_add.append((new_u, new_v, new_d)) + edges_to_add.append((new_u, new_v, new_data)) else: # want to find new_v such that we have @@ -1219,7 +1214,7 @@ def __mul__(self, scaling_matrix): # so that nodes on one side of supercell # are connected to nodes on opposite side - v_expec_frac = new_structure.lattice.get_fractional_coords(v_expec) + v_expec_frac = new_structure.lattice.get_fractional_coords(v_expect) # find new to_jimage # use np.around to fix issues with finite precision leading to incorrect image @@ -1227,27 +1222,27 @@ def __mul__(self, scaling_matrix): v_expec_image = v_expec_image - v_expec_image % 1 v_expec_frac = np.subtract(v_expec_frac, v_expec_image) - v_expec = new_structure.lattice.get_cartesian_coords(v_expec_frac) - v_present = kd_tree.query(v_expec) + v_expect = new_structure.lattice.get_cartesian_coords(v_expec_frac) + v_present = kd_tree.query(v_expect) v_present = v_present[1] if v_present[0] <= tol else None if v_present is not None: new_u = u new_v = v_present - new_d = dct.copy() + new_data = data.copy() new_to_jimage = tuple(map(int, v_expec_image)) # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u - new_to_jimage = tuple(np.multiply(-1, dct["to_jimage"]).astype(int)) + new_to_jimage = tuple(np.multiply(-1, data["to_jimage"]).astype(int)) - new_d["to_jimage"] = new_to_jimage + new_data["to_jimage"] = new_to_jimage edges_to_remove.append((u, v, k)) if (new_u, new_v, new_to_jimage) not in new_periodic_images: - edges_to_add.append((new_u, new_v, new_d)) + edges_to_add.append((new_u, new_v, new_data)) new_periodic_images.append((new_u, new_v, new_to_jimage)) logger.debug(f"Removing {len(edges_to_remove)} edges, adding {len(edges_to_add)} new edges.") @@ -1255,18 +1250,18 @@ def __mul__(self, scaling_matrix): # add/delete marked edges for edge in edges_to_remove: new_g.remove_edge(*edge) - for u, v, dct in edges_to_add: - new_g.add_edge(u, v, **dct) + for u, v, data in edges_to_add: + new_g.add_edge(u, v, **data) # return new instance of StructureGraph with supercell - dct = { + data = { "@module": type(self).__module__, "@class": type(self).__name__, "structure": new_structure.as_dict(), "graphs": json_graph.adjacency_data(new_g), } - return StructureGraph.from_dict(dct) + return type(self).from_dict(data) def __rmul__(self, other): return self.__mul__(other) @@ -1354,7 +1349,7 @@ def sort(self, key=None, reverse: bool = False) -> None: self.graph.add_edge(u, v, **d) def __copy__(self): - return StructureGraph.from_dict(self.as_dict()) + return type(self).from_dict(self.as_dict()) def __eq__(self, other: object) -> bool: """ @@ -2726,7 +2721,7 @@ def sort(self, key: Callable[[Molecule], float] | None = None, reverse: bool = F self.graph.add_edge(u, v, **data) def __copy__(self): - return MoleculeGraph.from_dict(self.as_dict()) + return type(self).from_dict(self.as_dict()) def __eq__(self, other: object) -> bool: """ diff --git a/pymatgen/analysis/interface_reactions.py b/pymatgen/analysis/interface_reactions.py index c8a19222d5c..fc69ad75df7 100644 --- a/pymatgen/analysis/interface_reactions.py +++ b/pymatgen/analysis/interface_reactions.py @@ -316,7 +316,7 @@ def _get_reaction(self, x: float) -> Reaction: return reaction - def _get_elem_amt_in_rxn(self, rxn: Reaction) -> int: + def _get_elem_amt_in_rxn(self, rxn: Reaction) -> float: """ Computes total number of atoms in a reaction formula for elements not in external reservoir. This method is used in the calculation diff --git a/pymatgen/analysis/interfaces/substrate_analyzer.py b/pymatgen/analysis/interfaces/substrate_analyzer.py index 7e069aaea09..5a528f74b20 100644 --- a/pymatgen/analysis/interfaces/substrate_analyzer.py +++ b/pymatgen/analysis/interfaces/substrate_analyzer.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core import Structure @@ -39,7 +40,7 @@ def from_zsl( substrate_miller, elasticity_tensor=None, ground_state_energy=0, - ): + ) -> Self: """Generate a substrate match from a ZSL match plus metadata.""" # Get the appropriate surface structure struct = SlabGenerator(film, film_miller, 20, 15, primitive=False).get_slab().oriented_unit_cell diff --git a/pymatgen/analysis/local_env.py b/pymatgen/analysis/local_env.py index f97c4a32c62..e90d11ddf43 100644 --- a/pymatgen/analysis/local_env.py +++ b/pymatgen/analysis/local_env.py @@ -34,6 +34,8 @@ openbabel = None if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core.composition import SpeciesLike @@ -643,9 +645,9 @@ def get_local_order_parameters(self, structure: Structure, n: int): structure: Structure object n (int): site index. - Returns (dict[str, float]): - A dict of order parameters (values) and the - underlying motif type (keys; for example, tetrahedral). + Returns: + dict[str, float]: A dict of order parameters (values) and the + underlying motif type (keys; for example, tetrahedral). """ # code from @nisse3000, moved here from graphs to avoid circular # import, also makes sense to have this as a general NN method @@ -4215,7 +4217,7 @@ def extend_structure_molecules(self) -> bool: return True @classmethod - def from_preset(cls, preset) -> CutOffDictNN: + def from_preset(cls, preset) -> Self: """ Initialize a CutOffDictNN according to a preset set of cutoffs. diff --git a/pymatgen/analysis/magnetism/analyzer.py b/pymatgen/analysis/magnetism/analyzer.py index 8ff4eb3b1c0..3388ca96d0f 100644 --- a/pymatgen/analysis/magnetism/analyzer.py +++ b/pymatgen/analysis/magnetism/analyzer.py @@ -414,8 +414,7 @@ def magmoms(self) -> np.ndarray: @property def types_of_magnetic_species(self) -> tuple[Element | Species | DummySpecies, ...]: - """Equivalent to Structure.types_of_specie but only returns - magnetic species. + """Equivalent to Structure.types_of_specie but only returns magnetic species. Returns: tuple: types of Species diff --git a/pymatgen/analysis/magnetism/heisenberg.py b/pymatgen/analysis/magnetism/heisenberg.py index 14734a158c0..90fa34130b0 100644 --- a/pymatgen/analysis/magnetism/heisenberg.py +++ b/pymatgen/analysis/magnetism/heisenberg.py @@ -34,7 +34,18 @@ class HeisenbergMapper: - """Class to compute exchange parameters from low energy magnetic orderings.""" + """Class to compute exchange parameters from low energy magnetic orderings. + + Attributes: + strategy (object): Class from pymatgen.analysis.local_env for constructing graphs. + sgraphs (list): StructureGraph objects. + unique_site_ids (dict): Maps each site to its unique numerical identifier. + wyckoff_ids (dict): Maps unique numerical identifier to wyckoff position. + nn_interactions (dict): {i: j} pairs of NN interactions between unique sites. + dists (dict): NN, NNN, and NNNN interaction distances + ex_mat (DataFrame): Invertible Heisenberg Hamiltonian for each graph. + ex_params (dict): Exchange parameter values (meV/atom) + """ def __init__(self, ordered_structures, energies, cutoff=0, tol: float = 0.02): """ @@ -57,16 +68,6 @@ def __init__(self, ordered_structures, energies, cutoff=0, tol: float = 0.02): Defaults to 0 (only NN, no NNN, etc.) tol (float): Tolerance (in Angstrom) on nearest neighbor distances being equal. - - Parameters: - strategy (object): Class from pymatgen.analysis.local_env for constructing graphs. - sgraphs (list): StructureGraph objects. - unique_site_ids (dict): Maps each site to its unique numerical identifier. - wyckoff_ids (dict): Maps unique numerical identifier to wyckoff position. - nn_interactions (dict): {i: j} pairs of NN interactions between unique sites. - dists (dict): NN, NNN, and NNNN interaction distances - ex_mat (DataFrame): Invertible Heisenberg Hamiltonian for each graph. - ex_params (dict): Exchange parameter values (meV/atom) """ # Save original copies of inputs self.ordered_structures_ = ordered_structures diff --git a/pymatgen/analysis/nmr.py b/pymatgen/analysis/nmr.py index 7dc15301146..0f3a1559c08 100644 --- a/pymatgen/analysis/nmr.py +++ b/pymatgen/analysis/nmr.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import namedtuple +from typing import TYPE_CHECKING import numpy as np @@ -11,6 +12,9 @@ from pymatgen.core.units import FloatWithUnit from pymatgen.util.due import Doi, due +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Shyam Dwaraknath" __copyright__ = "Copyright 2016, The Materials Project" __version__ = "0.2" @@ -36,7 +40,7 @@ class ChemicalShielding(SquareTensor): MehringNotation = namedtuple("MehringNotation", "sigma_iso, sigma_11, sigma_22, sigma_33") MarylandNotation = namedtuple("MarylandNotation", "sigma_iso, omega, kappa") - def __new__(cls, cs_matrix, vscale=None): + def __new__(cls, cs_matrix, vscale=None) -> Self | None: # type: ignore[misc] """ Create a Chemical Shielding tensor. Note that the constructor uses __new__ @@ -100,7 +104,7 @@ def maryland_values(self): return self.MarylandNotation(sigma_iso, omega, kappa) @classmethod - def from_maryland_notation(cls, sigma_iso, omega, kappa): + def from_maryland_notation(cls, sigma_iso, omega, kappa) -> Self: """ Initialize from Maryland notation. @@ -126,7 +130,7 @@ class ElectricFieldGradient(SquareTensor): Authors: Shyam Dwaraknath, Xiaohui Qu """ - def __new__(cls, efg_matrix, vscale=None): + def __new__(cls, efg_matrix, vscale=None) -> Self | None: # type: ignore[misc] """ Create a Chemical Shielding tensor. Note that the constructor uses __new__ diff --git a/pymatgen/analysis/phase_diagram.py b/pymatgen/analysis/phase_diagram.py index ffda294c050..60272cfe3a6 100644 --- a/pymatgen/analysis/phase_diagram.py +++ b/pymatgen/analysis/phase_diagram.py @@ -44,7 +44,7 @@ logger = logging.getLogger(__name__) -with open(os.path.join(os.path.dirname(__file__), "..", "util", "plotly_pd_layouts.json")) as file: +with open(os.path.join(os.path.dirname(__file__), "..", "util", "plotly_pd_layouts.json"), encoding="utf-8") as file: plotly_layouts = json.load(file) @@ -409,7 +409,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict[str, Any]) -> PhaseDiagram: + def from_dict(cls, dct: dict[str, Any]) -> Self: """ Args: dct (dict): dictionary representation of PhaseDiagram. diff --git a/pymatgen/analysis/piezo.py b/pymatgen/analysis/piezo.py index a29d393abf4..2f168e1eef4 100644 --- a/pymatgen/analysis/piezo.py +++ b/pymatgen/analysis/piezo.py @@ -3,11 +3,16 @@ from __future__ import annotations import warnings +from typing import TYPE_CHECKING import numpy as np from pymatgen.core.tensors import Tensor +if TYPE_CHECKING: + from numpy.typing import ArrayLike + from typing_extensions import Self + __author__ = "Shyam Dwaraknath" __copyright__ = "Copyright 2016, The Materials Project" __version__ = "1.0" @@ -20,7 +25,7 @@ class PiezoTensor(Tensor): """This class describes the 3x6 piezo tensor in Voigt notation.""" - def __new__(cls, input_array, tol: float = 1e-3): + def __new__(cls, input_array: ArrayLike, tol: float = 1e-3) -> Self: """ Create an PiezoTensor object. The constructor throws an error if the shape of the input_matrix argument is not 3x3x3, i. e. in true @@ -38,7 +43,7 @@ def __new__(cls, input_array, tol: float = 1e-3): return obj.view(cls) @classmethod - def from_vasp_voigt(cls, input_vasp_array): + def from_vasp_voigt(cls, input_vasp_array: ArrayLike) -> Self: """ Args: input_vasp_array (nd.array): Voigt form of tensor. diff --git a/pymatgen/analysis/pourbaix_diagram.py b/pymatgen/analysis/pourbaix_diagram.py index 2c04df3b3ca..c6fb4c31325 100644 --- a/pymatgen/analysis/pourbaix_diagram.py +++ b/pymatgen/analysis/pourbaix_diagram.py @@ -130,20 +130,13 @@ def name(self): @property def energy(self): - """ - Returns (float): total energy of the Pourbaix - entry (at pH, V = 0 vs. SHE). - """ + """Total energy of the Pourbaix entry (at pH, V = 0 vs. SHE).""" # Note: this implicitly depends on formation energies as input return self.uncorrected_energy + self.conc_term - (MU_H2O * self.nH2O) @property def energy_per_atom(self): - """ - energy per atom of the Pourbaix entry. - - Returns (float): energy per atom - """ + """Energy per atom of the Pourbaix entry.""" return self.energy / self.composition.num_atoms @property diff --git a/pymatgen/analysis/prototypes.py b/pymatgen/analysis/prototypes.py index 584330a0ef1..01ad61fcfd5 100644 --- a/pymatgen/analysis/prototypes.py +++ b/pymatgen/analysis/prototypes.py @@ -100,10 +100,11 @@ def get_prototypes(self, structure: Structure) -> list | None: Args: structure: structure to match - Returns (list): A list of dicts with keys 'snl' for the matched prototype and - 'tags', a dict of tags ('mineral', 'strukturbericht' and 'aflow') of that - prototype. This should be a list containing just a single entry, but it is - possible a material can match multiple prototypes. + Returns: + list | None: A list of dicts with keys 'snl' for the matched prototype and + 'tags', a dict of tags ('mineral', 'strukturbericht' and 'aflow') of that + prototype. This should be a list containing just a single entry, but it is + possible a material can match multiple prototypes. """ tags = self._match_single_prototype(structure) diff --git a/pymatgen/analysis/quasirrho.py b/pymatgen/analysis/quasirrho.py index 4de59a992ed..deefb2fd062 100644 --- a/pymatgen/analysis/quasirrho.py +++ b/pymatgen/analysis/quasirrho.py @@ -10,14 +10,6 @@ from __future__ import annotations -__author__ = "Alex Epstein" -__copyright__ = "Copyright 2020, The Materials Project" -__version__ = "0.1" -__maintainer__ = "Alex Epstein" -__email__ = "aepstein@lbl.gov" -__date__ = "August 1, 2023" -__credits__ = "Ryan Kingsbury, Steven Wheeler, Trevor Seguin, Evan Spotte-Smith" - from math import isclose from typing import TYPE_CHECKING @@ -28,10 +20,20 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core import Molecule from pymatgen.io.gaussian import GaussianOutput from pymatgen.io.qchem.outputs import QCOutput +__author__ = "Alex Epstein" +__copyright__ = "Copyright 2020, The Materials Project" +__version__ = "0.1" +__maintainer__ = "Alex Epstein" +__email__ = "aepstein@lbl.gov" +__date__ = "August 1, 2023" +__credits__ = "Ryan Kingsbury, Steven Wheeler, Trevor Seguin, Evan Spotte-Smith" + # Define useful constants kb = kb_ev * const.eV # Pymatgen kb [J/K] light_speed = const.speed_of_light * 100 # [cm/s] @@ -133,7 +135,7 @@ def __init__( self._get_quasirrho_thermo(mol=mol, mult=mult, frequencies=frequencies, elec_energy=energy, sigma_r=sigma_r) @classmethod - def from_gaussian_output(cls, output: GaussianOutput, **kwargs) -> QuasiRRHO: + def from_gaussian_output(cls, output: GaussianOutput, **kwargs) -> Self: """ Args: output (GaussianOutput): Pymatgen GaussianOutput object @@ -148,7 +150,7 @@ def from_gaussian_output(cls, output: GaussianOutput, **kwargs) -> QuasiRRHO: return cls(mol=mol, frequencies=vib_freqs, energy=elec_e, mult=mult, **kwargs) @classmethod - def from_qc_output(cls, output: QCOutput, **kwargs) -> QuasiRRHO: + def from_qc_output(cls, output: QCOutput, **kwargs) -> Self: """ Args: output (QCOutput): Pymatgen QCOutput object diff --git a/pymatgen/analysis/reaction_calculator.py b/pymatgen/analysis/reaction_calculator.py index 1968100b2c2..be8fc3150f5 100644 --- a/pymatgen/analysis/reaction_calculator.py +++ b/pymatgen/analysis/reaction_calculator.py @@ -5,7 +5,7 @@ import logging import re from itertools import chain, combinations -from typing import TYPE_CHECKING, no_type_check +from typing import TYPE_CHECKING, no_type_check, overload import numpy as np from monty.fractions import gcd_float @@ -20,6 +20,7 @@ from typing_extensions import Self + from pymatgen.core import Element, Species from pymatgen.util.typing import CompositionLike __author__ = "Shyue Ping Ong, Anubhav Jain" @@ -70,7 +71,7 @@ def __init__( # calculate net reaction coefficients self._coeffs: list[float] = [] - self._els: list[str] = [] + self._els: list[Element | Species] = [] self._all_comp: list[Composition] = [] for key in {*reactants_coeffs, *products_coeffs}: coeff = products_coeffs.get(key, 0) - reactants_coeffs.get(key, 0) @@ -79,6 +80,32 @@ def __init__( self._all_comp += [key] self._coeffs += [coeff] + def __eq__(self, other: object) -> bool: + if not isinstance(other, type(self)): + return NotImplemented + for comp in self._all_comp: + coeff2 = other.get_coeff(comp) if comp in other._all_comp else 0 + if abs(self.get_coeff(comp) - coeff2) > self.TOLERANCE: + return False + return True + + def __hash__(self) -> int: + # Necessity for hash method is unclear (see gh-3673) + return hash((frozenset(self.reactants_coeffs.items()), frozenset(self.products_coeffs.items()))) + + def __str__(self): + return self._str_from_comp(self._coeffs, self._all_comp)[0] + + __repr__ = __str__ + + @overload + def calculate_energy(self, energies: dict[Composition, ufloat]) -> ufloat: + pass + + @overload + def calculate_energy(self, energies: dict[Composition, float]) -> float: + pass + def calculate_energy(self, energies): """ Calculates the energy of the reaction. @@ -92,7 +119,7 @@ def calculate_energy(self, energies): """ return sum(amt * energies[c] for amt, c in zip(self._coeffs, self._all_comp)) - def normalize_to(self, comp, factor=1): + def normalize_to(self, comp: Composition, factor: float = 1) -> None: """ Normalizes the reaction to one of the compositions. By default, normalizes such that the composition given has a @@ -105,7 +132,7 @@ def normalize_to(self, comp, factor=1): scale_factor = abs(1 / self._coeffs[self._all_comp.index(comp)] * factor) self._coeffs = [c * scale_factor for c in self._coeffs] - def normalize_to_element(self, element, factor=1): + def normalize_to_element(self, element: Species | Element, factor: float = 1) -> None: """ Normalizes the reaction to one of the elements. By default, normalizes such that the amount of the element is 1. @@ -121,7 +148,7 @@ def normalize_to_element(self, element, factor=1): scale_factor = factor / current_el_amount self._coeffs = [c * scale_factor for c in coeffs] - def get_el_amount(self, element): + def get_el_amount(self, element: Element | Species) -> float: """ Returns the amount of the element in the reaction. @@ -134,35 +161,35 @@ def get_el_amount(self, element): return sum(self._all_comp[i][element] * abs(self._coeffs[i]) for i in range(len(self._all_comp))) / 2 @property - def elements(self): + def elements(self) -> list[Element | Species]: """List of elements in the reaction.""" - return self._els[:] + return self._els @property - def coeffs(self): + def coeffs(self) -> list[float]: """Final coefficients of the calculated reaction.""" return self._coeffs[:] @property - def all_comp(self): + def all_comp(self) -> list[Composition]: """List of all compositions in the reaction.""" return self._all_comp @property - def reactants(self): + def reactants(self) -> list[Composition]: """List of reactants.""" return [self._all_comp[i] for i in range(len(self._all_comp)) if self._coeffs[i] < 0] @property - def products(self): + def products(self) -> list[Composition]: """List of products.""" return [self._all_comp[i] for i in range(len(self._all_comp)) if self._coeffs[i] > 0] - def get_coeff(self, comp): + def get_coeff(self, comp: Composition) -> float: """Returns coefficient for a particular composition.""" return self._coeffs[self._all_comp.index(comp)] - def normalized_repr_and_factor(self): + def normalized_repr_and_factor(self) -> tuple[str, float]: """ Normalized representation for a reaction For example, ``4 Li + 2 O -> 2Li2O`` becomes ``2 Li + O -> Li2O``. @@ -170,28 +197,15 @@ def normalized_repr_and_factor(self): return self._str_from_comp(self._coeffs, self._all_comp, reduce=True) @property - def normalized_repr(self): + def normalized_repr(self) -> str: """ A normalized representation of the reaction. All factors are converted to lowest common factors. """ return self.normalized_repr_and_factor()[0] - def __eq__(self, other: object) -> bool: - if not isinstance(other, type(self)): - return NotImplemented - for comp in self._all_comp: - coeff2 = other.get_coeff(comp) if comp in other._all_comp else 0 - if abs(self.get_coeff(comp) - coeff2) > self.TOLERANCE: - return False - return True - - def __hash__(self) -> int: - # Necessity for hash method is unclear (see gh-3673) - return hash((frozenset(self.reactants_coeffs.items()), frozenset(self.products_coeffs.items()))) - @classmethod - def _str_from_formulas(cls, coeffs, formulas): + def _str_from_formulas(cls, coeffs, formulas) -> str: reactant_str = [] product_str = [] for amt, formula in zip(coeffs, formulas): @@ -207,7 +221,7 @@ def _str_from_formulas(cls, coeffs, formulas): return f"{' + '.join(reactant_str)} -> {' + '.join(product_str)}" @classmethod - def _str_from_comp(cls, coeffs, compositions, reduce=False): + def _str_from_comp(cls, coeffs, compositions, reduce=False) -> tuple[str, float]: r_coeffs = np.zeros(len(coeffs)) r_formulas = [] for idx, (amt, comp) in enumerate(zip(coeffs, compositions)): @@ -221,22 +235,18 @@ def _str_from_comp(cls, coeffs, compositions, reduce=False): factor = 1 return cls._str_from_formulas(r_coeffs, r_formulas), factor - def __str__(self): - return self._str_from_comp(self._coeffs, self._all_comp)[0] - - __repr__ = __str__ - - def as_entry(self, energies): + def as_entry(self, energies) -> ComputedEntry: """ Returns a ComputedEntry representation of the reaction. """ relevant_comp = [comp * abs(coeff) for coeff, comp in zip(self._coeffs, self._all_comp)] - comp = sum(relevant_comp, Composition()) + comp: Composition = sum(relevant_comp, Composition()) # type: ignore[assignment] + entry = ComputedEntry(0.5 * comp, self.calculate_energy(energies)) entry.name = str(self) return entry - def as_dict(self): + def as_dict(self) -> dict: """ Returns: A dictionary representation of BalancedReaction. @@ -262,7 +272,7 @@ def from_dict(cls, dct: dict) -> Self: return cls(reactants, products) @classmethod - def from_str(cls, rxn_str): + def from_str(cls, rxn_str: str) -> Self: """ Generates a balanced reaction from a string. The reaction must already be balanced. @@ -293,7 +303,7 @@ class Reaction(BalancedReaction): the *FIRST* product (or products, if underdetermined) has a coefficient of one. """ - def __init__(self, reactants, products): + def __init__(self, reactants: list[Composition], products: list[Composition]) -> None: """ Reactants and products to be specified as list of pymatgen.core.structure.Composition. e.g., [comp1, comp2]. @@ -367,11 +377,11 @@ def _balance_coeffs(self, comp_matrix, max_num_constraints): return np.squeeze(best_soln) - def copy(self): + def copy(self) -> Self: """Returns a copy of the Reaction object.""" return Reaction(self.reactants, self.products) - def as_dict(self): + def as_dict(self) -> dict: """ Returns: A dictionary representation of Reaction. @@ -403,7 +413,7 @@ class ReactionError(Exception): messages to cover situations not covered by standard exception classes. """ - def __init__(self, msg): + def __init__(self, msg: str) -> None: """ Create a ReactionError. @@ -412,7 +422,7 @@ def __init__(self, msg): """ self.msg = msg - def __str__(self): + def __str__(self) -> str: return self.msg @@ -423,7 +433,7 @@ class ComputedReaction(Reaction): energies. """ - def __init__(self, reactant_entries, product_entries): + def __init__(self, reactant_entries: list[ComputedEntry], product_entries: list[ComputedEntry]) -> None: """ Args: reactant_entries ([ComputedEntry]): List of reactant_entries. @@ -453,12 +463,12 @@ def all_entries(self): return entries @property - def calculated_reaction_energy(self): + def calculated_reaction_energy(self) -> float: """ - Returns (float): - The calculated reaction energy. + Returns: + float: The calculated reaction energy. """ - calc_energies = {} + calc_energies: dict[Composition, float] = {} for entry in self._reactant_entries + self._product_entries: comp, factor = entry.composition.get_reduced_composition_and_factor() @@ -467,12 +477,12 @@ def calculated_reaction_energy(self): return self.calculate_energy(calc_energies) @property - def calculated_reaction_energy_uncertainty(self): + def calculated_reaction_energy_uncertainty(self) -> float: """ Calculates the uncertainty in the reaction energy based on the uncertainty in the energies of the products and reactants. """ - calc_energies = {} + calc_energies: dict[Composition, float] = {} for entry in self._reactant_entries + self._product_entries: comp, factor = entry.composition.get_reduced_composition_and_factor() @@ -481,7 +491,7 @@ def calculated_reaction_energy_uncertainty(self): return self.calculate_energy(calc_energies).std_dev - def as_dict(self): + def as_dict(self) -> dict: """ Returns: A dictionary representation of ComputedReaction. diff --git a/pymatgen/analysis/surface_analysis.py b/pymatgen/analysis/surface_analysis.py index 10ecb279a1e..9b53c8c35fc 100644 --- a/pymatgen/analysis/surface_analysis.py +++ b/pymatgen/analysis/surface_analysis.py @@ -176,7 +176,8 @@ def surface_energy(self, ucell_entry, ref_entries=None): of the element ref_entry that is not in the list will be treated as a variable. - Returns (Add (Sympy class)): Surface energy + Returns: + float: The surface energy of the slab. """ # Set up ref_entries = ref_entries if ref_entries else [] @@ -329,7 +330,7 @@ def create_slab_label(self): @classmethod def from_computed_structure_entry( cls, entry, miller_index, label=None, adsorbates=None, clean_entry=None, **kwargs - ): + ) -> Self: """Returns SlabEntry from a ComputedStructureEntry.""" return cls( entry.structure, @@ -1568,7 +1569,7 @@ def is_converged(self, min_points_frac=0.015, tol: float = 0.0025): return all(all_flat) @classmethod - def from_files(cls, poscar_filename, locpot_filename, outcar_filename, shift=0, blength=3.5): + def from_files(cls, poscar_filename, locpot_filename, outcar_filename, shift=0, blength=3.5) -> Self: """ Initializes a WorkFunctionAnalyzer from POSCAR, LOCPOT, and OUTCAR files. diff --git a/pymatgen/analysis/transition_state.py b/pymatgen/analysis/transition_state.py index b9dc6675629..56be52650a9 100644 --- a/pymatgen/analysis/transition_state.py +++ b/pymatgen/analysis/transition_state.py @@ -10,6 +10,7 @@ import os from glob import glob +from typing import TYPE_CHECKING import matplotlib.pyplot as plt import numpy as np @@ -21,6 +22,9 @@ from pymatgen.io.vasp import Outcar from pymatgen.util.plotting import pretty_plot +if TYPE_CHECKING: + from typing_extensions import Self + class NEBAnalysis(MSONable): """An NEBAnalysis class.""" @@ -82,7 +86,7 @@ def setup_spline(self, spline_options=None): self.spline = CubicSpline(x=self.r, y=relative_energies, bc_type=((1, 0.0), (1, 0.0))) @classmethod - def from_outcars(cls, outcars, structures, **kwargs): + def from_outcars(cls, outcars, structures, **kwargs) -> Self: """ Initializes an NEBAnalysis from Outcar and Structure objects. Use the static constructors, e.g., from_dir instead if you @@ -188,7 +192,7 @@ def get_plot(self, normalize_rxn_coordinate: bool = True, label_barrier: bool = return ax @classmethod - def from_dir(cls, root_dir, relaxation_dirs=None, **kwargs): + def from_dir(cls, root_dir, relaxation_dirs=None, **kwargs) -> Self: """ Initializes a NEBAnalysis object from a directory of a NEB run. Note that OUTCARs must be present in all image directories. For the diff --git a/pymatgen/analysis/xps.py b/pymatgen/analysis/xps.py index 1df46ad9b21..e1ec8399b2d 100644 --- a/pymatgen/analysis/xps.py +++ b/pymatgen/analysis/xps.py @@ -31,6 +31,8 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.electronic_structure.dos import CompleteDos @@ -76,7 +78,7 @@ class XPS(Spectrum): YLABEL = "Intensity" @classmethod - def from_dos(cls, dos: CompleteDos): + def from_dos(cls, dos: CompleteDos) -> Self: """ Args: dos: CompleteDos object with project element-orbital DOS. diff --git a/pymatgen/apps/battery/conversion_battery.py b/pymatgen/apps/battery/conversion_battery.py index 8fbc99b1a52..b59bcc0bbec 100644 --- a/pymatgen/apps/battery/conversion_battery.py +++ b/pymatgen/apps/battery/conversion_battery.py @@ -16,6 +16,8 @@ if TYPE_CHECKING: from collections.abc import Iterable + from typing_extensions import Self + from pymatgen.entries.computed_entries import ComputedEntry @@ -43,7 +45,13 @@ def initial_comp(self) -> Composition: return Composition(self.initial_comp_formula) @classmethod - def from_composition_and_pd(cls, comp, pd, working_ion_symbol="Li", allow_unstable=False): + def from_composition_and_pd( + cls, + comp, + pd: PhaseDiagram, + working_ion_symbol: str = "Li", + allow_unstable: bool = False, + ) -> Self | None: """Convenience constructor to make a ConversionElectrode from a composition and a phase diagram. @@ -71,14 +79,16 @@ def from_composition_and_pd(cls, comp, pd, working_ion_symbol="Li", allow_unstab profile.reverse() if len(profile) < 2: return None - working_ion = working_ion_entry.elements[0].symbol - normalization_els = {el: amt for el, amt in comp.items() if el != Element(working_ion)} + + assert working_ion_entry is not None + working_ion_symbol = working_ion_entry.elements[0].symbol + normalization_els = {el: amt for el, amt in comp.items() if el != Element(working_ion_symbol)} framework = comp.as_dict() - if working_ion in framework: - framework.pop(working_ion) + if working_ion_symbol in framework: + framework.pop(working_ion_symbol) framework = Composition(framework) - v_pairs = [ + v_pairs: list[ConversionVoltagePair] = [ ConversionVoltagePair.from_steps( profile[i], profile[i + 1], @@ -88,15 +98,17 @@ def from_composition_and_pd(cls, comp, pd, working_ion_symbol="Li", allow_unstab for i in range(len(profile) - 1) ] - return ConversionElectrode( - voltage_pairs=v_pairs, + return cls( + voltage_pairs=v_pairs, # type: ignore[arg-type] working_ion_entry=working_ion_entry, initial_comp_formula=comp.reduced_formula, framework_formula=framework.reduced_formula, ) @classmethod - def from_composition_and_entries(cls, comp, entries_in_chemsys, working_ion_symbol="Li", allow_unstable=False): + def from_composition_and_entries( + cls, comp, entries_in_chemsys, working_ion_symbol="Li", allow_unstable=False + ) -> Self | None: """Convenience constructor to make a ConversionElectrode from a composition and all entries in a chemical system. @@ -111,7 +123,7 @@ def from_composition_and_entries(cls, comp, entries_in_chemsys, working_ion_symb for comparing with insertion electrodes """ pd = PhaseDiagram(entries_in_chemsys) - return ConversionElectrode.from_composition_and_pd(comp, pd, working_ion_symbol, allow_unstable) + return cls.from_composition_and_pd(comp, pd, working_ion_symbol, allow_unstable) def get_sub_electrodes(self, adjacent_only=True): """If this electrode contains multiple voltage steps, then it is possible @@ -278,7 +290,7 @@ class ConversionVoltagePair(AbstractVoltagePair): entries_discharge: Iterable[ComputedEntry] @classmethod - def from_steps(cls, step1, step2, normalization_els, framework_formula): + def from_steps(cls, step1, step2, normalization_els, framework_formula) -> Self: """Creates a ConversionVoltagePair from two steps in the element profile from a PD analysis. @@ -350,7 +362,7 @@ def from_steps(cls, step1, step2, normalization_els, framework_formula): entries_charge = step1["entries"] entries_discharge = step2["entries"] - return ConversionVoltagePair( + return cls( rxn=rxn, voltage=voltage, mAh=mAh, diff --git a/pymatgen/apps/battery/insertion_battery.py b/pymatgen/apps/battery/insertion_battery.py index aa34b4bd31c..74e97031726 100644 --- a/pymatgen/apps/battery/insertion_battery.py +++ b/pymatgen/apps/battery/insertion_battery.py @@ -20,6 +20,8 @@ if TYPE_CHECKING: from collections.abc import Iterable + from typing_extensions import Self + __author__ = "Anubhav Jain, Shyue Ping Ong" __copyright__ = "Copyright 2012, The Materials Project" @@ -40,7 +42,7 @@ def from_entries( entries: Iterable[ComputedEntry | ComputedStructureEntry], working_ion_entry: ComputedEntry | ComputedStructureEntry | PDEntry, strip_structures: bool = False, - ): + ) -> Self: """Create a new InsertionElectrode. Args: @@ -356,7 +358,7 @@ def __repr__(self): ) @classmethod - def from_dict_legacy(cls, dct): + def from_dict_legacy(cls, dct) -> Self: """ Args: dct (dict): Dict representation. @@ -387,7 +389,7 @@ class InsertionVoltagePair(AbstractVoltagePair): entry_discharge: ComputedEntry @classmethod - def from_entries(cls, entry1, entry2, working_ion_entry): + def from_entries(cls, entry1, entry2, working_ion_entry) -> Self: """ Args: entry1: Entry corresponding to one of the entries in the voltage step. diff --git a/pymatgen/cli/pmg_config.py b/pymatgen/cli/pmg_config.py index 3ec919857af..b5cfe0104e0 100755 --- a/pymatgen/cli/pmg_config.py +++ b/pymatgen/cli/pmg_config.py @@ -31,8 +31,7 @@ def setup_cp2k_data(cp2k_data_dirs: list[str]) -> None: except OSError: reply = input("Destination directory exists. Continue (y/n)?") if reply != "y": - print("Exiting ...") - raise SystemExit(0) + raise SystemExit("Exiting ...") print("Generating pymatgen resource directory for CP2K...") basis_files = glob(f"{data_dir}/*BASIS*") @@ -42,7 +41,7 @@ def setup_cp2k_data(cp2k_data_dirs: list[str]) -> None: for potential_file in potential_files: print(f"Processing... {potential_file}") - with open(potential_file) as file: + with open(potential_file, encoding="utf-8") as file: try: chunks = chunk(file.read()) except IndexError: @@ -52,9 +51,10 @@ def setup_cp2k_data(cp2k_data_dirs: list[str]) -> None: potential = GthPotential.from_str(chk) potential.filename = os.path.basename(potential_file) potential.version = None - settings[potential.element.symbol]["potentials"][potential.get_hash()] = jsanitize( - potential, strict=True - ) + if potential.element is not None: + settings[potential.element.symbol]["potentials"][potential.get_hash()] = jsanitize( + potential, strict=True + ) except ValueError: # Chunk was readable, but the element is not pmg recognized continue @@ -64,7 +64,7 @@ def setup_cp2k_data(cp2k_data_dirs: list[str]) -> None: for basis_file in basis_files: print(f"Processing... {basis_file}") - with open(basis_file) as file: + with open(basis_file, encoding="utf-8") as file: try: chunks = chunk(file.read()) except IndexError: @@ -87,7 +87,7 @@ def setup_cp2k_data(cp2k_data_dirs: list[str]) -> None: for el in settings: print(f"Writing {el} settings file") - with open(os.path.join(target_dir, el), mode="w") as file: + with open(os.path.join(target_dir, el), mode="w", encoding="utf-8") as file: yaml.dump(settings.get(el), file, default_flow_style=False) print( @@ -111,8 +111,7 @@ def setup_potcars(potcar_dirs: list[str]): except OSError: reply = input("Destination directory exists. Continue (y/n)? ") if reply != "y": - print("Exiting ...") - raise SystemExit(0) + raise SystemExit("Exiting ...") print("Generating pymatgen resources directory...") diff --git a/pymatgen/command_line/bader_caller.py b/pymatgen/command_line/bader_caller.py index b0b33d60ae8..18c5413a437 100644 --- a/pymatgen/command_line/bader_caller.py +++ b/pymatgen/command_line/bader_caller.py @@ -34,6 +34,8 @@ from pymatgen.io.vasp.outputs import Chgcar if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core import Structure __author__ = "shyuepingong" @@ -88,7 +90,7 @@ def temp_decompress(file: str | Path, target_dir: str = ".") -> str: """Utility function to copy a compressed file to a target directory (ScratchDir) and decompress it, to avoid modifying files in place. - Parameters: + Args: file (str | Path): The path to the compressed file to be decompressed. target_dir (str, optional): The target directory where the decompressed file will be stored. Defaults to "." (current directory). @@ -441,7 +443,7 @@ def summary(self) -> dict[str, Any]: return summary @classmethod - def from_path(cls, path: str, suffix: str = "") -> BaderAnalysis: + def from_path(cls, path: str, suffix: str = "") -> Self: """Convenient constructor that takes in the path name of VASP run to perform Bader analysis. diff --git a/pymatgen/command_line/vampire_caller.py b/pymatgen/command_line/vampire_caller.py index 7eac6877853..46caba678fb 100644 --- a/pymatgen/command_line/vampire_caller.py +++ b/pymatgen/command_line/vampire_caller.py @@ -74,7 +74,7 @@ def __init__( If False, attempt to use NN, NNN, etc. interactions. user_input_settings (dict): optional commands for VAMPIRE Monte Carlo - Parameters: + Attributes: sgraph (StructureGraph): Ground state graph. unique_site_ids (dict): Maps each site to its unique identifier nn_interactions (dict): {i: j} pairs of NN interactions diff --git a/pymatgen/core/composition.py b/pymatgen/core/composition.py index 848aded0315..bf51dc81fb3 100644 --- a/pymatgen/core/composition.py +++ b/pymatgen/core/composition.py @@ -12,7 +12,7 @@ from functools import total_ordering from itertools import combinations_with_replacement, product from math import isnan -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, cast from monty.fractions import gcd, gcd_float from monty.json import MSONable @@ -25,7 +25,10 @@ if TYPE_CHECKING: from collections.abc import Generator, Iterator -SpeciesLike = Union[str, Element, Species, DummySpecies] + from typing_extensions import Self + + from pymatgen.util.typing import SpeciesLike + module_dir = os.path.dirname(os.path.abspath(__file__)) @@ -617,7 +620,7 @@ def __repr__(self) -> str: return f"{cls_name}({formula!r})" @classmethod - def from_dict(cls, dct) -> Composition: + def from_dict(cls, dct: dict) -> Self: """Creates a composition from a dict generated by as_dict(). Strictly not necessary given that the standard constructor already takes in such an input, but this method preserves the standard pymatgen API of having @@ -630,7 +633,7 @@ def from_dict(cls, dct) -> Composition: return cls(dct) @classmethod - def from_weight_dict(cls, weight_dict) -> Composition: + def from_weight_dict(cls, weight_dict: dict[SpeciesLike, float]) -> Self: """Creates a Composition based on a dict of atomic fractions calculated from a dict of weight fractions. Allows for quick creation of the class from weight-based notations commonly used in the industry, such as diff --git a/pymatgen/core/interface.py b/pymatgen/core/interface.py index a75067347b9..aeeca3cd999 100644 --- a/pymatgen/core/interface.py +++ b/pymatgen/core/interface.py @@ -3,6 +3,7 @@ from __future__ import annotations from itertools import chain, combinations, product +from typing import TYPE_CHECKING import numpy as np from numpy.testing import assert_allclose @@ -16,6 +17,9 @@ from pymatgen.core.surface import Slab from pymatgen.symmetry.analyzer import SpacegroupAnalyzer +if TYPE_CHECKING: + from typing_extensions import Self + class Interface(Structure): """This class stores data for defining an interface between two structures. @@ -282,7 +286,7 @@ def as_dict(self): return dct @classmethod - def from_dict(cls, dct: dict) -> Interface: # type: ignore[override] + def from_dict(cls, dct: dict) -> Self: # type: ignore[override] """ Args: dct: dict. @@ -318,7 +322,7 @@ def from_slabs( vacuum_over_film: float = 0, interface_properties: dict | None = None, center_slab: bool = True, - ) -> Interface: + ) -> Self: """Makes an interface structure by merging a substrate and film slabs The film a- and b-vectors will be forced to be the substrate slab's a- and b-vectors. diff --git a/pymatgen/core/ion.py b/pymatgen/core/ion.py index 121588e5380..6fd41b174b7 100644 --- a/pymatgen/core/ion.py +++ b/pymatgen/core/ion.py @@ -4,12 +4,16 @@ import re from copy import deepcopy +from typing import TYPE_CHECKING from monty.json import MSONable from pymatgen.core.composition import Composition, reduce_formula from pymatgen.util.string import Stringify, charge_string, formula_double_format +if TYPE_CHECKING: + from typing_extensions import Self + class Ion(Composition, MSONable, Stringify): """Ion object. Just a Composition object with an additional variable to store @@ -19,7 +23,7 @@ class Ion(Composition, MSONable, Stringify): Mn[+2]. Note the order of the sign and magnitude in each representation. """ - def __init__(self, composition, charge=0.0, _properties=None) -> None: + def __init__(self, composition: Composition, charge: float = 0.0) -> None: """Flexible Ion construction, similar to Composition. For more information, please see pymatgen.core.Composition. """ @@ -27,7 +31,7 @@ def __init__(self, composition, charge=0.0, _properties=None) -> None: self._charge = charge @classmethod - def from_formula(cls, formula: str) -> Ion: + def from_formula(cls, formula: str) -> Self: """Creates Ion from formula. The net charge can either be represented as Mn++, Mn+2, Mn[2+], Mn[++], or Mn[+2]. Note the order of the sign and magnitude in each representation. @@ -120,8 +124,8 @@ def get_reduced_formula_and_factor(self, iupac_ordering: bool = False, hydrates: Ions containing metals. Returns: - A pretty normalized formula and a multiplicative factor, i.e., - H4O4 returns ('H2O2', 2.0). + tuple[str, float]: A pretty normalized formula and a multiplicative factor, i.e., + H4O4 returns ('H2O2', 2.0). """ all_int = all(abs(x - round(x)) < Composition.amount_tolerance for x in self.values()) if not all_int: @@ -212,7 +216,7 @@ def as_dict(self) -> dict[str, float]: return dct @classmethod - def from_dict(cls, dct) -> Ion: + def from_dict(cls, dct: dict) -> Self: """Generates an ion object from a dict created by as_dict(). Args: diff --git a/pymatgen/core/lattice.py b/pymatgen/core/lattice.py index f3e781a8453..bb2f463084a 100644 --- a/pymatgen/core/lattice.py +++ b/pymatgen/core/lattice.py @@ -319,6 +319,7 @@ def from_parameters( alpha: float, beta: float, gamma: float, + *, # help mypy separate positional and keyword-only arguments vesta: bool = False, pbc: PbcLike = (True, True, True), ) -> Self: @@ -431,7 +432,7 @@ def volume(self) -> float: @property def parameters(self) -> tuple[float, float, float, float, float, float]: - """Returns (a, b, c, alpha, beta, gamma).""" + """Returns 6-tuple of floats (a, b, c, alpha, beta, gamma).""" return (*self.lengths, *self.angles) @property diff --git a/pymatgen/core/libxcfunc.py b/pymatgen/core/libxcfunc.py index 2aee7408c4d..b60b112c17b 100644 --- a/pymatgen/core/libxcfunc.py +++ b/pymatgen/core/libxcfunc.py @@ -10,9 +10,13 @@ import json import os from enum import Enum, unique +from typing import TYPE_CHECKING from monty.json import MontyEncoder +if TYPE_CHECKING: + from typing_extensions import Self + # The libxc version used to generate this file! libxc_version = "3.0.0" @@ -25,7 +29,7 @@ __date__ = "May 16, 2016" # Loads libxc info from json file -with open(os.path.join(os.path.dirname(__file__), "libxc_docs.json")) as file: +with open(os.path.join(os.path.dirname(__file__), "libxc_docs.json"), encoding="utf-8") as file: _all_xcfuncs = {int(k): v for k, v in json.load(file).items()} @@ -486,7 +490,7 @@ def as_dict(self): return {"name": self.name, "@module": type(self).__module__, "@class": type(self).__name__} @classmethod - def from_dict(cls, dct: dict) -> LibxcFunc: + def from_dict(cls, dct: dict) -> Self: """Deserialize from MSONable dict representation.""" return cls[dct["name"]] diff --git a/pymatgen/core/operations.py b/pymatgen/core/operations.py index 06b05e85cb1..feced5a7c69 100644 --- a/pymatgen/core/operations.py +++ b/pymatgen/core/operations.py @@ -424,7 +424,7 @@ def as_xyz_str(self) -> str: return transformation_to_string(self.rotation_matrix, translation_vec=self.translation_vector, delim=", ") @classmethod - def from_xyz_str(cls, xyz_str: str) -> SymmOp: + def from_xyz_str(cls, xyz_str: str) -> Self: """ Args: xyz_str: string of the form 'x, y, z', '-x, -y, z', '-2y+1/2, 3x+1/2, z-y+1/2', etc. @@ -453,7 +453,7 @@ def from_xyz_str(cls, xyz_str: str) -> SymmOp: return cls.from_rotation_and_translation(rot_matrix, trans) @classmethod - def from_dict(cls, dct) -> SymmOp: + def from_dict(cls, dct) -> Self: """ Args: dct: dict. @@ -542,7 +542,7 @@ class or as list or np array-like return Magmom.from_global_moment_and_saxis(transformed_moment, magmom.saxis) @classmethod - def from_symmop(cls, symmop: SymmOp, time_reversal) -> MagSymmOp: + def from_symmop(cls, symmop: SymmOp, time_reversal) -> Self: """Initialize a MagSymmOp from a SymmOp and time reversal operator. Args: @@ -579,7 +579,7 @@ def from_rotation_and_translation_and_time_reversal( return MagSymmOp.from_symmop(symm_op, time_reversal) @classmethod - def from_xyzt_str(cls, xyzt_str: str) -> MagSymmOp: + def from_xyzt_str(cls, xyzt_str: str) -> Self: """ Args: xyzt_str (str): of the form 'x, y, z, +1', '-x, -y, z, -1', @@ -613,7 +613,7 @@ def as_dict(self) -> dict[str, Any]: } @classmethod - def from_dict(cls, dct: dict) -> MagSymmOp: + def from_dict(cls, dct: dict) -> Self: """ Args: dct: dict. diff --git a/pymatgen/core/periodic_table.py b/pymatgen/core/periodic_table.py index 8524a43c606..45b3ed71f3f 100644 --- a/pymatgen/core/periodic_table.py +++ b/pymatgen/core/periodic_table.py @@ -20,10 +20,12 @@ from pymatgen.util.string import Stringify, formula_double_format if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.util.typing import SpeciesLike # Loads element data from json file -with open(Path(__file__).absolute().parent / "periodic_table.json") as ptable_json: +with open(Path(__file__).absolute().parent / "periodic_table.json", encoding="utf-8") as ptable_json: _pt_data = json.load(ptable_json) _pt_row_sizes = (2, 8, 8, 18, 18, 32, 32) @@ -235,12 +237,12 @@ def __getattr__(self, item: str) -> Any: unit = "K^-1" else: unit = tokens[1] - val = FloatWithUnit(tokens[0], unit) + val = FloatWithUnit(float(tokens[0]), unit) else: unit = tokens[1].replace("", "^").replace("", "").replace("Ω", "ohm") units = Unit(unit) if set(units).issubset(SUPPORTED_UNIT_NAMES): - val = FloatWithUnit(tokens[0], unit) + val = FloatWithUnit(float(tokens[0]), unit) except ValueError: # Ignore error. val will just remain a string. pass @@ -1051,7 +1053,7 @@ def ionic_radius(self) -> float | None: return None @classmethod - def from_str(cls, species_string: str) -> Species: + def from_str(cls, species_string: str) -> Self: """Returns a Species from a string representation. Args: @@ -1105,6 +1107,7 @@ def __str__(self) -> str: if isinstance(abs_charge, float): abs_charge = f"{abs_charge:.2f}" output += f"{abs_charge}{'+' if self.oxi_state >= 0 else '-'}" + if self._spin is not None: spin = self._spin output += f",{spin=}" @@ -1197,16 +1200,20 @@ def get_crystal_field_spin( """ if coordination not in ("oct", "tet") or spin_config not in ("high", "low"): raise ValueError("Invalid coordination or spin config") + elec = self.full_electronic_structure if len(elec) < 4 or elec[-1][1] != "s" or elec[-2][1] != "d": raise AttributeError(f"Invalid element {self.symbol} for crystal field calculation") + n_electrons = elec[-1][2] + elec[-2][2] - self.oxi_state if n_electrons < 0 or n_electrons > 10: raise AttributeError(f"Invalid oxidation state {self.oxi_state} for element {self.symbol}") + if spin_config == "high": if n_electrons <= 5: return n_electrons return 10 - n_electrons + if spin_config == "low": if coordination == "oct": if n_electrons <= 3: @@ -1216,6 +1223,7 @@ def get_crystal_field_spin( if n_electrons <= 8: return n_electrons - 6 return 10 - n_electrons + if coordination == "tet": if n_electrons <= 2: return n_electrons @@ -1224,7 +1232,8 @@ def get_crystal_field_spin( if n_electrons <= 7: return n_electrons - 4 return 10 - n_electrons - raise RuntimeError(f"should not reach here, {spin_config=}, {coordination=}") + return None + return None def __deepcopy__(self, memo) -> Species: return Species(self.symbol, self.oxi_state, spin=self._spin) @@ -1240,7 +1249,7 @@ def as_dict(self) -> dict: } @classmethod - def from_dict(cls, dct) -> Species: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. @@ -1360,7 +1369,7 @@ def __deepcopy__(self, memo): return DummySpecies(self.symbol, self._oxi_state) @classmethod - def from_str(cls, species_string: str) -> DummySpecies: + def from_str(cls, species_string: str) -> Self: """Returns a Dummy from a string representation. Args: @@ -1399,7 +1408,7 @@ def as_dict(self) -> dict: } @classmethod - def from_dict(cls, dct) -> DummySpecies: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. diff --git a/pymatgen/core/sites.py b/pymatgen/core/sites.py index 1506197da15..12abd6e37f8 100644 --- a/pymatgen/core/sites.py +++ b/pymatgen/core/sites.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.util.typing import CompositionLike, SpeciesLike @@ -262,7 +263,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct: dict) -> Site: + def from_dict(cls, dct: dict) -> Self: """Create Site from dict representation.""" atoms_n_occu = {} for sp_occu in dct["species"]: @@ -590,7 +591,7 @@ def as_dict(self, verbosity: int = 0) -> dict: return dct @classmethod - def from_dict(cls, dct, lattice=None) -> PeriodicSite: + def from_dict(cls, dct, lattice=None) -> Self: """Create PeriodicSite from dict representation. Args: diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 4e610c50e50..1a2fa0d6289 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -105,12 +105,12 @@ def __getitem__(self, idx: int): """Make neighbor Tuple-like to retain backwards compatibility.""" return (self, self.nn_distance, self.index)[idx] - def as_dict(self) -> dict: # type: ignore + def as_dict(self) -> dict: """Note that method calls the super of Site, which is MSONable itself.""" return super(Site, self).as_dict() @classmethod - def from_dict(cls, dct: dict) -> Neighbor: # type: ignore + def from_dict(cls, dct: dict) -> Self: """Returns a Neighbor from a dict. Args: @@ -177,12 +177,12 @@ def __getitem__(self, idx: int | slice): """Make neighbor Tuple-like to retain backwards compatibility.""" return (self, self.nn_distance, self.index, self.image)[idx] - def as_dict(self) -> dict: # type: ignore + def as_dict(self) -> dict: # type: ignore[override] """Note that method calls the super of Site, which is MSONable itself.""" return super(Site, self).as_dict() @classmethod - def from_dict(cls, dct: dict) -> PeriodicNeighbor: # type: ignore + def from_dict(cls, dct: dict) -> Self: # type: ignore[override] """Returns a PeriodicNeighbor from a dict. Args: @@ -3657,7 +3657,7 @@ def from_str( # type: ignore[override] return cls.from_sites(mol, properties=mol.properties) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self | None: # type: ignore[override] """Reads a molecule from a file. Supported formats include xyz, gaussian input (gjf|g03|g09|com|inp), Gaussian output (.out|and pymatgen's JSON-serialized molecules. Using openbabel, diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 50e692a0e0c..5c5921c6d8e 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -35,6 +35,8 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.symmetry.groups import CrystalSystem __author__ = "Richard Tran, Wenhao Sun, Zihan Xu, Shyue Ping Ong" @@ -481,7 +483,7 @@ def as_dict(self): return dct @classmethod - def from_dict(cls, dct: dict) -> Slab: # type: ignore[override] + def from_dict(cls, dct: dict) -> Self: # type: ignore[override] """ Args: dct: dict. @@ -1093,21 +1095,21 @@ def get_slabs( matcher = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) new_slabs = [] - for g in matcher.group_structures(slabs): + for group in matcher.group_structures(slabs): # For each unique termination, symmetrize the # surfaces by removing sites from the bottom. if symmetrize: - slabs = self.nonstoichiometric_symmetrized_slab(g[0]) + slabs = self.nonstoichiometric_symmetrized_slab(group[0]) new_slabs.extend(slabs) else: - new_slabs.append(g[0]) + new_slabs.append(group[0]) match = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) new_slabs = [g[0] for g in match.group_structures(new_slabs)] return sorted(new_slabs, key=lambda s: s.energy) - def repair_broken_bonds(self, slab, bonds): + def repair_broken_bonds(self, slab: Slab, bonds): """This method will find undercoordinated atoms due to slab cleaving specified by the bonds parameter and move them to the other surface to make sure the bond is kept intact. diff --git a/pymatgen/core/tensors.py b/pymatgen/core/tensors.py index 14c31fc811d..684cc15520e 100644 --- a/pymatgen/core/tensors.py +++ b/pymatgen/core/tensors.py @@ -46,14 +46,14 @@ class Tensor(np.ndarray, MSONable): symbol = "T" - def __new__(cls, input_array, vscale=None, check_rank=None): + def __new__(cls, input_array, vscale=None, check_rank=None) -> Self: """Create a Tensor object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. Args: input_array: (array-like with shape 3^N): array-like representing - a tensor quantity in standard (i. e. non-voigt) notation + a tensor quantity in standard (i. e. non-Voigt) notation vscale: (N x M array-like): a matrix corresponding to the coefficients of the Voigt-notation tensor check_rank: (int): If not None, checks that input_array's rank == check_rank. @@ -125,8 +125,8 @@ def rotate(self, matrix, tol: float = 1e-3): matrix = SquareTensor(matrix) if not matrix.is_rotation(tol): raise ValueError("Rotation matrix is not valid.") - sop = SymmOp.from_rotation_and_translation(matrix, [0.0, 0.0, 0.0]) - return self.transform(sop) + symm_op = SymmOp.from_rotation_and_translation(matrix, [0.0, 0.0, 0.0]) + return self.transform(symm_op) def einsum_sequence(self, other_arrays, einsum_string=None): """Calculates the result of an einstein summation expression.""" @@ -154,9 +154,9 @@ def project(self, n): Args: n (3x1 array-like): direction to project onto - Returns (float): - scalar value corresponding to the projection of - the tensor into the vector + Returns: + float: scalar value corresponding to the projection of + the tensor into the vector """ n = get_uvec(n) return self.einsum_sequence([n] * self.rank) @@ -256,8 +256,8 @@ def round(self, decimals=0): If decimals is negative, it specifies the number of positions to the left of the decimal point. - Returns (Tensor): - rounded tensor of same type + Returns: + Tensor: rounded tensor of same type """ return type(self)(np.round(self, decimals=decimals)) @@ -639,12 +639,11 @@ def as_dict(self, voigt: bool = False) -> dict: """Serializes the tensor object. Args: - voigt (bool): flag for whether to store entries in - Voigt notation. Defaults to false, as information - may be lost in conversion. + voigt (bool): flag for whether to store entries in Voigt notation. + Defaults to false, as information may be lost in conversion. - Returns (dict): - serialized format tensor object + Returns: + dict: serialized format tensor object """ input_array = self.voigt if voigt else self dct = { @@ -657,7 +656,7 @@ def as_dict(self, voigt: bool = False) -> dict: return dct @classmethod - def from_dict(cls, dct: dict) -> Tensor: + def from_dict(cls, dct: dict) -> Self: """Instantiate Tensors from dicts (using MSONable API). Returns: @@ -843,7 +842,7 @@ def as_dict(self, voigt=False): return dct @classmethod - def from_dict(cls, dct: dict) -> TensorCollection: + def from_dict(cls, dct: dict) -> Self: """Creates TensorCollection from dict. Args: @@ -863,7 +862,7 @@ class SquareTensor(Tensor): (stress, strain etc.). """ - def __new__(cls, input_array, vscale=None): + def __new__(cls, input_array, vscale=None) -> Self: """Create a SquareTensor object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. Error is thrown when the class is initialized with non-square matrix. diff --git a/pymatgen/core/trajectory.py b/pymatgen/core/trajectory.py index 90d48393b64..966781c7654 100644 --- a/pymatgen/core/trajectory.py +++ b/pymatgen/core/trajectory.py @@ -9,7 +9,7 @@ from collections.abc import Iterator, Sequence from fnmatch import fnmatch from pathlib import Path -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union import numpy as np from monty.io import zopen @@ -19,6 +19,10 @@ from pymatgen.io.ase import AseAtomsAdaptor from pymatgen.io.vasp.outputs import Vasprun, Xdatcar +if TYPE_CHECKING: + from typing_extensions import Self + + __author__ = "Eric Sivonxay, Shyam Dwaraknath, Mingjian Wen, Evan Spotte-Smith" __version__ = "0.1" __date__ = "Jun 29, 2022" @@ -467,7 +471,7 @@ def as_dict(self) -> dict: } @classmethod - def from_structures(cls, structures: list[Structure], constant_lattice: bool = True, **kwargs) -> Trajectory: + def from_structures(cls, structures: list[Structure], constant_lattice: bool = True, **kwargs) -> Self: """Create trajectory from a list of structures. Note: Assumes no atoms removed during simulation. @@ -500,7 +504,7 @@ def from_structures(cls, structures: list[Structure], constant_lattice: bool = T ) @classmethod - def from_molecules(cls, molecules: list[Molecule], **kwargs) -> Trajectory: + def from_molecules(cls, molecules: list[Molecule], **kwargs) -> Self: """Create trajectory from a list of molecules. Note: Assumes no atoms removed during simulation. @@ -526,7 +530,7 @@ def from_molecules(cls, molecules: list[Molecule], **kwargs) -> Trajectory: ) @classmethod - def from_file(cls, filename: str | Path, constant_lattice: bool = True, **kwargs) -> Trajectory: + def from_file(cls, filename: str | Path, constant_lattice: bool = True, **kwargs) -> Self: """Create trajectory from XDATCAR, vasprun.xml file, or ASE trajectory (.traj) file. Args: diff --git a/pymatgen/core/units.py b/pymatgen/core/units.py index c2e2f88734d..5c66e6ade79 100644 --- a/pymatgen/core/units.py +++ b/pymatgen/core/units.py @@ -9,14 +9,17 @@ from __future__ import annotations import collections -import numbers import re from functools import partial -from typing import Any +from numbers import Number +from typing import TYPE_CHECKING, Any import numpy as np import scipy.constants as const +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Shyue Ping Ong, Matteo Giantomassi" __copyright__ = "Copyright 2011, The Materials Project" __version__ = "1.0" @@ -161,8 +164,6 @@ class Unit(collections.abc.Mapping): Only integer powers are supported for units. """ - Error = UnitError - def __init__(self, unit_def) -> None: """Constructs a unit. @@ -234,7 +235,7 @@ def as_base_units(self): for dct in DERIVED_UNITS.values(): if k in dct: for k2, v2 in dct[k].items(): - if isinstance(k2, numbers.Number): + if isinstance(k2, Number): factor *= k2 ** (v2 * v) else: b[k2] += v2 * v @@ -289,40 +290,7 @@ class FloatWithUnit(float): 32.932522246000005 eV """ - Error = UnitError - - @classmethod - def from_str(cls, s): - """Parse string to FloatWithUnit. - - Example: Memory.from_str("1. Mb") - """ - # Extract num and unit string. - s = s.strip() - for idx, char in enumerate(s): # noqa: B007 - if char.isalpha() or char.isspace(): - break - else: - raise Exception(f"Unit is missing in string {s}") - num, unit = float(s[:idx]), s[idx:] - - # Find unit type (set it to None if it cannot be detected) - for unit_type, dct in BASE_UNITS.items(): # noqa: B007 - if unit in dct: - break - else: - unit_type = None - - return cls(num, unit, unit_type=unit_type) - - def __new__(cls, val, unit, unit_type=None): - """Overrides __new__ since we are subclassing a Python primitive/.""" - new = float.__new__(cls, val) - new._unit = Unit(unit) - new._unit_type = unit_type - return new - - def __init__(self, val, unit, unit_type=None) -> None: + def __init__(self, val: float | Number, unit: str, unit_type: str | None = None) -> None: """Initializes a float with unit. Args: @@ -335,6 +303,13 @@ def __init__(self, val, unit, unit_type=None) -> None: self._unit = Unit(unit) self._unit_type = unit_type + def __new__(cls, val, unit, unit_type=None) -> Self: + """Overrides __new__ since we are subclassing a Python primitive.""" + new = float.__new__(cls, val) + new._unit = Unit(unit) + new._unit_type = unit_type + return new + def __str__(self) -> str: return f"{super().__str__()} {self._unit}" @@ -402,7 +377,7 @@ def __setstate__(self, state): self._unit = state["_unit"] @property - def unit_type(self) -> str: + def unit_type(self) -> str | None: """The type of unit. Energy, Charge, etc.""" return self._unit_type @@ -411,6 +386,26 @@ def unit(self) -> Unit: """The unit, e.g., "eV".""" return self._unit + @classmethod + def from_str(cls, s: str) -> Self: + """Parse string to FloatWithUnit. + Example: Memory.from_str("1. Mb"). + """ + # Extract num and unit string. + s = s.strip() + for _idx, char in enumerate(s): + if char.isalpha() or char.isspace(): + break + else: + raise ValueError(f"Unit is missing in string {s}") + num, unit = float(s[:_idx]), s[_idx:] + + # Find unit type (set it to None if it cannot be detected) + for unit_type, dct in BASE_UNITS.items(): + if unit in dct: + return cls(num, unit, unit_type=unit_type) + return cls(num, unit, unit_type=None) + def to(self, new_unit): """Conversion to a new_unit. Right now, only supports 1 to 1 mapping of units of each type. @@ -466,9 +461,7 @@ class ArrayWithUnit(np.ndarray): array([ 28.21138386, 56.42276772]) eV """ - Error = UnitError - - def __new__(cls, input_array, unit, unit_type=None): + def __new__(cls, input_array, unit, unit_type=None) -> Self: """Override __new__.""" # Input array is an already formed ndarray instance # We first cast to be our class type @@ -711,10 +704,12 @@ def obj_with_unit(obj: Any, unit: str) -> FloatWithUnit | ArrayWithUnit | dict[s """ unit_type = _UNAME2UTYPE[unit] - if isinstance(obj, numbers.Number): + if isinstance(obj, Number): return FloatWithUnit(obj, unit=unit, unit_type=unit_type) + if isinstance(obj, collections.abc.Mapping): - return {k: obj_with_unit(v, unit) for k, v in obj.items()} # type: ignore + return {k: obj_with_unit(v, unit) for k, v in obj.items()} # type: ignore[misc] + return ArrayWithUnit(obj, unit=unit, unit_type=unit_type) @@ -751,7 +746,7 @@ def wrapped_f(*args, **kwargs): if isinstance(val, collections.abc.Mapping): for k, v in val.items(): val[k] = FloatWithUnit(v, unit_type=unit_type, unit=unit) - elif isinstance(val, numbers.Number): + elif isinstance(val, Number): return FloatWithUnit(val, unit_type=unit_type, unit=unit) elif val is None: pass diff --git a/pymatgen/core/xcfunc.py b/pymatgen/core/xcfunc.py index fe7f6a07111..e8b6b935791 100644 --- a/pymatgen/core/xcfunc.py +++ b/pymatgen/core/xcfunc.py @@ -1,4 +1,4 @@ -"""This module provides.""" +"""This module provides class for XC correlation functional.""" from __future__ import annotations @@ -115,13 +115,29 @@ class XcFunc(MSONable): del xcf + def __init__(self, xc: LibxcFunc | None = None, x: LibxcFunc | None = None, c: LibxcFunc | None = None) -> None: + """ + Args: + xc: LibxcFunc for XC functional. + x: LibxcFunc for exchange part. Mutually exclusive with xc. + c: LibxcFunc for correlation part. Mutually exclusive with xc. + """ + # Consistency check + if xc is None: + if x is None or c is None: + raise ValueError("x or c must be specified when xc is None") + elif x is not None or c is not None: + raise ValueError("x and c should be None when xc is specified") + + self.xc, self.x, self.c = xc, x, c + @classmethod - def aliases(cls): + def aliases(cls) -> list[str]: """List of registered names.""" return [nt.name for nt in cls.defined_aliases.values()] @classmethod - def asxc(cls, obj): + def asxc(cls, obj) -> Self: """Convert object into Xcfunc.""" if isinstance(obj, cls): return obj @@ -130,9 +146,8 @@ def asxc(cls, obj): raise TypeError(f"Don't know how to convert <{type(obj)}:{obj}> to Xcfunc") @classmethod - def from_abinit_ixc(cls, ixc): + def from_abinit_ixc(cls, ixc: int) -> Self | None: """Build the object from Abinit ixc (integer).""" - ixc = int(ixc) if ixc == 0: return None if ixc > 0: @@ -150,12 +165,12 @@ def from_abinit_ixc(cls, ixc): return cls(x=x, c=c) @classmethod - def from_name(cls, name): + def from_name(cls, name: str) -> Self: """Build the object from one of the registered names.""" return cls.from_type_name(None, name) @classmethod - def from_type_name(cls, typ, name): + def from_type_name(cls, typ: str | None, name: str) -> Self: """Build the object from (type, name).""" # Try aliases first. for k, nt in cls.defined_aliases.items(): @@ -172,12 +187,11 @@ def from_type_name(cls, typ, name): # name="GGA_X_PBE+GGA_C_PBE" or name=""LDA_XC_TETER93" if "+" in name: x, c = (s.strip() for s in name.split("+")) - x, c = LibxcFunc[x], LibxcFunc[c] - return cls(x=x, c=c) - xc = LibxcFunc[name] - return cls(xc=xc) + return cls(x=LibxcFunc[x], c=LibxcFunc[c]) - def as_dict(self): + return cls(xc=LibxcFunc[name]) + + def as_dict(self) -> dict: """Serialize to MSONable dict representation e.g. to write to disk as JSON.""" dct = {"@module": type(self).__module__, "@class": type(self).__name__} if self.x is not None: @@ -193,49 +207,44 @@ def from_dict(cls, dct: dict) -> Self: """Deserialize from MSONable dict representation.""" return cls(xc=dct.get("xc"), x=dct.get("x"), c=dct.get("c")) - def __init__(self, xc=None, x=None, c=None) -> None: - """ - Args: - xc: LibxcFunc for XC functional. - x: LibxcFunc for exchange part. Mutually exclusive with xc. - c: LibxcFunc for correlation part. Mutually exclusive with xc. - """ - # Consistency check - if xc is None: - if x is None or c is None: - raise ValueError("x or c must be specified when xc is None") - elif x is not None or c is not None: - raise ValueError("x and c should be None when xc is specified") - - self.xc, self.x, self.c = xc, x, c - @lazy_property - def type(self): + def type(self) -> str | None: """The type of the functional.""" - if self.xc in self.defined_aliases: - return self.defined_aliases[self.xc].type + if self.xc in self.defined_aliases and self.xc is not None: + return self.defined_aliases[self.xc].type # type: ignore[index] + xc = self.x, self.c if xc in self.defined_aliases: - return self.defined_aliases[xc].type + return self.defined_aliases[xc].type # type: ignore[index] # If self is not in defined_aliases, use LibxcFunc family if self.xc is not None: return self.xc.family - return f"{self.x.family}+{self.c.family}" + + if self.x is not None and self.c is not None: + return f"{self.x.family}+{self.c.family}" + + return None @lazy_property - def name(self) -> str: + def name(self) -> str | None: """The name of the functional. If the functional is not found in the aliases, the string has the form X_NAME+C_NAME. """ if self.xc in self.defined_aliases: - return self.defined_aliases[self.xc].name + return self.defined_aliases[self.xc].name # type: ignore[index] + xc = (self.x, self.c) if xc in self.defined_aliases: - return self.defined_aliases[xc].name + return self.defined_aliases[xc].name # type: ignore[index] + if self.xc is not None: return self.xc.name - return f"{self.x.name}+{self.c.name}" + + if self.x is not None and self.c is not None: + return f"{self.x.name}+{self.c.name}" + + return None def __repr__(self) -> str: return str(self.name) diff --git a/pymatgen/electronic_structure/bandstructure.py b/pymatgen/electronic_structure/bandstructure.py index 456c5e03e7e..b3fe6560ab0 100644 --- a/pymatgen/electronic_structure/bandstructure.py +++ b/pymatgen/electronic_structure/bandstructure.py @@ -133,7 +133,7 @@ def as_dict(self) -> dict[str, Any]: } @classmethod - def from_dict(cls, dct) -> Kpoint: + def from_dict(cls, dct: dict) -> Self: """Create from dict. Args: @@ -643,7 +643,7 @@ def from_dict(cls, dct: dict) -> Self: return cls.from_old_dict(dct) @classmethod - def from_old_dict(cls, dct): + def from_old_dict(cls, dct) -> Self: """ Args: dct (dict): A dict with all data for a band structure symmetry line object. @@ -653,7 +653,7 @@ def from_old_dict(cls, dct): """ # Strip the label to recover initial string (see trick used in as_dict to handle $ chars) labels_dict = {k.strip(): v for k, v in dct["labels_dict"].items()} - projections = {} + projections: dict = {} structure = None if dct.get("projections"): structure = Structure.from_dict(dct["structure"]) @@ -674,7 +674,7 @@ def from_old_dict(cls, dct): dd.append(np.array(ddd)) projections[Spin(int(spin))] = np.array(dd) - return BandStructure( + return cls( dct["kpoints"], {Spin(int(k)): dct["bands"][k] for k in dct["bands"]}, Lattice(dct["lattice_rec"]["matrix"]), @@ -957,7 +957,7 @@ def from_dict(cls, dct: dict) -> Self: structure = Structure.from_dict(dct["structure"]) projections = {Spin(int(spin)): np.array(v) for spin, v in dct["projections"].items()} - return LobsterBandStructureSymmLine( + return cls( dct["kpoints"], {Spin(int(k)): dct["bands"][k] for k in dct["bands"]}, Lattice(dct["lattice_rec"]["matrix"]), @@ -973,10 +973,10 @@ def from_dict(cls, dct: dict) -> Self: "format. The old format will be retired in pymatgen " "5.0." ) - return LobsterBandStructureSymmLine.from_old_dict(dct) + return cls.from_old_dict(dct) @classmethod - def from_old_dict(cls, dct): + def from_old_dict(cls, dct) -> Self: """ Args: dct (dict): A dict with all data for a band structure symmetry line @@ -987,7 +987,7 @@ def from_old_dict(cls, dct): """ # Strip the label to recover initial string (see trick used in as_dict to handle $ chars) labels_dict = {k.strip(): v for k, v in dct["labels_dict"].items()} - projections = {} + projections: dict = {} structure = None if "projections" in dct and len(dct["projections"]) != 0: structure = Structure.from_dict(dct["structure"]) @@ -1001,7 +1001,7 @@ def from_old_dict(cls, dct): dd.append(np.array(ddd)) projections[Spin(int(spin))] = np.array(dd) - return LobsterBandStructureSymmLine( + return cls( dct["kpoints"], {Spin(int(k)): dct["bands"][k] for k in dct["bands"]}, Lattice(dct["lattice_rec"]["matrix"]), diff --git a/pymatgen/electronic_structure/boltztrap.py b/pymatgen/electronic_structure/boltztrap.py index 44eeaef1ee4..1b1d2d09692 100644 --- a/pymatgen/electronic_structure/boltztrap.py +++ b/pymatgen/electronic_structure/boltztrap.py @@ -25,7 +25,7 @@ import tempfile import time from shutil import which -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import numpy as np from monty.dev import requires @@ -1936,7 +1936,7 @@ def parse_cond_and_hall(path_dir, doping_levels=None): ) @classmethod - def from_files(cls, path_dir, dos_spin=1): + def from_files(cls, path_dir: str, dos_spin: Literal[-1, 1] = 1) -> Self: """Get a BoltztrapAnalyzer object from a set of files. Args: @@ -1957,7 +1957,7 @@ def from_files(cls, path_dir, dos_spin=1): *cond_and_hall, carrier_conc = cls.parse_cond_and_hall(path_dir, doping_levels) - return cls(gap, *cond_and_hall, in_trans, dos, partial_dos, carrier_conc, vol, warning) + return cls(gap, *cond_and_hall, in_trans, dos, partial_dos, carrier_conc, vol, warning) # type: ignore[call-arg] if run_type == "DOS": trim = in_trans["dos_type"] == "HISTO" diff --git a/pymatgen/electronic_structure/boltztrap2.py b/pymatgen/electronic_structure/boltztrap2.py index dc2fe479186..6ea642db8d2 100644 --- a/pymatgen/electronic_structure/boltztrap2.py +++ b/pymatgen/electronic_structure/boltztrap2.py @@ -29,6 +29,7 @@ from __future__ import annotations import warnings +from typing import TYPE_CHECKING import matplotlib.pyplot as plt import numpy as np @@ -43,6 +44,11 @@ from pymatgen.io.vasp import Vasprun from pymatgen.symmetry.bandstructure import HighSymmKpath +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + try: from BoltzTraP2 import bandlib as BL from BoltzTraP2 import fite, sphere, units @@ -132,7 +138,7 @@ def __init__(self, obj, structure=None, nelect=None) -> None: raise BoltztrapError("nelect must be given.") @classmethod - def from_file(cls, vasprun_file): + def from_file(cls, vasprun_file: str | Path) -> Self: """Get a vasprun.xml file and return a VasprunBSLoader.""" vrun_obj = Vasprun(vasprun_file, parse_projected_eigen=True) return cls(vrun_obj) @@ -348,10 +354,10 @@ def __init__(self, vrun_obj=None) -> None: self.cbm = self.fermi @classmethod - def from_file(cls, vasprun_file): + def from_file(cls, vasprun_file: str | Path) -> Self: """Get a vasprun.xml file and return a VasprunLoader.""" vrun_obj = Vasprun(vasprun_file, parse_projected_eigen=True) - return VasprunLoader(vrun_obj) + return cls(vrun_obj) def get_lattvec(self): """Lattice vectors.""" diff --git a/pymatgen/electronic_structure/cohp.py b/pymatgen/electronic_structure/cohp.py index 7ea9b43682f..bc56cc6ee6e 100644 --- a/pymatgen/electronic_structure/cohp.py +++ b/pymatgen/electronic_structure/cohp.py @@ -189,12 +189,12 @@ def has_antibnd_states_below_efermi(self, spin=None, limit=0.01): return dict_to_return @classmethod - def from_dict(cls, dct: dict[str, Any]) -> Cohp: + def from_dict(cls, dct: dict[str, Any]) -> Self: """Returns a COHP object from a dict representation of the COHP.""" icohp = {Spin(int(key)): np.array(val) for key, val in dct["ICOHP"].items()} if "ICOHP" in dct else None are_cobis = dct.get("are_cobis", False) are_multi_center_cobis = dct.get("are_multi_center_cobis", False) - return Cohp( + return cls( dct["efermi"], dct["energies"], {Spin(int(key)): np.array(val) for key, val in dct["COHP"].items()}, @@ -647,7 +647,7 @@ def from_dict(cls, dct: dict) -> Self: are_cobis = dct.get("are_cobis", False) - return CompleteCohp( + return cls( structure, avg_cohp, cohp_dict, @@ -661,7 +661,7 @@ def from_dict(cls, dct: dict) -> Self: @classmethod def from_file( cls, fmt, filename=None, structure_file=None, are_coops=False, are_cobis=False, are_multi_center_cobis=False - ): + ) -> Self: """ Creates a CompleteCohp object from an output file of a COHP calculation. Valid formats are either LMTO (for the Stuttgart @@ -698,7 +698,7 @@ def from_file( structure_file = "CTRL" if filename is None: filename = "COPL" - cohp_file = LMTOCopl(filename=filename, to_eV=True) + cohp_file: LMTOCopl | Cohpcar = LMTOCopl(filename=filename, to_eV=True) elif fmt == "LOBSTER": if ( (are_coops and are_cobis) @@ -765,7 +765,7 @@ def from_file( if fmt == "LMTO": # Calculate the average COHP for the LMTO file to be # consistent with LOBSTER output. - avg_data = {"COHP": {}, "ICOHP": {}} + avg_data: dict[str, dict] = {"COHP": {}, "ICOHP": {}} for i in avg_data: for spin in spins: rows = np.array([v[i][spin] for v in cohp_data.values()]) @@ -846,7 +846,7 @@ def from_file( for key, dct in cohp_data.items() } - return CompleteCohp( + return cls( structure, avg_cohp, cohp_dict, diff --git a/pymatgen/electronic_structure/core.py b/pymatgen/electronic_structure/core.py index 530c18916e8..a60ae880c9a 100644 --- a/pymatgen/electronic_structure/core.py +++ b/pymatgen/electronic_structure/core.py @@ -15,6 +15,8 @@ from typing_extensions import Self + from pymatgen.core import Lattice + @unique class Spin(Enum): @@ -386,7 +388,7 @@ def are_collinear(magmoms) -> bool: return num_ncl == 0 @classmethod - def from_moment_relative_to_crystal_axes(cls, moment, lattice): + def from_moment_relative_to_crystal_axes(cls, moment: list[float], lattice: Lattice) -> Self: """Obtaining a Magmom object from a magnetic moment provided relative to crystal axes. diff --git a/pymatgen/electronic_structure/dos.py b/pymatgen/electronic_structure/dos.py index 667e03a500d..e3ea0ce7f6f 100644 --- a/pymatgen/electronic_structure/dos.py +++ b/pymatgen/electronic_structure/dos.py @@ -22,6 +22,7 @@ from collections.abc import Mapping from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core.sites import PeriodicSite from pymatgen.util.typing import SpeciesLike @@ -355,9 +356,9 @@ def __str__(self) -> str: return "\n".join(str_arr) @classmethod - def from_dict(cls, dct) -> Dos: + def from_dict(cls, dct: dict) -> Self: """Returns Dos object from dict representation of Dos.""" - return Dos( + return cls( dct["efermi"], dct["energies"], {Spin(int(k)): v for k, v in dct["densities"].items()}, @@ -568,14 +569,14 @@ def get_fermi( return fermi @classmethod - def from_dict(cls, dct) -> FermiDos: + def from_dict(cls, dct: dict) -> Self: """Returns Dos object from dict representation of Dos.""" dos = Dos( dct["efermi"], dct["energies"], {Spin(int(k)): v for k, v in dct["densities"].items()}, ) - return FermiDos(dos, structure=Structure.from_dict(dct["structure"]), nelecs=dct["nelecs"]) + return cls(dos, structure=Structure.from_dict(dct["structure"]), nelecs=dct["nelecs"]) def as_dict(self) -> dict: """JSON-serializable dict representation of Dos.""" @@ -1248,7 +1249,7 @@ def get_dos_fp_similarity( ) @classmethod - def from_dict(cls, dct) -> CompleteDos: + def from_dict(cls, dct: dict) -> Self: """Returns CompleteDos object from dict representation.""" tdos = Dos.from_dict(dct) struct = Structure.from_dict(dct["structure"]) @@ -1260,7 +1261,7 @@ def from_dict(cls, dct) -> CompleteDos: orb = Orbital[orb_str] orb_dos[orb] = {Spin(int(k)): v for k, v in odos["densities"].items()} pdoss[at] = orb_dos - return CompleteDos(struct, tdos, pdoss) + return cls(struct, tdos, pdoss) def as_dict(self) -> dict: """JSON-serializable dict representation of CompleteDos.""" @@ -1394,7 +1395,7 @@ def get_element_spd_dos(self, el: SpeciesLike) -> dict[str, Dos]: # type: ignor return {orb: Dos(self.efermi, self.energies, densities) for orb, densities in el_dos.items()} # type: ignore @classmethod - def from_dict(cls, dct) -> LobsterCompleteDos: + def from_dict(cls, dct: dict) -> Self: """Hydrate CompleteDos object from dict representation.""" tdos = Dos.from_dict(dct) struct = Structure.from_dict(dct["structure"]) @@ -1406,7 +1407,7 @@ def from_dict(cls, dct) -> LobsterCompleteDos: orb = orb_str orb_dos[orb] = {Spin(int(k)): v for k, v in odos["densities"].items()} pdoss[at] = orb_dos - return LobsterCompleteDos(struct, tdos, pdoss) + return cls(struct, tdos, pdoss) def add_densities(density1: Mapping[Spin, ArrayLike], density2: Mapping[Spin, ArrayLike]) -> dict[Spin, np.ndarray]: diff --git a/pymatgen/entries/computed_entries.py b/pymatgen/entries/computed_entries.py index 4919e4fdd20..a24ad687412 100644 --- a/pymatgen/entries/computed_entries.py +++ b/pymatgen/entries/computed_entries.py @@ -25,6 +25,8 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core import Structure __author__ = "Ryan Kingsbury, Matt McDermott, Shyue Ping Ong, Anubhav Jain" @@ -473,7 +475,7 @@ def __eq__(self, other: object) -> bool: return True @classmethod - def from_dict(cls, dct: dict) -> ComputedEntry: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. @@ -609,7 +611,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct) -> ComputedStructureEntry: + def from_dict(cls, dct) -> Self: """ Args: dct (dict): Dict representation. @@ -877,7 +879,7 @@ def _g_delta_sisso(vol_per_atom, reduced_mass, temp) -> float: ) @classmethod - def from_pd(cls, pd, temp=300, gibbs_model="SISSO") -> list[GibbsComputedStructureEntry]: + def from_pd(cls, pd, temp=300, gibbs_model="SISSO") -> list[Self]: """Constructor method for initializing a list of GibbsComputedStructureEntry objects from an existing T = 0 K phase diagram composed of ComputedStructureEntry objects, as acquired from a thermochemical database; @@ -911,7 +913,7 @@ def from_pd(cls, pd, temp=300, gibbs_model="SISSO") -> list[GibbsComputedStructu return gibbs_entries @classmethod - def from_entries(cls, entries, temp=300, gibbs_model="SISSO") -> list[GibbsComputedStructureEntry]: + def from_entries(cls, entries, temp=300, gibbs_model="SISSO") -> list[Self]: """Constructor method for initializing GibbsComputedStructureEntry objects from T = 0 K ComputedStructureEntry objects, as acquired from a thermochemical database e.g. The Materials Project. @@ -942,7 +944,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct) -> GibbsComputedStructureEntry: + def from_dict(cls, dct) -> Self: """ Args: dct (dict): Dict representation. diff --git a/pymatgen/entries/entry_tools.py b/pymatgen/entries/entry_tools.py index c23c895b279..b3444ffeef8 100644 --- a/pymatgen/entries/entry_tools.py +++ b/pymatgen/entries/entry_tools.py @@ -23,6 +23,8 @@ if TYPE_CHECKING: from collections.abc import Iterable + from typing_extensions import Self + from pymatgen.entries import Entry from pymatgen.entries.computed_entries import ComputedEntry, ComputedStructureEntry @@ -314,7 +316,7 @@ def to_csv(self, filename: str, latexify_names: bool = False) -> None: writer.writerow(row) @classmethod - def from_csv(cls, filename: str): + def from_csv(cls, filename: str) -> Self: """Imports PDEntries from a csv. Args: diff --git a/pymatgen/ext/matproj.py b/pymatgen/ext/matproj.py index e86589a7b36..270096b9ef8 100644 --- a/pymatgen/ext/matproj.py +++ b/pymatgen/ext/matproj.py @@ -27,8 +27,12 @@ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer if TYPE_CHECKING: + from mp_api.client import MPRester as _MPResterNew + from typing_extensions import Self + from pymatgen.core.structure import Structure from pymatgen.entries.computed_entries import ComputedStructureEntry + from pymatgen.ext.matproj_legacy import _MPResterLegacy logger = logging.getLogger(__name__) @@ -94,7 +98,7 @@ def __getattr__(self, item): "used by 80% of users. If you are looking for the full functionality MPRester, pls install the mp-api ." ) - def __enter__(self): + def __enter__(self) -> Self: """Support for "with" context.""" return self @@ -372,8 +376,8 @@ class MPRester: for which API to use. """ - def __new__(cls, *args, **kwargs): - r""" + def __new__(cls, *args, **kwargs) -> _MPResterNew | _MPResterBasic | _MPResterLegacy: # type: ignore[misc] + """ Args: *args: Pass through to either legacy or new MPRester. **kwargs: Pass through to either legacy or new MPRester. diff --git a/pymatgen/ext/matproj_legacy.py b/pymatgen/ext/matproj_legacy.py index b45f6210efe..2057d253679 100644 --- a/pymatgen/ext/matproj_legacy.py +++ b/pymatgen/ext/matproj_legacy.py @@ -35,8 +35,11 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine from pymatgen.phonon.dos import CompletePhononDos + logger = logging.getLogger(__name__) MP_LOG_FILE = os.path.join(os.path.expanduser("~"), ".mprester.log.yaml") @@ -233,7 +236,7 @@ def __init__( except Exception: pass - def __enter__(self): + def __enter__(self) -> Self: """Support for "with" context.""" return self diff --git a/pymatgen/ext/optimade.py b/pymatgen/ext/optimade.py index 5c4de8f6218..99db62357b5 100644 --- a/pymatgen/ext/optimade.py +++ b/pymatgen/ext/optimade.py @@ -9,6 +9,7 @@ import requests from tqdm import tqdm +from typing_extensions import Self from pymatgen.core import DummySpecies, Structure from pymatgen.util.due import Doi, due @@ -546,7 +547,7 @@ def refresh_aliases(self, providers_url="https://providers.optimade.org/provider self.aliases = {alias: provider.base_url for alias, provider in structure_providers.items()} # TODO: revisit context manager logic here and in MPRester - def __enter__(self): + def __enter__(self) -> Self: """Support for "with" context.""" return self diff --git a/pymatgen/io/abinit/inputs.py b/pymatgen/io/abinit/inputs.py index 82eef25b127..72a4dab5ed6 100644 --- a/pymatgen/io/abinit/inputs.py +++ b/pymatgen/io/abinit/inputs.py @@ -145,7 +145,7 @@ class ShiftMode(Enum): OneSymmetric = "O" @classmethod - def from_object(cls, obj): + def from_object(cls, obj) -> Self: """ Returns an instance of ShiftMode based on the type of object passed. Converts strings to ShiftMode depending on the initial letter of the string. G for GammaCentered, M for MonkhorstPack, @@ -842,10 +842,9 @@ def to_str(self, post=None, with_structure=True, with_pseudos=True, exclude=None exclude: List of variable names that should be ignored. """ lines = [] - app = lines.append if self.comment: - app("# " + self.comment.replace("\n", "\n#")) + lines.append("# " + self.comment.replace("\n", "\n#")) post = post if post is not None else "" exclude = set(exclude) if exclude is not None else set() @@ -861,7 +860,7 @@ def to_str(self, post=None, with_structure=True, with_pseudos=True, exclude=None for name, value in items: # Build variable, convert to string and append it vname = name + post - app(str(InputVariable(vname, value))) + lines.append(str(InputVariable(vname, value))) out = "\n".join(lines) if not with_pseudos: @@ -1048,37 +1047,7 @@ class BasicMultiDataset: Error = BasicAbinitInputError - @classmethod - def from_inputs(cls, inputs): - """Build object from a list of BasicAbinitInput objects.""" - for inp in inputs: - if any(p1 != p2 for p1, p2 in zip(inputs[0].pseudos, inp.pseudos)): - raise ValueError("Pseudos must be consistent when from_inputs is invoked.") - - # Build BasicMultiDataset from input structures and pseudos and add inputs. - multi = cls( - structure=[inp.structure for inp in inputs], - pseudos=inputs[0].pseudos, - ndtset=len(inputs), - ) - - # Add variables - for inp, new_inp in zip(inputs, multi): - new_inp.set_vars(**inp) - - return multi - - @classmethod - def replicate_input(cls, input, ndtset): - """Construct a multidataset with ndtset from the BasicAbinitInput input.""" - multi = cls(input.structure, input.pseudos, ndtset=ndtset) - - for inp in multi: - inp.set_vars(**input) - - return multi - - def __init__(self, structure: Structure, pseudos, pseudo_dir="", ndtset=1): + def __init__(self, structure: Structure | Sequence[Structure], pseudos, pseudo_dir="", ndtset=1): """ Args: structure: file with the structure, |Structure| object or dictionary with ABINIT geo variable @@ -1118,6 +1087,36 @@ def __init__(self, structure: Structure, pseudos, pseudo_dir="", ndtset=1): assert len(structure) == ndtset self._inputs = [BasicAbinitInput(structure=s, pseudos=pseudos) for s in structure] + @classmethod + def from_inputs(cls, inputs: list[BasicAbinitInput]) -> Self: + """Build object from a list of BasicAbinitInput objects.""" + for inp in inputs: + if any(p1 != p2 for p1, p2 in zip(inputs[0].pseudos, inp.pseudos)): + raise ValueError("Pseudos must be consistent when from_inputs is invoked.") + + # Build BasicMultiDataset from input structures and pseudos and add inputs. + multi = cls( + structure=[inp.structure for inp in inputs], + pseudos=inputs[0].pseudos, + ndtset=len(inputs), + ) + + # Add variables + for inp, new_inp in zip(inputs, multi): + new_inp.set_vars(**inp) + + return multi + + @classmethod + def replicate_input(cls, input, ndtset): + """Construct a multidataset with ndtset from the BasicAbinitInput input.""" + multi = cls(input.structure, input.pseudos, ndtset=ndtset) + + for inp in multi: + inp.set_vars(**input) + + return multi + @property def ndtset(self): """Number of inputs in self.""" diff --git a/pymatgen/io/abinit/netcdf.py b/pymatgen/io/abinit/netcdf.py index 187efcc3002..74781f9bc87 100644 --- a/pymatgen/io/abinit/netcdf.py +++ b/pymatgen/io/abinit/netcdf.py @@ -13,6 +13,7 @@ from monty.dev import requires from monty.functools import lazy_property from monty.string import marquee +from typing_extensions import Self from pymatgen.core.structure import Structure from pymatgen.core.units import ArrayWithUnit @@ -93,7 +94,7 @@ def __init__(self, path): # See also https://github.com/Unidata/netcdf4-python/issues/785 self.rootgrp.set_auto_mask(False) - def __enter__(self): + def __enter__(self) -> Self: """Activated when used in the with statement.""" return self diff --git a/pymatgen/io/abinit/pseudos.py b/pymatgen/io/abinit/pseudos.py index 61ac992bb69..f15ae15a573 100644 --- a/pymatgen/io/abinit/pseudos.py +++ b/pymatgen/io/abinit/pseudos.py @@ -140,22 +140,21 @@ def to_str(self, verbose=0) -> str: """String representation.""" lines: list[str] = [] - app = lines.append - app(f"<{type(self).__name__}: {self.basename}>") - app(" summary: " + self.summary.strip()) - app(f" number of valence electrons: {self.Z_val}") - app(f" maximum angular momentum: {l2str(self.l_max)}") - app(f" angular momentum for local part: {l2str(self.l_local)}") - app(f" XC correlation: {self.xc}") - app(f" supports spin-orbit: {self.supports_soc}") + lines.append(f"<{type(self).__name__}: {self.basename}>") + lines.append(" summary: " + self.summary.strip()) + lines.append(f" number of valence electrons: {self.Z_val}") + lines.append(f" maximum angular momentum: {l2str(self.l_max)}") + lines.append(f" angular momentum for local part: {l2str(self.l_local)}") + lines.append(f" XC correlation: {self.xc}") + lines.append(f" supports spin-orbit: {self.supports_soc}") if self.isnc: - app(f" radius for non-linear core correction: {self.nlcc_radius}") + lines.append(f" radius for non-linear core correction: {self.nlcc_radius}") if self.has_hints: for accuracy in ("low", "normal", "high"): hint = self.hint_for_accuracy(accuracy=accuracy) - app(f" hint for {accuracy} accuracy: {hint}") + lines.append(f" hint for {accuracy} accuracy: {hint}") return "\n".join(lines) @@ -1545,7 +1544,7 @@ def as_table(cls, items): return cls(items) @classmethod - def from_dir(cls, top, exts=None, exclude_dirs="_*"): + def from_dir(cls, top, exts=None, exclude_dirs="_*") -> Self | None: """ Find all pseudos in the directory tree starting from top. diff --git a/pymatgen/io/adf.py b/pymatgen/io/adf.py index 006fc3ba84a..fe23e977c43 100644 --- a/pymatgen/io/adf.py +++ b/pymatgen/io/adf.py @@ -85,21 +85,15 @@ def __init__(self, name, options=None, subkeys=None): """ Initialization method. - Parameters - ---------- - name : str - The name of this key. - options : Sized - The options for this key. Each element can be a primitive object or - a tuple/list with two elements: the first is the name and the second - is a primitive object. - subkeys : Sized - The subkeys for this key. + Args: + name (str): The name of this key. + options : Sized + The options for this key. Each element can be a primitive object or + a tuple/list with two elements: the first is the name and the second is a primitive object. + subkeys (Sized): The subkeys for this key. Raises: - ------ - ValueError - If elements in ``subkeys`` are not ``AdfKey`` objects. + ValueError: If elements in ``subkeys`` are not ``AdfKey`` objects. """ self.name = name self.options = options if options is not None else [] @@ -143,9 +137,8 @@ def __str__(self): Return the string representation of this ``AdfKey``. Notes: - ----- - If this key is 'Atoms' and the coordinates are in Cartesian form, a - different string format will be used. + If this key is 'Atoms' and the coordinates are in Cartesian form, + a different string format will be used. """ adf_str = f"{self.key}" if len(self.options) > 0: @@ -174,19 +167,15 @@ def __eq__(self, other: object) -> bool: return False return str(self) == str(other) - def has_subkey(self, subkey): + def has_subkey(self, subkey: str | AdfKey) -> bool: """ Return True if this AdfKey contains the given subkey. - Parameters - ---------- - subkey : str or AdfKey - A key name or an AdfKey object. + Args: + subkey (str or AdfKey): A key name or an AdfKey object. Returns: - ------- - has : bool - True if this key contains the given key. Otherwise False. + bool: Whether this key contains the given key. """ if isinstance(subkey, str): key = subkey @@ -202,14 +191,11 @@ def add_subkey(self, subkey): """ Add a new subkey to this key. - Parameters - ---------- - subkey : AdfKey - A new subkey. + Args: + subkey (AdfKey): A new subkey. Notes: - ----- - Duplicate check will not be performed if this is an 'Atoms' block. + Duplicate check will not be performed if this is an 'Atoms' block. """ if self.key.lower() == "atoms" or not self.has_subkey(subkey): self.subkeys.append(subkey) @@ -218,10 +204,8 @@ def remove_subkey(self, subkey): """ Remove the given subkey, if existed, from this AdfKey. - Parameters - ---------- - subkey : str or AdfKey - The subkey to remove. + Args: + subkey (str or AdfKey): The subkey to remove. """ if len(self.subkeys) > 0: key = subkey if isinstance(subkey, str) else subkey.key @@ -234,16 +218,13 @@ def add_option(self, option): """ Add a new option to this key. - Parameters - ---------- - option : Sized or str or int or float - A new option to add. This must have the same format with existing - options. + Args: + option : Sized or str or int or float + A new option to add. This must have the same format + with existing options. Raises: - ------ - TypeError - If the format of the given ``option`` is different. + TypeError: If the format of the given ``option`` is different. """ if len(self.options) == 0: self.options.append(option) @@ -253,19 +234,15 @@ def add_option(self, option): raise TypeError("Option type is mismatched!") self.options.append(option) - def remove_option(self, option): + def remove_option(self, option: str | int) -> None: """ Remove an option. - Parameters - ---------- - option : str or int - The name (str) or index (int) of the option to remove. + Args: + option (str | int): The name or index of the option to remove. Raises: - ------ - TypeError - If the option has a wrong type. + TypeError: If the option has a wrong type. """ if len(self.options) > 0: if self._sized_op: @@ -280,19 +257,15 @@ def remove_option(self, option): raise TypeError("``option`` should be an integer index!") self.options.pop(option) - def has_option(self, option): + def has_option(self, option: str) -> bool: """ Return True if the option is included in this key. - Parameters - ---------- - option : str - The option. + Args: + option (str): The option. Returns: - ------- - has : bool - True if the option can be found. Otherwise False will be returned. + bool: Whether the option can be found. """ if len(self.options) == 0: return False @@ -331,29 +304,22 @@ def from_dict(cls, dct: dict) -> Self: return cls(key, options, subkeys) @classmethod - def from_str(cls, string: str) -> AdfKey: + def from_str(cls, string: str) -> Self: """ Construct an AdfKey object from the string. - Parameters - ---------- - string : str - A string. + Args: + string: str Returns: - ------- - adfkey : AdfKey An AdfKey object recovered from the string. Raises: - ------ - ValueError - Currently nested subkeys are not supported. If ``subend`` was found - a ValueError would be raised. + ValueError: Currently nested subkeys are not supported. + If ``subend`` was found a ValueError would be raised. Notes: - ----- - Only the first block key will be returned. + Only the first block key will be returned. """ def is_float(s) -> bool: @@ -418,9 +384,8 @@ class AdfTask(MSONable): Basic task for ADF. All settings in this class are independent of molecules. Notes: - ----- - Unlike other quantum chemistry packages (NWChem, Gaussian, ...), ADF does - not support calculating force/gradient. + Unlike other quantum chemistry packages (NWChem, Gaussian, ...), + ADF does not support calculating force/gradient. """ operations = dict( @@ -445,24 +410,15 @@ def __init__( """ Initialization method. - Parameters - ---------- - operation : str - The target operation. - basis_set : AdfKey - The basis set definitions for this task. Defaults to 'DZ/Large'. - xc : AdfKey - The exchange-correlation functionals. Defaults to PBE. - title : str - The title of this ADF task. - units : AdfKey - The units. Defaults to Angstroms/Degree. - geo_subkeys : Sized - The subkeys for the block key 'GEOMETRY'. - scf : AdfKey - The scf options. - other_directives : Sized - User-defined directives. + Args: + operation (str): The target operation. + basis_set (AdfKey): The basis set definitions for this task. Defaults to 'DZ/Large'. + xc (AdfKey): The exchange-correlation functionals. Defaults to PBE. + title (str): The title of this ADF task. + units (AdfKey): The units. Defaults to Angstroms/Degree. + geo_subkeys (Sized): The subkeys for the block key 'GEOMETRY'. + scf (AdfKey): The scf options. + other_directives (Sized): User-defined directives. """ if operation not in self.operations: raise AdfInputError(f"Invalid ADF task {operation}") @@ -504,15 +460,12 @@ def _setup_task(self, geo_subkeys): """ Setup the block 'Geometry' given subkeys and the task. - Parameters - ---------- - geo_subkeys : Sized - User-defined subkeys for the block 'Geometry'. + Args: + geo_subkeys (Sized): User-defined subkeys for the block 'Geometry'. Notes: - ----- - Most of the run types of ADF are specified in the Geometry block except - the 'AnalyticFreq'. + Most of the run types of ADF are specified in the Geometry + block except the 'AnalyticFreq'. """ self.geo = AdfKey("Geometry", subkeys=geo_subkeys) if self.operation.lower() == "energy": @@ -564,14 +517,10 @@ def from_dict(cls, dct: dict) -> Self: """ Construct a MSONable AdfTask object from the JSON dict. - Parameters - ---------- - dct : dict - A dict of saved attributes. + Args: + dct: A dict of saved attributes. Returns: - ------- - task : AdfTask An AdfTask object recovered from the JSON dict ``d``. """ @@ -597,10 +546,8 @@ def __init__(self, task): """ Initialization method. - Parameters - ---------- - task : AdfTask - An ADF task. + Args: + task (AdfTask): An ADF task. """ self.task = task @@ -608,12 +555,9 @@ def write_file(self, molecule, inp_file): """ Write an ADF input file. - Parameters - ---------- - molecule : Molecule - The molecule for this task. - inpfile : str - The name where the input file will be saved. + Args: + molecule (Molecule): The molecule for this task. + inpfile (str): The name where the input file will be saved. """ mol_blocks = [] atom_block = AdfKey("Atoms", options=["cartesian"]) @@ -642,41 +586,27 @@ class AdfOutput: A basic ADF output file parser. Attributes: - ---------- - is_failed : bool - True is the ADF job is terminated without success. Otherwise False. - is_internal_crash : bool - True if the job is terminated with internal crash. Please read 'TAPE13' - of the ADF manual for more detail. - error : str - The error description. - run_type : str - The RunType of this ADF job. Possible options are: 'SinglePoint', - 'GeometryOptimization', 'AnalyticalFreq' and 'NUmericalFreq'. - final_energy : float - The final molecule energy (a.u). - final_structure : GMolecule - The final structure of the molecule. - energies : Sized - The energy of each cycle. - structures : Sized - The structure of each cycle If geometry optimization is performed. - frequencies : array_like - The frequencies of the molecule. - normal_modes : array_like - The normal modes of the molecule. - freq_type : str - Either 'Analytical' or 'Numerical'. + is_failed (bool): Whether the ADF job is failed. + is_internal_crash (bool): Whether the job crashed. + Please read 'TAPE13' of the ADF manual for more detail. + error (str): The error description. + run_type (str): The RunType of this ADF job. Possible options are: + 'SinglePoint', 'GeometryOptimization', 'AnalyticalFreq' and 'NUmericalFreq'. + final_energy (float): The final molecule energy (a.u). + final_structure (GMolecule): The final structure of the molecule. + energies (Sized): The energy of each cycle. + structures (Sized): The structure of each cycle If geometry optimization is performed. + frequencies (array_like): The frequencies of the molecule. + normal_modes (array_like): The normal modes of the molecule. + freq_type (syr): Either 'Analytical' or 'Numerical'. """ def __init__(self, filename): """ Initialization method. - Parameters - ---------- - filename : str - The ADF output file to parse. + Args: + filename (str): The ADF output file to parse. """ self.filename = filename self._parse() @@ -714,15 +644,11 @@ def _sites_to_mol(sites): """ Return a ``Molecule`` object given a list of sites. - Parameters - ---------- - sites : list - A list of sites. + Args: + sites : A list of sites. Returns: - ------- - mol : Molecule - A ``Molecule`` object. + mol (Molecule): A ``Molecule`` object. """ return Molecule([site[0] for site in sites], [site[1] for site in sites]) diff --git a/pymatgen/io/aims/inputs.py b/pymatgen/io/aims/inputs.py index 128f8d89b26..e197ee9f7f0 100644 --- a/pymatgen/io/aims/inputs.py +++ b/pymatgen/io/aims/inputs.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + __author__ = "Thomas A. R. Purcell" __version__ = "1.0" __email__ = "purcellt@arizona.edu" @@ -41,7 +43,7 @@ class AimsGeometryIn(MSONable): _structure: Structure | Molecule @classmethod - def from_str(cls, contents: str) -> AimsGeometryIn: + def from_str(cls, contents: str) -> Self: """Create an input from the content of an input file Args: @@ -105,7 +107,7 @@ def from_str(cls, contents: str) -> AimsGeometryIn: return cls(_content="\n".join(content_lines), _structure=structure) @classmethod - def from_file(cls, filepath: str | Path) -> AimsGeometryIn: + def from_file(cls, filepath: str | Path) -> Self: """Create an AimsGeometryIn from an input file. Args: @@ -123,7 +125,7 @@ def from_file(cls, filepath: str | Path) -> AimsGeometryIn: return cls.from_str(content) @classmethod - def from_structure(cls, structure: Structure | Molecule) -> AimsGeometryIn: + def from_structure(cls, structure: Structure | Molecule) -> Self: """Construct an input file from an input structure. Args: @@ -190,7 +192,7 @@ def as_dict(self) -> dict[str, Any]: return dct @classmethod - def from_dict(cls, dct: dict[str, Any]) -> AimsGeometryIn: + def from_dict(cls, dct: dict[str, Any]) -> Self: """Initialize from dictionary. Args: @@ -378,7 +380,7 @@ def as_dict(self) -> dict[str, Any]: return dct @classmethod - def from_dict(cls, dct: dict[str, Any]) -> AimsCube: + def from_dict(cls, dct: dict[str, Any]) -> Self: """Initialize from dictionary. Args: @@ -634,7 +636,7 @@ def as_dict(self) -> dict[str, Any]: return dct @classmethod - def from_dict(cls, dct: dict[str, Any]) -> AimsControlIn: + def from_dict(cls, dct: dict[str, Any]) -> Self: """Initialize from dictionary. Args: diff --git a/pymatgen/io/aims/outputs.py b/pymatgen/io/aims/outputs.py index b6a189dece7..9c33c414f85 100644 --- a/pymatgen/io/aims/outputs.py +++ b/pymatgen/io/aims/outputs.py @@ -19,6 +19,7 @@ from pathlib import Path from emmet.core.math import Matrix3D, Vector3D + from typing_extensions import Self from pymatgen.core import Molecule, Structure @@ -61,7 +62,7 @@ def as_dict(self) -> dict[str, Any]: return dct @classmethod - def from_outfile(cls, outfile: str | Path) -> AimsOutput: + def from_outfile(cls, outfile: str | Path) -> Self: """Construct an AimsOutput from an output file. Args: @@ -76,7 +77,7 @@ def from_outfile(cls, outfile: str | Path) -> AimsOutput: return cls(results, metadata, structure_summary) @classmethod - def from_str(cls, content: str) -> AimsOutput: + def from_str(cls, content: str) -> Self: """Construct an AimsOutput from an output file. Args: @@ -91,7 +92,7 @@ def from_str(cls, content: str) -> AimsOutput: return cls(results, metadata, structure_summary) @classmethod - def from_dict(cls, dct: dict[str, Any]) -> AimsOutput: + def from_dict(cls, dct: dict[str, Any]) -> Self: """Construct an AimsOutput from a dictionary. Args: diff --git a/pymatgen/io/aims/sets/base.py b/pymatgen/io/aims/sets/base.py index c1e4c14b210..5612cdd5016 100644 --- a/pymatgen/io/aims/sets/base.py +++ b/pymatgen/io/aims/sets/base.py @@ -208,9 +208,8 @@ def get_input_set( # type: ignore properties: list[str] System properties that are being calculated - Returns - ------- - The input set for the calculation of structure + Returns: + AimsInputSet: The input set for the calculation of structure """ prev_structure, prev_parameters, _ = self._read_previous(prev_dir) @@ -274,9 +273,8 @@ def _get_properties( parameters: dict[str, Any] The parameters for this calculation - Returns - ------- - The list of properties to calculate + Returns: + list[str]: The list of properties to calculate """ if properties is None: properties = ["energy", "free_energy"] @@ -310,9 +308,8 @@ def _get_input_parameters( prev_parameters: dict[str, Any] The previous calculation's calculation parameters - Returns - ------- - The input object + Returns: + dict: The input object """ # Get the default configuration # FHI-aims recommends using their defaults so bare-bones default parameters @@ -363,9 +360,8 @@ def get_parameter_updates(self, structure: Structure | Molecule, prev_parameters prev_parameters: dict[str, Any] Previous calculation parameters. - Returns - ------- - A dictionary of updates to apply. + Returns: + dict: A dictionary of updates to apply. """ return prev_parameters @@ -388,9 +384,8 @@ def d2k( even: bool Round up to even numbers. - Returns - ------- - Monkhorst-Pack grid size in all directions + Returns: + dict: Monkhorst-Pack grid size in all directions """ recipcell = structure.lattice.inv_matrix return self.d2k_recipcell(recipcell, structure.lattice.pbc, kptdensity, even) @@ -405,9 +400,8 @@ def k2d(self, structure: Structure, k_grid: np.ndarray[int]): k_grid: np.ndarray[int] k_grid that was used. - Returns - ------- - Density of kpoints in each direction. result.mean() computes average density + Returns: + dict: Density of kpoints in each direction. result.mean() computes average density """ recipcell = structure.lattice.inv_matrix densities = k_grid / (2 * np.pi * np.sqrt((recipcell**2).sum(axis=1))) @@ -433,9 +427,8 @@ def d2k_recipcell( even: bool Round up to even numbers. - Returns - ------- - Monkhorst-Pack grid size in all directions + Returns: + dict: Monkhorst-Pack grid size in all directions """ if not isinstance(kptdensity, Iterable): kptdensity = 3 * [float(kptdensity)] @@ -461,9 +454,8 @@ def recursive_update(dct: dict, up: dict) -> dict: dct (dict): Input dictionary to modify up (dict): updates to apply - Returns - ------- - The updated dictionary. + Returns: + dict: The updated dictionary. Example ------- diff --git a/pymatgen/io/aims/sets/bs.py b/pymatgen/io/aims/sets/bs.py index 89cab4f0ada..7f3416d8dcf 100644 --- a/pymatgen/io/aims/sets/bs.py +++ b/pymatgen/io/aims/sets/bs.py @@ -88,9 +88,8 @@ def get_parameter_updates( prev_parameters: Dict[str, Any] The previous parameters - Returns - ------- - The updated for the parameters for the output section of FHI-aims + Returns: + dict: The updated for the parameters for the output section of FHI-aims """ if isinstance(structure, Molecule): raise ValueError("BandStructures can not be made for non-periodic systems") @@ -126,9 +125,8 @@ def get_parameter_updates(self, structure: Structure | Molecule, prev_parameters prev_parameters: Dict[str, Any] The previous parameters - Returns - ------- - The updated for the parameters for the output section of FHI-aims + Returns: + dict: The updated for the parameters for the output section of FHI-aims """ updates = {"anacon_type": "two-pole"} current_output = prev_parameters.get("output", []) diff --git a/pymatgen/io/aims/sets/core.py b/pymatgen/io/aims/sets/core.py index e1b4880367a..389970abd4e 100644 --- a/pymatgen/io/aims/sets/core.py +++ b/pymatgen/io/aims/sets/core.py @@ -34,9 +34,8 @@ def get_parameter_updates(self, structure: Structure | Molecule, prev_parameters prev_parameters: Dict[str, Any] The previous parameters - Returns - ------- - The updated for the parameters for the output section of FHI-aims + Returns: + dict: The updated for the parameters for the output section of FHI-aims """ return prev_parameters @@ -75,9 +74,8 @@ def get_parameter_updates(self, structure: Structure | Molecule, prev_parameters prev_parameters: Dict[str, Any] The previous parameters - Returns - ------- - The updated for the parameters for the output section of FHI-aims + Returns: + dict: The updated for the parameters for the output section of FHI-aims """ updates = {"relax_geometry": f"{self.method} {self.max_force:e}"} if isinstance(structure, Structure) and self.relax_cell: @@ -116,8 +114,7 @@ def get_parameter_updates(self, structure: Structure | Molecule, prev_parameters prev_parameters: Dict[str, Any] The previous parameters - Returns - ------- - The updated for the parameters for the output section of FHI-aims + Returns: + dict: The updated for the parameters for the output section of FHI-aims """ return {"use_pimd_wrapper": (self.host, self.port)} diff --git a/pymatgen/io/ase.py b/pymatgen/io/ase.py index 03ee711e58a..7981c845fe1 100644 --- a/pymatgen/io/ase.py +++ b/pymatgen/io/ase.py @@ -35,6 +35,7 @@ def __init__(self, *args, **kwargs): from typing import Any from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core.structure import SiteCollection @@ -63,12 +64,13 @@ def as_dict(atoms: Atoms) -> dict[str, Any]: "atoms_info": jsanitize(atoms.info, strict=True), } - def from_dict(dct: dict[str, Any]) -> MSONAtoms: + @classmethod + def from_dict(cls, dct: dict[str, Any]) -> Self: # Normally, we would want to this to be a wrapper around atoms.fromdict() with @module and # @class key-value pairs inserted. However, atoms.todict()/atoms.fromdict() is not meant # to be used in a round-trip fashion and does not work properly with constraints. # See ASE issue #1387. - mson_atoms = MSONAtoms(decode(dct["atoms_json"])) + mson_atoms = cls(decode(dct["atoms_json"])) atoms_info = MontyDecoder().process_decoded(dct["atoms_info"]) mson_atoms.info = atoms_info return mson_atoms diff --git a/pymatgen/io/cif.py b/pymatgen/io/cif.py index d5456a8d747..076025ed4ef 100644 --- a/pymatgen/io/cif.py +++ b/pymatgen/io/cif.py @@ -31,6 +31,8 @@ from pymatgen.util.coord import find_in_coord_list_pbc, in_coord_list_pbc if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core.trajectory import Vector3D __author__ = "Shyue Ping Ong, Will Richards, Matthew Horton" @@ -168,7 +170,7 @@ def _process_string(cls, string): return deq @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """ Reads CifBlock from string. @@ -180,7 +182,7 @@ def from_str(cls, string): """ q = cls._process_string(string) header = q.popleft()[0][5:] - data = {} + data: dict = {} loops = [] while q: s = q.popleft() @@ -235,7 +237,7 @@ def __str__(self): return f"{self.comment}\n{out}\n" @classmethod - def from_str(cls, string) -> CifFile: + def from_str(cls, string: str) -> Self: """Reads CifFile from a string. Args: @@ -262,7 +264,7 @@ def from_str(cls, string) -> CifFile: return cls(dct, string) @classmethod - def from_file(cls, filename: str | Path) -> CifFile: + def from_file(cls, filename: str | Path) -> Self: """ Reads CifFile from a filename. @@ -365,7 +367,7 @@ def is_magcif_incommensurate() -> bool: self._cif.data[key] = self._sanitize_data(self._cif.data[key]) @classmethod - def from_str(cls, cif_string: str, **kwargs) -> CifParser: + def from_str(cls, cif_string: str, **kwargs) -> Self: """ Creates a CifParser from a string. @@ -590,6 +592,8 @@ def _unique_coords( # Up to this point, magmoms have been defined relative # to crystal axis. Now convert to Cartesian and into # a Magmom object. + if lattice is None: + raise ValueError("Lattice cannot be None.") magmom = Magmom.from_moment_relative_to_crystal_axes( op.operate_magmom(tmp_magmom), lattice=lattice ) diff --git a/pymatgen/io/common.py b/pymatgen/io/common.py index 21913435a24..60484cdeb49 100644 --- a/pymatgen/io/common.py +++ b/pymatgen/io/common.py @@ -6,6 +6,7 @@ import json import warnings from copy import deepcopy +from typing import TYPE_CHECKING import numpy as np from monty.io import zopen @@ -16,6 +17,11 @@ from pymatgen.core.units import ang_to_bohr, bohr_to_angstrom from pymatgen.electronic_structure.core import Spin +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + class VolumetricData(MSONable): """ @@ -302,7 +308,7 @@ def to_hdf5(self, filename): file.attrs["structure_json"] = json.dumps(self.structure.as_dict()) @classmethod - def from_hdf5(cls, filename, **kwargs): + def from_hdf5(cls, filename: str, **kwargs) -> Self: """ Reads VolumetricData from HDF5 file. @@ -356,7 +362,7 @@ def to_cube(self, filename, comment=None): file.write("\n") @classmethod - def from_cube(cls, filename): + def from_cube(cls, filename: str | Path) -> Self: """ Initialize the cube object and store the data as data. diff --git a/pymatgen/io/core.py b/pymatgen/io/core.py index 0064c9a4af5..898ec149e70 100644 --- a/pymatgen/io/core.py +++ b/pymatgen/io/core.py @@ -39,6 +39,7 @@ if TYPE_CHECKING: from os import PathLike + __author__ = "Ryan Kingsbury" __email__ = "RKingsbury@lbl.gov" __status__ = "Development" @@ -75,7 +76,7 @@ def write_file(self, filename: str | PathLike) -> None: @classmethod @abc.abstractmethod - def from_str(cls, contents: str) -> InputFile: + def from_str(cls, contents: str) -> None: """ Create an InputFile object from a string. @@ -85,9 +86,10 @@ def from_str(cls, contents: str) -> InputFile: Returns: InputFile """ + raise NotImplementedError(f"from_str has not been implemented in {cls.__name__}") @classmethod - def from_file(cls, path: str | Path): + def from_file(cls, path: str | Path) -> None: """ Creates an InputFile object from a file. @@ -99,7 +101,7 @@ def from_file(cls, path: str | Path): """ filename = path if isinstance(path, Path) else Path(path) with zopen(filename, mode="rt") as file: - return cls.from_str(file.read()) + return cls.from_str(file.read()) # from_str not implemented def __str__(self) -> str: return self.get_str() @@ -226,7 +228,7 @@ def write_input( pass @classmethod - def from_directory(cls, directory: str | Path): + def from_directory(cls, directory: str | Path) -> None: """ Construct an InputSet from a directory of one or more files. @@ -253,11 +255,12 @@ class InputGenerator(MSONable): """ @abc.abstractmethod - def get_input_set(self) -> InputSet: + def get_input_set(self, *args, **kwargs): """ Generate an InputSet object. Typically the first argument to this method will be a Structure or other form of atomic coordinates. """ + raise NotImplementedError(f"get_input_set has not been implemented in {type(self).__name__}") class ParseError(SyntaxError): diff --git a/pymatgen/io/cp2k/inputs.py b/pymatgen/io/cp2k/inputs.py index 343aecbec74..b97f1fedae4 100644 --- a/pymatgen/io/cp2k/inputs.py +++ b/pymatgen/io/cp2k/inputs.py @@ -125,16 +125,16 @@ def __getitem__(self, item): def as_dict(self): """Get a dictionary representation of the Keyword.""" - dct = {} - dct["@module"] = type(self).__module__ - dct["@class"] = type(self).__name__ - dct["name"] = self.name - dct["values"] = self.values - dct["description"] = self.description - dct["repeats"] = self.repeats - dct["units"] = self.units - dct["verbose"] = self.verbose - return dct + return { + "@module": type(self).__module__, + "@class": type(self).__name__, + "name": self.name, + "values": self.values, + "description": self.description, + "repeats": self.repeats, + "units": self.units, + "verbose": self.verbose, + } def get_str(self) -> str: """String representation of Keyword.""" @@ -153,7 +153,7 @@ def from_dict(cls, dct: dict) -> Self: ) @classmethod - def from_str(cls, s): + def from_str(cls, s: str) -> Self: """ Initialize from a string. @@ -172,8 +172,8 @@ def from_str(cls, s): description = None units = re.findall(r"\[(.*)\]", s) or [None] s = re.sub(r"\[(.*)\]", "", s) - args = s.split() - args = list(map(postprocessor if args[0].upper() != "ELEMENT" else str, args)) + args: list[Any] = s.split() + args = list(map(postprocessor if args[0].upper() != "ELEMENT" else str, args)) # type: ignore[call-overload] args[0] = str(args[0]) return cls(*args, units=units[0], description=description) @@ -289,12 +289,12 @@ def __init__( Keyword objects """ self.name = name - self.subsections = subsections if subsections else {} + self.subsections = subsections or {} self.repeats = repeats self.description = description - keywords = keywords if keywords else {} + keywords = keywords or {} self.keywords = keywords - self.section_parameters = section_parameters if section_parameters else [] + self.section_parameters = section_parameters or [] self.location = location self.verbose = verbose self.alias = alias @@ -319,10 +319,7 @@ def __deepcopy__(self, memodict=None): ) def __getitem__(self, d): - r = self.get_keyword(d) - if not r: - r = self.get_section(d) - if r: + if r := self.get_keyword(d) or self.get_section(d): return r raise KeyError @@ -335,7 +332,7 @@ def __add__(self, other) -> Section: elif isinstance(other, (Section, SectionList)): self.insert(other) else: - TypeError("Can only add sections or keywords.") + raise TypeError("Can only add sections or keywords.") return self @@ -357,8 +354,7 @@ def setitem(self, key, value, strict=False): else: if not isinstance(value, (Keyword, KeywordList)): value = Keyword(key, value) - match = [k for k in self.keywords if key.upper() == k.upper()] - if match: + if match := [k for k in self.keywords if key.upper() == k.upper()]: del self.keywords[match[0]] self.keywords[key] = value elif not strict: @@ -369,14 +365,14 @@ def __delitem__(self, key): Delete section with name matching key OR delete all keywords with names matching this key. """ - lst = [sub_sec for sub_sec in self.subsections if sub_sec.upper() == key.upper()] - if lst: + if lst := [sub_sec for sub_sec in self.subsections if sub_sec.upper() == key.upper()]: del self.subsections[lst[0]] return - lst = [kw for kw in self.keywords if kw.upper() == key.upper()] - if lst: + + if lst := [kw for kw in self.keywords if kw.upper() == key.upper()]: del self.keywords[lst[0]] return + raise KeyError("No section or keyword matching the given key.") def __sub__(self, other): @@ -397,8 +393,7 @@ def get(self, d, default=None): d: the key to retrieve, if present default: what to return if d is not found """ - kw = self.get_keyword(d) - if kw: + if kw := self.get_keyword(d): return kw sec = self.get_section(d) if sec: @@ -466,14 +461,14 @@ def _update(d1, d2, strict=False): elif isinstance(v, (Keyword, KeywordList)): d1.setitem(k, v, strict=strict) elif isinstance(v, dict): - tmp = [_ for _ in d1.subsections if k.upper() == _.upper()] - if not tmp: + if tmp := [_ for _ in d1.subsections if k.upper() == _.upper()]: + Section._update(d1.subsections[tmp[0]], v, strict=strict) + else: if strict: continue d1.insert(Section(k, subsections={})) Section._update(d1.subsections[k], v, strict=strict) - else: - Section._update(d1.subsections[tmp[0]], v, strict=strict) + elif isinstance(v, Section): if not strict: d1.insert(v) @@ -498,7 +493,7 @@ def unset(self, dct: dict): elif isinstance(v, dict): self[k].unset(v) else: - TypeError("Can only add sections or keywords.") + raise TypeError("Can only add sections or keywords.") def inc(self, dct: dict): """Mongo style dict modification. Include.""" @@ -510,7 +505,7 @@ def inc(self, dct: dict): elif isinstance(val, dict): self[key].inc(val) else: - TypeError("Can only add sections or keywords.") + raise TypeError("Can only add sections or keywords.") def insert(self, d): """Insert a new section as a subsection of the current one.""" @@ -528,8 +523,7 @@ def check(self, path: str): _path = path.split("/") s = self.subsections for p in _path: - tmp = [_ for _ in s if p.upper() == _.upper()] - if tmp: + if tmp := [_ for _ in s if p.upper() == _.upper()]: s = s[tmp[0]].subsections else: return False @@ -679,7 +673,7 @@ class Cp2kInput(Section): def __init__(self, name: str = "CP2K_INPUT", subsections: dict | None = None, **kwargs): """Initialize Cp2kInput by calling the super.""" self.name = name - self.subsections = subsections if subsections else {} + self.subsections = subsections or {} self.kwargs = kwargs description = "CP2K Input" @@ -713,14 +707,14 @@ def _from_dict(cls, dct): ) @classmethod - def from_file(cls, filename: str): + def from_file(cls, filename: str | Path) -> Self: """Initialize from a file.""" with zopen(filename, mode="rt") as file: txt = preprocessor(file.read(), os.path.dirname(file.name)) return cls.from_str(txt) @classmethod - def from_str(cls, s: str): + def from_str(cls, s: str) -> Self: """Initialize from a string.""" lines = s.splitlines() lines = [line.replace("\t", "") for line in lines] @@ -729,7 +723,7 @@ def from_str(cls, s: str): return cls.from_lines(lines) @classmethod - def from_lines(cls, lines: list | tuple): + def from_lines(cls, lines: list | tuple) -> Self: """Helper method to read lines of file.""" cp2k_input = Cp2kInput("CP2K_INPUT", subsections={}) Cp2kInput._from_lines(cp2k_input, lines) @@ -756,8 +750,7 @@ def _from_lines(self, lines): name, section_parameters=subsection_params, alias=alias, subsections={}, description=description ) description = "" - tmp = self.by_path(current).get_section(sec.alias or sec.name) - if tmp: + if tmp := self.by_path(current).get_section(sec.alias or sec.name): if isinstance(tmp, SectionList): self.by_path(current)[sec.alias or sec.name].append(sec) else: @@ -767,8 +760,7 @@ def _from_lines(self, lines): current = f"{current}/{alias or name}" else: kwd = Keyword.from_str(line) - tmp = self.by_path(current).get(kwd.name) - if tmp: + if tmp := self.by_path(current).get(kwd.name): if isinstance(tmp, KeywordList): self.by_path(current).get(kwd.name).append(kwd) elif isinstance(self.by_path(current), SectionList): @@ -796,7 +788,7 @@ def write_file( if not os.path.isdir(output_dir) and make_dir_if_not_present: os.mkdir(output_dir) filepath = os.path.join(output_dir, input_filename) - with open(filepath, mode="w") as file: + with open(filepath, mode="w", encoding="utf-8") as file: file.write(self.get_str()) @@ -819,7 +811,7 @@ def __init__( """ self.project_name = project_name self.run_type = run_type - keywords = keywords if keywords else {} + keywords = keywords or {} description = ( "Section with general information regarding which kind of simulation to perform an general settings" @@ -845,8 +837,8 @@ class ForceEval(Section): def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs): """Initialize the ForceEval section.""" - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Parameters needed to calculate energy and forces and describe the system you want to analyze." @@ -895,8 +887,8 @@ def __init__( self.potential_filename = potential_filename self.uks = uks self.wfn_restart_file_name = wfn_restart_file_name - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Parameter needed by dft programs" @@ -928,8 +920,8 @@ class Subsys(Section): def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs): """Initialize the subsys section.""" - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "A subsystem: coordinates, topology, molecules and cell" super().__init__("SUBSYS", keywords=keywords, description=description, subsections=subsections, **kwargs) @@ -969,8 +961,8 @@ def __init__( self.eps_default = eps_default self.eps_pgf_orb = eps_pgf_orb self.extrapolation = extrapolation - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Parameters needed to set up the Quickstep framework" _keywords = { @@ -1028,8 +1020,8 @@ def __init__( self.eps_scf = eps_scf self.scf_guess = scf_guess - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Parameters needed to perform an SCF run." @@ -1086,8 +1078,8 @@ def __init__( self.ngrids = ngrids self.progression_factor = progression_factor - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = ( "Multigrid information. Multigrid allows for sharp gaussians and diffuse " "gaussians to be treated on different grids, where the spacing of FFT integration " @@ -1132,8 +1124,8 @@ def __init__( self.eps_iter = eps_iter self.eps_jacobi = eps_jacobi self.jacobi_threshold = jacobi_threshold - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} location = "CP2K_INPUT/FORCE_EVAL/DFT/SCF/DIAGONALIZATION" description = "Settings for the SCF's diagonalization routines" @@ -1189,8 +1181,8 @@ def __init__( """ self.new_prec_each = new_prec_each self.preconditioner = preconditioner - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} _keywords = { "NEW_PREC_EACH": Keyword("NEW_PREC_EACH", new_prec_each), "PRECONDITIONER": Keyword("PRECONDITIONER", preconditioner), @@ -1268,8 +1260,8 @@ def __init__( self.occupation_preconditioner = occupation_preconditioner self.energy_gap = energy_gap self.linesearch = linesearch - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = ( "Sets the various options for the orbital transformation (OT) method. " @@ -1314,7 +1306,7 @@ def __init__(self, lattice: Lattice, keywords: dict | None = None, **kwargs): keywords: additional keywords """ self.lattice = lattice - keywords = keywords if keywords else {} + keywords = keywords or {} description = "Lattice parameters and optional settings for creating a the CELL" _keywords = { @@ -1371,8 +1363,8 @@ def __init__( self.potential = potential self.ghost = ghost or False # if None, set False self.aux_basis = aux_basis - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "The description of this kind of atom including basis sets, element, etc." # Special case for closed-shell elements. Cannot impose magnetization in cp2k. @@ -1400,7 +1392,7 @@ def __init__( else aux_basis.get_keyword() ) - kind_name = alias if alias else specie.__str__() + kind_name = alias or specie.__str__() alias = kind_name section_parameters = [kind_name] @@ -1451,8 +1443,8 @@ def __init__( self.l = l self.u_minus_j = u_minus_j self.u_ramping = u_ramping - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Settings for on-site Hubbard +U correction for this atom kind." _keywords = { @@ -1487,8 +1479,8 @@ def __init__( """ self.structure = structure self.aliases = aliases - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = ( "The coordinates for simple systems (like small QM cells) are specified " "here by default using explicit XYZ coordinates. More complex systems " @@ -1525,8 +1517,8 @@ def __init__(self, ndigits: int = 6, keywords: dict | None = None, subsections: subsections: additional subsections """ self.ndigits = ndigits - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Controls printing of the overall density of states" _keywords = {"NDIGITS": Keyword("NDIGITS", ndigits)} keywords.update(_keywords) @@ -1549,8 +1541,8 @@ def __init__(self, nlumo: int = -1, keywords: dict | None = None, subsections: d subsections: additional subsections """ self.nlumo = nlumo - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Controls printing of the projected density of states" _keywords = {"NLUMO": Keyword("NLUMO", nlumo), "COMPONENTS": Keyword("COMPONENTS")} @@ -1579,8 +1571,8 @@ def __init__( subsections: additional subsections """ self.index = index - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Controls printing of the projected density of states decomposed by atom type" _keywords = {"COMPONENTS": Keyword("COMPONENTS"), "LIST": Keyword("LIST", index)} keywords.update(_keywords) @@ -1598,8 +1590,8 @@ class V_Hartree_Cube(Section): """Controls printing of the hartree potential as a cube file.""" def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs): - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = ( "Controls the printing of a cube file with eletrostatic potential generated by " "the total density (electrons+ions). It is valid only for QS with GPW formalism. " @@ -1630,8 +1622,8 @@ def __init__( self.write_cube = write_cube self.nhomo = nhomo self.nlumo = nlumo - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = ( "Controls the printing of a cube file with eletrostatic potential generated by " "the total density (electrons+ions). It is valid only for QS with GPW formalism. " @@ -1657,8 +1649,8 @@ class E_Density_Cube(Section): """Controls printing of the electron density cube file.""" def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs): - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = ( "Controls the printing of cube files with the electronic density and, for LSD " "calculations, the spin density." @@ -1688,8 +1680,8 @@ def __init__( self.elec_temp = elec_temp self.method = method self.fixed_magnetic_moment = fixed_magnetic_moment - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} description = "Activates smearing of electron occupations" _keywords = { @@ -1771,7 +1763,7 @@ def __init__( ) @classmethod - def from_el(cls, el, oxi_state=0, spin=0): + def from_el(cls, el: Element, oxi_state: int = 0, spin: int = 0) -> Self: """Create section from element, oxidation state, and spin.""" el = el if isinstance(el, Element) else Element(el) @@ -1795,7 +1787,7 @@ def f3(x): nel_beta = [] n_alpha = [] n_beta = [] - unpaired_orbital = None + unpaired_orbital: tuple[int, int, int] = (0, 0, 0) while tmp: tmp2 = -min((esv[0][2], tmp)) if tmp > 0 else min((f2(esv[0][1]) - esv[0][2], -tmp)) l_alpha.append(esv[0][1]) @@ -1808,6 +1800,9 @@ def f3(x): unpaired_orbital = esv[0][0], esv[0][1], esv[0][2] + tmp2 esv.pop(0) + if unpaired_orbital is None: + raise ValueError("unpaired_orbital cannot be None.") + if spin == "low-up": spin = unpaired_orbital[2] % 2 elif spin == "low-down": @@ -1846,9 +1841,9 @@ def __init__( subsections: dict | None = None, **kwargs, ): - self.functionals = functionals if functionals else [] - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + self.functionals = functionals or [] + keywords = keywords or {} + subsections = subsections or {} location = "CP2K_INPUT/FORCE_EVAL/DFT/XC/XC_FUNCTIONAL" for functional in self.functionals: @@ -1889,8 +1884,8 @@ def __init__( self.parameterization = parameterization self.scale_c = scale_c self.scale_x = scale_x - keywords = keywords if keywords else {} - subsections = subsections if subsections else {} + keywords = keywords or {} + subsections = subsections or {} location = "CP2K_INPUT/FORCE_EVAL/DFT/XC/XC_FUNCTIONAL/PBE" @@ -1956,7 +1951,7 @@ def __init__( keywords = {} self.kpts = kpts - self.weights = weights if weights else [1] * len(kpts) + self.weights = weights or [1] * len(kpts) assert len(self.kpts) == len(self.weights) self.eps_geo = eps_geo self.full_grid = full_grid @@ -1996,7 +1991,7 @@ def __init__( ) @classmethod - def from_kpoints(cls, kpoints: VaspKpoints, structure=None): + def from_kpoints(cls, kpoints: VaspKpoints, structure=None) -> Self: """ Initialize the section from a Kpoints object (pymatgen.io.vasp.inputs). CP2K does not have an automatic gamma-point constructor, so this is generally used @@ -2016,10 +2011,7 @@ def from_kpoints(cls, kpoints: VaspKpoints, structure=None): if kpoints.style == KpointsSupportedModes.Monkhorst: k = kpts[0] - if isinstance(k, (int, float)): - x, y, z = k, k, k - else: - x, y, z = k + x, y, z = (k, k, k) if isinstance(k, (int, float)) else k scheme = f"MONKHORST-PACK {x} {y} {z}" units = "B_VECTOR" elif kpoints.style == KpointsSupportedModes.Reciprocal: @@ -2113,7 +2105,7 @@ def __init__( self.kpoint_sets = SectionList(kpoint_sets) self.filename = filename self.added_mos = added_mos - keywords = keywords if keywords else {} + keywords = keywords or {} _keywords = { "FILE_NAME": Keyword("FILE_NAME", filename), "ADDED_MOS": Keyword("ADDED_MOS", added_mos), @@ -2219,7 +2211,7 @@ def softmatch(self, other): return all(not (v is not None and v != d2[k]) for k, v in d1.items()) @classmethod - def from_str(cls, string: str) -> BasisInfo: + def from_str(cls, string: str) -> Self: """Get summary info from a string.""" string = string.upper() data: dict[str, Any] = {} @@ -2420,7 +2412,7 @@ def get_str(self) -> str: return out @classmethod - def from_str(cls, string: str) -> GaussianTypeOrbitalBasisSet: + def from_str(cls, string: str) -> Self: """Read from standard cp2k GTO formatted string.""" lines = [line for line in string.split("\n") if line] firstline = lines[0].split() @@ -2450,7 +2442,7 @@ def from_str(cls, string: str) -> GaussianTypeOrbitalBasisSet: line_index = 2 for set_index in range(nset): setinfo = lines[line_index].split() - _n, _lmin, _lmax, _nexp = map(int, setinfo[0:4]) + _n, _lmin, _lmax, _nexp = map(int, setinfo[:4]) n.append(_n) lmin.append(_lmin) lmax.append(_lmax) @@ -2520,10 +2512,10 @@ def softmatch(self, other): return all(not (v is not None and v != d2[k]) for k, v in d1.items()) @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """Get a cp2k formatted string representation.""" string = string.upper() - data = {} + data: dict[str, Any] = {} if "NLCC" in string: data["nlcc"] = True if "GTH" in string: @@ -2614,7 +2606,7 @@ def get_section(self) -> Section: ) @classmethod - def from_section(cls, section: Section) -> GthPotential: + def from_section(cls, section: Section) -> Self: """Extract GTH-formatted string from a section and convert it to model.""" sec = copy.deepcopy(section) sec.verbosity(verbosity=False) @@ -2656,7 +2648,7 @@ def get_str(self) -> str: return out @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """Initialize model from a GTH formatted string.""" lines = [line for line in string.split("\n") if line] firstline = lines[0].split() @@ -2683,9 +2675,9 @@ def from_str(cls, string): ) nprj = int(lines[3].split()[0]) if len(lines) > 3 else 0 - radii = {} - nprj_ppnl = {} - hprj_ppnl = {} + radii: dict[int, float] = {} + nprj_ppnl: dict[int, int] = {} + hprj_ppnl: dict[int, dict] = {} lines = lines[4:] i = 0 ll = 0 @@ -2696,8 +2688,8 @@ def from_str(cls, string): radii[ll] = float(line[0]) nprj_ppnl[ll] = int(line[1]) hprj_ppnl[ll] = {x: {} for x in range(nprj_ppnl[ll])} - line = list(map(float, line[2:])) - hprj_ppnl[ll][0] = {j: float(ln) for j, ln in enumerate(line)} + _line = [float(i) for i in line[2:]] + hprj_ppnl[ll][0] = {j: float(ln) for j, ln in enumerate(_line)} L = 1 i += 1 @@ -2732,22 +2724,23 @@ class DataFile(MSONable): objects: Sequence | None = None @classmethod - def from_file(cls, filename): + def from_file(cls, filename) -> None: """Load from a file.""" - with open(filename) as file: - data = cls.from_str(file.read()) - for obj in data.objects: - obj.filename = filename - return data + raise NotImplementedError + # with open(filename, encoding="utf-8") as file: + # data = cls.from_str(file.read()) + # for obj in data.objects: + # obj.filename = filename + # return data @classmethod - def from_str(cls): + def from_str(cls) -> None: """Initialize from a string.""" raise NotImplementedError def write_file(self, filename): """Write to a file.""" - with open(filename, mode="w") as file: + with open(filename, mode="w", encoding="utf-8") as file: file.write(self.get_str()) def get_str(self) -> str: @@ -2763,7 +2756,7 @@ class BasisFile(DataFile): """Data file for basis sets only.""" @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: # type: ignore[override] """Initialize from a string representation.""" basis_sets = [GaussianTypeOrbitalBasisSet.from_str(c) for c in chunk(string)] return cls(objects=basis_sets) @@ -2774,7 +2767,7 @@ class PotentialFile(DataFile): """Data file for potentials only.""" @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: # type: ignore[override] """Initialize from a string representation.""" basis_sets = [GthPotential.from_str(c) for c in chunk(string)] return cls(objects=basis_sets) diff --git a/pymatgen/io/cp2k/outputs.py b/pymatgen/io/cp2k/outputs.py index 5d4bb4cf6af..9b2118c96f8 100644 --- a/pymatgen/io/cp2k/outputs.py +++ b/pymatgen/io/cp2k/outputs.py @@ -102,8 +102,7 @@ def cp2k_version(self): @property def completed(self): """Did the calculation complete.""" - c = self.data.get("completed", False) - if c: + if c := self.data.get("completed", False): return c[0][0] return c @@ -196,18 +195,12 @@ def is_molecule(self) -> bool: True if the cp2k output was generated for a molecule (i.e. no periodicity in the cell). """ - if self.data.get("poisson_periodicity", [[""]])[0][0].upper() == "NONE": - return True - return False + return self.data.get("poisson_periodicity", [[""]])[0][0].upper() == "NONE" @property def is_metal(self) -> bool: """Was a band gap found? i.e. is it a metal.""" - if self.band_gap is None: - return True - if self.band_gap <= 0: - return True - return False + return True if self.band_gap is None else self.band_gap <= 0 @property def is_hubbard(self) -> bool: @@ -276,7 +269,7 @@ def parse_structures(self, trajectory_file=None, lattice_file=None): default, so non static calculations have to reference the trajectory file. """ self.parse_initial_structure() - trajectory_file = trajectory_file if trajectory_file else self.filenames.get("trajectory") + trajectory_file = trajectory_file or self.filenames.get("trajectory") if isinstance(trajectory_file, list): if len(trajectory_file) == 1: trajectory_file = trajectory_file[0] @@ -300,8 +293,7 @@ def parse_structures(self, trajectory_file=None, lattice_file=None): lattices = [latt[2:].reshape(3, 3) for latt in latt_file] if not trajectory_file: - self.structures = [] - self.structures.append(self.initial_structure) + self.structures = [self.initial_structure] self.final_structure = self.structures[-1] else: mols = XYZ.from_file(trajectory_file).all_molecules @@ -609,8 +601,7 @@ def parse_dft_params(self): # Functional if self.input and self.input.check("FORCE_EVAL/DFT/XC/XC_FUNCTIONAL"): - xc_funcs = list(self.input["force_eval"]["dft"]["xc"]["xc_functional"].subsections) - if xc_funcs: + if xc_funcs := list(self.input["force_eval"]["dft"]["xc"]["xc_functional"].subsections): self.data["dft"]["functional"] = xc_funcs else: for v in self.input["force_eval"]["dft"]["xc"].subsections.values(): @@ -1251,10 +1242,7 @@ def parse_dos(self, dos_file=None, pdos_files=None, ldos_files=None): self.data["pdos"] = jsanitize(pdoss, strict=True) self.data["ldos"] = jsanitize(ldoss, strict=True) - if dos_file: - self.data["tdos"] = parse_dos(dos_file) - else: - self.data["tdos"] = tdos + self.data["tdos"] = parse_dos(dos_file) if dos_file else tdos if self.data.get("tdos"): self.band_gap = self.data["tdos"].get_gap() @@ -1295,7 +1283,7 @@ def parse_bandstructure(self, bandstructure_filename=None) -> None: else: return - with open(bandstructure_filename) as file: + with open(bandstructure_filename, encoding="utf-8") as file: lines = file.read().split("\n") data = np.loadtxt(bandstructure_filename) @@ -1317,7 +1305,7 @@ def parse_bandstructure(self, bandstructure_filename=None) -> None: nkpts += int(lines[0].split()[6]) elif line.split()[1] == "Point": kpts.append(list(map(float, line.split()[-4:-1]))) - elif line.split()[1] == "Special" in line: + elif line.split()[1] == "Special": splt = line.split() label = splt[7] if label.upper() == "GAMMA": diff --git a/pymatgen/io/cp2k/sets.py b/pymatgen/io/cp2k/sets.py index 0dee72c866b..949097db444 100644 --- a/pymatgen/io/cp2k/sets.py +++ b/pymatgen/io/cp2k/sets.py @@ -177,7 +177,7 @@ def __init__( super().__init__(name="CP2K_INPUT", subsections={}) self.structure = structure - self.basis_and_potential = basis_and_potential if basis_and_potential else {} + self.basis_and_potential = basis_and_potential or {} self.project_name = project_name self.charge = int(structure.charge) if not multiplicity and isinstance(self.structure, Molecule): @@ -200,7 +200,7 @@ def __init__( self.rel_cutoff = rel_cutoff self.ngrids = ngrids self.progression_factor = progression_factor - self.override_default_params = override_default_params if override_default_params else {} + self.override_default_params = override_default_params or {} self.wfn_restart_file_name = wfn_restart_file_name self.kpoints = kpoints self.smearing = smearing @@ -304,7 +304,7 @@ def __init__( MULTIPLICITY=self.multiplicity, CHARGE=self.charge, uks=self.kwargs.get("spin_polarized", True), - basis_set_filenames=self.basis_set_file_names if self.basis_set_file_names else [], + basis_set_filenames=self.basis_set_file_names or [], potential_filename=self.potential_file_name, subsections={"QS": qs, "SCF": scf, "MGRID": mgrid}, wfn_restart_file_name=wfn_restart_file_name, @@ -405,7 +405,7 @@ def get_basis_and_potential(structure, basis_and_potential): # Necessary if matching data to cp2k data files if have_element_file: - with open(os.path.join(SETTINGS.get("PMG_CP2K_DATA_DIR", "."), el)) as file: + with open(os.path.join(SETTINGS.get("PMG_CP2K_DATA_DIR", "."), el), encoding="utf-8") as file: yaml = YAML(typ="unsafe", pure=True) DATA = yaml.load(file) if not DATA.get("basis_sets"): @@ -567,7 +567,7 @@ def get_xc_functionals(xc_functionals: list | str | None = None) -> list: Get XC functionals. If simplified names are provided in kwargs, they will be expanded into their corresponding X and C names. """ - names = xc_functionals if xc_functionals else SETTINGS.get("PMG_DEFAULT_CP2K_FUNCTIONAL") + names = xc_functionals or SETTINGS.get("PMG_DEFAULT_CP2K_FUNCTIONAL") if not names: raise ValueError( "No XC functional provided. Specify kwarg xc_functional or configure PMG_DEFAULT_FUNCTIONAL " @@ -836,7 +836,7 @@ def activate_hybrid( pbe = PBE("ORIG", scale_c=1, scale_x=0) xc_functional = Xc_Functional(functionals=[], subsections={"PBE": pbe}) - potential_type = potential_type if potential_type else "SHORTRANGE" + potential_type = potential_type or "SHORTRANGE" xc_functional.insert( Section( "XWPBE", @@ -877,16 +877,15 @@ def activate_hybrid( ip_keywords["T_C_G_DATA"] = Keyword("T_C_G_DATA", "t_c_g.dat") ip_keywords["POTENTIAL_TYPE"] = Keyword("POTENTIAL_TYPE", potential_type) + elif hybrid_functional == "RSH": - """ - Activates range separated functional using mixing of the truncated - coulomb operator and the long range operator using scale_longrange, - scale_coulomb, cutoff_radius, and omega. - """ + # Activates range separated functional using mixing of the truncated + # coulomb operator and the long range operator using scale_longrange, + # scale_coulomb, cutoff_radius, and omega. pbe = PBE("ORIG", scale_c=1, scale_x=0) xc_functional = Xc_Functional(functionals=[], subsections={"PBE": pbe}) - potential_type = potential_type if potential_type else "MIX_CL_TRUNC" + potential_type = potential_type or "MIX_CL_TRUNC" hf_fraction = 1 ip_keywords.update( { diff --git a/pymatgen/io/cp2k/utils.py b/pymatgen/io/cp2k/utils.py index 2833d34829c..0c1ad20e157 100644 --- a/pymatgen/io/cp2k/utils.py +++ b/pymatgen/io/cp2k/utils.py @@ -29,11 +29,11 @@ def postprocessor(data: str) -> str | float | bool | None: """ data = data.strip().replace(" ", "_") # remove leading/trailing whitespace, replace spaces with _ - if data.lower() in ("false", "no", "f"): + if data.lower() in {"false", "no", "f"}: return False if data.lower() == "none": return None - if data.lower() in ("true", "yes", "t"): + if data.lower() in {"true", "yes", "t"}: return True if re.match(r"^-?\d+$", data): try: diff --git a/pymatgen/io/cssr.py b/pymatgen/io/cssr.py index 3ffdc04490f..af18ccaad9d 100644 --- a/pymatgen/io/cssr.py +++ b/pymatgen/io/cssr.py @@ -3,12 +3,18 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING from monty.io import zopen from pymatgen.core.lattice import Lattice from pymatgen.core.structure import Structure +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + __author__ = "Shyue Ping Ong" __copyright__ = "Copyright 2012, The Materials Project" __version__ = "0.1" @@ -56,7 +62,7 @@ def write_file(self, filename): file.write(str(self) + "\n") @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """ Reads a string representation to a Cssr object. @@ -72,17 +78,16 @@ def from_str(cls, string): tokens = lines[1].split() angles = [float(tok) for tok in tokens[0:3]] lattice = Lattice.from_parameters(*lengths, *angles) - sp = [] - coords = [] + sp, coords = [], [] for line in lines[4:]: - m = re.match(r"\d+\s+(\w+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)", line.strip()) - if m: - sp.append(m.group(1)) - coords.append([float(m.group(i)) for i in range(2, 5)]) + match = re.match(r"\d+\s+(\w+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)", line.strip()) + if match: + sp.append(match.group(1)) + coords.append([float(match.group(i)) for i in range(2, 5)]) return cls(Structure(lattice, sp, coords)) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Reads a CSSR file to a Cssr object. diff --git a/pymatgen/io/exciting/inputs.py b/pymatgen/io/exciting/inputs.py index 7544edf735f..3896f1ff6ed 100644 --- a/pymatgen/io/exciting/inputs.py +++ b/pymatgen/io/exciting/inputs.py @@ -3,6 +3,7 @@ from __future__ import annotations import xml.etree.ElementTree as ET +from typing import TYPE_CHECKING import numpy as np import scipy.constants as const @@ -13,6 +14,11 @@ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer from pymatgen.symmetry.bandstructure import HighSymmKpath +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + __author__ = "Christian Vorwerk" __copyright__ = "Copyright 2016" __version__ = "1.0" @@ -64,19 +70,27 @@ def lockxyz(self, lockxyz): self.structure.add_site_property("selective_dynamics", lockxyz) @classmethod - def from_str(cls, data): + def from_str(cls, data: str) -> Self: """Reads the exciting input from a string.""" - root = ET.fromstring(data) - species_node = root.find("structure").iter("species") + root: ET.Element = ET.fromstring(data) + struct = root.find("structure") + if struct is None: + raise ValueError("No structure found in input file!") + + species_node = struct.iter("species") elements = [] positions = [] vectors = [] lockxyz = [] # get title - title_in = str(root.find("title").text) + _title = root.find("title") + assert _title is not None, "title cannot be None." + title_in = str(_title.text) # Read elements and coordinates for nodes in species_node: - symbol = nodes.get("speciesfile").split(".")[0] + _speciesfile = nodes.get("speciesfile") + assert _speciesfile is not None, "speciesfile cannot be None." + symbol = _speciesfile.split(".")[0] if len(symbol.split("_")) == 2: symbol = symbol.split("_")[0] if Element.is_valid_symbol(symbol): @@ -84,40 +98,53 @@ def from_str(cls, data): element = symbol else: raise ValueError("Unknown element!") + for atom in nodes.iter("atom"): - x, y, z = atom.get("coord").split() + _coord = atom.get("coord") + assert _coord is not None, "coordinate cannot be None." + x, y, z = _coord.split() positions.append([float(x), float(y), float(z)]) elements.append(element) # Obtain lockxyz for each atom - if atom.get("lockxyz") is not None: + if atom.get("lockxyz") is None: + lockxyz.append([False, False, False]) + else: lxyz = [] - for line in atom.get("lockxyz").split(): + + _lockxyz = atom.get("lockxyz") + assert _lockxyz is not None, "lockxyz cannot be None." + for line in _lockxyz.split(): if line in ("True", "true"): lxyz.append(True) else: lxyz.append(False) lockxyz.append(lxyz) - else: - lockxyz.append([False, False, False]) + # check the atomic positions type - if "cartesian" in root.find("structure").attrib: - if root.find("structure").attrib["cartesian"]: + if "cartesian" in struct.attrib: + if struct.attrib["cartesian"]: cartesian = True for p in positions: for j in range(3): p[j] = p[j] * ExcitingInput.bohr2ang - print(positions) else: cartesian = False + + _crystal = struct.find("crystal") + assert _crystal is not None, "crystal cannot be None." + # get the scale attribute - scale_in = root.find("structure").find("crystal").get("scale") + scale_in = _crystal.get("scale") scale = float(scale_in) * ExcitingInput.bohr2ang if scale_in else ExcitingInput.bohr2ang + # get the stretch attribute - stretch_in = root.find("structure").find("crystal").get("stretch") + stretch_in = _crystal.get("stretch") stretch = np.array([float(a) for a in stretch_in]) if stretch_in else np.array([1.0, 1.0, 1.0]) + # get basis vectors and scale them accordingly - basisnode = root.find("structure").find("crystal").iter("basevect") + basisnode = _crystal.iter("basevect") for vect in basisnode: + assert vect.text is not None, "vectors cannot be None." x, y, z = vect.text.split() vectors.append( [ @@ -133,7 +160,7 @@ def from_str(cls, data): return cls(structure_in, title_in, lockxyz) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Args: filename: Filename @@ -333,7 +360,7 @@ def _indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): - elem.text = i + " " + elem.text = f"{i} " if not elem.tail or not elem.tail.strip(): elem.tail = i for el in elem: diff --git a/pymatgen/io/feff/inputs.py b/pymatgen/io/feff/inputs.py index 14f56bd58f2..ed66f133663 100644 --- a/pymatgen/io/feff/inputs.py +++ b/pymatgen/io/feff/inputs.py @@ -231,13 +231,13 @@ def formula(self): return self.struct.composition.formula @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str) -> Self: """Returns Header object from file.""" hs = cls.header_string_from_file(filename) return cls.from_str(hs) @staticmethod - def header_string_from_file(filename="feff.inp"): + def header_string_from_file(filename: str = "feff.inp"): """ Reads Header string from either a HEADER file or feff.inp file Will also read a header from a non-pymatgen generated feff.inp file. @@ -281,7 +281,7 @@ def header_string_from_file(filename="feff.inp"): return "".join(feff_header_str) @classmethod - def from_str(cls, header_str): + def from_str(cls, header_str: str) -> Self: """ Reads Header string and returns Header object if header was generated by pymatgen. @@ -309,15 +309,13 @@ def from_str(cls, header_str): a = float(basis_vec[0]) b = float(basis_vec[1]) c = float(basis_vec[2]) - lengths = [a, b, c] # alpha, beta, gamma basis_ang = lines[7].split(":")[-1].split() alpha = float(basis_ang[0]) beta = float(basis_ang[1]) gamma = float(basis_ang[2]) - angles = [alpha, beta, gamma] - lattice = Lattice.from_parameters(*lengths, *angles) + lattice = Lattice.from_parameters(a, b, c, alpha, beta, gamma) n_atoms = int(lines[8].split(":")[-1].split()[0]) @@ -644,7 +642,7 @@ def write_file(self, filename="PARAMETERS"): file.write(f"{self}\n") @classmethod - def from_file(cls, filename="feff.inp"): + def from_file(cls, filename: str = "feff.inp") -> Self: """ Creates a Feff_tag dictionary from a PARAMETER or feff.inp file. diff --git a/pymatgen/io/feff/outputs.py b/pymatgen/io/feff/outputs.py index 7895f6c51fd..a5f2763d35d 100644 --- a/pymatgen/io/feff/outputs.py +++ b/pymatgen/io/feff/outputs.py @@ -8,6 +8,7 @@ import re from collections import defaultdict +from typing import TYPE_CHECKING import numpy as np from monty.io import zopen @@ -18,6 +19,9 @@ from pymatgen.electronic_structure.dos import CompleteDos, Dos from pymatgen.io.feff import Header, Potential, Tags +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Alan Dozier, Kiran Mathew, Chen Zheng" __credits__ = "Anubhav Jain, Shyue Ping Ong" __copyright__ = "Copyright 2011, The Materials Project" @@ -42,7 +46,7 @@ def __init__(self, complete_dos, charge_transfer): self.charge_transfer = charge_transfer @classmethod - def from_file(cls, feff_inp_file="feff.inp", ldos_file="ldos"): + def from_file(cls, feff_inp_file: str = "feff.inp", ldos_file: str = "ldos") -> Self: """ Creates LDos object from raw Feff ldos files by by assuming they are numbered consecutively, i.e. ldos01.dat @@ -107,7 +111,7 @@ def from_file(cls, feff_inp_file="feff.inp", ldos_file="ldos"): for idx in range(len(ldos[1])): dos_energies.append(ldos[1][idx][0]) - all_pdos = [] + all_pdos: list[dict] = [] vorb = {"s": Orbital.s, "p": Orbital.py, "d": Orbital.dxy, "f": Orbital.f0} forb = {"s": 0, "p": 1, "d": 2, "f": 3} @@ -134,13 +138,13 @@ def from_file(cls, feff_inp_file="feff.inp", ldos_file="ldos"): t_dos = [0] * d_length for idx in range(n_sites): pot_index = pot_dict[structure.species[idx].symbol] - for v in forb.values(): - density = [ldos[pot_index][j][v + 1] for j in range(d_length)] + for forb_val in forb.values(): + density = [ldos[pot_index][j][forb_val + 1] for j in range(d_length)] for j in range(d_length): t_dos[j] = t_dos[j] + density[j] - t_dos = {Spin.up: t_dos} + _t_dos: dict = {Spin.up: t_dos} - dos = Dos(efermi, dos_energies, t_dos) + 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 cls(complete_dos, charge_transfer) @@ -287,7 +291,7 @@ def __init__(self, header, parameters, absorbing_atom, data): self.data = np.array(data) @classmethod - def from_file(cls, xmu_dat_file="xmu.dat", feff_inp_file="feff.inp"): + def from_file(cls, xmu_dat_file: str = "xmu.dat", feff_inp_file: str = "feff.inp") -> Self: """ Get Xmu from file. @@ -412,7 +416,7 @@ def fine_structure(self): return self.data[:, 3] @classmethod - def from_file(cls, eels_dat_file="eels.dat"): + def from_file(cls, eels_dat_file: str = "eels.dat") -> Self: """ Parse eels spectrum. @@ -425,7 +429,7 @@ def from_file(cls, eels_dat_file="eels.dat"): data = np.loadtxt(eels_dat_file) return cls(data) - def as_dict(self): + def as_dict(self) -> dict: """Returns dict representations of Xmu object.""" dct = MSONable.as_dict(self) dct["data"] = self.data.tolist() diff --git a/pymatgen/io/feff/sets.py b/pymatgen/io/feff/sets.py index 084ce17e0a4..3870560050d 100644 --- a/pymatgen/io/feff/sets.py +++ b/pymatgen/io/feff/sets.py @@ -14,6 +14,7 @@ import sys import warnings from copy import deepcopy +from typing import TYPE_CHECKING import numpy as np from monty.json import MSONable @@ -23,6 +24,9 @@ from pymatgen.core.structure import Molecule, Structure from pymatgen.io.feff.inputs import Atoms, Header, Potential, Tags +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Kiran Mathew" __credits__ = "Alan Dozier, Anubhav Jain, Shyue Ping Ong" __version__ = "1.1" @@ -282,15 +286,15 @@ def __str__(self): return "\n".join(output) @classmethod - def from_directory(cls, input_dir): + def from_directory(cls, input_dir: str) -> Self: """ Read in a set of FEFF input files from a directory, which is useful when existing FEFF input needs some adjustment. """ - sub_d = {} - for fname, ftype in [("HEADER", Header), ("PARAMETERS", Tags)]: - full_zpath = zpath(os.path.join(input_dir, fname)) - sub_d[fname.lower()] = ftype.from_file(full_zpath) + sub_d: dict = { + "header": Header.from_file(zpath(os.path.join(input_dir, "HEADER"))), + "parameters": Tags.from_file(zpath(os.path.join(input_dir, "PARAMETERS"))), + } # Generation of FEFFDict set requires absorbing atom, need to search # the index of absorption atom in the structure according to the diff --git a/pymatgen/io/gaussian.py b/pymatgen/io/gaussian.py index be194aacb4d..54c760ffd07 100644 --- a/pymatgen/io/gaussian.py +++ b/pymatgen/io/gaussian.py @@ -4,6 +4,7 @@ import re import warnings +from typing import TYPE_CHECKING import numpy as np import scipy.constants as cst @@ -17,6 +18,11 @@ from pymatgen.util.coord import get_angle from pymatgen.util.plotting import pretty_plot +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + __author__ = "Shyue Ping Ong, Germain Salvato-Vallverdu, Xin Chen" __copyright__ = "Copyright 2013, The Materials Virtual Lab" __version__ = "0.1" @@ -55,8 +61,7 @@ def read_route_line(route): route = route.replace(tok, "") for tok in route.split(): - if scrf_patt.match(tok): - m = scrf_patt.match(tok) + if m := scrf_patt.match(tok): route_params[m.group(1)] = m.group(2) elif tok.upper() in ["#", "#N", "#P", "#T"]: # does not store # in route to avoid error in input @@ -158,7 +163,7 @@ def __init__( self.link0_parameters = link0_parameters or {} self.route_parameters = route_parameters or {} self.input_parameters = input_parameters or {} - self.dieze_tag = dieze_tag if dieze_tag[0] == "#" else "#" + dieze_tag + self.dieze_tag = dieze_tag if dieze_tag[0] == "#" else f"#{dieze_tag}" self.gen_basis = gen_basis if gen_basis is not None: self.basis_set = "Gen" @@ -275,7 +280,7 @@ def _parse_species(sp_str): return Molecule(species, coords) @classmethod - def from_str(cls, contents): + def from_str(cls, contents: str) -> Self: """ Creates GaussianInput from a string. @@ -292,6 +297,7 @@ def from_str(cls, contents): for line in lines: if link0_patt.match(line): m = link0_patt.match(line) + assert m is not None link0_dict[m.group(1).strip("=")] = m.group(2) route_patt = re.compile(r"^#[sSpPnN]*.*") @@ -299,7 +305,7 @@ def from_str(cls, contents): route_index = None for idx, line in enumerate(lines): if route_patt.match(line): - route += " " + line + route += f" {line}" route_index = idx # This condition allows for route cards spanning multiple lines elif (line == "" or line.isspace()) and route_index: @@ -310,10 +316,11 @@ def from_str(cls, contents): functional, basis_set, route_paras, dieze_tag = read_route_line(route) ind = 2 title = [] + assert route_index is not None, "route_index cannot be None" while lines[route_index + ind].strip(): title.append(lines[route_index + ind].strip()) ind += 1 - title = " ".join(title) + title_str = " ".join(title) ind += 1 tokens = re.split(r"[,\s]+", lines[route_index + ind]) charge = int(float(tokens[0])) @@ -340,7 +347,7 @@ def from_str(cls, contents): mol, charge=charge, spin_multiplicity=spin_mult, - title=title, + title=title_str, functional=functional, basis_set=basis_set, route_parameters=route_paras, @@ -350,7 +357,7 @@ def from_str(cls, contents): ) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Creates GaussianInput from a file. @@ -458,7 +465,7 @@ def as_dict(self): } @classmethod - def from_dict(cls, dct: dict) -> GaussianInput: + def from_dict(cls, dct: dict) -> Self: """ Args: dct: dict @@ -721,8 +728,7 @@ def _parse(self, filename): std_structures.append(Molecule(sp, coords)) if parse_forces: - m = forces_patt.search(line) - if m: + if m := forces_patt.search(line): forces.extend([float(_v) for _v in m.groups()[2:5]]) elif forces_off_patt.search(line): self.cart_forces.append(forces) @@ -731,8 +737,7 @@ def _parse(self, filename): # read molecular orbital eigenvalues if read_eigen: - m = orbital_patt.search(line) - if m: + if m := orbital_patt.search(line): eigen_txt.append(line) else: read_eigen = False @@ -979,12 +984,12 @@ def _parse(self, filename): # store the structures. If symmetry is considered, the standard orientation # is used. Else the input orientation is used. + self.structures_input_orientation = input_structures if standard_orientation: self.structures = std_structures - self.structures_input_orientation = input_structures else: self.structures = input_structures - self.structures_input_orientation = input_structures + # store optimized structure in input orientation self.opt_structures = opt_structures @@ -1200,7 +1205,7 @@ def read_excitation_energies(self): if td and re.search(r"^\sExcited State\s*\d", line): val = [float(v) for v in float_patt.findall(line)] - transitions.append(tuple(val[0:3])) + transitions.append(tuple(val[:3])) line = file.readline() return transitions diff --git a/pymatgen/io/lammps/data.py b/pymatgen/io/lammps/data.py index 0c0594f4570..a35743051e2 100644 --- a/pymatgen/io/lammps/data.py +++ b/pymatgen/io/lammps/data.py @@ -37,6 +37,8 @@ from collections.abc import Sequence from typing import Any + from typing_extensions import Self + from pymatgen.core.sites import Site from pymatgen.core.structure import SiteCollection @@ -629,7 +631,7 @@ def label_topo(t) -> tuple: return self.box, ff, topo_list @classmethod - def from_file(cls, filename: str, atom_style: str = "full", sort_id: bool = False) -> LammpsData: + def from_file(cls, filename: str, atom_style: str = "full", sort_id: bool = False) -> Self: """ Constructor that parses a file. @@ -743,7 +745,7 @@ def parse_section(sec_lines) -> tuple[str, pd.DataFrame]: @classmethod def from_ff_and_topologies( cls, box: LammpsBox, ff: ForceField, topologies: Sequence[Topology], atom_style: str = "full" - ): + ) -> Self: """ Constructor building LammpsData from a ForceField object and a list of Topology objects. Do not support intermolecular @@ -818,7 +820,7 @@ def from_structure( ff_elements: Sequence[str] | None = None, atom_style: Literal["atomic", "charge"] = "charge", is_sort: bool = False, - ): + ) -> Self: """ Simple constructor building LammpsData from a structure without force field parameters and topologies. @@ -969,7 +971,7 @@ def from_bonding( dihedral: bool = True, tol: float = 0.1, **kwargs, - ): + ) -> Self: """ Another constructor that creates an instance from a molecule. Covalent bonds and other bond-based topologies (angles and @@ -1194,20 +1196,20 @@ def to_file(self, filename: str) -> None: yaml.dump(dct, file) @classmethod - def from_file(cls, filename: str) -> ForceField: + def from_file(cls, filename: str) -> Self: """ Constructor that reads in a file in YAML format. Args: filename (str): Filename. """ - with open(filename) as file: + with open(filename, encoding="utf-8") as file: yaml = YAML() d = yaml.load(file) return cls.from_dict(d) @classmethod - def from_dict(cls, dct: dict) -> ForceField: + def from_dict(cls, dct: dict) -> Self: """ Constructor that reads in a dictionary. @@ -1373,13 +1375,14 @@ def disassemble( for mol in self.mols ] + # NOTE (@janosh): The following two methods for override parent class LammpsData @classmethod - def from_ff_and_topologies(cls): + def from_ff_and_topologies(cls) -> None: # type: ignore[override] """Unsupported constructor for CombinedData objects.""" raise AttributeError("Unsupported constructor for CombinedData objects") @classmethod - def from_structure(cls): + def from_structure(cls) -> None: # type: ignore[override] """Unsupported constructor for CombinedData objects.""" raise AttributeError("Unsupported constructor for CombinedData objects") @@ -1406,7 +1409,7 @@ def parse_xyz(cls, filename: str | Path) -> pd.DataFrame: return df @classmethod - def from_files(cls, coordinate_file: str, list_of_numbers: list, *filenames) -> CombinedData: + def from_files(cls, coordinate_file: str, list_of_numbers: list, *filenames) -> Self: """ Constructor that parse a series of data file. @@ -1432,7 +1435,7 @@ def from_files(cls, coordinate_file: str, list_of_numbers: list, *filenames) -> @classmethod def from_lammpsdata( cls, mols: list, names: list, list_of_numbers: list, coordinates: pd.DataFrame, atom_style: str | None = None - ) -> CombinedData: + ) -> Self: """ Constructor that can infer atom_style. The input LammpsData objects are used non-destructively. diff --git a/pymatgen/io/lammps/generators.py b/pymatgen/io/lammps/generators.py index 0559b41a5dd..baa74a24963 100644 --- a/pymatgen/io/lammps/generators.py +++ b/pymatgen/io/lammps/generators.py @@ -42,7 +42,7 @@ class BaseLammpsGenerator(InputGenerator): The parameters are then replaced based on the values found in the settings dictionary that you provide, e.g., `{"nsteps": 1000}`. - Parameters: + Attributes: template: Path (string) to the template file used to create the InputFile for LAMMPS. calc_type: Human-readable string used to briefly describe the type of computations performed by LAMMPS. settings: Dictionary containing the values of the parameters to replace in the template. @@ -57,17 +57,16 @@ class BaseLammpsGenerator(InputGenerator): (https://github.com/Matgenix/atomate2-lammps). """ + inputfile: LammpsInputFile | None = field(default=None) template: str = field(default_factory=str) + data: LammpsData | CombinedData | None = field(default=None) settings: dict = field(default_factory=dict) calc_type: str = field(default="lammps") keep_stages: bool = field(default=True) - def __post_init__(self): - self.settings = self.settings or {} - - def get_input_set(self, structure: Structure | LammpsData | CombinedData | None) -> LammpsInputSet: # type: ignore + def get_input_set(self, structure: Structure | LammpsData | CombinedData) -> LammpsInputSet: """Generate a LammpsInputSet from the structure/data, tailored to the template file.""" - data = LammpsData.from_structure(structure) if isinstance(structure, Structure) else structure + data: LammpsData = LammpsData.from_structure(structure) if isinstance(structure, Structure) else structure # Load the template with zopen(self.template, mode="r") as file: @@ -83,7 +82,7 @@ def get_input_set(self, structure: Structure | LammpsData | CombinedData | None) class LammpsMinimization(BaseLammpsGenerator): - r""" + """ Generator that yields a LammpsInputSet tailored for minimizing the energy of a system by iteratively adjusting atom coordinates. Example usage: @@ -94,7 +93,7 @@ class LammpsMinimization(BaseLammpsGenerator): Do not forget to specify the force field, otherwise LAMMPS will not be able to run! - /!\ This InputSet and InputGenerator implementation is based on templates and is not intended to be very flexible. + This InputSet and InputGenerator implementation is based on templates and is not intended to be very flexible. For instance, pymatgen will not detect whether a given variable should be adapted based on others (e.g., the number of steps from the temperature), it will not check for convergence nor will it actually run LAMMPS. For additional flexibility and automation, use the atomate2-lammps implementation @@ -112,21 +111,21 @@ def __init__( force_field: str = "Unspecified force field!", keep_stages: bool = False, ) -> None: - r""" + """ Args: template: Path (string) to the template file used to create the InputFile for LAMMPS. - units: units to be used for the LAMMPS calculation (see official LAMMPS documentation). - atom_style: atom_style to be used for the LAMMPS calculation (see official LAMMPS documentation). - dimension: dimension to be used for the LAMMPS calculation (see official LAMMPS documentation). - boundary: boundary to be used for the LAMMPS calculation (see official LAMMPS documentation). - read_data: read_data to be used for the LAMMPS calculation (see official LAMMPS documentation). - force_field: force field to be used for the LAMMPS calculation (see official LAMMPS documentation). - Note that you should provide all the required information as a single string. - In case of multiple lines expected in the input file, - separate them with '\n' in force_field. + units: units to be used for the LAMMPS calculation (see LAMMPS docs). + atom_style: atom_style to be used for the LAMMPS calculation (see LAMMPS docs). + dimension: dimension to be used for the LAMMPS calculation (see LAMMPS docs). + boundary: boundary to be used for the LAMMPS calculation (see LAMMPS docs). + read_data: read_data to be used for the LAMMPS calculation (see LAMMPS docs). + force_field: force field to be used for the LAMMPS calculation (see LAMMPS docs). + Note that you should provide all the required information as a single string. + In case of multiple lines expected in the input file, + separate them with '\n' in force_field. keep_stages: If True, the string is formatted in a block structure with stage names - and newlines that differentiate commands in the respective stages of the InputFile. - If False, stage names are not printed and all commands appear in a single block. + and newlines that differentiate commands in the respective stages of the InputFile. + If False, stage names are not printed and all commands appear in a single block. """ if template is None: template = f"{template_dir}/minimization.template" diff --git a/pymatgen/io/lammps/inputs.py b/pymatgen/io/lammps/inputs.py index b7031b726a0..f9aed902006 100644 --- a/pymatgen/io/lammps/inputs.py +++ b/pymatgen/io/lammps/inputs.py @@ -28,6 +28,8 @@ if TYPE_CHECKING: from os import PathLike + from typing_extensions import Self + from pymatgen.io.core import InputSet __author__ = "Kiran Mathew, Brandon Wood, Zhi Deng, Manas Likhit, Guillaume Brunin (Matgenix)" @@ -536,7 +538,7 @@ def write_file(self, filename: str | PathLike, ignore_comments: bool = False, ke file.write(self.get_str(ignore_comments=ignore_comments, keep_stages=keep_stages)) @classmethod - def from_str(cls, contents: str, ignore_comments: bool = False, keep_stages: bool = False) -> LammpsInputFile: + def from_str(cls, contents: str, ignore_comments: bool = False, keep_stages: bool = False) -> Self: # type: ignore[override] """ Helper method to parse string representation of LammpsInputFile. If you created the input file by hand, there is no guarantee that the representation @@ -616,7 +618,7 @@ def from_str(cls, contents: str, ignore_comments: bool = False, keep_stages: boo return LIF @classmethod - def from_file(cls, path: str | Path, ignore_comments: bool = False, keep_stages: bool = False) -> LammpsInputFile: + def from_file(cls, path: str | Path, ignore_comments: bool = False, keep_stages: bool = False) -> Self: # type: ignore[override] """ Creates an InputFile object from a file. diff --git a/pymatgen/io/lammps/outputs.py b/pymatgen/io/lammps/outputs.py index ac39f46603d..15b2ad80602 100644 --- a/pymatgen/io/lammps/outputs.py +++ b/pymatgen/io/lammps/outputs.py @@ -20,6 +20,8 @@ if TYPE_CHECKING: from typing import Any + from typing_extensions import Self + __author__ = "Kiran Mathew, Zhi Deng" __copyright__ = "Copyright 2018, The Materials Virtual Lab" __version__ = "1.0" @@ -47,7 +49,7 @@ def __init__(self, timestep: int, natoms: int, box: LammpsBox, data: pd.DataFram self.data = data @classmethod - def from_str(cls, string: str) -> LammpsDump: + def from_str(cls, string: str) -> Self: """ Constructor from string parsing. @@ -71,7 +73,7 @@ def from_str(cls, string: str) -> LammpsDump: return cls(time_step, n_atoms, box, data) @classmethod - def from_dict(cls, dct: dict) -> LammpsDump: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. diff --git a/pymatgen/io/lammps/sets.py b/pymatgen/io/lammps/sets.py index d303b5ad30a..cb1cdb08e89 100644 --- a/pymatgen/io/lammps/sets.py +++ b/pymatgen/io/lammps/sets.py @@ -20,6 +20,8 @@ if TYPE_CHECKING: from pathlib import Path + from typing_extensions import Self + __author__ = "Ryan Kingsbury, Guillaume Brunin (Matgenix)" __copyright__ = "Copyright 2021, The Materials Project" __version__ = "0.2" @@ -73,7 +75,7 @@ def __init__( super().__init__(inputs={"in.lammps": self.inputfile, "system.data": self.data}) @classmethod - def from_directory(cls, directory: str | Path, keep_stages: bool = False) -> LammpsInputSet: + def from_directory(cls, directory: str | Path, keep_stages: bool = False) -> Self: # type: ignore[override] """ Construct a LammpsInputSet from a directory of two or more files. TODO: accept directories with only the input file, that should include the structure as well. @@ -88,7 +90,7 @@ def from_directory(cls, directory: str | Path, keep_stages: bool = False) -> Lam if isinstance(atom_style, list): raise ValueError("Variable atom_style is specified multiple times in the input file.") data_file = LammpsData.from_file(f"{directory}/system.data", atom_style=atom_style) - return LammpsInputSet(inputfile=input_file, data=data_file, calc_type="read_from_dir") + return cls(inputfile=input_file, data=data_file, calc_type="read_from_dir") def validate(self) -> bool: """ diff --git a/pymatgen/io/lmto.py b/pymatgen/io/lmto.py index 5844f5db2aa..0ceead79d5d 100644 --- a/pymatgen/io/lmto.py +++ b/pymatgen/io/lmto.py @@ -19,6 +19,8 @@ from pymatgen.util.num import round_to_sigfigs if TYPE_CHECKING: + from pathlib import Path + from typing_extensions import Self __author__ = "Marco Esters" @@ -139,7 +141,7 @@ def write_file(self, filename="CTRL", **kwargs): file.write(self.get_str(**kwargs)) @classmethod - def from_file(cls, filename="CTRL", **kwargs): + def from_file(cls, filename: str | Path = "CTRL", **kwargs) -> Self: """ Creates a CTRL file object from an existing file. @@ -151,11 +153,11 @@ def from_file(cls, filename="CTRL", **kwargs): """ with zopen(filename, mode="rt") as file: contents = file.read() - return LMTOCtrl.from_str(contents, **kwargs) + return cls.from_str(contents, **kwargs) @classmethod @no_type_check - def from_str(cls, data: str, sigfigs: int = 8) -> LMTOCtrl: + def from_str(cls, data: str, sigfigs: int = 8) -> Self: """ Creates a CTRL file object from a string. This will mostly be used to read an LMTOCtrl object from a CTRL file. Empty spheres diff --git a/pymatgen/io/lobster/inputs.py b/pymatgen/io/lobster/inputs.py index 476eaf23b31..495a28521d6 100644 --- a/pymatgen/io/lobster/inputs.py +++ b/pymatgen/io/lobster/inputs.py @@ -307,7 +307,7 @@ def from_dict(cls, dct: dict) -> Self: Returns: Lobsterin """ - return Lobsterin({k: v for k, v in dct.items() if k not in ["@module", "@class"]}) + return cls({k: v for k, v in dct.items() if k not in ["@module", "@class"]}) def write_INCAR( self, @@ -566,7 +566,7 @@ def write_KPOINTS( KpointObject.write_file(filename=KPOINTS_output) @classmethod - def from_file(cls, lobsterin: str): + def from_file(cls, lobsterin: str) -> Self: """ Args: lobsterin (str): path to lobsterin. diff --git a/pymatgen/io/lobster/lobsterenv.py b/pymatgen/io/lobster/lobsterenv.py index bb918fea043..1c2c124162f 100644 --- a/pymatgen/io/lobster/lobsterenv.py +++ b/pymatgen/io/lobster/lobsterenv.py @@ -32,6 +32,8 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core import Structure __author__ = "Janine George" @@ -572,7 +574,7 @@ def get_info_cohps_to_neighbors( if present: new_labels.append(key) new_atoms.append(atompair) - if len(new_labels) > 0: + if new_labels: divisor = len(new_labels) if per_bond else 1 plot_label = self._get_plot_label(new_atoms, per_bond) @@ -594,7 +596,7 @@ def _get_plot_label(self, atoms, per_bond): for atoms_names in atoms: new = [self._split_string(atoms_names[0])[0], self._split_string(atoms_names[1])[0]] new.sort() - string_here = new[0] + "-" + new[1] + string_here = f"{new[0]}-{new[1]}" all_labels.append(string_here) count = collections.Counter(all_labels) plotlabels = [] @@ -613,7 +615,7 @@ def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True) isites: list of site ids, if isite==None, all isites will be used onlycation_isites: will only use cations, if isite==None - Returns + Returns: ICOHPNeighborsInfo """ lowerlimit = self.lowerlimit @@ -732,7 +734,7 @@ def _evaluate_ce( additional_condition=additional_condition, ) - elif lowerlimit is None and (upperlimit is not None or lowerlimit is not None): + elif upperlimit is None or lowerlimit is None: raise ValueError("Please give two limits or leave them both at None") # find environments based on ICOHP values @@ -1267,7 +1269,7 @@ def from_Lobster( list_neighisite, structure: Structure, valences=None, - ): + ) -> Self: """ Will set up a LightStructureEnvironments from Lobster. diff --git a/pymatgen/io/lobster/outputs.py b/pymatgen/io/lobster/outputs.py index f670c5206e4..6f6c277af0a 100644 --- a/pymatgen/io/lobster/outputs.py +++ b/pymatgen/io/lobster/outputs.py @@ -116,7 +116,7 @@ def __init__( # contains all parameters that are needed to map the file. parameters = contents[1].split() # Subtract 1 to skip the average - num_bonds = int(parameters[0]) - 1 if not self.are_multi_center_cobis else int(parameters[0]) + num_bonds = int(parameters[0]) if self.are_multi_center_cobis else int(parameters[0]) - 1 self.efermi = float(parameters[-1]) self.is_spin_polarized = int(parameters[1]) == 2 spins = [Spin.up, Spin.down] if int(parameters[1]) == 2 else [Spin.up] @@ -1567,7 +1567,7 @@ def has_good_quality_check_occupied_bands( elif band2.all() > limit_deviation: return False else: - ValueError("number_occ_bands_spin_down has to be specified") + raise ValueError("number_occ_bands_spin_down has to be specified") return True @property diff --git a/pymatgen/io/nwchem.py b/pymatgen/io/nwchem.py index edc3d22b574..efd04e87a92 100644 --- a/pymatgen/io/nwchem.py +++ b/pymatgen/io/nwchem.py @@ -35,6 +35,8 @@ from pymatgen.core.units import Energy, FloatWithUnit if TYPE_CHECKING: + from pathlib import Path + from typing_extensions import Self NWCHEM_BASIS_LIBRARY = None @@ -209,7 +211,7 @@ def from_dict(cls, dct: dict) -> Self: Returns: NwTask """ - return NwTask( + return cls( charge=dct["charge"], spin_multiplicity=dct["spin_multiplicity"], title=dct["title"], @@ -234,7 +236,7 @@ def from_molecule( operation="optimize", theory_directives=None, alternate_directives=None, - ): + ) -> Self: """ Very flexible arguments to support many types of potential setups. Users should use more friendly static methods unless they need the @@ -282,7 +284,7 @@ def from_molecule( if isinstance(basis_set, str): basis_set = dict.fromkeys(elements, basis_set) - return NwTask( + return cls( charge, spin_multiplicity, basis_set, @@ -411,7 +413,7 @@ def from_dict(cls, dct: dict) -> Self: Returns: NwInput """ - return NwInput( + return cls( Molecule.from_dict(dct["mol"]), tasks=[NwTask.from_dict(dt) for dt in dct["tasks"]], directives=[tuple(li) for li in dct["directives"]], @@ -421,7 +423,7 @@ def from_dict(cls, dct: dict) -> Self: ) @classmethod - def from_str(cls, string_input): + def from_str(cls, string_input: str) -> Self: """ Read an NwInput from a string. Currently tested to work with files generated from this class itself. @@ -436,7 +438,7 @@ def from_str(cls, string_input): tasks = [] charge = spin_multiplicity = title = basis_set = None basis_set_option = None - theory_directives = {} + theory_directives: dict[str, dict[str, str]] = {} geom_options = symmetry_options = memory_options = None lines = string_input.strip().split("\n") while len(lines) > 0: @@ -505,7 +507,7 @@ def from_str(cls, string_input): else: directives.append(line.strip().split()) - return NwInput( + return cls( mol, tasks=tasks, directives=directives, @@ -515,7 +517,7 @@ def from_str(cls, string_input): ) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Read an NwInput from a file. Currently tested to work with files generated from this class itself. diff --git a/pymatgen/io/packmol.py b/pymatgen/io/packmol.py index a7506a2eacd..7c535346207 100644 --- a/pymatgen/io/packmol.py +++ b/pymatgen/io/packmol.py @@ -88,7 +88,7 @@ def run(self, path: str | Path, timeout=30): os.chdir(wd) @classmethod - def from_directory(cls, directory: str | Path): + def from_directory(cls, directory: str | Path) -> None: """ Construct an InputSet from a directory of one or more files. @@ -184,6 +184,9 @@ def get_input_set( # type: ignore net_volume = 0.0 for d in molecules: mol = Molecule.from_file(d["coords"]) if not isinstance(d["coords"], Molecule) else d["coords"] + + if mol is None: + raise ValueError("Molecule cannot be None.") # pad the calculated length by an amount related to the tolerance parameter # the amount to add was determined arbitrarily length = ( @@ -202,6 +205,10 @@ def get_input_set( # type: ignore mol = Molecule.from_file(str(d["coords"])) elif isinstance(d["coords"], Molecule): mol = d["coords"] + + if mol is None: + raise ValueError("Molecule cannot be None.") + fname = f"packmol_{d['name']}.xyz" mapping.update({fname: mol.to(fmt="xyz")}) if " " in str(fname): diff --git a/pymatgen/io/pwmat/inputs.py b/pymatgen/io/pwmat/inputs.py index 020e7e1a9b8..8598a3f5a2d 100644 --- a/pymatgen/io/pwmat/inputs.py +++ b/pymatgen/io/pwmat/inputs.py @@ -377,7 +377,7 @@ def __str__(self): return self.get_str() @classmethod - def from_str(cls, data: str, mag: bool = False) -> AtomConfig: + def from_str(cls, data: str, mag: bool = False) -> Self: """Reads a atom.config from a string. Args: @@ -404,7 +404,7 @@ def from_str(cls, data: str, mag: bool = False) -> AtomConfig: return cls(structure) @classmethod - def from_file(cls, filename: PathLike, mag: bool = False) -> AtomConfig: + def from_file(cls, filename: PathLike, mag: bool = False) -> Self: """Returns a AtomConfig from a file Args: @@ -418,7 +418,7 @@ def from_file(cls, filename: PathLike, mag: bool = False) -> AtomConfig: return cls.from_str(data=file.read(), mag=mag) @classmethod - def from_dict(cls, dct: dict) -> AtomConfig: + def from_dict(cls, dct: dict) -> Self: """Returns a AtomConfig object from a dictionary. Args: diff --git a/pymatgen/io/pwscf.py b/pymatgen/io/pwscf.py index 8bf4abf94cf..124614f2a67 100644 --- a/pymatgen/io/pwscf.py +++ b/pymatgen/io/pwscf.py @@ -13,6 +13,8 @@ from pymatgen.util.io_utils import clean_lines if TYPE_CHECKING: + from pathlib import Path + from typing_extensions import Self @@ -222,12 +224,12 @@ def write_file(self, filename): file.write(str(self)) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Reads an PWInput object from a file. Args: - filename (str): Filename for file + filename (str | Path): Filename for file Returns: PWInput object @@ -236,7 +238,7 @@ def from_file(cls, filename): return cls.from_str(file.read()) @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """ Reads an PWInput object from a string. @@ -263,7 +265,7 @@ def input_mode(line): return None return mode - sections = { + sections: dict[str, dict] = { "control": {}, "system": {}, "electrons": {}, @@ -275,7 +277,7 @@ def input_mode(line): species = [] coords = [] structure = None - site_properties = {"pseudo": []} + site_properties: dict[str, list] = {"pseudo": []} mode = None for line in lines: mode = input_mode(line) diff --git a/pymatgen/io/qchem/inputs.py b/pymatgen/io/qchem/inputs.py index 79fc744348a..a599701e69e 100644 --- a/pymatgen/io/qchem/inputs.py +++ b/pymatgen/io/qchem/inputs.py @@ -16,6 +16,8 @@ if TYPE_CHECKING: from pathlib import Path + from typing_extensions import Self + __author__ = "Brandon Wood, Samuel Blau, Shyam Dwaraknath, Julian Self, Evan Spotte-Smith, Ryan Kingsbury" __copyright__ = "Copyright 2018-2022, The Materials Project" __version__ = "0.1" @@ -303,7 +305,7 @@ def multi_job_string(job_list: list[QCInput]) -> str: return multi_job_string @classmethod - def from_str(cls, string: str) -> QCInput: + def from_str(cls, string: str) -> Self: # type: ignore[override] """ Read QcInput from string. @@ -378,7 +380,7 @@ def write_multi_job_file(job_list: list[QCInput], filename: str): file.write(QCInput.multi_job_string(job_list)) @classmethod - def from_file(cls, filename: str | Path) -> QCInput: + def from_file(cls, filename: str | Path) -> Self: # type: ignore[override] """ Create QcInput from file. @@ -392,7 +394,7 @@ def from_file(cls, filename: str | Path) -> QCInput: return cls.from_str(file.read()) @classmethod - def from_multi_jobs_file(cls, filename: str) -> list[QCInput]: + def from_multi_jobs_file(cls, filename: str) -> list[Self]: """ Create list of QcInput from a file. diff --git a/pymatgen/io/res.py b/pymatgen/io/res.py index ce96ab77bc5..c57f26985ff 100644 --- a/pymatgen/io/res.py +++ b/pymatgen/io/res.py @@ -6,16 +6,15 @@ from and back to a string/file is not guaranteed to be reversible, i.e. a diff on the output would not be empty. The difference should be limited to whitespace, float precision, and the REM entries. - """ from __future__ import annotations +import datetime import re from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Callable, Literal -import dateutil.parser # type: ignore[import] from monty.io import zopen from monty.json import MSONable @@ -26,6 +25,9 @@ if TYPE_CHECKING: from collections.abc import Iterator from datetime import date + from pathlib import Path + + from typing_extensions import Self from pymatgen.core.trajectory import Vector3D @@ -237,10 +239,9 @@ def _parse_str(cls, source: str) -> Res: return self._parse_txt() @classmethod - def _parse_file(cls, filename: str) -> Res: + def _parse_file(cls, filename: str | Path) -> Res: """Parses the res file as a file.""" self = cls() - self.filename = filename with zopen(filename, mode="r") as file: self.source = file.read() return self._parse_txt() @@ -333,12 +334,12 @@ def _site_spin(cls, spin: float | None) -> dict[str, float] | None: return {"magmom": spin} @classmethod - def from_str(cls, string: str) -> ResProvider: + def from_str(cls, string: str) -> Self: """Construct a Provider from a string.""" return cls(ResParser._parse_str(string)) @classmethod - def from_file(cls, filename: str) -> ResProvider: + def from_file(cls, filename: str | Path) -> Self: """Construct a Provider from a file.""" return cls(ResParser._parse_file(filename)) @@ -402,12 +403,12 @@ def __init__(self, res: Res, parse_rems: Literal["gentle", "strict"] = "gentle") self.parse_rems = parse_rems @classmethod - def from_str(cls, string: str, parse_rems: Literal["gentle", "strict"] = "gentle") -> AirssProvider: + def from_str(cls, string: str, parse_rems: Literal["gentle", "strict"] = "gentle") -> Self: """Construct a Provider from a string.""" return cls(ResParser._parse_str(string), parse_rems) @classmethod - def from_file(cls, filename: str, parse_rems: Literal["gentle", "strict"] = "gentle") -> AirssProvider: + def from_file(cls, filename: str | Path, parse_rems: Literal["gentle", "strict"] = "gentle") -> Self: """Construct a Provider from a file.""" return cls(ResParser._parse_file(filename), parse_rems) @@ -417,8 +418,11 @@ def _parse_date(cls, string: str) -> date: match = cls._date_fmt.search(string) if match is None: raise ResParseError(f"Could not parse the date from {string=}.") - date_string = match.group(0) - return dateutil.parser.parse(date_string) + + day, month, year, *_ = match.groups() + month_num = datetime.datetime.strptime(month, "%b").month + + return datetime.date(int(year), month_num, int(day)) def _raise_or_none(self, err: ResParseError) -> None: if self.parse_rems != "strict": diff --git a/pymatgen/io/shengbte.py b/pymatgen/io/shengbte.py index 3d38f5ee214..666d367a963 100644 --- a/pymatgen/io/shengbte.py +++ b/pymatgen/io/shengbte.py @@ -127,7 +127,7 @@ def __init__(self, ngrid: list[int] | None = None, temperature: float | dict[str f90nml, "ShengBTE Control object requires f90nml to be installed. Please get it at https://pypi.org/project/f90nml.", ) - def from_file(cls, filepath: str): + def from_file(cls, filepath: str) -> Self: """ Read a CONTROL namelist file and output a 'Control' object. @@ -167,7 +167,7 @@ def from_dict(cls, control_dict: dict) -> Self: f90nml, "ShengBTE Control object requires f90nml to be installed. Please get it at https://pypi.org/project/f90nml.", ) - def to_file(self, filename: str = "CONTROL"): + def to_file(self, filename: str = "CONTROL") -> None: """ Writes ShengBTE CONTROL file from 'Control' object. @@ -194,11 +194,11 @@ def to_file(self, filename: str = "CONTROL"): flags_nml = f90nml.Namelist({"flags": flags_dict}) control_str += str(flags_nml) + "\n" - with open(filename, mode="w") as file: + with open(filename, mode="w", encoding="utf-8") as file: file.write(control_str) @classmethod - def from_structure(cls, structure: Structure, reciprocal_density: int | None = 50000, **kwargs): + def from_structure(cls, structure: Structure, reciprocal_density: int | None = 50000, **kwargs) -> Self: """ Get a ShengBTE control object from a structure. diff --git a/pymatgen/io/vasp/inputs.py b/pymatgen/io/vasp/inputs.py index 0ab6af20afe..14655536231 100644 --- a/pymatgen/io/vasp/inputs.py +++ b/pymatgen/io/vasp/inputs.py @@ -6,6 +6,7 @@ from __future__ import annotations import codecs +import contextlib import hashlib import itertools import json @@ -37,6 +38,7 @@ if TYPE_CHECKING: from collections.abc import Iterator, Sequence + from pathlib import Path from numpy.typing import ArrayLike from typing_extensions import Self @@ -219,7 +221,7 @@ def __setattr__(self, name, value): super().__setattr__(name, value) @classmethod - def from_file(cls, filename, check_for_potcar=True, read_velocities=True, **kwargs) -> Poscar: + def from_file(cls, filename, check_for_potcar=True, read_velocities=True, **kwargs) -> Self: """ Reads a Poscar from a file. @@ -270,7 +272,7 @@ def from_file(cls, filename, check_for_potcar=True, read_velocities=True, **kwar return cls.from_str(file.read(), names, read_velocities=read_velocities) @classmethod - def from_str(cls, data, default_names=None, read_velocities=True) -> Poscar: + def from_str(cls, data, default_names=None, read_velocities=True) -> Self: """ Reads a Poscar from a string. @@ -350,11 +352,10 @@ def from_str(cls, data, default_names=None, read_velocities=True) -> Poscar: # ... n_lines_symbols = 1 for n_lines_symbols in range(1, 11): - try: + with contextlib.suppress(ValueError): int(lines[5 + n_lines_symbols].split()[0]) break - except ValueError: - pass + for i_line_symbols in range(6, 5 + n_lines_symbols): symbols.extend(lines[i_line_symbols].split()) n_atoms = [] @@ -382,13 +383,11 @@ def from_str(cls, data, default_names=None, read_velocities=True) -> Poscar: # them. This is in line with VASP's parsing order that the POTCAR # specified is the default used. if default_names: - try: + with contextlib.suppress(IndexError): atomic_symbols = [] for i, nat in enumerate(n_atoms): atomic_symbols.extend([default_names[i]] * nat) vasp5_symbols = True - except IndexError: - pass if not vasp5_symbols: ind = 6 if has_selective_dynamics else 3 @@ -607,7 +606,7 @@ def as_dict(self) -> dict: } @classmethod - def from_dict(cls, dct: dict) -> Poscar: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. @@ -718,7 +717,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct: dict[str, Any]) -> Incar: + def from_dict(cls, dct: dict[str, Any]) -> Self: """ Args: dct (dict): Serialized Incar @@ -785,7 +784,7 @@ def write_file(self, filename: PathLike): file.write(str(self)) @classmethod - def from_file(cls, filename: PathLike) -> Incar: + def from_file(cls, filename: PathLike) -> Self: """Reads an Incar object from a file. Args: @@ -798,7 +797,7 @@ def from_file(cls, filename: PathLike) -> Incar: return cls.from_str(file.read()) @classmethod - def from_str(cls, string: str) -> Incar: + def from_str(cls, string: str) -> Self: """Reads an Incar object from a string. Args: @@ -869,7 +868,7 @@ def smart_int_or_float(num_str): return float(num_str) return int(num_str) - try: + with contextlib.suppress(ValueError): if key in list_keys: output = [] tokens = re.findall(r"(-?\d+\.?\d*)\*?(-?\d+\.?\d*)?\*?(-?\d+\.?\d*)?", val) @@ -882,11 +881,10 @@ def smart_int_or_float(num_str): output.append(smart_int_or_float(tok[0])) return output if key in bool_keys: - m = re.match(r"^\.?([T|F|t|f])[A-Za-z]*\.?", val) - if m: + if m := re.match(r"^\.?([T|F|t|f])[A-Za-z]*\.?", val): return m.group(1).lower() == "t" - raise ValueError(key + " should be a boolean type!") + raise ValueError(f"{key} should be a boolean type!") if key in float_keys: return float(re.search(r"^-?\d*\.?\d*[e|E]?-?\d*", val).group(0)) # type: ignore @@ -897,19 +895,12 @@ def smart_int_or_float(num_str): if key in lower_str_keys: return val.strip().lower() - except ValueError: - pass - # Not in standard keys. We will try a hierarchy of conversions. - try: + with contextlib.suppress(ValueError): return int(val) - except ValueError: - pass - try: + with contextlib.suppress(ValueError): return float(val) - except ValueError: - pass if "true" in val.lower(): return True @@ -1006,7 +997,7 @@ def __str__(self): return str(self.name) @classmethod - def from_str(cls, mode: str) -> KpointsSupportedModes: + def from_str(cls, mode: str) -> Self: """ Args: mode: String @@ -1015,7 +1006,7 @@ def from_str(cls, mode: str) -> KpointsSupportedModes: Kpoints_supported_modes """ initial = mode.lower()[0] - for key in KpointsSupportedModes: + for key in cls: if key.name.lower()[0] == initial: return key raise ValueError(f"Invalid Kpoint {mode=}") @@ -1031,7 +1022,7 @@ def __init__( comment: str = "Default gamma", num_kpts: int = 0, style: KpointsSupportedModes = supported_modes.Gamma, - kpts: Sequence[float | Sequence] = ((1, 1, 1),), + kpts: Sequence[float | Sequence[float]] = ((1, 1, 1),), kpts_shift: Vector3D = (0, 0, 0), kpts_weights=None, coord_type=None, @@ -1089,7 +1080,7 @@ def __init__( self.style = style self.coord_type = coord_type self.kpts_weights = kpts_weights - self.kpts_shift = kpts_shift + self.kpts_shift = tuple(kpts_shift) self.labels = labels self.tet_number = tet_number self.tet_weight = tet_weight @@ -1355,7 +1346,7 @@ def __eq__(self, other: object) -> bool: return self.as_dict() == other.as_dict() @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Reads a Kpoints object from a KPOINTS file. @@ -1369,7 +1360,7 @@ def from_file(cls, filename): return cls.from_str(file.read()) @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """ Reads a Kpoints object from a KPOINTS string. @@ -1392,63 +1383,68 @@ def from_str(cls, string): coord_pattern = re.compile(r"^\s*([\d+.\-Ee]+)\s+([\d+.\-Ee]+)\s+([\d+.\-Ee]+)") # Automatic gamma and Monk KPOINTS, with optional shift - if style in ["g", "m"]: - kpts = [int(i) for i in lines[3].split()] - kpts_shift = (0, 0, 0) + if style in {"g", "m"}: + kpts = tuple(int(i) for i in lines[3].split()) + assert len(kpts) == 3 + + kpts_shift: tuple[float, float, float] = (0, 0, 0) if len(lines) > 4 and coord_pattern.match(lines[4]): - try: - kpts_shift = [float(i) for i in lines[4].split()] - except ValueError: - pass + with contextlib.suppress(ValueError): + _kpts_shift = tuple(float(i) for i in lines[4].split()) + if len(_kpts_shift) == 3: + kpts_shift = _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 = 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( + _style = cls.supported_modes.Cartesian if style in "ck" else cls.supported_modes.Reciprocal + _kpts_shift = tuple(float(i) for i in lines[6].split()) + if len(_kpts_shift) == 3: + kpts_shift = _kpts_shift + + return cls( comment=comment, num_kpts=num_kpts, - style=style, - kpts=kpts, + style=_style, + kpts=[[float(j) for j in lines[i].split()] for i in range(3, 6)], kpts_shift=kpts_shift, ) # Line-mode KPOINTS, usually used with band structures if style == "l": coord_type = "Cartesian" if lines[3].lower()[0] in "ck" else "Reciprocal" - style = cls.supported_modes.Line_mode - kpts = [] + _style = cls.supported_modes.Line_mode + _kpts: list[list[float]] = [] labels = [] patt = re.compile(r"([e0-9.\-]+)\s+([e0-9.\-]+)\s+([e0-9.\-]+)\s*!*\s*(.*)") for idx in range(4, len(lines)): line = lines[idx] m = patt.match(line) if m: - kpts.append([float(m.group(1)), float(m.group(2)), float(m.group(3))]) + _kpts.append([float(m.group(1)), float(m.group(2)), float(m.group(3))]) labels.append(m.group(4).strip()) - return Kpoints( + return cls( comment=comment, num_kpts=num_kpts, - style=style, - kpts=kpts, + style=_style, + kpts=_kpts, coord_type=coord_type, labels=labels, ) # Assume explicit KPOINTS if all else fails. - style = cls.supported_modes.Cartesian if style in "ck" else cls.supported_modes.Reciprocal - kpts = [] + _style = cls.supported_modes.Cartesian if style in "ck" else cls.supported_modes.Reciprocal + _kpts = [] kpts_weights = [] labels = [] tet_number = 0 - tet_weight = 0 + tet_weight: float = 0 tet_connections = None for idx in range(3, 3 + num_kpts): tokens = lines[idx].split() - kpts.append([float(j) for j in tokens[0:3]]) + _kpts.append([float(j) for j in tokens[:3]]) kpts_weights.append(float(tokens[3])) if len(tokens) > 4: labels.append(tokens[4]) @@ -1470,8 +1466,8 @@ def from_str(cls, string): return cls( comment=comment, num_kpts=num_kpts, - style=cls.supported_modes[str(style)], - kpts=kpts, + style=cls.supported_modes[str(_style)], + kpts=_kpts, kpts_weights=kpts_weights, tet_number=tet_number, tet_weight=tet_weight, @@ -1479,7 +1475,7 @@ def from_str(cls, string): labels=labels, ) - def write_file(self, filename): + def write_file(self, filename: str) -> None: """ Write Kpoints to a file. @@ -1840,7 +1836,7 @@ def from_file(cls, filename: str) -> Self: PotcarSingle """ match = re.search(r"(?<=POTCAR\.)(.*)(?=.gz)", str(filename)) - symbol = match.group(0) if match else "" + symbol = match[0] if match else "" try: with zopen(filename, mode="rt") as file: @@ -1852,7 +1848,7 @@ def from_file(cls, filename: str) -> Self: return cls(file.read(), symbol=symbol or None) @classmethod - def from_symbol_and_functional(cls, symbol: str, functional: str | None = None): + def from_symbol_and_functional(cls, symbol: str, functional: str | None = None) -> Self: """Makes a PotcarSingle from a symbol and functional. Args: @@ -2652,7 +2648,7 @@ def write_input(self, output_dir=".", make_dir_if_not_present=True): file.write(str(v)) @classmethod - def from_directory(cls, input_dir, optional_files=None): + def from_directory(cls, input_dir: str, optional_files: dict | None = None) -> Self: """ Read in a set of VASP input from a directory. Note that only the standard INCAR, POSCAR, POTCAR and KPOINTS files are read unless @@ -2673,14 +2669,14 @@ def from_directory(cls, input_dir, optional_files=None): ]: try: full_zpath = zpath(os.path.join(input_dir, fname)) - sub_dct[fname.lower()] = ftype.from_file(full_zpath) + sub_dct[fname.lower()] = ftype.from_file(full_zpath) # type: ignore[attr-defined] except FileNotFoundError: # handle the case where there is no KPOINTS file sub_dct[fname.lower()] = None - sub_dct["optional_files"] = {} - if optional_files is not None: - for fname, ftype in optional_files.items(): - sub_dct["optional_files"][fname] = ftype.from_file(os.path.join(input_dir, fname)) + sub_dct["optional_files"] = { + fname: ftype.from_file(os.path.join(input_dir, fname)) for fname, ftype in (optional_files or {}).items() + } + return cls(**sub_dct) def copy(self, deep: bool = True): diff --git a/pymatgen/io/vasp/optics.py b/pymatgen/io/vasp/optics.py index 7e9c0f1cff5..35ccd1a3cdf 100644 --- a/pymatgen/io/vasp/optics.py +++ b/pymatgen/io/vasp/optics.py @@ -19,6 +19,7 @@ from pathlib import Path from numpy.typing import ArrayLike, NDArray + from typing_extensions import Self __author__ = "Jimmy-Xuan Shen" __copyright__ = "Copyright 2022, The Materials Project" @@ -70,7 +71,7 @@ class DielectricFunctionCalculator(MSONable): volume: float @classmethod - def from_vasp_objects(cls, vrun: Vasprun, waveder: Waveder): + def from_vasp_objects(cls, vrun: Vasprun, waveder: Waveder) -> Self: """Construct a DielectricFunction from Vasprun, Kpoint, and Waveder objects. Args: @@ -94,7 +95,7 @@ def from_vasp_objects(cls, vrun: Vasprun, waveder: Waveder): if vrun.parameters["ISYM"] != 0: raise NotImplementedError("ISYM != 0 is not implemented yet") - return DielectricFunctionCalculator( + return cls( cder_real=waveder.cder_real, cder_imag=waveder.cder_imag, eigs=eigs, @@ -110,7 +111,7 @@ def from_vasp_objects(cls, vrun: Vasprun, waveder: Waveder): ) @classmethod - def from_directory(cls, directory: Path | str): + def from_directory(cls, directory: Path | str) -> Self: """Construct a DielectricFunction from a directory containing vasprun.xml and WAVEDER files.""" def _try_reading(dtypes): diff --git a/pymatgen/io/vasp/outputs.py b/pymatgen/io/vasp/outputs.py index a1a09371d4c..1c0fb5f6fa5 100644 --- a/pymatgen/io/vasp/outputs.py +++ b/pymatgen/io/vasp/outputs.py @@ -3624,7 +3624,7 @@ def __init__(self, poscar: Poscar, data: np.ndarray, **kwargs) -> None: self.name = poscar.comment @classmethod - def from_file(cls, filename, **kwargs): + def from_file(cls, filename: str, **kwargs) -> Self: """Read a LOCPOT file. Args: @@ -4894,9 +4894,6 @@ def __init__(self, filename, occu_tol=1e-8, separate_spins=False): reported for each individual spin channel. Defaults to False, which computes the eigenvalue band properties independent of the spin orientation. If True, the calculation must be spin-polarized. - - Returns: - a pymatgen.io.vasp.outputs.Eigenval object """ self.filename = filename self.occu_tol = occu_tol diff --git a/pymatgen/io/vasp/sets.py b/pymatgen/io/vasp/sets.py index 01d680da23d..f0dcbca2b1a 100644 --- a/pymatgen/io/vasp/sets.py +++ b/pymatgen/io/vasp/sets.py @@ -34,12 +34,13 @@ import re import shutil import warnings +from collections.abc import Sequence from copy import deepcopy from dataclasses import dataclass, field from glob import glob from itertools import chain from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Union +from typing import TYPE_CHECKING, Any, Literal, Union, cast from zipfile import ZipFile import numpy as np @@ -58,7 +59,7 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: - from collections.abc import Sequence + from typing_extensions import Self from pymatgen.core.trajectory import Vector3D @@ -1024,7 +1025,7 @@ def override_from_prev_calc(self, prev_calc_dir="."): return self @classmethod - def from_prev_calc(cls, prev_calc_dir, **kwargs): + def from_prev_calc(cls, prev_calc_dir: str, **kwargs) -> Self: """ Generate a set of VASP input files for static calculations from a directory of previous VASP run. @@ -2076,7 +2077,7 @@ def incar_updates(self) -> dict: return updates @classmethod - def from_prev_calc(cls, prev_calc_dir, mode="DIAG", **kwargs): + def from_prev_calc(cls, prev_calc_dir: str, mode: str = "DIAG", **kwargs) -> Self: """ Generate a set of VASP input files for GW or BSE calculations from a directory of previous Exact Diag VASP run. @@ -3070,9 +3071,7 @@ def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None) -> int: def _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: """Combine k-points files together.""" - labels = [] - kpoints = [] - weights = [] + labels, kpoints, weights = [], [], [] for kpoints_object in filter(None, kpoints_objects): if kpoints_object.style != Kpoints.supported_modes.Reciprocal: @@ -3092,7 +3091,7 @@ def _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: comment="Combined k-points", style=Kpoints.supported_modes.Reciprocal, num_kpts=len(kpoints), - kpts=kpoints, + kpts=cast(Sequence[Sequence[float]], kpoints), labels=labels, kpts_weights=weights, ) diff --git a/pymatgen/io/wannier90.py b/pymatgen/io/wannier90.py index a63a88e7cf3..295b8149036 100644 --- a/pymatgen/io/wannier90.py +++ b/pymatgen/io/wannier90.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + __author__ = "Mark Turiansky" __copyright__ = "Copyright 2011, The Materials Project" __version__ = "0.1" @@ -89,7 +91,7 @@ def data(self, value: np.ndarray) -> None: self.ng = self.data.shape[-3:] @classmethod - def from_file(cls, filename: str) -> object: + def from_file(cls, filename: str) -> Self: """ Reads the UNK data from file. diff --git a/pymatgen/io/xcrysden.py b/pymatgen/io/xcrysden.py index 41d2165f5af..29a230b3dfa 100644 --- a/pymatgen/io/xcrysden.py +++ b/pymatgen/io/xcrysden.py @@ -2,8 +2,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pymatgen.core import Element, Structure +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Matteo Giantomassi" __copyright__ = "Copyright 2013, The Materials Project" __version__ = "0.1" @@ -20,7 +25,7 @@ def __init__(self, structure: Structure): """ self.structure = structure - def to_str(self, atom_symbol=True): + def to_str(self, atom_symbol: bool = True) -> str: """ Returns a string with the structure in XSF format See http://www.xcrysden.org/doc/XSF.html. @@ -28,25 +33,24 @@ def to_str(self, atom_symbol=True): Args: atom_symbol (bool): Uses atom symbol instead of atomic number. Defaults to True. """ - lines = [] - app = lines.append + lines: list[str] = [] - app("CRYSTAL") - app("# Primitive lattice vectors in Angstrom") - app("PRIMVEC") + lines.append("CRYSTAL") + lines.append("# Primitive lattice vectors in Angstrom") + lines.append("PRIMVEC") cell = self.structure.lattice.matrix for i in range(3): - app(f" {cell[i][0]:.14f} {cell[i][1]:.14f} {cell[i][2]:.14f}") + lines.append(f" {cell[i][0]:.14f} {cell[i][1]:.14f} {cell[i][2]:.14f}") cart_coords = self.structure.cart_coords - app("# Cartesian coordinates in Angstrom.") - app("PRIMCOORD") - app(f" {len(cart_coords)} 1") + lines.append("# Cartesian coordinates in Angstrom.") + lines.append("PRIMCOORD") + lines.append(f" {len(cart_coords)} 1") for site, coord in zip(self.structure, cart_coords): sp = site.specie.symbol if atom_symbol else f"{site.specie.Z}" x, y, z = coord - app(f"{sp} {x:20.14f} {y:20.14f} {z:20.14f}") + lines.append(f"{sp} {x:20.14f} {y:20.14f} {z:20.14f}") if "vect" in site.properties: vx, vy, vz = site.properties["vect"] lines[-1] += f" {vx:20.14f} {vy:20.14f} {vz:20.14f}" @@ -54,7 +58,7 @@ def to_str(self, atom_symbol=True): return "\n".join(lines) @classmethod - def from_str(cls, input_string, cls_=None): + def from_str(cls, input_string: str, cls_=None) -> Self: """ Initialize a `Structure` object from a string with data in XSF format. @@ -62,26 +66,27 @@ def from_str(cls, input_string, cls_=None): input_string: String with the structure in XSF format. See http://www.xcrysden.org/doc/XSF.html cls_: Structure class to be created. default: pymatgen structure - """ - # CRYSTAL see (1) - # these are primitive lattice vectors (in Angstroms) - # PRIMVEC - # 0.0000000 2.7100000 2.7100000 see (2) - # 2.7100000 0.0000000 2.7100000 - # 2.7100000 2.7100000 0.0000000 - - # these are conventional lattice vectors (in Angstroms) - # CONVVEC - # 5.4200000 0.0000000 0.0000000 see (3) - # 0.0000000 5.4200000 0.0000000 - # 0.0000000 0.0000000 5.4200000 - - # these are atomic coordinates in a primitive unit cell (in Angstroms) - # PRIMCOORD - # 2 1 see (4) - # 16 0.0000000 0.0000000 0.0000000 see (5) - # 30 1.3550000 -1.3550000 -1.3550000 + Example file: + CRYSTAL see (1) + these are primitive lattice vectors (in Angstroms) + PRIMVEC + 0.0000000 2.7100000 2.7100000 see (2) + 2.7100000 0.0000000 2.7100000 + 2.7100000 2.7100000 0.0000000 + + these are conventional lattice vectors (in Angstroms) + CONVVEC + 5.4200000 0.0000000 0.0000000 see (3) + 0.0000000 5.4200000 0.0000000 + 0.0000000 0.0000000 5.4200000 + + these are atomic coordinates in a primitive unit cell (in Angstroms) + PRIMCOORD + 2 1 see (4) + 16 0.0000000 0.0000000 0.0000000 see (5) + 30 1.3550000 -1.3550000 -1.3550000 + """ lattice, coords, species = [], [], [] lines = input_string.splitlines() @@ -105,5 +110,4 @@ def from_str(cls, input_string, cls_=None): if cls_ is None: cls_ = Structure - s = cls_(lattice, species, coords, coords_are_cartesian=True) - return XSF(s) + return cls(cls_(lattice, species, coords, coords_are_cartesian=True)) diff --git a/pymatgen/io/xr.py b/pymatgen/io/xr.py index e576f1f47e4..07b3e54e070 100644 --- a/pymatgen/io/xr.py +++ b/pymatgen/io/xr.py @@ -11,6 +11,7 @@ import re from math import fabs +from typing import TYPE_CHECKING import numpy as np from monty.io import zopen @@ -18,6 +19,11 @@ from pymatgen.core.lattice import Lattice from pymatgen.core.structure import Structure +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + __author__ = "Nils Edvin Richard Zimmermann" __copyright__ = "Copyright 2016, The Materials Project" __version__ = "0.1" @@ -58,7 +64,7 @@ def __str__(self): output.append(f"{mat[j][0]:.4f} {mat[j][1]:.4f} {mat[j][2]:.4f}") return "\n".join(output) - def write_file(self, filename): + def write_file(self, filename: str | Path) -> None: """ Write out an xr file. @@ -69,7 +75,7 @@ def write_file(self, filename): file.write(str(self) + "\n") @classmethod - def from_str(cls, string, use_cores=True, thresh=1.0e-4): + def from_str(cls, string: str, use_cores: bool = True, thresh: float = 1.0e-4) -> Self: """ Creates an Xr object from a string representation. @@ -136,7 +142,7 @@ def from_str(cls, string, use_cores=True, thresh=1.0e-4): return cls(Structure(lattice, sp, coords, coords_are_cartesian=True)) @classmethod - def from_file(cls, filename, use_cores=True, thresh=1.0e-4): + def from_file(cls, filename: str | Path, use_cores: bool = True, thresh: float = 1.0e-4) -> Self: """ Reads an xr-formatted file to create an Xr object. diff --git a/pymatgen/io/xyz.py b/pymatgen/io/xyz.py index 5b4d66f82b5..74255f7d747 100644 --- a/pymatgen/io/xyz.py +++ b/pymatgen/io/xyz.py @@ -4,7 +4,7 @@ import re from io import StringIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pandas as pd from monty.io import zopen @@ -14,6 +14,9 @@ if TYPE_CHECKING: from collections.abc import Sequence + from pathlib import Path + + from typing_extensions import Self class XYZ: @@ -34,7 +37,7 @@ def __init__(self, mol: Molecule | Structure | Sequence[Molecule | Structure], c mol (Molecule | Structure): Input molecule or structure or list thereof. coord_precision: Precision to be used for coordinates. """ - self._mols = [mol] if isinstance(mol, SiteCollection) else mol + self._mols = cast(list[SiteCollection], [mol] if isinstance(mol, SiteCollection) else mol) self.precision = coord_precision @property @@ -71,7 +74,7 @@ def _from_frame_str(contents) -> Molecule: return Molecule(sp, coords) @classmethod - def from_str(cls, contents) -> XYZ: + def from_str(cls, contents: str) -> Self: """ Creates XYZ object from a string. @@ -96,7 +99,7 @@ def from_str(cls, contents) -> XYZ: return cls(mols) @classmethod - def from_file(cls, filename) -> XYZ: + def from_file(cls, filename: str | Path) -> Self: """ Creates XYZ object from a file. diff --git a/pymatgen/io/zeopp.py b/pymatgen/io/zeopp.py index 0341cf3379f..d27f61488bc 100644 --- a/pymatgen/io/zeopp.py +++ b/pymatgen/io/zeopp.py @@ -26,6 +26,7 @@ import os import re +from typing import TYPE_CHECKING from monty.dev import requires from monty.io import zopen @@ -44,6 +45,11 @@ except ImportError: zeo_found = False +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import Self + __author__ = "Bharat Medasani" __copyright__ = "Copyright 2013, The Materials Project" __version__ = "0.1" @@ -91,7 +97,7 @@ def __str__(self): return "\n".join(output) @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> Self: """ Reads a string representation to a ZeoCssr object. @@ -112,24 +118,25 @@ def from_str(cls, string): alpha = angles.pop(-1) angles.insert(0, alpha) lattice = Lattice.from_parameters(*lengths, *angles) + sp = [] coords = [] charge = [] for line in lines[4:]: - m = re.match( + match = re.match( r"\d+\s+(\w+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)\s+([0-9\-\.]+)\s+(?:0\s+){8}([0-9\-\.]+)", line.strip(), ) - if m: - sp.append(m.group(1)) + if match: + sp.append(match.group(1)) # 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]]) - charge.append(m.group(5)) + coords.append([float(match.group(i)) for i in [3, 4, 2]]) + charge.append(match.group(5)) return cls(Structure(lattice, sp, coords, site_properties={"charge": charge})) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Reads a CSSR file to a ZeoCssr object. @@ -158,7 +165,7 @@ def __init__(self, mol): super().__init__(mol) @classmethod - def from_str(cls, contents): + def from_str(cls, contents: str) -> Self: """ Creates Zeo++ Voronoi XYZ object from a string. from_string method of XYZ class is being redefined. @@ -185,7 +192,7 @@ def from_str(cls, contents): return cls(Molecule(sp, coords, site_properties={"voronoi_radius": prop})) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str | Path) -> Self: """ Creates XYZ object from a file. diff --git a/pymatgen/phonon/bandstructure.py b/pymatgen/phonon/bandstructure.py index 3ad8215a07e..9fdb73347cc 100644 --- a/pymatgen/phonon/bandstructure.py +++ b/pymatgen/phonon/bandstructure.py @@ -17,6 +17,7 @@ from os import PathLike from numpy.typing import ArrayLike + from typing_extensions import Self def get_reasonable_repetitions(n_atoms: int) -> tuple[int, int, int]: @@ -304,7 +305,7 @@ def as_dict(self) -> dict[str, Any]: return dct @classmethod - def from_dict(cls, dct: dict[str, Any]) -> PhononBandStructure: + def from_dict(cls, dct: dict[str, Any]) -> Self: """ Args: dct (dict): Dict representation of PhononBandStructure. @@ -630,7 +631,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct: dict) -> PhononBandStructureSymmLine: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. diff --git a/pymatgen/phonon/dos.py b/pymatgen/phonon/dos.py index 698f71f47f2..8f1874048fb 100644 --- a/pymatgen/phonon/dos.py +++ b/pymatgen/phonon/dos.py @@ -16,6 +16,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + BOLTZ_THZ_PER_K = const.value("Boltzmann constant in Hz/K") / const.tera # Boltzmann constant in THz/K THZ_TO_J = const.value("hertz-joule relationship") * const.tera @@ -133,7 +135,7 @@ def __str__(self) -> str: return "\n".join(str_arr) @classmethod - def from_dict(cls, dct: dict[str, Sequence]) -> PhononDos: + def from_dict(cls, dct: dict[str, Sequence]) -> Self: """Returns PhononDos object from dict representation of PhononDos.""" return cls(dct["frequencies"], dct["densities"]) @@ -458,7 +460,7 @@ def get_element_dos(self) -> dict: return {el: PhononDos(self.frequencies, densities) for el, densities in el_dos.items()} @classmethod - def from_dict(cls, dct: dict) -> CompletePhononDos: + def from_dict(cls, dct: dict) -> Self: """Returns CompleteDos object from dict representation.""" total_dos = PhononDos.from_dict(dct) struct = Structure.from_dict(dct["structure"]) diff --git a/pymatgen/phonon/gruneisen.py b/pymatgen/phonon/gruneisen.py index e364dfd37a1..447bf5a1ccb 100644 --- a/pymatgen/phonon/gruneisen.py +++ b/pymatgen/phonon/gruneisen.py @@ -27,6 +27,7 @@ from typing import Literal from numpy.typing import ArrayLike + from typing_extensions import Self __author__ = "A. Bonkowski, J. George, G. Petretto" __copyright__ = "Copyright 2021, The Materials Project" @@ -311,7 +312,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct: dict) -> GruneisenPhononBandStructure: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. @@ -392,7 +393,7 @@ def __init__( ) @classmethod - def from_dict(cls, dct: dict) -> GruneisenPhononBandStructureSymmLine: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. diff --git a/pymatgen/phonon/ir_spectra.py b/pymatgen/phonon/ir_spectra.py index 86706fd08ad..f9c1caacfa7 100644 --- a/pymatgen/phonon/ir_spectra.py +++ b/pymatgen/phonon/ir_spectra.py @@ -23,6 +23,7 @@ from matplotlib.axes import Axes from numpy.typing import ArrayLike + from typing_extensions import Self __author__ = "Henrique Miranda, Guido Petretto, Matteo Giantomassi" __copyright__ = "Copyright 2018, The Materials Project" @@ -59,7 +60,7 @@ def __init__( self.epsilon_infinity = np.array(epsilon_infinity) @classmethod - def from_dict(cls, dct: dict) -> IRDielectricTensor: + def from_dict(cls, dct: dict) -> Self: """Returns IRDielectricTensor from dict representation.""" structure = Structure.from_dict(dct["structure"]) oscillator_strength = dct["oscillator_strength"] diff --git a/pymatgen/phonon/thermal_displacements.py b/pymatgen/phonon/thermal_displacements.py index 4640548e6f6..68db082dc6a 100644 --- a/pymatgen/phonon/thermal_displacements.py +++ b/pymatgen/phonon/thermal_displacements.py @@ -491,9 +491,7 @@ def sort_order(site): return self.structure.copy(site_properties=site_properties) @classmethod - def from_structure_with_site_properties_Ucif( - cls, structure: Structure, temperature: float | None = None - ) -> ThermalDisplacementMatrices: + def from_structure_with_site_properties_Ucif(cls, structure: Structure, temperature: float | None = None) -> Self: """Will create this object with the help of a structure with site properties. Args: diff --git a/pymatgen/symmetry/groups.py b/pymatgen/symmetry/groups.py index dd0b863995c..69e75ee4f92 100644 --- a/pymatgen/symmetry/groups.py +++ b/pymatgen/symmetry/groups.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from numpy.typing import ArrayLike + from typing_extensions import Self # don't import at runtime to avoid circular import from pymatgen.core.lattice import Lattice @@ -476,7 +477,7 @@ def is_supergroup(self, subgroup: SymmetryGroup) -> bool: return subgroup.is_subgroup(self) @classmethod - def from_int_number(cls, int_number: int, hexagonal: bool = True) -> SpaceGroup: + def from_int_number(cls, int_number: int, hexagonal: bool = True) -> Self: """Obtains a SpaceGroup from its international number. Args: @@ -495,7 +496,7 @@ def from_int_number(cls, int_number: int, hexagonal: bool = True) -> SpaceGroup: symbol = sg_symbol_from_int_number(int_number, hexagonal=hexagonal) if not hexagonal and int_number in (146, 148, 155, 160, 161, 166, 167): symbol += ":R" - return SpaceGroup(symbol) + return cls(symbol) def __repr__(self) -> str: symbol = self.symbol diff --git a/pymatgen/symmetry/maggroups.py b/pymatgen/symmetry/maggroups.py index 004171e2c1c..aa2460be385 100644 --- a/pymatgen/symmetry/maggroups.py +++ b/pymatgen/symmetry/maggroups.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing_extensions import Self + from pymatgen.core.lattice import Lattice __author__ = "Matthew Horton, Shyue Ping Ong" @@ -284,7 +286,7 @@ def _parse_transformation(b): db.close() @classmethod - def from_og(cls, label: Sequence[int] | str) -> MagneticSpaceGroup: + def from_og(cls, label: Sequence[int] | str) -> Self: """Initialize from Opechowski and Guccione (OG) label or number. Args: diff --git a/pymatgen/symmetry/settings.py b/pymatgen/symmetry/settings.py index a329c4f4466..e48f7ac9070 100644 --- a/pymatgen/symmetry/settings.py +++ b/pymatgen/symmetry/settings.py @@ -4,6 +4,7 @@ import re from fractions import Fraction +from typing import TYPE_CHECKING import numpy as np from sympy import Matrix @@ -13,6 +14,9 @@ from pymatgen.core.operations import MagSymmOp, SymmOp from pymatgen.util.string import transformation_to_string +if TYPE_CHECKING: + from typing_extensions import Self + __author__ = "Matthew Horton" __copyright__ = "Copyright 2017, The Materials Project" __version__ = "0.1" @@ -56,7 +60,7 @@ def __init__(self, P, p): self._P, self._p = P, p @classmethod - def from_transformation_str(cls, transformation_string="a,b,c;0,0,0"): + def from_transformation_str(cls, transformation_string: str = "a,b,c;0,0,0") -> Self: """Construct SpaceGroupTransformation from its transformation string. Args: @@ -69,7 +73,7 @@ def from_transformation_str(cls, transformation_string="a,b,c;0,0,0"): return cls(P, p) @classmethod - def from_origin_shift(cls, origin_shift="0,0,0"): + def from_origin_shift(cls, origin_shift: str = "0,0,0") -> Self: """Construct SpaceGroupTransformation from its origin shift string. Args: diff --git a/pymatgen/transformations/standard_transformations.py b/pymatgen/transformations/standard_transformations.py index 7c6c3a16937..e72acd52bf2 100644 --- a/pymatgen/transformations/standard_transformations.py +++ b/pymatgen/transformations/standard_transformations.py @@ -211,7 +211,7 @@ def __init__(self, scaling_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1))): self.scaling_matrix = scaling_matrix @classmethod - def from_scaling_factors(cls, scale_a=1, scale_b=1, scale_c=1): + def from_scaling_factors(cls, scale_a: float = 1, scale_b: float = 1, scale_c: float = 1) -> Self: """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], diff --git a/pymatgen/util/provenance.py b/pymatgen/util/provenance.py index 26a301138fe..df6bbfc3cb2 100644 --- a/pymatgen/util/provenance.py +++ b/pymatgen/util/provenance.py @@ -311,7 +311,7 @@ def from_structures( data=None, histories=None, created_at=None, - ): + ) -> list[Self]: """A convenience method for getting a list of StructureNL objects by specifying structures and metadata separately. Some of the metadata is applied to all of the structures for ease of use. @@ -340,7 +340,7 @@ def from_structures( snl_list = [] for idx, struct in enumerate(structures): - snl = StructureNL( + snl = cls( struct, authors, projects=projects, diff --git a/pymatgen/util/string.py b/pymatgen/util/string.py index a056a0244dc..bc6f3b9b99a 100644 --- a/pymatgen/util/string.py +++ b/pymatgen/util/string.py @@ -334,7 +334,8 @@ def disordered_formula(disordered_struct, symbols=("x", "y", "z"), fmt="plain"): species more symbols will need to be added fmt (str): 'plain', 'HTML' or 'LaTeX' - Returns (str): a disordered formula string + Returns: + str: a disordered formula string """ # this is in string utils and not in Composition because we need to have access to # site occupancies to calculate this, so have to pass the full structure as an diff --git a/tests/analysis/test_reaction_calculator.py b/tests/analysis/test_reaction_calculator.py index 98536781de4..4385c7edbb8 100644 --- a/tests/analysis/test_reaction_calculator.py +++ b/tests/analysis/test_reaction_calculator.py @@ -372,7 +372,7 @@ def setUp(self): self.rxn = ComputedReaction(reactants, prods) - def test_calculated_reaction_energy(self): + def test_nd_reaction_energy(self): assert self.rxn.calculated_reaction_energy == approx(-5.60748821935) def test_calculated_reaction_energy_uncertainty(self): diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index 625bf8ecfb3..4d96bc49aed 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -635,14 +635,12 @@ def test_build_slab(self): assert len(slab) == len(recon_slab) - 2 assert recon_slab.is_symmetric() - # If a slab references another slab, - # make sure it is properly generated + # If a slab references another slab, make sure it is properly generated recon = ReconstructionGenerator(self.Ni, 10, 10, "fcc_111_adatom_ft_1x1") slab = recon.build_slabs()[0] assert slab.is_symmetric - # Test a reconstruction where it works on a specific - # termination (Fd-3m (111)) + # Test a reconstruction where it works on a specific termination (Fd-3m (111)) recon = ReconstructionGenerator(self.Si, 10, 10, "diamond_111_1x2") slab = recon.get_unreconstructed_slabs()[0] recon_slab = recon.build_slabs()[0] diff --git a/tests/io/test_adf.py b/tests/io/test_adf.py index 939f4ca99a2..188d9735066 100644 --- a/tests/io/test_adf.py +++ b/tests/io/test_adf.py @@ -71,15 +71,11 @@ def readfile(file_object): """` Return the content of the file as a string. - Parameters - ---------- - file_object : file or str - The file to read. This can be either a File object or a file path. + Args: + file_object (file or str): The file to read. This can be either a File object or a file path. Returns: - ------- - content : str - The content of the file. + content (str): The content of the file. """ if hasattr(file_object, "read"): return file_object.read() diff --git a/tests/io/test_core.py b/tests/io/test_core.py index bb12ebedb23..892c44d46e3 100644 --- a/tests/io/test_core.py +++ b/tests/io/test_core.py @@ -2,6 +2,7 @@ import copy import os +from typing import TYPE_CHECKING import pytest from monty.serialization import MontyDecoder @@ -11,6 +12,9 @@ from pymatgen.io.core import InputFile, InputSet from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest +if TYPE_CHECKING: + from typing_extensions import Self + class StructInputFile(InputFile): """Test implementation of an InputFile object for CIF.""" @@ -23,7 +27,7 @@ def get_str(self) -> str: return str(cw) @classmethod - def from_str(cls, contents: str): + def from_str(cls, contents: str) -> Self: # type: ignore[override] struct = Structure.from_str(contents, fmt="cif") return cls(structure=struct) diff --git a/tests/io/test_res.py b/tests/io/test_res.py index 445cd10d5ba..3cb19218d81 100644 --- a/tests/io/test_res.py +++ b/tests/io/test_res.py @@ -52,6 +52,8 @@ def test_misc(self, provider: AirssProvider): date, path = rs_info assert path == "/path/to/airss/run" assert date.day == 16 + assert date.month == 7 + assert date.year == 2021 castep_v = provider.get_castep_version() assert castep_v == "19.11" diff --git a/tests/io/vasp/test_inputs.py b/tests/io/vasp/test_inputs.py index e76cf1e5216..53115fd3c0f 100644 --- a/tests/io/vasp/test_inputs.py +++ b/tests/io/vasp/test_inputs.py @@ -814,12 +814,12 @@ def test_init(self): filepath = f"{VASP_IN_DIR}/KPOINTS_cartesian" kpoints = Kpoints.from_file(filepath) assert kpoints.kpts == [[0.25, 0, 0], [0, 0.25, 0], [0, 0, 0.25]], "Wrong kpoint lattice read" - assert kpoints.kpts_shift == [0.5, 0.5, 0.5], "Wrong kpoint shift read" + assert kpoints.kpts_shift == (0.5, 0.5, 0.5) filepath = f"{VASP_IN_DIR}/KPOINTS" kpoints = Kpoints.from_file(filepath) self.kpoints = kpoints - assert kpoints.kpts == [[2, 4, 6]] + assert kpoints.kpts == [(2, 4, 6)] filepath = f"{VASP_IN_DIR}/KPOINTS_band" kpoints = Kpoints.from_file(filepath)