Source code for lobsterpy.quality.analyze

"""Analyze and summarize LOBSTER calculation quality."""

from __future__ import annotations

import warnings
from pathlib import Path

from monty.os.path import zpath
from pymatgen.analysis.bond_valence import BVAnalyzer
from pymatgen.core.structure import Structure
from pymatgen.electronic_structure.dos import CompleteDos, LobsterCompleteDos
from pymatgen.io.lobster import (
    Bandoverlaps,
    Charge,
    Doscar,
    Lobsterin,
    Lobsterout,
)
from pymatgen.io.vasp import Vasprun

from lobsterpy.quality.describe import get_calc_quality_description


[docs] class LobsterCalcQuality: """ Analyze and summarize LOBSTER calculation quality. :param structure: pymatgen Structure object :param lobsterin: Lobsterin object :param lobsterout: Lobsterout object :param potcar_symbols: List of POTCAR symbols used in the calculation :param charge: Charge object (optional) :param bandoverlaps: Bandoverlaps object (optional) :param lobster_dos: LobsterCompleteDos object (optional) :param vasp_dos: CompleteDos object from VASP (optional) """ def __init__( self, *, structure: Structure, lobsterin: Lobsterin, lobsterout: Lobsterout, potcar_symbols: list[str], charge: Charge | None = None, bandoverlaps: Bandoverlaps | None = None, lobster_dos: LobsterCompleteDos | None = None, vasp_dos: CompleteDos | None = None, ): """ Initialize LobsterCalcQuality instance. :param structure: pymatgen Structure object :param lobsterin: Lobsterin object :param lobsterout: Lobsterout object :param potcar_symbols: List of POTCAR symbols used in the calculation :param charge: Charge object (optional) :param bandoverlaps: Bandoverlaps object (optional) :param lobster_dos: LobsterCompleteDos object (optional) :param vasp_dos: CompleteDos object from VASP (optional) """ self.structure = structure self.lobsterin = lobsterin self.lobsterout = lobsterout self.potcar_symbols = potcar_symbols self.charge = charge self.bandoverlaps = bandoverlaps self.lobster_dos = lobster_dos self.vasp_dos = vasp_dos
[docs] @classmethod def from_files( cls, *, poscar: Path, lobsterin: Path, lobsterout: Path, potcar: Path | None = None, potcar_symbols: list[str] | None = None, charge: Path | None = None, bandoverlaps: Path | None = None, doscar: Path | None = None, vasprun: Path | None = None, ) -> LobsterCalcQuality: """ Create LobsterCalcQuality instance from explicit file paths. :param poscar: Path to CONTCAR/POSCAR file :param lobsterin: Path to lobsterin file :param lobsterout: Path to lobsterout file :param potcar: Path to POTCAR file (optional if potcar_symbols or vasprun provided) :param potcar_symbols: List of POTCAR symbols (optional if potcar or vasprun provided) :param charge: Path to CHARGE.lobster file (optional) :param bandoverlaps: Path to bandOverlaps.lobster file (optional) :param doscar: Path to DOSCAR.lobster file (optional) :param vasprun: Path to vasprun.xml file (optional) """ structure = Structure.from_file(poscar) lobsterin_obj = Lobsterin.from_file(lobsterin) lobsterout_obj = Lobsterout(lobsterout) # POTCAR handling (logic unchanged) if potcar: potcar_symbols = Lobsterin._get_potcar_symbols(POTCAR_input=potcar) elif potcar_symbols: pass elif vasprun: vr = Vasprun(vasprun, parse_potcar_file=False, parse_eigen=False) potcar_symbols = [p.split()[1] for p in vr.potcar_symbols] else: raise ValueError( "Provide potcar, potcar_symbols, or vasprun.xml to identify basis set used for projections" ) return cls( structure=structure, lobsterin=lobsterin_obj, lobsterout=lobsterout_obj, potcar_symbols=potcar_symbols, charge=Charge(filename=charge) if charge else None, bandoverlaps=Bandoverlaps(filename=bandoverlaps) if bandoverlaps and Path(bandoverlaps).exists() else None, lobster_dos=Doscar(doscar=doscar, structure_file=None, structure=structure).completedos if doscar else None, vasp_dos=Vasprun(vasprun, parse_potcar_file=False, parse_eigen=False).complete_dos if vasprun else None, )
[docs] @classmethod def from_directory(cls, path_to_lobster_calc: Path | str) -> LobsterCalcQuality: """ Create LobsterCalcQuality instance from a directory containing LOBSTER calculation files. :param path_to_lobster_calc: Path to directory with LOBSTER calculation files """ if not isinstance(path_to_lobster_calc, Path): path_to_lobster_calc = Path(path_to_lobster_calc) return cls.from_files( poscar=zpath(path_to_lobster_calc / "CONTCAR"), lobsterin=zpath(path_to_lobster_calc / "lobsterin"), lobsterout=zpath(path_to_lobster_calc / "lobsterout"), potcar=zpath(path_to_lobster_calc / "POTCAR") if Path(zpath(path_to_lobster_calc / "POTCAR")).exists() else None, charge=zpath(path_to_lobster_calc / "CHARGE.lobster") if Path(zpath(path_to_lobster_calc / "CHARGE.lobster")).exists() else None, bandoverlaps=zpath(path_to_lobster_calc / "bandOverlaps.lobster") if Path(zpath(path_to_lobster_calc / "bandOverlaps.lobster")).exists() else None, doscar=zpath(path_to_lobster_calc / "DOSCAR.LSO.lobster") if Path(zpath(path_to_lobster_calc / "DOSCAR.LSO.lobster")).exists() else ( zpath(path_to_lobster_calc / "DOSCAR.lobster") if Path(zpath(path_to_lobster_calc / "DOSCAR.lobster")).exists() else None ), vasprun=zpath(path_to_lobster_calc / "vasprun.xml") if Path(zpath(path_to_lobster_calc / "vasprun.xml")).exists() else None, )
[docs] def get_calculation_quality_summary( self, *, dos_comparison: bool = False, bva_comp: bool = False, e_range: list[int] = [-5, 0], n_bins: int | None = None, ) -> dict: """ Get a summary of the LOBSTER calculation quality. :param dos_comparison: Whether to include DOS comparison analysis (requires DOS data) :param bva_comp: Whether to include bond valence analysis (requires CHARGE.l :param e_range: Energy range for DOS comparison (default: [-5, 0] eV) :param n_bins: Number of bins for DOS comparison (default: None, uses maximum available bins in the specified energy range) Returns: Dictionary summarizing the calculation quality """ quality = {} quality.update(self._minimal_basis()) quality.update(self._charge_spilling()) quality.update(self._band_overlaps()) if bva_comp: quality.update(self._bva_charge_comparison()) if dos_comparison: quality.update(self._dos_comparison(e_range, n_bins)) return quality
def _minimal_basis(self) -> dict: ref_bases = Lobsterin.get_all_possible_basis_functions( structure=self.structure, potcar_symbols=self.potcar_symbols, ) calc_basis = [" ".join(b.split()[1:]) for b in self.lobsterin["basisfunctions"]] minimal = calc_basis == list(ref_bases[0].values()) if not minimal: warnings.warn( "Consider rerunning the calc with the minimum basis as well. " "Choosing a larger basis set is recommended if charge " "spilling significantly improves.", stacklevel=2, ) return {"minimal_basis": minimal} def _charge_spilling(self) -> dict: lob = self.lobsterout return { "charge_spilling": { "abs_charge_spilling": round(sum(lob.charge_spilling) / 2 * 100, 4), "abs_total_spilling": round(sum(lob.total_spilling) / 2 * 100, 4), } } def _band_overlaps(self) -> dict: if not self.bandoverlaps: return { "band_overlaps_analysis": { "file_exists": False, "limit_maxDeviation": None, "has_good_quality_maxDeviation": True, "max_deviation": None, "percent_kpoints_abv_limit": None, } } total_kpoints = None for line in self.lobsterout.warning_lines: if "k-points could not be orthonormalized" in line: total_kpoints = int(line.split()[2]) dev_above = [d for d in self.bandoverlaps.max_deviation if d > 0.1] assert total_kpoints is not None # mypy type checking fails if not asserted return { "band_overlaps_analysis": { "file_exists": True, "limit_maxDeviation": 0.1, "has_good_quality_maxDeviation": self.bandoverlaps.has_good_quality_maxDeviation(0.1), "max_deviation": round(max(self.bandoverlaps.max_deviation), 4), "percent_kpoints_abv_limit": round(len(dev_above) / total_kpoints * 100, 4), } } def _bva_charge_comparison(self) -> dict: if not self.charge: raise ValueError("BVA comparison requested but CHARGE.lobster not provided") try: bva = BVAnalyzer() bva_oxi = ["POS" if v >= 0 else "NEG" for v in bva.get_valences(self.structure)] mull = ["POS" if v >= 0 else "NEG" for v in self.charge.Mulliken] loew = ["POS" if v >= 0 else "NEG" for v in self.charge.Loewdin] return { "charge_comparisons": { "bva_mulliken_agree": mull == bva_oxi, "bva_loewdin_agree": loew == bva_oxi, } } except ValueError: warnings.warn( "Oxidation states from BVA analyzer cannot be determined. Thus BVA charge comparison will be skipped", stacklevel=2, ) return {"charge_comparisons": {}} def _dos_comparison(self, e_range, n_bins) -> dict: if not self.lobster_dos or not self.vasp_dos: raise ValueError("DOS comparison requested but DOS data missing") dos_lob = self.lobster_dos dos_vasp = self.vasp_dos min_e = int(max(e_range[0], min(dos_vasp.energies), min(dos_lob.energies))) max_e = int(min(e_range[-1], max(dos_vasp.energies), max(dos_lob.energies))) minimum_n_bins = min( len(dos_vasp.energies[(dos_vasp.energies >= min_e) & (dos_vasp.energies <= max_e)]), len(dos_lob.energies[(dos_lob.energies >= min_e) & (dos_lob.energies <= max_e)]), ) if n_bins is None or n_bins > minimum_n_bins: n_bins = minimum_n_bins fp_kwargs = dict(min_e=min_e, max_e=max_e, n_bins=n_bins, normalize=True) dos_comp = {} for orb in dos_lob.get_spd_dos(): tani = dos_vasp.get_dos_fp_similarity( dos_lob.get_dos_fp(fp_type=orb.name, **fp_kwargs), dos_vasp.get_dos_fp(fp_type=orb.name, **fp_kwargs), metric="tanimoto", ) dos_comp[f"tanimoto_orb_{orb.name}"] = round(tani, 4) tani_sum = dos_vasp.get_dos_fp_similarity( dos_lob.get_dos_fp(fp_type="summed_pdos", **fp_kwargs), dos_vasp.get_dos_fp(fp_type="summed_pdos", **fp_kwargs), metric="tanimoto", ) dos_comp.update( { "tanimoto_summed": round(tani_sum, 4), "e_range": [min_e, max_e], "n_bins": n_bins, } ) return {"dos_comparisons": dos_comp}
[docs] @staticmethod def describe(quality_dict: dict) -> list[str]: """ Generate a text description of the LOBSTER calculation quality. :param quality_dict: python dictionary obtained from LobsterCalcQuality.get_calculation_quality_summary method Returns: list of strings describing the calculation quality """ return get_calc_quality_description(quality_dict)
[docs] @staticmethod def print_description(text: list[str]): """ Print the calculation quality description to the screen. :param text: list of strings obtained from LobsterCalcQuality.describe method """ print(" ".join(text))