Source code for lobsterpy.cohp.describe

# Copyright (c) lobsterpy development team
# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License

"""This module defines classes to describe the COHPs automatically."""

from __future__ import annotations

import warnings
from pathlib import Path

from lobsterpy.plotting import InteractiveCohpPlotter, PlainCohpPlotter


[docs] class Description: """ Base class that will write generate a text description for all relevant bonds. It analyses all relevant coordination environments in the system based on electronic structure theory. """ def __init__(self, analysis_object): """ Generate a text description for all relevant bonds. :param analysis_object: Analysis object from lobsterpy.analysis """ self.analysis_object = analysis_object self.set_description()
[docs] def set_description(self): """ Set the descriptions of the structures using the cation names, starting with numbers at 1. Uses the cation names from the lobster files. Returns: None """ self.condensed_bonding_analysis = self.analysis_object.condensed_bonding_analysis # set type of population analyzed type_pop = self.analysis_object._get_pop_type() # set units for populations units = " eV" if type_pop == "COHP" else "" if self.analysis_object.which_bonds == "cation-anion": relevant_cations = ", ".join( [ str(site.specie) + str(isite + 1) for isite, site in enumerate(self.analysis_object.structure) if isite in self.analysis_object.seq_ineq_ions ] ) self.text = [] self.text.append( "The compound " + str(self.condensed_bonding_analysis["formula"]) + " has " + str(self.condensed_bonding_analysis["number_of_considered_ions"]) + " symmetry-independent cation(s) with relevant cation-anion interactions: " + str(relevant_cations) + "." ) for key, item in self.condensed_bonding_analysis["sites"].items(): # It has 3 Ta-N (mean ICOHP: -4.78 eV, antibonding interactions below EFermi), bond_info = [] orb_info = [] for type, properties in item["bonds"].items(): if not properties["has_antibdg_states_below_Efermi"]: bond_info.append( str(properties["number_of_bonds"]) + " " + item["ion"] + "-" + str(type) + f" (mean I{type_pop}: " "" + properties[f"I{type_pop}_mean"] + f"{units}, 0.0 percent antibonding interaction below EFermi)" ) if self.analysis_object.orbital_resolved: text_orbital = self._generate_orbital_resolved_analysis_text( orbital_resolved_data=properties, type_pop=type_pop, atom_name=str(type), ion=item["ion"], ) orb_info.extend(text_orbital) else: bond_info.append( str(properties["number_of_bonds"]) + " " + item["ion"] + "-" + str(type) + f" (mean I{type_pop}: " "" + properties[f"I{type_pop}_mean"] + f"{units}, " + str(round(properties["antibonding"]["perc"] * 100, 3)) + " percent antibonding interaction below EFermi)" ) if self.analysis_object.orbital_resolved: text_orbital = self._generate_orbital_resolved_analysis_text( orbital_resolved_data=properties, type_pop=type_pop, atom_name=str(type), ion=item["ion"], ) orb_info.extend(text_orbital) bonds = ",".join(bond_info[0:-1]) + ", and " + bond_info[-1] if len(bond_info) > 1 else bond_info[0] if len(orb_info) > 1: orb_bonds = "".join(orb_info).replace(".In", ". In") else: orb_bonds = orb_info[0] if orb_info else "" if item["env"] == "O:6": self.text.append( str(item["ion"]) + str(key + 1) + " has an " + str(self._coordination_environment_to_text(item["env"])) + " coordination environment. It has " + str(bonds) + " bonds." ) if orb_bonds: self.text.append(orb_bonds) else: self.text.append( str(item["ion"]) + str(key + 1) + " has a " + str(self._coordination_environment_to_text(item["env"])) + " coordination environment. It has " + str(bonds) + " bonds." ) if orb_bonds: self.text.append(orb_bonds) elif self.analysis_object.which_bonds == "all": relevant_ions = ", ".join( [ str(site.specie) + str(isite + 1) for isite, site in enumerate(self.analysis_object.structure) if isite in self.analysis_object.seq_ineq_ions ] ) self.text = [] self.text.append( "The compound " + str(self.condensed_bonding_analysis["formula"]) + " has " + str(self.condensed_bonding_analysis["number_of_considered_ions"]) + " symmetry-independent atoms(s) with relevant bonds: " + str(relevant_ions) + "." ) for key, item in self.condensed_bonding_analysis["sites"].items(): # It has 3 Ta-N (mean ICOHP: -4.78 eV, antibonding interactions below EFermi), bond_info = [] orb_info = [] for type, properties in item["bonds"].items(): if not properties["has_antibdg_states_below_Efermi"]: bond_info.append( str(properties["number_of_bonds"]) + " " + item["ion"] + "-" + str(type) + f" (mean I{type_pop}: " "" + properties[f"I{type_pop}_mean"] + f"{units}, 0.0 percent antibonding interaction below EFermi)" ) if self.analysis_object.orbital_resolved: text_orbital = self._generate_orbital_resolved_analysis_text( orbital_resolved_data=properties, type_pop=type_pop, atom_name=str(type), ion=item["ion"], ) orb_info.extend(text_orbital) else: bond_info.append( str(properties["number_of_bonds"]) + " " + item["ion"] + "-" + str(type) + f" (mean I{type_pop}: " "" + properties[f"I{type_pop}_mean"] + f"{units}, " + str(round(properties["antibonding"]["perc"] * 100, 3)) + " percent antibonding interaction below EFermi)" ) if self.analysis_object.orbital_resolved: text_orbital = self._generate_orbital_resolved_analysis_text( orbital_resolved_data=properties, type_pop=type_pop, atom_name=str(type), ion=item["ion"], ) orb_info.extend(text_orbital) bonds = ",".join(bond_info[0:-1]) + ", and " + bond_info[-1] if len(bond_info) > 1 else bond_info[0] if len(orb_info) > 1: orb_bonds = "".join(orb_info).replace(".In", ". In") else: orb_bonds = orb_info[0] if orb_info else "" if item["env"] == "O:6": self.text.append( str(item["ion"]) + str(key + 1) + " has an " + str(self._coordination_environment_to_text(item["env"])) + " coordination environment. It has " + str(bonds) + " bonds." ) if orb_bonds: self.text.append(orb_bonds) else: self.text.append( str(item["ion"]) + str(key + 1) + " has a " + str(self._coordination_environment_to_text(item["env"])) + " coordination environment. It has " + str(bonds) + " bonds." ) if orb_bonds: self.text.append(orb_bonds) if ( "madelung_energy" in self.analysis_object.condensed_bonding_analysis and self.analysis_object.condensed_bonding_analysis["madelung_energy"] is not None ): self.text.append( "The Madelung energy of this crystal structure per unit cell is: " + str(self.analysis_object.condensed_bonding_analysis["madelung_energy"]) + " eV." )
def _generate_orbital_resolved_analysis_text( self, orbital_resolved_data: dict, ion: str, atom_name: str, type_pop: str, ): """ Generate text from orbital-resolved analysis data of the most relevant COHP, COOP, or COBI. :param orbital_resolved_data: dict of orbital data from condensed bonding analysis object :param ion: name of ion at the site :param atom_name: name of atomic speice to which ion is bonded :param type_pop: population type analysed could be "COHP" or "COOP" or "COBI" Returns: A python list with text describing the orbital which contributes the most to the bonding and antibonding in the bond at site """ orb_info = [] if orbital_resolved_data["orbital_data"]["orbital_summary_stats"]: orb_names = [] orb_contri = [] # get atom-pair list with ion placed first atom_pair = self.analysis_object._sort_name([ion, atom_name], nameion=ion) if "max_bonding_contribution" in orbital_resolved_data["orbital_data"]["orbital_summary_stats"]: for orb, data in orbital_resolved_data["orbital_data"]["orbital_summary_stats"][ "max_bonding_contribution" ].items(): atom_pair_with_orb_name = self.analysis_object._sort_orbital_atom_pair( atom_pair=atom_pair, complete_cohp=self.analysis_object.chemenv.completecohp, label=orbital_resolved_data["orbital_data"]["relevant_bonds"][0], orb_pair=orb, ) orb_names.append("-".join(atom_pair_with_orb_name)) orb_contri.append( str( round( data * 100, 3, ) ) ) orb_names_anti = [] orb_antibonding = [] if "max_antibonding_contribution" in orbital_resolved_data["orbital_data"]["orbital_summary_stats"]: for orb, data in orbital_resolved_data["orbital_data"]["orbital_summary_stats"][ "max_antibonding_contribution" ].items(): atom_pair_with_orb_name = self.analysis_object._sort_orbital_atom_pair( atom_pair=atom_pair, complete_cohp=self.analysis_object.chemenv.completecohp, label=orbital_resolved_data["orbital_data"]["relevant_bonds"][0], orb_pair=orb, ) orb_names_anti.append("-".join(atom_pair_with_orb_name)) orb_antibonding.append( str( round( data * 100, 3, ) ) ) if len(orb_contri) > 1: orb_name_contri = "" for inx, name in enumerate(orb_names): if len(orb_contri) == 2 and inx + 1 != len(orb_contri): orb_name_contri += f"{name} " elif 2 < len(orb_contri) != inx + 1: orb_name_contri += f"{name}, " else: orb_name_contri += f"and {name}" orb_name_contri += " orbitals, contributing " for inx, contribution in enumerate(orb_contri): if len(orb_contri) == 2 and inx + 1 != len(orb_contri): orb_name_contri += f"{contribution} " elif 2 < len(orb_contri) != inx + 1: orb_name_contri += f"{contribution}, " else: orb_name_contri += f"and {contribution} percent, respectively" num_bonds = len(orbital_resolved_data["orbital_data"]["relevant_bonds"]) bonds = "bonds" if num_bonds > 1 else "bond" orb_info.append( f"In the {num_bonds} " + "-".join(atom_pair) + f" {bonds}, relative to the summed I{type_pop}s, " + "the maximum bonding contribution is from " + orb_name_contri ) elif not orb_contri: num_bonds = len(orbital_resolved_data["orbital_data"]["relevant_bonds"]) bonds = "bonds" if num_bonds > 1 else "bond" orb_info.append( f"In the {num_bonds} " + "-".join(atom_pair) + f" {bonds}, relative to the summed I{type_pop}s, " + f"no orbital has a bonding contribution greater than " f"{self.analysis_object.orbital_cutoff*100} percent" ) else: num_bonds = len(orbital_resolved_data["orbital_data"]["relevant_bonds"]) bonds = "bonds" if num_bonds > 1 else "bond" orb_info.append( f"In the {num_bonds} " + "-".join(atom_pair) + f" {bonds}, relative to the summed I{type_pop}s, " + "the maximum bonding contribution is from the " + f"{orb_names[0]}" + f" orbital, contributing {orb_contri[0]} percent" ) if len(orb_antibonding) > 1: orb_anti = "" for inx, name in enumerate(orb_names_anti): if len(orb_names_anti) == 2 and inx + 1 != len(orb_names_anti): orb_anti += f"{name} " elif 2 < len(orb_antibonding) != inx + 1: orb_anti += f"{name}, " else: orb_anti += f"and {name}" orb_anti += " orbitals, contributing " for inx, contribution in enumerate(orb_antibonding): if len(orb_names_anti) == 2 and inx + 1 != len(orb_names_anti): orb_anti += f"{contribution} " elif 2 < len(orb_antibonding) != inx + 1: orb_anti += f"{contribution}, " else: orb_anti += f"and {contribution} percent, respectively." orb_info.append(f", whereas the maximum antibonding contribution is from {orb_anti}") elif not orb_antibonding: orb_info.append(", whereas no significant antibonding contribution is found in this bond.") else: orb_info.append( f", whereas the maximum antibonding contribution is from the " f"{orb_names_anti[0]} orbital, contributing {orb_antibonding[0]} percent." ) else: # get atom-pair list with ion placed first atom_pair = self.analysis_object._sort_name([ion, atom_name], nameion=ion) percentage_cutoff = round(self.analysis_object.orbital_cutoff * 100, 2) orb_info.append( f"No individual orbital interactions detected above {percentage_cutoff} percent" f" with summed I{type_pop} as reference for the " + "-".join(atom_pair) + " bond." ) return orb_info
[docs] def plot_cohps( self, xlim: list[float] | None = None, ylim: list[float] | None = [-4, 2], integrated: bool = False, title: str = "", save: bool = False, filename: str | None = None, sigma: float | None = None, hide: bool = False, ): """ Automatically generate plots of the most relevant COHPs, COOPs, or COBIs. :param save: will save the plot to a file :param filename: name of the file to save the plot. :param ylim: energy scale that is shown in plot (eV) :param xlim: energy range for COHPs in eV :param integrated: if True, integrated COHPs will be shown :param sigma: Standard deviation of Gaussian broadening applied to population data. If None, no broadening will be added. :param title: sets the title of figure generated :param hide: if True, the plot will not be shown. Returns: A matplotlib object. """ seq_cohps = self.analysis_object.seq_cohps if self.analysis_object.which_bonds == "cation-anion": seq_ineq_cations = self.analysis_object.seq_ineq_ions elif self.analysis_object.which_bonds == "all": seq_ineq_cations = self.analysis_object.seq_ineq_ions seq_labels = self.analysis_object.seq_labels_cohps structure = self.analysis_object.structure if len(seq_ineq_cations) >= 20: warnings.warn( "We will switch of displaying all plots " "as there are more than 20 inequivalent ions. " "We will instead save them in files called " "'automatic-analysis-*.png'." ) hide = True save = True if filename is None: filename = "./automatic_analysis.png" for iplot, (ication, labels, cohps) in enumerate(zip(seq_ineq_cations, seq_labels, seq_cohps)): namecation = str(structure[ication].specie) cp = PlainCohpPlotter( are_coops=self.analysis_object.are_coops, are_cobis=self.analysis_object.are_cobis, ) for label, cohp in zip(labels, cohps): if label is not None: cp.add_cohp(namecation + str(ication + 1) + ": " + label, cohp) plot = cp.get_plot(integrated=integrated, sigma=sigma) plot.ylim(ylim) if xlim is not None: plot.xlim(xlim) plot.title(title) if save: if len(seq_ineq_cations) > 1: if isinstance(filename, str): filename = Path(filename) # type: ignore filename_new = ( filename.parent / f"{filename.stem}-{iplot}{filename.suffix}" # type: ignore ) else: filename_new = filename plot.savefig(filename_new) if hide: plot.close() if not hide: plot.show()
[docs] def plot_interactive_cohps( self, ylim: list[float] | None = None, xlim: list[float] | None = None, save_as_html: bool = False, filename: str | None = None, integrated: bool = False, title: str = "", sigma: float | None = None, label_resolved: bool = False, orbital_resolved: bool = False, hide: bool = False, ): """ Automatically generate interactive plots of the most relevant COHPs, COBIs or COOPs. :param save_as_html: will save the plot to a html file :param filename: name of the file to save the plot. :param ylim: energy scale that is shown in plot (eV) :param xlim: energy range for COHPs in eV :param integrated: if True, integrated COHPs will be shown :param sigma: Standard deviation of Gaussian broadening applied to population data. If None, no broadening will be added. :param label_resolved: if true, relevant cohp curves will be further resolved based on band labels :param orbital_resolved: if true, relevant orbital interactions in cohp curves will be added to figure :param title: Title of the interactive plot :param hide: if True, the plot will not be shown. Returns: A plotly.graph_objects.Figure object. """ cba_cohp_plot_data = {} # Initialize dict to store plot data set_cohps = self.analysis_object.seq_cohps set_labels_cohps = self.analysis_object.seq_labels_cohps set_inequivalent_cations = self.analysis_object.seq_ineq_ions structure = self.analysis_object.structure for _iplot, (ication, labels, cohps) in enumerate(zip(set_inequivalent_cations, set_labels_cohps, set_cohps)): label_str = f"{structure[ication].specie!s}{ication + 1!s}: " for label, cohp in zip(labels, cohps): if label is not None: cba_cohp_plot_data[label_str + label] = cohp ip = InteractiveCohpPlotter( are_coops=self.analysis_object.are_coops, are_cobis=self.analysis_object.are_cobis, ) if label_resolved or orbital_resolved: ip.add_all_relevant_cohps( analyse=self.analysis_object, label_resolved=label_resolved, orbital_resolved=orbital_resolved, ) else: ip.add_cohps_from_plot_data(plot_data_dict=cba_cohp_plot_data) plot = ip.get_plot(integrated=integrated, xlim=xlim, ylim=ylim, sigma=sigma) plot.update_layout(title_text=title) if save_as_html: plot.write_html(filename, include_mathjax="cdn") if not hide: return plot.show() return plot
@staticmethod def _coordination_environment_to_text(ce: str): """ Convert a coordination environment string into a text description of the environment. :param ce: output from ChemEnv package (e.g., "O:6") Returns: A text description of coordination environment """ if ce == "S:1": return "single (CN=1)" if ce == "L:2": return "linear (CN=2)" if ce == "A:2": return "angular (CN=2)" if ce == "TL:3": return "trigonal planar (CN=3)" if ce == "TY:3": return "triangular non-coplanar (CN=3)" if ce == "TS:3": return "t-shaped (CN=3)" if ce == "T:4": return "tetrahedral (CN=4)" if ce == "S:4": return "square planar (CN=4)" if ce == "SY:4": return "square non-coplanar (CN=4)" if ce == "SS:4": return "see-saw like (CN=4)" if ce == "PP:5": return "pentagonal (CN=5)" if ce == "S:5": return "square pyramidal (CN=5)" if ce == "T:5": return "trigonal bipyramidal (CN=5)" if ce == "O:6": return "octahedral (CN=6)" if ce == "T:6": return "trigonal prismatic (CN=6)" if ce == "PP:6": return "pentagonal pyramidal (CN=6)" if ce == "PB:7": return "pentagonal bipyramidal (CN=7)" if ce == "ST:7": return "square-face capped trigonal prismatic (CN=7)" if ce == "ET:7": return "end-trigonal-face capped trigonal prismatic (CN=7)" if ce == "FO:7": return "face-capped octahedron (CN=7)" if ce == "C:8": return "cubic (CN=8)" if ce == "SA:8": return "square antiprismatic (CN=8)" if ce == "SBT:8": return "square-face bicapped trigonal prismatic (CN=8)" if ce == "TBT:8": return "triangular-face bicapped trigonal prismatic (CN=8)" if ce == "DD:8": return "dodecahedronal (with triangular faces) (CN=8)" if ce == "DDPN:8": return "dodecahedronal (with triangular faces - p2345 plane normalized) (CN=8)" if ce == "HB:8": return "hexagonal bipyramidal (CN=8)" if ce == "BO_1:8": return "bicapped octahedral (opposed cap faces) (CN=8)" if ce == "BO_2:8": return "bicapped octahedral (cap faces with one atom in common) (CN=8)" if ce == "BO_3:8": return "bicapped octahedral (cap faces with one edge in common) (CN=8)" if ce == "TC:9": return "triangular cupola (CN=9)" if ce == "TT_1:9": return "Tricapped triangular prismatic (three square - face caps) (CN=9)" if ce == "TT_2:9": return "Tricapped triangular prismatic (two square - face caps and one triangular - face cap) (CN=9)" if ce == "TT_3:9": return "Tricapped triangular prism (one square - face cap and two triangular - face caps) (CN=9)" if ce == "HD:9": return "Heptagonal dipyramidal (CN=9)" if ce == "TI:9": return "tridiminished icosohedral (CN=9)" if ce == "SMA:9": return "Square-face monocapped antiprism (CN=9)" if ce == "SS:9": return "Square-face capped square prismatic (CN=9)" if ce == "TO_1:9": return "Tricapped octahedral (all 3 cap faces share one atom) (CN=9)" if ce == "TO_2:9": return "Tricapped octahedral (cap faces are aligned) (CN=9)" if ce == "TO_3:9": return "Tricapped octahedron (all 3 cap faces are sharing one edge of a face) (CN=9)" if ce == "PP:10": return "Pentagonal prismatic (CN=10)" if ce == "PA:10": return "Pentagonal antiprismatic (CN=10)" if ce == "SBSA:10": return "Square-face bicapped square antiprismatic (CN=10)" if ce == "MI:10": return "Metabidiminished icosahedral (CN=10)" if ce == "S:10": return "sphenocoronal (CN=10)" if ce == "H:10": return "Hexadecahedral (CN=10)" if ce == "BS_1:10": return "Bicapped square prismatic (opposite faces) (CN=10)" if ce == "BS_1:10": return "Bicapped square prismatic (opposite faces) (CN=10)" if ce == "BS_2:10": return "Bicapped square prism(adjacent faces) (CN=10)" if ce == "TBSA:10": return "Trigonal-face bicapped square antiprismatic (CN=10)" if ce == "PCPA:11": return "Pentagonal - face capped pentagonal antiprismatic (CN=11)" if ce == "H:11": return "Hendecahedral (CN=11)" if ce == "SH:11": return "Sphenoid hendecahedral (CN=11)" if ce == "CO:11": return "Cs - octahedral (CN=11)" if ce == "DI:11": return "Diminished icosahedral (CN=12)" if ce == "I:12": return "Icosahedral (CN=12)" if ce == "PBP: 12": return "Pentagonal - face bicapped pentagonal prismatic (CN=12)" if ce == "TT:12": return "Truncated tetrahedral (CN=12)" if ce == "C:12": return "Cuboctahedral (CN=12)" if ce == "AC:12": return "Anticuboctahedral (CN=12)" if ce == "SC:12": return "Square cupola (CN=12)" if ce == "S:12": return "Sphenomegacorona (CN=12)" if ce == "HP:12": return "Hexagonal prismatic (CN=12)" if ce == "HA:12": return "Hexagonal antiprismatic (CN=12)" if ce == "SH:13": return "Square-face capped hexagonal prismatic (CN=13)" if ce == "1": return "1-fold" if ce == "2": return "2-fold" if ce == "3": return "3-fold" if ce == "4": return "4-fold" if ce == "5": return "5-fold" if ce == "6": return "6-fold" if ce == "7": return "7-fold" if ce == "8": return "8-fold" if ce == "9": return "9-fold" if ce == "10": return "10-fold" if ce == "11": return "11-fold" if ce == "12": return "12-fold" if ce == "13": return "13-fold" if ce == "14": return "14-fold" if ce == "15": return "15-fold" if ce == "16": return "16-fold" if ce == "17": return "17-fold" if ce == "18": return "18-fold" if ce == "19": return "19-fold" if ce == "20": return "20-fold" if ce == "21": return "21-fold" if ce == "22": return "22-fold" if ce == "23": return "23-fold" if ce == "24": return "24-fold" if ce == "25": return "25-fold" if ce == "26": return "26-fold" if ce == "27": return "27-fold" if ce == "28": return "28-fold" if ce == "29": return "29-fold" if ce == "30": return "30-fold" return ce
[docs] def write_description(self): """Print the description of the COHPs or COBIs or COOPs to the screen.""" for textpart in self.text: print(textpart)
[docs] @staticmethod def get_calc_quality_description(quality_dict): """ Generate a text description of the LOBSTER calculation quality. :param quality_dict: python dictionary from lobsterpy.analysis.get_lobster_calc_quality_summary """ text_des = [] for key, val in quality_dict.items(): if key == "minimal_basis": if val: text_des.append("The LOBSTER calculation used minimal basis.") if not val: text_des.append( "Consider rerunning the calculation with the minimum basis as well. Choosing a " "larger basis set is only recommended if you see a significant improvement of " "the charge spilling." ) elif key == "charge_spilling": text_des.append( "The absolute and total charge spilling for the calculation is {} and {} %, " "respectively.".format( quality_dict[key]["abs_charge_spilling"], quality_dict[key]["abs_total_spilling"], ) ) elif key == "band_overlaps_analysis": if quality_dict[key]["file_exists"]: if quality_dict[key]["has_good_quality_maxDeviation"]: text_des.append( "The bandOverlaps.lobster file is generated during the LOBSTER run. This " "indicates that the projected wave function is not completely orthonormalized; " "however, the maximal deviation values observed compared to the identity matrix " "is below the threshold of 0.1." ) else: text_des.append( "The bandOverlaps.lobster file is generated during the LOBSTER run. This " "indicates that the projected wave function is not completely orthonormalized. " "The maximal deviation value from the identity matrix is {}, and there are " "{} percent k-points above the deviation threshold of 0.1. Please check the " "results of other quality checks like dos comparisons, charges, " "charge spillings before using the results for further " "analysis.".format( quality_dict[key]["max_deviation"], quality_dict[key]["percent_kpoints_abv_limit"], ) ) else: text_des.append( "The projected wave function is completely orthonormalized as no " "bandOverlaps.lobster file is generated during the LOBSTER run." ) elif key == "charge_comparisons": if val: for charge in ["mulliken", "loewdin"]: if val[f"bva_{charge}_agree"]: text_des.append( f"The atomic charge signs from {charge.capitalize()} population analysis " f"agree with the bond valence analysis." ) if not val[f"bva_{charge}_agree"]: text_des.append( f"The atomic charge signs from {charge.capitalize()} population analysis " f"do not agree with the bond valence analysis." ) else: text_des.append( "Oxidation states from BVA analyzer cannot be determined. " "Thus BVA charge comparison is not conducted." ) elif key == "dos_comparisons": comp_types = [] tani_index = [] for orb in val: if orb.split("_")[-1] in ["s", "p", "d", "f", "summed"]: comp_types.append(orb.split("_")[-1]) tani_index.append(str(val[orb])) text_des.append( "The Tanimoto index from DOS comparisons in the energy range between {}, {} eV " "for {} orbitals are: {}.".format( val["e_range"][0], val["e_range"][1], ", ".join(comp_types), ", ".join(tani_index), ) ) return text_des
[docs] @staticmethod def write_calc_quality_description(calc_quality_text): """Print the calculation quality description to the screen.""" print(" ".join(calc_quality_text))